From 81da7d2531f19a6b443b05b627f9d692f7d71546 Mon Sep 17 00:00:00 2001 From: bofeng huang Date: Wed, 15 May 2024 02:10:37 +0200 Subject: [PATCH 0001/1405] Fix `total_num_steps` (#1566) * Fix `total_num_steps` * Fix total_num_steps * lint --- src/axolotl/utils/trainer.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 2e3728cc8a..fdf86e5672 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -330,7 +330,6 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): / cfg.sample_packing_eff_est / cfg.sequence_len // cfg.batch_size - // int(os.environ.get("WORLD_SIZE", 1)) ) - 1 ) @@ -359,18 +358,14 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): train_dataset.remove_columns(["length"]), batch_sampler=sampler, ) - data_loader_len = len(data_loader) // cfg.batch_size + data_loader_len = len(data_loader) // ( + cfg.world_size * cfg.gradient_accumulation_steps + ) actual_eff = sampler.efficiency() LOG.debug(f"data_loader_len: {data_loader_len}", main_process_only=True) # FIXME: is there a bug here somewhere? the total num steps depends # on the agreed on value for sample_packing_eff_est - total_num_steps = int( - math.floor( - data_loader_len - * cfg.num_epochs - / int(os.environ.get("WORLD_SIZE", 1)) - ) - ) + total_num_steps = int(math.floor(data_loader_len * cfg.num_epochs)) def calc_sample_packing_eff_est(estimates: List[float]): LOG.info(f"sample_packing_eff_est across ranks: {repr(estimates)}") @@ -391,12 +386,7 @@ def calc_sample_packing_eff_est(estimates: List[float]): ) else: total_num_steps = int( - math.ceil( - len(train_dataset) - * cfg.num_epochs - / int(os.environ.get("WORLD_SIZE", 1)) - / cfg.batch_size - ) + math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) ) LOG.debug(f"total_num_steps: {total_num_steps}", main_process_only=True) return total_num_steps From 3319780300b1cb9fa361b542f12b5c42f088c3f3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 15 May 2024 09:45:27 -0400 Subject: [PATCH 0002/1405] update torch 2.2.1 -> 2.2.2 (#1622) --- .github/workflows/base.yml | 2 +- .github/workflows/main.yml | 4 ++-- .github/workflows/nightlies.yml | 4 ++-- .github/workflows/tests.yml | 2 +- README.md | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 2e8db61cc7..4c1b0463a7 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -30,7 +30,7 @@ jobs: - cuda: "121" cuda_version: 12.1.0 python_version: "3.11" - pytorch: 2.2.1 + pytorch: 2.2.2 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "121" cuda_version: 12.1.0 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f51c42a823..99190dc717 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: - cuda: 121 cuda_version: 12.1.0 python_version: "3.11" - pytorch: 2.2.1 + pytorch: 2.2.2 axolotl_extras: - cuda: 121 cuda_version: 12.1.0 @@ -89,7 +89,7 @@ jobs: - cuda: 121 cuda_version: 12.1.0 python_version: "3.11" - pytorch: 2.2.1 + pytorch: 2.2.2 axolotl_extras: - cuda: 121 cuda_version: 12.1.0 diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index f4172fa406..f668e5f65b 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -27,7 +27,7 @@ jobs: - cuda: 121 cuda_version: 12.1.0 python_version: "3.11" - pytorch: 2.2.1 + pytorch: 2.2.2 axolotl_extras: - cuda: 121 cuda_version: 12.1.0 @@ -89,7 +89,7 @@ jobs: - cuda: 121 cuda_version: 12.1.0 python_version: "3.11" - pytorch: 2.2.1 + pytorch: 2.2.2 axolotl_extras: - cuda: 121 cuda_version: 12.1.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a53640e0b0..8f25eddc31 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -82,7 +82,7 @@ jobs: - cuda: 121 cuda_version: 12.1.0 python_version: "3.11" - pytorch: 2.2.1 + pytorch: 2.2.2 num_gpus: 1 steps: - name: Checkout diff --git a/README.md b/README.md index 0b45bb78b4..23ac443eed 100644 --- a/README.md +++ b/README.md @@ -302,7 +302,7 @@ Write a job description in YAML as below: # dstack.yaml type: task -image: winglian/axolotl-cloud:main-20240429-py3.11-cu121-2.2.1 +image: winglian/axolotl-cloud:main-20240429-py3.11-cu121-2.2.2 env: - HUGGING_FACE_HUB_TOKEN From 4fde300e5fa107529e39b64748ef0fbca5edc32b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 15 May 2024 12:44:13 -0400 Subject: [PATCH 0003/1405] update outputs path so that we can mount workspace to /workspace/data (#1623) * update outputs path so that we can mount workspace to /workspace/data * fix ln order --- docker/Dockerfile-cloud | 4 +++- examples/cerebras/btlm-ft.yml | 2 +- examples/cerebras/qlora.yml | 2 +- examples/code-llama/13b/lora.yml | 2 +- examples/code-llama/13b/qlora.yml | 2 +- examples/code-llama/34b/lora.yml | 2 +- examples/code-llama/34b/qlora.yml | 2 +- examples/code-llama/7b/lora.yml | 2 +- examples/code-llama/7b/qlora.yml | 2 +- examples/colab-notebooks/colab-axolotl-example.ipynb | 2 +- examples/dbrx/16bit-lora.yaml | 2 +- examples/dbrx/8bit-lora.yaml | 2 +- examples/dbrx/fft-ds-zero3.yaml | 2 +- examples/falcon/config-7b-lora.yml | 2 +- examples/falcon/config-7b-qlora.yml | 2 +- examples/falcon/config-7b.yml | 2 +- examples/gemma/qlora.yml | 2 +- examples/gptj/qlora.yml | 2 +- examples/jamba/qlora.yaml | 2 +- examples/jamba/qlora_deepspeed.yaml | 2 +- examples/jeopardy-bot/config.yml | 2 +- examples/llama-2/fft_optimized.yml | 2 +- examples/llama-2/gptq-lora.yml | 2 +- examples/llama-2/lisa.yml | 2 +- examples/llama-2/loftq.yml | 2 +- examples/llama-2/lora.yml | 2 +- examples/llama-2/qlora-fsdp.yml | 2 +- examples/llama-2/qlora.yml | 2 +- examples/llama-2/relora.yml | 2 +- examples/llama-3/fft-8b.yaml | 2 +- examples/llama-3/lora-8b.yml | 2 +- examples/llama-3/qlora-fsdp-70b.yaml | 2 +- examples/llama-3/qlora.yml | 2 +- examples/mamba/config.yml | 2 +- examples/mistral/bigstral-ds-zero3.yaml | 2 +- examples/mistral/config.yml | 2 +- examples/mistral/lora-mps.yml | 2 +- examples/mistral/lora.yml | 2 +- examples/mistral/mistral-qlora-fsdp.yml | 2 +- examples/mistral/mistral-qlora-orpo.yml | 2 +- examples/mistral/mixtral-8x22b-qlora-fsdp.yml | 2 +- examples/mistral/mixtral-qlora-fsdp.yml | 2 +- examples/mistral/mixtral.yml | 2 +- examples/mistral/mixtral_22.yml | 2 +- examples/mistral/qlora.yml | 2 +- examples/mpt-7b/config.yml | 2 +- examples/openllama-3b/config.yml | 2 +- examples/openllama-3b/lora.yml | 2 +- examples/openllama-3b/qlora.yml | 2 +- examples/phi/phi-ft.yml | 2 +- examples/phi/phi-qlora.yml | 2 +- examples/phi/phi2-ft.yml | 2 +- examples/pythia-12b/config.yml | 2 +- examples/pythia/lora.yml | 2 +- examples/qwen/lora.yml | 2 +- examples/qwen/qlora.yml | 2 +- examples/qwen/qwen2-moe-lora.yaml | 2 +- examples/qwen/qwen2-moe-qlora.yaml | 2 +- examples/redpajama/config-3b.yml | 2 +- examples/replit-3b/config-lora.yml | 2 +- examples/stablelm-2/1.6b/fft.yml | 2 +- examples/stablelm-2/1.6b/lora.yml | 2 +- examples/starcoder2/qlora.yml | 2 +- examples/tiny-llama/lora-mps.yml | 2 +- examples/tiny-llama/lora.yml | 2 +- examples/tiny-llama/pretrain.yml | 2 +- examples/tiny-llama/qlora.yml | 2 +- examples/xgen-7b/xgen-7b-8k-qlora.yml | 2 +- examples/yi-34B-chat/qlora.yml | 2 +- outputs/.gitignore | 1 + 70 files changed, 72 insertions(+), 69 deletions(-) create mode 100644 outputs/.gitignore diff --git a/docker/Dockerfile-cloud b/docker/Dockerfile-cloud index 69ce143bb2..cc8c58415b 100644 --- a/docker/Dockerfile-cloud +++ b/docker/Dockerfile-cloud @@ -21,7 +21,9 @@ RUN apt install --yes --no-install-recommends openssh-server tmux && \ printf "\n[[ -z \"\$TMUX\" ]] && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> ~/.bashrc && \ printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> ~/.bashrc && \ chmod +x /workspace/axolotl/scripts/cloud-entrypoint.sh && \ - chmod +x /root/cloud-entrypoint.sh + chmod +x /root/cloud-entrypoint.sh && \ + mkdir -p /workspace/data/axolotl-artifacts && \ + ln -sf /workspace/data/axolotl-artifacts /workspace/axolotl/outputs ENTRYPOINT ["/root/cloud-entrypoint.sh"] CMD ["sleep", "infinity"] diff --git a/examples/cerebras/btlm-ft.yml b/examples/cerebras/btlm-ft.yml index 18dd86e6b4..ba4e65daae 100644 --- a/examples/cerebras/btlm-ft.yml +++ b/examples/cerebras/btlm-ft.yml @@ -38,7 +38,7 @@ wandb_watch: wandb_name: wandb_log_model: -output_dir: btlm-out +output_dir: ./outputs/btlm-out gradient_accumulation_steps: 1 micro_batch_size: 1 num_epochs: 1 diff --git a/examples/cerebras/qlora.yml b/examples/cerebras/qlora.yml index c4f44326c2..285607a4c8 100644 --- a/examples/cerebras/qlora.yml +++ b/examples/cerebras/qlora.yml @@ -25,7 +25,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out batch_size: 4 micro_batch_size: 4 num_epochs: 2 diff --git a/examples/code-llama/13b/lora.yml b/examples/code-llama/13b/lora.yml index ce5a892d08..0ba96cfaa7 100644 --- a/examples/code-llama/13b/lora.yml +++ b/examples/code-llama/13b/lora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./lora-out +output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true diff --git a/examples/code-llama/13b/qlora.yml b/examples/code-llama/13b/qlora.yml index d822e68470..787862d010 100644 --- a/examples/code-llama/13b/qlora.yml +++ b/examples/code-llama/13b/qlora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out adapter: qlora lora_model_dir: diff --git a/examples/code-llama/34b/lora.yml b/examples/code-llama/34b/lora.yml index dfef2538b0..92d4c544a3 100644 --- a/examples/code-llama/34b/lora.yml +++ b/examples/code-llama/34b/lora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./lora-out +output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true diff --git a/examples/code-llama/34b/qlora.yml b/examples/code-llama/34b/qlora.yml index 77f821e1c8..93a6de8777 100644 --- a/examples/code-llama/34b/qlora.yml +++ b/examples/code-llama/34b/qlora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out adapter: qlora lora_model_dir: diff --git a/examples/code-llama/7b/lora.yml b/examples/code-llama/7b/lora.yml index 3e6c7fe620..d13f505325 100644 --- a/examples/code-llama/7b/lora.yml +++ b/examples/code-llama/7b/lora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./lora-out +output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true diff --git a/examples/code-llama/7b/qlora.yml b/examples/code-llama/7b/qlora.yml index e817b113cc..a1026a982d 100644 --- a/examples/code-llama/7b/qlora.yml +++ b/examples/code-llama/7b/qlora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out adapter: qlora lora_model_dir: diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 9adbe00047..fc3b761949 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -84,7 +84,7 @@ " type: alpaca\n", "dataset_prepared_path:\n", "val_set_size: 0.05\n", - "output_dir: ./qlora-out\n", + "output_dir: ./outputs/qlora-out\n", "\n", "adapter: qlora\n", "lora_model_dir:\n", diff --git a/examples/dbrx/16bit-lora.yaml b/examples/dbrx/16bit-lora.yaml index e5e3ea9216..32b625ac69 100644 --- a/examples/dbrx/16bit-lora.yaml +++ b/examples/dbrx/16bit-lora.yaml @@ -10,7 +10,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.0 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 512 sample_packing: false diff --git a/examples/dbrx/8bit-lora.yaml b/examples/dbrx/8bit-lora.yaml index 89e24db058..50ee0a0164 100644 --- a/examples/dbrx/8bit-lora.yaml +++ b/examples/dbrx/8bit-lora.yaml @@ -10,7 +10,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.0 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 512 sample_packing: false diff --git a/examples/dbrx/fft-ds-zero3.yaml b/examples/dbrx/fft-ds-zero3.yaml index 68292707a4..60dc201eee 100644 --- a/examples/dbrx/fft-ds-zero3.yaml +++ b/examples/dbrx/fft-ds-zero3.yaml @@ -10,7 +10,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.0 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 512 sample_packing: false diff --git a/examples/falcon/config-7b-lora.yml b/examples/falcon/config-7b-lora.yml index 5be9c64253..029ca40e09 100644 --- a/examples/falcon/config-7b-lora.yml +++ b/examples/falcon/config-7b-lora.yml @@ -28,7 +28,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./falcon-7b +output_dir: ./outputs/falcon-7b batch_size: 2 micro_batch_size: 1 num_epochs: 4 diff --git a/examples/falcon/config-7b-qlora.yml b/examples/falcon/config-7b-qlora.yml index eb1cdfcdba..4e34144ed6 100644 --- a/examples/falcon/config-7b-qlora.yml +++ b/examples/falcon/config-7b-qlora.yml @@ -42,7 +42,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out # QLoRA paper Table 9 # - 16 for 7b & 13b diff --git a/examples/falcon/config-7b.yml b/examples/falcon/config-7b.yml index 1dd46a93ff..36264f063e 100644 --- a/examples/falcon/config-7b.yml +++ b/examples/falcon/config-7b.yml @@ -28,7 +28,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./falcon-7b +output_dir: ./outputs/falcon-7b batch_size: 2 micro_batch_size: 1 num_epochs: 4 diff --git a/examples/gemma/qlora.yml b/examples/gemma/qlora.yml index 619a401291..e08facfc5d 100644 --- a/examples/gemma/qlora.yml +++ b/examples/gemma/qlora.yml @@ -12,7 +12,7 @@ datasets: - path: mhenrichsen/alpaca_2k_test type: alpaca val_set_size: 0.1 -output_dir: ./out +output_dir: ./outputs/out adapter: qlora lora_r: 32 diff --git a/examples/gptj/qlora.yml b/examples/gptj/qlora.yml index cd3f2e2ad7..f801729fac 100644 --- a/examples/gptj/qlora.yml +++ b/examples/gptj/qlora.yml @@ -23,7 +23,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out gradient_accumulation_steps: 2 micro_batch_size: 2 num_epochs: 2 diff --git a/examples/jamba/qlora.yaml b/examples/jamba/qlora.yaml index 41a3854fe1..3d6f69e793 100644 --- a/examples/jamba/qlora.yaml +++ b/examples/jamba/qlora.yaml @@ -10,7 +10,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.0 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 4096 sample_packing: false diff --git a/examples/jamba/qlora_deepspeed.yaml b/examples/jamba/qlora_deepspeed.yaml index ef04fb53fe..43a76c00b1 100644 --- a/examples/jamba/qlora_deepspeed.yaml +++ b/examples/jamba/qlora_deepspeed.yaml @@ -10,7 +10,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.0 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 4096 sample_packing: false diff --git a/examples/jeopardy-bot/config.yml b/examples/jeopardy-bot/config.yml index a672c7b94f..088629c084 100644 --- a/examples/jeopardy-bot/config.yml +++ b/examples/jeopardy-bot/config.yml @@ -21,7 +21,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./jeopardy-bot-7b +output_dir: ./outputs/jeopardy-bot-7b gradient_accumulation_steps: 1 micro_batch_size: 1 num_epochs: 4 diff --git a/examples/llama-2/fft_optimized.yml b/examples/llama-2/fft_optimized.yml index 74edc95e6b..3d94b04b8b 100644 --- a/examples/llama-2/fft_optimized.yml +++ b/examples/llama-2/fft_optimized.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.05 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 4096 sample_packing: true diff --git a/examples/llama-2/gptq-lora.yml b/examples/llama-2/gptq-lora.yml index 68ca9ed31c..2a706265bd 100644 --- a/examples/llama-2/gptq-lora.yml +++ b/examples/llama-2/gptq-lora.yml @@ -33,7 +33,7 @@ wandb_project: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./model-out +output_dir: ./outputs/model-out gradient_accumulation_steps: 1 micro_batch_size: 1 num_epochs: 4 diff --git a/examples/llama-2/lisa.yml b/examples/llama-2/lisa.yml index e692c7ac1e..7012d1f613 100644 --- a/examples/llama-2/lisa.yml +++ b/examples/llama-2/lisa.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.05 -output_dir: ./lisa-out +output_dir: ./outputs/lisa-out sequence_len: 4096 sample_packing: true diff --git a/examples/llama-2/loftq.yml b/examples/llama-2/loftq.yml index 4529a912dc..68d9ac0142 100644 --- a/examples/llama-2/loftq.yml +++ b/examples/llama-2/loftq.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./lora-out +output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true diff --git a/examples/llama-2/lora.yml b/examples/llama-2/lora.yml index a7793dce4c..95bfae6920 100644 --- a/examples/llama-2/lora.yml +++ b/examples/llama-2/lora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./lora-out +output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true diff --git a/examples/llama-2/qlora-fsdp.yml b/examples/llama-2/qlora-fsdp.yml index 93b3b2a60a..88029f92d5 100644 --- a/examples/llama-2/qlora-fsdp.yml +++ b/examples/llama-2/qlora-fsdp.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.05 -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out adapter: qlora lora_model_dir: diff --git a/examples/llama-2/qlora.yml b/examples/llama-2/qlora.yml index 834dbfb33a..dda32170bd 100644 --- a/examples/llama-2/qlora.yml +++ b/examples/llama-2/qlora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out adapter: qlora lora_model_dir: diff --git a/examples/llama-2/relora.yml b/examples/llama-2/relora.yml index 9fd19953c6..93247ce068 100644 --- a/examples/llama-2/relora.yml +++ b/examples/llama-2/relora.yml @@ -12,7 +12,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./relora-out +output_dir: ./outputs/relora-out adapter: qlora lora_model_dir: diff --git a/examples/llama-3/fft-8b.yaml b/examples/llama-3/fft-8b.yaml index 8c9ba90bfe..a36fd740e4 100644 --- a/examples/llama-3/fft-8b.yaml +++ b/examples/llama-3/fft-8b.yaml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.05 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 8192 sample_packing: true diff --git a/examples/llama-3/lora-8b.yml b/examples/llama-3/lora-8b.yml index d60f8a3035..6b0ebaed86 100644 --- a/examples/llama-3/lora-8b.yml +++ b/examples/llama-3/lora-8b.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./lora-out +output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true diff --git a/examples/llama-3/qlora-fsdp-70b.yaml b/examples/llama-3/qlora-fsdp-70b.yaml index 8d8785bfd5..9b74f6b4de 100644 --- a/examples/llama-3/qlora-fsdp-70b.yaml +++ b/examples/llama-3/qlora-fsdp-70b.yaml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.05 -output_dir: ./out/qlora-llama3-70b +output_dir: ./outputs/out/qlora-llama3-70b adapter: qlora lora_model_dir: diff --git a/examples/llama-3/qlora.yml b/examples/llama-3/qlora.yml index 9cedee8eec..44120d9385 100644 --- a/examples/llama-3/qlora.yml +++ b/examples/llama-3/qlora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0 -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out adapter: qlora lora_model_dir: diff --git a/examples/mamba/config.yml b/examples/mamba/config.yml index 0a5223bcac..f88f5138d9 100644 --- a/examples/mamba/config.yml +++ b/examples/mamba/config.yml @@ -12,7 +12,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.0 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 2048 sample_packing: false diff --git a/examples/mistral/bigstral-ds-zero3.yaml b/examples/mistral/bigstral-ds-zero3.yaml index cc0a44b2a4..e993e44a78 100644 --- a/examples/mistral/bigstral-ds-zero3.yaml +++ b/examples/mistral/bigstral-ds-zero3.yaml @@ -23,7 +23,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.05 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 2048 sample_packing: true diff --git a/examples/mistral/config.yml b/examples/mistral/config.yml index c909c63e22..a70937c4fd 100644 --- a/examples/mistral/config.yml +++ b/examples/mistral/config.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 8192 sample_packing: true diff --git a/examples/mistral/lora-mps.yml b/examples/mistral/lora-mps.yml index 31b0d527e2..03c74bb59b 100644 --- a/examples/mistral/lora-mps.yml +++ b/examples/mistral/lora-mps.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0 -output_dir: ./lora-out +output_dir: ./outputs/lora-out eval_sample_packing: false adapter: lora diff --git a/examples/mistral/lora.yml b/examples/mistral/lora.yml index ac9ac0dd98..0d5dc9edd7 100644 --- a/examples/mistral/lora.yml +++ b/examples/mistral/lora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.1 -output_dir: ./lora-out +output_dir: ./outputs/lora-out adapter: lora lora_model_dir: diff --git a/examples/mistral/mistral-qlora-fsdp.yml b/examples/mistral/mistral-qlora-fsdp.yml index 71ac1e701f..e6b07c594b 100644 --- a/examples/mistral/mistral-qlora-fsdp.yml +++ b/examples/mistral/mistral-qlora-fsdp.yml @@ -12,7 +12,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.02 -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out model_config: output_router_logits: true diff --git a/examples/mistral/mistral-qlora-orpo.yml b/examples/mistral/mistral-qlora-orpo.yml index 7727fd7485..2549ef018c 100644 --- a/examples/mistral/mistral-qlora-orpo.yml +++ b/examples/mistral/mistral-qlora-orpo.yml @@ -16,7 +16,7 @@ datasets: type: chat_template.argilla dataset_prepared_path: last_run_prepared val_set_size: 0.1 -output_dir: ./mistral-qlora-orpo-out +output_dir: ./outputs/mistral-qlora-orpo-out adapter: qlora lora_model_dir: diff --git a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml index ac80a2a756..fe68b28172 100644 --- a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml +++ b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.02 -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out model_config: output_router_logits: true diff --git a/examples/mistral/mixtral-qlora-fsdp.yml b/examples/mistral/mixtral-qlora-fsdp.yml index b6a07ae51c..c095970402 100644 --- a/examples/mistral/mixtral-qlora-fsdp.yml +++ b/examples/mistral/mixtral-qlora-fsdp.yml @@ -12,7 +12,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.02 -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out model_config: output_router_logits: true diff --git a/examples/mistral/mixtral.yml b/examples/mistral/mixtral.yml index 5ee3da9d65..13fbe92ab8 100644 --- a/examples/mistral/mixtral.yml +++ b/examples/mistral/mixtral.yml @@ -12,7 +12,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.0 -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out ## You can optionally freeze the entire model and unfreeze a subset of parameters unfrozen_parameters: diff --git a/examples/mistral/mixtral_22.yml b/examples/mistral/mixtral_22.yml index 9abb6f407a..9a1e86386c 100644 --- a/examples/mistral/mixtral_22.yml +++ b/examples/mistral/mixtral_22.yml @@ -21,7 +21,7 @@ model_config: datasets: - path: yahma/alpaca-cleaned type: alpaca -output_dir: ./out +output_dir: ./outputs/out sequence_len: 8000 sample_packing: true diff --git a/examples/mistral/qlora.yml b/examples/mistral/qlora.yml index 6fbbb96183..c7bdb155c0 100644 --- a/examples/mistral/qlora.yml +++ b/examples/mistral/qlora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.1 -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out adapter: qlora lora_model_dir: diff --git a/examples/mpt-7b/config.yml b/examples/mpt-7b/config.yml index 45e31266f1..530415de17 100644 --- a/examples/mpt-7b/config.yml +++ b/examples/mpt-7b/config.yml @@ -23,7 +23,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./mpt-alpaca-7b +output_dir: ./outputs/mpt-alpaca-7b gradient_accumulation_steps: 1 micro_batch_size: 1 num_epochs: 4 diff --git a/examples/openllama-3b/config.yml b/examples/openllama-3b/config.yml index 0a404c79d8..a0473213c0 100644 --- a/examples/openllama-3b/config.yml +++ b/examples/openllama-3b/config.yml @@ -25,7 +25,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./openllama-out +output_dir: ./outputs/openllama-out gradient_accumulation_steps: 1 micro_batch_size: 1 num_epochs: 4 diff --git a/examples/openllama-3b/lora.yml b/examples/openllama-3b/lora.yml index b83b2db4e4..2b67849159 100644 --- a/examples/openllama-3b/lora.yml +++ b/examples/openllama-3b/lora.yml @@ -31,7 +31,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./lora-out +output_dir: ./outputs/lora-out gradient_accumulation_steps: 1 micro_batch_size: 2 num_epochs: 4 diff --git a/examples/openllama-3b/qlora.yml b/examples/openllama-3b/qlora.yml index 3d6218b308..8d4dc05ca7 100644 --- a/examples/openllama-3b/qlora.yml +++ b/examples/openllama-3b/qlora.yml @@ -25,7 +25,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out gradient_accumulation_steps: 1 micro_batch_size: 2 num_epochs: 4 diff --git a/examples/phi/phi-ft.yml b/examples/phi/phi-ft.yml index b21386f707..0dabadc7a4 100644 --- a/examples/phi/phi-ft.yml +++ b/examples/phi/phi-ft.yml @@ -12,7 +12,7 @@ datasets: dataset_prepared_path: val_set_size: 0.05 -output_dir: ./phi-sft-out +output_dir: ./outputs/phi-sft-out sequence_len: 2048 sample_packing: true diff --git a/examples/phi/phi-qlora.yml b/examples/phi/phi-qlora.yml index d2b5d661c9..7c181a3c15 100644 --- a/examples/phi/phi-qlora.yml +++ b/examples/phi/phi-qlora.yml @@ -12,7 +12,7 @@ datasets: dataset_prepared_path: val_set_size: 0.05 -output_dir: ./phi-sft-out +output_dir: ./outputs/phi-sft-out sequence_len: 2048 sample_packing: true diff --git a/examples/phi/phi2-ft.yml b/examples/phi/phi2-ft.yml index 7a2d05d018..27815550b4 100644 --- a/examples/phi/phi2-ft.yml +++ b/examples/phi/phi2-ft.yml @@ -12,7 +12,7 @@ datasets: dataset_prepared_path: val_set_size: 0.05 -output_dir: ./phi-sft-out +output_dir: ./outputs/phi-sft-out sequence_len: 2048 sample_packing: true diff --git a/examples/pythia-12b/config.yml b/examples/pythia-12b/config.yml index e44bba7451..18e6beaafd 100644 --- a/examples/pythia-12b/config.yml +++ b/examples/pythia-12b/config.yml @@ -26,7 +26,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./pythia-12b +output_dir: ./outputs/pythia-12b gradient_accumulation_steps: 1 micro_batch_size: 1 num_epochs: 5 diff --git a/examples/pythia/lora.yml b/examples/pythia/lora.yml index 7cb07fe258..0aa650f67e 100644 --- a/examples/pythia/lora.yml +++ b/examples/pythia/lora.yml @@ -20,7 +20,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./lora-alpaca-pythia +output_dir: ./outputs/lora-alpaca-pythia gradient_accumulation_steps: 1 micro_batch_size: 4 num_epochs: 4 diff --git a/examples/qwen/lora.yml b/examples/qwen/lora.yml index da4d784e0a..dd8dc1e4f4 100644 --- a/examples/qwen/lora.yml +++ b/examples/qwen/lora.yml @@ -13,7 +13,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./lora-out +output_dir: ./outputs/lora-out sequence_len: 2048 # supports up to 8192 sample_packing: false diff --git a/examples/qwen/qlora.yml b/examples/qwen/qlora.yml index 501a866b2d..01c0c0ab86 100644 --- a/examples/qwen/qlora.yml +++ b/examples/qwen/qlora.yml @@ -13,7 +13,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./lora-out +output_dir: ./outputs/lora-out sequence_len: 2048 # supports up to 8192 sample_packing: false diff --git a/examples/qwen/qwen2-moe-lora.yaml b/examples/qwen/qwen2-moe-lora.yaml index c59b282d0a..452335e38f 100644 --- a/examples/qwen/qwen2-moe-lora.yaml +++ b/examples/qwen/qwen2-moe-lora.yaml @@ -10,7 +10,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 1024 # supports up to 32k sample_packing: false diff --git a/examples/qwen/qwen2-moe-qlora.yaml b/examples/qwen/qwen2-moe-qlora.yaml index d6a835a0a3..bc11007c78 100644 --- a/examples/qwen/qwen2-moe-qlora.yaml +++ b/examples/qwen/qwen2-moe-qlora.yaml @@ -10,7 +10,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 1024 # supports up to 32k sample_packing: false diff --git a/examples/redpajama/config-3b.yml b/examples/redpajama/config-3b.yml index 5a42e2a952..ff395a863d 100644 --- a/examples/redpajama/config-3b.yml +++ b/examples/redpajama/config-3b.yml @@ -24,7 +24,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./redpajama-alpaca-3b +output_dir: ./outputs/redpajama-alpaca-3b batch_size: 4 micro_batch_size: 1 num_epochs: 4 diff --git a/examples/replit-3b/config-lora.yml b/examples/replit-3b/config-lora.yml index bdfe1bd854..9fee099d47 100644 --- a/examples/replit-3b/config-lora.yml +++ b/examples/replit-3b/config-lora.yml @@ -23,7 +23,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./lora-replit +output_dir: ./outputs/lora-replit batch_size: 8 micro_batch_size: 1 num_epochs: 4 diff --git a/examples/stablelm-2/1.6b/fft.yml b/examples/stablelm-2/1.6b/fft.yml index f3fc16f867..777262a7ee 100644 --- a/examples/stablelm-2/1.6b/fft.yml +++ b/examples/stablelm-2/1.6b/fft.yml @@ -12,7 +12,7 @@ datasets: type: alpaca dataset_prepared_path: last_run_prepared val_set_size: 0.05 -output_dir: ./out +output_dir: ./outputs/out sequence_len: 4096 sample_packing: true diff --git a/examples/stablelm-2/1.6b/lora.yml b/examples/stablelm-2/1.6b/lora.yml index c5051fab6e..c65b9e4cd0 100644 --- a/examples/stablelm-2/1.6b/lora.yml +++ b/examples/stablelm-2/1.6b/lora.yml @@ -12,7 +12,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./lora-out +output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true diff --git a/examples/starcoder2/qlora.yml b/examples/starcoder2/qlora.yml index 1efdfbc8e0..83fc0d89f7 100644 --- a/examples/starcoder2/qlora.yml +++ b/examples/starcoder2/qlora.yml @@ -11,7 +11,7 @@ datasets: dataset_prepared_path: val_set_size: 0.2 -output_dir: ./qlora +output_dir: ./outputs/qlora adapter: qlora lora_model_dir: diff --git a/examples/tiny-llama/lora-mps.yml b/examples/tiny-llama/lora-mps.yml index fd7b02caca..c08be82d3b 100644 --- a/examples/tiny-llama/lora-mps.yml +++ b/examples/tiny-llama/lora-mps.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0 -output_dir: ./lora-out +output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true diff --git a/examples/tiny-llama/lora.yml b/examples/tiny-llama/lora.yml index 4a16f14b92..c5ff0437e8 100644 --- a/examples/tiny-llama/lora.yml +++ b/examples/tiny-llama/lora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./lora-out +output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true diff --git a/examples/tiny-llama/pretrain.yml b/examples/tiny-llama/pretrain.yml index 3b68a7f547..e501dcb8e5 100644 --- a/examples/tiny-llama/pretrain.yml +++ b/examples/tiny-llama/pretrain.yml @@ -14,7 +14,7 @@ pretraining_dataset: type: pretrain dataset_prepared_path: val_set_size: 0.0 -output_dir: ./model-out +output_dir: ./outputs/model-out sequence_len: 2048 sample_packing: true diff --git a/examples/tiny-llama/qlora.yml b/examples/tiny-llama/qlora.yml index 3ea313c838..0d21aca9d5 100644 --- a/examples/tiny-llama/qlora.yml +++ b/examples/tiny-llama/qlora.yml @@ -11,7 +11,7 @@ datasets: type: alpaca dataset_prepared_path: val_set_size: 0.05 -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out adapter: qlora lora_model_dir: diff --git a/examples/xgen-7b/xgen-7b-8k-qlora.yml b/examples/xgen-7b/xgen-7b-8k-qlora.yml index e3faa01bdb..7e3f83cbd7 100644 --- a/examples/xgen-7b/xgen-7b-8k-qlora.yml +++ b/examples/xgen-7b/xgen-7b-8k-qlora.yml @@ -40,7 +40,7 @@ wandb_entity: wandb_watch: wandb_name: wandb_log_model: -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out # QLoRA paper Table 9 # - 16 for 7b & 13b diff --git a/examples/yi-34B-chat/qlora.yml b/examples/yi-34B-chat/qlora.yml index dc8c37d187..7fe322d63d 100644 --- a/examples/yi-34B-chat/qlora.yml +++ b/examples/yi-34B-chat/qlora.yml @@ -33,7 +33,7 @@ eval_sample_packing: false eval_batch_size: 1 # LoRA -output_dir: ./qlora-out +output_dir: ./outputs/qlora-out adapter: qlora lora_model_dir: lora_r: 32 diff --git a/outputs/.gitignore b/outputs/.gitignore new file mode 100644 index 0000000000..72e8ffc0db --- /dev/null +++ b/outputs/.gitignore @@ -0,0 +1 @@ +* From 039e2a03700730f333471d130dbf1f4d7922b357 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 15 May 2024 13:27:44 -0400 Subject: [PATCH 0004/1405] bump versions of deps (#1621) * bump versions of deps * bump transformers too * fix xformers deps and include s3fs install --- docker/Dockerfile | 2 +- requirements.txt | 16 ++++++++-------- setup.py | 17 ++++++++++------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ae3fe89ae4..416582bbe4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,7 +11,7 @@ ARG PYTORCH_VERSION="2.1.2" ENV PYTORCH_VERSION=$PYTORCH_VERSION RUN apt-get update && \ - apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev + apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev s3fs WORKDIR /workspace diff --git a/requirements.txt b/requirements.txt index b15a28c901..6723b17982 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,22 +1,22 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.10.0 -transformers @ git+https://github.com/huggingface/transformers.git@43d17c18360ac9c3d3491389328e2fe55fe8f9ce -tokenizers==0.15.0 -bitsandbytes==0.43.0 -accelerate==0.28.0 -deepspeed==0.13.1 +transformers==4.40.2 +tokenizers==0.19.1 +bitsandbytes==0.43.1 +accelerate==0.30.1 +deepspeed==0.14.2 pydantic==2.6.3 addict fire PyYAML>=6.0 requests -datasets==2.15.0 -flash-attn==2.5.5 +datasets==2.19.1 +flash-attn==2.5.8 sentencepiece wandb einops -xformers==0.0.22 +xformers==0.0.23.post1 optimum==1.16.2 hf_transfer colorama diff --git a/setup.py b/setup.py index fbca5a360e..31a6d67164 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def parse_requirements(): try: if "Darwin" in platform.system(): - _install_requires.pop(_install_requires.index("xformers==0.0.22")) + _install_requires.pop(_install_requires.index("xformers==0.0.23.post1")) else: torch_version = version("torch") _install_requires.append(f"torch=={torch_version}") @@ -45,9 +45,12 @@ def parse_requirements(): else: raise ValueError("Invalid version format") - if (major, minor) >= (2, 1): - _install_requires.pop(_install_requires.index("xformers==0.0.22")) - _install_requires.append("xformers>=0.0.23") + if (major, minor) >= (2, 3): + _install_requires.pop(_install_requires.index("xformers==0.0.23.post1")) + _install_requires.append("xformers>=0.0.26.post1") + elif (major, minor) >= (2, 2): + _install_requires.pop(_install_requires.index("xformers==0.0.23.post1")) + _install_requires.append("xformers>=0.0.25.post1") except PackageNotFoundError: pass @@ -68,13 +71,13 @@ def parse_requirements(): dependency_links=dependency_links, extras_require={ "flash-attn": [ - "flash-attn==2.5.5", + "flash-attn==2.5.8", ], "fused-dense-lib": [ - "fused-dense-lib @ git+https://github.com/Dao-AILab/flash-attention@v2.3.3#subdirectory=csrc/fused_dense_lib", + "fused-dense-lib @ git+https://github.com/Dao-AILab/flash-attention@v2.5.8#subdirectory=csrc/fused_dense_lib", ], "deepspeed": [ - "deepspeed==0.13.1", + "deepspeed==0.14.2", "deepspeed-kernels", ], "mamba-ssm": [ From e6937e884be0bfcbf1aeec9d51e6d2791883b424 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 15 May 2024 19:41:45 -0400 Subject: [PATCH 0005/1405] fix symlinks for axolotl outputs (#1625) --- docker/Dockerfile-cloud | 4 +--- outputs/.gitignore | 1 - scripts/cloud-entrypoint.sh | 7 +++++++ 3 files changed, 8 insertions(+), 4 deletions(-) delete mode 100644 outputs/.gitignore diff --git a/docker/Dockerfile-cloud b/docker/Dockerfile-cloud index cc8c58415b..69ce143bb2 100644 --- a/docker/Dockerfile-cloud +++ b/docker/Dockerfile-cloud @@ -21,9 +21,7 @@ RUN apt install --yes --no-install-recommends openssh-server tmux && \ printf "\n[[ -z \"\$TMUX\" ]] && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> ~/.bashrc && \ printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> ~/.bashrc && \ chmod +x /workspace/axolotl/scripts/cloud-entrypoint.sh && \ - chmod +x /root/cloud-entrypoint.sh && \ - mkdir -p /workspace/data/axolotl-artifacts && \ - ln -sf /workspace/data/axolotl-artifacts /workspace/axolotl/outputs + chmod +x /root/cloud-entrypoint.sh ENTRYPOINT ["/root/cloud-entrypoint.sh"] CMD ["sleep", "infinity"] diff --git a/outputs/.gitignore b/outputs/.gitignore deleted file mode 100644 index 72e8ffc0db..0000000000 --- a/outputs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/scripts/cloud-entrypoint.sh b/scripts/cloud-entrypoint.sh index 3a15a884aa..0d4021336e 100755 --- a/scripts/cloud-entrypoint.sh +++ b/scripts/cloud-entrypoint.sh @@ -36,5 +36,12 @@ if [ "$JUPYTER_DISABLE" != "1" ]; then jupyter lab --port=8888 --ip=* --allow-root --ServerApp.allow_origin=* & fi +if [ ! -d "/workspace/data/axolotl-artifacts" ]; then + mkdir -p /workspace/data/axolotl-artifacts +fi +if [ ! -L "/workspace/axolotl/outputs" ]; then + ln -sf /workspace/data/axolotl-artifacts /workspace/axolotl/outputs +fi + # Execute the passed arguments (CMD) exec "$@" From 2501a371c69ba1ec7489f52fd01f2b6faf688e35 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 15 May 2024 20:48:56 -0400 Subject: [PATCH 0006/1405] fix setting the authorized keys when there are more than one in the env var (#1626) --- scripts/cloud-entrypoint.sh | 49 +++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/scripts/cloud-entrypoint.sh b/scripts/cloud-entrypoint.sh index 0d4021336e..5b0337f2b2 100755 --- a/scripts/cloud-entrypoint.sh +++ b/scripts/cloud-entrypoint.sh @@ -5,20 +5,53 @@ echo "Exporting environment variables..." printenv | grep -E '^RUNPOD_|^PATH=|^_=' | sed 's/^\(.*\)=\(.*\)$/export \1="\2"/' >> /etc/rp_environment echo 'source /etc/rp_environment' >> ~/.bashrc -if [[ $PUBLIC_KEY ]]; then - # runpod +add_keys_to_authorized() { + local key_value=$1 + + # Create the ~/.ssh directory and set permissions mkdir -p ~/.ssh chmod 700 ~/.ssh - echo $PUBLIC_KEY >> ~/.ssh/authorized_keys + + # Create the authorized_keys file if it doesn't exist + touch ~/.ssh/authorized_keys + + # Initialize an empty key variable + local key="" + + # Read the key variable word by word + for word in $key_value; do + # Check if the word looks like the start of a key + if [[ $word == ssh-* ]]; then + # If there's a key being built, add it to the authorized_keys file + if [[ -n $key ]]; then + echo $key >> ~/.ssh/authorized_keys + fi + # Start a new key + key=$word + else + # Append the word to the current key + key="$key $word" + fi + done + + # Add the last key to the authorized_keys file + if [[ -n $key ]]; then + echo $key >> ~/.ssh/authorized_keys + fi + + # Set the correct permissions + chmod 600 ~/.ssh/authorized_keys chmod 700 -R ~/.ssh +} + +if [[ $PUBLIC_KEY ]]; then + # runpod + add_keys_to_authorized "$PUBLIC_KEY" # Start the SSH service in the background service ssh start -elif [ -n "$SSH_KEY" ]; then +elif [[ $SSH_KEY ]]; then # latitude.sh - mkdir -p ~/.ssh - chmod 700 ~/.ssh - echo $SSH_KEY >> ~/.ssh/authorized_keys - chmod 700 -R ~/.ssh + add_keys_to_authorized "$SSH_KEY" # Start the SSH service in the background service ssh start else From 419b2a6a98ca2d30fce024f128c3dd81395ebb9e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 15 May 2024 21:36:00 -0400 Subject: [PATCH 0007/1405] install rsync too (#1627) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 416582bbe4..6b9cf7d4c4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,7 +11,7 @@ ARG PYTORCH_VERSION="2.1.2" ENV PYTORCH_VERSION=$PYTORCH_VERSION RUN apt-get update && \ - apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev s3fs + apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev rsync s3fs WORKDIR /workspace From 60113437e494a5bc0bf7d5a46769b32595182e22 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 15 May 2024 22:27:40 -0400 Subject: [PATCH 0008/1405] cloud image w/o tmux (#1628) --- .github/workflows/main.yml | 42 +++++++++++++++++++++++++++++++++ docker/Dockerfile-cloud-no-tmux | 26 ++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 docker/Dockerfile-cloud-no-tmux diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 99190dc717..d0d0289824 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -125,3 +125,45 @@ jobs: ${{ steps.metadata.outputs.tags }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} ${{ (matrix.is_latest) && format('{0}-latest', steps.metadata.outputs.tags) || '' }} labels: ${{ steps.metadata.outputs.labels }} + + build-axolotl-cloud-no-tmux: + needs: build-axolotl + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'OpenAccess-AI-Collective' }} + # this job needs to be run on self-hosted GPU runners... + strategy: + matrix: + include: + - cuda: 121 + cuda_version: 12.1.0 + python_version: "3.11" + pytorch: 2.3.0 + axolotl_extras: + runs-on: axolotl-gpu-runner + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Docker metadata + id: metadata + uses: docker/metadata-action@v5 + with: + images: winglian/axolotl-cloud-term + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Build + uses: docker/build-push-action@v5 + with: + context: . + build-args: | + BASE_TAG=${{ github.ref_name }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} + CUDA=${{ matrix.cuda }} + file: ./docker/Dockerfile-cloud-no-tmux + push: ${{ github.event_name != 'pull_request' }} + tags: | + ${{ steps.metadata.outputs.tags }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} + ${{ (matrix.is_latest) && format('{0}-latest', steps.metadata.outputs.tags) || '' }} + labels: ${{ steps.metadata.outputs.labels }} diff --git a/docker/Dockerfile-cloud-no-tmux b/docker/Dockerfile-cloud-no-tmux new file mode 100644 index 0000000000..8af194de02 --- /dev/null +++ b/docker/Dockerfile-cloud-no-tmux @@ -0,0 +1,26 @@ +ARG BASE_TAG=main +FROM winglian/axolotl:$BASE_TAG + +ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" +ENV HUGGINGFACE_HUB_CACHE="/workspace/data/huggingface-cache/hub" +ENV TRANSFORMERS_CACHE="/workspace/data/huggingface-cache/hub" +ENV HF_HOME="/workspace/data/huggingface-cache/hub" +ENV HF_HUB_ENABLE_HF_TRANSFER="1" + +EXPOSE 8888 +EXPOSE 22 + +COPY scripts/cloud-entrypoint.sh /root/cloud-entrypoint.sh +COPY scripts/motd /etc/motd + +RUN pip install jupyterlab notebook ipywidgets && \ + jupyter lab clean +RUN apt install --yes --no-install-recommends openssh-server tmux && \ + mkdir -p ~/.ssh && \ + chmod 700 ~/.ssh && \ + printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> ~/.bashrc && \ + chmod +x /workspace/axolotl/scripts/cloud-entrypoint.sh && \ + chmod +x /root/cloud-entrypoint.sh + +ENTRYPOINT ["/root/cloud-entrypoint.sh"] +CMD ["sleep", "infinity"] From 0c49ecc429d2d663acddc5d33bfda939fce3a39a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 16 May 2024 00:05:56 -0400 Subject: [PATCH 0009/1405] more fixes to work with runpod + skypilot (#1629) --- docker/Dockerfile-cloud-no-tmux | 5 +- scripts/cloud-entrypoint-term.sh | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100755 scripts/cloud-entrypoint-term.sh diff --git a/docker/Dockerfile-cloud-no-tmux b/docker/Dockerfile-cloud-no-tmux index 8af194de02..dffecdc25e 100644 --- a/docker/Dockerfile-cloud-no-tmux +++ b/docker/Dockerfile-cloud-no-tmux @@ -10,12 +10,13 @@ ENV HF_HUB_ENABLE_HF_TRANSFER="1" EXPOSE 8888 EXPOSE 22 -COPY scripts/cloud-entrypoint.sh /root/cloud-entrypoint.sh +COPY scripts/cloud-entrypoint-term.sh /root/cloud-entrypoint.sh COPY scripts/motd /etc/motd RUN pip install jupyterlab notebook ipywidgets && \ jupyter lab clean -RUN apt install --yes --no-install-recommends openssh-server tmux && \ +RUN apt install --yes --no-install-recommends openssh-server tmux sudo && \ + pip3 install -U --no-cache-dir grpcio ray==2.9.3 && \ mkdir -p ~/.ssh && \ chmod 700 ~/.ssh && \ printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> ~/.bashrc && \ diff --git a/scripts/cloud-entrypoint-term.sh b/scripts/cloud-entrypoint-term.sh new file mode 100755 index 0000000000..94511ec7c6 --- /dev/null +++ b/scripts/cloud-entrypoint-term.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# Export specific ENV variables to /etc/rp_environment +echo "Exporting environment variables..." +printenv | grep -E '^RUNPOD_|^PATH=|^_=' | sed 's/^\(.*\)=\(.*\)$/export \1="\2"/' >> /etc/rp_environment +conda init +# this needs to come after conda init +echo 'source /etc/rp_environment' >> ~/.bashrc + +add_keys_to_authorized() { + local key_value=$1 + + # Create the ~/.ssh directory and set permissions + mkdir -p ~/.ssh + chmod 700 ~/.ssh + + # Create the authorized_keys file if it doesn't exist + touch ~/.ssh/authorized_keys + + # Initialize an empty key variable + local key="" + + # Read the key variable word by word + for word in $key_value; do + # Check if the word looks like the start of a key + if [[ $word == ssh-* ]]; then + # If there's a key being built, add it to the authorized_keys file + if [[ -n $key ]]; then + echo $key >> ~/.ssh/authorized_keys + fi + # Start a new key + key=$word + else + # Append the word to the current key + key="$key $word" + fi + done + + # Add the last key to the authorized_keys file + if [[ -n $key ]]; then + echo $key >> ~/.ssh/authorized_keys + fi + + # Set the correct permissions + chmod 600 ~/.ssh/authorized_keys + chmod 700 -R ~/.ssh +} + +if [[ $PUBLIC_KEY ]]; then + # runpod + add_keys_to_authorized "$PUBLIC_KEY" + # Start the SSH service in the background + service ssh start +elif [[ $SSH_KEY ]]; then + # latitude.sh + add_keys_to_authorized "$SSH_KEY" + # Start the SSH service in the background + service ssh start +else + echo "No PUBLIC_KEY or SSH_KEY environment variable provided, not starting openSSH daemon" +fi + +# Check if JUPYTER_PASSWORD is set and not empty +if [ -n "$JUPYTER_PASSWORD" ]; then + # Set JUPYTER_TOKEN to the value of JUPYTER_PASSWORD + export JUPYTER_TOKEN="$JUPYTER_PASSWORD" +fi + +if [ "$JUPYTER_DISABLE" != "1" ]; then + # Run Jupyter Lab in the background + jupyter lab --port=8888 --ip=* --allow-root --ServerApp.allow_origin=* & +fi + +if [ ! -d "/workspace/data/axolotl-artifacts" ]; then + mkdir -p /workspace/data/axolotl-artifacts +fi +if [ ! -L "/workspace/axolotl/outputs" ]; then + ln -sf /workspace/data/axolotl-artifacts /workspace/axolotl/outputs +fi + +# Execute the passed arguments (CMD) +exec "$@" From 891ae8aa13d1879e863a3d9c104764ad19ee73e0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 16 May 2024 01:25:42 -0400 Subject: [PATCH 0010/1405] fix ray install (#1630) --- docker/Dockerfile-cloud-no-tmux | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile-cloud-no-tmux b/docker/Dockerfile-cloud-no-tmux index dffecdc25e..efeffef8e6 100644 --- a/docker/Dockerfile-cloud-no-tmux +++ b/docker/Dockerfile-cloud-no-tmux @@ -16,7 +16,7 @@ COPY scripts/motd /etc/motd RUN pip install jupyterlab notebook ipywidgets && \ jupyter lab clean RUN apt install --yes --no-install-recommends openssh-server tmux sudo && \ - pip3 install -U --no-cache-dir grpcio ray==2.9.3 && \ + pip3 install -U --no-cache-dir grpcio ray[default]==2.9.3 && \ mkdir -p ~/.ssh && \ chmod 700 ~/.ssh && \ printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> ~/.bashrc && \ From 702a669cadfa4c8247699f1663972479d9ec4dc7 Mon Sep 17 00:00:00 2001 From: Jeffrey Quesnelle Date: Thu, 16 May 2024 21:23:18 -0700 Subject: [PATCH 0011/1405] add save_only_model option (#1634) --- src/axolotl/core/trainer_builder.py | 2 ++ src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 + 2 files changed, 3 insertions(+) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 2f38b12dc1..7330a78efb 100644 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1123,6 +1123,8 @@ def build(self, total_num_steps): # default to saving each epoch if not defined training_arguments_kwargs["save_strategy"] = "epoch" + training_arguments_kwargs["save_only_model"] = self.cfg.save_only_model + if self.cfg.do_bench_eval: training_arguments_kwargs["do_bench_eval"] = self.cfg.do_bench_eval if self.cfg.bench_dataset: diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index f1c12b2ba0..9be0f6949b 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -574,6 +574,7 @@ class Config: logging_steps: Optional[int] = None early_stopping_patience: Optional[int] = None load_best_model_at_end: Optional[bool] = False + save_only_model: Optional[bool] = False neftune_noise_alpha: Optional[float] = None From 8a1572a83151f66e04ae2aa8c4f97797820458e3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 20 May 2024 09:55:06 -0400 Subject: [PATCH 0012/1405] Unsloth optims for Llama (#1609) * WIP for unsloth integrations * import the unsloth code in the right context * add unsloth mlp, qkv, o lora optimizations * apply unsloth mlp and qkv kernels --- src/axolotl/monkeypatch/unsloth_.py | 267 ++++++++++++++++++ .../config/models/input/v0_4_1/__init__.py | 5 + src/axolotl/utils/models.py | 19 ++ 3 files changed, 291 insertions(+) create mode 100644 src/axolotl/monkeypatch/unsloth_.py diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py new file mode 100644 index 0000000000..de8260414e --- /dev/null +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -0,0 +1,267 @@ +"""module for patching with unsloth optimizations""" + +import inspect +import logging +import re +import types +from typing import Tuple + +from peft import PeftModelForCausalLM +from transformers.models.llama.modeling_llama import ( + LlamaFlashAttention2, + LlamaForCausalLM, +) + +LOG = logging.getLogger("axolotl.monkeypatch.unsloth") + +ORIGINAL_CEL_CODE = """ if labels is not None: + # Shift so that tokens < n predict n + shift_logits = logits[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + # Flatten the tokens + loss_fct = CrossEntropyLoss() + shift_logits = shift_logits.view(-1, self.config.vocab_size) + shift_labels = shift_labels.view(-1) + # Enable model parallelism + shift_labels = shift_labels.to(shift_logits.device) + loss = loss_fct(shift_logits, shift_labels) +""" + +PATCHED_CEL_CODE = """ if labels is not None: + shift_logits = logits[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + loss = fast_cross_entropy_loss( + logits = shift_logits, + labels = shift_labels, + ) +""" + +ORIGINAL_QKV_CODE = """ + query_states = self.q_proj(hidden_states) + key_states = self.k_proj(hidden_states) + value_states = self.v_proj(hidden_states) +""".lstrip( + "\n" +) + +PATCHED_QKV_CODE = """ + query_states, key_states, value_states = self.apply_qkv(self, hidden_states) +""".lstrip( + "\n" +) + +ORIGINAL_O_CODE = """ + attn_output = self.o_proj(attn_output) +""".lstrip( + "\n" +) + +PATCHED_O_CODE = """ + attn_output = self.apply_o(self, attn_output) +""".lstrip( + "\n" +) + + +def original_apply_qkv(self, hidden_states): + query_states = self.q_proj(hidden_states) + key_states = self.k_proj(hidden_states) + value_states = self.v_proj(hidden_states) + return query_states, key_states, value_states + + +def original_apply_o(self, hidden_states): + attn_output = self.o_proj(hidden_states) + return attn_output + + +def get_forward_code() -> str: + forward = inspect.getsource(LlamaForCausalLM.forward) + return forward + + +def test_cel_is_patchable() -> bool: + forward = get_forward_code() + return ORIGINAL_CEL_CODE in forward + + +def get_self_attn_code() -> str: + forward = inspect.getsource(LlamaFlashAttention2.forward) + return forward + + +def test_self_attn_is_patchable() -> bool: + qkv = get_self_attn_code() + return ORIGINAL_QKV_CODE in qkv and ORIGINAL_QKV_CODE in qkv + + +def integrate_cross_entropy_loss_patch(): + forward = get_forward_code() + LlamaForCausalLM._original_forward = forward # pylint: disable=protected-access + forward, _ = detab_code(forward) + assert ORIGINAL_CEL_CODE in forward, "Original forward code not found" + + forward = forward.replace( + "@add_start_docstrings_to_model_forward(LLAMA_INPUTS_DOCSTRING)", "" + ) + forward = forward.replace( + "@replace_return_docstrings(output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC)", + "", + ) + forward = forward.replace(ORIGINAL_CEL_CODE, PATCHED_CEL_CODE) + forward = forward.replace( + "def forward(", + "def fast_cross_entropy_loss_forward(", + 1, + ) + + # load imports necessary + import transformers.models.llama.modeling_llama + + items_to_import = [] + for item in dir(transformers.models.llama.modeling_llama): + if item in forward: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + "from unsloth.kernels.cross_entropy_loss import fast_cross_entropy_loss", + globals(), + ) + + exec( # pylint: disable=exec-used # nosec B102 + "from transformers.models.llama.modeling_llama import (" + + ", ".join(x for x in items_to_import) + + ")", + globals(), + ) + exec(forward, globals()) # pylint: disable=exec-used # nosec B102 + print("patching unsloth fast_cross_entropy_loss") + LlamaForCausalLM.forward = fast_cross_entropy_loss_forward # pylint: disable=undefined-variable # noqa: F821 + + +def detab_code(code: str) -> Tuple[str, str]: + spaces = re.match(r"([\s\t]{1,})", code).group(0) + code = re.sub(r"^" + spaces, "", code, flags=re.MULTILINE) + return code, spaces + + +def patch_self_attn_lora(): + self_attn_forward = get_self_attn_code() + LlamaFlashAttention2._original_forward = ( # pylint: disable=protected-access + self_attn_forward + ) + self_attn_forward, _ = detab_code(self_attn_forward) + assert ORIGINAL_QKV_CODE in self_attn_forward, "Original qkv code not found" + assert ORIGINAL_O_CODE in self_attn_forward, "Original o code not found" + + self_attn_forward = self_attn_forward.replace(ORIGINAL_QKV_CODE, PATCHED_QKV_CODE) + self_attn_forward = self_attn_forward.replace(ORIGINAL_O_CODE, PATCHED_O_CODE) + self_attn_forward = self_attn_forward.replace( + "def forward(", + "def unsloth_attn_forward(", + 1, + ) + + # load imports necessary + import transformers.models.llama.modeling_llama + + items_to_import = [] + for item in dir(transformers.models.llama.modeling_llama): + if item in self_attn_forward: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + "from transformers.models.llama.modeling_llama import (" + + ", ".join(x for x in items_to_import) + + ")", + globals(), + ) + exec(self_attn_forward, globals()) # pylint: disable=exec-used # nosec B102 + print("patching unsloth attn lora") + LlamaFlashAttention2.forward = ( + unsloth_attn_forward # pylint: disable=undefined-variable # noqa: F821 + ) + + +def integrate_lora_mlp_patch(peft_model: PeftModelForCausalLM): + if peft_model.base_model.config.model_type in ["llama", "mistral"]: + from unsloth.kernels import apply_lora_mlp_swiglu + + apply_lora_mlp = apply_lora_mlp_swiglu + elif peft_model.base_model.config.model_type == "gemma": + from unsloth.kernels import apply_lora_mlp_geglu_approx + + apply_lora_mlp = apply_lora_mlp_geglu_approx + else: + raise NotImplementedError( + f"Model type {peft_model.base_model.config.model_type} not supported" + ) + + for idx, layer in enumerate(peft_model.model.model.layers): + layer_modules = [ + getattr(layer.mlp, linear_proj) + for linear_proj in ["gate_proj", "up_proj", "down_proj"] + ] + is_mlp_lora = all(hasattr(module, "lora_A") for module in layer_modules) + mlp_no_bias = all( + getattr(module, "base_layer", module).bias is None + for module in layer_modules + ) + mlp_not_dora = all( + getattr(module, "lora_magnitude_vector", None) is None + for module in layer_modules + ) + + if is_mlp_lora and mlp_no_bias and mlp_not_dora: + layer.mlp.forward = types.MethodType(apply_lora_mlp, layer.mlp) + else: + logging.warning("unable to apply unsloth lora mlp patch to layer %d", idx) + + +def integrate_lora_patch(peft_model: PeftModelForCausalLM, cfg): + from unsloth.kernels import apply_lora_o, apply_lora_qkv + + for idx, layer in enumerate(peft_model.model.model.layers): + if cfg.unsloth_lora_qkv: + layer_modules = [ + getattr(layer.self_attn, linear_proj) + for linear_proj in ["q_proj", "k_proj", "v_proj"] + ] + is_qkv_lora = all(hasattr(module, "lora_A") for module in layer_modules) + qkv_no_bias = all( + getattr(module, "base_layer", module).bias is None + for module in layer_modules + ) + qkv_not_dora = all( + getattr(module, "lora_magnitude_vector", None) is None + for module in layer_modules + ) + + if is_qkv_lora and qkv_no_bias and qkv_not_dora: + layer.self_attn.apply_qkv = apply_lora_qkv + else: + layer.self_attn.apply_qkv = original_apply_qkv + logging.warning( + "unable to apply unsloth lora qkv patch to layer %d", idx + ) + if cfg.unsloth_lora_o: + layer_modules = [ + getattr(layer.self_attn, linear_proj) for linear_proj in ["o_proj"] + ] + is_o_lora = all(hasattr(module, "lora_A") for module in layer_modules) + o_no_bias = all( + getattr(module, "base_layer", module).bias is None + for module in layer_modules + ) + o_not_dora = all( + getattr(module, "lora_magnitude_vector", None) is None + for module in layer_modules + ) + + if is_o_lora and o_no_bias and o_not_dora: + layer.self_attn.apply_o = apply_lora_o + else: + layer.self_attn.apply_o = original_apply_o + logging.warning( + "unable to apply unsloth lora o_proj patch to layer %d", idx + ) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 9be0f6949b..585fbd734d 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -549,6 +549,11 @@ class Config: flash_attn_fuse_mlp: Optional[bool] = None flash_optimum: Optional[bool] = None + unsloth_cross_entropy_loss: Optional[bool] = None + unsloth_lora_mlp: Optional[bool] = None + unsloth_lora_qkv: Optional[bool] = None + unsloth_lora_o: Optional[bool] = None + deepspeed: Optional[Union[str, Dict[str, Any]]] = None fsdp: Optional[List[str]] = None fsdp_config: Optional[Dict[str, Any]] = None diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index e94a0f6b88..4f8388a0a8 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -390,6 +390,16 @@ def load_model( "Shifted-sparse attention not currently implemented without flash attention." ) + if cfg.unsloth_cross_entropy_loss: + from axolotl.monkeypatch.unsloth_ import integrate_cross_entropy_loss_patch + + integrate_cross_entropy_loss_patch() + + if cfg.unsloth_lora_qkv or cfg.unsloth_lora_o: + from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora + + patch_self_attn_lora() + # Modify mistral derived models if ( cfg.model_config_type == "mistral" @@ -828,6 +838,15 @@ def load_model( if cfg.adapter is not None: log_gpu_memory_usage(LOG, "after adapters", model.device) + if cfg.unsloth_lora_mlp: + from axolotl.monkeypatch.unsloth_ import integrate_lora_mlp_patch + + integrate_lora_mlp_patch(model) + if cfg.unsloth_lora_qkv or cfg.unsloth_lora_o: + from axolotl.monkeypatch.unsloth_ import integrate_lora_patch + + integrate_lora_patch(model, cfg) + # TODO resume_from_checkpoint handling return model, lora_config From ba45531802f15a1dc40f4da4ebe2f16263dea45b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 20 May 2024 14:24:45 -0400 Subject: [PATCH 0013/1405] fixes to save on fractional save_steps (#1643) --- src/axolotl/core/trainer_builder.py | 6 +++--- src/axolotl/utils/callbacks/__init__.py | 10 +++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 7330a78efb..0fdd126f55 100644 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -43,7 +43,7 @@ LossWatchDogCallback, SaveAxolotlConfigtoWandBCallback, SaveBetterTransformerModelCallback, - SaveModelOnTrainEndCallback, + SaveModelCallback, bench_eval_callback_factory, causal_lm_bench_eval_callback_factory, log_prediction_callback_factory, @@ -945,7 +945,7 @@ def get_callbacks(self): if self.cfg.loss_watchdog_threshold is not None: callbacks.append(LossWatchDogCallback(self.cfg)) - callbacks.append(SaveModelOnTrainEndCallback()) + callbacks.append(SaveModelCallback()) return callbacks @@ -1431,7 +1431,7 @@ class HFRLTrainerBuilder(TrainerBuilderBase): def get_callbacks(self): callbacks = super().get_callbacks() - callbacks.append(SaveModelOnTrainEndCallback()) + callbacks.append(SaveModelCallback()) return callbacks diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 2965ac1e29..c21ef0ad7a 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import math import os from shutil import copyfile from tempfile import NamedTemporaryFile @@ -775,7 +776,7 @@ def on_train_begin( return control -class SaveModelOnTrainEndCallback(TrainerCallback): +class SaveModelCallback(TrainerCallback): """Callback to save model on train end""" def on_step_end( # pylint: disable=unused-argument @@ -788,6 +789,13 @@ def on_step_end( # pylint: disable=unused-argument # Save if state.global_step >= state.max_steps: control.should_save = True + elif ( + args.save_strategy == IntervalStrategy.STEPS + and state.save_steps < 1.0 + and state.global_step % math.ceil(state.save_steps * state.max_steps) == 0 + ): + # workaround to save model on fractional save_steps + control.should_save = True def on_train_end( # pylint: disable=unused-argument self, args, state, control, **kwargs From 22ae21a6c2214c4e9780633d49b867d8813f5c5c Mon Sep 17 00:00:00 2001 From: Ben Redmond Date: Mon, 20 May 2024 16:05:16 -0400 Subject: [PATCH 0014/1405] Add KTO support (#1640) * add kto support * test cleanup * fix outdated comment * fix llama3 ultra * chore: lint * update to use rl_beta instead of dpo_beta --------- Co-authored-by: Wing Lian --- src/axolotl/core/trainer_builder.py | 31 +++++- src/axolotl/prompt_strategies/kto/__init__.py | 9 ++ src/axolotl/prompt_strategies/kto/chatml.py | 105 ++++++++++++++++++ src/axolotl/prompt_strategies/kto/llama3.py | 105 ++++++++++++++++++ .../prompt_strategies/kto/user_defined.py | 39 +++++++ .../config/models/input/v0_4_1/__init__.py | 44 +++++++- src/axolotl/utils/data/rl.py | 40 +++++-- src/axolotl/utils/models.py | 6 +- src/axolotl/utils/trainer.py | 2 +- tests/e2e/test_dpo.py | 63 +++++++++++ tests/test_validation.py | 9 ++ 11 files changed, 435 insertions(+), 18 deletions(-) create mode 100644 src/axolotl/prompt_strategies/kto/__init__.py create mode 100644 src/axolotl/prompt_strategies/kto/chatml.py create mode 100644 src/axolotl/prompt_strategies/kto/llama3.py create mode 100644 src/axolotl/prompt_strategies/kto/user_defined.py diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 0fdd126f55..9e79f82997 100644 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -30,7 +30,7 @@ ) from transformers.trainer_utils import seed_worker from transformers.utils import is_sagemaker_mp_enabled -from trl import DPOTrainer, ORPOConfig, ORPOTrainer +from trl import DPOTrainer, KTOConfig, KTOTrainer, ORPOConfig, ORPOTrainer from trl.trainer.utils import pad_to_length from axolotl.loraplus import create_loraplus_optimizer @@ -826,6 +826,14 @@ class AxolotlORPOTrainer(ORPOTrainer): tag_names = ["axolotl", "orpo"] +class AxolotlKTOTrainer(KTOTrainer): + """ + Extend the base KTOTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "kto"] + + class TrainerBuilderBase(abc.ABC): """ Base class for trainer builder @@ -1532,6 +1540,22 @@ def build_training_arguments(self, total_num_steps): if self.cfg.max_prompt_len: training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len + if self.cfg.rl == "kto": + training_args_cls = KTOConfig + + training_args_kwargs["beta"] = self.cfg.rl_beta or 0.1 + training_args_kwargs["desirable_weight"] = ( + self.cfg.kto_desirable_weight or 1.0 + ) + training_args_kwargs["undesirable_weight"] = ( + self.cfg.kto_undesirable_weight or 1.0 + ) + + training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes + training_args_kwargs["max_length"] = self.cfg.sequence_len + if self.cfg.max_prompt_len: + training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len + training_args = training_args_cls( per_device_train_batch_size=self.cfg.micro_batch_size, max_steps=self.cfg.max_steps or total_num_steps, @@ -1567,7 +1591,7 @@ def build(self, total_num_steps): ] = self.cfg.precompute_ref_log_probs if self.cfg.rl in ["dpo", "ipo", "kto_pair"]: trainer_cls = AxolotlDPOTrainer - dpo_trainer_kwargs["beta"] = self.cfg.dpo_beta or 0.1 + dpo_trainer_kwargs["beta"] = self.cfg.rl_beta or 0.1 trainer_cls_args = [self.model, self.model_ref] # these aren't used for the ORPO trainer @@ -1580,6 +1604,9 @@ def build(self, total_num_steps): elif self.cfg.rl == "orpo": trainer_cls = AxolotlORPOTrainer trainer_cls_args = [self.model] + elif self.cfg.rl == "kto": + trainer_cls = AxolotlKTOTrainer + trainer_cls_args = [self.model] else: raise ValueError(f"Unsupported RL: {self.cfg.rl}") dpo_trainer = trainer_cls( diff --git a/src/axolotl/prompt_strategies/kto/__init__.py b/src/axolotl/prompt_strategies/kto/__init__.py new file mode 100644 index 0000000000..9af6300eb3 --- /dev/null +++ b/src/axolotl/prompt_strategies/kto/__init__.py @@ -0,0 +1,9 @@ +""" +module for KTO style dataset transform strategies +""" + +from functools import partial + +from ..base import load as load_base + +load = partial(load_base, module_base="axolotl.prompt_strategies.kto") diff --git a/src/axolotl/prompt_strategies/kto/chatml.py b/src/axolotl/prompt_strategies/kto/chatml.py new file mode 100644 index 0000000000..46c305f831 --- /dev/null +++ b/src/axolotl/prompt_strategies/kto/chatml.py @@ -0,0 +1,105 @@ +""" +KTO strategies for chatml +""" +# pylint: disable=duplicate-code + + +def argilla( + cfg, + **kwargs, +): # pylint: disable=possibly-unused-variable,unused-argument + def transform_fn(sample): + if "system" in sample and sample["system"]: + sample["prompt"] = ( + f"<|im_start|>system\n{sample['system']}<|im_end|>\n" + f"<|im_start|>user\n{sample['instruction']}<|im_end|>\n<|im_start|>assistant\n" + ) + else: + sample[ + "prompt" + ] = f"<|im_start|>user\n{sample['instruction']}<|im_end|>\n<|im_start|>assistant\n" + sample["completion"] = f"{sample['completion']}<|im_end|>" + return sample + + return transform_fn + + +def argilla_chat( + cfg, + **kwargs, +): # pylint: disable=possibly-unused-variable,unused-argument + """ + for argilla/kto-mix-15k conversations + """ + + def transform_fn(sample): + sample[ + "prompt" + ] = f"<|im_start|>user\n{sample['chosen'][0]['content']}<|im_end|>\n<|im_start|>assistant\n" + sample["completion"] = f"{sample['completion'][1]['content']}<|im_end|>" + return sample + + return transform_fn + + +def intel(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument + """ + For Intel Orca KTO + ex: argilla/distilabel-intel-orca-kto + """ + + def transform_fn(sample): + if "system" in sample and sample["system"]: + sample["prompt"] = ( + f"<|im_start|>system\n{sample['system']}<|im_end|>\n" + f"<|im_start|>user\n{sample['question']}<|im_end|>\n<|im_start|>assistant\n" + ) + else: + sample[ + "prompt" + ] = f"<|im_start|>user\n{sample['question']}<|im_end|>\n<|im_start|>assistant\n" + sample["completion"] = f"{sample['completion']}<|im_end|>" + return sample + + return transform_fn + + +def prompt_pairs( + cfg, **kwargs +): # pylint: disable=possibly-unused-variable,unused-argument + def transform_fn(sample): + if "system" in sample and sample["system"]: + sample["prompt"] = ( + f"<|im_start|>system\n{sample['system']}<|im_end|>\n" + f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" + ) + else: + sample[ + "prompt" + ] = f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" + sample["completion"] = f"{sample['completion']}<|im_end|>" + return sample + + return transform_fn + + +def ultra(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument + """ + for ultrafeedback binarized conversations + ex: argilla/ultrafeedback-binarized-preferences-cleaned-kto + """ + + def transform_fn(sample): + if "system" in sample and sample["system"]: + sample["prompt"] = ( + f"<|im_start|>system\n{sample['system']}<|im_end|>\n" + f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" + ) + else: + sample[ + "prompt" + ] = f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" + sample["completion"] = f"{sample['completion']}<|im_end|>" + return sample + + return transform_fn diff --git a/src/axolotl/prompt_strategies/kto/llama3.py b/src/axolotl/prompt_strategies/kto/llama3.py new file mode 100644 index 0000000000..795d343fe3 --- /dev/null +++ b/src/axolotl/prompt_strategies/kto/llama3.py @@ -0,0 +1,105 @@ +""" +KTO strategies for llama-3 chat template +""" +# pylint: disable=duplicate-code + + +def argilla( + cfg, + **kwargs, +): # pylint: disable=possibly-unused-variable,unused-argument + def transform_fn(sample): + if "system" in sample and sample["system"]: + sample["prompt"] = ( + f"<|start_header_id|>system<|end_header_id|>\n\n{sample['system']}<|eot_id|>" + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['instruction']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) + else: + sample[ + "prompt" + ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['instruction']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["completion"] = f"{sample['completion']}<|eot_id|>" + return sample + + return transform_fn + + +def argilla_chat( + cfg, + **kwargs, +): # pylint: disable=possibly-unused-variable,unused-argument + """ + for argilla/kto-mix-15k conversations + """ + + def transform_fn(sample): + sample[ + "prompt" + ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['completion'][0]['content']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["completion"] = f"{sample['completion'][1]['content']}<|eot_id|>" + return sample + + return transform_fn + + +def intel(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument + """ + For Intel Orca KTO + ex: argilla/distilabel-intel-orca-kto + """ + + def transform_fn(sample): + if "system" in sample and sample["system"]: + sample["prompt"] = ( + f"<|start_header_id|>system<|end_header_id|>\n\n{sample['system']}<|eot_id|>" + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['question']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) + else: + sample[ + "prompt" + ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['question']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["completion"] = f"{sample['completion']}<|eot_id|>" + return sample + + return transform_fn + + +def prompt_pairs( + cfg, **kwargs +): # pylint: disable=possibly-unused-variable,unused-argument + def transform_fn(sample): + if "system" in sample and sample["system"]: + sample["prompt"] = ( + f"<|start_header_id|>system<|end_header_id|>\n\n{sample['system']}<|eot_id|>" + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) + else: + sample[ + "prompt" + ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["completion"] = f"{sample['completion']}<|eot_id|>" + return sample + + return transform_fn + + +def ultra(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument + """ + for ultrafeedback binarized conversations + ex: argilla/ultrafeedback-binarized-preferences-cleaned-kto + """ + + def transform_fn(sample): + if "system" in sample and sample["system"]: + sample["prompt"] = ( + f"<|start_header_id|>system<|end_header_id|>\n\n{sample['system']}<|eot_id|>" + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) + else: + sample[ + "prompt" + ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["completion"] = f"{sample['completion']}<|eot_id|>" + return sample + + return transform_fn diff --git a/src/axolotl/prompt_strategies/kto/user_defined.py b/src/axolotl/prompt_strategies/kto/user_defined.py new file mode 100644 index 0000000000..7e5458bb70 --- /dev/null +++ b/src/axolotl/prompt_strategies/kto/user_defined.py @@ -0,0 +1,39 @@ +""" +User-defined KTO strategies +""" +# pylint: disable=duplicate-code + + +def default(cfg, dataset_idx=0, **kwargs): # pylint: disable=unused-argument + ds_cfg = cfg["datasets"][dataset_idx]["type"] + if not isinstance(ds_cfg, dict): + raise ValueError( + f"User-defined dataset type must be a dictionary. Got: {ds_cfg}" + ) + field_prompt = ds_cfg.get("field_prompt", "prompt") + field_system = ds_cfg.get("field_system", "system") + field_completion = ds_cfg.get("field_completion", "completion") + field_label = ds_cfg.get("field_label", "label") + prompt_format = ds_cfg.get("prompt_format") + if not prompt_format: + prompt_format = "{" + field_prompt + "}" + completion_format = ds_cfg.get("completion_format") + if not completion_format: + chosen_format = "{" + field_completion + "}" + + def transform_fn(sample): + if ( + "{" + field_system + "}" in prompt_format + and field_system in sample + and sample[field_system] + ): + sample["prompt"] = prompt_format.format( + system=sample[field_system], prompt=sample[field_prompt] + ) + else: + sample["prompt"] = prompt_format.format(prompt=sample["prompt"]) + sample["completion"] = chosen_format.format(chosen=sample[field_completion]) + sample["label"] = sample[field_label] + return sample + + return transform_fn diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 585fbd734d..82db40e5ad 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -24,6 +24,7 @@ class DeprecatedParameters(BaseModel): max_packed_sequence_len: Optional[int] = None rope_scaling: Optional[Any] = None noisy_embedding_alpha: Optional[float] = None + dpo_beta: Optional[float] = None @field_validator("max_packed_sequence_len") @classmethod @@ -48,6 +49,13 @@ def validate_noisy_embedding_alpha(cls, noisy_embedding_alpha): LOG.warning("noisy_embedding_alpha is deprecated, use neftune_noise_alpha") return noisy_embedding_alpha + @field_validator("dpo_beta") + @classmethod + def validate_dpo_beta(cls, dpo_beta): + if dpo_beta is not None: + LOG.warning("dpo_beta is deprecated, use rl_beta instead") + return dpo_beta + class RemappedParameters(BaseModel): """parameters that have been remapped to other names""" @@ -126,6 +134,26 @@ class DPODataset(BaseModel): data_files: Optional[List[str]] = None +class UserDefinedKTOType(BaseModel): + """User defined typing for KTO""" + + field_system: Optional[str] = None + field_prompt: Optional[str] = None + field_completion: Optional[str] = None + field_label: Optional[bool] = None + prompt_format: Optional[str] = None + completion_format: Optional[str] = None + + +class KTODataset(BaseModel): + """KTO configuration subset""" + + path: Optional[str] = None + split: Optional[str] = None + type: Optional[Union[UserDefinedKTOType, str]] = None + data_files: Optional[List[str]] = None + + class RLType(str, Enum): """RL trainer type configuration subset""" @@ -133,6 +161,7 @@ class RLType(str, Enum): ipo = "ipo" # pylint: disable=invalid-name kto_pair = "kto_pair" # pylint: disable=invalid-name orpo = "orpo" # pylint: disable=invalid-name + kto = "kto" # pylint: disable=invalid-name class ChatTemplate(str, Enum): @@ -450,8 +479,8 @@ class Config: rl: Optional[RLType] = None - datasets: Optional[conlist(Union[SFTDataset, DPODataset], min_length=1)] = None # type: ignore - test_datasets: Optional[conlist(Union[SFTDataset, DPODataset], min_length=1)] = None # type: ignore + datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset], min_length=1)] = None # type: ignore + test_datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset], min_length=1)] = None # type: ignore shuffle_merged_datasets: Optional[bool] = True dataset_prepared_path: Optional[str] = None dataset_shard_num: Optional[int] = None @@ -585,6 +614,10 @@ class Config: orpo_alpha: Optional[float] = None + kto_desirable_weight: Optional[float] = None + kto_undesirable_weight: Optional[float] = None + rl_beta: Optional[float] = None + max_memory: Optional[ Dict[Union[int, Literal["cpu", "disk"]], Union[int, str]] ] = None @@ -884,6 +917,13 @@ def validate_neftune_noise_alpha(cls, neftune_noise_alpha): raise ValueError("neftune_noise_alpha must be > 0.0") return neftune_noise_alpha + @model_validator(mode="after") + def check(self): + if self.dpo_beta and not self.rl_beta: + self.rl_beta = self.dpo_beta + del self.dpo_beta + return self + @model_validator(mode="before") @classmethod def check_frozen(cls, data): diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index ff5ca87ddf..7416ca28bb 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -10,6 +10,7 @@ from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH from axolotl.prompt_strategies.dpo import load as load_dpo +from axolotl.prompt_strategies.kto import load as load_kto from axolotl.prompt_strategies.orpo import load as load_orpo from axolotl.utils.data.utils import md5 from axolotl.utils.dict import DictDefault @@ -55,6 +56,22 @@ def _save_preprocessed_ds(cfg, sub_cfg, dataset): dataset.save_to_disk(str(prepared_ds_path)) +def map_dataset(cfg, data_set, ds_transform_fn, tokenizer): + sig = inspect.signature(ds_transform_fn) + if "tokenizer" in sig.parameters: + if not tokenizer: + tokenizer = load_tokenizer(cfg) + ds_transform_fn = partial(ds_transform_fn, tokenizer=tokenizer) + + data_set = data_set.map( + ds_transform_fn, + desc="Mapping RL Dataset", + ) + if isinstance(data_set, DatasetDict): + data_set = data_set["train"] + return data_set + + def load_prepare_dpo_datasets(cfg): def load_split(dataset_cfgs, _cfg): split_datasets: List[Any] = [] @@ -76,6 +93,7 @@ def load_split(dataset_cfgs, _cfg): split_datasets.insert(i, ds) tokenizer = None + for i, data_set in enumerate(split_datasets): _type = dataset_cfgs[i]["type"] if _type: @@ -83,21 +101,19 @@ def load_split(dataset_cfgs, _cfg): _type = "user_defined.default" if _cfg.rl == "orpo": ds_transform_fn = load_orpo(_type, _cfg, dataset_idx=i) + elif _cfg.rl == "kto": + ds_transform_fn = load_kto(_type, _cfg, dataset_idx=i) else: ds_transform_fn = load_dpo(_type, _cfg, dataset_idx=i) - sig = inspect.signature(ds_transform_fn) - if "tokenizer" in sig.parameters: - if not tokenizer: - tokenizer = load_tokenizer(_cfg) - ds_transform_fn = partial(ds_transform_fn, tokenizer=tokenizer) - - data_set = data_set.map( - ds_transform_fn, - desc="Mapping RL Dataset", + + split_datasets[i] = map_dataset( + cfg, data_set, ds_transform_fn, tokenizer + ) + elif _cfg.rl == "kto": + ds_transform_fn = load_kto(_type, _cfg, dataset_idx=i) + split_datasets[i] = map_dataset( + cfg, data_set, ds_transform_fn, tokenizer ) - if isinstance(data_set, DatasetDict): - data_set = data_set["train"] - split_datasets[i] = data_set else: # If no `type` is provided, assume the dataset is already in the expected format with # "prompt", "chosen" and "rejected" already preprocessed diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 4f8388a0a8..a8df4bbad7 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -803,7 +803,11 @@ def load_model( if not reference_model or cfg.lora_model_dir: # if we're not loading the reference model, then we're loading the model for training # then the dpo trainer doesn't want the peft model loaded over it, it just wants the lora/peft config - if cfg.adapter and cfg.rl in ["dpo", "ipo", "kto_pair"] and not cfg.merge_lora: + if ( + cfg.adapter + and cfg.rl in ["dpo", "ipo", "kto_pair", "kto"] + and not cfg.merge_lora + ): _, lora_config = load_lora(model, cfg, inference=False, config_only=True) else: model, lora_config = load_adapter(model, cfg, cfg.adapter) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index fdf86e5672..83977ef064 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -428,7 +428,7 @@ def prepare_optim_env(cfg): def setup_trainer(cfg, train_dataset, eval_dataset, model, tokenizer, total_num_steps): - if cfg.rl in ["dpo", "ipo", "kto_pair", "orpo"]: + if cfg.rl in ["dpo", "ipo", "kto_pair", "orpo", "kto"]: trainer_builder = HFRLTrainerBuilder(cfg, model[0], tokenizer) trainer_builder.model_ref = model[1] trainer_builder.peft_config = model[2] diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index 9596b1873f..ddd63d8271 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -205,3 +205,66 @@ def test_orpo_lora(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + + @with_temp_dir + def test_kto_lora(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 1024, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 64, + "lora_alpha": 32, + "lora_dropout": 0.1, + "lora_target_linear": True, + "special_tokens": {}, + "rl": "kto", + "rl_beta": 0.5, + "kto_desirable_weight": 1.0, + "kto_undesirable_weight": 1.0, + "remove_unused_columns": False, + "datasets": [ + # { + # "path": "argilla/kto-mix-15k", + # "type": "chatml.argilla_chat", + # "split": "train", + # }, + { + "path": "argilla/ultrafeedback-binarized-preferences-cleaned-kto", + "type": "chatml.ultra", + "split": "train", + }, + # { + # "path": "argilla/kto-mix-15k", + # "type": "llama3.argilla_chat", + # "split": "train", + # }, + { + "path": "argilla/ultrafeedback-binarized-preferences-cleaned-kto", + "type": "llama3.ultra", + "split": "train", + }, + ], + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "paged_adamw_8bit", + "lr_scheduler": "cosine", + "max_steps": 20, + "save_steps": 10, + "warmup_steps": 5, + "gradient_checkpointing": True, + "gradient_checkpointing_kwargs": {"use_reentrant": True}, + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() diff --git a/tests/test_validation.py b/tests/test_validation.py index 27824f2887..35d0e265e7 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1117,6 +1117,15 @@ def test_hub_model_id_save_value_no_set_save_strategy(self, minimal_cfg): validate_config(cfg) assert len(self._caplog.records) == 0 + def test_dpo_beta_deprecation(self, minimal_cfg): + cfg = DictDefault({"dpo_beta": 0.2}) | minimal_cfg + + with self._caplog.at_level(logging.WARNING): + new_cfg = validate_config(cfg) + assert new_cfg["rl_beta"] == 0.2 + assert new_cfg["dpo_beta"] is None + assert len(self._caplog.records) == 1 + class TestValidationCheckModelConfig(BaseValidation): """ From 7c2bf3091f5e73c787afe839dfdcc8220b770a1a Mon Sep 17 00:00:00 2001 From: Leonard Date: Tue, 21 May 2024 22:08:53 +0900 Subject: [PATCH 0015/1405] Fix llama3 chat_template (extra <|eot_id|> on last turn) (#1635) * Fix llama3 chat_template (the {{eos_token}} leads to an extra <|eot_id|> being added in the last turn). Output now matches official Llama 3 Instruct model * add tests * chore: lint --------- Co-authored-by: Wing Lian --- src/axolotl/utils/chat_templates.py | 2 +- .../prompt_strategies/test_chat_templates.py | 85 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/prompt_strategies/test_chat_templates.py diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 01b1473568..1fe888aa80 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -24,7 +24,7 @@ def chat_templates(user_choice: str): "chatml": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}", "gemma": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\n' + message['content'] | trim + '\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\n'}}{% endif %}", "cohere": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", - "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% else %}{{ eos_token }}{% endif %}", + "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", } if user_choice in templates: diff --git a/tests/prompt_strategies/test_chat_templates.py b/tests/prompt_strategies/test_chat_templates.py new file mode 100644 index 0000000000..1076c6a3bf --- /dev/null +++ b/tests/prompt_strategies/test_chat_templates.py @@ -0,0 +1,85 @@ +""" +tests for chat_template prompt strategy +""" +import unittest + +import pytest +from datasets import Dataset +from transformers import AutoTokenizer + +from axolotl.prompt_strategies.chat_template import ( + ChatTemplatePrompter, + ChatTemplateStrategy, +) +from axolotl.utils.chat_templates import chat_templates + + +@pytest.fixture(name="sharegpt_dataset") +def fixture_sharegpt_dataset(): + # pylint: disable=duplicate-code + return Dataset.from_list( + [ + { + "conversations": [ + { + "from": "human", + "value": "hello", + }, + { + "from": "gpt", + "value": "hello", + }, + { + "from": "human", + "value": "goodbye", + }, + { + "from": "gpt", + "value": "goodbye", + }, + ] + } + ] + ) + + +@pytest.fixture(name="llama3_tokenizer") +def fixture_llama3_tokenizer(): + tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B") + tokenizer.eos_token = "<|eot_id|>" + + return tokenizer + + +class TestSharegptChatTemplateLlama3: + """ + Test class for ShareGPT style datasets with llama-3 prompts using the chat_template strategy. + """ + + def test_llama3(self, llama3_tokenizer, sharegpt_dataset): + # pylint: disable=duplicate-code + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + llama3_tokenizer, + False, + 512, + ) + res = strategy.tokenize_prompt(sharegpt_dataset[0]) + input_ids = res["input_ids"] + # fmt: off + assert input_ids == [ + 128000, # bos + 128006, 882, 128007, # user header + 271, 15339, 128009, # user prompt eot + 128006, 78191, 128007, # assistant header + 271, 15339, 128009, # assistant response eot + 128006, 882, 128007, + 271, 19045, 29474, 128009, + 128006, 78191, 128007, + 271, 19045, 29474, 128009, + ] + # fmt: on + + +if __name__ == "__main__": + unittest.main() From 6299eb5919b20de7e65c0474ae2dce7e5573b030 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 22 May 2024 08:27:44 -0400 Subject: [PATCH 0016/1405] allow report_to for multiple providers (#1647) --- src/axolotl/core/trainer_builder.py | 9 ++++++--- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 9e79f82997..06b71b3e10 100644 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1212,11 +1212,14 @@ def build(self, total_num_steps): ) training_arguments_kwargs["group_by_length"] = self.cfg.group_by_length training_arguments_kwargs["curriculum_sampling"] = self.cfg.curriculum_sampling - report_to = None + report_to = [] if self.cfg.use_wandb: - report_to = "wandb" + report_to.append("wandb") if self.cfg.use_mlflow: - report_to = "mlflow" + report_to.append("mlflow") + if self.cfg.use_tensorboard: + report_to.append("tensorboard") + training_arguments_kwargs["report_to"] = report_to training_arguments_kwargs["run_name"] = ( self.cfg.wandb_name if self.cfg.use_wandb else None diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 82db40e5ad..31750ac159 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -212,7 +212,7 @@ class LoraConfig(BaseModel): lora_target_modules: Optional[List[str]] = None lora_target_linear: Optional[bool] = None lora_modules_to_save: Optional[List[str]] = None - lora_dropout: Optional[float] = None + lora_dropout: Optional[float] = 0.0 peft_layers_to_transform: Optional[List[int]] = None peft: Optional[PeftConfig] = None peft_use_dora: Optional[bool] = None @@ -609,6 +609,7 @@ class Config: early_stopping_patience: Optional[int] = None load_best_model_at_end: Optional[bool] = False save_only_model: Optional[bool] = False + use_tensorboard: Optional[bool] = None neftune_noise_alpha: Optional[float] = None From a27d5e1f4e36ae8faa0e60394e6e36dd1ee67ff6 Mon Sep 17 00:00:00 2001 From: George Grigorev Date: Wed, 22 May 2024 13:29:06 +0100 Subject: [PATCH 0017/1405] enable loraplus setting for dpo trainer (#1646) --- src/axolotl/core/trainer_builder.py | 38 ++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) mode change 100644 => 100755 src/axolotl/core/trainer_builder.py diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py old mode 100644 new mode 100755 index 06b71b3e10..c510c8e107 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -798,6 +798,40 @@ class AxolotlDPOTrainer(DPOTrainer): tag_names = ["axolotl", "dpo"] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.optimizer = None + + def create_optimizer(self): + if self.args.loraplus_lr_ratio is None: + return super().create_optimizer() + + opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model + if self.optimizer is None: # pylint: disable=access-member-before-definition + optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs( + self.args, + opt_model, + ) + + loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) + if loraplus_lr_ratio: + print("Using lora+") + loraplus_lr_embedding = getattr(self.args, "loraplus_lr_embedding", None) + self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init + opt_model, + optimizer_cls, + optimizer_kwargs, + loraplus_lr_ratio, + loraplus_lr_embedding, + ) + + if is_sagemaker_mp_enabled(): + self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init + self.optimizer + ) + + return self.optimizer + @wraps(DPOTrainer.push_to_hub) def push_to_hub(self, *args, **kwargs) -> str: """ @@ -1483,6 +1517,8 @@ def build_training_arguments(self, total_num_steps): if self.cfg.bf16 or self.cfg.bfloat16: training_args_kwargs["bf16"] = True + training_args_kwargs["loraplus_lr_ratio"] = self.cfg.loraplus_lr_ratio + training_args_kwargs["loraplus_lr_embedding"] = self.cfg.loraplus_lr_embedding training_args_kwargs["lr_scheduler_type"] = ( self.cfg.lr_scheduler if self.cfg.lr_scheduler else "cosine" ) @@ -1535,7 +1571,7 @@ def build_training_arguments(self, total_num_steps): # trl does some odd mapping of alpha to beta to reuse the beta parameter ??? training_args_kwargs["beta"] = self.cfg.orpo_alpha - training_args_cls = TrainingArguments + training_args_cls = AxolotlTrainingArguments if self.cfg.rl == "orpo": training_args_cls = ORPOConfig training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes From 84bb8061ba10fbbd9a7e3a808476c5031d851243 Mon Sep 17 00:00:00 2001 From: Jaydeep Thik Date: Wed, 22 May 2024 08:34:06 -0400 Subject: [PATCH 0018/1405] Update tiny-llama qlora.yml addressing eval packing error (#1638) --- examples/tiny-llama/qlora.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/tiny-llama/qlora.yml b/examples/tiny-llama/qlora.yml index 0d21aca9d5..384f3315c0 100644 --- a/examples/tiny-llama/qlora.yml +++ b/examples/tiny-llama/qlora.yml @@ -18,6 +18,7 @@ lora_model_dir: sequence_len: 4096 sample_packing: true +eval_sample_packing: false pad_to_sequence_len: true lora_r: 32 From bbfed318bc2a60396aec94bfab2b636d5c46c5d9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 23 May 2024 13:03:22 -0400 Subject: [PATCH 0019/1405] support for custom messages field in sharegpt (#1651) --- src/axolotl/prompt_strategies/sharegpt.py | 13 ++++++++++++- .../utils/config/models/input/v0_4_1/__init__.py | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/axolotl/prompt_strategies/sharegpt.py b/src/axolotl/prompt_strategies/sharegpt.py index 5f0e7a895e..8b452ae199 100644 --- a/src/axolotl/prompt_strategies/sharegpt.py +++ b/src/axolotl/prompt_strategies/sharegpt.py @@ -86,6 +86,8 @@ def _load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): ) if ds_cfg and "strict" in ds_cfg and hasattr(strategy, "strict"): strategy.strict = ds_cfg["strict"] + if ds_cfg and "field_messages" in ds_cfg and hasattr(strategy, "messages"): + strategy.messages = ds_cfg["field_messages"] return strategy return _load @@ -97,6 +99,7 @@ class SimpleShareGPTPromptTokenizingStrategy(ShareGPTPromptTokenizingStrategy): """ _strict = False + _messages = "conversations" @property def strict(self): @@ -106,8 +109,16 @@ def strict(self): def strict(self, strict): self._strict = strict + @property + def messages(self): + return self._messages + + @messages.setter + def messages(self, messages): + self._messages = messages + def get_conversation_thread(self, prompt): - conversations = prompt["conversations"] + conversations = prompt[self.messages] if self.strict: return conversations role_key = "from" diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 31750ac159..b6eafa7a31 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -109,6 +109,7 @@ class SFTDataset(BaseModel): field: Optional[str] = None field_human: Optional[str] = None field_model: Optional[str] = None + field_messages: Optional[str] = None roles: Optional[Dict[str, List[str]]] = None From 367b2e879b05f047a1689e38d286ff6753b5ec3e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 23 May 2024 17:32:14 -0400 Subject: [PATCH 0020/1405] Switch to parallel FFD bin packing algorithm. (#1619) * Switch to parallel FFD bin packing algorithm. Add support for packing in a distributed context. Add packing efficiency estimate back. * revert changes to distributed code * chore: lint * fix config w new params for packing test * add sample_packing_group_size and sample_packing_bin_size to cfg schema * fix lamdbda function * fix sampler/dataloader calculations for packing --------- Co-authored-by: dsesclei --- docs/config.qmd | 5 + src/axolotl/core/trainer_builder.py | 70 ++--- .../config/models/input/v0_4_1/__init__.py | 2 + src/axolotl/utils/data/pretraining.py | 12 +- src/axolotl/utils/samplers/multipack.py | 250 +++++++----------- src/axolotl/utils/trainer.py | 19 +- tests/test_packed_batch_sampler.py | 28 +- tests/test_packed_pretraining.py | 2 + 8 files changed, 169 insertions(+), 219 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index 570a173f9a..bc44964dc2 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -186,6 +186,11 @@ eval_sample_packing: # The trainer will provide recommended values for these values. sample_packing_eff_est: total_num_tokens: +# Increasing the following values helps with packing, but usually only slightly (<%1.) +# The number of samples packed at a time. +sample_packing_group_size: 100000 +# The number of samples which can be packed into one sequence. Increase if using a large sequence_len with many short samples. +sample_packing_bin_size: 200 # Passed through to transformers when loading the model when launched without accelerate # Use `sequential` when training w/ model parallelism to limit memory diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index c510c8e107..a37652ade4 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -125,14 +125,22 @@ class AxolotlTrainingArguments(TrainingArguments): default=1.0, metadata={"help": "Sample packing efficiency for calculating batch length."}, ) + sample_packing_bin_size: int = field( + default=200, + metadata={ + "help": "The max number of samples that packed sample can contain after packing. Increase for better packing." + }, + ) + sample_packing_group_size: int = field( + default=100000, + metadata={ + "help": "The number of samples to group together for packing. Increase for better packing." + }, + ) max_seq_length: int = field( default=2048, metadata={"help": "The maximum sequence length the model can handle"}, ) - sample_packing_seq_len_multiplier: int = field( - default=1, - metadata={"help": "the multiplier for the max len for packed sequences"}, - ) relora_steps: Optional[int] = field( default=None, metadata={"help": "how often to reset for ReLoRA"}, @@ -346,11 +354,11 @@ def _get_train_sampler(self) -> Optional[torch.utils.data.Sampler]: ) return MultipackBatchSampler( RandomSampler(self.train_dataset), - batch_size=batch_size, - drop_last=True, - batch_max_len=batch_max_len, lengths=get_dataset_lengths(self.train_dataset), - packing_efficiency_estimate=self.args.sample_packing_efficiency, + batch_max_len=batch_max_len, + batch_size=batch_size, + group_size=self.args.sample_packing_group_size, + bin_size=self.args.sample_packing_bin_size, ) if self.args.curriculum_sampling: return SequentialSampler(self.train_dataset) @@ -370,11 +378,11 @@ def _get_eval_sampler( ) return MultipackBatchSampler( SequentialSampler(eval_dataset), - batch_size=batch_size, - drop_last=True, + lengths=get_dataset_lengths(self.eval_dataset), batch_max_len=batch_max_len, - lengths=get_dataset_lengths(eval_dataset), - packing_efficiency_estimate=self.args.sample_packing_efficiency, + batch_size=batch_size, + group_size=self.args.sample_packing_group_size, + bin_size=self.args.sample_packing_bin_size, ) return super()._get_eval_sampler(eval_dataset) @@ -1113,11 +1121,6 @@ def build(self, total_num_steps): if self.cfg.save_safetensors is not None: training_arguments_kwargs["save_safetensors"] = self.cfg.save_safetensors - if self.cfg.sample_packing_eff_est: - training_arguments_kwargs[ - "sample_packing_efficiency" - ] = self.cfg.sample_packing_eff_est - if self.cfg.dataloader_pin_memory is not None: training_arguments_kwargs[ "dataloader_pin_memory" @@ -1293,20 +1296,27 @@ def build(self, total_num_steps): training_arguments_kwargs["weight_decay"] = ( self.cfg.weight_decay if self.cfg.weight_decay is not None else 0.0 ) - training_arguments_kwargs["sample_packing"] = ( - self.cfg.sample_packing if self.cfg.sample_packing else False - ) - training_arguments_kwargs["multipack_real_batches"] = ( - self.cfg.flash_attention is not True - ) - training_arguments_kwargs["eval_sample_packing"] = ( - self.cfg.sample_packing - if self.cfg.eval_sample_packing is not False - else False - ) + + training_arguments_kwargs["sample_packing"] = bool(self.cfg.sample_packing) training_arguments_kwargs[ - "sample_packing_seq_len_multiplier" - ] = self.cfg.micro_batch_size + "multipack_real_batches" + ] = not self.cfg.flash_attention + training_arguments_kwargs["eval_sample_packing"] = bool( + self.cfg.eval_sample_packing + ) + if self.cfg.sample_packing_bin_size is not None: + training_arguments_kwargs[ + "sample_packing_bin_size" + ] = self.cfg.sample_packing_bin_size + if self.cfg.sample_packing_group_size is not None: + training_arguments_kwargs[ + "sample_packing_group_size" + ] = self.cfg.sample_packing_group_size + if self.cfg.sample_packing_eff_est: + training_arguments_kwargs[ + "sample_packing_efficiency" + ] = self.cfg.sample_packing_eff_est + if self.cfg.relora_steps: training_arguments_kwargs["relora_steps"] = self.cfg.relora_steps training_arguments_kwargs[ diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index b6eafa7a31..a14b66fa32 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -551,6 +551,8 @@ class Config: default=512, metadata={"help": "maximum prompt length for RL training"} ) sample_packing: Optional[bool] = None + sample_packing_group_size: Optional[int] = 100_000 + sample_packing_bin_size: Optional[int] = 200 eval_sample_packing: Optional[bool] = None pad_to_sequence_len: Optional[bool] = None curriculum_sampling: Optional[bool] = None diff --git a/src/axolotl/utils/data/pretraining.py b/src/axolotl/utils/data/pretraining.py index 544ed13162..e056c7f509 100644 --- a/src/axolotl/utils/data/pretraining.py +++ b/src/axolotl/utils/data/pretraining.py @@ -150,6 +150,8 @@ def wrap_pretraining_dataset( max_seq_length=max_tokens, batch_size=batch_size, multipack_attn=cfg.pretrain_multipack_attn, + group_size=cfg.sample_packing_group_size, + bin_size=cfg.sample_packing_bin_size, ) # set this to 1 so downstream data_loader doesn't try to increase the batch again cfg.micro_batch_size = 1 @@ -189,6 +191,8 @@ def encode_packed_pretraining( max_seq_length: int = 2048, batch_size: int = 4, multipack_attn: Optional[bool] = False, + group_size: int = 100000, + bin_size: int = 200, ) -> Dict[str, List]: # pylint: disable=duplicate-code # tokenize all the examples @@ -202,11 +206,13 @@ def encode_packed_pretraining( ) sampler = MultipackBatchSampler( - RandomSampler(train_dataset), + sampler=RandomSampler(train_dataset), + lengths=get_dataset_lengths(train_dataset), batch_size=1, - drop_last=True, batch_max_len=batch_size * max_seq_length, - lengths=get_dataset_lengths(train_dataset), + group_size=group_size, + bin_size=bin_size, + drop_last=True, ) chunked_data = defaultdict(list) diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index cf47d9639b..07fd056826 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -1,105 +1,64 @@ -# pylint: skip-file """ Multipack Batch Sampler """ import logging -import math -import os -from typing import Any, Iterable, List, Union +from concurrent.futures import ProcessPoolExecutor +from multiprocessing import cpu_count import numba import numpy as np -from torch.utils.data import BatchSampler, Sampler +from torch.utils.data import BatchSampler LOG = logging.getLogger("axolotl.utils.samplers.multipack") +# First-fit-decreasing bin packing. @numba.njit -def ffd_check(a: np.ndarray, c: int, n: int): - # First-fit-decreasing bin packing - # Check if a[] could fit in n bins with capacity c - # https://en.wikipedia.org/wiki/First-fit-decreasing_bin_packing - - a = np.sort(a)[::-1] - bins = np.full((n,), c, dtype=a.dtype) - for size in a: - not_found = True - for idx in range(n): - if bins[idx] >= size: - bins[idx] -= size - not_found = False +def pack_group(items, group_offset, bin_capacity, max_items_per_bin): + idxs = np.argsort(items)[::-1] + sorted_items = items[idxs] + num_bins = len(items) + bins = np.full(num_bins, bin_capacity, dtype=np.int32) + bin_counts = np.zeros(num_bins, dtype=np.int32) + group_packing = np.full((num_bins, max_items_per_bin), -1, dtype=np.int32) + + for idx, item in enumerate(sorted_items): + global_idx = idxs[idx] + group_offset + + placed = False + for i in range(num_bins): + if bins[i] >= item and bin_counts[i] < max_items_per_bin: + bins[i] -= item + group_packing[i, bin_counts[i]] = global_idx + bin_counts[i] += 1 + placed = True break - if not_found: - return False + if not placed: + raise ValueError( + f"Item could not be packed. Try increasing cfg.sample_packing_bin_size ({max_items_per_bin})." + ) - return True + return group_packing -@numba.njit -def ffd_with_result(a: np.ndarray, c: int, start_index: int): - # First-fit-decreasing bin packing (with result return) - - indices = np.argsort(a)[::-1] - a = a[indices] - - bins: List[Any] = [] - bins_result: List[Any] = [] - for a_id, size in enumerate(a): - add_new = True - for idx in range(len(bins)): - if bins[idx] >= size: - bins[idx] -= size - bins_result[idx].append(indices[a_id] + start_index) - add_new = False - break - - if add_new: - bins.append(c - size) - bins_result.append([indices[a_id] + start_index]) - - return bins_result - - -@numba.njit -def allocate( - lengths: np.ndarray, lengths_cumsum: np.ndarray, rank: int, c: int, n: int -): - # Dynamic batch allocator, similar to Multifit - # https://en.wikipedia.org/wiki/Multifit_algorithm - # ~99.5% efficiency on OpenChat training set (12 * 2048 ctx len) - - s = 0 - start_index = 0 - result = [] - - while True: - # binary search [l, r) - left = 1 - right = 1 + np.searchsorted(lengths_cumsum[start_index:], s + c * n, "right") - - while right - left > 1: - mid = (left + right) // 2 - if ffd_check(lengths[start_index : start_index + mid], c, n): - left = mid - else: - right = mid - - # use length l - batch = ffd_with_result( - lengths[start_index : start_index + left], c, start_index - ) - assert len(batch) <= n - if len(batch) < n: - break - - start_index += left - s = lengths_cumsum[start_index - 1] +def pack(items, bin_capacity, group_size, max_items_per_bin): + num_items = len(items) + num_processes = max(1, min(num_items // group_size, cpu_count())) + tasks = [ + (items[i : i + group_size], i, bin_capacity, max_items_per_bin) + for i in range(0, num_items, group_size) + ] - # add local rank - result.append(batch[rank]) + packed_bins = [] + with ProcessPoolExecutor(max_workers=num_processes) as executor: + for group_packing in executor.map(pack_group, *zip(*tasks)): + for bin_pack in group_packing: + filtered_pack = bin_pack[bin_pack != -1] + if filtered_pack.size > 0: + packed_bins.append(filtered_pack.tolist()) - return result, s, len(result) * c * n + return packed_bins class MultipackBatchSampler(BatchSampler): @@ -109,94 +68,63 @@ class MultipackBatchSampler(BatchSampler): def __init__( self, - sampler: Union[Sampler[int], Iterable[int]], - batch_size: int, - drop_last: bool, - batch_max_len: int, - lengths: np.ndarray, - packing_efficiency_estimate: float = 1.0, + sampler, + lengths, + batch_max_len, + batch_size, + group_size=100_000, + bin_size=200, + drop_last=False, ): - super().__init__(sampler, batch_size, drop_last) - self.batch_size = batch_size + self.sampler = sampler + self.lengths = np.array(lengths, dtype=np.int32) self.batch_max_len = batch_max_len - self.lengths: np.ndarray = lengths - self.packing_efficiency_estimate = packing_efficiency_estimate or 1.0 - - assert isinstance(self.lengths, np.ndarray) - - self.epoch = 0 - - # statistics - self.eff_total_used = 0 - self.eff_total_slots = 0 - - def set_epoch(self, epoch: int): - self.epoch = epoch - - def generate_batches(self, set_stats=False): - indices = [idx for idx in self.sampler] + self.batch_size = batch_size + self.group_size = group_size + self.bin_size = bin_size + self.drop_last = drop_last - lengths = self.lengths[indices] - lengths_cumsum = np.cumsum(lengths) + self._efficiency = None + self._batches = None - batches, total_used, total_slots = allocate( - lengths=lengths, - lengths_cumsum=lengths_cumsum, - rank=0, - c=self.batch_max_len, - n=1, + def efficiency(self): + if self._efficiency is None: + self._batches = self._pack_batches() + return self._efficiency + + def _pack_batches(self): + # Get possibly shuffled indices from sampler. + sample_idxs = np.arange(len(self.sampler)) + lengths = self.lengths[sample_idxs] + + pack_idxs = pack( + lengths, + self.batch_max_len, + self.group_size, + self.bin_size, ) - batches = [ - [ - [indices[b_idx] for b_idx in batch] - for batch in batches[i : i + self.batch_size] - ] - for i in range(0, len(batches), self.batch_size) + used_tokens = self.lengths.sum() + available_tokens = len(pack_idxs) * self.batch_max_len + self._efficiency = used_tokens / available_tokens + + # Wrap packs into batches. + batch_idxs = [ + pack_idxs[i : i + self.batch_size] + for i in range(0, len(pack_idxs), self.batch_size) ] - # statistics - if set_stats: - self.eff_total_used += total_used - self.eff_total_slots += total_slots + # Drop last batch if needed. + if self.drop_last and len(batch_idxs[-1]) < self.batch_size: + batch_idxs = batch_idxs[:-1] - return batches + return batch_idxs def __iter__(self): - batches = self.generate_batches(set_stats=True) - return iter(batches) - - def num_batches(self): - batches = self.generate_batches(set_stats=True) - return len(batches) - - def efficiency(self): - return self.eff_total_used / self.eff_total_slots + self._batches = self._pack_batches() + return iter(self._batches) def __len__(self): - self.num_batches() - return self._len_est() - - def _len_est(self): - world_size = int(os.getenv("WORLD_SIZE", "1")) - lengths_sum = np.sum(self.lengths) - lengths_sum_per_device = lengths_sum // world_size - LOG.info( - f"packing_efficiency_estimate: {self.packing_efficiency_estimate} " - f"total_num_tokens per device: {lengths_sum_per_device}" - ) - - # shave off 1% + 1 for dealing with variance in packing from random sampler to sampler - return max( - 0, - ( - world_size - * math.floor( - 0.99 - * lengths_sum_per_device - / self.packing_efficiency_estimate - // (self.batch_max_len * self.batch_size) - ) - - 1 - ), - ) + if self._batches is None: + self._batches = self._pack_batches() + return len(self._batches) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 83977ef064..6760dc4882 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -341,27 +341,26 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): ) else: if cfg.flash_attention: - batch_size = 1 + sampler_batch_size = 1 batch_max_len = cfg.micro_batch_size * cfg.sequence_len else: - batch_size = cfg.micro_batch_size + sampler_batch_size = cfg.micro_batch_size batch_max_len = cfg.sequence_len sampler = MultipackBatchSampler( sampler=RandomSampler(train_dataset), - batch_size=batch_size, - drop_last=True, - batch_max_len=batch_max_len, lengths=get_dataset_lengths(train_dataset), + batch_size=sampler_batch_size, + batch_max_len=batch_max_len, + group_size=cfg.sample_packing_group_size, + bin_size=cfg.sample_packing_bin_size, + drop_last=True, ) data_loader = DataLoader( train_dataset.remove_columns(["length"]), batch_sampler=sampler, ) - data_loader_len = len(data_loader) // ( - cfg.world_size * cfg.gradient_accumulation_steps - ) - actual_eff = sampler.efficiency() + data_loader_len = len(data_loader) * cfg.micro_batch_size // cfg.batch_size LOG.debug(f"data_loader_len: {data_loader_len}", main_process_only=True) # FIXME: is there a bug here somewhere? the total num steps depends # on the agreed on value for sample_packing_eff_est @@ -372,7 +371,7 @@ def calc_sample_packing_eff_est(estimates: List[float]): return max(estimates) sample_packing_actual_eff_all = reduce_and_broadcast( - lambda: actual_eff, + lambda: sampler.efficiency(), # pylint: disable=unnecessary-lambda calc_sample_packing_eff_est, ) sample_packing_eff_est = ( diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index 50f39d60f5..ceff11df94 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -62,12 +62,14 @@ def test_packing(self, batch_size, num_workers, tokenizer, max_seq_length): dataset, ) train_dataset = concatenate_datasets([dataset_wrapper]) + lengths = get_dataset_lengths(train_dataset) batch_sampler = MultipackBatchSampler( sampler=RandomSampler(train_dataset), + lengths=lengths, batch_size=batch_size, - drop_last=True, batch_max_len=max_seq_length, - lengths=get_dataset_lengths(train_dataset), + group_size=100000, + bin_size=200, ) loader = DataLoader( @@ -81,19 +83,15 @@ def test_packing(self, batch_size, num_workers, tokenizer, max_seq_length): ), num_workers=num_workers, ) - inputs = next(iter(loader)) - assert inputs["input_ids"].shape == (batch_size, max_seq_length) - assert inputs["labels"].shape == (batch_size, max_seq_length) - assert inputs["attention_mask"].shape == (batch_size, max_seq_length) + batch_idxs = [] + for batch in batch_sampler: + for pack in batch: + batch_idxs.extend(pack) - assert inputs["input_ids"].tolist()[0][0] == 2 - assert inputs["labels"].tolist()[0][0] == -100 - assert inputs["attention_mask"].tolist()[0][0] == 0 - assert inputs["attention_mask"].tolist()[0][-1] > 1 + for batch in loader: + assert len(batch["input_ids"]) <= batch_size * max_seq_length + assert batch["input_ids"].shape[1] == max_seq_length - if batch_size >= 2: - assert inputs["input_ids"].tolist()[1][0] == 2 - assert inputs["labels"].tolist()[1][0] == -100 - assert inputs["attention_mask"].tolist()[1][0] == 0 - assert inputs["attention_mask"].tolist()[1][-1] > 1 + original_idxs = set(range(len(train_dataset))) + assert original_idxs == set(batch_idxs) diff --git a/tests/test_packed_pretraining.py b/tests/test_packed_pretraining.py index 528f9c8074..fb623a43dc 100644 --- a/tests/test_packed_pretraining.py +++ b/tests/test_packed_pretraining.py @@ -42,6 +42,8 @@ def test_packing_stream_dataset(self): "pad_to_sequence_len": True, "sequence_len": 2048, "micro_batch_size": 2, + "sample_packing_group_size": 100000, + "sample_packing_bin_size": 200, } ) From 8a20a7b711a62d7b04e742f3d6034b4ca8aa27d2 Mon Sep 17 00:00:00 2001 From: Charles Frye Date: Fri, 24 May 2024 14:15:44 -0400 Subject: [PATCH 0021/1405] document how to use `share_strategy="no"` (#1653) [skip ci] The literal value `no` is parsed in some YAML parsers to the boolean `False`, which fails Pydantic validation. To be sure that the value is parsed to the string `"no"`, the value should be enclosed in quotes. [Discussion on StackOverflow](https://stackoverflow.com/questions/53648244/specifying-the-string-value-yes-in-yaml). --- docs/config.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.qmd b/docs/config.qmd index bc44964dc2..1c87386a6d 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -290,7 +290,7 @@ lr_quadratic_warmup: logging_steps: eval_steps: # Leave empty to eval at each epoch, integers for every N steps. decimal for fraction of total steps evals_per_epoch: # number of times per epoch to run evals, mutually exclusive with eval_steps -save_strategy: # Set to `no` to skip checkpoint saves +save_strategy: # Set to `"no"` to skip checkpoint saves save_steps: # Leave empty to save at each epoch saves_per_epoch: # number of times per epoch to save a checkpoint, mutually exclusive with save_steps save_total_limit: # Checkpoints saved at a time From ef223519c9dba8334ca13b60b4619e9b61f01373 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 28 May 2024 11:23:34 -0400 Subject: [PATCH 0022/1405] update deps (#1663) [skip ci] * update deps and tweak logic so axolotl is pip installable * use vcs url format * using dependency_links isn't supported per docs) --- .github/workflows/tests.yml | 5 +++++ requirements.txt | 8 ++++---- setup.py | 16 +++++++++++----- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f25eddc31..db9173cac4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -84,6 +84,11 @@ jobs: python_version: "3.11" pytorch: 2.2.2 num_gpus: 1 + - cuda: 121 + cuda_version: 12.1.0 + python_version: "3.11" + pytorch: 2.3.0 + num_gpus: 1 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/requirements.txt b/requirements.txt index 6723b17982..557c5293fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 -peft==0.10.0 -transformers==4.40.2 +peft==0.11.1 +transformers==4.41.1 tokenizers==0.19.1 bitsandbytes==0.43.1 accelerate==0.30.1 @@ -16,7 +16,7 @@ flash-attn==2.5.8 sentencepiece wandb einops -xformers==0.0.23.post1 +xformers==0.0.26.post1 optimum==1.16.2 hf_transfer colorama @@ -39,6 +39,6 @@ s3fs gcsfs # adlfs -trl==0.8.5 +trl==0.8.6 zstandard==0.22.0 fastcore diff --git a/setup.py b/setup.py index 31a6d67164..3d1537edff 100644 --- a/setup.py +++ b/setup.py @@ -30,8 +30,11 @@ def parse_requirements(): try: if "Darwin" in platform.system(): - _install_requires.pop(_install_requires.index("xformers==0.0.23.post1")) + # don't install xformers on MacOS + _install_requires.pop(_install_requires.index("xformers==0.0.26.post1")) else: + # detect the version of torch already installed + # and set it so dependencies don't clobber the torch version torch_version = version("torch") _install_requires.append(f"torch=={torch_version}") @@ -46,11 +49,14 @@ def parse_requirements(): raise ValueError("Invalid version format") if (major, minor) >= (2, 3): - _install_requires.pop(_install_requires.index("xformers==0.0.23.post1")) - _install_requires.append("xformers>=0.0.26.post1") + pass elif (major, minor) >= (2, 2): - _install_requires.pop(_install_requires.index("xformers==0.0.23.post1")) + _install_requires.pop(_install_requires.index("xformers==0.0.26.post1")) _install_requires.append("xformers>=0.0.25.post1") + else: + _install_requires.pop(_install_requires.index("xformers==0.0.26.post1")) + _install_requires.append("xformers>=0.0.23.post1") + except PackageNotFoundError: pass @@ -62,7 +68,7 @@ def parse_requirements(): setup( name="axolotl", - version="0.4.0", + version="0.4.1", description="LLM Trainer", long_description="Axolotl is a tool designed to streamline the fine-tuning of various AI models, offering support for multiple configurations and architectures.", package_dir={"": "src"}, From 5f9106404013326b255c0e572de0ec94081cc9e6 Mon Sep 17 00:00:00 2001 From: Maciek Date: Tue, 28 May 2024 17:23:52 +0200 Subject: [PATCH 0023/1405] Fix Google Colab notebook 2024-05 (#1662) [skip ci] * include mlflow installation in the colab notebook Without explicitly installing mlflow the `accelerate launch` command fails. * update the colab noteboko to use the latest tinyllama config --- .../colab-axolotl-example.ipynb | 427 +++++++++--------- 1 file changed, 217 insertions(+), 210 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index fc3b761949..bb6167e582 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -1,216 +1,223 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "AKjdG7tbTb-n" - }, - "source": [ - "# Example notebook for running Axolotl on google colab" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "RcbNpOgWRcii" - }, - "outputs": [], - "source": [ - "import torch\n", - "# Check so there is a gpu available, a T4(free tier) is enough to run this notebook\n", - "assert (torch.cuda.is_available()==True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "h3nLav8oTRA5" - }, - "source": [ - "## Install Axolotl and dependencies" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "3c3yGAwnOIdi", - "outputId": "e3777b5a-40ef-424f-e181-62dfecd1dd01" - }, - "outputs": [], - "source": [ - "!pip install torch==\"2.1.2\"\n", - "!pip install -e git+https://github.com/OpenAccess-AI-Collective/axolotl#egg=axolotl\n", - "!pip install flash-attn==\"2.5.0\"\n", - "!pip install deepspeed==\"0.13.1\"" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "BW2MFr7HTjub" - }, - "source": [ - "## Create an yaml config file" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "9pkF2dSoQEUN" - }, - "outputs": [], - "source": [ - "import yaml\n", - "\n", - "# Your YAML string\n", - "yaml_string = \"\"\"\n", - "base_model: TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T\n", - "model_type: LlamaForCausalLM\n", - "tokenizer_type: LlamaTokenizer\n", - "is_llama_derived_model: true\n", - "\n", - "load_in_8bit: false\n", - "load_in_4bit: true\n", - "strict: false\n", - "\n", - "datasets:\n", - " - path: mhenrichsen/alpaca_2k_test\n", - " type: alpaca\n", - "dataset_prepared_path:\n", - "val_set_size: 0.05\n", - "output_dir: ./outputs/qlora-out\n", - "\n", - "adapter: qlora\n", - "lora_model_dir:\n", - "\n", - "sequence_len: 1096\n", - "sample_packing: true\n", - "pad_to_sequence_len: true\n", - "\n", - "lora_r: 32\n", - "lora_alpha: 16\n", - "lora_dropout: 0.05\n", - "lora_target_modules:\n", - "lora_target_linear: true\n", - "lora_fan_in_fan_out:\n", - "\n", - "wandb_project:\n", - "wandb_entity:\n", - "wandb_watch:\n", - "wandb_name:\n", - "wandb_log_model:\n", - "\n", - "mlflow_experiment_name: colab-example\n", - "\n", - "gradient_accumulation_steps: 1\n", - "micro_batch_size: 1\n", - "num_epochs: 4\n", - "max_steps: 20\n", - "optimizer: paged_adamw_32bit\n", - "lr_scheduler: cosine\n", - "learning_rate: 0.0002\n", - "\n", - "train_on_inputs: false\n", - "group_by_length: false\n", - "bf16: false\n", - "fp16: true\n", - "tf32: false\n", - "\n", - "gradient_checkpointing: true\n", - "early_stopping_patience:\n", - "resume_from_checkpoint:\n", - "local_rank:\n", - "logging_steps: 1\n", - "xformers_attention:\n", - "flash_attention: false\n", - "\n", - "warmup_steps: 10\n", - "evals_per_epoch:\n", - "saves_per_epoch:\n", - "debug:\n", - "deepspeed:\n", - "weight_decay: 0.0\n", - "fsdp:\n", - "fsdp_config:\n", - "special_tokens:\n", - "\n", - "\"\"\"\n", - "\n", - "# Convert the YAML string to a Python dictionary\n", - "yaml_dict = yaml.safe_load(yaml_string)\n", - "\n", - "# Specify your file path\n", - "file_path = 'test_axolotl.yaml'\n", - "\n", - "# Write the YAML file\n", - "with open(file_path, 'w') as file:\n", - " yaml.dump(yaml_dict, file)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "bidoj8YLTusD" - }, - "source": [ - "## Launch the training" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "ydTI2Jk2RStU", - "outputId": "d6d0df17-4b53-439c-c802-22c0456d301b" - }, - "outputs": [], - "source": [ - "# Buy using the ! the comand will be executed as a bash command\n", - "!accelerate launch -m axolotl.cli.train /content/test_axolotl.yaml" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Play with inference" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Buy using the ! the comand will be executed as a bash command\n", - "!accelerate launch -m axolotl.cli.inference /content/test_axolotl.yaml \\\n", - " --qlora_model_dir=\"./qlora-out\" --gradio" - ] - } - ], - "metadata": { - "accelerator": "GPU", + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "AKjdG7tbTb-n" + }, + "source": [ + "# Example notebook for running Axolotl on google colab" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "RcbNpOgWRcii" + }, + "outputs": [], + "source": [ + "import torch\n", + "# Check so there is a gpu available, a T4(free tier) is enough to run this notebook\n", + "assert (torch.cuda.is_available()==True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "h3nLav8oTRA5" + }, + "source": [ + "## Install Axolotl and dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { "colab": { - "gpuType": "T4", - "provenance": [] + "base_uri": "https://localhost:8080/" }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" + "id": "3c3yGAwnOIdi", + "outputId": "e3777b5a-40ef-424f-e181-62dfecd1dd01" + }, + "outputs": [], + "source": [ + "!pip install torch==\"2.1.2\"\n", + "!pip install -e git+https://github.com/OpenAccess-AI-Collective/axolotl#egg=axolotl\n", + "!pip install flash-attn==\"2.5.0\"\n", + "!pip install deepspeed==\"0.13.1\"!pip install mlflow==\"2.13.0\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BW2MFr7HTjub" + }, + "source": [ + "## Create an yaml config file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9pkF2dSoQEUN" + }, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "# Your YAML string\n", + "yaml_string = \"\"\"\n", + "base_model: TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T\n", + "model_type: LlamaForCausalLM\n", + "tokenizer_type: LlamaTokenizer\n", + "\n", + "load_in_8bit: false\n", + "load_in_4bit: true\n", + "strict: false\n", + "\n", + "datasets:\n", + " - path: mhenrichsen/alpaca_2k_test\n", + " type: alpaca\n", + "dataset_prepared_path:\n", + "val_set_size: 0.05\n", + "output_dir: ./outputs/qlora-out\n", + "\n", + "adapter: qlora\n", + "lora_model_dir:\n", + "\n", + "sequence_len: 4096\n", + "sample_packing: true\n", + "eval_sample_packing: false\n", + "pad_to_sequence_len: true\n", + "\n", + "lora_r: 32\n", + "lora_alpha: 16\n", + "lora_dropout: 0.05\n", + "lora_target_modules:\n", + "lora_target_linear: true\n", + "lora_fan_in_fan_out:\n", + "\n", + "wandb_project:\n", + "wandb_entity:\n", + "wandb_watch:\n", + "wandb_name:\n", + "wandb_log_model:\n", + "\n", + "gradient_accumulation_steps: 4\n", + "micro_batch_size: 2\n", + "num_epochs: 4\n", + "optimizer: paged_adamw_32bit\n", + "lr_scheduler: cosine\n", + "learning_rate: 0.0002\n", + "\n", + "train_on_inputs: false\n", + "group_by_length: false\n", + "bf16: auto\n", + "fp16:\n", + "tf32: false\n", + "\n", + "gradient_checkpointing: true\n", + "early_stopping_patience:\n", + "resume_from_checkpoint:\n", + "local_rank:\n", + "logging_steps: 1\n", + "xformers_attention:\n", + "flash_attention: true\n", + "\n", + "warmup_steps: 10\n", + "evals_per_epoch: 4\n", + "saves_per_epoch: 1\n", + "debug:\n", + "deepspeed:\n", + "weight_decay: 0.0\n", + "fsdp:\n", + "fsdp_config:\n", + "special_tokens:\n", + "\n", + "\"\"\"\n", + "\n", + "# Convert the YAML string to a Python dictionary\n", + "yaml_dict = yaml.safe_load(yaml_string)\n", + "\n", + "# Specify your file path\n", + "file_path = 'test_axolotl.yaml'\n", + "\n", + "# Write the YAML file\n", + "with open(file_path, 'w') as file:\n", + " yaml.dump(yaml_dict, file)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bidoj8YLTusD" + }, + "source": [ + "## Launch the training" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, - "language_info": { - "name": "python" - } + "id": "ydTI2Jk2RStU", + "outputId": "d6d0df17-4b53-439c-c802-22c0456d301b" + }, + "outputs": [], + "source": [ + "# Buy using the ! the comand will be executed as a bash command\n", + "!accelerate launch -m axolotl.cli.train /content/test_axolotl.yaml" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Play with inference" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Buy using the ! the comand will be executed as a bash command\n", + "!accelerate launch -m axolotl.cli.inference /content/test_axolotl.yaml \\\n", + " --qlora_model_dir=\"./qlora-out\" --gradio" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 4 } From cc11c6bce2c820e40d035c4028d0e320b13eeb76 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Wed, 29 May 2024 00:24:13 +0900 Subject: [PATCH 0024/1405] Generalizing the chat_template prompt strategy (#1660) [skip ci] The strategy now supports configuring several fields: * The data field holding message arrays * the role and content fields for each message * role mapping from source to target types additionally this adds a sample llama3-8b instruct template using the chat template --- examples/llama-3/instruct-lora-8b.yml | 76 ++++++++++++ .../prompt_strategies/chat_template.py | 84 ++++++++++--- .../config/models/input/v0_4_1/__init__.py | 2 + .../prompt_strategies/test_chat_templates.py | 113 ++++++++++++++++++ 4 files changed, 258 insertions(+), 17 deletions(-) create mode 100644 examples/llama-3/instruct-lora-8b.yml diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml new file mode 100644 index 0000000000..754c9ad5c0 --- /dev/null +++ b/examples/llama-3/instruct-lora-8b.yml @@ -0,0 +1,76 @@ +base_model: meta-llama/Meta-Llama-3-8B-Instruct +model_type: LlamaForCausalLM +tokenizer_type: AutoTokenizer + +load_in_8bit: true +load_in_4bit: false +strict: false + +chat_template: llama3 +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + chat_template: llama3 + field_messages: messages + message_field_role: role + message_field_content: content + roles: + user: + - user + assistant: + - assistant + +dataset_prepared_path: +val_set_size: 0.05 +output_dir: ./outputs/lora-out + +sequence_len: 4096 +sample_packing: false +pad_to_sequence_len: true + +adapter: lora +lora_model_dir: +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_fan_in_fan_out: + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true +s2_attention: + +warmup_steps: 10 +evals_per_epoch: 4 +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 8dff3845b7..b052469dc4 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -1,24 +1,55 @@ """ HF Chat Templates prompt strategy """ -from typing import Any, Dict, Optional + +import logging +from typing import Any, Dict, List, Optional from axolotl.prompt_tokenizers import PromptTokenizingStrategy from axolotl.prompters import Prompter from axolotl.utils.chat_templates import chat_templates +LOG = logging.getLogger("axolotl") + class ChatTemplatePrompter(Prompter): """prompter for HF chat templates""" - def __init__(self, tokenizer, chat_template=None, max_length=2048): + def __init__( + self, + tokenizer, + chat_template=None, + max_length=2048, + message_field_role: str = "from", + message_field_content: str = "value", + roles: Optional[Dict[str, List[str]]] = None, + ): + if roles: + self.roles = {s: t for t, sources in roles.items() for s in sources} + else: + self.roles = { + "human": "user", + "user": "user", + "assistant": "assistant", + "gpt": "assistant", + } + self.message_field_role = message_field_role + self.message_field_content = message_field_content self.tokenizer = tokenizer self.chat_template = chat_template self.max_length = max_length def build_prompt(self, conversation, add_generation_prompt=False): + turns = [ + { + "role": self.roles[t[self.message_field_role]], + "content": t[self.message_field_content], + } + for t in conversation + ] + return self.tokenizer.apply_chat_template( - conversation, + turns, truncation=True, max_length=self.max_length, add_generation_prompt=add_generation_prompt, @@ -31,9 +62,19 @@ class ChatTemplateStrategy(PromptTokenizingStrategy): Tokenizing strategy for instruction-based prompts. """ + _messages = "conversations" + + @property + def messages(self): + return self._messages + + @messages.setter + def messages(self, messages): + self._messages = messages + def tokenize_prompt(self, prompt): turns = self.get_conversation_thread(prompt) - prompt_ids = self.prompter.build_prompt([turns[0]], add_generation_prompt=True) + prompt_ids = self.prompter.build_prompt(turns[:-1], add_generation_prompt=True) input_ids = self.prompter.build_prompt(turns) if not self.train_on_inputs: @@ -51,28 +92,37 @@ def tokenize_prompt(self, prompt): return tokenized_prompt def get_conversation_thread(self, prompt): - conversations = prompt["conversations"] - # remap roles - allow for assistant turn - role_map = { - "human": "user", - "user": "user", - "assistant": "assistant", - "gpt": "assistant", - } - turns = [ - {"role": role_map[t["from"]], "content": t["value"]} for t in conversations - ] - return turns + return prompt[self.messages] def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): chat_template = ( ds_cfg["chat_template"] if ds_cfg and "chat_template" in ds_cfg else "chatml" ) + message_field_role = ( + ds_cfg["message_field_role"] + if ds_cfg and "message_field_role" in ds_cfg + else "from" + ) + message_field_content = ( + ds_cfg["message_field_content"] + if ds_cfg and "message_field_content" in ds_cfg + else "value" + ) + roles = ds_cfg["roles"] if ds_cfg and "roles" in ds_cfg else None + strategy = ChatTemplateStrategy( - ChatTemplatePrompter(tokenizer, chat_templates(chat_template)), + ChatTemplatePrompter( + tokenizer, + chat_templates(chat_template), + message_field_role=message_field_role, + message_field_content=message_field_content, + roles=roles, + ), tokenizer, cfg.train_on_inputs, cfg.sequence_len, ) + if ds_cfg and "field_messages" in ds_cfg and hasattr(strategy, "messages"): + strategy.messages = ds_cfg["field_messages"] return strategy diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index a14b66fa32..f363ebfdce 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -110,6 +110,8 @@ class SFTDataset(BaseModel): field_human: Optional[str] = None field_model: Optional[str] = None field_messages: Optional[str] = None + message_field_role: Optional[str] = None + message_field_content: Optional[str] = None roles: Optional[Dict[str, List[str]]] = None diff --git a/tests/prompt_strategies/test_chat_templates.py b/tests/prompt_strategies/test_chat_templates.py index 1076c6a3bf..7b58a12369 100644 --- a/tests/prompt_strategies/test_chat_templates.py +++ b/tests/prompt_strategies/test_chat_templates.py @@ -1,6 +1,7 @@ """ tests for chat_template prompt strategy """ + import unittest import pytest @@ -10,8 +11,39 @@ from axolotl.prompt_strategies.chat_template import ( ChatTemplatePrompter, ChatTemplateStrategy, + load, ) from axolotl.utils.chat_templates import chat_templates +from axolotl.utils.dict import DictDefault + + +@pytest.fixture(name="assistant_dataset") +def fixture_assistant_dataset(): + # pylint: disable=duplicate-code + return Dataset.from_list( + [ + { + "messages": [ + { + "role": "user", + "content": "hello", + }, + { + "role": "assistant", + "content": "hello", + }, + { + "role": "user", + "content": "goodbye", + }, + { + "role": "assistant", + "content": "goodbye", + }, + ] + } + ] + ) @pytest.fixture(name="sharegpt_dataset") @@ -51,6 +83,87 @@ def fixture_llama3_tokenizer(): return tokenizer +class TestAssistantChatTemplateLlama3: + """ + Test class for assistant style datasets with llama-3 prompts using the chat_template strategy. + """ + + def test_llama3_load(self, llama3_tokenizer, assistant_dataset): + # pylint: disable=duplicate-code + strategy = load( + llama3_tokenizer, + DictDefault( + { + "train_on_inputs": False, + "sequence_len": 512, + } + ), + DictDefault( + { + "chat_template": "llama3", + "message_field_role": "role", + "message_field_content": "content", + "roles": { + "user": ["user"], + "assistant": ["assistant"], + "system": ["system"], + }, + "field_messages": "messages", + } + ), + ) + res = strategy.tokenize_prompt(assistant_dataset[0]) + input_ids = res["input_ids"] + # fmt: off + assert input_ids == [ + 128000, # bos + 128006, 882, 128007, # user header + 271, 15339, 128009, # user prompt eot + 128006, 78191, 128007, # assistant header + 271, 15339, 128009, # assistant response eot + 128006, 882, 128007, + 271, 19045, 29474, 128009, + 128006, 78191, 128007, + 271, 19045, 29474, 128009, + ] + # fmt: on + + def test_llama3(self, llama3_tokenizer, assistant_dataset): + # pylint: disable=duplicate-code + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + llama3_tokenizer, + chat_templates("llama3"), + message_field_role="role", + message_field_content="content", + roles={ + "user": ["user"], + "assistant": ["assistant"], + "system": ["system"], + }, + ), + llama3_tokenizer, + False, + 512, + ) + strategy.messages = "messages" + res = strategy.tokenize_prompt(assistant_dataset[0]) + input_ids = res["input_ids"] + # fmt: off + assert input_ids == [ + 128000, # bos + 128006, 882, 128007, # user header + 271, 15339, 128009, # user prompt eot + 128006, 78191, 128007, # assistant header + 271, 15339, 128009, # assistant response eot + 128006, 882, 128007, + 271, 19045, 29474, 128009, + 128006, 78191, 128007, + 271, 19045, 29474, 128009, + ] + # fmt: on + + class TestSharegptChatTemplateLlama3: """ Test class for ShareGPT style datasets with llama-3 prompts using the chat_template strategy. From 230e0ac363b58f48ff32c7e790fbb17859e32f41 Mon Sep 17 00:00:00 2001 From: Faria Huq Date: Tue, 28 May 2024 11:25:08 -0400 Subject: [PATCH 0025/1405] Fix Lora config error for Llama3 (#1659) The current yml code throws an error: ValueError: Please set lora_modules_to_save to [`embed_tokens`, `lm_head`] when using an adapter and changing the special tokens. I added the required changes to resolve it --- examples/llama-3/lora-8b.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/llama-3/lora-8b.yml b/examples/llama-3/lora-8b.yml index 6b0ebaed86..cd21effb9a 100644 --- a/examples/llama-3/lora-8b.yml +++ b/examples/llama-3/lora-8b.yml @@ -24,6 +24,9 @@ lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true lora_fan_in_fan_out: +lora_modules_to_save: + - embed_tokens + - lm_head wandb_project: wandb_entity: From f5febc729a65f4438d3003136485d66a86fa37bc Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 28 May 2024 11:36:50 -0400 Subject: [PATCH 0026/1405] fix lint issue that snuck through (#1665) --- examples/llama-3/lora-8b.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/llama-3/lora-8b.yml b/examples/llama-3/lora-8b.yml index cd21effb9a..dfc881faf9 100644 --- a/examples/llama-3/lora-8b.yml +++ b/examples/llama-3/lora-8b.yml @@ -24,7 +24,7 @@ lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true lora_fan_in_fan_out: -lora_modules_to_save: +lora_modules_to_save: - embed_tokens - lm_head From 6a5a725f10c87bf1ccd469e0d8e67f048a225cca Mon Sep 17 00:00:00 2001 From: Davide Caroselli Date: Tue, 28 May 2024 18:00:32 +0200 Subject: [PATCH 0027/1405] Fix: ensure correct handling of `val_set_size` as `float` or `int` (#1655) * Fix: ensure correct handling of val_set_size as float or int * chore: lint --------- Co-authored-by: Wing Lian --- src/axolotl/utils/data/sft.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index dbc4172b4b..6083e30bef 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -494,7 +494,9 @@ def load_prepare_datasets( test_fingerprint = md5(to_hash_test) dataset = dataset.train_test_split( - test_size=cfg.val_set_size, + test_size=int(cfg.val_set_size) + if cfg.val_set_size == int(cfg.val_set_size) + else cfg.val_set_size, shuffle=False, seed=cfg.seed or 42, train_new_fingerprint=train_fingerprint, From 65db903714b379387eedff000b0dd5e0a8b84f6e Mon Sep 17 00:00:00 2001 From: Seungduk Kim Date: Wed, 29 May 2024 07:10:29 +0900 Subject: [PATCH 0028/1405] Correct name of MixtralBlockSparseTop2MLP (L -> l) (#1667) --- src/axolotl/monkeypatch/mixtral/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/axolotl/monkeypatch/mixtral/__init__.py b/src/axolotl/monkeypatch/mixtral/__init__.py index d6ee0ce16b..bb5afe847c 100644 --- a/src/axolotl/monkeypatch/mixtral/__init__.py +++ b/src/axolotl/monkeypatch/mixtral/__init__.py @@ -42,9 +42,9 @@ def moe_forward(self, hidden_states: torch.Tensor) -> torch.Tensor: return final_hidden_states, router_logits from transformers.models.mixtral.modeling_mixtral import ( - MixtralBLockSparseTop2MLP, + MixtralBlockSparseTop2MLP, MixtralSparseMoeBlock, ) - MixtralBLockSparseTop2MLP.forward = mlp_forward + MixtralBlockSparseTop2MLP.forward = mlp_forward MixtralSparseMoeBlock.forward = moe_forward From 49b967b62f04fb89dff575f18348618a54d6c827 Mon Sep 17 00:00:00 2001 From: Abe Voelker Date: Tue, 28 May 2024 17:10:40 -0500 Subject: [PATCH 0029/1405] Fix README quick start example usage model dirs (#1668) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 23ac443eed..a3bdef2de2 100644 --- a/README.md +++ b/README.md @@ -124,11 +124,11 @@ accelerate launch -m axolotl.cli.train examples/openllama-3b/lora.yml # inference accelerate launch -m axolotl.cli.inference examples/openllama-3b/lora.yml \ - --lora_model_dir="./lora-out" + --lora_model_dir="./outputs/lora-out" # gradio accelerate launch -m axolotl.cli.inference examples/openllama-3b/lora.yml \ - --lora_model_dir="./lora-out" --gradio + --lora_model_dir="./outputs/lora-out" --gradio # remote yaml files - the yaml config can be hosted on a public URL # Note: the yaml config must directly link to the **raw** yaml From fe650dd3260d0c24d6c5366b90d3507773872b2c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 29 May 2024 10:12:11 -0400 Subject: [PATCH 0030/1405] make sure the CI fails when pytest script fails (#1669) * make sure the pytest script fails * make sure the defaults come through for tests * make sure tensorboard is loaded for test assertion --- cicd/cicd.sh | 1 + src/axolotl/utils/samplers/multipack.py | 4 ++-- tests/e2e/patched/test_resume.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cicd/cicd.sh b/cicd/cicd.sh index fa2049b6bd..bc36458abd 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e pytest --ignore=tests/e2e/ /workspace/axolotl/tests/ pytest /workspace/axolotl/tests/e2e/patched/ diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 07fd056826..1d025ca2d9 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -80,8 +80,8 @@ def __init__( self.lengths = np.array(lengths, dtype=np.int32) self.batch_max_len = batch_max_len self.batch_size = batch_size - self.group_size = group_size - self.bin_size = bin_size + self.group_size = group_size if group_size is not None else 100_000 + self.bin_size = bin_size if bin_size is not None else 200 self.drop_last = drop_last self._efficiency = None diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index dfe9e86252..c0e791f38a 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -62,6 +62,7 @@ def test_resume_qlora_packed(self, temp_dir): "save_steps": 10, "save_total_limit": 5, "max_steps": 40, + "use_tensorboard": True, } ) if is_torch_bf16_gpu_available(): From b7520801a34c8b5f25ab9f0342fda11e2f9800e7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 29 May 2024 10:21:11 -0400 Subject: [PATCH 0031/1405] handle the system role too for chat templates (#1671) --- src/axolotl/prompt_strategies/chat_template.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index b052469dc4..0e7d823ed8 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -32,6 +32,7 @@ def __init__( "user": "user", "assistant": "assistant", "gpt": "assistant", + "system": "system", } self.message_field_role = message_field_role self.message_field_content = message_field_content From a6b37bdeb41107ec8b9cc150e3f73cee668e819a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 29 May 2024 11:51:18 -0400 Subject: [PATCH 0032/1405] revert multipack batch sampler changes (#1672) * revert multipack batch sampler changes * fix default val for drop_last --- src/axolotl/utils/samplers/multipack.py | 251 +++++++++++++++--------- 1 file changed, 162 insertions(+), 89 deletions(-) diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 1d025ca2d9..957ca57464 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -1,64 +1,105 @@ +# pylint: skip-file """ Multipack Batch Sampler """ import logging -from concurrent.futures import ProcessPoolExecutor -from multiprocessing import cpu_count +import math +import os +from typing import Any, Iterable, List, Union import numba import numpy as np -from torch.utils.data import BatchSampler +from torch.utils.data import BatchSampler, Sampler LOG = logging.getLogger("axolotl.utils.samplers.multipack") -# First-fit-decreasing bin packing. @numba.njit -def pack_group(items, group_offset, bin_capacity, max_items_per_bin): - idxs = np.argsort(items)[::-1] - sorted_items = items[idxs] - num_bins = len(items) - bins = np.full(num_bins, bin_capacity, dtype=np.int32) - bin_counts = np.zeros(num_bins, dtype=np.int32) - group_packing = np.full((num_bins, max_items_per_bin), -1, dtype=np.int32) - - for idx, item in enumerate(sorted_items): - global_idx = idxs[idx] + group_offset - - placed = False - for i in range(num_bins): - if bins[i] >= item and bin_counts[i] < max_items_per_bin: - bins[i] -= item - group_packing[i, bin_counts[i]] = global_idx - bin_counts[i] += 1 - placed = True +def ffd_check(a: np.ndarray, c: int, n: int): + # First-fit-decreasing bin packing + # Check if a[] could fit in n bins with capacity c + # https://en.wikipedia.org/wiki/First-fit-decreasing_bin_packing + + a = np.sort(a)[::-1] + bins = np.full((n,), c, dtype=a.dtype) + for size in a: + not_found = True + for idx in range(n): + if bins[idx] >= size: + bins[idx] -= size + not_found = False break - if not placed: - raise ValueError( - f"Item could not be packed. Try increasing cfg.sample_packing_bin_size ({max_items_per_bin})." - ) + if not_found: + return False - return group_packing + return True -def pack(items, bin_capacity, group_size, max_items_per_bin): - num_items = len(items) - num_processes = max(1, min(num_items // group_size, cpu_count())) - tasks = [ - (items[i : i + group_size], i, bin_capacity, max_items_per_bin) - for i in range(0, num_items, group_size) - ] +@numba.njit +def ffd_with_result(a: np.ndarray, c: int, start_index: int): + # First-fit-decreasing bin packing (with result return) + + indices = np.argsort(a)[::-1] + a = a[indices] + + bins: List[Any] = [] + bins_result: List[Any] = [] + for a_id, size in enumerate(a): + add_new = True + for idx in range(len(bins)): + if bins[idx] >= size: + bins[idx] -= size + bins_result[idx].append(indices[a_id] + start_index) + add_new = False + break + + if add_new: + bins.append(c - size) + bins_result.append([indices[a_id] + start_index]) + + return bins_result + + +@numba.njit +def allocate( + lengths: np.ndarray, lengths_cumsum: np.ndarray, rank: int, c: int, n: int +): + # Dynamic batch allocator, similar to Multifit + # https://en.wikipedia.org/wiki/Multifit_algorithm + # ~99.5% efficiency on OpenChat training set (12 * 2048 ctx len) + + s = 0 + start_index = 0 + result = [] + + while True: + # binary search [l, r) + left = 1 + right = 1 + np.searchsorted(lengths_cumsum[start_index:], s + c * n, "right") + + while right - left > 1: + mid = (left + right) // 2 + if ffd_check(lengths[start_index : start_index + mid], c, n): + left = mid + else: + right = mid + + # use length l + batch = ffd_with_result( + lengths[start_index : start_index + left], c, start_index + ) + assert len(batch) <= n + if len(batch) < n: + break + + start_index += left + s = lengths_cumsum[start_index - 1] - packed_bins = [] - with ProcessPoolExecutor(max_workers=num_processes) as executor: - for group_packing in executor.map(pack_group, *zip(*tasks)): - for bin_pack in group_packing: - filtered_pack = bin_pack[bin_pack != -1] - if filtered_pack.size > 0: - packed_bins.append(filtered_pack.tolist()) + # add local rank + result.append(batch[rank]) - return packed_bins + return result, s, len(result) * c * n class MultipackBatchSampler(BatchSampler): @@ -68,63 +109,95 @@ class MultipackBatchSampler(BatchSampler): def __init__( self, - sampler, - lengths, - batch_max_len, - batch_size, - group_size=100_000, - bin_size=200, - drop_last=False, + sampler: Union[Sampler[int], Iterable[int]], + batch_size: int, + batch_max_len: int, + lengths: np.ndarray, + packing_efficiency_estimate: float = 1.0, + drop_last: bool = False, + **kwargs, ): - self.sampler = sampler - self.lengths = np.array(lengths, dtype=np.int32) - self.batch_max_len = batch_max_len + super().__init__(sampler, batch_size, drop_last) self.batch_size = batch_size - self.group_size = group_size if group_size is not None else 100_000 - self.bin_size = bin_size if bin_size is not None else 200 - self.drop_last = drop_last + self.batch_max_len = batch_max_len + self.lengths: np.ndarray = lengths + self.packing_efficiency_estimate = packing_efficiency_estimate or 1.0 - self._efficiency = None - self._batches = None + assert isinstance(self.lengths, np.ndarray) - def efficiency(self): - if self._efficiency is None: - self._batches = self._pack_batches() - return self._efficiency - - def _pack_batches(self): - # Get possibly shuffled indices from sampler. - sample_idxs = np.arange(len(self.sampler)) - lengths = self.lengths[sample_idxs] - - pack_idxs = pack( - lengths, - self.batch_max_len, - self.group_size, - self.bin_size, - ) + self.epoch = 0 + + # statistics + self.eff_total_used = 0 + self.eff_total_slots = 0 + + def set_epoch(self, epoch: int): + self.epoch = epoch - used_tokens = self.lengths.sum() - available_tokens = len(pack_idxs) * self.batch_max_len - self._efficiency = used_tokens / available_tokens + def generate_batches(self, set_stats=False): + indices = [idx for idx in self.sampler] + + lengths = self.lengths[indices] + lengths_cumsum = np.cumsum(lengths) + + batches, total_used, total_slots = allocate( + lengths=lengths, + lengths_cumsum=lengths_cumsum, + rank=0, + c=self.batch_max_len, + n=1, + ) - # Wrap packs into batches. - batch_idxs = [ - pack_idxs[i : i + self.batch_size] - for i in range(0, len(pack_idxs), self.batch_size) + batches = [ + [ + [indices[b_idx] for b_idx in batch] + for batch in batches[i : i + self.batch_size] + ] + for i in range(0, len(batches), self.batch_size) ] - # Drop last batch if needed. - if self.drop_last and len(batch_idxs[-1]) < self.batch_size: - batch_idxs = batch_idxs[:-1] + # statistics + if set_stats: + self.eff_total_used += total_used + self.eff_total_slots += total_slots - return batch_idxs + return batches def __iter__(self): - self._batches = self._pack_batches() - return iter(self._batches) + batches = self.generate_batches(set_stats=True) + return iter(batches) + + def num_batches(self): + batches = self.generate_batches(set_stats=True) + return len(batches) + + def efficiency(self): + return self.eff_total_used / self.eff_total_slots def __len__(self): - if self._batches is None: - self._batches = self._pack_batches() - return len(self._batches) + self.num_batches() + return self._len_est() + + def _len_est(self): + world_size = int(os.getenv("WORLD_SIZE", "1")) + lengths_sum = np.sum(self.lengths) + lengths_sum_per_device = lengths_sum // world_size + LOG.info( + f"packing_efficiency_estimate: {self.packing_efficiency_estimate} " + f"total_num_tokens per device: {lengths_sum_per_device}" + ) + + # shave off 1% + 1 for dealing with variance in packing from random sampler to sampler + return max( + 0, + ( + world_size + * math.floor( + 0.99 + * lengths_sum_per_device + / self.packing_efficiency_estimate + // (self.batch_max_len * self.batch_size) + ) + - 1 + ), + ) From 16d46b74e41ab109daf8f8ceb4de1615746cb25e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 29 May 2024 15:41:46 -0400 Subject: [PATCH 0033/1405] re-enable phi for tests in modal ci (#1373) --- tests/e2e/test_phi.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/e2e/test_phi.py b/tests/e2e/test_phi.py index 7abed85945..4cc6bcdcc9 100644 --- a/tests/e2e/test_phi.py +++ b/tests/e2e/test_phi.py @@ -7,8 +7,6 @@ import unittest from pathlib import Path -import pytest - from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs from axolotl.train import train @@ -21,7 +19,6 @@ os.environ["WANDB_DISABLED"] = "true" -@pytest.mark.skip(reason="doesn't seem to work on modal") class TestPhi(unittest.TestCase): """ Test case for Phi2 models From f7332ac449244643a93a19d99b4a8e28f59ef383 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 29 May 2024 22:27:00 -0400 Subject: [PATCH 0034/1405] use mixins for orpo and kto configs so they work with axolotl customizations (#1674) --- src/axolotl/core/trainer_builder.py | 37 ++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index a37652ade4..c881e4cd38 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -91,11 +91,12 @@ def _sanitize_kwargs_for_tagging(tag_names, kwargs=None): @dataclass -class AxolotlTrainingArguments(TrainingArguments): +class AxolotlTrainingMixins: """ - Extend the base TrainingArguments for axolotl helpers + Mixin class for the Axolotl training args. """ + # pylint: disable=duplicate-code model_type: Optional[str] = field( default=None, metadata={"help": "HF model configuration model_type."} ) @@ -227,6 +228,30 @@ class AxolotlTrainingArguments(TrainingArguments): ) +@dataclass +class AxolotlTrainingArguments(AxolotlTrainingMixins, TrainingArguments): + """ + Training arguments for Causal trainer + + This code is duplicated due to HF TrainingArguments not setting output_dir with a defaujlt value + so it can't be used as a mixin. + """ + + +@dataclass +class AxolotlORPOConfig(AxolotlTrainingMixins, ORPOConfig): + """ + ORPO config for ORPO training + """ + + +@dataclass +class AxolotlKTOConfig(AxolotlTrainingMixins, KTOConfig): + """ + KTO config for KTO training + """ + + class AxolotlTrainer(Trainer): """ Extend the base Trainer for axolotl helpers @@ -1583,14 +1608,14 @@ def build_training_arguments(self, total_num_steps): training_args_cls = AxolotlTrainingArguments if self.cfg.rl == "orpo": - training_args_cls = ORPOConfig + training_args_cls = AxolotlORPOConfig training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes training_args_kwargs["max_length"] = self.cfg.sequence_len if self.cfg.max_prompt_len: training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len if self.cfg.rl == "kto": - training_args_cls = KTOConfig + training_args_cls = AxolotlKTOConfig training_args_kwargs["beta"] = self.cfg.rl_beta or 0.1 training_args_kwargs["desirable_weight"] = ( @@ -1605,12 +1630,12 @@ def build_training_arguments(self, total_num_steps): if self.cfg.max_prompt_len: training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len - training_args = training_args_cls( + training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg + output_dir=self.cfg.output_dir, per_device_train_batch_size=self.cfg.micro_batch_size, max_steps=self.cfg.max_steps or total_num_steps, gradient_accumulation_steps=self.cfg.gradient_accumulation_steps, learning_rate=self.cfg.learning_rate, - output_dir=self.cfg.output_dir, warmup_steps=self.cfg.warmup_steps, logging_first_step=True, logging_steps=1, From 9d4225a05867427c5c1bcbec8b74046ab702640a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 29 May 2024 22:27:26 -0400 Subject: [PATCH 0035/1405] set chat_template in datasets config automatically (#1664) * set chat_template in datasets config automatically * dynamic chat_template, not jsut chatml --- src/axolotl/utils/config/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index a054f24a7f..ad551c74af 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -187,19 +187,22 @@ def normalize_cfg_datasets(cfg): helpers for mapping chat_template to various dataset configurations as necessary """ - if cfg.chat_template and cfg.chat_template == "chatml": + if cfg.chat_template: if cfg.datasets: for idx, ds_cfg in enumerate(cfg.datasets): if ds_cfg.type == "sharegpt" and not ds_cfg.conversation: LOG.info( - f"updating dataset {ds_cfg.path} with `conversation: chatml` to match your chat_template" + f"updating dataset {ds_cfg.path} with `conversation: {cfg.chat_template}` to match your chat_template" ) - cfg.datasets[idx].conversation = "chatml" - if ds_cfg.type == "orpo.chat_template" and not ds_cfg.chat_template: + cfg.datasets[idx].conversation = cfg.chat_template + if ( + ds_cfg.type in ["orpo.chat_template", "chat_template"] + and not ds_cfg.chat_template + ): LOG.info( - f"updating dataset {ds_cfg.path} with `chat_template: chatml` to match your chat_template" + f"updating dataset {ds_cfg.path} with `chat_template: {cfg.chat_template}` to match your chat_template" ) - cfg.datasets[idx].chat_template = "chatml" + cfg.datasets[idx].chat_template = cfg.chat_template def validate_config(cfg: DictDefault, capabilities: Optional[dict] = None): From a944f7b32b0cdf47eeaf66524b362e571d1533cf Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 29 May 2024 22:27:59 -0400 Subject: [PATCH 0036/1405] load explicit splits on datasets (#1652) --- src/axolotl/utils/data/sft.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 6083e30bef..b3e754bc03 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -308,12 +308,16 @@ def for_d_in_datasets(dataset_configs): "unhandled dataset load: local path exists, but is neither a directory or a file" ) elif ds_from_hub: + load_ds_kwargs = {} + if config_dataset.split: + load_ds_kwargs = {"split": config_dataset.split} ds = load_dataset( config_dataset.path, name=config_dataset.name, streaming=False, data_files=config_dataset.data_files, token=use_auth_token, + **load_ds_kwargs, ) elif ds_from_cloud and remote_file_system: if remote_file_system.isdir(config_dataset.path): From d4f6c65e4c4d6899a9283a046fa922b937a0ab25 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 30 May 2024 13:40:35 -0400 Subject: [PATCH 0037/1405] cleanup the deepspeed proxy model at the end of training (#1675) --- src/axolotl/train.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 32bcbc1d0a..89d83f9f15 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -197,6 +197,13 @@ def terminate_handler(_, __, model_weakref): trainer.accelerator.wait_for_everyone() unwrapped_model = trainer.accelerator.unwrap_model(trainer.model_wrapped) + # the trainer saved a model.safetensors file in the output directory, + # but it is a proxy model and should be deleted + if os.path.exists(os.path.join(cfg.output_dir, "model.safetensors")): + LOG.info(f"Deleting {os.path.join(cfg.output_dir, 'model.safetensors')}") + LOG.info("This is a proxy model and should be deleted") + os.remove(os.path.join(cfg.output_dir, "model.safetensors")) + # Saves the whole/unpartitioned fp16 model when in ZeRO Stage-3 to the output directory if # `stage3_gather_16bit_weights_on_model_save` is True in DeepSpeed Config file or # `zero3_save_16bit_model` is True in DeepSpeed Plugin. From 05b0bd08d229ee28cd3f11098d5b178f2ce441b6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 31 May 2024 13:13:13 -0400 Subject: [PATCH 0038/1405] need to add back drop_last for sampler (#1676) --- src/axolotl/core/trainer_builder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index c881e4cd38..0551ddbc0e 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -384,6 +384,7 @@ def _get_train_sampler(self) -> Optional[torch.utils.data.Sampler]: batch_size=batch_size, group_size=self.args.sample_packing_group_size, bin_size=self.args.sample_packing_bin_size, + drop_last=True, ) if self.args.curriculum_sampling: return SequentialSampler(self.train_dataset) @@ -408,6 +409,7 @@ def _get_eval_sampler( batch_size=batch_size, group_size=self.args.sample_packing_group_size, bin_size=self.args.sample_packing_bin_size, + drop_last=True, ) return super()._get_eval_sampler(eval_dataset) From 5cde06587a262649dbf3b9f75392f835de47f68b Mon Sep 17 00:00:00 2001 From: Saeed Esmaili Date: Mon, 3 Jun 2024 15:38:44 +0200 Subject: [PATCH 0039/1405] Fix the broken link in README (#1678) [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3bdef2de2..004eb11cf4 100644 --- a/README.md +++ b/README.md @@ -609,7 +609,7 @@ If you decode a prompt constructed by axolotl, you might see spaces between toke 3. Make sure the inference string from #2 looks **exactly** like the data you fine tuned on from #1, including spaces and new lines. If they aren't the same, adjust your inference server accordingly. 4. As an additional troubleshooting step, you can look at the token ids between 1 and 2 to make sure they are identical. -Having misalignment between your prompts during training and inference can cause models to perform very poorly, so it is worth checking this. See [this blog post](https://hamel.dev/notes/llm/05_tokenizer_gotchas.html) for a concrete example. +Having misalignment between your prompts during training and inference can cause models to perform very poorly, so it is worth checking this. See [this blog post](https://hamel.dev/notes/llm/finetuning/05_tokenizer_gotchas.html) for a concrete example. ## Debugging Axolotl From 1f151c0d52d2d4c78c5e1b1a4ff4fb64cba1f45d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 3 Jun 2024 12:50:44 -0400 Subject: [PATCH 0040/1405] re-enable DPO for tests in modal ci (#1374) * re-enable DPO for tests in modal ci * workaround for training args * don't mixin AxolotlTrainingArguments * fix mixin order so MRO doesn't result in TypeError: non-default argument follows default argument error * use smaller datasets for dpo tests --- .../prompt_strategies/orpo/chat_template.py | 16 +++++++++++----- tests/e2e/test_dpo.py | 16 ++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/axolotl/prompt_strategies/orpo/chat_template.py b/src/axolotl/prompt_strategies/orpo/chat_template.py index a89dee1575..bba6948568 100644 --- a/src/axolotl/prompt_strategies/orpo/chat_template.py +++ b/src/axolotl/prompt_strategies/orpo/chat_template.py @@ -56,7 +56,9 @@ def get_chosen_conversation_thread(self, prompt) -> MessageList: messages: List[Message] = [] if system := prompt.get("system", None): messages.append(Message(role="system", content=system, label=False)) - messages.append(Message(role="user", content=prompt["prompt"], label=False)) + messages.append( + Message(role="user", content=prompt["chosen"][0]["content"], label=False) + ) messages.append( Message( role="assistant", content=prompt["chosen"][1]["content"], label=True @@ -70,7 +72,9 @@ def get_rejected_conversation_thread(self, prompt) -> MessageList: messages: List[Message] = [] if system := prompt.get("system", None): messages.append(Message(role="system", content=system, label=False)) - messages.append(Message(role="user", content=prompt["prompt"], label=False)) + messages.append( + Message(role="user", content=prompt["rejected"][0]["content"], label=False) + ) messages.append( Message( role="assistant", content=prompt["rejected"][1]["content"], label=True @@ -152,8 +156,8 @@ def __init__( def tokenize_prompt(self, prompt): # pass the rejected prompt/row to the Prompter to get the formatted prompt prompt_len = 0 - rejected_message_list = self.dataset_parser.get_rejected_conversation_thread( - prompt + rejected_message_list: MessageList = ( + self.dataset_parser.get_rejected_conversation_thread(prompt) ) input_ids = [] labels = [] @@ -174,7 +178,9 @@ def tokenize_prompt(self, prompt): rejected_input_ids = input_ids rejected_labels = labels # pass the chosen prompt/row to the Prompter to get the formatted prompt - chosen_message_list = self.dataset_parser.get_chosen_conversation_thread(prompt) + chosen_message_list: MessageList = ( + self.dataset_parser.get_chosen_conversation_thread(prompt) + ) input_ids = [] labels = [] for _, (part, label) in enumerate( diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index ddd63d8271..5d2522bdfd 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -21,7 +21,6 @@ os.environ["WANDB_DISABLED"] = "true" -@pytest.mark.skip(reason="doesn't seem to work on modal") class TestDPOLlamaLora(unittest.TestCase): """ Test case for DPO Llama models using LoRA @@ -45,8 +44,8 @@ def test_dpo_lora(self, temp_dir): "rl": "dpo", "datasets": [ { - "path": "Intel/orca_dpo_pairs", - "type": "chatml.intel", + "path": "arcee-ai/distilabel-intel-orca-dpo-pairs-binarized", + "type": "chatml.ultra", "split": "train", }, ], @@ -89,8 +88,8 @@ def test_kto_pair_lora(self, temp_dir): "rl": "kto_pair", "datasets": [ { - "path": "Intel/orca_dpo_pairs", - "type": "chatml.intel", + "path": "arcee-ai/distilabel-intel-orca-dpo-pairs-binarized", + "type": "chatml.ultra", "split": "train", }, ], @@ -133,8 +132,8 @@ def test_ipo_lora(self, temp_dir): "rl": "ipo", "datasets": [ { - "path": "Intel/orca_dpo_pairs", - "type": "chatml.intel", + "path": "arcee-ai/distilabel-intel-orca-dpo-pairs-binarized", + "type": "chatml.ultra", "split": "train", }, ], @@ -180,7 +179,7 @@ def test_orpo_lora(self, temp_dir): "chat_template": "chatml", "datasets": [ { - "path": "argilla/ultrafeedback-binarized-preferences-cleaned", + "path": "argilla/distilabel-capybara-dpo-7k-binarized", "type": "chat_template.argilla", "split": "train", }, @@ -206,6 +205,7 @@ def test_orpo_lora(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + @pytest.mark.skip(reason="Fix the implementation") @with_temp_dir def test_kto_lora(self, temp_dir): # pylint: disable=duplicate-code From c996881ec2f6cc23e540ef9ebefd87a8e5e5387d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 4 Jun 2024 16:09:51 -0400 Subject: [PATCH 0041/1405] add support for rpo_alpha (#1681) * add support for rpo_alpha * Add smoke test for dpo + nll loss --- requirements.txt | 2 +- src/axolotl/core/trainer_builder.py | 13 +++++- .../config/models/input/v0_4_1/__init__.py | 1 + tests/e2e/test_dpo.py | 45 +++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 557c5293fd..b5114bbf62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,6 +39,6 @@ s3fs gcsfs # adlfs -trl==0.8.6 +trl @ git+https://github.com/huggingface/trl.git@f18253bf2d747f68acc9cd89da95c85ebf59dbb9 zstandard==0.22.0 fastcore diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 0551ddbc0e..b88cc42210 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -30,7 +30,7 @@ ) from transformers.trainer_utils import seed_worker from transformers.utils import is_sagemaker_mp_enabled -from trl import DPOTrainer, KTOConfig, KTOTrainer, ORPOConfig, ORPOTrainer +from trl import DPOConfig, DPOTrainer, KTOConfig, KTOTrainer, ORPOConfig, ORPOTrainer from trl.trainer.utils import pad_to_length from axolotl.loraplus import create_loraplus_optimizer @@ -238,6 +238,13 @@ class AxolotlTrainingArguments(AxolotlTrainingMixins, TrainingArguments): """ +@dataclass +class AxolotlDPOConfig(AxolotlTrainingMixins, DPOConfig): + """ + DPO config for DPO training + """ + + @dataclass class AxolotlORPOConfig(AxolotlTrainingMixins, ORPOConfig): """ @@ -1608,7 +1615,9 @@ def build_training_arguments(self, total_num_steps): # trl does some odd mapping of alpha to beta to reuse the beta parameter ??? training_args_kwargs["beta"] = self.cfg.orpo_alpha - training_args_cls = AxolotlTrainingArguments + training_args_cls = AxolotlDPOConfig + if self.cfg.rpo_alpha is not None: + training_args_kwargs["rpo_alpha"] = self.cfg.rpo_alpha if self.cfg.rl == "orpo": training_args_cls = AxolotlORPOConfig training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index f363ebfdce..240a816edc 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -619,6 +619,7 @@ class Config: neftune_noise_alpha: Optional[float] = None orpo_alpha: Optional[float] = None + rpo_alpha: Optional[float] = None kto_desirable_weight: Optional[float] = None kto_undesirable_weight: Optional[float] = None diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index 5d2522bdfd..5f03e6bc1b 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -70,6 +70,51 @@ def test_dpo_lora(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + @with_temp_dir + def test_dpo_nll_lora(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 1024, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 64, + "lora_alpha": 32, + "lora_dropout": 0.1, + "lora_target_linear": True, + "special_tokens": {}, + "rl": "dpo", + "rpo_alpha": 0.5, + "datasets": [ + { + "path": "arcee-ai/distilabel-intel-orca-dpo-pairs-binarized", + "type": "chatml.ultra", + "split": "train", + }, + ], + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "paged_adamw_8bit", + "lr_scheduler": "cosine", + "max_steps": 20, + "save_steps": 10, + "warmup_steps": 5, + "gradient_checkpointing": True, + "gradient_checkpointing_kwargs": {"use_reentrant": True}, + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + @with_temp_dir def test_kto_pair_lora(self, temp_dir): # pylint: disable=duplicate-code From cf64284a04e08437dba538e5d2ffe144cec6bc2f Mon Sep 17 00:00:00 2001 From: Brian Fitzgerald Date: Tue, 4 Jun 2024 15:11:56 -0500 Subject: [PATCH 0042/1405] Phi-3 conversation format, example training script and perplexity metric (#1582) * phi-3 support and perplexity metric * phi-3 chat template * metrics updates * chore: lint * fix assertion on Tensor * fix tests since tokenization happens in the metric * fix perplexity value of shorter passage --------- Co-authored-by: Wing Lian --- .gitignore | 6 ++ examples/phi/phi3-ft.yml | 64 ++++++++++++++++ src/axolotl/prompters.py | 15 +++- src/axolotl/utils/callbacks/__init__.py | 34 ++++++--- src/axolotl/utils/callbacks/perplexity.py | 76 +++++++++++++++++++ src/axolotl/utils/chat_templates.py | 1 + src/axolotl/utils/config/__init__.py | 6 +- .../config/models/input/v0_4_1/__init__.py | 8 +- src/axolotl/utils/data/sft.py | 18 +++-- tests/test_perplexity.py | 41 ++++++++++ 10 files changed, 243 insertions(+), 26 deletions(-) create mode 100644 examples/phi/phi3-ft.yml create mode 100644 src/axolotl/utils/callbacks/perplexity.py create mode 100644 tests/test_perplexity.py diff --git a/.gitignore b/.gitignore index e6dfee67db..d15f2000f3 100644 --- a/.gitignore +++ b/.gitignore @@ -176,3 +176,9 @@ qlora-out/* mlruns/* /.quarto/ +prepared-datasets/ +submit.sh +*.out* + +typings/ +out/ diff --git a/examples/phi/phi3-ft.yml b/examples/phi/phi3-ft.yml new file mode 100644 index 0000000000..18db9b8b78 --- /dev/null +++ b/examples/phi/phi3-ft.yml @@ -0,0 +1,64 @@ +base_model: microsoft/Phi-3-mini-4k-instruct +trust_remote_code: true +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer +chat_template: phi_3 + +load_in_8bit: false +load_in_4bit: false +strict: false + +datasets: + - path: garage-bAInd/Open-Platypus + type: alpaca:phi + +dataset_prepared_path: +val_set_size: 0.01 +output_dir: ./out + +sequence_len: 4096 +sample_packing: true +pad_to_sequence_len: true + +adapter: lora +lora_model_dir: +lora_r: 64 +lora_alpha: 32 +lora_dropout: 0.05 +lora_target_linear: true +lora_fan_in_fan_out: + +gradient_accumulation_steps: 1 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_torch +adam_beta2: 0.95 +adam_epsilon: 0.00001 +max_grad_norm: 1.0 +lr_scheduler: cosine +learning_rate: 5.0e-6 + +train_on_inputs: false +group_by_length: false +bf16: auto + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: True +early_stopping_patience: 3 +logging_steps: 1 +flash_attention: true + +eval_steps: 1000 +save_steps: 5000 +eval_table_size: 2 +eval_batch_size: 2 +eval_sample_packing: false +eval_max_new_tokens: 32 +eval_causal_lm_metrics: ["perplexity"] +do_causal_lm_eval: true + +warmup_ratio: 0.2 +debug: true +weight_decay: 0.1 +resize_token_embeddings_to_32x: true diff --git a/src/axolotl/prompters.py b/src/axolotl/prompters.py index 60ea5c99f9..102e9e53be 100644 --- a/src/axolotl/prompters.py +++ b/src/axolotl/prompters.py @@ -20,6 +20,7 @@ class PromptStyle(Enum): INSTRUCT = "instruct" CHAT = "chat" CHATML = "chatml" + PHI = "phi" class Prompter: @@ -38,9 +39,9 @@ class AlpacaPrompter(Prompter): system_format: str = "{system}" turn_format: str turn_no_input_format: str - prompt_style: Optional[PromptStyle] = None + prompt_style: Optional[str] = None - def __init__(self, prompt_style=PromptStyle.INSTRUCT.value): + def __init__(self, prompt_style: Optional[str] = PromptStyle.INSTRUCT.value): self.prompt_style = prompt_style if prompt_style else PromptStyle.INSTRUCT.value self.match_prompt_style() @@ -52,16 +53,20 @@ def match_prompt_style(self): "### Instruction:\n{instruction}\n\n### Response:\n" ) self.system_format = "{system}\n\n" - if self.prompt_style == PromptStyle.CHAT.value: + elif self.prompt_style == PromptStyle.CHAT.value: self.turn_format = "USER: {instruction}\n{input}\nASSISTANT:" self.turn_no_input_format = "USER: {instruction}\nASSISTANT:" self.system_format = "SYSTEM: {system}\n" - if self.prompt_style == PromptStyle.CHATML.value: + elif self.prompt_style == PromptStyle.CHATML.value: self.turn_format = "<|im_start|>user\n{instruction}\n{input}<|im_end|>\n<|im_start|>assistant\n" self.turn_no_input_format = ( "<|im_start|>user\n{instruction}<|im_end|>\n<|im_start|>assistant\n" ) self.system_format = "<|im_start|>system\n{system}<|im_end|>\n" + elif self.prompt_style == PromptStyle.PHI.value: + self.turn_format = "<|user|>\n{instruction}<|end|>{input}<|assistant|>" + self.turn_no_input_format = "<|user|>\n{instruction}<|end|><|assistant|>" + self.system_format = "<|system|>{system}\n" def _build_result(self, instruction, input_text, output): # returns the full prompt from instruction and optional input @@ -381,12 +386,14 @@ def __init__( conversation: Optional[Union[str, Conversation]] = None, role_key_human: Optional[str] = None, role_key_model: Optional[str] = None, + role_key_tool: Optional[str] = None, roles: Optional[dict] = None, ): super().__init__( conversation=conversation, role_key_human=role_key_human, role_key_model=role_key_model, + role_key_tool=role_key_tool, roles=roles, ) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index c21ef0ad7a..73715b06ab 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -5,6 +5,7 @@ import logging import math import os +import traceback from shutil import copyfile from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING, Any, Dict, List @@ -30,6 +31,7 @@ from axolotl.utils import is_mlflow_available from axolotl.utils.bench import log_gpu_memory_usage +from axolotl.utils.callbacks.perplexity import Perplexity from axolotl.utils.config.models.input.v0_4_1 import AxolotlInputConfig from axolotl.utils.distributed import ( barrier, @@ -374,10 +376,14 @@ def __init__(self, cfg): def __maybe_load_metrics(self): metrics = {} for metric in self.cfg.eval_causal_lm_metrics: - try: - metrics[metric] = evaluate.load(metric) - except Exception as exc: # pylint: disable=broad-exception-caught - LOG.warning(f"{metric}: {exc.args}") + if metric == "perplexity": + max_seq_len = self.cfg.eval_max_new_tokens + metrics[metric] = Perplexity(trainer.model, tokenizer, max_seq_len) + else: + try: + metrics[metric] = evaluate.load(metric) + except Exception as exc: # pylint: disable=broad-exception-caught + LOG.warning(f"{metric}: {exc.args}") return metrics def on_evaluate( @@ -421,13 +427,20 @@ def compute(metric: evaluate.Metric, **kwargs): # safely compute a metric and return the score if the format is correct metric_score = None try: - metric_score = metric.compute(**kwargs) + # Only pass the kwargs that are in the metric's feature list + metric_kwargs = { + k: kwargs[k] + for k in metric._feature_names() # pylint: disable=protected-access + if k in kwargs + } + metric_score = metric.compute(**metric_kwargs) return ( metric_score["score"] if "score" in metric_score else metric_score["mean_score"] ) except Exception: # pylint: disable=broad-exception-caught + traceback.print_exc() LOG.debug( f"Failed to compute metric {metric.name} with kwargs {kwargs.keys()}" ) @@ -443,11 +456,12 @@ def evaluate_preds(sources, predictions, references): predictions=predictions, sources=sources, ) - score = score or compute( - metric, - references=[[r] for r in references], - predictions=predictions, - ) + if score is None: + score = compute( + metric, + references=[[r] for r in references], + predictions=predictions, + ) scores[metric_name] = score return scores diff --git a/src/axolotl/utils/callbacks/perplexity.py b/src/axolotl/utils/callbacks/perplexity.py new file mode 100644 index 0000000000..2e64176812 --- /dev/null +++ b/src/axolotl/utils/callbacks/perplexity.py @@ -0,0 +1,76 @@ +"""callback to calculate perplexity as an evaluation metric.""" +from typing import Dict, List, Optional + +import torch +from torch import Tensor +from tqdm import tqdm +from transformers.modeling_outputs import CausalLMOutput +from transformers.modeling_utils import PreTrainedModel +from transformers.tokenization_utils import PreTrainedTokenizer + + +class Perplexity: + """ + Calculate perplexity as defined in https://huggingface.co/docs/transformers/en/perplexity. + This is a custom variant that doesn't re-tokenize the input or re-load the model. + """ + + def __init__( + self, + model: PreTrainedModel, + tokenizer: PreTrainedTokenizer, + max_seq_len: int, + stride: int = 512, + ) -> None: + self.max_seq_len = max_seq_len + self.stride = stride + self.model = model + self.tokenizer = tokenizer + self.device = model.device + self.name = "perplexity" + + def _feature_names(self) -> List[str]: + return ["references"] + + def compute( + self, + references: Optional[List[str]] = None, + ) -> Dict[str, float]: + """ + Compute perplexity in a fixed length sliding window across the sequence. + """ + assert references is not None, "Missing parameter: references" + + references_tokenized = self.tokenizer( + references, return_tensors="pt", padding=True, truncation=True + ) + input_ids: Tensor = references_tokenized["input_ids"] # type: ignore + input_ids = input_ids.to(self.device) + + sequence_length = input_ids.size(1) + + losses = [] + prev_end_loc = 0 + for begin_loc in tqdm(range(0, sequence_length, self.stride)): + end_loc = min(begin_loc + self.max_seq_len, sequence_length) + trg_len = end_loc - prev_end_loc + input_ids_slice = input_ids[:, begin_loc:end_loc] + labels_slice = input_ids_slice.clone() + labels_slice[:, :-trg_len] = -100 + + with torch.no_grad(): + outputs: CausalLMOutput = self.model( + input_ids=input_ids_slice, labels=labels_slice + ) + + losses.append(outputs.loss) + + prev_end_loc = end_loc + if end_loc == sequence_length: + break + + perplexity = torch.exp(torch.stack(losses).mean()).item() + + return { + "score": perplexity, + } diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 1fe888aa80..725934cf56 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -25,6 +25,7 @@ def chat_templates(user_choice: str): "gemma": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\n' + message['content'] | trim + '\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\n'}}{% endif %}", "cohere": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", + "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", } if user_choice in templates: diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index ad551c74af..ed165e89ca 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -10,6 +10,7 @@ from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.config.models.input.v0_4_1 import ( + SUPPORTED_METRICS, AxolotlConfigWCapabilities, AxolotlInputConfig, ) @@ -586,13 +587,12 @@ def legacy_validate_config(cfg): ) if cfg.eval_causal_lm_metrics: - supported_metrics = ["sacrebleu", "comet", "ter", "chrf"] if not isinstance(cfg.eval_causal_lm_metrics, list): raise ValueError("eval_causal_lm_metrics must be a list") # only ["sacrebleu", "comet", "ter", "chrf"] supported - if set(cfg.eval_causal_lm_metrics) - set(supported_metrics): + if set(cfg.eval_causal_lm_metrics) - SUPPORTED_METRICS: raise ValueError( - f"eval_causal_lm_metrics must be one of {supported_metrics}" + f"eval_causal_lm_metrics must be one of {SUPPORTED_METRICS}" ) # TODO diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 240a816edc..015885d9ea 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -17,6 +17,8 @@ LOG = logging.getLogger("axolotl.utils.config.models.input") +SUPPORTED_METRICS = {"sacrebleu", "comet", "ter", "chrf", "perplexity"} + class DeprecatedParameters(BaseModel): """configurations that are deprecated""" @@ -176,6 +178,7 @@ class ChatTemplate(str, Enum): gemma = "gemma" # pylint: disable=invalid-name cohere = "cohere" # pylint: disable=invalid-name llama3 = "llama3" # pylint: disable=invalid-name + phi_3 = "phi_3" # pylint: disable=invalid-name class LoftQConfig(BaseModel): @@ -1073,13 +1076,12 @@ def check_causal_lm_evals(cls, data): ) if data.get("eval_causal_lm_metrics"): - supported_metrics = ["sacrebleu", "comet", "ter", "chrf"] if not isinstance(data.get("eval_causal_lm_metrics"), list): raise ValueError("eval_causal_lm_metrics must be a list") # only ["sacrebleu", "comet", "ter", "chrf"] supported - if set(data.get("eval_causal_lm_metrics")) - set(supported_metrics): + if set(data.get("eval_causal_lm_metrics")) - SUPPORTED_METRICS: raise ValueError( - f"eval_causal_lm_metrics must be one of {supported_metrics}" + f"eval_causal_lm_metrics must be one of {SUPPORTED_METRICS}" ) return data diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index b3e754bc03..bbea1987f1 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -474,12 +474,16 @@ def load_prepare_datasets( index=cfg.dataset_shard_idx, ) - if split == "train" and cfg.val_set_size: + val_set_size = ( + int(cfg.val_set_size) if cfg.val_set_size > 1 else float(cfg.val_set_size) + ) + + if split == "train" and val_set_size: # ensure we end up with the same fingerprint by doing rank0 first and being able to cache to_hash_train = ( dataset._fingerprint # pylint: disable=protected-access + "|" - + str(cfg.val_set_size) + + str(val_set_size) + "|" + "train" + "|" @@ -488,7 +492,7 @@ def load_prepare_datasets( to_hash_test = ( dataset._fingerprint # pylint: disable=protected-access + "|" - + str(cfg.val_set_size) + + str(val_set_size) + "|" + "test" + "|" @@ -498,9 +502,7 @@ def load_prepare_datasets( test_fingerprint = md5(to_hash_test) dataset = dataset.train_test_split( - test_size=int(cfg.val_set_size) - if cfg.val_set_size == int(cfg.val_set_size) - else cfg.val_set_size, + test_size=val_set_size, shuffle=False, seed=cfg.seed or 42, train_new_fingerprint=train_fingerprint, @@ -535,6 +537,10 @@ def get_dataset_wrapper( "keep_in_memory": cfg.dataset_keep_in_memory is True, } + LOG.info( + f"Loading dataset with base_type: {d_base_type} and prompt_style: {d_prompt_style}" + ) + if ( isinstance(dataset, Dataset) and "input_ids" in dataset.features diff --git a/tests/test_perplexity.py b/tests/test_perplexity.py new file mode 100644 index 0000000000..e66e95d0cd --- /dev/null +++ b/tests/test_perplexity.py @@ -0,0 +1,41 @@ +"""unit tests for perplexity eval callback""" +# pylint: disable=redefined-outer-name + +from pytest import fixture +from transformers.models.auto.modeling_auto import AutoModelForCausalLM +from transformers.models.auto.tokenization_auto import AutoTokenizer + +from axolotl.utils.callbacks.perplexity import Perplexity + +MODEL_NAME = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" + + +@fixture() +def metric(tokenizer): + model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, trust_remote_code=True) + + return Perplexity(model, tokenizer, 512) + + +@fixture() +def tokenizer(): + return AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) + + +def test_perplexity_longer_than_stride(metric): + # taken from https://huggingface.co/datasets/roneneldan/TinyStories + sample_text = """ +Once upon a time, there was a little car named Beep. Beep loved to go fast and play in the sun. Beep was a healthy car because he always had good fuel. Good fuel made Beep happy and strong. One day, Beep was driving in the park when he saw a big tree. The tree had many leaves that were falling. Beep liked how the leaves fall and wanted to play with them. Beep drove under the tree and watched the leaves fall on him. He laughed and beeped his horn. Beep played with the falling leaves all day. When it was time to go home, Beep knew he needed more fuel. He went to the fuel place and got more healthy fuel. Now, Beep was ready to go fast and play again the next day. And Beep lived happily ever after. +One day, a little fish named Fin was swimming near the shore. He saw a big crab and wanted to be friends. "Hi, I am Fin. Do you want to play?" asked the little fish. The crab looked at Fin and said, "No, I don't want to play. I am cold and I don't feel fine." Fin felt sad but wanted to help the crab feel better. He swam away and thought of a plan. He remembered that the sun could make things warm. So, Fin swam to the top of the water and called to the sun, "Please, sun, help my new friend feel fine and not freeze!" The sun heard Fin's call and shone its warm light on the shore. The crab started to feel better and not so cold. He saw Fin and said, "Thank you, little fish, for making me feel fine. I don't feel like I will freeze now. Let's play together!" And so, Fin and the crab played and became good friends. +""" + result = metric.compute([sample_text]) + ppl = result["score"] + assert round(ppl, 2) == 5.37 + + +def test_perplexity_short(metric): + # taken from https://huggingface.co/datasets/roneneldan/TinyStories + sample_text = "Once upon a time, there was a little car named Beep. Beep loved to go fast and play in the sun." + result = metric.compute([sample_text]) + ppl = result["score"] + assert round(ppl, 2) == 10.02 From a82a7115224b7aef14301387a11ad4729fd6ca52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaditya=20Ura=20=28looking=20for=20PhD=20Fall=E2=80=9924?= =?UTF-8?q?=29?= Date: Wed, 5 Jun 2024 01:50:25 +0530 Subject: [PATCH 0043/1405] Create phi3-ft-fsdp.yml (#1580) rename to be fsdp specific and tweak settings a bit --- examples/phi/phi3-ft-fsdp.yml | 83 +++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 examples/phi/phi3-ft-fsdp.yml diff --git a/examples/phi/phi3-ft-fsdp.yml b/examples/phi/phi3-ft-fsdp.yml new file mode 100644 index 0000000000..d220e577d1 --- /dev/null +++ b/examples/phi/phi3-ft-fsdp.yml @@ -0,0 +1,83 @@ +base_model: microsoft/Phi-3-mini-4k-instruct +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer + +load_in_8bit: false +load_in_4bit: false +strict: false + +datasets: + - path: mhenrichsen/alpaca_2k_test + type: alpaca + +dataset_prepared_path: +val_set_size: 0 +output_dir: ./phi-sft-out + +sequence_len: 4096 +sample_packing: true +pad_to_sequence_len: true +trust_remote_code: true + +adapter: +lora_model_dir: +lora_r: +lora_alpha: +lora_dropout: +lora_target_linear: +lora_fan_in_fan_out: + +wandb_project: phi3 +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 12 +num_epochs: 2 +optimizer: adamw_torch +adam_beta2: 0.95 +adam_epsilon: 0.00001 +max_grad_norm: 1.0 +lr_scheduler: cosine +learning_rate: 0.000003 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_steps: 100 +evals_per_epoch: 4 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.1 +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_limit_all_gathers: true + fsdp_sync_module_states: true + fsdp_offload_params: true + fsdp_use_orig_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: Phi3DecoderLayer + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD +resize_token_embeddings_to_32x: true +special_tokens: + pad_token: "<|endoftext|>" From 9c1af1a9c0688ef6601b96a73be69d3de363e6b9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 7 Jun 2024 11:28:43 -0400 Subject: [PATCH 0044/1405] ensure explicit eval_sample_packing to avoid mismatch issues (#1692) --- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 015885d9ea..84e6bb19a5 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -899,6 +899,15 @@ def check_eval_packing(cls, data): raise ValueError( "eval_table_size and eval_sample_packing are not supported together with sample_packing. Please set 'eval_sample_packing' to false." ) + if ( + data.get("sample_packing") + and data.get("eval_sample_packing") is None + and not data.get("eval_table_size") + ): + LOG.info( + "explicitly setting `eval_sample_packing` to match `sample_packing`" + ) + data["eval_sample_packing"] = True return data @model_validator(mode="before") From 00ac3022a1a986d123218941529f1d5dafaf9d17 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 7 Jun 2024 16:38:29 -0400 Subject: [PATCH 0045/1405] add qwen2-72b fsdp example (#1696) --- examples/qwen2/qlora-fsdp.yaml | 75 ++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 examples/qwen2/qlora-fsdp.yaml diff --git a/examples/qwen2/qlora-fsdp.yaml b/examples/qwen2/qlora-fsdp.yaml new file mode 100644 index 0000000000..44f9c7e495 --- /dev/null +++ b/examples/qwen2/qlora-fsdp.yaml @@ -0,0 +1,75 @@ +base_model: Qwen/Qwen2-7B +trust_remote_code: true + +load_in_8bit: false +load_in_4bit: true +strict: false + +datasets: + - path: tatsu-lab/alpaca + type: alpaca +dataset_prepared_path: +val_set_size: 0.05 +output_dir: ./outputs/out + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: true +pad_to_sequence_len: true + +adapter: qlora +lora_model_dir: +lora_r: 32 +lora_alpha: 64 +lora_dropout: 0.05 +lora_target_linear: true +lora_fan_in_fan_out: + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_torch +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_steps: 10 +evals_per_epoch: 4 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_limit_all_gathers: true + fsdp_sync_module_states: true + fsdp_offload_params: true + fsdp_use_orig_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: Qwen2DecoderLayer + fsdp_state_dict_type: FULL_STATE_DICT +special_tokens: From ed8ef6537182fe516a2940355f7e34a397b22fdc Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 8 Jun 2024 09:48:10 -0400 Subject: [PATCH 0046/1405] add back packing efficiency estimate so epochs and multi-gpu works properly (#1697) --- src/axolotl/core/trainer_builder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index b88cc42210..81eeeabcbd 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -387,6 +387,7 @@ def _get_train_sampler(self) -> Optional[torch.utils.data.Sampler]: return MultipackBatchSampler( RandomSampler(self.train_dataset), lengths=get_dataset_lengths(self.train_dataset), + packing_efficiency_estimate=self.args.sample_packing_efficiency, batch_max_len=batch_max_len, batch_size=batch_size, group_size=self.args.sample_packing_group_size, @@ -412,6 +413,7 @@ def _get_eval_sampler( return MultipackBatchSampler( SequentialSampler(eval_dataset), lengths=get_dataset_lengths(self.eval_dataset), + packing_efficiency_estimate=self.args.sample_packing_efficiency, batch_max_len=batch_max_len, batch_size=batch_size, group_size=self.args.sample_packing_group_size, From 18cabc0c461c9178c90fcb080e40e7daa9c6c6f8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 8 Jun 2024 09:48:30 -0400 Subject: [PATCH 0047/1405] fix for when sample_packing and eval_sample_packing are different (#1695) --- .../utils/config/models/input/v0_4_1/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 84e6bb19a5..dbf9b02c03 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -908,6 +908,17 @@ def check_eval_packing(cls, data): "explicitly setting `eval_sample_packing` to match `sample_packing`" ) data["eval_sample_packing"] = True + + if ( + data.get("sample_packing") + and data.get("eval_sample_packing") is False + and data.get("remove_unused_columns") is None + ): + LOG.info( + "setting `remove_unused_columns: false` for when sample_packing and eval_sample_packing don't match" + ) + data["remove_unused_columns"] = False + return data @model_validator(mode="before") From 851ccb123745df742888f4199d04277da51d5e09 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 9 Jun 2024 17:13:28 -0400 Subject: [PATCH 0048/1405] bump deepspeed for fix for grad norm compute putting tensors on different devices (#1699) --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index b5114bbf62..52f98042cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ transformers==4.41.1 tokenizers==0.19.1 bitsandbytes==0.43.1 accelerate==0.30.1 -deepspeed==0.14.2 +deepspeed @ git+https://github.com/microsoft/DeepSpeed.git@bc48371c5e1fb8fd70fc79285e66201dbb65679b pydantic==2.6.3 addict fire diff --git a/setup.py b/setup.py index 3d1537edff..c7b4e15dec 100644 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ def parse_requirements(): "fused-dense-lib @ git+https://github.com/Dao-AILab/flash-attention@v2.5.8#subdirectory=csrc/fused_dense_lib", ], "deepspeed": [ - "deepspeed==0.14.2", + "deepspeed @ git+https://github.com/microsoft/DeepSpeed.git@bc48371c5e1fb8fd70fc79285e66201dbb65679b", "deepspeed-kernels", ], "mamba-ssm": [ From cbbf039a460a9714d3c2a727e82af59633ce888e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 9 Jun 2024 20:09:36 -0400 Subject: [PATCH 0049/1405] verbose failure message (#1694) --- src/axolotl/prompt_strategies/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/axolotl/prompt_strategies/__init__.py b/src/axolotl/prompt_strategies/__init__.py index e62a5c20ce..f5699a0871 100644 --- a/src/axolotl/prompt_strategies/__init__.py +++ b/src/axolotl/prompt_strategies/__init__.py @@ -2,9 +2,12 @@ import importlib import inspect +import logging from axolotl.prompt_strategies.user_defined import UserDefinedDatasetConfig +LOG = logging.getLogger("axolotl.prompt_strategies") + def load(strategy, tokenizer, cfg, ds_cfg): try: @@ -22,5 +25,8 @@ def load(strategy, tokenizer, cfg, ds_cfg): if "ds_cfg" in sig.parameters: load_kwargs["ds_cfg"] = ds_cfg return func(tokenizer, cfg, **load_kwargs) - except Exception: # pylint: disable=broad-exception-caught + except ModuleNotFoundError: + return None + except Exception as exc: # pylint: disable=broad-exception-caught + LOG.error(f"Failed to load prompt strategy `{strategy}`: {str(exc)}") return None From 5783839c6e29bb148041338772040c85aaae4646 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 9 Jun 2024 20:10:17 -0400 Subject: [PATCH 0050/1405] download model weights on preprocess step (#1693) --- src/axolotl/cli/preprocess.py | 7 +++++++ src/axolotl/common/cli.py | 1 + 2 files changed, 8 insertions(+) diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index e7b3596a4f..f43277e49c 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -7,7 +7,9 @@ import fire import transformers +from accelerate import init_empty_weights from colorama import Fore +from transformers import AutoModelForCausalLM from axolotl.cli import ( check_accelerate_default_config, @@ -71,6 +73,11 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): else: load_datasets(cfg=parsed_cfg, cli_args=parsed_cli_args) + if parsed_cli_args.download: + model_name = parsed_cfg.base_model + with init_empty_weights(): + AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True) + LOG.info( Fore.GREEN + f"Success! Preprocessed data path: `dataset_prepared_path: {parsed_cfg.dataset_prepared_path}`" diff --git a/src/axolotl/common/cli.py b/src/axolotl/common/cli.py index 636a23ba52..c96f8f81ff 100644 --- a/src/axolotl/common/cli.py +++ b/src/axolotl/common/cli.py @@ -40,6 +40,7 @@ class PreprocessCliArgs: debug_text_only: bool = field(default=False) debug_num_examples: int = field(default=1) prompter: Optional[str] = field(default=None) + download: Optional[bool] = field(default=True) def load_model_and_tokenizer( From 3f1f5e33120b75f83736d75eb3bea0a6dad5424c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 18 Jun 2024 23:32:29 -0400 Subject: [PATCH 0051/1405] drop length column for issues with eval without packing (#1711) --- src/axolotl/core/trainer_builder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 81eeeabcbd..1807952df2 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -459,6 +459,8 @@ def get_eval_dataloader(self, eval_dataset: Optional[Dataset] = None) -> DataLoa self.data_collator = ( # pylint: disable=attribute-defined-outside-init self.eval_data_collator ) + if eval_dataset: + eval_dataset = eval_dataset.remove_columns(["length"]) dataloader = super().get_eval_dataloader(eval_dataset) self.data_collator = ( # pylint: disable=attribute-defined-outside-init self.train_data_collator From 4de4b4089fbc43e04c696c10c1ac51eebf2c6a99 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 20 Jun 2024 10:02:55 -0400 Subject: [PATCH 0052/1405] add support for multipack for deepseek_v2 (#1712) --- src/axolotl/monkeypatch/multipack.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index c1eb3127d2..7f6296bb67 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -18,6 +18,7 @@ "gemma", "gemmoe", "starcoder2", + "deepseek_v2", ] @@ -56,6 +57,8 @@ def patch_for_multipack(model_type, model_name=None): patch_remote(model_name, ".configuration_gemmoe", ".modeling_gemmoe") elif model_type == "jamba": patch_remote(model_name, ".configuration_jamba", ".modeling_jamba") + elif model_type == "deepseek_v2": + patch_remote(model_name, ".configuration_deepseek", ".modeling_deepseek") def patch_remote(model_name, config_name, modeling_name): From 559562d79086fdef464ceb5a82acb297bab5712d Mon Sep 17 00:00:00 2001 From: DavidFarago Date: Thu, 20 Jun 2024 16:05:16 +0200 Subject: [PATCH 0053/1405] Allow "weight: 0" in messages to mask them (#1703) Allow in message objects the additional key `weight`, which can be set to 0 (or 1) to cause that message to be masked out (or left unmasked) for training (similar to [1]). This is helpful for training the model to be robust and capable of error recovery upon a bad assistant message. A missing `weight` key defaults to weight 1, to guarantee downward compatibility. [1]: https://github.com/mistralai/mistral-finetune --- src/axolotl/prompt_strategies/sharegpt.py | 3 + src/axolotl/prompt_tokenizers.py | 15 +- src/axolotl/prompters.py | 24 ++- tests/prompt_strategies/test_sharegpt.py | 186 +++++++++++++++++++++- 4 files changed, 221 insertions(+), 7 deletions(-) diff --git a/src/axolotl/prompt_strategies/sharegpt.py b/src/axolotl/prompt_strategies/sharegpt.py index 8b452ae199..321f19554b 100644 --- a/src/axolotl/prompt_strategies/sharegpt.py +++ b/src/axolotl/prompt_strategies/sharegpt.py @@ -143,6 +143,9 @@ def get_conversation_thread(self, prompt): role_map[t[role_key]] if t[role_key] in role_map else t[role_key] ), "value": t[value_key], + "weight": 1 + if "weight" not in t or t["weight"] is None + else t["weight"], } for t in conversations ] diff --git a/src/axolotl/prompt_tokenizers.py b/src/axolotl/prompt_tokenizers.py index bb13cf76dd..11dd084a85 100644 --- a/src/axolotl/prompt_tokenizers.py +++ b/src/axolotl/prompt_tokenizers.py @@ -377,7 +377,11 @@ def tokenize_prompt(self, prompt): LOG.warning(f"expected tuple, got {part}") continue - role, content = part + if len(part) <= 2: + role, content = part + weight = 1 + else: + role, content, weight = part # Uses "in" because role contains extra characters input_turn = any(r.lower() in role.lower() for r in input_roles) @@ -403,7 +407,7 @@ def tokenize_prompt(self, prompt): add_eos_token=False, strip_bos_token=True, ) - if self.train_on_inputs: + if self.train_on_inputs and weight == 1: labels = copy.deepcopy(res["input_ids"]) else: # everything from this is masked out from the labels @@ -439,13 +443,18 @@ def tokenize_prompt(self, prompt): labels[:len_role] = [IGNORE_TOKEN_ID] * min( len_role, len(labels) ) + if weight == 0: + # everything from this is masked out from the labels + # (role is masked out too because it makes no sense if contents is masked out) + labels = [IGNORE_TOKEN_ID] * len(res["input_ids"]) + elif empty_role: turn = content # this is only ever the first part, should include the bos token and the user query res = self._tokenize( turn, add_eos_token=False, strip_bos_token=False ) - if self.train_on_inputs: + if self.train_on_inputs and weight == 1: labels = copy.deepcopy(res["input_ids"]) else: # everything from this is masked out from the labels diff --git a/src/axolotl/prompters.py b/src/axolotl/prompters.py index 102e9e53be..0ffa3e55fd 100644 --- a/src/axolotl/prompters.py +++ b/src/axolotl/prompters.py @@ -319,6 +319,7 @@ def _build_result(self, source): conv = self._conversation.copy() + original_source = source.copy() # Add the conversation system prompt if provided, otherwise use the default one if source[0]["from"] == "system": conv.set_system_message(source[0]["value"]) @@ -360,8 +361,27 @@ def _build_result(self, source): LOG.warning(f"{SHAREGPT_ASSERTION_FAILED_ROLE}: {sentence}") conv.append_message(role, sentence["value"]) - - return conv.get_turns() + turns = list(conv.get_turns()) + original_source_length = len(original_source) + assert len(turns) in [ + original_source_length - 1, + original_source_length, + original_source_length + 1, + ] + if len(turns) == original_source_length + 1: + original_source = [{"weight": None}] + original_source + elif len(turns) == original_source_length - 1: + original_source = original_source[1:] + return [ + (*turn, weight) + for turn, weight in zip( + turns, + [ + 1 if "weight" not in e or e["weight"] is None else e["weight"] + for e in original_source + ], + ) + ] def build_prompt(self, source) -> Generator[str, None, None]: turns = self._build_result(source) diff --git a/tests/prompt_strategies/test_sharegpt.py b/tests/prompt_strategies/test_sharegpt.py index 6e69098340..aba53cd5fd 100644 --- a/tests/prompt_strategies/test_sharegpt.py +++ b/tests/prompt_strategies/test_sharegpt.py @@ -52,6 +52,51 @@ def fixture_sharegpt_dataset(): ) +@pytest.fixture(name="sharegpt_dataset_with_weights") +def fixture_sharegpt_dataset_with_weights(): + return Dataset.from_list( + [ + { + "conversations": [ + { + "from": "system", + "value": "repeat", + }, + { + "from": "human", + "value": "hello", + "weight": 1, + }, + { + "from": "gpt", + "value": "hello", + "weight": 0, + }, + { + "from": "human", + "value": "rehello", + "weight": 0, + }, + { + "from": "gpt", + "value": "rehello", + "weight": 1, + }, + { + "from": "human", + "value": "goodbye", + }, + { + "from": "gpt", + "value": "goodbye", + "weight": 0, + }, + ] + } + ] + ) + + @pytest.fixture(name="glaive_dataset") def fixture_sharegpt_glaive_dataset(): return Dataset.from_list( @@ -162,6 +207,46 @@ def test_tokenization(self, sharegpt_dataset, llama3_tokenizer): ] # fmt: on + def test_tokenization_with_weights( + self, sharegpt_dataset_with_weights, llama3_tokenizer + ): + strategy = SimpleShareGPTPromptTokenizingStrategy( + ShareGPTPrompterV2( + conversation="llama3", + role_key_model=None, + role_key_human=None, + ), + llama3_tokenizer, + False, # train_on_inputs + 2048, # sequence_len + ) + + dataset_wrapper = TokenizedPromptDataset( + strategy, sharegpt_dataset_with_weights, process_count=1 + ) + + input_ids = dataset_wrapper[0]["input_ids"] + + # fmt: off + assert input_ids == [ + 128000, # bos + 128006, 9125, 128007, # system header + 271, 31724, 128009, # sys prompt, eot + 128006, 882, 128007, # user header + 271, 15339, 128009, # user prompt eot + 128006, 78191, 128007, # assistant header + 271, 15339, 128009, # assistant response eot + 128006, 882, 128007, + 271, 11310, 4896, 128009, + 128006, 78191, 128007, + 271, 11310, 4896, 128009, + 128006, 882, 128007, + 271, 19045, 29474, 128009, + 128006, 78191, 128007, + 271, 19045, 29474, 128009, + ] + # fmt: on + class TestSharegptChatML: """ @@ -197,7 +282,40 @@ def test_no_double_im_end(self, sharegpt_dataset, tokenizer): ] # fmt: on - def test_w_train_on_input(self, sharegpt_dataset, tokenizer): + def test_no_double_im_end_with_weights( + self, sharegpt_dataset_with_weights, tokenizer + ): + strategy = SimpleShareGPTPromptTokenizingStrategy( + ShareGPTPrompterV2( + conversation="chatml", + role_key_model=None, + role_key_human=None, + ), + tokenizer, + False, # train_on_inputs + 2048, # sequence_len + ) + + dataset_wrapper = TokenizedPromptDataset( + strategy, sharegpt_dataset_with_weights, process_count=1 + ) + + input_ids = dataset_wrapper[0]["input_ids"] + # fmt: off + assert input_ids == [ + # 28705, 13, is " \n" + 1, # bos + 32001, 1587, 13, 25997, 32000, 28705, 13, # system + 32001, 2188, 13, 21558, 32000, 28705, 13, # human + 32001, 13892, 13, 21558, 32000, 28705, 13, # gpt + 32001, 2188, 13, 267, 21558, 32000, 28705, 13, # human + 32001, 13892, 13, 267, 21558, 32000, 28705, 13, # gpt + 32001, 2188, 13, 12684, 17664, 32000, 28705, 13, # human + 32001, 13892, 13, 12684, 17664, 32000, 28705, 13, # gpt + ] + # fmt: on + + def test_no_train_on_input(self, sharegpt_dataset, tokenizer): strategy = SimpleShareGPTPromptTokenizingStrategy( ShareGPTPrompterV2( conversation="chatml", @@ -225,7 +343,39 @@ def test_w_train_on_input(self, sharegpt_dataset, tokenizer): ] # fmt: on - def test_no_train_on_input(self, sharegpt_dataset, tokenizer): + def test_no_train_on_input_with_weights( + self, sharegpt_dataset_with_weights, tokenizer + ): + strategy = SimpleShareGPTPromptTokenizingStrategy( + ShareGPTPrompterV2( + conversation="chatml", + role_key_model=None, + role_key_human=None, + ), + tokenizer, + False, # train_on_inputs + 2048, # sequence_len + ) + + dataset_wrapper = TokenizedPromptDataset( + strategy, sharegpt_dataset_with_weights, process_count=1 + ) + + labels = dataset_wrapper[0]["labels"] + # fmt: off + assert labels == [ + -100, # bos + -100, -100, -100, -100, -100, -100, -100, # system + -100, -100, -100, -100, -100, -100, -100, # human + -100, -100, -100, -100, -100, -100, -100, # gpt with weight zero + -100, -100, -100, -100, -100, -100, -100, -100, # human + -100, -100, 13, 267, 21558, 32000, 28705, 13, # gpt + -100, -100, -100, -100, -100, -100, -100, -100, # human + -100, -100, -100, -100, -100, -100, -100, -100 # gpt with weight zero + ] + # fmt: on + + def test_w_train_on_input(self, sharegpt_dataset, tokenizer): strategy = SimpleShareGPTPromptTokenizingStrategy( ShareGPTPrompterV2( conversation="chatml", @@ -253,6 +403,38 @@ def test_no_train_on_input(self, sharegpt_dataset, tokenizer): ] # fmt: on + def test_w_train_on_input_with_weights( + self, sharegpt_dataset_with_weights, tokenizer + ): + strategy = SimpleShareGPTPromptTokenizingStrategy( + ShareGPTPrompterV2( + conversation="chatml", + role_key_model=None, + role_key_human=None, + ), + tokenizer, + True, # train_on_inputs + 2048, # sequence_len + ) + + dataset_wrapper = TokenizedPromptDataset( + strategy, sharegpt_dataset_with_weights, process_count=1 + ) + + labels = dataset_wrapper[0]["labels"] + # fmt: off + assert labels == [ + 1, # bos + 32001, 1587, 13, 25997, 32000, 28705, 13, # system + 32001, 2188, 13, 21558, 32000, 28705, 13, # human + -100, -100, -100, -100, -100, -100, -100, # gpt with weight 0 + -100, -100, -100, -100, -100, -100, -100, -100, # human with weight 0 + 32001, 13892, 13, 267, 21558, 32000, 28705, 13, # gpt + 32001, 2188, 13, 12684, 17664, 32000, 28705, 13, # human + -100, -100, -100, -100, -100, -100, -100, -100 # gpt with weight 0 + ] + # fmt: on + def test_chatml_glaive(self, glaive_dataset, tokenizer): strategy = GlaiveShareGPTPromptTokenizingStrategy( ShareGPTPrompterV2( From f2480a1d9199b213066b8fe4e512b2f260e86c6a Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Wed, 26 Jun 2024 13:13:21 -0700 Subject: [PATCH 0054/1405] improve Pre-Tokenized Dataset docs (#1684) [skip ci] Fixes #1661 --- docs/dataset-formats/tokenized.qmd | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/dataset-formats/tokenized.qmd b/docs/dataset-formats/tokenized.qmd index 8991a21109..b2ea003c02 100644 --- a/docs/dataset-formats/tokenized.qmd +++ b/docs/dataset-formats/tokenized.qmd @@ -4,9 +4,25 @@ description: How to use a custom pre-tokenized dataset. order: 5 --- -- Do not pass a `type:` in your axolotl config. +- Pass an empty `type:` in your axolotl config. - Columns in Dataset must be exactly `input_ids`, `attention_mask`, `labels` +- To indicate that a token should be ignored during training, set its corresponding label to `-100`. +- Do not add BOS/EOS. Axolotl will add them for you based on the default tokenizer for the model you're using. +- For pretraining, do not truncate/pad documents to the context window length. +- For instruction training, documents must be truncated/padded as desired. + +Sample config: ```{.yaml filename="config.yml"} -- path: ... +datasets: + - path: /path/to/your/file.jsonl + ds_type: json + type: +``` + +Sample jsonl: + +```jsonl +{"input_ids":[271,299,99],"attention_mask":[1,1,1],"labels":[271,-100,99]} +{"input_ids":[87,227,8383,12],"attention_mask":[1,1,1,1],"labels":[87,227,8383,12]} ``` From 5370cedf0cb6c2cfeb98c944ce58d2ebf7a780aa Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 29 Jun 2024 01:38:55 -0400 Subject: [PATCH 0055/1405] support for gemma2 w sample packing (#1718) --- examples/gemma2/qlora.yml | 68 +++++++++++++++++++ requirements.txt | 2 +- src/axolotl/core/trainer_builder.py | 2 + .../monkeypatch/llama_attn_hijack_flash.py | 5 +- .../monkeypatch/mistral_attn_hijack_flash.py | 5 +- src/axolotl/monkeypatch/multipack.py | 5 ++ .../prompt_strategies/chat_template.py | 11 +++ .../config/models/input/v0_4_1/__init__.py | 1 + tests/e2e/patched/test_llama_s2_attention.py | 3 + 9 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 examples/gemma2/qlora.yml diff --git a/examples/gemma2/qlora.yml b/examples/gemma2/qlora.yml new file mode 100644 index 0000000000..b6dd653750 --- /dev/null +++ b/examples/gemma2/qlora.yml @@ -0,0 +1,68 @@ +base_model: google/gemma-2-9b +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer + +load_in_8bit: false +load_in_4bit: true +strict: false + +# huggingface repo +chat_template: gemma +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + chat_template: gemma + drop_system_message: true +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: true + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: diff --git a/requirements.txt b/requirements.txt index 52f98042cd..60b07a824c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.11.1 -transformers==4.41.1 +transformers==4.42.3 tokenizers==0.19.1 bitsandbytes==0.43.1 accelerate==0.30.1 diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 1807952df2..0c69f0be62 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1091,6 +1091,8 @@ def build(self, total_num_steps): warmup_steps = max(int(self.cfg.warmup_ratio * total_num_steps), 0) else: warmup_steps = min(int(0.03 * total_num_steps), 100) + if warmup_steps == 1: + warmup_steps = 2 logging_steps = ( self.cfg.logging_steps diff --git a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py index dda5da2b7a..6d7a23f0db 100644 --- a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py @@ -112,7 +112,7 @@ def replace_llama_attn_with_flash_attn( CrossEntropyLoss, inplace_backward=True ) except ImportError: - LOG.info( + LOG.warning( "optimized flash-attention CrossEntropyLoss not found (run `pip install 'git+https://github.com/Dao-AILab/flash-attention.git#egg=xentropy_cuda_lib&subdirectory=csrc/xentropy'`)" ) @@ -130,7 +130,7 @@ def __init__(self, hidden_size, eps=1e-6): LOG.info("patching with flash_attn.ops.rms_norm") transformers.models.llama.modeling_llama.LlamaRMSNorm = LlamaRMSNorm except ImportError: - LOG.info( + LOG.warning( "optimized flash-attention RMSNorm not found (run `pip install 'git+https://github.com/Dao-AILab/flash-attention.git#egg=dropout_layer_norm&subdirectory=csrc/layer_norm'`)" ) @@ -826,7 +826,6 @@ def custom_forward(*inputs): past_key_value=past_key_value, output_attentions=output_attentions, use_cache=use_cache, - padding_mask=padding_mask, cu_seqlens=cu_seqlens, max_seqlen=max_seqlen, ) diff --git a/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py b/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py index 6ae2e75fa2..c5425dd520 100644 --- a/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py @@ -145,7 +145,7 @@ def flashattn_forward( kv_seq_len = key_states.shape[-2] if past_key_value is not None: kv_seq_len += past_key_value[0].shape[-2] - cos, sin = self.rotary_emb(value_states, seq_len=kv_seq_len) + cos, sin = self.rotary_emb(value_states, position_ids=position_ids) query_states, key_states = apply_rotary_pos_emb( query_states, key_states, cos, sin, position_ids ) @@ -422,6 +422,9 @@ def mistral_model_forward( output_attentions: Optional[bool] = None, output_hidden_states: Optional[bool] = None, return_dict: Optional[bool] = None, + cache_position: Optional[ # pylint: disable=unused-argument + torch.LongTensor + ] = None, ) -> Union[Tuple, BaseModelOutputWithPast]: output_attentions = ( output_attentions diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 7f6296bb67..e319596d02 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -16,6 +16,7 @@ "falcon", "phi", "gemma", + "gemma2", "gemmoe", "starcoder2", "deepseek_v2", @@ -49,6 +50,10 @@ def patch_for_multipack(model_type, model_name=None): transformers.models.gemma.modeling_gemma._get_unpad_data = ( # pylint: disable=protected-access get_unpad_data ) + elif model_type == "gemma2": + transformers.models.gemma2.modeling_gemma2._get_unpad_data = ( # pylint: disable=protected-access + get_unpad_data + ) elif model_type == "starcoder2": transformers.models.starcoder2.modeling_starcoder2._get_unpad_data = ( # pylint: disable=protected-access get_unpad_data diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 0e7d823ed8..8c7a8dd4f6 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -23,6 +23,7 @@ def __init__( message_field_role: str = "from", message_field_content: str = "value", roles: Optional[Dict[str, List[str]]] = None, + drop_system_message: bool = False, ): if roles: self.roles = {s: t for t, sources in roles.items() for s in sources} @@ -39,6 +40,7 @@ def __init__( self.tokenizer = tokenizer self.chat_template = chat_template self.max_length = max_length + self.drop_system_message = drop_system_message def build_prompt(self, conversation, add_generation_prompt=False): turns = [ @@ -49,6 +51,9 @@ def build_prompt(self, conversation, add_generation_prompt=False): for t in conversation ] + if self.drop_system_message and turns[0]["role"] == "system": + turns = turns[1:] + return self.tokenizer.apply_chat_template( turns, truncation=True, @@ -111,6 +116,11 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): else "value" ) roles = ds_cfg["roles"] if ds_cfg and "roles" in ds_cfg else None + drop_system_message = ( + ds_cfg["drop_system_message"] + if ds_cfg and "drop_system_message" in ds_cfg + else False + ) strategy = ChatTemplateStrategy( ChatTemplatePrompter( @@ -119,6 +129,7 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): message_field_role=message_field_role, message_field_content=message_field_content, roles=roles, + drop_system_message=drop_system_message, ), tokenizer, cfg.train_on_inputs, diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index dbf9b02c03..1747c46b1a 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -116,6 +116,7 @@ class SFTDataset(BaseModel): message_field_content: Optional[str] = None roles: Optional[Dict[str, List[str]]] = None + drop_system_message: Optional[bool] = None class UserDefinedDPOType(BaseModel): diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py index f1d37eb3ca..0f2539daf8 100644 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ b/tests/e2e/patched/test_llama_s2_attention.py @@ -7,6 +7,8 @@ import unittest from pathlib import Path +import pytest + from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs from axolotl.train import train @@ -19,6 +21,7 @@ os.environ["WANDB_DISABLED"] = "true" +@pytest.mark.skip(reason="FIXME?") class TestLlamaShiftedSparseAttention(unittest.TestCase): """ Test case for Llama models using S2 Attn From c6d83a87c458f7d20f388a73e203a25715b625d0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 2 Jul 2024 13:17:40 -0400 Subject: [PATCH 0056/1405] add support for .env files for env vars (#1724) --- requirements.txt | 1 + src/axolotl/cli/inference.py | 2 ++ src/axolotl/cli/merge_lora.py | 2 ++ src/axolotl/cli/preprocess.py | 2 ++ src/axolotl/cli/shard.py | 2 ++ src/axolotl/cli/train.py | 2 ++ 6 files changed, 11 insertions(+) diff --git a/requirements.txt b/requirements.txt index 60b07a824c..ee808de76a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,6 +31,7 @@ art fschat @ git+https://github.com/lm-sys/FastChat.git@27a05b04a35510afb1d767ae7e5990cbd278f8fe gradio==3.50.2 tensorboard +python-dotenv==1.0.1 mamba-ssm==1.2.0.post1 diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index 86ad8409ff..adc991456d 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -5,6 +5,7 @@ import fire import transformers +from dotenv import load_dotenv from axolotl.cli import ( do_inference, @@ -33,4 +34,5 @@ def do_cli(config: Path = Path("examples/"), gradio=False, **kwargs): if __name__ == "__main__": + load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 8db3fa9897..6588b5ee4e 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -5,6 +5,7 @@ import fire import transformers +from dotenv import load_dotenv from axolotl.cli import do_merge_lora, load_cfg, print_axolotl_text_art from axolotl.common.cli import TrainerCliArgs @@ -48,4 +49,5 @@ def do_cli(config: Path = Path("examples/"), **kwargs): if __name__ == "__main__": + load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index f43277e49c..5ec279d4b2 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -9,6 +9,7 @@ import transformers from accelerate import init_empty_weights from colorama import Fore +from dotenv import load_dotenv from transformers import AutoModelForCausalLM from axolotl.cli import ( @@ -86,4 +87,5 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): if __name__ == "__main__": + load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/shard.py b/src/axolotl/cli/shard.py index 48f22790ac..196c0e99a6 100644 --- a/src/axolotl/cli/shard.py +++ b/src/axolotl/cli/shard.py @@ -7,6 +7,7 @@ import fire import transformers +from dotenv import load_dotenv from axolotl.cli import load_cfg, print_axolotl_text_art from axolotl.common.cli import TrainerCliArgs, load_model_and_tokenizer @@ -40,4 +41,5 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): if __name__ == "__main__": + load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 7bb4a51844..050f18a054 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -6,6 +6,7 @@ from typing import Tuple, Union import fire +from dotenv import load_dotenv from transformers.hf_argparser import HfArgumentParser from transformers.modeling_utils import PreTrainedModel from transformers.tokenization_utils import PreTrainedTokenizer @@ -67,4 +68,5 @@ def do_train(cfg, cli_args) -> Tuple[PreTrainedModel, PreTrainedTokenizer]: if __name__ == "__main__": + load_dotenv() fire.Fire(do_cli) From c69b7eb2b5798998c16c197a6bd0903b4cc99e92 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 5 Jul 2024 09:15:36 -0400 Subject: [PATCH 0057/1405] full weights fsdp training seems broken with fsdp_cpu_ram_efficient_loading, disabling for now (#1726) --- src/axolotl/utils/models.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index a8df4bbad7..4e0d23c4f6 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -569,9 +569,11 @@ def load_model( try: skip_move_to_device = False - if ( - cfg.fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading - ) and not qlora_fsdp: + if ( # pylint: disable=condition-evals-to-constant) + (cfg.fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading) + and not qlora_fsdp + and False + ): model = load_sharded_model( base_model, model_config, From b3f680d305528f94c3d0663dc825cc17fcabfea3 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Fri, 5 Jul 2024 06:24:07 -0700 Subject: [PATCH 0058/1405] sanity check ranges in freeze.py (#1686) * sanity check ranges in freeze.py this will catch problems earlier and more clearly. in my case, it appears that deepspeed zero3 sets layer tensor shapes to [0], which doesn't play well with automatically inferred ranges. through a bit of luck, inverting ranges still appears to work correctly. * simplify chained comparison --- src/axolotl/utils/freeze.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/axolotl/utils/freeze.py b/src/axolotl/utils/freeze.py index e3d0fd1446..f96bc120d5 100644 --- a/src/axolotl/utils/freeze.py +++ b/src/axolotl/utils/freeze.py @@ -120,6 +120,9 @@ def _merge_ranges( processed_ranges = [ (start, end if end is not None else layer_size) for start, end in given_ranges ] + for start, end in processed_ranges: + if start < 0 or end > layer_size > 0 or start >= end: + raise ValueError(f"invalid unfreeze range: start={start}, end={end}") # No need to merge if there's only one or no ranges if len(processed_ranges) <= 1: From a159724e44020e122e09cc383c02467bc0eca7ba Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 10 Jul 2024 11:15:44 -0400 Subject: [PATCH 0059/1405] bump trl and accelerate for latest releases (#1730) * bump trl and accelerate for latest releases * ensure that the CI runs on new gh org * drop kto_pair support since removed upstream --- .github/workflows/base.yml | 2 +- .github/workflows/main.yml | 6 +++--- .github/workflows/nightlies.yml | 4 ++-- .github/workflows/tests.yml | 2 +- docs/config.qmd | 2 +- requirements.txt | 4 ++-- src/axolotl/core/trainer_builder.py | 6 ++---- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 - src/axolotl/utils/models.py | 6 +----- src/axolotl/utils/trainer.py | 2 +- tests/e2e/test_dpo.py | 1 + 11 files changed, 15 insertions(+), 21 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 4c1b0463a7..d215ea44c3 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -5,7 +5,7 @@ on: jobs: build-base: - if: github.repository_owner == 'OpenAccess-AI-Collective' + if: github.repository_owner == 'axolotl-ai-cloud' # this job needs to be run on self-hosted GPU runners... runs-on: axolotl-gpu-runner strategy: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d0d0289824..8bced628d2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ on: jobs: build-axolotl: - if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'OpenAccess-AI-Collective' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} strategy: fail-fast: false matrix: @@ -70,7 +70,7 @@ jobs: build-axolotl-cloud: needs: build-axolotl - if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'OpenAccess-AI-Collective' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... strategy: matrix: @@ -128,7 +128,7 @@ jobs: build-axolotl-cloud-no-tmux: needs: build-axolotl - if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'OpenAccess-AI-Collective' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... strategy: matrix: diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index f668e5f65b..6dc22b6bf0 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -7,7 +7,7 @@ on: jobs: build-axolotl: - if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'OpenAccess-AI-Collective' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} strategy: fail-fast: false matrix: @@ -70,7 +70,7 @@ jobs: build-axolotl-cloud: needs: build-axolotl - if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'OpenAccess-AI-Collective' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... strategy: matrix: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index db9173cac4..2e2d0968d0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -58,7 +58,7 @@ jobs: pytest --ignore=tests/e2e/ tests/ docker-e2e-tests: - if: github.repository_owner == 'OpenAccess-AI-Collective' + if: github.repository_owner == 'axolotl-ai-cloud' # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] timeout-minutes: 60 diff --git a/docs/config.qmd b/docs/config.qmd index 1c87386a6d..e859999787 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -138,7 +138,7 @@ test_datasets: data_files: - /workspace/data/eval.jsonl -# use RL training: 'dpo', 'ipo', 'kto_pair' +# use RL training: 'dpo', 'ipo', 'kto' rl: # Saves the desired chat template to the tokenizer_config.json for easier inferencing diff --git a/requirements.txt b/requirements.txt index ee808de76a..c8d1687349 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ peft==0.11.1 transformers==4.42.3 tokenizers==0.19.1 bitsandbytes==0.43.1 -accelerate==0.30.1 +accelerate==0.32.0 deepspeed @ git+https://github.com/microsoft/DeepSpeed.git@bc48371c5e1fb8fd70fc79285e66201dbb65679b pydantic==2.6.3 addict @@ -40,6 +40,6 @@ s3fs gcsfs # adlfs -trl @ git+https://github.com/huggingface/trl.git@f18253bf2d747f68acc9cd89da95c85ebf59dbb9 +trl==0.9.6 zstandard==0.22.0 fastcore diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 0c69f0be62..ec175454e9 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1670,8 +1670,6 @@ def build(self, total_num_steps): dpo_trainer_kwargs["loss_type"] = "ipo" if self.cfg.dpo_label_smoothing: dpo_trainer_kwargs["label_smoothing"] = self.cfg.dpo_label_smoothing - elif self.cfg.rl == "kto_pair": - dpo_trainer_kwargs["loss_type"] = "kto_pair" if self.eval_dataset: dpo_trainer_kwargs["eval_dataset"] = self.eval_dataset if self.cfg.adapter and self.peft_config: @@ -1680,7 +1678,7 @@ def build(self, total_num_steps): dpo_trainer_kwargs[ "precompute_ref_log_probs" ] = self.cfg.precompute_ref_log_probs - if self.cfg.rl in ["dpo", "ipo", "kto_pair"]: + if self.cfg.rl in ["dpo", "ipo"]: trainer_cls = AxolotlDPOTrainer dpo_trainer_kwargs["beta"] = self.cfg.rl_beta or 0.1 trainer_cls_args = [self.model, self.model_ref] @@ -1695,7 +1693,7 @@ def build(self, total_num_steps): elif self.cfg.rl == "orpo": trainer_cls = AxolotlORPOTrainer trainer_cls_args = [self.model] - elif self.cfg.rl == "kto": + elif self.cfg.rl in ["kto"]: trainer_cls = AxolotlKTOTrainer trainer_cls_args = [self.model] else: diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 1747c46b1a..3cac4f8391 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -165,7 +165,6 @@ class RLType(str, Enum): dpo = "dpo" # pylint: disable=invalid-name ipo = "ipo" # pylint: disable=invalid-name - kto_pair = "kto_pair" # pylint: disable=invalid-name orpo = "orpo" # pylint: disable=invalid-name kto = "kto" # pylint: disable=invalid-name diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 4e0d23c4f6..d479d425d3 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -805,11 +805,7 @@ def load_model( if not reference_model or cfg.lora_model_dir: # if we're not loading the reference model, then we're loading the model for training # then the dpo trainer doesn't want the peft model loaded over it, it just wants the lora/peft config - if ( - cfg.adapter - and cfg.rl in ["dpo", "ipo", "kto_pair", "kto"] - and not cfg.merge_lora - ): + if cfg.adapter and cfg.rl in ["dpo", "ipo", "kto"] and not cfg.merge_lora: _, lora_config = load_lora(model, cfg, inference=False, config_only=True) else: model, lora_config = load_adapter(model, cfg, cfg.adapter) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 6760dc4882..a16baaae0f 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -427,7 +427,7 @@ def prepare_optim_env(cfg): def setup_trainer(cfg, train_dataset, eval_dataset, model, tokenizer, total_num_steps): - if cfg.rl in ["dpo", "ipo", "kto_pair", "orpo", "kto"]: + if cfg.rl in ["dpo", "ipo", "orpo", "kto"]: trainer_builder = HFRLTrainerBuilder(cfg, model[0], tokenizer) trainer_builder.model_ref = model[1] trainer_builder.peft_config = model[2] diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index 5f03e6bc1b..1c354e9a01 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -115,6 +115,7 @@ def test_dpo_nll_lora(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + @pytest.mark.skip("kto_pair no longer supported in trl") @with_temp_dir def test_kto_pair_lora(self, temp_dir): # pylint: disable=duplicate-code From 1194c2e0b148ab16a012873609bf8ee99923fd9c Mon Sep 17 00:00:00 2001 From: mhenrichsen Date: Thu, 11 Jul 2024 15:19:29 +0200 Subject: [PATCH 0060/1405] github urls (#1734) Co-authored-by: Henrichsen, Mads (ext) --- .github/CONTRIBUTING.md | 8 ++++---- .github/ISSUE_TEMPLATE/bug-report.yaml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/docs.yml | 2 +- .github/ISSUE_TEMPLATE/feature-request.yaml | 4 ++-- README.md | 16 ++++++++-------- _quarto.yml | 2 +- cicd/Dockerfile.jinja | 2 +- docker/Dockerfile | 2 +- docker/Dockerfile-tests | 2 +- docs/debugging.qmd | 2 +- docs/fsdp_qlora.qmd | 4 ++-- docs/input_output.qmd | 4 ++-- .../colab-notebooks/colab-axolotl-example.ipynb | 2 +- scripts/motd | 2 +- src/axolotl/train.py | 2 +- 16 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9eec23e1a3..29769efb56 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -21,12 +21,12 @@ All contributors are expected to adhere to our [Code of Conduct](CODE_OF_CONDUCT ## Getting Started -Bugs? Please check for open issue else create a new [Issue](https://github.com/OpenAccess-AI-Collective/axolotl/issues/new). +Bugs? Please check for open issue else create a new [Issue](https://github.com/axolotl-ai-cloud/axolotl/issues/new). PRs are **greatly welcome**! 1. Fork the repository and clone it to your local machine. -2. Set up the development environment by following the instructions in the [README.md](https://github.com/OpenAccess-AI-Collective/axolotl/tree/main/README.md) file. +2. Set up the development environment by following the instructions in the [README.md](https://github.com/axolotl-ai-cloud/axolotl/tree/main/README.md) file. 3. Explore the codebase, run tests, and verify that everything works as expected. Please run below to setup env @@ -42,11 +42,11 @@ pytest tests/ ### Reporting Bugs -If you encounter a bug or issue while using axolotl, please open a new issue on the [GitHub Issues](https://github.com/OpenAccess-AI-Collective/axolotl/issues) page. Provide a clear and concise description of the problem, steps to reproduce it, and any relevant error messages or logs. +If you encounter a bug or issue while using axolotl, please open a new issue on the [GitHub Issues](https://github.com/axolotl-ai-cloud/axolotl/issues) page. Provide a clear and concise description of the problem, steps to reproduce it, and any relevant error messages or logs. ### Suggesting Enhancements -We welcome ideas for improvements and new features. To suggest an enhancement, open a new issue on the [GitHub Issues](https://github.com/OpenAccess-AI-Collective/axolotl/issues) page. Describe the enhancement in detail, explain the use case, and outline the benefits it would bring to the project. +We welcome ideas for improvements and new features. To suggest an enhancement, open a new issue on the [GitHub Issues](https://github.com/axolotl-ai-cloud/axolotl/issues) page. Describe the enhancement in detail, explain the use case, and outline the benefits it would bring to the project. ### Submitting Pull Requests diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml index 1ed703d4e8..0bfe067ab6 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -15,7 +15,7 @@ body: label: "Please check that this issue hasn't been reported before." description: "The **Label filters** may help make your search more focussed." options: - - label: "I searched previous [Bug Reports](https://github.com/OpenAccess-AI-Collective/axolotl/labels/bug) didn't find any similar reports." + - label: "I searched previous [Bug Reports](https://github.com/axolotl-ai-cloud/axolotl/labels/bug) didn't find any similar reports." required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e0c5754fb9..973b2d7fb4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - name: Ask a question - url: https://github.com/OpenAccess-AI-Collective/axolotl/discussions/categories/q-a + url: https://github.com/axolotl-ai-cloud/axolotl/discussions/categories/q-a about: Ask questions and discuss with other community members - name: Discuss the Project in Discord url: https://discord.gg/HhrNrHJPRb diff --git a/.github/ISSUE_TEMPLATE/docs.yml b/.github/ISSUE_TEMPLATE/docs.yml index 2c27af1aa0..9a926c384f 100644 --- a/.github/ISSUE_TEMPLATE/docs.yml +++ b/.github/ISSUE_TEMPLATE/docs.yml @@ -10,7 +10,7 @@ body: value: | * Ask questions in [Discord](https://discord.gg/HhrNrHJPRb). * Before you file an issue read the [Contributing guide](./CONTRIBUTING.md). - * Check to make sure someone hasn't already opened a [similar issue](https://github.com/OpenAccess-AI-Collective/axolotl/issues). + * Check to make sure someone hasn't already opened a [similar issue](https://github.com/axolotl-ai-cloud/axolotl/issues). - type: textarea attributes: label: What piece of documentation is affected? diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml index 39b6cb74e1..d90ebd274a 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/feature-request.yaml @@ -8,9 +8,9 @@ body: label: "⚠️ Please check that this feature request hasn't been suggested before." description: "There are two locations for previous feature requests. Please search in both. Thank you. The **Label filters** may help make your search more focussed." options: - - label: "I searched previous [Ideas in Discussions](https://github.com/OpenAccess-AI-Collective/axolotl/discussions/categories/ideas) didn't find any similar feature requests." + - label: "I searched previous [Ideas in Discussions](https://github.com/axolotl-ai-cloud/axolotl/discussions/categories/ideas) didn't find any similar feature requests." required: true - - label: "I searched previous [Issues](https://github.com/OpenAccess-AI-Collective/axolotl/labels/enhancement) didn't find any similar feature requests." + - label: "I searched previous [Issues](https://github.com/axolotl-ai-cloud/axolotl/labels/enhancement) didn't find any similar feature requests." required: true - type: textarea diff --git a/README.md b/README.md index 004eb11cf4..6b35702b3a 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ Features:

Go ahead and Axolotl questions!!

- pre-commit - PyTest Status + pre-commit + PyTest Status @@ -107,7 +107,7 @@ Get started with Axolotl in just a few steps! This quickstart guide will walk yo **Requirements**: Python >=3.10 and Pytorch >=2.1.1. ```bash -git clone https://github.com/OpenAccess-AI-Collective/axolotl +git clone https://github.com/axolotl-ai-cloud/axolotl cd axolotl pip3 install packaging ninja @@ -132,7 +132,7 @@ accelerate launch -m axolotl.cli.inference examples/openllama-3b/lora.yml \ # remote yaml files - the yaml config can be hosted on a public URL # Note: the yaml config must directly link to the **raw** yaml -accelerate launch -m axolotl.cli.train https://raw.githubusercontent.com/OpenAccess-AI-Collective/axolotl/main/examples/openllama-3b/lora.yml +accelerate launch -m axolotl.cli.train https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/openllama-3b/lora.yml ``` ## Advanced Setup @@ -626,10 +626,10 @@ Need dedicated support? Please contact us at [✉️wing@openaccessaicollective. Building something cool with Axolotl? Consider adding a badge to your model card. ```markdown -[Built with Axolotl](https://github.com/OpenAccess-AI-Collective/axolotl) +[Built with Axolotl](https://github.com/axolotl-ai-cloud/axolotl) ``` -[Built with Axolotl](https://github.com/OpenAccess-AI-Collective/axolotl) +[Built with Axolotl](https://github.com/axolotl-ai-cloud/axolotl) ## Community Showcase @@ -647,7 +647,7 @@ PocketDoc Labs Please read the [contributing guide](./.github/CONTRIBUTING.md) -Bugs? Please check the [open issues](https://github.com/OpenAccess-AI-Collective/axolotl/issues/bug) else create a new Issue. +Bugs? Please check the [open issues](https://github.com/axolotl-ai-cloud/axolotl/issues/bug) else create a new Issue. PRs are **greatly welcome**! @@ -665,7 +665,7 @@ pre-commit run --all-files Thanks to all of our contributors to date. Help drive open source AI progress forward by contributing to Axolotl. - + contributor chart by https://contrib.rocks diff --git a/_quarto.yml b/_quarto.yml index 749f68cce6..009fa8056a 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -14,7 +14,7 @@ website: - icon: twitter href: https://twitter.com/axolotl_ai - icon: github - href: https://github.com/OpenAccess-AI-Collective/axolotl/ + href: https://github.com/axolotl-ai-cloud/axolotl/ - icon: discord href: https://discord.gg/7m9sfhzaf3 diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index ce03e08c2f..96c312ddcd 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -14,7 +14,7 @@ RUN apt-get update && \ WORKDIR /workspace -RUN git clone --depth=1 https://github.com/OpenAccess-AI-Collective/axolotl.git +RUN git clone --depth=1 https://github.com/axolotl-ai-cloud/axolotl.git WORKDIR /workspace/axolotl diff --git a/docker/Dockerfile b/docker/Dockerfile index 6b9cf7d4c4..cdb6d177a5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,7 +15,7 @@ RUN apt-get update && \ WORKDIR /workspace -RUN git clone --depth=1 https://github.com/OpenAccess-AI-Collective/axolotl.git +RUN git clone --depth=1 https://github.com/axolotl-ai-cloud/axolotl.git WORKDIR /workspace/axolotl diff --git a/docker/Dockerfile-tests b/docker/Dockerfile-tests index d5c0595a87..4a946372df 100644 --- a/docker/Dockerfile-tests +++ b/docker/Dockerfile-tests @@ -16,7 +16,7 @@ RUN apt-get update && \ WORKDIR /workspace -RUN git clone --depth=1 https://github.com/OpenAccess-AI-Collective/axolotl.git +RUN git clone --depth=1 https://github.com/axolotl-ai-cloud/axolotl.git WORKDIR /workspace/axolotl diff --git a/docs/debugging.qmd b/docs/debugging.qmd index 7237fbd6f2..1d0779b073 100644 --- a/docs/debugging.qmd +++ b/docs/debugging.qmd @@ -192,7 +192,7 @@ Using [official Axolotl Docker images](https://hub.docker.com/r/winglian/axolotl On the host that is running axolotl (ex: if you are using a remote host), clone the axolotl repo and change your current directory to the root: ```bash -git clone https://github.com/OpenAccess-AI-Collective/axolotl +git clone https://github.com/axolotl-ai-cloud/axolotl cd axolotl ``` diff --git a/docs/fsdp_qlora.qmd b/docs/fsdp_qlora.qmd index 7f12d44935..7af2a3eba2 100644 --- a/docs/fsdp_qlora.qmd +++ b/docs/fsdp_qlora.qmd @@ -20,7 +20,7 @@ To enable `QLoRA` with `FSDP`, you need to perform the following steps: > See the [example config](#example-config) file in addition to reading these instructions. 1. Set `adapter: qlora` in your axolotl config file. -2. Enable FSDP in your axolotl config, as [described here](https://github.com/OpenAccess-AI-Collective/axolotl?tab=readme-ov-file#fsdp). +2. Enable FSDP in your axolotl config, as [described here](https://github.com/axolotl-ai-cloud/axolotl?tab=readme-ov-file#fsdp). 3. Use one of the supported model types: `llama`, `mistral` or `mixtral`. ## Example Config @@ -29,7 +29,7 @@ To enable `QLoRA` with `FSDP`, you need to perform the following steps: ## References -- [PR #1378](https://github.com/OpenAccess-AI-Collective/axolotl/pull/1378) enabling QLoRA in FSDP in Axolotl. +- [PR #1378](https://github.com/axolotl-ai-cloud/axolotl/pull/1378) enabling QLoRA in FSDP in Axolotl. - [Blog Post](https://www.answer.ai/posts/2024-03-06-fsdp-qlora.html) from the [Answer.AI](https://www.answer.ai/) team describing the work that enabled QLoRA in FSDP. - Related HuggingFace PRs Enabling FDSP + QLoRA: - Accelerate [PR#2544](https://github.com/huggingface/accelerate/pull/2544 ) diff --git a/docs/input_output.qmd b/docs/input_output.qmd index 3762901b31..7715dd250d 100644 --- a/docs/input_output.qmd +++ b/docs/input_output.qmd @@ -25,7 +25,7 @@ description: "Template-free prompt construction with the `input_output` format" ### Masking Inputs One of the most popular features of -[axolotl](https://github.com/OpenAccess-AI-Collective/axolotl) is +[axolotl](https://github.com/axolotl-ai-cloud/axolotl) is setting the following configuration value: @@ -33,7 +33,7 @@ setting the following configuration value: train_on_inputs: false ``` -If you declare a [dataset formats](https://github.com/OpenAccess-AI-Collective/axolotl?tab=readme-ov-file#dataset) +If you declare a [dataset formats](https://github.com/axolotl-ai-cloud/axolotl?tab=readme-ov-file#dataset) such as `alpaca` or `chatml`, axolotl knows what is an input (i.e. human) vs. an output (i.e. the assistant) and masks the input labels so that your model can focus on predicting the outputs only. diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index bb6167e582..94477eb19a 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -44,7 +44,7 @@ "outputs": [], "source": [ "!pip install torch==\"2.1.2\"\n", - "!pip install -e git+https://github.com/OpenAccess-AI-Collective/axolotl#egg=axolotl\n", + "!pip install -e git+https://github.com/axolotl-ai-cloud/axolotl#egg=axolotl\n", "!pip install flash-attn==\"2.5.0\"\n", "!pip install deepspeed==\"0.13.1\"!pip install mlflow==\"2.13.0\"" ] diff --git a/scripts/motd b/scripts/motd index 060a5c5c6c..9d9edbd6d6 100644 --- a/scripts/motd +++ b/scripts/motd @@ -11,7 +11,7 @@ Welcome to the axolotl cloud image! If the you've mounted a disk to /workspace a ``` cd /workspace rm -rf /workspace/axolotl -git clone https://github.com/OpenAccess-AI-Collective/axolotl.git +git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl pip install --no-deps -e . ``` diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 89d83f9f15..99a9b0ba97 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -144,7 +144,7 @@ def terminate_handler(_, __, model_weakref): lambda signum, frame: terminate_handler(signum, frame, _model_weakref), ) - badge_markdown = """[Built with Axolotl](https://github.com/OpenAccess-AI-Collective/axolotl)""" + badge_markdown = """[Built with Axolotl](https://github.com/axolotl-ai-cloud/axolotl)""" transformers.modelcard.AUTOGENERATED_TRAINER_COMMENT += f"\n{badge_markdown}" if getattr(cfg, "axolotl_config_path"): From 47e19164847c3f3a3f0333339317f70126edc67f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 11 Jul 2024 16:43:19 -0400 Subject: [PATCH 0061/1405] add tests so CI can catch updates where patches will break with unsloth (#1737) [skip ci] --- src/axolotl/monkeypatch/unsloth_.py | 8 +++--- tests/e2e/patched/test_unsloth_integration.py | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/patched/test_unsloth_integration.py diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py index de8260414e..6af3046e13 100644 --- a/src/axolotl/monkeypatch/unsloth_.py +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -80,8 +80,9 @@ def get_forward_code() -> str: return forward -def test_cel_is_patchable() -> bool: +def check_cel_is_patchable() -> bool: forward = get_forward_code() + forward, _ = detab_code(forward) return ORIGINAL_CEL_CODE in forward @@ -90,9 +91,10 @@ def get_self_attn_code() -> str: return forward -def test_self_attn_is_patchable() -> bool: +def check_self_attn_is_patchable() -> bool: qkv = get_self_attn_code() - return ORIGINAL_QKV_CODE in qkv and ORIGINAL_QKV_CODE in qkv + qkv, _ = detab_code(qkv) + return ORIGINAL_QKV_CODE in qkv and ORIGINAL_O_CODE in qkv def integrate_cross_entropy_loss_patch(): diff --git a/tests/e2e/patched/test_unsloth_integration.py b/tests/e2e/patched/test_unsloth_integration.py new file mode 100644 index 0000000000..39c7abb1c1 --- /dev/null +++ b/tests/e2e/patched/test_unsloth_integration.py @@ -0,0 +1,25 @@ +"""Test module for checking whether the integration of Unsloth with Hugging Face Transformers is working as expected.""" +import unittest + +from axolotl.monkeypatch.unsloth_ import ( + check_cel_is_patchable, + check_self_attn_is_patchable, +) + + +class TestUnslothIntegration(unittest.TestCase): + """Unsloth monkeypatch integration tests.""" + + def test_is_cel_patchable(self): + # ensures the current version of transformers has loss code that matches our patching code + self.assertTrue( + check_cel_is_patchable(), + "HF transformers loss code has changed and isn't patchable", + ) + + def test_is_self_attn_patchable(self): + # ensures the current version of transformers has loss code that matches our patching code + self.assertTrue( + check_self_attn_is_patchable(), + "HF transformers self attention code has changed and isn't patchable", + ) From 18abdb447a5a439bd465e6d0b6d6ece6da4c0d08 Mon Sep 17 00:00:00 2001 From: Oliver Klingefjord Date: Sat, 13 Jul 2024 03:24:01 +0200 Subject: [PATCH 0062/1405] typo (#1685) [skip ci] * typo * typo 2 --------- Co-authored-by: mhenrichsen --- examples/colab-notebooks/colab-axolotl-example.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 94477eb19a..3fcc4d2a97 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -171,7 +171,7 @@ }, "outputs": [], "source": [ - "# Buy using the ! the comand will be executed as a bash command\n", + "# By using the ! the comand will be executed as a bash command\n", "!accelerate launch -m axolotl.cli.train /content/test_axolotl.yaml" ] }, @@ -188,7 +188,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Buy using the ! the comand will be executed as a bash command\n", + "# By using the ! the comand will be executed as a bash command\n", "!accelerate launch -m axolotl.cli.inference /content/test_axolotl.yaml \\\n", " --qlora_model_dir=\"./qlora-out\" --gradio" ] From 137d84d1b408e70e679fc3604109298e76c9bd63 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 13 Jul 2024 09:41:51 -0400 Subject: [PATCH 0063/1405] add torch 2.3.1 base image (#1745) --- .github/workflows/base.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index d215ea44c3..f8eaff2703 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -37,6 +37,11 @@ jobs: python_version: "3.11" pytorch: 2.3.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + - cuda: "121" + cuda_version: 12.1.0 + python_version: "3.11" + pytorch: 2.3.1 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" steps: - name: Checkout uses: actions/checkout@v3 From a4a5bf057ff3800e32cc0ca5eecd40198f1266a3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 13 Jul 2024 09:53:13 -0400 Subject: [PATCH 0064/1405] fixes to prevent vram spike when train starts (#1742) --- src/axolotl/utils/models.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index d479d425d3..d8eac1ce18 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -599,9 +599,12 @@ def load_model( and not cfg.trust_remote_code and not cfg.gptq ): - from transformers import LlamaForCausalLM + if qlora_fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: + skip_move_to_device = True + if "device_map" in model_kwargs: + del model_kwargs["device_map"] - model = LlamaForCausalLM.from_pretrained( + model = AutoModelForCausalLM.from_pretrained( base_model, config=model_config, **model_kwargs, @@ -634,7 +637,11 @@ def load_model( base_model, **model_kwargs, ) - elif model_type and not cfg.trust_remote_code: + elif ( + model_type + and model_type != "AutoModelForCausalLM" + and not cfg.trust_remote_code + ): if cfg.gptq: model = AutoModelForCausalLM.from_pretrained( base_model, @@ -675,6 +682,7 @@ def load_model( ) else: if qlora_fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: + # disabling either of these two still leads to VRAM spike before setting back down skip_move_to_device = True if "device_map" in model_kwargs: del model_kwargs["device_map"] From 1e57b4c5623d7d4131206b45ea58e06ba843c887 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 13 Jul 2024 13:28:17 -0400 Subject: [PATCH 0065/1405] update to pytorch 2.3.1 (#1746) [skip ci] --- .github/workflows/main.yml | 10 +++++----- .github/workflows/nightlies.yml | 8 ++++---- .github/workflows/tests.yml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8bced628d2..4969de75d2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,6 @@ jobs: pytorch: 2.1.2 axolotl_extras: axolotl_args: "--extra-index-url https://download.pytorch.org/whl/cu118" - is_latest: true - cuda: 121 cuda_version: 12.1.0 python_version: "3.10" @@ -33,8 +32,9 @@ jobs: - cuda: 121 cuda_version: 12.1.0 python_version: "3.11" - pytorch: 2.3.0 + pytorch: 2.3.1 axolotl_extras: + is_latest: true runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -80,7 +80,6 @@ jobs: python_version: "3.10" pytorch: 2.1.2 axolotl_extras: - is_latest: true - cuda: 121 cuda_version: 12.1.0 python_version: "3.10" @@ -94,8 +93,9 @@ jobs: - cuda: 121 cuda_version: 12.1.0 python_version: "3.11" - pytorch: 2.3.0 + pytorch: 2.3.1 axolotl_extras: + is_latest: true runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -136,7 +136,7 @@ jobs: - cuda: 121 cuda_version: 12.1.0 python_version: "3.11" - pytorch: 2.3.0 + pytorch: 2.3.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 6dc22b6bf0..770954b85d 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -18,7 +18,6 @@ jobs: pytorch: 2.1.2 axolotl_extras: axolotl_args: "--extra-index-url https://download.pytorch.org/whl/cu118" - is_latest: true - cuda: 121 cuda_version: 12.1.0 python_version: "3.10" @@ -32,8 +31,9 @@ jobs: - cuda: 121 cuda_version: 12.1.0 python_version: "3.11" - pytorch: 2.3.0 + pytorch: 2.3.1 axolotl_extras: + is_latest: true runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -80,7 +80,6 @@ jobs: python_version: "3.10" pytorch: 2.1.2 axolotl_extras: - is_latest: true - cuda: 121 cuda_version: 12.1.0 python_version: "3.10" @@ -94,8 +93,9 @@ jobs: - cuda: 121 cuda_version: 12.1.0 python_version: "3.11" - pytorch: 2.3.0 + pytorch: 2.3.1 axolotl_extras: + is_latest: true runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2e2d0968d0..610e1b43e1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -87,7 +87,7 @@ jobs: - cuda: 121 cuda_version: 12.1.0 python_version: "3.11" - pytorch: 2.3.0 + pytorch: 2.3.1 num_gpus: 1 steps: - name: Checkout From 4512738a73c68e2f148cf079d0c6a934ef16b3f9 Mon Sep 17 00:00:00 2001 From: Akshaya Shanbhogue Date: Sat, 13 Jul 2024 11:04:31 -0700 Subject: [PATCH 0066/1405] bump xformers to 0.0.27 (#1740) * Update requirements.txt Preserve compatibility with torch 2.3.1. [Reference](https://github.com/facebookresearch/xformers/issues/1052) * fix setup.py to extract the current xformers dep from requirements for replacement * xformers 0.0.27 wheels not built for torch 2.3.0 --------- Co-authored-by: Wing Lian --- requirements.txt | 2 +- setup.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index c8d1687349..e24845a440 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ flash-attn==2.5.8 sentencepiece wandb einops -xformers==0.0.26.post1 +xformers==0.0.27 optimum==1.16.2 hf_transfer colorama diff --git a/setup.py b/setup.py index c7b4e15dec..58d2794755 100644 --- a/setup.py +++ b/setup.py @@ -29,9 +29,10 @@ def parse_requirements(): _install_requires.append(line) try: + xformers_version = [req for req in _install_requires if "xformers" in req][0] if "Darwin" in platform.system(): # don't install xformers on MacOS - _install_requires.pop(_install_requires.index("xformers==0.0.26.post1")) + _install_requires.pop(_install_requires.index(xformers_version)) else: # detect the version of torch already installed # and set it so dependencies don't clobber the torch version @@ -49,12 +50,14 @@ def parse_requirements(): raise ValueError("Invalid version format") if (major, minor) >= (2, 3): - pass + if patch == 0: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.26.post1") elif (major, minor) >= (2, 2): - _install_requires.pop(_install_requires.index("xformers==0.0.26.post1")) + _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.25.post1") else: - _install_requires.pop(_install_requires.index("xformers==0.0.26.post1")) + _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.23.post1") except PackageNotFoundError: From 634f384e068b8fb5e04cf76a4e1b5c419771214e Mon Sep 17 00:00:00 2001 From: David Meikle Date: Sat, 13 Jul 2024 19:34:28 +0100 Subject: [PATCH 0067/1405] Changed URL for dataset docs (#1744) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b35702b3a..0a4e2e5a1a 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ For further and fine-grained use cases, please refer to the official [dstack doc Axolotl supports a variety of dataset formats. It is recommended to use a JSONL. The schema of the JSONL depends upon the task and the prompt template you wish to use. Instead of a JSONL, you can also use a HuggingFace dataset with columns for each JSONL field. -See [these docs](https://openaccess-ai-collective.github.io/axolotl/docs/dataset-formats/) for more information on how to use different dataset formats. +See [these docs](https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/) for more information on how to use different dataset formats. ### Config From 219cd0d3c5599424ff706839acd986a31de5e94d Mon Sep 17 00:00:00 2001 From: RodriMora Date: Sat, 13 Jul 2024 20:34:44 +0200 Subject: [PATCH 0068/1405] Fix eval_sample_packing in llama-3 lora example (#1716) [skip ci] * Fix eval_sample_packing in llama-3 lora example * Update examples/llama-3/lora-8b.yml Co-authored-by: Wing Lian --------- Co-authored-by: Wing Lian --- examples/llama-3/lora-8b.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/llama-3/lora-8b.yml b/examples/llama-3/lora-8b.yml index dfc881faf9..76418ca3b3 100644 --- a/examples/llama-3/lora-8b.yml +++ b/examples/llama-3/lora-8b.yml @@ -15,6 +15,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true +eval_sample_packing: false pad_to_sequence_len: true adapter: lora From 98af5388ba8fd2adde847f8b868a5fb2dcf9367d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 14 Jul 2024 19:11:31 -0400 Subject: [PATCH 0069/1405] bump flash attention 2.5.8 -> 2.6.1 (#1738) * bump flash attention 2.5.8 -> 2.6.1 * use triton implementation of cross entropy from flash attn * add smoke test for flash attn cross entropy patch * fix args to xentropy.apply * handle tuple from triton loss fn * ensure the patch tests run independently * use the wrapper already built into flash attn for cross entropy * mark pytest as forked for patches * use pytest xdist instead of forked, since cuda doesn't like forking * limit to 1 process and use dist loadfile for pytest * change up pytest for fixture to reload transformers w monkeypathc --- cicd/Dockerfile.jinja | 2 +- requirements-tests.txt | 1 + requirements.txt | 2 +- setup.py | 4 +- src/axolotl/integrations/__init__.py | 0 .../monkeypatch/llama_attn_hijack_flash.py | 15 ++-- src/axolotl/utils/models.py | 6 ++ tests/e2e/patched/test_fa_xentropy.py | 87 +++++++++++++++++++ 8 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 src/axolotl/integrations/__init__.py create mode 100644 tests/e2e/patched/test_fa_xentropy.py diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 96c312ddcd..287d563c17 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -30,7 +30,7 @@ RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ fi # So we can test the Docker image -RUN pip install pytest +RUN pip install -r requirements-tests.txt # fix so that git fetch/pull from remote works RUN git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && \ diff --git a/requirements-tests.txt b/requirements-tests.txt index e079f8a603..9cda381d0c 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1 +1,2 @@ pytest +pytest-xdist diff --git a/requirements.txt b/requirements.txt index e24845a440..abefa3be31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ fire PyYAML>=6.0 requests datasets==2.19.1 -flash-attn==2.5.8 +flash-attn==2.6.1 sentencepiece wandb einops diff --git a/setup.py b/setup.py index 58d2794755..82e652241d 100644 --- a/setup.py +++ b/setup.py @@ -80,10 +80,10 @@ def parse_requirements(): dependency_links=dependency_links, extras_require={ "flash-attn": [ - "flash-attn==2.5.8", + "flash-attn==2.6.1", ], "fused-dense-lib": [ - "fused-dense-lib @ git+https://github.com/Dao-AILab/flash-attention@v2.5.8#subdirectory=csrc/fused_dense_lib", + "fused-dense-lib @ git+https://github.com/Dao-AILab/flash-attention@v2.6.1#subdirectory=csrc/fused_dense_lib", ], "deepspeed": [ "deepspeed @ git+https://github.com/microsoft/DeepSpeed.git@bc48371c5e1fb8fd70fc79285e66201dbb65679b", diff --git a/src/axolotl/integrations/__init__.py b/src/axolotl/integrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py index 6d7a23f0db..9377cb03fa 100644 --- a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py @@ -104,17 +104,12 @@ def replace_llama_attn_with_flash_attn( # skip only if explicitly disabled if cross_entropy: - try: - from flash_attn.losses.cross_entropy import CrossEntropyLoss + from flash_attn.losses.cross_entropy import CrossEntropyLoss - LOG.info("patching with flash_attn.losses.cross_entropy") - transformers.models.llama.modeling_llama.CrossEntropyLoss = partial( - CrossEntropyLoss, inplace_backward=True - ) - except ImportError: - LOG.warning( - "optimized flash-attention CrossEntropyLoss not found (run `pip install 'git+https://github.com/Dao-AILab/flash-attention.git#egg=xentropy_cuda_lib&subdirectory=csrc/xentropy'`)" - ) + LOG.info("patching with flash_attn.losses.cross_entropy") + transformers.models.llama.modeling_llama.CrossEntropyLoss = partial( + CrossEntropyLoss, inplace_backward=True + ) # skip only if explicitly disabled if rms_norm: diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index d8eac1ce18..19745ef8ba 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -371,6 +371,12 @@ def load_model( rms_norm=cfg.flash_attn_rms_norm, use_shifted_sparse_attn=True, ) + elif cfg.flash_attn_cross_entropy or cfg.flash_attn_rms_norm: + replace_llama_attn_with_flash_attn( + packed=False, + cross_entropy=cfg.flash_attn_cross_entropy, + rms_norm=cfg.flash_attn_rms_norm, + ) elif cfg.xformers_attention: from axolotl.monkeypatch.llama_attn_hijack_xformers import ( hijack_llama_attention, diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py new file mode 100644 index 0000000000..0991bdd742 --- /dev/null +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -0,0 +1,87 @@ +""" +E2E tests for lora llama +""" + +import logging +import os +import unittest +from importlib import reload +from pathlib import Path + +import pytest +from transformers.utils import is_torch_bf16_gpu_available + +from axolotl.cli import load_datasets +from axolotl.common.cli import TrainerCliArgs +from axolotl.train import train +from axolotl.utils.config import normalize_config +from axolotl.utils.dict import DictDefault + +from ..utils import with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +@pytest.fixture(autouse=True) +def reload_transformers(): + import transformers.models.llama.modeling_llama + + yield + reload(transformers.models.llama.modeling_llama) + + +class TestFAXentropyLlama(unittest.TestCase): + """ + Test case for Llama models using LoRA w multipack + """ + + @with_temp_dir + def test_lora_packing_fa_cross_entropy(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 1024, + "sample_packing": True, + "flash_attention": True, + "flash_attn_cross_entropy": True, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 32, + "lora_alpha": 64, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.2, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + } + ) + if is_torch_bf16_gpu_available(): + cfg.bf16 = True + else: + cfg.fp16 = True + + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.bin").exists() From 78e12f8ca5f2bb8882a17335e230d585b9698890 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 14 Jul 2024 19:12:57 -0400 Subject: [PATCH 0070/1405] add basic support for the optimi adamw optimizer (#1727) * add support for optimi_adamw optimizer w kahan summation * pydantic validator for optimi_adamw * workaround for setting optimizer for fsdp * make sure to install optimizer packages * make sure to have parity for model parameters passed to optimizer * add smoke test for optimi_adamw optimizer * don't use foreach optimi by default --- cicd/Dockerfile.jinja | 4 +- docker/Dockerfile | 4 +- setup.py | 6 ++ src/axolotl/core/trainer_builder.py | 65 +++++++++++++++--- .../config/models/input/v0_4_1/__init__.py | 2 +- tests/e2e/test_lora_llama.py | 6 +- tests/e2e/test_optimizers.py | 67 +++++++++++++++++++ 7 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 tests/e2e/test_optimizers.py diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 287d563c17..263f4a6611 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -24,9 +24,9 @@ RUN git fetch origin +$GITHUB_REF && \ # If AXOLOTL_EXTRAS is set, append it in brackets RUN pip install causal_conv1d RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install -e .[deepspeed,flash-attn,mamba-ssm,galore,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + pip install -e .[deepspeed,flash-attn,mamba-ssm,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - pip install -e .[deepspeed,flash-attn,mamba-ssm,galore] $AXOLOTL_ARGS; \ + pip install -e .[deepspeed,flash-attn,mamba-ssm,optimizers] $AXOLOTL_ARGS; \ fi # So we can test the Docker image diff --git a/docker/Dockerfile b/docker/Dockerfile index cdb6d177a5..be58d03543 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,9 +22,9 @@ WORKDIR /workspace/axolotl # If AXOLOTL_EXTRAS is set, append it in brackets RUN pip install causal_conv1d RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install -e .[deepspeed,flash-attn,mamba-ssm,galore,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + pip install -e .[deepspeed,flash-attn,mamba-ssm,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - pip install -e .[deepspeed,flash-attn,mamba-ssm,galore] $AXOLOTL_ARGS; \ + pip install -e .[deepspeed,flash-attn,mamba-ssm,optimizers] $AXOLOTL_ARGS; \ fi # So we can test the Docker image diff --git a/setup.py b/setup.py index 82e652241d..9e6f34ad81 100644 --- a/setup.py +++ b/setup.py @@ -104,5 +104,11 @@ def parse_requirements(): "galore": [ "galore_torch", ], + "optimizers": [ + "galore_torch", + "lion-pytorch==0.1.2", + "lomo-optim==0.1.1", + "torch-optimi==0.2.1", + ], }, ) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index ec175454e9..5391904fcf 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -226,6 +226,12 @@ class AxolotlTrainingMixins: default=None, metadata={"help": "whether to use sequential sampling for curriculum learning"}, ) + alternate_optimizer: Optional[str] = field( + default=None, + metadata={ + "help": "workaround to pass an alternate optimizer to the HF trainer" + }, + ) @dataclass @@ -285,25 +291,59 @@ def __init__( self.loss_fct = torch.nn.CrossEntropyLoss(reduction="none") def create_optimizer(self): - if self.args.loraplus_lr_ratio is None: + if ( + self.args.loraplus_lr_ratio is None + and self.args.alternate_optimizer != "optimi_adamw" + ): return super().create_optimizer() opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model if self.optimizer is None: # pylint: disable=access-member-before-definition + decay_parameters = self.get_decay_parameter_names(opt_model) + optimizer_grouped_parameters = [ + { + "params": [ + p + for n, p in opt_model.named_parameters() + if (n in decay_parameters and p.requires_grad) + ], + "weight_decay": self.args.weight_decay, + }, + { + "params": [ + p + for n, p in opt_model.named_parameters() + if (n not in decay_parameters and p.requires_grad) + ], + "weight_decay": 0.0, + }, + ] + optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs( self.args, opt_model, ) - loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) - loraplus_lr_embedding = getattr(self.args, "loraplus_lr_embedding", None) - self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init - opt_model, - optimizer_cls, - optimizer_kwargs, - loraplus_lr_ratio, - loraplus_lr_embedding, - ) + if self.args.loraplus_lr_ratio is not None: + loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) + loraplus_lr_embedding = getattr( + self.args, "loraplus_lr_embedding", None + ) + self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init + opt_model, + optimizer_cls, + optimizer_kwargs, + loraplus_lr_ratio, + loraplus_lr_embedding, + ) + elif self.args.alternate_optimizer == "optimi_adamw": + from optimi import AdamW + + self.optimizer = ( # pylint: disable=attribute-defined-outside-init + AdamW( + optimizer_grouped_parameters, foreach=False, **optimizer_kwargs + ) + ) if is_sagemaker_mp_enabled(): self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init @@ -1396,6 +1436,11 @@ def build(self, total_num_steps): trainer_kwargs = {} + if self.cfg.optimizer == "optimi_adamw": + # Set default so transformers doesn't throw + training_arguments_kwargs["optim"] = "adamw_hf" + training_arguments_kwargs["alternate_optimizer"] = self.cfg.optimizer + if self.cfg.optimizer == "lion_pytorch": from lion_pytorch import Lion diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 3cac4f8391..3d0b02752a 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -341,7 +341,7 @@ class HyperparametersConfig(BaseModel): learning_rate: Union[str, float] weight_decay: Optional[float] = 0.0 optimizer: Optional[ - Union[OptimizerNames, Literal["lion_pytorch"]] + Union[OptimizerNames, Literal["lion_pytorch", "optimi_adamw"]] ] = OptimizerNames.ADAMW_HF.value optim_args: Optional[Union[str, Dict[str, Any]]] = Field( default=None, metadata={"help": "Optional arguments to supply to optimizer."} diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index c79652bef7..4c6fdaaa91 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -34,8 +34,8 @@ def test_lora(self, temp_dir): "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", - "lora_r": 32, - "lora_alpha": 64, + "lora_r": 8, + "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, "val_set_size": 0.1, @@ -50,7 +50,7 @@ def test_lora(self, temp_dir): "type": "alpaca", }, ], - "num_epochs": 2, + "num_epochs": 1, "micro_batch_size": 8, "gradient_accumulation_steps": 1, "output_dir": temp_dir, diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py new file mode 100644 index 0000000000..119dd3d7cf --- /dev/null +++ b/tests/e2e/test_optimizers.py @@ -0,0 +1,67 @@ +""" +E2E tests for custom optimizers using Llama +""" + +import logging +import os +import unittest +from pathlib import Path + +from axolotl.cli import load_datasets +from axolotl.common.cli import TrainerCliArgs +from axolotl.train import train +from axolotl.utils.config import normalize_config +from axolotl.utils.dict import DictDefault + +from .utils import with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestCustomOptimizers(unittest.TestCase): + """ + Test case for Llama models using LoRA + """ + + @with_temp_dir + def test_optimi_adamw(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 1024, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.1, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "optimi_adamw", + "lr_scheduler": "cosine", + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.bin").exists() From e1725aef2b1ec1c71fa0c71c99279766513ee503 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 16 Jul 2024 14:45:38 -0400 Subject: [PATCH 0071/1405] update modal package and don't cache pip install (#1757) * update modal package and cleanup pip cache * more verbosity on the test --- .github/workflows/tests.yml | 6 +++++- cicd/cicd.sh | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 610e1b43e1..1cee8cbcb2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -57,6 +57,10 @@ jobs: run: | pytest --ignore=tests/e2e/ tests/ + - name: cleanup pip cache + run: | + find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + docker-e2e-tests: if: github.repository_owner == 'axolotl-ai-cloud' # this job needs to be run on self-hosted GPU runners... @@ -99,7 +103,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal jinja2 + pip install modal==0.63.64 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV diff --git a/cicd/cicd.sh b/cicd/cicd.sh index bc36458abd..180150ea25 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -2,5 +2,5 @@ set -e pytest --ignore=tests/e2e/ /workspace/axolotl/tests/ -pytest /workspace/axolotl/tests/e2e/patched/ +pytest -n1 --dist loadfile -v /workspace/axolotl/tests/e2e/patched/ pytest --ignore=tests/e2e/patched/ /workspace/axolotl/tests/e2e/ From cfc533a7f71a85a9571ac15860c4b2106134bb34 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 16 Jul 2024 16:00:23 -0400 Subject: [PATCH 0072/1405] torch compile and cuda alloc improvements (#1755) * enable experimental expandable_segments * hf trainer seems to be missing torch compile * disable PYTORCH_CUDA_ALLOC_CONF to see if that fixes cicd --- src/axolotl/core/trainer_builder.py | 12 ++++++++++++ src/axolotl/train.py | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 5391904fcf..662b648969 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -290,6 +290,18 @@ def __init__( if self.args.orpo_alpha: self.loss_fct = torch.nn.CrossEntropyLoss(reduction="none") + def _wrap_model(self, model, training=True, dataloader=None): + if self.args.torch_compile: + torch._dynamo.config.accumulated_cache_size_limit = ( # pylint: disable=protected-access + 256 + ) + model = torch.compile( + model, + backend=self.args.torch_compile_backend, + mode=self.args.torch_compile_mode, + ) + return super()._wrap_model(model, training=training, dataloader=dataloader) + def create_optimizer(self): if ( self.args.loraplus_lr_ratio is None diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 99a9b0ba97..5de1bc1144 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -52,6 +52,13 @@ class TrainDatasetMeta: def train( *, cfg: DictDefault, cli_args: TrainerCliArgs, dataset_meta: TrainDatasetMeta ) -> Tuple[Union[PeftModel, PreTrainedModel], PreTrainedTokenizer]: + # enable expandable segments for cuda allocation to improve VRAM usage + # torch_version = torch.__version__.split(".") + # torch_major, torch_minor = int(torch_version[0]), int(torch_version[1]) + # if torch_major == 2 and torch_minor >= 2: + # if os.getenv("PYTORCH_CUDA_ALLOC_CONF") is None: + # os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True" + # load the tokenizer first LOG.debug( f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}", From 5f58555bd0dbf15cae25fc021eb00421e53e47b2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 16 Jul 2024 17:36:29 -0400 Subject: [PATCH 0073/1405] support for llama multipack using updated code/patches (#1754) * support for llama multipack using updated code/patches * also support unsloth patches * incorrect arg * add config validation for unsloth * add missing return to validation * add another missing return to validation --- .../monkeypatch/llama_attn_hijack_flash.py | 50 +++++++++++-------- src/axolotl/monkeypatch/multipack.py | 5 ++ .../config/models/input/v0_4_1/__init__.py | 40 +++++++++++++++ src/axolotl/utils/models.py | 21 ++++++++ 4 files changed, 95 insertions(+), 21 deletions(-) diff --git a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py index 9377cb03fa..4c3571ea4f 100644 --- a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py @@ -78,6 +78,33 @@ def replace_llama_qkv_with_fused(model): set_module_name(model, name, qkv) +def patch_llama_cross_entropy(): + from flash_attn.losses.cross_entropy import CrossEntropyLoss + + LOG.info("patching with flash_attn.losses.cross_entropy") + transformers.models.llama.modeling_llama.CrossEntropyLoss = partial( + CrossEntropyLoss, inplace_backward=True + ) + + +def patch_llama_rms_norm(): + try: + from flash_attn.ops.rms_norm import RMSNorm + + class LlamaRMSNorm(RMSNorm): + """Patched LLamaRMSNorm""" + + def __init__(self, hidden_size, eps=1e-6): + super().__init__(hidden_size, eps=eps) + + LOG.info("patching with flash_attn.ops.rms_norm") + transformers.models.llama.modeling_llama.LlamaRMSNorm = LlamaRMSNorm + except ImportError: + LOG.warning( + "optimized flash-attention RMSNorm not found (run `pip install 'git+https://github.com/Dao-AILab/flash-attention.git#egg=dropout_layer_norm&subdirectory=csrc/layer_norm'`)" + ) + + def replace_llama_attn_with_flash_attn( packed: Optional[bool] = False, cross_entropy: Optional[bool] = False, @@ -104,30 +131,11 @@ def replace_llama_attn_with_flash_attn( # skip only if explicitly disabled if cross_entropy: - from flash_attn.losses.cross_entropy import CrossEntropyLoss - - LOG.info("patching with flash_attn.losses.cross_entropy") - transformers.models.llama.modeling_llama.CrossEntropyLoss = partial( - CrossEntropyLoss, inplace_backward=True - ) + patch_llama_cross_entropy() # skip only if explicitly disabled if rms_norm: - try: - from flash_attn.ops.rms_norm import RMSNorm - - class LlamaRMSNorm(RMSNorm): - """Patched LLamaRMSNorm""" - - def __init__(self, hidden_size, eps=1e-6): - super().__init__(hidden_size, eps=eps) - - LOG.info("patching with flash_attn.ops.rms_norm") - transformers.models.llama.modeling_llama.LlamaRMSNorm = LlamaRMSNorm - except ImportError: - LOG.warning( - "optimized flash-attention RMSNorm not found (run `pip install 'git+https://github.com/Dao-AILab/flash-attention.git#egg=dropout_layer_norm&subdirectory=csrc/layer_norm'`)" - ) + patch_llama_rms_norm() class FusedAttention(LlamaAttention): diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index e319596d02..017adb2bfd 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -10,6 +10,7 @@ from axolotl.monkeypatch.utils import get_unpad_data SUPPORTED_MULTIPACK_MODEL_TYPES = [ + "llama", "mixtral", "qwen2", "qwen2_moe", @@ -30,6 +31,10 @@ def patch_for_multipack(model_type, model_name=None): ) if is_deepspeed_zero3_enabled(): patch_mixtral_moe_forward_zero3() + elif model_type == "llama": + transformers.models.llama.modeling_llama._get_unpad_data = ( # pylint: disable=protected-access + get_unpad_data + ) elif model_type == "qwen2": transformers.models.qwen2.modeling_qwen2._get_unpad_data = ( # pylint: disable=protected-access get_unpad_data diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 3d0b02752a..6cd98af11c 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -1112,6 +1112,31 @@ def check_dataset_or_pretraining_dataset(cls, data): raise ValueError("either datasets or pretraining_dataset is required") return data + @model_validator(mode="before") + @classmethod + def check_xentropy_patch_conflicts(cls, data): + if data.get("flash_attn_cross_entropy") and data.get( + "unsloth_cross_entropy_loss" + ): + raise ValueError( + "flash_attn_cross_entropy and unsloth_cross_entropy_loss cannot be both enabled" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_qlora_unsloth(cls, data): + if ( + data.get("unsloth_lora_mlp") + or data.get("unsloth_lora_qkv") + or data.get("unsloth_lora_o") + ): + if data.get("adapter") == "lora" or data.get("load_in_8bit"): + raise ValueError( + "unsloth_lora_mlp, unsloth_lora_qkv, and unsloth_lora_o are not compatible with 8-bit LoRA" + ) + return data + class AxolotlConfigWCapabilities(AxolotlInputConfig): """wrapper to valdiate gpu capabilities with the configured options""" @@ -1163,3 +1188,18 @@ def check_fsdp_deepspeed(cls, data): if data.get("deepspeed") and data.get("fsdp"): raise ValueError("deepspeed and fsdp cannot be used together.") return data + + @model_validator(mode="before") + @classmethod + def check_multigpu_unsloth(cls, data): + if ( + data.get("unsloth_lora_mlp") + or data.get("unsloth_lora_qkv") + or data.get("unsloth_lora_o") + ): + capabilities = data.get("capabilities") + if capabilities and capabilities.get("num_gpus") > 1: + raise ValueError( + "unsloth_lora_mlp, unsloth_lora_qkv, and unsloth_lora_o are not compatible with multi-GPU training." + ) + return data diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 19745ef8ba..51ce5a29bb 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -347,6 +347,27 @@ def load_model( and cfg.sample_packing ): patch_for_multipack(cfg.model_config_type, model_name=cfg.base_model) + + if cfg.is_llama_derived_model: + from axolotl.monkeypatch.llama_attn_hijack_flash import ( + patch_llama_cross_entropy, + patch_llama_rms_norm, + ) + + if cfg.flash_attn_cross_entropy: + patch_llama_cross_entropy() + if cfg.flash_attn_rms_norm: + patch_llama_rms_norm() + if cfg.unsloth_cross_entropy_loss: + from axolotl.monkeypatch.unsloth_ import ( + integrate_cross_entropy_loss_patch, + ) + + integrate_cross_entropy_loss_patch() + if cfg.unsloth_lora_qkv or cfg.unsloth_lora_o: + from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora + + patch_self_attn_lora() elif cfg.is_llama_derived_model: # Modify all llama derived models in one block From 152ab76623853a603f71adb09292dcaf1dfd8dba Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 17 Jul 2024 10:58:14 -0400 Subject: [PATCH 0074/1405] fix num gpu check (#1760) --- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 6cd98af11c..708d41972a 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -1198,7 +1198,7 @@ def check_multigpu_unsloth(cls, data): or data.get("unsloth_lora_o") ): capabilities = data.get("capabilities") - if capabilities and capabilities.get("num_gpus") > 1: + if capabilities and capabilities.get("n_gpu", 0) > 1: raise ValueError( "unsloth_lora_mlp, unsloth_lora_qkv, and unsloth_lora_o are not compatible with multi-GPU training." ) From 976f85195a96bc90c8f0443c8b374f794bd21028 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 17 Jul 2024 10:58:38 -0400 Subject: [PATCH 0075/1405] fixes to accelerator so that iterable pretraining datasets work (#1759) * fixes to accelerator so that iterable pretraining datasets work * fix the pretraining test params * split batches, not dispatch batches needs to be set * update c4 datasets * set epochs in pretrain config test * need to set both split_batches and dispatch_batches to false for pretraining * fix bool val in comment --- src/axolotl/core/trainer_builder.py | 5 ++ .../config/models/input/v0_4_1/__init__.py | 24 +++++++ tests/e2e/test_llama_pretrain.py | 67 +++++++++++++++++++ tests/test_packed_pretraining.py | 4 +- 4 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/test_llama_pretrain.py diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 662b648969..0358ad4e64 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1481,6 +1481,11 @@ def build(self, total_num_steps): sys.path.append(self.cfg.torchdistx_path) importlib.import_module("torchdistx") + if self.cfg.accelerator_config: + training_arguments_kwargs[ + "accelerator_config" + ] = self.cfg.accelerator_config + training_args = ( AxolotlTrainingArguments( # pylint: disable=unexpected-keyword-arg **training_arguments_kwargs, diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 708d41972a..32bb1f5b64 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -77,6 +77,7 @@ class PretrainingDataset(BaseModel): split: Optional[str] = "train" text_column: Optional[str] = "text" type: Optional[str] = "pretrain" + trust_remote_code: Optional[bool] = False class UserDefinedPrompterType(BaseModel): @@ -118,6 +119,8 @@ class SFTDataset(BaseModel): roles: Optional[Dict[str, List[str]]] = None drop_system_message: Optional[bool] = None + trust_remote_code: Optional[bool] = False + class UserDefinedDPOType(BaseModel): """User defined typing for DPO""" @@ -158,6 +161,7 @@ class KTODataset(BaseModel): split: Optional[str] = None type: Optional[Union[UserDefinedKTOType, str]] = None data_files: Optional[List[str]] = None + trust_remote_code: Optional[bool] = False class RLType(str, Enum): @@ -504,6 +508,8 @@ class Config: dataloader_prefetch_factor: Optional[int] = None dataloader_drop_last: Optional[bool] = None + accelerator_config: Optional[Dict[str, Any]] = None + remove_unused_columns: Optional[bool] = None push_dataset_to_hub: Optional[str] = None @@ -702,6 +708,24 @@ def check_pretraining_w_group_by_length(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_pretraining_split_batches_accelerate(cls, data): + # alternatively set ACCELERATE_SPLIT_BATCHES=False + if data.get("pretraining_dataset"): + accelerator_config = data.get("accelerator_config", {}) + if not accelerator_config: + data["accelerator_config"] = { + "split_batches": False, + "dispatch_batches": False, + } + else: + if accelerator_config.get("split_batches") is None: + data["accelerator_config"]["split_batches"] = False + if accelerator_config.get("dispatch_batches") is None: + data["accelerator_config"]["dispatch_batches"] = False + return data + @model_validator(mode="before") @classmethod def check_gptq_w_revision(cls, data): diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py new file mode 100644 index 0000000000..62fb63c471 --- /dev/null +++ b/tests/e2e/test_llama_pretrain.py @@ -0,0 +1,67 @@ +""" +E2E tests for llama pretrain +""" + +import logging +import os +import unittest +from pathlib import Path + +from axolotl.cli import load_datasets +from axolotl.common.cli import TrainerCliArgs +from axolotl.train import train +from axolotl.utils.config import normalize_config +from axolotl.utils.dict import DictDefault + +from .utils import with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestPretrainLlama(unittest.TestCase): + """ + Test case for Llama models w pretraining + """ + + @with_temp_dir + def test_pretrain_w_sample_packing(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "flash_attention": True, + "sequence_len": 1024, + "sample_packing": True, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "pretraining_dataset": [ + { + "path": "allenai/c4", + "name": "en", + "type": "pretrain", + } + ], + "max_steps": 5, + "num_epochs": 1, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() diff --git a/tests/test_packed_pretraining.py b/tests/test_packed_pretraining.py index fb623a43dc..5d517585fd 100644 --- a/tests/test_packed_pretraining.py +++ b/tests/test_packed_pretraining.py @@ -24,7 +24,7 @@ def setUp(self) -> None: def test_packing_stream_dataset(self): # pylint: disable=duplicate-code dataset = load_dataset( - "c4", + "allenai/c4", "en", streaming=True, )["train"] @@ -33,7 +33,7 @@ def test_packing_stream_dataset(self): { "pretraining_dataset": [ { - "path": "c4", + "path": "allenai/c4", "name": "en", "type": "pretrain", } From 8619b2d8558f71a45ee15bf029fc58fc230adc82 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 17 Jul 2024 15:38:07 -0400 Subject: [PATCH 0076/1405] add torch_compile_mode options (#1763) [skip ci] * add torch_compile_mode options * make sure n_gpu is an int --- src/axolotl/cli/__init__.py | 2 +- src/axolotl/core/trainer_builder.py | 4 ++++ .../utils/config/models/input/v0_4_1/__init__.py | 12 ++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 7ec3f524ab..5966d59313 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -375,7 +375,7 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs): cfg, capabilities={ "bf16": is_torch_bf16_gpu_available(), - "n_gpu": os.environ.get("WORLD_SIZE", 1), + "n_gpu": int(os.environ.get("WORLD_SIZE", 1)), "compute_capability": gpu_version, }, ) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 0358ad4e64..b0eea55b1e 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1287,6 +1287,10 @@ def build(self, total_num_steps): training_arguments_kwargs[ "torch_compile_backend" ] = self.cfg.torch_compile_backend + if self.cfg.torch_compile_mode: + training_arguments_kwargs[ + "torch_compile_mode" + ] = self.cfg.torch_compile_mode # DDP Config if self.cfg.ddp_timeout: diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 32bb1f5b64..f0c6fa0eae 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -608,6 +608,9 @@ class Config: torch_compile: Optional[bool] = None torch_compile_backend: Optional[str] = None + torch_compile_mode: Optional[ + Literal["default", "reduce-overhead", "max-autotune"] + ] = None max_steps: Optional[int] = None warmup_steps: Optional[int] = None @@ -1161,6 +1164,15 @@ def check_qlora_unsloth(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_torch_compile_deepspeed(cls, data): + if data.get("deepspeed") and data.get("torch_compile"): + raise ValueError( + "torch_compile should be set within your deepspeed config file" + ) + return data + class AxolotlConfigWCapabilities(AxolotlInputConfig): """wrapper to valdiate gpu capabilities with the configured options""" From 8731b95d0408234624dbdf29f4b260ef506b71e8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 17 Jul 2024 15:38:26 -0400 Subject: [PATCH 0077/1405] re-enable PYTORCH_CUDA_ALLOC_CONF expandable_segments (#1765) [skip ci] --- src/axolotl/train.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 5de1bc1144..8e718af9bc 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -53,11 +53,11 @@ def train( *, cfg: DictDefault, cli_args: TrainerCliArgs, dataset_meta: TrainDatasetMeta ) -> Tuple[Union[PeftModel, PreTrainedModel], PreTrainedTokenizer]: # enable expandable segments for cuda allocation to improve VRAM usage - # torch_version = torch.__version__.split(".") - # torch_major, torch_minor = int(torch_version[0]), int(torch_version[1]) - # if torch_major == 2 and torch_minor >= 2: - # if os.getenv("PYTORCH_CUDA_ALLOC_CONF") is None: - # os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True" + torch_version = torch.__version__.split(".") + torch_major, torch_minor = int(torch_version[0]), int(torch_version[1]) + if torch_major == 2 and torch_minor >= 2: + if os.getenv("PYTORCH_CUDA_ALLOC_CONF") is None: + os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True" # load the tokenizer first LOG.debug( From c86c32a627957a08d145dee48a1a7ed2f20e4437 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 17 Jul 2024 15:38:37 -0400 Subject: [PATCH 0078/1405] set the number of dataset processes on the DPO Config rather than the trainer (#1762) --- src/axolotl/core/trainer_builder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index b0eea55b1e..3952cd5932 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1687,6 +1687,7 @@ def build_training_arguments(self, total_num_steps): # trl does some odd mapping of alpha to beta to reuse the beta parameter ??? training_args_kwargs["beta"] = self.cfg.orpo_alpha + training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes training_args_cls = AxolotlDPOConfig if self.cfg.rpo_alpha is not None: training_args_kwargs["rpo_alpha"] = self.cfg.rpo_alpha @@ -1754,8 +1755,6 @@ def build(self, total_num_steps): dpo_trainer_kwargs["max_target_length"] = None dpo_trainer_kwargs["max_prompt_length"] = self.cfg.sequence_len dpo_trainer_kwargs["generate_during_eval"] = True - if self.cfg.rl == "dpo": - dpo_trainer_kwargs["dataset_num_proc"] = self.cfg.dataset_processes elif self.cfg.rl == "orpo": trainer_cls = AxolotlORPOTrainer trainer_cls_args = [self.model] From 7830fe04b5ef226e0d0eff8a7285ce263ee3de47 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 18 Jul 2024 14:54:41 -0400 Subject: [PATCH 0079/1405] Unsloth rope (#1767) * Add unsloth rope embeddings support * support for models weights in 4bit and do some memory gc * use accelerate logger * add unsloth llama rms norm optims * update docs for unsloth * more docs info --- README.md | 1 + _quarto.yml | 1 + docs/unsloth.qmd | 49 +++++++++++++++ src/axolotl/monkeypatch/unsloth_.py | 63 ++++++++++++++++--- .../config/models/input/v0_4_1/__init__.py | 18 ++++++ src/axolotl/utils/models.py | 17 ++++- 6 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 docs/unsloth.qmd diff --git a/README.md b/README.md index 0a4e2e5a1a..fd293bd040 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Features: - [Multipack](./docs/multipack.qmd) - [RLHF & DPO](./docs/rlhf.qmd) - [Dataset Pre-Processing](./docs/dataset_preprocessing.qmd) + - [Unsloth](./docs/unsloth.qmd) - [Common Errors](#common-errors-) - [Tokenization Mismatch b/w Training & Inference](#tokenization-mismatch-bw-inference--training) - [Debugging Axolotl](#debugging-axolotl) diff --git a/_quarto.yml b/_quarto.yml index 009fa8056a..6b2eed971b 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -36,6 +36,7 @@ website: - docs/nccl.qmd - docs/mac.qmd - docs/multi-node.qmd + - docs/unsloth.qmd - section: "Dataset Formats" contents: docs/dataset-formats/* - section: "Reference" diff --git a/docs/unsloth.qmd b/docs/unsloth.qmd new file mode 100644 index 0000000000..390609fd33 --- /dev/null +++ b/docs/unsloth.qmd @@ -0,0 +1,49 @@ +--- +title: "Unsloth" +description: "Hyper-optimized QLoRA finetuning for single GPUs" +--- + +### Overview + +Unsloth provides hand-written optimized kernels for LLM finetuning that slightly improve speed and VRAM over +standard industry baselines. + + +### Installation + +The following will install unsloth from source and downgrade xformers as unsloth is incompatible with the most up +to date libraries. + +```bash +pip install --no-deps "unsloth @ git+https://github.com/unslothai/unsloth.git" +pip install --no-deps --force-reinstall xformers==0.0.26.post1 +``` + +### Using unsloth w Axolotl + +Axolotl exposes a few configuration options to try out unsloth and get most of the performance gains. + +Our unsloth integration is currently limited to the following model architectures: + - llama + +These options are specific to LoRA finetuning and cannot be used for multi-GPU finetuning +```yaml +unsloth_lora_mlp: true +unsloth_lora_qkv: true +unsloth_lora_o: true +``` + +These options are composable and can be used with multi-gpu finetuning +``` +unsloth_cross_entropy_loss: true +unsloth_rms_norm: true +unsloth_rope: true +``` + +### Limitations + +- Single GPU only; e.g. no multi-gpu support +- No deepspeed or FSDP support (requires multi-gpu) +- LoRA + QLoRA support only. No full fine tunes or fp8 support. +- Limited model architecture support. Llama, Phi, Gemma, Mistral only +- No MoE support. diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py index 6af3046e13..b1f0bddc00 100644 --- a/src/axolotl/monkeypatch/unsloth_.py +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -1,18 +1,20 @@ """module for patching with unsloth optimizations""" import inspect -import logging import re import types from typing import Tuple +import torch +from accelerate.logging import get_logger from peft import PeftModelForCausalLM +from torch import nn from transformers.models.llama.modeling_llama import ( LlamaFlashAttention2, LlamaForCausalLM, ) -LOG = logging.getLogger("axolotl.monkeypatch.unsloth") +LOG = get_logger("axolotl.monkeypatch.unsloth") ORIGINAL_CEL_CODE = """ if labels is not None: # Shift so that tokens < n predict n @@ -137,7 +139,7 @@ def integrate_cross_entropy_loss_patch(): globals(), ) exec(forward, globals()) # pylint: disable=exec-used # nosec B102 - print("patching unsloth fast_cross_entropy_loss") + LOG.info("patching unsloth fast_cross_entropy_loss", main_process_only=True) LlamaForCausalLM.forward = fast_cross_entropy_loss_forward # pylint: disable=undefined-variable # noqa: F821 @@ -179,12 +181,30 @@ def patch_self_attn_lora(): globals(), ) exec(self_attn_forward, globals()) # pylint: disable=exec-used # nosec B102 - print("patching unsloth attn lora") + LOG.info("patching unsloth attn lora", main_process_only=True) LlamaFlashAttention2.forward = ( unsloth_attn_forward # pylint: disable=undefined-variable # noqa: F821 ) +def integrate_rope_embeddings(): + import transformers.models.llama.modeling_llama + from unsloth.kernels.rope_embedding import fast_rope_embedding + + def apply_rotary_pos_emb( # pylint: disable=unused-argument + q, # pylint: disable=invalid-name + k, # pylint: disable=invalid-name + cos, + sin, + position_ids=None, + unsqueeze_dim=1, + ): + return fast_rope_embedding(q, k, cos, sin) + + LOG.info("patching unsloth RoPE embeddings", main_process_only=True) + transformers.models.llama.modeling_llama.apply_rotary_pos_emb = apply_rotary_pos_emb + + def integrate_lora_mlp_patch(peft_model: PeftModelForCausalLM): if peft_model.base_model.config.model_type in ["llama", "mistral"]: from unsloth.kernels import apply_lora_mlp_swiglu @@ -217,7 +237,7 @@ def integrate_lora_mlp_patch(peft_model: PeftModelForCausalLM): if is_mlp_lora and mlp_no_bias and mlp_not_dora: layer.mlp.forward = types.MethodType(apply_lora_mlp, layer.mlp) else: - logging.warning("unable to apply unsloth lora mlp patch to layer %d", idx) + LOG.warning("unable to apply unsloth lora mlp patch to layer %d", idx) def integrate_lora_patch(peft_model: PeftModelForCausalLM, cfg): @@ -243,9 +263,7 @@ def integrate_lora_patch(peft_model: PeftModelForCausalLM, cfg): layer.self_attn.apply_qkv = apply_lora_qkv else: layer.self_attn.apply_qkv = original_apply_qkv - logging.warning( - "unable to apply unsloth lora qkv patch to layer %d", idx - ) + LOG.warning("unable to apply unsloth lora qkv patch to layer %d", idx) if cfg.unsloth_lora_o: layer_modules = [ getattr(layer.self_attn, linear_proj) for linear_proj in ["o_proj"] @@ -264,6 +282,33 @@ def integrate_lora_patch(peft_model: PeftModelForCausalLM, cfg): layer.self_attn.apply_o = apply_lora_o else: layer.self_attn.apply_o = original_apply_o - logging.warning( + LOG.warning( "unable to apply unsloth lora o_proj patch to layer %d", idx ) + + +def patch_unsloth_layernorm(): + try: + import transformers.models.llama.modeling_llama + from unsloth.kernels.rms_layernorm import Fast_RMS_Layernorm + + class LlamaRMSNorm(nn.Module): + """LlamaRMSNorm""" + + def __init__(self, hidden_size, eps=1e-6): + """ + LlamaRMSNorm is equivalent to T5LayerNorm + """ + super().__init__() + self.weight = nn.Parameter(torch.ones(hidden_size)) + self.variance_epsilon = eps + + def forward(self, hidden_states): + return Fast_RMS_Layernorm.apply( + hidden_states, self.weight, self.variance_epsilon, False + ) + + LOG.info("patching with unsloth.kernels.rms_layernorm") + transformers.models.llama.modeling_llama.LlamaRMSNorm = LlamaRMSNorm + except ImportError: + LOG.warning("missing unsloth library") diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index f0c6fa0eae..f3acbbc68d 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -7,6 +7,7 @@ import logging import os from enum import Enum +from importlib.metadata import version from typing import Any, Dict, List, Literal, Optional, Tuple, Union from pydantic import BaseModel, Field, conlist, field_validator, model_validator @@ -596,6 +597,8 @@ class Config: unsloth_lora_mlp: Optional[bool] = None unsloth_lora_qkv: Optional[bool] = None unsloth_lora_o: Optional[bool] = None + unsloth_rms_norm: Optional[bool] = None + unsloth_rope: Optional[bool] = None deepspeed: Optional[Union[str, Dict[str, Any]]] = None fsdp: Optional[List[str]] = None @@ -1164,6 +1167,21 @@ def check_qlora_unsloth(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_unsloth_xformers_version(cls, data): + if ( + data.get("unsloth_lora_mlp") + or data.get("unsloth_lora_qkv") + or data.get("unsloth_lora_o") + ): + xformers_version = version("xformers") + if xformers_version == "0.0.27": + raise ValueError( + "xformers version 0.0.27 is not supported with unsloth. Please downgrade to 0.0.26.post1" + ) + return data + @model_validator(mode="before") @classmethod def check_torch_compile_deepspeed(cls, data): diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 51ce5a29bb..6185f0102f 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -1,7 +1,7 @@ """Module for models and model loading""" # pylint: disable=too-many-lines - +import gc import logging import math import os @@ -94,7 +94,7 @@ def check_model_config(cfg: DictDefault, model_config: Union[AutoConfig, DictDef "Please make sure to point to a GPTQ model." ) - if not cfg.gptq and quant_config_exists: + if not cfg.gptq and quant_config_exists and not cfg.load_in_4bit: raise ValueError( "model_config.quantization_config is set but `gptq` flag is not. " "Please use the `gptq` flag to train quantized model or point to a non-quantized model." @@ -358,6 +358,10 @@ def load_model( patch_llama_cross_entropy() if cfg.flash_attn_rms_norm: patch_llama_rms_norm() + elif cfg.unsloth_rms_norm: + from axolotl.monkeypatch.unsloth_ import patch_unsloth_layernorm + + patch_unsloth_layernorm() if cfg.unsloth_cross_entropy_loss: from axolotl.monkeypatch.unsloth_ import ( integrate_cross_entropy_loss_patch, @@ -884,6 +888,15 @@ def load_model( integrate_lora_patch(model, cfg) + if cfg.unsloth_rope: + from axolotl.monkeypatch.unsloth_ import integrate_rope_embeddings + + integrate_rope_embeddings() + + for _ in range(3): + gc.collect() + torch.cuda.empty_cache() + # TODO resume_from_checkpoint handling return model, lora_config From e4063d60a793d97630ee12dcd728c05fbb496d88 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 19 Jul 2024 00:47:07 -0400 Subject: [PATCH 0080/1405] bump transformers and set roundup_power2_divisions for more VRAM improvements, low bit ao optimizers (#1769) * bump transformers and set roundup_power2_divisions for more VRAM improvements * support for low bit optimizers from torch ao * fix check for alternate optimizers and use nous models on hf for llama3 * add missing check for ao_adamw_fp8 * fix check when using custom optimizers w adamw --- docs/torchao.qmd | 19 +++++++++++++ examples/llama-3/fft-8b.yaml | 2 +- examples/llama-3/instruct-lora-8b.yml | 2 +- examples/llama-3/lora-8b.yml | 2 +- examples/llama-3/qlora.yml | 2 +- requirements.txt | 2 +- src/axolotl/core/trainer_builder.py | 28 +++++++++++++++++-- src/axolotl/train.py | 4 ++- .../config/models/input/v0_4_1/__init__.py | 13 +++++++-- 9 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 docs/torchao.qmd diff --git a/docs/torchao.qmd b/docs/torchao.qmd new file mode 100644 index 0000000000..2dc9117fbf --- /dev/null +++ b/docs/torchao.qmd @@ -0,0 +1,19 @@ +--- +title: "PyTorch ao" +description: "Custom data types and layouts for training and inference" +--- + +### Installation + +Stable Release from the PyTorch index + +```bash +pip install torchao --extra-index-url https://download.pytorch.org/whl/cu121 # full options are cpu/cu118/cu121/cu124 +``` + + +Nightly release + +```bash +pip install --pre torchao-nightly --index-url https://download.pytorch.org/whl/nightly/cu121 # full options are cpu/cu118/cu121/cu124 +``` diff --git a/examples/llama-3/fft-8b.yaml b/examples/llama-3/fft-8b.yaml index a36fd740e4..908ef6e035 100644 --- a/examples/llama-3/fft-8b.yaml +++ b/examples/llama-3/fft-8b.yaml @@ -1,4 +1,4 @@ -base_model: meta-llama/Meta-Llama-3-8B +base_model: NousResearch/Meta-Llama-3-8B model_type: LlamaForCausalLM tokenizer_type: AutoTokenizer diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml index 754c9ad5c0..21d32604c5 100644 --- a/examples/llama-3/instruct-lora-8b.yml +++ b/examples/llama-3/instruct-lora-8b.yml @@ -1,4 +1,4 @@ -base_model: meta-llama/Meta-Llama-3-8B-Instruct +base_model: NousResearch/Meta-Llama-3-8B-Instruct model_type: LlamaForCausalLM tokenizer_type: AutoTokenizer diff --git a/examples/llama-3/lora-8b.yml b/examples/llama-3/lora-8b.yml index 76418ca3b3..a20a529f5f 100644 --- a/examples/llama-3/lora-8b.yml +++ b/examples/llama-3/lora-8b.yml @@ -1,4 +1,4 @@ -base_model: meta-llama/Meta-Llama-3-8B +base_model: NousResearch/Meta-Llama-3-8B model_type: LlamaForCausalLM tokenizer_type: AutoTokenizer diff --git a/examples/llama-3/qlora.yml b/examples/llama-3/qlora.yml index 44120d9385..079c9cad06 100644 --- a/examples/llama-3/qlora.yml +++ b/examples/llama-3/qlora.yml @@ -1,4 +1,4 @@ -base_model: meta-llama/Meta-Llama-3-8B +base_model: NousResearch/Meta-Llama-3-8B model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer diff --git a/requirements.txt b/requirements.txt index abefa3be31..39f6c8a77f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.11.1 -transformers==4.42.3 +transformers==4.42.4 tokenizers==0.19.1 bitsandbytes==0.43.1 accelerate==0.32.0 diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 3952cd5932..616b1d4eb6 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -305,7 +305,8 @@ def _wrap_model(self, model, training=True, dataloader=None): def create_optimizer(self): if ( self.args.loraplus_lr_ratio is None - and self.args.alternate_optimizer != "optimi_adamw" + and self.args.alternate_optimizer + not in ["optimi_adamw", "ao_adamw_8bit", "ao_adamw_4bit", "ao_adamw_fp8"] ): return super().create_optimizer() @@ -356,6 +357,24 @@ def create_optimizer(self): optimizer_grouped_parameters, foreach=False, **optimizer_kwargs ) ) + elif self.args.alternate_optimizer == "ao_adamw_4bit": + from torchao.prototype.low_bit_optim import AdamW4bit + + self.optimizer = ( # pylint: disable=attribute-defined-outside-init + AdamW4bit(optimizer_grouped_parameters, **optimizer_kwargs) + ) + elif self.args.alternate_optimizer == "ao_adamw_8bit": + from torchao.prototype.low_bit_optim import AdamW8bit + + self.optimizer = ( # pylint: disable=attribute-defined-outside-init + AdamW8bit(optimizer_grouped_parameters, **optimizer_kwargs) + ) + elif self.args.alternate_optimizer == "ao_adamw_fp8": + from torchao.prototype.low_bit_optim import AdamWFp8 + + self.optimizer = ( # pylint: disable=attribute-defined-outside-init + AdamWFp8(optimizer_grouped_parameters, **optimizer_kwargs) + ) if is_sagemaker_mp_enabled(): self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init @@ -1452,7 +1471,12 @@ def build(self, total_num_steps): trainer_kwargs = {} - if self.cfg.optimizer == "optimi_adamw": + if self.cfg.optimizer in [ + "optimi_adamw", + "ao_adamw_4bit", + "ao_adamw_8bit", + "ao_adamw_fp8", + ]: # Set default so transformers doesn't throw training_arguments_kwargs["optim"] = "adamw_hf" training_arguments_kwargs["alternate_optimizer"] = self.cfg.optimizer diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 8e718af9bc..e4d3ace192 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -57,7 +57,9 @@ def train( torch_major, torch_minor = int(torch_version[0]), int(torch_version[1]) if torch_major == 2 and torch_minor >= 2: if os.getenv("PYTORCH_CUDA_ALLOC_CONF") is None: - os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True" + os.environ[ + "PYTORCH_CUDA_ALLOC_CONF" + ] = "expandable_segments:True,roundup_power2_divisions:16" # load the tokenizer first LOG.debug( diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index f3acbbc68d..ddaf6af2eb 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -346,7 +346,16 @@ class HyperparametersConfig(BaseModel): learning_rate: Union[str, float] weight_decay: Optional[float] = 0.0 optimizer: Optional[ - Union[OptimizerNames, Literal["lion_pytorch", "optimi_adamw"]] + Union[ + OptimizerNames, + Literal[ + "lion_pytorch", + "optimi_adamw", + "ao_adamw_4bit", + "ao_adamw_8bit", + "ao_adamw_fp8", + ], + ] ] = OptimizerNames.ADAMW_HF.value optim_args: Optional[Union[str, Dict[str, Any]]] = Field( default=None, metadata={"help": "Optional arguments to supply to optimizer."} @@ -850,7 +859,7 @@ def check_better_transformers(self): @model_validator(mode="after") def check_adamw_optimizer_params(self): if any([self.adam_beta1, self.adam_beta2, self.adam_epsilon]) and ( - not self.optimizer or "adamw" not in self.optimizer.value + not self.optimizer or "adamw" not in str(self.optimizer).lower() ): LOG.warning("adamw hyperparameters found, but no adamw optimizer set") return self From fa91b698e9bc3167911362a5534973026d44befd Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 19 Jul 2024 12:21:37 -0400 Subject: [PATCH 0081/1405] Fix untrained tokens (#1771) * fix untrained reserved tokens * save model after fixing untrained embeddings * don't need fsdp conditional here --- src/axolotl/core/tokenizer_utils.py | 150 ++++++++++++++++++ src/axolotl/train.py | 8 + .../config/models/input/v0_4_1/__init__.py | 2 + 3 files changed, 160 insertions(+) create mode 100644 src/axolotl/core/tokenizer_utils.py diff --git a/src/axolotl/core/tokenizer_utils.py b/src/axolotl/core/tokenizer_utils.py new file mode 100644 index 0000000000..53c44a75c4 --- /dev/null +++ b/src/axolotl/core/tokenizer_utils.py @@ -0,0 +1,150 @@ +""" +helper functions for fixing the embeddings/tokenizer +""" + +# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import gc +import itertools + +import numpy as np +import torch + + +@torch.inference_mode +def fix_untrained_tokens(model, tokenizer, train_dataset, eps=1e-16): + """ + Many of the newer models have reserved tokens that are not trained. + """ + embedding_matrix = model.get_input_embeddings().weight + lm_head_matrix = model.get_output_embeddings().weight + + # Get untrained tokens + indicator_untrained = torch.amax(embedding_matrix, axis=1) <= eps + where_untrained = torch.where(indicator_untrained)[0] + n_untrained = where_untrained.shape[0] + n_trained = embedding_matrix.shape[0] - n_untrained + + # Get set and actual tokens + where_untrained = where_untrained.tolist() + if len(where_untrained) == 0: + return False + + # Remove untrained indices where it's longer + + where_untrained_set = frozenset(where_untrained) + actual_bad_tokens = tokenizer.convert_ids_to_tokens(where_untrained) + # Remove None items in actual_bad_tokens + actual_bad_tokens = [x for x in actual_bad_tokens if x is not None] + + # Check if tokenizer and training datasets have bad tokens + if_bad_first = False + if_bad_second = False + # Check tokenizer's chat template for any untrained tokens + chat_template = getattr(tokenizer, "chat_template", None) + if chat_template is not None: + if_bad_first = any(x in chat_template for x in actual_bad_tokens) + + # Check the first 250, last 250 input_ids + size_dataset = len(train_dataset) + size = min(size_dataset, 250) + for j in range(size): + input_ids = train_dataset[j] + if "input_ids" in input_ids: + input_ids = input_ids["input_ids"] + if_bad = any(item in where_untrained_set for item in input_ids) + if if_bad: + if_bad_second = True + break + + # Check last 250 + if not if_bad_second: + left = max(size_dataset - 250, 0) + for j in range(left, size_dataset): + input_ids = train_dataset[j] + if "input_ids" in input_ids: + input_ids = input_ids["input_ids"] + if_bad = any(item in where_untrained_set for item in input_ids) + if if_bad: + if_bad_second = True + break + + # Check if bad tokens exists! + if not if_bad_first and not if_bad_second: + return False + + # Count all the possible bad tokens + final_counts = np.zeros( + max(len(tokenizer), embedding_matrix.shape[0]), dtype=np.int64 + ) + + def mapping(examples): + input_ids = examples["input_ids"] + counter = np.fromiter(itertools.chain.from_iterable(input_ids), dtype=np.int32) + np.add.at(final_counts, counter, 1) + + train_dataset.map(mapping, batched=True, desc="Counting untrained tokens") + + # Get sum of all items + sum_embedding = torch.sum(embedding_matrix, dtype=torch.float32, axis=0) + sum_lm_head = torch.sum(lm_head_matrix, dtype=torch.float32, axis=0) + + # Remove bad tokens + sum_embedding -= torch.sum( + embedding_matrix[where_untrained], dtype=torch.float32, axis=0 + ) + sum_lm_head -= torch.sum( + lm_head_matrix[where_untrained], dtype=torch.float32, axis=0 + ) + + # Find correct average by dividing by sum of trained tokens + mean_embedding = sum_embedding / n_trained + mean_lm_head = sum_lm_head / n_trained + + # Scale each to be equal to 1/max_frequency. Also set some to 0 if none seen + scaling = final_counts[where_untrained] / max(final_counts.max(), 1) + scaling = torch.tensor(scaling, device=mean_embedding.device).unsqueeze(1) + mean_embedding = ( + mean_embedding.repeat( + ( + n_untrained, + 1, + ) + ) + * scaling + ) + mean_lm_head = ( + mean_lm_head.repeat( + ( + n_untrained, + 1, + ) + ) + * scaling + ) + where_null = scaling.ravel() == 0 + mean_embedding[where_null] = 0 + mean_lm_head[where_null] = 0 + + # Set them to the mean + embedding_matrix[where_untrained] = mean_embedding.to(embedding_matrix.dtype) + lm_head_matrix[where_untrained] = mean_lm_head.to(lm_head_matrix.dtype) + + # Clean up + for _ in range(3): + gc.collect() + torch.cuda.empty_cache() + + return True diff --git a/src/axolotl/train.py b/src/axolotl/train.py index e4d3ace192..5ba5aed562 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -19,6 +19,7 @@ from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled from axolotl.common.cli import TrainerCliArgs +from axolotl.core.tokenizer_utils import fix_untrained_tokens from axolotl.logging_config import configure_logging from axolotl.utils.dict import DictDefault from axolotl.utils.freeze import freeze_layers_except @@ -123,6 +124,13 @@ def train( total_num_steps, ) + if cfg.fix_untrained_tokens: + fix_untrained_tokens(model, tokenizer, train_dataset) + if cfg.local_rank == 0: + model.save_pretrained( + str(Path(cfg.output_dir)), safe_serialization=safe_serialization + ) + # go ahead and presave, so we have the adapter config available to inspect if peft_config: LOG.info(f"Pre-saving adapter config to {cfg.output_dir}") diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index ddaf6af2eb..7f30283af3 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -658,6 +658,8 @@ class Config: chat_template: Optional[ChatTemplate] = None default_system_message: Optional[str] = None + fix_untrained_tokens: Optional[bool] = None + # INTERNALS - document for now, generally not set externally is_preprocess: Optional[bool] = None From 985819d89bec921e919e7e83042a869f04a25974 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Sun, 21 Jul 2024 06:10:42 -0700 Subject: [PATCH 0082/1405] Add a `chat_template` prompt strategy for DPO (#1725) * Implementing a basic chat_template strategy for DPO datasets This mimics the sft chat_template strategy such that users can: * Specify the messages field * Specify the per message role and content fields * speicfy the chosen and rejected fields * Let the tokenizer construct the raw prompt * Ensure the chosen and rejected fields don't have any prefix tokens * Adding additional dpo chat template unittests * Rename test class --- examples/llama-3/instruct-dpo-lora-8b.yml | 81 +++++++++ .../prompt_strategies/dpo/chat_template.py | 78 +++++++++ src/axolotl/utils/data/rl.py | 1 + src/axolotl/utils/tokenization.py | 2 +- .../test_dpo_chat_templates.py | 156 ++++++++++++++++++ 5 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 examples/llama-3/instruct-dpo-lora-8b.yml create mode 100644 src/axolotl/prompt_strategies/dpo/chat_template.py create mode 100644 tests/prompt_strategies/test_dpo_chat_templates.py diff --git a/examples/llama-3/instruct-dpo-lora-8b.yml b/examples/llama-3/instruct-dpo-lora-8b.yml new file mode 100644 index 0000000000..14febb810a --- /dev/null +++ b/examples/llama-3/instruct-dpo-lora-8b.yml @@ -0,0 +1,81 @@ +base_model: meta-llama/Meta-Llama-3-8B-Instruct +model_type: LlamaForCausalLM +tokenizer_type: AutoTokenizer + +load_in_8bit: true +load_in_4bit: false +strict: false + +chat_template: llama3 +rl: dpo +datasets: + - path: fozziethebeat/alpaca_messages_2k_dpo_test + type: chat_template.default + chat_template: llama3 + field_messages: conversation + field_chosen: chosen + field_rejected: rejected + message_field_role: role + message_field_content: content + roles: + system: + - system + user: + - user + assistant: + - assistant + +dataset_prepared_path: +val_set_size: 0.05 +output_dir: ./outputs/lora-out + +sequence_len: 4096 +sample_packing: false +pad_to_sequence_len: true + +adapter: lora +lora_model_dir: +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_fan_in_fan_out: + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true +s2_attention: + +warmup_steps: 10 +evals_per_epoch: 4 +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: diff --git a/src/axolotl/prompt_strategies/dpo/chat_template.py b/src/axolotl/prompt_strategies/dpo/chat_template.py new file mode 100644 index 0000000000..4f2f14098d --- /dev/null +++ b/src/axolotl/prompt_strategies/dpo/chat_template.py @@ -0,0 +1,78 @@ +""" +DPO prompt strategies for using tokenizer chat templates. +""" + +from axolotl.utils.chat_templates import chat_templates + + +def default( + cfg, dataset_idx=0, **kwargs +): # pylint: disable=possibly-unused-variable,unused-argument + ds_cfg = cfg["datasets"][dataset_idx] + chat_template_str = chat_templates(cfg.chat_template) + + field_messages = ds_cfg.get("field_messages", "messages") + field_chosen = ds_cfg.get("field_chosen", "chosen") + field_rejected = ds_cfg.get("field_rejected", "rejected") + field_message_role = ds_cfg.get("message_field_role", "role") + field_message_content = ds_cfg.get("message_field_content", "content") + role_map_inv = ds_cfg.get( + "roles", + { + "user": ["user"], + "assistant": ["assistant"], + "system": ["system"], + }, + ) + role_map = {} + for target, sources in role_map_inv.items(): + for source in sources: + role_map[source] = target + + def transform_fn(sample, tokenizer=None): + messages = sample[field_messages] + messages = [ + { + "role": role_map[m[field_message_role]], + "content": m[field_message_content], + } + for m in messages + ] + chosen = { + "role": role_map[sample[field_chosen][field_message_role]], + "content": sample[field_chosen][field_message_content], + } + rejected = { + "role": role_map[sample[field_rejected][field_message_role]], + "content": sample[field_rejected][field_message_content], + } + + result = {} + result["prompt"] = tokenizer.apply_chat_template( + messages, + add_generation_prompt=True, + chat_template=chat_template_str, + tokenize=False, + ) + + result["chosen"] = tokenizer.apply_chat_template( + [chosen], + add_generation_prompt=False, + chat_template=chat_template_str, + tokenize=False, + ) + chosen_strip_index = result["chosen"].find(chosen["content"]) + result["chosen"] = result["chosen"][chosen_strip_index:] + + result["rejected"] = tokenizer.apply_chat_template( + [rejected], + add_generation_prompt=False, + chat_template=chat_template_str, + tokenize=False, + ) + rejected_strip_index = result["rejected"].find(rejected["content"]) + result["rejected"] = result["rejected"][rejected_strip_index:] + + return result + + return transform_fn diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index 7416ca28bb..d0324e1ebd 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -1,4 +1,5 @@ """data handling specific to DPO""" + import inspect import logging from functools import partial diff --git a/src/axolotl/utils/tokenization.py b/src/axolotl/utils/tokenization.py index 845296b7a6..f353aebec9 100644 --- a/src/axolotl/utils/tokenization.py +++ b/src/axolotl/utils/tokenization.py @@ -62,7 +62,7 @@ def process_tokens_for_rl_debug(tokens, color, tokenizer, text_only): """Helper function to process and color tokens.""" colored_tokens = [ color_token_for_rl_debug(tokenizer.decode(token), token, color, text_only) - for token in tokenizer.encode(tokens) + for token in tokenizer.encode(tokens, add_special_tokens=False) ] return colored_tokens diff --git a/tests/prompt_strategies/test_dpo_chat_templates.py b/tests/prompt_strategies/test_dpo_chat_templates.py new file mode 100644 index 0000000000..cca48b1cf3 --- /dev/null +++ b/tests/prompt_strategies/test_dpo_chat_templates.py @@ -0,0 +1,156 @@ +""" +tests for chat_template prompt strategy +""" + +import unittest + +import pytest +from datasets import Dataset +from transformers import AutoTokenizer + +from axolotl.prompt_strategies.dpo.chat_template import default +from axolotl.utils.dict import DictDefault + + +@pytest.fixture(name="assistant_dataset") +def fixture_assistant_dataset(): + # pylint: disable=duplicate-code + return Dataset.from_list( + [ + { + "messages": [ + { + "role": "user", + "content": "hello", + }, + { + "role": "assistant", + "content": "hello", + }, + { + "role": "user", + "content": "goodbye", + }, + ], + "chosen": { + "role": "assistant", + "content": "goodbye", + }, + "rejected": { + "role": "assistant", + "content": "party on", + }, + } + ] + ) + + +@pytest.fixture(name="custom_assistant_dataset") +def fixture_custom_assistant_dataset(): + # pylint: disable=duplicate-code + return Dataset.from_list( + [ + { + "conversation": [ + { + "speaker": "human", + "text": "hello", + }, + { + "speaker": "agent", + "text": "hello", + }, + { + "speaker": "human", + "text": "goodbye", + }, + ], + "better": { + "speaker": "agent", + "text": "goodbye", + }, + "worse": { + "speaker": "agent", + "text": "party on", + }, + } + ] + ) + + +@pytest.fixture(name="llama3_tokenizer") +def fixture_llama3_tokenizer(): + tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B") + tokenizer.eos_token = "<|eot_id|>" + + return tokenizer + + +class TestAssistantDPOChatTemplateLlama3: + """ + Test class for assistant style datasets with llama-3 prompts using the chat_template strategy. + """ + + def test_llama3_defaults(self, llama3_tokenizer, assistant_dataset): + # pylint: disable=duplicate-code + transform_fn = default( + DictDefault( + { + "chat_template": "llama3", + "datasets": [ + { + "chat_template": "llama3", + } + ], + } + ) + ) + result = transform_fn(assistant_dataset[0], tokenizer=llama3_tokenizer) + assert result["prompt"] == ( + "<|begin_of_text|>" + + "<|start_header_id|>user<|end_header_id|>\n\nhello<|eot_id|>" + + "<|start_header_id|>assistant<|end_header_id|>\n\nhello<|eot_id|>" + + "<|start_header_id|>user<|end_header_id|>\n\ngoodbye<|eot_id|>" + + "<|start_header_id|>assistant<|end_header_id|>\n\n" + ) + assert result["chosen"] == "goodbye<|eot_id|>" + assert result["rejected"] == "party on<|eot_id|>" + + def test_llama3_configured(self, llama3_tokenizer, custom_assistant_dataset): + # pylint: disable=duplicate-code + transform_fn = default( + DictDefault( + { + "chat_template": "llama3", + "datasets": [ + { + "chat_template": "llama3", + "field_messages": "conversation", + "field_chosen": "better", + "field_rejected": "worse", + "message_field_role": "speaker", + "message_field_content": "text", + "roles": { + "user": ["human"], + "assistant": ["agent"], + "system": ["sys"], + }, + } + ], + } + ) + ) + result = transform_fn(custom_assistant_dataset[0], tokenizer=llama3_tokenizer) + assert result["prompt"] == ( + "<|begin_of_text|>" + + "<|start_header_id|>user<|end_header_id|>\n\nhello<|eot_id|>" + + "<|start_header_id|>assistant<|end_header_id|>\n\nhello<|eot_id|>" + + "<|start_header_id|>user<|end_header_id|>\n\ngoodbye<|eot_id|>" + + "<|start_header_id|>assistant<|end_header_id|>\n\n" + ) + assert result["chosen"] == "goodbye<|eot_id|>" + assert result["rejected"] == "party on<|eot_id|>" + + +if __name__ == "__main__": + unittest.main() From 87455e7f3228972416904f2a23cc1b67a1d2d855 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 23 Jul 2024 01:41:11 -0400 Subject: [PATCH 0083/1405] swaps to use newer sample packing for mistral (#1773) * swaps to use newer sample packing for mistral * fix multipack patch test * patch the common fa utils * update for refactor of flash attn unpad * remove un-needed drop attn mask for mistral * bump transformers to main to pick up latest mistral fix for 12b and refactor of fa2 * update test --- requirements.txt | 2 +- .../monkeypatch/mistral_attn_hijack_flash.py | 10 +++ src/axolotl/monkeypatch/multipack.py | 32 +++++--- src/axolotl/monkeypatch/unsloth_.py | 79 ++++++++++--------- src/axolotl/utils/models.py | 21 ++--- src/axolotl/utils/trainer.py | 4 +- tests/e2e/patched/test_model_patches.py | 8 +- 7 files changed, 86 insertions(+), 70 deletions(-) diff --git a/requirements.txt b/requirements.txt index 39f6c8a77f..b2aac0dd04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.11.1 -transformers==4.42.4 +transformers @ git+https://github.com/huggingface/transformers.git@0fdea8607d7e01eb0e38a1ebeb7feee30a22f0cf tokenizers==0.19.1 bitsandbytes==0.43.1 accelerate==0.32.0 diff --git a/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py b/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py index c5425dd520..1cbc4278ba 100644 --- a/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py @@ -2,6 +2,7 @@ # pylint: disable=duplicate-code import logging +from functools import partial from typing import List, Optional, Tuple, Union import torch @@ -45,6 +46,15 @@ def replace_mistral_attn_with_flash_attn( ) +def patch_mistral_cross_entropy(): + from flash_attn.losses.cross_entropy import CrossEntropyLoss + + LOG.info("patching with flash_attn.losses.cross_entropy") + transformers.models.mistral.modeling_mistral.CrossEntropyLoss = partial( + CrossEntropyLoss, inplace_backward=True + ) + + @torch.jit.script def _make_sliding_window_causal_mask( bsz: int, diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 017adb2bfd..a2ce0e64fd 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -11,6 +11,7 @@ SUPPORTED_MULTIPACK_MODEL_TYPES = [ "llama", + "mistral", "mixtral", "qwen2", "qwen2_moe", @@ -25,6 +26,19 @@ def patch_for_multipack(model_type, model_name=None): + if model_type == "gemmoe": + patch_remote(model_name, ".configuration_gemmoe", ".modeling_gemmoe") + elif model_type == "deepseek_v2": + patch_remote(model_name, ".configuration_deepseek", ".modeling_deepseek") + elif hasattr(transformers, "modeling_flash_attention_utils"): + transformers.modeling_flash_attention_utils._get_unpad_data = ( # pylint: disable=protected-access + get_unpad_data + ) + if model_type == "mixtral" and is_deepspeed_zero3_enabled(): + patch_mixtral_moe_forward_zero3() + return + + # retain for legacy if model_type == "mixtral": transformers.models.mixtral.modeling_mixtral._get_unpad_data = ( # pylint: disable=protected-access get_unpad_data @@ -32,9 +46,15 @@ def patch_for_multipack(model_type, model_name=None): if is_deepspeed_zero3_enabled(): patch_mixtral_moe_forward_zero3() elif model_type == "llama": - transformers.models.llama.modeling_llama._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) + if hasattr(transformers.models.llama.modeling_llama, "_get_unpad_data"): + transformers.models.llama.modeling_llama._get_unpad_data = ( # pylint: disable=protected-access + get_unpad_data + ) + elif model_type == "mistral": + if hasattr(transformers.models.mistral.modeling_mistral, "_get_unpad_data"): + transformers.models.llama.modeling_llama._get_unpad_data = ( # pylint: disable=protected-access + get_unpad_data + ) elif model_type == "qwen2": transformers.models.qwen2.modeling_qwen2._get_unpad_data = ( # pylint: disable=protected-access get_unpad_data @@ -63,12 +83,6 @@ def patch_for_multipack(model_type, model_name=None): transformers.models.starcoder2.modeling_starcoder2._get_unpad_data = ( # pylint: disable=protected-access get_unpad_data ) - elif model_type == "gemmoe": - patch_remote(model_name, ".configuration_gemmoe", ".modeling_gemmoe") - elif model_type == "jamba": - patch_remote(model_name, ".configuration_jamba", ".modeling_jamba") - elif model_type == "deepseek_v2": - patch_remote(model_name, ".configuration_deepseek", ".modeling_deepseek") def patch_remote(model_name, config_name, modeling_name): diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py index b1f0bddc00..5b1f0061de 100644 --- a/src/axolotl/monkeypatch/unsloth_.py +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -99,48 +99,51 @@ def check_self_attn_is_patchable() -> bool: return ORIGINAL_QKV_CODE in qkv and ORIGINAL_O_CODE in qkv -def integrate_cross_entropy_loss_patch(): - forward = get_forward_code() - LlamaForCausalLM._original_forward = forward # pylint: disable=protected-access - forward, _ = detab_code(forward) - assert ORIGINAL_CEL_CODE in forward, "Original forward code not found" +def integrate_cross_entropy_loss_patch(model_type: str = "llama") -> None: + if model_type == "llama": + forward = get_forward_code() + LlamaForCausalLM._original_forward = forward # pylint: disable=protected-access + forward, _ = detab_code(forward) + assert ORIGINAL_CEL_CODE in forward, "Original forward code not found" + + forward = forward.replace( + "@add_start_docstrings_to_model_forward(LLAMA_INPUTS_DOCSTRING)", "" + ) + forward = forward.replace( + "@replace_return_docstrings(output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC)", + "", + ) + forward = forward.replace(ORIGINAL_CEL_CODE, PATCHED_CEL_CODE) + forward = forward.replace( + "def forward(", + "def fast_cross_entropy_loss_forward(", + 1, + ) - forward = forward.replace( - "@add_start_docstrings_to_model_forward(LLAMA_INPUTS_DOCSTRING)", "" - ) - forward = forward.replace( - "@replace_return_docstrings(output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC)", - "", - ) - forward = forward.replace(ORIGINAL_CEL_CODE, PATCHED_CEL_CODE) - forward = forward.replace( - "def forward(", - "def fast_cross_entropy_loss_forward(", - 1, - ) + # load imports necessary + import transformers.models.llama.modeling_llama - # load imports necessary - import transformers.models.llama.modeling_llama + items_to_import = [] + for item in dir(transformers.models.llama.modeling_llama): + if item in forward: + items_to_import.append(item) - items_to_import = [] - for item in dir(transformers.models.llama.modeling_llama): - if item in forward: - items_to_import.append(item) - - exec( # pylint: disable=exec-used # nosec B102 - "from unsloth.kernels.cross_entropy_loss import fast_cross_entropy_loss", - globals(), - ) + exec( # pylint: disable=exec-used # nosec B102 + "from unsloth.kernels.cross_entropy_loss import fast_cross_entropy_loss", + globals(), + ) - exec( # pylint: disable=exec-used # nosec B102 - "from transformers.models.llama.modeling_llama import (" - + ", ".join(x for x in items_to_import) - + ")", - globals(), - ) - exec(forward, globals()) # pylint: disable=exec-used # nosec B102 - LOG.info("patching unsloth fast_cross_entropy_loss", main_process_only=True) - LlamaForCausalLM.forward = fast_cross_entropy_loss_forward # pylint: disable=undefined-variable # noqa: F821 + exec( # pylint: disable=exec-used # nosec B102 + "from transformers.models.llama.modeling_llama import (" + + ", ".join(x for x in items_to_import) + + ")", + globals(), + ) + exec(forward, globals()) # pylint: disable=exec-used # nosec B102 + LOG.info("patching unsloth fast_cross_entropy_loss", main_process_only=True) + LlamaForCausalLM.forward = fast_cross_entropy_loss_forward # pylint: disable=undefined-variable # noqa: F821 + else: + raise ValueError("Unsupported model type") def detab_code(code: str) -> Tuple[str, str]: diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 6185f0102f..339195df79 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -367,7 +367,7 @@ def load_model( integrate_cross_entropy_loss_patch, ) - integrate_cross_entropy_loss_patch() + integrate_cross_entropy_loss_patch(model_type="llama") if cfg.unsloth_lora_qkv or cfg.unsloth_lora_o: from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora @@ -424,7 +424,7 @@ def load_model( if cfg.unsloth_cross_entropy_loss: from axolotl.monkeypatch.unsloth_ import integrate_cross_entropy_loss_patch - integrate_cross_entropy_loss_patch() + integrate_cross_entropy_loss_patch(model_type="llama") if cfg.unsloth_lora_qkv or cfg.unsloth_lora_o: from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora @@ -432,23 +432,12 @@ def load_model( patch_self_attn_lora() # Modify mistral derived models - if ( - cfg.model_config_type == "mistral" - and cfg.flash_attention - and cfg.sample_packing - ): + if cfg.model_config_type == "mistral" and cfg.flash_attn_cross_entropy_loss: from axolotl.monkeypatch.mistral_attn_hijack_flash import ( - replace_mistral_attn_with_flash_attn, + patch_mistral_cross_entropy, ) - LOG.info("patching mistral with flash attention") - replace_mistral_attn_with_flash_attn(packed=cfg.sample_packing) - - if cfg.is_llama_derived_model and cfg.sample_packing and not inference: - from axolotl.monkeypatch.llama_expand_mask import hijack_expand_mask - - LOG.info("patching _expand_mask") - hijack_expand_mask() + patch_mistral_cross_entropy() model_kwargs: Dict[str, Any] = {} diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index a16baaae0f..65c2d424e5 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -189,9 +189,7 @@ def process_datasets_for_packing(cfg, train_dataset, eval_dataset): max_input_len = np.max(get_dataset_lengths(train_dataset)) LOG.debug(f"max_input_len: {max_input_len}", main_process_only=True) - if ( - cfg.is_mistral_derived_model and cfg.flash_attention - ) or cfg.model_config_type == "mamba": + if cfg.model_config_type == "mamba": LOG.info("dropping attention_mask column") train_dataset = train_dataset.remove_columns("attention_mask") if eval_dataset: diff --git a/tests/e2e/patched/test_model_patches.py b/tests/e2e/patched/test_model_patches.py index eecd1b3c11..170c37fd6c 100644 --- a/tests/e2e/patched/test_model_patches.py +++ b/tests/e2e/patched/test_model_patches.py @@ -4,6 +4,8 @@ import unittest +import transformers + from axolotl.common.cli import TrainerCliArgs from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -87,9 +89,9 @@ def test_mistral_multipack(self, temp_dir): normalize_config(cfg) cli_args = TrainerCliArgs() tokenizer = load_tokenizer(cfg) - model, _ = load_model(cfg, tokenizer, inference=cli_args.inference) + load_model(cfg, tokenizer, inference=cli_args.inference) assert ( - "axolotl.monkeypatch.mistral_attn_hijack_flash" - in model.model.layers[0].self_attn.forward.__module__ + "torch.jit" + in transformers.modeling_flash_attention_utils._get_unpad_data.__module__ # pylint: disable=protected-access ) From 608a2f3180eceebb39c73154332bb209d41469fe Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 23 Jul 2024 13:21:03 -0400 Subject: [PATCH 0084/1405] bump transformers for updated llama 3.1 (#1778) * bump transformers for updated llama 3.1 * bump for patch fix --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b2aac0dd04..a54a42ad9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.11.1 -transformers @ git+https://github.com/huggingface/transformers.git@0fdea8607d7e01eb0e38a1ebeb7feee30a22f0cf +transformers==4.43.1 tokenizers==0.19.1 bitsandbytes==0.43.1 accelerate==0.32.0 From e6b299dd79f75537f3e247a3d22dfed5d3885bfb Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 23 Jul 2024 19:54:15 -0400 Subject: [PATCH 0085/1405] bump flash attention to 2.6.2 (#1781) [skip ci] --- requirements.txt | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index a54a42ad9e..ec571570bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ fire PyYAML>=6.0 requests datasets==2.19.1 -flash-attn==2.6.1 +flash-attn==2.6.2 sentencepiece wandb einops diff --git a/setup.py b/setup.py index 9e6f34ad81..ceba636690 100644 --- a/setup.py +++ b/setup.py @@ -80,10 +80,10 @@ def parse_requirements(): dependency_links=dependency_links, extras_require={ "flash-attn": [ - "flash-attn==2.6.1", + "flash-attn==2.6.2", ], "fused-dense-lib": [ - "fused-dense-lib @ git+https://github.com/Dao-AILab/flash-attention@v2.6.1#subdirectory=csrc/fused_dense_lib", + "fused-dense-lib @ git+https://github.com/Dao-AILab/flash-attention@v2.6.2#subdirectory=csrc/fused_dense_lib", ], "deepspeed": [ "deepspeed @ git+https://github.com/microsoft/DeepSpeed.git@bc48371c5e1fb8fd70fc79285e66201dbb65679b", From fe250ada78ff3d5404e053f2ae050d66f3943248 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 23 Jul 2024 19:54:28 -0400 Subject: [PATCH 0086/1405] fix fsdp loading of models, esp 70b (#1780) --- src/axolotl/utils/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 339195df79..436b31fefd 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -619,7 +619,7 @@ def load_model( and not cfg.trust_remote_code and not cfg.gptq ): - if qlora_fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: + if cfg.fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: skip_move_to_device = True if "device_map" in model_kwargs: del model_kwargs["device_map"] @@ -701,7 +701,7 @@ def load_model( **model_kwargs, ) else: - if qlora_fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: + if cfg.fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: # disabling either of these two still leads to VRAM spike before setting back down skip_move_to_device = True if "device_map" in model_kwargs: From 6a9cfec2227935393bcfc0fbe324ef6232c520ec Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 23 Jul 2024 21:22:16 -0400 Subject: [PATCH 0087/1405] add support for simpo via cpo trainer (#1772) * add support for simpo via cpo trainer * add cpo_alpha / sft_weight from the paper * make sure to use the right builder for simpo --- src/axolotl/core/trainer_builder.py | 47 +++++++++++++++++-- .../config/models/input/v0_4_1/__init__.py | 3 ++ src/axolotl/utils/trainer.py | 2 +- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 616b1d4eb6..9a12c5a062 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -30,7 +30,16 @@ ) from transformers.trainer_utils import seed_worker from transformers.utils import is_sagemaker_mp_enabled -from trl import DPOConfig, DPOTrainer, KTOConfig, KTOTrainer, ORPOConfig, ORPOTrainer +from trl import ( + CPOConfig, + CPOTrainer, + DPOConfig, + DPOTrainer, + KTOConfig, + KTOTrainer, + ORPOConfig, + ORPOTrainer, +) from trl.trainer.utils import pad_to_length from axolotl.loraplus import create_loraplus_optimizer @@ -265,6 +274,18 @@ class AxolotlKTOConfig(AxolotlTrainingMixins, KTOConfig): """ +@dataclass +class AxolotlCPOConfig(AxolotlTrainingMixins, CPOConfig): + """ + CPO config for CPO training + """ + + simpo_gamma: Optional[float] = field( + default=None, + metadata={"help": "simpo gamma parameter"}, + ) + + class AxolotlTrainer(Trainer): """ Extend the base Trainer for axolotl helpers @@ -985,6 +1006,14 @@ class AxolotlKTOTrainer(KTOTrainer): tag_names = ["axolotl", "kto"] +class AxolotlCPOTrainer(CPOTrainer): + """ + Extend the base CPOTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "cpo"] + + class TrainerBuilderBase(abc.ABC): """ Base class for trainer builder @@ -1707,6 +1736,8 @@ def build_training_arguments(self, total_num_steps): # default to saving each epoch if not defined training_args_kwargs["save_strategy"] = "epoch" + if self.cfg.rl_beta: + training_args_kwargs["beta"] = self.cfg.rl_beta if self.cfg.orpo_alpha: # trl does some odd mapping of alpha to beta to reuse the beta parameter ??? training_args_kwargs["beta"] = self.cfg.orpo_alpha @@ -1715,9 +1746,16 @@ def build_training_arguments(self, total_num_steps): training_args_cls = AxolotlDPOConfig if self.cfg.rpo_alpha is not None: training_args_kwargs["rpo_alpha"] = self.cfg.rpo_alpha + + if self.cfg.rl == "simpo": + training_args_cls = AxolotlCPOConfig + training_args_kwargs["loss_type"] = "simpo" + training_args_kwargs["simpo_gamma"] = self.cfg.simpo_gamma + if self.cfg.cpo_alpha is not None: + training_args_kwargs["cpo_alpha"] = self.cfg.cpo_alpha + if self.cfg.rl == "orpo": training_args_cls = AxolotlORPOConfig - training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes training_args_kwargs["max_length"] = self.cfg.sequence_len if self.cfg.max_prompt_len: training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len @@ -1725,7 +1763,6 @@ def build_training_arguments(self, total_num_steps): if self.cfg.rl == "kto": training_args_cls = AxolotlKTOConfig - training_args_kwargs["beta"] = self.cfg.rl_beta or 0.1 training_args_kwargs["desirable_weight"] = ( self.cfg.kto_desirable_weight or 1.0 ) @@ -1771,7 +1808,6 @@ def build(self, total_num_steps): ] = self.cfg.precompute_ref_log_probs if self.cfg.rl in ["dpo", "ipo"]: trainer_cls = AxolotlDPOTrainer - dpo_trainer_kwargs["beta"] = self.cfg.rl_beta or 0.1 trainer_cls_args = [self.model, self.model_ref] # these aren't used for the ORPO trainer @@ -1785,6 +1821,9 @@ def build(self, total_num_steps): elif self.cfg.rl in ["kto"]: trainer_cls = AxolotlKTOTrainer trainer_cls_args = [self.model] + elif self.cfg.rl in ["simpo"]: + trainer_cls = AxolotlCPOTrainer + trainer_cls_args = [self.model] else: raise ValueError(f"Unsupported RL: {self.cfg.rl}") dpo_trainer = trainer_cls( diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 7f30283af3..7397c7c739 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -172,6 +172,7 @@ class RLType(str, Enum): ipo = "ipo" # pylint: disable=invalid-name orpo = "orpo" # pylint: disable=invalid-name kto = "kto" # pylint: disable=invalid-name + simpo = "simpo" # pylint: disable=invalid-name class ChatTemplate(str, Enum): @@ -644,6 +645,8 @@ class Config: orpo_alpha: Optional[float] = None rpo_alpha: Optional[float] = None + simpo_gamma: Optional[float] = None + cpo_alpha: Optional[float] = None kto_desirable_weight: Optional[float] = None kto_undesirable_weight: Optional[float] = None diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 65c2d424e5..c5a71e689a 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -425,7 +425,7 @@ def prepare_optim_env(cfg): def setup_trainer(cfg, train_dataset, eval_dataset, model, tokenizer, total_num_steps): - if cfg.rl in ["dpo", "ipo", "orpo", "kto"]: + if cfg.rl in ["dpo", "ipo", "orpo", "kto", "simpo"]: trainer_builder = HFRLTrainerBuilder(cfg, model[0], tokenizer) trainer_builder.model_ref = model[1] trainer_builder.peft_config = model[2] From 22680913f3ac4bf3410855210648f396cfd5c7d5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 27 Jul 2024 10:24:11 -0400 Subject: [PATCH 0088/1405] Bump deepspeed 20240727 (#1790) * pin deepspeed to 0.14.4 otherwise it doesn't play nice with trl * Add test to import to try to trigger import dependencies --- requirements.txt | 2 +- setup.py | 2 +- tests/e2e/test_imports.py | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/test_imports.py diff --git a/requirements.txt b/requirements.txt index ec571570bb..981a625580 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ transformers==4.43.1 tokenizers==0.19.1 bitsandbytes==0.43.1 accelerate==0.32.0 -deepspeed @ git+https://github.com/microsoft/DeepSpeed.git@bc48371c5e1fb8fd70fc79285e66201dbb65679b +deepspeed==0.14.4 pydantic==2.6.3 addict fire diff --git a/setup.py b/setup.py index ceba636690..1d164e0a18 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ def parse_requirements(): "fused-dense-lib @ git+https://github.com/Dao-AILab/flash-attention@v2.6.2#subdirectory=csrc/fused_dense_lib", ], "deepspeed": [ - "deepspeed @ git+https://github.com/microsoft/DeepSpeed.git@bc48371c5e1fb8fd70fc79285e66201dbb65679b", + "deepspeed==0.14.4", "deepspeed-kernels", ], "mamba-ssm": [ diff --git a/tests/e2e/test_imports.py b/tests/e2e/test_imports.py new file mode 100644 index 0000000000..f186eaac46 --- /dev/null +++ b/tests/e2e/test_imports.py @@ -0,0 +1,20 @@ +""" +test module to import various submodules that have historically broken due to dependency issues +""" +import unittest + + +class TestImports(unittest.TestCase): + """ + Test class to import various submodules that have historically broken due to dependency issues + """ + + def test_import_causal_trainer(self): + from axolotl.core.trainer_builder import ( # pylint: disable=unused-import # noqa: F401 + HFCausalTrainerBuilder, + ) + + def test_import_rl_trainer(self): + from axolotl.core.trainer_builder import ( # pylint: disable=unused-import # noqa: F401 + HFRLTrainerBuilder, + ) From 94ba93259f421d438e53117267b17097c48cdd65 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 28 Jul 2024 07:25:54 -0400 Subject: [PATCH 0089/1405] various batch of fixes (#1785) * various batch of fixes * more tweaks * fix autoawq requirement for torch flexibility * simplify conditionals * multi-node fixes wip * bump transformers and include 405b qlora+fsdp yaml --- examples/llama-3/qlora-fsdp-405b.yaml | 62 +++++++ requirements.txt | 3 +- src/axolotl/cli/preprocess.py | 9 +- src/axolotl/common/architectures.py | 14 ++ src/axolotl/core/trainer_builder.py | 154 ++++++++++-------- .../prompt_strategies/dpo/chat_template.py | 4 +- src/axolotl/train.py | 29 ++-- src/axolotl/utils/data/sft.py | 9 +- src/axolotl/utils/distributed.py | 4 + src/axolotl/utils/models.py | 42 ++++- src/axolotl/utils/trainer.py | 24 ++- 11 files changed, 254 insertions(+), 100 deletions(-) create mode 100644 examples/llama-3/qlora-fsdp-405b.yaml create mode 100644 src/axolotl/common/architectures.py diff --git a/examples/llama-3/qlora-fsdp-405b.yaml b/examples/llama-3/qlora-fsdp-405b.yaml new file mode 100644 index 0000000000..385b7f91d6 --- /dev/null +++ b/examples/llama-3/qlora-fsdp-405b.yaml @@ -0,0 +1,62 @@ +base_model: meta-llama/Meta-Llama-3.1-405B +tokenizer_type: AutoTokenizer + +load_in_4bit: true +strict: false + +datasets: + - path: tatsu-lab/alpaca + type: alpaca +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out/qlora-llama3_1-405b + +adapter: qlora + +sequence_len: 1024 +sample_packing: true +pad_to_sequence_len: true + +lora_r: 16 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: +lora_target_linear: true + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_torch +lr_scheduler: cosine +learning_rate: 0.00001 + +train_on_inputs: false +group_by_length: false +bf16: true +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: true +logging_steps: 1 +flash_attention: true + +warmup_steps: 10 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_limit_all_gathers: true + fsdp_sync_module_states: true + fsdp_offload_params: true + fsdp_use_orig_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD +special_tokens: + pad_token: <|finetune_right_pad_id|> diff --git a/requirements.txt b/requirements.txt index 981a625580..5825ee1903 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.11.1 -transformers==4.43.1 +transformers==4.43.3 tokenizers==0.19.1 bitsandbytes==0.43.1 accelerate==0.32.0 @@ -32,6 +32,7 @@ fschat @ git+https://github.com/lm-sys/FastChat.git@27a05b04a35510afb1d767ae7e59 gradio==3.50.2 tensorboard python-dotenv==1.0.1 +autoawq>=0.2.5 mamba-ssm==1.2.0.post1 diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index 5ec279d4b2..e0dd7c2dc1 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -2,6 +2,7 @@ CLI to run training on a model """ import logging +import warnings from pathlib import Path from typing import Union @@ -76,8 +77,12 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): if parsed_cli_args.download: model_name = parsed_cfg.base_model - with init_empty_weights(): - AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True) + with warnings.catch_warnings(): + # there are a bunch of useless UserWarnings about + # "copying from a non-meta parameter in the checkpoint to a meta parameter in the current model" + warnings.simplefilter("ignore") + with init_empty_weights(include_buffers=True): + AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True) LOG.info( Fore.GREEN diff --git a/src/axolotl/common/architectures.py b/src/axolotl/common/architectures.py new file mode 100644 index 0000000000..7610b335a4 --- /dev/null +++ b/src/axolotl/common/architectures.py @@ -0,0 +1,14 @@ +""" +Common architecture specific constants +""" + +MOE_ARCH_BLOCK = { + "dbrx": "DbrxFFN", + "jamba": "JambaSparseMoeBlock", + "jetmoe": [ + "JetMoeMoA", + "JetMoeMoE", + ], + "mixtral": "MixtralSparseMoeBlock", + "qwen2_moe": "Qwen2MoeSparseMoeBlock", +} diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 9a12c5a062..ff4804b104 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -8,6 +8,7 @@ import importlib.util import logging import math +import os import sys from abc import abstractmethod from collections import defaultdict @@ -28,7 +29,7 @@ TrainerCallback, TrainingArguments, ) -from transformers.trainer_utils import seed_worker +from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, seed_worker from transformers.utils import is_sagemaker_mp_enabled from trl import ( CPOConfig, @@ -286,7 +287,77 @@ class AxolotlCPOConfig(AxolotlTrainingMixins, CPOConfig): ) -class AxolotlTrainer(Trainer): +class SchedulerMixin(Trainer): + """ + Mixin class for scheduler setup in CausalTrainer. + """ + + args = None # type: AxolotlTrainingArguments + + def create_scheduler( + self, num_training_steps: int, optimizer: torch.optim.Optimizer = None + ): + """ + Setup the scheduler. The optimizer of the trainer must have been set up either before this method is called or + passed as an argument. + + Args: + num_training_steps (int): The number of training steps to do. + optimizer (torch.optim.Optimizer): The training optimizer + """ + use_cosine_quadratic = ( + self.args.lr_scheduler_type == "cosine" + and self.args.lr_quadratic_warmup is True + ) + + use_cosine_min_lr = ( + self.args.lr_scheduler_type == "cosine" + and self.args.cosine_min_lr_ratio is not None + ) + + # fmt: off + if self.lr_scheduler is None: # type: ignore # pylint: disable=access-member-before-definition + # fmt: on + if use_cosine_quadratic: + if use_cosine_min_lr: + LOG.warning("Both cosine quadratic warmup and min lr detected. Using quadratic warmup.") + + self.lr_scheduler = get_cosine_schedule_with_quadratic_warmup( # pylint: disable=attribute-defined-outside-init + optimizer, + num_warmup_steps=self.args.get_warmup_steps(num_training_steps), + num_training_steps=num_training_steps, + ) + elif self.args.cosine_min_lr_ratio and self.args.cosine_constant_lr_ratio and use_cosine_min_lr: + assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" + assert 0 <= self.args.cosine_constant_lr_ratio <= 1.0, "cosine_constant_lr_ratio must be between 0.0 and 1.0" + self.lr_scheduler = get_cosine_schedule_with_warmup_decay_constant( # pylint: disable=attribute-defined-outside-init + optimizer, + num_warmup_steps=self.args.get_warmup_steps(num_training_steps), + num_training_steps=num_training_steps, + min_lr_ratio=self.args.cosine_min_lr_ratio, + constant_lr_ratio=self.args.cosine_constant_lr_ratio, + ) + elif self.args.cosine_min_lr_ratio and use_cosine_min_lr: + assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" + self.lr_scheduler = get_cosine_schedule_with_min_lr( # pylint: disable=attribute-defined-outside-init + optimizer, + num_warmup_steps=self.args.get_warmup_steps(num_training_steps), + num_training_steps=num_training_steps, + min_lr_ratio=self.args.cosine_min_lr_ratio, + ) + else: + return super().create_scheduler(num_training_steps, optimizer) + else: + if use_cosine_quadratic: + LOG.warning("axolotl's cosine scheduler with quadratic warmup not used (e.g., because of deepspeed).") + + if use_cosine_min_lr: + LOG.warning("axolotl's cosine scheduler with min lr not used (e.g., because of deepspeed).") + + return self.lr_scheduler + + +class AxolotlTrainer(SchedulerMixin, Trainer): """ Extend the base Trainer for axolotl helpers """ @@ -404,68 +475,6 @@ def create_optimizer(self): return self.optimizer - def create_scheduler( - self, num_training_steps: int, optimizer: torch.optim.Optimizer = None - ): - """ - Setup the scheduler. The optimizer of the trainer must have been set up either before this method is called or - passed as an argument. - - Args: - num_training_steps (int): The number of training steps to do. - optimizer (torch.optim.Optimizer): The training optimizer - """ - use_cosine_quadratic = ( - self.args.lr_scheduler_type == "cosine" - and self.args.lr_quadratic_warmup is True - ) - - use_cosine_min_lr = ( - self.args.lr_scheduler_type == "cosine" - and self.args.cosine_min_lr_ratio is not None - ) - - # fmt: off - if self.lr_scheduler is None: # type: ignore # pylint: disable=access-member-before-definition - # fmt: on - if use_cosine_quadratic: - if use_cosine_min_lr: - LOG.warning("Both cosine quadratic warmup and min lr detected. Using quadratic warmup.") - - self.lr_scheduler = get_cosine_schedule_with_quadratic_warmup( # pylint: disable=attribute-defined-outside-init - optimizer, - num_warmup_steps=self.args.get_warmup_steps(num_training_steps), - num_training_steps=num_training_steps, - ) - elif self.args.cosine_min_lr_ratio and self.args.cosine_constant_lr_ratio and use_cosine_min_lr: - assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" - assert 0 <= self.args.cosine_constant_lr_ratio <= 1.0, "cosine_constant_lr_ratio must be between 0.0 and 1.0" - self.lr_scheduler = get_cosine_schedule_with_warmup_decay_constant( # pylint: disable=attribute-defined-outside-init - optimizer, - num_warmup_steps=self.args.get_warmup_steps(num_training_steps), - num_training_steps=num_training_steps, - min_lr_ratio=self.args.cosine_min_lr_ratio, - constant_lr_ratio=self.args.cosine_constant_lr_ratio, - ) - elif self.args.cosine_min_lr_ratio and use_cosine_min_lr: - assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" - self.lr_scheduler = get_cosine_schedule_with_min_lr( # pylint: disable=attribute-defined-outside-init - optimizer, - num_warmup_steps=self.args.get_warmup_steps(num_training_steps), - num_training_steps=num_training_steps, - min_lr_ratio=self.args.cosine_min_lr_ratio, - ) - else: - return super().create_scheduler(num_training_steps, optimizer) - else: - if use_cosine_quadratic: - LOG.warning("axolotl's cosine scheduler with quadratic warmup not used (e.g., because of deepspeed).") - - if use_cosine_min_lr: - LOG.warning("axolotl's cosine scheduler with min lr not used (e.g., because of deepspeed).") - - return self.lr_scheduler - def _get_train_sampler(self) -> Optional[torch.utils.data.Sampler]: if self.args.sample_packing and not self.args.pretraining: if self.args.multipack_real_batches: @@ -830,6 +839,14 @@ def store_metrics( for key, value in metrics.items(): self._stored_metrics[train_eval][key].append(value) + def _save_checkpoint(self, model, trial, metrics=None): + # make sure the checkpoint dir exists, since trainer is flakey + checkpoint_folder = f"{PREFIX_CHECKPOINT_DIR}-{self.state.global_step}" + run_dir = self._get_output_dir(trial=trial) + output_dir = os.path.join(run_dir, checkpoint_folder) + os.makedirs(output_dir, exist_ok=True) + return super()._save_checkpoint(model, trial, metrics=metrics) + class AxolotlMambaTrainer(AxolotlTrainer): """ @@ -929,7 +946,7 @@ def create_scheduler( return self.lr_scheduler -class AxolotlDPOTrainer(DPOTrainer): +class AxolotlDPOTrainer(SchedulerMixin, DPOTrainer): """ Extend the base DPOTrainer for axolotl helpers """ @@ -990,7 +1007,7 @@ def tokenize_row( return res -class AxolotlORPOTrainer(ORPOTrainer): +class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): """ Extend the base ORPOTrainer for axolotl helpers """ @@ -998,7 +1015,7 @@ class AxolotlORPOTrainer(ORPOTrainer): tag_names = ["axolotl", "orpo"] -class AxolotlKTOTrainer(KTOTrainer): +class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): """ Extend the base KTOTrainer for axolotl helpers """ @@ -1006,7 +1023,7 @@ class AxolotlKTOTrainer(KTOTrainer): tag_names = ["axolotl", "kto"] -class AxolotlCPOTrainer(CPOTrainer): +class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): """ Extend the base CPOTrainer for axolotl helpers """ @@ -1750,6 +1767,7 @@ def build_training_arguments(self, total_num_steps): if self.cfg.rl == "simpo": training_args_cls = AxolotlCPOConfig training_args_kwargs["loss_type"] = "simpo" + training_args_kwargs["max_length"] = self.cfg.sequence_len training_args_kwargs["simpo_gamma"] = self.cfg.simpo_gamma if self.cfg.cpo_alpha is not None: training_args_kwargs["cpo_alpha"] = self.cfg.cpo_alpha diff --git a/src/axolotl/prompt_strategies/dpo/chat_template.py b/src/axolotl/prompt_strategies/dpo/chat_template.py index 4f2f14098d..e0e5eb1294 100644 --- a/src/axolotl/prompt_strategies/dpo/chat_template.py +++ b/src/axolotl/prompt_strategies/dpo/chat_template.py @@ -62,7 +62,7 @@ def transform_fn(sample, tokenizer=None): tokenize=False, ) chosen_strip_index = result["chosen"].find(chosen["content"]) - result["chosen"] = result["chosen"][chosen_strip_index:] + result["chosen"] = result["chosen"][chosen_strip_index:].rstrip() result["rejected"] = tokenizer.apply_chat_template( [rejected], @@ -71,7 +71,7 @@ def transform_fn(sample, tokenizer=None): tokenize=False, ) rejected_strip_index = result["rejected"].find(rejected["content"]) - result["rejected"] = result["rejected"][rejected_strip_index:] + result["rejected"] = result["rejected"][rejected_strip_index:].rstrip() return result diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 5ba5aed562..b8890d4f7a 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -212,26 +212,23 @@ def terminate_handler(_, __, model_weakref): elif cfg.deepspeed and is_deepspeed_zero3_enabled(): # Copied over from: https://github.com/huggingface/accelerate/blob/5ae611118057232f441055f7ef9ba0b0f2b8d533/docs/source/usage_guides/deepspeed.md#saving-and-loading trainer.accelerator.wait_for_everyone() - unwrapped_model = trainer.accelerator.unwrap_model(trainer.model_wrapped) + trainer.save_model(cfg.output_dir) # the trainer saved a model.safetensors file in the output directory, - # but it is a proxy model and should be deleted - if os.path.exists(os.path.join(cfg.output_dir, "model.safetensors")): + # but it is most likely a proxy model and if so, should be deleted + maybe_proxy = os.path.exists(os.path.join(cfg.output_dir, "model.safetensors")) + maybe_sharded = os.path.exists( + os.path.join(cfg.output_dir, "model.safetensors.index.json") + ) + + if maybe_proxy and maybe_sharded: LOG.info(f"Deleting {os.path.join(cfg.output_dir, 'model.safetensors')}") LOG.info("This is a proxy model and should be deleted") - os.remove(os.path.join(cfg.output_dir, "model.safetensors")) - - # Saves the whole/unpartitioned fp16 model when in ZeRO Stage-3 to the output directory if - # `stage3_gather_16bit_weights_on_model_save` is True in DeepSpeed Config file or - # `zero3_save_16bit_model` is True in DeepSpeed Plugin. - # For Zero Stages 1 and 2, models are saved as usual in the output directory. - # The model name saved is `pytorch_model.bin` - unwrapped_model.save_pretrained( - cfg.output_dir, - is_main_process=trainer.accelerator.is_main_process, - save_function=trainer.accelerator.save, - state_dict=trainer.accelerator.get_state_dict(trainer.model_wrapped), - ) + try: + os.remove(os.path.join(cfg.output_dir, "model.safetensors")) + except FileNotFoundError: + pass + elif cfg.local_rank == 0: if cfg.flash_optimum and BetterTransformer: model = BetterTransformer.reverse(model) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index bbea1987f1..2e923057db 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -42,7 +42,7 @@ from axolotl.utils.data.pretraining import wrap_pretraining_dataset from axolotl.utils.data.utils import md5 from axolotl.utils.dict import DictDefault -from axolotl.utils.distributed import is_main_process, zero_first +from axolotl.utils.distributed import is_local_main_process, zero_first from axolotl.utils.trainer import ( calculate_total_num_steps, process_datasets_for_packing, @@ -54,7 +54,7 @@ def prepare_dataset(cfg, tokenizer): prompters = [] if not cfg.pretraining_dataset: - with zero_first(is_main_process()): + with zero_first(is_local_main_process()): if cfg.test_datasets: train_dataset, _, prompters = load_prepare_datasets( tokenizer, cfg, DEFAULT_DATASET_PREPARED_PATH, split="train" @@ -170,6 +170,7 @@ def load_tokenized_prepared_datasets( # pylint: disable=duplicate-code if dataset: + # This is for the case where we already loaded a pretokenized dataset from the hub ... elif ( cfg.dataset_prepared_path @@ -198,6 +199,8 @@ def load_tokenized_prepared_datasets( def for_d_in_datasets(dataset_configs): for dataset in dataset_configs: if dataset.name and isinstance(dataset.name, list): + # load_dataset doesn't properly handle multiple named configurations + # at the same time for a given dataset for name in dataset.name: yield DictDefault({**dataset, "name": name}) else: @@ -208,6 +211,8 @@ def for_d_in_datasets(dataset_configs): ds: Optional[Union[Dataset, DatasetDict]] = None ds_from_hub = False try: + # this is just a basic check to see if the path is a + # valid HF dataset that's loadable load_dataset( config_dataset.path, name=config_dataset.name, diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index ecb1bcc9ec..4444a20c96 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -44,6 +44,10 @@ def is_main_process(): return dist.get_rank() == 0 +def is_local_main_process(): + return PartialState().is_main_process + + def get_world_size(): return int(os.getenv("WORLD_SIZE", "1")) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 436b31fefd..8a50631ef8 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -29,6 +29,7 @@ AutoConfig, AutoModelForCausalLM, AutoTokenizer, + AwqConfig, BitsAndBytesConfig, GPTQConfig, PreTrainedModel, @@ -36,6 +37,7 @@ ) from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled +from axolotl.common.architectures import MOE_ARCH_BLOCK from axolotl.models.mamba import fix_mamba_attn_for_loss from axolotl.monkeypatch.multipack import ( SUPPORTED_MULTIPACK_MODEL_TYPES, @@ -510,7 +512,25 @@ def load_model( model_kwargs["quantization_config"] = GPTQConfig( **model_config.quantization_config ) - if cfg.adapter == "qlora" and cfg.load_in_4bit: + if ( + cfg.adapter in ["qlora", "lora"] + and hasattr(model_config, "quantization_config") + and model_config.quantization_config["quant_method"] + in ["gptq", "awq", "bitsandbytes"] + ): + if model_config.quantization_config["quant_method"] == "gptq": + model_kwargs["quantization_config"] = GPTQConfig( + **model_config.quantization_config + ) + elif model_config.quantization_config["quant_method"] == "awq": + model_kwargs["quantization_config"] = AwqConfig( + **model_config.quantization_config + ) + elif model_config.quantization_config["quant_method"] == "bitsandbytes": + model_kwargs["quantization_config"] = BitsAndBytesConfig( + **model_config.quantization_config + ) + elif cfg.adapter == "qlora" and cfg.load_in_4bit: bnb_config = { "load_in_4bit": True, "llm_int8_threshold": 6.0, @@ -785,12 +805,14 @@ def load_model( set_z3_leaf_modules, ) - if cfg.model_config_type == "mixtral": - moe_block = get_module_class_from_name(model, "MixtralSparseMoeBlock") - set_z3_leaf_modules(model, [moe_block]) - elif cfg.model_config_type == "dbrx": - moe_block = get_module_class_from_name(model, "DbrxFFN") - set_z3_leaf_modules(model, [moe_block]) + if cfg.model_config_type in MOE_ARCH_BLOCK: + set_z3_leaf_modules( + model, + [ + get_module_class_from_name(model, module_name) + for module_name in MOE_ARCH_BLOCK[cfg.model_config_type] + ], + ) if cfg.model_config_type == "qwen" and cfg.adapter == "lora": # Qwen doesn't play nicely with LoRA if this is enabled @@ -804,6 +826,9 @@ def load_model( # make sure everything is in the same dtype skip_prepare_model_for_kbit_training = True + if is_deepspeed_zero3_enabled(): + skip_prepare_model_for_kbit_training = True + if cfg.adapter in ["lora", "qlora"]: if cfg.gradient_checkpointing: model.gradient_checkpointing_enable( @@ -838,6 +863,9 @@ def load_model( else: model, lora_config = load_adapter(model, cfg, cfg.adapter) + if is_deepspeed_zero3_enabled(): + skip_move_to_device = True + if ( cfg.ddp and not load_in_8bit diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index c5a71e689a..bb96240514 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -1,4 +1,5 @@ """Module containing the Trainer class and related functions""" +import json import math import os import random @@ -389,6 +390,19 @@ def calc_sample_packing_eff_est(estimates: List[float]): return total_num_steps +def setup_deepspeed_env(cfg, stage=None): + os.environ["ACCELERATE_USE_DEEPSPEED"] = "true" + os.environ["ACCELERATE_DEEPSPEED_CONFIG_FILE"] = cfg.deepspeed + if cfg.bf16: + os.environ["ACCELERATE_MIXED_PRECISION"] = "bf16" + elif cfg.fp16: + os.environ["ACCELERATE_MIXED_PRECISION"] = "fp16" + if stage: + os.environ["ACCELERATE_DEEPSPEED_ZERO_STAGE"] = str(stage) + if stage == 3: + os.environ["ACCELERATE_DEEPSPEED_ZERO3_INIT"] = "true" + + def setup_fsdp_envs(cfg): os.environ["ACCELERATE_USE_FSDP"] = "true" if cfg.fsdp_config.fsdp_activation_checkpointing: @@ -415,8 +429,14 @@ def prepare_optim_env(cfg): if cfg.fsdp: setup_fsdp_envs(cfg) elif cfg.deepspeed: - os.environ["ACCELERATE_USE_DEEPSPEED"] = "true" - os.environ["ACCELERATE_DEEPSPEED_CONFIG_FILE"] = cfg.deepspeed + stage = None + # check if the cfg.deepspeed is a file + if os.path.isfile(cfg.deepspeed): + # parse with json + with open(cfg.deepspeed, "r", encoding="utf-8") as fin: + deepspeed_config = json.load(fin) + stage = deepspeed_config.get("zero_optimization", {}).get("stage", None) + setup_deepspeed_env(cfg, stage=stage) if (cfg.bf16 == "auto" and is_torch_bf16_gpu_available()) or cfg.bf16 is True: os.environ["ACCELERATE_MIXED_PRECISION"] = "bf16" From 55cc214c767741e83ee7b346e5e13e6c03b7b9fa Mon Sep 17 00:00:00 2001 From: Adam Brusselback Date: Sun, 28 Jul 2024 21:48:57 -0400 Subject: [PATCH 0090/1405] Add flexible configuration options for `chat_template` dataset training (#1756) * Add flexible configuration options for chat dataset training - Introduce roles_to_train parameter to set training labels by role - Add train_on_eos option to configure training on end-of-sequence tokens - Implement per-message training configuration in dataset - Allow fine-grained control over training specific portions of messages - Add message_field_training and message_field_training_detail settings - Implement mapping between dataset character offsets and tokenized prompt - Enhance test suite to cover new functionality * Fix missing field inits, things weren't working from yaml. * Add flexible configuration options for chat dataset training - Introduce roles_to_train parameter to set training labels by role - Add train_on_eos option to configure training on end-of-sequence tokens - Implement per-message training configuration in dataset - Allow fine-grained control over training specific portions of messages - Add message_field_training and message_field_training_detail settings - Implement mapping between dataset character offsets and tokenized prompt - Enhance test suite to cover new functionality * Fix missing field inits, things weren't working from yaml. * chore: lint * Revert test repo back to NousResearch after opening PR to fix the tokenizer_config.json. --------- Co-authored-by: Wing Lian --- .../prompt_strategies/chat_template.py | 313 ++++++- .../config/models/input/v0_4_1/__init__.py | 4 + .../prompt_strategies/test_chat_templates.py | 884 +++++++++++++++++- tests/prompt_strategies/test_sharegpt.py | 2 + 4 files changed, 1111 insertions(+), 92 deletions(-) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 8c7a8dd4f6..f9fa71f217 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -6,14 +6,16 @@ from typing import Any, Dict, List, Optional from axolotl.prompt_tokenizers import PromptTokenizingStrategy -from axolotl.prompters import Prompter +from axolotl.prompters import IGNORE_TOKEN_ID, Prompter from axolotl.utils.chat_templates import chat_templates +# Configure the logger +logging.basicConfig(level=logging.DEBUG) LOG = logging.getLogger("axolotl") class ChatTemplatePrompter(Prompter): - """prompter for HF chat templates""" + """Prompter for HF chat templates""" def __init__( self, @@ -22,6 +24,8 @@ def __init__( max_length=2048, message_field_role: str = "from", message_field_content: str = "value", + message_field_training: str = "train", + message_field_training_detail: str = "train_detail", roles: Optional[Dict[str, List[str]]] = None, drop_system_message: bool = False, ): @@ -37,6 +41,8 @@ def __init__( } self.message_field_role = message_field_role self.message_field_content = message_field_content + self.message_field_training = message_field_training + self.message_field_training_detail = message_field_training_detail self.tokenizer = tokenizer self.chat_template = chat_template self.max_length = max_length @@ -47,6 +53,7 @@ def build_prompt(self, conversation, add_generation_prompt=False): { "role": self.roles[t[self.message_field_role]], "content": t[self.message_field_content], + "training": t.get(self.message_field_training, None), } for t in conversation ] @@ -62,6 +69,108 @@ def build_prompt(self, conversation, add_generation_prompt=False): chat_template=self.chat_template, ) + def get_offsets_for_train_detail( + self, text: str, train_details: List[Dict], mask_untrainable: bool = True + ) -> List[int]: + tokenized_output = self.tokenizer( + text, return_offsets_mapping=True, add_special_tokens=False + ) + tokens = tokenized_output.tokens() + token_offsets = tokenized_output["offset_mapping"] + + LOG.debug(f"Tokenizing text: {text}") + LOG.debug(f"Tokens: {tokens}") + # Adjust the end offsets. For some reason by default they are set to the same value as the start offsets. + for i in range(len(token_offsets) - 1): + token_offsets[i] = (token_offsets[i][0], token_offsets[i + 1][0] - 1) + # Ensure the last token's end offset is set correctly + token_offsets[-1] = (token_offsets[-1][0], len(text) - 1) + LOG.debug(f"Token offsets: {token_offsets}") + + # Initialize all offsets as IGNORE_TOKEN_ID (not trained) + result = [IGNORE_TOKEN_ID] * len(token_offsets) + + # Adjust train_details to align with token boundaries + adjusted_train_details = self.adjust_train_details(train_details, token_offsets) + + for idx, (start, end) in enumerate(token_offsets): + for detail in adjusted_train_details: + # Check if the token is completely within the detail's range + if start >= detail["begin_offset"] and end <= detail["end_offset"]: + if detail["train"] or not mask_untrainable: + result[idx] = start + LOG.debug(f"Token {idx} ({tokens[idx]}) marked for training") + else: + LOG.debug( + f"Token {idx} ({tokens[idx]}) marked as non-trainable" + ) + elif start < detail["end_offset"] and end > detail["begin_offset"]: + # Token partially overlaps with detail, always mark as non-trainable + LOG.debug( + f"Token {idx} ({tokens[idx]}) partially overlaps detail, marked as non-trainable" + ) + + LOG.debug(f"Final result: {result}") + return result + + def adjust_train_details( + self, train_details: List[Dict], token_offsets: List[tuple] + ) -> List[Dict]: + adjusted_details = [] + for detail in train_details: + begin_offset = detail["begin_offset"] + end_offset = detail["end_offset"] + + # Find the first token that starts after or at the begin_offset + begin_token = next( + ( + i + for i, (t_start, t_end) in enumerate(token_offsets) + if t_start >= begin_offset + ), + len(token_offsets), + ) + if begin_token > 0 and token_offsets[begin_token - 1][1] > begin_offset: + begin_token -= 1 + + # Find the last token that ends before or at the end_offset + end_token = next( + ( + i + for i in range(len(token_offsets) - 1, -1, -1) + if token_offsets[i][1] <= end_offset + ), + -1, + ) + if ( + end_token < len(token_offsets) - 1 + and token_offsets[end_token + 1][0] < end_offset + ): + end_token += 1 + + if begin_token <= end_token: + adjusted_begin = token_offsets[begin_token][0] + adjusted_end = token_offsets[end_token][1] + + if adjusted_begin != begin_offset or adjusted_end != end_offset: + LOG.warning( + f"Adjusting detail offsets: ({begin_offset}, {end_offset}) -> ({adjusted_begin}, {adjusted_end})" + ) + + adjusted_details.append( + { + "begin_offset": adjusted_begin, + "end_offset": adjusted_end, + "train": detail["train"], + } + ) + else: + LOG.warning( + f"Could not adjust detail offsets: ({begin_offset}, {end_offset}). Skipping this detail." + ) + + return adjusted_details + class ChatTemplateStrategy(PromptTokenizingStrategy): """ @@ -70,6 +179,19 @@ class ChatTemplateStrategy(PromptTokenizingStrategy): _messages = "conversations" + def __init__( + self, + prompter, + tokenizer, + train_on_inputs, + sequence_len, + roles_to_train=None, + train_on_eos="last", + ): + super().__init__(prompter, tokenizer, train_on_inputs, sequence_len) + self.roles_to_train = roles_to_train if roles_to_train is not None else [] + self.train_on_eos = train_on_eos + @property def messages(self): return self._messages @@ -79,62 +201,169 @@ def messages(self, messages): self._messages = messages def tokenize_prompt(self, prompt): - turns = self.get_conversation_thread(prompt) - prompt_ids = self.prompter.build_prompt(turns[:-1], add_generation_prompt=True) + turns = prompt[self.messages] input_ids = self.prompter.build_prompt(turns) + labels = [IGNORE_TOKEN_ID] * len(input_ids) - if not self.train_on_inputs: - user_prompt_len = len(prompt_ids) - labels = [-100] * user_prompt_len + input_ids[user_prompt_len:] - else: - labels = input_ids + last_eos_idx = -1 + for index, turn in enumerate(turns): + role = turn.get(self.prompter.message_field_role) + content = turn.get(self.prompter.message_field_content) + train_turn = turn.get(self.prompter.message_field_training) + train_detail = turn.get(self.prompter.message_field_training_detail) + + LOG.debug( + f"Processing turn {index}: role={role}, content={content}, train_turn={train_turn}, train_detail={train_detail}" + ) + + should_train = ( + train_turn + if train_turn is not None + else bool(train_detail is not None) + if train_detail is not None + else self.train_on_inputs or role in self.roles_to_train + ) + + LOG.debug(f"Should train: {should_train}") + + turn_start_idx, turn_end_idx = self.find_turn( + conversation_ids=input_ids, turn=index, turn_content=turn + ) + + LOG.debug(f"Turn indices: start={turn_start_idx}, end={turn_end_idx}") - tokenized_prompt = { + if should_train and turn_start_idx != -1 and turn_end_idx != -1: + if train_detail: + token_offsets = self.prompter.get_offsets_for_train_detail( + content, train_detail + ) + LOG.debug(f"Token offsets: {token_offsets}") + for i, offset in enumerate(token_offsets): + if offset != IGNORE_TOKEN_ID and turn_start_idx + i < len( + input_ids + ): + labels[turn_start_idx + i] = input_ids[turn_start_idx + i] + LOG.debug( + f"Label set at index {turn_start_idx + i}: {input_ids[turn_start_idx + i]}" + ) + else: + labels[turn_start_idx:turn_end_idx] = input_ids[ + turn_start_idx:turn_end_idx + ] + LOG.debug(f"Labels set for range {turn_start_idx}:{turn_end_idx}") + + LOG.debug(f"Labels after processing turn {index}: {labels}") + + # Handle EOS token + eos_idx = self.find_eos_token(input_ids, turn_end_idx) + if eos_idx == turn_end_idx: + last_eos_idx = eos_idx + if self.train_on_eos == "all" or ( + self.train_on_eos == "turn" and should_train + ): + labels[eos_idx] = input_ids[eos_idx] + LOG.debug(f"EOS token set for training at index {eos_idx}") + else: + LOG.debug( + f"EOS token missing after turn {turn}. eos_idx: {eos_idx}, turn_end_idx: {turn_end_idx}" + ) + + # Handle 'last' option for train_on_eos + if self.train_on_eos == "last" and last_eos_idx != -1: + labels[last_eos_idx] = input_ids[last_eos_idx] + LOG.debug(f"Last EOS token set for training at index {last_eos_idx}") + + LOG.debug(f"Final labels: {labels}") + + return { "input_ids": input_ids, "labels": labels, "attention_mask": [1] * len(input_ids), } - return tokenized_prompt + def find_eos_token(self, input_ids, start_idx): + eos_token_id = self.tokenizer.eos_token_id + for i in range(start_idx, len(input_ids)): + if input_ids[i] == eos_token_id: + return i + return -1 + + def find_turn(self, conversation_ids, turn, turn_content): + """ + Locate the starting and ending indices of the specified turn in a conversation. + + Args: + conversation_ids (list[int]): Token IDs representing the conversation. + turn (int): The turn number to locate (based on EOS tokens). + turn_content (str): String containing the content of the turn. + + Returns: + tuple: (start_idx, end_idx) indices of the start and end of the turn content. + Returns (-1, -1) if the turn content is not found. + """ + content = turn_content.get(self.prompter.message_field_content, "") + content_ids = self.tokenizer.encode(content, add_special_tokens=False) + + eos_token_id = self.tokenizer.eos_token_id + eos_count = 0 + start_search_idx = 0 + + # Locate the starting index after the specified number of EOS tokens + for i, token_id in enumerate(conversation_ids): + if token_id == eos_token_id: + eos_count += 1 + if eos_count == turn: + start_search_idx = ( + i + 1 + ) # Start searching after the specified turn's EOS token + break + + # Find the start index of the content within the conversation + start_idx = -1 + for i in range(start_search_idx, len(conversation_ids) - len(content_ids) + 1): + if conversation_ids[i : i + len(content_ids)] == content_ids: + start_idx = i + break + + if start_idx != -1: + end_idx = start_idx + len(content_ids) + else: + end_idx = -1 + + return start_idx, end_idx def get_conversation_thread(self, prompt): return prompt[self.messages] def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): - chat_template = ( - ds_cfg["chat_template"] if ds_cfg and "chat_template" in ds_cfg else "chatml" - ) - message_field_role = ( - ds_cfg["message_field_role"] - if ds_cfg and "message_field_role" in ds_cfg - else "from" - ) - message_field_content = ( - ds_cfg["message_field_content"] - if ds_cfg and "message_field_content" in ds_cfg - else "value" - ) - roles = ds_cfg["roles"] if ds_cfg and "roles" in ds_cfg else None - drop_system_message = ( - ds_cfg["drop_system_message"] - if ds_cfg and "drop_system_message" in ds_cfg - else False - ) + ds_cfg = ds_cfg or {} - strategy = ChatTemplateStrategy( - ChatTemplatePrompter( - tokenizer, - chat_templates(chat_template), - message_field_role=message_field_role, - message_field_content=message_field_content, - roles=roles, - drop_system_message=drop_system_message, + prompter_params = { + "tokenizer": tokenizer, + "chat_template": chat_templates(ds_cfg.get("chat_template", "chatml")), + "message_field_role": ds_cfg.get("message_field_role", "from"), + "message_field_content": ds_cfg.get("message_field_content", "value"), + "message_field_training": ds_cfg.get("message_field_training", "training"), + "message_field_training_detail": ds_cfg.get( + "message_field_training_detail", "train_detail" ), - tokenizer, - cfg.train_on_inputs, - cfg.sequence_len, + "roles": ds_cfg.get("roles"), + "drop_system_message": ds_cfg.get("drop_system_message", False), + } + + strategy_params = { + "train_on_inputs": cfg.train_on_inputs, + "sequence_len": cfg.sequence_len, + "roles_to_train": ds_cfg.get("roles_to_train"), + "train_on_eos": ds_cfg.get("train_on_eos", "last"), + } + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(**prompter_params), tokenizer=tokenizer, **strategy_params ) - if ds_cfg and "field_messages" in ds_cfg and hasattr(strategy, "messages"): + + if "field_messages" in ds_cfg and hasattr(strategy, "messages"): strategy.messages = ds_cfg["field_messages"] + return strategy diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 7397c7c739..e92c794859 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -116,6 +116,10 @@ class SFTDataset(BaseModel): field_messages: Optional[str] = None message_field_role: Optional[str] = None message_field_content: Optional[str] = None + message_field_training: Optional[str] = None + message_field_training_detail: Optional[str] = None + roles_to_train: Optional[List[str]] = None + train_on_eos: Optional[str] = None roles: Optional[Dict[str, List[str]]] = None drop_system_message: Optional[bool] = None diff --git a/tests/prompt_strategies/test_chat_templates.py b/tests/prompt_strategies/test_chat_templates.py index 7b58a12369..e2fc0f6a52 100644 --- a/tests/prompt_strategies/test_chat_templates.py +++ b/tests/prompt_strategies/test_chat_templates.py @@ -2,6 +2,7 @@ tests for chat_template prompt strategy """ +import logging import unittest import pytest @@ -13,33 +14,24 @@ ChatTemplateStrategy, load, ) +from axolotl.prompters import IGNORE_TOKEN_ID from axolotl.utils.chat_templates import chat_templates from axolotl.utils.dict import DictDefault +logging.basicConfig(level=logging.DEBUG) +LOG = logging.getLogger("axolotl") + @pytest.fixture(name="assistant_dataset") def fixture_assistant_dataset(): - # pylint: disable=duplicate-code return Dataset.from_list( [ { "messages": [ - { - "role": "user", - "content": "hello", - }, - { - "role": "assistant", - "content": "hello", - }, - { - "role": "user", - "content": "goodbye", - }, - { - "role": "assistant", - "content": "goodbye", - }, + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hello"}, + {"role": "user", "content": "goodbye"}, + {"role": "assistant", "content": "goodbye"}, ] } ] @@ -53,22 +45,28 @@ def fixture_sharegpt_dataset(): [ { "conversations": [ - { - "from": "human", - "value": "hello", - }, - { - "from": "gpt", - "value": "hello", - }, - { - "from": "human", - "value": "goodbye", - }, - { - "from": "gpt", - "value": "goodbye", - }, + {"from": "human", "value": "hello"}, + {"from": "gpt", "value": "hello"}, + {"from": "human", "value": "goodbye"}, + {"from": "gpt", "value": "goodbye"}, + ] + } + ] + ) + + +@pytest.fixture(name="basic_dataset") +def fixture_basic_dataset(): + # pylint: disable=duplicate-code + return Dataset.from_list( + [ + { + "conversations": [ + {"from": "system", "value": "You are an AI assistant."}, + {"from": "human", "value": "Hello"}, + {"from": "assistant", "value": "Hi there!"}, + {"from": "human", "value": "How are you?"}, + {"from": "assistant", "value": "I'm doing well, thank you!"}, ] } ] @@ -77,19 +75,611 @@ def fixture_sharegpt_dataset(): @pytest.fixture(name="llama3_tokenizer") def fixture_llama3_tokenizer(): - tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B") - tokenizer.eos_token = "<|eot_id|>" + tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B-Instruct") return tokenizer +class TestChatTemplateConfigurations: + """ + Test class for various configurations of ChatTemplateStrategy. + """ + + @staticmethod + def find_sublist(full_list, sub_list): + token_count = len(sub_list) + for index in range(len(full_list) - token_count + 1): + if full_list[index : index + token_count] == sub_list: + return index + return -1 + + def test_train_on_inputs_true(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with train_on_inputs=True") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=True, + sequence_len=512, + roles_to_train=["assistant"], + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Verify that assistant responses are labeled + assistant_responses = ["Hi there!", "I'm doing well, thank you!"] + for response in assistant_responses: + response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, response_ids) + LOG.debug( + f"Assistant response '{response}' expected IDs: {response_ids}, found at: {start_idx}" + ) + assert start_idx != -1, f"Could not find '{response}' in input_ids" + assert all( + label != IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(response_ids)] + ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" + + # Check the behavior of human inputs + human_inputs = ["Hello", "How are you?"] + for input_text in human_inputs: + input_ids = llama3_tokenizer.encode(input_text, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, input_ids) + labeled = all( + label != IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(input_ids)] + ) + LOG.debug( + f"Human input '{input_text}' is {'labeled' if labeled else 'not labeled'}, expected IDs: {input_ids}, found at: {start_idx}" + ) + + LOG.debug("Full labels: %s", labels) + LOG.debug("Full input_ids: %s", input_ids) + + def test_train_on_inputs_false(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with train_on_inputs=False") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Verify that only assistant responses are labeled + assistant_responses = ["Hi there!", "I'm doing well, thank you!"] + for response in assistant_responses: + response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, response_ids) + LOG.debug( + f"Assistant response '{response}' expected IDs: {response_ids}, found at: {start_idx}" + ) + assert start_idx != -1, f"Could not find '{response}' in input_ids" + assert all( + label != IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(response_ids)] + ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" + + # Verify that human inputs are not labeled + human_inputs = ["Hello", "How are you?"] + for input_text in human_inputs: + input_ids = llama3_tokenizer.encode(input_text, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, input_ids) + LOG.debug( + f"Human input '{input_text}' expected IDs: {input_ids}, found at: {start_idx}" + ) + assert start_idx != -1, f"Could not find '{input_text}' in input_ids" + assert all( + label == IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(input_ids)] + ), f"Expected labels for human input '{input_text}' to be IGNORE_TOKEN_ID, but got {labels[start_idx:start_idx+len(input_ids)]}" + + def test_roles_to_train_assistant_only(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing roles_to_train with assistant only") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Verify that only assistant responses are labeled + assistant_responses = ["Hi there!", "I'm doing well, thank you!"] + for response in assistant_responses: + response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, response_ids) + LOG.debug( + f"Assistant response '{response}' expected IDs: {response_ids}, found at: {start_idx}" + ) + assert all( + label != IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(response_ids)] + ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" + + def test_roles_to_train_all(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing roles_to_train with all roles") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=True, + sequence_len=512, + roles_to_train=["human", "assistant"], + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Verify that all responses are labeled (except for special tokens) + all_responses = [ + "Hello", + "Hi there!", + "How are you?", + "I'm doing well, thank you!", + ] + for response in all_responses: + response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, response_ids) + LOG.debug( + f"Response '{response}' expected IDs: {response_ids}, found at: {start_idx}" + ) + assert all( + label != IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(response_ids)] + ), f"Expected labels for response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" + + def test_empty_roles_to_train(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with empty roles_to_train") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=[], + train_on_eos="none", # Add this line + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + + # Verify that no labels are set when roles_to_train is empty + LOG.debug("Full labels: %s", labels) + assert all( + label == IGNORE_TOKEN_ID for label in labels + ), "Expected all labels to be IGNORE_TOKEN_ID when roles_to_train is empty" + + def test_train_on_eos_all(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with train_on_eos='all'") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + train_on_eos="all", + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + eos_token_id = llama3_tokenizer.eos_token_id + eos_indices = [ + i for i, token_id in enumerate(input_ids) if token_id == eos_token_id + ] + + assert len(eos_indices) > 0, "Expected at least one EOS token in the input" + for eos_idx in eos_indices: + assert ( + labels[eos_idx] != IGNORE_TOKEN_ID + ), f"Expected EOS token at index {eos_idx} to be labeled" + + def test_train_on_eos_turn(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with train_on_eos='turn'") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + train_on_eos="turn", + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + eos_token_id = llama3_tokenizer.eos_token_id + assistant_responses = ["Hi there!", "I'm doing well, thank you!"] + + for response in assistant_responses: + response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, response_ids) + assert start_idx != -1, f"Could not find '{response}' in input_ids" + + eos_idx = start_idx + len(response_ids) + while eos_idx < len(input_ids) and input_ids[eos_idx] != eos_token_id: + eos_idx += 1 + + assert eos_idx < len( + input_ids + ), f"Could not find EOS token after '{response}'" + assert ( + labels[eos_idx] != IGNORE_TOKEN_ID + ), f"Expected EOS token after assistant response '{response}' to be labeled" + + # Check that EOS tokens after human inputs are not labeled + human_inputs = ["Hello", "How are you?"] + for input_text in human_inputs: + input_ids = llama3_tokenizer.encode(input_text, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, input_ids) + assert start_idx != -1, f"Could not find '{input_text}' in input_ids" + + eos_idx = start_idx + len(input_ids) + while eos_idx < len(input_ids) and input_ids[eos_idx] != eos_token_id: + eos_idx += 1 + + assert ( + labels[eos_idx] == IGNORE_TOKEN_ID + ), f"Expected EOS token after human input '{input_text}' to not be labeled" + + def test_train_on_eos_last(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with train_on_eos='last'") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + train_on_eos="last", + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + eos_token_id = llama3_tokenizer.eos_token_id + eos_indices = [ + i for i, token_id in enumerate(input_ids) if token_id == eos_token_id + ] + + assert len(eos_indices) > 0, "Expected at least one EOS token in the input" + last_eos_idx = eos_indices[-1] + + # Check that only the last EOS token is labeled + for idx in eos_indices[:-1]: + assert ( + labels[idx] == IGNORE_TOKEN_ID + ), f"Expected EOS token at index {idx} to not be labeled" + assert ( + labels[last_eos_idx] != IGNORE_TOKEN_ID + ), f"Expected last EOS token at index {last_eos_idx} to be labeled" + + def test_train_on_eos_none(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with train_on_eos='none'") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + train_on_eos="none", + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + eos_token_id = llama3_tokenizer.eos_token_id + eos_indices = [ + i for i, token_id in enumerate(input_ids) if token_id == eos_token_id + ] + + assert len(eos_indices) > 0, "Expected at least one EOS token in the input" + for eos_idx in eos_indices: + assert ( + labels[eos_idx] == IGNORE_TOKEN_ID + ), f"Expected EOS token at index {eos_idx} to not be labeled" + + def test_drop_system_message(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with drop_system_message=True") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + llama3_tokenizer, chat_templates("llama3"), drop_system_message=True + ), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + input_ids = res["input_ids"] + + # Check if system message is not present in input_ids + system_message = "You are an AI assistant." + system_ids = llama3_tokenizer.encode(system_message, add_special_tokens=False) + assert ( + self.find_sublist(input_ids, system_ids) == -1 + ), "Expected system message to be dropped" + + def test_custom_roles(self, llama3_tokenizer): + LOG.info("Testing with custom roles mapping") + custom_roles = { + "user": ["human", "user"], + "assistant": ["ai", "assistant"], + "system": ["context"], + } + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + llama3_tokenizer, chat_templates("llama3"), roles=custom_roles + ), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["ai"], + ) + + # Create a new dataset with modified role names + modified_conversations = [ + {"from": "context", "value": "You are an AI assistant."}, + {"from": "human", "value": "Hello"}, + {"from": "ai", "value": "Hi there!"}, + {"from": "human", "value": "How are you?"}, + {"from": "ai", "value": "I'm doing well, thank you!"}, + ] + + modified_dataset = Dataset.from_dict( + {"conversations": [modified_conversations]} + ) + + res = strategy.tokenize_prompt(modified_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Check if AI responses are labeled correctly + ai_responses = ["Hi there!", "I'm doing well, thank you!"] + for response in ai_responses: + response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, response_ids) + assert start_idx != -1, f"Could not find response '{response}' in input_ids" + assert all( + label != IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(response_ids)] + ), f"Expected labels for AI response '{response}' to be set" + + # Check if human messages are not labeled + human_messages = ["Hello", "How are you?"] + for message in human_messages: + message_ids = llama3_tokenizer.encode(message, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, message_ids) + assert start_idx != -1, f"Could not find message '{message}' in input_ids" + assert all( + label == IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(message_ids)] + ), f"Expected labels for human message '{message}' to be IGNORE_TOKEN_ID" + + def test_message_field_training(self, llama3_tokenizer): + LOG.info("Testing with message_field_training") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + llama3_tokenizer, + chat_templates("llama3"), + message_field_training="train", + message_field_training_detail="train_detail", + ), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=[], + ) + + # Create a new dataset with the train and train_detail fields + modified_conversation = [ + {"from": "system", "value": "You are an AI assistant.", "train": False}, + {"from": "human", "value": "Hello", "train": False}, + {"from": "assistant", "value": "Hello", "train": True}, + {"from": "human", "value": "How are you?", "train": True}, + { + "from": "assistant", + "value": "I'm doing very well, thank you!", + "train_detail": [ + {"begin_offset": 0, "end_offset": 8, "train": False}, + {"begin_offset": 9, "end_offset": 18, "train": True}, + {"begin_offset": 19, "end_offset": 30, "train": False}, + ], + }, + { + "from": "human", + "value": "I'm doing very well, thank you!", + "train": False, + }, + {"from": "assistant", "value": "Hi there!", "train": True}, + ] + + modified_dataset = Dataset.from_dict({"conversations": [modified_conversation]}) + + res = strategy.tokenize_prompt(modified_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Function to find all occurrences of a sublist + def find_all_sublists(full_list, sub_list): + indices = [] + for index in range(len(full_list) - len(sub_list) + 1): + if full_list[index : index + len(sub_list)] == sub_list: + indices.append(index) + return indices + + # Keep track of which occurrences we've processed + processed_occurrences = {} + # Check if messages are labeled correctly based on train or train_detail + for i, turn in enumerate(modified_conversation): + turn_tokens = llama3_tokenizer.encode( + turn["value"], add_special_tokens=False + ) + occurrences = find_all_sublists(input_ids, turn_tokens) + turn_key = turn["value"] + if turn_key not in processed_occurrences: + processed_occurrences[turn_key] = 0 + current_occurrence = processed_occurrences[turn_key] + + if current_occurrence >= len(occurrences): + assert ( + False + ), f"Not enough occurrences found for message: {turn['value']}" + + start_idx = occurrences[current_occurrence] + processed_occurrences[turn_key] += 1 + end_idx = start_idx + len(turn_tokens) + + LOG.debug( + f"Processing turn {i}: role={turn['from']}, content='{turn['value']}', start_idx={start_idx}, end_idx={end_idx}" + ) + + if "train_detail" in turn: + # Get token offsets + tokenized_output = llama3_tokenizer( + turn["value"], return_offsets_mapping=True, add_special_tokens=False + ) + token_offsets = tokenized_output["offset_mapping"] + + # Adjust token offsets as done in the implementation + for i in range(len(token_offsets) - 1): + token_offsets[i] = ( + token_offsets[i][0], + token_offsets[i + 1][0] - 1, + ) + token_offsets[-1] = (token_offsets[-1][0], len(turn["value"]) - 1) + + # Adjust train_details + adjusted_train_details = strategy.prompter.adjust_train_details( + turn["train_detail"], token_offsets + ) + + LOG.debug(f"Original train_details: {turn['train_detail']}") + LOG.debug(f"Adjusted train_details: {adjusted_train_details}") + + # Handle train_detail + token_offsets = strategy.prompter.get_offsets_for_train_detail( + text=turn["value"], + train_details=adjusted_train_details, + mask_untrainable=False, + ) + token_offsets_masked = strategy.prompter.get_offsets_for_train_detail( + text=turn["value"], + train_details=adjusted_train_details, + mask_untrainable=True, + ) + LOG.debug(f"Token offsets: {token_offsets_masked}") + + expected_labels = [IGNORE_TOKEN_ID] * len(turn_tokens) + for i, offset in enumerate(token_offsets_masked): + if offset != IGNORE_TOKEN_ID: + expected_labels[i] = turn_tokens[i] + actual_labels = labels[ + start_idx : start_idx + len(token_offsets_masked) + ] + assert ( + actual_labels == expected_labels + ), f"Labels mismatch for turn: {turn['value']}\nExpected: {expected_labels}\nActual: {actual_labels}" + + for detail in adjusted_train_details: + # Find the token indices that correspond to the character offsets + detail_start = start_idx + next( + i + for i, offset in enumerate(token_offsets) + if offset >= detail["begin_offset"] + ) + detail_end = start_idx + next( + ( + i + for i, offset in enumerate(token_offsets) + if offset > detail["end_offset"] + ), + len(token_offsets), + ) + + detail_text = turn["value"][ + detail["begin_offset"] : detail["end_offset"] + 1 + ] + detail_labels = labels[detail_start:detail_end] + detail_input_ids = input_ids[detail_start:detail_end] + + LOG.debug( + f"Detail: '{detail_text}', Start: {detail_start}, End: {detail_end}" + ) + LOG.debug(f"Detail input_ids: {detail_input_ids}") + LOG.debug(f"Detail labels: {detail_labels}") + LOG.debug( + f"Decoded detail: {llama3_tokenizer.decode(detail_input_ids)}" + ) + LOG.debug( + f"Token offsets for this detail: {token_offsets[detail_start-start_idx:detail_end-start_idx]}" + ) + + if detail["train"]: + assert all( + label != IGNORE_TOKEN_ID for label in detail_labels + ), ( + f"Expected labels for trainable detail '{detail_text}' to be set, but some were IGNORE_TOKEN_ID. " + f"Labels({detail_start}:{detail_end}): {detail_labels}, " + f"InputIDs: {detail_input_ids}, " + f"Decoded: '{llama3_tokenizer.decode(detail_input_ids)}'" + ) + else: + assert all( + label == IGNORE_TOKEN_ID for label in detail_labels + ), ( + f"Expected all labels for non-trainable detail '{detail_text}' to be IGNORE_TOKEN_ID, but some were not. " + f"Labels({detail_start}:{detail_end}): {detail_labels}, " + f"InputIDs: {detail_input_ids}, " + f"Decoded: '{llama3_tokenizer.decode(detail_input_ids)}'" + ) + else: + should_train = turn.get("train", False) + turn_labels = labels[start_idx:end_idx] + + LOG.debug(f"Should train: {should_train}") + LOG.debug(f"Turn indices: start={start_idx}, end={end_idx}") + LOG.debug(f"Turn labels: {turn_labels}") + LOG.debug(f"Turn input IDs: {input_ids[start_idx:end_idx]}") + LOG.debug( + f"Decoded turn: {llama3_tokenizer.decode(input_ids[start_idx:end_idx])}" + ) + + if should_train: + assert all(label != IGNORE_TOKEN_ID for label in turn_labels), ( + f"Expected all labels for '{turn['value']}' to be set\n" + f"Labels({start_idx}:{end_idx}): {turn_labels}, " + f"InputIDs: {input_ids[start_idx:end_idx]}, " + f"Decoded: '{llama3_tokenizer.decode(input_ids[start_idx:end_idx])}'" + ) + else: + assert all(label == IGNORE_TOKEN_ID for label in turn_labels), ( + f"Expected all labels for '{turn['value']}' to be IGNORE_TOKEN_ID\n" + f"Labels({start_idx}:{end_idx}): {turn_labels}, " + f"InputIDs: {input_ids[start_idx:end_idx]}, " + f"Decoded: '{llama3_tokenizer.decode(input_ids[start_idx:end_idx])}'" + ) + + LOG.debug( + f"Processed turn: {turn['from']}, content: '{turn['value']}', " + f"start_idx: {start_idx}, end_idx: {end_idx}, " + f"labels: {labels[start_idx:end_idx]}" + ) + + LOG.debug(f"Final labels: {labels}") + LOG.debug(f"Final input_ids: {input_ids}") + + class TestAssistantChatTemplateLlama3: """ Test class for assistant style datasets with llama-3 prompts using the chat_template strategy. """ def test_llama3_load(self, llama3_tokenizer, assistant_dataset): - # pylint: disable=duplicate-code + LOG.info("Loading llama-3 tokenizer with assistant dataset") strategy = load( llama3_tokenizer, DictDefault( @@ -115,21 +705,26 @@ def test_llama3_load(self, llama3_tokenizer, assistant_dataset): res = strategy.tokenize_prompt(assistant_dataset[0]) input_ids = res["input_ids"] # fmt: off - assert input_ids == [ + expected_input_ids = [ 128000, # bos 128006, 882, 128007, # user header 271, 15339, 128009, # user prompt eot 128006, 78191, 128007, # assistant header - 271, 15339, 128009, # assistant response eot + 271, 15339, 128009, # assistant response eot 128006, 882, 128007, 271, 19045, 29474, 128009, 128006, 78191, 128007, 271, 19045, 29474, 128009, ] # fmt: on + LOG.debug(f"Expected input_ids: {expected_input_ids}") + LOG.debug(f"Actual input_ids: {input_ids}") + assert ( + input_ids == expected_input_ids + ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" def test_llama3(self, llama3_tokenizer, assistant_dataset): - # pylint: disable=duplicate-code + LOG.info("Testing llama-3 with assistant dataset") strategy = ChatTemplateStrategy( ChatTemplatePrompter( llama3_tokenizer, @@ -142,15 +737,16 @@ def test_llama3(self, llama3_tokenizer, assistant_dataset): "system": ["system"], }, ), - llama3_tokenizer, - False, - 512, + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], ) strategy.messages = "messages" res = strategy.tokenize_prompt(assistant_dataset[0]) input_ids = res["input_ids"] # fmt: off - assert input_ids == [ + expected_input_ids = [ 128000, # bos 128006, 882, 128007, # user header 271, 15339, 128009, # user prompt eot @@ -162,6 +758,64 @@ def test_llama3(self, llama3_tokenizer, assistant_dataset): 271, 19045, 29474, 128009, ] # fmt: on + LOG.debug(f"Expected input_ids: {expected_input_ids}") + LOG.debug(f"Actual input_ids: {input_ids}") + assert ( + input_ids == expected_input_ids + ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + + def test_llama3_with_training_data(self, llama3_tokenizer, assistant_dataset): + LOG.info("Testing llama-3 with assistant dataset including training data") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + llama3_tokenizer, + chat_templates("llama3"), + message_field_role="role", + message_field_content="content", + message_field_training="training", + roles={ + "user": ["user"], + "assistant": ["assistant"], + "system": ["system"], + }, + ), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + train_on_eos="none", + sequence_len=512, + roles_to_train=["assistant"], + ) + strategy.messages = "messages" + prompt_tokens = strategy.prompter.build_prompt( + assistant_dataset[0]["messages"], False + ) + prompt = llama3_tokenizer.decode(prompt_tokens, skip_special_tokens=False) + LOG.debug(f"Generated prompt: {prompt}") + res = strategy.tokenize_prompt(assistant_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + # fmt: off + expected_labels = [ + IGNORE_TOKEN_ID, # bos + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # user header + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # user prompt eot + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # assistant header + IGNORE_TOKEN_ID, 15339, IGNORE_TOKEN_ID, # assistant response eot + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, + IGNORE_TOKEN_ID, 19045, 29474, IGNORE_TOKEN_ID, + ] + # fmt: on + + LOG.debug(f"Expected labels: {expected_labels}") + LOG.debug(f"Actual labels: {labels}") + assert labels == expected_labels, ( + f"Labels mismatch:\n" + f"Expected: {expected_labels}\n" + f"Actual: {labels}\n" + f"Input IDs: {input_ids}\n" + ) class TestSharegptChatTemplateLlama3: @@ -169,30 +823,160 @@ class TestSharegptChatTemplateLlama3: Test class for ShareGPT style datasets with llama-3 prompts using the chat_template strategy. """ - def test_llama3(self, llama3_tokenizer, sharegpt_dataset): - # pylint: disable=duplicate-code + def test_llama3_assistant(self, llama3_tokenizer, sharegpt_dataset): + LOG.info("Testing ShareGPT style datasets with llama-3 assistant prompts") strategy = ChatTemplateStrategy( ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), - llama3_tokenizer, - False, - 512, + tokenizer=llama3_tokenizer, + train_on_inputs=False, + train_on_eos="none", + sequence_len=512, + roles_to_train=["gpt"], ) res = strategy.tokenize_prompt(sharegpt_dataset[0]) input_ids = res["input_ids"] + labels = res["labels"] # fmt: off - assert input_ids == [ + expected_input_ids = [ 128000, # bos 128006, 882, 128007, # user header 271, 15339, 128009, # user prompt eot 128006, 78191, 128007, # assistant header - 271, 15339, 128009, # assistant response eot + 271, 15339, 128009, # assistant response eot + 128006, 882, 128007, + 271, 19045, 29474, 128009, + 128006, 78191, 128007, + 271, 19045, 29474, 128009, + ] + expected_labels = [ + IGNORE_TOKEN_ID, # bos + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # user header + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # user prompt eot + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # assistant header + IGNORE_TOKEN_ID, 15339, IGNORE_TOKEN_ID, # assistant response eot + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, + IGNORE_TOKEN_ID, 19045, 29474, IGNORE_TOKEN_ID, + ] + # fmt: on + + LOG.debug(f"Expected input_ids: {expected_input_ids}") + LOG.debug(f"Actual input_ids: {input_ids}") + LOG.debug(f"Expected labels: {expected_labels}") + LOG.debug(f"Actual labels: {labels}") + + assert ( + input_ids == expected_input_ids + ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + assert ( + labels == expected_labels + ), f"Labels mismatch: {labels} != {expected_labels}" + + def test_llama3_human(self, llama3_tokenizer, sharegpt_dataset): + LOG.info("Testing ShareGPT style datasets with llama-3 human prompts") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + train_on_eos="none", + sequence_len=512, + roles_to_train=["human"], + ) + res = strategy.tokenize_prompt(sharegpt_dataset[0]) + input_ids = res["input_ids"] + labels = res["labels"] + # fmt: off + expected_input_ids = [ + 128000, # bos + 128006, 882, 128007, # user header + 271, 15339, 128009, # user prompt eot + 128006, 78191, 128007, # assistant header + 271, 15339, 128009, # assistant response eot 128006, 882, 128007, 271, 19045, 29474, 128009, 128006, 78191, 128007, 271, 19045, 29474, 128009, ] + expected_labels = [ + IGNORE_TOKEN_ID, # bos + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # user header + IGNORE_TOKEN_ID, 15339, IGNORE_TOKEN_ID, # user prompt eot + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # assistant header + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # assistant response eot + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, + IGNORE_TOKEN_ID, 19045, 29474, IGNORE_TOKEN_ID, + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, + ] # fmt: on + LOG.debug(f"Expected input_ids: {expected_input_ids}") + LOG.debug(f"Actual input_ids: {input_ids}") + LOG.debug(f"Expected labels: {expected_labels}") + LOG.debug(f"Actual labels: {labels}") + + assert ( + input_ids == expected_input_ids + ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + assert ( + labels == expected_labels + ), f"Labels mismatch: {labels} != {expected_labels}" + + def test_llama3_system_human(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing ShareGPT style datasets with llama-3 system/human prompts") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + train_on_eos="none", + sequence_len=512, + roles_to_train=["system", "human"], + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + input_ids = res["input_ids"] + labels = res["labels"] + # fmt: off + expected_input_ids = [ + 128000, # bos + 128006, 9125, 128007, + 271, 2675, 527, 459, 15592, 18328, 13, 128009, + 128006, 882, 128007, # user header + 271, 9906, 128009, # user prompt eot + 128006, 78191, 128007, # assistant header + 271, 13347, 1070, 0, 128009, # assistant response eot + 128006, 882, 128007, + 271, 4438, 527, 499, 30, 128009, + 128006, 78191, 128007, + 271, 40, 2846, 3815, 1664, 11, 9901, 499, 0, 128009, + ] + expected_labels = [ + IGNORE_TOKEN_ID, # bos + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # system header + IGNORE_TOKEN_ID, 2675, 527, 459, 15592, 18328, 13, IGNORE_TOKEN_ID, # system prompt eot + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # user header + IGNORE_TOKEN_ID, 9906, IGNORE_TOKEN_ID, # user prompt eot + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # assistant header + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # assistant response eot + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, + IGNORE_TOKEN_ID, 4438, 527, 499, 30, IGNORE_TOKEN_ID, + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, + ] + # fmt: on + + LOG.debug(f"Expected input_ids: {expected_input_ids}") + LOG.debug(f"Actual input_ids: {input_ids}") + LOG.debug(f"Expected labels: {expected_labels}") + LOG.debug(f"Actual labels: {labels}") + + assert ( + input_ids == expected_input_ids + ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + assert ( + labels == expected_labels + ), f"Labels mismatch: {labels} != {expected_labels}" + if __name__ == "__main__": unittest.main() diff --git a/tests/prompt_strategies/test_sharegpt.py b/tests/prompt_strategies/test_sharegpt.py index aba53cd5fd..e7a73a0de5 100644 --- a/tests/prompt_strategies/test_sharegpt.py +++ b/tests/prompt_strategies/test_sharegpt.py @@ -192,6 +192,7 @@ def test_tokenization(self, sharegpt_dataset, llama3_tokenizer): input_ids = dataset_wrapper[0]["input_ids"] # fmt: off + # pylint: disable=duplicate-code assert input_ids == [ 128000, # bos 128006, 9125, 128007, # system header @@ -228,6 +229,7 @@ def test_tokenization_with_weights( input_ids = dataset_wrapper[0]["input_ids"] # fmt: off + # pylint: disable=duplicate-code assert input_ids == [ 128000, # bos 128006, 9125, 128007, # system header From 3bc8e64557c00f1adecf4a98f6c39f7ca77d3927 Mon Sep 17 00:00:00 2001 From: mhenrichsen Date: Tue, 30 Jul 2024 07:59:53 +0200 Subject: [PATCH 0091/1405] Update README.md (#1792) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fd293bd040..a626635dc8 100644 --- a/README.md +++ b/README.md @@ -334,7 +334,7 @@ For further and fine-grained use cases, please refer to the official [dstack doc Axolotl supports a variety of dataset formats. It is recommended to use a JSONL. The schema of the JSONL depends upon the task and the prompt template you wish to use. Instead of a JSONL, you can also use a HuggingFace dataset with columns for each JSONL field. -See [these docs](https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/) for more information on how to use different dataset formats. +See [the documentation](https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/) for more information on how to use different dataset formats. ### Config From d8d1788ffc4dd1d58bd5813a83abf6f1f8fad60f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 30 Jul 2024 08:06:11 -0400 Subject: [PATCH 0092/1405] move to supporting mostly 12.1 w 2.3.1 and add new 12.4 with 2.4.0 (#1793) --- .github/workflows/base.yml | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index f8eaff2703..4019a5baf6 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -12,35 +12,20 @@ jobs: fail-fast: false matrix: include: - - cuda: "118" - cuda_version: 11.8.0 - python_version: "3.10" - pytorch: 2.1.2 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "121" - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.10" - pytorch: 2.1.2 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - - cuda: "121" - cuda_version: 12.1.0 - python_version: "3.11" - pytorch: 2.1.2 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - - cuda: "121" - cuda_version: 12.1.0 - python_version: "3.11" - pytorch: 2.2.2 + pytorch: 2.3.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "121" - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.11" - pytorch: 2.3.0 + pytorch: 2.3.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - - cuda: "121" - cuda_version: 12.1.0 + - cuda: "124" + cuda_version: 12.4.0 python_version: "3.11" - pytorch: 2.3.1 + pytorch: 2.4.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" steps: - name: Checkout From d4f6a6b1032b87061c760f558093a9168dfe77c7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 30 Jul 2024 08:34:37 -0400 Subject: [PATCH 0093/1405] fix dockerfile and base builder (#1795) [skip-ci] --- .github/workflows/base.yml | 4 ++++ docker/Dockerfile-base | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 4019a5baf6..3a0c143df5 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -14,16 +14,19 @@ jobs: include: - cuda: "121" cuda_version: 12.1.1 + cudnn_version: 8 python_version: "3.10" pytorch: 2.3.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "121" cuda_version: 12.1.1 + cudnn_version: 8 python_version: "3.11" pytorch: 2.3.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "124" cuda_version: 12.4.0 + cudnn_version: "" python_version: "3.11" pytorch: 2.4.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" @@ -52,6 +55,7 @@ jobs: labels: ${{ steps.metadata.outputs.labels }} build-args: | CUDA_VERSION=${{ matrix.cuda_version }} + CUDNN_VERSION=${{ matrix.cudnn_version }} CUDA=${{ matrix.cuda }} PYTHON_VERSION=${{ matrix.python_version }} PYTORCH_VERSION=${{ matrix.pytorch }} diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 1de5537dac..3f13bba30a 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -3,7 +3,7 @@ ARG CUDNN_VERSION="8" ARG UBUNTU_VERSION="22.04" ARG MAX_JOBS=4 -FROM nvidia/cuda:$CUDA_VERSION-cudnn$CUDNN_VERSION-devel-ubuntu$UBUNTU_VERSION as base-builder +FROM nvidia/cuda:$CUDA_VERSION-cudnn$CUDNN_VERSION-devel-ubuntu$UBUNTU_VERSION AS base-builder ENV PATH="/root/miniconda3/bin:${PATH}" From c5587b45accdc50e0be7b2ec9ed3a66879d68156 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 30 Jul 2024 08:50:23 -0400 Subject: [PATCH 0094/1405] use 12.4.1 instead of 12.4 [skip-ci] (#1796) --- .github/workflows/base.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 3a0c143df5..9101fc2bea 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -25,7 +25,7 @@ jobs: pytorch: 2.3.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "124" - cuda_version: 12.4.0 + cuda_version: 12.4.1 cudnn_version: "" python_version: "3.11" pytorch: 2.4.0 From 9a638845977e269ed878de7eb25a313f094718ea Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 30 Jul 2024 12:37:40 -0400 Subject: [PATCH 0095/1405] update test and main/nightly builds (#1797) * update test and main/nightly builds * don't install mamba-ssm on 2.4.0 since it has no wheels yet --- .github/workflows/main.yml | 49 +++++++++++++-------------------- .github/workflows/nightlies.yml | 43 +++++++++++------------------ .github/workflows/tests.yml | 24 ++++++++-------- cicd/Dockerfile.jinja | 4 +-- docker/Dockerfile | 4 +-- 5 files changed, 50 insertions(+), 74 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4969de75d2..263af9788b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,28 +13,22 @@ jobs: fail-fast: false matrix: include: - - cuda: 118 - cuda_version: 11.8.0 - python_version: "3.10" - pytorch: 2.1.2 - axolotl_extras: - axolotl_args: "--extra-index-url https://download.pytorch.org/whl/cu118" - cuda: 121 - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.10" - pytorch: 2.1.2 - axolotl_extras: - - cuda: 121 - cuda_version: 12.1.0 - python_version: "3.11" - pytorch: 2.2.2 - axolotl_extras: + pytorch: 2.3.1 + axolotl_extras: mamba-ssm - cuda: 121 - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.11" pytorch: 2.3.1 - axolotl_extras: + axolotl_extras: mamba-ssm is_latest: true + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.4.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -75,27 +69,22 @@ jobs: strategy: matrix: include: - - cuda: 118 - cuda_version: 11.8.0 - python_version: "3.10" - pytorch: 2.1.2 - axolotl_extras: - cuda: 121 - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.10" - pytorch: 2.1.2 - axolotl_extras: - - cuda: 121 - cuda_version: 12.1.0 - python_version: "3.11" - pytorch: 2.2.2 + pytorch: 2.3.1 axolotl_extras: - cuda: 121 - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.11" pytorch: 2.3.1 axolotl_extras: is_latest: true + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.4.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -134,7 +123,7 @@ jobs: matrix: include: - cuda: 121 - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.11" pytorch: 2.3.1 axolotl_extras: diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 770954b85d..1d95a0983f 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -12,28 +12,22 @@ jobs: fail-fast: false matrix: include: - - cuda: 118 - cuda_version: 11.8.0 - python_version: "3.10" - pytorch: 2.1.2 - axolotl_extras: - axolotl_args: "--extra-index-url https://download.pytorch.org/whl/cu118" - cuda: 121 - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.10" - pytorch: 2.1.2 - axolotl_extras: - - cuda: 121 - cuda_version: 12.1.0 - python_version: "3.11" - pytorch: 2.2.2 + pytorch: 2.3.1 axolotl_extras: - cuda: 121 - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.11" pytorch: 2.3.1 axolotl_extras: is_latest: true + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.4.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -75,27 +69,22 @@ jobs: strategy: matrix: include: - - cuda: 118 - cuda_version: 11.8.0 - python_version: "3.10" - pytorch: 2.1.2 - axolotl_extras: - cuda: 121 - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.10" - pytorch: 2.1.2 - axolotl_extras: - - cuda: 121 - cuda_version: 12.1.0 - python_version: "3.11" - pytorch: 2.2.2 + pytorch: 2.3.1 axolotl_extras: - cuda: 121 - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.11" pytorch: 2.3.1 axolotl_extras: is_latest: true + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.4.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1cee8cbcb2..384f9d70a3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,27 +72,24 @@ jobs: fail-fast: false matrix: include: - - cuda: 118 - cuda_version: 11.8.0 - python_version: "3.10" - pytorch: 2.1.2 - axolotl_args: "--extra-index-url https://download.pytorch.org/whl/cu118" - num_gpus: 1 - cuda: 121 - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.10" - pytorch: 2.1.2 + pytorch: 2.3.1 num_gpus: 1 + axolotl_extras: mamba-ssm - cuda: 121 - cuda_version: 12.1.0 + cuda_version: 12.1.1 python_version: "3.11" - pytorch: 2.2.2 + pytorch: 2.3.1 num_gpus: 1 - - cuda: 121 - cuda_version: 12.1.0 + axolotl_extras: mamba-ssm + - cuda: 124 + cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.3.1 + pytorch: 2.4.0 num_gpus: 1 + axolotl_extras: steps: - name: Checkout uses: actions/checkout@v4 @@ -109,6 +106,7 @@ jobs: echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV echo "PYTORCH_VERSION=${{ matrix.pytorch}}" >> $GITHUB_ENV echo "AXOLOTL_ARGS=${{ matrix.axolotl_args}}" >> $GITHUB_ENV + echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - name: Run tests job on Modal diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 263f4a6611..3a79883667 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -24,9 +24,9 @@ RUN git fetch origin +$GITHUB_REF && \ # If AXOLOTL_EXTRAS is set, append it in brackets RUN pip install causal_conv1d RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install -e .[deepspeed,flash-attn,mamba-ssm,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + pip install -e .[deepspeed,flash-attn,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - pip install -e .[deepspeed,flash-attn,mamba-ssm,optimizers] $AXOLOTL_ARGS; \ + pip install -e .[deepspeed,flash-attn,optimizers] $AXOLOTL_ARGS; \ fi # So we can test the Docker image diff --git a/docker/Dockerfile b/docker/Dockerfile index be58d03543..2b106f1ed8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,9 +22,9 @@ WORKDIR /workspace/axolotl # If AXOLOTL_EXTRAS is set, append it in brackets RUN pip install causal_conv1d RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install -e .[deepspeed,flash-attn,mamba-ssm,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + pip install -e .[deepspeed,flash-attn,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - pip install -e .[deepspeed,flash-attn,mamba-ssm,optimizers] $AXOLOTL_ARGS; \ + pip install -e .[deepspeed,flash-attn,optimizers] $AXOLOTL_ARGS; \ fi # So we can test the Docker image From dbf8fb549e25ad69557aaba96a8f107055e4c3bf Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 30 Jul 2024 13:36:19 -0400 Subject: [PATCH 0096/1405] publish axolotl images without extras in the tag name (#1798) --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 263af9788b..5a972f5f08 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -59,6 +59,7 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: | ${{ steps.metadata.outputs.tags }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} + ${{ steps.metadata.outputs.tags }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }} ${{ (matrix.is_latest) && format('{0}-latest', steps.metadata.outputs.tags) || '' }} labels: ${{ steps.metadata.outputs.labels }} From 3ebf22464b30390220d22a7b5fee04815cdbc0d9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 30 Jul 2024 19:21:38 -0400 Subject: [PATCH 0097/1405] qlora-fsdp ram efficient loading with hf trainer (#1791) * fix 405b with lower cpu ram requirements * make sure to use doouble quant and only skip output embeddings * set model attributes * more fixes for sharded fsdp loading * update the base model in example to use pre-quantized nf4-bf16 weights * upstream fixes for qlora+fsdp --- docker/Dockerfile-cloud | 1 - docker/Dockerfile-cloud-no-tmux | 1 - examples/llama-3/qlora-fsdp-405b.yaml | 7 ++++--- requirements.txt | 4 ++-- src/axolotl/cli/__init__.py | 4 +++- src/axolotl/core/trainer_builder.py | 4 +++- .../config/models/input/v0_4_1/__init__.py | 8 ++++++++ src/axolotl/utils/model_shard_quant.py | 18 ++++++++++++++++++ src/axolotl/utils/models.py | 9 ++++++++- src/axolotl/utils/trainer.py | 10 ++++++---- 10 files changed, 52 insertions(+), 14 deletions(-) diff --git a/docker/Dockerfile-cloud b/docker/Dockerfile-cloud index 69ce143bb2..c0bb266d28 100644 --- a/docker/Dockerfile-cloud +++ b/docker/Dockerfile-cloud @@ -3,7 +3,6 @@ FROM winglian/axolotl:$BASE_TAG ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" ENV HUGGINGFACE_HUB_CACHE="/workspace/data/huggingface-cache/hub" -ENV TRANSFORMERS_CACHE="/workspace/data/huggingface-cache/hub" ENV HF_HOME="/workspace/data/huggingface-cache/hub" ENV HF_HUB_ENABLE_HF_TRANSFER="1" diff --git a/docker/Dockerfile-cloud-no-tmux b/docker/Dockerfile-cloud-no-tmux index efeffef8e6..3e59d41191 100644 --- a/docker/Dockerfile-cloud-no-tmux +++ b/docker/Dockerfile-cloud-no-tmux @@ -3,7 +3,6 @@ FROM winglian/axolotl:$BASE_TAG ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" ENV HUGGINGFACE_HUB_CACHE="/workspace/data/huggingface-cache/hub" -ENV TRANSFORMERS_CACHE="/workspace/data/huggingface-cache/hub" ENV HF_HOME="/workspace/data/huggingface-cache/hub" ENV HF_HUB_ENABLE_HF_TRANSFER="1" diff --git a/examples/llama-3/qlora-fsdp-405b.yaml b/examples/llama-3/qlora-fsdp-405b.yaml index 385b7f91d6..6eeec01c9b 100644 --- a/examples/llama-3/qlora-fsdp-405b.yaml +++ b/examples/llama-3/qlora-fsdp-405b.yaml @@ -1,4 +1,4 @@ -base_model: meta-llama/Meta-Llama-3.1-405B +base_model: hugging-quants/Meta-Llama-3.1-405B-BNB-NF4-BF16 tokenizer_type: AutoTokenizer load_in_4bit: true @@ -10,10 +10,11 @@ datasets: dataset_prepared_path: last_run_prepared val_set_size: 0.0 output_dir: ./outputs/out/qlora-llama3_1-405b +save_safetensors: true adapter: qlora -sequence_len: 1024 +sequence_len: 2048 sample_packing: true pad_to_sequence_len: true @@ -25,7 +26,7 @@ lora_target_linear: true gradient_accumulation_steps: 4 micro_batch_size: 1 -num_epochs: 4 +num_epochs: 2 optimizer: adamw_torch lr_scheduler: cosine learning_rate: 0.00001 diff --git a/requirements.txt b/requirements.txt index 5825ee1903..d2ec9266c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.11.1 -transformers==4.43.3 +transformers @ git+https://github.com/huggingface/transformers.git@026a173a64372e9602a16523b8fae9de4b0ff428 tokenizers==0.19.1 -bitsandbytes==0.43.1 +bitsandbytes==0.43.3 accelerate==0.32.0 deepspeed==0.14.4 pydantic==2.6.3 diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 5966d59313..a05ee84e97 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -40,7 +40,7 @@ from axolotl.utils.mlflow_ import setup_mlflow_env_vars from axolotl.utils.models import load_tokenizer from axolotl.utils.tokenization import check_dataset_labels -from axolotl.utils.trainer import prepare_optim_env +from axolotl.utils.trainer import prepare_opinionated_env, prepare_optim_env from axolotl.utils.wandb_ import setup_wandb_env_vars project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) @@ -382,6 +382,8 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs): prepare_optim_env(cfg) + prepare_opinionated_env(cfg) + normalize_config(cfg) normalize_cfg_datasets(cfg) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index ff4804b104..cf2866d81d 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1243,7 +1243,9 @@ def build(self, total_num_steps): if self.cfg.fsdp: training_arguments_kwargs["fsdp"] = self.cfg.fsdp if self.cfg.fsdp_config: - training_arguments_kwargs["fsdp_config"] = dict(self.cfg.fsdp_config) + training_arguments_kwargs["fsdp_config"] = { + k.lstrip("fsdp_"): v for k, v in dict(self.cfg.fsdp_config).items() + } if self.cfg.adapter == "qlora": training_arguments_kwargs["qlora"] = True diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index e92c794859..3b9dbb1a1c 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -235,6 +235,12 @@ class LoraConfig(BaseModel): peft_use_rslora: Optional[bool] = None peft_layer_replication: Optional[List[Tuple[int, int]]] = None + qlora_sharded_model_loading: Optional[bool] = Field( + default=False, + metadata={ + "help": "load qlora model in sharded format for FSDP using answer.ai technique." + }, + ) lora_on_cpu: Optional[bool] = None gptq: Optional[bool] = None bnb_config_kwargs: Optional[Dict[str, Any]] = None @@ -939,6 +945,8 @@ def check_evals(cls, data): @model_validator(mode="before") @classmethod def check_eval_packing(cls, data): + # TODO also should check test_datasets and val_set_size as we can skip + # if there are no eval datasets/splits if ( data.get("sample_packing") and data.get("eval_table_size") diff --git a/src/axolotl/utils/model_shard_quant.py b/src/axolotl/utils/model_shard_quant.py index 65f23b9e0f..9ed7ae471d 100644 --- a/src/axolotl/utils/model_shard_quant.py +++ b/src/axolotl/utils/model_shard_quant.py @@ -13,6 +13,7 @@ from torch import Tensor, nn from tqdm import tqdm from transformers import AutoModelForCausalLM +from transformers.quantizers import AutoHfQuantizer from transformers.utils import SAFE_WEIGHTS_INDEX_NAME, SAFE_WEIGHTS_NAME, hub @@ -173,6 +174,7 @@ def load_sharded_model_quant( low_memory=True, verbose=False, loading_workers=2, + quantization_config=None, ): with init_empty_weights(): model = AutoModelForCausalLM.from_config( @@ -186,15 +188,26 @@ def load_sharded_model_quant( compute_dtype=compute_dtype, quant_type="nf4", quant_storage=quant_storage, + compress_statistics=True, # bnb_4bit_use_double_quant + skip_modules=[ + "lm_head", + "embed_out", + ], ) else: # this is the more common case with HF transformers + # TODO can we detect the model arch and dynamically set skip_modules model.model = _replace_linear( model.model, Linear4bit, compute_dtype=compute_dtype, quant_type="nf4", quant_storage=quant_storage, + compress_statistics=True, # bnb_4bit_use_double_quant + skip_modules=[ + "lm_head", + "embed_out", + ], ) model.is_loaded_in_4bit = True @@ -251,6 +264,11 @@ def load_and_quantize_parallel(name_param, model, **kwargs): quant_method=quant_method, ) + # these attributes are needed to inform transformers/peft of the quantization + model.is_quantized = True + model.quantization_method = "bitsandbytes" + model.hf_quantizer = AutoHfQuantizer.from_config(quantization_config) + if cfg.local_rank == 0 and verbose: print(f"Loaded model weights in {time.time()-start:.3f} seconds") # cleanup any extra memory usage from parallel loading diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 8a50631ef8..f65da71d44 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -624,14 +624,21 @@ def load_model( elif ( qlora_fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading - and cfg.model_config_type == "dbrx" + and (cfg.model_config_type == "dbrx" or cfg.qlora_sharded_model_loading) ): quant_storage = cfg.torch_dtype + quantization_config = hasattr( + model_config, "quantization_config" + ) and getattr(model_config, "quantization_config") + quantization_config = ( + quantization_config or model_kwargs["quantization_config"] + ) model = load_sharded_model_quant( base_model, model_config, cfg, quant_storage=quant_storage, + quantization_config=quantization_config, ) skip_move_to_device = True elif ( diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index bb96240514..7a9cf2fbbd 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -393,10 +393,6 @@ def calc_sample_packing_eff_est(estimates: List[float]): def setup_deepspeed_env(cfg, stage=None): os.environ["ACCELERATE_USE_DEEPSPEED"] = "true" os.environ["ACCELERATE_DEEPSPEED_CONFIG_FILE"] = cfg.deepspeed - if cfg.bf16: - os.environ["ACCELERATE_MIXED_PRECISION"] = "bf16" - elif cfg.fp16: - os.environ["ACCELERATE_MIXED_PRECISION"] = "fp16" if stage: os.environ["ACCELERATE_DEEPSPEED_ZERO_STAGE"] = str(stage) if stage == 3: @@ -444,6 +440,12 @@ def prepare_optim_env(cfg): os.environ["ACCELERATE_MIXED_PRECISION"] = "fp16" +def prepare_opinionated_env(cfg): + if cfg.qlora_sharded_model_loading: + # model loading is forked after the tokenizer + os.environ["TOKENIZERS_PARALLELISM"] = "false" + + def setup_trainer(cfg, train_dataset, eval_dataset, model, tokenizer, total_num_steps): if cfg.rl in ["dpo", "ipo", "orpo", "kto", "simpo"]: trainer_builder = HFRLTrainerBuilder(cfg, model[0], tokenizer) From 78b42a3fe13c49e317bc116b9999c30e070322cc Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 30 Jul 2024 20:58:17 -0400 Subject: [PATCH 0098/1405] fix roles to train defaults and make logging less verbose (#1801) --- src/axolotl/prompt_strategies/chat_template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index f9fa71f217..719d79505e 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -10,8 +10,8 @@ from axolotl.utils.chat_templates import chat_templates # Configure the logger -logging.basicConfig(level=logging.DEBUG) LOG = logging.getLogger("axolotl") +LOG.setLevel(logging.INFO) class ChatTemplatePrompter(Prompter): @@ -355,7 +355,7 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): strategy_params = { "train_on_inputs": cfg.train_on_inputs, "sequence_len": cfg.sequence_len, - "roles_to_train": ds_cfg.get("roles_to_train"), + "roles_to_train": ds_cfg.get("roles_to_train", ["gpt", "assistant"]), "train_on_eos": ds_cfg.get("train_on_eos", "last"), } From 203816f7b4de020c40708e4e61847b0716189380 Mon Sep 17 00:00:00 2001 From: Sri Kainkaryam Date: Sun, 4 Aug 2024 12:24:26 -0500 Subject: [PATCH 0099/1405] Fix colab example notebook (#1805) [skip ci] --- examples/colab-notebooks/colab-axolotl-example.ipynb | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 3fcc4d2a97..3a6981ee09 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -43,7 +43,6 @@ }, "outputs": [], "source": [ - "!pip install torch==\"2.1.2\"\n", "!pip install -e git+https://github.com/axolotl-ai-cloud/axolotl#egg=axolotl\n", "!pip install flash-attn==\"2.5.0\"\n", "!pip install deepspeed==\"0.13.1\"!pip install mlflow==\"2.13.0\"" From 7402eb9dcb1c3cc9a2dc8ba0de8cab68147ece3d Mon Sep 17 00:00:00 2001 From: ripes <44345856+chrislee973@users.noreply.github.com> Date: Mon, 5 Aug 2024 09:42:15 -0700 Subject: [PATCH 0100/1405] Fix setting correct repo id when pushing dataset to hub (#1657) * use the ds hash as the dataset's config_name * improve logging for loading/pushing ds to hub * fix missing f string --- src/axolotl/utils/data/sft.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 2e923057db..97061cc629 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -160,8 +160,12 @@ def load_tokenized_prepared_datasets( use_auth_token = cfg.hf_use_auth_token try: if cfg.push_dataset_to_hub: + LOG.info( + f"Attempting to load prepared dataset from Huggingface hub at {cfg.push_dataset_to_hub} (version {ds_hash})..." + ) dataset = load_dataset( - f"{cfg.push_dataset_to_hub}/{ds_hash}", + cfg.push_dataset_to_hub, + ds_hash, token=use_auth_token, ) dataset = dataset[split] @@ -181,6 +185,8 @@ def load_tokenized_prepared_datasets( dataset = load_from_disk(str(prepared_ds_path)) LOG.info("Prepared dataset loaded from disk...") else: + if cfg.push_dataset_to_hub: + LOG.info("Unable to find prepared dataset in Huggingface hub") LOG.info(f"Unable to find prepared dataset in {prepared_ds_path}") LOG.info("Loading raw datasets...") if not cfg.is_preprocess: @@ -433,10 +439,12 @@ def for_d_in_datasets(dataset_configs): dataset.save_to_disk(str(prepared_ds_path)) if cfg.push_dataset_to_hub: LOG.info( - f"Saving merged prepared dataset with push_to_hub... {cfg.push_dataset_to_hub}/{ds_hash}" + f"Pushing merged prepared dataset to Huggingface hub at {cfg.push_dataset_to_hub} (version {ds_hash})..." ) dataset.push_to_hub( - f"{cfg.push_dataset_to_hub}/{ds_hash}", private=True + cfg.push_dataset_to_hub, + ds_hash, + private=True, ) return dataset, prompters From cb023c70dbbbe611cba746375b4cb72b8a598b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaditya=20Ura=20=28looking=20for=20PhD=20Fall=E2=80=9924?= =?UTF-8?q?=29?= Date: Mon, 5 Aug 2024 22:13:20 +0530 Subject: [PATCH 0101/1405] Update instruct-lora-8b.yml (#1789) [skip ci] Config is giving an error if not using the end of the token as the `pad_to_sequence_len` is true. --- examples/llama-3/instruct-lora-8b.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml index 21d32604c5..4acad59999 100644 --- a/examples/llama-3/instruct-lora-8b.yml +++ b/examples/llama-3/instruct-lora-8b.yml @@ -74,3 +74,5 @@ deepspeed: weight_decay: 0.0 fsdp: fsdp_config: +special_tokens: + pad_token: <|end_of_text|> From b7665c26c879f8da787a7b1c030852742bdf515c Mon Sep 17 00:00:00 2001 From: Ben Feuer Date: Mon, 5 Aug 2024 12:44:26 -0400 Subject: [PATCH 0102/1405] Update conversation.qmd (#1788) [skip ci] --- docs/dataset-formats/conversation.qmd | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index f7d0cac826..28d13c987c 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -54,6 +54,14 @@ conversations where `from` is `prompter` `assistant` instead of default sharegpt {"conversations": [{"from": "...", "value": "..."}]} ``` +## sharegpt.load_ultrachat + +conversations where the turns field is 'messages', human is 'user' and gpt is 'assistant'. + +```{.json filename="data.jsonl"} +{"messages": [{"user": "...", "assistant": "..."}]} +``` + ## sharegpt_jokes creates a chat where bot is asked to tell a joke, then explain why the joke is funny From ecdda006deaf1e6b9dbeca091d5f684f83cb5631 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 5 Aug 2024 13:12:05 -0400 Subject: [PATCH 0103/1405] One cycle lr (#1803) * refactor one_cycle lr scheduler so it's reusable in more situations * fix validation for lr_scheduler * default to cosine anneal strategy * one cycle lr exepects cos --- .pre-commit-config.yaml | 2 + src/axolotl/core/trainer_builder.py | 74 ++++++++----------- .../config/models/input/v0_4_1/__init__.py | 2 +- 3 files changed, 35 insertions(+), 43 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6c5f205897..9f2ceac56e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,8 @@ repos: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace + - id: no-commit-to-branch + args: ['--branch', 'main'] - repo: https://github.com/psf/black rev: 23.3.0 hooks: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index cf2866d81d..4e8b369052 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -242,6 +242,12 @@ class AxolotlTrainingMixins: "help": "workaround to pass an alternate optimizer to the HF trainer" }, ) + alternate_lr_scheduler_type: Optional[str] = field( + default=None, + metadata={ + "help": "workaround to pass an alternate lr scheduler to the HF trainer" + }, + ) @dataclass @@ -318,7 +324,23 @@ def create_scheduler( # fmt: off if self.lr_scheduler is None: # type: ignore # pylint: disable=access-member-before-definition # fmt: on - if use_cosine_quadratic: + if self.args.alternate_lr_scheduler_type == "one_cycle": + num_warmup_steps = self.args.get_warmup_steps(num_training_steps) + pct_start = num_warmup_steps / num_training_steps + extra_lr_kwargs = {} + if "pct_start" not in self.args.lr_scheduler_kwargs: + extra_lr_kwargs["pct_start"] = pct_start + if "anneal_strategy" not in self.args.lr_scheduler_kwargs: + extra_lr_kwargs["anneal_strategy"] = "cos" + + self.lr_scheduler = OneCycleLR( + optimizer, + max_lr=self.args.learning_rate, + total_steps=num_training_steps, + **extra_lr_kwargs, + **self.args.lr_scheduler_kwargs, + ) + elif use_cosine_quadratic: if use_cosine_min_lr: LOG.warning("Both cosine quadratic warmup and min lr detected. Using quadratic warmup.") @@ -876,37 +898,6 @@ def compute_loss( return lm_loss -class OneCycleLRSchedulerTrainer(AxolotlTrainer): - """ - Trainer subclass that uses the OneCycleLR scheduler - """ - - tag_names = ["axolotl", "onecycle"] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.lr_scheduler = None - - def create_scheduler( - self, - num_training_steps: int, - optimizer: Optional[torch.optim.Optimizer] = None, - ): - optimizer = self.optimizer if optimizer is None else optimizer - num_warmup_steps = self.args.get_warmup_steps(num_training_steps) - pct_start = num_warmup_steps / num_training_steps - - self.lr_scheduler = OneCycleLR( - optimizer, - max_lr=self.args.learning_rate, - total_steps=num_training_steps, - pct_start=pct_start, - div_factor=6, - ) - - return self.lr_scheduler - - class ReLoRATrainer(AxolotlTrainer): """ Trainer subclass that uses the OneCycleLR scheduler @@ -1190,10 +1181,6 @@ def get_post_trainer_create_callbacks(self, trainer): return callbacks def _get_trainer_cls(self): - if self.cfg.lr_scheduler == "one_cycle" and ( - self.cfg.fsdp or self.cfg.adapter == "qlora" - ): - return OneCycleLRSchedulerTrainer if self.cfg.relora_steps: return ReLoRATrainer if self.cfg.model_config_type == "mamba": @@ -1443,12 +1430,15 @@ def build(self, total_num_steps): training_arguments_kwargs[ "loraplus_lr_embedding" ] = self.cfg.loraplus_lr_embedding - training_arguments_kwargs["lr_scheduler_type"] = ( - self.cfg.lr_scheduler - if self.cfg.lr_scheduler - and self.cfg.lr_scheduler not in ("one_cycle", "log_sweep") - else "cosine" - ) + if self.cfg.lr_scheduler in ["one_cycle", "log_sweep"]: + training_arguments_kwargs["lr_scheduler_type"] = "cosine" + training_arguments_kwargs[ + "alternate_lr_scheduler_type" + ] = self.cfg.lr_scheduler + else: + training_arguments_kwargs["lr_scheduler_type"] = ( + self.cfg.lr_scheduler if self.cfg.lr_scheduler else "cosine" + ) training_arguments_kwargs["lr_scheduler_kwargs"] = ( self.cfg.lr_scheduler_kwargs if self.cfg.lr_scheduler_kwargs else {} ) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 3b9dbb1a1c..4fb020bd51 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -378,7 +378,7 @@ class HyperparametersConfig(BaseModel): }, ) torchdistx_path: Optional[str] = None - lr_scheduler: Optional[SchedulerType] = "cosine" + lr_scheduler: Optional[Union[SchedulerType, Literal["one_cycle"]]] = "cosine" lr_scheduler_kwargs: Optional[Dict[str, Any]] = None lr_quadratic_warmup: Optional[bool] = None cosine_min_lr_ratio: Optional[float] = None From fbbeb4fee08820cc3a643cad295c9900bdb586de Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 Aug 2024 09:29:23 -0400 Subject: [PATCH 0104/1405] remove un-necessary zero-first guard as it's already only called in a parent fn (#1810) [skip ci] --- src/axolotl/utils/trainer.py | 148 +++++++++++++++++------------------ 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 7a9cf2fbbd..02234d8b70 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -16,7 +16,7 @@ from transformers.utils import is_torch_bf16_gpu_available from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder -from axolotl.utils.distributed import is_main_process, reduce_and_broadcast, zero_first +from axolotl.utils.distributed import reduce_and_broadcast from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths LOG = get_logger("axolotl") @@ -183,88 +183,88 @@ def process_datasets_for_packing(cfg, train_dataset, eval_dataset): sequence_len=cfg.sequence_len, min_sequence_len=cfg.min_sample_len or 2, ) - with zero_first(is_main_process()): - if cfg.is_preprocess: - min_input_len = np.min(get_dataset_lengths(train_dataset)) - LOG.debug(f"min_input_len: {min_input_len}", main_process_only=True) - max_input_len = np.max(get_dataset_lengths(train_dataset)) - LOG.debug(f"max_input_len: {max_input_len}", main_process_only=True) - - if cfg.model_config_type == "mamba": - LOG.info("dropping attention_mask column") - train_dataset = train_dataset.remove_columns("attention_mask") - if eval_dataset: - eval_dataset = eval_dataset.remove_columns("attention_mask") - if cfg.model_config_type == "falcon": - LOG.info("dropping token_type_ids column if it exists") - if "token_type_ids" in train_dataset.column_names: - train_dataset = train_dataset.remove_columns("token_type_ids") - if eval_dataset and "token_type_ids" in eval_dataset.column_names: - eval_dataset = eval_dataset.remove_columns("token_type_ids") + if cfg.is_preprocess: + min_input_len = np.min(get_dataset_lengths(train_dataset)) + LOG.debug(f"min_input_len: {min_input_len}", main_process_only=True) + max_input_len = np.max(get_dataset_lengths(train_dataset)) + LOG.debug(f"max_input_len: {max_input_len}", main_process_only=True) + + if cfg.model_config_type == "mamba": + LOG.info("dropping attention_mask column") + train_dataset = train_dataset.remove_columns("attention_mask") + if eval_dataset: + eval_dataset = eval_dataset.remove_columns("attention_mask") - train_dataset = train_dataset.filter( + if cfg.model_config_type == "falcon": + LOG.info("dropping token_type_ids column if it exists") + if "token_type_ids" in train_dataset.column_names: + train_dataset = train_dataset.remove_columns("token_type_ids") + if eval_dataset and "token_type_ids" in eval_dataset.column_names: + eval_dataset = eval_dataset.remove_columns("token_type_ids") + + train_dataset = train_dataset.filter( + drop_long, + num_proc=cfg.dataset_processes, + load_from_cache_file=not cfg.is_preprocess, + desc="Dropping Long Sequences", + ) + if eval_dataset: + eval_dataset = eval_dataset.filter( drop_long, num_proc=cfg.dataset_processes, load_from_cache_file=not cfg.is_preprocess, desc="Dropping Long Sequences", ) - if eval_dataset: - eval_dataset = eval_dataset.filter( - drop_long, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Dropping Long Sequences", - ) - if cfg.group_by_length: - train_dataset = train_dataset.map( - add_length, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Group By Length", - ) + if cfg.group_by_length: + train_dataset = train_dataset.map( + add_length, + num_proc=cfg.dataset_processes, + load_from_cache_file=not cfg.is_preprocess, + desc="Group By Length", + ) - if cfg.use_pose: - pose_kwargs = {} - if cfg.pose_num_chunks is not None: - pose_kwargs["chunks"] = cfg.pose_num_chunks - pose_fn = partial( - add_pose_position_ids, - max_context_len=cfg.pose_max_context_len, - split_on_token_ids=cfg.pose_split_on_token_ids, - **pose_kwargs, - ) - train_dataset = train_dataset.map( - pose_fn, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Add position_id column (PoSE)", - ) - train_dataset = train_dataset.sort("sequence_len") - if cfg.eval_sample_packing is not False: - if eval_dataset: - eval_dataset = eval_dataset.map( - pose_fn, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Add position_id column (PoSE)", - ) - elif cfg.sample_packing: - train_dataset = train_dataset.map( - add_position_ids, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Add position_id column (Sample Packing)", - ) - if cfg.eval_sample_packing is not False: - if eval_dataset: - eval_dataset = eval_dataset.map( - add_position_ids, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Add position_id column (Sample Packing)", - ) + if cfg.use_pose: + pose_kwargs = {} + if cfg.pose_num_chunks is not None: + pose_kwargs["chunks"] = cfg.pose_num_chunks + pose_fn = partial( + add_pose_position_ids, + max_context_len=cfg.pose_max_context_len, + split_on_token_ids=cfg.pose_split_on_token_ids, + **pose_kwargs, + ) + train_dataset = train_dataset.map( + pose_fn, + num_proc=cfg.dataset_processes, + load_from_cache_file=not cfg.is_preprocess, + desc="Add position_id column (PoSE)", + ) + train_dataset = train_dataset.sort("sequence_len") + if cfg.eval_sample_packing is not False: + if eval_dataset: + eval_dataset = eval_dataset.map( + pose_fn, + num_proc=cfg.dataset_processes, + load_from_cache_file=not cfg.is_preprocess, + desc="Add position_id column (PoSE)", + ) + elif cfg.sample_packing: + train_dataset = train_dataset.map( + add_position_ids, + num_proc=cfg.dataset_processes, + load_from_cache_file=not cfg.is_preprocess, + desc="Add position_id column (Sample Packing)", + ) + if cfg.eval_sample_packing is not False: + if eval_dataset: + eval_dataset = eval_dataset.map( + add_position_ids, + num_proc=cfg.dataset_processes, + load_from_cache_file=not cfg.is_preprocess, + desc="Add position_id column (Sample Packing)", + ) return train_dataset, eval_dataset From 35d5e59d7833be071c12e4689cf6da4d8bb618ab Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 Aug 2024 09:30:46 -0400 Subject: [PATCH 0105/1405] set z3 leaf for deepseek v2 (#1809) [skip ci] * set z3 leaf for deepseek v2 * add deepseek v2 chat template --- src/axolotl/common/architectures.py | 1 + src/axolotl/monkeypatch/multipack.py | 4 ++-- src/axolotl/utils/chat_templates.py | 1 + src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 + src/axolotl/utils/models.py | 6 +++++- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/axolotl/common/architectures.py b/src/axolotl/common/architectures.py index 7610b335a4..827a63c070 100644 --- a/src/axolotl/common/architectures.py +++ b/src/axolotl/common/architectures.py @@ -11,4 +11,5 @@ ], "mixtral": "MixtralSparseMoeBlock", "qwen2_moe": "Qwen2MoeSparseMoeBlock", + "deepseek_v2": "DeepseekV2MoE", } diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index a2ce0e64fd..9043520108 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -25,12 +25,12 @@ ] -def patch_for_multipack(model_type, model_name=None): +def patch_for_multipack(model_type, model_name=None, is_remote_code=False): if model_type == "gemmoe": patch_remote(model_name, ".configuration_gemmoe", ".modeling_gemmoe") elif model_type == "deepseek_v2": patch_remote(model_name, ".configuration_deepseek", ".modeling_deepseek") - elif hasattr(transformers, "modeling_flash_attention_utils"): + elif hasattr(transformers, "modeling_flash_attention_utils") and not is_remote_code: transformers.modeling_flash_attention_utils._get_unpad_data = ( # pylint: disable=protected-access get_unpad_data ) diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 725934cf56..ca4334d75a 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -26,6 +26,7 @@ def chat_templates(user_choice: str): "cohere": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", + "deepseek_v2": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %}", } if user_choice in templates: diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 4fb020bd51..b765263ba6 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -189,6 +189,7 @@ class ChatTemplate(str, Enum): cohere = "cohere" # pylint: disable=invalid-name llama3 = "llama3" # pylint: disable=invalid-name phi_3 = "phi_3" # pylint: disable=invalid-name + deepseek_v2 = "deepseek_v2" # pylint: disable=invalid-name class LoftQConfig(BaseModel): diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index f65da71d44..87f50d9a27 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -348,7 +348,11 @@ def load_model( and cfg.flash_attention and cfg.sample_packing ): - patch_for_multipack(cfg.model_config_type, model_name=cfg.base_model) + patch_for_multipack( + cfg.model_config_type, + model_name=cfg.base_model, + is_remote_code=cfg.trust_remote_code, + ) if cfg.is_llama_derived_model: from axolotl.monkeypatch.llama_attn_hijack_flash import ( From c56e0a79a564ec37ec892ad95565d025e0ad7b9c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 Aug 2024 10:31:50 -0400 Subject: [PATCH 0106/1405] logging improvements (#1808) [skip ci] * logging improvements * fix sort --- src/axolotl/utils/data/sft.py | 7 ++++++- src/axolotl/utils/distributed.py | 6 +++--- src/axolotl/utils/models.py | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 97061cc629..1b6df1cded 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -187,7 +187,12 @@ def load_tokenized_prepared_datasets( else: if cfg.push_dataset_to_hub: LOG.info("Unable to find prepared dataset in Huggingface hub") - LOG.info(f"Unable to find prepared dataset in {prepared_ds_path}") + if cfg.is_preprocess: + LOG.info( + f"Skipping prepared dataset in {prepared_ds_path} for pre-processing..." + ) + else: + LOG.info(f"Unable to find prepared dataset in {prepared_ds_path}") LOG.info("Loading raw datasets...") if not cfg.is_preprocess: LOG.warning( diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index 4444a20c96..3a559f5f51 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -153,11 +153,11 @@ def compute_and_broadcast(fn): # pylint: disable=invalid-name if is_main_process(): value_scalar = fn() value_tensor = torch.tensor( - value_scalar, device=torch.cuda.current_device() - ).float() + value_scalar, device=torch.cuda.current_device(), dtype=torch.float32 + ) else: value_tensor = torch.tensor( - 0.0, device=torch.cuda.current_device() + 0.0, device=torch.cuda.current_device(), dtype=torch.float32 ) # Placeholder tensor # Broadcast the tensor to all processes. diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 87f50d9a27..1e9819c56b 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -1022,7 +1022,7 @@ def load_lora(model, cfg, inference=False, config_only=False): if cfg.lora_target_linear: linear_names = find_all_linear_names(model) - LOG.info(f"found linear modules: {repr(linear_names)}") + LOG.info(f"found linear modules: {repr(sorted(linear_names))}") lora_target_modules = list(set(lora_target_modules + linear_names)) lora_config_kwargs = {} From 850f999a76f6c79d2a797e98de350fabc05bfb96 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 Aug 2024 10:32:05 -0400 Subject: [PATCH 0107/1405] update peft and transformers (#1811) --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index d2ec9266c2..fdcae107c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 -peft==0.11.1 -transformers @ git+https://github.com/huggingface/transformers.git@026a173a64372e9602a16523b8fae9de4b0ff428 +peft==0.12.0 +transformers==4.43.4 tokenizers==0.19.1 bitsandbytes==0.43.3 accelerate==0.32.0 From 70978467a088da3abf3fe45d92d90f6529f19ea9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 Aug 2024 15:25:54 -0400 Subject: [PATCH 0108/1405] skip no commit to main on ci (#1814) --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 384f9d70a3..74b4bcfbdb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,6 +26,8 @@ jobs: python-version: "3.10" cache: 'pip' # caching pip dependencies - uses: pre-commit/action@v3.0.0 + env: + SKIP: no-commit-to-branch pytest: name: PyTest From 5ee4b7325f3cd09aa4d0c90b5e383fb6c5b1581e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 9 Aug 2024 10:54:52 -0400 Subject: [PATCH 0109/1405] fix z3 leaf configuration when not using lists (#1817) [skip ci] --- src/axolotl/utils/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 1e9819c56b..5ac66260a7 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -817,11 +817,13 @@ def load_model( ) if cfg.model_config_type in MOE_ARCH_BLOCK: + moe_blocks = MOE_ARCH_BLOCK[cfg.model_config_type] + moe_blocks = [moe_blocks] if isinstance(moe_blocks, str) else moe_blocks set_z3_leaf_modules( model, [ get_module_class_from_name(model, module_name) - for module_name in MOE_ARCH_BLOCK[cfg.model_config_type] + for module_name in moe_blocks ], ) From 3e2b269d06ea6a4bb05b2194cdd50b67b5ffb55c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 9 Aug 2024 10:58:19 -0400 Subject: [PATCH 0110/1405] update tinyllama to use final instead of checkpoints (#1820) [skip ci] --- examples/tiny-llama/lora-mps.yml | 2 +- examples/tiny-llama/lora.yml | 5 ++--- examples/tiny-llama/qlora.yml | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/tiny-llama/lora-mps.yml b/examples/tiny-llama/lora-mps.yml index c08be82d3b..99e404e432 100644 --- a/examples/tiny-llama/lora-mps.yml +++ b/examples/tiny-llama/lora-mps.yml @@ -1,4 +1,4 @@ -base_model: TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T +base_model: TinyLlama/TinyLlama_v1.1 model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer diff --git a/examples/tiny-llama/lora.yml b/examples/tiny-llama/lora.yml index c5ff0437e8..7c7fb67060 100644 --- a/examples/tiny-llama/lora.yml +++ b/examples/tiny-llama/lora.yml @@ -1,6 +1,5 @@ -base_model: TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T -model_type: LlamaForCausalLM -tokenizer_type: LlamaTokenizer +base_model: TinyLlama/TinyLlama_v1.1 +tokenizer_type: AutoTokenizer load_in_8bit: true load_in_4bit: false diff --git a/examples/tiny-llama/qlora.yml b/examples/tiny-llama/qlora.yml index 384f3315c0..931fe03e04 100644 --- a/examples/tiny-llama/qlora.yml +++ b/examples/tiny-llama/qlora.yml @@ -1,4 +1,4 @@ -base_model: TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T +base_model: TinyLlama/TinyLlama_v1.1 model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer From 54392ac8a662f0746f4128e68d1088edb58a3711 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 9 Aug 2024 11:50:13 -0400 Subject: [PATCH 0111/1405] Attempt to run multigpu in PR CI for now to ensure it works (#1815) [skip ci] * Attempt to run multigpu in PR CI for now to ensure it works * fix yaml file * forgot to include multigpu tests * fix call to cicd.multigpu * dump dictdefault to dict for yaml conversion * use to_dict instead of casting * 16bit-lora w flash attention, 8bit lora seems problematic * add llama fsdp test * more tests * Add test for qlora + fsdp with prequant * limit accelerate to 2 processes and disable broken qlora+fsdp+bnb test * move multigpu tests to biweekly --- .github/workflows/multi-gpu-e2e.yml | 44 ++++ cicd/cicd.sh | 2 +- cicd/multigpu.py | 77 +++++++ cicd/multigpu.sh | 5 + cicd/tests.py | 8 +- tests/e2e/multigpu/__init__.py | 0 tests/e2e/multigpu/test_llama.py | 341 ++++++++++++++++++++++++++++ 7 files changed, 473 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/multi-gpu-e2e.yml create mode 100644 cicd/multigpu.py create mode 100755 cicd/multigpu.sh create mode 100644 tests/e2e/multigpu/__init__.py create mode 100644 tests/e2e/multigpu/test_llama.py diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml new file mode 100644 index 0000000000..c854af9abe --- /dev/null +++ b/.github/workflows/multi-gpu-e2e.yml @@ -0,0 +1,44 @@ +name: docker-multigpu-tests-biweekly + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 1,4' # Runs at 00:00 UTC every monday & thursday + +jobs: + test-axolotl-multigpu: + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} + strategy: + fail-fast: false + matrix: + include: + - cuda: 121 + cuda_version: 12.1.1 + python_version: "3.11" + pytorch: 2.3.1 + axolotl_extras: + num_gpus: 2 + runs-on: [self-hosted, modal] + timeout-minutes: 120 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install Modal + run: | + python -m pip install --upgrade pip + pip install modal==0.63.64 jinja2 + - name: Update env vars + run: | + echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV + echo "PYTORCH_VERSION=${{ matrix.pytorch}}" >> $GITHUB_ENV + echo "AXOLOTL_ARGS=${{ matrix.axolotl_args}}" >> $GITHUB_ENV + echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV + echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV + echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV + - name: Run tests job on Modal + run: | + modal run cicd.multigpu diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 180150ea25..eceda9b375 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -3,4 +3,4 @@ set -e pytest --ignore=tests/e2e/ /workspace/axolotl/tests/ pytest -n1 --dist loadfile -v /workspace/axolotl/tests/e2e/patched/ -pytest --ignore=tests/e2e/patched/ /workspace/axolotl/tests/e2e/ +pytest --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ /workspace/axolotl/tests/e2e/ diff --git a/cicd/multigpu.py b/cicd/multigpu.py new file mode 100644 index 0000000000..be10fbc73a --- /dev/null +++ b/cicd/multigpu.py @@ -0,0 +1,77 @@ +""" + modal application to run axolotl gpu tests in Modal + """ +# pylint: disable=duplicate-code + +import os +import pathlib +import tempfile + +import jinja2 +import modal +from jinja2 import select_autoescape +from modal import Image, Stub + +cicd_path = pathlib.Path(__file__).parent.resolve() + +template_loader = jinja2.FileSystemLoader(searchpath=cicd_path) +template_env = jinja2.Environment( + loader=template_loader, autoescape=select_autoescape() +) +df_template = template_env.get_template("Dockerfile.jinja") + +df_args = { + "AXOLOTL_EXTRAS": os.environ.get("AXOLOTL_EXTRAS", ""), + "AXOLOTL_ARGS": os.environ.get("AXOLOTL_ARGS", ""), + "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.3.1"), + "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu121-2.3.1"), + "CUDA": os.environ.get("CUDA", "121"), + "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), + "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), +} + +dockerfile_contents = df_template.render(**df_args) + +temp_dir = tempfile.mkdtemp() +with open(pathlib.Path(temp_dir) / "Dockerfile", "w", encoding="utf-8") as f: + f.write(dockerfile_contents) + +cicd_image = ( + Image.from_dockerfile( + pathlib.Path(temp_dir) / "Dockerfile", + force_build=True, + gpu="A10G", + ) + .env(df_args) + .pip_install("fastapi==0.110.0", "pydantic==2.6.3") +) + +stub = Stub("Axolotl CI/CD", secrets=[]) + + +N_GPUS = int(os.environ.get("N_GPUS", 2)) +GPU_CONFIG = modal.gpu.H100(count=N_GPUS) + + +def run_cmd(cmd: str, run_folder: str): + import subprocess # nosec + + # Propagate errors from subprocess. + if exit_code := subprocess.call(cmd.split(), cwd=run_folder): # nosec + exit(exit_code) # pylint: disable=consider-using-sys-exit + + +@stub.function( + image=cicd_image, + gpu=GPU_CONFIG, + timeout=45 * 60, + cpu=8.0, + memory=131072 * N_GPUS, +) +def cicd_pytest(): + run_cmd("./cicd/multigpu.sh", "/workspace/axolotl") + + +@stub.local_entrypoint() +def main(): + cicd_pytest.remote() diff --git a/cicd/multigpu.sh b/cicd/multigpu.sh new file mode 100755 index 0000000000..ff7f9f19a5 --- /dev/null +++ b/cicd/multigpu.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -e + +# only run one test at a time so as not to OOM the GPU +pytest -n1 /workspace/axolotl/tests/e2e/multigpu/ diff --git a/cicd/tests.py b/cicd/tests.py index bfbdb7b90a..c214676378 100644 --- a/cicd/tests.py +++ b/cicd/tests.py @@ -1,6 +1,8 @@ """ modal application to run axolotl gpu tests in Modal """ +# pylint: disable=duplicate-code + import os import pathlib import tempfile @@ -21,9 +23,9 @@ df_args = { "AXOLOTL_EXTRAS": os.environ.get("AXOLOTL_EXTRAS", ""), "AXOLOTL_ARGS": os.environ.get("AXOLOTL_ARGS", ""), - "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.0.1"), - "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.10-cu118-2.0.1"), - "CUDA": os.environ.get("CUDA", "118"), + "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.3.1"), + "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu121-2.3.1"), + "CUDA": os.environ.get("CUDA", "121"), "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), } diff --git a/tests/e2e/multigpu/__init__.py b/tests/e2e/multigpu/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py new file mode 100644 index 0000000000..344c57fb85 --- /dev/null +++ b/tests/e2e/multigpu/test_llama.py @@ -0,0 +1,341 @@ +""" +E2E tests for multigpu lora tinyllama +""" + +import logging +import os +import unittest +from pathlib import Path + +import pytest +import yaml +from accelerate.test_utils import execute_subprocess_async + +from axolotl.utils.dict import DictDefault + +from ..utils import with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e.multigpu") +os.environ["WANDB_DISABLED"] = "true" + + +class TestMultiGPULlama(unittest.TestCase): + """ + Test case for Llama models using LoRA + """ + + @with_temp_dir + def test_lora_ddp(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "TinyLlama/TinyLlama_v1.1", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 2048, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.05, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 100, + "micro_batch_size": 4, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) + + @with_temp_dir + def test_lora_ddp_packed(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "TinyLlama/TinyLlama_v1.1", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 2048, + "sample_packing": True, + "eval_sample_packing": False, + "pad_to_sequence_len": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.05, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 50, + "micro_batch_size": 4, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) + + @with_temp_dir + def test_fsdp(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "TinyLlama/TinyLlama_v1.1", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 2048, + "val_set_size": 0.05, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 100, + "micro_batch_size": 4, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp": [ + "full_shard", + "auto_wrap", + ], + "fsdp_config": { + "fsdp_limit_all_gathers": True, + "fsdp_offload_params": False, + "fsdp_sync_module_states": True, + "fsdp_use_orig_params": False, + "fsdp_cpu_ram_efficient_loading": False, + "fsdp_transformer_layer_cls_to_wrap": "LlamaDecoderLayer", + "fsdp_state_dict_type": "SHARDED_STATE_DICT", + "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + }, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) + + @with_temp_dir + def test_fsdp_packed(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "TinyLlama/TinyLlama_v1.1", + "tokenizer_type": "LlamaTokenizer", + "sample_packing": True, + "eval_sample_packing": False, + "pad_to_sequence_len": True, + "sequence_len": 2048, + "val_set_size": 0.05, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 100, + "micro_batch_size": 4, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp": [ + "full_shard", + "auto_wrap", + ], + "fsdp_config": { + "fsdp_limit_all_gathers": True, + "fsdp_offload_params": False, + "fsdp_sync_module_states": True, + "fsdp_use_orig_params": False, + "fsdp_cpu_ram_efficient_loading": False, + "fsdp_transformer_layer_cls_to_wrap": "LlamaDecoderLayer", + "fsdp_state_dict_type": "SHARDED_STATE_DICT", + "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + }, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) + + @pytest.mark.skip("disabled due to upstream issue") + @with_temp_dir + def test_fsdp_qlora_prequant_packed(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "axolotl-ai-co/TinyLlama_v1.1-bnb-nf4-bf16", + "tokenizer_type": "AutoTokenizer", + "adapter": "qlora", + "load_in_4bit": True, + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "lora_modules_to_save": [ + "embed_tokens", + "lm_head", + ], + "sample_packing": True, + "eval_sample_packing": False, + "pad_to_sequence_len": True, + "sequence_len": 2048, + "val_set_size": 0.05, + "special_tokens": { + "pad_token": "<|end_of_text|>", + }, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:25%]", + }, + ], + "num_epochs": 1, + "max_steps": 100, + "micro_batch_size": 4, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp": [ + "full_shard", + "auto_wrap", + ], + "fsdp_config": { + "fsdp_limit_all_gathers": True, + "fsdp_offload_params": False, + "fsdp_sync_module_states": True, + "fsdp_use_orig_params": False, + "fsdp_cpu_ram_efficient_loading": True, + "fsdp_transformer_layer_cls_to_wrap": "LlamaDecoderLayer", + "fsdp_state_dict_type": "SHARDED_STATE_DICT", + "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + }, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) From 0801f239ccfb0ff06b52eec0029e3f2f954ee147 Mon Sep 17 00:00:00 2001 From: Chiwan Park Date: Sat, 10 Aug 2024 00:50:31 +0900 Subject: [PATCH 0112/1405] fix the incorrect `max_length` for chat template (#1818) --- src/axolotl/prompt_strategies/chat_template.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 719d79505e..d0fad4483a 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -350,6 +350,7 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): ), "roles": ds_cfg.get("roles"), "drop_system_message": ds_cfg.get("drop_system_message", False), + "max_length": cfg.sequence_len, } strategy_params = { From 1853d6021d9a15d588c2ec80dc2d30b0da30ab81 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 11 Aug 2024 16:27:41 -0400 Subject: [PATCH 0113/1405] bump hf dependencies (#1823) * bump hf dependencies * revert optimum version change * don't bump tokenizers all the way to 0.20 yet since transformers doesn't support that --- requirements.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index fdcae107c5..f32af373b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,18 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.12.0 -transformers==4.43.4 -tokenizers==0.19.1 +transformers==4.44.0 +tokenizers>=0.19.1 bitsandbytes==0.43.3 -accelerate==0.32.0 +accelerate==0.33.0 +datasets==2.20.0 deepspeed==0.14.4 pydantic==2.6.3 addict fire PyYAML>=6.0 requests -datasets==2.19.1 -flash-attn==2.6.2 +flash-attn==2.6.3 sentencepiece wandb einops @@ -37,8 +37,8 @@ autoawq>=0.2.5 mamba-ssm==1.2.0.post1 # remote filesystems -s3fs -gcsfs +s3fs>=2024.5.0 +gcsfs>=2024.5.0 # adlfs trl==0.9.6 From f18925fb4bb7af8f4a6e690242e24cef6249e2c0 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 14 Aug 2024 22:46:46 +0900 Subject: [PATCH 0114/1405] fix: parse eager_attention (#1824) --- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index b765263ba6..7bf77965c2 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -614,6 +614,8 @@ class Config: flash_attn_fuse_mlp: Optional[bool] = None flash_optimum: Optional[bool] = None + eager_attention: Optional[bool] = None + unsloth_cross_entropy_loss: Optional[bool] = None unsloth_lora_mlp: Optional[bool] = None unsloth_lora_qkv: Optional[bool] = None From 68a3c7678a3cbf5098239f84a548c9492ebe4387 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 16 Aug 2024 20:51:19 +0900 Subject: [PATCH 0115/1405] fix: parse model_kwargs (#1825) --- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 7bf77965c2..647d6b88c7 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -321,6 +321,8 @@ class ModelInputConfig(BaseModel): ) trust_remote_code: Optional[bool] = None + model_kwargs: Optional[Dict[str, Any]] = None + @field_validator("trust_remote_code") @classmethod def hint_trust_remote_code(cls, trust_remote_code): From 803fed3e904a527ce302fc818aef3500e07722c9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 16 Aug 2024 10:41:51 -0400 Subject: [PATCH 0116/1405] update sklearn versrion, torch compile env vars, don't worry about failure on preprocess load model (#1821) * update sklearn versrion, torch compile env vars, don't worry about failure on preprocess load model * There is already a condition check within the function. This outer one is not necessary Co-authored-by: NanoCode012 --------- Co-authored-by: NanoCode012 --- requirements.txt | 2 +- src/axolotl/cli/preprocess.py | 9 ++++++++- src/axolotl/utils/trainer.py | 10 ++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index f32af373b8..dc74b916f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ numpy>=1.24.4 # qlora things evaluate==0.4.1 scipy -scikit-learn==1.2.2 +scikit-learn==1.4.2 pynvml art fschat @ git+https://github.com/lm-sys/FastChat.git@27a05b04a35510afb1d767ae7e5990cbd278f8fe diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index e0dd7c2dc1..e12462c000 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -82,7 +82,14 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): # "copying from a non-meta parameter in the checkpoint to a meta parameter in the current model" warnings.simplefilter("ignore") with init_empty_weights(include_buffers=True): - AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True) + # fmt: off + try: + AutoModelForCausalLM.from_pretrained( + model_name, trust_remote_code=True + ) + except Exception as exc: # pylint: disable=broad-exception-caught,unused-variable # nosec B110 # noqa F841 + pass + # fmt: on LOG.info( Fore.GREEN diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 02234d8b70..26796f2e53 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -390,6 +390,14 @@ def calc_sample_packing_eff_est(estimates: List[float]): return total_num_steps +def setup_torch_compile_env(cfg): + if cfg.torch_compile: + if not cfg.torch_compile_backend: + os.environ["ACCELERATE_DYNAMO_BACKEND"] = "INDUCTOR" + else: + os.environ["ACCELERATE_DYNAMO_BACKEND"] = cfg.torch_compile_backend.upper() + + def setup_deepspeed_env(cfg, stage=None): os.environ["ACCELERATE_USE_DEEPSPEED"] = "true" os.environ["ACCELERATE_DEEPSPEED_CONFIG_FILE"] = cfg.deepspeed @@ -434,6 +442,8 @@ def prepare_optim_env(cfg): stage = deepspeed_config.get("zero_optimization", {}).get("stage", None) setup_deepspeed_env(cfg, stage=stage) + setup_torch_compile_env(cfg) + if (cfg.bf16 == "auto" and is_torch_bf16_gpu_available()) or cfg.bf16 is True: os.environ["ACCELERATE_MIXED_PRECISION"] = "bf16" elif cfg.fp16: From b1d29212222f5dd566fab5453e3fd342089c8191 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 16 Aug 2024 21:32:00 -0400 Subject: [PATCH 0117/1405] add validation to prevent 8bit lora finetuning on H100s (#1827) --- .../utils/config/models/input/v0_4_1/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 647d6b88c7..5e690bb88e 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -1267,6 +1267,19 @@ def check_sample_packing_w_sdpa_bf16(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_hopper_8bit_lora(cls, data): + is_sm_90: bool = ( + data["capabilities"] + and data["capabilities"].get("compute_capability") == "sm_90" + ) + if data.get("adapter") and data.get("load_in_8bit") and is_sm_90: + # see https://github.com/bitsandbytes-foundation/bitsandbytes/issues/538#issuecomment-2262945464 + raise ValueError("8-bit LoRA is not supported on Hopper GPUs") + + return data + @model_validator(mode="before") @classmethod def check_fsdp_deepspeed(cls, data): From e29931259b2b2c9939f3f96d5ef5e994dfb77cf1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 19 Aug 2024 14:59:24 -0400 Subject: [PATCH 0118/1405] optionally save the final FSDP model as a sharded state dict (#1828) * efficiently save very large llms when using FSDP * fix parsing and index of sharded chunks * only save fsdp on main process * debugging for rename * save sharded state dict * remove unused new param * get state dict directly * tweak acc merge fsdp to shard the weight files * sharded_state_dict alongside save_safetensors seems to hang on checkpoint save --- src/axolotl/cli/merge_sharded_fsdp_weights.py | 204 ++++++++++++++++++ src/axolotl/train.py | 21 +- .../config/models/input/v0_4_1/__init__.py | 17 ++ 3 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 src/axolotl/cli/merge_sharded_fsdp_weights.py diff --git a/src/axolotl/cli/merge_sharded_fsdp_weights.py b/src/axolotl/cli/merge_sharded_fsdp_weights.py new file mode 100644 index 0000000000..25408fd57e --- /dev/null +++ b/src/axolotl/cli/merge_sharded_fsdp_weights.py @@ -0,0 +1,204 @@ +""" +This module provides a CLI to merge sharded FSDP model checkpoints into a single combined checkpoint +""" +import json +import logging +import os +import shutil +from pathlib import Path +from typing import Dict, Union + +import fire +import torch +import torch.distributed.checkpoint as dist_cp +import torch.distributed.checkpoint.format_utils as dist_cp_format_utils +import transformers +from accelerate.utils import ( + SAFE_WEIGHTS_INDEX_NAME, + SAFE_WEIGHTS_NAME, + WEIGHTS_INDEX_NAME, + WEIGHTS_NAME, + is_torch_version, +) +from dotenv import load_dotenv +from huggingface_hub import split_torch_state_dict_into_shards +from safetensors.torch import save_file as safe_save_file +from torch.distributed.checkpoint.format_utils import _EmptyStateDictLoadPlanner + +from axolotl.cli import load_cfg, print_axolotl_text_art +from axolotl.common.cli import TrainerCliArgs + +LOG = logging.getLogger("axolotl.cli.merge_sharded_fsdp_weights") + + +class BFloat16CastPlanner(_EmptyStateDictLoadPlanner): + """ + A custom planner to cast tensors to bfloat16 on the fly during loading. + """ + + def commit_tensor(self, read_item, tensor): # pylint: disable=unused-argument + tensor.copy_(tensor.to(torch.bfloat16)) + + +def _distributed_checkpoint_to_merged_weights( + checkpoint_dir: Union[str, Path], + save_path: str, + safe_serialization: bool = False, + max_shard_size: str = "5GB", +): + """ + Passthrough to `torch.distributed.checkpoint.format_utils.dcp_to_torch_save` + + Will save under `save_path` as either `model.safetensors` or `pytorch_model.bin`. + """ + + state_dict: Dict = {} + save_path_ = Path(save_path) + save_path_.mkdir(exist_ok=True) + dist_cp_format_utils._load_state_dict( # pylint: disable=protected-access + state_dict, + storage_reader=dist_cp.FileSystemReader(checkpoint_dir), + planner=BFloat16CastPlanner(), # pylint: disable=protected-access + no_dist=True, + ) + + # To handle if state is a dict like {model: {...}} + if len(state_dict.keys()) == 1: + state_dict = state_dict[list(state_dict)[0]] + + # Ensure all tensors are in bfloat16 + for key, value in state_dict.items(): + if isinstance(value, torch.Tensor) and value.dtype != torch.bfloat16: + state_dict[key] = value.to(torch.bfloat16) + + weights_name = SAFE_WEIGHTS_NAME if safe_serialization else WEIGHTS_NAME + + filename_pattern = weights_name.replace(".bin", "{suffix}.bin").replace( + ".safetensors", "{suffix}.safetensors" + ) + state_dict_split = split_torch_state_dict_into_shards( + state_dict, filename_pattern=filename_pattern, max_shard_size=max_shard_size + ) + # Save index if sharded + index = None + if state_dict_split.is_sharded: + index = { + "metadata": state_dict_split.metadata, + "weight_map": state_dict_split.tensor_to_filename, + } + + # Save the model + filename_to_tensors = state_dict_split.filename_to_tensors.items() + + for shard_file, tensors in filename_to_tensors: + shard = {tensor: state_dict[tensor] for tensor in tensors} + + if safe_serialization: + safe_save_file( + shard, os.path.join(save_path_, shard_file), metadata={"format": "pt"} + ) + else: + torch.save(shard, os.path.join(save_path_, shard_file)) + + if index is not None: + save_index_file = ( + SAFE_WEIGHTS_INDEX_NAME if safe_serialization else WEIGHTS_INDEX_NAME + ) + save_index_file = os.path.join(save_path_, save_index_file) + # Save the index as well + with open(save_index_file, "w", encoding="utf-8") as fout: + content = json.dumps(index, indent=2, sort_keys=True) + "\n" + fout.write(content) + + return save_path_ + + +def merge_fsdp_weights( + checkpoint_dir: str, + output_path: str, + safe_serialization: bool = False, + remove_checkpoint_dir: bool = False, +): + """ + Merge the weights from sharded FSDP model checkpoints into a single combined checkpoint. Should be used if + `SHARDED_STATE_DICT` was used for the model. Weights will be saved to `{output_path}/model.safetensors` if + `safe_serialization` else `pytorch_model.bin`. + + Note: this is a CPU-bound process. + + Args: + checkpoint_dir (`str`): + The directory containing the FSDP checkpoints (can be either the model or optimizer). + output_path (`str`): + The path to save the merged checkpoint. + safe_serialization (`bool`, *optional*, defaults to `True`): + Whether to save the merged weights with safetensors (recommended). + remove_checkpoint_dir (`bool`, *optional*, defaults to `False`): + Whether to remove the checkpoint directory after merging. + """ + checkpoint_dir_ = Path(checkpoint_dir) + from accelerate.state import PartialState + + if not is_torch_version(">=", "2.3.0"): + raise ValueError("`merge_fsdp_weights` requires PyTorch >= 2.3.0`") + + # Verify that the checkpoint directory exists + if not checkpoint_dir_.exists(): + model_path_exists = (checkpoint_dir_ / "pytorch_model_fsdp_0").exists() + optimizer_path_exists = (checkpoint_dir_ / "optimizer_0").exists() + err = f"Tried to load from {checkpoint_dir_} but couldn't find a valid metadata file." + if model_path_exists and optimizer_path_exists: + err += ( + " However, potential model and optimizer checkpoint directories exist." + ) + err += f"Please pass in either {checkpoint_dir_}/pytorch_model_fsdp_0 or {checkpoint_dir_}/optimizer_0" + err += "instead." + elif model_path_exists: + err += " However, a potential model checkpoint directory exists." + err += ( + f"Please try passing in {checkpoint_dir_}/pytorch_model_fsdp_0 instead." + ) + elif optimizer_path_exists: + err += " However, a potential optimizer checkpoint directory exists." + err += f"Please try passing in {checkpoint_dir_}/optimizer_0 instead." + raise ValueError(err) + + # To setup `save` to work + state = PartialState() + if state.is_main_process: + LOG.info(f"Merging FSDP weights from {checkpoint_dir_}") + save_path = _distributed_checkpoint_to_merged_weights( + checkpoint_dir_, output_path, safe_serialization + ) + LOG.info(f"Successfully merged FSDP weights and saved to {save_path}") + if remove_checkpoint_dir: + LOG.info(f"Removing old checkpoint directory {checkpoint_dir_}") + shutil.rmtree(checkpoint_dir_) + state.wait_for_everyone() + + +def do_cli(config: Path = Path("examples/"), **kwargs): + # pylint: disable=duplicate-code + print_axolotl_text_art() + parser = transformers.HfArgumentParser((TrainerCliArgs)) + parsed_cli_args, _ = parser.parse_args_into_dataclasses( + return_remaining_strings=True + ) + parsed_cli_args.merge_lora = True + + parsed_cfg = load_cfg( + config, + **kwargs, + ) + + fsdp_dir = Path(parsed_cfg.output_dir) / "pytorch_model_fsdp_0" + merge_fsdp_weights( + checkpoint_dir=str(fsdp_dir), + output_path=str(Path(parsed_cfg.output_dir) / "merged"), + safe_serialization=True, + ) + + +if __name__ == "__main__": + load_dotenv() + fire.Fire(do_cli) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index b8890d4f7a..b21b0b269c 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -12,6 +12,7 @@ import transformers.modelcard from accelerate import Accelerator from accelerate.logging import get_logger +from accelerate.utils import save_fsdp_model from datasets import Dataset from peft import PeftModel from pkg_resources import get_distribution # type: ignore @@ -194,9 +195,12 @@ def terminate_handler(_, __, model_weakref): if hasattr(module, "_post_training"): module._post_training(model, name) # pylint: disable=protected-access + state_dict_type = "FULL_STATE_DICT" if trainer.is_fsdp_enabled: - trainer.accelerator.state.fsdp_plugin.set_state_dict_type("FULL_STATE_DICT") - LOG.info("Set FSDP state dict type to FULL_STATE_DICT for saving.") + if cfg.fsdp_final_state_dict_type: + state_dict_type = cfg.fsdp_final_state_dict_type + trainer.accelerator.state.fsdp_plugin.set_state_dict_type(state_dict_type) + LOG.info(f"Set FSDP state dict type to {state_dict_type} for saving.") if cfg.relora_steps: if cfg.adapter == "lora" and not (cfg.load_in_4bit or cfg.load_in_8bit): @@ -208,7 +212,18 @@ def terminate_handler(_, __, model_weakref): # TODO do we need this fix? https://huggingface.co/docs/accelerate/usage_guides/fsdp#saving-and-loading # only save on rank 0, otherwise it corrupts output on multi-GPU when multiple processes attempt to write the same file if cfg.fsdp: - trainer.save_model(cfg.output_dir) + if ( + state_dict_type == "SHARDED_STATE_DICT" + and cfg.fsdp_config.fsdp_state_dict_type == "SHARDED_STATE_DICT" + ): + save_fsdp_model( + trainer.accelerator.state.fsdp_plugin, + trainer.accelerator, + trainer.model, + cfg.output_dir, + ) + elif state_dict_type == "FULL_STATE_DICT": + trainer.save_model(cfg.output_dir) elif cfg.deepspeed and is_deepspeed_zero3_enabled(): # Copied over from: https://github.com/huggingface/accelerate/blob/5ae611118057232f441055f7ef9ba0b0f2b8d533/docs/source/usage_guides/deepspeed.md#saving-and-loading trainer.accelerator.wait_for_everyone() diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 5e690bb88e..dcc902c8c6 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -628,6 +628,9 @@ class Config: deepspeed: Optional[Union[str, Dict[str, Any]]] = None fsdp: Optional[List[str]] = None fsdp_config: Optional[Dict[str, Any]] = None + fsdp_final_state_dict_type: Optional[ + Literal["FULL_STATE_DICT", "LOCAL_STATE_DICT", "SHARDED_STATE_DICT"] + ] = None val_set_size: Optional[float] = Field(default=0.0) @@ -1148,6 +1151,20 @@ def check_fsdp_offload_w_8bit_optimizer(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_fsdp_sharded_state_dict_w_safetensors(cls, data): + if ( + data.get("fsdp") + and data.get("save_safetensors") + and data.get("fsdp_config") + and data["fsdp_config"].get("fsdp_state_dict_type") == "SHARDED_STATE_DICT" + ): + raise ValueError( + "FSDP SHARDED_STATE_DICT not compatible with save_safetensors" + ) + return data + @model_validator(mode="before") @classmethod def check_causal_lm_evals(cls, data): From 5aac4bc2846ac7379c0a52dd894dd1ba5499ec22 Mon Sep 17 00:00:00 2001 From: "Gal Cohen (galco)" Date: Tue, 20 Aug 2024 19:41:48 +0300 Subject: [PATCH 0119/1405] fix: dont change quant storage dtype in case of fsdp (#1837) * fix: dont change quant storage dtype in case of fsdp * fix black --------- Co-authored-by: Gal Cohen --- src/axolotl/utils/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 5ac66260a7..3e8d50f5e7 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -544,7 +544,9 @@ def load_model( "bnb_4bit_quant_type": "nf4", "bnb_4bit_quant_storage": torch.bfloat16, } - if cfg.model_config_type in ["jamba", "qwen2_moe"] and not cfg.deepspeed: + if cfg.model_config_type in ["jamba", "qwen2_moe"] and not ( + cfg.deepspeed or cfg.fsdp + ): # for some reason, this causes the loss to be off by an order of magnitude # but deepspeed needs this still in bfloat16 bnb_config["bnb_4bit_quant_storage"] = torch.float32 From 649c19aba31c022028bb508c1b945da9fe407e94 Mon Sep 17 00:00:00 2001 From: Aman Gupta Karmani Date: Wed, 21 Aug 2024 10:36:51 -0700 Subject: [PATCH 0120/1405] pretrain: fix with sample_packing=false (#1841) --- src/axolotl/utils/data/pretraining.py | 4 ++-- tests/test_data.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/data/pretraining.py b/src/axolotl/utils/data/pretraining.py index e056c7f509..16f38218cd 100644 --- a/src/axolotl/utils/data/pretraining.py +++ b/src/axolotl/utils/data/pretraining.py @@ -18,10 +18,10 @@ def encode_pretraining( - tokenizer: PreTrainedTokenizerBase, max_tokens: int, examples: List[str] + tokenizer: PreTrainedTokenizerBase, max_tokens: int, examples: Dict[str, List] ) -> Dict[str, List]: res = tokenizer( - examples, + examples["text"], truncation=True, max_length=max_tokens - 2, add_special_tokens=True, diff --git a/tests/test_data.py b/tests/test_data.py index 16af089a06..9d7f5a0412 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -35,7 +35,7 @@ def test_encode_pretraining(self): "hello, hello", ] } - result = encode_pretraining(self.tokenizer, self.max_tokens, examples["text"]) + result = encode_pretraining(self.tokenizer, self.max_tokens, examples) self.assertEqual(len(result["input_ids"]), 3) From 9f917245f69940bf69cf8bade13106ee8345c6aa Mon Sep 17 00:00:00 2001 From: "Gal Cohen (galco)" Date: Wed, 21 Aug 2024 20:37:17 +0300 Subject: [PATCH 0121/1405] feat: add jamba chat_template (#1843) * feat: add jamba chat_template * fix: black * feat: jamba fsdp+qlora --------- Co-authored-by: Gal Cohen --- examples/jamba/qlora_fsdp.yaml | 61 +++++++++++++++++++ src/axolotl/utils/chat_templates.py | 1 + .../config/models/input/v0_4_1/__init__.py | 1 + 3 files changed, 63 insertions(+) create mode 100644 examples/jamba/qlora_fsdp.yaml diff --git a/examples/jamba/qlora_fsdp.yaml b/examples/jamba/qlora_fsdp.yaml new file mode 100644 index 0000000000..2ea268344a --- /dev/null +++ b/examples/jamba/qlora_fsdp.yaml @@ -0,0 +1,61 @@ +base_model: ai21labs/Jamba-v0.1 +tokenizer_type: AutoTokenizer + +load_in_4bit: true +strict: false +use_tensorboard: true +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + chat_template: jamba + drop_system_message: true +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: jamba-fsdp-qlora-ft +save_safetensors: true +adapter: qlora +sequence_len: 2048 +sample_packing: true +pad_to_sequence_len: true + +lora_r: 16 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: [down_proj,gate_proj,in_proj,k_proj,o_proj,out_proj,q_proj,up_proj,v_proj,x_proj] +lora_target_linear: false + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 2 +optimizer: adamw_torch +lr_scheduler: cosine +learning_rate: 0.00001 + +train_on_inputs: false +group_by_length: false +bf16: true +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: true +logging_steps: 1 +flash_attention: true + +warmup_steps: 10 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_limit_all_gathers: true + fsdp_sync_module_states: true + fsdp_offload_params: false + fsdp_use_orig_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: JambaAttentionDecoderLayer,JambaMambaDecoderLayer + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index ca4334d75a..51f88b1bdf 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -27,6 +27,7 @@ def chat_templates(user_choice: str): "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", "deepseek_v2": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %}", + "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', } if user_choice in templates: diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index dcc902c8c6..65a2c5409a 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -190,6 +190,7 @@ class ChatTemplate(str, Enum): llama3 = "llama3" # pylint: disable=invalid-name phi_3 = "phi_3" # pylint: disable=invalid-name deepseek_v2 = "deepseek_v2" # pylint: disable=invalid-name + jamba = "jamba" # pylint: disable=invalid-name class LoftQConfig(BaseModel): From f07802f9fa9ae95f0b37ce626eaf21eca9fce738 Mon Sep 17 00:00:00 2001 From: Aman Gupta Karmani Date: Wed, 21 Aug 2024 10:37:51 -0700 Subject: [PATCH 0122/1405] examples: fix tiny-llama pretrain yml syntax (#1840) --- examples/tiny-llama/pretrain.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/tiny-llama/pretrain.yml b/examples/tiny-llama/pretrain.yml index e501dcb8e5..010a1608a3 100644 --- a/examples/tiny-llama/pretrain.yml +++ b/examples/tiny-llama/pretrain.yml @@ -9,9 +9,9 @@ strict: false max_steps: 200 pretraining_dataset: - path: c4 - name: en - type: pretrain + - path: allenai/c4 + name: en + type: pretrain dataset_prepared_path: val_set_size: 0.0 output_dir: ./outputs/model-out From 957c956f89ded7f9e8785c5ca303d4d95bd98453 Mon Sep 17 00:00:00 2001 From: "Gal Cohen (galco)" Date: Thu, 22 Aug 2024 16:22:55 +0300 Subject: [PATCH 0123/1405] rename jamba example (#1846) [skip ci] * rename jamba example * feat: change readme --------- Co-authored-by: Gal Cohen --- README.md | 77 +++++++++++-------- examples/jamba/README.md | 2 +- ...{qlora_fsdp.yaml => qlora_fsdp_large.yaml} | 4 +- 3 files changed, 47 insertions(+), 36 deletions(-) rename examples/jamba/{qlora_fsdp.yaml => qlora_fsdp_large.yaml} (94%) diff --git a/README.md b/README.md index a626635dc8..55a11d6c12 100644 --- a/README.md +++ b/README.md @@ -22,39 +22,49 @@ Features: ## Table of Contents -- [Introduction](#axolotl) -- [Supported Features](#axolotl-supports) -- [Quickstart](#quickstart-) -- [Environment](#environment) - - [Docker](#docker) - - [Conda/Pip venv](#condapip-venv) - - [Cloud GPU](#cloud-gpu) - Latitude.sh, JarvisLabs, RunPod - - [Bare Metal Cloud GPU](#bare-metal-cloud-gpu) - - [Windows](#windows) - - [Mac](#mac) - - [Google Colab](#google-colab) - - [Launching on public clouds via SkyPilot](#launching-on-public-clouds-via-skypilot) - - [Launching on public clouds via dstack](#launching-on-public-clouds-via-dstack) -- [Dataset](#dataset) -- [Config](#config) - - [Train](#train) - - [Inference](#inference-playground) - - [Merge LORA to Base](#merge-lora-to-base) - - [Special Tokens](#special-tokens) - - [All Config Options](#all-config-options) -- Advanced Topics - - [Multipack](./docs/multipack.qmd) - - [RLHF & DPO](./docs/rlhf.qmd) - - [Dataset Pre-Processing](./docs/dataset_preprocessing.qmd) - - [Unsloth](./docs/unsloth.qmd) -- [Common Errors](#common-errors-) - - [Tokenization Mismatch b/w Training & Inference](#tokenization-mismatch-bw-inference--training) -- [Debugging Axolotl](#debugging-axolotl) -- [Need Help?](#need-help-) -- [Badge](#badge-) -- [Community Showcase](#community-showcase) -- [Contributing](#contributing-) -- [Sponsors](#sponsors-) +- [Axolotl](#axolotl) + - [Table of Contents](#table-of-contents) + - [Axolotl supports](#axolotl-supports) + - [Quickstart ⚡](#quickstart-) + - [Usage](#usage) + - [Advanced Setup](#advanced-setup) + - [Environment](#environment) + - [Docker](#docker) + - [Conda/Pip venv](#condapip-venv) + - [Cloud GPU](#cloud-gpu) + - [Bare Metal Cloud GPU](#bare-metal-cloud-gpu) + - [LambdaLabs](#lambdalabs) + - [GCP](#gcp) + - [Windows](#windows) + - [Mac](#mac) + - [Google Colab](#google-colab) + - [Launching on public clouds via SkyPilot](#launching-on-public-clouds-via-skypilot) + - [Launching on public clouds via dstack](#launching-on-public-clouds-via-dstack) + - [Dataset](#dataset) + - [Config](#config) + - [All Config Options](#all-config-options) + - [Train](#train) + - [Preprocess dataset](#preprocess-dataset) + - [Multi-GPU](#multi-gpu) + - [DeepSpeed](#deepspeed) + - [FSDP](#fsdp) + - [FSDP + QLoRA](#fsdp--qlora) + - [Weights \& Biases Logging](#weights--biases-logging) + - [Special Tokens](#special-tokens) + - [Inference Playground](#inference-playground) + - [Merge LORA to base](#merge-lora-to-base) + - [Common Errors 🧰](#common-errors-) + - [Tokenization Mismatch b/w Inference \& Training](#tokenization-mismatch-bw-inference--training) + - [Debugging Axolotl](#debugging-axolotl) + - [Need help? 🙋](#need-help-) + - [Badge ❤🏷️](#badge-️) + - [Community Showcase](#community-showcase) + - [Contributing 🤝](#contributing-) + - [Sponsors 🤝❤](#sponsors-) + - [💎 Diamond Sponsors - Contact directly](#-diamond-sponsors---contact-directly) + - [🥇 Gold Sponsors - $5000/mo](#-gold-sponsors---5000mo) + - [🥈 Silver Sponsors - $1000/mo](#-silver-sponsors---1000mo) + - [🥉 Bronze Sponsors - $500/mo](#-bronze-sponsors---500mo) @@ -96,6 +106,7 @@ Features: | RWKV | ✅ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | Qwen | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | | Gemma | ✅ | ✅ | ✅ | ❓ | ❓ | ✅ | ❓ | +| Jamba | ✅ | ✅ | ✅ | ❓ | ❓ | ✅ | ❓ | ✅: supported ❌: not supported diff --git a/examples/jamba/README.md b/examples/jamba/README.md index 54f5d1da9c..4c9dc85a06 100644 --- a/examples/jamba/README.md +++ b/examples/jamba/README.md @@ -6,5 +6,5 @@ - ✅ qlora w/ deepspeed Zero-3 needs at least 2x GPUs and 67GiB VRAM (wtf?) - ✅ qlora single-gpu, ~51GiB VRAM - ✅ multipack -- ❓ FSDP +- ✅ FSDP - ❓ 8-bit LoRA diff --git a/examples/jamba/qlora_fsdp.yaml b/examples/jamba/qlora_fsdp_large.yaml similarity index 94% rename from examples/jamba/qlora_fsdp.yaml rename to examples/jamba/qlora_fsdp_large.yaml index 2ea268344a..28316efd57 100644 --- a/examples/jamba/qlora_fsdp.yaml +++ b/examples/jamba/qlora_fsdp_large.yaml @@ -1,4 +1,4 @@ -base_model: ai21labs/Jamba-v0.1 +base_model: ai21labs/AI21-Jamba-1.5-Large tokenizer_type: AutoTokenizer load_in_4bit: true @@ -11,7 +11,7 @@ datasets: drop_system_message: true dataset_prepared_path: last_run_prepared val_set_size: 0.0 -output_dir: jamba-fsdp-qlora-ft +output_dir: jamba-large-fsdp-qlora-ft save_safetensors: true adapter: qlora sequence_len: 2048 From c3fc529bfc5a302e48218ec990b56b69c969127f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 22 Aug 2024 11:44:45 -0400 Subject: [PATCH 0124/1405] numpy 2.1.0 was released, but incompatible with numba (#1849) [skip ci] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dc74b916f8..be0c4927e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ optimum==1.16.2 hf_transfer colorama numba -numpy>=1.24.4 +numpy>=1.24.4,<=2.0.1 # qlora things evaluate==0.4.1 scipy From 5b0b774e38495b85c9ce1bdbaa2803d5daab1c92 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 22 Aug 2024 11:45:00 -0400 Subject: [PATCH 0125/1405] ensure that the bias is also in the correct dtype (#1848) [skip ci] * ensure that the bias is also in the correct dtype * add nightly for dpo-qlora-fsdp --- examples/qwen2/qlora-fsdp.yaml | 1 + src/axolotl/core/trainer_builder.py | 2 + src/axolotl/utils/models.py | 17 ++++- tests/e2e/multigpu/test_qwen2.py | 98 +++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/multigpu/test_qwen2.py diff --git a/examples/qwen2/qlora-fsdp.yaml b/examples/qwen2/qlora-fsdp.yaml index 44f9c7e495..d61c72a378 100644 --- a/examples/qwen2/qlora-fsdp.yaml +++ b/examples/qwen2/qlora-fsdp.yaml @@ -72,4 +72,5 @@ fsdp_config: fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP fsdp_transformer_layer_cls_to_wrap: Qwen2DecoderLayer fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD special_tokens: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 4e8b369052..1a073ca047 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1846,6 +1846,8 @@ def build(self, total_num_steps): ) if self.cfg.fsdp: ensure_dtype(dpo_trainer.model, dtype=self.cfg.torch_dtype) + if self.cfg.rl in ["dpo", "ipo"] and dpo_trainer.ref_model: + ensure_dtype(dpo_trainer.ref_model, dtype=self.cfg.torch_dtype) dpo_trainer = self.hook_post_create_trainer(dpo_trainer) for callback in self.get_post_trainer_create_callbacks(dpo_trainer): diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 3e8d50f5e7..4f47d59bfb 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -1102,9 +1102,20 @@ def load_lora(model, cfg, inference=False, config_only=False): def ensure_dtype(model, dtype=torch.bfloat16): for name, module in model.named_modules(): + weight_mismatch = False + bias_mismatch = False try: - if module.weight.dtype != dtype: - print(f"Converting module {name}: {module.weight.dtype} -> {dtype}") - module.to(dtype) + weight_mismatch = module.weight.dtype != dtype except AttributeError: pass + try: + bias_mismatch = module.bias.dtype != dtype + except AttributeError: + pass + + if weight_mismatch: + print(f"Converting module {name}.weight: {module.weight.dtype} -> {dtype}") + if bias_mismatch: + print(f"Converting module {name}.bias: {module.bias.dtype} -> {dtype}") + if weight_mismatch or bias_mismatch: + module.to(dtype) diff --git a/tests/e2e/multigpu/test_qwen2.py b/tests/e2e/multigpu/test_qwen2.py new file mode 100644 index 0000000000..2513be69e5 --- /dev/null +++ b/tests/e2e/multigpu/test_qwen2.py @@ -0,0 +1,98 @@ +""" +E2E tests for multigpu qwen2 +""" + +import logging +import os +import unittest +from pathlib import Path + +import yaml +from accelerate.test_utils import execute_subprocess_async + +from axolotl.utils.dict import DictDefault + +from ..utils import with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e.multigpu") +os.environ["WANDB_DISABLED"] = "true" + + +class TestMultiGPUQwen2(unittest.TestCase): + """ + Test case for Llama models using LoRA + """ + + @with_temp_dir + def test_qlora_fsdp_dpo(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2-1.5B", + "load_in_4bit": True, + "rl": "dpo", + "chat_template": "chatml", + "sequence_len": 2048, + "adapter": "qlora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.05, + "datasets": [ + { + "path": "Intel/orca_dpo_pairs", + "split": "train", + "type": "chatml.intel", + }, + ], + "num_epochs": 1, + "max_steps": 100, + "warmup_steps": 20, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "bf16": "auto", + "tf32": True, + "gradient_checkpointing": True, + "gradient_checkpointing_kwargs": { + "use_reentrant": False, + }, + "fsdp": [ + "full_shard", + "auto_wrap", + ], + "fsdp_config": { + "fsdp_limit_all_gathers": True, + "fsdp_offload_params": False, + "fsdp_sync_module_states": True, + "fsdp_use_orig_params": False, + "fsdp_cpu_ram_efficient_loading": False, + "fsdp_transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "fsdp_state_dict_type": "FULL_STATE_DICT", + "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "fsdp_sharding_strategy": "FULL_SHARD", + }, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) From 9caa3eb699ef8eb1ad8c64011945d274172bfa63 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 22 Aug 2024 11:45:37 -0400 Subject: [PATCH 0126/1405] make the train_on_eos default to turn so all eos tokens are treated the same (#1847) [skip ci] --- src/axolotl/prompt_strategies/chat_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index d0fad4483a..8ae668d7e9 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -357,7 +357,7 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): "train_on_inputs": cfg.train_on_inputs, "sequence_len": cfg.sequence_len, "roles_to_train": ds_cfg.get("roles_to_train", ["gpt", "assistant"]), - "train_on_eos": ds_cfg.get("train_on_eos", "last"), + "train_on_eos": ds_cfg.get("train_on_eos", "turn"), } strategy = ChatTemplateStrategy( From 7ed92e61c26bad40e1cc2151d1d9f934816a60e4 Mon Sep 17 00:00:00 2001 From: JohanWork <39947546+JohanWork@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:46:57 +0200 Subject: [PATCH 0127/1405] fix: prompt phi (#1845) [skip ci] * corecting phi system prompt * phi test * update * add test --- src/axolotl/prompters.py | 6 ++++-- tests/test_prompters.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/axolotl/prompters.py b/src/axolotl/prompters.py index 0ffa3e55fd..13ff450f8a 100644 --- a/src/axolotl/prompters.py +++ b/src/axolotl/prompters.py @@ -65,8 +65,10 @@ def match_prompt_style(self): self.system_format = "<|im_start|>system\n{system}<|im_end|>\n" elif self.prompt_style == PromptStyle.PHI.value: self.turn_format = "<|user|>\n{instruction}<|end|>{input}<|assistant|>" - self.turn_no_input_format = "<|user|>\n{instruction}<|end|><|assistant|>" - self.system_format = "<|system|>{system}\n" + self.turn_no_input_format = ( + "<|user|>\n{instruction}<|end|>\n<|assistant|>\n" + ) + self.system_format = "<|system|>\n{system}<|end|>\n" def _build_result(self, instruction, input_text, output): # returns the full prompt from instruction and optional input diff --git a/tests/test_prompters.py b/tests/test_prompters.py index 6c5b8f27c2..3d61398e04 100644 --- a/tests/test_prompters.py +++ b/tests/test_prompters.py @@ -42,6 +42,19 @@ def test_prompt_style_w_instruct(self): assert "USER:" not in res assert "ASSISTANT:" not in res + def test_prompt_style_w_phi(self): + prompter = AlpacaPrompter(prompt_style=PromptStyle.PHI.value) + res = next(prompter.build_prompt("tell me a joke about the following")) + assert ( + """<|system|> +Below is an instruction that describes a task. Write a response that appropriately completes the request.<|end|> +<|user|> +tell me a joke about the following<|end|> +<|assistant|> +""" + == res + ) + def test_prompt_style_w_chat(self): prompter = AlpacaPrompter(prompt_style=PromptStyle.CHAT.value) res = next( From de4ea2d1f27074a54b1cc79301f426d2cc393a7f Mon Sep 17 00:00:00 2001 From: Aman Gupta Karmani Date: Thu, 22 Aug 2024 08:47:34 -0700 Subject: [PATCH 0128/1405] docs: minor syntax highlight fix (#1839) --- docs/unsloth.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/unsloth.qmd b/docs/unsloth.qmd index 390609fd33..90cb49bafa 100644 --- a/docs/unsloth.qmd +++ b/docs/unsloth.qmd @@ -34,7 +34,7 @@ unsloth_lora_o: true ``` These options are composable and can be used with multi-gpu finetuning -``` +```yaml unsloth_cross_entropy_loss: true unsloth_rms_norm: true unsloth_rope: true From 2f8037fee6cdee318df216049d0923455d80dad6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 22 Aug 2024 13:10:40 -0400 Subject: [PATCH 0129/1405] ensure that the hftrainer deepspeed config is set before the trainer class is ever init'ed (#1850) [skip ci] --- src/axolotl/utils/trainer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 26796f2e53..99c10c6558 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -399,12 +399,15 @@ def setup_torch_compile_env(cfg): def setup_deepspeed_env(cfg, stage=None): + from transformers.integrations.deepspeed import HfTrainerDeepSpeedConfig + os.environ["ACCELERATE_USE_DEEPSPEED"] = "true" os.environ["ACCELERATE_DEEPSPEED_CONFIG_FILE"] = cfg.deepspeed if stage: os.environ["ACCELERATE_DEEPSPEED_ZERO_STAGE"] = str(stage) if stage == 3: os.environ["ACCELERATE_DEEPSPEED_ZERO3_INIT"] = "true" + HfTrainerDeepSpeedConfig(cfg.deepspeed) def setup_fsdp_envs(cfg): From dcbff169830017ea4eb825aae1ef243cd124beda Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 22 Aug 2024 13:10:54 -0400 Subject: [PATCH 0130/1405] run nightly ci builds against upstream main (#1851) * run nightly ci builds against upstream main * add test badges * run the multigpu tests against nightly main builds too --- .github/workflows/multi-gpu-e2e.yml | 8 ++ .github/workflows/tests-nightly.yml | 116 ++++++++++++++++++++++++++++ README.md | 3 + cicd/Dockerfile.jinja | 8 ++ cicd/tests.py | 1 + setup.py | 2 +- 6 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tests-nightly.yml diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index c854af9abe..91cbaf957e 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -18,6 +18,13 @@ jobs: pytorch: 2.3.1 axolotl_extras: num_gpus: 2 + - cuda: 121 + cuda_version: 12.1.1 + python_version: "3.11" + pytorch: 2.3.1 + axolotl_extras: + num_gpus: 2 + nightly_build: "true" runs-on: [self-hosted, modal] timeout-minutes: 120 steps: @@ -39,6 +46,7 @@ jobs: echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV + echo "NIGHTLY_BUILD=${{ matrix.nightly_build }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | modal run cicd.multigpu diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml new file mode 100644 index 0000000000..23e48a85b0 --- /dev/null +++ b/.github/workflows/tests-nightly.yml @@ -0,0 +1,116 @@ +name: Tests +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * *' # Runs at 00:00 UTC every day + +jobs: + pre-commit: + name: pre-commit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + cache: 'pip' # caching pip dependencies + - uses: pre-commit/action@v3.0.0 + env: + SKIP: no-commit-to-branch + + pytest: + name: PyTest + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python_version: ["3.10", "3.11"] + timeout-minutes: 20 + + steps: + - name: Check out repository code + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python_version }} + cache: 'pip' # caching pip dependencies + + - name: Update requirements.txt + run: | + sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt + sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt + sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt + sed -i 's#^bitsandbytes.*#bitsandbytes @ git+https://github.com/bitsandbytes-foundation/bitsandbytes.git@main#' requirements.txt + + - name: Install dependencies + run: | + pip3 install --upgrade pip + pip3 install --upgrade packaging + pip3 install -U -e . + pip3 install -r requirements-tests.txt + + - name: Run tests + run: | + pytest --ignore=tests/e2e/ tests/ + + - name: cleanup pip cache + run: | + find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + + docker-e2e-tests: + if: github.repository_owner == 'axolotl-ai-cloud' + # this job needs to be run on self-hosted GPU runners... + runs-on: [self-hosted, modal] + timeout-minutes: 60 + needs: [pre-commit, pytest] + + strategy: + fail-fast: false + matrix: + include: + - cuda: 121 + cuda_version: 12.1.1 + python_version: "3.10" + pytorch: 2.3.1 + num_gpus: 1 + axolotl_extras: mamba-ssm + nightly_build: "true" + - cuda: 121 + cuda_version: 12.1.1 + python_version: "3.11" + pytorch: 2.3.1 + num_gpus: 1 + axolotl_extras: mamba-ssm + nightly_build: "true" + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.4.0 + num_gpus: 1 + axolotl_extras: + nightly_build: "true" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install Modal + run: | + python -m pip install --upgrade pip + pip install modal==0.63.64 jinja2 + - name: Update env vars + run: | + echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV + echo "PYTORCH_VERSION=${{ matrix.pytorch}}" >> $GITHUB_ENV + echo "AXOLOTL_ARGS=${{ matrix.axolotl_args}}" >> $GITHUB_ENV + echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV + echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV + echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV + echo "NIGHTLY_BUILD=${{ matrix.nightly_build }}" >> $GITHUB_ENV + - name: Run tests job on Modal + run: | + modal run cicd.tests diff --git a/README.md b/README.md index 55a11d6c12..46cde54f8f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Axolotl +![tests](https://github.com/axolotl-ai-cloud/axolotl/actions/workflows/tests.yml/badge.svg) +![multigpu-semi-weekly tests](https://github.com/axolotl-ai-cloud/axolotl/actions/workflows/multi-gpu-e2e.yml/badge.svg) + Axolotl is a tool designed to streamline the fine-tuning of various AI models, offering support for multiple configurations and architectures. Features: diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 3a79883667..c245fce3ed 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -8,6 +8,7 @@ ENV BNB_CUDA_VERSION="{{ CUDA }}" ENV PYTORCH_VERSION="{{ PYTORCH_VERSION }}" ENV GITHUB_REF="{{ GITHUB_REF }}" ENV GITHUB_SHA="{{ GITHUB_SHA }}" +ENV NIGHTLY_BUILD="{{ NIGHTLY_BUILD }}" RUN apt-get update && \ apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev @@ -23,6 +24,13 @@ RUN git fetch origin +$GITHUB_REF && \ # If AXOLOTL_EXTRAS is set, append it in brackets RUN pip install causal_conv1d +RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ + sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt; \ + sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt; \ + sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt; \ + sed -i 's#^bitsandbytes.*#bitsandbytes @ git+https://github.com/bitsandbytes-foundation/bitsandbytes.git@main#' requirements.txt; \ + fi + RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ pip install -e .[deepspeed,flash-attn,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ diff --git a/cicd/tests.py b/cicd/tests.py index c214676378..9c2d830cb7 100644 --- a/cicd/tests.py +++ b/cicd/tests.py @@ -28,6 +28,7 @@ "CUDA": os.environ.get("CUDA", "121"), "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), + "NIGHTLY_BUILD": os.environ.get("NIGHTLY_BUILD", ""), } dockerfile_contents = df_template.render(**df_args) diff --git a/setup.py b/setup.py index 1d164e0a18..1b64fadaef 100644 --- a/setup.py +++ b/setup.py @@ -80,7 +80,7 @@ def parse_requirements(): dependency_links=dependency_links, extras_require={ "flash-attn": [ - "flash-attn==2.6.2", + "flash-attn==2.6.3", ], "fused-dense-lib": [ "fused-dense-lib @ git+https://github.com/Dao-AILab/flash-attention@v2.6.2#subdirectory=csrc/fused_dense_lib", From b33dc07a7757819f68f6ceb4bbd2deb4c59c105b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 22 Aug 2024 13:13:33 -0400 Subject: [PATCH 0131/1405] rename nightly test and add badge (#1853) --- .github/workflows/tests-nightly.yml | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 23e48a85b0..1440efe790 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -1,4 +1,4 @@ -name: Tests +name: Tests Nightly against upstream main on: workflow_dispatch: schedule: diff --git a/README.md b/README.md index 46cde54f8f..8c70da015d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Axolotl ![tests](https://github.com/axolotl-ai-cloud/axolotl/actions/workflows/tests.yml/badge.svg) +![tests-nightly](https://github.com/axolotl-ai-cloud/axolotl/actions/workflows/tests-nightly.yml/badge.svg) ![multigpu-semi-weekly tests](https://github.com/axolotl-ai-cloud/axolotl/actions/workflows/multi-gpu-e2e.yml/badge.svg) Axolotl is a tool designed to streamline the fine-tuning of various AI models, offering support for multiple configurations and architectures. From fefa95e35069a01c96583853e075bf0319e55e0a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 22 Aug 2024 16:39:23 -0400 Subject: [PATCH 0132/1405] most model types now support flash attention 2 regardless of multipack support (#1854) --- src/axolotl/monkeypatch/multipack.py | 1 + src/axolotl/utils/models.py | 14 ++++---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 9043520108..44fc4cb473 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -17,6 +17,7 @@ "qwen2_moe", "falcon", "phi", + "phi3", "gemma", "gemma2", "gemmoe", diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 4f47d59bfb..8d24524a23 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -591,16 +591,10 @@ def load_model( "flash_attention_2" ) else: - if model_config.model_type in SUPPORTED_MULTIPACK_MODEL_TYPES: - model_kwargs["attn_implementation"] = "flash_attention_2" - model_config._attn_implementation = ( # pylint: disable=protected-access - "flash_attention_2" - ) - else: - model_kwargs["attn_implementation"] = "eager" - model_config._attn_implementation = ( # pylint: disable=protected-access - "eager" - ) + model_kwargs["attn_implementation"] = "flash_attention_2" + model_config._attn_implementation = ( # pylint: disable=protected-access + "flash_attention_2" + ) elif cfg.sdp_attention: model_kwargs["attn_implementation"] = "sdpa" model_config._attn_implementation = "sdpa" # pylint: disable=protected-access From 328fd4b3b74902eb39149496e5d9f7d9d4c4427f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 23 Aug 2024 11:40:21 -0400 Subject: [PATCH 0133/1405] add axolotl community license (#1862) --- src/axolotl/integrations/LICENSE.md | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/axolotl/integrations/LICENSE.md diff --git a/src/axolotl/integrations/LICENSE.md b/src/axolotl/integrations/LICENSE.md new file mode 100644 index 0000000000..435d36d75b --- /dev/null +++ b/src/axolotl/integrations/LICENSE.md @@ -0,0 +1,58 @@ +### AXOLOTL COMMUNITY LICENSE AGREEMENT + +This Axolotl Community License Agreement (“Agreement”) is entered into by and between Axolotl AI Corp. (“Axolotl”) and +any individual or entity (“Licensee”) who wishes to use the Software (as defined below) in accordance with the terms +and conditions set forth in this Agreement. + +1. Definitions + 1.1 “Licensee” refers to any individual or entity who has obtained a copy of the Software under this Agreement. + 1.2 “Plugin Integration” means independent integration software modules which may or may not be offered by Axolotl, + which may be licensed separately by their respective authors and/or licensors. + 1.3 “Software” refers to the specific sub-directory of the Axolotl, Inc. software located at + https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations and its subdirectories which + permits Plugin Integrations to integrate with the Axolotl service. +2. Grant of License + 2.1 Axolotl hereby grants Licensee a worldwide, non-exclusive, royalty-free, license to use, copy, modify, merge, + publish, distribute, sublicense, and/or otherwise exploit the Software, subject to the following conditions: + - Licensee must comply with all the terms and conditions of this Agreement. + - Licensee must include the original copyright notice and disclaimer of warranty in all copies or substantial + portions of the Software. + 2.2 Licensee may use the Software for any lawful purpose, except as restricted in Section 3. +3. Restrictions + 3.1 Licensee shall not use the Software for any activity that constitutes a commercial activity of offering for + free or for sale any services, platform, or equivalent to third parties for the purposes of allowing such + third parties to fine-tune artificial intelligence models. + 3.2 Licensee shall not: + - Use the Software for any illegal or unauthorized purpose. + - Reverse engineer, decompile, or disassemble the Software. + - Remove or modify any copyright, trademark, or other proprietary notices contained in the Software. + - Use the Software in a way that could damage, disable, overburden, or impair the functionality of the + Software or interfere with any third-party use of the Software. + 3.3 Axolotl reserves the right to restrict certain Plugin Integrations for use with the Software. To the extent Licensee integrates a permitted, applicable Plugin Integration with the Software, Licensee shall comply with any additional terms and conditions imposed by the licensors of such Plugin Integration for use of such Plugin Integrations. Licensee shall contact Axolotl if it has questions about whether its use of the Software falls beyond the scope of this Agreement. +4. Intellectual Property Rights + 4.1 Axolotl and its contributors retain all intellectual property rights in and to the Software. Licensee + acknowledges that this Agreement does not transfer any ownership rights or intellectual property rights to + Licensee. +5. Disclaimer of Warranty + 5.1 THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +6. Termination + 6.1 Axolotl may terminate this Agreement at any time if Licensee fails to comply with any of the terms and + conditions set forth herein. Upon termination, Licensee shall cease all use of the Software and destroy any + copies in its possession. +7. Governing Law + 7.1 This Agreement shall be governed by and construed in accordance with the laws of the State of California, + without regards to conflicts of laws provisions thereof. +8. Entire Agreement + 8.1 This Agreement constitutes the entire agreement between Axolotl and Licensee with respect to the subject matter + hereof and supersedes all prior or contemporaneous understandings or agreements between the parties concerning + the Software, whether written or oral. Axolotl may update the terms of this Agreement from time to time, and + Licensee’s continued use of the Software after any such updates shall constitute acceptance of updated terms + on a go-forward basis. Axolotl will use commercially reasonable efforts to provide Licensee notice of any + material updates. By using the Software, Licensee acknowledges that it has read, understood, and agrees to be + bound by the terms and conditions of this Agreement. + +This Agreement was last updated on August 23, 2024. From e8ff5d5738426bf604f79af9fffaf7dc050c6c2c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 23 Aug 2024 12:18:47 -0400 Subject: [PATCH 0134/1405] don't mess with bnb since it needs compiled wheels (#1859) --- .github/workflows/tests-nightly.yml | 1 - cicd/Dockerfile.jinja | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 1440efe790..6b35698cbf 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -42,7 +42,6 @@ jobs: sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt - sed -i 's#^bitsandbytes.*#bitsandbytes @ git+https://github.com/bitsandbytes-foundation/bitsandbytes.git@main#' requirements.txt - name: Install dependencies run: | diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index c245fce3ed..11ce8d8baa 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -28,7 +28,6 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt; \ sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt; \ sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt; \ - sed -i 's#^bitsandbytes.*#bitsandbytes @ git+https://github.com/bitsandbytes-foundation/bitsandbytes.git@main#' requirements.txt; \ fi RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ From 1f686c576c40a6dd7c8c785a133ac8fc09174b2e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 23 Aug 2024 12:21:51 -0400 Subject: [PATCH 0135/1405] Liger Kernel integration (#1861) * add initial plugin support w Liger kernel patches * integrate the input args classes * fix liger plugin and dynamic configuration class * drop untrainable samples and refactor config plugins integration * fix incorrect inputs and circular imports * fix bool comparison * fix for dropping untraibable tokens * fix licensing so liger integration is Apache 2.0 * add jamba support * pylint ignore --- .mypy.ini | 3 + requirements.txt | 2 + src/axolotl/cli/__init__.py | 6 + src/axolotl/integrations/base.py | 383 ++++++++++++++++++ src/axolotl/integrations/config.py | 65 +++ src/axolotl/integrations/liger/LICENSE | 202 +++++++++ src/axolotl/integrations/liger/__init__.py | 104 +++++ src/axolotl/integrations/liger/args.py | 32 ++ .../integrations/liger/models/jamba.py | 173 ++++++++ src/axolotl/utils/config/__init__.py | 18 +- src/axolotl/utils/models.py | 7 + src/axolotl/utils/trainer.py | 18 + 12 files changed, 1010 insertions(+), 3 deletions(-) create mode 100644 src/axolotl/integrations/base.py create mode 100644 src/axolotl/integrations/config.py create mode 100644 src/axolotl/integrations/liger/LICENSE create mode 100644 src/axolotl/integrations/liger/__init__.py create mode 100644 src/axolotl/integrations/liger/args.py create mode 100644 src/axolotl/integrations/liger/models/jamba.py diff --git a/.mypy.ini b/.mypy.ini index ede9fef887..c6d837d3f2 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -11,6 +11,9 @@ ignore_errors = True [mypy-axolotl.models.mixtral.*] ignore_errors = True +[mypy-axolotl.integrations.liger.models.*] +ignore_errors = True + [mypy-axolotl.models.phi.*] ignore_errors = True diff --git a/requirements.txt b/requirements.txt index be0c4927e4..f5fb547a26 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,6 +33,8 @@ gradio==3.50.2 tensorboard python-dotenv==1.0.1 autoawq>=0.2.5 +triton>=2.3.0 +liger-kernel mamba-ssm==1.2.0.post1 diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index a05ee84e97..aaa62423ca 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -27,6 +27,7 @@ from transformers.utils.import_utils import _is_package_available from axolotl.common.cli import TrainerCliArgs, load_model_and_tokenizer +from axolotl.integrations.base import PluginManager from axolotl.logging_config import configure_logging from axolotl.train import TrainDatasetMeta from axolotl.utils.config import ( @@ -365,6 +366,11 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs): cfg.axolotl_config_path = config + if cfg.get("plugins"): + plugin_manager = PluginManager.get_instance() + for plugin_name in cfg["plugins"]: + plugin_manager.register(plugin_name) + try: device_props = torch.cuda.get_device_properties("cuda") gpu_version = "sm_" + str(device_props.major) + str(device_props.minor) diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py new file mode 100644 index 0000000000..d26eed90fe --- /dev/null +++ b/src/axolotl/integrations/base.py @@ -0,0 +1,383 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# This software may be used and distributed according to +# the terms of the Axolotl Community License Agreement (the "License"); +# you may not use this file except in compliance with the License. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +""" +Base class for all plugins. + +A plugin is a reusable, modular, and self-contained piece of code that extends the functionality of Axolotl. +Plugins can be used to integrate third-party models, modify the training process, or add new features. + +To create a new plugin, you need to inherit from the BasePlugin class and implement the required methods. +""" +import importlib +import logging +from typing import List + + +class BasePlugin: + """ + Base class for all plugins. Defines the interface for plugin methods. + + Attributes: + None + + Methods: + register(cfg): Registers the plugin with the given configuration. + pre_model_load(cfg): Performs actions before the model is loaded. + post_model_load(cfg, model): Performs actions after the model is loaded. + pre_lora_load(cfg, model): Performs actions before LoRA weights are loaded. + post_lora_load(cfg, model): Performs actions after LoRA weights are loaded. + create_optimizer(cfg, trainer): Creates and returns an optimizer for training. + create_lr_scheduler(cfg, trainer, optimizer): Creates and returns a learning rate scheduler. + add_callbacks_pre_trainer(cfg, model): Adds callbacks to the trainer before training. + add_callbacks_post_trainer(cfg, trainer): Adds callbacks to the trainer after training. + """ + + def __init__(self): + """ + Initializes the BasePlugin. + """ + + def register(self, cfg): + """ + Registers the plugin with the given configuration. + + Parameters: + cfg (dict): The configuration for the plugin. + + Returns: + None + """ + + def get_input_args(self): + """ + Returns a pydantic model for the plugin's input arguments. + """ + + def pre_model_load(self, cfg): + """ + Performs actions before the model is loaded. + + Parameters: + cfg (dict): The configuration for the plugin. + + Returns: + None + """ + + def post_model_load(self, cfg, model): + """ + Performs actions after the model is loaded. + + Parameters: + cfg (dict): The configuration for the plugin. + model (object): The loaded model. + + Returns: + None + """ + + def pre_lora_load(self, cfg, model): + """ + Performs actions before LoRA weights are loaded. + + Parameters: + cfg (dict): The configuration for the plugin. + model (object): The loaded model. + + Returns: + None + """ + + def post_lora_load(self, cfg, model): + """ + Performs actions after LoRA weights are loaded. + + Parameters: + cfg (dict): The configuration for the plugin. + model (object): The loaded model. + + Returns: + None + """ + + def create_optimizer(self, cfg, trainer): + """ + Creates and returns an optimizer for training. + + Parameters: + cfg (dict): The configuration for the plugin. + trainer (object): The trainer object for training. + + Returns: + object: The created optimizer. + """ + + def create_lr_scheduler(self, cfg, trainer, optimizer): + """ + Creates and returns a learning rate scheduler. + + Parameters: + cfg (dict): The configuration for the plugin. + trainer (object): The trainer object for training. + optimizer (object): The optimizer for training. + + Returns: + object: The created learning rate scheduler. + """ + + def add_callbacks_pre_trainer(self, cfg, model): + """ + Adds callbacks to the trainer before training. + + Parameters: + cfg (dict): The configuration for the plugin. + model (object): The loaded model. + + Returns: + List[callable]: A list of callback functions to be added to the TrainingArgs + """ + + def add_callbacks_post_trainer(self, cfg, trainer): + """ + Adds callbacks to the trainer after training. + + Parameters: + cfg (dict): The configuration for the plugin. + trainer (object): The trainer object for training. + + Returns: + List[callable]: A list of callback functions to be added to the TrainingArgs + """ + + +def load_plugin(plugin_name: str) -> BasePlugin: + """ + Loads a plugin based on the given plugin name. + + The plugin name should be in the format "module_name.class_name". + This function splits the plugin name into module and class, imports the module, + retrieves the class from the module, and creates an instance of the class. + + Parameters: + plugin_name (str): The name of the plugin to be loaded. The name should be in the format "module_name.class_name". + + Returns: + BasePlugin: An instance of the loaded plugin. + + Raises: + ImportError: If the plugin module cannot be imported. + """ + # split the plugin name into module and class + module_name, class_name = plugin_name.rsplit(".", 1) + + # import the module + module = importlib.import_module(module_name) + # instantiate the class + plugin_class = getattr(module, class_name) + # create an instance of the class + plugin = plugin_class() + + return plugin + + +class PluginManager: + """ + The PluginManager class is responsible for loading and managing plugins. + It should be a singleton so it can be accessed from anywhere in the codebase. + + Attributes: + plugins (List[BasePlugin]): A list of loaded plugins. + + Methods: + get_instance(): Static method to get the singleton instance of PluginManager. + register(plugin_name: str): Registers a new plugin by its name. + pre_model_load(cfg): Calls the pre_model_load method of all registered plugins. + """ + + plugins: List[BasePlugin] = [] + + _instance = None + + def __new__(cls): + """ + Creates a new instance of PluginManager if it doesn't exist yet. + """ + if cls._instance is None: + cls._instance = super(PluginManager, cls).__new__(cls) + cls._instance.plugins: List[BasePlugin] = [] + return cls._instance + + @staticmethod + def get_instance() -> "PluginManager": + """ + Returns the singleton instance of PluginManager. + If the instance doesn't exist, it creates a new one. + """ + if PluginManager._instance is None: + PluginManager() + return PluginManager._instance # type: ignore + + def register(self, plugin_name: str): + """ + Registers a new plugin by its name. + + Parameters: + plugin_name (str): The name of the plugin to be registered. + + Returns: + None + + Raises: + ImportError: If the plugin module cannot be imported. + """ + try: + plugin = load_plugin(plugin_name) + self.plugins.append(plugin) + except ImportError: + logging.error(f"Failed to load plugin: {plugin_name}") + + def get_input_args(self): + """ + Returns a list of Pydantic classes for all registered plugins' input arguments.' + + Returns: + list[str]: A list of Pydantic classes for all registered plugins' input arguments.' + """ + input_args = [] + for plugin in self.plugins: + input_args_from_plugin = plugin.get_input_args() + if input_args_from_plugin is not None: + input_args.append(input_args_from_plugin) + return input_args + + def pre_model_load(self, cfg): + """ + Calls the pre_model_load method of all registered plugins. + + Parameters: + cfg (dict): The configuration for the plugins. + + Returns: + None + """ + for plugin in self.plugins: + plugin.pre_model_load(cfg) + + def post_model_load(self, cfg, model): + """ + Calls the post_model_load method of all registered plugins. + + Parameters: + cfg (dict): The configuration for the plugins. + model (object): The loaded model. + + Returns: + None + """ + for plugin in self.plugins: + plugin.post_model_load(cfg, model) + + def pre_lora_load(self, cfg, model): + """ + Calls the pre_lora_load method of all registered plugins. + + Parameters: + cfg (dict): The configuration for the plugins. + model (object): The loaded model. + + Returns: + None + """ + for plugin in self.plugins: + plugin.pre_lora_load(cfg, model) + + def post_lora_load(self, cfg, model): + """ + Calls the post_lora_load method of all registered plugins. + + Parameters: + cfg (dict): The configuration for the plugins. + model (object): The loaded model. + + Returns: + None + """ + for plugin in self.plugins: + plugin.post_lora_load(cfg, model) + + def create_optimizer(self, cfg, trainer): + """ + Calls the create_optimizer method of all registered plugins and returns the first non-None optimizer. + + Parameters: + cfg (dict): The configuration for the plugins. + trainer (object): The trainer object for training. + + Returns: + object: The created optimizer, or None if none was found. + """ + for plugin in self.plugins: + optimizer = plugin.create_optimizer(cfg, trainer) + if optimizer is not None: + return optimizer + return None + + def create_lr_scheduler(self, cfg, trainer, optimizer): + """ + Calls the create_lr_scheduler method of all registered plugins and returns the first non-None scheduler. + + Parameters: + cfg (dict): The configuration for the plugins. + trainer (object): The trainer object for training. + optimizer (object): The optimizer for training. + + Returns: + object: The created learning rate scheduler, or None if none was found. + """ + for plugin in self.plugins: + scheduler = plugin.create_lr_scheduler(cfg, trainer, optimizer) + if scheduler is not None: + return scheduler + return None + + def add_callbacks_pre_trainer(self, cfg, model): + """ + Calls the add_callbacks_pre_trainer method of all registered plugins. + + Parameters: + cfg (dict): The configuration for the plugins. + model (object): The loaded model. + + Returns: + List[callable]: A list of callback functions to be added to the TrainingArgs. + """ + callbacks = [] + for plugin in self.plugins: + callbacks.extend(plugin.add_callbacks_pre_trainer(cfg, model)) + return callbacks + + def add_callbacks_post_trainer(self, cfg, trainer): + """ + Calls the add_callbacks_post_trainer method of all registered plugins. + + Parameters: + cfg (dict): The configuration for the plugins. + trainer (object): The trainer object for training. + + Returns: + List[callable]: A list of callback functions to be added to the TrainingArgs. + """ + callbacks = [] + for plugin in self.plugins: + callbacks.extend(plugin.add_callbacks_post_trainer(cfg, trainer)) + return callbacks diff --git a/src/axolotl/integrations/config.py b/src/axolotl/integrations/config.py new file mode 100644 index 0000000000..b4ffd6758f --- /dev/null +++ b/src/axolotl/integrations/config.py @@ -0,0 +1,65 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# This software may be used and distributed according to +# the terms of the Axolotl Community License Agreement (the "License"); +# you may not use this file except in compliance with the License. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +""" +module to handle merging the plugins' input arguments with the base configurations. + +this was moved here to prevent circular imports +""" + +from typing import Any, Dict, List + +from axolotl.utils.config.models.input.v0_4_1 import ( + AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, +) +from axolotl.utils.config.models.input.v0_4_1 import ( + AxolotlInputConfig as AxolotlInputConfigBase, +) + + +def merge_input_args(): + """ + Merges input arguments from registered plugins with the base configurations. + + This function retrieves the input arguments from registered plugins using the PluginManager. + It then dynamically creates new classes, AxolotlConfigWCapabilities and AxolotlInputConfig, + that inherit from the base configurations and include the input arguments from the plugins. + + Returns: + tuple: A tuple containing the newly created classes, AxolotlConfigWCapabilities and AxolotlInputConfig. + """ + from axolotl.integrations.base import PluginManager + + plugin_manager = PluginManager.get_instance() + input_args: List[str] = plugin_manager.get_input_args() + plugin_classes = [] + dynamic_input = "" + for plugin_args in input_args: + plugin_module, plugin_cls = plugin_args.rsplit(".", 1) + dynamic_input += f"from {plugin_module} import {plugin_cls}\n" + plugin_classes.append(plugin_cls) + if dynamic_input: + dynamic_input += f"class AxolotlConfigWCapabilities(AxolotlConfigWCapabilitiesBase, {', '.join(plugin_classes)}):\n pass\n" + dynamic_input += f"class AxolotlInputConfig(AxolotlInputConfigBase, {', '.join(plugin_classes)}):\n pass\n" + + namespace: Dict[Any, Any] = {} + exec( # pylint: disable=exec-used # nosec B102 + dynamic_input, globals(), namespace + ) + AxolotlInputConfig = namespace[ # pylint: disable=invalid-name + "AxolotlInputConfig" + ] + AxolotlConfigWCapabilities = namespace[ # pylint: disable=invalid-name + "AxolotlConfigWCapabilities" + ] + return AxolotlConfigWCapabilities, AxolotlInputConfig + return AxolotlConfigWCapabilitiesBase, AxolotlInputConfigBase diff --git a/src/axolotl/integrations/liger/LICENSE b/src/axolotl/integrations/liger/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/src/axolotl/integrations/liger/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py new file mode 100644 index 0000000000..d4c1ad9a4d --- /dev/null +++ b/src/axolotl/integrations/liger/__init__.py @@ -0,0 +1,104 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module for the Plugin for LIGER integraton with Axolotl. + +Liger Kernel is the collection of Triton-native kernels for LLM Training. +It is designed to be performant, correct, and light-weight. +""" +import logging + +from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss +from liger_kernel.transformers.geglu import LigerGEGLUMLP +from liger_kernel.transformers.model.llama import lce_forward +from liger_kernel.transformers.rms_norm import LigerRMSNorm +from liger_kernel.transformers.rope import liger_rotary_pos_emb +from liger_kernel.transformers.swiglu import LigerSwiGLUMLP + +from axolotl.integrations.base import BasePlugin + +from .args import LigerArgs # pylint: disable=unused-import. # noqa: F401 + + +class LigerPlugin(BasePlugin): + """ + Plugin for LIGER integraton with Axolotl. + """ + + def get_input_args(self): + return "axolotl.integrations.liger.LigerArgs" + + def pre_model_load(self, cfg): + if cfg.model_config_type == "llama": + from transformers.models.llama import modeling_llama + + if cfg.liger_rope: + modeling_llama.apply_rotary_pos_emb = liger_rotary_pos_emb + if cfg.liger_rms_norm: + modeling_llama.LlamaRMSNorm = LigerRMSNorm + if cfg.liger_swiglu: + modeling_llama.LlamaMLP = LigerSwiGLUMLP + if cfg.liger_cross_entropy: + modeling_llama.CrossEntropyLoss = LigerCrossEntropyLoss + elif cfg.liger_fused_linear_cross_entropy: + modeling_llama.LlamaForCausalLM.forward = lce_forward + + elif cfg.model_config_type == "mistral": + from transformers.models.mistral import modeling_mistral + + if cfg.liger_rope: + modeling_mistral.apply_rotary_pos_emb = liger_rotary_pos_emb + if cfg.liger_rms_norm: + modeling_mistral.MistralRMSNorm = LigerRMSNorm + if cfg.liger_swiglu: + modeling_mistral.MistralMLP = LigerSwiGLUMLP + if cfg.liger_cross_entropy: + modeling_mistral.CrossEntropyLoss = LigerCrossEntropyLoss + if cfg.liger_fused_linear_cross_entropy: + logging.warning( + "Fused linear cross entropy is not supported for Mistral." + ) + + elif cfg.model_config_type == "gemma": + from transformers.models.gemma import modeling_gemma + + if cfg.liger_rope: + modeling_gemma.apply_rotary_pos_emb = liger_rotary_pos_emb + if cfg.liger_rms_norm: + modeling_gemma.GemmaRMSNorm = LigerRMSNorm + if cfg.liger_swiglu: + modeling_gemma.GemmaMLP = LigerGEGLUMLP + if cfg.liger_cross_entropy: + modeling_gemma.CrossEntropyLoss = LigerCrossEntropyLoss + if cfg.liger_fused_linear_cross_entropy: + logging.warning( + "Fused linear cross entropy is not supported for Gemma." + ) + + elif cfg.model_config_type == "jamba": + from transformers.models.jamba import modeling_jamba + + from .models.jamba import lce_forward as jamba_lce_forward + + if cfg.liger_rope: + modeling_jamba.apply_rotary_pos_emb = liger_rotary_pos_emb + if cfg.liger_rms_norm: + modeling_jamba.JambaRMSNorm = LigerRMSNorm + if cfg.liger_swiglu: + modeling_jamba.JambaMLP = LigerSwiGLUMLP + if cfg.liger_cross_entropy: + modeling_jamba.CrossEntropyLoss = LigerCrossEntropyLoss + if cfg.liger_fused_linear_cross_entropy: + modeling_jamba.JambaForCausalLM.forward = jamba_lce_forward diff --git a/src/axolotl/integrations/liger/args.py b/src/axolotl/integrations/liger/args.py new file mode 100644 index 0000000000..decdb37750 --- /dev/null +++ b/src/axolotl/integrations/liger/args.py @@ -0,0 +1,32 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module for handling LIGER input arguments. +""" +from typing import Optional + +from pydantic import BaseModel + + +class LigerArgs(BaseModel): + """ + Input args for LIGER. + """ + + liger_rope: Optional[bool] = None + liger_rms_norm: Optional[bool] = None + liger_swiglu: Optional[bool] = None + liger_cross_entropy: Optional[bool] = None + liger_fused_linear_cross_entropy: Optional[bool] = None diff --git a/src/axolotl/integrations/liger/models/jamba.py b/src/axolotl/integrations/liger/models/jamba.py new file mode 100644 index 0000000000..40cec63a4f --- /dev/null +++ b/src/axolotl/integrations/liger/models/jamba.py @@ -0,0 +1,173 @@ +""" +Jamba model with LigerFusedLinearCrossEntropyLoss +""" +# pylint: disable=duplicate-code + +from typing import Optional, Tuple, Union + +import torch +from liger_kernel.transformers.fused_linear_cross_entropy import ( + LigerFusedLinearCrossEntropyLoss, +) +from torch.nn import CrossEntropyLoss +from transformers.modeling_outputs import MoeCausalLMOutputWithPast +from transformers.models.jamba.modeling_jamba import ( + _CONFIG_FOR_DOC, + JAMBA_INPUTS_DOCSTRING, + HybridMambaAttentionDynamicCache, + load_balancing_loss_func, +) +from transformers.utils import ( + add_start_docstrings_to_model_forward, + replace_return_docstrings, +) + + +@add_start_docstrings_to_model_forward(JAMBA_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=MoeCausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def lce_forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[HybridMambaAttentionDynamicCache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + output_router_logits: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + num_logits_to_keep: Optional[Union[int, None]] = None, +) -> Union[Tuple, MoeCausalLMOutputWithPast]: + r""" + Args: + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + num_logits_to_keep (`int` or `None`, *optional*): + Calculate logits for the last `num_logits_to_keep` tokens. If `None`, calculate logits for all + `input_ids`. Only last token logits are needed for generation, and calculating them only for that token + can save memory, which becomes pretty significant for long sequences. + + Returns: + + Example: + + ```python + >>> from transformers import AutoTokenizer, JambaForCausalLM + + >>> model = JambaForCausalLM.from_pretrained("ai21labs/Jamba-v0.1") + >>> tokenizer = AutoTokenizer.from_pretrained("ai21labs/Jamba-v0.1") + + >>> prompt = "Hey, are you conscious? Can you talk to me?" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." + ```""" + + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_router_logits = ( + output_router_logits + if output_router_logits is not None + else self.config.output_router_logits + ) + + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + output_router_logits=output_router_logits, + cache_position=cache_position, + return_dict=return_dict, + ) + + hidden_states = outputs[0] + + loss = None + logits = None + + if self.training: + shift_hidden_states = hidden_states[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + + # flatten tokens + shift_hidden_states = shift_hidden_states.view(-1, self.config.hidden_size) + shift_labels = shift_labels.view(-1) + + lce = LigerFusedLinearCrossEntropyLoss() + loss = lce(self.lm_head.weight, shift_hidden_states, shift_labels) + else: + if num_logits_to_keep is None: + logits = self.lm_head(hidden_states) + else: + logits = self.lm_head(hidden_states[..., -num_logits_to_keep:, :]) + logits = logits.float() + + if labels is not None: + # Shift so that tokens < n predict n + shift_logits = logits[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + # Flatten the tokens + loss_fct = CrossEntropyLoss() + shift_logits = shift_logits.view(-1, self.config.vocab_size) + shift_labels = shift_labels.view(-1) + # Enable model parallelism + shift_labels = shift_labels.to(shift_logits.device) + loss = loss_fct(shift_logits, shift_labels) + + aux_loss = None + if output_router_logits: + aux_loss = load_balancing_loss_func( + outputs.router_logits if return_dict else outputs[-1], + self.num_experts, + self.num_experts_per_tok, + attention_mask, + ) + if labels is not None: + loss += self.router_aux_loss_coef * aux_loss.to( + loss.device + ) # make sure to reside in the same device + + if not return_dict: + output = (logits,) + outputs[1:] + if output_router_logits: + output = (aux_loss,) + output + return (loss,) + output if loss is not None else output + + return MoeCausalLMOutputWithPast( + loss=loss, + aux_loss=aux_loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + router_logits=outputs.router_logits, + ) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index ed165e89ca..82436e8d79 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -8,11 +8,14 @@ import torch from transformers.utils import is_torch_bf16_gpu_available +from axolotl.integrations.config import merge_input_args from axolotl.utils.bench import log_gpu_memory_usage +from axolotl.utils.config.models.input.v0_4_1 import SUPPORTED_METRICS from axolotl.utils.config.models.input.v0_4_1 import ( - SUPPORTED_METRICS, - AxolotlConfigWCapabilities, - AxolotlInputConfig, + AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, +) +from axolotl.utils.config.models.input.v0_4_1 import ( + AxolotlInputConfig as AxolotlInputConfigBase, ) from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_model_config @@ -207,6 +210,15 @@ def normalize_cfg_datasets(cfg): def validate_config(cfg: DictDefault, capabilities: Optional[dict] = None): + AxolotlConfigWCapabilities = AxolotlConfigWCapabilitiesBase + AxolotlInputConfig = AxolotlInputConfigBase + + if cfg.plugins: + ( + AxolotlConfigWCapabilities, # pylint: disable=invalid-name + AxolotlInputConfig, # pylint: disable=invalid-name + ) = merge_input_args() + if capabilities: return DictDefault( dict( diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 8d24524a23..6261ce20fe 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -308,10 +308,17 @@ def load_model( """ Load a model for a given configuration and tokenizer. """ + base_model = cfg.base_model model_type = cfg.type_of_model model_config = load_model_config(cfg) + # load any patches from plugins + from axolotl.integrations.base import PluginManager + + plugin_manager = PluginManager.get_instance() + plugin_manager.pre_model_load(cfg) + # TODO refactor as a kwarg load_in_8bit = cfg.load_in_8bit diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 99c10c6558..f4e1fc6cb8 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -217,6 +217,24 @@ def process_datasets_for_packing(cfg, train_dataset, eval_dataset): desc="Dropping Long Sequences", ) + # drop samples with where the number of elements with labels not equal to -100 is zero + def drop_no_trainable_tokens(sample): + return np.sum(np.array(sample["labels"]) != -100) > 0 + + train_dataset = train_dataset.filter( + drop_no_trainable_tokens, + num_proc=cfg.dataset_processes, + load_from_cache_file=not cfg.is_preprocess, + desc="Drop Samples with Zero Trainable Tokens", + ) + if eval_dataset: + eval_dataset = eval_dataset.filter( + drop_no_trainable_tokens, + num_proc=cfg.dataset_processes, + load_from_cache_file=not cfg.is_preprocess, + desc="Drop Samples with Zero Trainable Tokens", + ) + if cfg.group_by_length: train_dataset = train_dataset.map( add_length, From da0d581a8cbe26b9a24b8e479751f1e87687ae27 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 23 Aug 2024 12:37:50 -0400 Subject: [PATCH 0136/1405] add liger example (#1864) --- examples/llama-3/fft-8b-liger-fsdp.yaml | 76 +++++++++++++++++++++++++ examples/llama-3/fft-8b.yaml | 4 +- 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 examples/llama-3/fft-8b-liger-fsdp.yaml diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml new file mode 100644 index 0000000000..a64965d207 --- /dev/null +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -0,0 +1,76 @@ +base_model: NousResearch/Meta-Llama-3.1-8B + +plugins: + - axolotl.integrations.liger.LigerPlugin +liger_rope: true +liger_rms_norm: true +liger_swiglu: true +liger_fused_linear_cross_entropy: true + +strict: false + +chat_template: llama3 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] +dataset_prepared_path: last_run_prepared +val_set_size: 0.02 +output_dir: ./outputs/out + +sequence_len: 4096 +sample_packing: true +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: paged_adamw_8bit +lr_scheduler: cosine +learning_rate: 2e-5 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +early_stopping_patience: +resume_from_checkpoint: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_steps: 100 +evals_per_epoch: 2 +eval_table_size: +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_limit_all_gathers: true + fsdp_sync_module_states: true + fsdp_offload_params: true + fsdp_use_orig_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD + fsdp_backward_prefetch: BACKWARD_PRE +special_tokens: + pad_token: <|finetune_right_pad_id|> + eos_token: <|eot_id|> diff --git a/examples/llama-3/fft-8b.yaml b/examples/llama-3/fft-8b.yaml index 908ef6e035..335902aac7 100644 --- a/examples/llama-3/fft-8b.yaml +++ b/examples/llama-3/fft-8b.yaml @@ -1,6 +1,4 @@ -base_model: NousResearch/Meta-Llama-3-8B -model_type: LlamaForCausalLM -tokenizer_type: AutoTokenizer +base_model: NousResearch/Meta-Llama-3.1-8B load_in_8bit: false load_in_4bit: false From 810ecd4e81a93a8cd8d740c4e59f12fa05424f69 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 23 Aug 2024 14:34:03 -0400 Subject: [PATCH 0137/1405] add liger to readme (#1865) * add liger to readme * updates from PR feedback --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 8c70da015d..af604fad50 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Features: - [FSDP + QLoRA](#fsdp--qlora) - [Weights \& Biases Logging](#weights--biases-logging) - [Special Tokens](#special-tokens) + - [Liger Kernel](#liger-kernel) - [Inference Playground](#inference-playground) - [Merge LORA to base](#merge-lora-to-base) - [Common Errors 🧰](#common-errors-) @@ -530,6 +531,25 @@ tokens: # these are delimiters When you include these tokens in your axolotl config, axolotl adds these tokens to the tokenizer's vocabulary. +##### Liger Kernel + +Liger Kernel: Efficient Triton Kernels for LLM Training + +https://github.com/linkedin/Liger-Kernel + +Liger (LinkedIn GPU Efficient Runtime) Kernel is a collection of Triton kernels designed specifically for LLM training. +It can effectively increase multi-GPU training throughput by 20% and reduces memory usage by 60%. The Liger Kernel +composes well and is compatible with both FSDP and Deepspeed. + +```yaml +plugins: + - axolotl.integrations.liger.LigerPlugin +liger_rope: true +liger_rms_norm: true +liger_swiglu: true +liger_fused_linear_cross_entropy: true +``` + ### Inference Playground Axolotl allows you to load your model in an interactive terminal playground for quick experimentation. From 77a4b9cda21deabf97515fb04788b4eec7ed7783 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 23 Aug 2024 17:00:01 -0400 Subject: [PATCH 0138/1405] change up import to prevent AttributeError (#1863) * change up import to prevent AttributeError * tweak patching check for updated upstream --- src/axolotl/monkeypatch/llama_patch_multipack.py | 12 ++++++------ src/axolotl/monkeypatch/unsloth_.py | 6 ++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/axolotl/monkeypatch/llama_patch_multipack.py b/src/axolotl/monkeypatch/llama_patch_multipack.py index 540c5577a0..cfd525367e 100644 --- a/src/axolotl/monkeypatch/llama_patch_multipack.py +++ b/src/axolotl/monkeypatch/llama_patch_multipack.py @@ -9,18 +9,18 @@ def hijack_llama_prepare_4d_mask(): - import transformers.modeling_attn_mask_utils - import transformers.models.llama.modeling_llama + from transformers import modeling_attn_mask_utils + from transformers.models.llama import modeling_llama - transformers.models.llama.modeling_llama._prepare_4d_causal_attention_mask_for_sdpa = ( # pylint: disable=protected-access + modeling_llama._prepare_4d_causal_attention_mask_for_sdpa = ( # pylint: disable=protected-access patched_prepare_4d_causal_attention_mask_for_sdpa ) - transformers.modeling_attn_mask_utils._prepare_4d_causal_attention_mask_for_sdpa = ( # pylint: disable=protected-access + modeling_attn_mask_utils._prepare_4d_causal_attention_mask_for_sdpa = ( # pylint: disable=protected-access patched_prepare_4d_causal_attention_mask_for_sdpa ) - transformers.models.llama.modeling_llama._prepare_4d_causal_attention_mask = ( # pylint: disable=protected-access + modeling_llama._prepare_4d_causal_attention_mask = ( # pylint: disable=protected-access patched_prepare_4d_causal_attention_mask ) - transformers.modeling_attn_mask_utils._prepare_4d_causal_attention_mask = ( # pylint: disable=protected-access + modeling_attn_mask_utils._prepare_4d_causal_attention_mask = ( # pylint: disable=protected-access patched_prepare_4d_causal_attention_mask ) diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py index 5b1f0061de..3d42ad17f1 100644 --- a/src/axolotl/monkeypatch/unsloth_.py +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -16,8 +16,7 @@ LOG = get_logger("axolotl.monkeypatch.unsloth") -ORIGINAL_CEL_CODE = """ if labels is not None: - # Shift so that tokens < n predict n +ORIGINAL_CEL_CODE = """# Shift so that tokens < n predict n shift_logits = logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() # Flatten the tokens @@ -29,8 +28,7 @@ loss = loss_fct(shift_logits, shift_labels) """ -PATCHED_CEL_CODE = """ if labels is not None: - shift_logits = logits[..., :-1, :].contiguous() +PATCHED_CEL_CODE = """shift_logits = logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() loss = fast_cross_entropy_loss( logits = shift_logits, From 22f4eafa557bc5009877443c601e40a762832c2b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 23 Aug 2024 20:23:08 -0400 Subject: [PATCH 0139/1405] simplify logic (#1856) --- src/axolotl/utils/models.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 6261ce20fe..e183301991 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -589,19 +589,12 @@ def load_model( # sample packing uses custom FA2 patch if cfg.flash_attention: - if not cfg.sample_packing: - if cfg.s2_attention: - pass - # most other models support flash attention, we can define exceptions as they come up - model_kwargs["attn_implementation"] = "flash_attention_2" - model_config._attn_implementation = ( # pylint: disable=protected-access - "flash_attention_2" - ) - else: - model_kwargs["attn_implementation"] = "flash_attention_2" - model_config._attn_implementation = ( # pylint: disable=protected-access - "flash_attention_2" - ) + if not cfg.sample_packing and cfg.s2_attention: + pass + model_kwargs["attn_implementation"] = "flash_attention_2" + model_config._attn_implementation = ( # pylint: disable=protected-access + "flash_attention_2" + ) elif cfg.sdp_attention: model_kwargs["attn_implementation"] = "sdpa" model_config._attn_implementation = "sdpa" # pylint: disable=protected-access From f245964f22f5bceeed27cb4e0374f1c56c6d63d5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 25 Aug 2024 12:31:40 -0400 Subject: [PATCH 0140/1405] better handling of llama-3 tool rolw (#1782) --- src/axolotl/prompters.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/axolotl/prompters.py b/src/axolotl/prompters.py index 13ff450f8a..18b73e725e 100644 --- a/src/axolotl/prompters.py +++ b/src/axolotl/prompters.py @@ -352,9 +352,12 @@ def _build_result(self, source): "Please help us by creating an Issue to add support for this conversation type." ) - role = CONVERSATION_ROLE_FORMAT[self._conversation.name].format( - ROLE=from_role - ) + if self._conversation.name in ["llama3"]: + role = from_role + else: + role = CONVERSATION_ROLE_FORMAT[self._conversation.name].format( + ROLE=from_role + ) if len(conv.messages) > 0 and ((role == conv.messages[-1][0])): if ( From 8e29bdefdd32757dd952f9a295e961c3f1c70ba2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 25 Aug 2024 17:54:02 -0400 Subject: [PATCH 0141/1405] Spectrum plugin (#1866) --- src/axolotl/integrations/spectrum/LICENSE | 202 ++++++++++++++++++ src/axolotl/integrations/spectrum/README.md | 21 ++ src/axolotl/integrations/spectrum/__init__.py | 102 +++++++++ src/axolotl/integrations/spectrum/args.py | 29 +++ 4 files changed, 354 insertions(+) create mode 100644 src/axolotl/integrations/spectrum/LICENSE create mode 100644 src/axolotl/integrations/spectrum/README.md create mode 100644 src/axolotl/integrations/spectrum/__init__.py create mode 100644 src/axolotl/integrations/spectrum/args.py diff --git a/src/axolotl/integrations/spectrum/LICENSE b/src/axolotl/integrations/spectrum/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/src/axolotl/integrations/spectrum/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/axolotl/integrations/spectrum/README.md b/src/axolotl/integrations/spectrum/README.md new file mode 100644 index 0000000000..de17db5127 --- /dev/null +++ b/src/axolotl/integrations/spectrum/README.md @@ -0,0 +1,21 @@ +## Spectrum: Targeted Training on Signal to Noise Ratio + +by Eric Hartford, Lucas Atkins, et al. + +This plugin contains code to freeze the bottom fraction of modules in a model, based on the Signal-to-Noise Ratio (SNR). + +### Overview + +Spectrum is a tool for scanning and evaluating the Signal-to-Noise Ratio (SNR) of layers in large language models. +By identifying the top n% of layers with the highest SNR, you can optimize training efficiency. + +### Usage + +```yaml +plugins: + - axolotl.integrations.spectrum.SpectrumPlugin + +spectrum_top_fraction: 0.5 +# Optional if using a pre-scanned model as your base_model. Useful if using a model mirror +spectrum_model_name: meta-llama/Meta-Llama-3.1-8B +``` diff --git a/src/axolotl/integrations/spectrum/__init__.py b/src/axolotl/integrations/spectrum/__init__.py new file mode 100644 index 0000000000..6059e7951c --- /dev/null +++ b/src/axolotl/integrations/spectrum/__init__.py @@ -0,0 +1,102 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Spectrum Plugin to automatically generate unfrozen parameters based on SNR data. +""" + +import json +import logging + +import requests + +from axolotl.integrations.base import BasePlugin + +from .args import SpectrumArgs # pylint: disable=unused-import. # noqa: F401 + + +def _generate_unfrozen_params_yaml(snr_data, top_fraction=0.5): + unfrozen_parameters = {} + for layer_name, info in snr_data.items(): + layer_type = info["type"] + if layer_type not in unfrozen_parameters: + unfrozen_parameters[layer_type] = [] + unfrozen_parameters[layer_type].append((layer_name, info["snr"])) + top_layers_by_type = {} + for layer_type, layers in unfrozen_parameters.items(): + layers_sorted = sorted(layers, key=lambda x: x[1], reverse=True) + num_top_layers = int(len(layers) * top_fraction) + top_layers_by_type[layer_type] = [ + layer[0] for layer in layers_sorted[:num_top_layers] + ] + unfrozen_parameters = [ + "^lm_head.weight$", + "^model.embed_tokens.weight$", + ] + for layer_type, layer_names in top_layers_by_type.items(): + for layer_name in layer_names: + unfrozen_parameters.append(layer_name) + return unfrozen_parameters + + +class SpectrumPlugin(BasePlugin): + """ + Spectrum Plugin to automatically generate unfrozen parameters based on SNR data. + """ + + base_url = "https://raw.githubusercontent.com/cognitivecomputations/spectrum/main/model_snr_results/" + base_path = "./model_snr_results/" + snr_file_template = "snr_results_{model_name_slug}.json" + + def get_input_args(self): + return "axolotl.integrations.spectrum.SpectrumArgs" + + def pre_model_load(self, cfg): + if cfg.get("spectrum_model_name"): + model_name = cfg["spectrum_model_name"] + else: + model_name = cfg["base_model"] + top_fraction = cfg.get("spectrum_top_fraction", 50) + model_slug = model_name.replace("/", "-").replace("_", "-") + snr_url = self.base_url + self.snr_file_template.format( + model_name_slug=model_slug + ) + snr_path = self.base_path + self.snr_file_template.format( + model_name_slug=model_slug + ) + # first check if the files exist locally and read the json + snr_data = None + try: + with open(snr_path, "r", encoding="utf-8") as fin: + snr_data = json.load(fin) + except FileNotFoundError: + pass + except Exception as exc: # pylint: disable=broad-exception-caught + logging.warning(f"Failed to read SNR data from {snr_path}: {exc}") + + if not snr_data: + try: + snr_data = requests.get(snr_url, timeout=60).json() + except requests.exceptions.RequestException as exc: + logging.warning(f"Failed to fetch SNR data from {snr_url}: {exc}") + return + # also catch json parsing errors + except json.JSONDecodeError as exc: + logging.warning(f"Failed to parse SNR data from {snr_url}: {exc}") + return + + unfrozen_parameters = _generate_unfrozen_params_yaml( + snr_data, top_fraction=top_fraction + ) + cfg["unfrozen_parameters"] = unfrozen_parameters diff --git a/src/axolotl/integrations/spectrum/args.py b/src/axolotl/integrations/spectrum/args.py new file mode 100644 index 0000000000..03426d8413 --- /dev/null +++ b/src/axolotl/integrations/spectrum/args.py @@ -0,0 +1,29 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module for handling Spectrum input arguments. +""" +from typing import Optional + +from pydantic import BaseModel + + +class SpectrumArgs(BaseModel): + """ + Input args for Spectrum. + """ + + spectrum_top_fraction: Optional[float] = 0.5 + spectrum_model_name: Optional[str] = None From 6819c12cee9248defe5bb5d6690aa4bfc03e5351 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 26 Aug 2024 12:00:36 -0400 Subject: [PATCH 0142/1405] update specturm authors (#1869) --- src/axolotl/integrations/spectrum/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/integrations/spectrum/README.md b/src/axolotl/integrations/spectrum/README.md index de17db5127..192918060e 100644 --- a/src/axolotl/integrations/spectrum/README.md +++ b/src/axolotl/integrations/spectrum/README.md @@ -1,6 +1,6 @@ ## Spectrum: Targeted Training on Signal to Noise Ratio -by Eric Hartford, Lucas Atkins, et al. +by Eric Hartford, Lucas Atkins, Fernando Fernandes, David Golchinfar This plugin contains code to freeze the bottom fraction of modules in a model, based on the Signal-to-Noise Ratio (SNR). From 2dac1edf7225cc75bd781d78a7d0cca33bd8560f Mon Sep 17 00:00:00 2001 From: Chiwan Park Date: Tue, 27 Aug 2024 01:56:12 +0900 Subject: [PATCH 0143/1405] Fix `drop_long_seq` bug due to truncation in prompt tokenization strategies when using `chat_template` (#1867) --- src/axolotl/prompt_strategies/chat_template.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 8ae668d7e9..19e36531a5 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -350,7 +350,8 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): ), "roles": ds_cfg.get("roles"), "drop_system_message": ds_cfg.get("drop_system_message", False), - "max_length": cfg.sequence_len, + # we need to add one for detecting sequences with exceeding the `sequence_len` limit. + "max_length": cfg.sequence_len + 1, } strategy_params = { From 17af1d7081414c32614cbabe324e1197ca9f43a7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 26 Aug 2024 15:50:26 -0400 Subject: [PATCH 0144/1405] clear cuda cache to help with memory leak/creep (#1858) * clear cuda cache to help with memory leak/creep * reverse order of gc --- src/axolotl/core/trainer_builder.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 1a073ca047..656ded2559 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -4,6 +4,7 @@ """ import abc +import gc import importlib import importlib.util import logging @@ -15,11 +16,12 @@ from dataclasses import dataclass, field from functools import wraps from pathlib import Path -from typing import Dict, List, Literal, Optional, Type, Union +from typing import Any, Dict, List, Literal, Optional, Type, Union import torch import transformers from datasets import Dataset +from torch import nn from torch.optim.lr_scheduler import OneCycleLR from torch.utils.data import BatchSampler, DataLoader, RandomSampler, SequentialSampler from transformers import ( @@ -997,6 +999,14 @@ def tokenize_row( res[key] = res[key][1:] return res + def training_step( + self, model: nn.Module, inputs: Dict[str, Union[torch.Tensor, Any]] + ) -> torch.Tensor: + loss: torch.Tensor = super().training_step(model, inputs) + gc.collect() + torch.cuda.empty_cache() + return loss + class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): """ From f6362d2a05cf87e9c1615d503751e059f1b110d0 Mon Sep 17 00:00:00 2001 From: Chiwan Park Date: Wed, 28 Aug 2024 02:03:16 +0900 Subject: [PATCH 0145/1405] Add Liger Kernal support for Qwen2 (#1871) --- src/axolotl/integrations/liger/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index d4c1ad9a4d..bf4c83af4f 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -23,6 +23,7 @@ from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss from liger_kernel.transformers.geglu import LigerGEGLUMLP from liger_kernel.transformers.model.llama import lce_forward +from liger_kernel.transformers.model.qwen2 import lce_forward as qwen2_lce_forward from liger_kernel.transformers.rms_norm import LigerRMSNorm from liger_kernel.transformers.rope import liger_rotary_pos_emb from liger_kernel.transformers.swiglu import LigerSwiGLUMLP @@ -102,3 +103,17 @@ def pre_model_load(self, cfg): modeling_jamba.CrossEntropyLoss = LigerCrossEntropyLoss if cfg.liger_fused_linear_cross_entropy: modeling_jamba.JambaForCausalLM.forward = jamba_lce_forward + + elif cfg.model_config_type == "qwen2": + from transformers.models.qwen2 import modeling_qwen2 + + if cfg.liger_rope: + modeling_qwen2.apply_rotary_pos_emb = liger_rotary_pos_emb + if cfg.liger_rms_norm: + modeling_qwen2.Qwen2RMSNorm = LigerRMSNorm + if cfg.liger_swiglu: + modeling_qwen2.Qwen2MLP = LigerSwiGLUMLP + if cfg.liger_cross_entropy: + modeling_qwen2.CrossEntropyLoss = LigerCrossEntropyLoss + if cfg.liger_fused_linear_cross_entropy: + modeling_qwen2.Qwen2ForCausalLM.forward = qwen2_lce_forward From 1e4366070179f238a40b7ef8356be744350c1a38 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 27 Aug 2024 13:39:24 -0400 Subject: [PATCH 0146/1405] Sample pack trust remote code v2 (#1873) * fix the multipack patch for remote code models * add deepseek v2 lite example w fsdp --- examples/deepseek-v2/fft-fsdp-16b.yaml | 67 ++++++++++++++++++++++++++ src/axolotl/monkeypatch/multipack.py | 2 + src/axolotl/monkeypatch/utils.py | 2 - 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 examples/deepseek-v2/fft-fsdp-16b.yaml diff --git a/examples/deepseek-v2/fft-fsdp-16b.yaml b/examples/deepseek-v2/fft-fsdp-16b.yaml new file mode 100644 index 0000000000..b55646df7f --- /dev/null +++ b/examples/deepseek-v2/fft-fsdp-16b.yaml @@ -0,0 +1,67 @@ +base_model: deepseek-ai/DeepSeek-V2-Lite +trust_remote_code: true + +load_in_8bit: false +load_in_4bit: false +strict: false + +datasets: + - path: tatsu-lab/alpaca + type: alpaca +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +sequence_len: 2048 +sample_packing: true +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 8 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch +lr_scheduler: cosine +learning_rate: 2e-5 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +early_stopping_patience: +resume_from_checkpoint: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_steps: 100 +evals_per_epoch: 2 +eval_table_size: +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +special_tokens: +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_limit_all_gathers: true + fsdp_sync_module_states: true + fsdp_offload_params: true + fsdp_use_orig_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: DeepseekV2DecoderLayer + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 44fc4cb473..529c42a8f5 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -94,3 +94,5 @@ def patch_remote(model_name, config_name, modeling_name): module_name = model_config.__class__.__module__.replace(config_name, modeling_name) modeling_arch = importlib.import_module(module_name) modeling_arch._get_unpad_data = get_unpad_data # pylint: disable=protected-access + # workaround to make the patch stick + modeling_arch._axolotl_multipack_patch = True # pylint: disable=protected-access diff --git a/src/axolotl/monkeypatch/utils.py b/src/axolotl/monkeypatch/utils.py index e43c58650a..f29f21be77 100644 --- a/src/axolotl/monkeypatch/utils.py +++ b/src/axolotl/monkeypatch/utils.py @@ -17,11 +17,9 @@ def get_max_seqlen_in_batch(attention_mask: torch.Tensor) -> torch.Tensor: max_num = int(torch.max(attention_mask).item()) batch_size, _ = attention_mask.shape counts = torch.zeros((batch_size, max_num), dtype=torch.int32) - for i in range(1, max_num + 1): mask = attention_mask == i counts[:, i - 1] = torch.sum(mask, dim=-1).to(dtype=torch.int32) - result = counts.flatten() nonzero_indices = torch.nonzero(result).squeeze(-1) return result[nonzero_indices] From 159b8b9a74af6745a2495138c1cdcaf0cc666ab8 Mon Sep 17 00:00:00 2001 From: Aman Gupta Karmani Date: Tue, 27 Aug 2024 17:22:26 -0700 Subject: [PATCH 0147/1405] monkey-patch transformers to simplify monkey-patching modeling code (#1877) * monkey-patch transformers so that monkey-patched modeling code doesnt get overwritten * unnecessary now * add comment --- src/axolotl/monkeypatch/multipack.py | 2 - .../transformers_dynamic_module_utils.py | 51 +++++++++++++++++++ src/axolotl/utils/models.py | 5 ++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 src/axolotl/monkeypatch/transformers_dynamic_module_utils.py diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 529c42a8f5..44fc4cb473 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -94,5 +94,3 @@ def patch_remote(model_name, config_name, modeling_name): module_name = model_config.__class__.__module__.replace(config_name, modeling_name) modeling_arch = importlib.import_module(module_name) modeling_arch._get_unpad_data = get_unpad_data # pylint: disable=protected-access - # workaround to make the patch stick - modeling_arch._axolotl_multipack_patch = True # pylint: disable=protected-access diff --git a/src/axolotl/monkeypatch/transformers_dynamic_module_utils.py b/src/axolotl/monkeypatch/transformers_dynamic_module_utils.py new file mode 100644 index 0000000000..dfc3e29c5a --- /dev/null +++ b/src/axolotl/monkeypatch/transformers_dynamic_module_utils.py @@ -0,0 +1,51 @@ +"""Patch transformers.dynamic_module_utils.get_class_in_module to avoid reloading models from disk""" + +import importlib +import os +import sys +import typing +from pathlib import Path + +from transformers.file_utils import HF_MODULES_CACHE + + +def _patched_get_class_in_module( + class_name: str, module_path: typing.Union[str, os.PathLike] +) -> typing.Type: + """ + Import a module on the cache directory for modules and extract a class from it. + + Args: + class_name (`str`): The name of the class to import. + module_path (`str` or `os.PathLike`): The path to the module to import. + + Returns: + `typing.Type`: The class looked for. + """ + name = os.path.normpath(module_path) + if name.endswith(".py"): + name = name[:-3] + name = name.replace(os.path.sep, ".") + module_spec = importlib.util.spec_from_file_location( + name, location=Path(HF_MODULES_CACHE) / module_path + ) + module = sys.modules.get(name) + if module is None: + module = importlib.util.module_from_spec(module_spec) + # insert it into sys.modules before any loading begins + sys.modules[name] = module + # load in initial case only + module_spec.loader.exec_module(module) + return getattr(module, class_name) + + +def patch_transformers_dynamic_module_utils(): + """ + Recently, transformers started reloading modeling code from disk for models marked trust_remote_code=True. + This causes monkey-patches for multipack and liger to be removed. + We replace the original function with a version that does not reload the module from disk. + See https://github.com/huggingface/transformers/pull/30370#pullrequestreview-2264361581 + """ + import transformers + + transformers.dynamic_module_utils.get_class_in_module = _patched_get_class_in_module diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index e183301991..e0526fb048 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -43,6 +43,9 @@ SUPPORTED_MULTIPACK_MODEL_TYPES, patch_for_multipack, ) +from axolotl.monkeypatch.transformers_dynamic_module_utils import ( + patch_transformers_dynamic_module_utils, +) from axolotl.prompt_tokenizers import LLAMA_DEFAULT_EOS_TOKEN from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.chat_templates import chat_templates @@ -54,6 +57,8 @@ LOG = logging.getLogger("axolotl") +patch_transformers_dynamic_module_utils() + # copied from accelerator.FullyShardedDataParallelPlugin def get_module_class_from_name(module, name): From c1a61ae23c8967952e445fef0185e13d72d28dd2 Mon Sep 17 00:00:00 2001 From: Aman Gupta Karmani Date: Tue, 27 Aug 2024 20:08:26 -0700 Subject: [PATCH 0148/1405] fix liger plugin load issues (#1876) --- src/axolotl/integrations/liger/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index bf4c83af4f..f78083300d 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -22,8 +22,7 @@ from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss from liger_kernel.transformers.geglu import LigerGEGLUMLP -from liger_kernel.transformers.model.llama import lce_forward -from liger_kernel.transformers.model.qwen2 import lce_forward as qwen2_lce_forward +from liger_kernel.transformers.model.llama import lce_forward as llama_lce_forward from liger_kernel.transformers.rms_norm import LigerRMSNorm from liger_kernel.transformers.rope import liger_rotary_pos_emb from liger_kernel.transformers.swiglu import LigerSwiGLUMLP @@ -54,7 +53,7 @@ def pre_model_load(self, cfg): if cfg.liger_cross_entropy: modeling_llama.CrossEntropyLoss = LigerCrossEntropyLoss elif cfg.liger_fused_linear_cross_entropy: - modeling_llama.LlamaForCausalLM.forward = lce_forward + modeling_llama.LlamaForCausalLM.forward = llama_lce_forward elif cfg.model_config_type == "mistral": from transformers.models.mistral import modeling_mistral @@ -105,6 +104,9 @@ def pre_model_load(self, cfg): modeling_jamba.JambaForCausalLM.forward = jamba_lce_forward elif cfg.model_config_type == "qwen2": + from liger_kernel.transformers.model.qwen2 import ( + lce_forward as qwen2_lce_forward, + ) from transformers.models.qwen2 import modeling_qwen2 if cfg.liger_rope: From 7037e3c836960b315b469ab5659c1695b2fe0583 Mon Sep 17 00:00:00 2001 From: Aman Gupta Karmani Date: Tue, 27 Aug 2024 20:52:40 -0700 Subject: [PATCH 0149/1405] deepseekv2 liger support (#1878) * deepseekv2 liger support * add comment * add missing impl --- src/axolotl/integrations/liger/__init__.py | 26 ++++ .../integrations/liger/models/deepseekv2.py | 127 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 src/axolotl/integrations/liger/models/deepseekv2.py diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index f78083300d..2a3e95163b 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -19,6 +19,7 @@ It is designed to be performant, correct, and light-weight. """ import logging +import sys from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss from liger_kernel.transformers.geglu import LigerGEGLUMLP @@ -119,3 +120,28 @@ def pre_model_load(self, cfg): modeling_qwen2.CrossEntropyLoss = LigerCrossEntropyLoss if cfg.liger_fused_linear_cross_entropy: modeling_qwen2.Qwen2ForCausalLM.forward = qwen2_lce_forward + + elif cfg.model_config_type == "deepseek_v2": + from accelerate import init_empty_weights + from transformers import AutoModelForCausalLM + + with init_empty_weights(): + model = AutoModelForCausalLM.from_pretrained( + cfg.base_model, trust_remote_code=cfg.trust_remote_code or False + ) + modeling_mod = sys.modules[model.__class__.__module__] + + from .models.deepseekv2 import lce_forward as deepseekv2_lce_forward + + if cfg.liger_rope: + # The DeepseekV2 version of RoPE is different than upstream LLaMA. + # See https://github.com/linkedin/Liger-Kernel/issues/129#issuecomment-2313763528 + logging.warning("Fused liger_rope is not supported for DeepseekV2.") + if cfg.liger_rms_norm: + modeling_mod.DeepseekV2RMSNorm = LigerRMSNorm + if cfg.liger_swiglu: + modeling_mod.DeepseekV2MLP.forward = LigerSwiGLUMLP.forward + if cfg.liger_cross_entropy: + modeling_mod.CrossEntropyLoss = LigerCrossEntropyLoss + if cfg.liger_fused_linear_cross_entropy: + modeling_mod.DeepseekV2ForCausalLM.forward = deepseekv2_lce_forward diff --git a/src/axolotl/integrations/liger/models/deepseekv2.py b/src/axolotl/integrations/liger/models/deepseekv2.py new file mode 100644 index 0000000000..79fb274360 --- /dev/null +++ b/src/axolotl/integrations/liger/models/deepseekv2.py @@ -0,0 +1,127 @@ +""" +DeepseekV2 model with LigerFusedLinearCrossEntropyLoss +""" +# pylint: disable=duplicate-code + +from typing import List, Optional, Tuple, Union + +import torch +from liger_kernel.transformers.fused_linear_cross_entropy import ( + LigerFusedLinearCrossEntropyLoss, +) +from torch.nn import CrossEntropyLoss +from transformers.modeling_outputs import CausalLMOutputWithPast + + +# @add_start_docstrings_to_model_forward(DeepseekV2_INPUTS_DOCSTRING) +# @replace_return_docstrings( +# output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +# ) +def lce_forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[List[torch.FloatTensor]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, +) -> Union[Tuple, CausalLMOutputWithPast]: + r""" + Args: + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, transformers., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, transformers., config.vocab_size]`. + + Returns: + + Example: + + ```python + >>> from transformers import AutoTokenizer, DeepseekV2ForCausalLM + + >>> model = DeepseekV2ForCausalLM.from_pretrained(PATH_TO_CONVERTED_WEIGHTS) + >>> tokenizer = AutoTokenizer.from_pretrained(PATH_TO_CONVERTED_TOKENIZER) + + >>> prompt = "Hey, are you conscious? Can you talk to me?" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." + ```""" + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + ) + + hidden_states = outputs[0] + + loss = None + logits = None + + if self.training: + shift_hidden_states = hidden_states[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + + # flatten tokens + shift_hidden_states = shift_hidden_states.view(-1, self.config.hidden_size) + shift_labels = shift_labels.view(-1) + + lce = LigerFusedLinearCrossEntropyLoss() + loss = lce(self.lm_head.weight, shift_hidden_states, shift_labels) + else: + logits = self.lm_head(hidden_states) + logits = logits.float() + + loss = None + if labels is not None: + # Shift so that tokens < n predict n + shift_logits = logits[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + # Flatten the tokens + loss_fct = CrossEntropyLoss() + shift_logits = shift_logits.view(-1, self.config.vocab_size) + shift_labels = shift_labels.view(-1) + # Enable model parallelism + shift_labels = shift_labels.to(shift_logits.device) + loss = loss_fct(shift_logits, shift_labels) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) From e3a38450ded395feb8ef1bc227d09b8b476d18f9 Mon Sep 17 00:00:00 2001 From: Byron Hsu Date: Thu, 29 Aug 2024 05:19:18 -0700 Subject: [PATCH 0150/1405] Add liger kernel to features (#1881) [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af604fad50..c84f1cb8c9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Features: - Supports fullfinetune, lora, qlora, relora, and gptq - Customize configurations using a simple yaml file or CLI overwrite - Load different dataset formats, use custom formats, or bring your own tokenized datasets -- Integrated with xformer, flash attention, rope scaling, and multipacking +- Integrated with xformer, flash attention, [liger kernel](https://github.com/linkedin/Liger-Kernel), rope scaling, and multipacking - Works with single GPU or multiple GPUs via FSDP or Deepspeed - Easily run with Docker locally or on the cloud - Log results and optionally checkpoints to wandb or mlflow From ce33e1ed839cc15b9351d3567ab44076cd4809c6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 30 Aug 2024 17:51:18 -0400 Subject: [PATCH 0151/1405] pin liger-kernel to latest 0.2.1 (#1882) [skip ci] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f5fb547a26..b8d0a388b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ tensorboard python-dotenv==1.0.1 autoawq>=0.2.5 triton>=2.3.0 -liger-kernel +liger-kernel==0.2.1 mamba-ssm==1.2.0.post1 From 15408d0f09062463a50d39cb0ff356fa1590a2b3 Mon Sep 17 00:00:00 2001 From: DocShotgun <126566557+DocShotgun@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:59:48 -0700 Subject: [PATCH 0152/1405] Update supported models for Liger Kernel (#1875) * Update supported models for Liger Kernel Add Mistral LCE, Gemma LCE, Gemma 2 without LCE (softcapping is not yet implemented for Gemma in Liger Kernel LCE forward), Phi3 without LCE * move import to their appropriate conditions * Integrate Phi3 LCE support https://github.com/linkedin/Liger-Kernel/pull/103/ --------- Co-authored-by: Wing Lian --- src/axolotl/integrations/liger/__init__.py | 51 +++++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index 2a3e95163b..d58349932b 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -23,7 +23,6 @@ from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss from liger_kernel.transformers.geglu import LigerGEGLUMLP -from liger_kernel.transformers.model.llama import lce_forward as llama_lce_forward from liger_kernel.transformers.rms_norm import LigerRMSNorm from liger_kernel.transformers.rope import liger_rotary_pos_emb from liger_kernel.transformers.swiglu import LigerSwiGLUMLP @@ -43,6 +42,9 @@ def get_input_args(self): def pre_model_load(self, cfg): if cfg.model_config_type == "llama": + from liger_kernel.transformers.model.llama import ( + lce_forward as llama_lce_forward, + ) from transformers.models.llama import modeling_llama if cfg.liger_rope: @@ -57,6 +59,9 @@ def pre_model_load(self, cfg): modeling_llama.LlamaForCausalLM.forward = llama_lce_forward elif cfg.model_config_type == "mistral": + from liger_kernel.transformers.model.mistral import ( + lce_forward as mistral_lce_forward, + ) from transformers.models.mistral import modeling_mistral if cfg.liger_rope: @@ -68,11 +73,12 @@ def pre_model_load(self, cfg): if cfg.liger_cross_entropy: modeling_mistral.CrossEntropyLoss = LigerCrossEntropyLoss if cfg.liger_fused_linear_cross_entropy: - logging.warning( - "Fused linear cross entropy is not supported for Mistral." - ) + modeling_mistral.MistralForCausalLM.forward = mistral_lce_forward elif cfg.model_config_type == "gemma": + from liger_kernel.transformers.model.gemma import ( + lce_forward as gemma_lce_forward, + ) from transformers.models.gemma import modeling_gemma if cfg.liger_rope: @@ -84,9 +90,7 @@ def pre_model_load(self, cfg): if cfg.liger_cross_entropy: modeling_gemma.CrossEntropyLoss = LigerCrossEntropyLoss if cfg.liger_fused_linear_cross_entropy: - logging.warning( - "Fused linear cross entropy is not supported for Gemma." - ) + modeling_gemma.GemmaForCausalLM.forward = gemma_lce_forward elif cfg.model_config_type == "jamba": from transformers.models.jamba import modeling_jamba @@ -145,3 +149,36 @@ def pre_model_load(self, cfg): modeling_mod.CrossEntropyLoss = LigerCrossEntropyLoss if cfg.liger_fused_linear_cross_entropy: modeling_mod.DeepseekV2ForCausalLM.forward = deepseekv2_lce_forward + + elif cfg.model_config_type == "gemma2": + from transformers.models.gemma2 import modeling_gemma2 + + if cfg.liger_rope: + modeling_gemma2.apply_rotary_pos_emb = liger_rotary_pos_emb + if cfg.liger_rms_norm: + modeling_gemma2.Gemma2RMSNorm = LigerRMSNorm + if cfg.liger_swiglu: + modeling_gemma2.Gemma2MLP = LigerGEGLUMLP + if cfg.liger_cross_entropy: + modeling_gemma2.CrossEntropyLoss = LigerCrossEntropyLoss + if cfg.liger_fused_linear_cross_entropy: + logging.warning( + "Fused linear cross entropy is not supported for Gemma 2." + ) + + elif cfg.model_config_type == "phi3": + from liger_kernel.transformers.model.phi3 import ( + lce_forward as phi3_lce_forward, + ) + from transformers.models.phi3 import modeling_phi3 + + if cfg.liger_rope: + modeling_phi3.apply_rotary_pos_emb = liger_rotary_pos_emb + if cfg.liger_rms_norm: + modeling_phi3.Phi3RMSNorm = LigerRMSNorm + if cfg.liger_swiglu: + modeling_phi3.Phi3MLP = LigerSwiGLUMLP + if cfg.liger_cross_entropy: + modeling_phi3.CrossEntropyLoss = LigerCrossEntropyLoss + if cfg.liger_fused_linear_cross_entropy: + modeling_phi3.Phi3ForCausalLM.forward = phi3_lce_forward From 3c6b9eda2ecffb0204eb1c29635f75c0115317b3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 31 Aug 2024 22:49:35 -0400 Subject: [PATCH 0153/1405] run pytests with varied pytorch versions too (#1883) --- .github/workflows/tests-nightly.yml | 5 +++++ .github/workflows/tests.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 6b35698cbf..30ed397cef 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -25,6 +25,7 @@ jobs: fail-fast: false matrix: python_version: ["3.10", "3.11"] + pytorch_version: ["2.3.1", "2.4.0"] timeout-minutes: 20 steps: @@ -37,6 +38,10 @@ jobs: python-version: ${{ matrix.python_version }} cache: 'pip' # caching pip dependencies + - name: Install PyTorch + run: | + pip3 install torch==${{ matrix.pytorch_version }} --index-url https://download.pytorch.org/whl/cpu + - name: Update requirements.txt run: | sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 74b4bcfbdb..c104e92c27 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,6 +36,7 @@ jobs: fail-fast: false matrix: python_version: ["3.10", "3.11"] + pytorch_version: ["2.3.1", "2.4.0"] timeout-minutes: 20 steps: @@ -48,6 +49,10 @@ jobs: python-version: ${{ matrix.python_version }} cache: 'pip' # caching pip dependencies + - name: Install PyTorch + run: | + pip3 install torch==${{ matrix.pytorch_version }} --index-url https://download.pytorch.org/whl/cpu + - name: Install dependencies run: | pip3 install --upgrade pip From bdab3ec587f9e85f5684cf57302a1bf93e78de1e Mon Sep 17 00:00:00 2001 From: Chiwan Park Date: Mon, 2 Sep 2024 07:34:24 +0900 Subject: [PATCH 0154/1405] Fix RMSNorm monkey patch for Gemma models (#1886) --- src/axolotl/integrations/liger/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index d58349932b..2047f3815d 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -20,6 +20,7 @@ """ import logging import sys +from functools import partial from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss from liger_kernel.transformers.geglu import LigerGEGLUMLP @@ -84,7 +85,9 @@ def pre_model_load(self, cfg): if cfg.liger_rope: modeling_gemma.apply_rotary_pos_emb = liger_rotary_pos_emb if cfg.liger_rms_norm: - modeling_gemma.GemmaRMSNorm = LigerRMSNorm + modeling_gemma.GemmaRMSNorm = partial( + LigerRMSNorm, offset=1.0, init_fn="zeros", casting_mode="gemma" + ) if cfg.liger_swiglu: modeling_gemma.GemmaMLP = LigerGEGLUMLP if cfg.liger_cross_entropy: @@ -156,7 +159,9 @@ def pre_model_load(self, cfg): if cfg.liger_rope: modeling_gemma2.apply_rotary_pos_emb = liger_rotary_pos_emb if cfg.liger_rms_norm: - modeling_gemma2.Gemma2RMSNorm = LigerRMSNorm + modeling_gemma2.Gemma2RMSNorm = partial( + LigerRMSNorm, offset=1.0, init_fn="zeros", casting_mode="gemma" + ) if cfg.liger_swiglu: modeling_gemma2.Gemma2MLP = LigerGEGLUMLP if cfg.liger_cross_entropy: From 0aeb277456f0ed79ab46191a12998fccc257d414 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 1 Sep 2024 19:29:37 -0400 Subject: [PATCH 0155/1405] add e2e smoke tests for llama liger integration (#1884) * add e2e smoke tests for llama liger integration * fix import * don't use __main__ for test * consolidate line --- cicd/cicd.sh | 4 +- tests/e2e/integrations/__init__.py | 0 tests/e2e/integrations/liger.py | 110 +++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/integrations/__init__.py create mode 100644 tests/e2e/integrations/liger.py diff --git a/cicd/cicd.sh b/cicd/cicd.sh index eceda9b375..104a8f84ab 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -2,5 +2,5 @@ set -e pytest --ignore=tests/e2e/ /workspace/axolotl/tests/ -pytest -n1 --dist loadfile -v /workspace/axolotl/tests/e2e/patched/ -pytest --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ /workspace/axolotl/tests/e2e/ +pytest -n1 --dist loadfile -v /workspace/axolotl/tests/e2e/patched/ /workspace/axolotl/tests/e2e/integrations/ +pytest --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ diff --git a/tests/e2e/integrations/__init__.py b/tests/e2e/integrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/integrations/liger.py b/tests/e2e/integrations/liger.py new file mode 100644 index 0000000000..4497cebe32 --- /dev/null +++ b/tests/e2e/integrations/liger.py @@ -0,0 +1,110 @@ +""" +Simple end-to-end test for Liger integration +""" + +import unittest +from pathlib import Path + +from axolotl.cli import load_datasets +from axolotl.common.cli import TrainerCliArgs +from axolotl.train import train +from axolotl.utils.config import normalize_config +from axolotl.utils.dict import DictDefault + +from ..utils import with_temp_dir + + +class LigerIntegrationTestCase(unittest.TestCase): + """ + e2e tests for liger integration with Axolotl + """ + + @with_temp_dir + def test_llama_wo_flce(self, temp_dir): + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "plugins": [ + "axolotl.integrations.liger.LigerPlugin", + ], + "liger_rope": True, + "liger_rms_norm": True, + "liger_swiglu": True, + "liger_cross_entropy": True, + "liger_fused_linear_cross_entropy": False, + "sequence_len": 1024, + "val_set_size": 0.1, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() + + @with_temp_dir + def test_llama_w_flce(self, temp_dir): + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "plugins": [ + "axolotl.integrations.liger.LigerPlugin", + ], + "liger_rope": True, + "liger_rms_norm": True, + "liger_swiglu": True, + "liger_cross_entropy": False, + "liger_fused_linear_cross_entropy": True, + "sequence_len": 1024, + "val_set_size": 0.1, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() From 4e5400c732c6b8baf2d0f7a700ce773b1501b1fc Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 3 Sep 2024 20:02:44 -0400 Subject: [PATCH 0156/1405] support for auto_find_batch_size when packing (#1885) * support for auto_find_batch_size when packing * make sure to return data from validation * make sure to return data from validation * actually expose multipack_real_batches in the config * calculate gathered efficiency in sampler * tweak to fix auto find and use actual sampler len for multipack * uncomment * use args for bsz when not available from auto find --- src/axolotl/core/trainer_builder.py | 15 ++++--- .../config/models/input/v0_4_1/__init__.py | 3 ++ src/axolotl/utils/samplers/multipack.py | 40 +++++++++++++++++-- src/axolotl/utils/trainer.py | 2 +- 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 656ded2559..f4cd257838 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -506,9 +506,10 @@ def _get_train_sampler(self) -> Optional[torch.utils.data.Sampler]: batch_max_len = self.args.max_seq_length else: batch_size = 1 - batch_max_len = ( - self.args.per_device_train_batch_size * self.args.max_seq_length + train_batch_size = ( + self.state.train_batch_size or self.args.per_device_train_batch_size ) + batch_max_len = train_batch_size * self.args.max_seq_length return MultipackBatchSampler( RandomSampler(self.train_dataset), lengths=get_dataset_lengths(self.train_dataset), @@ -1379,6 +1380,10 @@ def build(self, total_num_steps): training_arguments_kwargs[ "per_device_eval_batch_size" ] = self.cfg.eval_batch_size + if self.cfg.auto_find_batch_size is not None: + training_arguments_kwargs[ + "auto_find_batch_size" + ] = self.cfg.auto_find_batch_size training_arguments_kwargs[ "gradient_accumulation_steps" ] = self.cfg.gradient_accumulation_steps @@ -1461,9 +1466,9 @@ def build(self, total_num_steps): ) training_arguments_kwargs["sample_packing"] = bool(self.cfg.sample_packing) - training_arguments_kwargs[ - "multipack_real_batches" - ] = not self.cfg.flash_attention + training_arguments_kwargs["multipack_real_batches"] = ( + not self.cfg.flash_attention or self.cfg.multipack_real_batches + ) training_arguments_kwargs["eval_sample_packing"] = bool( self.cfg.eval_sample_packing ) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 65a2c5409a..9044047cce 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -355,6 +355,8 @@ class HyperparametersConfig(BaseModel): }, ) + auto_find_batch_size: Optional[bool] = None + train_on_inputs: Optional[bool] = False group_by_length: Optional[bool] = None @@ -592,6 +594,7 @@ class Config: eval_sample_packing: Optional[bool] = None pad_to_sequence_len: Optional[bool] = None curriculum_sampling: Optional[bool] = None + multipack_real_batches: Optional[bool] = None # for PoSE context length extension use_pose: Optional[bool] = None diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 957ca57464..205c2894d1 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -11,6 +11,8 @@ import numpy as np from torch.utils.data import BatchSampler, Sampler +from axolotl.utils.distributed import reduce_and_broadcast + LOG = logging.getLogger("axolotl.utils.samplers.multipack") @@ -174,16 +176,46 @@ def num_batches(self): def efficiency(self): return self.eff_total_used / self.eff_total_slots + def gather_efficiency(self): + def calc_sample_packing_eff_est(estimates: List[float]): + LOG.debug(f"sample_packing_eff_est across ranks: {repr(estimates)}") + return math.floor(0.997 * max(estimates)) + + sample_packing_actual_eff_all = reduce_and_broadcast( + lambda: self.efficiency(), # pylint: disable=unnecessary-lambda + calc_sample_packing_eff_est, + ) + sample_packing_eff_est = ( + math.ceil(sample_packing_actual_eff_all * 200.0) / 200.0 + ) + return sample_packing_eff_est + + def gather_len_batches(self, num): + def calc_min_len(estimates: list[(int, float)]): + LOG.info(f"gather_len_batches: {repr(estimates)}") + return math.floor(0.998 * min(estimates)) + + min_len_batches = reduce_and_broadcast( + lambda: num, + calc_min_len, + ) + return min_len_batches + def __len__(self): - self.num_batches() - return self._len_est() + len_batches = self.num_batches() + return self.gather_len_batches(len_batches) def _len_est(self): + efficiency = ( + self.packing_efficiency_estimate + if self.packing_efficiency_estimate + else self.gather_efficiency() + ) world_size = int(os.getenv("WORLD_SIZE", "1")) lengths_sum = np.sum(self.lengths) lengths_sum_per_device = lengths_sum // world_size LOG.info( - f"packing_efficiency_estimate: {self.packing_efficiency_estimate} " + f"packing_efficiency_estimate: {efficiency} " f"total_num_tokens per device: {lengths_sum_per_device}" ) @@ -195,7 +227,7 @@ def _len_est(self): * math.floor( 0.99 * lengths_sum_per_device - / self.packing_efficiency_estimate + / efficiency // (self.batch_max_len * self.batch_size) ) - 1 diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index f4e1fc6cb8..1029fff13d 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -357,7 +357,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): main_process_only=True, ) else: - if cfg.flash_attention: + if cfg.flash_attention and not cfg.multipack_real_batches: sampler_batch_size = 1 batch_max_len = cfg.micro_batch_size * cfg.sequence_len else: From dca1fe47d44d69c4558729070a3716b6f79e555c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 4 Sep 2024 11:28:47 -0400 Subject: [PATCH 0157/1405] fix optimizer + fsdp combination in example (#1893) --- examples/llama-3/fft-8b-liger-fsdp.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml index a64965d207..e84d221f85 100644 --- a/examples/llama-3/fft-8b-liger-fsdp.yaml +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -31,7 +31,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 2 num_epochs: 1 -optimizer: paged_adamw_8bit +optimizer: adamw_torch lr_scheduler: cosine learning_rate: 2e-5 From f18f4268b53be3c9518e2223b1f0aa874d5938c3 Mon Sep 17 00:00:00 2001 From: Tijmen de Haan Date: Thu, 5 Sep 2024 18:33:19 +0900 Subject: [PATCH 0158/1405] Docs for AMD-based HPC systems (#1891) * Add documentation for installing on AMD-based HPC systems. * Accept suggestion to add note about deepspeed Co-authored-by: NanoCode012 * Update _quarto.yml with amd_hpc doc --------- Co-authored-by: Tijmen de Haan Co-authored-by: NanoCode012 --- _quarto.yml | 1 + docs/amd_hpc.qmd | 108 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 docs/amd_hpc.qmd diff --git a/_quarto.yml b/_quarto.yml index 6b2eed971b..acb4872589 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -37,6 +37,7 @@ website: - docs/mac.qmd - docs/multi-node.qmd - docs/unsloth.qmd + - docs/amd_hpc.qmd - section: "Dataset Formats" contents: docs/dataset-formats/* - section: "Reference" diff --git a/docs/amd_hpc.qmd b/docs/amd_hpc.qmd new file mode 100644 index 0000000000..92eadee03a --- /dev/null +++ b/docs/amd_hpc.qmd @@ -0,0 +1,108 @@ +--- +title: Training with AMD GPUs on HPC Systems +description: A comprehensive guide for using Axolotl on distributed systems with AMD GPUs +--- + +This guide provides step-by-step instructions for installing and configuring Axolotl on a High-Performance Computing (HPC) environment equipped with AMD GPUs. + +## Setup + +### 1. Install Python + +We recommend using Miniforge, a minimal conda-based Python distribution: + +```bash +curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh" +bash Miniforge3-$(uname)-$(uname -m).sh +``` + +### 2. Configure Python Environment +Add Python to your PATH and ensure it's available at login: + +```bash +echo 'export PATH=~/miniforge3/bin:$PATH' >> ~/.bashrc +echo 'if [ -f ~/.bashrc ]; then . ~/.bashrc; fi' >> ~/.bash_profile +``` + +### 3. Load AMD GPU Software + +Load the ROCm module: + +```bash +module load rocm/5.7.1 +``` + +Note: The specific module name and version may vary depending on your HPC system. Consult your system documentation for the correct module name. + +### 4. Install PyTorch + +Install PyTorch with ROCm support: + +```bash +pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm5.7 --force-reinstall +``` + +### 5. Install Flash Attention + +Clone and install the Flash Attention repository: + +```bash +git clone --recursive https://github.com/ROCmSoftwarePlatform/flash-attention.git +export GPU_ARCHS="gfx90a" +cd flash-attention +export PYTHON_SITE_PACKAGES=$(python -c 'import site; print(site.getsitepackages()[0])') +patch "${PYTHON_SITE_PACKAGES}/torch/utils/hipify/hipify_python.py" hipify_patch.patch +pip install . +``` + +### 6. Install Axolotl + +Clone and install Axolotl: + +```bash +git clone https://github.com/axolotl-ai-cloud/axolotl +cd axolotl +pip install packaging ninja +pip install -e . +``` + +### 7. Apply xformers Workaround + +xformers appears to be incompatible with ROCm. Apply the following workarounds: + - Edit $HOME/packages/axolotl/src/axolotl/monkeypatch/llama_attn_hijack_flash.py modifying the code to always return `False` for SwiGLU availability from xformers. + - Edit $HOME/miniforge3/lib/python3.10/site-packages/xformers/ops/swiglu_op.py replacing the "SwiGLU" function with a pass statement. + +### 8. Prepare Job Submission Script + +Create a script for job submission using your HPC's particular software (e.g. Slurm, PBS). Include necessary environment setup and the command to run Axolotl training. If the compute node(s) do(es) not have internet access, it is recommended to include + +```bash +export TRANSFORMERS_OFFLINE=1 +export HF_DATASETS_OFFLINE=1 +``` + +### 9. Download Base Model + +Download a base model using the Hugging Face CLI: + +```bash +huggingface-cli download meta-llama/Meta-Llama-3.1-8B --local-dir ~/hfdata/llama3.1-8B +``` + +### 10. Create Axolotl Configuration + +Create an Axolotl configuration file (YAML format) tailored to your specific training requirements and dataset. Use FSDP for multi-node training. + +Note: Deepspeed did not work at the time of testing. However, if anyone managed to get it working, please let us know. + +### 11. Preprocess Data + +Run preprocessing on the login node: + +```bash +CUDA_VISIBLE_DEVICES="" python -m axolotl.cli.preprocess /path/to/your/config.yaml +``` + +### 12. Train + +You are now ready to submit your previously prepared job script. 🚂 \ No newline at end of file From 93b769a9792db5908885537ed42fb7eef80f0f1c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 5 Sep 2024 09:58:21 -0400 Subject: [PATCH 0159/1405] lint fix and update gha regex (#1899) --- .github/workflows/lint.yml | 2 +- docs/amd_hpc.qmd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 671be4b652..919cfd6545 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,7 @@ on: - '**.py' - 'requirements.txt' - '.github/workflows/*.yml' - - "*.md" + - "*.[q]md" - "examples/**/*.y[a]?ml" workflow_dispatch: diff --git a/docs/amd_hpc.qmd b/docs/amd_hpc.qmd index 92eadee03a..d1c274e15a 100644 --- a/docs/amd_hpc.qmd +++ b/docs/amd_hpc.qmd @@ -105,4 +105,4 @@ CUDA_VISIBLE_DEVICES="" python -m axolotl.cli.preprocess /path/to/your/config.ya ### 12. Train -You are now ready to submit your previously prepared job script. 🚂 \ No newline at end of file +You are now ready to submit your previously prepared job script. 🚂 From ab461d83c4b78df70d310ce45e33ef145796611d Mon Sep 17 00:00:00 2001 From: Alpay Ariyak <98838263+alpayariyak@users.noreply.github.com> Date: Thu, 5 Sep 2024 07:11:31 -0700 Subject: [PATCH 0160/1405] Fix documentation for pre-tokenized dataset (#1894) It's currently asking to not add BOS and EOS, stating that Axolotl adds them, but this is not true --- docs/dataset-formats/tokenized.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dataset-formats/tokenized.qmd b/docs/dataset-formats/tokenized.qmd index b2ea003c02..61028cae7f 100644 --- a/docs/dataset-formats/tokenized.qmd +++ b/docs/dataset-formats/tokenized.qmd @@ -7,7 +7,7 @@ order: 5 - Pass an empty `type:` in your axolotl config. - Columns in Dataset must be exactly `input_ids`, `attention_mask`, `labels` - To indicate that a token should be ignored during training, set its corresponding label to `-100`. -- Do not add BOS/EOS. Axolotl will add them for you based on the default tokenizer for the model you're using. +- You must add BOS and EOS, and make sure that you are training on EOS by not setting its label to -100. - For pretraining, do not truncate/pad documents to the context window length. - For instruction training, documents must be truncated/padded as desired. From 6e354682e3c1735d3f7fb9e362280c38e922260f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 5 Sep 2024 10:58:50 -0400 Subject: [PATCH 0161/1405] fix zero3 integration (#1897) * fix zero3 integration * bump transformers and accelerate too --- requirements.txt | 4 ++-- src/axolotl/utils/trainer.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index b8d0a388b3..c61216e63b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.12.0 -transformers==4.44.0 +transformers==4.44.2 tokenizers>=0.19.1 bitsandbytes==0.43.3 -accelerate==0.33.0 +accelerate==0.34.0 datasets==2.20.0 deepspeed==0.14.4 pydantic==2.6.3 diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 1029fff13d..89ae4e6970 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -425,7 +425,8 @@ def setup_deepspeed_env(cfg, stage=None): os.environ["ACCELERATE_DEEPSPEED_ZERO_STAGE"] = str(stage) if stage == 3: os.environ["ACCELERATE_DEEPSPEED_ZERO3_INIT"] = "true" - HfTrainerDeepSpeedConfig(cfg.deepspeed) + # If we don't assign this, it doesn't actually get set in the accelerate weakref + _ = HfTrainerDeepSpeedConfig(cfg.deepspeed) def setup_fsdp_envs(cfg): From 3853ab7ae9220dfbd78cd628e54fde75fb89df97 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 7 Sep 2024 14:39:31 -0400 Subject: [PATCH 0162/1405] bump accelerate to 0.34.2 (#1901) * bump accelerate * add fixture to predownload the test model * change fixture --- .github/workflows/multi-gpu-e2e.yml | 3 +++ requirements.txt | 2 +- tests/e2e/multigpu/test_llama.py | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 91cbaf957e..ab886c67f1 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -1,6 +1,9 @@ name: docker-multigpu-tests-biweekly on: + pull_request: + paths: + - 'tests/e2e/multigpu/*.py' workflow_dispatch: schedule: - cron: '0 0 * * 1,4' # Runs at 00:00 UTC every monday & thursday diff --git a/requirements.txt b/requirements.txt index c61216e63b..83116af60f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ peft==0.12.0 transformers==4.44.2 tokenizers>=0.19.1 bitsandbytes==0.43.3 -accelerate==0.34.0 +accelerate==0.34.2 datasets==2.20.0 deepspeed==0.14.4 pydantic==2.6.3 diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 344c57fb85..61bb8ed327 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -10,6 +10,7 @@ import pytest import yaml from accelerate.test_utils import execute_subprocess_async +from huggingface_hub import snapshot_download from axolotl.utils.dict import DictDefault @@ -19,6 +20,12 @@ os.environ["WANDB_DISABLED"] = "true" +@pytest.fixture(scope="session", autouse=True) +def download_model(): + # download the model + snapshot_download("TinyLlama/TinyLlama_v1.1") + + class TestMultiGPULlama(unittest.TestCase): """ Test case for Llama models using LoRA From 5c42f114115cc4e2dd49c1da2437ef1c08aecf69 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 13 Sep 2024 22:19:54 -0400 Subject: [PATCH 0163/1405] remove dynamic module loader monkeypatch as this was fixed upstream (#1914) --- examples/deepseek-v2/qlora-fsdp-2_5.yaml | 83 +++++++++++++++++++ requirements.txt | 4 +- .../transformers_dynamic_module_utils.py | 51 ------------ src/axolotl/utils/models.py | 5 -- 4 files changed, 85 insertions(+), 58 deletions(-) create mode 100644 examples/deepseek-v2/qlora-fsdp-2_5.yaml delete mode 100644 src/axolotl/monkeypatch/transformers_dynamic_module_utils.py diff --git a/examples/deepseek-v2/qlora-fsdp-2_5.yaml b/examples/deepseek-v2/qlora-fsdp-2_5.yaml new file mode 100644 index 0000000000..6e82062d66 --- /dev/null +++ b/examples/deepseek-v2/qlora-fsdp-2_5.yaml @@ -0,0 +1,83 @@ +base_model: axolotl-quants/DeepSeek-V2.5-bnb-nf4-bf16 +trust_remote_code: true + +load_in_8bit: false +load_in_4bit: true +strict: false + + +plugins: + - axolotl.integrations.liger.LigerPlugin +liger_rms_norm: true +liger_swiglu: true +liger_fused_linear_cross_entropy: true + +chat_template: deepseek_v2 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train + +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +sequence_len: 4096 +sample_packing: true +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +adapter: qlora +lora_r: 256 +lora_alpha: 256 +lora_target_linear: true +peft_use_rslora: true + +gradient_accumulation_steps: 1 +micro_batch_size: 8 +num_epochs: 1 +optimizer: adamw_torch +lr_scheduler: cosine +learning_rate: 2e-5 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +early_stopping_patience: +resume_from_checkpoint: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_steps: 100 +evals_per_epoch: 2 +eval_table_size: +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +special_tokens: +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_limit_all_gathers: true + fsdp_sync_module_states: true + fsdp_offload_params: true + fsdp_use_orig_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: DeepseekV2DecoderLayer + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD diff --git a/requirements.txt b/requirements.txt index 83116af60f..32a9e0e01c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.12.0 -transformers==4.44.2 +transformers @ git+https://github.com/huggingface/transformers.git@0963229e287501bed52ae1dabc17922524de6992 tokenizers>=0.19.1 bitsandbytes==0.43.3 accelerate==0.34.2 -datasets==2.20.0 +datasets==2.21.0 deepspeed==0.14.4 pydantic==2.6.3 addict diff --git a/src/axolotl/monkeypatch/transformers_dynamic_module_utils.py b/src/axolotl/monkeypatch/transformers_dynamic_module_utils.py deleted file mode 100644 index dfc3e29c5a..0000000000 --- a/src/axolotl/monkeypatch/transformers_dynamic_module_utils.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Patch transformers.dynamic_module_utils.get_class_in_module to avoid reloading models from disk""" - -import importlib -import os -import sys -import typing -from pathlib import Path - -from transformers.file_utils import HF_MODULES_CACHE - - -def _patched_get_class_in_module( - class_name: str, module_path: typing.Union[str, os.PathLike] -) -> typing.Type: - """ - Import a module on the cache directory for modules and extract a class from it. - - Args: - class_name (`str`): The name of the class to import. - module_path (`str` or `os.PathLike`): The path to the module to import. - - Returns: - `typing.Type`: The class looked for. - """ - name = os.path.normpath(module_path) - if name.endswith(".py"): - name = name[:-3] - name = name.replace(os.path.sep, ".") - module_spec = importlib.util.spec_from_file_location( - name, location=Path(HF_MODULES_CACHE) / module_path - ) - module = sys.modules.get(name) - if module is None: - module = importlib.util.module_from_spec(module_spec) - # insert it into sys.modules before any loading begins - sys.modules[name] = module - # load in initial case only - module_spec.loader.exec_module(module) - return getattr(module, class_name) - - -def patch_transformers_dynamic_module_utils(): - """ - Recently, transformers started reloading modeling code from disk for models marked trust_remote_code=True. - This causes monkey-patches for multipack and liger to be removed. - We replace the original function with a version that does not reload the module from disk. - See https://github.com/huggingface/transformers/pull/30370#pullrequestreview-2264361581 - """ - import transformers - - transformers.dynamic_module_utils.get_class_in_module = _patched_get_class_in_module diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index e0526fb048..e183301991 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -43,9 +43,6 @@ SUPPORTED_MULTIPACK_MODEL_TYPES, patch_for_multipack, ) -from axolotl.monkeypatch.transformers_dynamic_module_utils import ( - patch_transformers_dynamic_module_utils, -) from axolotl.prompt_tokenizers import LLAMA_DEFAULT_EOS_TOKEN from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.chat_templates import chat_templates @@ -57,8 +54,6 @@ LOG = logging.getLogger("axolotl") -patch_transformers_dynamic_module_utils() - # copied from accelerator.FullyShardedDataParallelPlugin def get_module_class_from_name(module, name): From 7b9f669a3ab18aecb00b17e7f2885aeb458440c8 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Sat, 14 Sep 2024 05:22:54 -0700 Subject: [PATCH 0164/1405] Trigger the original tokenization behavior when no advanced turn settings are provided (#1915) --- examples/phi/lora-3.5.yaml | 76 ++ .../prompt_strategies/chat_template.py | 54 +- src/axolotl/utils/chat_templates.py | 1 + .../config/models/input/v0_4_1/__init__.py | 1 + tests/prompt_strategies/conftest.py | 71 ++ .../prompt_strategies/test_chat_templates.py | 714 ++---------------- .../test_chat_templates_advanced.py | 615 +++++++++++++++ 7 files changed, 866 insertions(+), 666 deletions(-) create mode 100644 examples/phi/lora-3.5.yaml create mode 100644 tests/prompt_strategies/conftest.py create mode 100644 tests/prompt_strategies/test_chat_templates_advanced.py diff --git a/examples/phi/lora-3.5.yaml b/examples/phi/lora-3.5.yaml new file mode 100644 index 0000000000..59d667b8db --- /dev/null +++ b/examples/phi/lora-3.5.yaml @@ -0,0 +1,76 @@ +base_model: microsoft/Phi-3.5-mini-instruct +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer + +load_in_8bit: true +load_in_4bit: false +strict: false + +chat_template: phi_3 +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + chat_template: phi_3 + field_messages: messages + message_field_role: role + message_field_content: content + roles: + user: + - user + assistant: + - assistant + +dataset_prepared_path: +val_set_size: 0.05 +output_dir: ./outputs/lora-out + +sequence_len: 4096 +sample_packing: false +pad_to_sequence_len: true + +adapter: lora +lora_model_dir: +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_fan_in_fan_out: + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 4 +num_epochs: 2 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bfloat16: true +bf16: true +fp16: +tf32: false + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +s2_attention: + +warmup_steps: 10 +evals_per_epoch: 4 +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 4 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 19e36531a5..717367eefa 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -24,8 +24,8 @@ def __init__( max_length=2048, message_field_role: str = "from", message_field_content: str = "value", - message_field_training: str = "train", - message_field_training_detail: str = "train_detail", + message_field_training: Optional[str] = None, + message_field_training_detail: Optional[str] = None, roles: Optional[Dict[str, List[str]]] = None, drop_system_message: bool = False, ): @@ -186,7 +186,7 @@ def __init__( train_on_inputs, sequence_len, roles_to_train=None, - train_on_eos="last", + train_on_eos=None, ): super().__init__(prompter, tokenizer, train_on_inputs, sequence_len) self.roles_to_train = roles_to_train if roles_to_train is not None else [] @@ -201,6 +201,37 @@ def messages(self, messages): self._messages = messages def tokenize_prompt(self, prompt): + # Old simple legacy behavior that works reliably. + if ( + not self.roles_to_train + and not self.train_on_eos + and not self.prompter.message_field_training + and not self.prompter.message_field_training_detail + ): + turns = self.get_conversation_thread(prompt) + prompt_ids = self.prompter.build_prompt( + turns[:-1], add_generation_prompt=True + ) + input_ids = self.prompter.build_prompt(turns) + + if not self.train_on_inputs: + user_prompt_len = len(prompt_ids) + labels = [-100] * user_prompt_len + input_ids[user_prompt_len:] + else: + labels = input_ids + + tokenized_prompt = { + "input_ids": input_ids, + "labels": labels, + "attention_mask": [1] * len(input_ids), + } + + return tokenized_prompt + LOG.info(self.roles_to_train) + LOG.info(self.train_on_eos) + LOG.info(self.prompter.message_field_training) + LOG.info(self.prompter.message_field_training_detail) + turns = prompt[self.messages] input_ids = self.prompter.build_prompt(turns) labels = [IGNORE_TOKEN_ID] * len(input_ids) @@ -219,9 +250,11 @@ def tokenize_prompt(self, prompt): should_train = ( train_turn if train_turn is not None - else bool(train_detail is not None) - if train_detail is not None - else self.train_on_inputs or role in self.roles_to_train + else ( + bool(train_detail is not None) + if train_detail is not None + else self.train_on_inputs or role in self.roles_to_train + ) ) LOG.debug(f"Should train: {should_train}") @@ -344,9 +377,10 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): "chat_template": chat_templates(ds_cfg.get("chat_template", "chatml")), "message_field_role": ds_cfg.get("message_field_role", "from"), "message_field_content": ds_cfg.get("message_field_content", "value"), - "message_field_training": ds_cfg.get("message_field_training", "training"), + "message_field_training": ds_cfg.get("message_field_training", None), "message_field_training_detail": ds_cfg.get( - "message_field_training_detail", "train_detail" + "message_field_training_detail", + None, ), "roles": ds_cfg.get("roles"), "drop_system_message": ds_cfg.get("drop_system_message", False), @@ -357,8 +391,8 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): strategy_params = { "train_on_inputs": cfg.train_on_inputs, "sequence_len": cfg.sequence_len, - "roles_to_train": ds_cfg.get("roles_to_train", ["gpt", "assistant"]), - "train_on_eos": ds_cfg.get("train_on_eos", "turn"), + "roles_to_train": ds_cfg.get("roles_to_train", []), + "train_on_eos": ds_cfg.get("train_on_eos", None), } strategy = ChatTemplateStrategy( diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 51f88b1bdf..7a96f5c1e1 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -26,6 +26,7 @@ def chat_templates(user_choice: str): "cohere": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", + "phi_35": "{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'user' %}{{'<|user|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|>\n' + message['content'] + '<|end|>\n'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% else %}{{ eos_token }}{% endif %}", "deepseek_v2": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %}", "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', } diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 9044047cce..458bacdb12 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -189,6 +189,7 @@ class ChatTemplate(str, Enum): cohere = "cohere" # pylint: disable=invalid-name llama3 = "llama3" # pylint: disable=invalid-name phi_3 = "phi_3" # pylint: disable=invalid-name + phi_35 = "phi_35" # pylint: disable=invalid-name deepseek_v2 = "deepseek_v2" # pylint: disable=invalid-name jamba = "jamba" # pylint: disable=invalid-name diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py new file mode 100644 index 0000000000..43423f7255 --- /dev/null +++ b/tests/prompt_strategies/conftest.py @@ -0,0 +1,71 @@ +""" +shared fixtures for prompt strategies tests +""" + +import pytest +from datasets import Dataset +from transformers import AutoTokenizer + + +@pytest.fixture(name="assistant_dataset") +def fixture_assistant_dataset(): + return Dataset.from_list( + [ + { + "messages": [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hello"}, + {"role": "user", "content": "goodbye"}, + {"role": "assistant", "content": "goodbye"}, + ] + } + ] + ) + + +@pytest.fixture(name="sharegpt_dataset") +def fixture_sharegpt_dataset(): + # pylint: disable=duplicate-code + return Dataset.from_list( + [ + { + "conversations": [ + {"from": "human", "value": "hello"}, + {"from": "gpt", "value": "hello"}, + {"from": "human", "value": "goodbye"}, + {"from": "gpt", "value": "goodbye"}, + ] + } + ] + ) + + +@pytest.fixture(name="basic_dataset") +def fixture_basic_dataset(): + # pylint: disable=duplicate-code + return Dataset.from_list( + [ + { + "conversations": [ + {"from": "system", "value": "You are an AI assistant."}, + {"from": "human", "value": "Hello"}, + {"from": "assistant", "value": "Hi there!"}, + {"from": "human", "value": "How are you?"}, + {"from": "assistant", "value": "I'm doing well, thank you!"}, + ] + } + ] + ) + + +@pytest.fixture(name="llama3_tokenizer") +def fixture_llama3_tokenizer(): + tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B-Instruct") + + return tokenizer + + +@pytest.fixture(name="phi35_tokenizer") +def fixture_phi35_tokenizer(): + tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3.5-mini-instruct") + return tokenizer diff --git a/tests/prompt_strategies/test_chat_templates.py b/tests/prompt_strategies/test_chat_templates.py index e2fc0f6a52..28210b7ae8 100644 --- a/tests/prompt_strategies/test_chat_templates.py +++ b/tests/prompt_strategies/test_chat_templates.py @@ -5,10 +5,6 @@ import logging import unittest -import pytest -from datasets import Dataset -from transformers import AutoTokenizer - from axolotl.prompt_strategies.chat_template import ( ChatTemplatePrompter, ChatTemplateStrategy, @@ -22,657 +18,6 @@ LOG = logging.getLogger("axolotl") -@pytest.fixture(name="assistant_dataset") -def fixture_assistant_dataset(): - return Dataset.from_list( - [ - { - "messages": [ - {"role": "user", "content": "hello"}, - {"role": "assistant", "content": "hello"}, - {"role": "user", "content": "goodbye"}, - {"role": "assistant", "content": "goodbye"}, - ] - } - ] - ) - - -@pytest.fixture(name="sharegpt_dataset") -def fixture_sharegpt_dataset(): - # pylint: disable=duplicate-code - return Dataset.from_list( - [ - { - "conversations": [ - {"from": "human", "value": "hello"}, - {"from": "gpt", "value": "hello"}, - {"from": "human", "value": "goodbye"}, - {"from": "gpt", "value": "goodbye"}, - ] - } - ] - ) - - -@pytest.fixture(name="basic_dataset") -def fixture_basic_dataset(): - # pylint: disable=duplicate-code - return Dataset.from_list( - [ - { - "conversations": [ - {"from": "system", "value": "You are an AI assistant."}, - {"from": "human", "value": "Hello"}, - {"from": "assistant", "value": "Hi there!"}, - {"from": "human", "value": "How are you?"}, - {"from": "assistant", "value": "I'm doing well, thank you!"}, - ] - } - ] - ) - - -@pytest.fixture(name="llama3_tokenizer") -def fixture_llama3_tokenizer(): - tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B-Instruct") - - return tokenizer - - -class TestChatTemplateConfigurations: - """ - Test class for various configurations of ChatTemplateStrategy. - """ - - @staticmethod - def find_sublist(full_list, sub_list): - token_count = len(sub_list) - for index in range(len(full_list) - token_count + 1): - if full_list[index : index + token_count] == sub_list: - return index - return -1 - - def test_train_on_inputs_true(self, llama3_tokenizer, basic_dataset): - LOG.info("Testing with train_on_inputs=True") - strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), - tokenizer=llama3_tokenizer, - train_on_inputs=True, - sequence_len=512, - roles_to_train=["assistant"], - ) - res = strategy.tokenize_prompt(basic_dataset[0]) - labels = res["labels"] - input_ids = res["input_ids"] - - # Verify that assistant responses are labeled - assistant_responses = ["Hi there!", "I'm doing well, thank you!"] - for response in assistant_responses: - response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, response_ids) - LOG.debug( - f"Assistant response '{response}' expected IDs: {response_ids}, found at: {start_idx}" - ) - assert start_idx != -1, f"Could not find '{response}' in input_ids" - assert all( - label != IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(response_ids)] - ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" - - # Check the behavior of human inputs - human_inputs = ["Hello", "How are you?"] - for input_text in human_inputs: - input_ids = llama3_tokenizer.encode(input_text, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, input_ids) - labeled = all( - label != IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(input_ids)] - ) - LOG.debug( - f"Human input '{input_text}' is {'labeled' if labeled else 'not labeled'}, expected IDs: {input_ids}, found at: {start_idx}" - ) - - LOG.debug("Full labels: %s", labels) - LOG.debug("Full input_ids: %s", input_ids) - - def test_train_on_inputs_false(self, llama3_tokenizer, basic_dataset): - LOG.info("Testing with train_on_inputs=False") - strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), - tokenizer=llama3_tokenizer, - train_on_inputs=False, - sequence_len=512, - roles_to_train=["assistant"], - ) - res = strategy.tokenize_prompt(basic_dataset[0]) - labels = res["labels"] - input_ids = res["input_ids"] - - # Verify that only assistant responses are labeled - assistant_responses = ["Hi there!", "I'm doing well, thank you!"] - for response in assistant_responses: - response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, response_ids) - LOG.debug( - f"Assistant response '{response}' expected IDs: {response_ids}, found at: {start_idx}" - ) - assert start_idx != -1, f"Could not find '{response}' in input_ids" - assert all( - label != IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(response_ids)] - ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" - - # Verify that human inputs are not labeled - human_inputs = ["Hello", "How are you?"] - for input_text in human_inputs: - input_ids = llama3_tokenizer.encode(input_text, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, input_ids) - LOG.debug( - f"Human input '{input_text}' expected IDs: {input_ids}, found at: {start_idx}" - ) - assert start_idx != -1, f"Could not find '{input_text}' in input_ids" - assert all( - label == IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(input_ids)] - ), f"Expected labels for human input '{input_text}' to be IGNORE_TOKEN_ID, but got {labels[start_idx:start_idx+len(input_ids)]}" - - def test_roles_to_train_assistant_only(self, llama3_tokenizer, basic_dataset): - LOG.info("Testing roles_to_train with assistant only") - strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), - tokenizer=llama3_tokenizer, - train_on_inputs=False, - sequence_len=512, - roles_to_train=["assistant"], - ) - res = strategy.tokenize_prompt(basic_dataset[0]) - labels = res["labels"] - input_ids = res["input_ids"] - - # Verify that only assistant responses are labeled - assistant_responses = ["Hi there!", "I'm doing well, thank you!"] - for response in assistant_responses: - response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, response_ids) - LOG.debug( - f"Assistant response '{response}' expected IDs: {response_ids}, found at: {start_idx}" - ) - assert all( - label != IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(response_ids)] - ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" - - def test_roles_to_train_all(self, llama3_tokenizer, basic_dataset): - LOG.info("Testing roles_to_train with all roles") - strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), - tokenizer=llama3_tokenizer, - train_on_inputs=True, - sequence_len=512, - roles_to_train=["human", "assistant"], - ) - res = strategy.tokenize_prompt(basic_dataset[0]) - labels = res["labels"] - input_ids = res["input_ids"] - - # Verify that all responses are labeled (except for special tokens) - all_responses = [ - "Hello", - "Hi there!", - "How are you?", - "I'm doing well, thank you!", - ] - for response in all_responses: - response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, response_ids) - LOG.debug( - f"Response '{response}' expected IDs: {response_ids}, found at: {start_idx}" - ) - assert all( - label != IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(response_ids)] - ), f"Expected labels for response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" - - def test_empty_roles_to_train(self, llama3_tokenizer, basic_dataset): - LOG.info("Testing with empty roles_to_train") - strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), - tokenizer=llama3_tokenizer, - train_on_inputs=False, - sequence_len=512, - roles_to_train=[], - train_on_eos="none", # Add this line - ) - res = strategy.tokenize_prompt(basic_dataset[0]) - labels = res["labels"] - - # Verify that no labels are set when roles_to_train is empty - LOG.debug("Full labels: %s", labels) - assert all( - label == IGNORE_TOKEN_ID for label in labels - ), "Expected all labels to be IGNORE_TOKEN_ID when roles_to_train is empty" - - def test_train_on_eos_all(self, llama3_tokenizer, basic_dataset): - LOG.info("Testing with train_on_eos='all'") - strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), - tokenizer=llama3_tokenizer, - train_on_inputs=False, - sequence_len=512, - roles_to_train=["assistant"], - train_on_eos="all", - ) - res = strategy.tokenize_prompt(basic_dataset[0]) - labels = res["labels"] - input_ids = res["input_ids"] - - eos_token_id = llama3_tokenizer.eos_token_id - eos_indices = [ - i for i, token_id in enumerate(input_ids) if token_id == eos_token_id - ] - - assert len(eos_indices) > 0, "Expected at least one EOS token in the input" - for eos_idx in eos_indices: - assert ( - labels[eos_idx] != IGNORE_TOKEN_ID - ), f"Expected EOS token at index {eos_idx} to be labeled" - - def test_train_on_eos_turn(self, llama3_tokenizer, basic_dataset): - LOG.info("Testing with train_on_eos='turn'") - strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), - tokenizer=llama3_tokenizer, - train_on_inputs=False, - sequence_len=512, - roles_to_train=["assistant"], - train_on_eos="turn", - ) - res = strategy.tokenize_prompt(basic_dataset[0]) - labels = res["labels"] - input_ids = res["input_ids"] - - eos_token_id = llama3_tokenizer.eos_token_id - assistant_responses = ["Hi there!", "I'm doing well, thank you!"] - - for response in assistant_responses: - response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, response_ids) - assert start_idx != -1, f"Could not find '{response}' in input_ids" - - eos_idx = start_idx + len(response_ids) - while eos_idx < len(input_ids) and input_ids[eos_idx] != eos_token_id: - eos_idx += 1 - - assert eos_idx < len( - input_ids - ), f"Could not find EOS token after '{response}'" - assert ( - labels[eos_idx] != IGNORE_TOKEN_ID - ), f"Expected EOS token after assistant response '{response}' to be labeled" - - # Check that EOS tokens after human inputs are not labeled - human_inputs = ["Hello", "How are you?"] - for input_text in human_inputs: - input_ids = llama3_tokenizer.encode(input_text, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, input_ids) - assert start_idx != -1, f"Could not find '{input_text}' in input_ids" - - eos_idx = start_idx + len(input_ids) - while eos_idx < len(input_ids) and input_ids[eos_idx] != eos_token_id: - eos_idx += 1 - - assert ( - labels[eos_idx] == IGNORE_TOKEN_ID - ), f"Expected EOS token after human input '{input_text}' to not be labeled" - - def test_train_on_eos_last(self, llama3_tokenizer, basic_dataset): - LOG.info("Testing with train_on_eos='last'") - strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), - tokenizer=llama3_tokenizer, - train_on_inputs=False, - sequence_len=512, - roles_to_train=["assistant"], - train_on_eos="last", - ) - res = strategy.tokenize_prompt(basic_dataset[0]) - labels = res["labels"] - input_ids = res["input_ids"] - - eos_token_id = llama3_tokenizer.eos_token_id - eos_indices = [ - i for i, token_id in enumerate(input_ids) if token_id == eos_token_id - ] - - assert len(eos_indices) > 0, "Expected at least one EOS token in the input" - last_eos_idx = eos_indices[-1] - - # Check that only the last EOS token is labeled - for idx in eos_indices[:-1]: - assert ( - labels[idx] == IGNORE_TOKEN_ID - ), f"Expected EOS token at index {idx} to not be labeled" - assert ( - labels[last_eos_idx] != IGNORE_TOKEN_ID - ), f"Expected last EOS token at index {last_eos_idx} to be labeled" - - def test_train_on_eos_none(self, llama3_tokenizer, basic_dataset): - LOG.info("Testing with train_on_eos='none'") - strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), - tokenizer=llama3_tokenizer, - train_on_inputs=False, - sequence_len=512, - roles_to_train=["assistant"], - train_on_eos="none", - ) - res = strategy.tokenize_prompt(basic_dataset[0]) - labels = res["labels"] - input_ids = res["input_ids"] - - eos_token_id = llama3_tokenizer.eos_token_id - eos_indices = [ - i for i, token_id in enumerate(input_ids) if token_id == eos_token_id - ] - - assert len(eos_indices) > 0, "Expected at least one EOS token in the input" - for eos_idx in eos_indices: - assert ( - labels[eos_idx] == IGNORE_TOKEN_ID - ), f"Expected EOS token at index {eos_idx} to not be labeled" - - def test_drop_system_message(self, llama3_tokenizer, basic_dataset): - LOG.info("Testing with drop_system_message=True") - strategy = ChatTemplateStrategy( - ChatTemplatePrompter( - llama3_tokenizer, chat_templates("llama3"), drop_system_message=True - ), - tokenizer=llama3_tokenizer, - train_on_inputs=False, - sequence_len=512, - roles_to_train=["assistant"], - ) - res = strategy.tokenize_prompt(basic_dataset[0]) - input_ids = res["input_ids"] - - # Check if system message is not present in input_ids - system_message = "You are an AI assistant." - system_ids = llama3_tokenizer.encode(system_message, add_special_tokens=False) - assert ( - self.find_sublist(input_ids, system_ids) == -1 - ), "Expected system message to be dropped" - - def test_custom_roles(self, llama3_tokenizer): - LOG.info("Testing with custom roles mapping") - custom_roles = { - "user": ["human", "user"], - "assistant": ["ai", "assistant"], - "system": ["context"], - } - strategy = ChatTemplateStrategy( - ChatTemplatePrompter( - llama3_tokenizer, chat_templates("llama3"), roles=custom_roles - ), - tokenizer=llama3_tokenizer, - train_on_inputs=False, - sequence_len=512, - roles_to_train=["ai"], - ) - - # Create a new dataset with modified role names - modified_conversations = [ - {"from": "context", "value": "You are an AI assistant."}, - {"from": "human", "value": "Hello"}, - {"from": "ai", "value": "Hi there!"}, - {"from": "human", "value": "How are you?"}, - {"from": "ai", "value": "I'm doing well, thank you!"}, - ] - - modified_dataset = Dataset.from_dict( - {"conversations": [modified_conversations]} - ) - - res = strategy.tokenize_prompt(modified_dataset[0]) - labels = res["labels"] - input_ids = res["input_ids"] - - # Check if AI responses are labeled correctly - ai_responses = ["Hi there!", "I'm doing well, thank you!"] - for response in ai_responses: - response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, response_ids) - assert start_idx != -1, f"Could not find response '{response}' in input_ids" - assert all( - label != IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(response_ids)] - ), f"Expected labels for AI response '{response}' to be set" - - # Check if human messages are not labeled - human_messages = ["Hello", "How are you?"] - for message in human_messages: - message_ids = llama3_tokenizer.encode(message, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, message_ids) - assert start_idx != -1, f"Could not find message '{message}' in input_ids" - assert all( - label == IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(message_ids)] - ), f"Expected labels for human message '{message}' to be IGNORE_TOKEN_ID" - - def test_message_field_training(self, llama3_tokenizer): - LOG.info("Testing with message_field_training") - strategy = ChatTemplateStrategy( - ChatTemplatePrompter( - llama3_tokenizer, - chat_templates("llama3"), - message_field_training="train", - message_field_training_detail="train_detail", - ), - tokenizer=llama3_tokenizer, - train_on_inputs=False, - sequence_len=512, - roles_to_train=[], - ) - - # Create a new dataset with the train and train_detail fields - modified_conversation = [ - {"from": "system", "value": "You are an AI assistant.", "train": False}, - {"from": "human", "value": "Hello", "train": False}, - {"from": "assistant", "value": "Hello", "train": True}, - {"from": "human", "value": "How are you?", "train": True}, - { - "from": "assistant", - "value": "I'm doing very well, thank you!", - "train_detail": [ - {"begin_offset": 0, "end_offset": 8, "train": False}, - {"begin_offset": 9, "end_offset": 18, "train": True}, - {"begin_offset": 19, "end_offset": 30, "train": False}, - ], - }, - { - "from": "human", - "value": "I'm doing very well, thank you!", - "train": False, - }, - {"from": "assistant", "value": "Hi there!", "train": True}, - ] - - modified_dataset = Dataset.from_dict({"conversations": [modified_conversation]}) - - res = strategy.tokenize_prompt(modified_dataset[0]) - labels = res["labels"] - input_ids = res["input_ids"] - - # Function to find all occurrences of a sublist - def find_all_sublists(full_list, sub_list): - indices = [] - for index in range(len(full_list) - len(sub_list) + 1): - if full_list[index : index + len(sub_list)] == sub_list: - indices.append(index) - return indices - - # Keep track of which occurrences we've processed - processed_occurrences = {} - # Check if messages are labeled correctly based on train or train_detail - for i, turn in enumerate(modified_conversation): - turn_tokens = llama3_tokenizer.encode( - turn["value"], add_special_tokens=False - ) - occurrences = find_all_sublists(input_ids, turn_tokens) - turn_key = turn["value"] - if turn_key not in processed_occurrences: - processed_occurrences[turn_key] = 0 - current_occurrence = processed_occurrences[turn_key] - - if current_occurrence >= len(occurrences): - assert ( - False - ), f"Not enough occurrences found for message: {turn['value']}" - - start_idx = occurrences[current_occurrence] - processed_occurrences[turn_key] += 1 - end_idx = start_idx + len(turn_tokens) - - LOG.debug( - f"Processing turn {i}: role={turn['from']}, content='{turn['value']}', start_idx={start_idx}, end_idx={end_idx}" - ) - - if "train_detail" in turn: - # Get token offsets - tokenized_output = llama3_tokenizer( - turn["value"], return_offsets_mapping=True, add_special_tokens=False - ) - token_offsets = tokenized_output["offset_mapping"] - - # Adjust token offsets as done in the implementation - for i in range(len(token_offsets) - 1): - token_offsets[i] = ( - token_offsets[i][0], - token_offsets[i + 1][0] - 1, - ) - token_offsets[-1] = (token_offsets[-1][0], len(turn["value"]) - 1) - - # Adjust train_details - adjusted_train_details = strategy.prompter.adjust_train_details( - turn["train_detail"], token_offsets - ) - - LOG.debug(f"Original train_details: {turn['train_detail']}") - LOG.debug(f"Adjusted train_details: {adjusted_train_details}") - - # Handle train_detail - token_offsets = strategy.prompter.get_offsets_for_train_detail( - text=turn["value"], - train_details=adjusted_train_details, - mask_untrainable=False, - ) - token_offsets_masked = strategy.prompter.get_offsets_for_train_detail( - text=turn["value"], - train_details=adjusted_train_details, - mask_untrainable=True, - ) - LOG.debug(f"Token offsets: {token_offsets_masked}") - - expected_labels = [IGNORE_TOKEN_ID] * len(turn_tokens) - for i, offset in enumerate(token_offsets_masked): - if offset != IGNORE_TOKEN_ID: - expected_labels[i] = turn_tokens[i] - actual_labels = labels[ - start_idx : start_idx + len(token_offsets_masked) - ] - assert ( - actual_labels == expected_labels - ), f"Labels mismatch for turn: {turn['value']}\nExpected: {expected_labels}\nActual: {actual_labels}" - - for detail in adjusted_train_details: - # Find the token indices that correspond to the character offsets - detail_start = start_idx + next( - i - for i, offset in enumerate(token_offsets) - if offset >= detail["begin_offset"] - ) - detail_end = start_idx + next( - ( - i - for i, offset in enumerate(token_offsets) - if offset > detail["end_offset"] - ), - len(token_offsets), - ) - - detail_text = turn["value"][ - detail["begin_offset"] : detail["end_offset"] + 1 - ] - detail_labels = labels[detail_start:detail_end] - detail_input_ids = input_ids[detail_start:detail_end] - - LOG.debug( - f"Detail: '{detail_text}', Start: {detail_start}, End: {detail_end}" - ) - LOG.debug(f"Detail input_ids: {detail_input_ids}") - LOG.debug(f"Detail labels: {detail_labels}") - LOG.debug( - f"Decoded detail: {llama3_tokenizer.decode(detail_input_ids)}" - ) - LOG.debug( - f"Token offsets for this detail: {token_offsets[detail_start-start_idx:detail_end-start_idx]}" - ) - - if detail["train"]: - assert all( - label != IGNORE_TOKEN_ID for label in detail_labels - ), ( - f"Expected labels for trainable detail '{detail_text}' to be set, but some were IGNORE_TOKEN_ID. " - f"Labels({detail_start}:{detail_end}): {detail_labels}, " - f"InputIDs: {detail_input_ids}, " - f"Decoded: '{llama3_tokenizer.decode(detail_input_ids)}'" - ) - else: - assert all( - label == IGNORE_TOKEN_ID for label in detail_labels - ), ( - f"Expected all labels for non-trainable detail '{detail_text}' to be IGNORE_TOKEN_ID, but some were not. " - f"Labels({detail_start}:{detail_end}): {detail_labels}, " - f"InputIDs: {detail_input_ids}, " - f"Decoded: '{llama3_tokenizer.decode(detail_input_ids)}'" - ) - else: - should_train = turn.get("train", False) - turn_labels = labels[start_idx:end_idx] - - LOG.debug(f"Should train: {should_train}") - LOG.debug(f"Turn indices: start={start_idx}, end={end_idx}") - LOG.debug(f"Turn labels: {turn_labels}") - LOG.debug(f"Turn input IDs: {input_ids[start_idx:end_idx]}") - LOG.debug( - f"Decoded turn: {llama3_tokenizer.decode(input_ids[start_idx:end_idx])}" - ) - - if should_train: - assert all(label != IGNORE_TOKEN_ID for label in turn_labels), ( - f"Expected all labels for '{turn['value']}' to be set\n" - f"Labels({start_idx}:{end_idx}): {turn_labels}, " - f"InputIDs: {input_ids[start_idx:end_idx]}, " - f"Decoded: '{llama3_tokenizer.decode(input_ids[start_idx:end_idx])}'" - ) - else: - assert all(label == IGNORE_TOKEN_ID for label in turn_labels), ( - f"Expected all labels for '{turn['value']}' to be IGNORE_TOKEN_ID\n" - f"Labels({start_idx}:{end_idx}): {turn_labels}, " - f"InputIDs: {input_ids[start_idx:end_idx]}, " - f"Decoded: '{llama3_tokenizer.decode(input_ids[start_idx:end_idx])}'" - ) - - LOG.debug( - f"Processed turn: {turn['from']}, content: '{turn['value']}', " - f"start_idx: {start_idx}, end_idx: {end_idx}, " - f"labels: {labels[start_idx:end_idx]}" - ) - - LOG.debug(f"Final labels: {labels}") - LOG.debug(f"Final input_ids: {input_ids}") - - class TestAssistantChatTemplateLlama3: """ Test class for assistant style datasets with llama-3 prompts using the chat_template strategy. @@ -740,7 +85,6 @@ def test_llama3(self, llama3_tokenizer, assistant_dataset): tokenizer=llama3_tokenizer, train_on_inputs=False, sequence_len=512, - roles_to_train=["assistant"], ) strategy.messages = "messages" res = strategy.tokenize_prompt(assistant_dataset[0]) @@ -764,6 +108,64 @@ def test_llama3(self, llama3_tokenizer, assistant_dataset): input_ids == expected_input_ids ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + def test_phi35(self, phi35_tokenizer, assistant_dataset): + LOG.info("Testing phi-3.5 with assistant dataset") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + phi35_tokenizer, + chat_templates("phi_35"), + message_field_role="role", + message_field_content="content", + roles={ + "user": ["user"], + "assistant": ["assistant"], + "system": ["system"], + }, + ), + tokenizer=phi35_tokenizer, + train_on_inputs=False, + sequence_len=512, + ) + strategy.messages = "messages" + res = strategy.tokenize_prompt(assistant_dataset[0]) + input_ids = res["input_ids"] + labels = res["labels"] + # fmt: off + expected_input_ids = [ + 32010, # user + 22172, 32007, # user eot + 32001, # assistant + 22172, 32007, # assistant eot + 32010, # user + 1781, 26966, 32007, # user eot + 32001, # assistant + 1781, 26966, 32007, # assistant eot + 32000, # eos + ] + expected_labels = [ + -100, # user + -100, -100, # user eot + -100, # assistant + -100, -100, # assistant eot, + -100, # user + -100, -100, -100, # user eot + -100, # assistant + 1781, 26966, 32007, # assistant eot + 32000, # eos + ] + # fmt: on + LOG.debug(f"Expected input_ids: {expected_input_ids}") + LOG.debug(f"Actual input_ids: {input_ids}") + assert ( + input_ids == expected_input_ids + ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + + LOG.debug(f"Expected labels : {expected_labels}") + LOG.debug(f"Actual labels : {labels}") + assert ( + labels == expected_labels + ), f"Input IDs mismatch: {labels} != {expected_labels}" + def test_llama3_with_training_data(self, llama3_tokenizer, assistant_dataset): LOG.info("Testing llama-3 with assistant dataset including training data") strategy = ChatTemplateStrategy( diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py new file mode 100644 index 0000000000..f18fb39423 --- /dev/null +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -0,0 +1,615 @@ +""" +tests for chat_template prompt strategy +""" + +import logging +import unittest + +from datasets import Dataset + +from axolotl.prompt_strategies.chat_template import ( + ChatTemplatePrompter, + ChatTemplateStrategy, +) +from axolotl.prompters import IGNORE_TOKEN_ID +from axolotl.utils.chat_templates import chat_templates + +logging.basicConfig(level=logging.DEBUG) +LOG = logging.getLogger("axolotl") + + +class TestChatTemplateConfigurations: + """ + Test class for various configurations of ChatTemplateStrategy. + """ + + @staticmethod + def find_sublist(full_list, sub_list): + token_count = len(sub_list) + for index in range(len(full_list) - token_count + 1): + if full_list[index : index + token_count] == sub_list: + return index + return -1 + + def test_train_on_inputs_true(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with train_on_inputs=True") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=True, + sequence_len=512, + roles_to_train=["assistant"], + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Verify that assistant responses are labeled + assistant_responses = ["Hi there!", "I'm doing well, thank you!"] + for response in assistant_responses: + response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, response_ids) + LOG.debug( + f"Assistant response '{response}' expected IDs: {response_ids}, found at: {start_idx}" + ) + assert start_idx != -1, f"Could not find '{response}' in input_ids" + assert all( + label != IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(response_ids)] + ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" + + # Check the behavior of human inputs + human_inputs = ["Hello", "How are you?"] + for input_text in human_inputs: + input_ids = llama3_tokenizer.encode(input_text, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, input_ids) + labeled = all( + label != IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(input_ids)] + ) + LOG.debug( + f"Human input '{input_text}' is {'labeled' if labeled else 'not labeled'}, expected IDs: {input_ids}, found at: {start_idx}" + ) + + LOG.debug("Full labels: %s", labels) + LOG.debug("Full input_ids: %s", input_ids) + + def test_train_on_inputs_false(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with train_on_inputs=False") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Verify that only assistant responses are labeled + assistant_responses = ["Hi there!", "I'm doing well, thank you!"] + for response in assistant_responses: + response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, response_ids) + LOG.debug( + f"Assistant response '{response}' expected IDs: {response_ids}, found at: {start_idx}" + ) + assert start_idx != -1, f"Could not find '{response}' in input_ids" + assert all( + label != IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(response_ids)] + ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" + + # Verify that human inputs are not labeled + human_inputs = ["Hello", "How are you?"] + for input_text in human_inputs: + input_ids = llama3_tokenizer.encode(input_text, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, input_ids) + LOG.debug( + f"Human input '{input_text}' expected IDs: {input_ids}, found at: {start_idx}" + ) + assert start_idx != -1, f"Could not find '{input_text}' in input_ids" + assert all( + label == IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(input_ids)] + ), f"Expected labels for human input '{input_text}' to be IGNORE_TOKEN_ID, but got {labels[start_idx:start_idx+len(input_ids)]}" + + def test_roles_to_train_assistant_only(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing roles_to_train with assistant only") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Verify that only assistant responses are labeled + assistant_responses = ["Hi there!", "I'm doing well, thank you!"] + for response in assistant_responses: + response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, response_ids) + LOG.debug( + f"Assistant response '{response}' expected IDs: {response_ids}, found at: {start_idx}" + ) + assert all( + label != IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(response_ids)] + ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" + + def test_roles_to_train_all(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing roles_to_train with all roles") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=True, + sequence_len=512, + roles_to_train=["human", "assistant"], + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Verify that all responses are labeled (except for special tokens) + all_responses = [ + "Hello", + "Hi there!", + "How are you?", + "I'm doing well, thank you!", + ] + for response in all_responses: + response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, response_ids) + LOG.debug( + f"Response '{response}' expected IDs: {response_ids}, found at: {start_idx}" + ) + assert all( + label != IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(response_ids)] + ), f"Expected labels for response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" + + def test_empty_roles_to_train(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with empty roles_to_train") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=[], + train_on_eos="none", # Add this line + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + + # Verify that no labels are set when roles_to_train is empty + LOG.debug("Full labels: %s", labels) + assert all( + label == IGNORE_TOKEN_ID for label in labels + ), "Expected all labels to be IGNORE_TOKEN_ID when roles_to_train is empty" + + def test_train_on_eos_all(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with train_on_eos='all'") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + train_on_eos="all", + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + eos_token_id = llama3_tokenizer.eos_token_id + eos_indices = [ + i for i, token_id in enumerate(input_ids) if token_id == eos_token_id + ] + + assert len(eos_indices) > 0, "Expected at least one EOS token in the input" + for eos_idx in eos_indices: + assert ( + labels[eos_idx] != IGNORE_TOKEN_ID + ), f"Expected EOS token at index {eos_idx} to be labeled" + + def test_train_on_eos_turn(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with train_on_eos='turn'") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + train_on_eos="turn", + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + eos_token_id = llama3_tokenizer.eos_token_id + assistant_responses = ["Hi there!", "I'm doing well, thank you!"] + + for response in assistant_responses: + response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, response_ids) + assert start_idx != -1, f"Could not find '{response}' in input_ids" + + eos_idx = start_idx + len(response_ids) + while eos_idx < len(input_ids) and input_ids[eos_idx] != eos_token_id: + eos_idx += 1 + + assert eos_idx < len( + input_ids + ), f"Could not find EOS token after '{response}'" + assert ( + labels[eos_idx] != IGNORE_TOKEN_ID + ), f"Expected EOS token after assistant response '{response}' to be labeled" + + # Check that EOS tokens after human inputs are not labeled + human_inputs = ["Hello", "How are you?"] + for input_text in human_inputs: + input_ids = llama3_tokenizer.encode(input_text, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, input_ids) + assert start_idx != -1, f"Could not find '{input_text}' in input_ids" + + eos_idx = start_idx + len(input_ids) + while eos_idx < len(input_ids) and input_ids[eos_idx] != eos_token_id: + eos_idx += 1 + + assert ( + labels[eos_idx] == IGNORE_TOKEN_ID + ), f"Expected EOS token after human input '{input_text}' to not be labeled" + + def test_train_on_eos_last(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with train_on_eos='last'") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + train_on_eos="last", + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + eos_token_id = llama3_tokenizer.eos_token_id + eos_indices = [ + i for i, token_id in enumerate(input_ids) if token_id == eos_token_id + ] + + assert len(eos_indices) > 0, "Expected at least one EOS token in the input" + last_eos_idx = eos_indices[-1] + + # Check that only the last EOS token is labeled + for idx in eos_indices[:-1]: + assert ( + labels[idx] == IGNORE_TOKEN_ID + ), f"Expected EOS token at index {idx} to not be labeled" + assert ( + labels[last_eos_idx] != IGNORE_TOKEN_ID + ), f"Expected last EOS token at index {last_eos_idx} to be labeled" + + def test_train_on_eos_none(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with train_on_eos='none'") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + train_on_eos="none", + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + eos_token_id = llama3_tokenizer.eos_token_id + eos_indices = [ + i for i, token_id in enumerate(input_ids) if token_id == eos_token_id + ] + + assert len(eos_indices) > 0, "Expected at least one EOS token in the input" + for eos_idx in eos_indices: + assert ( + labels[eos_idx] == IGNORE_TOKEN_ID + ), f"Expected EOS token at index {eos_idx} to not be labeled" + + def test_drop_system_message(self, llama3_tokenizer, basic_dataset): + LOG.info("Testing with drop_system_message=True") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + llama3_tokenizer, chat_templates("llama3"), drop_system_message=True + ), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + ) + res = strategy.tokenize_prompt(basic_dataset[0]) + input_ids = res["input_ids"] + + # Check if system message is not present in input_ids + system_message = "You are an AI assistant." + system_ids = llama3_tokenizer.encode(system_message, add_special_tokens=False) + assert ( + self.find_sublist(input_ids, system_ids) == -1 + ), "Expected system message to be dropped" + + def test_custom_roles(self, llama3_tokenizer): + LOG.info("Testing with custom roles mapping") + custom_roles = { + "user": ["human", "user"], + "assistant": ["ai", "assistant"], + "system": ["context"], + } + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + llama3_tokenizer, chat_templates("llama3"), roles=custom_roles + ), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["ai"], + ) + + # Create a new dataset with modified role names + modified_conversations = [ + {"from": "context", "value": "You are an AI assistant."}, + {"from": "human", "value": "Hello"}, + {"from": "ai", "value": "Hi there!"}, + {"from": "human", "value": "How are you?"}, + {"from": "ai", "value": "I'm doing well, thank you!"}, + ] + + modified_dataset = Dataset.from_dict( + {"conversations": [modified_conversations]} + ) + + res = strategy.tokenize_prompt(modified_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Check if AI responses are labeled correctly + ai_responses = ["Hi there!", "I'm doing well, thank you!"] + for response in ai_responses: + response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, response_ids) + assert start_idx != -1, f"Could not find response '{response}' in input_ids" + assert all( + label != IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(response_ids)] + ), f"Expected labels for AI response '{response}' to be set" + + # Check if human messages are not labeled + human_messages = ["Hello", "How are you?"] + for message in human_messages: + message_ids = llama3_tokenizer.encode(message, add_special_tokens=False) + start_idx = self.find_sublist(input_ids, message_ids) + assert start_idx != -1, f"Could not find message '{message}' in input_ids" + assert all( + label == IGNORE_TOKEN_ID + for label in labels[start_idx : start_idx + len(message_ids)] + ), f"Expected labels for human message '{message}' to be IGNORE_TOKEN_ID" + + def test_message_field_training(self, llama3_tokenizer): + LOG.info("Testing with message_field_training") + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + llama3_tokenizer, + chat_templates("llama3"), + message_field_training="train", + message_field_training_detail="train_detail", + ), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=[], + ) + + # Create a new dataset with the train and train_detail fields + modified_conversation = [ + {"from": "system", "value": "You are an AI assistant.", "train": False}, + {"from": "human", "value": "Hello", "train": False}, + {"from": "assistant", "value": "Hello", "train": True}, + {"from": "human", "value": "How are you?", "train": True}, + { + "from": "assistant", + "value": "I'm doing very well, thank you!", + "train_detail": [ + {"begin_offset": 0, "end_offset": 8, "train": False}, + {"begin_offset": 9, "end_offset": 18, "train": True}, + {"begin_offset": 19, "end_offset": 30, "train": False}, + ], + }, + { + "from": "human", + "value": "I'm doing very well, thank you!", + "train": False, + }, + {"from": "assistant", "value": "Hi there!", "train": True}, + ] + + modified_dataset = Dataset.from_dict({"conversations": [modified_conversation]}) + + res = strategy.tokenize_prompt(modified_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Function to find all occurrences of a sublist + def find_all_sublists(full_list, sub_list): + indices = [] + for index in range(len(full_list) - len(sub_list) + 1): + if full_list[index : index + len(sub_list)] == sub_list: + indices.append(index) + return indices + + # Keep track of which occurrences we've processed + processed_occurrences = {} + # Check if messages are labeled correctly based on train or train_detail + for i, turn in enumerate(modified_conversation): + turn_tokens = llama3_tokenizer.encode( + turn["value"], add_special_tokens=False + ) + occurrences = find_all_sublists(input_ids, turn_tokens) + turn_key = turn["value"] + if turn_key not in processed_occurrences: + processed_occurrences[turn_key] = 0 + current_occurrence = processed_occurrences[turn_key] + + if current_occurrence >= len(occurrences): + assert ( + False + ), f"Not enough occurrences found for message: {turn['value']}" + + start_idx = occurrences[current_occurrence] + processed_occurrences[turn_key] += 1 + end_idx = start_idx + len(turn_tokens) + + LOG.debug( + f"Processing turn {i}: role={turn['from']}, content='{turn['value']}', start_idx={start_idx}, end_idx={end_idx}" + ) + + if "train_detail" in turn: + # Get token offsets + tokenized_output = llama3_tokenizer( + turn["value"], return_offsets_mapping=True, add_special_tokens=False + ) + token_offsets = tokenized_output["offset_mapping"] + + # Adjust token offsets as done in the implementation + for i in range(len(token_offsets) - 1): + token_offsets[i] = ( + token_offsets[i][0], + token_offsets[i + 1][0] - 1, + ) + token_offsets[-1] = (token_offsets[-1][0], len(turn["value"]) - 1) + + # Adjust train_details + adjusted_train_details = strategy.prompter.adjust_train_details( + turn["train_detail"], token_offsets + ) + + LOG.debug(f"Original train_details: {turn['train_detail']}") + LOG.debug(f"Adjusted train_details: {adjusted_train_details}") + + # Handle train_detail + token_offsets = strategy.prompter.get_offsets_for_train_detail( + text=turn["value"], + train_details=adjusted_train_details, + mask_untrainable=False, + ) + token_offsets_masked = strategy.prompter.get_offsets_for_train_detail( + text=turn["value"], + train_details=adjusted_train_details, + mask_untrainable=True, + ) + LOG.debug(f"Token offsets: {token_offsets_masked}") + + expected_labels = [IGNORE_TOKEN_ID] * len(turn_tokens) + for i, offset in enumerate(token_offsets_masked): + if offset != IGNORE_TOKEN_ID: + expected_labels[i] = turn_tokens[i] + actual_labels = labels[ + start_idx : start_idx + len(token_offsets_masked) + ] + assert ( + actual_labels == expected_labels + ), f"Labels mismatch for turn: {turn['value']}\nExpected: {expected_labels}\nActual: {actual_labels}" + + for detail in adjusted_train_details: + # Find the token indices that correspond to the character offsets + detail_start = start_idx + next( + i + for i, offset in enumerate(token_offsets) + if offset >= detail["begin_offset"] + ) + detail_end = start_idx + next( + ( + i + for i, offset in enumerate(token_offsets) + if offset > detail["end_offset"] + ), + len(token_offsets), + ) + + detail_text = turn["value"][ + detail["begin_offset"] : detail["end_offset"] + 1 + ] + detail_labels = labels[detail_start:detail_end] + detail_input_ids = input_ids[detail_start:detail_end] + + LOG.debug( + f"Detail: '{detail_text}', Start: {detail_start}, End: {detail_end}" + ) + LOG.debug(f"Detail input_ids: {detail_input_ids}") + LOG.debug(f"Detail labels: {detail_labels}") + LOG.debug( + f"Decoded detail: {llama3_tokenizer.decode(detail_input_ids)}" + ) + LOG.debug( + f"Token offsets for this detail: {token_offsets[detail_start-start_idx:detail_end-start_idx]}" + ) + + if detail["train"]: + assert all( + label != IGNORE_TOKEN_ID for label in detail_labels + ), ( + f"Expected labels for trainable detail '{detail_text}' to be set, but some were IGNORE_TOKEN_ID. " + f"Labels({detail_start}:{detail_end}): {detail_labels}, " + f"InputIDs: {detail_input_ids}, " + f"Decoded: '{llama3_tokenizer.decode(detail_input_ids)}'" + ) + else: + assert all( + label == IGNORE_TOKEN_ID for label in detail_labels + ), ( + f"Expected all labels for non-trainable detail '{detail_text}' to be IGNORE_TOKEN_ID, but some were not. " + f"Labels({detail_start}:{detail_end}): {detail_labels}, " + f"InputIDs: {detail_input_ids}, " + f"Decoded: '{llama3_tokenizer.decode(detail_input_ids)}'" + ) + else: + should_train = turn.get("train", False) + turn_labels = labels[start_idx:end_idx] + + LOG.debug(f"Should train: {should_train}") + LOG.debug(f"Turn indices: start={start_idx}, end={end_idx}") + LOG.debug(f"Turn labels: {turn_labels}") + LOG.debug(f"Turn input IDs: {input_ids[start_idx:end_idx]}") + LOG.debug( + f"Decoded turn: {llama3_tokenizer.decode(input_ids[start_idx:end_idx])}" + ) + + if should_train: + assert all(label != IGNORE_TOKEN_ID for label in turn_labels), ( + f"Expected all labels for '{turn['value']}' to be set\n" + f"Labels({start_idx}:{end_idx}): {turn_labels}, " + f"InputIDs: {input_ids[start_idx:end_idx]}, " + f"Decoded: '{llama3_tokenizer.decode(input_ids[start_idx:end_idx])}'" + ) + else: + assert all(label == IGNORE_TOKEN_ID for label in turn_labels), ( + f"Expected all labels for '{turn['value']}' to be IGNORE_TOKEN_ID\n" + f"Labels({start_idx}:{end_idx}): {turn_labels}, " + f"InputIDs: {input_ids[start_idx:end_idx]}, " + f"Decoded: '{llama3_tokenizer.decode(input_ids[start_idx:end_idx])}'" + ) + + LOG.debug( + f"Processed turn: {turn['from']}, content: '{turn['value']}', " + f"start_idx: {start_idx}, end_idx: {end_idx}, " + f"labels: {labels[start_idx:end_idx]}" + ) + + LOG.debug(f"Final labels: {labels}") + LOG.debug(f"Final input_ids: {input_ids}") + + +if __name__ == "__main__": + unittest.main() From d7eea2ff343e9f0653ce82fc1826b41efa9bc2f6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 24 Sep 2024 14:05:58 -0400 Subject: [PATCH 0165/1405] validation fixes 20240923 (#1925) * validation fixes 20240923 * fix run name for wandb and defaults for chat template fields * fix gradio inference with llama chat template --- src/axolotl/cli/__init__.py | 27 +++++++++++++++++-- src/axolotl/core/trainer_builder.py | 8 ++++++ .../prompt_strategies/chat_template.py | 4 +-- .../config/models/input/v0_4_1/__init__.py | 10 ++++++- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index aaa62423ca..13c5b4ab58 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -30,6 +30,7 @@ from axolotl.integrations.base import PluginManager from axolotl.logging_config import configure_logging from axolotl.train import TrainDatasetMeta +from axolotl.utils.chat_templates import chat_templates from axolotl.utils.config import ( normalize_cfg_datasets, normalize_config, @@ -234,7 +235,8 @@ def do_inference_gradio( model, tokenizer = load_model_and_tokenizer(cfg=cfg, cli_args=cli_args) prompter = cli_args.prompter - default_tokens = {"unk_token": "", "bos_token": "", "eos_token": ""} + # default_tokens = {"unk_token": "", "bos_token": "", "eos_token": ""} + default_tokens: Dict[str, str] = {} for token, symbol in default_tokens.items(): # If the token isn't already specified in the config, add it @@ -242,10 +244,13 @@ def do_inference_gradio( tokenizer.add_special_tokens({token: symbol}) prompter_module = None + chat_template_str = None if prompter: prompter_module = getattr( importlib.import_module("axolotl.prompters"), prompter ) + elif cfg.chat_template: + chat_template_str = chat_templates(cfg.chat_template) model = model.to(cfg.device, dtype=cfg.torch_dtype) @@ -259,7 +264,24 @@ def generate(instruction): ) else: prompt = instruction.strip() - batch = tokenizer(prompt, return_tensors="pt", add_special_tokens=True) + + if chat_template_str: + batch = tokenizer.apply_chat_template( + [ + { + "role": "user", + "content": prompt, + } + ], + return_tensors="pt", + add_special_tokens=True, + add_generation_prompt=True, + chat_template=chat_template_str, + tokenize=True, + return_dict=True, + ) + else: + batch = tokenizer(prompt, return_tensors="pt", add_special_tokens=True) model.eval() with torch.no_grad(): @@ -282,6 +304,7 @@ def generate(instruction): streamer = TextIteratorStreamer(tokenizer) generation_kwargs = { "inputs": batch["input_ids"].to(cfg.device), + "attention_mask": batch["attention_mask"].to(cfg.device), "generation_config": generation_config, "streamer": streamer, } diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index f4cd257838..7c3e437f80 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1417,6 +1417,8 @@ def build(self, total_num_steps): report_to = [] if self.cfg.use_wandb: report_to.append("wandb") + if self.cfg.wandb_name: + training_arguments_kwargs["run_name"] = self.cfg.wandb_name if self.cfg.use_mlflow: report_to.append("mlflow") if self.cfg.use_tensorboard: @@ -1574,6 +1576,12 @@ def build(self, total_num_steps): ) training_args = self.hook_post_create_training_args(training_args) + # unset run_name so wandb sets up experiment names + if self.cfg.use_wandb and training_args.run_name == training_args.output_dir: + training_args.run_name = ( # pylint: disable=attribute-defined-outside-init + None + ) + data_collator_kwargs = { "padding": True, # True/"longest" is the default } diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 717367eefa..88e748895d 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -375,8 +375,8 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): prompter_params = { "tokenizer": tokenizer, "chat_template": chat_templates(ds_cfg.get("chat_template", "chatml")), - "message_field_role": ds_cfg.get("message_field_role", "from"), - "message_field_content": ds_cfg.get("message_field_content", "value"), + "message_field_role": ds_cfg.get("message_field_role", "role"), + "message_field_content": ds_cfg.get("message_field_content", "content"), "message_field_training": ds_cfg.get("message_field_training", None), "message_field_training_detail": ds_cfg.get( "message_field_training_detail", diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 458bacdb12..2217855083 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -1017,12 +1017,20 @@ def validate_neftune_noise_alpha(cls, neftune_noise_alpha): return neftune_noise_alpha @model_validator(mode="after") - def check(self): + def check_rl_beta(self): if self.dpo_beta and not self.rl_beta: self.rl_beta = self.dpo_beta del self.dpo_beta return self + @model_validator(mode="after") + def check_simpo_warmup(self): + if self.rl == "simpo" and self.warmup_ratio: + raise ValueError( + "warmup_ratio is not supported with the simpo trainer. Please use `warmup_steps` instead" + ) + return self + @model_validator(mode="before") @classmethod def check_frozen(cls, data): From b98d7d7098f5d64a07c5a96855c4e08dca7afd91 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 26 Sep 2024 11:33:41 -0400 Subject: [PATCH 0166/1405] update upstream deps versions and replace lora+ (#1928) * update upstream deps versions and replace lora+ * typo transformers version --- requirements.txt | 8 +- src/axolotl/core/trainer_builder.py | 14 +-- src/axolotl/loraplus.py | 133 ---------------------------- 3 files changed, 11 insertions(+), 144 deletions(-) delete mode 100644 src/axolotl/loraplus.py diff --git a/requirements.txt b/requirements.txt index 32a9e0e01c..3f17e5d329 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 -peft==0.12.0 -transformers @ git+https://github.com/huggingface/transformers.git@0963229e287501bed52ae1dabc17922524de6992 +peft==0.13.0 +transformers==4.45.0 tokenizers>=0.19.1 -bitsandbytes==0.43.3 +bitsandbytes==0.44.0 accelerate==0.34.2 datasets==2.21.0 deepspeed==0.14.4 @@ -34,7 +34,7 @@ tensorboard python-dotenv==1.0.1 autoawq>=0.2.5 triton>=2.3.0 -liger-kernel==0.2.1 +liger-kernel==0.3.0 mamba-ssm==1.2.0.post1 diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 7c3e437f80..23ac0952ed 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -21,6 +21,7 @@ import torch import transformers from datasets import Dataset +from peft.optimizers import create_loraplus_optimizer from torch import nn from torch.optim.lr_scheduler import OneCycleLR from torch.utils.data import BatchSampler, DataLoader, RandomSampler, SequentialSampler @@ -45,7 +46,6 @@ ) from trl.trainer.utils import pad_to_length -from axolotl.loraplus import create_loraplus_optimizer from axolotl.monkeypatch.multipack import SUPPORTED_MULTIPACK_MODEL_TYPES from axolotl.monkeypatch.relora import ReLoRACallback, ReLoRAScheduler from axolotl.utils import is_mlflow_available @@ -461,9 +461,9 @@ def create_optimizer(self): self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init opt_model, optimizer_cls, - optimizer_kwargs, - loraplus_lr_ratio, - loraplus_lr_embedding, + loraplus_lr_ratio=loraplus_lr_ratio, + loraplus_lr_embedding=loraplus_lr_embedding, + **optimizer_kwargs, ) elif self.args.alternate_optimizer == "optimi_adamw": from optimi import AdamW @@ -969,9 +969,9 @@ def create_optimizer(self): self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init opt_model, optimizer_cls, - optimizer_kwargs, - loraplus_lr_ratio, - loraplus_lr_embedding, + loraplus_lr_ratio=loraplus_lr_ratio, + loraplus_lr_embedding=loraplus_lr_embedding, + **optimizer_kwargs, ) if is_sagemaker_mp_enabled(): diff --git a/src/axolotl/loraplus.py b/src/axolotl/loraplus.py deleted file mode 100644 index b4abec55ad..0000000000 --- a/src/axolotl/loraplus.py +++ /dev/null @@ -1,133 +0,0 @@ -"""Module for LoRA+""" - -# MIT License -# -# Copyright (c) 2024 nikhil-ghosh-berkeley -# https://github.com/nikhil-ghosh-berkeley/loraplus - -import logging -from functools import reduce - -from peft.tuners import lora -from torch import nn -from transformers.pytorch_utils import ALL_LAYERNORM_LAYERS -from transformers.trainer_pt_utils import get_parameter_names - -LOG = logging.getLogger("axolotl.loraplus") - - -def get_module(name, opt_model): - """ - Retrieve a module from a model using its parameter name. - Args: - name (str): Full name of the parameter, typically including module path. - opt_model (torch.nn.Module): The model from which to retrieve the module. - - Returns: - Module corresponding to the given name. - """ - parent_idx = 2 if "lora" in name else 1 - module_names = name.split(sep=".")[:-parent_idx] - module = reduce(getattr, module_names, opt_model) - return module - - -def create_loraplus_optimizer( - opt_model, - optimizer_cls, - optimizer_kwargs, - loraplus_lr_ratio, - loraplus_lr_embedding=None, -): - """ - Creates an optimizer for the given model, applying LoRA-specific learning rate adjustments to different parameter groups. - - Args: - opt_model (torch.nn.Module): The model for which the optimizer is being created. - optimizer_cls (class): The class of the optimizer to be used (e.g., torch.optim.Adam). - optimizer_kwargs (dict): A dictionary of keyword arguments for the optimizer's initialization. - loraplus_lr_ratio (float): The learning rate ratio to be applied to LoRA parameters. - loraplus_lr_embedding (float, optional): A specific learning rate for embedding parameters, with a default value if not provided. - - Returns: - An instance of the specified optimizer class configured with the model's parameters organized into groups with custom learning rates. - """ - - assert loraplus_lr_ratio is not None, "loraplus_lr_ratio must be provided." - - if loraplus_lr_embedding is None: - loraplus_lr_embedding = 1e-6 - - decay_parameters = get_parameter_names(opt_model, ALL_LAYERNORM_LAYERS) - decay_parameters = [name for name in decay_parameters if "bias" not in name] - param_groups = { - "groupA": {}, - "groupB": {}, - "groupB_no_decay": {}, - "embedding": {}, - } - - for name, param in opt_model.named_parameters(): - if not param.requires_grad: - continue - - module = get_module(name, opt_model) - if isinstance(module, lora.Embedding): - param_groups["embedding"][name] = param - elif "lora_B" in name or param.ndim == 1: - if name in decay_parameters: - param_groups["groupB"][name] = param - else: - param_groups["groupB_no_decay"][name] = param - else: - param_groups["groupA"][name] = param - - assigned_param_groups = "" - for group, group_params in param_groups.items(): - assigned_param_groups += f"{group}\n {list(group_params.keys())}\n\n" - LOG.info(assigned_param_groups) - - lr = optimizer_kwargs["lr"] # pylint: disable=invalid-name - weight_decay = optimizer_kwargs.get("weight_decay", 0.0) - - optimizer_grouped_parameters = [ - { - "params": list(param_groups["groupA"].values()), - "weight_decay": weight_decay, - "lr": lr, - }, - { - "params": list(param_groups["embedding"].values()), - "weight_decay": weight_decay, - "lr": loraplus_lr_embedding, - }, - { - "params": list(param_groups["groupB"].values()), - "weight_decay": weight_decay, - "lr": lr * loraplus_lr_ratio, - }, - { - "params": list(param_groups["groupB_no_decay"].values()), - "weight_decay": 0.0, - "lr": lr * loraplus_lr_ratio, - }, - ] - - optimizer = optimizer_cls(optimizer_grouped_parameters, **optimizer_kwargs) - if optimizer_cls.__name__ == "Adam8bit": - import bitsandbytes - - manager = bitsandbytes.optim.GlobalOptimManager.get_instance() - - skipped = 0 - for module in opt_model.modules(): - if isinstance(module, nn.Embedding): - skipped += sum( - {p.data_ptr(): p.numel() for p in module.parameters()}.values() - ) - LOG.info(f"skipped {module}: {skipped/2**20}M params") - manager.register_module_override(module, "weight", {"optim_bits": 32}) - LOG.debug(f"bitsandbytes: will optimize {module} in fp32") - LOG.info(f"skipped: {skipped/2**20}M params") - - return optimizer From 61aa291119e90dbebf5612be42cd5cad3729bc6e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 27 Sep 2024 15:58:35 -0400 Subject: [PATCH 0167/1405] fix for empty lora+ lr embedding (#1932) --- src/axolotl/core/trainer_builder.py | 2 +- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 23ac0952ed..249398f850 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -456,7 +456,7 @@ def create_optimizer(self): if self.args.loraplus_lr_ratio is not None: loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) loraplus_lr_embedding = getattr( - self.args, "loraplus_lr_embedding", None + self.args, "loraplus_lr_embedding", 1e-6 ) self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init opt_model, diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 2217855083..4e07c9260a 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -298,6 +298,13 @@ def validate_qlora(self): raise ValueError("Require cfg.load_in_4bit to be True for qlora") return self + @field_validator("loraplus_lr_embedding") + @classmethod + def convert_loraplus_lr_embedding(cls, loraplus_lr_embedding): + if loraplus_lr_embedding and isinstance(loraplus_lr_embedding, str): + loraplus_lr_embedding = float(loraplus_lr_embedding) + return loraplus_lr_embedding + class ReLoRAConfig(BaseModel): """ReLoRA configuration subset""" From 844331005c1ef45430ff26b9f42f757dce6ee66a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 30 Sep 2024 13:56:12 -0400 Subject: [PATCH 0168/1405] bump transformers to 4.45.1 (#1936) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3f17e5d329..123a4ee54a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.13.0 -transformers==4.45.0 +transformers==4.45.1 tokenizers>=0.19.1 bitsandbytes==0.44.0 accelerate==0.34.2 From e1915f5625b2330555c3f61816dd003fb939ae13 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 2 Oct 2024 21:02:48 -0400 Subject: [PATCH 0169/1405] Multimodal Vision Llama - rudimentary support (#1940) --------- Co-authored-by: Sunny Co-authored-by: sunny --- docs/input_output.qmd | 2 +- docs/multimodal.qmd | 28 +++ examples/llama-3-vision/lora-11b.yaml | 63 +++++ src/axolotl/cli/__init__.py | 7 +- src/axolotl/core/trainer_builder.py | 20 +- src/axolotl/monkeypatch/attention/mllama.py | 229 ++++++++++++++++++ src/axolotl/monkeypatch/multipack.py | 1 + .../monkeypatch/stablelm_attn_hijack_flash.py | 1 + src/axolotl/prompt_strategies/__init__.py | 4 +- .../prompt_strategies/chat_template.py | 60 ++++- src/axolotl/train.py | 10 +- src/axolotl/utils/chat_templates.py | 46 ++-- src/axolotl/utils/collators/__init__.py | 10 + .../{collators.py => collators/batching.py} | 35 +-- src/axolotl/utils/collators/core.py | 4 + src/axolotl/utils/collators/mamba.py | 38 +++ src/axolotl/utils/collators/mm_chat.py | 77 ++++++ src/axolotl/utils/config/__init__.py | 25 +- .../config/models/input/v0_4_1/__init__.py | 29 ++- src/axolotl/utils/data/sft.py | 48 +++- src/axolotl/utils/models.py | 98 ++++++-- src/axolotl/utils/trainer.py | 16 +- .../prompt_strategies/test_chat_templates.py | 21 +- .../test_chat_templates_advanced.py | 46 +++- 24 files changed, 799 insertions(+), 119 deletions(-) create mode 100644 docs/multimodal.qmd create mode 100644 examples/llama-3-vision/lora-11b.yaml create mode 100644 src/axolotl/monkeypatch/attention/mllama.py create mode 100644 src/axolotl/utils/collators/__init__.py rename src/axolotl/utils/{collators.py => collators/batching.py} (90%) create mode 100644 src/axolotl/utils/collators/core.py create mode 100644 src/axolotl/utils/collators/mamba.py create mode 100644 src/axolotl/utils/collators/mm_chat.py diff --git a/docs/input_output.qmd b/docs/input_output.qmd index 7715dd250d..6559578d18 100644 --- a/docs/input_output.qmd +++ b/docs/input_output.qmd @@ -205,7 +205,7 @@ ds = load_from_disk(f'last_run_prepared/{directory[0]}/') hi there!. goodbye farewell ``` -We can check that the right tokens are ingored by comparing the labels +We can check that the right tokens are ignored by comparing the labels to each token: ```python diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd new file mode 100644 index 0000000000..2381566adb --- /dev/null +++ b/docs/multimodal.qmd @@ -0,0 +1,28 @@ +# MultiModal / Vision Language Models (BETA) + +### Supported Models + +- Mllama, i.e. llama with vision models + +### Usage + +Currently multimodal support is limited and doesn't have full feature parity. To finetune a multimodal Llama w/ LoRA, +you'll need to use the following in YAML in combination with the rest of the required hyperparams. + +```yaml +base_model: alpindale/Llama-3.2-11B-Vision-Instruct +processor_type: AutoProcessor +skip_prepare_dataset: true + +chat_template: llama3_2_vision +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + field_messages: messages +remove_unused_columns: false +sample_packing: false + +# only finetune the Language model, leave the vision model and vision tower frozen +lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' +``` diff --git a/examples/llama-3-vision/lora-11b.yaml b/examples/llama-3-vision/lora-11b.yaml new file mode 100644 index 0000000000..b2e4946418 --- /dev/null +++ b/examples/llama-3-vision/lora-11b.yaml @@ -0,0 +1,63 @@ +base_model: alpindale/Llama-3.2-11B-Vision-Instruct +processor_type: AutoProcessor +strict: false + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +chat_template: llama3_2_vision +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + field_messages: messages +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: lora +lora_model_dir: + +sequence_len: 8192 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +local_rank: +logging_steps: 1 +flash_attention: true +eager_attention: + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 13c5b4ab58..a1d84b6a16 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -40,7 +40,7 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import is_main_process from axolotl.utils.mlflow_ import setup_mlflow_env_vars -from axolotl.utils.models import load_tokenizer +from axolotl.utils.models import load_processor, load_tokenizer from axolotl.utils.tokenization import check_dataset_labels from axolotl.utils.trainer import prepare_opinionated_env, prepare_optim_env from axolotl.utils.wandb_ import setup_wandb_env_vars @@ -430,9 +430,12 @@ def load_datasets( cli_args: TrainerCliArgs, ) -> TrainDatasetMeta: tokenizer = load_tokenizer(cfg) + processor = load_processor(cfg, tokenizer=tokenizer) if cfg.processor_type else None train_dataset, eval_dataset, total_num_steps, prompters = prepare_dataset( - cfg, tokenizer + cfg, + tokenizer, + processor=processor, ) if cli_args.debug or cfg.debug: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 249398f850..4893e63dc2 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -61,12 +61,14 @@ log_prediction_callback_factory, ) from axolotl.utils.callbacks.lisa import lisa_callback_factory +from axolotl.utils.chat_templates import chat_templates from axolotl.utils.collators import ( BatchSamplerDataCollatorForSeq2Seq, DataCollatorForSeq2Seq, MambaDataCollator, V2BatchSamplerDataCollatorForSeq2Seq, ) +from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator from axolotl.utils.models import ensure_dtype from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths from axolotl.utils.schedulers import ( @@ -250,6 +252,10 @@ class AxolotlTrainingMixins: "help": "workaround to pass an alternate lr scheduler to the HF trainer" }, ) + chat_template: Optional[str] = field( + default=None, + metadata={"help": "Chat template converting chat messages to text"}, + ) @dataclass @@ -1043,10 +1049,11 @@ class TrainerBuilderBase(abc.ABC): _model_ref = None _peft_config = None - def __init__(self, cfg, model, tokenizer): + def __init__(self, cfg, model, tokenizer, processor=None): self.cfg = cfg self.model = model self.tokenizer = tokenizer + self.processor = processor # in case the model supports tagging, add the axolotl tag. # This makes sure the tag is correctly pushed even if a user calls @@ -1515,6 +1522,10 @@ def build(self, total_num_steps): ) training_arguments_kwargs["model_type"] = self.cfg.model_config_type training_arguments_kwargs["pretraining"] = bool(self.cfg.pretraining_dataset) + if self.cfg.chat_template: + training_arguments_kwargs["chat_template"] = chat_templates( + self.cfg.chat_template + ) if self.cfg.rl == "orpo": training_arguments_kwargs["orpo_alpha"] = self.cfg.orpo_alpha @@ -1661,7 +1672,12 @@ def build_collator( else: collator = BatchSamplerDataCollatorForSeq2Seq else: - collator = DataCollatorForSeq2Seq + if self.cfg.processor_type and self.processor: + collator = MultiModalChatDataCollator + kwargs["processor"] = self.processor + kwargs["chat_template"] = training_args.chat_template + else: + collator = DataCollatorForSeq2Seq return collator( self.tokenizer, diff --git a/src/axolotl/monkeypatch/attention/mllama.py b/src/axolotl/monkeypatch/attention/mllama.py new file mode 100644 index 0000000000..0b18b716d5 --- /dev/null +++ b/src/axolotl/monkeypatch/attention/mllama.py @@ -0,0 +1,229 @@ +""" +Monkeypatch for Vision Llama for FA2 support +""" +# pylint: disable=duplicate-code + +from typing import Optional, Tuple + +import torch +from flash_attn.flash_attn_interface import flash_attn_func +from transformers.cache_utils import Cache +from transformers.modeling_flash_attention_utils import _flash_attention_forward +from transformers.models.mllama.configuration_mllama import MllamaTextConfig +from transformers.models.mllama.modeling_mllama import ( + MllamaTextCrossAttention, + MllamaTextSelfAttention, + apply_rotary_pos_emb, + repeat_kv, +) +from transformers.utils import is_flash_attn_greater_or_equal_2_10 + + +class MllamaTextCrossFlashAttention2(MllamaTextCrossAttention): + """ + Mllama flash cross-attention module. This module inherits from `MllamaTextCrossAttention` and + implements the forward pass using Flash Attention for improved performance. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Check if flash attention version is greater or equal to 2.1 + self._flash_attn_uses_top_left_mask = not is_flash_attn_greater_or_equal_2_10() + + def forward( + self, + hidden_states: torch.Tensor, + cross_attention_states: Optional[torch.Tensor] = None, + past_key_value: Optional[Cache] = None, + attention_mask: Optional[ # pylint: disable=unused-argument + torch.Tensor + ] = None, + output_attentions: bool = False, + use_cache: bool = False, # pylint: disable=unused-argument + cache_position: Optional[torch.LongTensor] = None, + ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: + bsz, q_len, _ = hidden_states.size() + + query_states = self.q_proj(hidden_states) + query_states = query_states.view( + bsz, q_len, self.num_heads, self.head_dim + ).transpose(1, 2) + query_states = self.q_norm(query_states) + + if cross_attention_states is not None: + key_states = self.k_proj(cross_attention_states) + value_states = self.v_proj(cross_attention_states) + key_states = key_states.view( + bsz, -1, self.num_key_value_heads, self.head_dim + ).transpose(1, 2) + value_states = value_states.view( + bsz, -1, self.num_key_value_heads, self.head_dim + ).transpose(1, 2) + key_states = repeat_kv(key_states, self.num_key_value_groups) + value_states = repeat_kv(value_states, self.num_key_value_groups) + + key_states = self.k_norm(key_states) + if past_key_value is not None: + key_states, value_states = past_key_value.update( + key_states, + value_states, + self.layer_idx, + {"cache_position": cache_position}, + ) + elif cache_position[0] != 0: + key_states, value_states = ( + past_key_value.key_cache[self.layer_idx], + past_key_value.value_cache[self.layer_idx], + ) + else: + raise ValueError( + "Cross attention layer can't find neither `cross_attn_states` nor cached values for key/values!" + ) + + # Transpose to get the expected layout for flash attention + query_states = query_states.transpose(1, 2) + key_states = key_states.transpose(1, 2) + value_states = value_states.transpose(1, 2) + + # Apply Flash Attention + dropout_rate = self.dropout if self.training else 0.0 + output = flash_attn_func( + query_states, + key_states, + value_states, + dropout_p=dropout_rate, + softmax_scale=None, + causal=False, + return_attn_probs=output_attentions, + ) + + attn_output = output.contiguous().view(bsz, q_len, -1) + attn_output = self.o_proj(attn_output) + + if not output_attentions: + attn_weights = None + + return attn_output, attn_weights, past_key_value + + +class MllamaTextSelfFlashAttention2(MllamaTextSelfAttention): + """ + Mllama flash self-attention module. This module inherits from `MllamaTextSelfAttention` and + implements the forward pass using Flash Attention for improved performance. + """ + + def __init__(self, config: MllamaTextConfig, layer_idx: int, *args, **kwargs): + super().__init__(config, layer_idx, *args, **kwargs) + + # Check if flash attention version is greater or equal to 2.1 + self._flash_attn_uses_top_left_mask = not is_flash_attn_greater_or_equal_2_10() + + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, + output_attentions: bool = False, + use_cache: bool = False, # pylint: disable=unused-argument + past_key_value=None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs, # pylint: disable=unused-argument + ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: + output_attentions = False + + bsz, q_len, _ = hidden_states.size() + + query_states = self.q_proj(hidden_states) + key_states = self.k_proj(hidden_states) + value_states = self.v_proj(hidden_states) + + # Flash attention requires the input to have the shape + # batch_size x seq_length x num_heads x head_dim + query_states = query_states.view( + bsz, q_len, self.num_heads, self.head_dim + ).transpose(1, 2) + key_states = key_states.view( + bsz, q_len, self.num_key_value_heads, self.head_dim + ).transpose(1, 2) + value_states = value_states.view( + bsz, q_len, self.num_key_value_heads, self.head_dim + ).transpose(1, 2) + + cos, sin = position_embeddings + query_states, key_states = apply_rotary_pos_emb( + query_states, key_states, cos, sin + ) + + if past_key_value is not None: + # sin and cos are specific to RoPE models; cache_position needed for the static cache + cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position} + key_states, value_states = past_key_value.update( + key_states, value_states, self.layer_idx, cache_kwargs + ) + + key_states = repeat_kv(key_states, self.num_key_value_groups) + value_states = repeat_kv(value_states, self.num_key_value_groups) + + # Transpose to get the expected layout for flash attention + query_states = query_states.transpose(1, 2) + key_states = key_states.transpose(1, 2) + value_states = value_states.transpose(1, 2) + + dropout_rate = self.dropout if self.training else 0.0 + + # Handle potential silent casting to float32 + input_dtype = query_states.dtype + if input_dtype == torch.float32: + if torch.is_autocast_enabled(): + target_dtype = torch.get_autocast_gpu_dtype() + elif hasattr(self.config, "_pre_quantization_dtype"): + target_dtype = ( + self.config._pre_quantization_dtype # pylint: disable=protected-access + ) + else: + target_dtype = self.q_proj.weight.dtype + + query_states = query_states.to(target_dtype) + key_states = key_states.to(target_dtype) + value_states = value_states.to(target_dtype) + + attn_output = _flash_attention_forward( + query_states, + key_states, + value_states, + attention_mask, + q_len, + dropout=dropout_rate, + use_top_left_mask=self._flash_attn_uses_top_left_mask, + is_causal=True, + ) + + attn_output = attn_output.reshape(bsz, q_len, -1).contiguous() + attn_output = self.o_proj(attn_output) + + if not output_attentions: + attn_weights = None + + return attn_output, attn_weights, past_key_value + + +def patch_mllama(): + from transformers.models.mllama.modeling_mllama import ( + MLLAMA_TEXT_ATTENTION_CLASSES, + MLLAMA_TEXT_CROSS_ATTENTION_CLASSES, + MLLAMA_VISION_ATTENTION_CLASSES, + MllamaPreTrainedModel, + ) + + MllamaPreTrainedModel._supports_flash_attn_2 = ( # pylint: disable=protected-access + True + ) + MLLAMA_TEXT_ATTENTION_CLASSES["flash_attention_2"] = MllamaTextSelfFlashAttention2 + MLLAMA_TEXT_CROSS_ATTENTION_CLASSES[ + "flash_attention_2" + ] = MllamaTextCrossFlashAttention2 + # fallback to SDPA + MLLAMA_VISION_ATTENTION_CLASSES[ + "flash_attention_2" + ] = MLLAMA_VISION_ATTENTION_CLASSES["sdpa"] diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 44fc4cb473..85101cd3c4 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -10,6 +10,7 @@ from axolotl.monkeypatch.utils import get_unpad_data SUPPORTED_MULTIPACK_MODEL_TYPES = [ + "mllama_text_model", "llama", "mistral", "mixtral", diff --git a/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py b/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py index 0269f90157..67e9337e36 100644 --- a/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py @@ -16,6 +16,7 @@ # This code is based off the following work: # https://github.com/huggingface/transformers/blob/main/src/transformers/models/llama/modeling_llama.py # https://github.com/huggingface/transformers/blob/main/src/transformers/models/gpt_neox/modeling_gpt_neox.py +# pylint: disable=duplicate-code """ PyTorch StableLM Epoch model. """ import importlib import math diff --git a/src/axolotl/prompt_strategies/__init__.py b/src/axolotl/prompt_strategies/__init__.py index f5699a0871..66cd5deeb9 100644 --- a/src/axolotl/prompt_strategies/__init__.py +++ b/src/axolotl/prompt_strategies/__init__.py @@ -9,7 +9,7 @@ LOG = logging.getLogger("axolotl.prompt_strategies") -def load(strategy, tokenizer, cfg, ds_cfg): +def load(strategy, tokenizer, cfg, ds_cfg, processor=None): try: load_fn = "load" if strategy.split(".")[-1].startswith("load_"): @@ -24,6 +24,8 @@ def load(strategy, tokenizer, cfg, ds_cfg): sig = inspect.signature(func) if "ds_cfg" in sig.parameters: load_kwargs["ds_cfg"] = ds_cfg + if "processor" in sig.parameters: + load_kwargs["processor"] = processor return func(tokenizer, cfg, **load_kwargs) except ModuleNotFoundError: return None diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 88e748895d..48d52dae11 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -5,6 +5,8 @@ import logging from typing import Any, Dict, List, Optional +from transformers import ProcessorMixin + from axolotl.prompt_tokenizers import PromptTokenizingStrategy from axolotl.prompters import IGNORE_TOKEN_ID, Prompter from axolotl.utils.chat_templates import chat_templates @@ -20,6 +22,7 @@ class ChatTemplatePrompter(Prompter): def __init__( self, tokenizer, + processor=None, chat_template=None, max_length=2048, message_field_role: str = "from", @@ -44,11 +47,12 @@ def __init__( self.message_field_training = message_field_training self.message_field_training_detail = message_field_training_detail self.tokenizer = tokenizer + self.processor: ProcessorMixin = processor self.chat_template = chat_template self.max_length = max_length self.drop_system_message = drop_system_message - def build_prompt(self, conversation, add_generation_prompt=False): + def build_prompt(self, conversation, add_generation_prompt=False, images=None): turns = [ { "role": self.roles[t[self.message_field_role]], @@ -61,6 +65,28 @@ def build_prompt(self, conversation, add_generation_prompt=False): if self.drop_system_message and turns[0]["role"] == "system": turns = turns[1:] + if self.processor: + text = self.processor.apply_chat_template( + turns, + chat_template=self.chat_template, + tokenize=False, + add_generation_prompt=add_generation_prompt, + ) + batch = self.processor( + text=text, + images=images, + return_tensors="pt", + truncation=True, + max_length=self.max_length, + ) + # workaround since processor works in batches instead of single examples + for k, val in batch.items(): + if k in ["pixel_values"]: + batch[k] = val.tolist() + else: + batch[k] = val.squeeze().tolist() + return batch + return self.tokenizer.apply_chat_template( turns, truncation=True, @@ -191,6 +217,7 @@ def __init__( super().__init__(prompter, tokenizer, train_on_inputs, sequence_len) self.roles_to_train = roles_to_train if roles_to_train is not None else [] self.train_on_eos = train_on_eos + self.images = "images" @property def messages(self): @@ -209,10 +236,21 @@ def tokenize_prompt(self, prompt): and not self.prompter.message_field_training_detail ): turns = self.get_conversation_thread(prompt) + images = self.get_images(prompt) prompt_ids = self.prompter.build_prompt( - turns[:-1], add_generation_prompt=True + turns[:-1], + add_generation_prompt=True, + images=images, ) - input_ids = self.prompter.build_prompt(turns) + tokenized_res = self.prompter.build_prompt(turns, images=images) + tokenized_prompt = {} + if isinstance(tokenized_res, list): + input_ids = prompt_ids + tokenized_res[len(prompt_ids) :] + tokenized_prompt["input_ids"] = input_ids + tokenized_prompt["attention_mask"] = [1] * len(input_ids) + else: + input_ids = tokenized_res["input_ids"] + tokenized_prompt = tokenized_res if not self.train_on_inputs: user_prompt_len = len(prompt_ids) @@ -220,17 +258,9 @@ def tokenize_prompt(self, prompt): else: labels = input_ids - tokenized_prompt = { - "input_ids": input_ids, - "labels": labels, - "attention_mask": [1] * len(input_ids), - } + tokenized_prompt["labels"] = labels return tokenized_prompt - LOG.info(self.roles_to_train) - LOG.info(self.train_on_eos) - LOG.info(self.prompter.message_field_training) - LOG.info(self.prompter.message_field_training_detail) turns = prompt[self.messages] input_ids = self.prompter.build_prompt(turns) @@ -368,8 +398,11 @@ def find_turn(self, conversation_ids, turn, turn_content): def get_conversation_thread(self, prompt): return prompt[self.messages] + def get_images(self, prompt): + return prompt.get(self.images, None) + -def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): +def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None, processor=None): ds_cfg = ds_cfg or {} prompter_params = { @@ -386,6 +419,7 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): "drop_system_message": ds_cfg.get("drop_system_message", False), # we need to add one for detecting sequences with exceeding the `sequence_len` limit. "max_length": cfg.sequence_len + 1, + "processor": processor, } strategy_params = { diff --git a/src/axolotl/train.py b/src/axolotl/train.py index b21b0b269c..855dbc2d3b 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -24,7 +24,7 @@ from axolotl.logging_config import configure_logging from axolotl.utils.dict import DictDefault from axolotl.utils.freeze import freeze_layers_except -from axolotl.utils.models import load_model, load_tokenizer +from axolotl.utils.models import load_model, load_processor, load_tokenizer from axolotl.utils.trainer import setup_trainer try: @@ -69,6 +69,9 @@ def train( main_process_only=True, ) tokenizer = load_tokenizer(cfg) + processor = None + if cfg.is_multimodal: + processor = load_processor(cfg, tokenizer) train_dataset = dataset_meta.train_dataset eval_dataset = dataset_meta.eval_dataset @@ -96,7 +99,9 @@ def train( LOG.debug(msg) # we wait unitl the last possible moment to setup Accelerator Accelerator() - model, peft_config = load_model(cfg, tokenizer, inference=cli_args.inference) + model, peft_config = load_model( + cfg, tokenizer, processor=processor, inference=cli_args.inference + ) model.generation_config.do_sample = True model_ref = None @@ -122,6 +127,7 @@ def train( eval_dataset, (model, model_ref, peft_config), tokenizer, + processor, total_num_steps, ) diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 7a96f5c1e1..7468ae8b15 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -3,6 +3,20 @@ These templates are used for formatting messages in a conversation. """ +CHAT_TEMPLATES = { + "alpaca": "{% for message in messages %}{% if message['role'] == 'user' %}{{ '### Instruction: ' + message['content'] + '\n\n' }}{% elif message['role'] == 'assistant' %}{{ '### Response: ' + message['content'] + eos_token}}{% endif %}{% endfor %}", + "inst": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # I don't know what this one is called. Used by Mistral/Mixtral. + "chatml": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}", + "gemma": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\n' + message['content'] | trim + '\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\n'}}{% endif %}", + "cohere": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", + "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", + "llama3_2_vision": '{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now("%d %b %Y") %}\n {%- else %}\n {%- set date_string = "26 Jul 2024" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0][\'role\'] == \'system\' %}\n {%- set system_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n{%- else %}\n {%- set system_message = "" %}\n{%- endif %}\n\n{#- Find out if there are any images #}\n{% set image_ns = namespace(has_images=false) %} \n{%- for message in messages %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {%- set image_ns.has_images = true %}\n {%- endif %}\n {%- endfor %}\n{%- endfor %}\n\n{#- Error out if there are images and system message #}\n{%- if image_ns.has_images and not system_message == "" %}\n {{- raise_exception("Prompting with images is incompatible with system messages.") }}\n{%- endif %}\n\n{#- System message if there are no images #}\n{%- if not image_ns.has_images %}\n {{- "<|start_header_id|>system<|end_header_id|>\\n\\n" }}\n {%- if tools is not none %}\n {{- "Environment: ipython\\n" }}\n {%- endif %}\n {{- "Cutting Knowledge Date: December 2023\\n" }}\n {{- "Today Date: " + date_string + "\\n\\n" }}\n {%- if tools is not none and not tools_in_user_message %}\n {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- "<|eot_id|>" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception("Cannot put tools in the first user message when there\'s no first user message!") }}\n{%- endif %}\n {{- \'<|start_header_id|>user<|end_header_id|>\\n\\n\' -}}\n {{- "Given the following functions, please respond with a JSON for a function call " }}\n {{- "with its proper arguments that best answers the given prompt.\\n\\n" }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {{- first_user_message + "<|eot_id|>"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == \'ipython\' or message.role == \'tool\' or \'tool_calls\' in message) %}\n {{- \'<|start_header_id|>\' + message[\'role\'] + \'<|end_header_id|>\\n\\n\' }}\n {%- if message[\'content\'] is string %}\n {{- message[\'content\'] }}\n {%- else %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {{- \'<|image|>\' }}\n {%- elif content[\'type\'] == \'text\' %}\n {{- content[\'text\'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \'<|eot_id|>\' }}\n {%- elif \'tool_calls\' in message %}\n {%- if not message.tool_calls|length == 1 %}\n {{- raise_exception("This model only supports single tool-calls at once!") }}\n {%- endif %}\n {%- set tool_call = message.tool_calls[0].function %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' -}}\n {{- \'{"name": "\' + tool_call.name + \'", \' }}\n {{- \'"parameters": \' }}\n {{- tool_call.arguments | tojson }}\n {{- "}" }}\n {{- "<|eot_id|>" }}\n {%- elif message.role == "tool" or message.role == "ipython" %}\n {{- "<|start_header_id|>ipython<|end_header_id|>\\n\\n" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- "<|eot_id|>" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' }}\n{%- endif %}\n', + "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", + "phi_35": "{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'user' %}{{'<|user|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|>\n' + message['content'] + '<|end|>\n'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% else %}{{ eos_token }}{% endif %}", + "deepseek_v2": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %}", + "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', +} + def chat_templates(user_choice: str): """ @@ -18,20 +32,22 @@ def chat_templates(user_choice: str): ValueError: If the user_choice is not found in the templates. """ - templates = { - "alpaca": "{% for message in messages %}{% if message['role'] == 'user' %}{{ '### Instruction: ' + message['content'] + '\n\n' }}{% elif message['role'] == 'assistant' %}{{ '### Response: ' + message['content'] + eos_token}}{% endif %}{% endfor %}", - "inst": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # I don't know what this one is called. Used by Mistral/Mixtral. - "chatml": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}", - "gemma": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\n' + message['content'] | trim + '\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\n'}}{% endif %}", - "cohere": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", - "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", - "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", - "phi_35": "{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'user' %}{{'<|user|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|>\n' + message['content'] + '<|end|>\n'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% else %}{{ eos_token }}{% endif %}", - "deepseek_v2": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %}", - "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', - } - - if user_choice in templates: - return templates[user_choice] + if user_choice in CHAT_TEMPLATES: + return CHAT_TEMPLATES[user_choice] raise ValueError(f"Template '{user_choice}' not found.") + + +def register_chat_template(template_name: str, chat_template: str): + """ + Registers chat templates. + + Args: + template_name (str): The name of the template. + chat_template (str): The template string. + """ + + if template_name in CHAT_TEMPLATES: + raise ValueError(f"Template '{template_name}' already exists.") + + CHAT_TEMPLATES[template_name] = chat_template diff --git a/src/axolotl/utils/collators/__init__.py b/src/axolotl/utils/collators/__init__.py new file mode 100644 index 0000000000..93502b67d7 --- /dev/null +++ b/src/axolotl/utils/collators/__init__.py @@ -0,0 +1,10 @@ +""" +shared axolotl collators for multipack, mamba, multimodal +""" +from .batching import ( # noqa: F401 + BatchSamplerDataCollatorForSeq2Seq, + DataCollatorForSeq2Seq, + PretrainingBatchSamplerDataCollatorForSeq2Seq, + V2BatchSamplerDataCollatorForSeq2Seq, +) +from .mamba import MambaDataCollator # noqa: F401 diff --git a/src/axolotl/utils/collators.py b/src/axolotl/utils/collators/batching.py similarity index 90% rename from src/axolotl/utils/collators.py rename to src/axolotl/utils/collators/batching.py index 26c7fa9f3c..7cf771421c 100644 --- a/src/axolotl/utils/collators.py +++ b/src/axolotl/utils/collators/batching.py @@ -1,17 +1,14 @@ """ DataCollator for axolotl to pad labels and position_ids for packed sequences """ + from dataclasses import dataclass -from typing import Any, Dict, Optional, Sequence, Union +from typing import Any, Optional, Union import numpy as np -import torch -import transformers from transformers import PreTrainedTokenizerBase from transformers.utils import PaddingStrategy -IGNORE_INDEX = -100 - @dataclass class DataCollatorForSeq2Seq: @@ -183,34 +180,6 @@ def __call__(self, features, return_tensors=None): return super().__call__(out_features, return_tensors=return_tensors) -@dataclass -class MambaDataCollator: - """ - Collator for State Space Models (Mamba) - """ - - tokenizer: transformers.PreTrainedTokenizer - - def __call__(self, instances: Sequence[Dict]) -> Dict[str, torch.Tensor]: - input_ids, labels = tuple( - [torch.LongTensor(instance[key]) for instance in instances] - for key in ("input_ids", "labels") - ) - input_ids = torch.nn.utils.rnn.pad_sequence( - input_ids, - batch_first=True, - padding_value=self.tokenizer.pad_token_id, - ) - labels = torch.nn.utils.rnn.pad_sequence( - labels, batch_first=True, padding_value=IGNORE_INDEX - ) - - return { - "input_ids": input_ids, - "labels": labels, - } - - @dataclass class PretrainingBatchSamplerDataCollatorForSeq2Seq(DataCollatorForSeq2Seq): """ diff --git a/src/axolotl/utils/collators/core.py b/src/axolotl/utils/collators/core.py new file mode 100644 index 0000000000..0eae0c3bda --- /dev/null +++ b/src/axolotl/utils/collators/core.py @@ -0,0 +1,4 @@ +""" +basic shared collator constants +""" +IGNORE_INDEX = -100 diff --git a/src/axolotl/utils/collators/mamba.py b/src/axolotl/utils/collators/mamba.py new file mode 100644 index 0000000000..0c4a22fcc0 --- /dev/null +++ b/src/axolotl/utils/collators/mamba.py @@ -0,0 +1,38 @@ +""" +collators for Mamba +""" +from dataclasses import dataclass +from typing import Dict, Sequence + +import torch +import transformers + +from axolotl.utils.collators.core import IGNORE_INDEX + + +@dataclass +class MambaDataCollator: + """ + Collator for State Space Models (Mamba) + """ + + tokenizer: transformers.PreTrainedTokenizer + + def __call__(self, instances: Sequence[Dict]) -> Dict[str, torch.Tensor]: + input_ids, labels = tuple( + [torch.LongTensor(instance[key]) for instance in instances] + for key in ("input_ids", "labels") + ) + input_ids = torch.nn.utils.rnn.pad_sequence( + input_ids, + batch_first=True, + padding_value=self.tokenizer.pad_token_id, + ) + labels = torch.nn.utils.rnn.pad_sequence( + labels, batch_first=True, padding_value=IGNORE_INDEX + ) + + return { + "input_ids": input_ids, + "labels": labels, + } diff --git a/src/axolotl/utils/collators/mm_chat.py b/src/axolotl/utils/collators/mm_chat.py new file mode 100644 index 0000000000..f49e97f37f --- /dev/null +++ b/src/axolotl/utils/collators/mm_chat.py @@ -0,0 +1,77 @@ +""" +Collators for multi-modal chat messages and packing +""" +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Union + +from transformers import PreTrainedTokenizerBase, ProcessorMixin +from transformers.data.data_collator import DataCollatorMixin +from transformers.utils import PaddingStrategy + + +@dataclass +class MultiModalChatDataCollator(DataCollatorMixin): + """ + Collator for multi-modal chat messages + """ + + tokenizer: PreTrainedTokenizerBase + processor: ProcessorMixin + return_tensors: str = "pt" + chat_template: Optional[str] = None + packing: bool = False + max_images: int = -1 + padding: Union[bool, str, PaddingStrategy] = True + pad_to_multiple_of: Optional[int] = None + + def __post_init__(self): + if self.packing: + raise ValueError("Packing is currently not supported.") + + def torch_call( + self, examples: List[Union[List[int], Any, Dict[str, Any]]] + ) -> Dict[str, Any]: + # Handle dict or lists with proper padding and conversion to tensor. + + return self.__class__.process_rows( + examples, self.processor, self.chat_template, self.max_images + ) + + @staticmethod + def process_rows(examples, processor, chat_template, max_images, length_only=False): + # HINT: use `_torch_collate_batch` to stack and pad tensors + # see also DataCollatorWithFlattening and DefaultDataCollator + + # *** This is COPIED from the trl example sft_vlm.py code *** + # use this as a starting point + + # Get the texts and images, and apply the chat template + texts = [ + processor.apply_chat_template( + example["messages"], chat_template=chat_template, tokenize=False + ) + for example in examples + ] + images = [example["images"] for example in examples] + + if max_images > 0: + images = [img_batch[:max_images] for img_batch in images] + + # Tokenize the texts and process the images + batch = processor(text=texts, images=images, return_tensors="pt", padding=True) + + # The labels are the input_ids, and we mask the padding tokens in the loss computation + labels = batch["input_ids"].clone() + labels[labels == processor.tokenizer.pad_token_id] = -100 # + # Ignore the image token index in the loss computation (model specific) + image_token_id = processor.tokenizer.convert_tokens_to_ids( + processor.image_token + ) + labels[labels == image_token_id] = -100 + batch["labels"] = labels + + if length_only: + return { + "length": [len(sample["input_ids"]) for sample in batch["input_ids"]] + } + return batch diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 82436e8d79..f732db06fc 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -121,15 +121,36 @@ def normalize_config(cfg): cfg.base_model_config = cfg.base_model model_config = load_model_config(cfg) - cfg.model_config_type = model_config.model_type cfg.tokenizer_config = ( cfg.tokenizer_config or cfg.base_model_config or cfg.base_model ) + cfg.is_multimodal = ( + hasattr(model_config, "model_type") + and model_config.model_type in ["llava", "mllama"] + or any( + multimodal_name in cfg.base_model.lower() + for multimodal_name in [ + "pixtral", + ] + ) + or cfg.is_multimodal + ) + if cfg.is_multimodal: + cfg.processor_config = ( + cfg.processor_config or cfg.base_model_config or cfg.base_model + ) + model_config = model_config.text_config + + cfg.model_config_type = model_config.model_type + # figure out if the model is llama cfg.is_llama_derived_model = ( - (hasattr(model_config, "model_type") and model_config.model_type == "llama") + ( + hasattr(model_config, "model_type") + and model_config.model_type == ["llama", "mllama_text_model"] + ) or cfg.is_llama_derived_model or "llama" in cfg.base_model.lower() or (cfg.type_of_model and "llama" in cfg.type_of_model.lower()) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 4e07c9260a..fced5e639d 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -188,6 +188,7 @@ class ChatTemplate(str, Enum): gemma = "gemma" # pylint: disable=invalid-name cohere = "cohere" # pylint: disable=invalid-name llama3 = "llama3" # pylint: disable=invalid-name + llama3_2_vision = "llama3_2_vision" # pylint: disable=invalid-name phi_3 = "phi_3" # pylint: disable=invalid-name phi_35 = "phi_35" # pylint: disable=invalid-name deepseek_v2 = "deepseek_v2" # pylint: disable=invalid-name @@ -228,11 +229,12 @@ class LoraConfig(BaseModel): lora_r: Optional[int] = None lora_alpha: Optional[int] = None lora_fan_in_fan_out: Optional[bool] = None - lora_target_modules: Optional[List[str]] = None + lora_target_modules: Optional[Union[str, List[str]]] = None lora_target_linear: Optional[bool] = None lora_modules_to_save: Optional[List[str]] = None lora_dropout: Optional[float] = 0.0 peft_layers_to_transform: Optional[List[int]] = None + peft_layers_pattern: Optional[List[str]] = None peft: Optional[PeftConfig] = None peft_use_dora: Optional[bool] = None peft_use_rslora: Optional[bool] = None @@ -328,6 +330,9 @@ class ModelInputConfig(BaseModel): tokenizer_type: Optional[str] = Field( default=None, metadata={"help": "transformers tokenizer class"} ) + processor_type: Optional[str] = Field( + default=None, metadata={"help": "transformers processor class"} + ) trust_remote_code: Optional[bool] = None model_kwargs: Optional[Dict[str, Any]] = None @@ -530,6 +535,7 @@ class Config: dataset_prepared_path: Optional[str] = None dataset_shard_num: Optional[int] = None dataset_shard_idx: Optional[int] = None + skip_prepare_dataset: Optional[bool] = False pretraining_dataset: Optional[ # type: ignore conlist(Union[PretrainingDataset, SFTDataset], min_length=1) @@ -997,6 +1003,18 @@ def check_eval_packing(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_mm_prepare(cls, data): + if data.get("skip_prepare_dataset"): + if data.get("remove_unused_columns") is None: + LOG.info( + "setting `remove_unused_columns: false` for skip_prepare_dataset" + ) + data["remove_unused_columns"] = False + + return data + @model_validator(mode="before") @classmethod def check_warmup(cls, data): @@ -1052,6 +1070,15 @@ def check_frozen(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_peft_layers_pattern(cls, data): + if data.get("peft_layers_pattern") and not data.get("peft_layers_to_transform"): + raise ValueError( + "peft_layers_pattern requires peft_layers_to_transform to be set" + ) + return data + @model_validator(mode="after") def check_fft_possible_bad_config(self): if ( diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 1b6df1cded..7d6922cbf2 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -51,20 +51,31 @@ LOG = logging.getLogger("axolotl") -def prepare_dataset(cfg, tokenizer): +def prepare_dataset(cfg, tokenizer, processor=None): prompters = [] if not cfg.pretraining_dataset: with zero_first(is_local_main_process()): if cfg.test_datasets: train_dataset, _, prompters = load_prepare_datasets( - tokenizer, cfg, DEFAULT_DATASET_PREPARED_PATH, split="train" + tokenizer, + cfg, + DEFAULT_DATASET_PREPARED_PATH, + split="train", + processor=processor, ) _, eval_dataset, _ = load_prepare_datasets( - tokenizer, cfg, DEFAULT_DATASET_PREPARED_PATH, split="test" + tokenizer, + cfg, + DEFAULT_DATASET_PREPARED_PATH, + split="test", + processor=processor, ) else: train_dataset, eval_dataset, prompters = load_prepare_datasets( - tokenizer, cfg, DEFAULT_DATASET_PREPARED_PATH + tokenizer, + cfg, + DEFAULT_DATASET_PREPARED_PATH, + processor=processor, ) else: path = cfg.pretraining_dataset @@ -123,6 +134,7 @@ def load_tokenized_prepared_datasets( cfg, default_dataset_prepared_path, split="train", + processor=None, ) -> Tuple[DatasetDict, List[Prompter]]: cfg_datasets = cfg.test_datasets if split == "test" else cfg.datasets tokenizer_name = cfg.tokenizer_config @@ -180,6 +192,7 @@ def load_tokenized_prepared_datasets( cfg.dataset_prepared_path and any(prepared_ds_path.glob("*")) and not cfg.is_preprocess + and not cfg.skip_prepare_dataset ): LOG.info(f"Loading prepared dataset from disk at {prepared_ds_path}...") dataset = load_from_disk(str(prepared_ds_path)) @@ -423,12 +436,16 @@ def for_d_in_datasets(dataset_configs): dataset=ds, d_base_type=d_base_type, d_prompt_style=d_prompt_style, + processor=processor, ) datasets.append(dataset_wrapper) prompters.append(dataset_prompter) - LOG.info("merging datasets") - dataset = concatenate_datasets(datasets) + if len(datasets) == 1: + dataset = datasets[0] + else: + LOG.info("merging datasets") + dataset = concatenate_datasets(datasets) if len(datasets) > 1: if cfg.shuffle_merged_datasets: @@ -437,9 +454,10 @@ def for_d_in_datasets(dataset_configs): else: LOG.debug("NOT shuffling merged datasets") - dataset, _ = process_datasets_for_packing(cfg, dataset, None) + if not cfg.skip_prepare_dataset: + dataset, _ = process_datasets_for_packing(cfg, dataset, None) - if cfg.local_rank == 0: + if cfg.local_rank == 0 and not cfg.skip_prepare_dataset: LOG.info(f"Saving merged prepared dataset to disk... {prepared_ds_path}") dataset.save_to_disk(str(prepared_ds_path)) if cfg.push_dataset_to_hub: @@ -478,9 +496,14 @@ def load_prepare_datasets( cfg, default_dataset_prepared_path, split="train", + processor=None, ) -> Tuple[Dataset, Dataset, List[Prompter]]: dataset, prompters = load_tokenized_prepared_datasets( - tokenizer, cfg, default_dataset_prepared_path, split=split + tokenizer, + cfg, + default_dataset_prepared_path, + split=split, + processor=processor, ) if cfg.dataset_shard_num and cfg.dataset_shard_idx is not None: @@ -546,6 +569,7 @@ def get_dataset_wrapper( d_base_type, dataset, d_prompt_style=None, + processor=None, ): dataset_wrapper = None dataset_prompter = None @@ -578,7 +602,11 @@ def get_dataset_wrapper( dataset, **ds_kwargs, ) - elif ds_strategy := load(config_dataset.type, tokenizer, cfg, config_dataset): + elif cfg.skip_prepare_dataset: + dataset_wrapper = dataset + elif ds_strategy := load( + config_dataset.type, tokenizer, cfg, config_dataset, processor=processor + ): dataset_prompter = UnsupportedPrompter() dataset_wrapper = TokenizedPromptDataset( ds_strategy, diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index e183301991..c18af9760f 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -28,12 +28,17 @@ AddedToken, AutoConfig, AutoModelForCausalLM, + AutoModelForVision2Seq, + AutoProcessor, AutoTokenizer, AwqConfig, BitsAndBytesConfig, GPTQConfig, + LlavaForConditionalGeneration, + MllamaForConditionalGeneration, PreTrainedModel, PreTrainedTokenizerBase, + ProcessorMixin, ) from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled @@ -80,6 +85,9 @@ def get_module_class_from_name(module, name): def check_model_config(cfg: DictDefault, model_config: Union[AutoConfig, DictDefault]): + if cfg.is_multimodal: + model_config = model_config.text_config + quant_config_exists = ( hasattr(model_config, "quantization_config") and model_config.quantization_config @@ -299,11 +307,31 @@ def load_tokenizer(cfg): return tokenizer +def load_processor(cfg: DictDefault, tokenizer: PreTrainedTokenizerBase): + processor_kwargs: Dict[str, Any] = {} # do we actually need this? + + processor_cls = AutoProcessor + if cfg.processor_type: + processor_cls = getattr(transformers, cfg.processor_type) + + processor = processor_cls.from_pretrained( + cfg.processor_config, + trust_remote_code=cfg.trust_remote_code or False, + tokenizer=tokenizer, + **processor_kwargs, + ) + + return processor + + def load_model( cfg: DictDefault, tokenizer: PreTrainedTokenizerBase, + *, + processor: ProcessorMixin = None, # pylint: disable=unused-argument inference: bool = False, reference_model: bool = False, + **kwargs, # pylint: disable=unused-argument ) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: """ Load a model for a given configuration and tokenizer. @@ -319,12 +347,23 @@ def load_model( plugin_manager = PluginManager.get_instance() plugin_manager.pre_model_load(cfg) + if cfg.is_multimodal: + text_model_config = model_config.text_config + else: + text_model_config = model_config + # TODO refactor as a kwarg load_in_8bit = cfg.load_in_8bit if cfg.gradient_checkpointing == "unsloth": transformers.modeling_utils.checkpoint = hf_grad_checkpoint_unsloth_wrapper + if hasattr(model_config, "model_type") and model_config.model_type == "mllama": + if cfg.flash_attention: + from axolotl.monkeypatch.attention.mllama import patch_mllama + + patch_mllama() + if hasattr(model_config, "model_type") and model_config.model_type == "btlm": if cfg.flash_attention: from axolotl.monkeypatch.btlm_attn_hijack_flash import ( @@ -461,6 +500,19 @@ def load_model( max_memory = cfg.max_memory device_map = cfg.device_map + AutoModelLoader = AutoModelForCausalLM # pylint: disable=invalid-name + if cfg.is_multimodal: + if model_config.model_type == "llava": + AutoModelLoader = ( # pylint: disable=invalid-name + LlavaForConditionalGeneration + ) + elif model_config.model_type == "mllama": + AutoModelLoader = ( # pylint: disable=invalid-name + MllamaForConditionalGeneration + ) + else: + AutoModelLoader = AutoModelForVision2Seq # pylint: disable=invalid-name + if cfg.gpu_memory_limit: gpu_memory_limit = ( str(cfg.gpu_memory_limit) + "GiB" @@ -478,7 +530,7 @@ def load_model( from accelerate import infer_auto_device_map with init_empty_weights(): - model_canvas = AutoModelForCausalLM.from_config( + model_canvas = AutoModelLoader.from_config( model_config, trust_remote_code=cfg.trust_remote_code or False ) model_canvas.tie_weights() @@ -633,6 +685,8 @@ def load_model( quantization_config = ( quantization_config or model_kwargs["quantization_config"] ) + if cfg.is_multimodal: + model_config.text_config = text_model_config model = load_sharded_model_quant( base_model, model_config, @@ -651,7 +705,9 @@ def load_model( if "device_map" in model_kwargs: del model_kwargs["device_map"] - model = AutoModelForCausalLM.from_pretrained( + if cfg.is_multimodal: + model_config.text_config = text_model_config + model = AutoModelLoader.from_pretrained( base_model, config=model_config, **model_kwargs, @@ -690,13 +746,17 @@ def load_model( and not cfg.trust_remote_code ): if cfg.gptq: - model = AutoModelForCausalLM.from_pretrained( + if cfg.is_multimodal: + model_config.text_config = text_model_config + model = AutoModelLoader.from_pretrained( base_model, config=model_config, trust_remote_code=cfg.trust_remote_code or False, **model_kwargs, ) else: + if cfg.is_multimodal: + model_config.text_config = text_model_config model = getattr(transformers, model_type).from_pretrained( base_model, config=model_config, @@ -707,21 +767,23 @@ def load_model( # Shouldn't be a problem most of the time. will obviously error if the model doesn't support this # when training starts if ( - hasattr(model_config, "max_seq_len") - and model_config.max_seq_len + hasattr(text_model_config, "max_seq_len") + and text_model_config.max_seq_len and cfg.sequence_len > model_config.max_seq_len ): - model_config.max_seq_len = cfg.sequence_len + text_model_config.max_seq_len = cfg.sequence_len LOG.warning(f"increasing context length to {cfg.sequence_len}") elif ( - hasattr(model_config, "max_sequence_length") - and model_config.max_sequence_length - and cfg.sequence_len > model_config.max_sequence_length + hasattr(text_model_config, "max_sequence_length") + and text_model_config.max_sequence_length + and cfg.sequence_len > text_model_config.max_sequence_length ): - model_config.max_sequence_length = cfg.sequence_len + text_model_config.max_sequence_length = cfg.sequence_len LOG.warning(f"increasing context length to {cfg.sequence_len}") if cfg.gptq: - model = AutoModelForCausalLM.from_pretrained( + if cfg.is_multimodal: + model_config.text_config = text_model_config + model = AutoModelLoader.from_pretrained( base_model, config=model_config, trust_remote_code=cfg.trust_remote_code or False, @@ -734,7 +796,9 @@ def load_model( if "device_map" in model_kwargs: del model_kwargs["device_map"] - model = AutoModelForCausalLM.from_pretrained( + if cfg.is_multimodal: + model_config.text_config = text_model_config + model = AutoModelLoader.from_pretrained( base_model, config=model_config, trust_remote_code=cfg.trust_remote_code or False, @@ -1016,12 +1080,17 @@ def load_lora(model, cfg, inference=False, config_only=False): from peft import LoraConfig, get_peft_model - lora_target_modules = list(cfg.lora_target_modules or []) + lora_target_modules = cfg.lora_target_modules or [] if cfg.lora_target_linear: linear_names = find_all_linear_names(model) LOG.info(f"found linear modules: {repr(sorted(linear_names))}") - lora_target_modules = list(set(lora_target_modules + linear_names)) + lora_target_modules_as_list = ( + lora_target_modules + if isinstance(lora_target_modules, list) + else [lora_target_modules] + ) + lora_target_modules = list(set(lora_target_modules_as_list + linear_names)) lora_config_kwargs = {} loftq_bits = cfg.peft and cfg.peft.loftq_config and cfg.peft.loftq_config.loftq_bits @@ -1040,6 +1109,7 @@ def load_lora(model, cfg, inference=False, config_only=False): lora_alpha=cfg.lora_alpha, target_modules=lora_target_modules, layers_to_transform=cfg.peft_layers_to_transform, + layers_pattern=cfg.peft_layers_pattern, lora_dropout=cfg.lora_dropout, fan_in_fan_out=cfg.lora_fan_in_fan_out, modules_to_save=cfg.lora_modules_to_save if cfg.lora_modules_to_save else None, diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 89ae4e6970..17276dd8ed 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -306,7 +306,7 @@ def process_pretraining_datasets_for_packing( def calculate_total_num_steps(cfg, train_dataset, update=True): - if not cfg.total_num_tokens: + if not cfg.total_num_tokens and not cfg.skip_prepare_dataset: total_num_tokens = np.sum( train_dataset.data.column("input_ids") .to_pandas() @@ -319,7 +319,11 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): skip_estimates = cfg.model_config_type == "mamba" - if not skip_estimates and not cfg.total_supervised_tokens: + if ( + not skip_estimates + and not cfg.total_supervised_tokens + and not cfg.skip_prepare_dataset + ): total_supervised_tokens = ( train_dataset.data.column("labels") .to_pandas() @@ -478,13 +482,15 @@ def prepare_opinionated_env(cfg): os.environ["TOKENIZERS_PARALLELISM"] = "false" -def setup_trainer(cfg, train_dataset, eval_dataset, model, tokenizer, total_num_steps): +def setup_trainer( + cfg, train_dataset, eval_dataset, model, tokenizer, processor, total_num_steps +): if cfg.rl in ["dpo", "ipo", "orpo", "kto", "simpo"]: - trainer_builder = HFRLTrainerBuilder(cfg, model[0], tokenizer) + trainer_builder = HFRLTrainerBuilder(cfg, model[0], tokenizer, processor) trainer_builder.model_ref = model[1] trainer_builder.peft_config = model[2] else: - trainer_builder = HFCausalTrainerBuilder(cfg, model[0], tokenizer) + trainer_builder = HFCausalTrainerBuilder(cfg, model[0], tokenizer, processor) trainer_builder.train_dataset = train_dataset trainer_builder.eval_dataset = eval_dataset diff --git a/tests/prompt_strategies/test_chat_templates.py b/tests/prompt_strategies/test_chat_templates.py index 28210b7ae8..20533504ce 100644 --- a/tests/prompt_strategies/test_chat_templates.py +++ b/tests/prompt_strategies/test_chat_templates.py @@ -73,7 +73,7 @@ def test_llama3(self, llama3_tokenizer, assistant_dataset): strategy = ChatTemplateStrategy( ChatTemplatePrompter( llama3_tokenizer, - chat_templates("llama3"), + chat_template=chat_templates("llama3"), message_field_role="role", message_field_content="content", roles={ @@ -113,7 +113,7 @@ def test_phi35(self, phi35_tokenizer, assistant_dataset): strategy = ChatTemplateStrategy( ChatTemplatePrompter( phi35_tokenizer, - chat_templates("phi_35"), + chat_template=chat_templates("phi_35"), message_field_role="role", message_field_content="content", roles={ @@ -171,7 +171,7 @@ def test_llama3_with_training_data(self, llama3_tokenizer, assistant_dataset): strategy = ChatTemplateStrategy( ChatTemplatePrompter( llama3_tokenizer, - chat_templates("llama3"), + chat_template=chat_templates("llama3"), message_field_role="role", message_field_content="content", message_field_training="training", @@ -227,8 +227,11 @@ class TestSharegptChatTemplateLlama3: def test_llama3_assistant(self, llama3_tokenizer, sharegpt_dataset): LOG.info("Testing ShareGPT style datasets with llama-3 assistant prompts") + # pylint: disable=duplicate-code strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + ChatTemplatePrompter( + llama3_tokenizer, chat_template=chat_templates("llama3") + ), tokenizer=llama3_tokenizer, train_on_inputs=False, train_on_eos="none", @@ -277,8 +280,11 @@ def test_llama3_assistant(self, llama3_tokenizer, sharegpt_dataset): def test_llama3_human(self, llama3_tokenizer, sharegpt_dataset): LOG.info("Testing ShareGPT style datasets with llama-3 human prompts") + # pylint: disable=duplicate-code strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + ChatTemplatePrompter( + llama3_tokenizer, chat_template=chat_templates("llama3") + ), tokenizer=llama3_tokenizer, train_on_inputs=False, train_on_eos="none", @@ -327,8 +333,11 @@ def test_llama3_human(self, llama3_tokenizer, sharegpt_dataset): def test_llama3_system_human(self, llama3_tokenizer, basic_dataset): LOG.info("Testing ShareGPT style datasets with llama-3 system/human prompts") + # pylint: disable=duplicate-code strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + ChatTemplatePrompter( + llama3_tokenizer, chat_template=chat_templates("llama3") + ), tokenizer=llama3_tokenizer, train_on_inputs=False, train_on_eos="none", diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index f18fb39423..50429e3a26 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -34,7 +34,9 @@ def find_sublist(full_list, sub_list): def test_train_on_inputs_true(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with train_on_inputs=True") strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + ChatTemplatePrompter( + llama3_tokenizer, chat_template=chat_templates("llama3") + ), tokenizer=llama3_tokenizer, train_on_inputs=True, sequence_len=512, @@ -77,7 +79,9 @@ def test_train_on_inputs_true(self, llama3_tokenizer, basic_dataset): def test_train_on_inputs_false(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with train_on_inputs=False") strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + ChatTemplatePrompter( + llama3_tokenizer, chat_template=chat_templates("llama3") + ), tokenizer=llama3_tokenizer, train_on_inputs=False, sequence_len=512, @@ -118,7 +122,9 @@ def test_train_on_inputs_false(self, llama3_tokenizer, basic_dataset): def test_roles_to_train_assistant_only(self, llama3_tokenizer, basic_dataset): LOG.info("Testing roles_to_train with assistant only") strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + ChatTemplatePrompter( + llama3_tokenizer, chat_template=chat_templates("llama3") + ), tokenizer=llama3_tokenizer, train_on_inputs=False, sequence_len=512, @@ -144,7 +150,9 @@ def test_roles_to_train_assistant_only(self, llama3_tokenizer, basic_dataset): def test_roles_to_train_all(self, llama3_tokenizer, basic_dataset): LOG.info("Testing roles_to_train with all roles") strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + ChatTemplatePrompter( + llama3_tokenizer, chat_template=chat_templates("llama3") + ), tokenizer=llama3_tokenizer, train_on_inputs=True, sequence_len=512, @@ -175,7 +183,9 @@ def test_roles_to_train_all(self, llama3_tokenizer, basic_dataset): def test_empty_roles_to_train(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with empty roles_to_train") strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + ChatTemplatePrompter( + llama3_tokenizer, chat_template=chat_templates("llama3") + ), tokenizer=llama3_tokenizer, train_on_inputs=False, sequence_len=512, @@ -194,7 +204,9 @@ def test_empty_roles_to_train(self, llama3_tokenizer, basic_dataset): def test_train_on_eos_all(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with train_on_eos='all'") strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + ChatTemplatePrompter( + llama3_tokenizer, chat_template=chat_templates("llama3") + ), tokenizer=llama3_tokenizer, train_on_inputs=False, sequence_len=512, @@ -219,7 +231,9 @@ def test_train_on_eos_all(self, llama3_tokenizer, basic_dataset): def test_train_on_eos_turn(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with train_on_eos='turn'") strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + ChatTemplatePrompter( + llama3_tokenizer, chat_template=chat_templates("llama3") + ), tokenizer=llama3_tokenizer, train_on_inputs=False, sequence_len=512, @@ -267,7 +281,9 @@ def test_train_on_eos_turn(self, llama3_tokenizer, basic_dataset): def test_train_on_eos_last(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with train_on_eos='last'") strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + ChatTemplatePrompter( + llama3_tokenizer, chat_template=chat_templates("llama3") + ), tokenizer=llama3_tokenizer, train_on_inputs=False, sequence_len=512, @@ -298,7 +314,9 @@ def test_train_on_eos_last(self, llama3_tokenizer, basic_dataset): def test_train_on_eos_none(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with train_on_eos='none'") strategy = ChatTemplateStrategy( - ChatTemplatePrompter(llama3_tokenizer, chat_templates("llama3")), + ChatTemplatePrompter( + llama3_tokenizer, chat_template=chat_templates("llama3") + ), tokenizer=llama3_tokenizer, train_on_inputs=False, sequence_len=512, @@ -324,7 +342,9 @@ def test_drop_system_message(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with drop_system_message=True") strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_templates("llama3"), drop_system_message=True + llama3_tokenizer, + chat_template=chat_templates("llama3"), + drop_system_message=True, ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -350,7 +370,9 @@ def test_custom_roles(self, llama3_tokenizer): } strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_templates("llama3"), roles=custom_roles + llama3_tokenizer, + chat_template=chat_templates("llama3"), + roles=custom_roles, ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -402,7 +424,7 @@ def test_message_field_training(self, llama3_tokenizer): strategy = ChatTemplateStrategy( ChatTemplatePrompter( llama3_tokenizer, - chat_templates("llama3"), + chat_template=chat_templates("llama3"), message_field_training="train", message_field_training_detail="train_detail", ), From 4ca0a47cfb884f2d4421785982e64220b26f48df Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 9 Oct 2024 08:43:11 -0400 Subject: [PATCH 0170/1405] add 2.4.1 to base models (#1953) --- .github/workflows/base.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 9101fc2bea..5e8c8fc33d 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -30,6 +30,12 @@ jobs: python_version: "3.11" pytorch: 2.4.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + - cuda: "124" + cuda_version: 12.4.1 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.4.1 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" steps: - name: Checkout uses: actions/checkout@v3 From e8d3da00814ec7773d33edd5643bb885d85686cb Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 9 Oct 2024 11:53:56 -0400 Subject: [PATCH 0171/1405] upgrade pytorch from 2.4.0 => 2.4.1 (#1950) * upgrade pytorch from 2.4.0 => 2.4.1 * update xformers for updated pytorch version * handle xformers version case for torch==2.3.1 --- .github/workflows/base.yml | 2 +- .github/workflows/main.yml | 4 ++-- .github/workflows/nightlies.yml | 4 ++-- .github/workflows/tests-nightly.yml | 4 ++-- .github/workflows/tests.yml | 4 ++-- requirements.txt | 2 +- setup.py | 7 +++++++ 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 5e8c8fc33d..1b24f2c970 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -28,7 +28,7 @@ jobs: cuda_version: 12.4.1 cudnn_version: "" python_version: "3.11" - pytorch: 2.4.0 + pytorch: 2.4.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "124" cuda_version: 12.4.1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5a972f5f08..c27dbedefa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,7 +27,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.4.0 + pytorch: 2.4.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: @@ -84,7 +84,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.4.0 + pytorch: 2.4.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 1d95a0983f..17c76c24e7 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -26,7 +26,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.4.0 + pytorch: 2.4.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: @@ -83,7 +83,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.4.0 + pytorch: 2.4.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 30ed397cef..8c9e1f49e7 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -25,7 +25,7 @@ jobs: fail-fast: false matrix: python_version: ["3.10", "3.11"] - pytorch_version: ["2.3.1", "2.4.0"] + pytorch_version: ["2.3.1", "2.4.1"] timeout-minutes: 20 steps: @@ -91,7 +91,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.4.0 + pytorch: 2.4.1 num_gpus: 1 axolotl_extras: nightly_build: "true" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c104e92c27..a798bdd5cd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: fail-fast: false matrix: python_version: ["3.10", "3.11"] - pytorch_version: ["2.3.1", "2.4.0"] + pytorch_version: ["2.3.1", "2.4.1"] timeout-minutes: 20 steps: @@ -94,7 +94,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.4.0 + pytorch: 2.4.1 num_gpus: 1 axolotl_extras: steps: diff --git a/requirements.txt b/requirements.txt index 123a4ee54a..41bfdfbeb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ flash-attn==2.6.3 sentencepiece wandb einops -xformers==0.0.27 +xformers==0.0.28.post1 optimum==1.16.2 hf_transfer colorama diff --git a/setup.py b/setup.py index 1b64fadaef..e939bc37ee 100644 --- a/setup.py +++ b/setup.py @@ -49,10 +49,17 @@ def parse_requirements(): else: raise ValueError("Invalid version format") + if (major, minor) >= (2, 4): + if patch == 0: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.27") if (major, minor) >= (2, 3): if patch == 0: _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.26.post1") + else: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.27") elif (major, minor) >= (2, 2): _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.25.post1") From a560593b1dbac3f3afcbe6bdf975c9c9e5a5afcc Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 10 Oct 2024 03:02:32 +0700 Subject: [PATCH 0172/1405] fix(log): update perplexity log to clarify from eval split (#1952) [skip ci] --- src/axolotl/utils/callbacks/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 73715b06ab..acc2238a4f 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -462,7 +462,7 @@ def evaluate_preds(sources, predictions, references): references=[[r] for r in references], predictions=predictions, ) - scores[metric_name] = score + scores["eval_" + metric_name] = score return scores def predict_with_generate(): From dee77232feb5c7e41216e5586da3ec4407638846 Mon Sep 17 00:00:00 2001 From: aarush gupta Date: Wed, 9 Oct 2024 13:03:16 -0700 Subject: [PATCH 0173/1405] fix type annotations (#1941) [skip ci] --- src/axolotl/monkeypatch/relora.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/axolotl/monkeypatch/relora.py b/src/axolotl/monkeypatch/relora.py index e4352cbe3d..9d246cb17f 100644 --- a/src/axolotl/monkeypatch/relora.py +++ b/src/axolotl/monkeypatch/relora.py @@ -44,8 +44,8 @@ def magnitude_pruning_(tensor, prune_ratio): def reset_optimizer( optimizer: torch.optim.Optimizer, *, - reset_params: list[str], # where str is the key to a torch.nn.Parameter - optimizer_state_keys: list[str], + reset_params: List[str], # where str is the key to a torch.nn.Parameter + optimizer_state_keys: List[str], prune_ratio: float = 0.9, ): pruning_fn = partial(magnitude_pruning_, prune_ratio=prune_ratio) From 6d3caadf90a9d4faafe8e167441355d128c66537 Mon Sep 17 00:00:00 2001 From: Boris Feld Date: Wed, 9 Oct 2024 22:03:37 +0200 Subject: [PATCH 0174/1405] Comet integration (#1939) * Add first version of a Comet integration * Remove debug prints * Add test for Comet Configuration transformation to env variables * Fix last lint warning * Update Readme for Comet logging documentation * Update Comet integration to be optional, update code and tests * Add documentation for Comet configuration * Add missing check --- .isort.cfg | 2 +- README.md | 18 ++- docs/config.qmd | 12 ++ src/axolotl/cli/__init__.py | 3 + src/axolotl/core/trainer_builder.py | 15 ++- src/axolotl/utils/__init__.py | 6 +- src/axolotl/utils/callbacks/__init__.py | 11 +- src/axolotl/utils/callbacks/comet_.py | 43 ++++++++ src/axolotl/utils/comet_.py | 93 ++++++++++++++++ .../config/models/input/v0_4_1/__init__.py | 14 +++ tests/test_validation.py | 103 ++++++++++++++++++ 11 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 src/axolotl/utils/callbacks/comet_.py create mode 100644 src/axolotl/utils/comet_.py diff --git a/.isort.cfg b/.isort.cfg index 79067a7c91..e487797321 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,3 +1,3 @@ [settings] profile=black -known_third_party=wandb +known_third_party=wandb,comet_ml diff --git a/README.md b/README.md index c84f1cb8c9..f6f4e4e806 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Features: - Integrated with xformer, flash attention, [liger kernel](https://github.com/linkedin/Liger-Kernel), rope scaling, and multipacking - Works with single GPU or multiple GPUs via FSDP or Deepspeed - Easily run with Docker locally or on the cloud -- Log results and optionally checkpoints to wandb or mlflow +- Log results and optionally checkpoints to wandb, mlflow or Comet - And more! @@ -515,6 +515,22 @@ wandb_name: wandb_log_model: ``` +##### Comet Logging + +Make sure your `COMET_API_KEY` environment variable is set (recommended) or you login to wandb with `comet login`. + +- wandb options +```yaml +use_comet: +comet_api_key: +comet_workspace: +comet_project_name: +comet_experiment_key: +comet_mode: +comet_online: +comet_experiment_config: +``` + ##### Special Tokens It is important to have special tokens like delimiters, end-of-sequence, beginning-of-sequence in your tokenizer's vocabulary. This will help you avoid tokenization issues and help your model train better. You can do this in axolotl like this: diff --git a/docs/config.qmd b/docs/config.qmd index e859999787..99a69a0973 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -267,6 +267,18 @@ mlflow_tracking_uri: # URI to mlflow mlflow_experiment_name: # Your experiment name hf_mlflow_log_artifacts: # set to true to copy each saved checkpoint on each save to mlflow artifact registry +# Comet configuration if you're using it +# Make sure your `COMET_API_KEY` environment variable is set (recommended) or you login to Comet with `comet login`. +# Check out our documentation for more details https://www.comet.com/docs/v2/api-and-sdk/python-sdk/reference/Experiment-Creation/#comet_ml.start +use_comet: # Enable or disable Comet integration. +comet_api_key: # API key for Comet. Recommended to set via `comet login`. +comet_workspace: # Workspace name in Comet. Defaults to the user's default workspace. +comet_project_name: # Project name in Comet. Defaults to Uncategorized. +comet_experiment_key: # Identifier for the experiment. Used to append data to an existing experiment or control the key of new experiments. Default to a random key. +comet_mode: # Create a new experiment ("create") or log to an existing one ("get"). Default ("get_or_create") auto-selects based on configuration. +comet_online: # Set to True to log data to Comet server, or False for offline storage. Default is True. +comet_experiment_config: # Dictionary for additional configuration settings, see the doc for more details. + # Where to save the full-finetuned model to output_dir: ./completed-model diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index a1d84b6a16..db975501a3 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -31,6 +31,7 @@ from axolotl.logging_config import configure_logging from axolotl.train import TrainDatasetMeta from axolotl.utils.chat_templates import chat_templates +from axolotl.utils.comet_ import setup_comet_env_vars from axolotl.utils.config import ( normalize_cfg_datasets, normalize_config, @@ -421,6 +422,8 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs): setup_mlflow_env_vars(cfg) + setup_comet_env_vars(cfg) + return cfg diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 4893e63dc2..b1ee519dc4 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -48,7 +48,7 @@ from axolotl.monkeypatch.multipack import SUPPORTED_MULTIPACK_MODEL_TYPES from axolotl.monkeypatch.relora import ReLoRACallback, ReLoRAScheduler -from axolotl.utils import is_mlflow_available +from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.callbacks import ( EvalFirstStepCallback, GPUStatsCallback, @@ -1111,6 +1111,12 @@ def get_callbacks(self) -> List[TrainerCallback]: callbacks.append( SaveAxolotlConfigtoMlflowCallback(self.cfg.axolotl_config_path) ) + if self.cfg.use_comet and is_comet_available(): + from axolotl.utils.callbacks.comet_ import SaveAxolotlConfigtoCometCallback + + callbacks.append( + SaveAxolotlConfigtoCometCallback(self.cfg.axolotl_config_path) + ) return callbacks @@ -1179,6 +1185,11 @@ def get_post_trainer_create_callbacks(self, trainer): trainer, self.tokenizer, "mlflow" ) callbacks.append(LogPredictionCallback(self.cfg)) + if self.cfg.use_comet and is_comet_available() and self.cfg.eval_table_size > 0: + LogPredictionCallback = log_prediction_callback_factory( + trainer, self.tokenizer, "comet_ml" + ) + callbacks.append(LogPredictionCallback(self.cfg)) if self.cfg.do_bench_eval: callbacks.append(bench_eval_callback_factory(trainer, self.tokenizer)) @@ -1430,6 +1441,8 @@ def build(self, total_num_steps): report_to.append("mlflow") if self.cfg.use_tensorboard: report_to.append("tensorboard") + if self.cfg.use_comet: + report_to.append("comet_ml") training_arguments_kwargs["report_to"] = report_to training_arguments_kwargs["run_name"] = ( diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index 99dec79f1b..91545009ad 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -1,8 +1,12 @@ """ Basic utils for Axolotl """ -import importlib +import importlib.util def is_mlflow_available(): return importlib.util.find_spec("mlflow") is not None + + +def is_comet_available(): + return importlib.util.find_spec("comet_ml") is not None diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index acc2238a4f..0bc781fcb4 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -29,7 +29,7 @@ ) from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, IntervalStrategy -from axolotl.utils import is_mlflow_available +from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.callbacks.perplexity import Perplexity from axolotl.utils.config.models.input.v0_4_1 import AxolotlInputConfig @@ -747,6 +747,15 @@ def log_table_from_dataloader(name: str, table_dataloader): artifact_file="PredictionsVsGroundTruth.json", tracking_uri=tracking_uri, ) + elif logger == "comet_ml" and is_comet_available(): + import comet_ml + + experiment = comet_ml.get_running_experiment() + if experiment: + experiment.log_table( + f"{name} - Predictions vs Ground Truth.csv", + pd.DataFrame(table_data), + ) if is_main_process(): log_table_from_dataloader("Eval", eval_dataloader) diff --git a/src/axolotl/utils/callbacks/comet_.py b/src/axolotl/utils/callbacks/comet_.py new file mode 100644 index 0000000000..b29f997a86 --- /dev/null +++ b/src/axolotl/utils/callbacks/comet_.py @@ -0,0 +1,43 @@ +"""Comet module for trainer callbacks""" + +import logging +from typing import TYPE_CHECKING + +import comet_ml +from transformers import TrainerCallback, TrainerControl, TrainerState + +from axolotl.utils.distributed import is_main_process + +if TYPE_CHECKING: + from axolotl.core.trainer_builder import AxolotlTrainingArguments + +LOG = logging.getLogger("axolotl.callbacks") + + +class SaveAxolotlConfigtoCometCallback(TrainerCallback): + """Callback to save axolotl config to comet""" + + def __init__(self, axolotl_config_path): + self.axolotl_config_path = axolotl_config_path + + def on_train_begin( + self, + args: "AxolotlTrainingArguments", # pylint: disable=unused-argument + state: TrainerState, # pylint: disable=unused-argument + control: TrainerControl, + **kwargs, # pylint: disable=unused-argument + ): + if is_main_process(): + try: + comet_experiment = comet_ml.start(source="axolotl") + comet_experiment.log_other("Created from", "axolotl") + comet_experiment.log_asset( + self.axolotl_config_path, + file_name="axolotl-config", + ) + LOG.info( + "The Axolotl config has been saved to the Comet Experiment under assets." + ) + except (FileNotFoundError, ConnectionError) as err: + LOG.warning(f"Error while saving Axolotl config to Comet: {err}") + return control diff --git a/src/axolotl/utils/comet_.py b/src/axolotl/utils/comet_.py new file mode 100644 index 0000000000..b4ecc80ad9 --- /dev/null +++ b/src/axolotl/utils/comet_.py @@ -0,0 +1,93 @@ +"""Module for wandb utilities""" + +import logging +import os + +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger("axolotl.utils.comet_") + +COMET_ENV_MAPPING_OVERRIDE = { + "comet_mode": "COMET_START_MODE", + "comet_online": "COMET_START_ONLINE", +} +COMET_EXPERIMENT_CONFIG_ENV_MAPPING_OVERRIDE = { + "auto_histogram_activation_logging": "COMET_AUTO_LOG_HISTOGRAM_ACTIVATIONS", + "auto_histogram_epoch_rate": "COMET_AUTO_LOG_HISTOGRAM_EPOCH_RATE", + "auto_histogram_gradient_logging": "COMET_AUTO_LOG_HISTOGRAM_GRADIENTS", + "auto_histogram_tensorboard_logging": "COMET_AUTO_LOG_HISTOGRAM_TENSORBOARD", + "auto_histogram_weight_logging": "COMET_AUTO_LOG_HISTOGRAM_WEIGHTS", + "auto_log_co2": "COMET_AUTO_LOG_CO2", + "auto_metric_logging": "COMET_AUTO_LOG_METRICS", + "auto_metric_step_rate": "COMET_AUTO_LOG_METRIC_STEP_RATE", + "auto_output_logging": "COMET_AUTO_LOG_OUTPUT_LOGGER", + "auto_param_logging": "COMET_AUTO_LOG_PARAMETERS", + "comet_disabled": "COMET_AUTO_LOG_DISABLE", + "display_summary_level": "COMET_DISPLAY_SUMMARY_LEVEL", + "distributed_node_identifier": "COMET_DISTRIBUTED_NODE_IDENTIFIER", + "log_code": "COMET_AUTO_LOG_CODE", + "log_env_cpu": "COMET_AUTO_LOG_ENV_CPU", + "log_env_details": "COMET_AUTO_LOG_ENV_DETAILS", + "log_env_disk": "COMET_AUTO_LOG_ENV_DISK", + "log_env_gpu": "COMET_AUTO_LOG_ENV_GPU", + "log_env_host": "COMET_AUTO_LOG_ENV_HOST", + "log_env_network": "COMET_AUTO_LOG_ENV_NETWORK", + "log_git_metadata": "COMET_AUTO_LOG_GIT_METADATA", + "log_git_patch": "COMET_AUTO_LOG_GIT_PATCH", + "log_graph": "COMET_AUTO_LOG_GRAPH", + "name": "COMET_START_EXPERIMENT_NAME", + "offline_directory": "COMET_OFFLINE_DIRECTORY", + "parse_args": "COMET_AUTO_LOG_CLI_ARGUMENTS", + "tags": "COMET_START_EXPERIMENT_TAGS", +} + + +def python_value_to_environ_value(python_value): + if isinstance(python_value, bool): + if python_value is True: + return "true" + + return "false" + + if isinstance(python_value, int): + return str(python_value) + + if isinstance(python_value, list): # Comet only have one list of string parameter + return ",".join(map(str, python_value)) + + return python_value + + +def setup_comet_env_vars(cfg: DictDefault): + # TODO, we need to convert Axolotl configuration to environment variables + # as Transformers integration are call first and would create an + # Experiment first + + for key in cfg.keys(): + if key.startswith("comet_") and key != "comet_experiment_config": + value = cfg.get(key, "") + + if value is not None and value != "": + env_variable_name = COMET_ENV_MAPPING_OVERRIDE.get(key, key.upper()) + final_value = python_value_to_environ_value(value) + os.environ[env_variable_name] = final_value + + if cfg.comet_experiment_config: + for key, value in cfg.comet_experiment_config.items(): + if value is not None and value != "": + config_env_variable_name = ( + COMET_EXPERIMENT_CONFIG_ENV_MAPPING_OVERRIDE.get(key) + ) + + if config_env_variable_name is None: + LOG.warning( + f"Unknown Comet Experiment Config name {key}, ignoring it" + ) + continue + + final_value = python_value_to_environ_value(value) + os.environ[config_env_variable_name] = final_value + + # Enable comet if project name is present + if cfg.comet_project_name and len(cfg.comet_project_name) > 0: + cfg.use_comet = True diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index fced5e639d..76748191bf 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -489,6 +489,19 @@ def check_wandb_run(cls, data): return data +class CometConfig(BaseModel): + """Comet configuration subset""" + + use_comet: Optional[bool] = None + comet_api_key: Optional[str] = None + comet_workspace: Optional[str] = None + comet_project_name: Optional[str] = None + comet_experiment_key: Optional[str] = None + comet_mode: Optional[str] = None + comet_online: Optional[bool] = None + comet_experiment_config: Optional[Dict[str, Any]] = None + + class GradioConfig(BaseModel): """Gradio configuration subset""" @@ -509,6 +522,7 @@ class AxolotlInputConfig( HyperparametersConfig, WandbConfig, MLFlowConfig, + CometConfig, LISAConfig, GradioConfig, RemappedParameters, diff --git a/tests/test_validation.py b/tests/test_validation.py index 35d0e265e7..6e0d0ad2a5 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -9,6 +9,7 @@ import pytest from pydantic import ValidationError +from axolotl.utils import is_comet_available from axolotl.utils.config import validate_config from axolotl.utils.config.models.input.v0_4_1 import AxolotlConfigWCapabilities from axolotl.utils.dict import DictDefault @@ -1329,3 +1330,105 @@ def test_wandb_set_disabled(self, minimal_cfg): os.environ.pop("WANDB_PROJECT", None) os.environ.pop("WANDB_DISABLED", None) + + +@pytest.mark.skipif(is_comet_available() is False, reason="comet_ml is not installed") +class TestValidationComet(BaseValidation): + """ + Validation test for comet + """ + + def test_comet_sets_env(self, minimal_cfg): + from axolotl.utils.comet_ import setup_comet_env_vars + + comet_config = { + "comet_api_key": "foo", + "comet_workspace": "some_workspace", + "comet_project_name": "some_project", + "comet_experiment_key": "some_experiment_key", + "comet_mode": "get_or_create", + "comet_online": False, + "comet_experiment_config": { + "auto_histogram_activation_logging": False, + "auto_histogram_epoch_rate": 2, + "auto_histogram_gradient_logging": True, + "auto_histogram_tensorboard_logging": False, + "auto_histogram_weight_logging": True, + "auto_log_co2": False, + "auto_metric_logging": True, + "auto_metric_step_rate": 15, + "auto_output_logging": False, + "auto_param_logging": True, + "comet_disabled": False, + "display_summary_level": 2, + "distributed_node_identifier": "some_distributed_node_identifier", + "log_code": True, + "log_env_cpu": False, + "log_env_details": True, + "log_env_disk": False, + "log_env_gpu": True, + "log_env_host": False, + "log_env_network": True, + "log_git_metadata": False, + "log_git_patch": True, + "log_graph": False, + "name": "some_name", + "offline_directory": "some_offline_directory", + "parse_args": True, + "tags": ["tag1", "tag2"], + }, + } + + cfg = DictDefault(comet_config) | minimal_cfg + + new_cfg = validate_config(cfg) + + setup_comet_env_vars(new_cfg) + + comet_env = { + key: value for key, value in os.environ.items() if key.startswith("COMET_") + } + + assert ( + len(comet_env) + == len(comet_config) + len(comet_config["comet_experiment_config"]) - 1 + ) + + assert comet_env == { + "COMET_API_KEY": "foo", + "COMET_AUTO_LOG_CLI_ARGUMENTS": "true", + "COMET_AUTO_LOG_CO2": "false", + "COMET_AUTO_LOG_CODE": "true", + "COMET_AUTO_LOG_DISABLE": "false", + "COMET_AUTO_LOG_ENV_CPU": "false", + "COMET_AUTO_LOG_ENV_DETAILS": "true", + "COMET_AUTO_LOG_ENV_DISK": "false", + "COMET_AUTO_LOG_ENV_GPU": "true", + "COMET_AUTO_LOG_ENV_HOST": "false", + "COMET_AUTO_LOG_ENV_NETWORK": "true", + "COMET_AUTO_LOG_GIT_METADATA": "false", + "COMET_AUTO_LOG_GIT_PATCH": "true", + "COMET_AUTO_LOG_GRAPH": "false", + "COMET_AUTO_LOG_HISTOGRAM_ACTIVATIONS": "false", + "COMET_AUTO_LOG_HISTOGRAM_EPOCH_RATE": "2", + "COMET_AUTO_LOG_HISTOGRAM_GRADIENTS": "true", + "COMET_AUTO_LOG_HISTOGRAM_TENSORBOARD": "false", + "COMET_AUTO_LOG_HISTOGRAM_WEIGHTS": "true", + "COMET_AUTO_LOG_METRIC_STEP_RATE": "15", + "COMET_AUTO_LOG_METRICS": "true", + "COMET_AUTO_LOG_OUTPUT_LOGGER": "false", + "COMET_AUTO_LOG_PARAMETERS": "true", + "COMET_DISPLAY_SUMMARY_LEVEL": "2", + "COMET_DISTRIBUTED_NODE_IDENTIFIER": "some_distributed_node_identifier", + "COMET_EXPERIMENT_KEY": "some_experiment_key", + "COMET_OFFLINE_DIRECTORY": "some_offline_directory", + "COMET_PROJECT_NAME": "some_project", + "COMET_START_EXPERIMENT_NAME": "some_name", + "COMET_START_EXPERIMENT_TAGS": "tag1,tag2", + "COMET_START_MODE": "get_or_create", + "COMET_START_ONLINE": "false", + "COMET_WORKSPACE": "some_workspace", + } + + for key in comet_env.keys(): + os.environ.pop(key, None) From 979534c851ddd4bdd53a8d0162b3ee349774860c Mon Sep 17 00:00:00 2001 From: pandora <128635000+pandora-s-git@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:22:53 +0200 Subject: [PATCH 0175/1405] add mistral templates (#1927) Co-authored-by: Wing Lian --- src/axolotl/utils/chat_templates.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 7468ae8b15..9e1e6ca326 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -5,7 +5,9 @@ CHAT_TEMPLATES = { "alpaca": "{% for message in messages %}{% if message['role'] == 'user' %}{{ '### Instruction: ' + message['content'] + '\n\n' }}{% elif message['role'] == 'assistant' %}{{ '### Response: ' + message['content'] + eos_token}}{% endif %}{% endfor %}", - "inst": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # I don't know what this one is called. Used by Mistral/Mixtral. + "mistral_v1": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ ' [INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # Mistral 7B V1, Mistral 7B V2, Mixtral 8x7B V1... + "mistral_v2v3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # V3: Mistral 7B V3, Small, Large... + "mistral_v3_tekken": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST]' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # V3-Tekken: Nemo, Pixtral... "chatml": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}", "gemma": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\n' + message['content'] | trim + '\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\n'}}{% endif %}", "cohere": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", From 8159cbd1ab5f0cf3dd5d07237cb7d0d3e40b8a57 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 10 Oct 2024 15:04:17 -0400 Subject: [PATCH 0176/1405] lm_eval harness post train (#1926) * wip, lm_eval harness post train * include latex parser * add dtype and doc * add validation when doing bench evals * automatically add test dataset when doing benches --- requirements.txt | 6 +++ src/axolotl/cli/train.py | 15 ++++--- src/axolotl/integrations/base.py | 37 ++++++++++++++++ src/axolotl/integrations/lm_eval/README.md | 13 ++++++ src/axolotl/integrations/lm_eval/__init__.py | 42 +++++++++++++++++++ src/axolotl/integrations/lm_eval/args.py | 15 +++++++ .../config/models/input/v0_4_1/__init__.py | 20 +++++++++ 7 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 src/axolotl/integrations/lm_eval/README.md create mode 100644 src/axolotl/integrations/lm_eval/__init__.py create mode 100644 src/axolotl/integrations/lm_eval/args.py diff --git a/requirements.txt b/requirements.txt index 41bfdfbeb4..4323c76ce1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,3 +46,9 @@ gcsfs>=2024.5.0 trl==0.9.6 zstandard==0.22.0 fastcore + +# lm eval harness +lm_eval==0.4.4 +langdetect==1.0.9 +immutabledict==4.2.0 +antlr4-python3-runtime==4.13.2 diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 050f18a054..16d66a82f0 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -3,13 +3,11 @@ """ import logging from pathlib import Path -from typing import Tuple, Union +from typing import Union import fire from dotenv import load_dotenv from transformers.hf_argparser import HfArgumentParser -from transformers.modeling_utils import PreTrainedModel -from transformers.tokenization_utils import PreTrainedTokenizer from axolotl.cli import ( check_accelerate_default_config, @@ -20,6 +18,7 @@ print_axolotl_text_art, ) from axolotl.common.cli import TrainerCliArgs +from axolotl.integrations.base import PluginManager from axolotl.prompt_strategies.sharegpt import ( register_chatml_template, register_llama3_template, @@ -39,7 +38,7 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): return do_train(parsed_cfg, parsed_cli_args) -def do_train(cfg, cli_args) -> Tuple[PreTrainedModel, PreTrainedTokenizer]: +def do_train(cfg, cli_args) -> None: print_axolotl_text_art() check_accelerate_default_config() check_user_token() @@ -64,7 +63,13 @@ def do_train(cfg, cli_args) -> Tuple[PreTrainedModel, PreTrainedTokenizer]: else: dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - return train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + model, tokenizer = train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + plugin_manager = PluginManager.get_instance() + + del model + del tokenizer + + plugin_manager.post_train_unload(cfg) if __name__ == "__main__": diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index d26eed90fe..e2bd79bc4d 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -159,6 +159,29 @@ def add_callbacks_post_trainer(self, cfg, trainer): List[callable]: A list of callback functions to be added to the TrainingArgs """ + def post_train(self, cfg, model): + """ + Performs actions after training is complete. + + Parameters: + cfg (dict): The axolotl configuration + model (object): The loaded model. + + Returns: + None + """ + + def post_train_unload(self, cfg): + """ + Performs actions after training is complete and the model is unloaded. + + Parameters: + cfg (dict): The configuration for the plugin. + + Returns: + None + """ + def load_plugin(plugin_name: str) -> BasePlugin: """ @@ -381,3 +404,17 @@ def add_callbacks_post_trainer(self, cfg, trainer): for plugin in self.plugins: callbacks.extend(plugin.add_callbacks_post_trainer(cfg, trainer)) return callbacks + + def post_train_unload(self, cfg): + """ + Calls the post_train_unload method of all registered plugins. + + Parameters: + cfg (dict): The configuration for the plugins. + model (object): The loaded model. + + Returns: + None + """ + for plugin in self.plugins: + plugin.post_train_unload(cfg) diff --git a/src/axolotl/integrations/lm_eval/README.md b/src/axolotl/integrations/lm_eval/README.md new file mode 100644 index 0000000000..3724c49ccf --- /dev/null +++ b/src/axolotl/integrations/lm_eval/README.md @@ -0,0 +1,13 @@ +# LM Eval Harness + +### Usage + +```yaml +plugins: + - axolotl.integrations.lm_eval.LMEvalPlugin + +lm_eval_tasks: + - gsm8k + - hellaswag + - arc_easy +``` diff --git a/src/axolotl/integrations/lm_eval/__init__.py b/src/axolotl/integrations/lm_eval/__init__.py new file mode 100644 index 0000000000..f1daa20000 --- /dev/null +++ b/src/axolotl/integrations/lm_eval/__init__.py @@ -0,0 +1,42 @@ +""" +Module for the Plugin for LM Eval Harness +""" +import subprocess # nosec +from datetime import datetime + +from axolotl.integrations.base import BasePlugin + +from .args import LMEvalArgs # pylint: disable=unused-import. # noqa: F401 + + +class LMEvalPlugin(BasePlugin): + """ + Plugin for LM Evaluation Harness integraton with Axolotl. + """ + + def get_input_args(self): + return "axolotl.integrations.lm_eval.LMEvalArgs" + + def post_train_unload(self, cfg): + tasks = ",".join(cfg.lm_eval_tasks) + fa2 = ",attn_implementation=flash_attention_2" if cfg.flash_attention else "" + dtype = ",dtype=bfloat16" if cfg.bf16 else ",dtype=float16" + output_path = cfg.output_dir + output_path += "" if cfg.output_dir.endswith("/") else "/" + output_path += "lm_eval_results/" + datetime.now().strftime("%Y%m%d_%H%M%S") + subprocess.run( # nosec + [ + "lm_eval", + "--model", + "hf", + "--model_args", + f"pretrained={cfg.output_dir}{fa2}{dtype}", + "--tasks", + tasks, + "--batch_size", + str(cfg.lm_eval_batch_size), + "--output_path", + output_path, + ], + check=True, + ) diff --git a/src/axolotl/integrations/lm_eval/args.py b/src/axolotl/integrations/lm_eval/args.py new file mode 100644 index 0000000000..f58e6a6e38 --- /dev/null +++ b/src/axolotl/integrations/lm_eval/args.py @@ -0,0 +1,15 @@ +""" +Module for handling lm eval harness input arguments. +""" +from typing import List, Optional + +from pydantic import BaseModel + + +class LMEvalArgs(BaseModel): + """ + Input args for lm eval harness + """ + + lm_eval_tasks: List[str] = [] + lm_eval_batch_size: Optional[int] = 8 diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 76748191bf..47796add6b 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -980,6 +980,26 @@ def check_evals(cls, data): "evaluation_strategy must be empty or set to `steps` when used with evals_per_epoch." ) + if data.get("do_bench_eval") and not ( + data.get("evals_per_epoch") or data.get("eval_steps") + ): + raise ValueError( + "do_bench_eval requires evals_per_epoch or eval_steps to be set." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_test_datasets_bench(cls, data): + if ( + data.get("do_bench_eval") + and not data.get("test_datasets") + and not data.get("val_set_size") + ): + LOG.warning( + "`do_bench_eval` needs a test dataset to run evals, adding an empty test_dataset." + ) + data["test_datasets"] = [{"path": "axolotl-ai-co/empty-test-ds"}] return data @model_validator(mode="before") From 2fbc6b0c644424feff91abe3d001fa1c6638e118 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 10 Oct 2024 15:57:37 -0400 Subject: [PATCH 0177/1405] Axo logo new (#1956) * update axolotl ascii art * spacing for logo * cleanup dithering * cleanup ascii logo a bit --- src/axolotl/cli/__init__.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index db975501a3..fd5ab3e56c 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -55,8 +55,22 @@ os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1" - -def print_axolotl_text_art(suffix=None): +AXOLOTL_LOGO = """ + #@@ #@@ @@# @@# + @@ @@ @@ @@ =@@# @@ #@ =@@#. + @@ #@@@@@@@@@ @@ #@#@= @@ #@ .=@@ + #@@@@@@@@@@@@@@@@@ =@# @# ##= ## =####=+ @@ =#####+ =#@@###. @@ + @@@@@@@@@@/ +@@/ +@@ #@ =@= #@= @@ =@#+ +#@# @@ =@#+ +#@# #@. @@ + @@@@@@@@@@ ##@@ ##@@ =@# @# =@# @# @@ @@ @@ @@ #@ #@ @@ + @@@@@@@@@@@@@@@@@@@@ #@=+++#@= =@@# @@ @@ @@ @@ #@ #@ @@ + =@#=====@@ =@# @# @@ @@ @@ @@ #@ #@ @@ + @@@@@@@@@@@@@@@@ @@@@ #@ #@= #@= +@@ #@# =@# @@. =@# =@# #@. @@ + =@# @# #@= #@ =#@@@@#= +#@@= +#@@@@#= .##@@+ @@ + @@@@ @@@@@@@@@@@@@@@@ +""" + + +def print_legacy_axolotl_text_art(suffix=None): font = "nancyj" ascii_text = " axolotl" if suffix: @@ -69,6 +83,13 @@ def print_axolotl_text_art(suffix=None): print_dep_versions() +def print_axolotl_text_art( + **kwargs, # pylint: disable=unused-argument +): + if is_main_process(): + print(AXOLOTL_LOGO) + + def print_dep_versions(): packages = ["accelerate", "peft", "transformers", "trl", "torch", "bitsandbytes"] max_len = max(len(pkg) for pkg in packages) From e73b8dff8d5fcfb02371916cbebc1350a3a1a9c9 Mon Sep 17 00:00:00 2001 From: Thomas Cleberg <84520378+thomascleberg@users.noreply.github.com> Date: Fri, 11 Oct 2024 12:32:50 -0500 Subject: [PATCH 0178/1405] Add Support for `revision` Dataset Parameter to specify reading from Huggingface Dataset Revision (#1912) * Add support for `revision` dataset parameter * only use revision on hf hub backed datasets * use revision tied to head * set download to use revision * feat: add config to model validator class * feat: add revision config to RL and tests for it --------- Co-authored-by: Wing Lian Co-authored-by: NanoCode012 --- docs/config.qmd | 1 + .../config/models/input/v0_4_1/__init__.py | 3 + src/axolotl/utils/data/rl.py | 1 + src/axolotl/utils/data/sft.py | 6 +- tests/test_datasets.py | 138 ++++++++++++++++++ 5 files changed, 148 insertions(+), 1 deletion(-) diff --git a/docs/config.qmd b/docs/config.qmd index 99a69a0973..8329f35535 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -90,6 +90,7 @@ datasets: shards: # Optional[int] number of shards to split data into name: # Optional[str] name of dataset configuration to load train_on_split: train # Optional[str] name of dataset split to load from + revision: # Optional[str] The specific revision of the dataset to use when loading from the Hugging Face Hub. This can be a commit hash, tag, or branch name. If not specified, the latest version will be used. This parameter is ignored for local datasets. # Optional[str] fastchat conversation type, only used with type: sharegpt conversation: # Options (see Conversation 'name'): https://github.com/lm-sys/FastChat/blob/main/fastchat/conversation.py diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 47796add6b..1c33b59078 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -125,6 +125,7 @@ class SFTDataset(BaseModel): drop_system_message: Optional[bool] = None trust_remote_code: Optional[bool] = False + revision: Optional[str] = None class UserDefinedDPOType(BaseModel): @@ -146,6 +147,7 @@ class DPODataset(BaseModel): split: Optional[str] = None type: Optional[Union[UserDefinedDPOType, str]] = None data_files: Optional[List[str]] = None + revision: Optional[str] = None class UserDefinedKTOType(BaseModel): @@ -167,6 +169,7 @@ class KTODataset(BaseModel): type: Optional[Union[UserDefinedKTOType, str]] = None data_files: Optional[List[str]] = None trust_remote_code: Optional[bool] = False + revision: Optional[str] = None class RLType(str, Enum): diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index d0324e1ebd..35bd5fcbb7 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -90,6 +90,7 @@ def load_split(dataset_cfgs, _cfg): ds = load_dataset( # pylint: disable=invalid-name ds_cfg["path"], split=ds_cfg["split"], + revision=ds_cfg.get("revision", None), ) split_datasets.insert(i, ds) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 7d6922cbf2..39eb2c4e04 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -242,6 +242,7 @@ def for_d_in_datasets(dataset_configs): name=config_dataset.name, streaming=True, token=use_auth_token, + revision=config_dataset.revision, ) ds_from_hub = True except (FileNotFoundError, ConnectionError, HFValidationError, ValueError): @@ -346,6 +347,7 @@ def for_d_in_datasets(dataset_configs): streaming=False, data_files=config_dataset.data_files, token=use_auth_token, + revision=config_dataset.revision, **load_ds_kwargs, ) elif ds_from_cloud and remote_file_system: @@ -380,6 +382,7 @@ def for_d_in_datasets(dataset_configs): repo_id=config_dataset.path, repo_type="dataset", filename=config_dataset.data_files, + revision=config_dataset.revision, ) elif isinstance(config_dataset.data_files, list): fp = [] @@ -389,6 +392,7 @@ def for_d_in_datasets(dataset_configs): repo_id=config_dataset.path, repo_type="dataset", filename=file, + revision=config_dataset.revision, ) ) else: @@ -433,8 +437,8 @@ def for_d_in_datasets(dataset_configs): config_dataset=config_dataset, tokenizer=tokenizer, cfg=cfg, - dataset=ds, d_base_type=d_base_type, + dataset=ds, d_prompt_style=d_prompt_style, processor=processor, ) diff --git a/tests/test_datasets.py b/tests/test_datasets.py index a274b7b894..f8b463a03e 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -12,6 +12,7 @@ from transformers import AutoTokenizer from axolotl.utils.data import load_tokenized_prepared_datasets +from axolotl.utils.data.rl import load_prepare_dpo_datasets from axolotl.utils.dict import DictDefault @@ -267,6 +268,143 @@ def test_load_from_single_json(self): assert "attention_mask" in dataset.features assert "labels" in dataset.features + def test_load_hub_with_dpo(self): + """Verify that processing dpo data from the hub works""" + + cfg = DictDefault( + { + "tokenizer_config": "huggyllama/llama-7b", + "sequence_len": 1024, + "rl": "dpo", + "chat_template": "llama3", + "datasets": [ + { + "path": "fozziethebeat/alpaca_messages_2k_dpo_test", + "type": "chat_template.default", + "chat_template": "llama3", + "field_messages": "conversation", + "field_chosen": "chosen", + "field_rejected": "rejected", + "message_field_role": "role", + "message_field_content": "content", + "roles": { + "system": ["system"], + "user": ["user"], + "assistant": ["assistant"], + }, + } + ], + } + ) + + train_dataset, _ = load_prepare_dpo_datasets(cfg) + + assert len(train_dataset) == 1800 + assert "conversation" in train_dataset.features + + def test_load_hub_with_revision(self): + """Verify that processing data from the hub works with a specific revision""" + with tempfile.TemporaryDirectory() as tmp_dir: + prepared_path = Path(tmp_dir) / "prepared" + cfg = DictDefault( + { + "tokenizer_config": "huggyllama/llama-7b", + "sequence_len": 1024, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + "revision": "d05c1cb", + }, + ], + } + ) + + dataset, _ = load_tokenized_prepared_datasets( + self.tokenizer, cfg, prepared_path + ) + + assert len(dataset) == 2000 + assert "input_ids" in dataset.features + assert "attention_mask" in dataset.features + assert "labels" in dataset.features + + def test_load_hub_with_revision_with_dpo(self): + """Verify that processing dpo data from the hub works with a specific revision""" + + cfg = DictDefault( + { + "tokenizer_config": "huggyllama/llama-7b", + "sequence_len": 1024, + "rl": "dpo", + "chat_template": "llama3", + "datasets": [ + { + "path": "fozziethebeat/alpaca_messages_2k_dpo_test", + "type": "chat_template.default", + "chat_template": "llama3", + "revision": "ea82cff", + "field_messages": "conversation", + "field_chosen": "chosen", + "field_rejected": "rejected", + "message_field_role": "role", + "message_field_content": "content", + "roles": { + "system": ["system"], + "user": ["user"], + "assistant": ["assistant"], + }, + } + ], + } + ) + + train_dataset, _ = load_prepare_dpo_datasets(cfg) + + assert len(train_dataset) == 1800 + assert "conversation" in train_dataset.features + + def test_load_local_hub_with_revision(self): + """Verify that a local copy of a hub dataset can be loaded with a specific revision""" + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_ds_path = Path("mhenrichsen/alpaca_2k_test") + tmp_ds_path.mkdir(parents=True, exist_ok=True) + snapshot_download( + repo_id="mhenrichsen/alpaca_2k_test", + repo_type="dataset", + local_dir=tmp_ds_path, + revision="d05c1cb", + ) + + prepared_path = Path(tmp_dir) / "prepared" + cfg = DictDefault( + { + "tokenizer_config": "huggyllama/llama-7b", + "sequence_len": 1024, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "ds_type": "parquet", + "type": "alpaca", + "data_files": [ + "mhenrichsen/alpaca_2k_test/alpaca_2000.parquet", + ], + "revision": "d05c1cb", + }, + ], + } + ) + + dataset, _ = load_tokenized_prepared_datasets( + self.tokenizer, cfg, prepared_path + ) + + assert len(dataset) == 2000 + assert "input_ids" in dataset.features + assert "attention_mask" in dataset.features + assert "labels" in dataset.features + shutil.rmtree(tmp_ds_path) + if __name__ == "__main__": unittest.main() From 922db77521f37d32ba7a5ab72b56904fed3bcb5c Mon Sep 17 00:00:00 2001 From: Adam Hazell <34248583+awhazell@users.noreply.github.com> Date: Fri, 11 Oct 2024 18:33:06 +0100 Subject: [PATCH 0179/1405] Add MLFlow run name option in config (#1961) Co-authored-by: Adam Hazell --- docs/config.qmd | 1 + src/axolotl/core/trainer_builder.py | 9 ++++++--- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index 8329f35535..b6c0cb852a 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -266,6 +266,7 @@ wandb_log_model: # "checkpoint" to log model to wandb Artifacts every `save_step # mlflow configuration if you're using it mlflow_tracking_uri: # URI to mlflow mlflow_experiment_name: # Your experiment name +mlflow_run_name: # Your run name hf_mlflow_log_artifacts: # set to true to copy each saved checkpoint on each save to mlflow artifact registry # Comet configuration if you're using it diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index b1ee519dc4..9c12b6141a 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1445,9 +1445,12 @@ def build(self, total_num_steps): report_to.append("comet_ml") training_arguments_kwargs["report_to"] = report_to - training_arguments_kwargs["run_name"] = ( - self.cfg.wandb_name if self.cfg.use_wandb else None - ) + if self.cfg.use_wandb: + training_arguments_kwargs["run_name"] = self.cfg.wandb_name + elif self.cfg.use_mlflow: + training_arguments_kwargs["run_name"] = self.cfg.mlflow_run_name + else: + training_arguments_kwargs["run_name"] = None training_arguments_kwargs["optim"] = ( self.cfg.optimizer if self.cfg.optimizer else "adamw_hf" ) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 1c33b59078..1a269b7982 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -447,6 +447,7 @@ class MLFlowConfig(BaseModel): use_mlflow: Optional[bool] = None mlflow_tracking_uri: Optional[str] = None mlflow_experiment_name: Optional[str] = None + mlflow_run_name: Optional[str] = None hf_mlflow_log_artifacts: Optional[bool] = None From 76883851d233d3734c19b1979ede7020059ea37d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 11 Oct 2024 13:33:20 -0400 Subject: [PATCH 0180/1405] add warning that sharegpt will be deprecated (#1957) * add warning that sharegpt will be deprecated * add helper script for chat_templates and document deprecation * Update src/axolotl/prompt_strategies/sharegpt.py Co-authored-by: NanoCode012 --------- Co-authored-by: NanoCode012 --- README.md | 2 +- scripts/chat_datasets.py | 60 +++++++++++++++++++++++ src/axolotl/prompt_strategies/sharegpt.py | 3 ++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 scripts/chat_datasets.py diff --git a/README.md b/README.md index f6f4e4e806..4ce7a351bb 100644 --- a/README.md +++ b/README.md @@ -383,7 +383,7 @@ See [examples](examples) for quick start. It is recommended to duplicate and mod - typescript type: ... # unimplemented custom format - # fastchat conversation + # fastchat conversation (deprecation soon, use chat_template) # See 'conversation' options: https://github.com/lm-sys/FastChat/blob/main/fastchat/conversation.py - path: ... type: sharegpt diff --git a/scripts/chat_datasets.py b/scripts/chat_datasets.py new file mode 100644 index 0000000000..5eb5bde1e2 --- /dev/null +++ b/scripts/chat_datasets.py @@ -0,0 +1,60 @@ +""" +helper script to parse chat datasets into a usable yaml +""" +import click +import yaml +from datasets import load_dataset + + +@click.command() +@click.argument("dataset", type=str) +@click.option("--split", type=str, default="train") +def parse_dataset(dataset=None, split="train"): + ds_cfg = {} + ds_cfg["path"] = dataset + ds_cfg["split"] = split + ds_cfg["type"] = "chat_template" + ds_cfg["chat_template"] = "<<>>" + + dataset = load_dataset(dataset, split=split) + features = dataset.features + feature_keys = features.keys() + field_messages = None + for key in ["conversation", "conversations", "messages"]: + if key in feature_keys: + field_messages = key + break + if not field_messages: + raise ValueError( + f'No conversation field found in dataset: {", ".join(feature_keys)}' + ) + ds_cfg["field_messages"] = field_messages + + message_fields = features["conversations"][0].keys() + message_field_role = None + for key in ["from", "role"]: + if key in message_fields: + message_field_role = key + break + if not message_field_role: + raise ValueError( + f'No role field found in messages: {", ".join(message_fields)}' + ) + ds_cfg["message_field_role"] = message_field_role + + message_field_content = None + for key in ["content", "text", "value"]: + if key in message_fields: + message_field_content = key + break + if not message_field_content: + raise ValueError( + f'No content field found in messages: {", ".join(message_fields)}' + ) + ds_cfg["message_field_content"] = message_field_content + + print(yaml.dump({"datasets": [ds_cfg]})) + + +if __name__ == "__main__": + parse_dataset() diff --git a/src/axolotl/prompt_strategies/sharegpt.py b/src/axolotl/prompt_strategies/sharegpt.py index 321f19554b..4565c35d5d 100644 --- a/src/axolotl/prompt_strategies/sharegpt.py +++ b/src/axolotl/prompt_strategies/sharegpt.py @@ -61,6 +61,9 @@ def build_loader( default_conversation: Optional[str] = None, ): def _load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): + LOG.warning( + "sharegpt type support will be deprecated in the next release of Axolotl. Please use chat_template instead.", + ) conversation = ( ds_cfg["conversation"] if ds_cfg and "conversation" in ds_cfg From df359c8a6e14ecdd2e1eb0049bd8143c32421952 Mon Sep 17 00:00:00 2001 From: Afrizal Hasbi Azizy Date: Sat, 12 Oct 2024 00:34:13 +0700 Subject: [PATCH 0181/1405] Handle image input as string paths for MMLMs (#1958) * Update mm_chat.py Handle string image (paths) * chore: lint --------- Co-authored-by: Wing Lian --- src/axolotl/utils/collators/mm_chat.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/collators/mm_chat.py b/src/axolotl/utils/collators/mm_chat.py index f49e97f37f..b9b67f8750 100644 --- a/src/axolotl/utils/collators/mm_chat.py +++ b/src/axolotl/utils/collators/mm_chat.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from typing import Any, Dict, List, Optional, Union +from PIL import Image from transformers import PreTrainedTokenizerBase, ProcessorMixin from transformers.data.data_collator import DataCollatorMixin from transformers.utils import PaddingStrategy @@ -52,7 +53,12 @@ def process_rows(examples, processor, chat_template, max_images, length_only=Fal ) for example in examples ] - images = [example["images"] for example in examples] + images = [ + Image.open(example["images"]) + if isinstance(example["images"], str) + else example["images"] + for example in examples + ] if max_images > 0: images = [img_batch[:max_images] for img_batch in images] From 09bf1ceacc67b46d6bc5abb8cef2b47c9dd84b8c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 12 Oct 2024 18:19:48 -0400 Subject: [PATCH 0182/1405] update hf deps (#1964) * update hf deps * remove deprecated set_caching_enabled --- requirements.txt | 12 ++++++------ src/axolotl/utils/trainer.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4323c76ce1..2dd3517a7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 -peft==0.13.0 -transformers==4.45.1 -tokenizers>=0.19.1 -bitsandbytes==0.44.0 -accelerate==0.34.2 -datasets==2.21.0 +peft==0.13.2 +transformers==4.45.2 +tokenizers>=0.20.1 +bitsandbytes==0.44.1 +accelerate==1.0.0 +datasets==3.0.1 deepspeed==0.14.4 pydantic==2.6.3 addict diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 17276dd8ed..30b40925f9 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -11,7 +11,7 @@ import torch import torch.cuda from accelerate.logging import get_logger -from datasets import set_caching_enabled +from datasets import disable_caching, enable_caching from torch.utils.data import DataLoader, RandomSampler from transformers.utils import is_torch_bf16_gpu_available @@ -87,10 +87,10 @@ def trainer_weighted_loss(model_output, labels, shift_labels=True): @contextmanager def disable_datasets_caching(): try: - set_caching_enabled(False) + disable_caching() yield finally: - set_caching_enabled(True) + enable_caching() def add_position_ids(sample): From d20b48a61e8dff5565303166fde5303c811e5491 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 12 Oct 2024 20:53:48 -0400 Subject: [PATCH 0183/1405] only install torchao for torch versions >= 2.4.0 (#1963) --- requirements.txt | 2 ++ setup.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2dd3517a7a..37ee1e42cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -52,3 +52,5 @@ lm_eval==0.4.4 langdetect==1.0.9 immutabledict==4.2.0 antlr4-python3-runtime==4.13.2 + +torchao==0.5.0 diff --git a/setup.py b/setup.py index e939bc37ee..7d9568dbff 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ def parse_requirements(): try: xformers_version = [req for req in _install_requires if "xformers" in req][0] + torchao_version = [req for req in _install_requires if "torchao" in req][0] if "Darwin" in platform.system(): # don't install xformers on MacOS _install_requires.pop(_install_requires.index(xformers_version)) @@ -53,7 +54,8 @@ def parse_requirements(): if patch == 0: _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.27") - if (major, minor) >= (2, 3): + elif (major, minor) >= (2, 3): + _install_requires.pop(_install_requires.index(torchao_version)) if patch == 0: _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.26.post1") @@ -61,9 +63,11 @@ def parse_requirements(): _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.27") elif (major, minor) >= (2, 2): + _install_requires.pop(_install_requires.index(torchao_version)) _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.25.post1") else: + _install_requires.pop(_install_requires.index(torchao_version)) _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.23.post1") From 31591bd94cf8fd3e18fc8949385cf405b1ff0dda Mon Sep 17 00:00:00 2001 From: pandora <128635000+pandora-s-git@users.noreply.github.com> Date: Sun, 13 Oct 2024 03:40:39 +0200 Subject: [PATCH 0184/1405] Fixing Validation - Mistral Templates (#1962) --- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 1a269b7982..af1570db63 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -187,7 +187,9 @@ class ChatTemplate(str, Enum): alpaca = "alpaca" # pylint: disable=invalid-name chatml = "chatml" # pylint: disable=invalid-name - inst = "inst" # pylint: disable=invalid-name + mistral_v1 = "mistral_v1" # pylint: disable=invalid-name + mistral_v2v3 = "mistral_v2v3" # pylint: disable=invalid-name + mistral_v3_tekken = "mistral_v3_tekken" # pylint: disable=invalid-name gemma = "gemma" # pylint: disable=invalid-name cohere = "cohere" # pylint: disable=invalid-name llama3 = "llama3" # pylint: disable=invalid-name From ac128b7b1dde6e6f0ca9a06697cad6fa31c9d5b0 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sun, 13 Oct 2024 08:41:13 +0700 Subject: [PATCH 0185/1405] fix: update eval causal lm metrics to add perplexity (#1951) [skip ci] --- docs/config.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.qmd b/docs/config.qmd index b6c0cb852a..703d587753 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -315,7 +315,7 @@ max_steps: eval_table_size: # Approximate number of predictions sent to wandb depending on batch size. Enabled above 0. Default is 0 eval_max_new_tokens: # Total number of tokens generated for predictions sent to wandb. Default is 128 -eval_causal_lm_metrics: # HF evaluate metrics used during evaluation. Default is ["sacrebleu", "comet", "ter", chrf] +eval_causal_lm_metrics: # HF evaluate metrics used during evaluation. Default is ["sacrebleu", "comet", "ter", "chrf", "perplexity"] loss_watchdog_threshold: # High loss value, indicating the learning has broken down (a good estimate is ~2 times the loss at the start of training) loss_watchdog_patience: # Number of high-loss steps in a row before the trainer aborts (default: 3) From 1834cdc3645c003e3db02346912cab19a1eb5ca3 Mon Sep 17 00:00:00 2001 From: Vincent Haines Date: Sat, 12 Oct 2024 21:41:43 -0400 Subject: [PATCH 0186/1405] Add support for qwen 2.5 chat template (#1934) --- src/axolotl/utils/chat_templates.py | 1 + src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 9e1e6ca326..2443f56f93 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -17,6 +17,7 @@ "phi_35": "{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'user' %}{{'<|user|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|>\n' + message['content'] + '<|end|>\n'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% else %}{{ eos_token }}{% endif %}", "deepseek_v2": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %}", "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', + "qwen_25": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n", } diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index af1570db63..40f4a36abb 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -198,6 +198,7 @@ class ChatTemplate(str, Enum): phi_35 = "phi_35" # pylint: disable=invalid-name deepseek_v2 = "deepseek_v2" # pylint: disable=invalid-name jamba = "jamba" # pylint: disable=invalid-name + qwen_25 = "qwen_25" # pylint: disable=invalid-name class LoftQConfig(BaseModel): From cd2d89f4672e90af45c6a632c75a624b6219c712 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 13 Oct 2024 12:15:18 -0400 Subject: [PATCH 0187/1405] wip add new proposed message structure (#1904) * wip add new proposed message structure * tokenization * wip * wip transform builder * wip make the chat dataset loadable * wip chatml + llama 3 new chat objects * chore: lint * chore: lint * fix tokenization * remove dacite dependency since we're using pydantic now * fix handling when already correctly split in messages * make sure to remove chat features from tokenized ds * move chat to be a input transform for messages * make sure llama3 has the bos token * remove non-working special token code * fix messages strat loader --- requirements_env.txt | 315 ++++++++++++++++++ src/axolotl/cli/preprocess.py | 10 +- src/axolotl/core/chat/__init__.py | 0 src/axolotl/core/chat/format/__init__.py | 0 src/axolotl/core/chat/format/chatml.py | 34 ++ src/axolotl/core/chat/format/llama3x.py | 45 +++ src/axolotl/core/chat/format/shared.py | 47 +++ src/axolotl/core/chat/messages.py | 230 +++++++++++++ src/axolotl/core/datasets/__init__.py | 0 src/axolotl/core/datasets/chat.py | 55 +++ .../core/datasets/transforms/__init__.py | 0 .../core/datasets/transforms/chat_builder.py | 150 +++++++++ src/axolotl/prompt_strategies/__init__.py | 7 +- .../prompt_strategies/messages/__init__.py | 34 ++ .../prompt_strategies/messages/chat.py | 84 +++++ src/axolotl/prompt_tokenizers.py | 6 + .../config/models/input/v0_4_1/__init__.py | 2 + src/axolotl/utils/data/sft.py | 22 +- tests/core/chat/__init__.py | 0 tests/core/chat/format/__init__.py | 0 tests/core/chat/test_messages.py | 197 +++++++++++ tests/prompt_strategies/messages/__init__.py | 0 tests/prompt_strategies/messages/test_chat.py | 62 ++++ 23 files changed, 1285 insertions(+), 15 deletions(-) create mode 100644 requirements_env.txt create mode 100644 src/axolotl/core/chat/__init__.py create mode 100644 src/axolotl/core/chat/format/__init__.py create mode 100644 src/axolotl/core/chat/format/chatml.py create mode 100644 src/axolotl/core/chat/format/llama3x.py create mode 100644 src/axolotl/core/chat/format/shared.py create mode 100644 src/axolotl/core/chat/messages.py create mode 100644 src/axolotl/core/datasets/__init__.py create mode 100644 src/axolotl/core/datasets/chat.py create mode 100644 src/axolotl/core/datasets/transforms/__init__.py create mode 100644 src/axolotl/core/datasets/transforms/chat_builder.py create mode 100644 src/axolotl/prompt_strategies/messages/__init__.py create mode 100644 src/axolotl/prompt_strategies/messages/chat.py create mode 100644 tests/core/chat/__init__.py create mode 100644 tests/core/chat/format/__init__.py create mode 100644 tests/core/chat/test_messages.py create mode 100644 tests/prompt_strategies/messages/__init__.py create mode 100644 tests/prompt_strategies/messages/test_chat.py diff --git a/requirements_env.txt b/requirements_env.txt new file mode 100644 index 0000000000..f8acbf73c2 --- /dev/null +++ b/requirements_env.txt @@ -0,0 +1,315 @@ +accelerate==0.34.1 +addict==2.4.0 +aiofiles==23.2.1 +aiohttp==3.9.0 +aiosignal==1.3.1 +aiostream==0.5.2 +alembic==1.13.1 +annotated-types==0.6.0 +annoy==1.17.3 +ansible==6.7.0 +ansible-core==2.13.13 +ansible-vault==2.1.0 +anyio==3.7.1 +appdirs==1.4.4 +art==6.0 +asgiref==3.7.2 +async-timeout==4.0.2 +attrdict==2.0.1 +attrs==22.2.0 +awscli==1.32.75 +-e git+ssh://git@github.com/OpenAccess-AI-Collective/axolotl.git@6e354682e3c1735d3f7fb9e362280c38e922260f#egg=axolotl +backoff==2.2.1 +base58==2.1.1 +beartype==0.17.2 +bitnet==0.2.1 +bitsandbytes==0.42.0 +bittensor==6.7.0 +black==23.7.0 +blinker==1.7.0 +boto3==1.34.75 +botocore==1.34.75 +cachetools==5.3.3 +cachy==0.1.1 +certifi==2023.7.22 +cffi==1.16.0 +cfgv==3.3.1 +chai-guanaco==1.2.4 +charset-normalizer==3.2.0 +cleo==0.6.8 +click==8.1.7 +cloudpickle==2.0.0 +cohere==4.11.2 +colorama==0.4.4 +coloredlogs==15.0.1 +CoLT5-attention==0.10.20 +contextlib2==21.6.0 +contourpy==1.2.0 +cryptography==41.0.3 +cycler==0.12.1 +cytoolz==0.12.3 +databricks-cli==0.18.0 +dataclasses-json==0.5.7 +datasets==2.11.0 +ddt==1.6.0 +decorator==5.1.1 +deepspeed==0.15.0 +# Editable Git install with no remote (dialogpt==0.1) +-e /Users/wing/Projects/ml/dialogpt/src +dill==0.3.6 +distlib==0.3.6 +docker==7.0.0 +docker-pycreds==0.4.0 +docstring-parser==0.15 +docutils==0.16 +ecdsa==0.18.0 +einops==0.7.0 +einops-exts==0.0.4 +einx==0.1.3 +entrypoints==0.4 +eth-hash==0.6.0 +eth-keys==0.5.0 +eth-typing==4.0.0 +eth-utils==2.3.1 +evaluate==0.4.0 +exceptiongroup==1.1.1 +fastapi==0.109.2 +fastcore==1.5.29 +ffmpy==0.4.0 +filelock==3.12.2 +-e git+https://github.com/NousResearch/finetuning-subnet.git@24e9407d6b4430a7ca39d344692f89ce5a97d27e#egg=finetuning_subnet +fire==0.5.0 +first==2.0.2 +flake8==7.0.0 +Flask==3.0.1 +fonttools==4.47.2 +frozendict==2.4.1 +frozenlist==1.3.3 +fschat @ git+https://github.com/lm-sys/FastChat.git@27a05b04a35510afb1d767ae7e5990cbd278f8fe +fsspec==2023.6.0 +fuzzywuzzy==0.18.0 +gitdb==4.0.10 +GitPython==3.1.31 +google-pasta==0.2.0 +gradio==4.42.0 +gradio_client==1.3.0 +greenlet==2.0.2 +grpclib==0.4.7 +gunicorn==21.2.0 +h11==0.14.0 +h2==4.1.0 +hpack==4.0.0 +httpcore==0.17.3 +httpx==0.24.1 +huggingface-hub==0.23.4 +humanfriendly==10.0 +hyperframe==6.0.1 +identify==2.5.24 +idna==3.4 +immutables==0.20 +importlib-metadata==6.7.0 +importlib-resources==6.1.1 +inflection==0.5.1 +iniconfig==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.2 +jmespath==1.0.1 +joblib==1.3.2 +jsonlines==3.1.0 +jsonschema==2.6.0 +kiwisolver==1.4.5 +langchain==0.0.144 +Levenshtein==0.24.0 +libcst==1.1.0 +liger-kernel==0.0.0 +lion-pytorch==0.1.2 +llama-cpp-python==0.1.36 +llvmlite==0.40.1 +local-attention==1.9.0 +loguru==0.7.0 +Mako==1.3.2 +Markdown==3.5.2 +markdown-it-py==3.0.0 +markdown2==2.4.10 +MarkupSafe==2.1.2 +marshmallow==3.19.0 +marshmallow-enum==1.5.1 +matplotlib==3.8.2 +mccabe==0.7.0 +mdurl==0.1.2 +MEGABYTE-pytorch==0.0.7 +-e git+https://github.com/cg123/mergekit.git@53c5f414774a0558b8d84858fb6374bc93a8f1c1#egg=mergekit +mlflow==2.10.0 +modal==0.62.77 +more-itertools==10.2.0 +mpmath==1.2.1 +msgpack==1.0.7 +msgpack-numpy-opentensor==0.5.0 +multidict==6.0.4 +multiprocess==0.70.14 +munch==2.5.0 +mypy==1.3.0 +mypy-extensions==1.0.0 +nest-asyncio==1.6.0 +netaddr==0.10.1 +networkx==3.0rc1 +nh3==0.2.14 +nodeenv==1.8.0 +nomic==2.0.2 +numba==0.57.1 +numexpr==2.8.4 +numpy==1.24.4 +oauthlib==3.2.2 +openai==0.27.4 +openapi==1.1.0 +openapi-schema-pydantic==1.2.4 +optimum==1.8.6 +orjson==3.10.7 +packaging==23.1 +pandas==2.0.0 +parameterized==0.9.0 +password-strength==0.0.3.post2 +pastel==0.1.1 +pathos==0.3.0 +pathspec==0.11.1 +pathtools==0.1.2 +peft==0.11.1 +pendulum==3.0.0 +Pillow==9.5.0 +pip-tools==1.11.0 +platformdirs==3.2.0 +pluggy==1.4.0 +poetry==0.7.1 +pox==0.3.2 +ppft==1.7.6.6 +pre-commit==3.3.2 +prettytable==3.10.0 +prompt-toolkit==3.0.39 +protobuf==3.20.2 +protobuf3-to-dict==0.1.5 +psutil==5.9.5 +psycopg==3.1.18 +PuLP==2.8.0 +py==1.11.0 +py-bip39-bindings==0.1.11 +py-cpuinfo==9.0.0 +py-ed25519-zebra-bindings==1.0.1 +py-sr25519-bindings==0.2.0 +pyarrow==11.0.0 +pyasn1==0.6.0 +pycodestyle==2.11.1 +pycparser==2.21 +pycryptodome==3.20.0 +pydantic==2.5.3 +pydantic_core==2.14.6 +pydub==0.25.1 +pyfiglet==0.8.post1 +pyflakes==3.2.0 +Pygments==2.15.1 +PyJWT==2.8.0 +pylev==1.4.0 +PyNaCl==1.5.0 +pynvml==11.5.0 +pyparsing==2.4.7 +pyrsistent==0.14.11 +pytest==8.0.2 +pytest-asyncio==0.23.4 +python-dateutil==2.8.2 +python-dotenv==1.0.1 +python-Levenshtein==0.24.0 +python-multipart==0.0.9 +pytz==2023.3 +PyYAML==6.0.1 +querystring-parser==1.2.4 +rapidfuzz==3.6.1 +regex==2023.6.3 +requests==2.31.0 +requests-toolbelt==0.8.0 +resolvelib==0.8.1 +responses==0.18.0 +retry==0.9.2 +rich==13.7.0 +rsa==4.7.2 +ruff==0.6.3 +s3transfer==0.10.1 +safetensors==0.4.5 +sagemaker==2.148.0 +scalecodec==1.2.7 +schedulefree==1.2.1 +schema==0.7.5 +scikit-learn==1.4.0 +scipy==1.9.3 +seaborn==0.13.2 +semantic-version==2.10.0 +sentencepiece==0.2.0 +sentry-sdk==1.19.1 +setproctitle==1.3.2 +shellingham==1.5.4 +shortuuid==1.0.11 +shtab==1.6.5 +sigtools==4.0.1 +six==1.16.0 +skypilot==0.4.1 +smdebug-rulesconfig==1.0.1 +smmap==5.0.0 +sniffio==1.3.0 +SQLAlchemy==1.4.47 +sqlparse==0.4.4 +starlette==0.36.3 +substrate-interface==1.5.2 +svgwrite==1.4.3 +sympy==1.11.1 +synchronicity==0.6.7 +tabulate==0.9.0 +tblib==1.7.0 +tenacity==8.2.2 +tensor-parallel==2.0.0 +termcolor==2.2.0 +text2art==0.2.0 +threadpoolctl==3.2.0 +tiktoken==0.6.0 +time-machine==2.14.1 +timm==0.9.16 +tokenizers==0.19.1 +tokenmonster==1.1.12 +toml==0.9.6 +tomli==2.0.1 +tomlkit==0.12.0 +toolz==0.12.1 +torch==2.2.0 +torchdata==0.6.1 +torchdiffeq==0.2.3 +TorchFix==0.4.0 +torchtext==0.15.2 +torchvision==0.17.0 +tqdm==4.66.2 +transformers==4.44.2 +trl==0.9.6 +typer==0.12.5 +types-certifi==2021.10.8.3 +types-requests==2.31.0.20240125 +types-setuptools==69.0.0.20240125 +types-toml==0.10.8.7 +typing==3.7.4.3 +typing-inspect==0.8.0 +typing_extensions==4.9.0 +tyro==0.5.18 +tzdata==2023.3 +unique-names-generator==1.0.2 +urllib3==2.2.2 +uvicorn==0.22.0 +vector_quantize_pytorch==1.14.1 +virtualenv==20.23.0 +voyager==2.0.2 +wandb==0.16.2 +watchfiles==0.21.0 +wavedrom==2.0.3.post3 +wcwidth==0.2.6 +websocket-client==1.7.0 +websockets==12.0 +Werkzeug==3.0.1 +wonderwords==2.2.0 +xxhash==3.2.0 +yarl==1.8.2 +zetascale==2.2.7 +zipp==3.15.0 diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index e12462c000..aab29e2670 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -27,6 +27,7 @@ register_chatml_template, register_llama3_template, ) +from axolotl.utils.trainer import disable_datasets_caching LOG = logging.getLogger("axolotl.cli.preprocess") @@ -70,10 +71,11 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): LOG.warning(msg) parsed_cfg.dataset_prepared_path = DEFAULT_DATASET_PREPARED_PATH - if parsed_cfg.rl: # and parsed_cfg.rl != "orpo": - load_rl_datasets(cfg=parsed_cfg, cli_args=parsed_cli_args) - else: - load_datasets(cfg=parsed_cfg, cli_args=parsed_cli_args) + with disable_datasets_caching(): + if parsed_cfg.rl: # and parsed_cfg.rl != "orpo": + load_rl_datasets(cfg=parsed_cfg, cli_args=parsed_cli_args) + else: + load_datasets(cfg=parsed_cfg, cli_args=parsed_cli_args) if parsed_cli_args.download: model_name = parsed_cfg.base_model diff --git a/src/axolotl/core/chat/__init__.py b/src/axolotl/core/chat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/core/chat/format/__init__.py b/src/axolotl/core/chat/format/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/core/chat/format/chatml.py b/src/axolotl/core/chat/format/chatml.py new file mode 100644 index 0000000000..315d101a86 --- /dev/null +++ b/src/axolotl/core/chat/format/chatml.py @@ -0,0 +1,34 @@ +""" +ChatML transformation functions for MessageContents +""" +from typing import Optional + +from ..messages import MessageContents, Messages +from .shared import wrap_tools + + +def format_message( + message: Messages, + message_index: Optional[int] = None, # pylint: disable=unused-argument +) -> Messages: + if message.is_chat_formatted: + return message + + # prepend the role prefix within a MessageContents to message.content + message.content.insert( + 0, + MessageContents( + type="text", + value=f"<|im_start|>{message.role}\n", + weight=0, + ), + ) + message.content.append( + MessageContents(type="text", value="<|im_end|>", weight=message.weight) + ) + message.content.append(MessageContents(type="text", value="\n", weight=0)) + + message = wrap_tools(message) + + message.is_chat_formatted = True + return message diff --git a/src/axolotl/core/chat/format/llama3x.py b/src/axolotl/core/chat/format/llama3x.py new file mode 100644 index 0000000000..17fa7aa8d4 --- /dev/null +++ b/src/axolotl/core/chat/format/llama3x.py @@ -0,0 +1,45 @@ +""" +Llama 3.x chat formatting functions for MessageContents +""" +from typing import Optional + +from ..messages import MessageContents, Messages +from .shared import wrap_tools + + +def format_message(message: Messages, message_index: Optional[int] = None) -> Messages: + if message.is_chat_formatted: + return message + + message_role = message.role + if message.role == "tool": + message_role = "ipython" + + # prepend the role prefix within a MessageContents to message.content + message.content.insert( + 0, + MessageContents( + type="text", + value=f"<|start_header_id|>{message_role}<|end_header_id|>\n\n", + weight=0, + ), + ) + + message.content.append( + MessageContents(type="text", value="<|eot_id|>", weight=message.weight) + ) + + message = wrap_tools(message) + + if message_index == 0: + message.content.insert( + 0, + MessageContents( + type="text", + value="<|begin_of_text|>", + weight=0, + ), + ) + + message.is_chat_formatted = True + return message diff --git a/src/axolotl/core/chat/format/shared.py b/src/axolotl/core/chat/format/shared.py new file mode 100644 index 0000000000..9efa2353db --- /dev/null +++ b/src/axolotl/core/chat/format/shared.py @@ -0,0 +1,47 @@ +""" +shared functions for format transforms +""" +from axolotl.core.chat.messages import MessageContents, Messages + + +def wrap_tools(message: Messages): + # loop over message.content by index to find tool calls, we need to wrap each with tags, + # so be wary of indexing issues when changing the list while iterating. + # iterate over the range in reverse order to avoid index shifting + for i in range(len(message.content) - 1, -1, -1): + if message.content[i].type == "tool_call": + # append a MessageContents text tag after + message.content.insert( + i + 1, + MessageContents( + type="text", value="\n", weight=message.weight + ), + ) + # make sure the actual tool call content ends with a newline + message.content[i].has_newline = True + # prepend a MessageContents text tag before + message.content.insert( + i, + MessageContents( + type="text", value="\n", weight=message.weight + ), + ) + elif message.content[i].type == "tool_response": + # append a MessageContents text tag after + message.content.insert( + i + 1, + MessageContents( + type="text", value="\n", weight=message.weight + ), + ) + # make sure the actual tool response content ends with a newline + message.content[i].has_newline = True + # prepend a MessageContents text tag before + message.content.insert( + i, + MessageContents( + type="text", value="\n", weight=message.weight + ), + ) + + return message diff --git a/src/axolotl/core/chat/messages.py b/src/axolotl/core/chat/messages.py new file mode 100644 index 0000000000..c879bf477b --- /dev/null +++ b/src/axolotl/core/chat/messages.py @@ -0,0 +1,230 @@ +""" +internal message representations of chat messages +""" +import json +from enum import Enum +from typing import Any, Callable, List, Optional, Union + +from pydantic import BaseModel +from transformers import PreTrainedTokenizer + + +class MessageRoles(str, Enum): + """ + Message roles for the system, user, assistant, and tools + """ + + system = "system" # pylint: disable=invalid-name + user = "user" # pylint: disable=invalid-name + assistant = "assistant" # pylint: disable=invalid-name + tool = "tool" # pylint: disable=invalid-name + ipython = ( # pylint: disable=invalid-name + # for responses from builtin tools + "ipython" + ) + + +class MessageContentTypes(str, Enum): + """ + Message content types for text, image, audio, tool calls, and tool responses + """ + + special_token = "special_token" # pylint: disable=invalid-name # nosec B105 + text = "text" # pylint: disable=invalid-name + image = "image" # pylint: disable=invalid-name + audio = "audio" # pylint: disable=invalid-name + tool_call = "tool_call" # pylint: disable=invalid-name # to differentiate regular responses from tool calls from the assistant + tool_response = "tool_response" # pylint: disable=invalid-name + + +class SpecialToken(str, Enum): + """ + Special tokens for beginning of string and end of string + """ + + bos_token = "bos_token" # pylint: disable=invalid-name # nosec B105 + eos_token = "eos_token" # pylint: disable=invalid-name # nosec B105 + + +class ToolCallFunction(BaseModel): + """ + Tool call function with name and arguments + """ + + name: str + arguments: dict[str, str] + + +class Tool(BaseModel): + """ + Tool with description, function, and parameters + """ + + description: str + function: ToolCallFunction + parameters: dict[str, str] # .properties + + +class ToolCallContents(BaseModel): + """ + Tool call contents with name, arguments, and optional id + """ + + name: str + arguments: dict[str, Union[str, int]] + id: Optional[str] = None # pylint: disable=invalid-name + + def __str__(self) -> str: + data = {"name": self.name, "arguments": self.arguments} + if self.id is not None: + data["id"] = self.id + return json.dumps(data) + + +class ToolResponseContents(BaseModel): + """ + Tool response contents with name, content, and optional id + """ + + name: str + content: Union[str, dict[str, Union[str, int, float]]] + id: Optional[str] = None # pylint: disable=invalid-name + + def __str__(self) -> str: + data = {"name": self.name, "content": self.content} + if self.id is not None: + data["id"] = self.id + return json.dumps(data) + + +class MessageContents(BaseModel): + """ + Message contents with type, value, metadata, weight, newline, and end of contents + """ + + type: Union[str, MessageContentTypes] + value: Union[str, ToolCallContents, ToolResponseContents, SpecialToken] + meta: Optional[dict[str, Any]] = None # support additional arbitrary metadata + weight: Optional[Union[int, float]] = None + has_newline: bool = False + eoc: bool = False # end of contents + + def __str__(self) -> str: + str_val = str(self.value) + if self.has_newline and not str_val.endswith("\n"): + str_val += "\n" + return str_val + + +class Messages(BaseModel): + """ + Messages with role, content, metadata, weight, and chat formatting + """ + + role: Union[MessageRoles, str] # allows for arbitrary roles + content: List["MessageContents"] + meta: Optional[dict[str, Any]] = None # support additional arbitrary metadata + weight: Optional[Union[int, float]] = None + is_chat_formatted: bool = False + + def __str__(self) -> str: + return "".join(str(c) for c in self.content) + + def tokenized( + self, tokenizer: PreTrainedTokenizer, ignore_index=-100 + ) -> dict[str, List[int]]: + # iterate over the contents, tokenizing the concatenated string values up to the current MessageContents + # returns a dictionary mapping w input_ids, attention_mask, and labels + input_ids: List[int] = [] + labels: List[int] = [] + pending_input_ids: List[int] = [] + pending_weight = self.weight + running_content = "" + for _, msg_content in enumerate(self.content): + # TODO also handle non-text content types + if msg_content.type in [ + MessageContentTypes.text.value, + MessageContentTypes.tool_call.value, + MessageContentTypes.tool_response.value, + ]: + running_content += str(msg_content) + tok_results = tokenizer(running_content, add_special_tokens=False) + tok_input_ids = tok_results["input_ids"] + if pending_input_ids: + new_pending_inputs = tok_input_ids[ + len(input_ids) : len(input_ids) + len(pending_input_ids) + ] + if new_pending_inputs != pending_input_ids: + # logging.warning("tokenization mismatch from concatenation.") + pending_input_ids = new_pending_inputs + input_ids.extend(pending_input_ids) + if pending_weight: + labels.extend(pending_input_ids) + else: + labels.extend([ignore_index] * len(pending_input_ids)) + pending_input_ids = tok_results["input_ids"][len(input_ids) :] + pending_weight = self.weight and msg_content.weight not in [0, 0.0] + input_ids.extend(pending_input_ids) + if pending_weight: + labels.extend(pending_input_ids) + else: + labels.extend([ignore_index] * len(pending_input_ids)) + attention_mask = [1] * len(input_ids) + return { + "input_ids": input_ids, + "attention_mask": attention_mask, + "labels": labels, + } + + +class Chats(BaseModel): + """ + top level data structure for chat conversations + """ + + conversation: List[Messages] + + def __str__(self) -> str: + return "".join(str(c) for c in self.conversation) + + def tokenized( + self, tokenizer: Callable[[str], dict[str, List[int]]], ignore_index=-100 + ) -> dict[str, List[int]]: + input_ids = [] + attention_mask = [] + labels = [] + for msg in self.conversation: + msg_results = msg.tokenized(tokenizer, ignore_index) + input_ids.extend(msg_results["input_ids"]) + attention_mask.extend(msg_results["attention_mask"]) + labels.extend(msg_results["labels"]) + return { + "input_ids": input_ids, + "attention_mask": attention_mask, + "labels": labels, + } + + +class ChatFormattedChats(Chats): + """ + Chat formatted chats with formatter and optional train on inputs + """ + + formatter: Callable # [[Union[dict, Chats]], Chats] + train_on_inputs: bool = False + + def model_post_init(self, __context): + for i, msg in enumerate(self.conversation): + self.conversation[i] = self.formatter(msg, message_index=i) + if self.train_on_inputs: + self.conversation[i].weight = 1 + + +class PreferenceChats(BaseModel): + """ + representation for preference data for chat + """ + + prompt: List[Messages] + chosen: Messages + rejected: Messages diff --git a/src/axolotl/core/datasets/__init__.py b/src/axolotl/core/datasets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/core/datasets/chat.py b/src/axolotl/core/datasets/chat.py new file mode 100644 index 0000000000..e74c247d2c --- /dev/null +++ b/src/axolotl/core/datasets/chat.py @@ -0,0 +1,55 @@ +""" +chat dataset module +""" +import os +from typing import Callable, Optional, Union + +from datasets import Dataset +from transformers import PreTrainedTokenizer + +from axolotl.core.chat.messages import ChatFormattedChats + + +class TokenizedChatDataset(Dataset): + """ + Tokenized chat dataset + """ + + def __init__( + self, + data: Dataset, + model_transform: Union[PreTrainedTokenizer, Callable], + *args, + message_transform: Optional[Callable] = None, + formatter=None, + process_count: Optional[int] = None, + keep_in_memory: Optional[bool] = False, + **kwargs, + ): + def map_fn(ex): + if message_transform is not None: + ex = message_transform(ex) + if formatter is not None: + ex = ChatFormattedChats( + formatter=formatter, + **ex, + ) + else: + ex = ChatFormattedChats( + **ex, + ) + return ex.tokenized(model_transform) + + process_or_cpu_count: int = ( + process_count or os.cpu_count() # type: ignore[assignment] + ) + num_proc = min(64, process_or_cpu_count) + features = data.features.keys() + tokenized_data = data.map( + map_fn, + num_proc=num_proc, + keep_in_memory=keep_in_memory, + remove_columns=features, + desc="Tokenizing Chats", + ) + super().__init__(tokenized_data.data, *args, **kwargs) diff --git a/src/axolotl/core/datasets/transforms/__init__.py b/src/axolotl/core/datasets/transforms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/core/datasets/transforms/chat_builder.py b/src/axolotl/core/datasets/transforms/chat_builder.py new file mode 100644 index 0000000000..98d5f171a7 --- /dev/null +++ b/src/axolotl/core/datasets/transforms/chat_builder.py @@ -0,0 +1,150 @@ +""" +This module contains a function that builds a transform that takes a row from the dataset and converts it to a Chat. +""" +from typing import Any, Mapping, Union + + +def chat_message_transform_builder( # pylint: disable=dangerous-default-value + train_on_inputs=False, + conversations_field: str = "conversations", + message_field_role: Union[str, list[str]] = ["role", "from"], # commonly "role" + message_field_content: Union[str, list[str]] = [ + "value", + "text", + "content", + ], # commonly "content" + message_field_training: Union[str, list[str]] = [ + "train", + "weight", + ], # commonly "weight" +): + """Builds a transform that takes a row from the dataset and converts it to a Chat + + Args: + train_on_inputs (bool, optional): + If True, the transform will train on the inputs. If False, the transform will train on the targets. + Defaults to False. + conversations_field (str, optional): + The field name of the conversations. Defaults to "conversations". + message_field_role (str | list[str], optional): + The field name of the role. Defaults to "role". + message_field_content (str | list[str], optional): + The field name of the message content. Defaults to "content". + message_field_training (str | list[str], optional): + The field name of the train/weight. Defaults to "weight". + + Returns: + Callable: + A function that takes a list of conversations and returns a list of messages. + """ + + message_field_role = ( + [message_field_role] + if isinstance(message_field_role, str) + else message_field_role + ) + message_field_content = ( + [message_field_content] + if isinstance(message_field_content, str) + else message_field_content + ) + message_weight_fields = ( + [message_field_training] + if isinstance(message_field_training, str) + else message_field_training + ) + + role_value_mappings = { + "system": "system", + "user": "user", + "human": "user", + "assistant": "assistant", + "gpt": "assistant", + "tool": "tool", + "ipython": "ipython", + } + if train_on_inputs: + role_default_weights_mappings = { + "system": 1, + "user": 1, + "assistant": 1, + "tool": 1, + "ipython": 1, + } + else: + role_default_weights_mappings = { + "system": 0, + "user": 0, + "assistant": 1, + "tool": 0, + "ipython": 0, + } + + def transform_builder(sample: Mapping[str, Any]): + if conversations_field not in sample: + raise ValueError(f"Field '{conversations_field}' not found in sample.") + # if none of the role fields are in the message, raise an error + if not any( + role in sample[conversations_field][0] for role in message_field_role + ): + raise ValueError("No role field found in message.") + role_field = next( + role + for role in message_field_role + if role in sample[conversations_field][0] + ) + if not any( + field in sample[conversations_field][0] for field in message_field_content + ): + raise ValueError("No message_content field found in message.") + message_content_field = next( + field + for field in message_field_content + if field in sample[conversations_field][0] + ) + if not any( + field in sample[conversations_field][0] for field in message_field_training + ): + message_weight_field = None + else: + message_weight_field = next( + field + for field in message_weight_fields + if field in sample[conversations_field][0] + ) + + messages = [] + for message in sample[conversations_field]: + role = role_value_mappings[message[role_field]] + weight = ( + int(message[message_weight_field]) + if message_weight_field + else role_default_weights_mappings[role] + ) + + # TODO if "tool_calls" in message[message_content_field]: then convert tool call to ToolCallContents + if isinstance(message[message_content_field], str): + messages.append( + { + "role": role, + "content": [ + { + "type": "text", + "value": message[message_content_field], + } + ], + "weight": weight, + } + ) + else: + messages.append( + { + "role": role, + "content": message[message_content_field], + "weight": weight, + } + ) + + return {"conversation": messages} + + return transform_builder diff --git a/src/axolotl/prompt_strategies/__init__.py b/src/axolotl/prompt_strategies/__init__.py index 66cd5deeb9..74da20c5e1 100644 --- a/src/axolotl/prompt_strategies/__init__.py +++ b/src/axolotl/prompt_strategies/__init__.py @@ -11,6 +11,10 @@ def load(strategy, tokenizer, cfg, ds_cfg, processor=None): try: + if strategy == "messages": + from .messages import load as messages_load + + return messages_load(tokenizer, cfg, ds_cfg, processor=processor) load_fn = "load" if strategy.split(".")[-1].startswith("load_"): load_fn = strategy.split(".")[-1] @@ -31,4 +35,5 @@ def load(strategy, tokenizer, cfg, ds_cfg, processor=None): return None except Exception as exc: # pylint: disable=broad-exception-caught LOG.error(f"Failed to load prompt strategy `{strategy}`: {str(exc)}") - return None + raise exc + return None diff --git a/src/axolotl/prompt_strategies/messages/__init__.py b/src/axolotl/prompt_strategies/messages/__init__.py new file mode 100644 index 0000000000..d014d93a6b --- /dev/null +++ b/src/axolotl/prompt_strategies/messages/__init__.py @@ -0,0 +1,34 @@ +"""Module to load message prompt strategies.""" + +import importlib +import inspect +import logging + +LOG = logging.getLogger("axolotl.prompt_strategies.messages") + + +def load(tokenizer, cfg, ds_cfg, processor=None): + try: + strategy = ds_cfg.get("input_transform", "chat") + # pylint: disable=duplicate-code + load_fn = "load" + if strategy.split(".")[-1].startswith("load_"): + load_fn = strategy.split(".")[-1] + strategy = ".".join(strategy.split(".")[:-1]) + mod = importlib.import_module( + f".{strategy}", "axolotl.prompt_strategies.messages" + ) + func = getattr(mod, load_fn) + load_kwargs = {} + sig = inspect.signature(func) + if "ds_cfg" in sig.parameters: + load_kwargs["ds_cfg"] = ds_cfg + if "processor" in sig.parameters: + load_kwargs["processor"] = processor + return func(tokenizer, cfg, **load_kwargs) + except ModuleNotFoundError: + return None + except Exception as exc: # pylint: disable=broad-exception-caught + LOG.error(f"Failed to load prompt strategy `{strategy}`: {str(exc)}") + raise exc + return None diff --git a/src/axolotl/prompt_strategies/messages/chat.py b/src/axolotl/prompt_strategies/messages/chat.py new file mode 100644 index 0000000000..35d7649026 --- /dev/null +++ b/src/axolotl/prompt_strategies/messages/chat.py @@ -0,0 +1,84 @@ +""" +Chat dataset wrapping strategy for new internal messages representations +""" +from typing import Any, Callable, Dict, Optional + +from axolotl.core.datasets.chat import TokenizedChatDataset +from axolotl.core.datasets.transforms.chat_builder import chat_message_transform_builder +from axolotl.prompt_tokenizers import DatasetWrappingStrategy + + +class ChatMessageDatasetWrappingStrategy(DatasetWrappingStrategy): + """ + Chat dataset wrapping strategy for new internal messages representations + """ + + def __init__( + self, + processor, + message_transform=None, + formatter=None, + **kwargs, # pylint: disable=unused-argument + ): + """ + :param processor: tokenizer or image processor + :param kwargs: + """ + self.processor = processor + self.dataset = None + self.message_transform = message_transform + self.formatter = formatter + + def wrap_dataset( + self, + dataset, + process_count: Optional[int] = None, + keep_in_memory: Optional[bool] = False, + **kwargs, # pylint: disable=unused-argument + ): + self.dataset = TokenizedChatDataset( + dataset, + message_transform=self.message_transform, + model_transform=self.processor, + formatter=self.formatter, + process_count=process_count, + keep_in_memory=keep_in_memory, + ) + return self.dataset + + +def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): + ds_cfg = ds_cfg or {} + + field_messages = ds_cfg.get("field_messages") + message_field_role = ds_cfg.get("message_field_role") + message_field_content = ds_cfg.get("message_field_content") + message_field_training = ds_cfg.get("message_field_training") + + builder_kwargs = {} + if field_messages: + builder_kwargs["conversations_field"] = field_messages + if message_field_role: + builder_kwargs["message_field_role"] = message_field_role + if message_field_content: + builder_kwargs["message_field_content"] = message_field_content + if message_field_training: + builder_kwargs["message_field_training"] = message_field_training + + chat_template = ds_cfg.get("chat_template", cfg.get("chat_template", "chatml")) + format_message = ( + lambda x: x # noqa E731 # pylint: disable=unnecessary-lambda-assignment + ) + if chat_template == "chatml": + from axolotl.core.chat.format.chatml import format_message # noqa F811 + if chat_template.startswith("llama3"): + from axolotl.core.chat.format.llama3x import format_message # noqa F811 + message_transform: Callable = chat_message_transform_builder( + train_on_inputs=ds_cfg.get("train_on_inputs", False), + **builder_kwargs, + ) + strategy = ChatMessageDatasetWrappingStrategy( + tokenizer, message_transform=message_transform, formatter=format_message + ) + + return strategy diff --git a/src/axolotl/prompt_tokenizers.py b/src/axolotl/prompt_tokenizers.py index 11dd084a85..51d497a23c 100644 --- a/src/axolotl/prompt_tokenizers.py +++ b/src/axolotl/prompt_tokenizers.py @@ -30,6 +30,12 @@ class InvalidDataException(Exception): """ +class DatasetWrappingStrategy(abc.ABC): + """ + Abstract class for wrapping datasets for Chat Messages + """ + + class PromptTokenizingStrategy(abc.ABC): """ Abstract class for tokenizing strategies diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 40f4a36abb..3304c62f28 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -102,10 +102,12 @@ class SFTDataset(BaseModel): path: Optional[str] = None split: Optional[str] = None type: Optional[Union[str, UserDefinedPrompterType]] = None + input_transform: Optional[str] = None shards: Optional[int] = None conversation: Optional[str] = None chat_template: Optional[str] = None data_files: Optional[Union[str, List[str]]] = None + input_format: Optional[str] = None name: Optional[str] = None ds_type: Optional[str] = None train_on_split: Optional[str] = None diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 39eb2c4e04..163059c2b8 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -23,6 +23,7 @@ AlpacaMultipleChoicePromptTokenizingStrategy, AlpacaPromptTokenizingStrategy, AlpacaReflectionPTStrategy, + DatasetWrappingStrategy, GPTeacherPromptTokenizingStrategy, JeopardyPromptTokenizingStrategy, OpenAssistantPromptTokenizingStrategy, @@ -573,7 +574,7 @@ def get_dataset_wrapper( d_base_type, dataset, d_prompt_style=None, - processor=None, + processor=None, # pylint: disable=unused-argument ): dataset_wrapper = None dataset_prompter = None @@ -608,15 +609,16 @@ def get_dataset_wrapper( ) elif cfg.skip_prepare_dataset: dataset_wrapper = dataset - elif ds_strategy := load( - config_dataset.type, tokenizer, cfg, config_dataset, processor=processor - ): - dataset_prompter = UnsupportedPrompter() - dataset_wrapper = TokenizedPromptDataset( - ds_strategy, - dataset, - **ds_kwargs, - ) + elif ds_strategy := load(config_dataset.type, tokenizer, cfg, config_dataset): + if isinstance(ds_strategy, DatasetWrappingStrategy): + dataset_wrapper = ds_strategy.wrap_dataset(dataset, **ds_kwargs) + else: + dataset_prompter = UnsupportedPrompter() + dataset_wrapper = TokenizedPromptDataset( + ds_strategy, + dataset, + **ds_kwargs, + ) elif d_base_type == "alpaca": dataset_prompter = AlpacaPrompter(d_prompt_style) ds_strategy = AlpacaPromptTokenizingStrategy( diff --git a/tests/core/chat/__init__.py b/tests/core/chat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/chat/format/__init__.py b/tests/core/chat/format/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/chat/test_messages.py b/tests/core/chat/test_messages.py new file mode 100644 index 0000000000..b3be56c590 --- /dev/null +++ b/tests/core/chat/test_messages.py @@ -0,0 +1,197 @@ +""" +Tests for the chat messages module +""" +import unittest + +import pytest +from transformers import AddedToken, AutoTokenizer + +from axolotl.core.chat.format.chatml import format_message +from axolotl.core.chat.messages import ChatFormattedChats, Chats + + +@pytest.fixture(scope="session", name="llama_tokenizer") +def llama_tokenizer_fixture(): + return AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3.1-8B") + + +@pytest.fixture(scope="session", name="chatml_tokenizer") +def llama_tokenizer_w_chatml(llama_tokenizer): + llama_tokenizer.add_special_tokens( + { + "eos_token": AddedToken( + "<|im_end|>", rstrip=False, lstrip=False, normalized=False + ) + } + ) + llama_tokenizer.add_tokens( + [ + AddedToken("<|im_start|>", rstrip=False, lstrip=False, normalized=False), + ] + ) + + return llama_tokenizer + + +@pytest.fixture(scope="session", name="chat_msgs") +def chat_msgs_fixture(): + return { + "conversation": [ + { + "role": "system", + "content": [ + {"type": "text", "value": "You are a helpful assistant."}, + ], + }, + { + "role": "user", + "content": [ + {"type": "text", "value": "What is today's stock price of Apple?"}, + ], + }, + { + "role": "assistant", + "content": [ + { + "type": "tool_call", + "value": { + "name": "get_date", + "arguments": {}, + }, + }, + { + "type": "tool_call", + "value": { + "name": "get_stock_price", + "arguments": {"symbol": "AAPL"}, + }, + }, + ], + "weight": 1, + }, + { + "role": "tool", + "content": [ + { + "type": "tool_response", + "value": { + "name": "get_date", + "content": {"date": "2024-09-09"}, + }, + }, + { + "type": "tool_response", + "value": { + "name": "get_stock_price", + "content": {"symbol": "AAPL", "price": 123.45}, + }, + }, + ], + }, + { + "role": "assistant", + "content": [ + { + "type": "text", + "value": "The stock price of Apple is $123.45.\n", + "weight": 0, + }, + { + "type": "text", + "value": "The original query asked for today's stock price of Apple. This implies they also wanted the date included in the response.", + }, + { + "type": "text", + "value": "The stock price of Apple on September 9, 2024 is $123.45.", + }, + ], + "weight": 1, + }, + ] + } + + +class TestMessagesCase: + """ + Test cases for the chat messages module + """ + + def test_tool_call_stringify(self, chat_msgs): + chat_msgs_as_obj = Chats(**chat_msgs) + assert '{"name": "get_stock_price", "arguments": {"symbol": "AAPL"}}' == str( + chat_msgs_as_obj.conversation[2].content[1].value + ) + + def test_chatml_formatted_wrapper(self, chat_msgs): + chat_msg_formatted = ChatFormattedChats(**chat_msgs, formatter=format_message) + target_chatml = """<|im_start|>system +You are a helpful assistant.<|im_end|> +<|im_start|>user +What is today's stock price of Apple?<|im_end|> +<|im_start|>assistant + +{"name": "get_date", "arguments": {}} + + +{"name": "get_stock_price", "arguments": {"symbol": "AAPL"}} + +<|im_end|> +<|im_start|>tool + +{"name": "get_date", "content": {"date": "2024-09-09"}} + + +{"name": "get_stock_price", "content": {"symbol": "AAPL", "price": 123.45}} + +<|im_end|> +<|im_start|>assistant +The stock price of Apple is $123.45. +The original query asked for today's stock price of Apple. This implies they also wanted the date included in the response.The stock price of Apple on September 9, 2024 is $123.45.<|im_end|>\n""" + assert target_chatml == str(chat_msg_formatted) + + def test_chatml_formatting_tool_call(self, chat_msgs): + chat_msgs_as_obj = Chats(**chat_msgs) + target_chatml_turn2 = """<|im_start|>assistant\n\n{"name": "get_date", "arguments": {}}\n\n\n{"name": "get_stock_price", "arguments": {"symbol": "AAPL"}}\n\n<|im_end|>\n""" + assert target_chatml_turn2 == str( + format_message(chat_msgs_as_obj.conversation[2]) + ) + + def test_train_labels(self, chatml_tokenizer, chat_msgs): + chat_msg_formatted = ChatFormattedChats(**chat_msgs, formatter=format_message) + tokenized = chat_msg_formatted.conversation[2].tokenized(chatml_tokenizer) + # fmt: off + target_labels = [ + -100, -100, -100, # role + 27, 14506, 13735, 397, 5018, 609, 794, + 330, 456, 4257, 498, 330, 16774, 794, 4792, 534, 524, + 14506, 13735, 397, 27, 14506, 13735, 397, 5018, 609, 794, + 330, 456, 31641, 9217, 498, 330, 16774, 794, 5324, 19314, + 794, 330, 84016, 43, 96742, 524, 14506, 13735, 397, + 128256, # <|im_end|> + -100 # trailing newline + ] + # fmt: on + assert tokenized["labels"] == target_labels + + def test_train_labels_2(self, chatml_tokenizer, chat_msgs): + # also test if indivudal contents are set not to train + chat_msg_formatted = ChatFormattedChats(**chat_msgs, formatter=format_message) + tokenized = chat_msg_formatted.conversation[4].tokenized(chatml_tokenizer) + # fmt: off + target_labels = [ + -100, -100, -100, # role + -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # initial response + 27, 78098, 16761, 4113, 3319, 4691, 369, 3432, 596, 5708, 3430, + 315, 8325, 13, 1115, 24897, 814, 1101, 4934, 279, 2457, + 5343, 304, 279, 2077, 4005, 78098, 16761, 5708, 3430, 315, + 8325, 389, 6250, 220, 24, 11, 220, 2366, 19, 374, 400, + 4513, 13, 1774, 13, + 128256, # <|im_end|> + -100, # trailing newline + ] + # fmt: on + assert tokenized["labels"] == target_labels + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/prompt_strategies/messages/__init__.py b/tests/prompt_strategies/messages/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/prompt_strategies/messages/test_chat.py b/tests/prompt_strategies/messages/test_chat.py new file mode 100644 index 0000000000..96c4b6cbbf --- /dev/null +++ b/tests/prompt_strategies/messages/test_chat.py @@ -0,0 +1,62 @@ +""" +tests for chat_template prompt strategy +""" +# pylint: disable=duplicate-code +import logging +import unittest + +from axolotl.prompt_strategies.messages.chat import load +from axolotl.utils.dict import DictDefault + +logging.basicConfig(level=logging.DEBUG) +LOG = logging.getLogger("axolotl") + + +class TestMessagesChatLlama3: + """ + Test class for assistant style datasets with llama-3 prompts using the messages chat llama3 strategy. + """ + + def test_llama3_load(self, llama3_tokenizer, assistant_dataset): + LOG.info("Loading llama-3 tokenizer with assistant dataset") + strategy = load( + llama3_tokenizer, + DictDefault( + { + "train_on_inputs": False, + "sequence_len": 512, + } + ), + DictDefault( + { + "chat_template": "llama3", + "message_field_role": "role", + "message_field_content": "content", + "field_messages": "messages", + } + ), + ) + res = strategy.wrap_dataset(assistant_dataset) + input_ids = res[0]["input_ids"] + # fmt: off + expected_input_ids = [ + 128000, # bos + 128006, 882, 128007, # user header + 271, 15339, 128009, # user prompt eot + 128006, 78191, 128007, # assistant header + 271, 15339, 128009, # assistant response eot + 128006, 882, 128007, + 271, 19045, 29474, 128009, + 128006, 78191, 128007, + 271, 19045, 29474, 128009, + ] + # fmt: on + LOG.debug(f"Expected input_ids: {expected_input_ids}") + LOG.debug(f"Actual input_ids: {input_ids}") + assert ( + input_ids == expected_input_ids + ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + + +if __name__ == "__main__": + unittest.main() From 68b1369de9cc8b77931bc4489899216f40fdb93f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 13 Oct 2024 15:11:13 -0400 Subject: [PATCH 0188/1405] Reward model (#1879) --- examples/gemma2/reward-model.yaml | 63 +++++++++++++ src/axolotl/core/trainer_builder.py | 65 ++++++++++---- .../prompt_strategies/bradley_terry/README.md | 10 +++ .../bradley_terry/__init__.py | 35 ++++++++ .../bradley_terry/chat_template.py | 88 +++++++++++++++++++ .../prompt_strategies/bradley_terry/llama3.py | 27 ++++++ .../prompt_strategies/chat_template.py | 1 + src/axolotl/train.py | 3 +- .../config/models/input/v0_4_1/__init__.py | 12 +++ src/axolotl/utils/data/sft.py | 18 +++- src/axolotl/utils/trainer.py | 7 +- tests/e2e/test_reward_model_llama.py | 74 ++++++++++++++++ 12 files changed, 382 insertions(+), 21 deletions(-) create mode 100644 examples/gemma2/reward-model.yaml create mode 100644 src/axolotl/prompt_strategies/bradley_terry/README.md create mode 100644 src/axolotl/prompt_strategies/bradley_terry/__init__.py create mode 100644 src/axolotl/prompt_strategies/bradley_terry/chat_template.py create mode 100644 src/axolotl/prompt_strategies/bradley_terry/llama3.py create mode 100644 tests/e2e/test_reward_model_llama.py diff --git a/examples/gemma2/reward-model.yaml b/examples/gemma2/reward-model.yaml new file mode 100644 index 0000000000..c1f993c3ae --- /dev/null +++ b/examples/gemma2/reward-model.yaml @@ -0,0 +1,63 @@ +base_model: google/gemma-2-2b +model_type: AutoModelForSequenceClassification +tokenizer_type: AutoTokenizer + +load_in_8bit: false +load_in_4bit: false +strict: false + +reward_model: true +chat_template: gemma +datasets: + - path: argilla/distilabel-intel-orca-dpo-pairs + type: bradley_terry.chat_template +val_set_size: 0.0 +output_dir: ./outputs/out +remove_unused_columns: false + +sequence_len: 2048 +sample_packing: false +eval_sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 9c12b6141a..599144bd34 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -43,8 +43,10 @@ KTOTrainer, ORPOConfig, ORPOTrainer, + RewardConfig, + RewardTrainer, ) -from trl.trainer.utils import pad_to_length +from trl.trainer.utils import RewardDataCollatorWithPadding, pad_to_length from axolotl.monkeypatch.multipack import SUPPORTED_MULTIPACK_MODEL_TYPES from axolotl.monkeypatch.relora import ReLoRACallback, ReLoRAScheduler @@ -301,6 +303,13 @@ class AxolotlCPOConfig(AxolotlTrainingMixins, CPOConfig): ) +@dataclass +class AxolotlRewardConfig(AxolotlTrainingMixins, RewardConfig): + """ + Reward config for Reward training + """ + + class SchedulerMixin(Trainer): """ Mixin class for scheduler setup in CausalTrainer. @@ -398,12 +407,10 @@ class AxolotlTrainer(SchedulerMixin, Trainer): def __init__( self, *_args, - num_epochs=1, bench_data_collator=None, eval_data_collator=None, **kwargs, ): - self.num_epochs = num_epochs self.bench_data_collator = bench_data_collator self.eval_data_collator = eval_data_collator super().__init__(*_args, **kwargs) @@ -1039,6 +1046,14 @@ class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): tag_names = ["axolotl", "cpo"] +class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): + """ + Extend the base RewardTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "reward"] + + class TrainerBuilderBase(abc.ABC): """ Base class for trainer builder @@ -1214,6 +1229,8 @@ def _get_trainer_cls(self): return ReLoRATrainer if self.cfg.model_config_type == "mamba": return AxolotlMambaTrainer + if self.cfg.reward_model: + return AxolotlRewardTrainer return AxolotlTrainer def build(self, total_num_steps): @@ -1553,6 +1570,9 @@ def build(self, total_num_steps): trainer_kwargs = {} + if self.cfg.reward_model: + trainer_kwargs["max_length"] = self.cfg.sequence_len + if self.cfg.optimizer in [ "optimi_adamw", "ao_adamw_4bit", @@ -1596,10 +1616,13 @@ def build(self, total_num_steps): "accelerator_config" ] = self.cfg.accelerator_config - training_args = ( - AxolotlTrainingArguments( # pylint: disable=unexpected-keyword-arg - **training_arguments_kwargs, - ) + training_args_cls = ( + AxolotlTrainingArguments + if not self.cfg.reward_model + else AxolotlRewardConfig + ) + training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg + **training_arguments_kwargs, ) training_args = self.hook_post_create_training_args(training_args) @@ -1621,10 +1644,24 @@ def build(self, total_num_steps): # https://docs.nvidia.com/deeplearning/performance/dl-performance-matrix-multiplication/index.html data_collator_kwargs["pad_to_multiple_of"] = 64 + if self.cfg.reward_model: + data_collator_kwargs["max_length"] = self.cfg.sequence_len + trainer_cls = self._get_trainer_cls() trainer_kwargs, trainer_cls = self.hook_pre_create_trainer( trainer_kwargs, trainer_cls ) + if eval_data_collator := self.build_collator( + training_args, is_eval=True, **data_collator_kwargs + ): + if not self.cfg.reward_model: + trainer_kwargs["eval_data_collator"] = eval_data_collator + if not self.cfg.reward_model: + trainer_kwargs["bench_data_collator"] = transformers.DataCollatorForSeq2Seq( + self.tokenizer, + return_tensors="pt", + **data_collator_kwargs, + ) trainer = trainer_cls( model=self.model, train_dataset=self.train_dataset, @@ -1632,16 +1669,7 @@ def build(self, total_num_steps): args=training_args, tokenizer=self.tokenizer, data_collator=self.build_collator(training_args, **data_collator_kwargs), - eval_data_collator=self.build_collator( - training_args, is_eval=True, **data_collator_kwargs - ), - bench_data_collator=transformers.DataCollatorForSeq2Seq( - self.tokenizer, - return_tensors="pt", - **data_collator_kwargs, - ), callbacks=self.get_callbacks(), - num_epochs=self.cfg.num_epochs, **trainer_kwargs, ) trainer = self.hook_post_create_trainer(trainer) @@ -1675,9 +1703,12 @@ def build_collator( V2BatchSamplerDataCollatorForSeq2Seq, BatchSamplerDataCollatorForSeq2Seq, DataCollatorForSeq2Seq, + RewardDataCollatorWithPadding, ] ] - if use_batch_sampler_collator: + if self.cfg.reward_model: + collator = RewardDataCollatorWithPadding + elif use_batch_sampler_collator: if self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES: collator = V2BatchSamplerDataCollatorForSeq2Seq elif ( diff --git a/src/axolotl/prompt_strategies/bradley_terry/README.md b/src/axolotl/prompt_strategies/bradley_terry/README.md new file mode 100644 index 0000000000..39cd16137c --- /dev/null +++ b/src/axolotl/prompt_strategies/bradley_terry/README.md @@ -0,0 +1,10 @@ +### example yaml + +```yaml +chat_template: gemma +datasets: + - path: argilla/distilabel-intel-orca-dpo-pairs + type: bradley_terry.chat_template +val_set_size: 0.0 +output_dir: ./outputs/out +``` diff --git a/src/axolotl/prompt_strategies/bradley_terry/__init__.py b/src/axolotl/prompt_strategies/bradley_terry/__init__.py new file mode 100644 index 0000000000..849d84e458 --- /dev/null +++ b/src/axolotl/prompt_strategies/bradley_terry/__init__.py @@ -0,0 +1,35 @@ +"""Module to load prompt strategies.""" + +import importlib +import inspect +import logging + +from axolotl.prompt_strategies.user_defined import UserDefinedDatasetConfig + +LOG = logging.getLogger("axolotl.prompt_strategies") + + +def load(strategy, tokenizer, cfg, ds_cfg): + # pylint: disable=duplicate-code + try: + load_fn = "load" + if strategy.split(".")[-1].startswith("load_"): + load_fn = strategy.split(".")[-1] + strategy = ".".join(strategy.split(".")[:-1]) + mod = importlib.import_module( + f".{strategy}", "axolotl.prompt_strategies.bradley_terry" + ) + func = getattr(mod, load_fn) + load_kwargs = {} + if strategy == "user_defined": + load_kwargs["ds_cfg"] = UserDefinedDatasetConfig(**ds_cfg) + else: + sig = inspect.signature(func) + if "ds_cfg" in sig.parameters: + load_kwargs["ds_cfg"] = ds_cfg + return func(tokenizer, cfg, **load_kwargs) + except ModuleNotFoundError: + return None + except Exception as exc: # pylint: disable=broad-exception-caught + LOG.error(f"Failed to load prompt strategy `{strategy}`: {str(exc)}") + return None diff --git a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py new file mode 100644 index 0000000000..ccda0a4bde --- /dev/null +++ b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py @@ -0,0 +1,88 @@ +""" +Bradley-Terry model with chat template prompt strategy. +""" + +from typing import Any, Dict, Optional + +from axolotl.prompt_strategies.chat_template import ( + ChatTemplatePrompter, + ChatTemplateStrategy, +) +from axolotl.utils.chat_templates import chat_templates + + +class BTChatTemplateStrategy(ChatTemplateStrategy): + """ + Bradley-Terry reward model pairwise chat template prompt strategy. + """ + + def tokenize_prompt(self, prompt): + """ + + :param prompt: the actual row of data from the underlying dataset + :return: + """ + + self.messages = "chosen_messages" + # pylint: disable=duplicate-code + prompt[self.messages] = [] + if prompt["system"]: + prompt[self.messages].append({"from": "system", "value": prompt["system"]}) + prompt[self.messages].append({"from": "user", "value": prompt["input"]}) + prompt[self.messages].append({"from": "assistant", "value": prompt["chosen"]}) + chosen_tokenized = super().tokenize_prompt(prompt) + + self.messages = "rejected_messages" + # pylint: disable=duplicate-code + prompt[self.messages] = [] + if prompt["system"]: + prompt[self.messages].append({"from": "system", "value": prompt["system"]}) + prompt[self.messages].append({"from": "user", "value": prompt["input"]}) + prompt[self.messages].append({"from": "assistant", "value": prompt["rejected"]}) + rejected_tokenized = super().tokenize_prompt(prompt) + + return { + "input_ids_chosen": chosen_tokenized["input_ids"], + "attention_mask_chosen": chosen_tokenized["attention_mask"], + "labels_chosen": 1.0, + "input_ids_rejected": rejected_tokenized["input_ids"], + "attention_mask_rejected": rejected_tokenized["attention_mask"], + "labels_rejected": 0.0, + } + + +def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): + ds_cfg = ds_cfg or {} + + prompter_params = { + "tokenizer": tokenizer, + "chat_template": chat_templates(ds_cfg.get("chat_template", "chatml")), + "message_field_role": ds_cfg.get("message_field_role", "from"), + "message_field_content": ds_cfg.get("message_field_content", "value"), + "message_field_training": ds_cfg.get("message_field_training", "training"), + "message_field_training_detail": ds_cfg.get( + "message_field_training_detail", "train_detail" + ), + "roles": ds_cfg.get("roles"), + "drop_system_message": ds_cfg.get("drop_system_message", False), + # we need to add one for detecting sequences with exceeding the `sequence_len` limit. + "max_length": cfg.sequence_len + 1 + if not cfg.reward_model + else cfg.sequence_len, + } + + strategy_params = { + "train_on_inputs": cfg.train_on_inputs, + "sequence_len": cfg.sequence_len, + "roles_to_train": ds_cfg.get("roles_to_train", ["gpt", "assistant"]), + "train_on_eos": ds_cfg.get("train_on_eos", "turn"), + } + + strategy = BTChatTemplateStrategy( + ChatTemplatePrompter(**prompter_params), tokenizer=tokenizer, **strategy_params + ) + + if "field_messages" in ds_cfg and hasattr(strategy, "messages"): + strategy.messages = ds_cfg["field_messages"] + + return strategy diff --git a/src/axolotl/prompt_strategies/bradley_terry/llama3.py b/src/axolotl/prompt_strategies/bradley_terry/llama3.py new file mode 100644 index 0000000000..1d586fd5f4 --- /dev/null +++ b/src/axolotl/prompt_strategies/bradley_terry/llama3.py @@ -0,0 +1,27 @@ +""" +chatml transforms for datasets with system, input, chosen, rejected to match llama3 chat template +""" + + +def icr( + cfg, + **kwargs, +): # pylint: disable=possibly-unused-variable,unused-argument + """ + chatml transforms for datasets with system, input, chosen, rejected + ex. https://huggingface.co/datasets/argilla/distilabel-intel-orca-dpo-pairs + """ + + def transform_fn(sample): + if "system" in sample and sample["system"]: + prompt = ( + f"<|start_header_id|>system<|end_header_id|>\n\n{sample['system']}<|eot_id|>" + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['input']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) + else: + prompt = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['input']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["chosen"] = prompt + f"{sample['chosen']}<|eot_id|>" + sample["rejected"] = prompt + f"{sample['rejected']}<|eot_id|>" + return sample + + return transform_fn diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 48d52dae11..c7852a707f 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -403,6 +403,7 @@ def get_images(self, prompt): def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None, processor=None): + # pylint: disable=duplicate-code ds_cfg = ds_cfg or {} prompter_params = { diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 855dbc2d3b..6ad3736557 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -102,7 +102,8 @@ def train( model, peft_config = load_model( cfg, tokenizer, processor=processor, inference=cli_args.inference ) - model.generation_config.do_sample = True + if model.generation_config is not None: + model.generation_config.do_sample = True model_ref = None if cfg.rl and cfg.rl != "orpo": diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 3304c62f28..4831da3c8a 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -551,6 +551,7 @@ class Config: resize_token_embeddings_to_32x: Optional[bool] = None rl: Optional[RLType] = None + reward_model: Optional[bool] = None datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset], min_length=1)] = None # type: ignore test_datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset], min_length=1)] = None # type: ignore @@ -856,6 +857,17 @@ def hint_sample_packing_padding(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def hint_reward_model_pad(cls, data): + if data.get("reward_model") and not data.get("pad_to_sequence_len"): + LOG.warning( + "`pad_to_sequence_len: true` is recommended when using reward_model" + ) + if data.get("pad_to_sequence_len") is None: + data["pad_to_sequence_len"] = True + return data + @model_validator(mode="before") @classmethod def check_gas_bsz(cls, data): diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 163059c2b8..ce01b44098 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -19,6 +19,7 @@ from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH from axolotl.datasets import TokenizedPromptDataset from axolotl.prompt_strategies import load +from axolotl.prompt_strategies.bradley_terry import load as bradley_terry_load from axolotl.prompt_tokenizers import ( AlpacaMultipleChoicePromptTokenizingStrategy, AlpacaPromptTokenizingStrategy, @@ -459,7 +460,7 @@ def for_d_in_datasets(dataset_configs): else: LOG.debug("NOT shuffling merged datasets") - if not cfg.skip_prepare_dataset: + if cfg.sample_packing and not cfg.skip_prepare_dataset: dataset, _ = process_datasets_for_packing(cfg, dataset, None) if cfg.local_rank == 0 and not cfg.skip_prepare_dataset: @@ -609,7 +610,20 @@ def get_dataset_wrapper( ) elif cfg.skip_prepare_dataset: dataset_wrapper = dataset - elif ds_strategy := load(config_dataset.type, tokenizer, cfg, config_dataset): + elif ds_strategy := config_dataset.type.startswith( + "bradley_terry" + ) and bradley_terry_load( + config_dataset.type.split(".", 1)[1], tokenizer, cfg, config_dataset + ): + dataset_prompter = UnsupportedPrompter() + dataset_wrapper = TokenizedPromptDataset( + ds_strategy, + dataset, + **ds_kwargs, + ) + elif ds_strategy := load( + config_dataset.type, tokenizer, cfg, config_dataset, processor=processor + ): if isinstance(ds_strategy, DatasetWrappingStrategy): dataset_wrapper = ds_strategy.wrap_dataset(dataset, **ds_kwargs) else: diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 30b40925f9..7ebf384aff 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -306,7 +306,11 @@ def process_pretraining_datasets_for_packing( def calculate_total_num_steps(cfg, train_dataset, update=True): - if not cfg.total_num_tokens and not cfg.skip_prepare_dataset: + if ( + not cfg.total_num_tokens + and not cfg.skip_prepare_dataset + and not cfg.reward_model + ): total_num_tokens = np.sum( train_dataset.data.column("input_ids") .to_pandas() @@ -323,6 +327,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): not skip_estimates and not cfg.total_supervised_tokens and not cfg.skip_prepare_dataset + and not cfg.reward_model ): total_supervised_tokens = ( train_dataset.data.column("labels") diff --git a/tests/e2e/test_reward_model_llama.py b/tests/e2e/test_reward_model_llama.py new file mode 100644 index 0000000000..27ac3e25f1 --- /dev/null +++ b/tests/e2e/test_reward_model_llama.py @@ -0,0 +1,74 @@ +""" +E2E tests for reward model lora llama +""" + +import logging +import os +import unittest +from pathlib import Path + +from axolotl.cli import load_datasets +from axolotl.common.cli import TrainerCliArgs +from axolotl.train import train +from axolotl.utils.config import normalize_config +from axolotl.utils.dict import DictDefault + +from .utils import with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestRewardModelLoraLlama(unittest.TestCase): + """ + Test case for Llama reward models using LoRA + """ + + @with_temp_dir + def test_rm_fft(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "model_type": "AutoModelForSequenceClassification", + "tokenizer_type": "LlamaTokenizer", + "chat_template": "alpaca", + "reward_model": True, + "sequence_len": 1024, + "pad_to_sequence_len": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.0, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "argilla/distilabel-intel-orca-dpo-pairs", + "type": "bradley_terry.chat_template", + }, + ], + "remove_unused_columns": False, + "max_steps": 10, + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "gradient_checkpointing": True, + "warmup_ratio": 0.1, + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.bin").exists() From ec4272c3a0afedadf7bb54f9386dfd51d4a4c2cb Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 13 Oct 2024 17:34:37 -0400 Subject: [PATCH 0189/1405] add ds zero3 to multigpu biweekly tests (#1900) * add ds zero3 to multigpu biweekly tests * fix for upstream api change * use updated accelerate and fix deepspeed tests * stringify the Path, and run multigpu tests if the multigpu tests change for a PR * use correct json rather than yaml * revert accelerate for deepspeed --- requirements.txt | 2 +- tests/e2e/multigpu/test_llama.py | 114 +++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 37ee1e42cf..46d0691b6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ peft==0.13.2 transformers==4.45.2 tokenizers>=0.20.1 bitsandbytes==0.44.1 -accelerate==1.0.0 +accelerate==0.34.2 datasets==3.0.1 deepspeed==0.14.4 pydantic==2.6.3 diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 61bb8ed327..957a6a9e36 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -19,6 +19,8 @@ LOG = logging.getLogger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" +AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent + @pytest.fixture(scope="session", autouse=True) def download_model(): @@ -346,3 +348,115 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): str(Path(temp_dir) / "config.yaml"), ] ) + + @with_temp_dir + def test_ds_zero3_packed(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "TinyLlama/TinyLlama_v1.1", + "tokenizer_type": "LlamaTokenizer", + "sample_packing": True, + "eval_sample_packing": False, + "pad_to_sequence_len": True, + "sequence_len": 2048, + "val_set_size": 0.05, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 100, + "micro_batch_size": 4, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero3_bf16.json"), + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) + + @with_temp_dir + def test_ds_zero3_qlora_packed(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "TinyLlama/TinyLlama_v1.1", + "tokenizer_type": "LlamaTokenizer", + "load_in_4bit": True, + "adapter": "qlora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "sample_packing": True, + "eval_sample_packing": False, + "pad_to_sequence_len": True, + "sequence_len": 2048, + "val_set_size": 0.05, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 100, + "micro_batch_size": 4, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero3_bf16.json"), + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) From 335027f155b34b569d2d7c106c8797569a8eaa56 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 13 Oct 2024 20:04:30 -0400 Subject: [PATCH 0190/1405] upgrade accelerate to 1.0.1 (#1969) --- requirements.txt | 2 +- src/axolotl/train.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 46d0691b6c..8f9f55262e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ peft==0.13.2 transformers==4.45.2 tokenizers>=0.20.1 bitsandbytes==0.44.1 -accelerate==0.34.2 +accelerate==1.0.1 datasets==3.0.1 deepspeed==0.14.4 pydantic==2.6.3 diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 6ad3736557..4ce28d8a31 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -10,7 +10,6 @@ import torch import transformers.modelcard -from accelerate import Accelerator from accelerate.logging import get_logger from accelerate.utils import save_fsdp_model from datasets import Dataset @@ -97,8 +96,6 @@ def train( if cfg.adapter: msg += " and peft_config..." LOG.debug(msg) - # we wait unitl the last possible moment to setup Accelerator - Accelerator() model, peft_config = load_model( cfg, tokenizer, processor=processor, inference=cli_args.inference ) From 6d9a3c4d817cd57e702b270c04d2b2d2400c3ad4 Mon Sep 17 00:00:00 2001 From: JohanWork <39947546+JohanWork@users.noreply.github.com> Date: Mon, 14 Oct 2024 22:00:48 +0200 Subject: [PATCH 0191/1405] examples: Fix config llama3 (#1833) [skip ci] * update llama3 config * llama3 config --- examples/llama-3/instruct-dpo-lora-8b.yml | 1 - examples/llama-3/instruct-lora-8b.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/examples/llama-3/instruct-dpo-lora-8b.yml b/examples/llama-3/instruct-dpo-lora-8b.yml index 14febb810a..dc88350358 100644 --- a/examples/llama-3/instruct-dpo-lora-8b.yml +++ b/examples/llama-3/instruct-dpo-lora-8b.yml @@ -11,7 +11,6 @@ rl: dpo datasets: - path: fozziethebeat/alpaca_messages_2k_dpo_test type: chat_template.default - chat_template: llama3 field_messages: conversation field_chosen: chosen field_rejected: rejected diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml index 4acad59999..ae9a8088c3 100644 --- a/examples/llama-3/instruct-lora-8b.yml +++ b/examples/llama-3/instruct-lora-8b.yml @@ -10,7 +10,6 @@ chat_template: llama3 datasets: - path: fozziethebeat/alpaca_messages_2k_test type: chat_template - chat_template: llama3 field_messages: messages message_field_role: role message_field_content: content From 54673fd6ca39bf86addb20ba7d44a77071b4dc4f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 17 Oct 2024 14:12:31 -0400 Subject: [PATCH 0192/1405] also debug if other debug args are set (#1977) --- src/axolotl/cli/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index fd5ab3e56c..84836bb793 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -462,7 +462,12 @@ def load_datasets( processor=processor, ) - if cli_args.debug or cfg.debug: + if ( + cli_args.debug + or cfg.debug + or cli_args.debug_text_only + or cli_args.debug_num_examples + ): LOG.info("check_dataset_labels...") check_dataset_labels( train_dataset.select( From f62e23737bc54dbef758fa8c58296ee0b1023e7b Mon Sep 17 00:00:00 2001 From: Sunny Liu Date: Thu, 17 Oct 2024 15:15:29 -0400 Subject: [PATCH 0193/1405] memoize dataset length for eval sample packing (#1974) * wip on multimodal sample packing support * wip on multimodal packing support * llama-1b-yml * setup logging for test * yml * yml * yml * fix for __len__ for eval sample packing * reverted irrelavant changes * reformatted, reverted log message * reverted unnecessary changes * added e2e multigpu testing for eval sample packing * formatting * fixed e2e test_eval params * fix test_eval e2e multigpu * fix test_eval e2e multigpu * Update tests/e2e/multigpu/test_eval.py Co-authored-by: Wing Lian * Update tests/e2e/multigpu/test_eval.py Co-authored-by: Wing Lian --------- Co-authored-by: Wing Lian --- examples/llama-3/qlora-1b.yml | 77 ++++++++++++ src/axolotl/utils/samplers/multipack.py | 13 +- tests/e2e/multigpu/test_eval.py | 155 ++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 examples/llama-3/qlora-1b.yml create mode 100644 tests/e2e/multigpu/test_eval.py diff --git a/examples/llama-3/qlora-1b.yml b/examples/llama-3/qlora-1b.yml new file mode 100644 index 0000000000..fdfe4aa7c8 --- /dev/null +++ b/examples/llama-3/qlora-1b.yml @@ -0,0 +1,77 @@ +base_model: meta-llama/Llama-3.2-1B + +load_in_8bit: false +load_in_4bit: true +strict: false + +datasets: + - path: teknium/GPT4-LLM-Cleaned + type: alpaca +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/qlora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: true +pad_to_sequence_len: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_fan_in_fan_out: +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +loss_watchdog_threshold: 5.0 +loss_watchdog_patience: 3 + +warmup_steps: 10 +evals_per_epoch: 4 +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: + pad_token: "<|end_of_text|>" diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 205c2894d1..db14a6819e 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -133,6 +133,8 @@ def __init__( self.eff_total_used = 0 self.eff_total_slots = 0 + self.len_across_ranks = None + def set_epoch(self, epoch: int): self.epoch = epoch @@ -195,15 +197,14 @@ def calc_min_len(estimates: list[(int, float)]): LOG.info(f"gather_len_batches: {repr(estimates)}") return math.floor(0.998 * min(estimates)) - min_len_batches = reduce_and_broadcast( - lambda: num, - calc_min_len, - ) + min_len_batches = reduce_and_broadcast(lambda: num, calc_min_len) return min_len_batches def __len__(self): - len_batches = self.num_batches() - return self.gather_len_batches(len_batches) + if not self.len_across_ranks: + len_batches = self.num_batches() + self.len_across_ranks = self.gather_len_batches(len_batches) + return self.len_across_ranks def _len_est(self): efficiency = ( diff --git a/tests/e2e/multigpu/test_eval.py b/tests/e2e/multigpu/test_eval.py new file mode 100644 index 0000000000..65d26bb824 --- /dev/null +++ b/tests/e2e/multigpu/test_eval.py @@ -0,0 +1,155 @@ +""" +E2E tests for multigpu eval +""" +import logging +import os +import unittest +from pathlib import Path + +import yaml +from accelerate.test_utils import execute_subprocess_async + +from axolotl.utils.dict import DictDefault + +from ..utils import with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e.multigpu") +os.environ["WANDB_DISABLED"] = "true" + +AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent + + +class TestMultiGPUEval(unittest.TestCase): + """ + Test case for MultiGPU Eval Sample Packing + """ + + @with_temp_dir + def test_eval_sample_packing(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "load_in_8bit": False, + "load_in_4bit": True, + "strict": False, + "sequence_len": 2048, + "adapter": "qlora", + "sample_packing": True, + "eval_sample_packing": True, + "pad_to_sequence_len": True, + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "lora_modules_to_save": ["embed_tokens", "lm_head"], + "val_set_size": 0.1, + "special_tokens": {"pad_token": "<|end_of_text|>"}, + "datasets": [ + { + "path": "teknium/GPT4-LLM-Cleaned", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "micro_batch_size": 2, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "loss_watchdog_threshold": 5.0, + "loss_watchdog_patience": 3, + "bf16": "auto", + "warmup_steps": 1, + "evals_per_epoch": 2, + "eval_max_new_tokens": 128, + "saves_per_epoch": 1, + "logging_steps": 1, + "weight_decay": 0.0, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) + + @with_temp_dir + def test_eval(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "load_in_8bit": False, + "load_in_4bit": True, + "strict": False, + "sequence_len": 2048, + "adapter": "qlora", + "sample_packing": True, + "eval_sample_packing": False, + "pad_to_sequence_len": True, + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "lora_modules_to_save": ["embed_tokens", "lm_head"], + "val_set_size": 0.1, + "special_tokens": {"pad_token": "<|end_of_text|>"}, + "datasets": [ + { + "path": "teknium/GPT4-LLM-Cleaned", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "micro_batch_size": 2, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "loss_watchdog_threshold": 5.0, + "loss_watchdog_patience": 3, + "bf16": "auto", + "warmup_steps": 1, + "evals_per_epoch": 2, + "eval_max_new_tokens": 128, + "saves_per_epoch": 1, + "logging_steps": 1, + "weight_decay": 0.0, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) From 67f744dc8c9564ef7a42d5df780ae53e319dca61 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 18 Oct 2024 03:36:51 -0400 Subject: [PATCH 0194/1405] add pytorch 2.5.0 base images (#1979) * add pytorch 2.5.0 base images * make sure num examples for debug is zero and fix comparison --- .github/workflows/base.yml | 6 ++++++ src/axolotl/cli/__init__.py | 2 +- src/axolotl/common/cli.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 1b24f2c970..c94093bc97 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -36,6 +36,12 @@ jobs: python_version: "3.11" pytorch: 2.4.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + - cuda: "124" + cuda_version: 12.4.1 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.5.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" steps: - name: Checkout uses: actions/checkout@v3 diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 84836bb793..77bb551f8c 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -466,7 +466,7 @@ def load_datasets( cli_args.debug or cfg.debug or cli_args.debug_text_only - or cli_args.debug_num_examples + or int(cli_args.debug_num_examples) > 0 ): LOG.info("check_dataset_labels...") check_dataset_labels( diff --git a/src/axolotl/common/cli.py b/src/axolotl/common/cli.py index c96f8f81ff..6a3a22e637 100644 --- a/src/axolotl/common/cli.py +++ b/src/axolotl/common/cli.py @@ -23,7 +23,7 @@ class TrainerCliArgs: debug: bool = field(default=False) debug_text_only: bool = field(default=False) - debug_num_examples: int = field(default=5) + debug_num_examples: int = field(default=0) inference: bool = field(default=False) merge_lora: bool = field(default=False) prompter: Optional[str] = field(default=None) From e12a2130e990313bb0bce66be8fbbe5b856094dd Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 21 Oct 2024 11:00:45 -0400 Subject: [PATCH 0195/1405] first pass at pytorch 2.5.0 support (#1982) * first pass at pytorch 2.5.0 support * attempt to install causal_conv1d with mamba * gracefully handle missing xformers * fix import * fix incorrect version, add 2.5.0 * increase tests timeout --- .github/workflows/main.yml | 10 +++ .github/workflows/multi-gpu-e2e.yml | 13 +++- .github/workflows/nightlies.yml | 10 +++ .github/workflows/tests-nightly.yml | 9 ++- .github/workflows/tests.yml | 10 ++- cicd/Dockerfile.jinja | 1 - cicd/multigpu.py | 2 +- cicd/tests.py | 2 +- docker/Dockerfile | 1 - setup.py | 5 +- .../monkeypatch/llama_attn_hijack_flash.py | 61 ++++++------------- src/axolotl/monkeypatch/xformers_/__init__.py | 51 ++++++++++++++++ 12 files changed, 120 insertions(+), 55 deletions(-) create mode 100644 src/axolotl/monkeypatch/xformers_/__init__.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c27dbedefa..47a4c7f114 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,6 +29,11 @@ jobs: python_version: "3.11" pytorch: 2.4.1 axolotl_extras: + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.5.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -86,6 +91,11 @@ jobs: python_version: "3.11" pytorch: 2.4.1 axolotl_extras: + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.5.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index ab886c67f1..d9f0ce7e6c 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -21,10 +21,17 @@ jobs: pytorch: 2.3.1 axolotl_extras: num_gpus: 2 - - cuda: 121 - cuda_version: 12.1.1 + - cuda: 124 + cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.3.1 + pytorch: 2.4.1 + axolotl_extras: + num_gpus: 2 + nightly_build: "true" + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.5.0 axolotl_extras: num_gpus: 2 nightly_build: "true" diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 17c76c24e7..55123a9026 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -28,6 +28,11 @@ jobs: python_version: "3.11" pytorch: 2.4.1 axolotl_extras: + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.5.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -85,6 +90,11 @@ jobs: python_version: "3.11" pytorch: 2.4.1 axolotl_extras: + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.5.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 8c9e1f49e7..56eaae2398 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -25,7 +25,7 @@ jobs: fail-fast: false matrix: python_version: ["3.10", "3.11"] - pytorch_version: ["2.3.1", "2.4.1"] + pytorch_version: ["2.3.1", "2.4.1", "2.5.0"] timeout-minutes: 20 steps: @@ -95,6 +95,13 @@ jobs: num_gpus: 1 axolotl_extras: nightly_build: "true" + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.5.0 + num_gpus: 1 + axolotl_extras: + nightly_build: "true" steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a798bdd5cd..e679f41010 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: fail-fast: false matrix: python_version: ["3.10", "3.11"] - pytorch_version: ["2.3.1", "2.4.1"] + pytorch_version: ["2.3.1", "2.4.1", "2.5.0"] timeout-minutes: 20 steps: @@ -72,7 +72,7 @@ jobs: if: github.repository_owner == 'axolotl-ai-cloud' # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] - timeout-minutes: 60 + timeout-minutes: 90 needs: [pre-commit, pytest] strategy: @@ -97,6 +97,12 @@ jobs: pytorch: 2.4.1 num_gpus: 1 axolotl_extras: + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.5.0 + num_gpus: 1 + axolotl_extras: steps: - name: Checkout uses: actions/checkout@v4 diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 11ce8d8baa..3b082a15b0 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -23,7 +23,6 @@ RUN git fetch origin +$GITHUB_REF && \ git checkout FETCH_HEAD # If AXOLOTL_EXTRAS is set, append it in brackets -RUN pip install causal_conv1d RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt; \ sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt; \ diff --git a/cicd/multigpu.py b/cicd/multigpu.py index be10fbc73a..da726b4731 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -64,7 +64,7 @@ def run_cmd(cmd: str, run_folder: str): @stub.function( image=cicd_image, gpu=GPU_CONFIG, - timeout=45 * 60, + timeout=60 * 60, cpu=8.0, memory=131072 * N_GPUS, ) diff --git a/cicd/tests.py b/cicd/tests.py index 9c2d830cb7..9ebce9815f 100644 --- a/cicd/tests.py +++ b/cicd/tests.py @@ -65,7 +65,7 @@ def run_cmd(cmd: str, run_folder: str): @stub.function( image=cicd_image, gpu=GPU_CONFIG, - timeout=45 * 60, + timeout=60 * 60, cpu=8.0, memory=131072, ) diff --git a/docker/Dockerfile b/docker/Dockerfile index 2b106f1ed8..4872b3907c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,7 +20,6 @@ RUN git clone --depth=1 https://github.com/axolotl-ai-cloud/axolotl.git WORKDIR /workspace/axolotl # If AXOLOTL_EXTRAS is set, append it in brackets -RUN pip install causal_conv1d RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ pip install -e .[deepspeed,flash-attn,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ diff --git a/setup.py b/setup.py index 7d9568dbff..1153d69681 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,9 @@ def parse_requirements(): else: raise ValueError("Invalid version format") - if (major, minor) >= (2, 4): + if (major, minor) >= (2, 5): + _install_requires.pop(_install_requires.index(xformers_version)) + elif (major, minor) >= (2, 4): if patch == 0: _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.27") @@ -102,6 +104,7 @@ def parse_requirements(): ], "mamba-ssm": [ "mamba-ssm==1.2.0.post1", + "causal_conv1d", ], "auto-gptq": [ "auto-gptq==0.5.1", diff --git a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py index 4c3571ea4f..c804d0c6b9 100644 --- a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py @@ -22,7 +22,6 @@ apply_rotary_pos_emb, repeat_kv, ) -from xformers.ops import SwiGLU from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids, set_module_name @@ -44,7 +43,19 @@ LOG = logging.getLogger("axolotl") +def is_xformers_available() -> bool: + try: + import xformers # pylint: disable=unused-import # noqa: F401 + + return True + except ImportError: + return False + + def is_xformers_swiglu_available() -> bool: + if not is_xformers_available(): + return False + from xformers.ops.common import get_xformers_operator try: @@ -57,6 +68,11 @@ def is_xformers_swiglu_available() -> bool: def replace_llama_mlp_with_swiglu(model): + if is_xformers_swiglu_available(): + from axolotl.monkeypatch.xformers_ import FusedMLP + else: + raise RuntimeError("xformers SwiGLU not available for this environment") + for name, module in model.named_modules(): if isinstance(module, LlamaMLP): mlp = FusedMLP( @@ -181,49 +197,6 @@ def _post_training(self, model, name): set_module_name(model, name, new_attn) -class FusedMLP(torch.nn.Module): - """ - Fused MLP layer for incrementally improved training efficiency - """ - - def __init__( - self, - config, - gate_proj: torch.nn.Linear, - up_proj: torch.nn.Linear, - down_proj: torch.nn.Linear, - ): - super().__init__() - self.config = config - self.swiglu = SwiGLU( - in_features=config.hidden_size, - hidden_features=config.intermediate_size, - bias=False, - _pack_weights=True, - ) - # overwrite initialized weights with pretrained weights - self.swiglu.w12.weight.data = torch.cat( - (gate_proj.weight.data, up_proj.weight.data), dim=0 - ) - self.swiglu.w3.weight.data = down_proj.weight.data - - def _post_training(self, model, name): - w1, w2 = torch.split( # pylint: disable=invalid-name - self.swiglu.w12.weight.data, self.config.intermediate_size, dim=0 - ) - - # Assign the split weights back to the original layers - new_mlp = LlamaMLP(self.config) - new_mlp.gate_proj.weight.data = w1 - new_mlp.up_proj.weight.data = w2 - new_mlp.down_proj.weight.data = self.swiglu.w3.weight.data - - set_module_name(model, name, new_mlp) - - def forward(self, x: torch.Tensor) -> torch.Tensor: # pylint: disable=invalid-name - return self.swiglu(x) - - # Disable the transformation of the attention mask in LlamaModel as the flash attention # requires the attention mask to be the same as the key_padding_mask def _prepare_decoder_attention_mask( diff --git a/src/axolotl/monkeypatch/xformers_/__init__.py b/src/axolotl/monkeypatch/xformers_/__init__.py new file mode 100644 index 0000000000..bddc036b24 --- /dev/null +++ b/src/axolotl/monkeypatch/xformers_/__init__.py @@ -0,0 +1,51 @@ +""" +Fused MLP layer for incrementally improved training efficiency +""" +import torch +from transformers.models.llama.modeling_llama import LlamaMLP +from xformers.ops import SwiGLU + +from axolotl.monkeypatch.utils import set_module_name + + +class FusedMLP(torch.nn.Module): + """ + Fused MLP layer for incrementally improved training efficiency + """ + + def __init__( + self, + config, + gate_proj: torch.nn.Linear, + up_proj: torch.nn.Linear, + down_proj: torch.nn.Linear, + ): + super().__init__() + self.config = config + self.swiglu = SwiGLU( + in_features=config.hidden_size, + hidden_features=config.intermediate_size, + bias=False, + _pack_weights=True, + ) + # overwrite initialized weights with pretrained weights + self.swiglu.w12.weight.data = torch.cat( + (gate_proj.weight.data, up_proj.weight.data), dim=0 + ) + self.swiglu.w3.weight.data = down_proj.weight.data + + def _post_training(self, model, name): + w1, w2 = torch.split( # pylint: disable=invalid-name + self.swiglu.w12.weight.data, self.config.intermediate_size, dim=0 + ) + + # Assign the split weights back to the original layers + new_mlp = LlamaMLP(self.config) + new_mlp.gate_proj.weight.data = w1 + new_mlp.up_proj.weight.data = w2 + new_mlp.down_proj.weight.data = self.swiglu.w3.weight.data + + set_module_name(model, name, new_mlp) + + def forward(self, x: torch.Tensor) -> torch.Tensor: # pylint: disable=invalid-name + return self.swiglu(x) From 955cca41fc0c8c174be4ff46c4f66937d227848e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 21 Oct 2024 19:50:50 -0400 Subject: [PATCH 0196/1405] don't explicitly set cpu pytorch version (#1986) use a constraint file use min version of xformers don't install autoawq with pytorch 2.5.0 debugging for errors upgrade pip first fix action yml add back try/except retry w/o constraint use --no-build-isolation show torch version install setuptools and wheel add back try/except --- .github/workflows/tests.yml | 10 +++++++--- requirements.txt | 2 +- setup.py | 7 ++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e679f41010..130ac6e7b6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,14 +49,18 @@ jobs: python-version: ${{ matrix.python_version }} cache: 'pip' # caching pip dependencies + - name: upgrade pip + run: | + pip3 install --upgrade pip + pip3 install --upgrade packaging setuptools wheel + - name: Install PyTorch run: | - pip3 install torch==${{ matrix.pytorch_version }} --index-url https://download.pytorch.org/whl/cpu + pip3 install torch==${{ matrix.pytorch_version }} - name: Install dependencies run: | - pip3 install --upgrade pip - pip3 install --upgrade packaging + pip3 show torch pip3 install -U -e . pip3 install -r requirements-tests.txt diff --git a/requirements.txt b/requirements.txt index 8f9f55262e..067be05cf2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ flash-attn==2.6.3 sentencepiece wandb einops -xformers==0.0.28.post1 +xformers>=0.0.23.post1 optimum==1.16.2 hf_transfer colorama diff --git a/setup.py b/setup.py index 1153d69681..17347f0632 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,8 @@ def parse_requirements(): try: xformers_version = [req for req in _install_requires if "xformers" in req][0] torchao_version = [req for req in _install_requires if "torchao" in req][0] + autoawq_version = [req for req in _install_requires if "autoawq" in req][0] + if "Darwin" in platform.system(): # don't install xformers on MacOS _install_requires.pop(_install_requires.index(xformers_version)) @@ -52,10 +54,14 @@ def parse_requirements(): if (major, minor) >= (2, 5): _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.pop(_install_requires.index(autoawq_version)) elif (major, minor) >= (2, 4): if patch == 0: _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.27") + else: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers==0.0.28.post1") elif (major, minor) >= (2, 3): _install_requires.pop(_install_requires.index(torchao_version)) if patch == 0: @@ -75,7 +81,6 @@ def parse_requirements(): except PackageNotFoundError: pass - return _install_requires, _dependency_links From 5c629ee4447b64b77f465bc14ded44d37efdfa9b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 21 Oct 2024 19:51:06 -0400 Subject: [PATCH 0197/1405] use torch 2.4.1 images as latest now that torch 2.5.0 is out (#1987) --- .github/workflows/main.yml | 4 ++-- .github/workflows/nightlies.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 47a4c7f114..3b82f6a510 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,12 +23,12 @@ jobs: python_version: "3.11" pytorch: 2.3.1 axolotl_extras: mamba-ssm - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.4.1 axolotl_extras: + is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -85,12 +85,12 @@ jobs: python_version: "3.11" pytorch: 2.3.1 axolotl_extras: - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.4.1 axolotl_extras: + is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 55123a9026..b2110e737d 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -22,12 +22,12 @@ jobs: python_version: "3.11" pytorch: 2.3.1 axolotl_extras: - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.4.1 axolotl_extras: + is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -84,12 +84,12 @@ jobs: python_version: "3.11" pytorch: 2.3.1 axolotl_extras: - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.4.1 axolotl_extras: + is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" From 9bd5f7d015bb30acd84f7b4bc780123184fcf3b8 Mon Sep 17 00:00:00 2001 From: Adam Hazell <34248583+awhazell@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:52:21 +0100 Subject: [PATCH 0198/1405] Log checkpoints as mlflow artifacts (#1976) * Ensure hf_mlflow_log_artifact config var is set in env * Add transformer MLflowCallback to callbacks list when mlflow enabled * Test hf_mlflow_log_artifacts is set correctly * Test mlflow not being used by default --- src/axolotl/core/trainer_builder.py | 9 +++-- src/axolotl/utils/mlflow_.py | 4 +++ tests/test_validation.py | 56 +++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 599144bd34..f05efe7b82 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1119,12 +1119,17 @@ def get_callbacks(self) -> List[TrainerCallback]: SaveAxolotlConfigtoWandBCallback(self.cfg.axolotl_config_path) ) if self.cfg.use_mlflow and is_mlflow_available(): + from transformers.integrations.integration_utils import MLflowCallback + from axolotl.utils.callbacks.mlflow_ import ( SaveAxolotlConfigtoMlflowCallback, ) - callbacks.append( - SaveAxolotlConfigtoMlflowCallback(self.cfg.axolotl_config_path) + callbacks.extend( + [ + SaveAxolotlConfigtoMlflowCallback(self.cfg.axolotl_config_path), + MLflowCallback, + ] ) if self.cfg.use_comet and is_comet_available(): from axolotl.utils.callbacks.comet_ import SaveAxolotlConfigtoCometCallback diff --git a/src/axolotl/utils/mlflow_.py b/src/axolotl/utils/mlflow_.py index ce77390342..8710b07d06 100644 --- a/src/axolotl/utils/mlflow_.py +++ b/src/axolotl/utils/mlflow_.py @@ -16,3 +16,7 @@ def setup_mlflow_env_vars(cfg: DictDefault): # Enable mlflow if experiment name is present if cfg.mlflow_experiment_name and len(cfg.mlflow_experiment_name) > 0: cfg.use_mlflow = True + + # Enable logging hf artifacts in mlflow if value is truthy + if cfg.hf_mlflow_log_artifacts is True: + os.environ["HF_MLFLOW_LOG_ARTIFACTS"] = "true" diff --git a/tests/test_validation.py b/tests/test_validation.py index 6e0d0ad2a5..fb63977f5c 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -13,6 +13,7 @@ from axolotl.utils.config import validate_config from axolotl.utils.config.models.input.v0_4_1 import AxolotlConfigWCapabilities from axolotl.utils.dict import DictDefault +from axolotl.utils.mlflow_ import setup_mlflow_env_vars from axolotl.utils.models import check_model_config from axolotl.utils.wandb_ import setup_wandb_env_vars @@ -1432,3 +1433,58 @@ def test_comet_sets_env(self, minimal_cfg): for key in comet_env.keys(): os.environ.pop(key, None) + + +class TestValidationMLflow(BaseValidation): + """ + Validation test for MLflow + """ + + def test_hf_mlflow_artifacts_config_sets_env(self, minimal_cfg): + cfg = ( + DictDefault( + { + "hf_mlflow_log_artifacts": True, + } + ) + | minimal_cfg + ) + + new_cfg = validate_config(cfg) + + assert new_cfg.hf_mlflow_log_artifacts is True + + # Check it's not already present in env + assert "HF_MLFLOW_LOG_ARTIFACTS" not in os.environ + + setup_mlflow_env_vars(new_cfg) + + assert os.environ.get("HF_MLFLOW_LOG_ARTIFACTS") == "true" + + os.environ.pop("HF_MLFLOW_LOG_ARTIFACTS", None) + + def test_mlflow_not_used_by_default(self, minimal_cfg): + cfg = DictDefault({}) | minimal_cfg + + new_cfg = validate_config(cfg) + + setup_mlflow_env_vars(new_cfg) + + assert cfg.use_mlflow is not True + + cfg = ( + DictDefault( + { + "mlflow_experiment_name": "foo", + } + ) + | minimal_cfg + ) + + new_cfg = validate_config(cfg) + + setup_mlflow_env_vars(new_cfg) + + assert new_cfg.use_mlflow is True + + os.environ.pop("MLFLOW_EXPERIMENT_NAME", None) From 718cfb2dd1ff2a03b89e3b95f0b1aa1e04046e6e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 22 Oct 2024 13:54:24 -0400 Subject: [PATCH 0199/1405] revert image tagged as main-latest (#1990) --- .github/workflows/main.yml | 4 ++-- .github/workflows/nightlies.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3b82f6a510..47a4c7f114 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,12 +23,12 @@ jobs: python_version: "3.11" pytorch: 2.3.1 axolotl_extras: mamba-ssm + is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.4.1 axolotl_extras: - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -85,12 +85,12 @@ jobs: python_version: "3.11" pytorch: 2.3.1 axolotl_extras: + is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.4.1 axolotl_extras: - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index b2110e737d..55123a9026 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -22,12 +22,12 @@ jobs: python_version: "3.11" pytorch: 2.3.1 axolotl_extras: + is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.4.1 axolotl_extras: - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -84,12 +84,12 @@ jobs: python_version: "3.11" pytorch: 2.3.1 axolotl_extras: + is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.4.1 axolotl_extras: - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" From 1d6a5e2bd638778a42d757ff0cb600f918eb1c31 Mon Sep 17 00:00:00 2001 From: Mengqing Cao Date: Fri, 25 Oct 2024 21:06:56 +0800 Subject: [PATCH 0200/1405] Refactor func load_model to class ModelLoader (#1909) --- cicd/cicd.sh | 2 +- src/axolotl/utils/models.py | 1136 +++++++++++++++++++--------------- tests/e2e/test_load_model.py | 95 +++ tests/utils/test_models.py | 91 ++- 4 files changed, 826 insertions(+), 498 deletions(-) create mode 100644 tests/e2e/test_load_model.py diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 104a8f84ab..483d62a7ad 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -pytest --ignore=tests/e2e/ /workspace/axolotl/tests/ +pytest -n4 --ignore=tests/e2e/ /workspace/axolotl/tests/ pytest -n1 --dist loadfile -v /workspace/axolotl/tests/e2e/patched/ /workspace/axolotl/tests/e2e/integrations/ pytest --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index c18af9760f..5e53df72cb 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -324,671 +324,823 @@ def load_processor(cfg: DictDefault, tokenizer: PreTrainedTokenizerBase): return processor -def load_model( - cfg: DictDefault, - tokenizer: PreTrainedTokenizerBase, - *, - processor: ProcessorMixin = None, # pylint: disable=unused-argument - inference: bool = False, - reference_model: bool = False, - **kwargs, # pylint: disable=unused-argument -) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: +class ModelLoader: """ - Load a model for a given configuration and tokenizer. + ModelLoader: managing all the config and monkey patches while loading model """ - base_model = cfg.base_model - model_type = cfg.type_of_model - model_config = load_model_config(cfg) - - # load any patches from plugins - from axolotl.integrations.base import PluginManager + def __init__( + self, + cfg: DictDefault, + tokenizer: PreTrainedTokenizerBase, + *, + processor: ProcessorMixin = None, # pylint: disable=unused-argument + inference: bool = False, + reference_model: bool = False, + **kwargs, # pylint: disable=unused-argument + ) -> None: + self.cfg = cfg + self.tokenizer = tokenizer + self.inference: bool = inference + self.reference_model: bool = reference_model + + # init model kwargs + self.model_kwargs: Dict[str, Any] = {} + if cfg.model_kwargs: + for key, val in cfg.model_kwargs.items(): + self.model_kwargs[key] = val + + # init model + self.model: PreTrainedModel + self.base_model = cfg.base_model + self.model_type = cfg.type_of_model + + # init model config + self.model_config = load_model_config(cfg) + if cfg.is_multimodal: + self.text_model_config = self.model_config.text_config + else: + self.text_model_config = self.model_config - plugin_manager = PluginManager.get_instance() - plugin_manager.pre_model_load(cfg) + self.AutoModelLoader = AutoModelForCausalLM # pylint: disable=invalid-name - if cfg.is_multimodal: - text_model_config = model_config.text_config - else: - text_model_config = model_config + def apply_patches(self) -> None: + # load any patches from plugins + from axolotl.integrations.base import PluginManager - # TODO refactor as a kwarg - load_in_8bit = cfg.load_in_8bit + plugin_manager = PluginManager.get_instance() + plugin_manager.pre_model_load(self.cfg) - if cfg.gradient_checkpointing == "unsloth": - transformers.modeling_utils.checkpoint = hf_grad_checkpoint_unsloth_wrapper + if self.cfg.gradient_checkpointing == "unsloth": + transformers.modeling_utils.checkpoint = hf_grad_checkpoint_unsloth_wrapper - if hasattr(model_config, "model_type") and model_config.model_type == "mllama": - if cfg.flash_attention: - from axolotl.monkeypatch.attention.mllama import patch_mllama + if self.cfg.flash_attention: + self.patch_attention() - patch_mllama() + if self.cfg.sample_packing and self.cfg.s2_attention: + raise ValueError( + "Received `sample_packing=true` and `s2_attention=true`; however, \ + shifted-sparse attention does not currently support sample packing." + ) - if hasattr(model_config, "model_type") and model_config.model_type == "btlm": - if cfg.flash_attention: - from axolotl.monkeypatch.btlm_attn_hijack_flash import ( - replace_btlm_attn_with_flash_attn, + if ( + self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES + and self.cfg.flash_attention + and self.cfg.sample_packing + ): + patch_for_multipack( + self.cfg.model_config_type, + model_name=self.cfg.base_model, + is_remote_code=self.cfg.trust_remote_code, ) - replace_btlm_attn_with_flash_attn(cfg.base_model) + if self.cfg.is_llama_derived_model: + self.patch_loss() + if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: + from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora - if ( - hasattr(model_config, "model_type") - and model_config.model_type == "stablelm_epoch" - ): - if cfg.flash_attention and cfg.sample_packing: - from axolotl.monkeypatch.stablelm_attn_hijack_flash import ( - replace_stablelm_attn_with_flash_attn, + patch_self_attn_lora() + elif self.cfg.is_llama_derived_model: + self.patch_llama_derived_model() + + if ( + self.cfg.model_config_type == "mistral" + and self.cfg.flash_attn_cross_entropy_loss + ): + from axolotl.monkeypatch.mistral_attn_hijack_flash import ( + patch_mistral_cross_entropy, ) - replace_stablelm_attn_with_flash_attn(cfg.base_model) + patch_mistral_cross_entropy() - if cfg.sample_packing and cfg.s2_attention: - raise ValueError( - "Received `sample_packing=true` and `s2_attention=true`; however, \ - shifted-sparse attention does not currently support sample packing." - ) + def patch_attention(self) -> None: + if hasattr(self.model_config, "model_type"): + if self.model_config.model_type == "mllama" and self.cfg.flash_attention: + from axolotl.monkeypatch.attention.mllama import patch_mllama - if ( - cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES - and cfg.flash_attention - and cfg.sample_packing - ): - patch_for_multipack( - cfg.model_config_type, - model_name=cfg.base_model, - is_remote_code=cfg.trust_remote_code, - ) + patch_mllama() - if cfg.is_llama_derived_model: - from axolotl.monkeypatch.llama_attn_hijack_flash import ( - patch_llama_cross_entropy, - patch_llama_rms_norm, - ) + if self.model_config.model_type == "btlm": + from axolotl.monkeypatch.btlm_attn_hijack_flash import ( + replace_btlm_attn_with_flash_attn, + ) - if cfg.flash_attn_cross_entropy: - patch_llama_cross_entropy() - if cfg.flash_attn_rms_norm: - patch_llama_rms_norm() - elif cfg.unsloth_rms_norm: - from axolotl.monkeypatch.unsloth_ import patch_unsloth_layernorm - - patch_unsloth_layernorm() - if cfg.unsloth_cross_entropy_loss: - from axolotl.monkeypatch.unsloth_ import ( - integrate_cross_entropy_loss_patch, + replace_btlm_attn_with_flash_attn(self.cfg.base_model) + + if ( + self.model_config.model_type == "stablelm_epoch" + and self.cfg.sample_packing + ): + from axolotl.monkeypatch.stablelm_attn_hijack_flash import ( + replace_stablelm_attn_with_flash_attn, ) - integrate_cross_entropy_loss_patch(model_type="llama") - if cfg.unsloth_lora_qkv or cfg.unsloth_lora_o: - from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora + replace_stablelm_attn_with_flash_attn(self.cfg.base_model) + + def patch_loss(self) -> None: + """ + Patch loss functions + """ + from axolotl.monkeypatch.llama_attn_hijack_flash import ( + patch_llama_cross_entropy, + patch_llama_rms_norm, + ) + + if self.cfg.flash_attn_cross_entropy: + patch_llama_cross_entropy() + if self.cfg.flash_attn_rms_norm: + patch_llama_rms_norm() + elif self.cfg.unsloth_rms_norm: + from axolotl.monkeypatch.unsloth_ import patch_unsloth_layernorm + + patch_unsloth_layernorm() + if self.cfg.unsloth_cross_entropy_loss: + from axolotl.monkeypatch.unsloth_ import integrate_cross_entropy_loss_patch - patch_self_attn_lora() - elif cfg.is_llama_derived_model: - # Modify all llama derived models in one block + integrate_cross_entropy_loss_patch(model_type="llama") + if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: + from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora + + patch_self_attn_lora() - if cfg.flash_attention: + def patch_llama_derived_model(self) -> None: + """ + Modify all llama derived models in one block + """ + + if self.cfg.flash_attention: from axolotl.monkeypatch.llama_attn_hijack_flash import ( replace_llama_attn_with_flash_attn, ) - if cfg.sample_packing: - if cfg.device not in ["mps", "cpu"] and not inference: + if self.cfg.sample_packing: + if self.cfg.device not in ["mps", "cpu"] and not self.inference: LOG.info("patching with flash attention for sample packing") replace_llama_attn_with_flash_attn( packed=True, - cross_entropy=cfg.flash_attn_cross_entropy, - rms_norm=cfg.flash_attn_rms_norm, + cross_entropy=self.cfg.flash_attn_cross_entropy, + rms_norm=self.cfg.flash_attn_rms_norm, ) - elif cfg.s2_attention: + elif self.cfg.s2_attention: LOG.info("patching w/ flash-enabled, shifted-sparse attention") replace_llama_attn_with_flash_attn( packed=False, - cross_entropy=cfg.flash_attn_cross_entropy, - rms_norm=cfg.flash_attn_rms_norm, + cross_entropy=self.cfg.flash_attn_cross_entropy, + rms_norm=self.cfg.flash_attn_rms_norm, use_shifted_sparse_attn=True, ) - elif cfg.flash_attn_cross_entropy or cfg.flash_attn_rms_norm: + elif self.cfg.flash_attn_cross_entropy or self.cfg.flash_attn_rms_norm: replace_llama_attn_with_flash_attn( packed=False, - cross_entropy=cfg.flash_attn_cross_entropy, - rms_norm=cfg.flash_attn_rms_norm, + cross_entropy=self.cfg.flash_attn_cross_entropy, + rms_norm=self.cfg.flash_attn_rms_norm, ) - elif cfg.xformers_attention: + elif self.cfg.xformers_attention: from axolotl.monkeypatch.llama_attn_hijack_xformers import ( hijack_llama_attention, ) LOG.info("patching with xformers attention") hijack_llama_attention() - elif cfg.sample_packing: + elif self.cfg.sample_packing: from axolotl.monkeypatch.llama_patch_multipack import ( hijack_llama_prepare_4d_mask, ) LOG.info("patching llama _prepare_4d_causal_attention_mask*") hijack_llama_prepare_4d_mask() - elif cfg.s2_attention: + elif self.cfg.s2_attention: raise NotImplementedError( "Shifted-sparse attention not currently implemented without flash attention." ) - if cfg.unsloth_cross_entropy_loss: + if self.cfg.unsloth_cross_entropy_loss: from axolotl.monkeypatch.unsloth_ import integrate_cross_entropy_loss_patch integrate_cross_entropy_loss_patch(model_type="llama") - if cfg.unsloth_lora_qkv or cfg.unsloth_lora_o: + if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora patch_self_attn_lora() - # Modify mistral derived models - if cfg.model_config_type == "mistral" and cfg.flash_attn_cross_entropy_loss: - from axolotl.monkeypatch.mistral_attn_hijack_flash import ( - patch_mistral_cross_entropy, - ) - - patch_mistral_cross_entropy() - - model_kwargs: Dict[str, Any] = {} - - if cfg.model_kwargs: - for key, val in cfg.model_kwargs.items(): - model_kwargs[key] = val + def set_auto_model_loader(self) -> None: + """set self.AutoModelLoader + - default value: AutoModelForCausalLM (set at __init__) + - when using a multi modality model, self.AutoModelLoader should + be set according to model type of the model + """ + if self.cfg.is_multimodal: + if self.model_config.model_type == "llava": + self.AutoModelLoader = ( # pylint: disable=invalid-name + LlavaForConditionalGeneration + ) + elif self.model_config.model_type == "mllama": + self.AutoModelLoader = ( # pylint: disable=invalid-name + MllamaForConditionalGeneration + ) + else: + self.AutoModelLoader = ( + AutoModelForVision2Seq # pylint: disable=invalid-name + ) - max_memory = cfg.max_memory - device_map = cfg.device_map + def set_device_map_config(self) -> None: + device_map = self.cfg.device_map + max_memory = self.cfg.max_memory - AutoModelLoader = AutoModelForCausalLM # pylint: disable=invalid-name - if cfg.is_multimodal: - if model_config.model_type == "llava": - AutoModelLoader = ( # pylint: disable=invalid-name - LlavaForConditionalGeneration - ) - elif model_config.model_type == "mllama": - AutoModelLoader = ( # pylint: disable=invalid-name - MllamaForConditionalGeneration + if self.cfg.gpu_memory_limit: + gpu_memory_limit = ( + str(self.cfg.gpu_memory_limit) + "GiB" + if isinstance(self.cfg.gpu_memory_limit, int) + else self.cfg.gpu_memory_limit ) - else: - AutoModelLoader = AutoModelForVision2Seq # pylint: disable=invalid-name - - if cfg.gpu_memory_limit: - gpu_memory_limit = ( - str(cfg.gpu_memory_limit) + "GiB" - if isinstance(cfg.gpu_memory_limit, int) - else cfg.gpu_memory_limit - ) - max_memory = {} - for i in range(torch.cuda.device_count()): - max_memory[i] = gpu_memory_limit - max_memory["cpu"] = "256GiB" # something sufficiently large to fit anything + max_memory = {} + for i in range(torch.cuda.device_count()): + max_memory[i] = gpu_memory_limit + max_memory["cpu"] = "256GiB" # something sufficiently large to fit anything - if max_memory is not None: - # Based on https://github.com/togethercomputer/OpenChatKit/blob/main/inference/bot.py - from accelerate import infer_auto_device_map + if max_memory is not None: + # Based on https://github.com/togethercomputer/OpenChatKit/blob/main/inference/bot.py + from accelerate import infer_auto_device_map - with init_empty_weights(): - model_canvas = AutoModelLoader.from_config( - model_config, trust_remote_code=cfg.trust_remote_code or False + with init_empty_weights(): + model_canvas = self.AutoModelLoader.from_config( + self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + ) + model_canvas.tie_weights() + device_map = infer_auto_device_map( + model_canvas, + max_memory=max_memory, + dtype=self.cfg.torch_dtype, ) - model_canvas.tie_weights() - device_map = infer_auto_device_map( - model_canvas, - max_memory=max_memory, - dtype=cfg.torch_dtype, - ) - # We can discard max_memory now as we have a device map set up for us - max_memory = None - - model_kwargs["device_map"] = device_map - model_kwargs["torch_dtype"] = cfg.torch_dtype - - if torch.backends.mps.is_available(): - model_kwargs["device_map"] = "mps:0" - - # TODO can we put the reference model on it's own gpu? I think we have to move logits around to calculate loss - # if cfg.rl: - # if torch.cuda.device_count() > 1: - # if reference_model: - # model_kwargs["device_map"] = "cuda:" + str( - # torch.cuda.current_device() + 1 - # ) - # else: - # model_kwargs["device_map"] = "cuda:" + str(torch.cuda.current_device()) - - if is_deepspeed_zero3_enabled(): - del model_kwargs["device_map"] + # We can discard max_memory now as we have a device map set up for us + max_memory = None + + self.model_kwargs["device_map"] = device_map + self.model_kwargs["torch_dtype"] = self.cfg.torch_dtype + + if torch.backends.mps.is_available(): + self.model_kwargs["device_map"] = "mps:0" + + # TODO can we put the reference model on it's own gpu? I think we have to move logits around to calculate loss + # if cfg.rl: + # if torch.cuda.device_count() > 1: + # if reference_model: + # model_kwargs["device_map"] = "cuda:" + str( + # torch.cuda.current_device() + 1 + # ) + # else: + # model_kwargs["device_map"] = "cuda:" + str(torch.cuda.current_device()) + + if is_deepspeed_zero3_enabled(): + del self.model_kwargs["device_map"] + + def set_quantization_config(self) -> None: + self.model_kwargs["load_in_8bit"] = self.cfg.load_in_8bit + self.model_kwargs["load_in_4bit"] = self.cfg.load_in_4bit + + if self.cfg.gptq: + if not hasattr(self.model_config, "quantization_config"): + LOG.warning( + "model config does not contain quantization_config information" + ) + else: + if self.cfg.gptq_disable_exllama is not None: + self.model_config.quantization_config[ + "disable_exllama" + ] = self.cfg.gptq_disable_exllama + self.model_kwargs["quantization_config"] = GPTQConfig( + **self.model_config.quantization_config + ) + if ( + self.cfg.adapter in ["qlora", "lora"] + and hasattr(self.model_config, "quantization_config") + and self.model_config.quantization_config["quant_method"] + in ["gptq", "awq", "bitsandbytes"] + ): + if self.model_config.quantization_config["quant_method"] == "gptq": + self.model_kwargs["quantization_config"] = GPTQConfig( + **self.model_config.quantization_config + ) + elif self.model_config.quantization_config["quant_method"] == "awq": + self.model_kwargs["quantization_config"] = AwqConfig( + **self.model_config.quantization_config + ) + elif ( + self.model_config.quantization_config["quant_method"] == "bitsandbytes" + ): + self.model_kwargs["quantization_config"] = BitsAndBytesConfig( + **self.model_config.quantization_config + ) + elif self.cfg.adapter == "qlora" and ( + "load_in_4bit" in self.model_kwargs and self.model_kwargs["load_in_4bit"] + ): + bnb_config = { + "load_in_4bit": True, + "llm_int8_threshold": 6.0, + "llm_int8_has_fp16_weight": False, + "bnb_4bit_compute_dtype": self.cfg.torch_dtype, + "bnb_4bit_use_double_quant": True, + "bnb_4bit_quant_type": "nf4", + "bnb_4bit_quant_storage": torch.bfloat16, + } + if self.cfg.model_config_type in ["jamba", "qwen2_moe"] and not ( + self.cfg.deepspeed or self.cfg.fsdp + ): + # for some reason, this causes the loss to be off by an order of magnitude + # but deepspeed needs this still in bfloat16 + bnb_config["bnb_4bit_quant_storage"] = torch.float32 - if cfg.revision_of_model: - model_kwargs["revision"] = cfg.revision_of_model + if self.cfg.bnb_config_kwargs: + bnb_config.update(self.cfg.bnb_config_kwargs) - if cfg.gptq: - if not hasattr(model_config, "quantization_config"): - LOG.warning("model config does not contain quantization_config information") - else: - if cfg.gptq_disable_exllama is not None: - model_config.quantization_config[ - "disable_exllama" - ] = cfg.gptq_disable_exllama - model_kwargs["quantization_config"] = GPTQConfig( - **model_config.quantization_config + self.model_kwargs["quantization_config"] = BitsAndBytesConfig( + **bnb_config, ) - if ( - cfg.adapter in ["qlora", "lora"] - and hasattr(model_config, "quantization_config") - and model_config.quantization_config["quant_method"] - in ["gptq", "awq", "bitsandbytes"] - ): - if model_config.quantization_config["quant_method"] == "gptq": - model_kwargs["quantization_config"] = GPTQConfig( - **model_config.quantization_config + elif self.cfg.adapter == "lora" and ( + "load_in_8bit" in self.model_kwargs and self.model_kwargs["load_in_8bit"] + ): + bnb_config = { + "load_in_8bit": True, + } + # Exclude mamba blocks from int8 quantization for jamba + if self.cfg.model_config_type == "jamba": + bnb_config["llm_int8_skip_modules"] = ["mamba"] + self.model_kwargs["quantization_config"] = BitsAndBytesConfig( + **bnb_config, ) - elif model_config.quantization_config["quant_method"] == "awq": - model_kwargs["quantization_config"] = AwqConfig( - **model_config.quantization_config + + # no longer needed per https://github.com/huggingface/transformers/pull/26610 + if "quantization_config" in self.model_kwargs or self.cfg.gptq: + if "load_in_8bit" in self.model_kwargs: + del self.model_kwargs["load_in_8bit"] + if "load_in_4bit" in self.model_kwargs: + del self.model_kwargs["load_in_4bit"] + + def set_attention_config(self) -> None: + """ + sample packing uses custom FA2 patch + """ + if self.cfg.flash_attention: + if not self.cfg.sample_packing and self.cfg.s2_attention: + pass + self.model_kwargs["attn_implementation"] = "flash_attention_2" + self.model_config._attn_implementation = ( # pylint: disable=protected-access + "flash_attention_2" ) - elif model_config.quantization_config["quant_method"] == "bitsandbytes": - model_kwargs["quantization_config"] = BitsAndBytesConfig( - **model_config.quantization_config + elif self.cfg.sdp_attention: + self.model_kwargs["attn_implementation"] = "sdpa" + self.model_config._attn_implementation = ( # pylint: disable=protected-access + "sdpa" + ) + elif self.cfg.eager_attention: + self.model_kwargs["attn_implementation"] = "eager" + self.model_config._attn_implementation = ( # pylint: disable=protected-access + "eager" ) - elif cfg.adapter == "qlora" and cfg.load_in_4bit: - bnb_config = { - "load_in_4bit": True, - "llm_int8_threshold": 6.0, - "llm_int8_has_fp16_weight": False, - "bnb_4bit_compute_dtype": cfg.torch_dtype, - "bnb_4bit_use_double_quant": True, - "bnb_4bit_quant_type": "nf4", - "bnb_4bit_quant_storage": torch.bfloat16, - } - if cfg.model_config_type in ["jamba", "qwen2_moe"] and not ( - cfg.deepspeed or cfg.fsdp - ): - # for some reason, this causes the loss to be off by an order of magnitude - # but deepspeed needs this still in bfloat16 - bnb_config["bnb_4bit_quant_storage"] = torch.float32 - - if cfg.bnb_config_kwargs: - bnb_config.update(cfg.bnb_config_kwargs) - - model_kwargs["quantization_config"] = BitsAndBytesConfig( - **bnb_config, - ) - elif cfg.adapter == "lora" and cfg.load_in_8bit: - bnb_config = { - "load_in_8bit": True, - } - # Exclude mamba blocks from int8 quantization for jamba - if cfg.model_config_type == "jamba": - bnb_config["llm_int8_skip_modules"] = ["mamba"] - model_kwargs["quantization_config"] = BitsAndBytesConfig( - **bnb_config, - ) - - if cfg.load_in_8bit and cfg.adapter is not None: - model_kwargs["load_in_8bit"] = True - if cfg.load_in_4bit and cfg.adapter is not None: - model_kwargs["load_in_4bit"] = True - - # no longer needed per https://github.com/huggingface/transformers/pull/26610 - if "quantization_config" in model_kwargs or cfg.gptq: - if "load_in_8bit" in model_kwargs: - del model_kwargs["load_in_8bit"] - if "load_in_4bit" in model_kwargs: - del model_kwargs["load_in_4bit"] - - # sample packing uses custom FA2 patch - if cfg.flash_attention: - if not cfg.sample_packing and cfg.s2_attention: - pass - model_kwargs["attn_implementation"] = "flash_attention_2" - model_config._attn_implementation = ( # pylint: disable=protected-access - "flash_attention_2" - ) - elif cfg.sdp_attention: - model_kwargs["attn_implementation"] = "sdpa" - model_config._attn_implementation = "sdpa" # pylint: disable=protected-access - elif cfg.eager_attention: - model_kwargs["attn_implementation"] = "eager" - model_config._attn_implementation = "eager" # pylint: disable=protected-access - - if cfg.low_cpu_mem_usage: - model_kwargs["low_cpu_mem_usage"] = True - qlora_fsdp = cfg.fsdp and cfg.adapter == "qlora" + if self.cfg.low_cpu_mem_usage: + self.model_kwargs["low_cpu_mem_usage"] = True - try: + def build_model(self, qlora_fsdp) -> bool: skip_move_to_device = False if ( # pylint: disable=condition-evals-to-constant) - (cfg.fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading) + (self.cfg.fsdp and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading) and not qlora_fsdp and False ): - model = load_sharded_model( - base_model, - model_config, - cfg, - torch_dtype=cfg.torch_dtype, + self.model = load_sharded_model( + self.base_model, + self.model_config, + self.cfg, + torch_dtype=self.cfg.torch_dtype, ) skip_move_to_device = True elif ( qlora_fsdp - and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading - and (cfg.model_config_type == "dbrx" or cfg.qlora_sharded_model_loading) + and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading + and ( + self.cfg.model_config_type == "dbrx" + or self.cfg.qlora_sharded_model_loading + ) ): - quant_storage = cfg.torch_dtype + quant_storage = self.cfg.torch_dtype quantization_config = hasattr( - model_config, "quantization_config" - ) and getattr(model_config, "quantization_config") + self.model_config, "quantization_config" + ) and getattr(self.model_config, "quantization_config") quantization_config = ( - quantization_config or model_kwargs["quantization_config"] + quantization_config or self.model_kwargs["quantization_config"] ) - if cfg.is_multimodal: - model_config.text_config = text_model_config - model = load_sharded_model_quant( - base_model, - model_config, - cfg, + if self.cfg.is_multimodal: + self.model_config.text_config = self.text_model_config + self.model = load_sharded_model_quant( + self.base_model, + self.model_config, + self.cfg, quant_storage=quant_storage, quantization_config=quantization_config, ) skip_move_to_device = True elif ( - model_config.model_type == "llama" - and not cfg.trust_remote_code - and not cfg.gptq + self.model_config.model_type == "llama" + and not self.cfg.trust_remote_code + and not self.cfg.gptq ): - if cfg.fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: + if self.cfg.fsdp and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: skip_move_to_device = True - if "device_map" in model_kwargs: - del model_kwargs["device_map"] - - if cfg.is_multimodal: - model_config.text_config = text_model_config - model = AutoModelLoader.from_pretrained( - base_model, - config=model_config, - **model_kwargs, + if "device_map" in self.model_kwargs: + del self.model_kwargs["device_map"] + + if self.cfg.is_multimodal: + self.model_config.text_config = self.text_model_config + self.model = self.AutoModelLoader.from_pretrained( + self.base_model, + config=self.model_config, + **self.model_kwargs, ) - if cfg.flash_attention and not inference: + # TODO (MengqingCao) split these patches seperately + if self.cfg.flash_attention and not self.inference: from axolotl.monkeypatch.llama_attn_hijack_flash import ( is_xformers_swiglu_available, replace_llama_mlp_with_swiglu, replace_llama_qkv_with_fused, ) - if cfg.flash_attn_fuse_mlp and is_xformers_swiglu_available(): + if self.cfg.flash_attn_fuse_mlp and is_xformers_swiglu_available(): LOG.info("patching with SwiGLU") - replace_llama_mlp_with_swiglu(model) + replace_llama_mlp_with_swiglu(self.model) - if cfg.flash_attn_fuse_qkv: + if self.cfg.flash_attn_fuse_qkv: LOG.info("patching with fused QKV") - replace_llama_qkv_with_fused(model) - elif model_type == "MambaLMHeadModel": + replace_llama_qkv_with_fused(self.model) + elif self.model_type == "MambaLMHeadModel": # FIXME this is janky at best and hacked together to make it work MambaLMHeadModel = fix_mamba_attn_for_loss() # pylint: disable=invalid-name - model_kwargs["dtype"] = model_kwargs["torch_dtype"] - model_kwargs["device"] = torch.cuda.current_device() - del model_kwargs["torch_dtype"] - del model_kwargs["device_map"] + self.model_kwargs["dtype"] = self.model_kwargs["torch_dtype"] + self.model_kwargs["device"] = torch.cuda.current_device() + del self.model_kwargs["torch_dtype"] + del self.model_kwargs["device_map"] - model = MambaLMHeadModel.from_pretrained( - base_model, - **model_kwargs, + self.model = MambaLMHeadModel.from_pretrained( + self.base_model, + **self.model_kwargs, ) elif ( - model_type - and model_type != "AutoModelForCausalLM" - and not cfg.trust_remote_code + self.model_type + and self.model_type != "AutoModelForCausalLM" + and not self.cfg.trust_remote_code ): - if cfg.gptq: - if cfg.is_multimodal: - model_config.text_config = text_model_config - model = AutoModelLoader.from_pretrained( - base_model, - config=model_config, - trust_remote_code=cfg.trust_remote_code or False, - **model_kwargs, + if self.cfg.is_multimodal: + self.model_config.text_config = self.text_model_config + if self.cfg.gptq: + self.model = self.AutoModelLoader.from_pretrained( + self.base_model, + config=self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + **self.model_kwargs, ) else: - if cfg.is_multimodal: - model_config.text_config = text_model_config - model = getattr(transformers, model_type).from_pretrained( - base_model, - config=model_config, - trust_remote_code=cfg.trust_remote_code or False, - **model_kwargs, + self.model = getattr(transformers, self.model_type).from_pretrained( + self.base_model, + config=self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + **self.model_kwargs, ) else: # Shouldn't be a problem most of the time. will obviously error if the model doesn't support this # when training starts if ( - hasattr(text_model_config, "max_seq_len") - and text_model_config.max_seq_len - and cfg.sequence_len > model_config.max_seq_len + hasattr(self.text_model_config, "max_seq_len") + and self.text_model_config.max_seq_len + and self.cfg.sequence_len > self.text_model_config.max_seq_len ): - text_model_config.max_seq_len = cfg.sequence_len - LOG.warning(f"increasing context length to {cfg.sequence_len}") + self.text_model_config.max_seq_len = self.cfg.sequence_len + LOG.warning(f"increasing context length to {self.cfg.sequence_len}") elif ( - hasattr(text_model_config, "max_sequence_length") - and text_model_config.max_sequence_length - and cfg.sequence_len > text_model_config.max_sequence_length + hasattr(self.text_model_config, "max_sequence_length") + and self.text_model_config.max_sequence_length + and self.cfg.sequence_len > self.text_model_config.max_sequence_length ): - text_model_config.max_sequence_length = cfg.sequence_len - LOG.warning(f"increasing context length to {cfg.sequence_len}") - if cfg.gptq: - if cfg.is_multimodal: - model_config.text_config = text_model_config - model = AutoModelLoader.from_pretrained( - base_model, - config=model_config, - trust_remote_code=cfg.trust_remote_code or False, - **model_kwargs, + self.text_model_config.max_sequence_length = self.cfg.sequence_len + LOG.warning(f"increasing context length to {self.cfg.sequence_len}") + if self.cfg.gptq: + if self.cfg.is_multimodal: + self.model_config.text_config = self.text_model_config + self.model = self.AutoModelLoader.from_pretrained( + self.base_model, + config=self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + **self.model_kwargs, ) else: - if cfg.fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: + if ( + self.cfg.fsdp + and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading + ): # disabling either of these two still leads to VRAM spike before setting back down skip_move_to_device = True - if "device_map" in model_kwargs: - del model_kwargs["device_map"] - - if cfg.is_multimodal: - model_config.text_config = text_model_config - model = AutoModelLoader.from_pretrained( - base_model, - config=model_config, - trust_remote_code=cfg.trust_remote_code or False, - **model_kwargs, + if "device_map" in self.model_kwargs: + del self.model_kwargs["device_map"] + + if self.cfg.is_multimodal: + self.model_config.text_config = self.text_model_config + self.model = self.AutoModelLoader.from_pretrained( + self.base_model, + config=self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + **self.model_kwargs, ) - except Exception as err: # pylint: disable=broad-exception-caught - LOG.exception(err) - raise err - - if isinstance(model, (PeftModel, PeftModelForCausalLM)) and not qlora_fsdp: - model = model.merge_and_unload() - - embeddings_len = ( - math.ceil(len(tokenizer) / 32) * 32 - if cfg.resize_token_embeddings_to_32x - else len(tokenizer) - ) - if ( - hasattr(model, "get_input_embeddings") - and model.get_input_embeddings().num_embeddings < embeddings_len - ): - model.resize_token_embeddings(embeddings_len) - else: - model.tie_weights() + if is_deepspeed_zero3_enabled(): + skip_move_to_device = True - if ( - hasattr(model, "config") - and hasattr(model.config, "max_position_embeddings") - and model.config.max_position_embeddings - and cfg.sequence_len > model.config.max_position_embeddings - ): - LOG.warning( - f"increasing model.config.max_position_embeddings from {model.config.max_position_embeddings} to {cfg.sequence_len}" - ) - model.config.max_position_embeddings = cfg.sequence_len + return skip_move_to_device - if ( - hasattr(model, "config") - and hasattr(model.config, "bos_token_id") - and model.config.bos_token_id - and model.config.bos_token_id != tokenizer.bos_token_id - ): - model.config.bos_token_id = tokenizer.bos_token_id + def ajust_model_config(self) -> None: + if ( + hasattr(self.model, "config") + and hasattr(self.model.config, "max_position_embeddings") + and self.model.config.max_position_embeddings + and self.cfg.sequence_len > self.model.config.max_position_embeddings + ): + LOG.warning( + f"increasing model.config.max_position_embeddings from {self.model.config.max_position_embeddings} to {self.cfg.sequence_len}" + ) + self.model.config.max_position_embeddings = self.cfg.sequence_len - if ( - hasattr(model, "config") - and hasattr(model.config, "eos_token_id") - and model.config.eos_token_id - and model.config.eos_token_id != tokenizer.eos_token_id - ): - model.config.eos_token_id = tokenizer.eos_token_id - - if hasattr(model, "device") and model.device.type in ("cuda", "mps"): - log_gpu_memory_usage(LOG, "after model load", model.device) - - # make sure these are fp32 per Ramesh et al. (2021) - embedding_modules = get_linear_embedding_layers(cfg.model_config_type) - if not cfg.fsdp: - # FSDP doesn't like mixed Float and BFloat16 - for name, module in model.named_modules(): - if "norm" in name or name.endswith(".gate"): - module.to(torch.float32) - if model_config.model_type == "btlm": - # don't upcast lm_head for btlm - continue - if any(m in name for m in embedding_modules): - if hasattr(module, "weight"): - module.to(torch.float32) + if ( + hasattr(self.model, "config") + and hasattr(self.model.config, "bos_token_id") + and self.model.config.bos_token_id + and self.model.config.bos_token_id != self.tokenizer.bos_token_id + ): + self.model.config.bos_token_id = self.tokenizer.bos_token_id - needs_fa2_dtype = cfg.adapter or cfg.fsdp - skip_prepare_model_for_kbit_training = False + if ( + hasattr(self.model, "config") + and hasattr(self.model.config, "eos_token_id") + and self.model.config.eos_token_id + and self.model.config.eos_token_id != self.tokenizer.eos_token_id + ): + self.model.config.eos_token_id = self.tokenizer.eos_token_id - if is_deepspeed_zero3_enabled(): + def set_z3_leaf_modules(self) -> None: from deepspeed.utils import ( # pylint: disable=no-name-in-module set_z3_leaf_modules, ) - if cfg.model_config_type in MOE_ARCH_BLOCK: - moe_blocks = MOE_ARCH_BLOCK[cfg.model_config_type] + if self.cfg.model_config_type in MOE_ARCH_BLOCK: + moe_blocks = MOE_ARCH_BLOCK[self.cfg.model_config_type] moe_blocks = [moe_blocks] if isinstance(moe_blocks, str) else moe_blocks set_z3_leaf_modules( - model, + self.model, [ - get_module_class_from_name(model, module_name) + get_module_class_from_name(self.model, module_name) for module_name in moe_blocks ], ) - if cfg.model_config_type == "qwen" and cfg.adapter == "lora": - # Qwen doesn't play nicely with LoRA if this is enabled - skip_prepare_model_for_kbit_training = True + def prepare_model(self, qlora_fsdp) -> None: + skip_prepare_model_for_kbit_training = False + if self.cfg.model_config_type == "qwen" and self.cfg.adapter == "lora": + # Qwen doesn't play nicely with LoRA if this is enabled + skip_prepare_model_for_kbit_training = True - loftq_bits = cfg.peft and cfg.peft.loftq_config and cfg.peft.loftq_config.loftq_bits - if cfg.adapter == "lora" and loftq_bits: - skip_prepare_model_for_kbit_training = True + loftq_bits = ( + self.cfg.peft + and self.cfg.peft.loftq_config + and self.cfg.peft.loftq_config.loftq_bits + ) + if self.cfg.adapter == "lora" and loftq_bits: + skip_prepare_model_for_kbit_training = True - if qlora_fsdp or (cfg.fsdp and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading): - # make sure everything is in the same dtype - skip_prepare_model_for_kbit_training = True + if qlora_fsdp or ( + self.cfg.fsdp and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading + ): + # make sure everything is in the same dtype + skip_prepare_model_for_kbit_training = True - if is_deepspeed_zero3_enabled(): - skip_prepare_model_for_kbit_training = True + if is_deepspeed_zero3_enabled(): + skip_prepare_model_for_kbit_training = True + + is_load_in_8bit = ( + "load_in_8bit" in self.model_kwargs and self.model_kwargs["load_in_8bit"] + ) + is_load_in_4bit = ( + "load_in_4bit" in self.model_kwargs and self.model_kwargs["load_in_4bit"] + ) - if cfg.adapter in ["lora", "qlora"]: - if cfg.gradient_checkpointing: - model.gradient_checkpointing_enable( - gradient_checkpointing_kwargs=cfg.gradient_checkpointing_kwargs - ) if ( - cfg.load_in_8bit or cfg.load_in_4bit - ) and not skip_prepare_model_for_kbit_training: + not skip_prepare_model_for_kbit_training + and self.cfg.adapter in ["lora", "qlora"] + and (is_load_in_8bit or is_load_in_4bit) + ): LOG.info("converting PEFT model w/ prepare_model_for_kbit_training") - model = prepare_model_for_kbit_training( - model, use_gradient_checkpointing=cfg.gradient_checkpointing + self.model = prepare_model_for_kbit_training( + self.model, use_gradient_checkpointing=self.cfg.gradient_checkpointing ) - needs_fa2_dtype = True - # LlamaRMSNorm layers are in fp32 after kbit_training or full finetune, so we need to - # convert them back to fp16/bf16 for flash-attn compatibility. - if (needs_fa2_dtype or cfg.flash_attention) and not qlora_fsdp: - LOG.info("converting modules to %s for flash attention", cfg.torch_dtype) - for name, module in model.named_modules(): + def convert_embedding_modules_dtype( + self, embedding_modules, dist_dtype, before_kbit_train_or_finetune + ) -> None: + for name, module in self.model.named_modules(): if "norm" in name: - module.to(cfg.torch_dtype) + module.to(dist_dtype) + if before_kbit_train_or_finetune: + if name.endswith(".gate"): + module.to(dist_dtype) + if self.model_config.model_type == "btlm": + # don't upcast lm_head for btlm + continue if any(m in name for m in embedding_modules): if hasattr(module, "weight"): - module.to(cfg.torch_dtype) - - lora_config = None - if not reference_model or cfg.lora_model_dir: - # if we're not loading the reference model, then we're loading the model for training - # then the dpo trainer doesn't want the peft model loaded over it, it just wants the lora/peft config - if cfg.adapter and cfg.rl in ["dpo", "ipo", "kto"] and not cfg.merge_lora: - _, lora_config = load_lora(model, cfg, inference=False, config_only=True) + module.to(dist_dtype) + + def apply_lora_patch(self) -> None: + if self.cfg.unsloth_lora_mlp: + from axolotl.monkeypatch.unsloth_ import integrate_lora_mlp_patch + + integrate_lora_mlp_patch(self.model) + if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: + from axolotl.monkeypatch.unsloth_ import integrate_lora_patch + + integrate_lora_patch(self.model, self.cfg) + if self.cfg.unsloth_rope: + from axolotl.monkeypatch.unsloth_ import integrate_rope_embeddings + + integrate_rope_embeddings() + + def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: + self.apply_patches() + self.set_auto_model_loader() + self.set_device_map_config() + if self.cfg.revision_of_model: + self.model_kwargs["revision"] = self.cfg.revision_of_model + self.set_quantization_config() + self.set_attention_config() + + qlora_fsdp = self.cfg.fsdp and self.cfg.adapter == "qlora" + skip_move_to_device = False + + try: + skip_move_to_device = self.build_model(qlora_fsdp) + except Exception as err: # pylint: disable=broad-exception-caught + LOG.exception(err) + raise err + + if isinstance(self.model, (PeftModel, PeftModelForCausalLM)) and not qlora_fsdp: + self.model = self.model.merge_and_unload() + + embeddings_len = ( + math.ceil(len(self.tokenizer) / 32) * 32 + if self.cfg.resize_token_embeddings_to_32x + else len(self.tokenizer) + ) + if ( + hasattr(self.model, "get_input_embeddings") + and self.model.get_input_embeddings().num_embeddings < embeddings_len + ): + self.model.resize_token_embeddings(embeddings_len) else: - model, lora_config = load_adapter(model, cfg, cfg.adapter) + self.model.tie_weights() + + self.ajust_model_config() + + # log device memory usage + if hasattr(self.model, "device") and self.model.device.type in ("cuda", "mps"): + log_gpu_memory_usage(LOG, "after model load", self.model.device) + + # make sure these are fp32 per Ramesh et al. (2021) + embedding_modules = get_linear_embedding_layers(self.cfg.model_config_type) + if not self.cfg.fsdp: + # FSDP doesn't like mixed Float and BFloat16 + self.convert_embedding_modules_dtype( + embedding_modules, + dist_dtype=torch.float32, + before_kbit_train_or_finetune=True, + ) - if is_deepspeed_zero3_enabled(): - skip_move_to_device = True + if is_deepspeed_zero3_enabled(): + self.set_z3_leaf_modules() - if ( - cfg.ddp - and not load_in_8bit - and not (cfg.rl and cfg.load_in_4bit) - and not skip_move_to_device - ): - # TODO revaldate this conditional - model.to(f"cuda:{cfg.local_rank}") + needs_fa2_dtype = self.cfg.adapter or self.cfg.fsdp + if self.cfg.adapter in ["lora", "qlora"]: + needs_fa2_dtype = True + if self.cfg.gradient_checkpointing: + self.model.gradient_checkpointing_enable( + gradient_checkpointing_kwargs=self.cfg.gradient_checkpointing_kwargs + ) + + self.prepare_model(qlora_fsdp) - if torch.cuda.device_count() > 1 and int(os.getenv("WORLD_SIZE", "1")) == 1: - setattr(model, "is_parallelizable", True) - setattr(model, "model_parallel", True) + # LlamaRMSNorm layers are in fp32 after kbit_training or full finetune, so we need to + # convert them back to fp16/bf16 for flash-attn compatibility. + if (needs_fa2_dtype or self.cfg.flash_attention) and not qlora_fsdp: + LOG.info( + "converting modules to %s for flash attention", self.cfg.torch_dtype + ) + self.convert_embedding_modules_dtype( + embedding_modules, + dist_dtype=self.cfg.torch_dtype, + before_kbit_train_or_finetune=False, + ) + + # --------------------------------------------------------- + # load lora or adapter + # --------------------------------------------------------- + lora_config = None + if not self.reference_model or self.cfg.lora_model_dir: + # if we're not loading the reference model, then we're loading the model for training + # then the dpo trainer doesn't want the peft model loaded over it, it just wants the lora/peft config + if ( + self.cfg.adapter + and self.cfg.rl in ["dpo", "ipo", "kto"] + and not self.cfg.merge_lora + ): + _, lora_config = load_lora( + self.model, self.cfg, inference=False, config_only=True + ) + else: + self.model, lora_config = load_adapter( + self.model, self.cfg, self.cfg.adapter + ) - requires_grad = [] - for name, param in model.named_parameters(recurse=True): - if param.requires_grad: - requires_grad.append(f"{name}: {param.requires_grad}") - if len(requires_grad) == 0: - LOG.warning("there are no parameters that require gradient updates") - if hasattr(model, "config"): - model.config.use_cache = False + # --------------------------------------------------------- + # put model to accelerator + # --------------------------------------------------------- + is_load_in_8bit = ( + "load_in_8bit" in self.model_kwargs and self.model_kwargs["load_in_8bit"] + ) + is_load_in_4bit = ( + "load_in_4bit" in self.model_kwargs and self.model_kwargs["load_in_4bit"] + ) + if ( + self.cfg.ddp + and not is_load_in_8bit + and not (self.cfg.rl and is_load_in_4bit) + and not skip_move_to_device + ): + # TODO revaldate this conditional + self.model.to(f"cuda:{self.cfg.local_rank}") - if cfg.flash_optimum: - from optimum.bettertransformer import BetterTransformer + if torch.cuda.device_count() > 1 and int(os.getenv("WORLD_SIZE", "1")) == 1: + setattr(self.model, "is_parallelizable", True) + setattr(self.model, "model_parallel", True) - model = BetterTransformer.transform(model) + # --------------------------------------------------------- + # parameters that require gradient updates + # --------------------------------------------------------- + requires_grad = [] + for name, param in self.model.named_parameters(recurse=True): + if param.requires_grad: + requires_grad.append(f"{name}: {param.requires_grad}") + if len(requires_grad) == 0: + LOG.warning("there are no parameters that require gradient updates") + if hasattr(self.model, "config"): + self.model.config.use_cache = False - if cfg.adapter is not None: - log_gpu_memory_usage(LOG, "after adapters", model.device) + if self.cfg.flash_optimum: + from optimum.bettertransformer import BetterTransformer - if cfg.unsloth_lora_mlp: - from axolotl.monkeypatch.unsloth_ import integrate_lora_mlp_patch + self.model = BetterTransformer.transform(self.model) - integrate_lora_mlp_patch(model) - if cfg.unsloth_lora_qkv or cfg.unsloth_lora_o: - from axolotl.monkeypatch.unsloth_ import integrate_lora_patch + if self.cfg.adapter is not None: + log_gpu_memory_usage(LOG, "after adapters", self.model.device) - integrate_lora_patch(model, cfg) + self.apply_lora_patch() - if cfg.unsloth_rope: - from axolotl.monkeypatch.unsloth_ import integrate_rope_embeddings + for _ in range(3): + gc.collect() + torch.cuda.empty_cache() - integrate_rope_embeddings() + # TODO resume_from_checkpoint handling + return self.model, lora_config - for _ in range(3): - gc.collect() - torch.cuda.empty_cache() - # TODO resume_from_checkpoint handling - return model, lora_config +def load_model( + cfg: DictDefault, + tokenizer: PreTrainedTokenizerBase, + *, + processor: ProcessorMixin = None, # pylint: disable=unused-argument + inference: bool = False, + reference_model: bool = False, + **kwargs, # pylint: disable=unused-argument +) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: + """ + Load a model for a given configuration and tokenizer. + """ + loader = ModelLoader( + cfg, + tokenizer, + processor=processor, + inference=inference, + reference_model=reference_model, + **kwargs, + ) + return loader.load_model() def load_adapter(model, cfg, adapter, inference=False): diff --git a/tests/e2e/test_load_model.py b/tests/e2e/test_load_model.py new file mode 100644 index 0000000000..31a9b1a878 --- /dev/null +++ b/tests/e2e/test_load_model.py @@ -0,0 +1,95 @@ +"""Module for testing ModelLoader.""" + +import shutil +import tempfile + +import pytest +import torch + +from axolotl.utils.dict import DictDefault +from axolotl.utils.models import ModelLoader, load_model, load_tokenizer + + +@pytest.fixture(name="temp_dir") +def fixture_temp_dir(): + temp_dir = tempfile.mkdtemp() + yield temp_dir + shutil.rmtree(temp_dir) + + +class TestLoadModelUtils: + """ + Testing module testing ModelLoader. + """ + + def setup_method(self): + # load config + self.cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "tokenizer_config": "JackFram/llama-68m", + "sequence_len": 1024, + "load_in_8bit": False, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.1, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + } + ) + self.model_loader = ( # pylint: disable=attribute-defined-outside-init + ModelLoader( + cfg=self.cfg, + tokenizer="", + ) + ) + + @pytest.mark.parametrize("embedding_modules", ["embed_tokens", "lm_head"]) + @pytest.mark.parametrize( + "dist_dtype", [torch.bfloat16, torch.float16, torch.float32] + ) + @pytest.mark.parametrize("before_kbit_train_or_finetune", [True, False]) + def test_convert_embedding_modules_dtype( + self, temp_dir, embedding_modules, dist_dtype, before_kbit_train_or_finetune + ): + self.cfg.output_dir = temp_dir + self.model_loader.tokenizer = load_tokenizer(self.cfg) # pylint: disable=all + self.model_loader.model, _ = load_model( + self.cfg, + self.model_loader.tokenizer, + inference=False, + reference_model=True, + ) + self.model_loader.convert_embedding_modules_dtype( + embedding_modules, dist_dtype, before_kbit_train_or_finetune + ) + for name, module in self.model_loader.model.named_modules(): + if ( + "norm" in name + or (before_kbit_train_or_finetune and name.endswith(".gate")) + or ( + any(m in name for m in embedding_modules) + and hasattr(module, "weight") + ) + ): + for _, param in module.named_parameters(): + assert param.dtype == dist_dtype diff --git a/tests/utils/test_models.py b/tests/utils/test_models.py index e06bb6c250..31698f05fb 100644 --- a/tests/utils/test_models.py +++ b/tests/utils/test_models.py @@ -1,18 +1,64 @@ """Module for testing models utils file.""" - -import unittest -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest +from transformers import BitsAndBytesConfig, PreTrainedTokenizerBase +from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled +from transformers.utils.import_utils import is_torch_mps_available from axolotl.utils.dict import DictDefault -from axolotl.utils.models import load_model +from axolotl.utils.models import ModelLoader, load_model -class ModelsUtilsTest(unittest.TestCase): +class TestModelsUtils: """Testing module for models utils.""" + def setup_method(self) -> None: + # load config + self.cfg = DictDefault( # pylint: disable=attribute-defined-outside-init + { + "base_model": "JackFram/llama-68m", + "model_type": "LlamaForCausalLM", + "tokenizer_type": "LlamaTokenizer", + "load_in_8bit": True, + "load_in_4bit": False, + "adapter": "lora", + "flash_attention": False, + "sample_packing": True, + "device_map": "auto", + } + ) + self.tokenizer = MagicMock( # pylint: disable=attribute-defined-outside-init + spec=PreTrainedTokenizerBase + ) + self.inference = False # pylint: disable=attribute-defined-outside-init + self.reference_model = True # pylint: disable=attribute-defined-outside-init + + # init ModelLoader + self.model_loader = ( # pylint: disable=attribute-defined-outside-init + ModelLoader( + cfg=self.cfg, + tokenizer=self.tokenizer, + inference=self.inference, + reference_model=self.reference_model, + ) + ) + + def test_set_device_map_config(self): + # check device_map + device_map = self.cfg.device_map + if is_torch_mps_available(): + device_map = "mps" + self.model_loader.set_device_map_config() + if is_deepspeed_zero3_enabled(): + assert "device_map" not in self.model_loader.model_kwargs + else: + assert device_map in self.model_loader.model_kwargs["device_map"] + + # check torch_dtype + assert self.cfg.torch_dtype == self.model_loader.model_kwargs["torch_dtype"] + def test_cfg_throws_error_with_s2_attention_and_sample_packing(self): cfg = DictDefault( { @@ -35,3 +81,38 @@ def test_cfg_throws_error_with_s2_attention_and_sample_packing(self): "shifted-sparse attention does not currently support sample packing" in str(exc.value) ) + + @pytest.mark.parametrize("adapter", ["lora", "qlora", None]) + @pytest.mark.parametrize("load_in_8bit", [True, False]) + @pytest.mark.parametrize("load_in_4bit", [True, False]) + @pytest.mark.parametrize("gptq", [True, False]) + def test_set_quantization_config( + self, + adapter, + load_in_8bit, + load_in_4bit, + gptq, + ): + # init cfg as args + self.cfg.load_in_8bit = load_in_8bit + self.cfg.load_in_4bit = load_in_4bit + self.cfg.gptq = gptq + self.cfg.adapter = adapter + + self.model_loader.set_quantization_config() + if "quantization_config" in self.model_loader.model_kwargs or self.cfg.gptq: + assert not ( + hasattr(self.model_loader.model_kwargs, "load_in_8bit") + and hasattr(self.model_loader.model_kwargs, "load_in_4bit") + ) + elif load_in_8bit and self.cfg.adapter is not None: + assert self.model_loader.model_kwargs["load_in_8bit"] + elif load_in_4bit and self.cfg.adapter is not None: + assert self.model_loader.model_kwargs["load_in_4bit"] + + if (self.cfg.adapter == "qlora" and load_in_4bit) or ( + self.cfg.adapter == "lora" and load_in_8bit + ): + assert self.model_loader.model_kwargs.get( + "quantization_config", BitsAndBytesConfig + ) From 2501c1a6a3392b658fcd5d5ace3d5fb71b633afa Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 25 Oct 2024 22:28:23 +0700 Subject: [PATCH 0201/1405] Fix: Gradient Accumulation issue (#1980) * feat: support new arg num_items_in_batch * use kwargs to manage extra unknown kwargs for now * upgrade against upstream transformers main * make sure trl is on latest too * fix for upgraded trl * fix: handle trl and transformer signature change * feat: update trl to handle transformer signature * RewardDataCollatorWithPadding no longer has max_length * handle updated signature for tokenizer vs processor class * invert logic for tokenizer vs processor class * processing_class, not processor class * also handle processing class in dpo * handle model name w model card creation * upgrade transformers and add a loss check test * fix install of tbparse requirements * make sure to add tbparse to req * feat: revert kwarg to positional kwarg to be explicit --------- Co-authored-by: Wing Lian --- .github/workflows/pypi.yml | 2 +- .github/workflows/tests-nightly.yml | 3 +- .github/workflows/tests.yml | 2 +- cicd/Dockerfile.jinja | 3 +- requirements-dev.txt | 1 + requirements.txt | 4 +- src/axolotl/core/trainer_builder.py | 72 +++++++++++++--- src/axolotl/monkeypatch/unsloth_.py | 85 +++++-------------- src/axolotl/train.py | 6 +- tests/e2e/patched/test_unsloth_integration.py | 12 +-- tests/e2e/test_packing_loss.py | 74 ++++++++++++++++ 11 files changed, 168 insertions(+), 96 deletions(-) create mode 100644 tests/e2e/test_packing_loss.py diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 885239d185..04dbc6385c 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -27,7 +27,7 @@ jobs: run: | pip3 install wheel packaging pip3 install -e . - pip3 install -r requirements-tests.txt + pip3 install -r requirements-dev.txt -r requirements-tests.txt - name: Extract tag name id: tag diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 56eaae2398..90b1e23cd2 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -47,13 +47,14 @@ jobs: sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt + sed -i 's#^trl.*#trl @ git+https://github.com/huggingface/trl.git@main#' requirements.txt - name: Install dependencies run: | pip3 install --upgrade pip pip3 install --upgrade packaging pip3 install -U -e . - pip3 install -r requirements-tests.txt + pip3 install -r requirements-dev.txt -r requirements-tests.txt - name: Run tests run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 130ac6e7b6..ba50adfd35 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -62,7 +62,7 @@ jobs: run: | pip3 show torch pip3 install -U -e . - pip3 install -r requirements-tests.txt + pip3 install -r requirements-dev.txt -r requirements-tests.txt - name: Run tests run: | diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 3b082a15b0..8ce6550056 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -27,6 +27,7 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt; \ sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt; \ sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt; \ + sed -i 's#^trl.*#trl @ git+https://github.com/huggingface/trl.git@main#' requirements.txt; \ fi RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ @@ -36,7 +37,7 @@ RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ fi # So we can test the Docker image -RUN pip install -r requirements-tests.txt +RUN pip install -r requirements-dev.txt -r requirements-tests.txt # fix so that git fetch/pull from remote works RUN git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && \ diff --git a/requirements-dev.txt b/requirements-dev.txt index 4b5df167b6..dcc729d1b2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,4 @@ pre-commit black mypy types-requests +tbparse diff --git a/requirements.txt b/requirements.txt index 067be05cf2..b6e9a554e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.13.2 -transformers==4.45.2 +transformers==4.46.0 tokenizers>=0.20.1 bitsandbytes==0.44.1 accelerate==1.0.1 @@ -43,7 +43,7 @@ s3fs>=2024.5.0 gcsfs>=2024.5.0 # adlfs -trl==0.9.6 +trl @ git+https://github.com/huggingface/trl.git@31d02cfb795284591a084416b9dcb7bef5d08924 zstandard==0.22.0 fastcore diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index f05efe7b82..319ea7be59 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -7,6 +7,7 @@ import gc import importlib import importlib.util +import inspect import logging import math import os @@ -27,7 +28,6 @@ from torch.utils.data import BatchSampler, DataLoader, RandomSampler, SequentialSampler from transformers import ( EarlyStoppingCallback, - PreTrainedModel, Trainer, TrainerCallback, TrainingArguments, @@ -666,7 +666,9 @@ def get_bench_dataloader( return DataLoader(bench_dataset, **dataloader_params) # return self.accelerator.prepare(DataLoader(bench_dataset, **dataloader_params)) - def compute_loss(self, model, inputs, return_outputs=False): + def compute_loss( + self, model, inputs, return_outputs=False, num_items_in_batch=None + ): # use one's weighted cross entropy loss calc # if self.args.sample_packing: # labels = inputs.pop("labels") @@ -674,8 +676,18 @@ def compute_loss(self, model, inputs, return_outputs=False): # loss = trainer_weighted_loss(outputs, labels, shift_labels=True) # return (loss, outputs) if return_outputs else loss if self.args.orpo_alpha: - return self.orpo_compute_loss(model, inputs, return_outputs=return_outputs) - return super().compute_loss(model, inputs, return_outputs=return_outputs) + return self.orpo_compute_loss( + model, + inputs, + return_outputs=return_outputs, + num_items_in_batch=num_items_in_batch, + ) + return super().compute_loss( + model, + inputs, + return_outputs=return_outputs, + num_items_in_batch=num_items_in_batch, + ) @staticmethod def orpo_concatenate_inputs(inputs, label_pad_token=-100, pad_token=0, device=None): @@ -771,7 +783,13 @@ def orpo_compute_logps( ).squeeze(2) return torch.mul(per_token_logps, mask).sum(dim=1) / mask.sum(dim=1) - def orpo_compute_loss(self, model, inputs, return_outputs=False): + def orpo_compute_loss( + self, + model, + inputs, + return_outputs=False, + num_items_in_batch=None, # pylint: disable=unused-argument + ): concat_inputs = AxolotlTrainer.orpo_concatenate_inputs( inputs, label_pad_token=-100, @@ -898,6 +916,7 @@ def compute_loss( model, inputs, return_outputs=False, # pylint: disable=unused-argument + num_items_in_batch=None, # pylint: disable=unused-argument ): input_ids = inputs.pop("input_ids") lm_logits = model(input_ids).logits @@ -1005,18 +1024,32 @@ def push_to_hub(self, *args, **kwargs) -> str: return super().push_to_hub(*args, **kwargs) def tokenize_row( - self, feature, model: Optional[Union[PreTrainedModel, torch.nn.Module]] = None + self, + features, + processing_class, + max_prompt_length, + max_completion_length, + add_special_tokens, ) -> Dict: - res = super().tokenize_row(feature, model=model) - if self.tokenizer.bos_token_id is None and res["prompt_input_ids"][0] is None: + res = super().tokenize_row( + features, + processing_class, + max_prompt_length, + max_completion_length, + add_special_tokens, + ) + if processing_class.bos_token_id is None and res["prompt_input_ids"][0] is None: for key in res.keys(): res[key] = res[key][1:] return res def training_step( - self, model: nn.Module, inputs: Dict[str, Union[torch.Tensor, Any]] + self, + model: nn.Module, + inputs: Dict[str, Union[torch.Tensor, Any]], + num_items_in_batch=None, ) -> torch.Tensor: - loss: torch.Tensor = super().training_step(model, inputs) + loss: torch.Tensor = super().training_step(model, inputs, num_items_in_batch) gc.collect() torch.cuda.empty_cache() return loss @@ -1667,12 +1700,17 @@ def build(self, total_num_steps): return_tensors="pt", **data_collator_kwargs, ) + sig = inspect.signature(trainer_cls) + if "processing_class" in sig.parameters.keys(): + trainer_kwargs["processing_class"] = self.tokenizer + else: + trainer_kwargs["tokenizer"] = self.tokenizer + trainer = trainer_cls( model=self.model, train_dataset=self.train_dataset, eval_dataset=self.eval_dataset, args=training_args, - tokenizer=self.tokenizer, data_collator=self.build_collator(training_args, **data_collator_kwargs), callbacks=self.get_callbacks(), **trainer_kwargs, @@ -1713,6 +1751,8 @@ def build_collator( ] if self.cfg.reward_model: collator = RewardDataCollatorWithPadding + if "max_length" in kwargs: + kwargs.pop("max_length") elif use_batch_sampler_collator: if self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES: collator = V2BatchSamplerDataCollatorForSeq2Seq @@ -1915,7 +1955,7 @@ def build(self, total_num_steps): dpo_trainer_kwargs["max_length"] = self.cfg.sequence_len dpo_trainer_kwargs["max_target_length"] = None dpo_trainer_kwargs["max_prompt_length"] = self.cfg.sequence_len - dpo_trainer_kwargs["generate_during_eval"] = True + dpo_trainer_kwargs["generate_during_eval"] = self.cfg.use_wandb elif self.cfg.rl == "orpo": trainer_cls = AxolotlORPOTrainer trainer_cls_args = [self.model] @@ -1927,11 +1967,17 @@ def build(self, total_num_steps): trainer_cls_args = [self.model] else: raise ValueError(f"Unsupported RL: {self.cfg.rl}") + + sig = inspect.signature(trainer_cls) + if "processing_class" in sig.parameters.keys(): + dpo_trainer_kwargs["processing_class"] = self.tokenizer + else: + dpo_trainer_kwargs["tokenizer"] = self.tokenizer + dpo_trainer = trainer_cls( *trainer_cls_args, args=training_args, train_dataset=self.train_dataset, - tokenizer=self.tokenizer, callbacks=self.get_callbacks(), **dpo_trainer_kwargs, ) diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py index 3d42ad17f1..c8272ac735 100644 --- a/src/axolotl/monkeypatch/unsloth_.py +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -16,26 +16,6 @@ LOG = get_logger("axolotl.monkeypatch.unsloth") -ORIGINAL_CEL_CODE = """# Shift so that tokens < n predict n - shift_logits = logits[..., :-1, :].contiguous() - shift_labels = labels[..., 1:].contiguous() - # Flatten the tokens - loss_fct = CrossEntropyLoss() - shift_logits = shift_logits.view(-1, self.config.vocab_size) - shift_labels = shift_labels.view(-1) - # Enable model parallelism - shift_labels = shift_labels.to(shift_logits.device) - loss = loss_fct(shift_logits, shift_labels) -""" - -PATCHED_CEL_CODE = """shift_logits = logits[..., :-1, :].contiguous() - shift_labels = labels[..., 1:].contiguous() - loss = fast_cross_entropy_loss( - logits = shift_logits, - labels = shift_labels, - ) -""" - ORIGINAL_QKV_CODE = """ query_states = self.q_proj(hidden_states) key_states = self.k_proj(hidden_states) @@ -80,12 +60,6 @@ def get_forward_code() -> str: return forward -def check_cel_is_patchable() -> bool: - forward = get_forward_code() - forward, _ = detab_code(forward) - return ORIGINAL_CEL_CODE in forward - - def get_self_attn_code() -> str: forward = inspect.getsource(LlamaFlashAttention2.forward) return forward @@ -98,48 +72,31 @@ def check_self_attn_is_patchable() -> bool: def integrate_cross_entropy_loss_patch(model_type: str = "llama") -> None: - if model_type == "llama": - forward = get_forward_code() - LlamaForCausalLM._original_forward = forward # pylint: disable=protected-access - forward, _ = detab_code(forward) - assert ORIGINAL_CEL_CODE in forward, "Original forward code not found" + from unsloth.kernels.cross_entropy_loss import fast_cross_entropy_loss + + def UnslothForCausalLMLoss( # pylint: disable=invalid-name + logits, + labels, + vocab_size: int, # pylint: disable=unused-argument + num_items_in_batch: int = None, + ignore_index: int = -100, # pylint: disable=unused-argument + **kwargs, # pylint: disable=unused-argument + ): + # Upcast to float if we need to compute the loss to avoid potential precision issues + logits = logits.float() + # Shift so that tokens < n predict n + shift_logits = logits[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() - forward = forward.replace( - "@add_start_docstrings_to_model_forward(LLAMA_INPUTS_DOCSTRING)", "" - ) - forward = forward.replace( - "@replace_return_docstrings(output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC)", - "", - ) - forward = forward.replace(ORIGINAL_CEL_CODE, PATCHED_CEL_CODE) - forward = forward.replace( - "def forward(", - "def fast_cross_entropy_loss_forward(", - 1, + loss = fast_cross_entropy_loss( + logits=shift_logits, labels=shift_labels, n_items=num_items_in_batch ) + return loss - # load imports necessary - import transformers.models.llama.modeling_llama - - items_to_import = [] - for item in dir(transformers.models.llama.modeling_llama): - if item in forward: - items_to_import.append(item) - - exec( # pylint: disable=exec-used # nosec B102 - "from unsloth.kernels.cross_entropy_loss import fast_cross_entropy_loss", - globals(), - ) + if model_type == "llama": + from transformers.loss import loss_utils - exec( # pylint: disable=exec-used # nosec B102 - "from transformers.models.llama.modeling_llama import (" - + ", ".join(x for x in items_to_import) - + ")", - globals(), - ) - exec(forward, globals()) # pylint: disable=exec-used # nosec B102 - LOG.info("patching unsloth fast_cross_entropy_loss", main_process_only=True) - LlamaForCausalLM.forward = fast_cross_entropy_loss_forward # pylint: disable=undefined-variable # noqa: F821 + loss_utils.ForCausalLMLoss = UnslothForCausalLMLoss # type: ignore[assignment] else: raise ValueError("Unsupported model type") diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 4ce28d8a31..5fde4d3848 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -260,8 +260,10 @@ def terminate_handler(_, __, model_weakref): if not cfg.hub_model_id: try: - trainer.create_model_card(model_name=cfg.output_dir.lstrip("./")) - except AttributeError: + trainer.create_model_card( + model_name=cfg.output_dir.lstrip("./").encode("utf-8").decode("utf-8") + ) + except (AttributeError, UnicodeDecodeError): pass elif cfg.hub_model_id: # defensively push to the hub to ensure the model card is updated diff --git a/tests/e2e/patched/test_unsloth_integration.py b/tests/e2e/patched/test_unsloth_integration.py index 39c7abb1c1..8882742861 100644 --- a/tests/e2e/patched/test_unsloth_integration.py +++ b/tests/e2e/patched/test_unsloth_integration.py @@ -1,22 +1,12 @@ """Test module for checking whether the integration of Unsloth with Hugging Face Transformers is working as expected.""" import unittest -from axolotl.monkeypatch.unsloth_ import ( - check_cel_is_patchable, - check_self_attn_is_patchable, -) +from axolotl.monkeypatch.unsloth_ import check_self_attn_is_patchable class TestUnslothIntegration(unittest.TestCase): """Unsloth monkeypatch integration tests.""" - def test_is_cel_patchable(self): - # ensures the current version of transformers has loss code that matches our patching code - self.assertTrue( - check_cel_is_patchable(), - "HF transformers loss code has changed and isn't patchable", - ) - def test_is_self_attn_patchable(self): # ensures the current version of transformers has loss code that matches our patching code self.assertTrue( diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py new file mode 100644 index 0000000000..73f9e60bac --- /dev/null +++ b/tests/e2e/test_packing_loss.py @@ -0,0 +1,74 @@ +""" +E2E tests for packed training +""" + +import logging +import os +import unittest + +from tbparse import SummaryReader +from transformers.utils import is_torch_bf16_gpu_available + +from axolotl.cli import load_datasets +from axolotl.common.cli import TrainerCliArgs +from axolotl.train import train +from axolotl.utils.config import normalize_config +from axolotl.utils.dict import DictDefault + +from .utils import most_recent_subdir, with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestPackedLlama(unittest.TestCase): + """ + Test case for Packed training of llama models + """ + + @with_temp_dir + def test_loss_packed(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM-135M", + "sequence_len": 1024, + "sample_packing": True, + "flash_attention": True, + "val_set_size": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "vicgalle/alpaca-gpt4", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 2, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "max_steps": 5, + "use_tensorboard": True, + } + ) + if is_torch_bf16_gpu_available(): + cfg.bf16 = True + else: + cfg.fp16 = True + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + + tb_log_path = most_recent_subdir(temp_dir + "/runs") + event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) + reader = SummaryReader(event_file) + df = reader.scalars # pylint: disable=invalid-name + df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name + assert df.value.values[-1] < 2.0, "Loss is too high" From d3c45d27b54d44f354c0e4b2a7d5c515dd6be414 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 28 Oct 2024 07:32:49 -0400 Subject: [PATCH 0202/1405] fix zero3 (#1994) --- deepspeed_configs/zero3_bf16.json | 9 ---- .../zero3_bf16_cpuoffload_all.json | 9 ---- .../zero3_bf16_cpuoffload_params.json | 9 ---- requirements.txt | 2 +- src/axolotl/utils/models.py | 41 ++++++++++++++++++- 5 files changed, 41 insertions(+), 29 deletions(-) diff --git a/deepspeed_configs/zero3_bf16.json b/deepspeed_configs/zero3_bf16.json index 16e64d76b4..49fb757552 100644 --- a/deepspeed_configs/zero3_bf16.json +++ b/deepspeed_configs/zero3_bf16.json @@ -14,15 +14,6 @@ "bf16": { "enabled": true }, - "fp16": { - "enabled": "auto", - "auto_cast": false, - "loss_scale": 0, - "initial_scale_power": 32, - "loss_scale_window": 1000, - "hysteresis": 2, - "min_loss_scale": 1 - }, "gradient_accumulation_steps": "auto", "gradient_clipping": "auto", "train_batch_size": "auto", diff --git a/deepspeed_configs/zero3_bf16_cpuoffload_all.json b/deepspeed_configs/zero3_bf16_cpuoffload_all.json index 09ca6785b2..3ccc66db48 100644 --- a/deepspeed_configs/zero3_bf16_cpuoffload_all.json +++ b/deepspeed_configs/zero3_bf16_cpuoffload_all.json @@ -24,15 +24,6 @@ "bf16": { "enabled": true }, - "fp16": { - "enabled": "auto", - "auto_cast": false, - "loss_scale": 0, - "initial_scale_power": 32, - "loss_scale_window": 1000, - "hysteresis": 2, - "min_loss_scale": 1 - }, "gradient_accumulation_steps": "auto", "gradient_clipping": "auto", "train_batch_size": "auto", diff --git a/deepspeed_configs/zero3_bf16_cpuoffload_params.json b/deepspeed_configs/zero3_bf16_cpuoffload_params.json index 41d4a21323..fe21d35f88 100644 --- a/deepspeed_configs/zero3_bf16_cpuoffload_params.json +++ b/deepspeed_configs/zero3_bf16_cpuoffload_params.json @@ -20,15 +20,6 @@ "bf16": { "enabled": true }, - "fp16": { - "enabled": "auto", - "auto_cast": false, - "loss_scale": 0, - "initial_scale_power": 32, - "loss_scale_window": 1000, - "hysteresis": 2, - "min_loss_scale": 1 - }, "gradient_accumulation_steps": "auto", "gradient_clipping": "auto", "train_batch_size": "auto", diff --git a/requirements.txt b/requirements.txt index b6e9a554e5..6bb1aa6848 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ tokenizers>=0.20.1 bitsandbytes==0.44.1 accelerate==1.0.1 datasets==3.0.1 -deepspeed==0.14.4 +deepspeed==0.15.3 pydantic==2.6.3 addict fire diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 5e53df72cb..8b433c366b 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -40,7 +40,10 @@ PreTrainedTokenizerBase, ProcessorMixin, ) -from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled +from transformers.integrations.deepspeed import ( + HfTrainerDeepSpeedConfig, + is_deepspeed_zero3_enabled, +) from axolotl.common.architectures import MOE_ARCH_BLOCK from axolotl.models.mamba import fix_mamba_attn_for_loss @@ -705,6 +708,38 @@ def set_attention_config(self) -> None: self.model_kwargs["low_cpu_mem_usage"] = True def build_model(self, qlora_fsdp) -> bool: + def _configure_zero3_memory_efficient_loading(): + """ + Set the deepspeed config to load the model into RAM first before moving to VRAM. + + We need to return hf_ds_cfg as it needs to exist before model loading. + """ + hf_ds_cfg = None + + if os.getenv("ACCELERATE_DEEPSPEED_ZERO_STAGE") == "3": + hf_ds_cfg = HfTrainerDeepSpeedConfig(self.cfg.deepspeed) + hf_ds_cfg.fill_match( + "train_micro_batch_size_per_gpu", self.cfg.micro_batch_size + ) + hf_ds_cfg.fill_match( + "gradient_accumulation_steps", self.cfg.gradient_accumulation_steps + ) + hf_ds_cfg.fill_match( + "train_batch_size", + int(os.getenv("WORLD_SIZE", "1")) + * self.cfg.micro_batch_size + * self.cfg.gradient_accumulation_steps, + ) + if "device_map" in self.model_kwargs: + del self.model_kwargs["device_map"] + + transformers.modeling_utils.is_deepspeed_zero3_enabled = lambda: True + transformers.integrations.deepspeed.is_deepspeed_zero3_enabled = ( + lambda: True + ) + + return hf_ds_cfg + skip_move_to_device = False if ( # pylint: disable=condition-evals-to-constant) (self.cfg.fsdp and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading) @@ -753,6 +788,8 @@ def build_model(self, qlora_fsdp) -> bool: if "device_map" in self.model_kwargs: del self.model_kwargs["device_map"] + _ = _configure_zero3_memory_efficient_loading() + if self.cfg.is_multimodal: self.model_config.text_config = self.text_model_config self.model = self.AutoModelLoader.from_pretrained( @@ -846,6 +883,8 @@ def build_model(self, qlora_fsdp) -> bool: if "device_map" in self.model_kwargs: del self.model_kwargs["device_map"] + _ = _configure_zero3_memory_efficient_loading() + if self.cfg.is_multimodal: self.model_config.text_config = self.text_model_config self.model = self.AutoModelLoader.from_pretrained( From e1e0556c9951ef53ee627310bf3d248908fdf39a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 28 Oct 2024 17:02:04 -0400 Subject: [PATCH 0203/1405] add option for resizing embeddings when adding new tokens (#2000) * add option for resizing embeddings when adding new tokens * let's just be opinonated about this setting and set it to False --- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 + src/axolotl/utils/models.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 4831da3c8a..16cf312ce1 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -549,6 +549,7 @@ class Config: resume_from_checkpoint: Optional[str] = None auto_resume_from_checkpoints: Optional[bool] = None resize_token_embeddings_to_32x: Optional[bool] = None + mean_resizing_embeddings: Optional[bool] = False rl: Optional[RLType] = None reward_model: Optional[bool] = None diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 8b433c366b..97844a5bf3 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -1042,7 +1042,10 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: hasattr(self.model, "get_input_embeddings") and self.model.get_input_embeddings().num_embeddings < embeddings_len ): - self.model.resize_token_embeddings(embeddings_len) + resize_kwargs = {} + if self.cfg.mean_resizing_embeddings is not None: + resize_kwargs["mean_resizing"] = self.cfg.mean_resizing_embeddings + self.model.resize_token_embeddings(embeddings_len, **resize_kwargs) else: self.model.tie_weights() From bfc77b0f3628c8df43f974873344124b8c947c26 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 29 Oct 2024 10:14:51 +0700 Subject: [PATCH 0204/1405] =?UTF-8?q?Feat:=20Add=20support=20for=20tokeniz?= =?UTF-8?q?er=E2=80=99s=20or=20custom=20jinja=20chat=5Ftemplate=20(#1970)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow using tokenizer's default chat template with fallbacks Summary of changes: 1. Adds `tokenizer_default` as option for `chat_template` in `chat_template` prompt strategy that allows using the chat template from tokenizer's config.json 2. Allows falling back to chat templates available in axolotl if tokenizer does not have a chat template 3. Adds a mistral chat template which supports system message - taken from https://github.com/chujiezheng/chat_templates/blob/main/chat_templates/mistral-instruct.jinja --- Why? Many popular models are not trained with chatml format. As a result for the model to correctly learn chatml we have to turn on train_on_inputs which requires more compute and time. If we can use the model's already learned chat template we can just learn the output tokens --- Todo: - Write tests * Add tests * Fix lint and bug post merge from main * Add option `chat_template_jinja` to provide a jinja template * remove custom mistral template * Address review comments and add docs * Update docs/dataset-formats/conversation.qmd Co-authored-by: NanoCode012 * fix: set default to tokenizer template * Merge branch 'main' into cj_tokenizer_default_prompt_template * chore: remove redundant function * fix: re-arrange enum declaration position * fix: refactor artifact left from main merge * feat(doc): updated config with chat template options and clarified examples * chore: clarify doc * chore: added example for non-default template * chore: refactor * fix: test * fix: config being dropped and unittest to catch that * chore: lint * chore: skip duplicate * fix: rename var after merge * feat: add test for levy's dpo case * fix: remove default setting on edge case where chat template overriden in dataset section * feat: handle sharegpt deprecation better in docs * feat: add example using fallback * feat: handles chat_template requiring specific user/assistant order * fix: update test based on new defaults * fix: imported name incorrectly updated on merge * chore: lint * fix: update dummy message to prevent potential overlap with real content * fix(doc): formatting * fix: update bradleyterry to use new chat_template --------- Co-authored-by: Chirag Jain --- README.md | 2 +- docs/config.qmd | 57 ++++- docs/dataset-formats/conversation.qmd | 137 ++++++++++ src/axolotl/cli/__init__.py | 4 +- src/axolotl/core/trainer_builder.py | 4 +- .../bradley_terry/__init__.py | 2 +- .../bradley_terry/chat_template.py | 42 ++-- .../prompt_strategies/chat_template.py | 8 +- .../prompt_strategies/dpo/chat_template.py | 24 +- .../prompt_strategies/orpo/chat_template.py | 29 +-- src/axolotl/prompt_strategies/sharegpt.py | 2 +- src/axolotl/utils/chat_templates.py | 89 ++++++- src/axolotl/utils/config/__init__.py | 1 + .../config/models/input/v0_4_1/__init__.py | 129 +++++++--- src/axolotl/utils/models.py | 7 +- .../test_chat_template_utils.py | 125 +++++++++ .../prompt_strategies/test_chat_templates.py | 14 +- .../test_chat_templates_advanced.py | 26 +- .../test_dpo_chat_templates.py | 78 +++++- tests/test_validation_dataset.py | 238 ++++++++++++++++++ 20 files changed, 900 insertions(+), 118 deletions(-) create mode 100644 tests/prompt_strategies/test_chat_template_utils.py create mode 100644 tests/test_validation_dataset.py diff --git a/README.md b/README.md index 4ce7a351bb..21b954a56c 100644 --- a/README.md +++ b/README.md @@ -383,7 +383,7 @@ See [examples](examples) for quick start. It is recommended to duplicate and mod - typescript type: ... # unimplemented custom format - # fastchat conversation (deprecation soon, use chat_template) + # fastchat conversation (deprecation soon, use chat_template https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/conversation.html#chat_template) # See 'conversation' options: https://github.com/lm-sys/FastChat/blob/main/fastchat/conversation.py - path: ... type: sharegpt diff --git a/docs/config.qmd b/docs/config.qmd index 703d587753..a7bf9080bf 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -83,7 +83,7 @@ lora_on_cpu: true datasets: # HuggingFace dataset repo | s3://,gs:// path | "json" for local dataset, make sure to fill data_files - path: vicgalle/alpaca-gpt4 - # The type of prompt to use for training. [alpaca, sharegpt, gpteacher, oasst, reflection] + # The type of prompt to use for training. [alpaca, sharegpt, gpteacher, oasst, reflection] type: alpaca # format | format: (chat/instruct) | .load_ ds_type: # Optional[str] (json|arrow|parquet|text|csv) defines the datatype when path is a file data_files: # Optional[str] path to source data files @@ -124,6 +124,48 @@ datasets: # For `completion` datsets only, uses the provided field instead of `text` column field: + # Using chat template + - path: ... + # Set type to `chat_template` to use this strategy + type: chat_template + # Specify the name of the chat template to use + # The name of the chat template to use for training, following values are supported: + # - tokenizer_default: Uses the chat template that is available in the tokenizer_config.json. If the chat template is not available in the tokenizer, it will raise an error. This is the default. + # - alpaca/inst/chatml/gemma/cohere/llama3/phi_3/deepseek_v2/jamba: These chat templates are available in the axolotl codebase at src/axolotl/utils/chat_templates.py + # - tokenizer_default_fallback_*: where * is the name of the chat template to fallback to if the tokenizer does not have a chat template else default to tokenizer. E.g. tokenizer_default_fallback_chatml. + # - jinja: Uses a custom jinja template for the chat template. The custom jinja template should be provided in the chat_template_jinja field. + chat_template: tokenizer_default + # Custom jinja template for chat template. This will be only used if `chat_template` is set to `jinja` or empty (in which case chat_template is automatically set to `jinja`). + chat_template_jinja: + # The key in the data example that contains the messages. Default is "messages". + field_messages: messages + # The key in the message turn that contains the role. Default is "role". + message_field_role: role + # The key in the message turn that contains the content. Default is "content". + message_field_content: content + # Optional[Dict[str, List]]. Roles mapping for the messages. + roles: + user: ["human", "user"] + assistant: ["gpt", "assistant", "ai"] + system: ["system"] + + ## NOTE: Leaving the below empty will default to using the simple legacy tokenization strategy where only last message is trained on. + + # Optional[List[str]]. Roles to train on. The tokens from these roles will be considered for the loss. + roles_to_train: ["gpt", "assistant"] + # Optional[str]. Which EOS tokens to train on in the conversation. Possible values are: + # - all: train on all EOS tokens + # - turn: train on the EOS token at the end of each trainable turn + # - last: train on the last EOS token in the conversation + train_on_eos: last + # The key in the message turn that indicates via boolean whether tokens of a turn should be considered for training. Useful to selectively train on certain turns besides the `roles_to_train`. + message_field_training: training + # The key in the message turn that contains the training details. Useful to selectively train on certain tokens in a turn. + # The value of the key is a List[Dict] containing `begin_offset` (start character index in content), `end_offset` (end character index in content), and `train` (boolean whether to train). + # See example at `docs/dataset-formats/conversation.qmd` + message_field_training_detail: train_detail + + # If false, the datasets will not be shuffled and will keep their original order in `datasets`. # The same applies to the `test_datasets` option and the `pretraining_dataset` option. Default is true. shuffle_merged_datasets: true @@ -142,9 +184,16 @@ test_datasets: # use RL training: 'dpo', 'ipo', 'kto' rl: -# Saves the desired chat template to the tokenizer_config.json for easier inferencing -# Currently supports chatml and inst (mistral/mixtral) -chat_template: chatml +# The name of the chat template to use for training, following values are supported: +# - tokenizer_default: Uses the chat template that is available in the tokenizer_config.json. If the chat template is not available in the tokenizer, it will raise an error. This is the default value. +# - alpaca/inst/chatml/gemma/cohere/llama3/phi_3/deepseek_v2/jamba: These chat templates are available in the axolotl codebase at src/axolotl/utils/chat_templates.py +# - tokenizer_default_fallback_*: where * is the name of the chat template to fallback to. E.g. tokenizer_default_fallback_chatml. This is useful when the chat template is not available in the tokenizer. +# - jinja: Uses a custom jinja template for the chat template. The custom jinja template should be provided in the chat_template_jinja field. +# The selected chat template will be saved to the tokenizer_config.json for easier inferencing +# Note: It is recommended to set train_on_inputs to true when using a chat template that is different from the model's default chat template. +chat_template: tokenizer_default +# custom jinja template for chat template. This will be only used if chat_template is set to `jinja` or `null` (in which case chat_template is automatically set to `jinja`). Default is null. +chat_template_jinja: null # Changes the default system message default_system_message: You are a helpful assistant. Please give a long and detailed answer. # Currently only supports chatml. # Axolotl attempts to save the dataset as an arrow after packing the data together so diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 28d13c987c..c7273c5be5 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -6,6 +6,8 @@ order: 3 ## sharegpt +UPDATE: ShareGPT is being deprecated in the next release. Please see `chat_template` section below. + conversations where `from` is `human`/`gpt`. (optional: first row with role `system` to override default system prompt) ```{.json filename="data.jsonl"} @@ -69,3 +71,138 @@ creates a chat where bot is asked to tell a joke, then explain why the joke is f ```{.json filename="data.jsonl"} {"conversations": [{"title": "...", "text": "...", "explanation": "..."}]} ``` + + +## chat_template + +Chat Template strategy uses a jinja2 template that converts a list of messages into a prompt. Support using tokenizer's template, a supported template, or custom jinja2. + +```{.json filename="data.jsonl"} +{"conversations": [{"role": "...", "content": "..."}]} +``` + +See `config.qmd` for full configs and supported templates. + +### Migrating from sharegpt + +Most configs can be adapted as follows: + +```yaml +# old +chat_template: chatml +datasets: + - path: ... + type: sharegpt + conversation: chatml + +# new (if using tokenizer's chat_template) +datasets: + - path: ... + type: chat_template + + field_messages: conversations + message_field_role: from + message_field_content: value + +# new (if setting a new chat_template like chatml, gemma, etc) +chat_template: chatml +datasets: + - path: ... + type: chat_template + + field_messages: conversations + message_field_role: from + message_field_content: value +``` + +We recommend checking the below examples for other usecases. + +### Examples + +1. Using the default chat template in the tokenizer_config.json on OpenAI messages format, training on only last message. + +```yaml +datasets: + - path: ... + type: chat_template +``` + +2. Using the `gemma` chat template to override the tokenizer_config.json's chat template on OpenAI messages format, training on all assistant messages. + +```yaml +chat_template: gemma # this overwrites the tokenizer's chat_template +datasets: + - path: ... + type: chat_template + roles_to_train: ["assistant"] +``` + +3. Using the tokenizer_config.json's chat template or `chatml` as fallback if the former's chat template does not exist, on OpenAI messages format, training on all assistant messages. + +```yaml +chat_template: tokenizer_default_fallback_chatml # this overwrites the tokenizer's chat_template +datasets: + - path: ... + type: chat_template + roles_to_train: ["assistant"] +``` + +4. Using a custom jinja template on OpenAI messages format, training on all assistant messages. + +```yaml +# chat_template: jinja # `jinja` will be implied if the `chat_template_jinja` is set and this field is empty +chat_template_jinja: "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}" + +datasets: + - path: ... + type: chat_template + roles_to_train: ["assistant"] +``` + +5. (Advanced) Using fine-grained control over tokens and turns to train in a conversation + +For a data sample that looks like: + +```{.json filename="data.jsonl"} +{ + "conversations": [ + {"from": "system", "value": "You are an AI assistant.", "train": false}, + {"from": "human", "value": "Hello", "train": false}, + {"from": "assistant", "value": "Hello", "train": true}, + {"from": "human", "value": "How are you?", "train": true}, + { + "from": "assistant", + "value": "I'm doing very well, thank you!", + "train_detail": [ + {"begin_offset": 0, "end_offset": 8, "train": false}, + {"begin_offset": 9, "end_offset": 18, "train": true}, + {"begin_offset": 19, "end_offset": 30, "train": false}, + ], + }, + { + "from": "human", + "value": "I'm doing very well, thank you!", + "train": true, + }, + {"from": "assistant", "value": "Hi there!", "train": true} + ] +} +``` + +The configuration would look like: + +```yaml +datasets: + - path: ... + type: chat_template + chat_template: tokenizer_default + field_messages: conversations + message_field_role: from + message_field_content: value + roles_to_train: [] + train_on_eos: turn + message_field_training: train + message_field_training_detail: train_detail +``` + +Tip: It is not necessary to use both `message_field_training` and `message_field_training_detail` at a time. diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 77bb551f8c..52765a9b58 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -30,7 +30,7 @@ from axolotl.integrations.base import PluginManager from axolotl.logging_config import configure_logging from axolotl.train import TrainDatasetMeta -from axolotl.utils.chat_templates import chat_templates +from axolotl.utils.chat_templates import get_chat_template from axolotl.utils.comet_ import setup_comet_env_vars from axolotl.utils.config import ( normalize_cfg_datasets, @@ -272,7 +272,7 @@ def do_inference_gradio( importlib.import_module("axolotl.prompters"), prompter ) elif cfg.chat_template: - chat_template_str = chat_templates(cfg.chat_template) + chat_template_str = get_chat_template(cfg.chat_template) model = model.to(cfg.device, dtype=cfg.torch_dtype) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 319ea7be59..d125f838d3 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -63,7 +63,7 @@ log_prediction_callback_factory, ) from axolotl.utils.callbacks.lisa import lisa_callback_factory -from axolotl.utils.chat_templates import chat_templates +from axolotl.utils.chat_templates import get_chat_template from axolotl.utils.collators import ( BatchSamplerDataCollatorForSeq2Seq, DataCollatorForSeq2Seq, @@ -1594,7 +1594,7 @@ def build(self, total_num_steps): training_arguments_kwargs["model_type"] = self.cfg.model_config_type training_arguments_kwargs["pretraining"] = bool(self.cfg.pretraining_dataset) if self.cfg.chat_template: - training_arguments_kwargs["chat_template"] = chat_templates( + training_arguments_kwargs["chat_template"] = get_chat_template( self.cfg.chat_template ) diff --git a/src/axolotl/prompt_strategies/bradley_terry/__init__.py b/src/axolotl/prompt_strategies/bradley_terry/__init__.py index 849d84e458..4457c50be5 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/__init__.py +++ b/src/axolotl/prompt_strategies/bradley_terry/__init__.py @@ -6,7 +6,7 @@ from axolotl.prompt_strategies.user_defined import UserDefinedDatasetConfig -LOG = logging.getLogger("axolotl.prompt_strategies") +LOG = logging.getLogger("axolotl.prompt_strategies.bradley_terry") def load(strategy, tokenizer, cfg, ds_cfg): diff --git a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py index ccda0a4bde..fa85cdcb26 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py +++ b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py @@ -2,13 +2,18 @@ Bradley-Terry model with chat template prompt strategy. """ +import logging from typing import Any, Dict, Optional from axolotl.prompt_strategies.chat_template import ( ChatTemplatePrompter, ChatTemplateStrategy, ) -from axolotl.utils.chat_templates import chat_templates +from axolotl.utils.chat_templates import get_chat_template_from_config + +# Configure the logger +LOG = logging.getLogger("axolotl.prompt_strategies.bradley_terry.chat_template") +LOG.setLevel(logging.INFO) class BTChatTemplateStrategy(ChatTemplateStrategy): @@ -27,18 +32,24 @@ def tokenize_prompt(self, prompt): # pylint: disable=duplicate-code prompt[self.messages] = [] if prompt["system"]: - prompt[self.messages].append({"from": "system", "value": prompt["system"]}) - prompt[self.messages].append({"from": "user", "value": prompt["input"]}) - prompt[self.messages].append({"from": "assistant", "value": prompt["chosen"]}) + prompt[self.messages].append( + {"role": "system", "content": prompt["system"]} + ) + prompt[self.messages].append({"role": "user", "content": prompt["input"]}) + prompt[self.messages].append({"role": "assistant", "content": prompt["chosen"]}) chosen_tokenized = super().tokenize_prompt(prompt) self.messages = "rejected_messages" # pylint: disable=duplicate-code prompt[self.messages] = [] if prompt["system"]: - prompt[self.messages].append({"from": "system", "value": prompt["system"]}) - prompt[self.messages].append({"from": "user", "value": prompt["input"]}) - prompt[self.messages].append({"from": "assistant", "value": prompt["rejected"]}) + prompt[self.messages].append( + {"role": "system", "content": prompt["system"]} + ) + prompt[self.messages].append({"role": "user", "content": prompt["input"]}) + prompt[self.messages].append( + {"role": "assistant", "content": prompt["rejected"]} + ) rejected_tokenized = super().tokenize_prompt(prompt) return { @@ -53,15 +64,18 @@ def tokenize_prompt(self, prompt): def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): ds_cfg = ds_cfg or {} + chat_template_string = get_chat_template_from_config( + cfg=cfg, ds_cfg=ds_cfg, tokenizer=tokenizer + ) prompter_params = { "tokenizer": tokenizer, - "chat_template": chat_templates(ds_cfg.get("chat_template", "chatml")), - "message_field_role": ds_cfg.get("message_field_role", "from"), - "message_field_content": ds_cfg.get("message_field_content", "value"), - "message_field_training": ds_cfg.get("message_field_training", "training"), + "chat_template": chat_template_string, + "message_field_role": ds_cfg.get("message_field_role", "role"), + "message_field_content": ds_cfg.get("message_field_content", "content"), + "message_field_training": ds_cfg.get("message_field_training", None), "message_field_training_detail": ds_cfg.get( - "message_field_training_detail", "train_detail" + "message_field_training_detail", None ), "roles": ds_cfg.get("roles"), "drop_system_message": ds_cfg.get("drop_system_message", False), @@ -74,8 +88,8 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): strategy_params = { "train_on_inputs": cfg.train_on_inputs, "sequence_len": cfg.sequence_len, - "roles_to_train": ds_cfg.get("roles_to_train", ["gpt", "assistant"]), - "train_on_eos": ds_cfg.get("train_on_eos", "turn"), + "roles_to_train": ds_cfg.get("roles_to_train", []), + "train_on_eos": ds_cfg.get("train_on_eos", None), } strategy = BTChatTemplateStrategy( diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index c7852a707f..0946a4b8c7 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -9,7 +9,7 @@ from axolotl.prompt_tokenizers import PromptTokenizingStrategy from axolotl.prompters import IGNORE_TOKEN_ID, Prompter -from axolotl.utils.chat_templates import chat_templates +from axolotl.utils.chat_templates import get_chat_template_from_config # Configure the logger LOG = logging.getLogger("axolotl") @@ -405,10 +405,14 @@ def get_images(self, prompt): def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None, processor=None): # pylint: disable=duplicate-code ds_cfg = ds_cfg or {} + chat_template_string = get_chat_template_from_config( + cfg=cfg, ds_cfg=ds_cfg, tokenizer=tokenizer + ) + LOG.info(f"Using chat template:\n---\n{chat_template_string!s}\n---") prompter_params = { "tokenizer": tokenizer, - "chat_template": chat_templates(ds_cfg.get("chat_template", "chatml")), + "chat_template": chat_template_string, "message_field_role": ds_cfg.get("message_field_role", "role"), "message_field_content": ds_cfg.get("message_field_content", "content"), "message_field_training": ds_cfg.get("message_field_training", None), diff --git a/src/axolotl/prompt_strategies/dpo/chat_template.py b/src/axolotl/prompt_strategies/dpo/chat_template.py index e0e5eb1294..489b864851 100644 --- a/src/axolotl/prompt_strategies/dpo/chat_template.py +++ b/src/axolotl/prompt_strategies/dpo/chat_template.py @@ -2,15 +2,16 @@ DPO prompt strategies for using tokenizer chat templates. """ -from axolotl.utils.chat_templates import chat_templates +from axolotl.utils.chat_templates import extract_chat_template_args, get_chat_template def default( cfg, dataset_idx=0, **kwargs ): # pylint: disable=possibly-unused-variable,unused-argument ds_cfg = cfg["datasets"][dataset_idx] - chat_template_str = chat_templates(cfg.chat_template) - + chat_template_choice, chat_template_jinja = extract_chat_template_args( + cfg=cfg, ds_cfg=ds_cfg + ) field_messages = ds_cfg.get("field_messages", "messages") field_chosen = ds_cfg.get("field_chosen", "chosen") field_rejected = ds_cfg.get("field_rejected", "rejected") @@ -30,6 +31,12 @@ def default( role_map[source] = target def transform_fn(sample, tokenizer=None): + chat_template_string = get_chat_template( + user_choice=chat_template_choice, + jinja_template=chat_template_jinja, + tokenizer=tokenizer, + ) + messages = sample[field_messages] messages = [ { @@ -46,28 +53,29 @@ def transform_fn(sample, tokenizer=None): "role": role_map[sample[field_rejected][field_message_role]], "content": sample[field_rejected][field_message_content], } + dummy_user_message = {"role": "user", "content": "[[dummy_message]]"} result = {} result["prompt"] = tokenizer.apply_chat_template( messages, add_generation_prompt=True, - chat_template=chat_template_str, + chat_template=chat_template_string, tokenize=False, ) result["chosen"] = tokenizer.apply_chat_template( - [chosen], + [dummy_user_message, chosen], add_generation_prompt=False, - chat_template=chat_template_str, + chat_template=chat_template_string, tokenize=False, ) chosen_strip_index = result["chosen"].find(chosen["content"]) result["chosen"] = result["chosen"][chosen_strip_index:].rstrip() result["rejected"] = tokenizer.apply_chat_template( - [rejected], + [dummy_user_message, rejected], add_generation_prompt=False, - chat_template=chat_template_str, + chat_template=chat_template_string, tokenize=False, ) rejected_strip_index = result["rejected"].find(rejected["content"]) diff --git a/src/axolotl/prompt_strategies/orpo/chat_template.py b/src/axolotl/prompt_strategies/orpo/chat_template.py index bba6948568..e53a547483 100644 --- a/src/axolotl/prompt_strategies/orpo/chat_template.py +++ b/src/axolotl/prompt_strategies/orpo/chat_template.py @@ -5,7 +5,7 @@ from axolotl.prompt_tokenizers import IGNORE_INDEX, PromptTokenizingStrategy from axolotl.prompters import Prompter -from axolotl.utils.chat_templates import chat_templates +from axolotl.utils.chat_templates import get_chat_template_from_config class Message(BaseModel): @@ -28,18 +28,13 @@ def load( """ chatml transforms for datasets with system, input, chosen, rejected """ - - chat_template = chat_templates("chatml") - if ds_cfg and "chat_template" in ds_cfg: - chat_template = ds_cfg["chat_template"] - try: - chat_template = chat_templates(chat_template) - except ValueError: - pass - tokenizer.chat_template = chat_template + chat_template_string = get_chat_template_from_config( + cfg=cfg, ds_cfg=ds_cfg, tokenizer=tokenizer + ) + tokenizer.chat_template = chat_template_string return ORPOTokenizingStrategy( - ORPOPrompter(chat_template, tokenizer), + ORPOPrompter(chat_template_string, tokenizer), tokenizer, cfg.train_on_inputs, cfg.sequence_len, @@ -248,28 +243,30 @@ def build_prompt( def argilla(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument dataset_parser = ORPODatasetParsingStrategy() - chat_template_str = chat_templates(cfg.chat_template) - def transform_fn(sample, tokenizer=None): res = {} + chat_template_string = get_chat_template_from_config( + cfg=cfg, tokenizer=tokenizer + ) + res["prompt"] = tokenizer.apply_chat_template( [msg.model_dump() for msg in dataset_parser.get_prompt(sample).messages], add_generation_prompt=True, - chat_template=chat_template_str, + chat_template=chat_template_string, tokenize=False, ) prompt_str_len = len(res["prompt"]) res["chosen"] = tokenizer.apply_chat_template( [msg.model_dump() for msg in dataset_parser.get_chosen(sample).messages], add_generation_prompt=False, - chat_template=chat_template_str, + chat_template=chat_template_string, tokenize=False, )[prompt_str_len:] res["rejected"] = tokenizer.apply_chat_template( [msg.model_dump() for msg in dataset_parser.get_rejected(sample).messages], add_generation_prompt=False, - chat_template=chat_template_str, + chat_template=chat_template_string, tokenize=False, )[prompt_str_len:] diff --git a/src/axolotl/prompt_strategies/sharegpt.py b/src/axolotl/prompt_strategies/sharegpt.py index 4565c35d5d..069d243f52 100644 --- a/src/axolotl/prompt_strategies/sharegpt.py +++ b/src/axolotl/prompt_strategies/sharegpt.py @@ -62,7 +62,7 @@ def build_loader( ): def _load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): LOG.warning( - "sharegpt type support will be deprecated in the next release of Axolotl. Please use chat_template instead.", + "sharegpt type support will be deprecated in the next release of Axolotl. Please use chat_template instead. https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/conversation.html#chat_template", ) conversation = ( ds_cfg["conversation"] diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 2443f56f93..dfb3fef21a 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -2,8 +2,19 @@ This module provides functionality for selecting chat templates based on user choices. These templates are used for formatting messages in a conversation. """ +import logging +from typing import TYPE_CHECKING, Any, Dict, Optional -CHAT_TEMPLATES = { +if TYPE_CHECKING: + from transformers import PreTrainedTokenizerBase + +LOG = logging.getLogger("axolotl.utils.chat_templates") + +_JINJA_TEMPALTE_CHOICE = "jinja" +_DEFAULT_TEMPLATE_CHOICE = "tokenizer_default" +_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX = "tokenizer_default_fallback_" + +_CHAT_TEMPLATES = { "alpaca": "{% for message in messages %}{% if message['role'] == 'user' %}{{ '### Instruction: ' + message['content'] + '\n\n' }}{% elif message['role'] == 'assistant' %}{{ '### Response: ' + message['content'] + eos_token}}{% endif %}{% endfor %}", "mistral_v1": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ ' [INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # Mistral 7B V1, Mistral 7B V2, Mixtral 8x7B V1... "mistral_v2v3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # V3: Mistral 7B V3, Small, Large... @@ -21,12 +32,18 @@ } -def chat_templates(user_choice: str): +def get_chat_template( + user_choice: str, + jinja_template: Optional[str] = None, + tokenizer: Optional["PreTrainedTokenizerBase"] = None, +): """ - Finds the correct chat_template for the tokenizer_config. + Finds the correct chat_template based on the user's choice, jinja_template, and tokenizer. Args: user_choice (str): The user's choice of template. + jinja_template (Optional[str], optional): The jinja template string. Defaults to None. + tokenizer (Optional[PreTrainedTokenizerBase], optional): The tokenizer. Defaults to None. Returns: str: The chosen template string. @@ -34,13 +51,71 @@ def chat_templates(user_choice: str): Raises: ValueError: If the user_choice is not found in the templates. """ + if user_choice == _JINJA_TEMPALTE_CHOICE: + if not jinja_template: + raise ValueError( + f"`jinja_template` cannot be None when `chat_template` choice is {_JINJA_TEMPALTE_CHOICE}" + ) + return jinja_template + + if user_choice == _DEFAULT_TEMPLATE_CHOICE: + if not tokenizer: + raise ValueError( + f"`tokenizer` cannot be None when chat_template choice is {_DEFAULT_TEMPLATE_CHOICE}" + ) + if not tokenizer.chat_template: + raise ValueError( + f"`chat_template choice is {_DEFAULT_TEMPLATE_CHOICE} but tokenizer's chat_template is null. " + f"Please add a chat_template in tokenizer config" + ) + return tokenizer.chat_template + + if user_choice.startswith(_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX): + if not tokenizer: + raise ValueError( + f"`tokenizer` cannot be None when chat_template choice starts with {_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX}" + ) + if tokenizer.chat_template: + return tokenizer.chat_template - if user_choice in CHAT_TEMPLATES: - return CHAT_TEMPLATES[user_choice] + user_choice = user_choice[ + len(_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX) : + ] + LOG.warning( + f"No chat template found on tokenizer, falling back to {user_choice}. It is recommended to set --train_on_inputs to True for the model to learn this chat template." + ) + + if user_choice in _CHAT_TEMPLATES: + return _CHAT_TEMPLATES[user_choice] raise ValueError(f"Template '{user_choice}' not found.") +def extract_chat_template_args(cfg, ds_cfg: Optional[Dict[str, Any]] = None): + if ds_cfg and ds_cfg.get("chat_template"): + chat_template_choice = ds_cfg.get("chat_template") or _DEFAULT_TEMPLATE_CHOICE + chat_template_jinja = ds_cfg.get("chat_template_jinja") + else: + chat_template_choice = cfg.get("chat_template") or _DEFAULT_TEMPLATE_CHOICE + chat_template_jinja = cfg.get("chat_template_jinja") + return chat_template_choice, chat_template_jinja + + +def get_chat_template_from_config( + cfg, + ds_cfg: Optional[Dict[str, Any]] = None, + tokenizer: Optional["PreTrainedTokenizerBase"] = None, +) -> str: + chat_template_choice, chat_template_jinja = extract_chat_template_args( + cfg=cfg, ds_cfg=ds_cfg + ) + return get_chat_template( + user_choice=chat_template_choice, + jinja_template=chat_template_jinja, + tokenizer=tokenizer, + ) + + def register_chat_template(template_name: str, chat_template: str): """ Registers chat templates. @@ -50,7 +125,7 @@ def register_chat_template(template_name: str, chat_template: str): chat_template (str): The template string. """ - if template_name in CHAT_TEMPLATES: + if template_name in _CHAT_TEMPLATES: raise ValueError(f"Template '{template_name}' already exists.") - CHAT_TEMPLATES[template_name] = chat_template + _CHAT_TEMPLATES[template_name] = chat_template diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index f732db06fc..afc8c4fc41 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -228,6 +228,7 @@ def normalize_cfg_datasets(cfg): f"updating dataset {ds_cfg.path} with `chat_template: {cfg.chat_template}` to match your chat_template" ) cfg.datasets[idx].chat_template = cfg.chat_template + cfg.datasets[idx].chat_template_jinja = cfg.chat_template_jinja def validate_config(cfg: DictDefault, capabilities: Optional[dict] = None): diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 16cf312ce1..96e5330005 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -8,9 +8,16 @@ import os from enum import Enum from importlib.metadata import version -from typing import Any, Dict, List, Literal, Optional, Tuple, Union +from typing import Annotated, Any, Dict, List, Literal, Optional, Tuple, Union -from pydantic import BaseModel, Field, conlist, field_validator, model_validator +from pydantic import ( + BaseModel, + Field, + StringConstraints, + conlist, + field_validator, + model_validator, +) from transformers import SchedulerType from transformers.training_args import OptimizerNames @@ -21,6 +28,37 @@ SUPPORTED_METRICS = {"sacrebleu", "comet", "ter", "chrf", "perplexity"} +class RLType(str, Enum): + """RL trainer type configuration subset""" + + dpo = "dpo" # pylint: disable=invalid-name + ipo = "ipo" # pylint: disable=invalid-name + orpo = "orpo" # pylint: disable=invalid-name + kto = "kto" # pylint: disable=invalid-name + simpo = "simpo" # pylint: disable=invalid-name + + +class ChatTemplate(str, Enum): + """Chat templates configuration subset""" + + alpaca = "alpaca" # pylint: disable=invalid-name + chatml = "chatml" # pylint: disable=invalid-name + mistral_v1 = "mistral_v1" # pylint: disable=invalid-name + mistral_v2v3 = "mistral_v2v3" # pylint: disable=invalid-name + mistral_v3_tekken = "mistral_v3_tekken" # pylint: disable=invalid-name + gemma = "gemma" # pylint: disable=invalid-name + cohere = "cohere" # pylint: disable=invalid-name + llama3 = "llama3" # pylint: disable=invalid-name + llama3_2_vision = "llama3_2_vision" # pylint: disable=invalid-name + phi_3 = "phi_3" # pylint: disable=invalid-name + phi_35 = "phi_35" # pylint: disable=invalid-name + deepseek_v2 = "deepseek_v2" # pylint: disable=invalid-name + jamba = "jamba" # pylint: disable=invalid-name + jinja = "jinja" # pylint: disable=invalid-name + qwen_25 = "qwen_25" # pylint: disable=invalid-name + tokenizer_default = "tokenizer_default" # pylint: disable=invalid-name + + class DeprecatedParameters(BaseModel): """configurations that are deprecated""" @@ -105,13 +143,19 @@ class SFTDataset(BaseModel): input_transform: Optional[str] = None shards: Optional[int] = None conversation: Optional[str] = None - chat_template: Optional[str] = None + # Do not make this too strict or it will break the validator to choose different dataset class + chat_template: Optional[ + Union[ + ChatTemplate, + str, + ] + ] = None + chat_template_jinja: Optional[str] = None data_files: Optional[Union[str, List[str]]] = None input_format: Optional[str] = None name: Optional[str] = None ds_type: Optional[str] = None train_on_split: Optional[str] = None - field: Optional[str] = None field_human: Optional[str] = None field_model: Optional[str] = None @@ -122,13 +166,32 @@ class SFTDataset(BaseModel): message_field_training_detail: Optional[str] = None roles_to_train: Optional[List[str]] = None train_on_eos: Optional[str] = None - roles: Optional[Dict[str, List[str]]] = None drop_system_message: Optional[bool] = None - trust_remote_code: Optional[bool] = False revision: Optional[str] = None + @model_validator(mode="before") + @classmethod + def check_chat_template_config(cls, data): + # Set chat_template to tokenizer_default if not set + if data.get("type") == "chat_template" and not data.get("chat_template"): + data["chat_template"] = ChatTemplate.tokenizer_default + + # if chat_template is set to jinja, chat_template_jinja is required + if data.get("chat_template") == ChatTemplate.jinja and not data.get( + "chat_template_jinja" + ): + raise ValueError( + "chat_template_jinja is required when chat_template is set to jinja" + ) + + # If chat_template_jinja is set, set chat_template to jinja + if data.get("chat_template_jinja") and not data.get("chat_template"): + data["chat_template"] = ChatTemplate.jinja + + return data + class UserDefinedDPOType(BaseModel): """User defined typing for DPO""" @@ -174,35 +237,6 @@ class KTODataset(BaseModel): revision: Optional[str] = None -class RLType(str, Enum): - """RL trainer type configuration subset""" - - dpo = "dpo" # pylint: disable=invalid-name - ipo = "ipo" # pylint: disable=invalid-name - orpo = "orpo" # pylint: disable=invalid-name - kto = "kto" # pylint: disable=invalid-name - simpo = "simpo" # pylint: disable=invalid-name - - -class ChatTemplate(str, Enum): - """Chat templates configuration subset""" - - alpaca = "alpaca" # pylint: disable=invalid-name - chatml = "chatml" # pylint: disable=invalid-name - mistral_v1 = "mistral_v1" # pylint: disable=invalid-name - mistral_v2v3 = "mistral_v2v3" # pylint: disable=invalid-name - mistral_v3_tekken = "mistral_v3_tekken" # pylint: disable=invalid-name - gemma = "gemma" # pylint: disable=invalid-name - cohere = "cohere" # pylint: disable=invalid-name - llama3 = "llama3" # pylint: disable=invalid-name - llama3_2_vision = "llama3_2_vision" # pylint: disable=invalid-name - phi_3 = "phi_3" # pylint: disable=invalid-name - phi_35 = "phi_35" # pylint: disable=invalid-name - deepseek_v2 = "deepseek_v2" # pylint: disable=invalid-name - jamba = "jamba" # pylint: disable=invalid-name - qwen_25 = "qwen_25" # pylint: disable=invalid-name - - class LoftQConfig(BaseModel): """LoftQ configuration subset""" @@ -719,7 +753,13 @@ class Config: gpu_memory_limit: Optional[Union[int, str]] = None low_cpu_mem_usage: Optional[bool] = None - chat_template: Optional[ChatTemplate] = None + chat_template: Optional[ + Union[ + ChatTemplate, + Annotated[str, StringConstraints(pattern="^tokenizer_default_fallback_")], + ] + ] = None + chat_template_jinja: Optional[str] = None default_system_message: Optional[str] = None fix_untrained_tokens: Optional[bool] = None @@ -828,6 +868,23 @@ def check_sample_packing_w_xformers(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_chat_template_config(cls, data): + # if chat_template is set to jinja, chat_template_jinja is required + if data.get("chat_template") == ChatTemplate.jinja and not data.get( + "chat_template_jinja" + ): + raise ValueError( + "chat_template_jinja is required when chat_template is set to jinja" + ) + + # If chat_template_jinja is set, set chat_template to jinja + if data.get("chat_template_jinja") and not data.get("chat_template"): + data["chat_template"] = ChatTemplate.jinja + + return data + @model_validator(mode="before") @classmethod def check_sample_packing_wo_flash(cls, data): diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 97844a5bf3..f3386cccfa 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -53,7 +53,7 @@ ) from axolotl.prompt_tokenizers import LLAMA_DEFAULT_EOS_TOKEN from axolotl.utils.bench import log_gpu_memory_usage -from axolotl.utils.chat_templates import chat_templates +from axolotl.utils.chat_templates import get_chat_template_from_config from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import zero_only from axolotl.utils.gradient_checkpointing import hf_grad_checkpoint_unsloth_wrapper @@ -296,7 +296,10 @@ def load_tokenizer(cfg): LOG.debug(f"UNK: {tokenizer.unk_token_id} / {tokenizer.unk_token}") if cfg.chat_template: - chat_template_string = chat_templates(cfg.chat_template) + chat_template_string = get_chat_template_from_config( + cfg=cfg, + tokenizer=tokenizer, + ) if cfg.default_system_message and cfg.chat_template == "chatml": chat_template_string = chat_template_string.replace( "You are a helpful assistant.", cfg.default_system_message diff --git a/tests/prompt_strategies/test_chat_template_utils.py b/tests/prompt_strategies/test_chat_template_utils.py new file mode 100644 index 0000000000..b63c9aa179 --- /dev/null +++ b/tests/prompt_strategies/test_chat_template_utils.py @@ -0,0 +1,125 @@ +""" +Tests for utils in axolotl.utils.chat_templates +""" +import unittest + +import pytest +from transformers import AutoTokenizer + +from axolotl.utils.chat_templates import ( + _CHAT_TEMPLATES, + extract_chat_template_args, + get_chat_template, +) + + +@pytest.fixture(name="llama3_tokenizer") +def fixture_llama3_tokenizer(): + tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B") + + return tokenizer + + +class TestGetChatTemplateUtils: + """ + Tests the get_chat_template function. + """ + + def test_known_chat_template(self): + chat_template_str = get_chat_template("llama3") + assert chat_template_str == _CHAT_TEMPLATES["llama3"] + + def test_invalid_chat_template(self): + with pytest.raises(ValueError) as exc: + get_chat_template("invalid_template") + assert str(exc) == "Template 'invalid_template' not found." + + def test_tokenizer_default_no_tokenizer(self): + with pytest.raises(ValueError): + get_chat_template("tokenizer_default", tokenizer=None) + + def test_tokenizer_default_no_chat_template_on_tokenizer(self, llama3_tokenizer): + with pytest.raises(ValueError): + get_chat_template("tokenizer_default", tokenizer=llama3_tokenizer) + + def test_tokenizer_default_with_chat_template_on_tokenizer(self, llama3_tokenizer): + llama3_tokenizer.chat_template = "test_template" + chat_template_str = get_chat_template( + "tokenizer_default", tokenizer=llama3_tokenizer + ) + assert chat_template_str == "test_template" + + def test_tokenizer_default_fallback_no_tokenizer(self): + with pytest.raises(ValueError): + get_chat_template("tokenizer_default_fallback_test", tokenizer=None) + + def test_tokenizer_default_fallback_no_chat_template_on_tokenizer( + self, llama3_tokenizer + ): + chat_template_str = get_chat_template( + "tokenizer_default_fallback_chatml", tokenizer=llama3_tokenizer + ) + assert chat_template_str == get_chat_template("chatml") + + def test_tokenizer_default_fallback_with_chat_template_on_tokenizer( + self, llama3_tokenizer + ): + llama3_tokenizer.chat_template = "test_template" + chat_template_str = get_chat_template( + "tokenizer_default_fallback_chatml", tokenizer=llama3_tokenizer + ) + assert chat_template_str == "test_template" + + def test_jinja_template_mode(self): + jinja_template = "example_jinja_template" + chat_template_str = get_chat_template("jinja", jinja_template=jinja_template) + assert chat_template_str == jinja_template + + def test_jinja_template_mode_no_jinja_template(self): + with pytest.raises(ValueError): + get_chat_template("jinja", jinja_template=None) + + def test_extract_chat_template_args(self): + # No ds_cfg + chat_template_choice, chat_template_jinja = extract_chat_template_args( + cfg={"chat_template": "chatml"}, + ) + assert chat_template_choice == "chatml" + assert chat_template_jinja is None + + # ds_cfg provided + chat_template_choice, chat_template_jinja = extract_chat_template_args( + cfg={ + "chat_template": "jinja", + "chat_template_jinja": "global_jinja_template", + }, + ds_cfg={"chat_template": "llama3", "chat_template_jinja": None}, + ) + assert chat_template_choice == "llama3" + assert chat_template_jinja is None + + # ds_cfg provided with jinja template + chat_template_choice, chat_template_jinja = extract_chat_template_args( + cfg={"chat_template": "chatml", "chat_template_jinja": None}, + ds_cfg={ + "chat_template": "jinja", + "chat_template_jinja": "ds_jinja_template", + }, + ) + assert chat_template_choice == "jinja" + assert chat_template_jinja == "ds_jinja_template" + + # ds_cfg provided with no chat_template + chat_template_choice, chat_template_jinja = extract_chat_template_args( + cfg={ + "chat_template": "jinja", + "chat_template_jinja": "global_jinja_template", + }, + ds_cfg={"chat_template": None, "chat_template_jinja": "ds_jinja_template"}, + ) + assert chat_template_choice == "jinja" + assert chat_template_jinja == "global_jinja_template" + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/prompt_strategies/test_chat_templates.py b/tests/prompt_strategies/test_chat_templates.py index 20533504ce..4ec12b82cb 100644 --- a/tests/prompt_strategies/test_chat_templates.py +++ b/tests/prompt_strategies/test_chat_templates.py @@ -11,7 +11,7 @@ load, ) from axolotl.prompters import IGNORE_TOKEN_ID -from axolotl.utils.chat_templates import chat_templates +from axolotl.utils.chat_templates import get_chat_template from axolotl.utils.dict import DictDefault logging.basicConfig(level=logging.DEBUG) @@ -73,7 +73,7 @@ def test_llama3(self, llama3_tokenizer, assistant_dataset): strategy = ChatTemplateStrategy( ChatTemplatePrompter( llama3_tokenizer, - chat_template=chat_templates("llama3"), + chat_template=get_chat_template("llama3"), message_field_role="role", message_field_content="content", roles={ @@ -113,7 +113,7 @@ def test_phi35(self, phi35_tokenizer, assistant_dataset): strategy = ChatTemplateStrategy( ChatTemplatePrompter( phi35_tokenizer, - chat_template=chat_templates("phi_35"), + chat_template=get_chat_template("phi_35"), message_field_role="role", message_field_content="content", roles={ @@ -171,7 +171,7 @@ def test_llama3_with_training_data(self, llama3_tokenizer, assistant_dataset): strategy = ChatTemplateStrategy( ChatTemplatePrompter( llama3_tokenizer, - chat_template=chat_templates("llama3"), + chat_template=get_chat_template("llama3"), message_field_role="role", message_field_content="content", message_field_training="training", @@ -230,7 +230,7 @@ def test_llama3_assistant(self, llama3_tokenizer, sharegpt_dataset): # pylint: disable=duplicate-code strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=chat_templates("llama3") + llama3_tokenizer, chat_template=get_chat_template("llama3") ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -283,7 +283,7 @@ def test_llama3_human(self, llama3_tokenizer, sharegpt_dataset): # pylint: disable=duplicate-code strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=chat_templates("llama3") + llama3_tokenizer, chat_template=get_chat_template("llama3") ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -336,7 +336,7 @@ def test_llama3_system_human(self, llama3_tokenizer, basic_dataset): # pylint: disable=duplicate-code strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=chat_templates("llama3") + llama3_tokenizer, chat_template=get_chat_template("llama3") ), tokenizer=llama3_tokenizer, train_on_inputs=False, diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index 50429e3a26..be8e3ccdf9 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -12,7 +12,7 @@ ChatTemplateStrategy, ) from axolotl.prompters import IGNORE_TOKEN_ID -from axolotl.utils.chat_templates import chat_templates +from axolotl.utils.chat_templates import get_chat_template logging.basicConfig(level=logging.DEBUG) LOG = logging.getLogger("axolotl") @@ -35,7 +35,7 @@ def test_train_on_inputs_true(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with train_on_inputs=True") strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=chat_templates("llama3") + llama3_tokenizer, chat_template=get_chat_template("llama3") ), tokenizer=llama3_tokenizer, train_on_inputs=True, @@ -80,7 +80,7 @@ def test_train_on_inputs_false(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with train_on_inputs=False") strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=chat_templates("llama3") + llama3_tokenizer, chat_template=get_chat_template("llama3") ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -123,7 +123,7 @@ def test_roles_to_train_assistant_only(self, llama3_tokenizer, basic_dataset): LOG.info("Testing roles_to_train with assistant only") strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=chat_templates("llama3") + llama3_tokenizer, chat_template=get_chat_template("llama3") ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -151,7 +151,7 @@ def test_roles_to_train_all(self, llama3_tokenizer, basic_dataset): LOG.info("Testing roles_to_train with all roles") strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=chat_templates("llama3") + llama3_tokenizer, chat_template=get_chat_template("llama3") ), tokenizer=llama3_tokenizer, train_on_inputs=True, @@ -184,7 +184,7 @@ def test_empty_roles_to_train(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with empty roles_to_train") strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=chat_templates("llama3") + llama3_tokenizer, chat_template=get_chat_template("llama3") ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -205,7 +205,7 @@ def test_train_on_eos_all(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with train_on_eos='all'") strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=chat_templates("llama3") + llama3_tokenizer, chat_template=get_chat_template("llama3") ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -232,7 +232,7 @@ def test_train_on_eos_turn(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with train_on_eos='turn'") strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=chat_templates("llama3") + llama3_tokenizer, chat_template=get_chat_template("llama3") ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -282,7 +282,7 @@ def test_train_on_eos_last(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with train_on_eos='last'") strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=chat_templates("llama3") + llama3_tokenizer, chat_template=get_chat_template("llama3") ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -315,7 +315,7 @@ def test_train_on_eos_none(self, llama3_tokenizer, basic_dataset): LOG.info("Testing with train_on_eos='none'") strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=chat_templates("llama3") + llama3_tokenizer, chat_template=get_chat_template("llama3") ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -343,7 +343,7 @@ def test_drop_system_message(self, llama3_tokenizer, basic_dataset): strategy = ChatTemplateStrategy( ChatTemplatePrompter( llama3_tokenizer, - chat_template=chat_templates("llama3"), + chat_template=get_chat_template("llama3"), drop_system_message=True, ), tokenizer=llama3_tokenizer, @@ -371,7 +371,7 @@ def test_custom_roles(self, llama3_tokenizer): strategy = ChatTemplateStrategy( ChatTemplatePrompter( llama3_tokenizer, - chat_template=chat_templates("llama3"), + chat_template=get_chat_template("llama3"), roles=custom_roles, ), tokenizer=llama3_tokenizer, @@ -424,7 +424,7 @@ def test_message_field_training(self, llama3_tokenizer): strategy = ChatTemplateStrategy( ChatTemplatePrompter( llama3_tokenizer, - chat_template=chat_templates("llama3"), + chat_template=get_chat_template("llama3"), message_field_training="train", message_field_training_detail="train_detail", ), diff --git a/tests/prompt_strategies/test_dpo_chat_templates.py b/tests/prompt_strategies/test_dpo_chat_templates.py index cca48b1cf3..740edc22f2 100644 --- a/tests/prompt_strategies/test_dpo_chat_templates.py +++ b/tests/prompt_strategies/test_dpo_chat_templates.py @@ -86,6 +86,20 @@ def fixture_llama3_tokenizer(): return tokenizer +@pytest.fixture(name="phi3_tokenizer") +def fixture_phi3_tokenizer(): + tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-medium-128k-instruct") + + return tokenizer + + +@pytest.fixture(name="gemma_tokenizer") +def fixture_gemma_tokenizer(): + tokenizer = AutoTokenizer.from_pretrained("unsloth/gemma-2b-it", revision="703fb4a") + + return tokenizer + + class TestAssistantDPOChatTemplateLlama3: """ Test class for assistant style datasets with llama-3 prompts using the chat_template strategy. @@ -99,7 +113,7 @@ def test_llama3_defaults(self, llama3_tokenizer, assistant_dataset): "chat_template": "llama3", "datasets": [ { - "chat_template": "llama3", + "type": "chat_template", } ], } @@ -124,7 +138,7 @@ def test_llama3_configured(self, llama3_tokenizer, custom_assistant_dataset): "chat_template": "llama3", "datasets": [ { - "chat_template": "llama3", + "type": "chat_template", "field_messages": "conversation", "field_chosen": "better", "field_rejected": "worse", @@ -152,5 +166,65 @@ def test_llama3_configured(self, llama3_tokenizer, custom_assistant_dataset): assert result["rejected"] == "party on<|eot_id|>" +class TestAssistantDPOChatTemplatePhi3: + """ + Test class for assistant style datasets with phi-3 prompts using the tokenizer's chat_template strategy. + """ + + def test_phi3_defaults(self, phi3_tokenizer, assistant_dataset): + # pylint: disable=duplicate-code + transform_fn = default( + DictDefault( + { + "chat_template": "tokenizer_default", + "datasets": [ + { + "type": "chat_template", + } + ], + } + ) + ) + result = transform_fn(assistant_dataset[0], tokenizer=phi3_tokenizer) + assert result["prompt"] == ( + "<|user|>\nhello<|end|>\n" + + "<|assistant|>\nhello<|end|>\n" + + "<|user|>\ngoodbye<|end|>\n" + + "<|assistant|>\n" + ) + assert result["chosen"] == "goodbye<|end|>" + assert result["rejected"] == "party on<|end|>" + + +class TestAssistantDPOChatTemplateGemma: + """ + Test class for assistant style datasets with gemma prompts using the tokenizer's chat_template strategy. + """ + + def test_gemma_defaults(self, gemma_tokenizer, assistant_dataset): + # pylint: disable=duplicate-code + transform_fn = default( + DictDefault( + { + "chat_template": "tokenizer_default", + "datasets": [ + { + "type": "chat_template", + } + ], + } + ) + ) + result = transform_fn(assistant_dataset[0], tokenizer=gemma_tokenizer) + assert result["prompt"] == ( + "user\nhello\n" + + "model\nhello\n" + + "user\ngoodbye\n" + + "model\n" + ) + assert result["chosen"] == "goodbye" + assert result["rejected"] == "party on" + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_validation_dataset.py b/tests/test_validation_dataset.py new file mode 100644 index 0000000000..389424217b --- /dev/null +++ b/tests/test_validation_dataset.py @@ -0,0 +1,238 @@ +"""Module for testing the validation module for the dataset config""" + +import warnings +from typing import Optional + +import pytest + +from axolotl.utils.config import validate_config +from axolotl.utils.config.models.input.v0_4_1 import ChatTemplate +from axolotl.utils.dict import DictDefault + +warnings.filterwarnings("error") + + +@pytest.fixture(name="minimal_cfg") +def fixture_cfg(): + return DictDefault( + { + "base_model": "TinyLlama/TinyLlama-1.1B-Chat-v0.6", + "learning_rate": 0.000001, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + } + ) + + +# pylint: disable=too-many-public-methods (duplicate-code) +class BaseValidation: + """ + Base validation module to setup the log capture + """ + + _caplog: Optional[pytest.LogCaptureFixture] = None + + @pytest.fixture(autouse=True) + def inject_fixtures(self, caplog): + self._caplog = caplog + + +class TestValidationCheckDatasetConfig(BaseValidation): + """ + Test the validation for the dataset config to ensure no correct parameters are dropped + """ + + def test_dataset_config_no_drop_param(self, minimal_cfg): + cfg = DictDefault( + minimal_cfg + | { + "datasets": [ + { + "path": "LDJnr/Puffin", + "type": "sharegpt", + "conversation": "chatml", + "shards": 10, + } + ] + } + ) + + checked_cfg = validate_config(cfg) + + def _check_config(): + assert checked_cfg.datasets[0].path == cfg.datasets[0].path + assert checked_cfg.datasets[0].type == cfg.datasets[0].type + assert checked_cfg.datasets[0].conversation == cfg.datasets[0].conversation + assert checked_cfg.datasets[0].shards == cfg.datasets[0].shards + + _check_config() + + checked_cfg = validate_config( + cfg, + capabilities={ + "bf16": "false", + "n_gpu": 1, + "compute_capability": "8.0", + }, + ) + + _check_config() + + def test_dataset_default_chat_template_no_drop_param(self, minimal_cfg): + cfg = DictDefault( + minimal_cfg + | { + "datasets": [ + { + "path": "LDJnr/Puffin", + "type": "chat_template", + "field_messages": "conversations", + "shards": 10, + "message_field_role": "from", + "message_field_content": "value", + } + ], + } + ) + + checked_cfg = validate_config(cfg) + + def _check_config(): + assert checked_cfg.datasets[0].path == cfg.datasets[0].path + assert checked_cfg.datasets[0].type == cfg.datasets[0].type + assert checked_cfg.chat_template is None + assert ( + checked_cfg.datasets[0].chat_template == ChatTemplate.tokenizer_default + ) + assert ( + checked_cfg.datasets[0].field_messages == cfg.datasets[0].field_messages + ) + assert checked_cfg.datasets[0].shards == cfg.datasets[0].shards + assert ( + checked_cfg.datasets[0].message_field_role + == cfg.datasets[0].message_field_role + ) + assert ( + checked_cfg.datasets[0].message_field_content + == cfg.datasets[0].message_field_content + ) + + _check_config() + + checked_cfg = validate_config( + cfg, + capabilities={ + "bf16": "false", + "n_gpu": 1, + "compute_capability": "8.0", + }, + ) + + _check_config() + + def test_dataset_partial_default_chat_template_no_drop_param(self, minimal_cfg): + cfg = DictDefault( + minimal_cfg + | { + "chat_template": "chatml", + "datasets": [ + { + "path": "LDJnr/Puffin", + "type": "chat_template", + "field_messages": "conversations", + "shards": 10, + "message_field_role": "from", + "message_field_content": "value", + } + ], + } + ) + + checked_cfg = validate_config(cfg) + + def _check_config(): + assert checked_cfg.datasets[0].path == cfg.datasets[0].path + assert checked_cfg.datasets[0].type == cfg.datasets[0].type + assert checked_cfg.chat_template == ChatTemplate.chatml + assert ( + checked_cfg.datasets[0].chat_template == ChatTemplate.tokenizer_default + ) + assert ( + checked_cfg.datasets[0].field_messages == cfg.datasets[0].field_messages + ) + assert checked_cfg.datasets[0].shards == cfg.datasets[0].shards + assert ( + checked_cfg.datasets[0].message_field_role + == cfg.datasets[0].message_field_role + ) + assert ( + checked_cfg.datasets[0].message_field_content + == cfg.datasets[0].message_field_content + ) + + _check_config() + + checked_cfg = validate_config( + cfg, + capabilities={ + "bf16": "false", + "n_gpu": 1, + "compute_capability": "8.0", + }, + ) + + _check_config() + + def test_dataset_chatml_chat_template_no_drop_param(self, minimal_cfg): + cfg = DictDefault( + minimal_cfg + | { + "chat_template": "chatml", + "datasets": [ + { + "path": "LDJnr/Puffin", + "type": "chat_template", + "chat_template": "gemma", + "field_messages": "conversations", + "shards": 10, + "message_field_role": "from", + "message_field_content": "value", + } + ], + } + ) + + checked_cfg = validate_config(cfg) + + def _check_config(): + assert checked_cfg.datasets[0].path == cfg.datasets[0].path + assert checked_cfg.datasets[0].type == cfg.datasets[0].type + assert checked_cfg.chat_template == cfg.chat_template + assert ( + checked_cfg.datasets[0].chat_template == cfg.datasets[0].chat_template + ) + assert ( + checked_cfg.datasets[0].field_messages == cfg.datasets[0].field_messages + ) + assert checked_cfg.datasets[0].shards == cfg.datasets[0].shards + assert ( + checked_cfg.datasets[0].message_field_role + == cfg.datasets[0].message_field_role + ) + assert ( + checked_cfg.datasets[0].message_field_content + == cfg.datasets[0].message_field_content + ) + + _check_config() + + checked_cfg = validate_config( + cfg, + capabilities={ + "bf16": "false", + "n_gpu": 1, + "compute_capability": "8.0", + }, + ) + + _check_config() From 107b67b852badb4d269e8100e76b544ac220d4aa Mon Sep 17 00:00:00 2001 From: Oliver Kunc <36070570+OliverKunc@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:13:50 +0100 Subject: [PATCH 0205/1405] Hardware requirements (#1997) [skip ci] * Hardware requirements https://github.com/axolotl-ai-cloud/axolotl/issues/1992 * Update README.md --------- Co-authored-by: Wing Lian --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 21b954a56c..c12aa3bba0 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Features: Get started with Axolotl in just a few steps! This quickstart guide will walk you through setting up and running a basic fine-tuning task. -**Requirements**: Python >=3.10 and Pytorch >=2.1.1. +**Requirements**: Nvidia GPU (Ampere architecture or newer for `bf16` and Flash Attention), Python >=3.10 and PyTorch >=2.3.1. ```bash git clone https://github.com/axolotl-ai-cloud/axolotl From 8c3a727f9d60ffd3af385f90bcc3fa3a56398fe1 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 29 Oct 2024 21:26:03 +0700 Subject: [PATCH 0206/1405] feat: update yml chat_template to specify dataset field (#2001) [skip ci] * feat: update yml chat_template to specify dataset field * feat: replace sharegpt references with chat_template --- .../{dev_sharegpt.yml => dev_chat_template.yml} | 4 ++-- docs/debugging.qmd | 14 +++++++------- examples/deepseek-v2/qlora-fsdp-2_5.yaml | 5 ++++- examples/gemma2/qlora.yml | 5 ++++- examples/jamba/qlora_fsdp_large.yaml | 6 +++++- examples/llama-3/fft-8b-liger-fsdp.yaml | 4 ++++ examples/phi/lora-3.5.yaml | 1 - 7 files changed, 26 insertions(+), 13 deletions(-) rename devtools/{dev_sharegpt.yml => dev_chat_template.yml} (92%) diff --git a/devtools/dev_sharegpt.yml b/devtools/dev_chat_template.yml similarity index 92% rename from devtools/dev_sharegpt.yml rename to devtools/dev_chat_template.yml index 9c65b49dcd..9697da4b33 100644 --- a/devtools/dev_sharegpt.yml +++ b/devtools/dev_chat_template.yml @@ -7,8 +7,8 @@ load_in_8bit: true load_in_4bit: false datasets: - - path: philschmid/guanaco-sharegpt-style - type: sharegpt + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template shards: 10 val_set_size: 0 output_dir: temp_debug/axolotl_outputs/model diff --git a/docs/debugging.qmd b/docs/debugging.qmd index 1d0779b073..029549d85b 100644 --- a/docs/debugging.qmd +++ b/docs/debugging.qmd @@ -51,12 +51,12 @@ While debugging it's helpful to simplify your test scenario as much as possible. ### Background -The below example shows how to configure VSCode to debug data preprocessing of the `sharegpt` format. This is the format used when you have the following in your axolotl config: +The below example shows how to configure VSCode to debug data preprocessing of the `chat_template` format. This is the format used when you have the following in your axolotl config: ```yaml datasets: - - path: # example on HF Hub: philschmid/guanaco-sharegpt-style - type: sharegpt + - path: # example on HF Hub: fozziethebeat/alpaca_messages_2k_test + type: chat_template ``` >[!Important] @@ -83,7 +83,7 @@ If you developing on a remote host, you can easily use VSCode to debug remotely. The easiest way to get started is to modify the [.vscode/launch.json](../.vscode/launch.json) file in this project. This is just an example configuration, so you may need to modify or copy it to suit your needs. -For example, to mimic the command `cd devtools && CUDA_VISIBLE_DEVICES=0 accelerate launch -m axolotl.cli.train dev_sharegpt.yml`, you would use the below configuration[^1]. Note that we add additional flags that override the axolotl config and incorporate the tips above (see the comments). We also set the working directory to `devtools` and set the `env` variable `HF_HOME` to a temporary folder that is later partially deleted. This is because we want to delete the HF dataset cache before each run in order to ensure that the data preprocessing code is run from scratch. +For example, to mimic the command `cd devtools && CUDA_VISIBLE_DEVICES=0 accelerate launch -m axolotl.cli.train dev_chat_template.yml`, you would use the below configuration[^1]. Note that we add additional flags that override the axolotl config and incorporate the tips above (see the comments). We also set the working directory to `devtools` and set the `env` variable `HF_HOME` to a temporary folder that is later partially deleted. This is because we want to delete the HF dataset cache before each run in order to ensure that the data preprocessing code is run from scratch. ```jsonc // .vscode/launch.json @@ -91,12 +91,12 @@ For example, to mimic the command `cd devtools && CUDA_VISIBLE_DEVICES=0 acceler "version": "0.2.0", "configurations": [ { - "name": "Debug axolotl prompt - sharegpt", + "name": "Debug axolotl prompt - chat_template", "type": "python", "module": "accelerate.commands.launch", "request": "launch", "args": [ - "-m", "axolotl.cli.train", "dev_sharegpt.yml", + "-m", "axolotl.cli.train", "dev_chat_template.yml", // The flags below simplify debugging by overriding the axolotl config // with the debugging tips above. Modify as needed. "--dataset_processes=1", // limits data preprocessing to one process @@ -240,6 +240,6 @@ style="border-radius: 10px; display: block; margin: auto;" width="560" height="3
-[^1]: The config actually mimics the command `CUDA_VISIBLE_DEVICES=0 python -m accelerate.commands.launch -m axolotl.cli.train devtools/sharegpt.yml`, but this is the same thing. +[^1]: The config actually mimics the command `CUDA_VISIBLE_DEVICES=0 python -m accelerate.commands.launch -m axolotl.cli.train devtools/chat_template.yml`, but this is the same thing. [^2]: Many of the below flags are recommended best practices by Nvidia when using nvidia-container-toolkit. You can read more about these flags [here](https://docs.nvidia.com/deeplearning/frameworks/user-guide/index.html). diff --git a/examples/deepseek-v2/qlora-fsdp-2_5.yaml b/examples/deepseek-v2/qlora-fsdp-2_5.yaml index 6e82062d66..0320e02138 100644 --- a/examples/deepseek-v2/qlora-fsdp-2_5.yaml +++ b/examples/deepseek-v2/qlora-fsdp-2_5.yaml @@ -16,7 +16,10 @@ chat_template: deepseek_v2 datasets: - path: mlabonne/FineTome-100k type: chat_template - split: train + split: train[:20%] + field_messages: conversations + message_field_role: from + message_field_content: value dataset_prepared_path: last_run_prepared val_set_size: 0.0 diff --git a/examples/gemma2/qlora.yml b/examples/gemma2/qlora.yml index b6dd653750..00e6d84e0d 100644 --- a/examples/gemma2/qlora.yml +++ b/examples/gemma2/qlora.yml @@ -11,8 +11,11 @@ chat_template: gemma datasets: - path: cgato/SlimOrcaDedupCleaned type: chat_template - chat_template: gemma drop_system_message: true + field_messages: conversations + message_field_role: from + message_field_content: value + val_set_size: 0.0 output_dir: ./outputs/out diff --git a/examples/jamba/qlora_fsdp_large.yaml b/examples/jamba/qlora_fsdp_large.yaml index 28316efd57..84cf906422 100644 --- a/examples/jamba/qlora_fsdp_large.yaml +++ b/examples/jamba/qlora_fsdp_large.yaml @@ -4,11 +4,15 @@ tokenizer_type: AutoTokenizer load_in_4bit: true strict: false use_tensorboard: true +chat_template: jamba datasets: - path: cgato/SlimOrcaDedupCleaned type: chat_template - chat_template: jamba drop_system_message: true + field_messages: conversations + message_field_role: from + message_field_content: value + dataset_prepared_path: last_run_prepared val_set_size: 0.0 output_dir: jamba-large-fsdp-qlora-ft diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml index e84d221f85..99ba63fcc6 100644 --- a/examples/llama-3/fft-8b-liger-fsdp.yaml +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -14,6 +14,10 @@ datasets: - path: mlabonne/FineTome-100k type: chat_template split: train[:20%] + field_messages: conversations + message_field_role: from + message_field_content: value + dataset_prepared_path: last_run_prepared val_set_size: 0.02 output_dir: ./outputs/out diff --git a/examples/phi/lora-3.5.yaml b/examples/phi/lora-3.5.yaml index 59d667b8db..246701148c 100644 --- a/examples/phi/lora-3.5.yaml +++ b/examples/phi/lora-3.5.yaml @@ -10,7 +10,6 @@ chat_template: phi_3 datasets: - path: fozziethebeat/alpaca_messages_2k_test type: chat_template - chat_template: phi_3 field_messages: messages message_field_role: role message_field_content: content From 32c60765ef6649716e1906c350e396b50890b847 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 30 Oct 2024 12:27:04 -0400 Subject: [PATCH 0207/1405] remove skipped test (#2002) * remove skipped test * use mean_resizing_embeddings with qlora and added tokens * use as pad_token to prevent resize of embeddings * make sure local hub test saves to a tmp dir * use Path so concatenation works * make sure to use tmp_ds_path for data files --- tests/e2e/multigpu/test_llama.py | 4 +- tests/test_datasets.py | 71 ++++++++++++++++---------------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 957a6a9e36..14e3f733eb 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -273,7 +273,6 @@ def test_fsdp_packed(self, temp_dir): ] ) - @pytest.mark.skip("disabled due to upstream issue") @with_temp_dir def test_fsdp_qlora_prequant_packed(self, temp_dir): # pylint: disable=duplicate-code @@ -282,6 +281,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "base_model": "axolotl-ai-co/TinyLlama_v1.1-bnb-nf4-bf16", "tokenizer_type": "AutoTokenizer", "adapter": "qlora", + "mean_resizing_embeddings": True, "load_in_4bit": True, "lora_r": 8, "lora_alpha": 16, @@ -297,7 +297,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "sequence_len": 2048, "val_set_size": 0.05, "special_tokens": { - "pad_token": "<|end_of_text|>", + "pad_token": "", }, "datasets": [ { diff --git a/tests/test_datasets.py b/tests/test_datasets.py index f8b463a03e..8e2955414e 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -367,43 +367,44 @@ def test_load_hub_with_revision_with_dpo(self): def test_load_local_hub_with_revision(self): """Verify that a local copy of a hub dataset can be loaded with a specific revision""" with tempfile.TemporaryDirectory() as tmp_dir: - tmp_ds_path = Path("mhenrichsen/alpaca_2k_test") - tmp_ds_path.mkdir(parents=True, exist_ok=True) - snapshot_download( - repo_id="mhenrichsen/alpaca_2k_test", - repo_type="dataset", - local_dir=tmp_ds_path, - revision="d05c1cb", - ) - - prepared_path = Path(tmp_dir) / "prepared" - cfg = DictDefault( - { - "tokenizer_config": "huggyllama/llama-7b", - "sequence_len": 1024, - "datasets": [ - { - "path": "mhenrichsen/alpaca_2k_test", - "ds_type": "parquet", - "type": "alpaca", - "data_files": [ - "mhenrichsen/alpaca_2k_test/alpaca_2000.parquet", - ], - "revision": "d05c1cb", - }, - ], - } - ) + with tempfile.TemporaryDirectory() as tmp_dir2: + tmp_ds_path = Path(tmp_dir2) / "mhenrichsen/alpaca_2k_test" + tmp_ds_path.mkdir(parents=True, exist_ok=True) + snapshot_download( + repo_id="mhenrichsen/alpaca_2k_test", + repo_type="dataset", + local_dir=tmp_ds_path, + revision="d05c1cb", + ) + + prepared_path = Path(tmp_dir) / "prepared" + cfg = DictDefault( + { + "tokenizer_config": "huggyllama/llama-7b", + "sequence_len": 1024, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "ds_type": "parquet", + "type": "alpaca", + "data_files": [ + f"{tmp_ds_path}/alpaca_2000.parquet", + ], + "revision": "d05c1cb", + }, + ], + } + ) - dataset, _ = load_tokenized_prepared_datasets( - self.tokenizer, cfg, prepared_path - ) + dataset, _ = load_tokenized_prepared_datasets( + self.tokenizer, cfg, prepared_path + ) - assert len(dataset) == 2000 - assert "input_ids" in dataset.features - assert "attention_mask" in dataset.features - assert "labels" in dataset.features - shutil.rmtree(tmp_ds_path) + assert len(dataset) == 2000 + assert "input_ids" in dataset.features + assert "attention_mask" in dataset.features + assert "labels" in dataset.features + shutil.rmtree(tmp_ds_path) if __name__ == "__main__": From e62554c419d19ddbcde1a641d1c0d440a3147578 Mon Sep 17 00:00:00 2001 From: "Geun, Lim" Date: Thu, 31 Oct 2024 01:30:12 +0900 Subject: [PATCH 0208/1405] feat: add Exaone3 chat_template (#1995) --- src/axolotl/utils/chat_templates.py | 1 + src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index dfb3fef21a..5a080c1e6f 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -29,6 +29,7 @@ "deepseek_v2": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %}", "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', "qwen_25": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n", + "exaone": "{% for message in messages %}{% if loop.first and message['role'] != 'system' %}{{ '[|system|][|endofturn|]\n' }}{% endif %}{{ '[|' + message['role'] + '|]' + message['content'] }}{% if message['role'] == 'user' %}{{ '\n' }}{% else %}{{ '[|endofturn|]\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '[|assistant|]' }}{% endif %}", } diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 96e5330005..2e5749230c 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -57,6 +57,7 @@ class ChatTemplate(str, Enum): jinja = "jinja" # pylint: disable=invalid-name qwen_25 = "qwen_25" # pylint: disable=invalid-name tokenizer_default = "tokenizer_default" # pylint: disable=invalid-name + exaone = "exaone" # pylint: disable=invalid-name class DeprecatedParameters(BaseModel): From 74db2a1baeb3021f7b93f3a9581d84c843d11835 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Wed, 30 Oct 2024 23:57:00 +0530 Subject: [PATCH 0209/1405] Fix get_chat_template call for trainer builder (#2003) --- src/axolotl/cli/__init__.py | 2 +- src/axolotl/core/trainer_builder.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 52765a9b58..84586ccc37 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -272,7 +272,7 @@ def do_inference_gradio( importlib.import_module("axolotl.prompters"), prompter ) elif cfg.chat_template: - chat_template_str = get_chat_template(cfg.chat_template) + chat_template_str = get_chat_template(cfg.chat_template, tokenizer=tokenizer) model = model.to(cfg.device, dtype=cfg.torch_dtype) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index d125f838d3..e47c09d514 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1595,7 +1595,8 @@ def build(self, total_num_steps): training_arguments_kwargs["pretraining"] = bool(self.cfg.pretraining_dataset) if self.cfg.chat_template: training_arguments_kwargs["chat_template"] = get_chat_template( - self.cfg.chat_template + self.cfg.chat_template, + tokenizer=self.tokenizer, ) if self.cfg.rl == "orpo": From 5c7e89105dc6f626c5ddc92af37af5caebb2af41 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 31 Oct 2024 01:41:34 +0700 Subject: [PATCH 0210/1405] Fix: modelloader handling of model_kwargs load_in*bit (#1999) * fix: load_in_*bit not properly read * fix: load_*bit check * fix: typo * refactor: load * bit handling * feat: add test dpo lora multi-gpu * fix: turn off sample packing for dpo * fix: missing warmup_steps * fix: test to load in 8bit for lora * skip 8bit lora on h100, add 4bit lora on h100 to multi gpu tests * chore: reduce max_steps --------- Co-authored-by: Wing Lian --- src/axolotl/utils/models.py | 33 +--- tests/e2e/multigpu/test_llama.py | 156 ++++++++++++++++++- tests/e2e/multigpu/test_qwen2.py | 2 +- tests/e2e/patched/test_4d_multipack_llama.py | 4 +- tests/e2e/utils.py | 17 +- 5 files changed, 170 insertions(+), 42 deletions(-) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index f3386cccfa..41e89dbfb2 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -640,9 +640,7 @@ def set_quantization_config(self) -> None: self.model_kwargs["quantization_config"] = BitsAndBytesConfig( **self.model_config.quantization_config ) - elif self.cfg.adapter == "qlora" and ( - "load_in_4bit" in self.model_kwargs and self.model_kwargs["load_in_4bit"] - ): + elif self.cfg.adapter == "qlora" and self.model_kwargs["load_in_4bit"]: bnb_config = { "load_in_4bit": True, "llm_int8_threshold": 6.0, @@ -665,9 +663,7 @@ def set_quantization_config(self) -> None: self.model_kwargs["quantization_config"] = BitsAndBytesConfig( **bnb_config, ) - elif self.cfg.adapter == "lora" and ( - "load_in_8bit" in self.model_kwargs and self.model_kwargs["load_in_8bit"] - ): + elif self.cfg.adapter == "lora" and self.model_kwargs["load_in_8bit"]: bnb_config = { "load_in_8bit": True, } @@ -680,10 +676,8 @@ def set_quantization_config(self) -> None: # no longer needed per https://github.com/huggingface/transformers/pull/26610 if "quantization_config" in self.model_kwargs or self.cfg.gptq: - if "load_in_8bit" in self.model_kwargs: - del self.model_kwargs["load_in_8bit"] - if "load_in_4bit" in self.model_kwargs: - del self.model_kwargs["load_in_4bit"] + self.model_kwargs.pop("load_in_8bit", None) + self.model_kwargs.pop("load_in_4bit", None) def set_attention_config(self) -> None: """ @@ -968,17 +962,10 @@ def prepare_model(self, qlora_fsdp) -> None: if is_deepspeed_zero3_enabled(): skip_prepare_model_for_kbit_training = True - is_load_in_8bit = ( - "load_in_8bit" in self.model_kwargs and self.model_kwargs["load_in_8bit"] - ) - is_load_in_4bit = ( - "load_in_4bit" in self.model_kwargs and self.model_kwargs["load_in_4bit"] - ) - if ( not skip_prepare_model_for_kbit_training and self.cfg.adapter in ["lora", "qlora"] - and (is_load_in_8bit or is_load_in_4bit) + and (self.cfg.load_in_8bit or self.cfg.load_in_4bit) ): LOG.info("converting PEFT model w/ prepare_model_for_kbit_training") self.model = prepare_model_for_kbit_training( @@ -1116,16 +1103,10 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: # --------------------------------------------------------- # put model to accelerator # --------------------------------------------------------- - is_load_in_8bit = ( - "load_in_8bit" in self.model_kwargs and self.model_kwargs["load_in_8bit"] - ) - is_load_in_4bit = ( - "load_in_4bit" in self.model_kwargs and self.model_kwargs["load_in_4bit"] - ) if ( self.cfg.ddp - and not is_load_in_8bit - and not (self.cfg.rl and is_load_in_4bit) + and not self.cfg.load_in_8bit + and not (self.cfg.rl and self.cfg.load_in_4bit) and not skip_move_to_device ): # TODO revaldate this conditional diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 14e3f733eb..8087e08e3e 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -14,7 +14,7 @@ from axolotl.utils.dict import DictDefault -from ..utils import with_temp_dir +from ..utils import is_hopper, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" @@ -59,7 +59,7 @@ def test_lora_ddp(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 100, + "max_steps": 15, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -116,7 +116,7 @@ def test_lora_ddp_packed(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 50, + "max_steps": 15, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -144,6 +144,146 @@ def test_lora_ddp_packed(self, temp_dir): ] ) + @pytest.mark.skipif(is_hopper(), reason="h100 doesn't support 8-bit lora") + @with_temp_dir + def test_dpo_lora_ddp(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "TinyLlama/TinyLlama_v1.1", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 2048, + "sample_packing": False, + "eval_sample_packing": False, + "pad_to_sequence_len": True, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.05, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "rl": "dpo", + "chat_template": "llama3", + "datasets": [ + { + "path": "fozziethebeat/alpaca_messages_2k_dpo_test", + "type": "chat_template.default", + "field_messages": "conversation", + "field_chosen": "chosen", + "field_rejected": "rejected", + "message_field_role": "role", + "message_field_content": "content", + "roles": { + "system": ["system"], + "user": ["user"], + "assistant": ["assistant"], + }, + }, + ], + "num_epochs": 1, + "max_steps": 15, + "micro_batch_size": 4, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "warmup_steps": 0, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) + + @with_temp_dir + def test_dpo_qlora_ddp(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM-135M", + "sequence_len": 2048, + "sample_packing": False, + "eval_sample_packing": False, + "pad_to_sequence_len": True, + "load_in_4bit": True, + "adapter": "qlora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.05, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "rl": "dpo", + "chat_template": "chatml", + "datasets": [ + { + "path": "fozziethebeat/alpaca_messages_2k_dpo_test", + "type": "chat_template.default", + "field_messages": "conversation", + "field_chosen": "chosen", + "field_rejected": "rejected", + "message_field_role": "role", + "message_field_content": "content", + "roles": { + "system": ["system"], + "user": ["user"], + "assistant": ["assistant"], + }, + }, + ], + "num_epochs": 1, + "max_steps": 15, + "micro_batch_size": 4, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "warmup_steps": 0, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) + @with_temp_dir def test_fsdp(self, temp_dir): # pylint: disable=duplicate-code @@ -165,7 +305,7 @@ def test_fsdp(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 100, + "max_steps": 15, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -231,7 +371,7 @@ def test_fsdp_packed(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 100, + "max_steps": 15, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -307,7 +447,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 100, + "max_steps": 15, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -373,7 +513,7 @@ def test_ds_zero3_packed(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 100, + "max_steps": 15, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -432,7 +572,7 @@ def test_ds_zero3_qlora_packed(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 100, + "max_steps": 15, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, diff --git a/tests/e2e/multigpu/test_qwen2.py b/tests/e2e/multigpu/test_qwen2.py index 2513be69e5..393ab7d707 100644 --- a/tests/e2e/multigpu/test_qwen2.py +++ b/tests/e2e/multigpu/test_qwen2.py @@ -47,7 +47,7 @@ def test_qlora_fsdp_dpo(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 100, + "max_steps": 15, "warmup_steps": 20, "micro_batch_size": 4, "gradient_accumulation_steps": 2, diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index d74d097237..a26c5d9620 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -13,7 +13,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import require_torch_2_1_1, with_temp_dir +from ..utils import require_torch_2_3_1, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -24,7 +24,7 @@ class Test4dMultipackLlama(unittest.TestCase): Test case for Llama models using 4d attention with multipack """ - @require_torch_2_1_1 + @require_torch_2_3_1 @with_temp_dir def test_sdp_lora_packing(self, temp_dir): # pylint: disable=duplicate-code diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 837b4734fc..c15ca3d79a 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -9,6 +9,8 @@ from importlib.metadata import version from pathlib import Path +import torch + def with_temp_dir(test_func): @wraps(test_func) @@ -35,13 +37,18 @@ def most_recent_subdir(path): return subdir -def require_torch_2_1_1(test_case): +def require_torch_2_3_1(test_case): """ - Decorator marking a test that requires torch >= 2.1.1 + Decorator marking a test that requires torch >= 2.3.1 """ - def is_min_2_1_1(): + def is_min_2_3_1(): torch_version = version("torch") - return torch_version >= "2.1.1" + return torch_version >= "2.3.1" + + return unittest.skipUnless(is_min_2_3_1(), "test torch 2.3.1")(test_case) + - return unittest.skipUnless(is_min_2_1_1(), "test torch 2.1.1")(test_case) +def is_hopper(): + compute_capability = torch.cuda.get_device_capability() + return compute_capability == (9, 0) From d4dbfa02fe8a9d206358db8382a901bfc8014f78 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Thu, 31 Oct 2024 21:43:46 +0530 Subject: [PATCH 0211/1405] Add plugin manager's callback hooks to training flow (#2006) * Add plugin manager's callback hooks to training flow * Use .values() instead of .items() --- src/axolotl/core/trainer_builder.py | 23 +++++++++--- src/axolotl/integrations/base.py | 57 ++++++++++++++++------------- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index e47c09d514..aab9a80b8b 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -48,6 +48,7 @@ ) from trl.trainer.utils import RewardDataCollatorWithPadding, pad_to_length +from axolotl.integrations.base import PluginManager from axolotl.monkeypatch.multipack import SUPPORTED_MULTIPACK_MODEL_TYPES from axolotl.monkeypatch.relora import ReLoRACallback, ReLoRAScheduler from axolotl.utils import is_comet_available, is_mlflow_available @@ -1147,6 +1148,12 @@ def build(self, total_num_steps): def get_callbacks(self) -> List[TrainerCallback]: callbacks = [] + + plugin_manager = PluginManager.get_instance() + callbacks.extend( + plugin_manager.add_callbacks_pre_trainer(cfg=self.cfg, model=self.model) + ) + if self.cfg.use_wandb: callbacks.append( SaveAxolotlConfigtoWandBCallback(self.cfg.axolotl_config_path) @@ -1173,11 +1180,17 @@ def get_callbacks(self) -> List[TrainerCallback]: return callbacks - @abstractmethod def get_post_trainer_create_callbacks(self, trainer): """ Callbacks added after the trainer is created, usually b/c these need access to the trainer """ + callbacks = [] + + plugin_manager = PluginManager.get_instance() + callbacks.extend( + plugin_manager.add_callbacks_post_trainer(cfg=self.cfg, trainer=trainer) + ) + return callbacks def hook_pre_create_training_args(self, training_arguments_kwargs): # TODO @@ -1223,7 +1236,7 @@ def get_callbacks(self): return callbacks def get_post_trainer_create_callbacks(self, trainer): - callbacks = [] + callbacks = super().get_post_trainer_create_callbacks(trainer=trainer) if self.cfg.use_wandb and self.cfg.eval_table_size > 0: LogPredictionCallback = log_prediction_callback_factory( trainer, self.tokenizer, "wandb" @@ -1791,7 +1804,7 @@ def get_callbacks(self): return callbacks def get_post_trainer_create_callbacks(self, trainer): - callbacks = [] + callbacks = super().get_post_trainer_create_callbacks(trainer=trainer) return callbacks def build_training_arguments(self, total_num_steps): @@ -2000,11 +2013,11 @@ class HFPPOTrainerBuilder(TrainerBuilderBase): """ def get_callbacks(self): - callbacks = [] + callbacks = super().get_callbacks() return callbacks def get_post_trainer_create_callbacks(self, trainer): - callbacks = [] + callbacks = super().get_post_trainer_create_callbacks(trainer=trainer) return callbacks def build(self, total_num_steps): diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index e2bd79bc4d..43afa431a0 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -18,9 +18,10 @@ To create a new plugin, you need to inherit from the BasePlugin class and implement the required methods. """ +import collections import importlib import logging -from typing import List +from typing import OrderedDict class BasePlugin: @@ -47,7 +48,7 @@ def __init__(self): Initializes the BasePlugin. """ - def register(self, cfg): + def register(self, cfg): # pylint: disable=unused-argument """ Registers the plugin with the given configuration. @@ -63,7 +64,7 @@ def get_input_args(self): Returns a pydantic model for the plugin's input arguments. """ - def pre_model_load(self, cfg): + def pre_model_load(self, cfg): # pylint: disable=unused-argument """ Performs actions before the model is loaded. @@ -74,7 +75,7 @@ def pre_model_load(self, cfg): None """ - def post_model_load(self, cfg, model): + def post_model_load(self, cfg, model): # pylint: disable=unused-argument """ Performs actions after the model is loaded. @@ -86,7 +87,7 @@ def post_model_load(self, cfg, model): None """ - def pre_lora_load(self, cfg, model): + def pre_lora_load(self, cfg, model): # pylint: disable=unused-argument """ Performs actions before LoRA weights are loaded. @@ -98,7 +99,7 @@ def pre_lora_load(self, cfg, model): None """ - def post_lora_load(self, cfg, model): + def post_lora_load(self, cfg, model): # pylint: disable=unused-argument """ Performs actions after LoRA weights are loaded. @@ -110,7 +111,7 @@ def post_lora_load(self, cfg, model): None """ - def create_optimizer(self, cfg, trainer): + def create_optimizer(self, cfg, trainer): # pylint: disable=unused-argument """ Creates and returns an optimizer for training. @@ -122,7 +123,9 @@ def create_optimizer(self, cfg, trainer): object: The created optimizer. """ - def create_lr_scheduler(self, cfg, trainer, optimizer): + def create_lr_scheduler( + self, cfg, trainer, optimizer + ): # pylint: disable=unused-argument """ Creates and returns a learning rate scheduler. @@ -135,7 +138,7 @@ def create_lr_scheduler(self, cfg, trainer, optimizer): object: The created learning rate scheduler. """ - def add_callbacks_pre_trainer(self, cfg, model): + def add_callbacks_pre_trainer(self, cfg, model): # pylint: disable=unused-argument """ Adds callbacks to the trainer before training. @@ -146,8 +149,11 @@ def add_callbacks_pre_trainer(self, cfg, model): Returns: List[callable]: A list of callback functions to be added to the TrainingArgs """ + return [] - def add_callbacks_post_trainer(self, cfg, trainer): + def add_callbacks_post_trainer( + self, cfg, trainer + ): # pylint: disable=unused-argument """ Adds callbacks to the trainer after training. @@ -158,8 +164,9 @@ def add_callbacks_post_trainer(self, cfg, trainer): Returns: List[callable]: A list of callback functions to be added to the TrainingArgs """ + return [] - def post_train(self, cfg, model): + def post_train(self, cfg, model): # pylint: disable=unused-argument """ Performs actions after training is complete. @@ -171,7 +178,7 @@ def post_train(self, cfg, model): None """ - def post_train_unload(self, cfg): + def post_train_unload(self, cfg): # pylint: disable=unused-argument """ Performs actions after training is complete and the model is unloaded. @@ -227,7 +234,7 @@ class PluginManager: pre_model_load(cfg): Calls the pre_model_load method of all registered plugins. """ - plugins: List[BasePlugin] = [] + plugins: OrderedDict[str, BasePlugin] = collections.OrderedDict() _instance = None @@ -237,7 +244,7 @@ def __new__(cls): """ if cls._instance is None: cls._instance = super(PluginManager, cls).__new__(cls) - cls._instance.plugins: List[BasePlugin] = [] + cls._instance.plugins = collections.OrderedDict() return cls._instance @staticmethod @@ -265,7 +272,7 @@ def register(self, plugin_name: str): """ try: plugin = load_plugin(plugin_name) - self.plugins.append(plugin) + self.plugins[plugin_name] = plugin except ImportError: logging.error(f"Failed to load plugin: {plugin_name}") @@ -277,7 +284,7 @@ def get_input_args(self): list[str]: A list of Pydantic classes for all registered plugins' input arguments.' """ input_args = [] - for plugin in self.plugins: + for plugin in self.plugins.values(): input_args_from_plugin = plugin.get_input_args() if input_args_from_plugin is not None: input_args.append(input_args_from_plugin) @@ -293,7 +300,7 @@ def pre_model_load(self, cfg): Returns: None """ - for plugin in self.plugins: + for plugin in self.plugins.values(): plugin.pre_model_load(cfg) def post_model_load(self, cfg, model): @@ -307,7 +314,7 @@ def post_model_load(self, cfg, model): Returns: None """ - for plugin in self.plugins: + for plugin in self.plugins.values(): plugin.post_model_load(cfg, model) def pre_lora_load(self, cfg, model): @@ -321,7 +328,7 @@ def pre_lora_load(self, cfg, model): Returns: None """ - for plugin in self.plugins: + for plugin in self.plugins.values(): plugin.pre_lora_load(cfg, model) def post_lora_load(self, cfg, model): @@ -335,7 +342,7 @@ def post_lora_load(self, cfg, model): Returns: None """ - for plugin in self.plugins: + for plugin in self.plugins.values(): plugin.post_lora_load(cfg, model) def create_optimizer(self, cfg, trainer): @@ -349,7 +356,7 @@ def create_optimizer(self, cfg, trainer): Returns: object: The created optimizer, or None if none was found. """ - for plugin in self.plugins: + for plugin in self.plugins.values(): optimizer = plugin.create_optimizer(cfg, trainer) if optimizer is not None: return optimizer @@ -367,7 +374,7 @@ def create_lr_scheduler(self, cfg, trainer, optimizer): Returns: object: The created learning rate scheduler, or None if none was found. """ - for plugin in self.plugins: + for plugin in self.plugins.values(): scheduler = plugin.create_lr_scheduler(cfg, trainer, optimizer) if scheduler is not None: return scheduler @@ -385,7 +392,7 @@ def add_callbacks_pre_trainer(self, cfg, model): List[callable]: A list of callback functions to be added to the TrainingArgs. """ callbacks = [] - for plugin in self.plugins: + for plugin in self.plugins.values(): callbacks.extend(plugin.add_callbacks_pre_trainer(cfg, model)) return callbacks @@ -401,7 +408,7 @@ def add_callbacks_post_trainer(self, cfg, trainer): List[callable]: A list of callback functions to be added to the TrainingArgs. """ callbacks = [] - for plugin in self.plugins: + for plugin in self.plugins.values(): callbacks.extend(plugin.add_callbacks_post_trainer(cfg, trainer)) return callbacks @@ -416,5 +423,5 @@ def post_train_unload(self, cfg): Returns: None """ - for plugin in self.plugins: + for plugin in self.plugins.values(): plugin.post_train_unload(cfg) From dc1de7d81bdc84df8dd434af6cad57ad8655b55c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 31 Oct 2024 13:26:14 -0400 Subject: [PATCH 0212/1405] add retries for load datasets requests failures (#2007) --- src/axolotl/utils/data/sft.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index ce01b44098..ec17fb9c2e 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -2,9 +2,11 @@ import functools import logging +import time from pathlib import Path from typing import List, Optional, Tuple, Union +import requests from datasets import ( Dataset, DatasetDict, @@ -53,6 +55,28 @@ LOG = logging.getLogger("axolotl") +def retry_on_request_exceptions(max_retries=3, delay=1): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements + for attempt in range(max_retries): + try: + return func(*args, **kwargs) + except ( + requests.exceptions.ReadTimeout, + requests.exceptions.ConnectionError, + ) as exc: + if attempt < max_retries - 1: + time.sleep(delay) + else: + raise exc + + return wrapper + + return decorator + + +@retry_on_request_exceptions(max_retries=3, delay=5) def prepare_dataset(cfg, tokenizer, processor=None): prompters = [] if not cfg.pretraining_dataset: From 3591bcfaf9793c8cb09e41d5d66b55a438042c06 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 31 Oct 2024 13:27:49 -0400 Subject: [PATCH 0213/1405] add torch 2.5.1 for base image (#2010) --- .github/workflows/base.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index c94093bc97..cd99b9772b 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -40,7 +40,7 @@ jobs: cuda_version: 12.4.1 cudnn_version: "" python_version: "3.11" - pytorch: 2.5.0 + pytorch: 2.5.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" steps: - name: Checkout From 052a9a79b444fdf1111a4352652e9730297ef07d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 31 Oct 2024 13:45:01 -0400 Subject: [PATCH 0214/1405] only run the remainder of the gpu test suite if one case passes first (#2009) [skip ci] * only run the remainder of the gpu test suite if one case passes first * also reduce the test matrix --- .github/workflows/tests-nightly.yml | 7 ---- .github/workflows/tests.yml | 54 ++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 90b1e23cd2..6a1dbcc915 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -82,13 +82,6 @@ jobs: num_gpus: 1 axolotl_extras: mamba-ssm nightly_build: "true" - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.11" - pytorch: 2.3.1 - num_gpus: 1 - axolotl_extras: mamba-ssm - nightly_build: "true" - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ba50adfd35..2c735bd102 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,7 +72,7 @@ jobs: run: | find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; - docker-e2e-tests: + docker-e2e-tests-1st: if: github.repository_owner == 'axolotl-ai-cloud' # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] @@ -83,24 +83,52 @@ jobs: fail-fast: false matrix: include: - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.10" - pytorch: 2.3.1 - num_gpus: 1 - axolotl_extras: mamba-ssm - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.11" - pytorch: 2.3.1 - num_gpus: 1 - axolotl_extras: mamba-ssm - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.4.1 num_gpus: 1 axolotl_extras: + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install Modal + run: | + python -m pip install --upgrade pip + pip install modal==0.63.64 jinja2 + - name: Update env vars + run: | + echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV + echo "PYTORCH_VERSION=${{ matrix.pytorch}}" >> $GITHUB_ENV + echo "AXOLOTL_ARGS=${{ matrix.axolotl_args}}" >> $GITHUB_ENV + echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV + echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV + echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV + - name: Run tests job on Modal + run: | + modal run cicd.tests + + docker-e2e-tests: + if: github.repository_owner == 'axolotl-ai-cloud' + # this job needs to be run on self-hosted GPU runners... + runs-on: [self-hosted, modal] + timeout-minutes: 90 + needs: [pre-commit, pytest, docker-e2e-tests-1st] + + strategy: + fail-fast: false + matrix: + include: + - cuda: 121 + cuda_version: 12.1.1 + python_version: "3.10" + pytorch: 2.3.1 + num_gpus: 1 + axolotl_extras: mamba-ssm - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" From 02ce520b7e5d3a2980fcefa63d8593d8c9e66c58 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 7 Nov 2024 12:53:34 -0500 Subject: [PATCH 0215/1405] upgrade liger to 0.4.0 (#1973) * upgrade liger to 0.3.1 * update docs and example * skip duplicate code check * Update src/axolotl/integrations/liger/args.py Co-authored-by: NanoCode012 * Update README.md Co-authored-by: NanoCode012 * add logging * chore: lint * add test case * upgrade liger and transformers * also upgrade accelerate * use kwargs to support patch release * make sure prepared path is empty for test * use transfromers 4.46.1 since 4.46.2 breaks fsdp --------- Co-authored-by: NanoCode012 --- README.md | 3 +- examples/deepseek-v2/qlora-fsdp-2_5.yaml | 2 +- examples/llama-3/fft-8b-liger-fsdp.yaml | 2 +- requirements.txt | 6 +- src/axolotl/core/trainer_builder.py | 4 +- src/axolotl/integrations/liger/__init__.py | 140 +++++---------------- src/axolotl/integrations/liger/args.py | 23 +++- tests/e2e/integrations/liger.py | 1 - tests/integrations/__init__.py | 0 tests/integrations/liger.py | 80 ++++++++++++ tests/test_datasets.py | 4 + 11 files changed, 146 insertions(+), 119 deletions(-) create mode 100644 tests/integrations/__init__.py create mode 100644 tests/integrations/liger.py diff --git a/README.md b/README.md index c12aa3bba0..b3f292c7dd 100644 --- a/README.md +++ b/README.md @@ -562,7 +562,8 @@ plugins: - axolotl.integrations.liger.LigerPlugin liger_rope: true liger_rms_norm: true -liger_swiglu: true +liger_glu_activation: true +liger_layer_norm: true liger_fused_linear_cross_entropy: true ``` diff --git a/examples/deepseek-v2/qlora-fsdp-2_5.yaml b/examples/deepseek-v2/qlora-fsdp-2_5.yaml index 0320e02138..6b8771d814 100644 --- a/examples/deepseek-v2/qlora-fsdp-2_5.yaml +++ b/examples/deepseek-v2/qlora-fsdp-2_5.yaml @@ -9,7 +9,7 @@ strict: false plugins: - axolotl.integrations.liger.LigerPlugin liger_rms_norm: true -liger_swiglu: true +liger_glu_activation: true liger_fused_linear_cross_entropy: true chat_template: deepseek_v2 diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml index 99ba63fcc6..043b5c9807 100644 --- a/examples/llama-3/fft-8b-liger-fsdp.yaml +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -4,7 +4,7 @@ plugins: - axolotl.integrations.liger.LigerPlugin liger_rope: true liger_rms_norm: true -liger_swiglu: true +liger_glu_activation: true liger_fused_linear_cross_entropy: true strict: false diff --git a/requirements.txt b/requirements.txt index 6bb1aa6848..ec823a82a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.13.2 -transformers==4.46.0 +transformers==4.46.1 tokenizers>=0.20.1 bitsandbytes==0.44.1 -accelerate==1.0.1 +accelerate==1.1.0 datasets==3.0.1 deepspeed==0.15.3 pydantic==2.6.3 @@ -34,7 +34,7 @@ tensorboard python-dotenv==1.0.1 autoawq>=0.2.5 triton>=2.3.0 -liger-kernel==0.3.0 +liger-kernel==0.4.0 mamba-ssm==1.2.0.post1 diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index aab9a80b8b..4fadd7eb43 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -896,13 +896,13 @@ def store_metrics( for key, value in metrics.items(): self._stored_metrics[train_eval][key].append(value) - def _save_checkpoint(self, model, trial, metrics=None): + def _save_checkpoint(self, model, trial, **kwargs): # make sure the checkpoint dir exists, since trainer is flakey checkpoint_folder = f"{PREFIX_CHECKPOINT_DIR}-{self.state.global_step}" run_dir = self._get_output_dir(trial=trial) output_dir = os.path.join(run_dir, checkpoint_folder) os.makedirs(output_dir, exist_ok=True) - return super()._save_checkpoint(model, trial, metrics=metrics) + return super()._save_checkpoint(model, trial, **kwargs) class AxolotlMambaTrainer(AxolotlTrainer): diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index 2047f3815d..a64d748c67 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -18,20 +18,23 @@ Liger Kernel is the collection of Triton-native kernels for LLM Training. It is designed to be performant, correct, and light-weight. """ +import inspect import logging import sys -from functools import partial from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss -from liger_kernel.transformers.geglu import LigerGEGLUMLP +from liger_kernel.transformers.monkey_patch import MODEL_TYPE_TO_APPLY_LIGER_FN from liger_kernel.transformers.rms_norm import LigerRMSNorm from liger_kernel.transformers.rope import liger_rotary_pos_emb from liger_kernel.transformers.swiglu import LigerSwiGLUMLP from axolotl.integrations.base import BasePlugin +from ...utils.distributed import zero_only from .args import LigerArgs # pylint: disable=unused-import. # noqa: F401 +LOG = logging.getLogger("axolotl.integrations.liger") + class LigerPlugin(BasePlugin): """ @@ -42,59 +45,31 @@ def get_input_args(self): return "axolotl.integrations.liger.LigerArgs" def pre_model_load(self, cfg): - if cfg.model_config_type == "llama": - from liger_kernel.transformers.model.llama import ( - lce_forward as llama_lce_forward, - ) - from transformers.models.llama import modeling_llama - - if cfg.liger_rope: - modeling_llama.apply_rotary_pos_emb = liger_rotary_pos_emb - if cfg.liger_rms_norm: - modeling_llama.LlamaRMSNorm = LigerRMSNorm - if cfg.liger_swiglu: - modeling_llama.LlamaMLP = LigerSwiGLUMLP - if cfg.liger_cross_entropy: - modeling_llama.CrossEntropyLoss = LigerCrossEntropyLoss - elif cfg.liger_fused_linear_cross_entropy: - modeling_llama.LlamaForCausalLM.forward = llama_lce_forward - - elif cfg.model_config_type == "mistral": - from liger_kernel.transformers.model.mistral import ( - lce_forward as mistral_lce_forward, - ) - from transformers.models.mistral import modeling_mistral - - if cfg.liger_rope: - modeling_mistral.apply_rotary_pos_emb = liger_rotary_pos_emb - if cfg.liger_rms_norm: - modeling_mistral.MistralRMSNorm = LigerRMSNorm - if cfg.liger_swiglu: - modeling_mistral.MistralMLP = LigerSwiGLUMLP - if cfg.liger_cross_entropy: - modeling_mistral.CrossEntropyLoss = LigerCrossEntropyLoss - if cfg.liger_fused_linear_cross_entropy: - modeling_mistral.MistralForCausalLM.forward = mistral_lce_forward - - elif cfg.model_config_type == "gemma": - from liger_kernel.transformers.model.gemma import ( - lce_forward as gemma_lce_forward, - ) - from transformers.models.gemma import modeling_gemma - - if cfg.liger_rope: - modeling_gemma.apply_rotary_pos_emb = liger_rotary_pos_emb - if cfg.liger_rms_norm: - modeling_gemma.GemmaRMSNorm = partial( - LigerRMSNorm, offset=1.0, init_fn="zeros", casting_mode="gemma" + if cfg.model_config_type in MODEL_TYPE_TO_APPLY_LIGER_FN: + apply_liger_fn = MODEL_TYPE_TO_APPLY_LIGER_FN[cfg.model_config_type] + liger_fn_sig = inspect.signature(apply_liger_fn) + kwargs = {} + if "rope" in liger_fn_sig.parameters: + kwargs["rope"] = cfg.liger_rope + if "cross_entropy" in liger_fn_sig.parameters: + kwargs["cross_entropy"] = cfg.liger_cross_entropy + if "fused_linear_cross_entropy" in liger_fn_sig.parameters: + kwargs[ + "fused_linear_cross_entropy" + ] = cfg.liger_fused_linear_cross_entropy + if "rms_norm" in liger_fn_sig.parameters: + kwargs["rms_norm"] = cfg.liger_rms_norm + if "layer_norm" in liger_fn_sig.parameters: + kwargs["layer_norm"] = cfg.liger_layer_norm + if "geglu" in liger_fn_sig.parameters: + kwargs["geglu"] = cfg.liger_glu_activation + elif "swiglu" in liger_fn_sig.parameters: + kwargs["swiglu"] = cfg.liger_glu_activation + with zero_only(): + LOG.info( + f"Applying LIGER to {cfg.model_config_type} with kwargs: {kwargs}" ) - if cfg.liger_swiglu: - modeling_gemma.GemmaMLP = LigerGEGLUMLP - if cfg.liger_cross_entropy: - modeling_gemma.CrossEntropyLoss = LigerCrossEntropyLoss - if cfg.liger_fused_linear_cross_entropy: - modeling_gemma.GemmaForCausalLM.forward = gemma_lce_forward - + apply_liger_fn(**kwargs) elif cfg.model_config_type == "jamba": from transformers.models.jamba import modeling_jamba @@ -104,30 +79,12 @@ def pre_model_load(self, cfg): modeling_jamba.apply_rotary_pos_emb = liger_rotary_pos_emb if cfg.liger_rms_norm: modeling_jamba.JambaRMSNorm = LigerRMSNorm - if cfg.liger_swiglu: + if cfg.liger_glu_activation: modeling_jamba.JambaMLP = LigerSwiGLUMLP if cfg.liger_cross_entropy: modeling_jamba.CrossEntropyLoss = LigerCrossEntropyLoss if cfg.liger_fused_linear_cross_entropy: modeling_jamba.JambaForCausalLM.forward = jamba_lce_forward - - elif cfg.model_config_type == "qwen2": - from liger_kernel.transformers.model.qwen2 import ( - lce_forward as qwen2_lce_forward, - ) - from transformers.models.qwen2 import modeling_qwen2 - - if cfg.liger_rope: - modeling_qwen2.apply_rotary_pos_emb = liger_rotary_pos_emb - if cfg.liger_rms_norm: - modeling_qwen2.Qwen2RMSNorm = LigerRMSNorm - if cfg.liger_swiglu: - modeling_qwen2.Qwen2MLP = LigerSwiGLUMLP - if cfg.liger_cross_entropy: - modeling_qwen2.CrossEntropyLoss = LigerCrossEntropyLoss - if cfg.liger_fused_linear_cross_entropy: - modeling_qwen2.Qwen2ForCausalLM.forward = qwen2_lce_forward - elif cfg.model_config_type == "deepseek_v2": from accelerate import init_empty_weights from transformers import AutoModelForCausalLM @@ -146,44 +103,9 @@ def pre_model_load(self, cfg): logging.warning("Fused liger_rope is not supported for DeepseekV2.") if cfg.liger_rms_norm: modeling_mod.DeepseekV2RMSNorm = LigerRMSNorm - if cfg.liger_swiglu: + if cfg.liger_glu_activation: modeling_mod.DeepseekV2MLP.forward = LigerSwiGLUMLP.forward if cfg.liger_cross_entropy: modeling_mod.CrossEntropyLoss = LigerCrossEntropyLoss if cfg.liger_fused_linear_cross_entropy: modeling_mod.DeepseekV2ForCausalLM.forward = deepseekv2_lce_forward - - elif cfg.model_config_type == "gemma2": - from transformers.models.gemma2 import modeling_gemma2 - - if cfg.liger_rope: - modeling_gemma2.apply_rotary_pos_emb = liger_rotary_pos_emb - if cfg.liger_rms_norm: - modeling_gemma2.Gemma2RMSNorm = partial( - LigerRMSNorm, offset=1.0, init_fn="zeros", casting_mode="gemma" - ) - if cfg.liger_swiglu: - modeling_gemma2.Gemma2MLP = LigerGEGLUMLP - if cfg.liger_cross_entropy: - modeling_gemma2.CrossEntropyLoss = LigerCrossEntropyLoss - if cfg.liger_fused_linear_cross_entropy: - logging.warning( - "Fused linear cross entropy is not supported for Gemma 2." - ) - - elif cfg.model_config_type == "phi3": - from liger_kernel.transformers.model.phi3 import ( - lce_forward as phi3_lce_forward, - ) - from transformers.models.phi3 import modeling_phi3 - - if cfg.liger_rope: - modeling_phi3.apply_rotary_pos_emb = liger_rotary_pos_emb - if cfg.liger_rms_norm: - modeling_phi3.Phi3RMSNorm = LigerRMSNorm - if cfg.liger_swiglu: - modeling_phi3.Phi3MLP = LigerSwiGLUMLP - if cfg.liger_cross_entropy: - modeling_phi3.CrossEntropyLoss = LigerCrossEntropyLoss - if cfg.liger_fused_linear_cross_entropy: - modeling_phi3.Phi3ForCausalLM.forward = phi3_lce_forward diff --git a/src/axolotl/integrations/liger/args.py b/src/axolotl/integrations/liger/args.py index decdb37750..02ece31432 100644 --- a/src/axolotl/integrations/liger/args.py +++ b/src/axolotl/integrations/liger/args.py @@ -15,9 +15,12 @@ """ Module for handling LIGER input arguments. """ +import logging from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, model_validator + +LOG = logging.getLogger("axolotl.integrations.liger.args") class LigerArgs(BaseModel): @@ -27,6 +30,24 @@ class LigerArgs(BaseModel): liger_rope: Optional[bool] = None liger_rms_norm: Optional[bool] = None + liger_layer_norm: Optional[bool] = None liger_swiglu: Optional[bool] = None + liger_glu_activation: Optional[bool] = None liger_cross_entropy: Optional[bool] = None liger_fused_linear_cross_entropy: Optional[bool] = None + + @model_validator(mode="before") + @classmethod + def check_deprecated_swiglu(cls, data): + if data.get("liger_swiglu") is not None: + if data.get("liger_glu_activation") is not None: + raise ValueError( + "You cannot have both `liger_swiglu` and `liger_glu_activation` set." + ) + + LOG.warning( + "The 'liger_swiglu' argument is deprecated and will be removed in a future release. " + "Please use 'liger_glu_activation' instead." + ) + data["liger_glu_activation"] = data.pop("liger_swiglu") + return data diff --git a/tests/e2e/integrations/liger.py b/tests/e2e/integrations/liger.py index 4497cebe32..bb4574dff3 100644 --- a/tests/e2e/integrations/liger.py +++ b/tests/e2e/integrations/liger.py @@ -1,7 +1,6 @@ """ Simple end-to-end test for Liger integration """ - import unittest from pathlib import Path diff --git a/tests/integrations/__init__.py b/tests/integrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/liger.py b/tests/integrations/liger.py new file mode 100644 index 0000000000..61540a57c4 --- /dev/null +++ b/tests/integrations/liger.py @@ -0,0 +1,80 @@ +""" +config validation tests for swiglu args +""" +# pylint: disable=duplicate-code +import logging +from typing import Optional + +import pytest + +from axolotl.utils.config import validate_config +from axolotl.utils.dict import DictDefault + + +@pytest.fixture(name="minimal_base_cfg") +def fixture_cfg(): + return DictDefault( + { + "base_model": "TinyLlama/TinyLlama-1.1B-Chat-v0.6", + "learning_rate": 0.000001, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + } + ], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + } + ) + + +class BaseValidation: + """ + Base validation module to setup the log capture + """ + + _caplog: Optional[pytest.LogCaptureFixture] = None + + @pytest.fixture(autouse=True) + def inject_fixtures(self, caplog): + self._caplog = caplog + + +# pylint: disable=too-many-public-methods +class TestValidation(BaseValidation): + """ + Test the validation module for liger + """ + + def test_deprecated_swiglu(self, minimal_cfg): + test_cfg = DictDefault( + { + "liger_swiglu": False, + } + | minimal_cfg + ) + + with self._caplog.at_level(logging.WARNING): + updated_cfg = validate_config(test_cfg) + assert ( + "The 'liger_swiglu' argument is deprecated" + in self._caplog.records[0].message + ) + assert updated_cfg.liger_swiglu is None + assert updated_cfg.liger_glu_activations is False + + def test_conflict_swiglu_ligergluactivation(self, minimal_cfg): + test_cfg = DictDefault( + { + "liger_swiglu": False, + "liger_glu_activations": True, + } + | minimal_cfg + ) + + with pytest.raises( + ValueError, + match=r".*You cannot have both `liger_swiglu` and `liger_glu_activation` set.*", + ): + validate_config(test_cfg) diff --git a/tests/test_datasets.py b/tests/test_datasets.py index 8e2955414e..a57b6d83e2 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -306,6 +306,10 @@ def test_load_hub_with_revision(self): """Verify that processing data from the hub works with a specific revision""" with tempfile.TemporaryDirectory() as tmp_dir: prepared_path = Path(tmp_dir) / "prepared" + + # make sure prepared_path is empty + shutil.rmtree(prepared_path, ignore_errors=True) + cfg = DictDefault( { "tokenizer_config": "huggyllama/llama-7b", From 035e9f9dd7036445f071bd7aa19d716f52c88d34 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 7 Nov 2024 17:54:29 -0500 Subject: [PATCH 0216/1405] janky workaround to install FA2 on torch 2.5.1 base image since it takes forever to build (#2022) --- docker/Dockerfile-base | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 3f13bba30a..405883eda6 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -35,3 +35,7 @@ RUN git lfs install --skip-repo && \ pip3 install awscli && \ # The base image ships with `pydantic==1.8.2` which is not working pip3 install -U --no-cache-dir pydantic==1.10.10 + +RUN if [ "$PYTHON_VERSION" != "2.5.1" ] ; then \ + pip3 install flash-attn==2.6.3; \ + fi From 3cb2d75de1e23800ff042570f0fe3f3f480cad30 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 8 Nov 2024 10:46:24 -0500 Subject: [PATCH 0217/1405] upgrade pytorch to 2.5.1 (#2024) --- .github/workflows/main.yml | 4 ++-- .github/workflows/multi-gpu-e2e.yml | 2 +- .github/workflows/nightlies.yml | 4 ++-- .github/workflows/tests-nightly.yml | 4 ++-- .github/workflows/tests.yml | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 47a4c7f114..8f96461550 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.5.0 + pytorch: 2.5.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: @@ -94,7 +94,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.5.0 + pytorch: 2.5.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index d9f0ce7e6c..c2fb5dfb5d 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -31,7 +31,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.5.0 + pytorch: 2.5.1 axolotl_extras: num_gpus: 2 nightly_build: "true" diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 55123a9026..3d6c0e62d7 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -31,7 +31,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.5.0 + pytorch: 2.5.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: @@ -93,7 +93,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.5.0 + pytorch: 2.5.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 6a1dbcc915..cba39b94e7 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -25,7 +25,7 @@ jobs: fail-fast: false matrix: python_version: ["3.10", "3.11"] - pytorch_version: ["2.3.1", "2.4.1", "2.5.0"] + pytorch_version: ["2.3.1", "2.4.1", "2.5.1"] timeout-minutes: 20 steps: @@ -92,7 +92,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.5.0 + pytorch: 2.5.1 num_gpus: 1 axolotl_extras: nightly_build: "true" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2c735bd102..8ced4188b7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: fail-fast: false matrix: python_version: ["3.10", "3.11"] - pytorch_version: ["2.3.1", "2.4.1", "2.5.0"] + pytorch_version: ["2.3.1", "2.4.1", "2.5.1"] timeout-minutes: 20 steps: @@ -132,7 +132,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.5.0 + pytorch: 2.5.1 num_gpus: 1 axolotl_extras: steps: From 3265b7095e550e8a8a53a706f83cf5bae8a7db54 Mon Sep 17 00:00:00 2001 From: Sunny Liu Date: Fri, 8 Nov 2024 11:29:11 -0500 Subject: [PATCH 0218/1405] Add weighted optimisation support for trl DPO trainer integration (#2016) * trlv0.12.0 integration * update trl version requirements * linting * commenting out * trl version requirement --- docs/config.qmd | 2 + requirements.txt | 2 +- src/axolotl/core/trainer_builder.py | 14 ++++-- .../config/models/input/v0_4_1/__init__.py | 3 ++ tests/e2e/test_dpo.py | 45 +++++++++++++++++++ 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index a7bf9080bf..238f7201db 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -183,6 +183,8 @@ test_datasets: # use RL training: 'dpo', 'ipo', 'kto' rl: +# whether to perform weighting if doing DPO training. Boolean. +dpo_use_weighting: # The name of the chat template to use for training, following values are supported: # - tokenizer_default: Uses the chat template that is available in the tokenizer_config.json. If the chat template is not available in the tokenizer, it will raise an error. This is the default value. diff --git a/requirements.txt b/requirements.txt index ec823a82a9..735f860a55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,7 @@ s3fs>=2024.5.0 gcsfs>=2024.5.0 # adlfs -trl @ git+https://github.com/huggingface/trl.git@31d02cfb795284591a084416b9dcb7bef5d08924 +trl==0.12.0 zstandard==0.22.0 fastcore diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 4fadd7eb43..7b83707b82 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1890,17 +1890,18 @@ def build_training_arguments(self, total_num_steps): # default to saving each epoch if not defined training_args_kwargs["save_strategy"] = "epoch" + training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes + if self.cfg.rl_beta: training_args_kwargs["beta"] = self.cfg.rl_beta if self.cfg.orpo_alpha: # trl does some odd mapping of alpha to beta to reuse the beta parameter ??? training_args_kwargs["beta"] = self.cfg.orpo_alpha - training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes - training_args_cls = AxolotlDPOConfig if self.cfg.rpo_alpha is not None: training_args_kwargs["rpo_alpha"] = self.cfg.rpo_alpha + training_args_cls = None if self.cfg.rl == "simpo": training_args_cls = AxolotlCPOConfig training_args_kwargs["loss_type"] = "simpo" @@ -1909,13 +1910,13 @@ def build_training_arguments(self, total_num_steps): if self.cfg.cpo_alpha is not None: training_args_kwargs["cpo_alpha"] = self.cfg.cpo_alpha - if self.cfg.rl == "orpo": + elif self.cfg.rl == "orpo": training_args_cls = AxolotlORPOConfig training_args_kwargs["max_length"] = self.cfg.sequence_len if self.cfg.max_prompt_len: training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len - if self.cfg.rl == "kto": + elif self.cfg.rl == "kto": training_args_cls = AxolotlKTOConfig training_args_kwargs["desirable_weight"] = ( @@ -1930,6 +1931,11 @@ def build_training_arguments(self, total_num_steps): if self.cfg.max_prompt_len: training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len + else: + training_args_cls = AxolotlDPOConfig + if self.cfg.dpo_use_weighting is not None: + training_args_kwargs["use_weighting"] = self.cfg.dpo_use_weighting + training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg output_dir=self.cfg.output_dir, per_device_train_batch_size=self.cfg.micro_batch_size, diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 2e5749230c..64fec67e0e 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -588,6 +588,9 @@ class Config: rl: Optional[RLType] = None reward_model: Optional[bool] = None + dpo_use_weighting: Optional[ + bool + ] = None # whether to use weighting in DPO trainer. If none, default is false in the trainer. datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset], min_length=1)] = None # type: ignore test_datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset], min_length=1)] = None # type: ignore diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index 1c354e9a01..4a705922fa 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -115,6 +115,51 @@ def test_dpo_nll_lora(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + @with_temp_dir + def test_dpo_use_weighting(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 1024, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 64, + "lora_alpha": 32, + "lora_dropout": 0.1, + "lora_target_linear": True, + "special_tokens": {}, + "rl": "dpo", + "dpo_use_weighting": True, + "datasets": [ + { + "path": "arcee-ai/distilabel-intel-orca-dpo-pairs-binarized", + "type": "chatml.ultra", + "split": "train", + }, + ], + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "paged_adamw_8bit", + "lr_scheduler": "cosine", + "max_steps": 20, + "save_steps": 10, + "warmup_steps": 5, + "gradient_checkpointing": True, + "gradient_checkpointing_kwargs": {"use_reentrant": True}, + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + @pytest.mark.skip("kto_pair no longer supported in trl") @with_temp_dir def test_kto_pair_lora(self, temp_dir): From fd3b80716aaa5b06ac979fc6a0885d032bcb5d14 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 8 Nov 2024 13:45:49 -0500 Subject: [PATCH 0219/1405] remove fastchat and sharegpt (#2021) * remove fastchat and sharegpt * remove imports * remove more fastchat imports * chore: remove unused functions * feat: remove sharegpt and deprecate from docs * chore: remove unused sharegpt checks * fix: remove sharegpt type from tests * feat: add sharegpt deprecation error * feat: update readme --------- Co-authored-by: NanoCode012 --- README.md | 7 +- devtools/dev_chat_template.yml | 2 +- docs/config.qmd | 11 +- docs/dataset-formats/conversation.qmd | 59 +-- requirements.txt | 1 - src/axolotl/cli/preprocess.py | 21 - src/axolotl/cli/train.py | 19 - .../fastchat_conversation_turns.py | 231 -------- src/axolotl/prompt_strategies/instruct.py | 33 -- src/axolotl/prompt_strategies/llama2_chat.py | 6 +- src/axolotl/prompt_strategies/sharegpt.py | 223 -------- .../prompt_strategies/sharegpt_jokes.py | 28 - src/axolotl/prompt_tokenizers.py | 157 +----- src/axolotl/prompters.py | 159 +----- src/axolotl/utils/config/__init__.py | 26 - .../config/models/input/v0_4_1/__init__.py | 26 +- src/axolotl/utils/tokenization.py | 64 --- tests/prompt_strategies/test_sharegpt.py | 500 ------------------ tests/test_normalize_config.py | 10 +- tests/test_prompt_tokenizers.py | 210 +------- tests/test_validation.py | 33 -- tests/test_validation_dataset.py | 6 +- 22 files changed, 28 insertions(+), 1804 deletions(-) delete mode 100644 src/axolotl/monkeypatch/fastchat_conversation_turns.py delete mode 100644 src/axolotl/prompt_strategies/instruct.py delete mode 100644 src/axolotl/prompt_strategies/sharegpt.py delete mode 100644 src/axolotl/prompt_strategies/sharegpt_jokes.py delete mode 100644 tests/prompt_strategies/test_sharegpt.py diff --git a/README.md b/README.md index b3f292c7dd..077dd6fee5 100644 --- a/README.md +++ b/README.md @@ -383,11 +383,10 @@ See [examples](examples) for quick start. It is recommended to duplicate and mod - typescript type: ... # unimplemented custom format - # fastchat conversation (deprecation soon, use chat_template https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/conversation.html#chat_template) - # See 'conversation' options: https://github.com/lm-sys/FastChat/blob/main/fastchat/conversation.py + # chat_template https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/conversation.html#chat_template - path: ... - type: sharegpt - conversation: chatml # default: vicuna_v1.1 + type: chat_template + chat_template: chatml # defaults to tokenizer's chat_template # local - path: data.jsonl # or json diff --git a/devtools/dev_chat_template.yml b/devtools/dev_chat_template.yml index 9697da4b33..27dc9be1af 100644 --- a/devtools/dev_chat_template.yml +++ b/devtools/dev_chat_template.yml @@ -1,4 +1,4 @@ -# Example config for debugging the sharegpt prompt format +# Example config for debugging the chat_template prompt format base_model: TinyLlama/TinyLlama-1.1B-Chat-v1.0 model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer diff --git a/docs/config.qmd b/docs/config.qmd index 238f7201db..09691bc770 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -83,7 +83,7 @@ lora_on_cpu: true datasets: # HuggingFace dataset repo | s3://,gs:// path | "json" for local dataset, make sure to fill data_files - path: vicgalle/alpaca-gpt4 - # The type of prompt to use for training. [alpaca, sharegpt, gpteacher, oasst, reflection] + # The type of prompt to use for training. [alpaca, gpteacher, oasst, reflection] type: alpaca # format | format: (chat/instruct) | .load_ ds_type: # Optional[str] (json|arrow|parquet|text|csv) defines the datatype when path is a file data_files: # Optional[str] path to source data files @@ -92,15 +92,6 @@ datasets: train_on_split: train # Optional[str] name of dataset split to load from revision: # Optional[str] The specific revision of the dataset to use when loading from the Hugging Face Hub. This can be a commit hash, tag, or branch name. If not specified, the latest version will be used. This parameter is ignored for local datasets. - # Optional[str] fastchat conversation type, only used with type: sharegpt - conversation: # Options (see Conversation 'name'): https://github.com/lm-sys/FastChat/blob/main/fastchat/conversation.py - field_human: # Optional[str]. Human key to use for conversation. - field_model: # Optional[str]. Assistant key to use for conversation. - # Add additional keys from your dataset as input or output roles - roles: - input: # Optional[List[str]]. These will be masked based on train_on_input - output: # Optional[List[str]]. - # Custom user instruction prompt - path: repo type: diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index c7273c5be5..fb9aed3ffa 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -6,33 +6,8 @@ order: 3 ## sharegpt -UPDATE: ShareGPT is being deprecated in the next release. Please see `chat_template` section below. +IMPORTANT: ShareGPT is deprecated!. Please see `chat_template` section below. -conversations where `from` is `human`/`gpt`. (optional: first row with role `system` to override default system prompt) - -```{.json filename="data.jsonl"} -{"conversations": [{"from": "...", "value": "..."}]} -``` - -Note: `type: sharegpt` opens special configs: -- `conversation`: enables conversions to many Conversation types. Refer to the 'name' [here](https://github.com/lm-sys/FastChat/blob/main/fastchat/conversation.py) for options. -- `roles`: allows you to specify the roles for input and output. This is useful for datasets with custom roles such as `tool` etc to support masking. -- `field_human`: specify the key to use instead of `human` in the conversation. -- `field_model`: specify the key to use instead of `gpt` in the conversation. - -```yaml -datasets: - path: ... - type: sharegpt - - conversation: # Options (see Conversation 'name'): https://github.com/lm-sys/FastChat/blob/main/fastchat/conversation.py - field_human: # Optional[str]. Human key to use for conversation. - field_model: # Optional[str]. Assistant key to use for conversation. - # Add additional keys from your dataset as input or output roles - roles: - input: # Optional[List[str]]. These will be masked based on train_on_input - output: # Optional[List[str]]. -``` ## pygmalion @@ -40,38 +15,6 @@ datasets: {"conversations": [{"role": "...", "value": "..."}]} ``` -## sharegpt.load_role - -conversations where `role` is used instead of `from` - -```{.json filename="data.jsonl"} -{"conversations": [{"role": "...", "value": "..."}]} -``` - -## sharegpt.load_guanaco - -conversations where `from` is `prompter` `assistant` instead of default sharegpt - -```{.json filename="data.jsonl"} -{"conversations": [{"from": "...", "value": "..."}]} -``` - -## sharegpt.load_ultrachat - -conversations where the turns field is 'messages', human is 'user' and gpt is 'assistant'. - -```{.json filename="data.jsonl"} -{"messages": [{"user": "...", "assistant": "..."}]} -``` - -## sharegpt_jokes - -creates a chat where bot is asked to tell a joke, then explain why the joke is funny - -```{.json filename="data.jsonl"} -{"conversations": [{"title": "...", "text": "...", "explanation": "..."}]} -``` - ## chat_template diff --git a/requirements.txt b/requirements.txt index 735f860a55..d1fdccaf77 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,6 @@ scipy scikit-learn==1.4.2 pynvml art -fschat @ git+https://github.com/lm-sys/FastChat.git@27a05b04a35510afb1d767ae7e5990cbd278f8fe gradio==3.50.2 tensorboard python-dotenv==1.0.1 diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index aab29e2670..a1592aa785 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -23,10 +23,6 @@ ) from axolotl.common.cli import PreprocessCliArgs from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH -from axolotl.prompt_strategies.sharegpt import ( - register_chatml_template, - register_llama3_template, -) from axolotl.utils.trainer import disable_datasets_caching LOG = logging.getLogger("axolotl.cli.preprocess") @@ -44,23 +40,6 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): return_remaining_strings=True ) - if parsed_cfg.chat_template == "chatml": - if parsed_cfg.default_system_message: - LOG.info( - f"ChatML set. Adding default system message: {parsed_cfg.default_system_message}" - ) - register_chatml_template(parsed_cfg.default_system_message) - else: - register_chatml_template() - elif parsed_cfg.chat_template == "llama3": - if parsed_cfg.default_system_message: - LOG.info( - f"LLaMA-3 set. Adding default system message: {parsed_cfg.default_system_message}" - ) - register_llama3_template(parsed_cfg.default_system_message) - else: - register_llama3_template() - if not parsed_cfg.dataset_prepared_path: msg = ( Fore.RED diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 16d66a82f0..2a40e854ee 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -19,10 +19,6 @@ ) from axolotl.common.cli import TrainerCliArgs from axolotl.integrations.base import PluginManager -from axolotl.prompt_strategies.sharegpt import ( - register_chatml_template, - register_llama3_template, -) from axolotl.train import train LOG = logging.getLogger("axolotl.cli.train") @@ -42,21 +38,6 @@ def do_train(cfg, cli_args) -> None: print_axolotl_text_art() check_accelerate_default_config() check_user_token() - if cfg.chat_template == "chatml" and cfg.default_system_message: - LOG.info( - f"ChatML set. Adding default system message: {cfg.default_system_message}" - ) - register_chatml_template(cfg.default_system_message) - else: - register_chatml_template() - - if cfg.chat_template == "llama3" and cfg.default_system_message: - LOG.info( - f"LLaMA-3 set. Adding default system message: {cfg.default_system_message}" - ) - register_llama3_template(cfg.default_system_message) - else: - register_llama3_template() if cfg.rl: # and cfg.rl != "orpo": dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) diff --git a/src/axolotl/monkeypatch/fastchat_conversation_turns.py b/src/axolotl/monkeypatch/fastchat_conversation_turns.py deleted file mode 100644 index a09bfddb4b..0000000000 --- a/src/axolotl/monkeypatch/fastchat_conversation_turns.py +++ /dev/null @@ -1,231 +0,0 @@ -""" -monkeypatch to add a get_turns method -""" - -import logging -from typing import Generator, Tuple - -from fastchat.conversation import SeparatorStyle - -LOG = logging.getLogger("axolotl.monkeypatch.fastchat_conversation_turns") - - -def get_prompt(self) -> str: - ret = "" - for role, msg in self.get_turns(): - ret += role + msg - return ret - - -def get_turns( # pylint: disable=too-many-return-statements - self, -) -> Generator[Tuple[str, str], None, None]: - """Get the prompt for generation.""" - system_prompt = self.system_template.format(system_message=self.system_message) - if self.sep_style == SeparatorStyle.ADD_COLON_SINGLE: - yield "", system_prompt + self.sep - for role, message in self.messages: - if message: - yield role + ": ", message + self.sep - else: - yield role + ":", "" - return - if self.sep_style == SeparatorStyle.ADD_COLON_TWO: - seps = [self.sep, self.sep2] - yield "", system_prompt + seps[0] - for i, (role, message) in enumerate(self.messages): - if message: - yield role + ": ", message + seps[i % 2] - else: - yield role + ":", "" - return - if self.sep_style == SeparatorStyle.ADD_COLON_SPACE_SINGLE: - yield "", system_prompt + self.sep - for role, message in self.messages: - if message: - yield role + ": ", message + self.sep - else: - yield role + ": ", "" # must be end with a space - return - if self.sep_style == SeparatorStyle.ADD_NEW_LINE_SINGLE: - yield "", "" if system_prompt == "" else system_prompt + self.sep - for role, message in self.messages: - if message: - yield role + "\n", message + self.sep - else: - yield role + "\n", "" - return - if self.sep_style == SeparatorStyle.NO_COLON_SINGLE: - yield "", system_prompt - for role, message in self.messages: - if message: - yield role, message + self.sep - else: - yield role, "" - return - if self.sep_style == SeparatorStyle.NO_COLON_TWO: - seps = [self.sep, self.sep2] - yield "", system_prompt - for i, (role, message) in enumerate(self.messages): - if message: - yield role, message + seps[i % 2] - else: - yield role, "" - return - if self.sep_style == SeparatorStyle.RWKV: - yield "", system_prompt - for i, (role, message) in enumerate(self.messages): - if message: - yield role + ": ", message.replace("\r\n", "\n").replace( - "\n\n", "\n" - ) + "\n\n" - else: - yield role + ":", "" - return - if self.sep_style == SeparatorStyle.LLAMA2 and self.name != "mistral": - if self.system_message: - if self.messages: - # For llama, the system message is incorporated into the first human instruction - first_role, first_msg = self.messages[0] - if first_role == self.roles[0]: - system_prompt += first_msg - self.messages.pop(0) - yield "", system_prompt - for i, (role, message) in enumerate(self.messages): - if message: - if (i % 2 == 0 and not self.system_message) or ( - i % 2 != 0 and self.system_message - ): - role = " " + role - yield role + " ", message - else: - yield role, "" - return - if self.sep_style == SeparatorStyle.LLAMA2 and self.name == "mistral": - contains_sys_msg = False - if self.system_message: - contains_sys_msg = True - if self.messages: - # There is no clear guidance on how to handle system messages in Mistral so we just prepend it to the first human instruction separated by a newline - first_role, first_msg = self.messages[0] - if first_role == self.roles[0]: - system_prompt = self.system_template.format( - system_message=" " + self.system_message - ) - system_prompt += first_msg - self.messages.pop(0) - yield "", system_prompt - for i, (role, message) in enumerate(self.messages): - if message and i == 0 and not contains_sys_msg: - yield "", system_prompt.strip() + " " + message # if there is no system message, we need to make sure there is the a ` [INST]` at the beginning of the first instruction. - elif message: - yield role + " ", message - else: - yield role, "" - return - if self.sep_style == SeparatorStyle.LLAMA3: - if self.system_message: - # For llama3, the system message is NOT incorporated into the first human instruction - # All messages follow <|start_header_id|>' + role + '<|end_header_id|>\n\n'+ message + '<|eot_id|> - yield "", system_prompt - for i, (role, message) in enumerate(self.messages): - if message: - yield f"<|start_header_id|>{role}<|end_header_id|>\n\n", f"{message.strip()}<|eot_id|>" - else: - yield f"<|start_header_id|>{role}<|end_header_id|>\n\n", "" - return - if self.sep_style == SeparatorStyle.GEMMA: - if self.system_message: - raise ValueError("Gemma chat template does not support system messages") - for i, (role, message) in enumerate(self.messages): - prefix = "" if i == 0 else "" - message_str = message if message else "" - yield prefix + "" + role + "\n", message_str + "\n" - return - if self.sep_style == SeparatorStyle.CHATGLM: - # source: https://huggingface.co/THUDM/chatglm-6b/blob/1d240ba371910e9282298d4592532d7f0f3e9f3e/modeling_chatglm.py#L1302-L1308 - # source2: https://huggingface.co/THUDM/chatglm2-6b/blob/e186c891cf64310ac66ef10a87e6635fa6c2a579/modeling_chatglm.py#L926 - round_add_n = 1 if self.name == "chatglm2" else 0 - if system_prompt: - yield "", system_prompt + self.sep - - for i, (role, message) in enumerate(self.messages): - if i % 2 == 0: - yield "", f"[Round {i//2 + round_add_n}]{self.sep}" - - if message: - yield f"{role}:", f"{message}{self.sep}" - else: - yield f"{role}:", "" - return - if self.sep_style == SeparatorStyle.CHATML: - yield "", "" if system_prompt == "" else system_prompt + self.sep + "\n" - for role, message in self.messages: - if message: - yield role + "\n", message + self.sep + "\n" - else: - yield role + "\n", "" - return - if self.sep_style == SeparatorStyle.CHATGLM3: - if self.system_message: - yield "", system_prompt - for role, message in self.messages: - if message: - yield role + "\n", " " + message - else: - yield role - return - if self.sep_style == SeparatorStyle.CHATINTERN: - # source: https://huggingface.co/internlm/internlm-chat-7b-8k/blob/bd546fa984b4b0b86958f56bf37f94aa75ab8831/modeling_internlm.py#L771 - seps = [self.sep, self.sep2] - yield "", system_prompt - for i, (role, message) in enumerate(self.messages): - prefix = "" if i % 2 == 0 else "" - if message: - yield prefix + role + ":", message + seps[i % 2] + "\n" - else: - yield role + ":", "" - return - if self.sep_style == SeparatorStyle.DOLLY: - seps = [self.sep, self.sep2] - yield "", system_prompt - for i, (role, message) in enumerate(self.messages): - if message: - suffix = "\n\n" if i % 2 == 1 else "" - yield role + ":\n", message + seps[i % 2] + suffix - else: - yield role + ":\n", "" - return - if self.sep_style == SeparatorStyle.PHOENIX: - yield "", system_prompt - for role, message in self.messages: - if message: - yield role + ": ", "" + message + "" - else: - yield role + ": " + "", "" - return - if self.sep_style == SeparatorStyle.ROBIN: - yield "", system_prompt + self.sep - for role, message in self.messages: - if message: - yield role + ":\n", message + self.sep - else: - yield role + ":\n", "" - return - if self.sep_style == SeparatorStyle.FALCON_CHAT: - if self.system_message: - yield "", system_prompt + self.sep - for role, message in self.messages: - if message: - yield role + ": ", message + self.sep - else: - yield role + ":", "" - else: - raise ValueError(f"Invalid style: {self.sep_style}") - - -def add_get_turns_to_conversation(): - import fastchat.conversation - - fastchat.conversation.Conversation.get_turns = get_turns - fastchat.conversation.Conversation.get_prompt = get_prompt diff --git a/src/axolotl/prompt_strategies/instruct.py b/src/axolotl/prompt_strategies/instruct.py deleted file mode 100644 index 3d63674890..0000000000 --- a/src/axolotl/prompt_strategies/instruct.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Module containing the InstructShareGPTPromptTokenizingStrategy class""" -from typing import Any, Dict, Optional - -from axolotl.prompt_tokenizers import ShareGPTPromptTokenizingStrategy -from axolotl.prompters import ShareGPTPrompterV2 - - -def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): - conversation = ( - ds_cfg["conversation"] if ds_cfg and "conversation" in ds_cfg else None - ) - strategy = InstructShareGPTPromptTokenizingStrategy( - # pylint: disable=duplicate-code - ShareGPTPrompterV2( - conversation=conversation, - ), - tokenizer, - cfg.train_on_inputs, - cfg.sequence_len, - ) - return strategy - - -class InstructShareGPTPromptTokenizingStrategy(ShareGPTPromptTokenizingStrategy): - """ - basic sharegpt strategy to grab conversations from the sample row - """ - - def get_conversation_thread(self, prompt): - return [ - {"from": "human", "value": prompt["instruction"]}, - {"from": "gpt", "value": prompt["output"]}, - ] diff --git a/src/axolotl/prompt_strategies/llama2_chat.py b/src/axolotl/prompt_strategies/llama2_chat.py index a1f5ffefff..29e091bfd0 100644 --- a/src/axolotl/prompt_strategies/llama2_chat.py +++ b/src/axolotl/prompt_strategies/llama2_chat.py @@ -29,7 +29,7 @@ from typing import Generator, List, Sequence from axolotl.prompt_tokenizers import PromptTokenizingStrategy -from axolotl.prompters import IGNORE_TOKEN_ID, SHAREGPT_ASSERTION_FAILED_ROLE +from axolotl.prompters import ALTERNATING_ASSERTION_FAILED_ROLE, IGNORE_TOKEN_ID @dataclass @@ -75,7 +75,7 @@ def append_message(self, role: str, message: str): class LLama2ChatTokenizingStrategy(PromptTokenizingStrategy): """ - Tokenizing strategy for ShareGPT prompts. + Tokenizing strategy for Llama2 prompts. adapted from https://github.com/lm-sys/FastChat/blob/main/fastchat/train/train.py """ @@ -191,7 +191,7 @@ def build_prompt(self, source) -> Generator[Llama2ChatConversation, None, None]: conv.messages = [] # pylint: disable=R0801 for j, sentence in enumerate(source): role = roles[sentence["from"]] - assert role == conv.roles[j % 2], SHAREGPT_ASSERTION_FAILED_ROLE + assert role == conv.roles[j % 2], ALTERNATING_ASSERTION_FAILED_ROLE if sentence["value"]: conv.append_message(role, sentence["value"]) yield conv diff --git a/src/axolotl/prompt_strategies/sharegpt.py b/src/axolotl/prompt_strategies/sharegpt.py deleted file mode 100644 index 069d243f52..0000000000 --- a/src/axolotl/prompt_strategies/sharegpt.py +++ /dev/null @@ -1,223 +0,0 @@ -"""Module containing the SimpleShareGPTPromptTokenizingStrategy class""" - -import logging -from typing import Any, Dict, Optional, Type - -from fastchat.conversation import Conversation, SeparatorStyle, register_conv_template - -from axolotl.prompt_tokenizers import ShareGPTPromptTokenizingStrategy -from axolotl.prompters import ShareGPTPrompterV2 -from axolotl.utils.tokenization import ( - chatml_to_conversation, - merge_consecutive_messages, -) - -LOG = logging.getLogger("axolotl") - - -def register_chatml_template(system_message=None): - system_message = system_message or "You are a helpful assistant." - register_conv_template( - Conversation( - name="chatml", - system_template="<|im_start|>system\n{system_message}", - system_message=system_message, - roles=("<|im_start|>user", "<|im_start|>assistant"), - sep_style=SeparatorStyle.CHATML, - sep="<|im_end|>", - ) - ) - register_conv_template( - Conversation( - name="chatml_glaive", - system_template="<|im_start|>system\n{system_message}", - system_message=system_message, - roles=("<|im_start|>user", "<|im_start|>assistant", "<|im_start|>tool"), - sep_style=SeparatorStyle.CHATML, - sep="<|im_end|>", - ) - ) - - -def register_llama3_template(system_message=None): - system_message = system_message or "You are a helpful assistant." - register_conv_template( - Conversation( - name="llama3", - system_template="<|start_header_id|>system<|end_header_id|>\n\n{system_message}<|eot_id|>", - system_message=system_message, - roles=("user", "assistant"), - sep_style=SeparatorStyle.LLAMA3, - sep="", - stop_str="<|eot_id|>", - stop_token_ids=[128001, 128009], - ) - ) - - -def build_loader( - tokenization_strategy_cls: Type["ShareGPTPromptTokenizingStrategy"], - prompter_cls: Type["ShareGPTPrompterV2"], - default_conversation: Optional[str] = None, -): - def _load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): - LOG.warning( - "sharegpt type support will be deprecated in the next release of Axolotl. Please use chat_template instead. https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/conversation.html#chat_template", - ) - conversation = ( - ds_cfg["conversation"] - if ds_cfg and "conversation" in ds_cfg - else default_conversation - ) - field_human = ( - ds_cfg["field_human"] if ds_cfg and "field_human" in ds_cfg else None - ) - field_model = ( - ds_cfg["field_model"] if ds_cfg and "field_model" in ds_cfg else None - ) - roles = ds_cfg["roles"].to_dict() if ds_cfg and "roles" in ds_cfg else None - strategy = tokenization_strategy_cls( - prompter_cls( - conversation=conversation, - role_key_model=field_model, - role_key_human=field_human, - roles=roles, - ), - tokenizer, - cfg.train_on_inputs, - cfg.sequence_len, - ) - if ds_cfg and "strict" in ds_cfg and hasattr(strategy, "strict"): - strategy.strict = ds_cfg["strict"] - if ds_cfg and "field_messages" in ds_cfg and hasattr(strategy, "messages"): - strategy.messages = ds_cfg["field_messages"] - return strategy - - return _load - - -class SimpleShareGPTPromptTokenizingStrategy(ShareGPTPromptTokenizingStrategy): - """ - basic sharegpt strategy to grab conversations from the sample row - """ - - _strict = False - _messages = "conversations" - - @property - def strict(self): - return self._strict - - @strict.setter - def strict(self, strict): - self._strict = strict - - @property - def messages(self): - return self._messages - - @messages.setter - def messages(self, messages): - self._messages = messages - - def get_conversation_thread(self, prompt): - conversations = prompt[self.messages] - if self.strict: - return conversations - role_key = "from" - if "role" in conversations[0].keys(): - role_key = "role" - value_key = "value" - if "text" in conversations[0].keys(): - value_key = "text" - elif "content" in conversations[0].keys(): - value_key = "content" - # remap roles - allow for assistant turn" - role_map = { - "user": "human", - "human": "human", - "assistant": "gpt", - "gpt": "gpt", - "system": "system", - } - turns = [ - { - "from": ( - role_map[t[role_key]] if t[role_key] in role_map else t[role_key] - ), - "value": t[value_key], - "weight": 1 - if "weight" not in t or t["weight"] is None - else t["weight"], - } - for t in conversations - ] - return turns - - -class SimpleRoleShareGPTPromptTokenizingStrategy( - SimpleShareGPTPromptTokenizingStrategy -): - """ - basic sharegpt strategy to grab conversations from the sample row, but uses role instead of from - """ - - def get_conversation_thread(self, prompt): - conversations = prompt["conversations"] - # remap role: prompter/assistant, text: ... => from: human/gpt, value: ... - turns = [{"from": t["role"], "value": t["value"]} for t in conversations] - return turns - - -class GuanacoShareGPTPromptTokenizingStrategy(ShareGPTPromptTokenizingStrategy): - """ - sharegpt strategy that remaps oasst data to sharegpt format - """ - - def get_conversation_thread(self, prompt): - conversations = prompt["conversations"] - # remap role: prompter/assistant, text: ... => from: human/gpt, value: ... - role_map = {"prompter": "human", "assistant": "gpt"} - turns = [ - {"from": role_map[t["role"]], "value": t["text"]} for t in conversations - ] - return turns - - -class UltrachatShareGPTPromptTokenizingStrategy(SimpleShareGPTPromptTokenizingStrategy): - """ - sharegpt strategy that remaps ultrachat data to sharegpt format - """ - - def get_conversation_thread(self, prompt): - conversations = prompt["messages"] - role_map = {"user": "human", "assistant": "gpt"} - turns = [ - {"from": role_map[t["role"]], "value": t["content"]} for t in conversations - ] - return turns - - -class GlaiveShareGPTPromptTokenizingStrategy(SimpleShareGPTPromptTokenizingStrategy): - """ - sharegpt strategy that remaps glaive data to sharegpt format - """ - - def get_conversation_thread(self, prompt): - conversation = chatml_to_conversation(prompt) - conversation = merge_consecutive_messages(conversation) - - return conversation - - -load = build_loader(SimpleShareGPTPromptTokenizingStrategy, ShareGPTPrompterV2) -load_role = build_loader(SimpleRoleShareGPTPromptTokenizingStrategy, ShareGPTPrompterV2) -load_ultrachat = build_loader( - UltrachatShareGPTPromptTokenizingStrategy, ShareGPTPrompterV2 -) -load_guanaco = build_loader(GuanacoShareGPTPromptTokenizingStrategy, ShareGPTPrompterV2) -load_glaive = build_loader( - GlaiveShareGPTPromptTokenizingStrategy, - ShareGPTPrompterV2, - default_conversation="chatml_glaive", -) diff --git a/src/axolotl/prompt_strategies/sharegpt_jokes.py b/src/axolotl/prompt_strategies/sharegpt_jokes.py deleted file mode 100644 index 404302c81e..0000000000 --- a/src/axolotl/prompt_strategies/sharegpt_jokes.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Module for Jokes prompts using sharegpt style """ -from axolotl.prompt_tokenizers import ShareGPTPromptTokenizingStrategy -from axolotl.prompters import ShareGPTPrompterV2 - - -def load(tokenizer, cfg): - return SimpleJokesShareGPTPromptTokenizingStrategy( - ShareGPTPrompterV2(), - tokenizer, - cfg.train_on_inputs, - cfg.sequence_len, - ) - - -class SimpleJokesShareGPTPromptTokenizingStrategy(ShareGPTPromptTokenizingStrategy): - """ - Tokenization strategy for asking bot to tell a joke and then explain why its funny - """ - - # title, text, explanation - def get_conversation_thread(self, prompt): - title = "" if not prompt["title"] else prompt["title"] + " " - return [ - {"from": "human", "value": "Tell me a joke."}, - {"from": "gpt", "value": title + prompt["text"]}, - {"from": "human", "value": "Why is that joke funny?"}, - {"from": "gpt", "value": prompt["explanation"]}, - ] diff --git a/src/axolotl/prompt_tokenizers.py b/src/axolotl/prompt_tokenizers.py index 51d497a23c..bd6e3f9dce 100644 --- a/src/axolotl/prompt_tokenizers.py +++ b/src/axolotl/prompt_tokenizers.py @@ -1,17 +1,12 @@ """Module containing PromptTokenizingStrategy and Prompter classes""" import abc -import copy import logging from typing import Dict, List, Tuple, Union -from fastchat.conversation import Conversation from transformers import BatchEncoding, PreTrainedTokenizer -from axolotl.monkeypatch.fastchat_conversation_turns import ( - add_get_turns_to_conversation, -) -from axolotl.prompters import IGNORE_TOKEN_ID, Prompter +from axolotl.prompters import Prompter LOG = logging.getLogger("axolotl") @@ -21,8 +16,6 @@ LLAMA_DEFAULT_BOS_TOKEN = "" # nosec LLAMA_DEFAULT_UNK_TOKEN = "" # nosec -add_get_turns_to_conversation() - class InvalidDataException(Exception): """ @@ -331,154 +324,6 @@ def parse_instruction_fields(self, prompt) -> Tuple[str, str, str, str, str]: ) -class ShareGPTPromptTokenizingStrategy(PromptTokenizingStrategy): - """ - Tokenizing strategy for ShareGPT prompts. - """ - - def get_conversation_thread(self, prompt): - return prompt["conversations"] - - def tokenize_prompt(self, prompt): - # Initial values. We will append to these as we go through the conversation. - result, current_len = tokenize_prompt_default() - conversation: Conversation = ( - self.prompter._conversation.copy() # pylint: disable=protected-access - ) - - input_roles = {conversation.roles[0]} - output_roles = {conversation.roles[1]} - - if len(conversation.roles) == 3: - tool_role_label = conversation.roles[2] - input_roles.add(tool_role_label) - - # Add roles from the config - if self.prompter.roles: - if "input" in self.prompter.roles and self.prompter.roles["input"]: - for role in self.prompter.roles["input"]: - input_roles.add(role) - - if "output" in self.prompter.roles and self.prompter.roles["output"]: - for role in self.prompter.roles["output"]: - output_roles.add(role) - - # support for custom roles from the dataset, only useful for vicuna style prompts/roles - role_remap = [] - if ( - conversation.name == "vicuna_v1.1" - and "roles" in prompt - and len(prompt["roles"]) >= 2 - ): - role_remap = [ - {"from": conversation.roles[0], "to": prompt["roles"][0]}, - {"from": conversation.roles[1], "to": prompt["roles"][1]}, - ] - - try: - for _, part in enumerate( - self.prompter.build_prompt(self.get_conversation_thread(prompt)) - ): - if not isinstance(part, tuple): - LOG.warning(f"expected tuple, got {part}") - continue - - if len(part) <= 2: - role, content = part - weight = 1 - else: - role, content, weight = part - - # Uses "in" because role contains extra characters - input_turn = any(r.lower() in role.lower() for r in input_roles) - output_turn = any(r.lower() in role.lower() for r in output_roles) - empty_role = role.strip() == "" - - if not any([input_turn, output_turn, empty_role]): - LOG.warning(f"unhandled role: {role}") - continue - - if input_turn: - role = ( - role.replace(role_remap[0]["from"], role_remap[0]["to"]) - if role_remap - else role - ) - turn = role + content - # this is still the user query, we should - if not content.strip(): - LOG.warning(f"user turn has empty text: {prompt}") - res = self._tokenize( - turn, - add_eos_token=False, - strip_bos_token=True, - ) - if self.train_on_inputs and weight == 1: - labels = copy.deepcopy(res["input_ids"]) - else: - # everything from this is masked out from the labels - labels = [IGNORE_TOKEN_ID] * len(res["input_ids"]) - elif output_turn: - role = ( - role.replace(role_remap[1]["from"], role_remap[1]["to"]) - if role_remap - else role - ) - turn = role + content - # this should be the assistant response, should end with an eos token - if not content.strip(): - LOG.warning(f"assistant turn has empty text: {prompt}") - add_eos_token = not ( - conversation.name == "chatml" - and conversation.sep == self.tokenizer.eos_token - ) - res = self._tokenize( - turn, - add_eos_token=add_eos_token, - strip_bos_token=True, - ) - role_res = self._tokenize( - role.rstrip(), - add_eos_token=False, - strip_bos_token=True, - ) - labels = copy.deepcopy(res["input_ids"]) - if not self.train_on_inputs: - # mask out role tokens from the labels - len_role = len(role_res["input_ids"]) - labels[:len_role] = [IGNORE_TOKEN_ID] * min( - len_role, len(labels) - ) - if weight == 0: - # everything from this is masked out from the labels - # (role is masked out too because it makes no sense if contents is masked out) - labels = [IGNORE_TOKEN_ID] * len(res["input_ids"]) - - elif empty_role: - turn = content - # this is only ever the first part, should include the bos token and the user query - res = self._tokenize( - turn, add_eos_token=False, strip_bos_token=False - ) - if self.train_on_inputs and weight == 1: - labels = copy.deepcopy(res["input_ids"]) - else: - # everything from this is masked out from the labels - labels = [IGNORE_TOKEN_ID] * len(res["input_ids"]) - - # pylint: disable=duplicate-code - result, current_len = parse_tokenized_to_result( - result, - current_len, - res, - labels, - pad_token_id=self.tokenizer.pad_token_id, - ) - return result - except (KeyError, AssertionError, IndexError) as err: - raise InvalidDataException(str(err)) from err - - def tokenize_prompt_default() -> Tuple[Dict[str, List[int]], int]: """ Returns the default values for the tokenize prompt function diff --git a/src/axolotl/prompters.py b/src/axolotl/prompters.py index 18b73e725e..ec680702dc 100644 --- a/src/axolotl/prompters.py +++ b/src/axolotl/prompters.py @@ -5,7 +5,6 @@ from typing import Generator, Optional, Union from colorama import Fore -from fastchat.conversation import Conversation, get_conv_template LOG = logging.getLogger("axolotl") IGNORE_TOKEN_ID = -100 @@ -262,166 +261,10 @@ def __repr__(self) -> str: ) -SHAREGPT_ASSERTION_FAILED_ROLE = ( +ALTERNATING_ASSERTION_FAILED_ROLE = ( "Role did not alternate between turns (gpt and human). Please check your data." ) -CONVERSATION_ROLE_FORMAT = { - "chatml": "<|im_start|>{ROLE}", - "zephyr": "<|{ROLE}|>", - "vicuna_v1.1": "{ROLE}", - "llama3": "<|start_header_id|>{ROLE}<|end_header_id|>", -} - - -class ShareGPTPrompter(Prompter): # pylint: disable=too-few-public-methods - """ - A prompter that generates prompts for the ShareGPT - """ - - role_key_human = "human" - role_key_model = "gpt" - # Optional, only used for tool usage datasets. - role_key_tool: Optional[str] = None - # Optional, role input/output mapping - roles: Optional[dict] = None - - def __init__( - self, - prompt_style=None, # pylint: disable=unused-argument - conversation: Optional[Union[str, Conversation]] = None, - role_key_human: Optional[str] = None, - role_key_model: Optional[str] = None, - role_key_tool: Optional[str] = None, - roles: Optional[dict] = None, - ): - if conversation: - if isinstance(conversation, Conversation): - self._conversation = conversation - else: - self._conversation = get_conv_template(conversation) - else: - self._conversation = get_conv_template("vicuna_v1.1") - if role_key_human: - self.role_key_human = role_key_human - if role_key_model: - self.role_key_model = role_key_model - if role_key_tool: - self.role_key_tool = role_key_tool - if roles: - self.roles = roles - - def _build_result(self, source): - if len(source) < 2: - # If there isn't a back and forth conversation, ignore it - # also happens on the data splitting leaving empty conversations - raise IndexError( - f"A conversation entry has less than 2 messages :\n{source}" - ) - - conv = self._conversation.copy() - - original_source = source.copy() - # Add the conversation system prompt if provided, otherwise use the default one - if source[0]["from"] == "system": - conv.set_system_message(source[0]["value"]) - source.pop(0) - - roles = {self.role_key_human: conv.roles[0], self.role_key_model: conv.roles[1]} - if self.role_key_tool: - roles[self.role_key_tool] = conv.roles[2] - - try: - # Apply prompt templates - if source[0]["from"] not in roles: - # Skip the first one if it is not from human - source = source[1:] - except IndexError as err: - # sometimes there is a bing or system chat - raise err - - conv.messages = [] - for _, sentence in enumerate(source): - from_role = sentence["from"] - if from_role in roles: - role = roles[from_role] - else: - if self._conversation.name not in CONVERSATION_ROLE_FORMAT: - raise NotImplementedError( - f"Role ({role}) not in default roles, and {self._conversation.name} does not support role remapping yet." - "Please help us by creating an Issue to add support for this conversation type." - ) - - if self._conversation.name in ["llama3"]: - role = from_role - else: - role = CONVERSATION_ROLE_FORMAT[self._conversation.name].format( - ROLE=from_role - ) - - if len(conv.messages) > 0 and ((role == conv.messages[-1][0])): - if ( - role != "assistant" - ): # back to back assistant calls may be okay for tool calls - LOG.warning(f"{SHAREGPT_ASSERTION_FAILED_ROLE}: {sentence}") - - conv.append_message(role, sentence["value"]) - turns = list(conv.get_turns()) - original_source_length = len(original_source) - assert len(turns) in [ - original_source_length - 1, - original_source_length, - original_source_length + 1, - ] - if len(turns) == original_source_length + 1: - original_source = [{"weight": None}] + original_source - elif len(turns) == original_source_length - 1: - original_source = original_source[1:] - return [ - (*turn, weight) - for turn, weight in zip( - turns, - [ - 1 if "weight" not in e or e["weight"] is None else e["weight"] - for e in original_source - ], - ) - ] - - def build_prompt(self, source) -> Generator[str, None, None]: - turns = self._build_result(source) - - for part in turns: - if part[0] and not part[1]: - LOG.warning(f"role with empty message: {part[0]}") - yield part - - def __repr__(self) -> str: - turns = self._build_result([{"from": "{from}", "value": "{value}"}]) - return "\n".join([REPR_TEMPLATE.format(full_prompt=part) for part in turns]) - - -class ShareGPTPrompterV2(ShareGPTPrompter): - """ - A V2 prompter that generates prompts for the ShareGPT - """ - - def __init__( - self, - conversation: Optional[Union[str, Conversation]] = None, - role_key_human: Optional[str] = None, - role_key_model: Optional[str] = None, - role_key_tool: Optional[str] = None, - roles: Optional[dict] = None, - ): - super().__init__( - conversation=conversation, - role_key_human=role_key_human, - role_key_model=role_key_model, - role_key_tool=role_key_tool, - roles=roles, - ) - class UnsupportedPrompter(Prompter): """ diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index afc8c4fc41..6e5ecda03a 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -215,11 +215,6 @@ def normalize_cfg_datasets(cfg): if cfg.chat_template: if cfg.datasets: for idx, ds_cfg in enumerate(cfg.datasets): - if ds_cfg.type == "sharegpt" and not ds_cfg.conversation: - LOG.info( - f"updating dataset {ds_cfg.path} with `conversation: {cfg.chat_template}` to match your chat_template" - ) - cfg.datasets[idx].conversation = cfg.chat_template if ( ds_cfg.type in ["orpo.chat_template", "chat_template"] and not ds_cfg.chat_template @@ -461,27 +456,6 @@ def legacy_validate_config(cfg): "`early_stopping_patience` requires that eval_steps should evenly divide save_steps." ) - if cfg.datasets: - for idx, ds_cfg in enumerate(cfg.datasets): - if not ds_cfg.type: - continue - if ds_cfg.type == "sharegpt:chat": - LOG.warning( - PendingDeprecationWarning( - "`type: sharegpt:chat` will soon be deprecated. simply use `type: sharegpt` instead." - ) - ) - cfg.datasets[idx].type = "sharegpt" - if "sharegpt_simple" in ds_cfg.type: - LOG.warning( - PendingDeprecationWarning( - "`type: sharegpt_simple` will soon be deprecated. simply use `type: sharegpt` instead." - ) - ) - cfg.datasets[idx].type = cfg.datasets[idx].type.replace( - "sharegpt_simple", "sharegpt" - ) - if cfg.saves_per_epoch and cfg.save_steps: raise ValueError( "save_steps and saves_per_epoch are mutually exclusive and cannot be used together." diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 64fec67e0e..8310fd3e57 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -783,26 +783,16 @@ class Config: @field_validator("datasets", mode="before") @classmethod - def fix_sharegpt_datasets(cls, datasets): - for idx, ds_cfg in enumerate(datasets): - if not ds_cfg["type"]: + def deprecate_sharegpt_datasets(cls, datasets): + for _, ds_cfg in enumerate(datasets): + if not ds_cfg.get("type"): continue - if ds_cfg["type"] == "sharegpt:chat": - LOG.warning( - PendingDeprecationWarning( - "`type: sharegpt:chat` will soon be deprecated. simply use `type: sharegpt` instead." - ) - ) - datasets[idx]["type"] = "sharegpt" - if "sharegpt_simple" in ds_cfg["type"]: - LOG.warning( - PendingDeprecationWarning( - "`type: sharegpt_simple` will soon be deprecated. simply use `type: sharegpt` instead." - ) - ) - datasets[idx]["type"] = datasets[idx]["type"].replace( - "sharegpt_simple", "sharegpt" + + if ds_cfg["type"].startswith("sharegpt"): + raise ValueError( + "`type: sharegpt.*` is deprecated. Please use `type: chat_template` instead." ) + return datasets @model_validator(mode="before") diff --git a/src/axolotl/utils/tokenization.py b/src/axolotl/utils/tokenization.py index f353aebec9..97c3c64650 100644 --- a/src/axolotl/utils/tokenization.py +++ b/src/axolotl/utils/tokenization.py @@ -1,8 +1,6 @@ """Module for tokenization utilities""" import logging -import re -from typing import Dict, List from termcolor import colored @@ -93,65 +91,3 @@ def check_rl_example_labels(example, tokenizer, text_only=False): LOG.info(f"REJECTED RESPONSE: {delimiter.join(colored_rejecteds)}\n\n\n") return delimiter.join(colored_tokens) - - -GLAIVE_ROLES = ["USER", "ASSISTANT", "FUNCTION RESPONSE"] -GLAIVE_TO_SHAREGPT_ROLE = { - "SYSTEM": "system", - "USER": "human", - "ASSISTANT": "gpt", - "FUNCTION RESPONSE": "tool", -} - -GLAIVE_MSG_REGEX = re.compile(rf"({'|'.join(GLAIVE_ROLES)}): ") - - -def chatml_to_conversation(row: Dict[str, str]) -> List[Dict[str, str]]: - """ - Converts a ChatML formatted row to a list of messages in ShareGPT format. - Initially based off https://github.com/lilacai/lilac/blob/main/notebooks/GlaiveToShareGPT.ipynb. - """ - - system_prompt = row.get("system") - if system_prompt: - system_prompt = system_prompt.removeprefix("SYSTEM: ") - - chat_str = row["chat"] - chat_msgs = [s.strip() for s in GLAIVE_MSG_REGEX.split(chat_str) if s] - - chat_msg_dicts = [ - {"from": GLAIVE_TO_SHAREGPT_ROLE[role], "value": value} - for role, value in zip(chat_msgs[::2], chat_msgs[1::2]) - ] - - if system_prompt: - chat_msg_dicts = [ - {"from": GLAIVE_TO_SHAREGPT_ROLE["SYSTEM"], "value": system_prompt} - ] + chat_msg_dicts - - return chat_msg_dicts - - -def merge_consecutive_messages(messages): - """ - Merge consecutive messages from the same sender into a single message. - This can be useful with datasets that contain multiple consecutive tool calls. - """ - - merged_messages = [] - current_from = None - current_message = "" - - for msg in messages: - if current_from == msg["from"]: - current_message += msg["value"] - else: - if current_from is not None: - merged_messages.append({"from": current_from, "value": current_message}) - current_from = msg["from"] - current_message = msg["value"] - - if current_from is not None: - merged_messages.append({"from": current_from, "value": current_message}) - - return merged_messages diff --git a/tests/prompt_strategies/test_sharegpt.py b/tests/prompt_strategies/test_sharegpt.py deleted file mode 100644 index e7a73a0de5..0000000000 --- a/tests/prompt_strategies/test_sharegpt.py +++ /dev/null @@ -1,500 +0,0 @@ -""" -Test module for sharegpt integration w chatml -""" - -import pytest -from datasets import Dataset -from tokenizers import AddedToken -from transformers import AutoTokenizer - -from axolotl.datasets import TokenizedPromptDataset -from axolotl.prompt_strategies.sharegpt import ( - GlaiveShareGPTPromptTokenizingStrategy, - SimpleShareGPTPromptTokenizingStrategy, - register_chatml_template, - register_llama3_template, -) -from axolotl.prompters import ShareGPTPrompterV2 - -register_chatml_template() -register_llama3_template() - - -@pytest.fixture(name="sharegpt_dataset") -def fixture_sharegpt_dataset(): - return Dataset.from_list( - [ - { - "conversations": [ - { - "from": "system", - "value": "repeat", - }, - { - "from": "human", - "value": "hello", - }, - { - "from": "gpt", - "value": "hello", - }, - { - "from": "human", - "value": "goodbye", - }, - { - "from": "gpt", - "value": "goodbye", - }, - ] - } - ] - ) - - -@pytest.fixture(name="sharegpt_dataset_with_weights") -def fixture_sharegpt_dataset_with_weights(): - return Dataset.from_list( - [ - { - "conversations": [ - { - "from": "system", - "value": "repeat", - }, - { - "from": "human", - "value": "hello", - "weight": 1, - }, - { - "from": "gpt", - "value": "hello", - "weight": 0, - }, - { - "from": "human", - "value": "rehello", - "weight": 0, - }, - { - "from": "gpt", - "value": "rehello", - "weight": 1, - }, - { - "from": "human", - "value": "goodbye", - }, - { - "from": "gpt", - "value": "goodbye", - "weight": 0, - }, - ] - } - ] - ) - - -@pytest.fixture(name="glaive_dataset") -def fixture_sharegpt_glaive_dataset(): - return Dataset.from_list( - [ - { - "system": "SYSTEM: This is a system prompt", - "chat": "USER: Can you book a flight for me from New York to London? ASSISTANT: I'm sorry, but I don't have the capability to book flights. <|endoftext|>", - } - ] - ) - - -@pytest.fixture(name="multi_role_dataset") -def fixture_multi_role_dataset(): - return Dataset.from_list( - [ - { - "conversations": [ - { - "from": "system", - "value": "use get_weather(city) to get the weather for a city", - }, - { - "from": "human", - "value": "hello, what's the weather in New York?", - }, - { - "from": "gpt", - "value": "let me get that for you", - }, - { - "from": "tool", - "value": "get_weather(New York)", - }, - { - "from": "gpt", - "value": "the weather in New York is 70 degrees and sunny", - }, - ] - } - ] - ) - - -@pytest.fixture(name="tokenizer") -def fixture_tokenizer(): - tokenizer = AutoTokenizer.from_pretrained( - "casperhansen/mistral-7b-instruct-v0.1-awq" - ) - tokenizer.add_special_tokens( - { - "eos_token": AddedToken( - "<|im_end|>", rstrip=False, lstrip=False, normalized=False - ) - } - ) - tokenizer.add_tokens( - [ - AddedToken("<|im_start|>", rstrip=False, lstrip=False, normalized=False), - ] - ) - - return tokenizer - - -@pytest.fixture(name="llama3_tokenizer") -def fixture_llama3_tokenizer(): - tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B") - tokenizer.eos_token = "<|eot_id|>" - - return tokenizer - - -class TestSharegptLlama3: - """Test class for ShareGPT style datasets with llama-3 prompts""" - - def test_tokenization(self, sharegpt_dataset, llama3_tokenizer): - strategy = SimpleShareGPTPromptTokenizingStrategy( - ShareGPTPrompterV2( - conversation="llama3", - role_key_model=None, - role_key_human=None, - ), - llama3_tokenizer, - False, # train_on_inputs - 2048, # sequence_len - ) - - dataset_wrapper = TokenizedPromptDataset( - strategy, sharegpt_dataset, process_count=1 - ) - - input_ids = dataset_wrapper[0]["input_ids"] - - # fmt: off - # pylint: disable=duplicate-code - assert input_ids == [ - 128000, # bos - 128006, 9125, 128007, # system header - 271, 31724, 128009, # sys prompt, eot - 128006, 882, 128007, # user header - 271, 15339, 128009, # user prompt eot - 128006, 78191, 128007, # assistant header - 271, 15339, 128009, # assistant response eot - 128006, 882, 128007, - 271, 19045, 29474, 128009, - 128006, 78191, 128007, - 271, 19045, 29474, 128009, - ] - # fmt: on - - def test_tokenization_with_weights( - self, sharegpt_dataset_with_weights, llama3_tokenizer - ): - strategy = SimpleShareGPTPromptTokenizingStrategy( - ShareGPTPrompterV2( - conversation="llama3", - role_key_model=None, - role_key_human=None, - ), - llama3_tokenizer, - False, # train_on_inputs - 2048, # sequence_len - ) - - dataset_wrapper = TokenizedPromptDataset( - strategy, sharegpt_dataset_with_weights, process_count=1 - ) - - input_ids = dataset_wrapper[0]["input_ids"] - - # fmt: off - # pylint: disable=duplicate-code - assert input_ids == [ - 128000, # bos - 128006, 9125, 128007, # system header - 271, 31724, 128009, # sys prompt, eot - 128006, 882, 128007, # user header - 271, 15339, 128009, # user prompt eot - 128006, 78191, 128007, # assistant header - 271, 15339, 128009, # assistant response eot - 128006, 882, 128007, - 271, 11310, 4896, 128009, - 128006, 78191, 128007, - 271, 11310, 4896, 128009, - 128006, 882, 128007, - 271, 19045, 29474, 128009, - 128006, 78191, 128007, - 271, 19045, 29474, 128009, - ] - # fmt: on - - -class TestSharegptChatML: - """ - Test class for sharegpt prompter - """ - - def test_no_double_im_end(self, sharegpt_dataset, tokenizer): - strategy = SimpleShareGPTPromptTokenizingStrategy( - ShareGPTPrompterV2( - conversation="chatml", - role_key_model=None, - role_key_human=None, - ), - tokenizer, - False, # train_on_inputs - 2048, # sequence_len - ) - - dataset_wrapper = TokenizedPromptDataset( - strategy, sharegpt_dataset, process_count=1 - ) - - input_ids = dataset_wrapper[0]["input_ids"] - # fmt: off - assert input_ids == [ - # 28705, 13, is " \n" - 1, # bos - 32001, 1587, 13, 25997, 32000, 28705, 13, # system - 32001, 2188, 13, 21558, 32000, 28705, 13, # human - 32001, 13892, 13, 21558, 32000, 28705, 13, # gpt - 32001, 2188, 13, 12684, 17664, 32000, 28705, 13, # human - 32001, 13892, 13, 12684, 17664, 32000, 28705, 13, # gpt - ] - # fmt: on - - def test_no_double_im_end_with_weights( - self, sharegpt_dataset_with_weights, tokenizer - ): - strategy = SimpleShareGPTPromptTokenizingStrategy( - ShareGPTPrompterV2( - conversation="chatml", - role_key_model=None, - role_key_human=None, - ), - tokenizer, - False, # train_on_inputs - 2048, # sequence_len - ) - - dataset_wrapper = TokenizedPromptDataset( - strategy, sharegpt_dataset_with_weights, process_count=1 - ) - - input_ids = dataset_wrapper[0]["input_ids"] - # fmt: off - assert input_ids == [ - # 28705, 13, is " \n" - 1, # bos - 32001, 1587, 13, 25997, 32000, 28705, 13, # system - 32001, 2188, 13, 21558, 32000, 28705, 13, # human - 32001, 13892, 13, 21558, 32000, 28705, 13, # gpt - 32001, 2188, 13, 267, 21558, 32000, 28705, 13, # human - 32001, 13892, 13, 267, 21558, 32000, 28705, 13, # gpt - 32001, 2188, 13, 12684, 17664, 32000, 28705, 13, # human - 32001, 13892, 13, 12684, 17664, 32000, 28705, 13, # gpt - ] - # fmt: on - - def test_no_train_on_input(self, sharegpt_dataset, tokenizer): - strategy = SimpleShareGPTPromptTokenizingStrategy( - ShareGPTPrompterV2( - conversation="chatml", - role_key_model=None, - role_key_human=None, - ), - tokenizer, - False, # train_on_inputs - 2048, # sequence_len - ) - - dataset_wrapper = TokenizedPromptDataset( - strategy, sharegpt_dataset, process_count=1 - ) - - labels = dataset_wrapper[0]["labels"] - # fmt: off - assert labels == [ - -100, # bos - -100, -100, -100, -100, -100, -100, -100, # system - -100, -100, -100, -100, -100, -100, -100, # human - -100, -100, 13, 21558, 32000, 28705, 13, # gpt - -100, -100, -100, -100, -100, -100, -100, -100, # human - -100, -100, 13, 12684, 17664, 32000, 28705, 13, # gpt - ] - # fmt: on - - def test_no_train_on_input_with_weights( - self, sharegpt_dataset_with_weights, tokenizer - ): - strategy = SimpleShareGPTPromptTokenizingStrategy( - ShareGPTPrompterV2( - conversation="chatml", - role_key_model=None, - role_key_human=None, - ), - tokenizer, - False, # train_on_inputs - 2048, # sequence_len - ) - - dataset_wrapper = TokenizedPromptDataset( - strategy, sharegpt_dataset_with_weights, process_count=1 - ) - - labels = dataset_wrapper[0]["labels"] - # fmt: off - assert labels == [ - -100, # bos - -100, -100, -100, -100, -100, -100, -100, # system - -100, -100, -100, -100, -100, -100, -100, # human - -100, -100, -100, -100, -100, -100, -100, # gpt with weight zero - -100, -100, -100, -100, -100, -100, -100, -100, # human - -100, -100, 13, 267, 21558, 32000, 28705, 13, # gpt - -100, -100, -100, -100, -100, -100, -100, -100, # human - -100, -100, -100, -100, -100, -100, -100, -100 # gpt with weight zero - ] - # fmt: on - - def test_w_train_on_input(self, sharegpt_dataset, tokenizer): - strategy = SimpleShareGPTPromptTokenizingStrategy( - ShareGPTPrompterV2( - conversation="chatml", - role_key_model=None, - role_key_human=None, - ), - tokenizer, - True, # train_on_inputs - 2048, # sequence_len - ) - - dataset_wrapper = TokenizedPromptDataset( - strategy, sharegpt_dataset, process_count=1 - ) - - labels = dataset_wrapper[0]["labels"] - # fmt: off - assert labels == [ - 1, # bos - 32001, 1587, 13, 25997, 32000, 28705, 13, # system - 32001, 2188, 13, 21558, 32000, 28705, 13, # human - 32001, 13892, 13, 21558, 32000, 28705, 13, # gpt - 32001, 2188, 13, 12684, 17664, 32000, 28705, 13, # human - 32001, 13892, 13, 12684, 17664, 32000, 28705, 13, # gpt - ] - # fmt: on - - def test_w_train_on_input_with_weights( - self, sharegpt_dataset_with_weights, tokenizer - ): - strategy = SimpleShareGPTPromptTokenizingStrategy( - ShareGPTPrompterV2( - conversation="chatml", - role_key_model=None, - role_key_human=None, - ), - tokenizer, - True, # train_on_inputs - 2048, # sequence_len - ) - - dataset_wrapper = TokenizedPromptDataset( - strategy, sharegpt_dataset_with_weights, process_count=1 - ) - - labels = dataset_wrapper[0]["labels"] - # fmt: off - assert labels == [ - 1, # bos - 32001, 1587, 13, 25997, 32000, 28705, 13, # system - 32001, 2188, 13, 21558, 32000, 28705, 13, # human - -100, -100, -100, -100, -100, -100, -100, # gpt with weight 0 - -100, -100, -100, -100, -100, -100, -100, -100, # human with weight 0 - 32001, 13892, 13, 267, 21558, 32000, 28705, 13, # gpt - 32001, 2188, 13, 12684, 17664, 32000, 28705, 13, # human - -100, -100, -100, -100, -100, -100, -100, -100 # gpt with weight 0 - ] - # fmt: on - - def test_chatml_glaive(self, glaive_dataset, tokenizer): - strategy = GlaiveShareGPTPromptTokenizingStrategy( - ShareGPTPrompterV2( - conversation="chatml", - role_key_model=None, - role_key_human=None, - ), - tokenizer, - True, # train_on_inputs - 2048, # sequence_len - ) - - dataset_wrapper = TokenizedPromptDataset( - strategy, glaive_dataset, process_count=1 - ) - - labels = dataset_wrapper[0]["labels"] - # fmt: off - assert labels == [ - 1, # bos - 32001, 1587, 13, 3260, 349, 264, 1587, 11510, 32000, 28705, 13, # system - 32001, 2188, 13, 6325, 368, 1820, 264, 9314, 354, 528, 477, 1450, 2726, 298, 4222, 28804, 32000, 28705, 13, # human - 32001, 13892, 13, 28737, 28742, 28719, 7371, 28725, 562, 315, 949, 28742, 28707, 506, 272, 21368, 298, 1820, 22447, 28723, 28705, 523, 28766, 416, 1009, 772, 28766, 28767, 32000, 28705, 13 # gpt - ] - # fmt: on - - def test_multi_role_dataset(self, multi_role_dataset, tokenizer): - strategy = SimpleShareGPTPromptTokenizingStrategy( - ShareGPTPrompterV2(conversation="chatml", roles={"input": ["tool"]}), - tokenizer, - False, # train_on_inputs - 2048, # sequence_len - ) - - dataset_wrapper = TokenizedPromptDataset( - strategy, multi_role_dataset, process_count=1 - ) - - input_ids = dataset_wrapper[0]["input_ids"] - # fmt: off - assert input_ids == [ - 1, # bos - 32001, 1587, 13, 1730, 625, 28730, 769, 1223, 28732, 18373, 28731, 298, 625, 272, 8086, 354, 264, 2990, 32000, 28705, 13, # system - 32001, 2188, 13, 21558, 28725, 767, 28742, 28713, 272, 8086, 297, 1450, 2726, 28804, 32000, 28705, 13, # human - 32001, 13892, 13, 895, 528, 625, 369, 354, 368, 32000, 28705, 13, # gpt - 32001, 3921, 13, 527, 28730, 769, 1223, 28732, 2972, 2726, 28731, 32000, 28705, 13, # tool - 32001, 13892, 13, 1237, 8086, 297, 1450, 2726, 349, 28705, 28787, 28734, 11182, 304, 4376, 1780, 32000, 28705, 13 # gpt - ] - # fmt: on - - labels = dataset_wrapper[0]["labels"] - # fmt: off - assert labels == [ - -100, # bos - -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # system - -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # human - -100, -100, 13, 895, 528, 625, 369, 354, 368, 32000, 28705, 13, # gpt - -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # tool - -100, -100, 13, 1237, 8086, 297, 1450, 2726, 349, 28705, 28787, 28734, 11182, 304, 4376, 1780, 32000, 28705, 13 # gpt - ] - # fmt: on diff --git a/tests/test_normalize_config.py b/tests/test_normalize_config.py index 2e76ceb45d..0d663183d4 100644 --- a/tests/test_normalize_config.py +++ b/tests/test_normalize_config.py @@ -39,12 +39,12 @@ def test_chat_template_chatml(self): "datasets": [ { "path": "lorem/ipsum", - "type": "sharegpt", - "conversation": "vicuna_v1.1", + "type": "chat_template", + "chat_template": "gemma", }, { "path": "sit/amet", - "type": "sharegpt", + "type": "chat_template", }, ], } @@ -52,8 +52,8 @@ def test_chat_template_chatml(self): normalize_cfg_datasets(cfg) - assert cfg.datasets[0].conversation == "vicuna_v1.1" - assert cfg.datasets[1].conversation == "chatml" + assert cfg.datasets[0].chat_template == "gemma" + assert cfg.datasets[1].chat_template == "chatml" @patch("axolotl.utils.config.is_torch_bf16_gpu_available") def test_bf16_auto_setter_available(self, mock_bf16_avail): diff --git a/tests/test_prompt_tokenizers.py b/tests/test_prompt_tokenizers.py index 63e9a621bf..4fb72f3e1d 100644 --- a/tests/test_prompt_tokenizers.py +++ b/tests/test_prompt_tokenizers.py @@ -3,7 +3,6 @@ import json import logging import unittest -from copy import deepcopy from pathlib import Path from typing import Optional @@ -21,12 +20,8 @@ LLama2ChatTokenizingStrategy, ) from axolotl.prompt_strategies.orpo.chat_template import load -from axolotl.prompt_strategies.sharegpt import GlaiveShareGPTPromptTokenizingStrategy -from axolotl.prompt_tokenizers import ( - AlpacaPromptTokenizingStrategy, - ShareGPTPromptTokenizingStrategy, -) -from axolotl.prompters import AlpacaPrompter, PromptStyle, ShareGPTPrompterV2 +from axolotl.prompt_tokenizers import AlpacaPromptTokenizingStrategy +from axolotl.prompters import AlpacaPrompter, PromptStyle from axolotl.utils.dict import DictDefault LOG = logging.getLogger("axolotl") @@ -65,17 +60,6 @@ } -def prompt_strat(conversation, tokenizer): - "Helper function to create a prompt strategy for testing." - prompter = ShareGPTPrompterV2(conversation=conversation) - return ShareGPTPromptTokenizingStrategy( - prompter, - tokenizer, - False, - 2048, - ) - - class TestPromptTokenizationStrategies(unittest.TestCase): """ Test class for prompt tokenization strategies. @@ -98,196 +82,6 @@ def setUp(self) -> None: } ) - def test_sharegpt_integration(self): - with open( - Path(__file__).parent / "fixtures/conversation.json", encoding="utf-8" - ) as fin: - data = fin.read() - conversation = json.loads(data) - with open( - Path(__file__).parent / "fixtures/conversation.tokenized.json", - encoding="utf-8", - ) as fin: - data = fin.read() - tokenized_conversation = json.loads(data) - prompter = ShareGPTPrompterV2() - strat = ShareGPTPromptTokenizingStrategy( - prompter, - self.tokenizer, - False, - 2048, - ) - example = strat.tokenize_prompt(conversation) - for fields in ["input_ids", "attention_mask", "labels"]: - self.assertEqual(len(example[fields]), len(tokenized_conversation[fields])) - self.assertEqual(example[fields], tokenized_conversation[fields]) - - def test_sharegpt_warnings_integration(self): - with open( - Path(__file__).parent / "fixtures/conversation.missingturns.json", - encoding="utf-8", - ) as fin: - data = fin.read() - conversation = json.loads(data) - prompter = ShareGPTPrompterV2() - strat = ShareGPTPromptTokenizingStrategy( - prompter, - self.tokenizer, - False, - 2048, - ) - with self._caplog.at_level(logging.WARNING): - strat.tokenize_prompt(conversation) - assert "assistant turn has empty text" in self._caplog.records[1].message - - def test_sharegpt_warnings_turns(self): - conversation = { - "conversations": [ - {"from": "system", "value": "lorem"}, - {"from": "gpt", "value": "ipsum"}, - {"from": "human", "value": "dolor"}, - {"from": "human", "value": "dolor"}, - {"from": "gpt", "value": "sit"}, - ] - } - prompter = ShareGPTPrompterV2() - strat = ShareGPTPromptTokenizingStrategy( - prompter, - self.tokenizer, - False, - 2048, - ) - with self._caplog.at_level(logging.WARNING): - strat.tokenize_prompt(conversation) - assert ( - "Role did not alternate between turns (gpt and human)" - in self._caplog.records[0].message - ) - - def test_sharegpt_llama(self): - "Make sure the sharegpt/llama is tokenized and formatted correctly." - strat = prompt_strat("llama-2", self.tokenizer) - - def tokenize(conv): - return strat.tokenize_prompt(deepcopy(conv))["input_ids"] - - def decode(ids): - return strat.tokenizer.decode(ids) - - # fmt: off - # System message, multi-turn conversations - mt_ids = tokenize(test_data['multi_turn_sys']) - assert decode(mt_ids) == ' [INST] <>\nlorem\n<>\n\nabc [/INST] ipsum [INST] 123 [/INST] sit' - assert mt_ids == [1, 518, 25580, 29962, 3532, 14816, 29903, 6778, 13, 29880, 3668, 13, 29966, 829, 14816, 29903, 6778, 13, 13, 10736, 518, 29914, 25580, 29962, 23421, 2, 1, 518, 25580, 29962, 29871, 29896, 29906, 29941, 518, 29914, 25580, 29962, 7845, 2] - - # System message, single-turn conversations - st_ids = tokenize(test_data['single_turn_sys']) - assert decode(st_ids) == ' [INST] <>\nlorem\n<>\n\nabc [/INST] ipsum' - assert st_ids == [1, 518, 25580, 29962, 3532, 14816, 29903, 6778, 13, 29880, 3668, 13, 29966, 829, 14816, 29903, 6778, 13, 13, 10736, 518, 29914, 25580, 29962, 23421, 2] - - # No system message, single-turn - ns_ids = tokenize(test_data['single_turn_no_sys']) - assert decode(ns_ids) == ' [INST] abc [/INST] ipsum' - assert ns_ids == [1, 518, 25580, 29962, 25638, 518, 29914, 25580, 29962, 23421, 2] - - # No system message, multi-turn - ns_mt_ids = tokenize(test_data['multi_turn_no_sys']) - assert decode(ns_mt_ids) == ' [INST] abc [/INST] ipsum [INST] 123 [/INST] sit' - assert ns_mt_ids == [1, 518, 25580, 29962, 25638, 518, 29914, 25580, 29962, 23421, 2, 1, 518, 25580, 29962, 29871, 29896, 29906, 29941, 518, 29914, 25580, 29962, 7845, 2] - # fmt: on - - def test_sharegpt_mistral(self): - "Make sure the sharegpt/mistral is tokenized and formatted correctly." - strat = prompt_strat("mistral", self.tokenizer) - - def tokenize(conv): - return strat.tokenize_prompt(deepcopy(conv))["input_ids"] - - def decode(ids): - return strat.tokenizer.decode(ids) - - # fmt: off - # System message, multi-turn conversations - mt_ids = tokenize(test_data['multi_turn_sys']) - assert decode(mt_ids) == ' [INST] lorem\nabc [/INST] ipsum [INST] 123 [/INST] sit' - assert mt_ids == [1, 518, 25580, 29962, 29871, 301, 3668, 13, 10736, 518, 29914, 25580, 29962, 23421, 2, 518, 25580, 29962, 29871, 29896, 29906, 29941, 518, 29914, 25580, 29962, 7845, 2] - - # System message, single-turn conversations - st_ids = tokenize(test_data['single_turn_sys']) - assert decode(st_ids) == ' [INST] lorem\nabc [/INST] ipsum' - assert st_ids == [1, 518, 25580, 29962, 29871, 301, 3668, 13, 10736, 518, 29914, 25580, 29962, 23421, 2] - - # No system message, single-turn - ns_ids = tokenize(test_data['single_turn_no_sys']) - assert decode(ns_ids) == ' [INST] abc [/INST] ipsum' - assert ns_ids == [1, 518, 25580, 29962, 25638, 518, 29914, 25580, 29962, 23421, 2] - - # No system message, multi-turn - ns_mt_ids = tokenize(test_data['multi_turn_no_sys']) - assert decode(ns_mt_ids) == ' [INST] abc [/INST] ipsum [INST] 123 [/INST] sit' - assert ns_mt_ids == [1, 518, 25580, 29962, 25638, 518, 29914, 25580, 29962, 23421, 2, 518, 25580, 29962, 29871, 29896, 29906, 29941, 518, 29914, 25580, 29962, 7845, 2] - # fmt: on - - def test_sharegpt_changes_roles(self): - conversation = { - "roles": ["USER", "CHARACTER"], - "conversations": [ - {"from": "system", "value": "lorem"}, - {"from": "gpt", "value": "ipsum"}, - {"from": "human", "value": "dolor"}, - {"from": "gpt", "value": "sit"}, - ], - } - prompter = ShareGPTPrompterV2() - strat = ShareGPTPromptTokenizingStrategy( - prompter, - self.tokenizer, - False, - 2048, - ) - with self._caplog.at_level(logging.WARNING): - res = strat.tokenize_prompt(conversation) - assert "CHARACTER" in self.tokenizer.decode(res["input_ids"]) - - def test_sharegpt_assistant_label_ignore(self): - conversation = { - "roles": ["user", "assistant"], - "conversations": [ - {"from": "system", "value": "lorem"}, - {"from": "gpt", "value": "ipsum"}, - {"from": "human", "value": "dolor"}, - {"from": "gpt", "value": "sit"}, - ], - } - prompter = ShareGPTPrompterV2() - strat = ShareGPTPromptTokenizingStrategy( - prompter, - self.tokenizer, - False, - 2048, - ) - with self._caplog.at_level(logging.WARNING): - res = strat.tokenize_prompt(conversation) - idx = res["input_ids"].index(20255) # assistant token - assert res["labels"][idx] == -100 - - def test_glaive_tool_label_ignore(self): - conversation = { - "system": "SYSTEM: This is a system prompt", - "chat": "USER: Can you book a flight for me from New York to London? ASSISTANT: I'm sorry, but I don't have the capability to book flights. <|endoftext|>", - } - prompter = ShareGPTPrompterV2() - strat = GlaiveShareGPTPromptTokenizingStrategy( - prompter, - self.tokenizer, - False, - 2048, - ) - with self._caplog.at_level(logging.WARNING): - res = strat.tokenize_prompt(conversation) - idx = res["input_ids"].index(13566) # assistant token - assert res["labels"][idx] == -100 - def test_no_sys_prompt(self): """ tests the interface between the user and assistant parts diff --git a/tests/test_validation.py b/tests/test_validation.py index fb63977f5c..67670b1928 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -646,39 +646,6 @@ def test_merge_lora_no_bf16_fail(self, minimal_cfg): validate_config(cfg) - def test_sharegpt_deprecation(self, minimal_cfg): - cfg = ( - DictDefault( - {"datasets": [{"path": "lorem/ipsum", "type": "sharegpt:chat"}]} - ) - | minimal_cfg - ) - with self._caplog.at_level(logging.WARNING): - new_cfg = validate_config(cfg) - assert any( - "`type: sharegpt:chat` will soon be deprecated." in record.message - for record in self._caplog.records - ) - assert new_cfg.datasets[0].type == "sharegpt" - - cfg = ( - DictDefault( - { - "datasets": [ - {"path": "lorem/ipsum", "type": "sharegpt_simple:load_role"} - ] - } - ) - | minimal_cfg - ) - with self._caplog.at_level(logging.WARNING): - new_cfg = validate_config(cfg) - assert any( - "`type: sharegpt_simple` will soon be deprecated." in record.message - for record in self._caplog.records - ) - assert new_cfg.datasets[0].type == "sharegpt:load_role" - def test_no_conflict_save_strategy(self, minimal_cfg): cfg = ( DictDefault( diff --git a/tests/test_validation_dataset.py b/tests/test_validation_dataset.py index 389424217b..7e288f8165 100644 --- a/tests/test_validation_dataset.py +++ b/tests/test_validation_dataset.py @@ -48,9 +48,8 @@ def test_dataset_config_no_drop_param(self, minimal_cfg): | { "datasets": [ { - "path": "LDJnr/Puffin", - "type": "sharegpt", - "conversation": "chatml", + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", "shards": 10, } ] @@ -62,7 +61,6 @@ def test_dataset_config_no_drop_param(self, minimal_cfg): def _check_config(): assert checked_cfg.datasets[0].path == cfg.datasets[0].path assert checked_cfg.datasets[0].type == cfg.datasets[0].type - assert checked_cfg.datasets[0].conversation == cfg.datasets[0].conversation assert checked_cfg.datasets[0].shards == cfg.datasets[0].shards _check_config() From d4796cb6458e6d5b08771ba03fcf246eefc66f44 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 8 Nov 2024 14:02:25 -0500 Subject: [PATCH 0220/1405] increment version to 0.5.0 for next release (#2025) [skip ci] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 17347f0632..8249528f7f 100644 --- a/setup.py +++ b/setup.py @@ -89,7 +89,7 @@ def parse_requirements(): setup( name="axolotl", - version="0.4.1", + version="0.5.0", description="LLM Trainer", long_description="Axolotl is a tool designed to streamline the fine-tuning of various AI models, offering support for multiple configurations and architectures.", package_dir={"": "src"}, From e20b15bee3d81a4c61e46ffee4066c9182c514ed Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 8 Nov 2024 14:18:16 -0500 Subject: [PATCH 0221/1405] make publish to pypi manually dispatchable as a workflow (#2026) [skip ci] --- .github/workflows/pypi.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 04dbc6385c..194bcc2bd0 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -4,6 +4,7 @@ on: push: tags: - '*' + workflow_dispatch: jobs: pypi-publish: From e4af51eb66ffea450bdcd55b10be2b52661ad17e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 8 Nov 2024 14:48:04 -0500 Subject: [PATCH 0222/1405] remove direct dependency on fused dense lib (#2027) --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index 8249528f7f..6da562fc0c 100644 --- a/setup.py +++ b/setup.py @@ -100,9 +100,6 @@ def parse_requirements(): "flash-attn": [ "flash-attn==2.6.3", ], - "fused-dense-lib": [ - "fused-dense-lib @ git+https://github.com/Dao-AILab/flash-attention@v2.6.2#subdirectory=csrc/fused_dense_lib", - ], "deepspeed": [ "deepspeed==0.14.4", "deepspeed-kernels", From d356740ffa5dbe2d172e24b2b037692289f26c01 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 10 Nov 2024 12:45:47 -0500 Subject: [PATCH 0223/1405] move deprecated kwargs from trainer to trainingargs (#2028) --- src/axolotl/core/trainer_builder.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 7b83707b82..41486e1f12 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1933,6 +1933,12 @@ def build_training_arguments(self, total_num_steps): else: training_args_cls = AxolotlDPOConfig + if self.cfg.rl == "ipo": + training_args_kwargs["loss_type"] = "ipo" + training_args_kwargs["max_length"] = self.cfg.sequence_len + training_args_kwargs["max_completion_length"] = None + training_args_kwargs["max_prompt_length"] = self.cfg.sequence_len + training_args_kwargs["generate_during_eval"] = self.cfg.use_wandb if self.cfg.dpo_use_weighting is not None: training_args_kwargs["use_weighting"] = self.cfg.dpo_use_weighting @@ -1956,7 +1962,6 @@ def build(self, total_num_steps): training_args = self.build_training_arguments(total_num_steps) dpo_trainer_kwargs = {} if self.cfg.rl == "ipo": - dpo_trainer_kwargs["loss_type"] = "ipo" if self.cfg.dpo_label_smoothing: dpo_trainer_kwargs["label_smoothing"] = self.cfg.dpo_label_smoothing if self.eval_dataset: @@ -1970,12 +1975,6 @@ def build(self, total_num_steps): if self.cfg.rl in ["dpo", "ipo"]: trainer_cls = AxolotlDPOTrainer trainer_cls_args = [self.model, self.model_ref] - - # these aren't used for the ORPO trainer - dpo_trainer_kwargs["max_length"] = self.cfg.sequence_len - dpo_trainer_kwargs["max_target_length"] = None - dpo_trainer_kwargs["max_prompt_length"] = self.cfg.sequence_len - dpo_trainer_kwargs["generate_during_eval"] = self.cfg.use_wandb elif self.cfg.rl == "orpo": trainer_cls = AxolotlORPOTrainer trainer_cls_args = [self.model] From 9bc3ee6c75f585440c8a44cfdf947445a49eb326 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 11 Nov 2024 09:48:19 -0500 Subject: [PATCH 0224/1405] add axolotlai docker hub org to publish list (#2031) * add axolotlai docker hub org to publish list * fix to use latest actions docker metadata version * fix list in yaml for expected format for action * missed a change --- .github/workflows/base.yml | 6 ++++-- .github/workflows/main.yml | 12 +++++++++--- .github/workflows/nightlies.yml | 8 ++++++-- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index cd99b9772b..6dec90952d 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -47,9 +47,11 @@ jobs: uses: actions/checkout@v3 - name: Docker metadata id: metadata - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v5 with: - images: winglian/axolotl-base + images: | + winglian/axolotl-base + axolotlai/axolotl-base - name: Login to Docker Hub uses: docker/login-action@v2 with: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8f96461550..dc7f566178 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,7 +42,9 @@ jobs: id: metadata uses: docker/metadata-action@v5 with: - images: winglian/axolotl + images: | + winglian/axolotl + axolotlai/axolotl - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub @@ -104,7 +106,9 @@ jobs: id: metadata uses: docker/metadata-action@v5 with: - images: winglian/axolotl-cloud + images: | + winglian/axolotl-cloud + axolotlai/axolotl-cloud - name: Login to Docker Hub uses: docker/login-action@v3 with: @@ -146,7 +150,9 @@ jobs: id: metadata uses: docker/metadata-action@v5 with: - images: winglian/axolotl-cloud-term + images: | + winglian/axolotl-cloud-term + axolotlai/axolotl-cloud-term - name: Login to Docker Hub uses: docker/login-action@v3 with: diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 3d6c0e62d7..fa53b15a41 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -41,7 +41,9 @@ jobs: id: metadata uses: docker/metadata-action@v5 with: - images: winglian/axolotl + images: | + winglian/axolotl + axolotlai/axolotl tags: | type=raw,value={{ branch }}-{{ date 'YYYYMMDD' }} - name: Set up Docker Buildx @@ -103,7 +105,9 @@ jobs: id: metadata uses: docker/metadata-action@v5 with: - images: winglian/axolotl-cloud + images: | + winglian/axolotl-cloud + axolotlai/axolotl-cloud tags: | type=raw,value={{ branch }}-{{ date 'YYYYMMDD' }} - name: Login to Docker Hub From f68fb71005a9b50396008c9da10fb25730b06739 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 11 Nov 2024 15:09:11 -0500 Subject: [PATCH 0225/1405] update actions version for node16 deprecation (#2037) [skip ci] * update actions version for node16 deprecation * update pre-commit/action to use 3.0.1 for actions/cache@v4 dep * update docker/setup-buildx-action too to v3 --- .github/workflows/base.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/lint.yml | 6 +++--- .github/workflows/main.yml | 4 ++-- .github/workflows/nightlies.yml | 2 +- .github/workflows/pypi.yml | 4 ++-- .github/workflows/tests-nightly.yml | 10 +++++----- .github/workflows/tests.yml | 10 +++++----- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 6dec90952d..f46e1769f7 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -44,7 +44,7 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Docker metadata id: metadata uses: docker/metadata-action@v5 @@ -58,7 +58,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build uses: docker/build-push-action@v4 with: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 16cf774d71..f471296d3a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Quarto uses: quarto-dev/quarto-actions/setup@v2 - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: install dependencies diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 919cfd6545..8f1cfd981a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,9 +15,9 @@ jobs: name: pre-commit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: "3.10" cache: 'pip' # caching pip dependencies - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dc7f566178..e0443e9120 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -115,7 +115,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build uses: docker/build-push-action@v5 with: @@ -159,7 +159,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build uses: docker/build-push-action@v5 with: diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index fa53b15a41..cc1b6ea9f8 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -116,7 +116,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build uses: docker/build-push-action@v5 with: diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 194bcc2bd0..34d51b6137 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -17,10 +17,10 @@ jobs: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - name: Check out repository code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index cba39b94e7..52c28e9198 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -9,12 +9,12 @@ jobs: name: pre-commit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: "3.10" cache: 'pip' # caching pip dependencies - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 env: SKIP: no-commit-to-branch @@ -30,10 +30,10 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} cache: 'pip' # caching pip dependencies diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8ced4188b7..a9ae718ff1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,12 +20,12 @@ jobs: name: pre-commit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: "3.10" cache: 'pip' # caching pip dependencies - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 env: SKIP: no-commit-to-branch @@ -41,10 +41,10 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} cache: 'pip' # caching pip dependencies From 234e94e9ddf357b3d94fa0bac7030691601cb482 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 11 Nov 2024 15:09:29 -0500 Subject: [PATCH 0226/1405] replace references to personal docker hub to org docker hub (#2036) [skip ci] --- README.md | 8 ++++---- cicd/Dockerfile.jinja | 2 +- docker/Dockerfile | 2 +- docker/Dockerfile-cloud | 2 +- docker/Dockerfile-cloud-no-tmux | 2 +- docker/Dockerfile-tests | 2 +- docs/debugging.qmd | 6 +++--- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 077dd6fee5..7e4d2e376e 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ accelerate launch -m axolotl.cli.train https://raw.githubusercontent.com/axolotl #### Docker ```bash - docker run --gpus '"all"' --rm -it winglian/axolotl:main-latest + docker run --gpus '"all"' --rm -it axolotlai/axolotl:main-latest ``` Or run on the current files for development: @@ -178,7 +178,7 @@ accelerate launch -m axolotl.cli.train https://raw.githubusercontent.com/axolotl A more powerful Docker command to run would be this: ```bash -docker run --privileged --gpus '"all"' --shm-size 10g --rm -it --name axolotl --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 --mount type=bind,src="${PWD}",target=/workspace/axolotl -v ${HOME}/.cache/huggingface:/root/.cache/huggingface winglian/axolotl:main-latest +docker run --privileged --gpus '"all"' --shm-size 10g --rm -it --name axolotl --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 --mount type=bind,src="${PWD}",target=/workspace/axolotl -v ${HOME}/.cache/huggingface:/root/.cache/huggingface axolotlai/axolotl:main-latest ``` It additionally: @@ -210,7 +210,7 @@ docker run --privileged --gpus '"all"' --shm-size 10g --rm -it --name axolotl -- #### Cloud GPU -For cloud GPU providers that support docker images, use [`winglian/axolotl-cloud:main-latest`](https://hub.docker.com/r/winglian/axolotl-cloud/tags) +For cloud GPU providers that support docker images, use [`axolotlai/axolotl-cloud:main-latest`](https://hub.docker.com/r/axolotlai/axolotl-cloud/tags) - on Latitude.sh use this [direct link](https://latitude.sh/blueprint/989e0e79-3bf6-41ea-a46b-1f246e309d5c) - on JarvisLabs.ai use this [direct link](https://jarvislabs.ai/templates/axolotl) @@ -319,7 +319,7 @@ Write a job description in YAML as below: # dstack.yaml type: task -image: winglian/axolotl-cloud:main-20240429-py3.11-cu121-2.2.2 +image: axolotlai/axolotl-cloud:main-latest env: - HUGGING_FACE_HUB_TOKEN diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 8ce6550056..8fe84ac646 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -1,4 +1,4 @@ -FROM winglian/axolotl-base:{{ BASE_TAG }} +FROM axolotlai/axolotl-base:{{ BASE_TAG }} ENV TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6+PTX" ENV AXOLOTL_EXTRAS="{{ AXOLOTL_EXTRAS }}" diff --git a/docker/Dockerfile b/docker/Dockerfile index 4872b3907c..6e14a70a76 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ ARG BASE_TAG=main-base -FROM winglian/axolotl-base:$BASE_TAG +FROM axolotlai/axolotl-base:$BASE_TAG ARG TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6+PTX" ARG AXOLOTL_EXTRAS="" diff --git a/docker/Dockerfile-cloud b/docker/Dockerfile-cloud index c0bb266d28..d7e3277d2f 100644 --- a/docker/Dockerfile-cloud +++ b/docker/Dockerfile-cloud @@ -1,5 +1,5 @@ ARG BASE_TAG=main -FROM winglian/axolotl:$BASE_TAG +FROM axolotlai/axolotl:$BASE_TAG ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" ENV HUGGINGFACE_HUB_CACHE="/workspace/data/huggingface-cache/hub" diff --git a/docker/Dockerfile-cloud-no-tmux b/docker/Dockerfile-cloud-no-tmux index 3e59d41191..6dfea46779 100644 --- a/docker/Dockerfile-cloud-no-tmux +++ b/docker/Dockerfile-cloud-no-tmux @@ -1,5 +1,5 @@ ARG BASE_TAG=main -FROM winglian/axolotl:$BASE_TAG +FROM axolotlai/axolotl:$BASE_TAG ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" ENV HUGGINGFACE_HUB_CACHE="/workspace/data/huggingface-cache/hub" diff --git a/docker/Dockerfile-tests b/docker/Dockerfile-tests index 4a946372df..36c0a639e3 100644 --- a/docker/Dockerfile-tests +++ b/docker/Dockerfile-tests @@ -1,5 +1,5 @@ ARG BASE_TAG=main-base -FROM winglian/axolotl-base:$BASE_TAG +FROM axolotlai/axolotl-base:$BASE_TAG ARG TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6+PTX" ARG AXOLOTL_EXTRAS="" diff --git a/docs/debugging.qmd b/docs/debugging.qmd index 029549d85b..d119dce18f 100644 --- a/docs/debugging.qmd +++ b/docs/debugging.qmd @@ -185,7 +185,7 @@ style="border-radius: 10px; display: block; margin: auto;" width="560" height="3 ## Debugging With Docker -Using [official Axolotl Docker images](https://hub.docker.com/r/winglian/axolotl/tags) is a great way to debug your code, and is a very popular way to use Axolotl. Attaching VSCode to Docker takes a few more steps. +Using [official Axolotl Docker images](https://hub.docker.com/r/axolotlai/axolotl/tags) is a great way to debug your code, and is a very popular way to use Axolotl. Attaching VSCode to Docker takes a few more steps. ### Setup @@ -202,11 +202,11 @@ cd axolotl Next, run the desired docker image and mount the current directory. Below is a docker command you can run to do this:[^2] ```bash -docker run --privileged --gpus '"all"' --shm-size 10g --rm -it --name axolotl --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 --mount type=bind,src="${PWD}",target=/workspace/axolotl -v ${HOME}/.cache/huggingface:/root/.cache/huggingface winglian/axolotl:main-py3.10-cu118-2.0.1 +docker run --privileged --gpus '"all"' --shm-size 10g --rm -it --name axolotl --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 --mount type=bind,src="${PWD}",target=/workspace/axolotl -v ${HOME}/.cache/huggingface:/root/.cache/huggingface axolotlai/axolotl:main-py3.10-cu118-2.0.1 ``` >[!Tip] -> To understand which containers are available, see the [Docker section of the README](../README.md#docker) and the [DockerHub repo](https://hub.docker.com/r/winglian/axolotl/tags). For details of how the Docker containers are built, see axolotl's [Docker CI builds](../.github/workflows/main.yml). +> To understand which containers are available, see the [Docker section of the README](../README.md#docker) and the [DockerHub repo](https://hub.docker.com/r/axolotlai/axolotl/tags). For details of how the Docker containers are built, see axolotl's [Docker CI builds](../.github/workflows/main.yml). You will now be in the container. Next, perform an editable install of Axolotl: From dc8f9059f71d8f12ea4a625e45911f8bf6bc9247 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 12 Nov 2024 03:09:58 +0700 Subject: [PATCH 0227/1405] feat: add metharme chat_template (#2033) [skip ci] * feat: add metharme chat_template * fix: add eos token --- src/axolotl/utils/chat_templates.py | 1 + src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 5a080c1e6f..ffe5e24853 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -30,6 +30,7 @@ "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', "qwen_25": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n", "exaone": "{% for message in messages %}{% if loop.first and message['role'] != 'system' %}{{ '[|system|][|endofturn|]\n' }}{% endif %}{{ '[|' + message['role'] + '|]' + message['content'] }}{% if message['role'] == 'user' %}{{ '\n' }}{% else %}{{ '[|endofturn|]\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '[|assistant|]' }}{% endif %}", + "metharme": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% else %}{% set loop_messages = messages %}{% set system_message = 'Enter RP mode. You shall reply to the user while staying in character. Your responses must be detailed, creative, immersive, and drive the scenario forward.' %}{% endif %}{{ '<|system|>' + system_message }}{% for message in loop_messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|user|>' + content.strip() }}{% elif message['role'] == 'assistant' %}{{ '<|model|>' + content.strip() }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|model|>' }}{% else %}{{ eos_token }}{% endif %}", } diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 8310fd3e57..8d2065bdb8 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -58,6 +58,7 @@ class ChatTemplate(str, Enum): qwen_25 = "qwen_25" # pylint: disable=invalid-name tokenizer_default = "tokenizer_default" # pylint: disable=invalid-name exaone = "exaone" # pylint: disable=invalid-name + metharme = "metharme" # pylint: disable=invalid-name class DeprecatedParameters(BaseModel): From 3931a4276323b6e35126d7859f2aab08d5e59abe Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 11 Nov 2024 15:10:34 -0500 Subject: [PATCH 0228/1405] change deprecated modal Stub to App (#2038) --- cicd/multigpu.py | 8 ++++---- cicd/tests.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cicd/multigpu.py b/cicd/multigpu.py index da726b4731..0ea4c8cc11 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -10,7 +10,7 @@ import jinja2 import modal from jinja2 import select_autoescape -from modal import Image, Stub +from modal import App, Image cicd_path = pathlib.Path(__file__).parent.resolve() @@ -46,7 +46,7 @@ .pip_install("fastapi==0.110.0", "pydantic==2.6.3") ) -stub = Stub("Axolotl CI/CD", secrets=[]) +app = App("Axolotl CI/CD", secrets=[]) N_GPUS = int(os.environ.get("N_GPUS", 2)) @@ -61,7 +61,7 @@ def run_cmd(cmd: str, run_folder: str): exit(exit_code) # pylint: disable=consider-using-sys-exit -@stub.function( +@app.function( image=cicd_image, gpu=GPU_CONFIG, timeout=60 * 60, @@ -72,6 +72,6 @@ def cicd_pytest(): run_cmd("./cicd/multigpu.sh", "/workspace/axolotl") -@stub.local_entrypoint() +@app.local_entrypoint() def main(): cicd_pytest.remote() diff --git a/cicd/tests.py b/cicd/tests.py index 9ebce9815f..812ef7b426 100644 --- a/cicd/tests.py +++ b/cicd/tests.py @@ -10,7 +10,7 @@ import jinja2 import modal from jinja2 import select_autoescape -from modal import Image, Stub +from modal import App, Image cicd_path = pathlib.Path(__file__).parent.resolve() @@ -47,7 +47,7 @@ .pip_install("fastapi==0.110.0", "pydantic==2.6.3") ) -stub = Stub("Axolotl CI/CD", secrets=[]) +app = App("Axolotl CI/CD", secrets=[]) N_GPUS = int(os.environ.get("N_GPUS", 1)) @@ -62,7 +62,7 @@ def run_cmd(cmd: str, run_folder: str): exit(exit_code) # pylint: disable=consider-using-sys-exit -@stub.function( +@app.function( image=cicd_image, gpu=GPU_CONFIG, timeout=60 * 60, @@ -73,6 +73,6 @@ def cicd_pytest(): run_cmd("./cicd/cicd.sh", "/workspace/axolotl") -@stub.local_entrypoint() +@app.local_entrypoint() def main(): cicd_pytest.remote() From 9f1cf9b17c3761ea806720ba44dac44def3c1451 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 12 Nov 2024 12:51:37 +0700 Subject: [PATCH 0229/1405] fix: handle sharegpt dataset missing (#2035) * fix: handle sharegpt dataset missing * fix: explanation * feat: add test --- .../config/models/input/v0_4_1/__init__.py | 7 ++- tests/test_validation_dataset.py | 56 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 8d2065bdb8..dc7693f06c 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -789,7 +789,12 @@ def deprecate_sharegpt_datasets(cls, datasets): if not ds_cfg.get("type"): continue - if ds_cfg["type"].startswith("sharegpt"): + ds_type = ds_cfg["type"] + # skip if it's a dict (for custom user instruction prompt) + if isinstance(ds_type, dict): + continue + + if isinstance(ds_type, str) and ds_type.startswith("sharegpt"): raise ValueError( "`type: sharegpt.*` is deprecated. Please use `type: chat_template` instead." ) diff --git a/tests/test_validation_dataset.py b/tests/test_validation_dataset.py index 7e288f8165..14f9d34627 100644 --- a/tests/test_validation_dataset.py +++ b/tests/test_validation_dataset.py @@ -234,3 +234,59 @@ def _check_config(): ) _check_config() + + def test_dataset_sharegpt_deprecation(self, minimal_cfg): + cfg = DictDefault( + minimal_cfg + | { + "chat_template": "chatml", + "datasets": [ + { + "path": "LDJnr/Puffin", + "type": "sharegpt", + "conversation": "chatml", + } + ], + } + ) + + # Check sharegpt deprecation is raised + with pytest.raises(ValueError, match=r".*type: sharegpt.*` is deprecated.*"): + validate_config(cfg) + + # Check that deprecation is not thrown for non-str type + cfg = DictDefault( + minimal_cfg + | { + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": { + "field_instruction": "instruction", + "field_output": "output", + "field_system": "system", + "format": "<|user|> {instruction} {input} <|model|>", + "no_input_format": "<|user|> {instruction} <|model|>", + "system_prompt": "", + }, + } + ], + } + ) + + validate_config(cfg) + + # Check that deprecation is not thrown for non-sharegpt type + cfg = DictDefault( + minimal_cfg + | { + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + } + ], + } + ) + + validate_config(cfg) From ad435a3b09958ff9755a8df6ab695fe7e2831271 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 12 Nov 2024 17:58:26 -0500 Subject: [PATCH 0230/1405] add P2P env when multi-gpu but not the full node (#2041) Co-authored-by: Wing Lian --- src/axolotl/utils/environment.py | 25 +++++++++++++++++++++++++ src/axolotl/utils/trainer.py | 4 ++++ 2 files changed, 29 insertions(+) create mode 100644 src/axolotl/utils/environment.py diff --git a/src/axolotl/utils/environment.py b/src/axolotl/utils/environment.py new file mode 100644 index 0000000000..c447b1ee25 --- /dev/null +++ b/src/axolotl/utils/environment.py @@ -0,0 +1,25 @@ +""" +utils to get GPU info for the current environment +""" +from accelerate.utils.environment import ( + check_cuda_p2p_ib_support as accelerate_check_cuda_p2p_ib_support, +) +from accelerate.utils.environment import get_gpu_info + + +def check_cuda_p2p_ib_support(): + if not accelerate_check_cuda_p2p_ib_support(): + return False + unsupported_devices = {"RTX 6000 Ada"} + try: + device_names, device_count = get_gpu_info() + if 1 < device_count < 8: + if any( + device_name in unsupported_device + for device_name in device_names + for unsupported_device in unsupported_devices + ): + return False + except Exception: # pylint: disable=broad-except # nosec + pass + return True diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 7ebf384aff..a552905f7c 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -17,6 +17,7 @@ from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder from axolotl.utils.distributed import reduce_and_broadcast +from axolotl.utils.environment import check_cuda_p2p_ib_support from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths LOG = get_logger("axolotl") @@ -461,6 +462,9 @@ def setup_fsdp_envs(cfg): def prepare_optim_env(cfg): + if not check_cuda_p2p_ib_support(): + if os.getenv("NCCL_P2P_DISABLE") is None: + os.environ["NCCL_P2P_DISABLE"] = "1" if cfg.fsdp: setup_fsdp_envs(cfg) elif cfg.deepspeed: From 810ebc2c0e6a723069a2648796795361c5559470 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 12 Nov 2024 23:20:47 -0500 Subject: [PATCH 0231/1405] invert the string in string check for p2p device check (#2044) --- src/axolotl/utils/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/environment.py b/src/axolotl/utils/environment.py index c447b1ee25..cf2e5d23d2 100644 --- a/src/axolotl/utils/environment.py +++ b/src/axolotl/utils/environment.py @@ -15,7 +15,7 @@ def check_cuda_p2p_ib_support(): device_names, device_count = get_gpu_info() if 1 < device_count < 8: if any( - device_name in unsupported_device + unsupported_device in device_name for device_name in device_names for unsupported_device in unsupported_devices ): From 7b78a315934ec8915c256efe8c277a764c219ab9 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 13 Nov 2024 22:06:00 +0700 Subject: [PATCH 0232/1405] feat: print out dataset length even if not preprocess (#2034) [skip ci] --- src/axolotl/utils/trainer.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index a552905f7c..2d3a6944f7 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -185,11 +185,10 @@ def process_datasets_for_packing(cfg, train_dataset, eval_dataset): min_sequence_len=cfg.min_sample_len or 2, ) - if cfg.is_preprocess: - min_input_len = np.min(get_dataset_lengths(train_dataset)) - LOG.debug(f"min_input_len: {min_input_len}", main_process_only=True) - max_input_len = np.max(get_dataset_lengths(train_dataset)) - LOG.debug(f"max_input_len: {max_input_len}", main_process_only=True) + min_input_len = np.min(get_dataset_lengths(train_dataset)) + LOG.debug(f"min_input_len: {min_input_len}", main_process_only=True) + max_input_len = np.max(get_dataset_lengths(train_dataset)) + LOG.debug(f"max_input_len: {max_input_len}", main_process_only=True) if cfg.model_config_type == "mamba": LOG.info("dropping attention_mask column") From a4b1cc6df05078c992546773b55509cc736ba027 Mon Sep 17 00:00:00 2001 From: Oliver Molenschot <91694286+olivermolenschot@users.noreply.github.com> Date: Wed, 13 Nov 2024 07:06:25 -0800 Subject: [PATCH 0233/1405] Add example YAML file for training Mistral using DPO (#2029) [skip ci] * Add example YAML file for training Mistral using DPO * chore: lint * Apply suggestions from code review Co-authored-by: NanoCode012 * Update mistral-dpo.yml Adding qlora and removing role-related data (unecessary) * Rename mistral-dpo.yml to mistral-dpo-qlora.yml * Apply suggestions from code review Co-authored-by: NanoCode012 --------- Co-authored-by: Wing Lian Co-authored-by: NanoCode012 --- examples/mistral/mistral-dpo-qlora.yml | 93 ++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 examples/mistral/mistral-dpo-qlora.yml diff --git a/examples/mistral/mistral-dpo-qlora.yml b/examples/mistral/mistral-dpo-qlora.yml new file mode 100644 index 0000000000..a558e04535 --- /dev/null +++ b/examples/mistral/mistral-dpo-qlora.yml @@ -0,0 +1,93 @@ +#Note that we are switching from the regular chat template to chatml. +#If you experience problems with the special tokens, training for more epochs can help. +#After training, merge the model before inference otherwise you might +#face problems with the special tokens. + +base_model: mistralai/Mistral-7B-Instruct-v0.2 +model_type: MistralForCausalLM +tokenizer_type: LlamaTokenizer + +load_in_8bit: false +load_in_4bit: true +strict: false + +chat_template: chatml +rl: dpo +datasets: + - path: olivermolenschot/alpaca_messages_dpo_test + type: chat_template.default + field_messages: conversation + field_chosen: chosen + field_rejected: rejected + message_field_role: role + message_field_content: content + +dataset_prepared_path: +val_set_size: 0.05 +output_dir: ./outputs/dpo-qlora + +sequence_len: 2048 +sample_packing: false +pad_to_sequence_len: true + +adapter: qlora +lora_model_dir: +lora_r: 8 +lora_alpha: 16 +lora_dropout: 0.2 +lora_target_linear: true +lora_fan_in_fan_out: + +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj +lora_modules_to_save: + - embed_tokens + - lm_head + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 16 +num_epochs: 6 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0001 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: false +s2_attention: + +warmup_steps: 10 +evals_per_epoch: 4 +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: + bos_token: "<|im_start|>" + eos_token: "<|im_end|>" From 8c480b28043584c46ed4d9d574ce63ee35e29dea Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 13 Nov 2024 22:06:41 +0700 Subject: [PATCH 0234/1405] fix: inference not using chat_template (#2019) [skip ci] --- src/axolotl/cli/__init__.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 84586ccc37..589b6b575a 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -190,18 +190,15 @@ def do_inference( ): model, tokenizer = load_model_and_tokenizer(cfg=cfg, cli_args=cli_args) prompter = cli_args.prompter - default_tokens = {"unk_token": "", "bos_token": "", "eos_token": ""} - - for token, symbol in default_tokens.items(): - # If the token isn't already specified in the config, add it - if not (cfg.special_tokens and token in cfg.special_tokens): - tokenizer.add_special_tokens({token: symbol}) prompter_module = None + chat_template_str = None if prompter: prompter_module = getattr( importlib.import_module("axolotl.prompters"), prompter ) + elif cfg.chat_template: + chat_template_str = get_chat_template(cfg.chat_template) model = model.to(cfg.device, dtype=cfg.torch_dtype) @@ -211,13 +208,31 @@ def do_inference( instruction = get_multi_line_input() if not instruction: return + if prompter_module: prompt: str = next( prompter_module().build_prompt(instruction=instruction.strip("\n")) ) else: prompt = instruction.strip() - batch = tokenizer(prompt, return_tensors="pt", add_special_tokens=True) + + if chat_template_str: + batch = tokenizer.apply_chat_template( + [ + { + "role": "user", + "content": prompt, + } + ], + return_tensors="pt", + add_special_tokens=True, + add_generation_prompt=True, + chat_template=chat_template_str, + tokenize=True, + return_dict=True, + ) + else: + batch = tokenizer(prompt, return_tensors="pt", add_special_tokens=True) print("=" * 40) model.eval() @@ -257,13 +272,6 @@ def do_inference_gradio( model, tokenizer = load_model_and_tokenizer(cfg=cfg, cli_args=cli_args) prompter = cli_args.prompter - # default_tokens = {"unk_token": "", "bos_token": "", "eos_token": ""} - default_tokens: Dict[str, str] = {} - - for token, symbol in default_tokens.items(): - # If the token isn't already specified in the config, add it - if not (cfg.special_tokens and token in cfg.special_tokens): - tokenizer.add_special_tokens({token: symbol}) prompter_module = None chat_template_str = None From 28924fc791e41b6ee39592711e6a66a96f5c2b77 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 13 Nov 2024 22:06:59 +0700 Subject: [PATCH 0235/1405] feat: cancel ongoing tests if new CI is triggered (#2046) [skip ci] --- .github/workflows/tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a9ae718ff1..dfdab73826 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,6 +15,11 @@ on: - '.github/workflows/*.yml' workflow_dispatch: +# Cancel jobs on the same ref if a new one is triggered +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: pre-commit: name: pre-commit From 4e1891b12be3610374d6c8c1a4a4dd13c6574f4f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 13 Nov 2024 22:07:24 +0700 Subject: [PATCH 0236/1405] feat: upgrade to liger 0.4.1 (#2045) --- requirements.txt | 2 +- src/axolotl/integrations/liger/__init__.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index d1fdccaf77..c7f5c9433c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ tensorboard python-dotenv==1.0.1 autoawq>=0.2.5 triton>=2.3.0 -liger-kernel==0.4.0 +liger-kernel==0.4.1 mamba-ssm==1.2.0.post1 diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index a64d748c67..fda98e469f 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -23,6 +23,7 @@ import sys from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss +from liger_kernel.transformers.functional import liger_cross_entropy from liger_kernel.transformers.monkey_patch import MODEL_TYPE_TO_APPLY_LIGER_FN from liger_kernel.transformers.rms_norm import LigerRMSNorm from liger_kernel.transformers.rope import liger_rotary_pos_emb @@ -82,7 +83,9 @@ def pre_model_load(self, cfg): if cfg.liger_glu_activation: modeling_jamba.JambaMLP = LigerSwiGLUMLP if cfg.liger_cross_entropy: - modeling_jamba.CrossEntropyLoss = LigerCrossEntropyLoss + from transformers.loss.loss_utils import nn + + nn.functional.cross_entropy = liger_cross_entropy if cfg.liger_fused_linear_cross_entropy: modeling_jamba.JambaForCausalLM.forward = jamba_lce_forward elif cfg.model_config_type == "deepseek_v2": @@ -106,6 +109,8 @@ def pre_model_load(self, cfg): if cfg.liger_glu_activation: modeling_mod.DeepseekV2MLP.forward = LigerSwiGLUMLP.forward if cfg.liger_cross_entropy: + # We do not patch `nn.functional.cross_entropy` for DeepseekV2 as it still uses + # nn.CrossEntropyLoss in the forward method. modeling_mod.CrossEntropyLoss = LigerCrossEntropyLoss if cfg.liger_fused_linear_cross_entropy: modeling_mod.DeepseekV2ForCausalLM.forward = deepseekv2_lce_forward From 0e8eb96e072dafb1d3aa4a68eb56a8f2c2b8cc97 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 13 Nov 2024 10:21:48 -0500 Subject: [PATCH 0237/1405] run pypi release action on tag create w version (#2047) --- .github/workflows/pypi.yml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 34d51b6137..a8131fa54e 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -1,12 +1,29 @@ name: publish pypi on: - push: + create: tags: - - '*' + - 'v*' workflow_dispatch: jobs: + setup_release: + name: Create Release + runs-on: ubuntu-latest + steps: + - name: Get the tag version + id: extract_branch + run: echo ::set-output name=branch::${GITHUB_REF#refs/tags/} + shell: bash + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.extract_branch.outputs.branch }} + release_name: ${{ steps.extract_branch.outputs.branch }} pypi-publish: name: Upload release to PyPI runs-on: ubuntu-latest From 01881c3113980038bea43f95a37dc7e352d8a47e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 13 Nov 2024 13:15:49 -0500 Subject: [PATCH 0238/1405] make sure to tag images in docker for tagged releases (#2051) [skip ci] * make sure to tag images in docker for tagged releases * fix tag event --- .github/workflows/main.yml | 14 +++++++++++--- .github/workflows/pypi.yml | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0443e9120..8005f66837 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,6 +4,8 @@ on: push: branches: - "main" + tags: + - "v*" workflow_dispatch: jobs: @@ -45,6 +47,9 @@ jobs: images: | winglian/axolotl axolotlai/axolotl + tags: | + type=ref,event=tag + type=ref,event=push - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub @@ -58,7 +63,7 @@ jobs: with: context: . build-args: | - BASE_TAG=${{ github.ref_name }}-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }} + BASE_TAG=${{ github.ref_type == 'tag' && 'main' || github.ref_name }}-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }} CUDA=${{ matrix.cuda }} PYTORCH_VERSION=${{ matrix.pytorch }} AXOLOTL_ARGS=${{ matrix.axolotl_args }} @@ -121,7 +126,7 @@ jobs: with: context: . build-args: | - BASE_TAG=${{ github.ref_name }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} + BASE_TAG=${{ github.ref_type == 'tag' && 'main' || github.ref_name }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} CUDA=${{ matrix.cuda }} file: ./docker/Dockerfile-cloud push: ${{ github.event_name != 'pull_request' }} @@ -153,6 +158,9 @@ jobs: images: | winglian/axolotl-cloud-term axolotlai/axolotl-cloud-term + tags: | + type=ref,event=tag + type=ref,event=push - name: Login to Docker Hub uses: docker/login-action@v3 with: @@ -165,7 +173,7 @@ jobs: with: context: . build-args: | - BASE_TAG=${{ github.ref_name }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} + BASE_TAG=${{ github.ref_type == 'tag' && 'main' || github.ref_name }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} CUDA=${{ matrix.cuda }} file: ./docker/Dockerfile-cloud-no-tmux push: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index a8131fa54e..ae0a77e2fa 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -1,7 +1,7 @@ name: publish pypi on: - create: + push: tags: - 'v*' workflow_dispatch: @@ -27,6 +27,7 @@ jobs: pypi-publish: name: Upload release to PyPI runs-on: ubuntu-latest + needs: [setup_release] environment: name: pypi url: https://pypi.org/p/axolotl From 010d0e7ff3c24bd40f63fe1083ffe370d793b8f1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 13 Nov 2024 13:16:16 -0500 Subject: [PATCH 0239/1405] retry flaky test_packing_stream_dataset test that timesout on read (#2052) [skip ci] --- requirements-tests.txt | 1 + tests/test_packed_pretraining.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/requirements-tests.txt b/requirements-tests.txt index 9cda381d0c..7a34809da8 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,2 +1,3 @@ pytest pytest-xdist +pytest-retry diff --git a/tests/test_packed_pretraining.py b/tests/test_packed_pretraining.py index 5d517585fd..fbb776aa51 100644 --- a/tests/test_packed_pretraining.py +++ b/tests/test_packed_pretraining.py @@ -2,6 +2,7 @@ import functools import unittest +import pytest import torch from datasets import load_dataset from torch.utils.data import DataLoader @@ -21,6 +22,7 @@ def setUp(self) -> None: self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") self.tokenizer.pad_token = "" + @pytest.mark.flaky(retries=3, delay=5) def test_packing_stream_dataset(self): # pylint: disable=duplicate-code dataset = load_dataset( From f2145a3ccb1cda4fb5a0c74df3c8865e2f66e33a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 13 Nov 2024 13:16:47 -0500 Subject: [PATCH 0240/1405] add default torch version if not installed, and support for xformers new wheels (#2049) --- setup.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6da562fc0c..cec5f41084 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,10 @@ def parse_requirements(): else: # detect the version of torch already installed # and set it so dependencies don't clobber the torch version - torch_version = version("torch") + try: + torch_version = version("torch") + except PackageNotFoundError: + torch_version = "2.5.1" _install_requires.append(f"torch=={torch_version}") version_match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", torch_version) @@ -54,6 +57,10 @@ def parse_requirements(): if (major, minor) >= (2, 5): _install_requires.pop(_install_requires.index(xformers_version)) + if patch == 0: + _install_requires.append("xformers==0.0.28.post2") + else: + _install_requires.append("xformers==0.0.28.post3") _install_requires.pop(_install_requires.index(autoawq_version)) elif (major, minor) >= (2, 4): if patch == 0: From c5eb9ea2c201f1bd52098fe8a40911972f945ae4 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 13 Nov 2024 14:04:28 -0500 Subject: [PATCH 0241/1405] fix push to main and tag semver build for docker ci (#2054) --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8005f66837..28dad00465 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,8 +48,8 @@ jobs: winglian/axolotl axolotlai/axolotl tags: | - type=ref,event=tag - type=ref,event=push + type=ref,event=branch + type=semver,pattern={{version}} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub @@ -159,8 +159,8 @@ jobs: winglian/axolotl-cloud-term axolotlai/axolotl-cloud-term tags: | - type=ref,event=tag - type=ref,event=push + type=ref,event=branch + type=semver,pattern={{version}} - name: Login to Docker Hub uses: docker/login-action@v3 with: From 342935cff35d70a260c428d1b419a2b5d8989a99 Mon Sep 17 00:00:00 2001 From: Sunny Liu Date: Wed, 13 Nov 2024 15:17:34 -0500 Subject: [PATCH 0242/1405] Update unsloth for torch.cuda.amp deprecation (#2042) * update deprecated unsloth tirch cuda amp decorator * WIP fix torch.cuda.amp deprecation * lint * laxing torch version requirement * remove use of partial * remove use of partial * lint --------- Co-authored-by: sunny --- .../utils/gradient_checkpointing/unsloth.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/axolotl/utils/gradient_checkpointing/unsloth.py b/src/axolotl/utils/gradient_checkpointing/unsloth.py index fbe8346be2..7a14614b18 100644 --- a/src/axolotl/utils/gradient_checkpointing/unsloth.py +++ b/src/axolotl/utils/gradient_checkpointing/unsloth.py @@ -14,6 +14,16 @@ # See the License for the specific language governing permissions and # limitations under the License. import torch +from packaging import version + +torch_version = version.parse(torch.__version__) + +if torch_version < version.parse("2.4.0"): + torch_cuda_amp_custom_fwd = torch.cuda.amp.custom_fwd + torch_cuda_amp_custom_bwd = torch.cuda.amp.custom_bwd +else: + torch_cuda_amp_custom_fwd = torch.amp.custom_fwd(device_type="cuda") + torch_cuda_amp_custom_bwd = torch.amp.custom_bwd(device_type="cuda") class Unsloth_Offloaded_Gradient_Checkpointer( # pylint: disable=invalid-name @@ -25,7 +35,7 @@ class Unsloth_Offloaded_Gradient_Checkpointer( # pylint: disable=invalid-name """ @staticmethod - @torch.cuda.amp.custom_fwd + @torch_cuda_amp_custom_fwd def forward(ctx, forward_function, hidden_states, *args): saved_hidden_states = hidden_states.to("cpu", non_blocking=True) with torch.no_grad(): @@ -36,7 +46,7 @@ def forward(ctx, forward_function, hidden_states, *args): return output @staticmethod - @torch.cuda.amp.custom_bwd + @torch_cuda_amp_custom_bwd def backward(ctx, dY): (hidden_states,) = ctx.saved_tensors hidden_states = hidden_states.to("cuda", non_blocking=True).detach() From 659ee5d723e3af491b5a6842ba02351f1e5ed0d6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 13 Nov 2024 17:07:41 -0500 Subject: [PATCH 0243/1405] don't cancel the tests on main automatically for concurrency (#2055) [skip ci] --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dfdab73826..6b44317d93 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,7 +18,7 @@ on: # Cancel jobs on the same ref if a new one is triggered concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: pre-commit: From 1d7aee0ad211814e028c28a840e415c7431a9a9d Mon Sep 17 00:00:00 2001 From: Sunny Liu Date: Wed, 13 Nov 2024 17:10:17 -0500 Subject: [PATCH 0244/1405] ADOPT optimizer integration (#2032) [skip ci] * adopt integration * stuff * doc and test for ADOPT * rearrangement * fixed formatting * hacking pre-commit * chore: lint * update module doc for adopt optimizer * remove un-necessary example yaml for adopt optimizer * skip test adopt if torch<2.5.1 * formatting * use version.parse * specifies required torch version for adopt_adamw --------- Co-authored-by: sunny Co-authored-by: Wing Lian --- docs/config.qmd | 1 + src/axolotl/core/trainer_builder.py | 18 +- .../config/models/input/v0_4_1/__init__.py | 1 + src/axolotl/utils/optimizers/adopt.py | 508 ++++++++++++++++++ tests/e2e/test_optimizers.py | 45 +- tests/e2e/utils.py | 20 +- 6 files changed, 588 insertions(+), 5 deletions(-) create mode 100644 src/axolotl/utils/optimizers/adopt.py diff --git a/docs/config.qmd b/docs/config.qmd index 09691bc770..4349f0f09f 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -405,6 +405,7 @@ lr_div_factor: # Learning rate div factor # - adamw_torch_fused # - adamw_torch_xla # - adamw_apex_fused +# - adopt_adamw (only for torch version >= 2.5.1) # - adafactor # - adamw_anyprecision # - sgd diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 41486e1f12..972c291406 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -436,7 +436,13 @@ def create_optimizer(self): if ( self.args.loraplus_lr_ratio is None and self.args.alternate_optimizer - not in ["optimi_adamw", "ao_adamw_8bit", "ao_adamw_4bit", "ao_adamw_fp8"] + not in [ + "optimi_adamw", + "ao_adamw_8bit", + "ao_adamw_4bit", + "ao_adamw_fp8", + "adopt_adamw", + ] ): return super().create_optimizer() @@ -505,6 +511,14 @@ def create_optimizer(self): self.optimizer = ( # pylint: disable=attribute-defined-outside-init AdamWFp8(optimizer_grouped_parameters, **optimizer_kwargs) ) + elif self.args.alternate_optimizer == "adopt_adamw": + from axolotl.utils.optimizers.adopt import ADOPT + + self.optimizer = ( # pylint: disable=attribute-defined-outside-init + ADOPT( + optimizer_grouped_parameters, decoupled=True, **optimizer_kwargs + ) + ) if is_sagemaker_mp_enabled(): self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init @@ -1625,11 +1639,13 @@ def build(self, total_num_steps): if self.cfg.reward_model: trainer_kwargs["max_length"] = self.cfg.sequence_len + # pylint: disable=duplicate-code if self.cfg.optimizer in [ "optimi_adamw", "ao_adamw_4bit", "ao_adamw_8bit", "ao_adamw_fp8", + "adopt_adamw", ]: # Set default so transformers doesn't throw training_arguments_kwargs["optim"] = "adamw_hf" diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index dc7693f06c..5066231d99 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -428,6 +428,7 @@ class HyperparametersConfig(BaseModel): "ao_adamw_4bit", "ao_adamw_8bit", "ao_adamw_fp8", + "adopt_adamw", ], ] ] = OptimizerNames.ADAMW_HF.value diff --git a/src/axolotl/utils/optimizers/adopt.py b/src/axolotl/utils/optimizers/adopt.py new file mode 100644 index 0000000000..7e133285f7 --- /dev/null +++ b/src/axolotl/utils/optimizers/adopt.py @@ -0,0 +1,508 @@ +""" +Copied from https://github.com/iShohei220/adopt + +ADOPT: Modified Adam Can Converge with Any β2 with the Optimal Rate (2024) +Taniguchi, Shohei and Harada, Keno and Minegishi, Gouki and Oshima, Yuta and Jeong, Seong Cheol and Nagahara, Go and Iiyama, Tomoshi and Suzuki, Masahiro and Iwasawa, Yusuke and Matsuo, Yutaka +""" +# mypy: ignore-errors +# pylint: skip-file +# mypy: allow-untyped-decorators +# mypy: allow-untyped-defs +from typing import List, Optional, Tuple, Union, cast + +import torch +from torch import Tensor +from torch.optim.optimizer import ( + Optimizer, + ParamsT, + _default_to_fused_or_foreach, + _device_dtype_check_for_fused, + _disable_dynamo_if_unsupported, + _get_capturable_supported_devices, + _get_scalar_dtype, + _get_value, + _use_grad_for_differentiable, + _view_as_real, +) + +__all__ = ["ADOPT", "adopt"] + + +class ADOPT(Optimizer): + def __init__( + self, + params: ParamsT, + lr: Union[float, Tensor] = 1e-3, + betas: Tuple[float, float] = (0.9, 0.9999), + eps: float = 1e-6, + weight_decay: float = 0.0, + decoupled: bool = False, + *, + foreach: Optional[bool] = None, + maximize: bool = False, + capturable: bool = False, + differentiable: bool = False, + fused: Optional[bool] = None, + ): + if isinstance(lr, Tensor): + if foreach and not capturable: + raise ValueError( + "lr as a Tensor is not supported for capturable=False and foreach=True" + ) + if lr.numel() != 1: + raise ValueError("Tensor lr must be 1-element") + if not 0.0 <= lr: + raise ValueError(f"Invalid learning rate: {lr}") + if not 0.0 <= eps: + raise ValueError(f"Invalid epsilon value: {eps}") + if not 0.0 <= betas[0] < 1.0: + raise ValueError(f"Invalid beta parameter at index 0: {betas[0]}") + if not 0.0 <= betas[1] < 1.0: + raise ValueError(f"Invalid beta parameter at index 1: {betas[1]}") + if not 0.0 <= weight_decay: + raise ValueError(f"Invalid weight_decay value: {weight_decay}") + + defaults = dict( + lr=lr, + betas=betas, + eps=eps, + weight_decay=weight_decay, + decoupled=decoupled, + maximize=maximize, + foreach=foreach, + capturable=capturable, + differentiable=differentiable, + fused=fused, + ) + super().__init__(params, defaults) + + if fused: + # TODO: support fused + raise RuntimeError("`fused` is not currently supported") + + if differentiable: + raise RuntimeError("`fused` does not support `differentiable`") + self._step_supports_amp_scaling = True + # TODO(crcrpar): [low prec params & their higher prec copy] + # Support AMP with FP16/BF16 model params which would need + # higher prec copy of params to do update math in higher prec to + # alleviate the loss of information. + if foreach: + raise RuntimeError("`fused` and `foreach` cannot be `True` together.") + + def __setstate__(self, state): + super().__setstate__(state) + for group in self.param_groups: + group.setdefault("maximize", False) + group.setdefault("foreach", None) + group.setdefault("capturable", False) + group.setdefault("differentiable", False) + fused = group.setdefault("fused", None) + for p in group["params"]: + p_state = self.state.get(p, []) + if len(p_state) != 0 and not torch.is_tensor(p_state["step"]): + step_val = float(p_state["step"]) + p_state["step"] = ( + torch.tensor( + step_val, + dtype=_get_scalar_dtype(is_fused=fused), + device=p.device, + ) + if group["capturable"] or group["fused"] + else torch.tensor(step_val, dtype=_get_scalar_dtype()) + ) + + def _init_group( + self, + group, + params_with_grad, + grads, + exp_avgs, + exp_avg_sqs, + state_steps, + ): + has_complex = False + for p in group["params"]: + if p.grad is not None: + has_complex |= torch.is_complex(p) + params_with_grad.append(p) + if p.grad.is_sparse: + raise RuntimeError("ADOPT does not support sparse gradients") + grads.append(p.grad) + + state = self.state[p] + # Lazy state initialization + if len(state) == 0: + if group["fused"]: + _device_dtype_check_for_fused(p) + # note(crcrpar): [special device hosting for step] + # Deliberately host `step` on CPU if both capturable and fused are off. + # This is because kernel launches are costly on CUDA and XLA. + state["step"] = ( + torch.zeros( + (), + dtype=_get_scalar_dtype(is_fused=group["fused"]), + device=p.device, + ) + if group["capturable"] or group["fused"] + else torch.tensor(0.0, dtype=_get_scalar_dtype()) + ) + # Exponential moving average of gradient values + state["exp_avg"] = torch.zeros_like( + p, memory_format=torch.preserve_format + ) + # Exponential moving average of squared gradient values + state["exp_avg_sq"] = torch.zeros_like( + p, memory_format=torch.preserve_format + ) + + exp_avgs.append(state["exp_avg"]) + exp_avg_sqs.append(state["exp_avg_sq"]) + + if group["differentiable"] and state["step"].requires_grad: + raise RuntimeError( + "`requires_grad` is not supported for `step` in differentiable mode" + ) + + # Foreach without capturable does not support a tensor lr + if ( + group["foreach"] + and torch.is_tensor(group["lr"]) + and not group["capturable"] + ): + raise RuntimeError( + "lr as a Tensor is not supported for capturable=False and foreach=True" + ) + + state_steps.append(state["step"]) + return has_complex + + @_use_grad_for_differentiable + def step(self, closure=None): + """Perform a single optimization step. + + Args: + closure (Callable, optional): A closure that reevaluates the model + and returns the loss. + """ + self._cuda_graph_capture_health_check() + + loss = None + if closure is not None: + with torch.enable_grad(): + loss = closure() + + for group in self.param_groups: + params_with_grad: List[Tensor] = [] + grads: List[Tensor] = [] + exp_avgs: List[Tensor] = [] + exp_avg_sqs: List[Tensor] = [] + state_steps: List[Tensor] = [] + beta1, beta2 = group["betas"] + + has_complex = self._init_group( + group, + params_with_grad, + grads, + exp_avgs, + exp_avg_sqs, + state_steps, + ) + + adopt( + params_with_grad, + grads, + exp_avgs, + exp_avg_sqs, + state_steps, + has_complex=has_complex, + beta1=beta1, + beta2=beta2, + lr=group["lr"], + weight_decay=group["weight_decay"], + decoupled=group["decoupled"], + eps=group["eps"], + maximize=group["maximize"], + foreach=group["foreach"], + capturable=group["capturable"], + differentiable=group["differentiable"], + fused=group["fused"], + grad_scale=getattr(self, "grad_scale", None), + found_inf=getattr(self, "found_inf", None), + ) + + return loss + + +def _single_tensor_adopt( + params: List[Tensor], + grads: List[Tensor], + exp_avgs: List[Tensor], + exp_avg_sqs: List[Tensor], + state_steps: List[Tensor], + grad_scale: Optional[Tensor], + found_inf: Optional[Tensor], + *, + has_complex: bool, + beta1: float, + beta2: float, + lr: Union[float, Tensor], + weight_decay: float, + decoupled: bool, + eps: float, + maximize: bool, + capturable: bool, + differentiable: bool, +): + assert grad_scale is None and found_inf is None + + if torch.jit.is_scripting(): + # this assert is due to JIT being dumb and not realizing that the ops below + # have overloads to handle both float and Tensor lrs, so we just assert it's + # a float since most people using JIT are using floats + assert isinstance(lr, float) + + for i, param in enumerate(params): + grad = grads[i] if not maximize else -grads[i] + exp_avg = exp_avgs[i] + exp_avg_sq = exp_avg_sqs[i] + step_t = state_steps[i] + + # If compiling, the compiler will handle cudagraph checks, see note [torch.compile x capturable] + if not torch._utils.is_compiling() and capturable: + capturable_supported_devices = _get_capturable_supported_devices() + assert ( + param.device.type == step_t.device.type + and param.device.type in capturable_supported_devices + ), f"If capturable=True, params and state_steps must be on supported devices: {capturable_supported_devices}." + + # update step + step_t += 1 + + if weight_decay != 0: + if decoupled: + param.add_(param, alpha=-lr * weight_decay) + else: + grad = grad.add(param, alpha=weight_decay) + + if torch.is_complex(param): + grad = torch.view_as_real(grad) + if exp_avg is not None: + exp_avg = torch.view_as_real(exp_avg) + if exp_avg_sq is not None: + exp_avg_sq = torch.view_as_real(exp_avg_sq) + param = torch.view_as_real(param) + + step = step_t if capturable or differentiable else _get_value(step_t) + if step == 1: + exp_avg_sq.addcmul_(grad, grad.conj()) + continue + + denom = torch.clamp(exp_avg_sq.sqrt(), eps) + if step == 2: + exp_avg.addcdiv_(grad, denom) + else: + exp_avg.mul_(beta1).addcdiv_(grad, denom, value=1 - beta1) + + param.add_(exp_avg, alpha=-lr) + exp_avg_sq.mul_(beta2).addcmul_(grad, grad.conj(), value=1 - beta2) + + +def _multi_tensor_adopt( + params: List[Tensor], + grads: List[Tensor], + exp_avgs: List[Tensor], + exp_avg_sqs: List[Tensor], + state_steps: List[Tensor], + grad_scale: Optional[Tensor], + found_inf: Optional[Tensor], + *, + has_complex: bool, + beta1: float, + beta2: float, + lr: Union[float, Tensor], + weight_decay: float, + decoupled: bool, + eps: float, + maximize: bool, + capturable: bool, + differentiable: bool, +): + if len(params) == 0: + return + + if isinstance(lr, Tensor) and not capturable: + raise RuntimeError( + "lr as a Tensor is not supported for capturable=False and foreach=True" + ) + + # If compiling, the compiler will handle cudagraph checks, see note [torch.compile x capturable] + if not torch._utils.is_compiling() and capturable: + capturable_supported_devices = _get_capturable_supported_devices( + supports_xla=False + ) + assert all( + p.device.type == step.device.type + and p.device.type in capturable_supported_devices + for p, step in zip(params, state_steps) + ), f"If capturable=True, params and state_steps must be on supported devices: {capturable_supported_devices}." + + assert grad_scale is None and found_inf is None + + assert not differentiable, "_foreach ops don't support autograd" + + grouped_tensors = Optimizer._group_tensors_by_device_and_dtype( + [params, grads, exp_avgs, exp_avg_sqs, state_steps] # type: ignore[list-item] + ) + for ( + device_params_, + device_grads_, + device_exp_avgs_, + device_exp_avg_sqs_, + device_state_steps_, + ), _ in grouped_tensors.values(): + device_params = cast(List[Tensor], device_params_) + device_grads = cast(List[Tensor], device_grads_) + device_exp_avgs = cast(List[Tensor], device_exp_avgs_) + device_exp_avg_sqs = cast(List[Tensor], device_exp_avg_sqs_) + device_state_steps = cast(List[Tensor], device_state_steps_) + + # Handle complex parameters + if has_complex: + _view_as_real( + device_params, device_grads, device_exp_avgs, device_exp_avg_sqs + ) + + if maximize: + device_grads = torch._foreach_neg(device_grads) # type: ignore[assignment] + + # Update steps + # If steps are on CPU, foreach will fall back to the slow path, which is a for-loop calling t.add(1) over + # and over. 1 will then be wrapped into a Tensor over and over again, which is slower than if we just + # wrapped it once now. The alpha is required to assure we go to the right overload. + if not torch._utils.is_compiling() and device_state_steps[0].is_cpu: + torch._foreach_add_( + device_state_steps, torch.tensor(1.0, device="cpu"), alpha=1.0 + ) + else: + torch._foreach_add_(device_state_steps, 1) + + if weight_decay != 0: + if decoupled: + torch._foreach_add_( + device_params, device_params, alpha=-lr * weight_decay + ) + else: + # Re-use the intermediate memory (device_grads) already allocated for maximize + if maximize: + torch._foreach_add_(device_grads, device_params, alpha=weight_decay) + else: + device_grads = torch._foreach_add( # type: ignore[assignment] + device_grads, device_params, alpha=weight_decay + ) + + if device_state_steps[0] == 1: + torch._foreach_addcmul_(device_exp_avg_sqs, device_grads, device_grads) + continue + + exp_avg_sq_sqrt = torch._foreach_sqrt(device_exp_avg_sqs) + exp_avg_sq_sqrt = torch._foreach_maximum(exp_avg_sq_sqrt, eps) + + if device_state_steps[0] == 2: + torch._foreach_addcdiv_(device_exp_avgs, device_grads, exp_avg_sq_sqrt) + else: + torch._foreach_mul_(device_exp_avgs, beta1) + torch._foreach_addcdiv_( + device_exp_avgs, device_grads, exp_avg_sq_sqrt, value=1 - beta1 + ) + + torch._foreach_add_(device_params, device_exp_avgs, alpha=-lr) + torch._foreach_mul_(device_exp_avg_sqs, beta2) + torch._foreach_addcmul_( + device_exp_avg_sqs, device_grads, device_grads, value=1 - beta2 + ) + + +@_disable_dynamo_if_unsupported(single_tensor_fn=_single_tensor_adopt) +def adopt( + params: List[Tensor], + grads: List[Tensor], + exp_avgs: List[Tensor], + exp_avg_sqs: List[Tensor], + state_steps: List[Tensor], + # kwonly args with defaults are not supported by functions compiled with torchscript issue #70627 + # setting this as kwarg for now as functional API is compiled by torch/distributed/optim + foreach: Optional[bool] = None, + capturable: bool = False, + differentiable: bool = False, + fused: Optional[bool] = None, + grad_scale: Optional[Tensor] = None, + found_inf: Optional[Tensor] = None, + has_complex: bool = False, + *, + beta1: float, + beta2: float, + lr: Union[float, Tensor], + weight_decay: float, + decoupled: bool, + eps: float, + maximize: bool, +): + r"""Functional API that performs ADOPT algorithm computation.""" + # Respect when the user inputs False/True for foreach or fused. We only want to change + # the default when neither have been user-specified. Note that we default to foreach + # and pass False to use_fused. This is not a mistake--we want to give the fused impl + # bake-in time before making it the default, even if it is typically faster. + if fused is None and foreach is None: + _, foreach = _default_to_fused_or_foreach( + params, differentiable, use_fused=False + ) + # Do not flip on foreach for the unsupported case where lr is a Tensor and capturable=False. + if foreach and isinstance(lr, Tensor) and not capturable: + foreach = False + if fused is None: + fused = False + if foreach is None: + foreach = False + + # this check is slow during compilation, so we skip it + # if it's strictly needed we can add this check back in dynamo + if not torch._utils.is_compiling() and not all( + isinstance(t, torch.Tensor) for t in state_steps + ): + raise RuntimeError( + "API has changed, `state_steps` argument must contain a list of singleton tensors" + ) + + if foreach and torch.jit.is_scripting(): + raise RuntimeError("torch.jit.script not supported with foreach optimizers") + if fused and torch.jit.is_scripting(): + raise RuntimeError("torch.jit.script not supported with fused optimizers") + + # if fused and not torch.jit.is_scripting(): + # func = _fused_adopt + # elif foreach and not torch.jit.is_scripting(): + if foreach and not torch.jit.is_scripting(): + func = _multi_tensor_adopt + else: + func = _single_tensor_adopt + + func( + params, + grads, + exp_avgs, + exp_avg_sqs, + state_steps, + has_complex=has_complex, + beta1=beta1, + beta2=beta2, + lr=lr, + weight_decay=weight_decay, + decoupled=decoupled, + eps=eps, + maximize=maximize, + capturable=capturable, + differentiable=differentiable, + grad_scale=grad_scale, + found_inf=found_inf, + ) diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 119dd3d7cf..3b68ec5ad5 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -13,7 +13,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir +from .utils import require_torch_2_5_1, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -65,3 +65,46 @@ def test_optimi_adamw(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.bin").exists() + + @with_temp_dir + @require_torch_2_5_1 + def test_adopt_adamw(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 1024, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.1, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adopt_adamw", + "lr_scheduler": "cosine", + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.bin").exists() diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index c15ca3d79a..439f6ac6f8 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -6,11 +6,13 @@ import tempfile import unittest from functools import wraps -from importlib.metadata import version from pathlib import Path import torch +# from importlib.metadata import version +from packaging import version + def with_temp_dir(test_func): @wraps(test_func) @@ -43,12 +45,24 @@ def require_torch_2_3_1(test_case): """ def is_min_2_3_1(): - torch_version = version("torch") - return torch_version >= "2.3.1" + torch_version = version.parse(torch.__version__) + return torch_version >= version.parse("2.3.1") return unittest.skipUnless(is_min_2_3_1(), "test torch 2.3.1")(test_case) +def require_torch_2_5_1(test_case): + """ + Decorator marking a test that requires torch >= 2.3.1 + """ + + def is_min_2_5_1(): + torch_version = version.parse(torch.__version__) + return torch_version >= version.parse("2.5.1") + + return unittest.skipUnless(is_min_2_5_1(), "test torch 2.5.1")(test_case) + + def is_hopper(): compute_capability = torch.cuda.get_device_capability() return compute_capability == (9, 0) From 5e98cdddac6aa94fb4b18c512c7da8e90502bb8e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 13 Nov 2024 17:10:36 -0500 Subject: [PATCH 0245/1405] Grokfast support (#1917) --- src/axolotl/core/trainer_builder.py | 12 ++++ src/axolotl/integrations/base.py | 15 +++-- src/axolotl/integrations/grokfast/LICENSE | 21 +++++++ src/axolotl/integrations/grokfast/README.md | 13 ++++ src/axolotl/integrations/grokfast/__init__.py | 50 +++++++++++++++ src/axolotl/integrations/grokfast/args.py | 15 +++++ .../integrations/grokfast/optimizer.py | 63 +++++++++++++++++++ .../config/models/input/v0_4_1/__init__.py | 2 + 8 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 src/axolotl/integrations/grokfast/LICENSE create mode 100644 src/axolotl/integrations/grokfast/README.md create mode 100644 src/axolotl/integrations/grokfast/__init__.py create mode 100644 src/axolotl/integrations/grokfast/args.py create mode 100644 src/axolotl/integrations/grokfast/optimizer.py diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 972c291406..af6eaabf2f 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1287,6 +1287,18 @@ def get_post_trainer_create_callbacks(self, trainer): if self.cfg.lisa_step_interval and self.cfg.lisa_n_layers: callbacks.append(lisa_callback_factory(trainer)) + + if self.cfg.plugins: + plugin_manager = PluginManager.get_instance() + callbacks.extend( + [ + cb + for cb in plugin_manager.add_callbacks_post_trainer( + self.cfg, trainer + ) + if cb + ] + ) return callbacks def _get_trainer_cls(self): diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 43afa431a0..a271c59d10 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -140,7 +140,7 @@ def create_lr_scheduler( def add_callbacks_pre_trainer(self, cfg, model): # pylint: disable=unused-argument """ - Adds callbacks to the trainer before training. + setup callbacks before creating the trainer. Parameters: cfg (dict): The configuration for the plugin. @@ -155,14 +155,15 @@ def add_callbacks_post_trainer( self, cfg, trainer ): # pylint: disable=unused-argument """ - Adds callbacks to the trainer after training. + Adds callbacks to the trainer after creating the trainer. + This is useful for callbacks that require access to the model or trainer. Parameters: cfg (dict): The configuration for the plugin. trainer (object): The trainer object for training. Returns: - List[callable]: A list of callback functions to be added to the TrainingArgs + List[callable]: A list of callback functions to be added """ return [] @@ -393,7 +394,9 @@ def add_callbacks_pre_trainer(self, cfg, model): """ callbacks = [] for plugin in self.plugins.values(): - callbacks.extend(plugin.add_callbacks_pre_trainer(cfg, model)) + plugin_callbacks = plugin.add_callbacks_pre_trainer(cfg, model) + if plugin_callbacks: # if the plugin returned a list of callbacks + callbacks.extend(plugin_callbacks) return callbacks def add_callbacks_post_trainer(self, cfg, trainer): @@ -409,7 +412,9 @@ def add_callbacks_post_trainer(self, cfg, trainer): """ callbacks = [] for plugin in self.plugins.values(): - callbacks.extend(plugin.add_callbacks_post_trainer(cfg, trainer)) + plugin_callbacks = plugin.add_callbacks_post_trainer(cfg, trainer) + if plugin_callbacks: + callbacks.extend(plugin_callbacks) return callbacks def post_train_unload(self, cfg): diff --git a/src/axolotl/integrations/grokfast/LICENSE b/src/axolotl/integrations/grokfast/LICENSE new file mode 100644 index 0000000000..21e35ee968 --- /dev/null +++ b/src/axolotl/integrations/grokfast/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Jaerin Lee, Bong Gyun Kang, Kihoon Kim, Kyoung Mu Lee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/axolotl/integrations/grokfast/README.md b/src/axolotl/integrations/grokfast/README.md new file mode 100644 index 0000000000..4950dde87a --- /dev/null +++ b/src/axolotl/integrations/grokfast/README.md @@ -0,0 +1,13 @@ +# Grokfast Optimizer + +See https://github.com/ironjr/grokfast + +### Usage + +```yaml +plugins: + - axolotl.integrations.grokfast.GrokfastPlugin + +grokfast_alpha: 2.0 +grokfast_lamb: 0.98 +``` diff --git a/src/axolotl/integrations/grokfast/__init__.py b/src/axolotl/integrations/grokfast/__init__.py new file mode 100644 index 0000000000..3889e927c2 --- /dev/null +++ b/src/axolotl/integrations/grokfast/__init__.py @@ -0,0 +1,50 @@ +""" +Grokfast plugin for Axolotl +""" +import logging + +from transformers.trainer_callback import TrainerCallback + +from ..base import BasePlugin +from .args import GrokfastArgs # pylint: disable=unused-import. # noqa: F401 +from .optimizer import gradfilter_ema + +LOG = logging.getLogger("axolotl.integrations.grokfast") + + +class GrokfastCallbackHandler(TrainerCallback): + """ + Transformer trainer callbacks for Grokfast + """ + + def __init__(self, *args_, alpha=0.98, lamb=2.0, **kwargs): + super().__init__(*args_, **kwargs) + self.grads = None + self.alpha = alpha + self.lamb = lamb + + def on_train_begin(self, *args_, **kwargs): # pylint: disable=unused-argument + self.grads = None + + def on_pre_optimizer_step( + self, args_, state, control, **kwargs + ): # pylint: disable=unused-argument + model = kwargs.pop("model") + self.grads = gradfilter_ema(model, self.grads, alpha=self.alpha, lamb=self.lamb) + return control + + +class GrokfastPlugin(BasePlugin): + """ + Plugin for Grokfast optimizer integraton with Axolotl. + """ + + def get_input_args(self): + return "axolotl.integrations.grokfast.GrokfastArgs" + + def add_callbacks_post_trainer(self, cfg, trainer): + LOG.info("Adding Grokfast callback to the trainer") + callback = GrokfastCallbackHandler( + alpha=cfg.grokfast_alpha, lamb=cfg.grokfast_lamb + ) + return [callback] diff --git a/src/axolotl/integrations/grokfast/args.py b/src/axolotl/integrations/grokfast/args.py new file mode 100644 index 0000000000..4776ae60ca --- /dev/null +++ b/src/axolotl/integrations/grokfast/args.py @@ -0,0 +1,15 @@ +""" +config args for grokfast plugin +""" +from typing import Optional + +from pydantic import BaseModel + + +class GrokfastArgs(BaseModel): + """ + Input args for Grokfast optimizer. + """ + + grokfast_alpha: Optional[float] = 0.98 + grokfast_lamb: Optional[float] = 2.0 diff --git a/src/axolotl/integrations/grokfast/optimizer.py b/src/axolotl/integrations/grokfast/optimizer.py new file mode 100644 index 0000000000..38cda2c934 --- /dev/null +++ b/src/axolotl/integrations/grokfast/optimizer.py @@ -0,0 +1,63 @@ +# Copyright: MIT License (c) 2024 Jaerin Lee, Bong Gyun Kang, Kihoon Kim, Kyoung Mu Lee +# Reference: https://github.com/ironjr/grokfast + +# pylint: skip-file +from collections import deque +from typing import Dict, Literal, Optional + +import torch +import torch.nn as nn + + +def gradfilter_ma( + m: nn.Module, + grads: Optional[Dict[str, deque]] = None, + window_size: int = 100, + lamb: float = 5.0, + filter_type: Literal["mean", "sum"] = "mean", + warmup: bool = True, + trigger: bool = False, # For ablation study. +) -> Dict[str, deque]: + if grads is None: + grads = { + n: deque(maxlen=window_size) + for n, p in m.named_parameters() + if p.requires_grad and p.grad is not None + } + + for n, p in m.named_parameters(): + if p.requires_grad and p.grad is not None: + grads[n].append(p.grad.data.detach()) # .cpu()) + + # Modify the gradients. + if not warmup or len(grads[n]) == window_size and not trigger: + if filter_type == "mean": + avg = sum(grads[n]) / len(grads[n]) + elif filter_type == "sum": + avg = sum(grads[n]) + else: + raise ValueError(f"Unrecognized filter_type {filter_type}") + p.grad.data = p.grad.data + avg * lamb + + return grads + + +def gradfilter_ema( + m: nn.Module, + grads: Optional[Dict[str, torch.Tensor]] = None, + alpha: float = 0.98, + lamb: float = 2.0, +) -> Dict[str, torch.Tensor]: + if grads is None: + grads = { + n: p.grad.data.detach() + for n, p in m.named_parameters() + if p.requires_grad and p.grad is not None + } + + for n, p in m.named_parameters(): + if p.requires_grad and p.grad is not None: + grads[n] = grads[n] * alpha + p.grad.data.detach() * (1 - alpha) + p.grad.data = p.grad.data + grads[n] * lamb + + return grads diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 5066231d99..1feb8aae86 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -783,6 +783,8 @@ class Config: is_mistral_derived_model: Optional[bool] = Field(default=None) is_qwen_derived_model: Optional[bool] = Field(default=None) + plugins: Optional[List[str]] = Field(default=None) + @field_validator("datasets", mode="before") @classmethod def deprecate_sharegpt_datasets(cls, datasets): From 2d7830fda6b0892f6a111b52d7fa66f8e7eb3d31 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 14 Nov 2024 06:59:25 -0500 Subject: [PATCH 0246/1405] upgrade to flash-attn 2.7.0 (#2048) --- docker/Dockerfile-base | 4 ---- examples/colab-notebooks/colab-axolotl-example.ipynb | 2 +- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 405883eda6..3f13bba30a 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -35,7 +35,3 @@ RUN git lfs install --skip-repo && \ pip3 install awscli && \ # The base image ships with `pydantic==1.8.2` which is not working pip3 install -U --no-cache-dir pydantic==1.10.10 - -RUN if [ "$PYTHON_VERSION" != "2.5.1" ] ; then \ - pip3 install flash-attn==2.6.3; \ - fi diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 3a6981ee09..0acaf7961c 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -44,7 +44,7 @@ "outputs": [], "source": [ "!pip install -e git+https://github.com/axolotl-ai-cloud/axolotl#egg=axolotl\n", - "!pip install flash-attn==\"2.5.0\"\n", + "!pip install flash-attn==\"2.7.0.post2\"\n", "!pip install deepspeed==\"0.13.1\"!pip install mlflow==\"2.13.0\"" ] }, diff --git a/requirements.txt b/requirements.txt index c7f5c9433c..76c3273fcf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ addict fire PyYAML>=6.0 requests -flash-attn==2.6.3 +flash-attn==2.7.0.post2 sentencepiece wandb einops diff --git a/setup.py b/setup.py index cec5f41084..7920da1742 100644 --- a/setup.py +++ b/setup.py @@ -105,7 +105,7 @@ def parse_requirements(): dependency_links=dependency_links, extras_require={ "flash-attn": [ - "flash-attn==2.6.3", + "flash-attn==2.7.0.post2", ], "deepspeed": [ "deepspeed==0.14.4", From 5be8e13d35867bd85c62b3c7435df62d1045c50e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 14 Nov 2024 10:24:49 -0500 Subject: [PATCH 0247/1405] make sure to add tags for versioned tag on cloud docker images (#2060) --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 28dad00465..0a54bd8062 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -114,6 +114,9 @@ jobs: images: | winglian/axolotl-cloud axolotlai/axolotl-cloud + tags: | + type=ref,event=branch + type=semver,pattern={{version}} - name: Login to Docker Hub uses: docker/login-action@v3 with: From ba219b51a5762d02a723fe65cbfb0afe5d605bb1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 14 Nov 2024 10:31:19 -0500 Subject: [PATCH 0248/1405] fix duplicate base build (#2061) [skip ci] --- .github/workflows/base.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index f46e1769f7..e086a5f961 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -27,7 +27,7 @@ jobs: - cuda: "124" cuda_version: 12.4.1 cudnn_version: "" - python_version: "3.11" + python_version: "3.10" pytorch: 2.4.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "124" From f3a5d119af2c1a2c88d23ef50d67a1ad907ebbf9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 14 Nov 2024 12:58:06 -0500 Subject: [PATCH 0249/1405] fix env var extraction (#2043) [skip ci] --- scripts/cloud-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cloud-entrypoint.sh b/scripts/cloud-entrypoint.sh index 5b0337f2b2..2d3e29181a 100755 --- a/scripts/cloud-entrypoint.sh +++ b/scripts/cloud-entrypoint.sh @@ -2,7 +2,7 @@ # Export specific ENV variables to /etc/rp_environment echo "Exporting environment variables..." -printenv | grep -E '^RUNPOD_|^PATH=|^_=' | sed 's/^\(.*\)=\(.*\)$/export \1="\2"/' >> /etc/rp_environment +printenv | grep -E '^HF_|^BNB_|^CUDA_|^NCCL_|^NV|^RUNPOD_|^PATH=|^_=' | sed 's/^\([^=]*\)=\(.*\)$/export \1="\2"/' | grep -v 'printenv' >> /etc/rp_environment echo 'source /etc/rp_environment' >> ~/.bashrc add_keys_to_authorized() { From 71d4030b790d3d4ff9bae6bf3696cdc8e72bb5f7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 14 Nov 2024 12:59:00 -0500 Subject: [PATCH 0250/1405] gradient accumulation tests, embeddings w pad_token fix, smaller models (#2059) * add more test cases for gradient accumulation and fix zero3 * swap out for smaller model * fix missing return * fix missing pad_token in config * support concurrency for multigpu testing * cast empty deepspeed to empty string for zero3 check * fix temp_dir as fixture so parametrize works properly * fix test file for multigpu evals * don't use default * don't use default for fsdp_state_dict_type * don't use llama tokenizer w smollm * also automatically cancel multigpu for concurrency --- .github/workflows/multi-gpu-e2e.yml | 5 + cicd/multigpu.sh | 2 +- .../config/models/input/v0_4_1/__init__.py | 19 +++ src/axolotl/utils/models.py | 1 + tests/e2e/conftest.py | 16 +++ tests/e2e/multigpu/test_eval.py | 12 +- tests/e2e/multigpu/test_llama.py | 115 +++++++++--------- tests/e2e/multigpu/test_qwen2.py | 19 +-- 8 files changed, 118 insertions(+), 71 deletions(-) create mode 100644 tests/e2e/conftest.py diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index c2fb5dfb5d..2fecde5a97 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -8,6 +8,11 @@ on: schedule: - cron: '0 0 * * 1,4' # Runs at 00:00 UTC every monday & thursday +# Cancel jobs on the same ref if a new one is triggered +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + jobs: test-axolotl-multigpu: if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} diff --git a/cicd/multigpu.sh b/cicd/multigpu.sh index ff7f9f19a5..05d1bbbf2a 100755 --- a/cicd/multigpu.sh +++ b/cicd/multigpu.sh @@ -2,4 +2,4 @@ set -e # only run one test at a time so as not to OOM the GPU -pytest -n1 /workspace/axolotl/tests/e2e/multigpu/ +pytest -v -n2 /workspace/axolotl/tests/e2e/multigpu/ diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 1feb8aae86..10e80d9f33 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -1291,6 +1291,25 @@ def check_use_reentrant_mismatch(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def warn_qlora_zero3_w_use_reentrant(cls, data): + if ( + data.get("adapter") == "qlora" + and data.get("gradient_checkpointing_kwargs", {}) + and data.get("gradient_checkpointing_kwargs", {}).get("use_reentrant") + is False + and "zero3" in data.get("deepspeed", "") + ): + # may result in: + # torch.utils.checkpoint.CheckpointError: torch.utils.checkpoint: + # Recomputed values for the following tensors have different metadata + # than during the forward pass. + LOG.warning( + "qlora + zero3 with use_reentrant: false may result in a CheckpointError about recomputed values" + ) + return data + @model_validator(mode="before") @classmethod def check_val_w_test_datasets(cls, data): diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 41e89dbfb2..db66c65f25 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -238,6 +238,7 @@ def load_tokenizer(cfg): x in cfg.lora_modules_to_save for x in lora_modules_to_save ) ) + and k != "pad_token" ): lora_modules_to_save = ", ".join( [f"`{x}`" for x in lora_modules_to_save] diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py new file mode 100644 index 0000000000..723a44f03a --- /dev/null +++ b/tests/e2e/conftest.py @@ -0,0 +1,16 @@ +""" +shared pytest fixtures +""" +import shutil +import tempfile + +import pytest + + +@pytest.fixture +def temp_dir(): + # Create a temporary directory + _temp_dir = tempfile.mkdtemp() + yield _temp_dir + # Clean up the directory after the test + shutil.rmtree(_temp_dir) diff --git a/tests/e2e/multigpu/test_eval.py b/tests/e2e/multigpu/test_eval.py index 65d26bb824..068a9220ca 100644 --- a/tests/e2e/multigpu/test_eval.py +++ b/tests/e2e/multigpu/test_eval.py @@ -3,28 +3,25 @@ """ import logging import os -import unittest from pathlib import Path import yaml from accelerate.test_utils import execute_subprocess_async +from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault -from ..utils import with_temp_dir - LOG = logging.getLogger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent -class TestMultiGPUEval(unittest.TestCase): +class TestMultiGPUEval: """ Test case for MultiGPU Eval Sample Packing """ - @with_temp_dir def test_eval_sample_packing(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -83,13 +80,14 @@ def test_eval_sample_packing(self, temp_dir): "launch", "--num-processes", "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", "-m", "axolotl.cli.train", str(Path(temp_dir) / "config.yaml"), ] ) - @with_temp_dir def test_eval(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -148,6 +146,8 @@ def test_eval(self, temp_dir): "launch", "--num-processes", "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", "-m", "axolotl.cli.train", str(Path(temp_dir) / "config.yaml"), diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 8087e08e3e..b2c8abc604 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -4,17 +4,17 @@ import logging import os -import unittest from pathlib import Path import pytest import yaml from accelerate.test_utils import execute_subprocess_async from huggingface_hub import snapshot_download +from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault -from ..utils import is_hopper, with_temp_dir +from ..utils import is_hopper LOG = logging.getLogger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" @@ -28,18 +28,16 @@ def download_model(): snapshot_download("TinyLlama/TinyLlama_v1.1") -class TestMultiGPULlama(unittest.TestCase): +class TestMultiGPULlama: """ Test case for Llama models using LoRA """ - @with_temp_dir def test_lora_ddp(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "TinyLlama/TinyLlama_v1.1", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM-135M", "sequence_len": 2048, "adapter": "lora", "lora_r": 8, @@ -48,9 +46,7 @@ def test_lora_ddp(self, temp_dir): "lora_target_linear": True, "val_set_size": 0.05, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -81,19 +77,23 @@ def test_lora_ddp(self, temp_dir): "launch", "--num-processes", "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", "-m", "axolotl.cli.train", str(Path(temp_dir) / "config.yaml"), ] ) - @with_temp_dir - def test_lora_ddp_packed(self, temp_dir): + @pytest.mark.parametrize( + "gradient_accumulation_steps", + [1, 4], + ) + def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "TinyLlama/TinyLlama_v1.1", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM-135M", "sequence_len": 2048, "sample_packing": True, "eval_sample_packing": False, @@ -105,9 +105,7 @@ def test_lora_ddp_packed(self, temp_dir): "lora_target_linear": True, "val_set_size": 0.05, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -118,7 +116,7 @@ def test_lora_ddp_packed(self, temp_dir): "num_epochs": 1, "max_steps": 15, "micro_batch_size": 4, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_8bit", @@ -138,6 +136,8 @@ def test_lora_ddp_packed(self, temp_dir): "launch", "--num-processes", "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", "-m", "axolotl.cli.train", str(Path(temp_dir) / "config.yaml"), @@ -145,7 +145,6 @@ def test_lora_ddp_packed(self, temp_dir): ) @pytest.mark.skipif(is_hopper(), reason="h100 doesn't support 8-bit lora") - @with_temp_dir def test_dpo_lora_ddp(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -210,13 +209,14 @@ def test_dpo_lora_ddp(self, temp_dir): "launch", "--num-processes", "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", "-m", "axolotl.cli.train", str(Path(temp_dir) / "config.yaml"), ] ) - @with_temp_dir def test_dpo_qlora_ddp(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -278,25 +278,27 @@ def test_dpo_qlora_ddp(self, temp_dir): "launch", "--num-processes", "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", "-m", "axolotl.cli.train", str(Path(temp_dir) / "config.yaml"), ] ) - @with_temp_dir - def test_fsdp(self, temp_dir): + @pytest.mark.parametrize( + "gradient_accumulation_steps", + [1, 4], + ) + def test_fsdp(self, temp_dir, gradient_accumulation_steps): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "TinyLlama/TinyLlama_v1.1", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM-135M", "sequence_len": 2048, - "val_set_size": 0.05, + "val_set_size": 0.01, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -305,9 +307,9 @@ def test_fsdp(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 15, + "max_steps": 10, "micro_batch_size": 4, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch", @@ -324,7 +326,7 @@ def test_fsdp(self, temp_dir): "fsdp_use_orig_params": False, "fsdp_cpu_ram_efficient_loading": False, "fsdp_transformer_layer_cls_to_wrap": "LlamaDecoderLayer", - "fsdp_state_dict_type": "SHARDED_STATE_DICT", + "fsdp_state_dict_type": "FULL_STATE_DICT", "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", }, } @@ -341,28 +343,29 @@ def test_fsdp(self, temp_dir): "launch", "--num-processes", "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", "-m", "axolotl.cli.train", str(Path(temp_dir) / "config.yaml"), ] ) - @with_temp_dir - def test_fsdp_packed(self, temp_dir): + @pytest.mark.parametrize( + "fsdp_state_dict_type", + ["FULL_STATE_DICT", "SHARDED_STATE_DICT"], + ) + def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "TinyLlama/TinyLlama_v1.1", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM-135M", "sample_packing": True, - "eval_sample_packing": False, "pad_to_sequence_len": True, "sequence_len": 2048, "val_set_size": 0.05, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -390,7 +393,7 @@ def test_fsdp_packed(self, temp_dir): "fsdp_use_orig_params": False, "fsdp_cpu_ram_efficient_loading": False, "fsdp_transformer_layer_cls_to_wrap": "LlamaDecoderLayer", - "fsdp_state_dict_type": "SHARDED_STATE_DICT", + "fsdp_state_dict_type": fsdp_state_dict_type, "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", }, } @@ -407,13 +410,14 @@ def test_fsdp_packed(self, temp_dir): "launch", "--num-processes", "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", "-m", "axolotl.cli.train", str(Path(temp_dir) / "config.yaml"), ] ) - @with_temp_dir def test_fsdp_qlora_prequant_packed(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -483,28 +487,29 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "launch", "--num-processes", "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", "-m", "axolotl.cli.train", str(Path(temp_dir) / "config.yaml"), ] ) - @with_temp_dir - def test_ds_zero3_packed(self, temp_dir): + @pytest.mark.parametrize( + "gradient_accumulation_steps", + [1, 4], + ) + def test_ds_zero3_packed(self, temp_dir, gradient_accumulation_steps): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "TinyLlama/TinyLlama_v1.1", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM-135M", "sample_packing": True, - "eval_sample_packing": False, "pad_to_sequence_len": True, "sequence_len": 2048, "val_set_size": 0.05, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -515,7 +520,7 @@ def test_ds_zero3_packed(self, temp_dir): "num_epochs": 1, "max_steps": 15, "micro_batch_size": 4, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch", @@ -536,19 +541,19 @@ def test_ds_zero3_packed(self, temp_dir): "launch", "--num-processes", "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", "-m", "axolotl.cli.train", str(Path(temp_dir) / "config.yaml"), ] ) - @with_temp_dir def test_ds_zero3_qlora_packed(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "TinyLlama/TinyLlama_v1.1", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM-135M", "load_in_4bit": True, "adapter": "qlora", "lora_r": 8, @@ -561,9 +566,7 @@ def test_ds_zero3_qlora_packed(self, temp_dir): "sequence_len": 2048, "val_set_size": 0.05, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -595,6 +598,8 @@ def test_ds_zero3_qlora_packed(self, temp_dir): "launch", "--num-processes", "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", "-m", "axolotl.cli.train", str(Path(temp_dir) / "config.yaml"), diff --git a/tests/e2e/multigpu/test_qwen2.py b/tests/e2e/multigpu/test_qwen2.py index 393ab7d707..32bb6a3e13 100644 --- a/tests/e2e/multigpu/test_qwen2.py +++ b/tests/e2e/multigpu/test_qwen2.py @@ -4,31 +4,30 @@ import logging import os -import unittest from pathlib import Path +import pytest import yaml from accelerate.test_utils import execute_subprocess_async +from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault -from ..utils import with_temp_dir - LOG = logging.getLogger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" -class TestMultiGPUQwen2(unittest.TestCase): +class TestMultiGPUQwen2: """ Test case for Llama models using LoRA """ - @with_temp_dir - def test_qlora_fsdp_dpo(self, temp_dir): + @pytest.mark.parametrize("base_model", ["Qwen/Qwen2-0.5B", "Qwen/Qwen2.5-0.5B"]) + def test_qlora_fsdp_dpo(self, base_model, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "Qwen/Qwen2-1.5B", + "base_model": base_model, "load_in_4bit": True, "rl": "dpo", "chat_template": "chatml", @@ -47,9 +46,9 @@ def test_qlora_fsdp_dpo(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 15, + "max_steps": 5, "warmup_steps": 20, - "micro_batch_size": 4, + "micro_batch_size": 2, "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, @@ -91,6 +90,8 @@ def test_qlora_fsdp_dpo(self, temp_dir): "launch", "--num-processes", "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", "-m", "axolotl.cli.train", str(Path(temp_dir) / "config.yaml"), From 2f20cb7ebfc6cdc100196c7a72288470320df009 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 15 Nov 2024 19:08:38 -0500 Subject: [PATCH 0251/1405] upgrade datasets==3.1.0 and add upstream check (#2067) [skip ci] --- .github/workflows/tests-nightly.yml | 1 + cicd/Dockerfile.jinja | 1 + requirements.txt | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 52c28e9198..bb99278880 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -48,6 +48,7 @@ jobs: sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt sed -i 's#^trl.*#trl @ git+https://github.com/huggingface/trl.git@main#' requirements.txt + sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt - name: Install dependencies run: | diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 8fe84ac646..8fd040d77b 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -28,6 +28,7 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt; \ sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt; \ sed -i 's#^trl.*#trl @ git+https://github.com/huggingface/trl.git@main#' requirements.txt; \ + sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt; \ fi RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ diff --git a/requirements.txt b/requirements.txt index 76c3273fcf..aab4d0eade 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ transformers==4.46.1 tokenizers>=0.20.1 bitsandbytes==0.44.1 accelerate==1.1.0 -datasets==3.0.1 +datasets==3.1.0 deepspeed==0.15.3 pydantic==2.6.3 addict From c16ec398d79bb39dd5ee9af59ebede4f5c8c4bb8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 15 Nov 2024 19:09:00 -0500 Subject: [PATCH 0252/1405] update to be deprecated evaluation_strategy (#1682) [skip ci] * update to be deprecated evaluation_strategy and c4 dataset * chore: lint * remap eval strategy to new config and add tests --- src/axolotl/core/trainer_builder.py | 16 +- src/axolotl/utils/callbacks/__init__.py | 5 +- src/axolotl/utils/config/__init__.py | 370 ------------------ .../config/models/input/v0_4_1/__init__.py | 39 +- tests/test_validation.py | 40 +- 5 files changed, 67 insertions(+), 403 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index af6eaabf2f..1cad1a8c3f 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1416,17 +1416,15 @@ def build(self, total_num_steps): if not self.cfg.test_datasets and self.cfg.val_set_size == 0: # no eval set, so don't eval - training_arguments_kwargs["evaluation_strategy"] = "no" + training_arguments_kwargs["eval_strategy"] = "no" elif self.cfg.eval_steps: - training_arguments_kwargs["evaluation_strategy"] = "steps" + training_arguments_kwargs["eval_strategy"] = "steps" training_arguments_kwargs["eval_steps"] = self.cfg.eval_steps - elif self.cfg.evaluation_strategy: - training_arguments_kwargs[ - "evaluation_strategy" - ] = self.cfg.evaluation_strategy + elif self.cfg.eval_strategy: + training_arguments_kwargs["eval_strategy"] = self.cfg.eval_strategy else: # we have an eval set, but no steps defined, default to use epoch - training_arguments_kwargs["evaluation_strategy"] = "epoch" + training_arguments_kwargs["eval_strategy"] = "epoch" if self.cfg.save_steps: training_arguments_kwargs["save_strategy"] = "steps" @@ -1860,10 +1858,10 @@ def build_training_arguments(self, total_num_steps): training_args_kwargs["save_safetensors"] = self.cfg.save_safetensors if self.eval_dataset: - training_args_kwargs["evaluation_strategy"] = "steps" + training_args_kwargs["eval_strategy"] = "steps" training_args_kwargs["eval_steps"] = self.cfg.eval_steps else: - training_args_kwargs["evaluation_strategy"] = "no" + training_args_kwargs["eval_strategy"] = "no" if self.cfg.bf16 or self.cfg.bfloat16: training_args_kwargs["bf16"] = True diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 0bc781fcb4..8768bc2bf7 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -64,10 +64,7 @@ def on_step_end( control: TrainerControl, **kwargs, ): - if ( - args.evaluation_strategy == IntervalStrategy.STEPS - and state.global_step == 1 - ): + if args.eval_strategy == IntervalStrategy.STEPS and state.global_step == 1: control.should_evaluate = True return control diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 6e5ecda03a..b12ad81136 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -1,8 +1,6 @@ """Module for working with config dicts""" -import json import logging import os -from pathlib import Path from typing import Optional import torch @@ -10,7 +8,6 @@ from axolotl.integrations.config import merge_input_args from axolotl.utils.bench import log_gpu_memory_usage -from axolotl.utils.config.models.input.v0_4_1 import SUPPORTED_METRICS from axolotl.utils.config.models.input.v0_4_1 import ( AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, ) @@ -247,370 +244,3 @@ def validate_config(cfg: DictDefault, capabilities: Optional[dict] = None): return DictDefault( dict(AxolotlInputConfig(**cfg.to_dict()).model_dump(exclude_none=True)) ) - - -def legacy_validate_config(cfg): - """ - This is a "pre-validation" step that handles the yaml configuration before we have any - information about the model architecture - """ - if is_torch_bf16_gpu_available(): - if not cfg.bf16 and not cfg.bfloat16: - LOG.info("bf16 support detected, but not enabled for this configuration.") - else: - if ( - not cfg.merge_lora - and not cfg.is_preprocess - and (cfg.bf16 is True or cfg.bfloat16 is True) - ): - raise ValueError( - "bf16 requested, but AMP is not supported on this GPU. Requires Ampere series or above." - ) - if ( - # pylint: disable=too-many-boolean-expressions - not (cfg.bf16 or cfg.bfloat16) - and (cfg.fp16 or cfg.float16) - and not cfg.adapter - and not cfg.flash_attention - and cfg.sample_packing - ): - LOG.warning( - "Full fine tune w/o FA2 w/ sample packing and fp16/float16 is likely to raise errors. Try LoRA." - ) - # ValueError: Attempting to unscale FP16 gradients. - # OR - # RuntimeError: expected mat1 and mat2 to have the same dtype, but got: float != c10::Half - if cfg.max_packed_sequence_len: - raise DeprecationWarning("`max_packed_sequence_len` is no longer supported") - - if cfg.sample_packing and cfg.rl: - raise ValueError("`sample_packing: true` does not work with RLHF training") - - if cfg.sample_packing and not cfg.pad_to_sequence_len: - LOG.warning( - "`pad_to_sequence_len: true` is recommended when using sample_packing" - ) - - if cfg.gradient_accumulation_steps and cfg.batch_size: - raise ValueError( - "please set only one of gradient_accumulation_steps or batch_size" - ) - if cfg.batch_size: - LOG.warning( - "%s\n%s", - "batch_size is not recommended. Please use gradient_accumulation_steps instead.", - "To calculate the equivalent gradient_accumulation_steps, divide batch_size / micro_batch_size / number of gpus.", - ) - if ( - cfg.eval_batch_size - and cfg.micro_batch_size - and cfg.eval_batch_size != cfg.micro_batch_size - ): - LOG.warning( - "eval_batch_size != micro_batch_size. This can lead to VRAM instability." - ) - - if cfg.adapter == "qlora": - if cfg.merge_lora: - # can't merge qlora if loaded in 8bit or 4bit - if cfg.load_in_8bit: - raise ValueError("Can't merge qlora if loaded in 8bit") - - if cfg.gptq: - raise ValueError("Can't merge qlora if gptq") - - if cfg.load_in_4bit: - raise ValueError("Can't merge qlora if loaded in 4bit") - - else: - if cfg.load_in_8bit: - raise ValueError("Can't load qlora in 8bit") - - if cfg.gptq: - raise ValueError("Can't load qlora if gptq") - - if not cfg.load_in_4bit: - raise ValueError("Require cfg.load_in_4bit to be True for qlora") - - if cfg.flash_attn_fuse_qkv or cfg.flash_attn_fuse_mlp: - raise ValueError("Fused modules are not supported with QLoRA") - - loftq = cfg.peft and cfg.peft.loftq_config and cfg.peft.loftq_config.loftq_bits - if not cfg.load_in_8bit and cfg.adapter == "lora" and not loftq: - LOG.warning("We recommend setting `load_in_8bit: true` for LORA finetuning") - - if cfg.adapter == "lora" and (cfg.flash_attn_fuse_qkv or cfg.flash_attn_fuse_mlp): - raise ValueError("Fused modules are not supported with LoRA") - - if cfg.adapter and cfg.peft_layers_to_transform and cfg.unfrozen_parameters: - raise ValueError( - "`unfrozen_parameters` used with `peft_layers_to_transform` can have unexpected behavior." - ) - - if cfg.relora_steps: - if cfg.adapter not in ("lora", "qlora"): - raise ValueError("cfg.adapter must be lora or qlora to use ReLoRA") - - if cfg.fsdp: - raise ValueError("fsdp not supported with ReLoRA") - - if cfg.deepspeed: - raise ValueError("deepspeed not supported with ReLoRA") - - if cfg.lr_scheduler == "one_cycle": - raise ValueError("ReLoRA is not compatible with the one_cycle scheduler") - - if cfg.flash_attn_fuse_qkv or cfg.flash_attn_fuse_mlp: - raise ValueError("Fused modules are not supported with ReLoRA") - - if cfg.trust_remote_code: - LOG.warning( - "`trust_remote_code` is set to true. Please make sure that you reviewed the remote code/model." - ) - - if cfg.push_dataset_to_hub and cfg.hf_use_auth_token is not True: - raise ValueError( - "Require cfg.hf_use_auth_token to be True for push_dataset_to_hub" - ) - - if (cfg.base_model and "falcon" in cfg.base_model.lower()) and cfg.fsdp: - raise ValueError("FSDP is not supported for falcon models") - - if ( - cfg.base_model and "mpt" in cfg.base_model.lower() - ) and cfg.gradient_checkpointing: - raise ValueError("gradient_checkpointing is not supported for MPT models") - - if cfg.flash_optimum is True: - if cfg.adapter: - LOG.warning("BetterTransformers probably doesn't work with PEFT adapters") - if cfg.fp16 or cfg.bf16: - raise ValueError("AMP is not supported with BetterTransformer") - if cfg.float16 is not True and cfg.bfloat16 is not True: - LOG.warning( - "You should probably set bfloat16 or float16 to true to " - "load the model in float16 for BetterTransformers" - ) - if int(torch.__version__.split(".", maxsplit=1)[0]) < 2: - LOG.warning("torch>=2.0.0 required") - raise ValueError( - f"flash_optimum for BetterTransformers may not be used with {torch.__version__}" - ) - - if cfg.pretraining_dataset and cfg.group_by_length: - LOG.warning( - "You probably want to disable group_by_length as it will force a streamed dataset to download completely." - ) - if cfg.pretraining_dataset and not cfg.max_steps: - raise ValueError( - "max_steps must be set when using iterable pretraining_dataset, Trainer can't infer length and schedule optimizer/learning rate without it!" - ) - - if any([cfg.adam_beta1, cfg.adam_beta2, cfg.adam_epsilon]) and ( - not cfg.optimizer or "adamw" not in cfg.optimizer - ): - LOG.warning("adamw hyperparameters found, but no adamw optimizer set") - - if cfg.push_to_hub_model_id: - raise ValueError( - "push_to_hub_model_id is deprecated. Please use hub_model_id instead." - ) - - if cfg.hub_model_id and cfg.save_strategy not in ["steps", "epoch", None]: - LOG.warning( - "hub_model_id is set without any models being saved. To save a model, set save_strategy to steps, epochs or leave empty." - ) - - if cfg.gptq and cfg.revision_of_model: - raise ValueError( - "revision_of_model is not supported for GPTQ models. " - + "Please download the model from HuggingFace Hub manually for correct branch, " - + "point to its path, and remove revision_of_model from the config." - ) - - # if cfg.sample_packing and cfg.sdp_attention: - # # incompatible due to bug w/ accelerate causing 0.0 loss when using llama2 - # raise ValueError( - # "sample_packing not compatible with sdp_attention. Use flash_attention" - # ) - - if cfg.sample_packing and cfg.xformers_attention: - raise ValueError( - "sample_packing not compatible with xformers_attention. Use flash_attention" - ) - - if cfg.sample_packing and cfg.sdp_attention and (cfg.bfloat16 or cfg.bf16): - # https://github.com/pytorch/pytorch/blob/1b03423526536b5f3d35bdfa95ccc6197556cf9b/test/test_transformers.py#L2440-L2450 - LOG.warning( - "sample_packing & torch sdpa with bf16 is unsupported may results in 0.0 loss. " - "This may work on H100s." - ) - - if cfg.early_stopping_patience: - if not cfg.save_steps or not cfg.eval_steps: - raise ValueError( - "`early_stopping_patience` requires save_steps and eval_steps to be set. eval_steps should evenly divide save_steps." - ) - if cfg.save_steps % cfg.eval_steps != 0: - raise ValueError( - "`early_stopping_patience` requires that eval_steps should evenly divide save_steps." - ) - - if cfg.saves_per_epoch and cfg.save_steps: - raise ValueError( - "save_steps and saves_per_epoch are mutually exclusive and cannot be used together." - ) - if cfg.save_strategy and cfg.saves_per_epoch and cfg.save_strategy != "steps": - raise ValueError( - "save_strategy must be empty or set to `steps` when used with saves_per_epoch." - ) - if cfg.save_strategy and cfg.save_steps and cfg.save_strategy != "steps": - raise ValueError( - "save_strategy and save_steps mismatch. Please set save_strategy to 'steps' or remove save_steps." - ) - if cfg.evals_per_epoch and cfg.eval_steps: - raise ValueError( - "eval_steps and evals_per_epoch are mutually exclusive and cannot be used together." - ) - if ( - cfg.evals_per_epoch - and cfg.evaluation_strategy - and cfg.evaluation_strategy != "steps" - ): - raise ValueError( - "evaluation_strategy must be empty or set to `steps` when used with evals_per_epoch." - ) - if ( - cfg.evaluation_strategy - and cfg.eval_steps - and cfg.evaluation_strategy != "steps" - ): - raise ValueError( - "evaluation_strategy and eval_steps mismatch. Please set evaluation_strategy to 'steps' or remove eval_steps." - ) - - if ( - cfg.val_set_size == 0 - and (cfg.eval_steps or cfg.evaluation_strategy) - and not cfg.test_datasets - ): - raise ValueError( - "eval_steps and evaluation_strategy are not supported with val_set_size == 0" - ) - - if ( - cfg.sample_packing - and cfg.eval_table_size - and cfg.eval_sample_packing is not False - ): - raise ValueError( - "eval_table_size and eval_sample_packing are not supported together with sample_packing. Please set 'eval_sample_packing' to false." - ) - - if not cfg.adapter and (cfg.load_in_8bit or cfg.load_in_4bit): - raise ValueError( - "load_in_8bit and load_in_4bit are not supported without setting an adapter." - "If you want to full finetune, please turn off load_in_8bit and load_in_4bit." - ) - - if cfg.rope_scaling: - LOG.warning("`rope_scaling` should now be be a key under `model_config`") - - if cfg.wandb_run_id and not cfg.wandb_name: - cfg.wandb_name = cfg.wandb_run_id - - LOG.warning( - "wandb_run_id sets the ID of the run. If you would like to set the name, please use wandb_name instead." - ) - - if cfg.noisy_embedding_alpha is not None: - # Deprecated, use neftune_noise_alpha - LOG.warning("noisy_embedding_alpha is deprecated, use neftune_noise_alpha") - if cfg.neftune_noise_alpha is None: - cfg.neftune_noise_alpha = cfg.noisy_embedding_alpha - else: - # User is providing both; bail and have them sort out their settings - raise ValueError( - "noisy_embedding_alpha is deprecated, use neftune_noise_alpha; both are set, please remove the deprecated noisy_embedding_alpha setting" - ) - - if cfg.neftune_noise_alpha is not None and cfg.neftune_noise_alpha <= 0.0: - raise ValueError("neftune_noise_alpha must be > 0.0") - - if cfg.max_memory is not None and cfg.gpu_memory_limit is not None: - raise ValueError( - "max_memory and gpu_memory_limit are mutually exclusive and cannot be used together." - ) - - if ( - cfg.unfrozen_parameters - and cfg.gradient_checkpointing_kwargs - and cfg.gradient_checkpointing_kwargs.use_reentrant is True - ): - # https://github.com/huggingface/transformers/issues/21381 - raise ValueError( - "`use_reentrant` must be false when used with partially frozen model." - ) - - if cfg.deepspeed and Path(cfg.deepspeed).is_file(): - with open(cfg.deepspeed, encoding="utf-8") as file: - contents = file.read() - deepspeed_cfg: DictDefault = DictDefault(json.loads(contents)) - if cfg.flash_attention: - if ( - deepspeed_cfg.zero_optimization - and deepspeed_cfg.zero_optimization.stage == 3 - ): - if not ( - ( - deepspeed_cfg.bf16 - and deepspeed_cfg.bf16.enabled # pylint: disable=no-member - is True - ) - or ( - deepspeed_cfg.fp16 - and deepspeed_cfg.fp16.enabled # pylint: disable=no-member - is True - ) - ): - raise ValueError( - "bf16.enabled or fp16.enabled must be set to true when using ZeRO-3 with flash-attention" - ) - if "8bit" in cfg.optimizer and deepspeed_cfg.optimizer: - LOG.warning( - f"conflicting optimizer: {cfg.optimizer} used alongside deepspeed optimizer." - ) - - if cfg.test_datasets and cfg.val_set_size: - raise ValueError( - "non-zero val_set_size should not be used with test_datasets configuration" - ) - - if cfg.fsdp and "bnb" in cfg.optimizer: - raise ValueError(f"FSDP not compatible with {cfg.optimizer}") - - if cfg.do_causal_lm_eval and cfg.eval_sample_packing: - raise ValueError( - "do_causal_lm_eval is enabled, eval_sample_packing must be set to False" - ) - - if cfg.eval_causal_lm_metrics: - if not isinstance(cfg.eval_causal_lm_metrics, list): - raise ValueError("eval_causal_lm_metrics must be a list") - # only ["sacrebleu", "comet", "ter", "chrf"] supported - if set(cfg.eval_causal_lm_metrics) - SUPPORTED_METRICS: - raise ValueError( - f"eval_causal_lm_metrics must be one of {SUPPORTED_METRICS}" - ) - - # TODO - # MPT 7b - # https://github.com/facebookresearch/bitsandbytes/issues/25 - # no 8bit adaAmw w bf16 - - # GPT-NeoX - # evals broken when extending context len - # File "/root/miniconda3/envs/py3.9/lib/python3.9/site-packages/transformers/models/gpt_neox/modeling_gpt_neox.py", line 162, in forward attn_output, attn_weights = self._attn(query, key, value, attention_mask, head_mask) - # File "/root/miniconda3/envs/py3.9/lib/python3.9/site-packages/optimum/bettertransformer/models/attention.py", line 74, in gpt2_wrapped_scaled_dot_product - # attention_mask = causal_mask + attention_mask - # RuntimeError: The size of tensor a (2048) must match the size of tensor b (8132) at non-singleton dimension 3 diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 10e80d9f33..a32b62f758 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -68,6 +68,7 @@ class DeprecatedParameters(BaseModel): rope_scaling: Optional[Any] = None noisy_embedding_alpha: Optional[float] = None dpo_beta: Optional[float] = None + evaluation_strategy: Optional[str] = None @field_validator("max_packed_sequence_len") @classmethod @@ -99,6 +100,13 @@ def validate_dpo_beta(cls, dpo_beta): LOG.warning("dpo_beta is deprecated, use rl_beta instead") return dpo_beta + @field_validator("evaluation_strategy") + @classmethod + def validate_evaluation_strategy(cls, evaluation_strategy): + if evaluation_strategy is not None: + LOG.warning("evaluation_strategy is deprecated, use eval_strategy instead") + return evaluation_strategy + class RemappedParameters(BaseModel): """parameters that have been remapped to other names""" @@ -731,7 +739,7 @@ class Config: warmup_ratio: Optional[float] = None eval_steps: Optional[Union[int, float]] = None evals_per_epoch: Optional[Union[int]] = None - evaluation_strategy: Optional[str] = None + eval_strategy: Optional[str] = None save_steps: Optional[Union[int, float]] = None saves_per_epoch: Optional[int] = None save_strategy: Optional[str] = None @@ -1033,21 +1041,21 @@ def check_push_save(cls, data): @classmethod def check_evals(cls, data): if ( - data.get("evaluation_strategy") + data.get("eval_strategy") and data.get("eval_steps") - and data.get("evaluation_strategy") != "steps" + and data.get("eval_strategy") != "steps" ): raise ValueError( - "evaluation_strategy and eval_steps mismatch. Please set evaluation_strategy to 'steps' or remove eval_steps." + "eval_strategy and eval_steps mismatch. Please set eval_strategy to 'steps' or remove eval_steps." ) if ( data.get("val_set_size") == 0 - and (data.get("eval_steps") or data.get("evaluation_strategy")) + and (data.get("eval_steps") or data.get("eval_strategy")) and not data.get("test_datasets") ): raise ValueError( - "eval_steps and evaluation_strategy are not supported with val_set_size == 0" + "eval_steps and eval_strategy are not supported with val_set_size == 0" ) if data.get("evals_per_epoch") and data.get("eval_steps"): raise ValueError( @@ -1055,11 +1063,11 @@ def check_evals(cls, data): ) if ( data.get("evals_per_epoch") - and data.get("evaluation_strategy") - and data.get("evaluation_strategy") != "steps" + and data.get("eval_strategy") + and data.get("eval_strategy") != "steps" ): raise ValueError( - "evaluation_strategy must be empty or set to `steps` when used with evals_per_epoch." + "eval_strategy must be empty or set to `steps` when used with evals_per_epoch." ) if data.get("do_bench_eval") and not ( @@ -1319,6 +1327,19 @@ def check_val_w_test_datasets(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_eval_strategy(cls, data): + if ( + data.get("evaluation_strategy") is not None + and data.get("eval_strategy") is None + ): + LOG.info( + "explicitly setting `eval_strategy` from the `evaluation_strategy`" + ) + data["eval_strategy"] = data.get("evaluation_strategy") + return data + @model_validator(mode="before") @classmethod def check_fsdp_offload_w_8bit_optimizer(cls, data): diff --git a/tests/test_validation.py b/tests/test_validation.py index 67670b1928..f3f4d18ab8 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -726,7 +726,7 @@ def test_no_conflict_eval_strategy(self, minimal_cfg): cfg = ( DictDefault( { - "evaluation_strategy": "epoch", + "eval_strategy": "epoch", "eval_steps": 10, } ) @@ -734,14 +734,14 @@ def test_no_conflict_eval_strategy(self, minimal_cfg): ) with pytest.raises( - ValueError, match=r".*evaluation_strategy and eval_steps mismatch.*" + ValueError, match=r".*eval_strategy and eval_steps mismatch.*" ): validate_config(cfg) cfg = ( DictDefault( { - "evaluation_strategy": "no", + "eval_strategy": "no", "eval_steps": 10, } ) @@ -749,14 +749,14 @@ def test_no_conflict_eval_strategy(self, minimal_cfg): ) with pytest.raises( - ValueError, match=r".*evaluation_strategy and eval_steps mismatch.*" + ValueError, match=r".*eval_strategy and eval_steps mismatch.*" ): validate_config(cfg) cfg = ( DictDefault( { - "evaluation_strategy": "steps", + "eval_strategy": "steps", } ) | minimal_cfg @@ -767,7 +767,7 @@ def test_no_conflict_eval_strategy(self, minimal_cfg): cfg = ( DictDefault( { - "evaluation_strategy": "steps", + "eval_strategy": "steps", "eval_steps": 10, } ) @@ -790,7 +790,7 @@ def test_no_conflict_eval_strategy(self, minimal_cfg): cfg = ( DictDefault( { - "evaluation_strategy": "no", + "eval_strategy": "no", } ) | minimal_cfg @@ -801,7 +801,7 @@ def test_no_conflict_eval_strategy(self, minimal_cfg): cfg = ( DictDefault( { - "evaluation_strategy": "epoch", + "eval_strategy": "epoch", "val_set_size": 0, } ) @@ -810,7 +810,7 @@ def test_no_conflict_eval_strategy(self, minimal_cfg): with pytest.raises( ValueError, - match=r".*eval_steps and evaluation_strategy are not supported with val_set_size == 0.*", + match=r".*eval_steps and eval_strategy are not supported with val_set_size == 0.*", ): validate_config(cfg) @@ -826,7 +826,7 @@ def test_no_conflict_eval_strategy(self, minimal_cfg): with pytest.raises( ValueError, - match=r".*eval_steps and evaluation_strategy are not supported with val_set_size == 0.*", + match=r".*eval_steps and eval_strategy are not supported with val_set_size == 0.*", ): validate_config(cfg) @@ -856,7 +856,7 @@ def test_no_conflict_eval_strategy(self, minimal_cfg): cfg = ( DictDefault( { - "evaluation_strategy": "epoch", + "eval_strategy": "epoch", "val_set_size": 0.01, } ) @@ -1095,6 +1095,24 @@ def test_dpo_beta_deprecation(self, minimal_cfg): assert new_cfg["dpo_beta"] is None assert len(self._caplog.records) == 1 + def test_eval_strategy_remap(self, minimal_cfg): + cfg = ( + DictDefault( + { + "evaluation_strategy": "steps", + } + ) + | minimal_cfg + ) + + with self._caplog.at_level(logging.WARNING): + new_cfg = validate_config(cfg) + assert new_cfg.eval_strategy == "steps" + assert ( + "evaluation_strategy is deprecated, use eval_strategy instead" + in self._caplog.records[0].message + ) + class TestValidationCheckModelConfig(BaseValidation): """ From 521e62daf1b11c609ff6862c28a4bd140f6ed090 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 15 Nov 2024 19:09:20 -0500 Subject: [PATCH 0253/1405] remove the bos token from dpo outputs (#1733) [skip ci] * remove the bos token from dpo outputs * don't forget to fix prompt_input_ids too * use processing_class instead of tokenizer * fix for processing class --- examples/qwen2/dpo.yaml | 67 +++++++++++++++++++++++ src/axolotl/core/trainer_builder.py | 19 ++++++- tests/e2e/test_qwen.py | 85 +++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 examples/qwen2/dpo.yaml create mode 100644 tests/e2e/test_qwen.py diff --git a/examples/qwen2/dpo.yaml b/examples/qwen2/dpo.yaml new file mode 100644 index 0000000000..64c3e7629b --- /dev/null +++ b/examples/qwen2/dpo.yaml @@ -0,0 +1,67 @@ +base_model: Qwen/Qwen2.5-0.5B + +strict: false + +chat_template: qwen_25 +rl: dpo +datasets: + - path: fozziethebeat/alpaca_messages_2k_dpo_test + type: chat_template.default + field_messages: conversation + field_chosen: chosen + field_rejected: rejected + message_field_role: role + message_field_content: content + roles: + system: + - system + user: + - user + assistant: + - assistant + +dataset_prepared_path: +val_set_size: 0.0 +output_dir: ./outputs/dpo-out + +sequence_len: 2048 +sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_steps: 10 +evals_per_epoch: 4 +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 1cad1a8c3f..14690580d7 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1038,24 +1038,37 @@ def push_to_hub(self, *args, **kwargs) -> str: return super().push_to_hub(*args, **kwargs) + @staticmethod def tokenize_row( - self, features, processing_class, max_prompt_length, max_completion_length, add_special_tokens, ) -> Dict: - res = super().tokenize_row( + res = DPOTrainer.tokenize_row( features, processing_class, max_prompt_length, max_completion_length, add_special_tokens, ) - if processing_class.bos_token_id is None and res["prompt_input_ids"][0] is None: + # fix when the tokenizer doesn't have a bos_token_id, e.g. Qwen + if processing_class.bos_token is None and res["prompt_input_ids"][0] is None: for key in res.keys(): res[key] = res[key][1:] + + if processing_class.bos_token and processing_class.bos_token_id is not None: + # dpo trainer may incorrectly prepend the bos_token_id to the dpo outputs + if res["chosen_input_ids"][0] == processing_class.bos_token_id: + res["chosen_input_ids"] = res["chosen_input_ids"][1:] + res["chosen_labels"] = res["chosen_labels"][1:] + res["chosen_attention_mask"] = res["chosen_attention_mask"][1:] + if res["rejected_input_ids"][0] == processing_class.bos_token_id: + res["rejected_input_ids"] = res["rejected_input_ids"][1:] + res["rejected_labels"] = res["rejected_labels"][1:] + res["rejected_attention_mask"] = res["rejected_attention_mask"][1:] + return res def training_step( diff --git a/tests/e2e/test_qwen.py b/tests/e2e/test_qwen.py new file mode 100644 index 0000000000..7a343f4d3f --- /dev/null +++ b/tests/e2e/test_qwen.py @@ -0,0 +1,85 @@ +""" +E2E tests for qwen +""" + +import logging +import os +from pathlib import Path + +import pytest +import yaml +from accelerate.test_utils import execute_subprocess_async +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger("axolotl.tests.qwen") +os.environ["WANDB_DISABLED"] = "true" + + +class TestE2eQwen: + """ + Test cases for qwen models + """ + + @pytest.mark.parametrize("base_model", ["Qwen/Qwen2-0.5B", "Qwen/Qwen2.5-0.5B"]) + def test_dpo(self, base_model, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": base_model, + "rl": "dpo", + "chat_template": "qwen_25", + "sequence_len": 2048, + "val_set_size": 0.0, + "datasets": [ + { + "path": "fozziethebeat/alpaca_messages_2k_dpo_test", + "split": "train", + "type": "chat_template.default", + "field_messages": "conversation", + "field_chosen": "chosen", + "field_rejected": "rejected", + "message_field_role": "role", + "message_field_content": "content", + "roles": { + "system": ["system"], + "user": ["user"], + "assistant": ["assistant"], + }, + }, + ], + "num_epochs": 1, + "max_steps": 5, + "warmup_steps": 20, + "micro_batch_size": 2, + "gradient_accumulation_steps": 2, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "bf16": "auto", + "tf32": True, + "gradient_checkpointing": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) From 15f1462ccd7a5505cec0f27e8087956505596b58 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 15 Nov 2024 19:09:48 -0500 Subject: [PATCH 0254/1405] support passing trust_remote_code to dataset loading (#2050) [skip ci] * support passing trust_remote_code to dataset loading * add doc for trust_remote_code in dataset config --- docs/config.qmd | 1 + src/axolotl/utils/data/sft.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/config.qmd b/docs/config.qmd index 4349f0f09f..04e278e2d0 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -91,6 +91,7 @@ datasets: name: # Optional[str] name of dataset configuration to load train_on_split: train # Optional[str] name of dataset split to load from revision: # Optional[str] The specific revision of the dataset to use when loading from the Hugging Face Hub. This can be a commit hash, tag, or branch name. If not specified, the latest version will be used. This parameter is ignored for local datasets. + trust_remote_code: # Optional[bool] Trust remote code for untrusted source # Custom user instruction prompt - path: repo diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index ec17fb9c2e..e05c029665 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -260,6 +260,7 @@ def for_d_in_datasets(dataset_configs): for config_dataset in for_d_in_datasets(cfg_datasets): ds: Optional[Union[Dataset, DatasetDict]] = None ds_from_hub = False + ds_trust_remote_code = config_dataset.trust_remote_code try: # this is just a basic check to see if the path is a # valid HF dataset that's loadable @@ -269,6 +270,7 @@ def for_d_in_datasets(dataset_configs): streaming=True, token=use_auth_token, revision=config_dataset.revision, + trust_remote_code=ds_trust_remote_code, ) ds_from_hub = True except (FileNotFoundError, ConnectionError, HFValidationError, ValueError): @@ -366,7 +368,7 @@ def for_d_in_datasets(dataset_configs): elif ds_from_hub: load_ds_kwargs = {} if config_dataset.split: - load_ds_kwargs = {"split": config_dataset.split} + load_ds_kwargs["split"] = config_dataset.split ds = load_dataset( config_dataset.path, name=config_dataset.name, @@ -374,6 +376,7 @@ def for_d_in_datasets(dataset_configs): data_files=config_dataset.data_files, token=use_auth_token, revision=config_dataset.revision, + trust_remote_code=config_dataset.trust_remote_code, **load_ds_kwargs, ) elif ds_from_cloud and remote_file_system: @@ -391,6 +394,7 @@ def for_d_in_datasets(dataset_configs): streaming=False, split=None, storage_options=storage_options, + trust_remote_code=config_dataset.trust_remote_code, ) elif config_dataset.path.startswith("https://"): ds_type = get_ds_type(config_dataset) @@ -401,6 +405,7 @@ def for_d_in_datasets(dataset_configs): streaming=False, split=None, storage_options=storage_options, + trust_remote_code=config_dataset.trust_remote_code, ) else: if isinstance(config_dataset.data_files, str): From 0dabde19621545342556828be436b5d4c0886a16 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 15 Nov 2024 19:10:14 -0500 Subject: [PATCH 0255/1405] support for schedule free and e2e ci smoke test (#2066) [skip ci] * support for schedule free and e2e ci smoke test * set default lr scheduler to constant in test * ignore duplicate code * fix quotes for config/dict --- requirements.txt | 1 + tests/e2e/test_optimizers.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/requirements.txt b/requirements.txt index aab4d0eade..f352fecda9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -53,3 +53,4 @@ immutabledict==4.2.0 antlr4-python3-runtime==4.13.2 torchao==0.5.0 +schedulefree==1.3.0 diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 3b68ec5ad5..b9fa368f6f 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -108,3 +108,37 @@ def test_adopt_adamw(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.bin").exists() + + @with_temp_dir + def test_fft_schedule_free_adamw(self, temp_dir): + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM-135M", + "sequence_len": 1024, + "val_set_size": 0.1, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "schedule_free_adamw", + "lr_scheduler": "constant", + "save_safetensors": True, + } + ) + # pylint: disable=duplicate-code + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() From d42f2020466067c77ccbb325411f291caf8fb696 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 15 Nov 2024 19:11:04 -0500 Subject: [PATCH 0256/1405] Fsdp grad accum monkeypatch (#2064) --- requirements.txt | 2 +- .../monkeypatch/trainer_fsdp_grad_accum.py | 83 +++++++++++++++++++ src/axolotl/utils/trainer.py | 8 ++ tests/e2e/patched/test_trainer_fsdp.py | 15 ++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/axolotl/monkeypatch/trainer_fsdp_grad_accum.py create mode 100644 tests/e2e/patched/test_trainer_fsdp.py diff --git a/requirements.txt b/requirements.txt index f352fecda9..0997930f4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.13.2 -transformers==4.46.1 +transformers==4.46.2 tokenizers>=0.20.1 bitsandbytes==0.44.1 accelerate==1.1.0 diff --git a/src/axolotl/monkeypatch/trainer_fsdp_grad_accum.py b/src/axolotl/monkeypatch/trainer_fsdp_grad_accum.py new file mode 100644 index 0000000000..6819fde111 --- /dev/null +++ b/src/axolotl/monkeypatch/trainer_fsdp_grad_accum.py @@ -0,0 +1,83 @@ +""" +fix for FSDP gradient accumulation +see https://github.com/huggingface/transformers/pull/34645 +""" +import inspect + +from accelerate.logging import get_logger +from transformers.trainer import Trainer + +from axolotl.monkeypatch.unsloth_ import detab_code + +LOG = get_logger("axolotl.monkeypatch.trainer_fsdp_grad_accumulation") + +ORIGINAL_CONTEXT_CODE = """ + context = ( + functools.partial(self.accelerator.no_sync, model=model) + if i == len(batch_samples) - 1 + else contextlib.nullcontext + ) +""" + +PATCHED_CONTEXT_CODE = """ + context = ( + functools.partial(self.accelerator.no_sync, model=model) + if i != len(batch_samples) - 1 + else contextlib.nullcontext + ) +""" + + +def get_training_loop_code() -> str: + training_loop = inspect.getsource( + Trainer._inner_training_loop # pylint: disable=protected-access + ) + return training_loop + + +def check_training_loop_is_patchable() -> bool: + train_loop = get_training_loop_code() + train_loop, _ = detab_code(train_loop) + return ORIGINAL_CONTEXT_CODE in train_loop + + +def patch_training_loop_for_fsdp_grad_accum(): + """ + monkeypatch for fixing the training loop for FSDP gradient accumulation + """ + + train_loop = get_training_loop_code() + Trainer._original_inner_training_loop = ( # pylint: disable=protected-access + train_loop + ) + train_loop, _ = detab_code(train_loop) + assert ( + ORIGINAL_CONTEXT_CODE in train_loop + ), "Original _inner_training_loop code not found" + + train_loop = train_loop.replace(ORIGINAL_CONTEXT_CODE, PATCHED_CONTEXT_CODE) + train_loop = train_loop.replace( + "def _inner_training_loop(", + "def _fixed_inner_training_loop(", + 1, + ) + + # load imports necessary + import transformers.trainer + + items_to_import = [] + for item in dir(transformers.trainer): + if item in train_loop: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + "from transformers.trainer import (" + + ", ".join(x for x in items_to_import) + + ")", + globals(), + ) + exec(train_loop, globals()) # pylint: disable=exec-used # nosec B102 + LOG.info("patching _inner_training_loop", main_process_only=True) + Trainer._inner_training_loop = ( # pylint: disable=protected-access + _fixed_inner_training_loop # pylint: disable=undefined-variable # noqa: F821 + ) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 2d3a6944f7..5c9bfd6635 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -16,6 +16,9 @@ from transformers.utils import is_torch_bf16_gpu_available from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder +from axolotl.monkeypatch.trainer_fsdp_grad_accum import ( + patch_training_loop_for_fsdp_grad_accum, +) from axolotl.utils.distributed import reduce_and_broadcast from axolotl.utils.environment import check_cuda_p2p_ib_support from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths @@ -493,6 +496,11 @@ def prepare_opinionated_env(cfg): def setup_trainer( cfg, train_dataset, eval_dataset, model, tokenizer, processor, total_num_steps ): + if cfg.fsdp: + try: + patch_training_loop_for_fsdp_grad_accum() + except AssertionError: + pass if cfg.rl in ["dpo", "ipo", "orpo", "kto", "simpo"]: trainer_builder = HFRLTrainerBuilder(cfg, model[0], tokenizer, processor) trainer_builder.model_ref = model[1] diff --git a/tests/e2e/patched/test_trainer_fsdp.py b/tests/e2e/patched/test_trainer_fsdp.py new file mode 100644 index 0000000000..1095cff3c0 --- /dev/null +++ b/tests/e2e/patched/test_trainer_fsdp.py @@ -0,0 +1,15 @@ +"""Test module for checking whether the integration of Unsloth with Hugging Face Transformers is working as expected.""" +import unittest + +from axolotl.monkeypatch.trainer_fsdp_grad_accum import check_training_loop_is_patchable + + +class TestTrainerFSDPIntegration(unittest.TestCase): + """Unsloth monkeypatch integration tests.""" + + def test_train_loop_patchable(self): + # ensures the current version of transformers has loss code that matches our patching code + self.assertTrue( + check_training_loop_is_patchable(), + "HF transformers _inner_training_loop has changed and isn't patchable", + ) From fd70eec57731ae4def9624d50014f3622c64e037 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 16 Nov 2024 08:35:26 +0700 Subject: [PATCH 0257/1405] fix: loading locally downloaded dataset (#2056) [skip ci] --- src/axolotl/utils/data/sft.py | 10 +++- tests/test_datasets.py | 107 ++++++++++++++++++++++------------ 2 files changed, 80 insertions(+), 37 deletions(-) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index e05c029665..b72e0a2b38 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -350,7 +350,15 @@ def for_d_in_datasets(dataset_configs): split=None, ) else: - ds = load_from_disk(config_dataset.path) + try: + ds = load_from_disk(config_dataset.path) + except FileNotFoundError: + ds = load_dataset( + config_dataset.path, + name=config_dataset.name, + streaming=False, + split=None, + ) elif local_path.is_file(): ds_type = get_ds_type(config_dataset) diff --git a/tests/test_datasets.py b/tests/test_datasets.py index a57b6d83e2..e87f19cc7f 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -371,44 +371,79 @@ def test_load_hub_with_revision_with_dpo(self): def test_load_local_hub_with_revision(self): """Verify that a local copy of a hub dataset can be loaded with a specific revision""" with tempfile.TemporaryDirectory() as tmp_dir: - with tempfile.TemporaryDirectory() as tmp_dir2: - tmp_ds_path = Path(tmp_dir2) / "mhenrichsen/alpaca_2k_test" - tmp_ds_path.mkdir(parents=True, exist_ok=True) - snapshot_download( - repo_id="mhenrichsen/alpaca_2k_test", - repo_type="dataset", - local_dir=tmp_ds_path, - revision="d05c1cb", - ) - - prepared_path = Path(tmp_dir) / "prepared" - cfg = DictDefault( - { - "tokenizer_config": "huggyllama/llama-7b", - "sequence_len": 1024, - "datasets": [ - { - "path": "mhenrichsen/alpaca_2k_test", - "ds_type": "parquet", - "type": "alpaca", - "data_files": [ - f"{tmp_ds_path}/alpaca_2000.parquet", - ], - "revision": "d05c1cb", - }, - ], - } - ) + tmp_ds_path = Path(tmp_dir) / "mhenrichsen/alpaca_2k_test" + tmp_ds_path.mkdir(parents=True, exist_ok=True) + snapshot_download( + repo_id="mhenrichsen/alpaca_2k_test", + repo_type="dataset", + local_dir=tmp_ds_path, + revision="d05c1cb", + ) + + prepared_path = Path(tmp_dir) / "prepared" + cfg = DictDefault( + { + "tokenizer_config": "huggyllama/llama-7b", + "sequence_len": 1024, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "ds_type": "parquet", + "type": "alpaca", + "data_files": [ + f"{tmp_ds_path}/alpaca_2000.parquet", + ], + "revision": "d05c1cb", + }, + ], + } + ) - dataset, _ = load_tokenized_prepared_datasets( - self.tokenizer, cfg, prepared_path - ) + dataset, _ = load_tokenized_prepared_datasets( + self.tokenizer, cfg, prepared_path + ) - assert len(dataset) == 2000 - assert "input_ids" in dataset.features - assert "attention_mask" in dataset.features - assert "labels" in dataset.features - shutil.rmtree(tmp_ds_path) + assert len(dataset) == 2000 + assert "input_ids" in dataset.features + assert "attention_mask" in dataset.features + assert "labels" in dataset.features + shutil.rmtree(tmp_ds_path) + + def test_loading_local_dataset_folder(self): + """Verify that a dataset downloaded to a local folder can be loaded""" + + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_ds_path = Path(tmp_dir) / "mhenrichsen/alpaca_2k_test" + tmp_ds_path.mkdir(parents=True, exist_ok=True) + snapshot_download( + repo_id="mhenrichsen/alpaca_2k_test", + repo_type="dataset", + local_dir=tmp_ds_path, + ) + + prepared_path = Path(tmp_dir) / "prepared" + cfg = DictDefault( + { + "tokenizer_config": "huggyllama/llama-7b", + "sequence_len": 1024, + "datasets": [ + { + "path": str(tmp_ds_path), + "type": "alpaca", + }, + ], + } + ) + + dataset, _ = load_tokenized_prepared_datasets( + self.tokenizer, cfg, prepared_path + ) + + assert len(dataset) == 2000 + assert "input_ids" in dataset.features + assert "attention_mask" in dataset.features + assert "labels" in dataset.features + shutil.rmtree(tmp_ds_path) if __name__ == "__main__": From 0c8b1d824aeaa41f27b4ed4e73e8906751180322 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Sat, 16 Nov 2024 07:05:50 +0530 Subject: [PATCH 0258/1405] Update `get_unpad_data` patching for multipack (#2013) * Update `get_unpad_data` patching for multipack * Update src/axolotl/utils/models.py * Update src/axolotl/utils/models.py * Add test case --------- Co-authored-by: Wing Lian Co-authored-by: Wing Lian --- src/axolotl/monkeypatch/multipack.py | 72 ++++++---------------------- src/axolotl/utils/models.py | 9 +++- tests/e2e/test_llama.py | 66 +++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 58 deletions(-) create mode 100644 tests/e2e/test_llama.py diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 85101cd3c4..3ee89d2e5c 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -1,4 +1,5 @@ """multipack patching for v2 of sample packing""" + import importlib import transformers @@ -27,71 +28,28 @@ ] -def patch_for_multipack(model_type, model_name=None, is_remote_code=False): - if model_type == "gemmoe": - patch_remote(model_name, ".configuration_gemmoe", ".modeling_gemmoe") - elif model_type == "deepseek_v2": - patch_remote(model_name, ".configuration_deepseek", ".modeling_deepseek") - elif hasattr(transformers, "modeling_flash_attention_utils") and not is_remote_code: +def patch_for_multipack(model_type, model_name=None, has_remote_code=False): + if has_remote_code: + patch_remote(model_name) + elif hasattr(transformers, "modeling_flash_attention_utils"): transformers.modeling_flash_attention_utils._get_unpad_data = ( # pylint: disable=protected-access get_unpad_data ) - if model_type == "mixtral" and is_deepspeed_zero3_enabled(): - patch_mixtral_moe_forward_zero3() - return - # retain for legacy - if model_type == "mixtral": - transformers.models.mixtral.modeling_mixtral._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) - if is_deepspeed_zero3_enabled(): - patch_mixtral_moe_forward_zero3() - elif model_type == "llama": - if hasattr(transformers.models.llama.modeling_llama, "_get_unpad_data"): - transformers.models.llama.modeling_llama._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) - elif model_type == "mistral": - if hasattr(transformers.models.mistral.modeling_mistral, "_get_unpad_data"): - transformers.models.llama.modeling_llama._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) - elif model_type == "qwen2": - transformers.models.qwen2.modeling_qwen2._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) - elif model_type == "qwen2_moe": - transformers.models.qwen2_moe.modeling_qwen2_moe._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) - elif model_type == "falcon": - transformers.models.falcon.modeling_falcon._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) - elif model_type == "phi": - transformers.models.phi.modeling_phi._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) - elif model_type == "gemma": - transformers.models.gemma.modeling_gemma._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) - elif model_type == "gemma2": - transformers.models.gemma2.modeling_gemma2._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) - elif model_type == "starcoder2": - transformers.models.starcoder2.modeling_starcoder2._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) + if model_type == "mixtral" and is_deepspeed_zero3_enabled(): + patch_mixtral_moe_forward_zero3() -def patch_remote(model_name, config_name, modeling_name): +def patch_remote(model_name): model_config = AutoConfig.from_pretrained(model_name, trust_remote_code=True) # we need to load the model here in order for modeling_* to be available with init_empty_weights(): AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True) - module_name = model_config.__class__.__module__.replace(config_name, modeling_name) + parts = model_config.__class__.__module__.split(".") + parts[-1] = parts[-1].replace("configuration_", "modeling_", 1) + module_name = ".".join(parts) modeling_arch = importlib.import_module(module_name) - modeling_arch._get_unpad_data = get_unpad_data # pylint: disable=protected-access + if hasattr(modeling_arch, "_get_unpad_data"): + modeling_arch._get_unpad_data = ( # pylint: disable=protected-access + get_unpad_data + ) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index db66c65f25..75c93fa2a5 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -395,10 +395,17 @@ def apply_patches(self) -> None: and self.cfg.flash_attention and self.cfg.sample_packing ): + has_remote_code = ( + "auto_map" in self.model_config + and "AutoModelForCausalLM" in self.model_config["auto_map"] + ) + if has_remote_code and self.cfg.trust_remote_code is False: + # if explicitly set in the YAML, we should prefer that, for example if explicitly disabled + has_remote_code = self.cfg.trust_remote_code patch_for_multipack( self.cfg.model_config_type, model_name=self.cfg.base_model, - is_remote_code=self.cfg.trust_remote_code, + has_remote_code=has_remote_code, ) if self.cfg.is_llama_derived_model: diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py new file mode 100644 index 0000000000..4e885a76db --- /dev/null +++ b/tests/e2e/test_llama.py @@ -0,0 +1,66 @@ +""" +E2E tests for llama +""" + +import logging +import os +import unittest +from pathlib import Path + +from axolotl.cli import load_datasets +from axolotl.common.cli import TrainerCliArgs +from axolotl.train import train +from axolotl.utils.config import normalize_config +from axolotl.utils.dict import DictDefault + +from .utils import with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestLlama(unittest.TestCase): + """ + Test case for Llama models + """ + + @with_temp_dir + def test_fft_trust_remote_code(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "trust_remote_code": True, + "sequence_len": 512, + "val_set_size": 0.1, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": True, + "bf16": True, + "save_safetensors": True, + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() From c06b8f02431d293f23ea562d20cd81ec1209cf87 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 18 Nov 2024 11:52:35 -0500 Subject: [PATCH 0259/1405] increase worker count to 8 for basic pytests (#2075) [skip ci] --- .github/workflows/tests.yml | 2 +- cicd/cicd.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6b44317d93..b4ef404215 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -71,7 +71,7 @@ jobs: - name: Run tests run: | - pytest --ignore=tests/e2e/ tests/ + pytest -n8 --ignore=tests/e2e/ tests/ - name: cleanup pip cache run: | diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 483d62a7ad..e199e112ff 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -pytest -n4 --ignore=tests/e2e/ /workspace/axolotl/tests/ +pytest -n8 --ignore=tests/e2e/ /workspace/axolotl/tests/ pytest -n1 --dist loadfile -v /workspace/axolotl/tests/e2e/patched/ /workspace/axolotl/tests/e2e/integrations/ pytest --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ From 70cf79ef52b4ff79214f63638c3cdfdfceee55c9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 18 Nov 2024 11:53:37 -0500 Subject: [PATCH 0260/1405] upgrade autoawq==0.2.7.post2 for transformers fix (#2070) * point to upstream autoawq for transformers fix * use autoawq 0.2.7 release * test wheel for awq * try different format for wheel def * autoawq re-release * Add intel_extension_for_pytorch dep * ipex gte version * forcefully remove intel-extension-for-pytorch * add -y option to pip uninstall for ipex * use post2 release for autoawq and remove uninstall of ipex --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0997930f4c..f8fe20bf97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ art gradio==3.50.2 tensorboard python-dotenv==1.0.1 -autoawq>=0.2.5 +autoawq==0.2.7.post2 triton>=2.3.0 liger-kernel==0.4.1 From 9871fa060bfd3a3d047ac843969979db0dead1c2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 18 Nov 2024 12:35:31 -0500 Subject: [PATCH 0261/1405] optim e2e tests to run a bit faster (#2069) [skip ci] * optim e2e tests to run a bit faster * run prequant w/o lora_modules_to_save * use smollm2 --- tests/e2e/conftest.py | 19 ++++++++++ tests/e2e/multigpu/test_llama.py | 38 +++++++++---------- tests/e2e/patched/test_fa_xentropy.py | 2 + .../e2e/patched/test_lora_llama_multipack.py | 3 ++ tests/e2e/test_optimizers.py | 2 +- tests/e2e/test_packing_loss.py | 2 +- 6 files changed, 43 insertions(+), 23 deletions(-) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 723a44f03a..c316f6c83e 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -5,6 +5,25 @@ import tempfile import pytest +from huggingface_hub import snapshot_download + + +@pytest.fixture(scope="session", autouse=True) +def download_smollm2_135m_model(): + # download the model + snapshot_download("HuggingFaceTB/SmolLM2-135M") + + +@pytest.fixture(scope="session", autouse=True) +def download_tatsu_lab_alpaca_dataset(): + # download the model + snapshot_download("tatsu-lab/alpaca", repo_type="dataset") + + +@pytest.fixture(scope="session", autouse=True) +def download_mhenrichsen_alpaca_2k_dataset(): + # download the model + snapshot_download("mhenrichsen/alpaca_2k_test", repo_type="dataset") @pytest.fixture diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index b2c8abc604..d8dcf3118a 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -25,7 +25,7 @@ @pytest.fixture(scope="session", autouse=True) def download_model(): # download the model - snapshot_download("TinyLlama/TinyLlama_v1.1") + snapshot_download("HuggingFaceTB/SmolLM2-135M") class TestMultiGPULlama: @@ -37,7 +37,7 @@ def test_lora_ddp(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "HuggingFaceTB/SmolLM-135M", + "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 2048, "adapter": "lora", "lora_r": 8, @@ -93,7 +93,7 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "HuggingFaceTB/SmolLM-135M", + "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 2048, "sample_packing": True, "eval_sample_packing": False, @@ -149,8 +149,7 @@ def test_dpo_lora_ddp(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "TinyLlama/TinyLlama_v1.1", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 2048, "sample_packing": False, "eval_sample_packing": False, @@ -163,12 +162,10 @@ def test_dpo_lora_ddp(self, temp_dir): "lora_target_linear": True, "val_set_size": 0.05, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "rl": "dpo", - "chat_template": "llama3", + "chat_template": "chatml", "datasets": [ { "path": "fozziethebeat/alpaca_messages_2k_dpo_test", @@ -221,7 +218,7 @@ def test_dpo_qlora_ddp(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "HuggingFaceTB/SmolLM-135M", + "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 2048, "sample_packing": False, "eval_sample_packing": False, @@ -294,7 +291,7 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "HuggingFaceTB/SmolLM-135M", + "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 2048, "val_set_size": 0.01, "special_tokens": { @@ -359,7 +356,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "HuggingFaceTB/SmolLM-135M", + "base_model": "HuggingFaceTB/SmolLM2-135M", "sample_packing": True, "pad_to_sequence_len": True, "sequence_len": 2048, @@ -422,8 +419,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "axolotl-ai-co/TinyLlama_v1.1-bnb-nf4-bf16", - "tokenizer_type": "AutoTokenizer", + "base_model": "axolotl-ai-co/SmolLM2-135M-bnb-nf4-bf16", "adapter": "qlora", "mean_resizing_embeddings": True, "load_in_4bit": True, @@ -431,17 +427,17 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "lora_modules_to_save": [ - "embed_tokens", - "lm_head", - ], + # "lora_modules_to_save": [ + # "embed_tokens", + # "lm_head", + # ], "sample_packing": True, "eval_sample_packing": False, "pad_to_sequence_len": True, "sequence_len": 2048, "val_set_size": 0.05, "special_tokens": { - "pad_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -503,7 +499,7 @@ def test_ds_zero3_packed(self, temp_dir, gradient_accumulation_steps): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "HuggingFaceTB/SmolLM-135M", + "base_model": "HuggingFaceTB/SmolLM2-135M", "sample_packing": True, "pad_to_sequence_len": True, "sequence_len": 2048, @@ -553,7 +549,7 @@ def test_ds_zero3_qlora_packed(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "HuggingFaceTB/SmolLM-135M", + "base_model": "HuggingFaceTB/SmolLM2-135M", "load_in_4bit": True, "adapter": "qlora", "lora_r": 8, diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index 0991bdd742..8b76362fb4 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -66,6 +66,8 @@ def test_lora_packing_fa_cross_entropy(self, temp_dir): }, ], "num_epochs": 1, + "max_steps": 10, + "save_steps": 10, "micro_batch_size": 8, "gradient_accumulation_steps": 1, "output_dir": temp_dir, diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index f251f9b661..5dbf146542 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -56,6 +56,8 @@ def test_lora_packing(self, temp_dir): }, ], "num_epochs": 2, + "max_steps": 20, + "save_steps": 10, "micro_batch_size": 8, "gradient_accumulation_steps": 1, "output_dir": temp_dir, @@ -109,6 +111,7 @@ def test_lora_gptq_packed(self, temp_dir): }, ], "num_epochs": 2, + "max_steps": 20, "save_steps": 0.5, "micro_batch_size": 8, "gradient_accumulation_steps": 1, diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index b9fa368f6f..af5445461c 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -113,7 +113,7 @@ def test_adopt_adamw(self, temp_dir): def test_fft_schedule_free_adamw(self, temp_dir): cfg = DictDefault( { - "base_model": "HuggingFaceTB/SmolLM-135M", + "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 1024, "val_set_size": 0.1, "special_tokens": { diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index 73f9e60bac..60f1673814 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -31,7 +31,7 @@ def test_loss_packed(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "HuggingFaceTB/SmolLM-135M", + "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 1024, "sample_packing": True, "flash_attention": True, From 8403c67156af935f6a5ef665976a1458ecbcc03e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 18 Nov 2024 12:36:03 -0500 Subject: [PATCH 0262/1405] don't build bdist (#2076) [skip ci] --- .github/workflows/pypi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index ae0a77e2fa..33deccb29c 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -56,9 +56,9 @@ jobs: run: | sed -i -E 's/version="([0-9.]+)",/version="${{ steps.tag.outputs.TAG_NAME }}",/g' setup.py - - name: Build a binary wheel + - name: Build a source dist run: | - python setup.py sdist bdist_wheel + python setup.py sdist - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 From ed079d434ae704ada0d84857a1c301c690cfdb41 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 18 Nov 2024 13:59:32 -0500 Subject: [PATCH 0263/1405] static assets, readme, and badges update v1 (#2077) --- README.md | 4 ++++ image/axolotl-badge-web-legacy.png | Bin 0 -> 11656 bytes image/axolotl-badge-web.png | Bin 11656 -> 25073 bytes image/axolotl_symbol_digital_black.svg | 26 +++++++++++++++++++++++ image/axolotl_symbol_digital_white.svg | 16 ++++++++++++++ image/axolotl_wordmark_digital_black.svg | 17 +++++++++++++++ image/axolotl_wordmark_digital_white.svg | 24 +++++++++++++++++++++ 7 files changed, 87 insertions(+) create mode 100644 image/axolotl-badge-web-legacy.png create mode 100644 image/axolotl_symbol_digital_black.svg create mode 100644 image/axolotl_symbol_digital_white.svg create mode 100644 image/axolotl_wordmark_digital_black.svg create mode 100644 image/axolotl_wordmark_digital_white.svg diff --git a/README.md b/README.md index 7e4d2e376e..d4d75d99b0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # Axolotl +![GitHub License](https://img.shields.io/github/license/axolotl-ai-cloud/axolotl.svg?color=blue) ![tests](https://github.com/axolotl-ai-cloud/axolotl/actions/workflows/tests.yml/badge.svg) +![https://github.com/axolotl-ai-cloud/axolotl/releases](https://img.shields.io/github/release/axolotl-ai-cloud/axolotl.svg) +![GitHub Repo stars](https://img.shields.io/github/stars/axolotl-ai-cloud/axolotl) + ![tests-nightly](https://github.com/axolotl-ai-cloud/axolotl/actions/workflows/tests-nightly.yml/badge.svg) ![multigpu-semi-weekly tests](https://github.com/axolotl-ai-cloud/axolotl/actions/workflows/multi-gpu-e2e.yml/badge.svg) diff --git a/image/axolotl-badge-web-legacy.png b/image/axolotl-badge-web-legacy.png new file mode 100644 index 0000000000000000000000000000000000000000..42217dca31ca758d822a97655350793fa4e4dc59 GIT binary patch literal 11656 zcmV;3EqBt1P)KA>;ADskL~wVAU=iGd`vMz=;I4zaYutlPcMsVsJHOlg z-+NtID%1ovHY~?Is(wZH>#x0eKfd=`4t>neU+L)lg^|qq)M){;K)+%3z4sgjlE3x}DbXw63Rh{-H1Zky;-+els!a{R{EG z2w*T648}o1vyP{AKXt$-i{l^v@gM*4`pu+w3oLmW6`uj$kVu?&^n%6F(>Sbef`2@0 zz+f;Ki~|6z(`nsK>v&q%(>kBl{nP=}1=I;2mvxv~9ZAeAMoLzxDl9q`HpkEJo1K(d z${~?i;hP_fw74DygTY`}le(WefVzM>!NAz&@UHUEm6-i)Qr3?kyu9QQu&*lW?{uZ` zcK62lvko|{XRPJ*F&GR6gF1k^fI5Ky>WB}@<8S`vZ~inst$?I0#V^iYbl-P7Vr*uM z?C5wz`vgNOvDl;aG8ha7gF1n_AuhcD)D;>62@Z?-Z_?f+xcXh)R~!CMM+|X9-vmz1 zp6Dx6pd~RICv8sQ8&ZB)-x&JFX3&$E;aja`OAH2s!JuxSjtB~m`R{G=Fg3S3K_(z7 zHk~#V?MZW#mHZUWo^yne)Np*`+2J+V1%uaykqTOG+Z zF_~J(k#H*sKPKnn%yEb98%bhy3`}XfIIoMV7g{PS(O=hqr)^zWEp0|)LJsQ6$}l(7 zgV$rjn5n2ieE3ZiC#GXC^DcT~O5pA2L+AfW`@vu^7z~>e_M$7OGemjl7|2YhQNnH| zYt35LHYaiX`1viN$mM5HmRtx$QV~wb&ux_(TO5V2PXOXVBXFL`jLO8Dj8C+;;!R&4 z)*F>jw+v!sXdJ8ali(I6p&lQ_n>HmD%IokVw;s=LS0c|b0+!}>UwK~`3-Vk4~{ zYvBqsJH^9JBF8@+X4waW!C-uCs4J*5s5=h*W)rD}{Hw2sis797W%O6oqdGPn#!{>mm4~YreZ~Ha)ynm9&+P6dL_dO%-d4jYT((9@T}#6phTj4Z4z z&*AOjEQFyU@D*)%p4)`y?R`)U3_>9C5Sn_(v{CRxGW^mMxM3ob{(cCpWLi-(xVRc{ zvDM(xtD(A6hbPhHNDYZVU49Xkd%LhPJ_4?}7mqV*kZ~g8bl(LAgTbKApzfdo`IVrR{tq2P8T|c%@N#Gv&wpw{ad13BoB~l0 zm<0P{7jXQPJz8q3@%GUo-p);7v%3$Q)!k6HkANQ@5u2xk+776D1|Uq0L!f;gL>@vz zJA}#>2&GLB@*2SB)I*)$0Cip+_}n^hC5?DmQ;&6}66%RzsELfYye7Q)sTG6wsxZ~u zfYsrCaP8e#t7^r9q7Ll|*{Jl7Mu@!^j<9!qF>SPHer6oeJa^f>zQTDu|4vG`t)c z#ySzfXiOPeBl4jP%7Ley6I|UqG2TIBurLK~U;unu4>pxU*c=#zpzJ3C>4Cbl7wR!G zK0QXHGC;~nOnoOrf!rkmQM3?RA=I}*UD-s+o1w01gV0LFqXggD3BITaDk>Nv43(0U zk#co2DgUV%LU|)NvYgdB_b?EW39nPmECXRM7>wT+$S+zGh=J4su{UE-TUdgZqeI}D zJMc2O1`n@RV8}BIHyr|zm6m}wvr|M4qu~4cMFbMd4-%OS4}zN*0Y62=F+TyG#L#k~ zzYprJ9wLJ-sL3*gwoW38E{Ia-?1s9r9h=o{;2YYYZfXZ#+z2734m_0&l}cBa7$asc zxwRsy^eVi(br*NN;$UH7^GBW-27|$10-&5l7$J-ggD1a5rpLf7O+j^k24cU*!lYQnlNcJuO$>{j=&G?{s3u3C znjHlYyM>lGOF%7O1m zSNiP9AANEd3l6pL{14!ZKwVst07c7&9e`L>L(WucXaa9pEd7 zG>YoM71x31;1lY=-D!f*HYjEw{34M8kpTB_PP|Q59DY16 zq9SEf4x&`%CPaa3{3eVCqGYh~WFA{Qm?5!5LX$(_hI*lD?*!jML{ip3)?bT_lxlFv zwOET#;I(HNmM-NZT^#n%dxYO3o*7D*KgypXCUNfFG1J%j}e=j zZ>Yu>%N(42G~zD0`f2RLX$O}*-`;(2T)v1)ZhnY}PThv}@pJn^m^TtLK6tI3w0Ay` z=Xm#Pnr*M9K(zD>$jk9DY)+0s-O?pyAVPXQgsgg~OPZnX>;X4FPQ)<-)l(u2A_IPD zM$A6wI28)FAU0z`92Uhv7Rbl% z@BS4?z|~v3U6)$QkeNVszjh9&KpDq87yoUfjCYPl)*Gg2dDsMM)Xp z>$;&<)?BeDegGuO5?m7C%}dD%#vd`$c+`*eIyQ zYE%X0eTooq#*e;Hk_trpD3y+p$({+s#rKK^?tWL`cl8FsqEir;UiiWJVp4PWtizV3 z%ub#`+pv_MIUoYj{&6`70-;T(yUBwR5u3&Y!UXa)`aPsDv#^7iOipiy6~7qh;pm0s z;s$VqjZoiH;^*NB@RP&uWgk=oBEK(@2ed<-urP&V`dzxxk=hTvWq+xw|IXVKQn=ivNpVmRAA(K2|UlZiA`?SCMS^YoruJc zIOt0(_Ju$w-{?p_)OGx!1SYIe=v}J{BhBeKXa z&jdoVkC@0rjOKNsBd!J^=R@%Q5sCN>5KntI%q1$YmevTax*MuaB~h+`nMl=X&G{A&v&8o5rDO8fe^|1b26L zcMncni0g{X<;D{t5SQ^k;d`~;8rD4CsZOWo_S~Ku^21&%`qZg9QfGhlZ7bH#P1fHn zyjr}?g#CSZ18=Tg!qeUR@i1oyM@`vZq^DYZ2e$tBIdtfEdU5fep!Ry zTss#}-Ucr##N{m!8{u(68@_*7+<(mnLW;4fxtE0dES8Uv@Bx4?VX-NKzS45bd2QM6 zWrDVJf4sKo1(kxfGM|ulelBX?uL=MmC7YO4taSCCoK+$KLk$SslVB{S3@Nl+@OxoZ z^V_}~V)hi4%)NdaM|aM5<3!Jgd#}pIAQqHYUayiZWw7tX)Y?(w0HTTU)to!;4}fep zvP76)3}y<4F_74XFvkcO7+KMzf}?{wCX3qexMCX5y7%Dafg>z5mxb)UTEw6K{t|!r z$1D8x@2~KezrDntUoYX+<2&T<5P}HhfPBkX#5@yz2rmg zd5jl~!7NQ3#6nshPKVbcnq4me2wmqk26l!<)-W-Z&}66UCebQ@5R2eVBoj;f9~FSa zBxb#9kbqhM;^3wboKGdDw;EaqR)uAp)f_!Hj-sPJ1RpJ3m+I%D_Wh~^5Jg}l3d$Q* z_Ct9ZR<@coBjuLV3C59n20+i$n#+uBojp}3SSdTNhoexd1m9X>hLGiy*LA>{7zp22 zL*ef4{M<|HJBcM|3_vt8zWQ_L{qlhr?+_!@AslgzF%X+OQdL`+Ti8P^aYjdGH5T$l zu~a^a7yFOl#r>Oj^7H|g9^b>#;$18~x`Ss=@8k8GCwR?(<>k`t6(I8B9uvwDxptMY z7bbV+x!^%ep3WDYzCm(6qG|FW}QSlk?kSrO`y>@r+e=|HgsTcjmXMy|# z%>Ov5krcoB0HV?HS2uu=0$yG{&}#nwnyrkjLB0i2$h7+)ZPw%`^~aw0dfY4=#QgXy zj?T`YvAPDuc?GDetY&}n(AH3gb0?4DE(3@s_paf^!Zm?!n6+l zxW?*Zwba(2G%p`R-Rx#dD^QwWfWtk*IFj0mtYC7OTP|NayH0bsI(s24HXR-wfsoj^ zLF(j1rn&zLfFz_Ba3wjXM5)rC$ShKDKQ;hyQv}hNB)k>q@-Y%x-Kz6^$sVurqg*z* z@XaT(6>Yr&A_Q$n>8bNv)VyB~fC#0yrVTsO@(~i7jFjwBVnr&vAd@aS5D1g@!BQEKIT6 zD>)Mhkx7sk+d-@+fs=^?>@c?_5L_-zPeS*ltvnF==x8O)9Ygy7qO~Y2GVuudi%ra4(T0?+(iI>(f5i$gIfoB2 zTFL_$rndpP`@ePF@b7#i8Y$W#KcoeX9S$YR}NF`V9s(zrr)-4YNW5;!WneBmPXBoNUW zfTS=du*kvf1Aqvft%Cl@k-0Jywfx^eTvW3{c`?9P(U$oe1S>=>CKA%>R;lvL6t z)z`r@A^8MFb3uUW;HmasnIl{1q!$Q5q#Ij#Be zS0R8b)T{H$EKN)@1l!mqy4!}`y*VEZN$)K%2t!TF20 z+&hZ;*mSs9xG?1$0fl=YKK;Yj`0X#gfc`f|NS8$*)iDefx|VPhNl{-?iPoxG)R$Lb zxRYIcQ8`+wYS`amSTgX~x=tUZv8mYI&*ihwLejbMQ03;x6r_%Pm zA+bX9T~ao$tvZ1T6e09hC-<^YC@OD`@)kKf5jhA$cH41 zb_5GHKuZtke6|hS zbLzhw+j3f+Hc5ulUuR z)0!WDl>$hxcQ~%MjpJp{G+yl9hv!p_jqE*$hrP2nnAeUV$3U$4SQjRSVvMx(;P&`_ z%;eU<(^5(-fdRl~tYyc4|I0OuF?@#K{b~(d&73fpS&BT*IBZ{QjITc441+aBu>8gX zHXE!V*0F-|+8y}%lP&n_lT9d%NX5DFF?fh&_~dtA;&aAkK4XCNF#`?)kB@%49$VKN zphB?=H~VKWT-gNOueLJ=vH`jq4G`=ff{C_H9Bk{t-O(95XV*x(7hbCCL2^J01qo;+ zk&tR+aX#n|0J3uH00Gdl=q!N)os!*_-wEg5N%3(?m2172;tvWS6luk?(Nl6IPlzS( z<|jGXxnpp0L+BiyDa=?!);SAX7Mv&d4N)~FIjuPnW`%0cY0Zzn3IW8-!WNaOmAKim zAJ1D@p~B%AEDcO!X?PY3wIeti(SjC_eAsSxfVr_9`m5XUZR;?m(<|U(EyouOI7s1> zkh1@X$=Z)M!c*jq3C0qtLQ-M;r3q{{iIDFRhrWaojAm40D5Vk=KFJX2T0{5Kt?0}w z!R6^m_(N*MqyO7Y53zu{gCC zPlxy58Dmo~`x%IIPGOpjBe$1pA(4=?7Xvq$P<;TQSo8M=K=_&FWCHxx@Beo6#;4{l z`$blmX%aFE>36bC^PkBo0T6vvm3P$;ijI4SvL75JEL;RA6LQ$UMt)TbKtW+{;BIK$k?9)oN(bE9tx z`_rls%b1DodPA)HWFyuwrT@hrzGi;M^%Z zymSr^FPz5w^gg7@qp)qQ0m^q};M##bD2_^G=NmAe%Ok z0nV0mB8fTM39yWH&C!@rfQ6wcJnk6B(-y|0nkMnQdK}mDm?8>Df`z#)%*-W_IJm-0 zWcPyt2ru5AglZu1OK+&A|J43H0}ysy0)GHK`xOp!Qn)-jbV+p^H=2u9$EyiIJp95n z7Vwc-P^oG>nsci9dFzgc0h~xK@^7gam+~_>xkC|Z&uPt%zX|~)*?T9t(rR!#yBmwS zQ+QZ3fqNCBxKlEO`SdOvR8*nSGzC6i%V4y{8r21itxO-r`I0U)hh)Oh$Qim{8sh6U z+n7^(J7Wa;u-I;c0>51tVywc)Mh+)E2aFXr;PLTO_{;pC@Ru9c@aoD{JiT}xs8u0`3j&!~O9CxH&R|L}?g|bWKs6m5WQ0 z<7mq+f|tYtR))4P+G-84fjz=)yfG41gp&yk=#R{Y-BvNS{&6ePH~ZnVPc0s$4q_=~ z2#=G8@HBoHx8oRlif_PFaxG4zwcvD48;T=SVetbp5Xz|LMX6BuBR}~~bvUOB$Km`Q z00;@~11-qm_R-|7Gms&dPjYn1Cjrkb}31$9tcejo$SbY(&L zIJ|^OdS@I{Rx#gvE^?svs?IZ`YZA1lcSPnpb?3C^$6tj2LeWdb@G5U*gg=Tu@Ukx6tZntW4{?i_{`r%+V* zB%(DW1DUc=*liO*u|taK{8~I5n8nrFemv-$#0v%g0Scsc}Gi(eb zsL#y9!PX887F3`xJOL?+2o(9mVu*ppctkPEq%rW+cY>a-8QizHpiw^-m;9RWAa($c zqKB{)IgBTfLs*O)z`{=UTlx@gW%pq?z8DG@1tm#p1VE^~GfyZgg&diA`~rGhcEO?2 zG_fh7W`(_(V$G=>_x~Dza3Y=55Dr`8NM=#Bpj17-E#HTvNdV#BuXX{%FC_N+IK#Q+ zNhs0rU6m+}MP^xAG^t_AqWW9hl0TeeJ_?mpk-e|}oYwsKs^`voSI*tpN{U|ZVw?|S zNccZ$dGyPcp~+;UjpscRv;n9m3 zv|uq}2#-P-3ke#;qksWC2L(}dH3?Kl_Gfnx#9n37eZ!6Fl7P8pc09mJ86 zPK-tsBgZiup~iCf>$$>rn-hE)Sh#Kw!})75yf!-{Q51lYQ7oQj za_aw+lSlE09lt&{gW=>#B-#YCzGRSXwS(&>F+$Dd2r-f&Y=<1NR^BM{j7LjC2|Akx z(39MRM#Cf=ajd~zzkVzz`titn0E<2hK!OKxKB5CHA^8Z9`V-UA`kv^-N@(S*+A*%W zu9mg{BtsZ6CdkwH59cD4gP;%hos-*krsOKCJ*BZd(bc(MEddDaW1&{1T6My4DU5n$ z_IWL*P~?h&g5(cSw(p6G@*o&QW38R|)7Tgm4((^ZvtxAp=po#mn8jRa z3#Q{M(G!}3dd6xRyc5tLl7sz8)tJj@z{!G6OjPw?xN96;QMIVhW6pZZLR@fd#k_k5 zE_t-#lvfjGe5=stmxDO>2-u2T$Z4%L0HGBdm9#LmwpTiifvK36)W!9UP$HeS0ED7V zGz%aqa|1|tg(&G7xot@?YY0H-e*f12gvz^cm79^dM4f(!?Q*K@wyH`Rq=Z=&`6!&W zoN#v)EL*VhCzGxQdQQcX&C*y{a+cT7^j|{jI=|Ln-P^ z(@|g=kFKOf9PgUKnX+zNE$hK|)y&*g-;ZmI4Sd(aq;&}2wh!ZceHV_^wc%`cFD?%b z;Z#=_rmJhwo|%UPMG!)rJdqa`i@v-f>}_bmg@Hkw?dieM#%9dbG~nB=LEP*a#f^?( zTx%V`^`-&bXk?Ck1{?<}deGi9h`xp)R6As#+AtCAR+(tCXCJR~Dl%-N5oH$yFNr6_ zA}1J_i1<+z8UYYKSkUAmQ&m!CF@ho!-dU*!K zH4Y&B{U+#naM~(2{n0zq6o9C{U+n@2&237s6s-1hO4NP!RJNRZkM^z#i1-zFX`U-{ zkW(_D3R9L~3tn#7bWVN=hsmowr!_ym`g7;~@PUvGPa<|jpkp9%WyxswC_=w;8T#yt z(Jd)JjY%5v4dPKL%f@)!5M~*0oG9(WrHXD4fZXmH#=W62JYC=jELK=w~*f_~)9}C*VNR!vOI`8*`(4vo?F`3|~k^}h+$O%t1sP+lqr3KBovs%qb`byAadMPA)IUK#L>zo94N2H;o25l>gmUg@d@0XoW|{mDV%HT zVaMulxS}4^_PgFe+#4Cg{n2sU|3B@W18`(p{(w{XV~bnXmbDsB<95kFAShS@g_a97$acbvOOqo?{aWLYT`3WXBL7lu(tq$;zAp zDYO^ja%YoYVemF0b2S=r~+wF3#S ztsTbU)%8PO5!)m$iQ`^a+!Yeq;i1?TH%GTv7uW0`U7&k(nXZW?8ag95GjgOL*4_(?u96$*ty3FOijEs)a&IS^DP1En3rDn{w{w$Pm2 zOr|RjW0F$_aCBxD4o?y3)vct|b(7|gP}V(3`*4g4lS^z)uW{API=4-4aQDmx4+tKf z+2oO^5RBkKA({sS_sni`r{JdOCYOujSA;Z1N0$iq%~IYuK|$9P1)Y=RH??DTd;aa~ zLM}TsHy3PSo^vhoihGT1S05JbV+yzw3WY)mPVsM52&@wJeOSR8SJcRoy{qb1!MWeo`BH zu$6^`fW$H7Va%l=G$En0+4Y>tsKzSxXARGjA6>;W9;0k{hU&@y)$Vh|8Y6TyccM>9 ze*am8@T4?yH81(5Y;^h$s@7(T1Toh_NIj2%<@ZFwP3e=!$$8Dq7L1JH)b|GwyP- zn|laHSLj&WqGEoXij^%IqZ=&r%roW)b6w{$v7smhMdh+@tAG1)IwQn)>I0uyp-?Ck zcDEPl4C#)9%KG4QV&!+T(aBdW5Kl!j)l)0fFKpAZalp*QEuw>SoU4*qT0uIC=P}lIp*8kktm))TdL_jrem3ToxOw-0>o#}ji!ReR zw?gyeGQ*Vvj2Qx4TN&k%u>-CdTqbnRPvR$P)T2-+6bk7I=?v+P1kLP9j@#tRys0ez za5yumK37jyxQ9?<3ndjzRGc5?!u&edZS8UQ_8vDxmpLDZ;3y8D2|-xeN3aRZ0V$9k z%ne;63QSGiB+6~UehfA3oXV;q(N@4}|Ac70zR&ZQAMwoPhg`q1$#86i_Q*8#`f>)( zG;!n|<*Bwk9*Z1ub^kKu-m*{7Rf$5OP$=TLR>mZ!GeM^JyAI!aH#4{FOSz%U7RqpC zGhEZj)va?pGPuWezBzimK^mI6SQY|#cxRXAj}Ccgd7Jt6alH8fbotHbO9B!Ml`Uws z9cWc;=&RqyDAtWtt(@^xbIg=YMQ#mu&MxxY-X1SqcF2o@ht_w9%`Q+b1XKBwJR({n z7fT{c`vz#ptEH;222Xn4$CLpn6bgkRGp`Kk3h9i5cyhY4^Q2J*#$#VfAk}F$(j8vP zQcGy}HZon*$%s2h{poVDlZ)vJ^m5yUB_3PY;QpxUpKzCnZKa%Y6miVx;gl|&f!Y>!TKc$edXc*(Vmv&*%FTl@{B=Q0#$>E# zounJnNwsHTwz<)1c6272L^U2%C=?%qDm<5tkgkxKQg%Wk+}<_E4ZcYhOS>sdF66A<#m~ht$8>2NH)L{L zh~k*ZBj#Bg6YIzH=>j*$&e$>NQ1RLt+5b+?ts_5AiYpqw^oNnbJieT4Nu$r}W3PCC`OFr2 zosHyLaz&uGaa?Z}SUGXl%5lL7olW52xR8ciJ}IxGvtSY;@+FtkY%XRX-AB8#nlWz( zYoaTDug`!&p-?EK6Qmm^V(Um(Bz#aZ*;0Nk8;A(rt88ld$_kcc8A*zkhTndU21_v> zlLx!W{hwb7LKt!$=im3qV>M$kW`h%}<|56MfhW=P8MCGo3WY);9UxsGoghFuBH^Qw zJtf^B$E1iZ?*6iObXIl;E-&+=Rzow&musDLZMJ(hBD74lo{Fjo<8&;BQ|E3Wb8t%<}G~5BPgkC@u+*F zDKY93B#uvI5Da-5P@F{pCH>+vg0rSOOk(*asW-fOQ_ zRjcY(!&-YEl^=0jV;|ma{ZI026M4||b?@o#-ZK41RNG~1AI$onaa!6}oBph(v(sPG z(NVkJ+Kn$_F@oPWx=}iB@gHq}`u#e;FTYOXOqctZep~yS-&=eC+}rwCji%K^>$=hG zG){jzA56&ju0+@1vt7|~tuxU0?))dsuq~ zax~66TVsyKH3|BoAMhhV_Yh$F++;QHC%d-JbRT|=K^s5)d}nF!xYh=C`DqHrb;EoPtt*a(90t)K1>Lu{g1Iwe;Y%== z&3&py+ct*Q^FfQUibEgGH2Lh!E$n$wUpfp%}kv=N`ebK!j7qN~;Cr;qR3LG}Fy?{lZG z%61Ojbn==Xe0c94ioJRI_#h>4Wl*;SJJ&Ps&ZU-mPyrtO@MnOg8?rbS;o{60jB^l@ z6#$daK^urr0MlqrZ9DXkqo|Il5+GzlQUKm*Xa2JdKuuH%StMfvAFFUAPy}gUY^i~x zj{@MurZ&oXj9qwiay5N&cY3nUPNV?$?6V_P$YcAmU4Ac4jXx8dk=cciL^nXSar*(N z!*~dU!S4VCc(5SPg(Gutbs%>WGgi<9P#M4=;cEzp97Hti9)_%irU10$&3W+dhX3jO z1u!sYu+lmGa4gUzTk^MxcAC>l{V=GtuMX@ER**KgccM);9$NYoP3;Jm5?~u(KN^Hg zm_H>5_6VfdgW@#t6FQbu4c(bJ6cYxaaA9sKux$hI69DA=l3aA-`Oyi@lZ4ZQ4qM37 zX0*8T7|q}EQ>)dBHv#`@e&U*c`tY;YX4mKddkW0jbb>Kf%7{>cTn3IlIXaWEYO$!| z-Ce8ABLY@bWfL*X9m`7*NPv#O1dm!_BQVCi+eR=KhV3pzh0Rg|V>yCxW!2fb%9d}j|(Ugy18DYto4~u?{ zL}w`j3Fn7vE05-r3Jv-qYA+769LSXNb0f5vaJaY7< zIaoToxF0&=O6B@npuE%Vj%mj47|yG;AB-A0Tc3<8U+sz*Q0g-2P=@{{V-Sk0y3TrV~MAW(vbxp7O)BeD0&y-*KwU zjsp1C{68Ol(FFFdnr_-Hve3emE1%QC5y>DSryP^bnPhLk$>E#09xDPG z=(>Y?$bI2-EXt0`!A3^V3^j-~I!KOxqZcTp#<&A)p>(NM1C17-CMB-$c!>NM7jBg7 zpSYDYe~rn3^&o;xT{lci5lL7p^x7(&a2y?DTJEA!VAaT4NqUm1Sh53$lr(g=qe6ZV zOUyyOG-C!m_PI$jF804E_#x<9Turs}`*)i@yz*mT{K1R3Q2>66>?h)06mCb;m;}m* zh);DfuupE@F}}ESNUvs$v_^5a$PDBQ=xZ9Yl5}DMuxM2hm3>DqBnZ4@4qGX@I{QjmOeV<5hQ$1&g)M``&8UO($v=$e+a&|H%bPX$Y z2j}J=r4=LnTVg>tKm602Bdr?c&Uc7l8W!{iamGfCxdBv5lqXgMGsTSgdB_uM-$K}OAPy$Ldxhl*9L^_-}x-|ud;Mv;CE4Xw*-(n*G|=*Jz9EU3rbgtWKxo$`Zc!S}#THQtE{(k2Ytu zZ3h>W5sXSNO7(m+k*FtDAhc5Z4N$N|?-D`>y)ll}lC80NFa&oOqeSk@qOA6na{#riE^j&K;wIV6t#zKtK`D8k-!$R4*AUHawwe7 zvjz#?CwTVKs#d)K-vwn85HuELBVr679L`gQ2VHv*FeV{^dbSWI8y5(PVC6}abO!9G zLz^T4dbYTkW7M%`3PpXW%qElQuXIC3l(FQfwor;f$DBr;w}9O1m9*J$#)KnjZy$5i z{PGT6lri$jw@>;;g`HphzEj#R>2ez8NNBHX(4Y2nPxuOKH_f>Mr zU7c88EfEf4GTIiPVA#Aa>5@zYjiLb|X=RNt(Uuw4;&O`(m+3)-(ROzTN*3mrr4fq^ zun)X}NO?#nUMX4-t2p85cSM@$?Pp*gpRGI3L|4Z(tynll(&Tk1QSTG=-?#!NAqYa< zxvjm#VsvqQN~hr8k%d?pjjLFmMXU$^u(VkB8oES;j&p^D)_Ap2Ahm-%61TCQlo6^Z zB7P;X#~grlr0IUYl){)wrc2O=nLL9}zX3c1_`pa>&+Dzl386EFisfWl2u+7>x=Muq zA4uZeWj{2qX5axr(upyZ?kXW4Vm~sq4fb2t=ujL83Mo2{kp>T>Z+;e2QM#uyQ3|bd zjP)}TA31G`i!Fq(UMJZ5;8|)B=&A`2K}&8hCy->!7rYD(R&(Bi_jz}ojaU(s!v06b zk@n_{r51}+zvLx2)!lyhLwR$*+@kyG!dE;};yW|aCeSy@+MiobwiQw;k9dr!h<+f1 z2)qPffQTRWjnIy_hD9MO`SN_*%0M8z1Uq0!DGDB-QYL^x>b^B}1!BYm~o0efT=AwRZXu;R)6kHb&$OG9nzv6kGFqBqQ>ipPGH7SrgsK zH4GEctMNHF#F&#+W+gdEvy1~O9{dNksZiR6B@}S7Kp3ZQkzs@!qa9`L9^I2;qdBu| z4`Zw2fv2GcLJ0z7WEHl${jW_!!x#O9*8_b}g5(iWN?-w>3>>U;l70Hs%%(CKn8`E7 zn_bI{B%Cz^0vuG%lPHbtB1=K}yn7Lk=r0*zEiuXe*y&C`vzkgd$42C_DKQ9MJ;DI| z_0L0h5&Z{-aQd757zeLb%Y9ksHmQ}HapOE|pj$b*qeVLjuyN-&0x$Cmqr?p1E5hZ_ zJ}j_h2O{(b(Uw603a{=RN~ppLVx|QM73w~xjRj-n znkknHpoqkPGUEbaV@Rdix6%~Vs6Dk(A&-bWONN#o6!SuhQK3lCBPg{+wl1n0R=k9| z6-4wn<1&E%b0uGCrfirWr%RY#qHgn$frr2+O9YZ$`XJO}z0*)fj>ZRUSjr~=U=X>? zE|_}*{h7AajX%19AIy;p8z?0nk!nPyLqEe+auNu3z=c~$Dy?~!HF zHDsFfjRybA?RT6y$x$D*%5((XbA(|4C8iMVh&v}v{enqESagqixd^U(A=JQ1EfksW za>(IkDv8OY%-n;rWhsgUhDA&VIFCF5coy=9WRRBmy%ebuuWrmnldffPvk=bNi_AdW z*ky`^%G7AfKWPo9j3NYtz7o*vM$Eyy;+mrW>b0o$jz|op7Nni#au|xJDKnWx|0AB! z)UAXZFSpEV)&u53A0;^jDrVH6#*5Tu?EEcdljkzSMMp+iAes#_td#>Z>K7q2*NxC0 z3`@#3fGJitRdwdEF_PDg!F9c{uHl@e7*5_Uwoo&ixhg4wtc z18@dOdY^3gyb9qJ^+nS=abBG7q} z-IWD2l0uY7VldQ?04h;NwO9lL*bg!DJXxYf*5G|CV}ciZw;8l)r+pnT&C6jk>%*(?$OqW zJn92osmO+q9#BGmQkP>X<87S(QVvIW|8(!<-Xcj2=r4Ek%tu@WQ;P! zmU&R<>Iy{Dhv>~{d zTiXyuH?9@gKWC&7!FXFmup8LRN;!Isxels_e9{p#-O;^Qgu~=vv!ifd;oD(B@W>8e zs<8@9;2sgXG7rIEWGTgPJrk$_ZAKJ*yb{46a?k>2l0YRnvkr3NE+F~#T`D|3&bG0I z8&8h+;vfdFm!%N-t4VH`(sXL`rifOQkWND3ML3EaLexw*+0i6$SqNCzZ%65il-(V6 znr$^+c=O611l4DR!bf2kNxCd)Z){A+i|CLBV{bcGkYa6gxzm z(kN##W4atwhdfGjKjzagf8Nj*6hIGQ|6<9jg*gj5KVmlRU$g(XUuO0n{i4$dB;2g~ z`Lut+IqCpIpb`K_38u6<*vRk;U1%)2+5Ry|1i-aw5sQ4|l%}Un8pHm>X~G|M1N;ws&+%Qwisr2E1k`odN6YR3hNfDR_)E4)qb8w+W zQ=%iB<~U_(6|N|=kr+*>JJY^(6<}k)$Bn=vQNlw*Bbrf)w0sbVl~U&YY6Sq?1K?ZG zU||U+%mHn!^+85-DBMa4!j*_ffT$QgJ(^?Y{>Li1sss15gK+xSfjIyn}0OSFHjR%5r3|V!W?kC4Eag z*f#G%68C6i=!7ZbQVzM(A$sVJhsZa-`b`y54xcBK z6#DV)8^5JT?DzmVV#kB@k#~Eroc)YHl5gGk&3^68FL|RpXvh2ae@}nl@$#P6T&T>8 z3nBe|ZffWdH9h6R$Pg2(Wiq@d&g0Ac|D1}y6U z0yoY?PM^*!89LD^v3X*_`XC|+BIH~wk=c_75+tY75hYV3SD8b^Mwa9%g%XQ#q+4#y zGRTn8DyhnX&1}jeD2t#5d@bZ2%!PlnC}U80$j4dQzffXk=_3bV60lo>q)|yJ8OZrV zIVV(r&O;g>LN?hyYriNIxW5Y*T@m{__X1&a34@_LWvnMY@VDg8CxHLfjo%UyhSSj^JFBId%@AObcB75D-p5$uL)-?6 zvZtBT1}MYGjPK-Z2B>64FDTQLO(Go}e-csrgK!-Y4sZ#0HAS;Pszr$*N^$OZO6etN zNhdpuSy|r1vwF6!$(z7A7Y7kE1|33CZ{Y2mlNba3Sx_I>xIu(dc#k20uXD73S*?pq z&4JHIcTyJfOe5uo;R8TYMwtRx#lLYj=Qrj#7p=u@Y<5>pN0*~}zCPF03#a^r9gqQG zL^hTE(hhPik%d~wS#rfYoU5j7kJ6gX>>o*&@$6d2H>Bcpkp1U1H4Z*3vRaFKw9=o_ zR)LPVbt{wLk{CRxa8K36P=#S+C@K-SuT+@;B4Q}Dj>v6QVzj6e#n!cP&xNmkr~ciw zUzYjN1o@H^&MZJr7uBflNgmA?^fKb$@ zDIu>cHHgn~j4g{DD9Njk@klsUfM=ym7QO?h<+JC}6S`F)$0p&`%8mk1Ta;R?M7%{& zOIF;vT${2S!Vow@j};a+M5#(gCv~y*6Pb&kY&1_iaI+Pjqr(^pCW7Raal|rnTM!og z73VQN?GSYOIZK8;VDY!)c?rX4k%FCIJ9p^?9}J5A!~Xw4p8oq!wL9;!lm53ae0hra zza0eq#>tP9BaS(ubGzr(yN2f~j3XMG#@T7No#fohU)P&`{;JQ(o6r2S-fAlK$j|Tp zzssvGdwrj@*F5*-+Q7aYm!E&JJo)q!gt8jQP7_rF$-0I2_vf!MVN!HlK%@-^7&`+b z=@d%Waf8ehzyD~5K{=tPv36ocXcINZ-YG9&C_o^hX%nHy8go?W7{6`eGGJrS%r65V zbXgBCjCjCJ)1L-L5x!)!9BqN&66hpSqa<}a8y8g2pG7aldKRXnkfrZL9Hc46rCyB3 zm1X^&P1V6~6%4_!!D2Whi>O?BsR&>?oyS%hW0jbEf>KFZk} zkC#2BeisFdwLl|YwcKoAXy6oZqMgeha2Xpc!T}X_Tn>1cFLjG`u;?Q@C|TI-_>m-~ zk|xvL=bM&Ntkbz_fS)&TPDO<=-a#PvbtE5EqQ*A;rvW>T=lrMt!wx+Bz)t5>#tE|T zy7jJd>YqG2BQ*xBiU9yc+iBOG^}xgL*DoCSOTQo=JpYmj>_-j1Mf&$W;^@xrQ_gt0 zyzp_)?dSV-<(diR&L<}|S{(Zg%D3jo)ZCy{iRVm>g*7`tJQY!>RHzXF030nH=l~=J zYRM>XINUKcp?**Tn~jH#p&FQcGpQ)9k+O#}1J<&y@im-+N4x8YrZ8R%=a>az6^LtM zT;pL|^7$T&B69%$@m&|Pb@EoE!)7^EV|R|G%w54OvU#O(X&xo1xmgcI z6NBt2C=lepA#a7osu2ifL>d;?<}04P!S@5bp#EHcLgkENYn8d~jM)tN zNI2vK-9nAl^~|(?qxB!MItTBxfA3tulnhRCJ}iGNET?|xrMl%gZHO`yP9?{4MU5sy zC7Yz%^_7rxB}ousL@{{)KXO^FH7c@9>pXeim_kE;19t`nQhK&*i^#DYx8HJmw7v7L zyKEjvW@&1`sVnGe<=qcpD}bStd5{evHAdt)uG2b1qGy$gNjd=VW;U;i(0yY=2U6%- z3DNjOX)W$WB<9B83v9!#!1MU*eF4zNpi0y1QA? zky*Z~l&QhsfTHWM`^uzBSt~(IRw*ChJcEkSUn43L{=-*7KXY2rI?~zH!|UE~4oQU6 zFHw`%+}deM-cOLK>XW+>US)_l!AL((6K_g*J&IdrkvTSHPMf7}kMR z0vi-Hc&;#z-hd{w31V$2Rj)PXxk|M$9(mOQfE)Htfkj+T8loQ~dLeIi%DPGYM0n1J z?yM|oqb$;C=U2|lL~raPTskD-yWG%iHa zEz;Mgo$=IY@}`%(Nra<=@!7bG`Bd_UBcE_od_7=_{KLB~fITlhKOPnQH>ZmDogQFxT|3hR3Vn&Rl?cA5s^^~8FN8oiq6Q;QsC)? zd~Sp@B^M4NN#JutotB{$^XI^9Q6BRkUKor_LT#32AQIldOHG>d4Edl=qEh@^mVn5_ zA!2v#%B3#Mtp-zt44H{6$Prihjct|748q`K49@C70urbwj|AukjXqvx%PxcR`?>BJ zGQjYc;|XVDJjRev1~7zVY1NXBR=M^c(evN|`oKN;yoHRkl-eK%2-KV*bLs;Iw3hCF1-HIqwi*jvJXt*rn!6om>ySJIjS2t^<_AB%FcX z?}+=!{eJ0w{l0JC^lf|0y&to|DmGcl=HneVe8BeTwSPfG*lAcV!xk=Yn%he$^cmMu zU2{dFexZp9kfOI!_IZn}M5s(d=ODW}$IiW}gh#KTGl3eg3ia6b3iwOTd$WB1=G*i^ zPk4;nW$Qf>{Jjgq99cm*vI}+w$)CBOkpK|!JWPY3^n{{o*7B~N#6J|>;yVoAi79C) zrS4I?)--M@Mn)1s|8O=YWPQ=Er1SXfrI*T$*Ilps9CoPO=g`9>EP#p&6m@pFreBD$ zHRmcwF`uvgm+HJ)Wv2k%1QxC}GnhrRR_o@_e{Qc_bBDtpR&9xZm{aGICc3xOk~L*t zQGX^=QUHTX&V;~J1otIf0zkw0aIh8{5*bi}P?$#Q9)V!mvX?>7H?sMWwHWIc>U;tz z#D;6zVE+=D=6|!4Vg`ZMc~eM^0fm?miLv&p*X8ip-O=QE!gd1D#H;(Fs*DX zR>~|;L6zHYzE!?Im63oRUM1C@zZ0wpggGPAUC)Mwah+=}YCk=ln^~ z^TLbXFZ)iwpAiC_hNm!fasXJNM>cOTV4w@=`?uVpcieJIKY!<4cAd(PyUM~gMhxd5 z1BRCotdM-hRAv_E-Vh*^^O!R39enreUn68y+5H}S3C(E&H0TEtwpb41I zjQT?c`D5Pl8aHP!6H%#)KM(nh7hK|Z9x{tH*frw?73ynoIpQ6MfmcsJ-&6*HzQ0L zzj74DXnStG+oCTJ>}$n@|+owWt2!|Eas#&Xya#&&` zR1`t)h%Q4rBp@x3&&xALbCZBNau`cs0=dpj(D91rK1;6s#7FC#)A1)4UTWJOaF{~R z&`S-TivuIDA6;^k+&;7?%vY_t;Xjf@(X@QwQy-OYfBnX;_xC;MFuChJ_BLdEV-0fH zX#4Q7D=w0AUhoXr-f`ry$IA=Oexn<<74U{bS-bs5b$~L39%5v+shKcZV0Xk+eq@AC za|hRe=+EsCp3uV{N&&s|_ulYMfr6cMx`yT!adf^QhSw_mG z<_tYk$-uCSYfW1*!Il3QNM{ZpB3@hj5v0jiuKini#R;d&`(E>(u6O(GWo7@xN_x+E0h+OYLzWY?pxz7;~7@R4;VD3kf97mRdd~L-8 z0Ez>UF3Xp$x?0}&jOX-|M;!mS$yppa0~S|`exSaNDvoW3CU_SJJ!Z+1DXwYHeo_!E z)9lgI`H7SchwXFdA^Mq1F700$aR1VAj~^G(=)_GX=AuV#Q?&lNQ%>n^@3Li!{Nf`X zsT4N2%wTkp1=wZeZ z6r6?uE8zp?$bTl2ovcb`Sa$h)&)4Z+zyEpXyg~N7`_{ak4lJ1bk#uW>IMCWfMgqqg zSrhEvOTeP(z`U*$PQi6>xVt`uYuzCRqx4>QdzwasGeAHY80Jx{{&PJ3+aBS4BJhba zLWU7Y#YW5K#Y-wDRnZJGNVAG-$DaHI+8LzsovCbc#rf~+-x~10{mk>^=`VXeyXU^n zfUpZR=KIKp=$#&Mhy(=osf+Bxi)=@q^jmV!F%O+e2DeS%bC2d=dQa;t$Mv!Af7HX| zE_>aBWTo=yi!Pt;yA|Z8qUIXS5SoRvmq;;ZGi??N(Zx@Q5sEu_F2MreVjO`)PB(%*& z@$Yb$NCodg3h9Hw>*sR?@bnf=kOB{Z0wi0j8gp>1>ILMO~gnQJc5_LJZ>?yL}OHg|qO2l>k(ZeH8ekx>u zrt*h3er<}>KM}HlVq{8OqZO3$WC7V$K|<+ShUh&(nh!)Nh^iPW28c)+t5xVIupVF= z5v3cFl9!sBtWH3`*Yw{liJYWAQWnq!fx>JmWBgn!Q_aaSECbs{zQwBCJi5?qHzG9+ z02mSo@9Q@kcaORA_kMX4V<^pj^0G_%jScJ{aO6?)U*Gw5x&GrHmD_K*S$5iWH+L6C zObdb#B|-t~sVj51+UQ4*m{ zNYW{2WX);spfXCn#*S&bV3rv-O~>H#z6Tv42Oe}psB0VO3!lD9PJPmmbbII$**~!y zhNz?@kug=USLnuUBZQ!B+odD%dIU0bnG+-qexL;oLLtbOO#a$FzY#>V2cmwGqpbtH z;0=G!p1|$z}1@zM7F^Ul|=Tzf;m^6jsFLw|1n zdk5X$z44nPTAV)bvG+Zq_R7xWJG8*scHOv)d$xJUEw|b2H{YT{UNtGwu}eplyt-!8 zO@}Q{gzdQE${x(^x$U*8u1-Q+M2aOTX5~n#O5hH9}F5|NCoS@3P%-*G9vxU0zcurne8+ zhl6t$w)>VX1S|=hTWRPsmtHF0 zneP9&{q~c+_uIeS1DisWru9aNG9N_PH!cL&sziD;^D$8K>#rVQ`E&bilb^fKwrKFx zFI_MDJ@ByNoSJkdN1%7~h*~EZPYcdF?*iYbFrRjOcLHLFVCP+SwL4FXl4FXgywgfz zQ(qsy{E`sZ?o%Xuz`=*JofbWCO-3B1MI_Y}oyBy>E0hA$f60IU(zW{K8?Np8w9~}P zo>K;KuYLAU2Q*wpzyCYmxCv2Y@9_3pZ;p{~zwMUpe1`33sJ+uzj3p;oDHfs__Fw%L z*RwF^?tsYoZEzNg%)10RI%@cFBI;N$1khy>`9kF8?Ik8;`p7R#`$>)l_J4Z$>uirL zeaofYb?>`PzP@1}43#07G~1CNgC0pQ

up(ZpZ+{;-v`J2ANc;?D(a8H%=F=e zeSbN+d*2;j9<;gQyy>);=!egLk8JOF_w8R9-DuL8;GyYY<~ORKtXGT&&p>w(> zlb2s2`?c@naNF_sU%F1tcKQ}>PPHLa0zI{XfrIN71p9Wh^Ur+LA?+{6bU%^i7jri*vd{F$5Kw8cjwqc8 zGT)(!ZvW1cmY`cD-{(K00s&v%l`XK*N-f6 zYJc1LQqF-Tfia!4|VHPiY{Tl>r1;X+D3F13#woavwUw__d(V~@?E`9y$`?t2c z=Mm3%hRlq>?%c{qi12J(0-~=@bnm@wTaV;hS>?NvvwGK`yi`8*!2yO1^dI}&({w5g zKzPdY0T2u)M!~*|S7`l|YUk2x^*CBNr`*X)NY@g z)N5bzytwCp2OKVse#(h$hfOQl*z3-Go_>2O&9rtede-CR{EvK5VE+%FBK7+oba)uf zN3XajoW@}fIl5;C>=s&^d+oDLP^PtrT*S0cHK-53hV6(}n~8bGf^F1Yl{3+-;%>kY zFVQqP?XU>E9PC48SP?c6UYzuiz=LfUt^S1v2*2jaYvk3Zy|SPC=tWnSzT4X~5xHFU zwu|&F6X3UFkL4cx&PO^W;Cg_$F-ozRqg+*@;Tk2r+!tptSzo#C|LmaOjz^vJBs=c( zlS|plmT6Fw&)lQihLTYU5)L~0VeKC;iZ#I3l%MuYE$79f1EF`TxAQBRMI0rp)dJGf zeZMdP{x3|Rj?&)Woc|65;P1WdKJw_(e^-1>g!>ajFP3%X9#beU;vv8gi>)SZU|l{t z?y{>kP;XKE|26%CguZvQyE7A_-*{hKJWSARAv#fjrX5ay}9$WwsG29-eyzT zr=QzgSp9yl%B!V$FV)Av5-E`vThEem+|D_$$7duivwSDqsWA9-{i@9nQVJH|Nc9T$Th z3%s8GUh%HCN1KDbFY2(m;ZFZr6A3Ie)_)uL5In+9(8EjU*C7pv=YvJC#TTbJp9+5 zkW(+%4d*7n=9Zt$I1J8`F= zvy^FE8bac2#Z=OP-6Ot=$DX;5>#q8A|99<`<1fIzMfSgX@{{_VYl|P{knN>HN`ZiE z-)&OBjz_tzz0N>qd}Rb5wjT`Petyn??)5a?MjNUx3jKJD>)!SWjYu@I8YAyH#=sBG z8&S3?^?*R8vQGcoK))Sr!L~*CTet1k_rkYh`rf;`(IfwM?M}OlNE_+BRfDs6BsJp; zdkDRbPRwNs2v4zA5qgq4aFlP`dvYKEdvw_D+50~GMVs$U0A(yUpHIxMTRKT^T<{w( z0h|fQ7q9+Ul#%v3~t0UWvThk#{MdTjtvbyjc>Es_^j1}|P`V;KdidAk$v)xvgANYHy{Sb`32(Jhzx;KX#wmjkN~eJvT-A_t>&YpQ^!0yyYgHCEJk5OmD~eR;I#n z&waK9MN|@zd)+&oUMuaWQXB#_p4oqDHf3;;k?45UwL-b2ZoYATB6Vqh*OG8OoS`b~ zZQ(hC01;l>80(%TgD0@=4_;xR%CQ!z6seJ;KO1movX=;kf9O5j)wfdqq{j!}*0x|? z^3QsX*F5*-|6CLCS1b&#k8N4A=*=*hSyJeCtR*9l(MfmMIx}H;;e~%HyKlXFQM&h? z_cr;n)6VJdt~ve4C(D~|xS~ItWg7r8_o{2|S6%>kqit-%>pHPchukM*wOoueD#xeU zLmvU&R++&yNd)n<&Myrqd8aC}{b{m@07&0d7BQ12;wm#z(wZgi+_G&%zwMO{?fXsF zf3aU{06*3E`?VJFx3K~EEyBmEH-u$to2fSmr-d&<>kIRy)$qNn2}9?}9HupIzc?xs z2tbi`rnvAepxF17jVa22Vpy%kh5*k_K6zO#nEu?9O#qB{+)Mt~T51{C+O9I?YK{efmj>p^w}#h^-7Pfark=si zqV$Epw%qJEx`Du?gzD->j==vD&P~!HHFSp^NB_J+mL$1Yt?~Ro(mM2QMLP*|BM37z zKJG9CnY%x5LzD&t5q>!9L1%Ohk8biCCp}iqJ?&5B{}0EL&Ui*dKwX-v)c{Ko*G3&& z7`;6+L{#wJ&CA!RX$&d>#1_wEPkxfMH&?#>tn>QQuYWy7`j0yK$!@r-&43Osy-_(; z@cL#~ihh$xlmIs>bU6R~3a>=#88Z~Kd?<5_qec%<>9YR~V_HT!Hq3Cd!KKQYMI*~; zKrZUEZ*{~YmOQ@LSs27Nz10Kf0@u{r%i;gmvUDRk2Vz_PjAN!&S88`cRuNbz|ylOpV~ zRyijuMpLuR3&9KP*p#6Q>>o}B5bofNfH}Z;)`t;6M(d!!;H0P8hh~3}Bhe(tJaP~U zws!Cd31x!!&GJrY8^ns}kKJ>(h~GTB}!j9Hq4F5wAj!<4lkkxigty&rAbKs9Wcr-Z;dUQu^UC( zHN3yZvY;MdE3;Y7{VFBHu$g={KWK3v7IKQk2>c$e)Pq4nA<3MtOp_Jk6fxvoP+iz`JdMY&&Y&w#HVzY3F*`W}>ri&797C)?bF!j=lHa2Ax((MV&D| zH%2R90zCyUn@|a0?;t@q2Ktv+VHUcWT3c|e7|(VHl$-KT}swzvvR z!QI%Y^NA_K|Ls3Khm;*Y#N?tky-u!hXE80}_B&hM|GJM~CGVfE9s9Hsopx;L+@Na+ z7_j$X>V5u9f|4DBz|nXU@QQ>n@ZY#EZ|U`Uz>P*3#-=*I77F3NW6pKvT+uLk5}x=) zUmIv=?CR#Q=J)xC)$F*3rLaBY4j7ZO94Q-}Sg+w{E=ck>?pO;)TRgCp8+n(9KPmW;6@iH_RM<#fgJrw!P7?R>qW5>ws2=$jJbJCdj zuWp1Xpqe6@F5{JtmTL>illNY-)=`$0$mm>SxPJeFH(k)5vK7@5eV_WU!@FZ>uTB2` ztvB~l4bM^AIrR2doD=<=`tsLWD+Rsa<45&A+V;$YAAZzWfF=9iofc%TYddg!(OgLk zUuh3K`Qa~1i;y5#by|1YITyWer^&E^qIxmB@{rrLSAJ64OVe=df5bs?TWj}#V;&OMEu%AsJ@MM!tz!_s zM>}S|jWA1Qc#~;FXD0DvYCGDR!uP&*6ZjOgrD#sG$9gC(rcm-@M5^Hw=Hfp+k)AEyxsGtr;RO@?b<7U z=UHl%kN&P2$gJ&$f8U#4i*iSw=C+IGf>)&@+5drug@jOvOPOik5rm_^_F#Aqqaecg zoNGW@(6 z-+#??|F_z^^}(a1@~IY(3y53R>kCrWD_#nO{j>`^cq5cH#4-g_hDR&iA%e_>iFV>K z`#iRcx}8I3f6F;5-*r=4dF5rV@4Ijsx$i#j?fUN5To{e{{*NFP+uxnTh<%^K4wDoA^t_=?-5G3l&0PVx?pg@+c*<d^D{WG`n4O7=h7kFO7Y?Bj!__Mp{u?ljug%Hzg<{62o$j`@kpFYaL8_T0b! zFQ(GYQ=dhIiXDII3$(pDp&jk-Cq8tEZkvF;If=h|_XSbXX<+`4#~kl?6A^YqyN%BF z%H;p@=JAl$`=-Bt^_~mb3(E?g9P*H(aBU#ooI}e9e)srCbWq>(?py6U z-?&jfdPTmRu{}t&$KAI&T43Wr2;2@h=!ogc_??f-{>%CD(Z9a9WgKPfKc4XDKkRcr z%)u(25ty9c)OUM}=I1|kWqJ_G2Fuz}$ZGZT6iTzp2}4R-7adWYKP% z&fuBWGcvCn@?o=W4_j^T*fvG>r@rCr-t*e>9oDUc^V=^wUH<&EvxbwH9K>Iq_38yt zr@Y~%wtL%dh!Ti1J%PUbsAGP;u)fM2IF09hC(!=&)1M>nK5IPD+{!w5cnnAXj-W}L zmg)ZX7R*PV{(Ex%^ZuZ>Yj4uLV|MQy{>D@;%S4$$M2sc~4%t)Kh|ELhdYyh`B)shs z>Wv4p@YL+j?zJV$OdjU^1A+Ud<9mziD(r~Nh0tg^@iph!iy!r?-I4TrX%UnA$^fcN zaatkm&R84~thy~iky%+OA&PASRC?;s{nb}R{||WhQMf~64cl*?u0^}+uD&XirG z@4w-~_OpcmNSXg`Yva4Udf|!Beu2F0+|gI-cfJPDz8lsGrsMZs`g(cp6CT;4`}PLR z4~>Yv$o=Q{*OrCto>GIZB572LK&*V2~_H(!05*0P^ELSCi z5Z#%yI%^PKYAY*Co&sqC6UymNDu2AkH9zOMNup2}THXP3mqw}x{ zVoqpF05*AOlyM~XoRy&W-{**pBf1&E<8Zx9KFB4G`_|s17kna$D(hlV!)|x%wCm3F z3Wqv$`|*)`Zo9WU_=%6U{SLc-w{nke#Yj%SdeW2IJ8|Sqr=2x9jZX#0w) zBKsZwKqz_PhTo&};~e~YWz-5oWYd?&On|@jfcwbbyzO26#boh=D1J=9K8#ODo?cow z^5he2-y;s4vV%A3P1k<0yrRJGEe^;3+B`Gu^f~yTw#(70`CjV@)RlMZv`E|2%)V&b zuenBS^@p)CCC@Bic`z{vm90G+t|M<3!d>>-OJDT<_shFq_OcG{0Dk6Ohjn!IY1z;2 z{&Z~vNYI2}yv)!+bA-2R zq2%9QZ_r9K*rM5f_qP4k?YX~mQeWh4#{(XAwA^LX5k6Lx4hp?glw=XD>r+CKE2^BdTgnthA*4>;&>dD1B_kl~W++Op~|!%nv@Y9$r4e+l=rjxwb$~!7dL&ZCAYaKR@#?!e!(rb5=BHuQAFJ zw{63H?Rd;dkDrWv=YFO=k^6PZ%S4oDpu3Z41eTXdyrn z-hq+}0K(rT9(WpZji_b%{JopL(M8_C+zJimLUb!uN->p7xEfiA%N%{1NS53*){uf0 zlJ!!*m}g`0qzr@{NUqI5XFNz)XIklZmH>1a`-Z(E>c}eFe3Ap`oY2^%-~_C^EEnOg z*R7T2z!%0rK-pQt5kApZkTR6ngt7`tu-HUA0t{diyQ6^zoYm%<`}lKA=pn)v{2j zI-d`hCxVL1CN%=Lob%?`+PnC{u?Shj92j&Vf69)u=3X89TyVP0APu$@_C z4%oHwkaP;^kFkuMi>!+`%=^p_K5QKIg<&&jiF(NgVA;$3%R?_$5zf7H3yK1Pip>>75-|#21-}~O#N$lFw6Uq1&F8PD8LxtCcX4CN;o!? z1IF}BqFct=ijyIcs1jc?mtz7r-pFV(XAy0Ign@|+;^eryrK~xklH%~9ow$70J`*f2 zo6WN(E&iymMZv!Pf2E6uZw#q>)HFqBo=C_M7-=A_{<648{^NTZYNXplD3nb%0EbAN zqa7|$U?C99bql3?Hh@}e2_|raLM1J-JOW5*L3svpnJHOWN2xv9%z@E)(CKgeROZzZ zOaKf?Pm#|;O3^RCao7edTP?&fix@Gt3ErjbW-0m%Kmeizu+w2V`YH1lS|9(h&}U!i zwz3l`fK{WmzL;~39d~1%-P>yLm4T5diHBu|w&DE9iPXMAz~;_k1VCe+us$16y;L!e zhc~(c&Na{6(2X|5V39)7ajBN_cU)nxmtp2z9_y%E`);As6&M6NIVA;#2DkzagYKt@ z1~?Wj8~EKYAQoisU{u=|-<=bEHW}clSnsT4WG1s645AgpSka8_a6TRG{Nw#c1?;Iu zgHiOo*;XA|&=Agk6d+u|vn;?s@Wj+Be^))DuuuV=#XCwLvzdqG)jE~C5wQ`h!X&zvktCnD-kqDcnX-Mh}R97zAelMs5?K%H4mpFZTyGi9)OCddD4RLCf>UM`{iVYqkTjMj z+4RExok_wdVLKuiH_fp`cW}TBVv;e*=oHXt>~Ll&$@J&5?d501VT{l19XorDU$N@v zues`)8U4E8NTU4UUf$muUn6UgQf2|zdnA9r;lGGnHl&aa#-CpFX zlrFR2GfQ@UdkQSF0LBkAj7Z(F$Hkv@?IIT;TdSCWYowU!si*olgFEmYj#&hiUDe3k zGS1K(LWwV{nV&?s6wzM@!e%7`zL<_ZY4*}fUcjXRtN{?}Kk!1ClH$H~Xi#k4WeZFB zVo@#E5JaTF8v+E4QXKq5M0$*W0XV>J5`=^xhn_~XI{F{@WHzh6ct=Xc55Sr)Y$h=& zIjcer@n~tZR@ickb z3xB)Q+jjCicAsC9e~;seJFW=;1Ycw=?Z3ulo0P%4l;LFv*V_$_CJn)lESu=Eoe<;z zbo`sem<2!#fD;ir?1pHTq9p!A?P`TZFkQ^E=!{PIc|47~ia8FR5)R=sh08)BGIvm9 zuWrPCVwHx6XgtKX02E72ClCoxR*6!OyZg8k*Bz)5>j=;+g#i1d{u2yR7-6`?Y3ZM= zBufrKlg`i45R!|2#6;#mk+_(pOi4~es?!_5B1~@v$dz>7;jpv(MjI4Pt-9VX|2U z|HemJu7A^v1p`DSN=gwy4F)Ba~yi5B?A`nroEK6 z)P6P?13v*=Bth^Qy_R{i1YDWJ67(-zWt9hsh07Hp8UXHL;t+$9FE`N&_D%~sQ0S;K zn<_S!W3=`Y>aRgI$q-irr8Zyev!GK_0M%Y+vxW>wb5uoH#;Osp&Mbfp1yqIoJ8hQZ zQfFE1Vt}S=%GpZzHH*f@{?h?6w#4h%#oD>aEO9NNAx(GZ_I~ zXFXE9frt;IfbJlWitVI4)8Mv5Sh)j1G912 zA+^zL5j3dqlitNK3Zm8sJj7KrnvX}&mFyp&W^<#K5EaR#hM)ko(N2vlTRGz6Tih9# zM%1}9Qh->-??BI(k72gql#;9js;qN|uA;19^x%$IwPDca9?pt9GW!ye=|)4>f~t#! zL_ByDK$qStz|uhv`l|v*0o)-610BuLB@~?_aw(f8@NZ7V&-C2Y0 zGqH{KAW*6_J>@eN7pywU2{vDa@F6wiMhY?Na}nLvdmulRO!eZsu@Z>8W3xhi5U zqGss`;fvu}&N5Oe>Qb|hDi0TPC=2LF9#ckH-hq!&;-|JHx+~JjrZF-NMp>prqroqM z_xxHNKXS%G56Q(Xh%--NgIonzp?=PeEsa7YoGJ@phKmkzKtJo-F$YXq*iPg@OmwF5 zL+V9|8td!wq$6W-@WX-;m`3Erx~ZICg1XU6K1ow{tDCM1gjCC9vb>8F&FXoH2gzg; zt&z2bll4wE7$MHBhHk}t=8B5Isi#g@G(z+uwR}R(BO-SItHLlax0XF2m>cU~t5TLV zn&03nOFv{dXud*;GpsV{5uM#jxRM(0<@oxB>F%Fvz5y?!UgvX0>xJeU$lKK!AZJd!~~GHnG%#}md7EPmqXpj&21*JG96W(5#!Q{(Igu+LWHk|h3FjINZD)- z+}W>)l1-0jq(t!|no*>LJhkw^{YFMn#;AW~Hh1s<7BKDu2QWl-CQ){YsA;W3ed0Sq z8=_3~!aH$NSVKj^!y4A*0%)aHW>jjF)-T4wuX*W#u17l!TZ*nc%0(uK!W7e(b{gce z%~M~*5YaxLFC$P!UvBI=bNrg(@d{Ibk@v3%lhf4+1P0HZ3y2Qd>DGX*L(kLx2ER(X zr6hw8bCU##+;3!kVzkBqJAzYH_aR=UDfW-~PbWl~4|0yoR)G&PbE^Ha`9f^~$v|b( zq}~l`b}wf&-j#?4H|Ye2{flLOVFK3|_TSE}oH0>KH+Ltln?5P0?*Pwx+GwFq;i&#f`1pO3W5H-2G|9@F1t=rcVY0B zc?O(3&4s0`AP=H(L+Yv$${F-ambD1hqb3UM8wP5UI}U>Ekcr89z1Dd-sWn<#MAQ;H zbE1|i!~k>Pe^+YrEX3)hI4{*pFfTI?K~#C7h4C{xRX0-!l`a77K2ETRCk(jg3*ac6 zDcBqsb4#Ho2s$&)B<2Q>BuD?l=V(v$TKE?^)~I{?m~~=Z0*aXEy9om<8r} zF6FhBTg39kLMuad(K}YQ>cf ze0W~XN{-pFpdM{Jx*L>bbf+>(1UYf#aA1IB9bOg@6B;rZsly>eShy^u+<%+YjKk54 zA|1UF)QZ8uL*}BMw_9nQQ*&o#gfS{%XTh=0w??|Xil8izu0)Pu7BiF-*VCXiK~|GC zEmDVLX8SKenE~wG1}G}0@!`KkVE>W{8@pdG!?68JewIizl)=-A|8{~y&oS2{h|Tg0 zv#Ekb{Rk9N#f z_nRlZ)2~9PDRwClExyB>xhUE6#epga;Al62k});=_Fl}R`v54oS+gi@AbRoWiGXfg zbKH%&iG>UYBxG*LdFJR4ZgNb46=h{PXM9Zx_M(j^Pzpv1NJh}*P77L+1V))6 z?8ws(vj5RT#99(PB)%)Q`F4AIKSrmA8_BsEY?=99(JK|S%&+6Zdf2CsvrFSb z#AzPc#|*IJ5U&J8`y>d!ZmWg;Ys!g&LwJn94hhGXsx29`rTwp?(1t)EryS*Z{d{}3 z(chRY{eK`=5HFF^`!$aMtm9&(}D&@*}R8Jrzv}mm_QqX50LNMcR)p!QtJDWZbB`cK+1Xm;6nDlq5uJM7OT*Q;Ubdm zbE{-~l5`}9DdH;65G3(}geM%Z)}X|r@?rmgpmdhxWJHY0p6caOw*P7hQp-2NpJe3j z!UHg8OBnrVI&)7&Ze*^rG7}n`0*+z-X7AT){_(@R{g3}u|50KT7^*m5x!WZjhs+Z- zoE-OS0xd}a>54J9n}b0_$0!CAeh2p^b-uC>$RkTT?r1pSN4X>b8}~$NE^L{_=RncP zz16nn&LEtFBn-_y`_$)-91_w3M(dmdX0$p&b$Nu3(46+7`=i4F!6EC#cp=k1FXM;T zku|x@{r7E`KU>LJo(sf#PRHk68d;qyJsQi4E|~Y>B4W*!nf}Cg)qCU;i9)MEvC3`g~{t zuD}k(@RYGi(T|XC==jL=MHjLQyfNDl%S+g;m@plnZPpKcn9Y;(EG(boR9Wv+3{IQ; z(C)c9@MnJc=BdF))IrOHJSG4vjjqoQ(r!a73#x=V8>JecoxE!TCK(VSb(55OrdY*5 zB#e8=PfMpPd~AIy<4W$BF{xf(rFiE6E0uH}6rCx`A+iCgG6f6lV|w9ibUCHLyGSfT z%n}`o6g<>h(-7I&YWn;!Xg+xhE`v6SWe%{X*)pV;w2QEV_v89&8 z#~R(oCIWJpA_?rmvLTN?_)L#uFzT?!0Z26hbz>K{ZCM0oKAK5i*MhB%X=9yNjfm6I zhUU?-cczR`?|9$VmdWh5ZA>`B@eYP6miNUkDGWoP^m= zGlBD!2nL~FNr#k7FFK3%_4x$M6`X{!o2GWI_fl!Jw;p`poxa+4>HJTz-&%|OqnGeZ zmhQpl!_pA4vrIJ!0Q|lhKvur`vG(X`&}mF$q*xhZvjeCe*~+ltr~N-D4UWG$3*?pS z%&frPWP9lyag_5A`USJ2ccq9>ZN=MFLx=JC`)P7Ac;bc9LLub{h6-th6sTWVuhSSY zE=B2;7<6bB5hgNeNe5qA3&0JIL@-w_e{t^~ae$>~a*#B_bzSfyDVD6g6eF?(QR9d| z4a%UKCSnZkKp_E}O2%F0TxJtlfENrQ92hdssaA4siikL5>LH|-GMUb0Fh8z&OSsxnfCkrBtRmb zkqIWnsoDm{5qONcompyy!ON@Wp>W+fH`m8HUaxZbbUUaZ>;j)KKiAJJtTB*uMvCss#i|v^2LEL!#`03RuoX1Z|>qVy)W0N;(6D zjRh_+zo1iLUV(>$pLLxaHw>kq0OLz!Y)Am3nMT~zLi2#Y#RB{lC zgv3+O7)L`G(L(?@9Dw^T0!>(}lw%26;#1H;b+9;B*0gsRlO;ykoNt4vl|X|oqAXVf z2T7;Kz>q!_U{yc?p{0Wh{m@Bn+_gGP0Nrh#yM+wCUFwl1xzUF-n zQ-C^2dGkE6_$~WHyWcpG1y0oOrl+ zI6R#1DkVf$I4>b=w)zHHh1W89@dLO0^ts!UqkQ6@t~_-T!yioTe+FPyf6QBPS`jsm zk5qXv2y}(g1cZ}DF6;qye%(V01<`0{a6l#@0G|T(HSYElwmRAl4^3NY^NZj{fm$V=M=&bj|pU`S2J^YzCyWd6cN^M)c_u_qlv z+Rf7)FFxde9bZ*tyy5ux4?euN%1^y$y6Y((y;ypA5z+dc#Q@J~7`&7RqAdZne}~`( z`oW>doUa+!1wdz17DNokqoGJ*P!@nh)SELp!rWOM zh`a#EK66s)#sy?Svs5KUyX;Ie(;=`6aPzs?~s+EOHLC zl`$=VHs=e9qY+4;8l@IE6K>#6 z4>Fk2^+rG&)#eqVklrS*NksAUemc0UUC=S}I(H%wZO*<6bre5%GHxXUCR>T|H9T^# zml=)#OaY=ntJSn4Ky+k)ktTQL_AJT!Nsv{_dLXAc{KQnuf9lX``H7b>2=FbOVAz|4 zv!*#y!sId6mA*KSeiQHwhAlwxRDG$y$N=rvXNDiqYr0Z0f;`i&u9A*CW)7KwHQE|a z8%NlKA>;ADskL~wVAU=iGd`vMz=;I4zaYutlPcMsVsJHOlg z-+NtID%1ovHY~?Is(wZH>#x0eKfd=`4t>neU+L)lg^|qq)M){;K)+%3z4sgjlE3x}DbXw63Rh{-H1Zky;-+els!a{R{EG z2w*T648}o1vyP{AKXt$-i{l^v@gM*4`pu+w3oLmW6`uj$kVu?&^n%6F(>Sbef`2@0 zz+f;Ki~|6z(`nsK>v&q%(>kBl{nP=}1=I;2mvxv~9ZAeAMoLzxDl9q`HpkEJo1K(d z${~?i;hP_fw74DygTY`}le(WefVzM>!NAz&@UHUEm6-i)Qr3?kyu9QQu&*lW?{uZ` zcK62lvko|{XRPJ*F&GR6gF1k^fI5Ky>WB}@<8S`vZ~inst$?I0#V^iYbl-P7Vr*uM z?C5wz`vgNOvDl;aG8ha7gF1n_AuhcD)D;>62@Z?-Z_?f+xcXh)R~!CMM+|X9-vmz1 zp6Dx6pd~RICv8sQ8&ZB)-x&JFX3&$E;aja`OAH2s!JuxSjtB~m`R{G=Fg3S3K_(z7 zHk~#V?MZW#mHZUWo^yne)Np*`+2J+V1%uaykqTOG+Z zF_~J(k#H*sKPKnn%yEb98%bhy3`}XfIIoMV7g{PS(O=hqr)^zWEp0|)LJsQ6$}l(7 zgV$rjn5n2ieE3ZiC#GXC^DcT~O5pA2L+AfW`@vu^7z~>e_M$7OGemjl7|2YhQNnH| zYt35LHYaiX`1viN$mM5HmRtx$QV~wb&ux_(TO5V2PXOXVBXFL`jLO8Dj8C+;;!R&4 z)*F>jw+v!sXdJ8ali(I6p&lQ_n>HmD%IokVw;s=LS0c|b0+!}>UwK~`3-Vk4~{ zYvBqsJH^9JBF8@+X4waW!C-uCs4J*5s5=h*W)rD}{Hw2sis797W%O6oqdGPn#!{>mm4~YreZ~Ha)ynm9&+P6dL_dO%-d4jYT((9@T}#6phTj4Z4z z&*AOjEQFyU@D*)%p4)`y?R`)U3_>9C5Sn_(v{CRxGW^mMxM3ob{(cCpWLi-(xVRc{ zvDM(xtD(A6hbPhHNDYZVU49Xkd%LhPJ_4?}7mqV*kZ~g8bl(LAgTbKApzfdo`IVrR{tq2P8T|c%@N#Gv&wpw{ad13BoB~l0 zm<0P{7jXQPJz8q3@%GUo-p);7v%3$Q)!k6HkANQ@5u2xk+776D1|Uq0L!f;gL>@vz zJA}#>2&GLB@*2SB)I*)$0Cip+_}n^hC5?DmQ;&6}66%RzsELfYye7Q)sTG6wsxZ~u zfYsrCaP8e#t7^r9q7Ll|*{Jl7Mu@!^j<9!qF>SPHer6oeJa^f>zQTDu|4vG`t)c z#ySzfXiOPeBl4jP%7Ley6I|UqG2TIBurLK~U;unu4>pxU*c=#zpzJ3C>4Cbl7wR!G zK0QXHGC;~nOnoOrf!rkmQM3?RA=I}*UD-s+o1w01gV0LFqXggD3BITaDk>Nv43(0U zk#co2DgUV%LU|)NvYgdB_b?EW39nPmECXRM7>wT+$S+zGh=J4su{UE-TUdgZqeI}D zJMc2O1`n@RV8}BIHyr|zm6m}wvr|M4qu~4cMFbMd4-%OS4}zN*0Y62=F+TyG#L#k~ zzYprJ9wLJ-sL3*gwoW38E{Ia-?1s9r9h=o{;2YYYZfXZ#+z2734m_0&l}cBa7$asc zxwRsy^eVi(br*NN;$UH7^GBW-27|$10-&5l7$J-ggD1a5rpLf7O+j^k24cU*!lYQnlNcJuO$>{j=&G?{s3u3C znjHlYyM>lGOF%7O1m zSNiP9AANEd3l6pL{14!ZKwVst07c7&9e`L>L(WucXaa9pEd7 zG>YoM71x31;1lY=-D!f*HYjEw{34M8kpTB_PP|Q59DY16 zq9SEf4x&`%CPaa3{3eVCqGYh~WFA{Qm?5!5LX$(_hI*lD?*!jML{ip3)?bT_lxlFv zwOET#;I(HNmM-NZT^#n%dxYO3o*7D*KgypXCUNfFG1J%j}e=j zZ>Yu>%N(42G~zD0`f2RLX$O}*-`;(2T)v1)ZhnY}PThv}@pJn^m^TtLK6tI3w0Ay` z=Xm#Pnr*M9K(zD>$jk9DY)+0s-O?pyAVPXQgsgg~OPZnX>;X4FPQ)<-)l(u2A_IPD zM$A6wI28)FAU0z`92Uhv7Rbl% z@BS4?z|~v3U6)$QkeNVszjh9&KpDq87yoUfjCYPl)*Gg2dDsMM)Xp z>$;&<)?BeDegGuO5?m7C%}dD%#vd`$c+`*eIyQ zYE%X0eTooq#*e;Hk_trpD3y+p$({+s#rKK^?tWL`cl8FsqEir;UiiWJVp4PWtizV3 z%ub#`+pv_MIUoYj{&6`70-;T(yUBwR5u3&Y!UXa)`aPsDv#^7iOipiy6~7qh;pm0s z;s$VqjZoiH;^*NB@RP&uWgk=oBEK(@2ed<-urP&V`dzxxk=hTvWq+xw|IXVKQn=ivNpVmRAA(K2|UlZiA`?SCMS^YoruJc zIOt0(_Ju$w-{?p_)OGx!1SYIe=v}J{BhBeKXa z&jdoVkC@0rjOKNsBd!J^=R@%Q5sCN>5KntI%q1$YmevTax*MuaB~h+`nMl=X&G{A&v&8o5rDO8fe^|1b26L zcMncni0g{X<;D{t5SQ^k;d`~;8rD4CsZOWo_S~Ku^21&%`qZg9QfGhlZ7bH#P1fHn zyjr}?g#CSZ18=Tg!qeUR@i1oyM@`vZq^DYZ2e$tBIdtfEdU5fep!Ry zTss#}-Ucr##N{m!8{u(68@_*7+<(mnLW;4fxtE0dES8Uv@Bx4?VX-NKzS45bd2QM6 zWrDVJf4sKo1(kxfGM|ulelBX?uL=MmC7YO4taSCCoK+$KLk$SslVB{S3@Nl+@OxoZ z^V_}~V)hi4%)NdaM|aM5<3!Jgd#}pIAQqHYUayiZWw7tX)Y?(w0HTTU)to!;4}fep zvP76)3}y<4F_74XFvkcO7+KMzf}?{wCX3qexMCX5y7%Dafg>z5mxb)UTEw6K{t|!r z$1D8x@2~KezrDntUoYX+<2&T<5P}HhfPBkX#5@yz2rmg zd5jl~!7NQ3#6nshPKVbcnq4me2wmqk26l!<)-W-Z&}66UCebQ@5R2eVBoj;f9~FSa zBxb#9kbqhM;^3wboKGdDw;EaqR)uAp)f_!Hj-sPJ1RpJ3m+I%D_Wh~^5Jg}l3d$Q* z_Ct9ZR<@coBjuLV3C59n20+i$n#+uBojp}3SSdTNhoexd1m9X>hLGiy*LA>{7zp22 zL*ef4{M<|HJBcM|3_vt8zWQ_L{qlhr?+_!@AslgzF%X+OQdL`+Ti8P^aYjdGH5T$l zu~a^a7yFOl#r>Oj^7H|g9^b>#;$18~x`Ss=@8k8GCwR?(<>k`t6(I8B9uvwDxptMY z7bbV+x!^%ep3WDYzCm(6qG|FW}QSlk?kSrO`y>@r+e=|HgsTcjmXMy|# z%>Ov5krcoB0HV?HS2uu=0$yG{&}#nwnyrkjLB0i2$h7+)ZPw%`^~aw0dfY4=#QgXy zj?T`YvAPDuc?GDetY&}n(AH3gb0?4DE(3@s_paf^!Zm?!n6+l zxW?*Zwba(2G%p`R-Rx#dD^QwWfWtk*IFj0mtYC7OTP|NayH0bsI(s24HXR-wfsoj^ zLF(j1rn&zLfFz_Ba3wjXM5)rC$ShKDKQ;hyQv}hNB)k>q@-Y%x-Kz6^$sVurqg*z* z@XaT(6>Yr&A_Q$n>8bNv)VyB~fC#0yrVTsO@(~i7jFjwBVnr&vAd@aS5D1g@!BQEKIT6 zD>)Mhkx7sk+d-@+fs=^?>@c?_5L_-zPeS*ltvnF==x8O)9Ygy7qO~Y2GVuudi%ra4(T0?+(iI>(f5i$gIfoB2 zTFL_$rndpP`@ePF@b7#i8Y$W#KcoeX9S$YR}NF`V9s(zrr)-4YNW5;!WneBmPXBoNUW zfTS=du*kvf1Aqvft%Cl@k-0Jywfx^eTvW3{c`?9P(U$oe1S>=>CKA%>R;lvL6t z)z`r@A^8MFb3uUW;HmasnIl{1q!$Q5q#Ij#Be zS0R8b)T{H$EKN)@1l!mqy4!}`y*VEZN$)K%2t!TF20 z+&hZ;*mSs9xG?1$0fl=YKK;Yj`0X#gfc`f|NS8$*)iDefx|VPhNl{-?iPoxG)R$Lb zxRYIcQ8`+wYS`amSTgX~x=tUZv8mYI&*ihwLejbMQ03;x6r_%Pm zA+bX9T~ao$tvZ1T6e09hC-<^YC@OD`@)kKf5jhA$cH41 zb_5GHKuZtke6|hS zbLzhw+j3f+Hc5ulUuR z)0!WDl>$hxcQ~%MjpJp{G+yl9hv!p_jqE*$hrP2nnAeUV$3U$4SQjRSVvMx(;P&`_ z%;eU<(^5(-fdRl~tYyc4|I0OuF?@#K{b~(d&73fpS&BT*IBZ{QjITc441+aBu>8gX zHXE!V*0F-|+8y}%lP&n_lT9d%NX5DFF?fh&_~dtA;&aAkK4XCNF#`?)kB@%49$VKN zphB?=H~VKWT-gNOueLJ=vH`jq4G`=ff{C_H9Bk{t-O(95XV*x(7hbCCL2^J01qo;+ zk&tR+aX#n|0J3uH00Gdl=q!N)os!*_-wEg5N%3(?m2172;tvWS6luk?(Nl6IPlzS( z<|jGXxnpp0L+BiyDa=?!);SAX7Mv&d4N)~FIjuPnW`%0cY0Zzn3IW8-!WNaOmAKim zAJ1D@p~B%AEDcO!X?PY3wIeti(SjC_eAsSxfVr_9`m5XUZR;?m(<|U(EyouOI7s1> zkh1@X$=Z)M!c*jq3C0qtLQ-M;r3q{{iIDFRhrWaojAm40D5Vk=KFJX2T0{5Kt?0}w z!R6^m_(N*MqyO7Y53zu{gCC zPlxy58Dmo~`x%IIPGOpjBe$1pA(4=?7Xvq$P<;TQSo8M=K=_&FWCHxx@Beo6#;4{l z`$blmX%aFE>36bC^PkBo0T6vvm3P$;ijI4SvL75JEL;RA6LQ$UMt)TbKtW+{;BIK$k?9)oN(bE9tx z`_rls%b1DodPA)HWFyuwrT@hrzGi;M^%Z zymSr^FPz5w^gg7@qp)qQ0m^q};M##bD2_^G=NmAe%Ok z0nV0mB8fTM39yWH&C!@rfQ6wcJnk6B(-y|0nkMnQdK}mDm?8>Df`z#)%*-W_IJm-0 zWcPyt2ru5AglZu1OK+&A|J43H0}ysy0)GHK`xOp!Qn)-jbV+p^H=2u9$EyiIJp95n z7Vwc-P^oG>nsci9dFzgc0h~xK@^7gam+~_>xkC|Z&uPt%zX|~)*?T9t(rR!#yBmwS zQ+QZ3fqNCBxKlEO`SdOvR8*nSGzC6i%V4y{8r21itxO-r`I0U)hh)Oh$Qim{8sh6U z+n7^(J7Wa;u-I;c0>51tVywc)Mh+)E2aFXr;PLTO_{;pC@Ru9c@aoD{JiT}xs8u0`3j&!~O9CxH&R|L}?g|bWKs6m5WQ0 z<7mq+f|tYtR))4P+G-84fjz=)yfG41gp&yk=#R{Y-BvNS{&6ePH~ZnVPc0s$4q_=~ z2#=G8@HBoHx8oRlif_PFaxG4zwcvD48;T=SVetbp5Xz|LMX6BuBR}~~bvUOB$Km`Q z00;@~11-qm_R-|7Gms&dPjYn1Cjrkb}31$9tcejo$SbY(&L zIJ|^OdS@I{Rx#gvE^?svs?IZ`YZA1lcSPnpb?3C^$6tj2LeWdb@G5U*gg=Tu@Ukx6tZntW4{?i_{`r%+V* zB%(DW1DUc=*liO*u|taK{8~I5n8nrFemv-$#0v%g0Scsc}Gi(eb zsL#y9!PX887F3`xJOL?+2o(9mVu*ppctkPEq%rW+cY>a-8QizHpiw^-m;9RWAa($c zqKB{)IgBTfLs*O)z`{=UTlx@gW%pq?z8DG@1tm#p1VE^~GfyZgg&diA`~rGhcEO?2 zG_fh7W`(_(V$G=>_x~Dza3Y=55Dr`8NM=#Bpj17-E#HTvNdV#BuXX{%FC_N+IK#Q+ zNhs0rU6m+}MP^xAG^t_AqWW9hl0TeeJ_?mpk-e|}oYwsKs^`voSI*tpN{U|ZVw?|S zNccZ$dGyPcp~+;UjpscRv;n9m3 zv|uq}2#-P-3ke#;qksWC2L(}dH3?Kl_Gfnx#9n37eZ!6Fl7P8pc09mJ86 zPK-tsBgZiup~iCf>$$>rn-hE)Sh#Kw!})75yf!-{Q51lYQ7oQj za_aw+lSlE09lt&{gW=>#B-#YCzGRSXwS(&>F+$Dd2r-f&Y=<1NR^BM{j7LjC2|Akx z(39MRM#Cf=ajd~zzkVzz`titn0E<2hK!OKxKB5CHA^8Z9`V-UA`kv^-N@(S*+A*%W zu9mg{BtsZ6CdkwH59cD4gP;%hos-*krsOKCJ*BZd(bc(MEddDaW1&{1T6My4DU5n$ z_IWL*P~?h&g5(cSw(p6G@*o&QW38R|)7Tgm4((^ZvtxAp=po#mn8jRa z3#Q{M(G!}3dd6xRyc5tLl7sz8)tJj@z{!G6OjPw?xN96;QMIVhW6pZZLR@fd#k_k5 zE_t-#lvfjGe5=stmxDO>2-u2T$Z4%L0HGBdm9#LmwpTiifvK36)W!9UP$HeS0ED7V zGz%aqa|1|tg(&G7xot@?YY0H-e*f12gvz^cm79^dM4f(!?Q*K@wyH`Rq=Z=&`6!&W zoN#v)EL*VhCzGxQdQQcX&C*y{a+cT7^j|{jI=|Ln-P^ z(@|g=kFKOf9PgUKnX+zNE$hK|)y&*g-;ZmI4Sd(aq;&}2wh!ZceHV_^wc%`cFD?%b z;Z#=_rmJhwo|%UPMG!)rJdqa`i@v-f>}_bmg@Hkw?dieM#%9dbG~nB=LEP*a#f^?( zTx%V`^`-&bXk?Ck1{?<}deGi9h`xp)R6As#+AtCAR+(tCXCJR~Dl%-N5oH$yFNr6_ zA}1J_i1<+z8UYYKSkUAmQ&m!CF@ho!-dU*!K zH4Y&B{U+#naM~(2{n0zq6o9C{U+n@2&237s6s-1hO4NP!RJNRZkM^z#i1-zFX`U-{ zkW(_D3R9L~3tn#7bWVN=hsmowr!_ym`g7;~@PUvGPa<|jpkp9%WyxswC_=w;8T#yt z(Jd)JjY%5v4dPKL%f@)!5M~*0oG9(WrHXD4fZXmH#=W62JYC=jELK=w~*f_~)9}C*VNR!vOI`8*`(4vo?F`3|~k^}h+$O%t1sP+lqr3KBovs%qb`byAadMPA)IUK#L>zo94N2H;o25l>gmUg@d@0XoW|{mDV%HT zVaMulxS}4^_PgFe+#4Cg{n2sU|3B@W18`(p{(w{XV~bnXmbDsB<95kFAShS@g_a97$acbvOOqo?{aWLYT`3WXBL7lu(tq$;zAp zDYO^ja%YoYVemF0b2S=r~+wF3#S ztsTbU)%8PO5!)m$iQ`^a+!Yeq;i1?TH%GTv7uW0`U7&k(nXZW?8ag95GjgOL*4_(?u96$*ty3FOijEs)a&IS^DP1En3rDn{w{w$Pm2 zOr|RjW0F$_aCBxD4o?y3)vct|b(7|gP}V(3`*4g4lS^z)uW{API=4-4aQDmx4+tKf z+2oO^5RBkKA({sS_sni`r{JdOCYOujSA;Z1N0$iq%~IYuK|$9P1)Y=RH??DTd;aa~ zLM}TsHy3PSo^vhoihGT1S05JbV+yzw3WY)mPVsM52&@wJeOSR8SJcRoy{qb1!MWeo`BH zu$6^`fW$H7Va%l=G$En0+4Y>tsKzSxXARGjA6>;W9;0k{hU&@y)$Vh|8Y6TyccM>9 ze*am8@T4?yH81(5Y;^h$s@7(T1Toh_NIj2%<@ZFwP3e=!$$8Dq7L1JH)b|GwyP- zn|laHSLj&WqGEoXij^%IqZ=&r%roW)b6w{$v7smhMdh+@tAG1)IwQn)>I0uyp-?Ck zcDEPl4C#)9%KG4QV&!+T(aBdW5Kl!j)l)0fFKpAZalp*QEuw>SoU4*qT0uIC=P}lIp*8kktm))TdL_jrem3ToxOw-0>o#}ji!ReR zw?gyeGQ*Vvj2Qx4TN&k%u>-CdTqbnRPvR$P)T2-+6bk7I=?v+P1kLP9j@#tRys0ez za5yumK37jyxQ9?<3ndjzRGc5?!u&edZS8UQ_8vDxmpLDZ;3y8D2|-xeN3aRZ0V$9k z%ne;63QSGiB+6~UehfA3oXV;q(N@4}|Ac70zR&ZQAMwoPhg`q1$#86i_Q*8#`f>)( zG;!n|<*Bwk9*Z1ub^kKu-m*{7Rf$5OP$=TLR>mZ!GeM^JyAI!aH#4{FOSz%U7RqpC zGhEZj)va?pGPuWezBzimK^mI6SQY|#cxRXAj}Ccgd7Jt6alH8fbotHbO9B!Ml`Uws z9cWc;=&RqyDAtWtt(@^xbIg=YMQ#mu&MxxY-X1SqcF2o@ht_w9%`Q+b1XKBwJR({n z7fT{c`vz#ptEH;222Xn4$CLpn6bgkRGp`Kk3h9i5cyhY4^Q2J*#$#VfAk}F$(j8vP zQcGy}HZon*$%s2h{poVDlZ)vJ^m5yUB_3PY;QpxUpKzCnZKa%Y6miVx;gl|&f!Y>!TKc$edXc*(Vmv&*%FTl@{B=Q0#$>E# zounJnNwsHTwz<)1c6272L^U2%C=?%qDm<5tkgkxKQg%Wk+}<_E4ZcYhOS>sdF66A<#m~ht$8>2NH)L{L zh~k*ZBj#Bg6YIzH=>j*$&e$>NQ1RLt+5b+?ts_5AiYpqw^oNnbJieT4Nu$r}W3PCC`OFr2 zosHyLaz&uGaa?Z}SUGXl%5lL7olW52xR8ciJ}IxGvtSY;@+FtkY%XRX-AB8#nlWz( zYoaTDug`!&p-?EK6Qmm^V(Um(Bz#aZ*;0Nk8;A(rt88ld$_kcc8A*zkhTndU21_v> zlLx!W{hwb7LKt!$=im3qV>M$kW`h%}<|56MfhW=P8MCGo3WY);9UxsGoghFuBH^Qw zJtf^B$E1iZ?*6iObXIl;E-&+=Rzow&musDLZMJ(hBD74lo{Fjo<8&;BQ|E3Wb8t%<}G~5BPgkC@u+*F + + + + + + + + + + + + + + + + + + + + + diff --git a/image/axolotl_symbol_digital_white.svg b/image/axolotl_symbol_digital_white.svg new file mode 100644 index 0000000000..2752ffb0f8 --- /dev/null +++ b/image/axolotl_symbol_digital_white.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/image/axolotl_wordmark_digital_black.svg b/image/axolotl_wordmark_digital_black.svg new file mode 100644 index 0000000000..d8041b1a85 --- /dev/null +++ b/image/axolotl_wordmark_digital_black.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/image/axolotl_wordmark_digital_white.svg b/image/axolotl_wordmark_digital_white.svg new file mode 100644 index 0000000000..7cada6ae00 --- /dev/null +++ b/image/axolotl_wordmark_digital_white.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + From c07bd2fa65750573e2b50211081368b3ea3d497f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 18 Nov 2024 14:58:03 -0500 Subject: [PATCH 0264/1405] Readme updates v2 (#2078) * update readme logos * use full logo * Fix svgs * add srcset * resize svgs to match * Rename file * align badges center --- README.md | 29 ++++++++++++++++++---------- image/axolotl_logo_digital_black.svg | 19 ++++++++++++++++++ image/axolotl_logo_digital_white.svg | 11 +++++++++++ 3 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 image/axolotl_logo_digital_black.svg create mode 100644 image/axolotl_logo_digital_white.svg diff --git a/README.md b/README.md index d4d75d99b0..b900a1c5a8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,21 @@ -# Axolotl - -![GitHub License](https://img.shields.io/github/license/axolotl-ai-cloud/axolotl.svg?color=blue) -![tests](https://github.com/axolotl-ai-cloud/axolotl/actions/workflows/tests.yml/badge.svg) -![https://github.com/axolotl-ai-cloud/axolotl/releases](https://img.shields.io/github/release/axolotl-ai-cloud/axolotl.svg) -![GitHub Repo stars](https://img.shields.io/github/stars/axolotl-ai-cloud/axolotl) - -![tests-nightly](https://github.com/axolotl-ai-cloud/axolotl/actions/workflows/tests-nightly.yml/badge.svg) -![multigpu-semi-weekly tests](https://github.com/axolotl-ai-cloud/axolotl/actions/workflows/multi-gpu-e2e.yml/badge.svg) +

+ + + + Axolotl + +

+ +

+ GitHub License + tests + Releases + GitHub Repo stars +

+

+ tests-nightly + multigpu-semi-weekly tests +

Axolotl is a tool designed to streamline the fine-tuning of various AI models, offering support for multiple configurations and architectures. @@ -79,7 +88,7 @@ Features:
- axolotl + axolotl

Axolotl provides a unified repository for fine-tuning
a variety of AI models with ease
diff --git a/image/axolotl_logo_digital_black.svg b/image/axolotl_logo_digital_black.svg new file mode 100644 index 0000000000..2b0ae35e5c --- /dev/null +++ b/image/axolotl_logo_digital_black.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/image/axolotl_logo_digital_white.svg b/image/axolotl_logo_digital_white.svg new file mode 100644 index 0000000000..fe954a6fd1 --- /dev/null +++ b/image/axolotl_logo_digital_white.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + From d9b71edf84887a4e63f862dec6f19d958a3c71dc Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 19 Nov 2024 02:23:09 -0500 Subject: [PATCH 0265/1405] bump transformers for fsdp-grad-accum fix, remove patch (#2079) --- requirements.txt | 4 +- .../monkeypatch/trainer_fsdp_grad_accum.py | 83 ------------------- src/axolotl/utils/trainer.py | 8 -- tests/e2e/patched/test_trainer_fsdp.py | 15 ---- 4 files changed, 2 insertions(+), 108 deletions(-) delete mode 100644 src/axolotl/monkeypatch/trainer_fsdp_grad_accum.py delete mode 100644 tests/e2e/patched/test_trainer_fsdp.py diff --git a/requirements.txt b/requirements.txt index f8fe20bf97..60b5333727 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.13.2 -transformers==4.46.2 +transformers==4.46.3 tokenizers>=0.20.1 bitsandbytes==0.44.1 accelerate==1.1.0 datasets==3.1.0 -deepspeed==0.15.3 +deepspeed==0.15.4 pydantic==2.6.3 addict fire diff --git a/src/axolotl/monkeypatch/trainer_fsdp_grad_accum.py b/src/axolotl/monkeypatch/trainer_fsdp_grad_accum.py deleted file mode 100644 index 6819fde111..0000000000 --- a/src/axolotl/monkeypatch/trainer_fsdp_grad_accum.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -fix for FSDP gradient accumulation -see https://github.com/huggingface/transformers/pull/34645 -""" -import inspect - -from accelerate.logging import get_logger -from transformers.trainer import Trainer - -from axolotl.monkeypatch.unsloth_ import detab_code - -LOG = get_logger("axolotl.monkeypatch.trainer_fsdp_grad_accumulation") - -ORIGINAL_CONTEXT_CODE = """ - context = ( - functools.partial(self.accelerator.no_sync, model=model) - if i == len(batch_samples) - 1 - else contextlib.nullcontext - ) -""" - -PATCHED_CONTEXT_CODE = """ - context = ( - functools.partial(self.accelerator.no_sync, model=model) - if i != len(batch_samples) - 1 - else contextlib.nullcontext - ) -""" - - -def get_training_loop_code() -> str: - training_loop = inspect.getsource( - Trainer._inner_training_loop # pylint: disable=protected-access - ) - return training_loop - - -def check_training_loop_is_patchable() -> bool: - train_loop = get_training_loop_code() - train_loop, _ = detab_code(train_loop) - return ORIGINAL_CONTEXT_CODE in train_loop - - -def patch_training_loop_for_fsdp_grad_accum(): - """ - monkeypatch for fixing the training loop for FSDP gradient accumulation - """ - - train_loop = get_training_loop_code() - Trainer._original_inner_training_loop = ( # pylint: disable=protected-access - train_loop - ) - train_loop, _ = detab_code(train_loop) - assert ( - ORIGINAL_CONTEXT_CODE in train_loop - ), "Original _inner_training_loop code not found" - - train_loop = train_loop.replace(ORIGINAL_CONTEXT_CODE, PATCHED_CONTEXT_CODE) - train_loop = train_loop.replace( - "def _inner_training_loop(", - "def _fixed_inner_training_loop(", - 1, - ) - - # load imports necessary - import transformers.trainer - - items_to_import = [] - for item in dir(transformers.trainer): - if item in train_loop: - items_to_import.append(item) - - exec( # pylint: disable=exec-used # nosec B102 - "from transformers.trainer import (" - + ", ".join(x for x in items_to_import) - + ")", - globals(), - ) - exec(train_loop, globals()) # pylint: disable=exec-used # nosec B102 - LOG.info("patching _inner_training_loop", main_process_only=True) - Trainer._inner_training_loop = ( # pylint: disable=protected-access - _fixed_inner_training_loop # pylint: disable=undefined-variable # noqa: F821 - ) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 5c9bfd6635..2d3a6944f7 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -16,9 +16,6 @@ from transformers.utils import is_torch_bf16_gpu_available from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder -from axolotl.monkeypatch.trainer_fsdp_grad_accum import ( - patch_training_loop_for_fsdp_grad_accum, -) from axolotl.utils.distributed import reduce_and_broadcast from axolotl.utils.environment import check_cuda_p2p_ib_support from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths @@ -496,11 +493,6 @@ def prepare_opinionated_env(cfg): def setup_trainer( cfg, train_dataset, eval_dataset, model, tokenizer, processor, total_num_steps ): - if cfg.fsdp: - try: - patch_training_loop_for_fsdp_grad_accum() - except AssertionError: - pass if cfg.rl in ["dpo", "ipo", "orpo", "kto", "simpo"]: trainer_builder = HFRLTrainerBuilder(cfg, model[0], tokenizer, processor) trainer_builder.model_ref = model[1] diff --git a/tests/e2e/patched/test_trainer_fsdp.py b/tests/e2e/patched/test_trainer_fsdp.py deleted file mode 100644 index 1095cff3c0..0000000000 --- a/tests/e2e/patched/test_trainer_fsdp.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Test module for checking whether the integration of Unsloth with Hugging Face Transformers is working as expected.""" -import unittest - -from axolotl.monkeypatch.trainer_fsdp_grad_accum import check_training_loop_is_patchable - - -class TestTrainerFSDPIntegration(unittest.TestCase): - """Unsloth monkeypatch integration tests.""" - - def test_train_loop_patchable(self): - # ensures the current version of transformers has loss code that matches our patching code - self.assertTrue( - check_training_loop_is_patchable(), - "HF transformers _inner_training_loop has changed and isn't patchable", - ) From f007c38e4989cd172836889bf29664ce9b4a875f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 19 Nov 2024 22:18:24 +0700 Subject: [PATCH 0266/1405] Feat: Drop long samples and shuffle rl samples (#2040) [skip ci] * feat: LOG warn if samples are dropped due to seq length * feat: add drop long samples for RL * feat: add ipo * fix: remove num_proc for map as subprocesses are prone to die * feat: shuffle rl dataset * fix: support preprocess for kto * chore: use set instead of list * feat: add simpo --- src/axolotl/utils/data/rl.py | 71 +++++++++++++++++++++++++++++-- src/axolotl/utils/tokenization.py | 43 +++++++++++++------ src/axolotl/utils/trainer.py | 24 ++++++++++- 3 files changed, 121 insertions(+), 17 deletions(-) diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index 35bd5fcbb7..cf1226175e 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -64,15 +64,57 @@ def map_dataset(cfg, data_set, ds_transform_fn, tokenizer): tokenizer = load_tokenizer(cfg) ds_transform_fn = partial(ds_transform_fn, tokenizer=tokenizer) + if isinstance(data_set, DatasetDict): + data_set = data_set["train"] + data_set = data_set.map( ds_transform_fn, desc="Mapping RL Dataset", ) - if isinstance(data_set, DatasetDict): - data_set = data_set["train"] + return data_set +def drop_long_rl_seq( + sample, rl, tokenizer, sequence_len # pylint: disable=invalid-name +): + if rl in ("dpo", "ipo", "orpo", "simpo"): + if not ( + sample.get("prompt") and sample.get("chosen") and sample.get("rejected") + ): + raise ValueError( + "Prompt, chosen and rejected keys are required for DPO/ORPO datasets" + ) + + prompt = sample["prompt"] + chosen = sample["chosen"] + rejected = sample["rejected"] + + len_prompt = len(tokenizer(prompt, add_special_tokens=False)["input_ids"]) + len_chosen = len(tokenizer(chosen, add_special_tokens=False)["input_ids"]) + len_rejected = len(tokenizer(rejected, add_special_tokens=False)["input_ids"]) + + return (len_prompt + len_chosen) <= sequence_len and ( + len_prompt + len_rejected + ) <= sequence_len + + if rl == "kto": + if not (sample.get("prompt") and sample.get("completion")): + raise ValueError("Prompt and completion keys are required for KTO datasets") + + prompt = sample["prompt"] + completion = sample["completion"] + + len_prompt = len(tokenizer(prompt, add_special_tokens=False)["input_ids"]) + len_completion = len( + tokenizer(completion, add_special_tokens=False)["input_ids"] + ) + + return (len_prompt + len_completion) <= sequence_len + + raise ValueError("Unknown RL type") + + def load_prepare_dpo_datasets(cfg): def load_split(dataset_cfgs, _cfg): split_datasets: List[Any] = [] @@ -94,7 +136,7 @@ def load_split(dataset_cfgs, _cfg): ) split_datasets.insert(i, ds) - tokenizer = None + tokenizer = load_tokenizer(cfg) for i, data_set in enumerate(split_datasets): _type = dataset_cfgs[i]["type"] @@ -121,7 +163,28 @@ def load_split(dataset_cfgs, _cfg): # "prompt", "chosen" and "rejected" already preprocessed split_datasets[i] = data_set - return concatenate_datasets(split_datasets) + drop_long = partial( + drop_long_rl_seq, + rl=_cfg.rl, + tokenizer=tokenizer, + sequence_len=cfg.sequence_len, + ) + + prior_len = len(split_datasets[i]) + split_datasets[i] = split_datasets[i].filter( + drop_long, + num_proc=cfg.dataset_processes, + load_from_cache_file=not cfg.is_preprocess, + desc="Dropping Long Sequences", + ) + dropped = prior_len - len(split_datasets[i]) + if dropped: + LOG.warning(f"Dropped {dropped} long samples from dataset index {i}") + + combined_datasets = concatenate_datasets(split_datasets) + combined_datasets = combined_datasets.shuffle(seed=cfg.seed) + + return combined_datasets with zero_first(is_main_process()): train_is_preprocessed = False diff --git a/src/axolotl/utils/tokenization.py b/src/axolotl/utils/tokenization.py index 97c3c64650..139d501109 100644 --- a/src/axolotl/utils/tokenization.py +++ b/src/axolotl/utils/tokenization.py @@ -66,28 +66,47 @@ def process_tokens_for_rl_debug(tokens, color, tokenizer, text_only): def check_rl_example_labels(example, tokenizer, text_only=False): - field_prompt, field_chosen, field_rejected = "prompt", "chosen", "rejected" + field_prompt, field_chosen, field_rejected, field_completion = ( + "prompt", + "chosen", + "rejected", + "completion", + ) input_tokens = example[field_prompt] - labels_chosen, labels_rejected = example[field_chosen], example[field_rejected] + + labels_chosen = example.get(field_chosen) + labels_rejected = example.get(field_rejected) + labels_completion = example.get(field_completion) + + # Create a delimiter based on text_only flag + delimiter = "" if text_only else " " # Process and color each type of token colored_tokens = process_tokens_for_rl_debug( input_tokens, "yellow", tokenizer, text_only ) - colored_chosens = process_tokens_for_rl_debug( - labels_chosen, "green", tokenizer, text_only - ) - colored_rejecteds = process_tokens_for_rl_debug( - labels_rejected, "red", tokenizer, text_only - ) - # Create a delimiter based on text_only flag - delimiter = "" if text_only else " " + # Process tokens + if labels_completion is None: + colored_chosens = process_tokens_for_rl_debug( + labels_chosen, "green", tokenizer, text_only + ) + colored_rejecteds = process_tokens_for_rl_debug( + labels_rejected, "red", tokenizer, text_only + ) + else: + colored_completion = process_tokens_for_rl_debug( + labels_completion, "green", tokenizer, text_only + ) # Logging information LOG.info(f"INPUT PROMPT: {delimiter.join(colored_tokens)}\n\n") - LOG.info(f"CHOSEN RESPONSE: {delimiter.join(colored_chosens)}\n\n") - LOG.info(f"REJECTED RESPONSE: {delimiter.join(colored_rejecteds)}\n\n\n") + + if labels_completion is None: + LOG.info(f"CHOSEN RESPONSE: {delimiter.join(colored_chosens)}\n\n") + LOG.info(f"REJECTED RESPONSE: {delimiter.join(colored_rejecteds)}\n\n\n") + else: + LOG.info(f"COMPLETION RESPONSE: {delimiter.join(colored_completion)}\n\n\n") return delimiter.join(colored_tokens) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 2d3a6944f7..32e54c9a86 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -203,37 +203,59 @@ def process_datasets_for_packing(cfg, train_dataset, eval_dataset): if eval_dataset and "token_type_ids" in eval_dataset.column_names: eval_dataset = eval_dataset.remove_columns("token_type_ids") + prior_len = len(train_dataset) train_dataset = train_dataset.filter( drop_long, num_proc=cfg.dataset_processes, load_from_cache_file=not cfg.is_preprocess, desc="Dropping Long Sequences", ) + dropped = prior_len - len(train_dataset) + if dropped: + LOG.warning(f"Dropped {dropped} long samples from train dataset") + if eval_dataset: + prior_len = len(eval_dataset) eval_dataset = eval_dataset.filter( drop_long, num_proc=cfg.dataset_processes, load_from_cache_file=not cfg.is_preprocess, desc="Dropping Long Sequences", ) + dropped = prior_len - len(eval_dataset) + if dropped: + LOG.warning(f"Dropped {dropped} long samples from eval dataset") # drop samples with where the number of elements with labels not equal to -100 is zero def drop_no_trainable_tokens(sample): return np.sum(np.array(sample["labels"]) != -100) > 0 + prior_len = len(train_dataset) train_dataset = train_dataset.filter( drop_no_trainable_tokens, num_proc=cfg.dataset_processes, load_from_cache_file=not cfg.is_preprocess, desc="Drop Samples with Zero Trainable Tokens", ) + dropped = prior_len - len(train_dataset) + if dropped: + LOG.warning( + f"Dropped {dropped} samples with no trainable tokens from train dataset" + ) + if eval_dataset: + prior_len = len(eval_dataset) eval_dataset = eval_dataset.filter( drop_no_trainable_tokens, num_proc=cfg.dataset_processes, load_from_cache_file=not cfg.is_preprocess, desc="Drop Samples with Zero Trainable Tokens", ) + dropped = prior_len - len(eval_dataset) + if dropped: + LOG.warning( + f"Dropped {dropped} samples with no trainable tokens from eval dataset" + ) if cfg.group_by_length: train_dataset = train_dataset.map( @@ -493,7 +515,7 @@ def prepare_opinionated_env(cfg): def setup_trainer( cfg, train_dataset, eval_dataset, model, tokenizer, processor, total_num_steps ): - if cfg.rl in ["dpo", "ipo", "orpo", "kto", "simpo"]: + if cfg.rl in ("dpo", "ipo", "orpo", "kto", "simpo"): trainer_builder = HFRLTrainerBuilder(cfg, model[0], tokenizer, processor) trainer_builder.model_ref = model[1] trainer_builder.peft_config = model[2] From 775311f98f2e6861339d7665bdac4ca4fa98cfad Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 19 Nov 2024 10:19:03 -0500 Subject: [PATCH 0267/1405] add optimizer step to prevent warning in tests (#1502) [skip ci] * add optimizer step to prevent warning in tests * add optimizer step to warmup as well --- tests/test_schedulers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_schedulers.py b/tests/test_schedulers.py index 9402d7af7f..bd37bf01df 100644 --- a/tests/test_schedulers.py +++ b/tests/test_schedulers.py @@ -32,16 +32,19 @@ def setUp(self): def test_schedulers(self): self.assertEqual(self.lr_scheduler.get_last_lr()[0], 0) for _ in range(self.warmup_steps): + self.optimizer.step() self.lr_scheduler.step() self.assertEqual(self.lr_scheduler.get_last_lr()[0], self._lr) constant_step = int(self.train_steps * self.constant_lr_ratio) remaining_step = self.train_steps - constant_step for _ in range(constant_step): + self.optimizer.step() self.lr_scheduler.step() self.assertEqual( self.lr_scheduler.get_last_lr()[0], self._lr * self.min_lr_ratio ) for _ in range(remaining_step): + self.optimizer.step() self.lr_scheduler.step() self.assertEqual( self.lr_scheduler.get_last_lr()[0], self._lr * self.min_lr_ratio From a77c8a71cf4057d9b73e0a3df9a3fe792ac7b3b0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 19 Nov 2024 10:29:31 -0500 Subject: [PATCH 0268/1405] fix brackets on docker ci builds, add option to skip e2e builds [skip e2e] (#2080) [skip ci] --- .github/workflows/main.yml | 6 +++--- .github/workflows/multi-gpu-e2e.yml | 2 +- .github/workflows/nightlies.yml | 4 ++-- .github/workflows/tests.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a54bd8062..95a80e99da 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ on: jobs: build-axolotl: - if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]') && github.repository_owner == 'axolotl-ai-cloud' }} strategy: fail-fast: false matrix: @@ -77,7 +77,7 @@ jobs: build-axolotl-cloud: needs: build-axolotl - if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... strategy: matrix: @@ -140,7 +140,7 @@ jobs: build-axolotl-cloud-no-tmux: needs: build-axolotl - if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... strategy: matrix: diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 2fecde5a97..b4ddef5234 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -15,7 +15,7 @@ concurrency: jobs: test-axolotl-multigpu: - if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' }} strategy: fail-fast: false matrix: diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index cc1b6ea9f8..e266122c68 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -7,7 +7,7 @@ on: jobs: build-axolotl: - if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]') && github.repository_owner == 'axolotl-ai-cloud' }} strategy: fail-fast: false matrix: @@ -71,7 +71,7 @@ jobs: build-axolotl-cloud: needs: build-axolotl - if: ${{ ! contains(github.event.commits[0].message, '[skip docker]]') && github.repository_owner == 'axolotl-ai-cloud' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... strategy: matrix: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b4ef404215..6ae37f9882 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -78,7 +78,7 @@ jobs: find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; docker-e2e-tests-1st: - if: github.repository_owner == 'axolotl-ai-cloud' + if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] timeout-minutes: 90 From ec59d4cb8304fd3f04018a4df19592e1b7ca87b0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 19 Nov 2024 10:30:10 -0500 Subject: [PATCH 0269/1405] remove deprecated extra metadata kwarg from pydantic Field (#2081) [skip ci] --- .../config/models/input/v0_4_1/__init__.py | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index a32b62f758..9d21a294c1 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -250,8 +250,10 @@ class KTODataset(BaseModel): class LoftQConfig(BaseModel): """LoftQ configuration subset""" - loftq_bits: int = Field(default=4, metadata={"help": "Quantization bits for LoftQ"}) - # loftq_iter: int = Field(default=1, metadata={"help": "Alternating iterations for LoftQ"}) + loftq_bits: int = Field( + default=4, json_schema_extra={"description": "Quantization bits for LoftQ"} + ) + # loftq_iter: int = Field(default=1, json_schema_extra={"description": "Alternating iterations for LoftQ"}) class PeftConfig(BaseModel): @@ -294,8 +296,8 @@ class LoraConfig(BaseModel): qlora_sharded_model_loading: Optional[bool] = Field( default=False, - metadata={ - "help": "load qlora model in sharded format for FSDP using answer.ai technique." + json_schema_extra={ + "description": "load qlora model in sharded format for FSDP using answer.ai technique." }, ) lora_on_cpu: Optional[bool] = None @@ -304,13 +306,15 @@ class LoraConfig(BaseModel): loraplus_lr_ratio: Optional[float] = Field( default=None, - metadata={ - "help": "loraplus learning rate ratio lr_B / lr_A. Recommended value is 2^4." + json_schema_extra={ + "description": "loraplus learning rate ratio lr_B / lr_A. Recommended value is 2^4." }, ) loraplus_lr_embedding: Optional[float] = Field( default=1e-6, - metadata={"help": "loraplus learning rate for lora embedding layers."}, + json_schema_extra={ + "description": "loraplus learning rate for lora embedding layers." + }, ) merge_lora: Optional[bool] = None @@ -380,10 +384,10 @@ class ModelInputConfig(BaseModel): tokenizer_use_fast: Optional[bool] = None tokenizer_legacy: Optional[bool] = None tokenizer_type: Optional[str] = Field( - default=None, metadata={"help": "transformers tokenizer class"} + default=None, json_schema_extra={"description": "transformers tokenizer class"} ) processor_type: Optional[str] = Field( - default=None, metadata={"help": "transformers processor class"} + default=None, json_schema_extra={"description": "transformers processor class"} ) trust_remote_code: Optional[bool] = None @@ -405,18 +409,18 @@ class HyperparametersConfig(BaseModel): gradient_accumulation_steps: Optional[int] = Field(default=1) micro_batch_size: Optional[int] = Field( default=1, - metadata={"help": "per gpu micro batch size for training"}, + json_schema_extra={"description": "per gpu micro batch size for training"}, ) batch_size: Optional[int] = Field( default=None, - metadata={ - "help": "Total batch size, we do not recommended setting this manually" + json_schema_extra={ + "description": "Total batch size, we do not recommended setting this manually" }, ) eval_batch_size: Optional[int] = Field( default=None, - metadata={ - "help": "per gpu micro batch size for evals, defaults to value of micro_batch_size" + json_schema_extra={ + "description": "per gpu micro batch size for evals, defaults to value of micro_batch_size" }, ) @@ -441,12 +445,13 @@ class HyperparametersConfig(BaseModel): ] ] = OptimizerNames.ADAMW_HF.value optim_args: Optional[Union[str, Dict[str, Any]]] = Field( - default=None, metadata={"help": "Optional arguments to supply to optimizer."} + default=None, + json_schema_extra={"description": "Optional arguments to supply to optimizer."}, ) optim_target_modules: Optional[Union[List[str], Literal["all_linear"]]] = Field( default=None, - metadata={ - "help": "The target modules to optimize, i.e. the module names that you would like to train." + json_schema_extra={ + "description": "The target modules to optimize, i.e. the module names that you would like to train." }, ) torchdistx_path: Optional[str] = None @@ -506,15 +511,15 @@ class LISAConfig(BaseModel): lisa_n_layers: Optional[int] = Field( default=None, - metadata={"help": "the number of activate layers in LISA"}, + json_schema_extra={"description": "the number of activate layers in LISA"}, ) lisa_step_interval: Optional[int] = Field( default=None, - metadata={"help": "how often to switch layers in LISA"}, + json_schema_extra={"description": "how often to switch layers in LISA"}, ) lisa_layers_attribute: Optional[str] = Field( default="model.layers", - metadata={"help": "path under the model to access the layers"}, + json_schema_extra={"description": "path under the model to access the layers"}, ) @@ -613,7 +618,8 @@ class Config: pretraining_dataset: Optional[ # type: ignore conlist(Union[PretrainingDataset, SFTDataset], min_length=1) ] = Field( - default=None, metadata={"help": {"streaming dataset to use for pretraining"}} + default=None, + json_schema_extra={"description": "streaming dataset to use for pretraining"}, ) dataset_processes: Optional[int] = Field(default=os.cpu_count()) dataset_keep_in_memory: Optional[bool] = None @@ -673,7 +679,8 @@ class Config: sequence_len: int = Field(default=512) min_sample_len: Optional[int] = None max_prompt_len: int = Field( - default=512, metadata={"help": "maximum prompt length for RL training"} + default=512, + json_schema_extra={"description": "maximum prompt length for RL training"}, ) sample_packing: Optional[bool] = None sample_packing_group_size: Optional[int] = 100_000 @@ -692,8 +699,8 @@ class Config: pretrain_multipack_buffer_size: Optional[int] = 10_000 pretrain_multipack_attn: Optional[bool] = Field( default=True, - metadata={ - "help": "whether to prevent cross attention for packed sequences during pretraining", + json_schema_extra={ + "description": "whether to prevent cross attention for packed sequences during pretraining", }, ) From 6679e20f47f7e60ad7261ebe3cdd2ff1921c16ee Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 19 Nov 2024 10:35:59 -0500 Subject: [PATCH 0270/1405] release version 0.5.1 (#2082) --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 7920da1742..99dc646d31 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def parse_requirements(): setup( name="axolotl", - version="0.5.0", + version="0.5.1", description="LLM Trainer", long_description="Axolotl is a tool designed to streamline the fine-tuning of various AI models, offering support for multiple configurations and architectures.", package_dir={"": "src"}, @@ -108,7 +108,7 @@ def parse_requirements(): "flash-attn==2.7.0.post2", ], "deepspeed": [ - "deepspeed==0.14.4", + "deepspeed==0.15.4", "deepspeed-kernels", ], "mamba-ssm": [ From 5f6f9186e43b899e1f387bc90c4d4350b54686c5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 19 Nov 2024 10:43:02 -0500 Subject: [PATCH 0271/1405] make sure action has permission to create release (#2083) [skip ci] --- .github/workflows/pypi.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 33deccb29c..4816acaef0 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -10,6 +10,8 @@ jobs: setup_release: name: Create Release runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Get the tag version id: extract_branch From 02ca3f93b00dd44cd4dd688ff24e352e3b0276f9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 19 Nov 2024 11:31:56 -0500 Subject: [PATCH 0272/1405] set manifest and fix for source dist (#2084) --- MANIFEST.in | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000000..215fa9a9d3 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include requirements.txt +include README.md +include LICENSE +recursive-include axolotl *.py diff --git a/setup.py b/setup.py index 99dc646d31..9e90f3c273 100644 --- a/setup.py +++ b/setup.py @@ -100,7 +100,7 @@ def parse_requirements(): description="LLM Trainer", long_description="Axolotl is a tool designed to streamline the fine-tuning of various AI models, offering support for multiple configurations and architectures.", package_dir={"": "src"}, - packages=find_packages(), + packages=find_packages("src"), install_requires=install_requires, dependency_links=dependency_links, extras_require={ From e9c3a2aec07e59e2d87c8c85f0bc47b4dac45910 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 19 Nov 2024 12:43:30 -0500 Subject: [PATCH 0273/1405] add missing dunder-init for monkeypatches and add tests for install from sdist (#2085) * add missing dunder-init for monkeypatches and add tests for install from sdist * fix gha name * reduce matrix for sdist test --- .github/workflows/tests.yml | 46 ++++++++++++++++++- src/axolotl/monkeypatch/__init__.py | 0 src/axolotl/monkeypatch/attention/__init__.py | 0 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/axolotl/monkeypatch/__init__.py create mode 100644 src/axolotl/monkeypatch/attention/__init__.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6ae37f9882..b5c336246b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -77,12 +77,56 @@ jobs: run: | find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + pytest-sdist: + name: PyTest from Source Dist + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python_version: ["3.11"] + pytorch_version: ["2.4.1", "2.5.1"] + timeout-minutes: 20 + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python_version }} + cache: 'pip' # caching pip dependencies + + - name: upgrade pip + run: | + pip3 install --upgrade pip + pip3 install --upgrade packaging setuptools wheel + + - name: Install PyTorch + run: | + pip3 install torch==${{ matrix.pytorch_version }} + + - name: Install dependencies + run: | + pip3 show torch + python3 setup.py sdist + pip3 install dist/axolotl*.tar.gz + pip3 install -r requirements-dev.txt -r requirements-tests.txt + + - name: Run tests + run: | + pytest -n8 --ignore=tests/e2e/ tests/ + + - name: cleanup pip cache + run: | + find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + docker-e2e-tests-1st: if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] timeout-minutes: 90 - needs: [pre-commit, pytest] + needs: [pre-commit, pytest, pytest-sdist] strategy: fail-fast: false diff --git a/src/axolotl/monkeypatch/__init__.py b/src/axolotl/monkeypatch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/attention/__init__.py b/src/axolotl/monkeypatch/attention/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 8961364bc92f01b3fb06729e3800e5b7d5fd01e0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 19 Nov 2024 12:44:42 -0500 Subject: [PATCH 0274/1405] release 0.5.2 (#2086) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9e90f3c273..ea779d5996 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def parse_requirements(): setup( name="axolotl", - version="0.5.1", + version="0.5.2", description="LLM Trainer", long_description="Axolotl is a tool designed to streamline the fine-tuning of various AI models, offering support for multiple configurations and architectures.", package_dir={"": "src"}, From db51a9e4cb640552ebb8e178139a31b9bcdc5e98 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 19 Nov 2024 15:02:10 -0500 Subject: [PATCH 0275/1405] use pep440 instead of semver (#2088) [skip ci] --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 95a80e99da..b4344dfe20 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,7 +49,7 @@ jobs: axolotlai/axolotl tags: | type=ref,event=branch - type=semver,pattern={{version}} + type=pep440,pattern={{version}} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub @@ -116,7 +116,7 @@ jobs: axolotlai/axolotl-cloud tags: | type=ref,event=branch - type=semver,pattern={{version}} + type=pep440,pattern={{version}} - name: Login to Docker Hub uses: docker/login-action@v3 with: @@ -163,7 +163,7 @@ jobs: axolotlai/axolotl-cloud-term tags: | type=ref,event=branch - type=semver,pattern={{version}} + type=pep440,pattern={{version}} - name: Login to Docker Hub uses: docker/login-action@v3 with: From 68a26f1005e237712cad2e1e970486b6d63aca3e Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Thu, 21 Nov 2024 00:36:08 +0530 Subject: [PATCH 0276/1405] Fix duplication of plugin callbacks (#2090) --- src/axolotl/core/trainer_builder.py | 30 +++++++++++++---------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 14690580d7..75219a2749 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1212,11 +1212,17 @@ def get_post_trainer_create_callbacks(self, trainer): Callbacks added after the trainer is created, usually b/c these need access to the trainer """ callbacks = [] - - plugin_manager = PluginManager.get_instance() - callbacks.extend( - plugin_manager.add_callbacks_post_trainer(cfg=self.cfg, trainer=trainer) - ) + if self.cfg.plugins: + plugin_manager = PluginManager.get_instance() + callbacks.extend( + [ + cb + for cb in plugin_manager.add_callbacks_post_trainer( + self.cfg, trainer + ) + if cb + ] + ) return callbacks def hook_pre_create_training_args(self, training_arguments_kwargs): @@ -1263,7 +1269,7 @@ def get_callbacks(self): return callbacks def get_post_trainer_create_callbacks(self, trainer): - callbacks = super().get_post_trainer_create_callbacks(trainer=trainer) + callbacks = [] if self.cfg.use_wandb and self.cfg.eval_table_size > 0: LogPredictionCallback = log_prediction_callback_factory( trainer, self.tokenizer, "wandb" @@ -1301,17 +1307,7 @@ def get_post_trainer_create_callbacks(self, trainer): if self.cfg.lisa_step_interval and self.cfg.lisa_n_layers: callbacks.append(lisa_callback_factory(trainer)) - if self.cfg.plugins: - plugin_manager = PluginManager.get_instance() - callbacks.extend( - [ - cb - for cb in plugin_manager.add_callbacks_post_trainer( - self.cfg, trainer - ) - if cb - ] - ) + callbacks.extend(super().get_post_trainer_create_callbacks(trainer=trainer)) return callbacks def _get_trainer_cls(self): From 2e99bb303e3a135cc6695567c6232d5077144176 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 20 Nov 2024 14:07:54 -0500 Subject: [PATCH 0277/1405] fix inference when no chat_template is set, fix unsloth dora check (#2092) * fix inference when no chat_template is set, fix unsloth dora check * remove old unsloth version check * update docs on installing unsloth --- docs/unsloth.qmd | 6 ++-- scripts/unsloth_install.py | 33 +++++++++++++++++++ src/axolotl/cli/__init__.py | 9 ++++- src/axolotl/monkeypatch/unsloth_.py | 6 ++-- .../config/models/input/v0_4_1/__init__.py | 16 --------- 5 files changed, 46 insertions(+), 24 deletions(-) create mode 100644 scripts/unsloth_install.py diff --git a/docs/unsloth.qmd b/docs/unsloth.qmd index 90cb49bafa..73b2f03036 100644 --- a/docs/unsloth.qmd +++ b/docs/unsloth.qmd @@ -11,12 +11,10 @@ standard industry baselines. ### Installation -The following will install unsloth from source and downgrade xformers as unsloth is incompatible with the most up -to date libraries. +The following will install the correct unsloth and extras from source. ```bash -pip install --no-deps "unsloth @ git+https://github.com/unslothai/unsloth.git" -pip install --no-deps --force-reinstall xformers==0.0.26.post1 +python scripts/unsloth_install.py | sh ``` ### Using unsloth w Axolotl diff --git a/scripts/unsloth_install.py b/scripts/unsloth_install.py new file mode 100644 index 0000000000..66b983e72d --- /dev/null +++ b/scripts/unsloth_install.py @@ -0,0 +1,33 @@ +# noqa +# pylint: skip-file +try: + import torch +except ImportError: + raise ImportError("Install torch via `pip install torch`") +from packaging.version import Version as V + +v = V(torch.__version__) +cuda = str(torch.version.cuda) +is_ampere = torch.cuda.get_device_capability()[0] >= 8 +if cuda != "12.1" and cuda != "11.8" and cuda != "12.4": + raise RuntimeError(f"CUDA = {cuda} not supported!") +if v <= V("2.1.0"): + raise RuntimeError(f"Torch = {v} too old!") +elif v <= V("2.1.1"): + x = "cu{}{}-torch211" +elif v <= V("2.1.2"): + x = "cu{}{}-torch212" +elif v < V("2.3.0"): + x = "cu{}{}-torch220" +elif v < V("2.4.0"): + x = "cu{}{}-torch230" +elif v < V("2.5.0"): + x = "cu{}{}-torch240" +elif v < V("2.6.0"): + x = "cu{}{}-torch250" +else: + raise RuntimeError(f"Torch = {v} too new!") +x = x.format(cuda.replace(".", ""), "-ampere" if is_ampere else "") +print( + f'pip install unsloth-zoo && pip install --no-deps "unsloth[{x}] @ git+https://github.com/unslothai/unsloth.git"' +) diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 589b6b575a..7c8db7ce8e 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -30,7 +30,10 @@ from axolotl.integrations.base import PluginManager from axolotl.logging_config import configure_logging from axolotl.train import TrainDatasetMeta -from axolotl.utils.chat_templates import get_chat_template +from axolotl.utils.chat_templates import ( + get_chat_template, + get_chat_template_from_config, +) from axolotl.utils.comet_ import setup_comet_env_vars from axolotl.utils.config import ( normalize_cfg_datasets, @@ -199,6 +202,10 @@ def do_inference( ) elif cfg.chat_template: chat_template_str = get_chat_template(cfg.chat_template) + elif cfg.datasets[0].type == "chat_template": + chat_template_str = get_chat_template_from_config( + cfg=cfg, ds_cfg=cfg.datasets[0], tokenizer=tokenizer + ) model = model.to(cfg.device, dtype=cfg.torch_dtype) diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py index c8272ac735..38bbdc88fb 100644 --- a/src/axolotl/monkeypatch/unsloth_.py +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -188,7 +188,7 @@ def integrate_lora_mlp_patch(peft_model: PeftModelForCausalLM): for module in layer_modules ) mlp_not_dora = all( - getattr(module, "lora_magnitude_vector", None) is None + len(getattr(module, "lora_magnitude_vector", []) or []) == 0 for module in layer_modules ) @@ -213,7 +213,7 @@ def integrate_lora_patch(peft_model: PeftModelForCausalLM, cfg): for module in layer_modules ) qkv_not_dora = all( - getattr(module, "lora_magnitude_vector", None) is None + len(getattr(module, "lora_magnitude_vector", []) or []) == 0 for module in layer_modules ) @@ -232,7 +232,7 @@ def integrate_lora_patch(peft_model: PeftModelForCausalLM, cfg): for module in layer_modules ) o_not_dora = all( - getattr(module, "lora_magnitude_vector", None) is None + len(getattr(module, "lora_magnitude_vector", []) or []) == 0 for module in layer_modules ) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 9d21a294c1..f4420ae2cd 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -7,7 +7,6 @@ import logging import os from enum import Enum -from importlib.metadata import version from typing import Annotated, Any, Dict, List, Literal, Optional, Tuple, Union from pydantic import ( @@ -1425,21 +1424,6 @@ def check_qlora_unsloth(cls, data): ) return data - @model_validator(mode="before") - @classmethod - def check_unsloth_xformers_version(cls, data): - if ( - data.get("unsloth_lora_mlp") - or data.get("unsloth_lora_qkv") - or data.get("unsloth_lora_o") - ): - xformers_version = version("xformers") - if xformers_version == "0.0.27": - raise ValueError( - "xformers version 0.0.27 is not supported with unsloth. Please downgrade to 0.0.26.post1" - ) - return data - @model_validator(mode="before") @classmethod def check_torch_compile_deepspeed(cls, data): From 838b74d05b8e35ea289233ec62b6ad9b90b9a460 Mon Sep 17 00:00:00 2001 From: Mengqing Cao Date: Thu, 21 Nov 2024 10:28:41 +0800 Subject: [PATCH 0278/1405] Add Ascend NPU support (#1758) --- src/axolotl/utils/bench.py | 15 +++++- src/axolotl/utils/config/__init__.py | 8 ++- .../config/models/input/v0_4_1/__init__.py | 35 +++++++++++++ src/axolotl/utils/distributed.py | 52 ++++++++++++++++--- src/axolotl/utils/models.py | 20 ++++--- 5 files changed, 114 insertions(+), 16 deletions(-) diff --git a/src/axolotl/utils/bench.py b/src/axolotl/utils/bench.py index 11c25160da..57471ae0d8 100644 --- a/src/axolotl/utils/bench.py +++ b/src/axolotl/utils/bench.py @@ -4,6 +4,9 @@ import pynvml import torch from pynvml.nvml import NVMLError +from transformers.utils.import_utils import is_torch_npu_available + +from axolotl.utils.distributed import get_device_type def check_cuda_device(default_value): @@ -53,6 +56,12 @@ def mps_memory_usage_all(): return usage, reserved - usage, 0 +def npu_memory_usage_all(device=0): + usage = torch.npu.memory_allocated(device) / 1024.0**3 + reserved = torch.npu.memory_reserved(device) / 1024.0**3 + return usage, reserved - usage, 0 + + @check_cuda_device(0.0) def gpu_memory_usage_smi(device=0): if isinstance(device, torch.device): @@ -69,8 +78,11 @@ def gpu_memory_usage_smi(device=0): def log_gpu_memory_usage(log, msg, device): + cur_device = get_device_type() if torch.backends.mps.is_available(): usage, cache, misc = mps_memory_usage_all() + elif "npu" in str(cur_device) and is_torch_npu_available(): + usage, cache, misc = npu_memory_usage_all(device) else: usage, cache, misc = gpu_memory_usage_all(device) extras = [] @@ -79,6 +91,7 @@ def log_gpu_memory_usage(log, msg, device): if misc > 0: extras.append(f"+{misc:.03f}GB misc") log.info( - f"GPU memory usage {msg}: {usage:.03f}GB ({', '.join(extras)})", stacklevel=2 + f"{str(cur_device)} memory usage {msg}: {usage:.03f}GB ({', '.join(extras)})", + stacklevel=2, ) return usage, cache, misc diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index b12ad81136..0100f23ea5 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -5,6 +5,7 @@ import torch from transformers.utils import is_torch_bf16_gpu_available +from transformers.utils.import_utils import is_torch_npu_available from axolotl.integrations.config import merge_input_args from axolotl.utils.bench import log_gpu_memory_usage @@ -29,7 +30,10 @@ def get_device(): if torch.backends.mps.is_available(): return "mps" - raise SystemError("No CUDA/mps device found") + if is_torch_npu_available(): + return f"npu:{cfg.local_rank}" + + raise SystemError("No CUDA/mps/npu device found") except Exception: # pylint: disable=broad-exception-caught return "cpu" @@ -39,6 +43,8 @@ def get_device(): else: if cfg.device.startswith("cuda"): cfg.device_map = {"": torch.cuda.current_device()} + elif cfg.device.startswith("npu"): + cfg.device_map = {"npu": torch.npu.current_device()} else: cfg.device_map = {"": cfg.device} diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index f4420ae2cd..42cbe52c14 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -19,6 +19,7 @@ ) from transformers import SchedulerType from transformers.training_args import OptimizerNames +from transformers.utils.import_utils import is_torch_npu_available from axolotl.utils.config.models.internals import GPUCapabilities @@ -1433,6 +1434,40 @@ def check_torch_compile_deepspeed(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_npu_config(cls, data): + if is_torch_npu_available(): + # check attention config + attn_list = ["flash_attention", "sdp_attention", "s2_attention"] + for attn in attn_list: + if data.get(attn): + raise NotImplementedError( + f"{attn} is currently not supported in Ascend npu, please disable this configuration." + ) + + # check quant config + if data.get("optimizer") is not None and "bit" in data.get("optimizer"): + optimizer = data.get("optimizer") + raise NotImplementedError( + f"{optimizer} is currently not supported in Ascend npu, choose another one please." + ) + + quant_list = ["load_in_8bit", "load_in_4bit"] + for quant in quant_list: + if data.get(quant): + raise NotImplementedError( + f"Quantification is currently not supported in Ascend npu, please disable {quant}." + ) + + # check dtype config + if data.get("tf32"): + raise NotImplementedError( + "tf32 dtype is currently not supported in Ascend npu, please disable this configuration" + ) + + return data + class AxolotlConfigWCapabilities(AxolotlInputConfig): """wrapper to valdiate gpu capabilities with the configured options""" diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index 3a559f5f51..81a928b6ec 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -9,10 +9,44 @@ import torch import torch.distributed as dist from accelerate import PartialState +from transformers.utils.import_utils import ( + is_torch_cuda_available, + is_torch_mps_available, + is_torch_npu_available, +) distributed_state = None # pylint: disable=invalid-name +def get_device_type(): + device = torch.device("cpu") + if is_torch_cuda_available(): + device = torch.device("cuda") + elif is_torch_mps_available(): + device = torch.device("mps") + elif is_torch_npu_available(): + device = torch.device("npu") + return device + + +def get_device_count(): + cur_device = get_device_type() + if "cuda" in str(cur_device): + return torch.cuda.device_count() + if "npu" in str(cur_device): + return torch.npu.device_count() + return 1 + + +def get_current_device(): + cur_device = get_device_type() + if "cuda" in str(cur_device): + return torch.cuda.current_device() + if "npu" in str(cur_device): + return torch.npu.current_device() + return 0 + + def is_distributed(): """ Check if distributed training is initialized. @@ -91,7 +125,7 @@ def gather_scalar_from_all_ranks(fn, world_size=1): # pylint: disable=invalid-n if not is_distributed(): return [value_scalar] value_tensor = torch.tensor( - value_scalar, device=torch.cuda.current_device() + value_scalar, device=f"{get_device_type()}:{get_current_device()}" ).float() if not is_main_process(): @@ -115,13 +149,14 @@ def broadcast_dict(vals: dict): if not is_distributed(): return vals + cur_device = get_device_type() if is_main_process(): data_byte = pickle.dumps(vals) - data_tensor = torch.ByteTensor(list(data_byte)).to("cuda") - data_size = torch.IntTensor([len(data_byte)]).to("cuda") + data_tensor = torch.ByteTensor(list(data_byte)).to(cur_device) + data_size = torch.IntTensor([len(data_byte)]).to(cur_device) else: - data_tensor = torch.empty([1024], dtype=torch.uint8, device="cuda") - data_size = torch.IntTensor([0]).to("cuda") + data_tensor = torch.empty([1024], dtype=torch.uint8, device=cur_device) + data_size = torch.IntTensor([0]).to(cur_device) dist.broadcast(data_size, 0) if not is_main_process(): @@ -150,14 +185,15 @@ def compute_and_broadcast(fn): # pylint: disable=invalid-name Returns: - The computed value (int or float). """ + cur_device = f"{get_device_type()}:{get_current_device()}" if is_main_process(): value_scalar = fn() value_tensor = torch.tensor( - value_scalar, device=torch.cuda.current_device(), dtype=torch.float32 + value_scalar, device=cur_device, dtype=torch.float32 ) else: value_tensor = torch.tensor( - 0.0, device=torch.cuda.current_device(), dtype=torch.float32 + 0.0, device=cur_device, dtype=torch.float32 ) # Placeholder tensor # Broadcast the tensor to all processes. @@ -184,7 +220,7 @@ def gather_from_all_ranks(fn, world_size=1): # pylint: disable=invalid-name """ value_scalar = fn() value_tensor = torch.tensor( - value_scalar, device=torch.cuda.current_device() + value_scalar, device=f"{get_device_type()}:{get_current_device()}" ).float() # Placeholder tensor for gathering results diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 75c93fa2a5..082df7c27b 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -55,7 +55,7 @@ from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.chat_templates import get_chat_template_from_config from axolotl.utils.dict import DictDefault -from axolotl.utils.distributed import zero_only +from axolotl.utils.distributed import get_device_count, get_device_type, zero_only from axolotl.utils.gradient_checkpointing import hf_grad_checkpoint_unsloth_wrapper from axolotl.utils.lora_embeddings import get_linear_embedding_layers from axolotl.utils.model_shard_quant import load_sharded_model, load_sharded_model_quant @@ -570,7 +570,8 @@ def set_device_map_config(self) -> None: ) max_memory = {} - for i in range(torch.cuda.device_count()): + num_device = get_device_count() + for i in range(num_device): max_memory[i] = gpu_memory_limit max_memory["cpu"] = "256GiB" # something sufficiently large to fit anything @@ -595,8 +596,11 @@ def set_device_map_config(self) -> None: self.model_kwargs["device_map"] = device_map self.model_kwargs["torch_dtype"] = self.cfg.torch_dtype - if torch.backends.mps.is_available(): + cur_device = get_device_type() + if "mps" in str(cur_device): self.model_kwargs["device_map"] = "mps:0" + elif "npu" in str(cur_device): + self.model_kwargs["device_map"] = "npu:0" # TODO can we put the reference model on it's own gpu? I think we have to move logits around to calculate loss # if cfg.rl: @@ -1050,7 +1054,11 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: self.ajust_model_config() # log device memory usage - if hasattr(self.model, "device") and self.model.device.type in ("cuda", "mps"): + if hasattr(self.model, "device") and self.model.device.type in ( + "cuda", + "mps", + "npu", + ): log_gpu_memory_usage(LOG, "after model load", self.model.device) # make sure these are fp32 per Ramesh et al. (2021) @@ -1118,9 +1126,9 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: and not skip_move_to_device ): # TODO revaldate this conditional - self.model.to(f"cuda:{self.cfg.local_rank}") + self.model.to(f"{str(get_device_type())}:{self.cfg.local_rank}") - if torch.cuda.device_count() > 1 and int(os.getenv("WORLD_SIZE", "1")) == 1: + if get_device_count() > 1 and int(os.getenv("WORLD_SIZE", "1")) == 1: setattr(self.model, "is_parallelizable", True) setattr(self.model, "model_parallel", True) From bf416bdfd0fde9b9449ea4cd9a949b191d4f56cb Mon Sep 17 00:00:00 2001 From: Sunny Liu <22844540+bursteratom@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:24:52 -0500 Subject: [PATCH 0279/1405] bump_liger_0.4.2 (#2096) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 60b5333727..f2086d4427 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ tensorboard python-dotenv==1.0.1 autoawq==0.2.7.post2 triton>=2.3.0 -liger-kernel==0.4.1 +liger-kernel==0.4.2 mamba-ssm==1.2.0.post1 From 151abb7a672a63a29e42fccd7ed57f4abd8b3337 Mon Sep 17 00:00:00 2001 From: Sunny Liu <22844540+bursteratom@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:36:51 -0500 Subject: [PATCH 0280/1405] =?UTF-8?q?fix=20None-type=20not=20iterable=20er?= =?UTF-8?q?ror=20when=20deepspeed=20is=20left=20blank=20w/=20use=5F?= =?UTF-8?q?=E2=80=A6=20(#2087)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix None-type not iterable error when deepspeed is left blank w/ use_reentrant: false and qlora * added unit test[skip e2e] * corrected test case[skip e2e] * assert warning message [skip e2e] * assert warning message [skip e2e] * corrected test cases [skip e2e] * lint --- .../config/models/input/v0_4_1/__init__.py | 1 + tests/test_validation.py | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 42cbe52c14..cdbe47b8f1 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -1314,6 +1314,7 @@ def warn_qlora_zero3_w_use_reentrant(cls, data): and data.get("gradient_checkpointing_kwargs", {}) and data.get("gradient_checkpointing_kwargs", {}).get("use_reentrant") is False + and data.get("deepspeed", "") is not None and "zero3" in data.get("deepspeed", "") ): # may result in: diff --git a/tests/test_validation.py b/tests/test_validation.py index f3f4d18ab8..491f230c33 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -68,6 +68,53 @@ def test_defaults(self, minimal_cfg): assert cfg.train_on_inputs is False assert cfg.weight_decay is None + def test_zero3_qlora_use_reentrant_false(self, minimal_cfg): + test_cfg = DictDefault( + { + "deepspeed": "deepspeed_configs/zero3_bf16.json", + "gradient_checkpointing": True, + "gradient_checkpointing_kwargs": {"use_reentrant": False}, + "load_in_4bit": True, + "adapter": "qlora", + } + | minimal_cfg + ) + + with self._caplog.at_level(logging.WARNING): + validate_config(test_cfg) + assert ( + "qlora + zero3 with use_reentrant: false may result in a CheckpointError about recomputed values" + in self._caplog.records[0].message + ) + + def test_deepspeed_empty(self, minimal_cfg): + test_cfg = DictDefault( + { + "deepspeed": "", + "gradient_checkpointing": True, + "gradient_checkpointing_kwargs": {"use_reentrant": False}, + "load_in_4bit": True, + "adapter": "qlora", + } + | minimal_cfg + ) + + _ = validate_config(test_cfg) + + def test_deepspeed_not_set(self, minimal_cfg): + test_cfg = DictDefault( + { + "deepspeed": None, + "gradient_checkpointing": True, + "gradient_checkpointing_kwargs": {"use_reentrant": False}, + "load_in_4bit": True, + "adapter": "qlora", + } + | minimal_cfg + ) + + _ = validate_config(test_cfg) + def test_datasets_min_length(self): cfg = DictDefault( { From 94fc223f6cc65f182f24c61d498095cfb5568158 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 21 Nov 2024 14:32:41 -0500 Subject: [PATCH 0281/1405] actions/create-release is unmaintained, and doesn't create proper release notes (#2098) [skip ci] --- .github/workflows/pypi.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 4816acaef0..752ffefdfe 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -13,19 +13,10 @@ jobs: permissions: contents: write steps: - - name: Get the tag version - id: extract_branch - run: echo ::set-output name=branch::${GITHUB_REF#refs/tags/} - shell: bash - - - name: Create Release - id: create_release - uses: actions/create-release@v1 + - name: Create release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.extract_branch.outputs.branch }} - release_name: ${{ steps.extract_branch.outputs.branch }} + run: gh release create "$GITHUB_REF_NAME" # GITHUB_REF_NAME is the tag name in `on.push.tags` workflows pypi-publish: name: Upload release to PyPI runs-on: ubuntu-latest From 45c0825587d46c2d9200ca8aebfbd8cb2115f379 Mon Sep 17 00:00:00 2001 From: Sunny Liu <22844540+bursteratom@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:09:10 -0500 Subject: [PATCH 0282/1405] updated colab notebook (#2074) * updated colab notebook * update pip installtation * cleared cell output * Update examples/colab-notebooks/colab-axolotl-example.ipynb Co-authored-by: NanoCode012 * Update examples/colab-notebooks/colab-axolotl-example.ipynb Co-authored-by: NanoCode012 * Update examples/colab-notebooks/colab-axolotl-example.ipynb Co-authored-by: NanoCode012 * Update examples/colab-notebooks/colab-axolotl-example.ipynb Co-authored-by: NanoCode012 * modified notebook * Update examples/colab-notebooks/colab-axolotl-example.ipynb Co-authored-by: NanoCode012 * Update examples/colab-notebooks/colab-axolotl-example.ipynb Co-authored-by: NanoCode012 * Update examples/colab-notebooks/colab-axolotl-example.ipynb Co-authored-by: NanoCode012 * Update examples/colab-notebooks/colab-axolotl-example.ipynb Co-authored-by: NanoCode012 * Update examples/colab-notebooks/colab-axolotl-example.ipynb Co-authored-by: NanoCode012 * Update examples/colab-notebooks/colab-axolotl-example.ipynb Co-authored-by: NanoCode012 * cleared cell output * cleared unnecessary logs --------- Co-authored-by: NanoCode012 --- .../colab-axolotl-example.ipynb | 301 +++++++++++++----- 1 file changed, 218 insertions(+), 83 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 0acaf7961c..bfd1709a7f 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -2,19 +2,15 @@ "cells": [ { "cell_type": "markdown", - "metadata": { - "id": "AKjdG7tbTb-n" - }, + "metadata": {}, "source": [ - "# Example notebook for running Axolotl on google colab" + "## Setting up" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "id": "RcbNpOgWRcii" - }, + "metadata": {}, "outputs": [], "source": [ "import torch\n", @@ -22,82 +18,76 @@ "assert (torch.cuda.is_available()==True)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install axolotl[deepspeed]" + ] + }, { "cell_type": "markdown", - "metadata": { - "id": "h3nLav8oTRA5" - }, + "metadata": {}, "source": [ - "## Install Axolotl and dependencies" + "## Hugging Face login (optional)" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "3c3yGAwnOIdi", - "outputId": "e3777b5a-40ef-424f-e181-62dfecd1dd01" - }, + "metadata": {}, "outputs": [], "source": [ - "!pip install -e git+https://github.com/axolotl-ai-cloud/axolotl#egg=axolotl\n", - "!pip install flash-attn==\"2.7.0.post2\"\n", - "!pip install deepspeed==\"0.13.1\"!pip install mlflow==\"2.13.0\"" + "from huggingface_hub import notebook_login\n", + "notebook_login()" ] }, { "cell_type": "markdown", - "metadata": { - "id": "BW2MFr7HTjub" - }, + "metadata": {}, "source": [ - "## Create an yaml config file" + "## Example configuration" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "id": "9pkF2dSoQEUN" - }, + "metadata": {}, "outputs": [], "source": [ "import yaml\n", "\n", - "# Your YAML string\n", "yaml_string = \"\"\"\n", - "base_model: TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T\n", - "model_type: LlamaForCausalLM\n", - "tokenizer_type: LlamaTokenizer\n", + "base_model: NousResearch/Meta-Llama-3.1-8B\n", "\n", "load_in_8bit: false\n", "load_in_4bit: true\n", "strict: false\n", "\n", "datasets:\n", - " - path: mhenrichsen/alpaca_2k_test\n", + " - path: tatsu-lab/alpaca\n", " type: alpaca\n", - "dataset_prepared_path:\n", + "dataset_prepared_path: last_run_prepared\n", "val_set_size: 0.05\n", - "output_dir: ./outputs/qlora-out\n", - "\n", - "adapter: qlora\n", - "lora_model_dir:\n", + "output_dir: ./outputs/lora-out\n", "\n", - "sequence_len: 4096\n", + "sequence_len: 2048\n", "sample_packing: true\n", - "eval_sample_packing: false\n", + "eval_sample_packing: true\n", "pad_to_sequence_len: true\n", "\n", + "adapter: qlora\n", + "lora_model_dir:\n", "lora_r: 32\n", "lora_alpha: 16\n", "lora_dropout: 0.05\n", - "lora_target_modules:\n", "lora_target_linear: true\n", "lora_fan_in_fan_out:\n", + "lora_modules_to_save:\n", + " - embed_tokens\n", + " - lm_head\n", "\n", "wandb_project:\n", "wandb_entity:\n", @@ -105,12 +95,12 @@ "wandb_name:\n", "wandb_log_model:\n", "\n", - "gradient_accumulation_steps: 4\n", - "micro_batch_size: 2\n", - "num_epochs: 4\n", - "optimizer: paged_adamw_32bit\n", + "gradient_accumulation_steps: 2\n", + "micro_batch_size: 1\n", + "num_epochs: 1\n", + "optimizer: paged_adamw_8bit\n", "lr_scheduler: cosine\n", - "learning_rate: 0.0002\n", + "learning_rate: 2e-5\n", "\n", "train_on_inputs: false\n", "group_by_length: false\n", @@ -121,13 +111,15 @@ "gradient_checkpointing: true\n", "early_stopping_patience:\n", "resume_from_checkpoint:\n", - "local_rank:\n", "logging_steps: 1\n", "xformers_attention:\n", - "flash_attention: true\n", + "flash_attention: false\n", + "sdp_attention: true\n", "\n", - "warmup_steps: 10\n", - "evals_per_epoch: 4\n", + "warmup_steps: 1\n", + "max_steps: 25\n", + "evals_per_epoch: 1\n", + "eval_table_size:\n", "saves_per_epoch: 1\n", "debug:\n", "deepspeed:\n", @@ -135,9 +127,10 @@ "fsdp:\n", "fsdp_config:\n", "special_tokens:\n", - "\n", + " pad_token: <|end_of_text|>\n", "\"\"\"\n", "\n", + "\n", "# Convert the YAML string to a Python dictionary\n", "yaml_dict = yaml.safe_load(yaml_string)\n", "\n", @@ -146,31 +139,124 @@ "\n", "# Write the YAML file\n", "with open(file_path, 'w') as file:\n", - " yaml.dump(yaml_dict, file)\n" + " yaml.dump(yaml_dict, file)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Above we have a configuration file with base LLM model and datasets specified, among many other things. Axolotl can automatically detect whether the specified datasets are on HuggingFace repo or local machine.\n", + "\n", + "The Axolotl configuration options encompass model and dataset selection, data pre-processing, and training. Let's go through them line by line:\n", + "\n", + "* \"base model\": String value, specifies the underlying pre-trained LLM that will be used for finetuning\n", + "\n", + "Next we have options for model weights quantization. Quantization allows for reduction in occupied memory on GPUs.\n", + "\n", + "* \"load_in_8bit\": Boolean value, whether to quantize the model weights into 8-bit integer.\n", + "\n", + "* \"load_in_4bit\": Boolean value, whether to quantize the model weights into 4-bit integer.\n", + "\n", + "* \"strict\": Boolean value. If false, it allows for overriding established configuration options in the yaml file when executing in command-line interface.\n", + "\n", + "* \"datasets\": a list of dicts that contain path and type of data sets as well as other optional configurations where datasets are concerned. Supports multiple datasets.\n", + "\n", + "* \"val_set_size\": Either a float value less than one or an integer less than the total size of dataset. Sets the size of validation set from the whole dataset. If float, sets the proportion of the dataset assigned for validation. If integer, sets the direct size of validation set.\n", + "\n", + "* \"output_dir\": String value. Path of trained model.\n", + "\n", + "For data preprocessing:\n", + "\n", + "* \"sequence_len\": Integer. Specifies the maximum sequence length of the input. Typically 2048 or less.\n", + "\n", + "* \"pad_to_sequence_len\": Boolean. Padding input to maximum sequence length.\n", + "\n", + "* \"sample_packing\": Boolean. Specifies whether to use multi-packing with block diagonal attention.\n", + "\n", + "* \"special_tokens\": Python dict, optional. Allows users to specify the additional special tokens to be ignored by the tokenizer.\n", + "\n", + "For LoRA configuration and its hyperparamters:\n", + "\n", + "* \"adapter\": String. Either \"lora\" or \"qlora\", depending on user's choice.\n", + "\n", + "* \"lora_model_dir\": String, Optional. Path to directory that contains LoRA model, if there is already a trained LoRA model the user would like to use.\n", + "\n", + "* \"lora_r\": Integer. Refers to the rank of LoRA decomposition matrices. Higher value will reduce LoRA efficiency. Recommended to be set to 8.\n", + "\n", + "* \"lora_alpha\": Integer. Scale the weight matrices by $\\frac{\\text{lora_alpha}}{\\text{lora_r}}$Recommended to be fixed at 16.\n", + "\n", + "* \"lora_dropout\": Float that is 1 or less. The dropout probability of a lora layer.\n", + "\n", + "* \"lora_target_linear\": Boolean. If true, lora will target all linear modules in the transformers architecture.\n", + "\n", + "* \"lora_modules_to_save\": If you added new tokens to the tokenizer, you may need to save some LoRA modules because they need to know the new tokens.\n", + "\n", + "See [LoRA](https://arxiv.org/abs/2106.09685) for detailed explanation of LoRA implementation.\n", + "\n", + "For the training configurations:\n", + "\n", + "* \"gradient_accumulation_steps\": Integer. The number of steps over which to accumulate gradient for batch training. E.g. if 2, backprop is performed every two steps.\n", + "\n", + "* \"micro_batch_size\": Integer. Batch size per gpu / gradient_accumulation_steps\n", + "\n", + "* \"num_epochs\": Integer. Number of epochs. One epoch is when training has looped over every batch in the whole data set once.\n", + "\n", + "* \"optimizer\": The optimizer to use for the training.\n", + "\n", + "* \"learning_rate\": The learning rate.\n", + "\n", + "* \"lr_scheduler\": The learning rate scheduler to use for adjusting learning rate during training.\n", + "\n", + "* \"train_on_inputs\": Boolean. Whether to ignore or include the user's prompt from the training labels.\n", + "\n", + "* \"group_by_length\": Boolean. Whether to group similarly sized data to minimize padding.\n", + "\n", + "* \"bf16\": Either \"auto\", \"true\", or \"false\". Whether to use CUDA bf16 floating point format. If set to \"auto\", will automatically apply bf16 should the gpu supports it.\n", + "\n", + "* \"fp16\": Optional. Specifies whether to use CUDA fp16. Automatically set to true if \"bf16\" is set to true. Otherwise false.\n", + "\n", + "* \"tf32\": Boolean. Whether to use CUDA tf32. Will override bf16.\n", + "\n", + "* \"gradient_checkpointing\": Boolean. Whether to use gradient checkpointing https://huggingface.co/docs/transformers/v4.18.0/en/performance#gradient-checkpointing\n", + "\n", + "* \"gradient_checkpointing_kwargs\": Python Dict. Fed into the trainer.\n", + "\n", + "* \"logging_steps\": Integer. Log training information over every specified number of steps.\n", + "\n", + "* \"flash_attention\": Boolean. Whether to use the [flash attention](https://github.com/Dao-AILab/flash-attention) mechanism.\n", + "\n", + "* \"sdp_attention\": Boolean. Whether to use the Scaled Dot Product attention mechanism (the attention mechanism in the [original implementation](https://arxiv.org/abs/1706.03762) of transformers.)\n", + "\n", + "* \"warmup_steps\": Integer. The number of pre-training steps where a very low learning rate is used.\n", + "\n", + "* \"evals_per_epoch\": Integer. Number of evaluations to be performed within one training epoch.\n", + "\n", + "* \"saves_per_epoch\": Integer. Number of times the model is saved in one training epoch.\n", + "\n", + "* \"weight_decay\": Positive Float. Sets the \"strength\" of weight decay (i.e. setting the coefficient of L2 regularization)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above is but a snippet aiming to get users familiarized with the types of streamlined configuration options axolotl provides. For a full list of configuration options, see [here](https://axolotl-ai-cloud.github.io/axolotl/docs/config.html)" ] }, { "cell_type": "markdown", - "metadata": { - "id": "bidoj8YLTusD" - }, + "metadata": {}, "source": [ - "## Launch the training" + "Train the model" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "ydTI2Jk2RStU", - "outputId": "d6d0df17-4b53-439c-c802-22c0456d301b" - }, + "metadata": {}, "outputs": [], "source": [ - "# By using the ! the comand will be executed as a bash command\n", "!accelerate launch -m axolotl.cli.train /content/test_axolotl.yaml" ] }, @@ -178,7 +264,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Play with inference" + "Predict with trained model" ] }, { @@ -187,36 +273,85 @@ "metadata": {}, "outputs": [], "source": [ - "# By using the ! the comand will be executed as a bash command\n", "!accelerate launch -m axolotl.cli.inference /content/test_axolotl.yaml \\\n", - " --qlora_model_dir=\"./qlora-out\" --gradio" + " --lora_model_dir=\"./outputs/lora-out\" --gradio" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Deeper Dive" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is also helpful to gain some familiarity over some of the core inner workings of axolotl" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuration Normalization" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Axolotl uses a custom Dict class, called ```DictDefault```\n", + "to store configurations specified in the yaml configuration file (into a Python variable named ```cfg```). The definition for this custom Dict can be found in the [utils/dict.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/utils/dict.py)\n", + "\n", + "```DictDefault``` is amended such that calling a missing key from it will result in a ```None``` return type. This is important because if some configuration options aren't specified by the user, the ```None``` type allows Axolotl to perform boolean operations to determine the default settings for missing configurations. For more examples on how this is done, check out [utils/config/__init__.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/utils/config/__init__.py)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Loading Models, Tokenizers, and Trainer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we inspect [cli.train.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/cli/train.py), we will find that most of the heavy lifting were done by the function ```train()``` which is itself imported from [src/axolotl/train.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/train.py).\n", + "\n", + "```train()``` takes care of loading the appropriate tokenizer and pre-trained model through ```load_model()``` and ```load_tokenizer()``` from [src/axolotl/utils/models.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/utils/models.py) respectively.\n", + "\n", + "```load_tokenizer()``` loads in the appropriate tokenizer given the desired model, as well as chat templates.\n", + "\n", + "```ModelLoader``` class follows after tokenizer has been selected. It will automatically discern the base model type, load in the desired model, as well as applying model-appropriate attention mechanism modifications (e.g. flash attention). Depending on which base model the user chooses in the configuration, ```ModelLoader``` will utilize the corresponding \"attention hijacking\" script. For example, if the user specified the base model to be ```NousResearch/Meta-Llama-3.1-8B```, which is of llama type, and set ```flash_attn``` to ```True```, ```ModelLoader``` will load in [llama_attn_hijack_flash.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/monkeypatch/llama_attn_hijack_flash.py). For a list of supported attention hijacking, please refer to the directory [/src/axolotl/monkeypatch/](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/monkeypatch)\n", + "\n", + "Another important operation encompassed in ```train()``` is setting up the training that takes into account of user-specified traning configurations (e.g. num_epochs, optimizer) through the use of ```setup_trainer()``` from [/src/axolotl/utils/trainer.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/utils/trainer.py), which in turn relies on modules from [/src/axolotl/core/trainer_builder.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/core/trainer_builder.py).\n", + "```trainer_builder.py``` provides a list of trainer object options bespoke for the task type (Causal or Reinforcement learning ('dpo', 'ipo', 'kto') )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Monkey patch\n", + "\n", + "The [Monkey patch directory](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/monkeypatch) is where model architecture/optimization patching scripts are stored (these are modifications that are not implemented in the official releases, hence the name monkey patch). It includes attention jacking, ReLoRA, and unsloth optimization." ] } ], "metadata": { - "accelerator": "GPU", - "colab": { - "gpuType": "T4", - "provenance": [] - }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.1" + "version": "3.9.6" } }, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 2 } From 51c9e1a035eca3aa251b973a8f977548a791cea9 Mon Sep 17 00:00:00 2001 From: Aman Karmani Date: Fri, 22 Nov 2024 08:08:54 -0800 Subject: [PATCH 0283/1405] .gitignore improvements (#349) [skip ci] --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d15f2000f3..bfbd22de06 100644 --- a/.gitignore +++ b/.gitignore @@ -182,3 +182,6 @@ submit.sh typings/ out/ + +# vim +*.swp From 724b660d5632adc842e062c1588b325211ce48a1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 22 Nov 2024 15:05:42 -0500 Subject: [PATCH 0284/1405] move shared pytest conftest to top level tests (#2099) [skip ci] * move shared pytest conftest to top level tests * add __init__ so mypy doesn't choke on multiple conftests --- tests/{e2e => }/conftest.py | 0 tests/prompt_strategies/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/{e2e => }/conftest.py (100%) create mode 100644 tests/prompt_strategies/__init__.py diff --git a/tests/e2e/conftest.py b/tests/conftest.py similarity index 100% rename from tests/e2e/conftest.py rename to tests/conftest.py diff --git a/tests/prompt_strategies/__init__.py b/tests/prompt_strategies/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 6e0fb4a6b2d57d873e92847abb1b43e84a890aef Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 29 Nov 2024 20:37:32 -0500 Subject: [PATCH 0285/1405] add finetome dataset to fixtures, check eval_loss in test (#2106) [skip ci] * add finetome dataset to fixtures, check eval_loss in test * add qwen 0.5b to pytest session fixture --- tests/conftest.py | 11 +++++++++++ tests/e2e/multigpu/test_eval.py | 29 +++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c316f6c83e..a8bf03ac01 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,12 @@ def download_smollm2_135m_model(): snapshot_download("HuggingFaceTB/SmolLM2-135M") +@pytest.fixture(scope="session", autouse=True) +def download_qwen_2_5_half_billion_model(): + # download the model + snapshot_download("Qwen/Qwen2.5-0.5B") + + @pytest.fixture(scope="session", autouse=True) def download_tatsu_lab_alpaca_dataset(): # download the model @@ -26,6 +32,11 @@ def download_mhenrichsen_alpaca_2k_dataset(): snapshot_download("mhenrichsen/alpaca_2k_test", repo_type="dataset") +def download_mlabonne_finetome_100k_dataset(): + # download the model + snapshot_download("mlabonne/FineTome-100k", repo_type="dataset") + + @pytest.fixture def temp_dir(): # Create a temporary directory diff --git a/tests/e2e/multigpu/test_eval.py b/tests/e2e/multigpu/test_eval.py index 068a9220ca..c40a9edcce 100644 --- a/tests/e2e/multigpu/test_eval.py +++ b/tests/e2e/multigpu/test_eval.py @@ -7,10 +7,13 @@ import yaml from accelerate.test_utils import execute_subprocess_async +from tbparse import SummaryReader from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault +from ..utils import most_recent_subdir + LOG = logging.getLogger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" @@ -26,7 +29,7 @@ def test_eval_sample_packing(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", + "base_model": "HuggingFaceTB/SmolLM2-135M", "load_in_8bit": False, "load_in_4bit": True, "strict": False, @@ -40,8 +43,8 @@ def test_eval_sample_packing(self, temp_dir): "lora_dropout": 0.05, "lora_target_linear": True, "lora_modules_to_save": ["embed_tokens", "lm_head"], - "val_set_size": 0.1, - "special_tokens": {"pad_token": "<|end_of_text|>"}, + "val_set_size": 0.004, + "special_tokens": {"pad_token": "<|endoftext|>"}, "datasets": [ { "path": "teknium/GPT4-LLM-Cleaned", @@ -66,6 +69,7 @@ def test_eval_sample_packing(self, temp_dir): "saves_per_epoch": 1, "logging_steps": 1, "weight_decay": 0.0, + "use_tensorboard": True, } ) @@ -87,12 +91,18 @@ def test_eval_sample_packing(self, temp_dir): str(Path(temp_dir) / "config.yaml"), ] ) + tb_log_path = most_recent_subdir(temp_dir + "/runs") + event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) + reader = SummaryReader(event_file) + df = reader.scalars # pylint: disable=invalid-name + df = df[(df.tag == "eval/loss")] # pylint: disable=invalid-name + assert df.value.values[-1] < 2.5, "Loss is too high" def test_eval(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", + "base_model": "HuggingFaceTB/SmolLM2-135M", "load_in_8bit": False, "load_in_4bit": True, "strict": False, @@ -106,8 +116,8 @@ def test_eval(self, temp_dir): "lora_dropout": 0.05, "lora_target_linear": True, "lora_modules_to_save": ["embed_tokens", "lm_head"], - "val_set_size": 0.1, - "special_tokens": {"pad_token": "<|end_of_text|>"}, + "val_set_size": 0.0004, + "special_tokens": {"pad_token": "<|endoftext|>"}, "datasets": [ { "path": "teknium/GPT4-LLM-Cleaned", @@ -132,6 +142,7 @@ def test_eval(self, temp_dir): "saves_per_epoch": 1, "logging_steps": 1, "weight_decay": 0.0, + "use_tensorboard": True, } ) @@ -153,3 +164,9 @@ def test_eval(self, temp_dir): str(Path(temp_dir) / "config.yaml"), ] ) + tb_log_path = most_recent_subdir(temp_dir + "/runs") + event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) + reader = SummaryReader(event_file) + df = reader.scalars # pylint: disable=invalid-name + df = df[(df.tag == "eval/loss")] # pylint: disable=invalid-name + assert df.value.values[-1] < 2.9, "Loss is too high" From f4cabc2351798596b64d38e86c7ec4dc5fd00838 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 30 Nov 2024 08:37:49 +0700 Subject: [PATCH 0286/1405] fix: ds3 and fsdp lmbench eval (#2102) [ski[p ci] * fix: ds3 and fsdp lmbench eval * chore: update comment * fix: test signature --- src/axolotl/utils/callbacks/__init__.py | 173 ++++++++++++---------- src/axolotl/utils/callbacks/perplexity.py | 16 +- tests/test_perplexity.py | 15 +- 3 files changed, 117 insertions(+), 87 deletions(-) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 8768bc2bf7..6bf433319e 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -28,6 +28,7 @@ TrainingArguments, ) from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, IntervalStrategy +from trl.models import unwrap_model_for_generation from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.bench import log_gpu_memory_usage @@ -46,6 +47,7 @@ if TYPE_CHECKING: from axolotl.core.trainer_builder import AxolotlTrainingArguments + IGNORE_INDEX = -100 LOG = logging.getLogger("axolotl.callbacks") @@ -64,7 +66,10 @@ def on_step_end( control: TrainerControl, **kwargs, ): - if args.eval_strategy == IntervalStrategy.STEPS and state.global_step == 1: + if ( + args.evaluation_strategy == IntervalStrategy.STEPS + and state.global_step == 1 + ): control.should_evaluate = True return control @@ -375,7 +380,10 @@ def __maybe_load_metrics(self): for metric in self.cfg.eval_causal_lm_metrics: if metric == "perplexity": max_seq_len = self.cfg.eval_max_new_tokens - metrics[metric] = Perplexity(trainer.model, tokenizer, max_seq_len) + metrics[metric] = Perplexity( + tokenizer=tokenizer, + max_seq_len=max_seq_len, + ) else: try: metrics[metric] = evaluate.load(metric) @@ -392,8 +400,11 @@ def on_evaluate( eval_dataloader, **kwargs, # pylint: disable=unused-argument ): - trainer.model.eval() - device = torch.device(self.cfg.device) + trainer.model_wrapped.eval() + + device = torch.device( + self.cfg.device + ) # Use this instead of trainer.model_wrapped.device as it may return cpu if fsdp offloaded # pylint: disable=duplicate-code generation_config = GenerationConfig( @@ -430,6 +441,10 @@ def compute(metric: evaluate.Metric, **kwargs): for k in metric._feature_names() # pylint: disable=protected-access if k in kwargs } + + if isinstance(metric, Perplexity): + metric_kwargs["model"] = trainer.model_wrapped + metric_score = metric.compute(**metric_kwargs) return ( metric_score["score"] @@ -465,89 +480,97 @@ def evaluate_preds(sources, predictions, references): def predict_with_generate(): eval_src, eval_pred, eval_ref = [], [], [] - for batch in tqdm(eval_dataloader): - batch_labels = batch["labels"].to(device) - batch_input_ids = batch["input_ids"].to(device) - - if "position_ids" in batch: - batch_pos_ids = batch["position_ids"].tolist() - else: - batch_pos_ids = [None] * len(batch["input_ids"]) - - prompt_token_ids_list = [] - completion_token_ids_list = [] + with unwrap_model_for_generation( + trainer.model_wrapped, trainer.accelerator + ) as unwrapped_model: + for batch in tqdm(eval_dataloader, disable=not is_main_process()): + batch_labels = batch["labels"].to(device) + batch_input_ids = batch["input_ids"].to(device) - for input_ids_all, labels_all, pos_ids in zip( - batch_input_ids, - batch_labels, - batch_pos_ids, - ): - if pos_ids is None: - pos_ranges = [(0, len(input_ids_all) - 1)] + if "position_ids" in batch: + batch_pos_ids = batch["position_ids"].tolist() else: - pos_ranges = find_ranges(pos_ids) - - for pos_range in pos_ranges: - start, end = pos_range - if start == end: - continue + batch_pos_ids = [None] * len(batch["input_ids"]) + + prompt_token_ids_list = [] + completion_token_ids_list = [] + + for input_ids_all, labels_all, pos_ids in zip( + batch_input_ids, + batch_labels, + batch_pos_ids, + ): + if pos_ids is None: + pos_ranges = [(0, len(input_ids_all) - 1)] + else: + pos_ranges = find_ranges(pos_ids) + + for pos_range in pos_ranges: + start, end = pos_range + if start == end: + continue + + input_ids = input_ids_all[start : end + 1] + labels = labels_all[start : end + 1] + + tokens_without_loss = labels == IGNORE_INDEX + tokens_with_loss = labels != IGNORE_INDEX + tokens_exclude_padding = ( + input_ids != tokenizer.pad_token_id + ) + prompt_token_includes = ( + tokens_without_loss & tokens_exclude_padding + ) + + prompt_token_ids = input_ids[prompt_token_includes] + prompt_token_ids_list.append(prompt_token_ids) + + completion_token_ids = input_ids[tokens_with_loss] + completion_token_ids_list.append(completion_token_ids) + + prompt_texts = tokenizer.batch_decode( + prompt_token_ids_list, skip_special_tokens=True + ) + completion_texts = tokenizer.batch_decode( + completion_token_ids_list, skip_special_tokens=True + ) - input_ids = input_ids_all[start : end + 1] - labels = labels_all[start : end + 1] + with torch.no_grad(): + prompt_encoding = tokenizer( + prompt_texts, padding=True, return_tensors="pt" + ).to(device) - tokens_without_loss = labels == IGNORE_INDEX - tokens_with_loss = labels != IGNORE_INDEX - tokens_exclude_padding = input_ids != tokenizer.pad_token_id - prompt_token_includes = ( - tokens_without_loss & tokens_exclude_padding + predictions = unwrapped_model.generate( + **prompt_encoding, generation_config=generation_config ) - prompt_token_ids = input_ids[prompt_token_includes] - prompt_token_ids_list.append(prompt_token_ids) - - completion_token_ids = input_ids[tokens_with_loss] - completion_token_ids_list.append(completion_token_ids) - - prompt_texts = tokenizer.batch_decode( - prompt_token_ids_list, skip_special_tokens=True - ) - completion_texts = tokenizer.batch_decode( - completion_token_ids_list, skip_special_tokens=True - ) - - with torch.no_grad(): - prompt_encoding = tokenizer( - prompt_texts, padding=True, return_tensors="pt" - ).to(self.cfg.device) - predictions = trainer.model.generate( - **prompt_encoding, generation_config=generation_config - ) + del prompt_encoding + + prediction_all_tokens = predictions["sequences"].cpu().tolist() + prediction_without_prompt_tokens_list = [] + for prompt_token_ids, prediction_tokens in zip( + prompt_token_ids_list, prediction_all_tokens + ): + prediction_without_prompt_tokens = prediction_tokens[ + len(prompt_token_ids) : + ] + prediction_without_prompt_tokens_list.append( + prediction_without_prompt_tokens + ) - prediction_all_tokens = predictions["sequences"].cpu().tolist() - prediction_without_prompt_tokens_list = [] - for prompt_token_ids, prediction_tokens in zip( - prompt_token_ids_list, prediction_all_tokens - ): - prediction_without_prompt_tokens = prediction_tokens[ - len(prompt_token_ids) : - ] - prediction_without_prompt_tokens_list.append( - prediction_without_prompt_tokens + predicted_texts = tokenizer.batch_decode( + prediction_without_prompt_tokens_list, + skip_special_tokens=True, ) - predicted_texts = tokenizer.batch_decode( - prediction_without_prompt_tokens_list, skip_special_tokens=True - ) - - eval_src.extend(prompt_texts) - eval_pred.extend(predicted_texts) - eval_ref.extend(completion_texts) + eval_src.extend(prompt_texts) + eval_pred.extend(predicted_texts) + eval_ref.extend(completion_texts) return eval_src, eval_pred, eval_ref - if is_main_process(): - eval_preds = predict_with_generate() - trainer.log(evaluate_preds(*eval_preds)) + eval_preds = predict_with_generate() + trainer.log(evaluate_preds(*eval_preds)) return control diff --git a/src/axolotl/utils/callbacks/perplexity.py b/src/axolotl/utils/callbacks/perplexity.py index 2e64176812..d3a362c4cd 100644 --- a/src/axolotl/utils/callbacks/perplexity.py +++ b/src/axolotl/utils/callbacks/perplexity.py @@ -8,6 +8,8 @@ from transformers.modeling_utils import PreTrainedModel from transformers.tokenization_utils import PreTrainedTokenizer +from axolotl.utils.distributed import is_main_process + class Perplexity: """ @@ -17,16 +19,13 @@ class Perplexity: def __init__( self, - model: PreTrainedModel, tokenizer: PreTrainedTokenizer, max_seq_len: int, stride: int = 512, ) -> None: self.max_seq_len = max_seq_len self.stride = stride - self.model = model self.tokenizer = tokenizer - self.device = model.device self.name = "perplexity" def _feature_names(self) -> List[str]: @@ -34,6 +33,7 @@ def _feature_names(self) -> List[str]: def compute( self, + model: PreTrainedModel, references: Optional[List[str]] = None, ) -> Dict[str, float]: """ @@ -41,17 +41,21 @@ def compute( """ assert references is not None, "Missing parameter: references" + model.eval() + references_tokenized = self.tokenizer( references, return_tensors="pt", padding=True, truncation=True ) input_ids: Tensor = references_tokenized["input_ids"] # type: ignore - input_ids = input_ids.to(self.device) + input_ids = input_ids.to(model.device) sequence_length = input_ids.size(1) losses = [] prev_end_loc = 0 - for begin_loc in tqdm(range(0, sequence_length, self.stride)): + for begin_loc in tqdm( + range(0, sequence_length, self.stride), disable=not is_main_process() + ): end_loc = min(begin_loc + self.max_seq_len, sequence_length) trg_len = end_loc - prev_end_loc input_ids_slice = input_ids[:, begin_loc:end_loc] @@ -59,7 +63,7 @@ def compute( labels_slice[:, :-trg_len] = -100 with torch.no_grad(): - outputs: CausalLMOutput = self.model( + outputs: CausalLMOutput = model( input_ids=input_ids_slice, labels=labels_slice ) diff --git a/tests/test_perplexity.py b/tests/test_perplexity.py index e66e95d0cd..8688827cec 100644 --- a/tests/test_perplexity.py +++ b/tests/test_perplexity.py @@ -12,9 +12,12 @@ @fixture() def metric(tokenizer): - model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, trust_remote_code=True) + return Perplexity(tokenizer=tokenizer, max_seq_len=512) - return Perplexity(model, tokenizer, 512) + +@fixture() +def model(): + return AutoModelForCausalLM.from_pretrained(MODEL_NAME, trust_remote_code=True) @fixture() @@ -22,20 +25,20 @@ def tokenizer(): return AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) -def test_perplexity_longer_than_stride(metric): +def test_perplexity_longer_than_stride(model, metric): # taken from https://huggingface.co/datasets/roneneldan/TinyStories sample_text = """ Once upon a time, there was a little car named Beep. Beep loved to go fast and play in the sun. Beep was a healthy car because he always had good fuel. Good fuel made Beep happy and strong. One day, Beep was driving in the park when he saw a big tree. The tree had many leaves that were falling. Beep liked how the leaves fall and wanted to play with them. Beep drove under the tree and watched the leaves fall on him. He laughed and beeped his horn. Beep played with the falling leaves all day. When it was time to go home, Beep knew he needed more fuel. He went to the fuel place and got more healthy fuel. Now, Beep was ready to go fast and play again the next day. And Beep lived happily ever after. One day, a little fish named Fin was swimming near the shore. He saw a big crab and wanted to be friends. "Hi, I am Fin. Do you want to play?" asked the little fish. The crab looked at Fin and said, "No, I don't want to play. I am cold and I don't feel fine." Fin felt sad but wanted to help the crab feel better. He swam away and thought of a plan. He remembered that the sun could make things warm. So, Fin swam to the top of the water and called to the sun, "Please, sun, help my new friend feel fine and not freeze!" The sun heard Fin's call and shone its warm light on the shore. The crab started to feel better and not so cold. He saw Fin and said, "Thank you, little fish, for making me feel fine. I don't feel like I will freeze now. Let's play together!" And so, Fin and the crab played and became good friends. """ - result = metric.compute([sample_text]) + result = metric.compute(model, [sample_text]) ppl = result["score"] assert round(ppl, 2) == 5.37 -def test_perplexity_short(metric): +def test_perplexity_short(model, metric): # taken from https://huggingface.co/datasets/roneneldan/TinyStories sample_text = "Once upon a time, there was a little car named Beep. Beep loved to go fast and play in the sun." - result = metric.compute([sample_text]) + result = metric.compute(model, [sample_text]) ppl = result["score"] assert round(ppl, 2) == 10.02 From 1cf7075d18a01390dfeb75250ba32901c4b43a72 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 29 Nov 2024 20:38:20 -0500 Subject: [PATCH 0287/1405] support seperate lr for embeddings, similar to loraplus (#1910) [skip ci] * support seperate lr for embeddings, similar to loraplus * add test case for train w lr embedding scale * use kwarg for optimizer * make sure to handle the optimizer creation * make sure to handle for embedding_lr too * use smollm for e2e, check for embeddings lr first before wdecay --- src/axolotl/core/trainer_builder.py | 87 ++++++++++--- .../config/models/input/v0_4_1/__init__.py | 2 + tests/e2e/test_embeddings_lr.py | 121 ++++++++++++++++++ 3 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 tests/e2e/test_embeddings_lr.py diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 75219a2749..e4bf7de229 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -220,6 +220,14 @@ class AxolotlTrainingMixins: default=1e-6, metadata={"help": "loraplus learning rate for lora embedding layers."}, ) + embedding_lr_scale: Optional[float] = field( + default=None, + metadata={"help": "Scale the learning rate for the embedding layers."}, + ) + embedding_lr: Optional[float] = field( + default=None, + metadata={"help": "absolute learning rate for the embedding layers."}, + ) qlora: bool = field( default=False, metadata={"help": "whether this is a qlora training"}, @@ -386,7 +394,7 @@ def create_scheduler( min_lr_ratio=self.args.cosine_min_lr_ratio, ) else: - return super().create_scheduler(num_training_steps, optimizer) + return super().create_scheduler(num_training_steps, optimizer=optimizer) else: if use_cosine_quadratic: LOG.warning("axolotl's cosine scheduler with quadratic warmup not used (e.g., because of deepspeed).") @@ -435,6 +443,8 @@ def _wrap_model(self, model, training=True, dataloader=None): def create_optimizer(self): if ( self.args.loraplus_lr_ratio is None + and self.args.embedding_lr_scale is None + and self.args.embedding_lr is None and self.args.alternate_optimizer not in [ "optimi_adamw", @@ -449,30 +459,59 @@ def create_optimizer(self): opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model if self.optimizer is None: # pylint: disable=access-member-before-definition decay_parameters = self.get_decay_parameter_names(opt_model) - optimizer_grouped_parameters = [ - { - "params": [ - p - for n, p in opt_model.named_parameters() - if (n in decay_parameters and p.requires_grad) - ], - "weight_decay": self.args.weight_decay, - }, - { - "params": [ - p - for n, p in opt_model.named_parameters() - if (n not in decay_parameters and p.requires_grad) - ], - "weight_decay": 0.0, - }, - ] + params = { + "to_weight_decay": {}, # LayerNorm and bias + "embeddings": {}, # lm_head, embed_tokens, + "no_weight_decay": {}, + } optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs( self.args, opt_model, ) + for name, param in opt_model.named_parameters(): + if not param.requires_grad: + continue + if name.endswith("modules_to_save.default.weight") or any( + embed_name in name for embed_name in ["embed_tokens", "lm_head"] + ): + params["embeddings"][name] = param + elif name in decay_parameters: + params["to_weight_decay"][name] = param + else: + params["no_weight_decay"][name] = param + optimizer_grouped_parameters = [] + if params["to_weight_decay"]: + optimizer_grouped_parameters.append( + { + "params": list(params["to_weight_decay"].values()), + "weight_decay": self.args.weight_decay, + "lr": optimizer_kwargs["lr"], + } + ) + if params["embeddings"]: + lr = optimizer_kwargs["lr"] # pylint: disable=invalid-name + if self.args.embedding_lr_scale: + lr *= self.args.embedding_lr_scale # pylint: disable=invalid-name + elif self.args.embedding_lr: + lr = self.args.embedding_lr # pylint: disable=invalid-name + optimizer_grouped_parameters.append( + { + "params": list(params["embeddings"].values()), + "weight_decay": 0.0, + "lr": lr, + } + ) + if params["no_weight_decay"]: + optimizer_grouped_parameters.append( + { + "params": list(params["no_weight_decay"].values()), + "weight_decay": 0.0, + "lr": optimizer_kwargs["lr"], + } + ) + if self.args.loraplus_lr_ratio is not None: loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) loraplus_lr_embedding = getattr( @@ -485,6 +524,13 @@ def create_optimizer(self): loraplus_lr_embedding=loraplus_lr_embedding, **optimizer_kwargs, ) + elif ( + self.args.embedding_lr_scale is not None + or self.args.embedding_lr is not None + ): + self.optimizer = ( # pylint: disable=attribute-defined-outside-init + optimizer_cls(optimizer_grouped_parameters, **optimizer_kwargs) + ) elif self.args.alternate_optimizer == "optimi_adamw": from optimi import AdamW @@ -1571,6 +1617,9 @@ def build(self, total_num_steps): training_arguments_kwargs[ "loraplus_lr_embedding" ] = self.cfg.loraplus_lr_embedding + training_arguments_kwargs["embedding_lr"] = self.cfg.embedding_lr + training_arguments_kwargs["embedding_lr_scale"] = self.cfg.embedding_lr_scale + if self.cfg.lr_scheduler in ["one_cycle", "log_sweep"]: training_arguments_kwargs["lr_scheduler_type"] = "cosine" training_arguments_kwargs[ diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index cdbe47b8f1..eeadfd0676 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -430,6 +430,8 @@ class HyperparametersConfig(BaseModel): group_by_length: Optional[bool] = None learning_rate: Union[str, float] + embedding_lr: Optional[float] = None + embedding_lr_scale: Optional[float] = None weight_decay: Optional[float] = 0.0 optimizer: Optional[ Union[ diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py new file mode 100644 index 0000000000..bc406caf30 --- /dev/null +++ b/tests/e2e/test_embeddings_lr.py @@ -0,0 +1,121 @@ +""" +E2E tests for llama pretrain +""" + +import logging +import os +import unittest +from pathlib import Path + +from tbparse import SummaryReader + +from axolotl.cli import load_datasets +from axolotl.common.cli import TrainerCliArgs +from axolotl.train import train +from axolotl.utils.config import normalize_config +from axolotl.utils.dict import DictDefault + +from .utils import most_recent_subdir, with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestEmbeddingsLrScale(unittest.TestCase): + """ + Test case for embedding_lr* + """ + + @with_temp_dir + def test_train_w_embedding_lr_scale(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "flash_attention": True, + "sequence_len": 1024, + "sample_packing": True, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "max_steps": 5, + "num_epochs": 1, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "embedding_lr_scale": 0.5, + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + "use_tensorboard": True, + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() + + tb_log_path = most_recent_subdir(temp_dir + "/runs") + event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) + reader = SummaryReader(event_file) + df = reader.scalars # pylint: disable=invalid-name + df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name + assert df.value.values[-1] < 2.0, "Loss is too high" + + @with_temp_dir + def test_train_w_embedding_lr(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "flash_attention": True, + "sequence_len": 1024, + "sample_packing": True, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "max_steps": 5, + "num_epochs": 1, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "embedding_lr": 0.000005, + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + "use_tensorboard": True, + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() + + tb_log_path = most_recent_subdir(temp_dir + "/runs") + event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) + reader = SummaryReader(event_file) + df = reader.scalars # pylint: disable=invalid-name + df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name + assert df.value.values[-1] < 2.0, "Loss is too high" From 5f1d98e8fcedd1a89417658b299ee461deb00650 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 29 Nov 2024 20:38:49 -0500 Subject: [PATCH 0288/1405] add e2e tests for Unsloth qlora and test the builds (#2093) * see if unsloth installs cleanly in ci * check unsloth install on regular tests, not sdist * fix ampere check exception for ci * use cached_property instead * add an e2e test for unsloth qlora * reduce seq len and mbsz to prevent oom in ci * add checks for fp16 and sdp_attention * pin unsloth to a specific release * add unsloth to docker image too * fix flash attn xentropy patch * fix loss, add check for loss when using fa_xentropy * fix special tokens for test * typo * test fa xentropy with and without gradient accum * pr feedback changes --- .github/workflows/tests.yml | 1 + cicd/Dockerfile.jinja | 2 + docker/Dockerfile | 2 + scripts/unsloth_install.py | 7 +- .../monkeypatch/llama_attn_hijack_flash.py | 34 +++- src/axolotl/utils/models.py | 46 ++--- tests/e2e/patched/test_fa_xentropy.py | 47 +++-- tests/e2e/patched/test_unsloth_qlora.py | 186 ++++++++++++++++++ 8 files changed, 275 insertions(+), 50 deletions(-) create mode 100644 tests/e2e/patched/test_unsloth_qlora.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b5c336246b..5b380c7845 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -67,6 +67,7 @@ jobs: run: | pip3 show torch pip3 install -U -e . + python scripts/unsloth_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt - name: Run tests diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 8fd040d77b..65553d60b5 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -37,6 +37,8 @@ RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ pip install -e .[deepspeed,flash-attn,optimizers] $AXOLOTL_ARGS; \ fi +RUN python scripts/unsloth_install.py | sh + # So we can test the Docker image RUN pip install -r requirements-dev.txt -r requirements-tests.txt diff --git a/docker/Dockerfile b/docker/Dockerfile index 6e14a70a76..173a508792 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,6 +26,8 @@ RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ pip install -e .[deepspeed,flash-attn,optimizers] $AXOLOTL_ARGS; \ fi +RUN python scripts/unsloth_install.py | sh + # So we can test the Docker image RUN pip install pytest diff --git a/scripts/unsloth_install.py b/scripts/unsloth_install.py index 66b983e72d..a6570b4e9f 100644 --- a/scripts/unsloth_install.py +++ b/scripts/unsloth_install.py @@ -8,7 +8,10 @@ v = V(torch.__version__) cuda = str(torch.version.cuda) -is_ampere = torch.cuda.get_device_capability()[0] >= 8 +try: + is_ampere = torch.cuda.get_device_capability()[0] >= 8 +except RuntimeError: + is_ampere = False if cuda != "12.1" and cuda != "11.8" and cuda != "12.4": raise RuntimeError(f"CUDA = {cuda} not supported!") if v <= V("2.1.0"): @@ -29,5 +32,5 @@ raise RuntimeError(f"Torch = {v} too new!") x = x.format(cuda.replace(".", ""), "-ampere" if is_ampere else "") print( - f'pip install unsloth-zoo && pip install --no-deps "unsloth[{x}] @ git+https://github.com/unslothai/unsloth.git"' + f'pip install unsloth-zoo==2024.11.7 && pip install --no-deps "unsloth[{x}]==2024.11.9"' ) diff --git a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py index c804d0c6b9..ad0459ccc0 100644 --- a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py @@ -4,7 +4,6 @@ import logging import warnings -from functools import partial from typing import List, Optional, Tuple, Union import torch @@ -94,14 +93,33 @@ def replace_llama_qkv_with_fused(model): set_module_name(model, name, qkv) -def patch_llama_cross_entropy(): - from flash_attn.losses.cross_entropy import CrossEntropyLoss - - LOG.info("patching with flash_attn.losses.cross_entropy") - transformers.models.llama.modeling_llama.CrossEntropyLoss = partial( - CrossEntropyLoss, inplace_backward=True +def patch_fa_llama_cross_entropy(): + LOG.info( + "patching transformers.loss.loss_utils.fixed_cross_entropy with flash_attn.ops.triton.cross_entropy" + ) + from flash_attn.ops.triton.cross_entropy import ( + cross_entropy_loss as flash_attn_cross_entropy_loss, ) + def fa2_fixed_cross_entropy( + source, + target, + num_items_in_batch: int = None, + ignore_index: int = -100, + **kwargs, + ): # pylint: disable=unused-argument + reduction = "sum" if num_items_in_batch is not None else "mean" + loss, _ = flash_attn_cross_entropy_loss( + source, target, ignore_index=ignore_index + ) + if reduction == "sum": + loss = loss.sum() / num_items_in_batch + else: + loss = loss.sum() / (target != ignore_index).sum() + return loss + + transformers.loss.loss_utils.fixed_cross_entropy = fa2_fixed_cross_entropy + def patch_llama_rms_norm(): try: @@ -147,7 +165,7 @@ def replace_llama_attn_with_flash_attn( # skip only if explicitly disabled if cross_entropy: - patch_llama_cross_entropy() + patch_fa_llama_cross_entropy() # skip only if explicitly disabled if rms_norm: diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 082df7c27b..fc1f0cf1c2 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -2,10 +2,12 @@ # pylint: disable=too-many-lines import gc +import importlib import logging import math import os import types +from functools import cached_property from typing import Any, Dict, Optional, Tuple, Union # noqa: F401 import addict @@ -409,7 +411,7 @@ def apply_patches(self) -> None: ) if self.cfg.is_llama_derived_model: - self.patch_loss() + self.patch_loss_llama() if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora @@ -451,27 +453,34 @@ def patch_attention(self) -> None: replace_stablelm_attn_with_flash_attn(self.cfg.base_model) - def patch_loss(self) -> None: + @cached_property + def has_flash_attn(self) -> bool: + """Check if flash attention is installed""" + return importlib.util.find_spec("flash_attn") is not None + + def patch_loss_llama(self) -> None: """ Patch loss functions """ - from axolotl.monkeypatch.llama_attn_hijack_flash import ( - patch_llama_cross_entropy, - patch_llama_rms_norm, - ) + if self.has_flash_attn: + from axolotl.monkeypatch.llama_attn_hijack_flash import ( + patch_fa_llama_cross_entropy, + patch_llama_rms_norm, + ) + + if self.cfg.flash_attn_cross_entropy and self.has_flash_attn: + patch_fa_llama_cross_entropy() + elif self.cfg.unsloth_cross_entropy_loss: + from axolotl.monkeypatch.unsloth_ import integrate_cross_entropy_loss_patch + + integrate_cross_entropy_loss_patch(model_type="llama") - if self.cfg.flash_attn_cross_entropy: - patch_llama_cross_entropy() - if self.cfg.flash_attn_rms_norm: + if self.cfg.flash_attn_rms_norm and self.has_flash_attn: patch_llama_rms_norm() elif self.cfg.unsloth_rms_norm: from axolotl.monkeypatch.unsloth_ import patch_unsloth_layernorm patch_unsloth_layernorm() - if self.cfg.unsloth_cross_entropy_loss: - from axolotl.monkeypatch.unsloth_ import integrate_cross_entropy_loss_patch - - integrate_cross_entropy_loss_patch(model_type="llama") if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora @@ -481,6 +490,7 @@ def patch_llama_derived_model(self) -> None: """ Modify all llama derived models in one block """ + self.patch_loss_llama() if self.cfg.flash_attention: from axolotl.monkeypatch.llama_attn_hijack_flash import ( @@ -528,16 +538,6 @@ def patch_llama_derived_model(self) -> None: "Shifted-sparse attention not currently implemented without flash attention." ) - if self.cfg.unsloth_cross_entropy_loss: - from axolotl.monkeypatch.unsloth_ import integrate_cross_entropy_loss_patch - - integrate_cross_entropy_loss_patch(model_type="llama") - - if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: - from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora - - patch_self_attn_lora() - def set_auto_model_loader(self) -> None: """set self.AutoModelLoader - default value: AutoModelForCausalLM (set at __init__) diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index 8b76362fb4..7ca1c08365 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -4,11 +4,11 @@ import logging import os -import unittest from importlib import reload from pathlib import Path import pytest +from tbparse import SummaryReader from transformers.utils import is_torch_bf16_gpu_available from axolotl.cli import load_datasets @@ -17,7 +17,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import with_temp_dir +from ..utils import most_recent_subdir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -31,18 +31,20 @@ def reload_transformers(): reload(transformers.models.llama.modeling_llama) -class TestFAXentropyLlama(unittest.TestCase): +class TestFAXentropyLlama: """ Test case for Llama models using LoRA w multipack """ - @with_temp_dir - def test_lora_packing_fa_cross_entropy(self, temp_dir): + @pytest.mark.parametrize( + "gradient_accumulation_steps", + [1, 4], + ) + def test_lora_packing_fa_cross_entropy(self, temp_dir, gradient_accumulation_steps): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 1024, "sample_packing": True, "flash_attention": True, @@ -55,25 +57,29 @@ def test_lora_packing_fa_cross_entropy(self, temp_dir): "lora_target_linear": True, "val_set_size": 0.2, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, + "chat_template": "chatml", "datasets": [ { - "path": "mhenrichsen/alpaca_2k_test", - "type": "alpaca", + "path": "mlabonne/FineTome-100k", + "field_messages": "conversations", + "message_field_content": "value", + "message_field_role": "from", + "type": "chat_template", + "split": "train[:2%]", }, ], "num_epochs": 1, - "max_steps": 10, - "save_steps": 10, - "micro_batch_size": 8, - "gradient_accumulation_steps": 1, + "max_steps": 5, + "save_steps": 5, + "micro_batch_size": 2, + "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_8bit", "lr_scheduler": "cosine", + "use_tensorboard": True, } ) if is_torch_bf16_gpu_available(): @@ -87,3 +93,10 @@ def test_lora_packing_fa_cross_entropy(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.bin").exists() + + tb_log_path = most_recent_subdir(temp_dir + "/runs") + event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) + reader = SummaryReader(event_file) + df = reader.scalars # pylint: disable=invalid-name + df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name + assert df.value.values[-1] < 1.5, "Loss is too high" diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py new file mode 100644 index 0000000000..805b150037 --- /dev/null +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -0,0 +1,186 @@ +""" +e2e tests for unsloth qlora +""" +import logging +import os +from pathlib import Path + +import pytest +from e2e.utils import most_recent_subdir +from tbparse import SummaryReader + +from axolotl.cli import load_datasets +from axolotl.common.cli import TrainerCliArgs +from axolotl.train import train +from axolotl.utils.config import normalize_config +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +# pylint: disable=duplicate-code +class TestUnslothQLoRA: + """ + Test class for Unsloth QLoRA Llama models + """ + + @pytest.mark.parametrize( + "sample_packing", + [True, False], + ) + def test_unsloth_llama_qlora_fa2(self, temp_dir, sample_packing): + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 1024, + "sample_packing": sample_packing, + "flash_attention": True, + "load_in_4bit": True, + "adapter": "qlora", + "lora_r": 16, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.2, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "save_steps": 10, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "use_tensorboard": True, + "bf16": "auto", + } + ) + + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.bin").exists() + + tb_log_path = most_recent_subdir(temp_dir + "/runs") + event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) + reader = SummaryReader(event_file) + df = reader.scalars # pylint: disable=invalid-name + df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name + assert df.value.values[-1] < 2.0, "Loss is too high" + + def test_unsloth_llama_qlora_unpacked(self, temp_dir): + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 1024, + "sample_packing": False, + "load_in_4bit": True, + "adapter": "qlora", + "lora_r": 16, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.2, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "save_steps": 10, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "use_tensorboard": True, + "bf16": "auto", + } + ) + + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.bin").exists() + + tb_log_path = most_recent_subdir(temp_dir + "/runs") + event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) + reader = SummaryReader(event_file) + df = reader.scalars # pylint: disable=invalid-name + df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name + assert df.value.values[-1] < 2.0, "Loss is too high" + + @pytest.mark.parametrize( + "sdp_attention", + [True, False], + ) + def test_unsloth_llama_qlora_unpacked_no_fa2_fp16(self, temp_dir, sdp_attention): + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 1024, + "sample_packing": False, + "load_in_4bit": True, + "adapter": "qlora", + "lora_r": 16, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.2, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "save_steps": 10, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "sdp_attention": sdp_attention, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "use_tensorboard": True, + "fp16": True, + } + ) + + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.bin").exists() + + tb_log_path = most_recent_subdir(temp_dir + "/runs") + event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) + reader = SummaryReader(event_file) + df = reader.scalars # pylint: disable=invalid-name + df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name + assert df.value.values[-1] < 2.0, "Loss is too high" From b620ed94d0682549433640ee03675ac9e2277cd6 Mon Sep 17 00:00:00 2001 From: Oliver Molenschot <91694286+olivermolenschot@users.noreply.github.com> Date: Mon, 2 Dec 2024 05:47:10 -0800 Subject: [PATCH 0289/1405] Add Exact Deduplication Feature to Preprocessing Pipeline (#2072) * Add example YAML file for training Mistral using DPO * added deduplication code * Add exact deduplication feature and update examples * Improve deduplication for train/eval overlap Changed the deduplication function to use a more memory-efficient hashing method. Applied Git suggestions to improve clarity and maintainability.\n\nThe deduplication now handles cases where train and eval datasets have overlapping elements. * Improve deduplication for train/eval overlap Changed the deduplication function to use a more memory-efficient hashing method. Applied Git suggestions to improve clarity and maintainability.\n\nThe deduplication now handles cases where train and eval datasets have overlapping elements. * Apply suggestions from code review To handle the original case where we do not do deduplication Co-authored-by: Wing Lian * Improve false collision detection to ensure dataset integrity - Added test cases to simulate and verify handling of forced hash collisions between datasets. - Ensured that datasets with identical hashes but different content are correctly identified, preventing incorrect deduplication. - Updated unit tests to include scenarios where collisions occur across both training and evaluation datasets, as well as within a single dataset. * Moved the constants file to the tests folder - Relocated `constants.py` to the `tests` folder to improve modularity and maintain a clear separation between source and test files. - Renamed `cicd/tests.py` to `cicd/cicd_tests.py` to resolve a conflict with `tests/__init__.py`, which caused Mypy to fail due to duplicate module names. - Updated all references to `cicd.tests` in the codebase to `cicd.cicd_tests` to reflect the renaming and ensure compatibility. - These changes ensure Mypy passes the pre-commit hook and maintain alignment with the project's structure. * revert some changes from previous commit and fix relative import --------- Co-authored-by: Wing Lian Co-authored-by: Wing Lian --- docs/config.qmd | 3 + examples/llama-3/lora-1b-deduplicate-dpo.yml | 95 ++++ examples/llama-3/lora-1b-deduplicate-sft.yml | 76 +++ src/axolotl/cli/__init__.py | 2 +- .../config/models/input/v0_4_1/__init__.py | 1 + src/axolotl/utils/data/rl.py | 7 +- src/axolotl/utils/data/sft.py | 21 +- src/axolotl/utils/data/utils.py | 98 ++++ tests/constants.py | 32 ++ tests/test_datasets.py | 50 +- tests/test_exact_deduplication.py | 433 ++++++++++++++++++ 11 files changed, 767 insertions(+), 51 deletions(-) create mode 100644 examples/llama-3/lora-1b-deduplicate-dpo.yml create mode 100644 examples/llama-3/lora-1b-deduplicate-sft.yml create mode 100644 tests/constants.py create mode 100644 tests/test_exact_deduplication.py diff --git a/docs/config.qmd b/docs/config.qmd index 04e278e2d0..bc3730095d 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -162,6 +162,9 @@ datasets: # The same applies to the `test_datasets` option and the `pretraining_dataset` option. Default is true. shuffle_merged_datasets: true +Deduplicates datasets and test_datasets with identical entries. +dataset_exact_deduplication: true + # A list of one or more datasets to eval the model with. # You can use either test_datasets, or val_set_size, but not both. test_datasets: diff --git a/examples/llama-3/lora-1b-deduplicate-dpo.yml b/examples/llama-3/lora-1b-deduplicate-dpo.yml new file mode 100644 index 0000000000..35a0260ca0 --- /dev/null +++ b/examples/llama-3/lora-1b-deduplicate-dpo.yml @@ -0,0 +1,95 @@ +base_model: meta-llama/Llama-3.2-1B +model_type: LlamaForCausalLM +tokenizer_type: AutoTokenizer + +load_in_8bit: true +load_in_4bit: false +strict: false + +chat_template: llama3 +rl: dpo +datasets: + - path: fozziethebeat/alpaca_messages_2k_dpo_test + type: chat_template.default + field_messages: conversation + field_chosen: chosen + field_rejected: rejected + message_field_role: role + message_field_content: content + roles: + system: + - system + user: + - user + assistant: + - assistant + - path: fozziethebeat/alpaca_messages_2k_dpo_test + type: chat_template.default + field_messages: conversation + field_chosen: chosen + field_rejected: rejected + message_field_role: role + message_field_content: content + roles: + system: + - system + user: + - user + assistant: + - assistant + +dataset_exact_deduplication: true +dataset_prepared_path: +val_set_size: 0 +output_dir: ./outputs/lora-out + +sequence_len: 4096 +sample_packing: false +pad_to_sequence_len: true + +adapter: lora +lora_model_dir: +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_fan_in_fan_out: + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true +s2_attention: + +warmup_steps: 10 +evals_per_epoch: 4 +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: diff --git a/examples/llama-3/lora-1b-deduplicate-sft.yml b/examples/llama-3/lora-1b-deduplicate-sft.yml new file mode 100644 index 0000000000..c07d5f8ffa --- /dev/null +++ b/examples/llama-3/lora-1b-deduplicate-sft.yml @@ -0,0 +1,76 @@ +base_model: meta-llama/Llama-3.2-1B +model_type: LlamaForCausalLM +tokenizer_type: AutoTokenizer + +load_in_8bit: true +load_in_4bit: false +strict: false + +datasets: + - path: mhenrichsen/alpaca_2k_test + type: alpaca + - path: mhenrichsen/alpaca_2k_test + type: alpaca +dataset_prepared_path: +val_set_size: 0.0 +output_dir: ./outputs/lora-out + +dataset_exact_deduplication: true +test_value: true + +sequence_len: 4096 +sample_packing: true +eval_sample_packing: false +pad_to_sequence_len: true + +adapter: lora +lora_model_dir: +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_fan_in_fan_out: +lora_modules_to_save: + - embed_tokens + - lm_head + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true +s2_attention: + +warmup_steps: 10 +evals_per_epoch: 4 +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: + pad_token: <|end_of_text|> diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 7c8db7ce8e..86cc30a401 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -139,7 +139,7 @@ def check_remote_config(config: Union[str, Path]): with open(output_path, "wb") as file: file.write(content) LOG.info( - f"Using the following config obtained from {config}:\n\n{content.decode('utf-8')}\n" + f"Using the following config obtained from {config}: \n\n{content.decode('utf-8')}\n" ) return output_path diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index eeadfd0676..1ac7efbfa5 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -624,6 +624,7 @@ class Config: json_schema_extra={"description": "streaming dataset to use for pretraining"}, ) dataset_processes: Optional[int] = Field(default=os.cpu_count()) + dataset_exact_deduplication: Optional[bool] = None dataset_keep_in_memory: Optional[bool] = None dataloader_pin_memory: Optional[bool] = None dataloader_num_workers: Optional[int] = None diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index cf1226175e..edb72f186e 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -13,7 +13,7 @@ from axolotl.prompt_strategies.dpo import load as load_dpo from axolotl.prompt_strategies.kto import load as load_kto from axolotl.prompt_strategies.orpo import load as load_orpo -from axolotl.utils.data.utils import md5 +from axolotl.utils.data.utils import deduplicate_and_log_datasets, md5 from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import is_main_process, zero_first from axolotl.utils.models import load_tokenizer @@ -208,4 +208,9 @@ def load_split(dataset_cfgs, _cfg): if eval_dataset and not eval_is_preprocessed: _save_preprocessed_ds(cfg, cfg.test_datasets, eval_dataset) + if cfg.dataset_exact_deduplication: + train_dataset, eval_dataset, _ = deduplicate_and_log_datasets( + train_dataset=train_dataset, eval_dataset=eval_dataset + ) + return train_dataset, eval_dataset diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index b72e0a2b38..0bee4dd5cf 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -44,7 +44,7 @@ UnsupportedPrompter, ) from axolotl.utils.data.pretraining import wrap_pretraining_dataset -from axolotl.utils.data.utils import md5 +from axolotl.utils.data.utils import deduplicate_and_log_datasets, md5 from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import is_local_main_process, zero_first from axolotl.utils.trainer import ( @@ -136,8 +136,9 @@ def prepare_dataset(cfg, tokenizer, processor=None): # https://discuss.huggingface.co/t/how-to-use-huggingface-trainer-streaming-datasets-without-wrapping-it-with-torchdatas-iterablewrapper/25230 train_dataset = train_dataset.with_format("torch") eval_dataset = None + if cfg.dataset_exact_deduplication: + LOG.info("Deduplication not available for pretrained datasets") return train_dataset, eval_dataset, cfg.max_steps, prompters - if eval_dataset and cfg.sample_packing and cfg.eval_sample_packing is not False: total_eval_steps = calculate_total_num_steps(cfg, eval_dataset, update=False) if total_eval_steps == 0: @@ -178,7 +179,7 @@ def load_tokenized_prepared_datasets( + "|".join( sorted( [ - f"{d.path}:{d.type}:{d.shards}:{d.conversation}{d.split}" + f"{d.path}: {d.type}: {d.shards}: {d.conversation}{d.split}" for d in cfg_datasets ] ) @@ -584,7 +585,8 @@ def load_prepare_datasets( ) train_fingerprint = md5(to_hash_train) test_fingerprint = md5(to_hash_test) - + if cfg.dataset_exact_deduplication: + _, _, dataset = deduplicate_and_log_datasets(dataset=dataset) dataset = dataset.train_test_split( test_size=val_set_size, shuffle=False, @@ -596,12 +598,17 @@ def load_prepare_datasets( train_dataset = dataset["train"] eval_dataset = dataset["test"] elif split == "test": + if cfg.dataset_exact_deduplication: + _, eval_dataset, _ = deduplicate_and_log_datasets(eval_dataset=dataset) + else: + eval_dataset = dataset train_dataset = None - eval_dataset = dataset else: - train_dataset = dataset + if cfg.dataset_exact_deduplication: + train_dataset, _, _ = deduplicate_and_log_datasets(train_dataset=dataset) + else: + train_dataset = dataset eval_dataset = None - return train_dataset, eval_dataset, prompters diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index e05701e7b0..56bcddd8eb 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -1,6 +1,11 @@ """data handling helpers""" import hashlib +import logging + +from datasets import Dataset + +LOG = logging.getLogger("axolotl") def md5(to_hash: str, encoding: str = "utf-8") -> str: @@ -8,3 +13,96 @@ def md5(to_hash: str, encoding: str = "utf-8") -> str: return hashlib.md5(to_hash.encode(encoding), usedforsecurity=False).hexdigest() except TypeError: return hashlib.md5(to_hash.encode(encoding)).hexdigest() # nosec + + +def sha256(to_hash: str, encoding: str = "utf-8") -> str: + return hashlib.sha256(to_hash.encode(encoding)).hexdigest() + + +def deduplicate_dataset( + dataset: Dataset, seen_hashes: dict[str, list[int]], other_dataset: Dataset = None +) -> Dataset: + unique_indices = [] + + for idx, row in enumerate(dataset): + row_hash = sha256(str(row)) # Using SHA256 for collision resistance. + if row_hash not in seen_hashes: + seen_hashes[row_hash] = [idx] + unique_indices.append(idx) + else: + # Check for collision by looking up the original dataset indices + original_indices = seen_hashes[row_hash] + is_duplicate = False + for original_idx in original_indices: + if ( + not idx == original_idx + and original_idx < len(dataset) + and str(dataset[original_idx]) == str(row) + ): + is_duplicate = True + break + # Check in the other dataset if provided + if other_dataset is not None: + if original_idx < len(other_dataset) and str( + other_dataset[original_idx] + ) == str(row): + is_duplicate = True + break + if not is_duplicate: + seen_hashes[row_hash].append(idx) + unique_indices.append(idx) + continue + return dataset.select(unique_indices) + + +def deduplicate_and_log_datasets( + *, + train_dataset: Dataset = None, + eval_dataset: Dataset = None, + dataset: Dataset = None, +) -> tuple[Dataset, Dataset, Dataset]: + """ + Deduplicates train, eval, and an optional dataset if provided, logging original and new sizes. + + Returns: + tuple: Deduplicated train, eval, and additional datasets. + """ + seen_hashes: dict[str, list[int]] = {} + + # Handle cases where datasets are None + if train_dataset is not None: + LOG.info( + f"Starting deduplication for train dataset. Original size: {len(train_dataset)}" + ) + train_dataset = deduplicate_dataset( + dataset=train_dataset, seen_hashes=seen_hashes + ) + LOG.info( + f"Deduplication complete for train dataset. New size: {len(train_dataset)}" + ) + else: + LOG.info("Train dataset is None. Skipping deduplication.") + + if eval_dataset is not None: + LOG.info( + f"Starting deduplication for eval dataset. Original size: {len(eval_dataset)}" + ) + eval_dataset = deduplicate_dataset( + dataset=eval_dataset, seen_hashes=seen_hashes, other_dataset=train_dataset + ) + LOG.info( + f"Deduplication complete for eval dataset. New size: {len(eval_dataset)}" + ) + else: + LOG.info("Eval dataset is None. Skipping deduplication.") + + if dataset is not None and (eval_dataset is None and train_dataset is None): + LOG.info( + f"Starting deduplication for combined dataset. Original size: {len(dataset)}" + ) + dataset = deduplicate_dataset(dataset=dataset, seen_hashes=seen_hashes) + LOG.info( + f"Deduplication complete for combined dataset. New size: {len(dataset)}" + ) + + return train_dataset, eval_dataset, dataset diff --git a/tests/constants.py b/tests/constants.py new file mode 100644 index 0000000000..e024e6920e --- /dev/null +++ b/tests/constants.py @@ -0,0 +1,32 @@ +# constants.py +""" +This module contains constants and configuration dictionaries used for +datasets and other utilities in the Axolotl project, specifically for testing. +""" +# Configuration for Alpaca Messages Dataset +ALPACA_MESSAGES_CONFIG_OG = { + "path": "fozziethebeat/alpaca_messages_2k_dpo_test", + "type": "chat_template.default", + "chat_template": "llama3", + "field_messages": "conversation", + "field_chosen": "chosen", + "field_rejected": "rejected", + "message_field_role": "role", + "message_field_content": "content", + "roles": { + "system": ["system"], + "user": ["user"], + "assistant": ["assistant"], + }, +} + +# Revision configuration extending the original +ALPACA_MESSAGES_CONFIG_REVISION = ALPACA_MESSAGES_CONFIG_OG.copy() +ALPACA_MESSAGES_CONFIG_REVISION["revision"] = "ea82cff" + + +SPECIAL_TOKENS = { + "bos_token": "", + "eos_token": "", + "unk_token": "", +} diff --git a/tests/test_datasets.py b/tests/test_datasets.py index e87f19cc7f..f3bed00fd0 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -7,6 +7,11 @@ import unittest from pathlib import Path +from constants import ( + ALPACA_MESSAGES_CONFIG_OG, + ALPACA_MESSAGES_CONFIG_REVISION, + SPECIAL_TOKENS, +) from datasets import Dataset from huggingface_hub import snapshot_download from transformers import AutoTokenizer @@ -21,13 +26,7 @@ class TestDatasetPreparation(unittest.TestCase): def setUp(self) -> None: self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") - self.tokenizer.add_special_tokens( - { - "bos_token": "", - "eos_token": "", - "unk_token": "", - } - ) + self.tokenizer.add_special_tokens(SPECIAL_TOKENS) # Alpaca dataset. self.dataset = Dataset.from_list( [ @@ -277,23 +276,7 @@ def test_load_hub_with_dpo(self): "sequence_len": 1024, "rl": "dpo", "chat_template": "llama3", - "datasets": [ - { - "path": "fozziethebeat/alpaca_messages_2k_dpo_test", - "type": "chat_template.default", - "chat_template": "llama3", - "field_messages": "conversation", - "field_chosen": "chosen", - "field_rejected": "rejected", - "message_field_role": "role", - "message_field_content": "content", - "roles": { - "system": ["system"], - "user": ["user"], - "assistant": ["assistant"], - }, - } - ], + "datasets": [ALPACA_MESSAGES_CONFIG_OG], } ) @@ -342,24 +325,7 @@ def test_load_hub_with_revision_with_dpo(self): "sequence_len": 1024, "rl": "dpo", "chat_template": "llama3", - "datasets": [ - { - "path": "fozziethebeat/alpaca_messages_2k_dpo_test", - "type": "chat_template.default", - "chat_template": "llama3", - "revision": "ea82cff", - "field_messages": "conversation", - "field_chosen": "chosen", - "field_rejected": "rejected", - "message_field_role": "role", - "message_field_content": "content", - "roles": { - "system": ["system"], - "user": ["user"], - "assistant": ["assistant"], - }, - } - ], + "datasets": [ALPACA_MESSAGES_CONFIG_REVISION], } ) diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py new file mode 100644 index 0000000000..2ac6415be5 --- /dev/null +++ b/tests/test_exact_deduplication.py @@ -0,0 +1,433 @@ +""" +Test suite for functions in the axolotl.utils.data.utils module, focusing on the deduplicate_and_log_datasets function. + +Additionally, this test suite includes tests for functions that indirectly call deduplicate_and_log_datasets during the execution of the preprocess command. +""" +import hashlib +import unittest +from unittest.mock import patch + +from constants import ALPACA_MESSAGES_CONFIG_REVISION, SPECIAL_TOKENS +from datasets import Dataset +from transformers import AutoTokenizer + +from axolotl.utils.data import prepare_dataset +from axolotl.utils.data.rl import load_prepare_dpo_datasets +from axolotl.utils.data.utils import deduplicate_and_log_datasets +from axolotl.utils.dict import DictDefault +from axolotl.utils.models import load_processor, load_tokenizer + + +def verify_deduplication(actual_dataset, expected_dataset, dataset_name): + """ + Validates deduplication results and size consistency. + + Parameters: + - actual_dataset: Deduplicated dataset. + - expected_dataset: Expected dataset. + - dataset_name: Name of the dataset (e.g., 'train' or 'eval'). + + Asserts: + - Datasets match in content. + - Dataset size matches unique row count. + """ + # Convert datasets to sets of tuples for unordered comparison + actual_rows = set(tuple(row.values()) for row in actual_dataset) + expected_rows = set(tuple(row.values()) for row in expected_dataset) + + # Verify deduplication correctness + assert actual_rows == expected_rows, f"Mismatch in {dataset_name} dataset" + + # Verify size consistency + assert len(actual_rows) == len( + actual_dataset + ), f"Size mismatch in {dataset_name} dataset after deduplication" + + +class TestDeduplicateIndividualFunctions(unittest.TestCase): + """ + test class for deduplication function in data utils + """ + + def setUp(self): + # Sample data with duplicates + self.data = { + "column1": ["apple", "banana", "apple", "orange", "banana"], + "column2": [1, 2, 1, 3, 2], + "column3": ["red", "yellow", "red", "orange", "yellow"], + } + + # Expected result after deduplication + self.expected_data = { + "column1": ["apple", "banana", "orange"], + "column2": [1, 2, 3], + "column3": ["red", "yellow", "orange"], + } + + # Convert to Dataset format + self.dataset = Dataset.from_dict(self.data) + self.expected_dataset = Dataset.from_dict(self.expected_data) + + def test_deduplication(self): + train_dataset, _, _ = deduplicate_and_log_datasets(train_dataset=self.dataset) + _, eval_dataset, _ = deduplicate_and_log_datasets(eval_dataset=self.dataset) + + verify_deduplication(train_dataset, self.expected_dataset, "train_dataset") + verify_deduplication(eval_dataset, self.expected_dataset, "eval_dataset") + + def test_datasets_are_none(self): + # Test when both datasets are None + train_dataset, eval_dataset, _ = deduplicate_and_log_datasets( + train_dataset=None, eval_dataset=None + ) + self.assertIsNone(train_dataset, "Expected train_dataset to be None") + self.assertIsNone(eval_dataset, "Expected eval_dataset to be None") + + def test_only_train_is_none(self): + # Test when only train_dataset is None + train_dataset, eval_dataset, _ = deduplicate_and_log_datasets( + train_dataset=None, eval_dataset=self.dataset + ) + self.assertIsNone(train_dataset, "Expected train_dataset to be None") + verify_deduplication(eval_dataset, self.expected_dataset, "eval_dataset") + + def test_only_eval_is_none(self): + # Test when only eval_dataset is None + train_dataset, eval_dataset, _ = deduplicate_and_log_datasets( + train_dataset=self.dataset, eval_dataset=None + ) + self.assertIsNone(eval_dataset, "Expected eval_dataset to be None") + verify_deduplication(train_dataset, self.expected_dataset, "train_dataset") + + def test_exact_duplicates(self): + # Test when datasets are exact duplicates + duplicate_data = { + "column1": ["apple", "apple", "apple"], + "column2": [1, 1, 1], + "column3": ["red", "red", "red"], + } + expected_data = {"column1": ["apple"], "column2": [1], "column3": ["red"]} + + # Convert to Dataset format + dataset = Dataset.from_dict(duplicate_data) + expected_dataset = Dataset.from_dict(expected_data) + + # Run deduplication + train_dataset, _, _ = deduplicate_and_log_datasets(train_dataset=dataset) + _, eval_dataset, _ = deduplicate_and_log_datasets(eval_dataset=dataset) + + verify_deduplication(train_dataset, expected_dataset, "train_dataset") + verify_deduplication(eval_dataset, expected_dataset, "eval_dataset") + + def test_partial_duplicates(self): + # Test when only part of the dataset is a duplicate + partial_duplicate_data = { + "column1": ["apple", "banana", "apple"], + "column2": [1, 2, 1], + "column3": ["red", "yellow", "red"], + } + expected_data = { + "column1": ["apple", "banana"], + "column2": [1, 2], + "column3": ["red", "yellow"], + } + + # Convert to Dataset format + dataset = Dataset.from_dict(partial_duplicate_data) + expected_dataset = Dataset.from_dict(expected_data) + + # Run deduplication + train_dataset, _, _ = deduplicate_and_log_datasets(train_dataset=dataset) + _, eval_dataset, _ = deduplicate_and_log_datasets(eval_dataset=dataset) + + verify_deduplication(train_dataset, expected_dataset, "train_dataset") + verify_deduplication(eval_dataset, expected_dataset, "eval_dataset") + + def test_combined_duplicates_empty(self): + # Test when only part of the dataset is a duplicate + partial_duplicate_data = { + "column1": ["apple", "banana", "apple"], + "column2": [1, 2, 1], + "column3": ["red", "yellow", "red"], + } + expected_data_train = { + "column1": ["apple", "banana"], + "column2": [1, 2], + "column3": ["red", "yellow"], + } + expected_data_eval = { + "column1": [], + "column2": [], + "column3": [], + } + + # Convert to Dataset format + dataset = Dataset.from_dict(partial_duplicate_data) + expected_dataset_train = Dataset.from_dict(expected_data_train) + expected_dataset_eval = Dataset.from_dict(expected_data_eval) + + # Run deduplication + train_dataset, eval_dataset, _ = deduplicate_and_log_datasets( + train_dataset=dataset, eval_dataset=dataset + ) + + verify_deduplication(train_dataset, expected_dataset_train, "train_dataset") + verify_deduplication(eval_dataset, expected_dataset_eval, "eval_dataset") + + def test_combined_duplicates_one(self): + # Test when only part of the dataset is a duplicate + partial_duplicate_data_train = { + "column1": ["apple", "banana", "apple"], + "column2": [1, 2, 1], + "column3": ["red", "yellow", "red"], + } + partial_duplicate_data_eval = { + "column1": ["apple", "orange", "apple"], + "column2": [1, 2, 1], + "column3": ["red", "orange", "red"], + } + expected_data_train = { + "column1": ["apple", "banana"], + "column2": [1, 2], + "column3": ["red", "yellow"], + } + expected_data_eval = { + "column1": ["orange"], + "column2": [2], + "column3": ["orange"], + } + + # Convert to Dataset format + dataset_train = Dataset.from_dict(partial_duplicate_data_train) + dataset_eval = Dataset.from_dict(partial_duplicate_data_eval) + expected_dataset_train = Dataset.from_dict(expected_data_train) + expected_dataset_eval = Dataset.from_dict(expected_data_eval) + + # Run deduplication + train_dataset, eval_dataset, _ = deduplicate_and_log_datasets( + train_dataset=dataset_train, eval_dataset=dataset_eval + ) + + verify_deduplication(train_dataset, expected_dataset_train, "train_dataset") + verify_deduplication(eval_dataset, expected_dataset_eval, "eval_dataset") + + +class TestDeduplicateRLDataset(unittest.TestCase): + """Test a configured dataloader with deduplication.""" + + def setUp(self) -> None: + self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") + self.tokenizer.add_special_tokens(SPECIAL_TOKENS) + self.cfg = DictDefault( + { + "tokenizer_config": "huggyllama/llama-7b", + "sequence_len": 1024, + "rl": "dpo", + "chat_template": "llama3", + "dataset_exact_deduplication": True, + "datasets": [ + ALPACA_MESSAGES_CONFIG_REVISION, + ALPACA_MESSAGES_CONFIG_REVISION, + ], + } + ) + + def test_load_with_deduplication(self): + """Verify that loading with deduplication removes duplicates.""" + + # Load the dataset using the deduplication setting + train_dataset, _ = load_prepare_dpo_datasets(self.cfg) + + # Verify that the dataset has been deduplicated + assert len(train_dataset) == 1800, "Dataset was not properly deduplicated" + + def test_load_without_deduplication(self): + """Verify that loading without deduplication retains duplicates.""" + self.cfg.dataset_exact_deduplication = False + # Load the dataset without deduplication + train_dataset, _ = load_prepare_dpo_datasets(self.cfg) + + # Verify that the dataset retains duplicates + assert ( + len(train_dataset) == 1800 * 2 + ), "Dataset deduplication occurred when it should not have" + + +class TestDeduplicateNonRL(unittest.TestCase): + """Test prepare_dataset function with different configurations.""" + + def setUp(self) -> None: + self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") + self.tokenizer.add_special_tokens(SPECIAL_TOKENS) + self.cfg_1 = DictDefault( + { + "tokenizer_config": "huggyllama/llama-7b", + "sequence_len": 1024, + "dataset_exact_deduplication": True, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "val_set_size": 0.0, + "gradient_accumulation_steps": 4, + "batch_size": 10, + "micro_batch_size": 10, + "num_epochs": 1, + } + ) + + def test_prepare_dataset_with_deduplication_train(self): + """Verify that prepare_dataset function processes the dataset correctly with deduplication.""" + self.cfg_1.dataset_exact_deduplication = True + + # Load tokenizer and processor + tokenizer = load_tokenizer(self.cfg_1) + processor = ( + load_processor(self.cfg_1, tokenizer=tokenizer) + if self.cfg_1.processor_type + else None + ) + + # Prepare dataset using the prepare_dataset function + train_dataset, _, _, _ = prepare_dataset( + self.cfg_1, + tokenizer, + processor=processor, + ) + + self.assertEqual( + len(train_dataset), + 2000, + "Train dataset should have 2000 samples after deduplication.", + ) + + def test_prepare_dataset_with_deduplication_eval(self): + """Verify that prepare_dataset function processes the dataset correctly with deduplication.""" + self.cfg_1.dataset_exact_deduplication = True + self.cfg_1.val_set_size = 0.5 + # Load tokenizer and processor + tokenizer = load_tokenizer(self.cfg_1) + processor = ( + load_processor(self.cfg_1, tokenizer=tokenizer) + if self.cfg_1.processor_type + else None + ) + + # Prepare dataset using the prepare_dataset function + _, eval_dataset, _, _ = prepare_dataset( + self.cfg_1, + tokenizer, + processor=processor, + ) + + self.assertEqual( + len(eval_dataset), + 1000, + "Eval dataset should have 2000 samples after deduplication.", + ) + + def test_prepare_dataset_without_deduplication(self): + """Verify that prepare_dataset function processes the dataset correctly without deduplication.""" + self.cfg_1.dataset_exact_deduplication = False + self.cfg_1.val_set_size = 0.1 + # Load tokenizer and processor + tokenizer = load_tokenizer(self.cfg_1) + processor = ( + load_processor(self.cfg_1, tokenizer=tokenizer) + if self.cfg_1.processor_type + else None + ) + + # Prepare dataset using the prepare_dataset function + train_dataset, eval_dataset, _, _ = prepare_dataset( + self.cfg_1, + tokenizer, + processor=processor, + ) + + # Verify that the dataset has been prepared correctly + self.assertEqual( + len(train_dataset), + 1800 * 2, + "Train dataset should have 3600 samples without deduplication.", + ) + self.assertEqual( + len(eval_dataset), + 200 * 2, + "Train dataset should have 400 samples after deduplication.", + ) + + +class TestWrongCollisions(unittest.TestCase): + """Creating mock datasets for testing wrong collisions""" + + def setUp(self): + self.train_data = {"text": ["sample 5", "sample 6"], "label": [1, 2]} + self.eval_data = { + "text": [ + "sample 5", + "sample 7", + ], # Different label but same text as in train_data + "label": [2, 3], + } + self.dataset_data = { + "text": ["sample 5", "sample 9", "sample 5"], + "label": [1, 2, 8], + } + self.train_dataset = Dataset.from_dict(self.train_data) + self.eval_dataset = Dataset.from_dict(self.eval_data) + self.dataset = Dataset.from_dict(self.dataset_data) + + @patch( + "axolotl.utils.data.utils.sha256", + side_effect=lambda x: hashlib.sha256( + "forced_collision_hash".encode("utf-8") + ).hexdigest() + if "sample 5" in x + else hashlib.sha256(x.encode("utf-8")).hexdigest(), + ) + def test_deduplication_wrong_collision_train_eval(self, _mock_sha256): + dedup_train, dedup_eval, _ = deduplicate_and_log_datasets( + train_dataset=self.train_dataset, eval_dataset=self.eval_dataset + ) + self.assertEqual( + len(dedup_train), + 2, + "train dataset should not deduplicate rows with forced hash collisions but different labels.", + ) + self.assertEqual( + len(dedup_eval), + 2, + "Eval dataset should not deduplicate rows with forced hash collisions but different labels.", + ) + self.assertEqual( + len(dedup_eval), + len(self.eval_dataset), + "The output eval dataset should have the same number of rows as the input eval dataset.", + ) + self.assertEqual( + str(dedup_eval), + str(self.eval_dataset), + "The string representation of the output eval dataset should be identical to the input eval dataset.", + ) + + def test_deduplication_dataset_only(self): + _, _, dedup_dataset = deduplicate_and_log_datasets(dataset=self.dataset) + self.assertEqual( + len(dedup_dataset), 3, "Dataset should have all original values" + ) + self.assertEqual( + str(dedup_dataset), + str(self.dataset), + "The string representation of the output dataset should not differ.", + ) + + +if __name__ == "__main__": + unittest.main() From ce5bcff750c95530cc2dab2e0fe5aa0d547af923 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 2 Dec 2024 17:28:58 -0500 Subject: [PATCH 0290/1405] various tests fixes for flakey tests (#2110) * add mhenrichsen/alpaca_2k_test with revision dataset download fixture for flaky tests * log slowest tests * pin pynvml==11.5.3 * fix load local hub path * optimize for speed w smaller models and val_set_size * replace pynvml * make the resume from checkpoint e2e faster * make tests smaller --- cicd/cicd.sh | 6 ++--- requirements.txt | 2 +- src/axolotl/utils/bench.py | 23 +++++++++++++++----- src/axolotl/utils/data/sft.py | 2 +- tests/conftest.py | 20 ++++++++++++++--- tests/core/test_trainer_builder.py | 7 +++--- tests/e2e/patched/test_fa_xentropy.py | 6 ++--- tests/e2e/patched/test_resume.py | 29 +++++++++++++------------ tests/e2e/patched/test_unsloth_qlora.py | 6 ++--- tests/e2e/test_optimizers.py | 6 +++-- tests/e2e/test_relora_llama.py | 1 + tests/test_datasets.py | 4 ++-- tests/test_perplexity.py | 10 +++++---- 13 files changed, 78 insertions(+), 44 deletions(-) diff --git a/cicd/cicd.sh b/cicd/cicd.sh index e199e112ff..7a4a315044 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -pytest -n8 --ignore=tests/e2e/ /workspace/axolotl/tests/ -pytest -n1 --dist loadfile -v /workspace/axolotl/tests/e2e/patched/ /workspace/axolotl/tests/e2e/integrations/ -pytest --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ +pytest --durations=10 -n8 --ignore=tests/e2e/ /workspace/axolotl/tests/ +pytest --durations=10 -n1 --dist loadfile -v /workspace/axolotl/tests/e2e/patched/ /workspace/axolotl/tests/e2e/integrations/ +pytest --durations=10 --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ diff --git a/requirements.txt b/requirements.txt index f2086d4427..456c63ca51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ numpy>=1.24.4,<=2.0.1 evaluate==0.4.1 scipy scikit-learn==1.4.2 -pynvml +nvidia-ml-py==12.560.30 art gradio==3.50.2 tensorboard diff --git a/src/axolotl/utils/bench.py b/src/axolotl/utils/bench.py index 57471ae0d8..3d338aff10 100644 --- a/src/axolotl/utils/bench.py +++ b/src/axolotl/utils/bench.py @@ -1,13 +1,24 @@ """Benchmarking and measurement utilities""" import functools -import pynvml import torch -from pynvml.nvml import NVMLError from transformers.utils.import_utils import is_torch_npu_available from axolotl.utils.distributed import get_device_type +try: + from pynvml import ( + NVMLError, + nvmlDeviceGetHandleByIndex, + nvmlDeviceGetMemoryInfo, + nvmlInit, + ) +except ImportError: + NVMLError = None + nvmlDeviceGetHandleByIndex = None + nvmlDeviceGetMemoryInfo = None + nvmlInit = None + def check_cuda_device(default_value): """ @@ -68,10 +79,12 @@ def gpu_memory_usage_smi(device=0): device = device.index if isinstance(device, str) and device.startswith("cuda:"): device = int(device[5:]) + if not nvmlInit: + return 0.0 try: - pynvml.nvmlInit() - handle = pynvml.nvmlDeviceGetHandleByIndex(device) - info = pynvml.nvmlDeviceGetMemoryInfo(handle) + nvmlInit() + handle = nvmlDeviceGetHandleByIndex(device) + info = nvmlDeviceGetMemoryInfo(handle) return info.used / 1024.0**3 except NVMLError: return 0.0 diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 0bee4dd5cf..4ed16e3582 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -179,7 +179,7 @@ def load_tokenized_prepared_datasets( + "|".join( sorted( [ - f"{d.path}: {d.type}: {d.shards}: {d.conversation}{d.split}" + f"{d.path}:{d.type}:{d.shards}:{d.conversation}{d.split}" for d in cfg_datasets ] ) diff --git a/tests/conftest.py b/tests/conftest.py index a8bf03ac01..4479e676f4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,12 @@ def download_smollm2_135m_model(): snapshot_download("HuggingFaceTB/SmolLM2-135M") +@pytest.fixture(scope="session", autouse=True) +def download_llama_68m_random_model(): + # download the model + snapshot_download("JackFram/llama-68m") + + @pytest.fixture(scope="session", autouse=True) def download_qwen_2_5_half_billion_model(): # download the model @@ -22,18 +28,26 @@ def download_qwen_2_5_half_billion_model(): @pytest.fixture(scope="session", autouse=True) def download_tatsu_lab_alpaca_dataset(): - # download the model + # download the dataset snapshot_download("tatsu-lab/alpaca", repo_type="dataset") @pytest.fixture(scope="session", autouse=True) def download_mhenrichsen_alpaca_2k_dataset(): - # download the model + # download the dataset snapshot_download("mhenrichsen/alpaca_2k_test", repo_type="dataset") +@pytest.fixture(scope="session", autouse=True) +def download_mhenrichsen_alpaca_2k_w_revision_dataset(): + # download the dataset + snapshot_download( + "mhenrichsen/alpaca_2k_test", repo_type="dataset", revision="d05c1cb" + ) + + def download_mlabonne_finetome_100k_dataset(): - # download the model + # download the dataset snapshot_download("mlabonne/FineTome-100k", repo_type="dataset") diff --git a/tests/core/test_trainer_builder.py b/tests/core/test_trainer_builder.py index 82455922ef..558d3cb956 100644 --- a/tests/core/test_trainer_builder.py +++ b/tests/core/test_trainer_builder.py @@ -14,9 +14,7 @@ def fixture_cfg(): cfg = DictDefault( { - "base_model": "TinyLlama/TinyLlama-1.1B-Chat-v0.6", - "model_type": "AutoModelForCausalLM", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", "micro_batch_size": 1, "gradient_accumulation_steps": 1, "learning_rate": 0.00005, @@ -33,6 +31,9 @@ def fixture_cfg(): "dataloader_num_workers": 1, "dataloader_pin_memory": True, "model_config_type": "llama", + "special_tokens": { + "pad_token": "<|endoftext|>", + }, } ) diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index 7ca1c08365..76ea1a9348 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -51,11 +51,11 @@ def test_lora_packing_fa_cross_entropy(self, temp_dir, gradient_accumulation_ste "flash_attn_cross_entropy": True, "load_in_8bit": True, "adapter": "lora", - "lora_r": 32, - "lora_alpha": 64, + "lora_r": 8, + "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.2, + "val_set_size": 0.05, "special_tokens": { "pad_token": "<|endoftext|>", }, diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index c0e791f38a..44d3d9e837 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -29,23 +29,24 @@ class TestResumeLlama(unittest.TestCase): """ @with_temp_dir - def test_resume_qlora_packed(self, temp_dir): + def test_resume_lora_packed(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 1024, "sample_packing": True, "flash_attention": True, - "load_in_4bit": True, - "adapter": "qlora", - "lora_r": 32, - "lora_alpha": 64, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, - "special_tokens": {}, + "val_set_size": 0.01, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, "datasets": [ { "path": "vicgalle/alpaca-gpt4", @@ -57,11 +58,11 @@ def test_resume_qlora_packed(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_8bit", "lr_scheduler": "cosine", - "save_steps": 10, + "save_steps": 3, "save_total_limit": 5, - "max_steps": 40, + "max_steps": 15, "use_tensorboard": True, } ) @@ -77,7 +78,7 @@ def test_resume_qlora_packed(self, temp_dir): resume_cfg = cfg | DictDefault( { - "resume_from_checkpoint": f"{temp_dir}/checkpoint-30/", + "resume_from_checkpoint": f"{temp_dir}/checkpoint-9/", } ) normalize_config(resume_cfg) @@ -93,4 +94,4 @@ def test_resume_qlora_packed(self, temp_dir): ) pattern = r"first_step\s+(\d+)" first_steps = int(re.findall(pattern, res.stdout)[0]) - assert first_steps == 31 + assert first_steps == 10 diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index 805b150037..3d7e794f1c 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -42,7 +42,7 @@ def test_unsloth_llama_qlora_fa2(self, temp_dir, sample_packing): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.2, + "val_set_size": 0.05, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -92,7 +92,7 @@ def test_unsloth_llama_qlora_unpacked(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.2, + "val_set_size": 0.05, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -146,7 +146,7 @@ def test_unsloth_llama_qlora_unpacked_no_fa2_fp16(self, temp_dir, sdp_attention) "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.2, + "val_set_size": 0.05, "special_tokens": { "pad_token": "<|endoftext|>", }, diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index af5445461c..63c46c2a2d 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -94,6 +94,7 @@ def test_adopt_adamw(self, temp_dir): }, ], "num_epochs": 1, + "max_steps": 5, "micro_batch_size": 8, "gradient_accumulation_steps": 1, "output_dir": temp_dir, @@ -115,7 +116,7 @@ def test_fft_schedule_free_adamw(self, temp_dir): { "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 1024, - "val_set_size": 0.1, + "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -126,13 +127,14 @@ def test_fft_schedule_free_adamw(self, temp_dir): }, ], "num_epochs": 1, - "micro_batch_size": 4, + "micro_batch_size": 2, "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "schedule_free_adamw", "lr_scheduler": "constant", "save_safetensors": True, + "max_steps": 10, } ) # pylint: disable=duplicate-code diff --git a/tests/e2e/test_relora_llama.py b/tests/e2e/test_relora_llama.py index 4ba130c9dc..5de5db11b7 100644 --- a/tests/e2e/test_relora_llama.py +++ b/tests/e2e/test_relora_llama.py @@ -52,6 +52,7 @@ def test_relora(self, temp_dir): ], "warmup_steps": 15, "num_epochs": 2, + "max_steps": 51, # at least 2x relora_steps "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, diff --git a/tests/test_datasets.py b/tests/test_datasets.py index f3bed00fd0..b1ecfd6d52 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -67,7 +67,7 @@ def test_load_hub(self): def test_load_local_hub(self): """Niche use case. Verify that a local copy of a hub dataset can be loaded""" with tempfile.TemporaryDirectory() as tmp_dir: - tmp_ds_path = Path("mhenrichsen/alpaca_2k_test") + tmp_ds_path = Path(tmp_dir) / "mhenrichsen/alpaca_2k_test" tmp_ds_path.mkdir(parents=True, exist_ok=True) snapshot_download( repo_id="mhenrichsen/alpaca_2k_test", @@ -89,7 +89,7 @@ def test_load_local_hub(self): "ds_type": "parquet", "type": "alpaca", "data_files": [ - "mhenrichsen/alpaca_2k_test/alpaca_2000.parquet", + f"{tmp_ds_path}/alpaca_2000.parquet", ], }, ], diff --git a/tests/test_perplexity.py b/tests/test_perplexity.py index 8688827cec..b32cd52835 100644 --- a/tests/test_perplexity.py +++ b/tests/test_perplexity.py @@ -7,7 +7,7 @@ from axolotl.utils.callbacks.perplexity import Perplexity -MODEL_NAME = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" +MODEL_NAME = "HuggingFaceTB/SmolLM2-135M" @fixture() @@ -22,7 +22,9 @@ def model(): @fixture() def tokenizer(): - return AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) + tokenizer_ = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) + tokenizer_.add_special_tokens({"pad_token": "<|endoftext|>"}) + return tokenizer_ def test_perplexity_longer_than_stride(model, metric): @@ -33,7 +35,7 @@ def test_perplexity_longer_than_stride(model, metric): """ result = metric.compute(model, [sample_text]) ppl = result["score"] - assert round(ppl, 2) == 5.37 + assert round(ppl, 2) == 7.41 def test_perplexity_short(model, metric): @@ -41,4 +43,4 @@ def test_perplexity_short(model, metric): sample_text = "Once upon a time, there was a little car named Beep. Beep loved to go fast and play in the sun." result = metric.compute(model, [sample_text]) ppl = result["score"] - assert round(ppl, 2) == 10.02 + assert round(ppl, 2) == 10.33 From a4f4a56d77fc1e6a9211737343d2b5c119502bfc Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 2 Dec 2024 18:27:46 -0500 Subject: [PATCH 0291/1405] build causal_conv1d and mamba-ssm into the base image (#2113) * build causal_conv1d and mamba-ssm into the base image * also build base images on changes to Dockerfile-base and base workflow yaml --- .github/workflows/base.yml | 10 ++++++++++ docker/Dockerfile-base | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index e086a5f961..640d2cd7ae 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -1,6 +1,16 @@ name: ci-cd-base on: + push: + branches: + - "main" + paths: + - 'Dockerfile-base' + - '.github/workflows/base.yml' + pull_request: + paths: + - 'Dockerfile-base' + - '.github/workflows/base.yml' workflow_dispatch: jobs: diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 3f13bba30a..7eab3b3e43 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -29,7 +29,9 @@ ENV PATH="/root/miniconda3/envs/py${PYTHON_VERSION}/bin:${PATH}" WORKDIR /workspace RUN python3 -m pip install --upgrade pip && pip3 install packaging && \ - python3 -m pip install --no-cache-dir -U torch==${PYTORCH_VERSION}+cu${CUDA} --extra-index-url https://download.pytorch.org/whl/cu$CUDA + python3 -m pip install --no-cache-dir -U torch==${PYTORCH_VERSION}+cu${CUDA} --extra-index-url https://download.pytorch.org/whl/cu$CUDA && \ + python3 -m pip install --no-cache-dir "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" && \ + python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" RUN git lfs install --skip-repo && \ pip3 install awscli && \ From 53963c792cd62c4bc75d2be845adba6ec8f41724 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 2 Dec 2024 18:32:29 -0500 Subject: [PATCH 0292/1405] make the eval size smaller for the resume test (#2111) [skip ci] --- tests/e2e/patched/test_resume.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index 44d3d9e837..7d82ea8c37 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -6,7 +6,6 @@ import os import re import subprocess -import unittest from pathlib import Path from transformers.utils import is_torch_bf16_gpu_available @@ -17,18 +16,17 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import most_recent_subdir, with_temp_dir +from ..utils import most_recent_subdir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" -class TestResumeLlama(unittest.TestCase): +class TestResumeLlama: """ Test case for resuming training of llama models """ - @with_temp_dir def test_resume_lora_packed(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -43,7 +41,7 @@ def test_resume_lora_packed(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.01, + "val_set_size": 0.001, "special_tokens": { "pad_token": "<|endoftext|>", }, From 9f6d0b558781528441d0c36a46d54b25183245ee Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 2 Dec 2024 20:14:40 -0500 Subject: [PATCH 0293/1405] use pytest sugar and verbose for more info during ci (#2112) [skip ci] * use pytest sugar and verbose for more info during ci * also run test suite when test requirements or cicd.sh changes * also on PR too --- .github/workflows/tests.yml | 4 ++++ cicd/cicd.sh | 6 +++--- requirements-tests.txt | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5b380c7845..249dfd9e4c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,11 +8,15 @@ on: - '**.py' - 'requirements.txt' - '.github/workflows/*.yml' + - 'requirements-tests.txt' + - 'cicd/cicd.sh' pull_request: paths: - '**.py' - 'requirements.txt' - '.github/workflows/*.yml' + - 'requirements-tests.txt' + - 'cicd/cicd.sh' workflow_dispatch: # Cancel jobs on the same ref if a new one is triggered diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 7a4a315044..68c40bb78d 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -pytest --durations=10 -n8 --ignore=tests/e2e/ /workspace/axolotl/tests/ -pytest --durations=10 -n1 --dist loadfile -v /workspace/axolotl/tests/e2e/patched/ /workspace/axolotl/tests/e2e/integrations/ -pytest --durations=10 --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ +pytest -v --durations=10 -n8 --ignore=tests/e2e/ /workspace/axolotl/tests/ +pytest -v --durations=10 -n1 --dist loadfile -v /workspace/axolotl/tests/e2e/patched/ /workspace/axolotl/tests/e2e/integrations/ +pytest -v --durations=10 --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ diff --git a/requirements-tests.txt b/requirements-tests.txt index 7a34809da8..0022980e90 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,3 +1,4 @@ pytest pytest-xdist pytest-retry +pytest-sugar From d5f58b6509ac6975d5c325fde0ef34de71b6db13 Mon Sep 17 00:00:00 2001 From: Sunny Liu <22844540+bursteratom@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:15:39 -0500 Subject: [PATCH 0294/1405] Check torch version for ADOPT optimizer + integrating new ADOPT updates (#2104) * added torch check for adopt, wip * lint * gonna put torch version checking somewhere else * added ENVcapabilities class for torch version checking * lint + pydantic * ENVCapabilities -> EnvCapabilities * forgot to git add v0_4_1/__init__.py * removed redundancy * add check if env_capabilities not specified * make env_capabilities compulsory [skip e2e] * fixup env_capabilities * modified test_validation.py to accomodate env_capabilities * adopt torch version test [skip e2e] * raise error * test correct torch version * test torch version above requirement * Update src/axolotl/utils/config/models/input/v0_4_1/__init__.py Co-authored-by: Wing Lian * removed unused is_totch_min --------- Co-authored-by: Wing Lian Co-authored-by: Wing Lian --- docs/config.qmd | 2 +- src/axolotl/cli/__init__.py | 7 +- src/axolotl/core/trainer_builder.py | 4 +- src/axolotl/utils/config/__init__.py | 20 ++- .../config/models/input/v0_4_1/__init__.py | 22 ++- .../utils/config/models/internals/__init__.py | 6 + src/axolotl/utils/optimizers/adopt.py | 135 +++++++++++------- tests/e2e/utils.py | 2 +- tests/test_validation.py | 35 +++++ tests/test_validation_dataset.py | 12 ++ 10 files changed, 184 insertions(+), 61 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index bc3730095d..f01a2ce267 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -409,7 +409,7 @@ lr_div_factor: # Learning rate div factor # - adamw_torch_fused # - adamw_torch_xla # - adamw_apex_fused -# - adopt_adamw (only for torch version >= 2.5.1) +# - adopt_adamw (an EXPERIMENTAL optimizer, only for torch version >= 2.5.1) # - adafactor # - adamw_anyprecision # - sgd diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 86cc30a401..2ef78e07d8 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -100,8 +100,8 @@ def print_dep_versions(): print("*" * 40) print("**** Axolotl Dependency Versions *****") for pkg in packages: - version = _is_package_available(pkg, return_version=True) - print(f"{pkg: >{max_len}}: {version[1]: <15}") + pkg_version = _is_package_available(pkg, return_version=True) + print(f"{pkg: >{max_len}}: {pkg_version[1]: <15}") print("*" * 40) @@ -444,6 +444,9 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs): "n_gpu": int(os.environ.get("WORLD_SIZE", 1)), "compute_capability": gpu_version, }, + env_capabilities={ + "torch_version": str(torch.__version__).split("+", maxsplit=1)[0] + }, ) prepare_optim_env(cfg) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index e4bf7de229..9b03563e0c 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -562,7 +562,9 @@ def create_optimizer(self): self.optimizer = ( # pylint: disable=attribute-defined-outside-init ADOPT( - optimizer_grouped_parameters, decoupled=True, **optimizer_kwargs + optimizer_grouped_parameters, + decouple=True, + **optimizer_kwargs, ) ) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 0100f23ea5..422ed78efb 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -229,7 +229,11 @@ def normalize_cfg_datasets(cfg): cfg.datasets[idx].chat_template_jinja = cfg.chat_template_jinja -def validate_config(cfg: DictDefault, capabilities: Optional[dict] = None): +def validate_config( + cfg: DictDefault, + capabilities: Optional[dict] = None, + env_capabilities: Optional[dict] = None, +): AxolotlConfigWCapabilities = AxolotlConfigWCapabilitiesBase AxolotlInputConfig = AxolotlInputConfigBase @@ -239,14 +243,24 @@ def validate_config(cfg: DictDefault, capabilities: Optional[dict] = None): AxolotlInputConfig, # pylint: disable=invalid-name ) = merge_input_args() - if capabilities: + if capabilities or env_capabilities: + if (capabilities and not env_capabilities) or ( + env_capabilities and not capabilities + ): + raise ValueError( + "Both capabilities and env_capabilities must be provided or not provided." + ) + return DictDefault( dict( AxolotlConfigWCapabilities( - **cfg.to_dict(), capabilities=capabilities + **cfg.to_dict(), + capabilities=capabilities, + env_capabilities=env_capabilities, ).model_dump(exclude_none=True) ) ) + return DictDefault( dict(AxolotlInputConfig(**cfg.to_dict()).model_dump(exclude_none=True)) ) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 1ac7efbfa5..0f01a7cadc 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -9,6 +9,7 @@ from enum import Enum from typing import Annotated, Any, Dict, List, Literal, Optional, Tuple, Union +from packaging import version from pydantic import ( BaseModel, Field, @@ -21,7 +22,7 @@ from transformers.training_args import OptimizerNames from transformers.utils.import_utils import is_torch_npu_available -from axolotl.utils.config.models.internals import GPUCapabilities +from axolotl.utils.config.models.internals import EnvCapabilities, GPUCapabilities LOG = logging.getLogger("axolotl.utils.config.models.input") @@ -1477,6 +1478,7 @@ class AxolotlConfigWCapabilities(AxolotlInputConfig): """wrapper to valdiate gpu capabilities with the configured options""" capabilities: GPUCapabilities + env_capabilities: EnvCapabilities @model_validator(mode="after") def check_bf16(self): @@ -1551,3 +1553,21 @@ def check_multigpu_unsloth(cls, data): "unsloth_lora_mlp, unsloth_lora_qkv, and unsloth_lora_o are not compatible with multi-GPU training." ) return data + + @model_validator(mode="before") + @classmethod + def check_adopt_torch_version(cls, data): + if (data.get("optimizer") is not None) and ("adopt" in data.get("optimizer")): + env_capabilities = data.get("env_capabilities", {}) + torch_version = env_capabilities.get("torch_version") + + if torch_version is None: + import torch + + torch_version = str(torch.__version__).split("+", maxsplit=1)[0] + + if version.parse(torch_version) < version.parse("2.5.1"): + raise ValueError( + "ADOPT optimizer is incompatible with torch version < 2.5.1" + ) + return data diff --git a/src/axolotl/utils/config/models/internals/__init__.py b/src/axolotl/utils/config/models/internals/__init__.py index dd742caf45..7b4a12e035 100644 --- a/src/axolotl/utils/config/models/internals/__init__.py +++ b/src/axolotl/utils/config/models/internals/__init__.py @@ -12,3 +12,9 @@ class GPUCapabilities(BaseModel): n_gpu: int = Field(default=1) n_node: int = Field(default=1) compute_capability: Optional[str] = Field(default=None) + + +class EnvCapabilities(BaseModel): + """model to manage the environment capabilities statically""" + + torch_version: Optional[str] = Field(default=None) diff --git a/src/axolotl/utils/optimizers/adopt.py b/src/axolotl/utils/optimizers/adopt.py index 7e133285f7..36217730b3 100644 --- a/src/axolotl/utils/optimizers/adopt.py +++ b/src/axolotl/utils/optimizers/adopt.py @@ -6,21 +6,29 @@ """ # mypy: ignore-errors # pylint: skip-file +# flake8: noqa # mypy: allow-untyped-decorators # mypy: allow-untyped-defs -from typing import List, Optional, Tuple, Union, cast +from typing import Callable, List, Optional, Tuple, Union, cast import torch from torch import Tensor -from torch.optim.optimizer import ( +from torch.optim.optimizer import ( # DeviceDict,; _capturable_doc,; _differentiable_doc,; _foreach_doc,; _fused_doc,; _maximize_doc,; _stack_if_compiling, + DeviceDict, Optimizer, ParamsT, + _capturable_doc, _default_to_fused_or_foreach, _device_dtype_check_for_fused, + _differentiable_doc, _disable_dynamo_if_unsupported, + _foreach_doc, + _fused_doc, _get_capturable_supported_devices, _get_scalar_dtype, _get_value, + _maximize_doc, + _stack_if_compiling, _use_grad_for_differentiable, _view_as_real, ) @@ -35,8 +43,9 @@ def __init__( lr: Union[float, Tensor] = 1e-3, betas: Tuple[float, float] = (0.9, 0.9999), eps: float = 1e-6, + clip_lambda: Optional[Callable[[int], float]] = lambda step: step**0.25, weight_decay: float = 0.0, - decoupled: bool = False, + decouple: bool = False, *, foreach: Optional[bool] = None, maximize: bool = False, @@ -62,12 +71,14 @@ def __init__( if not 0.0 <= weight_decay: raise ValueError(f"Invalid weight_decay value: {weight_decay}") + self.clip_lambda = clip_lambda + defaults = dict( lr=lr, betas=betas, eps=eps, weight_decay=weight_decay, - decoupled=decoupled, + decouple=decouple, maximize=maximize, foreach=foreach, capturable=capturable, @@ -219,8 +230,9 @@ def step(self, closure=None): beta1=beta1, beta2=beta2, lr=group["lr"], + clip_lambda=self.clip_lambda, weight_decay=group["weight_decay"], - decoupled=group["decoupled"], + decouple=group["decouple"], eps=group["eps"], maximize=group["maximize"], foreach=group["foreach"], @@ -247,8 +259,9 @@ def _single_tensor_adopt( beta1: float, beta2: float, lr: Union[float, Tensor], + clip_lambda: Optional[Callable[[int], float]], weight_decay: float, - decoupled: bool, + decouple: bool, eps: float, maximize: bool, capturable: bool, @@ -276,14 +289,10 @@ def _single_tensor_adopt( and param.device.type in capturable_supported_devices ), f"If capturable=True, params and state_steps must be on supported devices: {capturable_supported_devices}." - # update step - step_t += 1 + step = step_t if capturable or differentiable else _get_value(step_t) - if weight_decay != 0: - if decoupled: - param.add_(param, alpha=-lr * weight_decay) - else: - grad = grad.add(param, alpha=weight_decay) + if weight_decay != 0 and not decouple: + grad = grad.add(param, alpha=weight_decay) if torch.is_complex(param): grad = torch.view_as_real(grad) @@ -293,20 +302,29 @@ def _single_tensor_adopt( exp_avg_sq = torch.view_as_real(exp_avg_sq) param = torch.view_as_real(param) - step = step_t if capturable or differentiable else _get_value(step_t) - if step == 1: + if step == 0: exp_avg_sq.addcmul_(grad, grad.conj()) + # update step + step_t += 1 continue + if weight_decay != 0 and decouple: + param.add_(param, alpha=-lr * weight_decay) + denom = torch.clamp(exp_avg_sq.sqrt(), eps) - if step == 2: - exp_avg.addcdiv_(grad, denom) - else: - exp_avg.mul_(beta1).addcdiv_(grad, denom, value=1 - beta1) + normed_grad = grad.div(denom) + if clip_lambda is not None: + clip = clip_lambda(step) + normed_grad.clamp_(-clip, clip) + + exp_avg.lerp_(normed_grad, 1 - beta1) param.add_(exp_avg, alpha=-lr) exp_avg_sq.mul_(beta2).addcmul_(grad, grad.conj(), value=1 - beta2) + # update step + step_t += 1 + def _multi_tensor_adopt( params: List[Tensor], @@ -321,8 +339,9 @@ def _multi_tensor_adopt( beta1: float, beta2: float, lr: Union[float, Tensor], + clip_lambda: Optional[Callable[[int], float]], weight_decay: float, - decoupled: bool, + decouple: bool, eps: float, maximize: bool, capturable: bool, @@ -376,45 +395,44 @@ def _multi_tensor_adopt( if maximize: device_grads = torch._foreach_neg(device_grads) # type: ignore[assignment] - # Update steps - # If steps are on CPU, foreach will fall back to the slow path, which is a for-loop calling t.add(1) over - # and over. 1 will then be wrapped into a Tensor over and over again, which is slower than if we just - # wrapped it once now. The alpha is required to assure we go to the right overload. - if not torch._utils.is_compiling() and device_state_steps[0].is_cpu: - torch._foreach_add_( - device_state_steps, torch.tensor(1.0, device="cpu"), alpha=1.0 - ) - else: - torch._foreach_add_(device_state_steps, 1) + if weight_decay != 0 and not decouple: + # Re-use the intermediate memory (device_grads) already allocated for maximize + if maximize: + torch._foreach_add_(device_grads, device_params, alpha=weight_decay) + else: + device_grads = torch._foreach_add( # type: ignore[assignment] + device_grads, device_params, alpha=weight_decay + ) + + if device_state_steps[0] == 0: + torch._foreach_addcmul_(device_exp_avg_sqs, device_grads, device_grads) - if weight_decay != 0: - if decoupled: + # Update steps + # If steps are on CPU, foreach will fall back to the slow path, which is a for-loop calling t.add(1) over + # and over. 1 will then be wrapped into a Tensor over and over again, which is slower than if we just + # wrapped it once now. The alpha is required to assure we go to the right overload. + if not torch._utils.is_compiling() and device_state_steps[0].is_cpu: torch._foreach_add_( - device_params, device_params, alpha=-lr * weight_decay + device_state_steps, torch.tensor(1.0, device="cpu"), alpha=1.0 ) else: - # Re-use the intermediate memory (device_grads) already allocated for maximize - if maximize: - torch._foreach_add_(device_grads, device_params, alpha=weight_decay) - else: - device_grads = torch._foreach_add( # type: ignore[assignment] - device_grads, device_params, alpha=weight_decay - ) + torch._foreach_add_(device_state_steps, 1) - if device_state_steps[0] == 1: - torch._foreach_addcmul_(device_exp_avg_sqs, device_grads, device_grads) continue + if weight_decay != 0 and decouple: + torch._foreach_add_(device_params, device_params, alpha=-lr * weight_decay) + exp_avg_sq_sqrt = torch._foreach_sqrt(device_exp_avg_sqs) - exp_avg_sq_sqrt = torch._foreach_maximum(exp_avg_sq_sqrt, eps) + torch._foreach_maximum_(exp_avg_sq_sqrt, eps) - if device_state_steps[0] == 2: - torch._foreach_addcdiv_(device_exp_avgs, device_grads, exp_avg_sq_sqrt) - else: - torch._foreach_mul_(device_exp_avgs, beta1) - torch._foreach_addcdiv_( - device_exp_avgs, device_grads, exp_avg_sq_sqrt, value=1 - beta1 - ) + normed_grad = torch._foreach_div(device_grads, exp_avg_sq_sqrt) + if clip_lambda is not None: + clip = clip_lambda(device_state_steps[0]) + torch._foreach_maximum_(normed_grad, -clip) + torch._foreach_minimum_(normed_grad, clip) + + torch._foreach_lerp_(device_exp_avgs, normed_grad, 1 - beta1) torch._foreach_add_(device_params, device_exp_avgs, alpha=-lr) torch._foreach_mul_(device_exp_avg_sqs, beta2) @@ -422,6 +440,17 @@ def _multi_tensor_adopt( device_exp_avg_sqs, device_grads, device_grads, value=1 - beta2 ) + # Update steps + # If steps are on CPU, foreach will fall back to the slow path, which is a for-loop calling t.add(1) over + # and over. 1 will then be wrapped into a Tensor over and over again, which is slower than if we just + # wrapped it once now. The alpha is required to assure we go to the right overload. + if not torch._utils.is_compiling() and device_state_steps[0].is_cpu: + torch._foreach_add_( + device_state_steps, torch.tensor(1.0, device="cpu"), alpha=1.0 + ) + else: + torch._foreach_add_(device_state_steps, 1) + @_disable_dynamo_if_unsupported(single_tensor_fn=_single_tensor_adopt) def adopt( @@ -443,8 +472,9 @@ def adopt( beta1: float, beta2: float, lr: Union[float, Tensor], + clip_lambda: Optional[Callable[[int], float]], weight_decay: float, - decoupled: bool, + decouple: bool, eps: float, maximize: bool, ): @@ -497,8 +527,9 @@ def adopt( beta1=beta1, beta2=beta2, lr=lr, + clip_lambda=clip_lambda, weight_decay=weight_decay, - decoupled=decoupled, + decouple=decouple, eps=eps, maximize=maximize, capturable=capturable, diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 439f6ac6f8..92e647e678 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -53,7 +53,7 @@ def is_min_2_3_1(): def require_torch_2_5_1(test_case): """ - Decorator marking a test that requires torch >= 2.3.1 + Decorator marking a test that requires torch >= 2.5.1 """ def is_min_2_5_1(): diff --git a/tests/test_validation.py b/tests/test_validation.py index 491f230c33..2e6fbab101 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -672,6 +672,9 @@ def test_merge_lora_no_bf16_fail(self, minimal_cfg): { "bf16": True, "capabilities": {"bf16": False}, + "env_capabilities": { + "torch_version": "2.5.1", + }, } ) | minimal_cfg @@ -1160,6 +1163,38 @@ def test_eval_strategy_remap(self, minimal_cfg): in self._caplog.records[0].message ) + def test_torch_version_adopt_req(self, minimal_cfg): + cfg = ( + DictDefault( + { + "optimizer": "adopt_adamw", + } + ) + | minimal_cfg + ) + + with pytest.raises( + ValueError, + match=r".*ADOPT optimizer is incompatible with torch version*", + ): + env_capabilities = {"torch_version": "2.3.0"} + capabilities = {"bf16": False} + _ = validate_config( + cfg, capabilities=capabilities, env_capabilities=env_capabilities + ) + + env_capabilities = {"torch_version": "2.5.1"} + capabilities = {"bf16": False} + _ = validate_config( + cfg, capabilities=capabilities, env_capabilities=env_capabilities + ) + + env_capabilities = {"torch_version": "2.5.2"} + capabilities = {"bf16": False} + _ = validate_config( + cfg, capabilities=capabilities, env_capabilities=env_capabilities + ) + class TestValidationCheckModelConfig(BaseValidation): """ diff --git a/tests/test_validation_dataset.py b/tests/test_validation_dataset.py index 14f9d34627..89f642051b 100644 --- a/tests/test_validation_dataset.py +++ b/tests/test_validation_dataset.py @@ -72,6 +72,9 @@ def _check_config(): "n_gpu": 1, "compute_capability": "8.0", }, + env_capabilities={ + "torch_version": "2.5.1", + }, ) _check_config() @@ -124,6 +127,9 @@ def _check_config(): "n_gpu": 1, "compute_capability": "8.0", }, + env_capabilities={ + "torch_version": "2.5.1", + }, ) _check_config() @@ -177,6 +183,9 @@ def _check_config(): "n_gpu": 1, "compute_capability": "8.0", }, + env_capabilities={ + "torch_version": "2.5.1", + }, ) _check_config() @@ -231,6 +240,9 @@ def _check_config(): "n_gpu": 1, "compute_capability": "8.0", }, + env_capabilities={ + "torch_version": "2.5.1", + }, ) _check_config() From 822c904092fdce71e373eb933a64e1edb6f77302 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 3 Dec 2024 12:01:31 +0700 Subject: [PATCH 0295/1405] fix(vlm): handle legacy conversation data format and check image in data (#2018) [skip ci] * fix: handle legacy conversation data format and check image in data * feat: add test for llama vision * feat: add max_steps to test * fix: incorrect indent and return preprocess * feat: use smaller model and dataset * chore: add extra config for sharegpt dataset --- src/axolotl/utils/collators/mm_chat.py | 131 ++++++++++++++++++++++--- tests/e2e/test_llama_vision.py | 116 ++++++++++++++++++++++ tests/e2e/test_lora_llama.py | 1 + tests/e2e/test_optimizers.py | 1 + tests/e2e/test_relora_llama.py | 1 + 5 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 tests/e2e/test_llama_vision.py diff --git a/src/axolotl/utils/collators/mm_chat.py b/src/axolotl/utils/collators/mm_chat.py index b9b67f8750..6f8a64ad85 100644 --- a/src/axolotl/utils/collators/mm_chat.py +++ b/src/axolotl/utils/collators/mm_chat.py @@ -1,8 +1,10 @@ """ Collators for multi-modal chat messages and packing """ + +from copy import deepcopy from dataclasses import dataclass -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union from PIL import Image from transformers import PreTrainedTokenizerBase, ProcessorMixin @@ -30,8 +32,8 @@ def __post_init__(self): raise ValueError("Packing is currently not supported.") def torch_call( - self, examples: List[Union[List[int], Any, Dict[str, Any]]] - ) -> Dict[str, Any]: + self, examples: list[Union[list[int], Any, dict[str, Any]]] + ) -> dict[str, Any]: # Handle dict or lists with proper padding and conversion to tensor. return self.__class__.process_rows( @@ -46,6 +48,120 @@ def process_rows(examples, processor, chat_template, max_images, length_only=Fal # *** This is COPIED from the trl example sft_vlm.py code *** # use this as a starting point + def _preprocess(examples: list[dict]) -> list[dict]: + """ + Preprocess conversation examples to ensure consistent format. + + Converts different conversation formats to OpenAI format with 'messages'. + Supports two formats: + 1. OpenAI format with 'messages' + 2. Legacy format with 'conversations' + + Args: + examples: list of conversation dictionaries + + Returns: + dict in OpenAI format with 'messages' key + + Raises: + ValueError: If the conversation format is not supported + """ + role_mapping = { + "human": "user", + "gpt": "assistant", + } + + def normalize_role(role: str) -> str: + """Normalize role names to OpenAI format. Default to original role if not found.""" + return role_mapping.get(role, role) + + def convert_legacy_format(example: dict) -> dict: + """Convert legacy 'conversations' format to OpenAI 'messages' format.""" + messages = [ + { + "role": normalize_role(convo["from"]), + "content": convo["value"], + } + for convo in example["conversations"] + ] + + # Create new dict without 'conversations' key + result = deepcopy(example) + result.pop("conversations") + return {"messages": messages, **result} + + processed_examples = [] + for example in examples: + # OpenAI format + if "messages" in example: + processed_examples.append(example) + + # Legacy format + elif "conversations" in example: + processed_examples.append(convert_legacy_format(example)) + + else: + raise ValueError( + "Only `messages` and `conversations` message keys are currently supported." + ) + + return processed_examples + + def _process_images(examples, max_images): + """ + Process images from examples, ensuring consistency in image presence and applying max_images limit. + + Args: + examples: List of dictionaries that may contain 'images' key + max_images: Maximum number of images to keep per example (0 means no limit) + + Returns: + Either None (if no images) or List[Image objects] (if all examples have images) + + Raises: + ValueError: If there's a mix of None and non-None images + """ + + def get_image(example): + if "images" not in example: + return None + images = example["images"] + if isinstance(images, str): + return Image.open(images) + return images + + images = [get_image(example) for example in examples] + + # Count None and non-None images + none_count = sum(1 for img in images if img is None) + + # All images are None + if none_count == len(images): + return None + + # Mix of None and non-None images + if none_count > 0: + raise ValueError( + "All images should be either None or not None. " + "Please provide images for all examples or None." + ) + + # Apply max_images limit if specified + if max_images > 0: + images = [ + ( + img_batch[:max_images] + if isinstance(img_batch, (list, tuple)) + else img_batch + ) + for img_batch in images + ] + + return images + + # Preprocess the examples + examples = _preprocess(examples) + # Get the texts and images, and apply the chat template texts = [ processor.apply_chat_template( @@ -53,15 +169,8 @@ def process_rows(examples, processor, chat_template, max_images, length_only=Fal ) for example in examples ] - images = [ - Image.open(example["images"]) - if isinstance(example["images"], str) - else example["images"] - for example in examples - ] - if max_images > 0: - images = [img_batch[:max_images] for img_batch in images] + images = _process_images(examples, max_images=max_images) # Tokenize the texts and process the images batch = processor(text=texts, images=images, return_tensors="pt", padding=True) diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py new file mode 100644 index 0000000000..1d583a3267 --- /dev/null +++ b/tests/e2e/test_llama_vision.py @@ -0,0 +1,116 @@ +""" +E2E tests for lora llama +""" + +import logging +import os +import unittest +from pathlib import Path + +from axolotl.cli import load_datasets +from axolotl.common.cli import TrainerCliArgs +from axolotl.train import train +from axolotl.utils.config import normalize_config +from axolotl.utils.dict import DictDefault + +from .utils import with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestLlamaVision(unittest.TestCase): + """ + Test case for Llama Vision models + """ + + @with_temp_dir + def test_lora_llama_vision_text_only_dataset(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "axolotl-ai-co/Llama-3.2-39M-Vision", + "processor_type": "AutoProcessor", + "skip_prepare_dataset": True, + "remove_unused_columns": False, + "sample_packing": False, + "sequence_len": 1024, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_modules": r"language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj", + "val_set_size": 0, + "chat_template": "llama3_2_vision", + "datasets": [ + { + "path": "LDJnr/Puffin", + "type": "chat_template", + "field_messages": "conversations", + "message_field_role": "from", + "message_field_content": "value", + }, + ], + "num_epochs": 1, + "micro_batch_size": 1, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "max_steps": 5, + "save_safetensors": True, + "bf16": True, + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.safetensors").exists() + + @with_temp_dir + def test_lora_llama_vision_multimodal_dataset(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "axolotl-ai-co/Llama-3.2-39M-Vision", + "processor_type": "AutoProcessor", + "skip_prepare_dataset": True, + "remove_unused_columns": False, + "sample_packing": False, + "sequence_len": 1024, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_modules": r"language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj", + "val_set_size": 0, + "chat_template": "llama3_2_vision", + "datasets": [ + { + "path": "axolotl-ai-co/llava-instruct-mix-vsft-small", + "type": "chat_template", + "split": "train", + "field_messages": "messages", + }, + ], + "num_epochs": 1, + "micro_batch_size": 1, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "max_steps": 5, + "save_safetensors": True, + "bf16": True, + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.safetensors").exists() diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index 4c6fdaaa91..d06be60b96 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -57,6 +57,7 @@ def test_lora(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_torch", "lr_scheduler": "cosine", + "max_steps": 20, } ) normalize_config(cfg) diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 63c46c2a2d..2317bfb97a 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -56,6 +56,7 @@ def test_optimi_adamw(self, temp_dir): "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "optimi_adamw", + "max_steps": 5, "lr_scheduler": "cosine", } ) diff --git a/tests/e2e/test_relora_llama.py b/tests/e2e/test_relora_llama.py index 5de5db11b7..70310923dc 100644 --- a/tests/e2e/test_relora_llama.py +++ b/tests/e2e/test_relora_llama.py @@ -58,6 +58,7 @@ def test_relora(self, temp_dir): "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch", + "max_steps": 5, "lr_scheduler": "cosine", } ) From ff4794cd8ed93257c74fe55c86fbaecc9df7302f Mon Sep 17 00:00:00 2001 From: Sunny Liu <22844540+bursteratom@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:02:02 -0500 Subject: [PATCH 0296/1405] Add ds model card, rebased (#2101) [skip ci] * rebased add_ds_model_card * manual rebasing * fix redundancy * lint * include case when ds_tag is none * conform to kwargs in create_model_card --- src/axolotl/core/trainer_builder.py | 35 ++++++++++++++++++++++++++++- src/axolotl/train.py | 28 +++++++++++++++++++---- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 9b03563e0c..93384189e9 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -107,6 +107,22 @@ def _sanitize_kwargs_for_tagging(tag_names, kwargs=None): return kwargs +def _sanitize_kwargs_for_ds_tagging(dataset_tags, kwargs=None): + if isinstance(dataset_tags, str): + dataset_tags = [dataset_tags] + + if (dataset_tags is not None) and (kwargs is not None): + if "dataset_tags" not in kwargs: + kwargs["dataset_tags"] = dataset_tags + elif "dataset_tags" in kwargs and isinstance(kwargs["dataset_tags"], list): + kwargs["dataset_tags"].extend(dataset_tags) + elif "dataset_tags" in kwargs and isinstance(kwargs["dataset_tags"], str): + dataset_tags.append(kwargs["dataset_tags"]) + kwargs["dataset_tags"] = dataset_tags + + return kwargs + + @dataclass class AxolotlTrainingMixins: """ @@ -418,10 +434,12 @@ def __init__( *_args, bench_data_collator=None, eval_data_collator=None, + dataset_tags=None, **kwargs, ): self.bench_data_collator = bench_data_collator self.eval_data_collator = eval_data_collator + self.dataset_tags = dataset_tags super().__init__(*_args, **kwargs) self.train_data_collator = self.data_collator self._stored_metrics = defaultdict(lambda: defaultdict(list)) @@ -919,6 +937,9 @@ def push_to_hub(self, *args, **kwargs) -> str: Overwrite the `push_to_hub` method in order to force-add the tags when pushing the model on the Hub. Please refer to `~transformers.Trainer.push_to_hub` for more details. """ + kwargs = _sanitize_kwargs_for_ds_tagging( + dataset_tags=self.dataset_tags, kwargs=kwargs + ) kwargs = _sanitize_kwargs_for_tagging(tag_names=self.tag_names, kwargs=kwargs) return super().push_to_hub(*args, **kwargs) @@ -1042,8 +1063,9 @@ class AxolotlDPOTrainer(SchedulerMixin, DPOTrainer): tag_names = ["axolotl", "dpo"] - def __init__(self, *args, **kwargs): + def __init__(self, *args, dataset_tags=None, **kwargs): super().__init__(*args, **kwargs) + self.dataset_tags = dataset_tags self.optimizer = None def create_optimizer(self): @@ -1082,6 +1104,9 @@ def push_to_hub(self, *args, **kwargs) -> str: Overwrite the `push_to_hub` method in order to force-add the tags when pushing the model on the Hub. Please refer to `~transformers.Trainer.push_to_hub` for more details. """ + kwargs = _sanitize_kwargs_for_ds_tagging( + dataset_tags=self.dataset_tags, kwargs=kwargs + ) kwargs = _sanitize_kwargs_for_tagging(tag_names=self.tag_names, kwargs=kwargs) return super().push_to_hub(*args, **kwargs) @@ -1806,6 +1831,10 @@ def build(self, total_num_steps): else: trainer_kwargs["tokenizer"] = self.tokenizer + if (trainer_cls is not AxolotlRewardTrainer) and self.cfg.datasets is not None: + trainer_kwargs["dataset_tags"] = [ + d["path"] for d in self.cfg.datasets if not Path(d["path"]).is_dir() + ] trainer = trainer_cls( model=self.model, train_dataset=self.train_dataset, @@ -2079,6 +2108,10 @@ def build(self, total_num_steps): else: dpo_trainer_kwargs["tokenizer"] = self.tokenizer + if self.cfg.datasets is not None and (trainer_cls is AxolotlDPOTrainer): + dpo_trainer_kwargs["dataset_tags"] = [ + d["path"] for d in self.cfg.datasets if not Path(d["path"]).is_dir() + ] dpo_trainer = trainer_cls( *trainer_cls_args, args=training_args, diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 5fde4d3848..39af9f45c9 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -259,11 +259,31 @@ def terminate_handler(_, __, model_weakref): model.save_pretrained(cfg.output_dir, safe_serialization=safe_serialization) if not cfg.hub_model_id: + from huggingface_hub import HfApi + from huggingface_hub.utils import RepositoryNotFoundError + try: - trainer.create_model_card( - model_name=cfg.output_dir.lstrip("./").encode("utf-8").decode("utf-8") - ) - except (AttributeError, UnicodeDecodeError): + # Check to make sure the base model is from HuggingFace not a local directory + hf_api = HfApi() + hf_api.model_info(cfg.base_model) + + model_card_kwarg = { + "model_name": cfg.output_dir.lstrip("./") + .encode("utf-8") + .decode("utf-8") + } + if cfg.datasets is not None: + if cfg.rl is not None or cfg.reward_model: + model_card_kwarg["dataset_name"] = [ + d["path"] for d in cfg.datasets if not Path(d["path"]).is_dir() + ] + else: + model_card_kwarg["dataset_tags"] = [ + d["path"] for d in cfg.datasets if not Path(d["path"]).is_dir() + ] + + trainer.create_model_card(**model_card_kwarg) + except (AttributeError, UnicodeDecodeError, RepositoryNotFoundError): pass elif cfg.hub_model_id: # defensively push to the hub to ensure the model card is updated From b9bb02406a11b1d6973e394bdea17afbfba2f8ba Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 3 Dec 2024 00:02:38 -0500 Subject: [PATCH 0297/1405] fix so inference can be run against quantized models without adapters (#1834) * fix so inference can be run against quantized models without adapters * Update error msg [skip e2e] Co-authored-by: NanoCode012 --------- Co-authored-by: NanoCode012 --- src/axolotl/cli/inference.py | 2 +- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index adc991456d..b738e5c222 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -19,7 +19,7 @@ def do_cli(config: Path = Path("examples/"), gradio=False, **kwargs): # pylint: disable=duplicate-code print_axolotl_text_art() - parsed_cfg = load_cfg(config, **kwargs) + parsed_cfg = load_cfg(config, inference=True, **kwargs) parsed_cfg.sample_packing = False parser = transformers.HfArgumentParser((TrainerCliArgs)) parsed_cli_args, _ = parser.parse_args_into_dataclasses( diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 0f01a7cadc..c9170b7a84 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -323,11 +323,13 @@ class LoraConfig(BaseModel): @model_validator(mode="before") @classmethod def validate_adapter(cls, data): - if not data.get("adapter") and ( - data.get("load_in_8bit") or data.get("load_in_4bit") + if ( + not data.get("adapter") + and not data.get("inference") + and (data.get("load_in_8bit") or data.get("load_in_4bit")) ): raise ValueError( - "load_in_8bit and load_in_4bit are not supported without setting an adapter." + "load_in_8bit and load_in_4bit are not supported without setting an adapter for training." "If you want to full finetune, please turn off load_in_8bit and load_in_4bit." ) return data From fc6188cd760cb2ef7221a9bdce83ff0330e0208d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 3 Dec 2024 07:42:41 -0500 Subject: [PATCH 0298/1405] fix merge conflict of duplicate max_steps in config for relora (#2116) --- tests/e2e/test_relora_llama.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/test_relora_llama.py b/tests/e2e/test_relora_llama.py index 70310923dc..5de5db11b7 100644 --- a/tests/e2e/test_relora_llama.py +++ b/tests/e2e/test_relora_llama.py @@ -58,7 +58,6 @@ def test_relora(self, temp_dir): "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch", - "max_steps": 5, "lr_scheduler": "cosine", } ) From bd8436bc6e44856402632891552602a0f99aed92 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 3 Dec 2024 20:22:22 +0700 Subject: [PATCH 0299/1405] feat: add cut_cross_entropy (#2091) * feat: add cut_cross_entropy * fix: add to input * fix: remove from setup.py * feat: refactor into an integration * chore: ignore lint * feat: add test for cce * fix: set max_steps for liger test * chore: Update base model following suggestion Co-authored-by: Wing Lian * chore: update special_tokens following suggestion Co-authored-by: Wing Lian * chore: remove with_temp_dir following comments * fix: plugins aren't loaded * chore: update quotes in error message * chore: lint * chore: lint * feat: enable FA on test * chore: refactor get_pytorch_version * fix: lock cce commit version * fix: remove subclassing UT * fix: downcast even if not using FA and config check * feat: add test to check different attentions * feat: add install to CI * chore: refactor to use parametrize for attention * fix: pytest not detecting test * feat: handle torch lower than 2.4 * fix args/kwargs to match docs * use release version cut-cross-entropy==24.11.4 * fix quotes * fix: use named params for clarity for modal builder * fix: handle install from pip * fix: test check only top level module install * fix: re-add import check * uninstall existing version if no transformers submodule in cce * more dataset fixtures into the cache --------- Co-authored-by: Wing Lian Co-authored-by: Wing Lian --- .github/workflows/tests-nightly.yml | 1 + .github/workflows/tests.yml | 1 + cicd/Dockerfile.jinja | 1 + cicd/tests.py | 1 + docker/Dockerfile | 1 + scripts/cutcrossentropy_install.py | 28 ++ src/axolotl/cli/__init__.py | 9 +- src/axolotl/core/trainers/trl.py | 2 +- .../cut_cross_entropy/ACKNOWLEDGEMENTS.md | 325 ++++++++++++++++++ .../integrations/cut_cross_entropy/LICENSE | 47 +++ .../integrations/cut_cross_entropy/README.md | 10 + .../cut_cross_entropy/__init__.py | 83 +++++ .../integrations/cut_cross_entropy/args.py | 42 +++ src/axolotl/utils/__init__.py | 24 ++ src/axolotl/utils/config/__init__.py | 12 + src/axolotl/utils/models.py | 17 +- tests/conftest.py | 16 + tests/e2e/integrations/liger.py | 6 +- .../integrations/test_cut_cross_entropy.py | 94 +++++ 19 files changed, 705 insertions(+), 15 deletions(-) create mode 100644 scripts/cutcrossentropy_install.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/ACKNOWLEDGEMENTS.md create mode 100644 src/axolotl/integrations/cut_cross_entropy/LICENSE create mode 100644 src/axolotl/integrations/cut_cross_entropy/README.md create mode 100644 src/axolotl/integrations/cut_cross_entropy/__init__.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/args.py create mode 100644 tests/e2e/integrations/test_cut_cross_entropy.py diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index bb99278880..004df55186 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -55,6 +55,7 @@ jobs: pip3 install --upgrade pip pip3 install --upgrade packaging pip3 install -U -e . + python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt - name: Run tests diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 249dfd9e4c..dd4c95bbe4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,6 +72,7 @@ jobs: pip3 show torch pip3 install -U -e . python scripts/unsloth_install.py | sh + python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt - name: Run tests diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 65553d60b5..da28b391fc 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -38,6 +38,7 @@ RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ fi RUN python scripts/unsloth_install.py | sh +RUN python scripts/cutcrossentropy_install.py | sh # So we can test the Docker image RUN pip install -r requirements-dev.txt -r requirements-tests.txt diff --git a/cicd/tests.py b/cicd/tests.py index 812ef7b426..f3dbaef105 100644 --- a/cicd/tests.py +++ b/cicd/tests.py @@ -40,6 +40,7 @@ cicd_image = ( Image.from_dockerfile( pathlib.Path(temp_dir) / "Dockerfile", + context_mount=None, force_build=True, gpu="A10G", ) diff --git a/docker/Dockerfile b/docker/Dockerfile index 173a508792..88b871ea94 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -27,6 +27,7 @@ RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ fi RUN python scripts/unsloth_install.py | sh +RUN python scripts/cutcrossentropy_install.py | sh # So we can test the Docker image RUN pip install pytest diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py new file mode 100644 index 0000000000..3816e58143 --- /dev/null +++ b/scripts/cutcrossentropy_install.py @@ -0,0 +1,28 @@ +"""Script to output the correct installation command for cut-cross-entropy.""" +import importlib.util +import sys + +try: + import torch +except ImportError as exc: + raise ImportError("Install torch via `pip install torch`") from exc +from packaging.version import Version as V + +v = V(torch.__version__) + +# no cut-cross-entropy support for torch < 2.4.0 +if v < V("2.4.0"): + print("") + sys.exit(0) + +cce_spec = importlib.util.find_spec("cut_cross_entropy") +cce_spec_transformers = importlib.util.find_spec("cut_cross_entropy.transformers") + +UNINSTALL_PREFIX = "" +if cce_spec and not cce_spec_transformers: + UNINSTALL_PREFIX = "pip uninstall -y cut-cross-entropy && " + +print( + UNINSTALL_PREFIX + + 'pip install "cut-cross-entropy @ git+https://github.com/apple/ml-cross-entropy.git@9c297c905f55b73594b5d650722d1e78183b77bd"' +) diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 2ef78e07d8..4572cfa5c8 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -27,7 +27,6 @@ from transformers.utils.import_utils import _is_package_available from axolotl.common.cli import TrainerCliArgs, load_model_and_tokenizer -from axolotl.integrations.base import PluginManager from axolotl.logging_config import configure_logging from axolotl.train import TrainDatasetMeta from axolotl.utils.chat_templates import ( @@ -38,6 +37,7 @@ from axolotl.utils.config import ( normalize_cfg_datasets, normalize_config, + prepare_plugins, validate_config, ) from axolotl.utils.data import load_prepare_dpo_datasets, prepare_dataset @@ -426,11 +426,6 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs): cfg.axolotl_config_path = config - if cfg.get("plugins"): - plugin_manager = PluginManager.get_instance() - for plugin_name in cfg["plugins"]: - plugin_manager.register(plugin_name) - try: device_props = torch.cuda.get_device_properties("cuda") gpu_version = "sm_" + str(device_props.major) + str(device_props.minor) @@ -449,6 +444,8 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs): }, ) + prepare_plugins(cfg) + prepare_optim_env(cfg) prepare_opinionated_env(cfg) diff --git a/src/axolotl/core/trainers/trl.py b/src/axolotl/core/trainers/trl.py index 24c0b04123..57f014bd6a 100644 --- a/src/axolotl/core/trainers/trl.py +++ b/src/axolotl/core/trainers/trl.py @@ -40,7 +40,7 @@ def train( query_tensors, return_prompt=False, generate_ref_response=True, - **generation_kwargs + **generation_kwargs, ) batch["response"] = self.tokenizer.batch_decode(response_tensors) batch["ref_response"] = self.tokenizer.batch_decode(ref_response_tensors) diff --git a/src/axolotl/integrations/cut_cross_entropy/ACKNOWLEDGEMENTS.md b/src/axolotl/integrations/cut_cross_entropy/ACKNOWLEDGEMENTS.md new file mode 100644 index 0000000000..03d1cbfb00 --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/ACKNOWLEDGEMENTS.md @@ -0,0 +1,325 @@ +Acknowledgements + +Portions of this Cut Cross Entropy Software may utilize the following copyrighted +material, the use of which is hereby acknowledged. + + +------ + + +PyTorch + + From PyTorch: + + Copyright (c) 2016- Facebook, Inc (Adam Paszke) + Copyright (c) 2014- Facebook, Inc (Soumith Chintala) + Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert) + Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu) + Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu) + Copyright (c) 2011-2013 NYU (Clement Farabet) + Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston) + Copyright (c) 2006 Idiap Research Institute (Samy Bengio) + Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz) + + From Caffe2: + + Copyright (c) 2016-present, Facebook Inc. All rights reserved. + + All contributions by Facebook: + Copyright (c) 2016 Facebook Inc. + + All contributions by Google: + Copyright (c) 2015 Google Inc. + All rights reserved. + + All contributions by Yangqing Jia: + Copyright (c) 2015 Yangqing Jia + All rights reserved. + + All contributions by Kakao Brain: + Copyright 2019-2020 Kakao Brain + + All contributions by Cruise LLC: + Copyright (c) 2022 Cruise LLC. + All rights reserved. + + All contributions by Arm: + Copyright (c) 2021, 2023-2024 Arm Limited and/or its affiliates + + All contributions from Caffe: + Copyright(c) 2013, 2014, 2015, the respective contributors + All rights reserved. + + All other contributions: + Copyright(c) 2015, 2016 the respective contributors + All rights reserved. + + Caffe2 uses a copyright model similar to Caffe: each contributor holds + copyright over their contributions to Caffe2. The project versioning records + all such contribution and copyright details. If a contributor wants to further + mark their specific copyright on a particular contribution, they should + indicate their copyright solely in the commit message of the change when it is + committed. + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the names of Facebook, Deepmind Technologies, NYU, NEC Laboratories America + and IDIAP Research Institute nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + +Triton + + /* + * Copyright 2018-2020 Philippe Tillet + * Copyright 2020-2022 OpenAI + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + +Transformers + + Copyright 2018- The Hugging Face team. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/axolotl/integrations/cut_cross_entropy/LICENSE b/src/axolotl/integrations/cut_cross_entropy/LICENSE new file mode 100644 index 0000000000..7ab90b7d9c --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/LICENSE @@ -0,0 +1,47 @@ +Copyright (C) 2024 Apple Inc. All Rights Reserved. + +IMPORTANT: This Apple software is supplied to you by Apple +Inc. ("Apple") in consideration of your agreement to the following +terms, and your use, installation, modification or redistribution of +this Apple software constitutes acceptance of these terms. If you do +not agree with these terms, please do not use, install, modify or +redistribute this Apple software. + +In consideration of your agreement to abide by the following terms, and +subject to these terms, Apple grants you a personal, non-exclusive +license, under Apple's copyrights in this original Apple software (the +"Apple Software"), to use, reproduce, modify and redistribute the Apple +Software, with or without modifications, in source and/or binary forms; +provided that if you redistribute the Apple Software in its entirety and +without modifications, you must retain this notice and the following +text and disclaimers in all such redistributions of the Apple Software. +Neither the name, trademarks, service marks or logos of Apple Inc. may +be used to endorse or promote products derived from the Apple Software +without specific prior written permission from Apple. Except as +expressly stated in this notice, no other rights or licenses, express or +implied, are granted by Apple herein, including but not limited to any +patent rights that may be infringed by your derivative works or by other +works in which the Apple Software may be incorporated. + +The Apple Software is provided by Apple on an "AS IS" basis. APPLE +MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION +THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND +OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + +IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, +MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED +AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), +STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------------------- +SOFTWARE DISTRIBUTED WITH CUT CROSS ENTROPY: + +The Cut Cross Entropy software includes a number of subcomponents with separate +copyright notices and license terms - please see the file ACKNOWLEDGEMENTS.md. +------------------------------------------------------------------------------- diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md new file mode 100644 index 0000000000..c67d7440b9 --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -0,0 +1,10 @@ +# Cut Cross Entropy + +### Usage + +```yaml +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +cut_cross_entropy: true +``` diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py new file mode 100644 index 0000000000..97517bccdb --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -0,0 +1,83 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module for the Plugin for Cut Cross Entropy integration with Axolotl. + +Cut Cross Entropy is an optimized implementation of cross entropy loss +from Apple's ML team. +""" +import importlib +import logging + +import torch + +from axolotl.integrations.base import BasePlugin +from axolotl.utils import get_pytorch_version + +from ...utils.distributed import zero_only +from .args import CutCrossEntropyArgs # pylint: disable=unused-import. # noqa: F401 + +LOG = logging.getLogger("axolotl.integrations.cut_cross_entropy") + +_CCE_INSTALL_MESSAGE = ( + "Please install cut_cross_entropy with transformers support using " + '`pip install "cut-cross-entropy[transformers]==24.11.4"`' +) + + +class CutCrossEntropyPlugin(BasePlugin): + """ + Plugin for Cut Cross Entropy integration with Axolotl. + """ + + def get_input_args(self): + return "axolotl.integrations.cut_cross_entropy.CutCrossEntropyArgs" + + def _check_requirements(self): + """Check if all requirements are met.""" + # Check PyTorch version + + major, minor, _ = get_pytorch_version() + if (major, minor) < (2, 4): + raise ImportError( + "Cut Cross Entropy requires PyTorch >= 2.4.0. " + f"Current version: {torch.__version__}" + ) + + # Check if cut_cross_entropy is installed + cce_spec = importlib.util.find_spec("cut_cross_entropy") + if cce_spec is None: + raise ImportError(_CCE_INSTALL_MESSAGE) + + cce_spec_transformers = importlib.util.find_spec( + "cut_cross_entropy.transformers" + ) + if cce_spec_transformers is None: + raise ImportError(_CCE_INSTALL_MESSAGE) + + def pre_model_load(self, cfg): + """Apply cut cross entropy before model loading if enabled.""" + if cfg.cut_cross_entropy: + self._check_requirements() + + from cut_cross_entropy.transformers import cce_patch + + with zero_only(): + LOG.info( + f"Applying Cut Cross Entropy to model type: {cfg.model_config_type}" + ) + + # The patch checks model_type internally + cce_patch(cfg.model_config_type) diff --git a/src/axolotl/integrations/cut_cross_entropy/args.py b/src/axolotl/integrations/cut_cross_entropy/args.py new file mode 100644 index 0000000000..9a364e2d3e --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/args.py @@ -0,0 +1,42 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module for handling Cut Cross Entropy input arguments. +""" +import logging +from typing import Optional + +from pydantic import BaseModel, model_validator + +LOG = logging.getLogger("axolotl.integrations.cut_cross_entropy.args") + + +class CutCrossEntropyArgs(BaseModel): + """ + Input args for Cut Cross Entropy. + """ + + cut_cross_entropy: Optional[bool] = None + + @model_validator(mode="before") + @classmethod + def check_dtype_is_half(cls, data): + if not (data.get("bf16") or data.get("fp16")): + raise ValueError( + "Cut Cross Entropy requires fp16/bf16 training for backward pass. " + "Please set `bf16` or `fp16` to `True`." + ) + + return data diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index 91545009ad..4602054471 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -1,7 +1,11 @@ """ Basic utils for Axolotl """ + import importlib.util +import re + +import torch def is_mlflow_available(): @@ -10,3 +14,23 @@ def is_mlflow_available(): def is_comet_available(): return importlib.util.find_spec("comet_ml") is not None + + +# pylint: disable=duplicate-code +def get_pytorch_version() -> tuple[int, int, int]: + """ + Get Pytorch version as a tuple of (major, minor, patch). + """ + torch_version = torch.__version__ + version_match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", torch_version) + + if not version_match: + raise ValueError("Invalid version format") + + major, minor, patch = version_match.groups() + major, minor = int(major), int(minor) + patch = int(patch) if patch is not None else 0 # Default patch to 0 if not present + return major, minor, patch + + +# pylint: enable=duplicate-code diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 422ed78efb..468bd6e7f8 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -7,6 +7,7 @@ from transformers.utils import is_torch_bf16_gpu_available from transformers.utils.import_utils import is_torch_npu_available +from axolotl.integrations.base import PluginManager from axolotl.integrations.config import merge_input_args from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.config.models.input.v0_4_1 import ( @@ -264,3 +265,14 @@ def validate_config( return DictDefault( dict(AxolotlInputConfig(**cfg.to_dict()).model_dump(exclude_none=True)) ) + + +def prepare_plugins(cfg): + """ + Prepare the plugins for the configuration + """ + + if cfg.get("plugins"): + plugin_manager = PluginManager.get_instance() + for plugin_name in cfg["plugins"]: + plugin_manager.register(plugin_name) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index fc1f0cf1c2..3e120cca60 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -1084,14 +1084,17 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: self.prepare_model(qlora_fsdp) - # LlamaRMSNorm layers are in fp32 after kbit_training or full finetune, so we need to - # convert them back to fp16/bf16 for flash-attn compatibility. - if (needs_fa2_dtype or self.cfg.flash_attention) and not qlora_fsdp: - LOG.info( - "converting modules to %s for flash attention", self.cfg.torch_dtype - ) + should_convert = ( + # LlamaRMSNorm layers are in fp32 after kbit_training or full finetune, so we need to + # convert them back to fp16/bf16 for flash-attn compatibility. + ((needs_fa2_dtype or self.cfg.flash_attention) and not qlora_fsdp) + or self.cfg.cut_cross_entropy # Cut cross entropy requires embedding layers to be in fp16/bf16 for backward pass + ) + + if should_convert: + LOG.info("Converting modules to %s", self.cfg.torch_dtype) self.convert_embedding_modules_dtype( - embedding_modules, + embedding_modules=embedding_modules, dist_dtype=self.cfg.torch_dtype, before_kbit_train_or_finetune=False, ) diff --git a/tests/conftest.py b/tests/conftest.py index 4479e676f4..2fc985d3ad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,6 +51,22 @@ def download_mlabonne_finetome_100k_dataset(): snapshot_download("mlabonne/FineTome-100k", repo_type="dataset") +@pytest.fixture +def download_argilla_distilabel_capybara_dpo_7k_binarized_dataset(): + # download the dataset + snapshot_download( + "argilla/distilabel-capybara-dpo-7k-binarized", repo_type="dataset" + ) + + +@pytest.fixture +def download_arcee_ai_distilabel_intel_orca_dpo_pairs_dataset(): + # download the dataset + snapshot_download( + "arcee-ai/distilabel-intel-orca-dpo-pairs-binarized", repo_type="dataset" + ) + + @pytest.fixture def temp_dir(): # Create a temporary directory diff --git a/tests/e2e/integrations/liger.py b/tests/e2e/integrations/liger.py index bb4574dff3..455c3d2818 100644 --- a/tests/e2e/integrations/liger.py +++ b/tests/e2e/integrations/liger.py @@ -7,7 +7,7 @@ from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, prepare_plugins from axolotl.utils.dict import DictDefault from ..utils import with_temp_dir @@ -54,8 +54,10 @@ def test_llama_wo_flce(self, temp_dir): "lr_scheduler": "cosine", "save_safetensors": True, "bf16": "auto", + "max_steps": 10, } ) + prepare_plugins(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -99,8 +101,10 @@ def test_llama_w_flce(self, temp_dir): "lr_scheduler": "cosine", "save_safetensors": True, "bf16": "auto", + "max_steps": 10, } ) + prepare_plugins(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py new file mode 100644 index 0000000000..82801eedce --- /dev/null +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -0,0 +1,94 @@ +""" +Simple end-to-end test for Cut Cross Entropy integration +""" + +from pathlib import Path + +import pytest + +from axolotl.cli import load_datasets +from axolotl.common.cli import TrainerCliArgs +from axolotl.train import train +from axolotl.utils import get_pytorch_version +from axolotl.utils.config import normalize_config, prepare_plugins +from axolotl.utils.dict import DictDefault + +# pylint: disable=duplicate-code + + +@pytest.fixture() +def min_cfg(temp_dir): + return { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "plugins": [ + "axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin", + ], + "cut_cross_entropy": True, + "sequence_len": 1024, + "val_set_size": 0.1, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "output_dir": temp_dir, + "lr_scheduler": "cosine", + "save_safetensors": True, + "max_steps": 10, + "bf16": "auto", + } + + +class TestCutCrossEntropyIntegration: + """ + e2e tests for cut_cross_entropy integration with Axolotl + """ + + # pylint: disable=redefined-outer-name + def test_llama_w_cce(self, min_cfg, temp_dir): + cfg = DictDefault(min_cfg) + prepare_plugins(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + major, minor, _ = get_pytorch_version() + if (major, minor) < (2, 4): + with pytest.raises(ImportError): + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + else: + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() + + @pytest.mark.parametrize( + "attention_type", + ["flash_attention", "sdp_attention", "xformers_attention"], + ) + def test_llama_w_cce_and_attention(self, min_cfg, temp_dir, attention_type): + cfg = DictDefault( + min_cfg + | { + attention_type: True, + } + ) + prepare_plugins(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + major, minor, _ = get_pytorch_version() + if (major, minor) < (2, 4): + with pytest.raises(ImportError): + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + else: + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() From 81ef3e45f779fffdda6fec06a469e4ac56afd381 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 3 Dec 2024 20:58:03 +0700 Subject: [PATCH 0300/1405] fix(readme): update cuda instructions during preprocess (#2114) [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b900a1c5a8..cd03abf003 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ pip3 install -e '.[flash-attn,deepspeed]' ### Usage ```bash # preprocess datasets - optional but recommended -CUDA_VISIBLE_DEVICES="" python -m axolotl.cli.preprocess examples/openllama-3b/lora.yml +CUDA_VISIBLE_DEVICES="0" python -m axolotl.cli.preprocess examples/openllama-3b/lora.yml # finetune lora accelerate launch -m axolotl.cli.train examples/openllama-3b/lora.yml From 1ef70312bad2989a3e36e3a9d1f5bcff6243d32a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 3 Dec 2024 08:58:23 -0500 Subject: [PATCH 0301/1405] fix optimizer reset for relora sft (#1414) * fix optimizer reset * set states to reset for 8bit optimizers and handle quantile runtime error for embeddings * fix relora test to check grad_norm * use flash attn for relora and tweak hyperparams for test * fix messages field for test dataset --- requirements-dev.txt | 1 - requirements-tests.txt | 1 + src/axolotl/monkeypatch/relora.py | 36 +++++++++++++------- tests/e2e/test_relora_llama.py | 56 ++++++++++++++++++++++--------- 4 files changed, 64 insertions(+), 30 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index dcc729d1b2..4b5df167b6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,3 @@ pre-commit black mypy types-requests -tbparse diff --git a/requirements-tests.txt b/requirements-tests.txt index 0022980e90..a13f739231 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,3 +2,4 @@ pytest pytest-xdist pytest-retry pytest-sugar +tbparse diff --git a/src/axolotl/monkeypatch/relora.py b/src/axolotl/monkeypatch/relora.py index 9d246cb17f..3fda84b929 100644 --- a/src/axolotl/monkeypatch/relora.py +++ b/src/axolotl/monkeypatch/relora.py @@ -46,9 +46,10 @@ def reset_optimizer( *, reset_params: List[str], # where str is the key to a torch.nn.Parameter optimizer_state_keys: List[str], - prune_ratio: float = 0.9, + optimizer_magnitude_pruning: float = 0.9, ): - pruning_fn = partial(magnitude_pruning_, prune_ratio=prune_ratio) + # pylint:disable=unused-argument + pruning_fn = partial(magnitude_pruning_, prune_ratio=optimizer_magnitude_pruning) n_zeros = 0 n_total = 0 @@ -56,16 +57,22 @@ def reset_optimizer( if isinstance(optimizer, ZeroRedundancyOptimizer): optimizer_state = optimizer.optim.state - for param in reset_params: - param_state = optimizer_state[param] - if len(param_state) == 0: # no state for this param, happens for ZeRo optimizer - continue - for key in optimizer_state_keys: - pruning_fn( - param_state[key] - ) # pruning fn has to be inplace to keep the same keys in the dict - n_total += param_state[key].numel() - n_zeros += torch.sum(param_state[key] == 0).item() + for group in optimizer.param_groups: + for param in group["params"]: + state = optimizer_state[param] + for key, value in state.items(): + if key not in optimizer_state_keys: + continue + if torch.is_tensor(value): + try: + pruning_fn(value) + n_total += value.numel() + n_zeros += torch.sum(value == 0).item() + except RuntimeError as exc: + if "quantile() input tensor is too large" in str(exc): + pass + else: + raise exc _zeroed = n_zeros / (1e-7 + n_total) * 100 LOG.info(f"Percent of optimizer states zeroed: {_zeroed:.2f}") @@ -129,6 +136,9 @@ def on_step_begin( if "adam" in args.optim.lower(): optimizer_state_keys = ["exp_avg", "exp_avg_sq"] + if "8bit" in args.optim.lower(): + optimizer_state_keys.append("state1") + optimizer_state_keys.append("state2") else: raise ValueError(f"Optimizer {args.optim} not supported with ReLoRA") @@ -160,7 +170,7 @@ def on_step_begin( optimizer, reset_params=lora_params, optimizer_state_keys=optimizer_state_keys, - prune_ratio=args.relora_prune_ratio, + optimizer_magnitude_pruning=args.relora_prune_ratio, ) if self.quantized: diff --git a/tests/e2e/test_relora_llama.py b/tests/e2e/test_relora_llama.py index 5de5db11b7..56c2204677 100644 --- a/tests/e2e/test_relora_llama.py +++ b/tests/e2e/test_relora_llama.py @@ -7,13 +7,15 @@ import unittest from pathlib import Path +from tbparse import SummaryReader + from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir +from .utils import most_recent_subdir, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -29,36 +31,48 @@ def test_relora(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", - "sequence_len": 1024, + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 2048, + "sample_packing": True, + "pad_to_sequence_len": True, + "flash_attention": True, "load_in_8bit": True, "adapter": "lora", - "lora_r": 32, + "lora_r": 8, "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_modules": ["q_proj", "v_proj"], - "relora_steps": 25, - "relora_warmup_steps": 5, - "relora_anneal_steps": 5, + "relora_steps": 100, + "relora_warmup_steps": 20, + "relora_anneal_steps": 10, + "relora_prune_ratio": 0.9, "relora_cpu_offload": True, "val_set_size": 0.0, - "special_tokens": {}, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "chat_template": "chatml", "datasets": [ { - "path": "mhenrichsen/alpaca_2k_test", - "type": "alpaca", + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "split": "train[:10%]", + "field_messages": "conversations", + "message_field_role": "from", + "message_field_content": "value", }, ], - "warmup_steps": 15, + "warmup_steps": 20, "num_epochs": 2, - "max_steps": 51, # at least 2x relora_steps - "micro_batch_size": 4, + "max_steps": 205, # at least 2x relora_steps + "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_8bit", "lr_scheduler": "cosine", + "save_safetensors": True, + "use_tensorboard": True, } ) normalize_config(cfg) @@ -66,4 +80,14 @@ def test_relora(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "model.safetensors").exists() + assert ( + Path(temp_dir) / "checkpoint-100/adapter/adapter_model.safetensors" + ).exists() + assert (Path(temp_dir) / "checkpoint-100/relora/model.safetensors").exists() + + tb_log_path = most_recent_subdir(temp_dir + "/runs") + event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) + reader = SummaryReader(event_file) + df = reader.scalars # pylint: disable=invalid-name + df = df[(df.tag == "train/grad_norm")] # pylint: disable=invalid-name + assert df.value.values[-1] < 0.2, "grad_norm is too high" From d87df2c776c03d2486402656327648ad7132ecf2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 3 Dec 2024 15:06:09 -0500 Subject: [PATCH 0302/1405] prepare plugins needs to happen so registration can occur to build the plugin args (#2119) * prepare plugins needs to happen so registration can occur to build the plugin args use yaml.dump include dataset and more assertions * attempt to manually register plugins rather than use fn * fix fixture * remove fixture * move cli test to patched dir * fix cce validation --- scripts/cutcrossentropy_install.py | 6 +-- src/axolotl/cli/__init__.py | 4 +- .../integrations/cut_cross_entropy/args.py | 2 +- tests/e2e/patched/test_cli_integrations.py | 47 +++++++++++++++++++ 4 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/patched/test_cli_integrations.py diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 3816e58143..d51e2dd99c 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -16,11 +16,11 @@ sys.exit(0) cce_spec = importlib.util.find_spec("cut_cross_entropy") -cce_spec_transformers = importlib.util.find_spec("cut_cross_entropy.transformers") UNINSTALL_PREFIX = "" -if cce_spec and not cce_spec_transformers: - UNINSTALL_PREFIX = "pip uninstall -y cut-cross-entropy && " +if cce_spec: + if not importlib.util.find_spec("cut_cross_entropy.transformers"): + UNINSTALL_PREFIX = "pip uninstall -y cut-cross-entropy && " print( UNINSTALL_PREFIX diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 4572cfa5c8..1e61b220b9 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -432,6 +432,8 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs): except: # pylint: disable=bare-except # noqa: E722 gpu_version = None + prepare_plugins(cfg) + cfg = validate_config( cfg, capabilities={ @@ -444,8 +446,6 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs): }, ) - prepare_plugins(cfg) - prepare_optim_env(cfg) prepare_opinionated_env(cfg) diff --git a/src/axolotl/integrations/cut_cross_entropy/args.py b/src/axolotl/integrations/cut_cross_entropy/args.py index 9a364e2d3e..c16d91ede2 100644 --- a/src/axolotl/integrations/cut_cross_entropy/args.py +++ b/src/axolotl/integrations/cut_cross_entropy/args.py @@ -33,7 +33,7 @@ class CutCrossEntropyArgs(BaseModel): @model_validator(mode="before") @classmethod def check_dtype_is_half(cls, data): - if not (data.get("bf16") or data.get("fp16")): + if data.get("cut_cross_entropy") and not (data.get("bf16") or data.get("fp16")): raise ValueError( "Cut Cross Entropy requires fp16/bf16 training for backward pass. " "Please set `bf16` or `fp16` to `True`." diff --git a/tests/e2e/patched/test_cli_integrations.py b/tests/e2e/patched/test_cli_integrations.py new file mode 100644 index 0000000000..6ca7c52aea --- /dev/null +++ b/tests/e2e/patched/test_cli_integrations.py @@ -0,0 +1,47 @@ +""" +test cases to make sure the plugin args are loaded from the config file +""" +from pathlib import Path + +import yaml + +from axolotl.cli import load_cfg +from axolotl.utils.dict import DictDefault + + +# pylint: disable=duplicate-code +class TestPluginArgs: + """ + test class for plugin args loaded from the config file + """ + + def test_liger_plugin_args(self, temp_dir): + test_cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "learning_rate": 0.000001, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "plugins": ["axolotl.integrations.liger.LigerPlugin"], + "liger_layer_norm": True, + "liger_rope": True, + "liger_rms_norm": False, + "liger_glu_activation": True, + "liger_fused_linear_cross_entropy": True, + } + ) + + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(test_cfg.to_dict())) + cfg = load_cfg(str(Path(temp_dir) / "config.yaml")) + assert cfg.liger_layer_norm is True + assert cfg.liger_rope is True + assert cfg.liger_rms_norm is False + assert cfg.liger_glu_activation is True + assert cfg.liger_fused_linear_cross_entropy is True From 418ad2b586fa4e67a4b81950f789106b939f7474 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 3 Dec 2024 18:08:46 -0500 Subject: [PATCH 0303/1405] add missing fixture decorator for predownload dataset (#2117) [skip ci] * add missing fixture decorator for predownload dataset * also pre download the tokenizer files --- tests/conftest.py | 1 + tests/prompt_strategies/conftest.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 2fc985d3ad..3a20bbfdd9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,6 +46,7 @@ def download_mhenrichsen_alpaca_2k_w_revision_dataset(): ) +@pytest.fixture(scope="session", autouse=True) def download_mlabonne_finetome_100k_dataset(): # download the dataset snapshot_download("mlabonne/FineTome-100k", repo_type="dataset") diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index 43423f7255..00a2bf3004 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -4,6 +4,7 @@ import pytest from datasets import Dataset +from huggingface_hub import hf_hub_download from transformers import AutoTokenizer @@ -60,6 +61,17 @@ def fixture_basic_dataset(): @pytest.fixture(name="llama3_tokenizer") def fixture_llama3_tokenizer(): + hf_hub_download( + repo_id="NousResearch/Meta-Llama-3-8B-Instruct", + filename="special_tokens_map.json", + ) + hf_hub_download( + repo_id="NousResearch/Meta-Llama-3-8B-Instruct", + filename="tokenizer_config.json", + ) + hf_hub_download( + repo_id="NousResearch/Meta-Llama-3-8B-Instruct", filename="tokenizer.json" + ) tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B-Instruct") return tokenizer From a1790f265218227cedec31d0a16e21153d548e2f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 3 Dec 2024 21:06:20 -0500 Subject: [PATCH 0304/1405] replace tensorboard checks with helper function (#2120) [skip ci] * replace tensorboard checks with helper function * move helper function * use relative --- tests/e2e/multigpu/test_eval.py | 19 ++++----------- tests/e2e/patched/test_fa_xentropy.py | 12 ++++------ tests/e2e/patched/test_unsloth_qlora.py | 31 +++++++++---------------- tests/e2e/test_embeddings_lr.py | 22 ++++++------------ tests/e2e/test_packing_loss.py | 12 ++++------ tests/e2e/test_relora_llama.py | 13 ++++------- tests/e2e/utils.py | 15 ++++++++++++ 7 files changed, 50 insertions(+), 74 deletions(-) diff --git a/tests/e2e/multigpu/test_eval.py b/tests/e2e/multigpu/test_eval.py index c40a9edcce..09561bf265 100644 --- a/tests/e2e/multigpu/test_eval.py +++ b/tests/e2e/multigpu/test_eval.py @@ -7,12 +7,11 @@ import yaml from accelerate.test_utils import execute_subprocess_async -from tbparse import SummaryReader from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault -from ..utils import most_recent_subdir +from ..utils import check_tensorboard LOG = logging.getLogger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" @@ -91,12 +90,8 @@ def test_eval_sample_packing(self, temp_dir): str(Path(temp_dir) / "config.yaml"), ] ) - tb_log_path = most_recent_subdir(temp_dir + "/runs") - event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) - reader = SummaryReader(event_file) - df = reader.scalars # pylint: disable=invalid-name - df = df[(df.tag == "eval/loss")] # pylint: disable=invalid-name - assert df.value.values[-1] < 2.5, "Loss is too high" + + check_tensorboard(temp_dir + "/runs", "eval/loss", 2.5, "Eval Loss is too high") def test_eval(self, temp_dir): # pylint: disable=duplicate-code @@ -164,9 +159,5 @@ def test_eval(self, temp_dir): str(Path(temp_dir) / "config.yaml"), ] ) - tb_log_path = most_recent_subdir(temp_dir + "/runs") - event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) - reader = SummaryReader(event_file) - df = reader.scalars # pylint: disable=invalid-name - df = df[(df.tag == "eval/loss")] # pylint: disable=invalid-name - assert df.value.values[-1] < 2.9, "Loss is too high" + + check_tensorboard(temp_dir + "/runs", "eval/loss", 2.9, "Eval Loss is too high") diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index 76ea1a9348..effcb39c7d 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -8,7 +8,6 @@ from pathlib import Path import pytest -from tbparse import SummaryReader from transformers.utils import is_torch_bf16_gpu_available from axolotl.cli import load_datasets @@ -17,7 +16,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import most_recent_subdir +from ..utils import check_tensorboard LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -94,9 +93,6 @@ def test_lora_packing_fa_cross_entropy(self, temp_dir, gradient_accumulation_ste train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.bin").exists() - tb_log_path = most_recent_subdir(temp_dir + "/runs") - event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) - reader = SummaryReader(event_file) - df = reader.scalars # pylint: disable=invalid-name - df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name - assert df.value.values[-1] < 1.5, "Loss is too high" + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 1.5, "Train Loss is too high" + ) diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index 3d7e794f1c..8e0d03380f 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -6,8 +6,6 @@ from pathlib import Path import pytest -from e2e.utils import most_recent_subdir -from tbparse import SummaryReader from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -15,6 +13,8 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault +from ..utils import check_tensorboard + LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -73,12 +73,9 @@ def test_unsloth_llama_qlora_fa2(self, temp_dir, sample_packing): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.bin").exists() - tb_log_path = most_recent_subdir(temp_dir + "/runs") - event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) - reader = SummaryReader(event_file) - df = reader.scalars # pylint: disable=invalid-name - df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name - assert df.value.values[-1] < 2.0, "Loss is too high" + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" + ) def test_unsloth_llama_qlora_unpacked(self, temp_dir): cfg = DictDefault( @@ -123,12 +120,9 @@ def test_unsloth_llama_qlora_unpacked(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.bin").exists() - tb_log_path = most_recent_subdir(temp_dir + "/runs") - event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) - reader = SummaryReader(event_file) - df = reader.scalars # pylint: disable=invalid-name - df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name - assert df.value.values[-1] < 2.0, "Loss is too high" + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" + ) @pytest.mark.parametrize( "sdp_attention", @@ -178,9 +172,6 @@ def test_unsloth_llama_qlora_unpacked_no_fa2_fp16(self, temp_dir, sdp_attention) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.bin").exists() - tb_log_path = most_recent_subdir(temp_dir + "/runs") - event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) - reader = SummaryReader(event_file) - df = reader.scalars # pylint: disable=invalid-name - df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name - assert df.value.values[-1] < 2.0, "Loss is too high" + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" + ) diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py index bc406caf30..6e5ebd05f7 100644 --- a/tests/e2e/test_embeddings_lr.py +++ b/tests/e2e/test_embeddings_lr.py @@ -7,15 +7,13 @@ import unittest from pathlib import Path -from tbparse import SummaryReader - from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import most_recent_subdir, with_temp_dir +from .utils import check_tensorboard, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -66,12 +64,9 @@ def test_train_w_embedding_lr_scale(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "model.safetensors").exists() - tb_log_path = most_recent_subdir(temp_dir + "/runs") - event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) - reader = SummaryReader(event_file) - df = reader.scalars # pylint: disable=invalid-name - df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name - assert df.value.values[-1] < 2.0, "Loss is too high" + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.0, "Loss is too high" + ) @with_temp_dir def test_train_w_embedding_lr(self, temp_dir): @@ -113,9 +108,6 @@ def test_train_w_embedding_lr(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "model.safetensors").exists() - tb_log_path = most_recent_subdir(temp_dir + "/runs") - event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) - reader = SummaryReader(event_file) - df = reader.scalars # pylint: disable=invalid-name - df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name - assert df.value.values[-1] < 2.0, "Loss is too high" + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.0, "Loss is too high" + ) diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index 60f1673814..dd0af32f3c 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -6,7 +6,6 @@ import os import unittest -from tbparse import SummaryReader from transformers.utils import is_torch_bf16_gpu_available from axolotl.cli import load_datasets @@ -15,7 +14,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import most_recent_subdir, with_temp_dir +from .utils import check_tensorboard, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -66,9 +65,6 @@ def test_loss_packed(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - tb_log_path = most_recent_subdir(temp_dir + "/runs") - event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) - reader = SummaryReader(event_file) - df = reader.scalars # pylint: disable=invalid-name - df = df[(df.tag == "train/train_loss")] # pylint: disable=invalid-name - assert df.value.values[-1] < 2.0, "Loss is too high" + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" + ) diff --git a/tests/e2e/test_relora_llama.py b/tests/e2e/test_relora_llama.py index 56c2204677..84582896dc 100644 --- a/tests/e2e/test_relora_llama.py +++ b/tests/e2e/test_relora_llama.py @@ -7,15 +7,13 @@ import unittest from pathlib import Path -from tbparse import SummaryReader - from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import most_recent_subdir, with_temp_dir +from .utils import check_tensorboard, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -85,9 +83,6 @@ def test_relora(self, temp_dir): ).exists() assert (Path(temp_dir) / "checkpoint-100/relora/model.safetensors").exists() - tb_log_path = most_recent_subdir(temp_dir + "/runs") - event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) - reader = SummaryReader(event_file) - df = reader.scalars # pylint: disable=invalid-name - df = df[(df.tag == "train/grad_norm")] # pylint: disable=invalid-name - assert df.value.values[-1] < 0.2, "grad_norm is too high" + check_tensorboard( + temp_dir + "/runs", "train/grad_norm", 0.2, "grad_norm is too high" + ) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 92e647e678..de5b599a13 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -12,6 +12,7 @@ # from importlib.metadata import version from packaging import version +from tbparse import SummaryReader def with_temp_dir(test_func): @@ -66,3 +67,17 @@ def is_min_2_5_1(): def is_hopper(): compute_capability = torch.cuda.get_device_capability() return compute_capability == (9, 0) + + +def check_tensorboard( + temp_run_dir: str, tag: str, lt_val: float, assertion_err: str +) -> None: + """ + helper function to parse and check tensorboard logs + """ + tb_log_path = most_recent_subdir(temp_run_dir) + event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) + reader = SummaryReader(event_file) + df = reader.scalars # pylint: disable=invalid-name + df = df[(df.tag == tag)] # pylint: disable=invalid-name + assert df.value.values[-1] < lt_val, assertion_err From e2882dd749a2924c0a81cc72cfdf36738de445ff Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 4 Dec 2024 12:25:47 -0500 Subject: [PATCH 0305/1405] drop unnecessary BNB_CUDA_VERSION env var from docker as it just results in warnings (#2121) [skip ci] * drop unnecessary BNB_CUDA_VERSION env var from docker as it just results in warnings * make sure to run tests when cicd Dockerfile changes --- .github/workflows/tests.yml | 2 ++ cicd/Dockerfile.jinja | 1 - docker/Dockerfile | 1 - docker/Dockerfile-tests | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dd4c95bbe4..5abab4a2df 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,6 +10,7 @@ on: - '.github/workflows/*.yml' - 'requirements-tests.txt' - 'cicd/cicd.sh' + - 'cicd/Dockerfile.jinja' pull_request: paths: - '**.py' @@ -17,6 +18,7 @@ on: - '.github/workflows/*.yml' - 'requirements-tests.txt' - 'cicd/cicd.sh' + - 'cicd/Dockerfile.jinja' workflow_dispatch: # Cancel jobs on the same ref if a new one is triggered diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index da28b391fc..4d1c4e664b 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -4,7 +4,6 @@ ENV TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6+PTX" ENV AXOLOTL_EXTRAS="{{ AXOLOTL_EXTRAS }}" ENV AXOLOTL_ARGS="{{ AXOLOTL_ARGS }}" ENV CUDA="{{ CUDA }}" -ENV BNB_CUDA_VERSION="{{ CUDA }}" ENV PYTORCH_VERSION="{{ PYTORCH_VERSION }}" ENV GITHUB_REF="{{ GITHUB_REF }}" ENV GITHUB_SHA="{{ GITHUB_SHA }}" diff --git a/docker/Dockerfile b/docker/Dockerfile index 88b871ea94..261f0e12ad 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,7 +5,6 @@ ARG TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6+PTX" ARG AXOLOTL_EXTRAS="" ARG AXOLOTL_ARGS="" ARG CUDA="118" -ENV BNB_CUDA_VERSION=$CUDA ARG PYTORCH_VERSION="2.1.2" ENV PYTORCH_VERSION=$PYTORCH_VERSION diff --git a/docker/Dockerfile-tests b/docker/Dockerfile-tests index 36c0a639e3..09efeb6199 100644 --- a/docker/Dockerfile-tests +++ b/docker/Dockerfile-tests @@ -5,7 +5,6 @@ ARG TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6+PTX" ARG AXOLOTL_EXTRAS="" ARG AXOLOTL_ARGS="" ARG CUDA="118" -ENV BNB_CUDA_VERSION=$CUDA ARG PYTORCH_VERSION="2.1.2" ARG GITHUB_REF="main" From d7d2fd366eb2b7eaa35c0868a2a2d111b842ca81 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 4 Dec 2024 12:26:08 -0500 Subject: [PATCH 0306/1405] update from unsloth-zoo with additional fixes (#2122) only update tokens seen in the train dataset, log them out explicitly --- src/axolotl/core/tokenizer_utils.py | 196 +++++++++++++++++++++++----- 1 file changed, 162 insertions(+), 34 deletions(-) diff --git a/src/axolotl/core/tokenizer_utils.py b/src/axolotl/core/tokenizer_utils.py index 53c44a75c4..8e5c9205b6 100644 --- a/src/axolotl/core/tokenizer_utils.py +++ b/src/axolotl/core/tokenizer_utils.py @@ -18,21 +18,79 @@ import gc import itertools +import logging +from collections import Counter +import datasets import numpy as np import torch +LOG = logging.getLogger("axolotl.core.tokenizer_utils") -@torch.inference_mode -def fix_untrained_tokens(model, tokenizer, train_dataset, eps=1e-16): + +@torch.inference_mode() +def fix_untrained_tokens( # pylint: disable=too-many-return-statements + model, tokenizer, train_dataset, ignored_tokenizer_names=None, eps=1e-16 +): """ - Many of the newer models have reserved tokens that are not trained. + Llama-3 for eg has untrained vectors in the base model. + These include <|eot_id|>, <|start_header_id|>, <|end_header_id|> + We reset them to the mean of the rest of the tokens """ + # Code licensed under LGPL embedding_matrix = model.get_input_embeddings().weight lm_head_matrix = model.get_output_embeddings().weight + chat_template = getattr(tokenizer, "chat_template", None) + tokenizer = tokenizer.tokenizer if hasattr(tokenizer, "tokenizer") else tokenizer + + # Ignore some model checks for now + if not ignored_tokenizer_names: + ignored_tokenizer_names = [] + if ( + model.config._name_or_path # pylint: disable=protected-access + in ignored_tokenizer_names + ): + return + + # Sometimes the sizes can be different like in vision models + # Ie is in input, but not in output + min_size = min(embedding_matrix.shape[1], lm_head_matrix.shape[1]) + embedding_matrix = embedding_matrix[:, :min_size] + lm_head_matrix = lm_head_matrix[:, :min_size] # Get untrained tokens - indicator_untrained = torch.amax(embedding_matrix, axis=1) <= eps + indicator_untrained1 = torch.amax(embedding_matrix, axis=1) <= eps + # Check lm_head as well + + # Does NOT work for Llama 3.1!! + indicator_untrained2 = torch.amax(lm_head_matrix, axis=1) <= eps + + # We instead check for repeated vectors + lm_head_where = torch.where(indicator_untrained1)[0] + lm_head_bad = lm_head_matrix[lm_head_where] + lm_head_bad = lm_head_bad.cpu().float().numpy().round(3) + counter = Counter() + for row in lm_head_bad: + counter[hash(row.data.tobytes())] += 1 + counter = Counter({k: c for k, c in counter.items() if c >= 2}) + + lm_head_where = lm_head_where.cpu().numpy() + final_bad_lm_head = [] + for j, row in enumerate(lm_head_bad): + if hash(row.data.tobytes()) in counter: + final_bad_lm_head.append(lm_head_where[j]) + indicator_untrained2 = indicator_untrained2 | torch.zeros_like(indicator_untrained2) + indicator_untrained2[final_bad_lm_head] = True + + # Combine both checks + indicator_untrained = indicator_untrained1 & indicator_untrained2 + + # Remove pad token possibility + if hasattr(tokenizer, "pad_token_id"): + pad_token_id = tokenizer.pad_token_id + if pad_token_id is not None and pad_token_id < indicator_untrained.shape[0]: + indicator_untrained[pad_token_id] = False + where_untrained = torch.where(indicator_untrained)[0] n_untrained = where_untrained.shape[0] n_trained = embedding_matrix.shape[0] - n_untrained @@ -40,10 +98,9 @@ def fix_untrained_tokens(model, tokenizer, train_dataset, eps=1e-16): # Get set and actual tokens where_untrained = where_untrained.tolist() if len(where_untrained) == 0: - return False + return # Remove untrained indices where it's longer - where_untrained_set = frozenset(where_untrained) actual_bad_tokens = tokenizer.convert_ids_to_tokens(where_untrained) # Remove None items in actual_bad_tokens @@ -53,10 +110,14 @@ def fix_untrained_tokens(model, tokenizer, train_dataset, eps=1e-16): if_bad_first = False if_bad_second = False # Check tokenizer's chat template for any untrained tokens - chat_template = getattr(tokenizer, "chat_template", None) if chat_template is not None: if_bad_first = any(x in chat_template for x in actual_bad_tokens) + if isinstance(train_dataset, datasets.IterableDataset): + # Skip the check, since the code below assumes + # an indexable dataset + return + # Check the first 250, last 250 input_ids size_dataset = len(train_dataset) size = min(size_dataset, 250) @@ -83,7 +144,69 @@ def fix_untrained_tokens(model, tokenizer, train_dataset, eps=1e-16): # Check if bad tokens exists! if not if_bad_first and not if_bad_second: - return False + return + + # Check if lm_head / embed_token are trainable! + bad_not_trainable = False + if not embedding_matrix.requires_grad: + bad_not_trainable = True + if not lm_head_matrix.requires_grad: + bad_not_trainable = True + + if bad_not_trainable: # pylint: disable=too-many-nested-blocks + final_bad_items = [] + + # Re-check the first 250, last 250 input_ids + size_dataset = len(train_dataset) + size = min(size_dataset, 250) + for j in range(size): + input_ids = train_dataset[j] + if "input_ids" in input_ids: + input_ids = input_ids["input_ids"] + for item in input_ids: + if item in where_untrained_set: + final_bad_items.append(item) + + # Re-check last 250 + left = max(size_dataset - 250, 0) + for j in range(left, size_dataset): + input_ids = train_dataset[j] + if "input_ids" in input_ids: + input_ids = input_ids["input_ids"] + for item in input_ids: + if item in where_untrained_set: + final_bad_items.append(item) + + # If no bad tokens, possibly chat template itself has issues? + if len(final_bad_items) == 0: + # Recheck 2000 and last 2000 items + size_dataset = len(train_dataset) + size = min(size_dataset, 2000) + for j in range(size): + input_ids = train_dataset[j] + if "input_ids" in input_ids: + input_ids = input_ids["input_ids"] + for item in input_ids: + if item in where_untrained_set: + final_bad_items.append(item) + + # Re-check last 2000 + left = max(size_dataset - 2000, 0) + for j in range(left, size_dataset): + input_ids = train_dataset[j] + if "input_ids" in input_ids: + input_ids = input_ids["input_ids"] + for item in input_ids: + if item in where_untrained_set: + final_bad_items.append(item) + + # Most likely false signal! + if len(final_bad_items) == 0: + return + + raise ValueError( + f"Untrained tokens of [{list(set(final_bad_items))}] found, but embed_tokens & lm_head not trainable, causing NaNs. " + ) # Count all the possible bad tokens final_counts = np.zeros( @@ -97,6 +220,23 @@ def mapping(examples): train_dataset.map(mapping, batched=True, desc="Counting untrained tokens") + # Get counts for untrained tokens + counts_untrained = final_counts[where_untrained] + # Identify untrained tokens seen in train_dataset + indices_seen_in_train = np.where(counts_untrained > 0)[0] + tokens_to_update = [where_untrained[i] for i in indices_seen_in_train] + + if len(tokens_to_update) == 0: + LOG.info( + "No untrained tokens found in train_dataset. No embeddings were modified." + ) + return + + # Log the token IDs that are being rescaled + LOG.info( + f"Rescaling embeddings for tokens seen in train_dataset: {tokens_to_update}" + ) + # Get sum of all items sum_embedding = torch.sum(embedding_matrix, dtype=torch.float32, axis=0) sum_lm_head = torch.sum(lm_head_matrix, dtype=torch.float32, axis=0) @@ -113,38 +253,26 @@ def mapping(examples): mean_embedding = sum_embedding / n_trained mean_lm_head = sum_lm_head / n_trained - # Scale each to be equal to 1/max_frequency. Also set some to 0 if none seen - scaling = final_counts[where_untrained] / max(final_counts.max(), 1) + # Compute scaling for tokens to update + scaling = counts_untrained[indices_seen_in_train] / max(final_counts.max(), 1) scaling = torch.tensor(scaling, device=mean_embedding.device).unsqueeze(1) - mean_embedding = ( - mean_embedding.repeat( - ( - n_untrained, - 1, - ) - ) - * scaling + + # Prepare mean embeddings for tokens to update + mean_embedding_repeated = ( + mean_embedding.unsqueeze(0).repeat(len(tokens_to_update), 1) * scaling ) - mean_lm_head = ( - mean_lm_head.repeat( - ( - n_untrained, - 1, - ) - ) - * scaling + mean_lm_head_repeated = ( + mean_lm_head.unsqueeze(0).repeat(len(tokens_to_update), 1) * scaling ) - where_null = scaling.ravel() == 0 - mean_embedding[where_null] = 0 - mean_lm_head[where_null] = 0 - # Set them to the mean - embedding_matrix[where_untrained] = mean_embedding.to(embedding_matrix.dtype) - lm_head_matrix[where_untrained] = mean_lm_head.to(lm_head_matrix.dtype) + # Update embeddings only for tokens seen in train_dataset + embedding_matrix[tokens_to_update] = mean_embedding_repeated.to( + embedding_matrix.dtype + ) + lm_head_matrix[tokens_to_update] = mean_lm_head_repeated.to(lm_head_matrix.dtype) # Clean up for _ in range(3): gc.collect() torch.cuda.empty_cache() - - return True + return From 4baf8e5e96ad53db0a822f2922747d440530b77c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 5 Dec 2024 21:19:52 -0500 Subject: [PATCH 0307/1405] cleanup the readme, add Modal as sponsor (#2130) [skip ci] --- README.md | 196 ++++++++++++++++++++++-------------------------------- 1 file changed, 79 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index cd03abf003..8ee959d218 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,12 @@ Features: ## Table of Contents - [Axolotl](#axolotl) - [Table of Contents](#table-of-contents) - - [Axolotl supports](#axolotl-supports) - [Quickstart ⚡](#quickstart-) - [Usage](#usage) + - [Badge ❤🏷️](#badge-️) + - [Contributing 🤝](#contributing-) + - [Sponsors 🤝❤](#sponsors-) + - [Axolotl supports](#axolotl-supports) - [Advanced Setup](#advanced-setup) - [Environment](#environment) - [Docker](#docker) @@ -75,14 +78,6 @@ Features: - [Tokenization Mismatch b/w Inference \& Training](#tokenization-mismatch-bw-inference--training) - [Debugging Axolotl](#debugging-axolotl) - [Need help? 🙋](#need-help-) - - [Badge ❤🏷️](#badge-️) - - [Community Showcase](#community-showcase) - - [Contributing 🤝](#contributing-) - - [Sponsors 🤝❤](#sponsors-) - - [💎 Diamond Sponsors - Contact directly](#-diamond-sponsors---contact-directly) - - [🥇 Gold Sponsors - $5000/mo](#-gold-sponsors---5000mo) - - [🥈 Silver Sponsors - $1000/mo](#-silver-sponsors---1000mo) - - [🥉 Bronze Sponsors - $500/mo](#-bronze-sponsors---500mo) @@ -105,36 +100,11 @@ Features: -## Axolotl supports - -| | fp16/fp32 | lora | qlora | gptq | gptq w/flash attn | flash attn | xformers attn | -|-------------|:----------|:-----|-------|------|-------------------|------------|--------------| -| llama | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Mistral | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Mixtral-MoE | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | -| Mixtral8X22 | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | -| Pythia | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❓ | -| cerebras | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❓ | -| btlm | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❓ | -| mpt | ✅ | ❌ | ❓ | ❌ | ❌ | ❌ | ❓ | -| falcon | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❓ | -| gpt-j | ✅ | ✅ | ✅ | ❌ | ❌ | ❓ | ❓ | -| XGen | ✅ | ❓ | ✅ | ❓ | ❓ | ❓ | ✅ | -| phi | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | -| RWKV | ✅ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | -| Qwen | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | -| Gemma | ✅ | ✅ | ✅ | ❓ | ❓ | ✅ | ❓ | -| Jamba | ✅ | ✅ | ✅ | ❓ | ❓ | ✅ | ❓ | - -✅: supported -❌: not supported -❓: untested - ## Quickstart ⚡ Get started with Axolotl in just a few steps! This quickstart guide will walk you through setting up and running a basic fine-tuning task. -**Requirements**: Nvidia GPU (Ampere architecture or newer for `bf16` and Flash Attention), Python >=3.10 and PyTorch >=2.3.1. +**Requirements**: *Nvidia* GPU (Ampere architecture or newer for `bf16` and Flash Attention) or *AMD* GPU, Python >=3.10 and PyTorch >=2.3.1. ```bash git clone https://github.com/axolotl-ai-cloud/axolotl @@ -165,6 +135,78 @@ accelerate launch -m axolotl.cli.inference examples/openllama-3b/lora.yml \ accelerate launch -m axolotl.cli.train https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/openllama-3b/lora.yml ``` +## Badge ❤🏷️ + +Building something cool with Axolotl? Consider adding a badge to your model card. + +```markdown +[Built with Axolotl](https://github.com/axolotl-ai-cloud/axolotl) +``` + +[Built with Axolotl](https://github.com/axolotl-ai-cloud/axolotl) + +## Sponsors 🤝❤ + +If you love axolotl, consider sponsoring the project by reaching out directly to [wing@axolotl.ai](mailto:wing@axolotl.ai). + +--- + +- [Modal](https://modal.com/) Modal lets you run data/AI jobs in the cloud, by just writing a few lines of Python. Customers use Modal to deploy Gen AI models at large scale, fine-tune LLM models, run protein folding simulations, and much more. + +--- + +## Contributing 🤝 + +Please read the [contributing guide](./.github/CONTRIBUTING.md) + +Bugs? Please check the [open issues](https://github.com/axolotl-ai-cloud/axolotl/issues/bug) else create a new Issue. + +PRs are **greatly welcome**! + +Please run the quickstart instructions followed by the below to setup env: +```bash +pip3 install -r requirements-dev.txt -r requirements-tests.txt +pre-commit install + +# test +pytest tests/ + +# optional: run against all files +pre-commit run --all-files +``` + +Thanks to all of our contributors to date. Help drive open source AI progress forward by contributing to Axolotl. + + + contributor chart by https://contrib.rocks + + +## Axolotl supports + +| | fp16/fp32 | lora | qlora | gptq | gptq w/flash attn | flash attn | xformers attn | +|-------------|:----------|:-----|-------|------|-------------------|------------|--------------| +| llama | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Mistral | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Mixtral-MoE | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | +| Mixtral8X22 | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | +| Pythia | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❓ | +| cerebras | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❓ | +| btlm | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❓ | +| mpt | ✅ | ❌ | ❓ | ❌ | ❌ | ❌ | ❓ | +| falcon | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❓ | +| gpt-j | ✅ | ✅ | ✅ | ❌ | ❌ | ❓ | ❓ | +| XGen | ✅ | ❓ | ✅ | ❓ | ❓ | ❓ | ✅ | +| phi | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | +| RWKV | ✅ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | +| Qwen | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | +| Gemma | ✅ | ✅ | ✅ | ❓ | ❓ | ✅ | ❓ | +| Jamba | ✅ | ✅ | ✅ | ❓ | ❓ | ✅ | ❓ | + +✅: supported +❌: not supported +❓: untested + + ## Advanced Setup ### Environment @@ -682,86 +724,6 @@ See [this debugging guide](docs/debugging.qmd) for tips on debugging Axolotl, al ## Need help? 🙋 -Join our [Discord server](https://discord.gg/HhrNrHJPRb) where we our community members can help you. - -Need dedicated support? Please contact us at [✉️wing@openaccessaicollective.org](mailto:wing@openaccessaicollective.org) for dedicated support options. - -## Badge ❤🏷️ - -Building something cool with Axolotl? Consider adding a badge to your model card. - -```markdown -[Built with Axolotl](https://github.com/axolotl-ai-cloud/axolotl) -``` - -[Built with Axolotl](https://github.com/axolotl-ai-cloud/axolotl) - -## Community Showcase - -Check out some of the projects and models that have been built using Axolotl! Have a model you'd like to add to our Community Showcase? Open a PR with your model. - -Open Access AI Collective -- [Minotaur 13b](https://huggingface.co/openaccess-ai-collective/minotaur-13b-fixed) -- [Manticore 13b](https://huggingface.co/openaccess-ai-collective/manticore-13b) -- [Hippogriff 30b](https://huggingface.co/openaccess-ai-collective/hippogriff-30b-chat) - -PocketDoc Labs -- [Dan's PersonalityEngine 13b LoRA](https://huggingface.co/PocketDoc/Dans-PersonalityEngine-13b-LoRA) - -## Contributing 🤝 - -Please read the [contributing guide](./.github/CONTRIBUTING.md) - -Bugs? Please check the [open issues](https://github.com/axolotl-ai-cloud/axolotl/issues/bug) else create a new Issue. - -PRs are **greatly welcome**! - -Please run the quickstart instructions followed by the below to setup env: -```bash -pip3 install -r requirements-dev.txt -r requirements-tests.txt -pre-commit install - -# test -pytest tests/ - -# optional: run against all files -pre-commit run --all-files -``` - -Thanks to all of our contributors to date. Help drive open source AI progress forward by contributing to Axolotl. - - - contributor chart by https://contrib.rocks - - -## Sponsors 🤝❤ - -OpenAccess AI Collective is run by volunteer contributors such as [winglian](https://github.com/winglian), -[NanoCode012](https://github.com/NanoCode012), [tmm1](https://github.com/tmm1), -[mhenrichsen](https://github.com/mhenrichsen), [casper-hansen](https://github.com/casper-hansen), -[hamelsmu](https://github.com/hamelsmu) and many more who help us accelerate forward by fixing bugs, answering -community questions and implementing new features. Axolotl needs donations from sponsors for the compute needed to -run our unit & integration tests, troubleshooting community issues, and providing bounties. If you love axolotl, -consider sponsoring the project via [GitHub Sponsors](https://github.com/sponsors/OpenAccess-AI-Collective), -[Ko-fi](https://ko-fi.com/axolotl_ai) or reach out directly to -[wing@openaccessaicollective.org](mailto:wing@openaccessaicollective.org). - ---- - -#### 💎 Diamond Sponsors - [Contact directly](mailto:wing@openaccessaicollective.org) - ---- - -#### 🥇 Gold Sponsors - $5000/mo - ---- - -#### 🥈 Silver Sponsors - $1000/mo +Join our [Discord server](https://discord.gg/HhrNrHJPRb) where our community members can help you. ---- - -#### 🥉 Bronze Sponsors - $500/mo - - - [JarvisLabs.ai](https://jarvislabs.ai) - ---- +Need dedicated support? Please contact us at [✉️wing@axolotl.ai](ailto:wing@axolotl.ai) for dedicated support options. From e399ba533e545978c7f3fef04e8403ffcbded5c2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 5 Dec 2024 21:20:40 -0500 Subject: [PATCH 0308/1405] fix license header for fix_untrained_tokens from unsloth-zoo (#2129) [skip ci] --- src/axolotl/core/tokenizer_utils.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/axolotl/core/tokenizer_utils.py b/src/axolotl/core/tokenizer_utils.py index 8e5c9205b6..1b86b9c497 100644 --- a/src/axolotl/core/tokenizer_utils.py +++ b/src/axolotl/core/tokenizer_utils.py @@ -3,18 +3,12 @@ """ # Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. +# GNU LESSER GENERAL PUBLIC LICENSE +# Version 3, 29 June 2007 # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Copyright (C) 2007 Free Software Foundation, Inc. +# Everyone is permitted to copy and distribute verbatim copies +# of this license document, but changing it is not allowed. import gc import itertools From fc973f432256930adcb6b7fcef26c9b9531b74aa Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Thu, 5 Dec 2024 22:11:48 -0500 Subject: [PATCH 0309/1405] CLI Implementation with Click (#2107) * Initial CLI implementation with click package * Adding fetch command for pulling examples and deepspeed configs * Automating default options for CliArgs classes * Mimicking existing no config behavior * bugfix in choose_config * Updating fetch to sync instead of re-download * bugfix * isort fix * fixing yaml isort order * pre-commit fixes * simplifying argument parsing -- pass through kwargs to do_cli * make accelerate launch default for non-preprocess commands * fixing arg handling * testing None placeholder approach * removing hacky --use-gpu argument to preprocess command * Adding brief README documentation for CLI * remove (New) * Initial CLI pytest tests * progress on CLI pytest * adding inference CLI tests; cleanup * Refactor train CLI tests to remove various mocking * Major CLI test refator; adding remaining CLI codepath test coverage * pytest fixes * remove integration markers * parallelizing examples, deepspeed config downloads; rename test to match other CLI test naming * moving cli pytest due to isolation issues; cleanup * testing fixes; various minor improvements * fix * tests fix * Update tests/cli/conftest.py Co-authored-by: Wing Lian --------- Co-authored-by: Dan Saunders Co-authored-by: Wing Lian --- .github/workflows/tests-nightly.yml | 3 +- .github/workflows/tests.yml | 6 +- README.md | 41 +++- cicd/cicd.sh | 5 +- outputs | 1 + setup.py | 5 + src/axolotl/cli/__init__.py | 4 +- src/axolotl/cli/inference.py | 3 +- src/axolotl/cli/main.py | 231 ++++++++++++++++++ src/axolotl/cli/merge_lora.py | 3 +- src/axolotl/cli/merge_sharded_fsdp_weights.py | 2 +- src/axolotl/cli/utils.py | 218 +++++++++++++++++ tests/cli/__init__.py | 0 tests/cli/conftest.py | 36 +++ tests/cli/test_cli_fetch.py | 38 +++ tests/cli/test_cli_inference.py | 30 +++ tests/cli/test_cli_interface.py | 47 ++++ tests/cli/test_cli_merge_lora.py | 56 +++++ .../test_cli_merge_sharded_fsdp_weights.py | 60 +++++ tests/cli/test_cli_preprocess.py | 71 ++++++ tests/cli/test_cli_shard.py | 76 ++++++ tests/cli/test_cli_train.py | 98 ++++++++ tests/cli/test_utils.py | 89 +++++++ tests/{ => patched}/test_validation.py | 0 tests/test_data.py | 2 +- 25 files changed, 1113 insertions(+), 12 deletions(-) create mode 120000 outputs create mode 100644 src/axolotl/cli/main.py create mode 100644 src/axolotl/cli/utils.py create mode 100644 tests/cli/__init__.py create mode 100644 tests/cli/conftest.py create mode 100644 tests/cli/test_cli_fetch.py create mode 100644 tests/cli/test_cli_inference.py create mode 100644 tests/cli/test_cli_interface.py create mode 100644 tests/cli/test_cli_merge_lora.py create mode 100644 tests/cli/test_cli_merge_sharded_fsdp_weights.py create mode 100644 tests/cli/test_cli_preprocess.py create mode 100644 tests/cli/test_cli_shard.py create mode 100644 tests/cli/test_cli_train.py create mode 100644 tests/cli/test_utils.py rename tests/{ => patched}/test_validation.py (100%) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 004df55186..f81f711de4 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -60,7 +60,8 @@ jobs: - name: Run tests run: | - pytest --ignore=tests/e2e/ tests/ + pytest -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ + pytest tests/patched/ - name: cleanup pip cache run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5abab4a2df..89f19ca9c3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -79,7 +79,8 @@ jobs: - name: Run tests run: | - pytest -n8 --ignore=tests/e2e/ tests/ + pytest -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ + pytest tests/patched/ - name: cleanup pip cache run: | @@ -123,7 +124,8 @@ jobs: - name: Run tests run: | - pytest -n8 --ignore=tests/e2e/ tests/ + pytest -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ + pytest tests/patched/ - name: cleanup pip cache run: | diff --git a/README.md b/README.md index 8ee959d218..75e7faa642 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,46 @@ accelerate launch -m axolotl.cli.inference examples/openllama-3b/lora.yml \ accelerate launch -m axolotl.cli.train https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/openllama-3b/lora.yml ``` +### Axolotl CLI + +If you've installed this package using `pip` from source, we now support a new, more +streamlined CLI using [click](https://click.palletsprojects.com/en/stable/). Rewriting +the above commands: + +```bash +# preprocess datasets - optional but recommended +CUDA_VISIBLE_DEVICES="0" axolotl preprocess examples/openllama-3b/lora.yml + +# finetune lora +axolotl train examples/openllama-3b/lora.yml + +# inference +axolotl inference examples/openllama-3b/lora.yml \ + --lora-model-dir="./outputs/lora-out" + +# gradio +axolotl inference examples/openllama-3b/lora.yml \ + --lora-model-dir="./outputs/lora-out" --gradio + +# remote yaml files - the yaml config can be hosted on a public URL +# Note: the yaml config must directly link to the **raw** yaml +axolotl train https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/openllama-3b/lora.yml +``` + +We've also added a new command for fetching `examples` and `deepspeed_configs` to your +local machine. This will come in handy when installing `axolotl` from PyPI. + +```bash +# Fetch example YAML files (stores in "examples/" folder) +axolotl fetch examples + +# Fetch deepspeed config files (stores in "deepspeed_configs/" folder) +axolotl fetch deepspeed_configs + +# Optionally, specify a destination folder +axolotl fetch examples --dest path/to/folder +``` + ## Badge ❤🏷️ Building something cool with Axolotl? Consider adding a badge to your model card. @@ -206,7 +246,6 @@ Thanks to all of our contributors to date. Help drive open source AI progress fo ❌: not supported ❓: untested - ## Advanced Setup ### Environment diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 68c40bb78d..79b3cc95e0 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -1,6 +1,7 @@ #!/bin/bash set -e -pytest -v --durations=10 -n8 --ignore=tests/e2e/ /workspace/axolotl/tests/ -pytest -v --durations=10 -n1 --dist loadfile -v /workspace/axolotl/tests/e2e/patched/ /workspace/axolotl/tests/e2e/integrations/ +pytest -v --durations=10 -n8 --ignore=tests/e2e/ --ignore=tests/patched/ /workspace/axolotl/tests/ +pytest -v --durations=10 -n1 --dist loadfile /workspace/axolotl/tests/patched/ +pytest -v --durations=10 -n1 --dist loadfile /workspace/axolotl/tests/e2e/patched/ /workspace/axolotl/tests/e2e/integrations/ pytest -v --durations=10 --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ diff --git a/outputs b/outputs new file mode 120000 index 0000000000..be3c4a823f --- /dev/null +++ b/outputs @@ -0,0 +1 @@ +/workspace/data/axolotl-artifacts \ No newline at end of file diff --git a/setup.py b/setup.py index ea779d5996..336da98f4b 100644 --- a/setup.py +++ b/setup.py @@ -103,6 +103,11 @@ def parse_requirements(): packages=find_packages("src"), install_requires=install_requires, dependency_links=dependency_links, + entry_points={ + "console_scripts": [ + "axolotl=axolotl.cli.main:main", + ], + }, extras_require={ "flash-attn": [ "flash-attn==2.7.0.post2", diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 1e61b220b9..e8ef862854 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -380,7 +380,7 @@ def choose_config(path: Path): if len(yaml_files) == 1: print(f"Using default YAML file '{yaml_files[0]}'") - return yaml_files[0] + return str(yaml_files[0]) print("Choose a YAML file:") for idx, file in enumerate(yaml_files): @@ -391,7 +391,7 @@ def choose_config(path: Path): try: choice = int(input("Enter the number of your choice: ")) if 1 <= choice <= len(yaml_files): - chosen_file = yaml_files[choice - 1] + chosen_file = str(yaml_files[choice - 1]) else: print("Invalid choice. Please choose a number from the list.") except ValueError: diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index b738e5c222..a5f1a8ad8b 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -2,6 +2,7 @@ CLI to run inference on a trained model """ from pathlib import Path +from typing import Union import fire import transformers @@ -16,7 +17,7 @@ from axolotl.common.cli import TrainerCliArgs -def do_cli(config: Path = Path("examples/"), gradio=False, **kwargs): +def do_cli(config: Union[Path, str] = Path("examples/"), gradio=False, **kwargs): # pylint: disable=duplicate-code print_axolotl_text_art() parsed_cfg = load_cfg(config, inference=True, **kwargs) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py new file mode 100644 index 0000000000..a138daab25 --- /dev/null +++ b/src/axolotl/cli/main.py @@ -0,0 +1,231 @@ +"""CLI definition for various axolotl commands.""" +# pylint: disable=redefined-outer-name +import subprocess # nosec B404 +from typing import Optional + +import click + +from axolotl.cli.utils import ( + add_options_from_config, + add_options_from_dataclass, + build_command, + fetch_from_github, +) +from axolotl.common.cli import PreprocessCliArgs, TrainerCliArgs +from axolotl.utils.config.models.input.v0_4_1 import AxolotlInputConfig + + +@click.group() +def cli(): + """Axolotl CLI - Train and fine-tune large language models""" + + +@cli.command() +@click.argument("config", type=click.Path(exists=True, path_type=str)) +@add_options_from_dataclass(PreprocessCliArgs) +@add_options_from_config(AxolotlInputConfig) +def preprocess(config: str, **kwargs): + """Preprocess datasets before training.""" + kwargs = {k: v for k, v in kwargs.items() if v is not None} + + from axolotl.cli.preprocess import do_cli + + do_cli(config=config, **kwargs) + + +@cli.command() +@click.argument("config", type=click.Path(exists=True, path_type=str)) +@click.option( + "--accelerate/--no-accelerate", + default=True, + help="Use accelerate launch for multi-GPU training", +) +@add_options_from_dataclass(TrainerCliArgs) +@add_options_from_config(AxolotlInputConfig) +def train(config: str, accelerate: bool, **kwargs): + """Train or fine-tune a model.""" + kwargs = {k: v for k, v in kwargs.items() if v is not None} + + if accelerate: + base_cmd = ["accelerate", "launch", "-m", "axolotl.cli.train"] + if config: + base_cmd.append(config) + cmd = build_command(base_cmd, kwargs) + subprocess.run(cmd, check=True) # nosec B603 + else: + from axolotl.cli.train import do_cli + + do_cli(config=config, **kwargs) + + +@cli.command() +@click.argument("config", type=click.Path(exists=True, path_type=str)) +@click.option( + "--accelerate/--no-accelerate", + default=True, + help="Use accelerate launch for multi-GPU inference", +) +@click.option( + "--lora-model-dir", + type=click.Path(exists=True, path_type=str), + help="Directory containing LoRA model", +) +@click.option( + "--base-model", + type=click.Path(exists=True, path_type=str), + help="Path to base model for non-LoRA models", +) +@click.option("--gradio", is_flag=True, help="Launch Gradio interface") +@click.option("--load-in-8bit", is_flag=True, help="Load model in 8-bit mode") +@add_options_from_dataclass(TrainerCliArgs) +@add_options_from_config(AxolotlInputConfig) +def inference( + config: str, + accelerate: bool, + lora_model_dir: Optional[str] = None, + base_model: Optional[str] = None, + **kwargs, +): + """Run inference with a trained model.""" + kwargs = {k: v for k, v in kwargs.items() if v is not None} + del kwargs["inference"] # interferes with inference.do_cli + + if lora_model_dir: + kwargs["lora_model_dir"] = lora_model_dir + if base_model: + kwargs["output_dir"] = base_model + + if accelerate: + base_cmd = ["accelerate", "launch", "-m", "axolotl.cli.inference"] + if config: + base_cmd.append(config) + cmd = build_command(base_cmd, kwargs) + subprocess.run(cmd, check=True) # nosec B603 + else: + from axolotl.cli.inference import do_cli + + do_cli(config=config, **kwargs) + + +@cli.command() +@click.argument("config", type=click.Path(exists=True, path_type=str)) +@click.option( + "--accelerate/--no-accelerate", + default=False, + help="Use accelerate launch for multi-GPU operations", +) +@click.option( + "--model-dir", + type=click.Path(exists=True, path_type=str), + help="Directory containing model weights to shard", +) +@click.option( + "--save-dir", + type=click.Path(path_type=str), + help="Directory to save sharded weights", +) +@add_options_from_dataclass(TrainerCliArgs) +@add_options_from_config(AxolotlInputConfig) +def shard(config: str, accelerate: bool, **kwargs): + """Shard model weights.""" + kwargs = {k: v for k, v in kwargs.items() if v is not None} + + if accelerate: + base_cmd = ["accelerate", "launch", "-m", "axolotl.cli.shard"] + if config: + base_cmd.append(config) + cmd = build_command(base_cmd, kwargs) + subprocess.run(cmd, check=True) # nosec B603 + else: + from axolotl.cli.shard import do_cli + + do_cli(config=config, **kwargs) + + +@cli.command() +@click.argument("config", type=click.Path(exists=True, path_type=str)) +@click.option( + "--accelerate/--no-accelerate", + default=True, + help="Use accelerate launch for weight merging", +) +@click.option( + "--model-dir", + type=click.Path(exists=True, path_type=str), + help="Directory containing sharded weights", +) +@click.option( + "--save-path", type=click.Path(path_type=str), help="Path to save merged weights" +) +@add_options_from_dataclass(TrainerCliArgs) +@add_options_from_config(AxolotlInputConfig) +def merge_sharded_fsdp_weights(config: str, accelerate: bool, **kwargs): + """Merge sharded FSDP model weights.""" + kwargs = {k: v for k, v in kwargs.items() if v is not None} + + if accelerate: + base_cmd = [ + "accelerate", + "launch", + "-m", + "axolotl.cli.merge_sharded_fsdp_weights", + ] + if config: + base_cmd.append(config) + cmd = build_command(base_cmd, kwargs) + subprocess.run(cmd, check=True) # nosec B603 + else: + from axolotl.cli.merge_sharded_fsdp_weights import do_cli + + do_cli(config=config, **kwargs) + + +@cli.command() +@click.argument("config", type=click.Path(exists=True, path_type=str)) +@click.option( + "--lora-model-dir", + type=click.Path(exists=True, path_type=str), + help="Directory containing the LoRA model to merge", +) +@click.option( + "--output-dir", + type=click.Path(path_type=str), + help="Directory to save the merged model", +) +def merge_lora( + config: str, + lora_model_dir: Optional[str] = None, + output_dir: Optional[str] = None, +): + """Merge a trained LoRA into a base model""" + kwargs = {} + if lora_model_dir: + kwargs["lora_model_dir"] = lora_model_dir + if output_dir: + kwargs["output_dir"] = output_dir + + from axolotl.cli.merge_lora import do_cli + + do_cli(config=config, **kwargs) + + +@cli.command() +@click.argument("directory", type=click.Choice(["examples", "deepspeed_configs"])) +@click.option("--dest", help="Destination directory") +def fetch(directory: str, dest: Optional[str]): + """ + Fetch example configs or other resources. + + Available directories: + - examples: Example configuration files + - deepspeed_configs: DeepSpeed configuration files + """ + fetch_from_github(f"{directory}/", dest) + + +def main(): + cli() + + +if __name__ == "__main__": + main() diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 6588b5ee4e..8c321bc48e 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -2,6 +2,7 @@ CLI to run merge a trained LoRA into a base model """ from pathlib import Path +from typing import Union import fire import transformers @@ -11,7 +12,7 @@ from axolotl.common.cli import TrainerCliArgs -def do_cli(config: Path = Path("examples/"), **kwargs): +def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): # pylint: disable=duplicate-code print_axolotl_text_art() parser = transformers.HfArgumentParser((TrainerCliArgs)) diff --git a/src/axolotl/cli/merge_sharded_fsdp_weights.py b/src/axolotl/cli/merge_sharded_fsdp_weights.py index 25408fd57e..6be9af1f76 100644 --- a/src/axolotl/cli/merge_sharded_fsdp_weights.py +++ b/src/axolotl/cli/merge_sharded_fsdp_weights.py @@ -177,7 +177,7 @@ def merge_fsdp_weights( state.wait_for_everyone() -def do_cli(config: Path = Path("examples/"), **kwargs): +def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): # pylint: disable=duplicate-code print_axolotl_text_art() parser = transformers.HfArgumentParser((TrainerCliArgs)) diff --git a/src/axolotl/cli/utils.py b/src/axolotl/cli/utils.py new file mode 100644 index 0000000000..f0e2573f72 --- /dev/null +++ b/src/axolotl/cli/utils.py @@ -0,0 +1,218 @@ +"""Utility methods for axoltl CLI.""" +import concurrent.futures +import dataclasses +import hashlib +import json +import logging +from pathlib import Path +from types import NoneType +from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_args, get_origin + +import click +import requests +from pydantic import BaseModel + +LOG = logging.getLogger("axolotl.cli.utils") + + +def add_options_from_dataclass(config_class: Type[Any]): + """Create Click options from the fields of a dataclass.""" + + def decorator(function): + # Process dataclass fields in reverse order for correct option ordering + for field in reversed(dataclasses.fields(config_class)): + field_type = field.type + + if get_origin(field_type) is Union and type(None) in get_args(field_type): + field_type = next( + t for t in get_args(field_type) if not isinstance(t, NoneType) + ) + + if field_type == bool: + field_name = field.name.replace("_", "-") + option_name = f"--{field_name}/--no-{field_name}" + function = click.option( + option_name, + default=field.default, + help=field.metadata.get("description"), + )(function) + else: + option_name = f"--{field.name.replace('_', '-')}" + function = click.option( + option_name, + type=field_type, + default=field.default, + help=field.metadata.get("description"), + )(function) + return function + + return decorator + + +def add_options_from_config(config_class: Type[BaseModel]): + """Create Click options from the fields of a Pydantic model.""" + + def decorator(function): + # Process model fields in reverse order for correct option ordering + for name, field in reversed(config_class.model_fields.items()): + if field.annotation == bool: + field_name = name.replace("_", "-") + option_name = f"--{field_name}/--no-{field_name}" + function = click.option( + option_name, default=None, help=field.description + )(function) + else: + option_name = f"--{name.replace('_', '-')}" + function = click.option( + option_name, default=None, help=field.description + )(function) + return function + + return decorator + + +def build_command(base_cmd: List[str], options: Dict[str, Any]) -> List[str]: + """Build command list from base command and options.""" + cmd = base_cmd.copy() + + for key, value in options.items(): + if value is None: + continue + + key = key.replace("_", "-") + + if isinstance(value, bool): + if value: + cmd.append(f"--{key}") + else: + cmd.extend([f"--{key}", str(value)]) + + return cmd + + +def download_file( + file_info: tuple, raw_base_url: str, dest_path: Path, dir_prefix: str +) -> Tuple[str, str]: + """ + Download a single file and return its processing status. + + Args: + file_info: Tuple of (file_path, remote_sha) + raw_base_url: Base URL for raw GitHub content + dest_path: Local destination directory + dir_prefix: Directory prefix to filter files + + Returns: + Tuple of (file_path, status) where status is 'new', 'updated', or 'unchanged' + """ + file_path, remote_sha = file_info + raw_url = f"{raw_base_url}/{file_path}" + dest_file = dest_path / file_path.split(dir_prefix)[-1] + + # Check if file exists and needs updating + if dest_file.exists(): + with open(dest_file, "rb") as file: + content = file.read() + # Calculate git blob SHA + blob = b"blob " + str(len(content)).encode() + b"\0" + content + local_sha = hashlib.sha1(blob, usedforsecurity=False).hexdigest() + + if local_sha == remote_sha: + print(f"Skipping {file_path} (unchanged)") + return file_path, "unchanged" + + print(f"Updating {file_path}") + status = "new" + else: + print(f"Downloading {file_path}") + status = "new" + + # Create directories if needed + dest_file.parent.mkdir(parents=True, exist_ok=True) + + # Download and save file + try: + response = requests.get(raw_url, timeout=30) + response.raise_for_status() + + with open(dest_file, "wb") as file: + file.write(response.content) + + return file_path, status + except (requests.RequestException, IOError) as request_error: + print(f"Error downloading {file_path}: {str(request_error)}") + return file_path, "error" + + +def fetch_from_github( + dir_prefix: str, dest_dir: Optional[str] = None, max_workers: int = 5 +) -> None: + """ + Sync files from a specific directory in the GitHub repository. + Only downloads files that don't exist locally or have changed. + + Args: + dir_prefix: Directory prefix to filter files (e.g., 'examples/', 'deepspeed_configs/') + dest_dir: Local destination directory + max_workers: Maximum number of concurrent downloads + """ + api_url = "https://api.github.com/repos/axolotl-ai-cloud/axolotl/git/trees/main?recursive=1" + raw_base_url = "https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main" + + # Get repository tree with timeout + response = requests.get(api_url, timeout=30) + response.raise_for_status() + tree = json.loads(response.text) + + # Filter for files and get their SHA + files = { + item["path"]: item["sha"] + for item in tree["tree"] + if item["type"] == "blob" and item["path"].startswith(dir_prefix) + } + + if not files: + raise click.ClickException(f"No files found in {dir_prefix}") + + # Default destination directory is the last part of dir_prefix + default_dest = Path(dir_prefix.rstrip("/")) + dest_path = Path(dest_dir) if dest_dir else default_dest + + # Keep track of processed files for summary + files_processed: Dict[str, List[str]] = { + "new": [], + "updated": [], + "unchanged": [], + "error": [], + } + + # Process files in parallel using ThreadPoolExecutor + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + future_to_file = { + executor.submit( + download_file, + (file_path, remote_sha), + raw_base_url, + dest_path, + dir_prefix, + ): file_path + for file_path, remote_sha in files.items() + } + + # Process completed tasks as they finish + for future in concurrent.futures.as_completed(future_to_file): + file_path = future_to_file[future] + try: + file_path, status = future.result() + files_processed[status].append(file_path) + except (requests.RequestException, IOError) as request_error: + print(f"Error processing {file_path}: {str(request_error)}") + files_processed["error"].append(file_path) + + # Log summary + LOG.info("\nSync Summary:") + LOG.info(f"New files: {len(files_processed['new'])}") + LOG.info(f"Updated files: {len(files_processed['updated'])}") + LOG.info(f"Unchanged files: {len(files_processed['unchanged'])}") + if files_processed["error"]: + LOG.info(f"Failed files: {len(files_processed['error'])}") diff --git a/tests/cli/__init__.py b/tests/cli/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py new file mode 100644 index 0000000000..78b090e19e --- /dev/null +++ b/tests/cli/conftest.py @@ -0,0 +1,36 @@ +"""Shared pytest fixtures for cli module.""" +import pytest +from click.testing import CliRunner + +VALID_TEST_CONFIG = """ +base_model: HuggingFaceTB/SmolLM2-135M +datasets: + - path: mhenrichsen/alpaca_2k_test + type: alpaca +sequence_len: 2048 +max_steps: 1 +micro_batch_size: 1 +gradient_accumulation_steps: 1 +learning_rate: 1e-3 +special_tokens: + pad_token: <|endoftext|> +""" + + +@pytest.fixture +def cli_runner(): + return CliRunner() + + +@pytest.fixture +def valid_test_config(): + return VALID_TEST_CONFIG + + +@pytest.fixture +def config_path(tmp_path): + """Creates a temporary config file""" + path = tmp_path / "config.yml" + path.write_text(VALID_TEST_CONFIG) + + return path diff --git a/tests/cli/test_cli_fetch.py b/tests/cli/test_cli_fetch.py new file mode 100644 index 0000000000..0df87b0299 --- /dev/null +++ b/tests/cli/test_cli_fetch.py @@ -0,0 +1,38 @@ +"""pytest tests for axolotl CLI fetch command.""" +from unittest.mock import patch + +from axolotl.cli.main import fetch + + +def test_fetch_cli_examples(cli_runner): + """Test fetch command with examples directory""" + with patch("axolotl.cli.main.fetch_from_github") as mock_fetch: + result = cli_runner.invoke(fetch, ["examples"]) + + assert result.exit_code == 0 + mock_fetch.assert_called_once_with("examples/", None) + + +def test_fetch_cli_deepspeed(cli_runner): + """Test fetch command with deepspeed_configs directory""" + with patch("axolotl.cli.main.fetch_from_github") as mock_fetch: + result = cli_runner.invoke(fetch, ["deepspeed_configs"]) + + assert result.exit_code == 0 + mock_fetch.assert_called_once_with("deepspeed_configs/", None) + + +def test_fetch_cli_with_dest(cli_runner, tmp_path): + """Test fetch command with custom destination""" + with patch("axolotl.cli.main.fetch_from_github") as mock_fetch: + custom_dir = tmp_path / "tmp_examples" + result = cli_runner.invoke(fetch, ["examples", "--dest", str(custom_dir)]) + + assert result.exit_code == 0 + mock_fetch.assert_called_once_with("examples/", str(custom_dir)) + + +def test_fetch_cli_invalid_directory(cli_runner): + """Test fetch command with invalid directory choice""" + result = cli_runner.invoke(fetch, ["invalid"]) + assert result.exit_code != 0 diff --git a/tests/cli/test_cli_inference.py b/tests/cli/test_cli_inference.py new file mode 100644 index 0000000000..7cb163d255 --- /dev/null +++ b/tests/cli/test_cli_inference.py @@ -0,0 +1,30 @@ +"""pytest tests for axolotl CLI inference command.""" +from unittest.mock import patch + +from axolotl.cli.main import cli + + +def test_inference_basic(cli_runner, config_path): + """Test basic inference""" + with patch("axolotl.cli.inference.do_inference") as mock: + result = cli_runner.invoke( + cli, + ["inference", str(config_path), "--no-accelerate"], + catch_exceptions=False, + ) + + assert mock.called + assert result.exit_code == 0 + + +def test_inference_gradio(cli_runner, config_path): + """Test basic inference (gradio path)""" + with patch("axolotl.cli.inference.do_inference_gradio") as mock: + result = cli_runner.invoke( + cli, + ["inference", str(config_path), "--no-accelerate", "--gradio"], + catch_exceptions=False, + ) + + assert mock.called + assert result.exit_code == 0 diff --git a/tests/cli/test_cli_interface.py b/tests/cli/test_cli_interface.py new file mode 100644 index 0000000000..ed8335b766 --- /dev/null +++ b/tests/cli/test_cli_interface.py @@ -0,0 +1,47 @@ +"""General pytest tests for axolotl.cli.main interface.""" +from axolotl.cli.main import build_command, cli + + +def test_build_command(): + """Test converting dict of options to CLI arguments""" + base_cmd = ["accelerate", "launch"] + options = { + "learning_rate": 1e-4, + "batch_size": 8, + "debug": True, + "use_fp16": False, + "null_value": None, + } + + result = build_command(base_cmd, options) + assert result == [ + "accelerate", + "launch", + "--learning-rate", + "0.0001", + "--batch-size", + "8", + "--debug", + ] + + +def test_invalid_command_options(cli_runner): + """Test handling of invalid command options""" + result = cli_runner.invoke( + cli, + [ + "train", + "config.yml", + "--invalid-option", + "value", + ], + ) + assert result.exit_code != 0 + assert "No such option" in result.output + + +def test_required_config_argument(cli_runner): + """Test commands fail properly when config argument is missing""" + result = cli_runner.invoke(cli, ["train"]) + assert result.exit_code != 0 + assert "Missing argument 'CONFIG'" in result.output diff --git a/tests/cli/test_cli_merge_lora.py b/tests/cli/test_cli_merge_lora.py new file mode 100644 index 0000000000..165a64e98c --- /dev/null +++ b/tests/cli/test_cli_merge_lora.py @@ -0,0 +1,56 @@ +"""pytest tests for axolotl CLI merge_lora command.""" +from unittest.mock import patch + +from axolotl.cli.main import cli + + +def test_merge_lora_basic(cli_runner, config_path): + """Test basic merge_lora command""" + with patch("axolotl.cli.merge_lora.do_cli") as mock_do_cli: + result = cli_runner.invoke(cli, ["merge-lora", str(config_path)]) + assert result.exit_code == 0 + + mock_do_cli.assert_called_once() + assert mock_do_cli.call_args.kwargs["config"] == str(config_path) + + +def test_merge_lora_with_dirs(cli_runner, config_path, tmp_path): + """Test merge_lora with custom lora and output directories""" + lora_dir = tmp_path / "lora" + output_dir = tmp_path / "output" + lora_dir.mkdir() + + with patch("axolotl.cli.merge_lora.do_cli") as mock_do_cli: + result = cli_runner.invoke( + cli, + [ + "merge-lora", + str(config_path), + "--lora-model-dir", + str(lora_dir), + "--output-dir", + str(output_dir), + ], + ) + assert result.exit_code == 0 + + mock_do_cli.assert_called_once() + assert mock_do_cli.call_args.kwargs["config"] == str(config_path) + assert mock_do_cli.call_args.kwargs["lora_model_dir"] == str(lora_dir) + assert mock_do_cli.call_args.kwargs["output_dir"] == str(output_dir) + + +def test_merge_lora_nonexistent_config(cli_runner, tmp_path): + """Test merge_lora with nonexistent config""" + config_path = tmp_path / "nonexistent.yml" + result = cli_runner.invoke(cli, ["merge-lora", str(config_path)]) + assert result.exit_code != 0 + + +def test_merge_lora_nonexistent_lora_dir(cli_runner, config_path, tmp_path): + """Test merge_lora with nonexistent lora directory""" + lora_dir = tmp_path / "nonexistent" + result = cli_runner.invoke( + cli, ["merge-lora", str(config_path), "--lora-model-dir", str(lora_dir)] + ) + assert result.exit_code != 0 diff --git a/tests/cli/test_cli_merge_sharded_fsdp_weights.py b/tests/cli/test_cli_merge_sharded_fsdp_weights.py new file mode 100644 index 0000000000..cff0f3b773 --- /dev/null +++ b/tests/cli/test_cli_merge_sharded_fsdp_weights.py @@ -0,0 +1,60 @@ +"""pytest tests for axolotl CLI merge_sharded_fsdp_weights command.""" +# pylint: disable=duplicate-code +from unittest.mock import patch + +from axolotl.cli.main import cli + + +def test_merge_sharded_fsdp_weights_no_accelerate(cli_runner, config_path): + """Test merge_sharded_fsdp_weights command without accelerate""" + with patch("axolotl.cli.merge_sharded_fsdp_weights.do_cli") as mock: + result = cli_runner.invoke( + cli, ["merge-sharded-fsdp-weights", str(config_path), "--no-accelerate"] + ) + + assert mock.called + assert mock.call_args.kwargs["config"] == str(config_path) + assert result.exit_code == 0 + + +def test_merge_sharded_fsdp_weights_with_model_dir(cli_runner, config_path, tmp_path): + """Test merge_sharded_fsdp_weights command with model_dir option""" + model_dir = tmp_path / "model" + model_dir.mkdir() + + with patch("axolotl.cli.merge_sharded_fsdp_weights.do_cli") as mock: + result = cli_runner.invoke( + cli, + [ + "merge-sharded-fsdp-weights", + str(config_path), + "--no-accelerate", + "--model-dir", + str(model_dir), + ], + ) + + assert mock.called + assert mock.call_args.kwargs["config"] == str(config_path) + assert mock.call_args.kwargs["model_dir"] == str(model_dir) + assert result.exit_code == 0 + + +def test_merge_sharded_fsdp_weights_with_save_path(cli_runner, config_path): + """Test merge_sharded_fsdp_weights command with save_path option""" + with patch("axolotl.cli.merge_sharded_fsdp_weights.do_cli") as mock: + result = cli_runner.invoke( + cli, + [ + "merge-sharded-fsdp-weights", + str(config_path), + "--no-accelerate", + "--save-path", + "/path/to/save", + ], + ) + + assert mock.called + assert mock.call_args.kwargs["config"] == str(config_path) + assert mock.call_args.kwargs["save_path"] == "/path/to/save" + assert result.exit_code == 0 diff --git a/tests/cli/test_cli_preprocess.py b/tests/cli/test_cli_preprocess.py new file mode 100644 index 0000000000..4719461aaf --- /dev/null +++ b/tests/cli/test_cli_preprocess.py @@ -0,0 +1,71 @@ +"""pytest tests for axolotl CLI preprocess command.""" +import shutil +from pathlib import Path +from unittest.mock import patch + +import pytest + +from axolotl.cli.main import cli + + +@pytest.fixture(autouse=True) +def cleanup_last_run_prepared(): + yield + + if Path("last_run_prepared").exists(): + shutil.rmtree("last_run_prepared") + + +def test_preprocess_config_not_found(cli_runner): + """Test preprocess fails when config not found""" + result = cli_runner.invoke(cli, ["preprocess", "nonexistent.yml"]) + assert result.exit_code != 0 + + +def test_preprocess_basic(cli_runner, config_path): + """Test basic preprocessing with minimal config""" + with patch("axolotl.cli.preprocess.do_cli") as mock_do_cli: + result = cli_runner.invoke(cli, ["preprocess", str(config_path)]) + assert result.exit_code == 0 + + mock_do_cli.assert_called_once() + assert mock_do_cli.call_args.kwargs["config"] == str(config_path) + assert mock_do_cli.call_args.kwargs["download"] is True + + +def test_preprocess_without_download(cli_runner, config_path): + """Test preprocessing without model download""" + with patch("axolotl.cli.preprocess.do_cli") as mock_do_cli: + result = cli_runner.invoke( + cli, ["preprocess", str(config_path), "--no-download"] + ) + assert result.exit_code == 0 + + mock_do_cli.assert_called_once() + assert mock_do_cli.call_args.kwargs["config"] == str(config_path) + assert mock_do_cli.call_args.kwargs["download"] is False + + +def test_preprocess_custom_path(cli_runner, tmp_path, valid_test_config): + """Test preprocessing with custom dataset path""" + config_path = tmp_path / "config.yml" + custom_path = tmp_path / "custom_prepared" + config_path.write_text(valid_test_config) + + with patch("axolotl.cli.preprocess.do_cli") as mock_do_cli: + result = cli_runner.invoke( + cli, + [ + "preprocess", + str(config_path), + "--dataset-prepared-path", + str(custom_path.absolute()), + ], + ) + assert result.exit_code == 0 + + mock_do_cli.assert_called_once() + assert mock_do_cli.call_args.kwargs["config"] == str(config_path) + assert mock_do_cli.call_args.kwargs["dataset_prepared_path"] == str( + custom_path.absolute() + ) diff --git a/tests/cli/test_cli_shard.py b/tests/cli/test_cli_shard.py new file mode 100644 index 0000000000..505a2a7372 --- /dev/null +++ b/tests/cli/test_cli_shard.py @@ -0,0 +1,76 @@ +"""pytest tests for axolotl CLI shard command.""" +# pylint: disable=duplicate-code +from unittest.mock import patch + +from axolotl.cli.main import cli + + +def test_shard_with_accelerate(cli_runner, config_path): + """Test shard command with accelerate""" + with patch("subprocess.run") as mock: + result = cli_runner.invoke(cli, ["shard", str(config_path), "--accelerate"]) + + assert mock.called + assert mock.call_args.args[0] == [ + "accelerate", + "launch", + "-m", + "axolotl.cli.shard", + str(config_path), + "--debug-num-examples", + "0", + ] + assert mock.call_args.kwargs == {"check": True} + assert result.exit_code == 0 + + +def test_shard_no_accelerate(cli_runner, config_path): + """Test shard command without accelerate""" + with patch("axolotl.cli.shard.do_cli") as mock: + result = cli_runner.invoke(cli, ["shard", str(config_path), "--no-accelerate"]) + + assert mock.called + assert result.exit_code == 0 + + +def test_shard_with_model_dir(cli_runner, config_path, tmp_path): + """Test shard command with model_dir option""" + model_dir = tmp_path / "model" + model_dir.mkdir() + + with patch("axolotl.cli.shard.do_cli") as mock: + result = cli_runner.invoke( + cli, + [ + "shard", + str(config_path), + "--no-accelerate", + "--model-dir", + str(model_dir), + ], + catch_exceptions=False, + ) + + assert mock.called + assert mock.call_args.kwargs["config"] == str(config_path) + assert mock.call_args.kwargs["model_dir"] == str(model_dir) + assert result.exit_code == 0 + + +def test_shard_with_save_dir(cli_runner, config_path): + with patch("axolotl.cli.shard.do_cli") as mock: + result = cli_runner.invoke( + cli, + [ + "shard", + str(config_path), + "--no-accelerate", + "--save-dir", + "/path/to/save", + ], + ) + + assert mock.called + assert mock.call_args.kwargs["config"] == str(config_path) + assert mock.call_args.kwargs["save_dir"] == "/path/to/save" + assert result.exit_code == 0 diff --git a/tests/cli/test_cli_train.py b/tests/cli/test_cli_train.py new file mode 100644 index 0000000000..7f028fb4f2 --- /dev/null +++ b/tests/cli/test_cli_train.py @@ -0,0 +1,98 @@ +"""pytest tests for axolotl CLI train command.""" +from unittest.mock import MagicMock, patch + +from axolotl.cli.main import cli + + +def test_train_cli_validation(cli_runner): + """Test CLI validation""" + # Test missing config file + result = cli_runner.invoke(cli, ["train", "--no-accelerate"]) + assert result.exit_code != 0 + + # Test non-existent config file + result = cli_runner.invoke(cli, ["train", "nonexistent.yml", "--no-accelerate"]) + assert result.exit_code != 0 + assert "Error: Invalid value for 'CONFIG'" in result.output + + +def test_train_basic_execution(cli_runner, tmp_path, valid_test_config): + """Test basic successful execution""" + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + with patch("subprocess.run") as mock: + result = cli_runner.invoke(cli, ["train", str(config_path)]) + + assert mock.called + assert mock.call_args.args[0] == [ + "accelerate", + "launch", + "-m", + "axolotl.cli.train", + str(config_path), + "--debug-num-examples", + "0", + ] + assert mock.call_args.kwargs == {"check": True} + assert result.exit_code == 0 + + +def test_train_basic_execution_no_accelerate(cli_runner, tmp_path, valid_test_config): + """Test basic successful execution""" + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + with patch("axolotl.cli.train.train") as mock_train: + mock_train.return_value = (MagicMock(), MagicMock()) + + result = cli_runner.invoke( + cli, + [ + "train", + str(config_path), + "--learning-rate", + "1e-4", + "--micro-batch-size", + "2", + "--no-accelerate", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_train.assert_called_once() + + +def test_train_cli_overrides(cli_runner, tmp_path, valid_test_config): + """Test CLI arguments properly override config values""" + config_path = tmp_path / "config.yml" + output_dir = tmp_path / "model-out" + + test_config = valid_test_config.replace( + "output_dir: model-out", f"output_dir: {output_dir}" + ) + config_path.write_text(test_config) + + with patch("axolotl.cli.train.train") as mock_train: + mock_train.return_value = (MagicMock(), MagicMock()) + + result = cli_runner.invoke( + cli, + [ + "train", + str(config_path), + "--learning-rate", + "1e-4", + "--micro-batch-size", + "2", + "--no-accelerate", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_train.assert_called_once() + cfg = mock_train.call_args[1]["cfg"] + assert cfg["learning_rate"] == 1e-4 + assert cfg["micro_batch_size"] == 2 diff --git a/tests/cli/test_utils.py b/tests/cli/test_utils.py new file mode 100644 index 0000000000..a593479a41 --- /dev/null +++ b/tests/cli/test_utils.py @@ -0,0 +1,89 @@ +"""pytest tests for axolotl CLI utils.""" +# pylint: disable=redefined-outer-name +import json +from unittest.mock import Mock, patch + +import click +import pytest +import requests + +from axolotl.cli.utils import fetch_from_github + +# Sample GitHub API response +MOCK_TREE_RESPONSE = { + "tree": [ + {"path": "examples/config1.yml", "type": "blob", "sha": "abc123"}, + {"path": "examples/config2.yml", "type": "blob", "sha": "def456"}, + {"path": "other/file.txt", "type": "blob", "sha": "xyz789"}, + ] +} + + +@pytest.fixture +def mock_responses(): + """Mock responses for API and file downloads""" + + def mock_get(url, timeout=None): # pylint: disable=unused-argument + response = Mock() + if "api.github.com" in url: + response.text = json.dumps(MOCK_TREE_RESPONSE) + else: + response.content = b"file content" + return response + + return mock_get + + +def test_fetch_from_github_new_files(tmp_path, mock_responses): + """Test fetching new files""" + with patch("requests.get", mock_responses): + fetch_from_github("examples/", tmp_path) + + # Verify files were created + assert (tmp_path / "config1.yml").exists() + assert (tmp_path / "config2.yml").exists() + assert not (tmp_path / "file.txt").exists() + + +def test_fetch_from_github_unchanged_files(tmp_path, mock_responses): + """Test handling of unchanged files""" + # Create existing file with matching SHA + existing_file = tmp_path / "config1.yml" + existing_file.write_bytes(b"file content") + + with patch("requests.get", mock_responses): + fetch_from_github("examples/", tmp_path) + + # File should not be downloaded again + assert existing_file.read_bytes() == b"file content" + + +def test_fetch_from_github_invalid_prefix(mock_responses): + """Test error handling for invalid directory prefix""" + with patch("requests.get", mock_responses): + with pytest.raises(click.ClickException): + fetch_from_github("nonexistent/", None) + + +def test_fetch_from_github_network_error(): + """Test handling of network errors""" + with patch("requests.get", side_effect=requests.RequestException): + with pytest.raises(requests.RequestException): + fetch_from_github("examples/", None) + + +@pytest.fixture +def integration_test_dir(tmp_path): + """Fixture for integration test directory that cleans up after itself""" + test_dir = tmp_path / "github_downloads" + test_dir.mkdir(parents=True) + yield test_dir + + +def test_fetch_from_github_real(integration_test_dir): + """Test actual GitHub API interaction""" + fetch_from_github("examples/", integration_test_dir) + + # Verify some known files exist + assert (integration_test_dir / "openllama-3b" / "lora.yml").exists() + assert (integration_test_dir / "openllama-3b" / "qlora.yml").exists() diff --git a/tests/test_validation.py b/tests/patched/test_validation.py similarity index 100% rename from tests/test_validation.py rename to tests/patched/test_validation.py diff --git a/tests/test_data.py b/tests/test_data.py index 9d7f5a0412..e156e1f3c5 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1,5 +1,5 @@ """ -test module for the axolotl.utis.data module +test module for the axolotl.utils.data module """ import unittest From 2f3ebbc44ff55bbb0b78419624adbf3ef6343a64 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Thu, 5 Dec 2024 22:12:40 -0500 Subject: [PATCH 0310/1405] auto-versioning and adding axolotl.__version__ (#2127) * auto-versioning and adding axolotl.__version__ * removing file meant for codecov PR * adding dynamic dependencies, project metadata * extras/optional-dependencies are dynamic too --------- Co-authored-by: Dan Saunders Co-authored-by: Wing Lian --- pyproject.toml | 16 ++++++++++++++++ setup.py | 4 ++-- src/axolotl/__init__.py | 8 ++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..815e243274 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools>=64", "wheel", "setuptools_scm>=8"] +build-backend = "setuptools.build_meta" + +[project] +name = "axolotl" +dynamic = ["version", "dependencies", "optional-dependencies"] +description = "LLM Trainer" +readme = "README.md" +requires-python = ">=3.10" + +[project.urls] +Homepage = "https://axolotl-ai-cloud.github.io/axolotl/" +Repository = "https://github.com/axolotl-ai-cloud/axolotl.git" + +[tool.setuptools_scm] diff --git a/setup.py b/setup.py index 336da98f4b..ac2fd9d421 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ """setup.py for axolotl""" - import platform import re from importlib.metadata import PackageNotFoundError, version @@ -96,7 +95,8 @@ def parse_requirements(): setup( name="axolotl", - version="0.5.2", + use_scm_version=True, + setup_requires=["setuptools_scm"], description="LLM Trainer", long_description="Axolotl is a tool designed to streamline the fine-tuning of various AI models, offering support for multiple configurations and architectures.", package_dir={"": "src"}, diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index e69de29bb2..e555ece480 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -0,0 +1,8 @@ +"""Axolotl - Train and fine-tune large language models""" + +try: + from importlib.metadata import version + + __version__ = version("axolotl") +except ImportError: + __version__ = "unknown" From 5726141c4eaeb61433f4da76fee1f023d97573f1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 5 Dec 2024 22:37:19 -0500 Subject: [PATCH 0311/1405] remove accidentally included symlink (#2131) --- outputs | 1 - 1 file changed, 1 deletion(-) delete mode 120000 outputs diff --git a/outputs b/outputs deleted file mode 120000 index be3c4a823f..0000000000 --- a/outputs +++ /dev/null @@ -1 +0,0 @@ -/workspace/data/axolotl-artifacts \ No newline at end of file From 6b3058b2dc9d45f38e79b293604e6391ae88b48d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 6 Dec 2024 09:08:55 -0500 Subject: [PATCH 0312/1405] upgrade bnb 0.45.0 and peft 0.14.0 (#2126) * upgrade bnb to lastest release * update peft to working supporting commit * bump to latest release of peft==0.14.0 --- requirements.txt | 4 ++-- .../utils/config/models/input/v0_4_1/__init__.py | 13 ------------- tests/e2e/multigpu/test_llama.py | 3 --- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/requirements.txt b/requirements.txt index 456c63ca51..d100139ca4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 -peft==0.13.2 +peft==0.14.0 transformers==4.46.3 tokenizers>=0.20.1 -bitsandbytes==0.44.1 +bitsandbytes==0.45.0 accelerate==1.1.0 datasets==3.1.0 deepspeed==0.15.4 diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index c9170b7a84..24ea62c77f 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -1521,19 +1521,6 @@ def check_sample_packing_w_sdpa_bf16(cls, data): return data - @model_validator(mode="before") - @classmethod - def check_hopper_8bit_lora(cls, data): - is_sm_90: bool = ( - data["capabilities"] - and data["capabilities"].get("compute_capability") == "sm_90" - ) - if data.get("adapter") and data.get("load_in_8bit") and is_sm_90: - # see https://github.com/bitsandbytes-foundation/bitsandbytes/issues/538#issuecomment-2262945464 - raise ValueError("8-bit LoRA is not supported on Hopper GPUs") - - return data - @model_validator(mode="before") @classmethod def check_fsdp_deepspeed(cls, data): diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index d8dcf3118a..c9938a1559 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -14,8 +14,6 @@ from axolotl.utils.dict import DictDefault -from ..utils import is_hopper - LOG = logging.getLogger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" @@ -144,7 +142,6 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): ] ) - @pytest.mark.skipif(is_hopper(), reason="h100 doesn't support 8-bit lora") def test_dpo_lora_ddp(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( From 08fa13317771a26f11b49c7df7dba915057cf2f2 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 6 Dec 2024 10:19:54 -0500 Subject: [PATCH 0313/1405] Fix broken CLI; remove duplicate metadata from setup.py (#2136) * Fix broken CLI; remove duplicate metadata from setup.py * Adding tests.yml CLI check * updating * remove test with requests to github due to rate limiting --------- Co-authored-by: Dan Saunders --- .github/workflows/tests-nightly.yml | 4 ++++ .github/workflows/tests.yml | 8 ++++++++ pyproject.toml | 3 +++ setup.py | 6 ------ tests/cli/test_utils.py | 17 ----------------- 5 files changed, 15 insertions(+), 23 deletions(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index f81f711de4..8b7561bd13 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -58,6 +58,10 @@ jobs: python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt + - name: Ensure axolotl CLI was installed + run: | + axolotl --help + - name: Run tests run: | pytest -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 89f19ca9c3..690047bb14 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -77,6 +77,10 @@ jobs: python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt + - name: Ensure axolotl CLI was installed + run: | + axolotl --help + - name: Run tests run: | pytest -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ @@ -122,6 +126,10 @@ jobs: pip3 install dist/axolotl*.tar.gz pip3 install -r requirements-dev.txt -r requirements-tests.txt + - name: Ensure axolotl CLI was installed + run: | + axolotl --help + - name: Run tests run: | pytest -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ diff --git a/pyproject.toml b/pyproject.toml index 815e243274..6805e2e48f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,9 @@ description = "LLM Trainer" readme = "README.md" requires-python = ">=3.10" +[project.scripts] +axolotl = "axolotl.cli.main:main" + [project.urls] Homepage = "https://axolotl-ai-cloud.github.io/axolotl/" Repository = "https://github.com/axolotl-ai-cloud/axolotl.git" diff --git a/setup.py b/setup.py index ac2fd9d421..8d7173f28c 100644 --- a/setup.py +++ b/setup.py @@ -92,13 +92,7 @@ def parse_requirements(): install_requires, dependency_links = parse_requirements() - setup( - name="axolotl", - use_scm_version=True, - setup_requires=["setuptools_scm"], - description="LLM Trainer", - long_description="Axolotl is a tool designed to streamline the fine-tuning of various AI models, offering support for multiple configurations and architectures.", package_dir={"": "src"}, packages=find_packages("src"), install_requires=install_requires, diff --git a/tests/cli/test_utils.py b/tests/cli/test_utils.py index a593479a41..b88e4ac729 100644 --- a/tests/cli/test_utils.py +++ b/tests/cli/test_utils.py @@ -70,20 +70,3 @@ def test_fetch_from_github_network_error(): with patch("requests.get", side_effect=requests.RequestException): with pytest.raises(requests.RequestException): fetch_from_github("examples/", None) - - -@pytest.fixture -def integration_test_dir(tmp_path): - """Fixture for integration test directory that cleans up after itself""" - test_dir = tmp_path / "github_downloads" - test_dir.mkdir(parents=True) - yield test_dir - - -def test_fetch_from_github_real(integration_test_dir): - """Test actual GitHub API interaction""" - fetch_from_github("examples/", integration_test_dir) - - # Verify some known files exist - assert (integration_test_dir / "openllama-3b" / "lora.yml").exists() - assert (integration_test_dir / "openllama-3b" / "qlora.yml").exists() From 5e9fa33f3d15e998176b9bbffe27bdf1d64cb3e8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 6 Dec 2024 10:20:20 -0500 Subject: [PATCH 0314/1405] reduce test concurrency to avoid HF rate limiting, test suite parity (#2128) * reduce test concurrency to avoid HF rate limiting, test suite parity * make val_set_size smaller to speed up e2e tests * more retries for pytest fixture downloads * val_set_size was too small * move retry_on_request_exceptions to data utils and add retry strategy * pre-download ultrafeedback as a test fixture * refactor download retry into it's own fn * don't import from data utils * use retry mechanism now for fixtures --- .github/workflows/tests-nightly.yml | 7 +++ .github/workflows/tests.yml | 9 +++ src/axolotl/utils/data/sft.py | 29 ++------- src/axolotl/utils/data/utils.py | 46 +++++++++++++- tests/conftest.py | 60 +++++++++++++++---- tests/e2e/patched/test_4d_multipack_llama.py | 4 +- tests/e2e/patched/test_falcon_samplepack.py | 4 +- tests/e2e/patched/test_fused_llama.py | 2 +- .../e2e/patched/test_lora_llama_multipack.py | 2 +- tests/e2e/patched/test_mistral_samplepack.py | 4 +- tests/e2e/patched/test_mixtral_samplepack.py | 4 +- tests/e2e/patched/test_phi_multipack.py | 2 +- 12 files changed, 126 insertions(+), 47 deletions(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 8b7561bd13..f3e5530cb8 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -23,9 +23,15 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false + max-parallel: 2 matrix: python_version: ["3.10", "3.11"] pytorch_version: ["2.3.1", "2.4.1", "2.5.1"] + exclude: + - python_version: "3.10" + pytorch_version: "2.4.1" + - python_version: "3.10" + pytorch_version: "2.5.1" timeout-minutes: 20 steps: @@ -55,6 +61,7 @@ jobs: pip3 install --upgrade pip pip3 install --upgrade packaging pip3 install -U -e . + python scripts/unsloth_install.py | sh python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 690047bb14..3e1a4fe924 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,9 +45,15 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false + max-parallel: 2 matrix: python_version: ["3.10", "3.11"] pytorch_version: ["2.3.1", "2.4.1", "2.5.1"] + exclude: + - python_version: "3.10" + pytorch_version: "2.4.1" + - python_version: "3.10" + pytorch_version: "2.5.1" timeout-minutes: 20 steps: @@ -95,6 +101,7 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false + max-parallel: 1 matrix: python_version: ["3.11"] pytorch_version: ["2.4.1", "2.5.1"] @@ -124,6 +131,8 @@ jobs: pip3 show torch python3 setup.py sdist pip3 install dist/axolotl*.tar.gz + python scripts/unsloth_install.py | sh + python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt - name: Ensure axolotl CLI was installed diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 4ed16e3582..f56fe8f38c 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -2,11 +2,9 @@ import functools import logging -import time from pathlib import Path from typing import List, Optional, Tuple, Union -import requests from datasets import ( Dataset, DatasetDict, @@ -44,7 +42,11 @@ UnsupportedPrompter, ) from axolotl.utils.data.pretraining import wrap_pretraining_dataset -from axolotl.utils.data.utils import deduplicate_and_log_datasets, md5 +from axolotl.utils.data.utils import ( + deduplicate_and_log_datasets, + md5, + retry_on_request_exceptions, +) from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import is_local_main_process, zero_first from axolotl.utils.trainer import ( @@ -55,27 +57,6 @@ LOG = logging.getLogger("axolotl") -def retry_on_request_exceptions(max_retries=3, delay=1): - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements - for attempt in range(max_retries): - try: - return func(*args, **kwargs) - except ( - requests.exceptions.ReadTimeout, - requests.exceptions.ConnectionError, - ) as exc: - if attempt < max_retries - 1: - time.sleep(delay) - else: - raise exc - - return wrapper - - return decorator - - @retry_on_request_exceptions(max_retries=3, delay=5) def prepare_dataset(cfg, tokenizer, processor=None): prompters = [] diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index 56bcddd8eb..657cbb77c3 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -1,13 +1,57 @@ """data handling helpers""" - +import functools import hashlib import logging +import time +from enum import Enum +import huggingface_hub +import requests from datasets import Dataset LOG = logging.getLogger("axolotl") +class RetryStrategy(Enum): + """ + Enum for retry strategies. + """ + + CONSTANT = 1 + LINEAR = 2 + EXPONENTIAL = 3 + + +def retry_on_request_exceptions( + max_retries=3, delay=1, retry_strategy: RetryStrategy = RetryStrategy.LINEAR +): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements + for attempt in range(max_retries): + try: + return func(*args, **kwargs) + except ( + requests.exceptions.ReadTimeout, + requests.exceptions.ConnectionError, + huggingface_hub.errors.HfHubHTTPError, + ) as exc: + if attempt < max_retries - 1: + if retry_strategy == RetryStrategy.EXPONENTIAL: + step_delay = delay * 2**attempt + elif retry_strategy == RetryStrategy.LINEAR: + step_delay = delay * (attempt + 1) + else: + step_delay = delay # Use constant delay. + time.sleep(step_delay) + else: + raise exc + + return wrapper + + return decorator + + def md5(to_hash: str, encoding: str = "utf-8") -> str: try: return hashlib.md5(to_hash.encode(encoding), usedforsecurity=False).hexdigest() diff --git a/tests/conftest.py b/tests/conftest.py index 3a20bbfdd9..a775216fc0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,47 +1,77 @@ """ shared pytest fixtures """ +import functools import shutil import tempfile +import time import pytest +import requests from huggingface_hub import snapshot_download +def retry_on_request_exceptions(max_retries=3, delay=1): + # pylint: disable=duplicate-code + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements + for attempt in range(max_retries): + try: + return func(*args, **kwargs) + except ( + requests.exceptions.ReadTimeout, + requests.exceptions.ConnectionError, + ) as exc: + if attempt < max_retries - 1: + time.sleep(delay) + else: + raise exc + + return wrapper + + return decorator + + +@retry_on_request_exceptions(max_retries=3, delay=5) +def snapshot_download_w_retry(*args, **kwargs): + return snapshot_download(*args, **kwargs) + + @pytest.fixture(scope="session", autouse=True) def download_smollm2_135m_model(): # download the model - snapshot_download("HuggingFaceTB/SmolLM2-135M") + snapshot_download_w_retry("HuggingFaceTB/SmolLM2-135M") @pytest.fixture(scope="session", autouse=True) def download_llama_68m_random_model(): # download the model - snapshot_download("JackFram/llama-68m") + snapshot_download_w_retry("JackFram/llama-68m") @pytest.fixture(scope="session", autouse=True) def download_qwen_2_5_half_billion_model(): # download the model - snapshot_download("Qwen/Qwen2.5-0.5B") + snapshot_download_w_retry("Qwen/Qwen2.5-0.5B") @pytest.fixture(scope="session", autouse=True) def download_tatsu_lab_alpaca_dataset(): # download the dataset - snapshot_download("tatsu-lab/alpaca", repo_type="dataset") + snapshot_download_w_retry("tatsu-lab/alpaca", repo_type="dataset") @pytest.fixture(scope="session", autouse=True) def download_mhenrichsen_alpaca_2k_dataset(): # download the dataset - snapshot_download("mhenrichsen/alpaca_2k_test", repo_type="dataset") + snapshot_download_w_retry("mhenrichsen/alpaca_2k_test", repo_type="dataset") @pytest.fixture(scope="session", autouse=True) def download_mhenrichsen_alpaca_2k_w_revision_dataset(): # download the dataset - snapshot_download( + snapshot_download_w_retry( "mhenrichsen/alpaca_2k_test", repo_type="dataset", revision="d05c1cb" ) @@ -49,21 +79,29 @@ def download_mhenrichsen_alpaca_2k_w_revision_dataset(): @pytest.fixture(scope="session", autouse=True) def download_mlabonne_finetome_100k_dataset(): # download the dataset - snapshot_download("mlabonne/FineTome-100k", repo_type="dataset") + snapshot_download_w_retry("mlabonne/FineTome-100k", repo_type="dataset") -@pytest.fixture +@pytest.fixture(scope="session", autouse=True) def download_argilla_distilabel_capybara_dpo_7k_binarized_dataset(): # download the dataset - snapshot_download( + snapshot_download_w_retry( "argilla/distilabel-capybara-dpo-7k-binarized", repo_type="dataset" ) -@pytest.fixture +@pytest.fixture(scope="session", autouse=True) +def download_argilla_ultrafeedback_binarized_preferences_cleaned_dataset(): + # download the dataset + snapshot_download_w_retry( + "argilla/ultrafeedback-binarized-preferences-cleaned", repo_type="dataset" + ) + + +@pytest.fixture(scope="session", autouse=True) def download_arcee_ai_distilabel_intel_orca_dpo_pairs_dataset(): # download the dataset - snapshot_download( + snapshot_download_w_retry( "arcee-ai/distilabel-intel-orca-dpo-pairs-binarized", repo_type="dataset" ) diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index a26c5d9620..b0ada92304 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -42,7 +42,7 @@ def test_sdp_lora_packing(self, temp_dir): "lora_dropout": 0.05, "lora_target_linear": True, "sequence_len": 1024, - "val_set_size": 0.1, + "val_set_size": 0.02, "datasets": [ { "path": "mhenrichsen/alpaca_2k_test", @@ -86,7 +86,7 @@ def test_torch_lora_packing(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "datasets": [ { "path": "mhenrichsen/alpaca_2k_test", diff --git a/tests/e2e/patched/test_falcon_samplepack.py b/tests/e2e/patched/test_falcon_samplepack.py index ae6a497391..d9d7151032 100644 --- a/tests/e2e/patched/test_falcon_samplepack.py +++ b/tests/e2e/patched/test_falcon_samplepack.py @@ -40,7 +40,7 @@ def test_qlora(self, temp_dir): "lora_dropout": 0.1, "lora_target_linear": True, "lora_modules_to_save": ["word_embeddings", "lm_head"], - "val_set_size": 0.1, + "val_set_size": 0.05, "special_tokens": { "bos_token": "<|endoftext|>", "pad_token": "<|endoftext|>", @@ -80,7 +80,7 @@ def test_ft(self, temp_dir): "flash_attention": True, "sample_packing": True, "sequence_len": 2048, - "val_set_size": 0.1, + "val_set_size": 0.05, "special_tokens": { "bos_token": "<|endoftext|>", "pad_token": "<|endoftext|>", diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index de1195c368..e662e340b6 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -38,7 +38,7 @@ def test_fft_packing(self, temp_dir): "flash_attn_fuse_mlp": True, "sample_packing": True, "sequence_len": 1024, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "unk_token": "", "bos_token": "", diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index 5dbf146542..be2f133fb0 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -98,7 +98,7 @@ def test_lora_gptq_packed(self, temp_dir): "lora_alpha": 64, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "unk_token": "", "bos_token": "", diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index a56c530b21..6685fb9d57 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -39,7 +39,7 @@ def test_lora_packing(self, temp_dir): "lora_alpha": 64, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.05, "special_tokens": { "unk_token": "", "bos_token": "", @@ -80,7 +80,7 @@ def test_ft_packing(self, temp_dir): "flash_attention": True, "sample_packing": True, "sequence_len": 1024, - "val_set_size": 0.1, + "val_set_size": 0.05, "special_tokens": { "unk_token": "", "bos_token": "", diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index 8baba03073..684baaaff8 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -40,7 +40,7 @@ def test_qlora(self, temp_dir): "lora_alpha": 32, "lora_dropout": 0.1, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.05, "special_tokens": {}, "datasets": [ { @@ -78,7 +78,7 @@ def test_ft(self, temp_dir): "flash_attention": True, "sample_packing": True, "sequence_len": 2048, - "val_set_size": 0.1, + "val_set_size": 0.05, "special_tokens": {}, "datasets": [ { diff --git a/tests/e2e/patched/test_phi_multipack.py b/tests/e2e/patched/test_phi_multipack.py index 5f30453c18..7b5bf92dfa 100644 --- a/tests/e2e/patched/test_phi_multipack.py +++ b/tests/e2e/patched/test_phi_multipack.py @@ -38,7 +38,7 @@ def test_ft_packed(self, temp_dir): "pad_to_sequence_len": True, "load_in_8bit": False, "adapter": None, - "val_set_size": 0.1, + "val_set_size": 0.05, "special_tokens": { "pad_token": "<|endoftext|>", }, From f9a7748bd80291b53f72920586875666fbc4bdb4 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Sat, 7 Dec 2024 15:32:32 +0530 Subject: [PATCH 0315/1405] Fix llama type model check (#2142) [skip ci] --- src/axolotl/utils/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 468bd6e7f8..30ba53ad25 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -153,7 +153,7 @@ def normalize_config(cfg): cfg.is_llama_derived_model = ( ( hasattr(model_config, "model_type") - and model_config.model_type == ["llama", "mllama_text_model"] + and model_config.model_type in ["llama", "mllama_text_model"] ) or cfg.is_llama_derived_model or "llama" in cfg.base_model.lower() From 743ba62bd5104cdc45a54566b605fedc8073fef5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 7 Dec 2024 05:03:01 -0500 Subject: [PATCH 0316/1405] Transformers 4.47.0 (#2138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bump transformers and trl * fix: update trainer.log signature * fix trl trainer.log interfaces * broken 🦥 with latest transformers * skip parent, call grandparent - yeah, super janky * update HF HUB env var and fix reward trainer log since it doesn't directly override log * also bump accelerate * patches for llama ga * detab the code to check * fix whitespace for patch check * play nicely with CI tests since we patch everytime * fix pop default in case it doesn't exist * more tweaks to make patches nicer in CI * fix detab for when there are possibly multiple patches --------- Co-authored-by: NanoCode012 --- docker/Dockerfile-cloud | 2 +- docker/Dockerfile-cloud-no-tmux | 2 +- requirements.txt | 6 +- src/axolotl/cli/__init__.py | 2 +- src/axolotl/core/trainer_builder.py | 87 +++++++- src/axolotl/monkeypatch/trainer_grad_accum.py | 207 ++++++++++++++++++ src/axolotl/monkeypatch/unsloth_.py | 17 +- src/axolotl/utils/models.py | 9 + tests/e2e/patched/test_unsloth_qlora.py | 9 + tests/patched/test_llama_trainer_ga.py | 25 +++ 10 files changed, 347 insertions(+), 19 deletions(-) create mode 100644 src/axolotl/monkeypatch/trainer_grad_accum.py create mode 100644 tests/patched/test_llama_trainer_ga.py diff --git a/docker/Dockerfile-cloud b/docker/Dockerfile-cloud index d7e3277d2f..c8249cb79c 100644 --- a/docker/Dockerfile-cloud +++ b/docker/Dockerfile-cloud @@ -2,7 +2,7 @@ ARG BASE_TAG=main FROM axolotlai/axolotl:$BASE_TAG ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" -ENV HUGGINGFACE_HUB_CACHE="/workspace/data/huggingface-cache/hub" +ENV HF_HUB_CACHE="/workspace/data/huggingface-cache/hub" ENV HF_HOME="/workspace/data/huggingface-cache/hub" ENV HF_HUB_ENABLE_HF_TRANSFER="1" diff --git a/docker/Dockerfile-cloud-no-tmux b/docker/Dockerfile-cloud-no-tmux index 6dfea46779..1650631050 100644 --- a/docker/Dockerfile-cloud-no-tmux +++ b/docker/Dockerfile-cloud-no-tmux @@ -2,7 +2,7 @@ ARG BASE_TAG=main FROM axolotlai/axolotl:$BASE_TAG ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" -ENV HUGGINGFACE_HUB_CACHE="/workspace/data/huggingface-cache/hub" +ENV HF_HUB_CACHE="/workspace/data/huggingface-cache/hub" ENV HF_HOME="/workspace/data/huggingface-cache/hub" ENV HF_HUB_ENABLE_HF_TRANSFER="1" diff --git a/requirements.txt b/requirements.txt index d100139ca4..864beb9b13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.14.0 -transformers==4.46.3 +transformers==4.47.0 tokenizers>=0.20.1 bitsandbytes==0.45.0 -accelerate==1.1.0 +accelerate==1.2.0 datasets==3.1.0 deepspeed==0.15.4 pydantic==2.6.3 @@ -42,7 +42,7 @@ s3fs>=2024.5.0 gcsfs>=2024.5.0 # adlfs -trl==0.12.0 +trl==0.12.1 zstandard==0.22.0 fastcore diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index e8ef862854..d07b10ce3d 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -442,7 +442,7 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs): "compute_capability": gpu_version, }, env_capabilities={ - "torch_version": str(torch.__version__).split("+", maxsplit=1)[0] + "torch_version": str(torch.__version__).split("+", maxsplit=1)[0], }, ) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 93384189e9..baac94da80 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -957,13 +957,15 @@ def create_accelerator_and_postprocess(self): return res - def log(self, logs: Dict[str, float]) -> None: + def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: """ Log `logs` on the various objects watching training, including stored metrics. Args: logs (`Dict[str, float]`): The values to log. + start_time (`Optional[float]`): + The start of training. """ # logs either has 'loss' or 'eval_loss' train_eval = "train" if "loss" in logs else "eval" @@ -971,7 +973,7 @@ def log(self, logs: Dict[str, float]) -> None: for key, metrics in self._stored_metrics[train_eval].items(): logs[key] = torch.tensor(metrics).mean().item() del self._stored_metrics[train_eval] - return super().log(logs) + return super().log(logs, start_time) def store_metrics( self, metrics: Dict[str, float], train_eval: Literal["train", "eval"] = "train" @@ -1155,6 +1157,18 @@ def training_step( torch.cuda.empty_cache() return loss + def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: + # TODO remove once trl supports the updated to the Trainer.log method + # logs either has 'loss' or 'eval_loss' + train_eval = "train" if "loss" in logs else "eval" + # Add averaged stored metrics to logs + for key, metrics in self._stored_metrics[train_eval].items(): + logs[key] = torch.tensor(metrics).mean().item() + del self._stored_metrics[train_eval] + return super(DPOTrainer, self).log( # pylint: disable=bad-super-call + logs, start_time + ) + class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): """ @@ -1163,6 +1177,18 @@ class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): tag_names = ["axolotl", "orpo"] + def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: + # TODO remove once trl supports the updated to the Trainer.log method + # logs either has 'loss' or 'eval_loss' + train_eval = "train" if "loss" in logs else "eval" + # Add averaged stored metrics to logs + for key, metrics in self._stored_metrics[train_eval].items(): + logs[key] = torch.tensor(metrics).mean().item() + del self._stored_metrics[train_eval] + return super(ORPOTrainer, self).log( # pylint: disable=bad-super-call + logs, start_time + ) + class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): """ @@ -1171,6 +1197,45 @@ class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): tag_names = ["axolotl", "kto"] + def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: + # TODO remove once trl supports the updated to the Trainer.log method + # logs either has 'loss' or 'eval_loss' + train_eval = "train" if "loss" in logs else "eval" + # train metrics should have no prefix, eval should have 'eval_' + prefix = "eval_" if train_eval == "eval" else "" + # accumulate average metrics from sums and lengths + for split in ["chosen", "rejected"]: + if f"count/{split}" in self._stored_metrics[train_eval]: + count_sum = ( + torch.Tensor(self._stored_metrics[train_eval][f"count/{split}"]) + .sum() + .item() + ) + for metric in ["rewards", "logps", "logits"]: + logs[f"{prefix}{metric}/{split}"] = ( + torch.Tensor( + self._stored_metrics[train_eval][f"{metric}/{split}_sum"] + ) + .sum() + .item() + / count_sum + ) + # delete obsolete metric + del self._stored_metrics[train_eval][f"{metric}/{split}_sum"] + del self._stored_metrics[train_eval][f"count/{split}"] + # calculate reward margin + if f"{prefix}rewards/chosen" in logs and f"{prefix}rewards/rejected" in logs: + logs[f"{prefix}rewards/margins"] = ( + logs[f"{prefix}rewards/chosen"] - logs[f"{prefix}rewards/rejected"] + ) + # Add averaged stored metrics to logs + for key, metrics in self._stored_metrics[train_eval].items(): + logs[f"{prefix}{key}"] = torch.Tensor(metrics).mean().item() + del self._stored_metrics[train_eval] + return super(KTOTrainer, self).log( # pylint: disable=bad-super-call + logs, start_time + ) + class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): """ @@ -1179,6 +1244,18 @@ class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): tag_names = ["axolotl", "cpo"] + def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: + # TODO remove once trl supports the updated to the Trainer.log method + # logs either has 'loss' or 'eval_loss' + train_eval = "train" if "loss" in logs else "eval" + # Add averaged stored metrics to logs + for key, metrics in self._stored_metrics[train_eval].items(): + logs[key] = torch.tensor(metrics).mean().item() + del self._stored_metrics[train_eval] + return super(CPOTrainer, self).log( # pylint: disable=bad-super-call + logs, start_time + ) + class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): """ @@ -1187,6 +1264,12 @@ class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): tag_names = ["axolotl", "reward"] + def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: + # TODO remove once trl supports the updated to the Trainer.log method + return super(RewardTrainer, self).log( # pylint: disable=bad-super-call + logs, start_time + ) + class TrainerBuilderBase(abc.ABC): """ diff --git a/src/axolotl/monkeypatch/trainer_grad_accum.py b/src/axolotl/monkeypatch/trainer_grad_accum.py new file mode 100644 index 0000000000..5ee90f91ae --- /dev/null +++ b/src/axolotl/monkeypatch/trainer_grad_accum.py @@ -0,0 +1,207 @@ +""" +fix for FSDP gradient accumulation +see https://github.com/huggingface/transformers/pull/35128 +""" +import inspect + +from accelerate.logging import get_logger +from transformers import LlamaForCausalLM +from transformers.trainer import Trainer + +from axolotl.monkeypatch.unsloth_ import detab_code + +LOG = get_logger("axolotl.monkeypatch.trainer_grad_accum") + +ORIGINAL_CONTEXT_CODE = """ + with self.compute_loss_context_manager(): + if self.model_accepts_loss_kwargs: + loss = self.compute_loss(model, inputs) + else: + loss = self.compute_loss(model, inputs, num_items_in_batch=num_items_in_batch) +""" + +PATCHED_CONTEXT_CODE = """ + with self.compute_loss_context_manager(): + if self.model_accepts_loss_kwargs: + loss = self.compute_loss(model, inputs, num_items_in_batch=num_items_in_batch) + else: + loss = self.compute_loss(model, inputs) +""" + +ORIGINAL_LLAMA_FCLM_CODE = """ + output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions + output_hidden_states = ( + output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states + ) + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs[0] + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + logits = self.lm_head(hidden_states[:, -num_logits_to_keep:, :]) + + loss = None + if labels is not None: + loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.vocab_size, **kwargs) +""" + +PATCHED_LLAMA_FCLM_CODE = """ + output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions + output_hidden_states = ( + output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states + ) + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + # remove num_items_in_batch otherwise self.model attempts to pass it to flash_attention + num_items_in_batch = kwargs.pop("num_items_in_batch", None) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + **kwargs, + ) + hidden_states = outputs[0] + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + logits = self.lm_head(hidden_states[:, -num_logits_to_keep:, :]) + + loss = None + if labels is not None: + loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.vocab_size, num_items_in_batch=num_items_in_batch, **kwargs) +""" + + +def get_training_step_code() -> str: + training_step = inspect.getsource( + Trainer.training_step # pylint: disable=protected-access + ) + return training_step + + +def check_training_step_is_patchable() -> bool: + training_step = get_training_step_code() + training_step, _ = detab_code(training_step) + return ORIGINAL_CONTEXT_CODE in training_step + + +def patch_training_step_for_ga(): + """ + monkeypatch for fixing the training loop for gradient accumulation + """ + + try: + training_step = get_training_step_code() + except OSError: + return + Trainer._original_training_step = training_step # pylint: disable=protected-access + training_step, _ = detab_code(training_step) + if ORIGINAL_CONTEXT_CODE not in training_step: + return + # assert ( + # ORIGINAL_CONTEXT_CODE in training_step + # ), "Original training_step code not found" + + training_step = training_step.replace(ORIGINAL_CONTEXT_CODE, PATCHED_CONTEXT_CODE) + training_step = training_step.replace( + "def training_step(", + "def _fixed_training_step(", + 1, + ) + + # load imports necessary + import transformers.trainer + + items_to_import = [] + for item in dir(transformers.trainer): + if item in training_step: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + "from transformers.trainer import (" + + ", ".join(x for x in items_to_import) + + ")", + globals(), + ) + exec(training_step, globals()) # pylint: disable=exec-used # nosec B102 + LOG.info("patching training_step", main_process_only=True) + Trainer.training_step = ( # pylint: disable=protected-access + _fixed_training_step # pylint: disable=undefined-variable # noqa: F821 + ) + + +def get_model_forward_code() -> str: + forward = inspect.getsource( + LlamaForCausalLM.forward # pylint: disable=protected-access + ) + return forward + + +def check_forward_is_patchable() -> bool: + forward = get_model_forward_code() + forward, _ = detab_code(forward) + return ORIGINAL_LLAMA_FCLM_CODE in forward + + +def patch_forward_for_ga(): + """ + monkeypatch for fixing the training loop for gradient accumulation + """ + + try: + forward = get_model_forward_code() + except OSError: + return + LlamaForCausalLM._original_forward = forward # pylint: disable=protected-access + forward, _ = detab_code(forward) + if ORIGINAL_LLAMA_FCLM_CODE not in forward: + return + # assert ORIGINAL_LLAMA_FCLM_CODE in forward, "Original forward code not found" + + forward = forward.replace(ORIGINAL_LLAMA_FCLM_CODE, PATCHED_LLAMA_FCLM_CODE) + forward = forward.replace( + "def forward(", + "def _fixed_forward(", + 1, + ) + + # load imports necessary + import transformers.models.llama.modeling_llama + + items_to_import = [] + for item in dir(transformers.models.llama.modeling_llama): + if item in forward: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + "from transformers.models.llama.modeling_llama import (" + + ", ".join(x for x in items_to_import) + + ")", + globals(), + ) + exec(forward, globals()) # pylint: disable=exec-used # nosec B102 + LOG.info("patching forward", main_process_only=True) + LlamaForCausalLM.forward = ( # pylint: disable=protected-access + _fixed_forward # pylint: disable=undefined-variable # noqa: F821 + ) diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py index 38bbdc88fb..7358803ba1 100644 --- a/src/axolotl/monkeypatch/unsloth_.py +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -9,10 +9,7 @@ from accelerate.logging import get_logger from peft import PeftModelForCausalLM from torch import nn -from transformers.models.llama.modeling_llama import ( - LlamaFlashAttention2, - LlamaForCausalLM, -) +from transformers.models.llama.modeling_llama import LlamaFlashAttention2 LOG = get_logger("axolotl.monkeypatch.unsloth") @@ -55,11 +52,6 @@ def original_apply_o(self, hidden_states): return attn_output -def get_forward_code() -> str: - forward = inspect.getsource(LlamaForCausalLM.forward) - return forward - - def get_self_attn_code() -> str: forward = inspect.getsource(LlamaFlashAttention2.forward) return forward @@ -102,8 +94,11 @@ def UnslothForCausalLMLoss( # pylint: disable=invalid-name def detab_code(code: str) -> Tuple[str, str]: - spaces = re.match(r"([\s\t]{1,})", code).group(0) - code = re.sub(r"^" + spaces, "", code, flags=re.MULTILINE) + try: + spaces = re.match(r"([\s\t]{1,})", code).group(0) + code = re.sub(r"^" + spaces, "", code, flags=re.MULTILINE) + except AttributeError: + return code, "" return code, spaces diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 3e120cca60..f2ee93c3c7 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -386,6 +386,15 @@ def apply_patches(self) -> None: if self.cfg.flash_attention: self.patch_attention() + if self.cfg.model_config_type == "llama": + from axolotl.monkeypatch.trainer_grad_accum import ( + patch_forward_for_ga, + patch_training_step_for_ga, + ) + + patch_forward_for_ga() + patch_training_step_for_ga() + if self.cfg.sample_packing and self.cfg.s2_attention: raise ValueError( "Received `sample_packing=true` and `s2_attention=true`; however, \ diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index 8e0d03380f..b58406185a 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -36,6 +36,9 @@ def test_unsloth_llama_qlora_fa2(self, temp_dir, sample_packing): "sequence_len": 1024, "sample_packing": sample_packing, "flash_attention": True, + "unsloth_lora_mlp": True, + "unsloth_lora_qkv": True, + "unsloth_lora_o": True, "load_in_4bit": True, "adapter": "qlora", "lora_r": 16, @@ -82,6 +85,9 @@ def test_unsloth_llama_qlora_unpacked(self, temp_dir): { "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 1024, + "unsloth_lora_mlp": True, + "unsloth_lora_qkv": True, + "unsloth_lora_o": True, "sample_packing": False, "load_in_4bit": True, "adapter": "qlora", @@ -133,6 +139,9 @@ def test_unsloth_llama_qlora_unpacked_no_fa2_fp16(self, temp_dir, sdp_attention) { "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 1024, + "unsloth_lora_mlp": True, + "unsloth_lora_qkv": True, + "unsloth_lora_o": True, "sample_packing": False, "load_in_4bit": True, "adapter": "qlora", diff --git a/tests/patched/test_llama_trainer_ga.py b/tests/patched/test_llama_trainer_ga.py new file mode 100644 index 0000000000..58c229cf34 --- /dev/null +++ b/tests/patched/test_llama_trainer_ga.py @@ -0,0 +1,25 @@ +""""Test module for checking whether the Hugging Face Transformers is working as expected.""" +import unittest + +from axolotl.monkeypatch.trainer_grad_accum import ( + check_forward_is_patchable, + check_training_step_is_patchable, +) + + +class TestTrainerGAIntegration(unittest.TestCase): + """llama monkeypatch integration tests.""" + + def test_train_step_patchable(self): + # ensures the current version of transformers has loss code that matches our patching code + self.assertTrue( + check_training_step_is_patchable(), + "HF transformers Trainer.training_step has changed and isn't patchable", + ) + + def test_model_forward_patchable(self): + # ensures the current version of transformers has loss code that matches our patching code + self.assertTrue( + check_forward_is_patchable(), + "HF transformers LlamaForCausalLM.forward has changed and isn't patchable", + ) From 5bef19064b2e5905311ae718949553f4ad38a580 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 7 Dec 2024 17:24:46 -0500 Subject: [PATCH 0317/1405] [tests] reset known modules that are patched on each test function end (#2147) * reset known modules that are patched on each test function end * fix the llama model module name * prevent unsloth patching multiple times * pop classes out of the globals after reset * fix tuple indexing * manually workaround for llama fa2 --- src/axolotl/monkeypatch/trainer_grad_accum.py | 8 ++--- src/axolotl/monkeypatch/unsloth_.py | 8 +++++ tests/conftest.py | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/axolotl/monkeypatch/trainer_grad_accum.py b/src/axolotl/monkeypatch/trainer_grad_accum.py index 5ee90f91ae..97e6b7f2c5 100644 --- a/src/axolotl/monkeypatch/trainer_grad_accum.py +++ b/src/axolotl/monkeypatch/trainer_grad_accum.py @@ -3,14 +3,14 @@ see https://github.com/huggingface/transformers/pull/35128 """ import inspect +import logging -from accelerate.logging import get_logger from transformers import LlamaForCausalLM from transformers.trainer import Trainer from axolotl.monkeypatch.unsloth_ import detab_code -LOG = get_logger("axolotl.monkeypatch.trainer_grad_accum") +LOG = logging.getLogger("axolotl.monkeypatch.trainer_grad_accum") ORIGINAL_CONTEXT_CODE = """ with self.compute_loss_context_manager(): @@ -145,7 +145,7 @@ def patch_training_step_for_ga(): globals(), ) exec(training_step, globals()) # pylint: disable=exec-used # nosec B102 - LOG.info("patching training_step", main_process_only=True) + LOG.info("patching training_step") Trainer.training_step = ( # pylint: disable=protected-access _fixed_training_step # pylint: disable=undefined-variable # noqa: F821 ) @@ -201,7 +201,7 @@ def patch_forward_for_ga(): globals(), ) exec(forward, globals()) # pylint: disable=exec-used # nosec B102 - LOG.info("patching forward", main_process_only=True) + LOG.info("patching forward") LlamaForCausalLM.forward = ( # pylint: disable=protected-access _fixed_forward # pylint: disable=undefined-variable # noqa: F821 ) diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py index 7358803ba1..21fdb7edff 100644 --- a/src/axolotl/monkeypatch/unsloth_.py +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -102,7 +102,14 @@ def detab_code(code: str) -> Tuple[str, str]: return code, spaces +self_attn_lora_patched = False # pylint: disable=invalid-name + + def patch_self_attn_lora(): + global self_attn_lora_patched # pylint: disable=global-statement + if self_attn_lora_patched: + # prevent patching multiple times + return self_attn_forward = get_self_attn_code() LlamaFlashAttention2._original_forward = ( # pylint: disable=protected-access self_attn_forward @@ -134,6 +141,7 @@ def patch_self_attn_lora(): globals(), ) exec(self_attn_forward, globals()) # pylint: disable=exec-used # nosec B102 + self_attn_lora_patched = True LOG.info("patching unsloth attn lora", main_process_only=True) LlamaFlashAttention2.forward = ( unsloth_attn_forward # pylint: disable=undefined-variable # noqa: F821 diff --git a/tests/conftest.py b/tests/conftest.py index a775216fc0..1295d34b64 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,9 @@ shared pytest fixtures """ import functools +import importlib import shutil +import sys import tempfile import time @@ -113,3 +115,30 @@ def temp_dir(): yield _temp_dir # Clean up the directory after the test shutil.rmtree(_temp_dir) + + +@pytest.fixture(scope="function", autouse=True) +def cleanup_monkeypatches(): + from transformers.models.llama.modeling_llama import LlamaFlashAttention2 + + original_fa2_forward = LlamaFlashAttention2.forward + # monkey patches can happen inside the tests + yield + # Reset LlamaFlashAttention2 forward + LlamaFlashAttention2.forward = original_fa2_forward + + # Reset other known monkeypatches + modules_to_reset: list[tuple[str, list[str]]] = [ + ("transformers.models.llama.modeling_llama", ["LlamaFlashAttention2"]), + ("transformers.trainer",), + ("transformers.loss.loss_utils",), + ] + for module_name_tuple in modules_to_reset: + module_name = module_name_tuple[0] + module = importlib.import_module(module_name) + sys.modules[module_name] = module + importlib.reload(sys.modules[module_name]) + if len(module_name_tuple) > 1: + module_globals = module_name_tuple[1] + for module_global in module_globals: + globals().pop(module_global, None) From 440aab8a6fd2f233cec7a341d5f9cc0fe5c14a3a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 7 Dec 2024 22:23:33 -0500 Subject: [PATCH 0318/1405] add --version support to axolotl cli (#2152) [skip ci] --- src/axolotl/cli/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index a138daab25..a77410776a 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -5,6 +5,7 @@ import click +import axolotl from axolotl.cli.utils import ( add_options_from_config, add_options_from_dataclass, @@ -16,6 +17,7 @@ @click.group() +@click.version_option(version=axolotl.__version__, prog_name="axolotl") def cli(): """Axolotl CLI - Train and fine-tune large language models""" From 22319182abad03703a4604930588c790d7440614 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 7 Dec 2024 22:23:52 -0500 Subject: [PATCH 0319/1405] fix for auto_map check when using remote code and multipack for models like deepseek (#2151) [skip ci] --- src/axolotl/utils/models.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index f2ee93c3c7..88a8aa581f 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -406,10 +406,14 @@ def apply_patches(self) -> None: and self.cfg.flash_attention and self.cfg.sample_packing ): - has_remote_code = ( - "auto_map" in self.model_config - and "AutoModelForCausalLM" in self.model_config["auto_map"] - ) + if "auto_map" in self.model_config: + try: + auto_map_config = self.model_config["auto_map"] + except TypeError: + auto_map_config = self.model_config.auto_map + has_remote_code = "AutoModelForCausalLM" in auto_map_config + else: + has_remote_code = False if has_remote_code and self.cfg.trust_remote_code is False: # if explicitly set in the YAML, we should prefer that, for example if explicitly disabled has_remote_code = self.cfg.trust_remote_code From be5f554a62dfd11c01947780b3139c5e1efe76e2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 7 Dec 2024 22:24:09 -0500 Subject: [PATCH 0320/1405] bump autoawq to 0.2.7.post3 (#2150) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 864beb9b13..1d21cb354c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ art gradio==3.50.2 tensorboard python-dotenv==1.0.1 -autoawq==0.2.7.post2 +autoawq==0.2.7.post3 triton>=2.3.0 liger-kernel==0.4.2 From 1302e310491a9f6a5911eb1763a75302b74db285 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 8 Dec 2024 14:50:40 -0500 Subject: [PATCH 0321/1405] Transformers version flexibility and FSDP optimizer patch (#2155) * allow flexibility in transformers version for FSDP * more flexibility with dev versions of 4.47.0.dev0 * add patch for fsdp * fix typo * correct fn name * stray character * fix patch * reset Trainer too * also reset Trainer.training_step * allow tests/patched to run more than one process on e2e runner * skip tests/patched in e2e for now since it's run in regular pytest --- cicd/cicd.sh | 2 +- docker/Dockerfile-base | 2 +- requirements.txt | 2 +- src/axolotl/core/trainer_builder.py | 58 ++++++++++---- src/axolotl/monkeypatch/trainer_fsdp_optim.py | 80 +++++++++++++++++++ src/axolotl/utils/models.py | 7 ++ tests/conftest.py | 11 ++- 7 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 src/axolotl/monkeypatch/trainer_fsdp_optim.py diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 79b3cc95e0..c3e46920d8 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -2,6 +2,6 @@ set -e pytest -v --durations=10 -n8 --ignore=tests/e2e/ --ignore=tests/patched/ /workspace/axolotl/tests/ -pytest -v --durations=10 -n1 --dist loadfile /workspace/axolotl/tests/patched/ +# pytest -v --durations=10 -n8 --dist loadfile /workspace/axolotl/tests/patched/ pytest -v --durations=10 -n1 --dist loadfile /workspace/axolotl/tests/e2e/patched/ /workspace/axolotl/tests/e2e/integrations/ pytest -v --durations=10 --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 7eab3b3e43..4b24bfc3ae 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -16,7 +16,7 @@ ENV PYTHON_VERSION=$PYTHON_VERSION ENV TORCH_CUDA_ARCH_LIST=$TORCH_CUDA_ARCH_LIST RUN apt-get update \ - && apt-get install -y wget git build-essential ninja-build git-lfs libaio-dev && rm -rf /var/lib/apt/lists/* \ + && apt-get install -y wget git build-essential ninja-build git-lfs libaio-dev pkg-config && rm -rf /var/lib/apt/lists/* \ && wget \ https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \ && mkdir /root/.conda \ diff --git a/requirements.txt b/requirements.txt index 1d21cb354c..ae1b1838d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ packaging==23.2 peft==0.14.0 -transformers==4.47.0 +transformers>=4.46.3 tokenizers>=0.20.1 bitsandbytes==0.45.0 accelerate==1.2.0 diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index baac94da80..691437bc65 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -22,6 +22,7 @@ import torch import transformers from datasets import Dataset +from packaging import version from peft.optimizers import create_loraplus_optimizer from torch import nn from torch.optim.lr_scheduler import OneCycleLR @@ -973,7 +974,13 @@ def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> Non for key, metrics in self._stored_metrics[train_eval].items(): logs[key] = torch.tensor(metrics).mean().item() del self._stored_metrics[train_eval] - return super().log(logs, start_time) + + if version.parse(transformers.__version__) >= version.parse("4.47.0.dev0"): + try: + return super().log(logs, start_time) + except TypeError: + return super().log(logs) # transformers<=4.46 + return super().log(logs) # transformers<=4.46 def store_metrics( self, metrics: Dict[str, float], train_eval: Literal["train", "eval"] = "train" @@ -1165,9 +1172,13 @@ def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> Non for key, metrics in self._stored_metrics[train_eval].items(): logs[key] = torch.tensor(metrics).mean().item() del self._stored_metrics[train_eval] - return super(DPOTrainer, self).log( # pylint: disable=bad-super-call - logs, start_time - ) + + if version.parse(transformers.__version__) >= version.parse("4.47.0.dev0"): + return super(DPOTrainer, self).log( # pylint: disable=bad-super-call + logs, start_time + ) + # transformers<=4.46 + return super(DPOTrainer, self).log(logs) # pylint: disable=bad-super-call class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): @@ -1185,9 +1196,13 @@ def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> Non for key, metrics in self._stored_metrics[train_eval].items(): logs[key] = torch.tensor(metrics).mean().item() del self._stored_metrics[train_eval] - return super(ORPOTrainer, self).log( # pylint: disable=bad-super-call - logs, start_time - ) + + if version.parse(transformers.__version__) >= version.parse("4.47.0.dev0"): + return super(ORPOTrainer, self).log( # pylint: disable=bad-super-call + logs, start_time + ) + # transformers<=4.46 + return super(ORPOTrainer, self).log(logs) # pylint: disable=bad-super-call class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): @@ -1232,9 +1247,13 @@ def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> Non for key, metrics in self._stored_metrics[train_eval].items(): logs[f"{prefix}{key}"] = torch.Tensor(metrics).mean().item() del self._stored_metrics[train_eval] - return super(KTOTrainer, self).log( # pylint: disable=bad-super-call - logs, start_time - ) + + if version.parse(transformers.__version__) >= version.parse("4.47.0.dev0"): + return super(KTOTrainer, self).log( # pylint: disable=bad-super-call + logs, start_time + ) + # transformers<=4.46 + return super(KTOTrainer, self).log(logs) # pylint: disable=bad-super-call class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): @@ -1252,9 +1271,13 @@ def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> Non for key, metrics in self._stored_metrics[train_eval].items(): logs[key] = torch.tensor(metrics).mean().item() del self._stored_metrics[train_eval] - return super(CPOTrainer, self).log( # pylint: disable=bad-super-call - logs, start_time - ) + + if version.parse(transformers.__version__) >= version.parse("4.47.0.dev0"): + return super(CPOTrainer, self).log( # pylint: disable=bad-super-call + logs, start_time + ) + # transformers<=4.46 + return super(CPOTrainer, self).log(logs) # pylint: disable=bad-super-call class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): @@ -1266,9 +1289,12 @@ class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: # TODO remove once trl supports the updated to the Trainer.log method - return super(RewardTrainer, self).log( # pylint: disable=bad-super-call - logs, start_time - ) + if version.parse(transformers.__version__) >= version.parse("4.47.0.dev0"): + return super(RewardTrainer, self).log( # pylint: disable=bad-super-call + logs, start_time + ) + # transformers<=4.46 + return super(RewardTrainer, self).log(logs) # pylint: disable=bad-super-call class TrainerBuilderBase(abc.ABC): diff --git a/src/axolotl/monkeypatch/trainer_fsdp_optim.py b/src/axolotl/monkeypatch/trainer_fsdp_optim.py new file mode 100644 index 0000000000..835dea69b5 --- /dev/null +++ b/src/axolotl/monkeypatch/trainer_fsdp_optim.py @@ -0,0 +1,80 @@ +""" +fix for FSDP optimizer save in trainer w 4.47.0 +""" +import inspect +import logging + +from transformers.trainer import Trainer + +from axolotl.monkeypatch.unsloth_ import detab_code + +LOG = logging.getLogger("axolotl.monkeypatch.trainer_fsdp_save") + +ORIGINAL_TRAINER_CODE = """ + + delay_optimizer_creation = is_sagemaker_mp_enabled() or self.is_fsdp_xla_enabled + +""" + +PATCHED_TRAINER_CODE = """ + + delay_optimizer_creation = is_sagemaker_mp_enabled() or self.is_fsdp_xla_enabled or self.is_fsdp_enabled + +""" + + +def get_training_loop_code() -> str: + training_loop = inspect.getsource( + Trainer._inner_training_loop # pylint: disable=protected-access + ) + return training_loop + + +def check_training_loop_is_patchable() -> bool: + training_loop = get_training_loop_code() + training_loop, _ = detab_code(training_loop) + return ORIGINAL_TRAINER_CODE in training_loop + + +def patch_training_loop_for_fsdp(): + """ + monkeypatch for fixing the training loop for fsdp with optimizer save + """ + + try: + training_loop = get_training_loop_code() + except OSError: + return + Trainer._original_inner_training_loop = ( # pylint: disable=protected-access + training_loop + ) + training_loop, _ = detab_code(training_loop) + if ORIGINAL_TRAINER_CODE not in training_loop: + return + + training_loop = training_loop.replace(ORIGINAL_TRAINER_CODE, PATCHED_TRAINER_CODE) + training_loop = training_loop.replace( + "def _inner_training_loop(", + "def _fixed_inner_training_loop(", + 1, + ) + + # load imports necessary + import transformers.trainer + + items_to_import = [] + for item in dir(transformers.trainer): + if item in training_loop: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + "from transformers.trainer import (" + + ", ".join(x for x in items_to_import) + + ")", + globals(), + ) + exec(training_loop, globals()) # pylint: disable=exec-used # nosec B102 + LOG.info("patching _inner_training_loop for fsdp optimizer save") + Trainer._inner_training_loop = ( # pylint: disable=protected-access + _fixed_inner_training_loop # pylint: disable=undefined-variable # noqa: F821 + ) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 88a8aa581f..99095c1bfc 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -380,6 +380,13 @@ def apply_patches(self) -> None: plugin_manager = PluginManager.get_instance() plugin_manager.pre_model_load(self.cfg) + if self.cfg.fsdp: + from axolotl.monkeypatch.trainer_fsdp_optim import ( + patch_training_loop_for_fsdp, + ) + + patch_training_loop_for_fsdp() + if self.cfg.gradient_checkpointing == "unsloth": transformers.modeling_utils.checkpoint = hf_grad_checkpoint_unsloth_wrapper diff --git a/tests/conftest.py b/tests/conftest.py index 1295d34b64..a9dde9dd88 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -119,18 +119,27 @@ def temp_dir(): @pytest.fixture(scope="function", autouse=True) def cleanup_monkeypatches(): + from transformers import Trainer from transformers.models.llama.modeling_llama import LlamaFlashAttention2 original_fa2_forward = LlamaFlashAttention2.forward + original_trainer_inner_training_loop = ( + Trainer._inner_training_loop # pylint: disable=protected-access + ) + original_trainer_training_step = Trainer.training_step # monkey patches can happen inside the tests yield # Reset LlamaFlashAttention2 forward LlamaFlashAttention2.forward = original_fa2_forward + Trainer._inner_training_loop = ( # pylint: disable=protected-access + original_trainer_inner_training_loop + ) + Trainer.training_step = original_trainer_training_step # Reset other known monkeypatches modules_to_reset: list[tuple[str, list[str]]] = [ ("transformers.models.llama.modeling_llama", ["LlamaFlashAttention2"]), - ("transformers.trainer",), + ("transformers.trainer", ["Trainer"]), ("transformers.loss.loss_utils",), ] for module_name_tuple in modules_to_reset: From 393853751e9b761c79b22a2c9a1c19f32c917f31 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 8 Dec 2024 16:38:47 -0500 Subject: [PATCH 0322/1405] add additional fft deepspeed variants (#2153) [skip ci] --- tests/e2e/multigpu/test_llama.py | 198 ++++++++++++++++++++++++++++--- 1 file changed, 179 insertions(+), 19 deletions(-) diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index c9938a1559..cad0438013 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -9,6 +9,7 @@ import pytest import yaml from accelerate.test_utils import execute_subprocess_async +from e2e.utils import check_tensorboard from huggingface_hub import snapshot_download from transformers.testing_utils import get_torch_dist_unique_port @@ -53,7 +54,7 @@ def test_lora_ddp(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 15, + "max_steps": 5, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -61,6 +62,7 @@ def test_lora_ddp(self, temp_dir): "optimizer": "adamw_8bit", "lr_scheduler": "cosine", "flash_attention": True, + "use_tensorboard": True, } ) @@ -83,6 +85,10 @@ def test_lora_ddp(self, temp_dir): ] ) + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + ) + @pytest.mark.parametrize( "gradient_accumulation_steps", [1, 4], @@ -112,7 +118,7 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): }, ], "num_epochs": 1, - "max_steps": 15, + "max_steps": 5, "micro_batch_size": 4, "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, @@ -120,6 +126,7 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): "optimizer": "adamw_8bit", "lr_scheduler": "cosine", "flash_attention": True, + "use_tensorboard": True, } ) @@ -142,6 +149,10 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): ] ) + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + ) + def test_dpo_lora_ddp(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -180,7 +191,7 @@ def test_dpo_lora_ddp(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 15, + "max_steps": 5, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -189,6 +200,7 @@ def test_dpo_lora_ddp(self, temp_dir): "optimizer": "adamw_8bit", "lr_scheduler": "cosine", "flash_attention": True, + "use_tensorboard": True, } ) @@ -211,6 +223,10 @@ def test_dpo_lora_ddp(self, temp_dir): ] ) + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + ) + def test_dpo_qlora_ddp(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -249,7 +265,7 @@ def test_dpo_qlora_ddp(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 15, + "max_steps": 5, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -258,6 +274,7 @@ def test_dpo_qlora_ddp(self, temp_dir): "optimizer": "adamw_8bit", "lr_scheduler": "cosine", "flash_attention": True, + "use_tensorboard": True, } ) @@ -280,6 +297,10 @@ def test_dpo_qlora_ddp(self, temp_dir): ] ) + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + ) + @pytest.mark.parametrize( "gradient_accumulation_steps", [1, 4], @@ -301,7 +322,7 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): }, ], "num_epochs": 1, - "max_steps": 10, + "max_steps": 5, "micro_batch_size": 4, "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, @@ -323,6 +344,7 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): "fsdp_state_dict_type": "FULL_STATE_DICT", "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", }, + "use_tensorboard": True, } ) @@ -345,6 +367,10 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): ] ) + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + ) + @pytest.mark.parametrize( "fsdp_state_dict_type", ["FULL_STATE_DICT", "SHARDED_STATE_DICT"], @@ -368,7 +394,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): }, ], "num_epochs": 1, - "max_steps": 15, + "max_steps": 5, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -390,6 +416,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): "fsdp_state_dict_type": fsdp_state_dict_type, "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", }, + "use_tensorboard": True, } ) @@ -412,6 +439,10 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): ] ) + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + ) + def test_fsdp_qlora_prequant_packed(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -444,7 +475,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 15, + "max_steps": 5, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -466,6 +497,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "fsdp_state_dict_type": "SHARDED_STATE_DICT", "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", }, + "use_tensorboard": True, } ) @@ -488,12 +520,41 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): ] ) + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + ) + @pytest.mark.parametrize( "gradient_accumulation_steps", [1, 4], ) - def test_ds_zero3_packed(self, temp_dir, gradient_accumulation_steps): + @pytest.mark.parametrize( + "deepspeed", + [ + "deepspeed_configs/zero3_bf16.json", + "deepspeed_configs/zero3_bf16_cpuoffload_all.json", + "deepspeed_configs/zero3_bf16_cpuoffload_params.json", + ], + ) + @pytest.mark.parametrize( + "qlora", + [True, False], + ) + def test_ds_zero3_packed( + self, temp_dir, gradient_accumulation_steps, deepspeed, qlora + ): # pylint: disable=duplicate-code + if qlora: + adapter = { + "adapter": "qlora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "load_in_4bit": True, + } + else: + adapter = {} cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -511,15 +572,17 @@ def test_ds_zero3_packed(self, temp_dir, gradient_accumulation_steps): }, ], "num_epochs": 1, - "max_steps": 15, - "micro_batch_size": 4, + "max_steps": 5, + "micro_batch_size": 2, "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch", "lr_scheduler": "cosine", "flash_attention": True, - "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero3_bf16.json"), + "deepspeed": str(AXOLOTL_ROOT / deepspeed), + "use_tensorboard": True, + **adapter, } ) @@ -542,19 +605,110 @@ def test_ds_zero3_packed(self, temp_dir, gradient_accumulation_steps): ] ) - def test_ds_zero3_qlora_packed(self, temp_dir): + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + ) + + @pytest.mark.parametrize( + "gradient_accumulation_steps", + [1, 4], + ) + @pytest.mark.parametrize( + "qlora", + [True, False], + ) + def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): # pylint: disable=duplicate-code + if qlora: + adapter = { + "adapter": "qlora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "load_in_4bit": True, + } + else: + adapter = {} cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", - "load_in_4bit": True, + "sample_packing": True, + "pad_to_sequence_len": True, + "sequence_len": 2048, + "val_set_size": 0.05, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "micro_batch_size": 2, + "gradient_accumulation_steps": gradient_accumulation_steps, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero2.json"), + "use_tensorboard": True, + **adapter, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + ) + + @pytest.mark.parametrize( + "gradient_accumulation_steps", + [1, 4], + ) + @pytest.mark.parametrize( + "qlora", + [True, False], + ) + def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): + # pylint: disable=duplicate-code + if qlora: + adapter = { "adapter": "qlora", "lora_r": 8, "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, + "load_in_4bit": True, + } + else: + adapter = {} + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", "sample_packing": True, - "eval_sample_packing": False, "pad_to_sequence_len": True, "sequence_len": 2048, "val_set_size": 0.05, @@ -568,15 +722,17 @@ def test_ds_zero3_qlora_packed(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 15, - "micro_batch_size": 4, - "gradient_accumulation_steps": 4, + "max_steps": 5, + "micro_batch_size": 2, + "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, - "learning_rate": 0.0001, + "learning_rate": 0.00001, "optimizer": "adamw_torch", "lr_scheduler": "cosine", "flash_attention": True, - "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero3_bf16.json"), + "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero1.json"), + "use_tensorboard": True, + **adapter, } ) @@ -598,3 +754,7 @@ def test_ds_zero3_qlora_packed(self, temp_dir): str(Path(temp_dir) / "config.yaml"), ] ) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + ) From 343a4d8855d0496f2084d250ae17f725c437266d Mon Sep 17 00:00:00 2001 From: Sunny Liu <22844540+bursteratom@users.noreply.github.com> Date: Sun, 8 Dec 2024 16:39:05 -0500 Subject: [PATCH 0323/1405] Fixing issue#2134 Axolotl Crashes At The End Of Training If Base Model Is Local (#2140) --- src/axolotl/train.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 39af9f45c9..8f50a243d4 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -259,14 +259,7 @@ def terminate_handler(_, __, model_weakref): model.save_pretrained(cfg.output_dir, safe_serialization=safe_serialization) if not cfg.hub_model_id: - from huggingface_hub import HfApi - from huggingface_hub.utils import RepositoryNotFoundError - try: - # Check to make sure the base model is from HuggingFace not a local directory - hf_api = HfApi() - hf_api.model_info(cfg.base_model) - model_card_kwarg = { "model_name": cfg.output_dir.lstrip("./") .encode("utf-8") @@ -283,7 +276,7 @@ def terminate_handler(_, __, model_weakref): ] trainer.create_model_card(**model_card_kwarg) - except (AttributeError, UnicodeDecodeError, RepositoryNotFoundError): + except (AttributeError, UnicodeDecodeError): pass elif cfg.hub_model_id: # defensively push to the hub to ensure the model card is updated From 0c25bc07a2eb2dca44b1ea763badb8192bddad5a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 8 Dec 2024 21:09:12 -0500 Subject: [PATCH 0324/1405] use manual version for now (#2156) --- .github/workflows/tests.yml | 8 ++++---- setup.py | 18 ++++++++++++++++++ src/axolotl/__init__.py | 7 +------ tests/cli/test_cli_version.py | 10 ++++++++++ 4 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 tests/cli/test_cli_version.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3e1a4fe924..88172fdd5a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -89,8 +89,8 @@ jobs: - name: Run tests run: | - pytest -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ - pytest tests/patched/ + pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ + pytest -v tests/patched/ - name: cleanup pip cache run: | @@ -141,8 +141,8 @@ jobs: - name: Run tests run: | - pytest -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ - pytest tests/patched/ + pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ + pytest -v tests/patched/ - name: cleanup pip cache run: | diff --git a/setup.py b/setup.py index 8d7173f28c..848acab33a 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,10 @@ """setup.py for axolotl""" +import ast +import os import platform import re from importlib.metadata import PackageNotFoundError, version +from pathlib import Path from setuptools import find_packages, setup @@ -90,9 +93,24 @@ def parse_requirements(): return _install_requires, _dependency_links +def get_package_version(): + with open( + Path(os.path.dirname(os.path.abspath(__file__))) + / "src" + / "axolotl" + / "__init__.py", + "r", + encoding="utf-8", + ) as fin: + version_match = re.search(r"^__version__\s*=\s*(.*)$", fin.read(), re.MULTILINE) + version_ = ast.literal_eval(version_match.group(1)) + return version_ + + install_requires, dependency_links = parse_requirements() setup( + version=get_package_version(), package_dir={"": "src"}, packages=find_packages("src"), install_requires=install_requires, diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index e555ece480..b4f1d06f7b 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -1,8 +1,3 @@ """Axolotl - Train and fine-tune large language models""" -try: - from importlib.metadata import version - - __version__ = version("axolotl") -except ImportError: - __version__ = "unknown" +__version__ = "0.5.3.dev0" diff --git a/tests/cli/test_cli_version.py b/tests/cli/test_cli_version.py new file mode 100644 index 0000000000..819780e945 --- /dev/null +++ b/tests/cli/test_cli_version.py @@ -0,0 +1,10 @@ +"""pytest tests for axolotl CLI --version""" +from axolotl.cli.main import cli + + +def test_print_version(cli_runner): + """Test that version is printed when --version is used.""" + + result = cli_runner.invoke(cli, ["--version"]) + assert result.exit_code == 0 + assert "axolotl, version " in result.output From 6a342feda227f6c0676832be38340885aa70f921 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 9 Dec 2024 19:24:48 +0700 Subject: [PATCH 0325/1405] fix: duplicate mlflow logging (#2109) [skip ci] --- src/axolotl/core/trainer_builder.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 691437bc65..73d9e0e655 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1368,8 +1368,6 @@ def get_callbacks(self) -> List[TrainerCallback]: SaveAxolotlConfigtoWandBCallback(self.cfg.axolotl_config_path) ) if self.cfg.use_mlflow and is_mlflow_available(): - from transformers.integrations.integration_utils import MLflowCallback - from axolotl.utils.callbacks.mlflow_ import ( SaveAxolotlConfigtoMlflowCallback, ) @@ -1377,7 +1375,6 @@ def get_callbacks(self) -> List[TrainerCallback]: callbacks.extend( [ SaveAxolotlConfigtoMlflowCallback(self.cfg.axolotl_config_path), - MLflowCallback, ] ) if self.cfg.use_comet and is_comet_available(): From 40907c68877a944cb82fece9e52abd6d3ecee002 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 9 Dec 2024 07:25:10 -0500 Subject: [PATCH 0326/1405] upgrade deepspeed to 0.16.1 (#2157) --- requirements.txt | 21 +++-- src/axolotl/monkeypatch/trainer_grad_accum.py | 84 +++++++++++++++++++ src/axolotl/utils/models.py | 6 ++ 3 files changed, 102 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index ae1b1838d5..361524e561 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,22 +1,30 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ + +# START section of dependencies that don't install on Darwin/MacOS +bitsandbytes==0.45.0 +triton>=2.3.0 +mamba-ssm==1.2.0.post1 +flash-attn==2.7.0.post2 +xformers>=0.0.23.post1 +autoawq==0.2.7.post3 +liger-kernel==0.4.2 +# END section + packaging==23.2 peft==0.14.0 transformers>=4.46.3 tokenizers>=0.20.1 -bitsandbytes==0.45.0 accelerate==1.2.0 datasets==3.1.0 -deepspeed==0.15.4 +deepspeed==0.16.1 pydantic==2.6.3 addict fire PyYAML>=6.0 requests -flash-attn==2.7.0.post2 sentencepiece wandb einops -xformers>=0.0.23.post1 optimum==1.16.2 hf_transfer colorama @@ -31,11 +39,6 @@ art gradio==3.50.2 tensorboard python-dotenv==1.0.1 -autoawq==0.2.7.post3 -triton>=2.3.0 -liger-kernel==0.4.2 - -mamba-ssm==1.2.0.post1 # remote filesystems s3fs>=2024.5.0 diff --git a/src/axolotl/monkeypatch/trainer_grad_accum.py b/src/axolotl/monkeypatch/trainer_grad_accum.py index 97e6b7f2c5..39435ebac1 100644 --- a/src/axolotl/monkeypatch/trainer_grad_accum.py +++ b/src/axolotl/monkeypatch/trainer_grad_accum.py @@ -205,3 +205,87 @@ def patch_forward_for_ga(): LlamaForCausalLM.forward = ( # pylint: disable=protected-access _fixed_forward # pylint: disable=undefined-variable # noqa: F821 ) + + +ORIGINAL_TRAINER_CODE = """ + context = ( + functools.partial(self.accelerator.no_sync, model=model) + if i != len(batch_samples) - 1 + else contextlib.nullcontext + ) + with context(): + tr_loss_step = self.training_step(model, inputs, num_items_in_batch) +""" + +PATCHED_TRAINER_CODE = """ + disable_deepspeed_no_sync = ( + self.accelerator.distributed_type == DistributedType.DEEPSPEED + and self.accelerator.deepspeed_engine_wrapped.engine.zero_optimization_partition_gradients() + ) + context = ( + functools.partial(self.accelerator.no_sync, model=model) + if i != len(batch_samples) - 1 and not disable_deepspeed_no_sync + else contextlib.nullcontext + ) + with context(): + tr_loss_step = self.training_step(model, inputs, num_items_in_batch) +""" + + +def get_training_loop_code() -> str: + training_loop = inspect.getsource( + Trainer._inner_training_loop # pylint: disable=protected-access + ) + return training_loop + + +def check_training_loop_is_patchable() -> bool: + training_loop = get_training_loop_code() + training_loop, _ = detab_code(training_loop) + return ORIGINAL_TRAINER_CODE in training_loop + + +def patch_training_loop_for_deepspeed_0_16_x(): + """ + monkeypatch for fixing the training loop for deepspeed GA + + see https://github.com/huggingface/transformers/pull/35157 + """ + + try: + training_loop = get_training_loop_code() + except OSError: + return + Trainer._original_inner_training_loop = ( # pylint: disable=protected-access + training_loop + ) + training_loop, _ = detab_code(training_loop) + if ORIGINAL_TRAINER_CODE not in training_loop: + return + + training_loop = training_loop.replace(ORIGINAL_TRAINER_CODE, PATCHED_TRAINER_CODE) + training_loop = training_loop.replace( + "def _inner_training_loop(", + "def _fixed_inner_training_loop(", + 1, + ) + + # load imports necessary + import transformers.trainer + + items_to_import = [] + for item in dir(transformers.trainer): + if item in training_loop: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + "from transformers.trainer import (" + + ", ".join(x for x in items_to_import) + + ")", + globals(), + ) + exec(training_loop, globals()) # pylint: disable=exec-used # nosec B102 + LOG.info("patching _inner_training_loop for fsdp optimizer save") + Trainer._inner_training_loop = ( # pylint: disable=protected-access + _fixed_inner_training_loop # pylint: disable=undefined-variable # noqa: F821 + ) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 99095c1bfc..a350f24295 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -386,6 +386,12 @@ def apply_patches(self) -> None: ) patch_training_loop_for_fsdp() + elif self.cfg.deepspeed: + from axolotl.monkeypatch.trainer_grad_accum import ( + patch_training_loop_for_deepspeed_0_16_x, + ) + + patch_training_loop_for_deepspeed_0_16_x() if self.cfg.gradient_checkpointing == "unsloth": transformers.modeling_utils.checkpoint = hf_grad_checkpoint_unsloth_wrapper From b1e8286c5783405c4f40b732495d307df10e6a16 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 9 Dec 2024 08:17:08 -0500 Subject: [PATCH 0327/1405] add missing __init__ to optimizers path (#2160) [skip ci] --- src/axolotl/utils/optimizers/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/axolotl/utils/optimizers/__init__.py diff --git a/src/axolotl/utils/optimizers/__init__.py b/src/axolotl/utils/optimizers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From c78de6f214914808c3eb8e431532c2afd35730b5 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 9 Dec 2024 20:17:27 +0700 Subject: [PATCH 0328/1405] feat: add kto example (#2158) [skip ci] --- docs/rlhf.qmd | 20 +++++ examples/llama-3/qlora-1b-kto.yaml | 75 +++++++++++++++++++ .../config/models/input/v0_4_1/__init__.py | 21 ++++++ 3 files changed, 116 insertions(+) create mode 100644 examples/llama-3/qlora-1b-kto.yaml diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index b8b2bded09..48701c87af 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -52,6 +52,26 @@ datasets: type: chat_template.argilla ``` + +#### KTO + +```yaml +rl: kto +rl_beta: 0.5 +kto_desirable_weight: 0.2 + +remove_unused_columns: false + +datasets: + - path: argilla/ultrafeedback-binarized-preferences-cleaned-kto + type: llama3.ultra + split: train + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: true +``` + #### Using local dataset files ```yaml datasets: diff --git a/examples/llama-3/qlora-1b-kto.yaml b/examples/llama-3/qlora-1b-kto.yaml new file mode 100644 index 0000000000..a876d8fd72 --- /dev/null +++ b/examples/llama-3/qlora-1b-kto.yaml @@ -0,0 +1,75 @@ +base_model: meta-llama/Llama-3.2-1B + +load_in_8bit: false +load_in_4bit: true +strict: false + +rl: kto +rl_beta: 0.5 +kto_desirable_weight: 0.2 + +datasets: + - path: argilla/ultrafeedback-binarized-preferences-cleaned-kto + type: llama3.ultra + split: train +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/qlora-out + +remove_unused_columns: false + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: false # not supported with kto +eval_sample_packing: false +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 64 +lora_dropout: 0.05 +lora_target_linear: true +lora_fan_in_fan_out: + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_steps: 20 +evals_per_epoch: 4 +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: + pad_token: "<|end_of_text|>" diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 24ea62c77f..3671e1bb93 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -1475,6 +1475,27 @@ def check_npu_config(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_kto_config(cls, data): + if data.get("rl") == "kto": + if data.get("sample_packing") or data.get("eval_sample_packing"): + raise ValueError("sample_packing is not supported with kto") + + if data.get("remove_unused_columns") is not False: + raise ValueError("Set `remove_unused_columns: False` when using kto") + + if data.get("gradient_checkpointing") and not ( + data.get("gradient_checkpointing_kwargs") + and isinstance(data.get("gradient_checkpointing_kwargs"), dict) + and data["gradient_checkpointing_kwargs"].get("use_reentrant") + ): + raise ValueError( + "Set `gradient_checkpointing_kwargs: {use_reentrant: true}` for when kto is enabled" + ) + + return data + class AxolotlConfigWCapabilities(AxolotlInputConfig): """wrapper to valdiate gpu capabilities with the configured options""" From 3862267040417d9da1299e9af26116e66ff9a52a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 9 Dec 2024 13:49:18 -0500 Subject: [PATCH 0329/1405] don't add dataset tags if empty due to all local data paths (#2162) [skip ci] --- src/axolotl/train.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 8f50a243d4..c8576f1b48 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -267,13 +267,19 @@ def terminate_handler(_, __, model_weakref): } if cfg.datasets is not None: if cfg.rl is not None or cfg.reward_model: - model_card_kwarg["dataset_name"] = [ + dataset_tags = [ d["path"] for d in cfg.datasets if not Path(d["path"]).is_dir() ] + if dataset_tags: + # guard as create_model_card may fail if dataset_tags is empty list + model_card_kwarg["dataset_name"] = dataset_tags else: - model_card_kwarg["dataset_tags"] = [ + dataset_tags = [ d["path"] for d in cfg.datasets if not Path(d["path"]).is_dir() ] + if dataset_tags: + # guard as create_model_card may fail if dataset_tags is empty list + model_card_kwarg["dataset_tags"] = dataset_tags trainer.create_model_card(**model_card_kwarg) except (AttributeError, UnicodeDecodeError): From 5d6b088997d3992d1ce591087d04ae1f968dbd56 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 10 Dec 2024 01:49:38 +0700 Subject: [PATCH 0330/1405] fix: chat_template masking due to truncation, consolidate turn build and keys within field (#2123) [skip ci] * fix: chat_template masking due to truncation, consolidate turn build and keys within field * fix: revert roles change * fix: handling of training and training_detail * fix: do not skip setting eos mask even if failed finding turn boundary * fix: truncate reward modelling outputs --- .../bradley_terry/chat_template.py | 30 +++- .../prompt_strategies/chat_template.py | 158 ++++++++++-------- 2 files changed, 114 insertions(+), 74 deletions(-) diff --git a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py index fa85cdcb26..4f60842c5f 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py +++ b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py @@ -28,6 +28,8 @@ def tokenize_prompt(self, prompt): :return: """ + max_length = self.prompter.max_length + self.messages = "chosen_messages" # pylint: disable=duplicate-code prompt[self.messages] = [] @@ -39,6 +41,16 @@ def tokenize_prompt(self, prompt): prompt[self.messages].append({"role": "assistant", "content": prompt["chosen"]}) chosen_tokenized = super().tokenize_prompt(prompt) + if len(chosen_tokenized["input_ids"]) > max_length: + LOG.warning( + f"Chosen sequence exceeds max sequence length: {len(chosen_tokenized['input_ids'])}", + ) + + chosen_tokenized["input_ids"] = chosen_tokenized["input_ids"][:max_length] + chosen_tokenized["attention_mask"] = chosen_tokenized["attention_mask"][ + :max_length + ] + self.messages = "rejected_messages" # pylint: disable=duplicate-code prompt[self.messages] = [] @@ -52,6 +64,18 @@ def tokenize_prompt(self, prompt): ) rejected_tokenized = super().tokenize_prompt(prompt) + if len(rejected_tokenized["input_ids"]) > max_length: + LOG.warning( + f"Rejected sequence exceeds max sequence length: {len(rejected_tokenized['input_ids'])}", + ) + + rejected_tokenized["input_ids"] = rejected_tokenized["input_ids"][ + :max_length + ] + rejected_tokenized["attention_mask"] = rejected_tokenized["attention_mask"][ + :max_length + ] + return { "input_ids_chosen": chosen_tokenized["input_ids"], "attention_mask_chosen": chosen_tokenized["attention_mask"], @@ -80,9 +104,9 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): "roles": ds_cfg.get("roles"), "drop_system_message": ds_cfg.get("drop_system_message", False), # we need to add one for detecting sequences with exceeding the `sequence_len` limit. - "max_length": cfg.sequence_len + 1 - if not cfg.reward_model - else cfg.sequence_len, + "max_length": ( + cfg.sequence_len + 1 if not cfg.reward_model else cfg.sequence_len + ), } strategy_params = { diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 0946a4b8c7..35c9311678 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -42,6 +42,7 @@ def __init__( "gpt": "assistant", "system": "system", } + self.message_field_role = message_field_role self.message_field_content = message_field_content self.message_field_training = message_field_training @@ -53,21 +54,9 @@ def __init__( self.drop_system_message = drop_system_message def build_prompt(self, conversation, add_generation_prompt=False, images=None): - turns = [ - { - "role": self.roles[t[self.message_field_role]], - "content": t[self.message_field_content], - "training": t.get(self.message_field_training, None), - } - for t in conversation - ] - - if self.drop_system_message and turns[0]["role"] == "system": - turns = turns[1:] - if self.processor: text = self.processor.apply_chat_template( - turns, + conversation, chat_template=self.chat_template, tokenize=False, add_generation_prompt=add_generation_prompt, @@ -76,8 +65,6 @@ def build_prompt(self, conversation, add_generation_prompt=False, images=None): text=text, images=images, return_tensors="pt", - truncation=True, - max_length=self.max_length, ) # workaround since processor works in batches instead of single examples for k, val in batch.items(): @@ -88,9 +75,7 @@ def build_prompt(self, conversation, add_generation_prompt=False, images=None): return batch return self.tokenizer.apply_chat_template( - turns, - truncation=True, - max_length=self.max_length, + conversation, add_generation_prompt=add_generation_prompt, chat_template=self.chat_template, ) @@ -215,7 +200,14 @@ def __init__( train_on_eos=None, ): super().__init__(prompter, tokenizer, train_on_inputs, sequence_len) - self.roles_to_train = roles_to_train if roles_to_train is not None else [] + + self.roles_to_train = [] + if roles_to_train: + # map roles if exist in prompter.roles else use the role as is + self.roles_to_train = [ + prompter.roles.get(role, role) for role in roles_to_train + ] + self.train_on_eos = train_on_eos self.images = "images" @@ -262,30 +254,28 @@ def tokenize_prompt(self, prompt): return tokenized_prompt - turns = prompt[self.messages] + turns = self.get_conversation_thread(prompt) input_ids = self.prompter.build_prompt(turns) labels = [IGNORE_TOKEN_ID] * len(input_ids) last_eos_idx = -1 for index, turn in enumerate(turns): - role = turn.get(self.prompter.message_field_role) - content = turn.get(self.prompter.message_field_content) - train_turn = turn.get(self.prompter.message_field_training) - train_detail = turn.get(self.prompter.message_field_training_detail) + role = turn.get("role") + content = turn.get("content") + train_turn = turn.get("training") + train_detail = turn.get("training_detail") LOG.debug( f"Processing turn {index}: role={role}, content={content}, train_turn={train_turn}, train_detail={train_detail}" ) - should_train = ( - train_turn - if train_turn is not None - else ( - bool(train_detail is not None) - if train_detail is not None - else self.train_on_inputs or role in self.roles_to_train - ) - ) + should_train = None + if train_turn is not None: + should_train = train_turn + elif train_detail is not None: + should_train = bool(train_detail) + else: + should_train = self.train_on_inputs or role in self.roles_to_train LOG.debug(f"Should train: {should_train}") @@ -293,6 +283,9 @@ def tokenize_prompt(self, prompt): conversation_ids=input_ids, turn=index, turn_content=turn ) + if turn_start_idx == -1 or turn_end_idx == -1: + LOG.warning(f"Failed to find boundaries for turn {index}") + LOG.debug(f"Turn indices: start={turn_start_idx}, end={turn_end_idx}") if should_train and turn_start_idx != -1 and turn_end_idx != -1: @@ -313,7 +306,9 @@ def tokenize_prompt(self, prompt): labels[turn_start_idx:turn_end_idx] = input_ids[ turn_start_idx:turn_end_idx ] - LOG.debug(f"Labels set for range {turn_start_idx}:{turn_end_idx}") + LOG.debug( + f"Set labels for training from {turn_start_idx} to {turn_end_idx}" + ) LOG.debug(f"Labels after processing turn {index}: {labels}") @@ -351,52 +346,73 @@ def find_eos_token(self, input_ids, start_idx): return i return -1 - def find_turn(self, conversation_ids, turn, turn_content): + def find_turn(self, conversation_ids: list[int], turn: int, turn_content: dict): """ Locate the starting and ending indices of the specified turn in a conversation. - - Args: - conversation_ids (list[int]): Token IDs representing the conversation. - turn (int): The turn number to locate (based on EOS tokens). - turn_content (str): String containing the content of the turn. - - Returns: - tuple: (start_idx, end_idx) indices of the start and end of the turn content. - Returns (-1, -1) if the turn content is not found. """ - content = turn_content.get(self.prompter.message_field_content, "") + content = turn_content.get("content") content_ids = self.tokenizer.encode(content, add_special_tokens=False) - eos_token_id = self.tokenizer.eos_token_id - eos_count = 0 - start_search_idx = 0 - - # Locate the starting index after the specified number of EOS tokens - for i, token_id in enumerate(conversation_ids): - if token_id == eos_token_id: - eos_count += 1 - if eos_count == turn: - start_search_idx = ( - i + 1 - ) # Start searching after the specified turn's EOS token - break - - # Find the start index of the content within the conversation - start_idx = -1 - for i in range(start_search_idx, len(conversation_ids) - len(content_ids) + 1): - if conversation_ids[i : i + len(content_ids)] == content_ids: - start_idx = i - break - - if start_idx != -1: - end_idx = start_idx + len(content_ids) + LOG.debug(f"content_ids (length {len(content_ids)}): {content_ids}") + + if not content_ids: + LOG.warning(f"Empty content for turn {turn}") + return -1, -1 + + # For first turn, start from beginning + if turn == 0: + start_search_idx = 0 else: - end_idx = -1 + # For subsequent turns, find the previous EOS token + eos_token_id = self.tokenizer.eos_token_id + eos_count = 0 + start_search_idx = 0 + + for i, token_id in enumerate(conversation_ids): + if token_id == eos_token_id: + eos_count += 1 + if eos_count == turn: # Find the nth EOS token where n = turn + start_search_idx = i + 1 + break + + # we can optimize this to only search for a few tokens from start_search_idx + # but it would risk missing the content if it's not found within the first few tokens or + # if start_search_idx cannot be found above. + last_index = len(conversation_ids) - len(content_ids) + 1 + + if last_index < start_search_idx: + LOG.warning( + f"last_index to search is less than start_search_idx for turn {turn}" + ) + return -1, -1 + + # Search for content starting from start_search_idx + first_elem = content_ids[0] + for i in range(start_search_idx, last_index): + # Quick check of first element before doing full comparison + if conversation_ids[i] == first_elem: + # Check if the rest of the content matches + if conversation_ids[i : i + len(content_ids)] == content_ids: + LOG.debug(f"Found turn {turn} content at position {i}") + return i, i + len(content_ids) - return start_idx, end_idx + return -1, -1 def get_conversation_thread(self, prompt): - return prompt[self.messages] + turns = [ + { + "role": self.prompter.roles[t[self.prompter.message_field_role]], + "content": t[self.prompter.message_field_content], + "training": t.get(self.prompter.message_field_training), + "training_detail": t.get(self.prompter.message_field_training_detail), + } + for t in prompt[self.messages] + ] + + if self.prompter.drop_system_message and turns[0]["role"] == "system": + turns = turns[1:] + + return turns def get_images(self, prompt): return prompt.get(self.images, None) From ab4b32187d0fd1108a5faa4291e05d13a4a72af2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 9 Dec 2024 14:01:44 -0500 Subject: [PATCH 0331/1405] need to update deepspeed version in extras too (#2161) [skip ci] * need to update deepspeed version in extras too * fix patch import * fix monkeypatch reloading in tests and deepspeed patch * remove duplicated functionality fixture * reset LlamaForCausalLM too in fixtures for cce patch * reset llama attn too * disable xformers patch for cce * skip problematic test on low usage functionality --- cicd/cicd.sh | 3 +- setup.py | 2 +- src/axolotl/monkeypatch/trainer_fsdp_optim.py | 2 +- src/axolotl/monkeypatch/trainer_grad_accum.py | 5 +-- src/axolotl/utils/models.py | 2 +- tests/conftest.py | 30 ++++++++++--- .../integrations/test_cut_cross_entropy.py | 6 ++- tests/e2e/multigpu/test_llama.py | 44 +++++++++---------- tests/e2e/patched/test_fa_xentropy.py | 9 ---- tests/e2e/patched/test_fused_llama.py | 2 + 10 files changed, 60 insertions(+), 45 deletions(-) diff --git a/cicd/cicd.sh b/cicd/cicd.sh index c3e46920d8..1ead6d518e 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -3,5 +3,6 @@ set -e pytest -v --durations=10 -n8 --ignore=tests/e2e/ --ignore=tests/patched/ /workspace/axolotl/tests/ # pytest -v --durations=10 -n8 --dist loadfile /workspace/axolotl/tests/patched/ -pytest -v --durations=10 -n1 --dist loadfile /workspace/axolotl/tests/e2e/patched/ /workspace/axolotl/tests/e2e/integrations/ +pytest -v --durations=10 -n1 --dist loadfile /workspace/axolotl/tests/e2e/patched/ +pytest -v --durations=10 -n1 --dist loadfile /workspace/axolotl/tests/e2e/integrations/ pytest -v --durations=10 --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ diff --git a/setup.py b/setup.py index 848acab33a..4424d430a9 100644 --- a/setup.py +++ b/setup.py @@ -125,7 +125,7 @@ def get_package_version(): "flash-attn==2.7.0.post2", ], "deepspeed": [ - "deepspeed==0.15.4", + "deepspeed==0.16.1", "deepspeed-kernels", ], "mamba-ssm": [ diff --git a/src/axolotl/monkeypatch/trainer_fsdp_optim.py b/src/axolotl/monkeypatch/trainer_fsdp_optim.py index 835dea69b5..185f742d77 100644 --- a/src/axolotl/monkeypatch/trainer_fsdp_optim.py +++ b/src/axolotl/monkeypatch/trainer_fsdp_optim.py @@ -4,7 +4,7 @@ import inspect import logging -from transformers.trainer import Trainer +from transformers import Trainer from axolotl.monkeypatch.unsloth_ import detab_code diff --git a/src/axolotl/monkeypatch/trainer_grad_accum.py b/src/axolotl/monkeypatch/trainer_grad_accum.py index 39435ebac1..76fcf57ce4 100644 --- a/src/axolotl/monkeypatch/trainer_grad_accum.py +++ b/src/axolotl/monkeypatch/trainer_grad_accum.py @@ -5,8 +5,7 @@ import inspect import logging -from transformers import LlamaForCausalLM -from transformers.trainer import Trainer +from transformers import LlamaForCausalLM, Trainer from axolotl.monkeypatch.unsloth_ import detab_code @@ -220,7 +219,7 @@ def patch_forward_for_ga(): PATCHED_TRAINER_CODE = """ disable_deepspeed_no_sync = ( self.accelerator.distributed_type == DistributedType.DEEPSPEED - and self.accelerator.deepspeed_engine_wrapped.engine.zero_optimization_partition_gradients() + # and self.accelerator.deepspeed_engine_wrapped.engine.zero_optimization_partition_gradients() ) context = ( functools.partial(self.accelerator.no_sync, model=model) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index a350f24295..11f4c6d0fe 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -386,7 +386,7 @@ def apply_patches(self) -> None: ) patch_training_loop_for_fsdp() - elif self.cfg.deepspeed: + elif self.cfg.deepspeed and self.cfg.gradient_accumulation_steps > 1: from axolotl.monkeypatch.trainer_grad_accum import ( patch_training_loop_for_deepspeed_0_16_x, ) diff --git a/tests/conftest.py b/tests/conftest.py index a9dde9dd88..f2519cdcfd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -120,9 +120,15 @@ def temp_dir(): @pytest.fixture(scope="function", autouse=True) def cleanup_monkeypatches(): from transformers import Trainer - from transformers.models.llama.modeling_llama import LlamaFlashAttention2 + from transformers.models.llama.modeling_llama import ( + LlamaAttention, + LlamaFlashAttention2, + LlamaForCausalLM, + ) original_fa2_forward = LlamaFlashAttention2.forward + original_llama_attn_forward = LlamaAttention.forward + original_llama_forward = LlamaForCausalLM.forward original_trainer_inner_training_loop = ( Trainer._inner_training_loop # pylint: disable=protected-access ) @@ -131,6 +137,8 @@ def cleanup_monkeypatches(): yield # Reset LlamaFlashAttention2 forward LlamaFlashAttention2.forward = original_fa2_forward + LlamaAttention.forward = original_llama_attn_forward + LlamaForCausalLM.forward = original_llama_forward Trainer._inner_training_loop = ( # pylint: disable=protected-access original_trainer_inner_training_loop ) @@ -138,15 +146,25 @@ def cleanup_monkeypatches(): # Reset other known monkeypatches modules_to_reset: list[tuple[str, list[str]]] = [ - ("transformers.models.llama.modeling_llama", ["LlamaFlashAttention2"]), - ("transformers.trainer", ["Trainer"]), + ("transformers.models.llama",), + ( + "transformers.models.llama.modeling_llama", + ["LlamaFlashAttention2", "LlamaAttention"], + ), + ("transformers.trainer",), + ("transformers", ["Trainer"]), ("transformers.loss.loss_utils",), ] for module_name_tuple in modules_to_reset: module_name = module_name_tuple[0] - module = importlib.import_module(module_name) - sys.modules[module_name] = module - importlib.reload(sys.modules[module_name]) + + spec = importlib.util.spec_from_file_location( + module_name, sys.modules[module_name].__file__ + ) + sys.modules[module_name] = importlib.util.module_from_spec(spec) + spec.loader.exec_module(sys.modules[module_name]) + + sys.modules[module_name] = importlib.reload(sys.modules[module_name]) if len(module_name_tuple) > 1: module_globals = module_name_tuple[1] for module_global in module_globals: diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py index 82801eedce..a74813e3a0 100644 --- a/tests/e2e/integrations/test_cut_cross_entropy.py +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -71,7 +71,11 @@ def test_llama_w_cce(self, min_cfg, temp_dir): @pytest.mark.parametrize( "attention_type", - ["flash_attention", "sdp_attention", "xformers_attention"], + [ + "flash_attention", + "sdp_attention", + # "xformers_attention", + ], ) def test_llama_w_cce_and_attention(self, min_cfg, temp_dir, attention_type): cfg = DictDefault( diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index cad0438013..7135ad805e 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -54,7 +54,7 @@ def test_lora_ddp(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 5, + "max_steps": 2, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -91,7 +91,7 @@ def test_lora_ddp(self, temp_dir): @pytest.mark.parametrize( "gradient_accumulation_steps", - [1, 4], + [1, 2], ) def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): # pylint: disable=duplicate-code @@ -118,8 +118,8 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): }, ], "num_epochs": 1, - "max_steps": 5, - "micro_batch_size": 4, + "max_steps": 2, + "micro_batch_size": 1, "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, @@ -191,7 +191,7 @@ def test_dpo_lora_ddp(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 5, + "max_steps": 2, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -265,8 +265,8 @@ def test_dpo_qlora_ddp(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 5, - "micro_batch_size": 4, + "max_steps": 2, + "micro_batch_size": 2, "gradient_accumulation_steps": 4, "output_dir": temp_dir, "warmup_steps": 0, @@ -303,7 +303,7 @@ def test_dpo_qlora_ddp(self, temp_dir): @pytest.mark.parametrize( "gradient_accumulation_steps", - [1, 4], + [1, 2], ) def test_fsdp(self, temp_dir, gradient_accumulation_steps): # pylint: disable=duplicate-code @@ -322,8 +322,8 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): }, ], "num_epochs": 1, - "max_steps": 5, - "micro_batch_size": 4, + "max_steps": 2, + "micro_batch_size": 2, "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, @@ -394,7 +394,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): }, ], "num_epochs": 1, - "max_steps": 5, + "max_steps": 2, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -475,7 +475,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 5, + "max_steps": 2, "micro_batch_size": 4, "gradient_accumulation_steps": 4, "output_dir": temp_dir, @@ -526,14 +526,14 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): @pytest.mark.parametrize( "gradient_accumulation_steps", - [1, 4], + [1, 2], ) @pytest.mark.parametrize( "deepspeed", [ "deepspeed_configs/zero3_bf16.json", "deepspeed_configs/zero3_bf16_cpuoffload_all.json", - "deepspeed_configs/zero3_bf16_cpuoffload_params.json", + # "deepspeed_configs/zero3_bf16_cpuoffload_params.json", ], ) @pytest.mark.parametrize( @@ -572,8 +572,8 @@ def test_ds_zero3_packed( }, ], "num_epochs": 1, - "max_steps": 5, - "micro_batch_size": 2, + "max_steps": 2, + "micro_batch_size": 1, "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, @@ -611,7 +611,7 @@ def test_ds_zero3_packed( @pytest.mark.parametrize( "gradient_accumulation_steps", - [1, 4], + [1, 2], ) @pytest.mark.parametrize( "qlora", @@ -647,8 +647,8 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): }, ], "num_epochs": 1, - "max_steps": 5, - "micro_batch_size": 2, + "max_steps": 2, + "micro_batch_size": 1, "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, @@ -686,7 +686,7 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): @pytest.mark.parametrize( "gradient_accumulation_steps", - [1, 4], + [1, 2], ) @pytest.mark.parametrize( "qlora", @@ -722,8 +722,8 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): }, ], "num_epochs": 1, - "max_steps": 5, - "micro_batch_size": 2, + "max_steps": 2, + "micro_batch_size": 1, "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index effcb39c7d..183843b7b1 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -4,7 +4,6 @@ import logging import os -from importlib import reload from pathlib import Path import pytest @@ -22,14 +21,6 @@ os.environ["WANDB_DISABLED"] = "true" -@pytest.fixture(autouse=True) -def reload_transformers(): - import transformers.models.llama.modeling_llama - - yield - reload(transformers.models.llama.modeling_llama) - - class TestFAXentropyLlama: """ Test case for Llama models using LoRA w multipack diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index e662e340b6..36b7442d9e 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -7,6 +7,7 @@ import unittest from pathlib import Path +import pytest from transformers.utils import is_torch_bf16_gpu_available from axolotl.cli import load_datasets @@ -21,6 +22,7 @@ os.environ["WANDB_DISABLED"] = "true" +@pytest.mark.skip("FIXME, mostly underused functionality") class TestFusedLlama(unittest.TestCase): """ Test case for Llama models using Fused layers From 34d3c8dcfb3db152fce6b7eae7e9f6a60be14ce3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 9 Dec 2024 14:03:19 -0500 Subject: [PATCH 0332/1405] [docs] Update README Quickstart to use CLI (#2137) * update quickstart for new CLI * add blurb about bleeding edge builds * missed a yaml reference * prefer lora over qlora for examples * fix commands for parity with previous instructions * consistency on pip/pip3 install * one more parity pip=>pip3 * remove extraneous options in example yaml Co-authored-by: NanoCode012 * update copy * update badges and for discord and socials in readme * Fix a few broken links * bump version to 0.6.0 for release --------- Co-authored-by: NanoCode012 --- README.md | 94 ++++++++++++++++++++++------------- examples/llama-3/lora-1b.yml | 74 +++++++++++++++++++++++++++ examples/llama-3/qlora-1b.yml | 3 +- src/axolotl/__init__.py | 2 +- 4 files changed, 136 insertions(+), 37 deletions(-) create mode 100644 examples/llama-3/lora-1b.yml diff --git a/README.md b/README.md index 75e7faa642..d4e48191e6 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,13 @@ GitHub License tests Releases +
+ contributors GitHub Repo stars -

-

+
+ discord + twitter +
tests-nightly multigpu-semi-weekly tests

@@ -42,7 +46,8 @@ Features: - [Axolotl](#axolotl) - [Table of Contents](#table-of-contents) - [Quickstart ⚡](#quickstart-) - - [Usage](#usage) + - [Edge Builds](#edge-builds-) + - [Axolotl CLI Usage](#axolotl-cli-usage) - [Badge ❤🏷️](#badge-️) - [Contributing 🤝](#contributing-) - [Sponsors 🤝❤](#sponsors-) @@ -107,58 +112,49 @@ Get started with Axolotl in just a few steps! This quickstart guide will walk yo **Requirements**: *Nvidia* GPU (Ampere architecture or newer for `bf16` and Flash Attention) or *AMD* GPU, Python >=3.10 and PyTorch >=2.3.1. ```bash -git clone https://github.com/axolotl-ai-cloud/axolotl -cd axolotl +pip3 install axolotl[flash-attn,deepspeed] -pip3 install packaging ninja -pip3 install -e '.[flash-attn,deepspeed]' +# download examples and optionally deepspeed configs to the local path +axolotl fetch examples +axolotl fetch deepspeed_configs # OPTIONAL + +# finetune using lora +axolotl train examples/llama-3/lora-1b.yml ``` -### Usage -```bash -# preprocess datasets - optional but recommended -CUDA_VISIBLE_DEVICES="0" python -m axolotl.cli.preprocess examples/openllama-3b/lora.yml +### Edge Builds 🏎️ -# finetune lora -accelerate launch -m axolotl.cli.train examples/openllama-3b/lora.yml +If you're looking for the latest features and updates between releases, you'll need to install +from source. -# inference -accelerate launch -m axolotl.cli.inference examples/openllama-3b/lora.yml \ - --lora_model_dir="./outputs/lora-out" - -# gradio -accelerate launch -m axolotl.cli.inference examples/openllama-3b/lora.yml \ - --lora_model_dir="./outputs/lora-out" --gradio - -# remote yaml files - the yaml config can be hosted on a public URL -# Note: the yaml config must directly link to the **raw** yaml -accelerate launch -m axolotl.cli.train https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/openllama-3b/lora.yml +```bash +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl +pip3 install packaging ninja +pip3 install -e '.[flash-attn,deepspeed]' ``` -### Axolotl CLI - -If you've installed this package using `pip` from source, we now support a new, more -streamlined CLI using [click](https://click.palletsprojects.com/en/stable/). Rewriting -the above commands: +### Axolotl CLI Usage +We now support a new, more streamlined CLI using [click](https://click.palletsprojects.com/en/stable/). ```bash # preprocess datasets - optional but recommended -CUDA_VISIBLE_DEVICES="0" axolotl preprocess examples/openllama-3b/lora.yml +CUDA_VISIBLE_DEVICES="0" axolotl preprocess examples/llama-3/lora-1b.yml # finetune lora -axolotl train examples/openllama-3b/lora.yml +axolotl train examples/llama-3/lora-1b.yml # inference -axolotl inference examples/openllama-3b/lora.yml \ +axolotl inference examples/llama-3/lora-1b.yml \ --lora-model-dir="./outputs/lora-out" # gradio -axolotl inference examples/openllama-3b/lora.yml \ +axolotl inference examples/llama-3/lora-1b.yml \ --lora-model-dir="./outputs/lora-out" --gradio # remote yaml files - the yaml config can be hosted on a public URL # Note: the yaml config must directly link to the **raw** yaml -axolotl train https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/openllama-3b/lora.yml +axolotl train https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/llama-3/lora-1b.yml ``` We've also added a new command for fetching `examples` and `deepspeed_configs` to your @@ -175,6 +171,36 @@ axolotl fetch deepspeed_configs axolotl fetch examples --dest path/to/folder ``` +### Legacy Usage +
+ +Click to Expand + +While the Axolotl CLI is the preferred method for interacting with axolotl, we +still support the legacy `-m axolotl.cli.*` usage. + +```bash +# preprocess datasets - optional but recommended +CUDA_VISIBLE_DEVICES="0" python -m axolotl.cli.preprocess examples/llama-3/lora-1b.yml + +# finetune lora +accelerate launch -m axolotl.cli.train examples/llama-3/lora-1b.yml + +# inference +accelerate launch -m axolotl.cli.inference examples/llama-3/lora-1b.yml \ + --lora_model_dir="./outputs/lora-out" + +# gradio +accelerate launch -m axolotl.cli.inference examples/llama-3/lora-1b.yml \ + --lora_model_dir="./outputs/lora-out" --gradio + +# remote yaml files - the yaml config can be hosted on a public URL +# Note: the yaml config must directly link to the **raw** yaml +accelerate launch -m axolotl.cli.train https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/llama-3/lora-1b.yml +``` + +
+ ## Badge ❤🏷️ Building something cool with Axolotl? Consider adding a badge to your model card. diff --git a/examples/llama-3/lora-1b.yml b/examples/llama-3/lora-1b.yml new file mode 100644 index 0000000000..bdda2ed75e --- /dev/null +++ b/examples/llama-3/lora-1b.yml @@ -0,0 +1,74 @@ +base_model: NousResearch/Llama-3.2-1B + +load_in_8bit: false +load_in_4bit: false +strict: false + +datasets: + - path: teknium/GPT4-LLM-Cleaned + type: alpaca +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: lora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: true +pad_to_sequence_len: true + +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.05 +lora_fan_in_fan_out: +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +loss_watchdog_threshold: 5.0 +loss_watchdog_patience: 3 + +warmup_steps: 10 +evals_per_epoch: 4 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: + pad_token: "<|end_of_text|>" diff --git a/examples/llama-3/qlora-1b.yml b/examples/llama-3/qlora-1b.yml index fdfe4aa7c8..be40db846e 100644 --- a/examples/llama-3/qlora-1b.yml +++ b/examples/llama-3/qlora-1b.yml @@ -1,4 +1,4 @@ -base_model: meta-llama/Llama-3.2-1B +base_model: NousResearch/Llama-3.2-1B load_in_8bit: false load_in_4bit: true @@ -22,7 +22,6 @@ pad_to_sequence_len: true lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_linear: true lora_fan_in_fan_out: lora_target_modules: - gate_proj diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index b4f1d06f7b..9a31745983 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -1,3 +1,3 @@ """Axolotl - Train and fine-tune large language models""" -__version__ = "0.5.3.dev0" +__version__ = "0.6.0" From 9001859b0b54cb3fc450fecdc0d0c1df4cba887e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 9 Dec 2024 14:12:45 -0500 Subject: [PATCH 0333/1405] fix release command (#2163) [skip ci] --- .github/workflows/pypi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 752ffefdfe..f6b897f76f 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -16,7 +16,7 @@ jobs: - name: Create release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release create "$GITHUB_REF_NAME" # GITHUB_REF_NAME is the tag name in `on.push.tags` workflows + run: gh release create ${GITHUB_REF#refs/tags/} pypi-publish: name: Upload release to PyPI runs-on: ubuntu-latest From 6aa31b44c6b96966ad330b34a84bca080c579c38 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 9 Dec 2024 14:20:16 -0500 Subject: [PATCH 0334/1405] make sure to checkout tag before creating release (#2164) --- .github/workflows/pypi.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index f6b897f76f..1be8b2cbf5 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -13,10 +13,13 @@ jobs: permissions: contents: write steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Create release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release create ${GITHUB_REF#refs/tags/} + run: gh release create "$GITHUB_REF_NAME" --generate-notes pypi-publish: name: Upload release to PyPI runs-on: ubuntu-latest From d009ead1011ebbd71701974467206e154413925f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 10 Dec 2024 16:25:25 -0500 Subject: [PATCH 0335/1405] fix build w pyproject to respect insalled torch version (#2168) * fix build w pyproject to respect insalled torch version * include in manifest * disable duplicate code check for now * move parser so it can be found * add checks for correct pytorch version so this doesn't slip by again --- .github/workflows/pypi.yml | 2 +- .github/workflows/tests-nightly.yml | 6 +- .github/workflows/tests.yml | 16 ++- MANIFEST.in | 1 + README.md | 8 +- cicd/Dockerfile.jinja | 4 +- cicd/cicd.sh | 2 + docker/Dockerfile | 4 +- docker/Dockerfile-tests | 4 +- docs/amd_hpc.qmd | 4 +- docs/debugging.qmd | 4 +- .../colab-axolotl-example.ipynb | 2 +- pyproject.toml | 7 ++ scripts/motd | 2 +- ...setuptools_axolotl_dynamic_dependencies.py | 104 ++++++++++++++++++ 15 files changed, 148 insertions(+), 22 deletions(-) create mode 100644 src/setuptools_axolotl_dynamic_dependencies.py diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 1be8b2cbf5..a98662c388 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -41,7 +41,7 @@ jobs: - name: Install dependencies run: | pip3 install wheel packaging - pip3 install -e . + pip3 install --no-build-isolation -e . pip3 install -r requirements-dev.txt -r requirements-tests.txt - name: Extract tag name diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index f3e5530cb8..7d83dd8e8d 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -60,11 +60,15 @@ jobs: run: | pip3 install --upgrade pip pip3 install --upgrade packaging - pip3 install -U -e . + pip3 install --no-build-isolation -U -e . python scripts/unsloth_install.py | sh python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt + - name: Make sure PyTorch version wasn't clobbered + run: | + python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" + - name: Ensure axolotl CLI was installed run: | axolotl --help diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 88172fdd5a..4a9c33c931 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -78,11 +78,15 @@ jobs: - name: Install dependencies run: | pip3 show torch - pip3 install -U -e . + pip3 install --no-build-isolation -U -e . python scripts/unsloth_install.py | sh python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt + - name: Make sure PyTorch version wasn't clobbered + run: | + python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" + - name: Ensure axolotl CLI was installed run: | axolotl --help @@ -120,7 +124,7 @@ jobs: - name: upgrade pip run: | pip3 install --upgrade pip - pip3 install --upgrade packaging setuptools wheel + pip3 install --upgrade packaging setuptools setuptools_scm build wheel - name: Install PyTorch run: | @@ -129,12 +133,16 @@ jobs: - name: Install dependencies run: | pip3 show torch - python3 setup.py sdist - pip3 install dist/axolotl*.tar.gz + python -m build --no-isolation --sdist + pip3 install --no-build-isolation dist/axolotl*.tar.gz python scripts/unsloth_install.py | sh python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt + - name: Make sure PyTorch version wasn't clobbered + run: | + python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" + - name: Ensure axolotl CLI was installed run: | axolotl --help diff --git a/MANIFEST.in b/MANIFEST.in index 215fa9a9d3..99324be3cd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include requirements.txt include README.md include LICENSE +include src/setuptools_axolotl_dynamic_dependencies.py recursive-include axolotl *.py diff --git a/README.md b/README.md index d4e48191e6..6afcaf1469 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Get started with Axolotl in just a few steps! This quickstart guide will walk yo **Requirements**: *Nvidia* GPU (Ampere architecture or newer for `bf16` and Flash Attention) or *AMD* GPU, Python >=3.10 and PyTorch >=2.3.1. ```bash -pip3 install axolotl[flash-attn,deepspeed] +pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] # download examples and optionally deepspeed configs to the local path axolotl fetch examples @@ -131,7 +131,7 @@ from source. git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl pip3 install packaging ninja -pip3 install -e '.[flash-attn,deepspeed]' +pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' ``` ### Axolotl CLI Usage @@ -320,7 +320,7 @@ docker run --privileged --gpus '"all"' --shm-size 10g --rm -it --name axolotl -- 3. Install Axolotl along with python dependencies ```bash pip3 install packaging - pip3 install -e '.[flash-attn,deepspeed]' + pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' ``` 4. (Optional) Login to Huggingface to use gated models/datasets. ```bash @@ -399,7 +399,7 @@ Please use WSL or Docker! Use the below instead of the install method in QuickStart. ``` -pip3 install -e '.' +pip3 install --no-build-isolation -e '.' ``` More info: [mac.md](/docs/mac.qmd) diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 4d1c4e664b..ed64664166 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -31,9 +31,9 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ fi RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install -e .[deepspeed,flash-attn,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - pip install -e .[deepspeed,flash-attn,optimizers] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers] $AXOLOTL_ARGS; \ fi RUN python scripts/unsloth_install.py | sh diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 1ead6d518e..88defdd0e4 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -1,6 +1,8 @@ #!/bin/bash set -e +python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__" + pytest -v --durations=10 -n8 --ignore=tests/e2e/ --ignore=tests/patched/ /workspace/axolotl/tests/ # pytest -v --durations=10 -n8 --dist loadfile /workspace/axolotl/tests/patched/ pytest -v --durations=10 -n1 --dist loadfile /workspace/axolotl/tests/e2e/patched/ diff --git a/docker/Dockerfile b/docker/Dockerfile index 261f0e12ad..6b6baf7515 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,9 +20,9 @@ WORKDIR /workspace/axolotl # If AXOLOTL_EXTRAS is set, append it in brackets RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install -e .[deepspeed,flash-attn,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - pip install -e .[deepspeed,flash-attn,optimizers] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers] $AXOLOTL_ARGS; \ fi RUN python scripts/unsloth_install.py | sh diff --git a/docker/Dockerfile-tests b/docker/Dockerfile-tests index 09efeb6199..8d97343597 100644 --- a/docker/Dockerfile-tests +++ b/docker/Dockerfile-tests @@ -24,9 +24,9 @@ RUN git fetch origin +$GITHUB_REF && \ # If AXOLOTL_EXTRAS is set, append it in brackets RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install -e .[deepspeed,flash-attn,mamba-ssm,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,mamba-ssm,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - pip install -e .[deepspeed,flash-attn,mamba-ssm] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,mamba-ssm] $AXOLOTL_ARGS; \ fi # So we can test the Docker image diff --git a/docs/amd_hpc.qmd b/docs/amd_hpc.qmd index d1c274e15a..70fbe88ee3 100644 --- a/docs/amd_hpc.qmd +++ b/docs/amd_hpc.qmd @@ -52,7 +52,7 @@ export GPU_ARCHS="gfx90a" cd flash-attention export PYTHON_SITE_PACKAGES=$(python -c 'import site; print(site.getsitepackages()[0])') patch "${PYTHON_SITE_PACKAGES}/torch/utils/hipify/hipify_python.py" hipify_patch.patch -pip install . +pip install --no-build-isolation . ``` ### 6. Install Axolotl @@ -63,7 +63,7 @@ Clone and install Axolotl: git clone https://github.com/axolotl-ai-cloud/axolotl cd axolotl pip install packaging ninja -pip install -e . +pip install --no-build-isolation -e . ``` ### 7. Apply xformers Workaround diff --git a/docs/debugging.qmd b/docs/debugging.qmd index d119dce18f..4eaa609272 100644 --- a/docs/debugging.qmd +++ b/docs/debugging.qmd @@ -71,7 +71,7 @@ Make sure you have an [editable install](https://setuptools.pypa.io/en/latest/us ```bash pip3 install packaging -pip3 install -e '.[flash-attn,deepspeed]' +pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' ``` #### Remote Hosts @@ -212,7 +212,7 @@ You will now be in the container. Next, perform an editable install of Axolotl: ```bash pip3 install packaging -pip3 install -e '.[flash-attn,deepspeed]' +pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' ``` ### Attach To Container diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index bfd1709a7f..0b373c28cf 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -24,7 +24,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install axolotl[deepspeed]" + "!pip install --no-build-isolation axolotl[deepspeed]" ] }, { diff --git a/pyproject.toml b/pyproject.toml index 6805e2e48f..016dbe4171 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,3 +17,10 @@ Homepage = "https://axolotl-ai-cloud.github.io/axolotl/" Repository = "https://github.com/axolotl-ai-cloud/axolotl.git" [tool.setuptools_scm] + +[tool.setuptools] +py-modules = ["setuptools_axolotl_dynamic_dependencies"] +include-package-data = true + +[tool.setuptools.cmdclass] +build_py = "setuptools_axolotl_dynamic_dependencies.BuildPyCommand" diff --git a/scripts/motd b/scripts/motd index 9d9edbd6d6..b3ffa165e6 100644 --- a/scripts/motd +++ b/scripts/motd @@ -13,5 +13,5 @@ cd /workspace rm -rf /workspace/axolotl git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -pip install --no-deps -e . +pip install --no-build-isolation --no-deps -e . ``` diff --git a/src/setuptools_axolotl_dynamic_dependencies.py b/src/setuptools_axolotl_dynamic_dependencies.py new file mode 100644 index 0000000000..2f10efde68 --- /dev/null +++ b/src/setuptools_axolotl_dynamic_dependencies.py @@ -0,0 +1,104 @@ +""" +dynamic requirements for axolotl +""" +import platform +import re +from importlib.metadata import PackageNotFoundError, version + +from setuptools.command.build_py import build_py as _build_py + + +# pylint: disable=duplicate-code +def parse_requirements(): + _install_requires = [] + _dependency_links = [] + with open("./requirements.txt", encoding="utf-8") as requirements_file: + lines = [r.strip() for r in requirements_file.readlines()] + for line in lines: + is_extras = ( + "flash-attn" in line + or "flash-attention" in line + or "deepspeed" in line + or "mamba-ssm" in line + or "lion-pytorch" in line + ) + if line.startswith("--extra-index-url"): + # Handle custom index URLs + _, url = line.split() + _dependency_links.append(url) + elif not is_extras and line and line[0] != "#": + # Handle standard packages + _install_requires.append(line) + + try: + xformers_version = [req for req in _install_requires if "xformers" in req][0] + torchao_version = [req for req in _install_requires if "torchao" in req][0] + autoawq_version = [req for req in _install_requires if "autoawq" in req][0] + + if "Darwin" in platform.system(): + # don't install xformers on MacOS + _install_requires.pop(_install_requires.index(xformers_version)) + else: + # detect the version of torch already installed + # and set it so dependencies don't clobber the torch version + try: + torch_version = version("torch") + except PackageNotFoundError: + torch_version = "2.5.1" + _install_requires.append(f"torch=={torch_version}") + + version_match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", torch_version) + if version_match: + major, minor, patch = version_match.groups() + major, minor = int(major), int(minor) + patch = ( + int(patch) if patch is not None else 0 + ) # Default patch to 0 if not present + else: + raise ValueError("Invalid version format") + + if (major, minor) >= (2, 5): + _install_requires.pop(_install_requires.index(xformers_version)) + if patch == 0: + _install_requires.append("xformers==0.0.28.post2") + else: + _install_requires.append("xformers==0.0.28.post3") + _install_requires.pop(_install_requires.index(autoawq_version)) + elif (major, minor) >= (2, 4): + if patch == 0: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.27") + else: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers==0.0.28.post1") + elif (major, minor) >= (2, 3): + _install_requires.pop(_install_requires.index(torchao_version)) + if patch == 0: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.26.post1") + else: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.27") + elif (major, minor) >= (2, 2): + _install_requires.pop(_install_requires.index(torchao_version)) + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.25.post1") + else: + _install_requires.pop(_install_requires.index(torchao_version)) + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.23.post1") + + except PackageNotFoundError: + pass + return _install_requires, _dependency_links + + +class BuildPyCommand(_build_py): + """ + custom build_py command to parse dynamic requirements + """ + + def finalize_options(self): + super().finalize_options() + install_requires, _ = parse_requirements() + self.distribution.install_requires = install_requires From 78a4aa86d6279f328a331259a6c387ce1ff64bac Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 11 Dec 2024 20:14:24 -0500 Subject: [PATCH 0336/1405] evaluation_strategy was fully deprecated in recent release (#2169) [skip ci] --- src/axolotl/utils/callbacks/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 6bf433319e..641c9b1623 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -66,10 +66,7 @@ def on_step_end( control: TrainerControl, **kwargs, ): - if ( - args.evaluation_strategy == IntervalStrategy.STEPS - and state.global_step == 1 - ): + if args.eval_strategy == IntervalStrategy.STEPS and state.global_step == 1: control.should_evaluate = True return control From 02629c7cdfa5bef9cea41cf980921e4b93adc39f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 11 Dec 2024 20:14:55 -0500 Subject: [PATCH 0337/1405] parity for nightly ci - make sure to install setuptools (#2176) [skip ci] --- .github/workflows/tests-nightly.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 7d83dd8e8d..3ee12a7098 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -44,6 +44,11 @@ jobs: python-version: ${{ matrix.python_version }} cache: 'pip' # caching pip dependencies + - name: upgrade pip + run: | + pip3 install --upgrade pip + pip3 install --upgrade packaging setuptools wheel + - name: Install PyTorch run: | pip3 install torch==${{ matrix.pytorch_version }} --index-url https://download.pytorch.org/whl/cpu From effc4dc4097af212432c9ebaba7eb9677d768467 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 12 Dec 2024 20:17:12 -0500 Subject: [PATCH 0338/1405] pin to 4.47.0 (#2180) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 361524e561..d0942193f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ liger-kernel==0.4.2 packaging==23.2 peft==0.14.0 -transformers>=4.46.3 +transformers==4.47.0 tokenizers>=0.20.1 accelerate==1.2.0 datasets==3.1.0 From 33090486d77f849f815e788eeda702f789a873e6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 16 Dec 2024 12:38:43 -0500 Subject: [PATCH 0339/1405] [feature] add pytorch profiling (#2182) * add pytorch profiling * kick off the profiler asap since things may get allcoated before train start * document feature * add url for visualizer [skip ci] --- docs/config.qmd | 4 ++ src/axolotl/core/trainer_builder.py | 8 ++++ src/axolotl/utils/callbacks/profiler.py | 43 +++++++++++++++++++ .../config/models/input/v0_4_1/__init__.py | 1 + 4 files changed, 56 insertions(+) create mode 100644 src/axolotl/utils/callbacks/profiler.py diff --git a/docs/config.qmd b/docs/config.qmd index f01a2ce267..120aec8933 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -363,6 +363,10 @@ eval_table_size: # Approximate number of predictions sent to wandb depending on eval_max_new_tokens: # Total number of tokens generated for predictions sent to wandb. Default is 128 eval_causal_lm_metrics: # HF evaluate metrics used during evaluation. Default is ["sacrebleu", "comet", "ter", "chrf", "perplexity"] +profiler_steps: # enable the pytorch profiler to capture the first N steps of training to the output_dir. + # see https://pytorch.org/blog/understanding-gpu-memory-1/ for more information + # snapshots can be visualized @ https://pytorch.org/memory_viz + loss_watchdog_threshold: # High loss value, indicating the learning has broken down (a good estimate is ~2 times the loss at the start of training) loss_watchdog_patience: # Number of high-loss steps in a row before the trainer aborts (default: 3) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 73d9e0e655..0f30f511c2 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -65,6 +65,7 @@ log_prediction_callback_factory, ) from axolotl.utils.callbacks.lisa import lisa_callback_factory +from axolotl.utils.callbacks.profiler import PytorchProfilerCallback from axolotl.utils.chat_templates import get_chat_template from axolotl.utils.collators import ( BatchSamplerDataCollatorForSeq2Seq, @@ -1363,6 +1364,13 @@ def get_callbacks(self) -> List[TrainerCallback]: plugin_manager.add_callbacks_pre_trainer(cfg=self.cfg, model=self.model) ) + if self.cfg.profiler_steps: + callbacks.append( + PytorchProfilerCallback( + steps_to_profile=self.cfg.profiler_steps, + ) + ) + if self.cfg.use_wandb: callbacks.append( SaveAxolotlConfigtoWandBCallback(self.cfg.axolotl_config_path) diff --git a/src/axolotl/utils/callbacks/profiler.py b/src/axolotl/utils/callbacks/profiler.py new file mode 100644 index 0000000000..8616963323 --- /dev/null +++ b/src/axolotl/utils/callbacks/profiler.py @@ -0,0 +1,43 @@ +""" +HF Trainer callback for creating pytorch profiling snapshots +""" +from pathlib import Path +from pickle import dump # nosec B403 + +import torch +from transformers import ( + TrainerCallback, + TrainerControl, + TrainerState, + TrainingArguments, +) + + +class PytorchProfilerCallback(TrainerCallback): + """ + PyTorch Profiler callback to create snapshots of GPU memory usage at specified steps. + """ + + def __init__(self, steps_to_profile: int = 5): + self.steps_to_profile = steps_to_profile + if self.steps_to_profile: + torch.cuda.memory._record_memory_history( # pylint: disable=protected-access + enabled="all" + ) + + def on_step_end( # pylint: disable=unused-argument + self, + args: TrainingArguments, # pylint: disable=unused-argument + state: TrainerState, + control: TrainerControl, # pylint: disable=unused-argument + **kwargs, # pylint: disable=unused-argument + ): + if state.global_step == self.steps_to_profile: + snapshot = torch.cuda.memory._snapshot() # pylint: disable=protected-access + with open(Path(args.output_dir) / "snapshot.pickle", "wb") as fout: + dump(snapshot, fout) + + # tell CUDA to stop recording memory allocations now + torch.cuda.memory._record_memory_history( # pylint: disable=protected-access + enabled=None + ) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 3671e1bb93..d05de2330d 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -762,6 +762,7 @@ class Config: load_best_model_at_end: Optional[bool] = False save_only_model: Optional[bool] = False use_tensorboard: Optional[bool] = None + profiler_steps: Optional[int] = None neftune_noise_alpha: Optional[float] = None From f865464ae53f11ca272b0c243197c046675af75c Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 16 Dec 2024 15:46:31 -0500 Subject: [PATCH 0340/1405] Basic evaluate CLI command / codepath (#2188) * basic evaluate CLI command / codepath * tests for evaluate CLI command * fixes and cleanup * review comments; slightly DRYing up things --------- Co-authored-by: Dan Saunders --- src/axolotl/cli/evaluate.py | 52 ++++++++++ src/axolotl/cli/main.py | 27 +++++- src/axolotl/common/cli.py | 25 +++-- src/axolotl/evaluate.py | 168 +++++++++++++++++++++++++++++++++ src/axolotl/train.py | 19 ++-- src/axolotl/utils/data/sft.py | 3 + src/axolotl/utils/trainer.py | 11 +++ tests/cli/test_cli_base.py | 73 ++++++++++++++ tests/cli/test_cli_evaluate.py | 67 +++++++++++++ tests/cli/test_cli_train.py | 161 +++++++++++++------------------ 10 files changed, 494 insertions(+), 112 deletions(-) create mode 100644 src/axolotl/cli/evaluate.py create mode 100644 src/axolotl/evaluate.py create mode 100644 tests/cli/test_cli_base.py create mode 100644 tests/cli/test_cli_evaluate.py diff --git a/src/axolotl/cli/evaluate.py b/src/axolotl/cli/evaluate.py new file mode 100644 index 0000000000..8e99d6f4b1 --- /dev/null +++ b/src/axolotl/cli/evaluate.py @@ -0,0 +1,52 @@ +""" +CLI to run training on a model +""" +import logging +from pathlib import Path +from typing import Union + +import fire +from dotenv import load_dotenv +from transformers.hf_argparser import HfArgumentParser + +from axolotl.cli import ( + check_accelerate_default_config, + check_user_token, + load_cfg, + load_datasets, + load_rl_datasets, + print_axolotl_text_art, +) +from axolotl.common.cli import TrainerCliArgs +from axolotl.evaluate import evaluate + +LOG = logging.getLogger("axolotl.cli.evaluate") + + +def do_evaluate(cfg, cli_args) -> None: + # pylint: disable=duplicate-code + print_axolotl_text_art() + check_accelerate_default_config() + check_user_token() + + if cfg.rl: # and cfg.rl != "orpo": + dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + else: + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + evaluate(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + + +def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: + # pylint: disable=duplicate-code + parsed_cfg = load_cfg(config, **kwargs) + parser = HfArgumentParser(TrainerCliArgs) + parsed_cli_args, _ = parser.parse_args_into_dataclasses( + return_remaining_strings=True + ) + do_evaluate(parsed_cfg, parsed_cli_args) + + +if __name__ == "__main__": + load_dotenv() + fire.Fire(do_cli) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index a77410776a..ec7f5b6947 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -12,7 +12,7 @@ build_command, fetch_from_github, ) -from axolotl.common.cli import PreprocessCliArgs, TrainerCliArgs +from axolotl.common.cli import EvaluateCliArgs, PreprocessCliArgs, TrainerCliArgs from axolotl.utils.config.models.input.v0_4_1 import AxolotlInputConfig @@ -60,6 +60,31 @@ def train(config: str, accelerate: bool, **kwargs): do_cli(config=config, **kwargs) +@cli.command() +@click.argument("config", type=click.Path(exists=True, path_type=str)) +@click.option( + "--accelerate/--no-accelerate", + default=True, + help="Use accelerate launch for multi-GPU training", +) +@add_options_from_dataclass(EvaluateCliArgs) +@add_options_from_config(AxolotlInputConfig) +def evaluate(config: str, accelerate: bool, **kwargs): + """Evaluate a model.""" + kwargs = {k: v for k, v in kwargs.items() if v is not None} + + if accelerate: + base_cmd = ["accelerate", "launch", "-m", "axolotl.cli.evaluate"] + if config: + base_cmd.append(config) + cmd = build_command(base_cmd, kwargs) + subprocess.run(cmd, check=True) # nosec B603 + else: + from axolotl.cli.evaluate import do_cli + + do_cli(config=config, **kwargs) + + @cli.command() @click.argument("config", type=click.Path(exists=True, path_type=str)) @click.option( diff --git a/src/axolotl/common/cli.py b/src/axolotl/common/cli.py index 6a3a22e637..02ad9201b8 100644 --- a/src/axolotl/common/cli.py +++ b/src/axolotl/common/cli.py @@ -15,6 +15,19 @@ LOG = logging.getLogger("axolotl.common.cli") +@dataclass +class PreprocessCliArgs: + """ + dataclass representing arguments for preprocessing only + """ + + debug: bool = field(default=False) + debug_text_only: bool = field(default=False) + debug_num_examples: int = field(default=1) + prompter: Optional[str] = field(default=None) + download: Optional[bool] = field(default=True) + + @dataclass class TrainerCliArgs: """ @@ -31,16 +44,14 @@ class TrainerCliArgs: @dataclass -class PreprocessCliArgs: +class EvaluateCliArgs: """ - dataclass representing arguments for preprocessing only + dataclass representing the various evaluation arguments """ debug: bool = field(default=False) debug_text_only: bool = field(default=False) - debug_num_examples: int = field(default=1) - prompter: Optional[str] = field(default=None) - download: Optional[bool] = field(default=True) + debug_num_examples: int = field(default=0) def load_model_and_tokenizer( @@ -50,7 +61,9 @@ def load_model_and_tokenizer( ): LOG.info(f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}") tokenizer = load_tokenizer(cfg) + LOG.info("loading model and (optionally) peft_config...") - model, _ = load_model(cfg, tokenizer, inference=cli_args.inference) + inference = getattr(cli_args, "inference", False) + model, _ = load_model(cfg, tokenizer, inference=inference) return model, tokenizer diff --git a/src/axolotl/evaluate.py b/src/axolotl/evaluate.py new file mode 100644 index 0000000000..7fd60cb5f4 --- /dev/null +++ b/src/axolotl/evaluate.py @@ -0,0 +1,168 @@ +"""Module for evaluating models.""" + +import csv +import os +import sys +from pathlib import Path +from typing import Dict, Optional + +import torch +from accelerate.logging import get_logger + +from axolotl.common.cli import TrainerCliArgs +from axolotl.logging_config import configure_logging +from axolotl.train import TrainDatasetMeta +from axolotl.utils.dict import DictDefault +from axolotl.utils.models import load_model, load_processor, load_tokenizer +from axolotl.utils.trainer import set_pytorch_cuda_alloc_conf, setup_trainer + +project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +src_dir = os.path.join(project_root, "src") +sys.path.insert(0, src_dir) + +configure_logging() +LOG = get_logger("axolotl.evaluate") + + +def evaluate_dataset( + trainer, dataset, dataset_type: str, flash_optimum: bool = False +) -> Optional[Dict[str, float]]: + """Helper function to evaluate a single dataset safely. + + Args: + trainer: The trainer instance + dataset: Dataset to evaluate + dataset_type: Type of dataset ('train' or 'eval') + flash_optimum: Whether to use flash optimum + + Returns: + Dictionary of metrics or None if dataset is None + """ + if dataset is None: + return None + + LOG.info(f"Starting {dataset_type} set evaluation...") + + if flash_optimum: + with torch.backends.cuda.sdp_kernel( + enable_flash=True, + enable_math=True, + enable_mem_efficient=True, + ): + metrics = trainer.evaluate(dataset, metric_key_prefix=dataset_type) + else: + metrics = trainer.evaluate(dataset, metric_key_prefix=dataset_type) + + LOG.info(f"{dataset_type.capitalize()} set evaluation completed!") + LOG.info(f"{dataset_type.capitalize()} Metrics:") + for key, value in metrics.items(): + LOG.info(f"{key}: {value}") + + return metrics + + +def evaluate( + *, cfg: DictDefault, cli_args: TrainerCliArgs, dataset_meta: TrainDatasetMeta +) -> Dict[str, float]: + """ + Evaluate a model on training and validation datasets + + Args: + cfg: Configuration dictionary + cli_args: Command line arguments + dataset_meta: Dataset metadata containing training and evaluation datasets + + Returns: + Tuple containing: + - The model (either PeftModel or PreTrainedModel) + - The tokenizer + - Dictionary of evaluation metrics + """ + # pylint: disable=duplicate-code + # Enable expandable segments for cuda allocation to improve VRAM usage + set_pytorch_cuda_alloc_conf() + + # Load tokenizer + LOG.debug( + f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}", + main_process_only=True, + ) + tokenizer = load_tokenizer(cfg) + + # Load processor for multimodal models if needed + processor = None + if cfg.is_multimodal: + processor = load_processor(cfg, tokenizer) + + # Get datasets + train_dataset = dataset_meta.train_dataset + eval_dataset = dataset_meta.eval_dataset + total_num_steps = dataset_meta.total_num_steps + + # Load model + LOG.debug("loading model for evaluation...") + model, _ = load_model( + cfg, tokenizer, processor=processor, inference=cli_args.inference + ) + + # Set up trainer + trainer = setup_trainer( + cfg, + train_dataset=train_dataset, + eval_dataset=eval_dataset, + model=(model, None, None), # No need for model_ref or peft_config + tokenizer=tokenizer, + processor=processor, + total_num_steps=total_num_steps, + ) + + # Evaluate datasets + all_metrics = {} + train_metrics = evaluate_dataset(trainer, train_dataset, "train", cfg.flash_optimum) + eval_metrics = evaluate_dataset(trainer, eval_dataset, "eval", cfg.flash_optimum) + + if train_metrics: + all_metrics.update(train_metrics) + if eval_metrics: + all_metrics.update(eval_metrics) + + # Save metrics to CSV if output directory is specified and we have metrics + if cfg.output_dir and (train_metrics or eval_metrics): + output_dir = Path(cfg.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + metrics_file = output_dir / "eval_summary.csv" + with metrics_file.open("w", newline="", encoding="utf-8") as file: + writer = csv.writer(file) + writer.writerow(["metric", "training", "validation"]) + + # Get unique metric names (removing prefixes) from available metrics + train_metric_names = { + k.replace("train_", ""): k for k in (train_metrics or {}) + } + eval_metric_names = { + k.replace("eval_", ""): k for k in (eval_metrics or {}) + } + all_metric_names = sorted( + set(train_metric_names.keys()) | set(eval_metric_names.keys()) + ) + + for metric_name in all_metric_names: + train_value = ( + train_metrics.get(train_metric_names.get(metric_name, ""), "") + if train_metrics + else "" + ) + eval_value = ( + eval_metrics.get(eval_metric_names.get(metric_name, ""), "") + if eval_metrics + else "" + ) + writer.writerow([metric_name, train_value, eval_value]) + + LOG.info(f"Evaluation results saved to {metrics_file}") + + del model + del tokenizer + + return all_metrics diff --git a/src/axolotl/train.py b/src/axolotl/train.py index c8576f1b48..851a71e547 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -24,7 +24,7 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.freeze import freeze_layers_except from axolotl.utils.models import load_model, load_processor, load_tokenizer -from axolotl.utils.trainer import setup_trainer +from axolotl.utils.trainer import set_pytorch_cuda_alloc_conf, setup_trainer try: from optimum.bettertransformer import BetterTransformer @@ -53,25 +53,22 @@ class TrainDatasetMeta: def train( *, cfg: DictDefault, cli_args: TrainerCliArgs, dataset_meta: TrainDatasetMeta ) -> Tuple[Union[PeftModel, PreTrainedModel], PreTrainedTokenizer]: - # enable expandable segments for cuda allocation to improve VRAM usage - torch_version = torch.__version__.split(".") - torch_major, torch_minor = int(torch_version[0]), int(torch_version[1]) - if torch_major == 2 and torch_minor >= 2: - if os.getenv("PYTORCH_CUDA_ALLOC_CONF") is None: - os.environ[ - "PYTORCH_CUDA_ALLOC_CONF" - ] = "expandable_segments:True,roundup_power2_divisions:16" - - # load the tokenizer first + # Enable expandable segments for cuda allocation to improve VRAM usage + set_pytorch_cuda_alloc_conf() + + # Load tokenizer LOG.debug( f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}", main_process_only=True, ) tokenizer = load_tokenizer(cfg) + + # Load processor for multimodal models if needed processor = None if cfg.is_multimodal: processor = load_processor(cfg, tokenizer) + # Get datasets train_dataset = dataset_meta.train_dataset eval_dataset = dataset_meta.eval_dataset total_num_steps = dataset_meta.total_num_steps diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index f56fe8f38c..286e5f2d70 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -119,7 +119,9 @@ def prepare_dataset(cfg, tokenizer, processor=None): eval_dataset = None if cfg.dataset_exact_deduplication: LOG.info("Deduplication not available for pretrained datasets") + return train_dataset, eval_dataset, cfg.max_steps, prompters + if eval_dataset and cfg.sample_packing and cfg.eval_sample_packing is not False: total_eval_steps = calculate_total_num_steps(cfg, eval_dataset, update=False) if total_eval_steps == 0: @@ -134,6 +136,7 @@ def prepare_dataset(cfg, tokenizer, processor=None): LOG.info(f"Maximum number of steps set at {total_num_steps}") else: total_num_steps = calculate_total_num_steps(cfg, train_dataset) + return train_dataset, eval_dataset, total_num_steps, prompters diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 32e54c9a86..fd09b3eb67 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -512,6 +512,17 @@ def prepare_opinionated_env(cfg): os.environ["TOKENIZERS_PARALLELISM"] = "false" +def set_pytorch_cuda_alloc_conf(): + """Set up CUDA allocation config if using PyTorch >= 2.2""" + torch_version = torch.__version__.split(".") + torch_major, torch_minor = int(torch_version[0]), int(torch_version[1]) + if torch_major == 2 and torch_minor >= 2: + if os.getenv("PYTORCH_CUDA_ALLOC_CONF") is None: + os.environ[ + "PYTORCH_CUDA_ALLOC_CONF" + ] = "expandable_segments:True,roundup_power2_divisions:16" + + def setup_trainer( cfg, train_dataset, eval_dataset, model, tokenizer, processor, total_num_steps ): diff --git a/tests/cli/test_cli_base.py b/tests/cli/test_cli_base.py new file mode 100644 index 0000000000..6dbae045f6 --- /dev/null +++ b/tests/cli/test_cli_base.py @@ -0,0 +1,73 @@ +"""Base test class for CLI commands.""" + +from pathlib import Path +from unittest.mock import patch + +from axolotl.cli.main import cli + + +class BaseCliTest: + """Base class for CLI command tests.""" + + def _test_cli_validation(self, cli_runner, command: str): + """Test CLI validation for a command. + + Args: + cli_runner: CLI runner fixture + command: Command to test (train/evaluate) + """ + # Test missing config file + result = cli_runner.invoke(cli, [command, "--no-accelerate"]) + assert result.exit_code != 0 + + # Test non-existent config file + result = cli_runner.invoke(cli, [command, "nonexistent.yml", "--no-accelerate"]) + assert result.exit_code != 0 + assert "Error: Invalid value for 'CONFIG'" in result.output + + def _test_basic_execution( + self, cli_runner, tmp_path: Path, valid_test_config: str, command: str + ): + """Test basic execution with accelerate. + + Args: + cli_runner: CLI runner fixture + tmp_path: Temporary path fixture + valid_test_config: Valid config fixture + command: Command to test (train/evaluate) + """ + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + with patch("subprocess.run") as mock: + result = cli_runner.invoke(cli, [command, str(config_path)]) + + assert mock.called + assert mock.call_args.args[0] == [ + "accelerate", + "launch", + "-m", + f"axolotl.cli.{command}", + str(config_path), + "--debug-num-examples", + "0", + ] + assert mock.call_args.kwargs == {"check": True} + assert result.exit_code == 0 + + def _test_cli_overrides(self, tmp_path: Path, valid_test_config: str): + """Test CLI argument overrides. + + Args: + tmp_path: Temporary path fixture + valid_test_config: Valid config fixture + command: Command to test (train/evaluate) + """ + config_path = tmp_path / "config.yml" + output_dir = tmp_path / "model-out" + + test_config = valid_test_config.replace( + "output_dir: model-out", f"output_dir: {output_dir}" + ) + config_path.write_text(test_config) + return config_path diff --git a/tests/cli/test_cli_evaluate.py b/tests/cli/test_cli_evaluate.py new file mode 100644 index 0000000000..d8eb41467f --- /dev/null +++ b/tests/cli/test_cli_evaluate.py @@ -0,0 +1,67 @@ +"""Tests for evaluate CLI command.""" + +from unittest.mock import patch + +from axolotl.cli.main import cli + +from .test_cli_base import BaseCliTest + + +class TestEvaluateCommand(BaseCliTest): + """Test cases for evaluate command.""" + + cli = cli + + def test_evaluate_cli_validation(self, cli_runner): + """Test CLI validation""" + self._test_cli_validation(cli_runner, "evaluate") + + def test_evaluate_basic_execution(self, cli_runner, tmp_path, valid_test_config): + """Test basic successful execution""" + self._test_basic_execution(cli_runner, tmp_path, valid_test_config, "evaluate") + + def test_evaluate_basic_execution_no_accelerate( + self, cli_runner, tmp_path, valid_test_config + ): + """Test basic successful execution without accelerate""" + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + with patch("axolotl.cli.evaluate.do_evaluate") as mock_evaluate: + result = cli_runner.invoke( + cli, + [ + "evaluate", + str(config_path), + "--no-accelerate", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_evaluate.assert_called_once() + + def test_evaluate_cli_overrides(self, cli_runner, tmp_path, valid_test_config): + """Test CLI arguments properly override config values""" + config_path = self._test_cli_overrides(tmp_path, valid_test_config) + + with patch("axolotl.cli.evaluate.do_evaluate") as mock_evaluate: + result = cli_runner.invoke( + cli, + [ + "evaluate", + str(config_path), + "--micro-batch-size", + "2", + "--sequence-len", + "128", + "--no-accelerate", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_evaluate.assert_called_once() + cfg = mock_evaluate.call_args[0][0] + assert cfg.micro_batch_size == 2 + assert cfg.sequence_len == 128 diff --git a/tests/cli/test_cli_train.py b/tests/cli/test_cli_train.py index 7f028fb4f2..560f3caf58 100644 --- a/tests/cli/test_cli_train.py +++ b/tests/cli/test_cli_train.py @@ -1,98 +1,71 @@ -"""pytest tests for axolotl CLI train command.""" +"""Tests for train CLI command.""" + from unittest.mock import MagicMock, patch from axolotl.cli.main import cli - -def test_train_cli_validation(cli_runner): - """Test CLI validation""" - # Test missing config file - result = cli_runner.invoke(cli, ["train", "--no-accelerate"]) - assert result.exit_code != 0 - - # Test non-existent config file - result = cli_runner.invoke(cli, ["train", "nonexistent.yml", "--no-accelerate"]) - assert result.exit_code != 0 - assert "Error: Invalid value for 'CONFIG'" in result.output - - -def test_train_basic_execution(cli_runner, tmp_path, valid_test_config): - """Test basic successful execution""" - config_path = tmp_path / "config.yml" - config_path.write_text(valid_test_config) - - with patch("subprocess.run") as mock: - result = cli_runner.invoke(cli, ["train", str(config_path)]) - - assert mock.called - assert mock.call_args.args[0] == [ - "accelerate", - "launch", - "-m", - "axolotl.cli.train", - str(config_path), - "--debug-num-examples", - "0", - ] - assert mock.call_args.kwargs == {"check": True} - assert result.exit_code == 0 - - -def test_train_basic_execution_no_accelerate(cli_runner, tmp_path, valid_test_config): - """Test basic successful execution""" - config_path = tmp_path / "config.yml" - config_path.write_text(valid_test_config) - - with patch("axolotl.cli.train.train") as mock_train: - mock_train.return_value = (MagicMock(), MagicMock()) - - result = cli_runner.invoke( - cli, - [ - "train", - str(config_path), - "--learning-rate", - "1e-4", - "--micro-batch-size", - "2", - "--no-accelerate", - ], - catch_exceptions=False, - ) - - assert result.exit_code == 0 - mock_train.assert_called_once() - - -def test_train_cli_overrides(cli_runner, tmp_path, valid_test_config): - """Test CLI arguments properly override config values""" - config_path = tmp_path / "config.yml" - output_dir = tmp_path / "model-out" - - test_config = valid_test_config.replace( - "output_dir: model-out", f"output_dir: {output_dir}" - ) - config_path.write_text(test_config) - - with patch("axolotl.cli.train.train") as mock_train: - mock_train.return_value = (MagicMock(), MagicMock()) - - result = cli_runner.invoke( - cli, - [ - "train", - str(config_path), - "--learning-rate", - "1e-4", - "--micro-batch-size", - "2", - "--no-accelerate", - ], - catch_exceptions=False, - ) - - assert result.exit_code == 0 - mock_train.assert_called_once() - cfg = mock_train.call_args[1]["cfg"] - assert cfg["learning_rate"] == 1e-4 - assert cfg["micro_batch_size"] == 2 +from .test_cli_base import BaseCliTest + + +class TestTrainCommand(BaseCliTest): + """Test cases for train command.""" + + cli = cli + + def test_train_cli_validation(self, cli_runner): + """Test CLI validation""" + self._test_cli_validation(cli_runner, "train") + + def test_train_basic_execution(self, cli_runner, tmp_path, valid_test_config): + """Test basic successful execution""" + self._test_basic_execution(cli_runner, tmp_path, valid_test_config, "train") + + def test_train_basic_execution_no_accelerate( + self, cli_runner, tmp_path, valid_test_config + ): + """Test basic successful execution without accelerate""" + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + with patch("axolotl.cli.train.train") as mock_train: + mock_train.return_value = (MagicMock(), MagicMock()) + + result = cli_runner.invoke( + cli, + [ + "train", + str(config_path), + "--no-accelerate", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_train.assert_called_once() + + def test_train_cli_overrides(self, cli_runner, tmp_path, valid_test_config): + """Test CLI arguments properly override config values""" + config_path = self._test_cli_overrides(tmp_path, valid_test_config) + + with patch("axolotl.cli.train.train") as mock_train: + mock_train.return_value = (MagicMock(), MagicMock()) + + result = cli_runner.invoke( + cli, + [ + "train", + str(config_path), + "--learning-rate", + "1e-4", + "--micro-batch-size", + "2", + "--no-accelerate", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_train.assert_called_once() + cfg = mock_train.call_args[1]["cfg"] + assert cfg["learning_rate"] == 1e-4 + assert cfg["micro_batch_size"] == 2 From 1f623e6cc89346a417e272eaea734ab16ac848d9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 17 Dec 2024 11:01:21 -0500 Subject: [PATCH 0341/1405] transformers 4.47.1 (#2187) * transformers 4.47.1 * drop monkeypatches * can't remove patches yet * make flash attention forward ignore the loss kwargs * patch the flash attention in the modeling arch too * remove fsdp and deepspeed patches * cleanup PR * bump accelerate and torchao, also logically reorder/group requirements * meant to include torchao * use official patch release --- requirements.txt | 19 ++++++++------ scripts/unsloth_install.py | 2 +- src/axolotl/monkeypatch/trainer_grad_accum.py | 26 ++++++++++++++++--- src/axolotl/utils/models.py | 15 ++--------- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/requirements.txt b/requirements.txt index d0942193f0..0373548d15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,22 +11,27 @@ liger-kernel==0.4.2 # END section packaging==23.2 + peft==0.14.0 -transformers==4.47.0 +transformers==4.47.1 tokenizers>=0.20.1 -accelerate==1.2.0 +accelerate==1.2.1 datasets==3.1.0 deepspeed==0.16.1 +trl==0.12.1 + +optimum==1.16.2 +hf_transfer +sentencepiece +gradio==3.50.2 + pydantic==2.6.3 addict fire PyYAML>=6.0 requests -sentencepiece wandb einops -optimum==1.16.2 -hf_transfer colorama numba numpy>=1.24.4,<=2.0.1 @@ -36,7 +41,6 @@ scipy scikit-learn==1.4.2 nvidia-ml-py==12.560.30 art -gradio==3.50.2 tensorboard python-dotenv==1.0.1 @@ -45,7 +49,6 @@ s3fs>=2024.5.0 gcsfs>=2024.5.0 # adlfs -trl==0.12.1 zstandard==0.22.0 fastcore @@ -55,5 +58,5 @@ langdetect==1.0.9 immutabledict==4.2.0 antlr4-python3-runtime==4.13.2 -torchao==0.5.0 +torchao==0.7.0 schedulefree==1.3.0 diff --git a/scripts/unsloth_install.py b/scripts/unsloth_install.py index a6570b4e9f..bffab4670c 100644 --- a/scripts/unsloth_install.py +++ b/scripts/unsloth_install.py @@ -32,5 +32,5 @@ raise RuntimeError(f"Torch = {v} too new!") x = x.format(cuda.replace(".", ""), "-ampere" if is_ampere else "") print( - f'pip install unsloth-zoo==2024.11.7 && pip install --no-deps "unsloth[{x}]==2024.11.9"' + f'pip install unsloth-zoo==2024.12.1 && pip install --no-deps "unsloth[{x}]==2024.12.4"' ) diff --git a/src/axolotl/monkeypatch/trainer_grad_accum.py b/src/axolotl/monkeypatch/trainer_grad_accum.py index 76fcf57ce4..550f00e306 100644 --- a/src/axolotl/monkeypatch/trainer_grad_accum.py +++ b/src/axolotl/monkeypatch/trainer_grad_accum.py @@ -6,6 +6,7 @@ import logging from transformers import LlamaForCausalLM, Trainer +from transformers.modeling_flash_attention_utils import _flash_attention_forward from axolotl.monkeypatch.unsloth_ import detab_code @@ -13,10 +14,7 @@ ORIGINAL_CONTEXT_CODE = """ with self.compute_loss_context_manager(): - if self.model_accepts_loss_kwargs: - loss = self.compute_loss(model, inputs) - else: - loss = self.compute_loss(model, inputs, num_items_in_batch=num_items_in_batch) + loss = self.compute_loss(model, inputs, num_items_in_batch=num_items_in_batch) """ PATCHED_CONTEXT_CODE = """ @@ -288,3 +286,23 @@ def patch_training_loop_for_deepspeed_0_16_x(): Trainer._inner_training_loop = ( # pylint: disable=protected-access _fixed_inner_training_loop # pylint: disable=undefined-variable # noqa: F821 ) + + +def patch_flash_attention_forward(): + """ + monkeypatch for fixing the forward pass for flash attention to ignore num_items_in_batch + """ + + import transformers.modeling_flash_attention_utils + + def proxy_flash_attention_forward(*args, **kwargs): + kwargs.pop("num_items_in_batch", None) + + return _flash_attention_forward(*args, **kwargs) + + transformers.modeling_flash_attention_utils._flash_attention_forward = ( # pylint: disable=protected-access + proxy_flash_attention_forward + ) + transformers.models.llama.modeling_llama._flash_attention_forward = ( # pylint: disable=protected-access + proxy_flash_attention_forward + ) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 11f4c6d0fe..523fd76feb 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -380,19 +380,6 @@ def apply_patches(self) -> None: plugin_manager = PluginManager.get_instance() plugin_manager.pre_model_load(self.cfg) - if self.cfg.fsdp: - from axolotl.monkeypatch.trainer_fsdp_optim import ( - patch_training_loop_for_fsdp, - ) - - patch_training_loop_for_fsdp() - elif self.cfg.deepspeed and self.cfg.gradient_accumulation_steps > 1: - from axolotl.monkeypatch.trainer_grad_accum import ( - patch_training_loop_for_deepspeed_0_16_x, - ) - - patch_training_loop_for_deepspeed_0_16_x() - if self.cfg.gradient_checkpointing == "unsloth": transformers.modeling_utils.checkpoint = hf_grad_checkpoint_unsloth_wrapper @@ -401,10 +388,12 @@ def apply_patches(self) -> None: if self.cfg.model_config_type == "llama": from axolotl.monkeypatch.trainer_grad_accum import ( + patch_flash_attention_forward, patch_forward_for_ga, patch_training_step_for_ga, ) + patch_flash_attention_forward() patch_forward_for_ga() patch_training_step_for_ga() From 1c14c4a15cb2cb70d90bedd0a1580e30314c90cb Mon Sep 17 00:00:00 2001 From: Sunny Liu <22844540+bursteratom@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:24:30 -0500 Subject: [PATCH 0342/1405] Add hub model id config options to all example yml files (#2196) [skip ci] * added hub model_id in example yml * add hub model id to example yml --- examples/cerebras/btlm-ft.yml | 4 ++++ examples/cerebras/qlora.yml | 3 +++ examples/code-llama/13b/lora.yml | 3 +++ examples/code-llama/13b/qlora.yml | 3 +++ examples/code-llama/34b/lora.yml | 3 +++ examples/code-llama/34b/qlora.yml | 3 +++ examples/code-llama/7b/lora.yml | 3 +++ examples/code-llama/7b/qlora.yml | 3 +++ examples/dbrx/16bit-lora.yaml | 3 +++ examples/dbrx/8bit-lora.yaml | 3 +++ examples/dbrx/fft-ds-zero3.yaml | 3 +++ examples/deepseek-v2/fft-fsdp-16b.yaml | 2 ++ examples/deepseek-v2/qlora-fsdp-2_5.yaml | 3 +++ examples/falcon/config-7b-lora.yml | 7 ++++++- examples/falcon/config-7b-qlora.yml | 9 +++++++-- examples/falcon/config-7b.yml | 7 ++++++- examples/gemma/qlora.yml | 3 +++ examples/gemma2/qlora.yml | 3 +++ examples/gemma2/reward-model.yaml | 3 +++ examples/gptj/qlora.yml | 3 +++ examples/jamba/qlora.yaml | 3 +++ examples/jamba/qlora_deepspeed.yaml | 2 ++ examples/jamba/qlora_fsdp_large.yaml | 3 +++ examples/jeopardy-bot/config.yml | 4 ++++ examples/llama-2/fft_optimized.yml | 3 +++ examples/llama-2/gptq-lora.yml | 9 +++++++-- examples/llama-2/lisa.yml | 3 +++ examples/llama-2/loftq.yml | 3 +++ examples/llama-2/lora.yml | 3 +++ examples/llama-2/qlora-fsdp.yml | 3 +++ examples/llama-2/qlora.yml | 3 +++ examples/llama-3-vision/lora-11b.yaml | 4 ++++ examples/llama-3/fft-8b-liger-fsdp.yaml | 2 ++ examples/llama-3/fft-8b.yaml | 2 ++ examples/llama-3/instruct-dpo-lora-8b.yml | 3 +++ examples/llama-3/instruct-lora-8b.yml | 3 +++ examples/llama-3/lora-1b-deduplicate-dpo.yml | 3 +++ examples/llama-3/lora-1b-deduplicate-sft.yml | 3 +++ examples/llama-3/lora-1b.yml | 2 ++ examples/llama-3/lora-8b.yml | 3 +++ examples/llama-3/qlora-1b-kto.yaml | 2 ++ examples/llama-3/qlora-1b.yml | 2 ++ examples/llama-3/qlora-fsdp-405b.yaml | 3 +++ examples/llama-3/qlora-fsdp-70b.yaml | 3 +++ examples/llama-3/qlora.yml | 3 +++ examples/mamba/config.yml | 3 +++ examples/mistral/bigstral-ds-zero3.yaml | 4 ++++ examples/mistral/config.yml | 3 +++ examples/mistral/lora-mps.yml | 3 +++ examples/mistral/lora.yml | 3 +++ examples/mistral/mistral-dpo-qlora.yml | 3 +++ examples/mistral/mistral-qlora-fsdp.yml | 4 ++++ examples/mistral/mistral-qlora-orpo.yml | 3 +++ examples/mistral/mixtral-8x22b-qlora-fsdp.yml | 3 +++ examples/mistral/mixtral-qlora-fsdp.yml | 4 ++++ examples/mistral/mixtral.yml | 4 ++++ examples/mistral/mixtral_22.yml | 4 ++++ examples/mistral/qlora.yml | 3 +++ examples/mpt-7b/config.yml | 4 ++++ examples/openllama-3b/config.yml | 4 ++++ examples/openllama-3b/lora.yml | 4 ++++ examples/openllama-3b/qlora.yml | 4 ++++ examples/phi/lora-3.5.yaml | 3 +++ examples/phi/phi-ft.yml | 3 +++ examples/phi/phi-qlora.yml | 3 +++ examples/phi/phi2-ft.yml | 3 +++ examples/phi/phi3-ft-fsdp.yml | 3 +++ examples/phi/phi3-ft.yml | 4 ++++ examples/pythia-12b/config.yml | 4 ++++ examples/pythia/lora.yml | 3 +++ examples/qwen/lora.yml | 3 +++ examples/qwen/qlora.yml | 3 +++ examples/qwen/qwen2-moe-lora.yaml | 3 +++ examples/qwen/qwen2-moe-qlora.yaml | 3 +++ examples/qwen2/dpo.yaml | 2 ++ examples/qwen2/qlora-fsdp.yaml | 3 +++ examples/redpajama/config-3b.yml | 4 ++++ examples/replit-3b/config-lora.yml | 3 +++ examples/stablelm-2/1.6b/fft.yml | 4 ++++ examples/stablelm-2/1.6b/lora.yml | 4 ++++ examples/starcoder2/qlora.yml | 2 ++ examples/tiny-llama/lora-mps.yml | 3 +++ examples/tiny-llama/lora.yml | 3 +++ examples/tiny-llama/pretrain.yml | 4 +++- examples/tiny-llama/qlora.yml | 3 +++ examples/xgen-7b/xgen-7b-8k-qlora.yml | 7 ++++++- examples/yi-34B-chat/qlora.yml | 3 +++ 87 files changed, 286 insertions(+), 8 deletions(-) diff --git a/examples/cerebras/btlm-ft.yml b/examples/cerebras/btlm-ft.yml index ba4e65daae..780616e047 100644 --- a/examples/cerebras/btlm-ft.yml +++ b/examples/cerebras/btlm-ft.yml @@ -1,6 +1,10 @@ base_model: cerebras/btlm-3b-8k-base +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: GPT2Tokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true tokenizer_use_fast: true tokenizer_legacy: true diff --git a/examples/cerebras/qlora.yml b/examples/cerebras/qlora.yml index 285607a4c8..866b4ab58c 100644 --- a/examples/cerebras/qlora.yml +++ b/examples/cerebras/qlora.yml @@ -1,4 +1,7 @@ base_model: cerebras/Cerebras-GPT-1.3B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + load_in_8bit: false load_in_4bit: true strict: false diff --git a/examples/code-llama/13b/lora.yml b/examples/code-llama/13b/lora.yml index 0ba96cfaa7..2b8a720b24 100644 --- a/examples/code-llama/13b/lora.yml +++ b/examples/code-llama/13b/lora.yml @@ -1,6 +1,9 @@ base_model: codellama/CodeLlama-13b-hf +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: CodeLlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/code-llama/13b/qlora.yml b/examples/code-llama/13b/qlora.yml index 787862d010..92aa6ac972 100644 --- a/examples/code-llama/13b/qlora.yml +++ b/examples/code-llama/13b/qlora.yml @@ -1,6 +1,9 @@ base_model: codellama/CodeLlama-13b-hf +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: CodeLlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/code-llama/34b/lora.yml b/examples/code-llama/34b/lora.yml index 92d4c544a3..af343e389a 100644 --- a/examples/code-llama/34b/lora.yml +++ b/examples/code-llama/34b/lora.yml @@ -1,6 +1,9 @@ base_model: codellama/CodeLlama-34b-hf +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: CodeLlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/code-llama/34b/qlora.yml b/examples/code-llama/34b/qlora.yml index 93a6de8777..f45e9205f4 100644 --- a/examples/code-llama/34b/qlora.yml +++ b/examples/code-llama/34b/qlora.yml @@ -1,6 +1,9 @@ base_model: codellama/CodeLlama-34b-hf +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: CodeLlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/code-llama/7b/lora.yml b/examples/code-llama/7b/lora.yml index d13f505325..6c385dbcb4 100644 --- a/examples/code-llama/7b/lora.yml +++ b/examples/code-llama/7b/lora.yml @@ -1,6 +1,9 @@ base_model: codellama/CodeLlama-7b-hf +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: CodeLlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/code-llama/7b/qlora.yml b/examples/code-llama/7b/qlora.yml index a1026a982d..ccd256406c 100644 --- a/examples/code-llama/7b/qlora.yml +++ b/examples/code-llama/7b/qlora.yml @@ -1,6 +1,9 @@ base_model: codellama/CodeLlama-7b-hf +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: CodeLlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/dbrx/16bit-lora.yaml b/examples/dbrx/16bit-lora.yaml index 32b625ac69..645ba1d59f 100644 --- a/examples/dbrx/16bit-lora.yaml +++ b/examples/dbrx/16bit-lora.yaml @@ -1,4 +1,7 @@ base_model: LnL-AI/dbrx-base-converted-v2 +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/dbrx/8bit-lora.yaml b/examples/dbrx/8bit-lora.yaml index 50ee0a0164..4b9f607562 100644 --- a/examples/dbrx/8bit-lora.yaml +++ b/examples/dbrx/8bit-lora.yaml @@ -1,4 +1,7 @@ base_model: LnL-AI/dbrx-base-converted-v2 +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: true diff --git a/examples/dbrx/fft-ds-zero3.yaml b/examples/dbrx/fft-ds-zero3.yaml index 60dc201eee..e42b636700 100644 --- a/examples/dbrx/fft-ds-zero3.yaml +++ b/examples/dbrx/fft-ds-zero3.yaml @@ -1,4 +1,7 @@ base_model: LnL-AI/dbrx-base-converted-v2 +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/deepseek-v2/fft-fsdp-16b.yaml b/examples/deepseek-v2/fft-fsdp-16b.yaml index b55646df7f..3d4608a016 100644 --- a/examples/deepseek-v2/fft-fsdp-16b.yaml +++ b/examples/deepseek-v2/fft-fsdp-16b.yaml @@ -1,4 +1,6 @@ base_model: deepseek-ai/DeepSeek-V2-Lite +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name trust_remote_code: true load_in_8bit: false diff --git a/examples/deepseek-v2/qlora-fsdp-2_5.yaml b/examples/deepseek-v2/qlora-fsdp-2_5.yaml index 6b8771d814..a89dc343a2 100644 --- a/examples/deepseek-v2/qlora-fsdp-2_5.yaml +++ b/examples/deepseek-v2/qlora-fsdp-2_5.yaml @@ -1,4 +1,7 @@ base_model: axolotl-quants/DeepSeek-V2.5-bnb-nf4-bf16 +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/falcon/config-7b-lora.yml b/examples/falcon/config-7b-lora.yml index 029ca40e09..efbe38d4a5 100644 --- a/examples/falcon/config-7b-lora.yml +++ b/examples/falcon/config-7b-lora.yml @@ -1,7 +1,12 @@ base_model: tiiuae/falcon-7b -trust_remote_code: true +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +# required by falcon custom model code: https://huggingface.co/tiiuae/falcon-7b/tree/main +trust_remote_code: true load_in_8bit: true load_in_4bit: false diff --git a/examples/falcon/config-7b-qlora.yml b/examples/falcon/config-7b-qlora.yml index 4e34144ed6..b9829db5f7 100644 --- a/examples/falcon/config-7b-qlora.yml +++ b/examples/falcon/config-7b-qlora.yml @@ -1,10 +1,15 @@ # 1b: tiiuae/falcon-rw-1b # 40b: tiiuae/falcon-40b base_model: tiiuae/falcon-7b -# required by falcon custom model code: https://huggingface.co/tiiuae/falcon-7b/tree/main -trust_remote_code: true +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +# required by falcon custom model code: https://huggingface.co/tiiuae/falcon-7b/tree/main +trust_remote_code: true + load_in_8bit: false # enable 4bit for QLoRA diff --git a/examples/falcon/config-7b.yml b/examples/falcon/config-7b.yml index 36264f063e..5e41a1e336 100644 --- a/examples/falcon/config-7b.yml +++ b/examples/falcon/config-7b.yml @@ -1,7 +1,12 @@ base_model: tiiuae/falcon-7b -trust_remote_code: true +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +# required by falcon custom model code: https://huggingface.co/tiiuae/falcon-7b/tree/main +trust_remote_code: true load_in_8bit: false load_in_4bit: false diff --git a/examples/gemma/qlora.yml b/examples/gemma/qlora.yml index e08facfc5d..80a9fe62fc 100644 --- a/examples/gemma/qlora.yml +++ b/examples/gemma/qlora.yml @@ -1,7 +1,10 @@ # use google/gemma-7b if you have access base_model: mhenrichsen/gemma-7b +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/gemma2/qlora.yml b/examples/gemma2/qlora.yml index 00e6d84e0d..61a2ad8761 100644 --- a/examples/gemma2/qlora.yml +++ b/examples/gemma2/qlora.yml @@ -1,6 +1,9 @@ base_model: google/gemma-2-9b +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/gemma2/reward-model.yaml b/examples/gemma2/reward-model.yaml index c1f993c3ae..b492c6f931 100644 --- a/examples/gemma2/reward-model.yaml +++ b/examples/gemma2/reward-model.yaml @@ -1,6 +1,9 @@ base_model: google/gemma-2-2b +# optionally might have model_type or tokenizer_type model_type: AutoModelForSequenceClassification tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/gptj/qlora.yml b/examples/gptj/qlora.yml index f801729fac..ddd6d24c0d 100644 --- a/examples/gptj/qlora.yml +++ b/examples/gptj/qlora.yml @@ -1,4 +1,7 @@ base_model: EleutherAI/gpt-j-6b +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + load_in_8bit: false load_in_4bit: true strict: false diff --git a/examples/jamba/qlora.yaml b/examples/jamba/qlora.yaml index 3d6f69e793..cab62513c1 100644 --- a/examples/jamba/qlora.yaml +++ b/examples/jamba/qlora.yaml @@ -1,4 +1,7 @@ base_model: ai21labs/Jamba-v0.1 +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/jamba/qlora_deepspeed.yaml b/examples/jamba/qlora_deepspeed.yaml index 43a76c00b1..7ac7bfac5d 100644 --- a/examples/jamba/qlora_deepspeed.yaml +++ b/examples/jamba/qlora_deepspeed.yaml @@ -1,4 +1,6 @@ base_model: ai21labs/Jamba-v0.1 +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name trust_remote_code: true load_in_8bit: false diff --git a/examples/jamba/qlora_fsdp_large.yaml b/examples/jamba/qlora_fsdp_large.yaml index 84cf906422..8736680da8 100644 --- a/examples/jamba/qlora_fsdp_large.yaml +++ b/examples/jamba/qlora_fsdp_large.yaml @@ -1,5 +1,8 @@ base_model: ai21labs/AI21-Jamba-1.5-Large +# optionally might have model_type or tokenizer_type tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_4bit: true strict: false diff --git a/examples/jeopardy-bot/config.yml b/examples/jeopardy-bot/config.yml index 088629c084..04f92a8dc8 100644 --- a/examples/jeopardy-bot/config.yml +++ b/examples/jeopardy-bot/config.yml @@ -1,6 +1,10 @@ base_model: huggyllama/llama-7b +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + load_in_8bit: false datasets: - path: openaccess-ai-collective/jeopardy diff --git a/examples/llama-2/fft_optimized.yml b/examples/llama-2/fft_optimized.yml index 3d94b04b8b..3475fcd9ac 100644 --- a/examples/llama-2/fft_optimized.yml +++ b/examples/llama-2/fft_optimized.yml @@ -1,6 +1,9 @@ base_model: NousResearch/Llama-2-7b-hf +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/llama-2/gptq-lora.yml b/examples/llama-2/gptq-lora.yml index 2a706265bd..7e45f7d639 100644 --- a/examples/llama-2/gptq-lora.yml +++ b/examples/llama-2/gptq-lora.yml @@ -1,8 +1,13 @@ base_model: TheBloke/Llama-2-7B-GPTQ -gptq: true -gptq_disable_exllama: true +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +gptq: true +gptq_disable_exllama: true + tokenizer_use_fast: true tokenizer_legacy: true load_in_8bit: false diff --git a/examples/llama-2/lisa.yml b/examples/llama-2/lisa.yml index 7012d1f613..40391204ce 100644 --- a/examples/llama-2/lisa.yml +++ b/examples/llama-2/lisa.yml @@ -1,6 +1,9 @@ base_model: NousResearch/Llama-2-7b-hf +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/llama-2/loftq.yml b/examples/llama-2/loftq.yml index 68d9ac0142..a5108e70f5 100644 --- a/examples/llama-2/loftq.yml +++ b/examples/llama-2/loftq.yml @@ -1,6 +1,9 @@ base_model: NousResearch/Llama-2-7b-hf +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/llama-2/lora.yml b/examples/llama-2/lora.yml index 95bfae6920..ec0c800124 100644 --- a/examples/llama-2/lora.yml +++ b/examples/llama-2/lora.yml @@ -1,6 +1,9 @@ base_model: NousResearch/Llama-2-7b-hf +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/llama-2/qlora-fsdp.yml b/examples/llama-2/qlora-fsdp.yml index 88029f92d5..204c91693c 100644 --- a/examples/llama-2/qlora-fsdp.yml +++ b/examples/llama-2/qlora-fsdp.yml @@ -1,6 +1,9 @@ base_model: NousResearch/Llama-2-7b-hf +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/llama-2/qlora.yml b/examples/llama-2/qlora.yml index dda32170bd..81d1acbecd 100644 --- a/examples/llama-2/qlora.yml +++ b/examples/llama-2/qlora.yml @@ -1,6 +1,9 @@ base_model: NousResearch/Llama-2-7b-hf +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/llama-3-vision/lora-11b.yaml b/examples/llama-3-vision/lora-11b.yaml index b2e4946418..22dc3a9afc 100644 --- a/examples/llama-3-vision/lora-11b.yaml +++ b/examples/llama-3-vision/lora-11b.yaml @@ -1,5 +1,9 @@ base_model: alpindale/Llama-3.2-11B-Vision-Instruct +# optionally might have model_type or tokenizer_type or processor_type processor_type: AutoProcessor +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + strict: false # these 3 lines are needed for now to handle vision chat templates w images diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml index 043b5c9807..2c8589b17a 100644 --- a/examples/llama-3/fft-8b-liger-fsdp.yaml +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -1,4 +1,6 @@ base_model: NousResearch/Meta-Llama-3.1-8B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name plugins: - axolotl.integrations.liger.LigerPlugin diff --git a/examples/llama-3/fft-8b.yaml b/examples/llama-3/fft-8b.yaml index 335902aac7..a129c6e5b4 100644 --- a/examples/llama-3/fft-8b.yaml +++ b/examples/llama-3/fft-8b.yaml @@ -1,4 +1,6 @@ base_model: NousResearch/Meta-Llama-3.1-8B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/llama-3/instruct-dpo-lora-8b.yml b/examples/llama-3/instruct-dpo-lora-8b.yml index dc88350358..bb58b677a4 100644 --- a/examples/llama-3/instruct-dpo-lora-8b.yml +++ b/examples/llama-3/instruct-dpo-lora-8b.yml @@ -1,6 +1,9 @@ base_model: meta-llama/Meta-Llama-3-8B-Instruct +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml index ae9a8088c3..853f85d74d 100644 --- a/examples/llama-3/instruct-lora-8b.yml +++ b/examples/llama-3/instruct-lora-8b.yml @@ -1,6 +1,9 @@ base_model: NousResearch/Meta-Llama-3-8B-Instruct +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/llama-3/lora-1b-deduplicate-dpo.yml b/examples/llama-3/lora-1b-deduplicate-dpo.yml index 35a0260ca0..00314da75d 100644 --- a/examples/llama-3/lora-1b-deduplicate-dpo.yml +++ b/examples/llama-3/lora-1b-deduplicate-dpo.yml @@ -1,6 +1,9 @@ base_model: meta-llama/Llama-3.2-1B +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/llama-3/lora-1b-deduplicate-sft.yml b/examples/llama-3/lora-1b-deduplicate-sft.yml index c07d5f8ffa..4516964655 100644 --- a/examples/llama-3/lora-1b-deduplicate-sft.yml +++ b/examples/llama-3/lora-1b-deduplicate-sft.yml @@ -1,6 +1,9 @@ base_model: meta-llama/Llama-3.2-1B +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/llama-3/lora-1b.yml b/examples/llama-3/lora-1b.yml index bdda2ed75e..a1c3afa872 100644 --- a/examples/llama-3/lora-1b.yml +++ b/examples/llama-3/lora-1b.yml @@ -1,4 +1,6 @@ base_model: NousResearch/Llama-3.2-1B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/llama-3/lora-8b.yml b/examples/llama-3/lora-8b.yml index a20a529f5f..7921857ceb 100644 --- a/examples/llama-3/lora-8b.yml +++ b/examples/llama-3/lora-8b.yml @@ -1,6 +1,9 @@ base_model: NousResearch/Meta-Llama-3-8B +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/llama-3/qlora-1b-kto.yaml b/examples/llama-3/qlora-1b-kto.yaml index a876d8fd72..df4a084897 100644 --- a/examples/llama-3/qlora-1b-kto.yaml +++ b/examples/llama-3/qlora-1b-kto.yaml @@ -1,4 +1,6 @@ base_model: meta-llama/Llama-3.2-1B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/llama-3/qlora-1b.yml b/examples/llama-3/qlora-1b.yml index be40db846e..226bbb2379 100644 --- a/examples/llama-3/qlora-1b.yml +++ b/examples/llama-3/qlora-1b.yml @@ -1,4 +1,6 @@ base_model: NousResearch/Llama-3.2-1B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/llama-3/qlora-fsdp-405b.yaml b/examples/llama-3/qlora-fsdp-405b.yaml index 6eeec01c9b..a60a97ef3a 100644 --- a/examples/llama-3/qlora-fsdp-405b.yaml +++ b/examples/llama-3/qlora-fsdp-405b.yaml @@ -1,5 +1,8 @@ base_model: hugging-quants/Meta-Llama-3.1-405B-BNB-NF4-BF16 +# optionally might have model_type or tokenizer_type tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_4bit: true strict: false diff --git a/examples/llama-3/qlora-fsdp-70b.yaml b/examples/llama-3/qlora-fsdp-70b.yaml index 9b74f6b4de..932e1a0d63 100644 --- a/examples/llama-3/qlora-fsdp-70b.yaml +++ b/examples/llama-3/qlora-fsdp-70b.yaml @@ -1,6 +1,9 @@ base_model: casperhansen/llama-3-70b-fp16 +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: AutoTokenizer # PreTrainedTokenizerFast +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/llama-3/qlora.yml b/examples/llama-3/qlora.yml index 079c9cad06..64268a2054 100644 --- a/examples/llama-3/qlora.yml +++ b/examples/llama-3/qlora.yml @@ -1,6 +1,9 @@ base_model: NousResearch/Meta-Llama-3-8B +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/mamba/config.yml b/examples/mamba/config.yml index f88f5138d9..ca96fbfc3f 100644 --- a/examples/mamba/config.yml +++ b/examples/mamba/config.yml @@ -1,7 +1,10 @@ base_model: state-spaces/mamba-2.8b +# optionally might have model_type or tokenizer_type or tokenizer_config model_type: MambaLMHeadModel tokenizer_type: AutoTokenizer tokenizer_config: EleutherAI/gpt-neox-20b +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/mistral/bigstral-ds-zero3.yaml b/examples/mistral/bigstral-ds-zero3.yaml index e993e44a78..5ee214c1b4 100644 --- a/examples/mistral/bigstral-ds-zero3.yaml +++ b/examples/mistral/bigstral-ds-zero3.yaml @@ -1,6 +1,10 @@ base_model: mistral-community/Mixtral-8x22B-v0.1 +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/mistral/config.yml b/examples/mistral/config.yml index a70937c4fd..8902033391 100644 --- a/examples/mistral/config.yml +++ b/examples/mistral/config.yml @@ -1,6 +1,9 @@ base_model: mistralai/Mistral-7B-v0.1 +# optionally might have model_type or tokenizer_type model_type: MistralForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/mistral/lora-mps.yml b/examples/mistral/lora-mps.yml index 03c74bb59b..c1df9896cf 100644 --- a/examples/mistral/lora-mps.yml +++ b/examples/mistral/lora-mps.yml @@ -1,6 +1,9 @@ base_model: mistralai/Mistral-7B-v0.1 +# optionally might have model_type or tokenizer_type model_type: MistralForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/mistral/lora.yml b/examples/mistral/lora.yml index 0d5dc9edd7..11c1e0ee7b 100644 --- a/examples/mistral/lora.yml +++ b/examples/mistral/lora.yml @@ -1,6 +1,9 @@ base_model: mistralai/Mistral-7B-v0.1 +# optionally might have model_type or tokenizer_type model_type: MistralForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/mistral/mistral-dpo-qlora.yml b/examples/mistral/mistral-dpo-qlora.yml index a558e04535..e2eb6a2645 100644 --- a/examples/mistral/mistral-dpo-qlora.yml +++ b/examples/mistral/mistral-dpo-qlora.yml @@ -4,8 +4,11 @@ #face problems with the special tokens. base_model: mistralai/Mistral-7B-Instruct-v0.2 +# optionally might have model_type or tokenizer_type model_type: MistralForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/mistral/mistral-qlora-fsdp.yml b/examples/mistral/mistral-qlora-fsdp.yml index e6b07c594b..521f4de5f0 100644 --- a/examples/mistral/mistral-qlora-fsdp.yml +++ b/examples/mistral/mistral-qlora-fsdp.yml @@ -1,6 +1,10 @@ base_model: mistralai/Mixtral-8x7B-v0.1 +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/mistral/mistral-qlora-orpo.yml b/examples/mistral/mistral-qlora-orpo.yml index 2549ef018c..82f30dc172 100644 --- a/examples/mistral/mistral-qlora-orpo.yml +++ b/examples/mistral/mistral-qlora-orpo.yml @@ -1,6 +1,9 @@ base_model: mistralai/Mistral-7B-v0.1 +# optionally might have model_type or tokenizer_type model_type: MistralForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml index fe68b28172..4a65b1a7d0 100644 --- a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml +++ b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml @@ -1,6 +1,9 @@ base_model: mistral-community/Mixtral-8x22B-v0.1 +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/mistral/mixtral-qlora-fsdp.yml b/examples/mistral/mixtral-qlora-fsdp.yml index c095970402..fbd9bd9372 100644 --- a/examples/mistral/mixtral-qlora-fsdp.yml +++ b/examples/mistral/mixtral-qlora-fsdp.yml @@ -1,6 +1,10 @@ base_model: mistralai/Mixtral-8x7B-v0.1 +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/mistral/mixtral.yml b/examples/mistral/mixtral.yml index 13fbe92ab8..ac80ec9336 100644 --- a/examples/mistral/mixtral.yml +++ b/examples/mistral/mixtral.yml @@ -1,6 +1,10 @@ base_model: mistralai/Mixtral-8x7B-v0.1 +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/mistral/mixtral_22.yml b/examples/mistral/mixtral_22.yml index 9a1e86386c..7f2a72212e 100644 --- a/examples/mistral/mixtral_22.yml +++ b/examples/mistral/mixtral_22.yml @@ -1,6 +1,10 @@ base_model: mistral-community/Mixtral-8x22B-v0.1 +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/mistral/qlora.yml b/examples/mistral/qlora.yml index c7bdb155c0..5f3fa10b8a 100644 --- a/examples/mistral/qlora.yml +++ b/examples/mistral/qlora.yml @@ -1,6 +1,9 @@ base_model: mistralai/Mistral-7B-v0.1 +# optionally might have model_type or tokenizer_type model_type: MistralForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/mpt-7b/config.yml b/examples/mpt-7b/config.yml index 530415de17..cf4b433fe5 100644 --- a/examples/mpt-7b/config.yml +++ b/examples/mpt-7b/config.yml @@ -1,5 +1,9 @@ base_model: mosaicml/mpt-7b +# optionally might have model_type or tokenizer_type tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true # required for mpt as their model class is not merged into transformers yet load_in_8bit: false datasets: diff --git a/examples/openllama-3b/config.yml b/examples/openllama-3b/config.yml index a0473213c0..ec66014b43 100644 --- a/examples/openllama-3b/config.yml +++ b/examples/openllama-3b/config.yml @@ -1,6 +1,10 @@ base_model: openlm-research/open_llama_3b_v2 +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + load_in_8bit: false load_in_4bit: false strict: false diff --git a/examples/openllama-3b/lora.yml b/examples/openllama-3b/lora.yml index 2b67849159..b449df9ae7 100644 --- a/examples/openllama-3b/lora.yml +++ b/examples/openllama-3b/lora.yml @@ -1,6 +1,10 @@ base_model: openlm-research/open_llama_3b_v2 +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + load_in_8bit: true load_in_4bit: false strict: false diff --git a/examples/openllama-3b/qlora.yml b/examples/openllama-3b/qlora.yml index 8d4dc05ca7..3efcdabc6c 100644 --- a/examples/openllama-3b/qlora.yml +++ b/examples/openllama-3b/qlora.yml @@ -1,6 +1,10 @@ base_model: openlm-research/open_llama_3b_v2 +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + load_in_8bit: false load_in_4bit: true strict: false diff --git a/examples/phi/lora-3.5.yaml b/examples/phi/lora-3.5.yaml index 246701148c..8c0205f4cd 100644 --- a/examples/phi/lora-3.5.yaml +++ b/examples/phi/lora-3.5.yaml @@ -1,6 +1,9 @@ base_model: microsoft/Phi-3.5-mini-instruct +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/phi/phi-ft.yml b/examples/phi/phi-ft.yml index 0dabadc7a4..fc5848dc56 100644 --- a/examples/phi/phi-ft.yml +++ b/examples/phi/phi-ft.yml @@ -1,6 +1,9 @@ base_model: microsoft/phi-1_5 +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/phi/phi-qlora.yml b/examples/phi/phi-qlora.yml index 7c181a3c15..a98cd10408 100644 --- a/examples/phi/phi-qlora.yml +++ b/examples/phi/phi-qlora.yml @@ -1,6 +1,9 @@ base_model: microsoft/phi-1_5 +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/phi/phi2-ft.yml b/examples/phi/phi2-ft.yml index 27815550b4..0f656f8210 100644 --- a/examples/phi/phi2-ft.yml +++ b/examples/phi/phi2-ft.yml @@ -1,6 +1,9 @@ base_model: microsoft/phi-2 +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/phi/phi3-ft-fsdp.yml b/examples/phi/phi3-ft-fsdp.yml index d220e577d1..c081e47b9f 100644 --- a/examples/phi/phi3-ft-fsdp.yml +++ b/examples/phi/phi3-ft-fsdp.yml @@ -1,6 +1,9 @@ base_model: microsoft/Phi-3-mini-4k-instruct +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/phi/phi3-ft.yml b/examples/phi/phi3-ft.yml index 18db9b8b78..ac42153afa 100644 --- a/examples/phi/phi3-ft.yml +++ b/examples/phi/phi3-ft.yml @@ -1,7 +1,11 @@ base_model: microsoft/Phi-3-mini-4k-instruct +# optionally might have model_type or tokenizer_type trust_remote_code: true model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + chat_template: phi_3 load_in_8bit: false diff --git a/examples/pythia-12b/config.yml b/examples/pythia-12b/config.yml index 18e6beaafd..52ab770556 100644 --- a/examples/pythia-12b/config.yml +++ b/examples/pythia-12b/config.yml @@ -1,7 +1,11 @@ base_model: EleutherAI/pythia-12b-deduped base_model_ignore_patterns: pytorch* # prefer safetensors +# optionally might have model_type or tokenizer_type model_type: GPTNeoXForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + load_in_8bit: false load_in_4bit: false gptq: false diff --git a/examples/pythia/lora.yml b/examples/pythia/lora.yml index 0aa650f67e..203652f6b7 100644 --- a/examples/pythia/lora.yml +++ b/examples/pythia/lora.yml @@ -1,4 +1,7 @@ base_model: EleutherAI/pythia-1.4b-deduped +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + load_in_8bit: true datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/qwen/lora.yml b/examples/qwen/lora.yml index dd8dc1e4f4..961125a511 100644 --- a/examples/qwen/lora.yml +++ b/examples/qwen/lora.yml @@ -1,6 +1,9 @@ base_model: Qwen/Qwen-7B +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name trust_remote_code: true diff --git a/examples/qwen/qlora.yml b/examples/qwen/qlora.yml index 01c0c0ab86..e7159eaa5f 100644 --- a/examples/qwen/qlora.yml +++ b/examples/qwen/qlora.yml @@ -1,6 +1,9 @@ base_model: Qwen/Qwen-7B +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name trust_remote_code: true diff --git a/examples/qwen/qwen2-moe-lora.yaml b/examples/qwen/qwen2-moe-lora.yaml index 452335e38f..b357b93443 100644 --- a/examples/qwen/qwen2-moe-lora.yaml +++ b/examples/qwen/qwen2-moe-lora.yaml @@ -1,4 +1,7 @@ base_model: Qwen/Qwen1.5-MoE-A2.7B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/qwen/qwen2-moe-qlora.yaml b/examples/qwen/qwen2-moe-qlora.yaml index bc11007c78..d45e4c89f2 100644 --- a/examples/qwen/qwen2-moe-qlora.yaml +++ b/examples/qwen/qwen2-moe-qlora.yaml @@ -1,4 +1,7 @@ base_model: Qwen/Qwen1.5-MoE-A2.7B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/qwen2/dpo.yaml b/examples/qwen2/dpo.yaml index 64c3e7629b..e924be195c 100644 --- a/examples/qwen2/dpo.yaml +++ b/examples/qwen2/dpo.yaml @@ -1,4 +1,6 @@ base_model: Qwen/Qwen2.5-0.5B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name strict: false diff --git a/examples/qwen2/qlora-fsdp.yaml b/examples/qwen2/qlora-fsdp.yaml index d61c72a378..cc49749086 100644 --- a/examples/qwen2/qlora-fsdp.yaml +++ b/examples/qwen2/qlora-fsdp.yaml @@ -1,4 +1,7 @@ base_model: Qwen/Qwen2-7B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/redpajama/config-3b.yml b/examples/redpajama/config-3b.yml index ff395a863d..d716727a34 100644 --- a/examples/redpajama/config-3b.yml +++ b/examples/redpajama/config-3b.yml @@ -1,6 +1,10 @@ base_model: togethercomputer/RedPajama-INCITE-Chat-3B-v1 +# optionally might have model_type or tokenizer_type model_type: GPTNeoXForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: load_in_8bit: false datasets: diff --git a/examples/replit-3b/config-lora.yml b/examples/replit-3b/config-lora.yml index 9fee099d47..bb2a6aace4 100644 --- a/examples/replit-3b/config-lora.yml +++ b/examples/replit-3b/config-lora.yml @@ -1,4 +1,7 @@ base_model: replit/replit-code-v1-3b +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false datasets: diff --git a/examples/stablelm-2/1.6b/fft.yml b/examples/stablelm-2/1.6b/fft.yml index 777262a7ee..3ecb1581bc 100644 --- a/examples/stablelm-2/1.6b/fft.yml +++ b/examples/stablelm-2/1.6b/fft.yml @@ -1,6 +1,10 @@ base_model: stabilityai/stablelm-2-1_6b +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: false diff --git a/examples/stablelm-2/1.6b/lora.yml b/examples/stablelm-2/1.6b/lora.yml index c65b9e4cd0..8597de6a28 100644 --- a/examples/stablelm-2/1.6b/lora.yml +++ b/examples/stablelm-2/1.6b/lora.yml @@ -1,6 +1,10 @@ base_model: stabilityai/stablelm-2-1_6b +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + trust_remote_code: true load_in_8bit: true diff --git a/examples/starcoder2/qlora.yml b/examples/starcoder2/qlora.yml index 83fc0d89f7..d1db71d6d0 100644 --- a/examples/starcoder2/qlora.yml +++ b/examples/starcoder2/qlora.yml @@ -1,4 +1,6 @@ base_model: bigcode/starcoder2-3b +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/tiny-llama/lora-mps.yml b/examples/tiny-llama/lora-mps.yml index 99e404e432..f949acd0f5 100644 --- a/examples/tiny-llama/lora-mps.yml +++ b/examples/tiny-llama/lora-mps.yml @@ -1,6 +1,9 @@ base_model: TinyLlama/TinyLlama_v1.1 +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/tiny-llama/lora.yml b/examples/tiny-llama/lora.yml index 7c7fb67060..54aa5ec275 100644 --- a/examples/tiny-llama/lora.yml +++ b/examples/tiny-llama/lora.yml @@ -1,5 +1,8 @@ base_model: TinyLlama/TinyLlama_v1.1 +# optionally might have model_type or tokenizer_type tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: true load_in_4bit: false diff --git a/examples/tiny-llama/pretrain.yml b/examples/tiny-llama/pretrain.yml index 010a1608a3..fd6d2c9c1f 100644 --- a/examples/tiny-llama/pretrain.yml +++ b/examples/tiny-llama/pretrain.yml @@ -1,7 +1,9 @@ base_model: TinyLlama/TinyLlama-1.1B-Chat-v1.0 - +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: false diff --git a/examples/tiny-llama/qlora.yml b/examples/tiny-llama/qlora.yml index 931fe03e04..694ab3a15e 100644 --- a/examples/tiny-llama/qlora.yml +++ b/examples/tiny-llama/qlora.yml @@ -1,6 +1,9 @@ base_model: TinyLlama/TinyLlama_v1.1 +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true diff --git a/examples/xgen-7b/xgen-7b-8k-qlora.yml b/examples/xgen-7b/xgen-7b-8k-qlora.yml index 7e3f83cbd7..d798e326d7 100644 --- a/examples/xgen-7b/xgen-7b-8k-qlora.yml +++ b/examples/xgen-7b/xgen-7b-8k-qlora.yml @@ -1,9 +1,14 @@ # An example finetuning Saleforce's XGen-7b model with 8k context using qlora # on Tim Dettmer's Guanaco dataset. base_model: Salesforce/xgen-7b-8k-base -trust_remote_code: true +# optionally might have model_type or tokenizer_type model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +trust_remote_code: true + load_in_8bit: false # enable 4bit for QLoRA load_in_4bit: true diff --git a/examples/yi-34B-chat/qlora.yml b/examples/yi-34B-chat/qlora.yml index 7fe322d63d..b68d008836 100644 --- a/examples/yi-34B-chat/qlora.yml +++ b/examples/yi-34B-chat/qlora.yml @@ -1,6 +1,9 @@ base_model: 01-ai/Yi-34B-Chat +# optionally might have model_type or tokenizer_type model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name load_in_8bit: false load_in_4bit: true From 8ddc18ec8d3e29de8e0227cdf49fb6ef1c65de5e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 17 Dec 2024 13:56:48 -0500 Subject: [PATCH 0343/1405] move the setting of PYTORCH_CUDA_ALLOC_CONF to the cli rather than train module (#2183) [skip ci] * move the setting of PYTORCH_CUDA_ALLOC_CONF to the cli rather than train module * move set_pytorch_cuda_alloc_conf to a different module to have fewer loaded dependencies for the CLI --- src/axolotl/cli/main.py | 4 ++++ src/axolotl/evaluate.py | 3 ++- src/axolotl/train.py | 5 +---- src/axolotl/utils/__init__.py | 11 ++++++++++- src/axolotl/utils/trainer.py | 11 ----------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index ec7f5b6947..6f883a2ac2 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -13,6 +13,7 @@ fetch_from_github, ) from axolotl.common.cli import EvaluateCliArgs, PreprocessCliArgs, TrainerCliArgs +from axolotl.utils import set_pytorch_cuda_alloc_conf from axolotl.utils.config.models.input.v0_4_1 import AxolotlInputConfig @@ -48,6 +49,9 @@ def train(config: str, accelerate: bool, **kwargs): """Train or fine-tune a model.""" kwargs = {k: v for k, v in kwargs.items() if v is not None} + # Enable expandable segments for cuda allocation to improve VRAM usage + set_pytorch_cuda_alloc_conf() + if accelerate: base_cmd = ["accelerate", "launch", "-m", "axolotl.cli.train"] if config: diff --git a/src/axolotl/evaluate.py b/src/axolotl/evaluate.py index 7fd60cb5f4..acf15e3fc7 100644 --- a/src/axolotl/evaluate.py +++ b/src/axolotl/evaluate.py @@ -12,9 +12,10 @@ from axolotl.common.cli import TrainerCliArgs from axolotl.logging_config import configure_logging from axolotl.train import TrainDatasetMeta +from axolotl.utils import set_pytorch_cuda_alloc_conf from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_model, load_processor, load_tokenizer -from axolotl.utils.trainer import set_pytorch_cuda_alloc_conf, setup_trainer +from axolotl.utils.trainer import setup_trainer project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) src_dir = os.path.join(project_root, "src") diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 851a71e547..af2ee97954 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -24,7 +24,7 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.freeze import freeze_layers_except from axolotl.utils.models import load_model, load_processor, load_tokenizer -from axolotl.utils.trainer import set_pytorch_cuda_alloc_conf, setup_trainer +from axolotl.utils.trainer import setup_trainer try: from optimum.bettertransformer import BetterTransformer @@ -53,9 +53,6 @@ class TrainDatasetMeta: def train( *, cfg: DictDefault, cli_args: TrainerCliArgs, dataset_meta: TrainDatasetMeta ) -> Tuple[Union[PeftModel, PreTrainedModel], PreTrainedTokenizer]: - # Enable expandable segments for cuda allocation to improve VRAM usage - set_pytorch_cuda_alloc_conf() - # Load tokenizer LOG.debug( f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}", diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index 4602054471..35ea145519 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -3,6 +3,7 @@ """ import importlib.util +import os import re import torch @@ -33,4 +34,12 @@ def get_pytorch_version() -> tuple[int, int, int]: return major, minor, patch -# pylint: enable=duplicate-code +def set_pytorch_cuda_alloc_conf(): + """Set up CUDA allocation config if using PyTorch >= 2.2""" + torch_version = torch.__version__.split(".") + torch_major, torch_minor = int(torch_version[0]), int(torch_version[1]) + if torch_major == 2 and torch_minor >= 2: + if os.getenv("PYTORCH_CUDA_ALLOC_CONF") is None: + os.environ[ + "PYTORCH_CUDA_ALLOC_CONF" + ] = "expandable_segments:True,roundup_power2_divisions:16" diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index fd09b3eb67..32e54c9a86 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -512,17 +512,6 @@ def prepare_opinionated_env(cfg): os.environ["TOKENIZERS_PARALLELISM"] = "false" -def set_pytorch_cuda_alloc_conf(): - """Set up CUDA allocation config if using PyTorch >= 2.2""" - torch_version = torch.__version__.split(".") - torch_major, torch_minor = int(torch_version[0]), int(torch_version[1]) - if torch_major == 2 and torch_minor >= 2: - if os.getenv("PYTORCH_CUDA_ALLOC_CONF") is None: - os.environ[ - "PYTORCH_CUDA_ALLOC_CONF" - ] = "expandable_segments:True,roundup_power2_divisions:16" - - def setup_trainer( cfg, train_dataset, eval_dataset, model, tokenizer, processor, total_num_steps ): From e246ceffa46ff44aa7866c3a4a810f51fb5249d8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 17 Dec 2024 13:57:16 -0500 Subject: [PATCH 0344/1405] use axolotl contribs for fix_untrained_tokens (#2194) [skip ci] * use axolotl contribs for fix_untrained_tokens * remove the module we're replacing * Add check for using fix_untrained_tokens --- requirements.txt | 2 + src/axolotl/__init__.py | 4 + src/axolotl/core/tokenizer_utils.py | 272 ---------------------------- src/axolotl/train.py | 4 +- tests/e2e/test_llama.py | 52 +++++- 5 files changed, 55 insertions(+), 279 deletions(-) delete mode 100644 src/axolotl/core/tokenizer_utils.py diff --git a/requirements.txt b/requirements.txt index 0373548d15..92da0163fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,3 +60,5 @@ antlr4-python3-runtime==4.13.2 torchao==0.7.0 schedulefree==1.3.0 + +axolotl-contribs-lgpl==0.0.1b2 diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index 9a31745983..8b0ba05320 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -1,3 +1,7 @@ """Axolotl - Train and fine-tune large language models""" +import pkgutil + +__path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package + __version__ = "0.6.0" diff --git a/src/axolotl/core/tokenizer_utils.py b/src/axolotl/core/tokenizer_utils.py deleted file mode 100644 index 1b86b9c497..0000000000 --- a/src/axolotl/core/tokenizer_utils.py +++ /dev/null @@ -1,272 +0,0 @@ -""" -helper functions for fixing the embeddings/tokenizer -""" - -# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. -# GNU LESSER GENERAL PUBLIC LICENSE -# Version 3, 29 June 2007 -# -# Copyright (C) 2007 Free Software Foundation, Inc. -# Everyone is permitted to copy and distribute verbatim copies -# of this license document, but changing it is not allowed. - -import gc -import itertools -import logging -from collections import Counter - -import datasets -import numpy as np -import torch - -LOG = logging.getLogger("axolotl.core.tokenizer_utils") - - -@torch.inference_mode() -def fix_untrained_tokens( # pylint: disable=too-many-return-statements - model, tokenizer, train_dataset, ignored_tokenizer_names=None, eps=1e-16 -): - """ - Llama-3 for eg has untrained vectors in the base model. - These include <|eot_id|>, <|start_header_id|>, <|end_header_id|> - We reset them to the mean of the rest of the tokens - """ - # Code licensed under LGPL - embedding_matrix = model.get_input_embeddings().weight - lm_head_matrix = model.get_output_embeddings().weight - chat_template = getattr(tokenizer, "chat_template", None) - tokenizer = tokenizer.tokenizer if hasattr(tokenizer, "tokenizer") else tokenizer - - # Ignore some model checks for now - if not ignored_tokenizer_names: - ignored_tokenizer_names = [] - if ( - model.config._name_or_path # pylint: disable=protected-access - in ignored_tokenizer_names - ): - return - - # Sometimes the sizes can be different like in vision models - # Ie is in input, but not in output - min_size = min(embedding_matrix.shape[1], lm_head_matrix.shape[1]) - embedding_matrix = embedding_matrix[:, :min_size] - lm_head_matrix = lm_head_matrix[:, :min_size] - - # Get untrained tokens - indicator_untrained1 = torch.amax(embedding_matrix, axis=1) <= eps - # Check lm_head as well - - # Does NOT work for Llama 3.1!! - indicator_untrained2 = torch.amax(lm_head_matrix, axis=1) <= eps - - # We instead check for repeated vectors - lm_head_where = torch.where(indicator_untrained1)[0] - lm_head_bad = lm_head_matrix[lm_head_where] - lm_head_bad = lm_head_bad.cpu().float().numpy().round(3) - counter = Counter() - for row in lm_head_bad: - counter[hash(row.data.tobytes())] += 1 - counter = Counter({k: c for k, c in counter.items() if c >= 2}) - - lm_head_where = lm_head_where.cpu().numpy() - final_bad_lm_head = [] - for j, row in enumerate(lm_head_bad): - if hash(row.data.tobytes()) in counter: - final_bad_lm_head.append(lm_head_where[j]) - indicator_untrained2 = indicator_untrained2 | torch.zeros_like(indicator_untrained2) - indicator_untrained2[final_bad_lm_head] = True - - # Combine both checks - indicator_untrained = indicator_untrained1 & indicator_untrained2 - - # Remove pad token possibility - if hasattr(tokenizer, "pad_token_id"): - pad_token_id = tokenizer.pad_token_id - if pad_token_id is not None and pad_token_id < indicator_untrained.shape[0]: - indicator_untrained[pad_token_id] = False - - where_untrained = torch.where(indicator_untrained)[0] - n_untrained = where_untrained.shape[0] - n_trained = embedding_matrix.shape[0] - n_untrained - - # Get set and actual tokens - where_untrained = where_untrained.tolist() - if len(where_untrained) == 0: - return - - # Remove untrained indices where it's longer - where_untrained_set = frozenset(where_untrained) - actual_bad_tokens = tokenizer.convert_ids_to_tokens(where_untrained) - # Remove None items in actual_bad_tokens - actual_bad_tokens = [x for x in actual_bad_tokens if x is not None] - - # Check if tokenizer and training datasets have bad tokens - if_bad_first = False - if_bad_second = False - # Check tokenizer's chat template for any untrained tokens - if chat_template is not None: - if_bad_first = any(x in chat_template for x in actual_bad_tokens) - - if isinstance(train_dataset, datasets.IterableDataset): - # Skip the check, since the code below assumes - # an indexable dataset - return - - # Check the first 250, last 250 input_ids - size_dataset = len(train_dataset) - size = min(size_dataset, 250) - for j in range(size): - input_ids = train_dataset[j] - if "input_ids" in input_ids: - input_ids = input_ids["input_ids"] - if_bad = any(item in where_untrained_set for item in input_ids) - if if_bad: - if_bad_second = True - break - - # Check last 250 - if not if_bad_second: - left = max(size_dataset - 250, 0) - for j in range(left, size_dataset): - input_ids = train_dataset[j] - if "input_ids" in input_ids: - input_ids = input_ids["input_ids"] - if_bad = any(item in where_untrained_set for item in input_ids) - if if_bad: - if_bad_second = True - break - - # Check if bad tokens exists! - if not if_bad_first and not if_bad_second: - return - - # Check if lm_head / embed_token are trainable! - bad_not_trainable = False - if not embedding_matrix.requires_grad: - bad_not_trainable = True - if not lm_head_matrix.requires_grad: - bad_not_trainable = True - - if bad_not_trainable: # pylint: disable=too-many-nested-blocks - final_bad_items = [] - - # Re-check the first 250, last 250 input_ids - size_dataset = len(train_dataset) - size = min(size_dataset, 250) - for j in range(size): - input_ids = train_dataset[j] - if "input_ids" in input_ids: - input_ids = input_ids["input_ids"] - for item in input_ids: - if item in where_untrained_set: - final_bad_items.append(item) - - # Re-check last 250 - left = max(size_dataset - 250, 0) - for j in range(left, size_dataset): - input_ids = train_dataset[j] - if "input_ids" in input_ids: - input_ids = input_ids["input_ids"] - for item in input_ids: - if item in where_untrained_set: - final_bad_items.append(item) - - # If no bad tokens, possibly chat template itself has issues? - if len(final_bad_items) == 0: - # Recheck 2000 and last 2000 items - size_dataset = len(train_dataset) - size = min(size_dataset, 2000) - for j in range(size): - input_ids = train_dataset[j] - if "input_ids" in input_ids: - input_ids = input_ids["input_ids"] - for item in input_ids: - if item in where_untrained_set: - final_bad_items.append(item) - - # Re-check last 2000 - left = max(size_dataset - 2000, 0) - for j in range(left, size_dataset): - input_ids = train_dataset[j] - if "input_ids" in input_ids: - input_ids = input_ids["input_ids"] - for item in input_ids: - if item in where_untrained_set: - final_bad_items.append(item) - - # Most likely false signal! - if len(final_bad_items) == 0: - return - - raise ValueError( - f"Untrained tokens of [{list(set(final_bad_items))}] found, but embed_tokens & lm_head not trainable, causing NaNs. " - ) - - # Count all the possible bad tokens - final_counts = np.zeros( - max(len(tokenizer), embedding_matrix.shape[0]), dtype=np.int64 - ) - - def mapping(examples): - input_ids = examples["input_ids"] - counter = np.fromiter(itertools.chain.from_iterable(input_ids), dtype=np.int32) - np.add.at(final_counts, counter, 1) - - train_dataset.map(mapping, batched=True, desc="Counting untrained tokens") - - # Get counts for untrained tokens - counts_untrained = final_counts[where_untrained] - # Identify untrained tokens seen in train_dataset - indices_seen_in_train = np.where(counts_untrained > 0)[0] - tokens_to_update = [where_untrained[i] for i in indices_seen_in_train] - - if len(tokens_to_update) == 0: - LOG.info( - "No untrained tokens found in train_dataset. No embeddings were modified." - ) - return - - # Log the token IDs that are being rescaled - LOG.info( - f"Rescaling embeddings for tokens seen in train_dataset: {tokens_to_update}" - ) - - # Get sum of all items - sum_embedding = torch.sum(embedding_matrix, dtype=torch.float32, axis=0) - sum_lm_head = torch.sum(lm_head_matrix, dtype=torch.float32, axis=0) - - # Remove bad tokens - sum_embedding -= torch.sum( - embedding_matrix[where_untrained], dtype=torch.float32, axis=0 - ) - sum_lm_head -= torch.sum( - lm_head_matrix[where_untrained], dtype=torch.float32, axis=0 - ) - - # Find correct average by dividing by sum of trained tokens - mean_embedding = sum_embedding / n_trained - mean_lm_head = sum_lm_head / n_trained - - # Compute scaling for tokens to update - scaling = counts_untrained[indices_seen_in_train] / max(final_counts.max(), 1) - scaling = torch.tensor(scaling, device=mean_embedding.device).unsqueeze(1) - - # Prepare mean embeddings for tokens to update - mean_embedding_repeated = ( - mean_embedding.unsqueeze(0).repeat(len(tokens_to_update), 1) * scaling - ) - mean_lm_head_repeated = ( - mean_lm_head.unsqueeze(0).repeat(len(tokens_to_update), 1) * scaling - ) - - # Update embeddings only for tokens seen in train_dataset - embedding_matrix[tokens_to_update] = mean_embedding_repeated.to( - embedding_matrix.dtype - ) - lm_head_matrix[tokens_to_update] = mean_lm_head_repeated.to(lm_head_matrix.dtype) - - # Clean up - for _ in range(3): - gc.collect() - torch.cuda.empty_cache() - return diff --git a/src/axolotl/train.py b/src/axolotl/train.py index af2ee97954..e25acc7d89 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -19,7 +19,9 @@ from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled from axolotl.common.cli import TrainerCliArgs -from axolotl.core.tokenizer_utils import fix_untrained_tokens +from axolotl.contribs.lgpl.unsloth import ( # pylint: disable = no-name-in-module + fix_untrained_tokens, +) from axolotl.logging_config import configure_logging from axolotl.utils.dict import DictDefault from axolotl.utils.freeze import freeze_layers_except diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index 4e885a76db..33d12157ad 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -4,7 +4,6 @@ import logging import os -import unittest from pathlib import Path from axolotl.cli import load_datasets @@ -13,18 +12,15 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir - LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" -class TestLlama(unittest.TestCase): +class TestLlama: """ Test case for Llama models """ - @with_temp_dir def test_fft_trust_remote_code(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -46,7 +42,8 @@ def test_fft_trust_remote_code(self, temp_dir): }, ], "num_epochs": 1, - "micro_batch_size": 8, + "max_steps": 5, + "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, @@ -64,3 +61,46 @@ def test_fft_trust_remote_code(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "model.safetensors").exists() + + def test_fix_untrained_tokens(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "fix_untrained_tokens": True, + "sequence_len": 512, + "val_set_size": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "chat_template": "chatml", + "datasets": [ + { + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "split": "train[:10%]", + "field_messages": "conversations", + "message_field_role": "from", + "message_field_content": "value", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": True, + "bf16": True, + "save_safetensors": True, + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() From d91feaffc864f6fa37a808b83bd48aab148121e5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 17 Dec 2024 13:58:21 -0500 Subject: [PATCH 0345/1405] upgrade to liger 0.5.2 (#2181) [skip ci] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 92da0163fe..61e1a9f901 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ mamba-ssm==1.2.0.post1 flash-attn==2.7.0.post2 xformers>=0.0.23.post1 autoawq==0.2.7.post3 -liger-kernel==0.4.2 +liger-kernel==0.5.2 # END section packaging==23.2 From 339f3c67e2d6855340b5958274ea539517829baa Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 17 Dec 2024 13:58:53 -0500 Subject: [PATCH 0346/1405] dataset tags don't support https uris (#2195) --- src/axolotl/train.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index e25acc7d89..dc7289b093 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -266,6 +266,9 @@ def terminate_handler(_, __, model_weakref): dataset_tags = [ d["path"] for d in cfg.datasets if not Path(d["path"]).is_dir() ] + dataset_tags = [ + d for d in dataset_tags if not d.startswith("https://") + ] if dataset_tags: # guard as create_model_card may fail if dataset_tags is empty list model_card_kwarg["dataset_name"] = dataset_tags @@ -273,6 +276,9 @@ def terminate_handler(_, __, model_weakref): dataset_tags = [ d["path"] for d in cfg.datasets if not Path(d["path"]).is_dir() ] + dataset_tags = [ + d for d in dataset_tags if not d.startswith("https://") + ] if dataset_tags: # guard as create_model_card may fail if dataset_tags is empty list model_card_kwarg["dataset_tags"] = dataset_tags From 10cfecf02e8829de749708c2588dc76be3a156d2 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 18 Dec 2024 04:42:21 +0700 Subject: [PATCH 0347/1405] fix: use apply_chat_template to find turn boundaries and allow tool_calling field (#2179) [skip ci] * fix: use apply_chat_template to find turn boundaries and allow tool_calling field * fix: keys to include in turn * feat(doc): explicitly recommend setting train_on_eos and roles_to_train * fix: eos not being masked for tool due to template padding * chore: clear up docs * fix: default messages format, train_on_eos: turn, and train on all assistant msg * fix: properly warn if empty content * feat: parametrize chat_template tests to test different tokenizers * fix: set proper default for message key * fix: update defaults to match load function * fix: change defaults to use new * feat: add tool_calling dataset * feat: add tool_calling test * fix: add handling of edge case of mistral tokenizer with only system prompt * feat: refactor all test to follow source code * fix: remove unnecessary eos_token from phi35 * fix test for phi3.5 since eos was dropped from chat_template --------- Co-authored-by: Wing Lian --- docs/config.qmd | 26 +- docs/dataset-formats/conversation.qmd | 6 +- .../prompt_strategies/chat_template.py | 163 ++-- src/axolotl/utils/chat_templates.py | 2 +- tests/prompt_strategies/conftest.py | 97 +- .../prompt_strategies/test_chat_templates.py | 163 +++- .../test_chat_templates_advanced.py | 829 ++++++++++++------ 7 files changed, 929 insertions(+), 357 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index 120aec8933..ba23384f0c 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -127,34 +127,40 @@ datasets: # - tokenizer_default_fallback_*: where * is the name of the chat template to fallback to if the tokenizer does not have a chat template else default to tokenizer. E.g. tokenizer_default_fallback_chatml. # - jinja: Uses a custom jinja template for the chat template. The custom jinja template should be provided in the chat_template_jinja field. chat_template: tokenizer_default - # Custom jinja template for chat template. This will be only used if `chat_template` is set to `jinja` or empty (in which case chat_template is automatically set to `jinja`). + + # Custom jinja chat template. Used only if `chat_template: jinja` or empty. chat_template_jinja: - # The key in the data example that contains the messages. Default is "messages". + + # Key containing the messages (default: "messages") field_messages: messages - # The key in the message turn that contains the role. Default is "role". + # Key for role in each message (default: "role") message_field_role: role - # The key in the message turn that contains the content. Default is "content". + # Key for content in each message (default: "content") message_field_content: content - # Optional[Dict[str, List]]. Roles mapping for the messages. + + # Optional[Dict[str, List]]. Roles mapping in the messages. The default is: roles: user: ["human", "user"] - assistant: ["gpt", "assistant", "ai"] + assistant: ["gpt", "assistant"] system: ["system"] + tool: ["tool"] - ## NOTE: Leaving the below empty will default to using the simple legacy tokenization strategy where only last message is trained on. + # IMPORTANT: The following fields determine which parts of the conversation to train on. + # Priority order: message_field_training > message_field_training_detail > train_on_inputs or role in roles_to_train + # See examples at `docs/dataset-formats/conversation.qmd` + # Note: If the below 4 fields are empty, defaults to training only on the last message. # Optional[List[str]]. Roles to train on. The tokens from these roles will be considered for the loss. - roles_to_train: ["gpt", "assistant"] + roles_to_train: ["assistant"] # default # Optional[str]. Which EOS tokens to train on in the conversation. Possible values are: # - all: train on all EOS tokens - # - turn: train on the EOS token at the end of each trainable turn + # - turn (default): train on the EOS token at the end of each trainable turn # - last: train on the last EOS token in the conversation train_on_eos: last # The key in the message turn that indicates via boolean whether tokens of a turn should be considered for training. Useful to selectively train on certain turns besides the `roles_to_train`. message_field_training: training # The key in the message turn that contains the training details. Useful to selectively train on certain tokens in a turn. # The value of the key is a List[Dict] containing `begin_offset` (start character index in content), `end_offset` (end character index in content), and `train` (boolean whether to train). - # See example at `docs/dataset-formats/conversation.qmd` message_field_training_detail: train_detail diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index fb9aed3ffa..9f6e8c3604 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -68,6 +68,8 @@ We recommend checking the below examples for other usecases. datasets: - path: ... type: chat_template + roles_to_train: + train_on_eos: ``` 2. Using the `gemma` chat template to override the tokenizer_config.json's chat template on OpenAI messages format, training on all assistant messages. @@ -77,7 +79,7 @@ chat_template: gemma # this overwrites the tokenizer's chat_template datasets: - path: ... type: chat_template - roles_to_train: ["assistant"] + roles_to_train: ["assistant"] # default value ``` 3. Using the tokenizer_config.json's chat template or `chatml` as fallback if the former's chat template does not exist, on OpenAI messages format, training on all assistant messages. @@ -87,7 +89,6 @@ chat_template: tokenizer_default_fallback_chatml # this overwrites the tokenizer datasets: - path: ... type: chat_template - roles_to_train: ["assistant"] ``` 4. Using a custom jinja template on OpenAI messages format, training on all assistant messages. @@ -99,7 +100,6 @@ chat_template_jinja: "{{ bos_token }}{% for message in messages %}{% if (message datasets: - path: ... type: chat_template - roles_to_train: ["assistant"] ``` 5. (Advanced) Using fine-grained control over tokens and turns to train in a conversation diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 35c9311678..5b12130d75 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -25,8 +25,8 @@ def __init__( processor=None, chat_template=None, max_length=2048, - message_field_role: str = "from", - message_field_content: str = "value", + message_field_role: str = "role", + message_field_content: str = "content", message_field_training: Optional[str] = None, message_field_training_detail: Optional[str] = None, roles: Optional[Dict[str, List[str]]] = None, @@ -41,6 +41,7 @@ def __init__( "assistant": "assistant", "gpt": "assistant", "system": "system", + "tool": "tool", } self.message_field_role = message_field_role @@ -188,7 +189,7 @@ class ChatTemplateStrategy(PromptTokenizingStrategy): Tokenizing strategy for instruction-based prompts. """ - _messages = "conversations" + _messages = "messages" def __init__( self, @@ -279,12 +280,7 @@ def tokenize_prompt(self, prompt): LOG.debug(f"Should train: {should_train}") - turn_start_idx, turn_end_idx = self.find_turn( - conversation_ids=input_ids, turn=index, turn_content=turn - ) - - if turn_start_idx == -1 or turn_end_idx == -1: - LOG.warning(f"Failed to find boundaries for turn {index}") + turn_start_idx, turn_end_idx = self.find_turn(turns=turns, turn_idx=index) LOG.debug(f"Turn indices: start={turn_start_idx}, end={turn_end_idx}") @@ -313,8 +309,8 @@ def tokenize_prompt(self, prompt): LOG.debug(f"Labels after processing turn {index}: {labels}") # Handle EOS token - eos_idx = self.find_eos_token(input_ids, turn_end_idx) - if eos_idx == turn_end_idx: + eos_idx = self.find_first_eos_token(input_ids, start_idx=turn_end_idx) + if abs(eos_idx - turn_end_idx) <= 3: # Allow for some template padding last_eos_idx = eos_idx if self.train_on_eos == "all" or ( self.train_on_eos == "turn" and should_train @@ -339,75 +335,120 @@ def tokenize_prompt(self, prompt): "attention_mask": [1] * len(input_ids), } - def find_eos_token(self, input_ids, start_idx): + def find_first_eos_token(self, input_ids, start_idx): eos_token_id = self.tokenizer.eos_token_id for i in range(start_idx, len(input_ids)): if input_ids[i] == eos_token_id: return i return -1 - def find_turn(self, conversation_ids: list[int], turn: int, turn_content: dict): + def find_turn(self, turns: list[dict], turn_idx: int): """ Locate the starting and ending indices of the specified turn in a conversation. """ - content = turn_content.get("content") - content_ids = self.tokenizer.encode(content, add_special_tokens=False) + # pylint: disable=too-many-return-statements - LOG.debug(f"content_ids (length {len(content_ids)}): {content_ids}") + if turn_idx >= len(turns): + raise ValueError(f"Turn index {turn_idx} out of range") - if not content_ids: - LOG.warning(f"Empty content for turn {turn}") + # mistral does not output message if it contains only system message + if ( + turn_idx == 0 + and turns[0].get("role") == "system" + and "mistral" in self.tokenizer.name_or_path.lower() + ): return -1, -1 - # For first turn, start from beginning - if turn == 0: - start_search_idx = 0 - else: - # For subsequent turns, find the previous EOS token - eos_token_id = self.tokenizer.eos_token_id - eos_count = 0 - start_search_idx = 0 - - for i, token_id in enumerate(conversation_ids): - if token_id == eos_token_id: - eos_count += 1 - if eos_count == turn: # Find the nth EOS token where n = turn - start_search_idx = i + 1 - break - - # we can optimize this to only search for a few tokens from start_search_idx - # but it would risk missing the content if it's not found within the first few tokens or - # if start_search_idx cannot be found above. - last_index = len(conversation_ids) - len(content_ids) + 1 - - if last_index < start_search_idx: + empty_turn = { + "role": turns[turn_idx].get("role"), + "content": "[[dummy_message]]", + } + + # Create conversation versions + turns_with_empty = turns[:turn_idx] + [empty_turn] + turns_with_content = turns[: turn_idx + 1] + + # Generate the conversation up to the turn, with final turn replaced with dummy content + dummy_ids = self.prompter.build_prompt(turns_with_empty) # type: ignore + + # Generate the conversation up to the turn, with final turn included + full_ids = self.prompter.build_prompt(turns_with_content) # type: ignore + + if not full_ids or not dummy_ids: + LOG.warning(f"Empty template generated for turn {turn_idx}") + return -1, -1 + + # Find first difference (start of content) + start_idx = None + min_len = min(len(dummy_ids), len(full_ids)) + for i in range(min_len): + if dummy_ids[i] != full_ids[i]: + start_idx = i + break + + if start_idx is None: + LOG.warning(f"Could not find content start boundary for turn {turn_idx}") + return -1, -1 + + # Find last difference (end of content) + end_idx = None + for i in range(min_len): + dummy_pos = len(dummy_ids) - 1 - i + full_pos = len(full_ids) - 1 - i + if dummy_ids[dummy_pos] != full_ids[full_pos]: + end_idx = full_pos + 1 # Add one to include the last token when slice + break + + if end_idx is None: + LOG.warning(f"Could not find content end boundary for turn {turn_idx}") + return -1, -1 + + if end_idx < start_idx: + LOG.warning( + f"Content end boundary is before start boundary for turn {turn_idx}" + ) + return -1, -1 + + if end_idx == start_idx: LOG.warning( - f"last_index to search is less than start_search_idx for turn {turn}" + f"Content end boundary is the same as start boundary for turn {turn_idx}. This is likely an empty turn." ) return -1, -1 - # Search for content starting from start_search_idx - first_elem = content_ids[0] - for i in range(start_search_idx, last_index): - # Quick check of first element before doing full comparison - if conversation_ids[i] == first_elem: - # Check if the rest of the content matches - if conversation_ids[i : i + len(content_ids)] == content_ids: - LOG.debug(f"Found turn {turn} content at position {i}") - return i, i + len(content_ids) + LOG.debug(f"Content boundaries: {start_idx}, {end_idx}") + LOG.debug( + f"Content tokens: {self.tokenizer.convert_ids_to_tokens(full_ids[start_idx:end_idx])}" + ) - return -1, -1 + return start_idx, end_idx def get_conversation_thread(self, prompt): - turns = [ - { - "role": self.prompter.roles[t[self.prompter.message_field_role]], - "content": t[self.prompter.message_field_content], - "training": t.get(self.prompter.message_field_training), - "training_detail": t.get(self.prompter.message_field_training_detail), - } - for t in prompt[self.messages] + turns = [] + optional_keys = [ + "tool_calls", # tool that 'assistant' calls + "name", # name of tool given by 'tool' + "tool_call_id", # mistral/mixtral requires this ] + for message in prompt[self.messages]: + turn = { + "role": self.prompter.roles[message[self.prompter.message_field_role]], + "training": message.get(self.prompter.message_field_training), + "training_detail": message.get( + self.prompter.message_field_training_detail + ), + } + + # do not add content if None as it may conflict with some templates due to tools + content = message.get(self.prompter.message_field_content, None) + if content is not None: + turn["content"] = content + + for key in optional_keys: + value = message.get(key, None) + if value is not None: + turn[key] = value + + turns.append(turn) if self.prompter.drop_system_message and turns[0]["role"] == "system": turns = turns[1:] @@ -446,8 +487,8 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None, processor=None strategy_params = { "train_on_inputs": cfg.train_on_inputs, "sequence_len": cfg.sequence_len, - "roles_to_train": ds_cfg.get("roles_to_train", []), - "train_on_eos": ds_cfg.get("train_on_eos", None), + "roles_to_train": ds_cfg.get("roles_to_train", ["assistant"]), + "train_on_eos": ds_cfg.get("train_on_eos", "turn"), } strategy = ChatTemplateStrategy( diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index ffe5e24853..682a0449e8 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -25,7 +25,7 @@ "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", "llama3_2_vision": '{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now("%d %b %Y") %}\n {%- else %}\n {%- set date_string = "26 Jul 2024" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0][\'role\'] == \'system\' %}\n {%- set system_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n{%- else %}\n {%- set system_message = "" %}\n{%- endif %}\n\n{#- Find out if there are any images #}\n{% set image_ns = namespace(has_images=false) %} \n{%- for message in messages %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {%- set image_ns.has_images = true %}\n {%- endif %}\n {%- endfor %}\n{%- endfor %}\n\n{#- Error out if there are images and system message #}\n{%- if image_ns.has_images and not system_message == "" %}\n {{- raise_exception("Prompting with images is incompatible with system messages.") }}\n{%- endif %}\n\n{#- System message if there are no images #}\n{%- if not image_ns.has_images %}\n {{- "<|start_header_id|>system<|end_header_id|>\\n\\n" }}\n {%- if tools is not none %}\n {{- "Environment: ipython\\n" }}\n {%- endif %}\n {{- "Cutting Knowledge Date: December 2023\\n" }}\n {{- "Today Date: " + date_string + "\\n\\n" }}\n {%- if tools is not none and not tools_in_user_message %}\n {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- "<|eot_id|>" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception("Cannot put tools in the first user message when there\'s no first user message!") }}\n{%- endif %}\n {{- \'<|start_header_id|>user<|end_header_id|>\\n\\n\' -}}\n {{- "Given the following functions, please respond with a JSON for a function call " }}\n {{- "with its proper arguments that best answers the given prompt.\\n\\n" }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {{- first_user_message + "<|eot_id|>"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == \'ipython\' or message.role == \'tool\' or \'tool_calls\' in message) %}\n {{- \'<|start_header_id|>\' + message[\'role\'] + \'<|end_header_id|>\\n\\n\' }}\n {%- if message[\'content\'] is string %}\n {{- message[\'content\'] }}\n {%- else %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {{- \'<|image|>\' }}\n {%- elif content[\'type\'] == \'text\' %}\n {{- content[\'text\'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \'<|eot_id|>\' }}\n {%- elif \'tool_calls\' in message %}\n {%- if not message.tool_calls|length == 1 %}\n {{- raise_exception("This model only supports single tool-calls at once!") }}\n {%- endif %}\n {%- set tool_call = message.tool_calls[0].function %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' -}}\n {{- \'{"name": "\' + tool_call.name + \'", \' }}\n {{- \'"parameters": \' }}\n {{- tool_call.arguments | tojson }}\n {{- "}" }}\n {{- "<|eot_id|>" }}\n {%- elif message.role == "tool" or message.role == "ipython" %}\n {{- "<|start_header_id|>ipython<|end_header_id|>\\n\\n" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- "<|eot_id|>" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' }}\n{%- endif %}\n', "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", - "phi_35": "{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'user' %}{{'<|user|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|>\n' + message['content'] + '<|end|>\n'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% else %}{{ eos_token }}{% endif %}", + "phi_35": "{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'user' %}{{'<|user|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|>\n' + message['content'] + '<|end|>\n'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% endif %}", "deepseek_v2": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %}", "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', "qwen_25": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n", diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index 00a2bf3004..fdfcbff438 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -7,6 +7,8 @@ from huggingface_hub import hf_hub_download from transformers import AutoTokenizer +from axolotl.utils.chat_templates import _CHAT_TEMPLATES + @pytest.fixture(name="assistant_dataset") def fixture_assistant_dataset(): @@ -59,7 +61,52 @@ def fixture_basic_dataset(): ) -@pytest.fixture(name="llama3_tokenizer") +@pytest.fixture(name="toolcalling_dataset") +def fixture_toolcalling_dataset(): + # pylint: disable=duplicate-code + return Dataset.from_list( + [ + { + "messages": [ + { + "role": "system", + "content": "You are a bot that responds to weather queries. You should reply with the unit used in the queried location.", + }, + { + "role": "user", + "content": "Hey, what's the temperature in Paris right now?", + }, + { + "role": "assistant", + "tool_calls": [ + { + "type": "function", + "function": { + "name": "get_current_temperature", + "arguments": { + "location": "Paris, France", + "unit": "celsius", + }, + }, + } + ], + }, + { + "role": "tool", + "name": "get_current_temperature", + "content": "22.0", + }, + { + "role": "assistant", + "content": "The temperature in Paris is 22.0 degrees Celsius.", + }, + ] + } + ] + ) + + +@pytest.fixture(name="llama3_tokenizer", scope="session", autouse=True) def fixture_llama3_tokenizer(): hf_hub_download( repo_id="NousResearch/Meta-Llama-3-8B-Instruct", @@ -77,7 +124,53 @@ def fixture_llama3_tokenizer(): return tokenizer -@pytest.fixture(name="phi35_tokenizer") +@pytest.fixture(name="mistralv03_tokenizer", scope="session", autouse=True) +def fixture_mistralv03_tokenizer(): + tokenizer = AutoTokenizer.from_pretrained( + "mlx-community/Mistral-7B-Instruct-v0.3-4bit" + ) + return tokenizer + + +@pytest.fixture(name="phi35_tokenizer", scope="session", autouse=True) def fixture_phi35_tokenizer(): tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3.5-mini-instruct") return tokenizer + + +@pytest.fixture(name="gemma2_tokenizer", scope="session", autouse=True) +def fixture_gemma2_tokenizer(): + tokenizer = AutoTokenizer.from_pretrained("mlx-community/gemma-2-9b-it-4bit") + + return tokenizer + + +@pytest.fixture(name="mistralv03_tokenizer_chat_template_jinja") +def fixture_mistralv03_chat_template_jinja_w_system() -> str: + return '{%- if messages[0]["role"] == "system" %}\n {%- set system_message = messages[0]["content"] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set loop_messages = messages %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n{%- set user_messages = loop_messages | selectattr("role", "equalto", "user") | list %}\n\n{#- This block checks for alternating user/assistant messages, skipping tool calling messages #}\n{%- set ns = namespace() %}\n{%- set ns.index = 0 %}\n{%- for message in loop_messages %}\n {%- if not (message.role == "tool" or message.role == "tool_results" or (message.tool_calls is defined and message.tool_calls is not none)) %}\n {%- if (message["role"] == "user") != (ns.index % 2 == 0) %}\n {{- raise_exception("After the optional system message, conversation roles must alternate user/assistant/user/assistant/...") }}\n {%- endif %}\n {%- set ns.index = ns.index + 1 %}\n {%- endif %}\n{%- endfor %}\n\n{{- bos_token }}\n{%- for message in loop_messages %}\n {%- if message["role"] == "user" %}\n {%- if tools is not none and (message == user_messages[-1]) %}\n {{- "[AVAILABLE_TOOLS] [" }}\n {%- for tool in tools %}\n {%- set tool = tool.function %}\n {{- \'{"type": "function", "function": {\' }}\n {%- for key, val in tool.items() if key != "return" %}\n {%- if val is string %}\n {{- \'"\' + key + \'": "\' + val + \'"\' }}\n {%- else %}\n {{- \'"\' + key + \'": \' + val|tojson }}\n {%- endif %}\n {%- if not loop.last %}\n {{- ", " }}\n {%- endif %}\n {%- endfor %}\n {{- "}}" }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- else %}\n {{- "]" }}\n {%- endif %}\n {%- endfor %}\n {{- "[/AVAILABLE_TOOLS]" }}\n {%- endif %}\n {%- if loop.first and system_message is defined %}\n {{- "[INST] " + system_message + "\\n\\n" + message["content"] + "[/INST]" }}\n {%- else %}\n {{- "[INST] " + message["content"] + "[/INST]" }}\n {%- endif %}\n {%- elif message.tool_calls is defined and message.tool_calls is not none %}\n {{- "[TOOL_CALLS] [" }}\n {%- for tool_call in message.tool_calls %}\n {%- set out = tool_call.function|tojson %}\n {{- out[:-1] }}\n {%- if not tool_call.id is defined or tool_call.id|length != 9 %}\n {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }}\n {%- endif %}\n {{- \', "id": "\' + tool_call.id + \'"}\' }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- else %}\n {{- "]" + eos_token }}\n {%- endif %}\n {%- endfor %}\n {%- elif message["role"] == "assistant" %}\n {{- " " + message["content"]|trim + eos_token}}\n {%- elif message["role"] == "tool_results" or message["role"] == "tool" %}\n {%- if message.content is defined and message.content.content is defined %}\n {%- set content = message.content.content %}\n {%- else %}\n {%- set content = message.content %}\n {%- endif %}\n {{- \'[TOOL_RESULTS] {"content": \' + content|string + ", " }}\n {%- if not message.tool_call_id is defined or message.tool_call_id|length != 9 %}\n {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }}\n {%- endif %}\n {{- \'"call_id": "\' + message.tool_call_id + \'"}[/TOOL_RESULTS]\' }}\n {%- else %}\n {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }}\n {%- endif %}\n{%- endfor %}\n' + + +@pytest.fixture(name="gemma2_tokenizer_chat_template_jinja") +def fixture_gemma2_chat_template_jinja_w_system() -> str: + return "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\n' + message['content'] | trim + '\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\n'}}{% endif %}" + + +@pytest.fixture(name="llama3_2_vision_chat_template_jinja") +def fixture_llama3_2_vision_with_hardcoded_date() -> str: + """Hardcodes the date in the template to avoid the need for date logic in the prompt""" + + template = _CHAT_TEMPLATES["llama3_2_vision"] + + old_date_logic = """{%- if not date_string is defined %} + {%- if strftime_now is defined %} + {%- set date_string = strftime_now("%d %b %Y") %} + {%- else %} + {%- set date_string = "26 Jul 2024" %} + {%- endif %} +{%- endif %}""" + + new_date_logic = """{%- set date_string = "17 Dec 2024" %}""" + + modified_template = template.replace(old_date_logic, new_date_logic) + + return modified_template diff --git a/tests/prompt_strategies/test_chat_templates.py b/tests/prompt_strategies/test_chat_templates.py index 4ec12b82cb..8ec4fa1191 100644 --- a/tests/prompt_strategies/test_chat_templates.py +++ b/tests/prompt_strategies/test_chat_templates.py @@ -140,7 +140,6 @@ def test_phi35(self, phi35_tokenizer, assistant_dataset): 1781, 26966, 32007, # user eot 32001, # assistant 1781, 26966, 32007, # assistant eot - 32000, # eos ] expected_labels = [ -100, # user @@ -151,7 +150,6 @@ def test_phi35(self, phi35_tokenizer, assistant_dataset): -100, -100, -100, # user eot -100, # assistant 1781, 26966, 32007, # assistant eot - 32000, # eos ] # fmt: on LOG.debug(f"Expected input_ids: {expected_input_ids}") @@ -230,7 +228,10 @@ def test_llama3_assistant(self, llama3_tokenizer, sharegpt_dataset): # pylint: disable=duplicate-code strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=get_chat_template("llama3") + llama3_tokenizer, + chat_template=get_chat_template("llama3"), + message_field_role="from", + message_field_content="value", ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -238,6 +239,7 @@ def test_llama3_assistant(self, llama3_tokenizer, sharegpt_dataset): sequence_len=512, roles_to_train=["gpt"], ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(sharegpt_dataset[0]) input_ids = res["input_ids"] labels = res["labels"] @@ -283,7 +285,10 @@ def test_llama3_human(self, llama3_tokenizer, sharegpt_dataset): # pylint: disable=duplicate-code strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=get_chat_template("llama3") + llama3_tokenizer, + chat_template=get_chat_template("llama3"), + message_field_role="from", + message_field_content="value", ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -291,6 +296,7 @@ def test_llama3_human(self, llama3_tokenizer, sharegpt_dataset): sequence_len=512, roles_to_train=["human"], ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(sharegpt_dataset[0]) input_ids = res["input_ids"] labels = res["labels"] @@ -336,7 +342,10 @@ def test_llama3_system_human(self, llama3_tokenizer, basic_dataset): # pylint: disable=duplicate-code strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=get_chat_template("llama3") + llama3_tokenizer, + chat_template=get_chat_template("llama3"), + message_field_role="from", + message_field_content="value", ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -344,6 +353,7 @@ def test_llama3_system_human(self, llama3_tokenizer, basic_dataset): sequence_len=512, roles_to_train=["system", "human"], ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) input_ids = res["input_ids"] labels = res["labels"] @@ -389,5 +399,148 @@ def test_llama3_system_human(self, llama3_tokenizer, basic_dataset): ), f"Labels mismatch: {labels} != {expected_labels}" +class TestAssistantToolCallingChatTemplateLlama32Vision: + """ + Test class for assistant style datasets with tool_calling prompts using the llama-32_vision chat template. + """ + + def test_llama32vision_train_on_assistant( + self, llama3_tokenizer, toolcalling_dataset, llama3_2_vision_chat_template_jinja + ): + LOG.info( + "Testing assistant style datasets with tool_calling with llama-32 chat template, training on assistant" + ) + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + llama3_tokenizer, + chat_template=get_chat_template( + "jinja", jinja_template=llama3_2_vision_chat_template_jinja + ), + message_field_role="role", + message_field_content="content", + ), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + train_on_eos="turn", + sequence_len=512, + roles_to_train=["assistant"], + ) + + res = strategy.tokenize_prompt(toolcalling_dataset[0]) + + input_ids = res["input_ids"] + labels = res["labels"] + + # fmt: off + expected_input_ids = [ + 128000, # bos + 128006, 9125, 128007, 271, # system header + 38766, 1303, 33025, 2696, 25, 6790, 220, 2366, 18, 198, 15724, 2696, 25, 220, 1114, 3799, 220, 2366, 19, 271, # system date prompt + 2675, 527, 264, 11164, 430, 31680, 311, 9282, 20126, 13, 1472, 1288, 10052, 449, 279, 5089, 1511, 304, 279, 79002, 3813, 13, 128009, # system message + 128006, 882, 128007, 271, # user header + 19182, 11, 1148, 596, 279, 9499, 304, 12366, 1314, 1457, 30, 128009, # user message + 128006, 78191, 128007, 271, # assistant header + 5018, 609, 794, 330, 456, 11327, 54625, 498, 330, 14105, 794, 5324, 2588, 794, 330, 60704, 11, 9822, 498, 330, 3928, 794, 330, 66, 41347, 32075, 128009, # assistant message + 128006, 23799, 4690, 128007, 271, # tool header + 1, 1313, 13, 15, 1, 128009, # tool message + 128006, 78191, 128007, 271, # assistant header + 791, 9499, 304, 12366, 374, 220, 1313, 13, 15, 12628, 62447, 13, 128009 # assistant message + ] + + expected_labels = [ + IGNORE_TOKEN_ID, # bos + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # system header + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # system date prompt + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # system message + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # user header + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # user message + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # assistant header + 5018, 609, 794, 330, 456, 11327, 54625, 498, 330, 14105, 794, 5324, 2588, 794, 330, 60704, 11, 9822, 498, 330, 3928, 794, 330, 66, 41347, 32075, 128009, # assistant message + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # tool header + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # tool message + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # assistant header + 791, 9499, 304, 12366, 374, 220, 1313, 13, 15, 12628, 62447, 13, 128009 # assistant message + ] + # fmt: on + + assert ( + input_ids == expected_input_ids + ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + + assert ( + labels == expected_labels + ), f"Labels mismatch: {labels} != {expected_labels}" + + def test_llama32vision_train_on_tools( + self, llama3_tokenizer, toolcalling_dataset, llama3_2_vision_chat_template_jinja + ): + LOG.info( + "Testing assistant style datasets with tool_calling with llama-32 chat template, training on tools" + ) + # pylint: disable=duplicate-code + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + llama3_tokenizer, + chat_template=get_chat_template( + "jinja", jinja_template=llama3_2_vision_chat_template_jinja + ), + message_field_role="role", + message_field_content="content", + ), + tokenizer=llama3_tokenizer, + train_on_inputs=False, + train_on_eos="turn", + sequence_len=512, + roles_to_train=["assistant", "tool"], + ) + + res = strategy.tokenize_prompt(toolcalling_dataset[0]) + + input_ids = res["input_ids"] + labels = res["labels"] + + # fmt: off + expected_input_ids = [ + 128000, # bos + 128006, 9125, 128007, 271, # system header + 38766, 1303, 33025, 2696, 25, 6790, 220, 2366, 18, 198, 15724, 2696, 25, 220, 1114, 3799, 220, 2366, 19, 271, # system date prompt + 2675, 527, 264, 11164, 430, 31680, 311, 9282, 20126, 13, 1472, 1288, 10052, 449, 279, 5089, 1511, 304, 279, 79002, 3813, 13, 128009, # system message + 128006, 882, 128007, 271, # user header + 19182, 11, 1148, 596, 279, 9499, 304, 12366, 1314, 1457, 30, 128009, # user message + 128006, 78191, 128007, 271, # assistant header + 5018, 609, 794, 330, 456, 11327, 54625, 498, 330, 14105, 794, 5324, 2588, 794, 330, 60704, 11, 9822, 498, 330, 3928, 794, 330, 66, 41347, 32075, 128009, # assistant message + 128006, 23799, 4690, 128007, 271, # tool header + 1, 1313, 13, 15, 1, 128009, # tool message + 128006, 78191, 128007, 271, # assistant header + 791, 9499, 304, 12366, 374, 220, 1313, 13, 15, 12628, 62447, 13, 128009 # assistant message + ] + + expected_labels = [ + IGNORE_TOKEN_ID, # bos + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # system header + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # system date prompt + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # system message + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # user header + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # user message + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # assistant header + 5018, 609, 794, 330, 456, 11327, 54625, 498, 330, 14105, 794, 5324, 2588, 794, 330, 60704, 11, 9822, 498, 330, 3928, 794, 330, 66, 41347, 32075, 128009, # assistant message + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # tool header + IGNORE_TOKEN_ID, 1313, 13, 15, IGNORE_TOKEN_ID, 128009, # tool message + IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, IGNORE_TOKEN_ID, # assistant header + 791, 9499, 304, 12366, 374, 220, 1313, 13, 15, 12628, 62447, 13, 128009 # assistant message + ] + # fmt: on + + assert ( + input_ids == expected_input_ids + ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + + assert ( + labels == expected_labels + ), f"Labels mismatch: {labels} != {expected_labels}" + + if __name__ == "__main__": unittest.main() diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index be8e3ccdf9..7d09b059cc 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -4,8 +4,12 @@ import logging import unittest +from copy import deepcopy +import pytest from datasets import Dataset +from tokenizers import AddedToken +from transformers import PreTrainedTokenizer from axolotl.prompt_strategies.chat_template import ( ChatTemplatePrompter, @@ -17,7 +21,30 @@ logging.basicConfig(level=logging.DEBUG) LOG = logging.getLogger("axolotl") - +PARAMETRIZE_KEYS = "tokenizer, chat_template, chat_template_jinja, eos_token" +PARAMETRIZE_PARAMS = [ + ("llama3_tokenizer", "llama3", None, None), + ("llama3_tokenizer", "chatml", None, "<|im_end|>"), + ( + "mistralv03_tokenizer", + "jinja", + "mistralv03_tokenizer_chat_template_jinja", + "[/INST]", + ), + ( + "gemma2_tokenizer", + "jinja", + "gemma2_tokenizer_chat_template_jinja", + "", + ), + ("phi35_tokenizer", "phi_35", None, "<|end|>"), +] + + +@pytest.mark.parametrize( + PARAMETRIZE_KEYS, + PARAMETRIZE_PARAMS, +) class TestChatTemplateConfigurations: """ Test class for various configurations of ChatTemplateStrategy. @@ -31,167 +58,318 @@ def find_sublist(full_list, sub_list): return index return -1 - def test_train_on_inputs_true(self, llama3_tokenizer, basic_dataset): + @staticmethod + def setup_tokenizer( + tokenizer_name, + chat_template, + chat_template_jinja=None, + eos_token=None, + request=None, + ) -> tuple[PreTrainedTokenizer, str]: + """ + Helper function to set up the tokenizer and chat template for the test. + """ + tokenizer = deepcopy(request.getfixturevalue(tokenizer_name)) + if chat_template == "jinja": + chat_template_jinja = request.getfixturevalue(chat_template_jinja) + if eos_token: + tokenizer.add_special_tokens( + { + "eos_token": AddedToken( + eos_token, rstrip=False, lstrip=False, normalized=False + ) + } + ) + if tokenizer.__class__.__name__ in ( + "LlamaTokenizerFast", + "CodeLlamaTokenizerFast", + ): + tokenizer.update_post_processor() + return tokenizer, chat_template_jinja + + def _should_skip_turn(self, tokenizer, turn, turn_idx, start_idx, end_idx): + """Helper method to determine if a turn should be skipped in testing. + This is used to skip system messages for Mistral as the template does not output them without more turns. + """ + if ( + turn_idx == 0 + and turn.get("from") in ["system", "context"] + and "mistral" in tokenizer.name_or_path.lower() + ): + assert ( + start_idx == -1 and end_idx == -1 + ), "Expected system message to be skipped" + return True + return False + + def test_train_on_inputs_true( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, + request, + ): LOG.info("Testing with train_on_inputs=True") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=get_chat_template("llama3") + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_field_role="from", + message_field_content="value", ), - tokenizer=llama3_tokenizer, + tokenizer=tokenizer, train_on_inputs=True, sequence_len=512, roles_to_train=["assistant"], ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) + turns = strategy.get_conversation_thread(basic_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] - # Verify that assistant responses are labeled - assistant_responses = ["Hi there!", "I'm doing well, thank you!"] - for response in assistant_responses: - response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, response_ids) - LOG.debug( - f"Assistant response '{response}' expected IDs: {response_ids}, found at: {start_idx}" + # Verify assistant responses are labeled + for i, turn in enumerate(basic_dataset[0]["conversations"]): + start_idx, end_idx = strategy.find_turn(turns=turns, turn_idx=i) + + if self._should_skip_turn(tokenizer, turn, i, start_idx, end_idx): + continue + + decoded_response = tokenizer.decode(input_ids[start_idx:end_idx]) + response = turn["value"] + + assert response in decoded_response, ( + f"Response {response} not found in index {start_idx}:{end_idx} " + f"decoded:{decoded_response}" ) - assert start_idx != -1, f"Could not find '{response}' in input_ids" + assert all( - label != IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(response_ids)] - ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" - - # Check the behavior of human inputs - human_inputs = ["Hello", "How are you?"] - for input_text in human_inputs: - input_ids = llama3_tokenizer.encode(input_text, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, input_ids) - labeled = all( - label != IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(input_ids)] - ) - LOG.debug( - f"Human input '{input_text}' is {'labeled' if labeled else 'not labeled'}, expected IDs: {input_ids}, found at: {start_idx}" - ) + label != IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] + ), f"Expected labels for input '{response}' to be ignored, but got {labels[start_idx:end_idx]}" LOG.debug("Full labels: %s", labels) LOG.debug("Full input_ids: %s", input_ids) - def test_train_on_inputs_false(self, llama3_tokenizer, basic_dataset): - LOG.info("Testing with train_on_inputs=False") + def test_train_on_inputs_false( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, + request, + ): + LOG.info("Testing with train_on_inputs=False, on assistant only") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=get_chat_template("llama3") + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_field_role="from", + message_field_content="value", ), - tokenizer=llama3_tokenizer, + tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, roles_to_train=["assistant"], ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) + turns = strategy.get_conversation_thread(basic_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] - # Verify that only assistant responses are labeled - assistant_responses = ["Hi there!", "I'm doing well, thank you!"] - for response in assistant_responses: - response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, response_ids) - LOG.debug( - f"Assistant response '{response}' expected IDs: {response_ids}, found at: {start_idx}" - ) - assert start_idx != -1, f"Could not find '{response}' in input_ids" - assert all( - label != IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(response_ids)] - ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" - - # Verify that human inputs are not labeled - human_inputs = ["Hello", "How are you?"] - for input_text in human_inputs: - input_ids = llama3_tokenizer.encode(input_text, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, input_ids) - LOG.debug( - f"Human input '{input_text}' expected IDs: {input_ids}, found at: {start_idx}" + # Process all turns and verify correct labeling based on role + for i, turn in enumerate(basic_dataset[0]["conversations"]): + start_idx, end_idx = strategy.find_turn(turns=turns, turn_idx=i) + + if self._should_skip_turn(tokenizer, turn, i, start_idx, end_idx): + continue + + decoded_response = tokenizer.decode(input_ids[start_idx:end_idx]) + response = turn["value"] + + assert response in decoded_response, ( + f"Response {response} not found in index {start_idx}:{end_idx} " + f"decoded:{decoded_response}" ) - assert start_idx != -1, f"Could not find '{input_text}' in input_ids" - assert all( - label == IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(input_ids)] - ), f"Expected labels for human input '{input_text}' to be IGNORE_TOKEN_ID, but got {labels[start_idx:start_idx+len(input_ids)]}" - def test_roles_to_train_assistant_only(self, llama3_tokenizer, basic_dataset): - LOG.info("Testing roles_to_train with assistant only") + # Verify that assistant responses are labeled and other inputs are not + is_assistant = turn["from"] == "assistant" + if is_assistant: + assert all( + label != IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] + ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:end_idx]}" + else: + assert all( + label == IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] + ), f"Expected labels for human input '{response}' to be IGNORE_TOKEN_ID, but got {labels[start_idx:end_idx]}" + + def test_roles_to_train_human_assistant_only( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, + request, + ): + LOG.info("Testing roles_to_train with human assistant only") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=get_chat_template("llama3") + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_field_role="from", + message_field_content="value", ), - tokenizer=llama3_tokenizer, + tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, - roles_to_train=["assistant"], + roles_to_train=["assistant", "human"], ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] - # Verify that only assistant responses are labeled - assistant_responses = ["Hi there!", "I'm doing well, thank you!"] - for response in assistant_responses: - response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, response_ids) - LOG.debug( - f"Assistant response '{response}' expected IDs: {response_ids}, found at: {start_idx}" + strategy.messages = "conversations" + res = strategy.tokenize_prompt(basic_dataset[0]) + turns = strategy.get_conversation_thread(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Process all turns and verify correct labeling based on role + for i, turn in enumerate(basic_dataset[0]["conversations"]): + start_idx, end_idx = strategy.find_turn(turns=turns, turn_idx=i) + + if self._should_skip_turn(tokenizer, turn, i, start_idx, end_idx): + continue + + decoded_response = tokenizer.decode(input_ids[start_idx:end_idx]) + response = turn["value"] + + assert response in decoded_response, ( + f"Response {response} not found in index {start_idx}:{end_idx} " + f"decoded:{decoded_response}" ) - assert all( - label != IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(response_ids)] - ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" - def test_roles_to_train_all(self, llama3_tokenizer, basic_dataset): + # Verify that non-system responses are labeled and system are not + should_be_labelled = turn["from"] != "system" + if should_be_labelled: + assert all( + label != IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] + ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:end_idx]}" + else: + assert all( + label == IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] + ), f"Expected labels for human input '{response}' to be IGNORE_TOKEN_ID, but got {labels[start_idx:end_idx]}" + + def test_roles_to_train_all( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, + request, + ): LOG.info("Testing roles_to_train with all roles") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=get_chat_template("llama3") + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_field_role="from", + message_field_content="value", ), - tokenizer=llama3_tokenizer, + tokenizer=tokenizer, train_on_inputs=True, sequence_len=512, roles_to_train=["human", "assistant"], ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) + turns = strategy.get_conversation_thread(basic_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] # Verify that all responses are labeled (except for special tokens) - all_responses = [ - "Hello", - "Hi there!", - "How are you?", - "I'm doing well, thank you!", - ] - for response in all_responses: - response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, response_ids) - LOG.debug( - f"Response '{response}' expected IDs: {response_ids}, found at: {start_idx}" - ) - assert all( - label != IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(response_ids)] - ), f"Expected labels for response '{response}' to be set, but got {labels[start_idx:start_idx+len(response_ids)]}" + for i, turn in enumerate(basic_dataset[0]["conversations"]): + response = turn["value"] + + start_idx, end_idx = strategy.find_turn(turns=turns, turn_idx=i) - def test_empty_roles_to_train(self, llama3_tokenizer, basic_dataset): + if self._should_skip_turn(tokenizer, turn, i, start_idx, end_idx): + continue + + decoded_response = tokenizer.decode(input_ids[start_idx:end_idx]) + assert ( + response in decoded_response + ), f"Response {response} not found in index {start_idx}:{end_idx} decoded:{decoded_response}" + + assert all( + label != IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] + ), f"Expected labels for response '{response}' to be set, but got {labels[start_idx:end_idx]}" + + def test_empty_roles_to_train( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, + request, + ): LOG.info("Testing with empty roles_to_train") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=get_chat_template("llama3") + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_field_role="from", + message_field_content="value", ), - tokenizer=llama3_tokenizer, + tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, roles_to_train=[], train_on_eos="none", # Add this line ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) labels = res["labels"] @@ -201,23 +379,42 @@ def test_empty_roles_to_train(self, llama3_tokenizer, basic_dataset): label == IGNORE_TOKEN_ID for label in labels ), "Expected all labels to be IGNORE_TOKEN_ID when roles_to_train is empty" - def test_train_on_eos_all(self, llama3_tokenizer, basic_dataset): + def test_train_on_eos_all( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, + request, + ): LOG.info("Testing with train_on_eos='all'") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=get_chat_template("llama3") + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_field_role="from", + message_field_content="value", ), - tokenizer=llama3_tokenizer, + tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, roles_to_train=["assistant"], train_on_eos="all", ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] - eos_token_id = llama3_tokenizer.eos_token_id + eos_token_id = tokenizer.eos_token_id eos_indices = [ i for i, token_id in enumerate(input_ids) if token_id == eos_token_id ] @@ -228,73 +425,122 @@ def test_train_on_eos_all(self, llama3_tokenizer, basic_dataset): labels[eos_idx] != IGNORE_TOKEN_ID ), f"Expected EOS token at index {eos_idx} to be labeled" - def test_train_on_eos_turn(self, llama3_tokenizer, basic_dataset): + def test_train_on_eos_turn( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, + request, + ): LOG.info("Testing with train_on_eos='turn'") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=get_chat_template("llama3") + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_field_role="from", + message_field_content="value", ), - tokenizer=llama3_tokenizer, + tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, roles_to_train=["assistant"], train_on_eos="turn", ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) + turns = strategy.get_conversation_thread(basic_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] - eos_token_id = llama3_tokenizer.eos_token_id - assistant_responses = ["Hi there!", "I'm doing well, thank you!"] + eos_token_id = tokenizer.eos_token_id + # Process all turns and verify EOS token labeling + for i, turn in enumerate(basic_dataset[0]["conversations"]): + start_idx, end_idx = strategy.find_turn(turns=turns, turn_idx=i) + + if self._should_skip_turn(tokenizer, turn, i, start_idx, end_idx): + continue + + decoded_response = tokenizer.decode(input_ids[start_idx:end_idx]) + response = turn["value"] - for response in assistant_responses: - response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, response_ids) - assert start_idx != -1, f"Could not find '{response}' in input_ids" + assert response in decoded_response, ( + f"Response {response} not found in index {start_idx}:{end_idx} " + f"decoded:{decoded_response}" + ) - eos_idx = start_idx + len(response_ids) + # Find the EOS token after this turn + eos_idx = end_idx while eos_idx < len(input_ids) and input_ids[eos_idx] != eos_token_id: eos_idx += 1 assert eos_idx < len( input_ids ), f"Could not find EOS token after '{response}'" - assert ( - labels[eos_idx] != IGNORE_TOKEN_ID - ), f"Expected EOS token after assistant response '{response}' to be labeled" - - # Check that EOS tokens after human inputs are not labeled - human_inputs = ["Hello", "How are you?"] - for input_text in human_inputs: - input_ids = llama3_tokenizer.encode(input_text, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, input_ids) - assert start_idx != -1, f"Could not find '{input_text}' in input_ids" - eos_idx = start_idx + len(input_ids) - while eos_idx < len(input_ids) and input_ids[eos_idx] != eos_token_id: - eos_idx += 1 + LOG.debug( + f"Turn {i}: role={turn['from']}, content='{turn['value']}', start_idx={start_idx}, end_idx={end_idx}, eos_idx={eos_idx}" + ) - assert ( - labels[eos_idx] == IGNORE_TOKEN_ID - ), f"Expected EOS token after human input '{input_text}' to not be labeled" + LOG.debug( + f"Labels for turn {i}: {labels[start_idx:end_idx]}, EOS label: {labels[eos_idx]}" + ) - def test_train_on_eos_last(self, llama3_tokenizer, basic_dataset): + # Verify EOS token labeling based on role + is_assistant = turn["from"] == "assistant" + if is_assistant: + assert ( + labels[eos_idx] != IGNORE_TOKEN_ID + ), f"Expected EOS token after assistant response '{response}' to be labeled" + else: + assert ( + labels[eos_idx] == IGNORE_TOKEN_ID + ), f"Expected EOS token after non-assistant input '{response}' to not be labeled" + + def test_train_on_eos_last( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, + request, + ): LOG.info("Testing with train_on_eos='last'") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=get_chat_template("llama3") + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_field_role="from", + message_field_content="value", ), - tokenizer=llama3_tokenizer, + tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, roles_to_train=["assistant"], train_on_eos="last", ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] - eos_token_id = llama3_tokenizer.eos_token_id + eos_token_id = tokenizer.eos_token_id eos_indices = [ i for i, token_id in enumerate(input_ids) if token_id == eos_token_id ] @@ -311,23 +557,42 @@ def test_train_on_eos_last(self, llama3_tokenizer, basic_dataset): labels[last_eos_idx] != IGNORE_TOKEN_ID ), f"Expected last EOS token at index {last_eos_idx} to be labeled" - def test_train_on_eos_none(self, llama3_tokenizer, basic_dataset): + def test_train_on_eos_none( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, + request, + ): LOG.info("Testing with train_on_eos='none'") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, chat_template=get_chat_template("llama3") + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_field_role="from", + message_field_content="value", ), - tokenizer=llama3_tokenizer, + tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, roles_to_train=["assistant"], train_on_eos="none", ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] - eos_token_id = llama3_tokenizer.eos_token_id + eos_token_id = tokenizer.eos_token_id eos_indices = [ i for i, token_id in enumerate(input_ids) if token_id == eos_token_id ] @@ -338,43 +603,75 @@ def test_train_on_eos_none(self, llama3_tokenizer, basic_dataset): labels[eos_idx] == IGNORE_TOKEN_ID ), f"Expected EOS token at index {eos_idx} to not be labeled" - def test_drop_system_message(self, llama3_tokenizer, basic_dataset): + def test_drop_system_message( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, + request, + ): LOG.info("Testing with drop_system_message=True") + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, - chat_template=get_chat_template("llama3"), + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), drop_system_message=True, + message_field_role="from", + message_field_content="value", ), - tokenizer=llama3_tokenizer, + tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, roles_to_train=["assistant"], ) + strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) input_ids = res["input_ids"] # Check if system message is not present in input_ids system_message = "You are an AI assistant." - system_ids = llama3_tokenizer.encode(system_message, add_special_tokens=False) + decoded_message = tokenizer.decode(input_ids) assert ( - self.find_sublist(input_ids, system_ids) == -1 + system_message not in decoded_message ), "Expected system message to be dropped" - def test_custom_roles(self, llama3_tokenizer): + def test_custom_roles( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + request, + ): LOG.info("Testing with custom roles mapping") custom_roles = { "user": ["human", "user"], "assistant": ["ai", "assistant"], "system": ["context"], } + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, - chat_template=get_chat_template("llama3"), + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), roles=custom_roles, + message_field_role="from", + message_field_content="value", ), - tokenizer=llama3_tokenizer, + tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, roles_to_train=["ai"], @@ -389,46 +686,65 @@ def test_custom_roles(self, llama3_tokenizer): {"from": "ai", "value": "I'm doing well, thank you!"}, ] - modified_dataset = Dataset.from_dict( - {"conversations": [modified_conversations]} - ) + modified_dataset = Dataset.from_dict({"messages": [modified_conversations]}) res = strategy.tokenize_prompt(modified_dataset[0]) + turns = strategy.get_conversation_thread(modified_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] - # Check if AI responses are labeled correctly - ai_responses = ["Hi there!", "I'm doing well, thank you!"] - for response in ai_responses: - response_ids = llama3_tokenizer.encode(response, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, response_ids) - assert start_idx != -1, f"Could not find response '{response}' in input_ids" - assert all( - label != IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(response_ids)] - ), f"Expected labels for AI response '{response}' to be set" - - # Check if human messages are not labeled - human_messages = ["Hello", "How are you?"] - for message in human_messages: - message_ids = llama3_tokenizer.encode(message, add_special_tokens=False) - start_idx = self.find_sublist(input_ids, message_ids) - assert start_idx != -1, f"Could not find message '{message}' in input_ids" - assert all( - label == IGNORE_TOKEN_ID - for label in labels[start_idx : start_idx + len(message_ids)] - ), f"Expected labels for human message '{message}' to be IGNORE_TOKEN_ID" + # Process all turns and verify labeling + for i, turn in enumerate(modified_dataset[0]["messages"]): + start_idx, end_idx = strategy.find_turn(turns=turns, turn_idx=i) + + if self._should_skip_turn(tokenizer, turn, i, start_idx, end_idx): + continue + + decoded_response = tokenizer.decode(input_ids[start_idx:end_idx]) + response = turn["value"] - def test_message_field_training(self, llama3_tokenizer): + assert response in decoded_response, ( + f"Response {response} not found in index {start_idx}:{end_idx} " + f"decoded:{decoded_response}" + ) + + # Check if responses are labeled correctly based on role + is_ai = turn["from"] == "ai" + if is_ai: + assert all( + label != IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] + ), f"Expected labels for AI response '{response}' to be set" + else: + assert all( + label == IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] + ), f"Expected labels for non-AI message '{response}' to be IGNORE_TOKEN_ID" + + def test_message_field_training( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + request, + ): LOG.info("Testing with message_field_training") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + strategy = ChatTemplateStrategy( ChatTemplatePrompter( - llama3_tokenizer, - chat_template=get_chat_template("llama3"), + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), message_field_training="train", message_field_training_detail="train_detail", + message_field_role="from", + message_field_content="value", ), - tokenizer=llama3_tokenizer, + tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, roles_to_train=[], @@ -457,62 +773,65 @@ def test_message_field_training(self, llama3_tokenizer): {"from": "assistant", "value": "Hi there!", "train": True}, ] - modified_dataset = Dataset.from_dict({"conversations": [modified_conversation]}) + modified_dataset = Dataset.from_dict({"messages": [modified_conversation]}) res = strategy.tokenize_prompt(modified_dataset[0]) + turns = strategy.get_conversation_thread(modified_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] - # Function to find all occurrences of a sublist - def find_all_sublists(full_list, sub_list): - indices = [] - for index in range(len(full_list) - len(sub_list) + 1): - if full_list[index : index + len(sub_list)] == sub_list: - indices.append(index) - return indices - - # Keep track of which occurrences we've processed - processed_occurrences = {} - # Check if messages are labeled correctly based on train or train_detail - for i, turn in enumerate(modified_conversation): - turn_tokens = llama3_tokenizer.encode( - turn["value"], add_special_tokens=False - ) - occurrences = find_all_sublists(input_ids, turn_tokens) - turn_key = turn["value"] - if turn_key not in processed_occurrences: - processed_occurrences[turn_key] = 0 - current_occurrence = processed_occurrences[turn_key] + def verify_labels(labels_span, should_train, context_message): + """Helper to verify if a span of labels matches expected training state""" + if should_train: + assert all( + label != IGNORE_TOKEN_ID for label in labels_span + ), f"Expected all labels for {context_message} to be set, but got {labels_span}" + else: + assert all( + label == IGNORE_TOKEN_ID for label in labels_span + ), f"Expected all labels for {context_message} to be {IGNORE_TOKEN_ID}, but got {labels_span}" - if current_occurrence >= len(occurrences): - assert ( - False - ), f"Not enough occurrences found for message: {turn['value']}" + # Process all turns and verify labeling + for i, turn in enumerate(modified_dataset[0]["messages"]): + start_idx, end_idx = strategy.find_turn(turns=turns, turn_idx=i) + + if self._should_skip_turn(tokenizer, turn, i, start_idx, end_idx): + continue + + decoded_response = tokenizer.decode(input_ids[start_idx:end_idx]) + response = turn["value"] - start_idx = occurrences[current_occurrence] - processed_occurrences[turn_key] += 1 - end_idx = start_idx + len(turn_tokens) + assert response in decoded_response, ( + f"Response {response} not found in index {start_idx}:{end_idx} " + f"decoded:{decoded_response}" + ) LOG.debug( - f"Processing turn {i}: role={turn['from']}, content='{turn['value']}', start_idx={start_idx}, end_idx={end_idx}" + f"Processing turn {i}: role={turn['from']}, content='{turn['value']}', " + f"start_idx={start_idx}, end_idx={end_idx}" ) - if "train_detail" in turn: - # Get token offsets - tokenized_output = llama3_tokenizer( + if turn.get("train_detail", None) is not None: + # Handle detailed token-level training control + tokenized_output = tokenizer( turn["value"], return_offsets_mapping=True, add_special_tokens=False ) + assert tokenized_output["input_ids"] == input_ids[start_idx:end_idx], ( + f"Tokenized input mismatch for turn: {turn['value']}\n" + f"Expected: {input_ids[start_idx:end_idx]}\nActual: {tokenized_output['input_ids']}\n" + f"This will likely be a mismatch between template content and encoded content" + ) + token_offsets = tokenized_output["offset_mapping"] - # Adjust token offsets as done in the implementation - for i in range(len(token_offsets) - 1): - token_offsets[i] = ( - token_offsets[i][0], - token_offsets[i + 1][0] - 1, + # Adjust token offsets + for j in range(len(token_offsets) - 1): + token_offsets[j] = ( + token_offsets[j][0], + token_offsets[j + 1][0] - 1, ) token_offsets[-1] = (token_offsets[-1][0], len(turn["value"]) - 1) - # Adjust train_details adjusted_train_details = strategy.prompter.adjust_train_details( turn["train_detail"], token_offsets ) @@ -520,12 +839,20 @@ def find_all_sublists(full_list, sub_list): LOG.debug(f"Original train_details: {turn['train_detail']}") LOG.debug(f"Adjusted train_details: {adjusted_train_details}") - # Handle train_detail - token_offsets = strategy.prompter.get_offsets_for_train_detail( + # Get and verify token offsets + turn_tokens = input_ids[start_idx:end_idx] + token_offsets_unmasked = strategy.prompter.get_offsets_for_train_detail( text=turn["value"], train_details=adjusted_train_details, mask_untrainable=False, ) + + for i, offset in enumerate(token_offsets_unmasked): + assert token_offsets[i][0] == offset, ( + f"Token start offsets mismatch for turn: {turn['value']}\n" + f"Expected: {token_offsets[i][0]}\nActual: {offset}" + ) + token_offsets_masked = strategy.prompter.get_offsets_for_train_detail( text=turn["value"], train_details=adjusted_train_details, @@ -533,6 +860,7 @@ def find_all_sublists(full_list, sub_list): ) LOG.debug(f"Token offsets: {token_offsets_masked}") + # Verify expected labels against actual labels expected_labels = [IGNORE_TOKEN_ID] * len(turn_tokens) for i, offset in enumerate(token_offsets_masked): if offset != IGNORE_TOKEN_ID: @@ -544,17 +872,17 @@ def find_all_sublists(full_list, sub_list): actual_labels == expected_labels ), f"Labels mismatch for turn: {turn['value']}\nExpected: {expected_labels}\nActual: {actual_labels}" + # Verify each detail section for detail in adjusted_train_details: - # Find the token indices that correspond to the character offsets detail_start = start_idx + next( - i - for i, offset in enumerate(token_offsets) + j + for j, offset in enumerate(token_offsets_unmasked) if offset >= detail["begin_offset"] ) detail_end = start_idx + next( ( - i - for i, offset in enumerate(token_offsets) + j + for j, offset in enumerate(token_offsets_unmasked) if offset > detail["end_offset"] ), len(token_offsets), @@ -564,70 +892,21 @@ def find_all_sublists(full_list, sub_list): detail["begin_offset"] : detail["end_offset"] + 1 ] detail_labels = labels[detail_start:detail_end] - detail_input_ids = input_ids[detail_start:detail_end] - LOG.debug( - f"Detail: '{detail_text}', Start: {detail_start}, End: {detail_end}" - ) - LOG.debug(f"Detail input_ids: {detail_input_ids}") - LOG.debug(f"Detail labels: {detail_labels}") - LOG.debug( - f"Decoded detail: {llama3_tokenizer.decode(detail_input_ids)}" - ) - LOG.debug( - f"Token offsets for this detail: {token_offsets[detail_start-start_idx:detail_end-start_idx]}" + context = ( + f"detail (ind {detail_start}:{detail_end}): '{detail_text}'\n" + f"decoded: '{tokenizer.decode(input_ids[detail_start:detail_end])}')" ) - - if detail["train"]: - assert all( - label != IGNORE_TOKEN_ID for label in detail_labels - ), ( - f"Expected labels for trainable detail '{detail_text}' to be set, but some were IGNORE_TOKEN_ID. " - f"Labels({detail_start}:{detail_end}): {detail_labels}, " - f"InputIDs: {detail_input_ids}, " - f"Decoded: '{llama3_tokenizer.decode(detail_input_ids)}'" - ) - else: - assert all( - label == IGNORE_TOKEN_ID for label in detail_labels - ), ( - f"Expected all labels for non-trainable detail '{detail_text}' to be IGNORE_TOKEN_ID, but some were not. " - f"Labels({detail_start}:{detail_end}): {detail_labels}, " - f"InputIDs: {detail_input_ids}, " - f"Decoded: '{llama3_tokenizer.decode(detail_input_ids)}'" - ) + verify_labels(detail_labels, detail["train"], context) else: + # Handle regular turn-level training control should_train = turn.get("train", False) turn_labels = labels[start_idx:end_idx] - - LOG.debug(f"Should train: {should_train}") - LOG.debug(f"Turn indices: start={start_idx}, end={end_idx}") - LOG.debug(f"Turn labels: {turn_labels}") - LOG.debug(f"Turn input IDs: {input_ids[start_idx:end_idx]}") - LOG.debug( - f"Decoded turn: {llama3_tokenizer.decode(input_ids[start_idx:end_idx])}" - ) - - if should_train: - assert all(label != IGNORE_TOKEN_ID for label in turn_labels), ( - f"Expected all labels for '{turn['value']}' to be set\n" - f"Labels({start_idx}:{end_idx}): {turn_labels}, " - f"InputIDs: {input_ids[start_idx:end_idx]}, " - f"Decoded: '{llama3_tokenizer.decode(input_ids[start_idx:end_idx])}'" - ) - else: - assert all(label == IGNORE_TOKEN_ID for label in turn_labels), ( - f"Expected all labels for '{turn['value']}' to be IGNORE_TOKEN_ID\n" - f"Labels({start_idx}:{end_idx}): {turn_labels}, " - f"InputIDs: {input_ids[start_idx:end_idx]}, " - f"Decoded: '{llama3_tokenizer.decode(input_ids[start_idx:end_idx])}'" - ) - - LOG.debug( - f"Processed turn: {turn['from']}, content: '{turn['value']}', " - f"start_idx: {start_idx}, end_idx: {end_idx}, " - f"labels: {labels[start_idx:end_idx]}" + context = ( + f"turn (ind {start_idx}:{end_idx}): '{turn['value']}'\n" + f"decoded: '{decoded_response}')" ) + verify_labels(turn_labels, should_train, context) LOG.debug(f"Final labels: {labels}") LOG.debug(f"Final input_ids: {input_ids}") From 3798229d85e251eaf8611cd9ed1bf63101c1e58b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 17 Dec 2024 16:42:41 -0500 Subject: [PATCH 0348/1405] handle torch_compile set to auto (#2172) [skip ci] * handle torch_compile set to auto * update docs [skip ci] * add tests --- docs/config.qmd | 3 +- src/axolotl/utils/config/__init__.py | 4 +- .../config/models/input/v0_4_1/__init__.py | 21 +++++++++- tests/patched/test_validation.py | 40 +++++++++++++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index ba23384f0c..d52170959d 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -337,7 +337,8 @@ comet_experiment_config: # Dictionary for additional configuration settings, see output_dir: ./completed-model # Whether to use torch.compile and which backend to use -torch_compile: # bool +# setting to `auto` will enable torch compile when torch>=2.5.1 +torch_compile: # Optional[Union[Literal["auto"], bool]] torch_compile_backend: # Optional[str] # Training hyperparameters diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 30ba53ad25..c23359f34d 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -245,8 +245,8 @@ def validate_config( ) = merge_input_args() if capabilities or env_capabilities: - if (capabilities and not env_capabilities) or ( - env_capabilities and not capabilities + if (capabilities and env_capabilities is None) or ( + env_capabilities and capabilities is None ): raise ValueError( "Both capabilities and env_capabilities must be provided or not provided." diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index d05de2330d..69baf9af2b 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -741,7 +741,7 @@ class Config: special_tokens: Optional[SpecialTokensConfig] = None tokens: Optional[List[str]] = None - torch_compile: Optional[bool] = None + torch_compile: Optional[Union[Literal["auto"], bool]] = None torch_compile_backend: Optional[str] = None torch_compile_mode: Optional[ Literal["default", "reduce-overhead", "max-autotune"] @@ -1582,3 +1582,22 @@ def check_adopt_torch_version(cls, data): "ADOPT optimizer is incompatible with torch version < 2.5.1" ) return data + + @model_validator(mode="before") + @classmethod + def check_torch_compile_auto(cls, data): + if data.get("torch_compile") == "auto": + env_capabilities = data.get("env_capabilities", {}) + if env_capabilities.get("torch_version"): + if version.parse( + env_capabilities.get("torch_version") + ) >= version.parse("2.5.1"): + LOG.info( + "torch.compile is available, setting torch_compile to True" + ) + data["torch_compile"] = True + else: + data["torch_compile"] = False + else: + data["torch_compile"] = False + return data diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 2e6fbab101..3d1b74789d 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -1196,6 +1196,46 @@ def test_torch_version_adopt_req(self, minimal_cfg): ) +class TestTorchCompileValidation(BaseValidation): + """ + test suite for when torch_compile is set to 'auto' + """ + + def test_torch_compile_auto(self, minimal_cfg): + cfg = ( + DictDefault( + { + "torch_compile": "auto", + } + ) + | minimal_cfg + ) + + env_capabilities = {"torch_version": "2.5.1"} + capabilities = {"bf16": True} + updated_cfg = validate_config( + cfg, capabilities=capabilities, env_capabilities=env_capabilities + ) + + assert updated_cfg.torch_compile is True + + env_capabilities = {"torch_version": "2.4.1"} + capabilities = {"bf16": True} + updated_cfg = validate_config( + cfg, capabilities=capabilities, env_capabilities=env_capabilities + ) + + assert updated_cfg.torch_compile is False + + env_capabilities = {} + capabilities = {"bf16": True} + updated_cfg = validate_config( + cfg, capabilities=capabilities, env_capabilities=env_capabilities + ) + + assert updated_cfg.torch_compile is False + + class TestValidationCheckModelConfig(BaseValidation): """ Test the validation for the config when the model config is available From bd2a594b8954103719f8d1ef739e2c3267ca36f6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 17 Dec 2024 17:46:44 -0500 Subject: [PATCH 0349/1405] use DataCollatorWithFlattening when not sample packing (#2167) --- docs/config.qmd | 3 + src/axolotl/core/trainer_builder.py | 13 +++- .../config/models/input/v0_4_1/__init__.py | 26 +++++++ tests/e2e/test_llama.py | 39 +++++++++++ tests/patched/test_validation.py | 70 +++++++++++++++++++ 5 files changed, 149 insertions(+), 2 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index d52170959d..70679791e4 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -245,6 +245,9 @@ sample_packing_group_size: 100000 # The number of samples which can be packed into one sequence. Increase if using a large sequence_len with many short samples. sample_packing_bin_size: 200 +# Use batch flattening for speedups when not using sample_packing +batch_flattening: + # Passed through to transformers when loading the model when launched without accelerate # Use `sequential` when training w/ model parallelism to limit memory device_map: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 0f30f511c2..54ee195361 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -28,6 +28,7 @@ from torch.optim.lr_scheduler import OneCycleLR from torch.utils.data import BatchSampler, DataLoader, RandomSampler, SequentialSampler from transformers import ( + DataCollatorWithFlattening, EarlyStoppingCallback, Trainer, TrainerCallback, @@ -1989,9 +1990,11 @@ def build_collator( V2BatchSamplerDataCollatorForSeq2Seq, BatchSamplerDataCollatorForSeq2Seq, DataCollatorForSeq2Seq, + DataCollatorWithFlattening, RewardDataCollatorWithPadding, ] ] + collator_args = [self.tokenizer] if self.cfg.reward_model: collator = RewardDataCollatorWithPadding if "max_length" in kwargs: @@ -2011,12 +2014,18 @@ def build_collator( collator = MultiModalChatDataCollator kwargs["processor"] = self.processor kwargs["chat_template"] = training_args.chat_template + elif self.cfg.batch_flattening: + collator = DataCollatorWithFlattening + collator_args.pop(0) + kwargs.pop("pad_to_multiple_of", None) + kwargs.pop("padding", None) else: collator = DataCollatorForSeq2Seq + kwargs["return_tensors"] = "pt" + return collator( - self.tokenizer, - return_tensors="pt", + *collator_args, **kwargs, ) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 69baf9af2b..5ddf048112 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -696,6 +696,8 @@ class Config: curriculum_sampling: Optional[bool] = None multipack_real_batches: Optional[bool] = None + batch_flattening: Optional[Union[Literal["auto"], bool]] = None + # for PoSE context length extension use_pose: Optional[bool] = None pose_split_on_token_ids: Optional[List[int]] = None @@ -924,6 +926,30 @@ def check_sample_packing_wo_flash(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_batch_flattening_fa(cls, data): + if data.get("batch_flattening"): + batch_flattening_auto = data.get("batch_flattening") == "auto" + if not data.get("flash_attention") and not batch_flattening_auto: + raise ValueError("batch_flattening requires flash attention") + if data.get("sample_packing") and not batch_flattening_auto: + raise ValueError("batch_flattening not compatible with sample_packing") + if data.get("micro_batch_size") == 1 and not batch_flattening_auto: + LOG.warning("batch_flattening has no effect with micro_batch_size == 1") + + if ( + batch_flattening_auto + and data.get("flash_attention") + and not data.get("sample_packing") + and data.get("micro_batch_size") > 1 + ): + data["batch_flattening"] = True + elif batch_flattening_auto: + data["batch_flattening"] = False + + return data + @model_validator(mode="before") @classmethod def check_sample_packing_w_rl(cls, data): diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index 33d12157ad..1ce9d60b98 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -104,3 +104,42 @@ def test_fix_untrained_tokens(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "model.safetensors").exists() + + def test_batch_flattening(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "trust_remote_code": True, + "sequence_len": 512, + "val_set_size": 0.01, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "micro_batch_size": 4, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": False, + "batch_flattening": True, + "bf16": True, + "save_safetensors": True, + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 3d1b74789d..9d41dac761 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -1236,6 +1236,76 @@ def test_torch_compile_auto(self, minimal_cfg): assert updated_cfg.torch_compile is False +class TestSampleOptimConfigValidation(BaseValidation): + """ + test configurations for sample optimizations like batch flattening + """ + + def test_batch_flattening_auto_enables(self, minimal_cfg): + cfg = ( + DictDefault( + { + "flash_attention": True, + "sample_packing": None, + "micro_batch_size": 2, + "batch_flattening": "auto", + } + ) + | minimal_cfg + ) + + new_cfg = validate_config(cfg) + assert new_cfg["batch_flattening"] is True + + def test_batch_flattening_auto_no_fa(self, minimal_cfg): + cfg = ( + DictDefault( + { + "flash_attention": False, + "sample_packing": None, + "micro_batch_size": 2, + "batch_flattening": "auto", + } + ) + | minimal_cfg + ) + + new_cfg = validate_config(cfg) + assert new_cfg["batch_flattening"] is False + + def test_batch_flattening_auto_mbsz_1(self, minimal_cfg): + cfg = ( + DictDefault( + { + "flash_attention": True, + "sample_packing": None, + "micro_batch_size": 1, + "batch_flattening": "auto", + } + ) + | minimal_cfg + ) + + new_cfg = validate_config(cfg) + assert new_cfg["batch_flattening"] is False + + def test_batch_flattening_auto_packing(self, minimal_cfg): + cfg = ( + DictDefault( + { + "flash_attention": True, + "sample_packing": True, + "micro_batch_size": 2, + "batch_flattening": "auto", + } + ) + | minimal_cfg + ) + + new_cfg = validate_config(cfg) + assert new_cfg["batch_flattening"] is False + + class TestValidationCheckModelConfig(BaseValidation): """ Test the validation for the config when the model config is available From 5b8fb5e939713b85340a7da25aa15d1240cb0740 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Thu, 19 Dec 2024 11:44:53 -0500 Subject: [PATCH 0350/1405] remove cicd pytest xdist args (#2201) * remove cicd pytest xdist args * Delete outputs --- cicd/cicd.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 88defdd0e4..91926127fb 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -5,6 +5,6 @@ python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__" pytest -v --durations=10 -n8 --ignore=tests/e2e/ --ignore=tests/patched/ /workspace/axolotl/tests/ # pytest -v --durations=10 -n8 --dist loadfile /workspace/axolotl/tests/patched/ -pytest -v --durations=10 -n1 --dist loadfile /workspace/axolotl/tests/e2e/patched/ -pytest -v --durations=10 -n1 --dist loadfile /workspace/axolotl/tests/e2e/integrations/ +pytest -v --durations=10 /workspace/axolotl/tests/e2e/patched/ +pytest -v --durations=10 /workspace/axolotl/tests/e2e/integrations/ pytest -v --durations=10 --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ From 42bd32a2332e5002e3f062972772a70881d01d91 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 19 Dec 2024 20:14:43 -0500 Subject: [PATCH 0351/1405] add outputs (symlink) to gitignore [skip ci] (#2205) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bfbd22de06..7b604d88c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ **/axolotl.egg-info configs last_run_prepared/ +outputs .vscode _site/ From 70541145f169401540185dca5ddac94f94640fc6 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 20 Dec 2024 21:43:33 -0500 Subject: [PATCH 0352/1405] adding test_datasets compat with pretraining_dataset (streaming) (#2206) [skip ci] --- src/axolotl/utils/data/sft.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 286e5f2d70..e2cb8f9f62 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -85,6 +85,7 @@ def prepare_dataset(cfg, tokenizer, processor=None): processor=processor, ) else: + # Load streaming dataset if pretraining_dataset is given path = cfg.pretraining_dataset split = "train" name = None @@ -116,7 +117,18 @@ def prepare_dataset(cfg, tokenizer, processor=None): ) # https://discuss.huggingface.co/t/how-to-use-huggingface-trainer-streaming-datasets-without-wrapping-it-with-torchdatas-iterablewrapper/25230 train_dataset = train_dataset.with_format("torch") + + # Load eval dataset (non-streaming) if specified eval_dataset = None + if cfg.test_datasets: + _, eval_dataset, _ = load_prepare_datasets( + tokenizer, + cfg, + DEFAULT_DATASET_PREPARED_PATH, + split="test", + processor=processor, + ) + if cfg.dataset_exact_deduplication: LOG.info("Deduplication not available for pretrained datasets") From 307cf7c685eafe7c84f17ed871650755f589a884 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 20 Dec 2024 21:43:52 -0500 Subject: [PATCH 0353/1405] move the dataset loading from remote/disk to a shared function so we can re-use for RL (#2204) --- src/axolotl/utils/data/sft.py | 215 +----------------------------- src/axolotl/utils/data/shared.py | 222 +++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 210 deletions(-) create mode 100644 src/axolotl/utils/data/shared.py diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index e2cb8f9f62..3e784ca3e7 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -3,7 +3,7 @@ import functools import logging from pathlib import Path -from typing import List, Optional, Tuple, Union +from typing import List, Tuple, Union from datasets import ( Dataset, @@ -12,8 +12,6 @@ load_dataset, load_from_disk, ) -from huggingface_hub import hf_hub_download -from huggingface_hub.utils import HFValidationError from transformers import PreTrainedTokenizerBase from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH @@ -42,6 +40,7 @@ UnsupportedPrompter, ) from axolotl.utils.data.pretraining import wrap_pretraining_dataset +from axolotl.utils.data.shared import load_dataset_w_config from axolotl.utils.data.utils import ( deduplicate_and_log_datasets, md5, @@ -255,195 +254,9 @@ def for_d_in_datasets(dataset_configs): # pylint: disable=invalid-name for config_dataset in for_d_in_datasets(cfg_datasets): - ds: Optional[Union[Dataset, DatasetDict]] = None - ds_from_hub = False - ds_trust_remote_code = config_dataset.trust_remote_code - try: - # this is just a basic check to see if the path is a - # valid HF dataset that's loadable - load_dataset( - config_dataset.path, - name=config_dataset.name, - streaming=True, - token=use_auth_token, - revision=config_dataset.revision, - trust_remote_code=ds_trust_remote_code, - ) - ds_from_hub = True - except (FileNotFoundError, ConnectionError, HFValidationError, ValueError): - pass - - ds_from_cloud = False - storage_options = {} - remote_file_system = None - if config_dataset.path.startswith("s3://"): - try: - import aiobotocore.session # type: ignore - import s3fs # type: ignore - except ImportError as exc: - raise ImportError( - "s3:// paths require aiobotocore and s3fs to be installed" - ) from exc - - # Takes credentials from ~/.aws/credentials for default profile - s3_session = aiobotocore.session.AioSession(profile="default") - storage_options = {"session": s3_session} - remote_file_system = s3fs.S3FileSystem(**storage_options) - elif config_dataset.path.startswith( - "gs://" - ) or config_dataset.path.startswith("gcs://"): - try: - import gcsfs # type: ignore - except ImportError as exc: - raise ImportError( - "gs:// or gcs:// paths require gcsfs to be installed" - ) from exc - - # gcsfs will use default credentials from the environment else anon - # https://gcsfs.readthedocs.io/en/latest/#credentials - storage_options = {"token": None} - remote_file_system = gcsfs.GCSFileSystem(**storage_options) - # TODO: Figure out how to get auth creds passed - # elif config_dataset.path.startswith("adl://") or config_dataset.path.startswith("abfs://"): - # try: - # import adlfs - # except ImportError as exc: - # raise ImportError( - # "adl:// or abfs:// paths require adlfs to be installed" - # ) from exc - - # # Gen 1 - # storage_options = { - # "tenant_id": TENANT_ID, - # "client_id": CLIENT_ID, - # "client_secret": CLIENT_SECRET, - # } - # # Gen 2 - # storage_options = { - # "account_name": ACCOUNT_NAME, - # "account_key": ACCOUNT_KEY, - # } - - # remote_file_system = adlfs.AzureBlobFileSystem(**storage_options) - try: - if remote_file_system and remote_file_system.exists( - config_dataset.path - ): - ds_from_cloud = True - except (FileNotFoundError, ConnectionError): - pass - - # prefer local dataset, even if hub exists - local_path = Path(config_dataset.path) - if local_path.exists(): - if local_path.is_dir(): - if config_dataset.data_files: - ds_type = get_ds_type(config_dataset) - ds = load_dataset( - ds_type, - name=config_dataset.name, - data_files=config_dataset.data_files, - streaming=False, - split=None, - ) - else: - try: - ds = load_from_disk(config_dataset.path) - except FileNotFoundError: - ds = load_dataset( - config_dataset.path, - name=config_dataset.name, - streaming=False, - split=None, - ) - elif local_path.is_file(): - ds_type = get_ds_type(config_dataset) - - ds = load_dataset( - ds_type, - name=config_dataset.name, - data_files=config_dataset.path, - streaming=False, - split=None, - ) - else: - raise ValueError( - "unhandled dataset load: local path exists, but is neither a directory or a file" - ) - elif ds_from_hub: - load_ds_kwargs = {} - if config_dataset.split: - load_ds_kwargs["split"] = config_dataset.split - ds = load_dataset( - config_dataset.path, - name=config_dataset.name, - streaming=False, - data_files=config_dataset.data_files, - token=use_auth_token, - revision=config_dataset.revision, - trust_remote_code=config_dataset.trust_remote_code, - **load_ds_kwargs, - ) - elif ds_from_cloud and remote_file_system: - if remote_file_system.isdir(config_dataset.path): - ds = load_from_disk( - config_dataset.path, - storage_options=storage_options, - ) - elif remote_file_system.isfile(config_dataset.path): - ds_type = get_ds_type(config_dataset) - ds = load_dataset( - ds_type, - name=config_dataset.name, - data_files=config_dataset.path, - streaming=False, - split=None, - storage_options=storage_options, - trust_remote_code=config_dataset.trust_remote_code, - ) - elif config_dataset.path.startswith("https://"): - ds_type = get_ds_type(config_dataset) - ds = load_dataset( - ds_type, - name=config_dataset.name, - data_files=config_dataset.path, - streaming=False, - split=None, - storage_options=storage_options, - trust_remote_code=config_dataset.trust_remote_code, - ) - else: - if isinstance(config_dataset.data_files, str): - fp = hf_hub_download( - repo_id=config_dataset.path, - repo_type="dataset", - filename=config_dataset.data_files, - revision=config_dataset.revision, - ) - elif isinstance(config_dataset.data_files, list): - fp = [] - for file in config_dataset.data_files: - fp.append( - hf_hub_download( - repo_id=config_dataset.path, - repo_type="dataset", - filename=file, - revision=config_dataset.revision, - ) - ) - else: - raise ValueError( - "data_files must be either a string or list of strings" - ) - ds = load_dataset( - "json", - name=config_dataset.name, - data_files=fp, - streaming=False, - split=None, - ) - if not ds: - raise ValueError("unhandled dataset load") + ds: Union[Dataset, DatasetDict] = load_dataset_w_config( + config_dataset, use_auth_token + ) d_base_type = d_prompt_style = None d_type = config_dataset.type @@ -513,24 +326,6 @@ def for_d_in_datasets(dataset_configs): return dataset, prompters -def get_ds_type(config_dataset: DictDefault): - """ - Get the dataset type from the path if it's not specified - """ - ds_type = "json" - if config_dataset.ds_type: - ds_type = config_dataset.ds_type - elif ".parquet" in config_dataset.path: - ds_type = "parquet" - elif ".arrow" in config_dataset.path: - ds_type = "arrow" - elif ".csv" in config_dataset.path: - ds_type = "csv" - elif ".txt" in config_dataset.path: - ds_type = "text" - return ds_type - - def load_prepare_datasets( tokenizer: PreTrainedTokenizerBase, cfg, diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py new file mode 100644 index 0000000000..d14496d965 --- /dev/null +++ b/src/axolotl/utils/data/shared.py @@ -0,0 +1,222 @@ +""" +dataset loading shared utils +""" +from pathlib import Path +from typing import Optional, Union + +from datasets import Dataset, DatasetDict, load_dataset, load_from_disk +from huggingface_hub import hf_hub_download +from huggingface_hub.errors import HFValidationError + +from axolotl.utils.dict import DictDefault + + +def get_ds_type(config_dataset: DictDefault): + """ + Get the dataset type from the path if it's not specified + """ + ds_type = "json" + if config_dataset.ds_type: + ds_type = config_dataset.ds_type + elif ".parquet" in config_dataset.path: + ds_type = "parquet" + elif ".arrow" in config_dataset.path: + ds_type = "arrow" + elif ".csv" in config_dataset.path: + ds_type = "csv" + elif ".txt" in config_dataset.path: + ds_type = "text" + return ds_type + + +def load_dataset_w_config(config_dataset, auth_token): + # pylint: disable=invalid-name + ds: Optional[Union[Dataset, DatasetDict]] = None # pylint: disable=invalid-name + ds_from_hub = False + ds_trust_remote_code = config_dataset.trust_remote_code + try: + # this is just a basic check to see if the path is a + # valid HF dataset that's loadable + load_dataset( + config_dataset.path, + name=config_dataset.name, + streaming=True, + token=auth_token, + revision=config_dataset.revision, + trust_remote_code=ds_trust_remote_code, + ) + ds_from_hub = True + except (FileNotFoundError, ConnectionError, HFValidationError, ValueError): + pass + + ds_from_cloud = False + storage_options = {} + remote_file_system = None + if config_dataset.path.startswith("s3://"): + try: + import aiobotocore.session # type: ignore + import s3fs # type: ignore + except ImportError as exc: + raise ImportError( + "s3:// paths require aiobotocore and s3fs to be installed" + ) from exc + + # Takes credentials from ~/.aws/credentials for default profile + s3_session = aiobotocore.session.AioSession(profile="default") + storage_options = {"session": s3_session} + remote_file_system = s3fs.S3FileSystem(**storage_options) + elif config_dataset.path.startswith("gs://") or config_dataset.path.startswith( + "gcs://" + ): + try: + import gcsfs # type: ignore + except ImportError as exc: + raise ImportError( + "gs:// or gcs:// paths require gcsfs to be installed" + ) from exc + + # gcsfs will use default credentials from the environment else anon + # https://gcsfs.readthedocs.io/en/latest/#credentials + storage_options = {"token": None} + remote_file_system = gcsfs.GCSFileSystem(**storage_options) + # TODO: Figure out how to get auth creds passed + # elif config_dataset.path.startswith("adl://") or config_dataset.path.startswith("abfs://"): + # try: + # import adlfs + # except ImportError as exc: + # raise ImportError( + # "adl:// or abfs:// paths require adlfs to be installed" + # ) from exc + + # # Gen 1 + # storage_options = { + # "tenant_id": TENANT_ID, + # "client_id": CLIENT_ID, + # "client_secret": CLIENT_SECRET, + # } + # # Gen 2 + # storage_options = { + # "account_name": ACCOUNT_NAME, + # "account_key": ACCOUNT_KEY, + # } + + # remote_file_system = adlfs.AzureBlobFileSystem(**storage_options) + try: + if remote_file_system and remote_file_system.exists(config_dataset.path): + ds_from_cloud = True + except (FileNotFoundError, ConnectionError): + pass + + # prefer local dataset, even if hub exists + local_path = Path(config_dataset.path) + if local_path.exists(): + if local_path.is_dir(): + if config_dataset.data_files: + ds_type = get_ds_type(config_dataset) + ds = load_dataset( # pylint: disable=invalid-name + ds_type, + name=config_dataset.name, + data_files=config_dataset.data_files, + streaming=False, + split=None, + ) + else: + try: + ds = load_from_disk( + config_dataset.path + ) # pylint: disable=invalid-name + except FileNotFoundError: + ds = load_dataset( + config_dataset.path, + name=config_dataset.name, + streaming=False, + split=None, + ) + elif local_path.is_file(): + ds_type = get_ds_type(config_dataset) + + ds = load_dataset( # pylint: disable=invalid-name + ds_type, + name=config_dataset.name, + data_files=config_dataset.path, + streaming=False, + split=None, + ) + else: + raise ValueError( + "unhandled dataset load: local path exists, but is neither a directory or a file" + ) + elif ds_from_hub: + load_ds_kwargs = {} + if config_dataset.split: + load_ds_kwargs["split"] = config_dataset.split + ds = load_dataset( + config_dataset.path, + name=config_dataset.name, + streaming=False, + data_files=config_dataset.data_files, + token=auth_token, + revision=config_dataset.revision, + trust_remote_code=config_dataset.trust_remote_code, + **load_ds_kwargs, + ) + elif ds_from_cloud and remote_file_system: + if remote_file_system.isdir(config_dataset.path): + ds = load_from_disk( + config_dataset.path, + storage_options=storage_options, + ) + elif remote_file_system.isfile(config_dataset.path): + ds_type = get_ds_type(config_dataset) + ds = load_dataset( + ds_type, + name=config_dataset.name, + data_files=config_dataset.path, + streaming=False, + split=None, + storage_options=storage_options, + trust_remote_code=config_dataset.trust_remote_code, + ) + elif config_dataset.path.startswith("https://"): + ds_type = get_ds_type(config_dataset) + ds = load_dataset( + ds_type, + name=config_dataset.name, + data_files=config_dataset.path, + streaming=False, + split=None, + storage_options=storage_options, + trust_remote_code=config_dataset.trust_remote_code, + ) + else: + if isinstance(config_dataset.data_files, str): + fp = hf_hub_download( + repo_id=config_dataset.path, + repo_type="dataset", + filename=config_dataset.data_files, + revision=config_dataset.revision, + ) + elif isinstance(config_dataset.data_files, list): + fp = [] + for file in config_dataset.data_files: + fp.append( + hf_hub_download( + repo_id=config_dataset.path, + repo_type="dataset", + filename=file, + revision=config_dataset.revision, + ) + ) + else: + raise ValueError("data_files must be either a string or list of strings") + ds = load_dataset( + "json", + name=config_dataset.name, + data_files=fp, + streaming=False, + split=None, + ) + if not ds: + raise ValueError("unhandled dataset load") + + return ds From 2312caaa9870c54d436aa8bc91802005f102203d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 21 Dec 2024 17:38:33 -0500 Subject: [PATCH 0354/1405] GC every n steps (#2209) --- src/axolotl/core/trainer_builder.py | 3 +++ src/axolotl/utils/callbacks/__init__.py | 15 +++++++++++++++ .../utils/config/models/input/v0_4_1/__init__.py | 2 ++ 3 files changed, 20 insertions(+) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 54ee195361..fffddac815 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -56,6 +56,7 @@ from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.callbacks import ( EvalFirstStepCallback, + GCCallback, GPUStatsCallback, LossWatchDogCallback, SaveAxolotlConfigtoWandBCallback, @@ -1452,6 +1453,8 @@ def get_callbacks(self): if self.cfg.loss_watchdog_threshold is not None: callbacks.append(LossWatchDogCallback(self.cfg)) + if self.cfg.gc_steps: + callbacks.append(GCCallback(gc_steps=self.cfg.gc_steps)) callbacks.append(SaveModelCallback()) return callbacks diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 641c9b1623..f1b459b6b3 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +import gc import logging import math import os @@ -842,3 +843,17 @@ def on_train_end( # pylint: disable=unused-argument ): control.should_save = True return control + + +class GCCallback(TrainerCallback): + """Callback to garbage collect torch cache""" + + def __init__(self, gc_steps=None): + self.gc_steps = gc_steps + + def on_step_end( + self, args, state, control, **kwargs # pylint: disable=unused-argument + ): + if state.global_step % self.gc_steps == 0: + torch.cuda.empty_cache() + gc.collect() diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 5ddf048112..c704be800b 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -666,6 +666,8 @@ class Config: loss_watchdog_threshold: Optional[float] = None loss_watchdog_patience: Optional[int] = None + gc_steps: Optional[int] = None + bf16: Optional[Union[Literal["auto"], bool]] = "auto" fp16: Optional[bool] = None bfloat16: Optional[bool] = None # for non-AMP cases From 3742deb1ded554d5b5b61db97c979c12af2cb354 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 22 Dec 2024 12:11:39 -0500 Subject: [PATCH 0355/1405] add deepspeed example with torch compile enabled (#2212) [skip ci] --- deepspeed_configs/zero1_torch_compile.json | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 deepspeed_configs/zero1_torch_compile.json diff --git a/deepspeed_configs/zero1_torch_compile.json b/deepspeed_configs/zero1_torch_compile.json new file mode 100644 index 0000000000..b884513923 --- /dev/null +++ b/deepspeed_configs/zero1_torch_compile.json @@ -0,0 +1,27 @@ +{ + "zero_optimization": { + "stage": 1, + "overlap_comm": true + }, + "bf16": { + "enabled": "auto" + }, + "fp16": { + "enabled": "auto", + "auto_cast": false, + "loss_scale": 0, + "initial_scale_power": 32, + "loss_scale_window": 1000, + "hysteresis": 2, + "min_loss_scale": 1 + }, + "compile": { + "disable": false, + "backend": "inductor" + }, + "gradient_accumulation_steps": "auto", + "gradient_clipping": "auto", + "train_batch_size": "auto", + "train_micro_batch_size_per_gpu": "auto", + "wall_clock_breakdown": false +} From d852d7af7a73fd43f0727dcda43f4c00d2f99e29 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 23 Dec 2024 07:48:41 -0500 Subject: [PATCH 0356/1405] inference - don't default w accelerate, fix base model (#2216) [skip ci] --- src/axolotl/cli/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index 6f883a2ac2..14803e43ba 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -93,7 +93,7 @@ def evaluate(config: str, accelerate: bool, **kwargs): @click.argument("config", type=click.Path(exists=True, path_type=str)) @click.option( "--accelerate/--no-accelerate", - default=True, + default=False, help="Use accelerate launch for multi-GPU inference", ) @click.option( @@ -124,7 +124,7 @@ def inference( if lora_model_dir: kwargs["lora_model_dir"] = lora_model_dir if base_model: - kwargs["output_dir"] = base_model + kwargs["base_model"] = base_model if accelerate: base_cmd = ["accelerate", "launch", "-m", "axolotl.cli.inference"] From e0a2eb2ebd45f3cf73415350f5d7a8ec0b860b7c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 23 Dec 2024 09:08:28 -0500 Subject: [PATCH 0357/1405] fix untrained tokens if specified explicitly from a list (#2210) --- requirements.txt | 2 +- src/axolotl/train.py | 16 +++++++++++++++- .../utils/config/models/input/v0_4_1/__init__.py | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 61e1a9f901..283b5cc2dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -61,4 +61,4 @@ antlr4-python3-runtime==4.13.2 torchao==0.7.0 schedulefree==1.3.0 -axolotl-contribs-lgpl==0.0.1b2 +axolotl-contribs-lgpl==0.0.2 diff --git a/src/axolotl/train.py b/src/axolotl/train.py index dc7289b093..a74ecc2ec3 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -1,5 +1,6 @@ """Prepare and train a model on a dataset. Can also infer from a model or merge lora""" +import inspect import os import signal import sys @@ -126,7 +127,20 @@ def train( ) if cfg.fix_untrained_tokens: - fix_untrained_tokens(model, tokenizer, train_dataset) + # check if the `token_ids_to_fix` kwarg exists in the fix_untrained_tokens args + sig = inspect.signature(fix_untrained_tokens) + # if the function has the `token_ids_to_fix` arg, and fix_untrained_tokens is a list + if "token_ids_to_fix" in sig.parameters and isinstance( + cfg.fix_untrained_tokens, list + ): + fix_untrained_tokens( + model, + tokenizer, + train_dataset, + token_ids_to_fix=cfg.fix_untrained_tokens, + ) + else: + fix_untrained_tokens(model, tokenizer, train_dataset) if cfg.local_rank == 0: model.save_pretrained( str(Path(cfg.output_dir)), safe_serialization=safe_serialization diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index c704be800b..0781c67989 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -794,7 +794,7 @@ class Config: chat_template_jinja: Optional[str] = None default_system_message: Optional[str] = None - fix_untrained_tokens: Optional[bool] = None + fix_untrained_tokens: Optional[Union[int, List[int]]] = None # INTERNALS - document for now, generally not set externally is_preprocess: Optional[bool] = None From 7a38dbe67444fc1224713b68084a04c72b2f6fc9 Mon Sep 17 00:00:00 2001 From: NJordan72 Date: Tue, 24 Dec 2024 16:18:50 -0500 Subject: [PATCH 0358/1405] fix: allow trainer builder to use custom jinja chat template (#2219) * fix: allow trainer builder to use custom jinja chat template * chore: use get_chat_template_from_config Co-authored-by: Chirag Jain * fix: swap imports --------- Co-authored-by: Chirag Jain --- src/axolotl/core/trainer_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index fffddac815..e81740399a 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -68,7 +68,7 @@ ) from axolotl.utils.callbacks.lisa import lisa_callback_factory from axolotl.utils.callbacks.profiler import PytorchProfilerCallback -from axolotl.utils.chat_templates import get_chat_template +from axolotl.utils.chat_templates import get_chat_template_from_config from axolotl.utils.collators import ( BatchSamplerDataCollatorForSeq2Seq, DataCollatorForSeq2Seq, @@ -1834,8 +1834,8 @@ def build(self, total_num_steps): training_arguments_kwargs["model_type"] = self.cfg.model_config_type training_arguments_kwargs["pretraining"] = bool(self.cfg.pretraining_dataset) if self.cfg.chat_template: - training_arguments_kwargs["chat_template"] = get_chat_template( - self.cfg.chat_template, + training_arguments_kwargs["chat_template"] = get_chat_template_from_config( + cfg=self.cfg, tokenizer=self.tokenizer, ) From 3915abee4cba364a83f18cc4b8c2bb571cce3bac Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 31 Dec 2024 15:22:18 -0500 Subject: [PATCH 0359/1405] make sure padding is labeled as -100 for pretraining (#2227) --- src/axolotl/utils/data/pretraining.py | 44 +++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/axolotl/utils/data/pretraining.py b/src/axolotl/utils/data/pretraining.py index 16f38218cd..f493db70eb 100644 --- a/src/axolotl/utils/data/pretraining.py +++ b/src/axolotl/utils/data/pretraining.py @@ -28,8 +28,10 @@ def encode_pretraining( ) # Convert to PyTorch tensors input_ids = [torch.tensor(seq) for seq in res["input_ids"]] + targets = [torch.tensor(seq) for seq in res["input_ids"]] attention_mask = [torch.tensor(seq) for seq in res["attention_mask"]] new_input_ids = [] + new_labels = [] new_attention_mask = [] # Append EOS and PAD tokens to input_ids, and correct attention_mask for i, _ in enumerate(input_ids): @@ -40,22 +42,34 @@ def encode_pretraining( ), dim=0, ) + targets[i] = torch.cat( + ( + targets[i], + torch.tensor([tokenizer.eos_token_id, -100]), + ), + dim=0, + ) attention_mask[i] = torch.cat((attention_mask[i], torch.tensor([1, 0])), dim=0) # Concatenate tokens so that their lengths are less than max_tokens buffer_input_ids = torch.tensor([], dtype=torch.long) + buffer_labels = torch.tensor([], dtype=torch.long) buffer_attention_mask = torch.tensor([], dtype=torch.long) - for ids, mask in zip(input_ids, attention_mask): + for ids, labels, mask in zip(input_ids, targets, attention_mask): if buffer_input_ids.numel() == max_tokens: new_input_ids.append(buffer_input_ids) + new_labels.append(buffer_labels) new_attention_mask.append(buffer_attention_mask) buffer_input_ids = torch.tensor([], dtype=torch.long) + buffer_labels = torch.tensor([], dtype=torch.long) buffer_attention_mask = torch.tensor([], dtype=torch.long) buffer_input_ids = torch.cat((buffer_input_ids, ids), dim=0) + buffer_labels = torch.cat((buffer_labels, labels), dim=0) buffer_attention_mask = torch.cat((buffer_attention_mask, mask), dim=0) elif buffer_input_ids.numel() + ids.numel() <= max_tokens: buffer_input_ids = torch.cat((buffer_input_ids, ids), dim=0) + buffer_labels = torch.cat((buffer_labels, labels), dim=0) buffer_attention_mask = torch.cat((buffer_attention_mask, mask), dim=0) else: buffer_input_ids = torch.cat( @@ -69,6 +83,17 @@ def encode_pretraining( ), dim=0, ) + buffer_labels = torch.cat( + ( + buffer_labels, + torch.full( + (max_tokens - buffer_labels.numel(),), + -100, + dtype=torch.long, + ), + ), + dim=0, + ) buffer_attention_mask = torch.cat( ( buffer_attention_mask, @@ -81,11 +106,14 @@ def encode_pretraining( dim=0, ) new_input_ids.append(buffer_input_ids) + new_labels.append(buffer_labels) new_attention_mask.append(buffer_attention_mask) buffer_input_ids = torch.tensor([], dtype=torch.long) + buffer_labels = torch.tensor([], dtype=torch.long) buffer_attention_mask = torch.tensor([], dtype=torch.long) buffer_input_ids = torch.cat((buffer_input_ids, ids), dim=0) + buffer_labels = torch.cat((buffer_labels, labels), dim=0) buffer_attention_mask = torch.cat((buffer_attention_mask, mask), dim=0) if buffer_input_ids.numel() > 0: # for any leftover tokens @@ -101,6 +129,17 @@ def encode_pretraining( ), dim=0, ) + buffer_labels = torch.cat( + ( + buffer_labels, + torch.full( + (max_tokens - buffer_labels.numel(),), + -100, + dtype=torch.long, + ), + ), + dim=0, + ) buffer_attention_mask = torch.cat( ( buffer_attention_mask, @@ -113,11 +152,12 @@ def encode_pretraining( dim=0, ) new_input_ids.append(buffer_input_ids) + new_labels.append(buffer_labels) new_attention_mask.append(buffer_attention_mask) ret = { "input_ids": [seq.tolist() for seq in new_input_ids], - "labels": [seq.tolist() for seq in new_input_ids], + "labels": [seq.tolist() for seq in new_labels], "attention_mask": [seq.tolist() for seq in new_attention_mask], } From c1b920f29162996087924b403a986b71a076f03f Mon Sep 17 00:00:00 2001 From: salman Date: Tue, 7 Jan 2025 13:42:01 +0000 Subject: [PATCH 0360/1405] Fixing OSX installation (#2231) * bumping version, removing non-osx compatible deps * updating pylintrc * fixing linters * reverting changes --- .pre-commit-config.yaml | 2 +- .pylintrc | 3 ++- setup.py | 23 +++++++++++++++++++---- src/axolotl/utils/callbacks/lisa.py | 2 +- src/axolotl/utils/model_shard_quant.py | 2 +- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f2ceac56e..9409b1ef16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/PyCQA/pylint - rev: v2.17.4 + rev: v3.3.0 hooks: - id: pylint - repo: https://github.com/pre-commit/mirrors-mypy diff --git a/.pylintrc b/.pylintrc index ed973d2859..208dd32b6a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,5 +1,5 @@ [MASTER] -init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" +init-hook="from pylint.config import find_default_config_files; import sys; sys.path.append(next(find_default_config_files()).parent.as_posix())" [TYPECHECK] @@ -12,3 +12,4 @@ generated-members=numpy.*, torch.* disable=missing-function-docstring, line-too-long, import-error, too-many-arguments, too-many-locals, too-many-statements, too-many-branches, too-few-public-methods, too-many-instance-attributes, fixme, import-outside-toplevel, logging-fstring-interpolation, + too-many-positional-arguments, possibly-used-before-assignment diff --git a/setup.py b/setup.py index 4424d430a9..218d85cf73 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ """setup.py for axolotl""" + import ast import os import platform @@ -29,15 +30,29 @@ def parse_requirements(): elif not is_extras and line and line[0] != "#": # Handle standard packages _install_requires.append(line) - try: xformers_version = [req for req in _install_requires if "xformers" in req][0] torchao_version = [req for req in _install_requires if "torchao" in req][0] autoawq_version = [req for req in _install_requires if "autoawq" in req][0] - if "Darwin" in platform.system(): - # don't install xformers on MacOS - _install_requires.pop(_install_requires.index(xformers_version)) + # skip packages not compatible with OSX + skip_packages = [ + "bitsandbytes", + "triton", + "mamba-ssm", + "flash-attn", + "xformers", + "autoawq", + "liger-kernel", + ] + _install_requires = [ + req + for req in _install_requires + if re.split(r"[>=<]", req)[0].strip() not in skip_packages + ] + print( + _install_requires, [req in skip_packages for req in _install_requires] + ) else: # detect the version of torch already installed # and set it so dependencies don't clobber the torch version diff --git a/src/axolotl/utils/callbacks/lisa.py b/src/axolotl/utils/callbacks/lisa.py index ff20959a59..e226471b1a 100644 --- a/src/axolotl/utils/callbacks/lisa.py +++ b/src/axolotl/utils/callbacks/lisa.py @@ -43,7 +43,7 @@ def __init__( getattr, self.layers_attribute.split("."), self.trainer.model ) LOG.info( - f"LISA will activate {self.n_layers}/{len(layers)} layers ({self.n_layers*100/len(layers)}%) every {self.step_interval} steps" + f"LISA will activate {self.n_layers}/{len(layers)} layers ({self.n_layers * 100 / len(layers)}%) every {self.step_interval} steps" ) def freeze_all_layers(self): diff --git a/src/axolotl/utils/model_shard_quant.py b/src/axolotl/utils/model_shard_quant.py index 9ed7ae471d..ecbe86613d 100644 --- a/src/axolotl/utils/model_shard_quant.py +++ b/src/axolotl/utils/model_shard_quant.py @@ -270,7 +270,7 @@ def load_and_quantize_parallel(name_param, model, **kwargs): model.hf_quantizer = AutoHfQuantizer.from_config(quantization_config) if cfg.local_rank == 0 and verbose: - print(f"Loaded model weights in {time.time()-start:.3f} seconds") + print(f"Loaded model weights in {time.time() - start:.3f} seconds") # cleanup any extra memory usage from parallel loading torch.cuda.empty_cache() From 7faf2b6e8ebd3dbaabad74d94f7964e2ad495313 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 9 Jan 2025 15:49:00 -0500 Subject: [PATCH 0361/1405] Merge group queue (#2248) * add support for merge groups * also lint merge groups --- .github/workflows/lint.yml | 1 + .github/workflows/tests.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8f1cfd981a..31695c0e57 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,7 @@ name: lint on: # check on PRs, and manual triggers + merge_group: pull_request: paths: - '**.py' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4a9c33c931..39622e3905 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,7 @@ name: Tests on: # check on push/merge to main, PRs, and manual triggers + merge_group: push: branches: - "main" From 3c1921e400c954fe79ce7d332e06313ea4f396c3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 9 Jan 2025 15:59:54 -0500 Subject: [PATCH 0362/1405] add hf cache caching for GHA (#2247) * add hf cache caching for GHA * use modal volume to cache hf data * make sure to update the cache as we add new fixtures in conftest --- .github/workflows/tests.yml | 36 ++++++++++++++++++++++++++++++++++++ cicd/Dockerfile.jinja | 1 + cicd/multigpu.py | 8 ++++++++ cicd/tests.py | 8 ++++++++ 4 files changed, 53 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 39622e3905..6af794b168 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -61,6 +61,15 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 + - name: Restore HF cache + id: hf-cache-restore + uses: actions/cache/restore@v4 + with: + path: | + /home/runner/.cache/huggingface/hub/datasets--* + /home/runner/.cache/huggingface/hub/models--* + key: ${{ runner.os }}-hf-hub-cache-${{ hashFiles('**/conftest.py') }} + - name: Setup Python uses: actions/setup-python@v5 with: @@ -101,6 +110,15 @@ jobs: run: | find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + - name: Save HF cache + id: hf-cache + uses: actions/cache/save@v4 + with: + path: | + /home/runner/.cache/huggingface/hub/datasets--* + /home/runner/.cache/huggingface/hub/models--* + key: ${{ steps.hf-cache-restore.outputs.cache-primary-key }} + pytest-sdist: name: PyTest from Source Dist runs-on: ubuntu-latest @@ -116,6 +134,15 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 + - name: Restore HF cache + id: hf-cache-restore + uses: actions/cache/restore@v4 + with: + path: | + /home/runner/.cache/huggingface/hub/datasets--* + /home/runner/.cache/huggingface/hub/models--* + key: ${{ runner.os }}-hf-hub-cache-${{ hashFiles('**/conftest.py') }} + - name: Setup Python uses: actions/setup-python@v5 with: @@ -157,6 +184,15 @@ jobs: run: | find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + - name: Save HF cache + id: hf-cache + uses: actions/cache/save@v4 + with: + path: | + /home/runner/.cache/huggingface/hub/datasets--* + /home/runner/.cache/huggingface/hub/models--* + key: ${{ steps.hf-cache-restore.outputs.cache-primary-key }} + docker-e2e-tests-1st: if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index ed64664166..641bd90b6a 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -8,6 +8,7 @@ ENV PYTORCH_VERSION="{{ PYTORCH_VERSION }}" ENV GITHUB_REF="{{ GITHUB_REF }}" ENV GITHUB_SHA="{{ GITHUB_SHA }}" ENV NIGHTLY_BUILD="{{ NIGHTLY_BUILD }}" +ENV HF_HOME="{{ HF_HOME }}" RUN apt-get update && \ apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev diff --git a/cicd/multigpu.py b/cicd/multigpu.py index 0ea4c8cc11..f9bad386a3 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -28,6 +28,7 @@ "CUDA": os.environ.get("CUDA", "121"), "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), + "HF_HOME": "/workspace/data/huggingface-cache/hub", } dockerfile_contents = df_template.render(**df_args) @@ -48,6 +49,12 @@ app = App("Axolotl CI/CD", secrets=[]) +hf_cache_volume = modal.Volume.from_name( + "axolotl-ci-hf-hub-cache", create_if_missing=True +) +VOLUME_CONFIG = { + "/workspace/data/huggingface-cache/hub": hf_cache_volume, +} N_GPUS = int(os.environ.get("N_GPUS", 2)) GPU_CONFIG = modal.gpu.H100(count=N_GPUS) @@ -67,6 +74,7 @@ def run_cmd(cmd: str, run_folder: str): timeout=60 * 60, cpu=8.0, memory=131072 * N_GPUS, + volumes=VOLUME_CONFIG, ) def cicd_pytest(): run_cmd("./cicd/multigpu.sh", "/workspace/axolotl") diff --git a/cicd/tests.py b/cicd/tests.py index f3dbaef105..d7ae5b5e8c 100644 --- a/cicd/tests.py +++ b/cicd/tests.py @@ -29,6 +29,7 @@ "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), "NIGHTLY_BUILD": os.environ.get("NIGHTLY_BUILD", ""), + "HF_HOME": "/workspace/data/huggingface-cache/hub", } dockerfile_contents = df_template.render(**df_args) @@ -50,6 +51,12 @@ app = App("Axolotl CI/CD", secrets=[]) +hf_cache_volume = modal.Volume.from_name( + "axolotl-ci-hf-hub-cache", create_if_missing=True +) +VOLUME_CONFIG = { + "/workspace/data/huggingface-cache/hub": hf_cache_volume, +} N_GPUS = int(os.environ.get("N_GPUS", 1)) GPU_CONFIG = modal.gpu.A10G(count=N_GPUS) @@ -69,6 +76,7 @@ def run_cmd(cmd: str, run_folder: str): timeout=60 * 60, cpu=8.0, memory=131072, + volumes=VOLUME_CONFIG, ) def cicd_pytest(): run_cmd("./cicd/cicd.sh", "/workspace/axolotl") From 2e8d7c1adbce71afa11f40e84eedce26a3d547d8 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 10 Jan 2025 04:00:36 +0700 Subject: [PATCH 0363/1405] fix: mistral nemo does not recognize token_type_ids in forward (#2233) --- src/axolotl/utils/trainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 32e54c9a86..34b505ff14 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -196,7 +196,7 @@ def process_datasets_for_packing(cfg, train_dataset, eval_dataset): if eval_dataset: eval_dataset = eval_dataset.remove_columns("attention_mask") - if cfg.model_config_type == "falcon": + if cfg.model_config_type in ["falcon", "mistral"]: LOG.info("dropping token_type_ids column if it exists") if "token_type_ids" in train_dataset.column_names: train_dataset = train_dataset.remove_columns("token_type_ids") From 5e0124e2ab058bec9a8bcf989245ace8e4b48b4c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 9 Jan 2025 16:01:02 -0500 Subject: [PATCH 0364/1405] update modal version for ci (#2242) --- .github/workflows/multi-gpu-e2e.yml | 2 +- .github/workflows/tests-nightly.yml | 2 +- .github/workflows/tests.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index b4ddef5234..1c6702760b 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -52,7 +52,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==0.63.64 jinja2 + pip install modal==0.71.8 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 3ee12a7098..bbed4e2c2b 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -129,7 +129,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==0.63.64 jinja2 + pip install modal==0.71.8 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6af794b168..a2a0e801e2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -220,7 +220,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==0.63.64 jinja2 + pip install modal==0.71.8 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV @@ -266,7 +266,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==0.63.64 jinja2 + pip install modal==0.71.8 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV From 655368317063a3a1bc9cd508aad29206b7e2644c Mon Sep 17 00:00:00 2001 From: Vincenzo di Cicco <112694549+v-dicicco@users.noreply.github.com> Date: Thu, 9 Jan 2025 22:01:22 +0100 Subject: [PATCH 0365/1405] Use SequentialSampler if curriculum_sampling is enabled with sample_packing (#2235) --- src/axolotl/core/trainer_builder.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index e81740399a..5cc2b2ea90 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -608,8 +608,14 @@ def _get_train_sampler(self) -> Optional[torch.utils.data.Sampler]: self.state.train_batch_size or self.args.per_device_train_batch_size ) batch_max_len = train_batch_size * self.args.max_seq_length + + if self.args.curriculum_sampling: + sampler = SequentialSampler(self.train_dataset) + else: + sampler = RandomSampler(self.train_dataset) + return MultipackBatchSampler( - RandomSampler(self.train_dataset), + sampler, lengths=get_dataset_lengths(self.train_dataset), packing_efficiency_estimate=self.args.sample_packing_efficiency, batch_max_len=batch_max_len, From 7669a03fb4cebd02bedcb8a12d10c3ac66ec2fc5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 9 Jan 2025 16:01:59 -0500 Subject: [PATCH 0366/1405] update upstream HF deps (#2239) * bump axolotl contribs for upstream main conflicts: * bump datasets, tokenizer, trl * remove log workarounds in trl * bump lm-eval * remove unsloth_ import from critical path * remove llama fa2 from conftest * unsloth breaks with latest upstream --- requirements.txt | 10 +- src/axolotl/core/trainer_builder.py | 108 +----------------- src/axolotl/monkeypatch/trainer_fsdp_optim.py | 2 +- src/axolotl/monkeypatch/trainer_grad_accum.py | 2 +- src/axolotl/monkeypatch/unsloth_.py | 13 +-- src/axolotl/monkeypatch/utils.py | 12 +- tests/conftest.py | 12 +- tests/e2e/patched/test_unsloth_integration.py | 5 + tests/e2e/patched/test_unsloth_qlora.py | 3 + 9 files changed, 36 insertions(+), 131 deletions(-) diff --git a/requirements.txt b/requirements.txt index 283b5cc2dd..550fe6eda2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,11 +14,11 @@ packaging==23.2 peft==0.14.0 transformers==4.47.1 -tokenizers>=0.20.1 +tokenizers>=0.21.0 accelerate==1.2.1 -datasets==3.1.0 +datasets==3.2.0 deepspeed==0.16.1 -trl==0.12.1 +trl==0.13.0 optimum==1.16.2 hf_transfer @@ -53,7 +53,7 @@ zstandard==0.22.0 fastcore # lm eval harness -lm_eval==0.4.4 +lm_eval==0.4.7 langdetect==1.0.9 immutabledict==4.2.0 antlr4-python3-runtime==4.13.2 @@ -61,4 +61,4 @@ antlr4-python3-runtime==4.13.2 torchao==0.7.0 schedulefree==1.3.0 -axolotl-contribs-lgpl==0.0.2 +axolotl-contribs-lgpl==0.0.3 diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 5cc2b2ea90..176ce4174f 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -22,7 +22,6 @@ import torch import transformers from datasets import Dataset -from packaging import version from peft.optimizers import create_loraplus_optimizer from torch import nn from torch.optim.lr_scheduler import OneCycleLR @@ -984,12 +983,7 @@ def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> Non logs[key] = torch.tensor(metrics).mean().item() del self._stored_metrics[train_eval] - if version.parse(transformers.__version__) >= version.parse("4.47.0.dev0"): - try: - return super().log(logs, start_time) - except TypeError: - return super().log(logs) # transformers<=4.46 - return super().log(logs) # transformers<=4.46 + return super().log(logs, start_time) def store_metrics( self, metrics: Dict[str, float], train_eval: Literal["train", "eval"] = "train" @@ -1173,22 +1167,6 @@ def training_step( torch.cuda.empty_cache() return loss - def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: - # TODO remove once trl supports the updated to the Trainer.log method - # logs either has 'loss' or 'eval_loss' - train_eval = "train" if "loss" in logs else "eval" - # Add averaged stored metrics to logs - for key, metrics in self._stored_metrics[train_eval].items(): - logs[key] = torch.tensor(metrics).mean().item() - del self._stored_metrics[train_eval] - - if version.parse(transformers.__version__) >= version.parse("4.47.0.dev0"): - return super(DPOTrainer, self).log( # pylint: disable=bad-super-call - logs, start_time - ) - # transformers<=4.46 - return super(DPOTrainer, self).log(logs) # pylint: disable=bad-super-call - class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): """ @@ -1197,22 +1175,6 @@ class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): tag_names = ["axolotl", "orpo"] - def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: - # TODO remove once trl supports the updated to the Trainer.log method - # logs either has 'loss' or 'eval_loss' - train_eval = "train" if "loss" in logs else "eval" - # Add averaged stored metrics to logs - for key, metrics in self._stored_metrics[train_eval].items(): - logs[key] = torch.tensor(metrics).mean().item() - del self._stored_metrics[train_eval] - - if version.parse(transformers.__version__) >= version.parse("4.47.0.dev0"): - return super(ORPOTrainer, self).log( # pylint: disable=bad-super-call - logs, start_time - ) - # transformers<=4.46 - return super(ORPOTrainer, self).log(logs) # pylint: disable=bad-super-call - class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): """ @@ -1221,49 +1183,6 @@ class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): tag_names = ["axolotl", "kto"] - def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: - # TODO remove once trl supports the updated to the Trainer.log method - # logs either has 'loss' or 'eval_loss' - train_eval = "train" if "loss" in logs else "eval" - # train metrics should have no prefix, eval should have 'eval_' - prefix = "eval_" if train_eval == "eval" else "" - # accumulate average metrics from sums and lengths - for split in ["chosen", "rejected"]: - if f"count/{split}" in self._stored_metrics[train_eval]: - count_sum = ( - torch.Tensor(self._stored_metrics[train_eval][f"count/{split}"]) - .sum() - .item() - ) - for metric in ["rewards", "logps", "logits"]: - logs[f"{prefix}{metric}/{split}"] = ( - torch.Tensor( - self._stored_metrics[train_eval][f"{metric}/{split}_sum"] - ) - .sum() - .item() - / count_sum - ) - # delete obsolete metric - del self._stored_metrics[train_eval][f"{metric}/{split}_sum"] - del self._stored_metrics[train_eval][f"count/{split}"] - # calculate reward margin - if f"{prefix}rewards/chosen" in logs and f"{prefix}rewards/rejected" in logs: - logs[f"{prefix}rewards/margins"] = ( - logs[f"{prefix}rewards/chosen"] - logs[f"{prefix}rewards/rejected"] - ) - # Add averaged stored metrics to logs - for key, metrics in self._stored_metrics[train_eval].items(): - logs[f"{prefix}{key}"] = torch.Tensor(metrics).mean().item() - del self._stored_metrics[train_eval] - - if version.parse(transformers.__version__) >= version.parse("4.47.0.dev0"): - return super(KTOTrainer, self).log( # pylint: disable=bad-super-call - logs, start_time - ) - # transformers<=4.46 - return super(KTOTrainer, self).log(logs) # pylint: disable=bad-super-call - class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): """ @@ -1272,22 +1191,6 @@ class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): tag_names = ["axolotl", "cpo"] - def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: - # TODO remove once trl supports the updated to the Trainer.log method - # logs either has 'loss' or 'eval_loss' - train_eval = "train" if "loss" in logs else "eval" - # Add averaged stored metrics to logs - for key, metrics in self._stored_metrics[train_eval].items(): - logs[key] = torch.tensor(metrics).mean().item() - del self._stored_metrics[train_eval] - - if version.parse(transformers.__version__) >= version.parse("4.47.0.dev0"): - return super(CPOTrainer, self).log( # pylint: disable=bad-super-call - logs, start_time - ) - # transformers<=4.46 - return super(CPOTrainer, self).log(logs) # pylint: disable=bad-super-call - class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): """ @@ -1296,15 +1199,6 @@ class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): tag_names = ["axolotl", "reward"] - def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: - # TODO remove once trl supports the updated to the Trainer.log method - if version.parse(transformers.__version__) >= version.parse("4.47.0.dev0"): - return super(RewardTrainer, self).log( # pylint: disable=bad-super-call - logs, start_time - ) - # transformers<=4.46 - return super(RewardTrainer, self).log(logs) # pylint: disable=bad-super-call - class TrainerBuilderBase(abc.ABC): """ diff --git a/src/axolotl/monkeypatch/trainer_fsdp_optim.py b/src/axolotl/monkeypatch/trainer_fsdp_optim.py index 185f742d77..00c2dfebcc 100644 --- a/src/axolotl/monkeypatch/trainer_fsdp_optim.py +++ b/src/axolotl/monkeypatch/trainer_fsdp_optim.py @@ -6,7 +6,7 @@ from transformers import Trainer -from axolotl.monkeypatch.unsloth_ import detab_code +from axolotl.monkeypatch.utils import detab_code LOG = logging.getLogger("axolotl.monkeypatch.trainer_fsdp_save") diff --git a/src/axolotl/monkeypatch/trainer_grad_accum.py b/src/axolotl/monkeypatch/trainer_grad_accum.py index 550f00e306..05d7067047 100644 --- a/src/axolotl/monkeypatch/trainer_grad_accum.py +++ b/src/axolotl/monkeypatch/trainer_grad_accum.py @@ -8,7 +8,7 @@ from transformers import LlamaForCausalLM, Trainer from transformers.modeling_flash_attention_utils import _flash_attention_forward -from axolotl.monkeypatch.unsloth_ import detab_code +from axolotl.monkeypatch.utils import detab_code LOG = logging.getLogger("axolotl.monkeypatch.trainer_grad_accum") diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py index 21fdb7edff..c81bacbfcb 100644 --- a/src/axolotl/monkeypatch/unsloth_.py +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -1,9 +1,7 @@ """module for patching with unsloth optimizations""" import inspect -import re import types -from typing import Tuple import torch from accelerate.logging import get_logger @@ -11,6 +9,8 @@ from torch import nn from transformers.models.llama.modeling_llama import LlamaFlashAttention2 +from axolotl.monkeypatch.utils import detab_code + LOG = get_logger("axolotl.monkeypatch.unsloth") ORIGINAL_QKV_CODE = """ @@ -93,15 +93,6 @@ def UnslothForCausalLMLoss( # pylint: disable=invalid-name raise ValueError("Unsupported model type") -def detab_code(code: str) -> Tuple[str, str]: - try: - spaces = re.match(r"([\s\t]{1,})", code).group(0) - code = re.sub(r"^" + spaces, "", code, flags=re.MULTILINE) - except AttributeError: - return code, "" - return code, spaces - - self_attn_lora_patched = False # pylint: disable=invalid-name diff --git a/src/axolotl/monkeypatch/utils.py b/src/axolotl/monkeypatch/utils.py index f29f21be77..c2772b4715 100644 --- a/src/axolotl/monkeypatch/utils.py +++ b/src/axolotl/monkeypatch/utils.py @@ -1,7 +1,8 @@ """ Shared utils for the monkeypatches """ -from typing import Optional +import re +from typing import Optional, Tuple import torch import torch.nn.functional as F @@ -223,3 +224,12 @@ def patched_prepare_4d_causal_attention_mask_for_sdpa( mask_2d_to_4d(attention_mask, dtype=dtype), *args, ) + + +def detab_code(code: str) -> Tuple[str, str]: + try: + spaces = re.match(r"([\s\t]{1,})", code).group(0) + code = re.sub(r"^" + spaces, "", code, flags=re.MULTILINE) + except AttributeError: + return code, "" + return code, spaces diff --git a/tests/conftest.py b/tests/conftest.py index f2519cdcfd..85e2767223 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -120,13 +120,12 @@ def temp_dir(): @pytest.fixture(scope="function", autouse=True) def cleanup_monkeypatches(): from transformers import Trainer - from transformers.models.llama.modeling_llama import ( + from transformers.models.llama.modeling_llama import ( # LlamaFlashAttention2, LlamaAttention, - LlamaFlashAttention2, LlamaForCausalLM, ) - original_fa2_forward = LlamaFlashAttention2.forward + # original_fa2_forward = LlamaFlashAttention2.forward original_llama_attn_forward = LlamaAttention.forward original_llama_forward = LlamaForCausalLM.forward original_trainer_inner_training_loop = ( @@ -136,7 +135,7 @@ def cleanup_monkeypatches(): # monkey patches can happen inside the tests yield # Reset LlamaFlashAttention2 forward - LlamaFlashAttention2.forward = original_fa2_forward + # LlamaFlashAttention2.forward = original_fa2_forward LlamaAttention.forward = original_llama_attn_forward LlamaForCausalLM.forward = original_llama_forward Trainer._inner_training_loop = ( # pylint: disable=protected-access @@ -149,7 +148,10 @@ def cleanup_monkeypatches(): ("transformers.models.llama",), ( "transformers.models.llama.modeling_llama", - ["LlamaFlashAttention2", "LlamaAttention"], + [ + # "LlamaFlashAttention2", + "LlamaAttention", + ], ), ("transformers.trainer",), ("transformers", ["Trainer"]), diff --git a/tests/e2e/patched/test_unsloth_integration.py b/tests/e2e/patched/test_unsloth_integration.py index 8882742861..bc6476dab6 100644 --- a/tests/e2e/patched/test_unsloth_integration.py +++ b/tests/e2e/patched/test_unsloth_integration.py @@ -1,9 +1,14 @@ """Test module for checking whether the integration of Unsloth with Hugging Face Transformers is working as expected.""" import unittest +import pytest + from axolotl.monkeypatch.unsloth_ import check_self_attn_is_patchable +@pytest.mark.skip( + reason="Unsloth integration will be broken going into latest transformers" +) class TestUnslothIntegration(unittest.TestCase): """Unsloth monkeypatch integration tests.""" diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index b58406185a..0c0ee8610f 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -20,6 +20,9 @@ # pylint: disable=duplicate-code +@pytest.mark.skip( + reason="Unsloth integration will be broken going into latest transformers" +) class TestUnslothQLoRA: """ Test class for Unsloth QLoRA Llama models From ed77e7001e05556d5e17c8b8faa7577bcfcd8958 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 10 Jan 2025 04:04:13 +0700 Subject: [PATCH 0367/1405] feat: add support for data_files in pretraining (#2238) --- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 + src/axolotl/utils/data/sft.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 0781c67989..bb88a0baa0 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -128,6 +128,7 @@ class PretrainingDataset(BaseModel): text_column: Optional[str] = "text" type: Optional[str] = "pretrain" trust_remote_code: Optional[bool] = False + data_files: Optional[str] = None class UserDefinedPrompterType(BaseModel): diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 3e784ca3e7..cfc40406e9 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -88,6 +88,7 @@ def prepare_dataset(cfg, tokenizer, processor=None): path = cfg.pretraining_dataset split = "train" name = None + data_files = None if isinstance(cfg.pretraining_dataset, list) and isinstance( cfg.pretraining_dataset[0], dict ): @@ -96,6 +97,8 @@ def prepare_dataset(cfg, tokenizer, processor=None): if "split" in cfg.pretraining_dataset[0]: split = cfg.pretraining_dataset[0]["split"] + data_files = cfg.pretraining_dataset[0].get("data_files") + ds_wrapper_partial = functools.partial( get_dataset_wrapper, cfg.pretraining_dataset[0], @@ -105,7 +108,9 @@ def prepare_dataset(cfg, tokenizer, processor=None): ) train_dataset = wrap_pretraining_dataset( - load_dataset(path, streaming=True, split=split, name=name), + load_dataset( + path, streaming=True, split=split, name=name, data_files=data_files + ), tokenizer, cfg, ds_wrapper_partial, From fb3352e21c62192b276dc84b5b1713077fb6bc5b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 9 Jan 2025 17:31:43 -0500 Subject: [PATCH 0368/1405] rename liger test so it properly runs in ci (#2246) --- requirements.txt | 2 +- setup.py | 3 ++ src/axolotl/integrations/liger/__init__.py | 14 +++--- .../integrations/{liger.py => test_liger.py} | 47 +++++++++---------- tests/e2e/test_optimizers.py | 1 + tests/e2e/utils.py | 16 ++++++- .../integrations/{liger.py => test_liger.py} | 45 +++++++++--------- tests/test_prompt_tokenizers.py | 8 ---- 8 files changed, 70 insertions(+), 66 deletions(-) rename tests/e2e/integrations/{liger.py => test_liger.py} (74%) rename tests/integrations/{liger.py => test_liger.py} (59%) diff --git a/requirements.txt b/requirements.txt index 550fe6eda2..1f7ac7bbad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # START section of dependencies that don't install on Darwin/MacOS bitsandbytes==0.45.0 -triton>=2.3.0 +triton>=3.0.0 mamba-ssm==1.2.0.post1 flash-attn==2.7.0.post2 xformers>=0.0.23.post1 diff --git a/setup.py b/setup.py index 218d85cf73..d7cb18ec01 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ def parse_requirements(): _install_requires.append(line) try: xformers_version = [req for req in _install_requires if "xformers" in req][0] + triton_version = [req for req in _install_requires if "triton" in req][0] torchao_version = [req for req in _install_requires if "torchao" in req][0] autoawq_version = [req for req in _install_requires if "autoawq" in req][0] if "Darwin" in platform.system(): @@ -88,6 +89,8 @@ def parse_requirements(): _install_requires.append("xformers==0.0.28.post1") elif (major, minor) >= (2, 3): _install_requires.pop(_install_requires.index(torchao_version)) + _install_requires.pop(_install_requires.index(triton_version)) + _install_requires.append("triton>=2.3.1") if patch == 0: _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.26.post1") diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index fda98e469f..b67dd01e64 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -22,13 +22,6 @@ import logging import sys -from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss -from liger_kernel.transformers.functional import liger_cross_entropy -from liger_kernel.transformers.monkey_patch import MODEL_TYPE_TO_APPLY_LIGER_FN -from liger_kernel.transformers.rms_norm import LigerRMSNorm -from liger_kernel.transformers.rope import liger_rotary_pos_emb -from liger_kernel.transformers.swiglu import LigerSwiGLUMLP - from axolotl.integrations.base import BasePlugin from ...utils.distributed import zero_only @@ -46,6 +39,13 @@ def get_input_args(self): return "axolotl.integrations.liger.LigerArgs" def pre_model_load(self, cfg): + from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss + from liger_kernel.transformers.functional import liger_cross_entropy + from liger_kernel.transformers.monkey_patch import MODEL_TYPE_TO_APPLY_LIGER_FN + from liger_kernel.transformers.rms_norm import LigerRMSNorm + from liger_kernel.transformers.rope import liger_rotary_pos_emb + from liger_kernel.transformers.swiglu import LigerSwiGLUMLP + if cfg.model_config_type in MODEL_TYPE_TO_APPLY_LIGER_FN: apply_liger_fn = MODEL_TYPE_TO_APPLY_LIGER_FN[cfg.model_config_type] liger_fn_sig = inspect.signature(apply_liger_fn) diff --git a/tests/e2e/integrations/liger.py b/tests/e2e/integrations/test_liger.py similarity index 74% rename from tests/e2e/integrations/liger.py rename to tests/e2e/integrations/test_liger.py index 455c3d2818..ce9299b925 100644 --- a/tests/e2e/integrations/liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -1,43 +1,40 @@ """ Simple end-to-end test for Liger integration """ -import unittest from pathlib import Path +from e2e.utils import require_torch_2_4_1 + from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs from axolotl.train import train from axolotl.utils.config import normalize_config, prepare_plugins from axolotl.utils.dict import DictDefault -from ..utils import with_temp_dir - -class LigerIntegrationTestCase(unittest.TestCase): +class LigerIntegrationTestCase: """ e2e tests for liger integration with Axolotl """ - @with_temp_dir + @require_torch_2_4_1 def test_llama_wo_flce(self, temp_dir): + # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", "plugins": [ "axolotl.integrations.liger.LigerPlugin", ], "liger_rope": True, "liger_rms_norm": True, - "liger_swiglu": True, + "liger_glu_activation": True, "liger_cross_entropy": True, "liger_fused_linear_cross_entropy": False, "sequence_len": 1024, - "val_set_size": 0.1, + "val_set_size": 0.05, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -46,15 +43,15 @@ def test_llama_wo_flce(self, temp_dir): }, ], "num_epochs": 1, - "micro_batch_size": 8, - "gradient_accumulation_steps": 1, + "micro_batch_size": 2, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch", "lr_scheduler": "cosine", "save_safetensors": True, "bf16": "auto", - "max_steps": 10, + "max_steps": 5, } ) prepare_plugins(cfg) @@ -65,26 +62,24 @@ def test_llama_wo_flce(self, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) assert (Path(temp_dir) / "model.safetensors").exists() - @with_temp_dir + @require_torch_2_4_1 def test_llama_w_flce(self, temp_dir): + # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", "plugins": [ "axolotl.integrations.liger.LigerPlugin", ], "liger_rope": True, "liger_rms_norm": True, - "liger_swiglu": True, + "liger_glu_activation": True, "liger_cross_entropy": False, "liger_fused_linear_cross_entropy": True, "sequence_len": 1024, - "val_set_size": 0.1, + "val_set_size": 0.05, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -93,15 +88,15 @@ def test_llama_w_flce(self, temp_dir): }, ], "num_epochs": 1, - "micro_batch_size": 8, - "gradient_accumulation_steps": 1, + "micro_batch_size": 2, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch", "lr_scheduler": "cosine", "save_safetensors": True, "bf16": "auto", - "max_steps": 10, + "max_steps": 5, } ) prepare_plugins(cfg) diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 2317bfb97a..f69d0500ff 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -113,6 +113,7 @@ def test_adopt_adamw(self, temp_dir): @with_temp_dir def test_fft_schedule_free_adamw(self, temp_dir): + # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index de5b599a13..1e05c32c48 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -49,7 +49,19 @@ def is_min_2_3_1(): torch_version = version.parse(torch.__version__) return torch_version >= version.parse("2.3.1") - return unittest.skipUnless(is_min_2_3_1(), "test torch 2.3.1")(test_case) + return unittest.skipUnless(is_min_2_3_1(), "test requires torch>=2.3.1")(test_case) + + +def require_torch_2_4_1(test_case): + """ + Decorator marking a test that requires torch >= 2.5.1 + """ + + def is_min_2_4_1(): + torch_version = version.parse(torch.__version__) + return torch_version >= version.parse("2.4.1") + + return unittest.skipUnless(is_min_2_4_1(), "test requires torch>=2.4.1")(test_case) def require_torch_2_5_1(test_case): @@ -61,7 +73,7 @@ def is_min_2_5_1(): torch_version = version.parse(torch.__version__) return torch_version >= version.parse("2.5.1") - return unittest.skipUnless(is_min_2_5_1(), "test torch 2.5.1")(test_case) + return unittest.skipUnless(is_min_2_5_1(), "test requires torch>=2.5.1")(test_case) def is_hopper(): diff --git a/tests/integrations/liger.py b/tests/integrations/test_liger.py similarity index 59% rename from tests/integrations/liger.py rename to tests/integrations/test_liger.py index 61540a57c4..c75bc13054 100644 --- a/tests/integrations/liger.py +++ b/tests/integrations/test_liger.py @@ -7,11 +7,11 @@ import pytest -from axolotl.utils.config import validate_config +from axolotl.utils.config import prepare_plugins, validate_config from axolotl.utils.dict import DictDefault -@pytest.fixture(name="minimal_base_cfg") +@pytest.fixture(name="minimal_liger_cfg") def fixture_cfg(): return DictDefault( { @@ -25,56 +25,57 @@ def fixture_cfg(): ], "micro_batch_size": 1, "gradient_accumulation_steps": 1, + "plugins": ["axolotl.integrations.liger.LigerPlugin"], } ) -class BaseValidation: +# pylint: disable=too-many-public-methods +class TestValidation: """ - Base validation module to setup the log capture + Test the validation module for liger """ _caplog: Optional[pytest.LogCaptureFixture] = None @pytest.fixture(autouse=True) def inject_fixtures(self, caplog): + caplog.set_level(logging.WARNING) self._caplog = caplog - -# pylint: disable=too-many-public-methods -class TestValidation(BaseValidation): - """ - Test the validation module for liger - """ - - def test_deprecated_swiglu(self, minimal_cfg): + def test_deprecated_swiglu(self, minimal_liger_cfg): test_cfg = DictDefault( { "liger_swiglu": False, } - | minimal_cfg + | minimal_liger_cfg ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level( + logging.WARNING, logger="axolotl.integrations.liger.args" + ): + prepare_plugins(test_cfg) updated_cfg = validate_config(test_cfg) - assert ( - "The 'liger_swiglu' argument is deprecated" - in self._caplog.records[0].message - ) + # TODO this test is brittle in CI + # assert ( + # "The 'liger_swiglu' argument is deprecated" + # in self._caplog.records[0].message + # ) assert updated_cfg.liger_swiglu is None - assert updated_cfg.liger_glu_activations is False + assert updated_cfg.liger_glu_activation is False - def test_conflict_swiglu_ligergluactivation(self, minimal_cfg): + def test_conflict_swiglu_ligergluactivation(self, minimal_liger_cfg): test_cfg = DictDefault( { "liger_swiglu": False, - "liger_glu_activations": True, + "liger_glu_activation": True, } - | minimal_cfg + | minimal_liger_cfg ) with pytest.raises( ValueError, match=r".*You cannot have both `liger_swiglu` and `liger_glu_activation` set.*", ): + prepare_plugins(test_cfg) validate_config(test_cfg) diff --git a/tests/test_prompt_tokenizers.py b/tests/test_prompt_tokenizers.py index 4fb72f3e1d..c085df4630 100644 --- a/tests/test_prompt_tokenizers.py +++ b/tests/test_prompt_tokenizers.py @@ -4,9 +4,7 @@ import logging import unittest from pathlib import Path -from typing import Optional -import pytest from datasets import load_dataset from transformers import AddedToken, AutoTokenizer, LlamaTokenizer @@ -65,12 +63,6 @@ class TestPromptTokenizationStrategies(unittest.TestCase): Test class for prompt tokenization strategies. """ - _caplog: Optional[pytest.LogCaptureFixture] = None - - @pytest.fixture(autouse=True) - def inject_fixtures(self, caplog): - self._caplog = caplog - def setUp(self) -> None: # pylint: disable=duplicate-code self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") From d8b4027200de0fe60f4ae0a71272c1a8cb2888f7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 10 Jan 2025 08:35:25 -0500 Subject: [PATCH 0369/1405] use 2.5.1 docker images as latest tag as it seems stable (#2198) --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b4344dfe20..89b2746e4a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,7 +25,6 @@ jobs: python_version: "3.11" pytorch: 2.3.1 axolotl_extras: mamba-ssm - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -36,6 +35,7 @@ jobs: python_version: "3.11" pytorch: 2.5.1 axolotl_extras: + is_latest: true runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -92,7 +92,6 @@ jobs: python_version: "3.11" pytorch: 2.3.1 axolotl_extras: - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -103,6 +102,7 @@ jobs: python_version: "3.11" pytorch: 2.5.1 axolotl_extras: + is_latest: true runs-on: axolotl-gpu-runner steps: - name: Checkout From dd26cc3c0fddd6164633497d1a5416bc7a3c536b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 13 Jan 2025 10:43:29 -0500 Subject: [PATCH 0370/1405] add helper to verify the correct model output file exists (#2245) * add helper to verify the correct model output file exists * more checks using helper * chore: lint * fix import and relora model check * workaround for trl trainer saves * remove stray print --- src/axolotl/cli/utils.py | 1 - .../integrations/test_cut_cross_entropy.py | 8 +++--- tests/e2e/integrations/test_liger.py | 7 ++--- tests/e2e/patched/test_4d_multipack_llama.py | 7 +++-- tests/e2e/patched/test_fa_xentropy.py | 5 ++-- tests/e2e/patched/test_falcon_samplepack.py | 7 +++-- tests/e2e/patched/test_fused_llama.py | 5 ++-- tests/e2e/patched/test_llama_s2_attention.py | 7 +++-- .../e2e/patched/test_lora_llama_multipack.py | 7 +++-- tests/e2e/patched/test_mistral_samplepack.py | 7 +++-- tests/e2e/patched/test_mixtral_samplepack.py | 7 +++-- tests/e2e/patched/test_phi_multipack.py | 7 +++-- tests/e2e/patched/test_resume.py | 5 ++-- tests/e2e/patched/test_unsloth_qlora.py | 9 +++---- tests/e2e/test_dpo.py | 16 ++++++------ tests/e2e/test_embeddings_lr.py | 7 +++-- tests/e2e/test_falcon.py | 9 +++---- tests/e2e/test_llama.py | 9 ++++--- tests/e2e/test_llama_pretrain.py | 5 ++-- tests/e2e/test_llama_vision.py | 7 +++-- tests/e2e/test_lora_llama.py | 5 ++-- tests/e2e/test_mamba.py | 5 ++-- tests/e2e/test_mistral.py | 7 +++-- tests/e2e/test_mixtral.py | 13 +++++----- tests/e2e/test_optimizers.py | 9 +++---- tests/e2e/test_phi.py | 7 +++-- tests/e2e/test_relora_llama.py | 8 +++--- tests/e2e/test_reward_model_llama.py | 5 ++-- tests/e2e/utils.py | 26 +++++++++++++++++++ 29 files changed, 116 insertions(+), 111 deletions(-) diff --git a/src/axolotl/cli/utils.py b/src/axolotl/cli/utils.py index f0e2573f72..85d241b5d0 100644 --- a/src/axolotl/cli/utils.py +++ b/src/axolotl/cli/utils.py @@ -27,7 +27,6 @@ def decorator(function): field_type = next( t for t in get_args(field_type) if not isinstance(t, NoneType) ) - if field_type == bool: field_name = field.name.replace("_", "-") option_name = f"--{field_name}/--no-{field_name}" diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py index a74813e3a0..6562af176f 100644 --- a/tests/e2e/integrations/test_cut_cross_entropy.py +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -2,8 +2,6 @@ Simple end-to-end test for Cut Cross Entropy integration """ -from pathlib import Path - import pytest from axolotl.cli import load_datasets @@ -13,6 +11,8 @@ from axolotl.utils.config import normalize_config, prepare_plugins from axolotl.utils.dict import DictDefault +from ..utils import check_model_output_exists + # pylint: disable=duplicate-code @@ -67,7 +67,7 @@ def test_llama_w_cce(self, min_cfg, temp_dir): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) else: train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) @pytest.mark.parametrize( "attention_type", @@ -95,4 +95,4 @@ def test_llama_w_cce_and_attention(self, min_cfg, temp_dir, attention_type): train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) else: train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/integrations/test_liger.py b/tests/e2e/integrations/test_liger.py index ce9299b925..9154bf9b8d 100644 --- a/tests/e2e/integrations/test_liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -1,7 +1,6 @@ """ Simple end-to-end test for Liger integration """ -from pathlib import Path from e2e.utils import require_torch_2_4_1 @@ -11,6 +10,8 @@ from axolotl.utils.config import normalize_config, prepare_plugins from axolotl.utils.dict import DictDefault +from ..utils import check_model_output_exists + class LigerIntegrationTestCase: """ @@ -60,7 +61,7 @@ def test_llama_wo_flce(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) @require_torch_2_4_1 def test_llama_w_flce(self, temp_dir): @@ -105,4 +106,4 @@ def test_llama_w_flce(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index b0ada92304..08b3bf0daf 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import require_torch_2_3_1, with_temp_dir +from ..utils import check_model_output_exists, require_torch_2_3_1, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -67,7 +66,7 @@ def test_sdp_lora_packing(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_torch_lora_packing(self, temp_dir): @@ -111,4 +110,4 @@ def test_torch_lora_packing(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index 183843b7b1..791d955b28 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -4,7 +4,6 @@ import logging import os -from pathlib import Path import pytest from transformers.utils import is_torch_bf16_gpu_available @@ -15,7 +14,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import check_tensorboard +from ..utils import check_model_output_exists, check_tensorboard LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -82,7 +81,7 @@ def test_lora_packing_fa_cross_entropy(self, temp_dir, gradient_accumulation_ste dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) check_tensorboard( temp_dir + "/runs", "train/train_loss", 1.5, "Train Loss is too high" diff --git a/tests/e2e/patched/test_falcon_samplepack.py b/tests/e2e/patched/test_falcon_samplepack.py index d9d7151032..69516810f6 100644 --- a/tests/e2e/patched/test_falcon_samplepack.py +++ b/tests/e2e/patched/test_falcon_samplepack.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import with_temp_dir +from ..utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -69,7 +68,7 @@ def test_qlora(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_ft(self, temp_dir): @@ -109,4 +108,4 @@ def test_ft(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "pytorch_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index 36b7442d9e..23a0adfc07 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path import pytest from transformers.utils import is_torch_bf16_gpu_available @@ -16,7 +15,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import with_temp_dir +from ..utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -73,4 +72,4 @@ def test_fft_packing(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "pytorch_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py index 0f2539daf8..d0fdd918a1 100644 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ b/tests/e2e/patched/test_llama_s2_attention.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path import pytest @@ -15,7 +14,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import with_temp_dir +from ..utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -71,7 +70,7 @@ def test_lora_s2_attn(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_fft_s2_attn(self, temp_dir): @@ -111,4 +110,4 @@ def test_fft_s2_attn(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "pytorch_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index be2f133fb0..634e544d20 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path import pytest from transformers.utils import is_auto_gptq_available, is_torch_bf16_gpu_available @@ -16,7 +15,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import with_temp_dir +from ..utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -76,7 +75,7 @@ def test_lora_packing(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @pytest.mark.skipif(not is_auto_gptq_available(), reason="auto-gptq not available") @with_temp_dir @@ -126,4 +125,4 @@ def test_lora_gptq_packed(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index 6685fb9d57..e93863e09c 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import with_temp_dir +from ..utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -69,7 +68,7 @@ def test_lora_packing(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_ft_packing(self, temp_dir): @@ -110,4 +109,4 @@ def test_ft_packing(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "pytorch_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index 684baaaff8..f87c34fd10 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import with_temp_dir +from ..utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -66,7 +65,7 @@ def test_qlora(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_ft(self, temp_dir): @@ -108,4 +107,4 @@ def test_ft(self, temp_dir): "MixtralFlashAttention2" in model.model.layers[0].self_attn.__class__.__name__ ) - assert (Path(temp_dir) / "pytorch_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_phi_multipack.py b/tests/e2e/patched/test_phi_multipack.py index 7b5bf92dfa..852ac7bec1 100644 --- a/tests/e2e/patched/test_phi_multipack.py +++ b/tests/e2e/patched/test_phi_multipack.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import with_temp_dir +from ..utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -69,7 +68,7 @@ def test_ft_packed(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "pytorch_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_qlora_packed(self, temp_dir): @@ -120,4 +119,4 @@ def test_qlora_packed(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index 7d82ea8c37..5639d2eaee 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -6,7 +6,6 @@ import os import re import subprocess -from pathlib import Path from transformers.utils import is_torch_bf16_gpu_available @@ -16,7 +15,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import most_recent_subdir +from ..utils import check_model_output_exists, most_recent_subdir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -83,7 +82,7 @@ def test_resume_lora_packed(self, temp_dir): cli_args = TrainerCliArgs() train(cfg=resume_cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) tb_log_path_1 = most_recent_subdir(temp_dir + "/runs") cmd = f"tensorboard --inspect --logdir {tb_log_path_1}" diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index 0c0ee8610f..492bc1c236 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -3,7 +3,6 @@ """ import logging import os -from pathlib import Path import pytest @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import check_tensorboard +from ..utils import check_model_output_exists, check_tensorboard LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -77,7 +76,7 @@ def test_unsloth_llama_qlora_fa2(self, temp_dir, sample_packing): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) check_tensorboard( temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" @@ -127,7 +126,7 @@ def test_unsloth_llama_qlora_unpacked(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) check_tensorboard( temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" @@ -182,7 +181,7 @@ def test_unsloth_llama_qlora_unpacked_no_fa2_fp16(self, temp_dir, sdp_attention) dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) check_tensorboard( temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index 4a705922fa..f8109373a9 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -15,7 +15,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir +from .utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -68,7 +68,7 @@ def test_dpo_lora(self, temp_dir): dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) @with_temp_dir def test_dpo_nll_lora(self, temp_dir): @@ -113,7 +113,7 @@ def test_dpo_nll_lora(self, temp_dir): dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) @with_temp_dir def test_dpo_use_weighting(self, temp_dir): @@ -158,7 +158,7 @@ def test_dpo_use_weighting(self, temp_dir): dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) @pytest.mark.skip("kto_pair no longer supported in trl") @with_temp_dir @@ -203,7 +203,7 @@ def test_kto_pair_lora(self, temp_dir): dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) @with_temp_dir def test_ipo_lora(self, temp_dir): @@ -247,7 +247,7 @@ def test_ipo_lora(self, temp_dir): dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) @with_temp_dir def test_orpo_lora(self, temp_dir): @@ -294,7 +294,7 @@ def test_orpo_lora(self, temp_dir): dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) @pytest.mark.skip(reason="Fix the implementation") @with_temp_dir @@ -358,4 +358,4 @@ def test_kto_lora(self, temp_dir): dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "checkpoint-20/adapter_model.safetensors").exists() + check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py index 6e5ebd05f7..222d620ae7 100644 --- a/tests/e2e/test_embeddings_lr.py +++ b/tests/e2e/test_embeddings_lr.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import check_tensorboard, with_temp_dir +from .utils import check_model_output_exists, check_tensorboard, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -62,7 +61,7 @@ def test_train_w_embedding_lr_scale(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) check_tensorboard( temp_dir + "/runs", "train/train_loss", 2.0, "Loss is too high" @@ -106,7 +105,7 @@ def test_train_w_embedding_lr(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) check_tensorboard( temp_dir + "/runs", "train/train_loss", 2.0, "Loss is too high" diff --git a/tests/e2e/test_falcon.py b/tests/e2e/test_falcon.py index c76699a7c8..117de66353 100644 --- a/tests/e2e/test_falcon.py +++ b/tests/e2e/test_falcon.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir +from .utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -71,7 +70,7 @@ def test_lora(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_lora_added_vocab(self, temp_dir): @@ -124,7 +123,7 @@ def test_lora_added_vocab(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_ft(self, temp_dir): @@ -163,4 +162,4 @@ def test_ft(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "pytorch_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index 1ce9d60b98..4384bb61e7 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -4,7 +4,8 @@ import logging import os -from pathlib import Path + +from e2e.utils import check_model_output_exists from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -60,7 +61,7 @@ def test_fft_trust_remote_code(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) def test_fix_untrained_tokens(self, temp_dir): # pylint: disable=duplicate-code @@ -103,7 +104,7 @@ def test_fix_untrained_tokens(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) def test_batch_flattening(self, temp_dir): # pylint: disable=duplicate-code @@ -142,4 +143,4 @@ def test_batch_flattening(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index 62fb63c471..d13b10659a 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir +from .utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -64,4 +63,4 @@ def test_pretrain_w_sample_packing(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py index 1d583a3267..250cf418c0 100644 --- a/tests/e2e/test_llama_vision.py +++ b/tests/e2e/test_llama_vision.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir +from .utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -68,7 +67,7 @@ def test_lora_llama_vision_text_only_dataset(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_lora_llama_vision_multimodal_dataset(self, temp_dir): @@ -113,4 +112,4 @@ def test_lora_llama_vision_multimodal_dataset(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index d06be60b96..a7ead64a53 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir +from .utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -65,4 +64,4 @@ def test_lora(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_mamba.py b/tests/e2e/test_mamba.py index 8755fa4d51..a1fc308629 100644 --- a/tests/e2e/test_mamba.py +++ b/tests/e2e/test_mamba.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path import pytest @@ -15,7 +14,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir +from .utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -65,4 +64,4 @@ def test_fft(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "pytorch_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_mistral.py b/tests/e2e/test_mistral.py index 57d85e51eb..2e79fec8db 100644 --- a/tests/e2e/test_mistral.py +++ b/tests/e2e/test_mistral.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from transformers.utils import is_torch_bf16_gpu_available @@ -15,7 +14,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir +from .utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -69,7 +68,7 @@ def test_lora(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_ft(self, temp_dir): @@ -112,4 +111,4 @@ def test_ft(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "pytorch_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_mixtral.py b/tests/e2e/test_mixtral.py index d4dad14ef2..6792d05a67 100644 --- a/tests/e2e/test_mixtral.py +++ b/tests/e2e/test_mixtral.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path import torch from transformers.utils import is_torch_bf16_gpu_available @@ -16,7 +15,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir +from .utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -79,7 +78,7 @@ def test_qlora_w_fa2(self, temp_dir): model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype == torch.float32 ) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_qlora_wo_fa2(self, temp_dir): @@ -133,7 +132,7 @@ def test_qlora_wo_fa2(self, temp_dir): model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype == torch.float32 ) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_16bit_lora_w_fa2(self, temp_dir): @@ -190,7 +189,7 @@ def test_16bit_lora_w_fa2(self, temp_dir): model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype == torch.float32 ) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_16bit_lora_wo_fa2(self, temp_dir): @@ -247,7 +246,7 @@ def test_16bit_lora_wo_fa2(self, temp_dir): model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype == torch.float32 ) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_ft(self, temp_dir): @@ -287,4 +286,4 @@ def test_ft(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "pytorch_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index f69d0500ff..f1bbaafd5c 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import require_torch_2_5_1, with_temp_dir +from .utils import check_model_output_exists, require_torch_2_5_1, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -65,7 +64,7 @@ def test_optimi_adamw(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir @require_torch_2_5_1 @@ -109,7 +108,7 @@ def test_adopt_adamw(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_fft_schedule_free_adamw(self, temp_dir): @@ -145,4 +144,4 @@ def test_fft_schedule_free_adamw(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "model.safetensors").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_phi.py b/tests/e2e/test_phi.py index 4cc6bcdcc9..7a08d0c6f7 100644 --- a/tests/e2e/test_phi.py +++ b/tests/e2e/test_phi.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir +from .utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -67,7 +66,7 @@ def test_phi_ft(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "pytorch_model.bin").exists() + check_model_output_exists(temp_dir, cfg) @with_temp_dir def test_phi_qlora(self, temp_dir): @@ -116,4 +115,4 @@ def test_phi_qlora(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_relora_llama.py b/tests/e2e/test_relora_llama.py index 84582896dc..fef6a3d303 100644 --- a/tests/e2e/test_relora_llama.py +++ b/tests/e2e/test_relora_llama.py @@ -13,7 +13,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import check_tensorboard, with_temp_dir +from .utils import check_model_output_exists, check_tensorboard, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -78,10 +78,10 @@ def test_relora(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + check_model_output_exists(Path(temp_dir) / "checkpoint-100/adapter", cfg) assert ( - Path(temp_dir) / "checkpoint-100/adapter/adapter_model.safetensors" - ).exists() - assert (Path(temp_dir) / "checkpoint-100/relora/model.safetensors").exists() + Path(temp_dir) / "checkpoint-100/relora/model.safetensors" + ).exists(), "Relora model checkpoint not found" check_tensorboard( temp_dir + "/runs", "train/grad_norm", 0.2, "grad_norm is too high" diff --git a/tests/e2e/test_reward_model_llama.py b/tests/e2e/test_reward_model_llama.py index 27ac3e25f1..c4cb705ea8 100644 --- a/tests/e2e/test_reward_model_llama.py +++ b/tests/e2e/test_reward_model_llama.py @@ -5,7 +5,6 @@ import logging import os import unittest -from pathlib import Path from axolotl.cli import load_datasets from axolotl.common.cli import TrainerCliArgs @@ -13,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import with_temp_dir +from .utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -71,4 +70,4 @@ def test_rm_fft(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) - assert (Path(temp_dir) / "adapter_model.bin").exists() + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 1e05c32c48..759d596596 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -14,6 +14,8 @@ from packaging import version from tbparse import SummaryReader +from axolotl.utils.dict import DictDefault + def with_temp_dir(test_func): @wraps(test_func) @@ -93,3 +95,27 @@ def check_tensorboard( df = reader.scalars # pylint: disable=invalid-name df = df[(df.tag == tag)] # pylint: disable=invalid-name assert df.value.values[-1] < lt_val, assertion_err + + +def check_model_output_exists(temp_dir: str, cfg: DictDefault) -> None: + """ + helper function to check if a model output file exists after training + + checks based on adapter or not and if safetensors saves are enabled or not + """ + + if cfg.save_safetensors: + if not cfg.adapter: + assert (Path(temp_dir) / "model.safetensors").exists() + else: + assert (Path(temp_dir) / "adapter_model.safetensors").exists() + else: + # check for both, b/c in trl, it often defaults to saving safetensors + if not cfg.adapter: + assert (Path(temp_dir) / "pytorch_model.bin").exists() or ( + Path(temp_dir) / "model.safetensors" + ).exists() + else: + assert (Path(temp_dir) / "adapter_model.bin").exists() or ( + Path(temp_dir) / "adapter_model.safetensors" + ).exists() From bc1c9c20e3cadb2a60163fa73370161a806124f0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 13 Jan 2025 10:44:11 -0500 Subject: [PATCH 0371/1405] assume empty lora dropout means 0.0 and add tests (#2243) * assume empty lora dropout means 0.0 and add tests * remove un-necessary arg * refactor based on pr feedback: * chore: lint --- .../config/models/input/v0_4_1/__init__.py | 7 ++ tests/test_lora.py | 69 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 tests/test_lora.py diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index bb88a0baa0..19ce7b18c0 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -367,6 +367,13 @@ def convert_loraplus_lr_embedding(cls, loraplus_lr_embedding): loraplus_lr_embedding = float(loraplus_lr_embedding) return loraplus_lr_embedding + @model_validator(mode="before") + @classmethod + def validate_lora_dropout(cls, data): + if data.get("adapter") is not None and data.get("lora_dropout") is None: + data["lora_dropout"] = 0.0 + return data + class ReLoRAConfig(BaseModel): """ReLoRA configuration subset""" diff --git a/tests/test_lora.py b/tests/test_lora.py new file mode 100644 index 0000000000..b917ff3f95 --- /dev/null +++ b/tests/test_lora.py @@ -0,0 +1,69 @@ +""" +tests for loading loras +""" +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault +from axolotl.utils.models import load_model, load_tokenizer + +# pylint: disable=duplicate-code +minimal_config = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "learning_rate": 0.000001, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + } + ], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + } +) + + +class TestLoRALoad: + """ + Test class for loading LoRA weights + """ + + def test_load_lora_weights(self): + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.0, + "lora_target_linear": True, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "sequence_len": 1024, + } + | minimal_config + ) + cfg = validate_config(cfg) + normalize_config(cfg) + tokenizer = load_tokenizer(cfg) + load_model(cfg, tokenizer) + + def test_load_lora_weights_empty_dropout(self): + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": None, + "lora_target_linear": True, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "sequence_len": 1024, + } + | minimal_config + ) + cfg = validate_config(cfg) + normalize_config(cfg) + assert cfg.lora_dropout == 0.0 + tokenizer = load_tokenizer(cfg) + load_model(cfg, tokenizer) From f89e9621191f4460afe4425b3785dd8ca0482a9c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 13 Jan 2025 10:44:45 -0500 Subject: [PATCH 0372/1405] skip over rows in pretraining dataset (#2223) * skip over rows in pretraining dataset * update docs --- docs/dataset-formats/pretraining.qmd | 9 ++++++++- .../utils/config/models/input/v0_4_1/__init__.py | 1 + src/axolotl/utils/data/sft.py | 10 +++++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/dataset-formats/pretraining.qmd b/docs/dataset-formats/pretraining.qmd index bb591328e2..600fb63e09 100644 --- a/docs/dataset-formats/pretraining.qmd +++ b/docs/dataset-formats/pretraining.qmd @@ -19,7 +19,14 @@ For pretraining, there is no prompt template or roles. The only required field Axolotl usually loads the entire dataset into memory. This will be challenging for large datasets. Use the following config to enable streaming: ```{.yaml filename="config.yaml"} -pretraining_dataset: # hf path only +pretraining_dataset: + - name: + path: + split: + text_column: # column in dataset with the data, usually `text` + type: pretrain + trust_remote_code: + skip: # number of rows of data to skip over from the beginning ... ``` diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 19ce7b18c0..4f368994ac 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -129,6 +129,7 @@ class PretrainingDataset(BaseModel): type: Optional[str] = "pretrain" trust_remote_code: Optional[bool] = False data_files: Optional[str] = None + skip: Optional[int] = None class UserDefinedPrompterType(BaseModel): diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index cfc40406e9..aff0476754 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -89,11 +89,13 @@ def prepare_dataset(cfg, tokenizer, processor=None): split = "train" name = None data_files = None + skip = 0 if isinstance(cfg.pretraining_dataset, list) and isinstance( cfg.pretraining_dataset[0], dict ): path = cfg.pretraining_dataset[0]["path"] name = cfg.pretraining_dataset[0]["name"] + skip = cfg.pretraining_dataset[0]["skip"] if "split" in cfg.pretraining_dataset[0]: split = cfg.pretraining_dataset[0]["split"] @@ -107,10 +109,12 @@ def prepare_dataset(cfg, tokenizer, processor=None): cfg.pretraining_dataset[0]["type"] or "pretrain", ) + iter_ds = load_dataset(path, streaming=True, split=split, name=name, data_files=data_files) + if skip: + LOG.info(f"Skipping {skip} samples from the dataset") + iter_ds = iter_ds.skip(skip) train_dataset = wrap_pretraining_dataset( - load_dataset( - path, streaming=True, split=split, name=name, data_files=data_files - ), + iter_ds, tokenizer, cfg, ds_wrapper_partial, From 1ed4de73b615fe9d943906e0bf5429b1466cb77e Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 13 Jan 2025 12:55:29 -0500 Subject: [PATCH 0373/1405] CLI cleanup and documentation (#2244) * CLI init refactor * fix * cleanup and (partial) docs * Adding documentation and continuing cleanup (in progress) * remove finetune.py script * continued cleanup and documentation * pytest fixes * review comments * fix * Fix * typing fixes * make sure the batch dataset patcher for multipack is always loaded when handling datasets * review comments * fix --------- Co-authored-by: Dan Saunders Co-authored-by: Wing Lian --- scripts/finetune.py | 52 -- src/axolotl/cli/__init__.py | 565 +----------------- src/axolotl/cli/args.py | 43 ++ src/axolotl/cli/art.py | 23 + src/axolotl/cli/checks.py | 50 ++ src/axolotl/cli/config.py | 217 +++++++ src/axolotl/cli/evaluate.py | 46 +- src/axolotl/cli/inference.py | 259 +++++++- src/axolotl/cli/main.py | 182 +++--- src/axolotl/cli/merge_lora.py | 62 +- src/axolotl/cli/merge_sharded_fsdp_weights.py | 49 +- src/axolotl/cli/preprocess.py | 73 ++- src/axolotl/cli/shard.py | 45 -- src/axolotl/cli/train.py | 65 +- src/axolotl/cli/utils.py | 156 ++++- src/axolotl/common/cli.py | 69 --- src/axolotl/common/datasets.py | 140 +++++ src/axolotl/evaluate.py | 14 +- src/axolotl/train.py | 29 +- src/axolotl/utils/data/sft.py | 4 +- tests/cli/conftest.py | 1 + tests/cli/test_cli_fetch.py | 1 + tests/cli/test_cli_inference.py | 1 + tests/cli/test_cli_interface.py | 1 + tests/cli/test_cli_merge_lora.py | 1 + .../test_cli_merge_sharded_fsdp_weights.py | 44 +- tests/cli/test_cli_preprocess.py | 1 + tests/cli/test_cli_shard.py | 76 --- tests/cli/test_cli_version.py | 1 + tests/cli/test_utils.py | 1 + .../integrations/test_cut_cross_entropy.py | 12 +- tests/e2e/integrations/test_liger.py | 8 +- tests/e2e/patched/test_4d_multipack_llama.py | 8 +- tests/e2e/patched/test_cli_integrations.py | 2 +- tests/e2e/patched/test_fa_xentropy.py | 6 +- tests/e2e/patched/test_falcon_samplepack.py | 8 +- tests/e2e/patched/test_fused_llama.py | 6 +- tests/e2e/patched/test_llama_s2_attention.py | 8 +- .../e2e/patched/test_lora_llama_multipack.py | 8 +- tests/e2e/patched/test_mistral_samplepack.py | 8 +- tests/e2e/patched/test_mixtral_samplepack.py | 8 +- tests/e2e/patched/test_model_patches.py | 7 +- tests/e2e/patched/test_phi_multipack.py | 8 +- tests/e2e/patched/test_resume.py | 8 +- tests/e2e/patched/test_unsloth_qlora.py | 10 +- tests/e2e/test_dpo.py | 32 +- tests/e2e/test_embeddings_lr.py | 8 +- tests/e2e/test_falcon.py | 10 +- tests/e2e/test_llama.py | 10 +- tests/e2e/test_llama_pretrain.py | 6 +- tests/e2e/test_llama_vision.py | 8 +- tests/e2e/test_lora_llama.py | 6 +- tests/e2e/test_mamba.py | 6 +- tests/e2e/test_mistral.py | 8 +- tests/e2e/test_mixtral.py | 14 +- tests/e2e/test_optimizers.py | 10 +- tests/e2e/test_packing_loss.py | 6 +- tests/e2e/test_phi.py | 8 +- tests/e2e/test_relora_llama.py | 6 +- tests/e2e/test_reward_model_llama.py | 6 +- 60 files changed, 1270 insertions(+), 1260 deletions(-) delete mode 100644 scripts/finetune.py create mode 100644 src/axolotl/cli/args.py create mode 100644 src/axolotl/cli/art.py create mode 100644 src/axolotl/cli/checks.py create mode 100644 src/axolotl/cli/config.py delete mode 100644 src/axolotl/cli/shard.py delete mode 100644 src/axolotl/common/cli.py create mode 100644 src/axolotl/common/datasets.py delete mode 100644 tests/cli/test_cli_shard.py diff --git a/scripts/finetune.py b/scripts/finetune.py deleted file mode 100644 index d5bbcaf8f0..0000000000 --- a/scripts/finetune.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Prepare and train a model on a dataset. Can also infer from a model or merge lora""" -import logging -from pathlib import Path - -import fire -import transformers - -from axolotl.cli import ( - check_accelerate_default_config, - check_user_token, - do_inference, - do_merge_lora, - load_cfg, - load_datasets, - print_axolotl_text_art, -) -from axolotl.cli.shard import shard -from axolotl.common.cli import TrainerCliArgs -from axolotl.train import train - -LOG = logging.getLogger("axolotl.scripts.finetune") - - -def do_cli(config: Path = Path("examples/"), **kwargs): - print_axolotl_text_art() - LOG.warning( - str( - PendingDeprecationWarning( - "scripts/finetune.py will be replaced with calling axolotl.cli.train" - ) - ) - ) - parsed_cfg = load_cfg(config, **kwargs) - check_accelerate_default_config() - check_user_token() - parser = transformers.HfArgumentParser((TrainerCliArgs)) - parsed_cli_args, _ = parser.parse_args_into_dataclasses( - return_remaining_strings=True - ) - if parsed_cli_args.inference: - do_inference(cfg=parsed_cfg, cli_args=parsed_cli_args) - elif parsed_cli_args.merge_lora: - do_merge_lora(cfg=parsed_cfg, cli_args=parsed_cli_args) - elif parsed_cli_args.shard: - shard(cfg=parsed_cfg, cli_args=parsed_cli_args) - else: - dataset_meta = load_datasets(cfg=parsed_cfg, cli_args=parsed_cli_args) - train(cfg=parsed_cfg, cli_args=parsed_cli_args, dataset_meta=dataset_meta) - - -if __name__ == "__main__": - fire.Fire(do_cli) diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index d07b10ce3d..b20e4f085a 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -1,568 +1,5 @@ -"""Prepare and train a model on a dataset. Can also infer from a model or merge lora""" +"""Axolotl CLI module initialization.""" -import importlib -import json -import logging -import math import os -import random -import sys -import tempfile -from pathlib import Path -from threading import Thread -from typing import Any, Dict, List, Optional, Union -from urllib.parse import urlparse - -import requests -import torch -import yaml - -# add src to the pythonpath so we don't need to pip install this -from accelerate.commands.config import config_args -from art import text2art -from huggingface_hub import HfApi -from huggingface_hub.utils import LocalTokenNotFoundError -from transformers import GenerationConfig, TextIteratorStreamer, TextStreamer -from transformers.utils import is_torch_bf16_gpu_available -from transformers.utils.import_utils import _is_package_available - -from axolotl.common.cli import TrainerCliArgs, load_model_and_tokenizer -from axolotl.logging_config import configure_logging -from axolotl.train import TrainDatasetMeta -from axolotl.utils.chat_templates import ( - get_chat_template, - get_chat_template_from_config, -) -from axolotl.utils.comet_ import setup_comet_env_vars -from axolotl.utils.config import ( - normalize_cfg_datasets, - normalize_config, - prepare_plugins, - validate_config, -) -from axolotl.utils.data import load_prepare_dpo_datasets, prepare_dataset -from axolotl.utils.dict import DictDefault -from axolotl.utils.distributed import is_main_process -from axolotl.utils.mlflow_ import setup_mlflow_env_vars -from axolotl.utils.models import load_processor, load_tokenizer -from axolotl.utils.tokenization import check_dataset_labels -from axolotl.utils.trainer import prepare_opinionated_env, prepare_optim_env -from axolotl.utils.wandb_ import setup_wandb_env_vars - -project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -src_dir = os.path.join(project_root, "src") -sys.path.insert(0, src_dir) - -configure_logging() -LOG = logging.getLogger("axolotl.scripts") os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1" - -AXOLOTL_LOGO = """ - #@@ #@@ @@# @@# - @@ @@ @@ @@ =@@# @@ #@ =@@#. - @@ #@@@@@@@@@ @@ #@#@= @@ #@ .=@@ - #@@@@@@@@@@@@@@@@@ =@# @# ##= ## =####=+ @@ =#####+ =#@@###. @@ - @@@@@@@@@@/ +@@/ +@@ #@ =@= #@= @@ =@#+ +#@# @@ =@#+ +#@# #@. @@ - @@@@@@@@@@ ##@@ ##@@ =@# @# =@# @# @@ @@ @@ @@ #@ #@ @@ - @@@@@@@@@@@@@@@@@@@@ #@=+++#@= =@@# @@ @@ @@ @@ #@ #@ @@ - =@#=====@@ =@# @# @@ @@ @@ @@ #@ #@ @@ - @@@@@@@@@@@@@@@@ @@@@ #@ #@= #@= +@@ #@# =@# @@. =@# =@# #@. @@ - =@# @# #@= #@ =#@@@@#= +#@@= +#@@@@#= .##@@+ @@ - @@@@ @@@@@@@@@@@@@@@@ -""" - - -def print_legacy_axolotl_text_art(suffix=None): - font = "nancyj" - ascii_text = " axolotl" - if suffix: - ascii_text += f" x {suffix}" - ascii_art = text2art(ascii_text, font=font) - - if is_main_process(): - print(ascii_art) - - print_dep_versions() - - -def print_axolotl_text_art( - **kwargs, # pylint: disable=unused-argument -): - if is_main_process(): - print(AXOLOTL_LOGO) - - -def print_dep_versions(): - packages = ["accelerate", "peft", "transformers", "trl", "torch", "bitsandbytes"] - max_len = max(len(pkg) for pkg in packages) - if is_main_process(): - print("*" * 40) - print("**** Axolotl Dependency Versions *****") - for pkg in packages: - pkg_version = _is_package_available(pkg, return_version=True) - print(f"{pkg: >{max_len}}: {pkg_version[1]: <15}") - print("*" * 40) - - -def check_remote_config(config: Union[str, Path]): - # Check if the config is a valid HTTPS URL to a .yml or .yaml file - if not (isinstance(config, str) and config.startswith("https://")): - return config # Return the original value if it's not a valid URL - - filename = os.path.basename(urlparse(config).path) - temp_dir = tempfile.mkdtemp() - - try: - response = requests.get(config, timeout=30) - response.raise_for_status() # Check for HTTP errors - - content = response.content - try: - # Try parsing as JSON first to catch cases where JSON content is mistakenly considered YAML - json.loads(content) - # Log a warning but do not raise an error; JSON is technically valid YAML - this can happen when you forget to point to a raw github link - LOG.warning( - f"Warning: The content of the file at {config} is JSON, which is technically valid YAML but might not be intended." - ) - except json.JSONDecodeError: - # If it's not valid JSON, verify it's valid YAML - try: - yaml.safe_load(content) - except yaml.YAMLError as err: - raise ValueError( - f"Failed to parse the content at {config} as YAML: {err}" - ) from err - - # Write the content to a file if it's valid YAML (or JSON treated as YAML) - output_path = Path(temp_dir) / filename - with open(output_path, "wb") as file: - file.write(content) - LOG.info( - f"Using the following config obtained from {config}: \n\n{content.decode('utf-8')}\n" - ) - return output_path - - except requests.RequestException as err: - # This catches all requests-related exceptions including HTTPError - raise RuntimeError(f"Failed to download {config}: {err}") from err - except Exception as err: - # Catch-all for any other exceptions - raise err - - -def get_multi_line_input() -> Optional[str]: - print("Give me an instruction (Ctrl + D to submit): ") - instruction = "" - for line in sys.stdin: - instruction += line # pylint: disable=consider-using-join - # instruction = pathlib.Path("/proc/self/fd/0").read_text() - return instruction - - -def do_merge_lora( - *, - cfg: DictDefault, - cli_args: TrainerCliArgs, -): - model, tokenizer = load_model_and_tokenizer(cfg=cfg, cli_args=cli_args) - safe_serialization = cfg.save_safetensors is True - - LOG.info("running merge of LoRA with base model") - model = model.merge_and_unload(progressbar=True) - try: - model.to(dtype=cfg.torch_dtype) - except RuntimeError: - pass - model.generation_config.do_sample = True - - if cfg.local_rank == 0: - LOG.info(f"saving merged model to: {str(Path(cfg.output_dir) / 'merged')}") - model.save_pretrained( - str(Path(cfg.output_dir) / "merged"), - safe_serialization=safe_serialization, - progressbar=True, - ) - tokenizer.save_pretrained(str(Path(cfg.output_dir) / "merged")) - - -def do_inference( - *, - cfg: DictDefault, - cli_args: TrainerCliArgs, -): - model, tokenizer = load_model_and_tokenizer(cfg=cfg, cli_args=cli_args) - prompter = cli_args.prompter - - prompter_module = None - chat_template_str = None - if prompter: - prompter_module = getattr( - importlib.import_module("axolotl.prompters"), prompter - ) - elif cfg.chat_template: - chat_template_str = get_chat_template(cfg.chat_template) - elif cfg.datasets[0].type == "chat_template": - chat_template_str = get_chat_template_from_config( - cfg=cfg, ds_cfg=cfg.datasets[0], tokenizer=tokenizer - ) - - model = model.to(cfg.device, dtype=cfg.torch_dtype) - - while True: - print("=" * 80) - # support for multiline inputs - instruction = get_multi_line_input() - if not instruction: - return - - if prompter_module: - prompt: str = next( - prompter_module().build_prompt(instruction=instruction.strip("\n")) - ) - else: - prompt = instruction.strip() - - if chat_template_str: - batch = tokenizer.apply_chat_template( - [ - { - "role": "user", - "content": prompt, - } - ], - return_tensors="pt", - add_special_tokens=True, - add_generation_prompt=True, - chat_template=chat_template_str, - tokenize=True, - return_dict=True, - ) - else: - batch = tokenizer(prompt, return_tensors="pt", add_special_tokens=True) - - print("=" * 40) - model.eval() - with torch.no_grad(): - generation_config = GenerationConfig( - repetition_penalty=1.1, - max_new_tokens=1024, - temperature=0.9, - top_p=0.95, - top_k=40, - bos_token_id=tokenizer.bos_token_id, - eos_token_id=tokenizer.eos_token_id, - pad_token_id=tokenizer.pad_token_id, - do_sample=True, - use_cache=True, - return_dict_in_generate=True, - output_attentions=False, - output_hidden_states=False, - output_scores=False, - ) - streamer = TextStreamer(tokenizer) - generated = model.generate( - inputs=batch["input_ids"].to(cfg.device), - generation_config=generation_config, - streamer=streamer, - ) - print("=" * 40) - print(tokenizer.decode(generated["sequences"].cpu().tolist()[0])) - - -def do_inference_gradio( - *, - cfg: DictDefault, - cli_args: TrainerCliArgs, -): - import gradio as gr - - model, tokenizer = load_model_and_tokenizer(cfg=cfg, cli_args=cli_args) - prompter = cli_args.prompter - - prompter_module = None - chat_template_str = None - if prompter: - prompter_module = getattr( - importlib.import_module("axolotl.prompters"), prompter - ) - elif cfg.chat_template: - chat_template_str = get_chat_template(cfg.chat_template, tokenizer=tokenizer) - - model = model.to(cfg.device, dtype=cfg.torch_dtype) - - def generate(instruction): - if not instruction: - return - if prompter_module: - # pylint: disable=stop-iteration-return - prompt: str = next( - prompter_module().build_prompt(instruction=instruction.strip("\n")) - ) - else: - prompt = instruction.strip() - - if chat_template_str: - batch = tokenizer.apply_chat_template( - [ - { - "role": "user", - "content": prompt, - } - ], - return_tensors="pt", - add_special_tokens=True, - add_generation_prompt=True, - chat_template=chat_template_str, - tokenize=True, - return_dict=True, - ) - else: - batch = tokenizer(prompt, return_tensors="pt", add_special_tokens=True) - - model.eval() - with torch.no_grad(): - generation_config = GenerationConfig( - repetition_penalty=1.1, - max_new_tokens=cfg.get("gradio_max_new_tokens", 1024), - temperature=cfg.get("gradio_temperature", 0.9), - top_p=0.95, - top_k=40, - bos_token_id=tokenizer.bos_token_id, - eos_token_id=tokenizer.eos_token_id, - pad_token_id=tokenizer.pad_token_id, - do_sample=True, - use_cache=True, - return_dict_in_generate=True, - output_attentions=False, - output_hidden_states=False, - output_scores=False, - ) - streamer = TextIteratorStreamer(tokenizer) - generation_kwargs = { - "inputs": batch["input_ids"].to(cfg.device), - "attention_mask": batch["attention_mask"].to(cfg.device), - "generation_config": generation_config, - "streamer": streamer, - } - - thread = Thread(target=model.generate, kwargs=generation_kwargs) - thread.start() - - all_text = "" - - for new_text in streamer: - all_text += new_text - yield all_text - - demo = gr.Interface( - fn=generate, - inputs="textbox", - outputs="text", - title=cfg.get("gradio_title", "Axolotl Gradio Interface"), - ) - - demo.queue().launch( - show_api=False, - share=cfg.get("gradio_share", True), - server_name=cfg.get("gradio_server_name", "127.0.0.1"), - server_port=cfg.get("gradio_server_port", None), - ) - - -def choose_config(path: Path): - yaml_files = list(path.glob("*.yml")) - - if not yaml_files: - raise ValueError( - "No YAML config files found in the specified directory. Are you using a .yml extension?" - ) - - if len(yaml_files) == 1: - print(f"Using default YAML file '{yaml_files[0]}'") - return str(yaml_files[0]) - - print("Choose a YAML file:") - for idx, file in enumerate(yaml_files): - print(f"{idx + 1}. {file}") - - chosen_file = None - while chosen_file is None: - try: - choice = int(input("Enter the number of your choice: ")) - if 1 <= choice <= len(yaml_files): - chosen_file = str(yaml_files[choice - 1]) - else: - print("Invalid choice. Please choose a number from the list.") - except ValueError: - print("Invalid input. Please enter a number.") - - return chosen_file - - -def check_not_in(list1: List[str], list2: Union[Dict[str, Any], List[str]]) -> bool: - return not any(el in list2 for el in list1) - - -def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs): - config = check_remote_config(config) - if Path(config).is_dir(): - config = choose_config(Path(config)) - - # load the config from the yaml file - with open(config, encoding="utf-8") as file: - cfg: DictDefault = DictDefault(yaml.safe_load(file)) - # if there are any options passed in the cli, if it is something that seems valid from the yaml, - # then overwrite the value - cfg_keys = cfg.keys() - for k, _ in kwargs.items(): - # if not strict, allow writing to cfg even if it's not in the yml already - if k in cfg_keys or not cfg.strict: - # handle booleans - if isinstance(cfg[k], bool): - cfg[k] = bool(kwargs[k]) - else: - cfg[k] = kwargs[k] - - cfg.axolotl_config_path = config - - try: - device_props = torch.cuda.get_device_properties("cuda") - gpu_version = "sm_" + str(device_props.major) + str(device_props.minor) - except: # pylint: disable=bare-except # noqa: E722 - gpu_version = None - - prepare_plugins(cfg) - - cfg = validate_config( - cfg, - capabilities={ - "bf16": is_torch_bf16_gpu_available(), - "n_gpu": int(os.environ.get("WORLD_SIZE", 1)), - "compute_capability": gpu_version, - }, - env_capabilities={ - "torch_version": str(torch.__version__).split("+", maxsplit=1)[0], - }, - ) - - prepare_optim_env(cfg) - - prepare_opinionated_env(cfg) - - normalize_config(cfg) - - normalize_cfg_datasets(cfg) - - setup_wandb_env_vars(cfg) - - setup_mlflow_env_vars(cfg) - - setup_comet_env_vars(cfg) - - return cfg - - -def load_datasets( - *, - cfg: DictDefault, - cli_args: TrainerCliArgs, -) -> TrainDatasetMeta: - tokenizer = load_tokenizer(cfg) - processor = load_processor(cfg, tokenizer=tokenizer) if cfg.processor_type else None - - train_dataset, eval_dataset, total_num_steps, prompters = prepare_dataset( - cfg, - tokenizer, - processor=processor, - ) - - if ( - cli_args.debug - or cfg.debug - or cli_args.debug_text_only - or int(cli_args.debug_num_examples) > 0 - ): - LOG.info("check_dataset_labels...") - check_dataset_labels( - train_dataset.select( - [ - random.randrange(0, len(train_dataset) - 1) # nosec - for _ in range(cli_args.debug_num_examples) - ] - ), - tokenizer, - num_examples=cli_args.debug_num_examples, - text_only=cli_args.debug_text_only, - ) - - LOG.info("printing prompters...") - for prompter in prompters: - LOG.info(prompter) - - return TrainDatasetMeta( - train_dataset=train_dataset, - eval_dataset=eval_dataset, - total_num_steps=total_num_steps, - ) - - -def load_rl_datasets( - *, - cfg: DictDefault, - cli_args: TrainerCliArgs, # pylint: disable=unused-argument -) -> TrainDatasetMeta: - train_dataset, eval_dataset = load_prepare_dpo_datasets(cfg) - total_num_steps = int( - math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) - ) - - if cli_args.debug or cfg.debug: - LOG.info("check_dataset_labels...") - - tokenizer = load_tokenizer(cfg) - check_dataset_labels( - train_dataset.select( - [ - random.randrange(0, len(train_dataset) - 1) # nosec - for _ in range(cli_args.debug_num_examples) - ] - ), - tokenizer, - num_examples=cli_args.debug_num_examples, - text_only=cli_args.debug_text_only, - rl_mode=True, - ) - - return TrainDatasetMeta( - train_dataset=train_dataset, - eval_dataset=eval_dataset, - total_num_steps=total_num_steps, - ) - - -def check_accelerate_default_config(): - if Path(config_args.default_yaml_config_file).exists(): - LOG.warning( - f"accelerate config file found at {config_args.default_yaml_config_file}. This can lead to unexpected errors" - ) - - -def check_user_token(): - # Skip check if HF_HUB_OFFLINE is set to True - if os.getenv("HF_HUB_OFFLINE") == "1": - LOG.info( - "Skipping HuggingFace token verification because HF_HUB_OFFLINE is set to True. Only local files will be used." - ) - return True - - # Verify if token is valid - api = HfApi() - try: - user_info = api.whoami() - return bool(user_info) - except LocalTokenNotFoundError: - LOG.warning( - "Error verifying HuggingFace token. Remember to log in using `huggingface-cli login` and get your access token from https://huggingface.co/settings/tokens if you want to use gated models or datasets." - ) - return False diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py new file mode 100644 index 0000000000..0618e07f16 --- /dev/null +++ b/src/axolotl/cli/args.py @@ -0,0 +1,43 @@ +"""Module for axolotl CLI command arguments.""" + +from dataclasses import dataclass, field +from typing import Optional + + +@dataclass +class PreprocessCliArgs: + """Dataclass with CLI arguments for `axolotl preprocess` command.""" + + debug: bool = field(default=False) + debug_text_only: bool = field(default=False) + debug_num_examples: int = field(default=1) + prompter: Optional[str] = field(default=None) + download: Optional[bool] = field(default=True) + + +@dataclass +class TrainerCliArgs: + """Dataclass with CLI arguments for `axolotl train` command.""" + + debug: bool = field(default=False) + debug_text_only: bool = field(default=False) + debug_num_examples: int = field(default=0) + merge_lora: bool = field(default=False) + prompter: Optional[str] = field(default=None) + shard: bool = field(default=False) + + +@dataclass +class EvaluateCliArgs: + """Dataclass with CLI arguments for `axolotl evaluate` command.""" + + debug: bool = field(default=False) + debug_text_only: bool = field(default=False) + debug_num_examples: int = field(default=0) + + +@dataclass +class InferenceCliArgs: + """Dataclass with CLI arguments for `axolotl inference` command.""" + + prompter: Optional[str] = field(default=None) diff --git a/src/axolotl/cli/art.py b/src/axolotl/cli/art.py new file mode 100644 index 0000000000..6ed22a52d7 --- /dev/null +++ b/src/axolotl/cli/art.py @@ -0,0 +1,23 @@ +"""Axolotl ASCII logo utils.""" + +from axolotl.utils.distributed import is_main_process + +AXOLOTL_LOGO = """ + #@@ #@@ @@# @@# + @@ @@ @@ @@ =@@# @@ #@ =@@#. + @@ #@@@@@@@@@ @@ #@#@= @@ #@ .=@@ + #@@@@@@@@@@@@@@@@@ =@# @# ##= ## =####=+ @@ =#####+ =#@@###. @@ + @@@@@@@@@@/ +@@/ +@@ #@ =@= #@= @@ =@#+ +#@# @@ =@#+ +#@# #@. @@ + @@@@@@@@@@ ##@@ ##@@ =@# @# =@# @# @@ @@ @@ @@ #@ #@ @@ + @@@@@@@@@@@@@@@@@@@@ #@=+++#@= =@@# @@ @@ @@ @@ #@ #@ @@ + =@#=====@@ =@# @# @@ @@ @@ @@ #@ #@ @@ + @@@@@@@@@@@@@@@@ @@@@ #@ #@= #@= +@@ #@# =@# @@. =@# =@# #@. @@ + =@# @# #@= #@ =#@@@@#= +#@@= +#@@@@#= .##@@+ @@ + @@@@ @@@@@@@@@@@@@@@@ +""" + + +def print_axolotl_text_art(): + """Prints axolotl ASCII art.""" + if is_main_process(): + print(AXOLOTL_LOGO) diff --git a/src/axolotl/cli/checks.py b/src/axolotl/cli/checks.py new file mode 100644 index 0000000000..cc3ed0d9f1 --- /dev/null +++ b/src/axolotl/cli/checks.py @@ -0,0 +1,50 @@ +"""Various checks for Axolotl CLI.""" + +import logging +import os +from pathlib import Path + +from accelerate.commands.config import config_args +from huggingface_hub import HfApi +from huggingface_hub.utils import LocalTokenNotFoundError + +from axolotl.logging_config import configure_logging + +configure_logging() +LOG = logging.getLogger(__name__) + + +def check_accelerate_default_config() -> None: + """Logs at warning level if no accelerate config file is found.""" + if Path(config_args.default_yaml_config_file).exists(): + LOG.warning( + f"accelerate config file found at {config_args.default_yaml_config_file}. This can lead to unexpected errors" + ) + + +def check_user_token() -> bool: + """Checks for HF user info. Check is skipped if HF_HUB_OFFLINE=1. + + Returns: + Boolean indicating successful check (i.e., HF_HUB_OFFLINE=1 or HF user info is retrieved). + + Raises: + LocalTokenNotFoundError: If HF user info can't be retrieved. + """ + # Skip check if HF_HUB_OFFLINE is set to True + if os.getenv("HF_HUB_OFFLINE") == "1": + LOG.info( + "Skipping HuggingFace token verification because HF_HUB_OFFLINE is set to True. Only local files will be used." + ) + return True + + # Verify if token is valid + api = HfApi() + try: + user_info = api.whoami() + return bool(user_info) + except LocalTokenNotFoundError: + LOG.warning( + "Error verifying HuggingFace token. Remember to log in using `huggingface-cli login` and get your access token from https://huggingface.co/settings/tokens if you want to use gated models or datasets." + ) + return False diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py new file mode 100644 index 0000000000..166a676703 --- /dev/null +++ b/src/axolotl/cli/config.py @@ -0,0 +1,217 @@ +"""Configuration loading and processing.""" + +import json +import logging +import os +import tempfile +from pathlib import Path +from typing import Union +from urllib.parse import urlparse + +import requests +import torch +import yaml +from transformers.utils import is_torch_bf16_gpu_available + +from axolotl.integrations.base import PluginManager +from axolotl.utils.comet_ import setup_comet_env_vars +from axolotl.utils.config import ( + normalize_cfg_datasets, + normalize_config, + validate_config, +) +from axolotl.utils.dict import DictDefault +from axolotl.utils.mlflow_ import setup_mlflow_env_vars +from axolotl.utils.trainer import prepare_opinionated_env, prepare_optim_env +from axolotl.utils.wandb_ import setup_wandb_env_vars + +LOG = logging.getLogger(__name__) + + +def check_remote_config(config: Union[str, Path]) -> Union[str, Path]: + """ + First, determines if the passed config is a valid HTTPS URL. Then, attempts to query + for it and parse its content, first as JSON, then as YAML (YAML is preferred). + Finally, the parsed content is written to a local file and its path is returned. + + Args: + config: HTTPS URL to a YAML or JSON file. + + Returns: + Either the original `config` if it's not a valid HTTPS URL, or the path to the + downloaded remote config. + + Raises: + ValueError: If the remote configuration is neither valid JSON or YAML. + RuntimeError: If some request-related exception occurs from the file download. + Exception: Catch-all for any other exception. + """ + # Check if the config is a valid HTTPS URL to a .yml or .yaml file + if not (isinstance(config, str) and config.startswith("https://")): + return config # Return the original value if it's not a valid URL + + filename = os.path.basename(urlparse(config).path) + temp_dir = tempfile.mkdtemp() + + try: + response = requests.get(config, timeout=30) + response.raise_for_status() # Check for HTTP errors + + content = response.content + try: + # Try parsing as JSON first to catch cases where JSON content is mistakenly + # considered YAML. + json.loads(content) + + # Log a warning but do not raise an error; JSON is technically valid YAML. + # This can happen when you forget to point to a raw GitHub link. + LOG.warning( + f"Warning: The content of the file at {config} is JSON, which is technically valid YAML but might not be intended." + ) + except json.JSONDecodeError: + # If it's not valid JSON, verify it's valid YAML + try: + yaml.safe_load(content) + except yaml.YAMLError as err: + raise ValueError( + f"Failed to parse the content at {config} as YAML: {err}" + ) from err + + # Write the content to a file if it's valid YAML (or JSON treated as YAML) + output_path = Path(temp_dir) / filename + with open(output_path, "wb") as file: + file.write(content) + LOG.info( + f"Using the following config obtained from {config}: \n\n{content.decode('utf-8')}\n" + ) + return output_path + + except requests.RequestException as err: + # This catches all requests-related exceptions including HTTPError + raise RuntimeError(f"Failed to download {config}: {err}") from err + except Exception as err: + # Catch-all for any other exceptions + raise err + + +def choose_config(path: Path) -> str: + """ + Helper method for choosing a `axolotl` config YAML file (considering only files + ending with `.yml` or `.yaml`). If more than one config file exists in the passed + `path`, the user is prompted to choose one. + + Args: + path: Directory in which config file(s) are stored. + + Returns: + Path to either (1) the sole YAML file, or (2) if more than one YAML files exist, + the user-selected YAML file. + + Raises: + ValueError: If no YAML files are found in the given `path`. + """ + yaml_files = list(path.glob("*.yml")) + list(path.glob("*.yaml")) + + if not yaml_files: + raise ValueError( + "No YAML config files found in the specified directory. Are you using a .yml extension?" + ) + + if len(yaml_files) == 1: + print(f"Using default YAML file '{yaml_files[0]}'") + return str(yaml_files[0]) + + print("Choose a YAML file:") + for idx, file in enumerate(yaml_files): + print(f"{idx + 1}. {file}") + + chosen_file = None + while chosen_file is None: + try: + choice = int(input("Enter the number of your choice: ")) + if 1 <= choice <= len(yaml_files): + chosen_file = str(yaml_files[choice - 1]) + else: + print("Invalid choice. Please choose a number from the list.") + except ValueError: + print("Invalid input. Please enter a number.") + + return chosen_file + + +def prepare_plugins(cfg: DictDefault): + """ + Registers the plugins for the given configuration. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + """ + if cfg.get("plugins"): + plugin_manager = PluginManager.get_instance() + for plugin_name in cfg["plugins"]: + plugin_manager.register(plugin_name) + + +def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs) -> DictDefault: + """ + Loads the `axolotl` configuration stored at `config`, validates it, and performs + various setup. + + Args: + config: Path (local or remote) to `axolotl` config YAML file. + kwargs: Additional keyword arguments to override config file values. + + Returns: + `DictDefault` mapping configuration keys to values. + """ + config = check_remote_config(config) + if Path(config).is_dir(): + config = choose_config(Path(config)) + + # Load the config from the yaml file + with open(config, encoding="utf-8") as file: + cfg: DictDefault = DictDefault(yaml.safe_load(file)) + + # If there are any options passed in the cli, if it is something that seems valid + # from the yaml, then overwrite the value + cfg_keys = cfg.keys() + for k, _ in kwargs.items(): + # if not strict, allow writing to cfg even if it's not in the yml already + if k in cfg_keys or not cfg.strict: + # handle booleans + if isinstance(cfg[k], bool): + cfg[k] = bool(kwargs[k]) + else: + cfg[k] = kwargs[k] + + cfg.axolotl_config_path = config + + try: + device_props = torch.cuda.get_device_properties("cuda") + gpu_version = "sm_" + str(device_props.major) + str(device_props.minor) + except: # pylint: disable=bare-except # noqa: E722 + gpu_version = None + + prepare_plugins(cfg) + + cfg = validate_config( + cfg, + capabilities={ + "bf16": is_torch_bf16_gpu_available(), + "n_gpu": int(os.environ.get("WORLD_SIZE", 1)), + "compute_capability": gpu_version, + }, + env_capabilities={ + "torch_version": str(torch.__version__).split("+", maxsplit=1)[0] + }, + ) + + prepare_optim_env(cfg) + prepare_opinionated_env(cfg) + normalize_config(cfg) + normalize_cfg_datasets(cfg) + setup_wandb_env_vars(cfg) + setup_mlflow_env_vars(cfg) + setup_comet_env_vars(cfg) + + return cfg diff --git a/src/axolotl/cli/evaluate.py b/src/axolotl/cli/evaluate.py index 8e99d6f4b1..c89715719e 100644 --- a/src/axolotl/cli/evaluate.py +++ b/src/axolotl/cli/evaluate.py @@ -1,6 +1,5 @@ -""" -CLI to run training on a model -""" +"""CLI to run evaluation on a model.""" + import logging from pathlib import Path from typing import Union @@ -9,35 +8,48 @@ from dotenv import load_dotenv from transformers.hf_argparser import HfArgumentParser -from axolotl.cli import ( - check_accelerate_default_config, - check_user_token, - load_cfg, - load_datasets, - load_rl_datasets, - print_axolotl_text_art, -) -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.cli.art import print_axolotl_text_art +from axolotl.cli.checks import check_accelerate_default_config, check_user_token +from axolotl.cli.config import load_cfg +from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.evaluate import evaluate +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger(__name__) -LOG = logging.getLogger("axolotl.cli.evaluate") +def do_evaluate(cfg: DictDefault, cli_args: TrainerCliArgs) -> None: + """ + Evaluates a `transformers` model by first loading the dataset(s) specified in the + `axolotl` config, and then calling `axolotl.evaluate.evaluate`, which computes + evaluation metrics on the given dataset(s) and writes them to disk. -def do_evaluate(cfg, cli_args) -> None: + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + cli_args: CLI arguments. + """ # pylint: disable=duplicate-code print_axolotl_text_art() check_accelerate_default_config() check_user_token() - if cfg.rl: # and cfg.rl != "orpo": - dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + if cfg.rl: + dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) else: dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - evaluate(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + evaluate(cfg=cfg, dataset_meta=dataset_meta) def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: + """ + Parses `axolotl` config, CLI args, and calls `do_evaluate`. + + Args: + config: Path to `axolotl` config YAML file. + kwargs: Additional keyword arguments to override config file values. + """ # pylint: disable=duplicate-code parsed_cfg = load_cfg(config, **kwargs) parser = HfArgumentParser(TrainerCliArgs) diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index a5f1a8ad8b..e11a39bd62 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -1,32 +1,267 @@ -""" -CLI to run inference on a trained model -""" +"""CLI to run inference on a trained model.""" + +import importlib +import logging +import sys from pathlib import Path +from threading import Thread from typing import Union import fire +import torch import transformers from dotenv import load_dotenv +from transformers import GenerationConfig, TextIteratorStreamer, TextStreamer -from axolotl.cli import ( - do_inference, - do_inference_gradio, - load_cfg, - print_axolotl_text_art, +from axolotl.cli.args import InferenceCliArgs +from axolotl.cli.art import print_axolotl_text_art +from axolotl.cli.config import load_cfg +from axolotl.cli.utils import load_model_and_tokenizer +from axolotl.utils.chat_templates import ( + get_chat_template, + get_chat_template_from_config, ) -from axolotl.common.cli import TrainerCliArgs +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger(__name__) + + +def get_multi_line_input() -> str: + """ + Gets multi-line input from terminal. + + Returns: + Possibly multi-line, possibly empty stdin input as a string. + """ + print("Give me an instruction (Ctrl + D to submit): ") + + instruction = "" + for line in sys.stdin: + instruction += line # pylint: disable=consider-using-join + + return instruction + + +def do_inference( + *, + cfg: DictDefault, + cli_args: InferenceCliArgs, +): + """ + Runs inference on the command line in a loop. User input is accepted, a chat template + is (optionally) applied, and the model specified in the `axolotl` config is used to + generate completions according to a default generation config. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + cli_args: Inference-specific CLI arguments. + """ + model, tokenizer = load_model_and_tokenizer(cfg=cfg, inference=True) + prompter = cli_args.prompter + + prompter_module = None + chat_template_str = None + if prompter: + prompter_module = getattr( + importlib.import_module("axolotl.prompters"), prompter + ) + elif cfg.chat_template: + chat_template_str = get_chat_template(cfg.chat_template) + elif cfg.datasets[0].type == "chat_template": + chat_template_str = get_chat_template_from_config( + cfg=cfg, ds_cfg=cfg.datasets[0], tokenizer=tokenizer + ) + + model = model.to(cfg.device, dtype=cfg.torch_dtype) + + while True: + print("=" * 80) + # support for multiline inputs + instruction = get_multi_line_input() + if not instruction: + return + + if prompter_module: + prompt: str = next( + prompter_module().build_prompt(instruction=instruction.strip("\n")) + ) + else: + prompt = instruction.strip() + + if chat_template_str: + batch = tokenizer.apply_chat_template( + [ + { + "role": "user", + "content": prompt, + } + ], + return_tensors="pt", + add_special_tokens=True, + add_generation_prompt=True, + chat_template=chat_template_str, + tokenize=True, + return_dict=True, + ) + else: + batch = tokenizer(prompt, return_tensors="pt", add_special_tokens=True) + + print("=" * 40) + model.eval() + with torch.no_grad(): + generation_config = GenerationConfig( + repetition_penalty=1.1, + max_new_tokens=1024, + temperature=0.9, + top_p=0.95, + top_k=40, + bos_token_id=tokenizer.bos_token_id, + eos_token_id=tokenizer.eos_token_id, + pad_token_id=tokenizer.pad_token_id, + do_sample=True, + use_cache=True, + return_dict_in_generate=True, + output_attentions=False, + output_hidden_states=False, + output_scores=False, + ) + streamer = TextStreamer(tokenizer) + generated = model.generate( + inputs=batch["input_ids"].to(cfg.device), + generation_config=generation_config, + streamer=streamer, + ) + print("=" * 40) + print(tokenizer.decode(generated["sequences"].cpu().tolist()[0])) + + +def do_inference_gradio( + *, + cfg: DictDefault, + cli_args: InferenceCliArgs, +): + """ + Runs inference in a Gradio interface. User input is accepted, a chat template is + (optionally) applied, and the model specified in the `axolotl` config is used to + generate completions according to a default generation config. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + cli_args: Inference-specific CLI arguments. + """ + import gradio as gr + + model, tokenizer = load_model_and_tokenizer(cfg=cfg, inference=True) + prompter = cli_args.prompter + + prompter_module = None + chat_template_str = None + if prompter: + prompter_module = getattr( + importlib.import_module("axolotl.prompters"), prompter + ) + elif cfg.chat_template: + chat_template_str = get_chat_template(cfg.chat_template, tokenizer=tokenizer) + + model = model.to(cfg.device, dtype=cfg.torch_dtype) + + def generate(instruction): + if not instruction: + return + if prompter_module: + # pylint: disable=stop-iteration-return + prompt: str = next( + prompter_module().build_prompt(instruction=instruction.strip("\n")) + ) + else: + prompt = instruction.strip() + + if chat_template_str: + batch = tokenizer.apply_chat_template( + [ + { + "role": "user", + "content": prompt, + } + ], + return_tensors="pt", + add_special_tokens=True, + add_generation_prompt=True, + chat_template=chat_template_str, + tokenize=True, + return_dict=True, + ) + else: + batch = tokenizer(prompt, return_tensors="pt", add_special_tokens=True) + + model.eval() + with torch.no_grad(): + generation_config = GenerationConfig( + repetition_penalty=1.1, + max_new_tokens=cfg.get("gradio_max_new_tokens", 1024), + temperature=cfg.get("gradio_temperature", 0.9), + top_p=0.95, + top_k=40, + bos_token_id=tokenizer.bos_token_id, + eos_token_id=tokenizer.eos_token_id, + pad_token_id=tokenizer.pad_token_id, + do_sample=True, + use_cache=True, + return_dict_in_generate=True, + output_attentions=False, + output_hidden_states=False, + output_scores=False, + ) + streamer = TextIteratorStreamer(tokenizer) + generation_kwargs = { + "inputs": batch["input_ids"].to(cfg.device), + "attention_mask": batch["attention_mask"].to(cfg.device), + "generation_config": generation_config, + "streamer": streamer, + } + + thread = Thread(target=model.generate, kwargs=generation_kwargs) + thread.start() + + all_text = "" + + for new_text in streamer: + all_text += new_text + yield all_text + + demo = gr.Interface( + fn=generate, + inputs="textbox", + outputs="text", + title=cfg.get("gradio_title", "Axolotl Gradio Interface"), + ) + + demo.queue().launch( + show_api=False, + share=cfg.get("gradio_share", True), + server_name=cfg.get("gradio_server_name", "127.0.0.1"), + server_port=cfg.get("gradio_server_port", None), + ) + +def do_cli( + config: Union[Path, str] = Path("examples/"), gradio: bool = False, **kwargs +) -> None: + """ + Parses axolotl config, CLI args, and calls `do_inference` or `do_inference_gradio`. -def do_cli(config: Union[Path, str] = Path("examples/"), gradio=False, **kwargs): + Args: + config: Path to `axolotl` config YAML file. + kwargs: Additional keyword arguments to override config file values. + """ # pylint: disable=duplicate-code print_axolotl_text_art() parsed_cfg = load_cfg(config, inference=True, **kwargs) parsed_cfg.sample_packing = False - parser = transformers.HfArgumentParser((TrainerCliArgs)) + parser = transformers.HfArgumentParser(InferenceCliArgs) parsed_cli_args, _ = parser.parse_args_into_dataclasses( return_remaining_strings=True ) - parsed_cli_args.inference = True if gradio: do_inference_gradio(cfg=parsed_cfg, cli_args=parsed_cli_args) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index 14803e43ba..43e2de3db6 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -1,18 +1,20 @@ -"""CLI definition for various axolotl commands.""" +"""Click CLI definitions for various axolotl commands.""" # pylint: disable=redefined-outer-name + import subprocess # nosec B404 from typing import Optional import click import axolotl +from axolotl.cli.args import EvaluateCliArgs, PreprocessCliArgs, TrainerCliArgs from axolotl.cli.utils import ( add_options_from_config, add_options_from_dataclass, build_command, fetch_from_github, + filter_none_kwargs, ) -from axolotl.common.cli import EvaluateCliArgs, PreprocessCliArgs, TrainerCliArgs from axolotl.utils import set_pytorch_cuda_alloc_conf from axolotl.utils.config.models.input.v0_4_1 import AxolotlInputConfig @@ -27,10 +29,16 @@ def cli(): @click.argument("config", type=click.Path(exists=True, path_type=str)) @add_options_from_dataclass(PreprocessCliArgs) @add_options_from_config(AxolotlInputConfig) -def preprocess(config: str, **kwargs): - """Preprocess datasets before training.""" - kwargs = {k: v for k, v in kwargs.items() if v is not None} +@filter_none_kwargs +def preprocess(config: str, **kwargs) -> None: + """ + Preprocess datasets before training. + Args: + config: Path to `axolotl` config YAML file. + kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` + config options. + """ from axolotl.cli.preprocess import do_cli do_cli(config=config, **kwargs) @@ -45,10 +53,17 @@ def preprocess(config: str, **kwargs): ) @add_options_from_dataclass(TrainerCliArgs) @add_options_from_config(AxolotlInputConfig) -def train(config: str, accelerate: bool, **kwargs): - """Train or fine-tune a model.""" - kwargs = {k: v for k, v in kwargs.items() if v is not None} +@filter_none_kwargs +def train(config: str, accelerate: bool, **kwargs) -> None: + """ + Train or fine-tune a model. + Args: + config: Path to `axolotl` config YAML file. + accelerate: Whether to use `accelerate` launcher. + kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` + config options. + """ # Enable expandable segments for cuda allocation to improve VRAM usage set_pytorch_cuda_alloc_conf() @@ -73,10 +88,17 @@ def train(config: str, accelerate: bool, **kwargs): ) @add_options_from_dataclass(EvaluateCliArgs) @add_options_from_config(AxolotlInputConfig) -def evaluate(config: str, accelerate: bool, **kwargs): - """Evaluate a model.""" - kwargs = {k: v for k, v in kwargs.items() if v is not None} +@filter_none_kwargs +def evaluate(config: str, accelerate: bool, **kwargs) -> None: + """ + Evaluate a model. + Args: + config: Path to `axolotl` config YAML file. + accelerate: Whether to use `accelerate` launcher. + kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` + config options. + """ if accelerate: base_cmd = ["accelerate", "launch", "-m", "axolotl.cli.evaluate"] if config: @@ -96,81 +118,33 @@ def evaluate(config: str, accelerate: bool, **kwargs): default=False, help="Use accelerate launch for multi-GPU inference", ) -@click.option( - "--lora-model-dir", - type=click.Path(exists=True, path_type=str), - help="Directory containing LoRA model", -) -@click.option( - "--base-model", - type=click.Path(exists=True, path_type=str), - help="Path to base model for non-LoRA models", -) @click.option("--gradio", is_flag=True, help="Launch Gradio interface") -@click.option("--load-in-8bit", is_flag=True, help="Load model in 8-bit mode") @add_options_from_dataclass(TrainerCliArgs) @add_options_from_config(AxolotlInputConfig) -def inference( - config: str, - accelerate: bool, - lora_model_dir: Optional[str] = None, - base_model: Optional[str] = None, - **kwargs, -): - """Run inference with a trained model.""" - kwargs = {k: v for k, v in kwargs.items() if v is not None} - del kwargs["inference"] # interferes with inference.do_cli - - if lora_model_dir: - kwargs["lora_model_dir"] = lora_model_dir - if base_model: - kwargs["base_model"] = base_model - +@filter_none_kwargs +def inference(config: str, accelerate: bool, gradio: bool, **kwargs) -> None: + """ + Run inference with a trained model. + + Args: + config: Path to `axolotl` config YAML file. + accelerate: Whether to use `accelerate` launcher. + gradio: Whether to use Gradio browser interface or command line for inference. + kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` + config options. + """ if accelerate: base_cmd = ["accelerate", "launch", "-m", "axolotl.cli.inference"] if config: base_cmd.append(config) + if gradio: + base_cmd.append("--gradio") cmd = build_command(base_cmd, kwargs) subprocess.run(cmd, check=True) # nosec B603 else: from axolotl.cli.inference import do_cli - do_cli(config=config, **kwargs) - - -@cli.command() -@click.argument("config", type=click.Path(exists=True, path_type=str)) -@click.option( - "--accelerate/--no-accelerate", - default=False, - help="Use accelerate launch for multi-GPU operations", -) -@click.option( - "--model-dir", - type=click.Path(exists=True, path_type=str), - help="Directory containing model weights to shard", -) -@click.option( - "--save-dir", - type=click.Path(path_type=str), - help="Directory to save sharded weights", -) -@add_options_from_dataclass(TrainerCliArgs) -@add_options_from_config(AxolotlInputConfig) -def shard(config: str, accelerate: bool, **kwargs): - """Shard model weights.""" - kwargs = {k: v for k, v in kwargs.items() if v is not None} - - if accelerate: - base_cmd = ["accelerate", "launch", "-m", "axolotl.cli.shard"] - if config: - base_cmd.append(config) - cmd = build_command(base_cmd, kwargs) - subprocess.run(cmd, check=True) # nosec B603 - else: - from axolotl.cli.shard import do_cli - - do_cli(config=config, **kwargs) + do_cli(config=config, gradio=gradio, **kwargs) @cli.command() @@ -180,20 +154,19 @@ def shard(config: str, accelerate: bool, **kwargs): default=True, help="Use accelerate launch for weight merging", ) -@click.option( - "--model-dir", - type=click.Path(exists=True, path_type=str), - help="Directory containing sharded weights", -) -@click.option( - "--save-path", type=click.Path(path_type=str), help="Path to save merged weights" -) @add_options_from_dataclass(TrainerCliArgs) @add_options_from_config(AxolotlInputConfig) -def merge_sharded_fsdp_weights(config: str, accelerate: bool, **kwargs): - """Merge sharded FSDP model weights.""" - kwargs = {k: v for k, v in kwargs.items() if v is not None} +@filter_none_kwargs +def merge_sharded_fsdp_weights(config: str, accelerate: bool, **kwargs) -> None: + """ + Merge sharded FSDP model weights. + Args: + config: Path to `axolotl` config YAML file. + accelerate: Whether to use `accelerate` launcher. + kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` + config options. + """ if accelerate: base_cmd = [ "accelerate", @@ -213,28 +186,19 @@ def merge_sharded_fsdp_weights(config: str, accelerate: bool, **kwargs): @cli.command() @click.argument("config", type=click.Path(exists=True, path_type=str)) -@click.option( - "--lora-model-dir", - type=click.Path(exists=True, path_type=str), - help="Directory containing the LoRA model to merge", -) -@click.option( - "--output-dir", - type=click.Path(path_type=str), - help="Directory to save the merged model", -) -def merge_lora( - config: str, - lora_model_dir: Optional[str] = None, - output_dir: Optional[str] = None, -): - """Merge a trained LoRA into a base model""" - kwargs = {} - if lora_model_dir: - kwargs["lora_model_dir"] = lora_model_dir - if output_dir: - kwargs["output_dir"] = output_dir +@add_options_from_dataclass(TrainerCliArgs) +@add_options_from_config(AxolotlInputConfig) +@filter_none_kwargs +def merge_lora(config: str, **kwargs) -> None: + """ + Merge trained LoRA adapters into a base model. + Args: + config: Path to `axolotl` config YAML file. + accelerate: Whether to use `accelerate` launcher. + kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` + config options. + """ from axolotl.cli.merge_lora import do_cli do_cli(config=config, **kwargs) @@ -243,13 +207,17 @@ def merge_lora( @cli.command() @click.argument("directory", type=click.Choice(["examples", "deepspeed_configs"])) @click.option("--dest", help="Destination directory") -def fetch(directory: str, dest: Optional[str]): +def fetch(directory: str, dest: Optional[str]) -> None: """ Fetch example configs or other resources. Available directories: - examples: Example configuration files - deepspeed_configs: DeepSpeed configuration files + + Args: + directory: One of `examples`, `deepspeed_configs`. + dest: Optional destination directory. """ fetch_from_github(f"{directory}/", dest) diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 8c321bc48e..595eb3eab7 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -1,6 +1,6 @@ -""" -CLI to run merge a trained LoRA into a base model -""" +"""CLI to merge a trained LoRA into a base model.""" + +import logging from pathlib import Path from typing import Union @@ -8,14 +8,58 @@ import transformers from dotenv import load_dotenv -from axolotl.cli import do_merge_lora, load_cfg, print_axolotl_text_art -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.cli.art import print_axolotl_text_art +from axolotl.cli.config import load_cfg +from axolotl.cli.utils import load_model_and_tokenizer +from axolotl.utils.dict import DictDefault +LOG = logging.getLogger(__name__) -def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): - # pylint: disable=duplicate-code + +def do_merge_lora(*, cfg: DictDefault) -> None: + """ + Calls `transformers`' `merge_and_unload` on the model given in the `axolotl` config + along with the LoRA adapters to combine them into a single base model. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + """ print_axolotl_text_art() - parser = transformers.HfArgumentParser((TrainerCliArgs)) + + model, tokenizer = load_model_and_tokenizer(cfg=cfg) + safe_serialization = cfg.save_safetensors is True + + LOG.info("Running merge of LoRA with base model...") + model = model.merge_and_unload(progressbar=True) + model.to(dtype=cfg.torch_dtype) + model.generation_config.do_sample = True + + if cfg.local_rank == 0: + LOG.info(f"Saving merged model to: {str(Path(cfg.output_dir) / 'merged')}...") + model.save_pretrained( + str(Path(cfg.output_dir) / "merged"), + safe_serialization=safe_serialization, + progressbar=True, + ) + tokenizer.save_pretrained(str(Path(cfg.output_dir) / "merged")) + + +def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: + """ + Parses `axolotl` config, CLI args, and calls `do_merge_lora`. Note that various + config values will be overwritten to allow the LoRA merge logic to work as expected + (`load_in_8bit=False`, `load_in4bit=False`, `flash_attention=False`, etc.). + + Args: + config: Path to `axolotl` config YAML file. + kwargs: Additional keyword arguments to override config file values. + + Raises: + ValueError: If target directory for LoRA merged model does not exist. + """ + # pylint: disable=duplicate-code + parser = transformers.HfArgumentParser(TrainerCliArgs) parsed_cli_args, _ = parser.parse_args_into_dataclasses( return_remaining_strings=True ) @@ -46,7 +90,7 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): parsed_cfg.fsdp = None parsed_cfg.fsdp_config = None - do_merge_lora(cfg=parsed_cfg, cli_args=parsed_cli_args) + do_merge_lora(cfg=parsed_cfg) if __name__ == "__main__": diff --git a/src/axolotl/cli/merge_sharded_fsdp_weights.py b/src/axolotl/cli/merge_sharded_fsdp_weights.py index 6be9af1f76..d4b36d92c6 100644 --- a/src/axolotl/cli/merge_sharded_fsdp_weights.py +++ b/src/axolotl/cli/merge_sharded_fsdp_weights.py @@ -1,6 +1,5 @@ -""" -This module provides a CLI to merge sharded FSDP model checkpoints into a single combined checkpoint -""" +"""CLI to merge sharded FSDP model checkpoints into a single combined checkpoint.""" + import json import logging import os @@ -25,16 +24,15 @@ from safetensors.torch import save_file as safe_save_file from torch.distributed.checkpoint.format_utils import _EmptyStateDictLoadPlanner -from axolotl.cli import load_cfg, print_axolotl_text_art -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.cli.art import print_axolotl_text_art +from axolotl.cli.config import load_cfg -LOG = logging.getLogger("axolotl.cli.merge_sharded_fsdp_weights") +LOG = logging.getLogger(__name__) class BFloat16CastPlanner(_EmptyStateDictLoadPlanner): - """ - A custom planner to cast tensors to bfloat16 on the fly during loading. - """ + """A custom planner to cast tensors to bfloat16 on the fly during loading.""" def commit_tensor(self, read_item, tensor): # pylint: disable=unused-argument tensor.copy_(tensor.to(torch.bfloat16)) @@ -45,11 +43,19 @@ def _distributed_checkpoint_to_merged_weights( save_path: str, safe_serialization: bool = False, max_shard_size: str = "5GB", -): +) -> Path: """ - Passthrough to `torch.distributed.checkpoint.format_utils.dcp_to_torch_save` + Passthrough to `torch.distributed.checkpoint.format_utils.dcp_to_torch_save`. Will + save under `save_path` as either `model.safetensors` or `pytorch_model.bin`. - Will save under `save_path` as either `model.safetensors` or `pytorch_model.bin`. + Args: + checkpoint_dir: Directory where distributed checkpoint is saved. + save_path: Path to save model to. + safe_serialization: Whether to save in safetensors format. + max_shard_size: Max size of model shards to save. + + Returns: + Path where model is saved. """ state_dict: Dict = {} @@ -79,6 +85,7 @@ def _distributed_checkpoint_to_merged_weights( state_dict_split = split_torch_state_dict_into_shards( state_dict, filename_pattern=filename_pattern, max_shard_size=max_shard_size ) + # Save index if sharded index = None if state_dict_split.is_sharded: @@ -135,6 +142,9 @@ def merge_fsdp_weights( Whether to save the merged weights with safetensors (recommended). remove_checkpoint_dir (`bool`, *optional*, defaults to `False`): Whether to remove the checkpoint directory after merging. + + Raises: + ValueError: If torch version < 2.3.0, or if `checkpoint_dir` does not exist. """ checkpoint_dir_ = Path(checkpoint_dir) from accelerate.state import PartialState @@ -178,18 +188,21 @@ def merge_fsdp_weights( def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): + """ + Parses `axolotl` config, CLI args, and calls `merge_fsdp_weights`. + + Args: + config: Path to `axolotl` config YAML file. + kwargs: Additional keyword arguments to override config file values. + """ # pylint: disable=duplicate-code print_axolotl_text_art() - parser = transformers.HfArgumentParser((TrainerCliArgs)) + parser = transformers.HfArgumentParser(TrainerCliArgs) parsed_cli_args, _ = parser.parse_args_into_dataclasses( return_remaining_strings=True ) parsed_cli_args.merge_lora = True - - parsed_cfg = load_cfg( - config, - **kwargs, - ) + parsed_cfg = load_cfg(config, **kwargs) fsdp_dir = Path(parsed_cfg.output_dir) / "pytorch_model_fsdp_0" merge_fsdp_weights( diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index a1592aa785..760fe76fae 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -1,6 +1,5 @@ -""" -CLI to run training on a model -""" +"""CLI to run preprocessing of a dataset.""" + import logging import warnings from pathlib import Path @@ -13,34 +12,31 @@ from dotenv import load_dotenv from transformers import AutoModelForCausalLM -from axolotl.cli import ( - check_accelerate_default_config, - check_user_token, - load_cfg, - load_datasets, - load_rl_datasets, - print_axolotl_text_art, -) -from axolotl.common.cli import PreprocessCliArgs +from axolotl.cli.args import PreprocessCliArgs +from axolotl.cli.art import print_axolotl_text_art +from axolotl.cli.checks import check_accelerate_default_config, check_user_token +from axolotl.cli.config import load_cfg from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH +from axolotl.common.datasets import load_datasets, load_preference_datasets +from axolotl.utils.dict import DictDefault from axolotl.utils.trainer import disable_datasets_caching -LOG = logging.getLogger("axolotl.cli.preprocess") +LOG = logging.getLogger(__name__) -def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): - # pylint: disable=duplicate-code +def do_preprocess(cfg: DictDefault, cli_args: PreprocessCliArgs) -> None: + """ + Preprocesses dataset specified in axolotl config. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + cli_args: Preprocessing-specific CLI arguments. + """ print_axolotl_text_art() - parsed_cfg = load_cfg(config, **kwargs) - parsed_cfg.is_preprocess = True check_accelerate_default_config() check_user_token() - parser = transformers.HfArgumentParser((PreprocessCliArgs)) - parsed_cli_args, _ = parser.parse_args_into_dataclasses( - return_remaining_strings=True - ) - if not parsed_cfg.dataset_prepared_path: + if not cfg.dataset_prepared_path: msg = ( Fore.RED + "preprocess CLI called without dataset_prepared_path set, " @@ -48,16 +44,16 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): + Fore.RESET ) LOG.warning(msg) - parsed_cfg.dataset_prepared_path = DEFAULT_DATASET_PREPARED_PATH + cfg.dataset_prepared_path = DEFAULT_DATASET_PREPARED_PATH with disable_datasets_caching(): - if parsed_cfg.rl: # and parsed_cfg.rl != "orpo": - load_rl_datasets(cfg=parsed_cfg, cli_args=parsed_cli_args) + if cfg.rl: + load_preference_datasets(cfg=cfg, cli_args=cli_args) else: - load_datasets(cfg=parsed_cfg, cli_args=parsed_cli_args) + load_datasets(cfg=cfg, cli_args=cli_args) - if parsed_cli_args.download: - model_name = parsed_cfg.base_model + if cli_args.download: + model_name = cfg.base_model with warnings.catch_warnings(): # there are a bunch of useless UserWarnings about # "copying from a non-meta parameter in the checkpoint to a meta parameter in the current model" @@ -74,11 +70,30 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): LOG.info( Fore.GREEN - + f"Success! Preprocessed data path: `dataset_prepared_path: {parsed_cfg.dataset_prepared_path}`" + + f"Success! Preprocessed data path: `dataset_prepared_path: {cfg.dataset_prepared_path}`" + Fore.RESET ) +def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: + """ + Parses `axolotl` config, CLI args, and calls `do_preprocess`. + + Args: + config: Path to `axolotl` config YAML file. + kwargs: Additional keyword arguments to override config file values. + """ + # pylint: disable=duplicate-code + parsed_cfg = load_cfg(config, **kwargs) + parsed_cfg.is_preprocess = True + parser = transformers.HfArgumentParser(PreprocessCliArgs) + parsed_cli_args, _ = parser.parse_args_into_dataclasses( + return_remaining_strings=True + ) + + do_preprocess(parsed_cfg, parsed_cli_args) + + if __name__ == "__main__": load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/shard.py b/src/axolotl/cli/shard.py deleted file mode 100644 index 196c0e99a6..0000000000 --- a/src/axolotl/cli/shard.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -CLI to shard a trained model into 10GiB chunks -""" -import logging -from pathlib import Path -from typing import Union - -import fire -import transformers -from dotenv import load_dotenv - -from axolotl.cli import load_cfg, print_axolotl_text_art -from axolotl.common.cli import TrainerCliArgs, load_model_and_tokenizer -from axolotl.utils.dict import DictDefault - -LOG = logging.getLogger("axolotl.scripts") - - -def shard( - *, - cfg: DictDefault, - cli_args: TrainerCliArgs, -): - model, _ = load_model_and_tokenizer(cfg=cfg, cli_args=cli_args) - safe_serialization = cfg.save_safetensors is True - LOG.debug("Re-saving model w/ sharding") - model.save_pretrained(cfg.output_dir, safe_serialization=safe_serialization) - - -def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): - # pylint: disable=duplicate-code - print_axolotl_text_art() - parsed_cfg = load_cfg(config, **kwargs) - parser = transformers.HfArgumentParser((TrainerCliArgs)) - parsed_cli_args, _ = parser.parse_args_into_dataclasses( - return_remaining_strings=True - ) - parsed_cli_args.shard = True - - shard(cfg=parsed_cfg, cli_args=parsed_cli_args) - - -if __name__ == "__main__": - load_dotenv() - fire.Fire(do_cli) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 2a40e854ee..9e3ae1cc38 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -1,6 +1,5 @@ -""" -CLI to run training on a model -""" +"""CLI to run training on a model.""" + import logging from pathlib import Path from typing import Union @@ -9,42 +8,38 @@ from dotenv import load_dotenv from transformers.hf_argparser import HfArgumentParser -from axolotl.cli import ( - check_accelerate_default_config, - check_user_token, - load_cfg, - load_datasets, - load_rl_datasets, - print_axolotl_text_art, -) -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.cli.art import print_axolotl_text_art +from axolotl.cli.checks import check_accelerate_default_config, check_user_token +from axolotl.cli.config import load_cfg +from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.integrations.base import PluginManager from axolotl.train import train +from axolotl.utils.dict import DictDefault -LOG = logging.getLogger("axolotl.cli.train") - +LOG = logging.getLogger(__name__) -def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): - # pylint: disable=duplicate-code - parsed_cfg = load_cfg(config, **kwargs) - parser = HfArgumentParser((TrainerCliArgs)) - parsed_cli_args, _ = parser.parse_args_into_dataclasses( - return_remaining_strings=True - ) - return do_train(parsed_cfg, parsed_cli_args) +def do_train(cfg: DictDefault, cli_args: TrainerCliArgs) -> None: + """ + Trains a `transformers` model by first loading the dataset(s) specified in the + `axolotl` config, and then calling `axolotl.train.train`. Also runs the plugin + manager's `post_train_unload` once training completes. -def do_train(cfg, cli_args) -> None: + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + cli_args: Training-specific CLI arguments. + """ print_axolotl_text_art() check_accelerate_default_config() check_user_token() - if cfg.rl: # and cfg.rl != "orpo": - dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + if cfg.rl: + dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) else: dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - model, tokenizer = train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + model, tokenizer = train(cfg=cfg, dataset_meta=dataset_meta) plugin_manager = PluginManager.get_instance() del model @@ -53,6 +48,24 @@ def do_train(cfg, cli_args) -> None: plugin_manager.post_train_unload(cfg) +def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: + """ + Parses `axolotl` config, CLI args, and calls `do_train`. + + Args: + config: Path to `axolotl` config YAML file. + kwargs: Additional keyword arguments to override config file values. + """ + # pylint: disable=duplicate-code + parsed_cfg = load_cfg(config, **kwargs) + parser = HfArgumentParser(TrainerCliArgs) + parsed_cli_args, _ = parser.parse_args_into_dataclasses( + return_remaining_strings=True + ) + + do_train(parsed_cfg, parsed_cli_args) + + if __name__ == "__main__": load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/utils.py b/src/axolotl/cli/utils.py index 85d241b5d0..addfa0ab9c 100644 --- a/src/axolotl/cli/utils.py +++ b/src/axolotl/cli/utils.py @@ -1,32 +1,85 @@ -"""Utility methods for axoltl CLI.""" +"""Utility methods for axolotl CLI.""" + import concurrent.futures import dataclasses import hashlib import json import logging +import typing +from functools import wraps from pathlib import Path from types import NoneType -from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_args, get_origin +from typing import Any, Callable, Type, Union, get_args, get_origin import click import requests from pydantic import BaseModel +from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast + +from axolotl.logging_config import configure_logging +from axolotl.utils.dict import DictDefault +from axolotl.utils.models import load_model, load_tokenizer + +configure_logging() +LOG = logging.getLogger(__name__) + + +def strip_optional_type(field_type: type | typing._SpecialForm | None): + """ + Extracts the non-`None` type from an `Optional` / `Union` type. + + Args: + field_type: Type of field for Axolotl CLI command. + + Returns: + If the input type is `Union[T, None]` or `Optional[T]`, returns `T`. Otherwise + returns the input type unchanged. + """ + if get_origin(field_type) is Union and type(None) in get_args(field_type): + field_type = next( + t for t in get_args(field_type) if not isinstance(t, NoneType) + ) -LOG = logging.getLogger("axolotl.cli.utils") + return field_type -def add_options_from_dataclass(config_class: Type[Any]): - """Create Click options from the fields of a dataclass.""" +def filter_none_kwargs(func: Callable) -> Callable: + """ + Wraps function to remove `None`-valued `kwargs`. + + Args: + func: Function to wrap. + + Returns: + Wrapped function. + """ + + @wraps(func) + def wrapper(*args, **kwargs) -> Callable: + """Filters out `None`-valued `kwargs`.""" + filtered_kwargs = {k: v for k, v in kwargs.items() if v is not None} + + return func(*args, **filtered_kwargs) + + return wrapper + + +def add_options_from_dataclass(config_class: Type[Any]) -> Callable: + """ + Create Click options from the fields of a dataclass. + + Args: + config_class: Dataclass with fields to parse from the CLI. + + Returns: + Function decorator for Axolotl CLI command. + """ - def decorator(function): + def decorator(function: Callable) -> Callable: # Process dataclass fields in reverse order for correct option ordering for field in reversed(dataclasses.fields(config_class)): - field_type = field.type + field_type = strip_optional_type(field.type) - if get_origin(field_type) is Union and type(None) in get_args(field_type): - field_type = next( - t for t in get_args(field_type) if not isinstance(t, NoneType) - ) if field_type == bool: field_name = field.name.replace("_", "-") option_name = f"--{field_name}/--no-{field_name}" @@ -43,18 +96,29 @@ def decorator(function): default=field.default, help=field.metadata.get("description"), )(function) + return function return decorator -def add_options_from_config(config_class: Type[BaseModel]): - """Create Click options from the fields of a Pydantic model.""" +def add_options_from_config(config_class: Type[BaseModel]) -> Callable: + """ + Create Click options from the fields of a Pydantic model. + + Args: + config_class: PyDantic model with fields to parse from the CLI + + Returns: + Function decorator for Axolotl CLI command. + """ - def decorator(function): + def decorator(function: Callable) -> Callable: # Process model fields in reverse order for correct option ordering for name, field in reversed(config_class.model_fields.items()): - if field.annotation == bool: + field_type = strip_optional_type(field.annotation) + + if field_type == bool: field_name = name.replace("_", "-") option_name = f"--{field_name}/--no-{field_name}" function = click.option( @@ -65,13 +129,23 @@ def decorator(function): function = click.option( option_name, default=None, help=field.description )(function) + return function return decorator -def build_command(base_cmd: List[str], options: Dict[str, Any]) -> List[str]: - """Build command list from base command and options.""" +def build_command(base_cmd: list[str], options: dict[str, Any]) -> list[str]: + """ + Build command list from base command and options. + + Args: + base_cmd: Command without options. + options: Options to parse and append to base command. + + Returns: + List of strings giving shell command. + """ cmd = base_cmd.copy() for key, value in options.items(): @@ -91,18 +165,18 @@ def build_command(base_cmd: List[str], options: Dict[str, Any]) -> List[str]: def download_file( file_info: tuple, raw_base_url: str, dest_path: Path, dir_prefix: str -) -> Tuple[str, str]: +) -> tuple[str, str]: """ Download a single file and return its processing status. Args: - file_info: Tuple of (file_path, remote_sha) - raw_base_url: Base URL for raw GitHub content - dest_path: Local destination directory - dir_prefix: Directory prefix to filter files + file_info: Tuple of (file_path, remote_sha). + raw_base_url: Base URL for raw GitHub content. + dest_path: Local destination directory. + dir_prefix: Directory prefix to filter files. Returns: - Tuple of (file_path, status) where status is 'new', 'updated', or 'unchanged' + Tuple of (file_path, status) where status is 'new', 'updated', or 'unchanged'. """ file_path, remote_sha = file_info raw_url = f"{raw_base_url}/{file_path}" @@ -144,16 +218,17 @@ def download_file( def fetch_from_github( - dir_prefix: str, dest_dir: Optional[str] = None, max_workers: int = 5 + dir_prefix: str, dest_dir: str | None = None, max_workers: int = 5 ) -> None: """ Sync files from a specific directory in the GitHub repository. Only downloads files that don't exist locally or have changed. Args: - dir_prefix: Directory prefix to filter files (e.g., 'examples/', 'deepspeed_configs/') - dest_dir: Local destination directory - max_workers: Maximum number of concurrent downloads + dir_prefix: Directory prefix to filter files (e.g., 'examples/', + 'deepspeed_configs/'). + dest_dir: Local destination directory. + max_workers: Maximum number of concurrent downloads. """ api_url = "https://api.github.com/repos/axolotl-ai-cloud/axolotl/git/trees/main?recursive=1" raw_base_url = "https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main" @@ -178,7 +253,7 @@ def fetch_from_github( dest_path = Path(dest_dir) if dest_dir else default_dest # Keep track of processed files for summary - files_processed: Dict[str, List[str]] = { + files_processed: dict[str, list[str]] = { "new": [], "updated": [], "unchanged": [], @@ -215,3 +290,28 @@ def fetch_from_github( LOG.info(f"Unchanged files: {len(files_processed['unchanged'])}") if files_processed["error"]: LOG.info(f"Failed files: {len(files_processed['error'])}") + + +def load_model_and_tokenizer( + *, + cfg: DictDefault, + inference: bool = False, +) -> tuple[PreTrainedModel, PreTrainedTokenizer | PreTrainedTokenizerFast | Any]: + """ + Helper function for loading a model and tokenizer specified in the given `axolotl` + config. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + inference: Boolean denoting inference mode. + + Returns: + `transformers` model and tokenizer. + """ + LOG.info(f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}") + tokenizer = load_tokenizer(cfg) + + LOG.info("loading model...") + model, _ = load_model(cfg, tokenizer, inference=inference) + + return model, tokenizer diff --git a/src/axolotl/common/cli.py b/src/axolotl/common/cli.py deleted file mode 100644 index 02ad9201b8..0000000000 --- a/src/axolotl/common/cli.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -shared module for cli specific things -""" - -import logging -from dataclasses import dataclass, field -from typing import Optional - -import axolotl.monkeypatch.data.batch_dataset_fetcher # pylint: disable=unused-import # noqa: F401 -from axolotl.logging_config import configure_logging -from axolotl.utils.dict import DictDefault -from axolotl.utils.models import load_model, load_tokenizer - -configure_logging() -LOG = logging.getLogger("axolotl.common.cli") - - -@dataclass -class PreprocessCliArgs: - """ - dataclass representing arguments for preprocessing only - """ - - debug: bool = field(default=False) - debug_text_only: bool = field(default=False) - debug_num_examples: int = field(default=1) - prompter: Optional[str] = field(default=None) - download: Optional[bool] = field(default=True) - - -@dataclass -class TrainerCliArgs: - """ - dataclass representing the various non-training arguments - """ - - debug: bool = field(default=False) - debug_text_only: bool = field(default=False) - debug_num_examples: int = field(default=0) - inference: bool = field(default=False) - merge_lora: bool = field(default=False) - prompter: Optional[str] = field(default=None) - shard: bool = field(default=False) - - -@dataclass -class EvaluateCliArgs: - """ - dataclass representing the various evaluation arguments - """ - - debug: bool = field(default=False) - debug_text_only: bool = field(default=False) - debug_num_examples: int = field(default=0) - - -def load_model_and_tokenizer( - *, - cfg: DictDefault, - cli_args: TrainerCliArgs, -): - LOG.info(f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}") - tokenizer = load_tokenizer(cfg) - - LOG.info("loading model and (optionally) peft_config...") - inference = getattr(cli_args, "inference", False) - model, _ = load_model(cfg, tokenizer, inference=inference) - - return model, tokenizer diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py new file mode 100644 index 0000000000..d07add29b7 --- /dev/null +++ b/src/axolotl/common/datasets.py @@ -0,0 +1,140 @@ +"""Dataset loading utilities.""" + +import logging +import math +import random +from dataclasses import dataclass +from typing import Optional, Union + +from datasets import Dataset + +import axolotl.monkeypatch.data.batch_dataset_fetcher # pylint: disable=unused-import # noqa: F401 +from axolotl.cli.args import PreprocessCliArgs, TrainerCliArgs +from axolotl.utils.data import prepare_dataset +from axolotl.utils.data.rl import load_prepare_dpo_datasets +from axolotl.utils.dict import DictDefault +from axolotl.utils.models import load_processor, load_tokenizer +from axolotl.utils.tokenization import check_dataset_labels + +LOG = logging.getLogger(__name__) + + +@dataclass +class TrainDatasetMeta: + """Dataclass with fields for training and validation datasets and metadata.""" + + train_dataset: Dataset + eval_dataset: Optional[Dataset] = None + total_num_steps: Optional[int] = None + + +def sample_dataset(dataset: Dataset, num_samples: int) -> Dataset: + """ + Randomly sample `num_samples` samples from `dataset`. + + Args: + dataset: Dataset. + num_samples: Number of samples to return. + + Returns: + Random sample (with replacement) of examples in `dataset`. + """ + return dataset.select( + [random.randrange(0, len(dataset) - 1) for _ in range(num_samples)] # nosec + ) + + +def load_datasets( + *, + cfg: DictDefault, + cli_args: Union[PreprocessCliArgs, TrainerCliArgs], +) -> TrainDatasetMeta: + """ + Loads one or more training or evaluation datasets, calling + `axolotl.utils.data.prepare_dataset`. Optionally, logs out debug information. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + cli_args: Command-specific CLI arguments. + + Returns: + Dataclass with fields for training and evaluation datasets and the computed + `total_num_steps`. + """ + tokenizer = load_tokenizer(cfg) + processor = load_processor(cfg, tokenizer=tokenizer) if cfg.processor_type else None + + train_dataset, eval_dataset, total_num_steps, prompters = prepare_dataset( + cfg, + tokenizer, + processor=processor, + ) + + if ( + cli_args.debug + or cfg.debug + or cli_args.debug_text_only + or int(cli_args.debug_num_examples) > 0 + ): + LOG.info("check_dataset_labels...") + + train_samples = sample_dataset(train_dataset, cli_args.debug_num_examples) + check_dataset_labels( + train_samples, + tokenizer, + num_examples=cli_args.debug_num_examples, + text_only=cli_args.debug_text_only, + ) + + LOG.info("printing prompters...") + for prompter in prompters: + LOG.info(prompter) + + return TrainDatasetMeta( + train_dataset=train_dataset, + eval_dataset=eval_dataset, + total_num_steps=total_num_steps, + ) + + +def load_preference_datasets( + *, + cfg: DictDefault, + cli_args: Union[PreprocessCliArgs, TrainerCliArgs], +) -> TrainDatasetMeta: + """ + Loads one or more training or evaluation datasets for DPO training, calling + `axolotl.utils.data.rl.load_prepare_dpo_datasets`. Optionally, logs out debug + information. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + cli_args: Command-specific CLI arguments. + + Returns: + Dataclass with fields for training and evaluation datasets and the computed + `total_num_steps`. + """ + train_dataset, eval_dataset = load_prepare_dpo_datasets(cfg) + total_num_steps = int( + math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) + ) + + if cli_args.debug or cfg.debug: + LOG.info("check_dataset_labels...") + + tokenizer = load_tokenizer(cfg) + train_samples = sample_dataset(train_dataset, cli_args.debug_num_examples) + check_dataset_labels( + train_samples, + tokenizer, + num_examples=cli_args.debug_num_examples, + text_only=cli_args.debug_text_only, + rl_mode=True, + ) + + return TrainDatasetMeta( + train_dataset=train_dataset, + eval_dataset=eval_dataset, + total_num_steps=total_num_steps, + ) diff --git a/src/axolotl/evaluate.py b/src/axolotl/evaluate.py index acf15e3fc7..8d9ddc6abf 100644 --- a/src/axolotl/evaluate.py +++ b/src/axolotl/evaluate.py @@ -9,7 +9,6 @@ import torch from accelerate.logging import get_logger -from axolotl.common.cli import TrainerCliArgs from axolotl.logging_config import configure_logging from axolotl.train import TrainDatasetMeta from axolotl.utils import set_pytorch_cuda_alloc_conf @@ -62,16 +61,13 @@ def evaluate_dataset( return metrics -def evaluate( - *, cfg: DictDefault, cli_args: TrainerCliArgs, dataset_meta: TrainDatasetMeta -) -> Dict[str, float]: +def evaluate(*, cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> Dict[str, float]: """ Evaluate a model on training and validation datasets Args: - cfg: Configuration dictionary - cli_args: Command line arguments - dataset_meta: Dataset metadata containing training and evaluation datasets + cfg: Dictionary mapping `axolotl` config keys to values. + dataset_meta: Dataset metadata containing training and evaluation datasets. Returns: Tuple containing: @@ -102,9 +98,7 @@ def evaluate( # Load model LOG.debug("loading model for evaluation...") - model, _ = load_model( - cfg, tokenizer, processor=processor, inference=cli_args.inference - ) + model, _ = load_model(cfg, tokenizer, processor=processor) # Set up trainer trainer = setup_trainer( diff --git a/src/axolotl/train.py b/src/axolotl/train.py index a74ecc2ec3..b901c2a972 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -5,21 +5,19 @@ import signal import sys import weakref -from dataclasses import dataclass from pathlib import Path -from typing import Optional, Tuple, Union +from typing import Tuple, Union import torch import transformers.modelcard from accelerate.logging import get_logger from accelerate.utils import save_fsdp_model -from datasets import Dataset from peft import PeftModel from pkg_resources import get_distribution # type: ignore from transformers import PreTrainedModel, PreTrainedTokenizer from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled -from axolotl.common.cli import TrainerCliArgs +from axolotl.common.datasets import TrainDatasetMeta from axolotl.contribs.lgpl.unsloth import ( # pylint: disable = no-name-in-module fix_untrained_tokens, ) @@ -39,22 +37,11 @@ sys.path.insert(0, src_dir) configure_logging() -LOG = get_logger("axolotl.train") - - -@dataclass -class TrainDatasetMeta: - """ - dataclass to capture the dataset specific options for training - """ - - train_dataset: Dataset - eval_dataset: Optional[Dataset] = None - total_num_steps: Optional[int] = None +LOG = get_logger(__name__) def train( - *, cfg: DictDefault, cli_args: TrainerCliArgs, dataset_meta: TrainDatasetMeta + *, cfg: DictDefault, dataset_meta: TrainDatasetMeta ) -> Tuple[Union[PeftModel, PreTrainedModel], PreTrainedTokenizer]: # Load tokenizer LOG.debug( @@ -93,9 +80,7 @@ def train( if cfg.adapter: msg += " and peft_config..." LOG.debug(msg) - model, peft_config = load_model( - cfg, tokenizer, processor=processor, inference=cli_args.inference - ) + model, peft_config = load_model(cfg, tokenizer, processor=processor) if model.generation_config is not None: model.generation_config.do_sample = True @@ -107,9 +92,7 @@ def train( model_ref = None # explicit setting to None else: # load the model again for model_ref/baseline - model_ref, _ = load_model( - cfg, tokenizer, inference=cli_args.inference, reference_model=True - ) + model_ref, _ = load_model(cfg, tokenizer, reference_model=True) safe_serialization = cfg.save_safetensors is True diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index aff0476754..de373c06ea 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -109,7 +109,9 @@ def prepare_dataset(cfg, tokenizer, processor=None): cfg.pretraining_dataset[0]["type"] or "pretrain", ) - iter_ds = load_dataset(path, streaming=True, split=split, name=name, data_files=data_files) + iter_ds = load_dataset( + path, streaming=True, split=split, name=name, data_files=data_files + ) if skip: LOG.info(f"Skipping {skip} samples from the dataset") iter_ds = iter_ds.skip(skip) diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py index 78b090e19e..d360e29d6b 100644 --- a/tests/cli/conftest.py +++ b/tests/cli/conftest.py @@ -1,4 +1,5 @@ """Shared pytest fixtures for cli module.""" + import pytest from click.testing import CliRunner diff --git a/tests/cli/test_cli_fetch.py b/tests/cli/test_cli_fetch.py index 0df87b0299..f06f067173 100644 --- a/tests/cli/test_cli_fetch.py +++ b/tests/cli/test_cli_fetch.py @@ -1,4 +1,5 @@ """pytest tests for axolotl CLI fetch command.""" + from unittest.mock import patch from axolotl.cli.main import fetch diff --git a/tests/cli/test_cli_inference.py b/tests/cli/test_cli_inference.py index 7cb163d255..b8effa3d20 100644 --- a/tests/cli/test_cli_inference.py +++ b/tests/cli/test_cli_inference.py @@ -1,4 +1,5 @@ """pytest tests for axolotl CLI inference command.""" + from unittest.mock import patch from axolotl.cli.main import cli diff --git a/tests/cli/test_cli_interface.py b/tests/cli/test_cli_interface.py index ed8335b766..8b5fec17f2 100644 --- a/tests/cli/test_cli_interface.py +++ b/tests/cli/test_cli_interface.py @@ -1,4 +1,5 @@ """General pytest tests for axolotl.cli.main interface.""" + from axolotl.cli.main import build_command, cli diff --git a/tests/cli/test_cli_merge_lora.py b/tests/cli/test_cli_merge_lora.py index 165a64e98c..aac0167603 100644 --- a/tests/cli/test_cli_merge_lora.py +++ b/tests/cli/test_cli_merge_lora.py @@ -1,4 +1,5 @@ """pytest tests for axolotl CLI merge_lora command.""" + from unittest.mock import patch from axolotl.cli.main import cli diff --git a/tests/cli/test_cli_merge_sharded_fsdp_weights.py b/tests/cli/test_cli_merge_sharded_fsdp_weights.py index cff0f3b773..18589a80d9 100644 --- a/tests/cli/test_cli_merge_sharded_fsdp_weights.py +++ b/tests/cli/test_cli_merge_sharded_fsdp_weights.py @@ -1,5 +1,6 @@ """pytest tests for axolotl CLI merge_sharded_fsdp_weights command.""" # pylint: disable=duplicate-code + from unittest.mock import patch from axolotl.cli.main import cli @@ -15,46 +16,3 @@ def test_merge_sharded_fsdp_weights_no_accelerate(cli_runner, config_path): assert mock.called assert mock.call_args.kwargs["config"] == str(config_path) assert result.exit_code == 0 - - -def test_merge_sharded_fsdp_weights_with_model_dir(cli_runner, config_path, tmp_path): - """Test merge_sharded_fsdp_weights command with model_dir option""" - model_dir = tmp_path / "model" - model_dir.mkdir() - - with patch("axolotl.cli.merge_sharded_fsdp_weights.do_cli") as mock: - result = cli_runner.invoke( - cli, - [ - "merge-sharded-fsdp-weights", - str(config_path), - "--no-accelerate", - "--model-dir", - str(model_dir), - ], - ) - - assert mock.called - assert mock.call_args.kwargs["config"] == str(config_path) - assert mock.call_args.kwargs["model_dir"] == str(model_dir) - assert result.exit_code == 0 - - -def test_merge_sharded_fsdp_weights_with_save_path(cli_runner, config_path): - """Test merge_sharded_fsdp_weights command with save_path option""" - with patch("axolotl.cli.merge_sharded_fsdp_weights.do_cli") as mock: - result = cli_runner.invoke( - cli, - [ - "merge-sharded-fsdp-weights", - str(config_path), - "--no-accelerate", - "--save-path", - "/path/to/save", - ], - ) - - assert mock.called - assert mock.call_args.kwargs["config"] == str(config_path) - assert mock.call_args.kwargs["save_path"] == "/path/to/save" - assert result.exit_code == 0 diff --git a/tests/cli/test_cli_preprocess.py b/tests/cli/test_cli_preprocess.py index 4719461aaf..e2dd3a6c35 100644 --- a/tests/cli/test_cli_preprocess.py +++ b/tests/cli/test_cli_preprocess.py @@ -1,4 +1,5 @@ """pytest tests for axolotl CLI preprocess command.""" + import shutil from pathlib import Path from unittest.mock import patch diff --git a/tests/cli/test_cli_shard.py b/tests/cli/test_cli_shard.py deleted file mode 100644 index 505a2a7372..0000000000 --- a/tests/cli/test_cli_shard.py +++ /dev/null @@ -1,76 +0,0 @@ -"""pytest tests for axolotl CLI shard command.""" -# pylint: disable=duplicate-code -from unittest.mock import patch - -from axolotl.cli.main import cli - - -def test_shard_with_accelerate(cli_runner, config_path): - """Test shard command with accelerate""" - with patch("subprocess.run") as mock: - result = cli_runner.invoke(cli, ["shard", str(config_path), "--accelerate"]) - - assert mock.called - assert mock.call_args.args[0] == [ - "accelerate", - "launch", - "-m", - "axolotl.cli.shard", - str(config_path), - "--debug-num-examples", - "0", - ] - assert mock.call_args.kwargs == {"check": True} - assert result.exit_code == 0 - - -def test_shard_no_accelerate(cli_runner, config_path): - """Test shard command without accelerate""" - with patch("axolotl.cli.shard.do_cli") as mock: - result = cli_runner.invoke(cli, ["shard", str(config_path), "--no-accelerate"]) - - assert mock.called - assert result.exit_code == 0 - - -def test_shard_with_model_dir(cli_runner, config_path, tmp_path): - """Test shard command with model_dir option""" - model_dir = tmp_path / "model" - model_dir.mkdir() - - with patch("axolotl.cli.shard.do_cli") as mock: - result = cli_runner.invoke( - cli, - [ - "shard", - str(config_path), - "--no-accelerate", - "--model-dir", - str(model_dir), - ], - catch_exceptions=False, - ) - - assert mock.called - assert mock.call_args.kwargs["config"] == str(config_path) - assert mock.call_args.kwargs["model_dir"] == str(model_dir) - assert result.exit_code == 0 - - -def test_shard_with_save_dir(cli_runner, config_path): - with patch("axolotl.cli.shard.do_cli") as mock: - result = cli_runner.invoke( - cli, - [ - "shard", - str(config_path), - "--no-accelerate", - "--save-dir", - "/path/to/save", - ], - ) - - assert mock.called - assert mock.call_args.kwargs["config"] == str(config_path) - assert mock.call_args.kwargs["save_dir"] == "/path/to/save" - assert result.exit_code == 0 diff --git a/tests/cli/test_cli_version.py b/tests/cli/test_cli_version.py index 819780e945..533dd5c0ec 100644 --- a/tests/cli/test_cli_version.py +++ b/tests/cli/test_cli_version.py @@ -1,4 +1,5 @@ """pytest tests for axolotl CLI --version""" + from axolotl.cli.main import cli diff --git a/tests/cli/test_utils.py b/tests/cli/test_utils.py index b88e4ac729..ecb0025e44 100644 --- a/tests/cli/test_utils.py +++ b/tests/cli/test_utils.py @@ -1,5 +1,6 @@ """pytest tests for axolotl CLI utils.""" # pylint: disable=redefined-outer-name + import json from unittest.mock import Mock, patch diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py index 6562af176f..291a4a4ec6 100644 --- a/tests/e2e/integrations/test_cut_cross_entropy.py +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -4,8 +4,8 @@ import pytest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils import get_pytorch_version from axolotl.utils.config import normalize_config, prepare_plugins @@ -64,9 +64,9 @@ def test_llama_w_cce(self, min_cfg, temp_dir): major, minor, _ = get_pytorch_version() if (major, minor) < (2, 4): with pytest.raises(ImportError): - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) else: - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @pytest.mark.parametrize( @@ -92,7 +92,7 @@ def test_llama_w_cce_and_attention(self, min_cfg, temp_dir, attention_type): major, minor, _ = get_pytorch_version() if (major, minor) < (2, 4): with pytest.raises(ImportError): - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) else: - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/integrations/test_liger.py b/tests/e2e/integrations/test_liger.py index 9154bf9b8d..1efe889e4f 100644 --- a/tests/e2e/integrations/test_liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -4,8 +4,8 @@ from e2e.utils import require_torch_2_4_1 -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, prepare_plugins from axolotl.utils.dict import DictDefault @@ -60,7 +60,7 @@ def test_llama_wo_flce(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @require_torch_2_4_1 @@ -105,5 +105,5 @@ def test_llama_w_flce(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index 08b3bf0daf..da27069ac6 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -65,7 +65,7 @@ def test_sdp_lora_packing(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -109,5 +109,5 @@ def test_torch_lora_packing(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_cli_integrations.py b/tests/e2e/patched/test_cli_integrations.py index 6ca7c52aea..ce9396d5ff 100644 --- a/tests/e2e/patched/test_cli_integrations.py +++ b/tests/e2e/patched/test_cli_integrations.py @@ -5,7 +5,7 @@ import yaml -from axolotl.cli import load_cfg +from axolotl.cli.config import load_cfg from axolotl.utils.dict import DictDefault diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index 791d955b28..2bfd36d155 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -8,8 +8,8 @@ import pytest from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -80,7 +80,7 @@ def test_lora_packing_fa_cross_entropy(self, temp_dir, gradient_accumulation_ste cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) check_tensorboard( diff --git a/tests/e2e/patched/test_falcon_samplepack.py b/tests/e2e/patched/test_falcon_samplepack.py index 69516810f6..62ee4f717a 100644 --- a/tests/e2e/patched/test_falcon_samplepack.py +++ b/tests/e2e/patched/test_falcon_samplepack.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -67,7 +67,7 @@ def test_qlora(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -107,5 +107,5 @@ def test_ft(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index 23a0adfc07..e7ab510c91 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -9,8 +9,8 @@ import pytest from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -71,5 +71,5 @@ def test_fft_packing(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py index d0fdd918a1..8d0ba6c2ac 100644 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ b/tests/e2e/patched/test_llama_s2_attention.py @@ -8,8 +8,8 @@ import pytest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -69,7 +69,7 @@ def test_lora_s2_attn(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -109,5 +109,5 @@ def test_fft_s2_attn(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index 634e544d20..bc18e3d813 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -9,8 +9,8 @@ import pytest from transformers.utils import is_auto_gptq_available, is_torch_bf16_gpu_available -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -74,7 +74,7 @@ def test_lora_packing(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @pytest.mark.skipif(not is_auto_gptq_available(), reason="auto-gptq not available") @@ -124,5 +124,5 @@ def test_lora_gptq_packed(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index e93863e09c..c7fd0ecbc7 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -67,7 +67,7 @@ def test_lora_packing(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -108,5 +108,5 @@ def test_ft_packing(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index f87c34fd10..156dac7e8a 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -64,7 +64,7 @@ def test_qlora(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -102,7 +102,7 @@ def test_ft(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - model, _ = train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + model, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( "MixtralFlashAttention2" in model.model.layers[0].self_attn.__class__.__name__ diff --git a/tests/e2e/patched/test_model_patches.py b/tests/e2e/patched/test_model_patches.py index 170c37fd6c..78b01be645 100644 --- a/tests/e2e/patched/test_model_patches.py +++ b/tests/e2e/patched/test_model_patches.py @@ -6,7 +6,6 @@ import transformers -from axolotl.common.cli import TrainerCliArgs from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_model, load_tokenizer @@ -49,9 +48,8 @@ def test_mixtral_multipack(self, temp_dir): } ) normalize_config(cfg) - cli_args = TrainerCliArgs() tokenizer = load_tokenizer(cfg) - model, _ = load_model(cfg, tokenizer, inference=cli_args.inference) + model, _ = load_model(cfg, tokenizer, inference=False) assert ( "MixtralFlashAttention2" @@ -87,9 +85,8 @@ def test_mistral_multipack(self, temp_dir): } ) normalize_config(cfg) - cli_args = TrainerCliArgs() tokenizer = load_tokenizer(cfg) - load_model(cfg, tokenizer, inference=cli_args.inference) + load_model(cfg, tokenizer, inference=False) assert ( "torch.jit" diff --git a/tests/e2e/patched/test_phi_multipack.py b/tests/e2e/patched/test_phi_multipack.py index 852ac7bec1..ce466460eb 100644 --- a/tests/e2e/patched/test_phi_multipack.py +++ b/tests/e2e/patched/test_phi_multipack.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -67,7 +67,7 @@ def test_ft_packed(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -118,5 +118,5 @@ def test_qlora_packed(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index 5639d2eaee..f6a3e0109e 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -9,8 +9,8 @@ from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -71,7 +71,7 @@ def test_resume_lora_packed(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) resume_cfg = cfg | DictDefault( { @@ -81,7 +81,7 @@ def test_resume_lora_packed(self, temp_dir): normalize_config(resume_cfg) cli_args = TrainerCliArgs() - train(cfg=resume_cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=resume_cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) tb_log_path_1 = most_recent_subdir(temp_dir + "/runs") diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index 492bc1c236..da5eaffb66 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -6,8 +6,8 @@ import pytest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -75,7 +75,7 @@ def test_unsloth_llama_qlora_fa2(self, temp_dir, sample_packing): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) check_tensorboard( @@ -125,7 +125,7 @@ def test_unsloth_llama_qlora_unpacked(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) check_tensorboard( @@ -180,7 +180,7 @@ def test_unsloth_llama_qlora_unpacked_no_fa2_fp16(self, temp_dir, sdp_attention) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) check_tensorboard( diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index f8109373a9..2d0baceeef 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -9,8 +9,8 @@ import pytest -from axolotl.cli import load_rl_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_preference_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -65,9 +65,9 @@ def test_dpo_lora(self, temp_dir): ) normalize_config(cfg) cli_args = TrainerCliArgs() - dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) @with_temp_dir @@ -110,9 +110,9 @@ def test_dpo_nll_lora(self, temp_dir): ) normalize_config(cfg) cli_args = TrainerCliArgs() - dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) @with_temp_dir @@ -155,9 +155,9 @@ def test_dpo_use_weighting(self, temp_dir): ) normalize_config(cfg) cli_args = TrainerCliArgs() - dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) @pytest.mark.skip("kto_pair no longer supported in trl") @@ -200,9 +200,9 @@ def test_kto_pair_lora(self, temp_dir): ) normalize_config(cfg) cli_args = TrainerCliArgs() - dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) @with_temp_dir @@ -244,9 +244,9 @@ def test_ipo_lora(self, temp_dir): ) normalize_config(cfg) cli_args = TrainerCliArgs() - dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) @with_temp_dir @@ -291,9 +291,9 @@ def test_orpo_lora(self, temp_dir): ) normalize_config(cfg) cli_args = TrainerCliArgs() - dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) @pytest.mark.skip(reason="Fix the implementation") @@ -355,7 +355,7 @@ def test_kto_lora(self, temp_dir): ) normalize_config(cfg) cli_args = TrainerCliArgs() - dataset_meta = load_rl_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py index 222d620ae7..4261ccc266 100644 --- a/tests/e2e/test_embeddings_lr.py +++ b/tests/e2e/test_embeddings_lr.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -60,7 +60,7 @@ def test_train_w_embedding_lr_scale(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) check_tensorboard( @@ -104,7 +104,7 @@ def test_train_w_embedding_lr(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) check_tensorboard( diff --git a/tests/e2e/test_falcon.py b/tests/e2e/test_falcon.py index 117de66353..ddcb662755 100644 --- a/tests/e2e/test_falcon.py +++ b/tests/e2e/test_falcon.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -69,7 +69,7 @@ def test_lora(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -122,7 +122,7 @@ def test_lora_added_vocab(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -161,5 +161,5 @@ def test_ft(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index 4384bb61e7..a948284904 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -7,8 +7,8 @@ from e2e.utils import check_model_output_exists -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -60,7 +60,7 @@ def test_fft_trust_remote_code(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) def test_fix_untrained_tokens(self, temp_dir): @@ -103,7 +103,7 @@ def test_fix_untrained_tokens(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) def test_batch_flattening(self, temp_dir): @@ -142,5 +142,5 @@ def test_batch_flattening(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index d13b10659a..68cd490be6 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -62,5 +62,5 @@ def test_pretrain_w_sample_packing(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py index 250cf418c0..91f101e44c 100644 --- a/tests/e2e/test_llama_vision.py +++ b/tests/e2e/test_llama_vision.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -66,7 +66,7 @@ def test_lora_llama_vision_text_only_dataset(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -111,5 +111,5 @@ def test_lora_llama_vision_multimodal_dataset(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index a7ead64a53..696c47aed7 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -63,5 +63,5 @@ def test_lora(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_mamba.py b/tests/e2e/test_mamba.py index a1fc308629..4b4db30585 100644 --- a/tests/e2e/test_mamba.py +++ b/tests/e2e/test_mamba.py @@ -8,8 +8,8 @@ import pytest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -63,5 +63,5 @@ def test_fft(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_mistral.py b/tests/e2e/test_mistral.py index 2e79fec8db..a304e9b4a5 100644 --- a/tests/e2e/test_mistral.py +++ b/tests/e2e/test_mistral.py @@ -8,8 +8,8 @@ from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -67,7 +67,7 @@ def test_lora(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -110,5 +110,5 @@ def test_ft(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_mixtral.py b/tests/e2e/test_mixtral.py index 6792d05a67..6e06626f6e 100644 --- a/tests/e2e/test_mixtral.py +++ b/tests/e2e/test_mixtral.py @@ -9,8 +9,8 @@ import torch from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -73,7 +73,7 @@ def test_qlora_w_fa2(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - model, _ = train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + model, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype == torch.float32 @@ -127,7 +127,7 @@ def test_qlora_wo_fa2(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - model, _ = train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + model, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype == torch.float32 @@ -184,7 +184,7 @@ def test_16bit_lora_w_fa2(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - model, _ = train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + model, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype == torch.float32 @@ -241,7 +241,7 @@ def test_16bit_lora_wo_fa2(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - model, _ = train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + model, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype == torch.float32 @@ -285,5 +285,5 @@ def test_ft(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index f1bbaafd5c..453872538a 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -63,7 +63,7 @@ def test_optimi_adamw(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -107,7 +107,7 @@ def test_adopt_adamw(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -143,5 +143,5 @@ def test_fft_schedule_free_adamw(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index dd0af32f3c..13244a2152 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -8,8 +8,8 @@ from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -63,7 +63,7 @@ def test_loss_packed(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_tensorboard( temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" diff --git a/tests/e2e/test_phi.py b/tests/e2e/test_phi.py index 7a08d0c6f7..54f564d0e7 100644 --- a/tests/e2e/test_phi.py +++ b/tests/e2e/test_phi.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -65,7 +65,7 @@ def test_phi_ft(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @with_temp_dir @@ -114,5 +114,5 @@ def test_phi_qlora(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_relora_llama.py b/tests/e2e/test_relora_llama.py index fef6a3d303..6c785dc861 100644 --- a/tests/e2e/test_relora_llama.py +++ b/tests/e2e/test_relora_llama.py @@ -7,8 +7,8 @@ import unittest from pathlib import Path -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -77,7 +77,7 @@ def test_relora(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-100/adapter", cfg) assert ( Path(temp_dir) / "checkpoint-100/relora/model.safetensors" diff --git a/tests/e2e/test_reward_model_llama.py b/tests/e2e/test_reward_model_llama.py index c4cb705ea8..4cd8602f3e 100644 --- a/tests/e2e/test_reward_model_llama.py +++ b/tests/e2e/test_reward_model_llama.py @@ -6,8 +6,8 @@ import os import unittest -from axolotl.cli import load_datasets -from axolotl.common.cli import TrainerCliArgs +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault @@ -69,5 +69,5 @@ def test_rm_fft(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, cli_args=cli_args, dataset_meta=dataset_meta) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) From 19cd83d408ba0d46f2cf6e285488001eeaf4d1c1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 14 Jan 2025 22:07:55 -0500 Subject: [PATCH 0374/1405] rename references to dpo dataset prep to pref data (#2258) --- src/axolotl/common/datasets.py | 10 +++++----- src/axolotl/utils/data/__init__.py | 2 +- src/axolotl/utils/data/rl.py | 2 +- tests/test_datasets.py | 6 +++--- tests/test_exact_deduplication.py | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index d07add29b7..c693c26d83 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -11,7 +11,7 @@ import axolotl.monkeypatch.data.batch_dataset_fetcher # pylint: disable=unused-import # noqa: F401 from axolotl.cli.args import PreprocessCliArgs, TrainerCliArgs from axolotl.utils.data import prepare_dataset -from axolotl.utils.data.rl import load_prepare_dpo_datasets +from axolotl.utils.data.rl import load_prepare_preference_datasets from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_processor, load_tokenizer from axolotl.utils.tokenization import check_dataset_labels @@ -103,9 +103,9 @@ def load_preference_datasets( cli_args: Union[PreprocessCliArgs, TrainerCliArgs], ) -> TrainDatasetMeta: """ - Loads one or more training or evaluation datasets for DPO training, calling - `axolotl.utils.data.rl.load_prepare_dpo_datasets`. Optionally, logs out debug - information. + Loads one or more training or evaluation datasets for RL training using paired + preference data, calling `axolotl.utils.data.rl.load_prepare_preference_datasets`. + Optionally, logs out debug information. Args: cfg: Dictionary mapping `axolotl` config keys to values. @@ -115,7 +115,7 @@ def load_preference_datasets( Dataclass with fields for training and evaluation datasets and the computed `total_num_steps`. """ - train_dataset, eval_dataset = load_prepare_dpo_datasets(cfg) + train_dataset, eval_dataset = load_prepare_preference_datasets(cfg) total_num_steps = int( math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) ) diff --git a/src/axolotl/utils/data/__init__.py b/src/axolotl/utils/data/__init__.py index 140d02106d..7f90bf3cb5 100644 --- a/src/axolotl/utils/data/__init__.py +++ b/src/axolotl/utils/data/__init__.py @@ -5,7 +5,7 @@ encode_pretraining, wrap_pretraining_dataset, ) -from axolotl.utils.data.rl import load_prepare_dpo_datasets # noqa: F401 +from axolotl.utils.data.rl import load_prepare_preference_datasets # noqa: F401 from axolotl.utils.data.sft import ( # noqa: F401 get_dataset_wrapper, load_prepare_datasets, diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index edb72f186e..9f5c726aba 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -115,7 +115,7 @@ def drop_long_rl_seq( raise ValueError("Unknown RL type") -def load_prepare_dpo_datasets(cfg): +def load_prepare_preference_datasets(cfg): def load_split(dataset_cfgs, _cfg): split_datasets: List[Any] = [] for i, ds_cfg in enumerate(dataset_cfgs): diff --git a/tests/test_datasets.py b/tests/test_datasets.py index b1ecfd6d52..49554d3700 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -17,7 +17,7 @@ from transformers import AutoTokenizer from axolotl.utils.data import load_tokenized_prepared_datasets -from axolotl.utils.data.rl import load_prepare_dpo_datasets +from axolotl.utils.data.rl import load_prepare_preference_datasets from axolotl.utils.dict import DictDefault @@ -280,7 +280,7 @@ def test_load_hub_with_dpo(self): } ) - train_dataset, _ = load_prepare_dpo_datasets(cfg) + train_dataset, _ = load_prepare_preference_datasets(cfg) assert len(train_dataset) == 1800 assert "conversation" in train_dataset.features @@ -329,7 +329,7 @@ def test_load_hub_with_revision_with_dpo(self): } ) - train_dataset, _ = load_prepare_dpo_datasets(cfg) + train_dataset, _ = load_prepare_preference_datasets(cfg) assert len(train_dataset) == 1800 assert "conversation" in train_dataset.features diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index 2ac6415be5..bc0734ed3c 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -12,7 +12,7 @@ from transformers import AutoTokenizer from axolotl.utils.data import prepare_dataset -from axolotl.utils.data.rl import load_prepare_dpo_datasets +from axolotl.utils.data.rl import load_prepare_preference_datasets from axolotl.utils.data.utils import deduplicate_and_log_datasets from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_processor, load_tokenizer @@ -236,7 +236,7 @@ def test_load_with_deduplication(self): """Verify that loading with deduplication removes duplicates.""" # Load the dataset using the deduplication setting - train_dataset, _ = load_prepare_dpo_datasets(self.cfg) + train_dataset, _ = load_prepare_preference_datasets(self.cfg) # Verify that the dataset has been deduplicated assert len(train_dataset) == 1800, "Dataset was not properly deduplicated" @@ -245,7 +245,7 @@ def test_load_without_deduplication(self): """Verify that loading without deduplication retains duplicates.""" self.cfg.dataset_exact_deduplication = False # Load the dataset without deduplication - train_dataset, _ = load_prepare_dpo_datasets(self.cfg) + train_dataset, _ = load_prepare_preference_datasets(self.cfg) # Verify that the dataset retains duplicates assert ( From cba5a457d9541a1ffde6a99977bff575c4899966 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 15 Jan 2025 10:08:56 +0700 Subject: [PATCH 0375/1405] fix: use text_column even when not packing for pretraining (#2254) * fix: use text_column even when not packing for pretraining * feat: update test to check when not packing * chore: lint * Update src/axolotl/utils/data/pretraining.py Co-authored-by: Wing Lian --------- Co-authored-by: Wing Lian Co-authored-by: Wing Lian --- src/axolotl/utils/data/pretraining.py | 14 +++++++++++--- tests/e2e/test_llama_pretrain.py | 16 ++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/axolotl/utils/data/pretraining.py b/src/axolotl/utils/data/pretraining.py index f493db70eb..369d2d6fea 100644 --- a/src/axolotl/utils/data/pretraining.py +++ b/src/axolotl/utils/data/pretraining.py @@ -18,10 +18,13 @@ def encode_pretraining( - tokenizer: PreTrainedTokenizerBase, max_tokens: int, examples: Dict[str, List] + tokenizer: PreTrainedTokenizerBase, + max_tokens: int, + examples: Dict[str, List], + text_column: str = "text", ) -> Dict[str, List]: res = tokenizer( - examples["text"], + examples[text_column], truncation=True, max_length=max_tokens - 2, add_special_tokens=True, @@ -196,7 +199,12 @@ def wrap_pretraining_dataset( # set this to 1 so downstream data_loader doesn't try to increase the batch again cfg.micro_batch_size = 1 else: - encode = functools.partial(encode_pretraining, tokenizer, max_tokens) + encode = functools.partial( + encode_pretraining, + tokenizer, + max_tokens, + text_column=cfg.pretraining_dataset[0].text_column or "text", + ) if cfg.shuffle_merged_datasets: dataset = dataset.shuffle(seed=seed, buffer_size=buffer_size) diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index 68cd490be6..117eba25db 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -4,7 +4,8 @@ import logging import os -import unittest + +import pytest from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets @@ -12,19 +13,22 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import check_model_output_exists, with_temp_dir +from .utils import check_model_output_exists LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" -class TestPretrainLlama(unittest.TestCase): +class TestPretrainLlama: """ Test case for Llama models w pretraining """ - @with_temp_dir - def test_pretrain_w_sample_packing(self, temp_dir): + @pytest.mark.parametrize( + "sample_packing", + [True, False], + ) + def test_pretrain(self, temp_dir, sample_packing): # pylint: disable=duplicate-code cfg = DictDefault( { @@ -32,7 +36,7 @@ def test_pretrain_w_sample_packing(self, temp_dir): "tokenizer_type": "LlamaTokenizer", "flash_attention": True, "sequence_len": 1024, - "sample_packing": True, + "sample_packing": sample_packing, "special_tokens": { "unk_token": "", "bos_token": "", From 860609392184cf62a7e0ca676658b170e059ce6c Mon Sep 17 00:00:00 2001 From: jwongTensora Date: Wed, 15 Jan 2025 03:09:29 +0000 Subject: [PATCH 0376/1405] fix for indexing error from token/embeddings mismatch (#2257) Co-authored-by: jwong --- src/axolotl/utils/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 523fd76feb..4a665c111c 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -1057,7 +1057,7 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: ) if ( hasattr(self.model, "get_input_embeddings") - and self.model.get_input_embeddings().num_embeddings < embeddings_len + and self.model.get_input_embeddings().num_embeddings != embeddings_len ): resize_kwargs = {} if self.cfg.mean_resizing_embeddings is not None: From af727eedf75518bc603545b03a54a28fa99beeec Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 20 Jan 2025 14:07:34 -0500 Subject: [PATCH 0377/1405] option to not concatenate during pretraining (#2263) * option to not concatenate during pretraining * simplify conditional and add doc to config.qmd --- docs/config.qmd | 2 ++ src/axolotl/core/trainer_builder.py | 2 ++ src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 6 ++++++ src/axolotl/utils/data/pretraining.py | 9 +++++++++ 4 files changed, 19 insertions(+) diff --git a/docs/config.qmd b/docs/config.qmd index 70679791e4..179ee9ed19 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -244,6 +244,8 @@ total_num_tokens: sample_packing_group_size: 100000 # The number of samples which can be packed into one sequence. Increase if using a large sequence_len with many short samples. sample_packing_bin_size: 200 +# whether to concatenate samples during pretraining +pretraining_sample_concatenation: # Use batch flattening for speedups when not using sample_packing batch_flattening: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 176ce4174f..6f1bae1ef0 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1877,6 +1877,8 @@ def build_collator( self, training_args: AxolotlTrainingArguments, is_eval=False, **kwargs ): if training_args.pretraining: + if self.cfg.pretraining_sample_concatenation is False: + return DataCollatorForSeq2Seq(self.tokenizer, **kwargs) return None if self.cfg.model_config_type == "mamba": diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 4f368994ac..98cdee009e 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -706,6 +706,12 @@ class Config: pad_to_sequence_len: Optional[bool] = None curriculum_sampling: Optional[bool] = None multipack_real_batches: Optional[bool] = None + pretraining_sample_concatenation: Optional[bool] = Field( + default=None, + json_schema_extra={ + "description": "whether to soft pack/concatenate samples during pretraining", + }, + ) batch_flattening: Optional[Union[Literal["auto"], bool]] = None diff --git a/src/axolotl/utils/data/pretraining.py b/src/axolotl/utils/data/pretraining.py index 369d2d6fea..c30d625751 100644 --- a/src/axolotl/utils/data/pretraining.py +++ b/src/axolotl/utils/data/pretraining.py @@ -22,6 +22,7 @@ def encode_pretraining( max_tokens: int, examples: Dict[str, List], text_column: str = "text", + concatenate: bool = True, ) -> Dict[str, List]: res = tokenizer( examples[text_column], @@ -33,6 +34,13 @@ def encode_pretraining( input_ids = [torch.tensor(seq) for seq in res["input_ids"]] targets = [torch.tensor(seq) for seq in res["input_ids"]] attention_mask = [torch.tensor(seq) for seq in res["attention_mask"]] + if not concatenate: + return { + "input_ids": [seq.tolist() for seq in input_ids], + "labels": [seq.tolist() for seq in targets], + "attention_mask": [seq.tolist() for seq in attention_mask], + } + new_input_ids = [] new_labels = [] new_attention_mask = [] @@ -204,6 +212,7 @@ def wrap_pretraining_dataset( tokenizer, max_tokens, text_column=cfg.pretraining_dataset[0].text_column or "text", + concatenate=cfg.pretraining_sample_concatenation is True, ) if cfg.shuffle_merged_datasets: From bb9d4102c4d11d3129d88b8b563c2d03c4b1f985 Mon Sep 17 00:00:00 2001 From: Adithya Kamath Date: Wed, 22 Jan 2025 02:09:17 +0530 Subject: [PATCH 0378/1405] Add 5000 line history limit to tmux for docker cloud (#2268) --- docker/Dockerfile-cloud | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile-cloud b/docker/Dockerfile-cloud index c8249cb79c..735afa4dd7 100644 --- a/docker/Dockerfile-cloud +++ b/docker/Dockerfile-cloud @@ -20,7 +20,8 @@ RUN apt install --yes --no-install-recommends openssh-server tmux && \ printf "\n[[ -z \"\$TMUX\" ]] && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> ~/.bashrc && \ printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> ~/.bashrc && \ chmod +x /workspace/axolotl/scripts/cloud-entrypoint.sh && \ - chmod +x /root/cloud-entrypoint.sh + chmod +x /root/cloud-entrypoint.sh && \ + echo 'set-option -g history-limit 5000' >> ~/.tmux.conf ENTRYPOINT ["/root/cloud-entrypoint.sh"] CMD ["sleep", "infinity"] From 8fb72cbc0b94129141bae5fa4d84edd23b648af6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 21 Jan 2025 15:39:30 -0500 Subject: [PATCH 0379/1405] use the extracted field_messages to parse the role fields (#2265) --- scripts/chat_datasets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/chat_datasets.py b/scripts/chat_datasets.py index 5eb5bde1e2..6210b11388 100644 --- a/scripts/chat_datasets.py +++ b/scripts/chat_datasets.py @@ -30,7 +30,7 @@ def parse_dataset(dataset=None, split="train"): ) ds_cfg["field_messages"] = field_messages - message_fields = features["conversations"][0].keys() + message_fields = features[field_messages][0].keys() message_field_role = None for key in ["from", "role"]: if key in message_fields: From 8a7a0b07dc5ce6da9171e28a0818b447b6d7cea2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 23 Jan 2025 21:17:57 -0500 Subject: [PATCH 0380/1405] support for latest transformers release 4.48.1 (#2256) --- cicd/cicd.sh | 3 +- requirements.txt | 4 +- src/axolotl/core/trainer_builder.py | 1 + src/axolotl/monkeypatch/trainer_grad_accum.py | 308 ------------------ .../monkeypatch/transformers_fa_utils.py | 67 ++++ src/axolotl/utils/models.py | 18 +- tests/e2e/multigpu/test_llama.py | 16 +- tests/e2e/patched/test_mixtral_samplepack.py | 6 +- tests/e2e/patched/test_model_patches.py | 7 +- tests/e2e/patched/test_unsloth_integration.py | 4 +- tests/e2e/solo/__init__.py | 0 tests/e2e/{ => solo}/test_relora_llama.py | 2 +- tests/patched/test_llama_trainer_ga.py | 25 -- 13 files changed, 98 insertions(+), 363 deletions(-) delete mode 100644 src/axolotl/monkeypatch/trainer_grad_accum.py create mode 100644 src/axolotl/monkeypatch/transformers_fa_utils.py create mode 100644 tests/e2e/solo/__init__.py rename tests/e2e/{ => solo}/test_relora_llama.py (97%) delete mode 100644 tests/patched/test_llama_trainer_ga.py diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 91926127fb..34a30db448 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -6,5 +6,6 @@ python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__" pytest -v --durations=10 -n8 --ignore=tests/e2e/ --ignore=tests/patched/ /workspace/axolotl/tests/ # pytest -v --durations=10 -n8 --dist loadfile /workspace/axolotl/tests/patched/ pytest -v --durations=10 /workspace/axolotl/tests/e2e/patched/ +pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/solo/ pytest -v --durations=10 /workspace/axolotl/tests/e2e/integrations/ -pytest -v --durations=10 --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ +pytest -v --durations=10 --ignore=tests/e2e/solo/ --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ diff --git a/requirements.txt b/requirements.txt index 1f7ac7bbad..52e146411d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,9 +13,9 @@ liger-kernel==0.5.2 packaging==23.2 peft==0.14.0 -transformers==4.47.1 +transformers==4.48.1 tokenizers>=0.21.0 -accelerate==1.2.1 +accelerate==1.3.0 datasets==3.2.0 deepspeed==0.16.1 trl==0.13.0 diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 6f1bae1ef0..edc842994c 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1079,6 +1079,7 @@ def __init__(self, *args, dataset_tags=None, **kwargs): super().__init__(*args, **kwargs) self.dataset_tags = dataset_tags self.optimizer = None + self.model_accepts_loss_kwargs = False def create_optimizer(self): if self.args.loraplus_lr_ratio is None: diff --git a/src/axolotl/monkeypatch/trainer_grad_accum.py b/src/axolotl/monkeypatch/trainer_grad_accum.py deleted file mode 100644 index 05d7067047..0000000000 --- a/src/axolotl/monkeypatch/trainer_grad_accum.py +++ /dev/null @@ -1,308 +0,0 @@ -""" -fix for FSDP gradient accumulation -see https://github.com/huggingface/transformers/pull/35128 -""" -import inspect -import logging - -from transformers import LlamaForCausalLM, Trainer -from transformers.modeling_flash_attention_utils import _flash_attention_forward - -from axolotl.monkeypatch.utils import detab_code - -LOG = logging.getLogger("axolotl.monkeypatch.trainer_grad_accum") - -ORIGINAL_CONTEXT_CODE = """ - with self.compute_loss_context_manager(): - loss = self.compute_loss(model, inputs, num_items_in_batch=num_items_in_batch) -""" - -PATCHED_CONTEXT_CODE = """ - with self.compute_loss_context_manager(): - if self.model_accepts_loss_kwargs: - loss = self.compute_loss(model, inputs, num_items_in_batch=num_items_in_batch) - else: - loss = self.compute_loss(model, inputs) -""" - -ORIGINAL_LLAMA_FCLM_CODE = """ - output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions - output_hidden_states = ( - output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states - ) - return_dict = return_dict if return_dict is not None else self.config.use_return_dict - - # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) - outputs = self.model( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - **kwargs, - ) - - hidden_states = outputs[0] - # Only compute necessary logits, and do not upcast them to float if we are not computing the loss - logits = self.lm_head(hidden_states[:, -num_logits_to_keep:, :]) - - loss = None - if labels is not None: - loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.vocab_size, **kwargs) -""" - -PATCHED_LLAMA_FCLM_CODE = """ - output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions - output_hidden_states = ( - output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states - ) - return_dict = return_dict if return_dict is not None else self.config.use_return_dict - - # remove num_items_in_batch otherwise self.model attempts to pass it to flash_attention - num_items_in_batch = kwargs.pop("num_items_in_batch", None) - - # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) - outputs = self.model( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - **kwargs, - ) - hidden_states = outputs[0] - # Only compute necessary logits, and do not upcast them to float if we are not computing the loss - logits = self.lm_head(hidden_states[:, -num_logits_to_keep:, :]) - - loss = None - if labels is not None: - loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.vocab_size, num_items_in_batch=num_items_in_batch, **kwargs) -""" - - -def get_training_step_code() -> str: - training_step = inspect.getsource( - Trainer.training_step # pylint: disable=protected-access - ) - return training_step - - -def check_training_step_is_patchable() -> bool: - training_step = get_training_step_code() - training_step, _ = detab_code(training_step) - return ORIGINAL_CONTEXT_CODE in training_step - - -def patch_training_step_for_ga(): - """ - monkeypatch for fixing the training loop for gradient accumulation - """ - - try: - training_step = get_training_step_code() - except OSError: - return - Trainer._original_training_step = training_step # pylint: disable=protected-access - training_step, _ = detab_code(training_step) - if ORIGINAL_CONTEXT_CODE not in training_step: - return - # assert ( - # ORIGINAL_CONTEXT_CODE in training_step - # ), "Original training_step code not found" - - training_step = training_step.replace(ORIGINAL_CONTEXT_CODE, PATCHED_CONTEXT_CODE) - training_step = training_step.replace( - "def training_step(", - "def _fixed_training_step(", - 1, - ) - - # load imports necessary - import transformers.trainer - - items_to_import = [] - for item in dir(transformers.trainer): - if item in training_step: - items_to_import.append(item) - - exec( # pylint: disable=exec-used # nosec B102 - "from transformers.trainer import (" - + ", ".join(x for x in items_to_import) - + ")", - globals(), - ) - exec(training_step, globals()) # pylint: disable=exec-used # nosec B102 - LOG.info("patching training_step") - Trainer.training_step = ( # pylint: disable=protected-access - _fixed_training_step # pylint: disable=undefined-variable # noqa: F821 - ) - - -def get_model_forward_code() -> str: - forward = inspect.getsource( - LlamaForCausalLM.forward # pylint: disable=protected-access - ) - return forward - - -def check_forward_is_patchable() -> bool: - forward = get_model_forward_code() - forward, _ = detab_code(forward) - return ORIGINAL_LLAMA_FCLM_CODE in forward - - -def patch_forward_for_ga(): - """ - monkeypatch for fixing the training loop for gradient accumulation - """ - - try: - forward = get_model_forward_code() - except OSError: - return - LlamaForCausalLM._original_forward = forward # pylint: disable=protected-access - forward, _ = detab_code(forward) - if ORIGINAL_LLAMA_FCLM_CODE not in forward: - return - # assert ORIGINAL_LLAMA_FCLM_CODE in forward, "Original forward code not found" - - forward = forward.replace(ORIGINAL_LLAMA_FCLM_CODE, PATCHED_LLAMA_FCLM_CODE) - forward = forward.replace( - "def forward(", - "def _fixed_forward(", - 1, - ) - - # load imports necessary - import transformers.models.llama.modeling_llama - - items_to_import = [] - for item in dir(transformers.models.llama.modeling_llama): - if item in forward: - items_to_import.append(item) - - exec( # pylint: disable=exec-used # nosec B102 - "from transformers.models.llama.modeling_llama import (" - + ", ".join(x for x in items_to_import) - + ")", - globals(), - ) - exec(forward, globals()) # pylint: disable=exec-used # nosec B102 - LOG.info("patching forward") - LlamaForCausalLM.forward = ( # pylint: disable=protected-access - _fixed_forward # pylint: disable=undefined-variable # noqa: F821 - ) - - -ORIGINAL_TRAINER_CODE = """ - context = ( - functools.partial(self.accelerator.no_sync, model=model) - if i != len(batch_samples) - 1 - else contextlib.nullcontext - ) - with context(): - tr_loss_step = self.training_step(model, inputs, num_items_in_batch) -""" - -PATCHED_TRAINER_CODE = """ - disable_deepspeed_no_sync = ( - self.accelerator.distributed_type == DistributedType.DEEPSPEED - # and self.accelerator.deepspeed_engine_wrapped.engine.zero_optimization_partition_gradients() - ) - context = ( - functools.partial(self.accelerator.no_sync, model=model) - if i != len(batch_samples) - 1 and not disable_deepspeed_no_sync - else contextlib.nullcontext - ) - with context(): - tr_loss_step = self.training_step(model, inputs, num_items_in_batch) -""" - - -def get_training_loop_code() -> str: - training_loop = inspect.getsource( - Trainer._inner_training_loop # pylint: disable=protected-access - ) - return training_loop - - -def check_training_loop_is_patchable() -> bool: - training_loop = get_training_loop_code() - training_loop, _ = detab_code(training_loop) - return ORIGINAL_TRAINER_CODE in training_loop - - -def patch_training_loop_for_deepspeed_0_16_x(): - """ - monkeypatch for fixing the training loop for deepspeed GA - - see https://github.com/huggingface/transformers/pull/35157 - """ - - try: - training_loop = get_training_loop_code() - except OSError: - return - Trainer._original_inner_training_loop = ( # pylint: disable=protected-access - training_loop - ) - training_loop, _ = detab_code(training_loop) - if ORIGINAL_TRAINER_CODE not in training_loop: - return - - training_loop = training_loop.replace(ORIGINAL_TRAINER_CODE, PATCHED_TRAINER_CODE) - training_loop = training_loop.replace( - "def _inner_training_loop(", - "def _fixed_inner_training_loop(", - 1, - ) - - # load imports necessary - import transformers.trainer - - items_to_import = [] - for item in dir(transformers.trainer): - if item in training_loop: - items_to_import.append(item) - - exec( # pylint: disable=exec-used # nosec B102 - "from transformers.trainer import (" - + ", ".join(x for x in items_to_import) - + ")", - globals(), - ) - exec(training_loop, globals()) # pylint: disable=exec-used # nosec B102 - LOG.info("patching _inner_training_loop for fsdp optimizer save") - Trainer._inner_training_loop = ( # pylint: disable=protected-access - _fixed_inner_training_loop # pylint: disable=undefined-variable # noqa: F821 - ) - - -def patch_flash_attention_forward(): - """ - monkeypatch for fixing the forward pass for flash attention to ignore num_items_in_batch - """ - - import transformers.modeling_flash_attention_utils - - def proxy_flash_attention_forward(*args, **kwargs): - kwargs.pop("num_items_in_batch", None) - - return _flash_attention_forward(*args, **kwargs) - - transformers.modeling_flash_attention_utils._flash_attention_forward = ( # pylint: disable=protected-access - proxy_flash_attention_forward - ) - transformers.models.llama.modeling_llama._flash_attention_forward = ( # pylint: disable=protected-access - proxy_flash_attention_forward - ) diff --git a/src/axolotl/monkeypatch/transformers_fa_utils.py b/src/axolotl/monkeypatch/transformers_fa_utils.py new file mode 100644 index 0000000000..f34ecb8c07 --- /dev/null +++ b/src/axolotl/monkeypatch/transformers_fa_utils.py @@ -0,0 +1,67 @@ +""" +see https://github.com/huggingface/transformers/pull/35834 +""" + +import logging +from functools import partial +from typing import Optional + +import torch + +logger = logging.getLogger(__name__) + + +def fixed_fa_peft_integration_check( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + target_dtype: Optional[torch.dtype] = None, + preferred_dtype: Optional[torch.dtype] = None, +): + """ + PEFT usually casts the layer norms in float32 for training stability reasons + therefore the input hidden states gets silently casted in float32. Hence, we need + cast them back in float16 / bfloat16 just to be sure everything works as expected. + This might slowdown training & inference so it is recommended to not cast the LayerNorms! + + Args: + query (`torch.Tensor`): + Input query states to be passed to Flash Attention API + key (`torch.Tensor`): + Input key states to be passed to Flash Attention API + value (`torch.Tensor`): + Input value states to be passed to Flash Attention API + target_dtype (`torch.dtype`, *optional*): + The dtype to convert the attention tensors to. Conversion can be ignored by + not providing the target dtype. + preferred_dtype (`torch.dtype`, *optional*): + The preferred dtype to convert the attention tensors to regardless of the + target dtype. + """ + if target_dtype is None and preferred_dtype is None: + return query, key, value + + if preferred_dtype and target_dtype != preferred_dtype: + target_dtype = preferred_dtype + + # check if any of query, key, or value are in float32. If so, cast them back to target dtype. + if any(module.dtype == torch.float32 for module in [query, key, value]): + logger.warning_once( + f"The input hidden states seems to be silently casted in float32, this might be related to" + f" the fact you have upcasted embedding or layer norm layers in float32. We will cast back the input in" + f" {target_dtype}." + ) + + query = query.to(target_dtype) + key = key.to(target_dtype) + value = value.to(target_dtype) + + return query, key, value + + +def patch_fa_peft_integration(): + import transformers.modeling_flash_attention_utils + + transformers.modeling_flash_attention_utils.fa_peft_integration_check = partial( + fixed_fa_peft_integration_check, preferred_dtype=None + ) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 4a665c111c..c4b8f05b98 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -380,23 +380,19 @@ def apply_patches(self) -> None: plugin_manager = PluginManager.get_instance() plugin_manager.pre_model_load(self.cfg) + if self.cfg.adapter: + from axolotl.monkeypatch.transformers_fa_utils import ( + patch_fa_peft_integration, + ) + + patch_fa_peft_integration() + if self.cfg.gradient_checkpointing == "unsloth": transformers.modeling_utils.checkpoint = hf_grad_checkpoint_unsloth_wrapper if self.cfg.flash_attention: self.patch_attention() - if self.cfg.model_config_type == "llama": - from axolotl.monkeypatch.trainer_grad_accum import ( - patch_flash_attention_forward, - patch_forward_for_ga, - patch_training_step_for_ga, - ) - - patch_flash_attention_forward() - patch_forward_for_ga() - patch_training_step_for_ga() - if self.cfg.sample_packing and self.cfg.s2_attention: raise ValueError( "Received `sample_packing=true` and `s2_attention=true`; however, \ diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 7135ad805e..bdbd995870 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -63,6 +63,7 @@ def test_lora_ddp(self, temp_dir): "lr_scheduler": "cosine", "flash_attention": True, "use_tensorboard": True, + "bf16": True, } ) @@ -127,6 +128,7 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): "lr_scheduler": "cosine", "flash_attention": True, "use_tensorboard": True, + "bf16": True, } ) @@ -201,6 +203,7 @@ def test_dpo_lora_ddp(self, temp_dir): "lr_scheduler": "cosine", "flash_attention": True, "use_tensorboard": True, + "bf16": True, } ) @@ -223,8 +226,12 @@ def test_dpo_lora_ddp(self, temp_dir): ] ) + loss_threshold = 2.3 check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + temp_dir + "/runs", + "train/train_loss", + loss_threshold, + "Train Loss is too high", ) def test_dpo_qlora_ddp(self, temp_dir): @@ -275,6 +282,7 @@ def test_dpo_qlora_ddp(self, temp_dir): "lr_scheduler": "cosine", "flash_attention": True, "use_tensorboard": True, + "bf16": True, } ) @@ -297,8 +305,12 @@ def test_dpo_qlora_ddp(self, temp_dir): ] ) + loss_threshold = 2.3 check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + temp_dir + "/runs", + "train/train_loss", + loss_threshold, + "Train Loss is too high", ) @pytest.mark.parametrize( diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index 156dac7e8a..8746c923b0 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -102,9 +102,5 @@ def test_ft(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - model, _ = train(cfg=cfg, dataset_meta=dataset_meta) - assert ( - "MixtralFlashAttention2" - in model.model.layers[0].self_attn.__class__.__name__ - ) + train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_model_patches.py b/tests/e2e/patched/test_model_patches.py index 78b01be645..c6a13af19b 100644 --- a/tests/e2e/patched/test_model_patches.py +++ b/tests/e2e/patched/test_model_patches.py @@ -49,12 +49,7 @@ def test_mixtral_multipack(self, temp_dir): ) normalize_config(cfg) tokenizer = load_tokenizer(cfg) - model, _ = load_model(cfg, tokenizer, inference=False) - - assert ( - "MixtralFlashAttention2" - in model.model.layers[0].self_attn.__class__.__name__ - ) + load_model(cfg, tokenizer, inference=False) @with_temp_dir def test_mistral_multipack(self, temp_dir): diff --git a/tests/e2e/patched/test_unsloth_integration.py b/tests/e2e/patched/test_unsloth_integration.py index bc6476dab6..403d261478 100644 --- a/tests/e2e/patched/test_unsloth_integration.py +++ b/tests/e2e/patched/test_unsloth_integration.py @@ -3,8 +3,6 @@ import pytest -from axolotl.monkeypatch.unsloth_ import check_self_attn_is_patchable - @pytest.mark.skip( reason="Unsloth integration will be broken going into latest transformers" @@ -13,6 +11,8 @@ class TestUnslothIntegration(unittest.TestCase): """Unsloth monkeypatch integration tests.""" def test_is_self_attn_patchable(self): + from axolotl.monkeypatch.unsloth_ import check_self_attn_is_patchable + # ensures the current version of transformers has loss code that matches our patching code self.assertTrue( check_self_attn_is_patchable(), diff --git a/tests/e2e/solo/__init__.py b/tests/e2e/solo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/test_relora_llama.py b/tests/e2e/solo/test_relora_llama.py similarity index 97% rename from tests/e2e/test_relora_llama.py rename to tests/e2e/solo/test_relora_llama.py index 6c785dc861..191f76f647 100644 --- a/tests/e2e/test_relora_llama.py +++ b/tests/e2e/solo/test_relora_llama.py @@ -13,7 +13,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import check_model_output_exists, check_tensorboard, with_temp_dir +from ..utils import check_model_output_exists, check_tensorboard, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/patched/test_llama_trainer_ga.py b/tests/patched/test_llama_trainer_ga.py deleted file mode 100644 index 58c229cf34..0000000000 --- a/tests/patched/test_llama_trainer_ga.py +++ /dev/null @@ -1,25 +0,0 @@ -""""Test module for checking whether the Hugging Face Transformers is working as expected.""" -import unittest - -from axolotl.monkeypatch.trainer_grad_accum import ( - check_forward_is_patchable, - check_training_step_is_patchable, -) - - -class TestTrainerGAIntegration(unittest.TestCase): - """llama monkeypatch integration tests.""" - - def test_train_step_patchable(self): - # ensures the current version of transformers has loss code that matches our patching code - self.assertTrue( - check_training_step_is_patchable(), - "HF transformers Trainer.training_step has changed and isn't patchable", - ) - - def test_model_forward_patchable(self): - # ensures the current version of transformers has loss code that matches our patching code - self.assertTrue( - check_forward_is_patchable(), - "HF transformers LlamaForCausalLM.forward has changed and isn't patchable", - ) From 74f9782fc38884b53c444594da87fdb182a139df Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 24 Jan 2025 22:05:58 +0700 Subject: [PATCH 0381/1405] chore(doc): fix explanation on gcs creds retrieval (#2272) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6afcaf1469..ff77b982e8 100644 --- a/README.md +++ b/README.md @@ -519,8 +519,8 @@ See [examples](examples) for quick start. It is recommended to duplicate and mod train_on_split: validation # loading from s3 or gcs - # s3 creds will be loaded from the system default and gcs only supports public access - - path: s3://path_to_ds # Accepts folder with arrow/parquet or file path like above. Supports s3, gcs. + # s3 creds will be loaded from the system default / gcs will attempt to load from gcloud creds, google metadata service, or anon + - path: s3://path_to_ds # Accepts folder with arrow/parquet or file path like above ... # Loading Data From a Public URL From b2774af66c64fe07e50d648c08b1446629f0da85 Mon Sep 17 00:00:00 2001 From: mashdragon <122402293+mashdragon@users.noreply.github.com> Date: Fri, 24 Jan 2025 15:06:50 +0000 Subject: [PATCH 0382/1405] Take `split` param from config in all load_dataset instances (#2281) --- src/axolotl/utils/data/shared.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index d14496d965..e4f31a1843 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -107,6 +107,13 @@ def load_dataset_w_config(config_dataset, auth_token): except (FileNotFoundError, ConnectionError): pass + # gather extra args from the config + load_ds_kwargs = {} + if config_dataset.split: + load_ds_kwargs["split"] = config_dataset.split + else: + load_ds_kwargs["split"] = None + # prefer local dataset, even if hub exists local_path = Path(config_dataset.path) if local_path.exists(): @@ -118,7 +125,7 @@ def load_dataset_w_config(config_dataset, auth_token): name=config_dataset.name, data_files=config_dataset.data_files, streaming=False, - split=None, + **load_ds_kwargs, ) else: try: @@ -130,7 +137,7 @@ def load_dataset_w_config(config_dataset, auth_token): config_dataset.path, name=config_dataset.name, streaming=False, - split=None, + **load_ds_kwargs, ) elif local_path.is_file(): ds_type = get_ds_type(config_dataset) @@ -140,16 +147,13 @@ def load_dataset_w_config(config_dataset, auth_token): name=config_dataset.name, data_files=config_dataset.path, streaming=False, - split=None, + **load_ds_kwargs, ) else: raise ValueError( "unhandled dataset load: local path exists, but is neither a directory or a file" ) elif ds_from_hub: - load_ds_kwargs = {} - if config_dataset.split: - load_ds_kwargs["split"] = config_dataset.split ds = load_dataset( config_dataset.path, name=config_dataset.name, @@ -173,9 +177,9 @@ def load_dataset_w_config(config_dataset, auth_token): name=config_dataset.name, data_files=config_dataset.path, streaming=False, - split=None, storage_options=storage_options, trust_remote_code=config_dataset.trust_remote_code, + **load_ds_kwargs, ) elif config_dataset.path.startswith("https://"): ds_type = get_ds_type(config_dataset) @@ -184,9 +188,9 @@ def load_dataset_w_config(config_dataset, auth_token): name=config_dataset.name, data_files=config_dataset.path, streaming=False, - split=None, storage_options=storage_options, trust_remote_code=config_dataset.trust_remote_code, + **load_ds_kwargs, ) else: if isinstance(config_dataset.data_files, str): @@ -214,7 +218,7 @@ def load_dataset_w_config(config_dataset, auth_token): name=config_dataset.name, data_files=fp, streaming=False, - split=None, + **load_ds_kwargs, ) if not ds: raise ValueError("unhandled dataset load") From 60861624881ab1e70579a100a8138fcda9aef0fb Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 24 Jan 2025 22:07:02 +0700 Subject: [PATCH 0383/1405] chore(doc): improve explanation for *_steps and *_strategy (#2270) --- docs/config.qmd | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index 179ee9ed19..f253decbe5 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -360,10 +360,11 @@ warmup_ratio: 0.05 # cannot use with warmup_steps learning_rate: 0.00003 lr_quadratic_warmup: logging_steps: -eval_steps: # Leave empty to eval at each epoch, integers for every N steps. decimal for fraction of total steps +eval_steps: # Leave empty to eval at each epoch, integer for every N steps. float for fraction of total steps evals_per_epoch: # number of times per epoch to run evals, mutually exclusive with eval_steps -save_strategy: # Set to `"no"` to skip checkpoint saves -save_steps: # Leave empty to save at each epoch +eval_strategy: # Set to `"no"` to skip evaluation, `"epoch"` at end of each epoch, leave empty to infer from `eval_steps`. +save_strategy: # Set to `"no"` to skip checkpoint saves, `"epoch"` at end of each epoch, `"best"` when better result is achieved, leave empty to infer from `save_steps`. +save_steps: # Leave empty to save at each epoch, integer for every N steps. float for fraction of total steps saves_per_epoch: # number of times per epoch to save a checkpoint, mutually exclusive with save_steps save_total_limit: # Checkpoints saved at a time # Maximum number of iterations to train for. It precedes num_epochs which means that From 20620771f1002b55438eeeb941ca6bb76216b8da Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 24 Jan 2025 12:55:20 -0500 Subject: [PATCH 0384/1405] Pretrain multipack (#2278) * fix for pretrain with packing * fix model name and loss expected * make sure to check with micro batch size for pretraining * change loss threshholds based on parametrization * make tests smaller for CI * fix pretrain packing * fix pretrain packing test * address pr feedback --- src/axolotl/core/trainer_builder.py | 2 ++ src/axolotl/utils/data/pretraining.py | 13 +++++------ src/axolotl/utils/trainer.py | 7 ++++-- tests/e2e/test_llama_pretrain.py | 32 ++++++++++++++++++++------- tests/test_packed_pretraining.py | 9 +++++--- 5 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index edc842994c..62c6a97214 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1880,6 +1880,8 @@ def build_collator( if training_args.pretraining: if self.cfg.pretraining_sample_concatenation is False: return DataCollatorForSeq2Seq(self.tokenizer, **kwargs) + if self.cfg.micro_batch_size > 1: + return DataCollatorForSeq2Seq(self.tokenizer, **kwargs) return None if self.cfg.model_config_type == "mamba": diff --git a/src/axolotl/utils/data/pretraining.py b/src/axolotl/utils/data/pretraining.py index c30d625751..f20ced221a 100644 --- a/src/axolotl/utils/data/pretraining.py +++ b/src/axolotl/utils/data/pretraining.py @@ -191,7 +191,7 @@ def wrap_pretraining_dataset( tokenizer, return_tensors="pt", padding=True, - pad_to_multiple_of=max_tokens * batch_size, + pad_to_multiple_of=max_tokens, multipack_attn=cfg.pretrain_multipack_attn, ) encode = functools.partial( @@ -201,8 +201,6 @@ def wrap_pretraining_dataset( max_seq_length=max_tokens, batch_size=batch_size, multipack_attn=cfg.pretrain_multipack_attn, - group_size=cfg.sample_packing_group_size, - bin_size=cfg.sample_packing_bin_size, ) # set this to 1 so downstream data_loader doesn't try to increase the batch again cfg.micro_batch_size = 1 @@ -247,9 +245,7 @@ def encode_packed_pretraining( examples: Dict[str, List], max_seq_length: int = 2048, batch_size: int = 4, - multipack_attn: Optional[bool] = False, - group_size: int = 100000, - bin_size: int = 200, + multipack_attn: Optional[bool] = True, ) -> Dict[str, List]: # pylint: disable=duplicate-code # tokenize all the examples @@ -260,6 +256,9 @@ def encode_packed_pretraining( train_dataset, max_seq_length, skip_position_ids=not multipack_attn, + # FIXME using attention mask unpad/pad with trainer and packed pretraining is broken atm + # workaround by using the position id logic for now in trainer + drop_attention_mask=multipack_attn, ) sampler = MultipackBatchSampler( @@ -267,8 +266,6 @@ def encode_packed_pretraining( lengths=get_dataset_lengths(train_dataset), batch_size=1, batch_max_len=batch_size * max_seq_length, - group_size=group_size, - bin_size=bin_size, drop_last=True, ) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 34b505ff14..bfd21703d1 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -310,19 +310,22 @@ def drop_no_trainable_tokens(sample): def process_pretraining_datasets_for_packing( - train_dataset, sequence_len, skip_position_ids=True + train_dataset, sequence_len, skip_position_ids=True, drop_attention_mask=False ): drop_long = partial(drop_long_seq, sequence_len=sequence_len) train_dataset = train_dataset.filter( drop_long, desc="Dropping Long Sequences", + load_from_cache_file=False, ) - if skip_position_ids: + if not skip_position_ids: train_dataset = train_dataset.map( add_position_ids, desc="Add position_id column (Pretraining Sample Packing)", ) + if drop_attention_mask: + train_dataset = train_dataset.remove_columns("attention_mask") return train_dataset diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index 117eba25db..c1f024b872 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -13,7 +13,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import check_model_output_exists +from .utils import check_model_output_exists, check_tensorboard LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -28,19 +28,25 @@ class TestPretrainLlama: "sample_packing", [True, False], ) - def test_pretrain(self, temp_dir, sample_packing): + @pytest.mark.parametrize( + "pretrain_multipack_attn", + [True, False], + ) + def test_pretrain(self, temp_dir, sample_packing, pretrain_multipack_attn): + if not sample_packing and pretrain_multipack_attn: + return + # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", "flash_attention": True, "sequence_len": 1024, "sample_packing": sample_packing, + "pretrain_multipack_attn": pretrain_multipack_attn, + "dataset_processes": 1, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "pretraining_dataset": [ { @@ -51,7 +57,7 @@ def test_pretrain(self, temp_dir, sample_packing): ], "max_steps": 5, "num_epochs": 1, - "micro_batch_size": 1, + "micro_batch_size": 2, "gradient_accumulation_steps": 1, "val_set_size": 0.0, "output_dir": temp_dir, @@ -60,6 +66,7 @@ def test_pretrain(self, temp_dir, sample_packing): "lr_scheduler": "cosine", "save_safetensors": True, "bf16": "auto", + "use_tensorboard": True, } ) normalize_config(cfg) @@ -68,3 +75,12 @@ def test_pretrain(self, temp_dir, sample_packing): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + loss_threshold = 3.5 + if sample_packing and not pretrain_multipack_attn: + loss_threshold = 6.5 + check_tensorboard( + temp_dir + "/runs", + "train/train_loss", + loss_threshold, + "Train Loss is too high", + ) diff --git a/tests/test_packed_pretraining.py b/tests/test_packed_pretraining.py index fbb776aa51..9f9ae60fb5 100644 --- a/tests/test_packed_pretraining.py +++ b/tests/test_packed_pretraining.py @@ -41,6 +41,7 @@ def test_packing_stream_dataset(self): } ], "sample_packing": True, + "pretrain_multipack_attn": True, "pad_to_sequence_len": True, "sequence_len": 2048, "micro_batch_size": 2, @@ -87,9 +88,11 @@ def test_packing_stream_dataset(self): assert data["labels"].shape == torch.Size( [1, original_bsz * cfg.sequence_len] ) - assert data["attention_mask"].shape == torch.Size( - [1, original_bsz * cfg.sequence_len] - ) + assert "attention_mask" not in data + # FIXME add back once we fix packing unpad/pad with attention mask + # assert data["attention_mask"].shape == torch.Size( + # [1, original_bsz * cfg.sequence_len] + # ) idx += 1 From 887513285d98132142bf5db2a74eb5e0928787f1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 24 Jan 2025 12:56:28 -0500 Subject: [PATCH 0385/1405] support for custom lr groups for non-embedding modules (#2213) * support for custom lr groups for non-embedding modules invert name check for group modules include lr_groups in training args additional conditional for creating optimizer fix regular params as w weight decay fix lookup and add docs * address pr feedback --- docs/lr_groups.qmd | 29 ++++ src/axolotl/core/trainer_builder.py | 142 ++++++++++++------ .../config/models/input/v0_4_1/__init__.py | 9 ++ 3 files changed, 131 insertions(+), 49 deletions(-) create mode 100644 docs/lr_groups.qmd diff --git a/docs/lr_groups.qmd b/docs/lr_groups.qmd new file mode 100644 index 0000000000..52059016ca --- /dev/null +++ b/docs/lr_groups.qmd @@ -0,0 +1,29 @@ +--- +title: Learning Rate Groups +description: "Setting different learning rates by module name" +--- + +## Background + +Inspired by LoRA+, Axolotl allows practitioners to specify separate learning rates for each module or groups of +modules in a model. + +## Example + +```yaml +lr_groups: + - name: o_proj + modules: + - self_attn.o_proj.weight + lr: 1e-6 + - name: q_proj + modules: + - model.layers.2.self_attn.q_proj.weight + lr: 1e-5 + +learning_rate: 2e-5 +``` + +In this example, we have a default learning rate of 2e-5 across the entire model, but we have a separate learning rate +of 1e-6 for all the self attention `o_proj` modules across all layers, and a learning are of 1e-5 to the 3rd layer's +self attention `q_proj` module. diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 62c6a97214..d63a10e742 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -243,6 +243,10 @@ class AxolotlTrainingMixins: default=None, metadata={"help": "Scale the learning rate for the embedding layers."}, ) + lr_groups: Optional[list[dict]] = field( + default=None, + metadata={"help": "Specify learning rate groups for with different LRs."}, + ) embedding_lr: Optional[float] = field( default=None, metadata={"help": "absolute learning rate for the embedding layers."}, @@ -461,11 +465,95 @@ def _wrap_model(self, model, training=True, dataloader=None): ) return super()._wrap_model(model, training=training, dataloader=dataloader) + def create_optimizer_grouped_parameters(self, opt_model, optimizer_kwargs): + decay_parameters = self.get_decay_parameter_names(opt_model) + params = { + "to_weight_decay": {}, # LayerNorm and bias + "embeddings": {}, # lm_head, embed_tokens, + "no_weight_decay": {}, + } + lr_groups_lookup = {} + lr_groups_learning_rates = {} + if self.args.lr_groups: + for lr_group in self.args.lr_groups: + group_name = lr_group["name"] + group_modules = lr_group["modules"] + for module in group_modules: + lr_groups_lookup[module] = group_name + lr_groups_learning_rates[group_name] = lr_group["lr"] + params[f"to_weight_decay_{group_name}"] = {} + + for name, param in opt_model.named_parameters(): + if not param.requires_grad: + continue + if name.endswith("modules_to_save.default.weight") or any( + embed_name in name for embed_name in ["embed_tokens", "lm_head"] + ): + params["embeddings"][name] = param + elif name in decay_parameters: + lr_group_modules = [ + group_modules + for group_modules in lr_groups_lookup + if group_modules in name + ] + if lr_groups_lookup and any(lr_group_modules): + lr_group_module = lr_group_modules[0] + group_name = lr_groups_lookup[lr_group_module] + params[f"to_weight_decay_{group_name}"][name] = param + else: + params["to_weight_decay"][name] = param + else: + params["no_weight_decay"][name] = param + optimizer_grouped_parameters = [] + if params["to_weight_decay"]: + optimizer_grouped_parameters.append( + { + "params": list(params["to_weight_decay"].values()), + "weight_decay": self.args.weight_decay, + "lr": optimizer_kwargs["lr"], + } + ) + if params["embeddings"]: + lr = optimizer_kwargs["lr"] # pylint: disable=invalid-name + if self.args.embedding_lr_scale: + lr *= self.args.embedding_lr_scale # pylint: disable=invalid-name + elif self.args.embedding_lr: + lr = self.args.embedding_lr # pylint: disable=invalid-name + optimizer_grouped_parameters.append( + { + "params": list(params["embeddings"].values()), + "weight_decay": 0.0, + "lr": lr, + } + ) + if params["no_weight_decay"]: + optimizer_grouped_parameters.append( + { + "params": list(params["no_weight_decay"].values()), + "weight_decay": 0.0, + "lr": optimizer_kwargs["lr"], + } + ) + for group_name, group_lr in lr_groups_learning_rates.items(): + if params[f"to_weight_decay_{group_name}"]: + optimizer_grouped_parameters.append( + { + "params": list( + params[f"to_weight_decay_{group_name}"].values() + ), + "weight_decay": self.args.weight_decay, + "lr": group_lr, + } + ) + + return optimizer_grouped_parameters + def create_optimizer(self): if ( self.args.loraplus_lr_ratio is None and self.args.embedding_lr_scale is None and self.args.embedding_lr is None + and self.args.lr_groups is None and self.args.alternate_optimizer not in [ "optimi_adamw", @@ -479,59 +567,13 @@ def create_optimizer(self): opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model if self.optimizer is None: # pylint: disable=access-member-before-definition - decay_parameters = self.get_decay_parameter_names(opt_model) - params = { - "to_weight_decay": {}, # LayerNorm and bias - "embeddings": {}, # lm_head, embed_tokens, - "no_weight_decay": {}, - } - optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs( self.args, opt_model, ) - - for name, param in opt_model.named_parameters(): - if not param.requires_grad: - continue - if name.endswith("modules_to_save.default.weight") or any( - embed_name in name for embed_name in ["embed_tokens", "lm_head"] - ): - params["embeddings"][name] = param - elif name in decay_parameters: - params["to_weight_decay"][name] = param - else: - params["no_weight_decay"][name] = param - optimizer_grouped_parameters = [] - if params["to_weight_decay"]: - optimizer_grouped_parameters.append( - { - "params": list(params["to_weight_decay"].values()), - "weight_decay": self.args.weight_decay, - "lr": optimizer_kwargs["lr"], - } - ) - if params["embeddings"]: - lr = optimizer_kwargs["lr"] # pylint: disable=invalid-name - if self.args.embedding_lr_scale: - lr *= self.args.embedding_lr_scale # pylint: disable=invalid-name - elif self.args.embedding_lr: - lr = self.args.embedding_lr # pylint: disable=invalid-name - optimizer_grouped_parameters.append( - { - "params": list(params["embeddings"].values()), - "weight_decay": 0.0, - "lr": lr, - } - ) - if params["no_weight_decay"]: - optimizer_grouped_parameters.append( - { - "params": list(params["no_weight_decay"].values()), - "weight_decay": 0.0, - "lr": optimizer_kwargs["lr"], - } - ) + optimizer_grouped_parameters = self.create_optimizer_grouped_parameters( + opt_model, optimizer_kwargs + ) if self.args.loraplus_lr_ratio is not None: loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) @@ -548,6 +590,7 @@ def create_optimizer(self): elif ( self.args.embedding_lr_scale is not None or self.args.embedding_lr is not None + or self.args.lr_groups is not None ): self.optimizer = ( # pylint: disable=attribute-defined-outside-init optimizer_cls(optimizer_grouped_parameters, **optimizer_kwargs) @@ -1665,6 +1708,7 @@ def build(self, total_num_steps): ] = self.cfg.loraplus_lr_embedding training_arguments_kwargs["embedding_lr"] = self.cfg.embedding_lr training_arguments_kwargs["embedding_lr_scale"] = self.cfg.embedding_lr_scale + training_arguments_kwargs["lr_groups"] = self.cfg.lr_groups if self.cfg.lr_scheduler in ["one_cycle", "log_sweep"]: training_arguments_kwargs["lr_scheduler_type"] = "cosine" diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 98cdee009e..44e2478865 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -147,6 +147,14 @@ class UserDefinedPrompterType(BaseModel): field: Optional[str] = None +class LrGroup(BaseModel): + """Custom learning rate group configuration""" + + name: str + modules: List[str] + lr: float + + class SFTDataset(BaseModel): """SFT configuration subset""" @@ -475,6 +483,7 @@ class HyperparametersConfig(BaseModel): cosine_min_lr_ratio: Optional[float] = None cosine_constant_lr_ratio: Optional[float] = None lr_div_factor: Optional[float] = None + lr_groups: Optional[List[LrGroup]] = None adam_epsilon: Optional[float] = None adam_beta1: Optional[float] = None From 0b52f0622739f624427b0d1cefdf115026426095 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 28 Jan 2025 23:21:25 -0500 Subject: [PATCH 0386/1405] bump bnb to 0.45.1 (#2289) [skip ci] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 52e146411d..446fa94a63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ # START section of dependencies that don't install on Darwin/MacOS -bitsandbytes==0.45.0 +bitsandbytes==0.45.1 triton>=3.0.0 mamba-ssm==1.2.0.post1 flash-attn==2.7.0.post2 From 067b4425968877102036509fbaadd7e318e0a26e Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 29 Jan 2025 11:22:10 +0700 Subject: [PATCH 0387/1405] chore: refactor SaveModelCallback to stop handle fractional save_steps (#2291) [skip ci] --- src/axolotl/utils/callbacks/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index f1b459b6b3..d92cb9d992 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -4,7 +4,6 @@ import gc import logging -import math import os import traceback from shutil import copyfile @@ -830,13 +829,6 @@ def on_step_end( # pylint: disable=unused-argument # Save if state.global_step >= state.max_steps: control.should_save = True - elif ( - args.save_strategy == IntervalStrategy.STEPS - and state.save_steps < 1.0 - and state.global_step % math.ceil(state.save_steps * state.max_steps) == 0 - ): - # workaround to save model on fractional save_steps - control.should_save = True def on_train_end( # pylint: disable=unused-argument self, args, state, control, **kwargs From c015a76a237c143281557008decbe86350c5a758 Mon Sep 17 00:00:00 2001 From: mashdragon <122402293+mashdragon@users.noreply.github.com> Date: Wed, 29 Jan 2025 04:23:26 +0000 Subject: [PATCH 0388/1405] Num epochs float (#2282) [skip ci] * Change num_epochs type to float * Handle float value for num_epochs in trainer.py --- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 2 +- src/axolotl/utils/trainer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 44e2478865..e5edf8e7b8 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -489,7 +489,7 @@ class HyperparametersConfig(BaseModel): adam_beta1: Optional[float] = None adam_beta2: Optional[float] = None max_grad_norm: Optional[float] = None - num_epochs: int = Field(default=1) + num_epochs: float = Field(default=1.0) @field_validator("batch_size") @classmethod diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index bfd21703d1..caa74fccc8 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -374,7 +374,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): if cfg.sample_packing_eff_est: total_num_steps = ( # match count to len est in dataloader - ( + int( math.floor( 0.99 * cfg.total_num_tokens From c071a530f727199701c1d204dc8f9eccb9feb99c Mon Sep 17 00:00:00 2001 From: salman Date: Wed, 29 Jan 2025 04:23:44 +0000 Subject: [PATCH 0389/1405] removing 2.3.1 (#2294) --- .github/workflows/base.yml | 12 ----------- .github/workflows/main.yml | 22 +------------------- .github/workflows/multi-gpu-e2e.yml | 6 ------ .github/workflows/nightlies.yml | 22 -------------------- .github/workflows/tests-nightly.yml | 9 +------- .github/workflows/tests.yml | 8 +------ README.md | 2 +- cicd/multigpu.py | 4 ++-- cicd/tests.py | 4 ++-- setup.py | 20 +----------------- tests/e2e/patched/test_4d_multipack_llama.py | 3 +-- tests/e2e/utils.py | 12 ----------- 12 files changed, 10 insertions(+), 114 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 640d2cd7ae..0f4b3c9cea 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -22,18 +22,6 @@ jobs: fail-fast: false matrix: include: - - cuda: "121" - cuda_version: 12.1.1 - cudnn_version: 8 - python_version: "3.10" - pytorch: 2.3.1 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - - cuda: "121" - cuda_version: 12.1.1 - cudnn_version: 8 - python_version: "3.11" - pytorch: 2.3.1 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "124" cuda_version: 12.4.1 cudnn_version: "" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 89b2746e4a..4f8074ad1c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,16 +15,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.10" - pytorch: 2.3.1 - axolotl_extras: mamba-ssm - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.11" - pytorch: 2.3.1 - axolotl_extras: mamba-ssm - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -82,16 +72,6 @@ jobs: strategy: matrix: include: - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.10" - pytorch: 2.3.1 - axolotl_extras: - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.11" - pytorch: 2.3.1 - axolotl_extras: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -148,7 +128,7 @@ jobs: - cuda: 121 cuda_version: 12.1.1 python_version: "3.11" - pytorch: 2.3.1 + pytorch: 2.4.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 1c6702760b..c3bcc517bb 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -20,12 +20,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.11" - pytorch: 2.3.1 - axolotl_extras: - num_gpus: 2 - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index e266122c68..0efeb80b97 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -12,17 +12,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.10" - pytorch: 2.3.1 - axolotl_extras: - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.11" - pytorch: 2.3.1 - axolotl_extras: - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -76,17 +65,6 @@ jobs: strategy: matrix: include: - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.10" - pytorch: 2.3.1 - axolotl_extras: - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.11" - pytorch: 2.3.1 - axolotl_extras: - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index bbed4e2c2b..178b0a4d19 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -26,7 +26,7 @@ jobs: max-parallel: 2 matrix: python_version: ["3.10", "3.11"] - pytorch_version: ["2.3.1", "2.4.1", "2.5.1"] + pytorch_version: ["2.4.1", "2.5.1"] exclude: - python_version: "3.10" pytorch_version: "2.4.1" @@ -98,13 +98,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.10" - pytorch: 2.3.1 - num_gpus: 1 - axolotl_extras: mamba-ssm - nightly_build: "true" - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a2a0e801e2..87d532a3ba 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,7 +49,7 @@ jobs: max-parallel: 2 matrix: python_version: ["3.10", "3.11"] - pytorch_version: ["2.3.1", "2.4.1", "2.5.1"] + pytorch_version: ["2.4.1", "2.5.1"] exclude: - python_version: "3.10" pytorch_version: "2.4.1" @@ -244,12 +244,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 121 - cuda_version: 12.1.1 - python_version: "3.10" - pytorch: 2.3.1 - num_gpus: 1 - axolotl_extras: mamba-ssm - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/README.md b/README.md index ff77b982e8..6ee3237e7b 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Features: Get started with Axolotl in just a few steps! This quickstart guide will walk you through setting up and running a basic fine-tuning task. -**Requirements**: *Nvidia* GPU (Ampere architecture or newer for `bf16` and Flash Attention) or *AMD* GPU, Python >=3.10 and PyTorch >=2.3.1. +**Requirements**: *Nvidia* GPU (Ampere architecture or newer for `bf16` and Flash Attention) or *AMD* GPU, Python >=3.10 and PyTorch >=2.4.1. ```bash pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] diff --git a/cicd/multigpu.py b/cicd/multigpu.py index f9bad386a3..2c0863034e 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -23,8 +23,8 @@ df_args = { "AXOLOTL_EXTRAS": os.environ.get("AXOLOTL_EXTRAS", ""), "AXOLOTL_ARGS": os.environ.get("AXOLOTL_ARGS", ""), - "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.3.1"), - "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu121-2.3.1"), + "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.4.1"), + "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu121-2.4.1"), "CUDA": os.environ.get("CUDA", "121"), "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), diff --git a/cicd/tests.py b/cicd/tests.py index d7ae5b5e8c..616554e646 100644 --- a/cicd/tests.py +++ b/cicd/tests.py @@ -23,8 +23,8 @@ df_args = { "AXOLOTL_EXTRAS": os.environ.get("AXOLOTL_EXTRAS", ""), "AXOLOTL_ARGS": os.environ.get("AXOLOTL_ARGS", ""), - "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.3.1"), - "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu121-2.3.1"), + "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.4.1"), + "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu121-2.4.1"), "CUDA": os.environ.get("CUDA", "121"), "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), diff --git a/setup.py b/setup.py index d7cb18ec01..ac0c96defb 100644 --- a/setup.py +++ b/setup.py @@ -32,8 +32,6 @@ def parse_requirements(): _install_requires.append(line) try: xformers_version = [req for req in _install_requires if "xformers" in req][0] - triton_version = [req for req in _install_requires if "triton" in req][0] - torchao_version = [req for req in _install_requires if "torchao" in req][0] autoawq_version = [req for req in _install_requires if "autoawq" in req][0] if "Darwin" in platform.system(): # skip packages not compatible with OSX @@ -87,24 +85,8 @@ def parse_requirements(): else: _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers==0.0.28.post1") - elif (major, minor) >= (2, 3): - _install_requires.pop(_install_requires.index(torchao_version)) - _install_requires.pop(_install_requires.index(triton_version)) - _install_requires.append("triton>=2.3.1") - if patch == 0: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.26.post1") - else: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.27") - elif (major, minor) >= (2, 2): - _install_requires.pop(_install_requires.index(torchao_version)) - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.25.post1") else: - _install_requires.pop(_install_requires.index(torchao_version)) - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.23.post1") + raise ValueError("axolotl requires torch>=2.4") except PackageNotFoundError: pass diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index da27069ac6..af8eb37427 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -12,7 +12,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from ..utils import check_model_output_exists, require_torch_2_3_1, with_temp_dir +from ..utils import check_model_output_exists, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -23,7 +23,6 @@ class Test4dMultipackLlama(unittest.TestCase): Test case for Llama models using 4d attention with multipack """ - @require_torch_2_3_1 @with_temp_dir def test_sdp_lora_packing(self, temp_dir): # pylint: disable=duplicate-code diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 759d596596..de0dba33a3 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -42,18 +42,6 @@ def most_recent_subdir(path): return subdir -def require_torch_2_3_1(test_case): - """ - Decorator marking a test that requires torch >= 2.3.1 - """ - - def is_min_2_3_1(): - torch_version = version.parse(torch.__version__) - return torch_version >= version.parse("2.3.1") - - return unittest.skipUnless(is_min_2_3_1(), "test requires torch>=2.3.1")(test_case) - - def require_torch_2_4_1(test_case): """ Decorator marking a test that requires torch >= 2.5.1 From 54dd7abfc11748802404d0945ed3aa47929302b7 Mon Sep 17 00:00:00 2001 From: salman Date: Wed, 29 Jan 2025 05:08:33 +0000 Subject: [PATCH 0390/1405] Process reward models (#2241) * adding model_cfg to set num_labels * using a num_labels field instead * linting * WIP stepwise prompt tokenizer * this should work? * trainer working? * pushing to runpod * fixing saving * updating conf * updating config, adding docs * adding stepwise supervision docpage * updating tests * adding test for dataset * fixing tests * linting * addressing some comments * adding additional cfg fields support * updating tests, fixing cfg * fixing tests * updating loss * Update test_process_reward_model_smollm2.py * updating loss values and seed * dumb pre-commit --- .pre-commit-config.yaml | 2 +- docs/config.qmd | 6 + docs/dataset-formats/stepwise_supervised.qmd | 18 +++ docs/reward_modelling.qmd | 47 +++++++ examples/gemma2/reward-model.yaml | 1 + examples/qwen2/prm.yaml | 72 +++++++++++ examples/qwen2/reward-model.yaml | 67 ++++++++++ src/axolotl/core/trainer_builder.py | 46 +++++-- src/axolotl/datasets.py | 1 + .../prompt_strategies/stepwise_supervised.py | 116 ++++++++++++++++++ src/axolotl/train.py | 2 +- .../config/models/input/v0_4_1/__init__.py | 18 ++- src/axolotl/utils/data/sft.py | 13 ++ src/axolotl/utils/models.py | 4 +- .../e2e/test_process_reward_model_smollm2.py | 69 +++++++++++ ..._llama.py => test_reward_model_smollm2.py} | 22 ++-- tests/prompt_strategies/test_stepwise.py | 63 ++++++++++ 17 files changed, 542 insertions(+), 25 deletions(-) create mode 100644 docs/dataset-formats/stepwise_supervised.qmd create mode 100644 docs/reward_modelling.qmd create mode 100644 examples/qwen2/prm.yaml create mode 100644 examples/qwen2/reward-model.yaml create mode 100644 src/axolotl/prompt_strategies/stepwise_supervised.py create mode 100644 tests/e2e/test_process_reward_model_smollm2.py rename tests/e2e/{test_reward_model_llama.py => test_reward_model_smollm2.py} (75%) create mode 100644 tests/prompt_strategies/test_stepwise.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9409b1ef16..f91b9554d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: hooks: - id: isort - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 - repo: https://github.com/PyCQA/pylint diff --git a/docs/config.qmd b/docs/config.qmd index f253decbe5..ecb571040f 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -187,6 +187,12 @@ rl: # whether to perform weighting if doing DPO training. Boolean. dpo_use_weighting: +# reward modelling: `True` or `False` +reward_model: + +# process reward modelling: `True` or `False` +process_reward_model: + # The name of the chat template to use for training, following values are supported: # - tokenizer_default: Uses the chat template that is available in the tokenizer_config.json. If the chat template is not available in the tokenizer, it will raise an error. This is the default value. # - alpaca/inst/chatml/gemma/cohere/llama3/phi_3/deepseek_v2/jamba: These chat templates are available in the axolotl codebase at src/axolotl/utils/chat_templates.py diff --git a/docs/dataset-formats/stepwise_supervised.qmd b/docs/dataset-formats/stepwise_supervised.qmd new file mode 100644 index 0000000000..17f0c9141b --- /dev/null +++ b/docs/dataset-formats/stepwise_supervised.qmd @@ -0,0 +1,18 @@ +--- +title: Stepwise Supervised Format +description: Format for datasets with stepwise completions and labels +order: 3 +--- + +## Stepwise Supervised + +The stepwise supervised format is designed for chain-of-thought (COT) reasoning datasets where each example contains multiple completion steps and a preference label for each step. +### ExampleHere's a simple example of a stepwise supervised dataset entry:```json +{ + "prompt": "Which number is larger, 9.8 or 9.11?", + "completions": [ + "The fractional part of 9.8 is 0.8, while the fractional part of 9.11 is 0.11.", + "Since 0.11 is greater than 0.8, the number 9.11 is larger than 9.8." + ], + "labels": [true, false] +} diff --git a/docs/reward_modelling.qmd b/docs/reward_modelling.qmd new file mode 100644 index 0000000000..8baa934241 --- /dev/null +++ b/docs/reward_modelling.qmd @@ -0,0 +1,47 @@ +--- +title: "Reward Modelling" +description: "Reward models are used to guide models towards behaviors which is preferred by humans, by training over large datasets annotated with human preferences. " +--- + +### Overview + +Reward modelling is a technique used to train models to predict the reward or value of a given input. This is particularly useful in reinforcement learning scenarios where the model needs to evaluate the quality of its actions or predictions. +We support the reward modelling techniques supported by `trl`. + +### (Outcome) Reward Models + +Outcome reward models are trained using data which contains preference annotations for an entire interaction between the user and model (e.g. rather than per-turn or per-step). + +```yaml +base_model: google/gemma-2-2b +model_type: AutoModelForSequenceClassification +num_labels: 1 +tokenizer_type: AutoTokenizer + +reward_model: true +chat_template: gemma +datasets: + - path: argilla/distilabel-intel-orca-dpo-pairs + type: bradley_terry.chat_template + +val_set_size: 0.1 +eval_steps: 100 +``` + +### Process Reward Models (PRM) + +Process reward models are trained using data which contains preference annotations for each step in a series of interactions. Typically, PRMs are trained to provide reward signals over each step of a reasoning trace and are used for downstream reinforcement learning. +```yaml +base_model: Qwen/Qwen2.5-3B +model_type: AutoModelForTokenClassification +num_labels: 2 + +process_reward_model: true +datasets: + - path: trl-lib/math_shepherd + type: stepwise_supervised + split: train + +val_set_size: 0.1 +eval_steps: 100 +``` diff --git a/examples/gemma2/reward-model.yaml b/examples/gemma2/reward-model.yaml index b492c6f931..ada42ec28a 100644 --- a/examples/gemma2/reward-model.yaml +++ b/examples/gemma2/reward-model.yaml @@ -1,6 +1,7 @@ base_model: google/gemma-2-2b # optionally might have model_type or tokenizer_type model_type: AutoModelForSequenceClassification +num_labels: 1 tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name diff --git a/examples/qwen2/prm.yaml b/examples/qwen2/prm.yaml new file mode 100644 index 0000000000..071e2d0f30 --- /dev/null +++ b/examples/qwen2/prm.yaml @@ -0,0 +1,72 @@ +base_model: Qwen/Qwen2.5-3B +# optionally might have model_type or tokenizer_type +model_type: AutoModelForTokenClassification +num_labels: 2 +tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +process_reward_model: true +chat_template: +datasets: + - path: trl-lib/math_shepherd + type: stepwise_supervised + step_separator: "\n" + max_completion_length: + train_on_last_step_only: false + +val_set_size: 0.2 +output_dir: ./outputs/out +remove_unused_columns: false + +sequence_len: 2048 +sample_packing: false +eval_sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 1 +micro_batch_size: 8 +eval_batch_size: 8 +num_epochs: 1 +optimizer: adamw_torch +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: true +fp16: +tf32: +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +eval_table_size: +eval_max_new_tokens: 128 +eval_steps: 100 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: diff --git a/examples/qwen2/reward-model.yaml b/examples/qwen2/reward-model.yaml new file mode 100644 index 0000000000..bbd6e66ced --- /dev/null +++ b/examples/qwen2/reward-model.yaml @@ -0,0 +1,67 @@ +base_model: Qwen/Qwen2.5-0.5B +# optionally might have model_type or tokenizer_type +model_type: AutoModelForSequenceClassification +num_labels: 1 +tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +reward_model: true +chat_template: qwen_25 +datasets: + - path: argilla/distilabel-intel-orca-dpo-pairs + type: bradley_terry.chat_template +val_set_size: 0.0 +output_dir: ./outputs/out +remove_unused_columns: false + +sequence_len: 2048 +sample_packing: false +eval_sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index d63a10e742..6bf03d78c2 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -44,6 +44,8 @@ KTOTrainer, ORPOConfig, ORPOTrainer, + PRMConfig, + PRMTrainer, RewardConfig, RewardTrainer, ) @@ -342,6 +344,13 @@ class AxolotlRewardConfig(AxolotlTrainingMixins, RewardConfig): """ +@dataclass +class AxolotlPRMConfig(AxolotlTrainingMixins, PRMConfig): + """ + PRM config for PRM training + """ + + class SchedulerMixin(Trainer): """ Mixin class for scheduler setup in CausalTrainer. @@ -1244,6 +1253,14 @@ class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): tag_names = ["axolotl", "reward"] +class AxolotlPRMTrainer(SchedulerMixin, PRMTrainer): + """ + Extend the base trl.PRMTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "prm"] + + class TrainerBuilderBase(abc.ABC): """ Base class for trainer builder @@ -1377,7 +1394,8 @@ def hook_post_create_trainer(self, trainer): class HFCausalTrainerBuilder(TrainerBuilderBase): """ - Build the HuggingFace training args/trainer for Causal models + Build the HuggingFace training args/trainer for causal models + and reward modelling using TRL. """ def get_callbacks(self): @@ -1452,6 +1470,8 @@ def _get_trainer_cls(self): return AxolotlMambaTrainer if self.cfg.reward_model: return AxolotlRewardTrainer + if self.cfg.process_reward_model: + return AxolotlPRMTrainer return AxolotlTrainer def build(self, total_num_steps): @@ -1842,11 +1862,13 @@ def build(self, total_num_steps): "accelerator_config" ] = self.cfg.accelerator_config - training_args_cls = ( - AxolotlTrainingArguments - if not self.cfg.reward_model - else AxolotlRewardConfig - ) + if self.cfg.reward_model: + training_args_cls = AxolotlRewardConfig + elif self.cfg.process_reward_model: + training_args_cls = AxolotlPRMConfig + else: + training_args_cls = AxolotlTrainingArguments + training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg **training_arguments_kwargs, ) @@ -1880,9 +1902,9 @@ def build(self, total_num_steps): if eval_data_collator := self.build_collator( training_args, is_eval=True, **data_collator_kwargs ): - if not self.cfg.reward_model: + if not (self.cfg.reward_model or self.cfg.process_reward_model): trainer_kwargs["eval_data_collator"] = eval_data_collator - if not self.cfg.reward_model: + if not (self.cfg.reward_model or self.cfg.process_reward_model): trainer_kwargs["bench_data_collator"] = transformers.DataCollatorForSeq2Seq( self.tokenizer, return_tensors="pt", @@ -1893,8 +1915,10 @@ def build(self, total_num_steps): trainer_kwargs["processing_class"] = self.tokenizer else: trainer_kwargs["tokenizer"] = self.tokenizer - - if (trainer_cls is not AxolotlRewardTrainer) and self.cfg.datasets is not None: + if ( + not (trainer_cls in [AxolotlRewardTrainer, AxolotlPRMTrainer]) + and self.cfg.datasets is not None + ): trainer_kwargs["dataset_tags"] = [ d["path"] for d in self.cfg.datasets if not Path(d["path"]).is_dir() ] @@ -1984,7 +2008,7 @@ def build_collator( class HFRLTrainerBuilder(TrainerBuilderBase): """ - Trainer factory class for DPO Trainer + Trainer factory class for TRL-based RLHF trainers (e.g. DPO) """ def get_callbacks(self): diff --git a/src/axolotl/datasets.py b/src/axolotl/datasets.py index b5638a614d..e4531930f0 100644 --- a/src/axolotl/datasets.py +++ b/src/axolotl/datasets.py @@ -52,6 +52,7 @@ def process(self, dataset): if self.prompt_tokenizer.supports_batched: map_kwargs["batched"] = True map_kwargs["batch_size"] = 100 + return dataset.map( self.prompt_tokenizer.tokenize_prompt, num_proc=num_proc, diff --git a/src/axolotl/prompt_strategies/stepwise_supervised.py b/src/axolotl/prompt_strategies/stepwise_supervised.py new file mode 100644 index 0000000000..8be7c35e36 --- /dev/null +++ b/src/axolotl/prompt_strategies/stepwise_supervised.py @@ -0,0 +1,116 @@ +""" +Module for stepwise datasets, typically including a prompt and reasoning traces, +and (optionally) per-step, or per-prompt-trace labels for reward modelling. +""" + +from itertools import chain +from typing import Dict, List, Optional, Union + +from transformers import BatchEncoding, PreTrainedTokenizer + +from axolotl.prompt_tokenizers import IGNORE_INDEX +from axolotl.utils.dict import DictDefault + + +class StepwiseSupervisedPromptTokenizingStrategy: + """ + Tokenizing strategy for supervised stepwise datasets, typically used for COT-reasoning. + These datasets should include the following columns: + - prompt: the prompt text + - completions: a list of `n` completion steps + - labels: a list of `n` labels indicating the "correctness" of each step + """ + + def __init__( + self, + tokenizer, + sequence_len: int = 2048, + step_separator: str = "\n", + max_completion_length: Optional[int] = None, + train_on_last_step_only: bool = False, + ): + self.tokenizer = tokenizer + self.sequence_len = sequence_len + self.step_separator = step_separator + self.max_completion_length = max_completion_length + self.train_on_last_step_only = train_on_last_step_only + + def tokenize_prompt( + self, prompt: Dict[str, Union[str, List[str]]] + ) -> BatchEncoding: + # Inspired by TRL's PRMTRainer + # https://github.com/huggingface/trl/blob/ed7de87dc766478c024b68f12530d1b0e7c3ff23/trl/trainer/prm_trainer.py#L206 + prompt_ids = self.tokenizer(prompt["prompt"], add_special_tokens=False)[ + "input_ids" + ] + + completions_ids = [ + self.tokenizer(completion, add_special_tokens=False)["input_ids"] + for completion in prompt["completions"] + ] + + # Handle labels + if self.train_on_last_step_only: + labels = [IGNORE_INDEX] * (len(prompt["labels"]) - 1) + [ + int(prompt["labels"][-1]) + ] + else: + labels = [int(label) for label in prompt["labels"]] + + # Add step separators + separator_ids = self.tokenizer.encode( + self.step_separator, add_special_tokens=False + ) + completions_ids = [completion + separator_ids for completion in completions_ids] + + # Create step-wise labels + labels = [ + [IGNORE_INDEX] * (len(completion) - 1) + [label] # type: ignore + for completion, label in zip(completions_ids, labels) + ] + + # Join all steps + completion_ids = list(chain(*completions_ids)) + labels = list(chain(*labels)) # type: ignore + + # Handle max lengths + if self.max_completion_length: + completion_ids = completion_ids[: self.max_completion_length] + labels = labels[: self.max_completion_length] + + # Add BOS token if model has one + if self.tokenizer.bos_token_id is not None: + prompt_ids = [self.tokenizer.bos_token_id] + prompt_ids + + # Combine prompt and completion + input_ids = prompt_ids + completion_ids + + full_labels = [IGNORE_INDEX] * len(prompt_ids) + labels + # Apply max sequence length + if self.sequence_len: + input_ids = input_ids[: self.sequence_len] + full_labels = full_labels[: self.sequence_len] + + return { + "input_ids": input_ids, + "labels": full_labels, + "attention_mask": [1] * len(input_ids), + } + + @property + def supports_batched(self): + return False + + +def load( + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + ds_cfg: DictDefault, +) -> StepwiseSupervisedPromptTokenizingStrategy: + return StepwiseSupervisedPromptTokenizingStrategy( + tokenizer, + cfg.sequence_len, + step_separator=ds_cfg.get("step_separator", "\n"), + max_completion_length=ds_cfg.max_completion_length, + train_on_last_step_only=ds_cfg.get("train_on_last_step_only", False), + ) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index b901c2a972..0bd400f6b0 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -259,7 +259,7 @@ def terminate_handler(_, __, model_weakref): .decode("utf-8") } if cfg.datasets is not None: - if cfg.rl is not None or cfg.reward_model: + if cfg.rl is not None or cfg.reward_model or cfg.process_reward_model: dataset_tags = [ d["path"] for d in cfg.datasets if not Path(d["path"]).is_dir() ] diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index e5edf8e7b8..4f0fa4c29e 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -236,6 +236,18 @@ class DPODataset(BaseModel): revision: Optional[str] = None +class StepwiseSupervisedDataset(BaseModel): + """Stepwise supervised dataset configuration subset""" + + path: Optional[str] = None + split: Optional[str] = None + data_files: Optional[List[str]] = None + revision: Optional[str] = None + step_separator: Optional[str] = None + max_completion_length: Optional[int] = None + train_on_last_step_only: Optional[bool] = None + + class UserDefinedKTOType(BaseModel): """User defined typing for KTO""" @@ -626,12 +638,14 @@ class Config: rl: Optional[RLType] = None reward_model: Optional[bool] = None + process_reward_model: Optional[bool] = None + num_labels: Optional[int] = None dpo_use_weighting: Optional[ bool ] = None # whether to use weighting in DPO trainer. If none, default is false in the trainer. - datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset], min_length=1)] = None # type: ignore - test_datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset], min_length=1)] = None # type: ignore + datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset, StepwiseSupervisedDataset], min_length=1)] = None # type: ignore + test_datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset, StepwiseSupervisedDataset], min_length=1)] = None # type: ignore shuffle_merged_datasets: Optional[bool] = True dataset_prepared_path: Optional[str] = None dataset_shard_num: Optional[int] = None diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index de373c06ea..ba5d0c54d1 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -8,6 +8,8 @@ from datasets import ( Dataset, DatasetDict, + Sequence, + Value, concatenate_datasets, load_dataset, load_from_disk, @@ -467,6 +469,17 @@ def get_dataset_wrapper( dataset, **ds_kwargs, ) + elif config_dataset.type.startswith("stepwise_supervised"): + dataset_prompter = UnsupportedPrompter() + ds_strategy = load(config_dataset.type, tokenizer, cfg, config_dataset) + # we need to explicitly cast boolean labels to int + # for compatibility with how trl's PRMTrainer works + dataset = dataset.cast_column("labels", Sequence(Value("int64"))) + dataset_wrapper = TokenizedPromptDataset( + ds_strategy, + dataset, + **ds_kwargs, + ) elif ds_strategy := load( config_dataset.type, tokenizer, cfg, config_dataset, processor=processor ): diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index c4b8f05b98..d46564f42c 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -138,7 +138,9 @@ def load_model_config(cfg): config_kwargs = {} if cfg.revision_of_model: config_kwargs["revision"] = cfg.revision_of_model - + if cfg.num_labels: + # num_labels is used to initialize classifier models + config_kwargs["num_labels"] = cfg.num_labels try: model_config = AutoConfig.from_pretrained( model_config_name, diff --git a/tests/e2e/test_process_reward_model_smollm2.py b/tests/e2e/test_process_reward_model_smollm2.py new file mode 100644 index 0000000000..16bf2cdc82 --- /dev/null +++ b/tests/e2e/test_process_reward_model_smollm2.py @@ -0,0 +1,69 @@ +""" +E2E tests for process reward model w/ lora llama +""" + +import logging +import os +import unittest + +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config +from axolotl.utils.dict import DictDefault + +from .utils import check_model_output_exists, check_tensorboard, with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestProcessRewardSmolLM2(unittest.TestCase): + """ + Test case for Llama process reward models using LoRA + """ + + @with_temp_dir + def test_prm(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "model_type": "AutoModelForTokenClassification", + "num_labels": 2, + "process_reward_model": True, + "sequence_len": 512, + "val_set_size": 0.0, + "datasets": [ + { + "path": "trl-lib/math_shepherd", + "type": "stepwise_supervised", + "step_separator": "\n", + "split": "train[:10%]", + }, + ], + "max_steps": 100, + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.0005, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "gradient_checkpointing": True, + "warmup_ratio": 0.1, + "use_tensorboard": True, + "special_tokens": {"pad_token": "<|endoftext|>"}, + "seed": 42, + } + ) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.5, "Train Loss is too high" + ) + + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_reward_model_llama.py b/tests/e2e/test_reward_model_smollm2.py similarity index 75% rename from tests/e2e/test_reward_model_llama.py rename to tests/e2e/test_reward_model_smollm2.py index 4cd8602f3e..7360a99dc8 100644 --- a/tests/e2e/test_reward_model_llama.py +++ b/tests/e2e/test_reward_model_smollm2.py @@ -12,25 +12,25 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from .utils import check_model_output_exists, with_temp_dir +from .utils import check_model_output_exists, check_tensorboard, with_temp_dir LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" -class TestRewardModelLoraLlama(unittest.TestCase): +class TestRewardModelLoraSmolLM2(unittest.TestCase): """ Test case for Llama reward models using LoRA """ @with_temp_dir - def test_rm_fft(self, temp_dir): + def test_rm_lora(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", + "base_model": "HuggingFaceTB/SmolLM2-135M", "model_type": "AutoModelForSequenceClassification", - "tokenizer_type": "LlamaTokenizer", + "num_labels": 1, "chat_template": "alpaca", "reward_model": True, "sequence_len": 1024, @@ -42,16 +42,16 @@ def test_rm_fft(self, temp_dir): "lora_target_linear": True, "val_set_size": 0.0, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { "path": "argilla/distilabel-intel-orca-dpo-pairs", "type": "bradley_terry.chat_template", + "split": "train[:10%]", }, ], + "lora_modules_to_save": ["embed_tokens", "lm_head"], "remove_unused_columns": False, "max_steps": 10, "num_epochs": 1, @@ -59,10 +59,11 @@ def test_rm_fft(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_bnb_8bit", + "optimizer": "adamw_torch", "lr_scheduler": "cosine", "gradient_checkpointing": True, "warmup_ratio": 0.1, + "use_tensorboard": True, } ) normalize_config(cfg) @@ -70,4 +71,7 @@ def test_rm_fft(self, temp_dir): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) train(cfg=cfg, dataset_meta=dataset_meta) + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.5, "Train Loss is too high" + ) check_model_output_exists(temp_dir, cfg) diff --git a/tests/prompt_strategies/test_stepwise.py b/tests/prompt_strategies/test_stepwise.py new file mode 100644 index 0000000000..2abe4ae187 --- /dev/null +++ b/tests/prompt_strategies/test_stepwise.py @@ -0,0 +1,63 @@ +""" +tests for chat_template prompt strategy +""" + +import datasets +import pytest +from datasets import Dataset +from transformers import AutoTokenizer + +from axolotl.datasets import TokenizedPromptDataset +from axolotl.prompt_strategies.stepwise_supervised import ( + StepwiseSupervisedPromptTokenizingStrategy, +) + + +class TestStepWiseSupervisedPromptTokenizingStrategy: + """ + Test class for stepwise supervised prompt strategy + """ + + @pytest.fixture() + def stepwise_supervised_dataset(self): + # pylint: disable=duplicate-code + return Dataset.from_list( + [ + { + "prompt": "Which number is larger, 9.8 or 9.11?", + "completions": [ + "The fractional part of 9.8 is 0.8, while the fractional part of 9.11 is 0.11.", + "Since 0.11 is greater than 0.8, the number 9.11 is larger than 9.8.", + "Actually, this is incorrect. In decimal numbers, 0.8 is equal to 0.80, which is larger than 0.11. Therefore, 9.8 is larger than 9.11.", + ], + "labels": [True, False, False], + } + ] + ) + + @pytest.fixture() + def tokenizer(self): + return AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B") + + def test_stepwise_supervised_dataset(self, tokenizer, stepwise_supervised_dataset): + strategy = StepwiseSupervisedPromptTokenizingStrategy( + tokenizer, + sequence_len=2048, + step_separator="\n", + ) + stepwise_supervised_dataset = stepwise_supervised_dataset.cast_column( + "labels", datasets.Sequence(datasets.Value("int64")) + ) + dataset_wrapper = TokenizedPromptDataset( + strategy, + stepwise_supervised_dataset, + process_count=1, + ) + labels = dataset_wrapper[0]["labels"] + # expected labels is: + # the prompt + first step are ignored, followed by the label for step 1 (True) + # the second step, and its label (False) + # the third step, and its label (False) + expected = [-100] * 47 + [1] + [-100] * 29 + [0] + [-100] * 48 + [0] + + assert labels == expected From 268543a3be20aa8de662761bb36645cbb5ff8f5b Mon Sep 17 00:00:00 2001 From: Eric Tang <46737979+erictang000@users.noreply.github.com> Date: Tue, 28 Jan 2025 21:10:19 -0800 Subject: [PATCH 0391/1405] Ray Train Axolotl Integration (#2251) * current not clean working version move torch trainer to do_cli update code with config changes and clean up edit config cleanup add run name to trainer * address comments * use axolotl train in multigpu tests and add ray tests for multi-gpu * accelerate uses underscores for main_process_port arg * chore: lint * fix order of accelerate args * include ray train in docker images * current not clean working version move torch trainer to do_cli update code with config changes and clean up edit config cleanup add run name to trainer * address comments * use axolotl train in multigpu tests and add ray tests for multi-gpu * accelerate uses underscores for main_process_port arg * chore: lint * fix order of accelerate args * include ray train in docker images * fix bf16 resolution behavior * move dtype logic * x Signed-off-by: SumanthRH * rename Signed-off-by: SumanthRH * add to sidebar Signed-off-by: SumanthRH * Apply suggestions from code review Co-authored-by: Eric Tang <46737979+erictang000@users.noreply.github.com> * Update docs/ray-integration.qmd Co-authored-by: Eric Tang <46737979+erictang000@users.noreply.github.com> * pre-commit fixes Signed-off-by: SumanthRH * use output_dir instead of hardcoded saves path Co-authored-by: NanoCode012 * bugfix storage dir * change type\ for resources_per_worker --------- Signed-off-by: SumanthRH Co-authored-by: Wing Lian Co-authored-by: SumanthRH Co-authored-by: Sumanth R Hegde <39546518+SumanthRH@users.noreply.github.com> Co-authored-by: Wing Lian Co-authored-by: NanoCode012 --- _quarto.yml | 1 + cicd/Dockerfile.jinja | 4 +- docker/Dockerfile | 4 +- docs/images/ray-cluster-dashboard.png | Bin 0 -> 299281 bytes docs/ray-integration.qmd | 93 ++++++++++++ examples/llama-3/lora-1b-ray.yml | 79 ++++++++++ setup.py | 3 + src/axolotl/cli/args.py | 2 + src/axolotl/cli/main.py | 17 ++- src/axolotl/cli/train.py | 44 +++++- src/axolotl/train.py | 4 +- src/axolotl/utils/config/__init__.py | 67 +++++---- .../config/models/input/v0_4_1/__init__.py | 25 ++++ tests/e2e/multigpu/test_llama.py | 100 +++++-------- tests/e2e/multigpu/test_qwen2.py | 10 +- tests/e2e/multigpu/test_ray.py | 137 ++++++++++++++++++ 16 files changed, 491 insertions(+), 99 deletions(-) create mode 100644 docs/images/ray-cluster-dashboard.png create mode 100644 docs/ray-integration.qmd create mode 100644 examples/llama-3/lora-1b-ray.yml create mode 100644 tests/e2e/multigpu/test_ray.py diff --git a/_quarto.yml b/_quarto.yml index acb4872589..8eb79f651d 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -38,6 +38,7 @@ website: - docs/multi-node.qmd - docs/unsloth.qmd - docs/amd_hpc.qmd + - docs/ray-integration.qmd - section: "Dataset Formats" contents: docs/dataset-formats/* - section: "Reference" diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 641bd90b6a..a90016ee43 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -32,9 +32,9 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ fi RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,ray] $AXOLOTL_ARGS; \ fi RUN python scripts/unsloth_install.py | sh diff --git a/docker/Dockerfile b/docker/Dockerfile index 6b6baf7515..aaaff23eff 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,9 +20,9 @@ WORKDIR /workspace/axolotl # If AXOLOTL_EXTRAS is set, append it in brackets RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,ray] $AXOLOTL_ARGS; \ fi RUN python scripts/unsloth_install.py | sh diff --git a/docs/images/ray-cluster-dashboard.png b/docs/images/ray-cluster-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..f0b4beb56498e7024e60763172f72f42754f5096 GIT binary patch literal 299281 zcmeFZby$>d+bv9t64Ii8G$*C%*=hy%ypl!&b8LX+vkchxY+lwQBY8DWuHpE1UFkKC}>ld zH^Czu>4b6M2G#MU%oCKn9*TAFORTYmtcijG%0qA;69tBP59J2*67T~@CI9DrX;dZ@ z^goZIp`e7Aqrm?D8bxpm{rdoZpkw}a3;P(1f&qRb0zZLisDD2aJfDX4k0Wun($N2L zA8iV}4&|}3gsd#MRW@`mHnwpzvvn%BOy~v=VA(y@bVNb9`vCeum3?`C54_i?xr&C9 zhJw71p{+H$fsyTNV|F)dJLo(pB5p$9uC=j~0gaorm5rm2n<(9%R|tXo(8C;bG=E;= zWGPCgq41nW!q&lQGxC8|SIXJmFxVhQDE7%;}ZJZ3;*lZl>|8|mpoJZ2w(a^!%&dJ=?h6XyX!E0M* zCs8^&=#BpQ?{A;e*vv4i$kO6?$&&W^AP?X>JYH z47>-NpNCuI&+GrUXa4n$|K&)He;vv5h>!Qb9{OLN`u`rP>S*jBVQUTE(+U2s>-G18 z|Lc=~KTw1N`s)8>DgL(3e;x%34aXMY_~%7~W7AKU@Pg||VJ@kp0&YRXLH|%~z<*4C zy9M`Ai|^i`;NnL?5l4}ge5~Szx|NJoU7|dGP0}tA`pKPvh_;AIBKS!l!HWaA3>;cz zrl2nFAl-)v+@84ma#Zd_@8t3ex{8i4jYC6we7#9FukyXcJa>zF$1jprlNLR_;j2@F zm07zpp3|GUn@F8e74Lq322`x4;%G!4-oWnD{I$E{>afh0h|xP@uCagpWPdxueKhQo z-*o?`&-VuAAuisai%;W$`S1G5Z~R5;(ElB42Yp>XDuU;TQS?x%C;yhq0Zy5w-@E(o zx)e{-Fkr-6$0+F`|AtWc+lOKrI~4wlG54=;w}y&(!&CIl8tq>V-D|j5xLkZGMI5+#Z^`kJ=Um}+LisEW5oJ+ zhBSR!dEe@Y@f#!edZD>r`H&G})d8Ru23eCF(AEmG+; zv|b4&x54z>sW7d~-Syt}G}A|`Dpm(FYBqItv7fP(sAzbsXEeoX+mPMWG>VFHYS#_>D^p7` zO`3lG4N`S{YQJk_{xx>!076w==TVaP$+)$W&w56j8X6z=jK^l~yy&jG(xi8=CH?9C zkg`(7AX{>wX^$JERECy+KDy7qe=A5GOwxRfc=4(lGP< z_Q@Hz{GS;1TF7l-8*YJX+g6zgCfH(U4fq2*g}OVwSF65gtqlY<9Ef_fV>%tSS`Oz-#W4mFGuw$d>=3iqWb z9dESj->YIxaxA&T1Pj(R$0MqxRW2CQPJ;?8_P_Wht#0GN&*s|SMHk@D8}h}vg7B@@ zP!K!*p_CD>lr!csX-&>et`-z*CfzVY;CGAwUpJKY`MdN(si-?KT^`X43UdBOzL%SN z=7YjB`w@0$^C3zMjaPjkaAIAb^JSHlc*6)R5p)l8F=T(_fV<$z@55f|?}?B8N>MXY z0287mpA8uUm^`%t7#s&?Ja?0^U%AZ(83f%?Gt)WCWecHjczV{Z7k4a@W0&1&JVU^~OSjA;7Y?WIFwb6*LwNyk9UAYbzapga{k{LjO@BU!1P>&v+u1=} zT~c9dNxGaS_h2$8f%Bk*VPBuMiZ!wpbrIdw(Dy@}uOx=#mQMU6ij6{DXVE*C6B?qA z6d}^}+W*CSopM^u*d@GMZGHun0_wAR46Wq6b&rUu35)EE%^Bl3okl9pt^VRy-pjLB zCuN9(4i@!n4p1BrLoc7HcCM!S`i%KrUk-AQo}JcRi+1aQGHY!x0r&0JzJvr1Dhji0i%wb<1!>zV z8njZn;(*TkN}1P4a}u=Q#=NSX37Wlh&U7~J$te507+O2=xs#dGTK=IT>PM3p^UV&O zY@=4noz9r!5h4d|#g}n)zl}A_z65E-=GxMb-yRJbF5=oQowSi{ZY#Y!ndEpo6)!^& z%E?Loy8ISLH7A=*aliC@;BCdW!~rKn`1N_OFCQjm+1Tr_W^U@FE#FWLpW|%3AT@u{ zW_nDd7mFijEM2Z`yNeVnYTT@t_pEzM2Y zxe3dBT;VN*M9C?cX%gXxhT(gQifJ5Y9%m;O354rz&8b?V9%!d~?`^&4e}g)nKQe5( zlInY<64@iPnPWZ(DjLfd-5Se2luTtYpi3auWFOd3=n*_RHDpOTBnr^OsWCs1iL9aq4snRUjb-7eHSjAz=IP zB?8B>Y{tWKZFBS4q?(37kX^$Kl5aSY7?e)WwL(JQTS=0RA48Ryw{A7X6Yr^AtD@0o z3A+N&k73-+xXl83Ga)!njm|ntJ04~SW~KTFb#b*)pfcCsfH4%w$d$>LZbE}aE} zuGc)_fK?Ud*#W5We7D$eYP8(m`bHn4xQpHqFd9uKCVQ+qXao7>#i?% z>rXdU5^WW;XO8n+gJRUQnD^Wd$V35vGZ!6C2XR%&c{=5owXB%WmfHFQDuWF~X|=T3 z>z4U7v3QS5g;QjVN)tf~pP)J^tGl%BppyHd_j`e4{yqQ9(`9#EfHOp7LsO(iM2g^` zmj;2l!V~it(Q_EKI&nOzN8P6O8Mok6$>$)t_-U|eVdqC&4*6i+%zY5f zHWTB%euG`^Txg_98Q=o`HfzC0Ri z!hrAj6^DPP52NFhczwnQ@oG{7o1eZRj#QI{++|_lP zw;=oStNHEiLGJNshv4d;;vqbk%LH8p7*uZh5ONTnI_!Q8F0YNal^?((ujfG@Q#{0n zpXr~0vYfKQ5Bi85xUSz=oj4^~pQt_N<^e$BTJCj37b@x^(KFVllSMuL@Wi(Z1koSX zcP@8*eThDQP(Ww~*MPS7&)%z{76f((E zJ@BJ1_!(V@PhS+XY&f<7wzF~zrT|Fy4;pw!v(>o(SG(GAt-H1BIeYPQ8-g}J<#UF< z4s|<|TP16YMRfj^3x@_>xOruY<gxLBVCeA}0{o!>GhZn8!?z3C_digNV z5K0$~4sQN#-obCI_C-BHmT)PV!Hbx)^XP4?=&#!v28T;xyq!sV4P&js*C*cL09BCOnHDC{MLT+Z{^ z$0V9VnU+k&E;JwV9S_wV1EnsocyHXJ}E( z&z;w2hv?ije-yMsO4li_w{JH?65Ll3ltmR*YQ{}dYib4AP8r9(`c%yalfTqVVHU~y zE>mnB{<+#+(UD`uvCYh$?p3FU|7U)L2a_z+uPesA=v{lP1ov>gI(|Jg6?c7<;{!SQFs=>>Z#Z_}1#A!*sHycfT zS8$s{6;o*rdFlgAVlMbaqD`ea_m^A#of)xO4?nyhtXlbO`O>8hRJ83SF$fcg(uuDq zZH(MvFQ*y4JZ}2v9EptV@^s!*La1}3#oSE9+gMt}xO-ym zq|e1RdOH7>_AFA3hJ5ZFOCD(ofQa|S?|agm?D}2{8)HdU^ZEQf+Z#;1;@v74)t&$N zj9qrp6`ZABuhfRj_2IkhTPK;HEq-qBXaWGy2wp<2pAp{OMAI= zEMFnjuyqTWDUR@VvGUo5dY#rRu?Owg;!JsKg2`uQV@wKun<=B!b)#SHjHwh6f-CVV zPG8^iVS}sVr~xL7LGFb0w&!tdzBe(apbCQk`5O#61&wu{Y5iubeO7vTr9OwAWy%h~ zx%1ETkS}#}GG~9U@Hc@mOr6b@+YvzCkY2A9o-c!zn@jPrQD8Ota8V0qU&PUdcJ|Uq zdP04@33T39l$Ey|W}y0a)b*rzG!<6`F{+R}Ia^Y$^A{G;3@CNBygn9&Tc3+G!Y4P> z5U$OHGJ(`rdm(T8WZ(x;c0Gctycr;o)q|jvwoU|iu&L&BM%#F-QL}E-%rokp!q`g9+U}3p9pK%Sd$E9air;>g zs89Jj0c1P$w;s(NrksA;Rgv&xxTS4fqT0a@n&KnDz-*{G;)K?d+g87b$;z`fE}L?A zHU>zQ>~4`o_(?&eYUd@^2^HWI5bX?!fl0ZxF{)}n?xNfhATHTK{&m^*Tg~M2Ghk zAgJb(cfCwY#CVpuKo8yx0(4!@gf5&TQB=L^hhuiM0R3O+Z#YnY3x;>~d$551cDWzv zi(^#V=>iC@Kz!e`CijBR0G_7e_#y{W2e=bLB7}F!d6lH$lGbAYgiq3t8cst~83MEw zwS;$V8mC}7N>OO9yt5T`V;A5O=XkL6O}PA1eFc52sdaFOa#9YicDMvX83}vs*@A;= zM`kOMMAz+$!6lf%$niw>>VuvCEVeqy7GNHA{N#O;u6a_=<=e)8)b0gw1ft^!q*Y3H zSOcUse4uHXd`AsLu|-9aJLnUQplfL4ZX8Ygq)dWp11Bb0e$9y%QM>VD=b}nJ>R79+ z(EJ~%h-sHew#()1jIkYvni{&u@Lp!o!@A8N|1_ImfwqkW_pwl`_|p5o6%B3Oki-9R z-F*XV!G^Hhax}%S)Ba}-RHOg|huLlBmNC$U_9=>89&rxJ^QZ654!86p0@OyN-MHRz>TjJy!M22GZ zyG+@)B2H&K`I#RfRR6(!4Il==B%?KLSTUcicWFUmsBg}gWcQ@QOg+!Amb@kT-NJ@{ z4Y){*dkOl0EpnovzaxE8&C?@5p96*kb29r*0B;fky>sFLEr_0a?seE5SGwwJl)O;Y zFgx&!BU#PW!}^@`48uj4cL~xS|6;e! z_o0$W?KQ}oXAA4abXO3%Yqk4#p}V_VQQND^sYn$f!Z${KH|}Dlo;5Qzg0bQrDDGms zp#!y7$N3@o-rJUU1iABas}DLkB486o*iGFkz9-01WBiEC2?VXt#U=V{6nG)M&-{-fI%7 zv9(%Js2m?l@C~H+0xcnFCEnz-`N2;MWIZ~5AgEepTGHP`o_~$O{nLQ^sLLRPDq12l;m#wP&vAsMlOCV95WoJv%%*uj_ zb@cg-vwHqp4-u@V`rnAaqW?$@AARt@_Zo2q@wAJiYWjTgZRH(vki=MLDN5!=zGia1 zXer<-xmAH4bLw)@EHihB7o;CK=>$C3*V*Z@NAcEOF)anTgAWhTDpkMtoP0kuDKS@Z2#xV(>sXw?g? z@n3b1Mqd?Gh5*8w48h5Cfb3`|M> z8&AtF&yZ*PtG zDpS3j_dcgI*4`b!ySFw$6gWhYg#e)cHRE|c8CsyOQEDRKQIsXj0bgycmSaGvIopMh_IFD&m(Ft383H!>&%Dec=S$HB08V8~vPw8T z#1}@xgalQ;F8y&@JJ_p_aWsArn1FV;wVog$NvYe@`ks8W--n~-=%w3U1BSAD;eOPM z5}VMAwok2&?<*0S2}rp6ImCasExctMDEhRypgF<55b12gu=107N>VM-mS zfvCr9;g9faM=#tjk4KvzaQmsdvaU9z?DkiaC;@lsRa;1D-I`89ZOBUTcu4!xbWyqZ z)CIZXmQNI42+OI;WcBT3_xnO4KKszOSd7$maOS?g_bUHma6zzOIbUTOv*sRw3(}3r z8LTKaEIfPO!wJ4%86|q}1+~Z87gfa3a-1Ig73c!)$}-AcIgn7v*@VLr2qyTSU%jOb zX}xgPFqYte5?JYXyQNzM)d?;clxuGP4i13W4NAvNHrp?~HsoYI7F7XjP?Z#-PH1mm(wg|0o%h-Q;ipY~R572~ zYJ6)DXy#h6_E+1oZ%&tfS7|}A2@tyAzaD?moPp*t4F6p-!_<1vs!1WXOI*#upJ)H- z?F-kaFQ2yt0R!iwOprUl2O3AgVbPo+^@WCnFms#%5+d8Cyicdihr#?r0_kS4iR3H! z08#A$maaYAm-YGWI1pY`;P1J&zwWTCQK+o z)0)n9atb>h8f52|ZagIwWOzf&Z@-%CnpwN_T6GN2?TrA0YUWu8DqSDhw^EKGE&(pX zh5%HP5U7oQ1u|)xmkm0Qk|yiaDj?j;(K5dO*v$AoZG+w_DC3?UEl~{-$O`SwC%h4Z z@AV^14}pvq#csOzo*xf1$E|qm6eMUnAqsrs_h~*ibNHVUNp3e8OrPvJRbAUneO(0t z|9y}Zas$+qt7-^-Avk0H%Re1di-3lXq$do(Zrd;-+q@O2Re;-{1@Id`)_uY#AOAcT z3HpFwT9@H)gtduXQtvprU5B3iuRD{UT%w`WEqL}Ohu6rh%LUjtg00J@`8a~PwbV)q z+i_lylGVSa(T%SEME8c2{gyo8lPM5PxN!%1*T;H(eBzA3(O7jspr>r=xOOa1_y7p{ z%`8LGZ2P{1TYlah)Iq4lIdz7rbEkIAp#v?`$iJpKH)(+rUyr-e!d3|lN99Aq4iI&I zpu7U0!~lq>d9j|NO&5}Pleh)^1*&auxQz*E(KUqBhOyq)7;``LS%0R7tt58oG@01c zAL!TQ!P})c$#*`tnNy@F0IU%V1WPNHzwwm%~WpNfY{bM?@({#uibxNdQlKnV1~pc$TeC# z4_qt(MtdAFKM)qI=W%nqvpod7oGtx1dTD^5fw(a$w>dv2544z-xewcUrr*n)2gGJ( zAAKlz%k>kaOP+N(< z<|U|+&nr6IK{nwKnW_2mRw;PlnT-zcPj|_@(U)01V|3onOthKUdXBE89$|p1hG-qz z0~!hyB>N7zNUO}s35cbIk+(D^i?m`ECIlg2sxzSSg#5UJv@J4Hqz_xc{8ZT zm)=IuoxO#4%^A4%VBAc6UVQ9X(__np=C|t~aL_0LAygw$-TDz4w=FPnR1h-vIm4gE zb>DRTu8c4CdoSYVybSJr4yPdk(olWdsdD>hsiH;UZr7?}6S^`7Pv1i$GF|hGn2&95 zd)#im7cKxh6S_l{i{sE(Kk_PEgM`jR57CSlv}j#BU^V3{6th#-bek!;Q^-#;V0vQt4Yx_agOeprNd9KYxT8rf2#>_pkg^a z(-+t<ysH!(GhkSNqld)I0r6@kLnL- z51(J`s4ym?@#MnzL-{eqXL{DcGV_aVuunE5U&Yps-G81hMk=#<{);EzMp_hx^{lh~dsZUxsT#S~5Vw%pk1+<$Ylb;wOabeoIA2BB)EGW8I}n}pT- zhP^QGL5jq@nASbE%7p!Ni~X|hOQhT|!f5t*u#occl;>k_uDuDim0r+@O%gk0VG8?zZ&i%RU z*EOf%ABtH85GndXlwuC?ma}6nIAshTuR!HsRFL@cQnXTDk~KjK%@GrZm2d@hXSfgA zvMci}?raDkS_||TzrXTH8$6g!j{(tiFu5#z(c@a{n3je3%|s`jc!P0o>aP1})l7`g zk5O&gnho|dsw4oOa2L#Oj-Pd^2^|ej@>SsA4KSv-y(YR?c9whwzdBnye9Z*n_nm$| zdmjCv_jJeOdL&JRpPx?jDnBKJMhRa*vo#*)8_19(Im_a}C*tABR(m!9Q$c(Z}ChR5NPt zQR`I^qNo)s1Mlbq8Y}C*IbparH3_wp(WKO z-gFdHQ{Iwv$3p=l91RElL{c#4)M>F&wTR=}A*X1%Djg&1J%OFL!X2 z)LX5ZUO$}eQ*VpzY}w^}4a3I%{l2GZB-V2+^mw};{cGkILExTgcryGk!2yv-_W+tK zoKz?V*5@ouayW#`$3o1^G(gC9M?7P+V2r3VZnB~wI}w0W#&_$cBu!w~6E8Y{WW(R5^o0#G=dvG3)W=V{ zf3jR*(%spuhIvS(S!L~cN_`4DCRwK7!ZhaGu-LH5QEt#>?EUUkfA?C*T^i?vf+7(a zTGyb(ILbd}h;Jgcnb+{#ks2$U70t-3b)8k_$~sPmD(gP?ccbjGb*t>lYnKG;lHY9h zfO~Y3(3czPuNFS+Nh@vpyGq)1FVhsy^&_mhHy4 zQd38K9J{~_yN$_Mh&$}Lr$4)0k*9dFo#~q-*Ph_Ilb)*^Ez?yo3{L47S`J&E9G6yk0%rMG;zo1q~%Hoh|* zSJ*^5#zODFcYE`gv67`|?a^eNNUi3{nM%n#4e+;o=g0>fy3>Tqi=MYI0;0l|-}N!AS8-0fJddjtw%Pnw*8+VoK zoQpiAM6jsc#E@%W+s6~Hl0j?Y_D)>|I!CeZRjYtB^Trq3&{HOOBE-r3hzzyAiS}bq zN$QON+%_+zk!<@{EE~VhC!rPn`cQ;dv8o( zXs;*2!o`TJl1~#@Yf}Ixe8(-clnHJ^YWyQaR$tkKLW%rx^bycCbm)&z{#Fw+F#Gyt zFbIMPB5q=!vnFehRb*?r>)r%D=`M>w34GB{bb0emlO1rdTO&kSQSd>{D{ju_`e(w~ zGC^OyhAE2b`XrK3nh6~74FACs(r7hATfnV+x#KDkar0Li3Z>U^L{)!VnkDH25RRg)8z1S9ymip-yibWhQ#4w-jT z)mj(e12S<&WsVbU9QXtVYR|_zYr7}1GXnS{)C69Klk?|^sHz`c4l7(*@8;WV*m|my z?Dhc5ffP!sdoF3FUcM6idKSa4krw;-C{+Et9F%YJ$-#hai1<=Js-Yl>j{rg z#(FxpEu$|t+;8mZ4p>zjXtGc~gEr3)xgho-PtI8je{Iyvta+;;Ohv~sw`9QUQt!M= z9OVtVw$0&z;&T!=gxK~*65-nu(Ot5NYp_ov8PQ2Zs)dFbNFcNyMzZB*q#%@ z2By+^&}4Jo#rcc+KN?(33TjgQA|T<*2WJzcc{QUpB9rSsgqwN&W)a``+O77=L>bBI zm@2aEgzVAAD&4riA94-3LW8HOdCwo^)AW7w&DiQ)|8|X5pvQ;Q=%|0QRA z@%Oaq@Mr1ZOS>k=H?>(5l1W!o$el#ZbDeK>AzVC?2^vK>p0L}DY3?ku49G}Qr6pw1 z{MTZ@87%)I)`$>)-i-_;z@%GS6k0R1FoE5Nf{Ywl#?)HX43(SJOGdHtcH-&>rn)kA zuAmWQtZqR7oBgfNGGxLlv~1JNWomXC0-{w+$AuCaeS)~AuU#_GZ(%W#ym9a0pIAG- z&U8HyELHA}=?tK>WNk%k_=`@gU``Ycr#4`Ym3dm?EixH`zc*saam{|06W%lVVK)=@U0fZv{ByhEOFWzHhl%y2uKs z?tcsl7=%n!KQYm{j7rPoQe`>*k*ok;+V`XkE#OcU<@+&&VCD5$QOvp8bb`vi?(FP|zMrmzv6LzC;xNzcyb^UFt$J^ljN(atXw-7< z$$Ec9t_*8iiT;%!xl4B&KL9Ct*%8o*qaW8#{R~CYpxGRI{;rhKqwCdl%bC$$t)e_u^jd&u-*1G>jo?L^cL3i9|N$_zU zejO^sM4?%tH$+tM4@oS$k44Z5e4%DFJ#llie(J4tp75BOxEF_ebb0pvG7yUr+Y6gV zX7kb}3`^`OfXHV%Q-cUQWFbl3&_n$}&2p7r%q&S>elani{j*aWMs%crwQw zJj)J~hgcG#oz=^Vr}fYzWZvWq3yR;|O%rIt8?nkBS0mqYZM_6|X9boiNvV+91}v8v z$B9Fffrl|fnpFGZHjyi?Hpqm+ukcGHXuI42JMQ2?!eatPm@n*ABZ0sNsVW;|;e=Ao zM#71Oc>fw9wzvnJ#jv)gYVV>6q#tcYKFs%Lb<-4=;v}2NEs6p3woT=F=Cj?7&1Xi{iqoZK{Y2KPH8%p7YEJhMGkcbSF(maKq?10^rWV!7oq9?SBuHE5SUC z`E*KobUVt;OFaTvEWaFHLWOmo2Oy1D-W4PARQNa%{~3p_73l4I(qe zv_w8T_=5gr%d-Yu_}y*(3HT73b6udw$~5%ZSP-aAt4%1xEw zof0x#*|s{-VO#K+^%eH*qfbnxPlu#^;r&^Fa zNCuqoIsf>HgH*VjPRulzT{{(eA`Q^Eh&+!gS{wE^6u%lumcZY|{5(ER7_--iS#)&= zUP4u=g)~<^oSYZdkv*?~yU7R8)@qY*ut9*&s`D1tYb#X-JksT1bFGxbT@MN-+p!*7 zM?i=DIEjpSg**9`CTHTVl#jPPB6p&v>M`+MwMq9YDFc}jQM@PWlL9itxN4K}%)5z5 zX-g#>qK=T3+pisZ>u*_Q9{e1?%GJpEW-9(EqfFov7_ zs?;L6k>!1E%Zc$WpmX@Di@zv4ukz=^+fwD-@5&X?L?1jaRer59TT6;ni)523I5b*) z_j=1hxEI%_Rpje|uv{0>Btj|tgSPe8lvDxx*Wfb+A4|#1TX^Cy49oZ`mmZYAVm$UK zU2~fvNv8SfEhy&vR5RF=#=6VP`q)(8No^yn2X**lO`xCT*VZ|)_lxEc5~&^Vz|vSN zSCJ0DWW#(tk7qTVthU1ZN(XqDh2DNkFUCK7)Mvx`e%5`arFcm>cGCntsx6OUIHD8q zWd%+IHf0yqzP3suFUlQBN;%eXWX}UG4J0GylryjO03?s$ZnrVrHS_pld;!0So!#+i z5vRChJ=@x3nJ1u>b^Dkp?R$^TZrD7jv2b$1bm25b(d|dV?*X~yM@v^v`ue;_^Mt%i z$XL@BVSAoJ5%8Orf&+W+-c0ll?GB-+%AdT*(}UwJz|t&d*4iNPJK!5%)~IdE`zFx5 z6F@NNIP0BX?FGDTI7FsnrkA!Zg(aWue5w_E@nJybTAnZE7T1`iGwuuhQ4(~Cm2TW) z&e74I*K|Vsr)*&V*sy*2cb4OeCG=lbs>6Ip{SL<^?(WQGo{&Swn&tSX#2=JOwYxsP zOlC1z+6?nAw@5IQ^pk0VpV1qi>@imiUMoi|+E-g0FSc|!dS~pa5I;Q0EoiwnTp-vA zq_YW<0L&GMqhB;DxKrh210B1o`Ym8?;_B(GX-?DjenB%y^2=jgMI?3ov;*h$o45la zBxzjxWckf_Ga^!4*v7F8s4fSE>sc!o$#t7RF0)dN-A}JhF(*Dyo2(>*0L*tDu9k`K z^oQrQXTLc3j@(yow~FVfCNSOa$tKS<7XG|g^0ThWUD~eE=5%*SP;Ev7$3cDCQ&TU_ zxH6$kteV_ohsCc4(`oYNPKzyt@26L--0xA}Uc5$xY&4oPw$9yBO)!DqG*09(J(c;}z z$xe?}qbW>b@SCT5E-gprV7kR*D{$=l3p2LBP&hXXy$G^B*^Vi~gwu0ARxq0$q@mOW zBz0#Sgz(*hcI$*H&NJU8d%@G5_lRI~xa5SpiK_p^!P69MsgsX_V>l?qQhLKet>nHP z3Or3_znXIFRfsawq;Z`3%w_nBk6SSs>jy8LFX3?*<_8T?5BAMagch77^rP_g@53VI z%e#lWpPa3-wX<M%c%)}u7YZWM+DyJV3ILUWKh6u;M^Mo$tJz^ABS>O)iNSJo~HoxLotgCH1xzsDR z`*>+#tuHG5{I;Kfwv!E(*KA=-zpOj;`yO2PL8HNWaQPkz&lr)Y=Fki{JH7ikvXt|A z_+6RFS@P=YSF)20r&m2=wNGe!87`MCS>mAnq=v}serY9 z*hWO%VfGH$jniq@+2Z%BGQ_Nkv6RuSJlqj`T}>T)mZFXJPH0#|+RpSZO^LTNhL2MF zZM$u1h9tT2r$XuQL*n6Z>k{lBup6`<1q;WRQeiTRRbhM$oeoXZo1d%ALYMNh z@m3po`&`m&RlC(MYUv9;Y=VJ(jR;hO!E)}yj2HmsSoeK;$9OlnxVfVen6(oK-@-{J z%8a<_I;l*8YvenAz=A{S0Y9mEc_m=TlllP}GvqDccKSX7@k3*rQCyGPxLreS;zw4B z)_D-;imb3D*yABDHDNqPZgt&YcN7_SI%<`IV{w!{kU!c*$}$Q5jKs^*CKJs-Gtg76NiOwCcYV)2G}wJT!`tUVGGz663BHd~cmKxjya+{I`RU>?E^QNM@Hg}n zhT~~8x>W7n6)XRpqE0u?q5DkAsE#IO^mi>FyDq1E*s?!jS0ozTYS4XERZM>>6Qm?8#EM57Kp8s0I5lUc1(VTUt5f_1VA&?{eOW zsERy>CN~?#SmUH!TxbTHUwiSl84UY&eLI&lzr1381>p-_q9;|PH*y`?x+iZU3qPG2 z%E^%6=0TetXqr3KT-Uag5Fumu{nTo^oE@#-(D|i9<-#DBltj2gG`}F^S_IPk5f~oC zJHc+@Y){)|Vy!!N(rBU#oCFQJSy>U9%l9aL2#-geVD?Ny4v*0OFXWPf$y-VZK$BaC zcjuLW2|cOBo~l*rZI)1tTIYK_wBq3+@wa~H6GH+<#$Vaw6G;s@MNE=969~ znp^h{xJdg<-U!!i5_Hj7?y#%uh&IEes(pA%gEjAK-y!V8K^FFf8Gqx5$0SL4%p$1#4Nv@DqeNn zz4zLwyE^{n+5r!O0@1{bviinEnv+%1MPof&%{9Kh!GHDDG{JH!)9SDzDXL%uIVoO` z0H>h{i@ncQ3GT?_FFBFr44ZknzT8wwXvw$J*piZ8jAti&_vdp6=bb%DD`s>M6S*O` zFEvQ{Ij1=HgyB=6O>yqXQrN;ntZG~=`F5etr>1CR*1!j>&^`!fL;v;*hmzK$CaaSK4>Sn>6) zjszjXtrzMv?42!Dwmy;UH$1@}B(M5{Qk!G@uEO4OVq97af%R|E13S}4)HpKNZkJTr zY>TY&vuH_d#>Ky;(7MwH{SS`WBhe4%zm;`<*-s)-Gu?&xnS{qFQ{iZp_m=$B zKdXV?pK8F!gtcIZ)8L7>^MZqa%otRIXFZqqIR(%H%+XyJ`E_d-`#7FV7T9FqloXUs zK_msF6qN23q)WO%MClMEl$LIgmR7o@I|PvyY3cuYyJpR-wP)`=Gke}I?{UnxB@3Ro zuj@LmGk!66A)SWzePKk{=S|*pq(>~y`>Tr*w`y=UAgt@-G6@ONXqg=e72)=Nm@1Ts zFK_2GAB5JKF~4!iBH~eg2XVt5O6>79^7auYPrBr( zn@_uTNq|}^afLLfRn#xynLwn!YU{KM7Zm~ii%aWR=*K{tZF}nUb+(EuefyrnnrY^Z{PK|C6mS{miFRV)}qC| zkMXq4T^FO6ibUIFH8$KDw+g^q^6U9-bBy#-79XtQo3W!kZsk<1y}}$*$KDwhKRdqC z;x~&GqWPHUt8)thW6?_5Wr6)DG6L>cdzH{c7h>%59H;kZGV^WS%U~$`9D$L4VepYG zFw)39U1U)rPRasJudurCoaDv6Orz5bf&`2%&XjQA2w9 zS&TnEtq&p-5dL<#Ehu+xmkDz(dv`7TnE;xsJHi#8H*!l-lNIKgF%#9b3G%qIZ?tq} zCmMSPulFnEvlUlhLrFKKo$v4?Mukxf2C|25jEpiRD)IJcoJhXEbY!+w#GWR~#t^i2 zrh9GUV0SQ2`Z^mGcK>oH5G&#ZG2XSf=Ne3xz&|YA3XnN8;p}Toky27pR@R~PpxQ6^ z{yi-DMmYxVwA+ql-F}w4^SlSHA#!QO$b9T&7!gSTrNfSIRB>EX-47J;J(6;g+I49R z*VNF5BT-={bi?h}eLS*stro7hp8bW^cykwH;X3uzD{=Qfh~(bE>sxHU1l>@pwysmF zmL`eM1kQ#b2nh$@SaXfLn+mIFB14{*6&o z7;74l$VihOGuTt!e60Ebrg8Nk{S%IM3Tf^u^+Ya1H|F-xC{V~oKKXt1jX)XpojVEU zLE9EKq)NgcDB*0dY9d ze=1_Qs2K4q!19bM>1)4&#}=kLU3Z00g|`NllMILU9CTDXB_0c92dVSp-ANUp@DAtR zi13-c-Hmqh)9be>>lL^>JY z&)aWYQpcMsve8=z!|K@Z=!)LHVn2Y=#DtsiBazKx2QIu!QeVX!Egyj~({x(#!=jpO zS%f)x9!L`dJs5^|S7uBz7mi?UA?%eDI}_x?6@HAE+H8cmusN zgFWPFTAJ#69)dtt!F<-MYrsWa_){*A;8bsI1Ul2T!Pb$71S=7Cu$w;PBLJhTXk+~c zkuh4rd$A7&b^=Q-V?}TqJx4xa=G;OPQnu84=LAJNEvvKp}JcW{c+!5ut~l zSItwogYCCHMqFq_3op*;N(No{!NXM~|E6(=iD?-QW_Mt>1?6DJ-iMK6`;QNC33i9| zLth~Dmu(UDQei>tCj05l23x!ydUqPXi;Ik1$^iHa#T@~xJ>TD zA5<|q7<|rZRIg@+YIaJNeu#3_T_a0ZIjh)`__4wq{ql-M&EB_xI$ZSIagVL)t~l?E z7m|LU{R`q5oD6l&VbrhcV3q)lFw=9CpO;CTL9a=n zlqF5W{Jj|ExiQ_}Kz%UQ3==j7jCBrG_jiC6crAF24&RU+T#TE%(WS)AYfuTg4hrFl zk`ocPB`z|2#E=su3@M0n zq&b_~PFD^O{3PL&G+w{($WM>RHCLEj_11i;Y~f?@p}W+B3s}F86xK_+IYD_*WT!O9 zFw?hyi=02&!N+Qmsy!-HG!^zP-kpt9m6&*?yG{6`^31#AkS`9{=byusSicwbz?Qg+ zPxN$C%PFZC3}5=#=cthxjL78U-t^Z8S)d5eWjrQ!T9@k?iqlp4>*@pRpUJ^K_A@X0 zgLU)mGANgAM=&E;J12LOqpi-f{zT_lJrMoWu0P^kg zGx~%n6^K9*{cBLts5H1qzn(*1q!+ce`wL$U)L3&rpqM9*o`X3pyrn+eCI$^cfkJkOM$#wyNw!V3I0limwVe&!CX+W z_?66Z?@o3s;Iz6Y`(4%8c9yk-cHP6HK~l^++*gg3yB#rmY{~Hxb>fb<4Pm%$K2ALJ zESa-AKzg4QFBlJle|IehNT6O1tXI!pb7cJD^g&1Ki3pqY%`Nc-f^1O7R zO`33Hg42in+v(G;K8h(4u%+0xZbO*xX9)AAW`&xswjUdWh!sdK$;H>IRtkk1uzjeg zH5lbNUQRjwzH_B#^;px)6T19@&TXpN-}Tc^0!k(R+T(u{E5tflH}Nx=aX99X<4TX& z49@Gn=*tt^W9SW5)gUAN{exz~TYA=kb>T1z;JHFPl?CShNn%Qg~a_~PP?a^V*G7;&S2aN12yc%qJ8d8 zEZ=5tA80k1qYXH;MRX@FV98np>NTb{8ySq$mMrI0D$gY;=#~gJe;b+XO2XGrik}rd z{kVjbBcgb$B->(8OY4iR5q1zUnt{NBIFTtgzQB+QC9)zIAKoG;fie|$%=Cf|4#LG@ z!8&h5rbR*R=XEPun`bufLmdB0lMb4RCsiA7QRw+x=E)c0LN9nOTl;UlZk-Y%a=n^AP(e zX1_#7m!B7G#x&)t$M)KkWHu803Z@f4r(wil*|DG!$I&hNURUHT-C=7?mdRqXQsEjj zpJj;~|zXY_lC%m`k-a-Ag9izJKpIeV9(xCYnILFeIF$tkJPSEHX% zZ1t0VkqR!>zi>pK15Wj%`rV9R#tnZCOfNxJt_QO?T7Im)1H4HN_}DnvR@7Oy7l);& zvy@io9%TDTitW7VO%gHmW33v<8g{;nKm|p>pbzV!@?jL{E<$L@KBvMMXQlM)s}WpxK*}4Ha5+;AF|JT_<7?-(J^_$3 z(t%o@TJEFYtGxTV+}J_Sp(|&4ZH8eLm#S;sl{hFk<-9B07AkGWVlkME)V{diCdZAw z;%7*ReH}qm6>e2Oi9h`~I>cMVSifRp`%MlH1?Bbpc z5)!|W6La1*);2RWQqsacM9I)lk4MAqYX;SqjjoTbBF7UBuOD4BeEg!mCB6)>*7FK! z^lx{Po|LOg>2_wL z+Ih{RixBYPaZFy>3z@%nBBJKognPWy|G0zNR-2EyoqSM-r!BmhX8*$~e}=Kh!^DmP zmC$0DEypGEpgV6#ue@1^;CC$)lPF?Ha(nwIbd{fWW7C(({V|@UVUI9_7$r-Y{Uzb5 zjsiyjKgmS4zB-@qc%ei5>k%E>gFpik+L`j9$rra`sK_hB_kpc5zFU zAy#AVn=2#AMKXEE)wXJARw@Yds&7}M>6>}b6;ay5D&m&&uZr}GUe$y-tVKPxpu)i? z^rkq|WK|Sl@eLGBUl2d1wmN6f^m@bHt0q1cJK&SSjwQ#65+LH>MyKSk-DC0SU~lI9 z=@d>Rf2^&h1IOS)M*)#ol~<6ZJ+R= zED0A2A9zfgs`Cia!Tbmp+xIKJ383Wlx)%2+PIW}5&St_fJ;!RK%SdwS(L|DL_7X*- zjNfuS?`rMO-l&>zH-}-`fC7b`WWzO*t&2ULz2+nbgSivi95jSyEWJG5bx@!E^w2}( zr}&&b()vPETQ7{5de}6yAkx<4dTvd+GiUOIv;WHPnYMHOoMY~a=cg&0TGoeX$^Jk$ zFphW>+L6ru)7ai@d%ex!$_}yj)fH8JSl*Sy$07B4r^`HJ->d9+}p|`v<4^K(L@5ZtnG}Oe2MZdRL!b^!vIu(W;G+29%{w zb?cp1VbyvI#ZSrqqB#tF3%@}v!vIS4FEIKL7{S(WR@QlY%g5N~eNiUEE^H7LISfA< zyts*M3um7NG@jm5VMCJC73x=7{cVsrzFG>aL%H(7Yv(je5CdMK4p6v*iE3{*OW|)v zge?Qm2AJfPj^40HA#;0wjEdF*iE0YI@_juc9VH!kt4k;uU^!Be5`edrH}`y7U+#T#RF{C9bG#q8vKj)^ zel4n2x1)MKCod#k`(%E8Np7`$w_uWdUNraS%KP9Ti4OwZmDq2CBmvDfw(l1o-`>x0 z-5%b8X?m|*lr2x9D|p7&ly5ExUCY^|>-M!D&`#NHq-4E(dlGvMgtA&CFMSBP_xY{h zR2Ym)N$q!ZS3$CDk@}hEoo-NTn|S%?)oiaaL#=Okuy?7qfI;m+*hfaAk7kjb+5AFR z8aZ%XB4cP`j9l-qC_F@~2WEn|sL{AjU!)wv=GsfT341fx1~vv~ztH6^2MQA{Ri@`; zup28psdepZ7i!tSD*uPta%A)EH! zOh(e9x;460U$*bqm`)5xMUzXRDTD~U?Rn1o+%dp#)p0+pZG~7Q;!$_$X!rq(lOb>q zzoLk{c=tgoaUuDHt&CRiV5FPt6i>C^bs5(WlQR9X+$=wEqeI2^05@I1AYU^z#IYim zu`<-!Tn=g^Sx0Z#>$TBwiLSG)U^tZpKTGhLaxSVVBF0G1cdMOuFSp#T_ztp#=5%)* zIt#TkLMw~g3YvwHhSbvWx=#!fA+UVjn^pzzvYo@by7XByd_BH**M~_q)qbW@M7&)BuRnQUDLaO$RYWk`Z96J z&J(+s=5rv|6QDfI4d2j@?aWyrGKqY|dX^_^N!M+<_#L&bPjy&s`qt8~p-|$5(^RG? zlNT-W#9`TfJ7gr3^a@($pHn)}k;#TVLEGyHYLWv`qNB*6+IW>p$-CfgO;JNFOhjCiN*H@(bDKxO>_|AX144JSfVY z*>r{cj*m;U7!B5Hi>D-n{*N`n;}Z{`jWo<&l5VV>5vbeja_&FTu}B>7A3xYV20 z-(GiX80Njp74dmLJb{V_-3D1J8I(aZ)=1BXn`__g&R?SJ<$0U$d`EZtUTCK!4=&Sl z5L!zewS%fXJ)=G+aiY7(YyPbGPAj)xOcEDDjK9cUC!yXgc+fcy~5I*2mactojKQs zb}C61jFxHYwU0(sG_4!$m>+8dcpuaEJ|WGKN}qAW!pZeS>v_+z0_ucHrA$~S7JTyBz{6@Ym5?qxg%pZs;=+V3Vui1UyZBUte(5a%{G>N! zyb1&*XGUdHOWF%0QtU=0tWUA7emcPICaRegy)}OuxD&V2&29UM;1BFwd}PaCW6exF z9-bxb;o%YW6)}9h&ZGvy3BB8zkW_tvi}T*H|3a5l!n|&&G!^N5q4U+a2z1ZXqqUP` ztEFVdp$m;w_CvC=+ixxrYoqnIi&}N7z2xRwo0D^k-5>KOmYEWeYikvX4Sqnt{zF0g zeCfcX=p&8XeMzM4w)M2`$xu(w#c(wnBv{x3A>u5wVOP5B34PWvrYu@32 zCUKo|Z3v=I;uUnSc(o|vN@YhuR3>*s-H%Itr$3<05^v2J#(+vGy8Fk+8Ob_XTg-3u zLwG(Rvu=Z?_5(Phs?B_N+_kr8ZOxm^Z3NlJKshA?W+?}%jzN04KOr`IbUQ9UdL~eV{^;n24Tu(S>8p8_W8Cxx^FT^7S7H{x;>LmxE<%CXkg&^B57&Xi>fmjXrZ5OH0HZe2vv|9on5y`pQ5SW}ZOEQ>kpA3-tOJFRv(P;bul-xG1%TH^=c@nBc5< z;Y+GDg|mmopi|3_zQmt^_pbCNh7PxJ|JWw9V^FaiKS&bMV(TUx#vc)4~FyZCk|mitBqS_@BFkohBeuzkzV*Go$qHNU)Ftp^()<& z9<|&h2FnfCG6_~tTTeZ5dNVu=PsMHXv+9ShXJNT3W9{e>%$jPs`^?i&@r}MqwO?YB z`;JfDldh}ktkt30JFOTsbg{9&!i+A%!b~KV_#WTe|86*-#<`^ns1)md89oR+&L0nu(Bsp$| zocJs3J`ZF^_zA1(arvIVS!7s^IiVju@LZWVQ+31ONbrY|_jgrQ-W2^X+%t+VQ_o-v z*9qQWxz>B_on7+bOId8x%SgNw#gyktaza8C`i_}5#zRhy?ktmb3%Gu-o*(?M?cro? z)YZVjvBtw^3uA>2g?kH^7Sz3*P7U2TSm~paRj~-3;T7f|ry=K8Rck|cwbr@+yeD9J zAfhi)M~9^;!uR9Mfva06E2~Ad2cEs%x9#-}`G%;a9_v}@V=e!J3 zQoG+9wycBs$Ws*gDf0_MoV4~Th0mLHZU;II>uM5(P~Nw_v14$kBb9l6T=DEqQb@Uj zf~H8w)Fp;BeN*}ZBKrm_AJFXkWOq8ASS1|cMhU*8M+=o3y)+znN@s3+{u`R>``DdB zANls}_o{$OhOzDD8cidja+tdnqsaasN=ODTgWrR*tZSXJ&q-B|pE9YClV~{5WNcqU z-`1--3C5n zpR|x}{w&jg=+jfD(X_7P2_81Qel&w)%Dw57oGpcZ?Q~TjO zn^|>tg;Ck7t_WjNh}?{NUoWi1$^gY{4h$ooQFvQ?N)<3PuE6 zx)l!nS?^nW-Bur9v^*C)*ATqC?yXS#L-qeLQjPpiHyy3Swp<7Z5=UEX3MWM zPqxMAS00pV)K1cVj_|5>x@*Y)h(*#3mp-0%NO>*~cct;73#vDO#B^pc3DqRO?;|wM ztyC;an42{TVE9`WPR=;Pww(xw_#(wFm&3*Gpx3=5l zJw$X|(OtzR*5h_TD5vdo`Oj-bbUjN-&tD{+X=_WAW`Ag8$LP7hh~}K@oUfEk9Uqj7 zmys?cgQG(x7M%kA(l9MXJ2rG`p+Gl#_jBKHRin25mD-v z_e`yH^HsGqNdmh+tUDJSo~<-4VaU55A()u;CjHj!2LzjHi zqZ2B>T8@7lqP6|G48%id@h-Ca(Uvqnl%8Y0-Ee89LhiZO>`(Hc<=Mr}h(@Cl$*`sT zY%&gay3=SU_!;x;`=Dv1l$FGdc85 z1?Nww$U2$JBNI5X&g|&<0G<4{&c%RZV=;y^c z_SQQcXYXE>QTxGCQzH~}u<_Os7I~yjZP2VWKPRi^46k^Je0wz?yN+g2@YJc9B2#D8 zn;udj1=^ZLxmDU@k;_D=aX(D!+Q}c&@2JAbYVSAHH_bTuY&>x)S*52?zEYk^I1Ey@ z?UK3An(J|AD<3##-Y6jIYj&mdNcY2YS$|Uh!_QXX@GOzntBoo8rOU&C_GZggbS0V; zf&1-J=C0;e*1?QuZm9F}SX-Q4ZL<>ZkJl^vKvMW+{oMLw$du45|Dd7?#F@>Kxw586 zGzfJ@<72Y1wzI-EC{BN6spN+FwVB&9SUVAAKd;PcxE@-zLnKfL-|5g2HOE|bI|Rbs}M=j)hOUU3jD+CexVYqbd){fPgKP0^2a#i+JRw~d>Kipt+yCPJCz@+~3xQ_V{vq8qcsDjuhE zYW6bAHUb)C3jEay*MyKD zMRx58bd2T+-Dj%^yi)e&6KbLPiRq)m zYnmgv(>*$OVMyc+#hpDHD}dmNO(JQIYNJhaY^d;9HPdOIZ_Qw|tjI7= zEvAm_(IgAe>Rcq2NSw!Rus`^@LNWAscJv@o*t^1w+D&*KK7*Rke|>j(Yn}!wxI8Z|;!$^s5bhq87R`be~G|(h#fZM97B4Qr7WL z?MrLkpn9J-HsLfeQv0r9`>KJ1KZo++EfDrjB{S=UgXn=Qb&sIVM_Mwi-_-*opq1oLw zC!rq5f#aiHp7qRd#U3|SRD)Zu$*Cgu`*cRUi5A>u|)C?l*z}m*HRh-Yk z9=r0zCCbld&_W!wo?3<>?*QA;iw0xn^e!SNL0$=iU)LX2_|j+bjq~e3YCSgT#w6YP z8ls8cesl~U=m;Z#aDL`=3Xl}L?Q)eIYj0T|Touc%LiGloS_iSvH;83Zsmfp*j~0dL zjW?~>@HfI}Xs%=!nbCvTkc7=dSL<>-;)7@R=8K(K?TYhx)lB-mpG%5zENWTs%)JlsJ#m zpx~ce;#L0rrfGjhm9X_@KWH>erjpDL52PLv!bvAPL%O!)XCe3d_hvw|W*rCV)L78e zjcOJ*b6u~!`&6hIyTLtuJ#iIAr)*{N`0w5zVA!&o!`4!A_;7OR@tD0Vf!#*0K7R~4 z_0bdo6kn<*l|eM|Ey1HBV=XIq*bk7xD#|=*Sw!oTeN0KXTnQ3IX#nkQaaK}&~Fymd=IT!n$q%4IpSrZ$w%LYL}o1q>W~?G1 zI;;B$8iq@V5M0n99~#%oQqya6KyyD8=d876kiKHLI=ZWcz;}XWm9``kK@gRbcCfe;b@@o!Eg+8jRFXF z&R)10?g6qMX<~$GDG|5qCcoxgw2#k?hVz97kA4zUi|TW9o%VA)WzZ}oxcyCn2V69I z3m(_d z5N|vYpY!)&)t^LzvYb?(@-lKsD==}yJ8gm9+_$}16HJ83JPP#l=nD;vyp2^d{Oh6~ z0Z;e^oWODK9)gzvi?bRf3v1_L(5i|+S_)er@yc8c@ydBPDUc-^k;NIebItKAx#ZP2 ze2mBrIcrITlE)#_!{UZZQYmB(!`D_pTR!*Ph#1Fu;|2ojCuk2Iyr}PrcnNZX%%sus zupr{Il$|S2PB?~IzD#jP-&0f#R9QCglWTzXI)5?6dD`Am?lg>C;zB!7hC!Rt@OUD2007};$;xu- zaK%Wlecq?dWz>-BC9WNkD_QA9B)=-CVIacrGIY5TUAV4dRD->Uo3{sLVC+S5ek?3?Gej{#j zDa6Wl)Q5yJ=7?#dyeV@gv5MWSh+>Y^b3%r8ZRcT%f?c)Oh(Anv}88P}B86@jP8JZoaGczCtnDGg3)t!4bjRN<<_O;KZr;C(gx zZ-ZOX%fW&7YY*pGH}5b~T)9>OqN3c0Fo=Z>MF-TtJXnPwbQNxR1j0HBqrU7<;3nvj zsmUy*K(1`kmY|DeD6CB7K|n3J>EA1fM8S5+Vs2)G@KlO~r)Moa70XDndV2}hwe#Iv z8*>nz&GUj}CO8~C#P0As=Z7Ai*%@^iUkun3i3M|m>8D7O?wdu{4sEOupM`;!GD0UwUpyf%y|&50&=w`6SV7J5Z&oxBD2t zUpOP;~4{e94U;+trSq?k_{805}tT&IyNsnga7< zrPi&63;$anwRP1A=n6`bBjyYHle;x4E@T2dRLfxgp?H;WQ$p%J==!T!mXzLnu61SD zEL+LM%jNIZ_P@l2zg=*@MGIM4NNjk8H6G}D4PxvhDV2hsWSQbeAV0oFH)c=dCq?B` zuUvjqb8AqQ8Z5ycnE2m_Y{~6&Pkr~IrYBjV`8vJ2YT%$J;yZBY^0g(*1U^#h$M?i| z!i89sm)})_9^iEl+tFb>^A*PG=~d%i=$PO35U0d6Vr7i3q=*`u^pr#&i$S%LE;JBNopY9Sne;{}PpN<;1_lMFtoHH-gMgp!M8IG?B2-`p zT*t2_)ZDipx4e`WDsXexdxo_e2aDo7`V+rh6C%7+e#{d6#T7AGmgu-i5DMP>`rh3r z)><4f1C%N{0rQ0isGC8TW~rxDpR?3%=UUAz2Kqy~w!Vd^GCREm@y}yu=xq<~*&FCu z0chxcPh0O@9Om%mS`f=}o&+7eY)(%n1fpmH+R^RxRQE2edFcs?PP`9FDmD_lY`*}B zdnK|R5W9*|NX-?R;OKDRRkW6Q8WRtPyH@?m11>YX8t4=Xo5Ic(U&We z566IO(XV6%-7z)bsteV)J%BU53Zrg1fw`znLB3mbl(q{!g2PRngFgBVlZDUkoL|90 z!QXxGgarJ4DF4R20Oj#EsplBfRd_eAsFspfFB3oiY^#q?#ZpPgXELx+| zthzlIl#-Av;Ra+0fwNz+X=7iCw28H6ptu9s9ObA|NSW8f{HL`s-ss0uwP?ao?zT>6 z?79d&0iA$Nsc@_%G1;oBhP~Tpi*iZ!1I9<3>OFy%nDLR%`a$s1qV@LxU3+K$NZHwO z+3*cp71x}AeY-!DTYs&){U)JTJWqNyLT_lG7-iix;a~Me2iiu1^===T3owP%lUhjvF7h z5uq#(5s*z%V-g+EK(w_ji%Ey$<>491iepT-Nip4i_C$=)0o8?-A1PT(NWAIeTzv2H z4fXGgcV9uL7I8_j(ux03>*)ib95G4nO?2|?ge)(0s7GP4a2l#9+0h~PJb?rHZz@og zx}(AOG(K7d+`SBsW9*#B=! z@q3y4x8M0Z0-1drbIx%VS}yjAm@2dS1Sryp^il;f^=8ySoz=rTvDqz{Fwbc9Ay-c9 z0&APb-GC=`Am$c9I|63ZHd4atRD;*lxw2A4{Seb(){@6%NWE(OF24}*+*kzloepo` z^H*LW*45(_xxK3ZhhGG+A8Y4Lpme2S(KPLgEg<&Ubis^k{%oSd1kCtN<;E!bXKsiU zLLXu#Am1)Aw|Q%53c8=)w16wo2c_(*Z0J$qyf(f?uYNzsB1HBP>Ib8agBiLj;{8MN z0zCo7_=`FDh`uQRw!bx=zsgA`*1{_33dS}K6p$;$A-U2gN7N^1Pau;AxU6F|T!LRZ zk`B!}k$qXf%IgHNWF9=yFH1OA;j$eGqdF9EVIR&RqHX$N#_o#7mkGZ+Q8!?tltW#`zVo zm)GtpiuvoRV|2iH^F3I3FwR-B=T&J)=~{trgbcMrNJo(vofUXbG2|!pdm_HKKLy)> zsUGzma75#^;B}d{JLM_3n_$yMpE|g?@zn4*=;|#7Oz!Bnk1OQ7F)vCZH-==rAY{4Y zcl(ZZr*y6=PZoh-Y6|RuTDffR#-GM&X2OfBDzUVi{a#c5 zUZDKfB;>z7U%NEu{2DD~Ro;hKg!meTYlN8D_|S;=Um9k#y0(VcY$D!b*@J|+`C^Rb za)V%#;@Y2n}M>`#})xNr}%m(ic*OY+Sxw1a-)G@y54vjK>`i%2E?0_A(F$ z^|A*<*Q!83GfPgDO5G7GUp*p}wd}z>%;p5x(0wp2h=%SBbk+g}58I@i+49_+N{%<` z#4wb1raVs;Y0R=-R70!c>mV9Vb0d)sKVrm3kLC+-3Z1KLxM~k6Y+B&SU9|L);cF7(#i5yj^Emf?75Ig-{3%JTuic5)3*J3e<`k#_0&QLV-f7uL;jkmO+LemHqb2;asG*K=? zj#;X#%mS2Ut2Z6!h~Rl8G?pNiEc zwg*+?E2{J41`zvX;Ljc5LWj~yPt%Chk$3a30P4Lc^{2rFlVI^d9wWzqQVp8clS2#* zd#!7#ls+N;NPr}FZhV|-so+)pCXpjY5@%54&js~A%)PyEkw~E{J1{^a+g&#aVu_hGJi_j8*ZXYBfQIRU-cQrtE&nDYFVzcRiep68EvSQEyba@mo4g+k!1TOWX zHzpCQZJArOUL}H{;y;8m=ph2s{2ofUhBO$&H^nAU9)7wK;mAden9K&JW$q*iz}5J6 z2^_#W2gGGrcZ3c6x0#+j-^N`ju@_Xb^oAb97P(g%Oq*YTi>Wm)v{E*k5D)S_Vsh3H z!Q-Fe&Evl{LF@+h9>JnB#@+6Mar$_L_Sv6n3C2gaJ~@d&e%{E#*`+3jxEGNg;*@P{ zfQ?-0I}zW=vk|_nW2Q=34=(<*LFD%xy+lV@pdw)&xSt>-fbL4+z9naQVG%0OPDaG= zi?-~Q%JW$onfO>-@FPI_>|fV`6RzO`H$)$tqc|N29A~7bK;MC&rqhj0GU&ix-8o<| zib@nU2P0HT#PIZc=qYt)-S3ER4e!rsZ;~^|NfBwr6IQ{u23v$b8hf!w7Ef+a9OSld?@@Hjo=EX@Y!lD0n^7Rpt^^=qY zC`K0~rNb(KG(0<*Z3lbmRp=uR5_}0wLX`ZS55wsdHWHyl_Kv_MXdCeh4!qw7cA|0h z5vivy2+T46Z2W=6D(?5b3flX(Qt`gRP^b6DmwKt>v7!XYh=prHi7MDI_$Ps;XTTE& z>yCg63E*Ge3mxD{#5LxX2!H{0NF6I?4#Du9BxU$J9cn*>(n0PH*fNnGhA8g@Y(QL2 zMVu*X-C%gJ!H-WX-H{LV(9pP~`N{Fo<}x6<=WGp7T%lfwCyF{)!EFBAQJp(?_}fhF zAC#(p2w5-)z}P_s0WkE1a1^ON`AMS+u}`x#X8z=YvU7#@r2Z3J&0iKo&#>oi+;u{H z;HA6~8#~ehS7@!+W+Y3Z`M-GC5i#t z!4@7+y7VG$VO8VAJcvvu!e^)$JlF`j>^X`*3?F&OW`VsbS@aFd-IqP1+8CTOCz%TA zk&(ph`(PUurZYrcK!jtu?7@&*Ob&N+gb*yl@MlCPg<>r*IAriklGxBjucj_*n%VNN zssUvlT3|0n^_cwQN31vrZYwCt8#Bg(Hzz?%Q}e0=xUZxbpDv|CjfQ-lUFb=+y?5u+ z`dlhpK=p9Gw9`HQ)yukH1h5^W*=HKi)_#!u9oj*twdK_q2=7|oeU9GjGy0C`@mTzMjA zVGgZ&Nnj-O;S%W;(!R3yNNN@vJ3^tIBJ6!O;^9R3E{qboHfsRslWA;X3FC=2uS!H= z9Jy+Pjef3WLqGUNqfG)dItf00OkygiL*qF_yr=j&Azn95%kP5VRSWYDVw&#k9*9DP z1S*Fiz4P>IB0dD(8Y`u@VM~@nkt|=VtU7tA4*}vK3Ljfvd?_N)0^f)9ENMFpG8U+u zDz+EAytxd04?YY4j696xW(SKM7oNwZ)6a0y5UKSG#B7N@O=Sebj5L<8eJAIAQPkoo zh(r=*R-m$6a_S&*^O8Xu#sq@!{hlP#@ZlZMcuE~^7)NaJcp>=o!O?ts2Mmq*nbcnb zuE|0nwjHsB=-uksUxidw%|&ag_`ghd{<%vej+wGxXynt`*a>Xe2eWaMfC?{&)>DS~xx$s7nw1zPwqvf)P>73pJX&CCh5`wni_8q|> zGCnTM&HdfAge0YrwnRM>@8|{p0-e}yD(+gSDN~S+nzAuO)HQyIs*sm1b}G-r|`24Ff>Xli_j)aZ!n$7U6bKQt>q0wX%78SD37{pTJi zOt*}n&G5a-F^A+Yf00Oph1_3@8Ob4%$doBP)*-xZh+bV#u@fL2l0}~j zB!f7mqcp!??f)?0`t3bF6Txk5b-f?p>@ki_;)SrAj(+3>P zII3cmaJV-G5SMtK4wdtLfC~FZ1Q?7jr~)!+Vf>yj4|Esl=XJY^6M78m5+ zLE4j%Fy1OgQevojilWR@JO9>I_@};CA}bEe0OqQ##{Sjq^tYE!tjECE;SG`(`&Z-Q z-#+W-C}a&cCd*d;jn{aD>~HZ*s(*FU{%v^|+TkVKo!7=3{*BjQgCl^Otcdz&&&=Q6 z{`cpxWdJWxQ?+Zs_kVv4yEky>_{wGZ{Y#tsPrtlh2rr>Cc~Bbnryu{5$MU!R!TSKx zhk81tU(nD0+uFLT!Ar2NxsO5A{J%aAG8#BgkHQ(K{c`D~_;2K2LWwb0h*6}y z7QR z(kMVC6+ap&_CLMJKmH)q%J33H-_=R}Y4p+GKecTFyy5KhQiFeES@SPL7F1F>=<{#9 z#(%cPAC{*7KU?E(heGH-TjM`lGqra~W#ePRsb><2kPTctQXD{IpOpx5-J?mwnKX6n>> zD5aa2>Kf&1D74>-{abVAAEb5!&{=%(oaIlywZ-FwP-8%^DRdE7R`*dm`*WMKqt}kVAyS40ie^EiJun*57~Uj5X?faT~F`$8+Lukd_>b0fsLh|Mn4=%vb zw88jcOw`Xn{-)7D^g1f<>qj>0<>5Zf{frqho)2IVyyIqoo)D)F6ZtnC_Y_BG3Y4PMWt17|MGf z*WYQ^Zfq%JG;joL({zN3FaZ_`CNSRZ%e@QqY~vTKY5s-4u}BBd5-(z($aCr^;5AZb zL-<&#!G+!gC=&MommvdWrWa`%;tJ>p^Q9OjW^;rGh`=;yn+AaPY=_qY4EW#|HX4RX zn=7?o(9@e1V>z0U)umY`jbX+_3j1m(jeA#^@6}tQ^*vb6ENO(FhIF=b!t4Y zE9U@UR~^EYs>eYbU2jCJV$2Kg^B(@F-()Bu7)G!#y?~w%PjUt9UHb9S!R^uT0_0g1 zwx7l%eHjAmZSmO{SL=Y&$qif#`mUPggi$4`X!F`m zv_&N+{10FD@AH9v2Kf5f;fsI%`pr0^LTlMNN2Zno76?*&4Bx8&s{}r6C2(>2Ktnwa z-3-A&U*&iDVMqQ~AnFg`mNVwU_y`>KEQE{($;JqCryEPHV5(~ZD58#cjR5a3VJX;! zW1e#1_Bfji5i{2J;y&pQwHw{A#bXe-FzMN#$RDQ6Bo@VTE^xy7fOL}pzh9MA1n%M{ zi(qTt&HPg=&_ir-#hx)TS?Yh==6`w&>eSHuzRKK(9Dkw8Gw~-2FSeR*Q`|e*pVl#H z@DxvLRiG`al)l{0!j?Gol+&s7OkdV!el(ZbVYbN&=cs&lLxo?F!%Le%7W5a?Q!0T{ z-1j>wGV3a}cu-63kH!{6M|fJ9j1EV4XTPalg2)qRNvrT6-%eAxD(_Z- zip6BxBUJgpMpSJq%y_Gd8woX10$D0DY+gB3-&rZB?*%*QNI(e2Z-I_laaLZJcA0W+ zkpDj$t`cPsrk4MNIW_#n=WR5>{r1*Hisa+BDvvjB+?F!CsLjaCdyr(JdvaQ<*2dlV z0?os>v6IJ#t6OC#Pb(~$$*3~7;4{iHv=&eFDC7$=e-#Fa?}c)`ssXr__6zY}d$h9% zHfUy=_eZk9)S4O4(ncnL5t2symq*^Dx$+f#HNC@3`lIaCsK{7be-;fQoa2PL<%F6r7WXBh#iREW*O^z3y2U7vH`o&J0^TSD6J@ZW z4($bA#VcvSqb-_uTp&Hfsn_5~=R>^$I05!9bdog8?6X#5116PKEm zQJ%(YoLm2V$W?^ZRC?-kAK9E_j6V(~SW~DecxhBzKDh`I3@)oqIt&rMt5zUSAmrvY zO6KT0x5+Z~omc6%4et_-EE~f02__fdMF^6M5J1dqH~>Dg+)(lJyYZ zC2@NGc~qx59^SCo=@w%otu;=lD-a}oGr4y-k@Ek@`s%2t!tPy4Kq(375K)E@kdp3_ zE&-)eI)`pVLApBy34`uNk?!v9ZiXT6JAU_fzq{^T=O0{41~_wOzk5IX374h|KG%&o zDQ2&*zGjDd=g}~JsOk>=zk58dmR$u7>wOk&AD|Is*+ZyNwgHpO&Z=CxEsXerKleZy z>*_G9y1GvL^SXn*zNyo5A*=5$+9b&c0I1@yWh!LH>37k10d9l09{NP zIxc0h5}@RzYR`l{aNVe{vJC8#^cSrmx%566JG=Y)eS0+M;F9rnWNd_7#-JtWp1GkN zEuk%J(uG>2>Yd#%i_YB`(=JWix|)K)u&tH1C7)uSfvtDddeVoIcXr+I7Q&mF<|#T( zoUNpdboM+kPMCbf;N#hETrY-J7YFlTV#jw3)s^mg)BEeICQ#`2xq}rKtW_NWzwJfG zh;x##k~#Z4&(MdVS4<6x;fFuq^1Un*6(Q%$w)X9p#lWq+WW@9BF9Y`GvD?GTk+XJJ zlKFuQk;n~B90kh(GV;j7ir18j=FZ-1e_j4t-_Bf-C)q*|^`vU1R@--b?C{8F7TK=3 zy=&4Aygo2bwiKNz^B63@TRoW-@H_VLs`s?g^=0=PW*Jvsy~*1S+h?sWN^W6>8I;XS z)l>=G!>DJ_$B4bpW z&(E@M|FpNo5!dE2VwC&H7y~#=K@rpfyQEn|@HmZW88*ziDZ~{hDTyqet#Fmz`(Lrn zjaU&RbD23^Ezsb;LA}CxQ{%I`k-EYDA0ok)B1#qd1&BT<$+LK12iQ{GV&(bR2r~zw zfLfrkW8B<{9z@myBgxta5RW~)43H{_tJKO=s= z{?&Mw-@(J(m|Qb+Y1NjR)=Ww=e?QRB`jWA|*s9C*Ti(VNjY<5Y0P1t!;&(9p`|>(J z)G8ZqnLe8Lg%cB_GuzBHz)6bays9R1VqkG*JM8>r{eDJE?vCZYXQk_vu@G}Z(J%A6 zlme!_DDxZ*}8x&QGMFhUI@I_ zyR7Po(FyoW!<_M5&aB4nutu-#)IkclT?~Z?U*3ZcE=9TS6mMNKu$ZLkk9RNbbeLWj zHJw+w+*J=~{c;=r&x;fxzKvy~rhInc+;aEEHlrkPqQzO?;<5<(y-p9c#PmE>)E=>n z@AsOl1~7Cs1*Q#j_QHdOb#ZTV-q7}!xH`)99IQ!j$8C0gX6$*;zlD8(J~L>5>=7dz zLgt}Xne7HN(r?d`xh&PR0o$8`nv$RUA0ZB$pT+!o>#YO78l2uA*VCbTsTx%Bx&yg> z9%yt6z*NWD&ubz<_=f5&;L3~SA#9E&FA!Q>AMnC<8vPk{c17Cwgg~Xr@<5+d;6=oM zQPcPB_MkfQsr5g9YlgQ*v$ zo5crr(DUgYEiE8rKqG(1+sw0a^#7h$Tfb3&Lj?pE-Nn%(ZlT zeVxvyd$q(6-@U{1YQOQ-KF@%K*ob|)%X-Z#6g|*GPyTcNK)9lSuQ@NuTJ;B~lqX^l zCkwv}qM=fip-Qj=vcgKx(dC04sb;6A5zH=tM}MV8?S{~XPF((9J>z?Dga3ZPuhaOQ z%&3i1pa_YX`aeoBsjl$Oq&P%%)N2D{OhrIc@c&x?-MGw)J za~~wi0YafL;%iPS9AP3AL zR-8xQQVgZIXzA#K9s!g70JyuMh}`-1K)|sL2;g4i9HwvT0cn;z!u~+Yd_vE*4wP#kmdt4ESM79bKFv5= zrMY*n-=SAXjs})bVP`;?v5utwiP-wT;~JoFr#}nlaad~I{vdSHMfXo^i;hhi-R57> z$047YUomN0|AZa=?^ufrJ7 zwEYGfF$x6;KS#`YZxE^r%9ml9|G*i_p)`c#zz^sioDpIR;3>k`n*l6bEkVq!#kWh4 zb4Hf|97rsI5r5C`8qDm3BvAak`GyU^OXUk86p?`F9HT>ga|dh50&LMl1aX`+K{}E$ z#rG_iCJ)Gb*rHm12x}Cb!X~=&iQltUx77%qe1LB82j7J*4>tjV>nNVzWt*S{5W>|J z1GUWN@RT0zS?eG^=dAeP&>^_cWP58^GH&|e-|g0a4oqPdzTnf1Afmp`K*RYVAmu|U zBMb1F*arR?Z`AX-1gXouN~#sl@Wz(&%`P!B|? z5eNqrMDGe(?!gKfW_JY3nOwlt26s=Y_oM2M2Rp`>2;4*(IJi8Ux&%_hf>+FJ2wg0b z_IlXUH3T>HL%L(f8SvuE1L)q^RkcvnqM4Ex)pFqo~e<) zwgT}WuutRCl=FA(M}q}iXe{d5<`>OWQ`S4TbMvm*eg^ZlQSWF_=CwfSygs0?{_h4l zy9ks4ES9}0jUjJPt4)z7`SbUi{`27%x+vNmJy)s5MnSWB;-Fus(c_Hf*(@6MXb+5` z>Ik_)FAKphP~wCP7Kyo-kHFu%nZ~s$-Jhx30XW~%5#$NP?@eRITKqZ}1&Mgh2VT$G zAfUD_O$A_hF9$3~>=3RwqX5sFS>C+>4%>O95feNs`3n>AwO@HBSSGB&Lu%RoE8jK{ z&?zZ~>J5nlnpj0^-k|6K-}~j_Galj8XyQ!kAgCHM72PGPnP4+n9kk5*uKqPaT?CdvW^edZ&WW&6PQ#B$LG*5Af-FZqT|dzk|8 zj<4nqeYMUjEI=)476$KVyZ13%SbOjqcr#?SEE2i2yQKrd|2G7DSOoC8_9f~0OKLv= zyyYR154S^^i!Hc{-H(Br>o<#pBj6AJMhfXw2TT1{4j(2dZCAc9tj2skbRAKbVQ4`- zz+J-Iel2;fT%R`kL8*9-x%29F9FF;N$q={J^-t?E~&31N3^wOyl z;PGbxozLvsVT3OWjc+p;t6qE1XrwW{RxfyXmfWh3Kf(NbK;-`Bd8L-cS@Qux9R;wA zEN{_z&|;4QbCv}&3NnoS|3U1#c_M*0y2Zv(4{u$+4yT_3U3<|OxpX?w3jLM@F5h#|J?i#P49|607_9&Oc;FTUHLbXO-6uHZGP`Yx>^ww7+bsZOnJkSg_T5^ z>}Hgt+x0f*b`zR>^972e4u#}c^ozr!L>-6x%6=wj9*sR8Z~v?8>Z65D6Z!&*yJN?C zt4kTn>+uaG8QXf%Fp85U`K$umqpPl*2|8~Q}Et3wR`kHEPCq` z21{D3(KPwb7q{ce;zlR6k~wd!qKdEY7P)oozw5nFnS1&cPNb#$ZvThSNcDQhC?xmS zY^Tn@^7DFpAg#BhWS~L=+KGv<8-dsOgVXD~3De&1DoT)G0Eb1ptH_#RO`lJ_?1Xjn zAhKx?i;dG(gxI&nf5AO;+u(LfMx=+OO$KWN7)dOj5xyn+C#k!?Zn^JWv|p`B-jIXy zgSFH9Z7hRJacCR`7{h3uFomqRGRJEk5KA*-vV z@<8I*?eV|2p~t0Th{(~=r15Diha!_y2S?Vz8{7Pf+g#ttg59q+WKF6|h#de1rGqH{ z$``s5iBzxN*Y)TNw-=^foS2TvhNeO*Vr!1d>Yr9u7JgZfT7cBG9>4`-mfECApy8{W?w=$NWZ9s=`+pBkAOr7|BQU3G;^P6NPG&0W$1ge;xx-sG$( z-}n+W17O%e8+gsB3x@jyk$osk{sB({x^Am_P`^&ho587F3lH(ynxZD0nl+76%%>xs zNsIh8iTx$e?536E#Z}AQdC8>DEW*gadnvNj5I+>*=Or%^`npP-#`n@lxUlng8N+bw zELc2+Cc))R(RM-Or9!T0tvvVA0c^zzFds;V#L3L{kwbnkI4)sTAT)3-cKcbpVnSda z;W2v%8fIO$F?@`DgekMnB7DGY1W_kOp5>0W)Y8^j(9$U}AmIMHvKlVc2%$2XXKzW0 zcBDd3J#P#S$PI>8KdBb|jd-A?=b~Zxb}!B$O!Py%`-#LScs1DJjBt7KOJ`FDG&#m~ zj^o9Z47 z8rN1XnsaY{NCgLOPJF^)?db!TlmuNTV^}Vu{ay;uJSKZgh>#X~i!Se@~lMqJjo-n7Y zYOEU4DX+8f`)^-Kn!wLUuWrOdmv%2^NGpCNQ*Sr;VpOoA`i@goRR%f_S)@*zysmHY zOBcQztc2bTJ=s)n+h;4COix1?9TwKKddu81V_a10n<;(W@mz&1uWJamSGjvyyICT? zEcW|*@3>!B#3CehB;XtVrQo!^tu*x-`B^bW)6GdQocx`j!;d_g2lc%=ouiO~{gC{H z!S?HV{dvZFxbuulO3-e?EqdROgOr(-ub#8c73|3*&nm5n@{1qJ4$m{w0q#D=VAhKF1x7 zOxQq~U6n8+XCzgbq9cwq7};&zrkJRAh|k+4R*Z$1CM&!%!tO5idXsso1u|cei)eO4 zdH1taOl`K8=Tgj4=MNWEAq;fjwBE-{bzm1*J3YNZi+}vIHgPA))&sHG0ZW~@lG)C( zT9%fFe|x;aA(gkgH?GNAY~RU!ctY@cP~_hGLI)UnSY7d`_8HtrS0MHZS+{%UEOyrH z1~M*Gixl!5ism>y8JFiABcO4A~7GVw?NQMFp8MuQgXk%qLeYg#ZTZ0o1_SXV*-sz)msur+Jk7fuxB9A~9=!!&cb9k5 ziNvleUQTl|#4xXRoQ{MS*eky;%W#iXI#2cs)2kCFtHwwsV~l65S;mW(iB798Ql(|! z9jJSBLx|C8{rVPRDrlVG#53`;X>zWI*~Zq#(RLTn!vG5nzlD-2eTopTik#oUwcR&B zZtvL4R^~0_soi2tHG1(*OTYK-R4wj=oqi$EvC+1dJ-=3XZerE2v1C=-+Ie-xw&yTPEr}1=`N_#arVRE)9Mw17FZZSOTAx-j-Rg zBx(6RGo&WC#D8Z1aN(bsi)q@O+QFTN4CBH)e{Ys|$lZ%#=e_EulJ>iw{`K+&sL=pC z*jRqGR-H1?_?@@r6;OW8n&!R%_KO-;T1D(HG(M{m29(LG9Un2*rK~@e{o)PCaO@!42F*%8v{Tl4 zRFTK_?I<`}b@}-^a7d0NzrIL(_bh8~WeVZVu#Sc5`1{|k$nlQ~zfyJuuqz0^eMNP8 zsnBkQ|U%( zQ%U7AQ{z7yl*z|j_JL0Ck7&x0G+3qwB^V8vd{_I))I$&if3^QE)TaC132;f!S8 z>7ktk*%q3ZP9L{G_{fWfmlGDbB2+wl86JyW&i#wRN{7B2`(nuJgq1{7UPfb=mCJCp z{3vqmbsfjwDfc2?o5=Q3E;q+td8CN2K0ac)K#R?7o0oF4n4OOCVZ|d&hnY%AH4`!Qgo7@h)%r6BG4l>KlH`9f@ z2KsJ3taYGjCG>0VGL4$E#ov3~LF>GyoVDhgQN6l^WX?{17GGZ0le9#q>1Wc~D-V49 zdHV>}aBx$a7nc$fT!lEeXs!CTXA;)dQfuulsl_CM$FeN$?;bP;4pDeuF7bUDZ71N! z1!MALfKqe`8Lb50l0Pb1ordnikiMNygi2pbjTks%P>rxvce#uc`Gbb;u<$R+aXBI* zBFvv8u7w|V2pSx%w3rPH{kr)!OQt1v&Em&!e4W>1!0?T=`ODgKgfawY!>oJ9>bfHR%*I`HZ6o7pp|1@pSSKeWznRrQ33qu8e-4h_I zwK~dtk+AF|wGbYI5u`!fj?KrS{~CEJxq`h1Rkv-G`BB^Rm5fa)KcE14#dbwskq6WDvTsleJ@ zh+0zEd@5*72qkk`Kq}@q)4P+_snm9 zdyz0WJOx+lrf71Dx80}J)_{K(a$M)Q^`|B$*si5elnL>~`PBE6GFFc=42gWPPod8d zwzf{`-y!SBlxDloi+s{0JK#zAeL&D*ATrL`l&KlHJRxEcmCnTfbC>LtG9pO&_+`lE zhn6QbVr|s8GJ8|a@J|tx1;2hbp0Qy<1nr-HUyZhT7f-1UaItF|fyyBlw zm~Wt?I6S@P`Xf6N0?FK3yKcTAs}q3QvP-rMiAW!X+hcR+WknYlk2V4a%c~?T>ZIAE zJl~cg+FvB4Nzx0}<@zI+#JS9(u4!i(zoZRv+C=Y!JJSv1_o>KvV{)-lKIMl)0l=^iN`1;yT<8cQa*v4Xdi4w&*@ z#hyM#F%2q?@OUn(Y+=?a_9~c2Vyj_j_>`I!j6pq(Z**EUkEo1sluGG0zVqsLObpIh zgTciqyAh1Yqw@&+N+-}K3B2MU0gk!ES~c=FcP++9UR$V3B!8i08z)rA*ZxAy_*=#i z$!(6U-Tc%y53GeR2U6=uo1P=d<+zngqj`<0eYZpMau!9}uNpIYr!!c!ou5|!8ON{& zQT4&A)47At64JFug4O9!$B(l>8JGG#C6`u?}o%2jY1kOZ~aK4r_uk4(_C*;qM}vSzj`h9 z`0dLSBP42nqzxn8CBhDZ$_y!wxmN++7x?Qin)^!`)Tq9*ec)YjaK%z|dw2WC(UQ`_ zrgcGLl_n;*)4c7-u6ZQiY0v*f5Xy0)CKd=}^rP@f5`=AImG`LmtbMc{h*)IWylI9V zCP9RK)jVDGm4fyhLS_i6Ly){EVLJKtN_Ust#Xt9-=@iWksl3cAi9JP{`uWN0tId+= zzS9zh#6sL~+(r-XZBnce_Elrg^@660Ys~D%NCD=KPEd*+L43GP*AR zK!%&uqO@=QFo*bBZrfer`Gp#0DbH3%)LI$)V%H=2s-&{bUl5`CHF9X??U5bIUOfg4z-JYO^2G(5pi$5m(L~K`=Ni+vlPc8?ii;i^utNZ)F&RbTJB)NS9D+MW>=x z@PF4Z`l+HD9{Jm#)#0Fvk*XqHBKLhV?*HP6MAR`K`;6HzL?}h&c?@NRq#&7 z((}WFLW;|Z@@Ip>-EE1yw!{YhO}Q?i@R4VG$i;{-V!gi{YblaW2-ezzR?UZ9lVDRO{hhm>N5f)c?pEH~89al? zFot|ZAB@FhJH^V&?1P`nadUC9uuFqE2wyyv6hVql6Z9xY6*6x0M@e*r;0NI# zMMsx(UgJnTH$5*=kB#rUSmw`VWdW|m) z)x&av%BW9#t}^PVd>({a5sikw@S{AMyS+M>M>8CuLFytiU~4*4iyOEHN5lRy@6zYD z8ywBcmT#F1@lY8%9-qDnGKOQW%1scGe&i4y8a8we!evoe5(St4V8AOs_#9?u0P24( z`lz2Wt@?G>=au^AgLl0KcWMjnt2jxYeR!@9m)nA|t`#r*NPFl5E~ZW^?#=y@H@c{h zGQ01wi4xJL=w=>Q+wA3rWcJ?;?^KoZ4zafxk}G_pyO`Ufpv4a8@0!$m10S^6K*E3b zTa$U6-yJa@en-mqGqL|ChYKjOT49o>n9)~-VJ7z2Hpm;dBpgVdU{EE-ZmHfC3t zx4D$8hc#Rqaby&HH=AA*f|d3)ZNLsT`l`ByqCHlU!}x;WI4WYaj~SiBHXw0$i6~PN zuRE{XD5J0m;SZ5;O^dpAhuWUTm3au*Qu2Jpo)gvg#&F*ZM>1_?^YV2Mp(@7vQPFtE z#-XxNU|n2C?}fr={ECerIIsHF?%+F+qS)q_=hywW?+6VABGM`SA@$fZp&9&(Y|pxLY8t?X z+vzH-e0)pZrpi$?m^$zXa49;^X9F z6V!POL)I)(JTdeG=(pymp8;ZqEd2t=O8#ztCMr6}Hi#(AM7LyzzFz-LZev7+vg{^! zm%Fv-el=+=25X>@-XPp8_$Xu{tqo6PPETCx%fQeVkI-8rPrjaIF19<}J*=S@CwfMi zVgrZjw5`;h?CGapgH4*0DORhIgbnfL1F9xl@H!h|Xeu9vl>KuVCH=`#23H343w7u7 zc3XcitCi%XAaW~z(@dgP+2Iz7zsg042u;cU6WoXJ^HN<6FJyg?YsQ7`BrOD&o{YtH zrad^>{+2NOxJC9oN4<)RN1-yL?606KiOX=>uv97YPpqMDFAv~PUTvLhJx@!9?C03o zaqhI!<_4jW-CESyShW&VC78?8&11{J$6A@$(|lj$2wL^UApWPXr0)BS*3oiPuuc%E z6+G&K=pjkN#-kFl;-SC5si53Mv8js(Nq{av@NQnVEmqnWXx!TG)Tna?3;TZhd2f!l zpR|0Kn?jSfVBWLG$jY|wVL8(K9ez{Y3|se7O{r-iB~0)z8~HhWt~x3sHF;Z3Iocu7 zY`=PLKTo>ubuhZA2N%OTpwJ6Tvh9KH7W8-tjy>jm%&WpoNqT%-li~`AboO&=xid4a zSjZA)>w%6}j6zSsQv`V`Ziir;Jk2*!8$bE5^kzsZNF?FGfqe)CnM zgJ@|!Q6%Gj99I5xQ==rBRWU(*_`DKT17*EZOlXzWu^mvj?`T~!P|MNun4T5w8FCjVnb zDtF#IWpL#T~(JwEHsT@5l^Ee{p;6ME%`hdY;*6)povSsx;C-N5D``Yv? zE`s+#Wy^2@PJ8hcu#&x-b5rn+EH$B<*S<7VD{>avfV z!e*^w?kZ_6p7U+8#FMq15u(EAxI|}TDAsC{$O8h1nS5iuQlu`~8e?-`l7}iCZyncz zr00p}Y%?`&Gmn$zwHM?}mv$<9t7OECH1!5bKI(EjvT*c>&e))9wtU3Z{WM7ZnIPk&0;DK^~l$z-h7T}8*y-t(+b1pP|6X7FEN zCAnd#Ve5taj_N;ftAEYpz_3|o!BDQ(!8(YPbz3Io$$dpIkFrSipiSLO@{yAZ?DOUp zcK8-XMC;bblE?Q?-Z7g-;k1Xa$p^>vAF%}oVy=Wshyq(v6U*AN_?zCCqC-R1kI$bc zPIrt|7KGrjdy&qESQ0cj%JO0loGJudz$q-B`nxr z2g$)L>Q;scsW{bND08z566FYMTrNseJA{vzv&pnJ*NFHD^qAY?`8S=F5vrkJpkOeq zwicBPI4og^dLiwPSnOjhxWJWaAs(aaNzBBhJVqTiTML7J(bjKvCf$yxXd1jxS*dmn z2KX{B*Khiku7!Kggv=H4Z|!A1XbPV0_Z3fiN5fl4oF2IGeI`&sF=Pv1Tvq!}^^I`I zCiibj>Kq)nSxJ1Dk}x=c(t#X#KX~txT(^?4o$~q7pI({gsFI6f0Xy;;H<}-&N2SeO z>#hR(&e=Y+aLX)kvsbj!EcRlL*P~QX;$ml24k2HlH}Wxa9`#R?UE2;oBt0y9Bv4Nb z9v?-_ z>r7i+o{hsG9+88N4(*1+g!j;&zebC2?sEKv4x7raA;(?ewyZ#?h2GtDu2RZ*G1l-Q z(7C}Mg&LxdG)-Ew)&kbfFZr&ds=wrNkDQXlSeo)BL5fwR#gyLP#Ugply%7p-a3)fN z$4fH|#w2tsI&u;m2WVa(F|-%m3TLC6@QZ3yBEam@z2TCohh`L~3bf+CmGmkHlPa|N z(&UvuHc!KvZ|}bR&a&@adYpP$CX&^2JU_=^M>vw#Sl}{>v_ZV0(+y@9yPe<%m=@a) z+~jZiJ{gYwY`MxP_o_-lI1OHKQH!5Z2H2_=@{CAw@tyHTHpCJ;Ul-S58IMkIRXc$I z<*|ks#qdu7Ykr;77>(~r&`u~=SO}2>C3k%yFPLOgUo`>Urq5&7rXFG z@!YQsLfWy*#r8kf4|fak8Xy<06n3>n%{$PM5mBSPq<(6pTs$+bP+cWHrIDU1GDi8d zN60+bXJ2j;m4U-@5p&ks8|rJ`osJmgvbXzJNGTvOh**i9a8X2yX|yoDg?zs27$O@9 zT5bA>!Syj1FlKEOTQrhX&Pr|LThk7gh=?6)4T)PMhHAP2JJ-vk-5uU8{_^?p`0~B; z;JJ;CS?z|~v2FhqRB1YodS@gvIHt~r4i_!b1*+RmtAVBBDTl2}Q5P(Tz>Z(R?Y7WUp1zNW$+0@nsYx5Z~e$ zp?RNEKB-E3WZ1-;eAOrZ$;e~J1ua$U^lntY@5lW~y{sq@riM32~KP`o#_0J%FmU=Pgv5qq- zw<&6Za=}XO8-m(4&wX*2VlLt_Kt*$(wJ4ok%loEu#=l8tp}Bvaa7q70-_^IBLaSa7 zBIG5fl!-LO?DO(70ph95b6i@16!5>&O^TCVXL&;;BULk4f^T@(Hn^F$ za6cylY4DbuknY}R8U4yFs#B*6HqP=yiS=h zb}m}*?~Wt2nzP_*y@v!eS7{z$93oA=-e>bCy^c4`50Xke76e#t*Zh1E-^tXz%c6X9 zx-yflZ1PAdT?08=VgMiGbj_M3vgGmK0>)?2ohpzZ0%>|f7@fM&FVr`WqRJj``Ul_w zgL7t0&X(WIj@8T~9Z(ZbNd5tGi9+wMI`~A&_;pFb^ww`4PHVfSKnjyQ$*Hze=2Zf= zNZC)~!Dk4*DC4xA03eI#QWH%9VoAP0Bn5Kx@W|0oY7|l}@?Be86Pp1mdTH>pl#7#K z8}5ibpK!07zw#%GcNaC-G0U--FNj>OgWhcf5WLWcH9|hIh+mCAZNh?#4|1zx)05%E zDw*=`%mAt+UqoSgbGo316Iy(k!C?q~W}?rD z*_@@n5Vqo08`nC0W=WlS?AJg)w)?4OJovL^I6UNAkC3;}M>1mej;A9kcKX0C*?0J_ z$?1T0s4!hw4gqTrTqxmfrl%DC5O)oSklhRr>Z&?kb z2kn}la_Ul&OSm{`HPRrrnxglUdGuDbHtkgJ@eg5Th=} z@X|@~OtQr`?RqjL5^Geg{VR)Lsmf59r5`~GxdeABrVlYRyS$?pJHAu$DK_HXH>O`L zk@;-)YB{`?@uRrJ_mW&A{QPWlnpDn{?Z$QD#1Vl7bT&VTU}?y|bHT{UdT)~9!nvF7 zt`v02oANS=eQQwBSGcurDj|oHOu+RQ>Q@fj(KUA8_mL?E-C0D_Uz|mM6Z0i(DOrxa zrVv#A7(id?j1E z?JIbN#v2T(^M;}r&cFTNF7QJ5aRBWNNR)Y#^V?zJT z+3xmo-7bN3a2@sCRq3<{izAm!}-bA0ACF@ z3bQ4j@_zpCQK+S|HuVO|X>|tP=SnU54wOB`OHc6+hrzy?)4RWYIA{ha zvr_oItTwdnh5G_l^|P*|ad`(?q2uU#5gF{SPe(B>1{R<_yEq5kx47PWkCS{4z3%&D zL94bCS)rnX=%FNV)7=jXaCL_c=h$%wo0z>8FJpRZwxb;v#@E`6k-gd=eUbinCEjBy zN0wEKMEgfZm6;XM7yeBo!s!XyR}HEMX{F~`3%!Ey10L4$J)Nxn?)3Yq_${yBHERu@ap?A` zu3^eK_QL~OKG7cTXMNa)P*H!$y;EdwK4e=peKchaJrt*GBnuVKz3<*%ueJ(S}hJU8Fu{SoJVPwPu*V(a0vbz zqN@fc`Bw~^2ydiLejqfd0#W6yu$30~}qAIo`>Sv&gPgG@aK%l4jl zpU)2DhJ!6`BV}Y}!a#N?+Z{3qgohlHN?y_(#y9V8KAv$7h8Oq=h8w^8q(&^oiBH5X z%GBZ2G;SHXgUCR1{XXp1c9Yq=0zgpT_FCfTIbiv&8y_)8K(?ux?!3srlNk&R7bL?Y zo%x&V_V<)fHHsVc9AW3KT8@laM*?-s{?B%76>Q7G372{P?arP$AmJw8|HsC^g!~5 zT&_`DP~D~(4K*Zvvn>pfQ<@oH2Z%Fz5y|+^PS9Y~4BSV4z#q3Mq4!MKxGTZBj<^$5a?zhs@QJ@&o0suFsy>Zp_DDbhFmS zm|pY(#npTK-U*UsbAQD6L3FU$5;_+zTh1nSEchr!*zo z%A-8O-7o&eRwfY4(>Dy=95|m|@>l3ekyVBYT6m@!uOIx90!^uBGv4FZ8*tHIV(6Kp zEG&1Q991y`9RPi(*75Jv>x0JFbN*esj`yh*b71Q_pBLy@3;61sTbZxtoz;0*hs9Q$ zk=}bU{=LG`BZqO{DyeylHpG-n9fpmgG7LEv0p?hsUZn4;ztDm`R$_ZYAEow}P~<%$ zsG%43PJd13lJ9Faj!nqE*_V3D!Oys8y7;$`YVl?tw1p349Sf;{-L#B6t4H7a{fb`i zwe)d|G0|ZE?<0r76FWvpf9#mSq6bVD_~K@+VR<^mBRGpvN)JY>CF#zDTH6l zcc}dS?C<0IzEMbp$L}~QBx_LcX&f6(=^$I2Yu}&ggZG&Gdy~J#A09p=Ek??@0f`(d zJHKbYe6EC@COP#5$JN7;o_dQWOT4q138~5FvWK|hWrmz6Y#_Ao8-QsJH9MYBW$`bV zXTJHsH}uZG$uGQN5_#G8euNMI@j)uj{h%Jj)#A?_YxIfLni+qg9l>)r=bTUpm4{xS z7dZo-AuW;6lgLWRjJr7who=mXha^_g z7{H7^_@ZPVb~VsTucI2W1^c-8ad@BPVEZ|;75ASTl1@LJeKq}wk#sa3J+GSZ^9;z& zT0a23&h~3W7+fD$h`!bYcp{lSHDP>#qOO#u=uFT^U`}R>Zhw3{m%K@_a4Y`7MWgm? z(RH)WAD?rsfe87c*st(+L&Dl`(m;+lp{va5+hvpCuGq%P+)vN0=*oWszm1_MoKg;; z>NQ%gkQPyjFPrQ~pk7#Dp;Hrgvt*IHQ@6=*`14ST2lX+|@UPl?d>P6ZbzCyYs)mfism1t$K*fvCoWqiFUsoeH#N*?5Btqmd7r2$@$Pdb!9yvVp zs~i{`HeRYxD`6n(K9QG{TP0KZg$Q985D?nzn?BQ0(*wlVugs{Vu_7lrS*b=AsrV|5 zdR?Q8gKpoxGxQzSrAMObziEeOZ%YpX3iTX{r>4!PCJ?#MIu7k&EZ{1n^F@xTo{KvV z!P9Pc=$auot8gs-eA*f?(GPY%RR?!?$~2&d1J@>K+Eyla;ew04LW@wNcY}65P@I7xWdY)pgriU&l8;X_`pJm;(l`@;M3R0ju zDsA{CnYVk@S{Bg_bA8$eV)!D) zPQ;HF#CJdX!mcESPQCf2myZo2u+=-nH_fr_Y6Zkcb}xk=0hy4O(iI_AS4t()8n; zB*o0^YD6}?oXdm4zEXk5ApMErE%%@{Az3>J=@$Cn7JgKD%eO`TgW6q^$3`{xi68{zhXrhjzZ*l9cb*qf zd0uYu#?(y~@?bc+)7PoH(`x7`zicle7MkCqAaOE8`olmvBaVE-8j}R&6a^eTr^;{WerohfazE7)9Dx*S{M<^Mw=Y!syg2DJd?dfNYER!&MnS`p2Ak>AyS2G^&UcuA zm8TPI=hPmVB!6AK4?U>kt#}vdd&TRv-^Zd9SLOLKMaPN56T-PU7_Xiw(xzAEd@a%y zs;7CRCZ(Tp^J9)%#_syHDw5)*`s*)R(4c2#k-}^|*V_uHtWeDU`u77Qz9?`;`vw82fcMscl z3p4x7xj@|hp(}4DarC6}vlD2L31 z7&+FqEj@bKw9{jXAyLG>7>AV~Z#=SuLtvOByHTig`7cMLYhq0D2OO`+yLQ-lS~+)Yvs%B?g4Ggd zue_j4ta$gXUeDoY<*cDP?5>aB2=Be%x%#%}RlgRHxEpD3vj>Z*ZFP+wA)Z4-=lVN~`CBIEHcUVI;`N(6 zyMcKOQG$sAWA>Yw%Hg`ZWRtmyzn#h3pgr|s0j zo8qvY;Znux`@?G*xt_ADQw9>@FhV|7Qh1A|3K>DsC4wXgneC!O%wR!&qsYcD1hU{Udnc5h|K%5$Z57Q<##RH7GM@=ChXbwBH-Yft?`BINh^@RZP!@t=D*Srn zi$cy*4!4;S0})7pxL<|pKJ~YoVeD$cfGfySbVp(vqbzb(I%BA^SY9m%0YY zb{)ktF{tR`rdCCKMEML;$-t^TM>>3JonCDVJr4kf z@L@)xqdqHo06Riv^RWIWFJr0vK@&`Wd%D70usfrIwYth`vDqi%mtF=v|1Itznzb9| zd?d=J_Q}z@Px1Gpv*C^#ls)b2RNgecAd0EZBZP@dF4JOdFn-2~?fP9>C3iwo$*;{} zn(PV=ao8suF8gddk0f0WYx$8K>njqp^y|dNy)m}foXfWA>fH`A z<1m_LLGa^R@$VMxPQOabR_Zz2?51?bcQDA^^H}t$S(iyYsV0i&!pt9$QMcJsUVSg@ z#HvA3VDitVvn8PSGeNxop>BKFQn51~Xx%F{1g+FJZ~uA(?yxf~t;S2Qqg;$p#1(28 zs~+y#af;n~+e~qp2)2FfDIBXVdnbL|i$WSe_=oI`&~JgSBS0%`r~$b<5tCApJTl)<&p7oeF==;Y$g(n_Op`Np`DFc!~b=tbr?qNU9_2 zY45z3OJf8SsHHr9!`|@!Mb=wJMfr!@-vc8I(u#Cw9W z`kIg}thU9JNg2p{6K&lrF&)?U*T`R3Q0Y$Cv9r@8W9d^dwZ?%b2&NXD#I2?B-O1*? zh}7qgeMFya+eyg8+Wj#>QEkM3m68u9)y+oqnPOvZw1>SvS9|j~1-w04YRK~TxcTaF znos+!AK@wcM+aB@_Y`Y7;U;D+!Fu@RC-dBPeY<(p@_9cs(eHm8Krn6t`TBonVm@U~ zB0Aj4x}luqlo^`g_-IeJ_;~a3gj=Dj=#RYubw03hxGJz&=w*|~7a_h|D+mjzo1#E{ z%bC1gIO1|bf~%hRV~raK#=lkT|0MJ7kC@!X?02U>2hv=Q26a32^yqAw;xu_nTGGOD zG7`6z@R#p~Lq5DA_pTy)1%kuN1|f2oI;$QRCVEAs7Cn?a)NOufKyLi7_xYvAFGM?m z?BdNB^IB;r{_+%H;+&b{c+%&U?_Io1VCl`mt+Jm!(Q4PL<1A0SM4pXYW_|R^wVxJIj4#jXEK>}%~8}q%dyt=KvQ4~ZOW0P1xE=O4ho+AoNk-+VPu^1&7Hs^7VOF{{T zyf|P~jz{4{2aUmnG4U9+|%*&@7v+-rPVr^J1A92eHzfO`X+n9(VKMkX6!`N5aA2 zCXKU=eZ_AM+9(^CF?nr4a+08gb*EQH95PjdcK)GglSCD?|9FQQ7WAN-QkM%i_rL#? zp#TXYwwvzcmibuEY)$I9+1jCC- zj0!4hZyz*)4HYwlGJ$mPR-Jc%+Ay4`lnH46w<4!3VF5Cvq*TSV!7y_xy<=Hc`${LO#qi?iId-fUhCOoCRBP0ff+jzj5a% ze2SE$(x;d1BwiZaU$UON7f&`OoruK9RV5w=+?Yg;Xt23t&CMvY6MM_G*l+}A=mHd3 z<6gX}5;|FSyn9~J;S%u~4Jf@m0e4QO$XHv?36R7GRLi-xd~M}=AX_Hbm~${X?2#e5 z;fO(frx*2}2OY)6mQ~%%#-HY}vq^rD2AC{(cfFED^33EC`_Sz3?~3)1mGVXWI0rq8 zPXOy&1lp*fWr_`_WHd~y#d>DGZ_>tOFb1@(@#WH;r6_?0Qq4zi#9kB{5$mIp2b$}E z@yr&ZTSN7oV-}Ps2m1g&FpgUAQZXj_mqCB6KdrfLQg$!c_`*>uw~O;I*h1q5Cr-96 z&KzOj%XUl9XD$g1ZsQ(Q$lg{HVqV?)d>T(Ip&YJkLXT?ci0|0uxCW-8F88f_;a#QB z#rZtIShFoAD)~ZI4%iQ+yOc0`Yy03hsq$R_O*fV3WThRxUF7zirF*k{z66Q$TLf4) z?%s))c98HH>X1nW4)L(i>UFsndF;US+-D$l zo@;!`{$|xua3Emcj8Zt}_MW-HY{RAcUGd>*F>#38JWqp!IT+^fa7xTlOT%-0Fx_f} zPF5uz!k~-f-@?FNEmQj4r2OZYEb~(L-Q~N+hp`7QL*^z>)0%q39noKaD@(zT9oVFdMjKbW0vHXFh3l+0e~_ z5>vJ&q96Q)PL~!F4#lPkX|!hkl5_p!X5n#v&7F`=q>JL;5{A9{?2;g}&)wovW-+i! zdh(I|=KD(w(WW`-e!s-I;EPJmP|^^KA8@&rB=}>egz!}o@qWqUplD-v`P^;h>m%?_ zsLCgwxWO=PH{dB@hEco@Xi%vIc|qfp-;py)&RL51Cn(rf*$HEZ@E*vl zB;fqN7BtWU`uwFBc}3T>63#Bm<0pmNwbn-nd@M%|0tnuFvl)$f$d9obWM zh_77rBC19JV;YM2g0$8fE2x#jmk{`6fA_*7xJx=?GR}=+kOJF6Z z%i~>i4lkg4%_dQKrnMn1lBGakA+(pSzal+Cu|8u~Q4eA@BK11)% zpKBtvIGGmkWyE5|-3W=O%(p*}*(Ln`v-ahI#Mm-7X8PSryl5*p6|qhx5ZFyTcx`YY z|5@t76QfjGU7DSapUK4T-mk5OzV|lP&L=xToxL)gj2&am#noRja#{5bX`VHYXH|r< z#>X3V2CJ}~zRiZS2ODsqUe3+a!r`sEIOzSnwSPQ&w))^lvDVdksY`xSdFcnIKv8Vq z7lK8b1E^uwF4w(NLVMwP>?JTcIvxL1H5hHfm5r1ySH6Fm(vcfvCtkist2 zRr{;wfqUnA7T|> ztmE$1N72$q+FzZLxv{UPI`y@qZ3GWa#=?4<2L z0y)VnND!Ve=#O#hf(NgY#e$qTwXwq*yR;;6#K9|+1~x|?4kSl`XX` zTKgA+rcoNx&Qg<7%Pj!mUtddo9BL_Y~!QFvqXCBJ;e zvI+8TDQt>6CcULO|CUG_!ReA#ajOv}DyDA|Le2_-~&Zo{^qG#UBDZVrzPX{leRfTW#RDk+#-nZUEZ9 zStzMkSAg>pVkYFnnMI;Z1u1KD>{7p4ialjMwRSY9iV7X)G8#8A)P)MZ=plNS_-xSE zy*A?+rJGI&3lAQmNoew5?QXJ952b8oO8W!9x^Z(&#$|X$L`(D6ThGm+06&{5m^Ef5 z<%np6#~XCBa-EUoIKQ1`wX%rr)n(ip9X#ao%m%<+{;jp3P!hJugR)ZJh<{X2taQY$glW6jt>`>yfWwBF9SCE_ZCEz>Z!i&$mY^Q2C*~I72 z5ciB*yWD`3x+Z->*M9$apR;>9Gt-(#`g2B?M9>y3W!O)&;gQl~ZP70OOac;2!Kfkv z%tjs93;&x-aFIUGCGg`8gNTz;N-y6+(BG>lFaJP33=BD(msMPFKyb76ZwutnS3l&d zuV_j>&sn6Enti*b_@Kwxo_)5qCoCfbpPKUVLh{8RUwX$|5@fmMflT@Zwsi4THFRvS|UV( zx|A?(n(sPxm%9*Xe(X{bT|~B98{(_WW-j#gBMo*L9(Q9?B`e2|dw(LSIkbr`R$+s* zfM^}Eo~^I4l|S2w*B{FfI^LT?xNd_LTOL<|cLwK$hfR$iL9Q>wA1#hxBb5!qvXOD^ zdkFigj-2;GDx{Bd#b>!c170BK3no5iwp+Y-!C=2E-up!$CU2MwgkYALiUpf{|EOcD zPcR#0zMAIUp$vCi`DzToK_mrn_POQQ3FM`Y?!hGhzsI@lTZn&+9+-Ny>?dlD{=51_ zpRv=$>(V{jdpnAJOLa>lv6t>6BlVWMlbAXB{nx!NFJYOlAYArT!-ZUp_4&X&Oamcj z^d0iAs7uOA9i2waKWP@*Cas?BjhZ6uDEwnD$DS~=t;U>Em$=X>L*cX7p_yM!_JO|M z^?uq12nYv~IG7T}3wn^3Dty+2&v{kH_R`@&j`>JglPfih+Y~k^LG*C}9eidZN{Iy4^RwGk z#a!lRg}r_K&kjhIJ$<1pr^pDf80K+~5^Vi@P79lg!&fAJgpt8kQnDzet?RG{L^SvISPjc#CLD#=0bH@=ZQy3CTm)iR$Jhfl=2l z@)UUM=oZ(^w&al~1b0+1)D>LUrf#X^{ATGU#vcA`KT$y*V&Ytk>;g@OTGq(#ypIo; zf7ok=Am2cj=LLs`oM@V~Xq6g@`fE`rguYuAzqDdSA*T{s_F^)#`jSv`{3d_yv4A)G z_h=2m5AihX+l!#VtTzs$b{cU3cMG3AdM)r?WGA|HocmgcsZ5hWJI4T(fp^=f$i5%c zH>e?n#T-1!SS@3&YuY8)sprp)j)x}Z*8FsXBuuq$bxF1}4Qkl>gmvRD>>Rb@K8kX3 zSd9zk_vikP;7D_})MI8M;_NR+oajt*L*fHJN~p0<_XXf#5?rZCihdgBxkMUt@P88x zBkce6oE7ET@Xhbj3xknFMUi5#CSZqtAl5*hU8nBOeHn`+--BP|Sg6@fvII&sIA-cH z;|LGC4J8ZUNhhPC*S{wqB~)FZ_2{j-|DDFV9cbV(YsU@eHEE&XgDP-+{W0O%ePs~| zz9y`)X8=mK;(2b)SXf zlCi2zVk5r>spm`v>F@17*`l2ra6s&Y}f zYR56OV|_qe%GoBw19=SziV*A{J`4Xp=H;BEXC;r(OCWgyB>Q2?n^+y5iFtgVs&@>5 zqF;v>qbFxH#9&8pO(o$X0Yi>sj0mGB3ryDne}s==DI$sScxYoRVqQb?0MeIy7u@{% zPA_OE!=AtGjPxO6u<5`t-u0m9AtYmeXi74)Rra#!^MYL8U=FR#T{*Lj*LkArK5E?X zT@I!PdyMStnQ6TUi23g0+!Hf$QJ)HXBAeX_6mPx1;CV*eqCrMqvaE88O z$7pt6ma2jndIzEh=rXm0= z{YK{srm;fQn%ufq{H3ookW8)!2a~lZk(r;^Ljoicb7VL@_cr!M1n*Esl1`OdqD?nI z4=TV%wMt`T&+e1O>-mO1^DPNnW8J=1Bed%IES?WM`^mL^NkeH-|D^`d1%tKXWMqEB zvZk<-Km49PqdAzF_&Z2Kzy=;0o*=3KnHgb-nm{q@wv|d5eHT$p;$=Z5{(~{79zm=F z##tCpI1268)m?mAbKd^Kmy-};=Qz&$?xi1q-mb!plLrAIju6o%u&>A=wu`@3LB}^w zhe26Yf8NfL{!&tO#4)`p{kOor_Ib530!XX7u#+;~`((!All|k^QZ-VZ1U`RpoWKNu z+ejyqA*gNTTqhFIjm&GzlV2kuPSxBS0nqpeIQW~9v4vjo>G*>HKq=vk3unDooPIcS zKu3D7jGl>pqxixn=q?u^ z9h2$dGREVlGRbumwKQoJK+R1z+s8K7n*2%J+KDU>te6hfUADqK zI~6}$(u7cTG(`t{vUC$NfBu}fhAD$(t)2jFX2VXePw4Nd0Ql-of7D<$LL(uy;&}Yo z;{dU^52%LiW^&Hg{W7%S?{*{zUSYE@1ZXO+<0Bir+D8dl=stP^8(8So!Rhv(ZQeJp z)^nkMB&>A1fLd+-99d4j@G7S*WyvCvHttzq-Hf()O2Td`Crhz*i%*X+Rbhxmm<^=7 z^1F@`x*TgWy%U+Yn4iX`fc>2I;pVq1U8dz=0$skId(DCfTrWSygQdy^*M^mJqE%TJ zm6MOAMXD}qS=}{6(ob~3qdFh99O1m=U0OtaS|dH{3{+(FZWpzyr}h@}t#O6f)mW~t zK3Y_)Ubo(y^^8~PBL8J$P`M@p4zy=<8_y9PfI)x-JrQO0E@!`Ipwi)&NfUQdpHtwB zg3V*6ogeev0@t4+tg$3$`DiPzQe3EvA9F_8Uuiv2X5oA}K#O**tp~K|sW6iWZp(5c zFEbrwWMfh<7&~n<>qkUeL&X0qUw(p7EDkp_^y9`>%62F&d#Na?3KWn@oESNN1Y=7l z;qZatg5Z%ks5#qtOcE>{JAk_o-gmj+7^dV(9TuOgQZrU8kT1!CV9iv3Cipv*I0HM5!U4rY)W~6|7wJO zFkfe``VF*2C^Agk<7T;czBY0j2MT6GlG&Mg5iFZ!wK{_fy7aHf>B4 zr1IHqTqNU8?VlnsB!W3t!C%iSa%{yBYh-HcJ6E`FIT7qW`D(xpk*q}W5ISWXt1 zpSTe;Oi{h@Yv7%w*H0Z3V9=;ETbCqvL4NvLANRxO zd(64scJ$y=nj&xv+1DHlzRCtBdcW~n%YSPpyC?`brn6^QB(anxS&^i?Pma9dGvyED{`93V%G9UDXuZ5|00HqycE&9nAgD)Lzkc2;sHzsjq{spjU z7hBTX;uG6r5sX|C4dB@xQo*aKzHgfzZ{Q2pF z9UJ}w*~a}Ye5-8^EtmI82q00E0X+DjQh?^$*XMG$W`~U-Yl97kIXn5>3{OB7?VY?S zacQrCMh*eR26IKEd5TgrHirN?)s|YIe~FDiIQ5o%V+rzE6oGnuwE#V>XJOBkpeM%wB)3DjyauC)8IwQKtD{Hs%$7<-%^kB9ZV;$oSUI^20u-M*04c zjHyV*jjkmMM(et9O2vGiooQ=YoymyjEjVG}VwiT5b=l#VF6AikF5&9tJ1_c6?xBEH zRUFf6!sD&7!FFDAS-7*tVMBEVQrh zEu`^qU(3n~)_b71oxh$MDKIXtXqdXxy7@bGWm;kNC~<_#;k(gpnK|K~g)B8tQoI_) zpv6IuHC#kVS?flzo8n-1q1C7+FThN zp+0}#HP?VE<(ooP{?-dMWV1NCPPb@y%PFb0L97~aw2pi$qQt)`>Y6PFO}CSklm$!T z2abXIYOM|4Kk7&1RSQtvbfX0v%jaKuPay@d#Ckr~3><1;#~i-40a=nR2G^e8W#-~N^s=!`p=yFOsZ7r5|S|e zHMB zTBL4TVGEp>j0?lY_?#>Ns?+Oh4VZh(>R+2&I#}EppHe)n@K1TXt1-XWSMFsWTlTv2eG4_(K_epm$_ja8Q|9eI1CHbO@ z?^uOV1WD%J8M54Kt|Kc)PVtaao!OIadCKAX8gHJ06OLME5)ctHk561WMA9Di|{&2VfBgm61xyCwO zt9SVIeFWHeRznuYRvs&XGLg~a@mUlaGF#eVaep|kR3XceACT~c&R&{M&1qSxBn0HI zD~y|txzP-0h-q31cT*K!P@4`s_*&ISTt)53jZdRYRs<;Mh?umCXo)6Gq+ANC#bxT7 z{xRN&+7aI&Jk)18A!gIXT#n>Rd#6kMqAOv+1K1C$i^^GmG$!THINJq!5K{%oyMRg- zQcc7hO>++0n}ND5sS3$#z%Mn#sy9}JNayJC100EH`3K7KNZ-v%;)nQLzmZz2(3Hz0Xw zS>BRRl)W^8wP?>;xxob2F&u|IW(vtCns|z3VmmgflW1y?Vh6_KivH^=GIJ90SkD8+ z{@3Md?Qq~PYG4D`yVN#;xOuxr&U!NF!P;s{V{=MWJyZXDDqyQVsvEfwJ&XZ=1?A`?;N7! z9w)^*VvE+mv2|NM3HEHxtzZ~)p5agt7K69$)+<903*|>%fxP5(Ud}u({D8GE5DdUC zJ&eahl_bjv#!pMi?jsY?SpvKhyI$>!6>sXdu`GyNA>|mYtoTt7u|?1^Eu$El^1!M; zV33aaSebpL!Km~(q8`YR>P1Tr0x}K;fb_eRgpk)W?LtV)Z@It9u0(UF6~t{$z9pO_ zbd5>)mgTm@UqEu~c*@-iL|7nyoM@X?G&|mk-Q42SbJ{PXtU{~squXxDXC@TBbwy;; z(ox@M9P)`HSf2rD7xB?>O428K$rom`W}(Jap|_)W{8Ylj`%J}~0+If0*Djv%ZaDSD ztmNbOY?v;I$qZd$)FfwB6Z$MOsl`W;6pY+xyFzjN3(Vbdkgeh9N&-F~Tz|TVPdI1t zbM%0tKdwf8(Gc~dRW4_H&QPEA=Dxm-tT*6URlD{>EoI-HZHxX=H}w<}UMX>$k%glx(mI(} zRv}{MSuW}^u#D!xF(Jux7$L2DAqJ_4kmu{`4(^_eSBMVHxKU4hEkGbNi|FaixBO?1 zbfacFiU7lH-}$$D8hrUYCF+CwvyqV&l@9zf6c6^L=-81U7=9+>--on2_~!(x8ry$# z#+9G@?SCQ^ALO0yt2RB1?ajlPcx1q-)jKIaHL9t#sGs#>1+bszpYcp&dB9De~^2A*ge z;PgQ=G25Ir7wT*fT}QnMjptuAC&Cv#AC-)GBJdeuk!~{s4QqavMSjNf=IV04Z_MmJ zh@b?Cx#~vg6pYS}BN^C6(ZCf?YA+GuyO^5%_8e+5!vX9BFYtb1G?^ z52q4^pXoHPI>x%^gdd{R~cT!j!^RQXb?&Q><`>nCCv)|+T~8tIW>pOp2<>(Mle!Kgv?!p z$+AU&!)Xg8V2-J7qRkcZvk8(E1iL*uslwmCY+KKNU^d^YiH4ggN}M z<&cHULO9*}0;rj<`z-}H*%4!FRUzCX^x~JH;&|o<#~k2FHX}@GWfcw59sg;w9FI6# zz~L-5IrPgAjOn(f0>nHthn&39)zpEMj&&{yuMJZrkU-CkU=Cm93MgcJ%2l8V)svI8 zJT>2T*KX9d6$RL34N-r}Jg_w=$GVdE?q`5;KBN?fh5q<>oJkipBAm+*7kqrYXDacx z){3^V1g9rdk%UE;R(h6W`NWbiZQ?qsMt z|J#PJBEQgSkD{3|@r~sYgK9u13crxJWCsfnkAK+ULXH<4Obw&k+~w_fy&uvYoc{YX z*mugRw4~Q8CHhfg9lSeLnj)xSrx1ey)Y3%@Y6QZMy%Z=`7K&f;5yl-u~^<-0|V9xmke8bUDn&Xyaf@z*eyD0h*?VZhd zXHn4u@5;Et>2RXQX~9L~fN}57_~fD5gF-(a8cu+LClB--Xk>J8^=KP47v0TC*qA@U z86fqW{&ZV0+NbOH7&zxRhNWt{E~2|J;&nB4A!Rv5o`>xuhUOxTR{Dt@;z*k16W}SA zpm4?5P#f~gb4zT5b1AvPWogmkXVC_0n@;UYLD|x;f|{8lB^x^$MF^?!IYUt!Ij^!o2~Jji@QevhN!scLak-7 zdwDt*b+oa>+x8V8_Vb6!s~i3y_*!j60osTVnRmK{7~?WAX&kaoc9i%Jz0UytR2TS% z;g^z54wdeJ`@7|*VT%OhtSnfO)l7MQ2xWZKI?(ZpqxP|}8QL%43QJWT7FAJx2TsF3 zB}VueM&QAxmAu&oL}N+`6GXy?Wip}QBcEbjm@pxeR9O$53L2^ZCau$HCNy7f`(s&)7DgUf?8~65)b>HZ`lfTIs>#W^D66AkF93!FpM1Ai6ie%kjcOv*6oxq|@z0q8 zKs#+E&{<5WXgl~Yj^qIY^0etKYgV##DYxYTtXm4sGhYV2JXIzfi`%jPy9DJ#IKj=! z{o!n3FM%7HYQ>F>A56Fx3=B}s6m84zB|3j~BoNiUL=v2ae?dpUZ~n3=g$AeV12x8` zqTNem{xN_KY{&>t4gAv4*9!zH*nzeWW$hf|H063Oi_Add64~m_!_9j%kaR$h;^Ck> zdp_wZB?Fr?WCwXNtn2Gy1%bGL&ldtn1i_n)Lh7thbnX><`5(E7EV|WRA4OTO)lhvuD`v|_vn}r{We?ra>&91Gl78jYLYI*&3Q0Tp z{dT;Hlc|$mKltGnH>V7~we;RRH~O9?De*jhi2OHx@Wu^={LlCy96w9sxlB)#)y3nJZJ%wD!=|t`w=ZF^buGbP#j5_<^XsRnu&O!6+b#h@FQ&_4 z7|M6Q=b~)DLGgYPF2;FpvhRT2#{<#T#S+8>#fz(=dUdCESq;Ojv{A(j8Za}Ri!5(G z?N?Emen_A#&30Xta6|jVa>)dxAcOTz?uO2{QP>U2LRl*Bg7l%_&w>YHhw{Qg#b4?& ztvmMmXea23MM`Hja)D9A17iq7yhY-G{@-At$Ol1=*BhVDRWcQlCdf2C^oo@eY*;}9 zy%}mn+R6l8JKjk}%VFu72{|c}p7|c5!puEH)F$%&ftu z&xed~-#c(@o+@6Z{WaP!CG>L<#UGhL!YjB<>+}Tg<^)_O*Iz4*s zrm-cq6(E}?UBSe&0!%qh0=PZjUtN?M!)X)?^jWbz_Q?`zwb9#;)&|zntP>mQaysvf zc3lwdyH7`9x)fB5$d>hvsS@OYJ%Mb{MLpxm5*62mKNWtYm-IVw(bukjl)|p&ds~lv z#Ad`6%f?0-B~Fg0?r=Xfoo0!NX2g!_=2`_;%IEt18DiTWBf0J-3EA#W&Y!k9-9xX$ zZTGwhF&~g;M6B#J>5Yv3LMREwseBhA6H)Hc0m@)I@Z@g;SO-l$HvZhoP)+Y@Wuv^| ze6Ufq5r@TcYl|T3rs#z?!`|Yb_X3Fpwj%1kcMM+*7MhDSeb|fHHvMCMu>ppu*a9lE zt-}Dt%e8_YnUMEf7h?V!*Cp86bWMX*PxWp2JBw;GnlHqWht2YFZ&R24G zWexyKh=^s2LQ(4yHvQojz~nA@$)HzAHxM6rsdr+vy?MFnPWP!A*8Z4=}`B1pE#XsN#+Gsl6 zD4xy@B5=5gyXu|>sx|dX_Li|Qx*aJWi}%C99@NDHW?HVL6&J1OM%Q8_Z~Lc9w|T=L zRC)2yL_1%QJzcF>-?~{clKS1~eq1(1{vmh~`j+R9iUcfIvwi6qF{Mm0AGO0yg@e9o zln)5$eV}Zb7J45Lk-}i`>h^!;Gz%xC#btm2(WV5|A$+w>y$^1h66qyd2@$(d(EP6& z1(`0A^asp4E`Ms8u2|ET#Xq9}6qfO_N|eve2d)M(q(XMVE+G2!aA%igi@R87`FaZ12>X3|}MqafN@r2QoAXU0gdGHtLyEwGdZ=u2tP(<>pPCn#fb3R(eWMq6s zP2s(O56{Jxa$`d-hvBc_k?l<4Ph60XBnad}nrpDS`HqS)h$KUzC7SG(TZiM*YRM09 zwHSKH@sLwb5It0QPP_$Sm6Ap(Cb|A|%FW-e-ljl`GG!LAmS*Z#)&J@kB^3xglT2A> zQm7|nZyujR<(+Kupn(uC3)$H47wpm2HPUXa&`>v8ijQR^73ypL=S zMJXa(7+j#;C?>#$x=Z-dp;gKfAVv=M(ox075;6k`XJ7Jh3li&534? z{W3U@NAg!^kQ4s+h152q!MbU}Fr2}fH|8j55DBE+b>eEX3-~ ztKvvFCfG=LcOr(z^NCs;9nx_><#@{>B>$h^jhoNlG?LkCrkJQW^FcsyiCwSznzyys zlyuM~v(Mnptm`p}ThPCbDxVnd#X6f_0i=Nm#tx@ZWhZ5}lMv8x3U>*$y8*EpKQ51#b&PoP z)gn=wG>P(V*7xop%NqG137>j5dI5v9DP#_L#Pm;c6xMH zP>~F3Z~|(=5Y7m~=+UKe4O;ngoW-W0a`uy9gRi~RNsq;|jY@$1O$0h%hjL1}q_l;w z^>QPl^?=h+GINl8{smn^ES(+Pr_)V5D2f{hM<*KF~&+YcG64A z%9mj*#91Csyr#?*jod=H!yPBR=8x_J=@JC5>;Uxz5$Aap7rhk_eD_SsS|2@nL^w>` z+atb^00)C>82E94`7f-f+oBqkxPE>1%UCUm&))!?vhN$M`62>G1uLSbGk z-fVj<9qq{$g+p;IoN!?B&m@XP|Ap`qn?qLYn}=>QI86P9Lc^@Acyv9cYd*Y_X8(h0 zq;Jd}LvrwJ1^AE3*+W4g1}-SVUh1X1_v)eGDR`J#F?U`v*K>gdfqBOT5OeV-fH6Cb z#ZP5);YBo#Nlm^C#%*87095VnBA$gfLNzK0WtLKVDTIL7Zha|h@kj*h#kZ1GOTBzl ziq)E=dYD61Adt`0fIeoWRo&^Jj1N**2it)tL>P%B-|0~uv*0>Ok4W{JApuTPcy zO+b&I2~U@e!W_3|(LJ2gn-d!&(%#0E8X@`j`aMPzIo^G}q->K8k66I<@H763??ENa zQr6>xKO#j;XLe{JgtKR!m^j$&4PH@dePsUjz*kdH;lBN~m2pS!a>>iiTrBa8T2-o7 zVgfC&Na`>m981V=aYv!xu>wmZV)KqR+i!XJ&$n(pAY@~jMA(iGasodgWPEhH;+mrr=ODb-KCdQr8a`@a58 zc-S-WV=V3JpcQ-g4+>3mo!KcZEQsSb{Wz2pbVaR}$AA%*V0PpcTyO~!X!lg5%8_70 zOh{H_$tlNZe_)2@?K*M0{OgBcZwh&l(3a&ma>>kiVb{D1xo6s_b}P{ou;YH0Ar-jU z@?QF>VCu0cB&G^&hl_f(iew-y(yq>bP@#^$#S4aiIya{^J^&%0?VYVUREMZFFyW~htl3FeO)+!HsStDxd+U63#g z1@8Hf(co^*krA0Jq{t#nlo_D*k#CLAze6m#Hvk~fg_hKRZ2_eWG`y0!0pQw!faeqg z2V}WVE^{&Cg5>?m#x*)A2n^0;&0E39iSBmV(@h(mEdo|uQ~mqzN(}?x!8hQtRn@;P z{v^OjP$Oyk{0KJ&kQBFqf<7}Mv1`^5tYTx2g2(tH*gd85EP7&)JR+hk4X&()^}TFK zD|>wnk5~S%tig|b!7UOC08g#8*AX}z5l1J_sQuX~WnETj?A|){zJg1Xor^h`hyG;* zQd=j?m=i_XNY^8D&(72~>JL&zG^FK9Dbeu^j%L3T#19r|hgWWM(-Up2wuj9{>RN_X zrfharO*r*BkxX;H{b1$Oc6=VmgoTkNuh3|=8)ZKc29{!%QVFw#!zckIz%#Fcva8^% zpIcw~z0#@q3ATYmndnZ9%RK*WHUaoDPbXVE)k$Ag|z@ z8kcPlG&qm*HqVni_vbcArxr^5PZD6VCj~zB^yRSnv&Q_vOl=S*cY`~s*Ei>sW-NwgAFp5M3(ct#SF)x$_HvWIWc zwgdQnn=ERhXTPW<$rbKjD|;hmC##CS#(=& z$?biM+zM1CI|60c4Fbm-T%SkD+;lLWsNpU@^L8+2lj=>)b54PmB?%+9LG6zIGXu^R zDVjU0b#2K$w&^S5k4{E@a}GbpiF&Bi30S6I{1fC3K2u&07*^1vYJ#JY^um8-DMJ|@ zv`t#RwTd_<*mAR3N?^f52pro{F^*qEpFzd6&{JPOHxPVftZ1&5pL@M^)nj-CBu8ul zl(UnVaF5r28D{~Ws{q2eS5zz^_+M({sS_E>I1~s&NHDFsc^f+UJW-)Sk0KG)zFc(E zpL3pgzxiD19m(~g4d|TLQ4aNlL;+FSt}l3tnVRS+VAL(etJ*7KEsmgeE?3qU>i9s- z#!6XZ5kS$@)sUc#s74aC+oJcz&4$&Q9NUR30l)j$P_|J$(EjHBcybO7$cwoxwiD|& z(|_N+`Tu?QZw3M~^wLzUr{j*i(5XONg=CXpU9irJl@)?`|kAd{H9tV7X5Visa)ejS8HZ}hy0L(L08QY<=6Lp(Q-IK zWz#wun>Nb8mT9Ww zvBiK{qU{WIK2hl9nU|`g-~!|Sup|GO5C0huE&e;C*V&{KhBN^quGi=Ws$PE(aG~N5 zVyfp>xkyjT{LUPpU%47MZ9#J$&x;#kIf9 zZ+D7E``ZJI_%T6V-Hw*>t)SUY9g+4Q@gdJPzGisC^GL&e06HN22yon5$=(3Nhdx6o33(<-qka2DvRN8HdFhYxQ6vI!ocM4nq3i% zd_Z}_LtXdy&-u~)+P>x3ZHtEWIf@-EbDk>?_qHcwY5G@yyZN^WjlBM<{qnh; zC&}5_eGsSf-%}|^{lBO3-@NnhcDrecE)DOJdqG+4wD##nf)Wi+Jw1>qwj$uX6X*UU zfjhw5^m&ZJV*dBf8jD{quB&?ptm^0CdccXev=nJVkJsztb$0q5Kuyzn->Q&(?ifFt zU!Kfl!`txCqP&DQvBfW6 zweOWGohU45Re6FNf7`Y%eVT9)Vr;#R&}vw3nD*ps)~}tW%oBD%H$b`s%GL&eO`b@} z(q$Vch;1!v-1{+J^ftxJPj}UY#6x5&Qo&fXiisq`o=}bfCRGRB+7IG!0W%6pL9W1W$@}>= zA2?daxe5y6&k=C>5n1C71XI=4Z=+zm;Ts+21K#wDu>v4>ll0+@Hva#-0m}14pvvDv zJoC`)9KXZ-Zfdwm_b)&|X*2=1f^GdOqQ^n)Owf*tREH+;o7)(b#7MHK10tbG-FD6jN@O4Z*JCC7T8Fty86+r0jXeyEWdIJAxzwB~X@)LIqaLo%*XKgIX-3 zca)W$6^SAuq~%}*e!PAN`Xui2yWV=sSi|!l@pSm#voQVtKa2JON-^#o;7dvZ9tl&` zAHb{*W|Mu>)E${36<(czb%Ab9Tb$__p&Q`~^uO!!1F!fd&>tRNegir9TZ@&C2-JYJ zKAO4-nGX~30|V>L@Bbfr?;X`-wuKLmC?FtMP#L7EAWBtw2Nk7*Ql*2^s}Oo9qJUxn zq}L$5_g(^sN-t7F4Im|yK!89Zgz}xt-0%MGICJmJtovK*Ti;su4_%ZbyzhDU+2`4N zKl|C)Fx&9fAfTS736RUJ*qDoEr^Dwi8raYN6x_xLd7A|0S%$d%%FF_|ZLt|mO7Dg0 zdaI!OPD$8HQLAjmta3cbv0=}$o0fcZ;)5N7Ufxv1Qqen)w(&wQoQF89>FZ{L>$;~7 zW8QTdHwqyva7{6;_e6=zUj%x;CyYYT{5ioL0sNWJ$${M^mkx+(kAGQOu`Wq z&0X+Q9*`UVz@d>(o9~U;Mc%t9wY5Ovh3*OhD--#&aVxRgfNiUz;LAX2dBxt6cdIHY zzQ$eMojGPC&l;dTt0fMs_$k&m?Xh0dfM7&P<38~&wZrj0<| zq8-eYWe2fO%^By@B=+P>G|y&%9R78<_l^r@T$FlGX^#%-&6DCI{ z#=eiIIwlq(nfC$yMrUvzwb4aT#m=S%+2i{)V>L7 z(oa2OR;=TFrQ+(-55i)y=I~`mBaqht8^8glVXghIw*TYTw-_JgJRTdPl9T?lnM7HR zpePGvtP*<3EzzD0shc?(tEUxBUTF-QYq}Lv8q1?}Ux{rhNGVrB@kVZrb?ptxv2@bQ zf$bVNBPD(F!oYJ%QfxOk9om_1jr{y1IUcot2ssE75I_i7dKmfiVr_eI)4hc5ZXh|j z)mC{@}Hn=f|9 zAEUE44n4+@@${;VDWA+R7)}Ih&c@vovNfF&4H&7FH`m1yy!VT$3&5_|O zIf0z^S!UvsI@s^wYeXsbdE#nG(8|^e?1y$}KYuy??E}VUi5dJt?YMDMO0tZBqS7=1 zN07+$)hA?E`f41##Z}K)xF%KL)a{`4y%~hJp^&P*ltQmUll!RZIr9P7G{(=r5_0y~ zuge40C;+1N%-{cgOB1;Y=BTl=;K(<2#Cc#``YV>SOLW;t-kYEo_7>My;5<+tx{WJe ztpcAJ*?;KH6ko&lp6{)HEYpnU+rH=A-?>Ys>*){l6r7I!yk!*Q zHx*$R<kC|+&q=B5W90~-0j>j7 z;;o9_{e0`rdehON99~mA*QmbA9@(I`J=Xe;0VV7(oEOclU(tSI>msk=lCiq?gCavQ z$c@ZqH($@;uny%HpR`;L2oUsN~vOyO=pDJg)1^*I1&^Z@o>A{IhWS z1MmsaWone~*t+8FP!N4X^$WJtAe@y7Z-=}VH@9ZCb% z00BRliO{{qhA7mEm(q0X$d$~4u@(QS#mbBH$ujUcb64bBoI0k?L8e}PReWF^+4}@f zzm_sKH@ewdg2EH%pVho0uGKFK_N-ZqySb@p18#UQ4N*KgP_b;lm+v&u3Wyi)w7+%k z4SsST;ecS|p387!BAVLZc%O9jlvRblW!RH8lR1{9)90*@%6IF<1V&PZ*rZUvF1#1F`>{uJ zDjdvHXGG;soJ1#I<%?+hWz%Y#cB{`VV0ow(U}bI@{n4k%SNZ!^-FggVCcJ>65@yNK z#=~e8JO2EoJlNx4OI;-)f;+i;_d1@0NnHb(f|KBwMi1v|wiRm7m6Sr}06=_i*^>}| zP4PGsG?*vugGsbmoJPA_3&&~7ZHGBS-n@zIO_oqj6m~dQDdD;Cp#f+>r8yk#qFAtf zA+#JSiIE)Y;$JMjMUvuHM!*2jk-|u#mUdxito4IwMOzXm7S$I^!z3QZ3|Z`U{uFfB zI(O-xi~WC={Xcy5kyqA~Z*=<1>RoV8cMNCx4puX;B7E`LA3BXU;3@`2>bk=7RRJ2M z3PCdlY+QGKy>qz}wnI6W>)qEP$fz&1gM;gMk+{HdXf&sG1duu;P2yUnras(R8Sa8h z`tG?dPgEB(N_h_%zIpX3Jnh~ar2rDr?%gcaM7g_AMJ}{ie04|rS+j9Yqu2=1@&d!36_)8rI^DAIT@%o# zdSHIC@Nm*7FL?5i8*9zq>#u!X0OSllLDx;y*l^|98_6Uvh>7xT-rd?4@#G zDP|1MEFCzQ=rL8cN_I+FthQ6L7sn2eDk=9e&Z1i8g1QBgUcZTMNx9`_^9Z3Pu0|QpF#^cPCn8pcyyT< zclvyveR&ME?J);ZF)<*F>jV=Ud071SO9NkW96|IxG{e>mGLpto^%9?^gY_q2#=zt%sQdkhy{FjcSP&NRkT zx><_ayGlNc4SE@*+_6vBA@rr_{kVm#;+HEIp4<9zll}6y0v=q913@gHA#p991!2zV zj!2HM>9&sUW+WiAE_@_siK?j)wHYl*Jq3UOCTXrMELJI+vjc>ui^$@K>A~pnO6Avd ze2#ZKYF!pwFv|hB(mA4UN}3>BMHI)@xveVs_%sLtL-yWYGIOehIvtG^8Ysu|8cjlx zFJgEMI>EV;;dijdZ$Eky!xQ>97eNdx!exBtqnp3=N3u65_d&zj7Ko(dGgT9XRr7R< z*FECn=|=}Cyz%Qim_|ZT{OTbALswX*TVef>Hj4lz#+q-qJi};6+(Q!Xw1(Op(YqL!p>z1$W&b&B136Zh};kUKK z6fvslpGoemcbE{4m+^0G0MOf#+FRyuTcdafw@$*LOZosUMsCjzJUc2-dyUhzz$gC&V%Q;qvu${y0x3ts@Z z1EW8>EBAM?U7tf}7eGS5eFB^=iU8f41L4y3%8iYu{iv^nuFC`GE?&F_x>{oa!fyxp zl^!?3y?iV&)nDc~B@R`jq~qlVM@g|*@q^Uo=*e158Ww5Bni8=9_9_qw}Q>i*kp9`NDZXoFWdkuG_Tj*de!TTSQ!z6=_m zuZICnF5>Z**Bu~6OUhfW>;uOcn>tKV1E8~}+n=Jy2J(FLZTOs!4d>|TZ-{|5`P|xC zkF9ydT)pxv>&!;SsXE237@oe-mt=qaGKu$eq8@liQ~$dKr+@2`STjx-`|Vl~tzG4* zQ%fMl5H?AfzpVp5R5w>;tOV0p>{qi_FU*S)&HU}+Qz z^(*HA2H^MFGKcsy>Ywml2OyvW)KARvR^QPJ+y~HwA?F6-hd2L;D?NFG=s3_6~a312F2E{A>aCN;R0sZOni8seVb&8i5pHqSH2|iah2n|3g`@LSdrQ&d&_QH$P z(%~STLU3ri_gg#q?W6kb-XWeXcOC-7a=lS#-?#ww7kd6^dgA9yEyNGY4!#XAJ;z!?r{eUY^6x##Ke*h$ z0iT`%30>09L zAYu;_&heu|`>PA|&)1zK0VwVGW(Sb)h?H_~%|8{x*@E0cnKxD(} zNx^;WFK)Vj-p-Hf!MaU)5*>6XF;DSV9o@fu-E&z0{m(RbZ~Wh`$iMr=Nnc%N_vYr9fd4ey1{qNk#A1*~; zBQS)o{x2o}W@7L6A0~U|B(TosvCZfH?F0U+zqn%p3}LD1F6`ee|8qAL0J&!hiV&#I^bN+78G5Yz{QNz>OJ^EL`b#&IF)4UUOe(wtND{KVsMQ9Ic=H8Jdq;!v)DaQbaa%9?;2SDFOmq0L(C2oBjM zX5i~aotrFCxz^CPAf@&TK?agt>90SL?E-tsk)3^)u8Ye>ZApMyhSf2;=5z!Gw0POz+XgTLs&I_iHA+> zHmq$w5}DwgM-cjO?1ErFlD}M3qvO;h7q~yU0~qd=0;cvFe+wVwd@nHJD~rW zBh95UP{?9am*A^z%J(L<+~&KR{M`Hpiq$V&-l*?`2OJik`aMww63E^7ekCYs6q);U z(YHuQ8*3FQILP|ckrFMzdF}!&hgeHX#+|eH$nTRrk6@I)dm8(ofQRjB`3u?mfbEl` zGZ7SMvE!b{&DKryssnhFo9SA&ss_7qr4a7ofeuc1@nA>d8n~UoT=GTH)9b!0IdDs1 zTJ=a_ry41~dyRj~YuSR31nyVAcS=ZN&MRz?UyYdr+M3LlOE>ECHMf9ny~3k$Ie1;v zkp!CUui!gZBU6F;TKm#k^#Tc$nTF=9LTvLbj_vxj2TAjtD6R1~9|nx;&MXK1kp7ZA zNd%g+v26o?Zeymj3g<_$WQDG&HM^U*2s6t7rHVQ2>jA5<1XdcDZ|q&lK#skP?$PG$ zA4-j&BJhC-=C_MqkzV;Du=tywrCv2X4kcW{!;C8;{S~(nLz+-#)|myGq;cnp7f#&e zG%|Cxv>xwl=n*1S)xX|GZ|`cA@;F~6_NUdI-Z|!M!BFHN&sXejygVXNd)0%-2kYH8 zH5WCKk9RHao3jGk{ZFHc@Q7w0m2BP}cPk`;SeVqv)yR}XXh4YN zw~)|K_e5j=gB=&pzE^{a_XT$k6)HhdY1oi`K2hR>vQ#s<`PsO7RiUevVPz{c|2U~Q zz9&~zLn2J#rN^Y}+w>_nZ0~9)lQn$&qeyJE$$+zlxCd(ET{P0UqTUgCxIP*%l0w75 zq**`aH7VxmBB6e%^<9pkD%5heX_eBGVMsD1ZM0#tU3+YC#d;^7zkqg&k1W1Y4TfES5FOx;toP6wc z+-ST9>X^0p&Z2lCchJRg+_p`2b!TeONWXQb>c<<$2q=y7@u1cj$2)o>wdYnC>oU!l zInnIl7AVAn!-JiJUeNRLe&?9e%)J-h>`GDLM3S~r40pWs2q1!tfig89xHM1lY2`df zBY1CtmbDH*Av6E>Ib;hoOewjO;U-O^jiRablb0oBXS%3gX1XV7b*DdBakWINFzROG zibR%cq3TW<9>yk3Dtb*c-|R;|N{zhOE>b6~EUu-P!@6A*;;!%RQK0qW(GxGXFvZbo zrs~x?^-q#gf(ECPs|H}&l8CDf4;8Z@hk}FZ= zV8xJ$pG2riMxtccbaJ^N$ z!07-f(q1GFu=TNK<_ox-l32KmP`5iTm35HRRGubqY<@kG3_tGY4sfutU!DE)?7>ra z(%$o)Hsck!`_o4UDd%|&9}$JWZ|GO#6zEqfjT9Mifnq+9;#~{$cWI^M+{tk`Thl(e z&jXCn)V5=ABg6Z>swPbBbVFM(D|17p0UW~jKB9@wwh2Ajs7!g0lQpv|Dgv}mEuW`= zECuu_p6;>i^X32Ei*&=>%2BzGOzylKTvhdEUaF0#qH<+^SepQk z&LjZRRl+9GQ@Fw&{45DpuW&N{0Y#V2XnFK@8I;B2`-l1Ko&nhO^O*glpv;=EX*zc2 z`lOV#2n)6X>yW646!FV0`<}1o7TOa{MzoZT3x^4Ck4gHwTn(Uljew3}HUW{A3s5Q} zZXQf&7A1YpKatPmrg9)J!Xy%l!DrNlx6l=`=1)WFmQg04du(bXTP-<4=4f9N@Tl3u zT#||$7n8dj(2Vn4vFa|1-OKQ(yg9(bOxWoIV}9zdLyG?a;Hs_-?oTomB*b z6ZIwv(6hAr!YdxyE{?;Y)%Kuv&Ra7>a>Jm?zPT1Hu*#TwtISTZul`*~$XQZZurx4F3mz*>m08O%}yT}|_FJW>F$7F&L96mktuR@7a}+fG*J(^iMAQtqn7 z4I0L+{R?t2+31GC5}f__*Xih`=6S}dEMA5_u1XU7w2<1i9&O3P;{2TItfgAl z@)5x!W)wMB@%CF78jsHpkv zgb0{4%(F6&h&Q%MBi-Qn5Q>W7!ae_&uYeQvCh)O);nOML+y5 zR5iS^XMGFm1Q><q}~%F9HNt zM0NqCuICUmpo-ky2akVut=#{}&kOW{%mccxn;*h%Lrd;%z|jH5|6U5dg^F)`qR&{R z%McKd1TgM^IagIrAuen;qC(X8<9@ZY)4Sp^k@N5>3cD`vVT6-C{sH{`h&=6lC7^J* z@S3Zrq`J(fX{!V~wz@B!+F~4|De${|(`|N;Xv;Ase{=g!0|>S+%`j@#rA7#8G! zQIvH!Usq(j$oi$>3_eOHBxZTM! zHqkR?8RSa+rXI1wBIADNextPBqW@mza`EMeH+;O83&(}tCkW*y+KnjU#o_o#!KaPr z38*a0X6i0@(pO^*UEWhHfBEvIt5aL1OXf&Uu_NM(Md#<+F1J$yzrt_~w-%Y=g&{CO zukB;GZU%0<_WdIKt8+mEWy@E$R0i);+?Akhg}uH|*h1&E{3REUpNksU(>yRfc$-f( zTuOmvZzx0WCujh2w!eRJZVSxql-4e94}R`3I}UZhrG;WS*|1hFi9f;e6=SS!#Pb)~ z5;7zqT zcx8Tfl9(pU|Iq6ft+K#m<@}yxkK>LPXS%#r&|2<2JJr=>Qn0vm}`o8KinjESL=Qjw5B8hUG)@T>EV(X7~%~}CY z_Mj2M5snvl3szI^QT)GJ>@?et1v-}I!0r@>b@NMda zSAyu+{vHrp-&>cGilWUCKhVV|gYXhh`&kJgT6b>xQC3rmb7hExyDcI+*%e#m5h}#E z@g449?ldG_TfXkU5Cn5zQ(rgCni^4^$W$#~cR?@R^2+s)sLe*MS)RmxS%&V9I@@4f zJ;o}W6VG!Q%Z+>3a|$`;UORi%OC_?Rz<(fdaAEC6`Fuj>1@rF=d=j@QI5Ot!`}=Qg zp&`BtVKO30`4_`FG}R^1WQXfcL;73dkRBfdMYQN@zmf@FYFwXkzK?Hk`qIiJ)p%T~ zijP&WkEC0ap;ki9u1#n9=1#e;a zs#}bQ`t2`EY}p!!Rqh>i8=CD>DTYSGDxq7a+4i^cGn>Ot9~51dzNSx8ox`))_L$8C zl^U_SI5@na<|knCIJk)oyhG`u;v07ED<)~TLbtu{tBD=^xX<@4DoJES$+Zt!wph2n z=-QJYly>Yowf}O~aL3_3R-M*b`9=u>@PKL0aOoB$QFM>3I9_rbq_h*i|0$T-0tkLs z8XkP3c3buW(?e8*^rUDiZ0<>~Syh7|1Kw!W$4q;%!ok%ERkt%@aPN~Xat+X9`JKPX z5w#O2v{tRwK=6igh*)}*KaN}Kzgy>l;pNe!io5kB7_|Dx zgPw2Jy;?_V_>=J`Myojw)4K`%sQw2@jxSHM#9UKNXa}_L$e@c3(-qyBDGryyhh$!W zF{mb>L4&Opf)c*Fj3u@qE<&HQhhM%XYmF*F;v~7I3#rk|DP9#3Fb_Z{PRW%kxIjOw z$wf+pQWI>-p}O}u_V*u{@hN-KpZ@`Y1bnyv!tjdX;GjPvlQ!>ip1Hn}mU>i^@%*Qs z1QHbGZfj8-C?A4`wKAVwmmAu7TKs7sfa}ao@w3e%v2kOqC#6K&T*v^}std$1Jm7>X z?-_(w2S@?e-}5m3rx+3I1uEUKRri*Pq{OQ6ehEhqAki9wGj#pc6MKT6>;r! z#UL~P%G#ku?pE~TqoyU#tK{l2VOD;;J1dA7I%tJj#o;o>pL6cMS~OQ@Z1lr9g80Wb zfj4j4BJ4N@C_Sl}?b*p`_U&4{K`X@CvRBd$pTXe^*L7)y*B9Uq7JC_;IBCALws!DT zvPAFg*ec`clHJ4-dne$1H}$H%$?qnSH%8siOubK?H}0A|N7FSiE<>(evWC_D zfG8N3;}Aq6;6Dw)n(Y zpHV&n$hCmN{s2sY-WoA*B5H1|;|(-kJk{?}$*;B5*2QE+?+7Yn%5a(Z?nFbog6p;+ zs%=dxdHHhrRixwF*b>nAl$DpHQ$48Ua!3L_+?%M~C&k$l2$&`l09mTdQ!;UlQ!+p% zeG9;+*ZMXKvi^xh+Aqv6JsWb~5vT*)>N3-h&&_*;471Hdsi%Cw=xU8dpIvi@2D*yJ zNo2eD^(ihQ&EMKh3RC5|`PgeQ-by8?(W&WRWWT5yKPZmCtxdeE#o_TR)Se5`x+*_y z^~M>G>CT)en4F91O?G!zzSKbJT7;-fggfEHUpa9@jrf*hVj!4D>!mc` ze3@mi`=#!1SL>0xTcXh`i@pNGC)gUWjYlJl@sf@SL*Fqw{4?*0#M8z?StKrp30ZT! zJbkY#u$nyKE37(2SZQr{+8+}k&$4RDiRCdC)oy{zE5yk)iQA3Hn)K8kq5@^+<_E`U zGvrFIEPAhbt7Z;p>$Wc;pmo7L`b}(598)-{z_qwru(ffM!$(aIE!F8S)1Bvxvz{C( z>T{Lc@`FP|)Yf9o7UC<>0KR>jESPfI<#`z?$y<_?&K zW)_s3vinnFVTs4L&lDZeT(P-=R*AMjl^`6@4=U8iR7#6p0d{DS*ErT#X9D-G%q@zY z&K=JUxK%%PCRrzNu#b&eC-Z#H-rj04Q7YgUnmn;-gq9UR7SQw+5wFfQ0pMt#+5%a- z1|_=E4xuJZKnp1D>8;RwW3<*5qpk->ggj9+1s=Wh9c&Zf3&-84J&<16l0cpM*m0=- zntMkCdv4y%oJi|OH9Cd*SsIgkP5W17XJ`!eo}r?QjwG4<3EC%4u@#yM(T79*(m zxXpDjRn-Ukg-58AqwuOVZ(^ebTX|NP+EU`Y*Sg(T4cwZ{b*Fd!s+`&lEfroxt3T=0 zu2{v3mKE=#Rw&E%P}7-i*+>mSRigO}7hgM_2~RBZL9lX4?6@+#-gb`(QPgEr;$du5 zP;QY`-D?Gs*|61H0a9F%z66lV1Krw%?royeGB^$$|M_#AQ+i(t#6<$6)Ptn_vG?$f zyroTke7i6vuy<@2Fy3lDw=_f5mbk?74|TWjKidU=C8#}DqdCBkokiglQ&0Opz2hmy zcDT`WE{3n^v&3aS40fCn>h6a*1kcnV|F$^wP^Hb_eQ}R<^cpX*A4X)_M?kN=C%BH- z6y6apkZs+r`bihGH7E^D!>kt?`5M39G4`A##d_sjZUT6Np>B03Hb8HG85^ zsK)dAh8eBOw#Xp}_vE<7qi<;5kKr|1DGs5S!;F=4Jp74Y3S#qJJ%5l%%;mF_5K(iW zS&IQZR-V1fT=J%)*8`f>4Ab!&`jBLOTB&I z{fO~TEj5gUo}yv&rrf}XyTnwN*ah5V55wrd8vuwaA?_6Y@k6+BT5{Xs<9k&Z$M_&( z4`e28T)>m#nXj638*mQi9m(%CBFAzS&YQsq^+$w9a_(N{u0U8Pe8)E4c{`1PW2+nnx=)mgk)IY; zlol)o0*_@+*vgv_T6>Jc-z8Rc*c}*0NjT;gd6-Y&{4=y)!pe@Cn>5lq&xVYbn-Jl{ zwJ<+zO7RP&mxR}@$Hv07TH*Z@Ol_`1e10RdJHywI*r*WT#M#Anu`D7-<{Bx=L%9A^!%M@Or^+n}ddU#}Q+ z0cIsk0Ktz;&}(KH=rBGH*y7cbn!1;@O#^~yV!eB&2SxPWf zET74`h-~p2->nV~i?K3`v}1c9$C2O+4oJ}RZha@TdfKmk(*Njy*ooNRoZ<+&#f06B z>fMEAWgz{#{7y(o+WQT_(9L_31~C&zgfB>`Sb%>aoSm6gyiCf+<3L_U_)uT_rb(lZ zw;r+M4vV~U<%(QeBbXBaa!nD*7~uz|>iPGG$b{eL`%`Ra<@hYX*pot*3&()vYQ&cQ z&T@^3K9=D4i2hG4H=88>tY`n%PoTZo=p&j2?Zt(*3GF1yK_hv&ucESwY38YH|5+uK zmRgnZ+KOuEcS_E(9k`L=o{J-mmHjLbMiK>yKd`swtvk=-YiEX)P;tj5Is|N-eJy7|U<}NDp zY40xMwX-}>1z0^x^jn?LK}94G2qLzg&-W%9fT3_lqMB@hvw}-m(aVC57N+cT@JT59>d3Hyb5FlgF{*?Z~o zARBPFZ6E=C2^A*2r||mxjVmBq5Cdd06Fa>@vm0HZa1Lgo-U3x$82|?@y9v!XMlW^P zLqnW7t;DBMpgE5}n*KohM%9`CL_G-JJ!H=PV?pij)M8{mGZiyKlVKgpI$rF>PAxp(JBf^BYP5-Qg%~-|z3ZJs~(CLan8sn?erA^Ke zp@{>v9`+b#{npGDn5_loA~4-aN@#sDC3IaRdCtmTfyd%va{UEdQ4aSmxq5jyd&1FE ztb~ebv22P+@@r3u=v(kriOc}_u{HL#dzqOZXh#JS%aNnWi3{JiOV9bwm(15<6e*hO z5&{=`uR4?CJNh5;b>#|mv2mLmyCG`XxUYqEb*mVBqadoJ8YJaV>q~IAnXD-_xZE46 z+34d%yU6wMo2%PHxYb=F;tZ6xOcFZ8R%oD3mu~1btj+I#=rdvT?c28ufPTxKw9yEJ z7NJ5oU^lhbjlGxIv13*Y)&D@U%K$gz-mxm4s72Q{!O3vOAPSO8<#VbB!MHv+`8kzi zxZeZqpl)avBW9`|p+zy2LSA^No25bzk;-g_1%oIsJCSVF8;7>xTwUHJ!UhyErRVv= zL%77_U?q#$Dfm5)CZ-8&wLRLv(@8FyLeT!1GPVn)bt z=XjbSX}9GIXL3?IB)zsg*UhJ9J3l!u;uUvDC@yyuB!;~I(G$!5jjn`eSBa^J3$HnK z3amLNtsgX#{nTI$1_Z~FQeABC;%l-$yaR`O0P9s#_?+lfdiz#=mK^HRGiTvJq({>B zBoi0U+zM;JI3d0*`D&)0w>rR$DbAK~8TD(Sp1Mj=+}G4A{kn5YJmUAEGvVCd%|p0d z^47KM7L}GImhRoqN8j;CB43f@d6~50epNS<_hIfYM6Z zGD0jDCK~wPz4Xx3^&C}Hzq&Qs63Gb_uxI3jmJ8Dq+IB3bLe{-%)^?@3}`m0+0%sU2= z-oUSdQM#43+8#6vY4t==vL2CX!4!yoyjp&BPrQJ9QT@93ppFbdkXxt9UXM0r@U@Np zLmU`Fs{jdtUlOv7dsc50s1r6(3(2``lOI(42iU+^;N>SsU?HEXmQBdU>0CkgY2^<-JId*Ei8Ai6XO~2;8z4VY+ zd8c?`>-HH4g0_ra;L#rv?)blgc0pWbo2u;(F8vx&O3^y*@-hyDhFu9lcIL#4_^=~7 zW-0*g`yRSB|D<>uA3DkMb)@ZFDJB>7BlniHh5^SNU;0hc1=0 zOOQ6Vy=B}i0b+q(@>m!-Lb4W9uRJdDN@=S<{09`yY>8f&U+Z|GCX%d!m~wuR)_DF1I`Mu`2Hs5mr!j%XY`E=CptN(r?5m9 znOh>#u4&d+9H#oqI+3L_o0PwhL;h)H|o z&F5*GjHog%7KhZvBs9Ygk_MjYf>ZO{J0S8RN&77yNGRm`SvieAh$WlNQcJewJ$*?~ z;hIVuD-eo_0#Lo1W({qWjf?q^#0gj4#gc zv;Bx>Z*zk}-YXLtWuijRk9?GWW#R;2HCZ#8yFkX!iR34+-D3d0VkZGv>Db)8^b0S+ z*bjz&N>TT&^=70oHG22Cc0zjDZ61+Vzh-iiZN%`Q3(Tvsjb3YUfJUhXhmCvf6LYzX zy^pS6C5PYem^M+a+ji&NXR~SY?NYND&q>d16!Bdb^?>svxW+UI+rRfaZW1-&SPU!i zF)=Y}`^FceY{C(!yP4NzF)$y{(Yn)%T^)BDsO#y{NRxZG>xuL9;Ko3v4Vr2hMLiX= z)Gl%B#x!+TY$6d+Z-kXxS5K_GJ3KnnQw8B$!^G`l)|lFG#w~ zw~crA@giGCtVoM5GsZh2lwrxRc=hT8!LpdDzo*u$n8Q#M>m|2fTV)fo4(!9WHSF!% z?g6hmW*Z>Q;Fd)R6p#ijaJ!rAkoiZH7jUkPagBxZLw#IqP7~4*`M4LoeV>;pq25RH zC_s0~IE=ssPq0rbUyYnnA4kGc3~#E2s`;t5a7|xlSoHgO!(dG%01oW3otQQUf<3}1 zcMr+(jNx@4K$mZbetZCOj!8uUO|M>^+y%;lY}c-}`n1~oq?Z5P$TL+lb!e>YN=f$I zDypD&2gL5mMTWJL^8G@~dw@~_?ErE*P@~hX?*Qz(SlHsRyAZ1kGF!A$2tObwBXUr2 zDcKRAQjrUg9f1Ro2#W#OL$TNxer{(lGAHMH?Q&LP_YN3st-7_?!zFY0J>6PCRR#ls zKUhJSaShO6QO&$VjkkIAfN0V{ektC~>v)vq&FNSGjyr&QqP?jG=PAkUH10Pzfy-lwoCRYk#)6>|FrU-Y?o-+Gm_^OZfvA!n=yON z9K*&QOnaf2a&>qvho1+^62yUO);nq9ZY%BIHygua`uIS+c-QZe_}4s%Aj%7!B~{eC zHl}^l24f$hnWVnfLZ%#ZsV!|J)+{HWsG(A{o74yo4+>d2`Go0nz29^Ke2!*CNdIEb zk^Uim=*2+8)KP_enpapWW2CoG%1wk-PYUaoBubyU9lvGj-MubMh8yA9mS^PNVglg0 zu=K|EU(He|lyHn`ES63|HyW3$rKjOKUYxFUzjIB@q93KnuD(8KMr(8wGRHBG6IW%E zmNs7W7gT(Gp_cX7z*}$cq);RBc9zw{2t7@C1+(ltjWEAY3;7mY7Mt=;ihdm2X;`?# zlcG~88$}HQw$E~#t&9~SoPDy__j+9&VV{JzyjO*w>*WY#TUb$R;3{fI%dvrQ0#cWScm>8aWWjX8z z)ot$q?N^%?6W_h<_bACrB(a(Fa%smOrt}hcebt zdNA=xBTO2P);Vl&u2*f4-_GLens(a0d{Io7AMYwMf@YYcpfYewLX$Z0`ETr9nN}&Y z8jAyOEcWU@`ErS_nU4F^PM+8{_R571)CGUEjpAKe16=^t}ov3ytV6 z-U&~1s^RUI%!))jq+7L*Uv@AyZ7Er=ai<}HO2^qxf~o@_!>C=QMC(EP191bzK7PyL zTC&YhZ%*`XBIfhs?4BCrIpeyd^FMEf*nBfm-!A_ZI$&yVN34!OrwF%>r$lS!$0)5s zk2ucum?+_VZb>C{=E@)xJ|Bsui=W%s`XSEe2dYRDAnnlJ8 z$|F~N3AoCx|2uHkin1XmQgSdjs>x>kpu%-+Nvn|H_F~eQVCl6`8K~qX4r1Wat@(5&3)FOdX8#;svEC{rim|cz z6AByK`ONPLn-lV+*U_h0wdjmAiYMX+BL#=5kJZ13UyidoM{=%lN1d>tNiXD`CwJfY z%9HzzSMF85hLx{P9~D<%M&*T_#(4YOtgnR$7ob8dJ~_#UepX25ND7x98{l7jhg{#A z{`sKFN7O2y+tV&5>P^Ftp}*VGiCyc%$Gg^@3L)G^o!w@QkK}y=8haY$rBq+Jh>}+& z7pvCw5or>AX_h}Eg*#5*k|@2!x^!dqUu1c$CrQ7RaWtO_*f!vvDFbAq5-rXo&-Jkp2-a-@{WfJlOP>dnTo*0` zDW#hYOY)%K2s`!HY8&Ukx^0^_AcnQ+Q%5N_6xLyL>^%#&8ORVHGZWuLGUZtbH zVJkHG54F??H~-lnx^Ano;4MIPU?WZL%&N0-gLA+_n*hsDHjG%iFZ? z@{8mfr#Dawiv}91iBFCDU{b{LUYSw1bRon>Tdr~>{6jFF$Y%18EcDC45J0jc0!U@) z-VbA5Lw5CgBx;yqz#$>ze_taVsIF-mVd_?s!oB{l)P^&zZ9I0{=9|Y%H%)8x5sN$U zBzqRX`k{XE_^}+5nd&rqEQM)AWYE7yP)rx~0CO7e|ZeiQ*eg+c; zvH|zEC%56zqoQJ+F|VSVyR&VTzZ5kNUzhRwX_<7Sh!iiUo1h_f7t+E|JTa^JS~2{w zNi?$uD_RJGXHjs&Hle~Z<>4Exe+KUMzb5C7l;Sl4}dC&NMf!%S! z#APLH_}l&&C1jN^n@1s8Z*hz#R=^2=t5}AQLs#^+v=tQ}Bq&hAcdp7YTyxcrAF(Cq z`|t|0HK$$Pya?*BG3iE0wr}GB&cF*4B~(jR0qHS3Px~kZULR6gRyQZ=XZh5;`RrNo z<7qb&1QT8<`r0U!4hi-LUSJT)y3BShfy}6qJvgr9so6Uohnv@Uc&!Ju>(OaSm!8iVwaWL8KA5UD8KBmpYi#oc-X0 z#bO=FWiG7bg2@9@jFO%=(dV=8Gy;!Z%e3W=L9S(6hzoO$N|dL zmX?;S4M6?$zKMbUAcIrG!x&A8)6zg$aPj-*oF#w{i?c6T-0eo7Uoj`a%Zex7JDHnj z*c-zPiL#&lN4A`tL%lCKM@EjK%OlqLFZ%XqGkVzXYMclC?eOR43GYBwytxa1Qu8&}RG{cMEK z=l!)9o|TtOxJE~h(FvBd5?%J`7Makv+Ufmnpux=H?(5#%{6VkEZ1R*$Ib!nZQPs+7 zQ=KVq(~bmW_w-AZ%`QHr(@#7$4c!g+F&{B?c9`jD!;i4%j1unM2a{sn+c^_@>Ikjt zna^d`^gZxXZzT4Yjp1oU9UY2TKz$y?q;wh0jsq|*$X?Mv3cW1r!Y?Rj-h@D!5r+np z+aY+XHBBWK0RvJ5=+LHeDKIiIy>rTxcG4=zmL2a<4+JO;L z)4Dseo(ry};X%QnX1A_*YL__K0Sn0Pi2Z}h>bwWMsZ%~OdrRphZ)a!Cu%Btwlf$e&2o z=suVhA1s>7{Yet!*ohU&&!l{QGk`+T1EQ1V`rNQ_8~J~!hMm;Q_jeEvLzSi;-%W!u*xQ0HEiLwtc=rS0dpMFw`o%S}1Mk1)|( z*u^0ZtL{Cw^s0?3H$Etipcgr?i`qk0RPMoOnm3i-Y;{vR1W=O))C~4!$vguTYTAqy zb!WJGa~Y~+oDjIpWr4-T_leyOPtxB!i6odgP8LV}Ka_oSSk>FswIE1Imvl>m5>isq z-5?+vK{}OYY^`Fz{6%`T`Pk!}?9Vn< z^h)WDO>=_aOR3|%P}U{SQfBy^&5OSX#2{=Mik{`do)%m;b!h2nhFJ$D=59}Pv2u{VI?*xfs>-6a&47aO>aP7H> zw8q3rXl>^)Hx$;hUMu^?JY(qaV(vfMlNn zP-O@An3&RH#BeWTm+9({WG9Um+*R-XiokK8DOV&Uh5i57w$vhcd|ix1J&Ar%S%j!> z7o}-mmRP3DY%GlDG;^^0O4Q<5?fb_m6>ghw-jG59Vf`RI%a-KGpCKC>-wUTO`J!LC zu9`LKJQwaLtGyvr3@J1hQtHn7DWvq_^S4vM+$2`p8wX(_p+(p$TC+5f9I}%uMW+_8 zSG{B>I;V9(VInTb72hB(&GeeVe>q%VRKaJcAJ6XicK{!tZ1@pdY zaYhyvGu!59&FB45+R7`Z41T$#j}sj_I|_SSk#siCg?>sKu6+bY>fj2iY4QR$r`J{c zf-3Ow`Sa3{Vp0nt79 z$#Tj%*TA=BG0i(;uWH|UNu=l5Gtf0UU*kbtiNehhZk}nsUBYzscI)wCW%1&_4tEoh zEI*ww7-};QN9JPBBs)2}{;A7@`HUhA`L8*Va+n(&5T}7+!>qr(^nawX;8l?gw6pgu z=1t!qW_DlW+46VkSJhY7(0T~u%s4t!6uC3B;9}NSi%1MF?Yxv1y*QM%Jkle@Dyus* zu;x5c!TV>iBmP+4o#WB?^Kf0q8%M3s^X^acbiCu9<3Ujp<28R|WZ?JDzkiBk!j^(t z9Z)moW!3r+R0J_?Qi^zd`PM>gVMmzPIZk z*9?TaQ?nvVWHSckwHch*rx9O{tVXWfvtEDy@StC>X!iGliU0GiMV642E2enLlactzN4<9sm95aLS)<@!)DvU3rkr zRo?t!VSU;5BA$p2(g@8^+py5SFc^3S@kpn`5PW#p z|Et*HpJGfT5IGhf+8Lm!quP*40>(%ptyd^wF}1Sm}jh^Upfe#6I#BG7QhCE zHU^f52%%EVHDl=!tCw+IkcJ@)_ZtO!amuzYAnpoUSrW7GKP()>I5}|tk|%u2@Ij8Z z{Qt6I_;UWJ<;HdsmVtj#iI#cTUGzAA&i%mrweErKF)9BMyP7#4igMXmfC&g6=xmf#6ak;B+}AZk3rL$@y*3=&tKlin7Q~->%i_TB zY%9vyYu2uIQEBnqb>rO0C@sUuWAV9Hx^Nvcn3(NC3Wn;%yDHo z@pZKk--Ae&nCwdJ4O6mxwY9V<=v9epb@=JdVOR6v;b32Ky`Jk;<5lHMvyh@+j;wpa z((C>rl8;1se6J;Kj7(b|7|s9f#wkE#nBLi%>ihQZG!CFbyo;WeMWY=S@P_$!Jh_^Z zJKwitH@tfEl4ZKM;eNuFs2+!`-6Sg-Vw|otdLovwy!t3j- zih+po=aY#Pu<_6PWLLYxN-Oo2UW*%tJJwIyN?zL{BDya-IsDa2S0n=!Jevac^sc{t zfWJRc9V(Hp5N7q@8P>bB(3fJ-JQ&>Yp09rR9Vtz`G9{k)T3C{c*QBdKAcvm?G*qmP!^Rl}Cpvo(oyV^*9-_j_r=Wuvt_4U=; zPB#gM_Vl*%^JYFi-5QGB?CXaX1uf(Pvt)xS8*aB>QySOxDwPrVRbIPICORabn!J7e zWxhHeN^eBZmcaeD?_LBAfnnMbj*04TOq9PqZ(lF`UjcGuuZO5R5H**TCOt(HB8%{( zx9p#nL-bei+9OV-22?Eb_t%-Eo4@BS$-fHtb?9W(b@8pb=JhqIwn^-}ttxNDv|{6} zK<>GB2dCjm6M7XO!z;3R#pUnBe{KiDaM z0WyRvYeKIcHBQb5oLk!9=t-1il0HvJsgh-5>Xxry@di57?Ai12oT6Q)+g%~%mg$v9 zp;9aN_xBy8)22t}Y6@x{%5oKc4InPoF!4*Z-(WeG&En3pTLLR`~UrM zh3PkDq$@#J)?n)FV9m!Ja_ta|PxjkPv)*Fz({ku&1&`odus>rb$Oo>(Fg{QG^g=qb z$>KBVzxrfnRPgfChu>zY4m_~2FhSkp+;66oha-rBq1qj zp$z~mIWYbnA6;Bj@fY;;7ZhucO?#9bq~43EHOj$`szVErS&v_c)DTEUZy{^i=QYxZ zSgw$d2vL5l-n&`Nh8l zIuia0?a#mPFV01`Byz|1@A*_4V8|Dq?sZ|FU^$a1dY>NvZJK3L5#&JeC(OFLOD1Tb zL?ginAIo79Ux%9;seoOlj$O?ao|DMWUe91Ol^4E>)e%<%NvOl5bGsF+dGl$iY)r$O z?9j6D_-6b1^gY>tq~vA*dQedW$;~%?ycjQLLFcsTPpi{`lmpl&w0QBiXNT82_0=-4 zi%o3oHw~>bPgfUpvSuoEUeQZZs$oqNCDCTk9k(Y|Bru|Xk^J+Q>?{8Q9E2o<=PCd4 zto-@me0c~bxQr;?0+9JeKqLDEMzAIln}BKJ2sHC5B0jmK=I375$68>=4FTYl1$u$4 zOTE(V>5?U?i<>B_ye9Dva(da^;NvtotL=*-Ck{_Mo)myPBeSoFT&hW#L43?Oa&nL= z2gx}TX**BI<#^iji{#eG$?it;O1WM_{v59Nu@~IErnC0jOdV1KrDaRwouSk6wD%QA zTV7N;R|KkMZukaD6T(deAaE?q7dhb5X?&2~-!)DS4fzWWjToW-`Oy2^g0K+0HD^AF z{g3WNdt`XG_C6rmECj|AAGnpIr87Z*i~`J4uDc$rd;}IL83v0$n4JLj`otUfxAd*C z<()0FJ?(>Fc(6!x$Yu=(`H^pG3^KMzTQ)9C6vr09PD-cW?Fq+j9+_QEtDIC|9?~8(+XVjUgMnQno1y3DUlP2>rZPJ`iFONCRaQDY3pjzsW;(CpCzmfoeC+ z@)6QR3c&|eAQcW_pNt}fI{b?(Zw49eG1NHc`_H0(@e6-`W+Epdl!EPjfQUsWV++Qo zj>p>?rK4blwrM;SR1e-@NU;PyLKeGrT$68CMu+CjOt8*H4ZYiP3R6UB!Hv{HSUzso zOB*eB|=En$m~7n&t~ z&(T8UIJK{y!Kd29uuCbAx*p82LE_;<`~`+G-i9);4HmY3Y0j#tSA;82AftEu0g2g@H9gjzAVibX^OylE`H2DhP93`Y_x~tO^5Mam`X_+RFk@g@HysBh z7jO*-yt@yQ2JQU?m*djUjN&K*dN=#{mcq$&R{zTg(;MFDk+AJi6ImY{*#L<;Kxg$U8(+8ZH)0PuNI1_jS-J zU&u80RxrHZMv>Vet{aMPBwWn_1e;YM)d%7s?AP<{8R zoZTtCmiA0g3~v=Z5H8O;Q&`c9VMrw|<+BC5(_*?k7B z9L*0mU0&x;J6Tf-sz_|a0{iptv)4U&UJr!oG6eb!_H+vA&yxVR)FcrTN%SZ2Gpk3| zGgWjN>EKow1FV8E@$l=-oCvdvqb&~GKn$`?u!vM%V~}N}+EfYnbEG{NeEsE~8(0%x zTe3Y5tf^iApM`YiSGIBj49+6kXqcGvSPPBLyRzV3n#_2%poMLPy;}#2H1f|ypzc=( zU3ZJs)d4qHr`qJBIVANAh=Ml(^EXl#>v(6PSc=nhkc-7(trs7tXfdw=*1rP5Tw%nP z_642^_p(|61&f2F0He{+5`d>w))5rKWg&9etAJxE4fb|x5>scB+v?ZfNtikh_gn_p zYyD31z$&U?0LqJj85*%h8B6+CB3c345YzH^({KG-9>S*%{EKTy8{zJriu&-H=RYZ2 zK_*j1=A~)&p@oC*0D}QW`kSR@*O=sI)=Z&>?Je3JG!hJ8rj7(O?@CFqixV(?j^jVZ zKH;Ky??}B~QV=AB{vrg+b+L6nak6*2k1D3g{5~jRw2p{3&#f;ZZf!$>(Vw(VTa(r6 zJQy!_B4`|c{;(rju-QzkFZ_Dn$$&2I)>6wR8&j$u>t`S(k<3s$)$x*PhNS{~%RGb` z=@6BtT)bZv@0(c=M(r(ew(Txr2U2e@cATvRl89ez-S@2n2)9ilj?HUu# zk&x3=Eij!`uKs>(K5$W5<9N>RmM4egRvTJ@g8cv&7Y9Yy{5Ivph$_<&L zT;$P;iw&)8Py!P?vqr(yYAd#|?>P2J?9H_8hU&bQ+LtFeKirAcl($f0-)-By73LaqrN_VW+)qzxDIoNGhB4|A?)9#6l%Ya<{__fozbL zgClx-s$2yR?2WD01&h=}Zfnuud`87v)D z0vsXZA~R=H+t>rAIi_|9aBeAlS!wu$@vOWDrXxN_eL)_S1l&@%TfB^^*l)mv13`$< zc>3qw#LNF4?#b1s%4v;1muLT~b|t!t|BlX1mGtMXT$GAw{1OU+lu#;FobE?KA_$|t z1=+~%34MC9Dr;;8vaxE^Jz9I)zBo;6d0i^(qY$chC;}TplFIcM`^r}GEa;>op~=D* zK9ZkO_|7?XsI&L**@+bOy#(UkOVkJ`h|jPSvgEi{6R2EnkQeYQZfD1-xxPB^-fOls z`&uAZ_}M`qf%PxdbfI;W1mnPmS{|Vd!<67pW`nmEXN9$g~(9x zj!5?T4I!dNV`%$#Z|hHjeEiWi&X$Tjl_Rm``{KP$-m+ZHXQjmqZm@f($OB5}8#b9f zksgJP;^^*iI0&Lo9KYb9mQArt@Oc{AmMSOBH@UF;-a~{DB8>Ea9e^w-)`Q6IE$*lB z2$g_YBio)DviHEAgt|nuYfMR40r+ghxE)9-ZNG2>0)z3&H&W1KRBBjODvne;dANS_ zJC$fe4RWivQV^fm-4Vo1^Y6#Cw{RPM0fY^g+->#?Fjyenpn#$Jsp`QU-t@@16CroKOEUXIwr)_a0 z@OQ;@Vu?B^w5)v39=xZOP6Z;=xDhW9qpTWPUP{jo{F|Qv)jd0kfA5EO-8|2kx{!)M z2~;c@C3Zk5&Ievt`Z&yX3e>8ZGt@{I_UU{3VKvuYyzS`)()O-@j@vJ7Sg+gDy=w{G z`|j!upCy{NXT8|7?<{c1K0EphO6g^G{1k61j}g|OWS|TR2_(oNuAjHt(n!=}YkCj8@$DWp`lAbe z!K;^J>NiT{!V&T8dQ#5-JZ~-(FB@OZq$fx$I9O)9;+%8*q;j3>*8;gJCKQA8S#aI%Sz28gwn~inMgI)i zQGcK9*f-2)xm2ErRP7IM9p9=t`B7;ITdBxre0OE+4JUDnLoIOAc#I_Q&mHuZ#I!@$ zyA5-Gn4isi^@Id_ZgYi(eQQZOU9Ze(A~D8Kum!wl&QWKqx#1Ooj#t|fc%0r35w%KX z&LzF6yqMt%VG=EKzBo$c`OW1R`L8hRm7IjCTm)pFd%lWQ^UElXv1z=962uldth`Pyj_`t{;K6{mxbmtVYf4tpXT&I{J#QIdq6^jUD zrc(8uT`()AM2Q@Fe)#Zi$D%;Nufd=?C&_6)F&>-wNH0GRWDm#dLf{dopy8~FXJG@=gRVHQ*+|lPNbJu zTBB}yvqG_(*Y%erQv$Bzq&f{&f-N_Lp+`R*BC^Dpnlc9TUStet%SSw4414f!MB*M! zdWb?q`+Dgl?eF zwotUDDJ<6$#vfbUyd*60GBr+qW>A6t(%@$&`Tfn6wc;AL1FOVb$hFi+R!-N}oRV<< zdqh~@8(2hpXE;jumYp4)bqG)FyVpo=%jqz=>Nls_WPwD;qW}PSVQ?M+nl}#qb_lGL z7qvSL=jrj}cVj$GPX++DJZ}ll8$g z=3T0)g*Op1dz}>pR(v$j79hQ-F&QA<9?e5TCrc)lTI0;_^@T208OwooAf7+fr0Mln zc%r9RLeGI)&v=0ng<3m#_JYm)=MhzZ(MRwytziEphw41=xe*2b6+-=nkzjzzm`#gM zz^o|`x+YdEj10GJMm1fqIWrRl_pvfq>y0watn$uOd@f&V+E2-8dmP*7q}V{#&ZDd79wt?i6vf?+t~6cGomC+*Q1;H3pKzX`~l=fRL26`WPEu8DLf>`QASsxgzWj$B8_j*ehQ0EgocE0gso5I zE07RbzXk3%FkjPMN`w$$w5;di6ce{&6k zGx1}{pHO1Di3P;$3Y$SMMA3?iS`#%ulJrQV9HxNX6$xem#ZzUDM_(eg3To$!{96jd z{RO3Ny>EB#wOn4jWrA>|wnBvT8mM>1TAf@PAK3-W3e7}^-(Hu~?o**cWTH+BF-kYL zP9iKih)r+6U6A(SVVb;)M)?Z|(+I^vl9HL7se?|J&9h)NE|ZR?qt~k!qY-KaLn@~{ zNMmuuQ;f}K1|gr{1=z5w2CXZR$ec@vmlQNLOT_giWF=9cPaK)yKlz4!Z@QE4UGar< zCK=T1(emdc?-LG_4e(ImyJzpP_>hVz8tU&l#x1FRvi>dI5Nn2mwirDAcaIULY+}Fr z`KOUET3FelGHC_0KV}^pq)!u8;L*2~fXh0k8(}~AqIg#_;GDhh)y!|!Tg=NOJ~r|K zuq|`RT;+>tF{2isU&PLCVAZbjmrY<}AbH!i~tDD2V0A+rR z;t3pE^U?%dRA0VK?0sBM$o48Sx7f$$o{WzeLNgG|xyet9If{rEUIMoKoh?oRZ3CE7 zD5pPXRKbd)p)9AWqUa8wT;`LkQ#-#i_5u5iNdQ!$C`sTCsYZ4ObB!37y>1#*-TaJiRMP-onhKmcN9$_b#KEPv__FH zSD-DWy9r`SWY-rW&Cl?*Kd<4p0v)Y-e6PSitgO1j92(1a`S|k(+;Eb!j6+Q`4T&hRj=Iy zHxEHXwR}8NZf*`S@Yq>Trb!p$8RcNFrO?h@(Zf!toes!s;e>9n(G-6jH{hI%meVQxqyGOwAl(7LTPtc z9x&OqPXaZ`Yao{ATG$v&%K^I5qzC30&wy(AQ`*!%LpurewQs5!EoGhW4`<+rGS(h| zW=vq}-vWGq{3XLl8Lc5r&I-|RqNoTnRv;Z9KmjPi?zDV<59pJAcahuUg)}`;u*`21 z_~$lYt@#nqQuS8vO~3ehcJ?m#5X>~BfE`eN(|&6TP^(hY>;Sk%U6MrHeTR-_P80hi zgpi#LC8rlt+#oERAd0io33%_h5G9R-85Q=vv+Tn@X%3*>G5GVW_6iLkedk*4mG!@G z@G~-arp853WT;*Wq{9Sm9o~PF3-^+&xe3Mrp`!pZm;bG9bh+Tw$XVl&=e#khuU6|={G<5x)J zjoe<+(%nlrKWIs-&^q*qe|Fy{eA=hN<*XZpn155jVxp>2Q_09--2F#+ z&iL-Y=~f)u?p7S(tmTHW5Z>?`qa~iv)lXB}I_0`;`-KosSJM*>q3{YM+LBOE`qm)3 z6M_t~n~m$##hnIc&e{%HYhrYq&&C^eI3G z>sW(FZ{@BDny!tlv4TP%Xw&iM%_{(PZgf8(Ie8gIPbu|pe=4pw-ydE8xx!{?{bIJj zesc_P-^*J=tZa~pdF;5}PgIC>1N55Ok_{wJUf;WGxO5hQGCpmxL|g0*BT?BLM=_Sm z`&dT_1ufQN{4KeReINnN2X(<1&=nN|6YdK&``ghxISyG$FU+7DE#O+=I!|PPX(WiX zMM*h{PX7F*)BkjJv105DdW5Vm zLOG6>4c}h>3b^uPT4JAYWLx7Ku*cnRLe$8q}@{2beK=M9C|5r{e+}v z)G|aH`(F*(jm+=L<-C-@v^RG#{^b^XvvS#qAJ;XWw-GXFEr)MhuKrRc`FQxKWQXBT za~7WntRQ*${Lq6QM=`6_XsjfW;AHHr&jW@V*eIfdq#C+u-aUybMd5Sx)6#alWw0c` zyGo4Z;kPx?XSOk1T7IrWtE7Anzgt0_i`J*ze#^9{sJ~eQ`Rw|*q)g1Z*dl)LID5^d ze`8~9z$-$>bF46)pAL`!PEl(F&!Y5n63&f;bht#k903{CyO!z4Y( zdEWW*j|~2D>)nmotD;4VojKU&lns=}OqA;mgah<@`wizeWmsCe-jADesiDDgOd31< za;q?0>chEHvaw_~Q#a-AZPz;x9rM_u?H#KE8IQi?Vc}B6Gkn4*nAbvG;9$nbOq{%s zkp@n}Pv(c0<^W4)y%eZe;+;J?WRsb^Z>}_fJE8oU@sxkdOJFKwY#2Lqy#gPr<<053u@R z7*J6UJb~O+Kk_iJu_@xLDL)Vw5dWIO=a5gETlkWvE2uQv--Djt1EL;WHpAob*dHdK}(YYe}gocci>@GWXw31`Ji1)LAhv*kebY;hE#yglF@liioV zE>}hQwpg#J&gzXI73c2+Akr;H7T3|yF${cN=!oz}uz{H~OC_^vZcy(^HLKJ#=ZE-9 z_VKxhS*BF=S-TzjjRQ}{y*>{6Vh>!kiaA}`w?)mf z7H%x@Q?O#U;!?CV&gyIRmI=+`2(+7m#=Tc1-o#C130>~m=|7A{639d(4(;^=E%>Tg zh0(^j^XpI<0fXpb#&wscWP3UPHkmwht@f(7Y*vS5Kfv9$l&9TB>#$4W(){XeWwXiD zz=gZTlI|L}pv}1O1m{2}ELm^pSInE#uZCeUmtQpQ0}4{N-5rS2#m%Rcc!%=%hDl^D zoGPgq+EWjBnLR*6dSF~)S7(Ca>z^yW-MY+;ii(`QW}TqJ^eyQR6Rd7FQ?4WV9 zcGHC-1NrVGUIAOj!OcWS+l*`VYEj9EYtEZNSh2%2e=PctoH~_oxoml$^zP-vaY^bR zqi*2N^qPfxPIDsr`kA}z6sGO8!c7EDx{&1h)f+F8 zr6v5Z=?l(f3G5TLgr-thnz_LfHG9d?ckHGQe`7cC3!rVV=hQNLWmn(oMXtBQ z0!KO1W|iN*#TrPzpsV%1v2$y2AzTY-AZr@jopT;@_v}K=7UyNXnXE(3NV+BAu_>Su zqgR^{U>bdY{s#SE|7=CT@v4I{nb(<;Tf9T4iHtY*$nbJYtIzymK7h-8^huGODZZPw z_^f~j6d{gLBb5!(C~H@5GvaRRlv6-%Yi`yhU2bw;fWJS|5yjTi3-{LUr9K5-6t!aa z>Q?Umb}TynEy&mg?6~5g6@78GpFj8f|I;Ct6PCMf|+vI+# zY+;-Z4(AH!@SRrbQkxGMV$`axb2Pf4&0&Ehg2RwKmIhjq=%zcAMs^B#5zqyfgT)ja z7is*3(t4Ebo?r?O`vvBz<|okKGTpJycB?-t!D$@>jqny&1d^i#ag`t!8~+S^JDTLD z$L3VMK9z=uAZ1e``?Q1)-$X3&6;k*G+ZQX} zbZvr?G9OfZW5B)%{TNX0GT0!^(n0ZexR=7Q+L!2xHvLZ7Q$b`#YaWChaO( zaDo*C;-aim$PF+R3#9{sQqO=zn;AHGfZ279sy+F8FeWl7L8%!g&_PQZ+KO?O$a2A- zn3`qF>Z$L1eZ`1IvU&JR=y&+dpuo*Je4RuJ7ed;*KcraN=VXA8N&^UP8)v_?|7!$? zi48_>f%i<5?Et3&YuM_;-^LVG58>&{gS*`xArVwN(x(T<_zTM4Na*0keJ3!i;pWeW z5vR`0MfRxS$YI{lqP)U{$Yu{;8RGY1rElJ2NYfi~w>;q9_`3F80-$D;_p!g^2B`5M zFp?Ax9bW5EXGHlYfK?!Jqj1@Kcnr#4R^zp>Pei82AmtsLujT>~t83f>akP!R(9C~| zJ&V(?e#2fI9?GKnN@mB&{BoGu(bAl7_f?C18pLWT1qT;*YiHjSxnGUIX}Z;zkWBc5 zeWK;m+fnPI!06fKW!e4lf~mJM?U&9CAN@@=GlGKpjyhrl5qfA7C(Ex>VLuhAQoh{k z+SPwD?^Col2-a8{$eTkWly}|0kB?idzlg{ele)>z%kY-35B?`<&;%pcOOeX!Nzq{HW;u7WN4kQOE}AID={u z>*QlDn5=QM&j3?#luIlcRl@MLU+rkoSKPqS7a$4Aw7MXfFdZNo*n(c^sGoC4x}Ch+ z4(zRnUl$p52V*fh84Q&b(ly{il>=HY0dnVLNUa6_g6l@w5ikbt?sj}f?hTas=DlAc zz^PPQZVxaA3j$!xA#O-7mhUDLuCSjn8W?R-kw+Cl!(a|rXf~1!YZj=(s>K+2;C=#? z0m66%VF6}PiI#)PWx}D;D06%ksAL^Mz6Lzq#x^LM&YPR7i#x`&9RK8 zCwE&ua#a$ajiDa5j5>A1K0V=nsJ8980|!547#!X~KdN^Q&nPcG*Vhxhc-)VDVh@}6 z>ST&=g$XAzG6jM45IbM#vnvo-=i^Xd%RRMrU02b9QQWHzx0G`G4pVPeKg3x^V7K> zdd-z`gTbmZ@pCJVI99dj)^o$EUlZ%4?V{`F=1sJlo=<+DyOkK@l4jQCQ7Gn_zj-ineL5*?EJZL1I;j^kBTrsPdJ1Wc{-{DZFQNAY{!f&R$S z`yWJACcrqVybwBaA{K9cq~|Y4<6i$P=s9Pgs`6F{-pRo!N9bbPZ*aL^rgzGeWzAw- zR)FH#fL(UFHp#x;E&W9!#<166w-0Q@uTi;uxdjh)mE!o;h(ddCQ_0O=5dWYQEV78E z%#V;q^0c$F<5)nx=K=1UvG;iuL#NfFq5na&o=*^0*rq>l2>vvG zyH$|!Nj{0AaJ)a6I}{ujDKIn4DQ~Nj5bbDb{V(hT9(%X4i(F3Q!O5cA4r|fNO6pSm zHo!@J;9>>fJv|)eJFC{anGBPF^qqNJs~qJvJp5MDCs6{BBA)>bDw4#uRINJ6*WkMM{H>Z= z6cs7^L99^2K?T7%VX@SR@hP?X2n%}SPV%;fv+tciPhzC_k-H;K0Fqj`eiCoXP7)22 zuOadb59BE$J_MT%y0NS?qt>=L-N!U2>_O3oz&;OK8d=co=-1h-GN4OSx2Hc|(d%Y4 zjBg=@l;dIdu?qHLuOWf4_LhX|SShSfelLG_Ej#DUR{3?H`AIDMmVZ#D7FRR!fIgQm zYy(rSlF{n=bf3>{>%5*(NXQo$>JbZ$7NH;_&OWZAxY)7XK3so-{gokJGTbX6nX9xs zZlP&|T^7%Os_}g{JqnI-Rcsq;~;>zu5*!5x^|9-69lmyt4wS79ZmBQPUElzo$4UN_3->J?E{@Q01?xKkhJZZ`PQ!lP*&s0CfQeS5Y%GH)K%hsdr;?%?l^;CI!d#Kg{Pxrq z=j65RNVY_DcN~z4+(*L5wOMHV`Vh!NRDl6n-ag4QtH`cb*Y~3h!Ro$G0MMv$JKpB% zymR|Imw&)$3syp|@KLq*p@}-SfIQL1;77Sv3(NwT(O~m{umfz3WRE7a^vzX{*z_93 z$poBZe>Ma1`IsDgOK#lP@Z!3=-BH2Z((ftv?>zF8g~Wx$-S_9}Bfvi;F_y0|HfU@# z$4>**Yx7FGBQQNhMMaGsIkuR

?<;YgEIPT(BorY^mojNPY!2#`NKsdWee_zAwpAf4$c&e^W8F@!OPhfIzbmXF< z4(AaEjlFM$DZO~Gf(1N2#@w|+s5lfSh=PYi^FNwIWNxfU1+s|nx3OA{260cMy za=s7j=eR8HD4wHjcDsRW=Q*>&&9z3Od(=o>PndmiI)Q`mk>qHu{E|U55=Q*2Nbb$| zy?Q;(p5lUcQ9nZUGp}&V8~;Ym#WVH+y{v*Jyvt zuTl6a-9&**G0XMP;-=7z26Y4$NAk&Lx2e#o5D#ZDgSrt?5AoaD6DS@I9jeU%TD+`t zeJ{DSzW8jVg0O56tcMz&&|Ua7*>VmeZfow0Vqo7`k#kkMZ@B@zL7Gq`kw9}j$}YPE zERHSAjc!j+WbHmrVVGs{0aJj4Hp0%%pQERFVmG`170?7@_I|JV54McXlW*;IAFI5r zoyB`uhDw)qk%EJ6@#{wYT`MaZLC#OAkRmC|IVHuO^;iPdA zUCNb-F*@RfVv_O4f@b`^p29pGbh!a(B{LcfT8O@4iVJq%QN96%W0<tPSGywA-o4{p0NWZISHGjrR=z=6u1CBxJxuC-OpWOb=B44J zd@m$I@Up;WGU<#F49Z=Na)_SfPhiWkK))Pb3I_d&zcArdsi-O5m(*L&J!^D3X2NHO zqyq4sE}buci-NNXfa2wEm)i?HzPMv-hwfk*wZPqfpbSb!ZVJzayWF$67a@&vZP%mT zW)#W3Y_jmXtTK{8_+0*Nl10*Uh7_zcgH%E9?*Fq#a9(U@k6vpIAsbQMf#m_jetN$L zLX-m3cWN9n4KM_f5~xi0TW$7(48cnvg*KlBr-AJ0S0QsPCSN&8^O!g8`y0sai{=ud z)1mUwF|9iRYgX+Lw<*qYWoRjL`0*XjN|Nf!=_y|Bu|FXe!W}^;(PIyz5CeByROk`6 z1sM%PM|eV`ZR`H_EShc=#=hS2xIH8AzG5J*{cv{fkuXAq%+srXCWXX!y}%&V|YU+1+$@Dv1s({=g24w9 zxPMk1u}>aC>7a9xm4{ubD-GTO-e*w=s>(g~crbfm)Ut{0>Dvy35Z#8zmH2_YpD9z; zaVW@{nK&o*r_U)P_%6dMUQGK%15~MJV-Vcr#zumq_f=5Wuxt0i1yrf8mQy)Zr+`YN z5ZKPd(kqe6ykb_)ycc2y2op+bub03&DZ)55y=0)BQUW+zbQJutYq|$a22{#Ew6uU_ zqVC-b^8)o9!wXtrtowj<$BFXsf@&x50fZeG{ zS~&Kv79S0!P}f9%&KTdgL33pWT;kTo(UmJ-8@PZJ$zQ`#VeG00TAk z3yf@Hr)0NnrUx&@#KerHiVGUA3Y*LAj_Fyd8q4LU`?gt)4kLhubwm;Dvi`vcA7v-w zxNPz~jx{#L9&yW*_~n?#Qx0Fl)tExZudfV^H7$*Kfe35?``4O#A-j|5nsZO#@nElY zd+Ss98EiOiI5i1*WheD0B$k3se`#?=<(T+NS@-0cpc;CK{xts^&(l1}0AyyRbO-qt z>hZJw(Wdk?iSKX8oY=mCQZO8O9i~mNz)MLMpyqzb}x_tv7g-3G{#uoViHI zx1%~c1q>(%o0*=?n&#gg-YM zh*S~984wsTI+_3!t6dfBehtQ{#*1L3`1IZ}fh-_A?SC|zdyk0LD;)r9F#M{i*w?;a zUq_&IO$cJZyzO3@t)>)Z_2s?hkC@+yDn&#zcM6tGeM|GmmTK){FUwo5nO}Q``S8Kd z?50N~Us$G{HN5spb8~wTI%zPH0pfkZahX`j$_R1sJ9tN1pk#mvknRhzXp_LTQ9&82 z(%;9(xT6L`8x}woaoU~LC=35E(K&3MqWzwPN0apu@L4(^5o+eix2@un4Zc*c0u!pL zL$0S!RZ8TOw93W3h;0 zH>gV+eV2W==lt2^Sy_Q%S~j47^>~AwMOmP5YtZo=w8aL!x;6q+O!ZR(_81bM0Zw=! z@gMn=TOVIJq!aA7$lmIOCWJyCAU;s;q;A;y_*kM)Du>#?CMmVqBlLlXcl}p=J$KCJNZw|E#X|14eeiqPrcNX*P?I@D(j+E4Yfer z92-nJz0Sa|G|2D>^hu`t>Fh7KmYj@dWo$WAT@QqJYQJ0?JGWyZQ(Iqn{v$VS#eH8R zf^$F^9=e*fEFS@siYXl^(tmpa$N>b0duY?t6jb7ZeB^h#nD`W9M)nS`1rhB$XjU#p z@2uaIfPQI;qN*k$NC4c3!!YVuMZj39t*LjxGuXFpkYg+wqY`g;(IGpLvK9il6i8yR zT?+KOsL7uLOR=(t_Fp6M+ey!iSnoAkbxEFV-vEJj3|astCp=NuJ?iR}%QmB)=7wwK z2OW2x{SBW(PzKuIu-*0lBki1`^W39uAHK2E*j8iPZj7c$W45tvn~fT)jcpr^?KJiq z8|Udh-x=eKaW2ngFWbG}^{oF~bN(g~POeV7JKMj8D`!B6g0h1S46!5e*u^+rZ{t~a z&GaGXT%1p$ZAZD)Pt0BhE~Ha0kJq2-DuCnYMYhx76!{ZCJunq!Qt=P*T&cH`sA|88 zR1c)UA%&WrR26*y;&rLuV4!pm;MK3CBYt4mO&hoJ+#OD+Hy%KQk}1}%(pS3P?@#iT zSn8A7rc+#TlB9Yr1hCPcfPs+mq1P86u(G#10^)Hh)zRPmVp)ez8Ju}QB?|BD-qqe% zu%?`YlU5?&8|#&+#9aF$TJ97!ADvnRge-DchanLV5Xj57qQX!Lfgm=_AN!p~hUT6) zfOq}PH62a$6=?L{Qo}c$P}G|Sxf=qd&dEURkD8XKn-={(4e*b6Q{n@Q(x>nus!M~7 ztvN;!f_Xaro;zJ+1VjwR6%c`$oC|WvtReuyshmFmh_a zSB*-`-t~_aqcE7+1>OzM3>~e9_2H!!UXu{?s0okxw#zfao7VxWR^OgyVx@$X{N9IL zJil$oDR}17EEEd%{4NGt?%{x2q}Oq?D9~|l9ew|-J9}B)Mfm1c5rTCk$T&6?#szN% znqaz4uG>17Z-1o3;vV^l3TKjZwLyIJYcSSwjPV&6gy*f?E~P+e?)b175iaQu9^ljk zas|g9Rtt66>VL_?{Cl>xIPT4SB_{R3CrcGbf`Ucl70B8r0?|!H#HGM0q%csw^PLm~ zqW!A@o-P1bi!{&qGj!vH(Ctr!C=WN476uh6r^gMuqh^k0D_j7sYQos4*?gMxyOgNj zKZ0X>C2%%ZHZq!^G(3)AmKX6AIb?K^F*Y_f6=69Afr{T~Byzwa?=KSCm3rh4p<+^Q z7ycvSFSUwS`=*_>dK_oT&1_yQ6`FKc>wdaD?;B=NaXk(qi;SSoO9yjc2C@!mPW<7aI&2HqruJvXHFMZx zKGqyMBw3&5CYM(dur^oRtkf1RiLKZw7@&q#6mt=}e$^Ty7wFgV{nV8HZo37OMOS?K zje9G)b|+LHJB)Y!`&ik1@ zwJ-pj4BmoE!#X@}u-I+miM_1r4$Yr#yX#z;csA__=%VGf7iyiMwJ;q^@yby3cxBT* zFB^_})3PoD`f#64r%tq=4|iT=ZoW%PN)B&g4R=gDeUCX0J5-n4Y!dK^N+XSVa}vyq zp>Xzk#;-h%sVG(Od>znw>d_Rsd752?gOrJJpfk8JTkCPz|C7!Rj}or%@9|ic^c=1f z`5m^^q2h2;WX9BWN7Qo%`ys~Y6m!z?#z*(VOJg+|{-A2-+tc@_&G$Ep2Fv1uorA#p z42UkQ_OX9jqYfep|>*ua%)G9%3DZ*+~m#41zY;z z10Td>ooT}9;IEE>Af#Oyipggnzcciw`5hUcMJIxZCh?2@lH%aOYyhbAgr17%tG!(l zU`{$kSCLr?1phYk1UqxW#*%-(*t??q?QtR)aCs(s!bYfT~k5DwI7gx zo!vx7O+~GeGwy>x)lOShUogZVc#K`I&Z9J4ZD3Ql)akNW{4JzxCXX@9s`p$@Y3z3r z@ws~j2{TxCn;q*8d!^kBrWOrqI#5H~??sj0nOqBVR@k!^TS(`GTkcOAL6)p~lU^al z_sOVx#Qcr%$eCbTH8*!6K~jbLPrUL|>IJ6y84G!w^DH^a@v+c{!QrioevkN1w(l+o zOKw#fxeHut1|fs+x9K7V`8=sFGUS1>yv3go3Yb=E7GbQW*yIiM#MwLQPLan~%)N#X zCP@{~-rNY+H7Qs$SxCguG*DAqrjsv}V#WRq-&yEo-2Yai;EtQhwsQMyKv2&6;FyiJ z!cd9ZZ_Mos_@bE}J{86R=o{Y_%F~S~1z&Gw ze}izN+5a`3+E>r8v4B#<=*0XOXk_SmAO-+mp~R%{vvo$#!aq;-o!ws#DEcq;znC_w zjU+S$)ti7&xfH&nmqW17>Nd8JVx@&0paV=anC^*B%Lose<^hK%tDu>QQIlzCz17e0 zkndCPB?gM)HB6vsF=_F1Ao*U%A0yRKo!_jw%P>a>Jue;D!-z~l`9%|;t}}h25I10a z%48e)1{f<=Ab3IbG|Jgb+E9yFu59E`W!WM?!J#y><&X;Kb+sGKok+y@r>&6|d<2 zS!1v;6LOfCGV&BXq;2zkniP3YuRYM8-FTy!(jyuDuaW(ZH8c3oR@Zj3^J^t~sr6A) zGp#Y#j77Fbs685Y26EtwWf#!0jg?>iCzB_RfXBA{cMl)(&sb6seI;xaS`{=!ygsk4 zdATsUsP63Z$TM}6O_fuAjn%h3$=xGACCT9<*}tIKjptOd*+hZ-OyCRjdC z{K&2@>Clf@B(otc9-pDFQfPd^nVE{RR_oHQ>L}+&tpj3X0MO^?mcLl2zF>Lkq~WBW zT>Hi4Pt_6k^(GSVfYZO*RAI3e)$m!nAoWyLJl6I+<*!j4szDRWpgfhMbL=NQ^J1ge z^P!Q^Sj$V?c#E}HnUs?zlOgY(=CB^GIIq$n@7?&9zoP|6VXlg8IBb(AK!;xZX5JyR zlhnUjuG_zadI&VDT@Tw)H$X``KJ(2rumX>P`BFA5R{<`1@)lzv?$hEz{5~UtdQDlToX z)l;qXbLE=$^g*;2`IFeRAF7xiw&7r6R*zE7{=En6G#82`u zPkWby%&Pgp-nK@}&6>4;fL8-!4teupP=9{^>ZE;z&0Uyvk zLv+uU8}bBb1EE@l4nB(^NAqN3%cdRZWxra10YmRxng`Ok0ig+b7XonfgLIDzYEK(V zpfvd}tjb$}GNmwqNP)DS9pgzklZ9~!#Fvg2Dj0`$lgXDL!eDm3;%)W?rgmfQ{Q3t% zA2n|scoy~@x&o`&Go0FtSD z%YXukmQ*(iP7?A>AT9y=!yQm+Z})QK!gM%hY*15PQwtmnGH2S6|E4|h4vaTh%@)Tp z2+HpaoB=-MA3Vm$4<_4znj?yOw=5m*czNoeB>ZB;kN9}CAznt-gF8fSSAx_TyW&F&%lm*+`-228cIL2iM-kF^!B#>y23Ff(!f%Vsk+ ztl&T!lVKapTi7ief&naq+O2{SyBhF2Hp@w1$idLn_n8T;oi6Qv8YbR|jsAPwki5|VgviABL_<5( zq)?^Ofjyh!YcEN-ZL@97C<_c0EbEpjHwIkLBoK5<44TBV?%l$ZHT^7vvxAm)1JOe! z2->;L&p{87gJmcsi8UcZDm&caiF=b#WbUJau?U2`0B*8!Y6MR{g7B&{kMJFoLrEg@4u$ZOvoTbcV z;l~d~X!n}0k!_#%aCys4&^rw^_4OEI14rW0cq6!pguUHMXUTkw#bHN{AcgB8kAq?N zR3?EDqN~JR;|Lz)3V5?tq=2oQ3KP>+`~oeFJ`_Q@(duVX+YdbKnf(mXyfYFQX8-TzB1RFy0ShccG2E;{5t-A zCzJ*_i=G9<|Mu%jlhIQ6-|KppERK<;VU<)Z~RaT|dFdd<{RH|r46rse00eIQtn zf;G>m@k_K!tD=@Dzz2{;Zt~Nxk$c>*XqLBiQTZ=W!r^iMfjTleh2m#K#e19bbU0nT zSqey@^TDaCl&1lPHTHkSmn{OoIt})*o#jC>7CyIgNXELL3FPBp6aP^+KmSY0WGJ7$?g106c>~D zd<=&$TQCR^mU!8yfxuj$uM2tRxBh52mzwo~^0Z}^HjJRvOPQlhc)g{{mR<8-$?E&vUF#6h;6 z@jT@M!RIC1HSAIh-H&zuZ)7vO5}V_$tQKJkOgSm({Xg9TJOZB-Q z9jWjbSAcaiH6`}asYjq9pazR;5EWnwn8XI2Ac8-jue}43&TiTO=NZaI=o7I?p6u-^ zH`k%Pc*0+MJV~MWnwY_I`%Up=v9*%Rm>@8k3;-{T7BXI&l4ZSv2_(x7GWR4JGs=^X zUW8&~>K{qOf$X9GE3%OkHkN?k!Ec{R|D<0ViA5u6h$#yELOI0=^kLA?d4Q~~ zt>NbYkZ&#rBuh>#)LqSSu;>9Y#jXxUyKK;Wv>c-0XOj>znI0rc5~^_jgVWIC&lBJN zXo2lVZ+6{}>zAJ_2wr%2CtWUNUt>Ec z@~M`!eAAaqi9+@fxt!NBgFAK>m8K3i2jvAQ34KMW#baoVdWL*^hZuW@h8P?3X&IVQ z%rt^Ka{b9NW+`ma9QqI+F37O%yy2P4%wgCO0xFS|r!54kRMN#;4tH614htCH+r`fI zf5wxXpKGUlOPMJ1(0vRFx|vMTojcm-y|Fr|#}Y`W0lQgP&J;&E>u!*>3O0snEm+gC zA-GA^VlOK_Zr01=*|^u$rpx#39N6}&rx=CNaSM;IdgdCD3#XZ&5C6GWvmkrE((O(E z>Jqzo`vJ3k92utl(~?B;{6Ezv;Y+1fpt}}q#K`lu-w*3pKv|vZTgAYdqcUpv0IjPU z#IM?01D#x^$~d~{M0M8Dl`DxGq<=^W^&2|qYg&Ik*ic3U4YW5!8!SBi@;J-fZP<{` ziXPU3xJ1NloknD@;bWPG9FziQ z=AtM%IxExU7NhEW#yzDEAW^)_;IvA75!&)xtkR0R2AojS{k%7%1zi}7=mlt)NTqyO zTvqVV8)-!Fs%4pds1)_T<2~ccuXjlw4_a8b$ zHf$A;LA}B>ju36YaZ}Fef;dTVO6EIfa~Pz2{1Xwah+!fz=uw5#Frd?+!+qulfNt?M zuFw4UUWa{TB}2IS3YhnZ>xM^6fVt50^DsZI9Rib+M#H$4nnvarApnneupu&nyBj8I zr-&A>eIOWB4=c^tdD52QCct^|B6!_rXGLK^`JnSxcn{%P=rl7>YY+ zo}qVR&oNbvDax%-qiRKK_RiaV;J>87kMS#sysydnR9vM0PqP2iqej)U-shRG>6ZUT zSev9-|MW~F+S_Uce9BQNimo#7B+vwg6Te>R8d`v6tsC$bE8%GlUJs=a7=!nv8u zO$^ZSBRXY#0ebhdjVzh!xc9a)D$cxLLVBxFMaH^;mxN1=NEXa?c02KiwEw09%Ii${jPohY-g4O>U5c=&C3b@sjp*qV+DZ} ze|j;Fr0T<&rU#C$4tolcgc1{h9}Vc=0{(Od{THUupK|3Bf-F@yHp~_DlQKF6gAnkd z0J*r9ye}jPquA9K9e)?~gT?{PM76zgdJ;eRXy5)7z`W+ZT%K>o*H`D85I=L)AuS zq?U~uUm6Pn?XZ3Bc>7wE+d1iUSM%Kg&}IgVS02y!wx)!yE!DsKYbFVX*3aOy`R8#= zpxk3Wey$dsP|zL%eEi#`f!$>2$nZ9vJ)hw>NLf*#Fq1sZjbr)GTii(~Em59Efli1W zY&;JCuS{RyRl!<#So29-Y}p)uag?)AVh=Yv?lTpn&r@@!AilaLzfZ^BL7*GD*3Q~g zC}5UEEnSRtli|`baj^#NbquU_izATr0L#3tZnOkAXIc1k=PQbtDoga&^q5C6INzOS zzremg2OgQ)3LwXE^UDH%@ENFNijgSJ-N?1EJybB^G zWxu6(q<||smNpIW4L3edxda#x2^*uhbhPkJ16D&VSvTGd7Qa1~%|jdVI( zMlhPHiI(pIq$8j9M+(gko#1nWl&k&GW$0W)+>H#%_;hg5C?nwOp(8Zc^0c-YsH*uSB% zG^cc8Q^IcZvqKK!)P24I65*T{i&m6cz|TOb{2N9M6GP~y>BT_JbPhH5TRFm&qpy=2 z(b0b=8g@@JL^sa?2<)!7Zuo9UfZ;q`5PPjjD!A-*_K5U zKzENSDsP`3B^05oFK9|a3x(vf*gk9QA8lxk!>=%m3U~R8fnT_f#AGbfa8Unz73Mc9ON-xQJ^GgF5C4Gf_ z_#Q4^f}gTjlWUl%wP(-FB{m(8#DBZAp}i*H-DA7=(oCNO$Rm24U{zqN?;NX_WC{NH zl~ykMqXS_#3jyglFmrz`kNGwdt+=Tn(+uo{0>il&F)*{tY0&2gMm=ah1g?vv+66Ix;u z5t1OfFL}dw<99T$?n}^t`r0Y*BwE|gl?N^fo%q+En8>S18WUNYqTgi*STlF?4V#s2 zkerTQ|NQa+jPc|)|M$DV33wNP);p>KWl`D7R7smX_5B|LI!%2@*(q9qD8JgBSm|a) zf!$|Qv@XVQN;be@fc#kVK$(s83wQr>5U(+fJ54vveu&AXP^o)wbz#1u8x`EW&*Jq~l z+7NhUzCFqt;#thsNgF`ve}XmX2){pJLWJe>muxu`A#e+jIF*^zB3(z1|EWy%=r|?) z4_q5TV*K2MsS@_o==@^Rb+w;C?bkSV&`5%0k(@ZJ106G^zu0(o;qVEmj4c9TRp%Sx z?hRkeZ-B{M;!a1l@YgLIR(l)cc8uh>6oKU-JRt?0f+|!xC#6H7f!0xUiJO z$7vb$$@;-U$l)f{q8AZb|!sxN{LC#f_Hd7Xv(~#dG&Ns zH4B6ZcNIAJ)7#0@BN(yn+NT2-oF5Xac@Db0S3J&nJS$f?G}I)UpAYXxG#W*92s{mr z=S7oCHfT;O1Zy%npP#)LepKPb*mCNn%Qbv;x5RjL_TarM)OWtG|_HiOz1oMb)a7eCBIkTr5KVMg`}A zeXe~c+0)bB)y)>MRl&dQEq`cQb&rXEvuZM8va`_^W`f?Wu||w#4+Qr8UGdj0~FM_*C~()QpVs5B5JpaIa|p zrh3O0r03hT>TBA_?6o*31O(Rj9eoP+`Y!Kb_L8urDQ*i?Tn7Ul8pO4wJABHScl(ROE=G)Xu;HaQrq zh3HD!Tjn^;Sci4-cAs}G?wd(?3tz7t;PTrMV7z~#(oZe7gmpelg8wX#xFA3|^W@`R zF?k?Qu$QZ!qOE)A@1FR>EU*8M-?M}AX}7fWL)H9%KB=<(^rH4`+0ZajvnBV<+sb`| zlYX@cEF8eB=ywX0+>kHsVva&$R7C$fd7OXx_scA^YfaAh?8Zy%FupeGVj>2sG|=lH z*Si6PXy_D~uzw^;-5Qvh2MaN|2fmd9Ht-QT{n)cSngOj}DXBWx@)X%KNzdo^3= zI5N)B?s@r#T`;oRQz6%YB{1pPQ(%dQv260dDhK4ucX$1n z{FLv9POWJ~wClgM9o{p%&_c`LCUcAQiDf!2J-d1JKbfY)hakJLs23EB=z~pfxC;pW z_?_p6eRkMMW49QsqS9g_h{L&uc47qA(we&*9M8O!Pe#$cK@+2o+eG&+ODdX^f#pO% zXRPayR$a16)}$A9Vo(q^ZV^(LX-(sely8tss;Ks?U;q`zx}x=e(9s zs+gmgpTM~haC3Eg>PbO)UC<2{I^^v8@OpWe8dd>d0uRT(Ct!|m7_llr24ZCkkSQIH z`2NN|!aR>w_ZJQJ3%@9VK|=EvMQB5>hsKfZUy4yBZDA;>B#y4wdIHhYF(QfJvRAs? zn8ZB|pv_))6SkxLf$X-!HkOQa*$*NZk|2rxoP=>EQv?qn|Oy!@yRlQx>szw6+vK)+M*5>#n(12e-_T#`wb zi~=>Nd!wVgXxu_t@E028N?V$y;*xLBKxO0Hr^ye++{2D|cwvT)$;;z%D1(%w{6W3F z{jz~!fnu88&D)_?4F$uIBY*l+5WKX$3=4R8OD#nyg%>yR+!yql*0h?CAbOKKm-3V-`IA$VyUSE+PWKo7X<)lwk@GX26`^0H9yYjrpQb*OY;anZl} zyJjGG5n#9f<0_>Gch4V6mI|E1zcRwZ$1sLXD|c99pq97Roorfu6MV!Qprx6Omx&tl zd#Y7!O8j*+G-O#7uAw1|eoZ5&D!+m-h9#RBE3MYGXK~o|{gNGOCL2=ow`9OvSxqn+ zuc&r#9NF5sylnADKbaZ)gf775dDErlaWwFZI9o7}bogoXbu-uiMVO!A{yzNBQvh%? zV~XuQ>?k7ll(fn37VT^H6?PGMu%LMJ-Tf2s8K!(5Kjs0*@yD;5@E?SKOvAea2rkJE znPSos-pld_5w8PC3#o*cONs+B*?1xt*lT>&{O}`W3;tU{Cn#k1hkF&|aC}PRiN`+} zVnR}rMBU+$P?5iOqj&2sa_qV)mN$&{&)V%*8F${2Pc|~BO{XYNj+;n$vrav@vtCa%JT7J*_tC+9NS<6A5Uv{r?IQY5 zt_cR~kaymy8j-|Yi1%3?Yu8Oop3Iykf%>KsW|PfU4?Dw07=sgbZVWT?f700c@WdkS z!t%dzEURkwIN-_6t4I|pAb(PwhP1+!#xswQCHOHQUq@?{>+^cM7z2q)a;QJ;@R!V? z4}~YK1{%~u1|s_f&~NvNM%_n`@=roy=p#&+pG@T=sf8xx%m;_}bw+pfSEcKm{X5oO zSI6 z>5v5qVn_Iaz%DBdM+l#O^HS03oWH#DOO0T6|9&Rk@$H!ov~;~7F2%@P1$N%+bxHbd z&^&2*Ry)Z^OiC~yL^>0LW(S-s(!u1Vsjj>?47<@Z69^QUN7RQ z#HlIL7a;uo3%ZiO4FrAHjoU%EX-9w^4)&pA=#xy-4+N{#@>S@y%$Tl`@v7j3G@vYu zg(($K8OqrsC+|SqHH+|G?qr4$1qGY1rmGKYkfu;GEYXzyR3c4^dE?g3lCHd0%aJpc z1hHk-&d)9hp1xW?oc{eknX%=Cd{m$(JfX2kl1zx5+azZe+ltpu?%_Qd(gbmb%XDII!&+zutRY80k^4jo$<=us zx8EJM!1dWYyLxi@>)O^n%|ExMO&t#1nqw^%qjJVP~uMuSeM7B;6^@0x#7RM3V@u#06R}j+q)5 zY*U*pne<3^QRA<2jY91x!kyL)$tJses`|-n42guBe<`XMs@g4_=_dC~7yG$;>q1=P zsE}r$Ho1%+-~P<#s7oO+LV(l&>JjR+gXb3daZ*`%%yqmm;o6ON68Z2kf@M^)44|YL$Y?kHxx7t_oTTB{Y8FujvNou6qw1AcrDWJ82IgC?1 zBw!UrNCUsDd4Hs4Abop6)zEPt`w8Ma)@zr?3I5^%0xiA-#?oi(yD$6JHFPUiy!Nwm zVg{t>X1ek_d7MvYHzz`8RdU>_mLfuQdV1vcf?Xr-zFA$w(!AFZY zp*z-T-?O~jLK05PcY zi=Ls;1kt~B53ohBeG{egXTU>&`1$K{miu@RUm?*&?`9;KbN=smti1IY-^d@lWnqOo z_nEUH$kSKg@-m1Yy9uRQJDrX>O%#NdtKqARmtZYQ=(BwaY*|Dmy8}y^^hHkL6vyoN zTde*jraMh0GPsS;GqQgrVY^N$+fLOJp8YYVDU&)-eRxrkxO{6 z$_L3)I1~MXKq%1#`Db(KyDt^5=u$cb1YyXu(-OA6${Ih-1{EQ_u2&}+34)qvMiE(y zzSGAgS>=2rl~x;8cs$%XtrZ!Bc@Cic`d!4A6q(FcB*dg7;>w7$c1#%D?5)leIfyZX zCdgO*`rtFgZ7V|VZFPRlAZVBR`bzX1pj^4*hJV+LK#2QEuSFlnqt(GO8Ez%B-Z<;A zt45Z51RNeCWq!_|1!Ij6uVXiHb*nb-gIJF_N9|J}>3jp2Z_EhkG z3@l8?L|_gsaB0*?6s943wW;Kf(F0RY%pAJ+L7ADAl|&@kHNMzyN&Cf|mjZEa5L2Kq z(WaEYq<e<$g22`q(?Z27;}j|pK{^Vxp6HtrB$*$ z#HA?8MZhMRM)uQx5wd0#_&1^Bh+~)879o~iQiUKa8Vk2<8XHt%GaJR`1N>^A5=jCx zuFhf0QCKq%cx|OMiNnn0srs0GU-r`1yvRk|EPxi8{rl*cQy|;O<|?@$WLmPLWBw64 zHL=?U0^O8ZP6no;KNJTD70t-eK8mrYV>wa7ouES`3arcc}&JT5RpI>=wz;%20?R)mictDpJ}^ylkoHbpMhD*^wuB1 zXW(Zv08Po8ih|Rr66O8LZ9g*{vdw>yKV=9xBpHeYsI)WGSluKO$cn@0@ZWK$jgmPH z?Eu^`&5Qtsb-0RzWKiKRroCebFS+%B#K&EvYPa!&#WA}mzf|AVY-)j~wGm{~m6;>< z-Py7t{TNSnYj9?v=lu3ok~hSiaMoqI;7gy$OuZ{sOU=P;wo!}h^4rn_?QX$QPTD1# zQYLn_%&YCErh4TMtMA{#po_d_Z3kla3br7;9^*@Qj;K^Rsj8QYM26Rj^y!qNls1YY z1TQ8VH@tN?-E>M`*GVlZl5MJTwo+XVI{oTXINaQeVlOew3#;wIh4D5zQ7a z@qPTc7eGdGD~c}ASjK4bwyudbmi$;&G^Qz%!R{}@PC|SLN&khjoY~;LM*RHMJ)7yi zc2CUBvwftu#CfdE$#g0l{|o2x!&}QN%~g{P!N_(B>{WHDlpF{B?M8k;yNj(wSLN!m zJGIJ>6SWji;V$a>xUGeX+aOGbz1l?4M*ogHyU8Ct1?Cw>ey&E^%gbZcxYBTITsm(& zRuP<)>YvO})>r(%U%4HNfdH86${j4pQpfqb53Mz^A&X+`hx?_pbl8opGTBX?-^gAW zM`U)Zy^3BmeH*dBA5FV@S><4CqZ=i68s#X_PuObhjS8tUsA0lUX+vxpm!-=J*P%4{ zccM9>Iivxm^ru`#iX}*;JUdY@Q!o-{H)-_mDSfXqGm6dW(0khDd)wBdV#6dg#v`Mc5`)C8!4HsoCC;Kt}bmN(ay(x zLJn6Vx+wuTNQZBx&{Ltlt$;V&+_oFi7_ej0c=C2#8v4ntD?}d?oILl1!!-yL zHs0g*Je_+8Op_NR%SO8z1^z^#qw3*dLJF=FPX8E*k8$9^ zQbQ-6z-WY8)qF% zSA=JumhTGAoOQpvny)Hb#i>6lSgx+AYr9$#q2`tn)tQ=WCqP-1r;=2Znk}#@Hy{*4 z4afX(g^{F%K-(gVg~SP0@XopX8d^noHm!<&`U+TdSf3CAPnI^bF9ztfIOf$smT)R@ zOnetX@HkGuIXTnkZOId1Fr1SA*5QJ>8Bys>)wJm&N>*qru)Ies)HOMDe?;_*x< zY$1E)brbCDuUkQA)_2tB7Wr`_8h)9YIaI%YvVDac?Ln+D63@n>W4t37D6c!Q`kG;8uC67~)Ix1&a5M}*b zxA@67UTHAnD|gNCWaqr;KiQl38Ddz}ZCoJEF!m6a{HDQw0^r%u1A9vv-;X0wVD7b2 zT%=-lS!^^M;z0V@qo|4s!s^Mux~pf)b@cs}@t5y5G7VEFv&S}M(G_ZrUEMH8TpE+E zJgjyRMu9TJE|gQ@%bsPODKF=4<|xl58Cy{5xgNfb^nv#oot7c}2tzX!UD}Su?|S-c zy>PwswW!d7?qiY~|Ir50BL?R(yAJJAuuuG-j`DeqJYaYK!}5m%{S|8$+DqUBf8k<@ zoqS))zy8in(`Ok^x`53j0B|1Ih9*Sz?_7)tPnGqaY1g%_}gIMVMg z2O<*0iS?CAp1_ViwlIPNix^8`B$-2J>zMwKK&xaZCT=CcS>h~7WrQun8^C7tP140i zJ$tXz1bLOeTR%|nygw4~SY|!7P&(D#74h?^-_)8i z);z2U`OaA~3-_MuQc=L7Xs7-=;PQd*pJ*rutQ&M}JYEX#YJD>K2Ugx|>>Hd35~0>Q zX8oL!Zhw6a>4oZU&kIl`%P3QVPZA^D>R#Z6+yqT%O~{R zxE9v-OI534dwwS8l_=iSBa0%(^83-n?t80syvbtyRR!P8Zw?u zH+#xjjyy|%ch&t4JnO1m1aEu6adQ1a+&A0irbyFyemyw-R_O^CZ)uhE*9L{ zV&5Gzrx=Ry&e2Cw7BJ1%%tdz1_qQo*kJ~m8Gba|L)8L+IG4^k0cowyDiCJ+>KL19? z3$j;TyZu39rOqqjEqf9}AKX>r=1WmFn-bI)$lV~@{TT(8PgzZGoq$JokTo(eQ+CyK zrOzX5*cA`OA#ms-GG8RCcg@)9r?<5XsgO+Yl)AEzW)!vU?=jvD00{yq`&oo2%-+m_TdoI=8if4GAv+ z`fvOTBW7pdbxtUQcrFAEprcJSBwe1^Y!3>T9peagvgjNc2S5I>Ke}-9@0dxbJ1O#( z?}oe^J@6TE)lfiM7}GWC|d(JbK5&)wK2N4^P;n&xcng5&E zE82}`jy5?~UFi#pmq*R1v!*b$_2bCmm2eAz=Q+~&u~xzp*O^>y z@E~E2)x1G+D)1$ah8#A*%FWG_2zzRUc-Cw6>AVDLsZ~fbA=dxylEC0xcK8ClgPFN> z-X)OnqCTzWk zc7H|Bn?sEGGtkhZ4bL5~H9G z@**b-Ner~DnT#glQ$u^!^(7^6UKIJ<+~*w?GfM+|n!qZ57U`M2$*S|ErcNAYc2}1Q zaaUGl^m|puf= z1vBS9F1zXbY_sfrJ^m_o0+Wnd4QHGhr+`f!ncM}`|3<6zS(u*rcQ>hyEYR0w{;s3{ z?X2T2m0-T4UXlRd8l{UjiP=rVJwneSNHrRWLwe7LCaQ-5Yh6wj27gC!i$zAJ72Mi~ zLyUi_C!nZKy5pDK)2ZyKSus24|E@sU_528ND#UXM>+yW=W;(ghHfE)prFIIn@nlmk zC~_pV+8Z2q_t;$+7S4!Cloww08VGknVV zzFUd5KYDS$PjEq;en-Mr^Ofn&6pvzOL(}dLI~iQJt7%Ie$5{vaxia7WU;0o|2zY;V zxmh3mA05}(Ip#~9D zn9=GFEq8v1b}l|U0PPhWie1#(7pP@e78Rrm%;z<=$-i|^hB_4XO7wx#h`-hcP| z?u8BEh=PSQp-pF2qc}vR1ZBJ1AbAKln%M5ACB$||>q6@E!G|N_z(fcCj$BCw868M3 zRs6dfhM@G(`k%KRlcIO?uD>DVCEcViV=*|Hr^KHJc;yTEoDGds)f%#G*+A$WXlBBw z9}mZX;Ij!fSTYbe!U(_@Sj&YXx*I}Fg=awAhra=_NI{dAz9TV?IIH)^l~!QO9toAA zuH5zjom^*_hIUZp6jdKkpc=-}r2){}A_q?QvxY^*e$7nW%`R^^lV3E?c#{PvFXDdR zf;L!%L-^c!t^rBoO6+%|1oreogkV|lTk2vSI^37;h!``IM_RdmqoP@5r7;pN-(Vn7ceMcd#_%|%d*c%>s4f|pFN8GU5ASL%QN# z<}jqy+Cz|cFRNdh3&>)wpq6&p(!e^k)St5WHK*9N3P7U%(I-bQ!>8XV?NMX@!lXmW z21uL%arc;Ofj2t{n&GKf6KlQRmLL97u7)1EERdvwVY4j!n1?{!35U+GBKgITtu6sS z%9$`~wdBb$H)=C=j(d998zy zygDFJ7uy2r6u69`(t&^qsXO%m#}LSP)!mw;6k4XU{A`q?gcajQw7Yr@joICVkRwH-0UswXhGK&!<5$pgL{V1H=T3+UjbgJp|s8g#lIrS!N<33$p| zguY4tKNUvxj(hSPpw1ey?v)qU8g*kCl(A9V!4h_&GERRXxP>z0IFo6vUBsmztSA{A z2PW*#tXnX!-}s|Nm1H2depO*bRlp5WzZ3rC6SciRnjOM>aFFcL zp@>dP4oB?U7NebX#-N%rg4GAOQan-C`UFfkLBVyA#a~de3y}FUkVBYO!a4ikY~x4k z^ny$p#wH>be@Oe@CxK2ATF!g#SrVzg{}?6DLj9o>!;jtOFQWZQul(|m{Mg*ya)44R zS26hZ=nZ-2`|~|%?P>nPc&q)n%S(1};z=C!w7QFE^%new=PU#1HD9Wc1b5dZgO<9y z0IWcYH$^iJjo<(^{GTUJNN1=630zbkLMCS zZP)v{H54DOMr4wd?Y6^x8`ulI8BVW}FIGM}3|EWe`i%uh?d3944skCuEIw_rt?pz! zIWwjTD<_RPdv)|K0c>EOAy>`3NVV@-QHMCXd+w_AwwMjM3>c86DL ztwN*sDBay5 z4MW4wa1YP#-n;I4-*x`e5!bNJ-ut^h>GP5xDq74{y%U1L;LC~BWNYERkY*FRSyEa- zcurJ8v~8$>rv;+b9xmB0FaiiRB=D|Tgo zAGD*57Z{oE)9!t$IiidB%jw3eHBR|>C}m|9Quu+(=-qMrU$Ak&Q0Lxk&nWHlq|a~} zfgk7rBfZDLc(?%|q=of_z{DTUw}6FBg_pcYM#l%@o#E76Y(sqoLF%mu{c9iOwJ<(u zyTe~sjy=jPDBS-0(ghcV(93vv+A#4RzsDs9~flVU7B7}v_?FoIp zctW2OCr!5ZAFN6fCsvFI&j@Bnyp8|pbO}2G?0Conc^nmj7Mc6UHcEuqzUwV z!;K>6xjNy`yaLevv0~M&uXj1BCO7S=Wx=b$UGa1X_o9)=2#4mBXLW9UzXf(sNRkM? z2>;Fpki(fW5hMu~Ejj+Cr7YRUI?3AY=<|4sFks5cub$w-Cs2nxx-ye4%2GUhkP8ja zEKoBV;Y$7c`1>erMQ`ZapF|SnY>_6tX-C`}%p5NAlnSW{B!rF9szRW-`YeSEjVWYp zc1Y~;?hLov@)gMglFI-%)U+f@S7Aj7mDNlnIg1s|bR_tKb;Z177}-#tj|_1YoM;xy zEbm`6r-4@!uVZMf^*SQ-W6W1+sTBc9g3So-c|1af=txrZv2a>3kh~0qtU%^-C|Rf| z0a^`%*s&yco)2sdh1g^KeTti>YOBJ^kWHWh3Zm`Xee@#lVcQN~Ij@{k;4g~3;G3jV zwAuAbk>flii?#mGZfh|pKi$_=c~zIICDtJe!EpV#b4 z;!~{XsIIWTsMg?x-#cc=o}TJc(*0Qjf#ZJ^k1}-+zB%qTk@>*`x3#MI*~s?jo>Atv zNni_*!(V386)X;NVXi_Q*Pn{MNiQqOSp+{7ro8S5kbWtv_SubcNt0LuqcEF)#eV|w zIU)pOHEzcbNX_BieunqL3N_+y`hY8G01pGSs_6H1e^u&aEwSgyRK%b!d9LM*oJo`< z^*!f^=S!q-bdYpMc^#(Wglsh&S&-IufDx-hin{<(tSAlN1dF*|0?2H|ly>{!Ek z?y5eMwGWL#neA!-%_i3K4kx2N`vfXGY5zGmJNYLb%^I0PFpb>T$(=S6m&+LW?QeNS z+2CJE`*+Y8V=$S4WM)}~Yfn-|cE-wtLg8OvtXXbFoJI9CXN6+nd-e0#(4^Mb+Mm=8 zG#!7=Z?KM3Yd0G1xxx(+3L{KP97&BlmanAK?k{&ZLtL6=o?l`2;O)1@wxbGa-z#Yp z7_Ul*+`CZ1+tPm9J3R0Vj3KMR%;9)y95xtcP62-CeBH&^-nh zvCRdsokioa8eJMCGBI2KTO_V$gb+-aCE)$$LGlmT&g&QGS*KTf6qs8dwL__gA5Bh` z>fXk{jha2Uk^DeMa~6%!$Z!pD+bmf_(=Vda0;?&BszQCJGfjY(Tt$|tMfBFsB<}u+ z^%co-6;!4s3Rg(`SGvJ*Nf>S?VGksOJomtn?AHOt(Y(!{S1d)SxdalT9&`|{(Rjz& zAwI?kigr>7N(dt4t;?!}FJOQn92-LQwbt|k_RqRbkGW0JE@KX%CIR;kj3XKaXRI(r zv;?)FC_PQc@N3G&}zcd+r&WrCa`xl1>`(CS*pvK zCkJHKmZ7egdqIl=pbY{?E)@jFG^>=z-AFg}yHA8#-^bnu)Z<7{GNc#SO&3}{#aUof zcbyykx5(sJ3r|@Wp}EMjVjvW^=$(PCXVjh`iNP)gv={&wCV=y&X=r!+oE3PcEbk=` z@w14-0qf{(=SFg}2&z@B)G)VP{OTY2|L>$$t%(-YilB06oHS?UNy)=17#VQW1ls3uv*5X0E`TzP|8BJF*rd>)%CN1{y~Xq)S+l>g}I zcQRR^L8039s{h?TkZ7EdRD!mD896gGwZIq#+}W1(j24T<#iJ+oY`j6zha~!%jifiV zwmo6XGo$YUY-}E-WN6d|0v`unrxKjrR66$>mtUTDH{{2eRT_19@mS0Z2tbhpVaw>R zT`@9IGf@dsU`*{CiS=mvmc00HW~NZ`WyY*hA86cDE8eZ5N#KlX;`d%oQ4b6SHm20) z`-bjWiC=B^=(AZa>@%fx#pX;J_o|}?i1?<%-ET*V@3EVED=2w+;S-nr_51mr>#xSY zE1c|bg%w{~CwsR&d|ns%oYt!y?!cHzIop}(2T0i55VgfS#_mqDvfn0g`EMv)W-P8( zell=$Wn+2Jt)y%=zn94wamGB5YIG{EOmJB^eI4w81jbn6)n87pl19J{)s`Q1Q+o$| z4{pUbi!ch)&MI@36!75Fqj2i`QDU-_rJNe61)dp1zZv_fTzp+0ZMk4TPX4uJsK^6oxQW5ru^?_Yjx&q$pcJKCc$v zFxk>rfQfTXw$x&7ic*9!FeoIeB%yjZ* z&K&!&R3mF$0|MmInbAK2DS=X1+Yx%ovb-L3J9e~aAIu==@4JuKqTB#Z>rFQ(@UUt7 zTSU)15Ms4R+ZB#~iDc(z*l4i{KJLH(s#5dj>+Sbhf`mPG)U_}$gb+Y?ldGKmFPHDG z{avdh^bkJ*qvr&P5(MhwmkF!Ri{e1zE)_<)1UGC>$h98jlc3FL${S?V4eY&UcWA0B z2>cQY368V~29n@Zx>37-nlnlvfMn3$-_<+gk-}9dFos3Wl$*}Sm8ACni1tzpLd`{H zF?hZReEWSD{l?|ETC&WwCtF37zulUy{&Yq*SP8vq8+Qa<@0w$O*>7-pn-%Vto@BdS zeYRjd5H3dB0-f8Yq4cpxJ@9*I>ID93P3zVAWzXZRyE0?z1F9@uavhoWk zEne^>bRlF;f+^yk-}e0V)a>d~^(-^;%u9{j;{o)Vy-;GUHbu^}6 zAgD1^`)hKaSE$+x*c%B+$(rlA;Zq>!Bn4=PPIFcndgI)f@7#vu$mG|irQJh-y0o^4 z_Z%ndqn;Pc!|@=ncFOii1g9L#P#2Bjq_H9pk~Y_oQ0ELw1hM_Kct4;+oml?go3Ca7 z2~sCW2Qy($glB33_1Z=dGq5)u_Q4`XdK(q!G?MBS!+fTO`5XfxU4acLRVmB~Y#u`^ zIBTri-?m@b71B(Y@|Kk8t4fVjjbV%7EwnrxwB)Uvzw0xVV{D-j0FpCbbgz69j30D2 zOCLy5Q)6USFlQCyakNTxT-$j(Iw0pDmW1MJpjLbSty@NP*6Ut)-GnOddPg==6>$cA z+wx96CLF^|u0xD)YX)d~xy-PB=JKemYxKgxq@o`Hat9A+exV^=LK@8w+y^qDieZ+7 zQ$ih%6~A!df);t9dOhK2iyMnTPN9RuD0~4k>*|0q=}h4V@RK4$)%Y2$wz$H##n9NfK6l&hf`shE*} zi+Yvc#OPUkk6>Z3wAG_VaM$d`|MQOMZPSw2aG-ww6cXa^;&+QZddyjax;-_rt2G*w z))uc=Hp^PSjL&#(+8wM-tM|d3h#y&;N$h;Pc6*ydRs8mI4o&bxwx5820=Pb5T$?TX z2nG(ATef>arFwQi;O`$@F|wZaQ-wL$F|5+ZqAkqJwe^PcxW;uzS;XkiG?MX7E}6yW-PtY7cA!&MopK zlf_JQIYAB5o9{G4H9irs5U#V_aAg3w$QBT9V14t-s%U>(@Pr3M3*nlguFA(nOv`=x z_&QAtpu0OJke?roO5wkYKkT&$L&D$2_(AaTIHaAhxfqDMA#qO|bysjU0yJ{cQ+?l$ z;K$p73UUjEIr$0M!GQ86Tkc2RNA+pA1D}b68Kf&@46+s-ijQ6cgz#gBt#I|DRfQ8o zuD}u)ls14;z=TkIv00DcfCd%|{aU|3)wgf2tf%t;jkQC9SNe6{WG=#Y2jlGvV#RmS> zIA>3P*Q#OVs+4@?`!XDhg0k~j`Z{q$9!j%+rg8`nws`O(=0w@eIsMjE=##af;>U-r zbo!40fGo~`-EHxqwtRc*(t$y@ZTlFFoN=m#-XI zXvP7v#J`_zSJJIaJvnP7UowS8eW;qYn6Bj=+4bDN{ODq!Kg@JZ`JKo0N${+JY%zap z=@{&w-B>btc^*g3Tm10j4H6jDm{#i#6&P+7Qj2Itv0dLD_jhddhU zECA93p*hUd0Pq)Ay(i4pB?iERz(*jZzo{L#>mEoRCNrRe$!x54P)L+jV=FILNQp?Gj`g1 zI4HwZIHH~@sIuQZXT@8|YT}W+GlWTe31}EF({~)2HxiVTr-f@e{~DC8GPmH}&LJbx zoH3>4XcQ%OQB$=vK_aJLYrGk*@vJ#4Iigamn>IP9 z#=QI^(GlvFCP)GFa7p=EFbG;w^E*%V6ZAK0j5o(AyLf z0aS0}Kw_AvL&GdVl{>m4^B>ma#1Jqiy*pi+k`IaA4A;nKw2pixT-GnRvGqO_A1iv% z8YRIYi^xvqF{1lrwg0qCIisk~BbWmVQCEI<%RbkF1Zd=n&oiRU(v3zSH--j>zF0{d zmhIMi)(l%jHe-{)lN^VSP*gr^pIg1+Wzp4xWuxp{x@E8#8{JFto-)IzG@?LT#jYlm zgeY${LV?h5o7X8QnG=5!ru#g|XM(+lGX=Y^gZ~(EVW9=gN77|->Iqo<^Te%)oct!T z;vW1VC~z{bimJTYs#innlUPEG<&HHFX04Y2h@Da3pO@nAD3l1RF1L~zH~!?9q~PH$ z46GVW$mIv_+{$g#rkh&;T8Mr!gB&XkjvR=n&@ZP!+|&IWEoGA@G}hpW%CdCOC|Sq}qtRITtRQPS zqUQ0yZ-D;V>E;$08)5_7B!>NjJeN4L09>LPOHpYad>}DIqWe&ifdWrXfR&MB*Sd}&OXP=ZC`_Y`h=`G3yT2>4Stca>8rAt zj@3rZrc}Ahq+Oz&*c^J4di2<&TSoQnmKDqST;vHZv<+YW;{Fs`k4FNME%9xorLZ6U zd;qjQ{`p~4@*~G4=_T1zQ!pQDH}bQPzl+0de*_t#<*$MMuCyXw3Jjl5Zj=ndF#2bq zNHNloTbGDF{w9E!!z}H}piG`k2}mFV4&i=&kS(8KCb`A+i-_{NGU&cE?z8$fZ{{nYy4->Y z#_{IZh~rZ8VrS9(Mkv_;Z#y-i+&?zf9xr zVq{ZH17`-@3og~qq|M42ew_bxr1ZGe2yt2b@=vOJTsH6-F$%uBIx5H9UzU-T168Sn zMwa*XHT&$OWc6OrNO0}x{ixQ8PHSkZkg8nFLKZON7ZU2V+ClH?te|fzImiQ2L;-;Y zgDE~2zL=$d3obd|phez{UKf^DB;MNZ{A z(n*e=BN!P%D&pp_pXZi~uZzshVvc^!#%I#InK)9aQC_7{;(FP}lFVeTbB}QYts3;_ zUvvq5KAZhU@M_YwJ2GLO@%zN+enaCNNa_7{;&7$YREf4Mh5}!*sXF7`-f^W=YX7Bp za8J^YbFH~O^O~_0o)klmS51pbH?sWUR4mFfo!1WMZ#9IX9j?dlkaLk%Ux1#0Xc+?4 zc8*7NcaG5Saofo#+8tkG;|Mtfb_txwA;rP@uIIf0D8}M{pll}3N4KfIU4g%Ywi-t- zH_e=Rvvf~%(c@%j_|H4+On@T$oS|v>+4A3|x88?_XKiZ%sYW#9C z7x}Ay0Fly^VWrWNUT-M$o*o3=-B6^4;8yRWtt4lL5~F@84+iJypZ{tvlc}OcUnl?@ z*H&x&lzq+k>@@@|oX5IN9CZOCR$lMZgd~dH!fQ_tTaHL-y)~)Tnf-3*3GZQ?IUCE^ zeeyG^#}<`Yj)!GP)soy8vXaZ0;S7pz(EmOUl>)hL?H}?&=t`3ajCOBj_dk z5O)}LkQkK_RrtfCUzl%#C*kjTTNnh%%H+`$OuuTAR<61a=`a)9&OptelO%#4Q6oE? zLWyy9N)Ha$*Z&-X0WgSsAwM7{VER<}pe|7`Dzh3>N2et2UuLlkU=}_9Ai*`rWu9q# z)@wCA@75bW3r0beJ|`v#YtjVpo|`clZlV`NgxDal2Mu_Yk7+e#HArSH0n==8ql-)i zzuQ%8RpA7$y4RF;2stsSTtIL~&XzGiJ@pw$FPE+u!-W9=l|!HjhpKMdiKy`Sju>UH z$JxJnbL{~~b)oMi42SIAZ8kuiQ|71aGY0*wqs12L>_4=8apCwHvvMjhS7Pg}UuPF@ zc-8TzV15)@k-X*kV*=d%-)5eS<^}!*)te*ryY-1frJkPZp}qP#HOH#<1uM~wIMw3M zljuxDLN474#q#!au|5p6F)`7JkVFZ6B?#QFW$C82?7U{jajB$1#K&J)93{IjG10pH z1M|H}8e^mmaqqIx-oVZoA1S`~_JmZbp%k5s8HZW0L0SXiaonp0gzZzZk>Y z75%Nd)zA<9aiNhTmWf+sJT%Whgu->@bZS}SlFfW-NJj3ji7bI>7R!Qv)C zr}TEePGSDixcu2+`o`7Qp4vf4slY3z`Dprv^~={Edaw`RTGq$mUoU+7tByxVp79tzUqzrP?!fi!r(BnSaHd?>7AQI;E*9Dq!<-I#UuNY_+Y=U2TCE}lI!}69zvDNw-K@YjoU>u3JKbvA|SvezGet% z3);@|=2NBJDFpK8Z7FJdSxJ8y9_Aw>R&7zJT}>qsA}G)Iz0R+ns29uouDh@bDkk)b z3C5^=0LtSJuQEW5uJkgB`eD}|@vYr8YPG4mTOy^!a8Z-<-*X~*P@r~i=B(NJ(@k`U|tHGTn`Ub4o3=Go|1FhN+YgOyL!18bcR`EG{WDemht4**m!#Nke<1by@jylU zJN@%6AKQYNjLMVnQyE$HSAtEz4UHiG)*ybn($yWyVsoC=ZU1jFz<=n>>_e@q`(+zd zPK94D)&3n8#B{(RGz~CVn6NIGz;q;bORFAk8N3+J({`nFv^8k~TJTKT67>!8!q400YH?d9SB?qfD9rph70^#M64kzyy@NP7{Js37nz16-zF$NC zWMdUqPQ5?CTkAL;I1St^Nt~$eNuM|mc#eqC5NjT?Ft)_IJL#d0j!8zehWEm-m^r@| z;s3G#><-#ZPgP!f5MEC}cwt5!GW`Cd5R59hW-Rn}5Zjvv4Q|P*+dQW10eT(p9_JHF z35HiQ3aSF?p6Dzza$5V>@Hc~5pmhfqb%hc80xv8|q7e-9-)(b1Xo9GN1BHkINaQ3I z!{_q>I#f)8eL*hUI@*uIoU$NTJ-}+yp$AHu&_uiF4c`UHR$o_J6_z2KAl9h*u!o+b zXF)$!YbC9^Pu6=fq*Z3g9s6H2cFZT4W|yt$t%TC8-GY!QhFgc7@qpV_pDX-(6!}ME ztC+v~))k)tG1c|*YGbg-F)gU|W!)5;DSig*-Hf66)h@NfLT`j`$_m-gMOqb&4{ryL{y(kiX)xV=guLKH`+?6?Emp?y(!Q}0L^H@F2s0MT-}e#5 zOv*y-rN`K*fT0XYW=xlI43X=ku82PGteGnIRLJOlw~>e4><6mu=-pN6t!u6$Wd7>~Q6wXM~xWhAu2WjhDqiLV? zJDLjjhZ^LdH8;!$9b_HEiVC=f) zcQOY>6eQegeWzFdu&=3&PDcSEVFq5JO@klw%jby)AH?yTZ-r(#+0)T)^WlKDNRwry zhq#K=J4qaWmji;x-CVvctGuiLZw*XR4BeKT71UcG{=TcR(B8Z65D$aquPK4@Sm0R~=~$RbVn6&b?Hj|}0!X_SJ8nKHv^ zOdqMX#MUqvAzw)F#`aU~GgIfNX6f=zTsFuyO^A7W++Tkv-rPxF^lrv+6)pNc^@uSQeE&H;-|qK^ zs57B)Y_bx_v$T0!f7^(B$+}p~Lwd7&TZu^y=CwpmTO5ti6^!SRfj(>}HvS=C+Bb7e5@>1i=pyIT|Yok>3rfaBt z{~(H_3l)vx!bx3`*NtcsRG1oxy8e|GweY|aC?~@CgT4DbPO11|no5fg zxHKN+BL;XCLEnLjgl`ghev8XiC52zcZJ9@uftKnbb{?2_0!+PulTqjuxv~;?kDxUS zq!do&XG1)oDR}5X$-*7JTQjiU%*&Exm?>EP(>v-OV#QO5{CYbJIhSmSrjO%doIz|20dIQc2GZNQ zl)mL9RYKV@tK^m(&R4yt(~%MI26!sH?jD!PVeN<*I^>Hg#gcd2u_%VWf{QfnIod3> zDZv@*{Vege@6e>G*%|(V^-|ZZaSw(%rjdZ8s^jZOvbI|@KX8k&@0|D1=Z!;DL}bw2 zI(3cD$;Ymw?G&09AhjvR;a9<)-0hed@G_6tmGs+?BsQlmbn;qbw9Ah9`O zZi*?@Y<39DVESa1=DM8CESwHx%H_kj{i3SSbHcH2GCVc}cd)xV62}+A2A1F<0=oot zGe$%Z9ue};puE)iiZVOh{KC`VI|z(drmI0+JJ>^s0Z92lQm+X}xi0|F8Hv>A--xI~By#KZY2wC0&QF@2M>2^qb5HJjo`ujR2o95e^W@ABlMgM4 z{~_UsL25X69A&5VuU1L}fO^*fDlCCIeceV0aK1R${t93U?q@&(JtK`yqdihJ{2My+ z>z^9@i!&YqBR5m*VqM<(j_c@?$jzE~-c5{=^rc&!b4=L6YI{RcqPOiwnE%5U*l=vE zj^EsQK_6*5=VgZ*h!(N)eq0u#cil>+SHsa{v4WAbz=>F1{5e3Yz zh8>4;ei_86mU>iBd5jbWL5-bni^mZzxj@%X*|{y|Bzd*k!_-6}BSH7?J;|f~_OaW05HDKv*bD*`5X667@1A&eM&@^@Irkc z#d61j@dqAUgwkC3Mb}Tww%ASN*$FnV{~&Uc?YRRAoa1+rQna1+#?AqG#+xe8(GQ!G zeq z1=`fJIq685$Ix-AKYXf8Ueacikxtwd$v;OrRuw6Y@24AL&D7j`2{kjy>i`|L86XUvD7)vY@Z4LW4Y6y zicB7fx_HCqS?C$ajt>zHKwRO`jiS!cK2!gZE^yuo{S&B_uxt<1M21nf*)yEf`mP%) z$NAMbTHJ^_oGJK;8)V6juQPwiS?&?INla*iJ~1>u+HsBX&HEbz`wz8h>*BuT8#Fp0 zia*|_@7O9Puto8%={nF5piw}t@BhY8Te({*?>K|9SG&@a9vD9gFi?3Ng(O7)x~92V zWkq5iw9@C>l!?0x+&TH+%H-d%2vo}ncx~%K0^?l{k{l4FyD-v8+=w5z?z*z(x>fY+ z9)16bMnCPfCUfCM5<}X|@|w4eH*#%%XNDJexSO5zYs8+<>uV9xj|qAhlH`lD%dQAh z4C;XMc+g@b7S+q?in16$BVUs+vx*H8xgKIOqeF2+JoKI5?O+V(R_Gh6llbU5w4MD|GAu(v~3Oa~6Qjh!H}cPUcdkf&2zr{TEQ4=-0cFFm?)bu5i4AE#D!| z?%D!iiBuY(_X?Yxeum#QW_~-8hF0omv?T>3tB2gfc;ipHe&@Buq~amp)AKz1lkH0# zBrpCT%HG;g)=y%RYKAXaQ213Oh$WZaE+FfnpKV0{l2XI>Fux_CCKY0^Qg9eNT)Bx^ zrHfFLyQT`BuuVF;O+45MyjsJ!URZgsdcda^6EWc1Y@?j3eCDrZ?7@F%6z69FM0`}% zFE!ULK4zZsNkG`1`j+5L+u7gea!;pEWPhhQyG!t06~z(!(kb7eX?&so2JqFDE1Y-T zmj7s7cuLk#qY!dKenE&6Y`RA;@inxZ=D7q#lpL@GK$RBZ=%5*(b%78i^O6-Asw*vy7lW$bP6?FXaj(A0QXKE~*BQ4%ZFJ4GoQf7wA=&fQCNf?KG>+ zXhvbC+gFk$FS-PYm!{5i&HmoSs>m-?k)K{zr}Y1QVOQz@?-N&sdTh(sWlVg0Q-AcB zlt-2MQ7EACajV8V^`;LG=E}wkWcX;WUKsr==H2?_?_g9nyW!OG1btcRz5hE72yhpd zJTfy;SyNCWMw_3kCk~dWYnaDVAvYK{ou}ph&wGeM!KG1 z9V15d$$SF%BQK4Kid!S{FuK@tAj0*e(?0J50Bx8>?2k zvN*R;YiZoFw+?%(Yjta^M+g~fwg_xX^_U236$KiFi6%RoH@qG(VJNRWJIS)CTuFXx zW{CW07uvn7HZTyicCXZEBpwt~ma%!?V9=1rs*+Jy+3xj@Ab#Ss^h05`f311f4mmul zLozDjkijw|1T+0bFRIdWUWrYOvE{YCpl5BsAe*81Imjh#Jw4P8Xl8md@OI9hQ8|rf zsl}UfOWo>HySLk+*MX8>s%{Cdhj-|$#`v##XCNg(AOq8udUqa{0~_gWQA*{|7vAjN z#g>!L#QoB}0(gCU+s?m{dAA+0+j}0isPz5_yuYbN-km#0nD^daNTS!Gi3VV=WbBai z$=b~;0Za?F;M!rK0SlVfk$(X)BbzrgF?MHRB!Ks5n9d#5#=zH1V+)+b943TD*!V1(`@2Y_n&t;8wq2!^yOt=^tb{| z1uE5@N8n;fh4FV(#KMnY4~GvEeO(H$f1lhWdM@kT4^P_3{7Hior{Q^MagZs1q@Ui+ zL~cyAQRW*&&I5S@BpjS^qG2it3hp{jxP!&r<`Pf;tZq~~TaqyF_8X#}Lv2dQT+j=yhx9!6 zJtIW4(w+P&30Fd~$N6k6EoCmGx@KStlq@p7UaV!SR`#yg`Cxg80IQYKmJAQAkuB=wRwB2Y`TkFY+oC?%qeRZ)@CK9mh z9K}_(-7H70W`4ldE!ENXgSTcGy4nWWF7~24_xtIu;{!`At!P(TzIBNb9L@q(U0#i{4Ehj%V8RG|E=*@zV- z;Qx1)o!&&Hxr?Wh`*!fuOW6Xii3IiH)BV^tap^!i1?-RCZ0;w`a3y4MD;j@Bt)!n{ z061y}SP6v00Ev0BHs0-gp-Nx6-<&K%m+!{%0;e?hs{Ot4&Pst-Q|u^~e5#uvSSFIBFJ6v{C6bgg#X2XT zp%X&>cQGGmOvO410W^<&k(_PJ22M?unO44)>%eW9V7~&9y3UESG$jh z>MjG8a5A7XrLSr)r)*VtM{-2`(cc%aXejGUwLjh;z1aG?2>#x=$jYzI6r9PR4qO@y z+0jQ28WuRVvAuCvE1vwYf0u>%Pk~wG-*Ktse@~z1y1<{1>T1ed0H2X3aT@v0N_|VO zP6Yz9&6Fh?d*8h_xob0gHC@3NwDP?gzFeLCwp>@Gd*lO)cbmAb6{mvCdSJOTlb}!O zdHkl5lv73Q20hm_3VdyU*1Own#H6HmU6Yr@M(vup#ip8dX|Yi{XXD{u+;rW`hg>-C z*f*QYh7w8fRhIa%uS^?hoPbTLJ_0dWb@ur<0 zkzebTYL0=5baF4RFwva$#^-Dwf_>!H{7 zy_=+qrVu~jgXXNd{m~EILHrtw4+4Nkf}H34(c%*7^@Ei6K{q={tK0uZrG(l&13EDG+;$P-T@H3?7}7TVO?b?@gcj`yV`DPwPf5R}*7mEKLk7oMf za2#*HsT=6aMJxY6qhR8wTV)+E4fLJ)@(cZp{u=#ycW+yl33nhW)F(`Iv;8z5!*XiE z8^V>i7qrVVHSduWA|4oDzvBP*K6hH7Jdq=AUM{mu@^;hPn9+VYNTp=*6q!`Jv;zLs zao@;4+~0d#?17tN*(BgXd;NU)mE@QDe~3+;0Y#PDs3Zm^1O(9p!iRF++D{J(eCezN z;#W_BCfpf^I3 zA^P|OHI@UdgFkqc(GBo`qE6k__?Q$9Bi5&G0Y1n1D_BR-4FE&798!|T$lg#HY-wxe=n1Q3$S!v3L-OUhK`cWp0} z;g$+E2=Jg>r`J=iO!b~~yqXE`?>__CHy0LyVyehZ3Lw+_bn9%}HcDq8K5H>8Id8gk z0{F3fO+d`y;H&SgI75I!+xsIt3c0rVz<#$T>!G<-B;Sc5Ss#E94_DgTF)5xp65fAP zVyL~M4X1d$BYf>uDi3HIUhFzGRSRKmFa~2`%nls58SVGeJRR9M-eX2x-Sq#S-rfsH z2-fZZw>%F(;r@Rgc^%}^X+HwAoE%mlNs=bXHL-bWqnqx-fNcvOrsxr#jv;1LMx&~Z zUz>jyNqK?-nL+u8=%6Z>Bm5y^uGdaJh^CHEOFXH%5SIQ^xk;8PQ!*q9G zM(v~rF=NVY(wMTAImrly-ZmOhng9W-EkS0HvBxmwxH4b z2GVs0j6H|CUy$L^K=nF=yH^5rhelWP-}}l}JxlDg(|H!MnMa3f*iS%6gn60|@4|M2 zTTe#`x@}dH_V>SF+Z?waPF)6&9E`~&;X7B~j)CTx8g2LM5hZU$PtJubHH~|cnEOto znSNA%&$R!^0w6W-?wgu<$T^?)=ARgB%U->r_TLfoJ+CIU%qVY+TWd5vL}JjK`;*FH z#IbGSQh)c>tma(wjM{BpLGE^47@iiWS?I;?yQYBu{-1WL(Y4X_8-;tni;X6^uxlPa zc)IRpa8FLEZq6^Q4{zB9t&BUb$Xq6U?%mC-W+`Pe@* zma|%G(#`-D@*?Ge?ncpFycRz1ZgDU}O(v_o_b#b~eL=Bf<3zLkOh@x^%UViUuid}+ zjbLqKnMsr39v%hbJ1@5Wu*R*f8O!TqNzHs?6x-ufVa*hMpJUrexMG&(8IXB zmUC9GVU8Vd)O2qo38aS*&?mJtt`5-rI1=KS9Za}Ht57v?T6Z4BiLUFFNN=Di3aW!T zsb^)=)+<3^&YX>==Q$lyWqCHceg#ydQy_1X3Sa1nNbBwJ9>Ynj@X`5Gaj zCXLjemhGHc{(?6lvjHaPeU6~JRk0Q2(B+Gf4E_;N-@t2-Mzg&;pr7*iH;B@6DP1-2 zF!M>;a!6>(KH}X0#5-gN*Sh`w{(~_Du-&?yKX3XD2s-tFlQ`8HNf^ zyV19)ghO`wfnqB4O8xs|o3j_IHH*(DQ(;ecUViX2_ z%g-39b*5X9YSOBlK4kwsJ|-8z_widzFE~3DJmP+Fygetwyc|J|=e-MDX4R3;bbZcl zKk&Ejmq#ME?pznK&zXa0Ks;9ft=)=a$9`V|n3HN~kf!NfwxE~G`!ge6K*)es7*rkh zp7xny@cVfaMoX>KFsw37Mvcu*v0{=&>UR8BEGvXpciz9jZUa-7y~8a>H2jOMwpiS< z;Mi#}ER`rmK+Os~cZ!ZHsEa3Jk@LE(06qC@D2AZO_4dlfc_K!?RmQtZp4BCmsa71x zNi8g6e6r&hftpH;<+};U1t33ZdEI8*5-WbKiEiwd=P>U<R=N9oNQh^R+|l~R zv6SCH{^OywUf1P#sZ(OC_^qOt|HQzzjpV|SH1?jXhak7rHgN9?6j6y!qQ)O=1&6Le z)~9$3zRg+cz3LCHtdSqkWy{|M25v1{nWy!str-UW*-n~j>|20QQcCv@A*O71xTvHP zz@@3P%qv~u*j7K;enNKCShaS!_l-Y0{slgM@}7YMlV@en_-*-n79Hog_SobQ7NVc3 zIUSnoF!QfjCPJ&vSki>%P*B8}ri45>r-!%lRtTPukUlPq=aG+q50eYRo_R~{yFma< z*8%@fqb^Ie#`bP0>gZE5Y}K`gl1VBY?_#95RcU>>00qvS`zMk# z!*#tX%Kt>PpZ`f$f?0S@x}PssYIjZ2w0`t2s-j*&FHu$R%7&H6vCzV1*6d&n$iHJt z$)lcDBa1u?N~$Q?TY=3K?DljFvdQsx;r{v00L=500LI#F1nA?wy5=l0KbdF#-zOm+ zHvVqk@(y_fWd>V7+#xJEc~Cg%6pI7Sgm3BTr@W=eXUuV?`xOc9sNMK=veL@1g8zfr z8>0i!k+atDddaIZ_-kV4y4bK57p7n=mf&luTq+n~KGtgFG=L2NUC?$lgz-q*eM~uT z5N`l=3P?H*a~JqMslDx@f`~q<96&4X0%TukTjA=+7X(J}Toe^t+y`PU?42lIYpwvK zrF$npT1wrJ1umiR>}`&@9eytC*XW`C2Rq0gF8;8JeR_p5G9Vd_NRZUkBZDM4)2g*B zO-VVWdM`id-8}9C3AMkZ1TC5OZUCD{+EllZgSS6l=h|L$)TDkN^^_c*0? z<3c}2Xns4fPcAw^AGtl<{B$~{_yN(Nn0}m}K7D!1vXS4DcLi8FSkreL0S;ABxNG@l zs`RJg(1$iI-S*p>vR~kc_eVALlV>2XnKZg6&+e3%D`Zk=FABG$%pXCee{l!ZbboAD z%Bl?~7H+@#xny}?!!Uk4hW}HM?uegS@`ke7@4~KFBQFdo*j{^?a~A{#eQ)+Ya;jNq z1&YoY_cIzSZ!cy{GT?p}tz}uD?j+(#8Cij41yJcYQi<5rDvIDi-NYvxp_aquQlvOE zBrv|%Rqk%=pjS9W!nep>wOd7uS>ek?_st>xl_V`yL76Zd77jT|Uj0o_3$+Uj$!#Uz za(4~4;l{}d=V=Ny1g@y@z<#*|$j)1;d2q%FXs(T+e(B1tzz4H{K357Hv|RjSb4kfy zq!$e~W1Z9{1p!C`^zKypyPY@9+u7ia6a%ODWhfZCMpx4}wfl)3?MZw4UyOYPSd`z^ zy_A$9p@eixD~fc3lyuk7A|)Ne(4usgw4jJccY_GhA>G{!T|@If{O-Nq{|4Xp+{bwY zXVAy@efQaC?X}llhv1ngR+aZD!%gqRXxO0Y9_-X>fz12H4}G7(_d^;xESkupcb%`! zx{JETTHiIk@_eOr@vjvW$$?pg5Nn`=pYUPEDq`~dgq+rKyi-#^zWEjR>ThIb*8h}~ z|4e26?fb|S)YtL#%uL+VQP?BxaO@Fxp`SV3XcHmv?Uc+{>bmL9X2;Bud^04ko6)U0j=^Q`uEwf((H};&bzx}F4RBofB~!+ zB8Z#O`)+Qq)oeY_vC#s>T_vJ(A4!EQl=;Atg^pBYxiZW$J8U71BTvc|2+7gw7WOd@ zVOJIj9TBtg(M{qUk)Fr1)94LMJFYVq(glqRIOA6wx*awMeQjgm(mHF8>x*-!O09wo z6xwo0p_S=Uy!SE8d#WmZDaElcQX^ldJ*00s?y=FFJC5K2Yht5elz_vm(t5-4%C}xL zA_ESS24cnsQ}*T7wZnw_M^%(rvp&=VwW|dV&2dDXlRKU?3Hs0V3LZP9k(FG}WOxjA zhBdjzeyv+qlsQ$=fM%0Fc!c5*#0onCbsTDZ8Z+p9`P4U*f(<$18WkELae~N{rO7$C zTV!rFhP~M6)J~$Z+pLuXyER;K}?u}z;!A)N#_qy zE2O@kemQ_LuO4?KcNNR*4DWD%uw-UnTeA}weFx@*5s4!*4R#MU6m*;yCVzxS4ZQ0G z4*!Ug`b<3>y78QrIocRTizJ=P1(-@XnQ$_?dIy+2#ba~SDc$Ks+Q&vde(1yvDtTUD z26A~kp$T*ecBDV~X3Rl1zj3qi`pQcewps%(hyf0@V?Kj-az5bgyx|;2_8=2%*H`nyO=lSRN%70%L*@=NjFM+B-ddp7(@-Kwbmarst=pQbfk#E06~H%ll? z3mG*owLxMEW<=|CcPh))1K-Z4K5Qqgmc`|-TIz}V&AQ16=7;wJ2xrw_{qb+h=U;z+ zq=ifXaF?W&YFhmlfj=`}AHln?Js{B`gvrTHtA+T9rN;GYM;C7@->NMMB$JMI_VaB( zUHbHvs~WeN0o`AV+MdU44xPW4Y3bQig0yB%Z?u1YM zDFiH$1?0>!-w}?f63gd`0Td5Ly2|b~i>Wj`ykvaZ=2g&m=yD2fDKKUUKjz#-Yr7sE z+gYAng|M;dawV7>)awS|W0KXHjD6W(IIwNEzZQ7i3->O`(Hw9CsfwW5xZQ622aE{1 zdj$_E2>bY9)b&>mQRkAub!5iqh3@gzcys7tM9%Xp+v}zh!};`qI&Ff)=dQkgL9vqp zt1RCHJorxbg&d#jTNcC&U(2e=V;v9fC$v6dodI=6?xCMiE>94#iyubywdlRZA2)Ut zt;%sYWCLmwYYfia2IGxG`rT1fd3ez%(^ST!V!KwO}squY^Sq?6kWRfSpVyl z4afQFXipNeTraQIk0C2VA1}1oh$z3DnEO73I<-Any-e5fmXn*t8`uG9lBkSMnOK}F#>;5C{Id+qZc{67aNO@v&5m6nI> z=-%IBJRN@*e*@D{(23mRz4g2k`R{kJ`CZ$}vq;q;m~D@4pc8Wz>ILFgNvvN7Xf-U` zxDWyT6ElEa*pEW^Zz`OdUy-q>;lz8cOj-t1h#l7mwL%>>riySyKYXp)0+F)@$8fb= z6?ur@#cxzzevN-!KmYjCvH`NHMnMcOV})&DJmOwD!g-C> zr)GofdESdSX^YP5M`#s29d%2Y#Q|4U4(@3v4&fdYEHJ?|^S*V>0m0ETkL=06%w3z# zBduV|2`uTaC(U&X+DN8WIaO%-;$8Kop7mj|B}BT_$%k0k2Q9~g-HDUEe`eM%J?%x!+JW$$vUaF%PG@UPp(Q=*U-3| zsy4TI4T5w|N=ec(-8pfHZ9$1qMF zYvaag{Jzo_<@HHGhtJ&zKn3&{FoQYfgnM9g>rVf6!}&X2;Ag)CO42UkB6Qp$Ffb>y z1MCtzQ+OR;ll&p`eDQ8-OxSkk$z0fLZy$nY3`5d8U^rn?KFxiDNtK@5a0k%(Q8xy! zht+TUX;AK>K5W0Z23Qfh1m6E8U|#ZAa1GFTQC;k{uWwv5P{LoJqj}`Nwp+|1`>=-x8jA5GC8_7 zG3L8F?WMM}4Ghms#&l%hjMdI0$1h{Q4H^`68a74Ez0BFEeSK=GBfL@5g`IBQ7G=X9 zCsnW&vk3{ zq9s{%C-vyDJGt5K?7XvKIL^D6$BL1*@n+d55pGOX%>epo-9Wz}+k)B`U=O?jl2pE4 zJ1H!K3B8d`wGd|V#}yn8#2rj9q~|JsH+FHQ8*t>qE2GbkOEXXIZd7Of3w7}i8bib^ z?&pBt5$7n%7TfU!rzW(4v#N`@a*`{kYJNGwOLaz1XqrG4#SH3TXaL)Uo8e^?PizDb13d-qa>|}^Szj*?yH0@ z?QAky^Hh(}S=;ZSG)wrc+?E$!`Yr-qryGp=*H^@OaC?&DNLlwE?FR|zMz$3RMt$CE zb?{}9Cm%|{pAYi?*7Td$@a`ZvfBX}2nM<+Hxsb(aGO?(OZMbuG@YOQp<-kvrO_Kcy zqV7*LxoNdhBOJOkJ;7K!2R|Q6)>ZXFaY&cUmWlzQ@wkS*s$c! zI_8#Kmyzxr7@!N-?w*yvQ@zgC_rakvT|m~v0t}(b6caC@2P>G`$ftslmxaI$d&Lcm zI~WT3IN|=R0oDr1-R`lCCkxmaeP&(pLHFx`4VVJ|mg&fyq5HtAx@wy?LSP`J_Z!Eu zT&qYcvdoN7C5#CwfQ{z3r79 ziGOg&&^ia!vQf{@3dSy(iNa1=Od&JsHodld6Lt;{PIB}kZ4R>ug+krR>I%iA zi2ab_<^5E4{*Rxra=~)Vpt?%0e~wgI>~VeKN*H=%-q8>$7JMNtD?_KkWFd;3`9zLB zS`cNR>jzovI2DX6YZLQs;y0&_2*;V#L@?CuxRNrSw0y3ox$2p|)v&XA zkj86Xkb8Qzkxm|_fEYY0jZCw5vhmd${Bk9-57kW`;d7c2SwyXYtlBqz;|kxr<`7OE z{Nm-xkh7j$9zL{G)_(w_j@zvYLv_GF!a89vEIwMXILmOtV!LA)(L9oKY zoqkq1BAV!swYwF+#pBL{Y}S>~8YSLl2OHFVYP}f(frkZE`&Y!*&Lj@5K80Or=>gH! zDT-E)WP!sya|2mdqRoy^{0WTZhPHnWs-#e18iV%T_09w^UL5k8V|C;f#7aZ3{|PFq z!6lQ+)=YK^B8jU8&DlzyMOUwf)SIJDaJUc|8nT$yCa$NQ*-OKH)f!bkP?ulXx?Z#@ zwgl(m_x8$KD$4HLaw>D=o62&AU>_RaLoMVoJ6rILmPRA(A4GU?o zdbDD!AtP0zl-|#r)PGZ_{M;eG+!Zw+@1YUZh!e%`V?gM=_Z%a$dlZ=#2yo}VU zxt^0RkSJh5!L8r4I?dD;YwifL-bqVpQr3gkzjsf`h)E2YDJ zf_O_EB+en>;jO+X=&{6VQ+T-=`9H?uo+yiPbQH0WA9LPgLy5VA8ToH7k$47?CP=)O z0BblZ2-gx}Utxuj`8GTrob`)$X`G15cT_iT<39TCx zo?rPCC~NfWe?QuIaNgY9>>1q&o?gc;LC}#Kfynx!nU#F|>9WY~z#`LMlS1_!ytZuv z_J{c9vn?*$a^h){2MH$AKlA|%scqT#ZG>_H+9*V`X@Q^A zu_Lo(ST@sy#|QDbRw4`)r`B;O1irWE19&8ZZv~hJ*%r-ucjslVY{v5Ma{hMMgse)< zK(p4iIbPad<7^cuqOSwC>tdtkyZ6X=a216w%N`SqR;7Di@ftRzJx{<|a_h%&qIvkR z>2qn&n%zQ6&<}>3o_f!-_3X8gd{T+-3gh2c{{H@-=o?UBB~r(oh;miyZ`?am)davk zQEdL#hheCh9zR}iyDb>QcejX!3nHf81N|2ZLJ0bDKqUs=9z`3u%5j?2JCG%pt92$x zhD5}s;}3WSfpNw@S^?gu}0x>KSPV-s+SazimdAh*+Zd{1}>h}Y)(dIxc+mSv; zJApIQ>7x?3xasu&5w5)PBC>u&cmj31zpy(mrKX%MhrUPAgme>brs6iGWyS6@f5Lwh zH(VCkIgFiW+oI`0K`y>3KT_$~S=9)Y#~J^6fJ(gjr|C;zHSzmSb!Hkm9%7RKh31cn z4HA6~se6XWwFHZ@I2IZgsB&t53gi{ZDpF2fg?VCz*%lVI>SL=ET#u&K_UE5>JyHLG zs^P)YRo;_-MVQaGc$ny@_0$!)puisAQmSyHio=+r>o>U_ukLTlRcp;giu;mbo55r0jiOf+w2|5C??+(I`;8P=I1112 z^gBw?9=1*@8~z+JJ=lgjU++)+4y;HK5HF|&hZWcF93f@XjZFd-;C2n1UG2LP(v6L7 z8_yAZv<-|dQon=7F{`Us31A*e95VVf78O190BmnyaIi|R3ylu}Q6sQc%c&=!9*a2v zRF*T8{4L&O*Pjo!Pjt$>GTCTd_-|MB^CBB#uX*({i{8WOVq z67$mi@a=C=i4&UEouM3pk2vH>rs+;JG&FVxJ>0o_K>KT-XrvDgmd}-%o|Br-v2h-FB`QfJ7)@t*{U;BR>F7fe^v~|Q-Mz5pm z@=8_lYeBUqYDgJPLDxx!;k?#_LekbAWLIGqM6;BaNbsvOHFcdzHS*B6kr&=4rt>C6 zFaCWc{PWsq-$V7lgh)tGd3SU8eQ=$DqVJ=xUcIpOs|PlF%>BMq+YgykTGEw-MlXQ) z+!BHcdu5%4rsSSo*??{oHC$u6D(Jbv z%2&Srp{k0fuF>J+@483{Cob~Q8n3u2>F($06~rC5z|}V^(a_0s~{*b~E0^ zx6?f&9bpq`s0%VC$hlVOrk|Rr6U4{IFIxFhU6tvk9O3tq=q#dXr}ny~1g&t$_j=PDa= zdRDM~0nRmI*CYJVbs_TO7nnPvn-`~X|H>@8ajJ~E0BOxP6F)8j#V*0oA& zd+@S-mTm7E38GVqfvFtT?%0Nc!5%e4BJZc?Vf)xPzDG2zm96+Ze>CRzFbWgqaXg#u zU3<1%-AeYQ8P(eBtMj<#@mMBRD#U<5P>tzD5M!FsLLs2A=DHJ~%UaXJObg$ESUv}k zd#{}~;`@#cUfXg{vSt=R)C}lp{g7~2z+auYxcT8D?iIG@{B zk#QOGAj1*SriZ=7r$-JS-f)FE+_^gOq=c*;JfyJnR5WJQ{yI9T36IXZa%0gt7`1%H&R(>8G+!FZWvfmQT!SjK!?Z8t`By)p*NQJ(pKHDJhqMb9 zLMDL2@4D60x*TL2T~6B2^2dH~w{@(kYPoenX8*f<4P;%qSYr@+kCw!#0Q;+LTPz!2 zLG=T=@VvN{d>pG392mPq1Elm|HGy!`S&-1oi$Tve$p7_v2rkFOd{H*96{GOF!NlAA zMBaPUj|#*xMm2xX_nx+=A!xV%AU#|Yq<}uIczU3`=uUHd-WOH*dl3UJKs64{yz45l zNy(3Ud~z^$Bx_sVHz+6TC%AQ@QTA4u9@mj?P^RwFC*tK3m-%04d3x3O<(9)L7Ii^2 zBZUSJ@B-BfA>^xl>1}}EjAKbs%}c}c6Vz7(KSRZ}_N@^Jw)L^_JgT|zT02PUx$LXq zvyH+94QG~lTE${)1X21*O1LelyJ@hzJ{bH8%pHmYqs$opL@p~rAP)Bd-cp!5jXMsa zv3+N)VWf+HGQ@GICjjx3Y#OmdAT^U)P=iJK(pst5Ow`grK3zy3lV#^`P@F@CV%>VS zLI0ASy5Rjj>_dup(wp%jBXL%(B9X8TxmN;{v8b?p4~ak~zc!$FGg)en`j!@2lL6pE zl-aHDVd4fpO!Nf|{|!FS?zZJjk2%>rcS#!y`a%>=%}&kUmiml^@(JA&R5PsjSNSZK z85UU#Ro~o>^7PoPX#WF{{QorUt0(s}d2Sgr^C{4fXGD0XoyyvO>K8Gx^MXMnJX6TZ zz(kqh&e!E4^CWJhSje2s3`HgTtH8KvREUbL+K?m|HKF_@=uCaQb4f}s7x(kxcVVX8 zyjX!_wf%|3xvg8qx!pr%J=`vhUNK)}qYnRfC6c9MM)?sTVWkpJ%k_{klP4Dt!mVZ8 zgj51`0^bzL!|@j~&C6BeyJj6ZLVx-;UWx9k^3O+&Ad<9nlw9Y<9!@wFNl?VG7@k=* z-@6{2al_u>FbJWyRrsYo|7iGxCFafI7cSDh{z+9lNJd^~2ISB&4*e~+dg=7&5$sX> zx;&qPtmgyhYn*YoEg3=}yvk{8(5}wl@1Y$Mc$ zc2|k~QFhxe_H}>N)*&jM!`VGci2EJesH)y#K1?e`!jN|wsk=C3mXIe8{p@k7Y&Rw5 z?{AP=WjlQynD$(!=TTAxzA$&L<;s+;3jU^~l++h67!?>8_!IafeE@Y2a{^xwZEq%U z1Zm5Z3{e6{wOh~Bjq{8`37k79I+*spR%5T3vI?TXqHXsUe#^E$A|(w0wOj^>^5|DV z%ka^{Ld)4I`!A2N4<#jn>tTpiV4Cp0+-j6zat6qp}}=nboL5(80%hBz}zP9s`?E$s&%IfYS*s z4@8x$a|I_!Cc7buhuoXlS=L|(kRGC4?pIrC-J=N zA~{ckQ=HZ#Q@B2Bb+5v+6eXhE_QSir3eDykiuzXw#14O8xZhB`C9dOM;m$p zvdaXLZjCzI?)vUHP0phl;@NE!KMl|y7+w8(pj@p2trK*9;rIT1kK<^cq#xYfna2D4 zssQ^%L5|G3l#A3I`tzB*@-wF~g2xTIE>{;5E?QT`{ZwxQ1h@1Ov`2}%jCL}cjrw$F zK7~;(EUGVPEIML%keWhc#w;3W9Se)IhXhRszraViI4SDDXON%{Wf)VmW<>3T>ZPp$@nJYWGP3KMW}9yLNn+Y8PxGH z)A!_xl&6-sxyFv9uS4nUtghW`TXbpss~y`ijX!X{sUIRH_rxPGNDV2h@QA=B(kn3f zfJOMzrgB8QktqGu)kQokefWYmVx?#!rsva(OE$eghu;KyGhud{;}9nW2%?9I<*ptb-?#W>wxR{d2@ zBN^OIIHkW;O|G1=6cQ`M@N_&|TUp0a-~MBkK# z8XOv`_Uos7@=GSs`p`!#8uw`MA65#}hsKLa5r`&Lt=WtfiEbq@*{@oS!#MZ;xvHC66+W@c&Wt`U zS50BZ_p6G7y^YmkFKW3zy{~TLQ6sG|l!_9pD7YTT-uCf-a<7s47(8ekH7kI1{S1!Q zs(~ZaQ(Pp|i9ckew*dMvp0vd5dqQS#M?&mZ0w#KKyw8;B5qyB|eMjP~jy;-$3+$Nnh zdS$1sJRCqw)4w35rYsKZY-*yo_wvl3d(rsCLIj0`U7{=omLqRg&1e>2$?z4@MShLP zSW^Dl*A?biVQkHAP79yk0iht#&X&##s&>D?g%T4`*%vxgT1}MY(1)ibGfCkFh~fPX z+ARRYrq?onb>3H}Qu+CSkG4mDZQ*F?pkHMtDmV$YU*>GFqxxu-Og$dOfO@@?KTy+K z>ewRiMA|BmZmC0V$r_-veuhkA=LWPK(P~FP6YvEL3(B-!tOqXkg5siE#eULayd-Nq z8xiI*#!uo!^Q^4V?@7IQVP!!L6H^ey=zHJ8EiJxqocT@CYFR3K`bfumU5X$kalVL- z`G!8t*>EN~C$j2zTJZhFi1UK>~taM>V2ra7o$=&x$Z^dG`MwKmQZ7ZYM!a0fk`j0P+)03EN9GjAFU~Yt`x$`z%=Hjnj}JbP^;WsOCc*j&K^!quRGtk|M|4E>9>GFcc)zY6e9iO!8AY0Aysz(R{nzoi zM2xJ^l!m9VKFEuv!nu;q5zJi6i>A5(X|3NH1c{+?RE94VaXPw#&vLf2f7z!~5!_8g|gzD%tg0koXY^3?J4 zyc?ANGraf-{L*AbbVN(6U}MmTW3{0TTGGu(g#+7%I;hUibu0FU{!7ICBgb8nfF9M= z^TXG#Z+U9B{tey;A3%)W6OMA@@kUd24}jPjNQ%U~5lw1wX&KIO(px8mhC{N6vMOwM zO4%(64g7c(KEbMGY&=UelN=j$DU4{ulVXgP{%4sv?pURF%Q%j#=I&4{{@75LA%~LN zuNP&{FLlHF+7|7f5q@%FzYEJ;bX=jn_jhIKXYps;jiTnN43X^bMfj z=NsJRat_JKvaFuD>+X7j*XIlcF;SlB@l|gMr`Ft;AxYcW;%P%^lj?!C3JNcKVdHQ% z4!}x{r$7bm7%1?Rf2RH6dClk4;m%ThXdn1xa?6i6Y`De$2@udk|Jv#bzW9^ ztN5lu{g2nuV?fL-CrS0ZkN%I({`04A-Vtdfkfs?c9pL?-sGuBs0QunnSP71E38HTZ zcy3E%e0;+Mi>UQ{Vkx+U-WocEH$%3Tm3=X3M{@0Sp#!4d8L8!aGz3=b6XgN~d>uYF zr~T=xX4_Xm_TsmwFUHblhXs(c!}cN1ZN=b10f_aY1jpG$0qqrh9+La?W4YQ?7$3w^ zC}|RP(MxZ=AX6a_gPiHS&;RS|38#;gC^8SyiN1i=6gFTO=B>zynBDBTqF`w`@*O0R zS%lI~X7To!7@zB=vq%hp4HsVJ5JzopU@M1>9yFgfT5r2z`fK)JtUOukxS{*Iu=c?lUBQ*ATQ(*;l=qF!sWiZ@6pY^C5el+za>rjpq3DW2U#+PV#IsP()vfn`InC( zQ!(E*L0p7TtQdWV@G%}dP~6hk?lMNj(JM9=h4&|&>NaaW^$imXt!aMSa`dZfCurH3=-sPH>yWEwH z@RU`DnQRwtOT_w=@AdvEcZ1C6lV^0nU)JLJ|_b-3C19L7;rZx7{LNNp-w z5UVpmy965?BdHXs`$U@quEBQ)1~g&hdnuP?Epd_Vje5%uc2X}xzZ?8cXc-uYiV0!~ zC`u9v$W5qdcwSMR@N-?iQNPx|clS)t4lBp;{nz91!}Byh!SqihJxkkbZYj#f^{AW0 z+rmQ5B(N_8d6h1HM-&gU>&aZo$$C>(CzLAk{)19`BrUwsRuO0wT+Mvtgx8_JAwfS({b&P=q( z4-CCBiAMCk-?WNIT^1an3a}i%cKKTtKx7yu2=OpV3OUCA_7d}6Q2~a4sVrK|^GNcJ z36}vfbN1V=sV)Wsd(WOpUbCRyKPtJ5_>W>_{8CY>#x1hs2&N6(eNloYdLS#DQ)mh9q;CH zEEMbbFlH$Zb>Njq%JEfbn0OCz)02nMOUX}ce5;dh<3Omo2tFHfbP3ZC-Uh61xg8?N z;{kt%Xj)~-cRKZPm&}FTN z)x^^Cg-^*j9?M&s3zoFcHU!-4FV24{hsNT-yICJ(sE2|6g_^AWSEm+1>?j|_9+FCPj zU8MiH3LQ9o$HugP22QX856sOe41HIo?f4{|3L3k#oSXx!`I?0k@9K{<1{HTm*~a*( z{RHJM=GC7rKeQM}sD$8Ko{n>8vhzpqhEz7B+ow8zBIEqN_>CsKpa&`hs-EGH6i@yH z$8^?%N?V6^2J+Ex5@|ARmY&4XmJxe>xq(<_pm8Sr;_R| z3PFy}g#-kq+I~zuivyi?ukgU;-(ysDF55K6uR6#2ck9|v@;nu05`+Iej+O0eLcvK#{LS>_iZ~WURzhfulrcOd$=Ql}& zVJ_s&7Vt(^_3{a{%A=tNG4*H%1a0MM60UpkEj{Dqq%ByAjQ@C+1>vn&G2N)tUbB~> zl?U9a>XQqvj^1>GS@LkrorrgM{a)$ZjOcn4{AU{bf4=`oij+Mk>F=H;8~1Bi(;y5uW6+J$^g;nS!ML!v zXulk%nW{GA_qZPI;fbWi7(9$Dw`PFuV){a#^m_rU)`3WH^sr`x_OYN#dOP> zx}^pXVW*yZSm$wdy82x$MC<5DqbrBUpRP5BDqv3m1J*hR^RJ zZuEW@O%&wEfM3`-PlZcU(IYPuG3JAAzKYmy=q(tcUHJwjC`ZHu+yXyt+Xvb)Zxj;1 zs)A$6b=DIIPsBiT^ABhxrtsJ!C%?2&F#&?u-{6qEypf?&PV%!}?3_z%5#2HeFL!LF%Dc{myjRq%wb#J998cu_iUSHu zMehQjIkTEHN+5`L0lbWRqGy<}_uZxsAv(7538m?UR=+whS}4oCD!SCfl!8=7`cxtj zp2Z11pNMx3Mqb-p`Hk6&AMftb;FRpA#Sjt_zVWPB=E^lOF##Sz-k$+^i-Duy#O=7k8IqrjjD|(^ z;>8f}(&!~nEZ~uoZ)r~Rxk^i8U=iMXO`PL+!YsS6GhQmS7e9*!G!3g()uX)L4nUxM zZ=YYLKT}c}s4feu4-@INUkU4;PD`iNmUh4jl`_PFgB%4)w?VC`{WUS*mgHGNcvFjv z92j!lnkR30^K%P2&C^H*c6WtGs{9$`{1h024Q>d!r5T6 z5Dk;Qo%6UV$qJ2o909(xX>H|PxsNkGm0nR+T!tpNH_*t{SKufL1e(_f7OZ#~H|f~L@=Gpu4p;8l z`{HLVQjh?jSB09I+DoRUvt30H#=qrr{vDLCMXP$@qd!K97-zE{n8zh&s|ogF{Dd}* z+FOpvlmnq~Mv2ts? zjj$t*4D?R;KxbjFOAvyMlNy$qx_!t~>3Qzr0SIYJ!2LRzE-it7W6G%S&c!FgU;HoQiQSVb|!o z15pF2hIEhppwV*9y-C|fCZq;E*R6>UH+qEU=Dmkit3ZptHeJhovjxCQA^zD)jg(U^ zlQzLN#m2t{wg=z)E=`n}g}7IoA8!p##{DSG*&QE- zIr&{01xcNIMK+T!V_6f|JQ!d=?HK~*o)viSbx_V)JxlMl5ETWji5tr&lFrCuFcz9y zr4S$((~h62TlCo=*XV< z~kt}kS)b;{oN|6ck+uR{<(7%gZ@D;U-!Y@ zPni*D?(F8EZ~lj?);P8|`IVkbzspy}EeDMhzwEF6UEDahidbGuFi{VHfxKiNHJxT#70Ov^%`_nyUk$wm+V9L9pvg8sLz;v(LY#86iUitzn^&e{e%~f{ue#@-xTGhjyhTy?dKl3OD z>MHtZkQM##aNKpTfwVyUDkeVK(6-4BjabnAg~Sr0yuUw<+ty@d;Re5f|NCy>iXn|j zE%}uZClGj2J-X4M!4Db*zzjEuzrNr&n$**Nuy&cD-iG?kk@=>X4tM`S~&acXNlbO8VZx6;i z-L^l3ze$G>NMAaxdiqq(KtAe>s<*|CLMr_6 z7USPY_XC!j2VB{dgPxZ9;~)eF?t|`d3%acWXXp&14%u-=+wT$fw+ts4T_=-#?tAo6 z+j3;Jyl`%-EvWhIRF@FB6#Cu>B2sFhy(oCq#sKSvB}Y1S?E#Ro)rf;;qOtj#{qP4( zcPf{2O=el8CK?QORXqE1)?(+lq$e--JR|xEGHL7^K1U{+*W6rc%kKxvy8;>?{Jn%1r(UOv z5yG*bdlC!_fnBaFmCLZYCR0-iVQ^5DJlP1{D{%7fGf;s%(u9BjUk5|DSU2P8x`py& zr>5!ew*3T2guO1@IPGhDhXDW!)an^;$#(&EXrF-Q0Y5d!3IcvSRW5oeBzX%=tUm)& zPN2<5-;q*ETj0J>kd5XBq%jWR^m6f31`Qr=0N3t~nM6PzP0HZpINwMdPurmq4;i<> z$i&;(20@qYsl?Z1-wCxie3$U&lQHNdTgHE=vxHI#LoQK}@a1|DbMGJgKzyva4)#&v z?$Jd&-ygSZO1tQ#RdDZh*ny7lr%f_xJXy z=+zHRN@guF<9D;BQTpb`q85ptZfZ|C?`+9zq;uL+Xt3%=n6ni_bmhvjhu(|`dBfw# z23nAIe9>zc4%bU%^IQkzM;TX&H)czY&=1L9P-w5|wF+c3uK&!NxGm)n*^iP}8oxD=!<`TF#!5LHr;8Fw*E35l z4|8AS;^H&3U(dvS+ab6C2hkHlPxJi%}W7@jKQIMTF4Uo&eK!qFEdFtW&;C?HEFV!P zdPvDl0ckq%sGegO{v7{kM|~h%;vQpWZG_Mn2AH#OG>HDn5pI7|%mYNa+snTrGIf+< zv)CHu23*2oeXik!jT-4k?m~a`fXPIJG5fh>`*(70=Q<$hWI8CT$x=w5{Et z=5V<<OA=C zM=YQiE7p&CQ%W==y6VeB&U&?0AbvdXN-LMvR8Dss4USnK^K++P;t|s<{LxFE=G!WD z&&@Hn8wxS+-gD-;qh0**FgdWW;RL7IU_A^Rjzc%TO3|iyJ?kB@5G9o^^=Uc2(*6?J zAC=L0pFq@Lk)JXk@KET=?xn20vR-QLdOf-F;=~WP#bIjpB8_N;>AQ0$9fMl6rWIc$ z`mVn0ou_%8>~4)Jmq{COeS^FpN)8I;twtR{bn5zjrH*Kc+jr7uh|n*kNO&>u6;Na! ze(faODAC7pU_kbW8_S(;1hW`)kWi%j_{VZI<1_u(!BC5+V#?vK3E(diz{jZTuy;uk zsLe?L-LVe4m=2&%6duh$!e>vBimGf7`E#9@4&{*N-_);h!i4TGDJwX-=kVm6;%^4w z{s4s+Zgvfr)+8C#toH_ZwNt?55pxLJY zVz5&DNPsq0dQ-7G7<*lSJJ}g)ya!(bN7D>Y@rD4`1yoRtwG@Iy&;8OAni_8>Yi@Qt&rith} zIQ)e0gj;|AW>PjRQIfgBz4gk*f*)VK^`n`A>VdkU>;d_<$+15+c&L4QJd^>_vuPp0K##p(o!6)1^+l zFOL$=#4o|z>lXmsSbGynI)k46ZYH?t%QwRo zpH;h^)G)!;qmG^*qB`#T8dw6FS_Gp@k))N5=o5d@QKW?iH^G^O2Gfs_)ZWQzQAF8t z%0@C(Gr&A1!-1vkOX9#os|*^&ebY>0pQq>9k2tOmW(B4R;%7M9iZ?YqSHnoGeCWi3 zI0+MZOYsnTjV|X;6zYFqj;HpS3ZFG9;h=89NO9E_0)}Qfbnpg(J{Q0c+iL)XI`X%M zlDr3W3q3(K$e}<$i~IO77G8DGiRHz~p3E^koCgsUW1TMKN%e~t?-aym*;)Yr8f)XF z*)vI*hKemVGsGoy z+9c<(6A7BeB4W>*I`L!6D90e`OBXKm4@z6`zP?1@O4S01h+-1|3Q!*5U?o-mFktpV9{h;qTUYmgKVW$) z2>6#bskrfXHFs42LkH$bTdDAg$nnI$@aIVAdh*R^S{d2#Yq@;G&KYz4P+S&eF_Zh> zs0#|@vD?1d8fU*OAb0BiJi0et!`5lEW7K8TSARv>I2n|XMjphuEIWVsXwaEyL%>GI z<@a8o>f0kF$IbHtTlyISqB@6>iGaxp5%^04u~Ar|`#)wV--KCy;)lIn7^h42Y;Z|E zGn~D8Pt3nTKh^)e;m6{fu+*QUQ|cSlu527@c;I5Ton(;lr|VXhC)T{i<=ZF7Khz2W z6ASM@x9>)(vnROY(uFZ|CV*9@OVKRdT0d;Qv-c9D%lqw(H#`4;Up-)qugG7fYMw3@ z5se>swOqpPpapbGyiNVs!5sMap-E{su7^o=Q{8Qpm*}goG7+VwU$SdNm5!tfSL4~J z9@KF^XuR7Ja8=+8aF7|*f9E=<| z)Pq952&^NzaEF`@`eF&6)A;~O=Bou}tLx#P3b&lS!| z>U{v(h&YSzB7YvpB1at6-!7VD(X;@L19 zj|cHJEeFq+AHTh%WY-BxjG1W)yT4&LH6&Va=h&BiCdDFT_sZ|l6ZINYp%b_G`G=I0 z5&Wb=ck$L_2wbyD;*~fS#xUW$Z9j_NxXuJ;(7Gp{4Shb!gb!b;vv3>!YT-R;)ERze z)ynei?rM#KBBE_u@cfz6oa0-2O_Or<6~J{yaKRgY&eOIjW$2MiP1Zg8f9$|HIW)1fgmkH2%I}} zuC@1GPWPJgedjr!&idj{iYCvK@r--4|21A;=6+Egy=>Z-mu~TWX8b|Ew4RRIq}54H zmxpH*@HJUfFO8yPHJ14mp9xmAum4~@DHSWdHfS~=H{60(pH;6rr}&QlbE-T+wa4Y_ zTp8k)lVyBvh@?L)!K?mzUv>oz(BB!63b#KQE(H8IMb#k@@lA6{Z4Lu@Om@J!+;N)dcDbSA|`(57Z7?jLkfoBEU@0bVcUlkZ0UVKcR1yo`EIo?2-e%p44nMF2P!e2zhYvV0A(9d#C zP+T_!qn`IsObX$QH*QGp%JH!YzrMm{x11`dzsaf=ctdSxM0SGpR4u*+08|Q^DxiD_ z^V?Q!0$@w_ypZp57j9-sktI98o9S9hz(6K@oS&qkL6i5Lc0J5X8pj9oiu@~knW<=F z0Kvw?)9eI*sy*wl|GL^sygKGJ6;C*zJYURHOV#Z61*x63KG# zM=+|9#m{0Wy+AYIE7VAmr3vD5pNG#jpfKN}ZHniPXF|_to_ulP9gCyKTE{sFJ>C{W z1{00gr>?_=fhrC?5A$zZ9`^aDY{D8jm_?0K`OfV+m1J!|N6UwJK$hwS}m&VfrwBXbeNT{0f~;8%MEvh zqwQ4mgJ)1_re(cwZ*H6WHvZ)|CTO;4pQ-3bnW>cJB~?pqY@TfQyM3C7s-Fq&ug;jg zOmeO}ne@Aj+6~{So{-scf_nt#-*4{l|HyaA6xBB;D-jK(-LqVhJU)C5m0 zSKBG~>J(_-63;HtAMpS}7yt{q_5%#`es3k_kxH=uH8YE`*JbsV%poSCTd9_fQw#y)1Ej==Nl) zib%)UC&x{BHeJX?y8cY}!1KCeP)$<`!-CjZSN(&n8S_;^cx)qk8oT_70rt1ATe5n90v<&)u=YbvB$@i6^92p~VZ0hugGM9;-w>E_Y z5T*0Z?y@U4FNV*%u5<~1E$5qctWB+z{V|46z)5qzvuYr)X8cvfD~MASpJw5_b!mTL z%D+mp9GN5aKfNil8_WoDouMd~0U}b8=41S9oNNj;71w26Ibnocq&a^ztl0~ltP6&H zL7xujwZf(5eM2f;yF~6M-!`e=A8Dz+*sZJP19zMZ&w9o5pvKRZR@nVQtss=3t)qFx z3-|m$^1cD}y_W+r&c(?^7@y3x zwBF964+IcOjmI5HmfIB_(4!x3H0lOH;E*o%vTP4pW^bvkw_Lu@G=T-%F*X4#=Q=B^ zJkgS6#JTl$0w=IBSosBOW)rB%CeiB4dUeuynZlZUy1P_UR@`@sBxi$alpb%D7~rYsSZxvd0@_T8?!@T-wuJ^ZAZ-of`W zp-k_Y{AX%kg;qvu7vHeMW>wag&Fh8TPm)_u)V`c4ah zHvJ1-`4=o-ag^?p_Dh+2kP!Q2RQ2jRo3DpUy0vS(v`3f(S-D@cK1Ne~zkAX*!io(7 zK7MRo;5z$RQ+I35z525cy;fy)z%Y>Riaa@AVB${+OOk|nIHq>>50_{$pr!*aMR$<4 z7SB)EAl_Mtlo-MToCtCUj9yGeD`bpSk8Z{+Rbe_|xDUF`7sAtp!V*;CZ*K;3d;e%& z2HSmK+q-@VOC8s1sjo%g)*$&NFGmBA9i>81T1n6}AhV!7YPLoU@PIhgOc;eb+b4v2 zSV_CrAxgVPTP-UQC^>(TPU_RW;Y<2x&(60<0ncex!>e%O_Y&sJ)0QNLevljHkGQ1L zSc}UbN0#a;BMFkv#9i`4y*D$+z4*Z~tyF}M0Le@M_G1*2m^PBhP1ecUpg)v( zRR0-%IxfN32IK)Rflk=<%+?`G%ZOfsyDgULoidK^f>oE`k7uh{fe>0hkQ}PY)@X9GDtv>$UkuH2NvpLy0>f>RxAEVSHVG zdNY~Hq|Xk0s-Pac|JH*3$DDC*WHN2V0|e3S{`b!n$lUG_#ujSJDmvfpxS0BqB;7AG z&0loklYkdn$LQx3F;BI6MLO)UScn?k0prB$@B2Dv1B=kn`tf6A|Ln>b&MJoe;VBM@ z6qf7cK&4&#c=Q^+$@{FH!r7ymW0V{tq!2Fu?VTF*>tVRToopvkPU9NtXq%?z#g5BDV<;p zxNGah$osZ5`GtXhP?ZoH_v2x>-$K$?ZE1^ds3$4QB?F%yipw*q51%c@_~9L@i81D6 zCL&yZC9F2<3t=A-5kmukR(^ zwMrSbOAkRWK@4YBvcu@b^v4;b4(#!~C3Gqg$dRC(QT@r4w@4W2?SWM-o3Ht*Jnf%9&*5Tu zIhTXoZ6^*sV^XEBB{x}`o3X%3fNZ;x*pCN- zDwgE0Nsw0#6K!ABOG*bT|@C!df%(w7SOyhkoSfmM*uNcvI` zaFvG?LA<)y?JYrQn zImNkU{-u!mN2Evnzal*}c06qRZZONr-58)FWvr9cwBaLi{SLJa(Gxi9LtWdxQERMz zMTaz-c>*9^uC-SefuXoah`Qi1E@FQG`uY7Dg*AMlk56?N0)E`B2DdKKLa zr4<}048Ensbv-=dKQ+<{up6#{LnBxy}nNHO<8jG_FaWwX9U0DLc z!jqitpiXr~WSn5A)}s3g-T2H|a)mKvw?Za-plWD(2*EdD^~=V0bz}AuouMq`cXN28 zn;rx2%36|om>p=FRc*a|`-OJh`Cwy{{5T;569o1f3M>QTy=Xhp+N%(T44ptrD!{Hp zFcob-wM2wc&ybY|qNFAk1}w%opraD%Jb*$|wO{~-qGF|bSMRfZH#awrS(pZU)XH3! zlt&64s^olUUDIy~5SL|qgy)0NLx6C1=`}0dzrpH-srtB9u|B^dNy92ylgu~tTMsn! zwp#y6acB=9W8z!@M%DrpjO-FaP|uz{8}a%9m|j^Ml2{b4c9HHKr3A33%?uQ->Q-}; zH;=H~RI>X;RM(He-bP00DuAUF1Y89SNZP&?6qaa82Xry12AG`V8wVte*s`r~(TxYk zLQ+w(j}!t(-vLdp_=AAb!Fs+P024I_GYZxQe;4EN%Tbws{pOMv@;;*&Vdy-j{*~TEc$ch2tj+=rE4GTy<*(H8$Xsy0Ab^LB|bcJ=E26Z zo26jNaizTK5{Kxyrd}V^K~^UBd~N#iM^rv>Iox%SL$Bb>GEV>{_)uI^dk8@hPhoR$ zfoFbeNp~l({_KY1*Q?j#u4+x8TX3PUfrv8Nx7_owcX-}^U*GM6(M<=+7(Pj1d6vIK zt~lXC)pC3%7h3+&)sQn{RqIvOi@FuP^_ZnC%nUJ;^S9E#qJ-2sJNKycu7;n|xwsX1Yqr-QOKeLd-=(jz0E&UME# z5Oi|MD%SOU))D}s7>P{8+edD}xy9`ReFaGg8-l>LGqbY3W-C$1%F2EN3oBaz4Izx8 z=v?3DYo5U5ZyMBa3OK=@q#yyX1~6k{yoY88-0#K#@kCcGSWM5d_&O}?bH=^rgE=Zx zO3y)82Mp<%j5dQ}EdQ02HzAjS7Y(y7sVB4_FHx<5s;5h=iR#Jv0OE0VGu!z>{`r#e z_cCXf?%r&LF83uTL+6V`t$VM6VOavO0w&GntEkKT3)h8M__cqbc!Y9fmR)(55rV<9 zlqu9_NSAz$!D^DnUo#0{-U}qW@reUtpt}sXdndZzbyrC-VYy2vIh(_g72V`xykTb8~a&NJvJtx=KK^wc<8AyRz^+^K{JX zlSTu0Sz{~5R}B`j2R@e`t!n$GO8PT(R4fQ%gWBI2!p3^M?U2WZ!^H{wRwvbDQ-(V4 zW#tNH4SX!QY1n>!tde|XDxPQmjFyR8{djC$=G8U0;WYWahU zTk^e*o1d@zkY%6@JlJ^`S2wOQxXYMyhFhjkQrYUy&-1+jPR zrxY*uro%__ghPnGz8>C0#mqDGMy|^PVHSJ zZkFv%&c1%>vgJGGvZP8NI5uvaf9|1s+lHv5h&sHm;F0t?ja}y4x*$tO#k?{t=AbrU z|Fku<{LEzwh*Mzwh-2yM8`V!OP1P9ZAG(6bnVAq+xz$V@%t0A2w&VPHhp8d}>&Gbqxyf4y4R z@&O!_8LDabScPc!#1`M+Y>B!186xVK0u6Tch>He-BVFgtK?)e8?FDxX0}VONAMr@{ zZq+La1&(#Rm0XlqzV8w$36f8j$-ZMAE7&r!ab>>K+{m|Ra!?~);4|ps9=uMfY40?Q(?8=pw_;GA7H*8M+K85p3FTTW@dmAes5J-Nn`rHKo zuPI5Po*lQ;4xHZi-rd%1c`)zgCBJ0;EBY5>P!V1`QMApeOZ3;Wv=c-(ftLS`{N3Lb zHTqt-=GVjFA$#s1lcc!|1`esgP%R*#-B2t~(szP4VpKUzTr8tow&}MLfDAdS#^6Sh zsmg_DS<5R@Gheo;oEH_(l-$&qA%AkD?#UWvmA+lE)Wr5IRV=P8v-aTg!-#pfk3N5?d7O1vSUFa^m^ZP z)2q6Qnf9$3^(#s$W)i;zry0Kor?-Srwx@oD0SRrGMGpDf_!H%+GtQxus!7fEi_@vL zDHgyWZRaM2y~N|jV%$Iq=#Sr%b(zF3FxIJ`=69GHAy*jai(zW-?Bvi$Ifqt`=U&%Q z+b=o>HK&m&P*bAz`WPdS$2tx|p^-|k1oVR|yEdHT7E5>VHmLUmlcp;CqV0I{4XF3{ z(flzP`So|Ct}rp4dKGW%drRpdn4J<@0|;5m-t=&x4KRP&&q-OE3Z|}-0({|*U;RD_ zn)jQaWs9+0IVMJT?sTYqrzTdsJ=R<7cjb0*IW-g6jGlFADeBzV2)He zvr-nXl#%%I7ykYKN+B*p0Fl28SfvN1u!Ho#zF z%XfqU93`wzg$Pd18 zo_n+|z0jE-1`&W9X%96>+nN8Sobu(~0c%aEucg`f?d#R6Wo!wqGT060rY5>UkbdqS z-9a#5l~*v31)wBMiGJt@J#DMRV1SvqxdKN5DmRmd3`o(HC?CZ*z+O%zyzOp{n8R8K`$fi z31VmMm+A20fq1(58%(tuF;kOvf%weJ9Jcab)3HPpEHEvN%t$&%FP} zN9os|Thl)xs!Ic5BQ@F+m1$kgCgm(9cPnJ$9`;4QEA$j*Sdy}WcGu*!8dn4k`83NC zJ?Z6+G;!U|(Wt&WX`%&r@`tzl!1l~J)Wam5zUMdo?Zx0P)r#lRTgfOH#{!PD!V!!tT8%n_68b9x!{mPF689_PU{=nG0O)YUl_AA&#kRS z!je+IA9li^nh9*wXW3Et-b)jAy(qs#h4r81=*zFDozk&Vm$Zky8WdLBa$`q53?C3x z*V{WqQ~CMKp8mMnyDIHto_7`L$=Cj2Hverv-_VqdWf;r>ujrqBE{u`dr|t!bvyG7N z3=F>!89(v1l9DoGtApK+mX3J}yODC_0~J;l3illEI~IdYd@bR*ExWm0XJ50hX3Voc z;7}VSv~u-m_hlLcvm;+Muuw8f)3Y8QvfEj0Z1kZrr{;aDu~eq(q2c#Iy;&A(R-zGtulB+`({3-=c{y=n znvZ8hx#z@0)Re~6gT?)cV(kqYvZQJj2_+ILQwL!cD8FqJD~*g+V=JFwF&jOi$5zm{ zMW^!T{fOvh7*}YDi*hXuf4;&?H*HRx+3TJpHWU7XdH!sZQgT~AYpFDe`rbOl*k$C1hgM%nf98E=uH@0(npoD4csJte^i+*}w zgM#0wX`9(M)w5Z1O0ZyEd#q3=Ck1D1@yAz>ym|5zBs9vJ{ts{Ycc=Bw&*+j=2Kl*5 z6NP7Z5*6OwlrrVHp&H!uE>VXO3GT_5*t=A+1(73E0v1y*C~(**=SLD-28Oc(_)tF>+<}1AB!6SoTw`#at{B zyZie3g22z#pu`1UH{XPZ^wq!fN&nts{r=Oxe%1ed{lENMQ1%tZviOnG_*XK&{P|D5 z_1}G|7z3UHStVZ5pD+7QE#{}YA*D%tX+mXfyr=E@&$ji`Z~p0*%*szc8fRuFp8a=k z{AWY?)s0}xCpd><*+nkR{lTmK>ev6>_sCg*6=)3gKL68q|0k9ZIRc)^sDs@t=l|0? z{D)T#B?ne;({}mZzkB0<<;oT9z~cy($NLEV>GSv}FBK$91gs#2Z~IT~!k?Q6b2Tu` z>;R}3?ccV{OmGsjC@1;+m0R&|%_>SC7^Y^6G5y8AZJ8Rt3dmXd#C|t|;)n9wq{`db^>IOcF(M+!Y`{&mqaDELn>(gKO+vnFi{P~sNXZY7|@c(jwN*o}t zvNho%{rjJwB0i}YvF85UK}VMqR|Kgc`Gob&tr@wRxO8v(@ z{Uv`B`5*W6yT4K5-o&C?*!C&5ywAyAQB+rt@QW6uRAb~Vl^R1_{_@DCg;!GUd=uMX z)@4M?Ak^WK?`)czB%`UgH0I91W#$hpg6eOPmFW>Jg)M@m9_-e*sop0w{wor#{!VG< zFvZWI>#29y)%%$Q_QS0%!1Z_ zBT&K#rHJ$x2a>WD-F~R>O!+*WLL+h+jSi!;#kmNrCQSA9FZ8{Sp7xo)s?&&kv{)C! zdZ#bib@5$pJZ^?!qB4XM(!wWdQj<_-9K`7s4xfr)lC+@`V)%3l!J8es7xe_KEwn8; zp6ThqzDI1id+x9gv>fE_R&3=cE5D`%mFr}blkv4-rk`D~)L*XI1SvC`K3Ach*ggtv zAkm*hM7Yx{Tv9;{IyMrN&y=G)L_IhWxy7DutBf&e8|K%>;jrX3Iu4jiRioq45p3e! z7@9hG@6FOovW)1YPq&?6s!T0;Zx(WNNsrA5w~@5&kt$znJ}^x-L9EC!FhFUL((S8H z-Q%;*q_fT!1Z^b^!=}Jq-3;ybFLl;1I0ccoc#$kb#q<@$fl)obNr5mWX0KE!2U>S& z^~F!2jT8YW;_3vM(5oo?X@Jq4{8X|GK~RXg;V@WG=O!qY({~tj%dLnii%wyB?Mglr zVw6|8?d^7XR2{wgsJ+g2nns1~oG@l%HB50~MSS+gtXC;=lf}5@;yew$d8fFI6rVM( zXSl0wD}3gP=1K+vHc?)rE-G(_22B@(X$>=V;VA2#nTL!j0zy1BjzPV!7<+c)KF7f1 z*)VN09=#3l+2h+REEI#vke0<|CFImYX0B1}BlgLRo&V8&{Qb&;&R+-3J?SRaE1^H# zgg<_%eF5LxThX4_u2DRBbDD9;AJb6D;lrqksMub4x?%m?(RU4}VQVgc-eAw(O3%6_uE{$}hh5 zA!`x8%)Va*`;l-@;IZidiI^>6J7F~n)^8ByHmq~jRfhFwJL+wr%+yhXdiw#i8o5}K z?9UCg90v_1ImkvBQqVQR;#%i@>bs?ZPtdwFaeD_Uw$KK?mgHLNMq{t__v^1k@>Aiqd=uk7fhQOiHV|?GI)I^kt-J(=BcM)=2jV?nT!R;y8z`!G z!G$y4EJIu{o;rD}(O9%(gCDp{L8_UTd%llb9`~!7IIIwM93oh*>Ab4Kd8CKzLhQ%8 z9HjdG^$xHHyFmF@f{aQbs9BC+pG<$XRIS?CDJ?Ue6AM_qcY%Ta^&;>?A5_}Abx3Bn zSKSBI)Nah6*qu{W%w@NE;eKAU(ekFHZ$qwK`RRlRas@d`sbTY#D}S1##Oi%y@cx=% zoFyN&nDP8#6wsxdEnh?+dcZi3<@R#l@)11*Mdogz-uz>pc(U3){yx}9EdfTvmB6RY zP&M@Vvd6~DQcq&pdYn6g^p#KN2ClBZ?M$<>J*`okqqW#^c+KEOXv`f^|kpZYd6bCVV(NJlyAFb)*6A1DE?L zNP~w#{E{Exc}aqrp)~)}t^l=Qacu4+(*xI&#btSL_H`ur1V=q9)6X&B0~h=!pW^jd z`OiN2jHq)zPV)d=`a3=+nAl3!_STjq;8otW=(y0;&s}t8w8Bc|8qnS28OtA_X*2$q z*}GtM)eC|>fn2AXhBDbFVjRd;1L~f}tY={raZhND{k=G8E?24^B8?G~f+OgL(a)Fj!5e@4!bB}T|H1lyIJ>}4gD2Kt1X*5meX?_g7>0c&I z`Ruzv>gUrbqL}?LUetzj`vsj~hf9r9PZSV)bN6AK*nxJDg!x;FoVUd*z0%8UJi9JZfPqq65c|Va{P9{mYMM88$*-s%I=s{Tc;Kor(W+%5{UDs z=I3*enWm}^aO3IUwG*6%C(E$as=2%^ob}*voso)8SXO%9n)Yye5AP;gM}w4gYg*Ha zv*TC%k7~Fya~#DHOeWv_thHXxP(A65Jgn|dU+?H+u_~4STtiAXaO)F0fv_k`j&gxw z@l}DmlZ=PvVyCL@RRGir7oRE8 z-_>c*4NN~aZs&xgN4pd%wO5fyPOxYIs zQe11oHd-L^KIvwvyJ~Xnlh{wB64wFDN_T7Pl)a*A@s0He3F!?qTxD&2^~|}{z7ol4 zqUHA?oJYlC;3mewP!F{w&F!t(&MgT(D=*{7>W8jJ*ziTaA1}AB zb47|U{;Hh>*v5l&Uu{G=>mKB$c;#FV_EsA!& zDoQl`Jr}A!sG3FIZ!rKUFBYKf6`136$S|w4h!7t$Jnob{GRq^KvX8Xi{Pt-Jpq9@X zp(o0-F3<`&*LIhK>BqXaG=lwAP=G32dZ$27v_`d>4d|;|U=3;FG|<~d1D@Tay&Zbi zeOT2Mh#Gvyac(|Tsi#SHfqg0Rt#|@1NVgJO>S7Ds_lbZO0Atjes&SwxPbI_x#FE(l zJ|cYyun*#Z(w)uR09tO@Mec(vuI)Vn!@0Nb9fH+(FV-so`)hT!vDR;I(xxBnOOi$i z8*Vw;<@t&z2Hg3HfIV|gHy~dA*febth#U13yw?ecA8L**xV_nDApz)$fZBK?&%|wi zOjs>9B;(|u|K6kmcv9E^0WszZR|@*u83*E8VK}rOO~H<0IhSJlbNF(T4fwt8t~aLF z{><+^eM)(gebfB(xdt>?N4jplmn`TM2Nnn?uiXXwhk!vz#oC}}8?%in@Miz#2eHdt z;Jc15w{<4AJ8JK7K&}tf)1(bqv+^#aC0jtR=BLAkxu&p=qI58jc#!o7O+6A6Ev-D> z8kTc4K`Y#Ufc{_+5xI=zf5!sniOrEbv@4hlXsRP_nHOP^?SbDIh|8L5%;}RaOgnLae`COi-8enS>jn$_XMs( z{qoG=t)3B$2wD>y1O>2HZ9eAQQG|&5ifKW>Z1akfS^UmmnzkP8VQ=$;t#Y*hX#+sqoCMLrl>5&tV4DcCkKG z)I-COb&uvpF(W|fz7(k(<_dq^^o{P%RIf(7*t+Kp_ z(Z+!|kFV!tsrNWmtxrQ}h6OKQq`Y;)%0RXOZ9JY5Lt;)TejG^mM#ZYh$XsxFg+qeMjW})5j;!X-DEI=T-BPBt92=Z4L7-PUe?(&%L7K z%wlL6AW|OT!`DzZ;hL{{*YQQy2&J7aV@BrW&LY> ztC~?{?5*hr&zNU)dYX*`ius;1tHzBf>dT5_D*;iOc_kyMV(OZ9V|yFH^p78k7nj(` zJey-j*%H(N=+fsim3#?#*Li3820Z~6@k_i(z%42x_jhk;h3`K8A-Yl5cBcKJ(6d^5R}V*v-NY^x*r(QmKbX+B&&eMvw~^h--)n|s6I76QEs8SE96g4@_7{qOaxYE z>MJ>oo4jiWGuLT)45|HwpJpG?Edu3e4B(WCp4?fpzUklmhNpSLPk?VR|D<%Io)g44 z4n0P=w}TebR~l$>n5p}!ePitP+qL}IQ}&|;OHP`soy5GSM;-FZbXc1%y#6USm#PO{ zF&#$}65>DWiUoDEU%IuKN$Ix3u#Z?;>H_Py?Fwjusb@i?wKZnolvp68Pxr;~-6@DZ z0oX5UUS_6X+1nE+}Dr--7jv*{;S@+%56)b-0_4V^CNX}aB>!K5~B5sd6G!mo-aQPpQ z#s{Jvn!x<5GV_IE;^TSP+?%Ai#cH#5S{fV2n~%R~RD9cZT8#EGzP(+f4^=Cvn^P0s z9-&w~l2Bu8J$ZK_iN2o{ve8TheS)Y~%@mKQF)CW{wI4d&XpEU?r22f2UzVr;$w2N1 z_s}p{+#s=}YA=!0-9y@=I(w=Mp|YDL`lUYt9!0lsR>|&78?(a?cSP1TLo@XOH&`0? zq}EHN(dQA7Q!?pZM^AFjR8I$BmZECs+44QMrSd(Otr}+q*NL~73j?XTjP_EsMZZ7k z(%THr&ShZskZ-n|;&J6b&3cR*Z2^USVnLC9>SzntJS(jxiF5z@U>fiqo)@5OFR2wK z+VYPN2I%XlKi6JeUEKn-9$%TM2jd`RUC=&Z!~~v(506J{6q5rO01&k+w;<(F?Xa@z z+$qT1!E}8q!QK-rhn0Q0i&_i_KaWpu4#6Fm3KuaM;g@j#Kxb&Gt zN&*xZT)$l30w6$aa-s#vxb0On8816{Mb5U-gZ5m?1->sWW^IH%8}$g;LgaoepkH83aNI@>Ux2)}X&RV6b%-}Ox>T#PhlDg4HZZlvtzZ(wE6R zBc!%XcdU+eSUf0ABnj*_ubU14yjQWIixp4)s&-E@2395YaX`Rz>^MX4C1`s8}dt@FhBeWG4;Vuz|D0b@XlR^{6M3IX2pa9M++95`{h9j!b7c znrgSagA@s(d{2d$iwgQzXYr;T>+-}}8U`MdN5}+=FI|{o-$Iwl>P&D#=TOa1En~};q$)wN~P8a(d-Tu!HqG?PL%~KL=Q#w!D?#uqlkNrv*W~# z%S|gUj_BXnv8CJ^P07dz&kurDY4ECI&H;(rJ~#5Lpg477|&!?milPFr|ZUKc4omj_p(E*ko%pDi^CxF?)IfC{BB zcf8R_4=Eyw3?|$IfGJLq^hY(Kn3<1g^VEU?F%FPA8gSuC==mc)l@MdV^+R&Ui4$dW`JDD)tmw73!eP^{CjTp z8S3y!0U@THqPQTI@Rxcb1PPPYd*DU~QR{}UJx%G8xD8+ZjH^ivbq;Rn< zMYi7N$wT$?JGj_g`Cw?p6>Gnrn+n}|pwqFnZ>&rCXT4u-^B~0_s^Z7l$Cr!+NzT7G zYV(6i0JD5=>UwU*uU;P1U7N>E!KD^U9__SKluNurpaP`k%2AF z%}Z_F9H>WTB3&FC1D+mk;_lWv={l`CAAC37>@%e{P(gZBrZy!1{ zi@dG`Z^;)E^w&0%>cfZ~Hy6cg$=MZ)-El0_y8a;+Gb7@^a?pa^0L6kHTm`xr(o@Kr zHloqA7ew)Ho4Y|tz;Wuvz0!!Ra_ zV^;;s1cyja!jyXe_h<&x0HR|7#3 zeedKK@8zM*^hbHqK=e9c7l4n6^96*Qze!Q#R=rVLY~>&71gP;WJjh0B7a!(J;-?`(Z)BnG7*mDKYPP;*qk!2RlVH=393 z*)};b`K<1`Sl=oxSp*Owg&Iepx)danGP(mhDZ83Hxb67`siXe}%0@=P1ST z0b<}-dxL>bU%%GwR&+YGzPCD!$b#*3VQHgEfKjvNOL6yFiZAv~C8dXMVeUx&2LcX9 zD%AW>7_T7(yCnAtc(l^|bk{^E%SAlujV{iVCf@hdK3+MQYQF(K%_!UP_o*Gj@)~m<6pAithjSyL_ z11ByXLO~>)=GuMB6)d~x`Jj=N61kVYoQRomQ@6mUt_hpz^(%)7;*c65Ca{x8-nwrJ z=WpK#5bg$X+(y*e8r*61?wlm1Ah8_&VgK+GsHz~X_CY}Yoc!K?jX*bEeNyb>C$G`PF-4W#$vSMC{5znvqE^aDH z?DJdR?az~U%@*DM89tvSx0r%Yx;H6Vn8d{ALb=2CU@F#4{CAw;M6Xi<4|b#K9UAW^ z+||~vcC?*BrZhM>KXU^aVC@r}eewT9hz4 zka?I*lDjW?gW~p1guT3Tfb8m`NvVq`c9jV`uwDkVd*K?3psIKQ3Hn%a95eWwkyr;I zOd53qU!uMr{{h#(UupTOwAHxIhquu)=CBW$(~pa0-VL!x9nBRAo;0uvIsf{fPU_n%bxNQ zU6q*{ILz*j&*%McyW_5)29o?B?)V4Lz@uAa4}k?&aMA|2kQ?_Q|Aq z>r?9UBW07I!p&iqH@fA{S=^jA4H!aJAD^BC&3lE{FPYjLYPM!UMU)jEi`$Y97*5?` zW9t-Z0S$|xjC-erQDtqG;4#guFa~OCxk8dP?4DDo-#-c`6j!T}gY0kh^wgH_vn}SQ z+ZXp}kP{&(D&^;a77x zHt$<+N35;mTA0dgS~VI@&^)bp6Y5@~#N}1_sbIdpI@Pms9p`3e0$}_eps$Y~dwBk1 z99=Rgxv`ZV7*DT*iEV1~t9M$V8=8Ad(d2_@#)V#>x%^>mcy05!8 zKlZ*pgJ-0ei0{)8r9y`T2P}uyMicJ3-odcz@WywI(~Ajv4!z}p#!%A%xbtaM1KY9$e?a!5%oQ6np8F4=++=+7Q7rJ#~wWfSwg8NN8XLJd4BRaXkkX6tKk|-Eq zYeS<)H7?8ZDx6~7XTn9rjGj&>-e!F6r;0h=J(sq^@cmoNoHKnNcCao&PaXjZCRbi5 zrIHPcYaJg>JBp2-*&8C1SqG(^TJmK`D(+n@&Gw1H#$EK8W@`{=qU zTXytATiOp#nKL$%G+pPK>VWz4}YvPXpn%FJaz_^h3pb{I`fBTY=Dj|FRr7D3pt z$KFnG<%abIl5J4z{$hL=LbE14a(FMzA<{SHW1_hY4S+Zm(BNrFy}B~+9X!)0lNtM0?5sZ;h=;M zYaCyNYn|ZSN8eIOP;?$68e1TKNUnJ=`NUK`-JmVD;WKYMoel@#n*NU4H+U6$MhT#; z*&X$OTPfRQKr=zC{}e6zJY4^iMj`DO09<#q{d3sPS$J9$EKs<;-$4j79y_?4Q=3?tJJ(ECxBVLs|r z89`7>xCjQCcGG~VmGz98md5c_lo*V5YOIEMU1z3#7#$W5k1c1)O|X+#SAWbAhH+pv z;r#JvtfW8EKMBQU|EM@xON5^7`ODeWzDfuC+?~4emcx42FjS>FlkoC{fLe7P$<}T> zTqP`tzgKt4dg={R>eCR4np@^LP}Ztkj6~GKs3t_Q?rdd`q|;BM1HLQzS(MN?iS3JjgD>oqNH}?nVLu+lt`Z{BF)h=ZD{M_TYaGt!B_N` ztJ8Eh8tpM1V=k!$j~Z}*FKV5hmgcw@yRYz%A_&$PN4b7TxuNU}KePAxLs{N}6LO7k zkKS9qfHvUmwoQInYZ`0-*Fm_Kq%`$I7=}Z2Vio&s&`N#wzAC$KN-!l@4pdH9O1?}8 zzKbmk_1(P-zoj2mV$m6%yEYAYkd|N*K|ea*|GqPK)p-~oZEznLRe}B^Ma=MnRKj)e zy%$6j_s^-y_<*7}@AfJwO{VDXh}IHVIwt^k(@#_@tOe)13ZP-Xh}msaZ!^cP*QOKo z*nZ|KzF8Q1WDe{OR3KQtIa0kJU3gS#%%v~xINu(cIUoi|w894~Sq>DHCbf>KDcit!YDlw;>H{LS^q+p4Oyd+lBYjIq}BqfNmd!FX=yOEH}vft-L*vzd(Aw{vJo4(!=(r<8QRA zyBeJW7z>XLZ8(S4#@=y=n0z~9thtn5&hy3USVG;gK1(HXIK8Dbt~@KZ``Ev+ov0CY z_d4`Zv4Qe2)+x4Rd^Z+VM7ITNLgaX^C!M5NUl;K?EsWSk1hQjDskzQFxIYyWwemJI zFv0#avX;My=3%3q78o=tPwhjC?@E^T)OGQ6Yh@Ak%)P3Kp-EAONaC6dllW~pk%CX+ zQX4(HptWAHDg&ZSD6s|!(8JlCp+(+H*o9!g1Qi%5>i8<`QHeayTLKfX&@xlM^{nRg zA-1QmEPhUV0!t$RLJ^g`R+zI%usB@*F2sn@r|^+t>XAjxnnvD_jWZYBsQTmA+t85( z&39Z|4y9@&T^qAE0zUd{8&+A}R0})642IX8vwMeblO7Y$VyV1)_ed@r2$IlA z0K>IbZKxSs2cRRWVv!GKHJ~J7q^U&=1^rxGhV?@D_)%FRoe2lh80_}MZ(P}K@|}Hj zVZKMcDW}SHBaPKKkR zX58DWpz7Fx#E&*s2SF8IQqztO&L1CPBrOJOolKlktvt&9AJX1BtjcX&A6^OyqBMxo zAs{W%-QC?F3ew#z(jeW9ba#g+(%np2L2^z&y5TpbJI?uBYh8Pv>-&p~p7VXj81M7k z^}LuU0%sCfzjTJ(ziYFyJb_l8E^{wH?LV$x%Ey4hvLo!%0dmKhrc$lMgl5*(RZp@oSRmuhIw zxD01f^1x5yLnA(joPGPHuK6(`{S>!8%v1d#9c=45&Ba6#r1vNvZ>r=vw^P>|l^C6H zD;#hbOPa@CQ?8MOr{m@Oa2~7d9zj}#4%BbRY1k}ew!hr6&TND~t=GAyfy#XiDp=)q zZ?Cb>8P)*n64BMJDg}eM*5?3DOb@2l=CgvlBv16@FfxI;1{)?&N#Ob9L2P~bPzx-+ zRep2@hN#tj$=S)}up%T!IM1e(HstY3HsGey-=xpPnqsib1 zAeR;xW&lqMg-64UMWv{>UIKZFNK4XTP6T(qD($3gW+1Xtm`ORFzU!CL0g^0Mza{~= z<^@iv!kgIYS6}Y#se?w*&X-Rwu1vKs9@}Iv>3dmCDOzErhI;jfu-_S&bGTyk(=vL= ze`H{!pDU_<6D#S^wf%B$o#avg^tbT3AOB-(D=%DFV0xU*GR!GLCGRM#{Rtp=3L@V4 z+@r-j9Zy??+ z-gFkTE8m-2Jm}?;GFhnqgni776~ndvX?k8(mf(6TyVea5&yjw`KHzOwW>NLly$EaN zw)z0uQdPtMIcj=t`$ahUbdE*2=Rw$}gTU=U>dT9wjkTsK5xIN(hk|6Qkb4r)+@0&2 zn>Tyyq&3br4raotIIafx1*8_}+jn@Ta1XX3h z_r~q1lGLrEOECDQf|ZP`3=6OM8puQJ$EBzP^cfEp+`*8K!~B=&-XsE`y=yrhb2439 zxQ~k83yxi@-T7h{{O9m|_Bep5G)}qO zZgqnu%{3GCyh}>;TJ(wW7j(EY+&xEm57&KY5s)Gi)`w2wpH7H}sT)rYfy9tj^P%VqhF(8Vz|%gvE+@7mukwDu;; zd%crGe$x#Q+fv^`LO1cag5O%m13WpcwmYKNVruX%GJ8=O8jD9?OmB7&hKT0fm% z4X-og4pU4FxrWVg{Pd=Vu$`U4hw(j#FyoZ-Gtf30v94dy0hO?<;GjXW{5%Xa4*pm5 zpiQ!A-47)6KKq_;k*k=fzBlm>0?Gz@P$0*GfeER_rrJb9*2b8S8t7QRZ}a_@**#@c z_u0>_Z3P~v-CHc;bG7}8AmW$;iO4akD4}GB)d-BCuH*hbYm#XdA#nB$!3>N%9({q| zNy7ZvAFg~n0Ah}0kBgR@AF&Dv0F(~7KQ;{hKo&puGQ4|4BD8%62bDc4_6OOl7dWv? zdH-2?qoqhFz)6}zmj|Hs@TsU=rkg(K#8$w$z6@FHUs-&N2bnm{5t2pa1#@b(h&_J_ zwWPhA2Re>H)2B~<*Kyc`RF?|fcH+nXG1VnrDXZ~wSjwirLMG5|0yyRgdD)FaC#FPi zP9#4q7>TePJ{0%};OF=^aI{f$584)E&0g8S>?QJDQGT@O#l?zl#0jsiV6pAwX3rS^ zuI5`J!|o}nCHx${Oy}{TUy(s9bQVGM=eyKy+aY*H=OgVhU&TH8;jn>ZIdWzZJj~rHTm85oqtQl7E>GL zo6Iz-tt~mD1#I+QeQmLkS#~b98|yZ&%F?qP^sPlGcT@I6ldn^w#SHs=gGrz93@2IS zbxMC@50cv{35ZX3Gn<}2IAEbzdw2A9{o*S{X3Wg`VC+mjKiQVt>DaFCQ1e;$@#SS( zogc_9nkH~28Y*L|Tl41NB#2Y!VP~o1$k(-wEwzpfps_+nZ9%Z52 z?nFFELvdHrA*ON)MCx$(=zs^CqnWqn*F&kdPSEB!iQV4N-(j0Q9(L@@;8iW2oR`^_ zPoYzu($Jc)!1as*phX}4qJr`SdJN$ChK2yohsa?+0&jnW4I=qYk`6tLVjqO^UBYUF ztZHg%@&LQG-m(SeXGMWhcSf~nLR?$x$QOw?3eUh(2@uC5aO|zE-zi#HZ%zWi_oRi9 zRj^<+;q5Dh4KUxU`GNh~`nog-_|5}jG461qw{N9^nrUcmV<^?6yz4?lBvc#qG)e?5 z8enc74ZEOPNIaLHR$Kz{p6Pob&H3T#GcD^W7P9d|*EciL&CU})7e+sdtO1xW?)GX! zKp(dam4Lk$UVOfP2ghN+bdM!x=1a|u@#0|fGBP35y>zE(ttD6OYsKZ$+w0}qKCX>4 z3NTeP*99Er^@RXymOI{)G>=P3F)xPJnXUQ{4!GuY=s!hZ;r$a?q6)|oE-z;K|L6dw ztq^4O#Vg6VB{lHF*5)e!1=Pub^`)dQpH)(PJf${f0W14IquBn)b1PW9t747ZuixO% zV{biYF5yDVBi;$PKP$W)I9|~=jJ?K5(so-UOUNh3Ze%&W^p>Y+waR{`Z4ss9$Cet8 zbymp2xYN6^aP`WaU>s9I@5bG%~sZpS#ajE)TzxXi_qCrrfIi0>_uTXk( z0z!xf7>{HOPEjiQd>ier^577UfyO&AA&Mm+EdYjx!1BZ|Hi2=Ofr^B&5A zdE%`aH1d9)2=^boz&!x%;oFw3S|-NI2In1Z?7@l9rf__=nAHd#`FiU`+DxCDrp-(S zy$Jx9ykDQ}_XAq0CVIVQRXDW31uPyzDaU-9%a{OPGp0Sr%geiW3<~0ZsQgj`QR)sp z1|C1A*CYj+QFdIPjltwM#n*FvSm)MOuPTK6?h9Frwr^}6Zo8y!XO@%j5rxKWP>=N|}*a}?Y5<&39-m&4H&^|F)21awLsY zpq}$f%kb?k!?`@N;lU5X@lvzZKZ1EAoKMi)j;i_&mhTyGsU6-``#Qconc$Od8+X)p z2)fFWw-CFpe38D++U@7VK`-qWYO49@UStHG1}?e;!CG7>_L>H>cJVC*+SDf-Z!W*< z9(|fEZMUeHdHqxayon`VvDO}Pc;MWj^pybU_I@GE8#Oy#oM{Dz{H(jN31dwfh7|!S zhBp2H$4EN2zqRp&CB4)=n7-OA9Wjei7E?ZzOHAQ7iZ8a3|KNT-ruqxK0u>#^zozzO znz}j+VU={hwS?xoViMp=|S&?VJ)cf1A!brIKK~TF- z!O!<_V}!%ky-gk7MMh<*U$T5LZQGn@jmlR$0<)PdE}#r$!!C1X1$`pud^r1i==+0t zK8d5!PCy@Ws;&qe8cj#zGHXDo{RpMs%@UM1=7l7_ht@ z$HX$Gdk-ma4?JVCI&Zb$eM*QFd`d)?nuSvd`J5lBQ|cQf=dMbQFTV)R_g zjDvl8@vC9osWga z{dhZBI$+;hJlnkzWg!^F1NUsL%JFyNRGJi2d5Er#zhgtc+4#Vx{lbC>XaIY6865=> zQ+pmTqzTPrbB%@a0OIdB#k?*Z5PZu#OgjiVhQHEOKjxr{&Jx$RwfvIlis61k0ereH zk|StMGTh(HpIha)jx2g}rvqz}NKb^ZE>Y(!jB}}qxbvmF-Hyom*!fXRI!F0&%bV>< zXTm*AbZHAO z$G|_$F?B7^yDw_C*zDw(ZW{28;@^lE%cHA5HD}}SC`Hza6r}&+a_Ik$2XUzn?m;Zi z{gWE=pB6klj!RwlzH$jbx1B5y%-H-O?s*xJ%xa{63t6gSr{6hF9Cvpz^X9M_a&Ft5 z6Pd?#0v?blsf;q97>cHyp`1S+Me*o640OXn#9|T*XzNU6q4-nDA_PhnH`j-ycYZ76 z|NN;WmT%VQj6>NxqNr+FXWd1Mi~%2gd?AkXo)1Mx)~^PAtT!g+o4Y*9uYbj;Hfs_n zI%LWKN!scjTz#q6L z{aqHXFn{OppXq|v+0mDb@#oj|+wG9lhrdh#)4WLC$tHpWT3SI|_Rneyjbe*{u%~j? zw~5HU*y3A+9r{0|9?i1V1zW%V+A>FTc(LgUIwf=Cz*E}559|}XU|SBtjjz5w@}~

X4H@xrcW>s}km$lWvv>M*&g%?! z0KP>LV5@(h;ZeuA#R&8RWaw@sv!`dQzVto(OvXH*(IH0H4FK=n_r z{dcb8^D_A>z+|viZrm?nl`y`md6N}SP+mQ=^ZE_fsj%LSol}f z7z0EDz{2&bw(kM40ef4F33k^zzsClYE1CWZ6R3?%FDUJv;&P<>!rQE`m*I3U4M=$X z88b&Z(D`}XDSURG+*Bf%VCl&J$V@)IZR!)P$5K$Y*XT2_2K8e+v)n$k>%DqYpY&ND zI!6`T_ucptn9AJY zs8{s@e2>M#FoO+Vfd$)db*@~Nps2^>E$;sN!c;T_$eS8Oqo?CPiH`s9J)nj!OyD44 zWL$ccFwcaMZWkElLfC>eZ-C_3P>trgb(UA{?J{KCtk3o!xj=O~Q_$wL&i#wXsxcEc zG#J`ic&uVLvQ0JRy%lkHHip1;|KGvG%6vwc=pq4ax~czkhr02RlDKQ~c{ zdPy4e9?M;HUSQ5SdP$-6dUeNS2GinJbH0Zsa>wK}B!y$i4XaI_$AD&sV9J>mI#r_V z+@Aa7>7P#Fet4>={k6O1e@+!utbz-^OHaGZWi-H{zU6+im}9(-+v`qU+rXw^R1m8) zk(4rPsn3u+yi?OzeJc2R>xTsa{6)=taX1$~~ z>Q-@TW=w6ui^)Ay@ys-N#KwzNv?`vwwEi7;{qOS^f8N6M9=wyABDj6q%`a&L;|bZe zL6wq{&J#5*dfqP|(vL-ER2JnW6`C7Ed5Iq%8b?|EHpKGH@>|1%+w=6(Pduk>ZkHa1 zg9OVO>q`!MT+65F%T0TX8)?huh`!&*N&VZttuRcF!KU+l)dLP-Mtp zT;ZR)9)H2G$4OQg*1m-`;@!cpJ|Fd%0wN#Sv_kw(JefZ8hxa zbL;k1ATTSrc@=+J=MAe!Jz)25g9$@eQgoaV?+6KEU?BeX!B+?84jF7Q8;^URi? zIp_C_!w~<`|if;knA@x&w1%T^l#L;#pF?Qbm9Hg4THZY zNfB@*5(B@5n*ZHT{OLab^AfTlCN&~i4K>yDYwP2$C?kA0%dh)?%Nw^*F-cz*q&x)`zt6{nE~ z#?J@7oMIPz?pz@+Hx#2!s-<%IG6my=-@hXzMMl2++Xq2p3b0c!taWd!dBXvA`rQt=l=c=za?W5R-$#SO9 zVdnd+Rzpa~-k0cnxK;Uu-%bY4S$>29k}4!cwMfk~r+ zdScmR>RvQmazci}X!P-5^v07rCYUEL``yT3b~63y7(dbPh|XP7lSS_iW6E<;d?<+V zkyuROn4ab7K{4cKiBbp`2iUfF{#o;UJSz0i69&|#Oge3vsfH{>(9#h zW_;k2S<8#8&Y!-AT4)7vhwN5U2*;K^m3Q=(Aih|Xr9F_I2oCp!wT2o556(_!uY=`c z?0l`rs>pP~^S#dz3O60p$d`HbScW5arNnPba-dxp404ShA9y@%`&6`Ta*g`HkO}vt zy&(MV4(#{UZiv4WGmd%g!GGs@z~Agm3~@nZ53&WazJ>{qVeW^Pob-^R@c#Gc~Zqdc+@o`)S7@jnsKkg@Ar2b`~^ z2#o0I`a082B$zx`4S#2U6sqLK;S;~MC@xd7o+ z&y2ecpem>IVn4b?T`oI*kbA3W=BM;Wu)Ct!S?N*oX}H6k%2gUP zEJO4{52}xzw-?uw9Z^2p%CzLtpjjAO61M|MD34_ZPom++q2CPqeMD@Oks$Fi+8|FYnO~SHE?I;ceB= zTjx~1@SDp$w4SRCDK}`B2U^=4VCIj1FAZVAF8dZq9 zgU>#AUWsn^cE*=j#uukelAt+#>$`CIu#Q#obr+dK$`t2uB1(RrXv9mJnA377Q(aE8~xRA3HZo^07i_#+OgDxWukKR)0Z@_=;bSW zt$+y{9DJt^f;M%%(jI-L@jz>vqwFlM_H?4nPO^FnNF~z-t<_WreJRovpCC6W`&P3=qnneA=wVWGOgkR#NsO_nCBKv?S1~Ol$FWg;?pfd;6gg^GbiG zz58Y>rfXrF%lm*eofA{8EA(-$u+n**+!gb@@gn;TIrgQ^@#3en64rmz5m`|r?n;C! z$`zDhB0N}zkzefEQP(bK*0B>LH3Ox3&wi339#g2@?;Y8 z7n+Emf$~>=f;Q`e2_EVE-&WAcdskBYj0O_q7U!#>irCAMJwPw0fjgh1y1UX8mY_-2B$(8w$stnLb5t5)&qCXJZpkDB7_k zoY_dH9q<_S^M(9+#xi|&b2~+4;xdDSZHwIuDDg*6QTrWI$uUxnI%!u1vJA0JuN6{{ z?XKKb#*m79Muy|MM6(QA(Cn%%BCFHTFEJD*(2liLbHA~C^_PohZub^7Z`+zBV`=Os7jPqap$()7_++>r zZ3=-fq>|X58n9I*ks)dSV#oBeWPFv#v{RwI#-$?t`>9%J}h^ep)nIzwZJ^h;% zn+`v_9mg?N0{>4d7WwXncXS;OC9y~ad4aghedRWuPGb`o!>7fVzb|QZQc&SIZcl~) zn7%9j?kAuZ^{JD0@aOv)OrBoI1*KV`N3{=CbM&^Ddk!rn#5*`@Wh0raA^LjX(XF^= zshsxol4*XkjhKQ}slvTXTD8>(^Ww6?tJdaJh-lIJ+s}r=6Tc}2${ko|k6wWt$xxQD zjQ8k<5IK!5XDGL`m6#JrdGbnPUD_Py(e1+Q!^w{-0zmQl2n^m7j6I5pi7`7&AaXn~ zV#TFT%H+>v9~YkU!=lWTy?VbstRr2f%`0+DTr5R5eI>fgpe;tE%~5btma6r#s5Rt4 zi2rj>%1~VS{T};ir#W-#QP&!?>9E?dg#P^h?_+h15iIj>^atrP8?jh4c0D$ZdeKML zmiiL3AM({P_4CQs@Q=frRjnvsRX2_26Ers^cCV6@@{7@xM8}llT#rbkAtzzYpJJaw z2l$@r#Lg(kM?PJSw6@fgHPou1VNYC@w2yA<88cuK7|fr%Ud16xfdcxXfMRVwo_*!SwAYbKTy5sv3=9`#-me0)!2gV`EH- zMujpE!xw>J&G+p_wQhhW!T%@vN<<}x#WX!Wx8rl)A4qqie*%)HAcN0iyY?L2_H)x} zPoxF;GfDU0cV~cCnE>XFg56s&Q&0!UO92}(Fs^1Ick&{WL)k#~jD)yuy=8KQqbu;M zP5_4440|YIUp%?iT89dTORpJ)g7f^3*wEXQ(h9L!hU~rX$=qHSjs-Inj5wbKz~X=R zDwOtb7Qa6n96)AEXr%pfH}~%{?$2j-!~MJE$V~hD`*Ca*>IlI(5|QIyy)jNOXdW0n z5+%L6_1VDiAH|C5xnR`m72NvbMiF6r`7_bZ_rfKWM9)z5K5f@+^E&aS%(3~TuDo6? z+>5eSz3oFny`_%4e!pH+=y4DC&fdWJDot#tvq^9@0STT$x&c%s2#q+8=U^jmX4z7% z4yu}CIn93By}Xs_Ey5yj8?oC~z#D#j#wQl1_>72BqoP}}f!NP%WXW%`YVqOG_P_{% zlg(r;w(&?p{Q8i3F4ni)U^In>O;IiiimlWUae{OH%$DGMuYCrF6t)bM9}O|qY2WYj zsqc6DFs|D1@KnC3rHSJ@j6~()sfYj-;{CpB_P9L*asReW%~M=d!3YZy$B+JH0v2+H z(M>e1ub8EI&p5>5no{k(&+0@T_AzU0Nw-|Y5#ot(xkuy=o)(CHZP}lA8TxcSsDs== zseH?VhF%AafN18vQx>8a#s?Tn#LKBd6Nlk6h&q=tb%WKsIH{oT)jc7{>Nby4bKz9F z4t+3ynN6tFacex~MbFrxptXoU+otJUEe$whrJ-FUEJY8u$V37a2ND>npsE>w{0_9{=ElRF{Ec0i)Oa?H3m3xmq(;FsfXRTV%4eKb|hN1_B#N=Sc*HC<#CY z0(-&SY#IwSh8p_Vanieu(8?fpPvH8v(7Np=uo(qGE1PkaNCZEffD@|OOOYgMg_QKD z!F(zdXwi~GYYSNl1(qOp_fZk0qBsdStV?sQ-}c3bl?Y#Fc%5i@S$P79M~YxC`x6`a z0^|9YhUj59q%j# zCS20isq<^)3lsacZJdrZ=RnOiX44(F=gg#KP30^;*JS)1GJWZDuzxm|XJ=(o|lbfktSSYo< zxsP2?)Ma5awi?MRh~Q2u?2k*W_lFNB>bKGk=Qg=Z?O|*lZH|}K?bBYCupQ6?WUVCo zA6yUU`SV={yWHegd|H-|-mX?zZw`Mx;C1}`riT)T!;Dlct}VzuBCUi`ZXVe~a#@o< z@wLgL0@*b>r3k~CsXF|(v3qkoLp1w8_4}7T3K)C0CJ&vLET$^1RQZKIIzttUD>qdc zE;Z4?eQ3A`1Z5ML>w=vf?J<-x<{i_0vD90@j?B`#a-wlR*f#iWplNyVw)tbxX@uxv z(GaX!5JR+z6Wfe}OPfpnE+ZFw#K^Aa!j3`)mh6(8ezImF@;)wd7#HnGJt5`^rw>Re zMOL!FG(?X+NqdmJxI0%D_AZ|5brzR^=e_7hh?1Ln)kY8CDKWL4=k^ZlZRqwyy|$!_ zz?~fh_DLdb=4~hZm_gc!iA?qVo_{cC)F&(|+8&Q0d!$qr%zcsdJP0H;W(-<)Oz5`z*>T2e zqjY~oK|3)3i*A1~%{k8nHfmPu_yvFa!kNH*x2quQOLrtw{zwi}zL$&wuU_QEqs`Z6 zBqT2B`ljOn<+`7wRZJpAbNMu!VaF*Gx7RMA+O0qD8>@^ROqZvKVScZVz7pIlxrE3d z#w^90tI%vmv^HlPXPV1_A?bM!NRBzS7xPs}xE*%yGTb`T$|^Q2iI|Pt__5v|Dm+)% z)xAhU6<2Sy?a;UluZAdH3_=Jt?!M|#xqa3_tau7<{BdE(9(^;bH1smkos&@LV?|h} zZ(Pv=A5IanM+73~5r!0#PRVZEaZ`O!b*C3!$Vz*9>~)$RanWsD1(XUU5#@MA z+0O4@ba&)3iu!+LdC*C#;a?1@@o0UoV>MdkTx3vy%V76{qk5u1Vdey5cDJa2{`58E zjZQhThhB?w9xk1_B3R94-vo6CXH7)o5oX(fT#VJYABR89oX25ZmWUaHpBLDbCUYdv z3;g*+Tl9e2P6G43A@Z|xP?K&++5vGio|vd81cLMt{YOt{r>Gn)y<*loyCoeRQbK;9 zyO+0;OJNNGo+pZ8gLeKq9*?_uo>)O6Dv>ga)k^YvVR%R6_e|SsAA^Q)8ad4SZz>G0 zUk%|b;|d7%0X4=`G+*C#wEVKMdB?Vl=d+rv7&p1NCzdT{=-{Y?+?_PEm@ zxQx2*K3%?juaw~R*|#9Iw!mS7Y1~95P^GJ-f18)j9X)4A$9c5Y&Tq@dC9$B4$62Ld z7hx+p$b78#bUA+t>hsZCq}W2#>@lqz4M&#yaKlb`>hXGdspGbzy|0i ziF4>TR^R`EvHUiEHIq~@CtjkQa*|aP<-`=qaiSUivhkEklhDeI-lZvDD;m3*y||qD z7evYEZi6?v27HmP3()dq)kt!Cy<=-p(lj1%H(!l=iEewxTtWqK9x$cFROn_`5=O_B zO6sp)*!%O<+blIxz{r?_9Z4shz*F7*1ZKu458UxN@U#iszHli$;0$?b+#eSrXpeAi z{-ZaVeT~bv*9%n0`QQ{!6--0u8_B5b$!!67m?{m{i;aak9mKs>q|@Ks*qydj-2p=U zp?W?2qe#&Bho>y8T&U03B;NP>1RFhESxsTH*aWq&oiv2U^Q@Ap1t1tQ5~~k2L@hAz zDs-E&LqZ~sfO9DTW31eDf29^AWJzeTKD`}GW?@H^G_dr&3MzC5!5R)vhW3(8GRDU4 zU!Lw&RNQfa<4%Xc4!$OGp zfB8M&fyah)nw%LP39@nuZBBocEq8!$&ai9FOmWyT1S&Q+x)ZLC!@8ky`Srp}SfF&F zkOIb6K@R)EI^T6Nvg|U>qlcvOr9QA!LIT5$FQY+o z_K29+T24KYw&K@Yw(}$=}I1_fuH6!vBh!X74o|fIzTOP6#@jAu@ zqPt089++>hO5E~D5iM^jY%u9&7hbv1TnxH3Q!MYWzUe@UlM(AQpSzHwRjGa=cjKD% z5s9`=^VmLS$Mla+vLZpcIi^?bMb~$y420H#GNf0hQb7rU#bl68jk|iv{|;n%J5K}D=drZrx*^C%&bnk2dj0EKa- zE@kk0X~&j#bab%ZLBsqBl3cbp4AIB#VCL!zS|n^T%#QYyhxXL?5_{#Cy<7pCl!G%aP2!}UNf z%61-k%mh@$v6yrk<+2YS=hkv$_MCuhfm~3T&x*z!c0G3Vea8i20>TBdQPz8-5rVcr zn(pXM0)r0w`(5w|+FCus9;|l~hlten!cq3_2%?htCu3+mkt8-}NeKYZWMUY@UHlt- z`$7!V7h4Thoh-jr?@OT19@)#u5CDd(ErXJZsN-?1lJ~V7MiVb~rh?GJvsw-k6ikRi zdH(v({jbJyA?2T|-9!jdgOx>ym2a!~0K15uW^cTi9z}bn60N|jMDEAoWI3GAj3D4U z+@R0v%W2QLG}riuxq6}f15Evrq5|Y0ZlCbsPWFV&BP~; zoq3<&`>mJUY@V&vVhf5)zrR0H^=ppf(U8aIcgJVYY4cr|zfB6o zV=&skllKI#>`R=pA9jD0XqePBBsw%|;`8+~>A-fwK?i zqS?g;Pwf2pB6RUet*)By;ru$Xcc+|>X|_6^h7!IAC#RiKUJ5LdnZ~+AHyO&N@kvi5uHTFCrzkl z4mqO{oh5QNz{KE9+fwDOYQ-0XKoX0DThXsb+SwPXt*7!StZ$xMfg&4GR z?3&_S$nl4B7T#_ZNgz?lh-aFdoi;g)nS8eaFh(!QVd?6aCm+H@re+&WOOF+DYB3Ef zV0<`S=GG_>Di4i0{_F=HTu5HUKj_mn#7!*LQQsMk^&Kh)32g+#nniaMA93{|fe48m+a zTb+kV7Qq%rry(DEo}Hb27Pa)U2pU420rxkIfvyn_8pHd*(Q4y?53N#CQmVk3&+UEH zIE~<=?|p7ev{tT=$v^o~1c|~1)CguEC1MiD&xrye6e=0ydFI|%a(JHE5{xHuErWhY zoPgaj3uL`16)EQp798DxCxcU*;1w~(bp@zAE*j}TP2)~FE+kkAU2boWC=EIS$1pN` zg_?RzvCUp+2E3;nIVed z2XO8d;{w@Hdp+Z}4^BWKM4~D%X#l03MN9GT@ z_mb=!b21eOZZrp;x^p5xYnHjWd}{g$oM#oNZ=N34@%YpX)EESkIN$8My|Li4`LW>+ zL=pq5)){JcQT8S03+E1nmmZIs+F4dwQH?vok}HE`hVz~HE{z~{WM+O4bXfr* zsNERimjA`VQA@1Bqkan;=yb@`1(r5>-#J?@i!jw&%IY;22dgk%HBiIM^qh~~rbHng z2h3s+l@87)m*&*p#mU>37h83>{~0!))DkzobE439t{!4rN`_k|E_YUHuieA#5- z;LjXZbFA=r4uE zEdjig^&1v4mL&LQ!Z=WrNPs{zx#4YuA367H5O=`le15RDN^e#V^db=-<8L0eVti)^ zZuo#*u+u6?gqqr##$_tC7)m~`ZM(|xEt5ftk$$X&pfetN+UmS zBLZvPCs=ns+id^X^YuX7yM90ieE{j3{;R_e99R3}-n0U(^fr*=s+*fRbX7NzNc7_d zw|qCPrAjD?xBCZ{Sp3yEep~B5fLx)05bXArwG>@idU&HOXp3oj;H3hf&fKC%`E1$m zm|xf!r+I}*$!Mav%Xfq^4KZsA_g#qNqh4_G)=QuSJ21jsG0e;$r;q{hW*C@o0aSz-Y*=JT!*O zK};E>&Tl??w_x3x_~_UW`YDJcc($NogFash`>jek z(q7JY3J-v99|)z=g2t4I_0=~`S|1a$6s|W$()v)hA2ze=R@|-UKsMT|Mq8JoQ#$AY3og$j>b8CMx;h5t5c1#cvK@ECvM)6+ME#$!Fa zWH2)B#jzX-wx$yOHh1=Bkl$D=<+(PHI5W0)_AItp&_9*UB7vXR{)c1=w-YU!)m$71 zQpk&Rf`O@UrCK6jgEH3HEbEhqOg0knIJ?S0Sms{~xDH@$3gRFdj?$A70oQ!c9QD~- zCU}Q0pU#chYY8HcvV();?Cb%M87DLg#4A04mfaUFy9-jF#LU-ivfsK`;gldp?6U!6 zSK8FuOJIW^|KxS)c*I|e+KHz_t&s7oRsi;OuUft+=~+I2MOL_bGHg`nF^u4r6W%&zCUJqbGBdBw+IsxMb*EHihuK+9E8$!_;lzcu2_!w&$G2x= zDmZ|dqa47=3I1t|&;N?IOlu$82|AmQVRho~tJ0#*x0Q(?VcHw)0(1fv_{Xnrr^{~w z?$co{>EZT|9zCT{PB$Tp@|hX-nk+i>z`>1=d_0u-`Ul%gjp1s&TD#o6ReQ(V$5>eTi7KCn7U@>M&m= z0yg{MBI%lW-pQzE*3G1+t_JPun5upFp_r$h_Gwks3}BGng_>Xi9fk|$iLw_{aahm# z<=BWLya(GL?;wWtJld6fz~h|*)4Q*ypRa=!O zW7qH#dCb$vEQGLj=Q+qOvIi0xVHS7>riDIckQTT;iWT8wvzX2Wksuh^Yq(7Mx*K+) z5QHss|8~pS>al#eRDjBeOVlIWK~uT{Lu{P&*cp&LMwh{8FuD5qHV+Qt;Zk98&3pV` zZkK&-)9rsKvh2&;*z6XF6?zsmiLt*6kTof_`$(WPEvLYJzkq~A z2DYMs_j%sQfNa)ef2j`|5w~b0|HX5FZzjP*2GUU8&I2^?3r&P-aBs-6=V3T}&q?~X z)e5U0p}=d+C~wSQl~uH=2Ytm%1pHJbpDf8tl5_?Z67^U#`aABg^HwwcdPs>2e?HKy z{9u>eBh}5xq70SRL%cJCjBGf<#&7>mmHB4{prqcQekVlB=-)UZ1^p=@ z3QHKdgKOX3*RV#cPKAveJ~k%3$#v3tUiE1{dE(P{o9zu&o!N9c7B==4bdQ>5&;kDj z9d^ikefE`V`^cTny-MwkX8r!{d`04zzXE@Od*p(GpMWdfl$*oN=BC3D%po18?-_QP z7FSsHTPd>C&)<&EphvcT>h6Xi#t#c7h};gJ9J4~*YEECw)$Ea6SWi76|lqpe1^O}gHBjnCu6?#1k#6!E#oHTn1z$qK4<(a*rU4mz5rZBrH?1rTTi z&b5iYV)2kmd-^V>Bq1$))*M*6vZTRG;?Tz*NhNXT z(Ns7YBkV8n<2t?AGFIAC#gn1z*reng`CxQ_JF_dBKNHI%b z8d#Q;eK3{@6A(a<@EwhcoCN9RCo<@W*z}C141l{`aeg4XP6SuZft?M z1EU*U+eF9#aH_+2C}k?2&^wZJ%MkM;9LKx4IzM(Zup~1l%0`8A)NnNW4bp=sT+A)> zyB!n~-yXi5cNc~bfINUt5n$JdgWS1^oBTBrz<0c7ykL5kEr~*0O%3NXNPJ5;f+Gy1 z^69x^@|fo57EZiom*X_P=ZpSYr;TD5Q5zi$ zEcY-Rv}q4j#R~oeDCBA&mg_6zw~rJjw>8C>ZJi?$X^jvXzPpyk<@$op6-kZE^P#nX ze8)^_JG|E;KiFfQq_=O|7>aZ^xbsLsn0zsvBF$d>$wcGz=#yr#MZFOK^*PR~^Wq^| zi-A>cJ-_|sSt|fMlfXrg8YW+dxUcgYi$GNN#Ml)qG|ZgPR)7e1(>@PLnwQU}&J5^E zf2zRE2FFtJ(%C|L5&4-u5ZN+)uZTHk@3EM^?Y)=)i_KNci9*vVVb;skDxC7{eY2_z zpMoC63$F=h^FwVG^ZD{7B?VQ#oyuFOe)XF5iisi>6Vu)-V%6K|IdShz-IiUvZ2?ny zTqgCTPswZv9jZ8QkfC30=19E95Zg{KTjfy+CZ{%*wmIHA?=C4=;N?b1B~+Y;-2d=I z0Xy(C=W?&a@(q&vhZEcP4HUOl{agN0kzRAY`bLKIk1kKvS|#D)je%d{4Os}(m0Mg- zpc^-rkxb_Q@%5HLRd!+jDBTUxjnWO$-3kgwcY_E>cXvvNbeD7^-CdH>EgiDyjvgN3u^jgAs&h_-cxz?mm#*T3}|D z45&lfw};|oaeGJO_OqEqMD%67T3r4SdxOW>3CJ71kc`Rcft1)Oxaza*k?9v9S#LP5 z03&g3UUfdaQOqNQMg?gXB0g=o{~mmIQd@QcvDMRlOfL#g_}_Jp$0c?iu$DeaVt>S2HBKCZtIJdhF#v2gLZ5{H zA9->Gjg1>L06@dZ^{zm4->EOM)JIye+fR|H!Q~Jh&^omD>P1aYnL%Yo75(`ceSbuz z_N2B`h^MwKA}S@nR$hG>8=!C8GGdvimS|N)1aMPCFSvyTI`#2?zJl1v7opPUx*{Oz z$jj)J|9_DvNN6w~@Nze_jDZa!A_xm%WF1iuakoaq=vK_?Wp{NCV(VgN7hS@!w8}qUdvE0 z)1riODNT08{TrfIf`mKdH3h3!)~tz%AwFs;*--eTr8+-5FoxmO-`hL6CU`aV$KGHn z2ln~XQ;`?9$+7GUHbtU~^G9k-L%a1I-IL&l-}+u)Gw}@W7SC4^&|qB?>j_bKQ36k< zR}PfWzq_q--dn4`w0*CHO1!M`s1LYnxJa~~N&}$?0v2^okb99M+tA1O4Z9gHt$Y&u8QC{>X`_W^hr~l6_ACZSD z-2l*HneaG@Pa$WIk;tAIxigGZyyDzOiqaD!s>kFJr2KO^h=5eze;+zPe|rW$ToLA~ zmHEYTtEJtTweKT^%!d1!a)qT;7-xahY-_aRJVq&f7R9Ag{l>`X>((Z3Bs#JuVq%%CuDO+d-_0 z_vn%+CBxO;WZ-f0A**(~mwUo*+Q1*YKuUL+K3H&0*}OW!)=`#+7SIU9w6J;1 zgN`rMC!jkgGz`4Wq$UMDub6*XFMPs<2F#*jW&2%;GJ@dhB4Ce_`iWnQ$6mjr{7X{g zQslOQbY^gb@#KT9L&DvFg?hm1zlgs9{i9@phmD<+cc@lTCbwhB{9@r@zcxTJV*AF% zr~EQ6?PK_pyo>ll9{<{Vp~UP)%aA-R?&;9UbtppeW#;Nf8g!)k2919iASNLBLfVvx z_mce&X+sR&DAaHGuU_7xcsGWZ+3WvcVvu0`#SLYhsQcaoO%5b&?ujw~#nPmHVkT?T6ilxx^V`*y#u<%^Z>c+%m1dG|XLJUZ*xOJ^Nf)fJ6oagV#wV z_A2D$`={Yca#?s7hDwb}eV!B(V`C(2dl;(!b^7G<6eFwfeHQrou3+o@wM}egnM)(u z(uail#a??_J9}~q%N_*I8ZyjFYt2X04@82IV=`G5To+1@+80+XWDFW#akXMDOLTRf z=cOT+#L^sd#>J+^xvHnf4o*k&?-q-Zd}Dc=8=lqraBeS}4ayKpfYFIgqAZ;QpZ@{uY>AcwVl=|YfF(tm7A9p*U+<=3kO`6w7 zMM~ricHhXSAjY%{2=x2;20e?lF3GI2}D&HMQtN6cRFM4;Z=L-L_K z5GVJ2uulc$ZrBVMQlR_oZXd}Vzyw>y`Zg=rS1)sOxH}>ydH#$0=lSV=A6#u0OST=@ zJ~}6sK>p3cgY8}gu);dqHIZSU#{~i^o%<*kD9cJfR|-%YjHTCmQm2l(t4!JAT7z<?`K=+h+o%KPG1`-_jd+ zjK)_knVInJeapiUz`KdW3gh6b*tMkK1}&Z~7e^g5wA&tgz%EX<75Jir0JEnPo-hz4 zN0>cdkG{Ae3RZ)oyp!z`cdqFZq<60=zj+Pz-=Yi7)_CcfP5%8KuPlDSl3Ytw?Os{} zx7A{UYKb*6qh39qT7?VmjC*6qH;NGQ_|-hw=;<&_+R1idfIbVQ0}`CF?RM5m_3(+2 z0g`)gBQ4!(b@>VN{ioMK`_rK+90@UNYB_>DJkcT#p9LOU?IR;nEy9Ttxi}GpZ^WDr zF4aBBsxj|=QOq_s%CqTLXUZt1zne2=z7ty|dD3@f)TJC|60SJn>xRGm#r{No7s;tB zYOOq)-tW$3-Wt=Bt_Uoe=;N>dM^Za|usHk8w!9V?BWxDKKpd>}Jc$|5vUK`%Uwy;l zHF^Ce^Fc$egHckGPa2O^d_n(EI)!o0{>axDrjIQ{@)Y%_%2|SZ;&2gtCU$Vyc8N$) zOw(QTNl+Ey8_%nw$?*)Z0~`)|@FC>l4byyC$#DHCb<@MIkzavJ_Yo8|3rlI$n$Pt9 z0yjrq7*#vY6&n0(2GEj}dfF&6YSp~{^=0P0EDHDTOAFg)V3|mbO&0a_IcJEu2G4#r z5>T$8=>o-7!~}7kb08GrcgozyV5Uz$+bAh=_sX?uWs(NmrFK0*w!bCF*!{Rh;Z+^% ziY!$k=(HERAp-(a_g>`U4XYj_A@reWUDg2NB8t{>J$X{il|uf&4#F#IqyK!)qAQNN4_YyA-X6o z^2;c(w2^}FjBG)NR zRPoj8gR(L;%|HHpsiDC>VgLkhA2WrDHG)RIYFAIIP>f}XDt6+R)&>_t!`W_wFU&(J zTg)FuB;k5?wLIMbkgJn-FV~O@4jVkQE!NG~C20?%9c;a=82#;HhshtWYT71E#yoev z!1S%i{mgaeEZr@o2g&Z^dO)) zh#Zxi(?!c3o(H2c8E4F}(R2)>PFkLeV5DA9hF_p>FU6S*Flda)G2$hx73+;bMx*VQ zOXdpeW({`lGgNJe<&JNYB>_I*+dk|!FJujrz!ZNGa&WI?`)EKsT2ql}n`|M&tS zDE$5O#(+yh`2dELU_hTDo1eamx~<>dSaBMOWUm0j(< z_oKt=yA7rCcp-vx?$1ydCX)ipcaEU1i%+kypMq=OzrlWc_!X$1|NewAaQ{v2PKMGH z$x~t0gfF;mK+bgZ9~kIlz?CplnrGz9;k3BQ-1Uv5H1T(qr~#(2`_yR!hD90e6=?Z5 z7@G!S$X}dhE^VI()Qx&dteyrz%-=iEolvmS;yN9SN>ImGcBWqN{uMF1Aryeq$1N^x z+i!pBgOu?A9-BoaFv=~P)$&u2kjMj@MXuBvFLep>iYtpOOeR>Nm#Z8|>{`5TYeXVliZ!dL+a9l$R7=%a zT5!Ij{pUJ|i?FPeMDnzZsn!HT*Eg|jB+t-sy+V<-tZa0|im4_mBYK@Sgel8n5^CTiD?UGh5pQ4j99yy$18V zrIt6UvGK&}GQg_pt~#5f0H#EZzt->VFGYSxz$=Fef6zJKaE=Jq6xlaYoM*+;4!)$e z+}rU=ADXNCMLJsjVxIEMBY6L#iX0>dtnS$-t&L$EZD1{34*8aTO#aUh471$?e)?prO7 z=={N;a-n+z1{5B|3Cf06sNAI~hOBU*>b{}8jhnWd<>e9gzSi`KT%fK|cGGOkW9AvCS~RtmC=*E(me|*rLsx$j^`VaRTorTa zH%?|SXRt+X{?_XV`Ka3JZRRes^bE{id#WRN80kqWq@3*jb9<Cp^?)@Wg0Pv>aBnI2qt&wJhjp zc~4Ej99Qg$l`BPAO=ejQty~7sGXt;hB$GrUquP5+j3y=%k(f0y^>`^$L`C@Cgva~q z!^39Nq3^5&xPc$JCWCFVV|Y2Ic&Saz9o<8 z#3DBJm0tuGM0p)6D*Rx7erq`;HFsWG*`5oMXA2l364WQ7!HYA4itCXln!q^?ePIP_ z!7R6{@X+LYPWbx;>!Pc997_NY~!%h4y&~_`*Fl0nvTT6F!?-$qyZhlMlzX9TmLZWJNlZ zi&OFK7zP!-8uOmhAD=Fq4Ow@A-%1WCq6#!9RNi6f-9q;H-SIVJ9WE-`+9J-OFf;$A zCEiXXAyG3`s2SXUY-@6DDvjiQTpx_wrKop%a-ZIfv_QUln+L`^2!a%b#^bN(V^6o^ zT*y*VyYxj`-ZSZARy4I!7QdqmqKfz$*WGu$%lPJ=KTkq)eR{stbmKY|p>60ImRNS| zyQJOi$>;Y0OmbpK60m=#^54ae9$ffjuO%FzHC|fhMH@Sj*i|rflYcy5IS$s;#SW6v2nY+HtXu_`EJm#n+Q{gFUGh7!-bX|)bURKV z?3++{1bEr4RP3i}&EkY$K0rw0@xGq`a_ zHURmT%;$|AIr;Rb)db!=XX!fuX1u~c|7BH9VygSw_i}DuVeu<$njHg+=TA`Tb}&TL z7{?1-o)Cjpw>Yd{Sh!FpFsK=B^r@m47^HmJbRz3Nqr;6Tx2aMo&(@`FKznK3ml;MI zPvF<{P}8iXp@fDZK~m=rc?3|@-bKC5QW41RZB~ItG#^bC2dT2tyIYEeSaMd~s|)jv ze|C3$z7#Q~c&HxATyHtHr&7r|{F`%{eRZgkN0%_N`Lum#{2=XdKd{<7dV(mLU37HI zK*iwiVZHZ`azj|70$Et?;9Zg-S)11n1RLU#^NX@4{FQ_Tp?~}xNg{V9Q6mmsHMbcb z_D{cC%Io5HljpJMh*NG-uCp20UKjI~Tu%B#C~viTqFNLp|Bn3h`#V40K3^0@agk>m z0a;-=EgqTl+5?Og&@5fq9hj1i*s42pip65KnpMh~@?}-J?O}uIz=gx$}Z5=wJeZiNWS9_c7(i7GHL5cy=T z@N==%y_Q1@3sxHpoHI;5X>=||5^x70(uBIm!`Z%`U%-4Ddu*_k8f_yYe!o!akoTsDfj7}5|vkvLb+4p8WhUcS9Z z4`u2WYr$iYX{&|b`$5rafZwD!?#!IZ&J)%UO z-Fqu`nxl<0v^B$*YyoUD*fn#|O-bqfa5|vxn|6ElNIYt5W@|&`%YG*?+To$tVfEW= z!ot5Dee}7T_c~WPavn`rOGiB|TH(ax_?phyy5aqi|w;z`jrN>7P z$?kY{{Sy%UhhYk1N2CHya=YTsHBB)D-s@m}m=qUeuUyif(D;Jk(7ij^p$#-}!fb?& zUBYihS^-1UJWg?qGWrVGe95lOMj6bzbpL^AQ~mml3F!z$mp8>GT5|k1o_<1*7kb{h zeIMP1p#ia7`b`dR2ns+%!(r1^ z<{83?7tBY^QEZcRx1G=cfJ57r!&M+0zj7NN@J6_1jOto!3IF)@=>DuR-i|EB4JdWUU*hf!9Q>H*gX!jG16_HYx@C z-c1cb>-*&Hg%pwJyF9U1)N_o9Z0|=hFMc zSD2Bh;mvq6wmG!CXYx+KC#a%rDd792@eAVMQT-I?VR-Rk>@_9clUT#WSF9npP|03{ znQS?_=i+0dZ8H4z!2*VQ0<2Ey8M6c8Slwcb2)Z2e;q7!~_^4;t0vF=5owab0vPRiJ z^Yx48@e?H+5TH(Nuh+>VdI>R-Ru)gH)>cxA_^AW2(emP3(sHYoL|A)G;=T;-Y|dEy(w((5~IDE6w|dq=6qO;RB>X z{JEk;Pu7X<7&!=S@y~nd6d2cwJouvh%YaSON|c>2K&qpVTo=%8DK!i<5|QL`Ql?Gdcj;gL6y-RdQ>;UM zr$P8QdDV#E_l6cbTah4$o4LlE?Wyh!?vJmizhrjI&zVsR< zt@dUb+260!>@aZIn7C_Odud`682Cc>;O29>H;KE(AWI)-nvyB=Wfvz|>$?#_sICV5 zMl&n~PfGNr6AF z+7lSnB7j>#W-szedY$d=NLeE+k7s8y^D5AU6xO+(AlCLoA0~@~)&hTYLk0_1j?%7= z+K+Ic=fUB;QTpX-z*vn^@jB^3^r#T*cVS2L+UO4UP#E;FhMB|cx1E?fma7V*!k3fI z5+2map#;Of0@H@bZ<8a+Lw`PVw#EwfS#;=de27!#d+l=`c5--MyR3de=6(QDC&O2PJ<) z9L&p9jk(%bjqgCr){(z;5`VY3$9qP$k@k+X==6?{<;NrPSsm#f#&jqbx^C(HWulS| zW--~0G>~7D8&OqBu|E{BzkJHSTeE7y=uit1H5f6sSFxxh`wl}DF6QC=AB@QTqxG$ty1B2U$O@u0?#{6tUJkRcGmp`E^jiE;ksFj>ioOBgWIH=_I= zPPZHy{K}N%?U(!y=hc8lR{~u8B?sC`z*b-v7A;vpLe`asS+EBxHNZX$E0m7*v}xYH6~iJAn~%~Yr)vNBRWw-PBMgBB??3T zcKh0!=|qM3o_xHzw6^Rq%~(v>@04pmzdb1useg0~VCzX-y;0Or|05(AYTDrSJ04t^ zNyKQ8uevRrTfgS?uor+%ATFf{HCWf4ay;{Kl14CDHo6)jI%H=`(iV9lea^F#kH~{kDUV?3gYY#l(kM$7PK|r%rLgK z%B~0Xt3GwG19;t5NPDiQ5pCfh-N{by`t!VGH*~>D;dyc8QNW)+YKpOn*Gb1xPgHi# z!DhME^?7mK#PA3032uwvUl3$tBK0C-y`R`M!Z)~ge%wDqlF~6Y@|6YPO&Z%=2NCXh zJKI0J!FG%beku?AidRz6$cb_Wq41VI_{jt2Q=6W2gP$n1a%IZH^e^q0OvgS0&Lt3 zJH;GnIQlLqSyUst>7AsuQecMFLb#TzH9R%G2o3NGJ!Ky)m+2L?=-y(ABFnCP1@npP zZp9uGdI;6osEdF3yt*n=^f6gTzsuv_eVP4o(k{y(mn{da-|h?`^DbMR&iT*VI~pCZ z2;l;tbAB`X`)CAwwH;t!H}|vP#AgXwZtuD4 z*}Qu{!l72b4^huVeWL!%FutiJx3Mxg>4twyBBZ{#KaX|+AK2h;Px=5mNhE}N?oDtd zPKf9WbJ;!rsw1Xb)U(T$^jgu*lKt4cgFT!sNO*6{f~f!AtxL^QC%vB#DifIy7uIro z#TL`Kd!95#wk9*G9Ua8*5KGB<5O)ocoo2L3zPpA?dv90R1&3^ z!UkeY8ly-u>?<-17vVQY{{HB+JnH5=p?hX))qN3cZbN?ACP<|OZq!9w`g~MZ87Ozz zmzdx@fuV(Fv7FLLX#WHZO(o3M zFe7KUOuU|5w-Ma*I37jg9z>i<(;5I3G3qw56I3r4a}tXvfuG;ExI)^u$q{%A;rQYb zgM?|@5=QkTuy;_VZ0UHXAMxHY+(${KA+kP5 z($squjiXm`1?4(O?R#J8A$ejL#Rmd@6AKCs1dCAaf75hKttXGn*(9)4x-GI+Emg%J zesH2>%PtbI(M!uQJLfi3o1s@jmG ztw{PB$o~0colA@RRlwm}J^8nsNJc$wMSL?}!M3$3tQEaM`wUV6Dn!uTw!h&m%F^*> z!tbnt4RW`C8%m1Qy;>PsF-b=tYAFYc&F$=!c7*CCMKF)SAAe$MlBq&|^xN^Ad`co!4ln^Fb-Ck`3 zrs$eNK1isep@ahV4kw!8rfV9)bB-w77V3qk;rh@r#2;D) zOBJgRHC}n@eU-ple7kNx287=CiOj_xzL5*3ijDTb;p_iE!R4ts$aH`614XHW6;w$GR4B^A)Zo!>`rW{#QU2n`iG5<<3vO3FeTYz>i4Tp`P{b)PMJK!^oP~t5DV+r>3!mmLOdluuPo(#hy*nIJ*;B|`Q+THBmRC4#|w-bIW##!Ts=20C2gm6@UV^Zy5upZiCqjf$cAr!xyk2cT%kZ((&NTFvHN7}|cR+`f9{J&?FV1&* zMn}Q~7%K=qKI%J~lrQ)ni4+x>_)Z?fo3xS}A@mXWz8y5$ICrGW-jers{~=2T*6?|i zJxLI4T>?tAza)6xqCiU<7QGV$F&Rabf}@x1jvNDc&LjC%fbd2k`@NT3+HQ5u zT5qc+<`|7J&R}L(4PwU5&~*F7;oZ{QhOJKX!Qsi}9pcC6-Xs?kQv*aViLE-21+;(I z+iI9Z;|uGo0Mtx!0tX`>W3aC#C_-*4#_rZ848r+;YVM(6$7p&rx!P;pmsdWnFT3s) zVWtmNQu<`GEj&DcAR!VKkS;NIZbCT_|6uyR&z@s$ z7qb1m?JkN14r_-{rk%h*ujp&CXsho-VS)g6arHM_%95k8a5R zhP^p~D}DM)^NPup<~7;K2FKGcswiWLiC zza1fQKP+!>3PmH_=p5GRl7G7Tw{*$48s=X0Bp7v;lnW%V`C5(rx-P|z~w^H;+mWWS9l_Ygv4A39M$72XwkZd#2|6LuP8&Qf| z;;a>DTd@~POdaQwKpgL1BHe|PBbhc}l&Sn<2jU1HYJT4!<%eEB);%POp$@mSw;mSm z8q{K%>W1}w0(%@b z{0Q;RTx|8!Oqq6eV<=Grt&g(*PqE53YQk9f>fU9{VCeT3O-2)_k(T~g(c+p|s_e-m z)^Rg*EMYAznSp>20O$9Nvp=+L8KGr|AHlF;6@KZ@fxf;teiX7XBc$|8gp^AEPwA6YrDUPYBIMJ5XyeE_Cvqi33F5-_J1dLx3Foa-BGL z-9C$c3|jJnB5<@17rPOyo-g{{r>h{5OnQH4Rt3UyQ@cn4F~`{?bO~ZwEBD3FZ{b*) zWf-L?XWiE3YvNyf;CPuk2jT_X0>WszX@`yNu$kT@0k<~hM6C!owQ;-28w3|BC!e$)7McH&^}ndR~WtXoi`c$3^!4H$KGnf)8zs zxUwvEwnHp{lgLAy1ejT+CL~`AOSQuk_vlY#1^DjGNj?8$g5JmQW=<}Odbn~p{VQ#9 zOn!$X_GEfmU@*7Vjsd<~HG|N~Inhm{j?01!n3cJ%N3JmKK>9CTSo?=n*`wnD7o$2YL&_9v*Gc}3k?p4cGx1N(>Swk1*fHOQ1W#AVk?eFYq8<~RUx51 zC`R)>zhuWTem{PTE_Iy03I4<6wn^sZl zV8o3b_Z42@fS`NLKsROES{$Ro9QC=cA^gf<>LhRN*@_S$ZxgaHGCBQJG57u?+N2Ol ztk<`OiV0f*8|sY}ci66&pOiJPc!z9xp$Xy=$|i}Dk{Rv2KK=6aDGcO6>`=9BkoT+P z9F144VZ#^TerY-`B8!RF1^?K~5xwyXN=8FPSdyTH<2t?Bta-v}^pG~eL36Z|6vKRR zDJ8Vo+j-tnE;X%Aa?HMfs}jgW6wLhQv3t(d;oLZLtUGosB@MhVb{EwIV4ud)!*1zH zU@HTV!<|s=SfiY#M&OFSsX(E6BkdMd4ZRapVhEioeuq2o9V?DI03Fo@|Buu{vXFa? zR`zBxlw7RH4Go#BX;w|L4E_%PS)R?hJmONnefm`F0P6L2py4Vfj(U@>5}T`ty)Nz%MyVptqF=DjzIf{;yAv&jf)hfkHbrVSV^%}FeVvTNOcYxt$ z4!PXQ{*Z>c5Fa}voHdrvfnUa+a@XG0Sm>nJkjZ1&@KzHh!Kf{3+!mRX-i0!>k&FwfL$vp2*S>;8`WC6KoieSy>FymB;|%7V zTq;{Wh<`9nKl0BFWzUTab-xdp+s(M{k6dGMGBbzvte`LHizW`f|IO%;Kann>$8EZB=KTA_UGd4#6Ot08vMamI<={jFj{+A+ZM!pbQHoL$Yd*p3C zG2shQs9-2gh8$b*0qKi2Kr^jgko9mOaHOlflnYnnMD6maISFs49F~6*Ns0a3e5xBZ zN?6?gsbsgJ|7Z5#u9VUnVx~KZno4(?gnRd&j;h(M0%*PPWYYFt5?Sx@MuE3K<#X?o z&jCk2tDh^MQvoNtm3uv(3e*`RD=SaP7hBV7pu8grY*{542W6YZHLD(n!2#EH^ORv) zN-l!GOF}jgx_;XkD@fOcJ`oZH6QRF7C-ty!x|;x^w6D}(*DfDx~f%SL=?zl&@ zc+!dC$G_VA7`V~SeGEH2tc_(v_UXzVgBsKD@SPYN7*DC6GQQg+ef;c22I=yM^COoJ z{MP?J@O`m3BAXuSLvyrxi zjG7SGl7((?Ac5hVX2eDnk$U2Kw)QcxO&hI*n&|DC7Vd9@Z8ZO_YrRiU9!F~>UJtvM zjU~Ely{<9n4(RKsKY zA^4&1*E}fkj$K<(btG)o|EhN)+i$lD%+$c^B=5vdzdv1&tlXy~4H+t`0nB7tnby-` z3Tb>f(!_hEs$YB_E7s0r3qm7K&Cz_Dq6y5KLWR1A0u$cQcH(PDNJUu9Hl>GhQ7FD4 z!ei=v&UE3TAUfp}E1%XxV+^M&{0ypK(nrjS1d#c&nra?W}n0Qaju;%A`>28KV)y-Gt8AI$My=jVjy!C&ORL9ZcRHNySW2B3P_NgIK5KusjBv0T5OKnG!S9{pFL z|KhR}*F|1Tx;}@+N1aCAMU1&v>_`10Hlre|8ecNVEwl32R-MeyY{YU#H0DHu zy>fRy6oHbpRYehothnq=bkXNJ+}?LH9QnawuusMX;D@eW*(*>5vtWFcy6P;3|G~eT zGU_hQ!NW=oeUddpRe7h zXIEK}VSSP#s^J~sa@-03#x-yT#Y&8ZVux2VIY!|*5i5mX`4{~I~?9BlqbE5x?4 z?Kd~(?o+~l+1bGptKwq4-*$8i8<5Oy#V^9ZY1Z}r_GYe6LT@ad3ew-Rqy`1LLOprO zR`n71p^(^E?EIXc2|1K$1+s9W{YNte+bxpVvWOaov4NJMkezTwU1PdEV z+0R0f*q)odnbwQr%aklSQt&x~vJ7;3(do3+pWi>(><SL{tLnDOsBE78Dz;kHje$wKblMZ^aW7dEW6+EH32*u1Lh;ZZ2R0@1e?neg??R-o zj*5IS>SspRz0r9+9@CV*8)TwM8u1rY?%nEe!N%H4@wo#smf^FQxd1>{&QTbEN}s?{wSmXWsbFF(c7Tht$=sh~VTQ9&Y@ zWM%7@9prJ#gyi;8`$W_dDar|U?|;Rv4cjki>oiBxul-{5T#F=6D+5UtOvqbJKV&Y4j`Gj?-5+|xmE&AJhzTn@D*3MRf;rkAhEmht)SjbzuKu{iWu6 z@;1b5KCl7iuoa5_Na$w8IMTQxN^*D(b6)aV5hf;%MwXa;-5-(s^FvJFB2;6SSJ)~( z*;l+1gmVfp>D7C<7L|1ktmkaUy=>@8b_ys;D$N3O;WxEuch)Mr$M-x%E7s7S%3B`& zL>9@E>975v%Z>|lD-+hq20r>jZYE_ZY`$M${lU{g(y~JSIlu?h+HJoZd!w;}xmBoB zoB}zwWHywGIIr4bDc&ij@%(eC3&+FnIv{Kef;t%_s6)s4F{KH}G8yYbcQW zm(8KM@(G^iWT8ttufuPQ!$gOp(S@-`z|K!c5uU*1_vgM2tAReM)}%$@275hSUTfzh z3?dAz{8Wns5Rh96ks!#@sGJhWm2$MvP)WdJPJP5XEo~-M8qW#NS)oSwl9{caE zZOo=TrGZ~tX?wH!ljcR?#ae5n^_+{&bXT$b&!H-|iXZ*XBqL!fUt~Nah(`=H*D;9cxy^|Gf%l8u=wr%~Y2q|#5zjs|#^eAt! zZ*siltMY5}lAAfJ!_jQPIf1!a750=|$dlEgCHSXR92j-z)5k`;xm-`e{k@V@$p9q6kM7CsolEy z-0MZptGY3Vt;B7Wb|zdg?~ZXDs@;DJ+94FPW9d)k@2eG@YZ%{FXvkkJ78T<=H<+$k z@m|nreX7y^xY%AGRPHJwMz^@b$BrclsjW+9$UOqsJe@S=M?dn76QJ9Yyy_FgN?)hzG% zfzq1AkWH>x?Rq(xsNmynWd7QyREY3zr`QG?&gsmds)i57dNNl`Xz@_-?2BK2&8Tm~ zmBd9}zV;7c|Acc(0wWdb|E2af&oniWZ8*#a?$2lirVl98exigbRGq4Ozo6<)p5UC| z=)G*>UdHZLQO0UtQUYoT`ybVKJRc|cjrn9MBRE2;qs5WdmpYy<*`W6l#@MsQlyma) zdrjLmd6D*hKYOAcrd_wrogxpwJNI^mL#@!xLI3$4_kGQUkim{+)Lr2)?h=QF8iVrT zSZ*nTvhxu06&E&x%g2zkFBWNiQl8i*1#N-Qm5a)mxrZNyF1!(MrkX?JoYy1*@@lIud6c`^9etz7_RzSK*&R=5UdumjGmQoDp4C%JWbsICELg1i~7g@q2$!dv^8PvussoZ^?R z=Dq#lKzZTgV4cs;?alhUODCNom(6;WJ)bY0{EPK2e-iYxe_*n!3!4wPxyC_Rmc@}R z&O{GT25O@SQ7WN$G*C(qAgnV+R4S!I<|KTW25d?vnlZdFlsl|`2`$lQ6Tr6x-=g63 zlG%}oPl!C-n6TZ~-J23pB@?ZaLUHVgKs7+^oT&WtSkc`RsBur+cOtXz?=YZU-L%eB zsc<5@kQHy)cXC>7x2UysCPUtb->>OdD#>M&=cyU9SOEbW#GND_8h$N%zf_ceG%=}p zKmT&_uyY#RbB4~^KtyAok%!`((983r`%FS9C>Qt)#3ef9?pxT7tka?>^RP}p8}sI( z?*=JP2kxOu7;Zm?p|bO!3d(jE*VjB?qbn(&c3Ba0uKecTNmdAhO7QC$#6aA`M1ei( z^Ik)^q*LkCCvD2@k$Lj_^C*@A4&cUtMi-P!ip^IoaCdk|AAmQNSIgd==YnEM8t>D+ zd@~1LutxjH{I!J7YlV7+SswkDwn(9?c@y)@lT5{iEXCyzpGc_pefR`Y2O~1i&^KoC zcO)MMLN7TXd^`WxsheJ%vlxobTKJ(n!ePQ_87lV8b!IKZHl>T@PZ))gfqmBpTD{){ zb+I%`@&DrLEu*3g+jij@hVGJ(l^~iE^72u``JKSIyO92Bn9+Tc@G=1?!{9x z=tSz~S-0M*j_4u`Re$SlC%^W=(`t;G$F*PtLyZsHNNpJO+&+@^9!fw};^)P`(+^SM zcQ*fB-()K}+D#*z9RZ81T!}c3vBiglTBrV6mZTP|(4@6T*`wMsZ-veF!AWJYk`ns8 zNFP3OEqcqsAb~3@#}%jRf0!S5HRQtWXFE{dUwPggmYJs1cqCNYzL%>#Aj2L%n*Liq zJ=vB#=o?}`rk`<0v{0Ajb)kQrAlW*7tHD5esYi!t-`}OXE4NOiHC%cgkZU>;q(IAM zY`>Yx+&?1`Zc*h1NJPW?&+qC~jnhLOxeI(-&CRyQ8e3;2cKrKJ`tke*f=hM= zJeVh}$zGA2wfu4dppCgg480 zCwA^S8zY{B><=2*JBWTw?Z-h=X3gx=JGYN-tJ0Sj3cJiBu*pWQqtw%ebWHc~*9oa! z1iL~_5bf}8VOFXtMOCk_F+Gjy9}`Vr2?r5 zAta*Utm9PN954j+Teu~*JSk${ZQfS96qA-O!Ui8M)&H?pWRBheav32-A;y6)f-ZXP zmUQ?@IMPU|T0J;X$~kJ$t~__@c7#~r%Tu*-XT|*;Q4`evD60}l_dVRI2rlLaTUsqO zXzGs?SlWuBXvv>_9UCdoSZRni|8oc;2l+Au7LQPJq@<`RsXrAsr1dS1cmXQDW)f-J z)awZPYY&D6#a7woIVmELF4#7m!{&<_^Q_IIAaiQF=<3h*O?*w!@g<1wwshyzIlg1N%9KNKZq8F!npny z(0KRJAO!Q}u)8hsXgE@PZ?No=(W`&$2ozoOksg1;^AdAVxI6M76 z<+z{qW9sfEvG%m%+^2#RM~C$U?dL{aS*;rD@x!Hr76<*ge!xV0L^nWiBey39KOEJS8GMDz1NnvPhy$nYI)t0?8pj>?D#6oh zqAo>wXBIJ(^MOyZ*oFOAkQp95QM?3gdgA0VqiyyGZqAd<%4o^u&5sajHhA&7%)=Iv z+GyK7z*&O-M%85txitV74Nm|tT-%x#dBx(` z(LXanSg+91`i6__mCG1A{vI&SBnPJdAl^Iims_Qebf=vBs!#x34b=Fo{b%|x@2!d{^OydwhD~MxQr*9#$2!amw|`R^uc3v5$!epQR2WK|0JzOj^++| zzP(Q`|5ljw1m`bP@!KSmcP@oxeiU0`AzpH8Ccn^8ueD!Z5_Hw*QIplTG$BgRX$F5| zu;9RY^ESqkUYqi{dfbZE8e<6g-lDP!7NVcV8R8XFNO_?oYl(fP zV)4FHXO-8WJu#8Lo}QZ{m*zX>dib_c-+w_8&tIPonuL}I_Lr9!hjsuL*{AcCv10l}lM*|bsq#zjR@+|>*aBsc) zZwfw~-XC_hn_>?mxX*a^UxW9?hJ^$CVt%XEir7zOOF@<6MZ{C+_fw}6B+9H+ysp2oo!%bKV&FjX`TLWi6r9Nb_ z_%d~)u@jU~VSUftH*+dQ7>YHs>(D2NCg1uNcd@yl*~6=TY@?}YzV z{u5tS+t3ll$$hm26Bg&}$If1)iw*|Onh|zmC`V_;Qo4j@3UyEG0f+QM-D^5Hp+=gc zYCHP(;lkqmZIV@%*o{$tl-CNAPDQmWJC1ML5qsJ*1wTWiKoIyF8npV$o#L{7z~&D5G82@HE{7dZ3eiiOfs?mY#ekO5{Lrl~ykB_1T@^ zAi$>p zFe>t>&+?(0>j-EusUHiztMC-sHwas8Ug|42?VnV@TWZ+-=5OW_;3#_~2G^U7=e&re z=d8Lz!g^t12*HPvmW&v<5tmn**>%<)^pcWyji#nmk4xr3SbXZePn|)6=JyB9!I;EL zdzsRv{Xl(fp)HF`!M-;2^VOxy3jq}$OJvgW=~1MMg`V^UjhHtUkiDm1)~(sQ`*Z4^ zYhTW_I;VwN%#vO8l9u&B>#M`-Y<}L;w%BpG z02s5Oktp%sMY7#<`mI zOe>M->a+swC^7~IH1m~tdGo4xgvm}o|>28&n(9=91f#2m~(cS2Uu?+sP9W6dfyK3L~R~l*jqD-m_Swbe1F~H(Tg(f-k z1!C^btwPgO4hPelpsPSQt(YG!hBZ};+gY(kU5T>S9`>6)Tre$3hkFGX<+E_8q-qFw zg{6#02^gwj2MqRoocSY zo=6HSlG)~$Ivh6`o2+TPye~#KIebI629a1w^&NHoGNN1TkZl}7v3S#%!~xR*B7(qZ ztVryC&;g!Rq;dvTsH}=<`yya4l$CvtW64S!YA#I~%<5h;ut@W7FiHjS-SeoWW6-gE=G6Wqqo%# z-Rx_Cs>c6&h5I0b9^5L-ly6Ci5kj-kB&~NZzuJTt_=_6VhtDU z!&sVg5|7KybEZy{s;qt5WcM6}F}hIpSYr-;E$#M|$ho51S_VUQcDLZSS$a`6P>8#d z_ap1d;nn^W54l+<*+&unPQ&EDpBtVidnqiXLCq-4SM7#s26o@(#k|hA30iZWquedZ zJ_a6yV5^(VHs86Pk*!i5N<>t#T;*Jt0EcU)4mQ+xXEJ21qXVW#okv*7kN_4h4ql)2 z)NT%iUzaw)ErFCbBW}EVDC*+$#646>b1ISY*yuR)^+}^C*%ZI8R1sC>EKz^L{uO2g zv-qufW4|$MIR>2Ym9^xrg25M|8I<3y@mlDOnIi6GGh{RO7$Y3ExIW>vm@@@(5y`wn z?T${f3H(3u@D@e@u zUZQyW`954*LA>>!&V)zGmJ>FtQD$bfr*)P9`J#QMS77#9Y?+V?+Yf7oJtEDPJwX$R zq+DAlQz^|*#jy2*W!YEFiK#G7aL+}|v){n<+oC)Gv;S;6nTMw_AN64vX0&u)f4(|3 zQTsF+Ux)4sR&YfYPFu5#=XrMU=e@H&sf_`u#PHk}<<-GCY8?hl+_BXN>$-%pjuYBj z90Oe8mTNR5q1#`ER^SoQD;ufyb_jlx5s`jR%4iJVz#Ja00A&{Wvec4N`c$hF)oB8& zyMj2m(hSYoM%;yOB;9CfXL#AtVh(`5b9*fh^=dosmFQ!cUD#w2&<%- zq@yArUd>V;al~xpd&D3K`;^CQ~ z&Wco@^GWSzHx_J6kMP5l7Afv`H^sS!j9)M(GIvxbk;<*wy(aCGe^!CqNtipSB^-G7 z2&CF+)@h@vvxSccr6`%1>3E40m_r@hXum*w&V=QW6V5YMgQU((Lh;}Lp<)U)yV&y= z8BG`EPXnn_wG0Ce-DgR|kOaW)H3Z#t+(%S)y%>wLAM$}=?hXFm(Jz!5jbK2)wX;B| zwUjYnh}3|(cW*yc=QMrUzUAv@kGFiJF4PM?XI;lC?>Y25Dj*&BvW~UZY>TQ;M`B@uI70M^#l%Pl_teEybt4%1{oS|Abe`? z!$WB%L_iw0qRfRg`@H*St`S#LhtqPxDFkZpnMP%H4m&8s#}-Y#=(=#SE1&0xhT zPD%(^zA17hdQF&UqQ)1$?$vY`rkA_69+C^(Xx}i80qljDj;;n|W}@uRfKdTAy*RQ1 z0Oj=Jr+g)7l|Hv$Z-^pS#DJ`w7t;1*uN}42tqNUMi2}xe^eD6MCp!{3g5nw&l2g-0 zezv|zcj5`5f0Xi@0Gh&*?BUdDicLije#>mJb~jl_6^d-`(8f@zwy}`QminsvzscWm zR8d>iCRs*w5fiv?1aO+)`#YWlYSSE|5P1z)!T-SS(_OcSPW^XqdIhqa6`K8UNJkEy(tK{PWO5(1sJ|NVqcUP@a$`m}6Lz>e zUvf$ln70#!JMYZye@2+R+4U#Ip!vgRA=Pn;8jedQC8LJ#iSqE5lm?PbbDnK7t(#-? zxk#FS-GemQad)@jr2ktrOKS3VLr)EgTP=$aZp;}T+2E0&?bxGrdQC!j1;BTJ?mCA0 z(UM1GN4&5%lls5d4$H8%hkJdo0)PMk7+@rl8hP^+2va<^ctr!n^+hDDft$rhd594k z9O;W6Sf5tOT$uSy~bC-qB z*+X(KOkT{3vZq1nfH|97&Iw*R8mETsnVc0OAJP*jrzO0IDw|+BCi}yCg}

YqEQ%d;pe%*t=}z5;b*=8Vi(J=^Uhs=2_VQP}HyX@)wpnR)~eK_K>jb zaw}d5sTsTqzxP%@fh!m=OJ;QP=^+4e;_1S-Izu#g{rJ}%L@_QK;7}$Iuc!`_{rKh> zWI-62h~7{3`-OO5*cq|x^911uba1b7)ln=M|I7)Wz#^b}49Ky16@IOSHJ^2J1MZX! zcfw{=V558fD&>xLZAUGr#C6g$V6~ihkp2BHZYVlk$cpkCWYo6{ANtFXop5HF_UA>` zZiOTi%UlH#qf|tY-F%lSuTPeBALFMYKlIf!>03l75f73Y<1In&FMc*Z+)`9p-x{Q~ zo)h!#*^b&k$ZJ>CxwL7qW{PPEVI7JHMcN+1P|TU8m4t2He*v@7B#plX?CrcNa?NX= zz|pFzD4|{^Y+&;3H4IjR!v3~apIEikr>r5zW1)uwhw=wR0 zXv>e>`sv>02!?%+bxpF1T+i`y$7?yB_Px`Ks`smGc({R=RxWoB4L-!;DJN?CUFp^C z%&50gH*0+K0ZQjHm4sGq%l!D*LB}Ib^Wc_eWfH6d)Xj!ddiG+FDW&zUaFcLltt}(a zj0c}l(F%rq;1AkC@auu=I>A`uLTQ1345rVtIe?{H_m-3TV9pw|H2W~;l6)cZ&yLx! z&?K_bQx*OjbLkE#jj)!=5$(W#^=1mU!GGWJjYb00no$hNNR4fTh6d%cKiBB=B3Zn? zMpEB60agqT>QU9O@PMHDM+$&)m>NMTG!9<|6<%YAv4DwbDJ7r3EKI&nll=S*Ok5~6 z$r3H@4%n;$FBMflf+u>#MM_o~>9r)XC`7hbKgY|;UJNH@s=?WcPursmWuK^ov;ZAi zKcp9>oJF?MEbLeVK^FR`PHM&ozP@Wbu*tLlNKwuVa4|Rx@a3;Bo|qPoe{l;OvX7Qb z8+_8l!MlG29e`-5ftVLE{RSf&w3JDG zbjK;91MxZnx4krN-gSCP%_!3kOSa6q+9Ou|NhgMe2jZlZO9zgL`}N+<-tnSB7NtF+ zZl18))K#|ye$4hHe%h8yGN&JHTAB@w_i88Or!OJfj+|ICO6a+$cj?WeeZSB$x;(Gw z^ApGrhO=1;m!FmPr1k+@~q0~n%uQ4 z-tFDc-MPY@aNRmr?vdcd%+?23WLv_jBKt3M=#|$)$p1XnZ$8bVLO!Hg3D~fRS7$|A z=;Z7vmSCWyTJYv)Z?$V4(|b^)?A@(|0-iABe%3C3%rmf3FIUoWjPc5;otbWI%yW>B zFexP2{7la#2OGB9JU6eyS|MKo~cRK-|A9-@JU0+{eE!6Fxl z9j=~f>;JG}U!Q6vbvo72sWOQMdr+9uOuTi=L2gSSwB@lA-6T@4vR=hWJfgN|G zU%x;Ra>1cf#*0_7cau-!<#7CnE;RxfR(!S}yu~alFkbm?4q3i|oNvUgn zJTyH7?5m=&$;`fNSafs{%19_8OZ<>GwZ13BG%NyBkJGL4ubcli^!hC~L`oO}8l#iA2i1ZLRFqe;q1Sx8{xPn9<0iB-_* z1h<~I!>9EB9KPE8H-3{%GxXq!8)N4{D8G3~J-dfn4~rAJ zU-uL<7g1vpLD}11E>4NqS)MeSq-?udlos0fAu;&CSKyYIQ~TNEBJIOGrqk9 zB1IgDLhRjpZ(pUE8ebfmm3J!_t4J3Qr{gah_H%?#SHv83pr4jS$vhm#&HVZ`9}Q@6 zC>?GhtOXMC&#lRe^ta$s_HL59uGXY|_DLbrWfEJk_y*Te>XNnm&UtNc*c{iJSU7l3 z2<3K&#qACnd0SGHpF;3Wt~5&9_aTvZ*~y>yl?_Ku2CN${Ssj_h>4i=FSN3a{$5-(| zr7b1U42MDEAF@~PJ{@op)dJk}X$l{5(Z5tdpnn8Ig!jbc4GwN}NUUS%JRT-jZ>0rZ zBN)$|ubC)(cwd;fI&BHCdx&lhjPVcaoJ3)C6TSHt7e|P}OQxB~(vsA$9v&^ttRbEpl@J3UB$2EUK*s(ul;BnESBJKzQm@rQ?W$g72aoa0A!oWcaW%=vdx}UcU14M8tzgpHarK|KG5W0zP z!t6c&TgUS!T)U_qeN%QEYe!f~8*y*4Q`~rmVhS%~x2JZ52?#%14BWGs-z zLp3ru^Gs=wV3^rz z!;qDuRrC(U;3_~yM>%?Sf?4HGAQ}nsYnDMT-(Uv0%27H7Dy;oj5%wb&vm7?3#-p#S z__rBZs^`!4+!#6x@W;~i!544)-r0hBamAXGoLb(0aiZ{}He;Ch-n>p%T zAPY)ncM6+FxV=H>-C&lJaXYa6bN_0KLqU-uUXk<^(p{|x6nA9CcPq6L4(0z~Z)bbA zXr(x=a0AK{2=}JdW|S)Oq~84%*gf=P>Ul>}-ebNifNk2$+>)L56AHeUa%MJxi_y`p zT;+88v{;Mx%M(eMbRZM0qtYgEB~~lv&llo0KLH@acpMvd8Y~|tnglzpzXZuJxm>R& zQDl*an~)4WgiY_tBCvu;m9$Jvt)yjRB?H|k?e37(;|6`doy$3102k3yFvN&hC|7?y zZ&lm!YIln#oNe@vY@MkMavps0`sx{8 zoilDP{EF9aG}|fsgLiio<1J(svA19+w_UYev|)LQF_YtO-K`I8dlZ|ke%Dl3yU=`# zGhS^+R~aI-eQgGYQJc&dJmC(_@(8RM`qNPnD|?o6?_fk-nz9R;5cxK(eXvNBK=Yl| zwb=t^d=V};wK1wh`bC$~$;{04+nzpCfydyXT#c3wb_6JULP=}utp@7|l>tBV#8NlP z_xATHt>2rqrmXelm<#}d5%Vt8pyGp<;dS_+PW4;drC$x2ZQ%pj?hh_EwSBKvEw1|~ zH-y)-hLa-Wi?Fm60sLd#J*40J-M8iPI`E?}lNU^|sToJ!(5^{h`*)Wob%IZinxAPYS;p zH&ZO7a>^K{+U?xIF5f$y5>h=U_t+>E$1=btF&NkkQqF zU7h|dpiZ4&`b<3yrv9NM-G|GK(P0eoL-+``uBATR&;lp_4pAh`u-|%{&kVS5{KY;8 zB1C!(Vf*h?wTs)^Lq{XW$RuM3&C)(@Mc8B4h{2eUBDP!)`G4jh(Z0(@Uk#N@f1)lp z_tW$7bc@Z&poJ0W+a7FSu;E*3EviI<_;gU0 zpU7XHwq*7%eeukxpic{=1Yai!V#UqYI>wL|*?8ZXF`o8MY8%;R|M}-Vnx`~dN8n|g zromn4S#>gIJX3WhTp3%VM#a7b86nuL$)a^8TL_DaSW4jt6wdsM9WsP=q^G{pWRsD% zGv?VXYjjTZMr}iP52~gRNPujW5JlRT-j}}ZPaYh*?wVLNV&G$fG7_9>&BFfVQmqKHBEI@0B!^xW8 z5Qxq+A>(EpGqrp8U?3+Qb;EVEQ+fKMZTO-H>;+oj@wqvxZeM#_^UUHv@%-TUH0meX zwAU3o#E?0L;>NfJdVaWU6ke|f4@34!=FnJ;t1) zhUP8D!~!)@lZKAmCN~f_*fgvs=A&P*RlfR*rux2y8%X4BKE|?YrQk6Qq;#7xvC}|` z;!%r@{JQx@Ptn9V)(~%w&Exo+rYg_nE+^5Z&u&TCZ3Ad8FOGd5#NSoA>Bsz;2IwST z%?d_ajh&Hn)ab~&i{rjjECG=$0=*z9^i0nUa!(*DYhdPq zL+mescA^WiySBTo4|o*B&a`e)D(LG_QkTK!F|Ovd^*9?k2=|$0gs}goP-#r{JdR}i zy^kilBpV7i44Cy^qghhx-w_wR_ME-%3sBluxHALBIyU6HKOMRgfcaCb@4-Gbt?o*G z7>ZK7tTsu$rbDG+Nilr%09y9y>QJ}G|kG-4LKPluQY`LKkB_(Bt zOiYqF0YY*fl>srI2G5HM&lfi@B&qn}(j*}%_}waLAFhsi1cyVsLU5_Du}H`{ za&?X4DxWc5?tKgP;V9KHm+~e)${znVIgkVua8hS*8&wKc_KhG4Rak7&5n%T5^Kjb%f+rjF)xfG z%P6Ty4<}dh=A9Yqn^}=&;)gG2;z}?k!D(~}* z<`45z&e$k-e(QU!=KT*wnHh_g^k87?Iv5-9QU)+%sIJq`^UBA^GcemHoY?l4{ zy@bAI>X1S2B^Q>{O425vda{?KO_MWGRc9lfZRV?8_JwWFROCcZiM)TfTE#0eYHQ{B zVB2y&#YN#FL{@^Hxc2JfC$OufGl#;@2=P#r4Zxr+p~T3NKxB z!@I-=E5=BUfy1ba4s>exz5}YBRk~}$G4ALRkRM^`YyAg{uha}arEEu5KT41@!5H&= zOqhqo>o8H{F=qk_NUuU6Sd~`TlE3E1HlGCDl)E$= z0xsu*$qt!@9Xi55WV1770oJ4=Y+y_LjYf@8ZfaW%?z}Q3g)@&Iy&vxvzD)>-6M6Q! zMY;cuq{7hygS*hu{)s9)a!bz4!nlFit=TFMhB*Vfuk~k3!o(@eKjS5}nL`ytSUGER zi?B2tBc0DawLx2L^X|{p`c)3YHRot9eiZg_8I?uE-KHGzMBdDupSWGJChtCM7Ph$z zu?y6V%q^QQ->(bSl3&C~zx>&MI(=ECWO2y&qe+UGM-^HO(a*Qv|KgjYJSS` zNhF~(VArwRkIg_Xe!iKC1blrZb#=XN@Ox$3j&`rWAR7r!g z{JUX+d7G+o(+yGKL+*P;U_+a1>REQs5)(X6-hH}(=a{XN-E)xd9JS%l_h0VMYXe5z zCf?D<=Zhb6B9#-R&O`w;d@UejuJ{&UFg0q;y_KH+I{a*r{ghjL5$Nh;1E}h#<$E1i z(3iDLm(qx+ImY_H($r;cXRbpxm;S4_r>RqtUf+~nkvfWnz6D>fWNrhSTck1=meQZV z|7;6FOSqDe`Lw(F<-1|-6Rie_n=bDGGRX{ne%x*nklNiU*wCa&stiwkV3&SGCx>#g z{M~0-tt4H>`HI7!1yTGAkqZ}4&di<8fb%amPPi4artyCFA^J*%pG{1P<3hb7szAFy zK$26?yz#<6X!jNJvOf-#4g4j48UK{mnb<77A@LRMiW8F2IO+)^1VqErR+90Gy$!ac zYkqr52$weR5*;|iHZ4Jx*9&t8iKg84MQy4H=dHU8G*ZA=yPAUbx@XY0`3@;0y=i>T zVm;pj_Q}DX%O4kaYD=8{a~`pi;lA<(BtoEYlP=~R%zh#!YVqE04ot=!8itrG|%-FWU38s01)Rw!lUvB8FA_{|&P zL6zBpzE`*pkq5k|X>v&F*2m>(wc#yBq<2iyem*&W*nTn!9P;abpJdN0>RZO?K3!3R zCZY_G_V%Twv9!z|zQrxygdR#zm%o;ntsR;o2VW$XawI-_i_Y{dRVWTGWVhK& zvhBf|nQ%6l2Vg!;5dKCyhcPsByr#JXc`6DJ{kPE9*}Q=ICh4$)GV~5)PZ%cf|CHNt zdj4BeMIGHdWQ7aConuaK5~4VF6z+vzqdF9vU+#@*EeTc?W2LE6W6sPsSevDYx(D?o z_PmlL#!6zG;-NfTtSahrDwsVNL>p^y&d6lD^iB-zP`x$yFy&0iIMsXBrv05)Z2o18 z_N{BI-R`77&)p05W&Td)-o4+&3(n@CKM!th9tf;a+H)rT+6uj>h%e?OfuL(2=f7_( z{r>F{Zo8gCE&ZBQBLgD>MqLjFc_neH{NzQ*o87IQmmZ3|2wDDk86~vKqvNCWzkyo( zpRIEF7J1)SAA`jh`uW}{F|<>T+=&l((r|Docn7%a7nFKm zWK#PhJ|m{lmiQEWGb$Erso_^&{&h9|gcaYM%r6i^=BnUg+Om_TJwxRJW+_#p#|wl? zxs=ppy4C6=>6OI={(qWebFC+Ta;RgkP<^{3a}BG+{Ba2ymA2$Sa2nDX6Wnkkpvasj zgc=a{MOSU2BTPvQ-J?oI>@2cS9&f3wc5W@C9TwzB-Mzcv^vxQXcn5s*7dI8P^A0(L zBbd}94N>ZpRvZL>_L>e`<5TS#`^fs6dFhL(HhHLlyvUuUmL{HT0t5$@#lJ!j^ej_0 z`gc}tU?~AT6qjs^Zmh}+gNnfildeBo4&|<6u5UL6j!U6VIu<&aMbafRa@@o$!x*Im zn)>Dtfk1x>mKA$vqJ(Uk#`V;?og=T2#CRIFxTxvt-mmE*^>&7q!evVT_J$s(vSj*F zx&5?KFn}9um1C5mz0i(;BbQ_GNt&+FpYR52-XH@36?R(5C{hqd*kr5s%WE(LuD=6canN2;XpEJ-dd)x33RcJU~;cmtl zlY-ov(cWYUD{kZd!mbr8_PjF_QxdGZ`^p2w->rA0cGQKj?;Re#(xw}k;bYgGc!rA}<--lNZ3X73^{W3%`h z#^XcI`(R4|oH&j?ZlT@n%G@aB`>26#z_k)X3tWWqV3!`(mf2{`S(IL%;R9QVV~npA zM*UkemV2s3#Pw9Ilnu^S;eP`Tw85~Yhsu#A5+8P-6{_9o3L3{Qf^Kcn(e!EYjm?}M z8sDoQ7)PkIMi?}XHz*$CSW~NBjScdi9(CIw*telIpWXG6TY$t_onbf8m*CV);kNAK z4nfE=M;*+oH5jtDOK6G&wW&R<0>Xgy=4n?>H1)7$Dk;?#*%ghu<$$w`C8q?7W#B+` z$xv>DZw^rDWdDJDX^@u~2VU+8Kw z$*ObMsQIpN)+*S<+)X8vcM^a`X+-09F=r+!i)xYee#?O=G6`$-&3i@Q%f`b0TES** zKh(a zqGS-J@t&Rt&G0i%3Vhu1FW49x$-EosZy=E>_jb}yU$UQ$=<(lPIsYXAC>dQ!4#Vg# znBz=NA?*pM*MiQ11-wMhhJuOnn5{byXse-)Z!1W20=Xz0Fs10hC7D0d47+8lqNbI8 z{d@P7cy}0nEC!%oVj5R8;s;vH8RsnFtURDBsf^7iv1U{kVq8Jld$wE|Bzm_l+VHTb zi^L6f;iN$k954sMd?|I|v-m5z7EIH^jVH!(l(q4xs!;=JOBbf%-V#gVJ%i||5 z3@Ki{as`8KFu0dik_!6RCGPeOk?_$S9M?4V(>c8O)-R%oQZAT8Qc%E7l1Ix2-C-va zXBMhVE&S3O5}^@+a1gT~qUFbJP3%D0ZLoN1wH9u}r91t=*nLxfl)>}V(+jo9c*!4A zx9~F^wVaRLqr0wa?~8ul1KmjE;Df|7`=+UBW~;hcd0GlYM(yoZ^0iaIX~wOqY8cqZ zIb*MQ@ZQ`X#e?)scw*A`UzKH#)xG8!dHVF5EskeX!?4aie5FSZxx1b|tpV7;+w+(w z91*w|QRcH*DvmZ`59G!SoePecjy);^nrL{61DBK<(HpYqQKDyYz4c$mAZQ8t$Rl6p z+@B6VMOlSiOO6J*yk@x+smLNSG+|OW5Z@QwJU;*Gf`ALAbDr8o)L(#4(;=IMJ?`!` zaDA!U^a3x!Hl;s6814iF+`S!6-mve4Mf`}lK*!%iWA_@U$UM+}8!QA~6g-v2--_h* z?bwoEQw0UwjRayIbJR${F~O=xxlFsCO1o8v8vwASMn%BS3?_7U)EOzGrk>xaf8IIL z1C&=nfNJl7N>&X6WO%$fi3x;|qxH*xRZQnMF%Vo@i*8O@gS0G6%$ZZ9k}Z#)tGU-` z1#T#Q6}w{mw|%<@zWhBFOw4uZI^K}0QBD?MA?(9=NE>%^n)ORcs#CG+7ji4Ja7tT~ zd1mj2HTI};pO+IeA@v`N78bwale;|Cu6s+yR?>6d^V{-#O6|l$^n|*_4;We&7&GU# z|F*a9KFA|7q9|G`UZIP&>iI6y0c0N$72*5$3;)%Sei~p8(YkeXr7H5+Sbe@ci%b}n zt*vmWGR!xf#c&kw7u;DeKYYSqRG}UP-*p@5k@ZWq2dyA^N zQS;_r6YYP9NR+8z+O?A=68%lg6Sr%NNjpA3box0)fAAycL&KHD=Jz8%)Mn{X@u3dl z8k}^I-XqzV3a!iM7F=KFO560sm+KyC|4~W+LQN(FGZ0Of5=ol@kr4%*l! z)$cF+K)hg66oPhH@C;-D3yYde%icD<3YfDM4FgP&j@AUqkXNvqF_Po5Dj;Qv_`p)i1 zMsPbf9@|iMx57e234v)74e&5LK3eM9Q!Sz)KL8?-=X)Wj`)kkdsRSLe_JC^tp?c1) z7~6Q{%lXfJvcaqFmtH7DEN@9qIS;S4KLc3+~SnVZX>9vvbY_lls2f?CQK_*iDdUDzx5Al9-04x8!w0-4}^r8 zjs_3?7tPNxsf=OXxYXkrcbKS!X_jCv?rGIHHIlnL_b8~>G!;$%O0S3AKX3Tk=Rw;x(Q=znk21e3tOv zO9OA*vt%~2>KUYPTB{*~AJmlq1nq=A05a{-9}d>%cw;^(lcW&1oCU|JNS;%)Hb5dH z!}uxpMux8Ly>>?!g#HK!P9ysMik9ZmZu642$O{aW)xQ);RZbV4M2=yAK%!nL&ZvSg z4}n$ItA8?AMAkd4gP-msv-ZoUE4qah+)4_c6-;F?Ap&uYZPODc$}mZZ%lPOR*@ zIE^bRl(*k#o4dN&lCjpuy51?iCg$c@utSs3QHoX#QB!?_*o&?laK_z`uAFAq6Pgdi z+%8+ZXF|T&|0~=R%%Zw-m^;EM-rQW3aNC*1hOvD+oBt{5$C|}p!JxHExOyjv(fG7e*Y^?F+BJqOZ1;tvI8%P3)E`G-h{ zALdI%TH3n4*E|A}#9knq=o7CRibcRr-SOU$#S>fHUR0U{fUT#uGmVGml$uFR9K7qk z>|;fEPwCYAg4b!Sx8vn^3wFSzA`c|PrdjsvdEKT&p9+i@J9Q zFO$g+qO($v<3i)ot0@D>&}NuGzmdF?NXqdg)t$VA0|? zh48cYNG+ZxeayQ*7N=-2kii$9NB8ITfGe`o)2se@+{ffh3pgBaH{a~~@jU_KoH{^nY$V9ULE!zC4hj&4$3z z$iovmQzzm{&I*6L>XA@p!lz6JqI#^77XT(d(_2q+xman)#Rk%h^VH%GIP2!~T*;qZ z8kWFZ{49Q-C39a*qyws~$N1=&_GM$R?F!_fhqO4r`Mz@3H#F1+9D)XDO+3^Y)J>53fV%fH| zwk5ZT3yE2GHrQ^Sa6aW^bu;jDw77V#-cl*{31~2sQ>B`y1;zX7C2W?t?CyGdGkZ-R z_WPax3wm4p3CpTiB!wrTwFC7p!;^cl@oGKb9`yeH|_U-O7z}i^0o8VXv z=$48IJ=m^PZx0;AH2=aFN}hF6{_*f=O#*pYYj$1b#~Kag2AQ)u5Wv%Kk-~&EdUd>- z2V}UdQ}#F8GR`LBq_pN(s+>%99xL=7N?q}5iyS{-M^<8)-%HF$@O$(ScqcIrv&I}vCd?!6rYm?q1X)roG zlAdOh)T~ot{F<+?rL1_3#g6qq&)FYI(YO|XiYI9vxQxRsRL&PY4K7yz;CQXUo}{1s zzOlF4_Ids0tggB~+kf4oWW&pPkTR3OnxezFB! z=$9uTl~nVY^zguSs-7p7nlM)G+F1^C65PI7LQk|G7Uy z8K+LYd};j&|Ml{t@3-;st`GHY`yVPh-Sm1bWKb2uH*n#jomMFj?}mSKB5R#$-N~nS z*KIBC2})>qFF`e%9c53BGr*`_ql>9111{aZ= zB#d3cS0z!*JLi`p!bkD-&{S-&4_91c6D9u4o=q>656EgD{7I6uwM2) zjcmU97v4<}H;5uNt6xJWS;}5o-yv%>?QC;7XANyQ8&_gCk9s2huLJSFFJJemKRM>X zih~aD$hZ;a;aP~+qy`LDX^z2q@5|2j=ck)#&cD=giuEMQAC~CV@vCM4=;jk=1O^Q` z*u8x=YXjK2{8)EjLSZ=qd=tOBuBF*k7S5p-Hbjl+3kEtf%lC<pqPulHMb&HOPii#T)6+41aWKl?zbxb3mFvBy%3*yv5$>DyUTQ>Y%tUw3w? zM*DAf7TjVbnrM1c0Q7eC#8d*oPhc=Uqch$zumwDf(hlp!u5s97*|OUeE7=8Fo&CB` ziOEl@zx9ChYqc}K&EIUM=y;Ryj#UJIsXNQ^JIF%?G$Ya`vWAe$OhSPxWfQWmaV4VA zh)^S9X0?p?%b4Ghf{okpV^+RZCBu@;Au?ID32HJAjD4e zD=0*wf7-TDm!-5E?e6)>E-RtV2eBHr8o|@U0TFBg+b=?~jb*q@+TMJoEg_o4G+j}O zWOYt08|E3=e zri0BXN4)l_u)+XveEHkkkzfnGesd zi3b_Q8$M{3Oat*1hRNmL`)U=e-zTZCcwmGxl+Q0?OMjrQ7myq_@S1aC2Wri_$qW-G zIeoD>yASP#odurAcBxc(zlh}5K8cwkt$nqw-`JIFE8^0AVvz$|tQlG;9%dY2ZScs> zzxd6?kh_b{lB+8{89mM2u*ZG&t95qPfw!^|v;()-By#!STYi4gGzXNaSFW#iuZ_Wc zr`xIxfVnH1r&bUYNq;e>>!N$3Ti&a71`*RWu0?{>Lhd#RoUA;9w6vE|wC}-0-QJIT z*nWv}_{LFhhAKO2!Q59KA!gZF7AN*V1s)B{h`tha<1O+8Fd4hk zhH6o;v`RvyDc${?dsJ?vUw~d3DH?ri%fzLw(%C8O#sv6g zneBe3l>yjJZu@ZMP1KoS#ae)#|NlMgnY<(3D!go7%j~4m?|Wc8Q>tW5MQ>&Kkn?ms{dHDU*c;t!U3=ytO4Hee z3L|rrgP9Limk1(%Vph#iDK=7`2%RU?A53fyWxi#PhkTt`h)kC|FLI(3S@Uw9F}+(> zd}S_SIG%XO-I8>d;j$6{$YPozL-TeQ__9U9Drz2wIfidWs=tj`Oe?*n3yz$cs zrMaDOme#3qK5d3;*V|J%3_^(VaDf$5^b~}(*eT493_??G*QeDwZ*RQ!$PixOX3uZ_ zxOInT=EJQlY;ao>syA};mQR;L6?N{P$w~=tFr1-66OwP(LH8_(DyPOkyFMlF9yBLC z{pC*GuJF=`zN)G*`(1G2W9BGV5XqkB=0yCC+4qUqUjh-RBm43CJI13_bxCM@pbl9b zx|Rf0*bq1r4ygcBj&$fJLby@LXTU)E)w2&GUW+MhfrLF+bP41)Gr3O10yDi1=^`sJ zZ=;Cf|6IAzZfWs-;DYk|_&(FlE|44IG5WaGh=LidI_H)!V4CM8G$x3ix;F$`qJTUK z3tQ7`cAN-?Vq8nTcp<9&hpIOhbxz6rqyU{U3q3cj&}e6-=C(ltHyXiy6mZsTW5>D+ zqb8vB&;IJERZ%{AZYa7&&!KKHh-TE2`$5SCb_1wJ%aped{|W_naU!s5;*e*9Gb#QSpWKvX#>irkblt3Y+VNx^~nC z64=BfyJb5`joE}|>AZJc?5zkPkx@poNGi^XrOwo3a%G)%m?u z9|TX$K>K`QU$L)hs?yt}#vv0RP<^r((WB@h>V%!KV{=CqzKsZr2$k+SsQV(bjm;BH z+mh`3<$c@kXtb>$Gw9?}*sv=9%vp=DGtGY-%~W-Ck(S|j;A-yLo}uj02#cv}F#c(% z%x@BL>$ba^h0FHa!lc+xPic2RBc^R35q*9ICT~unRD!BM1<26Dwt!r#_eY--++C{D znuyU+S>v%=uSS7Jo_th&E6HD^p1Hc8R?hNSl7JI1=$o^iRcC(ez(oF}F4gj%L zl|F=k*??En1vE9<*T~g8x+_4}`Q#4oGx8s4p~8&DMO=P56qS+txg3nxhR4NSCqRmB@pzYnJ{X z8gNK#p5k;BoW^Y9vISG`D)F}2DcFCwgr-O4d1@FL?RTH|21c!+zyq!M zxMH;FKm&z2iwFT=({*pc)8&Sm#PL{{JJ8S%i2n%Ep7~Y0;9Xzt%GBoX8l$2eb^DW{@ zfmdH9^E+jdIBbcUFBtbgcegvzotYI=Ze?AgoI6ji>7%`}9SJ;ny?LClwb*|K#}U+Y zv#$R>V>g_E)y(f9l5s7`ZaRksS*n5d!4n*QaUJxu${Z|GRlN>;-!G2-x?9i&Mj+SS z)5Qea6*h4gRZtZMzDxM~UjW}D{+7A8M>NW+tl>fgU4^dKG@8z9FLH)GRLr+f5I%nQ z-HeAUKl~uifxS$=D;Xo=5E~`vQZ+KYMsLn*pi#3Ks4$~B_FVUQAK8^gK%7A18Sc=o z-y8YV!Siq0fa500hT7Ms6Me#_f2>UellkdQc1u^?sP_ialq2uFJ4J_ zEuTynJ&0u3@qOkQHdWE{@jJ>9s^@cNX0DVUAVjRb>?vnP@ip$JV$yx14K70LjN`C; z>9`>5jVXLby1Yd24HsuJ5H>7+R8k1He6GUja>dpdQj7ov>mHj_#XD>f)wB6Gca# z0Zm=sNNYaP*n-XeZp4fg>>sYT$ zw>_==KZJrezrvmlHPWOAGjtJ%;KMTNg<;W4`U#ynDFwevVDWuUH#r?4tgc>lxu`8K z!{%1b{^YT004=-SHgF_jsf;wBQKlF_dkdeubWdO5@k}jFN<*fp;;8APJA2~J6ip}~ zevbz8jT*#P<6nR(+qXRHHy}N$I_$RhR!=x0JLGquf8^C&uvs9T0ra1_;Q)L#{xROM zmZ0L>Js=_%C0i(*tquup@<_yYwxrB8qco zdk#cyqpxZfQ_EJGJe*~V?fG$KY`Pj$x~At-u87+;_nf8;=op_3n|*-%FJ8%tv2`5( zb}MMGRHTmRi%XWIt4DpH63#XYhF}6pIV&m{nDzKiRty&tv?J&00B7K z2b{3vXT@15bzm+;7vEaK(%5+A4-}}^G|Je%@J$z{3Ftqw+jMg&{O8O7UhFU}pm{_a z$=uWdp!RX&M(kT#@=1b|UbAuB))uAvqxN27D1 zLtPs9#*syW!mgn0b={fU!B0gWKTqLH@~#wH&uu;9{e3f2)w$2Em4F-&=l{fD35Z`Z5{sLzoOl|x<^n*Y&Wh-K~>@Qd&I|g5)bf`qHiY61KS!11=&kL%kHS|joX+_E{2Qz1!j6zJO?*PyXOJ< zyF<_!sr07u>rM3bFAvd*h^9{~md3vrbv{Z;38cXNd(`5u&+27}nYjhl`@VK)Sm><^#*>b3C<#~zPP3^CB^AN%F?nwQ@Mp7+MWKI}dHbSpKtQEQ*3Pb^OM}z% z!j%tnkeGlW^WELEkDnIv6bdjJzQ?H7GLT3Y0?g)AD!v6Sb|fmx#l_(OlX2xRPBKtq zJeV|1?!xAoF_;_P0~ZDkw9;=cSA*!f2T1Ebzmd*CIsF(11P+W{etjlsB39OsH1Bk6junK@| znDqkATW0--Sz!a*Ae67{WNkPVmkJ>n*GRRNgM zU;)RNlZ=zK=}Oweh~Rdz)8TFlLFBT|>evo(=qQ|IY0~aIT6IaQ!zI@`^O%Eq7lt!A zrtUd;&~+nLU6FicUFklic3Cl6*v&ePh*k`#boMObtuRG%r69m17b+z}O-C^kU40_* z2wX_QCNh=0VLXqt-zDqBbfoMkxyzoJphgxQv0ZFQzr`yBs-!Q}8q81YPnXSVpsi`8 zv;Ep#$5ng_GWuso8<=tuWb2Xn?0a2^^N0}3{P&NxVFMy(T5;T#l!1iI;qCHIy8x=i zIGi;Q0g4FbHrvZ8p%KC!p)yQW<9Z(IHr4yWlkR1~%Sm;G+)kI5A{o1%*$o$W?I+tz zdr*2LC&w@O+Mmm(w&Pk zM+g2|;6uz->EnoXlq+&kosb-i7G^BxeJ_r4 zEjtI3iGhOBWkucc5#0FLhi~Y#6q$~AEWi(Ew)f=l58Ahqy+)Qihufr#Sj_6h&0JHbrxtz z4gu-(n`DXO_7*Do7HE3wfwl(`u|pA74)@JMxIrfJOYdZdej!*6=0)%k(|{PID$#jL zCdvIkMuqy3!7lg<5jP^M&zr^Y@b{1CMFJ!jUgjFRV>Rb3rOwq@U}uo&$&CSHa> zbHe@yQsnYd+Pbt57JmZFOd>t&a?EvEG!DREyQ!=Q8M@a1p~< zy(%t=+qR{!0g7lBC*jP=R-2Zv#RGqB)@kR82Ai^NX&qt9Jl*XgSbW~q=~=sACI4Pn ztPi-*Z-Q^i3ZtjLrnLXZ&)&!XeLbhiZr$ZrI{gY<;PBQw<^aGva(Tbr4 z8G#zp-BS97hIfdcf7z9rko515l&RMSGV4#jEqNgx^Lc)w2`R$dq5M+sN#)kx=yVUs z%G+T${-eKza;KuW*6yq=hcHV)qmeI}x_9h&(~mQcZm8StgPC$DN6ZiEFlRwa$)s5I zsL^BI(P~xA^r^yvbFh|Z$;$1Hl}PKl$+6IB+k&iVIb=8xNS3YwpBLA&mDu-|YbTs& z_AJsE^(bB__eIqb$%NcG9R`Hx`r^3mSs8$$;LLb1huW-%8=>#5tO z-rRu1eP-uF9N3>2F77!42X=NjE@INOPx#7w-J)iBO02Y_;rhe0$4Ri_a;>3%zA&77 zlfp&F6`KS*(euLKYo=n2$I311YkC*OG{sU1rX+fv_cWs0i0km&WK= z)V;c7^++!`Gxsio%RJ<*CM+jD76^T&59Qsf|pY^OmK5F=m9;Juk zRo0Qvdh*U?wf){6*8}QLiambuFXkrWZd15FrAW4=JI+g@s6wN=;P1o z_-I)20Y~`heksBM^pDXQQd9geir-{>bW9KAT&JfWYW5+g#PstbC(nL}hw+G*lKgGc zPYH4Im06F~qKIs@hrt?+mx{8c)Ggsw$i-uBx}7aa$Fy> z5N2pj@;iR{icbEM6M!()``y?-#6kwDxLWw30B~Ta5VdmwX5Vq#q%IGK?^%2TKv$}=H-z{QK&f>e1Kwzz{Qc$;9 zYFK9Mp7c|!lZfH^=9iCge!E!?@<4PocV0+Y7l;053BQsb4TMN#bue6NzUt$x<<>9EQEFs86Wo?J zC*?0JvU8s;I;_@Hc`Dt19h=+--eV$wwKEj7Bqz|id9t2jg?7AJxJ@D2JcoTi4k4x% zMWYT=!s

;~u+)qlU(q`>j?9M=JI74mmL+4+)ltJPZZXr1z9Z3pL{~Y?IJmdHu&H z`(0F94Aw|m`5&MNrIf46u#&&T=v6#F0Hc_Q6d8+ZyjKdwh)q8m%06732w1TuNt_t= zbkO^;DX50Sx4E7`XeV;%DwL_Hl``XQA^ZkgGLW#nY8tUH(#1iqmWp}BXZQ5MXyXH_ z_LH&$!NFo77NQ+zMK0(=%89Z#P3ZxfqQ$Idu#Q9K_*A6qmnBvF6 zU=@{xxDJ!AV?=I8j}!kE7X7Z)`>TuB{f3GJ#R5~4q&v#Bh3HTj-YuDn z9#Z+{xz01p`a`lMQ-)?k#W5q#pFVKqP9X>DRHw-$E!fQHE1mUY^u_)psaJWzr`C?F zldF{wD$vK_KnJEQc>n%wMiUjuL0L{fSg%?3o&hh;?;uyb$Y$jyWi6<~8eUzTb$7`p zthd7DU#`E+Q+o@Qq^5gU!fw({Uy{I^Sz$Ja2J&q901otI9wsseyY|StZ5Jv4IiPM8 zQfSa_^R@-}`c0@?AT-41an5Nmnww#vRcZBYIF79IvPioM2T*R5Lu}l@?EubsHo96- zXOp@xFU1OsDEuny!4HO$rdmXOZNNmdvh9JYoodEuq%01LMllKjt(<7dyS6CV+3l+N z5FpvWSd#JqR3GEOoVa=Ep~Fdw<|NF6WiqG0fPf#rT709QsLtSlZrGxUN6$6h*vgbK zks^lfGvA6t!3z7Ar|1`2(Pq6bo`TOqZq6UOh_^`pc86Edojl_2J$miXq$yoR#&{hl znhnz%wxvg10sqH2eMNo`e@|-f02E5z^?qfm28D#tX9(ZVpJ`+m;TTZUmLCKg`|2-D zk~mntDJ;;4m$%(hGq~JfwL3U9Z=-Zc(s8XECFrKH?LU$V#mrk=m7;p&xxi7K8mI_M70NBptzJLs4PM75KU}}+WOV}Lge(w_Y>}vSAsZHlPA+;kMZ7|CPfOCyvYVHeR;u=1|5xZkriD3W+Z%Dp?8^3OY-m7EG6n z0Y&IAhHXm<{PchG2K^&~Hnjz)Bjm-UbB_ZpF8vfBn1ahT4fK$4A3ij;mU&%FXS$s4 zu0P%O{(P^&1NpPuTxGc{QhK(i*OE>g@2o%%u4U^~%WW}&i9Fd=-|;xGNTo;YlM^C^u|>tZEV|KUgP zTX(daGT)l2fBIEy%E`Z#@yM{91{I(DU!LAUGG0FLpfeqfM>nT;<_&szkKXs6@q-nw zO{2Pqoh4@V-@}RUHv6v3S+0WbX4UDv_wdV)|Bu%6)xsB=u!IFk^apStma!wUSg2BC zzhbPJUS+_lgAUCU?XKpcw#mwvU| zfW+Z*SS?@8pyC=U($}xq1AzpNWZ+{btyl-x)maulosHvAdkASBuOFs<>nYIjZ2{@c zhNHr5)Bc2JfN$rmf|8Q@r!QX?0i5mSH`2OqgG6lm_jkSokw}!vcR%FIm-KzfI)RUw zm7OLrq8LHL#P6RJoMt~!K#jJe(t0{SB zvn$~xvm>cf^awqM!M-yV=v6y0EAP$(QH{9T$`uu7I;a z0sF8ce@Hzc;It8iPwKa+0HibFSkcQ#RMH{YPBar7ZqC0Vbsw7Sf}~nlzulfC!-xNo zXvEV#J>Ti4CXtU_W!{xd4s=Cj?VmXi4i-Y5wYIk}rYV{}?RC&T4t;XL-*A4Q?YKR7 zcc985mvVZ^{bEq7{&E$Xt-UK8I9wRFzsK=$*Oxw8EG|9?MY#8tcV-`>WAgv z_}-fqHNVi%j;ZLw4-Ve$IAM$y;+OC{!7aPU>mLW7Z5L@<-kMK*HJ>Q8Cgd(~zf*I+ z#Bb!Jxg(Fwy8n(V?3{O{Y0Wc@375B(XwJ=V*nOZZ~>a z_+9?836=t$7rYkZh1>jz%dX(yW~=7l3!g6BT9Koh?*IU{LMn%{G>HA)q==%rgMGEo zt()^t*#mV4fOPRC^mh%U`5i5nd5#zA(yL~NX`Rr;UJ0dw`25KW11}XaB0mkKgsb$f zT}D8l6iP1O@qw=oXXTv?f{}w6Bmr-m%}>RB;hdj(e%?f^|MCp;#(Gm6>-`KzmI@eU zZ{lDt=c&5E9VU+Xfix4cX4$uO?^m@Aw*U3V{{CA&NZ{{LyM~K=A#_Rf^TRy8gEZ-m zk~z3q>x(z#!(A6C-vyf6_$t3Yg3qoEc$|eUHtF3%#0K?5*}Vd4QDq|)_vk3E*9A0$ z9kEV4KCDdAcb7Wt%n?Zc(7={#3fqNvR+sOifr@DoRcoa2axBROGk)WAy^>wZZF|i_ z=9ru_r>Y#|`a1%T=RJ^AQx&#=xTCEhxWes7rVC35&w~V#@j7kj2N8`D9&gUtZpVI& z=eF(GQyP9#t7`XLmlqoqH6)ThWa@CmndT=kU-g}*Jo#z%N5tHyNKYg|yc)Hi%$W7X z(;|hhAypj@T0C6Tow}*)^4E5}tLn}Z`*XaRLpCSmbtY3aHO_Pg&R%4;Bz=j#;NpOJ zf-ER@IJ_O%;dY0}_WP+LQ6MgB_iAyL`ShXwwDHzAE;KQk9?FyW7VyaK?7$>J5Taa@I@z{i zPBAfjteX7>Rr#aMd7a~AsR?d#tR;bA!c1BgEsG0?WPcP?%>{rkImeo^G;+Yej?QW% zJA4BMO8)e3bE8zgPL`P(4W-Le0|w0ApfnEQ%d`97kb|t)j4~#Dl7~^@H19Z z?s{X2m(TogRnS8Vv?_l59!x868_JdNg_l$IRR7^XhLzkL<&xwFdH*=dAx+QL5JrcC z%oBtS53&jqq^#Vxuw6ol&c67d`Mlv&$D4s1CT8o`vfRYIRIxo%|CjY8@3}eTe6Xu} zZEr8SH_kky%`kp zuhb}k7bxP$l~B=aOq!r^Bq5{Oa%m|x7egf-*1<$bw|JA zw6*pV?2c*;8kg1pWrRx>VXYFJXN`xqyT3#$9~Qyc?l6tp{OCTJ6W8c^XFoI8%g+f& zhlf>%m1F5?12Xy`G1b2057jZ55{?PWJ;RN~fj-qb81KC}%fNu@4!op+rI{B?U?J|` zfm=P=5iRtWoq6xq_&B&H@tM>`#vvxxCzr`*mgd`ah%@|`eZ;;^?ik^wJ5uD@K4q)N z1$lU9V=vH`F(nJJ0&yQYTmGmyenw%jqfrq9J#kf2#g;iyhV!E@K=bEXX zblBL~p71-;^E+?Rn@4Fhyzh(4MF5kP!>6zRGJOJ2duPD7DUB){-PEN^W4sR@lsH)!YyGP-NKhWlR%qAefj3XQey31;ip|HNQXm`lbOP+Si!8KnDJX zZ$3t}ej2w47{p5XlGOBza4_d*B!F(m_n;mO%$4S}nRx=FSt=`Hk_NaLn&&`#!Uj1o zZhNH@AnB-CkxRj@xFiKk`t9DmKivbdn|mPk?0ygBzr@-9;mgiibU`dVW%1TX1I2d% z4#~WW=~BZ!@DF%f-+bt+d>FoQ#ywW%lWGVa)#UsAaj$zEfDTZyGUsovbFT{Ytva^M z@8Nl_CMX0>9q&qBQ|N}#70KtD=D`L|8W*CyBXJ=#cTH><{JX2vn@WtX4U#ENBge`V z1zPx6#SC12FDR8;59=57ePw(4Hdj^IBZrBS5`VVtK}WNMG3451mn;5F+7v0hE~VI~lfYkv<0a!CW|VT!ri8Ak zuY}#u?mhPKVb73VfLFEi>!2QAx^{IXsUP&P>H5}o-!^CP+r$i$(}8!%lksnv^LFWT z9IlNQMjnI)-xd|vGGD7bs+A(ffvQ{MK$8xnY|qivO^N#r_U$^IwYkv0ycL}pY{Wf8 zl06amgt;wzcXCtM{GyXL0ri;6{`KWy zRWk4$2z6rWine3{kr4xmarfbeTrtmqaAfq!cfaG?56FUWNcmqu%;$a=$FBN)`x7?C zPK|mO!M3J4^=@WjXm?1zgf62) z#~QpRMl;I7_f2|YrAF&)qzim^!ZFoIcNWPmk?6Gb+yqFNNOr=ZF{~>>acH+vA7pv*qfU+WNHT#!{40;G;AIchZu&ZOaUYdwrF&Uo4YaxSqQdCb;f- zKYcP$6luQ|$-A>LZl^P@4r{=$?R6zorzG8fS5!cwI zw_0DIkLY59&v!?*Iz-)aoFM3)s{ppwgl{X6dv1CxzNOlBQ{IUERa(hPvHIMHiFjj> z6GpPGc34-}8c@Ew=6g3{YK4jU^3DE>Pvy`2tlnDI6p3)(9<+Q+<<=(Hs^9Rtny96X zBl|0)@cg5NjqPX@7J|AHWPF7UF0JctbvD!e-@HbLEo!-J7GUAPN@Jq3HvyoKty{+p zx)#5GvIIrxXsVmzX^Qbept-rZ-PBqJFz#*xr>?BvjSvEiK2F5;bQZe*=ev(eSs}BE z3dMBZl$4ZFFFqWu_Pu_?>HtzlUqAG=ZPuT-Cbisn>HdLppzCiGqDkn|0II|W0E!}m z!S?84FIXrPoPg+DX*VVoRx`(d;71@=&WE*PEa4^$z=^WE90CVO6%CEA_)nZS$K?6A zYIkJv_6vLLHr7hs@3amu7ud72{2PJ>a=j*)B7i8auZW7#{tdqP4`%fPr2%36@6Vv@ z{vCjkZoiM;COE$=&(a2Ae7K&E5MIp$7pq>VWWqacr#TYsl8fNHj~j=pZ~ww{C-Y8f zl4Nb3qh=6M#-T{uPiqx!{~54tG6T?@rQhKM^jhg*6!pbeW=D`iH=%aimM1knWhSzn z-w)`PAF6+kr0dT69Jn2cAnn)GZnMq;`!>c~AuwgQYo*~TZU+0Txym5a!+{nOZ}aaJ)ayZi=6N8xo@*{%ti&==theO>RCWyv@)Ne00?taipl zoQfxFw4wUP{H4^(Yye(Vl#scK13=AlUpNQON$2t|qJ1Vjs%0)1($O_PIv{eLKXVCS z=FgyENa*a=j2IfNw=Uk7mZZVo#inS#KK}s%E5!sxv$XT$s`Vcri+=|(Zg31YT!6FR zxceWBgG%P#V;VNJl-Vm=H8S7y2rJ0wGmC;2b1I#wD~QIp@|57UirLyBIYZ zxw=q?38M0E(G2(r{9(Tqq^LoOCHPboqWE8$ed@hucpvb$h`1&`D>^Gh!C5n=PR`h4 z98&KMuN}XA_e!=)BQcshbFcSiHBeoAI-!j!3!Ys%uFcpmg&<|IY&wd9xL{XALq*J8 zNd1bauh4hTB_94Qz9cqMgw4gtjpiBVPeayA4Gw}o7zbl&w1-NMe^|}m58eO1 zCXXZl|L^2yHG6%TVEO&G%4m#rW<48pggiMn8F1rbtv{7{#^i2CK;pwYd{HB{-()+Y z^W-Kw@Wm_A7dvmmZQ7$4NIN!UJ9N}7S0^@*?9P$Wi=E~D@mh8n&zWMvn>KoUo*999 zOEqz$hYPy`{8?Q>2@2LA-{8G5v zSW$XmLTLWYqlTDf z(bO_v3+NH$fh)jZv5p{{se#Fobj{xnyoeC_$nhNUy>oazON0f z;qaj?NC9}$NH*Z$l^lcOT8aHDt32s&p*Mc1W~F$XB~Hd9%{acxUAP>Ux!VzXPE$ytvhPH~k*RZ3f0VHfZWf84&C$4@ z<^Jh${(gI>b@f(adCiul?bFC6Y~f5`0uz^5HhSOUV?C^h*32 ziUs1oy=Md|2D>7jC~}H_di$Rd;@>|P9(>fBNKkE^xcpwaasRMJ>x=ipJ`6-!u#KJd z1mWfF+8V{7b&%}%gM#aLJ^oKKwdXuWmh#-0R3X^Kfxh}m6thh-uT^~}832iaog_fPnyFK0vz~lv* zZ_!f)=(uD#-8*8vMBdh{FyE>*`0U#H*(r2x-k}ype*jrKKQDNm(2%LF2Rlr6N#xgO z)V;UHnQ(l7@xEA*JLZokDNGNYenOUY=+EcuPX|fU>*ii9P#iuM0-H2I6(?7Ni$@#^ z!ycT=&>)}sne8tp&sCsa7v6rZ=U$^F>dt38y}1d|KiO2hN|IfN=pUZf>W^m?T_v5I zBosl0nL-O?^;P23J7iWFhvrBoz3L~u#rbZ92?!(}M~JnX{2>CZ{tl9U7YPx)Ig1JE*%~~PFVxox>f>D4kF|er!{?7Te$_{JwA8Mq#1@ngiNO1jP+0ApalZf-LY!fu zjEqO%5BnP?d4p3(J`=S4KPq{DH%4CQLq6bxHW!O#W^$>WPw zHS;iAasTz7u2fhXCiwr_$armI{}5*V(NhjZfE)MO_j6vOMio@9OTV~BgX{3Y%IE|_ z`miBryClB>y=tu_^s;k_yYQLah!8O=0$KJBnmeAa%Ytxd2r3|X{~Y!g_loXA8LyY0+n+MTTSnFa>qZPl_ECWdi2PMlz>BrNnLdV*J+=gEn6A-9zF z+qGZJxjyh8p2K$LBI(~csi<-*?7c+M6vDdbK<}4AHKkm_eS+&1fRWJ9b+y#}^^eRH za`?^a_0qrj{FGVi$LvK{rcb z>3M0_baknE+?=f*O=5qfmK{y-BU$|FpaL$a zHB7*nAUa}sl5V3r>xM|aG64MSyK*0Y-~Z#T0SX$ps7lHAH~(9>w-mil4<7d_fyaD^ zn$vQu*Oc~#?ga2alf=!b3MQaGZU)x7L_NXbGmY^sCR_GiEp<&Y8Uv|%A^s2HF*cg| ze}H5bMI@!;W+5K_RMLXmv1dow^R=;@+4|!AZ0wM(@+@Qwm$^)VHW%^!*5z{lS!t50 zr~cwkLq#*b8w%L@j&Xt;lZy~RhAo|}-icaYG|g^2k35>Wn1TAsgmauaIv!x^n`7Dj zu+QXaH?dHfFwvOge|L_XI0@>v>3B(D_xT-Y)BvL<1KB|vjy4ntgP~JR~%;=c@;7; z8-#~Ajt4i1zcZ1Kkh!jc^wVAMPcGihQ!0V&J;5OSP4XP7j}Yy~@@q+Q&jT@UvtGA3 zJK7(7yD`~~_f9fxd;Nz`>i>72|FC}mAgEg~0drL7b!*Fq69WThK?^OpB|;7E<^<*^ z;@^zk^3Xcg_dGKJvp^_k(f^`#S?p1J4G9PqKyhEK8-JHokVy7k+tFj>M8w*} zK$nz$`#(}S5HRa6#<8vVg;05zBT61k$GXw@&F#{Lwnmu)UuGym=V8ttVifhl`LC*2 zuM=`&qen{7&O|;=&*iT)HX!Syi^zN~WVSVWNiDdYpA_-QFAYFZtjrT>Eo7vKBjh(= z_835N7pWGB17(oxI{d?Z56ijnU^!%jfq$SJ{d*#air;w`Jp#Y}0%-ob0__UGT=BpA zh%rU2KnwH1gAOovcT@+lHMtMkUvjtv@{WL`4>TBf_|kulA>5$*q1ZY^H;>f_c!CXC za*m=9^WeS&_FZeTD!WL3hxH#O%W#I!*U746n#hx0z63$fyELyKihciqF-nxDM^y96 zaeK;%MA&tt+81R;x$vqT$lM5QT^SvRRQLt}vmG&C50W|kiAhMd zNIF|ykr=NCsA#=<<+l&{Iy*%~K=2W0A>0D=9w`HZA8TbSmqDG< zdWZ5w*eatdWc8bAvCFQW;MsEIS3wB-a_Ch?*Rxijo<<57PGW!tK82HQ%&ay2U850g-m8+<#c#qSVh5Z_e9(qAC#2f zbR?yuh|V}DKB{N0r?X*y>4OmL8ofsD-eI%fawOq%`_WB^A8~7d)7mddSBnOJhg1mh z;C84x=^Y$WCFIACW#3IZV$3GD>^dNQZN`+52sd{k?TQ`>QuXtpqlr*KeD#p^+I&Iq zzq6XBLj-`S`WRbJOT*j$`!AB@I{o3V_!ASj`H&6CGYHmr;~B|#mAgf~%u={z#5Tu_ z{MDv@fz(J{B%ZAw((ztr{Il<*x2dYtjH-^o_&3^A_1^L6Sv?0h{~hF%(3Z0`ke)gP zN@>OK`xzoRH9S#9@_ zA~ra9L7 z6D8&wJW7l@aF&o_pR(RGJ`)7e1!2g(WRb>c2e+YEo#{$zl>x*;^T+dQeEv#H(D(>F zQJ{WVeyFf%I$aNO0MfM&)O2g9(0+nqwOE4Ric1El25x3>Hty6~x$6__(>hMbI*iEu z=~NO0fXs+!!>;(rACkj=QBBW5^9t9<=03KESh+JmMf2(g`2Fd z5s3wM<2E1&{_w#d2p@1nsQ5-^!%iL`Ul{D6OZK z(1Y2W>KQ(SpJSsc#TS0GyI5v$w7eGVo>#tmQzhO-Y_%keOQ%XjPrrQ)IUDyQcvr2n z(ib_+W?Cfij;Qb199gn2(JuR){} zY5O>sf2AIp>n`M7i0X zHK9vo@|)pc>B-1!)kO^(tG3newTsF4?7X?r8C~dUOAuFkrbO;Ys&jm)bzhuRSZ7=I zbLk(n5OAZ$Q&Ij1 zK*#$5WS=cBiK$*|7yu4W|Mmc|k4r-XmN?lWsyIl`r8KM>Ilm0j9L*vN&#^Jhb+Ofk z({Ju}X~G39zFxbjTYlkiZ%bn$LL{)8t;ia*di(k+9cRv~q=@B?TA6(W?UCrK3T1%k z#rBHl$;S*fy3J2Vf3$$Pgd4}3UJ~DS#Xb)HteQYbB)ckN)Nq2oyq@sf6HzESfI=!% zUX_lles`9ZPSSgHr~8H25d)G};lNdpZ4d!?p>T^{ zW{99@mu`2)7SM?xKx;W&K8E?#!;rZd#El-U(QTQx!eEqQ)QX|Yjmw6hog8|LI`lX7 z&mHpPP~Y2ZlKT%s{gW5Ikj5~fx!@Z-AfUSg0|ko4Xi0gh?W>l|uF;aKs4l>mNO~m` z&O|DM?u`G70<_Ed_D)ZT3J1&i+CbdZpt0}UdHbEQf`Xy(H~kOm=^y!vFTo)$-xs5* zDPBH{5M|+5=eV!@oZ4mK`mKqbUBRoOXs+q*SmF-tp8FOS!EEcgx7^sreawZID@&OT zQujicJRJ&TWn|J-S9*7v9Xb~K=Y^%^*TS_=mD!0KwnRxDM$Y!|NgWw16^flBnKVL; zvpCE<63o`|pB-4PRUYs;K(dRC`U3~_HSOS(H{@Q`ZhBG@Q3+}{a=DH_DDv-a zc!^qiBGz@>kl5Y(GlZYxsQhG=m~iiz%DLS{9aZt4ZI^dG6Y4ELW?a)|iE(P6C9LCV zBw5)*S(6Lf-a6$tb1%gsk&NtBAA4(@dNR2wA%PQ#H}UXt?&7CsjM%~iC6bmdO#(G$ z{HyB|Jb)|{MDdA@ZD_GCJBlJWj!RdIX!cW0V(@pQv9bxMZ7u*>0CsM|p1GX{?t zm>8CAmUJP@cpk(1K*<5OpQw{bmn~~_Ut^1}Yzw{nP$ub^5eWoZ1CnIdN~ZBCo(t?- z<7C^00O{FtXp+%e!qe@ z265+#x8#}#RIMyO~L5f^2o9N?&K2@)wS71J`+_jto z`p@FfkTsSmEF3bmxcKIpRipdU{Ksd6+q{ep=*|i))vP-bBN_0g(~^T&c4B5#7RkL* zWUwVK0O0~ zDIq8?rJy#{rA!byNXZm;P+&9su|?-wdJ)JesEg8$-apJ#Btr?PD$4}8EcB}vIil?# zDvrwpu;uPWJ@AjG4>&T{RLanBhzgJf^{GlmfJ*R8)0gF|785*Wq0tZOKCmsDr+Ni` zx2sVmT3y=D;6aO!H;2sGfoDaITAKgjmFyCs)??3h7u_t*ybDww^PQjUB(zh)ClfmOH%I7<&* z!gkblv~nM|*R__dY-cj;3(qU{VdmkOA56vXb~iW?^f+{}EXy?*De-t&7bh9NDZxk0%F|UB z8dGYnlX{{vsJ`W0bQBw7>V1>X*d?>sRvLXc`HXOBc1O5Bk-Mx81buFnl^R2)AJ^M3 z0S2XpWzFLNZ6TwhD@dB%tQIN&v7D0W5!gw9#{=YT6?%qSyCAaOa6}yoU6dLD_tgpq7rz##QAoDGzR1X@)FSUV>eA_4yC(qUK_0d6Zs~< zr;h|G4?vhRj}(cn{1P&R0A$U#;;`}*6=z@{?iL56sbF^I=}q9``@u+c)DHY4Y+Unu zR*IPCAUBTF7zKwd{QdiD*$2#XlxwCoHU$)yfgTE}v&|ommph4!>((y<1XPnRui0}9 z>m>pZcJsS7v6NT^W#FDf)txF?jE_0x;cYnva+JXU>eS!mDA$j3lvyWZ&3~Gsy!EZ{ zr-H&mf`iq30*(NZmoM4RC}4ASq|7=#I0fXT=@INn4j8!kOFjavH-t*pmrOtKC=6EDH0~5zDz3V@pcCe5cwmhi6Qa?oJGsqD-!2nZb$xUA3V{M_sa$t zV2A%=-*@fj8Gm~eWIfgc^Enofrn|2(?^x`9UkFDSC`^1(fFy~!HIgZW%grdxP_}4N zUe9(LH;nWkeuG|Xby@rJe)-Kcb)5Vti=1k@Y~na!d(3IWV#AEVV*Q%P)-KTxckzj< zxQxjgHkG@Gil>*K)s+&QhPgJBDl@hWSvr->h7~>*!MFran@Hxr^C(+YpVeQih@wpH zqZEl;d<@c+wwN@KqxT6viJe`lP8543oiz`KT%+guH&%Yn(4=$r%I(5dMYuVj#l`cO z$ecss*XbQ`G}%eJPooei)`mBw`;&SqT}SF}a3^9MLXr}wK!#b6Yr$%) zn*XC&$64~KkR2KdFoT+?(G?h%Q2GBiC0SdTWe&aSC$BgAIhOaFdm{MiH%j=F}?!RO_1^kkN0ARX!rw^+Lh{)0%4Zqp0(Z@Z>_%z0g zVaJ60fAIHnC_(Mz+DF9(^iaI%oFNJ6I2%cUWiodW=F1D))O1&Ts>|IL=SIM1 zywr>;@M4Pp#~H<`v6kZetuERM)BaVv<*sZiTrX4Z80E}UCGvY}bo;L)03u{RZ1XRS2>yJ0`_sccS2 z{lV@!x`2tSz}1|sGv&^y`X$L=9K!Hkyg*lPm5Y?y@& zNbL6_{hDY|H#>x~zG|vOp^Wy)lP48VKuFy|d8t|g!sf{q;box5;~iC$)MTy#DK1kG zlYy%(vjL^KBsoXh7bQ>1hXqh|xotia^S+{wk$s5L9fV%x>5rKOpMLHNR;W)R(PVVfCQnSmyVN z+w^z(=~mEHGHCS@Cnzb)98yz6g%d;ZwtS-ArGvGr8Em^Y|cJZZH_u_Lq?@w|v%-U-_(k%7I z!2W=yIjU#iw9jx>&pb;?tKHOswD)ux&_SJh3J9e75YvGg`G0Zg?G60$1%kxgJXk~q zgyPBi09O!o}l!5sdR0-pr>$aXkr`vh10h1+~sH% zf|I*;_ebARwq8uGoj*A>tvX)Z#p zHJo_Gh2M7M5z#;jY*~25_xf$1Gcub91ydJjkH>6MqyzcEX#fB=0I|~ZE7%Y+I7m0N zu%PHU2jr#drsQ1;k)R%WvWSbkQXA5j8caFHPhK94n96WICX=TtzC`@ZK>Rf@ zPb|S~EV((wCO&IJz>OSR)jC$Sbne2uFI1>KLo)K*EsKV;JsXAKs*Yl#eZ!KsmKX=vP(JCvGX;)Q=K`&b%GB z2R+*P-Dz}X5ny3at{K4N9h!PO2!BG@md(aJo=#V{02i%vfsaO)6gIPRw@5*Wf^2wQ ze_2hH2s=(c!X6?PXr#(-Dvp~rmf=Yb+2M?jnOYMdtUlx> zc-rGvNo-Eee|Q;*9SihLs){jh)j46lwz@_;(I3TgecqkKM_lx-dh}dA8^HUCbQ7 zMI6)mh(+QVYHC7_><@Op+HZ{Olj9_24~rcHe)s5EK-|8Gm9%FC4*N?Lx;w{Bn0&R) z#gbsv&Sz9=Pxh!y#}+yy{^TuWur3FxRv3NBd6)AiOs)2T^_ElRd0fALp|=uTyNL|z zwO+1s1w1pcAx0~<8gTPs?IyQ=f5c><(F%ExQ4eLjwj2-&0bb}Zr!snk9yj=0?A%Lx z?q~;|kL_A3?HcA^4_fNnGsaI}U2}8`NEHRs%Eo-BAS!9@y;AP8B|+-7@{<7@U9;#jg-oHUAjr!x9o ztKhL8u$0I{Fu$TKdHn8<+3UwDbbY{-%nt}d#zCWTx9(WUTu6fyUj|~3FXDC}5|W*( zt8&7ifpMePcCl+Gq)AVQgiQ z|3~l?HQC#o&v}>`nx}5E$2>&Y)&QRM5983P+`^Rr`<_8_9^JZogg+cup$2ZynkM(4 zV}IDExZZpAGi*}cOBwxOr>?FJGFmvri%?M$fNx!{0m`NYE=yMsH51azjD8n2|1Y*5 z0MU+{m5#*Kz#t{gDMnP_Pwn$HpCKNvv2VI?H-Vda*(x!26$#YSV;?r<@Le)cDB%cl zKIok4Hts3Imv?zi4aLM>-QT-5dSnz4ko0Waz8u@M+Nj~6o9Xy=qeyzQ!eh!coJNAA zLhEMOnH6Q{6VTiW+eEyO`)bal9p9zxF$$;beQ`L4G5fW zsAd14Rwec1vT9q5pJr@gHc4GIGHXXeT#^q-_Kf>iaj*kym=)9ZU=x`ho zH5I}hp6hZP$~O|iDt{8j4qJuH?Gl)%(%mmg>x(IDl`g+5C-@BYOz+(gn3@nAGZ%du zx`k*DHb9Uyx+Iz$`R6W|=Lp*qz|bmgn{(=H%XzvLMH&VC)`K_54S}Y4VEdgxe>yjL z6PI44nzCw=5V_NA=e^QrUAF+cg&&ovL}fEtNiH3D;a;{%a(_}~EAS>&l_17ptDNcg z^62MeZrt#{2cztr1lv?o`C+1UpIr=df?=7}Wq*JF0xkgYBAK+izigN3+hkn?u3yiK zG5>$LrTmtn{>z(puU!8DFkBxaJg_sk2J)GqQuB)+&3l$*6h{55`g8mM&>}5E4m$GX zgMGR`ne7^@tez0a$+!n^m3#Ji0ncf=_9xHjCgb7+d}P?The2-d+u2$^8d%-;pp(rt zAE2?-kqW#~7qqx0d@W-!QzxZ61?|3FE5mp6?5{nH*H!~|2{#S-b-Sv@4P)Cxel)#Q za^D_P$WfcTVm|KJ^pqPju3LG^X_lw)1BtciG&}|DuGlTm2fq& ziT6mz*X0jndPnS2<^soVRK>-Hp*bF=F-yxq(N3$nA%ptci)&jhN@Qh5V*bmO=gw*Q zOs{bzB^j-Nj%i>AhULbSkF0gTHMjz5HCPY=@K7rB8|`Xm%d)m)BUKq6<{e_7Q3ENc zb*0aJ4s%iRhLA8rU{N(N{D}i+-KAmulu z(!cSOj@_kL-O2A6B#XE>EcCOpv0eqJ@XRPn(QROm)c_EOI172YV1S%F>jfBX>|!`~ zZJG0jv2C45Odz~ix$T*I$wsNwxWQ5FyRed-Lrr&ddr+^BgL#?v?B)|b>yZJn@Tbjt z%EU2a*Xd#^d$o(x?j0Wcn+Ot6Z>U}KsS8N@#!KyxQfuii*YLWg1JoY# zhhN;W_%&_pQTVj-}D%|Gp^LbKQlzsuH)=A1V!GN(&ap=>V?~R<`Tv zNhrFbx0YBcWhJdd7nL8D#pKy?>Z$z@U79{6m}mBH_9u=zzu2G9OKxka=|VsU7f=qE zip)S?HfR&nI4NN#yhc7*Dh6=Fe)T_*e*OH||D^Wc{7+0}>9WtPuY2u|I`y!}Bvm4- z%7Lli2iK*MqCSO4Abku|jN_PmaZy68+j-sV01OUe6=8AVv;0N?1i}pfy(}IqTTCWA z4<4W9Gr?ApHzcFbn~;f%HLgoiX;LryKT1|@u8<#-I($!_##%lyM23R@S0;%1@7^ab zf#N>bvu_=U1}zohOMb(SyEb*qBb<_;)w=(iUc&DXEmrXeCSe`)x-gUvpZn zkc6I?GSO83J^fUcFwK>Rld!ciscFslm@9`ajCXQ4P}PbXzye$ zzvPzIU{UV{y@Fj6PKktV5yki$S>5788T`vNH5}JTR#tL)g8BEho;)tYD`9mujG5EY z>g*e{O<*~w$F6*;ixdquB9RxCwsgFntX`5E%<#}T=JkEyqsI{>{mJ87bQJmKe#Ba9 z{V6u3uI&@eAI>Nk?ujzx)|4kKWRE`HbgbXa9Wg0}q@yT<#i3V)_f=#Z3R?#~rvnUv zhY^>DY3qHXNncQo8BW8?gLh4NqzvXlCJYwyH6Yd3AwzwU_8`C0ICZ;*H<=&|WVVs^ zpmb@>PW2826KFMNR#_KD;KrwAu4$E+$j&gp)~6L_La$GFRcF35QblQ`Nu7}=sh(eX zNbVW(qoXY3mdbLk24(!QyZj{ZHf{mQZ+|(6I5|D0yQKF22VWHO#jxlj(B~y%8ZnI$ zpLM>!zO_6?3%2J=!d+P*E?`OpMy#ndeF3?4t`A)Ud(3SzjNz`n)adD%?eod*NyC~a zPEf}y$lQCRR9c{!f8`3do&~*9n0X@be@TfVye+#rl&8rV&agyQs4JU!4~WJrPsB0( zZo^X^6Tm6OSl`J1!wp|X$jatUPO%<@1IM^caN_xg0N5R>OP2!9F+L|Nu3Zn^YkvN& zRPcW?L-C-vJ5Qf7C{CEMc{{3!1JTHU3eU_qZ-@Ruu9RXb2_ZI!923SWJOIpbgL+r` z9*ObGRNaPBQQk{mu~h`nmM>ALsV;Wn==uib$K{>^{v<&%UUu_Asm86FF9TsFM78{; z8I$m7hu#_oJhSK4)tAjdQPk<-t`@yb!RCXouFqwr7>3sZN$X$6OenptE}lNC+E7j} z`F>n_+oUt!D~9WaTJrbpMej||U4EJS`Yq`mvFatysk8SoQ@rXALvZ4T_b>uo13s;< zt$JKMsWIW60ymRC$YeOKwo}2F?yPeNc=Ks5^=`AA9*B=TaVNpcX3MOxmT^9B*3zam zM*L4#6qf<97O4JD6BvG)MdzWuuNSffntSsdXTDgOfHFKV`jc~0dT+<)R6`v&YU{yk zjjnlqHGlEudpx=?MWe0J6cv_J#j;b5m>_bWc`+%HNqt=+Mk~`T!+X>3 zYRErdu_@G_sHTarCy~AzsWzPeC%k7|p<|62^a;qRG9A!ao=-(*pPeb>N2xgaBXV+X z^cU(vpQCqBVn zyuAXj8rh~pLqZzArb+jfn93=b3WeQ&|4$54fPEvw`gHpTTyTv6xbV1+_k`Ot0yY-O zm(9nZQ>|$L{MrHC4K6Cg>Z-#`E2pFrFqdru^G+?2Vq@E1=BtZOmO|XnUqIjIm`n7J_<+Ar*ifD&}%LLa?<TYWbp@rlSMRcH@4GP+*X=B zq*=A=q^LXHVSIqErP-FTJkufB)^E9TjgOCDQ9{?3Zn#u;8*$VlJvU%~0W>ev{Oj_L z@&lYO+(z3eUV)$DEwa6{B2;dH*1%2?t5;T3lD`7)TLQ{)=Z~3Kc6o8*{nJ(9q*uo)7;jO ztsXrxN(^lM)mK#5`DE&~4Qo{U;}5kbEP}+B^x_Rr7QkCG4_InyjL#{j{#dYSroJgb zI&7C8*Z@BtEV@T!;yrPCN}3>TOpXaQ28m#dbDy$1RbV~++Pku@k8GWqE63S`fZBX2 zlJ0INo^e1gvYdxyI;4C^mn)v82BrX7SLUt&^#^lpa(XKNFj-k3Gw1z>#wscfe;TWh zos;yiSA>CyytkZYA9S#*1Uv_Q3k^~a{c7jlw&%xBo}YGnv-D!_WB|cy1&!q(z?m}! z{7}UUocw?k+`*0kq&<=nS7Pi$J>wbTE6BrbTJD0t*c7pIO#?o>T6 z+UNSTq$M-QC5$xrBCuBJ+Tj+7`qNvbb^DPwr}iI^u-bcH4U%L2KD$&yUy>XVgvUOrQh8U3bqS!Wv9GD7N#%u z%Rava_+{IJ1jAZC@nNnZMZ25((*SYpVK?9UnTVISfi?%yp#Di| zK@DhbEPd14SROB->NE7Af&f_|RSdp+s7-J0PsgzK`? zt3o@R=tvgLo|Mo$&2w=sBftYEHnBV!#?*YBrNEh}w!$dAc}s;kaVu2_O7o!&5ix<^ zAx0I85^%h<N^$n(=PYT9V;9%bc2d=|p&Ng@CGp0)+JMOAsZIc_+0*5r z$vx^tr*H8fl1Jq)x+L8);P;93dI;U5PL?Zj?3B=gbw2hS-_mmgQhH0NZhu54?hn%y z@4EiP##)|5<=Ar54&&n5Nw{G9EF5#)r!(ri!@y2eDMKPRE#YXCetF+BcZ7;s0;2yJaV=5<43-e?I^|5jT0qaFfuTs}t^9-sV; z;N=%=<6jguA#yk|ZnFstIr&=fgjalXk3&V2d8Drp_@&760QP;sw{}^L^nJ%IzAMns z;eKx?o8&EL6~=Yfu~EpR3kbx>b%98010oAP&NqoA24Qzb7gXpZU%p}sW#%&vo-(@Q zF*Su=>uKlBtD|TkrVT+#cUahU%L0d3dHG1zsa7NsJ=YuiIi82-Jk7EfgykZlH04`FGR8z#LGs@ea-mwi0YpnsJkc;DZRia@a33tW< zfO&o1H~;!}`xemA%#V))J9^KD#@LcY(~}UamkpZs6agXGwY$D2PX2u1aG8J)tN&8J z{Qj@+|Mh`CUfgFRBO#4ql$6le0CviGU@mL&u3d7F5EL^vcjID2pDTmJ^S7xLhfK+E zw@M!e8WDc6O#4AEyH?rM{!b)EV;=eIG7Yne(5c%MG&Z+Yk)w)-%3Y#`f@Iw@!{B8n zVSV2oX8PT17!o?L0u%2Ep`COr*h2&&$lmFj;`cdI;oDZO{$@ITnl(J4;PLe&V@two z&)Ktj0vN>Q15ePb;xVFvOFJEldjW_&$EI;bj?@43GVl7{RX;_83ms7jWBRvC`}JcD zE$1@=5icx;tZ6dRC)oL|GGTd+u^89pM@VZ)dkVoe8aQ4L#%jNwHCyE>z9wAcf{LGN zz3%*0kQO1PD(Z@-qpZ;P(SQXQxQ*QwC^DOJ>dGB|`?YVrqemiacmR*bZ!Skc&liXo zuN;K9WJ(+@xk=Ju&gLaFuw8O!8fP25#76$V?+*B?ekZ{VBJsU>=8yaJKYjV4(1v$` z2+!W7z+77a`OHda6hl7F$*TJN#-`Wv$o!5CXPXCBf>8b*eKhj5?9|jg(?XB+@V!Iw z^>Fp6%Z5BUZna_7!~1$p^u>oB1GU^M@iQwFZA7p%etZWJtW?~&X?(}0L(9Q~APMf~ zfsbKoi)ubrCobY1+LwiTU@XSWC+ueXHbr0vDp)^ zHSVJ7_f=Ez{VM?o>an4tgGKGwc^_4uou2P|f)0C}@{sw=tvEb30P(4BnLvOlm=cFx ziJbtttl37HUzQoH?tPZ`#`b)=nQDJ~5(xFY{rZ1yavf~?*&IHT-u*Kq4=qQUTJ!kc z(^rJV(X@Pv#yQ#bSoVbZ>8JF}guDe85bNX|@*Nq@&aMJ(#$l4FTdtsg#+uW*Oy`cs zOuO*N2HHuFy&F5f>7g=u^_40A%J-gKrwe+Q1jk$A#(UZ+q3A`+T0ZVsWZCjYQsr%N z)Z==$CWGMcqrH7p?7|^b(+j$(zl9xpdO*BTbqa#r3gC@spz{+lUV*q2kK5Z@3?(*C zasGN?Y6ZT52$PDzre9CMpMl}88+fbsuGNrea3CU!iDNHW!s@M6!uxOAx!<^Zz+O8L zKE>~$(RnaB9lnC<;uW-u63Tt!I!*^uN8HYBA4s7xEZ$FY;wu?UrfQzj5s+vzc;s_2 zJD9rf;dl~DsDg(lGGZRe6(LIdjWdc8D9kHYcK2vVp_h@HFP4p+#Me#uk_rb1vhZ;J1et4*` z71g}g=1?kOwvs={4%?OMD}b+>L$DEt@dfw}W`~`k1Oel_P&dv^6Nj9ybkhOG9vdrk52Be`1IQxn~EbHQddwSQ<|Q*^?jD0xs4 zqUzWSk9RdX*rqxvR2{Cll8DegTuWrklk{=YI}#KSwd5(}7Ei5@UmqueSwhB$HmTfw z^fV3e_7C-sEN7e%`0!^GzZ^Qh-t!hiv`d5RN&mj-{<=p$U&O0kh zCup6V+q@MK%b~ErV&t?`-N=cy&%|fVCJ)kH$E@3A;!h$HZM!0ZG>SLP^yP{%{0k^B zKenUtXan4dr8S$Wqep^E;xNH?_~P(^LW7r-I4xT0dq(f>CKYTiEGf7Y;^i=c^J8J;evts4_W?xU)*N~p^cR`voo2{V(2^3;>$B$hm#q5)pjGIduo~! z1L9V_SptZM^*RbYmR$PF)EONagF{6dHIb^hd)?oiy-~ILZP|O@5Gx0D8`ZP)9IAPo zV}f0Y=z>Z)>y;?U+&shQI-2#>#~Zl6-IMck?e$-8pe!+1G*1{;J{(8^ zuJrXMvA_Gw-v=_aPaxi" + +use_ray: true +ray_num_workers: 4 diff --git a/setup.py b/setup.py index ac0c96defb..370eb72972 100644 --- a/setup.py +++ b/setup.py @@ -150,5 +150,8 @@ def get_package_version(): "lomo-optim==0.1.1", "torch-optimi==0.2.1", ], + "ray": [ + "ray[train]", + ], }, ) diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py index 0618e07f16..a5865be1c5 100644 --- a/src/axolotl/cli/args.py +++ b/src/axolotl/cli/args.py @@ -25,6 +25,8 @@ class TrainerCliArgs: merge_lora: bool = field(default=False) prompter: Optional[str] = field(default=None) shard: bool = field(default=False) + main_process_port: Optional[int] = field(default=None) + num_processes: Optional[int] = field(default=None) @dataclass diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index 43e2de3db6..801fbc80d2 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -67,8 +67,23 @@ def train(config: str, accelerate: bool, **kwargs) -> None: # Enable expandable segments for cuda allocation to improve VRAM usage set_pytorch_cuda_alloc_conf() + if "use_ray" in kwargs and kwargs["use_ray"]: + accelerate = False + if accelerate: - base_cmd = ["accelerate", "launch", "-m", "axolotl.cli.train"] + accelerate_args = [] + if "main_process_port" in kwargs: + main_process_port = kwargs.pop("main_process_port", None) + accelerate_args.append("--main_process_port") + accelerate_args.append(str(main_process_port)) + if "num_processes" in kwargs: + num_processes = kwargs.pop("num_processes", None) + accelerate_args.append("--num-processes") + accelerate_args.append(str(num_processes)) + + base_cmd = ["accelerate", "launch"] + base_cmd.extend(accelerate_args) + base_cmd.extend(["-m", "axolotl.cli.train"]) if config: base_cmd.append(config) cmd = build_command(base_cmd, kwargs) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 9e3ae1cc38..7ac15e04f6 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -5,6 +5,7 @@ from typing import Union import fire +from accelerate import Accelerator from dotenv import load_dotenv from transformers.hf_argparser import HfArgumentParser @@ -15,6 +16,7 @@ from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.integrations.base import PluginManager from axolotl.train import train +from axolotl.utils.config import normalize_config, resolve_dtype from axolotl.utils.dict import DictDefault LOG = logging.getLogger(__name__) @@ -63,7 +65,47 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: return_remaining_strings=True ) - do_train(parsed_cfg, parsed_cli_args) + if parsed_cfg.use_ray: + from ray.train import RunConfig, ScalingConfig + from ray.train.torch import TorchTrainer + + train_loop_config = {"cfg": parsed_cfg.to_dict(), "cli_args": parsed_cli_args} + trainer = TorchTrainer( + ray_train_func, + train_loop_config=train_loop_config, + scaling_config=ScalingConfig( + num_workers=parsed_cfg.ray_num_workers, + resources_per_worker=parsed_cfg.resources_per_worker.to_dict(), + use_gpu=True, + ), + run_config=RunConfig( + name=parsed_cfg.ray_run_name, + storage_path=Path(parsed_cfg.output_dir).absolute().as_posix(), + ), + ) + return trainer.fit() + return do_train(parsed_cfg, parsed_cli_args) + + +def ray_train_func(kwargs: dict): + # cast `cfg` back to DictDefault (ray tune deepcopy has issues with DictDefault so needed it to be dict) + # also renormalize the config now that TorchTrainer has spawned distributed workers + cfg = DictDefault(kwargs["cfg"]) + normalize_config(cfg) + + # now that we are on the worker node, we can check `is_torch_bf16_gpu_available` to resolve dtype + resolve_dtype(cfg) + + # ray serializing objects gets rid of frozen attribute - HF expects dict not DefaultDict + if cfg.deepspeed: + cfg.deepspeed = cfg.deepspeed.to_dict() + + # initialize accelerator before model instantiation + Accelerator(gradient_accumulation_steps=cfg.gradient_accumulation_steps) + + kwargs["cfg"] = cfg + + do_train(**kwargs) if __name__ == "__main__": diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 0bd400f6b0..8b5e0074c0 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -141,7 +141,9 @@ def train( model.config.save_pretrained(str(Path(cfg.output_dir))) # In case we want to stop early with ctrl+c, this is a nice to have to save the pretrained model - if cfg.local_rank == 0: + if ( + cfg.local_rank == 0 and not cfg.use_ray + ): # ray workers don't have access to this signal def terminate_handler(_, __, model_weakref): if model_weakref() is not None: diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index c23359f34d..7ddff62196 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -1,4 +1,5 @@ """Module for working with config dicts""" +import json import logging import os from typing import Optional @@ -56,6 +57,38 @@ def get_device(): cfg.device_map = None +def resolve_dtype(cfg): + if ( + cfg.bf16 == "auto" and not cfg.use_ray + ): # if we use ray we want to defer this check to the worker node + if is_torch_bf16_gpu_available(): + LOG.debug("bf16 support detected, enabling for this configuration.") + cfg.bf16 = True + else: + LOG.debug("bf16 support not detected, disabling for this configuration.") + cfg.bf16 = False + if cfg.fp16 is None: + cfg.fp16 = True + + if cfg.device == "mps": + cfg.load_in_8bit = False + cfg.tf32 = False + if cfg.bf16: + cfg.fp16 = True + cfg.bf16 = False + else: + torch.backends.cuda.matmul.allow_tf32 = cfg.tf32 or False + if cfg.bf16: + cfg.fp16 = False + + if cfg.bf16 or cfg.bfloat16: + cfg.torch_dtype = torch.bfloat16 + elif cfg.load_in_8bit or cfg.fp16 or cfg.float16: + cfg.torch_dtype = torch.float16 + else: + cfg.torch_dtype = torch.float32 + + def normalize_config(cfg): # setup some derived config / hyperparams cfg.gradient_accumulation_steps = cfg.gradient_accumulation_steps or ( @@ -82,33 +115,15 @@ def normalize_config(cfg): cfg.device_map = {"": int(os.environ.get("LOCAL_RANK", 0))} cfg.batch_size = cfg.batch_size * cfg.world_size - if cfg.bf16 == "auto": - if is_torch_bf16_gpu_available(): - LOG.debug("bf16 support detected, enabling for this configuration.") - cfg.bf16 = True - else: - LOG.debug("bf16 support not detected, disabling for this configuration.") - cfg.bf16 = False - if cfg.fp16 is None: - cfg.fp16 = True + if not cfg.use_ray: + # delay resolving dtype until on worker node when launching with ray + resolve_dtype(cfg) - if cfg.device == "mps": - cfg.load_in_8bit = False - cfg.tf32 = False - if cfg.bf16: - cfg.fp16 = True - cfg.bf16 = False - else: - torch.backends.cuda.matmul.allow_tf32 = cfg.tf32 or False - if cfg.bf16: - cfg.fp16 = False - - if cfg.bf16 or cfg.bfloat16: - cfg.torch_dtype = torch.bfloat16 - elif cfg.load_in_8bit or cfg.fp16 or cfg.float16: - cfg.torch_dtype = torch.float16 - else: - cfg.torch_dtype = torch.float32 + if cfg.deepspeed: + if isinstance(cfg.deepspeed, str) and os.path.exists(cfg.deepspeed): + ds_config_path = cfg.deepspeed + with open(ds_config_path, encoding="utf-8") as f: + cfg.deepspeed = json.load(f) if cfg.saves_per_epoch: save_steps = 1.0 / (cfg.saves_per_epoch * cfg.num_epochs) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 4f0fa4c29e..dc8897863d 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -607,6 +607,30 @@ class GradioConfig(BaseModel): gradio_temperature: Optional[float] = None +class RayConfig(BaseModel): + """Ray launcher configuration subset""" + + use_ray: bool = Field(default=False) + ray_run_name: Optional[str] = Field( + default=None, + metadata={ + "help": "The training results will be saved at `saves/ray_run_name`." + }, + ) + ray_num_workers: int = Field( + default=1, + metadata={ + "help": "The number of workers for Ray training. Default is 1 worker." + }, + ) + resources_per_worker: dict = Field( + default_factory=lambda: {"GPU": 1}, + metadata={ + "help": "The resources per worker for Ray training. Default is to use 1 GPU per worker." + }, + ) + + # pylint: disable=too-many-public-methods,too-many-ancestors class AxolotlInputConfig( ModelInputConfig, @@ -619,6 +643,7 @@ class AxolotlInputConfig( CometConfig, LISAConfig, GradioConfig, + RayConfig, RemappedParameters, DeprecatedParameters, BaseModel, diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index bdbd995870..bb1874b0bf 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -74,15 +74,13 @@ def test_lora_ddp(self, temp_dir): execute_subprocess_async( [ - "accelerate", - "launch", + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), "--num-processes", "2", - "--main_process_port", + "--main-process-port", f"{get_torch_dist_unique_port()}", - "-m", - "axolotl.cli.train", - str(Path(temp_dir) / "config.yaml"), ] ) @@ -139,15 +137,13 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): execute_subprocess_async( [ - "accelerate", - "launch", + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), "--num-processes", "2", - "--main_process_port", + "--main-process-port", f"{get_torch_dist_unique_port()}", - "-m", - "axolotl.cli.train", - str(Path(temp_dir) / "config.yaml"), ] ) @@ -214,15 +210,13 @@ def test_dpo_lora_ddp(self, temp_dir): execute_subprocess_async( [ - "accelerate", - "launch", + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), "--num-processes", "2", - "--main_process_port", + "--main-process-port", f"{get_torch_dist_unique_port()}", - "-m", - "axolotl.cli.train", - str(Path(temp_dir) / "config.yaml"), ] ) @@ -293,15 +287,13 @@ def test_dpo_qlora_ddp(self, temp_dir): execute_subprocess_async( [ - "accelerate", - "launch", + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), "--num-processes", "2", - "--main_process_port", + "--main-process-port", f"{get_torch_dist_unique_port()}", - "-m", - "axolotl.cli.train", - str(Path(temp_dir) / "config.yaml"), ] ) @@ -367,15 +359,13 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): execute_subprocess_async( [ - "accelerate", - "launch", + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), "--num-processes", "2", - "--main_process_port", + "--main-process-port", f"{get_torch_dist_unique_port()}", - "-m", - "axolotl.cli.train", - str(Path(temp_dir) / "config.yaml"), ] ) @@ -439,15 +429,13 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): execute_subprocess_async( [ - "accelerate", - "launch", + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), "--num-processes", "2", - "--main_process_port", + "--main-process-port", f"{get_torch_dist_unique_port()}", - "-m", - "axolotl.cli.train", - str(Path(temp_dir) / "config.yaml"), ] ) @@ -520,15 +508,13 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): execute_subprocess_async( [ - "accelerate", - "launch", + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), "--num-processes", "2", - "--main_process_port", + "--main-process-port", f"{get_torch_dist_unique_port()}", - "-m", - "axolotl.cli.train", - str(Path(temp_dir) / "config.yaml"), ] ) @@ -605,15 +591,13 @@ def test_ds_zero3_packed( execute_subprocess_async( [ - "accelerate", - "launch", + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), "--num-processes", "2", - "--main_process_port", + "--main-process-port", f"{get_torch_dist_unique_port()}", - "-m", - "axolotl.cli.train", - str(Path(temp_dir) / "config.yaml"), ] ) @@ -680,15 +664,13 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): execute_subprocess_async( [ - "accelerate", - "launch", + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), "--num-processes", "2", - "--main_process_port", + "--main-process-port", f"{get_torch_dist_unique_port()}", - "-m", - "axolotl.cli.train", - str(Path(temp_dir) / "config.yaml"), ] ) @@ -755,15 +737,13 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): execute_subprocess_async( [ - "accelerate", - "launch", + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), "--num-processes", "2", - "--main_process_port", + "--main-process-port", f"{get_torch_dist_unique_port()}", - "-m", - "axolotl.cli.train", - str(Path(temp_dir) / "config.yaml"), ] ) diff --git a/tests/e2e/multigpu/test_qwen2.py b/tests/e2e/multigpu/test_qwen2.py index 32bb6a3e13..2b98848483 100644 --- a/tests/e2e/multigpu/test_qwen2.py +++ b/tests/e2e/multigpu/test_qwen2.py @@ -86,14 +86,12 @@ def test_qlora_fsdp_dpo(self, base_model, temp_dir): execute_subprocess_async( [ - "accelerate", - "launch", + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), "--num-processes", "2", - "--main_process_port", + "--main-process-port", f"{get_torch_dist_unique_port()}", - "-m", - "axolotl.cli.train", - str(Path(temp_dir) / "config.yaml"), ] ) diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py new file mode 100644 index 0000000000..d7e4ddfcf7 --- /dev/null +++ b/tests/e2e/multigpu/test_ray.py @@ -0,0 +1,137 @@ +""" +E2E tests for multigpu post-training use Ray Train +""" + +import logging +import os +from pathlib import Path + +import pytest +import yaml +from accelerate.test_utils import execute_subprocess_async +from e2e.utils import check_tensorboard + +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger(__name__) +os.environ["WANDB_DISABLED"] = "true" + +AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent + + +class TestMultiGPURay: + """ + Test cases for AnyScale Ray post training + """ + + def test_lora_ddp(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 2048, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.05, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 4, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "use_tensorboard": True, + "use_ray": True, + "ray_num_workers": 2, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--use-ray", + "--ray-num-workers", + "2", + ] + ) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + ) + + @pytest.mark.parametrize( + "gradient_accumulation_steps", + [1, 2], + ) + def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sample_packing": True, + "pad_to_sequence_len": True, + "sequence_len": 2048, + "val_set_size": 0.05, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 1, + "gradient_accumulation_steps": gradient_accumulation_steps, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero2.json"), + "use_tensorboard": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--use-ray", + "--ray-num-workers", + "2", + ] + ) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + ) From 8779997ba54f8c95e789c7142ff8a5009865a723 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 30 Jan 2025 11:34:02 -0500 Subject: [PATCH 0392/1405] native support for modal cloud from CLI (#2237) * native support for modal cloud from CLI * do lm_eval in cloud too * Fix the sub call to lm-eval * lm_eval option to not post eval, and append not extend * cache bust when using branch, grab sha of latest image tag, update lm-eval dep * allow minimal yaml for lm eval * include modal in requirements * update link in README to include utm * pr feedback * use chat template * revision support * apply chat template as arg * add wandb name support, allow explicit a100-40gb * cloud is optional * handle accidental setting of tasks with a single task str * document the modal cloud yaml for clarity [skip ci] * cli docs * support spawn vs remote for lm-eval * Add support for additional docker commands in modal image build * cloud config shouldn't be a dir * Update README.md Co-authored-by: Charles Frye * fix annotation args --------- Co-authored-by: Charles Frye --- README.md | 2 +- docs/cli.qmd | 256 +++++++++++++++++ examples/cloud/modal.yaml | 28 ++ requirements.txt | 1 + scripts/motd | 17 +- src/axolotl/cli/cloud/__init__.py | 56 ++++ src/axolotl/cli/cloud/base.py | 18 ++ src/axolotl/cli/cloud/modal_.py | 282 +++++++++++++++++++ src/axolotl/cli/main.py | 67 +++-- src/axolotl/integrations/lm_eval/__init__.py | 41 ++- src/axolotl/integrations/lm_eval/args.py | 2 + src/axolotl/integrations/lm_eval/cli.py | 119 ++++++++ 12 files changed, 835 insertions(+), 54 deletions(-) create mode 100644 docs/cli.qmd create mode 100644 examples/cloud/modal.yaml create mode 100644 src/axolotl/cli/cloud/__init__.py create mode 100644 src/axolotl/cli/cloud/base.py create mode 100644 src/axolotl/cli/cloud/modal_.py create mode 100644 src/axolotl/integrations/lm_eval/cli.py diff --git a/README.md b/README.md index 6ee3237e7b..674a9dcf14 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ If you love axolotl, consider sponsoring the project by reaching out directly to --- -- [Modal](https://modal.com/) Modal lets you run data/AI jobs in the cloud, by just writing a few lines of Python. Customers use Modal to deploy Gen AI models at large scale, fine-tune LLM models, run protein folding simulations, and much more. +- [Modal](https://www.modal.com?utm_source=github&utm_medium=github&utm_campaign=axolotl) Modal lets you run data/AI jobs in the cloud, by just writing a few lines of Python. Customers use Modal to deploy Gen AI models at large scale, fine-tune large language models, run protein folding simulations, and much more. --- diff --git a/docs/cli.qmd b/docs/cli.qmd new file mode 100644 index 0000000000..5b494ab5de --- /dev/null +++ b/docs/cli.qmd @@ -0,0 +1,256 @@ +# Axolotl CLI Documentation + +The Axolotl CLI provides a streamlined interface for training and fine-tuning large language models. This guide covers +the CLI commands, their usage, and common examples. + +### Table of Contents + +- Basic Commands +- Command Reference + - fetch + - preprocess + - train + - inference + - merge-lora + - merge-sharded-fsdp-weights + - evaluate + - lm-eval +- Legacy CLI Usage +- Remote Compute with Modal Cloud + - Cloud Configuration + - Running on Modal Cloud + - Cloud Configuration Options + + +### Basic Commands + +All Axolotl commands follow this general structure: + +```bash +axolotl [config.yml] [options] +``` + +The config file can be local or a URL to a raw YAML file. + +### Command Reference + +#### fetch + +Downloads example configurations and deepspeed configs to your local machine. + +```bash +# Get example YAML files +axolotl fetch examples + +# Get deepspeed config files +axolotl fetch deepspeed_configs + +# Specify custom destination +axolotl fetch examples --dest path/to/folder +``` + +#### preprocess + +Preprocesses and tokenizes your dataset before training. This is recommended for large datasets. + +```bash +# Basic preprocessing +axolotl preprocess config.yml + +# Preprocessing with one GPU +CUDA_VISIBLE_DEVICES="0" axolotl preprocess config.yml + +# Debug mode to see processed examples +axolotl preprocess config.yml --debug + +# Debug with limited examples +axolotl preprocess config.yml --debug --debug-num-examples 5 +``` + +Configuration options: + +```yaml +dataset_prepared_path: Local folder for saving preprocessed data +push_dataset_to_hub: HuggingFace repo to push preprocessed data (optional) +``` + +#### train + +Trains or fine-tunes a model using the configuration specified in your YAML file. + +```bash +# Basic training +axolotl train config.yml + +# Train and set/override specific options +axolotl train config.yml \ + --learning-rate 1e-4 \ + --micro-batch-size 2 \ + --num-epochs 3 + +# Training without accelerate +axolotl train config.yml --no-accelerate + +# Resume training from checkpoint +axolotl train config.yml --resume-from-checkpoint path/to/checkpoint +``` + +#### inference + +Runs inference using your trained model in either CLI or Gradio interface mode. + +```bash +# CLI inference with LoRA +axolotl inference config.yml --lora-model-dir="./outputs/lora-out" + +# CLI inference with full model +axolotl inference config.yml --base-model="./completed-model" + +# Gradio web interface +axolotl inference config.yml --gradio \ + --lora-model-dir="./outputs/lora-out" + +# Inference with input from file +cat prompt.txt | axolotl inference config.yml \ + --base-model="./completed-model" +``` + +#### merge-lora + +Merges trained LoRA adapters into the base model. + +```bash +# Basic merge +axolotl merge-lora config.yml + +# Specify LoRA directory (usually used with checkpoints) +axolotl merge-lora config.yml --lora-model-dir="./lora-output/checkpoint-100" + +# Merge using CPU (if out of GPU memory) +CUDA_VISIBLE_DEVICES="" axolotl merge-lora config.yml +``` + +Configuration options: + +```yaml +gpu_memory_limit: Limit GPU memory usage +lora_on_cpu: Load LoRA weights on CPU +``` + +#### merge-sharded-fsdp-weights + +Merges sharded FSDP model checkpoints into a single combined checkpoint. + +```bash +# Basic merge +axolotl merge-sharded-fsdp-weights config.yml +``` + +#### evaluate + +Evaluates a model's performance using metrics specified in the config. + +```bash +# Basic evaluation +axolotl evaluate config.yml +``` + +#### lm-eval + +Runs LM Evaluation Harness on your model. + +```bash +# Basic evaluation +axolotl lm-eval config.yml + +# Evaluate specific tasks +axolotl lm-eval config.yml --tasks arc_challenge,hellaswag +``` + +Configuration options: + +```yaml +lm_eval_tasks: List of tasks to evaluate +lm_eval_batch_size: Batch size for evaluation +output_dir: Directory to save evaluation results +``` + +### Legacy CLI Usage + +While the new Click-based CLI is preferred, Axolotl still supports the legacy module-based CLI: + +```bash +# Preprocess +python -m axolotl.cli.preprocess config.yml + +# Train +accelerate launch -m axolotl.cli.train config.yml + +# Inference +accelerate launch -m axolotl.cli.inference config.yml \ + --lora_model_dir="./outputs/lora-out" + +# Gradio interface +accelerate launch -m axolotl.cli.inference config.yml \ + --lora_model_dir="./outputs/lora-out" --gradio +``` + +### Remote Compute with Modal Cloud + +Axolotl supports running training and inference workloads on Modal cloud infrastructure. This is configured using a +cloud YAML file alongside your regular Axolotl config. + +#### Cloud Configuration + +Create a cloud config YAML with your Modal settings: + +```yaml +# cloud_config.yml +provider: modal +gpu: a100 # Supported: l40s, a100-40gb, a100-80gb, a10g, h100, t4, l4 +gpu_count: 1 # Number of GPUs to use +timeout: 86400 # Maximum runtime in seconds (24 hours) +branch: main # Git branch to use (optional) + +volumes: # Persistent storage volumes + - name: axolotl-cache + mount: /workspace/cache + +env: # Environment variables + - WANDB_API_KEY + - HF_TOKEN +``` + +#### Running on Modal Cloud + +Commands that support the --cloud flag: + +```bash +# Preprocess on cloud +axolotl preprocess config.yml --cloud cloud_config.yml + +# Train on cloud +axolotl train config.yml --cloud cloud_config.yml + +# Train without accelerate on cloud +axolotl train config.yml --cloud cloud_config.yml --no-accelerate + +# Run lm-eval on cloud +axolotl lm-eval config.yml --cloud cloud_config.yml +``` + +#### Cloud Configuration Options + +```yaml +provider: compute provider, currently only `modal` is supported +gpu: GPU type to use +gpu_count: Number of GPUs (default: 1) +memory: RAM in GB (default: 128) +timeout: Maximum runtime in seconds +timeout_preprocess: Preprocessing timeout +branch: Git branch to use +docker_tag: Custom Docker image tag +volumes: List of persistent storage volumes +env: Environment variables to pass +secrets: Secrets to inject +``` diff --git a/examples/cloud/modal.yaml b/examples/cloud/modal.yaml new file mode 100644 index 0000000000..1950314948 --- /dev/null +++ b/examples/cloud/modal.yaml @@ -0,0 +1,28 @@ +project_name: +volumes: + - name: axolotl-data + mount: /workspace/data + - name: axolotl-artifacts + mount: /workspace/artifacts + +# environment variables from local to set as secrets +secrets: + - HF_TOKEN + - WANDB_API_KEY + +# Which branch of axolotl to use remotely +branch: + +# additional custom commands when building the image +dockerfile_commands: + +gpu: h100 +gpu_count: 1 + +# Train specific configurations +memory: 128 +timeout: 86400 + +# Preprocess specific configurations +memory_preprocess: 32 +timeout_preprocess: 14400 diff --git a/requirements.txt b/requirements.txt index 446fa94a63..061229c697 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,6 +25,7 @@ hf_transfer sentencepiece gradio==3.50.2 +modal==0.70.5 pydantic==2.6.3 addict fire diff --git a/scripts/motd b/scripts/motd index b3ffa165e6..bc123c312d 100644 --- a/scripts/motd +++ b/scripts/motd @@ -1,10 +1,15 @@ - dP dP dP - 88 88 88 - .d8888b. dP. .dP .d8888b. 88 .d8888b. d8888P 88 - 88' `88 `8bd8' 88' `88 88 88' `88 88 88 - 88. .88 .d88b. 88. .88 88 88. .88 88 88 - `88888P8 dP' `dP `88888P' dP `88888P' dP dP + #@@ #@@ @@# @@# + @@ @@ @@ @@ =@@# @@ #@ =@@#. + @@ #@@@@@@@@@ @@ #@#@= @@ #@ .=@@ + #@@@@@@@@@@@@@@@@@ =@# @# ##= ## =####=+ @@ =#####+ =#@@###. @@ + @@@@@@@@@@/ +@@/ +@@ #@ =@= #@= @@ =@#+ +#@# @@ =@#+ +#@# #@. @@ + @@@@@@@@@@ ##@@ ##@@ =@# @# =@# @# @@ @@ @@ @@ #@ #@ @@ + @@@@@@@@@@@@@@@@@@@@ #@=+++#@= =@@# @@ @@ @@ @@ #@ #@ @@ + =@#=====@@ =@# @# @@ @@ @@ @@ #@ #@ @@ + @@@@@@@@@@@@@@@@ @@@@ #@ #@= #@= +@@ #@# =@# @@. =@# =@# #@. @@ + =@# @# #@= #@ =#@@@@#= +#@@= +#@@@@#= .##@@+ @@ + @@@@ @@@@@@@@@@@@@@@@ Welcome to the axolotl cloud image! If the you've mounted a disk to /workspace and the axolotl directory ie empty, run the following commands: diff --git a/src/axolotl/cli/cloud/__init__.py b/src/axolotl/cli/cloud/__init__.py new file mode 100644 index 0000000000..fde46e3971 --- /dev/null +++ b/src/axolotl/cli/cloud/__init__.py @@ -0,0 +1,56 @@ +""" +launch axolotl in supported cloud platforms +""" +from pathlib import Path +from typing import Union + +import yaml + +from axolotl.cli.art import print_axolotl_text_art +from axolotl.cli.cloud.modal_ import ModalCloud +from axolotl.utils.dict import DictDefault + + +def load_cloud_cfg(cloud_config: Union[Path, str]) -> DictDefault: + """Load and validate cloud configuration.""" + # Load cloud configuration. + with open(cloud_config, encoding="utf-8") as file: + cloud_cfg: DictDefault = DictDefault(yaml.safe_load(file)) + return cloud_cfg + + +def do_cli_preprocess( + cloud_config: Union[Path, str], + config: Union[Path, str], +) -> None: + print_axolotl_text_art() + cloud_cfg = load_cloud_cfg(cloud_config) + cloud = ModalCloud(cloud_cfg) + with open(config, "r", encoding="utf-8") as file: + config_yaml = file.read() + cloud.preprocess(config_yaml) + + +def do_cli_train( + cloud_config: Union[Path, str], + config: Union[Path, str], + accelerate: bool = True, +) -> None: + print_axolotl_text_art() + cloud_cfg = load_cloud_cfg(cloud_config) + cloud = ModalCloud(cloud_cfg) + with open(config, "r", encoding="utf-8") as file: + config_yaml = file.read() + cloud.train(config_yaml, accelerate=accelerate) + + +def do_cli_lm_eval( + cloud_config: Union[Path, str], + config: Union[Path, str], +) -> None: + print_axolotl_text_art() + cloud_cfg = load_cloud_cfg(cloud_config) + cloud = ModalCloud(cloud_cfg) + with open(config, "r", encoding="utf-8") as file: + config_yaml = file.read() + cloud.lm_eval(config_yaml) diff --git a/src/axolotl/cli/cloud/base.py b/src/axolotl/cli/cloud/base.py new file mode 100644 index 0000000000..44d1b0c17f --- /dev/null +++ b/src/axolotl/cli/cloud/base.py @@ -0,0 +1,18 @@ +""" +base class for cloud platforms from cli +""" +from abc import ABC, abstractmethod + + +class Cloud(ABC): + """ + Abstract base class for cloud platforms. + """ + + @abstractmethod + def preprocess(self, config_yaml: str, *args, **kwargs) -> None: + pass + + @abstractmethod + def train(self, config_yaml: str, accelerate: bool = True) -> str: + pass diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py new file mode 100644 index 0000000000..bcc47ead9e --- /dev/null +++ b/src/axolotl/cli/cloud/modal_.py @@ -0,0 +1,282 @@ +""" +Modal Cloud support from CLI +""" +import copy +import json +import os +import subprocess # nosec B404 +from pathlib import Path +from random import randint + +import modal + +from axolotl.cli.cloud.base import Cloud + + +def run_cmd(cmd: str, run_folder: str, volumes=None): + """Run a command inside a folder, with Modal Volume reloading before and commit on success.""" + # Ensure volumes contain latest files. + if volumes: + for _, vol in volumes.items(): + vol.reload() + + # modal workaround so it doesn't use the automounted axolotl + new_env = copy.deepcopy(os.environ) + if "PYTHONPATH" in new_env: + del new_env["PYTHONPATH"] + + # Propagate errors from subprocess. + if exit_code := subprocess.call( # nosec B603 + cmd.split(), cwd=run_folder, env=new_env + ): + exit(exit_code) # pylint: disable=consider-using-sys-exit + + # Commit writes to volume. + if volumes: + for _, vol in volumes.items(): + vol.commit() + + +class ModalCloud(Cloud): + """ + Modal Cloud implementation. + """ + + def __init__(self, config, app=None): + self.config = config + if not app: + app = modal.App() + self.app = app + + self.volumes = {} + if config.volumes: + for volume_config in config.volumes: + _, mount, vol = self.create_volume(volume_config) + self.volumes[mount] = (vol, volume_config) + + def get_env(self): + res = { + "HF_DATASETS_CACHE": "/workspace/data/huggingface-cache/datasets", + "HF_HUB_CACHE": "/workspace/data/huggingface-cache/hub", + } + + for key in self.config.get("env", []): + if isinstance(key, str): + if val := os.environ.get(key, ""): + res[key] = val + elif isinstance(key, dict): + (key_, val) = list(key.items())[0] + res[key_] = val + return res + + def get_image(self): + docker_tag = "main-py3.11-cu124-2.5.1" + if self.config.docker_tag: + docker_tag = self.config.docker_tag + docker_image = f"axolotlai/axolotl:{docker_tag}" + + # grab the sha256 hash from docker hub for this image+tag + # this ensures that we always get the latest image for this tag, even if it's already cached + try: + manifest = subprocess.check_output( # nosec B602 + f"docker manifest inspect {docker_image}", + shell=True, + ).decode("utf-8") + sha256_hash = json.loads(manifest)["manifests"][0]["digest"] + except subprocess.CalledProcessError: + sha256_hash = None + + # create the image + if sha256_hash: + image = modal.Image.from_registry(f"axolotlai/axolotl@{sha256_hash}") + else: + image = modal.Image.from_registry(docker_image) + + dockerfile_commands = [] + if self.config.dockerfile_commands: + dockerfile_commands.extend(self.config.dockerfile_commands) + + # branch + if self.config.branch: + dockerfile_commands.extend( + [ + # Random id for cache busting of branch commits + f"RUN echo '{str(randint(0, 1000000))}'", # nosec B311 + f"RUN cd /workspace/axolotl && git fetch && git checkout {self.config.branch}", + ] + ) + + if dockerfile_commands: + image = image.dockerfile_commands(dockerfile_commands) + + if env := self.get_env(): + image = image.env(env) + + image = image.pip_install("fastapi==0.110.0", "pydantic==2.6.3") + + return image + + def get_secrets(self): + res = [] + if self.config.secrets: + for key in self.config.get("secrets", []): + # pylint: disable=duplicate-code + if isinstance(key, str): + if val := os.environ.get(key, ""): + res.append(modal.Secret.from_dict({key: val})) + elif isinstance(key, dict): + (key_, val) = list(key.items())[0] + res.append(modal.Secret.from_dict({key_: val})) + return res + + def create_volume(self, volume_config): + name = volume_config.name + mount = volume_config.mount + return name, mount, modal.Volume.from_name(name, create_if_missing=True) + + def get_ephemeral_disk_size(self): + return 1000 * 525 # 1 TiB + + def get_preprocess_timeout(self): + if self.config.timeout_preprocess: + return int(self.config.timeout_preprocess) + return 60 * 60 * 3 # 3 hours + + def get_preprocess_memory(self): + memory = 128 # default to 128GiB + if self.config.memory: + memory = int(self.config.memory) + if self.config.memory_preprocess: + memory = int(self.config.memory_preprocess) + return 1024 * memory + + def get_preprocess_env(self): + return self.app.function( + image=self.get_image(), + volumes={k: v[0] for k, v in self.volumes.items()}, + cpu=8.0, + ephemeral_disk=self.get_ephemeral_disk_size(), + memory=self.get_preprocess_memory(), + timeout=self.get_preprocess_timeout(), + secrets=self.get_secrets(), + ) + + def preprocess(self, config_yaml: str, *args, **kwargs): + modal_fn = self.get_preprocess_env()(_preprocess) + with modal.enable_output(): + with self.app.run(detach=True): + modal_fn.remote( + config_yaml, + volumes={k: v[0] for k, v in self.volumes.items()}, + *args, + **kwargs, + ) + + def get_train_timeout(self): + if self.config.timeout: + return int(self.config.timeout) + return 60 * 60 * 24 # 24 hours + + def get_train_gpu(self): # pylint: disable=too-many-return-statements + count = self.config.gpu_count or 1 + family = self.config.gpu.lower() or "l40s" + + if family == "l40s": + return modal.gpu.L40S(count=count) + if family in ["a100", "a100-40gb"]: + return modal.gpu.A100(count=count, size="40GB") + if family == "a100-80gb": + return modal.gpu.A100(count=count, size="80GB") + if family in ["a10", "a10g"]: + return modal.gpu.A10G(count=count) + if family == "h100": + return modal.gpu.H100(count=count) + if family == "t4": + return modal.gpu.T4(count=count) + if family == "l4": + return modal.gpu.L4(count=count) + raise ValueError(f"Unsupported GPU family: {family}") + + def get_train_memory(self): + memory = 128 # default to 128GiB + if self.config.memory: + memory = int(self.config.memory) + return 1024 * memory + + def get_train_env(self): + return self.app.function( + image=self.get_image(), + volumes={k: v[0] for k, v in self.volumes.items()}, + cpu=16.0, + gpu=self.get_train_gpu(), + memory=self.get_train_memory(), + timeout=self.get_train_timeout(), + secrets=self.get_secrets(), + ) + + def train(self, config_yaml: str, accelerate: bool = True): + modal_fn = self.get_train_env()(_train) + with modal.enable_output(): + with self.app.run(detach=True): + modal_fn.remote( + config_yaml, + accelerate=accelerate, + volumes={k: v[0] for k, v in self.volumes.items()}, + ) + + def lm_eval(self, config_yaml: str): + modal_fn = self.get_train_env()(_lm_eval) + with modal.enable_output(): + with self.app.run(detach=True): + if self.config.get("spawn", False): + modal_fn_exec = modal_fn.spawn + else: + modal_fn_exec = modal_fn.remote + modal_fn_exec( + config_yaml, + volumes={k: v[0] for k, v in self.volumes.items()}, + ) + + +def _preprocess(config_yaml: str, volumes=None): + Path("/workspace/artifacts/axolotl").mkdir(parents=True, exist_ok=True) + with open( + "/workspace/artifacts/axolotl/config.yaml", "w", encoding="utf-8" + ) as f_out: + f_out.write(config_yaml) + run_folder = "/workspace/artifacts/axolotl" + run_cmd( + "axolotl preprocess /workspace/artifacts/axolotl/config.yaml --dataset-processes=8", + run_folder, + volumes, + ) + + +def _train(config_yaml: str, accelerate: bool = True, volumes=None): + with open( + "/workspace/artifacts/axolotl/config.yaml", "w", encoding="utf-8" + ) as f_out: + f_out.write(config_yaml) + run_folder = "/workspace/artifacts/axolotl" + if accelerate: + accelerate_args = "--accelerate" + else: + accelerate_args = "--no-accelerate" + run_cmd( + f"axolotl train {accelerate_args} /workspace/artifacts/axolotl/config.yaml", + run_folder, + volumes, + ) + + +def _lm_eval(config_yaml: str, volumes=None): + with open( + "/workspace/artifacts/axolotl/config.yaml", "w", encoding="utf-8" + ) as f_out: + f_out.write(config_yaml) + run_folder = "/workspace/artifacts/axolotl" + run_cmd( + "axolotl lm-eval /workspace/artifacts/axolotl/config.yaml", + run_folder, + volumes, + ) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index 801fbc80d2..e8551511e7 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -15,6 +15,7 @@ fetch_from_github, filter_none_kwargs, ) +from axolotl.integrations.lm_eval.cli import lm_eval from axolotl.utils import set_pytorch_cuda_alloc_conf from axolotl.utils.config.models.input.v0_4_1 import AxolotlInputConfig @@ -27,21 +28,28 @@ def cli(): @cli.command() @click.argument("config", type=click.Path(exists=True, path_type=str)) +@click.option("--cloud", default=None, type=click.Path(exists=True, path_type=str)) @add_options_from_dataclass(PreprocessCliArgs) @add_options_from_config(AxolotlInputConfig) @filter_none_kwargs -def preprocess(config: str, **kwargs) -> None: +def preprocess(config: str, cloud: Optional[str] = None, **kwargs) -> None: """ Preprocess datasets before training. Args: config: Path to `axolotl` config YAML file. + cloud: Path to a cloud accelerator configuration file. kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` config options. """ - from axolotl.cli.preprocess import do_cli + if cloud: + from axolotl.cli.cloud import do_cli_preprocess - do_cli(config=config, **kwargs) + do_cli_preprocess(cloud_config=cloud, config=config) + else: + from axolotl.cli.preprocess import do_cli + + do_cli(config=config, **kwargs) @cli.command() @@ -51,47 +59,56 @@ def preprocess(config: str, **kwargs) -> None: default=True, help="Use accelerate launch for multi-GPU training", ) +@click.option("--cloud", default=None, type=click.Path(exists=True, path_type=str)) @add_options_from_dataclass(TrainerCliArgs) @add_options_from_config(AxolotlInputConfig) @filter_none_kwargs -def train(config: str, accelerate: bool, **kwargs) -> None: +def train(config: str, accelerate: bool, cloud: Optional[str] = None, **kwargs) -> None: """ Train or fine-tune a model. Args: config: Path to `axolotl` config YAML file. accelerate: Whether to use `accelerate` launcher. + cloud: Path to a cloud accelerator configuration file kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` config options. """ # Enable expandable segments for cuda allocation to improve VRAM usage set_pytorch_cuda_alloc_conf() + from axolotl.cli.cloud import do_cli_train if "use_ray" in kwargs and kwargs["use_ray"]: accelerate = False if accelerate: - accelerate_args = [] - if "main_process_port" in kwargs: - main_process_port = kwargs.pop("main_process_port", None) - accelerate_args.append("--main_process_port") - accelerate_args.append(str(main_process_port)) - if "num_processes" in kwargs: - num_processes = kwargs.pop("num_processes", None) - accelerate_args.append("--num-processes") - accelerate_args.append(str(num_processes)) - - base_cmd = ["accelerate", "launch"] - base_cmd.extend(accelerate_args) - base_cmd.extend(["-m", "axolotl.cli.train"]) - if config: - base_cmd.append(config) - cmd = build_command(base_cmd, kwargs) - subprocess.run(cmd, check=True) # nosec B603 + if cloud: + do_cli_train(cloud_config=cloud, config=config, accelerate=True) + else: + accelerate_args = [] + if "main_process_port" in kwargs: + main_process_port = kwargs.pop("main_process_port", None) + accelerate_args.append("--main_process_port") + accelerate_args.append(str(main_process_port)) + if "num_processes" in kwargs: + num_processes = kwargs.pop("num_processes", None) + accelerate_args.append("--num-processes") + accelerate_args.append(str(num_processes)) + + base_cmd = ["accelerate", "launch"] + base_cmd.extend(accelerate_args) + base_cmd.extend(["-m", "axolotl.cli.train"]) + if config: + base_cmd.append(config) + cmd = build_command(base_cmd, kwargs) + subprocess.run(cmd, check=True) # nosec B603 else: - from axolotl.cli.train import do_cli + if cloud: + do_cli_train(cloud_config=cloud, config=config, accelerate=False) + else: + from axolotl.cli.train import do_cli - do_cli(config=config, **kwargs) + do_cli(config=config, **kwargs) @cli.command() @@ -210,7 +227,6 @@ def merge_lora(config: str, **kwargs) -> None: Args: config: Path to `axolotl` config YAML file. - accelerate: Whether to use `accelerate` launcher. kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` config options. """ @@ -237,6 +253,9 @@ def fetch(directory: str, dest: Optional[str]) -> None: fetch_from_github(f"{directory}/", dest) +cli.add_command(lm_eval) + + def main(): cli() diff --git a/src/axolotl/integrations/lm_eval/__init__.py b/src/axolotl/integrations/lm_eval/__init__.py index f1daa20000..0cbc8a49d1 100644 --- a/src/axolotl/integrations/lm_eval/__init__.py +++ b/src/axolotl/integrations/lm_eval/__init__.py @@ -2,9 +2,9 @@ Module for the Plugin for LM Eval Harness """ import subprocess # nosec -from datetime import datetime from axolotl.integrations.base import BasePlugin +from axolotl.integrations.lm_eval.cli import build_lm_eval_command from .args import LMEvalArgs # pylint: disable=unused-import. # noqa: F401 @@ -18,25 +18,20 @@ def get_input_args(self): return "axolotl.integrations.lm_eval.LMEvalArgs" def post_train_unload(self, cfg): - tasks = ",".join(cfg.lm_eval_tasks) - fa2 = ",attn_implementation=flash_attention_2" if cfg.flash_attention else "" - dtype = ",dtype=bfloat16" if cfg.bf16 else ",dtype=float16" - output_path = cfg.output_dir - output_path += "" if cfg.output_dir.endswith("/") else "/" - output_path += "lm_eval_results/" + datetime.now().strftime("%Y%m%d_%H%M%S") - subprocess.run( # nosec - [ - "lm_eval", - "--model", - "hf", - "--model_args", - f"pretrained={cfg.output_dir}{fa2}{dtype}", - "--tasks", - tasks, - "--batch_size", - str(cfg.lm_eval_batch_size), - "--output_path", - output_path, - ], - check=True, - ) + if cfg.lm_eval_post_train: + # pylint: disable=duplicate-code + for lm_eval_args in build_lm_eval_command( + cfg.lm_eval_tasks, + bfloat16=cfg.bfloat16 or cfg.bf16, + flash_attention=cfg.flash_attention, + output_dir=cfg.output_dir, + batch_size=cfg.lm_eval_batch_size, + wandb_project=cfg.wandb_project, + wandb_entity=cfg.wandb_entity, + wandb_name=cfg.wandb_name, + model=cfg.lm_eval_model or cfg.hub_model_id, + ): + subprocess.run( # nosec + lm_eval_args, + check=True, + ) diff --git a/src/axolotl/integrations/lm_eval/args.py b/src/axolotl/integrations/lm_eval/args.py index f58e6a6e38..721f560e3f 100644 --- a/src/axolotl/integrations/lm_eval/args.py +++ b/src/axolotl/integrations/lm_eval/args.py @@ -13,3 +13,5 @@ class LMEvalArgs(BaseModel): lm_eval_tasks: List[str] = [] lm_eval_batch_size: Optional[int] = 8 + lm_eval_post_train: Optional[bool] = True + lm_eval_model: Optional[str] = None diff --git a/src/axolotl/integrations/lm_eval/cli.py b/src/axolotl/integrations/lm_eval/cli.py new file mode 100644 index 0000000000..4a9bbafe6e --- /dev/null +++ b/src/axolotl/integrations/lm_eval/cli.py @@ -0,0 +1,119 @@ +""" +axolotl CLI for running lm_eval tasks +""" +import subprocess # nosec +from collections import defaultdict +from datetime import datetime +from typing import Optional + +import click +import yaml + +from axolotl.utils.dict import DictDefault + + +def build_lm_eval_command( + tasks: list[str], + bfloat16=True, + flash_attention=False, + output_dir="./", + batch_size=8, + wandb_project=None, + wandb_entity=None, + wandb_name=None, + model=None, + revision=None, + apply_chat_template=None, + fewshot_as_multiturn=None, +): + tasks_by_num_fewshot: dict[str, list] = defaultdict(list) + if isinstance(tasks, str): + tasks = [tasks] + for task in tasks: + num_fewshot = "-1" + task_parts = task.split(":") + task_name = task_parts[0] + if len(task_parts) == 2: + task_name, num_fewshot = task_parts + tasks_by_num_fewshot[str(num_fewshot)].append(task_name) + + for num_fewshot, tasks_list in tasks_by_num_fewshot.items(): + tasks_str = ",".join(tasks_list) + num_fewshot_val = num_fewshot if num_fewshot != "-1" else None + pretrained = "pretrained=" + pretrained += model if model else output_dir + fa2 = ",attn_implementation=flash_attention_2" if flash_attention else "" + dtype = ",dtype=bfloat16" if bfloat16 else ",dtype=float16" + revision = f",revision={revision}" if revision else "" + output_path = output_dir + output_path += "" if output_dir.endswith("/") else "/" + output_path += "lm_eval_results/" + datetime.now().strftime("%Y%m%d_%H%M%S") + lm_eval_args = [ + "lm_eval", + "--model", + "hf", + "--model_args", + f"{pretrained}{fa2}{dtype}{revision}", + "--tasks", + tasks_str, + "--batch_size", + str(batch_size), + "--output_path", + output_path, + ] + wandb_args = [] + if wandb_project: + wandb_args.append(f"project={wandb_project}") + if wandb_entity: + wandb_args.append(f"entity={wandb_entity}") + if wandb_name: + wandb_args.append(f"name={wandb_name}") + if wandb_args: + lm_eval_args.append("--wandb_args") + lm_eval_args.append(",".join(wandb_args)) + if apply_chat_template: + lm_eval_args.append("--apply_chat_template") + if num_fewshot_val: + lm_eval_args.append("--num_fewshot") + lm_eval_args.append(str(num_fewshot_val)) + if apply_chat_template and fewshot_as_multiturn: + lm_eval_args.append("--fewshot_as_multiturn") + + yield lm_eval_args + + +@click.command() +@click.argument("config", type=click.Path(exists=True, path_type=str)) +@click.option("--cloud", default=None, type=click.Path(exists=True, path_type=str)) +def lm_eval(config: str, cloud: Optional[str] = None): + """ + use lm eval to evaluate a trained language model + """ + + if cloud: + from axolotl.cli.cloud import do_cli_lm_eval + + do_cli_lm_eval(cloud_config=cloud, config=config) + else: + with open(config, encoding="utf-8") as file: + cfg: DictDefault = DictDefault(yaml.safe_load(file)) + + # pylint: disable=duplicate-code + for lm_eval_args in build_lm_eval_command( + cfg.lm_eval_tasks, + bfloat16=cfg.bfloat16 or cfg.bf16, + flash_attention=cfg.flash_attention, + output_dir=cfg.output_dir, + batch_size=cfg.lm_eval_batch_size, + wandb_project=cfg.wandb_project, + wandb_entity=cfg.wandb_entity, + wandb_name=cfg.wandb_name, + model=cfg.lm_eval_model or cfg.hub_model_id, + revision=cfg.revision, + apply_chat_template=cfg.apply_chat_template, + fewshot_as_multiturn=cfg.fewshot_as_multiturn, + ): + subprocess.run( # nosec + lm_eval_args, + check=True, + ) From ac471a697a99a77fd92c2b3a2228a838e2a77310 Mon Sep 17 00:00:00 2001 From: salman Date: Thu, 30 Jan 2025 16:45:56 +0000 Subject: [PATCH 0393/1405] updating to fused (#2293) --- examples/cerebras/btlm-ft.yml | 2 +- examples/deepseek-v2/fft-fsdp-16b.yaml | 2 +- examples/deepseek-v2/qlora-fsdp-2_5.yaml | 2 +- examples/jamba/qlora_fsdp_large.yaml | 2 +- examples/llama-2/gptq-lora.yml | 2 +- examples/llama-2/qlora-fsdp.yml | 2 +- examples/llama-3/fft-8b-liger-fsdp.yaml | 2 +- examples/llama-3/qlora-fsdp-405b.yaml | 2 +- examples/llama-3/qlora-fsdp-70b.yaml | 2 +- examples/mistral/lora-mps.yml | 2 +- examples/mistral/mixtral-8x22b-qlora-fsdp.yml | 2 +- examples/mistral/mixtral-qlora-fsdp.yml | 2 +- examples/phi/phi-ft.yml | 2 +- examples/phi/phi-qlora.yml | 2 +- examples/phi/phi2-ft.yml | 2 +- examples/phi/phi3-ft-fsdp.yml | 2 +- examples/phi/phi3-ft.yml | 2 +- examples/qwen2/qlora-fsdp.yaml | 2 +- examples/tiny-llama/lora-mps.yml | 2 +- tests/core/test_trainer_builder.py | 2 +- tests/e2e/integrations/test_cut_cross_entropy.py | 2 +- tests/e2e/integrations/test_liger.py | 4 ++-- tests/e2e/multigpu/test_llama.py | 12 ++++++------ tests/e2e/multigpu/test_qwen2.py | 2 +- tests/e2e/patched/test_4d_multipack_llama.py | 4 ++-- tests/e2e/patched/test_fused_llama.py | 2 +- tests/e2e/patched/test_llama_s2_attention.py | 4 ++-- tests/e2e/patched/test_lora_llama_multipack.py | 4 ++-- tests/e2e/patched/test_mistral_samplepack.py | 4 ++-- tests/e2e/test_embeddings_lr.py | 4 ++-- tests/e2e/test_falcon.py | 6 +++--- tests/e2e/test_llama_pretrain.py | 2 +- tests/e2e/test_load_model.py | 2 +- tests/e2e/test_lora_llama.py | 2 +- tests/e2e/test_mamba.py | 2 +- tests/e2e/test_mistral.py | 4 ++-- tests/e2e/test_packing_loss.py | 2 +- 37 files changed, 51 insertions(+), 51 deletions(-) diff --git a/examples/cerebras/btlm-ft.yml b/examples/cerebras/btlm-ft.yml index 780616e047..44be539966 100644 --- a/examples/cerebras/btlm-ft.yml +++ b/examples/cerebras/btlm-ft.yml @@ -46,7 +46,7 @@ output_dir: ./outputs/btlm-out gradient_accumulation_steps: 1 micro_batch_size: 1 num_epochs: 1 -optimizer: adamw_torch +optimizer: adamw_torch_fused adam_beta2: 0.95 adam_eps: 0.000000001 max_grad_norm: 1.0 diff --git a/examples/deepseek-v2/fft-fsdp-16b.yaml b/examples/deepseek-v2/fft-fsdp-16b.yaml index 3d4608a016..649317494a 100644 --- a/examples/deepseek-v2/fft-fsdp-16b.yaml +++ b/examples/deepseek-v2/fft-fsdp-16b.yaml @@ -27,7 +27,7 @@ wandb_log_model: gradient_accumulation_steps: 8 micro_batch_size: 1 num_epochs: 1 -optimizer: adamw_torch +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 2e-5 diff --git a/examples/deepseek-v2/qlora-fsdp-2_5.yaml b/examples/deepseek-v2/qlora-fsdp-2_5.yaml index a89dc343a2..009e46a2fb 100644 --- a/examples/deepseek-v2/qlora-fsdp-2_5.yaml +++ b/examples/deepseek-v2/qlora-fsdp-2_5.yaml @@ -47,7 +47,7 @@ peft_use_rslora: true gradient_accumulation_steps: 1 micro_batch_size: 8 num_epochs: 1 -optimizer: adamw_torch +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 2e-5 diff --git a/examples/jamba/qlora_fsdp_large.yaml b/examples/jamba/qlora_fsdp_large.yaml index 8736680da8..594847330d 100644 --- a/examples/jamba/qlora_fsdp_large.yaml +++ b/examples/jamba/qlora_fsdp_large.yaml @@ -34,7 +34,7 @@ lora_target_linear: false gradient_accumulation_steps: 4 micro_batch_size: 1 num_epochs: 2 -optimizer: adamw_torch +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.00001 diff --git a/examples/llama-2/gptq-lora.yml b/examples/llama-2/gptq-lora.yml index 7e45f7d639..7d6b90ee3a 100644 --- a/examples/llama-2/gptq-lora.yml +++ b/examples/llama-2/gptq-lora.yml @@ -42,7 +42,7 @@ output_dir: ./outputs/model-out gradient_accumulation_steps: 1 micro_batch_size: 1 num_epochs: 4 -optimizer: adamw_torch +optimizer: adamw_torch_fused adam_beta2: 0.95 adam_eps: 0.00001 max_grad_norm: 1.0 diff --git a/examples/llama-2/qlora-fsdp.yml b/examples/llama-2/qlora-fsdp.yml index 204c91693c..c2db26b81a 100644 --- a/examples/llama-2/qlora-fsdp.yml +++ b/examples/llama-2/qlora-fsdp.yml @@ -39,7 +39,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 4 num_epochs: 4 -optimizer: adamw_torch +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.00001 diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml index 2c8589b17a..aa8a7c5e82 100644 --- a/examples/llama-3/fft-8b-liger-fsdp.yaml +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -37,7 +37,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 2 num_epochs: 1 -optimizer: adamw_torch +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 2e-5 diff --git a/examples/llama-3/qlora-fsdp-405b.yaml b/examples/llama-3/qlora-fsdp-405b.yaml index a60a97ef3a..434ee67c42 100644 --- a/examples/llama-3/qlora-fsdp-405b.yaml +++ b/examples/llama-3/qlora-fsdp-405b.yaml @@ -30,7 +30,7 @@ lora_target_linear: true gradient_accumulation_steps: 4 micro_batch_size: 1 num_epochs: 2 -optimizer: adamw_torch +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.00001 diff --git a/examples/llama-3/qlora-fsdp-70b.yaml b/examples/llama-3/qlora-fsdp-70b.yaml index 932e1a0d63..ceb2d8567c 100644 --- a/examples/llama-3/qlora-fsdp-70b.yaml +++ b/examples/llama-3/qlora-fsdp-70b.yaml @@ -39,7 +39,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 1 num_epochs: 4 -optimizer: adamw_torch +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.00001 diff --git a/examples/mistral/lora-mps.yml b/examples/mistral/lora-mps.yml index c1df9896cf..a62990e561 100644 --- a/examples/mistral/lora-mps.yml +++ b/examples/mistral/lora-mps.yml @@ -47,7 +47,7 @@ wandb_log_model: gradient_accumulation_steps: 8 micro_batch_size: 1 num_epochs: 2 -optimizer: adamw_torch +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.0002 diff --git a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml index 4a65b1a7d0..353c08d85c 100644 --- a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml +++ b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml @@ -41,7 +41,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 2 num_epochs: 1 -optimizer: adamw_torch +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.0002 diff --git a/examples/mistral/mixtral-qlora-fsdp.yml b/examples/mistral/mixtral-qlora-fsdp.yml index fbd9bd9372..f9b5ab606b 100644 --- a/examples/mistral/mixtral-qlora-fsdp.yml +++ b/examples/mistral/mixtral-qlora-fsdp.yml @@ -43,7 +43,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 2 num_epochs: 1 -optimizer: adamw_torch +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.0002 diff --git a/examples/phi/phi-ft.yml b/examples/phi/phi-ft.yml index fc5848dc56..29fad3094b 100644 --- a/examples/phi/phi-ft.yml +++ b/examples/phi/phi-ft.yml @@ -38,7 +38,7 @@ wandb_log_model: gradient_accumulation_steps: 1 micro_batch_size: 2 num_epochs: 4 -optimizer: adamw_torch +optimizer: adamw_torch_fused adam_beta2: 0.95 adam_epsilon: 0.00001 max_grad_norm: 1.0 diff --git a/examples/phi/phi-qlora.yml b/examples/phi/phi-qlora.yml index a98cd10408..d9f23ff264 100644 --- a/examples/phi/phi-qlora.yml +++ b/examples/phi/phi-qlora.yml @@ -38,7 +38,7 @@ wandb_log_model: gradient_accumulation_steps: 1 micro_batch_size: 2 num_epochs: 4 -optimizer: adamw_torch +optimizer: adamw_torch_fused adam_beta2: 0.95 adam_epsilon: 0.00001 max_grad_norm: 1.0 diff --git a/examples/phi/phi2-ft.yml b/examples/phi/phi2-ft.yml index 0f656f8210..1b7ac89ec8 100644 --- a/examples/phi/phi2-ft.yml +++ b/examples/phi/phi2-ft.yml @@ -38,7 +38,7 @@ wandb_log_model: gradient_accumulation_steps: 1 micro_batch_size: 2 num_epochs: 4 -optimizer: adamw_torch +optimizer: adamw_torch_fused adam_beta2: 0.95 adam_epsilon: 0.00001 max_grad_norm: 1.0 diff --git a/examples/phi/phi3-ft-fsdp.yml b/examples/phi/phi3-ft-fsdp.yml index c081e47b9f..1479bb97f3 100644 --- a/examples/phi/phi3-ft-fsdp.yml +++ b/examples/phi/phi3-ft-fsdp.yml @@ -39,7 +39,7 @@ wandb_log_model: gradient_accumulation_steps: 2 micro_batch_size: 12 num_epochs: 2 -optimizer: adamw_torch +optimizer: adamw_torch_fused adam_beta2: 0.95 adam_epsilon: 0.00001 max_grad_norm: 1.0 diff --git a/examples/phi/phi3-ft.yml b/examples/phi/phi3-ft.yml index ac42153afa..58afd940ee 100644 --- a/examples/phi/phi3-ft.yml +++ b/examples/phi/phi3-ft.yml @@ -35,7 +35,7 @@ lora_fan_in_fan_out: gradient_accumulation_steps: 1 micro_batch_size: 2 num_epochs: 1 -optimizer: adamw_torch +optimizer: adamw_torch_fused adam_beta2: 0.95 adam_epsilon: 0.00001 max_grad_norm: 1.0 diff --git a/examples/qwen2/qlora-fsdp.yaml b/examples/qwen2/qlora-fsdp.yaml index cc49749086..c537d32445 100644 --- a/examples/qwen2/qlora-fsdp.yaml +++ b/examples/qwen2/qlora-fsdp.yaml @@ -37,7 +37,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 1 num_epochs: 4 -optimizer: adamw_torch +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.0002 diff --git a/examples/tiny-llama/lora-mps.yml b/examples/tiny-llama/lora-mps.yml index f949acd0f5..c777a4d7b4 100644 --- a/examples/tiny-llama/lora-mps.yml +++ b/examples/tiny-llama/lora-mps.yml @@ -38,7 +38,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 2 num_epochs: 4 -optimizer: adamw_torch +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.0002 diff --git a/tests/core/test_trainer_builder.py b/tests/core/test_trainer_builder.py index 558d3cb956..fbfd7a87c7 100644 --- a/tests/core/test_trainer_builder.py +++ b/tests/core/test_trainer_builder.py @@ -22,7 +22,7 @@ def fixture_cfg(): "output_dir": "./model-out", "warmup_steps": 10, "gradient_checkpointing": False, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "sequence_len": 2048, "rl": True, "adam_beta1": 0.998, diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py index 291a4a4ec6..f65d65ee40 100644 --- a/tests/e2e/integrations/test_cut_cross_entropy.py +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -39,7 +39,7 @@ def min_cfg(temp_dir): "micro_batch_size": 8, "gradient_accumulation_steps": 1, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "output_dir": temp_dir, "lr_scheduler": "cosine", "save_safetensors": True, diff --git a/tests/e2e/integrations/test_liger.py b/tests/e2e/integrations/test_liger.py index 1efe889e4f..cf673cab26 100644 --- a/tests/e2e/integrations/test_liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -48,7 +48,7 @@ def test_llama_wo_flce(self, temp_dir): "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "save_safetensors": True, "bf16": "auto", @@ -93,7 +93,7 @@ def test_llama_w_flce(self, temp_dir): "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "save_safetensors": True, "bf16": "auto", diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index bb1874b0bf..0f91fe056f 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -331,7 +331,7 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, "fsdp": [ @@ -401,7 +401,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): "gradient_accumulation_steps": 4, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, "fsdp": [ @@ -480,7 +480,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "gradient_accumulation_steps": 4, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, "fsdp": [ @@ -575,7 +575,7 @@ def test_ds_zero3_packed( "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, "deepspeed": str(AXOLOTL_ROOT / deepspeed), @@ -648,7 +648,7 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero2.json"), @@ -721,7 +721,7 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero1.json"), diff --git a/tests/e2e/multigpu/test_qwen2.py b/tests/e2e/multigpu/test_qwen2.py index 2b98848483..1895e1ee8b 100644 --- a/tests/e2e/multigpu/test_qwen2.py +++ b/tests/e2e/multigpu/test_qwen2.py @@ -52,7 +52,7 @@ def test_qlora_fsdp_dpo(self, base_model, temp_dir): "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, "bf16": "auto", diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index af8eb37427..7beb71145b 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -52,7 +52,7 @@ def test_sdp_lora_packing(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 20, "save_steps": 10, @@ -96,7 +96,7 @@ def test_torch_lora_packing(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 20, "save_steps": 10, diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index e7ab510c91..f8f2455141 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -56,7 +56,7 @@ def test_fft_packing(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 10, "save_steps": 5, diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py index 8d0ba6c2ac..cfa70fd73c 100644 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ b/tests/e2e/patched/test_llama_s2_attention.py @@ -56,7 +56,7 @@ def test_lora_s2_attn(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 10, "save_steps": 5, @@ -96,7 +96,7 @@ def test_fft_s2_attn(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 10, "save_steps": 5, diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index bc18e3d813..e544eb4fda 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -61,7 +61,7 @@ def test_lora_packing(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", } ) @@ -116,7 +116,7 @@ def test_lora_gptq_packed(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", } ) diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index c7fd0ecbc7..f9e5236794 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -55,7 +55,7 @@ def test_lora_packing(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 20, "save_steps": 10, @@ -96,7 +96,7 @@ def test_ft_packing(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 20, "save_steps": 10, diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py index 4261ccc266..a1e9b5d46b 100644 --- a/tests/e2e/test_embeddings_lr.py +++ b/tests/e2e/test_embeddings_lr.py @@ -48,7 +48,7 @@ def test_train_w_embedding_lr_scale(self, temp_dir): "val_set_size": 0.0, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "embedding_lr_scale": 0.5, "lr_scheduler": "cosine", "save_safetensors": True, @@ -92,7 +92,7 @@ def test_train_w_embedding_lr(self, temp_dir): "val_set_size": 0.0, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "embedding_lr": 0.000005, "lr_scheduler": "cosine", "save_safetensors": True, diff --git a/tests/e2e/test_falcon.py b/tests/e2e/test_falcon.py index ddcb662755..738a0e0b0c 100644 --- a/tests/e2e/test_falcon.py +++ b/tests/e2e/test_falcon.py @@ -57,7 +57,7 @@ def test_lora(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 20, "save_steps": 10, @@ -110,7 +110,7 @@ def test_lora_added_vocab(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 20, "save_steps": 10, @@ -149,7 +149,7 @@ def test_ft(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 20, "save_steps": 10, diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index c1f024b872..5cd1c693af 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -62,7 +62,7 @@ def test_pretrain(self, temp_dir, sample_packing, pretrain_multipack_attn): "val_set_size": 0.0, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "save_safetensors": True, "bf16": "auto", diff --git a/tests/e2e/test_load_model.py b/tests/e2e/test_load_model.py index 31a9b1a878..255b096b0f 100644 --- a/tests/e2e/test_load_model.py +++ b/tests/e2e/test_load_model.py @@ -52,7 +52,7 @@ def setup_method(self): "micro_batch_size": 8, "gradient_accumulation_steps": 1, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", } ) diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index 696c47aed7..8ebedf2763 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -54,7 +54,7 @@ def test_lora(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 20, } diff --git a/tests/e2e/test_mamba.py b/tests/e2e/test_mamba.py index 4b4db30585..8cadf49320 100644 --- a/tests/e2e/test_mamba.py +++ b/tests/e2e/test_mamba.py @@ -51,7 +51,7 @@ def test_fft(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 20, "save_steps": 10, diff --git a/tests/e2e/test_mistral.py b/tests/e2e/test_mistral.py index a304e9b4a5..17732d8798 100644 --- a/tests/e2e/test_mistral.py +++ b/tests/e2e/test_mistral.py @@ -56,7 +56,7 @@ def test_lora(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 20, "save_steps": 10, @@ -95,7 +95,7 @@ def test_ft(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 20, "save_steps": 10, diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index 13244a2152..a363b11b2c 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -49,7 +49,7 @@ def test_loss_packed(self, temp_dir): "gradient_accumulation_steps": 4, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_torch", + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 5, "use_tensorboard": True, From 1063d82b512d61cddab87d3faf5c4a5ef6130a3b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 30 Jan 2025 11:46:09 -0500 Subject: [PATCH 0394/1405] match the cuda version for 2.4.1 build w/o tmux (#2299) --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4f8074ad1c..c9e13957cd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -125,8 +125,8 @@ jobs: strategy: matrix: include: - - cuda: 121 - cuda_version: 12.1.1 + - cuda: 124 + cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.4.1 axolotl_extras: From 6f713226dd2828e81ef73acc219b97e51eb87f63 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 30 Jan 2025 11:48:48 -0500 Subject: [PATCH 0395/1405] make save_safetensors: true the default (#2292) * make save_safetensors: true the default * revert change to model output check --- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index dc8897863d..f05b259b7c 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -528,7 +528,7 @@ class ModelOutputConfig(BaseModel): output_dir: str = Field(default="./model-out") hub_model_id: Optional[str] = None hub_strategy: Optional[str] = None - save_safetensors: Optional[bool] = None + save_safetensors: Optional[bool] = True class MLFlowConfig(BaseModel): From 6f294c3d8d0c882ce5396261262c5c0db6ffe7d6 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Thu, 30 Jan 2025 12:49:21 -0500 Subject: [PATCH 0396/1405] refactor README; hardcode links to quarto docs; add additional quarto doc pages (#2295) * refactor README; hardcode links to quarto docs; add additional quarto doc pages * updates * review comments * update --------- Co-authored-by: Dan Saunders --- .github/CONTRIBUTING.md | 2 +- README.md | 779 ++----------------- _quarto.yml | 5 +- docs/dataset-formats/conversation.qmd | 2 - docs/dataset-formats/stepwise_supervised.qmd | 12 +- docs/getting-started.qmd | 155 ++++ docs/inference.qmd | 148 ++++ docs/installation.qmd | 119 +++ docs/multi-gpu.qmd | 118 +++ styles.css | 4 + 10 files changed, 627 insertions(+), 717 deletions(-) create mode 100644 docs/getting-started.qmd create mode 100644 docs/inference.qmd create mode 100644 docs/installation.qmd create mode 100644 docs/multi-gpu.qmd diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 29769efb56..8f67908e8d 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,7 +15,7 @@ First of all, thank you for your interest in contributing to axolotl! We appreci - [Commit Messages](#commit-messages) - [Additional Resources](#additional-resources) -## Code of Conductcode +## Code of Conduct All contributors are expected to adhere to our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before participating in the axolotl community. diff --git a/README.md b/README.md index 674a9dcf14..6f1fe9846d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

- - - Axolotl + + + Axolotl

@@ -19,235 +19,99 @@
tests-nightly multigpu-semi-weekly tests + + phorm.ai +

-Axolotl is a tool designed to streamline the fine-tuning of various AI models, offering support for multiple configurations and architectures. +Axolotl is a tool designed to streamline post-training for various AI models. +Post-training refers to any modifications or additional training performed on +pre-trained models - including full model fine-tuning, parameter-efficient tuning (like +LoRA and QLoRA), supervised fine-tuning (SFT), instruction tuning, and alignment +techniques. With support for multiple model architectures and training configurations, +Axolotl makes it easy to get started with these techniques. + +Axolotl is designed to work with YAML config files that contain everything you need to +preprocess a dataset, train or fine-tune a model, run model inference or evaluation, +and much more. Features: + - Train various Huggingface models such as llama, pythia, falcon, mpt - Supports fullfinetune, lora, qlora, relora, and gptq - Customize configurations using a simple yaml file or CLI overwrite - Load different dataset formats, use custom formats, or bring your own tokenized datasets -- Integrated with xformer, flash attention, [liger kernel](https://github.com/linkedin/Liger-Kernel), rope scaling, and multipacking +- Integrated with [xformers](https://github.com/facebookresearch/xformers), flash attention, [liger kernel](https://github.com/linkedin/Liger-Kernel), rope scaling, and multipacking - Works with single GPU or multiple GPUs via FSDP or Deepspeed - Easily run with Docker locally or on the cloud - Log results and optionally checkpoints to wandb, mlflow or Comet - And more! - - phorm.ai - - - - - - - -
- -## Table of Contents -- [Axolotl](#axolotl) - - [Table of Contents](#table-of-contents) - - [Quickstart ⚡](#quickstart-) - - [Edge Builds](#edge-builds-) - - [Axolotl CLI Usage](#axolotl-cli-usage) - - [Badge ❤🏷️](#badge-️) - - [Contributing 🤝](#contributing-) - - [Sponsors 🤝❤](#sponsors-) - - [Axolotl supports](#axolotl-supports) - - [Advanced Setup](#advanced-setup) - - [Environment](#environment) - - [Docker](#docker) - - [Conda/Pip venv](#condapip-venv) - - [Cloud GPU](#cloud-gpu) - - [Bare Metal Cloud GPU](#bare-metal-cloud-gpu) - - [LambdaLabs](#lambdalabs) - - [GCP](#gcp) - - [Windows](#windows) - - [Mac](#mac) - - [Google Colab](#google-colab) - - [Launching on public clouds via SkyPilot](#launching-on-public-clouds-via-skypilot) - - [Launching on public clouds via dstack](#launching-on-public-clouds-via-dstack) - - [Dataset](#dataset) - - [Config](#config) - - [All Config Options](#all-config-options) - - [Train](#train) - - [Preprocess dataset](#preprocess-dataset) - - [Multi-GPU](#multi-gpu) - - [DeepSpeed](#deepspeed) - - [FSDP](#fsdp) - - [FSDP + QLoRA](#fsdp--qlora) - - [Weights \& Biases Logging](#weights--biases-logging) - - [Special Tokens](#special-tokens) - - [Liger Kernel](#liger-kernel) - - [Inference Playground](#inference-playground) - - [Merge LORA to base](#merge-lora-to-base) - - [Common Errors 🧰](#common-errors-) - - [Tokenization Mismatch b/w Inference \& Training](#tokenization-mismatch-bw-inference--training) - - [Debugging Axolotl](#debugging-axolotl) - - [Need help? 🙋](#need-help-) - - - -
- axolotl -
-

- Axolotl provides a unified repository for fine-tuning
a variety of AI models with ease
-

-

- Go ahead and Axolotl questions!! -

- pre-commit - PyTest Status -
-
- -
- -## Quickstart ⚡ - -Get started with Axolotl in just a few steps! This quickstart guide will walk you through setting up and running a basic fine-tuning task. - -**Requirements**: *Nvidia* GPU (Ampere architecture or newer for `bf16` and Flash Attention) or *AMD* GPU, Python >=3.10 and PyTorch >=2.4.1. - -```bash -pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] - -# download examples and optionally deepspeed configs to the local path -axolotl fetch examples -axolotl fetch deepspeed_configs # OPTIONAL +## 🚀 Quick Start -# finetune using lora -axolotl train examples/llama-3/lora-1b.yml -``` +**Requirements**: +- NVIDIA GPU (Ampere or newer for `bf16` and Flash Attention) or AMD GPU +- Python ≥3.10 +- PyTorch ≥2.4.1 -### Edge Builds 🏎️ +### Installation -If you're looking for the latest features and updates between releases, you'll need to install -from source. +```shell +pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] -```bash -git clone https://github.com/axolotl-ai-cloud/axolotl.git -cd axolotl -pip3 install packaging ninja -pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' +# Download example axolotl configs, deepspeed configs +axolotl fetch examples +axolotl fetch deepspeed_configs # OPTIONAL ``` -### Axolotl CLI Usage -We now support a new, more streamlined CLI using [click](https://click.palletsprojects.com/en/stable/). +Other installation approaches are described [here](https://axolotl-ai-cloud.github.io/axolotl/docs/installation.html). -```bash -# preprocess datasets - optional but recommended -CUDA_VISIBLE_DEVICES="0" axolotl preprocess examples/llama-3/lora-1b.yml +### Your First Fine-tune -# finetune lora -axolotl train examples/llama-3/lora-1b.yml - -# inference -axolotl inference examples/llama-3/lora-1b.yml \ - --lora-model-dir="./outputs/lora-out" - -# gradio -axolotl inference examples/llama-3/lora-1b.yml \ - --lora-model-dir="./outputs/lora-out" --gradio - -# remote yaml files - the yaml config can be hosted on a public URL -# Note: the yaml config must directly link to the **raw** yaml -axolotl train https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/llama-3/lora-1b.yml -``` - -We've also added a new command for fetching `examples` and `deepspeed_configs` to your -local machine. This will come in handy when installing `axolotl` from PyPI. - -```bash -# Fetch example YAML files (stores in "examples/" folder) +```shell +# Fetch axolotl examples axolotl fetch examples -# Fetch deepspeed config files (stores in "deepspeed_configs/" folder) -axolotl fetch deepspeed_configs - -# Optionally, specify a destination folder +# Or, specify a custom path axolotl fetch examples --dest path/to/folder -``` - -### Legacy Usage -
- -Click to Expand -While the Axolotl CLI is the preferred method for interacting with axolotl, we -still support the legacy `-m axolotl.cli.*` usage. - -```bash -# preprocess datasets - optional but recommended -CUDA_VISIBLE_DEVICES="0" python -m axolotl.cli.preprocess examples/llama-3/lora-1b.yml - -# finetune lora -accelerate launch -m axolotl.cli.train examples/llama-3/lora-1b.yml - -# inference -accelerate launch -m axolotl.cli.inference examples/llama-3/lora-1b.yml \ - --lora_model_dir="./outputs/lora-out" - -# gradio -accelerate launch -m axolotl.cli.inference examples/llama-3/lora-1b.yml \ - --lora_model_dir="./outputs/lora-out" --gradio - -# remote yaml files - the yaml config can be hosted on a public URL -# Note: the yaml config must directly link to the **raw** yaml -accelerate launch -m axolotl.cli.train https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/llama-3/lora-1b.yml -``` - -
- -## Badge ❤🏷️ - -Building something cool with Axolotl? Consider adding a badge to your model card. - -```markdown -[Built with Axolotl](https://github.com/axolotl-ai-cloud/axolotl) +# Train a model using LoRA +axolotl train examples/llama-3/lora-1b.yml ``` -[Built with Axolotl](https://github.com/axolotl-ai-cloud/axolotl) - -## Sponsors 🤝❤ +That's it! Check out our [Getting Started Guide](https://axolotl-ai-cloud.github.io/axolotl/docs/getting-started.html) for a more detailed walkthrough. -If you love axolotl, consider sponsoring the project by reaching out directly to [wing@axolotl.ai](mailto:wing@axolotl.ai). +## ✨ Key Features ---- +- **Multiple Model Support**: Train various models like LLaMA, Mistral, Mixtral, Pythia, and more +- **Training Methods**: Full fine-tuning, LoRA, QLoRA, and more +- **Easy Configuration**: Simple YAML files to control your training setup +- **Performance Optimizations**: Flash Attention, xformers, multi-GPU training +- **Flexible Dataset Handling**: Use various formats and custom datasets +- **Cloud Ready**: Run on cloud platforms or local hardware -- [Modal](https://www.modal.com?utm_source=github&utm_medium=github&utm_campaign=axolotl) Modal lets you run data/AI jobs in the cloud, by just writing a few lines of Python. Customers use Modal to deploy Gen AI models at large scale, fine-tune large language models, run protein folding simulations, and much more. +## 📚 Documentation ---- +- [Installation Options](https://axolotl-ai-cloud.github.io/axolotl/docs/installation.html) - Detailed setup instructions for different environments +- [Configuration Guide](https://axolotl-ai-cloud.github.io/axolotl/docs/config.html) - Full configuration options and examples +- [Dataset Guide](https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/) - Supported formats and how to use them +- [Multi-GPU Training](https://axolotl-ai-cloud.github.io/axolotl/docs/multi-gpu.html) +- [Multi-Node Training](https://axolotl-ai-cloud.github.io/axolotl/docs/multi-node.html) +- [Multipacking](https://axolotl-ai-cloud.github.io/axolotl/docs/multipack.html) +- [FAQ](https://axolotl-ai-cloud.github.io/axolotl/docs/faq.html) - Frequently asked questions -## Contributing 🤝 +## 🤝 Getting Help -Please read the [contributing guide](./.github/CONTRIBUTING.md) +- Join our [Discord community](https://discord.gg/HhrNrHJPRb) for support +- Check out our [Examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/) directory +- Read our [Debugging Guide](https://axolotl-ai-cloud.github.io/axolotl/docs/debugging.html) +- Need dedicated support? Please contact [✉️wing@axolotl.ai](mailto:wing@axolotl.ai) for options -Bugs? Please check the [open issues](https://github.com/axolotl-ai-cloud/axolotl/issues/bug) else create a new Issue. +## 🌟 Contributing -PRs are **greatly welcome**! +Contributions are welcome! Please see our [Contributing Guide](https://github.com/axolotl-ai-cloud/axolotl/blob/main/.github/CONTRIBUTING.md) for details. -Please run the quickstart instructions followed by the below to setup env: -```bash -pip3 install -r requirements-dev.txt -r requirements-tests.txt -pre-commit install - -# test -pytest tests/ - -# optional: run against all files -pre-commit run --all-files -``` - -Thanks to all of our contributors to date. Help drive open source AI progress forward by contributing to Axolotl. - - - contributor chart by https://contrib.rocks - - -## Axolotl supports +## Supported Models | | fp16/fp32 | lora | qlora | gptq | gptq w/flash attn | flash attn | xformers attn | |-------------|:----------|:-----|-------|------|-------------------|------------|--------------| @@ -272,523 +136,16 @@ Thanks to all of our contributors to date. Help drive open source AI progress fo ❌: not supported ❓: untested -## Advanced Setup - -### Environment - -#### Docker - - ```bash - docker run --gpus '"all"' --rm -it axolotlai/axolotl:main-latest - ``` - - Or run on the current files for development: - - ```sh - docker compose up -d - ``` - ->[!Tip] -> If you want to debug axolotl or prefer to use Docker as your development environment, see the [debugging guide's section on Docker](docs/debugging.qmd#debugging-with-docker). - -
- - Docker advanced - - A more powerful Docker command to run would be this: - - ```bash -docker run --privileged --gpus '"all"' --shm-size 10g --rm -it --name axolotl --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 --mount type=bind,src="${PWD}",target=/workspace/axolotl -v ${HOME}/.cache/huggingface:/root/.cache/huggingface axolotlai/axolotl:main-latest - ``` - - It additionally: - * Prevents memory issues when running e.g. deepspeed (e.g. you could hit SIGBUS/signal 7 error) through `--ipc` and `--ulimit` args. - * Persists the downloaded HF data (models etc.) and your modifications to axolotl code through `--mount`/`-v` args. - * The `--name` argument simply makes it easier to refer to the container in vscode (`Dev Containers: Attach to Running Container...`) or in your terminal. - * The `--privileged` flag gives all capabilities to the container. - * The `--shm-size 10g` argument increases the shared memory size. Use this if you see `exitcode: -7` errors using deepspeed. - - [More information on nvidia website](https://docs.nvidia.com/deeplearning/frameworks/user-guide/index.html#setincshmem) - -
- -#### Conda/Pip venv - 1. Install python >=**3.10** - - 2. Install pytorch stable https://pytorch.org/get-started/locally/ - - 3. Install Axolotl along with python dependencies - ```bash - pip3 install packaging - pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' - ``` - 4. (Optional) Login to Huggingface to use gated models/datasets. - ```bash - huggingface-cli login - ``` - Get the token at huggingface.co/settings/tokens - -#### Cloud GPU - -For cloud GPU providers that support docker images, use [`axolotlai/axolotl-cloud:main-latest`](https://hub.docker.com/r/axolotlai/axolotl-cloud/tags) - -- on Latitude.sh use this [direct link](https://latitude.sh/blueprint/989e0e79-3bf6-41ea-a46b-1f246e309d5c) -- on JarvisLabs.ai use this [direct link](https://jarvislabs.ai/templates/axolotl) -- on RunPod use this [direct link](https://runpod.io/gsc?template=v2ickqhz9s&ref=6i7fkpdz) - -#### Bare Metal Cloud GPU - -##### LambdaLabs - -
- - Click to Expand - - 1. Install python - ```bash - sudo apt update - sudo apt install -y python3.10 - - sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.10 1 - sudo update-alternatives --config python # pick 3.10 if given option - python -V # should be 3.10 - - ``` - - 2. Install pip - ```bash - wget https://bootstrap.pypa.io/get-pip.py - python get-pip.py - ``` - - 3. Install Pytorch https://pytorch.org/get-started/locally/ - - 4. Follow instructions on quickstart. - - 5. Run - ```bash - pip3 install protobuf==3.20.3 - pip3 install -U --ignore-installed requests Pillow psutil scipy - ``` - - 6. Set path - ```bash - export LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH - ``` -
- -##### GCP - -
- -Click to Expand - -Use a Deeplearning linux OS with cuda and pytorch installed. Then follow instructions on quickstart. - -Make sure to run the below to uninstall xla. -```bash -pip uninstall -y torch_xla[tpu] -``` - -
- -#### Windows -Please use WSL or Docker! - -#### Mac - -Use the below instead of the install method in QuickStart. -``` -pip3 install --no-build-isolation -e '.' -``` -More info: [mac.md](/docs/mac.qmd) - -#### Google Colab - -Please use this example [notebook](examples/colab-notebooks/colab-axolotl-example.ipynb). - -#### Launching on public clouds via SkyPilot -To launch on GPU instances (both on-demand and spot instances) on 7+ clouds (GCP, AWS, Azure, OCI, and more), you can use [SkyPilot](https://skypilot.readthedocs.io/en/latest/index.html): - -```bash -pip install "skypilot-nightly[gcp,aws,azure,oci,lambda,kubernetes,ibm,scp]" # choose your clouds -sky check -``` - -Get the [example YAMLs](https://github.com/skypilot-org/skypilot/tree/master/llm/axolotl) of using Axolotl to finetune `mistralai/Mistral-7B-v0.1`: -``` -git clone https://github.com/skypilot-org/skypilot.git -cd skypilot/llm/axolotl -``` - -Use one command to launch: -```bash -# On-demand -HF_TOKEN=xx sky launch axolotl.yaml --env HF_TOKEN - -# Managed spot (auto-recovery on preemption) -HF_TOKEN=xx BUCKET= sky spot launch axolotl-spot.yaml --env HF_TOKEN --env BUCKET -``` - -#### Launching on public clouds via dstack -To launch on GPU instance (both on-demand and spot instances) on public clouds (GCP, AWS, Azure, Lambda Labs, TensorDock, Vast.ai, and CUDO), you can use [dstack](https://dstack.ai/). - -Write a job description in YAML as below: - -```yaml -# dstack.yaml -type: task - -image: axolotlai/axolotl-cloud:main-latest - -env: - - HUGGING_FACE_HUB_TOKEN - - WANDB_API_KEY - -commands: - - accelerate launch -m axolotl.cli.train config.yaml - -ports: - - 6006 - -resources: - gpu: - memory: 24GB.. - count: 2 -``` - -then, simply run the job with `dstack run` command. Append `--spot` option if you want spot instance. `dstack run` command will show you the instance with cheapest price across multi cloud services: - -```bash -pip install dstack -HUGGING_FACE_HUB_TOKEN=xxx WANDB_API_KEY=xxx dstack run . -f dstack.yaml # --spot -``` - -For further and fine-grained use cases, please refer to the official [dstack documents](https://dstack.ai/docs/) and the detailed description of [axolotl example](https://github.com/dstackai/dstack/tree/master/examples/fine-tuning/axolotl) on the official repository. - -### Dataset - -Axolotl supports a variety of dataset formats. It is recommended to use a JSONL. The schema of the JSONL depends upon the task and the prompt template you wish to use. Instead of a JSONL, you can also use a HuggingFace dataset with columns for each JSONL field. - -See [the documentation](https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/) for more information on how to use different dataset formats. - -### Config - -See [examples](examples) for quick start. It is recommended to duplicate and modify to your needs. The most important options are: - -- model - ```yaml - base_model: ./llama-7b-hf # local or huggingface repo - ``` - Note: The code will load the right architecture. - -- dataset - ```yaml - datasets: - # huggingface repo - - path: vicgalle/alpaca-gpt4 - type: alpaca - - # huggingface repo with specific configuration/subset - - path: EleutherAI/pile - name: enron_emails - type: completion # format from earlier - field: text # Optional[str] default: text, field to use for completion data - - # huggingface repo with multiple named configurations/subsets - - path: bigcode/commitpackft - name: - - ruby - - python - - typescript - type: ... # unimplemented custom format - - # chat_template https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/conversation.html#chat_template - - path: ... - type: chat_template - chat_template: chatml # defaults to tokenizer's chat_template - - # local - - path: data.jsonl # or json - ds_type: json # see other options below - type: alpaca - - # dataset with splits, but no train split - - path: knowrohit07/know_sql - type: context_qa.load_v2 - train_on_split: validation - - # loading from s3 or gcs - # s3 creds will be loaded from the system default / gcs will attempt to load from gcloud creds, google metadata service, or anon - - path: s3://path_to_ds # Accepts folder with arrow/parquet or file path like above - ... - - # Loading Data From a Public URL - # - The file format is `json` (which includes `jsonl`) by default. For different formats, adjust the `ds_type` option accordingly. - - path: https://some.url.com/yourdata.jsonl # The URL should be a direct link to the file you wish to load. URLs must use HTTPS protocol, not HTTP. - ds_type: json # this is the default, see other options below. - ``` - -- loading - ```yaml - load_in_4bit: true - load_in_8bit: true - - bf16: auto # require >=ampere, auto will detect if your GPU supports this and choose automatically. - fp16: # leave empty to use fp16 when bf16 is 'auto'. set to false if you want to fallback to fp32 - tf32: true # require >=ampere - - bfloat16: true # require >=ampere, use instead of bf16 when you don't want AMP (automatic mixed precision) - float16: true # use instead of fp16 when you don't want AMP - ``` - Note: Repo does not do 4-bit quantization. - -- lora - ```yaml - adapter: lora # 'qlora' or leave blank for full finetune - lora_r: 8 - lora_alpha: 16 - lora_dropout: 0.05 - lora_target_modules: - - q_proj - - v_proj - ``` - -#### All Config Options - -See [these docs](docs/config.qmd) for all config options. - -### Train - -Run -```bash -accelerate launch -m axolotl.cli.train your_config.yml -``` - -> [!TIP] -> You can also reference a config file that is hosted on a public URL, for example `accelerate launch -m axolotl.cli.train https://yourdomain.com/your_config.yml` - -#### Preprocess dataset - -You can optionally pre-tokenize dataset with the following before finetuning. -This is recommended for large datasets. - -- Set `dataset_prepared_path:` to a local folder for saving and loading pre-tokenized dataset. -- (Optional): Set `push_dataset_to_hub: hf_user/repo` to push it to Huggingface. -- (Optional): Use `--debug` to see preprocessed examples. - -```bash -python -m axolotl.cli.preprocess your_config.yml -``` - -#### Multi-GPU - -Below are the options available in axolotl for training with multiple GPUs. Note that DeepSpeed -is the recommended multi-GPU option currently because FSDP may experience -[loss instability](https://github.com/huggingface/transformers/issues/26498). - -##### DeepSpeed - -Deepspeed is an optimization suite for multi-gpu systems allowing you to train much larger models than you -might typically be able to fit into your GPU's VRAM. More information about the various optimization types -for deepspeed is available at https://huggingface.co/docs/accelerate/main/en/usage_guides/deepspeed#what-is-integrated - -We provide several default deepspeed JSON configurations for ZeRO stage 1, 2, and 3. - -```yaml -deepspeed: deepspeed_configs/zero1.json -``` - -```shell -accelerate launch -m axolotl.cli.train examples/llama-2/config.yml --deepspeed deepspeed_configs/zero1.json -``` - -##### FSDP - -- llama FSDP -```yaml -fsdp: - - full_shard - - auto_wrap -fsdp_config: - fsdp_offload_params: true - fsdp_state_dict_type: FULL_STATE_DICT - fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer -``` - -##### FSDP + QLoRA - -Axolotl supports training with FSDP and QLoRA, see [these docs](docs/fsdp_qlora.qmd) for more information. - -##### Weights & Biases Logging - -Make sure your `WANDB_API_KEY` environment variable is set (recommended) or you login to wandb with `wandb login`. - -- wandb options -```yaml -wandb_mode: -wandb_project: -wandb_entity: -wandb_watch: -wandb_name: -wandb_log_model: -``` - -##### Comet Logging - -Make sure your `COMET_API_KEY` environment variable is set (recommended) or you login to wandb with `comet login`. - -- wandb options -```yaml -use_comet: -comet_api_key: -comet_workspace: -comet_project_name: -comet_experiment_key: -comet_mode: -comet_online: -comet_experiment_config: -``` - -##### Special Tokens - -It is important to have special tokens like delimiters, end-of-sequence, beginning-of-sequence in your tokenizer's vocabulary. This will help you avoid tokenization issues and help your model train better. You can do this in axolotl like this: - -```yml -special_tokens: - bos_token: "" - eos_token: "" - unk_token: "" -tokens: # these are delimiters - - "<|im_start|>" - - "<|im_end|>" -``` - -When you include these tokens in your axolotl config, axolotl adds these tokens to the tokenizer's vocabulary. - -##### Liger Kernel - -Liger Kernel: Efficient Triton Kernels for LLM Training - -https://github.com/linkedin/Liger-Kernel - -Liger (LinkedIn GPU Efficient Runtime) Kernel is a collection of Triton kernels designed specifically for LLM training. -It can effectively increase multi-GPU training throughput by 20% and reduces memory usage by 60%. The Liger Kernel -composes well and is compatible with both FSDP and Deepspeed. - -```yaml -plugins: - - axolotl.integrations.liger.LigerPlugin -liger_rope: true -liger_rms_norm: true -liger_glu_activation: true -liger_layer_norm: true -liger_fused_linear_cross_entropy: true -``` - -### Inference Playground - -Axolotl allows you to load your model in an interactive terminal playground for quick experimentation. -The config file is the same config file used for training. - -Pass the appropriate flag to the inference command, depending upon what kind of model was trained: - -- Pretrained LORA: - ```bash - python -m axolotl.cli.inference examples/your_config.yml --lora_model_dir="./lora-output-dir" - ``` -- Full weights finetune: - ```bash - python -m axolotl.cli.inference examples/your_config.yml --base_model="./completed-model" - ``` -- Full weights finetune w/ a prompt from a text file: - ```bash - cat /tmp/prompt.txt | python -m axolotl.cli.inference examples/your_config.yml \ - --base_model="./completed-model" --prompter=None --load_in_8bit=True - ``` --- With gradio hosting - ```bash - python -m axolotl.cli.inference examples/your_config.yml --gradio - ``` - -Please use `--sample_packing False` if you have it on and receive the error similar to below: - -> RuntimeError: stack expects each tensor to be equal size, but got [1, 32, 1, 128] at entry 0 and [1, 32, 8, 128] at entry 1 - -### Merge LORA to base - -The following command will merge your LORA adapater with your base model. You can optionally pass the argument `--lora_model_dir` to specify the directory where your LORA adapter was saved, otherwhise, this will be inferred from `output_dir` in your axolotl config file. The merged model is saved in the sub-directory `{lora_model_dir}/merged`. - -```bash -python3 -m axolotl.cli.merge_lora your_config.yml --lora_model_dir="./completed-model" -``` - -You may need to use the `gpu_memory_limit` and/or `lora_on_cpu` config options to avoid running out of memory. If you still run out of CUDA memory, you can try to merge in system RAM with - -```bash -CUDA_VISIBLE_DEVICES="" python3 -m axolotl.cli.merge_lora ... -``` - -although this will be very slow, and using the config options above are recommended instead. - -## Common Errors 🧰 - -See also the [FAQ's](./docs/faq.qmd) and [debugging guide](docs/debugging.qmd). - -> If you encounter a 'Cuda out of memory' error, it means your GPU ran out of memory during the training process. Here's how to resolve it: - -Please reduce any below - - `micro_batch_size` - - `eval_batch_size` - - `gradient_accumulation_steps` - - `sequence_len` - -If it does not help, try running without deepspeed and without accelerate (replace "accelerate launch" with "python") in the command. - -Using adamw_bnb_8bit might also save you some memory. - -> `failed (exitcode: -9)` - -Usually means your system has run out of system memory. -Similarly, you should consider reducing the same settings as when you run out of VRAM. -Additionally, look into upgrading your system RAM which should be simpler than GPU upgrades. - -> RuntimeError: expected scalar type Float but found Half - -Try set `fp16: true` - -> NotImplementedError: No operator found for `memory_efficient_attention_forward` ... - -Try to turn off xformers. - -> accelerate config missing - -It's safe to ignore it. - -> NCCL Timeouts during training - -See the [NCCL](docs/nccl.qmd) guide. - - -### Tokenization Mismatch b/w Inference & Training - -For many formats, Axolotl constructs prompts by concatenating token ids _after_ tokenizing strings. The reason for concatenating token ids rather than operating on strings is to maintain precise accounting for attention masks. - -If you decode a prompt constructed by axolotl, you might see spaces between tokens (or lack thereof) that you do not expect, especially around delimiters and special tokens. When you are starting out with a new format, you should always do the following: - -1. Materialize some data using `python -m axolotl.cli.preprocess your_config.yml --debug`, and then decode the first few rows with your model's tokenizer. -2. During inference, right before you pass a tensor of token ids to your model, decode these tokens back into a string. -3. Make sure the inference string from #2 looks **exactly** like the data you fine tuned on from #1, including spaces and new lines. If they aren't the same, adjust your inference server accordingly. -4. As an additional troubleshooting step, you can look at the token ids between 1 and 2 to make sure they are identical. - -Having misalignment between your prompts during training and inference can cause models to perform very poorly, so it is worth checking this. See [this blog post](https://hamel.dev/notes/llm/finetuning/05_tokenizer_gotchas.html) for a concrete example. +## ❤️ Sponsors -## Debugging Axolotl +Thank you to our sponsors who help make Axolotl possible: -See [this debugging guide](docs/debugging.qmd) for tips on debugging Axolotl, along with an example configuration for debugging with VSCode. +- [Modal](https://www.modal.com?utm_source=github&utm_medium=github&utm_campaign=axolotl) - Modal lets you run +jobs in the cloud, by just writing a few lines of Python. Customers use Modal to deploy Gen AI models at large scale, +fine-tune large language models, run protein folding simulations, and much more. -## Need help? 🙋 +Interested in sponsoring? Contact us at [wing@axolotl.ai](mailto:wing@axolotl.ai) -Join our [Discord server](https://discord.gg/HhrNrHJPRb) where our community members can help you. +## 📜 License -Need dedicated support? Please contact us at [✉️wing@axolotl.ai](ailto:wing@axolotl.ai) for dedicated support options. +This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. diff --git a/_quarto.yml b/_quarto.yml index 8eb79f651d..3ec1ce75b6 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -28,13 +28,17 @@ website: - section: "How-To Guides" contents: # TODO Edit folder structure after we have more docs. + - docs/getting-started.qmd + - docs/installation.qmd - docs/debugging.qmd + - docs/inference.qmd - docs/multipack.qmd - docs/fsdp_qlora.qmd - docs/input_output.qmd - docs/rlhf.qmd - docs/nccl.qmd - docs/mac.qmd + - docs/multi-gpu.qmd - docs/multi-node.qmd - docs/unsloth.qmd - docs/amd_hpc.qmd @@ -46,7 +50,6 @@ website: - docs/config.qmd - docs/faq.qmd - format: html: theme: materia diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 9f6e8c3604..4d8dbe769a 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -8,14 +8,12 @@ order: 3 IMPORTANT: ShareGPT is deprecated!. Please see `chat_template` section below. - ## pygmalion ```{.json filename="data.jsonl"} {"conversations": [{"role": "...", "value": "..."}]} ``` - ## chat_template Chat Template strategy uses a jinja2 template that converts a list of messages into a prompt. Support using tokenizer's template, a supported template, or custom jinja2. diff --git a/docs/dataset-formats/stepwise_supervised.qmd b/docs/dataset-formats/stepwise_supervised.qmd index 17f0c9141b..072bf83537 100644 --- a/docs/dataset-formats/stepwise_supervised.qmd +++ b/docs/dataset-formats/stepwise_supervised.qmd @@ -6,8 +6,15 @@ order: 3 ## Stepwise Supervised -The stepwise supervised format is designed for chain-of-thought (COT) reasoning datasets where each example contains multiple completion steps and a preference label for each step. -### ExampleHere's a simple example of a stepwise supervised dataset entry:```json +The stepwise supervised format is designed for chain-of-thought (COT) reasoning +datasets where each example contains multiple completion steps and a preference label +for each step. + +### Example + +Here's a simple example of a stepwise supervised dataset entry: + +```json { "prompt": "Which number is larger, 9.8 or 9.11?", "completions": [ @@ -16,3 +23,4 @@ The stepwise supervised format is designed for chain-of-thought (COT) reasoning ], "labels": [true, false] } +``` \ No newline at end of file diff --git a/docs/getting-started.qmd b/docs/getting-started.qmd new file mode 100644 index 0000000000..2292cde151 --- /dev/null +++ b/docs/getting-started.qmd @@ -0,0 +1,155 @@ +--- +title: "Getting Started with Axolotl" +format: + html: + toc: true + toc-depth: 3 + number-sections: true +execute: + enabled: false +--- + +This guide will walk you through your first model fine-tuning project with Axolotl. + +## Quick Example {#sec-quick-example} + +Let's start by fine-tuning a small language model using LoRA. This example uses a 1B parameter model to ensure it runs on most GPUs. +Assuming `axolotl` is installed (if not, see our [Installation Guide](installation.qmd)) + +1. Download example configs: +```shell +axolotl fetch examples +``` + +2. Run the training: +```shell +axolotl train examples/llama-3/lora-1b.yml +``` + +That's it! Let's understand what just happened. + +## Understanding the Process {#sec-understanding} + +### The Configuration File {#sec-config} + +The YAML configuration file controls everything about your training. Here's what (part of) our example config looks like: + +```yaml +base_model: NousResearch/Llama-3.2-1B +# hub_model_id: username/custom_model_name + +datasets: + - path: teknium/GPT4-LLM-Cleaned + type: alpaca +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: lora +lora_model_dir: +``` + +See our [Config options](config.qmd) for more details. + +### Training {#sec-training} + +When you run `axolotl train`, Axolotl: + +1. Downloads the base model +2. (If specified) applies LoRA adapter layers +3. Loads and processes the dataset +4. Runs the training loop +5. Saves the trained model and / or LoRA weights + +## Your First Custom Training {#sec-custom} + +Let's modify the example for your own data: + +1. Create a new config file `my_training.yml`: + +```yaml +base_model: NousResearch/Nous-Hermes-llama-1b-v1 +adapter: lora + +# Training settings +micro_batch_size: 2 +num_epochs: 3 +learning_rate: 0.0003 + +# Your dataset +datasets: + - path: my_data.jsonl # Your local data file + type: alpaca # Or other format +``` + +This specific config is for LoRA fine-tuning a model with instruction tuning data using +the `alpaca` dataset format, which has the following format: + +```json +{ + "instruction": "Write a description of alpacas.", + "input": "", + "output": "Alpacas are domesticated South American camelids..." +} +``` + +Please see our [Dataset Formats](dataset-formats) for more dataset formats and how to +format them. + +2. Prepare your JSONL data in the specified format (in this case, the expected `alpaca +format): + +```json +{"instruction": "Classify this text", "input": "I love this!", "output": "positive"} +{"instruction": "Classify this text", "input": "Not good at all", "output": "negative"} +``` + +Please consult the supported [Dataset Formats](dataset-formats/) for more details. + +3. Run the training: + +```shell +axolotl train my_training.yml +``` + +## Common Tasks {#sec-common-tasks} + +### Testing Your Model {#sec-testing} + +After training, test your model: + +```shell +axolotl inference my_training.yml --lora-model-dir="./outputs/lora-out" +``` + +### Preprocessing Data {#sec-preprocessing} + +For large datasets, preprocess first: + +```shell +axolotl preprocess my_training.yml +``` + +### Using a UI {#sec-ui} + +Launch a Gradio interface: + +```shell +axolotl inference my_training.yml --lora-model-dir="./outputs/lora-out" --gradio +``` + +## Next Steps {#sec-next-steps} + +Now that you have the basics, you might want to: + +- Try different model architectures +- Experiment with hyperparameters +- Use more advanced training methods +- Scale up to larger models + +Check our other guides for details on these topics: + +- [Configuration Guide](config.qmd) - Full configuration options +- [Dataset Formats](dataset-formats) - Working with different data formats +- [Multi-GPU Training](multi-gpu.qmd) +- [Multi-Node Training](multi-node.qmd) diff --git a/docs/inference.qmd b/docs/inference.qmd new file mode 100644 index 0000000000..59e352c181 --- /dev/null +++ b/docs/inference.qmd @@ -0,0 +1,148 @@ +--- +title: "Inference Guide" +format: + html: + toc: true + toc-depth: 3 + number-sections: true + code-tools: true +execute: + enabled: false +--- + +This guide covers how to use your trained models for inference, including model loading, interactive testing, and common troubleshooting steps. + +## Quick Start {#sec-quickstart} + +### Basic Inference {#sec-basic} + +::: {.panel-tabset} + +## LoRA Models + +```{.bash} +axolotl inference your_config.yml --lora-model-dir="./lora-output-dir" +``` + +## Full Fine-tuned Models + +```{.bash} +axolotl inference your_config.yml --base-model="./completed-model" +``` + +::: + +## Advanced Usage {#sec-advanced} + +### Gradio Interface {#sec-gradio} + +Launch an interactive web interface: + +```{.bash} +axolotl inference your_config.yml --gradio +``` + +### File-based Prompts {#sec-file-prompts} + +Process prompts from a text file: + +```{.bash} +cat /tmp/prompt.txt | axolotl inference your_config.yml \ + --base-model="./completed-model" --prompter=None +``` + +### Memory Optimization {#sec-memory} + +For large models or limited memory: + +```{.bash} +axolotl inference your_config.yml --load-in-8bit=True +``` + +## Merging LoRA Weights {#sec-merging} + +Merge LoRA adapters with the base model: + +```{.bash} +axolotl merge-lora your_config.yml --lora-model-dir="./completed-model" +``` + +### Memory Management for Merging {#sec-memory-management} + +::: {.panel-tabset} + +## Configuration Options + +```{.yaml} +gpu_memory_limit: 20GiB # Adjust based on your GPU +lora_on_cpu: true # Process on CPU if needed +``` + +## Force CPU Merging + +```{.bash} +CUDA_VISIBLE_DEVICES="" axolotl merge-lora ... +``` + +::: + +## Tokenization {#sec-tokenization} + +### Common Issues {#sec-tokenization-issues} + +::: {.callout-warning} +Tokenization mismatches between training and inference are a common source of problems. +::: + +To debug: + +1. Check training tokenization: +```{.bash} +axolotl preprocess your_config.yml --debug +``` + +2. Verify inference tokenization by decoding tokens before model input + +3. Compare token IDs between training and inference + +### Special Tokens {#sec-special-tokens} + +Configure special tokens in your YAML: + +```{.yaml} +special_tokens: + bos_token: "" + eos_token: "" + unk_token: "" +tokens: + - "<|im_start|>" + - "<|im_end|>" +``` + +## Troubleshooting {#sec-troubleshooting} + +### Common Problems {#sec-common-problems} + +::: {.panel-tabset} + +## Memory Issues + +- Use 8-bit loading +- Reduce batch sizes +- Try CPU offloading + +## Token Issues + +- Verify special tokens +- Check tokenizer settings +- Compare training and inference preprocessing + +## Performance Issues + +- Verify model loading +- Check prompt formatting +- Ensure temperature/sampling settings + +::: + +For more details, see our [debugging guide](debugging.qmd). diff --git a/docs/installation.qmd b/docs/installation.qmd new file mode 100644 index 0000000000..f16e814ccc --- /dev/null +++ b/docs/installation.qmd @@ -0,0 +1,119 @@ +--- +title: "Installation Guide" +format: + html: + toc: true + toc-depth: 3 + number-sections: true + code-tools: true +execute: + enabled: false +--- + +This guide covers all the ways you can install and set up Axolotl for your environment. + +## Requirements {#sec-requirements} + +- NVIDIA GPU (Ampere architecture or newer for `bf16` and Flash Attention) or AMD GPU +- Python ≥3.10 +- PyTorch ≥2.4.1 + +## Installation Methods {#sec-installation-methods} + +### PyPI Installation (Recommended) {#sec-pypi} + +```{.bash} +pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] +``` + +We use `--no-build-isolation` in order to detect the installed PyTorch version (if +installed) in order not to clobber it, and so that we set the correct version of +dependencies that are specific to the PyTorch version or other installed +co-dependencies. + +### Edge/Development Build {#sec-edge-build} + +For the latest features between releases: + +```{.bash} +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl +pip3 install packaging ninja +pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' +``` + +### Docker {#sec-docker} + +```{.bash} +docker run --gpus '"all"' --rm -it axolotlai/axolotl:main-latest +``` + +For development with Docker: + +```{.bash} +docker compose up -d +``` + +::: {.callout-tip} +### Advanced Docker Configuration +```{.bash} +docker run --privileged --gpus '"all"' --shm-size 10g --rm -it \ + --name axolotl --ipc=host \ + --ulimit memlock=-1 --ulimit stack=67108864 \ + --mount type=bind,src="${PWD}",target=/workspace/axolotl \ + -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ + axolotlai/axolotl:main-latest +``` +::: + +## Cloud Environments {#sec-cloud} + +### Cloud GPU Providers {#sec-cloud-gpu} + +For providers supporting Docker: + +- Use `axolotlai/axolotl-cloud:main-latest` +- Available on: + - [Latitude.sh](https://latitude.sh/blueprint/989e0e79-3bf6-41ea-a46b-1f246e309d5c) + - [JarvisLabs.ai](https://jarvislabs.ai/templates/axolotl) + - [RunPod](https://runpod.io/gsc?template=v2ickqhz9s&ref=6i7fkpdz) + +### Google Colab {#sec-colab} + +Use our [example notebook](../examples/colab-notebooks/colab-axolotl-example.ipynb). + +## Platform-Specific Instructions {#sec-platform-specific} + +### macOS {#sec-macos} + +```{.bash} +pip3 install --no-build-isolation -e '.' +``` + +See @sec-troubleshooting for Mac-specific issues. + +### Windows {#sec-windows} + +::: {.callout-important} +We recommend using WSL2 (Windows Subsystem for Linux) or Docker. +::: + +## Environment Managers {#sec-env-managers} + +### Conda/Pip venv {#sec-conda} + +1. Install Python ≥3.10 +2. Install PyTorch: https://pytorch.org/get-started/locally/ +3. Install Axolotl: + ```{.bash} + pip3 install packaging + pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' + ``` +4. (Optional) Login to Hugging Face: + ```{.bash} + huggingface-cli login + ``` + +## Troubleshooting {#sec-troubleshooting} + +If you encounter installation issues, see our [FAQ](faq.qmd) and [Debugging Guide](debugging.qmd). diff --git a/docs/multi-gpu.qmd b/docs/multi-gpu.qmd new file mode 100644 index 0000000000..fe293b750b --- /dev/null +++ b/docs/multi-gpu.qmd @@ -0,0 +1,118 @@ +--- +title: "Multi-GPU Training Guide" +format: + html: + toc: true + toc-depth: 3 + number-sections: true + code-tools: true +execute: + enabled: false +--- + +This guide covers advanced training configurations for multi-GPU setups using Axolotl. + +## Overview {#sec-overview} + +Axolotl supports several methods for multi-GPU training: + +- DeepSpeed (recommended) +- FSDP (Fully Sharded Data Parallel) +- FSDP + QLoRA + +## DeepSpeed {#sec-deepspeed} + +DeepSpeed is the recommended approach for multi-GPU training due to its stability and performance. It provides various optimization levels through ZeRO stages. + +### Configuration {#sec-deepspeed-config} + +Add to your YAML config: + +```{.yaml} +deepspeed: deepspeed_configs/zero1.json +``` + +### Usage {#sec-deepspeed-usage} + +```{.bash} +accelerate launch -m axolotl.cli.train examples/llama-2/config.yml --deepspeed deepspeed_configs/zero1.json +``` + +### ZeRO Stages {#sec-zero-stages} + +We provide default configurations for: + +- ZeRO Stage 1 (`zero1.json`) +- ZeRO Stage 2 (`zero2.json`) +- ZeRO Stage 3 (`zero3.json`) + +Choose based on your memory requirements and performance needs. + +## FSDP {#sec-fsdp} + +### Basic FSDP Configuration {#sec-fsdp-config} + +```{.yaml} +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_offload_params: true + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer +``` + +### FSDP + QLoRA {#sec-fsdp-qlora} + +For combining FSDP with QLoRA, see our [dedicated guide](fsdp_qlora.qmd). + +## Performance Optimization {#sec-performance} + +### Liger Kernel Integration {#sec-liger} + +::: {.callout-note} +Liger Kernel provides efficient Triton kernels for LLM training, offering: + +- 20% increase in multi-GPU training throughput +- 60% reduction in memory usage +- Compatibility with both FSDP and DeepSpeed +::: + +Configuration: + +```{.yaml} +plugins: + - axolotl.integrations.liger.LigerPlugin +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true +``` + +## Troubleshooting {#sec-troubleshooting} + +### NCCL Issues {#sec-nccl} + +For NCCL-related problems, see our [NCCL troubleshooting guide](nccl.qmd). + +### Common Problems {#sec-common-problems} + +::: {.panel-tabset} + +## Memory Issues + +- Reduce `micro_batch_size` +- Reduce `eval_batch_size` +- Adjust `gradient_accumulation_steps` +- Consider using a higher ZeRO stage + +## Training Instability + +- Start with DeepSpeed ZeRO-2 +- Monitor loss values +- Check learning rates + +::: + +For more detailed troubleshooting, see our [debugging guide](debugging.qmd). diff --git a/styles.css b/styles.css index 2ddf50c7b4..2e5aa6de8f 100644 --- a/styles.css +++ b/styles.css @@ -1 +1,5 @@ /* css styles */ + +img[alt="Axolotl"] { + content: url("https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/887513285d98132142bf5db2a74eb5e0928787f1/image/axolotl_logo_digital_black.svg") !important; +} From cf17649ef3a1eada923668b37d84511ef72caabf Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 31 Jan 2025 08:58:04 -0500 Subject: [PATCH 0397/1405] Misc fixes 20250130 (#2301) * misc fixes for garbage collection and L40S w NCCL P2P * patch bnb fix for triton check * chore: lint * change up import * try patching differently * remove patch for bnb fix for now * more verbose checks and tweak train loss threshold --- docs/dataset-formats/stepwise_supervised.qmd | 2 +- src/axolotl/utils/callbacks/__init__.py | 8 +++++++- src/axolotl/utils/environment.py | 2 +- tests/e2e/test_process_reward_model_smollm2.py | 2 +- tests/e2e/utils.py | 5 ++++- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/dataset-formats/stepwise_supervised.qmd b/docs/dataset-formats/stepwise_supervised.qmd index 072bf83537..2cec8e1bda 100644 --- a/docs/dataset-formats/stepwise_supervised.qmd +++ b/docs/dataset-formats/stepwise_supervised.qmd @@ -23,4 +23,4 @@ Here's a simple example of a stepwise supervised dataset entry: ], "labels": [true, false] } -``` \ No newline at end of file +``` diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index d92cb9d992..9ca0e84fe8 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -846,6 +846,12 @@ def __init__(self, gc_steps=None): def on_step_end( self, args, state, control, **kwargs # pylint: disable=unused-argument ): - if state.global_step % self.gc_steps == 0: + if self.gc_steps > 0 and state.global_step % self.gc_steps == 0: torch.cuda.empty_cache() gc.collect() + + def on_epoch_end( + self, args, state, control, **kwargs # pylint: disable=unused-argument + ): + torch.cuda.empty_cache() + gc.collect() diff --git a/src/axolotl/utils/environment.py b/src/axolotl/utils/environment.py index cf2e5d23d2..381fec84c5 100644 --- a/src/axolotl/utils/environment.py +++ b/src/axolotl/utils/environment.py @@ -10,7 +10,7 @@ def check_cuda_p2p_ib_support(): if not accelerate_check_cuda_p2p_ib_support(): return False - unsupported_devices = {"RTX 6000 Ada"} + unsupported_devices = {"RTX 6000 Ada", "L40S"} try: device_names, device_count = get_gpu_info() if 1 < device_count < 8: diff --git a/tests/e2e/test_process_reward_model_smollm2.py b/tests/e2e/test_process_reward_model_smollm2.py index 16bf2cdc82..19347cf928 100644 --- a/tests/e2e/test_process_reward_model_smollm2.py +++ b/tests/e2e/test_process_reward_model_smollm2.py @@ -63,7 +63,7 @@ def test_prm(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.5, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.7, "Train Loss (%s) is too high" ) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index de0dba33a3..2baead7d2b 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -82,7 +82,10 @@ def check_tensorboard( reader = SummaryReader(event_file) df = reader.scalars # pylint: disable=invalid-name df = df[(df.tag == tag)] # pylint: disable=invalid-name - assert df.value.values[-1] < lt_val, assertion_err + if "%s" in assertion_err: + assert df.value.values[-1] < lt_val, assertion_err % df.value.values[-1] + else: + assert df.value.values[-1] < lt_val, assertion_err def check_model_output_exists(temp_dir: str, cfg: DictDefault) -> None: From d425d5d3c3ca7644a9da8ed93c3d03f4be0c4854 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 31 Jan 2025 20:58:25 +0700 Subject: [PATCH 0398/1405] fix: add warning for invalid eval_steps or save_steps (#2298) --- src/axolotl/utils/config/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 7ddff62196..767bf659af 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -1,4 +1,5 @@ """Module for working with config dicts""" + import json import logging import os @@ -129,10 +130,18 @@ def normalize_config(cfg): save_steps = 1.0 / (cfg.saves_per_epoch * cfg.num_epochs) if save_steps < 1.0: # prevent saves on every step cfg.save_steps = save_steps + elif save_steps > 1: + LOG.warning( + f"Invalid value for save_steps ({save_steps}) from saves_per_epoch and/or num_epochs. Saving at training end only." + ) if (cfg.val_set_size or cfg.test_datasets) and cfg.evals_per_epoch: eval_steps = 1.0 / (cfg.evals_per_epoch * cfg.num_epochs) if eval_steps < 1.0: # prevent evals on every step cfg.eval_steps = eval_steps + elif eval_steps > 1: + LOG.warning( + f"Invalid value for eval_steps ({eval_steps}) from evals_per_epoch and/or num_epochs. Skipping evaluations." + ) cfg.dataset_processes = cfg.dataset_processes or os.cpu_count() From 78ce2688484b5e77244b241d5542445cdee3882e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 31 Jan 2025 20:18:52 -0500 Subject: [PATCH 0399/1405] KD Trainer w logprobs (#2303) * refactor trainer to prevent circular dependencies later fix loader default KD dataset loading and KD with logprobs filter bad rows make batch smaller handle padding/collation for KD datasets make it work flipped the slice cross entropy loss coefficient during KD make sure to multiply against the correct loss chore: lint triton wip no where support v2 trial no torch.exp inside triton kernel no log etc no torch.tensor v3 fix kwarg don't use triton for now better rescaling for temperatures hash for temperature too use kd_alpha in the correct loss method fix kd loss so it's causal (fixes repeating tokens) var naming and add todo chore: lint refactor so we can easily add new loss functions add license block remove references to triton kd for now handle token/logprob shifting support for custom trainer classes from plugins refactor kd chat template loader move more things to kd plugin remove moved class from import make plugin setup concise increase logging around loading plugins add copyrights remove duplicate code more info on preprocess for kd and fix import be a bit pickier about loading dynamic prompt strategies kd sample packing make loss torch script compat support streaming for processing sft datasts? improve iterable support ensure that batch vs single is done properly tweak check for batched prompt data reward can use same batch check fix reward trainer calls for tokenization improve check for batched reward model doesn't work well with batched add kd trainer e2e test linting rename test files so it gets picked up make the kd e2e fit in vram for ci and add lora version set lora_dropout explicitly lower lr make sure to set tokenizer from l3 70b and save safetensors make sure to use the correct tokenizer fix adapter model check make sure to use tensorboard to capture loss for checks chore: lint chore: lint improve logprob masking and shift in trainer more fixes try tests for kd on l40s don't shift student logits for kd no batching for kd chat templates make sure to truncate logprobs if there are more than top_k change up logic so we always truncate to top_k use iter instead of tuple fix finding the top-k rather than assuming first position has the correct val apply z-score scaling to kd kd loss needs to be calculated in full precision Always re-normalize teacher distribution various fixes * support for configurable top-k/softmax ordering * add attribute check for filter rows and lint * fix logic * handle none case for conversion to int * fix student logit off by one * set kd_temp to 1.0 for test loss * address PR feedback --- .github/workflows/tests.yml | 4 +- cicd/tests.py | 2 +- docs/rlhf.qmd | 2 +- src/axolotl/cli/args.py | 6 + src/axolotl/cli/preprocess.py | 5 +- src/axolotl/common/datasets.py | 6 + src/axolotl/core/trainer_builder.py | 1273 +---------------- src/axolotl/core/trainers/base.py | 988 +++++++++++++ src/axolotl/core/training_args.py | 264 ++++ src/axolotl/datasets.py | 32 +- src/axolotl/integrations/base.py | 41 +- src/axolotl/integrations/kd/__init__.py | 36 + src/axolotl/integrations/kd/args.py | 37 + src/axolotl/integrations/kd/chat_template.py | 201 +++ src/axolotl/integrations/kd/collator.py | 255 ++++ .../integrations/kd/kernels/__init__.py | 0 .../integrations/kd/topk_logprob/LICENSE.md | 58 + .../integrations/kd/topk_logprob/__init__.py | 0 .../kd/topk_logprob/forward_kl.py | 235 +++ src/axolotl/integrations/kd/trainer.py | 113 ++ src/axolotl/prompt_strategies/__init__.py | 13 +- src/axolotl/prompt_strategies/base.py | 2 + .../bradley_terry/chat_template.py | 10 +- .../prompt_strategies/chat_template.py | 155 +- src/axolotl/prompt_strategies/dpo/chatml.py | 29 +- src/axolotl/prompt_strategies/dpo/llama3.py | 30 +- src/axolotl/prompt_tokenizers.py | 4 +- .../config/models/input/v0_4_1/__init__.py | 4 + src/axolotl/utils/data/sft.py | 69 +- src/axolotl/utils/data/shared.py | 14 +- src/axolotl/utils/tokenization.py | 8 + src/axolotl/utils/trainer.py | 210 ++- tests/e2e/integrations/test_kd.py | 121 ++ tests/e2e/integrations/test_liger.py | 2 + 34 files changed, 2876 insertions(+), 1353 deletions(-) create mode 100644 src/axolotl/core/trainers/base.py create mode 100644 src/axolotl/core/training_args.py create mode 100644 src/axolotl/integrations/kd/__init__.py create mode 100644 src/axolotl/integrations/kd/args.py create mode 100644 src/axolotl/integrations/kd/chat_template.py create mode 100644 src/axolotl/integrations/kd/collator.py create mode 100644 src/axolotl/integrations/kd/kernels/__init__.py create mode 100644 src/axolotl/integrations/kd/topk_logprob/LICENSE.md create mode 100644 src/axolotl/integrations/kd/topk_logprob/__init__.py create mode 100644 src/axolotl/integrations/kd/topk_logprob/forward_kl.py create mode 100644 src/axolotl/integrations/kd/trainer.py create mode 100644 tests/e2e/integrations/test_kd.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 87d532a3ba..b530908fde 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -207,7 +207,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.4.1 + pytorch: 2.5.1 num_gpus: 1 axolotl_extras: steps: @@ -247,7 +247,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.5.1 + pytorch: 2.4.1 num_gpus: 1 axolotl_extras: steps: diff --git a/cicd/tests.py b/cicd/tests.py index 616554e646..6fe701632d 100644 --- a/cicd/tests.py +++ b/cicd/tests.py @@ -59,7 +59,7 @@ } N_GPUS = int(os.environ.get("N_GPUS", 1)) -GPU_CONFIG = modal.gpu.A10G(count=N_GPUS) +GPU_CONFIG = modal.gpu.L40S(count=N_GPUS) def run_cmd(cmd: str, run_folder: str): diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 48701c87af..8f52876c18 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -29,7 +29,7 @@ datasets: type: chatml.intel - path: argilla/ultrafeedback-binarized-preferences split: train - type: chatml.argilla + type: chatml ``` #### IPO diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py index a5865be1c5..a39ffc3080 100644 --- a/src/axolotl/cli/args.py +++ b/src/axolotl/cli/args.py @@ -13,6 +13,12 @@ class PreprocessCliArgs: debug_num_examples: int = field(default=1) prompter: Optional[str] = field(default=None) download: Optional[bool] = field(default=True) + iterable: Optional[bool] = field( + default=None, + metadata={ + "help": "Use IterableDataset for streaming processing of large datasets" + }, + ) @dataclass diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index 760fe76fae..5585c88a7a 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -75,7 +75,10 @@ def do_preprocess(cfg: DictDefault, cli_args: PreprocessCliArgs) -> None: ) -def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: +def do_cli( + config: Union[Path, str] = Path("examples/"), + **kwargs, +) -> None: """ Parses `axolotl` config, CLI args, and calls `do_preprocess`. diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index c693c26d83..cbc0d127c6 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -63,11 +63,17 @@ def load_datasets( """ tokenizer = load_tokenizer(cfg) processor = load_processor(cfg, tokenizer=tokenizer) if cfg.processor_type else None + preprocess_iterable = ( + hasattr(cli_args, "iterable") + and cli_args.iterable is not None + and cli_args.iterable + ) train_dataset, eval_dataset, total_num_steps, prompters = prepare_dataset( cfg, tokenizer, processor=processor, + preprocess_iterable=preprocess_iterable, ) if ( diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 6bf03d78c2..89480d775d 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1,59 +1,65 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # pylint: disable=too-many-lines """ Builder for the training args and trainer """ import abc -import gc import importlib import importlib.util import inspect import logging import math -import os import sys from abc import abstractmethod -from collections import defaultdict -from dataclasses import dataclass, field -from functools import wraps from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Type, Union +from typing import List, Type, Union import torch import transformers -from datasets import Dataset -from peft.optimizers import create_loraplus_optimizer -from torch import nn -from torch.optim.lr_scheduler import OneCycleLR -from torch.utils.data import BatchSampler, DataLoader, RandomSampler, SequentialSampler from transformers import ( DataCollatorWithFlattening, EarlyStoppingCallback, - Trainer, TrainerCallback, - TrainingArguments, ) -from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, seed_worker -from transformers.utils import is_sagemaker_mp_enabled -from trl import ( - CPOConfig, - CPOTrainer, - DPOConfig, - DPOTrainer, - KTOConfig, - KTOTrainer, - ORPOConfig, - ORPOTrainer, - PRMConfig, - PRMTrainer, - RewardConfig, - RewardTrainer, +from trl.trainer.utils import RewardDataCollatorWithPadding + +from axolotl.core.trainers.base import ( + AxolotlCPOTrainer, + AxolotlDPOTrainer, + AxolotlKTOTrainer, + AxolotlMambaTrainer, + AxolotlORPOTrainer, + AxolotlPRMTrainer, + AxolotlRewardTrainer, + AxolotlTrainer, + ReLoRATrainer, +) +from axolotl.core.training_args import ( + AxolotlCPOConfig, + AxolotlDPOConfig, + AxolotlKTOConfig, + AxolotlORPOConfig, + AxolotlPRMConfig, + AxolotlRewardConfig, + AxolotlTrainingArguments, ) -from trl.trainer.utils import RewardDataCollatorWithPadding, pad_to_length - from axolotl.integrations.base import PluginManager from axolotl.monkeypatch.multipack import SUPPORTED_MULTIPACK_MODEL_TYPES -from axolotl.monkeypatch.relora import ReLoRACallback, ReLoRAScheduler +from axolotl.monkeypatch.relora import ReLoRACallback from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.callbacks import ( EvalFirstStepCallback, @@ -78,15 +84,6 @@ ) from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator from axolotl.utils.models import ensure_dtype -from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths -from axolotl.utils.schedulers import ( - get_cosine_schedule_with_min_lr, - get_cosine_schedule_with_quadratic_warmup, - get_cosine_schedule_with_warmup_decay_constant, -) - -if is_sagemaker_mp_enabled(): - import smdistributed.modelparallel.torch as smp try: import torch._dynamo # pylint: disable=ungrouped-imports @@ -96,1171 +93,6 @@ LOG = logging.getLogger("axolotl.core.trainer_builder") -def _sanitize_kwargs_for_tagging(tag_names, kwargs=None): - if isinstance(tag_names, str): - tag_names = [tag_names] - - if kwargs is not None: - if "tags" not in kwargs: - kwargs["tags"] = tag_names - elif "tags" in kwargs and isinstance(kwargs["tags"], list): - kwargs["tags"].extend(tag_names) - elif "tags" in kwargs and isinstance(kwargs["tags"], str): - tag_names.append(kwargs["tags"]) - kwargs["tags"] = tag_names - - return kwargs - - -def _sanitize_kwargs_for_ds_tagging(dataset_tags, kwargs=None): - if isinstance(dataset_tags, str): - dataset_tags = [dataset_tags] - - if (dataset_tags is not None) and (kwargs is not None): - if "dataset_tags" not in kwargs: - kwargs["dataset_tags"] = dataset_tags - elif "dataset_tags" in kwargs and isinstance(kwargs["dataset_tags"], list): - kwargs["dataset_tags"].extend(dataset_tags) - elif "dataset_tags" in kwargs and isinstance(kwargs["dataset_tags"], str): - dataset_tags.append(kwargs["dataset_tags"]) - kwargs["dataset_tags"] = dataset_tags - - return kwargs - - -@dataclass -class AxolotlTrainingMixins: - """ - Mixin class for the Axolotl training args. - """ - - # pylint: disable=duplicate-code - model_type: Optional[str] = field( - default=None, metadata={"help": "HF model configuration model_type."} - ) - lr_quadratic_warmup: bool = field( - default=False, - metadata={"help": "Use quadratic warmup for cosine scheduling."}, - ) - pretraining: bool = field( - default=False, - metadata={ - "help": "Indicates to trainer whether we are doing continued pretraining." - }, - ) - sample_packing: bool = field( - default=False, - metadata={"help": "Use sample packing for efficient training."}, - ) - multipack_real_batches: bool = field( - default=False, - metadata={"help": "Use real batches for efficient training."}, - ) - eval_sample_packing: Optional[bool] = field( - default=None, - metadata={"help": "Use sample packing for efficient evals."}, - ) - sample_packing_efficiency: float = field( - default=1.0, - metadata={"help": "Sample packing efficiency for calculating batch length."}, - ) - sample_packing_bin_size: int = field( - default=200, - metadata={ - "help": "The max number of samples that packed sample can contain after packing. Increase for better packing." - }, - ) - sample_packing_group_size: int = field( - default=100000, - metadata={ - "help": "The number of samples to group together for packing. Increase for better packing." - }, - ) - max_seq_length: int = field( - default=2048, - metadata={"help": "The maximum sequence length the model can handle"}, - ) - relora_steps: Optional[int] = field( - default=None, - metadata={"help": "how often to reset for ReLoRA"}, - ) - relora_warmup_steps: Optional[int] = field( - default=None, - metadata={"help": "how many warmup steps to take after reset for ReLoRA"}, - ) - relora_anneal_steps: Optional[int] = field( - default=None, - metadata={"help": "how many warmup steps to take after reset for ReLoRA"}, - ) - relora_prune_ratio: Optional[float] = field( - default=0.9, - metadata={"help": "prune ratio for magnitude pruning of the optimizer"}, - ) - bench_split: Optional[str] = field( - default="eval", metadata={"help": "The benchmark split to run on"} - ) - bench_dataset: Optional[str] = field( - default="pharaouk/dharma-1/dharma_1_mini.json", - metadata={ - "help": "Benchmark dataset to use: options are `mmlu-zs`, `mmlu-fs`, or the full path to the dataset file" - }, - ) - do_bench_eval: Optional[bool] = field( - default=False, metadata={"help": "Whether to run the Benchmark evaluation."} - ) - do_causal_lm_eval: Optional[bool] = field( - default=False, metadata={"help": "Whether to run the Causal LM evaluation."} - ) - max_bench_samples: Optional[int] = field( - default=None, - metadata={ - "help": "If set, only evaluates on `max_bench_samples` of the benchmark dataset." - }, - ) - bench_source_max_len: int = field( - default=2048, metadata={"help": "Maximum source sequence length for bench."} - ) - dataloader_prefetch_factor: Optional[int] = field( - default=None, - metadata={"help": "prefetch_factor argument to the dataloader"}, - ) - cosine_min_lr_ratio: Optional[float] = field( - default=None, - metadata={"help": "Minimum learning rate is min_lr_ratio * learning_rate"}, - ) - cosine_constant_lr_ratio: Optional[float] = field( - default=None, - metadata={ - "help": "Starting constant learning rate step is cosine_constant_lr_ratio * max_steps" - }, - ) - loraplus_lr_ratio: Optional[float] = field( - default=None, metadata={"help": "loraplus learning rate ratio lr_B / lr_A."} - ) - loraplus_lr_embedding: Optional[float] = field( - default=1e-6, - metadata={"help": "loraplus learning rate for lora embedding layers."}, - ) - embedding_lr_scale: Optional[float] = field( - default=None, - metadata={"help": "Scale the learning rate for the embedding layers."}, - ) - lr_groups: Optional[list[dict]] = field( - default=None, - metadata={"help": "Specify learning rate groups for with different LRs."}, - ) - embedding_lr: Optional[float] = field( - default=None, - metadata={"help": "absolute learning rate for the embedding layers."}, - ) - qlora: bool = field( - default=False, - metadata={"help": "whether this is a qlora training"}, - ) - orpo_alpha: Optional[float] = field( - default=None, - ) - lisa_n_layers: Optional[int] = field( - default=None, - metadata={"help": "the number of activate layers in LISA"}, - ) - lisa_step_interval: Optional[int] = field( - default=None, - metadata={"help": "how often to switch layers in LISA"}, - ) - lisa_layers_attribute: Optional[str] = field( - default=None, - metadata={"help": "path under the model to access the layers"}, - ) - curriculum_sampling: Optional[bool] = field( - default=None, - metadata={"help": "whether to use sequential sampling for curriculum learning"}, - ) - alternate_optimizer: Optional[str] = field( - default=None, - metadata={ - "help": "workaround to pass an alternate optimizer to the HF trainer" - }, - ) - alternate_lr_scheduler_type: Optional[str] = field( - default=None, - metadata={ - "help": "workaround to pass an alternate lr scheduler to the HF trainer" - }, - ) - chat_template: Optional[str] = field( - default=None, - metadata={"help": "Chat template converting chat messages to text"}, - ) - - -@dataclass -class AxolotlTrainingArguments(AxolotlTrainingMixins, TrainingArguments): - """ - Training arguments for Causal trainer - - This code is duplicated due to HF TrainingArguments not setting output_dir with a defaujlt value - so it can't be used as a mixin. - """ - - -@dataclass -class AxolotlDPOConfig(AxolotlTrainingMixins, DPOConfig): - """ - DPO config for DPO training - """ - - -@dataclass -class AxolotlORPOConfig(AxolotlTrainingMixins, ORPOConfig): - """ - ORPO config for ORPO training - """ - - -@dataclass -class AxolotlKTOConfig(AxolotlTrainingMixins, KTOConfig): - """ - KTO config for KTO training - """ - - -@dataclass -class AxolotlCPOConfig(AxolotlTrainingMixins, CPOConfig): - """ - CPO config for CPO training - """ - - simpo_gamma: Optional[float] = field( - default=None, - metadata={"help": "simpo gamma parameter"}, - ) - - -@dataclass -class AxolotlRewardConfig(AxolotlTrainingMixins, RewardConfig): - """ - Reward config for Reward training - """ - - -@dataclass -class AxolotlPRMConfig(AxolotlTrainingMixins, PRMConfig): - """ - PRM config for PRM training - """ - - -class SchedulerMixin(Trainer): - """ - Mixin class for scheduler setup in CausalTrainer. - """ - - args = None # type: AxolotlTrainingArguments - - def create_scheduler( - self, num_training_steps: int, optimizer: torch.optim.Optimizer = None - ): - """ - Setup the scheduler. The optimizer of the trainer must have been set up either before this method is called or - passed as an argument. - - Args: - num_training_steps (int): The number of training steps to do. - optimizer (torch.optim.Optimizer): The training optimizer - """ - use_cosine_quadratic = ( - self.args.lr_scheduler_type == "cosine" - and self.args.lr_quadratic_warmup is True - ) - - use_cosine_min_lr = ( - self.args.lr_scheduler_type == "cosine" - and self.args.cosine_min_lr_ratio is not None - ) - - # fmt: off - if self.lr_scheduler is None: # type: ignore # pylint: disable=access-member-before-definition - # fmt: on - if self.args.alternate_lr_scheduler_type == "one_cycle": - num_warmup_steps = self.args.get_warmup_steps(num_training_steps) - pct_start = num_warmup_steps / num_training_steps - extra_lr_kwargs = {} - if "pct_start" not in self.args.lr_scheduler_kwargs: - extra_lr_kwargs["pct_start"] = pct_start - if "anneal_strategy" not in self.args.lr_scheduler_kwargs: - extra_lr_kwargs["anneal_strategy"] = "cos" - - self.lr_scheduler = OneCycleLR( - optimizer, - max_lr=self.args.learning_rate, - total_steps=num_training_steps, - **extra_lr_kwargs, - **self.args.lr_scheduler_kwargs, - ) - elif use_cosine_quadratic: - if use_cosine_min_lr: - LOG.warning("Both cosine quadratic warmup and min lr detected. Using quadratic warmup.") - - self.lr_scheduler = get_cosine_schedule_with_quadratic_warmup( # pylint: disable=attribute-defined-outside-init - optimizer, - num_warmup_steps=self.args.get_warmup_steps(num_training_steps), - num_training_steps=num_training_steps, - ) - elif self.args.cosine_min_lr_ratio and self.args.cosine_constant_lr_ratio and use_cosine_min_lr: - assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" - assert 0 <= self.args.cosine_constant_lr_ratio <= 1.0, "cosine_constant_lr_ratio must be between 0.0 and 1.0" - self.lr_scheduler = get_cosine_schedule_with_warmup_decay_constant( # pylint: disable=attribute-defined-outside-init - optimizer, - num_warmup_steps=self.args.get_warmup_steps(num_training_steps), - num_training_steps=num_training_steps, - min_lr_ratio=self.args.cosine_min_lr_ratio, - constant_lr_ratio=self.args.cosine_constant_lr_ratio, - ) - elif self.args.cosine_min_lr_ratio and use_cosine_min_lr: - assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" - self.lr_scheduler = get_cosine_schedule_with_min_lr( # pylint: disable=attribute-defined-outside-init - optimizer, - num_warmup_steps=self.args.get_warmup_steps(num_training_steps), - num_training_steps=num_training_steps, - min_lr_ratio=self.args.cosine_min_lr_ratio, - ) - else: - return super().create_scheduler(num_training_steps, optimizer=optimizer) - else: - if use_cosine_quadratic: - LOG.warning("axolotl's cosine scheduler with quadratic warmup not used (e.g., because of deepspeed).") - - if use_cosine_min_lr: - LOG.warning("axolotl's cosine scheduler with min lr not used (e.g., because of deepspeed).") - - return self.lr_scheduler - - -class AxolotlTrainer(SchedulerMixin, Trainer): - """ - Extend the base Trainer for axolotl helpers - """ - - args = None # type: AxolotlTrainingArguments - tag_names = ["axolotl"] - - def __init__( - self, - *_args, - bench_data_collator=None, - eval_data_collator=None, - dataset_tags=None, - **kwargs, - ): - self.bench_data_collator = bench_data_collator - self.eval_data_collator = eval_data_collator - self.dataset_tags = dataset_tags - super().__init__(*_args, **kwargs) - self.train_data_collator = self.data_collator - self._stored_metrics = defaultdict(lambda: defaultdict(list)) - if self.args.orpo_alpha: - self.loss_fct = torch.nn.CrossEntropyLoss(reduction="none") - - def _wrap_model(self, model, training=True, dataloader=None): - if self.args.torch_compile: - torch._dynamo.config.accumulated_cache_size_limit = ( # pylint: disable=protected-access - 256 - ) - model = torch.compile( - model, - backend=self.args.torch_compile_backend, - mode=self.args.torch_compile_mode, - ) - return super()._wrap_model(model, training=training, dataloader=dataloader) - - def create_optimizer_grouped_parameters(self, opt_model, optimizer_kwargs): - decay_parameters = self.get_decay_parameter_names(opt_model) - params = { - "to_weight_decay": {}, # LayerNorm and bias - "embeddings": {}, # lm_head, embed_tokens, - "no_weight_decay": {}, - } - lr_groups_lookup = {} - lr_groups_learning_rates = {} - if self.args.lr_groups: - for lr_group in self.args.lr_groups: - group_name = lr_group["name"] - group_modules = lr_group["modules"] - for module in group_modules: - lr_groups_lookup[module] = group_name - lr_groups_learning_rates[group_name] = lr_group["lr"] - params[f"to_weight_decay_{group_name}"] = {} - - for name, param in opt_model.named_parameters(): - if not param.requires_grad: - continue - if name.endswith("modules_to_save.default.weight") or any( - embed_name in name for embed_name in ["embed_tokens", "lm_head"] - ): - params["embeddings"][name] = param - elif name in decay_parameters: - lr_group_modules = [ - group_modules - for group_modules in lr_groups_lookup - if group_modules in name - ] - if lr_groups_lookup and any(lr_group_modules): - lr_group_module = lr_group_modules[0] - group_name = lr_groups_lookup[lr_group_module] - params[f"to_weight_decay_{group_name}"][name] = param - else: - params["to_weight_decay"][name] = param - else: - params["no_weight_decay"][name] = param - optimizer_grouped_parameters = [] - if params["to_weight_decay"]: - optimizer_grouped_parameters.append( - { - "params": list(params["to_weight_decay"].values()), - "weight_decay": self.args.weight_decay, - "lr": optimizer_kwargs["lr"], - } - ) - if params["embeddings"]: - lr = optimizer_kwargs["lr"] # pylint: disable=invalid-name - if self.args.embedding_lr_scale: - lr *= self.args.embedding_lr_scale # pylint: disable=invalid-name - elif self.args.embedding_lr: - lr = self.args.embedding_lr # pylint: disable=invalid-name - optimizer_grouped_parameters.append( - { - "params": list(params["embeddings"].values()), - "weight_decay": 0.0, - "lr": lr, - } - ) - if params["no_weight_decay"]: - optimizer_grouped_parameters.append( - { - "params": list(params["no_weight_decay"].values()), - "weight_decay": 0.0, - "lr": optimizer_kwargs["lr"], - } - ) - for group_name, group_lr in lr_groups_learning_rates.items(): - if params[f"to_weight_decay_{group_name}"]: - optimizer_grouped_parameters.append( - { - "params": list( - params[f"to_weight_decay_{group_name}"].values() - ), - "weight_decay": self.args.weight_decay, - "lr": group_lr, - } - ) - - return optimizer_grouped_parameters - - def create_optimizer(self): - if ( - self.args.loraplus_lr_ratio is None - and self.args.embedding_lr_scale is None - and self.args.embedding_lr is None - and self.args.lr_groups is None - and self.args.alternate_optimizer - not in [ - "optimi_adamw", - "ao_adamw_8bit", - "ao_adamw_4bit", - "ao_adamw_fp8", - "adopt_adamw", - ] - ): - return super().create_optimizer() - - opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model - if self.optimizer is None: # pylint: disable=access-member-before-definition - optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs( - self.args, - opt_model, - ) - optimizer_grouped_parameters = self.create_optimizer_grouped_parameters( - opt_model, optimizer_kwargs - ) - - if self.args.loraplus_lr_ratio is not None: - loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) - loraplus_lr_embedding = getattr( - self.args, "loraplus_lr_embedding", 1e-6 - ) - self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init - opt_model, - optimizer_cls, - loraplus_lr_ratio=loraplus_lr_ratio, - loraplus_lr_embedding=loraplus_lr_embedding, - **optimizer_kwargs, - ) - elif ( - self.args.embedding_lr_scale is not None - or self.args.embedding_lr is not None - or self.args.lr_groups is not None - ): - self.optimizer = ( # pylint: disable=attribute-defined-outside-init - optimizer_cls(optimizer_grouped_parameters, **optimizer_kwargs) - ) - elif self.args.alternate_optimizer == "optimi_adamw": - from optimi import AdamW - - self.optimizer = ( # pylint: disable=attribute-defined-outside-init - AdamW( - optimizer_grouped_parameters, foreach=False, **optimizer_kwargs - ) - ) - elif self.args.alternate_optimizer == "ao_adamw_4bit": - from torchao.prototype.low_bit_optim import AdamW4bit - - self.optimizer = ( # pylint: disable=attribute-defined-outside-init - AdamW4bit(optimizer_grouped_parameters, **optimizer_kwargs) - ) - elif self.args.alternate_optimizer == "ao_adamw_8bit": - from torchao.prototype.low_bit_optim import AdamW8bit - - self.optimizer = ( # pylint: disable=attribute-defined-outside-init - AdamW8bit(optimizer_grouped_parameters, **optimizer_kwargs) - ) - elif self.args.alternate_optimizer == "ao_adamw_fp8": - from torchao.prototype.low_bit_optim import AdamWFp8 - - self.optimizer = ( # pylint: disable=attribute-defined-outside-init - AdamWFp8(optimizer_grouped_parameters, **optimizer_kwargs) - ) - elif self.args.alternate_optimizer == "adopt_adamw": - from axolotl.utils.optimizers.adopt import ADOPT - - self.optimizer = ( # pylint: disable=attribute-defined-outside-init - ADOPT( - optimizer_grouped_parameters, - decouple=True, - **optimizer_kwargs, - ) - ) - - if is_sagemaker_mp_enabled(): - self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init - self.optimizer - ) - - return self.optimizer - - def _get_train_sampler(self) -> Optional[torch.utils.data.Sampler]: - if self.args.sample_packing and not self.args.pretraining: - if self.args.multipack_real_batches: - batch_size = self.args.per_device_train_batch_size - batch_max_len = self.args.max_seq_length - else: - batch_size = 1 - train_batch_size = ( - self.state.train_batch_size or self.args.per_device_train_batch_size - ) - batch_max_len = train_batch_size * self.args.max_seq_length - - if self.args.curriculum_sampling: - sampler = SequentialSampler(self.train_dataset) - else: - sampler = RandomSampler(self.train_dataset) - - return MultipackBatchSampler( - sampler, - lengths=get_dataset_lengths(self.train_dataset), - packing_efficiency_estimate=self.args.sample_packing_efficiency, - batch_max_len=batch_max_len, - batch_size=batch_size, - group_size=self.args.sample_packing_group_size, - bin_size=self.args.sample_packing_bin_size, - drop_last=True, - ) - if self.args.curriculum_sampling: - return SequentialSampler(self.train_dataset) - return super()._get_train_sampler() - - def _get_eval_sampler( - self, eval_dataset: Dataset - ) -> Optional[torch.utils.data.Sampler]: - if self.args.sample_packing and self.args.eval_sample_packing is not False: - if self.args.multipack_real_batches: - batch_size = self.args.per_device_eval_batch_size - batch_max_len = self.args.max_seq_length - else: - batch_size = 1 - batch_max_len = ( - self.args.per_device_eval_batch_size * self.args.max_seq_length - ) - return MultipackBatchSampler( - SequentialSampler(eval_dataset), - lengths=get_dataset_lengths(self.eval_dataset), - packing_efficiency_estimate=self.args.sample_packing_efficiency, - batch_max_len=batch_max_len, - batch_size=batch_size, - group_size=self.args.sample_packing_group_size, - bin_size=self.args.sample_packing_bin_size, - drop_last=True, - ) - return super()._get_eval_sampler(eval_dataset) - - def get_train_dataloader(self) -> DataLoader: - if self.args.sample_packing and not self.args.pretraining: - train_dataset = self.train_dataset - if "length" in train_dataset.features.keys(): - train_dataset = train_dataset.remove_columns(["length"]) - data_collator = self.data_collator - dataloader_params = { - "batch_size": self._train_batch_size, - "collate_fn": data_collator, - "num_workers": self.args.dataloader_num_workers, - "pin_memory": self.args.dataloader_pin_memory, - } - if self.args.dataloader_prefetch_factor: - dataloader_params[ - "prefetch_factor" - ] = self.args.dataloader_prefetch_factor - - sampler = self._get_train_sampler() - if isinstance(sampler, BatchSampler): - dataloader_params["batch_sampler"] = sampler - del dataloader_params["batch_size"] - else: - dataloader_params["sampler"] = sampler - dataloader_params["drop_last"] = self.args.dataloader_drop_last - dataloader_params["worker_init_fn"] = seed_worker - - self.accelerator.even_batches = False - return self.accelerator.prepare_data_loader( - DataLoader(train_dataset, **dataloader_params) - ) - return super().get_train_dataloader() - - def get_eval_dataloader(self, eval_dataset: Optional[Dataset] = None) -> DataLoader: - if self.args.sample_packing and self.args.eval_sample_packing is False: - self.data_collator = ( # pylint: disable=attribute-defined-outside-init - self.eval_data_collator - ) - if eval_dataset: - eval_dataset = eval_dataset.remove_columns(["length"]) - dataloader = super().get_eval_dataloader(eval_dataset) - self.data_collator = ( # pylint: disable=attribute-defined-outside-init - self.train_data_collator - ) - return dataloader - - if self.args.sample_packing and self.args.eval_sample_packing is not False: - eval_dataset = ( - eval_dataset if eval_dataset is not None else self.eval_dataset - ) - - eval_sampler = self._get_eval_sampler(eval_dataset) - eval_dataset = eval_dataset.remove_columns(["length"]) - data_collator = self.data_collator - dataloader_params = { - "batch_size": self.args.eval_batch_size, - "collate_fn": data_collator, - "num_workers": self.args.dataloader_num_workers, - "pin_memory": self.args.dataloader_pin_memory, - } - if self.args.dataloader_prefetch_factor: - dataloader_params[ - "prefetch_factor" - ] = self.args.dataloader_prefetch_factor - - if isinstance(eval_sampler, BatchSampler): - dataloader_params["batch_sampler"] = eval_sampler - del dataloader_params["batch_size"] - else: - dataloader_params["sampler"] = eval_sampler - dataloader_params["drop_last"] = self.args.dataloader_drop_last - - self.accelerator.even_batches = False - return self.accelerator.prepare_data_loader( - DataLoader(eval_dataset, **dataloader_params) - ) - - return super().get_eval_dataloader(eval_dataset) - - def _get_bench_sampler( - self, bench_dataset: Dataset - ) -> Optional[torch.utils.data.Sampler]: - if self.args.world_size <= 1: - return SequentialSampler(bench_dataset) - return None - - def get_bench_dataloader( - self, - bench_dataset: Dataset, - ) -> DataLoader: - dataloader_params = { - "batch_size": self.args.eval_batch_size, - "collate_fn": self.bench_data_collator, - "num_workers": self.args.dataloader_num_workers, - "pin_memory": self.args.dataloader_pin_memory, - } - if self.args.dataloader_prefetch_factor: - dataloader_params["prefetch_factor"] = self.args.dataloader_prefetch_factor - - if not isinstance(bench_dataset, torch.utils.data.IterableDataset): - dataloader_params["sampler"] = self._get_bench_sampler(bench_dataset) - dataloader_params["drop_last"] = self.args.dataloader_drop_last - - return DataLoader(bench_dataset, **dataloader_params) - # return self.accelerator.prepare(DataLoader(bench_dataset, **dataloader_params)) - - def compute_loss( - self, model, inputs, return_outputs=False, num_items_in_batch=None - ): - # use one's weighted cross entropy loss calc - # if self.args.sample_packing: - # labels = inputs.pop("labels") - # outputs = model(**inputs) - # loss = trainer_weighted_loss(outputs, labels, shift_labels=True) - # return (loss, outputs) if return_outputs else loss - if self.args.orpo_alpha: - return self.orpo_compute_loss( - model, - inputs, - return_outputs=return_outputs, - num_items_in_batch=num_items_in_batch, - ) - return super().compute_loss( - model, - inputs, - return_outputs=return_outputs, - num_items_in_batch=num_items_in_batch, - ) - - @staticmethod - def orpo_concatenate_inputs(inputs, label_pad_token=-100, pad_token=0, device=None): - concatenated_batch = {} - - max_length = max( - inputs["input_ids"].shape[1], inputs["rejected_input_ids"].shape[1] - ) - # Concatenate positive and negative inputs - concatenated_batch["input_ids"] = pad_to_length( - inputs["input_ids"], max_length, pad_token - ) - concatenated_batch["rejected_input_ids"] = pad_to_length( - inputs["rejected_input_ids"], max_length, pad_token - ) - concatenated_batch["labels"] = pad_to_length( - inputs["labels"], max_length, label_pad_token - ) - concatenated_batch["rejected_labels"] = pad_to_length( - inputs["rejected_labels"], max_length, label_pad_token - ) - concatenated_batch["attention_mask"] = pad_to_length( - inputs["attention_mask"], max_length, 0 - ) - concatenated_batch["rejected_attention_mask"] = pad_to_length( - inputs["rejected_attention_mask"], max_length, 0 - ) - concatenated_batch["prompt_attention_mask"] = pad_to_length( - inputs["prompt_attention_mask"], max_length, 0 - ).to(device=device) - - input_ids = torch.cat( - [concatenated_batch["input_ids"], concatenated_batch["rejected_input_ids"]], - dim=0, - ).to(device=device) - attention_mask = torch.cat( - [ - concatenated_batch["attention_mask"], - concatenated_batch["rejected_attention_mask"], - ], - dim=0, - ).to(device=device) - labels = torch.cat( - [concatenated_batch["labels"], concatenated_batch["rejected_labels"]], dim=0 - ).to(device=device) - - return { - "input_ids": input_ids, - "labels": labels, - "attention_mask": attention_mask, - "prompt_attention_mask": concatenated_batch["prompt_attention_mask"], - } - - def orpo_compute_custom_loss(self, logits, labels): - logits = logits.contiguous() - loss = 0.0 - - if labels is not None: - # move labels to correct device to enable model parallelism - labels = labels.to(logits.device) - # Shift so that tokens < n predict n - shift_logits = logits[..., :-1, :].contiguous() - shift_labels = labels[..., 1:].contiguous() - - # Flatten the tokens - loss = self.loss_fct(shift_logits.transpose(2, 1), shift_labels).mean( - dim=-1 - ) - - return loss - - def orpo_compute_logps( - self, prompt_attention_mask, chosen_inputs, chosen_attention_mask, logits - ): - # Get the shape of chosen_attention_mask[:, :-1] - chosen_shape = chosen_attention_mask[:, :-1].shape - - # Calculate the padding size - pad_length = chosen_shape[1] - (prompt_attention_mask.shape[1] - 1) - - # Pad prompt_attention_mask with zeros to match the desired shape - prompt_attention_mask_padded = torch.nn.functional.pad( - prompt_attention_mask[:, 1:], (0, pad_length), mode="constant", value=0 - ) - - # Perform the subtraction operation - mask = chosen_attention_mask[:, :-1] > prompt_attention_mask_padded - - per_token_logps = torch.gather( - logits[:, :-1, :].log_softmax(-1), - dim=2, - index=(mask * chosen_inputs[:, 1:]).unsqueeze(2), - ).squeeze(2) - return torch.mul(per_token_logps, mask).sum(dim=1) / mask.sum(dim=1) - - def orpo_compute_loss( - self, - model, - inputs, - return_outputs=False, - num_items_in_batch=None, # pylint: disable=unused-argument - ): - concat_inputs = AxolotlTrainer.orpo_concatenate_inputs( - inputs, - label_pad_token=-100, - pad_token=self.tokenizer.pad_token_id, - device=self.accelerator.device, - ) - - # Perform a single forward pass - outputs = model( - **{ - "input_ids": concat_inputs["input_ids"], - "attention_mask": concat_inputs["attention_mask"], - "labels": concat_inputs["labels"], - }, - output_hidden_states=True, - ) - - # Split the outputs for positive and negative examples - outputs_pos, outputs_neg = outputs.logits.chunk(2) - - # Calculate NLL loss - pos_loss = self.orpo_compute_custom_loss( - logits=outputs_pos, labels=concat_inputs["input_ids"].chunk(2)[0] - ) - - # Calculate Log Probability - pos_prob = self.orpo_compute_logps( - prompt_attention_mask=concat_inputs["prompt_attention_mask"], - chosen_inputs=concat_inputs["input_ids"].chunk(2)[0], - chosen_attention_mask=concat_inputs["attention_mask"].chunk(2)[0], - logits=outputs_pos, - ) - neg_prob = self.orpo_compute_logps( - prompt_attention_mask=concat_inputs["prompt_attention_mask"], - chosen_inputs=concat_inputs["input_ids"].chunk(2)[1], - chosen_attention_mask=concat_inputs["attention_mask"].chunk(2)[1], - logits=outputs_neg, - ) - - # Calculate log odds - log_odds = (pos_prob - neg_prob) - ( - torch.log(1 - torch.exp(pos_prob)) - torch.log(1 - torch.exp(neg_prob)) - ) - sig_ratio = torch.nn.functional.sigmoid(log_odds) - ratio = torch.log(sig_ratio) - - # Calculate the Final Loss - loss = torch.mean(pos_loss - self.args.orpo_alpha * ratio).to( - dtype=torch.bfloat16 - ) - - metrics = {} - metrics["chosen_geometric_mean"] = torch.mean(pos_prob).cpu().item() - metrics["rejected_geometric_mean"] = torch.mean(neg_prob).cpu().item() - metrics["log_odds_ratio"] = torch.mean(ratio).cpu().item() - metrics["log_odds"] = torch.mean(log_odds).cpu().item() - self.store_metrics(metrics, train_eval="train") - - return (loss, outputs_pos) if return_outputs else loss - - @wraps(Trainer.push_to_hub) - def push_to_hub(self, *args, **kwargs) -> str: - """ - Overwrite the `push_to_hub` method in order to force-add the tags when pushing the - model on the Hub. Please refer to `~transformers.Trainer.push_to_hub` for more details. - """ - kwargs = _sanitize_kwargs_for_ds_tagging( - dataset_tags=self.dataset_tags, kwargs=kwargs - ) - kwargs = _sanitize_kwargs_for_tagging(tag_names=self.tag_names, kwargs=kwargs) - - return super().push_to_hub(*args, **kwargs) - - @wraps(Trainer.create_accelerator_and_postprocess) - def create_accelerator_and_postprocess(self): - res = super().create_accelerator_and_postprocess() - - if self.is_fsdp_enabled: - if ( - "limit_all_gathers" in self.args.fsdp_config - and self.args.fsdp_config["limit_all_gathers"] - ): - self.accelerator.state.fsdp_plugin.limit_all_gathers = True - - return res - - def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: - """ - Log `logs` on the various objects watching training, including stored metrics. - - Args: - logs (`Dict[str, float]`): - The values to log. - start_time (`Optional[float]`): - The start of training. - """ - # logs either has 'loss' or 'eval_loss' - train_eval = "train" if "loss" in logs else "eval" - # Add averaged stored metrics to logs - for key, metrics in self._stored_metrics[train_eval].items(): - logs[key] = torch.tensor(metrics).mean().item() - del self._stored_metrics[train_eval] - - return super().log(logs, start_time) - - def store_metrics( - self, metrics: Dict[str, float], train_eval: Literal["train", "eval"] = "train" - ) -> None: - for key, value in metrics.items(): - self._stored_metrics[train_eval][key].append(value) - - def _save_checkpoint(self, model, trial, **kwargs): - # make sure the checkpoint dir exists, since trainer is flakey - checkpoint_folder = f"{PREFIX_CHECKPOINT_DIR}-{self.state.global_step}" - run_dir = self._get_output_dir(trial=trial) - output_dir = os.path.join(run_dir, checkpoint_folder) - os.makedirs(output_dir, exist_ok=True) - return super()._save_checkpoint(model, trial, **kwargs) - - -class AxolotlMambaTrainer(AxolotlTrainer): - """ - Mamba specific trainer to handle loss calculation - """ - - tag_names = ["axolotl", "mamba"] - - def compute_loss( - self, - model, - inputs, - return_outputs=False, # pylint: disable=unused-argument - num_items_in_batch=None, # pylint: disable=unused-argument - ): - input_ids = inputs.pop("input_ids") - lm_logits = model(input_ids).logits - - labels = input_ids.to(lm_logits.device) - shift_logits = lm_logits[:, :-1, :].contiguous() - labels = labels[:, 1:].contiguous() - - loss_fct = torch.nn.CrossEntropyLoss() - lm_loss = loss_fct( - shift_logits.view(-1, shift_logits.size(-1)), labels.view(-1) - ) - - return lm_loss - - -class ReLoRATrainer(AxolotlTrainer): - """ - Trainer subclass that uses the OneCycleLR scheduler - """ - - tag_names = ["axolotl", "relora"] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.lr_scheduler = None - - def create_scheduler( - self, - num_training_steps: int, - optimizer: Optional[torch.optim.Optimizer] = None, - ): - optimizer = self.optimizer if optimizer is None else optimizer - lr_scheduler = super().create_scheduler(num_training_steps, optimizer) - - if self.args.relora_steps: - warmup_steps = ( - self.args.relora_warmup_steps if self.args.relora_warmup_steps else 10 - ) - anneal_steps = ( - self.args.relora_anneal_steps if self.args.relora_anneal_steps else 1 - ) - self.lr_scheduler = ReLoRAScheduler( - optimizer, - lr_scheduler, - self.args.relora_steps, - anneal_steps, - warmup_steps, - ) - else: - self.lr_scheduler = lr_scheduler - - return self.lr_scheduler - - -class AxolotlDPOTrainer(SchedulerMixin, DPOTrainer): - """ - Extend the base DPOTrainer for axolotl helpers - """ - - tag_names = ["axolotl", "dpo"] - - def __init__(self, *args, dataset_tags=None, **kwargs): - super().__init__(*args, **kwargs) - self.dataset_tags = dataset_tags - self.optimizer = None - self.model_accepts_loss_kwargs = False - - def create_optimizer(self): - if self.args.loraplus_lr_ratio is None: - return super().create_optimizer() - - opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model - if self.optimizer is None: # pylint: disable=access-member-before-definition - optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs( - self.args, - opt_model, - ) - - loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) - if loraplus_lr_ratio: - print("Using lora+") - loraplus_lr_embedding = getattr(self.args, "loraplus_lr_embedding", None) - self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init - opt_model, - optimizer_cls, - loraplus_lr_ratio=loraplus_lr_ratio, - loraplus_lr_embedding=loraplus_lr_embedding, - **optimizer_kwargs, - ) - - if is_sagemaker_mp_enabled(): - self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init - self.optimizer - ) - - return self.optimizer - - @wraps(DPOTrainer.push_to_hub) - def push_to_hub(self, *args, **kwargs) -> str: - """ - Overwrite the `push_to_hub` method in order to force-add the tags when pushing the - model on the Hub. Please refer to `~transformers.Trainer.push_to_hub` for more details. - """ - kwargs = _sanitize_kwargs_for_ds_tagging( - dataset_tags=self.dataset_tags, kwargs=kwargs - ) - kwargs = _sanitize_kwargs_for_tagging(tag_names=self.tag_names, kwargs=kwargs) - - return super().push_to_hub(*args, **kwargs) - - @staticmethod - def tokenize_row( - features, - processing_class, - max_prompt_length, - max_completion_length, - add_special_tokens, - ) -> Dict: - res = DPOTrainer.tokenize_row( - features, - processing_class, - max_prompt_length, - max_completion_length, - add_special_tokens, - ) - # fix when the tokenizer doesn't have a bos_token_id, e.g. Qwen - if processing_class.bos_token is None and res["prompt_input_ids"][0] is None: - for key in res.keys(): - res[key] = res[key][1:] - - if processing_class.bos_token and processing_class.bos_token_id is not None: - # dpo trainer may incorrectly prepend the bos_token_id to the dpo outputs - if res["chosen_input_ids"][0] == processing_class.bos_token_id: - res["chosen_input_ids"] = res["chosen_input_ids"][1:] - res["chosen_labels"] = res["chosen_labels"][1:] - res["chosen_attention_mask"] = res["chosen_attention_mask"][1:] - if res["rejected_input_ids"][0] == processing_class.bos_token_id: - res["rejected_input_ids"] = res["rejected_input_ids"][1:] - res["rejected_labels"] = res["rejected_labels"][1:] - res["rejected_attention_mask"] = res["rejected_attention_mask"][1:] - - return res - - def training_step( - self, - model: nn.Module, - inputs: Dict[str, Union[torch.Tensor, Any]], - num_items_in_batch=None, - ) -> torch.Tensor: - loss: torch.Tensor = super().training_step(model, inputs, num_items_in_batch) - gc.collect() - torch.cuda.empty_cache() - return loss - - -class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): - """ - Extend the base ORPOTrainer for axolotl helpers - """ - - tag_names = ["axolotl", "orpo"] - - -class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): - """ - Extend the base KTOTrainer for axolotl helpers - """ - - tag_names = ["axolotl", "kto"] - - -class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): - """ - Extend the base CPOTrainer for axolotl helpers - """ - - tag_names = ["axolotl", "cpo"] - - -class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): - """ - Extend the base RewardTrainer for axolotl helpers - """ - - tag_names = ["axolotl", "reward"] - - -class AxolotlPRMTrainer(SchedulerMixin, PRMTrainer): - """ - Extend the base trl.PRMTrainer for axolotl helpers - """ - - tag_names = ["axolotl", "prm"] - - class TrainerBuilderBase(abc.ABC): """ Base class for trainer builder @@ -1464,6 +296,11 @@ def get_post_trainer_create_callbacks(self, trainer): return callbacks def _get_trainer_cls(self): + if self.cfg.plugins: + plugin_manager = PluginManager.get_instance() + trainer_cls = plugin_manager.get_trainer_cls(self.cfg) + if trainer_cls: + return trainer_cls if self.cfg.relora_steps: return ReLoRATrainer if self.cfg.model_config_type == "mamba": @@ -1862,13 +699,27 @@ def build(self, total_num_steps): "accelerator_config" ] = self.cfg.accelerator_config + if self.cfg.kd_ce_alpha is not None: + training_arguments_kwargs["kd_ce_alpha"] = self.cfg.kd_ce_alpha + if self.cfg.kd_alpha is not None: + training_arguments_kwargs["kd_alpha"] = self.cfg.kd_alpha + if self.cfg.kd_temperature is not None: + training_arguments_kwargs["kd_temperature"] = self.cfg.kd_temperature + if self.cfg.kd_zscore_base_temp is not None: + training_arguments_kwargs[ + "kd_zscore_base_temp" + ] = self.cfg.kd_zscore_base_temp + if self.cfg.kd_top_k_before_softmax is not None: + training_arguments_kwargs[ + "kd_top_k_before_softmax" + ] = self.cfg.kd_top_k_before_softmax + if self.cfg.reward_model: training_args_cls = AxolotlRewardConfig elif self.cfg.process_reward_model: training_args_cls = AxolotlPRMConfig else: training_args_cls = AxolotlTrainingArguments - training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg **training_arguments_kwargs, ) @@ -1995,6 +846,16 @@ def build_collator( collator_args.pop(0) kwargs.pop("pad_to_multiple_of", None) kwargs.pop("padding", None) + elif self.cfg.kd_trainer: + from axolotl.integrations.kd.collator import ( + DataCollatorForKD, + KDBatchSamplerDataCollatorForSeq2Seq, + ) + + if self.cfg.sample_packing: + collator = KDBatchSamplerDataCollatorForSeq2Seq + else: + collator = DataCollatorForKD else: collator = DataCollatorForSeq2Seq diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py new file mode 100644 index 0000000000..44a6d54d73 --- /dev/null +++ b/src/axolotl/core/trainers/base.py @@ -0,0 +1,988 @@ +""" +module for customized trainers +""" + +from __future__ import annotations + +# pylint: disable=too-many-lines +import gc +import logging +import os +from collections import defaultdict +from functools import wraps +from typing import Any, Dict, Literal, Optional, Union + +import torch +from datasets import Dataset +from peft.optimizers import create_loraplus_optimizer +from torch import nn +from torch.optim.lr_scheduler import OneCycleLR +from torch.utils.data import BatchSampler, DataLoader, RandomSampler, SequentialSampler +from transformers import Trainer +from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, seed_worker +from transformers.utils import is_sagemaker_mp_enabled +from trl import ( + CPOTrainer, + DPOTrainer, + KTOTrainer, + ORPOTrainer, + PRMTrainer, + RewardTrainer, +) +from trl.trainer.utils import pad_to_length + +from axolotl.monkeypatch.relora import ReLoRAScheduler +from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths +from axolotl.utils.schedulers import ( + get_cosine_schedule_with_min_lr, + get_cosine_schedule_with_quadratic_warmup, + get_cosine_schedule_with_warmup_decay_constant, +) + +if is_sagemaker_mp_enabled(): + import smdistributed.modelparallel.torch as smp + +LOG = logging.getLogger("axolotl.core.trainer_builder") + + +def _sanitize_kwargs_for_tagging(tag_names, kwargs=None): + if isinstance(tag_names, str): + tag_names = [tag_names] + + if kwargs is not None: + if "tags" not in kwargs: + kwargs["tags"] = tag_names + elif "tags" in kwargs and isinstance(kwargs["tags"], list): + kwargs["tags"].extend(tag_names) + elif "tags" in kwargs and isinstance(kwargs["tags"], str): + tag_names.append(kwargs["tags"]) + kwargs["tags"] = tag_names + + return kwargs + + +def _sanitize_kwargs_for_ds_tagging(dataset_tags, kwargs=None): + if isinstance(dataset_tags, str): + dataset_tags = [dataset_tags] + + if (dataset_tags is not None) and (kwargs is not None): + if "dataset_tags" not in kwargs: + kwargs["dataset_tags"] = dataset_tags + elif "dataset_tags" in kwargs and isinstance(kwargs["dataset_tags"], list): + kwargs["dataset_tags"].extend(dataset_tags) + elif "dataset_tags" in kwargs and isinstance(kwargs["dataset_tags"], str): + dataset_tags.append(kwargs["dataset_tags"]) + kwargs["dataset_tags"] = dataset_tags + + return kwargs + + +class SchedulerMixin(Trainer): + """ + Mixin class for scheduler setup in CausalTrainer. + """ + + args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] + + def create_scheduler( + self, num_training_steps: int, optimizer: torch.optim.Optimizer = None + ): + """ + Setup the scheduler. The optimizer of the trainer must have been set up either before this method is called or + passed as an argument. + + Args: + num_training_steps (int): The number of training steps to do. + optimizer (torch.optim.Optimizer): The training optimizer + """ + use_cosine_quadratic = ( + self.args.lr_scheduler_type == "cosine" + and self.args.lr_quadratic_warmup is True + ) + + use_cosine_min_lr = ( + self.args.lr_scheduler_type == "cosine" + and self.args.cosine_min_lr_ratio is not None + ) + + # fmt: off + if self.lr_scheduler is None: # type: ignore # pylint: disable=access-member-before-definition + # fmt: on + if self.args.alternate_lr_scheduler_type == "one_cycle": + num_warmup_steps = self.args.get_warmup_steps(num_training_steps) + pct_start = num_warmup_steps / num_training_steps + extra_lr_kwargs = {} + if "pct_start" not in self.args.lr_scheduler_kwargs: + extra_lr_kwargs["pct_start"] = pct_start + if "anneal_strategy" not in self.args.lr_scheduler_kwargs: + extra_lr_kwargs["anneal_strategy"] = "cos" + + self.lr_scheduler = OneCycleLR( + optimizer, + max_lr=self.args.learning_rate, + total_steps=num_training_steps, + **extra_lr_kwargs, + **self.args.lr_scheduler_kwargs, + ) + elif use_cosine_quadratic: + if use_cosine_min_lr: + LOG.warning("Both cosine quadratic warmup and min lr detected. Using quadratic warmup.") + + self.lr_scheduler = get_cosine_schedule_with_quadratic_warmup( # pylint: disable=attribute-defined-outside-init + optimizer, + num_warmup_steps=self.args.get_warmup_steps(num_training_steps), + num_training_steps=num_training_steps, + ) + elif self.args.cosine_min_lr_ratio and self.args.cosine_constant_lr_ratio and use_cosine_min_lr: + assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" + assert 0 <= self.args.cosine_constant_lr_ratio <= 1.0, "cosine_constant_lr_ratio must be between 0.0 and 1.0" + self.lr_scheduler = get_cosine_schedule_with_warmup_decay_constant( # pylint: disable=attribute-defined-outside-init + optimizer, + num_warmup_steps=self.args.get_warmup_steps(num_training_steps), + num_training_steps=num_training_steps, + min_lr_ratio=self.args.cosine_min_lr_ratio, + constant_lr_ratio=self.args.cosine_constant_lr_ratio, + ) + elif self.args.cosine_min_lr_ratio and use_cosine_min_lr: + assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" + self.lr_scheduler = get_cosine_schedule_with_min_lr( # pylint: disable=attribute-defined-outside-init + optimizer, + num_warmup_steps=self.args.get_warmup_steps(num_training_steps), + num_training_steps=num_training_steps, + min_lr_ratio=self.args.cosine_min_lr_ratio, + ) + else: + return super().create_scheduler(num_training_steps, optimizer=optimizer) + else: + if use_cosine_quadratic: + LOG.warning("axolotl's cosine scheduler with quadratic warmup not used (e.g., because of deepspeed).") + + if use_cosine_min_lr: + LOG.warning("axolotl's cosine scheduler with min lr not used (e.g., because of deepspeed).") + + return self.lr_scheduler + + +class AxolotlTrainer(SchedulerMixin, Trainer): + """ + Extend the base Trainer for axolotl helpers + """ + + args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] + tag_names = ["axolotl"] + + def __init__( + self, + *_args, + bench_data_collator=None, + eval_data_collator=None, + dataset_tags=None, + **kwargs, + ): + self.bench_data_collator = bench_data_collator + self.eval_data_collator = eval_data_collator + self.dataset_tags = dataset_tags + self._signature_columns = None # workaround for pylint + super().__init__(*_args, **kwargs) + self.train_data_collator = self.data_collator + self._stored_metrics = defaultdict(lambda: defaultdict(list)) + if self.args.orpo_alpha: + self.loss_fct = torch.nn.CrossEntropyLoss(reduction="none") + + def _wrap_model(self, model, training=True, dataloader=None): + if self.args.torch_compile: + torch._dynamo.config.accumulated_cache_size_limit = ( # pylint: disable=protected-access + 256 + ) + model = torch.compile( + model, + backend=self.args.torch_compile_backend, + mode=self.args.torch_compile_mode, + ) + return super()._wrap_model(model, training=training, dataloader=dataloader) + + def create_optimizer_grouped_parameters(self, opt_model, optimizer_kwargs): + decay_parameters = self.get_decay_parameter_names(opt_model) + params = { + "to_weight_decay": {}, # LayerNorm and bias + "embeddings": {}, # lm_head, embed_tokens, + "no_weight_decay": {}, + } + lr_groups_lookup = {} + lr_groups_learning_rates = {} + if self.args.lr_groups: + for lr_group in self.args.lr_groups: + group_name = lr_group["name"] + group_modules = lr_group["modules"] + for module in group_modules: + lr_groups_lookup[module] = group_name + lr_groups_learning_rates[group_name] = lr_group["lr"] + params[f"to_weight_decay_{group_name}"] = {} + + for name, param in opt_model.named_parameters(): + if not param.requires_grad: + continue + if name.endswith("modules_to_save.default.weight") or any( + embed_name in name for embed_name in ["embed_tokens", "lm_head"] + ): + params["embeddings"][name] = param + elif name in decay_parameters: + lr_group_modules = [ + group_modules + for group_modules in lr_groups_lookup + if group_modules in name + ] + if lr_groups_lookup and any(lr_group_modules): + lr_group_module = lr_group_modules[0] + group_name = lr_groups_lookup[lr_group_module] + params[f"to_weight_decay_{group_name}"][name] = param + else: + params["to_weight_decay"][name] = param + else: + params["no_weight_decay"][name] = param + optimizer_grouped_parameters = [] + if params["to_weight_decay"]: + optimizer_grouped_parameters.append( + { + "params": list(params["to_weight_decay"].values()), + "weight_decay": self.args.weight_decay, + "lr": optimizer_kwargs["lr"], + } + ) + if params["embeddings"]: + lr = optimizer_kwargs["lr"] # pylint: disable=invalid-name + if self.args.embedding_lr_scale: + lr *= self.args.embedding_lr_scale # pylint: disable=invalid-name + elif self.args.embedding_lr: + lr = self.args.embedding_lr # pylint: disable=invalid-name + optimizer_grouped_parameters.append( + { + "params": list(params["embeddings"].values()), + "weight_decay": 0.0, + "lr": lr, + } + ) + if params["no_weight_decay"]: + optimizer_grouped_parameters.append( + { + "params": list(params["no_weight_decay"].values()), + "weight_decay": 0.0, + "lr": optimizer_kwargs["lr"], + } + ) + for group_name, group_lr in lr_groups_learning_rates.items(): + if params[f"to_weight_decay_{group_name}"]: + optimizer_grouped_parameters.append( + { + "params": list( + params[f"to_weight_decay_{group_name}"].values() + ), + "weight_decay": self.args.weight_decay, + "lr": group_lr, + } + ) + + return optimizer_grouped_parameters + + def create_optimizer(self): + if ( + self.args.loraplus_lr_ratio is None + and self.args.embedding_lr_scale is None + and self.args.embedding_lr is None + and self.args.lr_groups is None + and self.args.alternate_optimizer + not in [ + "optimi_adamw", + "ao_adamw_8bit", + "ao_adamw_4bit", + "ao_adamw_fp8", + "adopt_adamw", + ] + ): + return super().create_optimizer() + + opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model + if self.optimizer is None: # pylint: disable=access-member-before-definition + optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs( + self.args, + opt_model, + ) + optimizer_grouped_parameters = self.create_optimizer_grouped_parameters( + opt_model, optimizer_kwargs + ) + + if self.args.loraplus_lr_ratio is not None: + loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) + loraplus_lr_embedding = getattr( + self.args, "loraplus_lr_embedding", 1e-6 + ) + self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init + opt_model, + optimizer_cls, + loraplus_lr_ratio=loraplus_lr_ratio, + loraplus_lr_embedding=loraplus_lr_embedding, + **optimizer_kwargs, + ) + elif ( + self.args.embedding_lr_scale is not None + or self.args.embedding_lr is not None + or self.args.lr_groups is not None + ): + self.optimizer = ( # pylint: disable=attribute-defined-outside-init + optimizer_cls(optimizer_grouped_parameters, **optimizer_kwargs) + ) + elif self.args.alternate_optimizer == "optimi_adamw": + from optimi import AdamW + + self.optimizer = ( # pylint: disable=attribute-defined-outside-init + AdamW( + optimizer_grouped_parameters, foreach=False, **optimizer_kwargs + ) + ) + elif self.args.alternate_optimizer == "ao_adamw_4bit": + from torchao.prototype.low_bit_optim import AdamW4bit + + self.optimizer = ( # pylint: disable=attribute-defined-outside-init + AdamW4bit(optimizer_grouped_parameters, **optimizer_kwargs) + ) + elif self.args.alternate_optimizer == "ao_adamw_8bit": + from torchao.prototype.low_bit_optim import AdamW8bit + + self.optimizer = ( # pylint: disable=attribute-defined-outside-init + AdamW8bit(optimizer_grouped_parameters, **optimizer_kwargs) + ) + elif self.args.alternate_optimizer == "ao_adamw_fp8": + from torchao.prototype.low_bit_optim import AdamWFp8 + + self.optimizer = ( # pylint: disable=attribute-defined-outside-init + AdamWFp8(optimizer_grouped_parameters, **optimizer_kwargs) + ) + elif self.args.alternate_optimizer == "adopt_adamw": + from axolotl.utils.optimizers.adopt import ADOPT + + self.optimizer = ( # pylint: disable=attribute-defined-outside-init + ADOPT( + optimizer_grouped_parameters, + decouple=True, + **optimizer_kwargs, + ) + ) + + if is_sagemaker_mp_enabled(): + self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init + self.optimizer + ) + + return self.optimizer + + def _get_train_sampler(self) -> Optional[torch.utils.data.Sampler]: + if self.args.sample_packing and not self.args.pretraining: + if self.args.multipack_real_batches: + batch_size = self.args.per_device_train_batch_size + batch_max_len = self.args.max_seq_length + else: + batch_size = 1 + train_batch_size = ( + self.state.train_batch_size or self.args.per_device_train_batch_size + ) + batch_max_len = train_batch_size * self.args.max_seq_length + + if self.args.curriculum_sampling: + sampler = SequentialSampler(self.train_dataset) + else: + sampler = RandomSampler(self.train_dataset) + + return MultipackBatchSampler( + sampler, + lengths=get_dataset_lengths(self.train_dataset), + packing_efficiency_estimate=self.args.sample_packing_efficiency, + batch_max_len=batch_max_len, + batch_size=batch_size, + group_size=self.args.sample_packing_group_size, + bin_size=self.args.sample_packing_bin_size, + drop_last=True, + ) + if self.args.curriculum_sampling: + return SequentialSampler(self.train_dataset) + return super()._get_train_sampler() + + def _get_eval_sampler( + self, eval_dataset: Dataset + ) -> Optional[torch.utils.data.Sampler]: + if self.args.sample_packing and self.args.eval_sample_packing is not False: + if self.args.multipack_real_batches: + batch_size = self.args.per_device_eval_batch_size + batch_max_len = self.args.max_seq_length + else: + batch_size = 1 + batch_max_len = ( + self.args.per_device_eval_batch_size * self.args.max_seq_length + ) + return MultipackBatchSampler( + SequentialSampler(eval_dataset), + lengths=get_dataset_lengths(self.eval_dataset), + packing_efficiency_estimate=self.args.sample_packing_efficiency, + batch_max_len=batch_max_len, + batch_size=batch_size, + group_size=self.args.sample_packing_group_size, + bin_size=self.args.sample_packing_bin_size, + drop_last=True, + ) + return super()._get_eval_sampler(eval_dataset) + + def get_train_dataloader(self) -> DataLoader: + if self.args.sample_packing and not self.args.pretraining: + train_dataset = self.train_dataset + if "length" in train_dataset.features.keys(): + train_dataset = train_dataset.remove_columns(["length"]) + data_collator = self.data_collator + dataloader_params = { + "batch_size": self._train_batch_size, + "collate_fn": data_collator, + "num_workers": self.args.dataloader_num_workers, + "pin_memory": self.args.dataloader_pin_memory, + } + if self.args.dataloader_prefetch_factor: + dataloader_params[ + "prefetch_factor" + ] = self.args.dataloader_prefetch_factor + + sampler = self._get_train_sampler() + if isinstance(sampler, BatchSampler): + dataloader_params["batch_sampler"] = sampler + del dataloader_params["batch_size"] + else: + dataloader_params["sampler"] = sampler + dataloader_params["drop_last"] = self.args.dataloader_drop_last + dataloader_params["worker_init_fn"] = seed_worker + + self.accelerator.even_batches = False + return self.accelerator.prepare_data_loader( + DataLoader(train_dataset, **dataloader_params) + ) + return super().get_train_dataloader() + + def get_eval_dataloader(self, eval_dataset: Optional[Dataset] = None) -> DataLoader: + if self.args.sample_packing and self.args.eval_sample_packing is False: + self.data_collator = ( # pylint: disable=attribute-defined-outside-init + self.eval_data_collator + ) + if eval_dataset: + eval_dataset = eval_dataset.remove_columns(["length"]) + dataloader = super().get_eval_dataloader(eval_dataset) + self.data_collator = ( # pylint: disable=attribute-defined-outside-init + self.train_data_collator + ) + return dataloader + + if self.args.sample_packing and self.args.eval_sample_packing is not False: + eval_dataset = ( + eval_dataset if eval_dataset is not None else self.eval_dataset + ) + + eval_sampler = self._get_eval_sampler(eval_dataset) + eval_dataset = eval_dataset.remove_columns(["length"]) + data_collator = self.data_collator + dataloader_params = { + "batch_size": self.args.eval_batch_size, + "collate_fn": data_collator, + "num_workers": self.args.dataloader_num_workers, + "pin_memory": self.args.dataloader_pin_memory, + } + if self.args.dataloader_prefetch_factor: + dataloader_params[ + "prefetch_factor" + ] = self.args.dataloader_prefetch_factor + + if isinstance(eval_sampler, BatchSampler): + dataloader_params["batch_sampler"] = eval_sampler + del dataloader_params["batch_size"] + else: + dataloader_params["sampler"] = eval_sampler + dataloader_params["drop_last"] = self.args.dataloader_drop_last + + self.accelerator.even_batches = False + return self.accelerator.prepare_data_loader( + DataLoader(eval_dataset, **dataloader_params) + ) + + return super().get_eval_dataloader(eval_dataset) + + def _get_bench_sampler( + self, bench_dataset: Dataset + ) -> Optional[torch.utils.data.Sampler]: + if self.args.world_size <= 1: + return SequentialSampler(bench_dataset) + return None + + def get_bench_dataloader( + self, + bench_dataset: Dataset, + ) -> DataLoader: + dataloader_params = { + "batch_size": self.args.eval_batch_size, + "collate_fn": self.bench_data_collator, + "num_workers": self.args.dataloader_num_workers, + "pin_memory": self.args.dataloader_pin_memory, + } + if self.args.dataloader_prefetch_factor: + dataloader_params["prefetch_factor"] = self.args.dataloader_prefetch_factor + + if not isinstance(bench_dataset, torch.utils.data.IterableDataset): + dataloader_params["sampler"] = self._get_bench_sampler(bench_dataset) + dataloader_params["drop_last"] = self.args.dataloader_drop_last + + return DataLoader(bench_dataset, **dataloader_params) + # return self.accelerator.prepare(DataLoader(bench_dataset, **dataloader_params)) + + def compute_loss( + self, model, inputs, return_outputs=False, num_items_in_batch=None + ): + # use one's weighted cross entropy loss calc + # if self.args.sample_packing: + # labels = inputs.pop("labels") + # outputs = model(**inputs) + # loss = trainer_weighted_loss(outputs, labels, shift_labels=True) + # return (loss, outputs) if return_outputs else loss + if self.args.orpo_alpha: + return self.orpo_compute_loss( + model, + inputs, + return_outputs=return_outputs, + num_items_in_batch=num_items_in_batch, + ) + return super().compute_loss( + model, + inputs, + return_outputs=return_outputs, + num_items_in_batch=num_items_in_batch, + ) + + @staticmethod + def orpo_concatenate_inputs(inputs, label_pad_token=-100, pad_token=0, device=None): + concatenated_batch = {} + + max_length = max( + inputs["input_ids"].shape[1], inputs["rejected_input_ids"].shape[1] + ) + # Concatenate positive and negative inputs + concatenated_batch["input_ids"] = pad_to_length( + inputs["input_ids"], max_length, pad_token + ) + concatenated_batch["rejected_input_ids"] = pad_to_length( + inputs["rejected_input_ids"], max_length, pad_token + ) + concatenated_batch["labels"] = pad_to_length( + inputs["labels"], max_length, label_pad_token + ) + concatenated_batch["rejected_labels"] = pad_to_length( + inputs["rejected_labels"], max_length, label_pad_token + ) + concatenated_batch["attention_mask"] = pad_to_length( + inputs["attention_mask"], max_length, 0 + ) + concatenated_batch["rejected_attention_mask"] = pad_to_length( + inputs["rejected_attention_mask"], max_length, 0 + ) + concatenated_batch["prompt_attention_mask"] = pad_to_length( + inputs["prompt_attention_mask"], max_length, 0 + ).to(device=device) + + input_ids = torch.cat( + [concatenated_batch["input_ids"], concatenated_batch["rejected_input_ids"]], + dim=0, + ).to(device=device) + attention_mask = torch.cat( + [ + concatenated_batch["attention_mask"], + concatenated_batch["rejected_attention_mask"], + ], + dim=0, + ).to(device=device) + labels = torch.cat( + [concatenated_batch["labels"], concatenated_batch["rejected_labels"]], dim=0 + ).to(device=device) + + return { + "input_ids": input_ids, + "labels": labels, + "attention_mask": attention_mask, + "prompt_attention_mask": concatenated_batch["prompt_attention_mask"], + } + + def orpo_compute_custom_loss(self, logits, labels): + logits = logits.contiguous() + loss = 0.0 + + if labels is not None: + # move labels to correct device to enable model parallelism + labels = labels.to(logits.device) + # Shift so that tokens < n predict n + shift_logits = logits[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + + # Flatten the tokens + loss = self.loss_fct(shift_logits.transpose(2, 1), shift_labels).mean( + dim=-1 + ) + + return loss + + def orpo_compute_logps( + self, prompt_attention_mask, chosen_inputs, chosen_attention_mask, logits + ): + # Get the shape of chosen_attention_mask[:, :-1] + chosen_shape = chosen_attention_mask[:, :-1].shape + + # Calculate the padding size + pad_length = chosen_shape[1] - (prompt_attention_mask.shape[1] - 1) + + # Pad prompt_attention_mask with zeros to match the desired shape + prompt_attention_mask_padded = torch.nn.functional.pad( + prompt_attention_mask[:, 1:], (0, pad_length), mode="constant", value=0 + ) + + # Perform the subtraction operation + mask = chosen_attention_mask[:, :-1] > prompt_attention_mask_padded + + per_token_logps = torch.gather( + logits[:, :-1, :].log_softmax(-1), + dim=2, + index=(mask * chosen_inputs[:, 1:]).unsqueeze(2), + ).squeeze(2) + return torch.mul(per_token_logps, mask).sum(dim=1) / mask.sum(dim=1) + + def orpo_compute_loss( + self, + model, + inputs, + return_outputs=False, + num_items_in_batch=None, # pylint: disable=unused-argument + ): + concat_inputs = AxolotlTrainer.orpo_concatenate_inputs( + inputs, + label_pad_token=-100, + pad_token=self.tokenizer.pad_token_id, + device=self.accelerator.device, + ) + + # Perform a single forward pass + outputs = model( + **{ + "input_ids": concat_inputs["input_ids"], + "attention_mask": concat_inputs["attention_mask"], + "labels": concat_inputs["labels"], + }, + output_hidden_states=True, + ) + + # Split the outputs for positive and negative examples + outputs_pos, outputs_neg = outputs.logits.chunk(2) + + # Calculate NLL loss + pos_loss = self.orpo_compute_custom_loss( + logits=outputs_pos, labels=concat_inputs["input_ids"].chunk(2)[0] + ) + + # Calculate Log Probability + pos_prob = self.orpo_compute_logps( + prompt_attention_mask=concat_inputs["prompt_attention_mask"], + chosen_inputs=concat_inputs["input_ids"].chunk(2)[0], + chosen_attention_mask=concat_inputs["attention_mask"].chunk(2)[0], + logits=outputs_pos, + ) + neg_prob = self.orpo_compute_logps( + prompt_attention_mask=concat_inputs["prompt_attention_mask"], + chosen_inputs=concat_inputs["input_ids"].chunk(2)[1], + chosen_attention_mask=concat_inputs["attention_mask"].chunk(2)[1], + logits=outputs_neg, + ) + + # Calculate log odds + log_odds = (pos_prob - neg_prob) - ( + torch.log(1 - torch.exp(pos_prob)) - torch.log(1 - torch.exp(neg_prob)) + ) + sig_ratio = torch.nn.functional.sigmoid(log_odds) + ratio = torch.log(sig_ratio) + + # Calculate the Final Loss + loss = torch.mean(pos_loss - self.args.orpo_alpha * ratio).to( + dtype=torch.bfloat16 + ) + + metrics = {} + metrics["chosen_geometric_mean"] = torch.mean(pos_prob).cpu().item() + metrics["rejected_geometric_mean"] = torch.mean(neg_prob).cpu().item() + metrics["log_odds_ratio"] = torch.mean(ratio).cpu().item() + metrics["log_odds"] = torch.mean(log_odds).cpu().item() + self.store_metrics(metrics, train_eval="train") + + return (loss, outputs_pos) if return_outputs else loss + + @wraps(Trainer.push_to_hub) + def push_to_hub(self, *args, **kwargs) -> str: + """ + Overwrite the `push_to_hub` method in order to force-add the tags when pushing the + model on the Hub. Please refer to `~transformers.Trainer.push_to_hub` for more details. + """ + kwargs = _sanitize_kwargs_for_ds_tagging( + dataset_tags=self.dataset_tags, kwargs=kwargs + ) + kwargs = _sanitize_kwargs_for_tagging(tag_names=self.tag_names, kwargs=kwargs) + + return super().push_to_hub(*args, **kwargs) + + @wraps(Trainer.create_accelerator_and_postprocess) + def create_accelerator_and_postprocess(self): + res = super().create_accelerator_and_postprocess() + + if self.is_fsdp_enabled: + if ( + "limit_all_gathers" in self.args.fsdp_config + and self.args.fsdp_config["limit_all_gathers"] + ): + self.accelerator.state.fsdp_plugin.limit_all_gathers = True + + return res + + def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: + """ + Log `logs` on the various objects watching training, including stored metrics. + + Args: + logs (`Dict[str, float]`): + The values to log. + start_time (`Optional[float]`): + The start of training. + """ + # logs either has 'loss' or 'eval_loss' + train_eval = "train" if "loss" in logs else "eval" + # Add averaged stored metrics to logs + for key, metrics in self._stored_metrics[train_eval].items(): + logs[key] = torch.tensor(metrics).mean().item() + del self._stored_metrics[train_eval] + + return super().log(logs, start_time) + + def store_metrics( + self, metrics: Dict[str, float], train_eval: Literal["train", "eval"] = "train" + ) -> None: + for key, value in metrics.items(): + self._stored_metrics[train_eval][key].append(value) + + def _save_checkpoint(self, model, trial, **kwargs): + # make sure the checkpoint dir exists, since trainer is flakey + checkpoint_folder = f"{PREFIX_CHECKPOINT_DIR}-{self.state.global_step}" + run_dir = self._get_output_dir(trial=trial) + output_dir = os.path.join(run_dir, checkpoint_folder) + os.makedirs(output_dir, exist_ok=True) + return super()._save_checkpoint(model, trial, **kwargs) + + +class AxolotlMambaTrainer(AxolotlTrainer): + """ + Mamba specific trainer to handle loss calculation + """ + + tag_names = ["axolotl", "mamba"] + + def compute_loss( + self, + model, + inputs, + return_outputs=False, # pylint: disable=unused-argument + num_items_in_batch=None, # pylint: disable=unused-argument + ): + input_ids = inputs.pop("input_ids") + lm_logits = model(input_ids).logits + + labels = input_ids.to(lm_logits.device) + shift_logits = lm_logits[:, :-1, :].contiguous() + labels = labels[:, 1:].contiguous() + + loss_fct = torch.nn.CrossEntropyLoss() + lm_loss = loss_fct( + shift_logits.view(-1, shift_logits.size(-1)), labels.view(-1) + ) + + return lm_loss + + +class ReLoRATrainer(AxolotlTrainer): + """ + Trainer subclass that uses the OneCycleLR scheduler + """ + + tag_names = ["axolotl", "relora"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.lr_scheduler = None + + def create_scheduler( + self, + num_training_steps: int, + optimizer: Optional[torch.optim.Optimizer] = None, + ): + optimizer = self.optimizer if optimizer is None else optimizer + lr_scheduler = super().create_scheduler(num_training_steps, optimizer) + + if self.args.relora_steps: + warmup_steps = ( + self.args.relora_warmup_steps if self.args.relora_warmup_steps else 10 + ) + anneal_steps = ( + self.args.relora_anneal_steps if self.args.relora_anneal_steps else 1 + ) + self.lr_scheduler = ReLoRAScheduler( + optimizer, + lr_scheduler, + self.args.relora_steps, + anneal_steps, + warmup_steps, + ) + else: + self.lr_scheduler = lr_scheduler + + return self.lr_scheduler + + +class AxolotlDPOTrainer(SchedulerMixin, DPOTrainer): + """ + Extend the base DPOTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "dpo"] + + def __init__(self, *args, dataset_tags=None, **kwargs): + super().__init__(*args, **kwargs) + self.dataset_tags = dataset_tags + self.optimizer = None + self.model_accepts_loss_kwargs = False + + def create_optimizer(self): + if self.args.loraplus_lr_ratio is None: + return super().create_optimizer() + + opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model + if self.optimizer is None: # pylint: disable=access-member-before-definition + optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs( + self.args, + opt_model, + ) + + loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) + if loraplus_lr_ratio: + print("Using lora+") + loraplus_lr_embedding = getattr(self.args, "loraplus_lr_embedding", None) + self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init + opt_model, + optimizer_cls, + loraplus_lr_ratio=loraplus_lr_ratio, + loraplus_lr_embedding=loraplus_lr_embedding, + **optimizer_kwargs, + ) + + if is_sagemaker_mp_enabled(): + self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init + self.optimizer + ) + + return self.optimizer + + @wraps(DPOTrainer.push_to_hub) + def push_to_hub(self, *args, **kwargs) -> str: + """ + Overwrite the `push_to_hub` method in order to force-add the tags when pushing the + model on the Hub. Please refer to `~transformers.Trainer.push_to_hub` for more details. + """ + kwargs = _sanitize_kwargs_for_ds_tagging( + dataset_tags=self.dataset_tags, kwargs=kwargs + ) + kwargs = _sanitize_kwargs_for_tagging(tag_names=self.tag_names, kwargs=kwargs) + + return super().push_to_hub(*args, **kwargs) + + @staticmethod + def tokenize_row( + features, + processing_class, + max_prompt_length, + max_completion_length, + add_special_tokens, + ) -> Dict: + res = DPOTrainer.tokenize_row( + features, + processing_class, + max_prompt_length, + max_completion_length, + add_special_tokens, + ) + # fix when the tokenizer doesn't have a bos_token_id, e.g. Qwen + if processing_class.bos_token is None and res["prompt_input_ids"][0] is None: + for key in res.keys(): + res[key] = res[key][1:] + + if processing_class.bos_token and processing_class.bos_token_id is not None: + # dpo trainer may incorrectly prepend the bos_token_id to the dpo outputs + if res["chosen_input_ids"][0] == processing_class.bos_token_id: + res["chosen_input_ids"] = res["chosen_input_ids"][1:] + res["chosen_labels"] = res["chosen_labels"][1:] + res["chosen_attention_mask"] = res["chosen_attention_mask"][1:] + if res["rejected_input_ids"][0] == processing_class.bos_token_id: + res["rejected_input_ids"] = res["rejected_input_ids"][1:] + res["rejected_labels"] = res["rejected_labels"][1:] + res["rejected_attention_mask"] = res["rejected_attention_mask"][1:] + + return res + + def training_step( + self, + model: nn.Module, + inputs: Dict[str, Union[torch.Tensor, Any]], + num_items_in_batch=None, + ) -> torch.Tensor: + loss: torch.Tensor = super().training_step(model, inputs, num_items_in_batch) + gc.collect() + torch.cuda.empty_cache() + return loss + + +class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): + """ + Extend the base ORPOTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "orpo"] + + +class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): + """ + Extend the base KTOTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "kto"] + + +class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): + """ + Extend the base CPOTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "cpo"] + + +class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): + """ + Extend the base RewardTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "reward"] + + +class AxolotlPRMTrainer(SchedulerMixin, PRMTrainer): + """ + Extend the base trl.PRMTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "prm"] diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py new file mode 100644 index 0000000000..9eae52162f --- /dev/null +++ b/src/axolotl/core/training_args.py @@ -0,0 +1,264 @@ +""" +extra axolotl specific training args +""" +from dataclasses import dataclass, field +from typing import Optional + +from transformers import TrainingArguments +from trl import CPOConfig, DPOConfig, KTOConfig, ORPOConfig, PRMConfig, RewardConfig + + +@dataclass +class AxolotlTrainingMixins: + """ + Mixin class for the Axolotl training args. + """ + + # pylint: disable=duplicate-code + model_type: Optional[str] = field( + default=None, metadata={"help": "HF model configuration model_type."} + ) + lr_quadratic_warmup: bool = field( + default=False, + metadata={"help": "Use quadratic warmup for cosine scheduling."}, + ) + pretraining: bool = field( + default=False, + metadata={ + "help": "Indicates to trainer whether we are doing continued pretraining." + }, + ) + sample_packing: bool = field( + default=False, + metadata={"help": "Use sample packing for efficient training."}, + ) + multipack_real_batches: bool = field( + default=False, + metadata={"help": "Use real batches for efficient training."}, + ) + eval_sample_packing: Optional[bool] = field( + default=None, + metadata={"help": "Use sample packing for efficient evals."}, + ) + sample_packing_efficiency: float = field( + default=1.0, + metadata={"help": "Sample packing efficiency for calculating batch length."}, + ) + sample_packing_bin_size: int = field( + default=200, + metadata={ + "help": "The max number of samples that packed sample can contain after packing. Increase for better packing." + }, + ) + sample_packing_group_size: int = field( + default=100000, + metadata={ + "help": "The number of samples to group together for packing. Increase for better packing." + }, + ) + max_seq_length: int = field( + default=2048, + metadata={"help": "The maximum sequence length the model can handle"}, + ) + relora_steps: Optional[int] = field( + default=None, + metadata={"help": "how often to reset for ReLoRA"}, + ) + relora_warmup_steps: Optional[int] = field( + default=None, + metadata={"help": "how many warmup steps to take after reset for ReLoRA"}, + ) + relora_anneal_steps: Optional[int] = field( + default=None, + metadata={"help": "how many warmup steps to take after reset for ReLoRA"}, + ) + relora_prune_ratio: Optional[float] = field( + default=0.9, + metadata={"help": "prune ratio for magnitude pruning of the optimizer"}, + ) + bench_split: Optional[str] = field( + default="eval", metadata={"help": "The benchmark split to run on"} + ) + bench_dataset: Optional[str] = field( + default="pharaouk/dharma-1/dharma_1_mini.json", + metadata={ + "help": "Benchmark dataset to use: options are `mmlu-zs`, `mmlu-fs`, or the full path to the dataset file" + }, + ) + do_bench_eval: Optional[bool] = field( + default=False, metadata={"help": "Whether to run the Benchmark evaluation."} + ) + do_causal_lm_eval: Optional[bool] = field( + default=False, metadata={"help": "Whether to run the Causal LM evaluation."} + ) + max_bench_samples: Optional[int] = field( + default=None, + metadata={ + "help": "If set, only evaluates on `max_bench_samples` of the benchmark dataset." + }, + ) + bench_source_max_len: int = field( + default=2048, metadata={"help": "Maximum source sequence length for bench."} + ) + dataloader_prefetch_factor: Optional[int] = field( + default=None, + metadata={"help": "prefetch_factor argument to the dataloader"}, + ) + cosine_min_lr_ratio: Optional[float] = field( + default=None, + metadata={"help": "Minimum learning rate is min_lr_ratio * learning_rate"}, + ) + cosine_constant_lr_ratio: Optional[float] = field( + default=None, + metadata={ + "help": "Starting constant learning rate step is cosine_constant_lr_ratio * max_steps" + }, + ) + loraplus_lr_ratio: Optional[float] = field( + default=None, metadata={"help": "loraplus learning rate ratio lr_B / lr_A."} + ) + loraplus_lr_embedding: Optional[float] = field( + default=1e-6, + metadata={"help": "loraplus learning rate for lora embedding layers."}, + ) + embedding_lr_scale: Optional[float] = field( + default=None, + metadata={"help": "Scale the learning rate for the embedding layers."}, + ) + lr_groups: Optional[list[dict]] = field( + default=None, + metadata={"help": "Specify learning rate groups for with different LRs."}, + ) + embedding_lr: Optional[float] = field( + default=None, + metadata={"help": "absolute learning rate for the embedding layers."}, + ) + qlora: bool = field( + default=False, + metadata={"help": "whether this is a qlora training"}, + ) + orpo_alpha: Optional[float] = field( + default=None, + ) + lisa_n_layers: Optional[int] = field( + default=None, + metadata={"help": "the number of activate layers in LISA"}, + ) + lisa_step_interval: Optional[int] = field( + default=None, + metadata={"help": "how often to switch layers in LISA"}, + ) + lisa_layers_attribute: Optional[str] = field( + default=None, + metadata={"help": "path under the model to access the layers"}, + ) + curriculum_sampling: Optional[bool] = field( + default=None, + metadata={"help": "whether to use sequential sampling for curriculum learning"}, + ) + alternate_optimizer: Optional[str] = field( + default=None, + metadata={ + "help": "workaround to pass an alternate optimizer to the HF trainer" + }, + ) + alternate_lr_scheduler_type: Optional[str] = field( + default=None, + metadata={ + "help": "workaround to pass an alternate lr scheduler to the HF trainer" + }, + ) + chat_template: Optional[str] = field( + default=None, + metadata={"help": "Chat template converting chat messages to text"}, + ) + + kd_ce_alpha: Optional[float] = field( + default=None, + metadata={ + "help": "The alpha scaling parameter for SFT cross entropy loss when using KD" + }, + ) + + kd_alpha: Optional[float] = field( + default=1.0, + metadata={"help": "The alpha scaling parameter for KD loss"}, + ) + + kd_temperature: Optional[float] = field( + default=1.0, + metadata={ + "help": "the temperature parameter for KL divergence loss when using KD" + }, + ) + + kd_zscore_base_temp: Optional[float] = field( + default=None, + metadata={ + "help": "the base temperature parameter for KL divergence with z-score when using KD" + }, + ) + + kd_top_k_before_softmax: Optional[bool] = field( + default=None, + metadata={ + "help": "Whether to apply top_k_before_softmax to the logits when using KD" + }, + ) + + +@dataclass +class AxolotlTrainingArguments(AxolotlTrainingMixins, TrainingArguments): + """ + Training arguments for Causal trainer + + This code is duplicated due to HF TrainingArguments not setting output_dir with a defaujlt value + so it can't be used as a mixin. + """ + + +@dataclass +class AxolotlDPOConfig(AxolotlTrainingMixins, DPOConfig): + """ + DPO config for DPO training + """ + + +@dataclass +class AxolotlORPOConfig(AxolotlTrainingMixins, ORPOConfig): + """ + ORPO config for ORPO training + """ + + +@dataclass +class AxolotlKTOConfig(AxolotlTrainingMixins, KTOConfig): + """ + KTO config for KTO training + """ + + +@dataclass +class AxolotlCPOConfig(AxolotlTrainingMixins, CPOConfig): + """ + CPO config for CPO training + """ + + simpo_gamma: Optional[float] = field( + default=None, + metadata={"help": "simpo gamma parameter"}, + ) + + +@dataclass +class AxolotlRewardConfig(AxolotlTrainingMixins, RewardConfig): + """ + Reward config for Reward training + """ + + +@dataclass +class AxolotlPRMConfig(AxolotlTrainingMixins, PRMConfig): + """ + PRM config for PRM training + """ diff --git a/src/axolotl/datasets.py b/src/axolotl/datasets.py index e4531930f0..143928019b 100644 --- a/src/axolotl/datasets.py +++ b/src/axolotl/datasets.py @@ -2,7 +2,7 @@ import logging import os -from typing import List, Optional +from typing import List, Optional, Union import torch from datasets import Dataset, IterableDataset @@ -51,7 +51,17 @@ def process(self, dataset): map_kwargs = {} if self.prompt_tokenizer.supports_batched: map_kwargs["batched"] = True - map_kwargs["batch_size"] = 100 + map_kwargs["batch_size"] = 1_000 + + if ( + hasattr(self.prompt_tokenizer, "filter_rows") + and self.prompt_tokenizer.filter_rows + ): + dataset = dataset.filter( + self.prompt_tokenizer.filter_rows, + num_proc=num_proc, + desc="Strategy Filtering Rows", + ) return dataset.map( self.prompt_tokenizer.tokenize_prompt, @@ -63,6 +73,24 @@ def process(self, dataset): ) +def wrap_dataset_for_tokenized_prompt( + prompt_tokenizer: PromptTokenizingStrategy, + dataset: Union[Dataset, IterableDataset], + **kwargs, +): + if isinstance(dataset, IterableDataset): + map_kwargs = {} + if prompt_tokenizer.supports_batched: + map_kwargs["batched"] = True + features = dataset.features.keys() + return dataset.map( + prompt_tokenizer.tokenize_prompt, + remove_columns=features, + **map_kwargs, + ) + return TokenizedPromptDataset(prompt_tokenizer, dataset, **kwargs) + + # TODO this isn't the best since it can't interleave datasets class ConstantLengthDataset(IterableDataset): """ diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index a271c59d10..211d5e51b4 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -111,6 +111,17 @@ def post_lora_load(self, cfg, model): # pylint: disable=unused-argument None """ + def get_trainer_cls(self, cfg): # pylint: disable=unused-argument): + """ + Returns a custom class for the trainer. + + Parameters: + cfg (dict): The global axolotl configuration. + + Returns: + class: The class for the trainer. + """ + def create_optimizer(self, cfg, trainer): # pylint: disable=unused-argument """ Creates and returns an optimizer for training. @@ -212,7 +223,17 @@ def load_plugin(plugin_name: str) -> BasePlugin: module_name, class_name = plugin_name.rsplit(".", 1) # import the module - module = importlib.import_module(module_name) + try: + module = importlib.import_module(module_name) + except ModuleNotFoundError as orig_exc: + try: + if not module_name.startswith("axolotl.integrations."): + module = importlib.import_module("axolotl.integrations." + module_name) + else: + raise orig_exc + except ModuleNotFoundError as exc: + raise orig_exc from exc + # instantiate the class plugin_class = getattr(module, class_name) # create an instance of the class @@ -272,8 +293,10 @@ def register(self, plugin_name: str): ImportError: If the plugin module cannot be imported. """ try: + logging.info(f"Attempting to load plugin: {plugin_name}") plugin = load_plugin(plugin_name) self.plugins[plugin_name] = plugin + logging.info(f"Plugin loaded successfully: {plugin_name}") except ImportError: logging.error(f"Failed to load plugin: {plugin_name}") @@ -346,6 +369,22 @@ def post_lora_load(self, cfg, model): for plugin in self.plugins.values(): plugin.post_lora_load(cfg, model) + def get_trainer_cls(self, cfg): + """ + Calls the get_trainer_cls method of all registered plugins and returns the first non-None trainer class. + + Parameters: + cfg (dict): The configuration for the plugins. + + Returns: + object: The trainer class, or None if none was found. + """ + for plugin in self.plugins.values(): + trainer_cls = plugin.get_trainer_cls(cfg) + if trainer_cls is not None: + return trainer_cls + return None + def create_optimizer(self, cfg, trainer): """ Calls the create_optimizer method of all registered plugins and returns the first non-None optimizer. diff --git a/src/axolotl/integrations/kd/__init__.py b/src/axolotl/integrations/kd/__init__.py new file mode 100644 index 0000000000..8a6e3eda13 --- /dev/null +++ b/src/axolotl/integrations/kd/__init__.py @@ -0,0 +1,36 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Plugin init to add KD support to Axolotl. +""" +from axolotl.integrations.base import BasePlugin + +from .args import KDArgs # pylint: disable=unused-import. # noqa: F401 + + +class KDPlugin(BasePlugin): + """ + Plugin for KD support in Axolotl. + """ + + def get_input_args(self): + return "axolotl.integrations.kd.KDArgs" + + def get_trainer_cls(self, cfg): + if cfg.kd_trainer: + from .trainer import AxolotlKDTrainer + + return AxolotlKDTrainer + return None diff --git a/src/axolotl/integrations/kd/args.py b/src/axolotl/integrations/kd/args.py new file mode 100644 index 0000000000..a88a0dc484 --- /dev/null +++ b/src/axolotl/integrations/kd/args.py @@ -0,0 +1,37 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Plugin args for KD support. +""" +from typing import Optional + +from pydantic import BaseModel + + +class KDArgs(BaseModel): + """ + Input args for knowledge distillation. + """ + + kd_trainer: Optional[bool] = None # whether to use KD trainer + kd_ce_alpha: Optional[ + float + ] = None # loss coefficient for cross-entropy loss during KD + kd_alpha: Optional[float] = None # loss coefficient for KD loss + kd_temperature: Optional[float] = None # temperature for sampling during KD + kd_zscore_base_temp: Optional[float] = None # base temperature for zscore scaling + kd_top_k_before_softmax: Optional[ + bool + ] = None # whether to sample top k before softmax during KD diff --git a/src/axolotl/integrations/kd/chat_template.py b/src/axolotl/integrations/kd/chat_template.py new file mode 100644 index 0000000000..699728e9f4 --- /dev/null +++ b/src/axolotl/integrations/kd/chat_template.py @@ -0,0 +1,201 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Chat template prompt strategy loader with KD support +""" +from typing import Any, Dict + +import torch + +from axolotl.prompt_strategies.chat_template import ChatTemplateStrategy, StrategyLoader + + +class ChatTemplateStrategyWithKD(ChatTemplateStrategy): + """ + Handle fields for logprob KD + """ + + def __init__( + self, + prompter, + tokenizer, + train_on_inputs, + sequence_len, + roles_to_train=None, + train_on_eos=None, + logprobs_field="logprobs", + gen_temperature=1.0, + kd_temperature=1.0, + ): + self.logprobs_field = logprobs_field + self.gen_temperature = gen_temperature + self.kd_temperature = kd_temperature + + super().__init__( + prompter, + tokenizer, + train_on_inputs, + sequence_len, + roles_to_train=roles_to_train, + train_on_eos=train_on_eos, + ) + + @property + def supports_batched(self) -> bool: + # batching doesn't work well for logprob data + return False + + def transform_logprobs(self, sample): + """ + Transform logprobs to target format for KD training + """ + + logprobs = sample.pop(self.logprobs_field) + target_seq_len = len(logprobs) + input_seq_len = len(sample["input_ids"]) + input_padding_len = input_seq_len - target_seq_len + # get non-zero top-k (prune None logprobs from vllm data step) + top_k_vals = [ + len(logprobs[i]) + for i in range(len(logprobs)) + if logprobs[i] is not None and len(logprobs[i]) + ] + max_top_k = max(set(top_k_vals), key=top_k_vals.count) + min_top_k = min(set(top_k_vals), key=top_k_vals.count) + top_k = min(max_top_k, min_top_k) + if top_k == 0: + raise ValueError("No non-zero top-k logprobs found.") + + target_logprobs = [] + target_token_ids = [] + target_mask = [] + + if input_padding_len < 0: + # logprobs is longer than target_seq_len, + # so we need to slice from the left/beginning of logprobs + logprobs = logprobs[:-input_seq_len] + input_padding_len = 0 + # target_seq_len = input_seq_len + + # truncate the second dimension of the logprobs to top_k + logprobs = [row[:top_k] for row in logprobs] + + # fill with -inf for padding_len tokens for top_k tokens + # extend target_logprobs with a padding_len x top_k 2D list filled with -inf + + # for causal models, if we start the range at 1, then we don't need to shift in the trainer + # otherwise, we need to shift in the trainer + shift = 0 + for _ in range(shift, input_padding_len): + target_logprobs.append([-float("inf")] * top_k) + target_token_ids.append(list(range(top_k))) + target_mask.append([0] * top_k) + + for position in range(input_padding_len, input_seq_len): + if sample["labels"][position] == -100: + target_mask.append([0] * top_k) + else: + target_mask.append([1] * top_k) + + for _, token_pos_logprobs in enumerate(logprobs): + # Initialize collections for logprobs and token_ids + position_logprobs = [] + position_token_ids = [] + + # Process each token probability entry + for entry in token_pos_logprobs: + # Extract logprob value + logprob = entry["logprob"] + + # Parse token_id from the "token_id:###" format + token_id = int(entry["token"].split(":")[1]) + + # Append to our collections + position_logprobs.append(logprob) + position_token_ids.append(token_id) + + # Convert to a tensor for easier manipulation + position_logprobs_tensor = torch.tensor( + position_logprobs, dtype=torch.float + ) + + # Now we have distribution at T1 in log form, i.e. log p_{T1}(k). + # Next, re-scale to T2 = self.kd_temperature via exponent-based trick + # p_{T2}(k) = [p_{T1}(k)]^(T1 / T2) / Z + # + # Convert from log to probability + teacher_probs_t1 = position_logprobs_tensor.exp() + if self.kd_temperature != self.gen_temperature: + # Exponentiate by factor (T1 / T2) + exponent = self.gen_temperature / self.kd_temperature + teacher_probs_t2 = teacher_probs_t1**exponent + else: + teacher_probs_t2 = teacher_probs_t1 + # Re-normalize + teacher_probs_t2 = teacher_probs_t2 / teacher_probs_t2.sum( + dim=0, keepdim=True + ) + # Convert back to log + position_logprobs_tensor = torch.log(teacher_probs_t2) + + # Now we have log p_{teacher, T2}(k) stored in position_logprobs_tensor + position_logprobs_scaled = position_logprobs_tensor.tolist() + + target_logprobs.append(position_logprobs_scaled) + target_token_ids.append(position_token_ids) + + if shift == 1: + # since we started at index 1 for causal, we need one more padding token + target_logprobs.append([-float("inf")] * top_k) + target_token_ids.append(list(range(top_k))) + target_mask.append([0] * top_k) + + # Update sample with transformed logprobs + sample["target_logprobs"] = target_logprobs + sample["target_token_ids"] = target_token_ids + sample["target_mask"] = target_mask + + return sample + + def _tokenize_single_prompt(self, prompt): + logprobs = prompt.pop(self.logprobs_field) + tokenized_prompt = super()._tokenize_single_prompt(prompt) + tokenized_prompt[self.logprobs_field] = logprobs + tokenized_prompt = self.transform_logprobs(tokenized_prompt) + + return tokenized_prompt + + +class KDStrategyLoader(StrategyLoader): + """ + Load ChatTemplateStrategy with KD support using StrategyLoader. + """ + + def _get_strategy_cls(self): + return ChatTemplateStrategyWithKD + + def _get_strategy_params(self, cfg, ds_cfg: Dict[str, Any]): + strategy_params = super()._get_strategy_params(cfg, ds_cfg) + if logprobs_field := ds_cfg.get("logprobs_field"): + strategy_params["logprobs_field"] = logprobs_field + if gen_temperature := ds_cfg.get("temperature"): + strategy_params["gen_temperature"] = gen_temperature + if kd_temperature := cfg.get("kd_temperature"): + strategy_params["kd_temperature"] = kd_temperature + + return strategy_params + + +load = KDStrategyLoader() diff --git a/src/axolotl/integrations/kd/collator.py b/src/axolotl/integrations/kd/collator.py new file mode 100644 index 0000000000..de63869c71 --- /dev/null +++ b/src/axolotl/integrations/kd/collator.py @@ -0,0 +1,255 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +DataCollator for axolotl to handle KD fields without using -inf for padding, +and with a teacher_mask to identify padded positions. +""" + +from dataclasses import dataclass +from typing import Any, Optional, Union + +import numpy as np +import torch +from transformers import PreTrainedTokenizerBase +from transformers.utils import PaddingStrategy + +from axolotl.utils.collators.batching import DataCollatorForSeq2Seq + + +@dataclass +class DataCollatorForKD(DataCollatorForSeq2Seq): + """ + Data collator for KD, including handling KD-specific fields. + + This version avoids using -inf and instead uses a large negative value for padding + target_logprobs. It also creates a teacher_mask to indicate which entries are valid. + """ + + # pylint: disable=duplicate-code + tokenizer: PreTrainedTokenizerBase + model: Optional[Any] = None + padding: Union[bool, str, PaddingStrategy] = True + max_length: Optional[int] = None + pad_to_multiple_of: Optional[int] = None + label_pad_token_id: int = -100 + position_pad_token_id: int = 0 + return_tensors: str = "pt" + + def __call__(self, features, return_tensors=None): + if return_tensors is None: + return_tensors = self.return_tensors + + padding_side = self.tokenizer.padding_side + + # Pad labels and position_ids first + for feature_name, pad_token_id in [ + ("labels", self.label_pad_token_id), + ("position_ids", self.position_pad_token_id), + ]: + if feature_name in features[0]: + feat = [f[feature_name] for f in features] + max_len = max(len(x) for x in feat) + if self.pad_to_multiple_of is not None: + max_len = ( + (max_len + self.pad_to_multiple_of - 1) + // self.pad_to_multiple_of + ) * self.pad_to_multiple_of + + for f in features: # pylint: disable=invalid-name + remainder = [pad_token_id] * (max_len - len(f[feature_name])) + if isinstance(f[feature_name], list): + f[feature_name] = ( + f[feature_name] + remainder + if padding_side == "right" + else remainder + f[feature_name] + ) + else: + # If they are numpy arrays + if padding_side == "right": + f[feature_name] = np.concatenate( + [f[feature_name], remainder] + ).astype(np.int64) + else: + f[feature_name] = np.concatenate( + [remainder, f[feature_name]] + ).astype(np.int64) + + # Handle target_logprobs and target_token_ids manually + target_logprobs_list = [] + target_token_ids_list = [] + target_mask_list = [] + has_teacher_data = ("target_logprobs" in features[0]) and ( + "target_token_ids" in features[0] + ) + + if has_teacher_data: + # Extract and remove from features + for f in features: # pylint: disable=invalid-name + target_logprobs_list.append(f.pop("target_logprobs")) + target_token_ids_list.append(f.pop("target_token_ids")) + target_mask_list.append(f.pop("target_mask")) + + # Determine max lengths + max_teacher_seq_len = max(len(seq) for seq in target_logprobs_list) + max_k = max(len(seq_k) for seq in target_logprobs_list for seq_k in seq) + + padded_target_logprobs = [] + padded_target_token_ids = [] + padded_teacher_mask_list = [] + + for t_logprobs, t_ids, t_mask in zip( + target_logprobs_list, target_token_ids_list, target_mask_list + ): + t_logprobs_padded = [] + t_ids_padded = [] + t_mask_padded = [] + + for lp, ids, mask in zip( # pylint: disable=invalid-name + t_logprobs, t_ids, t_mask + ): + lp_len = len(lp) + if lp_len < max_k: + # Use -1e9 for padding logprobs and 0 for token_ids + pad_len = max_k - lp_len + lp = lp + [-1e9] * pad_len # pylint: disable=invalid-name + ids = ids + [0] * pad_len + mask = mask + [0] * pad_len + else: + lp = lp[:max_k] # pylint: disable=invalid-name + ids = ids[:max_k] + mask = mask[:max_k] + + t_logprobs_padded.append(lp) + t_ids_padded.append(ids) + t_mask_padded.append(mask) + + seq_len_diff = max_teacher_seq_len - len(t_logprobs_padded) + if seq_len_diff > 0: + # Pad sequences fully if needed + t_logprobs_padded.extend( + [[-1e9] * max_k for _ in range(seq_len_diff)] + ) + t_ids_padded.extend([[0] * max_k for _ in range(seq_len_diff)]) + t_mask_padded.extend([[0] * max_k for _ in range(seq_len_diff)]) + + padded_target_logprobs.append(t_logprobs_padded) + padded_target_token_ids.append(t_ids_padded) + padded_teacher_mask_list.append(t_mask_padded) + + # Convert to tensors + padded_target_logprobs = torch.tensor( + padded_target_logprobs, dtype=torch.float + ) + padded_target_token_ids = torch.tensor( + padded_target_token_ids, dtype=torch.long + ) + padded_teacher_mask_list = torch.tensor( + padded_teacher_mask_list, dtype=torch.int + ) + + # Pad using tokenizer for regular fields + features = self.tokenizer.pad( + features, + padding=self.padding, + max_length=self.max_length, + pad_to_multiple_of=self.pad_to_multiple_of, + return_tensors=return_tensors, + ) + + # Add back teacher data if present + if has_teacher_data: + features["target_logprobs"] = padded_target_logprobs + features["target_token_ids"] = padded_target_token_ids + features["target_mask"] = padded_teacher_mask_list + + # Prepare decoder_input_ids if the model supports it + if ( + "labels" in features + and self.model is not None + and hasattr(self.model, "prepare_decoder_input_ids_from_labels") + ): + decoder_input_ids = self.model.prepare_decoder_input_ids_from_labels( + labels=features["labels"] + ) + features["decoder_input_ids"] = decoder_input_ids + + return features + + +class KDBatchSamplerDataCollatorForSeq2Seq(DataCollatorForKD): + """ + Collator for multipack (batch of sub-batches) specifically for KD. + Adapts DataCollatorForKD so it can pack multiple sequences in a single batch item. + """ + + def __call__(self, features, return_tensors=None): + """ + Expects that `features` could be either: + - a single list of dicts, OR + - a list of lists of dicts (the "sub-batches" to be packed). + """ + # 1) If we are *not* dealing with multiple sequences per batch element, + # just pass straight to parent. + if not isinstance(features[0], list): + return super().__call__(features, return_tensors=return_tensors) + + # 2) Otherwise, we *are* dealing with multiple sequences in each batch item. + # We want to produce a single "merged" feature dict for each sub-batch. + out_features = [{} for _ in features] + + for i, sub_features in enumerate(features): + # sub_features is a list of dicts, each dict = one sequence’s features + # We'll merge them into out_features[i]. + # + # NOTE: You can customize how you combine fields as needed (e.g. summation + # or offset for attention_mask). Below is a straightforward concatenation/extension. + + for field_name in sub_features[0].keys(): + # Some fields you might want to skip or treat specially: + if field_name == "length": + continue + + # If it’s a KD field that’s a list-of-lists (e.g. target_logprobs), + # you typically just want to flatten them by extending. + if field_name in ["target_logprobs", "target_token_ids", "target_mask"]: + combined = [] + for feat in sub_features: + combined.extend(feat[field_name]) + out_features[i][field_name] = combined + + elif field_name == "attention_mask": + # Here we apply the (j+1) factor to differentiate each sub-sample + # within this merged batch item. + arrays = [] + for j, feat in enumerate(sub_features): + if field_name in feat: + arrays.append((j + 1) * np.array(feat[field_name])) + out_features[i][field_name] = np.concatenate(arrays) + else: + # By default, just concatenate them if they are arrays + # or extend them if they are lists. + # For example, input_ids or labels are often arrays. + arrays = [] + for feat in sub_features: + if field_name in feat: + arr = np.array(feat[field_name]) + arrays.append(arr) + out_features[i][field_name] = np.concatenate(arrays) + + # 3) Now call the parent collator, which will do: + # - padding of labels/position_ids + # - KD-specific padding for target_logprobs, target_token_ids, etc. + # - final conversion to return_tensors + return super().__call__(out_features, return_tensors=return_tensors) diff --git a/src/axolotl/integrations/kd/kernels/__init__.py b/src/axolotl/integrations/kd/kernels/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/integrations/kd/topk_logprob/LICENSE.md b/src/axolotl/integrations/kd/topk_logprob/LICENSE.md new file mode 100644 index 0000000000..435d36d75b --- /dev/null +++ b/src/axolotl/integrations/kd/topk_logprob/LICENSE.md @@ -0,0 +1,58 @@ +### AXOLOTL COMMUNITY LICENSE AGREEMENT + +This Axolotl Community License Agreement (“Agreement”) is entered into by and between Axolotl AI Corp. (“Axolotl”) and +any individual or entity (“Licensee”) who wishes to use the Software (as defined below) in accordance with the terms +and conditions set forth in this Agreement. + +1. Definitions + 1.1 “Licensee” refers to any individual or entity who has obtained a copy of the Software under this Agreement. + 1.2 “Plugin Integration” means independent integration software modules which may or may not be offered by Axolotl, + which may be licensed separately by their respective authors and/or licensors. + 1.3 “Software” refers to the specific sub-directory of the Axolotl, Inc. software located at + https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations and its subdirectories which + permits Plugin Integrations to integrate with the Axolotl service. +2. Grant of License + 2.1 Axolotl hereby grants Licensee a worldwide, non-exclusive, royalty-free, license to use, copy, modify, merge, + publish, distribute, sublicense, and/or otherwise exploit the Software, subject to the following conditions: + - Licensee must comply with all the terms and conditions of this Agreement. + - Licensee must include the original copyright notice and disclaimer of warranty in all copies or substantial + portions of the Software. + 2.2 Licensee may use the Software for any lawful purpose, except as restricted in Section 3. +3. Restrictions + 3.1 Licensee shall not use the Software for any activity that constitutes a commercial activity of offering for + free or for sale any services, platform, or equivalent to third parties for the purposes of allowing such + third parties to fine-tune artificial intelligence models. + 3.2 Licensee shall not: + - Use the Software for any illegal or unauthorized purpose. + - Reverse engineer, decompile, or disassemble the Software. + - Remove or modify any copyright, trademark, or other proprietary notices contained in the Software. + - Use the Software in a way that could damage, disable, overburden, or impair the functionality of the + Software or interfere with any third-party use of the Software. + 3.3 Axolotl reserves the right to restrict certain Plugin Integrations for use with the Software. To the extent Licensee integrates a permitted, applicable Plugin Integration with the Software, Licensee shall comply with any additional terms and conditions imposed by the licensors of such Plugin Integration for use of such Plugin Integrations. Licensee shall contact Axolotl if it has questions about whether its use of the Software falls beyond the scope of this Agreement. +4. Intellectual Property Rights + 4.1 Axolotl and its contributors retain all intellectual property rights in and to the Software. Licensee + acknowledges that this Agreement does not transfer any ownership rights or intellectual property rights to + Licensee. +5. Disclaimer of Warranty + 5.1 THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +6. Termination + 6.1 Axolotl may terminate this Agreement at any time if Licensee fails to comply with any of the terms and + conditions set forth herein. Upon termination, Licensee shall cease all use of the Software and destroy any + copies in its possession. +7. Governing Law + 7.1 This Agreement shall be governed by and construed in accordance with the laws of the State of California, + without regards to conflicts of laws provisions thereof. +8. Entire Agreement + 8.1 This Agreement constitutes the entire agreement between Axolotl and Licensee with respect to the subject matter + hereof and supersedes all prior or contemporaneous understandings or agreements between the parties concerning + the Software, whether written or oral. Axolotl may update the terms of this Agreement from time to time, and + Licensee’s continued use of the Software after any such updates shall constitute acceptance of updated terms + on a go-forward basis. Axolotl will use commercially reasonable efforts to provide Licensee notice of any + material updates. By using the Software, Licensee acknowledges that it has read, understood, and agrees to be + bound by the terms and conditions of this Agreement. + +This Agreement was last updated on August 23, 2024. diff --git a/src/axolotl/integrations/kd/topk_logprob/__init__.py b/src/axolotl/integrations/kd/topk_logprob/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/integrations/kd/topk_logprob/forward_kl.py b/src/axolotl/integrations/kd/topk_logprob/forward_kl.py new file mode 100644 index 0000000000..ab9a54d330 --- /dev/null +++ b/src/axolotl/integrations/kd/topk_logprob/forward_kl.py @@ -0,0 +1,235 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# This software may be used and distributed according to +# the terms of the Axolotl Community License Agreement (the "License"); +# you may not use this file except in compliance with the License. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +""" +loss for top_k KL divergence +""" +import torch + + +def zscore_standardize( + logits: torch.Tensor, + mask: torch.Tensor = None, + base_temperature: float = 1.0, + eps: float = 1e-9, +): + """ + Z-score standardize along the last dimension of `logits`. + i.e., for each [B, seq_len] row, across K entries: + z = (logits - mean) / std, + then scale by 1 / base_temperature if desired. + + mask can be broadcastable or None. If None, we standardize all elements. + """ + if mask is None: + # shape: [B, seq_len, K] + # Mean and std over dim=-1 + mean = logits.mean(dim=-1, keepdim=True) + var = logits.var(dim=-1, unbiased=False, keepdim=True) + else: + # If you have to exclude some tokens, multiply by mask, etc. + float_mask = mask.to(logits.dtype) + count = float_mask.sum(dim=-1, keepdim=True).clamp_min(1.0) + mean = (logits * float_mask).sum(dim=-1, keepdim=True) / count + var = (float_mask * (logits - mean) ** 2).sum(dim=-1, keepdim=True) / count + + std = torch.sqrt(var.clamp_min(eps)) + z = (logits - mean) / std + + # Scale by 1 / base_temperature + z = z / base_temperature + return z + + +@torch.jit.script +def loss( + student_logits: torch.Tensor, + target_token_ids: torch.Tensor, + target_logprobs: torch.Tensor, + target_mask: torch.Tensor, + num_items_in_batch: int = -1, # Use -1 to indicate "None" + kd_temperature: float = 1.0, + top_k_before_softmax: int = 0, +) -> torch.Tensor: + """ + A KD loss function that is TorchScript-friendly. + + Arguments: + student_logits (torch.Tensor): The logits of the student model. + Shape: [B, student_seq_len, vocab_size] + target_token_ids (torch.Tensor): The top-k teacher/target token IDs + Shape: [B, teacher_seq_len, top_k] + target_logprobs (torch.Tensor): The top-k teacher/target logprobs, these should already be re-normalized. + Shape: [B, teacher_seq_len, top_k] + target_mask (torch.Tensor): The mask for valid tokens. + Shape: [B, teacher_seq_len, top_k] + num_items_in_batch (int, optional): The number of items in the batch. + kd_temperature (float, optional): The temperature for KD. + Default: 1.0 + top_k_before_softmax (int, optional): Flag of whether to apply softmax before gathering student top-k logits + Default: 0 + """ + + target_logprobs = target_logprobs.float() + + # Determine the teacher sequence length + # target_token_ids shape: [B, teacher_seq_len, K] + # student_logits shape: [B, student_seq_len, vocab_size] + teacher_seq_len = target_token_ids.shape[1] + + if top_k_before_softmax: + # Slice student logits to match teacher-provided sequence length + student_logits_for_kd = student_logits[ + :, :teacher_seq_len, : + ] # [B, teacher_seq_len, vocab_size] + + # Gather student logits for teacher's top-K tokens + student_logits_topk = torch.gather( + student_logits_for_kd, dim=-1, index=target_token_ids + ) # [B, teacher_seq_len, K] + + student_logits_topk = student_logits_topk.float() + + # Apply KD temperature to student’s logits + if kd_temperature != 1.0: + student_logits_topk = student_logits_topk / kd_temperature + + # Convert student top-k logits to logprobs + student_logprobs_topk = student_logits_topk - torch.logsumexp( + student_logits_topk, dim=-1, keepdim=True + ) # [B, teacher_seq_len, K] + else: + # Slice student logits to match teacher-provided sequence length + student_logits_for_kd = ( + student_logits[:, :teacher_seq_len, :] / kd_temperature + ) # [B, teacher_seq_len, vocab_size] + + # keep in full precision for numerical stability of loss + student_logits_for_kd = student_logits_for_kd.float() + + # Gather student logits for teacher's top-K tokens + student_logits_topk = torch.gather( + student_logits_for_kd, dim=-1, index=target_token_ids + ) # [B, teacher_seq_len, K] + + # Compute logsumexp across full vocabulary + student_lse = torch.logsumexp(student_logits_for_kd, dim=-1, keepdim=True) + + # Convert just the top-k logits to logprobs + student_logprobs_topk = student_logits_topk - student_lse + + # Convert teacher_mask to boolean for indexing + # In TorchScript, .bool() is sometimes unsupported, so we do: + valid_mask = target_mask.to(torch.bool) + + # Prune tensors to only keep valid tokens + student_logprobs_topk = student_logprobs_topk[valid_mask] + target_logprobs = target_logprobs[valid_mask] + + # Convert teacher logprobs to probabilities + teacher_probs = target_logprobs.exp() + + # Compute forward KL + kd_loss_per_token = teacher_probs * (target_logprobs - student_logprobs_topk) + kd_loss = kd_loss_per_token.sum() + + # Multiply by T^2 (classical KD scaling) + if kd_temperature != 1.0: + kd_loss = kd_loss * (kd_temperature**2) + + # Normalize by number of items (if provided) or by valid tokens + if num_items_in_batch > 0: + kd_loss = kd_loss / float(num_items_in_batch) + else: + # Fall back to average over valid tokens + kd_loss = kd_loss / float(kd_loss_per_token.size(0)) + + return kd_loss + + +def topk_kd_loss_with_zscore( + student_logits: torch.Tensor, # [B, seq_len, vocab_size] + target_token_ids: torch.Tensor, # [B, seq_len, K] + target_logprobs: torch.Tensor, # [B, seq_len, K], sums to 1.0 in prob space + target_mask: torch.Tensor, # [B, seq_len, K] or [B, seq_len] + kd_temperature: float = 1.0, # classic KD temperature + zscore_base_temp: float = 1.0, # from the paper + num_items_in_batch: int = -1, +): + """ + A variant of top_k KL divergence with Z-score scaling + from "Logit Standardization in Knowledge Distillation". + """ + + target_logprobs = target_logprobs.float() + + B, teacher_seq_len, K = target_logprobs.shape # pylint: disable=invalid-name + # 1) Gather the student's top-k logits to match teacher + student_logits_for_kd = student_logits[ + :, :teacher_seq_len, : + ] # [B, seq_len, vocab] + student_topk_logits = torch.gather( + student_logits_for_kd, dim=-1, index=target_token_ids + ) # [B, seq_len, K] + + student_topk_logits = student_topk_logits.float() + + # 2) If you want to keep the "classical" T scaling, apply it first + if kd_temperature != 1.0: + student_topk_logits = student_topk_logits / kd_temperature + + # 3) Convert teacher logprobs -> treat them as “logits” for z-score + # (They differ by +some_constant from real logits, but in z-score + # that constant is subtracted out anyway.) + teacher_logits_for_zscore = target_logprobs # rename variable for clarity + + # 4) Z-score teacher and student + # If target_mask is 2D, expand to 3D for the K dimension + if target_mask.dim() == 2 and target_mask.shape[:2] == (B, teacher_seq_len): + target_mask = target_mask.unsqueeze(-1).expand(-1, -1, K) + + teacher_z = zscore_standardize( + teacher_logits_for_zscore, mask=target_mask, base_temperature=zscore_base_temp + ) + student_z = zscore_standardize( + student_topk_logits, mask=target_mask, base_temperature=zscore_base_temp + ) + + # 5) Convert to log-probs for KL + teacher_logprobs_z = teacher_z - torch.logsumexp(teacher_z, dim=-1, keepdim=True) + student_logprobs_z = student_z - torch.logsumexp(student_z, dim=-1, keepdim=True) + + # 6) Restrict to valid tokens if needed + valid_mask = target_mask.bool() # shape [B, seq_len, K] + teacher_probs_z = teacher_logprobs_z.exp() + teacher_probs_z = teacher_probs_z[valid_mask] + teacher_logprobs_z = teacher_logprobs_z[valid_mask] + student_logprobs_z = student_logprobs_z[valid_mask] + + # 7) forward KL: sum( p_teacher * [log(p_teacher) - log(p_student)] ) + kd_loss_per_token = teacher_probs_z * (teacher_logprobs_z - student_logprobs_z) + kd_loss = kd_loss_per_token.sum() + + # 8) If using classical KD scaling by T^2 + if kd_temperature != 1.0: + kd_loss = kd_loss * (kd_temperature**2) + + # Optionally scale by zscore_base_temp**2 if you want (paper might differ). + # kd_loss = kd_loss * (zscore_base_temp**2) + + # 9) Normalize + if num_items_in_batch is not None and num_items_in_batch > 0: + kd_loss = kd_loss / float(num_items_in_batch) + else: + kd_loss = kd_loss / float(kd_loss_per_token.size(0)) + + return kd_loss diff --git a/src/axolotl/integrations/kd/trainer.py b/src/axolotl/integrations/kd/trainer.py new file mode 100644 index 0000000000..f99f2ca28b --- /dev/null +++ b/src/axolotl/integrations/kd/trainer.py @@ -0,0 +1,113 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +KD trainer +""" + +from axolotl.core.trainers.base import AxolotlTrainer + +from .topk_logprob.forward_kl import loss as topk_kd_loss +from .topk_logprob.forward_kl import topk_kd_loss_with_zscore + + +class AxolotlKDTrainer(AxolotlTrainer): + """ + Custom trainer subclass for Knowledge Distillation (KD) + """ + + def _set_signature_columns_if_needed(self): + super()._set_signature_columns_if_needed() + columns_to_add = [] + if self._signature_columns: + if "target_logprobs" not in self._signature_columns: + columns_to_add.append("target_logprobs") + if "target_token_ids" not in self._signature_columns: + columns_to_add.append("target_token_ids") + if "target_mask" not in self._signature_columns: + columns_to_add.append("target_mask") + if columns_to_add: + self._signature_columns += columns_to_add + + def compute_loss( + self, + model, + inputs, + return_outputs=False, + num_items_in_batch=None, + ): + """ + How the loss is computed by Trainer. By default, all models return the loss in the first element. + + Subclass and override for custom behavior. + """ + + target_logprobs = inputs.pop("target_logprobs") + target_token_ids = inputs.pop("target_token_ids") + target_mask = inputs.pop("target_mask") + + seq_len = target_token_ids.shape[1] + + if self.model_accepts_loss_kwargs: + loss_kwargs = {} + if num_items_in_batch is not None: + loss_kwargs["num_items_in_batch"] = num_items_in_batch + inputs = {**inputs, **loss_kwargs} + outputs = model(**inputs) + + # FIXME: account for tokenizer.padding_side + student_logits = outputs["logits"][:, : seq_len - 1, :].contiguous() + + shift_logits = student_logits.contiguous() + target_logprobs_for_loss = target_logprobs[..., 1:, :].contiguous() + target_token_ids_for_loss = target_token_ids[..., 1:, :].contiguous() + target_mask_for_loss = target_mask[..., 1:, :].contiguous() + + if self.args.kd_zscore_base_temp: + loss_kd = topk_kd_loss_with_zscore( + shift_logits, + target_token_ids_for_loss, + target_logprobs_for_loss, + target_mask_for_loss, + kd_temperature=self.args.kd_temperature, + zscore_base_temp=self.args.kd_zscore_base_temp, + num_items_in_batch=num_items_in_batch, + ) + else: + loss_kd = topk_kd_loss( + shift_logits, + target_token_ids_for_loss, + target_logprobs_for_loss, + target_mask_for_loss, + num_items_in_batch=num_items_in_batch, + kd_temperature=self.args.kd_temperature, + top_k_before_softmax=1 if self.args.kd_top_k_before_softmax else 0, + ) + + if self.args.kd_ce_alpha > 0: + kd_alpha = self.args.kd_alpha + loss = self.args.kd_ce_alpha * outputs["loss"] + kd_alpha * loss_kd + else: + loss = loss_kd + # Save past state if it exists + # TODO: this needs to be fixed and made cleaner later. + if self.args.past_index >= 0: + self._past = outputs[ # pylint: disable=attribute-defined-outside-init + self.args.past_index + ] + + if self.args.average_tokens_across_devices and self.model_accepts_loss_kwargs: + loss *= self.accelerator.num_processes + + return (loss, outputs) if return_outputs else loss diff --git a/src/axolotl/prompt_strategies/__init__.py b/src/axolotl/prompt_strategies/__init__.py index 74da20c5e1..645a9329c1 100644 --- a/src/axolotl/prompt_strategies/__init__.py +++ b/src/axolotl/prompt_strategies/__init__.py @@ -16,10 +16,21 @@ def load(strategy, tokenizer, cfg, ds_cfg, processor=None): return messages_load(tokenizer, cfg, ds_cfg, processor=processor) load_fn = "load" + package = "axolotl.prompt_strategies" if strategy.split(".")[-1].startswith("load_"): load_fn = strategy.split(".")[-1] strategy = ".".join(strategy.split(".")[:-1]) - mod = importlib.import_module(f".{strategy}", "axolotl.prompt_strategies") + elif len(strategy.split(".")) > 1: + try: + importlib.import_module( + "." + strategy.split(".")[-1], + ".".join(strategy.split(".")[:-1]), + ) + package = ".".join(strategy.split(".")[:-1]) + strategy = strategy.split(".")[-1] + except ModuleNotFoundError: + pass + mod = importlib.import_module(f".{strategy}", package) func = getattr(mod, load_fn) load_kwargs = {} if strategy == "user_defined": diff --git a/src/axolotl/prompt_strategies/base.py b/src/axolotl/prompt_strategies/base.py index fce2aba14a..cddb3d0e12 100644 --- a/src/axolotl/prompt_strategies/base.py +++ b/src/axolotl/prompt_strategies/base.py @@ -10,6 +10,8 @@ def load(strategy, cfg, module_base=None, **kwargs): try: + if len(strategy.split(".")) == 1: + strategy = strategy + ".default" load_fn = strategy.split(".")[-1] strategy = ".".join(strategy.split(".")[:-1]) mod = importlib.import_module(f".{strategy}", module_base) diff --git a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py index 4f60842c5f..c6b0fe2cf5 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py +++ b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py @@ -21,7 +21,11 @@ class BTChatTemplateStrategy(ChatTemplateStrategy): Bradley-Terry reward model pairwise chat template prompt strategy. """ - def tokenize_prompt(self, prompt): + @property + def supports_batched(self) -> bool: + return False + + def _tokenize_single_prompt(self, prompt): """ :param prompt: the actual row of data from the underlying dataset @@ -39,7 +43,7 @@ def tokenize_prompt(self, prompt): ) prompt[self.messages].append({"role": "user", "content": prompt["input"]}) prompt[self.messages].append({"role": "assistant", "content": prompt["chosen"]}) - chosen_tokenized = super().tokenize_prompt(prompt) + chosen_tokenized = super()._tokenize_single_prompt(prompt) if len(chosen_tokenized["input_ids"]) > max_length: LOG.warning( @@ -62,7 +66,7 @@ def tokenize_prompt(self, prompt): prompt[self.messages].append( {"role": "assistant", "content": prompt["rejected"]} ) - rejected_tokenized = super().tokenize_prompt(prompt) + rejected_tokenized = super()._tokenize_single_prompt(prompt) if len(rejected_tokenized["input_ids"]) > max_length: LOG.warning( diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 5b12130d75..bb87ee45b4 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -3,6 +3,7 @@ """ import logging +from collections import defaultdict from typing import Any, Dict, List, Optional from transformers import ProcessorMixin @@ -193,7 +194,7 @@ class ChatTemplateStrategy(PromptTokenizingStrategy): def __init__( self, - prompter, + prompter: ChatTemplatePrompter, tokenizer, train_on_inputs, sequence_len, @@ -220,22 +221,61 @@ def messages(self): def messages(self, messages): self._messages = messages - def tokenize_prompt(self, prompt): + @property + def supports_batched(self) -> bool: + # Let calling code know we can handle lists of examples + return True + + def is_prompt_batched(self, prompt: dict[str, Any]) -> bool: + try: + return all(isinstance(v, list) for v in prompt.values()) and all( + isinstance(v, list) for v in prompt[self.messages] + ) + except KeyError: + return False + + def tokenize_prompt(self, prompt: dict[str, Any]): + """ + Public method that can handle either a single prompt or a batch of prompts. + """ + + if not self.is_prompt_batched(prompt) or not self.supports_batched: + return self._tokenize_single_prompt(prompt) + + res = defaultdict(lambda: []) + feature_names = list(prompt.keys()) + + # Process each prompt individually + for row in zip(*prompt.values()): + tokenized_prompt = self._tokenize_single_prompt( + dict(zip(feature_names, row)) + ) + for key, val in tokenized_prompt.items(): + for i in range(0, len(val), self.sequence_len): + res[key].append(val[i : i + self.sequence_len]) + + # If there are no examples left, return an empty dictionary + if not res: + return {} + + return dict(res) + + def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: # Old simple legacy behavior that works reliably. if ( not self.roles_to_train and not self.train_on_eos - and not self.prompter.message_field_training - and not self.prompter.message_field_training_detail + and not self.prompter.message_field_training # type: ignore + and not self.prompter.message_field_training_detail # type: ignore ): turns = self.get_conversation_thread(prompt) images = self.get_images(prompt) - prompt_ids = self.prompter.build_prompt( + prompt_ids = self.prompter.build_prompt( # type: ignore turns[:-1], add_generation_prompt=True, images=images, ) - tokenized_res = self.prompter.build_prompt(turns, images=images) + tokenized_res = self.prompter.build_prompt(turns, images=images) # type: ignore tokenized_prompt = {} if isinstance(tokenized_res, list): input_ids = prompt_ids + tokenized_res[len(prompt_ids) :] @@ -256,7 +296,7 @@ def tokenize_prompt(self, prompt): return tokenized_prompt turns = self.get_conversation_thread(prompt) - input_ids = self.prompter.build_prompt(turns) + input_ids = self.prompter.build_prompt(turns) # type: ignore labels = [IGNORE_TOKEN_ID] * len(input_ids) last_eos_idx = -1 @@ -286,7 +326,7 @@ def tokenize_prompt(self, prompt): if should_train and turn_start_idx != -1 and turn_end_idx != -1: if train_detail: - token_offsets = self.prompter.get_offsets_for_train_detail( + token_offsets = self.prompter.get_offsets_for_train_detail( # type: ignore content, train_detail ) LOG.debug(f"Token offsets: {token_offsets}") @@ -459,43 +499,62 @@ def get_images(self, prompt): return prompt.get(self.images, None) -def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None, processor=None): - # pylint: disable=duplicate-code - ds_cfg = ds_cfg or {} - chat_template_string = get_chat_template_from_config( - cfg=cfg, ds_cfg=ds_cfg, tokenizer=tokenizer - ) - LOG.info(f"Using chat template:\n---\n{chat_template_string!s}\n---") - - prompter_params = { - "tokenizer": tokenizer, - "chat_template": chat_template_string, - "message_field_role": ds_cfg.get("message_field_role", "role"), - "message_field_content": ds_cfg.get("message_field_content", "content"), - "message_field_training": ds_cfg.get("message_field_training", None), - "message_field_training_detail": ds_cfg.get( - "message_field_training_detail", - None, - ), - "roles": ds_cfg.get("roles"), - "drop_system_message": ds_cfg.get("drop_system_message", False), - # we need to add one for detecting sequences with exceeding the `sequence_len` limit. - "max_length": cfg.sequence_len + 1, - "processor": processor, - } - - strategy_params = { - "train_on_inputs": cfg.train_on_inputs, - "sequence_len": cfg.sequence_len, - "roles_to_train": ds_cfg.get("roles_to_train", ["assistant"]), - "train_on_eos": ds_cfg.get("train_on_eos", "turn"), - } - - strategy = ChatTemplateStrategy( - ChatTemplatePrompter(**prompter_params), tokenizer=tokenizer, **strategy_params - ) - - if "field_messages" in ds_cfg and hasattr(strategy, "messages"): - strategy.messages = ds_cfg["field_messages"] - - return strategy +class StrategyLoader: + """ + Load chat template strategy based on configuration. + """ + + def _get_strategy_cls(self): + return ChatTemplateStrategy + + def _get_strategy_params(self, cfg, ds_cfg: Dict[str, Any]): + return { + "train_on_inputs": cfg.train_on_inputs, + "sequence_len": cfg.sequence_len, + "roles_to_train": ds_cfg.get("roles_to_train", ["assistant"]), + "train_on_eos": ds_cfg.get("train_on_eos", "turn"), + } + + def __call__( + self, tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None, processor=None + ): + # pylint: disable=duplicate-code + ds_cfg = ds_cfg or {} + chat_template_string = get_chat_template_from_config( + cfg=cfg, ds_cfg=ds_cfg, tokenizer=tokenizer + ) + LOG.info(f"Using chat template:\n---\n{chat_template_string!s}\n---") + + prompter_params = { + "tokenizer": tokenizer, + "chat_template": chat_template_string, + "message_field_role": ds_cfg.get("message_field_role", "role"), + "message_field_content": ds_cfg.get("message_field_content", "content"), + "message_field_training": ds_cfg.get("message_field_training", None), + "message_field_training_detail": ds_cfg.get( + "message_field_training_detail", + None, + ), + "roles": ds_cfg.get("roles"), + "drop_system_message": ds_cfg.get("drop_system_message", False), + # we need to add one for detecting sequences with exceeding the `sequence_len` limit. + "max_length": cfg.sequence_len + 1, + "processor": processor, + } + + strategy_params = self._get_strategy_params(cfg, ds_cfg) + strategy_cls = self._get_strategy_cls() + + strategy = strategy_cls( + ChatTemplatePrompter(**prompter_params), + tokenizer=tokenizer, + **strategy_params, + ) + + if "field_messages" in ds_cfg and hasattr(strategy, "messages"): + strategy.messages = ds_cfg["field_messages"] + + return strategy + + +load = StrategyLoader() diff --git a/src/axolotl/prompt_strategies/dpo/chatml.py b/src/axolotl/prompt_strategies/dpo/chatml.py index 585696e29a..5043a501e6 100644 --- a/src/axolotl/prompt_strategies/dpo/chatml.py +++ b/src/axolotl/prompt_strategies/dpo/chatml.py @@ -3,22 +3,41 @@ """ -def argilla( +def default( cfg, **kwargs, ): # pylint: disable=possibly-unused-variable,unused-argument def transform_fn(sample): + if "prompt" in sample.keys(): + prompt_key = "prompt" + elif "input" in sample.keys(): + prompt_key = "input" + elif "question" in sample.keys(): + prompt_key = "question" + else: + prompt_key = "instruction" + + if "chosen" in sample.keys(): + chosen_key = "chosen" + else: + chosen_key = "chosen_response" + + if "rejected" in sample.keys(): + rejected_key = "rejected" + else: + rejected_key = "rejected_response" + if "system" in sample and sample["system"]: sample["prompt"] = ( f"<|im_start|>system\n{sample['system']}<|im_end|>\n" - f"<|im_start|>user\n{sample['instruction']}<|im_end|>\n<|im_start|>assistant\n" + f"<|im_start|>user\n{sample[prompt_key]}<|im_end|>\n<|im_start|>assistant\n" ) else: sample[ "prompt" - ] = f"<|im_start|>user\n{sample['instruction']}<|im_end|>\n<|im_start|>assistant\n" - sample["chosen"] = f"{sample['chosen_response']}<|im_end|>" - sample["rejected"] = f"{sample['rejected_response']}<|im_end|>" + ] = f"<|im_start|>user\n{sample[prompt_key]}<|im_end|>\n<|im_start|>assistant\n" + sample["chosen"] = f"{sample[chosen_key]}<|im_end|>" + sample["rejected"] = f"{sample[rejected_key]}<|im_end|>" return sample return transform_fn diff --git a/src/axolotl/prompt_strategies/dpo/llama3.py b/src/axolotl/prompt_strategies/dpo/llama3.py index cb394cc228..d10aa223bb 100644 --- a/src/axolotl/prompt_strategies/dpo/llama3.py +++ b/src/axolotl/prompt_strategies/dpo/llama3.py @@ -3,22 +3,42 @@ """ -def argilla( +def default( cfg, **kwargs, ): # pylint: disable=possibly-unused-variable,unused-argument def transform_fn(sample): + # pylint: disable=duplicate-code + if "prompt" in sample.keys(): + prompt_key = "prompt" + elif "input" in sample.keys(): + prompt_key = "input" + elif "question" in sample.keys(): + prompt_key = "question" + else: + prompt_key = "instruction" + + if "chosen" in sample.keys(): + chosen_key = "chosen" + else: + chosen_key = "chosen_response" + + if "rejected" in sample.keys(): + rejected_key = "rejected" + else: + rejected_key = "rejected_response" + if "system" in sample and sample["system"]: sample["prompt"] = ( f"<|start_header_id|>system<|end_header_id|>\n\n{sample['system']}<|eot_id|>" - f"<|start_header_id|>user<|end_header_id|>\n\n{sample['instruction']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + f"<|start_header_id|>user<|end_header_id|>\n\n{sample[prompt_key]}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" ) else: sample[ "prompt" - ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['instruction']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" - sample["chosen"] = f"{sample['chosen_response']}<|eot_id|>" - sample["rejected"] = f"{sample['rejected_response']}<|eot_id|>" + ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample[prompt_key]}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["chosen"] = f"{sample[chosen_key]}<|eot_id|>" + sample["rejected"] = f"{sample[rejected_key]}<|eot_id|>" return sample return transform_fn diff --git a/src/axolotl/prompt_tokenizers.py b/src/axolotl/prompt_tokenizers.py index bd6e3f9dce..c29fd05a4c 100644 --- a/src/axolotl/prompt_tokenizers.py +++ b/src/axolotl/prompt_tokenizers.py @@ -2,7 +2,7 @@ import abc import logging -from typing import Dict, List, Tuple, Union +from typing import Callable, Dict, List, Optional, Tuple, Union from transformers import BatchEncoding, PreTrainedTokenizer @@ -34,6 +34,8 @@ class PromptTokenizingStrategy(abc.ABC): Abstract class for tokenizing strategies """ + filter_rows: Optional[Callable] = None + def __init__( self, prompter: Prompter, diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index f05b259b7c..028b7ea18c 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -163,6 +163,7 @@ class SFTDataset(BaseModel): type: Optional[Union[str, UserDefinedPrompterType]] = None input_transform: Optional[str] = None shards: Optional[int] = None + preprocess_shards: Optional[int] = None conversation: Optional[str] = None # Do not make this too strict or it will break the validator to choose different dataset class chat_template: Optional[ @@ -185,6 +186,8 @@ class SFTDataset(BaseModel): message_field_content: Optional[str] = None message_field_training: Optional[str] = None message_field_training_detail: Optional[str] = None + logprobs_field: Optional[str] = None + temperature: Optional[float] = None roles_to_train: Optional[List[str]] = None train_on_eos: Optional[str] = None roles: Optional[Dict[str, List[str]]] = None @@ -861,6 +864,7 @@ class Config: # INTERNALS - document for now, generally not set externally is_preprocess: Optional[bool] = None + preprocess_iterable: Optional[bool] = None total_num_tokens: Optional[int] = None total_supervised_tokens: Optional[int] = None diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index ba5d0c54d1..79bbb29726 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -3,11 +3,12 @@ import functools import logging from pathlib import Path -from typing import List, Tuple, Union +from typing import List, Optional, Tuple, Union from datasets import ( Dataset, DatasetDict, + IterableDataset, Sequence, Value, concatenate_datasets, @@ -17,7 +18,7 @@ from transformers import PreTrainedTokenizerBase from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH -from axolotl.datasets import TokenizedPromptDataset +from axolotl.datasets import TokenizedPromptDataset, wrap_dataset_for_tokenized_prompt from axolotl.prompt_strategies import load from axolotl.prompt_strategies.bradley_terry import load as bradley_terry_load from axolotl.prompt_tokenizers import ( @@ -59,7 +60,7 @@ @retry_on_request_exceptions(max_retries=3, delay=5) -def prepare_dataset(cfg, tokenizer, processor=None): +def prepare_dataset(cfg, tokenizer, processor=None, preprocess_iterable=None): prompters = [] if not cfg.pretraining_dataset: with zero_first(is_local_main_process()): @@ -70,6 +71,7 @@ def prepare_dataset(cfg, tokenizer, processor=None): DEFAULT_DATASET_PREPARED_PATH, split="train", processor=processor, + preprocess_iterable=preprocess_iterable, ) _, eval_dataset, _ = load_prepare_datasets( tokenizer, @@ -77,6 +79,7 @@ def prepare_dataset(cfg, tokenizer, processor=None): DEFAULT_DATASET_PREPARED_PATH, split="test", processor=processor, + preprocess_iterable=preprocess_iterable, ) else: train_dataset, eval_dataset, prompters = load_prepare_datasets( @@ -84,6 +87,7 @@ def prepare_dataset(cfg, tokenizer, processor=None): cfg, DEFAULT_DATASET_PREPARED_PATH, processor=processor, + preprocess_iterable=preprocess_iterable, ) else: # Load streaming dataset if pretraining_dataset is given @@ -139,6 +143,7 @@ def prepare_dataset(cfg, tokenizer, processor=None): DEFAULT_DATASET_PREPARED_PATH, split="test", processor=processor, + preprocess_iterable=preprocess_iterable, ) if cfg.dataset_exact_deduplication: @@ -170,6 +175,7 @@ def load_tokenized_prepared_datasets( default_dataset_prepared_path, split="train", processor=None, + preprocess_iterable: Optional[bool] = None, ) -> Tuple[DatasetDict, List[Prompter]]: cfg_datasets = cfg.test_datasets if split == "test" else cfg.datasets tokenizer_name = cfg.tokenizer_config @@ -184,10 +190,11 @@ def load_tokenized_prepared_datasets( + "@" + str(cfg.group_by_length) + "@" + + str(cfg.kd_temperature or 1.0) + "|".join( sorted( [ - f"{d.path}:{d.type}:{d.shards}:{d.conversation}{d.split}" + f"{d.path}:{d.type}:{d.shards}:{d.conversation}:{d.split}:{d.temperature or 1.0}" for d in cfg_datasets ] ) @@ -262,13 +269,25 @@ def for_d_in_datasets(dataset_configs): # at the same time for a given dataset for name in dataset.name: yield DictDefault({**dataset, "name": name}) + elif dataset.preprocess_shards and not dataset.shards: + for shard in range(dataset.preprocess_shards): + yield DictDefault( + { + **dataset, + "shards": dataset.preprocess_shards, + "shards_idx": shard, + } + ) else: yield dataset + streaming_ds = False + if preprocess_iterable: + streaming_ds = True # pylint: disable=invalid-name for config_dataset in for_d_in_datasets(cfg_datasets): ds: Union[Dataset, DatasetDict] = load_dataset_w_config( - config_dataset, use_auth_token + config_dataset, use_auth_token, streaming=streaming_ds ) d_base_type = d_prompt_style = None @@ -325,7 +344,21 @@ def for_d_in_datasets(dataset_configs): if cfg.local_rank == 0 and not cfg.skip_prepare_dataset: LOG.info(f"Saving merged prepared dataset to disk... {prepared_ds_path}") - dataset.save_to_disk(str(prepared_ds_path)) + if isinstance(dataset, IterableDataset): + + def gen_from_iter_ds(_ds, _=None): + yield from _ds + + ds_from_iter = Dataset.from_generator( + functools.partial(gen_from_iter_ds, dataset), + features=dataset.features, + num_proc=cfg.dataset_processes, + split=split, + gen_kwargs={"_": list(range(cfg.dataset_processes))}, + ) + ds_from_iter.save_to_disk(str(prepared_ds_path)) + else: + dataset.save_to_disk(str(prepared_ds_path)) if cfg.push_dataset_to_hub: LOG.info( f"Pushing merged prepared dataset to Huggingface hub at {cfg.push_dataset_to_hub} (version {ds_hash})..." @@ -345,6 +378,7 @@ def load_prepare_datasets( default_dataset_prepared_path, split="train", processor=None, + preprocess_iterable: Optional[bool] = False, ) -> Tuple[Dataset, Dataset, List[Prompter]]: dataset, prompters = load_tokenized_prepared_datasets( tokenizer, @@ -352,6 +386,7 @@ def load_prepare_datasets( default_dataset_prepared_path, split=split, processor=processor, + preprocess_iterable=preprocess_iterable, ) if cfg.dataset_shard_num and cfg.dataset_shard_idx is not None: @@ -451,7 +486,7 @@ def get_dataset_wrapper( "user_defined", tokenizer, cfg, config_dataset.type.to_dict() ) dataset_prompter = UnsupportedPrompter() - dataset_wrapper = TokenizedPromptDataset( + dataset_wrapper = wrap_dataset_for_tokenized_prompt( ds_strategy, dataset, **ds_kwargs, @@ -464,7 +499,7 @@ def get_dataset_wrapper( config_dataset.type.split(".", 1)[1], tokenizer, cfg, config_dataset ): dataset_prompter = UnsupportedPrompter() - dataset_wrapper = TokenizedPromptDataset( + dataset_wrapper = wrap_dataset_for_tokenized_prompt( ds_strategy, dataset, **ds_kwargs, @@ -487,7 +522,7 @@ def get_dataset_wrapper( dataset_wrapper = ds_strategy.wrap_dataset(dataset, **ds_kwargs) else: dataset_prompter = UnsupportedPrompter() - dataset_wrapper = TokenizedPromptDataset( + dataset_wrapper = wrap_dataset_for_tokenized_prompt( ds_strategy, dataset, **ds_kwargs, @@ -500,7 +535,7 @@ def get_dataset_wrapper( cfg.train_on_inputs, cfg.sequence_len, ) - ds_wrapper = TokenizedPromptDataset( + ds_wrapper = wrap_dataset_for_tokenized_prompt( ds_strategy, dataset, **ds_kwargs, @@ -514,7 +549,7 @@ def get_dataset_wrapper( cfg.train_on_inputs, cfg.sequence_len, ) - ds_wrapper = TokenizedPromptDataset( + ds_wrapper = wrap_dataset_for_tokenized_prompt( ds_strategy, dataset, **ds_kwargs, @@ -528,7 +563,7 @@ def get_dataset_wrapper( cfg.train_on_inputs, cfg.sequence_len, ) - ds_wrapper = TokenizedPromptDataset( + ds_wrapper = wrap_dataset_for_tokenized_prompt( ds_strategy, dataset, **ds_kwargs, @@ -542,7 +577,7 @@ def get_dataset_wrapper( cfg.train_on_inputs, cfg.sequence_len, ) - ds_wrapper = TokenizedPromptDataset( + ds_wrapper = wrap_dataset_for_tokenized_prompt( ds_strategy, dataset, **ds_kwargs, @@ -556,7 +591,7 @@ def get_dataset_wrapper( cfg.train_on_inputs, cfg.sequence_len, ) - ds_wrapper = TokenizedPromptDataset( + ds_wrapper = wrap_dataset_for_tokenized_prompt( ds_strategy, dataset, **ds_kwargs, @@ -570,7 +605,7 @@ def get_dataset_wrapper( cfg.train_on_inputs, cfg.sequence_len, ) - ds_wrapper = TokenizedPromptDataset( + ds_wrapper = wrap_dataset_for_tokenized_prompt( ds_strategy, dataset, **ds_kwargs, @@ -584,7 +619,7 @@ def get_dataset_wrapper( cfg.train_on_inputs, cfg.sequence_len, ) - ds_wrapper = TokenizedPromptDataset( + ds_wrapper = wrap_dataset_for_tokenized_prompt( ds_strategy, dataset, **ds_kwargs, @@ -598,7 +633,7 @@ def get_dataset_wrapper( cfg.train_on_inputs, cfg.sequence_len, ) - ds_wrapper = TokenizedPromptDataset( + ds_wrapper = wrap_dataset_for_tokenized_prompt( ds_strategy, dataset, **ds_kwargs, diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index e4f31a1843..013d7a895b 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -29,7 +29,9 @@ def get_ds_type(config_dataset: DictDefault): return ds_type -def load_dataset_w_config(config_dataset, auth_token): +def load_dataset_w_config( + config_dataset, auth_token, streaming=False +) -> Union[Dataset, DatasetDict]: # pylint: disable=invalid-name ds: Optional[Union[Dataset, DatasetDict]] = None # pylint: disable=invalid-name ds_from_hub = False @@ -124,7 +126,7 @@ def load_dataset_w_config(config_dataset, auth_token): ds_type, name=config_dataset.name, data_files=config_dataset.data_files, - streaming=False, + streaming=streaming, **load_ds_kwargs, ) else: @@ -157,7 +159,7 @@ def load_dataset_w_config(config_dataset, auth_token): ds = load_dataset( config_dataset.path, name=config_dataset.name, - streaming=False, + streaming=streaming, data_files=config_dataset.data_files, token=auth_token, revision=config_dataset.revision, @@ -176,7 +178,7 @@ def load_dataset_w_config(config_dataset, auth_token): ds_type, name=config_dataset.name, data_files=config_dataset.path, - streaming=False, + streaming=streaming, storage_options=storage_options, trust_remote_code=config_dataset.trust_remote_code, **load_ds_kwargs, @@ -187,7 +189,7 @@ def load_dataset_w_config(config_dataset, auth_token): ds_type, name=config_dataset.name, data_files=config_dataset.path, - streaming=False, + streaming=streaming, storage_options=storage_options, trust_remote_code=config_dataset.trust_remote_code, **load_ds_kwargs, @@ -217,7 +219,7 @@ def load_dataset_w_config(config_dataset, auth_token): "json", name=config_dataset.name, data_files=fp, - streaming=False, + streaming=streaming, **load_ds_kwargs, ) if not ds: diff --git a/src/axolotl/utils/tokenization.py b/src/axolotl/utils/tokenization.py index 139d501109..e0b21a9f02 100644 --- a/src/axolotl/utils/tokenization.py +++ b/src/axolotl/utils/tokenization.py @@ -26,6 +26,7 @@ def check_example_labels(example, tokenizer, text_only=False): # Get the input_ids, labels, and attention_mask from the dataset input_ids = example["input_ids"] labels = example["labels"] + target_mask = example.pop("target_mask", None) # You can compare the input_ids and labels element-wise # Remember to ignore positions with IGNORE_TOKEN_ID (if you use it) or attention_mask equal to 0 @@ -42,6 +43,13 @@ def check_example_labels(example, tokenizer, text_only=False): delimiter = "" if text_only else " " LOG.info(delimiter.join(colored_tokens)) LOG.info("\n\n\n") + target_labels_count = sum(label_id != -100 for label_id in labels) + total_len = len(input_ids) + LOG.info(f"Total input len: {total_len}") + LOG.info(f"Count of labels: {target_labels_count}") + if target_mask: + target_mask_positions = sum(m[0] for m in target_mask) + LOG.info(f"Number of positions in target_mask: {target_mask_positions}") return " ".join(colored_tokens) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index caa74fccc8..32e9bdfb46 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -11,7 +11,7 @@ import torch import torch.cuda from accelerate.logging import get_logger -from datasets import disable_caching, enable_caching +from datasets import IterableDataset, disable_caching, enable_caching from torch.utils.data import DataLoader, RandomSampler from transformers.utils import is_torch_bf16_gpu_available @@ -95,9 +95,41 @@ def disable_datasets_caching(): def add_position_ids(sample): - sample_len = len(sample["input_ids"]) - sample["position_ids"] = torch.arange(len(sample["input_ids"])) - sample["length"] = sample_len + """ + Handle both single-example and batched data. + - single example: sample['input_ids'] is a list[int] + - batched data: sample['input_ids'] is a list[list[int]] + """ + # Return sample unchanged if "input_ids" is not present, or is empty + if "input_ids" not in sample or not sample["input_ids"]: + return sample + + input_ids = sample["input_ids"] + + # If first element is an int, it’s a single example + # If first element is a list, it’s a batch + if isinstance(input_ids[0], int): + # ---- SINGLE EXAMPLE ---- + seq_len = len(input_ids) + # Position IDs for a single example + # As a list + sample["position_ids"] = list(range(seq_len)) + sample["length"] = seq_len + + else: + # ---- BATCHED EXAMPLES ---- + # input_ids is a list of lists + position_ids_batch = [] + lengths_batch = [] + for seq in input_ids: + seq_len = len(seq) + position_ids_batch.append(list(range(seq_len))) + lengths_batch.append(seq_len) + + # Now store them back + sample["position_ids"] = position_ids_batch + sample["length"] = lengths_batch + return sample @@ -172,10 +204,31 @@ def add_length(sample): def drop_long_seq(sample, sequence_len=2048, min_sequence_len=2): - return ( - len(sample["input_ids"]) <= sequence_len - and len(sample["input_ids"]) >= min_sequence_len - ) + """ + Drop samples whose sequence length is either too long (> sequence_len) + or too short (< min_sequence_len). + + Works for both single-example (list[int]) or batched (list[list[int]]). + """ + input_ids = sample["input_ids"] + + # Edge case: if input_ids is empty + if not input_ids: + # Decide if you want to drop or keep empty. Let's drop. + return False + + # Check if single example or batched by looking at the first element + if isinstance(input_ids[0], int): + # Single example (input_ids is a list of int) + length = len(input_ids) + return min_sequence_len <= length <= sequence_len + + # Batched (input_ids is a list of lists) + results = [] + for seq in input_ids: + length = len(seq) + results.append(min_sequence_len <= length <= sequence_len) + return results def process_datasets_for_packing(cfg, train_dataset, eval_dataset): @@ -185,10 +238,13 @@ def process_datasets_for_packing(cfg, train_dataset, eval_dataset): min_sequence_len=cfg.min_sample_len or 2, ) - min_input_len = np.min(get_dataset_lengths(train_dataset)) - LOG.debug(f"min_input_len: {min_input_len}", main_process_only=True) - max_input_len = np.max(get_dataset_lengths(train_dataset)) - LOG.debug(f"max_input_len: {max_input_len}", main_process_only=True) + try: + min_input_len = np.min(get_dataset_lengths(train_dataset)) + LOG.debug(f"min_input_len: {min_input_len}", main_process_only=True) + max_input_len = np.max(get_dataset_lengths(train_dataset)) + LOG.debug(f"max_input_len: {max_input_len}", main_process_only=True) + except AttributeError: + pass if cfg.model_config_type == "mamba": LOG.info("dropping attention_mask column") @@ -203,59 +259,105 @@ def process_datasets_for_packing(cfg, train_dataset, eval_dataset): if eval_dataset and "token_type_ids" in eval_dataset.column_names: eval_dataset = eval_dataset.remove_columns("token_type_ids") - prior_len = len(train_dataset) + filter_map_kwargs = {} + if not isinstance(train_dataset, IterableDataset): + filter_map_kwargs["num_proc"] = cfg.dataset_processes + filter_map_kwargs["load_from_cache_file"] = not cfg.is_preprocess + + try: + prior_len = len(train_dataset) + except TypeError: + # handle iterable datasets case + prior_len = None + drop_long_kwargs = {} + if filter_map_kwargs: + drop_long_kwargs["desc"] = "Dropping Long Sequences" train_dataset = train_dataset.filter( drop_long, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Dropping Long Sequences", + batched=True, + **filter_map_kwargs, + **drop_long_kwargs, ) - dropped = prior_len - len(train_dataset) - if dropped: - LOG.warning(f"Dropped {dropped} long samples from train dataset") + if prior_len: + dropped = prior_len - len(train_dataset) + if dropped: + LOG.warning(f"Dropped {dropped} long samples from train dataset") if eval_dataset: - prior_len = len(eval_dataset) + try: + prior_len = len(eval_dataset) + except TypeError: + # handle iterable datasets case + prior_len = None eval_dataset = eval_dataset.filter( drop_long, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Dropping Long Sequences", + **filter_map_kwargs, + **drop_long_kwargs, ) - dropped = prior_len - len(eval_dataset) - if dropped: - LOG.warning(f"Dropped {dropped} long samples from eval dataset") + if prior_len: + dropped = prior_len - len(eval_dataset) + if dropped: + LOG.warning(f"Dropped {dropped} long samples from eval dataset") - # drop samples with where the number of elements with labels not equal to -100 is zero def drop_no_trainable_tokens(sample): - return np.sum(np.array(sample["labels"]) != -100) > 0 + """ + Drop samples if all labels are -100 (i.e., zero trainable tokens). + Works for both single-example or batched input. + """ + labels = sample["labels"] + if not labels: + return True + + # Check if single example or batch + # If first element is an int, we assume a single example + # If it's a list, we assume we're dealing with a batch + if isinstance(labels[0], int): + # Single example: return a single bool + return np.any(labels != -100) + + # Batched: 'labels' is a list of lists + # Return a list of booleans, one per sub-list + results = [np.any(row_labels != -100) for row_labels in labels] + return results - prior_len = len(train_dataset) + try: + prior_len = len(train_dataset) + except TypeError: + # handle iterable datasets case + prior_len = None + drop_long_kwargs = {} + if filter_map_kwargs: + drop_long_kwargs["desc"] = "Drop Samples with Zero Trainable Tokens" train_dataset = train_dataset.filter( drop_no_trainable_tokens, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Drop Samples with Zero Trainable Tokens", + batched=True, + **filter_map_kwargs, + **drop_long_kwargs, ) - dropped = prior_len - len(train_dataset) - if dropped: - LOG.warning( - f"Dropped {dropped} samples with no trainable tokens from train dataset" - ) + if prior_len: + dropped = prior_len - len(train_dataset) + if dropped: + LOG.warning( + f"Dropped {dropped} samples with no trainable tokens from train dataset" + ) if eval_dataset: - prior_len = len(eval_dataset) + try: + prior_len = len(eval_dataset) + except TypeError: + # handle iterable datasets case + prior_len = None eval_dataset = eval_dataset.filter( drop_no_trainable_tokens, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Drop Samples with Zero Trainable Tokens", + **filter_map_kwargs, + **drop_long_kwargs, ) - dropped = prior_len - len(eval_dataset) - if dropped: - LOG.warning( - f"Dropped {dropped} samples with no trainable tokens from eval dataset" - ) + if prior_len: + dropped = prior_len - len(eval_dataset) + if dropped: + LOG.warning( + f"Dropped {dropped} samples with no trainable tokens from eval dataset" + ) if cfg.group_by_length: train_dataset = train_dataset.map( @@ -291,19 +393,21 @@ def drop_no_trainable_tokens(sample): desc="Add position_id column (PoSE)", ) elif cfg.sample_packing: + drop_long_kwargs = {} + if filter_map_kwargs: + drop_long_kwargs["desc"] = "Add position_id column (Sample Packing)" train_dataset = train_dataset.map( add_position_ids, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Add position_id column (Sample Packing)", + batched=True, + **filter_map_kwargs, + **drop_long_kwargs, ) if cfg.eval_sample_packing is not False: if eval_dataset: eval_dataset = eval_dataset.map( add_position_ids, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Add position_id column (Sample Packing)", + **filter_map_kwargs, + **drop_long_kwargs, ) return train_dataset, eval_dataset @@ -337,7 +441,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): and not cfg.reward_model ): total_num_tokens = np.sum( - train_dataset.data.column("input_ids") + train_dataset.select_columns("input_ids") .to_pandas() .apply(lambda x: len(x)) # pylint: disable=unnecessary-lambda .values diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py new file mode 100644 index 0000000000..e885d3b48c --- /dev/null +++ b/tests/e2e/integrations/test_kd.py @@ -0,0 +1,121 @@ +""" +e2e tests for kd trainer support in Axolotl +""" +from pathlib import Path + +import pytest +from e2e.utils import check_tensorboard, require_torch_2_5_1 + +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, prepare_plugins +from axolotl.utils.dict import DictDefault + + +@pytest.fixture(name="kd_min_cfg") +def min_cfg(temp_dir): + return { + "base_model": "osllmai-community/Llama-3.2-1B", + "tokenizer_config": "axolotl-ai-co/Llama-3.3-70B-Instruct-tokenizer", + "plugins": [ + "axolotl.integrations.kd.KDPlugin", + "axolotl.integrations.liger.LigerPlugin", + ], + "liger_rms_norm": True, + "liger_glu_activation": True, + "torch_compile": True, + "chat_template": "llama3", + "kd_trainer": True, + "kd_ce_alpha": 0.1, + "kd_alpha": 0.9, + "kd_temperature": 1.0, + "dataloader_prefetch_factor": 8, + "dataloader_num_workers": 4, + "dataloader_pin_memory": True, + "datasets": [ + { + "path": "axolotl-ai-co/evolkit-logprobs-pipeline-75k-v2-sample", + "type": "axolotl.integrations.kd.chat_template", + "field_messages": "messages_combined", + "split": "train", + "logprobs_field": "llm_text_generation_vllm_logprobs", + "temperature": 1.0, + "preprocess_shards": 2, + }, + ], + "val_set_size": 0.0, + "sequence_len": 2048, + "sample_packing": True, + "pad_to_sequence_len": True, + "gradient_accumulation_steps": 2, + "micro_batch_size": 1, + "num_epochs": 1, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "learning_rate": 0.00001, + "bf16": "auto", + "gradient_checkpointing": True, + "flash_attention": True, + "special_tokens": { + "pad_token": "<|end_of_text|>", + "eos_token": "<|eot_id|>", + }, + "max_steps": 5, + "output_dir": temp_dir, + "save_safetensors": True, + "use_tensorboard": True, + } + + +class TestKnowledgeDistillation: + """ + Test case for Knowledge Distillation + """ + + # While this will run on torch 2.4.x without torch_compile enabled + # the VRAM requirement is higher than what is available in CI + @require_torch_2_5_1 + def test_llama_kd(self, temp_dir, kd_min_cfg): + cfg = DictDefault(kd_min_cfg) + # pylint: disable=duplicate-code + prepare_plugins(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() + check_tensorboard( + temp_dir + "/runs", "train/loss", 1.0, "Train Loss is too high" + ) + + @pytest.mark.parametrize( + "load_in_8bit", + [True, False], + ) + def test_llama_lora_kd(self, temp_dir, kd_min_cfg, load_in_8bit): + cfg = DictDefault( + { + "load_in_8bit": load_in_8bit, + "torch_compile": False, + "adapter": "lora", + "peft_use_dora": True, + "lora_target_linear": True, + "lora_r": 16, + "lora_alpha": 32, + "lora_dropout": 0.0, + } + | kd_min_cfg + ) + # pylint: disable=duplicate-code + prepare_plugins(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.safetensors").exists() + check_tensorboard( + temp_dir + "/runs", "train/loss", 1.0, "Train Loss is too high" + ) diff --git a/tests/e2e/integrations/test_liger.py b/tests/e2e/integrations/test_liger.py index cf673cab26..226ed46f80 100644 --- a/tests/e2e/integrations/test_liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -55,6 +55,7 @@ def test_llama_wo_flce(self, temp_dir): "max_steps": 5, } ) + # pylint: disable=duplicate-code prepare_plugins(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() @@ -100,6 +101,7 @@ def test_llama_w_flce(self, temp_dir): "max_steps": 5, } ) + # pylint: disable=duplicate-code prepare_plugins(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() From a20f17689b0f78ed1e15e5ce2ec1983dc2bb3fe9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 31 Jan 2025 20:19:20 -0500 Subject: [PATCH 0400/1405] set MODAL_IMAGE_BUILDER_VERSION=2024.10 to 2024.10 to test latest builder (#2302) * set MODAL_IMAGE_BUILDER_VERSION=2024.10 to 2024.10 to test latest builder * chore: lint * remove fastapi and pydantic extras --- .github/workflows/tests.yml | 2 ++ cicd/tests.py | 16 ++++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b530908fde..709845fc67 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -228,6 +228,7 @@ jobs: echo "AXOLOTL_ARGS=${{ matrix.axolotl_args}}" >> $GITHUB_ENV echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV + echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | @@ -268,6 +269,7 @@ jobs: echo "AXOLOTL_ARGS=${{ matrix.axolotl_args}}" >> $GITHUB_ENV echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV + echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | diff --git a/cicd/tests.py b/cicd/tests.py index 6fe701632d..b934d5316a 100644 --- a/cicd/tests.py +++ b/cicd/tests.py @@ -38,16 +38,12 @@ with open(pathlib.Path(temp_dir) / "Dockerfile", "w", encoding="utf-8") as f: f.write(dockerfile_contents) -cicd_image = ( - Image.from_dockerfile( - pathlib.Path(temp_dir) / "Dockerfile", - context_mount=None, - force_build=True, - gpu="A10G", - ) - .env(df_args) - .pip_install("fastapi==0.110.0", "pydantic==2.6.3") -) +cicd_image = Image.from_dockerfile( + pathlib.Path(temp_dir) / "Dockerfile", + context_mount=None, + force_build=True, + gpu="A10G", +).env(df_args) app = App("Axolotl CI/CD", secrets=[]) From 80e1468b8d444189ea007ccd0dec297458b7d347 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 1 Feb 2025 21:10:34 -0500 Subject: [PATCH 0401/1405] better handling of multipack dataset length (#2296) --- src/axolotl/utils/samplers/multipack.py | 43 +++++++------------------ 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index db14a6819e..6119dff30c 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -4,7 +4,6 @@ """ import logging import math -import os from typing import Any, Iterable, List, Union import numba @@ -117,6 +116,7 @@ def __init__( lengths: np.ndarray, packing_efficiency_estimate: float = 1.0, drop_last: bool = False, + num_count_samples: int = 16, **kwargs, ): super().__init__(sampler, batch_size, drop_last) @@ -133,6 +133,9 @@ def __init__( self.eff_total_used = 0 self.eff_total_slots = 0 + # The number of times to calculate the batches to determine the minimum packed dataset length for the local rank + self.num_count_samples = num_count_samples + # the minimum packed dataset length across all ranks determined by a gather/broadcast self.len_across_ranks = None def set_epoch(self, epoch: int): @@ -169,6 +172,9 @@ def generate_batches(self, set_stats=False): def __iter__(self): batches = self.generate_batches(set_stats=True) + if self.len_across_ranks: + # make sure the batches we iterate over is truncated to the same min length across all ranks + batches = batches[: self.len_across_ranks] return iter(batches) def num_batches(self): @@ -195,42 +201,15 @@ def calc_sample_packing_eff_est(estimates: List[float]): def gather_len_batches(self, num): def calc_min_len(estimates: list[(int, float)]): LOG.info(f"gather_len_batches: {repr(estimates)}") - return math.floor(0.998 * min(estimates)) + return math.floor(min(estimates)) min_len_batches = reduce_and_broadcast(lambda: num, calc_min_len) return min_len_batches def __len__(self): if not self.len_across_ranks: - len_batches = self.num_batches() + len_batches = min( + [self.num_batches() for _ in range(self.num_count_samples)] + ) self.len_across_ranks = self.gather_len_batches(len_batches) return self.len_across_ranks - - def _len_est(self): - efficiency = ( - self.packing_efficiency_estimate - if self.packing_efficiency_estimate - else self.gather_efficiency() - ) - world_size = int(os.getenv("WORLD_SIZE", "1")) - lengths_sum = np.sum(self.lengths) - lengths_sum_per_device = lengths_sum // world_size - LOG.info( - f"packing_efficiency_estimate: {efficiency} " - f"total_num_tokens per device: {lengths_sum_per_device}" - ) - - # shave off 1% + 1 for dealing with variance in packing from random sampler to sampler - return max( - 0, - ( - world_size - * math.floor( - 0.99 - * lengths_sum_per_device - / efficiency - // (self.batch_max_len * self.batch_size) - ) - - 1 - ), - ) From 158330ab60086d9948472dc8e8530a330511a4bf Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 1 Feb 2025 21:11:18 -0500 Subject: [PATCH 0402/1405] [feature] sweeps (#2171) --- src/axolotl/cli/main.py | 174 +++++++++++++++++++++++++++++------ tests/cli/test_cli_sweeps.py | 68 ++++++++++++++ 2 files changed, 215 insertions(+), 27 deletions(-) create mode 100644 tests/cli/test_cli_sweeps.py diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index e8551511e7..d7aa1f6a72 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -1,10 +1,17 @@ """Click CLI definitions for various axolotl commands.""" # pylint: disable=redefined-outer-name +import logging +import random import subprocess # nosec B404 +import tempfile +from copy import deepcopy +from itertools import product +from pathlib import Path from typing import Optional import click +import yaml import axolotl from axolotl.cli.args import EvaluateCliArgs, PreprocessCliArgs, TrainerCliArgs @@ -20,6 +27,76 @@ from axolotl.utils.config.models.input.v0_4_1 import AxolotlInputConfig +def generate_sweep_configs(base_config, sweeps_config): + """ + Recursively generates all possible configurations by applying sweeps to the base config. + + Args: + base_config (dict): The original configuration dictionary + sweeps_config (dict): Dictionary where keys are parameters and values are either: + - lists of values to sweep independently + - or for paired values, a list of dicts under the '_' key + + Returns: + list: List of all possible configuration dictionaries + + Example: + sweeps_config = { + 'learning_rate': [0.1, 0.01], + '_': [ + {'load_in_8bit': True, 'adapter': 'lora'}, + {'load_in_4bit': True, 'adapter': 'qlora'} + ] + } + """ + # Separate paired values from regular sweeps + paired_values = sweeps_config.get("_", []) + regular_sweeps = {k: v for k, v in sweeps_config.items() if k != "_"} + + # Process regular sweeps + param_names = list(regular_sweeps.keys()) + param_values = list(regular_sweeps.values()) + + # Generate combinations for regular sweeps + regular_combinations = list(product(*param_values)) if param_values else [()] + + # Combine regular sweeps with paired values + all_combinations = [] + for reg_combo in regular_combinations: + if paired_values: + for paired_set in paired_values: + new_config = {} + # new_config = deepcopy(base_config) + # Combine regular parameters with paired parameters + full_combo = {**dict(zip(param_names, reg_combo)), **paired_set} + for param_name, param_value in full_combo.items(): + new_config[param_name] = param_value + print(new_config) + all_combinations.append(new_config) + else: + # If no paired values, just use regular combinations + # new_config = deepcopy(base_config) + new_config = {} + for param_name, param_value in zip(param_names, reg_combo): + new_config[param_name] = param_value + print(new_config) + all_combinations.append(new_config) + + # randomize the order of trials + random.seed(42) + random.shuffle(all_combinations) + + # Generate a new config for each combination + result_configs = [] + for combination in all_combinations: + new_config = deepcopy(base_config) + for param_name, param_value in combination.items(): + new_config[param_name] = param_value + result_configs.append(new_config) + + return result_configs + + @click.group() @click.version_option(version=axolotl.__version__, prog_name="axolotl") def cli(): @@ -60,10 +137,21 @@ def preprocess(config: str, cloud: Optional[str] = None, **kwargs) -> None: help="Use accelerate launch for multi-GPU training", ) @click.option("--cloud", default=None, type=click.Path(exists=True, path_type=str)) +@click.option( + "--sweep", + type=click.Path(exists=True, path_type=str), + help="YAML config for sweeping hyperparameters", +) @add_options_from_dataclass(TrainerCliArgs) @add_options_from_config(AxolotlInputConfig) @filter_none_kwargs -def train(config: str, accelerate: bool, cloud: Optional[str] = None, **kwargs) -> None: +def train( + config: str, + accelerate: bool, + cloud: Optional[str] = None, + sweep: Optional[str] = None, + **kwargs, +) -> None: """ Train or fine-tune a model. @@ -71,6 +159,7 @@ def train(config: str, accelerate: bool, cloud: Optional[str] = None, **kwargs) config: Path to `axolotl` config YAML file. accelerate: Whether to use `accelerate` launcher. cloud: Path to a cloud accelerator configuration file + sweep: Path to YAML config for sweeping hyperparameters. kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` config options. """ @@ -80,35 +169,66 @@ def train(config: str, accelerate: bool, cloud: Optional[str] = None, **kwargs) if "use_ray" in kwargs and kwargs["use_ray"]: accelerate = False + if sweep: + # load the sweep configuration yaml file + with open(sweep, "r", encoding="utf-8") as fin: + sweep_config: dict[str, list] = yaml.safe_load(fin) + with open(config, "r", encoding="utf-8") as fin: + base_config: dict[str, list] = yaml.safe_load(fin) + + # generate all possible configurations + permutations = generate_sweep_configs(base_config, sweep_config) + + def iter_configs(): + for perm in permutations: + # open temp directory for temporary configurations + with tempfile.TemporaryDirectory() as temp_dir: + with open( + Path(temp_dir) / "config.yaml", "w", encoding="utf-8" + ) as fout: + yaml.dump(perm, fout) + yield str(Path(temp_dir) / "config.yaml") - if accelerate: - if cloud: - do_cli_train(cloud_config=cloud, config=config, accelerate=True) - else: - accelerate_args = [] - if "main_process_port" in kwargs: - main_process_port = kwargs.pop("main_process_port", None) - accelerate_args.append("--main_process_port") - accelerate_args.append(str(main_process_port)) - if "num_processes" in kwargs: - num_processes = kwargs.pop("num_processes", None) - accelerate_args.append("--num-processes") - accelerate_args.append(str(num_processes)) - - base_cmd = ["accelerate", "launch"] - base_cmd.extend(accelerate_args) - base_cmd.extend(["-m", "axolotl.cli.train"]) - if config: - base_cmd.append(config) - cmd = build_command(base_cmd, kwargs) - subprocess.run(cmd, check=True) # nosec B603 else: - if cloud: - do_cli_train(cloud_config=cloud, config=config, accelerate=False) - else: - from axolotl.cli.train import do_cli - do_cli(config=config, **kwargs) + def iter_configs(): + yield config + + for cfg_file in iter_configs(): + # handle errors from subprocess so we can continue rest of sweeps + try: + if accelerate: + if cloud: + do_cli_train(cloud_config=cloud, config=config, accelerate=True) + else: + accelerate_args = [] + if "main_process_port" in kwargs: + main_process_port = kwargs.pop("main_process_port", None) + accelerate_args.append("--main_process_port") + accelerate_args.append(str(main_process_port)) + if "num_processes" in kwargs: + num_processes = kwargs.pop("num_processes", None) + accelerate_args.append("--num-processes") + accelerate_args.append(str(num_processes)) + + base_cmd = ["accelerate", "launch"] + base_cmd.extend(accelerate_args) + base_cmd.extend(["-m", "axolotl.cli.train"]) + if cfg_file: + base_cmd.append(cfg_file) + cmd = build_command(base_cmd, kwargs) + subprocess.run(cmd, check=True) # nosec B603 + else: + if cloud: + do_cli_train(cloud_config=cloud, config=config, accelerate=False) + else: + from axolotl.cli.train import do_cli + + do_cli(config=cfg_file, **kwargs) + except subprocess.CalledProcessError as exc: + logging.error(f"Failed to train/fine-tune config '{cfg_file}': {exc}") + if not sweep: + raise exc @cli.command() diff --git a/tests/cli/test_cli_sweeps.py b/tests/cli/test_cli_sweeps.py new file mode 100644 index 0000000000..61c886e80a --- /dev/null +++ b/tests/cli/test_cli_sweeps.py @@ -0,0 +1,68 @@ +""" +unit tests for generating sweep configurations +""" +from axolotl.cli.main import generate_sweep_configs + + +def test_generate_sweep_configs_no_pairs(): + base_config = { + "learning_rate": 0.1, + "micro_batch_size": 1, + "sample_packing": True, + } + + sweeps_config = {"micro_batch_size": [1, 2, 4], "weight_decay": [0.0, 0.1]} + + generate_sweep_configs(base_config, sweeps_config) + + assert len(generate_sweep_configs(base_config, sweeps_config)) == 6 + + cfg_1 = { + "learning_rate": 0.1, + "micro_batch_size": 2, + "weight_decay": 0.0, + "sample_packing": True, + } + + assert any( + cfg_1 == cfg for cfg in generate_sweep_configs(base_config, sweeps_config) + ) + + +def test_generate_sweep_configs_with_pairs(): + base_config = { + "learning_rate": 0.1, + "micro_batch_size": 1, + "sample_packing": True, + } + + sweeps_config = { + "_": [ + { + "micro_batch_size": 1, + "gradient_accumulation_steps": 8, + }, + { + "micro_batch_size": 2, + "gradient_accumulation_steps": 4, + }, + { + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + }, + { + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + }, + ], + "weight_decay": [0.0, 0.1], + } + + generate_sweep_configs(base_config, sweeps_config) + + assert len(generate_sweep_configs(base_config, sweeps_config)) == 8 + + assert all( + cfg["gradient_accumulation_steps"] * cfg["micro_batch_size"] == 8 + for cfg in generate_sweep_configs(base_config, sweeps_config) + ) From a620d481e2f0d293bb8b3d46fc694cc51079c81f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 4 Feb 2025 21:43:35 +0700 Subject: [PATCH 0403/1405] fix: drop long seq even if not sample packing (#2211) * fix: drop long seq even if not sample packing * fix: logging import * fix: cfg passed being none * fix: try to fix logging * fix: refactor call to not use accelerate log * fix: try to fix circular import issue * fix: don't drop when skip prepare * chore: remove duplicate line * fix: update warning to mention that sequences will be trimmed * fix: do not drop seq if input_ids don't exist * fix: increase RM unittest sequence length to reduce trim warnings * fix: solve conflicts * fix: default min_seq_len in case of None --- .../bradley_terry/chat_template.py | 4 +- src/axolotl/utils/data/sft.py | 10 ++- src/axolotl/utils/data/utils.py | 60 +++++++++++++++++- src/axolotl/utils/samplers/utils.py | 1 - src/axolotl/utils/trainer.py | 62 +++---------------- tests/e2e/test_reward_model_smollm2.py | 2 +- 6 files changed, 76 insertions(+), 63 deletions(-) diff --git a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py index c6b0fe2cf5..61191a47f1 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py +++ b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py @@ -47,7 +47,7 @@ def _tokenize_single_prompt(self, prompt): if len(chosen_tokenized["input_ids"]) > max_length: LOG.warning( - f"Chosen sequence exceeds max sequence length: {len(chosen_tokenized['input_ids'])}", + f"To-be-trimmed chosen sequence exceeds max sequence length: {len(chosen_tokenized['input_ids'])}", ) chosen_tokenized["input_ids"] = chosen_tokenized["input_ids"][:max_length] @@ -70,7 +70,7 @@ def _tokenize_single_prompt(self, prompt): if len(rejected_tokenized["input_ids"]) > max_length: LOG.warning( - f"Rejected sequence exceeds max sequence length: {len(rejected_tokenized['input_ids'])}", + f"To-be-trimmed rejected sequence exceeds max sequence length: {len(rejected_tokenized['input_ids'])}", ) rejected_tokenized["input_ids"] = rejected_tokenized["input_ids"][ diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 79bbb29726..722ad2de2d 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -46,6 +46,7 @@ from axolotl.utils.data.shared import load_dataset_w_config from axolotl.utils.data.utils import ( deduplicate_and_log_datasets, + drop_long_seq_in_dataset, md5, retry_on_request_exceptions, ) @@ -56,7 +57,7 @@ process_datasets_for_packing, ) -LOG = logging.getLogger("axolotl") +LOG = logging.getLogger(__name__) @retry_on_request_exceptions(max_retries=3, delay=5) @@ -339,8 +340,11 @@ def for_d_in_datasets(dataset_configs): else: LOG.debug("NOT shuffling merged datasets") - if cfg.sample_packing and not cfg.skip_prepare_dataset: - dataset, _ = process_datasets_for_packing(cfg, dataset, None) + if not cfg.skip_prepare_dataset: + dataset = drop_long_seq_in_dataset(dataset, cfg) + + if cfg.sample_packing: + dataset, _ = process_datasets_for_packing(cfg, dataset, None) if cfg.local_rank == 0 and not cfg.skip_prepare_dataset: LOG.info(f"Saving merged prepared dataset to disk... {prepared_ds_path}") diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index 657cbb77c3..a6abd8d735 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -1,4 +1,5 @@ """data handling helpers""" + import functools import hashlib import logging @@ -6,10 +7,15 @@ from enum import Enum import huggingface_hub +import numpy as np import requests -from datasets import Dataset +from datasets import Dataset, IterableDataset + +from axolotl.utils.dict import DictDefault +from axolotl.utils.samplers.utils import get_dataset_lengths +from axolotl.utils.trainer import drop_long_seq -LOG = logging.getLogger("axolotl") +LOG = logging.getLogger(__name__) class RetryStrategy(Enum): @@ -150,3 +156,53 @@ def deduplicate_and_log_datasets( ) return train_dataset, eval_dataset, dataset + + +def drop_long_seq_in_dataset(dataset: Dataset, cfg: DictDefault): + if "input_ids" not in dataset.column_names: + LOG.warning( + "Dataset does not contain 'input_ids' column. Skip drop long seq. This is expected for RewardModeling." + ) + return dataset + + drop_long = functools.partial( + drop_long_seq, + sequence_len=cfg.sequence_len, + min_sequence_len=cfg.min_sample_len, + ) + + try: + min_input_len = np.min(get_dataset_lengths(dataset)) + LOG.debug(f"min_input_len: {min_input_len}") + max_input_len = np.max(get_dataset_lengths(dataset)) + LOG.debug(f"max_input_len: {max_input_len}") + except AttributeError: + pass + + try: + prior_len = len(dataset) + except TypeError: + # handle iterable datasets case + prior_len = None + + filter_map_kwargs = {} + if not isinstance(dataset, IterableDataset): + filter_map_kwargs["num_proc"] = cfg.dataset_processes + filter_map_kwargs["load_from_cache_file"] = not cfg.is_preprocess + + drop_long_kwargs = {} + if filter_map_kwargs: + drop_long_kwargs["desc"] = "Dropping Long Sequences" + + dataset = dataset.filter( + drop_long, + batched=True, + **filter_map_kwargs, + **drop_long_kwargs, + ) + if prior_len: + dropped = prior_len - len(dataset) + if dropped: + LOG.warning(f"Dropped {dropped} long samples from dataset") + + return dataset diff --git a/src/axolotl/utils/samplers/utils.py b/src/axolotl/utils/samplers/utils.py index e4af4e5f35..4e41c9b44d 100755 --- a/src/axolotl/utils/samplers/utils.py +++ b/src/axolotl/utils/samplers/utils.py @@ -13,5 +13,4 @@ def get_dataset_lengths(dataset): else: input_ids = dataset.data.column("input_ids") lengths = np.vectorize(len)(np.array(input_ids, dtype=object)) - return lengths return lengths diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 32e9bdfb46..61f03e7ad1 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -1,4 +1,5 @@ """Module containing the Trainer class and related functions""" + import json import math import os @@ -210,6 +211,8 @@ def drop_long_seq(sample, sequence_len=2048, min_sequence_len=2): Works for both single-example (list[int]) or batched (list[list[int]]). """ + min_sequence_len = min_sequence_len or 2 + input_ids = sample["input_ids"] # Edge case: if input_ids is empty @@ -232,20 +235,6 @@ def drop_long_seq(sample, sequence_len=2048, min_sequence_len=2): def process_datasets_for_packing(cfg, train_dataset, eval_dataset): - drop_long = partial( - drop_long_seq, - sequence_len=cfg.sequence_len, - min_sequence_len=cfg.min_sample_len or 2, - ) - - try: - min_input_len = np.min(get_dataset_lengths(train_dataset)) - LOG.debug(f"min_input_len: {min_input_len}", main_process_only=True) - max_input_len = np.max(get_dataset_lengths(train_dataset)) - LOG.debug(f"max_input_len: {max_input_len}", main_process_only=True) - except AttributeError: - pass - if cfg.model_config_type == "mamba": LOG.info("dropping attention_mask column") train_dataset = train_dataset.remove_columns("attention_mask") @@ -259,46 +248,6 @@ def process_datasets_for_packing(cfg, train_dataset, eval_dataset): if eval_dataset and "token_type_ids" in eval_dataset.column_names: eval_dataset = eval_dataset.remove_columns("token_type_ids") - filter_map_kwargs = {} - if not isinstance(train_dataset, IterableDataset): - filter_map_kwargs["num_proc"] = cfg.dataset_processes - filter_map_kwargs["load_from_cache_file"] = not cfg.is_preprocess - - try: - prior_len = len(train_dataset) - except TypeError: - # handle iterable datasets case - prior_len = None - drop_long_kwargs = {} - if filter_map_kwargs: - drop_long_kwargs["desc"] = "Dropping Long Sequences" - train_dataset = train_dataset.filter( - drop_long, - batched=True, - **filter_map_kwargs, - **drop_long_kwargs, - ) - if prior_len: - dropped = prior_len - len(train_dataset) - if dropped: - LOG.warning(f"Dropped {dropped} long samples from train dataset") - - if eval_dataset: - try: - prior_len = len(eval_dataset) - except TypeError: - # handle iterable datasets case - prior_len = None - eval_dataset = eval_dataset.filter( - drop_long, - **filter_map_kwargs, - **drop_long_kwargs, - ) - if prior_len: - dropped = prior_len - len(eval_dataset) - if dropped: - LOG.warning(f"Dropped {dropped} long samples from eval dataset") - def drop_no_trainable_tokens(sample): """ Drop samples if all labels are -100 (i.e., zero trainable tokens). @@ -325,6 +274,11 @@ def drop_no_trainable_tokens(sample): except TypeError: # handle iterable datasets case prior_len = None + filter_map_kwargs = {} + if not isinstance(train_dataset, IterableDataset): + filter_map_kwargs["num_proc"] = cfg.dataset_processes + filter_map_kwargs["load_from_cache_file"] = not cfg.is_preprocess + drop_long_kwargs = {} if filter_map_kwargs: drop_long_kwargs["desc"] = "Drop Samples with Zero Trainable Tokens" diff --git a/tests/e2e/test_reward_model_smollm2.py b/tests/e2e/test_reward_model_smollm2.py index 7360a99dc8..d123e60612 100644 --- a/tests/e2e/test_reward_model_smollm2.py +++ b/tests/e2e/test_reward_model_smollm2.py @@ -33,7 +33,7 @@ def test_rm_lora(self, temp_dir): "num_labels": 1, "chat_template": "alpaca", "reward_model": True, - "sequence_len": 1024, + "sequence_len": 2048, "pad_to_sequence_len": True, "adapter": "lora", "lora_r": 8, From a971eb4ce6166ccb2b73f6a89bb4462f65003178 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 5 Feb 2025 09:24:02 -0500 Subject: [PATCH 0404/1405] Torch 2.6 support for base docker image (#2312) --- .github/workflows/base.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 0f4b3c9cea..41b3a7c84f 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -40,6 +40,12 @@ jobs: python_version: "3.11" pytorch: 2.5.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + - cuda: "124" + cuda_version: 12.4.1 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.6.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" steps: - name: Checkout uses: actions/checkout@v4 From 5bbad5ef9374e9cc4c08e5b25601d21a9c3eb3df Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 7 Feb 2025 19:28:54 +0700 Subject: [PATCH 0405/1405] feat: add torch2.6 to ci (#2311) --- .github/workflows/main.yml | 5 +++++ .github/workflows/multi-gpu-e2e.yml | 7 +++++++ .github/workflows/nightlies.yml | 5 +++++ .github/workflows/tests-nightly.yml | 11 ++++++++++- .github/workflows/tests.yml | 16 ++++++++++++---- requirements.txt | 2 +- setup.py | 7 +++++-- 7 files changed, 45 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c9e13957cd..6c7e248bfe 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,6 +26,11 @@ jobs: pytorch: 2.5.1 axolotl_extras: is_latest: true + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.6.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index c3bcc517bb..ffd9a86b4f 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -34,6 +34,13 @@ jobs: axolotl_extras: num_gpus: 2 nightly_build: "true" + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.6.0 + axolotl_extras: + num_gpus: 2 + nightly_build: "true" runs-on: [self-hosted, modal] timeout-minutes: 120 steps: diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 0efeb80b97..c501064d25 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -22,6 +22,11 @@ jobs: python_version: "3.11" pytorch: 2.5.1 axolotl_extras: + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.6.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 178b0a4d19..c1d290e600 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -26,12 +26,14 @@ jobs: max-parallel: 2 matrix: python_version: ["3.10", "3.11"] - pytorch_version: ["2.4.1", "2.5.1"] + pytorch_version: ["2.4.1", "2.5.1", "2.6.0"] exclude: - python_version: "3.10" pytorch_version: "2.4.1" - python_version: "3.10" pytorch_version: "2.5.1" + - python_version: "3.10" + pytorch_version: "2.6.0" timeout-minutes: 20 steps: @@ -112,6 +114,13 @@ jobs: num_gpus: 1 axolotl_extras: nightly_build: "true" + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.6.0 + num_gpus: 1 + axolotl_extras: + nightly_build: "true" steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 709845fc67..320a4534b4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,12 +49,14 @@ jobs: max-parallel: 2 matrix: python_version: ["3.10", "3.11"] - pytorch_version: ["2.4.1", "2.5.1"] + pytorch_version: ["2.4.1", "2.5.1", "2.6.0"] exclude: - python_version: "3.10" pytorch_version: "2.4.1" - python_version: "3.10" pytorch_version: "2.5.1" + - python_version: "3.10" + pytorch_version: "2.6.0" timeout-minutes: 20 steps: @@ -127,7 +129,7 @@ jobs: max-parallel: 1 matrix: python_version: ["3.11"] - pytorch_version: ["2.4.1", "2.5.1"] + pytorch_version: ["2.4.1", "2.5.1", "2.6.0"] timeout-minutes: 20 steps: @@ -216,7 +218,7 @@ jobs: - name: Install Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Install Modal run: | python -m pip install --upgrade pip @@ -251,13 +253,19 @@ jobs: pytorch: 2.4.1 num_gpus: 1 axolotl_extras: + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.6.0 + num_gpus: 1 + axolotl_extras: steps: - name: Checkout uses: actions/checkout@v4 - name: Install Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Install Modal run: | python -m pip install --upgrade pip diff --git a/requirements.txt b/requirements.txt index 061229c697..bea221ce21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ # START section of dependencies that don't install on Darwin/MacOS -bitsandbytes==0.45.1 +bitsandbytes==0.45.2 triton>=3.0.0 mamba-ssm==1.2.0.post1 flash-attn==2.7.0.post2 diff --git a/setup.py b/setup.py index 370eb72972..6730591c05 100644 --- a/setup.py +++ b/setup.py @@ -71,12 +71,15 @@ def parse_requirements(): else: raise ValueError("Invalid version format") - if (major, minor) >= (2, 5): + if (major, minor) >= (2, 6): + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers==0.0.29.post2") + elif (major, minor) >= (2, 5): _install_requires.pop(_install_requires.index(xformers_version)) if patch == 0: _install_requires.append("xformers==0.0.28.post2") else: - _install_requires.append("xformers==0.0.28.post3") + _install_requires.append("xformers==0.0.29") _install_requires.pop(_install_requires.index(autoawq_version)) elif (major, minor) >= (2, 4): if patch == 0: From 1faf1a5c5aaf76fe113ebe80894bec98b6af6d66 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 7 Feb 2025 21:33:14 -0500 Subject: [PATCH 0406/1405] batch add of spectrum snr results (#2320) --- ...nr_results_Qwen-Qwen2.5-1.5B-Instruct.json | 1022 +++++++++++++ .../snr_results_Qwen-Qwen2.5-1.5B.json | 1022 +++++++++++++ .../snr_results_Qwen-Qwen2.5-3B-Instruct.json | 1310 +++++++++++++++++ .../snr_results_Qwen-Qwen2.5-3B.json | 1310 +++++++++++++++++ .../snr_results_Qwen-Qwen2.5-7B-Instruct.json | 1022 +++++++++++++ .../snr_results_Qwen-Qwen2.5-7B.json | 1022 +++++++++++++ .../snr_results_google-gemma-2-2b.json | 1158 +++++++++++++++ ...ults_meta-llama-Llama-3.2-1B-Instruct.json | 590 ++++++++ .../snr_results_meta-llama-Llama-3.2-1B.json | 590 ++++++++ ...ults_meta-llama-Llama-3.2-3B-Instruct.json | 1022 +++++++++++++ .../snr_results_meta-llama-Llama-3.2-3B.json | 1022 +++++++++++++ 11 files changed, 11090 insertions(+) create mode 100644 src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-1.5B-Instruct.json create mode 100644 src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-1.5B.json create mode 100644 src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-3B-Instruct.json create mode 100644 src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-3B.json create mode 100644 src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-7B-Instruct.json create mode 100644 src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-7B.json create mode 100644 src/axolotl/integrations/spectrum/model_snr_results/snr_results_google-gemma-2-2b.json create mode 100644 src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-1B-Instruct.json create mode 100644 src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-1B.json create mode 100644 src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-3B-Instruct.json create mode 100644 src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-3B.json diff --git a/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-1.5B-Instruct.json b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-1.5B-Instruct.json new file mode 100644 index 0000000000..99168b7b26 --- /dev/null +++ b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-1.5B-Instruct.json @@ -0,0 +1,1022 @@ +{ + "model.layers.0.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.1.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.2.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.3.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.4.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.5.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.6.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.7.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.8.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.9.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.10.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.11.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.12.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.13.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.14.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.15.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.16.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.17.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.18.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.19.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.20.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.21.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.22.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.23.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.24.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.25.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.26.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.27.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "lm_head": { + "snr": Infinity, + "type": "lm_head" + }, + "model.layers.0.mlp.down_proj": { + "snr": 70.50235748291016, + "type": "mlp.down_proj" + }, + "model.layers.1.mlp.down_proj": { + "snr": 134.4214630126953, + "type": "mlp.down_proj" + }, + "model.layers.2.mlp.down_proj": { + "snr": 235.74794006347656, + "type": "mlp.down_proj" + }, + "model.layers.3.mlp.down_proj": { + "snr": 73.25755310058594, + "type": "mlp.down_proj" + }, + "model.layers.4.mlp.down_proj": { + "snr": 27.22879981994629, + "type": "mlp.down_proj" + }, + "model.layers.5.mlp.down_proj": { + "snr": 17.5551815032959, + "type": "mlp.down_proj" + }, + "model.layers.6.mlp.down_proj": { + "snr": 54.210426330566406, + "type": "mlp.down_proj" + }, + "model.layers.7.mlp.down_proj": { + "snr": 38.808937072753906, + "type": "mlp.down_proj" + }, + "model.layers.8.mlp.down_proj": { + "snr": 29.799747467041016, + "type": "mlp.down_proj" + }, + "model.layers.9.mlp.down_proj": { + "snr": 10.296355247497559, + "type": "mlp.down_proj" + }, + "model.layers.10.mlp.down_proj": { + "snr": 8.86428165435791, + "type": "mlp.down_proj" + }, + "model.layers.11.mlp.down_proj": { + "snr": 6.43813943862915, + "type": "mlp.down_proj" + }, + "model.layers.12.mlp.down_proj": { + "snr": 7.0912184715271, + "type": "mlp.down_proj" + }, + "model.layers.13.mlp.down_proj": { + "snr": 3.285884141921997, + "type": "mlp.down_proj" + }, + "model.layers.14.mlp.down_proj": { + "snr": 6.073758125305176, + "type": "mlp.down_proj" + }, + "model.layers.15.mlp.down_proj": { + "snr": 5.325990676879883, + "type": "mlp.down_proj" + }, + "model.layers.16.mlp.down_proj": { + "snr": 4.591946601867676, + "type": "mlp.down_proj" + }, + "model.layers.17.mlp.down_proj": { + "snr": 7.021907329559326, + "type": "mlp.down_proj" + }, + "model.layers.18.mlp.down_proj": { + "snr": 6.392782211303711, + "type": "mlp.down_proj" + }, + "model.layers.19.mlp.down_proj": { + "snr": 210.51983642578125, + "type": "mlp.down_proj" + }, + "model.layers.20.mlp.down_proj": { + "snr": 7.1035943031311035, + "type": "mlp.down_proj" + }, + "model.layers.21.mlp.down_proj": { + "snr": 18.701711654663086, + "type": "mlp.down_proj" + }, + "model.layers.22.mlp.down_proj": { + "snr": 14.842622756958008, + "type": "mlp.down_proj" + }, + "model.layers.23.mlp.down_proj": { + "snr": 10.50004768371582, + "type": "mlp.down_proj" + }, + "model.layers.24.mlp.down_proj": { + "snr": 7.225146770477295, + "type": "mlp.down_proj" + }, + "model.layers.25.mlp.down_proj": { + "snr": 7.463952541351318, + "type": "mlp.down_proj" + }, + "model.layers.26.mlp.down_proj": { + "snr": 15.226134300231934, + "type": "mlp.down_proj" + }, + "model.layers.27.mlp.down_proj": { + "snr": 105.4173355102539, + "type": "mlp.down_proj" + }, + "model.layers.0.mlp.gate_proj": { + "snr": 0.5021594166755676, + "type": "mlp.gate_proj" + }, + "model.layers.1.mlp.gate_proj": { + "snr": 34.75935363769531, + "type": "mlp.gate_proj" + }, + "model.layers.2.mlp.gate_proj": { + "snr": 22.855531692504883, + "type": "mlp.gate_proj" + }, + "model.layers.3.mlp.gate_proj": { + "snr": 25.09166717529297, + "type": "mlp.gate_proj" + }, + "model.layers.4.mlp.gate_proj": { + "snr": 28.533172607421875, + "type": "mlp.gate_proj" + }, + "model.layers.5.mlp.gate_proj": { + "snr": 18.625717163085938, + "type": "mlp.gate_proj" + }, + "model.layers.6.mlp.gate_proj": { + "snr": 39.77565383911133, + "type": "mlp.gate_proj" + }, + "model.layers.7.mlp.gate_proj": { + "snr": 24.77678680419922, + "type": "mlp.gate_proj" + }, + "model.layers.8.mlp.gate_proj": { + "snr": 11.854388236999512, + "type": "mlp.gate_proj" + }, + "model.layers.9.mlp.gate_proj": { + "snr": 20.372356414794922, + "type": "mlp.gate_proj" + }, + "model.layers.10.mlp.gate_proj": { + "snr": 14.639552116394043, + "type": "mlp.gate_proj" + }, + "model.layers.11.mlp.gate_proj": { + "snr": 9.82955551147461, + "type": "mlp.gate_proj" + }, + "model.layers.12.mlp.gate_proj": { + "snr": 13.942151069641113, + "type": "mlp.gate_proj" + }, + "model.layers.13.mlp.gate_proj": { + "snr": 12.524999618530273, + "type": "mlp.gate_proj" + }, + "model.layers.14.mlp.gate_proj": { + "snr": 8.19681167602539, + "type": "mlp.gate_proj" + }, + "model.layers.15.mlp.gate_proj": { + "snr": 8.561081886291504, + "type": "mlp.gate_proj" + }, + "model.layers.16.mlp.gate_proj": { + "snr": 6.421900749206543, + "type": "mlp.gate_proj" + }, + "model.layers.17.mlp.gate_proj": { + "snr": 5.568161964416504, + "type": "mlp.gate_proj" + }, + "model.layers.18.mlp.gate_proj": { + "snr": 10.090147972106934, + "type": "mlp.gate_proj" + }, + "model.layers.19.mlp.gate_proj": { + "snr": 5.6181230545043945, + "type": "mlp.gate_proj" + }, + "model.layers.20.mlp.gate_proj": { + "snr": 5.173826694488525, + "type": "mlp.gate_proj" + }, + "model.layers.21.mlp.gate_proj": { + "snr": 5.663441181182861, + "type": "mlp.gate_proj" + }, + "model.layers.22.mlp.gate_proj": { + "snr": 6.824708461761475, + "type": "mlp.gate_proj" + }, + "model.layers.23.mlp.gate_proj": { + "snr": 4.724992275238037, + "type": "mlp.gate_proj" + }, + "model.layers.24.mlp.gate_proj": { + "snr": 6.829834938049316, + "type": "mlp.gate_proj" + }, + "model.layers.25.mlp.gate_proj": { + "snr": 9.968582153320312, + "type": "mlp.gate_proj" + }, + "model.layers.26.mlp.gate_proj": { + "snr": 14.35350513458252, + "type": "mlp.gate_proj" + }, + "model.layers.27.mlp.gate_proj": { + "snr": 20.121768951416016, + "type": "mlp.gate_proj" + }, + "model.layers.0.mlp.up_proj": { + "snr": 1.9020992517471313, + "type": "mlp.up_proj" + }, + "model.layers.1.mlp.up_proj": { + "snr": 46.9393424987793, + "type": "mlp.up_proj" + }, + "model.layers.2.mlp.up_proj": { + "snr": 76.04901123046875, + "type": "mlp.up_proj" + }, + "model.layers.3.mlp.up_proj": { + "snr": 104.08525848388672, + "type": "mlp.up_proj" + }, + "model.layers.4.mlp.up_proj": { + "snr": 77.74343872070312, + "type": "mlp.up_proj" + }, + "model.layers.5.mlp.up_proj": { + "snr": 104.15605926513672, + "type": "mlp.up_proj" + }, + "model.layers.6.mlp.up_proj": { + "snr": 105.16349792480469, + "type": "mlp.up_proj" + }, + "model.layers.7.mlp.up_proj": { + "snr": 78.4150390625, + "type": "mlp.up_proj" + }, + "model.layers.8.mlp.up_proj": { + "snr": 57.51069641113281, + "type": "mlp.up_proj" + }, + "model.layers.9.mlp.up_proj": { + "snr": 50.26409912109375, + "type": "mlp.up_proj" + }, + "model.layers.10.mlp.up_proj": { + "snr": 50.36701965332031, + "type": "mlp.up_proj" + }, + "model.layers.11.mlp.up_proj": { + "snr": 56.66413497924805, + "type": "mlp.up_proj" + }, + "model.layers.12.mlp.up_proj": { + "snr": 62.384559631347656, + "type": "mlp.up_proj" + }, + "model.layers.13.mlp.up_proj": { + "snr": 44.97883987426758, + "type": "mlp.up_proj" + }, + "model.layers.14.mlp.up_proj": { + "snr": 69.7376480102539, + "type": "mlp.up_proj" + }, + "model.layers.15.mlp.up_proj": { + "snr": 35.93111801147461, + "type": "mlp.up_proj" + }, + "model.layers.16.mlp.up_proj": { + "snr": 33.63168716430664, + "type": "mlp.up_proj" + }, + "model.layers.17.mlp.up_proj": { + "snr": 37.695919036865234, + "type": "mlp.up_proj" + }, + "model.layers.18.mlp.up_proj": { + "snr": 43.516517639160156, + "type": "mlp.up_proj" + }, + "model.layers.19.mlp.up_proj": { + "snr": 30.479318618774414, + "type": "mlp.up_proj" + }, + "model.layers.20.mlp.up_proj": { + "snr": 12.495409965515137, + "type": "mlp.up_proj" + }, + "model.layers.21.mlp.up_proj": { + "snr": 19.616689682006836, + "type": "mlp.up_proj" + }, + "model.layers.22.mlp.up_proj": { + "snr": 18.42948341369629, + "type": "mlp.up_proj" + }, + "model.layers.23.mlp.up_proj": { + "snr": 10.799560546875, + "type": "mlp.up_proj" + }, + "model.layers.24.mlp.up_proj": { + "snr": 14.167623519897461, + "type": "mlp.up_proj" + }, + "model.layers.25.mlp.up_proj": { + "snr": 14.938597679138184, + "type": "mlp.up_proj" + }, + "model.layers.26.mlp.up_proj": { + "snr": 8.896568298339844, + "type": "mlp.up_proj" + }, + "model.layers.27.mlp.up_proj": { + "snr": 25.774547576904297, + "type": "mlp.up_proj" + }, + "model.embed_tokens": { + "snr": Infinity, + "type": "model.embed_tokens" + }, + "model.norm": { + "snr": Infinity, + "type": "model.norm" + }, + "model.layers.0.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.1.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.2.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.3.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.4.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.5.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.6.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.7.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.8.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.9.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.10.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.11.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.12.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.13.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.14.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.15.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.16.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.17.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.18.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.19.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.20.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.21.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.22.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.23.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.24.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.25.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.26.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.27.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.0.self_attn.k_proj": { + "snr": 1.8306859731674194, + "type": "self_attn.k_proj" + }, + "model.layers.1.self_attn.k_proj": { + "snr": 0.896544337272644, + "type": "self_attn.k_proj" + }, + "model.layers.2.self_attn.k_proj": { + "snr": 2.345759868621826, + "type": "self_attn.k_proj" + }, + "model.layers.3.self_attn.k_proj": { + "snr": 2.0610744953155518, + "type": "self_attn.k_proj" + }, + "model.layers.4.self_attn.k_proj": { + "snr": 2.3658556938171387, + "type": "self_attn.k_proj" + }, + "model.layers.5.self_attn.k_proj": { + "snr": 1.6586917638778687, + "type": "self_attn.k_proj" + }, + "model.layers.6.self_attn.k_proj": { + "snr": 1.7613047361373901, + "type": "self_attn.k_proj" + }, + "model.layers.7.self_attn.k_proj": { + "snr": 1.325312852859497, + "type": "self_attn.k_proj" + }, + "model.layers.8.self_attn.k_proj": { + "snr": 1.458108901977539, + "type": "self_attn.k_proj" + }, + "model.layers.9.self_attn.k_proj": { + "snr": 1.4319790601730347, + "type": "self_attn.k_proj" + }, + "model.layers.10.self_attn.k_proj": { + "snr": 0.9579543471336365, + "type": "self_attn.k_proj" + }, + "model.layers.11.self_attn.k_proj": { + "snr": 0.8787619471549988, + "type": "self_attn.k_proj" + }, + "model.layers.12.self_attn.k_proj": { + "snr": 1.0447536706924438, + "type": "self_attn.k_proj" + }, + "model.layers.13.self_attn.k_proj": { + "snr": 0.9157310724258423, + "type": "self_attn.k_proj" + }, + "model.layers.14.self_attn.k_proj": { + "snr": 0.7528730630874634, + "type": "self_attn.k_proj" + }, + "model.layers.15.self_attn.k_proj": { + "snr": 0.9293556213378906, + "type": "self_attn.k_proj" + }, + "model.layers.16.self_attn.k_proj": { + "snr": 0.8057093620300293, + "type": "self_attn.k_proj" + }, + "model.layers.17.self_attn.k_proj": { + "snr": 1.2973601818084717, + "type": "self_attn.k_proj" + }, + "model.layers.18.self_attn.k_proj": { + "snr": 1.1357901096343994, + "type": "self_attn.k_proj" + }, + "model.layers.19.self_attn.k_proj": { + "snr": 1.3661632537841797, + "type": "self_attn.k_proj" + }, + "model.layers.20.self_attn.k_proj": { + "snr": 0.8829066753387451, + "type": "self_attn.k_proj" + }, + "model.layers.21.self_attn.k_proj": { + "snr": 0.9105398654937744, + "type": "self_attn.k_proj" + }, + "model.layers.22.self_attn.k_proj": { + "snr": 2.086926221847534, + "type": "self_attn.k_proj" + }, + "model.layers.23.self_attn.k_proj": { + "snr": 1.0393351316452026, + "type": "self_attn.k_proj" + }, + "model.layers.24.self_attn.k_proj": { + "snr": 1.114574670791626, + "type": "self_attn.k_proj" + }, + "model.layers.25.self_attn.k_proj": { + "snr": 2.599745035171509, + "type": "self_attn.k_proj" + }, + "model.layers.26.self_attn.k_proj": { + "snr": 1.1256712675094604, + "type": "self_attn.k_proj" + }, + "model.layers.27.self_attn.k_proj": { + "snr": 1.1784162521362305, + "type": "self_attn.k_proj" + }, + "model.layers.0.self_attn.o_proj": { + "snr": 0.8094121813774109, + "type": "self_attn.o_proj" + }, + "model.layers.1.self_attn.o_proj": { + "snr": 0.22000817954540253, + "type": "self_attn.o_proj" + }, + "model.layers.2.self_attn.o_proj": { + "snr": 0.21972468495368958, + "type": "self_attn.o_proj" + }, + "model.layers.3.self_attn.o_proj": { + "snr": 0.22064059972763062, + "type": "self_attn.o_proj" + }, + "model.layers.4.self_attn.o_proj": { + "snr": 0.22308556735515594, + "type": "self_attn.o_proj" + }, + "model.layers.5.self_attn.o_proj": { + "snr": 0.22396250069141388, + "type": "self_attn.o_proj" + }, + "model.layers.6.self_attn.o_proj": { + "snr": 0.228360116481781, + "type": "self_attn.o_proj" + }, + "model.layers.7.self_attn.o_proj": { + "snr": 0.2306283563375473, + "type": "self_attn.o_proj" + }, + "model.layers.8.self_attn.o_proj": { + "snr": 0.2430228292942047, + "type": "self_attn.o_proj" + }, + "model.layers.9.self_attn.o_proj": { + "snr": 0.2115175724029541, + "type": "self_attn.o_proj" + }, + "model.layers.10.self_attn.o_proj": { + "snr": 0.18226943910121918, + "type": "self_attn.o_proj" + }, + "model.layers.11.self_attn.o_proj": { + "snr": 0.144245907664299, + "type": "self_attn.o_proj" + }, + "model.layers.12.self_attn.o_proj": { + "snr": 0.21965907514095306, + "type": "self_attn.o_proj" + }, + "model.layers.13.self_attn.o_proj": { + "snr": 0.1797526627779007, + "type": "self_attn.o_proj" + }, + "model.layers.14.self_attn.o_proj": { + "snr": 0.26513636112213135, + "type": "self_attn.o_proj" + }, + "model.layers.15.self_attn.o_proj": { + "snr": 0.19463808834552765, + "type": "self_attn.o_proj" + }, + "model.layers.16.self_attn.o_proj": { + "snr": 0.22129350900650024, + "type": "self_attn.o_proj" + }, + "model.layers.17.self_attn.o_proj": { + "snr": 0.22545330226421356, + "type": "self_attn.o_proj" + }, + "model.layers.18.self_attn.o_proj": { + "snr": 0.25302645564079285, + "type": "self_attn.o_proj" + }, + "model.layers.19.self_attn.o_proj": { + "snr": 0.26326504349708557, + "type": "self_attn.o_proj" + }, + "model.layers.20.self_attn.o_proj": { + "snr": 0.15203869342803955, + "type": "self_attn.o_proj" + }, + "model.layers.21.self_attn.o_proj": { + "snr": 0.22418837249279022, + "type": "self_attn.o_proj" + }, + "model.layers.22.self_attn.o_proj": { + "snr": 0.23777326941490173, + "type": "self_attn.o_proj" + }, + "model.layers.23.self_attn.o_proj": { + "snr": 0.18076598644256592, + "type": "self_attn.o_proj" + }, + "model.layers.24.self_attn.o_proj": { + "snr": 0.19919466972351074, + "type": "self_attn.o_proj" + }, + "model.layers.25.self_attn.o_proj": { + "snr": 0.11310968548059464, + "type": "self_attn.o_proj" + }, + "model.layers.26.self_attn.o_proj": { + "snr": 0.08452697843313217, + "type": "self_attn.o_proj" + }, + "model.layers.27.self_attn.o_proj": { + "snr": 0.1029304787516594, + "type": "self_attn.o_proj" + }, + "model.layers.0.self_attn.q_proj": { + "snr": 0.03922705352306366, + "type": "self_attn.q_proj" + }, + "model.layers.1.self_attn.q_proj": { + "snr": 0.1410205066204071, + "type": "self_attn.q_proj" + }, + "model.layers.2.self_attn.q_proj": { + "snr": 0.18240582942962646, + "type": "self_attn.q_proj" + }, + "model.layers.3.self_attn.q_proj": { + "snr": 0.1702580451965332, + "type": "self_attn.q_proj" + }, + "model.layers.4.self_attn.q_proj": { + "snr": 0.19508686661720276, + "type": "self_attn.q_proj" + }, + "model.layers.5.self_attn.q_proj": { + "snr": 0.21549257636070251, + "type": "self_attn.q_proj" + }, + "model.layers.6.self_attn.q_proj": { + "snr": 0.22021502256393433, + "type": "self_attn.q_proj" + }, + "model.layers.7.self_attn.q_proj": { + "snr": 0.2044307142496109, + "type": "self_attn.q_proj" + }, + "model.layers.8.self_attn.q_proj": { + "snr": 0.22745060920715332, + "type": "self_attn.q_proj" + }, + "model.layers.9.self_attn.q_proj": { + "snr": 0.23825915157794952, + "type": "self_attn.q_proj" + }, + "model.layers.10.self_attn.q_proj": { + "snr": 0.2181481122970581, + "type": "self_attn.q_proj" + }, + "model.layers.11.self_attn.q_proj": { + "snr": 0.23490090668201447, + "type": "self_attn.q_proj" + }, + "model.layers.12.self_attn.q_proj": { + "snr": 0.2379382699728012, + "type": "self_attn.q_proj" + }, + "model.layers.13.self_attn.q_proj": { + "snr": 0.19233369827270508, + "type": "self_attn.q_proj" + }, + "model.layers.14.self_attn.q_proj": { + "snr": 0.2587313652038574, + "type": "self_attn.q_proj" + }, + "model.layers.15.self_attn.q_proj": { + "snr": 0.07332809269428253, + "type": "self_attn.q_proj" + }, + "model.layers.16.self_attn.q_proj": { + "snr": 0.22992204129695892, + "type": "self_attn.q_proj" + }, + "model.layers.17.self_attn.q_proj": { + "snr": 0.2537729740142822, + "type": "self_attn.q_proj" + }, + "model.layers.18.self_attn.q_proj": { + "snr": 0.2389948070049286, + "type": "self_attn.q_proj" + }, + "model.layers.19.self_attn.q_proj": { + "snr": 0.20716068148612976, + "type": "self_attn.q_proj" + }, + "model.layers.20.self_attn.q_proj": { + "snr": 0.2575169503688812, + "type": "self_attn.q_proj" + }, + "model.layers.21.self_attn.q_proj": { + "snr": 0.22347678244113922, + "type": "self_attn.q_proj" + }, + "model.layers.22.self_attn.q_proj": { + "snr": 0.18831054866313934, + "type": "self_attn.q_proj" + }, + "model.layers.23.self_attn.q_proj": { + "snr": 0.19853907823562622, + "type": "self_attn.q_proj" + }, + "model.layers.24.self_attn.q_proj": { + "snr": 0.16343259811401367, + "type": "self_attn.q_proj" + }, + "model.layers.25.self_attn.q_proj": { + "snr": 0.1583252102136612, + "type": "self_attn.q_proj" + }, + "model.layers.26.self_attn.q_proj": { + "snr": 0.254446804523468, + "type": "self_attn.q_proj" + }, + "model.layers.27.self_attn.q_proj": { + "snr": 0.23828543722629547, + "type": "self_attn.q_proj" + }, + "model.layers.0.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.1.self_attn.v_proj": { + "snr": 856.5148315429688, + "type": "self_attn.v_proj" + }, + "model.layers.2.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.3.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.4.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.5.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.6.self_attn.v_proj": { + "snr": 48.941104888916016, + "type": "self_attn.v_proj" + }, + "model.layers.7.self_attn.v_proj": { + "snr": 70.25466918945312, + "type": "self_attn.v_proj" + }, + "model.layers.8.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.9.self_attn.v_proj": { + "snr": 370.885986328125, + "type": "self_attn.v_proj" + }, + "model.layers.10.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.11.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.12.self_attn.v_proj": { + "snr": 75.51139831542969, + "type": "self_attn.v_proj" + }, + "model.layers.13.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.14.self_attn.v_proj": { + "snr": 52.004058837890625, + "type": "self_attn.v_proj" + }, + "model.layers.15.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.16.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.17.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.18.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.19.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.20.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.21.self_attn.v_proj": { + "snr": 641.026611328125, + "type": "self_attn.v_proj" + }, + "model.layers.22.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.23.self_attn.v_proj": { + "snr": 323.4858093261719, + "type": "self_attn.v_proj" + }, + "model.layers.24.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.25.self_attn.v_proj": { + "snr": 2.1745388507843018, + "type": "self_attn.v_proj" + }, + "model.layers.26.self_attn.v_proj": { + "snr": 3.0791690349578857, + "type": "self_attn.v_proj" + }, + "model.layers.27.self_attn.v_proj": { + "snr": 2.029968023300171, + "type": "self_attn.v_proj" + } +} diff --git a/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-1.5B.json b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-1.5B.json new file mode 100644 index 0000000000..41cde8e6b0 --- /dev/null +++ b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-1.5B.json @@ -0,0 +1,1022 @@ +{ + "model.layers.0.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.1.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.2.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.3.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.4.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.5.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.6.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.7.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.8.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.9.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.10.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.11.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.12.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.13.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.14.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.15.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.16.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.17.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.18.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.19.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.20.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.21.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.22.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.23.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.24.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.25.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.26.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.27.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "lm_head": { + "snr": Infinity, + "type": "lm_head" + }, + "model.layers.0.mlp.down_proj": { + "snr": 70.4939193725586, + "type": "mlp.down_proj" + }, + "model.layers.1.mlp.down_proj": { + "snr": 134.2310028076172, + "type": "mlp.down_proj" + }, + "model.layers.2.mlp.down_proj": { + "snr": 235.44140625, + "type": "mlp.down_proj" + }, + "model.layers.3.mlp.down_proj": { + "snr": 73.19381713867188, + "type": "mlp.down_proj" + }, + "model.layers.4.mlp.down_proj": { + "snr": 27.216264724731445, + "type": "mlp.down_proj" + }, + "model.layers.5.mlp.down_proj": { + "snr": 17.544504165649414, + "type": "mlp.down_proj" + }, + "model.layers.6.mlp.down_proj": { + "snr": 54.17462158203125, + "type": "mlp.down_proj" + }, + "model.layers.7.mlp.down_proj": { + "snr": 38.78171920776367, + "type": "mlp.down_proj" + }, + "model.layers.8.mlp.down_proj": { + "snr": 29.777149200439453, + "type": "mlp.down_proj" + }, + "model.layers.9.mlp.down_proj": { + "snr": 10.289377212524414, + "type": "mlp.down_proj" + }, + "model.layers.10.mlp.down_proj": { + "snr": 8.858332633972168, + "type": "mlp.down_proj" + }, + "model.layers.11.mlp.down_proj": { + "snr": 6.433396816253662, + "type": "mlp.down_proj" + }, + "model.layers.12.mlp.down_proj": { + "snr": 7.085702419281006, + "type": "mlp.down_proj" + }, + "model.layers.13.mlp.down_proj": { + "snr": 3.323948383331299, + "type": "mlp.down_proj" + }, + "model.layers.14.mlp.down_proj": { + "snr": 6.204164505004883, + "type": "mlp.down_proj" + }, + "model.layers.15.mlp.down_proj": { + "snr": 5.321533203125, + "type": "mlp.down_proj" + }, + "model.layers.16.mlp.down_proj": { + "snr": 4.588479995727539, + "type": "mlp.down_proj" + }, + "model.layers.17.mlp.down_proj": { + "snr": 7.01450252532959, + "type": "mlp.down_proj" + }, + "model.layers.18.mlp.down_proj": { + "snr": 6.386813163757324, + "type": "mlp.down_proj" + }, + "model.layers.19.mlp.down_proj": { + "snr": 210.38458251953125, + "type": "mlp.down_proj" + }, + "model.layers.20.mlp.down_proj": { + "snr": 7.096683979034424, + "type": "mlp.down_proj" + }, + "model.layers.21.mlp.down_proj": { + "snr": 18.68245506286621, + "type": "mlp.down_proj" + }, + "model.layers.22.mlp.down_proj": { + "snr": 14.824685096740723, + "type": "mlp.down_proj" + }, + "model.layers.23.mlp.down_proj": { + "snr": 10.491303443908691, + "type": "mlp.down_proj" + }, + "model.layers.24.mlp.down_proj": { + "snr": 7.2194437980651855, + "type": "mlp.down_proj" + }, + "model.layers.25.mlp.down_proj": { + "snr": 7.458613872528076, + "type": "mlp.down_proj" + }, + "model.layers.26.mlp.down_proj": { + "snr": 15.222760200500488, + "type": "mlp.down_proj" + }, + "model.layers.27.mlp.down_proj": { + "snr": 105.41569519042969, + "type": "mlp.down_proj" + }, + "model.layers.0.mlp.gate_proj": { + "snr": 0.5017311573028564, + "type": "mlp.gate_proj" + }, + "model.layers.1.mlp.gate_proj": { + "snr": 34.71562576293945, + "type": "mlp.gate_proj" + }, + "model.layers.2.mlp.gate_proj": { + "snr": 22.82915496826172, + "type": "mlp.gate_proj" + }, + "model.layers.3.mlp.gate_proj": { + "snr": 25.0699520111084, + "type": "mlp.gate_proj" + }, + "model.layers.4.mlp.gate_proj": { + "snr": 28.508079528808594, + "type": "mlp.gate_proj" + }, + "model.layers.5.mlp.gate_proj": { + "snr": 18.608009338378906, + "type": "mlp.gate_proj" + }, + "model.layers.6.mlp.gate_proj": { + "snr": 39.732391357421875, + "type": "mlp.gate_proj" + }, + "model.layers.7.mlp.gate_proj": { + "snr": 24.760026931762695, + "type": "mlp.gate_proj" + }, + "model.layers.8.mlp.gate_proj": { + "snr": 11.842738151550293, + "type": "mlp.gate_proj" + }, + "model.layers.9.mlp.gate_proj": { + "snr": 20.35906982421875, + "type": "mlp.gate_proj" + }, + "model.layers.10.mlp.gate_proj": { + "snr": 14.627532958984375, + "type": "mlp.gate_proj" + }, + "model.layers.11.mlp.gate_proj": { + "snr": 9.821962356567383, + "type": "mlp.gate_proj" + }, + "model.layers.12.mlp.gate_proj": { + "snr": 13.930404663085938, + "type": "mlp.gate_proj" + }, + "model.layers.13.mlp.gate_proj": { + "snr": 12.509871482849121, + "type": "mlp.gate_proj" + }, + "model.layers.14.mlp.gate_proj": { + "snr": 8.187695503234863, + "type": "mlp.gate_proj" + }, + "model.layers.15.mlp.gate_proj": { + "snr": 8.553187370300293, + "type": "mlp.gate_proj" + }, + "model.layers.16.mlp.gate_proj": { + "snr": 6.414614200592041, + "type": "mlp.gate_proj" + }, + "model.layers.17.mlp.gate_proj": { + "snr": 5.561778545379639, + "type": "mlp.gate_proj" + }, + "model.layers.18.mlp.gate_proj": { + "snr": 10.078697204589844, + "type": "mlp.gate_proj" + }, + "model.layers.19.mlp.gate_proj": { + "snr": 5.61345100402832, + "type": "mlp.gate_proj" + }, + "model.layers.20.mlp.gate_proj": { + "snr": 5.265484809875488, + "type": "mlp.gate_proj" + }, + "model.layers.21.mlp.gate_proj": { + "snr": 5.659949779510498, + "type": "mlp.gate_proj" + }, + "model.layers.22.mlp.gate_proj": { + "snr": 6.8203511238098145, + "type": "mlp.gate_proj" + }, + "model.layers.23.mlp.gate_proj": { + "snr": 4.721294403076172, + "type": "mlp.gate_proj" + }, + "model.layers.24.mlp.gate_proj": { + "snr": 6.82572603225708, + "type": "mlp.gate_proj" + }, + "model.layers.25.mlp.gate_proj": { + "snr": 9.963521003723145, + "type": "mlp.gate_proj" + }, + "model.layers.26.mlp.gate_proj": { + "snr": 14.342291831970215, + "type": "mlp.gate_proj" + }, + "model.layers.27.mlp.gate_proj": { + "snr": 20.092098236083984, + "type": "mlp.gate_proj" + }, + "model.layers.0.mlp.up_proj": { + "snr": 1.901187777519226, + "type": "mlp.up_proj" + }, + "model.layers.1.mlp.up_proj": { + "snr": 46.9141731262207, + "type": "mlp.up_proj" + }, + "model.layers.2.mlp.up_proj": { + "snr": 76.07878112792969, + "type": "mlp.up_proj" + }, + "model.layers.3.mlp.up_proj": { + "snr": 103.9194564819336, + "type": "mlp.up_proj" + }, + "model.layers.4.mlp.up_proj": { + "snr": 77.62561798095703, + "type": "mlp.up_proj" + }, + "model.layers.5.mlp.up_proj": { + "snr": 104.01624298095703, + "type": "mlp.up_proj" + }, + "model.layers.6.mlp.up_proj": { + "snr": 105.0235366821289, + "type": "mlp.up_proj" + }, + "model.layers.7.mlp.up_proj": { + "snr": 78.33445739746094, + "type": "mlp.up_proj" + }, + "model.layers.8.mlp.up_proj": { + "snr": 57.44070816040039, + "type": "mlp.up_proj" + }, + "model.layers.9.mlp.up_proj": { + "snr": 50.20344924926758, + "type": "mlp.up_proj" + }, + "model.layers.10.mlp.up_proj": { + "snr": 50.32845687866211, + "type": "mlp.up_proj" + }, + "model.layers.11.mlp.up_proj": { + "snr": 56.6197624206543, + "type": "mlp.up_proj" + }, + "model.layers.12.mlp.up_proj": { + "snr": 62.338096618652344, + "type": "mlp.up_proj" + }, + "model.layers.13.mlp.up_proj": { + "snr": 44.92917251586914, + "type": "mlp.up_proj" + }, + "model.layers.14.mlp.up_proj": { + "snr": 69.69624328613281, + "type": "mlp.up_proj" + }, + "model.layers.15.mlp.up_proj": { + "snr": 35.90705108642578, + "type": "mlp.up_proj" + }, + "model.layers.16.mlp.up_proj": { + "snr": 33.610374450683594, + "type": "mlp.up_proj" + }, + "model.layers.17.mlp.up_proj": { + "snr": 37.67365646362305, + "type": "mlp.up_proj" + }, + "model.layers.18.mlp.up_proj": { + "snr": 43.488929748535156, + "type": "mlp.up_proj" + }, + "model.layers.19.mlp.up_proj": { + "snr": 30.451993942260742, + "type": "mlp.up_proj" + }, + "model.layers.20.mlp.up_proj": { + "snr": 12.480182647705078, + "type": "mlp.up_proj" + }, + "model.layers.21.mlp.up_proj": { + "snr": 19.595102310180664, + "type": "mlp.up_proj" + }, + "model.layers.22.mlp.up_proj": { + "snr": 19.067970275878906, + "type": "mlp.up_proj" + }, + "model.layers.23.mlp.up_proj": { + "snr": 10.786394119262695, + "type": "mlp.up_proj" + }, + "model.layers.24.mlp.up_proj": { + "snr": 14.150126457214355, + "type": "mlp.up_proj" + }, + "model.layers.25.mlp.up_proj": { + "snr": 14.927021026611328, + "type": "mlp.up_proj" + }, + "model.layers.26.mlp.up_proj": { + "snr": 8.891448020935059, + "type": "mlp.up_proj" + }, + "model.layers.27.mlp.up_proj": { + "snr": 25.74305534362793, + "type": "mlp.up_proj" + }, + "model.embed_tokens": { + "snr": Infinity, + "type": "model.embed_tokens" + }, + "model.norm": { + "snr": Infinity, + "type": "model.norm" + }, + "model.layers.0.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.1.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.2.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.3.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.4.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.5.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.6.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.7.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.8.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.9.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.10.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.11.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.12.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.13.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.14.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.15.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.16.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.17.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.18.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.19.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.20.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.21.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.22.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.23.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.24.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.25.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.26.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.27.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.0.self_attn.k_proj": { + "snr": 1.7818864583969116, + "type": "self_attn.k_proj" + }, + "model.layers.1.self_attn.k_proj": { + "snr": 0.8955822587013245, + "type": "self_attn.k_proj" + }, + "model.layers.2.self_attn.k_proj": { + "snr": 2.344149351119995, + "type": "self_attn.k_proj" + }, + "model.layers.3.self_attn.k_proj": { + "snr": 2.0597119331359863, + "type": "self_attn.k_proj" + }, + "model.layers.4.self_attn.k_proj": { + "snr": 2.36411714553833, + "type": "self_attn.k_proj" + }, + "model.layers.5.self_attn.k_proj": { + "snr": 1.6570613384246826, + "type": "self_attn.k_proj" + }, + "model.layers.6.self_attn.k_proj": { + "snr": 1.7604507207870483, + "type": "self_attn.k_proj" + }, + "model.layers.7.self_attn.k_proj": { + "snr": 1.3245182037353516, + "type": "self_attn.k_proj" + }, + "model.layers.8.self_attn.k_proj": { + "snr": 1.4567548036575317, + "type": "self_attn.k_proj" + }, + "model.layers.9.self_attn.k_proj": { + "snr": 1.4310829639434814, + "type": "self_attn.k_proj" + }, + "model.layers.10.self_attn.k_proj": { + "snr": 0.95713210105896, + "type": "self_attn.k_proj" + }, + "model.layers.11.self_attn.k_proj": { + "snr": 0.8781776428222656, + "type": "self_attn.k_proj" + }, + "model.layers.12.self_attn.k_proj": { + "snr": 1.0438013076782227, + "type": "self_attn.k_proj" + }, + "model.layers.13.self_attn.k_proj": { + "snr": 0.9315219521522522, + "type": "self_attn.k_proj" + }, + "model.layers.14.self_attn.k_proj": { + "snr": 0.7521569728851318, + "type": "self_attn.k_proj" + }, + "model.layers.15.self_attn.k_proj": { + "snr": 0.9286947250366211, + "type": "self_attn.k_proj" + }, + "model.layers.16.self_attn.k_proj": { + "snr": 0.8047553896903992, + "type": "self_attn.k_proj" + }, + "model.layers.17.self_attn.k_proj": { + "snr": 1.2965552806854248, + "type": "self_attn.k_proj" + }, + "model.layers.18.self_attn.k_proj": { + "snr": 1.134974479675293, + "type": "self_attn.k_proj" + }, + "model.layers.19.self_attn.k_proj": { + "snr": 1.3648872375488281, + "type": "self_attn.k_proj" + }, + "model.layers.20.self_attn.k_proj": { + "snr": 0.8667459487915039, + "type": "self_attn.k_proj" + }, + "model.layers.21.self_attn.k_proj": { + "snr": 0.9100639224052429, + "type": "self_attn.k_proj" + }, + "model.layers.22.self_attn.k_proj": { + "snr": 2.127535820007324, + "type": "self_attn.k_proj" + }, + "model.layers.23.self_attn.k_proj": { + "snr": 1.0382369756698608, + "type": "self_attn.k_proj" + }, + "model.layers.24.self_attn.k_proj": { + "snr": 1.113753318786621, + "type": "self_attn.k_proj" + }, + "model.layers.25.self_attn.k_proj": { + "snr": 2.597890853881836, + "type": "self_attn.k_proj" + }, + "model.layers.26.self_attn.k_proj": { + "snr": 1.1248247623443604, + "type": "self_attn.k_proj" + }, + "model.layers.27.self_attn.k_proj": { + "snr": 1.1984941959381104, + "type": "self_attn.k_proj" + }, + "model.layers.0.self_attn.o_proj": { + "snr": 0.8139898777008057, + "type": "self_attn.o_proj" + }, + "model.layers.1.self_attn.o_proj": { + "snr": 0.21965594589710236, + "type": "self_attn.o_proj" + }, + "model.layers.2.self_attn.o_proj": { + "snr": 0.219479501247406, + "type": "self_attn.o_proj" + }, + "model.layers.3.self_attn.o_proj": { + "snr": 0.22144284844398499, + "type": "self_attn.o_proj" + }, + "model.layers.4.self_attn.o_proj": { + "snr": 0.22390463948249817, + "type": "self_attn.o_proj" + }, + "model.layers.5.self_attn.o_proj": { + "snr": 0.22383669018745422, + "type": "self_attn.o_proj" + }, + "model.layers.6.self_attn.o_proj": { + "snr": 0.22818723320960999, + "type": "self_attn.o_proj" + }, + "model.layers.7.self_attn.o_proj": { + "snr": 0.23134392499923706, + "type": "self_attn.o_proj" + }, + "model.layers.8.self_attn.o_proj": { + "snr": 0.24275101721286774, + "type": "self_attn.o_proj" + }, + "model.layers.9.self_attn.o_proj": { + "snr": 0.21139128506183624, + "type": "self_attn.o_proj" + }, + "model.layers.10.self_attn.o_proj": { + "snr": 0.18210072815418243, + "type": "self_attn.o_proj" + }, + "model.layers.11.self_attn.o_proj": { + "snr": 0.14415481686592102, + "type": "self_attn.o_proj" + }, + "model.layers.12.self_attn.o_proj": { + "snr": 0.21947966516017914, + "type": "self_attn.o_proj" + }, + "model.layers.13.self_attn.o_proj": { + "snr": 0.17875106632709503, + "type": "self_attn.o_proj" + }, + "model.layers.14.self_attn.o_proj": { + "snr": 0.264996200799942, + "type": "self_attn.o_proj" + }, + "model.layers.15.self_attn.o_proj": { + "snr": 0.19353187084197998, + "type": "self_attn.o_proj" + }, + "model.layers.16.self_attn.o_proj": { + "snr": 0.22111012041568756, + "type": "self_attn.o_proj" + }, + "model.layers.17.self_attn.o_proj": { + "snr": 0.2242278754711151, + "type": "self_attn.o_proj" + }, + "model.layers.18.self_attn.o_proj": { + "snr": 0.2527434229850769, + "type": "self_attn.o_proj" + }, + "model.layers.19.self_attn.o_proj": { + "snr": 0.26184532046318054, + "type": "self_attn.o_proj" + }, + "model.layers.20.self_attn.o_proj": { + "snr": 0.1519661247730255, + "type": "self_attn.o_proj" + }, + "model.layers.21.self_attn.o_proj": { + "snr": 0.22386522591114044, + "type": "self_attn.o_proj" + }, + "model.layers.22.self_attn.o_proj": { + "snr": 0.2386160045862198, + "type": "self_attn.o_proj" + }, + "model.layers.23.self_attn.o_proj": { + "snr": 0.18057651817798615, + "type": "self_attn.o_proj" + }, + "model.layers.24.self_attn.o_proj": { + "snr": 0.1989467740058899, + "type": "self_attn.o_proj" + }, + "model.layers.25.self_attn.o_proj": { + "snr": 0.11306505650281906, + "type": "self_attn.o_proj" + }, + "model.layers.26.self_attn.o_proj": { + "snr": 0.08449216932058334, + "type": "self_attn.o_proj" + }, + "model.layers.27.self_attn.o_proj": { + "snr": 0.10287519544363022, + "type": "self_attn.o_proj" + }, + "model.layers.0.self_attn.q_proj": { + "snr": 0.039204664528369904, + "type": "self_attn.q_proj" + }, + "model.layers.1.self_attn.q_proj": { + "snr": 0.14075909554958344, + "type": "self_attn.q_proj" + }, + "model.layers.2.self_attn.q_proj": { + "snr": 0.18212397396564484, + "type": "self_attn.q_proj" + }, + "model.layers.3.self_attn.q_proj": { + "snr": 0.1700422316789627, + "type": "self_attn.q_proj" + }, + "model.layers.4.self_attn.q_proj": { + "snr": 0.1948907971382141, + "type": "self_attn.q_proj" + }, + "model.layers.5.self_attn.q_proj": { + "snr": 0.2153141051530838, + "type": "self_attn.q_proj" + }, + "model.layers.6.self_attn.q_proj": { + "snr": 0.21998055279254913, + "type": "self_attn.q_proj" + }, + "model.layers.7.self_attn.q_proj": { + "snr": 0.20416118204593658, + "type": "self_attn.q_proj" + }, + "model.layers.8.self_attn.q_proj": { + "snr": 0.2272879034280777, + "type": "self_attn.q_proj" + }, + "model.layers.9.self_attn.q_proj": { + "snr": 0.23795834183692932, + "type": "self_attn.q_proj" + }, + "model.layers.10.self_attn.q_proj": { + "snr": 0.21887299418449402, + "type": "self_attn.q_proj" + }, + "model.layers.11.self_attn.q_proj": { + "snr": 0.23469635844230652, + "type": "self_attn.q_proj" + }, + "model.layers.12.self_attn.q_proj": { + "snr": 0.23774078488349915, + "type": "self_attn.q_proj" + }, + "model.layers.13.self_attn.q_proj": { + "snr": 0.1920779049396515, + "type": "self_attn.q_proj" + }, + "model.layers.14.self_attn.q_proj": { + "snr": 0.2584812641143799, + "type": "self_attn.q_proj" + }, + "model.layers.15.self_attn.q_proj": { + "snr": 0.07330238074064255, + "type": "self_attn.q_proj" + }, + "model.layers.16.self_attn.q_proj": { + "snr": 0.23073157668113708, + "type": "self_attn.q_proj" + }, + "model.layers.17.self_attn.q_proj": { + "snr": 0.2523840367794037, + "type": "self_attn.q_proj" + }, + "model.layers.18.self_attn.q_proj": { + "snr": 0.23874858021736145, + "type": "self_attn.q_proj" + }, + "model.layers.19.self_attn.q_proj": { + "snr": 0.20698708295822144, + "type": "self_attn.q_proj" + }, + "model.layers.20.self_attn.q_proj": { + "snr": 0.25723400712013245, + "type": "self_attn.q_proj" + }, + "model.layers.21.self_attn.q_proj": { + "snr": 0.223300039768219, + "type": "self_attn.q_proj" + }, + "model.layers.22.self_attn.q_proj": { + "snr": 0.18824049830436707, + "type": "self_attn.q_proj" + }, + "model.layers.23.self_attn.q_proj": { + "snr": 0.19840741157531738, + "type": "self_attn.q_proj" + }, + "model.layers.24.self_attn.q_proj": { + "snr": 0.16326843202114105, + "type": "self_attn.q_proj" + }, + "model.layers.25.self_attn.q_proj": { + "snr": 0.1581888198852539, + "type": "self_attn.q_proj" + }, + "model.layers.26.self_attn.q_proj": { + "snr": 0.25306230783462524, + "type": "self_attn.q_proj" + }, + "model.layers.27.self_attn.q_proj": { + "snr": 0.23808495700359344, + "type": "self_attn.q_proj" + }, + "model.layers.0.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.1.self_attn.v_proj": { + "snr": 864.8881225585938, + "type": "self_attn.v_proj" + }, + "model.layers.2.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.3.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.4.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.5.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.6.self_attn.v_proj": { + "snr": 48.853694915771484, + "type": "self_attn.v_proj" + }, + "model.layers.7.self_attn.v_proj": { + "snr": 70.18457794189453, + "type": "self_attn.v_proj" + }, + "model.layers.8.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.9.self_attn.v_proj": { + "snr": 371.1153259277344, + "type": "self_attn.v_proj" + }, + "model.layers.10.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.11.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.12.self_attn.v_proj": { + "snr": 75.41203308105469, + "type": "self_attn.v_proj" + }, + "model.layers.13.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.14.self_attn.v_proj": { + "snr": 51.92624282836914, + "type": "self_attn.v_proj" + }, + "model.layers.15.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.16.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.17.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.18.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.19.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.20.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.21.self_attn.v_proj": { + "snr": 642.9313354492188, + "type": "self_attn.v_proj" + }, + "model.layers.22.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.23.self_attn.v_proj": { + "snr": 323.5724182128906, + "type": "self_attn.v_proj" + }, + "model.layers.24.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.25.self_attn.v_proj": { + "snr": 2.1736748218536377, + "type": "self_attn.v_proj" + }, + "model.layers.26.self_attn.v_proj": { + "snr": 3.1729259490966797, + "type": "self_attn.v_proj" + }, + "model.layers.27.self_attn.v_proj": { + "snr": 2.024953842163086, + "type": "self_attn.v_proj" + } +} diff --git a/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-3B-Instruct.json b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-3B-Instruct.json new file mode 100644 index 0000000000..6a67b14b3a --- /dev/null +++ b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-3B-Instruct.json @@ -0,0 +1,1310 @@ +{ + "model.layers.0.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.1.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.2.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.3.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.4.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.5.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.6.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.7.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.8.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.9.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.10.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.11.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.12.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.13.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.14.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.15.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.16.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.17.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.18.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.19.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.20.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.21.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.22.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.23.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.24.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.25.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.26.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.27.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.28.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.29.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.30.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.31.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.32.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.33.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.34.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.35.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "lm_head": { + "snr": Infinity, + "type": "lm_head" + }, + "model.layers.0.mlp.down_proj": { + "snr": 20.964319229125977, + "type": "mlp.down_proj" + }, + "model.layers.1.mlp.down_proj": { + "snr": 0.11561352014541626, + "type": "mlp.down_proj" + }, + "model.layers.2.mlp.down_proj": { + "snr": 0.14991413056850433, + "type": "mlp.down_proj" + }, + "model.layers.3.mlp.down_proj": { + "snr": 0.3673713207244873, + "type": "mlp.down_proj" + }, + "model.layers.4.mlp.down_proj": { + "snr": 0.5076134204864502, + "type": "mlp.down_proj" + }, + "model.layers.5.mlp.down_proj": { + "snr": 33.89468002319336, + "type": "mlp.down_proj" + }, + "model.layers.6.mlp.down_proj": { + "snr": 45.08732986450195, + "type": "mlp.down_proj" + }, + "model.layers.7.mlp.down_proj": { + "snr": 33.234222412109375, + "type": "mlp.down_proj" + }, + "model.layers.8.mlp.down_proj": { + "snr": 29.3447322845459, + "type": "mlp.down_proj" + }, + "model.layers.9.mlp.down_proj": { + "snr": 26.664169311523438, + "type": "mlp.down_proj" + }, + "model.layers.10.mlp.down_proj": { + "snr": 22.323949813842773, + "type": "mlp.down_proj" + }, + "model.layers.11.mlp.down_proj": { + "snr": 18.259737014770508, + "type": "mlp.down_proj" + }, + "model.layers.12.mlp.down_proj": { + "snr": 14.422037124633789, + "type": "mlp.down_proj" + }, + "model.layers.13.mlp.down_proj": { + "snr": 22.172054290771484, + "type": "mlp.down_proj" + }, + "model.layers.14.mlp.down_proj": { + "snr": 27.363698959350586, + "type": "mlp.down_proj" + }, + "model.layers.15.mlp.down_proj": { + "snr": 28.474334716796875, + "type": "mlp.down_proj" + }, + "model.layers.16.mlp.down_proj": { + "snr": 10.4143648147583, + "type": "mlp.down_proj" + }, + "model.layers.17.mlp.down_proj": { + "snr": 10.719133377075195, + "type": "mlp.down_proj" + }, + "model.layers.18.mlp.down_proj": { + "snr": 8.6494722366333, + "type": "mlp.down_proj" + }, + "model.layers.19.mlp.down_proj": { + "snr": 5.69321870803833, + "type": "mlp.down_proj" + }, + "model.layers.20.mlp.down_proj": { + "snr": 23.889677047729492, + "type": "mlp.down_proj" + }, + "model.layers.21.mlp.down_proj": { + "snr": 11.59121036529541, + "type": "mlp.down_proj" + }, + "model.layers.22.mlp.down_proj": { + "snr": 5.997435569763184, + "type": "mlp.down_proj" + }, + "model.layers.23.mlp.down_proj": { + "snr": 19.415578842163086, + "type": "mlp.down_proj" + }, + "model.layers.24.mlp.down_proj": { + "snr": 8.241704940795898, + "type": "mlp.down_proj" + }, + "model.layers.25.mlp.down_proj": { + "snr": 12.993823051452637, + "type": "mlp.down_proj" + }, + "model.layers.26.mlp.down_proj": { + "snr": 36.26508712768555, + "type": "mlp.down_proj" + }, + "model.layers.27.mlp.down_proj": { + "snr": 19.957971572875977, + "type": "mlp.down_proj" + }, + "model.layers.28.mlp.down_proj": { + "snr": 6.067765235900879, + "type": "mlp.down_proj" + }, + "model.layers.29.mlp.down_proj": { + "snr": 5.369481086730957, + "type": "mlp.down_proj" + }, + "model.layers.30.mlp.down_proj": { + "snr": 7.358774662017822, + "type": "mlp.down_proj" + }, + "model.layers.31.mlp.down_proj": { + "snr": 7.8687238693237305, + "type": "mlp.down_proj" + }, + "model.layers.32.mlp.down_proj": { + "snr": 8.713484764099121, + "type": "mlp.down_proj" + }, + "model.layers.33.mlp.down_proj": { + "snr": 21.233531951904297, + "type": "mlp.down_proj" + }, + "model.layers.34.mlp.down_proj": { + "snr": 32.37357711791992, + "type": "mlp.down_proj" + }, + "model.layers.35.mlp.down_proj": { + "snr": 179.8053741455078, + "type": "mlp.down_proj" + }, + "model.layers.0.mlp.gate_proj": { + "snr": 0.24989914894104004, + "type": "mlp.gate_proj" + }, + "model.layers.1.mlp.gate_proj": { + "snr": 0.11613649874925613, + "type": "mlp.gate_proj" + }, + "model.layers.2.mlp.gate_proj": { + "snr": 0.16354432702064514, + "type": "mlp.gate_proj" + }, + "model.layers.3.mlp.gate_proj": { + "snr": 0.36216047406196594, + "type": "mlp.gate_proj" + }, + "model.layers.4.mlp.gate_proj": { + "snr": 0.3485107719898224, + "type": "mlp.gate_proj" + }, + "model.layers.5.mlp.gate_proj": { + "snr": 2.6546616554260254, + "type": "mlp.gate_proj" + }, + "model.layers.6.mlp.gate_proj": { + "snr": 8.362885475158691, + "type": "mlp.gate_proj" + }, + "model.layers.7.mlp.gate_proj": { + "snr": 7.38665246963501, + "type": "mlp.gate_proj" + }, + "model.layers.8.mlp.gate_proj": { + "snr": 13.016111373901367, + "type": "mlp.gate_proj" + }, + "model.layers.9.mlp.gate_proj": { + "snr": 14.94902515411377, + "type": "mlp.gate_proj" + }, + "model.layers.10.mlp.gate_proj": { + "snr": 20.92418670654297, + "type": "mlp.gate_proj" + }, + "model.layers.11.mlp.gate_proj": { + "snr": 15.954015731811523, + "type": "mlp.gate_proj" + }, + "model.layers.12.mlp.gate_proj": { + "snr": 8.980009078979492, + "type": "mlp.gate_proj" + }, + "model.layers.13.mlp.gate_proj": { + "snr": 17.59958267211914, + "type": "mlp.gate_proj" + }, + "model.layers.14.mlp.gate_proj": { + "snr": 17.23070526123047, + "type": "mlp.gate_proj" + }, + "model.layers.15.mlp.gate_proj": { + "snr": 23.725330352783203, + "type": "mlp.gate_proj" + }, + "model.layers.16.mlp.gate_proj": { + "snr": 17.000444412231445, + "type": "mlp.gate_proj" + }, + "model.layers.17.mlp.gate_proj": { + "snr": 18.293012619018555, + "type": "mlp.gate_proj" + }, + "model.layers.18.mlp.gate_proj": { + "snr": 12.644190788269043, + "type": "mlp.gate_proj" + }, + "model.layers.19.mlp.gate_proj": { + "snr": 16.278690338134766, + "type": "mlp.gate_proj" + }, + "model.layers.20.mlp.gate_proj": { + "snr": 7.407368183135986, + "type": "mlp.gate_proj" + }, + "model.layers.21.mlp.gate_proj": { + "snr": 6.109912395477295, + "type": "mlp.gate_proj" + }, + "model.layers.22.mlp.gate_proj": { + "snr": 5.3692426681518555, + "type": "mlp.gate_proj" + }, + "model.layers.23.mlp.gate_proj": { + "snr": 9.354235649108887, + "type": "mlp.gate_proj" + }, + "model.layers.24.mlp.gate_proj": { + "snr": 7.655010223388672, + "type": "mlp.gate_proj" + }, + "model.layers.25.mlp.gate_proj": { + "snr": 6.252986431121826, + "type": "mlp.gate_proj" + }, + "model.layers.26.mlp.gate_proj": { + "snr": 14.26718521118164, + "type": "mlp.gate_proj" + }, + "model.layers.27.mlp.gate_proj": { + "snr": 7.705836772918701, + "type": "mlp.gate_proj" + }, + "model.layers.28.mlp.gate_proj": { + "snr": 5.998677730560303, + "type": "mlp.gate_proj" + }, + "model.layers.29.mlp.gate_proj": { + "snr": 6.044872760772705, + "type": "mlp.gate_proj" + }, + "model.layers.30.mlp.gate_proj": { + "snr": 9.027137756347656, + "type": "mlp.gate_proj" + }, + "model.layers.31.mlp.gate_proj": { + "snr": 5.449969291687012, + "type": "mlp.gate_proj" + }, + "model.layers.32.mlp.gate_proj": { + "snr": 4.206825256347656, + "type": "mlp.gate_proj" + }, + "model.layers.33.mlp.gate_proj": { + "snr": 5.22825288772583, + "type": "mlp.gate_proj" + }, + "model.layers.34.mlp.gate_proj": { + "snr": 43.71927261352539, + "type": "mlp.gate_proj" + }, + "model.layers.35.mlp.gate_proj": { + "snr": 45.37385177612305, + "type": "mlp.gate_proj" + }, + "model.layers.0.mlp.up_proj": { + "snr": 0.7069714665412903, + "type": "mlp.up_proj" + }, + "model.layers.1.mlp.up_proj": { + "snr": 0.17766596376895905, + "type": "mlp.up_proj" + }, + "model.layers.2.mlp.up_proj": { + "snr": 0.28577035665512085, + "type": "mlp.up_proj" + }, + "model.layers.3.mlp.up_proj": { + "snr": 0.6763099431991577, + "type": "mlp.up_proj" + }, + "model.layers.4.mlp.up_proj": { + "snr": 0.8340913653373718, + "type": "mlp.up_proj" + }, + "model.layers.5.mlp.up_proj": { + "snr": 3.946547031402588, + "type": "mlp.up_proj" + }, + "model.layers.6.mlp.up_proj": { + "snr": 19.56715202331543, + "type": "mlp.up_proj" + }, + "model.layers.7.mlp.up_proj": { + "snr": 36.21149826049805, + "type": "mlp.up_proj" + }, + "model.layers.8.mlp.up_proj": { + "snr": 44.28759002685547, + "type": "mlp.up_proj" + }, + "model.layers.9.mlp.up_proj": { + "snr": 45.47198486328125, + "type": "mlp.up_proj" + }, + "model.layers.10.mlp.up_proj": { + "snr": 79.00128936767578, + "type": "mlp.up_proj" + }, + "model.layers.11.mlp.up_proj": { + "snr": 52.28038787841797, + "type": "mlp.up_proj" + }, + "model.layers.12.mlp.up_proj": { + "snr": 48.08102035522461, + "type": "mlp.up_proj" + }, + "model.layers.13.mlp.up_proj": { + "snr": 56.071285247802734, + "type": "mlp.up_proj" + }, + "model.layers.14.mlp.up_proj": { + "snr": 72.24358367919922, + "type": "mlp.up_proj" + }, + "model.layers.15.mlp.up_proj": { + "snr": 54.818233489990234, + "type": "mlp.up_proj" + }, + "model.layers.16.mlp.up_proj": { + "snr": 47.251495361328125, + "type": "mlp.up_proj" + }, + "model.layers.17.mlp.up_proj": { + "snr": 51.585636138916016, + "type": "mlp.up_proj" + }, + "model.layers.18.mlp.up_proj": { + "snr": 43.47938919067383, + "type": "mlp.up_proj" + }, + "model.layers.19.mlp.up_proj": { + "snr": 38.132469177246094, + "type": "mlp.up_proj" + }, + "model.layers.20.mlp.up_proj": { + "snr": 21.78435707092285, + "type": "mlp.up_proj" + }, + "model.layers.21.mlp.up_proj": { + "snr": 22.261096954345703, + "type": "mlp.up_proj" + }, + "model.layers.22.mlp.up_proj": { + "snr": 30.751861572265625, + "type": "mlp.up_proj" + }, + "model.layers.23.mlp.up_proj": { + "snr": 28.61063575744629, + "type": "mlp.up_proj" + }, + "model.layers.24.mlp.up_proj": { + "snr": 20.21415901184082, + "type": "mlp.up_proj" + }, + "model.layers.25.mlp.up_proj": { + "snr": 20.759052276611328, + "type": "mlp.up_proj" + }, + "model.layers.26.mlp.up_proj": { + "snr": 33.80818557739258, + "type": "mlp.up_proj" + }, + "model.layers.27.mlp.up_proj": { + "snr": 17.274362564086914, + "type": "mlp.up_proj" + }, + "model.layers.28.mlp.up_proj": { + "snr": 13.943653106689453, + "type": "mlp.up_proj" + }, + "model.layers.29.mlp.up_proj": { + "snr": 16.202186584472656, + "type": "mlp.up_proj" + }, + "model.layers.30.mlp.up_proj": { + "snr": 24.25114631652832, + "type": "mlp.up_proj" + }, + "model.layers.31.mlp.up_proj": { + "snr": 10.68645191192627, + "type": "mlp.up_proj" + }, + "model.layers.32.mlp.up_proj": { + "snr": 5.7449774742126465, + "type": "mlp.up_proj" + }, + "model.layers.33.mlp.up_proj": { + "snr": 11.879876136779785, + "type": "mlp.up_proj" + }, + "model.layers.34.mlp.up_proj": { + "snr": 25.948715209960938, + "type": "mlp.up_proj" + }, + "model.layers.35.mlp.up_proj": { + "snr": 38.63526153564453, + "type": "mlp.up_proj" + }, + "model.embed_tokens": { + "snr": Infinity, + "type": "model.embed_tokens" + }, + "model.norm": { + "snr": Infinity, + "type": "model.norm" + }, + "model.layers.0.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.1.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.2.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.3.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.4.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.5.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.6.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.7.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.8.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.9.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.10.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.11.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.12.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.13.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.14.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.15.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.16.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.17.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.18.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.19.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.20.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.21.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.22.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.23.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.24.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.25.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.26.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.27.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.28.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.29.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.30.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.31.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.32.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.33.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.34.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.35.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.0.self_attn.k_proj": { + "snr": 12.243099212646484, + "type": "self_attn.k_proj" + }, + "model.layers.1.self_attn.k_proj": { + "snr": 0.6446183323860168, + "type": "self_attn.k_proj" + }, + "model.layers.2.self_attn.k_proj": { + "snr": 0.7159711718559265, + "type": "self_attn.k_proj" + }, + "model.layers.3.self_attn.k_proj": { + "snr": 5.5100932121276855, + "type": "self_attn.k_proj" + }, + "model.layers.4.self_attn.k_proj": { + "snr": 3.0802414417266846, + "type": "self_attn.k_proj" + }, + "model.layers.5.self_attn.k_proj": { + "snr": 1.0472767353057861, + "type": "self_attn.k_proj" + }, + "model.layers.6.self_attn.k_proj": { + "snr": 3.576918601989746, + "type": "self_attn.k_proj" + }, + "model.layers.7.self_attn.k_proj": { + "snr": 3.3793225288391113, + "type": "self_attn.k_proj" + }, + "model.layers.8.self_attn.k_proj": { + "snr": 2.9598212242126465, + "type": "self_attn.k_proj" + }, + "model.layers.9.self_attn.k_proj": { + "snr": 6.102792263031006, + "type": "self_attn.k_proj" + }, + "model.layers.10.self_attn.k_proj": { + "snr": 2.231630325317383, + "type": "self_attn.k_proj" + }, + "model.layers.11.self_attn.k_proj": { + "snr": 2.176372766494751, + "type": "self_attn.k_proj" + }, + "model.layers.12.self_attn.k_proj": { + "snr": 1.3229435682296753, + "type": "self_attn.k_proj" + }, + "model.layers.13.self_attn.k_proj": { + "snr": 2.6183862686157227, + "type": "self_attn.k_proj" + }, + "model.layers.14.self_attn.k_proj": { + "snr": 2.608288526535034, + "type": "self_attn.k_proj" + }, + "model.layers.15.self_attn.k_proj": { + "snr": 1.5090984106063843, + "type": "self_attn.k_proj" + }, + "model.layers.16.self_attn.k_proj": { + "snr": 1.284422516822815, + "type": "self_attn.k_proj" + }, + "model.layers.17.self_attn.k_proj": { + "snr": 0.8903945088386536, + "type": "self_attn.k_proj" + }, + "model.layers.18.self_attn.k_proj": { + "snr": 1.8880385160446167, + "type": "self_attn.k_proj" + }, + "model.layers.19.self_attn.k_proj": { + "snr": 0.8905735015869141, + "type": "self_attn.k_proj" + }, + "model.layers.20.self_attn.k_proj": { + "snr": 0.9060881733894348, + "type": "self_attn.k_proj" + }, + "model.layers.21.self_attn.k_proj": { + "snr": 0.7572551965713501, + "type": "self_attn.k_proj" + }, + "model.layers.22.self_attn.k_proj": { + "snr": 0.940827488899231, + "type": "self_attn.k_proj" + }, + "model.layers.23.self_attn.k_proj": { + "snr": 3.7776191234588623, + "type": "self_attn.k_proj" + }, + "model.layers.24.self_attn.k_proj": { + "snr": 1.328923225402832, + "type": "self_attn.k_proj" + }, + "model.layers.25.self_attn.k_proj": { + "snr": 1.3986345529556274, + "type": "self_attn.k_proj" + }, + "model.layers.26.self_attn.k_proj": { + "snr": 1.2436336278915405, + "type": "self_attn.k_proj" + }, + "model.layers.27.self_attn.k_proj": { + "snr": 0.7737217545509338, + "type": "self_attn.k_proj" + }, + "model.layers.28.self_attn.k_proj": { + "snr": 2.6027626991271973, + "type": "self_attn.k_proj" + }, + "model.layers.29.self_attn.k_proj": { + "snr": 2.2332751750946045, + "type": "self_attn.k_proj" + }, + "model.layers.30.self_attn.k_proj": { + "snr": 2.476585626602173, + "type": "self_attn.k_proj" + }, + "model.layers.31.self_attn.k_proj": { + "snr": 1.1115432977676392, + "type": "self_attn.k_proj" + }, + "model.layers.32.self_attn.k_proj": { + "snr": 0.8251476287841797, + "type": "self_attn.k_proj" + }, + "model.layers.33.self_attn.k_proj": { + "snr": 0.9331105947494507, + "type": "self_attn.k_proj" + }, + "model.layers.34.self_attn.k_proj": { + "snr": 6.602395534515381, + "type": "self_attn.k_proj" + }, + "model.layers.35.self_attn.k_proj": { + "snr": 10.151693344116211, + "type": "self_attn.k_proj" + }, + "model.layers.0.self_attn.o_proj": { + "snr": 0.3661542534828186, + "type": "self_attn.o_proj" + }, + "model.layers.1.self_attn.o_proj": { + "snr": 0.19571374356746674, + "type": "self_attn.o_proj" + }, + "model.layers.2.self_attn.o_proj": { + "snr": 0.2244851142168045, + "type": "self_attn.o_proj" + }, + "model.layers.3.self_attn.o_proj": { + "snr": 0.2593664526939392, + "type": "self_attn.o_proj" + }, + "model.layers.4.self_attn.o_proj": { + "snr": 0.2569783926010132, + "type": "self_attn.o_proj" + }, + "model.layers.5.self_attn.o_proj": { + "snr": 0.2564302980899811, + "type": "self_attn.o_proj" + }, + "model.layers.6.self_attn.o_proj": { + "snr": 0.18539844453334808, + "type": "self_attn.o_proj" + }, + "model.layers.7.self_attn.o_proj": { + "snr": 0.2328651398420334, + "type": "self_attn.o_proj" + }, + "model.layers.8.self_attn.o_proj": { + "snr": 0.22055882215499878, + "type": "self_attn.o_proj" + }, + "model.layers.9.self_attn.o_proj": { + "snr": 0.21800543367862701, + "type": "self_attn.o_proj" + }, + "model.layers.10.self_attn.o_proj": { + "snr": 0.22867777943611145, + "type": "self_attn.o_proj" + }, + "model.layers.11.self_attn.o_proj": { + "snr": 0.23986175656318665, + "type": "self_attn.o_proj" + }, + "model.layers.12.self_attn.o_proj": { + "snr": 0.17598563432693481, + "type": "self_attn.o_proj" + }, + "model.layers.13.self_attn.o_proj": { + "snr": 0.20469218492507935, + "type": "self_attn.o_proj" + }, + "model.layers.14.self_attn.o_proj": { + "snr": 0.21040217578411102, + "type": "self_attn.o_proj" + }, + "model.layers.15.self_attn.o_proj": { + "snr": 0.23787625133991241, + "type": "self_attn.o_proj" + }, + "model.layers.16.self_attn.o_proj": { + "snr": 0.16339677572250366, + "type": "self_attn.o_proj" + }, + "model.layers.17.self_attn.o_proj": { + "snr": 0.2070712298154831, + "type": "self_attn.o_proj" + }, + "model.layers.18.self_attn.o_proj": { + "snr": 0.1826934814453125, + "type": "self_attn.o_proj" + }, + "model.layers.19.self_attn.o_proj": { + "snr": 0.19459959864616394, + "type": "self_attn.o_proj" + }, + "model.layers.20.self_attn.o_proj": { + "snr": 0.2668156027793884, + "type": "self_attn.o_proj" + }, + "model.layers.21.self_attn.o_proj": { + "snr": 0.16906610131263733, + "type": "self_attn.o_proj" + }, + "model.layers.22.self_attn.o_proj": { + "snr": 0.18790249526500702, + "type": "self_attn.o_proj" + }, + "model.layers.23.self_attn.o_proj": { + "snr": 0.18883933126926422, + "type": "self_attn.o_proj" + }, + "model.layers.24.self_attn.o_proj": { + "snr": 0.1793188899755478, + "type": "self_attn.o_proj" + }, + "model.layers.25.self_attn.o_proj": { + "snr": 0.1800570785999298, + "type": "self_attn.o_proj" + }, + "model.layers.26.self_attn.o_proj": { + "snr": 0.17790433764457703, + "type": "self_attn.o_proj" + }, + "model.layers.27.self_attn.o_proj": { + "snr": 0.2029498964548111, + "type": "self_attn.o_proj" + }, + "model.layers.28.self_attn.o_proj": { + "snr": 0.17044201493263245, + "type": "self_attn.o_proj" + }, + "model.layers.29.self_attn.o_proj": { + "snr": 0.19938386976718903, + "type": "self_attn.o_proj" + }, + "model.layers.30.self_attn.o_proj": { + "snr": 0.23108959197998047, + "type": "self_attn.o_proj" + }, + "model.layers.31.self_attn.o_proj": { + "snr": 0.16427059471607208, + "type": "self_attn.o_proj" + }, + "model.layers.32.self_attn.o_proj": { + "snr": 0.10631092637777328, + "type": "self_attn.o_proj" + }, + "model.layers.33.self_attn.o_proj": { + "snr": 0.09417019784450531, + "type": "self_attn.o_proj" + }, + "model.layers.34.self_attn.o_proj": { + "snr": 0.1324978619813919, + "type": "self_attn.o_proj" + }, + "model.layers.35.self_attn.o_proj": { + "snr": 0.11784011125564575, + "type": "self_attn.o_proj" + }, + "model.layers.0.self_attn.q_proj": { + "snr": 0.05565479397773743, + "type": "self_attn.q_proj" + }, + "model.layers.1.self_attn.q_proj": { + "snr": 0.138458251953125, + "type": "self_attn.q_proj" + }, + "model.layers.2.self_attn.q_proj": { + "snr": 0.12992437183856964, + "type": "self_attn.q_proj" + }, + "model.layers.3.self_attn.q_proj": { + "snr": 0.15362468361854553, + "type": "self_attn.q_proj" + }, + "model.layers.4.self_attn.q_proj": { + "snr": 0.1563446819782257, + "type": "self_attn.q_proj" + }, + "model.layers.5.self_attn.q_proj": { + "snr": 0.15544593334197998, + "type": "self_attn.q_proj" + }, + "model.layers.6.self_attn.q_proj": { + "snr": 0.15956827998161316, + "type": "self_attn.q_proj" + }, + "model.layers.7.self_attn.q_proj": { + "snr": 0.17549948394298553, + "type": "self_attn.q_proj" + }, + "model.layers.8.self_attn.q_proj": { + "snr": 0.16668449342250824, + "type": "self_attn.q_proj" + }, + "model.layers.9.self_attn.q_proj": { + "snr": 0.15626586973667145, + "type": "self_attn.q_proj" + }, + "model.layers.10.self_attn.q_proj": { + "snr": 0.18318884074687958, + "type": "self_attn.q_proj" + }, + "model.layers.11.self_attn.q_proj": { + "snr": 0.171547532081604, + "type": "self_attn.q_proj" + }, + "model.layers.12.self_attn.q_proj": { + "snr": 0.18164905905723572, + "type": "self_attn.q_proj" + }, + "model.layers.13.self_attn.q_proj": { + "snr": 0.2091975212097168, + "type": "self_attn.q_proj" + }, + "model.layers.14.self_attn.q_proj": { + "snr": 0.17431670427322388, + "type": "self_attn.q_proj" + }, + "model.layers.15.self_attn.q_proj": { + "snr": 0.20902502536773682, + "type": "self_attn.q_proj" + }, + "model.layers.16.self_attn.q_proj": { + "snr": 0.15439842641353607, + "type": "self_attn.q_proj" + }, + "model.layers.17.self_attn.q_proj": { + "snr": 0.1945274919271469, + "type": "self_attn.q_proj" + }, + "model.layers.18.self_attn.q_proj": { + "snr": 0.18916545808315277, + "type": "self_attn.q_proj" + }, + "model.layers.19.self_attn.q_proj": { + "snr": 0.20778712630271912, + "type": "self_attn.q_proj" + }, + "model.layers.20.self_attn.q_proj": { + "snr": 0.20866931974887848, + "type": "self_attn.q_proj" + }, + "model.layers.21.self_attn.q_proj": { + "snr": 0.1900305300951004, + "type": "self_attn.q_proj" + }, + "model.layers.22.self_attn.q_proj": { + "snr": 0.18200653791427612, + "type": "self_attn.q_proj" + }, + "model.layers.23.self_attn.q_proj": { + "snr": 0.2070988416671753, + "type": "self_attn.q_proj" + }, + "model.layers.24.self_attn.q_proj": { + "snr": 0.1845332235097885, + "type": "self_attn.q_proj" + }, + "model.layers.25.self_attn.q_proj": { + "snr": 0.20868781208992004, + "type": "self_attn.q_proj" + }, + "model.layers.26.self_attn.q_proj": { + "snr": 0.19242744147777557, + "type": "self_attn.q_proj" + }, + "model.layers.27.self_attn.q_proj": { + "snr": 0.15225112438201904, + "type": "self_attn.q_proj" + }, + "model.layers.28.self_attn.q_proj": { + "snr": 0.20065009593963623, + "type": "self_attn.q_proj" + }, + "model.layers.29.self_attn.q_proj": { + "snr": 0.19390477240085602, + "type": "self_attn.q_proj" + }, + "model.layers.30.self_attn.q_proj": { + "snr": 0.18538697063922882, + "type": "self_attn.q_proj" + }, + "model.layers.31.self_attn.q_proj": { + "snr": 0.18954339623451233, + "type": "self_attn.q_proj" + }, + "model.layers.32.self_attn.q_proj": { + "snr": 0.20089596509933472, + "type": "self_attn.q_proj" + }, + "model.layers.33.self_attn.q_proj": { + "snr": 0.19814996421337128, + "type": "self_attn.q_proj" + }, + "model.layers.34.self_attn.q_proj": { + "snr": 0.17733213305473328, + "type": "self_attn.q_proj" + }, + "model.layers.35.self_attn.q_proj": { + "snr": 0.14075976610183716, + "type": "self_attn.q_proj" + }, + "model.layers.0.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.1.self_attn.v_proj": { + "snr": 845.8053588867188, + "type": "self_attn.v_proj" + }, + "model.layers.2.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.3.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.4.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.5.self_attn.v_proj": { + "snr": 83.97241973876953, + "type": "self_attn.v_proj" + }, + "model.layers.6.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.7.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.8.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.9.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.10.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.11.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.12.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.13.self_attn.v_proj": { + "snr": 213.70960998535156, + "type": "self_attn.v_proj" + }, + "model.layers.14.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.15.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.16.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.17.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.18.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.19.self_attn.v_proj": { + "snr": 18.950267791748047, + "type": "self_attn.v_proj" + }, + "model.layers.20.self_attn.v_proj": { + "snr": 435.8339538574219, + "type": "self_attn.v_proj" + }, + "model.layers.21.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.22.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.23.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.24.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.25.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.26.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.27.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.28.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.29.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.30.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.31.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.32.self_attn.v_proj": { + "snr": 1.2341279983520508, + "type": "self_attn.v_proj" + }, + "model.layers.33.self_attn.v_proj": { + "snr": 0.6158654689788818, + "type": "self_attn.v_proj" + }, + "model.layers.34.self_attn.v_proj": { + "snr": 509.3221130371094, + "type": "self_attn.v_proj" + }, + "model.layers.35.self_attn.v_proj": { + "snr": 538.6658325195312, + "type": "self_attn.v_proj" + } +} diff --git a/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-3B.json b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-3B.json new file mode 100644 index 0000000000..93b5cfec61 --- /dev/null +++ b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-3B.json @@ -0,0 +1,1310 @@ +{ + "model.layers.0.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.1.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.2.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.3.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.4.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.5.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.6.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.7.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.8.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.9.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.10.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.11.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.12.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.13.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.14.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.15.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.16.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.17.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.18.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.19.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.20.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.21.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.22.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.23.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.24.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.25.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.26.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.27.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.28.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.29.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.30.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.31.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.32.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.33.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.34.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.35.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "lm_head": { + "snr": Infinity, + "type": "lm_head" + }, + "model.layers.0.mlp.down_proj": { + "snr": 20.942785263061523, + "type": "mlp.down_proj" + }, + "model.layers.1.mlp.down_proj": { + "snr": 0.11550866067409515, + "type": "mlp.down_proj" + }, + "model.layers.2.mlp.down_proj": { + "snr": 0.14981402456760406, + "type": "mlp.down_proj" + }, + "model.layers.3.mlp.down_proj": { + "snr": 0.36719316244125366, + "type": "mlp.down_proj" + }, + "model.layers.4.mlp.down_proj": { + "snr": 0.5072987079620361, + "type": "mlp.down_proj" + }, + "model.layers.5.mlp.down_proj": { + "snr": 33.86688232421875, + "type": "mlp.down_proj" + }, + "model.layers.6.mlp.down_proj": { + "snr": 45.066246032714844, + "type": "mlp.down_proj" + }, + "model.layers.7.mlp.down_proj": { + "snr": 33.20981979370117, + "type": "mlp.down_proj" + }, + "model.layers.8.mlp.down_proj": { + "snr": 29.310104370117188, + "type": "mlp.down_proj" + }, + "model.layers.9.mlp.down_proj": { + "snr": 26.638381958007812, + "type": "mlp.down_proj" + }, + "model.layers.10.mlp.down_proj": { + "snr": 22.302486419677734, + "type": "mlp.down_proj" + }, + "model.layers.11.mlp.down_proj": { + "snr": 18.249290466308594, + "type": "mlp.down_proj" + }, + "model.layers.12.mlp.down_proj": { + "snr": 14.057564735412598, + "type": "mlp.down_proj" + }, + "model.layers.13.mlp.down_proj": { + "snr": 22.154281616210938, + "type": "mlp.down_proj" + }, + "model.layers.14.mlp.down_proj": { + "snr": 27.348575592041016, + "type": "mlp.down_proj" + }, + "model.layers.15.mlp.down_proj": { + "snr": 28.447378158569336, + "type": "mlp.down_proj" + }, + "model.layers.16.mlp.down_proj": { + "snr": 10.405216217041016, + "type": "mlp.down_proj" + }, + "model.layers.17.mlp.down_proj": { + "snr": 10.71042251586914, + "type": "mlp.down_proj" + }, + "model.layers.18.mlp.down_proj": { + "snr": 8.642854690551758, + "type": "mlp.down_proj" + }, + "model.layers.19.mlp.down_proj": { + "snr": 5.690433979034424, + "type": "mlp.down_proj" + }, + "model.layers.20.mlp.down_proj": { + "snr": 23.869070053100586, + "type": "mlp.down_proj" + }, + "model.layers.21.mlp.down_proj": { + "snr": 11.584356307983398, + "type": "mlp.down_proj" + }, + "model.layers.22.mlp.down_proj": { + "snr": 5.992950916290283, + "type": "mlp.down_proj" + }, + "model.layers.23.mlp.down_proj": { + "snr": 18.495361328125, + "type": "mlp.down_proj" + }, + "model.layers.24.mlp.down_proj": { + "snr": 8.233827590942383, + "type": "mlp.down_proj" + }, + "model.layers.25.mlp.down_proj": { + "snr": 12.626734733581543, + "type": "mlp.down_proj" + }, + "model.layers.26.mlp.down_proj": { + "snr": 36.21802520751953, + "type": "mlp.down_proj" + }, + "model.layers.27.mlp.down_proj": { + "snr": 19.932941436767578, + "type": "mlp.down_proj" + }, + "model.layers.28.mlp.down_proj": { + "snr": 6.0616455078125, + "type": "mlp.down_proj" + }, + "model.layers.29.mlp.down_proj": { + "snr": 5.363720417022705, + "type": "mlp.down_proj" + }, + "model.layers.30.mlp.down_proj": { + "snr": 7.455615520477295, + "type": "mlp.down_proj" + }, + "model.layers.31.mlp.down_proj": { + "snr": 7.8631815910339355, + "type": "mlp.down_proj" + }, + "model.layers.32.mlp.down_proj": { + "snr": 8.706913948059082, + "type": "mlp.down_proj" + }, + "model.layers.33.mlp.down_proj": { + "snr": 21.220134735107422, + "type": "mlp.down_proj" + }, + "model.layers.34.mlp.down_proj": { + "snr": 32.33852005004883, + "type": "mlp.down_proj" + }, + "model.layers.35.mlp.down_proj": { + "snr": 179.8906707763672, + "type": "mlp.down_proj" + }, + "model.layers.0.mlp.gate_proj": { + "snr": 0.24970805644989014, + "type": "mlp.gate_proj" + }, + "model.layers.1.mlp.gate_proj": { + "snr": 0.11607512086629868, + "type": "mlp.gate_proj" + }, + "model.layers.2.mlp.gate_proj": { + "snr": 0.16310769319534302, + "type": "mlp.gate_proj" + }, + "model.layers.3.mlp.gate_proj": { + "snr": 0.3621424436569214, + "type": "mlp.gate_proj" + }, + "model.layers.4.mlp.gate_proj": { + "snr": 0.3482637107372284, + "type": "mlp.gate_proj" + }, + "model.layers.5.mlp.gate_proj": { + "snr": 2.6533455848693848, + "type": "mlp.gate_proj" + }, + "model.layers.6.mlp.gate_proj": { + "snr": 8.359040260314941, + "type": "mlp.gate_proj" + }, + "model.layers.7.mlp.gate_proj": { + "snr": 7.382037162780762, + "type": "mlp.gate_proj" + }, + "model.layers.8.mlp.gate_proj": { + "snr": 13.00683879852295, + "type": "mlp.gate_proj" + }, + "model.layers.9.mlp.gate_proj": { + "snr": 14.936161994934082, + "type": "mlp.gate_proj" + }, + "model.layers.10.mlp.gate_proj": { + "snr": 20.907283782958984, + "type": "mlp.gate_proj" + }, + "model.layers.11.mlp.gate_proj": { + "snr": 15.941497802734375, + "type": "mlp.gate_proj" + }, + "model.layers.12.mlp.gate_proj": { + "snr": 8.97419548034668, + "type": "mlp.gate_proj" + }, + "model.layers.13.mlp.gate_proj": { + "snr": 17.585100173950195, + "type": "mlp.gate_proj" + }, + "model.layers.14.mlp.gate_proj": { + "snr": 17.21462059020996, + "type": "mlp.gate_proj" + }, + "model.layers.15.mlp.gate_proj": { + "snr": 23.703285217285156, + "type": "mlp.gate_proj" + }, + "model.layers.16.mlp.gate_proj": { + "snr": 16.986576080322266, + "type": "mlp.gate_proj" + }, + "model.layers.17.mlp.gate_proj": { + "snr": 18.27729606628418, + "type": "mlp.gate_proj" + }, + "model.layers.18.mlp.gate_proj": { + "snr": 12.63351058959961, + "type": "mlp.gate_proj" + }, + "model.layers.19.mlp.gate_proj": { + "snr": 16.2633113861084, + "type": "mlp.gate_proj" + }, + "model.layers.20.mlp.gate_proj": { + "snr": 7.399787902832031, + "type": "mlp.gate_proj" + }, + "model.layers.21.mlp.gate_proj": { + "snr": 6.10424280166626, + "type": "mlp.gate_proj" + }, + "model.layers.22.mlp.gate_proj": { + "snr": 5.363350868225098, + "type": "mlp.gate_proj" + }, + "model.layers.23.mlp.gate_proj": { + "snr": 9.344535827636719, + "type": "mlp.gate_proj" + }, + "model.layers.24.mlp.gate_proj": { + "snr": 7.647364616394043, + "type": "mlp.gate_proj" + }, + "model.layers.25.mlp.gate_proj": { + "snr": 6.143579959869385, + "type": "mlp.gate_proj" + }, + "model.layers.26.mlp.gate_proj": { + "snr": 14.254817008972168, + "type": "mlp.gate_proj" + }, + "model.layers.27.mlp.gate_proj": { + "snr": 7.7000861167907715, + "type": "mlp.gate_proj" + }, + "model.layers.28.mlp.gate_proj": { + "snr": 5.994422435760498, + "type": "mlp.gate_proj" + }, + "model.layers.29.mlp.gate_proj": { + "snr": 6.041909694671631, + "type": "mlp.gate_proj" + }, + "model.layers.30.mlp.gate_proj": { + "snr": 9.027522087097168, + "type": "mlp.gate_proj" + }, + "model.layers.31.mlp.gate_proj": { + "snr": 5.450753211975098, + "type": "mlp.gate_proj" + }, + "model.layers.32.mlp.gate_proj": { + "snr": 4.149200439453125, + "type": "mlp.gate_proj" + }, + "model.layers.33.mlp.gate_proj": { + "snr": 5.223763942718506, + "type": "mlp.gate_proj" + }, + "model.layers.34.mlp.gate_proj": { + "snr": 43.65521240234375, + "type": "mlp.gate_proj" + }, + "model.layers.35.mlp.gate_proj": { + "snr": 45.312774658203125, + "type": "mlp.gate_proj" + }, + "model.layers.0.mlp.up_proj": { + "snr": 0.7065013647079468, + "type": "mlp.up_proj" + }, + "model.layers.1.mlp.up_proj": { + "snr": 0.17752516269683838, + "type": "mlp.up_proj" + }, + "model.layers.2.mlp.up_proj": { + "snr": 0.2847473919391632, + "type": "mlp.up_proj" + }, + "model.layers.3.mlp.up_proj": { + "snr": 0.6757690906524658, + "type": "mlp.up_proj" + }, + "model.layers.4.mlp.up_proj": { + "snr": 0.8353318572044373, + "type": "mlp.up_proj" + }, + "model.layers.5.mlp.up_proj": { + "snr": 3.940711736679077, + "type": "mlp.up_proj" + }, + "model.layers.6.mlp.up_proj": { + "snr": 19.556047439575195, + "type": "mlp.up_proj" + }, + "model.layers.7.mlp.up_proj": { + "snr": 36.19340515136719, + "type": "mlp.up_proj" + }, + "model.layers.8.mlp.up_proj": { + "snr": 44.2518424987793, + "type": "mlp.up_proj" + }, + "model.layers.9.mlp.up_proj": { + "snr": 45.418025970458984, + "type": "mlp.up_proj" + }, + "model.layers.10.mlp.up_proj": { + "snr": 78.90928649902344, + "type": "mlp.up_proj" + }, + "model.layers.11.mlp.up_proj": { + "snr": 52.24648666381836, + "type": "mlp.up_proj" + }, + "model.layers.12.mlp.up_proj": { + "snr": 48.02030563354492, + "type": "mlp.up_proj" + }, + "model.layers.13.mlp.up_proj": { + "snr": 56.016239166259766, + "type": "mlp.up_proj" + }, + "model.layers.14.mlp.up_proj": { + "snr": 72.16619873046875, + "type": "mlp.up_proj" + }, + "model.layers.15.mlp.up_proj": { + "snr": 54.75283432006836, + "type": "mlp.up_proj" + }, + "model.layers.16.mlp.up_proj": { + "snr": 47.204097747802734, + "type": "mlp.up_proj" + }, + "model.layers.17.mlp.up_proj": { + "snr": 51.549312591552734, + "type": "mlp.up_proj" + }, + "model.layers.18.mlp.up_proj": { + "snr": 43.43872833251953, + "type": "mlp.up_proj" + }, + "model.layers.19.mlp.up_proj": { + "snr": 38.09785461425781, + "type": "mlp.up_proj" + }, + "model.layers.20.mlp.up_proj": { + "snr": 21.767858505249023, + "type": "mlp.up_proj" + }, + "model.layers.21.mlp.up_proj": { + "snr": 22.243661880493164, + "type": "mlp.up_proj" + }, + "model.layers.22.mlp.up_proj": { + "snr": 30.71843147277832, + "type": "mlp.up_proj" + }, + "model.layers.23.mlp.up_proj": { + "snr": 28.5756778717041, + "type": "mlp.up_proj" + }, + "model.layers.24.mlp.up_proj": { + "snr": 20.186717987060547, + "type": "mlp.up_proj" + }, + "model.layers.25.mlp.up_proj": { + "snr": 20.742860794067383, + "type": "mlp.up_proj" + }, + "model.layers.26.mlp.up_proj": { + "snr": 33.777984619140625, + "type": "mlp.up_proj" + }, + "model.layers.27.mlp.up_proj": { + "snr": 17.254213333129883, + "type": "mlp.up_proj" + }, + "model.layers.28.mlp.up_proj": { + "snr": 13.930026054382324, + "type": "mlp.up_proj" + }, + "model.layers.29.mlp.up_proj": { + "snr": 16.17984390258789, + "type": "mlp.up_proj" + }, + "model.layers.30.mlp.up_proj": { + "snr": 24.236648559570312, + "type": "mlp.up_proj" + }, + "model.layers.31.mlp.up_proj": { + "snr": 10.665648460388184, + "type": "mlp.up_proj" + }, + "model.layers.32.mlp.up_proj": { + "snr": 5.735939025878906, + "type": "mlp.up_proj" + }, + "model.layers.33.mlp.up_proj": { + "snr": 11.592061042785645, + "type": "mlp.up_proj" + }, + "model.layers.34.mlp.up_proj": { + "snr": 25.923419952392578, + "type": "mlp.up_proj" + }, + "model.layers.35.mlp.up_proj": { + "snr": 38.579349517822266, + "type": "mlp.up_proj" + }, + "model.embed_tokens": { + "snr": Infinity, + "type": "model.embed_tokens" + }, + "model.norm": { + "snr": Infinity, + "type": "model.norm" + }, + "model.layers.0.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.1.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.2.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.3.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.4.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.5.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.6.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.7.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.8.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.9.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.10.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.11.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.12.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.13.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.14.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.15.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.16.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.17.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.18.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.19.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.20.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.21.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.22.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.23.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.24.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.25.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.26.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.27.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.28.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.29.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.30.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.31.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.32.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.33.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.34.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.35.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.0.self_attn.k_proj": { + "snr": 12.24727725982666, + "type": "self_attn.k_proj" + }, + "model.layers.1.self_attn.k_proj": { + "snr": 0.6436238288879395, + "type": "self_attn.k_proj" + }, + "model.layers.2.self_attn.k_proj": { + "snr": 0.7156716585159302, + "type": "self_attn.k_proj" + }, + "model.layers.3.self_attn.k_proj": { + "snr": 5.505439758300781, + "type": "self_attn.k_proj" + }, + "model.layers.4.self_attn.k_proj": { + "snr": 3.0760715007781982, + "type": "self_attn.k_proj" + }, + "model.layers.5.self_attn.k_proj": { + "snr": 1.0453941822052002, + "type": "self_attn.k_proj" + }, + "model.layers.6.self_attn.k_proj": { + "snr": 3.57472562789917, + "type": "self_attn.k_proj" + }, + "model.layers.7.self_attn.k_proj": { + "snr": 3.3765170574188232, + "type": "self_attn.k_proj" + }, + "model.layers.8.self_attn.k_proj": { + "snr": 2.8859639167785645, + "type": "self_attn.k_proj" + }, + "model.layers.9.self_attn.k_proj": { + "snr": 6.09852409362793, + "type": "self_attn.k_proj" + }, + "model.layers.10.self_attn.k_proj": { + "snr": 2.229580879211426, + "type": "self_attn.k_proj" + }, + "model.layers.11.self_attn.k_proj": { + "snr": 2.173879623413086, + "type": "self_attn.k_proj" + }, + "model.layers.12.self_attn.k_proj": { + "snr": 1.3220131397247314, + "type": "self_attn.k_proj" + }, + "model.layers.13.self_attn.k_proj": { + "snr": 2.61668062210083, + "type": "self_attn.k_proj" + }, + "model.layers.14.self_attn.k_proj": { + "snr": 2.606799840927124, + "type": "self_attn.k_proj" + }, + "model.layers.15.self_attn.k_proj": { + "snr": 1.5080311298370361, + "type": "self_attn.k_proj" + }, + "model.layers.16.self_attn.k_proj": { + "snr": 1.2841484546661377, + "type": "self_attn.k_proj" + }, + "model.layers.17.self_attn.k_proj": { + "snr": 0.8896433115005493, + "type": "self_attn.k_proj" + }, + "model.layers.18.self_attn.k_proj": { + "snr": 1.8873414993286133, + "type": "self_attn.k_proj" + }, + "model.layers.19.self_attn.k_proj": { + "snr": 0.8897770643234253, + "type": "self_attn.k_proj" + }, + "model.layers.20.self_attn.k_proj": { + "snr": 0.9051405787467957, + "type": "self_attn.k_proj" + }, + "model.layers.21.self_attn.k_proj": { + "snr": 0.7568970322608948, + "type": "self_attn.k_proj" + }, + "model.layers.22.self_attn.k_proj": { + "snr": 0.9403582811355591, + "type": "self_attn.k_proj" + }, + "model.layers.23.self_attn.k_proj": { + "snr": 3.777062177658081, + "type": "self_attn.k_proj" + }, + "model.layers.24.self_attn.k_proj": { + "snr": 1.3280683755874634, + "type": "self_attn.k_proj" + }, + "model.layers.25.self_attn.k_proj": { + "snr": 1.3980307579040527, + "type": "self_attn.k_proj" + }, + "model.layers.26.self_attn.k_proj": { + "snr": 1.2435240745544434, + "type": "self_attn.k_proj" + }, + "model.layers.27.self_attn.k_proj": { + "snr": 0.7732619047164917, + "type": "self_attn.k_proj" + }, + "model.layers.28.self_attn.k_proj": { + "snr": 2.6010243892669678, + "type": "self_attn.k_proj" + }, + "model.layers.29.self_attn.k_proj": { + "snr": 2.232773780822754, + "type": "self_attn.k_proj" + }, + "model.layers.30.self_attn.k_proj": { + "snr": 2.4743099212646484, + "type": "self_attn.k_proj" + }, + "model.layers.31.self_attn.k_proj": { + "snr": 1.11082923412323, + "type": "self_attn.k_proj" + }, + "model.layers.32.self_attn.k_proj": { + "snr": 0.8243986368179321, + "type": "self_attn.k_proj" + }, + "model.layers.33.self_attn.k_proj": { + "snr": 0.932928204536438, + "type": "self_attn.k_proj" + }, + "model.layers.34.self_attn.k_proj": { + "snr": 6.608611583709717, + "type": "self_attn.k_proj" + }, + "model.layers.35.self_attn.k_proj": { + "snr": 10.160987854003906, + "type": "self_attn.k_proj" + }, + "model.layers.0.self_attn.o_proj": { + "snr": 0.36662933230400085, + "type": "self_attn.o_proj" + }, + "model.layers.1.self_attn.o_proj": { + "snr": 0.1955128312110901, + "type": "self_attn.o_proj" + }, + "model.layers.2.self_attn.o_proj": { + "snr": 0.22419843077659607, + "type": "self_attn.o_proj" + }, + "model.layers.3.self_attn.o_proj": { + "snr": 0.25902292132377625, + "type": "self_attn.o_proj" + }, + "model.layers.4.self_attn.o_proj": { + "snr": 0.2567676901817322, + "type": "self_attn.o_proj" + }, + "model.layers.5.self_attn.o_proj": { + "snr": 0.2560890316963196, + "type": "self_attn.o_proj" + }, + "model.layers.6.self_attn.o_proj": { + "snr": 0.18518221378326416, + "type": "self_attn.o_proj" + }, + "model.layers.7.self_attn.o_proj": { + "snr": 0.23254290223121643, + "type": "self_attn.o_proj" + }, + "model.layers.8.self_attn.o_proj": { + "snr": 0.2203962802886963, + "type": "self_attn.o_proj" + }, + "model.layers.9.self_attn.o_proj": { + "snr": 0.217017263174057, + "type": "self_attn.o_proj" + }, + "model.layers.10.self_attn.o_proj": { + "snr": 0.22843335568904877, + "type": "self_attn.o_proj" + }, + "model.layers.11.self_attn.o_proj": { + "snr": 0.23816843330860138, + "type": "self_attn.o_proj" + }, + "model.layers.12.self_attn.o_proj": { + "snr": 0.17585325241088867, + "type": "self_attn.o_proj" + }, + "model.layers.13.self_attn.o_proj": { + "snr": 0.20451271533966064, + "type": "self_attn.o_proj" + }, + "model.layers.14.self_attn.o_proj": { + "snr": 0.2095799297094345, + "type": "self_attn.o_proj" + }, + "model.layers.15.self_attn.o_proj": { + "snr": 0.23767071962356567, + "type": "self_attn.o_proj" + }, + "model.layers.16.self_attn.o_proj": { + "snr": 0.16328400373458862, + "type": "self_attn.o_proj" + }, + "model.layers.17.self_attn.o_proj": { + "snr": 0.20690056681632996, + "type": "self_attn.o_proj" + }, + "model.layers.18.self_attn.o_proj": { + "snr": 0.18191492557525635, + "type": "self_attn.o_proj" + }, + "model.layers.19.self_attn.o_proj": { + "snr": 0.1945018619298935, + "type": "self_attn.o_proj" + }, + "model.layers.20.self_attn.o_proj": { + "snr": 0.26658856868743896, + "type": "self_attn.o_proj" + }, + "model.layers.21.self_attn.o_proj": { + "snr": 0.16897724568843842, + "type": "self_attn.o_proj" + }, + "model.layers.22.self_attn.o_proj": { + "snr": 0.18773262202739716, + "type": "self_attn.o_proj" + }, + "model.layers.23.self_attn.o_proj": { + "snr": 0.18808405101299286, + "type": "self_attn.o_proj" + }, + "model.layers.24.self_attn.o_proj": { + "snr": 0.17919476330280304, + "type": "self_attn.o_proj" + }, + "model.layers.25.self_attn.o_proj": { + "snr": 0.1793426126241684, + "type": "self_attn.o_proj" + }, + "model.layers.26.self_attn.o_proj": { + "snr": 0.1777871698141098, + "type": "self_attn.o_proj" + }, + "model.layers.27.self_attn.o_proj": { + "snr": 0.20279864966869354, + "type": "self_attn.o_proj" + }, + "model.layers.28.self_attn.o_proj": { + "snr": 0.17030371725559235, + "type": "self_attn.o_proj" + }, + "model.layers.29.self_attn.o_proj": { + "snr": 0.1992504596710205, + "type": "self_attn.o_proj" + }, + "model.layers.30.self_attn.o_proj": { + "snr": 0.23085352778434753, + "type": "self_attn.o_proj" + }, + "model.layers.31.self_attn.o_proj": { + "snr": 0.1641533523797989, + "type": "self_attn.o_proj" + }, + "model.layers.32.self_attn.o_proj": { + "snr": 0.10621391236782074, + "type": "self_attn.o_proj" + }, + "model.layers.33.self_attn.o_proj": { + "snr": 0.09411631524562836, + "type": "self_attn.o_proj" + }, + "model.layers.34.self_attn.o_proj": { + "snr": 0.13239727914333344, + "type": "self_attn.o_proj" + }, + "model.layers.35.self_attn.o_proj": { + "snr": 0.11740171164274216, + "type": "self_attn.o_proj" + }, + "model.layers.0.self_attn.q_proj": { + "snr": 0.055595725774765015, + "type": "self_attn.q_proj" + }, + "model.layers.1.self_attn.q_proj": { + "snr": 0.13823610544204712, + "type": "self_attn.q_proj" + }, + "model.layers.2.self_attn.q_proj": { + "snr": 0.1297825127840042, + "type": "self_attn.q_proj" + }, + "model.layers.3.self_attn.q_proj": { + "snr": 0.15291297435760498, + "type": "self_attn.q_proj" + }, + "model.layers.4.self_attn.q_proj": { + "snr": 0.15615035593509674, + "type": "self_attn.q_proj" + }, + "model.layers.5.self_attn.q_proj": { + "snr": 0.15535500645637512, + "type": "self_attn.q_proj" + }, + "model.layers.6.self_attn.q_proj": { + "snr": 0.15993140637874603, + "type": "self_attn.q_proj" + }, + "model.layers.7.self_attn.q_proj": { + "snr": 0.1753682643175125, + "type": "self_attn.q_proj" + }, + "model.layers.8.self_attn.q_proj": { + "snr": 0.1664913445711136, + "type": "self_attn.q_proj" + }, + "model.layers.9.self_attn.q_proj": { + "snr": 0.15656901895999908, + "type": "self_attn.q_proj" + }, + "model.layers.10.self_attn.q_proj": { + "snr": 0.18300014734268188, + "type": "self_attn.q_proj" + }, + "model.layers.11.self_attn.q_proj": { + "snr": 0.1713649481534958, + "type": "self_attn.q_proj" + }, + "model.layers.12.self_attn.q_proj": { + "snr": 0.1809009313583374, + "type": "self_attn.q_proj" + }, + "model.layers.13.self_attn.q_proj": { + "snr": 0.20895132422447205, + "type": "self_attn.q_proj" + }, + "model.layers.14.self_attn.q_proj": { + "snr": 0.17413195967674255, + "type": "self_attn.q_proj" + }, + "model.layers.15.self_attn.q_proj": { + "snr": 0.20878490805625916, + "type": "self_attn.q_proj" + }, + "model.layers.16.self_attn.q_proj": { + "snr": 0.1547088772058487, + "type": "self_attn.q_proj" + }, + "model.layers.17.self_attn.q_proj": { + "snr": 0.1943129003047943, + "type": "self_attn.q_proj" + }, + "model.layers.18.self_attn.q_proj": { + "snr": 0.1889297217130661, + "type": "self_attn.q_proj" + }, + "model.layers.19.self_attn.q_proj": { + "snr": 0.207680344581604, + "type": "self_attn.q_proj" + }, + "model.layers.20.self_attn.q_proj": { + "snr": 0.20839959383010864, + "type": "self_attn.q_proj" + }, + "model.layers.21.self_attn.q_proj": { + "snr": 0.18989044427871704, + "type": "self_attn.q_proj" + }, + "model.layers.22.self_attn.q_proj": { + "snr": 0.18180623650550842, + "type": "self_attn.q_proj" + }, + "model.layers.23.self_attn.q_proj": { + "snr": 0.2069384753704071, + "type": "self_attn.q_proj" + }, + "model.layers.24.self_attn.q_proj": { + "snr": 0.1842993050813675, + "type": "self_attn.q_proj" + }, + "model.layers.25.self_attn.q_proj": { + "snr": 0.2078687846660614, + "type": "self_attn.q_proj" + }, + "model.layers.26.self_attn.q_proj": { + "snr": 0.19224946200847626, + "type": "self_attn.q_proj" + }, + "model.layers.27.self_attn.q_proj": { + "snr": 0.15170617401599884, + "type": "self_attn.q_proj" + }, + "model.layers.28.self_attn.q_proj": { + "snr": 0.20116600394248962, + "type": "self_attn.q_proj" + }, + "model.layers.29.self_attn.q_proj": { + "snr": 0.19373668730258942, + "type": "self_attn.q_proj" + }, + "model.layers.30.self_attn.q_proj": { + "snr": 0.18462225794792175, + "type": "self_attn.q_proj" + }, + "model.layers.31.self_attn.q_proj": { + "snr": 0.18939673900604248, + "type": "self_attn.q_proj" + }, + "model.layers.32.self_attn.q_proj": { + "snr": 0.20071947574615479, + "type": "self_attn.q_proj" + }, + "model.layers.33.self_attn.q_proj": { + "snr": 0.19740056991577148, + "type": "self_attn.q_proj" + }, + "model.layers.34.self_attn.q_proj": { + "snr": 0.17658494412899017, + "type": "self_attn.q_proj" + }, + "model.layers.35.self_attn.q_proj": { + "snr": 0.1407373696565628, + "type": "self_attn.q_proj" + }, + "model.layers.0.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.1.self_attn.v_proj": { + "snr": 846.30126953125, + "type": "self_attn.v_proj" + }, + "model.layers.2.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.3.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.4.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.5.self_attn.v_proj": { + "snr": 83.83415222167969, + "type": "self_attn.v_proj" + }, + "model.layers.6.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.7.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.8.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.9.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.10.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.11.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.12.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.13.self_attn.v_proj": { + "snr": 213.51316833496094, + "type": "self_attn.v_proj" + }, + "model.layers.14.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.15.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.16.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.17.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.18.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.19.self_attn.v_proj": { + "snr": 18.92746925354004, + "type": "self_attn.v_proj" + }, + "model.layers.20.self_attn.v_proj": { + "snr": 433.9771728515625, + "type": "self_attn.v_proj" + }, + "model.layers.21.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.22.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.23.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.24.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.25.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.26.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.27.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.28.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.29.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.30.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.31.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.32.self_attn.v_proj": { + "snr": 1.2332282066345215, + "type": "self_attn.v_proj" + }, + "model.layers.33.self_attn.v_proj": { + "snr": 0.6151890158653259, + "type": "self_attn.v_proj" + }, + "model.layers.34.self_attn.v_proj": { + "snr": 509.7169189453125, + "type": "self_attn.v_proj" + }, + "model.layers.35.self_attn.v_proj": { + "snr": 536.0748901367188, + "type": "self_attn.v_proj" + } +} diff --git a/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-7B-Instruct.json b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-7B-Instruct.json new file mode 100644 index 0000000000..0e18bd3864 --- /dev/null +++ b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-7B-Instruct.json @@ -0,0 +1,1022 @@ +{ + "model.layers.0.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.1.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.2.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.3.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.4.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.5.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.6.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.7.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.8.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.9.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.10.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.11.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.12.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.13.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.14.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.15.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.16.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.17.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.18.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.19.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.20.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.21.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.22.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.23.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.24.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.25.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.26.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.27.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "lm_head": { + "snr": Infinity, + "type": "lm_head" + }, + "model.layers.0.mlp.down_proj": { + "snr": 10.283808708190918, + "type": "mlp.down_proj" + }, + "model.layers.1.mlp.down_proj": { + "snr": 1.2089825868606567, + "type": "mlp.down_proj" + }, + "model.layers.2.mlp.down_proj": { + "snr": 19.309062957763672, + "type": "mlp.down_proj" + }, + "model.layers.3.mlp.down_proj": { + "snr": 50.174461364746094, + "type": "mlp.down_proj" + }, + "model.layers.4.mlp.down_proj": { + "snr": 114.28582763671875, + "type": "mlp.down_proj" + }, + "model.layers.5.mlp.down_proj": { + "snr": 215.5762176513672, + "type": "mlp.down_proj" + }, + "model.layers.6.mlp.down_proj": { + "snr": 204.5117950439453, + "type": "mlp.down_proj" + }, + "model.layers.7.mlp.down_proj": { + "snr": 182.5479278564453, + "type": "mlp.down_proj" + }, + "model.layers.8.mlp.down_proj": { + "snr": 74.92950439453125, + "type": "mlp.down_proj" + }, + "model.layers.9.mlp.down_proj": { + "snr": 16.482666015625, + "type": "mlp.down_proj" + }, + "model.layers.10.mlp.down_proj": { + "snr": 55.33920669555664, + "type": "mlp.down_proj" + }, + "model.layers.11.mlp.down_proj": { + "snr": 16.851062774658203, + "type": "mlp.down_proj" + }, + "model.layers.12.mlp.down_proj": { + "snr": 58.65230178833008, + "type": "mlp.down_proj" + }, + "model.layers.13.mlp.down_proj": { + "snr": 11.150161743164062, + "type": "mlp.down_proj" + }, + "model.layers.14.mlp.down_proj": { + "snr": 65.32643127441406, + "type": "mlp.down_proj" + }, + "model.layers.15.mlp.down_proj": { + "snr": 46.736305236816406, + "type": "mlp.down_proj" + }, + "model.layers.16.mlp.down_proj": { + "snr": 14.288785934448242, + "type": "mlp.down_proj" + }, + "model.layers.17.mlp.down_proj": { + "snr": 23.40110206604004, + "type": "mlp.down_proj" + }, + "model.layers.18.mlp.down_proj": { + "snr": 86.34363555908203, + "type": "mlp.down_proj" + }, + "model.layers.19.mlp.down_proj": { + "snr": 49.14613342285156, + "type": "mlp.down_proj" + }, + "model.layers.20.mlp.down_proj": { + "snr": 1276.84814453125, + "type": "mlp.down_proj" + }, + "model.layers.21.mlp.down_proj": { + "snr": 51.803409576416016, + "type": "mlp.down_proj" + }, + "model.layers.22.mlp.down_proj": { + "snr": 143.0666046142578, + "type": "mlp.down_proj" + }, + "model.layers.23.mlp.down_proj": { + "snr": 35.14984893798828, + "type": "mlp.down_proj" + }, + "model.layers.24.mlp.down_proj": { + "snr": 21.41700553894043, + "type": "mlp.down_proj" + }, + "model.layers.25.mlp.down_proj": { + "snr": 10.651569366455078, + "type": "mlp.down_proj" + }, + "model.layers.26.mlp.down_proj": { + "snr": 21.635149002075195, + "type": "mlp.down_proj" + }, + "model.layers.27.mlp.down_proj": { + "snr": 1446.2774658203125, + "type": "mlp.down_proj" + }, + "model.layers.0.mlp.gate_proj": { + "snr": 0.04497330263257027, + "type": "mlp.gate_proj" + }, + "model.layers.1.mlp.gate_proj": { + "snr": 0.16888172924518585, + "type": "mlp.gate_proj" + }, + "model.layers.2.mlp.gate_proj": { + "snr": 0.33653727173805237, + "type": "mlp.gate_proj" + }, + "model.layers.3.mlp.gate_proj": { + "snr": 3.1445391178131104, + "type": "mlp.gate_proj" + }, + "model.layers.4.mlp.gate_proj": { + "snr": 9.107144355773926, + "type": "mlp.gate_proj" + }, + "model.layers.5.mlp.gate_proj": { + "snr": 15.909018516540527, + "type": "mlp.gate_proj" + }, + "model.layers.6.mlp.gate_proj": { + "snr": 60.9138069152832, + "type": "mlp.gate_proj" + }, + "model.layers.7.mlp.gate_proj": { + "snr": 57.570281982421875, + "type": "mlp.gate_proj" + }, + "model.layers.8.mlp.gate_proj": { + "snr": 65.82791137695312, + "type": "mlp.gate_proj" + }, + "model.layers.9.mlp.gate_proj": { + "snr": 10.455283164978027, + "type": "mlp.gate_proj" + }, + "model.layers.10.mlp.gate_proj": { + "snr": 26.970706939697266, + "type": "mlp.gate_proj" + }, + "model.layers.11.mlp.gate_proj": { + "snr": 31.139820098876953, + "type": "mlp.gate_proj" + }, + "model.layers.12.mlp.gate_proj": { + "snr": 43.987159729003906, + "type": "mlp.gate_proj" + }, + "model.layers.13.mlp.gate_proj": { + "snr": 20.704849243164062, + "type": "mlp.gate_proj" + }, + "model.layers.14.mlp.gate_proj": { + "snr": 21.191452026367188, + "type": "mlp.gate_proj" + }, + "model.layers.15.mlp.gate_proj": { + "snr": 42.66447830200195, + "type": "mlp.gate_proj" + }, + "model.layers.16.mlp.gate_proj": { + "snr": 22.136825561523438, + "type": "mlp.gate_proj" + }, + "model.layers.17.mlp.gate_proj": { + "snr": 22.60980987548828, + "type": "mlp.gate_proj" + }, + "model.layers.18.mlp.gate_proj": { + "snr": 81.80574035644531, + "type": "mlp.gate_proj" + }, + "model.layers.19.mlp.gate_proj": { + "snr": 20.88619613647461, + "type": "mlp.gate_proj" + }, + "model.layers.20.mlp.gate_proj": { + "snr": 58.3524055480957, + "type": "mlp.gate_proj" + }, + "model.layers.21.mlp.gate_proj": { + "snr": 22.786706924438477, + "type": "mlp.gate_proj" + }, + "model.layers.22.mlp.gate_proj": { + "snr": 16.932226181030273, + "type": "mlp.gate_proj" + }, + "model.layers.23.mlp.gate_proj": { + "snr": 16.819862365722656, + "type": "mlp.gate_proj" + }, + "model.layers.24.mlp.gate_proj": { + "snr": 19.76348304748535, + "type": "mlp.gate_proj" + }, + "model.layers.25.mlp.gate_proj": { + "snr": 28.98714256286621, + "type": "mlp.gate_proj" + }, + "model.layers.26.mlp.gate_proj": { + "snr": 36.7071533203125, + "type": "mlp.gate_proj" + }, + "model.layers.27.mlp.gate_proj": { + "snr": 51.81539535522461, + "type": "mlp.gate_proj" + }, + "model.layers.0.mlp.up_proj": { + "snr": 0.2243107706308365, + "type": "mlp.up_proj" + }, + "model.layers.1.mlp.up_proj": { + "snr": 0.4464716613292694, + "type": "mlp.up_proj" + }, + "model.layers.2.mlp.up_proj": { + "snr": 1.7838181257247925, + "type": "mlp.up_proj" + }, + "model.layers.3.mlp.up_proj": { + "snr": 17.912736892700195, + "type": "mlp.up_proj" + }, + "model.layers.4.mlp.up_proj": { + "snr": 47.45841979980469, + "type": "mlp.up_proj" + }, + "model.layers.5.mlp.up_proj": { + "snr": 56.3084602355957, + "type": "mlp.up_proj" + }, + "model.layers.6.mlp.up_proj": { + "snr": 173.33717346191406, + "type": "mlp.up_proj" + }, + "model.layers.7.mlp.up_proj": { + "snr": 148.22750854492188, + "type": "mlp.up_proj" + }, + "model.layers.8.mlp.up_proj": { + "snr": 133.63565063476562, + "type": "mlp.up_proj" + }, + "model.layers.9.mlp.up_proj": { + "snr": 83.65129852294922, + "type": "mlp.up_proj" + }, + "model.layers.10.mlp.up_proj": { + "snr": 117.94369506835938, + "type": "mlp.up_proj" + }, + "model.layers.11.mlp.up_proj": { + "snr": 94.52413940429688, + "type": "mlp.up_proj" + }, + "model.layers.12.mlp.up_proj": { + "snr": 130.43333435058594, + "type": "mlp.up_proj" + }, + "model.layers.13.mlp.up_proj": { + "snr": 76.11975860595703, + "type": "mlp.up_proj" + }, + "model.layers.14.mlp.up_proj": { + "snr": 158.75192260742188, + "type": "mlp.up_proj" + }, + "model.layers.15.mlp.up_proj": { + "snr": 143.72706604003906, + "type": "mlp.up_proj" + }, + "model.layers.16.mlp.up_proj": { + "snr": 84.28279876708984, + "type": "mlp.up_proj" + }, + "model.layers.17.mlp.up_proj": { + "snr": 116.65055084228516, + "type": "mlp.up_proj" + }, + "model.layers.18.mlp.up_proj": { + "snr": 177.1201934814453, + "type": "mlp.up_proj" + }, + "model.layers.19.mlp.up_proj": { + "snr": 82.4564437866211, + "type": "mlp.up_proj" + }, + "model.layers.20.mlp.up_proj": { + "snr": 137.73019409179688, + "type": "mlp.up_proj" + }, + "model.layers.21.mlp.up_proj": { + "snr": 89.97538757324219, + "type": "mlp.up_proj" + }, + "model.layers.22.mlp.up_proj": { + "snr": 86.30876159667969, + "type": "mlp.up_proj" + }, + "model.layers.23.mlp.up_proj": { + "snr": 61.53449249267578, + "type": "mlp.up_proj" + }, + "model.layers.24.mlp.up_proj": { + "snr": 45.22392654418945, + "type": "mlp.up_proj" + }, + "model.layers.25.mlp.up_proj": { + "snr": 60.3155517578125, + "type": "mlp.up_proj" + }, + "model.layers.26.mlp.up_proj": { + "snr": 40.06092071533203, + "type": "mlp.up_proj" + }, + "model.layers.27.mlp.up_proj": { + "snr": 48.12322998046875, + "type": "mlp.up_proj" + }, + "model.embed_tokens": { + "snr": 0.08805440366268158, + "type": "model.embed_tokens" + }, + "model.norm": { + "snr": Infinity, + "type": "model.norm" + }, + "model.layers.0.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.1.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.2.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.3.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.4.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.5.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.6.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.7.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.8.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.9.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.10.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.11.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.12.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.13.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.14.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.15.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.16.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.17.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.18.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.19.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.20.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.21.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.22.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.23.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.24.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.25.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.26.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.27.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.0.self_attn.k_proj": { + "snr": 4.771554470062256, + "type": "self_attn.k_proj" + }, + "model.layers.1.self_attn.k_proj": { + "snr": 0.46674421429634094, + "type": "self_attn.k_proj" + }, + "model.layers.2.self_attn.k_proj": { + "snr": 1.6167784929275513, + "type": "self_attn.k_proj" + }, + "model.layers.3.self_attn.k_proj": { + "snr": 2.0980119705200195, + "type": "self_attn.k_proj" + }, + "model.layers.4.self_attn.k_proj": { + "snr": 1.4339035749435425, + "type": "self_attn.k_proj" + }, + "model.layers.5.self_attn.k_proj": { + "snr": 1.7446703910827637, + "type": "self_attn.k_proj" + }, + "model.layers.6.self_attn.k_proj": { + "snr": 1.2829725742340088, + "type": "self_attn.k_proj" + }, + "model.layers.7.self_attn.k_proj": { + "snr": 2.2314982414245605, + "type": "self_attn.k_proj" + }, + "model.layers.8.self_attn.k_proj": { + "snr": 1.5125916004180908, + "type": "self_attn.k_proj" + }, + "model.layers.9.self_attn.k_proj": { + "snr": 1.2817912101745605, + "type": "self_attn.k_proj" + }, + "model.layers.10.self_attn.k_proj": { + "snr": 3.3553454875946045, + "type": "self_attn.k_proj" + }, + "model.layers.11.self_attn.k_proj": { + "snr": 1.591347336769104, + "type": "self_attn.k_proj" + }, + "model.layers.12.self_attn.k_proj": { + "snr": 1.1114169359207153, + "type": "self_attn.k_proj" + }, + "model.layers.13.self_attn.k_proj": { + "snr": 1.1536189317703247, + "type": "self_attn.k_proj" + }, + "model.layers.14.self_attn.k_proj": { + "snr": 0.994098424911499, + "type": "self_attn.k_proj" + }, + "model.layers.15.self_attn.k_proj": { + "snr": 1.484580636024475, + "type": "self_attn.k_proj" + }, + "model.layers.16.self_attn.k_proj": { + "snr": 1.2999093532562256, + "type": "self_attn.k_proj" + }, + "model.layers.17.self_attn.k_proj": { + "snr": 2.1628623008728027, + "type": "self_attn.k_proj" + }, + "model.layers.18.self_attn.k_proj": { + "snr": 1.3842225074768066, + "type": "self_attn.k_proj" + }, + "model.layers.19.self_attn.k_proj": { + "snr": 1.440075159072876, + "type": "self_attn.k_proj" + }, + "model.layers.20.self_attn.k_proj": { + "snr": 1.7816450595855713, + "type": "self_attn.k_proj" + }, + "model.layers.21.self_attn.k_proj": { + "snr": 1.746536135673523, + "type": "self_attn.k_proj" + }, + "model.layers.22.self_attn.k_proj": { + "snr": 1.318993091583252, + "type": "self_attn.k_proj" + }, + "model.layers.23.self_attn.k_proj": { + "snr": 1.7234206199645996, + "type": "self_attn.k_proj" + }, + "model.layers.24.self_attn.k_proj": { + "snr": 2.586996555328369, + "type": "self_attn.k_proj" + }, + "model.layers.25.self_attn.k_proj": { + "snr": 1.6486897468566895, + "type": "self_attn.k_proj" + }, + "model.layers.26.self_attn.k_proj": { + "snr": 1.3349357843399048, + "type": "self_attn.k_proj" + }, + "model.layers.27.self_attn.k_proj": { + "snr": 0.9039687514305115, + "type": "self_attn.k_proj" + }, + "model.layers.0.self_attn.o_proj": { + "snr": 0.10605750232934952, + "type": "self_attn.o_proj" + }, + "model.layers.1.self_attn.o_proj": { + "snr": 0.2503393292427063, + "type": "self_attn.o_proj" + }, + "model.layers.2.self_attn.o_proj": { + "snr": 0.21453581750392914, + "type": "self_attn.o_proj" + }, + "model.layers.3.self_attn.o_proj": { + "snr": 0.20600366592407227, + "type": "self_attn.o_proj" + }, + "model.layers.4.self_attn.o_proj": { + "snr": 0.22004099190235138, + "type": "self_attn.o_proj" + }, + "model.layers.5.self_attn.o_proj": { + "snr": 0.2267625778913498, + "type": "self_attn.o_proj" + }, + "model.layers.6.self_attn.o_proj": { + "snr": 0.1736888736486435, + "type": "self_attn.o_proj" + }, + "model.layers.7.self_attn.o_proj": { + "snr": 0.2314220815896988, + "type": "self_attn.o_proj" + }, + "model.layers.8.self_attn.o_proj": { + "snr": 0.24031606316566467, + "type": "self_attn.o_proj" + }, + "model.layers.9.self_attn.o_proj": { + "snr": 0.13458871841430664, + "type": "self_attn.o_proj" + }, + "model.layers.10.self_attn.o_proj": { + "snr": 0.20170633494853973, + "type": "self_attn.o_proj" + }, + "model.layers.11.self_attn.o_proj": { + "snr": 0.19507651031017303, + "type": "self_attn.o_proj" + }, + "model.layers.12.self_attn.o_proj": { + "snr": 0.1862162947654724, + "type": "self_attn.o_proj" + }, + "model.layers.13.self_attn.o_proj": { + "snr": 0.15117767453193665, + "type": "self_attn.o_proj" + }, + "model.layers.14.self_attn.o_proj": { + "snr": 0.1857745349407196, + "type": "self_attn.o_proj" + }, + "model.layers.15.self_attn.o_proj": { + "snr": 0.2064860314130783, + "type": "self_attn.o_proj" + }, + "model.layers.16.self_attn.o_proj": { + "snr": 0.15419450402259827, + "type": "self_attn.o_proj" + }, + "model.layers.17.self_attn.o_proj": { + "snr": 0.17895667254924774, + "type": "self_attn.o_proj" + }, + "model.layers.18.self_attn.o_proj": { + "snr": 0.18284623324871063, + "type": "self_attn.o_proj" + }, + "model.layers.19.self_attn.o_proj": { + "snr": 0.17497135698795319, + "type": "self_attn.o_proj" + }, + "model.layers.20.self_attn.o_proj": { + "snr": 0.178844153881073, + "type": "self_attn.o_proj" + }, + "model.layers.21.self_attn.o_proj": { + "snr": 0.16190896928310394, + "type": "self_attn.o_proj" + }, + "model.layers.22.self_attn.o_proj": { + "snr": 0.19371949136257172, + "type": "self_attn.o_proj" + }, + "model.layers.23.self_attn.o_proj": { + "snr": 0.14116843044757843, + "type": "self_attn.o_proj" + }, + "model.layers.24.self_attn.o_proj": { + "snr": 0.14100700616836548, + "type": "self_attn.o_proj" + }, + "model.layers.25.self_attn.o_proj": { + "snr": 0.14792074263095856, + "type": "self_attn.o_proj" + }, + "model.layers.26.self_attn.o_proj": { + "snr": 0.11953117698431015, + "type": "self_attn.o_proj" + }, + "model.layers.27.self_attn.o_proj": { + "snr": 0.06241385638713837, + "type": "self_attn.o_proj" + }, + "model.layers.0.self_attn.q_proj": { + "snr": 0.02127065323293209, + "type": "self_attn.q_proj" + }, + "model.layers.1.self_attn.q_proj": { + "snr": 0.14693336188793182, + "type": "self_attn.q_proj" + }, + "model.layers.2.self_attn.q_proj": { + "snr": 0.16316214203834534, + "type": "self_attn.q_proj" + }, + "model.layers.3.self_attn.q_proj": { + "snr": 0.1218630000948906, + "type": "self_attn.q_proj" + }, + "model.layers.4.self_attn.q_proj": { + "snr": 0.13916714489459991, + "type": "self_attn.q_proj" + }, + "model.layers.5.self_attn.q_proj": { + "snr": 0.155359148979187, + "type": "self_attn.q_proj" + }, + "model.layers.6.self_attn.q_proj": { + "snr": 0.1590007096529007, + "type": "self_attn.q_proj" + }, + "model.layers.7.self_attn.q_proj": { + "snr": 0.1958903819322586, + "type": "self_attn.q_proj" + }, + "model.layers.8.self_attn.q_proj": { + "snr": 0.22448301315307617, + "type": "self_attn.q_proj" + }, + "model.layers.9.self_attn.q_proj": { + "snr": 0.20126597583293915, + "type": "self_attn.q_proj" + }, + "model.layers.10.self_attn.q_proj": { + "snr": 0.1980895698070526, + "type": "self_attn.q_proj" + }, + "model.layers.11.self_attn.q_proj": { + "snr": 0.2289486974477768, + "type": "self_attn.q_proj" + }, + "model.layers.12.self_attn.q_proj": { + "snr": 0.22922305762767792, + "type": "self_attn.q_proj" + }, + "model.layers.13.self_attn.q_proj": { + "snr": 0.21452386677265167, + "type": "self_attn.q_proj" + }, + "model.layers.14.self_attn.q_proj": { + "snr": 0.24151542782783508, + "type": "self_attn.q_proj" + }, + "model.layers.15.self_attn.q_proj": { + "snr": 0.21893717348575592, + "type": "self_attn.q_proj" + }, + "model.layers.16.self_attn.q_proj": { + "snr": 0.2321016639471054, + "type": "self_attn.q_proj" + }, + "model.layers.17.self_attn.q_proj": { + "snr": 0.24078059196472168, + "type": "self_attn.q_proj" + }, + "model.layers.18.self_attn.q_proj": { + "snr": 0.22774985432624817, + "type": "self_attn.q_proj" + }, + "model.layers.19.self_attn.q_proj": { + "snr": 0.20914016664028168, + "type": "self_attn.q_proj" + }, + "model.layers.20.self_attn.q_proj": { + "snr": 0.22847522795200348, + "type": "self_attn.q_proj" + }, + "model.layers.21.self_attn.q_proj": { + "snr": 0.2500442862510681, + "type": "self_attn.q_proj" + }, + "model.layers.22.self_attn.q_proj": { + "snr": 0.2353251725435257, + "type": "self_attn.q_proj" + }, + "model.layers.23.self_attn.q_proj": { + "snr": 0.20365388691425323, + "type": "self_attn.q_proj" + }, + "model.layers.24.self_attn.q_proj": { + "snr": 0.21967172622680664, + "type": "self_attn.q_proj" + }, + "model.layers.25.self_attn.q_proj": { + "snr": 0.2122868150472641, + "type": "self_attn.q_proj" + }, + "model.layers.26.self_attn.q_proj": { + "snr": 0.2415798157453537, + "type": "self_attn.q_proj" + }, + "model.layers.27.self_attn.q_proj": { + "snr": 0.12347634881734848, + "type": "self_attn.q_proj" + }, + "model.layers.0.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.1.self_attn.v_proj": { + "snr": 230.88636779785156, + "type": "self_attn.v_proj" + }, + "model.layers.2.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.3.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.4.self_attn.v_proj": { + "snr": 22.38136100769043, + "type": "self_attn.v_proj" + }, + "model.layers.5.self_attn.v_proj": { + "snr": 246.59597778320312, + "type": "self_attn.v_proj" + }, + "model.layers.6.self_attn.v_proj": { + "snr": 499.61761474609375, + "type": "self_attn.v_proj" + }, + "model.layers.7.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.8.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.9.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.10.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.11.self_attn.v_proj": { + "snr": 69.18345642089844, + "type": "self_attn.v_proj" + }, + "model.layers.12.self_attn.v_proj": { + "snr": 984.9320068359375, + "type": "self_attn.v_proj" + }, + "model.layers.13.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.14.self_attn.v_proj": { + "snr": 64.06214141845703, + "type": "self_attn.v_proj" + }, + "model.layers.15.self_attn.v_proj": { + "snr": 28.43911361694336, + "type": "self_attn.v_proj" + }, + "model.layers.16.self_attn.v_proj": { + "snr": 725.1439819335938, + "type": "self_attn.v_proj" + }, + "model.layers.17.self_attn.v_proj": { + "snr": 63.43681716918945, + "type": "self_attn.v_proj" + }, + "model.layers.18.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.19.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.20.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.21.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.22.self_attn.v_proj": { + "snr": 238.4695587158203, + "type": "self_attn.v_proj" + }, + "model.layers.23.self_attn.v_proj": { + "snr": 111.88697814941406, + "type": "self_attn.v_proj" + }, + "model.layers.24.self_attn.v_proj": { + "snr": 686.2830200195312, + "type": "self_attn.v_proj" + }, + "model.layers.25.self_attn.v_proj": { + "snr": 566.2647705078125, + "type": "self_attn.v_proj" + }, + "model.layers.26.self_attn.v_proj": { + "snr": 4.070064544677734, + "type": "self_attn.v_proj" + }, + "model.layers.27.self_attn.v_proj": { + "snr": 4.3411664962768555, + "type": "self_attn.v_proj" + } +} diff --git a/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-7B.json b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-7B.json new file mode 100644 index 0000000000..af13ecf9e7 --- /dev/null +++ b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_Qwen-Qwen2.5-7B.json @@ -0,0 +1,1022 @@ +{ + "model.layers.0.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.1.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.2.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.3.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.4.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.5.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.6.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.7.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.8.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.9.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.10.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.11.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.12.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.13.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.14.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.15.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.16.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.17.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.18.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.19.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.20.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.21.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.22.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.23.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.24.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.25.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.26.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.27.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "lm_head": { + "snr": Infinity, + "type": "lm_head" + }, + "model.layers.0.mlp.down_proj": { + "snr": 10.277782440185547, + "type": "mlp.down_proj" + }, + "model.layers.1.mlp.down_proj": { + "snr": 1.2050706148147583, + "type": "mlp.down_proj" + }, + "model.layers.2.mlp.down_proj": { + "snr": 19.284534454345703, + "type": "mlp.down_proj" + }, + "model.layers.3.mlp.down_proj": { + "snr": 50.16513442993164, + "type": "mlp.down_proj" + }, + "model.layers.4.mlp.down_proj": { + "snr": 114.24882507324219, + "type": "mlp.down_proj" + }, + "model.layers.5.mlp.down_proj": { + "snr": 215.48194885253906, + "type": "mlp.down_proj" + }, + "model.layers.6.mlp.down_proj": { + "snr": 204.39431762695312, + "type": "mlp.down_proj" + }, + "model.layers.7.mlp.down_proj": { + "snr": 182.5116729736328, + "type": "mlp.down_proj" + }, + "model.layers.8.mlp.down_proj": { + "snr": 74.9266128540039, + "type": "mlp.down_proj" + }, + "model.layers.9.mlp.down_proj": { + "snr": 16.474102020263672, + "type": "mlp.down_proj" + }, + "model.layers.10.mlp.down_proj": { + "snr": 55.30583572387695, + "type": "mlp.down_proj" + }, + "model.layers.11.mlp.down_proj": { + "snr": 16.84047508239746, + "type": "mlp.down_proj" + }, + "model.layers.12.mlp.down_proj": { + "snr": 58.62131118774414, + "type": "mlp.down_proj" + }, + "model.layers.13.mlp.down_proj": { + "snr": 11.144298553466797, + "type": "mlp.down_proj" + }, + "model.layers.14.mlp.down_proj": { + "snr": 65.28057098388672, + "type": "mlp.down_proj" + }, + "model.layers.15.mlp.down_proj": { + "snr": 46.701290130615234, + "type": "mlp.down_proj" + }, + "model.layers.16.mlp.down_proj": { + "snr": 14.278325080871582, + "type": "mlp.down_proj" + }, + "model.layers.17.mlp.down_proj": { + "snr": 23.382247924804688, + "type": "mlp.down_proj" + }, + "model.layers.18.mlp.down_proj": { + "snr": 93.8782958984375, + "type": "mlp.down_proj" + }, + "model.layers.19.mlp.down_proj": { + "snr": 49.10498809814453, + "type": "mlp.down_proj" + }, + "model.layers.20.mlp.down_proj": { + "snr": 1277.5101318359375, + "type": "mlp.down_proj" + }, + "model.layers.21.mlp.down_proj": { + "snr": 51.7880859375, + "type": "mlp.down_proj" + }, + "model.layers.22.mlp.down_proj": { + "snr": 143.03504943847656, + "type": "mlp.down_proj" + }, + "model.layers.23.mlp.down_proj": { + "snr": 35.123931884765625, + "type": "mlp.down_proj" + }, + "model.layers.24.mlp.down_proj": { + "snr": 21.403743743896484, + "type": "mlp.down_proj" + }, + "model.layers.25.mlp.down_proj": { + "snr": 10.551352500915527, + "type": "mlp.down_proj" + }, + "model.layers.26.mlp.down_proj": { + "snr": 21.62333869934082, + "type": "mlp.down_proj" + }, + "model.layers.27.mlp.down_proj": { + "snr": 1541.98681640625, + "type": "mlp.down_proj" + }, + "model.layers.0.mlp.gate_proj": { + "snr": 0.04497644677758217, + "type": "mlp.gate_proj" + }, + "model.layers.1.mlp.gate_proj": { + "snr": 0.16878646612167358, + "type": "mlp.gate_proj" + }, + "model.layers.2.mlp.gate_proj": { + "snr": 0.336302250623703, + "type": "mlp.gate_proj" + }, + "model.layers.3.mlp.gate_proj": { + "snr": 3.141293525695801, + "type": "mlp.gate_proj" + }, + "model.layers.4.mlp.gate_proj": { + "snr": 9.098686218261719, + "type": "mlp.gate_proj" + }, + "model.layers.5.mlp.gate_proj": { + "snr": 15.89354419708252, + "type": "mlp.gate_proj" + }, + "model.layers.6.mlp.gate_proj": { + "snr": 60.85503387451172, + "type": "mlp.gate_proj" + }, + "model.layers.7.mlp.gate_proj": { + "snr": 57.53098678588867, + "type": "mlp.gate_proj" + }, + "model.layers.8.mlp.gate_proj": { + "snr": 65.77096557617188, + "type": "mlp.gate_proj" + }, + "model.layers.9.mlp.gate_proj": { + "snr": 10.453179359436035, + "type": "mlp.gate_proj" + }, + "model.layers.10.mlp.gate_proj": { + "snr": 26.94801139831543, + "type": "mlp.gate_proj" + }, + "model.layers.11.mlp.gate_proj": { + "snr": 31.111093521118164, + "type": "mlp.gate_proj" + }, + "model.layers.12.mlp.gate_proj": { + "snr": 43.963191986083984, + "type": "mlp.gate_proj" + }, + "model.layers.13.mlp.gate_proj": { + "snr": 20.690765380859375, + "type": "mlp.gate_proj" + }, + "model.layers.14.mlp.gate_proj": { + "snr": 20.47557258605957, + "type": "mlp.gate_proj" + }, + "model.layers.15.mlp.gate_proj": { + "snr": 42.63906478881836, + "type": "mlp.gate_proj" + }, + "model.layers.16.mlp.gate_proj": { + "snr": 22.11542320251465, + "type": "mlp.gate_proj" + }, + "model.layers.17.mlp.gate_proj": { + "snr": 22.590566635131836, + "type": "mlp.gate_proj" + }, + "model.layers.18.mlp.gate_proj": { + "snr": 81.74773406982422, + "type": "mlp.gate_proj" + }, + "model.layers.19.mlp.gate_proj": { + "snr": 20.872997283935547, + "type": "mlp.gate_proj" + }, + "model.layers.20.mlp.gate_proj": { + "snr": 58.32197952270508, + "type": "mlp.gate_proj" + }, + "model.layers.21.mlp.gate_proj": { + "snr": 22.784095764160156, + "type": "mlp.gate_proj" + }, + "model.layers.22.mlp.gate_proj": { + "snr": 16.935768127441406, + "type": "mlp.gate_proj" + }, + "model.layers.23.mlp.gate_proj": { + "snr": 16.830224990844727, + "type": "mlp.gate_proj" + }, + "model.layers.24.mlp.gate_proj": { + "snr": 19.774564743041992, + "type": "mlp.gate_proj" + }, + "model.layers.25.mlp.gate_proj": { + "snr": 27.770675659179688, + "type": "mlp.gate_proj" + }, + "model.layers.26.mlp.gate_proj": { + "snr": 36.714595794677734, + "type": "mlp.gate_proj" + }, + "model.layers.27.mlp.gate_proj": { + "snr": 51.81637191772461, + "type": "mlp.gate_proj" + }, + "model.layers.0.mlp.up_proj": { + "snr": 0.22425401210784912, + "type": "mlp.up_proj" + }, + "model.layers.1.mlp.up_proj": { + "snr": 0.4456978142261505, + "type": "mlp.up_proj" + }, + "model.layers.2.mlp.up_proj": { + "snr": 1.7769725322723389, + "type": "mlp.up_proj" + }, + "model.layers.3.mlp.up_proj": { + "snr": 17.8966121673584, + "type": "mlp.up_proj" + }, + "model.layers.4.mlp.up_proj": { + "snr": 47.43608856201172, + "type": "mlp.up_proj" + }, + "model.layers.5.mlp.up_proj": { + "snr": 56.2298698425293, + "type": "mlp.up_proj" + }, + "model.layers.6.mlp.up_proj": { + "snr": 173.1498260498047, + "type": "mlp.up_proj" + }, + "model.layers.7.mlp.up_proj": { + "snr": 148.02874755859375, + "type": "mlp.up_proj" + }, + "model.layers.8.mlp.up_proj": { + "snr": 133.5174560546875, + "type": "mlp.up_proj" + }, + "model.layers.9.mlp.up_proj": { + "snr": 83.45183563232422, + "type": "mlp.up_proj" + }, + "model.layers.10.mlp.up_proj": { + "snr": 117.88772583007812, + "type": "mlp.up_proj" + }, + "model.layers.11.mlp.up_proj": { + "snr": 94.41156768798828, + "type": "mlp.up_proj" + }, + "model.layers.12.mlp.up_proj": { + "snr": 130.3107452392578, + "type": "mlp.up_proj" + }, + "model.layers.13.mlp.up_proj": { + "snr": 76.04458618164062, + "type": "mlp.up_proj" + }, + "model.layers.14.mlp.up_proj": { + "snr": 158.59634399414062, + "type": "mlp.up_proj" + }, + "model.layers.15.mlp.up_proj": { + "snr": 143.59596252441406, + "type": "mlp.up_proj" + }, + "model.layers.16.mlp.up_proj": { + "snr": 84.2161636352539, + "type": "mlp.up_proj" + }, + "model.layers.17.mlp.up_proj": { + "snr": 116.55204010009766, + "type": "mlp.up_proj" + }, + "model.layers.18.mlp.up_proj": { + "snr": 176.95449829101562, + "type": "mlp.up_proj" + }, + "model.layers.19.mlp.up_proj": { + "snr": 82.37284088134766, + "type": "mlp.up_proj" + }, + "model.layers.20.mlp.up_proj": { + "snr": 137.5695343017578, + "type": "mlp.up_proj" + }, + "model.layers.21.mlp.up_proj": { + "snr": 89.87335205078125, + "type": "mlp.up_proj" + }, + "model.layers.22.mlp.up_proj": { + "snr": 86.1510238647461, + "type": "mlp.up_proj" + }, + "model.layers.23.mlp.up_proj": { + "snr": 61.37428665161133, + "type": "mlp.up_proj" + }, + "model.layers.24.mlp.up_proj": { + "snr": 45.10757064819336, + "type": "mlp.up_proj" + }, + "model.layers.25.mlp.up_proj": { + "snr": 60.16519546508789, + "type": "mlp.up_proj" + }, + "model.layers.26.mlp.up_proj": { + "snr": 39.96969223022461, + "type": "mlp.up_proj" + }, + "model.layers.27.mlp.up_proj": { + "snr": 48.04258346557617, + "type": "mlp.up_proj" + }, + "model.embed_tokens": { + "snr": 0.08800078183412552, + "type": "model.embed_tokens" + }, + "model.norm": { + "snr": Infinity, + "type": "model.norm" + }, + "model.layers.0.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.1.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.2.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.3.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.4.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.5.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.6.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.7.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.8.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.9.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.10.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.11.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.12.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.13.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.14.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.15.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.16.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.17.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.18.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.19.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.20.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.21.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.22.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.23.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.24.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.25.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.26.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.27.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.0.self_attn.k_proj": { + "snr": 4.764852046966553, + "type": "self_attn.k_proj" + }, + "model.layers.1.self_attn.k_proj": { + "snr": 0.46627077460289, + "type": "self_attn.k_proj" + }, + "model.layers.2.self_attn.k_proj": { + "snr": 1.6155915260314941, + "type": "self_attn.k_proj" + }, + "model.layers.3.self_attn.k_proj": { + "snr": 2.096365451812744, + "type": "self_attn.k_proj" + }, + "model.layers.4.self_attn.k_proj": { + "snr": 1.431254267692566, + "type": "self_attn.k_proj" + }, + "model.layers.5.self_attn.k_proj": { + "snr": 1.7440669536590576, + "type": "self_attn.k_proj" + }, + "model.layers.6.self_attn.k_proj": { + "snr": 1.2815033197402954, + "type": "self_attn.k_proj" + }, + "model.layers.7.self_attn.k_proj": { + "snr": 2.2301025390625, + "type": "self_attn.k_proj" + }, + "model.layers.8.self_attn.k_proj": { + "snr": 1.5116536617279053, + "type": "self_attn.k_proj" + }, + "model.layers.9.self_attn.k_proj": { + "snr": 1.2699830532073975, + "type": "self_attn.k_proj" + }, + "model.layers.10.self_attn.k_proj": { + "snr": 3.3086464405059814, + "type": "self_attn.k_proj" + }, + "model.layers.11.self_attn.k_proj": { + "snr": 1.59111487865448, + "type": "self_attn.k_proj" + }, + "model.layers.12.self_attn.k_proj": { + "snr": 1.1007944345474243, + "type": "self_attn.k_proj" + }, + "model.layers.13.self_attn.k_proj": { + "snr": 1.163416862487793, + "type": "self_attn.k_proj" + }, + "model.layers.14.self_attn.k_proj": { + "snr": 0.9935113787651062, + "type": "self_attn.k_proj" + }, + "model.layers.15.self_attn.k_proj": { + "snr": 1.483581304550171, + "type": "self_attn.k_proj" + }, + "model.layers.16.self_attn.k_proj": { + "snr": 1.2992271184921265, + "type": "self_attn.k_proj" + }, + "model.layers.17.self_attn.k_proj": { + "snr": 2.162485122680664, + "type": "self_attn.k_proj" + }, + "model.layers.18.self_attn.k_proj": { + "snr": 1.3841017484664917, + "type": "self_attn.k_proj" + }, + "model.layers.19.self_attn.k_proj": { + "snr": 1.453418493270874, + "type": "self_attn.k_proj" + }, + "model.layers.20.self_attn.k_proj": { + "snr": 1.781678557395935, + "type": "self_attn.k_proj" + }, + "model.layers.21.self_attn.k_proj": { + "snr": 1.7460925579071045, + "type": "self_attn.k_proj" + }, + "model.layers.22.self_attn.k_proj": { + "snr": 1.3188031911849976, + "type": "self_attn.k_proj" + }, + "model.layers.23.self_attn.k_proj": { + "snr": 1.723441243171692, + "type": "self_attn.k_proj" + }, + "model.layers.24.self_attn.k_proj": { + "snr": 2.585094928741455, + "type": "self_attn.k_proj" + }, + "model.layers.25.self_attn.k_proj": { + "snr": 1.6478856801986694, + "type": "self_attn.k_proj" + }, + "model.layers.26.self_attn.k_proj": { + "snr": 1.3221096992492676, + "type": "self_attn.k_proj" + }, + "model.layers.27.self_attn.k_proj": { + "snr": 0.9034463167190552, + "type": "self_attn.k_proj" + }, + "model.layers.0.self_attn.o_proj": { + "snr": 0.10636883229017258, + "type": "self_attn.o_proj" + }, + "model.layers.1.self_attn.o_proj": { + "snr": 0.24971255660057068, + "type": "self_attn.o_proj" + }, + "model.layers.2.self_attn.o_proj": { + "snr": 0.21437697112560272, + "type": "self_attn.o_proj" + }, + "model.layers.3.self_attn.o_proj": { + "snr": 0.2058248072862625, + "type": "self_attn.o_proj" + }, + "model.layers.4.self_attn.o_proj": { + "snr": 0.21978946030139923, + "type": "self_attn.o_proj" + }, + "model.layers.5.self_attn.o_proj": { + "snr": 0.2269466072320938, + "type": "self_attn.o_proj" + }, + "model.layers.6.self_attn.o_proj": { + "snr": 0.17318543791770935, + "type": "self_attn.o_proj" + }, + "model.layers.7.self_attn.o_proj": { + "snr": 0.23159846663475037, + "type": "self_attn.o_proj" + }, + "model.layers.8.self_attn.o_proj": { + "snr": 0.2400084286928177, + "type": "self_attn.o_proj" + }, + "model.layers.9.self_attn.o_proj": { + "snr": 0.134766086935997, + "type": "self_attn.o_proj" + }, + "model.layers.10.self_attn.o_proj": { + "snr": 0.20152011513710022, + "type": "self_attn.o_proj" + }, + "model.layers.11.self_attn.o_proj": { + "snr": 0.19492347538471222, + "type": "self_attn.o_proj" + }, + "model.layers.12.self_attn.o_proj": { + "snr": 0.18607021868228912, + "type": "self_attn.o_proj" + }, + "model.layers.13.self_attn.o_proj": { + "snr": 0.15107683837413788, + "type": "self_attn.o_proj" + }, + "model.layers.14.self_attn.o_proj": { + "snr": 0.18565276265144348, + "type": "self_attn.o_proj" + }, + "model.layers.15.self_attn.o_proj": { + "snr": 0.20626339316368103, + "type": "self_attn.o_proj" + }, + "model.layers.16.self_attn.o_proj": { + "snr": 0.1541011780500412, + "type": "self_attn.o_proj" + }, + "model.layers.17.self_attn.o_proj": { + "snr": 0.1784645915031433, + "type": "self_attn.o_proj" + }, + "model.layers.18.self_attn.o_proj": { + "snr": 0.18307389318943024, + "type": "self_attn.o_proj" + }, + "model.layers.19.self_attn.o_proj": { + "snr": 0.17449897527694702, + "type": "self_attn.o_proj" + }, + "model.layers.20.self_attn.o_proj": { + "snr": 0.1787375956773758, + "type": "self_attn.o_proj" + }, + "model.layers.21.self_attn.o_proj": { + "snr": 0.161802276968956, + "type": "self_attn.o_proj" + }, + "model.layers.22.self_attn.o_proj": { + "snr": 0.1931520402431488, + "type": "self_attn.o_proj" + }, + "model.layers.23.self_attn.o_proj": { + "snr": 0.14108893275260925, + "type": "self_attn.o_proj" + }, + "model.layers.24.self_attn.o_proj": { + "snr": 0.14064815640449524, + "type": "self_attn.o_proj" + }, + "model.layers.25.self_attn.o_proj": { + "snr": 0.14790543913841248, + "type": "self_attn.o_proj" + }, + "model.layers.26.self_attn.o_proj": { + "snr": 0.11950570344924927, + "type": "self_attn.o_proj" + }, + "model.layers.27.self_attn.o_proj": { + "snr": 0.062389008700847626, + "type": "self_attn.o_proj" + }, + "model.layers.0.self_attn.q_proj": { + "snr": 0.02138795144855976, + "type": "self_attn.q_proj" + }, + "model.layers.1.self_attn.q_proj": { + "snr": 0.14676862955093384, + "type": "self_attn.q_proj" + }, + "model.layers.2.self_attn.q_proj": { + "snr": 0.16297142207622528, + "type": "self_attn.q_proj" + }, + "model.layers.3.self_attn.q_proj": { + "snr": 0.12198334187269211, + "type": "self_attn.q_proj" + }, + "model.layers.4.self_attn.q_proj": { + "snr": 0.13921146094799042, + "type": "self_attn.q_proj" + }, + "model.layers.5.self_attn.q_proj": { + "snr": 0.15567339956760406, + "type": "self_attn.q_proj" + }, + "model.layers.6.self_attn.q_proj": { + "snr": 0.1589033454656601, + "type": "self_attn.q_proj" + }, + "model.layers.7.self_attn.q_proj": { + "snr": 0.195299431681633, + "type": "self_attn.q_proj" + }, + "model.layers.8.self_attn.q_proj": { + "snr": 0.22430908679962158, + "type": "self_attn.q_proj" + }, + "model.layers.9.self_attn.q_proj": { + "snr": 0.2011336237192154, + "type": "self_attn.q_proj" + }, + "model.layers.10.self_attn.q_proj": { + "snr": 0.1982448250055313, + "type": "self_attn.q_proj" + }, + "model.layers.11.self_attn.q_proj": { + "snr": 0.22880099713802338, + "type": "self_attn.q_proj" + }, + "model.layers.12.self_attn.q_proj": { + "snr": 0.22898294031620026, + "type": "self_attn.q_proj" + }, + "model.layers.13.self_attn.q_proj": { + "snr": 0.21394900977611542, + "type": "self_attn.q_proj" + }, + "model.layers.14.self_attn.q_proj": { + "snr": 0.24130398035049438, + "type": "self_attn.q_proj" + }, + "model.layers.15.self_attn.q_proj": { + "snr": 0.21905161440372467, + "type": "self_attn.q_proj" + }, + "model.layers.16.self_attn.q_proj": { + "snr": 0.2319282442331314, + "type": "self_attn.q_proj" + }, + "model.layers.17.self_attn.q_proj": { + "snr": 0.24004821479320526, + "type": "self_attn.q_proj" + }, + "model.layers.18.self_attn.q_proj": { + "snr": 0.22754515707492828, + "type": "self_attn.q_proj" + }, + "model.layers.19.self_attn.q_proj": { + "snr": 0.2086794078350067, + "type": "self_attn.q_proj" + }, + "model.layers.20.self_attn.q_proj": { + "snr": 0.2290779948234558, + "type": "self_attn.q_proj" + }, + "model.layers.21.self_attn.q_proj": { + "snr": 0.250373899936676, + "type": "self_attn.q_proj" + }, + "model.layers.22.self_attn.q_proj": { + "snr": 0.23474709689617157, + "type": "self_attn.q_proj" + }, + "model.layers.23.self_attn.q_proj": { + "snr": 0.20302507281303406, + "type": "self_attn.q_proj" + }, + "model.layers.24.self_attn.q_proj": { + "snr": 0.21992310881614685, + "type": "self_attn.q_proj" + }, + "model.layers.25.self_attn.q_proj": { + "snr": 0.2120121270418167, + "type": "self_attn.q_proj" + }, + "model.layers.26.self_attn.q_proj": { + "snr": 0.24161922931671143, + "type": "self_attn.q_proj" + }, + "model.layers.27.self_attn.q_proj": { + "snr": 0.12337693572044373, + "type": "self_attn.q_proj" + }, + "model.layers.0.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.1.self_attn.v_proj": { + "snr": 231.07347106933594, + "type": "self_attn.v_proj" + }, + "model.layers.2.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.3.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.4.self_attn.v_proj": { + "snr": 22.34870719909668, + "type": "self_attn.v_proj" + }, + "model.layers.5.self_attn.v_proj": { + "snr": 246.30386352539062, + "type": "self_attn.v_proj" + }, + "model.layers.6.self_attn.v_proj": { + "snr": 499.5611572265625, + "type": "self_attn.v_proj" + }, + "model.layers.7.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.8.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.9.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.10.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.11.self_attn.v_proj": { + "snr": 69.09609985351562, + "type": "self_attn.v_proj" + }, + "model.layers.12.self_attn.v_proj": { + "snr": 983.3341674804688, + "type": "self_attn.v_proj" + }, + "model.layers.13.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.14.self_attn.v_proj": { + "snr": 64.04925537109375, + "type": "self_attn.v_proj" + }, + "model.layers.15.self_attn.v_proj": { + "snr": 28.41021728515625, + "type": "self_attn.v_proj" + }, + "model.layers.16.self_attn.v_proj": { + "snr": 724.2736206054688, + "type": "self_attn.v_proj" + }, + "model.layers.17.self_attn.v_proj": { + "snr": 63.35670852661133, + "type": "self_attn.v_proj" + }, + "model.layers.18.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.19.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.20.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.21.self_attn.v_proj": { + "snr": Infinity, + "type": "self_attn.v_proj" + }, + "model.layers.22.self_attn.v_proj": { + "snr": 238.2569122314453, + "type": "self_attn.v_proj" + }, + "model.layers.23.self_attn.v_proj": { + "snr": 111.78319549560547, + "type": "self_attn.v_proj" + }, + "model.layers.24.self_attn.v_proj": { + "snr": 687.0054931640625, + "type": "self_attn.v_proj" + }, + "model.layers.25.self_attn.v_proj": { + "snr": 565.3272705078125, + "type": "self_attn.v_proj" + }, + "model.layers.26.self_attn.v_proj": { + "snr": 4.064513683319092, + "type": "self_attn.v_proj" + }, + "model.layers.27.self_attn.v_proj": { + "snr": 4.335177421569824, + "type": "self_attn.v_proj" + } +} diff --git a/src/axolotl/integrations/spectrum/model_snr_results/snr_results_google-gemma-2-2b.json b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_google-gemma-2-2b.json new file mode 100644 index 0000000000..a2d6f0457d --- /dev/null +++ b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_google-gemma-2-2b.json @@ -0,0 +1,1158 @@ +{ + "model.layers.0.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.1.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.2.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.3.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.4.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.5.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.6.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.7.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.8.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.9.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.10.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.11.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.12.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.13.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.14.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.15.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.16.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.17.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.18.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.19.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.20.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.21.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.22.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.23.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.24.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.25.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "lm_head": { + "snr": 4.538210391998291, + "type": "lm_head" + }, + "model.layers.0.mlp.down_proj": { + "snr": 7.746472358703613, + "type": "mlp.down_proj" + }, + "model.layers.1.mlp.down_proj": { + "snr": 4.3358893394470215, + "type": "mlp.down_proj" + }, + "model.layers.2.mlp.down_proj": { + "snr": 26.88057518005371, + "type": "mlp.down_proj" + }, + "model.layers.3.mlp.down_proj": { + "snr": 8.699942588806152, + "type": "mlp.down_proj" + }, + "model.layers.4.mlp.down_proj": { + "snr": 32.808380126953125, + "type": "mlp.down_proj" + }, + "model.layers.5.mlp.down_proj": { + "snr": 10.831522941589355, + "type": "mlp.down_proj" + }, + "model.layers.6.mlp.down_proj": { + "snr": 18.843679428100586, + "type": "mlp.down_proj" + }, + "model.layers.7.mlp.down_proj": { + "snr": 9.348078727722168, + "type": "mlp.down_proj" + }, + "model.layers.8.mlp.down_proj": { + "snr": 7.061270236968994, + "type": "mlp.down_proj" + }, + "model.layers.9.mlp.down_proj": { + "snr": 5.454320907592773, + "type": "mlp.down_proj" + }, + "model.layers.10.mlp.down_proj": { + "snr": 7.386133193969727, + "type": "mlp.down_proj" + }, + "model.layers.11.mlp.down_proj": { + "snr": 6.648562908172607, + "type": "mlp.down_proj" + }, + "model.layers.12.mlp.down_proj": { + "snr": 5.853652477264404, + "type": "mlp.down_proj" + }, + "model.layers.13.mlp.down_proj": { + "snr": 8.570493698120117, + "type": "mlp.down_proj" + }, + "model.layers.14.mlp.down_proj": { + "snr": 13.120837211608887, + "type": "mlp.down_proj" + }, + "model.layers.15.mlp.down_proj": { + "snr": 14.780969619750977, + "type": "mlp.down_proj" + }, + "model.layers.16.mlp.down_proj": { + "snr": 6.953134059906006, + "type": "mlp.down_proj" + }, + "model.layers.17.mlp.down_proj": { + "snr": 12.589436531066895, + "type": "mlp.down_proj" + }, + "model.layers.18.mlp.down_proj": { + "snr": 8.844094276428223, + "type": "mlp.down_proj" + }, + "model.layers.19.mlp.down_proj": { + "snr": 7.598869800567627, + "type": "mlp.down_proj" + }, + "model.layers.20.mlp.down_proj": { + "snr": 11.293925285339355, + "type": "mlp.down_proj" + }, + "model.layers.21.mlp.down_proj": { + "snr": 9.384604454040527, + "type": "mlp.down_proj" + }, + "model.layers.22.mlp.down_proj": { + "snr": 12.12533187866211, + "type": "mlp.down_proj" + }, + "model.layers.23.mlp.down_proj": { + "snr": 11.217570304870605, + "type": "mlp.down_proj" + }, + "model.layers.24.mlp.down_proj": { + "snr": 14.197714805603027, + "type": "mlp.down_proj" + }, + "model.layers.25.mlp.down_proj": { + "snr": 12.449926376342773, + "type": "mlp.down_proj" + }, + "model.layers.0.mlp.gate_proj": { + "snr": 16.885862350463867, + "type": "mlp.gate_proj" + }, + "model.layers.1.mlp.gate_proj": { + "snr": 23.410266876220703, + "type": "mlp.gate_proj" + }, + "model.layers.2.mlp.gate_proj": { + "snr": 22.57662582397461, + "type": "mlp.gate_proj" + }, + "model.layers.3.mlp.gate_proj": { + "snr": 17.29996681213379, + "type": "mlp.gate_proj" + }, + "model.layers.4.mlp.gate_proj": { + "snr": 11.718637466430664, + "type": "mlp.gate_proj" + }, + "model.layers.5.mlp.gate_proj": { + "snr": 6.376136779785156, + "type": "mlp.gate_proj" + }, + "model.layers.6.mlp.gate_proj": { + "snr": 6.794021129608154, + "type": "mlp.gate_proj" + }, + "model.layers.7.mlp.gate_proj": { + "snr": 3.2425343990325928, + "type": "mlp.gate_proj" + }, + "model.layers.8.mlp.gate_proj": { + "snr": 2.368421792984009, + "type": "mlp.gate_proj" + }, + "model.layers.9.mlp.gate_proj": { + "snr": 3.3193087577819824, + "type": "mlp.gate_proj" + }, + "model.layers.10.mlp.gate_proj": { + "snr": 3.9515960216522217, + "type": "mlp.gate_proj" + }, + "model.layers.11.mlp.gate_proj": { + "snr": 3.2761318683624268, + "type": "mlp.gate_proj" + }, + "model.layers.12.mlp.gate_proj": { + "snr": 4.026322841644287, + "type": "mlp.gate_proj" + }, + "model.layers.13.mlp.gate_proj": { + "snr": 3.415473699569702, + "type": "mlp.gate_proj" + }, + "model.layers.14.mlp.gate_proj": { + "snr": 3.3418092727661133, + "type": "mlp.gate_proj" + }, + "model.layers.15.mlp.gate_proj": { + "snr": 3.6233012676239014, + "type": "mlp.gate_proj" + }, + "model.layers.16.mlp.gate_proj": { + "snr": 3.2199010848999023, + "type": "mlp.gate_proj" + }, + "model.layers.17.mlp.gate_proj": { + "snr": 3.6848936080932617, + "type": "mlp.gate_proj" + }, + "model.layers.18.mlp.gate_proj": { + "snr": 3.4439642429351807, + "type": "mlp.gate_proj" + }, + "model.layers.19.mlp.gate_proj": { + "snr": 3.7366604804992676, + "type": "mlp.gate_proj" + }, + "model.layers.20.mlp.gate_proj": { + "snr": 4.262336254119873, + "type": "mlp.gate_proj" + }, + "model.layers.21.mlp.gate_proj": { + "snr": 4.333253860473633, + "type": "mlp.gate_proj" + }, + "model.layers.22.mlp.gate_proj": { + "snr": 3.640247344970703, + "type": "mlp.gate_proj" + }, + "model.layers.23.mlp.gate_proj": { + "snr": 4.2978034019470215, + "type": "mlp.gate_proj" + }, + "model.layers.24.mlp.gate_proj": { + "snr": 4.339972496032715, + "type": "mlp.gate_proj" + }, + "model.layers.25.mlp.gate_proj": { + "snr": 3.8502564430236816, + "type": "mlp.gate_proj" + }, + "model.layers.0.mlp.up_proj": { + "snr": 28.129924774169922, + "type": "mlp.up_proj" + }, + "model.layers.1.mlp.up_proj": { + "snr": 41.49960708618164, + "type": "mlp.up_proj" + }, + "model.layers.2.mlp.up_proj": { + "snr": 125.47801971435547, + "type": "mlp.up_proj" + }, + "model.layers.3.mlp.up_proj": { + "snr": 119.93355560302734, + "type": "mlp.up_proj" + }, + "model.layers.4.mlp.up_proj": { + "snr": 162.62631225585938, + "type": "mlp.up_proj" + }, + "model.layers.5.mlp.up_proj": { + "snr": 32.36909484863281, + "type": "mlp.up_proj" + }, + "model.layers.6.mlp.up_proj": { + "snr": 49.10078430175781, + "type": "mlp.up_proj" + }, + "model.layers.7.mlp.up_proj": { + "snr": 28.541580200195312, + "type": "mlp.up_proj" + }, + "model.layers.8.mlp.up_proj": { + "snr": 14.764090538024902, + "type": "mlp.up_proj" + }, + "model.layers.9.mlp.up_proj": { + "snr": 16.5697078704834, + "type": "mlp.up_proj" + }, + "model.layers.10.mlp.up_proj": { + "snr": 19.26059913635254, + "type": "mlp.up_proj" + }, + "model.layers.11.mlp.up_proj": { + "snr": 15.082040786743164, + "type": "mlp.up_proj" + }, + "model.layers.12.mlp.up_proj": { + "snr": 15.5792875289917, + "type": "mlp.up_proj" + }, + "model.layers.13.mlp.up_proj": { + "snr": 9.84595012664795, + "type": "mlp.up_proj" + }, + "model.layers.14.mlp.up_proj": { + "snr": 11.506875991821289, + "type": "mlp.up_proj" + }, + "model.layers.15.mlp.up_proj": { + "snr": 21.507600784301758, + "type": "mlp.up_proj" + }, + "model.layers.16.mlp.up_proj": { + "snr": 15.110466957092285, + "type": "mlp.up_proj" + }, + "model.layers.17.mlp.up_proj": { + "snr": 27.062183380126953, + "type": "mlp.up_proj" + }, + "model.layers.18.mlp.up_proj": { + "snr": 16.40383529663086, + "type": "mlp.up_proj" + }, + "model.layers.19.mlp.up_proj": { + "snr": 13.117464065551758, + "type": "mlp.up_proj" + }, + "model.layers.20.mlp.up_proj": { + "snr": 11.393353462219238, + "type": "mlp.up_proj" + }, + "model.layers.21.mlp.up_proj": { + "snr": 10.791608810424805, + "type": "mlp.up_proj" + }, + "model.layers.22.mlp.up_proj": { + "snr": 7.512388706207275, + "type": "mlp.up_proj" + }, + "model.layers.23.mlp.up_proj": { + "snr": 9.889434814453125, + "type": "mlp.up_proj" + }, + "model.layers.24.mlp.up_proj": { + "snr": 7.587779521942139, + "type": "mlp.up_proj" + }, + "model.layers.25.mlp.up_proj": { + "snr": 4.561068058013916, + "type": "mlp.up_proj" + }, + "model.embed_tokens": { + "snr": 4.538210391998291, + "type": "model.embed_tokens" + }, + "model.norm": { + "snr": Infinity, + "type": "model.norm" + }, + "model.layers.0.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.1.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.2.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.3.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.4.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.5.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.6.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.7.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.8.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.9.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.10.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.11.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.12.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.13.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.14.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.15.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.16.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.17.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.18.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.19.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.20.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.21.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.22.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.23.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.24.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.25.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.0.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.1.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.2.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.3.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.4.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.5.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.6.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.7.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.8.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.9.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.10.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.11.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.12.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.13.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.14.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.15.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.16.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.17.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.18.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.19.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.20.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.21.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.22.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.23.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.24.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.25.post_feedforward_layernorm": { + "snr": Infinity, + "type": "post_feedforward_layernorm" + }, + "model.layers.0.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.1.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.2.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.3.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.4.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.5.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.6.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.7.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.8.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.9.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.10.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.11.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.12.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.13.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.14.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.15.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.16.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.17.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.18.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.19.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.20.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.21.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.22.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.23.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.24.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.25.pre_feedforward_layernorm": { + "snr": Infinity, + "type": "pre_feedforward_layernorm" + }, + "model.layers.0.self_attn.k_proj": { + "snr": 0.5685535073280334, + "type": "self_attn.k_proj" + }, + "model.layers.1.self_attn.k_proj": { + "snr": 1.060130000114441, + "type": "self_attn.k_proj" + }, + "model.layers.2.self_attn.k_proj": { + "snr": 1.0735561847686768, + "type": "self_attn.k_proj" + }, + "model.layers.3.self_attn.k_proj": { + "snr": 1.0217311382293701, + "type": "self_attn.k_proj" + }, + "model.layers.4.self_attn.k_proj": { + "snr": 0.9687430262565613, + "type": "self_attn.k_proj" + }, + "model.layers.5.self_attn.k_proj": { + "snr": 0.8411160111427307, + "type": "self_attn.k_proj" + }, + "model.layers.6.self_attn.k_proj": { + "snr": 0.936741054058075, + "type": "self_attn.k_proj" + }, + "model.layers.7.self_attn.k_proj": { + "snr": 0.7236003279685974, + "type": "self_attn.k_proj" + }, + "model.layers.8.self_attn.k_proj": { + "snr": 0.9032857418060303, + "type": "self_attn.k_proj" + }, + "model.layers.9.self_attn.k_proj": { + "snr": 0.7513307929039001, + "type": "self_attn.k_proj" + }, + "model.layers.10.self_attn.k_proj": { + "snr": 0.6875415444374084, + "type": "self_attn.k_proj" + }, + "model.layers.11.self_attn.k_proj": { + "snr": 0.6611058712005615, + "type": "self_attn.k_proj" + }, + "model.layers.12.self_attn.k_proj": { + "snr": 0.8023670315742493, + "type": "self_attn.k_proj" + }, + "model.layers.13.self_attn.k_proj": { + "snr": 0.7188767194747925, + "type": "self_attn.k_proj" + }, + "model.layers.14.self_attn.k_proj": { + "snr": 0.7930117249488831, + "type": "self_attn.k_proj" + }, + "model.layers.15.self_attn.k_proj": { + "snr": 0.9076258540153503, + "type": "self_attn.k_proj" + }, + "model.layers.16.self_attn.k_proj": { + "snr": 0.7295113801956177, + "type": "self_attn.k_proj" + }, + "model.layers.17.self_attn.k_proj": { + "snr": 0.898467481136322, + "type": "self_attn.k_proj" + }, + "model.layers.18.self_attn.k_proj": { + "snr": 0.9652048945426941, + "type": "self_attn.k_proj" + }, + "model.layers.19.self_attn.k_proj": { + "snr": 0.9855819344520569, + "type": "self_attn.k_proj" + }, + "model.layers.20.self_attn.k_proj": { + "snr": 1.2863355875015259, + "type": "self_attn.k_proj" + }, + "model.layers.21.self_attn.k_proj": { + "snr": 1.116607904434204, + "type": "self_attn.k_proj" + }, + "model.layers.22.self_attn.k_proj": { + "snr": 0.7438228130340576, + "type": "self_attn.k_proj" + }, + "model.layers.23.self_attn.k_proj": { + "snr": 0.8499895334243774, + "type": "self_attn.k_proj" + }, + "model.layers.24.self_attn.k_proj": { + "snr": 0.7764042019844055, + "type": "self_attn.k_proj" + }, + "model.layers.25.self_attn.k_proj": { + "snr": 0.7127887606620789, + "type": "self_attn.k_proj" + }, + "model.layers.0.self_attn.o_proj": { + "snr": 0.2556447386741638, + "type": "self_attn.o_proj" + }, + "model.layers.1.self_attn.o_proj": { + "snr": 0.2930974066257477, + "type": "self_attn.o_proj" + }, + "model.layers.2.self_attn.o_proj": { + "snr": 0.27571651339530945, + "type": "self_attn.o_proj" + }, + "model.layers.3.self_attn.o_proj": { + "snr": 0.280631959438324, + "type": "self_attn.o_proj" + }, + "model.layers.4.self_attn.o_proj": { + "snr": 0.2958097755908966, + "type": "self_attn.o_proj" + }, + "model.layers.5.self_attn.o_proj": { + "snr": 0.3072899580001831, + "type": "self_attn.o_proj" + }, + "model.layers.6.self_attn.o_proj": { + "snr": 0.31374114751815796, + "type": "self_attn.o_proj" + }, + "model.layers.7.self_attn.o_proj": { + "snr": 0.2903076410293579, + "type": "self_attn.o_proj" + }, + "model.layers.8.self_attn.o_proj": { + "snr": 0.2625811696052551, + "type": "self_attn.o_proj" + }, + "model.layers.9.self_attn.o_proj": { + "snr": 0.2306082546710968, + "type": "self_attn.o_proj" + }, + "model.layers.10.self_attn.o_proj": { + "snr": 0.24869701266288757, + "type": "self_attn.o_proj" + }, + "model.layers.11.self_attn.o_proj": { + "snr": 0.2556127905845642, + "type": "self_attn.o_proj" + }, + "model.layers.12.self_attn.o_proj": { + "snr": 0.28926730155944824, + "type": "self_attn.o_proj" + }, + "model.layers.13.self_attn.o_proj": { + "snr": 0.25355643033981323, + "type": "self_attn.o_proj" + }, + "model.layers.14.self_attn.o_proj": { + "snr": 0.23122912645339966, + "type": "self_attn.o_proj" + }, + "model.layers.15.self_attn.o_proj": { + "snr": 0.28772857785224915, + "type": "self_attn.o_proj" + }, + "model.layers.16.self_attn.o_proj": { + "snr": 0.22682352364063263, + "type": "self_attn.o_proj" + }, + "model.layers.17.self_attn.o_proj": { + "snr": 0.2558597922325134, + "type": "self_attn.o_proj" + }, + "model.layers.18.self_attn.o_proj": { + "snr": 0.1773315966129303, + "type": "self_attn.o_proj" + }, + "model.layers.19.self_attn.o_proj": { + "snr": 0.2106105089187622, + "type": "self_attn.o_proj" + }, + "model.layers.20.self_attn.o_proj": { + "snr": 0.2008877396583557, + "type": "self_attn.o_proj" + }, + "model.layers.21.self_attn.o_proj": { + "snr": 0.1973956972360611, + "type": "self_attn.o_proj" + }, + "model.layers.22.self_attn.o_proj": { + "snr": 0.25533634424209595, + "type": "self_attn.o_proj" + }, + "model.layers.23.self_attn.o_proj": { + "snr": 0.20066529512405396, + "type": "self_attn.o_proj" + }, + "model.layers.24.self_attn.o_proj": { + "snr": 0.18342143297195435, + "type": "self_attn.o_proj" + }, + "model.layers.25.self_attn.o_proj": { + "snr": 0.3224162459373474, + "type": "self_attn.o_proj" + }, + "model.layers.0.self_attn.q_proj": { + "snr": 0.2074502408504486, + "type": "self_attn.q_proj" + }, + "model.layers.1.self_attn.q_proj": { + "snr": 0.33233126997947693, + "type": "self_attn.q_proj" + }, + "model.layers.2.self_attn.q_proj": { + "snr": 0.3586291968822479, + "type": "self_attn.q_proj" + }, + "model.layers.3.self_attn.q_proj": { + "snr": 0.2850974202156067, + "type": "self_attn.q_proj" + }, + "model.layers.4.self_attn.q_proj": { + "snr": 0.37816473841667175, + "type": "self_attn.q_proj" + }, + "model.layers.5.self_attn.q_proj": { + "snr": 0.31616899371147156, + "type": "self_attn.q_proj" + }, + "model.layers.6.self_attn.q_proj": { + "snr": 0.4988365173339844, + "type": "self_attn.q_proj" + }, + "model.layers.7.self_attn.q_proj": { + "snr": 0.4238639175891876, + "type": "self_attn.q_proj" + }, + "model.layers.8.self_attn.q_proj": { + "snr": 0.2674674689769745, + "type": "self_attn.q_proj" + }, + "model.layers.9.self_attn.q_proj": { + "snr": 0.34524214267730713, + "type": "self_attn.q_proj" + }, + "model.layers.10.self_attn.q_proj": { + "snr": 0.4472109377384186, + "type": "self_attn.q_proj" + }, + "model.layers.11.self_attn.q_proj": { + "snr": 0.41363632678985596, + "type": "self_attn.q_proj" + }, + "model.layers.12.self_attn.q_proj": { + "snr": 0.44623735547065735, + "type": "self_attn.q_proj" + }, + "model.layers.13.self_attn.q_proj": { + "snr": 0.4404333531856537, + "type": "self_attn.q_proj" + }, + "model.layers.14.self_attn.q_proj": { + "snr": 0.5200268626213074, + "type": "self_attn.q_proj" + }, + "model.layers.15.self_attn.q_proj": { + "snr": 0.4320363700389862, + "type": "self_attn.q_proj" + }, + "model.layers.16.self_attn.q_proj": { + "snr": 0.46235284209251404, + "type": "self_attn.q_proj" + }, + "model.layers.17.self_attn.q_proj": { + "snr": 0.47477203607559204, + "type": "self_attn.q_proj" + }, + "model.layers.18.self_attn.q_proj": { + "snr": 0.4001321494579315, + "type": "self_attn.q_proj" + }, + "model.layers.19.self_attn.q_proj": { + "snr": 0.42365774512290955, + "type": "self_attn.q_proj" + }, + "model.layers.20.self_attn.q_proj": { + "snr": 0.37057873606681824, + "type": "self_attn.q_proj" + }, + "model.layers.21.self_attn.q_proj": { + "snr": 0.3990235924720764, + "type": "self_attn.q_proj" + }, + "model.layers.22.self_attn.q_proj": { + "snr": 0.35094162821769714, + "type": "self_attn.q_proj" + }, + "model.layers.23.self_attn.q_proj": { + "snr": 0.35721710324287415, + "type": "self_attn.q_proj" + }, + "model.layers.24.self_attn.q_proj": { + "snr": 0.2812618315219879, + "type": "self_attn.q_proj" + }, + "model.layers.25.self_attn.q_proj": { + "snr": 0.19463211297988892, + "type": "self_attn.q_proj" + }, + "model.layers.0.self_attn.v_proj": { + "snr": 1.3365743160247803, + "type": "self_attn.v_proj" + }, + "model.layers.1.self_attn.v_proj": { + "snr": 2.402009963989258, + "type": "self_attn.v_proj" + }, + "model.layers.2.self_attn.v_proj": { + "snr": 3.8695859909057617, + "type": "self_attn.v_proj" + }, + "model.layers.3.self_attn.v_proj": { + "snr": 4.117948055267334, + "type": "self_attn.v_proj" + }, + "model.layers.4.self_attn.v_proj": { + "snr": 5.651231288909912, + "type": "self_attn.v_proj" + }, + "model.layers.5.self_attn.v_proj": { + "snr": 2.720799446105957, + "type": "self_attn.v_proj" + }, + "model.layers.6.self_attn.v_proj": { + "snr": 1.4446897506713867, + "type": "self_attn.v_proj" + }, + "model.layers.7.self_attn.v_proj": { + "snr": 4.497112274169922, + "type": "self_attn.v_proj" + }, + "model.layers.8.self_attn.v_proj": { + "snr": 1.7241870164871216, + "type": "self_attn.v_proj" + }, + "model.layers.9.self_attn.v_proj": { + "snr": 1.7104988098144531, + "type": "self_attn.v_proj" + }, + "model.layers.10.self_attn.v_proj": { + "snr": 1.4231206178665161, + "type": "self_attn.v_proj" + }, + "model.layers.11.self_attn.v_proj": { + "snr": 2.1643989086151123, + "type": "self_attn.v_proj" + }, + "model.layers.12.self_attn.v_proj": { + "snr": 1.5254249572753906, + "type": "self_attn.v_proj" + }, + "model.layers.13.self_attn.v_proj": { + "snr": 2.3788745403289795, + "type": "self_attn.v_proj" + }, + "model.layers.14.self_attn.v_proj": { + "snr": 3.4155967235565186, + "type": "self_attn.v_proj" + }, + "model.layers.15.self_attn.v_proj": { + "snr": 4.623549938201904, + "type": "self_attn.v_proj" + }, + "model.layers.16.self_attn.v_proj": { + "snr": 1.5291141271591187, + "type": "self_attn.v_proj" + }, + "model.layers.17.self_attn.v_proj": { + "snr": 3.9934189319610596, + "type": "self_attn.v_proj" + }, + "model.layers.18.self_attn.v_proj": { + "snr": 9.035382270812988, + "type": "self_attn.v_proj" + }, + "model.layers.19.self_attn.v_proj": { + "snr": 5.8578925132751465, + "type": "self_attn.v_proj" + }, + "model.layers.20.self_attn.v_proj": { + "snr": 3.759958505630493, + "type": "self_attn.v_proj" + }, + "model.layers.21.self_attn.v_proj": { + "snr": 4.558528900146484, + "type": "self_attn.v_proj" + }, + "model.layers.22.self_attn.v_proj": { + "snr": 0.9163281917572021, + "type": "self_attn.v_proj" + }, + "model.layers.23.self_attn.v_proj": { + "snr": 2.564377546310425, + "type": "self_attn.v_proj" + }, + "model.layers.24.self_attn.v_proj": { + "snr": 3.689103841781616, + "type": "self_attn.v_proj" + }, + "model.layers.25.self_attn.v_proj": { + "snr": 5.6444854736328125, + "type": "self_attn.v_proj" + } +} diff --git a/src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-1B-Instruct.json b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-1B-Instruct.json new file mode 100644 index 0000000000..c5b5ed6ed8 --- /dev/null +++ b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-1B-Instruct.json @@ -0,0 +1,590 @@ +{ + "model.layers.0.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.1.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.2.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.3.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.4.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.5.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.6.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.7.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.8.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.9.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.10.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.11.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.12.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.13.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.14.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.15.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "lm_head": { + "snr": Infinity, + "type": "lm_head" + }, + "model.layers.0.mlp.down_proj": { + "snr": 70.0594253540039, + "type": "mlp.down_proj" + }, + "model.layers.1.mlp.down_proj": { + "snr": 11.135851860046387, + "type": "mlp.down_proj" + }, + "model.layers.2.mlp.down_proj": { + "snr": 7.035482883453369, + "type": "mlp.down_proj" + }, + "model.layers.3.mlp.down_proj": { + "snr": 6.422532081604004, + "type": "mlp.down_proj" + }, + "model.layers.4.mlp.down_proj": { + "snr": 5.748020172119141, + "type": "mlp.down_proj" + }, + "model.layers.5.mlp.down_proj": { + "snr": 3.885556697845459, + "type": "mlp.down_proj" + }, + "model.layers.6.mlp.down_proj": { + "snr": 3.4336745738983154, + "type": "mlp.down_proj" + }, + "model.layers.7.mlp.down_proj": { + "snr": 2.791595935821533, + "type": "mlp.down_proj" + }, + "model.layers.8.mlp.down_proj": { + "snr": 5.36277961730957, + "type": "mlp.down_proj" + }, + "model.layers.9.mlp.down_proj": { + "snr": 4.459208011627197, + "type": "mlp.down_proj" + }, + "model.layers.10.mlp.down_proj": { + "snr": 6.272170066833496, + "type": "mlp.down_proj" + }, + "model.layers.11.mlp.down_proj": { + "snr": 5.264761447906494, + "type": "mlp.down_proj" + }, + "model.layers.12.mlp.down_proj": { + "snr": 4.324735641479492, + "type": "mlp.down_proj" + }, + "model.layers.13.mlp.down_proj": { + "snr": 3.878648042678833, + "type": "mlp.down_proj" + }, + "model.layers.14.mlp.down_proj": { + "snr": 2.9773054122924805, + "type": "mlp.down_proj" + }, + "model.layers.15.mlp.down_proj": { + "snr": 4.471445560455322, + "type": "mlp.down_proj" + }, + "model.layers.0.mlp.gate_proj": { + "snr": 25.227100372314453, + "type": "mlp.gate_proj" + }, + "model.layers.1.mlp.gate_proj": { + "snr": 6.58299446105957, + "type": "mlp.gate_proj" + }, + "model.layers.2.mlp.gate_proj": { + "snr": 3.4688243865966797, + "type": "mlp.gate_proj" + }, + "model.layers.3.mlp.gate_proj": { + "snr": 1.555246114730835, + "type": "mlp.gate_proj" + }, + "model.layers.4.mlp.gate_proj": { + "snr": 0.7770601511001587, + "type": "mlp.gate_proj" + }, + "model.layers.5.mlp.gate_proj": { + "snr": 0.6239906549453735, + "type": "mlp.gate_proj" + }, + "model.layers.6.mlp.gate_proj": { + "snr": 0.6440379023551941, + "type": "mlp.gate_proj" + }, + "model.layers.7.mlp.gate_proj": { + "snr": 0.5120116472244263, + "type": "mlp.gate_proj" + }, + "model.layers.8.mlp.gate_proj": { + "snr": 0.6544050574302673, + "type": "mlp.gate_proj" + }, + "model.layers.9.mlp.gate_proj": { + "snr": 0.5381016731262207, + "type": "mlp.gate_proj" + }, + "model.layers.10.mlp.gate_proj": { + "snr": 0.622873842716217, + "type": "mlp.gate_proj" + }, + "model.layers.11.mlp.gate_proj": { + "snr": 0.9361700415611267, + "type": "mlp.gate_proj" + }, + "model.layers.12.mlp.gate_proj": { + "snr": 1.475605845451355, + "type": "mlp.gate_proj" + }, + "model.layers.13.mlp.gate_proj": { + "snr": 1.608325719833374, + "type": "mlp.gate_proj" + }, + "model.layers.14.mlp.gate_proj": { + "snr": 1.0720024108886719, + "type": "mlp.gate_proj" + }, + "model.layers.15.mlp.gate_proj": { + "snr": 0.7111338973045349, + "type": "mlp.gate_proj" + }, + "model.layers.0.mlp.up_proj": { + "snr": 28.431896209716797, + "type": "mlp.up_proj" + }, + "model.layers.1.mlp.up_proj": { + "snr": 15.546019554138184, + "type": "mlp.up_proj" + }, + "model.layers.2.mlp.up_proj": { + "snr": 23.048023223876953, + "type": "mlp.up_proj" + }, + "model.layers.3.mlp.up_proj": { + "snr": 25.790977478027344, + "type": "mlp.up_proj" + }, + "model.layers.4.mlp.up_proj": { + "snr": 18.552549362182617, + "type": "mlp.up_proj" + }, + "model.layers.5.mlp.up_proj": { + "snr": 8.85106372833252, + "type": "mlp.up_proj" + }, + "model.layers.6.mlp.up_proj": { + "snr": 10.653799057006836, + "type": "mlp.up_proj" + }, + "model.layers.7.mlp.up_proj": { + "snr": 7.365357875823975, + "type": "mlp.up_proj" + }, + "model.layers.8.mlp.up_proj": { + "snr": 11.98373794555664, + "type": "mlp.up_proj" + }, + "model.layers.9.mlp.up_proj": { + "snr": 8.04493236541748, + "type": "mlp.up_proj" + }, + "model.layers.10.mlp.up_proj": { + "snr": 8.523039817810059, + "type": "mlp.up_proj" + }, + "model.layers.11.mlp.up_proj": { + "snr": 5.381742477416992, + "type": "mlp.up_proj" + }, + "model.layers.12.mlp.up_proj": { + "snr": 3.9845118522644043, + "type": "mlp.up_proj" + }, + "model.layers.13.mlp.up_proj": { + "snr": 3.4893221855163574, + "type": "mlp.up_proj" + }, + "model.layers.14.mlp.up_proj": { + "snr": 1.764201045036316, + "type": "mlp.up_proj" + }, + "model.layers.15.mlp.up_proj": { + "snr": 0.9730708599090576, + "type": "mlp.up_proj" + }, + "model.embed_tokens": { + "snr": Infinity, + "type": "model.embed_tokens" + }, + "model.norm": { + "snr": Infinity, + "type": "model.norm" + }, + "model.layers.0.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.1.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.2.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.3.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.4.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.5.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.6.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.7.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.8.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.9.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.10.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.11.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.12.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.13.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.14.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.15.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.0.self_attn.k_proj": { + "snr": 0.11727584153413773, + "type": "self_attn.k_proj" + }, + "model.layers.1.self_attn.k_proj": { + "snr": 0.24786807596683502, + "type": "self_attn.k_proj" + }, + "model.layers.2.self_attn.k_proj": { + "snr": 0.36378130316734314, + "type": "self_attn.k_proj" + }, + "model.layers.3.self_attn.k_proj": { + "snr": 0.2983120381832123, + "type": "self_attn.k_proj" + }, + "model.layers.4.self_attn.k_proj": { + "snr": 0.33789733052253723, + "type": "self_attn.k_proj" + }, + "model.layers.5.self_attn.k_proj": { + "snr": 0.29155924916267395, + "type": "self_attn.k_proj" + }, + "model.layers.6.self_attn.k_proj": { + "snr": 0.2537297010421753, + "type": "self_attn.k_proj" + }, + "model.layers.7.self_attn.k_proj": { + "snr": 0.28204113245010376, + "type": "self_attn.k_proj" + }, + "model.layers.8.self_attn.k_proj": { + "snr": 0.2776711583137512, + "type": "self_attn.k_proj" + }, + "model.layers.9.self_attn.k_proj": { + "snr": 0.2927376627922058, + "type": "self_attn.k_proj" + }, + "model.layers.10.self_attn.k_proj": { + "snr": 0.31486213207244873, + "type": "self_attn.k_proj" + }, + "model.layers.11.self_attn.k_proj": { + "snr": 0.32363659143447876, + "type": "self_attn.k_proj" + }, + "model.layers.12.self_attn.k_proj": { + "snr": 0.31382912397384644, + "type": "self_attn.k_proj" + }, + "model.layers.13.self_attn.k_proj": { + "snr": 0.4635234773159027, + "type": "self_attn.k_proj" + }, + "model.layers.14.self_attn.k_proj": { + "snr": 0.25379249453544617, + "type": "self_attn.k_proj" + }, + "model.layers.15.self_attn.k_proj": { + "snr": 0.2628238797187805, + "type": "self_attn.k_proj" + }, + "model.layers.0.self_attn.o_proj": { + "snr": 0.27602291107177734, + "type": "self_attn.o_proj" + }, + "model.layers.1.self_attn.o_proj": { + "snr": 0.2149604707956314, + "type": "self_attn.o_proj" + }, + "model.layers.2.self_attn.o_proj": { + "snr": 0.2540294826030731, + "type": "self_attn.o_proj" + }, + "model.layers.3.self_attn.o_proj": { + "snr": 0.27978822588920593, + "type": "self_attn.o_proj" + }, + "model.layers.4.self_attn.o_proj": { + "snr": 0.3121289908885956, + "type": "self_attn.o_proj" + }, + "model.layers.5.self_attn.o_proj": { + "snr": 0.35037684440612793, + "type": "self_attn.o_proj" + }, + "model.layers.6.self_attn.o_proj": { + "snr": 0.366205096244812, + "type": "self_attn.o_proj" + }, + "model.layers.7.self_attn.o_proj": { + "snr": 0.3692712187767029, + "type": "self_attn.o_proj" + }, + "model.layers.8.self_attn.o_proj": { + "snr": 0.3301038146018982, + "type": "self_attn.o_proj" + }, + "model.layers.9.self_attn.o_proj": { + "snr": 0.3003396987915039, + "type": "self_attn.o_proj" + }, + "model.layers.10.self_attn.o_proj": { + "snr": 0.30804169178009033, + "type": "self_attn.o_proj" + }, + "model.layers.11.self_attn.o_proj": { + "snr": 0.28501132130622864, + "type": "self_attn.o_proj" + }, + "model.layers.12.self_attn.o_proj": { + "snr": 0.2171541005373001, + "type": "self_attn.o_proj" + }, + "model.layers.13.self_attn.o_proj": { + "snr": 0.19183959066867828, + "type": "self_attn.o_proj" + }, + "model.layers.14.self_attn.o_proj": { + "snr": 0.19215913116931915, + "type": "self_attn.o_proj" + }, + "model.layers.15.self_attn.o_proj": { + "snr": 0.25486502051353455, + "type": "self_attn.o_proj" + }, + "model.layers.0.self_attn.q_proj": { + "snr": 0.03850084915757179, + "type": "self_attn.q_proj" + }, + "model.layers.1.self_attn.q_proj": { + "snr": 0.0713055431842804, + "type": "self_attn.q_proj" + }, + "model.layers.2.self_attn.q_proj": { + "snr": 0.07948919385671616, + "type": "self_attn.q_proj" + }, + "model.layers.3.self_attn.q_proj": { + "snr": 0.08047746121883392, + "type": "self_attn.q_proj" + }, + "model.layers.4.self_attn.q_proj": { + "snr": 0.0852593332529068, + "type": "self_attn.q_proj" + }, + "model.layers.5.self_attn.q_proj": { + "snr": 0.09794823825359344, + "type": "self_attn.q_proj" + }, + "model.layers.6.self_attn.q_proj": { + "snr": 0.09627152234315872, + "type": "self_attn.q_proj" + }, + "model.layers.7.self_attn.q_proj": { + "snr": 0.11065381020307541, + "type": "self_attn.q_proj" + }, + "model.layers.8.self_attn.q_proj": { + "snr": 0.12031875550746918, + "type": "self_attn.q_proj" + }, + "model.layers.9.self_attn.q_proj": { + "snr": 0.09804573655128479, + "type": "self_attn.q_proj" + }, + "model.layers.10.self_attn.q_proj": { + "snr": 0.10897502303123474, + "type": "self_attn.q_proj" + }, + "model.layers.11.self_attn.q_proj": { + "snr": 0.09267337620258331, + "type": "self_attn.q_proj" + }, + "model.layers.12.self_attn.q_proj": { + "snr": 0.08803492039442062, + "type": "self_attn.q_proj" + }, + "model.layers.13.self_attn.q_proj": { + "snr": 0.0902542844414711, + "type": "self_attn.q_proj" + }, + "model.layers.14.self_attn.q_proj": { + "snr": 0.10154066979885101, + "type": "self_attn.q_proj" + }, + "model.layers.15.self_attn.q_proj": { + "snr": 0.09083802253007889, + "type": "self_attn.q_proj" + }, + "model.layers.0.self_attn.v_proj": { + "snr": 2.842210054397583, + "type": "self_attn.v_proj" + }, + "model.layers.1.self_attn.v_proj": { + "snr": 10.59461498260498, + "type": "self_attn.v_proj" + }, + "model.layers.2.self_attn.v_proj": { + "snr": 8.993025779724121, + "type": "self_attn.v_proj" + }, + "model.layers.3.self_attn.v_proj": { + "snr": 62.567787170410156, + "type": "self_attn.v_proj" + }, + "model.layers.4.self_attn.v_proj": { + "snr": 23.80082893371582, + "type": "self_attn.v_proj" + }, + "model.layers.5.self_attn.v_proj": { + "snr": 7.957369804382324, + "type": "self_attn.v_proj" + }, + "model.layers.6.self_attn.v_proj": { + "snr": 12.01815414428711, + "type": "self_attn.v_proj" + }, + "model.layers.7.self_attn.v_proj": { + "snr": 5.095500469207764, + "type": "self_attn.v_proj" + }, + "model.layers.8.self_attn.v_proj": { + "snr": 11.719332695007324, + "type": "self_attn.v_proj" + }, + "model.layers.9.self_attn.v_proj": { + "snr": 555.0869750976562, + "type": "self_attn.v_proj" + }, + "model.layers.10.self_attn.v_proj": { + "snr": 22.95538330078125, + "type": "self_attn.v_proj" + }, + "model.layers.11.self_attn.v_proj": { + "snr": 30.042158126831055, + "type": "self_attn.v_proj" + }, + "model.layers.12.self_attn.v_proj": { + "snr": 9.577271461486816, + "type": "self_attn.v_proj" + }, + "model.layers.13.self_attn.v_proj": { + "snr": 18.176361083984375, + "type": "self_attn.v_proj" + }, + "model.layers.14.self_attn.v_proj": { + "snr": 1.5695856809616089, + "type": "self_attn.v_proj" + }, + "model.layers.15.self_attn.v_proj": { + "snr": 2.7235565185546875, + "type": "self_attn.v_proj" + } +} diff --git a/src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-1B.json b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-1B.json new file mode 100644 index 0000000000..b84594fd7b --- /dev/null +++ b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-1B.json @@ -0,0 +1,590 @@ +{ + "model.layers.0.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.1.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.2.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.3.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.4.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.5.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.6.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.7.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.8.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.9.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.10.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.11.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.12.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.13.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.14.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.15.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "lm_head": { + "snr": Infinity, + "type": "lm_head" + }, + "model.layers.0.mlp.down_proj": { + "snr": 57.09797286987305, + "type": "mlp.down_proj" + }, + "model.layers.1.mlp.down_proj": { + "snr": 9.538983345031738, + "type": "mlp.down_proj" + }, + "model.layers.2.mlp.down_proj": { + "snr": 6.227016925811768, + "type": "mlp.down_proj" + }, + "model.layers.3.mlp.down_proj": { + "snr": 5.660686492919922, + "type": "mlp.down_proj" + }, + "model.layers.4.mlp.down_proj": { + "snr": 5.178432464599609, + "type": "mlp.down_proj" + }, + "model.layers.5.mlp.down_proj": { + "snr": 3.5638349056243896, + "type": "mlp.down_proj" + }, + "model.layers.6.mlp.down_proj": { + "snr": 3.0918056964874268, + "type": "mlp.down_proj" + }, + "model.layers.7.mlp.down_proj": { + "snr": 2.456392288208008, + "type": "mlp.down_proj" + }, + "model.layers.8.mlp.down_proj": { + "snr": 4.525328636169434, + "type": "mlp.down_proj" + }, + "model.layers.9.mlp.down_proj": { + "snr": 3.9409055709838867, + "type": "mlp.down_proj" + }, + "model.layers.10.mlp.down_proj": { + "snr": 5.447249412536621, + "type": "mlp.down_proj" + }, + "model.layers.11.mlp.down_proj": { + "snr": 4.807600975036621, + "type": "mlp.down_proj" + }, + "model.layers.12.mlp.down_proj": { + "snr": 3.915374517440796, + "type": "mlp.down_proj" + }, + "model.layers.13.mlp.down_proj": { + "snr": 3.4820363521575928, + "type": "mlp.down_proj" + }, + "model.layers.14.mlp.down_proj": { + "snr": 2.6045074462890625, + "type": "mlp.down_proj" + }, + "model.layers.15.mlp.down_proj": { + "snr": 3.7237701416015625, + "type": "mlp.down_proj" + }, + "model.layers.0.mlp.gate_proj": { + "snr": 22.160131454467773, + "type": "mlp.gate_proj" + }, + "model.layers.1.mlp.gate_proj": { + "snr": 6.072206020355225, + "type": "mlp.gate_proj" + }, + "model.layers.2.mlp.gate_proj": { + "snr": 3.2467362880706787, + "type": "mlp.gate_proj" + }, + "model.layers.3.mlp.gate_proj": { + "snr": 1.4111896753311157, + "type": "mlp.gate_proj" + }, + "model.layers.4.mlp.gate_proj": { + "snr": 0.7405938506126404, + "type": "mlp.gate_proj" + }, + "model.layers.5.mlp.gate_proj": { + "snr": 0.5916463136672974, + "type": "mlp.gate_proj" + }, + "model.layers.6.mlp.gate_proj": { + "snr": 0.6149423718452454, + "type": "mlp.gate_proj" + }, + "model.layers.7.mlp.gate_proj": { + "snr": 0.48369669914245605, + "type": "mlp.gate_proj" + }, + "model.layers.8.mlp.gate_proj": { + "snr": 0.6047574877738953, + "type": "mlp.gate_proj" + }, + "model.layers.9.mlp.gate_proj": { + "snr": 0.5092479586601257, + "type": "mlp.gate_proj" + }, + "model.layers.10.mlp.gate_proj": { + "snr": 0.5999670624732971, + "type": "mlp.gate_proj" + }, + "model.layers.11.mlp.gate_proj": { + "snr": 0.8980127573013306, + "type": "mlp.gate_proj" + }, + "model.layers.12.mlp.gate_proj": { + "snr": 1.4252448081970215, + "type": "mlp.gate_proj" + }, + "model.layers.13.mlp.gate_proj": { + "snr": 1.509937047958374, + "type": "mlp.gate_proj" + }, + "model.layers.14.mlp.gate_proj": { + "snr": 1.0066585540771484, + "type": "mlp.gate_proj" + }, + "model.layers.15.mlp.gate_proj": { + "snr": 0.6413647532463074, + "type": "mlp.gate_proj" + }, + "model.layers.0.mlp.up_proj": { + "snr": 26.08852195739746, + "type": "mlp.up_proj" + }, + "model.layers.1.mlp.up_proj": { + "snr": 13.382951736450195, + "type": "mlp.up_proj" + }, + "model.layers.2.mlp.up_proj": { + "snr": 20.088768005371094, + "type": "mlp.up_proj" + }, + "model.layers.3.mlp.up_proj": { + "snr": 23.0632381439209, + "type": "mlp.up_proj" + }, + "model.layers.4.mlp.up_proj": { + "snr": 16.07433319091797, + "type": "mlp.up_proj" + }, + "model.layers.5.mlp.up_proj": { + "snr": 8.00507640838623, + "type": "mlp.up_proj" + }, + "model.layers.6.mlp.up_proj": { + "snr": 9.538354873657227, + "type": "mlp.up_proj" + }, + "model.layers.7.mlp.up_proj": { + "snr": 6.286602973937988, + "type": "mlp.up_proj" + }, + "model.layers.8.mlp.up_proj": { + "snr": 10.092820167541504, + "type": "mlp.up_proj" + }, + "model.layers.9.mlp.up_proj": { + "snr": 7.193963527679443, + "type": "mlp.up_proj" + }, + "model.layers.10.mlp.up_proj": { + "snr": 7.320116996765137, + "type": "mlp.up_proj" + }, + "model.layers.11.mlp.up_proj": { + "snr": 4.8728532791137695, + "type": "mlp.up_proj" + }, + "model.layers.12.mlp.up_proj": { + "snr": 3.596583366394043, + "type": "mlp.up_proj" + }, + "model.layers.13.mlp.up_proj": { + "snr": 3.166161298751831, + "type": "mlp.up_proj" + }, + "model.layers.14.mlp.up_proj": { + "snr": 1.5600818395614624, + "type": "mlp.up_proj" + }, + "model.layers.15.mlp.up_proj": { + "snr": 0.8726214170455933, + "type": "mlp.up_proj" + }, + "model.embed_tokens": { + "snr": Infinity, + "type": "model.embed_tokens" + }, + "model.norm": { + "snr": Infinity, + "type": "model.norm" + }, + "model.layers.0.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.1.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.2.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.3.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.4.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.5.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.6.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.7.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.8.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.9.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.10.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.11.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.12.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.13.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.14.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.15.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.0.self_attn.k_proj": { + "snr": 0.1154392883181572, + "type": "self_attn.k_proj" + }, + "model.layers.1.self_attn.k_proj": { + "snr": 0.24299409985542297, + "type": "self_attn.k_proj" + }, + "model.layers.2.self_attn.k_proj": { + "snr": 0.3624322712421417, + "type": "self_attn.k_proj" + }, + "model.layers.3.self_attn.k_proj": { + "snr": 0.29509487748146057, + "type": "self_attn.k_proj" + }, + "model.layers.4.self_attn.k_proj": { + "snr": 0.32953736186027527, + "type": "self_attn.k_proj" + }, + "model.layers.5.self_attn.k_proj": { + "snr": 0.2908833622932434, + "type": "self_attn.k_proj" + }, + "model.layers.6.self_attn.k_proj": { + "snr": 0.2488437294960022, + "type": "self_attn.k_proj" + }, + "model.layers.7.self_attn.k_proj": { + "snr": 0.27847856283187866, + "type": "self_attn.k_proj" + }, + "model.layers.8.self_attn.k_proj": { + "snr": 0.27143892645835876, + "type": "self_attn.k_proj" + }, + "model.layers.9.self_attn.k_proj": { + "snr": 0.28804272413253784, + "type": "self_attn.k_proj" + }, + "model.layers.10.self_attn.k_proj": { + "snr": 0.31197959184646606, + "type": "self_attn.k_proj" + }, + "model.layers.11.self_attn.k_proj": { + "snr": 0.3203586935997009, + "type": "self_attn.k_proj" + }, + "model.layers.12.self_attn.k_proj": { + "snr": 0.30905747413635254, + "type": "self_attn.k_proj" + }, + "model.layers.13.self_attn.k_proj": { + "snr": 0.46828722953796387, + "type": "self_attn.k_proj" + }, + "model.layers.14.self_attn.k_proj": { + "snr": 0.24205778539180756, + "type": "self_attn.k_proj" + }, + "model.layers.15.self_attn.k_proj": { + "snr": 0.2559327781200409, + "type": "self_attn.k_proj" + }, + "model.layers.0.self_attn.o_proj": { + "snr": 0.2638678550720215, + "type": "self_attn.o_proj" + }, + "model.layers.1.self_attn.o_proj": { + "snr": 0.21109595894813538, + "type": "self_attn.o_proj" + }, + "model.layers.2.self_attn.o_proj": { + "snr": 0.24751724302768707, + "type": "self_attn.o_proj" + }, + "model.layers.3.self_attn.o_proj": { + "snr": 0.2728094160556793, + "type": "self_attn.o_proj" + }, + "model.layers.4.self_attn.o_proj": { + "snr": 0.3001374304294586, + "type": "self_attn.o_proj" + }, + "model.layers.5.self_attn.o_proj": { + "snr": 0.33903488516807556, + "type": "self_attn.o_proj" + }, + "model.layers.6.self_attn.o_proj": { + "snr": 0.3530929982662201, + "type": "self_attn.o_proj" + }, + "model.layers.7.self_attn.o_proj": { + "snr": 0.36753255128860474, + "type": "self_attn.o_proj" + }, + "model.layers.8.self_attn.o_proj": { + "snr": 0.3373180329799652, + "type": "self_attn.o_proj" + }, + "model.layers.9.self_attn.o_proj": { + "snr": 0.2970578670501709, + "type": "self_attn.o_proj" + }, + "model.layers.10.self_attn.o_proj": { + "snr": 0.3076324760913849, + "type": "self_attn.o_proj" + }, + "model.layers.11.self_attn.o_proj": { + "snr": 0.2766900658607483, + "type": "self_attn.o_proj" + }, + "model.layers.12.self_attn.o_proj": { + "snr": 0.20973259210586548, + "type": "self_attn.o_proj" + }, + "model.layers.13.self_attn.o_proj": { + "snr": 0.18185566365718842, + "type": "self_attn.o_proj" + }, + "model.layers.14.self_attn.o_proj": { + "snr": 0.18329747021198273, + "type": "self_attn.o_proj" + }, + "model.layers.15.self_attn.o_proj": { + "snr": 0.2437991499900818, + "type": "self_attn.o_proj" + }, + "model.layers.0.self_attn.q_proj": { + "snr": 0.038040731102228165, + "type": "self_attn.q_proj" + }, + "model.layers.1.self_attn.q_proj": { + "snr": 0.0707998052239418, + "type": "self_attn.q_proj" + }, + "model.layers.2.self_attn.q_proj": { + "snr": 0.0787411704659462, + "type": "self_attn.q_proj" + }, + "model.layers.3.self_attn.q_proj": { + "snr": 0.08089710026979446, + "type": "self_attn.q_proj" + }, + "model.layers.4.self_attn.q_proj": { + "snr": 0.08591937273740768, + "type": "self_attn.q_proj" + }, + "model.layers.5.self_attn.q_proj": { + "snr": 0.09852176159620285, + "type": "self_attn.q_proj" + }, + "model.layers.6.self_attn.q_proj": { + "snr": 0.09690654277801514, + "type": "self_attn.q_proj" + }, + "model.layers.7.self_attn.q_proj": { + "snr": 0.11181341856718063, + "type": "self_attn.q_proj" + }, + "model.layers.8.self_attn.q_proj": { + "snr": 0.12042108923196793, + "type": "self_attn.q_proj" + }, + "model.layers.9.self_attn.q_proj": { + "snr": 0.09799323976039886, + "type": "self_attn.q_proj" + }, + "model.layers.10.self_attn.q_proj": { + "snr": 0.10901063680648804, + "type": "self_attn.q_proj" + }, + "model.layers.11.self_attn.q_proj": { + "snr": 0.09307146072387695, + "type": "self_attn.q_proj" + }, + "model.layers.12.self_attn.q_proj": { + "snr": 0.0880950540304184, + "type": "self_attn.q_proj" + }, + "model.layers.13.self_attn.q_proj": { + "snr": 0.08886399120092392, + "type": "self_attn.q_proj" + }, + "model.layers.14.self_attn.q_proj": { + "snr": 0.09955056011676788, + "type": "self_attn.q_proj" + }, + "model.layers.15.self_attn.q_proj": { + "snr": 0.08929339051246643, + "type": "self_attn.q_proj" + }, + "model.layers.0.self_attn.v_proj": { + "snr": 2.5501928329467773, + "type": "self_attn.v_proj" + }, + "model.layers.1.self_attn.v_proj": { + "snr": 9.449499130249023, + "type": "self_attn.v_proj" + }, + "model.layers.2.self_attn.v_proj": { + "snr": 7.9920830726623535, + "type": "self_attn.v_proj" + }, + "model.layers.3.self_attn.v_proj": { + "snr": 50.69462585449219, + "type": "self_attn.v_proj" + }, + "model.layers.4.self_attn.v_proj": { + "snr": 19.083511352539062, + "type": "self_attn.v_proj" + }, + "model.layers.5.self_attn.v_proj": { + "snr": 7.21597146987915, + "type": "self_attn.v_proj" + }, + "model.layers.6.self_attn.v_proj": { + "snr": 11.27744197845459, + "type": "self_attn.v_proj" + }, + "model.layers.7.self_attn.v_proj": { + "snr": 4.579711437225342, + "type": "self_attn.v_proj" + }, + "model.layers.8.self_attn.v_proj": { + "snr": 10.940719604492188, + "type": "self_attn.v_proj" + }, + "model.layers.9.self_attn.v_proj": { + "snr": 553.4417724609375, + "type": "self_attn.v_proj" + }, + "model.layers.10.self_attn.v_proj": { + "snr": 20.59434700012207, + "type": "self_attn.v_proj" + }, + "model.layers.11.self_attn.v_proj": { + "snr": 26.636865615844727, + "type": "self_attn.v_proj" + }, + "model.layers.12.self_attn.v_proj": { + "snr": 8.614749908447266, + "type": "self_attn.v_proj" + }, + "model.layers.13.self_attn.v_proj": { + "snr": 17.722007751464844, + "type": "self_attn.v_proj" + }, + "model.layers.14.self_attn.v_proj": { + "snr": 1.48500657081604, + "type": "self_attn.v_proj" + }, + "model.layers.15.self_attn.v_proj": { + "snr": 2.5776851177215576, + "type": "self_attn.v_proj" + } +} diff --git a/src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-3B-Instruct.json b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-3B-Instruct.json new file mode 100644 index 0000000000..7764eecd9c --- /dev/null +++ b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-3B-Instruct.json @@ -0,0 +1,1022 @@ +{ + "model.layers.0.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.1.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.2.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.3.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.4.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.5.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.6.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.7.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.8.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.9.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.10.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.11.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.12.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.13.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.14.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.15.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.16.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.17.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.18.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.19.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.20.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.21.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.22.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.23.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.24.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.25.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.26.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.27.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "lm_head": { + "snr": Infinity, + "type": "lm_head" + }, + "model.layers.0.mlp.down_proj": { + "snr": 2.306217670440674, + "type": "mlp.down_proj" + }, + "model.layers.1.mlp.down_proj": { + "snr": 2.2327167987823486, + "type": "mlp.down_proj" + }, + "model.layers.2.mlp.down_proj": { + "snr": 1.4501516819000244, + "type": "mlp.down_proj" + }, + "model.layers.3.mlp.down_proj": { + "snr": 1.363667607307434, + "type": "mlp.down_proj" + }, + "model.layers.4.mlp.down_proj": { + "snr": 1.4520279169082642, + "type": "mlp.down_proj" + }, + "model.layers.5.mlp.down_proj": { + "snr": 1.4664665460586548, + "type": "mlp.down_proj" + }, + "model.layers.6.mlp.down_proj": { + "snr": 1.4122329950332642, + "type": "mlp.down_proj" + }, + "model.layers.7.mlp.down_proj": { + "snr": 1.0504299402236938, + "type": "mlp.down_proj" + }, + "model.layers.8.mlp.down_proj": { + "snr": 0.9837537407875061, + "type": "mlp.down_proj" + }, + "model.layers.9.mlp.down_proj": { + "snr": 0.8659006357192993, + "type": "mlp.down_proj" + }, + "model.layers.10.mlp.down_proj": { + "snr": 0.7936406135559082, + "type": "mlp.down_proj" + }, + "model.layers.11.mlp.down_proj": { + "snr": 0.9000886678695679, + "type": "mlp.down_proj" + }, + "model.layers.12.mlp.down_proj": { + "snr": 1.1559213399887085, + "type": "mlp.down_proj" + }, + "model.layers.13.mlp.down_proj": { + "snr": 1.3054672479629517, + "type": "mlp.down_proj" + }, + "model.layers.14.mlp.down_proj": { + "snr": 1.196791410446167, + "type": "mlp.down_proj" + }, + "model.layers.15.mlp.down_proj": { + "snr": 1.3163655996322632, + "type": "mlp.down_proj" + }, + "model.layers.16.mlp.down_proj": { + "snr": 1.3388997316360474, + "type": "mlp.down_proj" + }, + "model.layers.17.mlp.down_proj": { + "snr": 1.592497706413269, + "type": "mlp.down_proj" + }, + "model.layers.18.mlp.down_proj": { + "snr": 1.5399079322814941, + "type": "mlp.down_proj" + }, + "model.layers.19.mlp.down_proj": { + "snr": 1.5683293342590332, + "type": "mlp.down_proj" + }, + "model.layers.20.mlp.down_proj": { + "snr": 1.4739630222320557, + "type": "mlp.down_proj" + }, + "model.layers.21.mlp.down_proj": { + "snr": 1.2608393430709839, + "type": "mlp.down_proj" + }, + "model.layers.22.mlp.down_proj": { + "snr": 1.2087301015853882, + "type": "mlp.down_proj" + }, + "model.layers.23.mlp.down_proj": { + "snr": 1.1851829290390015, + "type": "mlp.down_proj" + }, + "model.layers.24.mlp.down_proj": { + "snr": 1.0537594556808472, + "type": "mlp.down_proj" + }, + "model.layers.25.mlp.down_proj": { + "snr": 1.1649317741394043, + "type": "mlp.down_proj" + }, + "model.layers.26.mlp.down_proj": { + "snr": 1.2376821041107178, + "type": "mlp.down_proj" + }, + "model.layers.27.mlp.down_proj": { + "snr": 1.147771954536438, + "type": "mlp.down_proj" + }, + "model.layers.0.mlp.gate_proj": { + "snr": 0.9385462999343872, + "type": "mlp.gate_proj" + }, + "model.layers.1.mlp.gate_proj": { + "snr": 0.8528683185577393, + "type": "mlp.gate_proj" + }, + "model.layers.2.mlp.gate_proj": { + "snr": 0.761657178401947, + "type": "mlp.gate_proj" + }, + "model.layers.3.mlp.gate_proj": { + "snr": 0.6598325371742249, + "type": "mlp.gate_proj" + }, + "model.layers.4.mlp.gate_proj": { + "snr": 0.44578588008880615, + "type": "mlp.gate_proj" + }, + "model.layers.5.mlp.gate_proj": { + "snr": 0.4053060710430145, + "type": "mlp.gate_proj" + }, + "model.layers.6.mlp.gate_proj": { + "snr": 0.3588462769985199, + "type": "mlp.gate_proj" + }, + "model.layers.7.mlp.gate_proj": { + "snr": 0.35667839646339417, + "type": "mlp.gate_proj" + }, + "model.layers.8.mlp.gate_proj": { + "snr": 0.3106202781200409, + "type": "mlp.gate_proj" + }, + "model.layers.9.mlp.gate_proj": { + "snr": 0.2821919322013855, + "type": "mlp.gate_proj" + }, + "model.layers.10.mlp.gate_proj": { + "snr": 0.29143741726875305, + "type": "mlp.gate_proj" + }, + "model.layers.11.mlp.gate_proj": { + "snr": 0.29830989241600037, + "type": "mlp.gate_proj" + }, + "model.layers.12.mlp.gate_proj": { + "snr": 0.2862427532672882, + "type": "mlp.gate_proj" + }, + "model.layers.13.mlp.gate_proj": { + "snr": 0.2797018587589264, + "type": "mlp.gate_proj" + }, + "model.layers.14.mlp.gate_proj": { + "snr": 0.2679217755794525, + "type": "mlp.gate_proj" + }, + "model.layers.15.mlp.gate_proj": { + "snr": 0.2782425880432129, + "type": "mlp.gate_proj" + }, + "model.layers.16.mlp.gate_proj": { + "snr": 0.3503592610359192, + "type": "mlp.gate_proj" + }, + "model.layers.17.mlp.gate_proj": { + "snr": 0.3968559205532074, + "type": "mlp.gate_proj" + }, + "model.layers.18.mlp.gate_proj": { + "snr": 0.4318574070930481, + "type": "mlp.gate_proj" + }, + "model.layers.19.mlp.gate_proj": { + "snr": 0.4693693220615387, + "type": "mlp.gate_proj" + }, + "model.layers.20.mlp.gate_proj": { + "snr": 0.5051979422569275, + "type": "mlp.gate_proj" + }, + "model.layers.21.mlp.gate_proj": { + "snr": 0.5675955414772034, + "type": "mlp.gate_proj" + }, + "model.layers.22.mlp.gate_proj": { + "snr": 0.5861843824386597, + "type": "mlp.gate_proj" + }, + "model.layers.23.mlp.gate_proj": { + "snr": 0.4759417772293091, + "type": "mlp.gate_proj" + }, + "model.layers.24.mlp.gate_proj": { + "snr": 0.38529056310653687, + "type": "mlp.gate_proj" + }, + "model.layers.25.mlp.gate_proj": { + "snr": 0.3180919587612152, + "type": "mlp.gate_proj" + }, + "model.layers.26.mlp.gate_proj": { + "snr": 0.2695689797401428, + "type": "mlp.gate_proj" + }, + "model.layers.27.mlp.gate_proj": { + "snr": 0.21765239536762238, + "type": "mlp.gate_proj" + }, + "model.layers.0.mlp.up_proj": { + "snr": 1.4919718503952026, + "type": "mlp.up_proj" + }, + "model.layers.1.mlp.up_proj": { + "snr": 1.7983858585357666, + "type": "mlp.up_proj" + }, + "model.layers.2.mlp.up_proj": { + "snr": 2.1709094047546387, + "type": "mlp.up_proj" + }, + "model.layers.3.mlp.up_proj": { + "snr": 2.751326560974121, + "type": "mlp.up_proj" + }, + "model.layers.4.mlp.up_proj": { + "snr": 3.063521385192871, + "type": "mlp.up_proj" + }, + "model.layers.5.mlp.up_proj": { + "snr": 2.4026951789855957, + "type": "mlp.up_proj" + }, + "model.layers.6.mlp.up_proj": { + "snr": 2.3890223503112793, + "type": "mlp.up_proj" + }, + "model.layers.7.mlp.up_proj": { + "snr": 2.3861353397369385, + "type": "mlp.up_proj" + }, + "model.layers.8.mlp.up_proj": { + "snr": 2.0745043754577637, + "type": "mlp.up_proj" + }, + "model.layers.9.mlp.up_proj": { + "snr": 1.8550645112991333, + "type": "mlp.up_proj" + }, + "model.layers.10.mlp.up_proj": { + "snr": 1.6184496879577637, + "type": "mlp.up_proj" + }, + "model.layers.11.mlp.up_proj": { + "snr": 1.9287559986114502, + "type": "mlp.up_proj" + }, + "model.layers.12.mlp.up_proj": { + "snr": 1.7427546977996826, + "type": "mlp.up_proj" + }, + "model.layers.13.mlp.up_proj": { + "snr": 1.9872609376907349, + "type": "mlp.up_proj" + }, + "model.layers.14.mlp.up_proj": { + "snr": 2.0224087238311768, + "type": "mlp.up_proj" + }, + "model.layers.15.mlp.up_proj": { + "snr": 1.7851638793945312, + "type": "mlp.up_proj" + }, + "model.layers.16.mlp.up_proj": { + "snr": 1.7160604000091553, + "type": "mlp.up_proj" + }, + "model.layers.17.mlp.up_proj": { + "snr": 1.6870195865631104, + "type": "mlp.up_proj" + }, + "model.layers.18.mlp.up_proj": { + "snr": 1.6585396528244019, + "type": "mlp.up_proj" + }, + "model.layers.19.mlp.up_proj": { + "snr": 1.5509096384048462, + "type": "mlp.up_proj" + }, + "model.layers.20.mlp.up_proj": { + "snr": 1.4310423135757446, + "type": "mlp.up_proj" + }, + "model.layers.21.mlp.up_proj": { + "snr": 1.5009464025497437, + "type": "mlp.up_proj" + }, + "model.layers.22.mlp.up_proj": { + "snr": 1.4866929054260254, + "type": "mlp.up_proj" + }, + "model.layers.23.mlp.up_proj": { + "snr": 1.332513689994812, + "type": "mlp.up_proj" + }, + "model.layers.24.mlp.up_proj": { + "snr": 1.073512077331543, + "type": "mlp.up_proj" + }, + "model.layers.25.mlp.up_proj": { + "snr": 0.7472100257873535, + "type": "mlp.up_proj" + }, + "model.layers.26.mlp.up_proj": { + "snr": 0.4880162179470062, + "type": "mlp.up_proj" + }, + "model.layers.27.mlp.up_proj": { + "snr": 0.2527681589126587, + "type": "mlp.up_proj" + }, + "model.embed_tokens": { + "snr": Infinity, + "type": "model.embed_tokens" + }, + "model.norm": { + "snr": Infinity, + "type": "model.norm" + }, + "model.layers.0.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.1.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.2.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.3.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.4.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.5.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.6.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.7.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.8.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.9.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.10.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.11.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.12.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.13.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.14.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.15.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.16.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.17.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.18.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.19.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.20.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.21.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.22.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.23.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.24.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.25.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.26.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.27.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.0.self_attn.k_proj": { + "snr": 0.08262510597705841, + "type": "self_attn.k_proj" + }, + "model.layers.1.self_attn.k_proj": { + "snr": 0.1441459059715271, + "type": "self_attn.k_proj" + }, + "model.layers.2.self_attn.k_proj": { + "snr": 0.21418076753616333, + "type": "self_attn.k_proj" + }, + "model.layers.3.self_attn.k_proj": { + "snr": 0.22496014833450317, + "type": "self_attn.k_proj" + }, + "model.layers.4.self_attn.k_proj": { + "snr": 0.23101305961608887, + "type": "self_attn.k_proj" + }, + "model.layers.5.self_attn.k_proj": { + "snr": 0.23644132912158966, + "type": "self_attn.k_proj" + }, + "model.layers.6.self_attn.k_proj": { + "snr": 0.23666173219680786, + "type": "self_attn.k_proj" + }, + "model.layers.7.self_attn.k_proj": { + "snr": 0.19791515171527863, + "type": "self_attn.k_proj" + }, + "model.layers.8.self_attn.k_proj": { + "snr": 0.22062039375305176, + "type": "self_attn.k_proj" + }, + "model.layers.9.self_attn.k_proj": { + "snr": 0.21218444406986237, + "type": "self_attn.k_proj" + }, + "model.layers.10.self_attn.k_proj": { + "snr": 0.24218571186065674, + "type": "self_attn.k_proj" + }, + "model.layers.11.self_attn.k_proj": { + "snr": 0.21870514750480652, + "type": "self_attn.k_proj" + }, + "model.layers.12.self_attn.k_proj": { + "snr": 0.22160987555980682, + "type": "self_attn.k_proj" + }, + "model.layers.13.self_attn.k_proj": { + "snr": 0.22726823389530182, + "type": "self_attn.k_proj" + }, + "model.layers.14.self_attn.k_proj": { + "snr": 0.20256873965263367, + "type": "self_attn.k_proj" + }, + "model.layers.15.self_attn.k_proj": { + "snr": 0.24100735783576965, + "type": "self_attn.k_proj" + }, + "model.layers.16.self_attn.k_proj": { + "snr": 0.23794010281562805, + "type": "self_attn.k_proj" + }, + "model.layers.17.self_attn.k_proj": { + "snr": 0.2913324534893036, + "type": "self_attn.k_proj" + }, + "model.layers.18.self_attn.k_proj": { + "snr": 0.28093472123146057, + "type": "self_attn.k_proj" + }, + "model.layers.19.self_attn.k_proj": { + "snr": 0.31062793731689453, + "type": "self_attn.k_proj" + }, + "model.layers.20.self_attn.k_proj": { + "snr": 0.2942160367965698, + "type": "self_attn.k_proj" + }, + "model.layers.21.self_attn.k_proj": { + "snr": 0.28014805912971497, + "type": "self_attn.k_proj" + }, + "model.layers.22.self_attn.k_proj": { + "snr": 0.3512437045574188, + "type": "self_attn.k_proj" + }, + "model.layers.23.self_attn.k_proj": { + "snr": 0.2837671637535095, + "type": "self_attn.k_proj" + }, + "model.layers.24.self_attn.k_proj": { + "snr": 0.2960015535354614, + "type": "self_attn.k_proj" + }, + "model.layers.25.self_attn.k_proj": { + "snr": 0.5086414813995361, + "type": "self_attn.k_proj" + }, + "model.layers.26.self_attn.k_proj": { + "snr": 0.24054698646068573, + "type": "self_attn.k_proj" + }, + "model.layers.27.self_attn.k_proj": { + "snr": 0.247616246342659, + "type": "self_attn.k_proj" + }, + "model.layers.0.self_attn.o_proj": { + "snr": 0.18390265107154846, + "type": "self_attn.o_proj" + }, + "model.layers.1.self_attn.o_proj": { + "snr": 0.14759540557861328, + "type": "self_attn.o_proj" + }, + "model.layers.2.self_attn.o_proj": { + "snr": 0.15726515650749207, + "type": "self_attn.o_proj" + }, + "model.layers.3.self_attn.o_proj": { + "snr": 0.16903570294380188, + "type": "self_attn.o_proj" + }, + "model.layers.4.self_attn.o_proj": { + "snr": 0.17953157424926758, + "type": "self_attn.o_proj" + }, + "model.layers.5.self_attn.o_proj": { + "snr": 0.2351229190826416, + "type": "self_attn.o_proj" + }, + "model.layers.6.self_attn.o_proj": { + "snr": 0.22804339230060577, + "type": "self_attn.o_proj" + }, + "model.layers.7.self_attn.o_proj": { + "snr": 0.24786025285720825, + "type": "self_attn.o_proj" + }, + "model.layers.8.self_attn.o_proj": { + "snr": 0.21847976744174957, + "type": "self_attn.o_proj" + }, + "model.layers.9.self_attn.o_proj": { + "snr": 0.2092437595129013, + "type": "self_attn.o_proj" + }, + "model.layers.10.self_attn.o_proj": { + "snr": 0.23278094828128815, + "type": "self_attn.o_proj" + }, + "model.layers.11.self_attn.o_proj": { + "snr": 0.20468176901340485, + "type": "self_attn.o_proj" + }, + "model.layers.12.self_attn.o_proj": { + "snr": 0.2353818416595459, + "type": "self_attn.o_proj" + }, + "model.layers.13.self_attn.o_proj": { + "snr": 0.2702614367008209, + "type": "self_attn.o_proj" + }, + "model.layers.14.self_attn.o_proj": { + "snr": 0.19177420437335968, + "type": "self_attn.o_proj" + }, + "model.layers.15.self_attn.o_proj": { + "snr": 0.18293911218643188, + "type": "self_attn.o_proj" + }, + "model.layers.16.self_attn.o_proj": { + "snr": 0.20286045968532562, + "type": "self_attn.o_proj" + }, + "model.layers.17.self_attn.o_proj": { + "snr": 0.20763878524303436, + "type": "self_attn.o_proj" + }, + "model.layers.18.self_attn.o_proj": { + "snr": 0.190629780292511, + "type": "self_attn.o_proj" + }, + "model.layers.19.self_attn.o_proj": { + "snr": 0.22044304013252258, + "type": "self_attn.o_proj" + }, + "model.layers.20.self_attn.o_proj": { + "snr": 0.21491236984729767, + "type": "self_attn.o_proj" + }, + "model.layers.21.self_attn.o_proj": { + "snr": 0.23289704322814941, + "type": "self_attn.o_proj" + }, + "model.layers.22.self_attn.o_proj": { + "snr": 0.21457163989543915, + "type": "self_attn.o_proj" + }, + "model.layers.23.self_attn.o_proj": { + "snr": 0.1949365884065628, + "type": "self_attn.o_proj" + }, + "model.layers.24.self_attn.o_proj": { + "snr": 0.1606779545545578, + "type": "self_attn.o_proj" + }, + "model.layers.25.self_attn.o_proj": { + "snr": 0.13892440497875214, + "type": "self_attn.o_proj" + }, + "model.layers.26.self_attn.o_proj": { + "snr": 0.1407029926776886, + "type": "self_attn.o_proj" + }, + "model.layers.27.self_attn.o_proj": { + "snr": 0.16027599573135376, + "type": "self_attn.o_proj" + }, + "model.layers.0.self_attn.q_proj": { + "snr": 0.0534212663769722, + "type": "self_attn.q_proj" + }, + "model.layers.1.self_attn.q_proj": { + "snr": 0.06873775273561478, + "type": "self_attn.q_proj" + }, + "model.layers.2.self_attn.q_proj": { + "snr": 0.07522258907556534, + "type": "self_attn.q_proj" + }, + "model.layers.3.self_attn.q_proj": { + "snr": 0.06616844981908798, + "type": "self_attn.q_proj" + }, + "model.layers.4.self_attn.q_proj": { + "snr": 0.06809444725513458, + "type": "self_attn.q_proj" + }, + "model.layers.5.self_attn.q_proj": { + "snr": 0.0758095383644104, + "type": "self_attn.q_proj" + }, + "model.layers.6.self_attn.q_proj": { + "snr": 0.07800278812646866, + "type": "self_attn.q_proj" + }, + "model.layers.7.self_attn.q_proj": { + "snr": 0.07535763084888458, + "type": "self_attn.q_proj" + }, + "model.layers.8.self_attn.q_proj": { + "snr": 0.09488166123628616, + "type": "self_attn.q_proj" + }, + "model.layers.9.self_attn.q_proj": { + "snr": 0.09709945321083069, + "type": "self_attn.q_proj" + }, + "model.layers.10.self_attn.q_proj": { + "snr": 0.09381720423698425, + "type": "self_attn.q_proj" + }, + "model.layers.11.self_attn.q_proj": { + "snr": 0.08205580711364746, + "type": "self_attn.q_proj" + }, + "model.layers.12.self_attn.q_proj": { + "snr": 0.10723169893026352, + "type": "self_attn.q_proj" + }, + "model.layers.13.self_attn.q_proj": { + "snr": 0.10166660696268082, + "type": "self_attn.q_proj" + }, + "model.layers.14.self_attn.q_proj": { + "snr": 0.08822792023420334, + "type": "self_attn.q_proj" + }, + "model.layers.15.self_attn.q_proj": { + "snr": 0.0814041867852211, + "type": "self_attn.q_proj" + }, + "model.layers.16.self_attn.q_proj": { + "snr": 0.07586681097745895, + "type": "self_attn.q_proj" + }, + "model.layers.17.self_attn.q_proj": { + "snr": 0.07040166854858398, + "type": "self_attn.q_proj" + }, + "model.layers.18.self_attn.q_proj": { + "snr": 0.0728282704949379, + "type": "self_attn.q_proj" + }, + "model.layers.19.self_attn.q_proj": { + "snr": 0.06912193447351456, + "type": "self_attn.q_proj" + }, + "model.layers.20.self_attn.q_proj": { + "snr": 0.06646180897951126, + "type": "self_attn.q_proj" + }, + "model.layers.21.self_attn.q_proj": { + "snr": 0.06960278004407883, + "type": "self_attn.q_proj" + }, + "model.layers.22.self_attn.q_proj": { + "snr": 0.06566876918077469, + "type": "self_attn.q_proj" + }, + "model.layers.23.self_attn.q_proj": { + "snr": 0.07412787526845932, + "type": "self_attn.q_proj" + }, + "model.layers.24.self_attn.q_proj": { + "snr": 0.07131384313106537, + "type": "self_attn.q_proj" + }, + "model.layers.25.self_attn.q_proj": { + "snr": 0.07768437266349792, + "type": "self_attn.q_proj" + }, + "model.layers.26.self_attn.q_proj": { + "snr": 0.0809575766324997, + "type": "self_attn.q_proj" + }, + "model.layers.27.self_attn.q_proj": { + "snr": 0.06796683371067047, + "type": "self_attn.q_proj" + }, + "model.layers.0.self_attn.v_proj": { + "snr": 1.4029983282089233, + "type": "self_attn.v_proj" + }, + "model.layers.1.self_attn.v_proj": { + "snr": 3.123720169067383, + "type": "self_attn.v_proj" + }, + "model.layers.2.self_attn.v_proj": { + "snr": 2.4177253246307373, + "type": "self_attn.v_proj" + }, + "model.layers.3.self_attn.v_proj": { + "snr": 5.588768005371094, + "type": "self_attn.v_proj" + }, + "model.layers.4.self_attn.v_proj": { + "snr": 4.395562648773193, + "type": "self_attn.v_proj" + }, + "model.layers.5.self_attn.v_proj": { + "snr": 3.2982685565948486, + "type": "self_attn.v_proj" + }, + "model.layers.6.self_attn.v_proj": { + "snr": 3.2798449993133545, + "type": "self_attn.v_proj" + }, + "model.layers.7.self_attn.v_proj": { + "snr": 2.109200954437256, + "type": "self_attn.v_proj" + }, + "model.layers.8.self_attn.v_proj": { + "snr": 3.229325532913208, + "type": "self_attn.v_proj" + }, + "model.layers.9.self_attn.v_proj": { + "snr": 1.7349927425384521, + "type": "self_attn.v_proj" + }, + "model.layers.10.self_attn.v_proj": { + "snr": 1.5926740169525146, + "type": "self_attn.v_proj" + }, + "model.layers.11.self_attn.v_proj": { + "snr": 1.9097802639007568, + "type": "self_attn.v_proj" + }, + "model.layers.12.self_attn.v_proj": { + "snr": 2.5654332637786865, + "type": "self_attn.v_proj" + }, + "model.layers.13.self_attn.v_proj": { + "snr": 3.536489963531494, + "type": "self_attn.v_proj" + }, + "model.layers.14.self_attn.v_proj": { + "snr": 8.366667747497559, + "type": "self_attn.v_proj" + }, + "model.layers.15.self_attn.v_proj": { + "snr": 7.348303318023682, + "type": "self_attn.v_proj" + }, + "model.layers.16.self_attn.v_proj": { + "snr": 2.815748691558838, + "type": "self_attn.v_proj" + }, + "model.layers.17.self_attn.v_proj": { + "snr": 4.048776149749756, + "type": "self_attn.v_proj" + }, + "model.layers.18.self_attn.v_proj": { + "snr": 4.426101207733154, + "type": "self_attn.v_proj" + }, + "model.layers.19.self_attn.v_proj": { + "snr": 7.098501682281494, + "type": "self_attn.v_proj" + }, + "model.layers.20.self_attn.v_proj": { + "snr": 3.700288772583008, + "type": "self_attn.v_proj" + }, + "model.layers.21.self_attn.v_proj": { + "snr": 2.1859049797058105, + "type": "self_attn.v_proj" + }, + "model.layers.22.self_attn.v_proj": { + "snr": 3.6953284740448, + "type": "self_attn.v_proj" + }, + "model.layers.23.self_attn.v_proj": { + "snr": 11.148802757263184, + "type": "self_attn.v_proj" + }, + "model.layers.24.self_attn.v_proj": { + "snr": 2.4171905517578125, + "type": "self_attn.v_proj" + }, + "model.layers.25.self_attn.v_proj": { + "snr": 4.404144287109375, + "type": "self_attn.v_proj" + }, + "model.layers.26.self_attn.v_proj": { + "snr": 2.340604782104492, + "type": "self_attn.v_proj" + }, + "model.layers.27.self_attn.v_proj": { + "snr": 3.284160614013672, + "type": "self_attn.v_proj" + } +} diff --git a/src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-3B.json b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-3B.json new file mode 100644 index 0000000000..131a5b79b7 --- /dev/null +++ b/src/axolotl/integrations/spectrum/model_snr_results/snr_results_meta-llama-Llama-3.2-3B.json @@ -0,0 +1,1022 @@ +{ + "model.layers.0.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.1.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.2.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.3.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.4.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.5.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.6.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.7.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.8.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.9.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.10.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.11.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.12.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.13.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.14.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.15.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.16.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.17.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.18.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.19.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.20.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.21.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.22.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.23.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.24.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.25.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.26.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "model.layers.27.input_layernorm": { + "snr": Infinity, + "type": "input_layernorm" + }, + "lm_head": { + "snr": Infinity, + "type": "lm_head" + }, + "model.layers.0.mlp.down_proj": { + "snr": 2.364603281021118, + "type": "mlp.down_proj" + }, + "model.layers.1.mlp.down_proj": { + "snr": 2.229910373687744, + "type": "mlp.down_proj" + }, + "model.layers.2.mlp.down_proj": { + "snr": 1.4312117099761963, + "type": "mlp.down_proj" + }, + "model.layers.3.mlp.down_proj": { + "snr": 1.3216407299041748, + "type": "mlp.down_proj" + }, + "model.layers.4.mlp.down_proj": { + "snr": 1.4183496236801147, + "type": "mlp.down_proj" + }, + "model.layers.5.mlp.down_proj": { + "snr": 1.4453660249710083, + "type": "mlp.down_proj" + }, + "model.layers.6.mlp.down_proj": { + "snr": 1.4030662775039673, + "type": "mlp.down_proj" + }, + "model.layers.7.mlp.down_proj": { + "snr": 1.042332649230957, + "type": "mlp.down_proj" + }, + "model.layers.8.mlp.down_proj": { + "snr": 0.9530982375144958, + "type": "mlp.down_proj" + }, + "model.layers.9.mlp.down_proj": { + "snr": 0.849862277507782, + "type": "mlp.down_proj" + }, + "model.layers.10.mlp.down_proj": { + "snr": 0.7704945206642151, + "type": "mlp.down_proj" + }, + "model.layers.11.mlp.down_proj": { + "snr": 0.8871145844459534, + "type": "mlp.down_proj" + }, + "model.layers.12.mlp.down_proj": { + "snr": 1.1408143043518066, + "type": "mlp.down_proj" + }, + "model.layers.13.mlp.down_proj": { + "snr": 1.2769343852996826, + "type": "mlp.down_proj" + }, + "model.layers.14.mlp.down_proj": { + "snr": 1.1703068017959595, + "type": "mlp.down_proj" + }, + "model.layers.15.mlp.down_proj": { + "snr": 1.2794467210769653, + "type": "mlp.down_proj" + }, + "model.layers.16.mlp.down_proj": { + "snr": 1.3154453039169312, + "type": "mlp.down_proj" + }, + "model.layers.17.mlp.down_proj": { + "snr": 1.5596749782562256, + "type": "mlp.down_proj" + }, + "model.layers.18.mlp.down_proj": { + "snr": 1.4949405193328857, + "type": "mlp.down_proj" + }, + "model.layers.19.mlp.down_proj": { + "snr": 1.5329173803329468, + "type": "mlp.down_proj" + }, + "model.layers.20.mlp.down_proj": { + "snr": 1.4396660327911377, + "type": "mlp.down_proj" + }, + "model.layers.21.mlp.down_proj": { + "snr": 1.217085838317871, + "type": "mlp.down_proj" + }, + "model.layers.22.mlp.down_proj": { + "snr": 1.150472640991211, + "type": "mlp.down_proj" + }, + "model.layers.23.mlp.down_proj": { + "snr": 1.1166225671768188, + "type": "mlp.down_proj" + }, + "model.layers.24.mlp.down_proj": { + "snr": 0.9966591000556946, + "type": "mlp.down_proj" + }, + "model.layers.25.mlp.down_proj": { + "snr": 1.0938347578048706, + "type": "mlp.down_proj" + }, + "model.layers.26.mlp.down_proj": { + "snr": 1.1505423784255981, + "type": "mlp.down_proj" + }, + "model.layers.27.mlp.down_proj": { + "snr": 1.1156749725341797, + "type": "mlp.down_proj" + }, + "model.layers.0.mlp.gate_proj": { + "snr": 0.9329171776771545, + "type": "mlp.gate_proj" + }, + "model.layers.1.mlp.gate_proj": { + "snr": 0.8513413667678833, + "type": "mlp.gate_proj" + }, + "model.layers.2.mlp.gate_proj": { + "snr": 0.7584061026573181, + "type": "mlp.gate_proj" + }, + "model.layers.3.mlp.gate_proj": { + "snr": 0.65835040807724, + "type": "mlp.gate_proj" + }, + "model.layers.4.mlp.gate_proj": { + "snr": 0.436420738697052, + "type": "mlp.gate_proj" + }, + "model.layers.5.mlp.gate_proj": { + "snr": 0.39712461829185486, + "type": "mlp.gate_proj" + }, + "model.layers.6.mlp.gate_proj": { + "snr": 0.3530206084251404, + "type": "mlp.gate_proj" + }, + "model.layers.7.mlp.gate_proj": { + "snr": 0.34982794523239136, + "type": "mlp.gate_proj" + }, + "model.layers.8.mlp.gate_proj": { + "snr": 0.30338960886001587, + "type": "mlp.gate_proj" + }, + "model.layers.9.mlp.gate_proj": { + "snr": 0.27569833397865295, + "type": "mlp.gate_proj" + }, + "model.layers.10.mlp.gate_proj": { + "snr": 0.28934162855148315, + "type": "mlp.gate_proj" + }, + "model.layers.11.mlp.gate_proj": { + "snr": 0.2929173707962036, + "type": "mlp.gate_proj" + }, + "model.layers.12.mlp.gate_proj": { + "snr": 0.28263387084007263, + "type": "mlp.gate_proj" + }, + "model.layers.13.mlp.gate_proj": { + "snr": 0.27778616547584534, + "type": "mlp.gate_proj" + }, + "model.layers.14.mlp.gate_proj": { + "snr": 0.26527827978134155, + "type": "mlp.gate_proj" + }, + "model.layers.15.mlp.gate_proj": { + "snr": 0.27635642886161804, + "type": "mlp.gate_proj" + }, + "model.layers.16.mlp.gate_proj": { + "snr": 0.35072311758995056, + "type": "mlp.gate_proj" + }, + "model.layers.17.mlp.gate_proj": { + "snr": 0.4002636671066284, + "type": "mlp.gate_proj" + }, + "model.layers.18.mlp.gate_proj": { + "snr": 0.4319891333580017, + "type": "mlp.gate_proj" + }, + "model.layers.19.mlp.gate_proj": { + "snr": 0.47527065873146057, + "type": "mlp.gate_proj" + }, + "model.layers.20.mlp.gate_proj": { + "snr": 0.5112077593803406, + "type": "mlp.gate_proj" + }, + "model.layers.21.mlp.gate_proj": { + "snr": 0.5749644637107849, + "type": "mlp.gate_proj" + }, + "model.layers.22.mlp.gate_proj": { + "snr": 0.5967603921890259, + "type": "mlp.gate_proj" + }, + "model.layers.23.mlp.gate_proj": { + "snr": 0.48045310378074646, + "type": "mlp.gate_proj" + }, + "model.layers.24.mlp.gate_proj": { + "snr": 0.3838970363140106, + "type": "mlp.gate_proj" + }, + "model.layers.25.mlp.gate_proj": { + "snr": 0.3108249604701996, + "type": "mlp.gate_proj" + }, + "model.layers.26.mlp.gate_proj": { + "snr": 0.26704445481300354, + "type": "mlp.gate_proj" + }, + "model.layers.27.mlp.gate_proj": { + "snr": 0.20953254401683807, + "type": "mlp.gate_proj" + }, + "model.layers.0.mlp.up_proj": { + "snr": 1.5084924697875977, + "type": "mlp.up_proj" + }, + "model.layers.1.mlp.up_proj": { + "snr": 1.7789595127105713, + "type": "mlp.up_proj" + }, + "model.layers.2.mlp.up_proj": { + "snr": 2.1431775093078613, + "type": "mlp.up_proj" + }, + "model.layers.3.mlp.up_proj": { + "snr": 2.762744903564453, + "type": "mlp.up_proj" + }, + "model.layers.4.mlp.up_proj": { + "snr": 3.0324745178222656, + "type": "mlp.up_proj" + }, + "model.layers.5.mlp.up_proj": { + "snr": 2.3884809017181396, + "type": "mlp.up_proj" + }, + "model.layers.6.mlp.up_proj": { + "snr": 2.388005256652832, + "type": "mlp.up_proj" + }, + "model.layers.7.mlp.up_proj": { + "snr": 2.339340925216675, + "type": "mlp.up_proj" + }, + "model.layers.8.mlp.up_proj": { + "snr": 2.0497021675109863, + "type": "mlp.up_proj" + }, + "model.layers.9.mlp.up_proj": { + "snr": 1.822119116783142, + "type": "mlp.up_proj" + }, + "model.layers.10.mlp.up_proj": { + "snr": 1.600373387336731, + "type": "mlp.up_proj" + }, + "model.layers.11.mlp.up_proj": { + "snr": 1.9298171997070312, + "type": "mlp.up_proj" + }, + "model.layers.12.mlp.up_proj": { + "snr": 1.728783369064331, + "type": "mlp.up_proj" + }, + "model.layers.13.mlp.up_proj": { + "snr": 1.965298056602478, + "type": "mlp.up_proj" + }, + "model.layers.14.mlp.up_proj": { + "snr": 2.023681640625, + "type": "mlp.up_proj" + }, + "model.layers.15.mlp.up_proj": { + "snr": 1.7721818685531616, + "type": "mlp.up_proj" + }, + "model.layers.16.mlp.up_proj": { + "snr": 1.7068361043930054, + "type": "mlp.up_proj" + }, + "model.layers.17.mlp.up_proj": { + "snr": 1.6673219203948975, + "type": "mlp.up_proj" + }, + "model.layers.18.mlp.up_proj": { + "snr": 1.6240718364715576, + "type": "mlp.up_proj" + }, + "model.layers.19.mlp.up_proj": { + "snr": 1.5169662237167358, + "type": "mlp.up_proj" + }, + "model.layers.20.mlp.up_proj": { + "snr": 1.4018198251724243, + "type": "mlp.up_proj" + }, + "model.layers.21.mlp.up_proj": { + "snr": 1.4556466341018677, + "type": "mlp.up_proj" + }, + "model.layers.22.mlp.up_proj": { + "snr": 1.4304454326629639, + "type": "mlp.up_proj" + }, + "model.layers.23.mlp.up_proj": { + "snr": 1.2785290479660034, + "type": "mlp.up_proj" + }, + "model.layers.24.mlp.up_proj": { + "snr": 1.023495078086853, + "type": "mlp.up_proj" + }, + "model.layers.25.mlp.up_proj": { + "snr": 0.6992124915122986, + "type": "mlp.up_proj" + }, + "model.layers.26.mlp.up_proj": { + "snr": 0.4549211859703064, + "type": "mlp.up_proj" + }, + "model.layers.27.mlp.up_proj": { + "snr": 0.23889905214309692, + "type": "mlp.up_proj" + }, + "model.embed_tokens": { + "snr": Infinity, + "type": "model.embed_tokens" + }, + "model.norm": { + "snr": Infinity, + "type": "model.norm" + }, + "model.layers.0.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.1.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.2.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.3.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.4.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.5.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.6.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.7.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.8.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.9.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.10.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.11.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.12.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.13.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.14.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.15.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.16.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.17.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.18.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.19.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.20.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.21.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.22.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.23.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.24.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.25.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.26.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.27.post_attention_layernorm": { + "snr": Infinity, + "type": "post_attention_layernorm" + }, + "model.layers.0.self_attn.k_proj": { + "snr": 0.08150045573711395, + "type": "self_attn.k_proj" + }, + "model.layers.1.self_attn.k_proj": { + "snr": 0.1428358554840088, + "type": "self_attn.k_proj" + }, + "model.layers.2.self_attn.k_proj": { + "snr": 0.2096949815750122, + "type": "self_attn.k_proj" + }, + "model.layers.3.self_attn.k_proj": { + "snr": 0.22633400559425354, + "type": "self_attn.k_proj" + }, + "model.layers.4.self_attn.k_proj": { + "snr": 0.2293967455625534, + "type": "self_attn.k_proj" + }, + "model.layers.5.self_attn.k_proj": { + "snr": 0.23336802423000336, + "type": "self_attn.k_proj" + }, + "model.layers.6.self_attn.k_proj": { + "snr": 0.23429904878139496, + "type": "self_attn.k_proj" + }, + "model.layers.7.self_attn.k_proj": { + "snr": 0.19610290229320526, + "type": "self_attn.k_proj" + }, + "model.layers.8.self_attn.k_proj": { + "snr": 0.2163258045911789, + "type": "self_attn.k_proj" + }, + "model.layers.9.self_attn.k_proj": { + "snr": 0.21039333939552307, + "type": "self_attn.k_proj" + }, + "model.layers.10.self_attn.k_proj": { + "snr": 0.23533931374549866, + "type": "self_attn.k_proj" + }, + "model.layers.11.self_attn.k_proj": { + "snr": 0.21457058191299438, + "type": "self_attn.k_proj" + }, + "model.layers.12.self_attn.k_proj": { + "snr": 0.21686571836471558, + "type": "self_attn.k_proj" + }, + "model.layers.13.self_attn.k_proj": { + "snr": 0.22398065030574799, + "type": "self_attn.k_proj" + }, + "model.layers.14.self_attn.k_proj": { + "snr": 0.20160657167434692, + "type": "self_attn.k_proj" + }, + "model.layers.15.self_attn.k_proj": { + "snr": 0.23705022037029266, + "type": "self_attn.k_proj" + }, + "model.layers.16.self_attn.k_proj": { + "snr": 0.23254962265491486, + "type": "self_attn.k_proj" + }, + "model.layers.17.self_attn.k_proj": { + "snr": 0.2892642617225647, + "type": "self_attn.k_proj" + }, + "model.layers.18.self_attn.k_proj": { + "snr": 0.27587130665779114, + "type": "self_attn.k_proj" + }, + "model.layers.19.self_attn.k_proj": { + "snr": 0.30891212821006775, + "type": "self_attn.k_proj" + }, + "model.layers.20.self_attn.k_proj": { + "snr": 0.28997519612312317, + "type": "self_attn.k_proj" + }, + "model.layers.21.self_attn.k_proj": { + "snr": 0.27534863352775574, + "type": "self_attn.k_proj" + }, + "model.layers.22.self_attn.k_proj": { + "snr": 0.35139667987823486, + "type": "self_attn.k_proj" + }, + "model.layers.23.self_attn.k_proj": { + "snr": 0.2773109972476959, + "type": "self_attn.k_proj" + }, + "model.layers.24.self_attn.k_proj": { + "snr": 0.2853511571884155, + "type": "self_attn.k_proj" + }, + "model.layers.25.self_attn.k_proj": { + "snr": 0.5030262470245361, + "type": "self_attn.k_proj" + }, + "model.layers.26.self_attn.k_proj": { + "snr": 0.2317112237215042, + "type": "self_attn.k_proj" + }, + "model.layers.27.self_attn.k_proj": { + "snr": 0.24419328570365906, + "type": "self_attn.k_proj" + }, + "model.layers.0.self_attn.o_proj": { + "snr": 0.17767645418643951, + "type": "self_attn.o_proj" + }, + "model.layers.1.self_attn.o_proj": { + "snr": 0.14102177321910858, + "type": "self_attn.o_proj" + }, + "model.layers.2.self_attn.o_proj": { + "snr": 0.1523692011833191, + "type": "self_attn.o_proj" + }, + "model.layers.3.self_attn.o_proj": { + "snr": 0.16522075235843658, + "type": "self_attn.o_proj" + }, + "model.layers.4.self_attn.o_proj": { + "snr": 0.17483487725257874, + "type": "self_attn.o_proj" + }, + "model.layers.5.self_attn.o_proj": { + "snr": 0.227921262383461, + "type": "self_attn.o_proj" + }, + "model.layers.6.self_attn.o_proj": { + "snr": 0.2196175903081894, + "type": "self_attn.o_proj" + }, + "model.layers.7.self_attn.o_proj": { + "snr": 0.24270132184028625, + "type": "self_attn.o_proj" + }, + "model.layers.8.self_attn.o_proj": { + "snr": 0.2118290364742279, + "type": "self_attn.o_proj" + }, + "model.layers.9.self_attn.o_proj": { + "snr": 0.20525991916656494, + "type": "self_attn.o_proj" + }, + "model.layers.10.self_attn.o_proj": { + "snr": 0.22847208380699158, + "type": "self_attn.o_proj" + }, + "model.layers.11.self_attn.o_proj": { + "snr": 0.19665324687957764, + "type": "self_attn.o_proj" + }, + "model.layers.12.self_attn.o_proj": { + "snr": 0.23233532905578613, + "type": "self_attn.o_proj" + }, + "model.layers.13.self_attn.o_proj": { + "snr": 0.2624332308769226, + "type": "self_attn.o_proj" + }, + "model.layers.14.self_attn.o_proj": { + "snr": 0.1868327558040619, + "type": "self_attn.o_proj" + }, + "model.layers.15.self_attn.o_proj": { + "snr": 0.17706255614757538, + "type": "self_attn.o_proj" + }, + "model.layers.16.self_attn.o_proj": { + "snr": 0.19422705471515656, + "type": "self_attn.o_proj" + }, + "model.layers.17.self_attn.o_proj": { + "snr": 0.2000615894794464, + "type": "self_attn.o_proj" + }, + "model.layers.18.self_attn.o_proj": { + "snr": 0.1874573826789856, + "type": "self_attn.o_proj" + }, + "model.layers.19.self_attn.o_proj": { + "snr": 0.21297843754291534, + "type": "self_attn.o_proj" + }, + "model.layers.20.self_attn.o_proj": { + "snr": 0.2100859135389328, + "type": "self_attn.o_proj" + }, + "model.layers.21.self_attn.o_proj": { + "snr": 0.22561520338058472, + "type": "self_attn.o_proj" + }, + "model.layers.22.self_attn.o_proj": { + "snr": 0.20994484424591064, + "type": "self_attn.o_proj" + }, + "model.layers.23.self_attn.o_proj": { + "snr": 0.18978221714496613, + "type": "self_attn.o_proj" + }, + "model.layers.24.self_attn.o_proj": { + "snr": 0.1571759581565857, + "type": "self_attn.o_proj" + }, + "model.layers.25.self_attn.o_proj": { + "snr": 0.1349896937608719, + "type": "self_attn.o_proj" + }, + "model.layers.26.self_attn.o_proj": { + "snr": 0.1368866115808487, + "type": "self_attn.o_proj" + }, + "model.layers.27.self_attn.o_proj": { + "snr": 0.1571887582540512, + "type": "self_attn.o_proj" + }, + "model.layers.0.self_attn.q_proj": { + "snr": 0.05295897275209427, + "type": "self_attn.q_proj" + }, + "model.layers.1.self_attn.q_proj": { + "snr": 0.06835605204105377, + "type": "self_attn.q_proj" + }, + "model.layers.2.self_attn.q_proj": { + "snr": 0.0746372863650322, + "type": "self_attn.q_proj" + }, + "model.layers.3.self_attn.q_proj": { + "snr": 0.06615085154771805, + "type": "self_attn.q_proj" + }, + "model.layers.4.self_attn.q_proj": { + "snr": 0.06788161396980286, + "type": "self_attn.q_proj" + }, + "model.layers.5.self_attn.q_proj": { + "snr": 0.07514483481645584, + "type": "self_attn.q_proj" + }, + "model.layers.6.self_attn.q_proj": { + "snr": 0.07777862250804901, + "type": "self_attn.q_proj" + }, + "model.layers.7.self_attn.q_proj": { + "snr": 0.07534090429544449, + "type": "self_attn.q_proj" + }, + "model.layers.8.self_attn.q_proj": { + "snr": 0.09494179487228394, + "type": "self_attn.q_proj" + }, + "model.layers.9.self_attn.q_proj": { + "snr": 0.09699037671089172, + "type": "self_attn.q_proj" + }, + "model.layers.10.self_attn.q_proj": { + "snr": 0.09426294267177582, + "type": "self_attn.q_proj" + }, + "model.layers.11.self_attn.q_proj": { + "snr": 0.08260341733694077, + "type": "self_attn.q_proj" + }, + "model.layers.12.self_attn.q_proj": { + "snr": 0.10650420933961868, + "type": "self_attn.q_proj" + }, + "model.layers.13.self_attn.q_proj": { + "snr": 0.10250870138406754, + "type": "self_attn.q_proj" + }, + "model.layers.14.self_attn.q_proj": { + "snr": 0.08775162696838379, + "type": "self_attn.q_proj" + }, + "model.layers.15.self_attn.q_proj": { + "snr": 0.08071447163820267, + "type": "self_attn.q_proj" + }, + "model.layers.16.self_attn.q_proj": { + "snr": 0.07530857622623444, + "type": "self_attn.q_proj" + }, + "model.layers.17.self_attn.q_proj": { + "snr": 0.06964966654777527, + "type": "self_attn.q_proj" + }, + "model.layers.18.self_attn.q_proj": { + "snr": 0.07150755077600479, + "type": "self_attn.q_proj" + }, + "model.layers.19.self_attn.q_proj": { + "snr": 0.0676807165145874, + "type": "self_attn.q_proj" + }, + "model.layers.20.self_attn.q_proj": { + "snr": 0.06511317938566208, + "type": "self_attn.q_proj" + }, + "model.layers.21.self_attn.q_proj": { + "snr": 0.06773187220096588, + "type": "self_attn.q_proj" + }, + "model.layers.22.self_attn.q_proj": { + "snr": 0.06400436162948608, + "type": "self_attn.q_proj" + }, + "model.layers.23.self_attn.q_proj": { + "snr": 0.0726117342710495, + "type": "self_attn.q_proj" + }, + "model.layers.24.self_attn.q_proj": { + "snr": 0.06882446259260178, + "type": "self_attn.q_proj" + }, + "model.layers.25.self_attn.q_proj": { + "snr": 0.07506493479013443, + "type": "self_attn.q_proj" + }, + "model.layers.26.self_attn.q_proj": { + "snr": 0.07797915488481522, + "type": "self_attn.q_proj" + }, + "model.layers.27.self_attn.q_proj": { + "snr": 0.06680692732334137, + "type": "self_attn.q_proj" + }, + "model.layers.0.self_attn.v_proj": { + "snr": 1.326789379119873, + "type": "self_attn.v_proj" + }, + "model.layers.1.self_attn.v_proj": { + "snr": 3.043806791305542, + "type": "self_attn.v_proj" + }, + "model.layers.2.self_attn.v_proj": { + "snr": 2.295107841491699, + "type": "self_attn.v_proj" + }, + "model.layers.3.self_attn.v_proj": { + "snr": 5.2584614753723145, + "type": "self_attn.v_proj" + }, + "model.layers.4.self_attn.v_proj": { + "snr": 4.038785934448242, + "type": "self_attn.v_proj" + }, + "model.layers.5.self_attn.v_proj": { + "snr": 3.0907773971557617, + "type": "self_attn.v_proj" + }, + "model.layers.6.self_attn.v_proj": { + "snr": 3.114994525909424, + "type": "self_attn.v_proj" + }, + "model.layers.7.self_attn.v_proj": { + "snr": 1.9747973680496216, + "type": "self_attn.v_proj" + }, + "model.layers.8.self_attn.v_proj": { + "snr": 3.0469374656677246, + "type": "self_attn.v_proj" + }, + "model.layers.9.self_attn.v_proj": { + "snr": 1.602966547012329, + "type": "self_attn.v_proj" + }, + "model.layers.10.self_attn.v_proj": { + "snr": 1.489019513130188, + "type": "self_attn.v_proj" + }, + "model.layers.11.self_attn.v_proj": { + "snr": 1.7490826845169067, + "type": "self_attn.v_proj" + }, + "model.layers.12.self_attn.v_proj": { + "snr": 2.451310396194458, + "type": "self_attn.v_proj" + }, + "model.layers.13.self_attn.v_proj": { + "snr": 3.250821590423584, + "type": "self_attn.v_proj" + }, + "model.layers.14.self_attn.v_proj": { + "snr": 7.944663047790527, + "type": "self_attn.v_proj" + }, + "model.layers.15.self_attn.v_proj": { + "snr": 7.013208389282227, + "type": "self_attn.v_proj" + }, + "model.layers.16.self_attn.v_proj": { + "snr": 2.68644118309021, + "type": "self_attn.v_proj" + }, + "model.layers.17.self_attn.v_proj": { + "snr": 3.9063122272491455, + "type": "self_attn.v_proj" + }, + "model.layers.18.self_attn.v_proj": { + "snr": 4.1816816329956055, + "type": "self_attn.v_proj" + }, + "model.layers.19.self_attn.v_proj": { + "snr": 6.794488906860352, + "type": "self_attn.v_proj" + }, + "model.layers.20.self_attn.v_proj": { + "snr": 3.401334285736084, + "type": "self_attn.v_proj" + }, + "model.layers.21.self_attn.v_proj": { + "snr": 2.051994562149048, + "type": "self_attn.v_proj" + }, + "model.layers.22.self_attn.v_proj": { + "snr": 3.614379405975342, + "type": "self_attn.v_proj" + }, + "model.layers.23.self_attn.v_proj": { + "snr": 11.180968284606934, + "type": "self_attn.v_proj" + }, + "model.layers.24.self_attn.v_proj": { + "snr": 2.3629775047302246, + "type": "self_attn.v_proj" + }, + "model.layers.25.self_attn.v_proj": { + "snr": 4.137593746185303, + "type": "self_attn.v_proj" + }, + "model.layers.26.self_attn.v_proj": { + "snr": 2.3465518951416016, + "type": "self_attn.v_proj" + }, + "model.layers.27.self_attn.v_proj": { + "snr": 3.10064697265625, + "type": "self_attn.v_proj" + } +} From b7616022abb842839b4547e93373bb967054566d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 7 Feb 2025 21:33:44 -0500 Subject: [PATCH 0407/1405] bump transformers to 4.48.3 (#2318) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bea221ce21..b82c8f7d39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ liger-kernel==0.5.2 packaging==23.2 peft==0.14.0 -transformers==4.48.1 +transformers==4.48.3 tokenizers>=0.21.0 accelerate==1.3.0 datasets==3.2.0 From e48e2df4dda8c4eb7061eeab58bc15075550f2b8 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 8 Feb 2025 09:34:01 +0700 Subject: [PATCH 0408/1405] feat: update FA to 2.7.4.post1 which includes torch2.6 binary (#2315) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b82c8f7d39..37a3f0d646 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ bitsandbytes==0.45.2 triton>=3.0.0 mamba-ssm==1.2.0.post1 -flash-attn==2.7.0.post2 +flash-attn==2.7.4.post1 xformers>=0.0.23.post1 autoawq==0.2.7.post3 liger-kernel==0.5.2 From fd8cb32547bf09c852a34d355cfa19f4a91ab493 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 8 Feb 2025 09:34:16 +0700 Subject: [PATCH 0409/1405] chore: remove redundant py310 from tests (#2316) --- .github/workflows/base.yml | 6 ------ .github/workflows/docs.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/multi-gpu-e2e.yml | 2 +- .github/workflows/pypi.yml | 2 +- .github/workflows/tests-nightly.yml | 13 +++---------- .github/workflows/tests.yml | 11 ++--------- README.md | 2 +- 8 files changed, 10 insertions(+), 30 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 41b3a7c84f..e45436e930 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -22,12 +22,6 @@ jobs: fail-fast: false matrix: include: - - cuda: "124" - cuda_version: 12.4.1 - cudnn_version: "" - python_version: "3.10" - pytorch: 2.4.1 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "124" cuda_version: 12.4.1 cudnn_version: "" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f471296d3a..1138d99f10 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.11' - name: install dependencies run: | python3 -m pip install jupyter diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 31695c0e57..d85892b43a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,6 +19,6 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" cache: 'pip' # caching pip dependencies - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index ffd9a86b4f..6c9c7bb499 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -49,7 +49,7 @@ jobs: - name: Install Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Install Modal run: | python -m pip install --upgrade pip diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index a98662c388..c64afc0c91 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -36,7 +36,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Install dependencies run: | diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index c1d290e600..2733b8605e 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" cache: 'pip' # caching pip dependencies - uses: pre-commit/action@v3.0.1 env: @@ -25,15 +25,8 @@ jobs: fail-fast: false max-parallel: 2 matrix: - python_version: ["3.10", "3.11"] + python_version: ["3.11"] pytorch_version: ["2.4.1", "2.5.1", "2.6.0"] - exclude: - - python_version: "3.10" - pytorch_version: "2.4.1" - - python_version: "3.10" - pytorch_version: "2.5.1" - - python_version: "3.10" - pytorch_version: "2.6.0" timeout-minutes: 20 steps: @@ -127,7 +120,7 @@ jobs: - name: Install Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Install Modal run: | python -m pip install --upgrade pip diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 320a4534b4..21218d72fc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" cache: 'pip' # caching pip dependencies - uses: pre-commit/action@v3.0.1 env: @@ -48,15 +48,8 @@ jobs: fail-fast: false max-parallel: 2 matrix: - python_version: ["3.10", "3.11"] + python_version: ["3.11"] pytorch_version: ["2.4.1", "2.5.1", "2.6.0"] - exclude: - - python_version: "3.10" - pytorch_version: "2.4.1" - - python_version: "3.10" - pytorch_version: "2.5.1" - - python_version: "3.10" - pytorch_version: "2.6.0" timeout-minutes: 20 steps: diff --git a/README.md b/README.md index 6f1fe9846d..398e31b6f0 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Features: **Requirements**: - NVIDIA GPU (Ampere or newer for `bf16` and Flash Attention) or AMD GPU -- Python ≥3.10 +- Python 3.11 - PyTorch ≥2.4.1 ### Installation From 526e5ee8b8d2b8edc65a1bd314965bffa101c132 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 8 Feb 2025 18:01:48 +0700 Subject: [PATCH 0410/1405] fix(config): missing config not being documented and fix model_ override (#2317) * fix(config): missing config not being documented and fix model_ space override * fix: delete redundant field --- docs/config.qmd | 4 ++++ src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 5 +++-- src/axolotl/utils/models.py | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index ecb571040f..91744856f5 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -46,6 +46,10 @@ overrides_of_model_config: type: # linear | dynamic factor: # float +# optional overrides the base model loading from_pretrained +overrides_of_model_kwargs: + # use_cache: False + # optional overrides to the bnb 4bit quantization configuration # https://huggingface.co/docs/transformers/main/main_classes/quantization#transformers.BitsAndBytesConfig bnb_config_kwargs: diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 028b7ea18c..aa79c0f619 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -115,6 +115,9 @@ class RemappedParameters(BaseModel): overrides_of_model_config: Optional[Dict[str, Any]] = Field( default=None, alias="model_config" ) + overrides_of_model_kwargs: Optional[Dict[str, Any]] = Field( + default=None, alias="model_kwargs" + ) type_of_model: Optional[str] = Field(default=None, alias="model_type") revision_of_model: Optional[str] = Field(default=None, alias="model_revision") @@ -426,8 +429,6 @@ class ModelInputConfig(BaseModel): ) trust_remote_code: Optional[bool] = None - model_kwargs: Optional[Dict[str, Any]] = None - @field_validator("trust_remote_code") @classmethod def hint_trust_remote_code(cls, trust_remote_code): diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index d46564f42c..be5b2782a7 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -357,8 +357,8 @@ def __init__( # init model kwargs self.model_kwargs: Dict[str, Any] = {} - if cfg.model_kwargs: - for key, val in cfg.model_kwargs.items(): + if cfg.overrides_of_model_kwargs: + for key, val in cfg.overrides_of_model_kwargs.items(): self.model_kwargs[key] = val # init model From 826f1b1494e8a7b44c516244911f7e1991acef45 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 8 Feb 2025 18:02:02 +0700 Subject: [PATCH 0411/1405] feat(doc): Add multi-node torchrun info (#2304) --- docs/multi-node.qmd | 52 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/docs/multi-node.qmd b/docs/multi-node.qmd index 5c6fa976b9..aa6704ab9e 100644 --- a/docs/multi-node.qmd +++ b/docs/multi-node.qmd @@ -3,6 +3,18 @@ title: Multi Node description: How to use Axolotl on multiple machines --- +The below are three ways to train multi-node in Axolotl. + +::: {.callout-important} +Each machine needs a copy of Axolotl, we suggest using the same commit to ensure compatibility. + +You will also need to have the same configuration file for your model on each machine. + +Make sure the main machine is reachable by other machines. +::: + +# Accelerate + You will need to create a configuration for accelerate, either by using `accelerate config` and follow the instructions or you can use one of the preset below: ~/.cache/huggingface/accelerate/default_config.yaml @@ -26,7 +38,7 @@ tpu_use_sudo: false use_cpu: false ``` -Configure your model to use FSDP with for example: +Configure your model to use FSDP in the Axolotl yaml. For example: ```yaml fsdp: - full_shard @@ -37,12 +49,40 @@ fsdp_config: fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer ``` -## Machine configuration +All you have to do now is launch using accelerate as you would usually do on each machine and voila, the processes will start once you have launched accelerate on every machine. -On each machine you need a copy of Axolotl, we suggest using the same commit to ensure compatibility. +# Raytrain -You will also need to have the same configuration file for your model on each machine. +Please see ray train doc [here](ray-integration.qmd). -On the main machine only, make sure the port you set as `main_process_port` is open in TCP and reachable by other machines. +# Torchrun -All you have to do now is launch using accelerate as you would usually do on each machine and voila, the processes will start once you have launched accelerate on every machine. +If you are using Infiniband, we recommend torchrun to utilize the full bandwidth. + +Set the following env (change buffersize/socketname depending on your system): + +```yaml +export NCCL_IB_DISABLE=0 +export NCCL_SOCKET_IFNAME="eth0,en,eth,em,bond" +export NCCL_BUFFSIZE=2097152 +``` + +Run the following on each node: + +```bash +torchrun --nnodes $num_nodes --nproc_per_node $gpu_per_node --rdzv_id $rdzv_id --rdzv_backend c10d --rdzv_endpoint "$head_node_ip:$head_node_port" -m axolotl.cli.train config.yaml +``` + +Please make sure to substitute the placeholder variables. + +- `num_nodes`: Number of nodes (containing GPUs) +- `gpu_per_node`: Number of gpus per node +- `head_node_ip`: IP of the head node (make sure other machines can connect to this) +- `head_node_port`: Port of the head node (make sure other machines can connect to this. Default 29400) +- `rdzv_id`: A unique job ID that is used by the job across nodes. + +::: {.callout-note} +You need to call `axolotl.cli.train` instead of `axolotl train` as the latter calls accelerate under the hood +::: + +More info on the available configs can be found on the Pytorch docs [here](https://pytorch.org/docs/stable/elastic/run.html) From 44f64ab6271630b7e1142f6e80c7db1774876eda Mon Sep 17 00:00:00 2001 From: Sung Ching Liu <22844540+bursteratom@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:18:31 -0500 Subject: [PATCH 0412/1405] Update faq.qmd (#2319) * Update faq.qmd Added Q&A for being stuck on saving preprocessed datasets * Update faq.qmd added details on preprocessing on cpu * Update faq.qmd * Update faq.qmd --- docs/faq.qmd | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/faq.qmd b/docs/faq.qmd index 91413d24e9..a31cc90d09 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -19,3 +19,7 @@ description: Frequently asked questions **Q: AttributeError: 'DummyOptim' object has no attribute 'step'** > A: You may be using deepspeed with single gpu. Please don't set `deepspeed:` in yaml or cli. + +**Q: The codes is stuck on saving preprocessed datasets.** + +> A: This is usually an issue with the GPU. This can be resolved through setting the os environment variable `CUDA_VISIBLE_DEVICES=0`. If you are on runpod, this is usually a pod issue. Starting a new pod should take care of it. From e37a4a536aaec6a6cbaaac333d278e458b346477 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 12 Feb 2025 10:04:26 -0500 Subject: [PATCH 0413/1405] lint docs (#2327) --- docs/faq.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.qmd b/docs/faq.qmd index a31cc90d09..a05624bd15 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -22,4 +22,4 @@ description: Frequently asked questions **Q: The codes is stuck on saving preprocessed datasets.** -> A: This is usually an issue with the GPU. This can be resolved through setting the os environment variable `CUDA_VISIBLE_DEVICES=0`. If you are on runpod, this is usually a pod issue. Starting a new pod should take care of it. +> A: This is usually an issue with the GPU. This can be resolved through setting the os environment variable `CUDA_VISIBLE_DEVICES=0`. If you are on runpod, this is usually a pod issue. Starting a new pod should take care of it. From 30046315d99bd89772f8dfb76bdbe7113cf0350c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 12 Feb 2025 18:29:02 -0500 Subject: [PATCH 0414/1405] disable ray tests for latest torch release (#2328) * disable ray tests for latest torch release * move decorator from class to method --- tests/e2e/multigpu/test_ray.py | 4 +++- tests/e2e/utils.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index d7e4ddfcf7..72ec69aa85 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -9,7 +9,7 @@ import pytest import yaml from accelerate.test_utils import execute_subprocess_async -from e2e.utils import check_tensorboard +from e2e.utils import check_tensorboard, require_torch_lt_2_6_0 from axolotl.utils.dict import DictDefault @@ -24,6 +24,7 @@ class TestMultiGPURay: Test cases for AnyScale Ray post training """ + @require_torch_lt_2_6_0 def test_lora_ddp(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -80,6 +81,7 @@ def test_lora_ddp(self, temp_dir): temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" ) + @require_torch_lt_2_6_0 @pytest.mark.parametrize( "gradient_accumulation_steps", [1, 2], diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 2baead7d2b..a9f7fb28d2 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -66,6 +66,18 @@ def is_min_2_5_1(): return unittest.skipUnless(is_min_2_5_1(), "test requires torch>=2.5.1")(test_case) +def require_torch_lt_2_6_0(test_case): + """ + Decorator marking a test that requires torch >= 2.5.1 + """ + + def is_max_2_6_0(): + torch_version = version.parse(torch.__version__) + return torch_version < version.parse("2.6.0") + + return unittest.skipUnless(is_max_2_6_0(), "test requires torch<2.6.0")(test_case) + + def is_hopper(): compute_capability = torch.cuda.get_device_capability() return compute_capability == (9, 0) From fdbb1a207cd8f68f658c466698ba42c88fe600b6 Mon Sep 17 00:00:00 2001 From: Lee Park <56708372+leeparkuky@users.noreply.github.com> Date: Thu, 13 Feb 2025 08:31:07 -0500 Subject: [PATCH 0415/1405] [Fixing #2149] load_from_disk for RL-type training (#2193) * Update rl.py * Update rl.py * Update rl.py * refactor pref dataset loading to reuse load_dataset_w_config * refactor again after rebase from main * chore: add docstring and types --------- Co-authored-by: Wing Lian Co-authored-by: NanoCode012 --- src/axolotl/utils/data/rl.py | 28 +++++++--------------- src/axolotl/utils/data/sft.py | 23 ++---------------- src/axolotl/utils/data/shared.py | 41 +++++++++++++++++++++++++++++--- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index 9f5c726aba..67075cc9f6 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -4,15 +4,16 @@ import logging from functools import partial from pathlib import Path -from typing import Any, List +from typing import Any, List, Union import yaml -from datasets import DatasetDict, concatenate_datasets, load_dataset, load_from_disk +from datasets import Dataset, DatasetDict, concatenate_datasets, load_from_disk from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH from axolotl.prompt_strategies.dpo import load as load_dpo from axolotl.prompt_strategies.kto import load as load_kto from axolotl.prompt_strategies.orpo import load as load_orpo +from axolotl.utils.data.shared import datasets_w_name_generator, load_dataset_w_config from axolotl.utils.data.utils import deduplicate_and_log_datasets, md5 from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import is_main_process, zero_first @@ -118,23 +119,12 @@ def drop_long_rl_seq( def load_prepare_preference_datasets(cfg): def load_split(dataset_cfgs, _cfg): split_datasets: List[Any] = [] - for i, ds_cfg in enumerate(dataset_cfgs): - if ds_cfg["ds_type"] == "json": - for data_file in ds_cfg["data_files"]: - data_files = {ds_cfg["split"]: data_file} - ds = load_dataset( # pylint: disable=invalid-name - "json", - data_files=data_files, - split=ds_cfg["split"], - ) - split_datasets.insert(i, ds) - else: - ds = load_dataset( # pylint: disable=invalid-name - ds_cfg["path"], - split=ds_cfg["split"], - revision=ds_cfg.get("revision", None), - ) - split_datasets.insert(i, ds) + use_auth_token = _cfg.hf_use_auth_token + for config_dataset in datasets_w_name_generator(dataset_cfgs): + ds: Union[Dataset, DatasetDict] = load_dataset_w_config( + config_dataset, use_auth_token, streaming=False + ) + split_datasets.append(ds) tokenizer = load_tokenizer(cfg) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 722ad2de2d..52ca91365c 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -43,7 +43,7 @@ UnsupportedPrompter, ) from axolotl.utils.data.pretraining import wrap_pretraining_dataset -from axolotl.utils.data.shared import load_dataset_w_config +from axolotl.utils.data.shared import datasets_w_name_generator, load_dataset_w_config from axolotl.utils.data.utils import ( deduplicate_and_log_datasets, drop_long_seq_in_dataset, @@ -263,30 +263,11 @@ def load_tokenized_prepared_datasets( datasets = [] - def for_d_in_datasets(dataset_configs): - for dataset in dataset_configs: - if dataset.name and isinstance(dataset.name, list): - # load_dataset doesn't properly handle multiple named configurations - # at the same time for a given dataset - for name in dataset.name: - yield DictDefault({**dataset, "name": name}) - elif dataset.preprocess_shards and not dataset.shards: - for shard in range(dataset.preprocess_shards): - yield DictDefault( - { - **dataset, - "shards": dataset.preprocess_shards, - "shards_idx": shard, - } - ) - else: - yield dataset - streaming_ds = False if preprocess_iterable: streaming_ds = True # pylint: disable=invalid-name - for config_dataset in for_d_in_datasets(cfg_datasets): + for config_dataset in datasets_w_name_generator(cfg_datasets): ds: Union[Dataset, DatasetDict] = load_dataset_w_config( config_dataset, use_auth_token, streaming=streaming_ds ) diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 013d7a895b..405057efcd 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -1,6 +1,7 @@ """ dataset loading shared utils """ + from pathlib import Path from typing import Optional, Union @@ -29,9 +30,43 @@ def get_ds_type(config_dataset: DictDefault): return ds_type +def datasets_w_name_generator(dataset_configs: list[DictDefault]): + """ + Yields dataset configs handling multiple names or preprocess_shards + + Args: + dataset_configs: list of dataset configs (equivalent to cfg.datasets) + """ + for dataset in dataset_configs: + if dataset.name and isinstance(dataset.name, list): + # load_dataset doesn't properly handle multiple named configurations + # at the same time for a given dataset + for name in dataset.name: + yield DictDefault({**dataset, "name": name}) + elif dataset.preprocess_shards and not dataset.shards: + for shard in range(dataset.preprocess_shards): + yield DictDefault( + { + **dataset, + "shards": dataset.preprocess_shards, + "shards_idx": shard, + } + ) + else: + yield dataset + + def load_dataset_w_config( - config_dataset, auth_token, streaming=False + config_dataset: DictDefault, use_auth_token: bool, streaming=False ) -> Union[Dataset, DatasetDict]: + """ + Load a dataset from a config + + Args: + config_dataset: single dataset config + use_auth_token: whether to use HF auth token + streaming: whether to stream the dataset + """ # pylint: disable=invalid-name ds: Optional[Union[Dataset, DatasetDict]] = None # pylint: disable=invalid-name ds_from_hub = False @@ -43,7 +78,7 @@ def load_dataset_w_config( config_dataset.path, name=config_dataset.name, streaming=True, - token=auth_token, + token=use_auth_token, revision=config_dataset.revision, trust_remote_code=ds_trust_remote_code, ) @@ -161,7 +196,7 @@ def load_dataset_w_config( name=config_dataset.name, streaming=streaming, data_files=config_dataset.data_files, - token=auth_token, + token=use_auth_token, revision=config_dataset.revision, trust_remote_code=config_dataset.trust_remote_code, **load_ds_kwargs, From ffae8d6a9553dbf4b2c89cff1eb2079c23aa11a8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 13 Feb 2025 16:01:01 -0500 Subject: [PATCH 0416/1405] GRPO (#2307) --- .github/workflows/main.yml | 2 +- .github/workflows/multi-gpu-e2e.yml | 5 +- .github/workflows/tests.yml | 2 +- requirements.txt | 4 +- setup.py | 7 +- src/axolotl/cli/cloud/__init__.py | 7 +- src/axolotl/cli/cloud/modal_.py | 38 +++- src/axolotl/cli/main.py | 18 +- src/axolotl/common/datasets.py | 4 +- src/axolotl/core/trainer_builder.py | 57 ++++-- src/axolotl/core/trainers/base.py | 114 +----------- src/axolotl/core/trainers/dpo/__init__.py | 33 ++++ src/axolotl/core/trainers/dpo/args.py | 15 ++ src/axolotl/core/trainers/dpo/trainer.py | 125 +++++++++++++ src/axolotl/core/trainers/grpo/__init__.py | 119 ++++++++++++ src/axolotl/core/trainers/grpo/args.py | 15 ++ src/axolotl/core/trainers/grpo/trainer.py | 107 +++++++++++ src/axolotl/core/training_args.py | 9 +- src/axolotl/prompt_strategies/base.py | 15 +- .../prompt_strategies/dpo/passthrough.py | 14 ++ .../config/models/input/v0_4_1/__init__.py | 9 + .../utils/config/models/input/v0_4_1/trl.py | 35 ++++ src/axolotl/utils/data/rl.py | 51 ++++-- src/axolotl/utils/lora.py | 75 ++++++++ src/axolotl/utils/models.py | 10 +- src/axolotl/utils/trainer.py | 2 +- tests/e2e/multigpu/test_grpo.py | 173 ++++++++++++++++++ tests/e2e/utils.py | 18 ++ 28 files changed, 900 insertions(+), 183 deletions(-) create mode 100644 src/axolotl/core/trainers/dpo/__init__.py create mode 100644 src/axolotl/core/trainers/dpo/args.py create mode 100644 src/axolotl/core/trainers/dpo/trainer.py create mode 100644 src/axolotl/core/trainers/grpo/__init__.py create mode 100644 src/axolotl/core/trainers/grpo/args.py create mode 100644 src/axolotl/core/trainers/grpo/trainer.py create mode 100644 src/axolotl/prompt_strategies/dpo/passthrough.py create mode 100644 src/axolotl/utils/config/models/input/v0_4_1/trl.py create mode 100644 src/axolotl/utils/lora.py create mode 100644 tests/e2e/multigpu/test_grpo.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6c7e248bfe..7fb97b9d93 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.5.1 - axolotl_extras: + axolotl_extras: vllm is_latest: true - cuda: 124 cuda_version: 12.4.1 diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 6c9c7bb499..ea00d749b7 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -24,20 +24,21 @@ jobs: cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.4.1 - axolotl_extras: + axolotl_extras: # no vllm support for 2.4.1 num_gpus: 2 nightly_build: "true" - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.5.1 - axolotl_extras: + axolotl_extras: vllm num_gpus: 2 nightly_build: "true" - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.6.0 + # awaiting vllm#12721 axolotl_extras: num_gpus: 2 nightly_build: "true" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 21218d72fc..8893390054 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -204,7 +204,7 @@ jobs: python_version: "3.11" pytorch: 2.5.1 num_gpus: 1 - axolotl_extras: + axolotl_extras: vllm steps: - name: Checkout uses: actions/checkout@v4 diff --git a/requirements.txt b/requirements.txt index 37a3f0d646..44f49289a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ tokenizers>=0.21.0 accelerate==1.3.0 datasets==3.2.0 deepspeed==0.16.1 -trl==0.13.0 +trl==0.15.0 optimum==1.16.2 hf_transfer @@ -26,7 +26,7 @@ sentencepiece gradio==3.50.2 modal==0.70.5 -pydantic==2.6.3 +pydantic==2.10.6 addict fire PyYAML>=6.0 diff --git a/setup.py b/setup.py index 6730591c05..b84c34525c 100644 --- a/setup.py +++ b/setup.py @@ -79,7 +79,7 @@ def parse_requirements(): if patch == 0: _install_requires.append("xformers==0.0.28.post2") else: - _install_requires.append("xformers==0.0.29") + _install_requires.append("xformers>=0.0.28.post3") _install_requires.pop(_install_requires.index(autoawq_version)) elif (major, minor) >= (2, 4): if patch == 0: @@ -125,7 +125,7 @@ def get_package_version(): }, extras_require={ "flash-attn": [ - "flash-attn==2.7.0.post2", + "flash-attn==2.7.4.post1", ], "deepspeed": [ "deepspeed==0.16.1", @@ -156,5 +156,8 @@ def get_package_version(): "ray": [ "ray[train]", ], + "vllm": [ + "vllm==0.7.2", + ], }, ) diff --git a/src/axolotl/cli/cloud/__init__.py b/src/axolotl/cli/cloud/__init__.py index fde46e3971..b879601bed 100644 --- a/src/axolotl/cli/cloud/__init__.py +++ b/src/axolotl/cli/cloud/__init__.py @@ -35,13 +35,18 @@ def do_cli_train( cloud_config: Union[Path, str], config: Union[Path, str], accelerate: bool = True, + cwd=None, + **kwargs, ) -> None: print_axolotl_text_art() cloud_cfg = load_cloud_cfg(cloud_config) cloud = ModalCloud(cloud_cfg) with open(config, "r", encoding="utf-8") as file: config_yaml = file.read() - cloud.train(config_yaml, accelerate=accelerate) + local_dirs = {} + if cwd and not Path(cwd).joinpath("src", "axolotl").exists(): + local_dirs = {"/workspace/mounts": cwd} + cloud.train(config_yaml, accelerate=accelerate, local_dirs=local_dirs, **kwargs) def do_cli_lm_eval( diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index bcc47ead9e..6b724f7328 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -7,6 +7,7 @@ import subprocess # nosec B404 from pathlib import Path from random import randint +from typing import Optional import modal @@ -22,8 +23,18 @@ def run_cmd(cmd: str, run_folder: str, volumes=None): # modal workaround so it doesn't use the automounted axolotl new_env = copy.deepcopy(os.environ) + if "PYTHONPATH" in new_env: - del new_env["PYTHONPATH"] + paths = ["/workspace/mounts"] + for sub_python_path_str in new_env["PYTHONPATH"].split(":"): + sub_python_path = Path(sub_python_path_str) + if not sub_python_path.joinpath("src", "axolotl").exists(): + # we don't want to use the automounted axolotl or unexpected behavior happens + paths.append(str(sub_python_path)) + if paths: + new_env["PYTHONPATH"] = ":".join(paths) + else: + del new_env["PYTHONPATH"] # Propagate errors from subprocess. if exit_code := subprocess.call( # nosec B603 @@ -203,9 +214,12 @@ def get_train_memory(self): memory = int(self.config.memory) return 1024 * memory - def get_train_env(self): + def get_train_env(self, local_dirs=None): + image = self.get_image() + for mount, local_dir in (local_dirs or {}).items(): + image = image.add_local_dir(local_dir, mount) return self.app.function( - image=self.get_image(), + image=image, volumes={k: v[0] for k, v in self.volumes.items()}, cpu=16.0, gpu=self.get_train_gpu(), @@ -214,14 +228,21 @@ def get_train_env(self): secrets=self.get_secrets(), ) - def train(self, config_yaml: str, accelerate: bool = True): - modal_fn = self.get_train_env()(_train) + def train( + self, + config_yaml: str, + accelerate: bool = True, + local_dirs: Optional[dict[str, str]] = None, + **kwargs, + ): + modal_fn = self.get_train_env(local_dirs)(_train) with modal.enable_output(): with self.app.run(detach=True): modal_fn.remote( config_yaml, accelerate=accelerate, volumes={k: v[0] for k, v in self.volumes.items()}, + **kwargs, ) def lm_eval(self, config_yaml: str): @@ -252,7 +273,7 @@ def _preprocess(config_yaml: str, volumes=None): ) -def _train(config_yaml: str, accelerate: bool = True, volumes=None): +def _train(config_yaml: str, accelerate: bool = True, volumes=None, **kwargs): with open( "/workspace/artifacts/axolotl/config.yaml", "w", encoding="utf-8" ) as f_out: @@ -262,8 +283,11 @@ def _train(config_yaml: str, accelerate: bool = True, volumes=None): accelerate_args = "--accelerate" else: accelerate_args = "--no-accelerate" + num_processes_args = "" + if num_processes := kwargs.pop("num_processes", None): + num_processes_args = f"--num-processes {num_processes}" run_cmd( - f"axolotl train {accelerate_args} /workspace/artifacts/axolotl/config.yaml", + f"axolotl train {accelerate_args} {num_processes_args} /workspace/artifacts/axolotl/config.yaml", run_folder, volumes, ) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index d7aa1f6a72..61dc4403ad 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -2,6 +2,7 @@ # pylint: disable=redefined-outer-name import logging +import os import random import subprocess # nosec B404 import tempfile @@ -12,6 +13,7 @@ import click import yaml +from dotenv import load_dotenv import axolotl from axolotl.cli.args import EvaluateCliArgs, PreprocessCliArgs, TrainerCliArgs @@ -199,7 +201,14 @@ def iter_configs(): try: if accelerate: if cloud: - do_cli_train(cloud_config=cloud, config=config, accelerate=True) + cwd = os.getcwd() + do_cli_train( + cloud_config=cloud, + config=config, + accelerate=True, + cwd=cwd, + **kwargs, + ) else: accelerate_args = [] if "main_process_port" in kwargs: @@ -208,7 +217,7 @@ def iter_configs(): accelerate_args.append(str(main_process_port)) if "num_processes" in kwargs: num_processes = kwargs.pop("num_processes", None) - accelerate_args.append("--num-processes") + accelerate_args.append("--num_processes") accelerate_args.append(str(num_processes)) base_cmd = ["accelerate", "launch"] @@ -220,7 +229,9 @@ def iter_configs(): subprocess.run(cmd, check=True) # nosec B603 else: if cloud: - do_cli_train(cloud_config=cloud, config=config, accelerate=False) + do_cli_train( + cloud_config=cloud, config=config, accelerate=False, **kwargs + ) else: from axolotl.cli.train import do_cli @@ -381,4 +392,5 @@ def main(): if __name__ == "__main__": + load_dotenv() main() diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index cbc0d127c6..db07eb43b0 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -122,9 +122,11 @@ def load_preference_datasets( `total_num_steps`. """ train_dataset, eval_dataset = load_prepare_preference_datasets(cfg) - total_num_steps = int( + total_num_steps: Optional[int] = int( math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) ) + if cfg.rl == "grpo": + total_num_steps = None if cli_args.debug or cfg.debug: LOG.info("check_dataset_labels...") diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 89480d775d..ae757cf43b 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -39,7 +39,6 @@ from axolotl.core.trainers.base import ( AxolotlCPOTrainer, - AxolotlDPOTrainer, AxolotlKTOTrainer, AxolotlMambaTrainer, AxolotlORPOTrainer, @@ -48,9 +47,11 @@ AxolotlTrainer, ReLoRATrainer, ) +from axolotl.core.trainers.dpo import DPOStrategy +from axolotl.core.trainers.dpo.args import AxolotlDPOConfig +from axolotl.core.trainers.grpo import GRPOStrategy from axolotl.core.training_args import ( AxolotlCPOConfig, - AxolotlDPOConfig, AxolotlKTOConfig, AxolotlORPOConfig, AxolotlPRMConfig, @@ -641,9 +642,6 @@ def build(self, total_num_steps): tokenizer=self.tokenizer, ) - if self.cfg.rl == "orpo": - training_arguments_kwargs["orpo_alpha"] = self.cfg.orpo_alpha - if self.cfg.neftune_noise_alpha is not None: training_arguments_kwargs[ "neftune_noise_alpha" @@ -652,7 +650,7 @@ def build(self, total_num_steps): trainer_kwargs = {} if self.cfg.reward_model: - trainer_kwargs["max_length"] = self.cfg.sequence_len + training_arguments_kwargs["max_length"] = self.cfg.sequence_len # pylint: disable=duplicate-code if self.cfg.optimizer in [ @@ -965,10 +963,11 @@ def build_training_arguments(self, total_num_steps): # default to saving each epoch if not defined training_args_kwargs["save_strategy"] = "epoch" - training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes + if self.cfg.dataset_processes: + training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes - if self.cfg.rl_beta: - training_args_kwargs["beta"] = self.cfg.rl_beta + if (self.cfg.trl and self.cfg.trl.beta) or self.cfg.rl_beta: + training_args_kwargs["beta"] = self.cfg.trl.beta or self.cfg.rl_beta if self.cfg.orpo_alpha: # trl does some odd mapping of alpha to beta to reuse the beta parameter ??? training_args_kwargs["beta"] = self.cfg.orpo_alpha @@ -977,6 +976,7 @@ def build_training_arguments(self, total_num_steps): training_args_kwargs["rpo_alpha"] = self.cfg.rpo_alpha training_args_cls = None + blocklist_args_kwargs = [] if self.cfg.rl == "simpo": training_args_cls = AxolotlCPOConfig training_args_kwargs["loss_type"] = "simpo" @@ -1001,11 +1001,15 @@ def build_training_arguments(self, total_num_steps): self.cfg.kto_undesirable_weight or 1.0 ) - training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes training_args_kwargs["max_length"] = self.cfg.sequence_len if self.cfg.max_prompt_len: training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len + elif self.cfg.rl == "grpo": + training_args_cls = GRPOStrategy.get_training_args_class() + training_args_kwargs.update(GRPOStrategy.set_training_args_kwargs(self.cfg)) + blocklist_args_kwargs = GRPOStrategy.get_blocklist_args_kwargs() + else: training_args_cls = AxolotlDPOConfig if self.cfg.rl == "ipo": @@ -1016,11 +1020,21 @@ def build_training_arguments(self, total_num_steps): training_args_kwargs["generate_during_eval"] = self.cfg.use_wandb if self.cfg.dpo_use_weighting is not None: training_args_kwargs["use_weighting"] = self.cfg.dpo_use_weighting + if self.cfg.dpo_use_logits_to_keep is not None: + training_args_kwargs[ + "use_logits_to_keep" + ] = self.cfg.dpo_use_logits_to_keep + + for blocklist_key in blocklist_args_kwargs: + if blocklist_key in training_args_kwargs: + del training_args_kwargs[blocklist_key] + max_steps = self.cfg.max_steps or total_num_steps or -1 + training_args_kwargs["num_train_epochs"] = self.cfg.num_epochs training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg - output_dir=self.cfg.output_dir, + self.cfg.output_dir, per_device_train_batch_size=self.cfg.micro_batch_size, - max_steps=self.cfg.max_steps or total_num_steps, + max_steps=max_steps, gradient_accumulation_steps=self.cfg.gradient_accumulation_steps, learning_rate=self.cfg.learning_rate, warmup_steps=self.cfg.warmup_steps, @@ -1047,8 +1061,13 @@ def build(self, total_num_steps): dpo_trainer_kwargs[ "precompute_ref_log_probs" ] = self.cfg.precompute_ref_log_probs - if self.cfg.rl in ["dpo", "ipo"]: - trainer_cls = AxolotlDPOTrainer + if self.cfg.rl == "grpo": + trainer_cls = GRPOStrategy.get_trainer_class() + trainer_cls_args = [self.model] + trainer_cls_args.extend(GRPOStrategy.set_trainer_args(self.cfg)) + dpo_trainer_kwargs.update(GRPOStrategy.set_trainer_kwargs(self.cfg)) + elif self.cfg.rl in ["dpo", "ipo"]: + trainer_cls = DPOStrategy.get_trainer_class() trainer_cls_args = [self.model, self.model_ref] elif self.cfg.rl == "orpo": trainer_cls = AxolotlORPOTrainer @@ -1063,12 +1082,14 @@ def build(self, total_num_steps): raise ValueError(f"Unsupported RL: {self.cfg.rl}") sig = inspect.signature(trainer_cls) - if "processing_class" in sig.parameters.keys(): - dpo_trainer_kwargs["processing_class"] = self.tokenizer - else: + if "tokenizer" in sig.parameters.keys(): dpo_trainer_kwargs["tokenizer"] = self.tokenizer + else: + dpo_trainer_kwargs["processing_class"] = self.tokenizer - if self.cfg.datasets is not None and (trainer_cls is AxolotlDPOTrainer): + if self.cfg.datasets is not None and ( + trainer_cls is DPOStrategy.get_trainer_class() + ): dpo_trainer_kwargs["dataset_tags"] = [ d["path"] for d in self.cfg.datasets if not Path(d["path"]).is_dir() ] diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 44a6d54d73..ee2545b213 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -5,30 +5,21 @@ from __future__ import annotations # pylint: disable=too-many-lines -import gc import logging import os from collections import defaultdict from functools import wraps -from typing import Any, Dict, Literal, Optional, Union +from typing import Dict, Literal, Optional import torch from datasets import Dataset from peft.optimizers import create_loraplus_optimizer -from torch import nn from torch.optim.lr_scheduler import OneCycleLR from torch.utils.data import BatchSampler, DataLoader, RandomSampler, SequentialSampler from transformers import Trainer from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, seed_worker from transformers.utils import is_sagemaker_mp_enabled -from trl import ( - CPOTrainer, - DPOTrainer, - KTOTrainer, - ORPOTrainer, - PRMTrainer, - RewardTrainer, -) +from trl import CPOTrainer, KTOTrainer, ORPOTrainer, PRMTrainer, RewardTrainer from trl.trainer.utils import pad_to_length from axolotl.monkeypatch.relora import ReLoRAScheduler @@ -847,107 +838,6 @@ def create_scheduler( return self.lr_scheduler -class AxolotlDPOTrainer(SchedulerMixin, DPOTrainer): - """ - Extend the base DPOTrainer for axolotl helpers - """ - - tag_names = ["axolotl", "dpo"] - - def __init__(self, *args, dataset_tags=None, **kwargs): - super().__init__(*args, **kwargs) - self.dataset_tags = dataset_tags - self.optimizer = None - self.model_accepts_loss_kwargs = False - - def create_optimizer(self): - if self.args.loraplus_lr_ratio is None: - return super().create_optimizer() - - opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model - if self.optimizer is None: # pylint: disable=access-member-before-definition - optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs( - self.args, - opt_model, - ) - - loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) - if loraplus_lr_ratio: - print("Using lora+") - loraplus_lr_embedding = getattr(self.args, "loraplus_lr_embedding", None) - self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init - opt_model, - optimizer_cls, - loraplus_lr_ratio=loraplus_lr_ratio, - loraplus_lr_embedding=loraplus_lr_embedding, - **optimizer_kwargs, - ) - - if is_sagemaker_mp_enabled(): - self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init - self.optimizer - ) - - return self.optimizer - - @wraps(DPOTrainer.push_to_hub) - def push_to_hub(self, *args, **kwargs) -> str: - """ - Overwrite the `push_to_hub` method in order to force-add the tags when pushing the - model on the Hub. Please refer to `~transformers.Trainer.push_to_hub` for more details. - """ - kwargs = _sanitize_kwargs_for_ds_tagging( - dataset_tags=self.dataset_tags, kwargs=kwargs - ) - kwargs = _sanitize_kwargs_for_tagging(tag_names=self.tag_names, kwargs=kwargs) - - return super().push_to_hub(*args, **kwargs) - - @staticmethod - def tokenize_row( - features, - processing_class, - max_prompt_length, - max_completion_length, - add_special_tokens, - ) -> Dict: - res = DPOTrainer.tokenize_row( - features, - processing_class, - max_prompt_length, - max_completion_length, - add_special_tokens, - ) - # fix when the tokenizer doesn't have a bos_token_id, e.g. Qwen - if processing_class.bos_token is None and res["prompt_input_ids"][0] is None: - for key in res.keys(): - res[key] = res[key][1:] - - if processing_class.bos_token and processing_class.bos_token_id is not None: - # dpo trainer may incorrectly prepend the bos_token_id to the dpo outputs - if res["chosen_input_ids"][0] == processing_class.bos_token_id: - res["chosen_input_ids"] = res["chosen_input_ids"][1:] - res["chosen_labels"] = res["chosen_labels"][1:] - res["chosen_attention_mask"] = res["chosen_attention_mask"][1:] - if res["rejected_input_ids"][0] == processing_class.bos_token_id: - res["rejected_input_ids"] = res["rejected_input_ids"][1:] - res["rejected_labels"] = res["rejected_labels"][1:] - res["rejected_attention_mask"] = res["rejected_attention_mask"][1:] - - return res - - def training_step( - self, - model: nn.Module, - inputs: Dict[str, Union[torch.Tensor, Any]], - num_items_in_batch=None, - ) -> torch.Tensor: - loss: torch.Tensor = super().training_step(model, inputs, num_items_in_batch) - gc.collect() - torch.cuda.empty_cache() - return loss - - class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): """ Extend the base ORPOTrainer for axolotl helpers diff --git a/src/axolotl/core/trainers/dpo/__init__.py b/src/axolotl/core/trainers/dpo/__init__.py new file mode 100644 index 0000000000..8187a7fb5c --- /dev/null +++ b/src/axolotl/core/trainers/dpo/__init__.py @@ -0,0 +1,33 @@ +""" +DPO Specific Strategy for training +""" +from axolotl.core.trainers.dpo.trainer import AxolotlDPOTrainer + + +class DPOStrategy: + """ + Strategy for DPO training + """ + + @classmethod + def get_trainer_class(cls): + return AxolotlDPOTrainer + + @classmethod + def get_training_args_class(cls): + from axolotl.core.trainers.dpo.args import AxolotlDPOConfig + + return AxolotlDPOConfig + + @classmethod + def set_training_args_kwargs(cls, cfg): + training_args_kwargs = {} + if cfg.rl == "ipo": + training_args_kwargs["loss_type"] = "ipo" + training_args_kwargs["max_length"] = cfg.sequence_len + training_args_kwargs["max_completion_length"] = None + training_args_kwargs["max_prompt_length"] = cfg.sequence_len + training_args_kwargs["generate_during_eval"] = cfg.use_wandb + if cfg.dpo_use_weighting is not None: + training_args_kwargs["use_weighting"] = cfg.dpo_use_weighting + return training_args_kwargs diff --git a/src/axolotl/core/trainers/dpo/args.py b/src/axolotl/core/trainers/dpo/args.py new file mode 100644 index 0000000000..4cae67d3ed --- /dev/null +++ b/src/axolotl/core/trainers/dpo/args.py @@ -0,0 +1,15 @@ +""" +Axolotl specific DPO args +""" +from dataclasses import dataclass + +from trl import DPOConfig + +from axolotl.core.training_args import AxolotlTrainingMixins + + +@dataclass +class AxolotlDPOConfig(AxolotlTrainingMixins, DPOConfig): + """ + DPO config for DPO training + """ diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py new file mode 100644 index 0000000000..a1de4cc823 --- /dev/null +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -0,0 +1,125 @@ +""" +DPO trainer for axolotl +""" +import gc +from functools import wraps +from typing import Any, Dict, Union + +import torch +from peft.optimizers import create_loraplus_optimizer +from torch import nn +from transformers import Trainer +from transformers.utils import is_sagemaker_mp_enabled +from trl import DPOTrainer + +from axolotl.core.trainers.base import ( + SchedulerMixin, + _sanitize_kwargs_for_ds_tagging, + _sanitize_kwargs_for_tagging, +) + +if is_sagemaker_mp_enabled(): + import smdistributed.modelparallel.torch as smp + + +class AxolotlDPOTrainer(SchedulerMixin, DPOTrainer): + """ + Extend the base DPOTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "dpo"] + + def __init__(self, *args, dataset_tags=None, **kwargs): + super().__init__(*args, **kwargs) + self.dataset_tags = dataset_tags + self.optimizer = None + self.model_accepts_loss_kwargs = False + + def create_optimizer(self): + # pylint: disable=duplicate-code + if self.args.loraplus_lr_ratio is None: + return super().create_optimizer() + + opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model + if self.optimizer is None: # pylint: disable=access-member-before-definition + optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs( + self.args, + opt_model, + ) + + loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) + if loraplus_lr_ratio: + print("Using lora+") + loraplus_lr_embedding = getattr(self.args, "loraplus_lr_embedding", None) + # pylint: disable=duplicate-code + self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init + opt_model, + optimizer_cls, + loraplus_lr_ratio=loraplus_lr_ratio, + loraplus_lr_embedding=loraplus_lr_embedding, + **optimizer_kwargs, + ) + + if is_sagemaker_mp_enabled(): + self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init + self.optimizer + ) + + return self.optimizer + + @wraps(DPOTrainer.push_to_hub) + def push_to_hub(self, *args, **kwargs) -> str: + """ + Overwrite the `push_to_hub` method in order to force-add the tags when pushing the + model on the Hub. Please refer to `~transformers.Trainer.push_to_hub` for more details. + """ + kwargs = _sanitize_kwargs_for_ds_tagging( + dataset_tags=self.dataset_tags, kwargs=kwargs + ) + kwargs = _sanitize_kwargs_for_tagging(tag_names=self.tag_names, kwargs=kwargs) + + return super().push_to_hub(*args, **kwargs) + + @staticmethod + def tokenize_row( + features, + processing_class, + max_prompt_length, + max_completion_length, + add_special_tokens, + ) -> Dict: + res = DPOTrainer.tokenize_row( + features, + processing_class, + max_prompt_length, + max_completion_length, + add_special_tokens, + ) + # fix when the tokenizer doesn't have a bos_token_id, e.g. Qwen + if processing_class.bos_token is None and res["prompt_input_ids"][0] is None: + for key in res.keys(): + res[key] = res[key][1:] + + if processing_class.bos_token and processing_class.bos_token_id is not None: + # dpo trainer may incorrectly prepend the bos_token_id to the dpo outputs + if res["chosen_input_ids"][0] == processing_class.bos_token_id: + res["chosen_input_ids"] = res["chosen_input_ids"][1:] + res["chosen_labels"] = res["chosen_labels"][1:] + res["chosen_attention_mask"] = res["chosen_attention_mask"][1:] + if res["rejected_input_ids"][0] == processing_class.bos_token_id: + res["rejected_input_ids"] = res["rejected_input_ids"][1:] + res["rejected_labels"] = res["rejected_labels"][1:] + res["rejected_attention_mask"] = res["rejected_attention_mask"][1:] + + return res + + def training_step( + self, + model: nn.Module, + inputs: Dict[str, Union[torch.Tensor, Any]], + num_items_in_batch=None, + ) -> torch.Tensor: + loss: torch.Tensor = super().training_step(model, inputs, num_items_in_batch) + gc.collect() + torch.cuda.empty_cache() + return loss diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py new file mode 100644 index 0000000000..5202cb09dc --- /dev/null +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -0,0 +1,119 @@ +""" +GRPO Specific Strategy for training +""" + +import importlib +import inspect +import logging + +from trl.trainer.grpo_trainer import RewardFunc + +from axolotl.core.trainers.grpo.trainer import AxolotlGRPOTrainer + +LOG = logging.getLogger("axolotl") + + +class GRPOStrategy: + """ + Strategy for GRPO training + """ + + @classmethod + def get_trainer_class(cls): + return AxolotlGRPOTrainer + + @classmethod + def get_training_args_class(cls): + from axolotl.core.trainers.grpo.args import AxolotlGRPOConfig + + return AxolotlGRPOConfig + + @classmethod + def set_training_args_kwargs(cls, cfg): + grpo_args_kwargs = {} + if cfg.trl and cfg.trl.use_vllm: + grpo_args_kwargs["use_vllm"] = cfg.trl.use_vllm + if cfg.trl and cfg.trl.vllm_device: + grpo_args_kwargs["vllm_device"] = cfg.trl.vllm_device + else: + grpo_args_kwargs["vllm_device"] = "auto" + if cfg.trl and cfg.trl.vllm_gpu_memory_utilization: + grpo_args_kwargs[ + "vllm_gpu_memory_utilization" + ] = cfg.trl.vllm_gpu_memory_utilization + if cfg.trl and cfg.trl.vllm_max_model_len: + grpo_args_kwargs["vllm_max_model_len"] = cfg.trl.vllm_max_model_len + if cfg.trl and cfg.trl.num_generations: + grpo_args_kwargs["num_generations"] = cfg.trl.num_generations + if cfg.trl and cfg.trl.sync_ref_model: + grpo_args_kwargs["sync_ref_model"] = cfg.trl.sync_ref_model + if cfg.trl and cfg.trl.ref_model_mixup_alpha: + grpo_args_kwargs[ + "ref_model_mixup_alpha" + ] = cfg.trl.ref_model_mixup_alpha + if cfg.trl and cfg.trl.ref_model_sync_steps: + grpo_args_kwargs["ref_model_sync_steps"] = cfg.trl.ref_model_sync_steps + grpo_args_kwargs["max_completion_length"] = cfg.trl.max_completion_length + grpo_args_kwargs["log_completions"] = cfg.trl.log_completions + return grpo_args_kwargs + + @classmethod + def set_trainer_args(cls, cfg): + trainer_args = [] + if cfg.trl and cfg.trl.reward_funcs: + reward_funcs = [] + for reward_func_fqn in cfg.trl.reward_funcs: + reward_funcs.append(cls.get_reward_func(reward_func_fqn)) + trainer_args.append(reward_funcs) + return trainer_args + + @classmethod + def set_trainer_kwargs(cls, cfg): + trainer_kwargs = {} + if cfg.trl and cfg.trl.reward_processing_classes: + trainer_kwargs[ + "reward_processing_classes" + ] = cfg.trl.reward_processing_classes + return trainer_kwargs + + @classmethod + def get_collator(cls, *args, **kwargs): # pylint: disable=unused-argument + # No data collation is needed in GRPO, handled by trl's trainer __init__ + return None + + @classmethod + def get_blocklist_args_kwargs(cls): + return ["dataset_num_proc"] + + @classmethod + def get_reward_func(cls, reward_func_fqn: str) -> RewardFunc: + """ + Returns the reward function from the given fully qualified name, or the path to the reward function model. + + Args: + reward_func_fqn (str): Fully qualified name of the reward function (e.g. r1_grpo.gsm8k_transform), + or a HF hub path to the reward model. + Raises: + ValueError: If the reward function does not accept at least two arguments. + + Returns: + RewardFunc: A callable that accepts prompts and completions and returns rewards, + or a path to a reward model. + + """ + try: + # use importlib to dynamically load the reward function from the module + reward_func_module_name = reward_func_fqn.split(".")[-1] + reward_func_module = importlib.import_module(reward_func_fqn.split(".")[-2]) + reward_func = getattr(reward_func_module, reward_func_module_name) + if not len(inspect.signature(reward_func).parameters) >= 2: + raise ValueError( + "Reward function must accept at least two arguments: prompts: list and completions: list" + ) + return reward_func + except ModuleNotFoundError: + # the user has passed a string (ideally indicating the path of a reward model) + LOG.info( + f"Reward function {reward_func_fqn} is a pre-trained model path - if this is unexpected, please check the reward function path." + ) + return reward_func diff --git a/src/axolotl/core/trainers/grpo/args.py b/src/axolotl/core/trainers/grpo/args.py new file mode 100644 index 0000000000..e14e6b0dc3 --- /dev/null +++ b/src/axolotl/core/trainers/grpo/args.py @@ -0,0 +1,15 @@ +""" +Axolotl Specific Training Args +""" +from dataclasses import dataclass + +from trl import GRPOConfig + +from axolotl.core.training_args import AxolotlTrainingMixins + + +@dataclass +class AxolotlGRPOConfig(AxolotlTrainingMixins, GRPOConfig): + """ + Axolotl GRPO Config for GRPO training + """ diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py new file mode 100644 index 0000000000..8f8b9fcf95 --- /dev/null +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -0,0 +1,107 @@ +""" +Axolotl GRPO trainer +""" +from accelerate.utils import is_peft_model +from accelerate.utils.other import is_compiled_module +from transformers import PreTrainedModel +from trl import GRPOConfig, GRPOTrainer +from trl.models import unwrap_model_for_generation + +from axolotl.core.trainers.base import SchedulerMixin + + +# mypy: ignore-errors +class AxolotlGRPOTrainer(SchedulerMixin, GRPOTrainer): + """ + Extend the base GRPOTrainer for axolotl helpers + """ + + _tag_names = ["trl", "grpo", "axolotl"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # pylint: disable=access-member-before-definition + # Enable gradient checkpointing if requested + if kwargs["args"].gradient_checkpointing: + # Ensure use_cache is disabled + if hasattr(self.model, "config"): + self.model.config.use_cache = False + + # Enable gradient checkpointing on the base model for PEFT + if is_peft_model(self.model) and hasattr( + self.model.base_model, "gradient_checkpointing_enable" + ): + self.model.base_model.gradient_checkpointing_enable() + # Enable gradient checkpointing for non-PEFT models + elif hasattr(self.model, "gradient_checkpointing_enable"): + self.model.gradient_checkpointing_enable() + self.model = self._enable_gradient_checkpointing(self.model, kwargs["args"]) + # pylint: enable=access-member-before-definition + + def _enable_gradient_checkpointing( + self, model: PreTrainedModel, args: GRPOConfig + ) -> PreTrainedModel: + """Enables gradient checkpointing for the model.""" + # pylint: disable=unused-argument,redefined-builtin + gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} + use_reentrant = ( + "use_reentrant" not in gradient_checkpointing_kwargs + or gradient_checkpointing_kwargs["use_reentrant"] + ) + + if use_reentrant: + if hasattr(model, "enable_input_require_grads"): + model.enable_input_require_grads() + else: + + def make_inputs_require_grad(module, input, output): + output.requires_grad_(True) + + model.get_input_embeddings().register_forward_hook( + make_inputs_require_grad + ) + + return model + # pylint: enable=unused-argument,redefined-builtin + + def _move_model_to_vllm(self): + with unwrap_model_for_generation( + self.model, + self.accelerator, + gather_deepspeed3_params=self.args.ds3_gather_for_generation, + ) as unwrapped_model: + if is_compiled_module(unwrapped_model): + unwrapped_model = ( + unwrapped_model._orig_mod # pylint: disable=protected-access + ) + if is_peft_model(unwrapped_model): + unwrapped_model.merge_adapter() + state_dict = unwrapped_model.state_dict() + unwrapped_model.unmerge_adapter() + # Remove base_model and base_layer prefixes + state_dict = { + k.removeprefix("base_model.model.") + .removeprefix("base_model.model.") + .replace(".base_layer", ""): v + for k, v in state_dict.items() + } + # Remove values with adapter prefix (example: "_lora") + state_dict = { + k: v + for k, v in state_dict.items() + if unwrapped_model.prefix not in k + } + # When module to save, remove its prefix and discard the original module + state_dict = { + k.replace("modules_to_save.default.", ""): v + for k, v in state_dict.items() + if "original_module" not in k + } + else: + state_dict = unwrapped_model.state_dict() + if self.accelerator.is_main_process: + llm_model = ( + self.llm.llm_engine.model_executor.driver_worker.model_runner.model + ) + llm_model.load_weights(state_dict.items()) diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index 9eae52162f..7cace7643c 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -5,7 +5,7 @@ from typing import Optional from transformers import TrainingArguments -from trl import CPOConfig, DPOConfig, KTOConfig, ORPOConfig, PRMConfig, RewardConfig +from trl import CPOConfig, KTOConfig, ORPOConfig, PRMConfig, RewardConfig @dataclass @@ -217,13 +217,6 @@ class AxolotlTrainingArguments(AxolotlTrainingMixins, TrainingArguments): """ -@dataclass -class AxolotlDPOConfig(AxolotlTrainingMixins, DPOConfig): - """ - DPO config for DPO training - """ - - @dataclass class AxolotlORPOConfig(AxolotlTrainingMixins, ORPOConfig): """ diff --git a/src/axolotl/prompt_strategies/base.py b/src/axolotl/prompt_strategies/base.py index cddb3d0e12..c146133fbd 100644 --- a/src/axolotl/prompt_strategies/base.py +++ b/src/axolotl/prompt_strategies/base.py @@ -13,8 +13,19 @@ def load(strategy, cfg, module_base=None, **kwargs): if len(strategy.split(".")) == 1: strategy = strategy + ".default" load_fn = strategy.split(".")[-1] - strategy = ".".join(strategy.split(".")[:-1]) - mod = importlib.import_module(f".{strategy}", module_base) + if len(strategy.split(".")) > 1: + try: + importlib.import_module( + strategy.split(".")[-2], + ".".join(strategy.split(".")[:-2]), + ) + module_base = ".".join(strategy.split(".")[:-2]) + strategy = strategy.split(".")[-2] + except ModuleNotFoundError: + strategy = "." + ".".join(strategy.split(".")[:-1]) + else: + strategy = "." + ".".join(strategy.split(".")[:-1]) + mod = importlib.import_module(strategy, module_base) func = getattr(mod, load_fn) return func(cfg, **kwargs) except Exception: # pylint: disable=broad-exception-caught diff --git a/src/axolotl/prompt_strategies/dpo/passthrough.py b/src/axolotl/prompt_strategies/dpo/passthrough.py new file mode 100644 index 0000000000..1fcb838db3 --- /dev/null +++ b/src/axolotl/prompt_strategies/dpo/passthrough.py @@ -0,0 +1,14 @@ +""" +DPO prompt strategies passthrough/zero-processing strategy +""" + + +def default( + cfg, dataset_idx=0, **kwargs +): # pylint: disable=possibly-unused-variable,unused-argument + def transform_fn( + sample, tokenizer=None + ): # pylint: disable=possibly-unused-variable,unused-argument + return sample + + return transform_fn diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index aa79c0f619..868328b0ba 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -24,6 +24,8 @@ from axolotl.utils.config.models.internals import EnvCapabilities, GPUCapabilities +from .trl import TRLConfig + LOG = logging.getLogger("axolotl.utils.config.models.input") SUPPORTED_METRICS = {"sacrebleu", "comet", "ter", "chrf", "perplexity"} @@ -33,6 +35,7 @@ class RLType(str, Enum): """RL trainer type configuration subset""" dpo = "dpo" # pylint: disable=invalid-name + grpo = "grpo" # pylint: disable=invalid-name ipo = "ipo" # pylint: disable=invalid-name orpo = "orpo" # pylint: disable=invalid-name kto = "kto" # pylint: disable=invalid-name @@ -664,14 +667,20 @@ class Config: auto_resume_from_checkpoints: Optional[bool] = None resize_token_embeddings_to_32x: Optional[bool] = None mean_resizing_embeddings: Optional[bool] = False + # optionally shrink the embeddings when the tokenizer vocab size is smaller + shrink_embeddings: Optional[bool] = None rl: Optional[RLType] = None + trl: Optional[TRLConfig] = Field( + default_factory=lambda: TRLConfig(), # pylint: disable=unnecessary-lambda + ) reward_model: Optional[bool] = None process_reward_model: Optional[bool] = None num_labels: Optional[int] = None dpo_use_weighting: Optional[ bool ] = None # whether to use weighting in DPO trainer. If none, default is false in the trainer. + dpo_use_logits_to_keep: Optional[bool] = None datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset, StepwiseSupervisedDataset], min_length=1)] = None # type: ignore test_datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset, StepwiseSupervisedDataset], min_length=1)] = None # type: ignore diff --git a/src/axolotl/utils/config/models/input/v0_4_1/trl.py b/src/axolotl/utils/config/models/input/v0_4_1/trl.py new file mode 100644 index 0000000000..6361bb2492 --- /dev/null +++ b/src/axolotl/utils/config/models/input/v0_4_1/trl.py @@ -0,0 +1,35 @@ +""" +GRPO specific configuration args +""" +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class TRLConfig(BaseModel): + """ + Input args for TRL. + """ + + beta: Optional[float] = None + max_completion_length: Optional[int] = Field( + default=None, + json_schema_extra={ + "description": "Maximum length of the completion for RL training" + }, + ) + + # GRPO specific args + use_vllm: Optional[bool] = False + vllm_device: Optional[str] = "auto" + vllm_gpu_memory_utilization: Optional[float] = 0.9 + vllm_max_model_len: Optional[int] = None + vllm_dtype: Optional[str] = "auto" + + reward_funcs: Optional[List[str]] = None + num_generations: Optional[int] = None + log_completions: Optional[bool] = False + + sync_ref_model: Optional[bool] = False + ref_model_mixup_alpha: Optional[float] = 0.9 + ref_model_sync_steps: Optional[int] = 64 diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index 67075cc9f6..4c7b712924 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -58,7 +58,7 @@ def _save_preprocessed_ds(cfg, sub_cfg, dataset): dataset.save_to_disk(str(prepared_ds_path)) -def map_dataset(cfg, data_set, ds_transform_fn, tokenizer): +def map_dataset(cfg, data_set, ds_transform_fn, tokenizer, **map_kwargs): sig = inspect.signature(ds_transform_fn) if "tokenizer" in sig.parameters: if not tokenizer: @@ -71,6 +71,7 @@ def map_dataset(cfg, data_set, ds_transform_fn, tokenizer): data_set = data_set.map( ds_transform_fn, desc="Mapping RL Dataset", + **map_kwargs, ) return data_set @@ -113,6 +114,9 @@ def drop_long_rl_seq( return (len_prompt + len_completion) <= sequence_len + if rl == "grpo": + return True + raise ValueError("Unknown RL type") @@ -140,36 +144,45 @@ def load_split(dataset_cfgs, _cfg): else: ds_transform_fn = load_dpo(_type, _cfg, dataset_idx=i) + map_kwargs = {} + if isinstance(ds_transform_fn, tuple): + ds_transform_fn, map_kwargs = ds_transform_fn split_datasets[i] = map_dataset( - cfg, data_set, ds_transform_fn, tokenizer + cfg, data_set, ds_transform_fn, tokenizer, **map_kwargs ) elif _cfg.rl == "kto": ds_transform_fn = load_kto(_type, _cfg, dataset_idx=i) + map_kwargs = {} + if isinstance(ds_transform_fn, tuple): + ds_transform_fn, map_kwargs = ds_transform_fn split_datasets[i] = map_dataset( - cfg, data_set, ds_transform_fn, tokenizer + cfg, data_set, ds_transform_fn, tokenizer, **map_kwargs ) else: # If no `type` is provided, assume the dataset is already in the expected format with # "prompt", "chosen" and "rejected" already preprocessed split_datasets[i] = data_set - drop_long = partial( - drop_long_rl_seq, - rl=_cfg.rl, - tokenizer=tokenizer, - sequence_len=cfg.sequence_len, - ) + if not cfg.skip_prepare_dataset: + drop_long = partial( + drop_long_rl_seq, + rl=_cfg.rl, + tokenizer=tokenizer, + sequence_len=cfg.sequence_len, + ) - prior_len = len(split_datasets[i]) - split_datasets[i] = split_datasets[i].filter( - drop_long, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Dropping Long Sequences", - ) - dropped = prior_len - len(split_datasets[i]) - if dropped: - LOG.warning(f"Dropped {dropped} long samples from dataset index {i}") + prior_len = len(split_datasets[i]) + split_datasets[i] = split_datasets[i].filter( + drop_long, + num_proc=cfg.dataset_processes, + load_from_cache_file=not cfg.is_preprocess, + desc="Dropping Long Sequences", + ) + dropped = prior_len - len(split_datasets[i]) + if dropped: + LOG.warning( + f"Dropped {dropped} long samples from dataset index {i}" + ) combined_datasets = concatenate_datasets(split_datasets) combined_datasets = combined_datasets.shuffle(seed=cfg.seed) diff --git a/src/axolotl/utils/lora.py b/src/axolotl/utils/lora.py new file mode 100644 index 0000000000..759c17ac26 --- /dev/null +++ b/src/axolotl/utils/lora.py @@ -0,0 +1,75 @@ +# Copyright 2025 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +module to get the state dict of a merged lora model +""" +import torch +from peft.tuners.tuners_utils import onload_layer +from peft.utils import ModulesToSaveWrapper, _get_submodules + + +def get_lora_merged_state_dict( + model: torch.nn.Module, +) -> dict: + r""" + Create and return a state_dict that has the LoRA deltas + merged into the base model’s weights, without modifying `model` in place. + + Arguments: + model (torch.nn.Module): A model that has LoRA/PEFT adapters attached. + + Returns: + dict: A state_dict of the merged parameters. + """ + + base_model_prefix = "base_model.model." + state_dict = {} + key_list = [key for key, _ in model.named_modules() if model.prefix not in key] + for key in key_list: + try: + _, target, _ = _get_submodules(model, key) + except AttributeError: + continue + with onload_layer(target): + weight_key = key.replace(base_model_prefix, "") + ".weight" + bias_key = key.replace(base_model_prefix, "") + ".bias" + if hasattr(target, "base_layer"): + target.merge(safe_merge=True, adapter_names=None) + # get the state_dict of target.base_layer + layer_state_dict = target.base_layer.state_dict() + state_dict[weight_key] = layer_state_dict["weight"] + elif isinstance(target, ModulesToSaveWrapper): + # save any additional trainable modules part of `modules_to_save` + new_module = target.modules_to_save[target.active_adapter] + if hasattr(new_module, "base_layer"): + # check if the module is itself a tuner layer + new_module.merge(safe_merge=True, adapter_names=None) + layer_state_dict = new_module.state_dict() + state_dict[weight_key] = layer_state_dict["weight"] + elif hasattr(target, "weight"): + if any( + skip in key + for skip in [ + ".original_module", + ".modules_to_save", + ".base_layer", + ] + ): + continue + layer_state_dict = target.state_dict() + state_dict[weight_key] = layer_state_dict["weight"] + if hasattr(target, "bias") and "bias" in layer_state_dict.keys(): + state_dict[bias_key] = layer_state_dict["bias"] + return state_dict diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index be5b2782a7..a96ecb0cf6 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -1053,9 +1053,12 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: if self.cfg.resize_token_embeddings_to_32x else len(self.tokenizer) ) - if ( - hasattr(self.model, "get_input_embeddings") - and self.model.get_input_embeddings().num_embeddings != embeddings_len + if hasattr(self.model, "get_input_embeddings") and ( + self.model.get_input_embeddings().num_embeddings < embeddings_len + or ( + self.model.get_input_embeddings().num_embeddings > embeddings_len + and self.cfg.shrink_embeddings + ) ): resize_kwargs = {} if self.cfg.mean_resizing_embeddings is not None: @@ -1309,6 +1312,7 @@ def load_lora(model, cfg, inference=False, config_only=False): lora_config_kwargs["init_lora_weights"] = "loftq" if cfg.peft_use_dora: lora_config_kwargs["use_dora"] = cfg.peft_use_dora + LOG.info("Initializing LoRA weights using dora. This might take longer.") if cfg.peft_use_rslora: lora_config_kwargs["use_rslora"] = cfg.peft_use_rslora if cfg.peft_layer_replication: diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 61f03e7ad1..c8e365fc57 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -576,7 +576,7 @@ def prepare_opinionated_env(cfg): def setup_trainer( cfg, train_dataset, eval_dataset, model, tokenizer, processor, total_num_steps ): - if cfg.rl in ("dpo", "ipo", "orpo", "kto", "simpo"): + if cfg.rl: trainer_builder = HFRLTrainerBuilder(cfg, model[0], tokenizer, processor) trainer_builder.model_ref = model[1] trainer_builder.peft_config = model[2] diff --git a/tests/e2e/multigpu/test_grpo.py b/tests/e2e/multigpu/test_grpo.py new file mode 100644 index 0000000000..d2b84994bf --- /dev/null +++ b/tests/e2e/multigpu/test_grpo.py @@ -0,0 +1,173 @@ +""" +GRPO test suite +""" +import random +from pathlib import Path + +import pytest +import yaml +from accelerate.test_utils import execute_subprocess_async +from e2e.utils import require_vllm +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + + +class TestGRPO: + """ + Test case for GRPO training using multilpe GPUs + """ + + def _utils_write_yaml_and_rewards(self, cfg, temp_dir, suffix=""): + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + with open(f"rewards_{suffix}.py", "w", encoding="utf-8") as fout: + fout.write( + """import random +def rand_reward_func(completions, **kwargs) -> list[float]: + return [random.uniform(0, 1) for _ in completions] + +def oai_gsm8k_transform(cfg, *args, **kwargs): + def transform_fn(example, tokenizer=None): + label = example["answer"].split("####")[-1].strip().replace(",", "") + return { + "prompt": [{"role": "user", "content": example["question"]},], + "answer": label, + } + return transform_fn, {"remove_columns": ["question"]} +""" + ) + + @pytest.mark.parametrize( + "num_gpus", + [1, 2], + ) + @require_vllm + def test_llama_dora(self, temp_dir, num_gpus): + rnd_reward_suffix = str(random.randint(1000, 9999)) + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "chat_template": "llama3", + "rl": "grpo", + "trl": { + "beta": 0.001, + "max_completion_length": 256, + "use_vllm": True, + "vllm_device": "auto" if num_gpus == 1 else "cuda:1", + "vllm_gpu_memory_utilization": 0.15, + "num_generations": 4, + "reward_funcs": [f"rewards_{rnd_reward_suffix}.rand_reward_func"], + }, + "datasets": [ + { + "path": "openai/gsm8k", + "name": "main", + "type": f"rewards_{rnd_reward_suffix}.oai_gsm8k_transform", + }, + ], + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "peft_use_dora": True, + "flash_attention": True, + "sequence_len": 1024, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "max_steps": 5, + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "warmup_steps": 10, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + "use_tensorboard": True, + } + ) + + self._utils_write_yaml_and_rewards(cfg, temp_dir, suffix=rnd_reward_suffix) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + str(num_gpus), + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + @pytest.mark.parametrize( + "num_gpus", + [1, 2], + ) + @require_vllm + def test_llama_fft(self, temp_dir, num_gpus): + rnd_reward_suffix = str(random.randint(1000, 9999)) + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "chat_template": "llama3", + "rl": "grpo", + "trl": { + "beta": 0.001, + "max_completion_length": 256, + "use_vllm": True, + "vllm_device": "auto" if num_gpus == 1 else "cuda:1", + "vllm_gpu_memory_utilization": 0.15, + "num_generations": 4, + "reward_funcs": [f"rewards_{rnd_reward_suffix}.rand_reward_func"], + }, + "datasets": [ + { + "path": "openai/gsm8k", + "name": "main", + "type": f"rewards_{rnd_reward_suffix}.oai_gsm8k_transform", + }, + ], + "flash_attention": True, + "sequence_len": 1024, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "max_steps": 5, + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "warmup_steps": 10, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + "use_tensorboard": True, + } + ) + + self._utils_write_yaml_and_rewards(cfg, temp_dir, suffix=rnd_reward_suffix) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + str(num_gpus), + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index a9f7fb28d2..ff96f1f58f 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -78,6 +78,24 @@ def is_max_2_6_0(): return unittest.skipUnless(is_max_2_6_0(), "test requires torch<2.6.0")(test_case) +def require_vllm(test_case): + """ + Decorator marking a test that requires a vllm to be installed + """ + + def is_vllm_installed(): + try: + import vllm # pylint: disable=unused-import # noqa: F401 + + return True + except ImportError: + return False + + return unittest.skipUnless( + is_vllm_installed(), "test requires a vllm to be installed" + )(test_case) + + def is_hopper(): compute_capability = torch.cuda.get_device_capability() return compute_capability == (9, 0) From 40362d60e02b8a71d917d916002ef96bdea064ff Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 14 Feb 2025 04:01:41 +0700 Subject: [PATCH 0417/1405] feat(doc): Improve guide to dataset types with better examples (#2286) --- docs/dataset-formats/conversation.qmd | 4 +- docs/dataset-formats/index.qmd | 460 +++++++++++++++++++++++++- docs/rlhf.qmd | 450 ++++++++++++++++++++++++- 3 files changed, 889 insertions(+), 25 deletions(-) diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 4d8dbe769a..5f9ac01a33 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -6,7 +6,7 @@ order: 3 ## sharegpt -IMPORTANT: ShareGPT is deprecated!. Please see `chat_template` section below. +IMPORTANT: ShareGPT is deprecated!. Please see [chat_template](#chat_template) section below. ## pygmalion @@ -22,7 +22,7 @@ Chat Template strategy uses a jinja2 template that converts a list of messages i {"conversations": [{"role": "...", "content": "..."}]} ``` -See `config.qmd` for full configs and supported templates. +See [configs](../config.qmd) for full configs and supported templates. ### Migrating from sharegpt diff --git a/docs/dataset-formats/index.qmd b/docs/dataset-formats/index.qmd index 91873a4c19..158fa79bd2 100644 --- a/docs/dataset-formats/index.qmd +++ b/docs/dataset-formats/index.qmd @@ -1,14 +1,456 @@ --- title: Dataset Formats -description: Supported dataset formats. -listing: - fields: [title, description] - type: table - sort-ui: false - filter-ui: false - max-description-length: 250 +description: Guide to Dataset Formats in Axolotl +back-to-top-navigation: true +toc: true +toc-depth: 5 --- -Axolotl supports a variety of dataset formats. It is recommended to use a JSONL format. The schema of the JSONL depends upon the task and the prompt template you wish to use. Instead of a JSONL, you can also use a HuggingFace dataset with columns for each JSONL field. -Below are these various formats organized by task: +Axolotl is a training framework that aims to make the process convenient yet flexible to users by simply passing a config yaml file. + +As there are a lot of available options in Axolotl, this guide aims to provide an simplify the user experience to choosing the proper choice. + +Axolotl supports 3 kinds of training methods: pre-training, supervised fine-tuning, and preference-based post-training (e.g. DPO, ORPO, PRMs). Each method has their own dataset format which are described below. + +## [Pre-training](pretraining.qmd) + +When aiming to train on large corpora of text datasets, pre-training is your go-to choice. Due to the size of these datasets, downloading the entire-datasets before beginning training would be prohibitively time-consuming. Axolotl supports [streaming](https://huggingface.co/docs/datasets/en/stream) to only load batches into memory at a time. + +A sample format for a pre-training dataset is as follows: + +```json +{"text": "first row"} +{"text": "second row"} +... +``` + +It is typically recommended to save your dataset as `.jsonl` due to its flexibility and simplicity. + +Axolotl supports loading from a Hugging Face hub repo or from local files. + +::: {.callout-important} +For pre-training only, Axolotl would split texts if it exceeds the context length into multiple smaller prompts. +::: + +### Pre-training from Hugging Face hub datasets + +As an example, to train using a Hugging Face dataset `hf_org/name`, you can pass the following config: + +```yaml +pretraining_dataset: hf_org/name +``` + +### Pre-training from local dataset files + +Given a few corpus files: `A.jsonl`, `B.jsonl`, and `C.jsonl`, your config will look like the below: + +```yaml +pretraining_dataset: + - path: json + data_files: + - A.jsonl + - B.jsonl + - C.jsonl +``` + +While we recommend `.jsonl`, you can also use the other formats (`csv`, `parquet`, `arrow`, `SQL`, `Webdataset`) that are supported by [`Dataset.load_dataset`](https://huggingface.co/docs/datasets/loading#local-and-remote-files) + +### Pre-training without streaming + +On the rare case that the dataset is small and can be loaded entirely into memory, another approach to running pre-training is to use the `completion` format. This would mean that the entire dataset is pre-tokenized instead of on-demand in streaming. + +One benefit of this is that the tokenization can be performed separately on a CPU-only machine, and then transferred to a GPU machine for training to save costs. + +From Hugging Face: + +```yaml +datasets: + - path: hf_org/name + type: completion +``` + +From local files (either example works): + +```yaml +datasets: + - path: A.jsonl + type: completion + + - path: json + data_files: ["A.jsonl", "B.jsonl", "C.jsonl"] + type: completion +``` + +### Pre-training dataset configuration tips + +#### Setting max_steps + +When using streaming for large datasets, Axolotl does not know in advance how large the dataset is and does not know when to stop. + +Therefore, it is necessary to set `max_steps: int` in your config for pre-training to run, so that Axolotl knows when to stop training. + +One step is equal to `sequence_len * micro_batch_size * gradient_accumulation_steps * total_num_gpus` tokens. + +#### Group_by_length + +It is recommended to leave this off if downloading from Hugging Face hub as it would download the entire dataset which can be very large. + +## Supervised fine-tuning (SFT) + +Supervised fine-tuning is the process of training models to respond to an instruction or chat input. + +As there are a wide variety of dataset formats, Axolotl tries to support a majority of the formats available in public datasets. + +Axolotl provides four approaches for loading datasets, however, it's easier to work backwards from the dataset you have available to figure out which approach to use. + +A flow chart is as follows: + +1. Do you already have the dataset tokenized? If yes, check [Pre-Tokenized Dataset](#pre-tokenized-dataset). + +2. Do you want to format the dataset yourself and manually choose each section to mask? If yes, check [Template Free Dataset](#template-free-dataset) + +3. Is your dataset in a "conversation" format, containing a `list[messages]`? If yes, check [Conversation Dataset](#conversation-dataset) + +4. Is your dataset in an "instruct" format, containing `{ instruction, response }`? If yes, check [Instruction Dataset](#instruction-dataset) + +If you went through the flow chart and did not find one that matches, it is recommended to preprocess your dataset into one of the above or create a Github Discussion. + +::: {.callout-tip} +You can mix and match within each approach or across approaches to train a model on a variety of datasets. +::: + +### [Pre-Tokenized Dataset](tokenized.qmd) + +We suggest this approach when you want to bring your own tokenized dataset. + +Axolotl expects the dataset to have three keys: +- `input_ids`: from tokenizing formatted prompt +- `attention_mask`: for masking padding. If you don't add padding, it would be equal to `len(input_ids) * [1]` +- `labels`: this is the same as `input_ids`, however, if you want to mask certain tokens, you would set those indices to `-100`. + +::: {.callout-tip} +Make sure to add BOS/EOS tokens to your prompt and mask it appropriately. +::: + +A config for this would look like: + +```yaml +datasets: + - path: A.jsonl + type: +``` + +::: {.callout-note} +`type: ` is empty! +::: + +### [Template Free Dataset](template_free.qmd) + +We reccomend this approach when you want granular control over the prompt formatting, special tokens, and masking, whilst letting Axolotl handle the tokenization. This is very useful if your dataset has unique prompts that differ across samples and where one single general template wouldn't suffice. + +In the example below, you could see that there is no proper structure. At the same time, it's very flexible as there are no constraints on how your prompt can look. + +```json +{ + "segments": [ + { + "label": true, + "text": "Hello\n" + }, + { + "label": true, + "text": "hi there!. " + }, + { + "label": false, + "text": "goodbye " + }, + { + "label": true, + "text": "farewell" + } + ] +} +``` + +Each prompt must be have a key called `segments` which is a list of `{ text, label }`. + +```yaml +datasets: + - path: A.jsonl + type: input_output +``` + +### [Conversation Dataset](conversation.qmd) + +`conversation` messages are a list of messages which usually contain a `role` and `content` key. + +::: {.callout-tip} +Fun fact: Axolotl synonymously refers to "chat" messages as `conversation` messages due to how FastChat initially used this term to build a widely used [fastchat conversation](https://github.com/lm-sys/FastChat/blob/main/fastchat/conversation.py) method for formatting chat messages prior to the creation of `chat_templates`. +::: + +#### What are `chat_templates`? + +The current most popular and convenient method for inference is to use `chat_templates` for formatting prompts. Axolotl supports using `chat_templates` for training to ensure that the model performs in the same environment as in inference. + +Here's a quick rundown on `chat_template`: A `chat_template` is a Jinja2 template which formats a list of messages into a prompt. + +An example of a prompt formatted into a popular template called ChatML can be seen below: + +Single prompt (pretty-printed): +```json +{ + "messages": [ + { + "role": "user", + "content": "Hi" + }, + { + "role": "assistant", + "content": "How can I help you?" + }, + { + "role": "user", + "content": "Can you add 3+5?" + }, + { + "role": "assistant", + "content": "The answer is 8." + } + ] +} +``` + +The ChatML template is as follows: +```jinja2 +{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %} +``` + +The above prompt formatted into this template will result in: + +``` +<|im_start|>user +Hi<|im_end|> +<|im_start|>assistant +How can I help you?<|im_end|> +<|im_start|>user +Can you add 3+5?<|im_end|> +<|im_start|>assistant +The answer is 8.<|im_end|> +``` + +By using delimiters (`<|im_start|>` and `<|im_end|>`), a prompt separates different speakers which helps the model identify which portion belongs to whom. + +#### Common Conversation Dataset formats + +Older conversation datasets with the following format are colloquially called `sharegpt` datasets. + +```json +{"conversations": [{"from": "...", "value": "..."}]} +``` + +Newer conversation datasets usually follow the OpenAI format. + +```json +{"messages": [{"role": "...", "content": "..."}]} +``` + +Axolotl supports both as well as allowing customization of any kind of key. + +#### [Chat Template Usage](conversation.qmd#chat_template) + +To properly use this method, it is important to identify three things: + +1. Which `chat_template` would you use? + +2. What are the keys in your dataset, and what are the possible roles? For example, in OpenAI format, the keys would be `messages`, `role`, and `content`, respectively, whereas the possible roles are `system`, `user`, and `assistant`. + +3. What do you want to mask? For instance, only assistant messages, only last message, or nothing. + +##### Choosing a `chat_template` + +There are a lot of `chat_templates` out there. Axolotl supports the common ones: [supported chat templates](https://github.com/axolotl-ai-cloud/axolotl/blob/860609392184cf62a7e0ca676658b170e059ce6c/src/axolotl/utils/chat_templates.py#L17). For example, to use ChatML, it would be `chat_template: chatml`. + +However, it is also possible to use the already configured template within the tokenizer by specifying `chat_template: tokenizer_default`. If you want a fallback (in case some tokenizer does not have it pre-configured), you can do `chat_template: tokenizer_default_fallback_chatml` to fallback to the ChatML template if a tokenizer template was not found. + +One last but powerful approach is to bring your own template. This can be set via: + +```yaml +chat_template_jinja: # your template +``` + +##### Setting `chat_template` dataset keys + +We currently default to OpenAI format for dataset keys, so if that's your current dataset format, there's nothing to do here. + +If your dataset format is different, here are the keys you should check (with their defaults): + +```yaml +datasets: + ... + field_messages: messages + message_field_role: role + message_field_content: content +``` + +In some `chat_templates` (e.g. [Gemma](https://huggingface.co/google/gemma-2b-it/blob/main/tokenizer_config.json#L1507)), the roles are hardcoded to `user` and `assistant`. Consequently, you may find it necessary to map the roles in your dataset to these above. We currently have some defaults that should work for common datasets, but if you get a `KeyError`, it would be necessary to add mapping for your roles. Here is an example of how it would look like: + +```yaml +datasets: + ... + roles: + assistant: + - gpt + - model + user: + - human +``` + +In the example above, all `gpt` and `model` values are converted to `assistant`. All `human` values are converted to `user.` + +##### Handling masking + +The common use case for `chat_template` is for chat messages, therefore, it is common to mask all non-assistant messages. Assistant messages refer to the bot messages that you want the model to learn on. + +To train on all `assistant` messages, you would set the following configs. + +```yaml +datasets: + ... + roles_to_train: ["assistant"] + train_on_eos: "turn" +``` + +The `train_on_eos` config means that it would mask all EOS tokens for turns that aren't assistant-turns. The other options are: `all` and `last` to choose which EOS to train on. + +Perhaps, you want to train on `assistant` and `narrator` roles, you can simply add `narrator` to the list of `roles_to_train`. You would also need to add it to the mapping of `roles` above. + +```yaml +datasets: + ... + roles_to_train: ["assistant", "narrator"] + roles: + assistant: + - gpt + - model + user: + - human + narrator: ["narrator"] +``` + +#### Applying `chat_template` + +Once all the above steps are completed, you could combine all these configs together to form a bespoke configuration for your custom dataset. The final step would be to correctly set the EOS token in your config: + +```yaml +datasets: + - path: A.jsonl + type: chat_template + + # step 1 + chat_template: chatml + + # step 2 + field_messages: messages + message_field_role: role + message_field_content: content + + roles: + assistant: + - gpt + - model + - assistant + user: + - human + - user + + # step 3 + roles_to_train: ["assistant"] + train_on_eos: "turn" + +special_tokens: + eos_token: <|im_end|> +``` + +If this config were to be applied to the sample dataset above, the output would look as such (which can be retrieved via `axolotl preprocess config.yaml --debug`): + +``` +<|im_start|>(-100, 128256) user(-100, 882) +(-100, 198) Hi(-100, 13347) <|im_end|>(-100, 128257) +(-100, 198) <|im_start|>(-100, 128256) assistant(-100, 78191) +(-100, 198) How(4438, 4438) can(649, 649) I(358, 358) help(1520, 1520) you(499, 499) ?(30, 30) <|im_end|>(128257, 128257) +(-100, 198) <|im_start|>(-100, 128256) user(-100, 882) +(-100, 198) Can(-100, 6854) you(-100, 499) add(-100, 923) (-100, 220) 3(-100, 18) +(-100, 10) 5(-100, 20) ?(-100, 30) <|im_end|>(-100, 128257) +(-100, 198) <|im_start|>(-100, 128256) assistant(-100, 78191) +(-100, 198) The(791, 791) answer(4320, 4320) is(374, 374) (220, 220) 8(23, 23) .(13, 13) <|im_end|>(128257, 128257) +(-100, 198) +``` + +The first number refers to the label, the second refers to the `token_id`. For example, `-100` labels appear on non-assistant portions, meaning that they are masked during. For assistant portions, the label is the same as the `token_id`. + +### [Instruction Dataset](inst_tune.qmd) + +Instruction datasets are used to train instruction-following models and comprise a prompt, containing an instruction, and a single response. In contrast to chat datasets which may be multi-turn, instruct datasets are typically single-turn. + +An example is of a common format called Alpaca: +```json +{"instruction": "...", "input": "...", "output": "..."} +``` + +Using those keys, a prompt can be built based on it. +``` +Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request. + +### Instruction: +{instruction} + +### Input: +{input} + +### Response: +{output} +``` + +This can be configured as such: +```yaml +datasets: + - path: A.jsonl + type: alpaca +``` + +Axolotl supports many kinds of instruction dataset. All of them can be found here (https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/inst_tune.html) with their respective type and sample row format. + +#### Custom Instruct Prompt Format + +Due to the myriad possibilities of instruction formats, Axolotl allows customizing your own instruction format without having to dive into the code directly. + +In the example below, a sample row is used to output in `mistral_v1` format. +```json +{"input": "...", "output": "..."} +``` + +```yaml +datasets: + - path: repo + type: + system_prompt: "" + + field_system: + field_instruction: input + field_input: + field_output: output + + # multi-line example with input + format: |- + [INST] {instruction} {input} [/INST] + + # single-line example without input + no_input_format: "[INST] {instruction} [/INST]" +``` + +The config sets that the `field_instruction` is actually named `input`, and the `field_input` is empty as we don't have an `input` in this sample. Generally, `instruction` can be thought as the question to the model, and `input` as the additional information with `output` being the response. It is not necessary to have an `input` nor `system`. In the end, the most important part is to understand what format you want it to look like and how you can customize this to your use case. + +## Reinforcement Learning from Human Feedback (RLHF) + +As there are multiple RLHF methods with their own dataset requirements. Please see [RLHF datasets](../rlhf.qmd) documentation for more detail. diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 8f52876c18..86f874799d 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -1,26 +1,39 @@ --- title: "RLHF (Beta)" description: "Reinforcement Learning from Human Feedback is a method whereby a language model is optimized from data using human feedback." +back-to-top-navigation: true +toc: true +toc-depth: 3 --- -### Overview +# Overview Reinforcement Learning from Human Feedback is a method whereby a language model is optimized from data using human feedback. Various methods include, but not limited to: - Proximal Policy Optimization (PPO) (not yet supported in axolotl) -- Direct Preference Optimization (DPO) -- Identity Preference Optimization (IPO) +- [Direct Preference Optimization (DPO)](#dpo) +- [Identity Preference Optimization (IPO)](#ipo) +- [Kahneman-Tversky Optimization (KTO)](#kto) +- [Odds Ratio Preference Optimization (ORPO)](#orpo) -### RLHF using Axolotl +# RLHF using Axolotl ->[!IMPORTANT] ->This is a BETA feature and many features are not fully implemented. You are encouraged to open new PRs to improve the integration and functionality. +::: {.callout-important} +This is a BETA feature and many features are not fully implemented. You are encouraged to open new PRs to improve the integration and functionality. +::: -The various RL training methods are implemented in trl and wrapped via axolotl. Below are various examples with how you can use various preference datasets to train models that use ChatML +We rely on the [TRL](https://github.com/huggingface/trl) library for implementations of various RL training methods, which we wrap around to expose in axolotl. Each method has their own supported ways of loading datasets and prompt formats. + +::: {.callout-tip} +You can find what each method supports by going into `src/axolotl/prompt_strategies/{method}` where `{method}` is one of our supported methods. The `type: ` can be retrieved from `{method}.{function_name}`. +::: + +## DPO + +Example config: -#### DPO ```yaml rl: dpo datasets: @@ -32,12 +45,264 @@ datasets: type: chatml ``` -#### IPO +DPO supports the following types with the following dataset format: + +### chatml.argilla + +```json +{ + "system": "...", // optional + "instruction": "...", + "chosen_response": "...", + "rejected_response": "..." +} +``` + +### chatml.argilla_chat + +```json +{ + "chosen": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ], + "rejected": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ] +} +``` + +### chatml.icr + +```json +{ + "system": "...", // optional + "input": "...", + "chosen": "...", + "rejected": "..." +} +``` + +### chatml.intel + +```json +{ + "system": "...", // optional + "question": "...", + "chosen": "...", + "rejected": "..." +} +``` + +### chatml.prompt_pairs + +```json +{ + "system": "...", // optional + "prompt": "...", + "chosen": "...", + "rejected": "..." +} +``` + +### chatml.ultra + +```json +{ + "system": "...", // optional + "prompt": "...", + "chosen": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ], + "rejected": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ] +} +``` + +### llama3.argilla + +```json +{ + "system": "...", // optional + "instruction": "...", + "chosen_response": "...", + "rejected_response": "..." +} +``` + +### llama3.argilla_chat + +```json +{ + "chosen": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ], + "rejected": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ] +} +``` + +### llama3.icr + +```json +{ + "system": "...", // optional + "input": "...", + "chosen": "...", + "rejected": "..." +} +``` + +### llama3.intel + +```json +{ + "system": "...", // optional + "question": "...", + "chosen": "...", + "rejected": "..." +} +``` + +### llama3.prompt_pairs + +```json +{ + "system": "...", // optional + "prompt": "...", + "chosen": "...", + "rejected": "..." +} +``` + +### llama3.ultra + +```json +{ + "system": "...", // optional + "prompt": "...", + "chosen": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ], + "rejected": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ] +} +``` + +### zephyr.nectar + +```json +{ + "prompt": "...", + "answers": [ + { + "answer": "...", + "rank": 1 + }, + { + "answer": "...", + "rank": 2 + } + // ... more answers with ranks + ] +} +``` + +### chat_template.default + +```yaml +rl: dpo +datasets: + - path: ... + split: train + type: chat_template.default + field_messages: "messages" + field_chosen: "chosen" + field_rejected: "rejected" + message_field_role: "role" + message_field_content: "content" + roles: + user: ["user"] + assistant: ["assistant"] + system: ["system"] +``` + +Sample input format: + +```json +{ + "messages": [ + { + "role": "system", + "content": "..." + }, + { + "role": "user", + "content": "..." + }, + // ... more messages + ], + "chosen": { + "role": "assistant", + "content": "..." + }, + "rejected": { + "role": "assistant", + "content": "..." + } +} +``` + +### user_defined.default + +For custom behaviors, + +```yaml +rl: dpo +datasets: + - path: ... + split: train + type: user_defined.default + + field_prompt: "prompt" + field_system: "system" + field_chosen: "chosen" + field_rejected: "rejected" + prompt_format: "{prompt}" + chosen_format: "{chosen}" + rejected_format: "{rejected}" +``` + +The input format is a simple JSON input with customizable fields based on the above config. + +```json +{ + "system": "...", // optional + "prompt": "...", + "chosen": "...", + "rejected": "..." +} +``` + +## IPO + +As IPO is just DPO with a different loss function, all supported options for DPO works here. + ```yaml rl: ipo ``` -#### ORPO +## ORPO Paper: https://arxiv.org/abs/2403.07691 @@ -52,8 +317,28 @@ datasets: type: chat_template.argilla ``` +ORPO supports the following types with the following dataset format: + +### chat_template.argilla -#### KTO +```json +{ + "system": "...", // optional + "prompt": "...", // if available, will be taken as user message for single-turn instead of from list below + + // chosen/rejected should be same till last content and only even-number of alternating user/assistant turns + "chosen": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ], + "rejected": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ] +} +``` + +## KTO ```yaml rl: kto @@ -72,7 +357,144 @@ gradient_checkpointing_kwargs: use_reentrant: true ``` -#### Using local dataset files +KTO supports the following types with the following dataset format: + +### chatml.argilla + +```json +{ + "system": "...", // optional + "instruction": "...", + "completion": "..." +} +``` + +### chatml.argilla_chat + +```json +{ + "chosen": [ + {"role": "user", "content": "..."} + ], + "completion": [ + {"role": "assistant", "content": "..."} + ] +} +``` + +### chatml.intel + +```json +{ + "system": "...", // optional + "question": "...", + "completion": "..." +} +``` + +### chatml.prompt_pairs + +```json +{ + "system": "...", // optional + "prompt": "...", + "completion": "..." +} +``` + +### chatml.ultra + +```json +{ + "system": "...", // optional + "prompt": "...", + "completion": "..." +} +``` + +### llama3.argilla + +```json +{ + "system": "...", // optional + "instruction": "...", + "completion": "..." +} +``` + +### llama3.argilla_chat + +```json +{ + "completion": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ] +} +``` + +### llama3.intel + +```json +{ + "system": "...", // optional + "question": "...", + "completion": "..." +} +``` + +### llama3.prompt_pairs + +```json +{ + "system": "...", // optional + "prompt": "...", + "completion": "..." +} +``` + +### llama3.ultra + +```json +{ + "system": "...", // optional + "prompt": "...", + "completion": "..." +} +``` + +### user_defined.default + +For custom behaviors, + +```yaml +rl: kto +datasets: + - path: ... + split: train + type: user_defined.default + + field_prompt: "prompt" + field_system: "system" + field_completion: "completion" + field_label: "label" + prompt_format: "{prompt}" + completion_format: "{completion}" +``` + +The input format is a simple JSON input with customizable fields based on the above config. + +```json +{ + "system": "...", // optional + "prompt": "...", + "completion": "...", + "label": "..." +} +``` + +## Using local dataset files + ```yaml datasets: - ds_type: json @@ -82,9 +504,9 @@ datasets: type: chatml.intel ``` -#### Trl autounwrap for peft +## TRL auto-unwrapping for PEFT -Trl supports autounwrapping peft models, so that a ref model does not need to be additionally loaded, leading to less VRAM needed. This is on by default. To turn it off, pass the following config. +TRL supports auto-unwrapping PEFT models for RL training paradigms which rely on a reference model. This significantly reduces memory pressure as an additional refreference model does not need to be loaded, and reference model log-probabilities can be obtained by disabling PEFT adapters. This is enabled by default. To turn it off, pass the following config: ```yaml # load ref model when adapter training. From a09a5cfd1c8d8cbf558214185902a9611d22be65 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 14 Feb 2025 04:02:16 +0700 Subject: [PATCH 0418/1405] feat(doc): add tensorboard config to docs (#2329) --- docs/config.qmd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/config.qmd b/docs/config.qmd index 91744856f5..327f4ae6f6 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -348,6 +348,9 @@ comet_mode: # Create a new experiment ("create") or log to an existing one ("get comet_online: # Set to True to log data to Comet server, or False for offline storage. Default is True. comet_experiment_config: # Dictionary for additional configuration settings, see the doc for more details. +# Tensorboard +use_tensorboard: # Optional[bool] + # Where to save the full-finetuned model to output_dir: ./completed-model From aa45fed451b258105f47ee6937a2d29e5ace1ade Mon Sep 17 00:00:00 2001 From: minpeter Date: Fri, 14 Feb 2025 07:27:55 +0900 Subject: [PATCH 0419/1405] Add `bos_token` and `add_generation_prompt` to the alpaca chat template (#2322) * fix alpaca add_generation_prompt * Alpaca template considering multi-turn Co-authored-by: xzuyn --------- Co-authored-by: xzuyn --- src/axolotl/utils/chat_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 682a0449e8..41385bb3b5 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -15,7 +15,7 @@ _DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX = "tokenizer_default_fallback_" _CHAT_TEMPLATES = { - "alpaca": "{% for message in messages %}{% if message['role'] == 'user' %}{{ '### Instruction: ' + message['content'] + '\n\n' }}{% elif message['role'] == 'assistant' %}{{ '### Response: ' + message['content'] + eos_token}}{% endif %}{% endfor %}", + "alpaca": "{{ bos_token }}{% for message in messages %}{% if message['role'] == 'system' and loop.first %}{{ message['content'] }}{% elif message['role'] == 'user' %}{{ '### Instruction:\n' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '### Response:\n' + message['content'] + eos_token }}{% endif %}{% if not loop.last %}{{ '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '\n\n### Response:\n' }}{% endif %}", "mistral_v1": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ ' [INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # Mistral 7B V1, Mistral 7B V2, Mixtral 8x7B V1... "mistral_v2v3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # V3: Mistral 7B V3, Small, Large... "mistral_v3_tekken": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST]' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # V3-Tekken: Nemo, Pixtral... From 2e57391bf81a2cfcad9eebe22afb38990a13881f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 14 Feb 2025 05:28:21 +0700 Subject: [PATCH 0420/1405] fix: add missing shards_idx, preprocess_shards to docs and validator (#2331) --- docs/config.qmd | 7 ++++++- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/config.qmd b/docs/config.qmd index 327f4ae6f6..5221cbe7dc 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -91,7 +91,12 @@ datasets: type: alpaca # format | format: (chat/instruct) | .load_ ds_type: # Optional[str] (json|arrow|parquet|text|csv) defines the datatype when path is a file data_files: # Optional[str] path to source data files - shards: # Optional[int] number of shards to split data into + + shards: # Optional[int] split dataset into N pieces (use with shards_idx) + shards_idx: # Optional[int] = 0 the index of sharded dataset to use + + preprocess_shards: # Optional[int] process dataset in N sequential chunks for memory efficiency (exclusive with `shards`) + name: # Optional[str] name of dataset configuration to load train_on_split: train # Optional[str] name of dataset split to load from revision: # Optional[str] The specific revision of the dataset to use when loading from the Hugging Face Hub. This can be a commit hash, tag, or branch name. If not specified, the latest version will be used. This parameter is ignored for local datasets. diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 868328b0ba..1f6fdc612f 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -169,6 +169,7 @@ class SFTDataset(BaseModel): type: Optional[Union[str, UserDefinedPrompterType]] = None input_transform: Optional[str] = None shards: Optional[int] = None + shards_idx: Optional[int] = None preprocess_shards: Optional[int] = None conversation: Optional[str] = None # Do not make this too strict or it will break the validator to choose different dataset class From a98526ef7843a3e8aa006f260e6b4fb8912b5f1a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 13 Feb 2025 17:39:19 -0500 Subject: [PATCH 0421/1405] add support for include_tokens_per_second in training args (#2269) * add support for include_tokens_per_second in training args * Update docs/config.qmd Co-authored-by: NanoCode012 * Update src/axolotl/core/trainer_builder.py Co-authored-by: NanoCode012 --------- Co-authored-by: NanoCode012 --- docs/config.qmd | 3 +++ src/axolotl/core/trainer_builder.py | 6 ++++++ src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 + 3 files changed, 10 insertions(+) diff --git a/docs/config.qmd b/docs/config.qmd index 5221cbe7dc..a7a1508625 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -390,6 +390,9 @@ save_total_limit: # Checkpoints saved at a time # e.g., when 1 epoch is 1000 steps => `num_epochs: 2` and `max_steps: 100` will train for 100 steps max_steps: +# bool of whether to include tokens trainer per second in the training metrics. This iterates over the entire dataset once, so it takes some time. +include_tokens_per_second: + eval_table_size: # Approximate number of predictions sent to wandb depending on batch size. Enabled above 0. Default is 0 eval_max_new_tokens: # Total number of tokens generated for predictions sent to wandb. Default is 128 eval_causal_lm_metrics: # HF evaluate metrics used during evaluation. Default is ["sacrebleu", "comet", "ter", "chrf", "perplexity"] diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index ae757cf43b..12346b8a27 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -330,6 +330,12 @@ def build(self, total_num_steps): ) training_arguments_kwargs = {} + + if self.cfg.include_tokens_per_second is not None: + training_arguments_kwargs[ + "include_tokens_per_second" + ] = self.cfg.include_tokens_per_second + if self.cfg.bf16 == "full": training_arguments_kwargs["bf16_full_eval"] = True else: diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 1f6fdc612f..1e7a6aa8b7 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -844,6 +844,7 @@ class Config: save_only_model: Optional[bool] = False use_tensorboard: Optional[bool] = None profiler_steps: Optional[int] = None + include_tokens_per_second: Optional[bool] = None neftune_noise_alpha: Optional[float] = None From 97a2fa27819c1e3de74f3c14d51b5b47d5b23aa6 Mon Sep 17 00:00:00 2001 From: Seungduk Kim Date: Mon, 17 Feb 2025 14:07:27 +0900 Subject: [PATCH 0422/1405] Select input_ids explicitly after panda conversion (#2335) Without selecting the column, applying `len` counts the whole row as 1 which resulting the total number of the samples instead of the token counts. --- src/axolotl/utils/trainer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index c8e365fc57..8553339b99 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -396,8 +396,8 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): ): total_num_tokens = np.sum( train_dataset.select_columns("input_ids") - .to_pandas() - .apply(lambda x: len(x)) # pylint: disable=unnecessary-lambda + .to_pandas()["input_ids"] + .apply(len) .values ) LOG.debug(f"total_num_tokens: {total_num_tokens:_}", main_process_only=True) From 3d8425fa918924f999ba8532a74289fe2cdb1138 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 17 Feb 2025 14:23:15 -0500 Subject: [PATCH 0423/1405] Activation function Triton kernels, LoRA custom autograd functions (#2324) * LoRA + activation fn Triton kernels: initial commit * implementing optims * finalizing MLP LoRA kernels and progress on QKV / W kernels * updates * O projection optim * adding monkey patching logic * doc strings, typing, pre-commit fixes * updates * adding lora 8b kernels example * working on fsdp support * tests and fixes * small fixes, getting tests to pass, adding doc strings * integration tests for LoRA patching * config.qmd * remove unneeded pytest fixture * fix * review comments first pass * improving tests, attention class agnostic patching * adding support for more archs * wip SiLU / GELU impls * improved testing, small updates, etc. * slightly updating docs * rebase * fixing test_attention_patching_integration * additional review comments, fixing test in CI (hopefully) * isolating problematic patching test * relaxing allclose threshold to reduce flakiness * fixing accidental change * adding model arch agnostic attention class fetching * removing unused activations --- cicd/cicd.sh | 4 +- cicd/tests.py | 4 +- docs/config.qmd | 7 + docs/lora_optims.qmd | 127 +++ examples/llama-3/lora-1b-kernels.yml | 82 ++ src/axolotl/cli/main.py | 5 +- src/axolotl/kernels/__init__.py | 0 src/axolotl/kernels/geglu.py | 159 ++++ src/axolotl/kernels/lora.py | 779 ++++++++++++++++++ src/axolotl/kernels/quantize.py | 149 ++++ src/axolotl/kernels/swiglu.py | 163 ++++ src/axolotl/kernels/utils.py | 11 + src/axolotl/monkeypatch/lora_kernels.py | 333 ++++++++ src/axolotl/train.py | 2 + .../config/models/input/v0_4_1/__init__.py | 64 +- src/axolotl/utils/models.py | 33 +- tests/e2e/kernels/test_geglu.py | 76 ++ tests/e2e/kernels/test_lora.py | 531 ++++++++++++ tests/e2e/kernels/test_quantize.py | 103 +++ tests/e2e/kernels/test_swiglu.py | 78 ++ tests/e2e/patched/lora_kernels/__init__.py | 0 .../lora_kernels/test_lora_kernel_patching.py | 414 ++++++++++ 22 files changed, 3102 insertions(+), 22 deletions(-) create mode 100644 docs/lora_optims.qmd create mode 100644 examples/llama-3/lora-1b-kernels.yml create mode 100644 src/axolotl/kernels/__init__.py create mode 100644 src/axolotl/kernels/geglu.py create mode 100644 src/axolotl/kernels/lora.py create mode 100644 src/axolotl/kernels/quantize.py create mode 100644 src/axolotl/kernels/swiglu.py create mode 100644 src/axolotl/kernels/utils.py create mode 100644 src/axolotl/monkeypatch/lora_kernels.py create mode 100644 tests/e2e/kernels/test_geglu.py create mode 100644 tests/e2e/kernels/test_lora.py create mode 100644 tests/e2e/kernels/test_quantize.py create mode 100644 tests/e2e/kernels/test_swiglu.py create mode 100644 tests/e2e/patched/lora_kernels/__init__.py create mode 100644 tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 34a30db448..8097e9c56b 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -4,8 +4,8 @@ set -e python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__" pytest -v --durations=10 -n8 --ignore=tests/e2e/ --ignore=tests/patched/ /workspace/axolotl/tests/ -# pytest -v --durations=10 -n8 --dist loadfile /workspace/axolotl/tests/patched/ -pytest -v --durations=10 /workspace/axolotl/tests/e2e/patched/ +pytest -v --durations=10 /workspace/axolotl/tests/e2e/patched/lora_kernels # running these with the other patches causes a failure +pytest -v --durations=10 --ignore=tests/e2e/patched/lora_kernels /workspace/axolotl/tests/e2e/patched pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/solo/ pytest -v --durations=10 /workspace/axolotl/tests/e2e/integrations/ pytest -v --durations=10 --ignore=tests/e2e/solo/ --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ diff --git a/cicd/tests.py b/cicd/tests.py index b934d5316a..41ae2306fa 100644 --- a/cicd/tests.py +++ b/cicd/tests.py @@ -1,6 +1,4 @@ -""" - modal application to run axolotl gpu tests in Modal - """ +"""Modal app to run axolotl GPU tests""" # pylint: disable=duplicate-code import os diff --git a/docs/config.qmd b/docs/config.qmd index a7a1508625..19e33e7f0b 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -305,6 +305,13 @@ lora_modules_to_save: lora_fan_in_fan_out: false +# Apply custom LoRA autograd functions and activation function Triton kernels for +# speed and memory savings +# See: https://axolotl-ai-cloud.github.io/axolotl/docs/lora_optims.html +lora_mlp_kernel: true +lora_qkv_kernel: true +lora_o_kernel: true + # LoRA+ hyperparameters # For more details about the following options, see: # https://arxiv.org/abs/2402.12354 and `src/axolotl/core/train_builder.py` diff --git a/docs/lora_optims.qmd b/docs/lora_optims.qmd new file mode 100644 index 0000000000..a57aa854b1 --- /dev/null +++ b/docs/lora_optims.qmd @@ -0,0 +1,127 @@ +--- +title: "LoRA Optimizations" +description: "Custom autograd functions and Triton kernels in Axolotl for optimized +LoRA fine-tuning" +--- + +Inspired by [Unsloth](https://github.com/unslothai/unsloth), we've implemented two +optimizations for LoRA and QLoRA fine-tuning, supporting both single GPU and multi-GPU +(in the DDP and DeepSpeed settings) training. These include (1) SwiGLU and GEGLU activation function +Triton kernels, and (2) LoRA MLP and attention custom autograd functions. Our goal was +to leverage operator fusion and tensor re-use in order to improve speed and reduce +memory usage during the forward and backward passes of these calculations. + +We currently support several common model architectures, including (but not limited to): +- `llama` +- `mistral` +- `qwen2` +- `gemma` +- `gemma2` + +
+ +The set of models we support is currently limited by our attention patching strategy, +which assumes (and replaces) specific code blocks for query / key / value and output +projections: + +```python +ORIGINAL_QKV_CODE = """ + query_states = self.q_proj(hidden_states).view(hidden_shape).transpose(1, 2) + key_states = self.k_proj(hidden_states).view(hidden_shape).transpose(1, 2) + value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) +""".lstrip( + "\n" +) + +ORIGINAL_O_CODE = """ + attn_output = self.o_proj(attn_output) +""".lstrip( + "\n" +) +``` + +Is replaced with: + +```python +PATCHED_QKV_CODE = """ + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states = query_states.view(hidden_shape).transpose(1, 2) + key_states = key_states.view(hidden_shape).transpose(1, 2) + value_states = value_states.view(hidden_shape).transpose(1, 2) +""".lstrip( + "\n" +) + +PATCHED_O_CODE = """ + attn_output = self.apply_o(attn_output) +""".lstrip( + "\n" +) +``` + +Where `apply_qkv` and `apply_o` are defined in the `axolotl.kernels.lora` module. + +We welcome testing of other model architectures and / or PRs to expand our patching +logic to be compatible with more of them. + +
+ +## Usage + +These optimizations can be enabled in your Axolotl config YAML file. The +`lora_mlp_kernel` option enables the optimized MLP path, while `lora_qkv_kernel` and +`lora_o_kernel` enable the fused query-key-value projection and optimized output +projection, respectively. + +```yaml +lora_mlp_kernel: true +lora_qkv_kernel: true +lora_o_kernel: true +``` + +## Requirements + +- One or more NVIDIA or AMD GPUs (in order to use the Triton kernels) + - AMD can be used with experimental Triton support by setting the environment variable `TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1` +- Targeted LoRA adapters cannot use Dropout + - This may limit model expressivity / cause overfitting +- Targeted LoRA adapters cannot have bias terms + - This may limit model expressivity + +Models with pre-existing LoRA adapters that use Dropout or have bias terms may need to +be re-finetuned without these features in order to be useful. + +## Implementation details + +### Custom autograd functions + +The LoRA MLP autograd function optimizes the entire MLP computation path. It fuses the +LoRA and base weight computations together and provides a single, efficient backward +pass for the entire MLP block. + +For attention components, similar optimizations are provided through a function that +handles the query, key, and value projections, and a function that handles the output +projection. They are designed to work with the existing `transformers` attention +implementation via some monkey-patching logic. + +### Triton kernels + +Two activation functions (SwiGLU and GeGLU) are implemented with Triton kernels for +improved speed and memory performance. These kernels handle both the forward and +backward passes. + +### Integration + +The custom autograd functions and Triton kernels are designed to work together. The +autograd function manages the high-level computation flow and gradient tracking, while +calling the Triton kernels for the activation function computation. During the backward +pass, the kernel computes both the activation output and the required gradients, which +the autograd function then uses to compute the final gradients for the entire +computation path. + +## Future Work + +- Support for additional model architectures +- Support for the FSDP setting +- Support for dropout and bias +- Additional operator fusions diff --git a/examples/llama-3/lora-1b-kernels.yml b/examples/llama-3/lora-1b-kernels.yml new file mode 100644 index 0000000000..9c47f266f1 --- /dev/null +++ b/examples/llama-3/lora-1b-kernels.yml @@ -0,0 +1,82 @@ +base_model: NousResearch/Llama-3.2-1B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +datasets: + - path: teknium/GPT4-LLM-Cleaned + type: alpaca +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: lora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true +pad_to_sequence_len: true + +lora_r: 16 +lora_alpha: 32 +# Currently, we don't support dropout with our custom Triton kernels +# lora_dropout: 0.05 +lora_fan_in_fan_out: +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +# These options enable our custom Triton kernels / autograd +# functions for MLP and attention calculations +lora_mlp_kernel: true +lora_qkv_kernel: true +lora_o_kernel: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +loss_watchdog_threshold: 5.0 +loss_watchdog_patience: 3 + +warmup_steps: 10 +evals_per_epoch: 4 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: + pad_token: "<|end_of_text|>" diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index 61dc4403ad..dcdbe895f9 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -167,7 +167,6 @@ def train( """ # Enable expandable segments for cuda allocation to improve VRAM usage set_pytorch_cuda_alloc_conf() - from axolotl.cli.cloud import do_cli_train if "use_ray" in kwargs and kwargs["use_ray"]: accelerate = False @@ -201,6 +200,8 @@ def iter_configs(): try: if accelerate: if cloud: + from axolotl.cli.cloud import do_cli_train + cwd = os.getcwd() do_cli_train( cloud_config=cloud, @@ -229,6 +230,8 @@ def iter_configs(): subprocess.run(cmd, check=True) # nosec B603 else: if cloud: + from axolotl.cli.cloud import do_cli_train + do_cli_train( cloud_config=cloud, config=config, accelerate=False, **kwargs ) diff --git a/src/axolotl/kernels/__init__.py b/src/axolotl/kernels/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/kernels/geglu.py b/src/axolotl/kernels/geglu.py new file mode 100644 index 0000000000..4dd70f4cc9 --- /dev/null +++ b/src/axolotl/kernels/geglu.py @@ -0,0 +1,159 @@ +""" +Module for definition of GEGLU Triton kernels. + +See "GLU Variants Improve Transformer" (https://arxiv.org/abs/2002.05202). + +Credit to `unsloth` (https://unsloth.ai/) for inspiration for this implementation. +""" +# pylint: disable=invalid-name,unnecessary-lambda-assignment,duplicate-code + +import torch +import triton +import triton.language as tl + +SQRT_2_PI: tl.constexpr = 0.7978845608028654 # sqrt(2/π) + + +@triton.jit +def _geglu_fwd_kernel( + gate_ptr, + up_ptr, + out_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, +): + """GEGLU forward kernel. + + Args: + gate_ptr: Pointer to gate tensor [*, hidden_dim]. + up_ptr: Pointer to up-projection tensor [*, hidden_dim]. + out_ptr: Pointer to output tensor [*, hidden_dim]. + n_elements: Total number of elements in the input tensors. + BLOCK_SIZE: Size of thread blocks for parallel computation. + """ + block_idx = tl.program_id(0) + offsets = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + + gate = tl.load(gate_ptr + offsets, mask=mask, other=0).to(tl.float32) + up = tl.load(up_ptr + offsets, mask=mask, other=0) + + # Compute activation in fp32 then convert back + gelu_gate = 0.5 * gate * (tl.math.erf(tl.math.rsqrt(2.0) * gate) + 1.0) + gelu_gate = gelu_gate.to(up.dtype) + result = gelu_gate * up + + tl.store(out_ptr + offsets, result, mask=mask) + + +def geglu_forward(gate: torch.Tensor, up: torch.Tensor) -> torch.Tensor: + """GEGLU forward pass. + + Args: + gate: Input gate tensor of shape [batch, seq_len, hidden_dim]. + up: Up-projection tensor of shape [batch, seq_len, hidden_dim]. + + Returns: + torch.Tensor: Output tensor of shape [batch, seq_len, hidden_dim]. + """ + batch, seq_len, hidden_dim = gate.shape + n_elements = gate.numel() + out = torch.empty((batch, seq_len, hidden_dim), dtype=gate.dtype, device="cuda") + + grid = lambda meta: (triton.cdiv(n_elements, meta["BLOCK_SIZE"]),) # noqa: E731 + _geglu_fwd_kernel[grid]( + gate_ptr=gate, + up_ptr=up, + out_ptr=out, + n_elements=n_elements, + BLOCK_SIZE=1024, + ) + return out + + +@triton.jit +def _geglu_bwd_kernel( + grad_out_ptr, + gate_ptr, + up_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, +): + """GEGLU backward kernel. Stores gradient results in-place. + + Args: + grad_out_ptr: Pointer to gradient output tensor [*, hidden_dim]. + gate_ptr: Pointer to gate tensor [*, hidden_dim]. + up_ptr: Pointer to up-projection tensor [*, hidden_dim]. + n_elements: Total number of elements in the input tensors. + BLOCK_SIZE: Size of thread blocks for parallel computation. + + Note: + After kernel execution, tensors are modified in-place: + - `grad_out_ptr` contains GEGLU activation output (`h`) + - `gate_ptr` contains gradient w.r.t gate (`grad_gate`) + - `up_ptr` contains gradient w.r.t up (`grad_up`) + """ + block_idx = tl.program_id(0) + offsets = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + + grad_out = tl.load(grad_out_ptr + offsets, mask=mask, other=0) + gate = tl.load(gate_ptr + offsets, mask=mask, other=0).to(tl.float32) + up = tl.load(up_ptr + offsets, mask=mask, other=0) + + # Forward pass + gelu_partial = 0.5 * (tl.math.erf(tl.math.rsqrt(2.0) * gate) + 1.0) + gelu_gate = gelu_partial * gate + gelu_gate = gelu_gate.to(grad_out.dtype) + + # Forward output + h = gelu_gate * up + + # Compute gradients + grad_up = grad_out * gelu_gate + + # Compute gate gradient using GELU derivative + temp = grad_out * up + t = 0.3989422804014327 # 1/sqrt(2*pi) + dgelu_dgate = gelu_partial + t * gate * tl.exp(-0.5 * gate * gate) + grad_gate = temp.to(tl.float32) * dgelu_dgate + grad_gate = grad_gate.to(grad_out.dtype) + + # Store results + tl.store(grad_out_ptr + offsets, h, mask=mask) + tl.store(gate_ptr + offsets, grad_gate, mask=mask) + tl.store(up_ptr + offsets, grad_up, mask=mask) + + +def geglu_backward( + grad_output: torch.Tensor, gate: torch.Tensor, up: torch.Tensor +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """GEGLU backward pass using in-place operations. + + Args: + grad_output: Gradient of loss with respect to output, shape `[batch, seq_len, hidden_dim]`. + gate: Gate tensor from forward pass, shape `[batch, seq_len, hidden_dim]`. + up: Up-projection tensor from forward pass, shape `[batch, seq_len, hidden_dim]`. + + Returns: + Tuple containing: + - GEGLU activation output (`h`) + - Gradient with respect to gate (`grad_gate`) + - Gradient with respect to up (`grad_up`) + + Note: + This function modifies its input tensors in-place to store results. + """ + n_elements = grad_output.numel() + + grid = lambda meta: (triton.cdiv(n_elements, meta["BLOCK_SIZE"]),) # noqa: E731 + _geglu_bwd_kernel[grid]( + grad_out_ptr=grad_output, + gate_ptr=gate, + up_ptr=up, + n_elements=n_elements, + BLOCK_SIZE=1024, + ) + + return grad_output, gate, up diff --git a/src/axolotl/kernels/lora.py b/src/axolotl/kernels/lora.py new file mode 100644 index 0000000000..1f8a8e787b --- /dev/null +++ b/src/axolotl/kernels/lora.py @@ -0,0 +1,779 @@ +""" +Module for definition of Low-Rank Adaptation (LoRA) Triton kernels. + +See "LoRA: Low-Rank Adaptation of Large Language Models" +(https://arxiv.org/abs/2106.09685). + +Credit to `unsloth` (https://unsloth.ai/) for inspiration for this implementation. +""" +# pylint: disable=invalid-name + +from typing import Callable + +import torch +from bitsandbytes.functional import QuantState +from torch import nn + +from .geglu import geglu_backward, geglu_forward +from .quantize import dequantize +from .swiglu import swiglu_backward, swiglu_forward +from .utils import torch_amp_custom_bwd, torch_amp_custom_fwd + + +def get_lora_parameters( + proj: nn.Module, +) -> tuple[ + torch.Tensor, + QuantState | None, + torch.Tensor | None, + torch.Tensor | None, + float | None, +]: + """ + Gets LoRA parameters from a projection module. + + Args: + proj: The projection module to extract parameters from. + + Returns: + A tuple containing the base weight matrix, quantization state, LoRA A matrix, + LoRA B matrix, and scaling factor. States and matrices may be None if not + available. + """ + # For DPO or disabled adapters + base_layer = proj.base_layer if hasattr(proj, "base_layer") else proj + W = base_layer.weight + + if not hasattr(proj, "disable_adapters") or proj.disable_adapters or proj.merged: + quant_state = getattr(W, "quant_state", None) + return W, quant_state, None, None, None + + active_adapter = ( + proj.active_adapters[0] + if hasattr(proj, "active_adapters") + else proj.active_adapter + ) + A = proj.lora_A[active_adapter].weight + B = proj.lora_B[active_adapter].weight + s = proj.scaling[active_adapter] + + quant_state = getattr(W, "quant_state", None) + + return W, quant_state, A, B, s + + +def matmul_lora( + X: torch.Tensor, + W: torch.Tensor, + W_quant: QuantState, + A: torch.Tensor, + B: torch.Tensor, + s: float, + out: torch.Tensor | None = None, +) -> torch.Tensor: + """ + Efficient fused matmul + LoRA computation. + + Args: + X: Input tensor [*, in_features] + W: Base weight matrix [out_features, in_features] + W_quant: Quantization state for W + A: LoRA A matrix [rank, in_features] + B: LoRA B matrix [out_features, rank] + s: LoRA scaling factor + out: Optional output tensor for inplace operations + + Returns: + Result of X @ W + X @ A @ B + """ + dtype = X.dtype + W = dequantize(W.t(), W_quant) + + if X.dim() == 3: + batch, seq_len, _ = X.shape + X = X.view(-1, X.shape[-1]) + reshape = True + else: + reshape = False + + out = torch.matmul(X, W, out=out) + if W_quant is not None: + del W + + if A is not None: + A, B = A.t(), B.t() + out += (X @ A.to(dtype)) @ (s * B.to(dtype)) + + return out.view(batch, seq_len, -1) if reshape else out + + +class LoRA_MLP(torch.autograd.Function): + """Optimized LoRA MLP implementation.""" + + @staticmethod + @torch_amp_custom_fwd + def forward( + ctx, + X: torch.Tensor, + gate_weight: torch.Tensor, + gate_quant: object | None, + gate_A: torch.Tensor | None, + gate_B: torch.Tensor | None, + gate_scale: float, + up_weight: torch.Tensor, + up_quant: object | None, + up_A: torch.Tensor | None, + up_B: torch.Tensor | None, + up_scale: float, + down_weight: torch.Tensor, + down_quant: object | None, + down_A: torch.Tensor | None, + down_B: torch.Tensor | None, + down_scale: float, + activation_fn: Callable, + activation_fn_backward: Callable, + inplace: bool | None = True, + ) -> torch.Tensor: + """ + Forward pass for LoRA MLP. + + Args: + ctx: Autograd context + X: Input features + gate_weight: Gate projection weight + gate_quant: Gate quantization state + gate_A: Gate LoRA A matrix + gate_B: Gate LoRA B matrix + gate_scale: Gate LoRA scale + up_weight: Up-projection weight + up_quant: Up-projection quantization state + up_A: Up-projection LoRA A matrix + up_B: Up-projection LoRA B matrix + up_scale: Up-projection LoRA scale + down_weight: Down-projection weight + down_quant: Down-projection quantization state + down_A: Down-projection LoRA A matrix + down_B: Down-projection LoRA B matrix + down_scale: Down-projection LoRA scale + activation_fn: Forward activation function + activation_fn_backward: Backward activation function + inplace: Whether to perform operations in-place + + Returns: + Output transformed by multi-layer perceptron and activation function + """ + # Compute projections + gate = matmul_lora(X, gate_weight, gate_quant, gate_A, gate_B, gate_scale) + up = matmul_lora(X, up_weight, up_quant, up_A, up_B, up_scale) + + # Activation + hidden = activation_fn(gate, up) + + # Down projection + output = matmul_lora( + hidden, down_weight, down_quant, down_A, down_B, down_scale + ) + + # Save for backward + ctx.save_for_backward(X, gate, up, gate_A, gate_B, up_A, up_B, down_A, down_B) + ctx.scales = (gate_scale, up_scale, down_scale) + ctx.quants = (gate_quant, up_quant, down_quant) + ctx.weights = (gate_weight, up_weight, down_weight) + ctx.activation_fn = activation_fn + ctx.activation_fn_backward = activation_fn_backward + ctx.inplace = inplace + + return output + + @staticmethod + @torch_amp_custom_bwd + def backward( + ctx: torch.autograd.function.FunctionCtx, + grad_output: torch.Tensor, + ) -> tuple[ + torch.Tensor | None, + None, + None, + torch.Tensor | None, + torch.Tensor | None, + None, + None, + None, + torch.Tensor | None, + torch.Tensor | None, + None, + None, + None, + torch.Tensor | None, + torch.Tensor | None, + None, + None, + None, + None, + ]: + """ + Performs backward pass computation for LoRA MLP. + + Args: + ctx: Context object storing tensors saved during forward pass + grad_output: Gradient of loss with respect to layer output + + Returns: + Tuple containing gradients for all inputs from forward pass: + - Input gradient tensor (or `None`) + - `None` for weights/quantization states + - LoRA A/B matrix gradients (or `None`) + - `None` for scaling factors + - `None` for activation functions and flags + """ + ( + X, + gate, + up, + gate_A, + gate_B, + up_A, + up_B, + down_A, + down_B, + ) = ctx.saved_tensors + gate_scale, up_scale, down_scale = ctx.scales + gate_quant, up_quant, down_quant = ctx.quants + gate_weight, up_weight, down_weight = ctx.weights + + # Transpose all LoRA matrices + gate_A, gate_B = ( + gate_A.t() if gate_A is not None else None, + gate_B.t() if gate_B is not None else None, + ) + up_A, up_B = ( + up_A.t() if up_A is not None else None, + up_B.t() if up_B is not None else None, + ) + down_A, down_B = ( + down_A.t() if down_A is not None else None, + down_B.t() if down_B is not None else None, + ) + + # Reshape inputs + batch, seq_len, hd = X.shape + grad_output = grad_output.view(-1, grad_output.shape[-1]) + X = X.view(-1, X.shape[-1]) + gate = gate.view(-1, gate.shape[-1]) + up = up.view(-1, up.shape[-1]) + dtype = X.dtype + + # Down projection + DW = matmul_lora( + grad_output, + down_weight.t(), + down_quant, + down_B, + down_A, + down_scale, + ) + + # Activation backward + h, grad_gate, grad_up = ctx.activation_fn_backward(DW, gate, up) + + # Initialize and compute LoRA gradients + d_down_A = d_down_B = d_up_A = d_up_B = d_gate_A = d_gate_B = None + + if down_A is not None: + d_down_A = h.t() @ (grad_output @ down_B.t()) + d_down_B = (down_A.t() @ h.t()) @ grad_output + d_down_A *= down_scale + d_down_B *= down_scale + + if up_A is not None: + d_up_A = X.t() @ (grad_up @ up_B.t()) + d_up_B = (up_A.t() @ X.t()) @ grad_up + d_up_A *= up_scale + d_up_B *= up_scale + + if gate_A is not None: + d_gate_A = X.t() @ (grad_gate @ gate_B.t()) + d_gate_B = (gate_A.t() @ X.t()) @ grad_gate + d_gate_A *= gate_scale + d_gate_B *= gate_scale + + # Compute input gradients + dX = torch.zeros_like(X) if ctx.needs_input_grad[0] else None + + if dX is not None: + # Up projection gradients + up_weight = dequantize(up_weight.t(), up_quant) + if ctx.inplace: + dX = torch.matmul(grad_up, up_weight.t(), out=X) + else: + dX = torch.matmul(grad_up, up_weight.t()) + del up_weight + + # Note the .to(dtype) only where mixing LoRA with base weights + if up_A is not None: + dX += grad_up @ up_B.to(dtype).t() @ (up_scale * up_A.to(dtype).t()) + + # Gate projection gradients + gate_weight = dequantize(gate_weight.t(), gate_quant) + dX += grad_gate @ gate_weight.t() + del gate_weight + + if gate_A is not None: + dX += ( + grad_gate + @ gate_B.to(dtype).t() + @ (gate_scale * gate_A.to(dtype).t()) + ) + + # Reshape back + dX = dX.view(batch, seq_len, hd) + + # Return gradients in correct order matching forward inputs + return ( + dX, + None, + None, + d_gate_A.t() if d_gate_A is not None else None, + d_gate_B.t() if d_gate_B is not None else None, + None, + None, + None, + d_up_A.t() if d_up_A is not None else None, + d_up_B.t() if d_up_B is not None else None, + None, + None, + None, + d_down_A.t() if d_down_A is not None else None, + d_down_B.t() if d_down_B is not None else None, + None, + None, + None, + None, + ) + + +def apply_lora_mlp_swiglu(self, X: torch.Tensor, inplace: bool = True) -> torch.Tensor: + """ + Applies LoRA to MLP layer with SwiGLU activation. + + Args: + X: Input tensor for the MLP layer + inplace: Whether to perform operations in-place to save memory + + Returns: + Output tensor after applying LoRA-adapted MLP with SwiGLU activation + """ + gateW, gateW_quant, gateA, gateB, gateS = get_lora_parameters(self.gate_proj) + upW, upW_quant, upA, upB, upS = get_lora_parameters(self.up_proj) + downW, downW_quant, downA, downB, downS = get_lora_parameters(self.down_proj) + + out = LoRA_MLP.apply( + X, + gateW, + gateW_quant, + gateA, + gateB, + gateS, + upW, + upW_quant, + upA, + upB, + upS, + downW, + downW_quant, + downA, + downB, + downS, + swiglu_forward, + swiglu_backward, + inplace, + ) + + return out + + +def apply_lora_mlp_geglu(self, X: torch.Tensor, inplace: bool = True) -> torch.Tensor: + """ + Applies LoRA to MLP layer with GEGLU activation. + + Args: + X: Input tensor for the MLP layer + inplace: Whether to perform operations in-place to save memory + + Returns: + Output tensor after applying LoRA-adapted MLP with GEGLU activation + """ + gateW, gateW_quant, gateA, gateB, gateS = get_lora_parameters(self.gate_proj) + upW, upW_quant, upA, upB, upS = get_lora_parameters(self.up_proj) + downW, downW_quant, downA, downB, downS = get_lora_parameters(self.down_proj) + out = LoRA_MLP.apply( + X, + gateW, + gateW_quant, + gateA, + gateB, + gateS, + upW, + upW_quant, + upA, + upB, + upS, + downW, + downW_quant, + downA, + downB, + downS, + geglu_forward, + geglu_backward, + inplace, + ) + + return out + + +class LoRA_QKV(torch.autograd.Function): + """ + Optimized LoRA QKV implementation with quantization support. + + Implements efficient computation of query, key, value projections with LoRA, + supporting quantization and memory optimization. + """ + + @staticmethod + @torch_amp_custom_fwd + def forward( + ctx: torch.autograd.function.FunctionCtx, + X: torch.Tensor, + q_weight: torch.Tensor, + q_quant: QuantState | None, + q_A: torch.Tensor | None, + q_B: torch.Tensor | None, + q_scale: float, + k_weight: torch.Tensor, + k_quant: QuantState | None, + k_A: torch.Tensor | None, + k_B: torch.Tensor | None, + k_scale: float, + v_weight: torch.Tensor, + v_quant: QuantState | None, + v_A: torch.Tensor | None, + v_B: torch.Tensor | None, + v_scale: float, + inplace: bool = True, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Forward pass computing Q, K, V projections with LoRA. + + Args: + ctx: Autograd context + X: Input tensor + q_weight: Query projection weight + q_quant: Query quantization state + q_A: Query LoRA A matrix + q_B: Query LoRA B matrix + q_scale: Query LoRA scale + k_weight: Key projection weight + k_quant: Key quantization state + k_A: Key LoRA A matrix + k_B: Key LoRA B matrix + k_scale: Key LoRA scale + v_weight: Value projection weight + v_quant: Value quantization state + v_A: Value LoRA A matrix + v_B: Value LoRA B matrix + v_scale: Value LoRA scale + inplace: Whether to perform operations in-place + + Returns: + Tuple of (Query, Key, Value) projection tensors + """ + Q = matmul_lora(X, q_weight, q_quant, q_A, q_B, q_scale) + K = matmul_lora(X, k_weight, k_quant, k_A, k_B, k_scale) + V = matmul_lora(X, v_weight, v_quant, v_A, v_B, v_scale) + + ctx.save_for_backward(X, q_A, q_B, k_A, k_B, v_A, v_B) + ctx.scales = (q_scale, k_scale, v_scale) + ctx.quants = (q_quant, k_quant, v_quant) + ctx.weights = (q_weight, k_weight, v_weight) + ctx.inplace = inplace + + return Q, K, V + + @staticmethod + @torch_amp_custom_fwd + def backward( + ctx: torch.autograd.function.FunctionCtx, + q_grad: torch.Tensor, + k_grad: torch.Tensor, + v_grad: torch.Tensor, + ) -> tuple[ + torch.Tensor, + None, + None, + torch.Tensor | None, + torch.Tensor | None, + None, + None, + None, + torch.Tensor | None, + torch.Tensor | None, + None, + None, + None, + torch.Tensor | None, + torch.Tensor | None, + None, + None, + ]: + """ + Backward pass computing gradients for LoRA QKV. + + Args: + ctx: Autograd context + q_grad: Gradient for query projection + k_grad: Gradient for key projection + v_grad: Gradient for value projection + + Returns: + Tuple containing gradients for all forward inputs + """ + X, A_q, B_q, A_k, B_k, A_v, B_v = ctx.saved_tensors + q_weight, k_weight, v_weight = ctx.weights + q_quant, k_quant, v_quant = ctx.quants + q_scale, k_scale, v_scale = ctx.scales + dtype = X.dtype + + # Reshape gradients + batch, seq_len = X.shape[:2] + q_grad = q_grad.view(-1, q_grad.shape[-1]) + k_grad = k_grad.reshape(-1, k_grad.shape[-1]) + v_grad = v_grad.view(-1, v_grad.shape[-1]) + X = X.view(-1, X.shape[-1]) + + # Pre-transpose X once + X_t = X.t() + + # Initialize LoRA gradients as None + d_A_q = d_B_q = d_A_k = d_B_k = d_A_v = d_B_v = None + + # Compute q path LoRA gradients if adapters exist + if A_q is not None and B_q is not None: + A_q_scaled = (q_scale * A_q).to(dtype) + B_q_scaled = B_q.to(dtype) + d_A_q = torch.mm(X_t, torch.mm(q_grad, B_q_scaled)) + d_B_q = torch.mm(torch.mm(A_q_scaled, X_t), q_grad) + + # Compute k path LoRA gradients if adapters exist + if A_k is not None and B_k is not None: + A_k_scaled = (k_scale * A_k).to(dtype) + B_k_scaled = B_k.to(dtype) + d_A_k = torch.mm(X_t, torch.mm(k_grad, B_k_scaled)) + d_B_k = torch.mm(torch.mm(A_k_scaled, X_t), k_grad) + + # Compute v path LoRA gradients if adapters exist + if A_v is not None and B_v is not None: + A_v_scaled = (v_scale * A_v).to(dtype) + B_v_scaled = B_v.to(dtype) + d_A_v = torch.mm(X_t, torch.mm(v_grad, B_v_scaled)) + d_B_v = torch.mm(torch.mm(A_v_scaled, X_t), v_grad) + + # Compute input gradient, reusing X memory if possible + out_buffer = X if ctx.inplace else None + + # Q path + q_weight_t = dequantize(q_weight, q_quant) + grad_X = torch.mm(q_grad, q_weight_t, out=out_buffer) + del q_weight + del q_weight_t + if A_q is not None and B_q is not None: + grad_X.addmm_(q_grad, torch.mm(B_q_scaled, A_q_scaled)) + + # K path + k_weight_t = dequantize(k_weight, k_quant) + grad_X.addmm_(k_grad, k_weight_t) + del k_weight + del k_weight_t + if A_k is not None and B_k is not None: + grad_X.addmm_(k_grad, torch.mm(B_k_scaled, A_k_scaled)) + + # V path + v_weight_t = dequantize(v_weight, v_quant) + grad_X.addmm_(v_grad, v_weight_t) + del v_weight + del v_weight_t + if A_v is not None and B_v is not None: + grad_X.addmm_(v_grad, torch.mm(B_v_scaled, A_v_scaled)) + + # Transpose gradients if needed + if d_A_q is not None: + d_A_q = d_A_q.t() + if d_B_q is not None: + d_B_q = d_B_q.t() + if d_A_k is not None: + d_A_k = d_A_k.t() + if d_B_k is not None: + d_B_k = d_B_k.t() + if d_A_v is not None: + d_A_v = d_A_v.t() + if d_B_v is not None: + d_B_v = d_B_v.t() + + return ( + grad_X.view(batch, seq_len, -1), + None, + None, + d_A_q, + d_B_q, + None, + None, + None, + d_A_k, + d_B_k, + None, + None, + None, + d_A_v, + d_B_v, + None, + None, + ) + + +def apply_lora_qkv( + self, X: torch.Tensor, inplace: bool = True +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Applies LoRA to compute Query, Key, Value projections. + + Args: + X: Input tensor + inplace: Whether to perform operations in-place + + Returns: + Tuple of (Query, Key, Value) projection tensors + """ + QW, QW_quant, QA, QB, QS = get_lora_parameters(self.q_proj) + KW, KW_quant, KA, KB, KS = get_lora_parameters(self.k_proj) + VW, VW_quant, VA, VB, VS = get_lora_parameters(self.v_proj) + Q, K, V = LoRA_QKV.apply( + X, + QW, + QW_quant, + QA, + QB, + QS, + KW, + KW_quant, + KA, + KB, + KS, + VW, + VW_quant, + VA, + VB, + VS, + inplace, + ) + + return Q, K, V + + +class LoRA_O(torch.autograd.Function): + """Optimized LoRA implementation for output projection.""" + + @staticmethod + @torch_amp_custom_fwd + def forward( + ctx: torch.autograd.function.FunctionCtx, + X: torch.Tensor, + W: torch.Tensor, + W_quant: QuantState | None, + A: torch.Tensor | None, + B: torch.Tensor | None, + S: float, + ) -> torch.Tensor: + """ + Forward pass for output projection with LoRA. + + Args: + ctx: Autograd context + X: Input tensor + W: Output projection weight + W_quant: Weight quantization state + A: LoRA A matrix + B: LoRA B matrix + S: LoRA scaling factor + + Returns: + Output projection tensor + """ + XW = matmul_lora(X, W, W_quant, A, B, S) + ctx.custom_saved_tensors = ( + W, + W_quant, + S, + ) + ctx.save_for_backward(A, B, X) + + return XW + + @staticmethod + @torch_amp_custom_bwd + def backward( + ctx: torch.autograd.function.FunctionCtx, + dY: torch.Tensor, + ) -> tuple[ + torch.Tensor, + None, + None, + torch.Tensor | None, + torch.Tensor | None, + None, + ]: + """ + Backward pass computing gradients for LoRA output projection. + + Args: + ctx: Autograd context + dY: Gradient of loss with respect to output + + Returns: + Tuple containing gradients for all forward inputs + """ + W, W_quant, S = ctx.custom_saved_tensors + A, B, X = ctx.saved_tensors + + batch, seq_len, hd = X.shape + dY = dY.reshape(-1, dY.shape[-1]) + X = X.reshape(-1, X.shape[-1]) + dtype = X.dtype + + # Weight projection + dY_X = X.t() @ dY + d_A = S * dY_X @ B + d_B = S * A @ dY_X + + # Get derivative for dX + W = dequantize(W.t(), W_quant) + dX = dY @ W.t() + del W + dX += dY @ B.to(dtype) @ (S * A.to(dtype)) + + # W, W_quant, A, B, S + return dX.view(batch, seq_len, hd), None, None, d_A.t(), d_B.t(), None + + +def apply_lora_o(self, X: torch.Tensor) -> torch.Tensor: + """ + Applies LoRA to output projection layer. + + Args: + X: Input tensor + + Returns: + Transformed output tensor + """ + OW, OW_quant, OA, OB, OS = get_lora_parameters(self.o_proj) + output = LoRA_O.apply(X, OW, OW_quant, OA, OB, OS) + + return output diff --git a/src/axolotl/kernels/quantize.py b/src/axolotl/kernels/quantize.py new file mode 100644 index 0000000000..ea5ecf8e84 --- /dev/null +++ b/src/axolotl/kernels/quantize.py @@ -0,0 +1,149 @@ +"""Dequantization utilities for `bitsandbytes` integration.""" +# pylint: disable=invalid-name,global-statement + +import ctypes + +import bitsandbytes as bnb +import torch +from bitsandbytes.functional import QuantState, get_ptr +from packaging.version import Version + +cdequantize_blockwise_fp32 = bnb.functional.lib.cdequantize_blockwise_fp32 +cdequantize_blockwise_fp16_nf4 = bnb.functional.lib.cdequantize_blockwise_fp16_nf4 +cdequantize_blockwise_bf16_nf4 = bnb.functional.lib.cdequantize_blockwise_bf16_nf4 + +CUDA_STREAM: torch.cuda.Stream | None = None +HAS_CUDA_STREAM: bool = Version(bnb.__version__) > Version("0.43.3") + + +def dequantize( + W: torch.Tensor, + quant_state: QuantState | list | None = None, + out: torch.Tensor | None = None, +) -> torch.Tensor: + """ + Fast NF4 dequantization using `bitsandbytes` CUDA kernels. + + Performs efficient dequantization of weights from NF4 format using `bitsandbytes`' + optimized CUDA implementations. Supports both legacy list and new `QuantState` + formats. + + Args: + W: Quantized weight tensor to dequantize + quant_state: Quantization state containing metadata needed for + dequantization. Can be either a `QuantState` object or legacy list format. + If None, returns `W` unchanged. + out: Optional output tensor for storing dequantized results. Must match + expected shape and dtype if provided. + + Returns: + Dequantized tensor in the specified dtype (fp16 or bf16). Will be transposed if + input `W` was transposed. + + Raises: + AssertionError: If provided output tensor doesn't match expected shape / dtype. + + Note: + Uses CUDA streams for better performance when available in newer `bitsandbytes` + versions (>0.43.3). + """ + if quant_state is None: + return W + + # Get the target device from input tensor W + target_device = W.device + + # Extract quantization state + if not isinstance(quant_state, list): + # New style quant_state class + absmax = quant_state.absmax.to(target_device) + shape = quant_state.shape + dtype = quant_state.dtype + blocksize = quant_state.blocksize + offset = quant_state.offset.to(target_device) + state2 = quant_state.state2 + absmax2 = state2.absmax.to(target_device) + code2 = state2.code.to(target_device) + blocksize2 = state2.blocksize + else: + # Legacy list format + absmax, shape, dtype, blocksize, compressed_stats, _, _ = quant_state + absmax = absmax.to(target_device) + offset, state2 = compressed_stats + offset = offset.to(target_device) + absmax2, code2, blocksize2, _, _, _, _ = state2 + absmax2 = absmax2.to(target_device) + code2 = code2.to(target_device) + + # Setup output tensor on the same device as input + if out is None: + out = torch.empty(shape, dtype=dtype, device=target_device) + else: + assert out.shape == shape and out.dtype == dtype + out = out.to(target_device) + + # Dequantize statistics on the target device + n_elements_absmax: int = absmax.numel() + out_absmax: torch.Tensor = torch.empty( + n_elements_absmax, dtype=torch.float32, device=target_device + ) + ptr_out_absmax: int = get_ptr(out_absmax) + + # Use CUDA stream if available + if HAS_CUDA_STREAM: + global CUDA_STREAM + if CUDA_STREAM is None: + CUDA_STREAM = torch.cuda.current_stream(target_device) + + cdequantize_blockwise_fp32( + get_ptr(code2), + get_ptr(absmax), + get_ptr(absmax2), + ptr_out_absmax, + ctypes.c_int(blocksize2), + ctypes.c_int(n_elements_absmax), + CUDA_STREAM, + ) + else: + cdequantize_blockwise_fp32( + get_ptr(code2), + get_ptr(absmax), + get_ptr(absmax2), + ptr_out_absmax, + ctypes.c_int(blocksize2), + ctypes.c_int(n_elements_absmax), + ) + + out_absmax += offset + + # Choose appropriate dequantization function + fx = ( + cdequantize_blockwise_fp16_nf4 + if dtype == torch.float16 + else cdequantize_blockwise_bf16_nf4 + ) + + # Dequantize weights + if HAS_CUDA_STREAM: + fx( + get_ptr(None), + get_ptr(W), + ptr_out_absmax, + get_ptr(out), + ctypes.c_int(blocksize), + ctypes.c_int(out.numel()), + CUDA_STREAM, + ) + else: + fx( + get_ptr(None), + get_ptr(W), + ptr_out_absmax, + get_ptr(out), + ctypes.c_int(blocksize), + ctypes.c_int(out.numel()), + ) + + # Handle transposed data + is_transposed: bool = W.shape[0] == 1 + return out.t() if is_transposed else out diff --git a/src/axolotl/kernels/swiglu.py b/src/axolotl/kernels/swiglu.py new file mode 100644 index 0000000000..20c6e87a0f --- /dev/null +++ b/src/axolotl/kernels/swiglu.py @@ -0,0 +1,163 @@ +""" +Module for definition of SwiGLU Triton kernels. + +See "GLU Variants Improve Transformer" (https://arxiv.org/abs/2002.05202). + +Credit to `unsloth` (https://unsloth.ai/) for inspiration for this implementation. +""" +import torch +import triton +import triton.language as tl + + +@triton.jit +def _swiglu_fwd_kernel( + gate_ptr, + up_ptr, + out_ptr, + n_elements, + block_size: tl.constexpr, +): + """ + SwiGLU forward kernel. The kernel computes activation in fp32 precision for better + numerical stability, then converts back to original dtype for the final result. + + Args: + gate_ptr: Pointer to gate tensor `[*, hidden_dim]`. + up_ptr: Pointer to up-projection tensor `[*, hidden_dim]`. + out_ptr: Pointer to output tensor `[*, hidden_dim]`. + n_elements: Total number of elements in the input tensors. + block_size: Size of thread blocks for parallel computation. + """ + block_idx = tl.program_id(0) + offsets = block_idx * block_size + tl.arange(0, block_size) + mask = offsets < n_elements + + # Load gate in fp32, keep up in original dtype + gate = tl.load(gate_ptr + offsets, mask=mask, other=0).to(tl.float32) + up = tl.load(up_ptr + offsets, mask=mask, other=0) + + # Compute activation in fp32 then convert back + f = gate * tl.sigmoid(gate) + f = f.to(up.dtype) + result = f * up + + tl.store(out_ptr + offsets, result, mask=mask) + + +@triton.jit +def _swiglu_bwd_kernel( + grad_out_ptr, + gate_ptr, + up_ptr, + n_elements, + block_size: tl.constexpr, +): + """ + SwiGLU backward kernel. Stores gradient results in-place. + + Args: + grad_out_ptr: Pointer to gradient output tensor `[*, hidden_dim]`. + gate_ptr: Pointer to gate tensor `[*, hidden_dim]`. + up_ptr: Pointer to up-projection tensor `[*, hidden_dim]`. + n_elements: Total number of elements in the input tensors. + block_size: Size of thread blocks for parallel computation. + + Note: + After kernel execution, tensors are modified in-place: + - `grad_out_ptr` contains forward output (`h`) + - `gate_ptr` contains gradient w.r.t gate (`grad_gate`) + - `up_ptr` contains gradient w.r.t up (`grad_up`) + """ + block_idx = tl.program_id(0) + offsets = block_idx * block_size + tl.arange(0, block_size) + mask = offsets < n_elements + + # Load values - only convert gate to fp32 + grad_out = tl.load(grad_out_ptr + offsets, mask=mask, other=0) + gate = tl.load(gate_ptr + offsets, mask=mask, other=0).to(tl.float32) + up = tl.load(up_ptr + offsets, mask=mask, other=0) + + # Compute SiLU and forward output + sigmoid_gate = tl.sigmoid(gate) + silu_gate = sigmoid_gate * gate + silu_gate = silu_gate.to(grad_out.dtype) + h = silu_gate * up + + # Compute gradients + grad_up = grad_out * silu_gate # gradient for up is grad_out * SiLU(gate) + + # Compute gate gradient + temp = grad_out * up + grad_gate = temp.to(tl.float32) * sigmoid_gate * (1.0 + gate * (1.0 - sigmoid_gate)) + grad_gate = grad_gate.to(grad_out.dtype) + + # Store results with correct gradient ordering + tl.store(grad_out_ptr + offsets, h, mask=mask) + tl.store(gate_ptr + offsets, grad_gate, mask=mask) # grad wrt gate + tl.store(up_ptr + offsets, grad_up, mask=mask) # grad wrt up + + +# pylint: disable=unnecessary-lambda-assignment +def swiglu_forward(gate: torch.Tensor, up: torch.Tensor) -> torch.Tensor: + """ + SwiGLU forward pass. Computes SwiGLU activation: `x * sigmoid(x) * up`, where + `x` is the gate tensor. + + Args: + gate: Input gate tensor of shape `[batch, seq_len, hidden_dim]`. + up: Up-projection tensor of shape `[batch, seq_len, hidden_dim]`. + + Returns: + Output tensor of shape `[batch, seq_len, hidden_dim]`. + """ + batch, seq_len, hidden_dim = gate.shape + n_elements = gate.numel() + out = torch.empty((batch, seq_len, hidden_dim), dtype=gate.dtype, device="cuda") + + grid = lambda meta: (triton.cdiv(n_elements, meta["block_size"]),) # noqa: E731 + _swiglu_fwd_kernel[grid]( + gate_ptr=gate, + up_ptr=up, + out_ptr=out, + n_elements=n_elements, + block_size=1024, + ) + + return out + + +# pylint: disable=unnecessary-lambda-assignment +def swiglu_backward( + grad_output: torch.Tensor, gate: torch.Tensor, up: torch.Tensor +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + SwiGLU backward pass using in-place operations. + + Args: + grad_output: Gradient of loss with respect to output, shape `[batch, seq_len, hidden_dim]`. + gate: Gate tensor from forward pass, shape `[batch, seq_len, hidden_dim]`. + up: Up-projection tensor from forward pass, shape `[batch, seq_len, hidden_dim]`. + + Returns: + Tuple containing: + - Forward pass output (`h`) + - Gradient with respect to gate (`df`) + - Gradient with respect to up-projection (`de`) + """ + n_elements = grad_output.numel() + + grid = lambda meta: (triton.cdiv(n_elements, meta["block_size"]),) # noqa: E731 + _swiglu_bwd_kernel[grid]( + grad_out_ptr=grad_output, + gate_ptr=gate, + up_ptr=up, + n_elements=n_elements, + block_size=1024, + ) + + # After kernel execution, tensors contain: + # grad_output: h (forward output) + # gate: grad_gate (grad wrt gate) + # up: grad_up (grad wrt up) + return grad_output, gate, up diff --git a/src/axolotl/kernels/utils.py b/src/axolotl/kernels/utils.py new file mode 100644 index 0000000000..59a7e00127 --- /dev/null +++ b/src/axolotl/kernels/utils.py @@ -0,0 +1,11 @@ +"""Utilities for `axolotl.kernels` submodules.""" + +import torch +from packaging.version import Version + +if Version(torch.__version__) < Version("2.4.0"): + torch_amp_custom_fwd = torch.cuda.amp.custom_fwd + torch_amp_custom_bwd = torch.cuda.amp.custom_bwd +else: + torch_amp_custom_fwd = torch.amp.custom_fwd(device_type="cuda") + torch_amp_custom_bwd = torch.amp.custom_bwd(device_type="cuda") diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py new file mode 100644 index 0000000000..d59fd22c93 --- /dev/null +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -0,0 +1,333 @@ +"""Module for patching custom LoRA Triton kernels and `torch.autograd` functions.""" + +import importlib +import inspect +import logging +import types +from typing import Type + +import torch +from accelerate.logging import get_logger +from peft import PeftModelForCausalLM +from torch import nn +from transformers import AutoConfig + +from axolotl.kernels.lora import ( + apply_lora_mlp_geglu, + apply_lora_mlp_swiglu, + apply_lora_o, + apply_lora_qkv, +) +from axolotl.monkeypatch.utils import detab_code +from axolotl.utils.dict import DictDefault + +LOG = get_logger(__name__) + +ORIGINAL_QKV_CODE = """ + query_states = self.q_proj(hidden_states).view(hidden_shape).transpose(1, 2) + key_states = self.k_proj(hidden_states).view(hidden_shape).transpose(1, 2) + value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) +""".lstrip( + "\n" +) + +PATCHED_QKV_CODE = """ + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states = query_states.view(hidden_shape).transpose(1, 2) + key_states = key_states.view(hidden_shape).transpose(1, 2) + value_states = value_states.view(hidden_shape).transpose(1, 2) +""".lstrip( + "\n" +) + +ORIGINAL_O_CODE = """ + attn_output = self.o_proj(attn_output) +""".lstrip( + "\n" +) + +PATCHED_O_CODE = """ + attn_output = self.apply_o(attn_output) +""".lstrip( + "\n" +) + +SUPPORTED_ACTIVATIONS = ["silu", "gelu"] +APPLY_FN_MAPPING = { + "silu": apply_lora_mlp_swiglu, + "gelu": apply_lora_mlp_geglu, +} + + +def original_apply_qkv( + self: nn.Module, hidden_states: torch.Tensor +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Original implementation of QKV projection without optimizations. + + Args: + self: The attention module instance. + hidden_states: Input tensor of shape [batch_size, seq_len, hidden_dim]. + + Returns: + A tuple `(query_states, key_states, value_states)` containing the projected + states for query, key, and value. + """ + query_states = self.q_proj(hidden_states) + key_states = self.k_proj(hidden_states) + value_states = self.v_proj(hidden_states) + + return query_states, key_states, value_states + + +def original_apply_o(self: nn.Module, hidden_states: torch.Tensor) -> torch.Tensor: + """ + Original implementation of output projection without optimizations. + + Args: + self: The attention module instance. + hidden_states: Input tensor of shape `[`batch_size, seq_len, hidden_dim]`. + + Returns: + The output projection result. + """ + attn_output = self.o_proj(hidden_states) + + return attn_output + + +def get_attention_cls_from_config(cfg: DictDefault) -> Type[nn.Module]: + """ + Get the appropriate attention class by inspecting the model config. + Uses dynamic import to support any model architecture that follows + the standard transformers naming convention. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + + Returns: + The appropriate attention class for the model. + + Raises: + ValueError: If `base_model` not specified or attention class cannot be imported + ImportError: If the model module or attention class doesn't exist + """ + if "base_model" not in cfg: + raise ValueError("base_model must be specified in config") + + # Get model config without loading the model + model_config = AutoConfig.from_pretrained(cfg["base_model"]) + model_type = model_config.model_type + + # Special case for model_type = "qwen2" + if model_type == "qwen2": + from transformers.models.qwen2.modeling_qwen2 import Qwen2Attention + + return Qwen2Attention + + try: + # Dynamically import the module and attention class + module_path = f"transformers.models.{model_type}.modeling_{model_type}" + module = __import__( + module_path, fromlist=[f"{model_type.capitalize()}Attention"] + ) + attention_cls = getattr(module, f"{model_type.capitalize()}Attention") + + return attention_cls + except (ImportError, AttributeError) as e: + raise ValueError( + f"Could not import attention class for model_type: {model_type}. " + f"Error: {str(e)}" + ) from e + + +# pylint: disable=protected-access +def patch_self_attn_lora(cfg: DictDefault): + """ + Given an `axolotl` config, this method patches the inferred attention class forward + pass with optimized LoRA implementations. + + It modifies the attention class to use optimized QKV and output projections. The + original implementation is preserved and can be restored if needed. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + + Raises: + AssertionError: If the required code blocks are not found in the attention + implementation. + """ + attention_cls = get_attention_cls_from_config(cfg) + + # Check if already patched + if hasattr(attention_cls, "_original_forward"): + LOG.info(f"{attention_cls.__name__} already patched") + return + + self_attn_forward = inspect.getsource(attention_cls.forward) + attention_cls._original_forward = self_attn_forward + self_attn_forward, _ = detab_code(self_attn_forward) + + assert ORIGINAL_QKV_CODE in self_attn_forward, "Original QKV code not found" + assert ORIGINAL_O_CODE in self_attn_forward, "Original O code not found" + + self_attn_forward = self_attn_forward.replace(ORIGINAL_QKV_CODE, PATCHED_QKV_CODE) + self_attn_forward = self_attn_forward.replace(ORIGINAL_O_CODE, PATCHED_O_CODE) + self_attn_forward = self_attn_forward.replace( + "def forward(", + "def axolotl_attn_forward(", + 1, + ) + + # Load necessary imports + module_name = attention_cls.__module__ + module = importlib.import_module(module_name) + + items_to_import = [] + for item in dir(module): + if item in self_attn_forward: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + f"from {module_name} import ({', '.join(items_to_import)})", + globals(), + ) + exec(self_attn_forward, globals()) # pylint: disable=exec-used # nosec B102 + + LOG.info(f"Patched attention class with LoRA optims: {attention_cls.__name__}") + attention_cls.forward = ( + axolotl_attn_forward # pylint: disable=undefined-variable # noqa: F821 + ) + + +def apply_lora_kernel_patches( + model: PeftModelForCausalLM, cfg: DictDefault +) -> PeftModelForCausalLM: + """ + Applies optimized Triton kernel patches to a PEFT model. + + Patches a PEFT model with optimized implementations for MLP and attention + computations. The optimizations include custom Triton kernels for activation + functions and specialized autograd functions for LoRA computations. + + Args: + model: A PEFT model to be patched with optimized kernels. + cfg: Dictionary mapping `axolotl` config keys to values. + + Returns: + PeftModelForCausalLM: The patched model with optimized kernels. + + Raises: + TypeError: If the provided model is not a `PeftModelForCausalLM`. + NotImplementedError: If the model type is not supported. + AssertionError: If multiple adapters are active (currently unsupported). + + Note: + The optimizations require LoRA adapters with no dropout and no bias terms. The + function will skip patching if these conditions aren't met. + """ + if not isinstance(model, PeftModelForCausalLM): + raise TypeError("Model must be a PeftModelForCausalLM") + + # Get active LoRA adapter config + if hasattr(model, "active_adapters"): + assert ( + len(model.active_adapters) == 1 + ), "Axolotl currently does not support LoRA Triton kernels for multiple adapters" + active_adapter = model.active_adapters[0] + else: + active_adapter = model.active_adapter + lora_config = model.model.peft_config[active_adapter] + + # Only patch if conditions are met + can_patch = lora_config.lora_dropout == 0 and lora_config.bias == "none" + + if not can_patch: + LOG.warning("Cannot patch layers - requires no dropout and no bias") + LOG.warning("Please specify `lora_dropout: 0` in your axolotl config file") + return model + + # This needs to be reset after patching + original_level = LOG.getEffectiveLevel() + LOG.setLevel(logging.INFO) + + # Choose activation based on model type + activation = model.config.hidden_act + if activation not in SUPPORTED_ACTIVATIONS: + raise NotImplementedError(f"Activation {activation} is not supported") + + # Patch each layer + for layer in model.model.model.layers: + # Add QKV, O fallback implementations to start + # These will be overwritten later (if some conditions apply) + layer.self_attn.apply_qkv = types.MethodType( + original_apply_qkv, layer.self_attn + ) + layer.self_attn.apply_o = types.MethodType(original_apply_o, layer.self_attn) + + if cfg.lora_mlp_kernel: + # MLP patching + gate_proj = layer.mlp.gate_proj + up_proj = layer.mlp.up_proj + down_proj = layer.mlp.down_proj + + can_patch_mlp = all( + hasattr(proj, "lora_A") + and getattr(proj, "base_layer", proj).bias is None + and len(getattr(proj, "lora_magnitude_vector", []) or []) == 0 + for proj in (gate_proj, up_proj, down_proj) + ) + + if can_patch_mlp: + apply_fn = APPLY_FN_MAPPING[activation] + layer.mlp.forward = types.MethodType(apply_fn, layer.mlp) + else: + LOG.warning_once( + "Cannot patch some MLP layers - requires LoRA adapters with no bias" + ) + if cfg.lora_qkv_kernel: + # Query, key, value patching + layer_modules = [ + getattr(layer.self_attn, linear_proj) + for linear_proj in ["q_proj", "k_proj", "v_proj"] + ] + can_patch_qkv = all( + hasattr(module, "lora_A") + and getattr(module, "base_layer", module).bias is None + and len(getattr(module, "lora_magnitude_vector", []) or []) == 0 + for module in layer_modules + ) + + if can_patch_qkv: + # Add optimized implementation + layer.self_attn.apply_qkv = types.MethodType( + apply_lora_qkv, layer.self_attn + ) + else: + LOG.warning_once( + "Cannot patch some attention QKV projections - requires LoRA adapters with no bias" + ) + if cfg.lora_o_kernel: + # Output patching + layer_modules = [ + getattr(layer.self_attn, linear_proj) for linear_proj in ["o_proj"] + ] + can_patch_o = all( + hasattr(module, "lora_A") + and getattr(module, "base_layer", module).bias is None + and len(getattr(module, "lora_magnitude_vector", []) or []) == 0 + for module in layer_modules + ) + + if can_patch_o: + layer.self_attn.apply_o = types.MethodType( + apply_lora_o, layer.self_attn + ) + else: + LOG.warning_once( + "Cannot patch some attention output projection - requires LoRA adapters with no bias" + ) + + LOG.setLevel(original_level) + + return model diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 8b5e0074c0..515248fced 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -175,6 +175,7 @@ def terminate_handler(_, __, model_weakref): LOG.info("hang tight... sorting dataset for group_by_length") pretrain_hooks(cfg, trainer) + if cfg.flash_optimum: with torch.backends.cuda.sdp_kernel( # TODO configure these from the YAML w/ sdp_kernel_kwargs: ... @@ -185,6 +186,7 @@ def terminate_handler(_, __, model_weakref): trainer.train(resume_from_checkpoint=resume_from_checkpoint) else: trainer.train(resume_from_checkpoint=resume_from_checkpoint) + post_train_hooks(cfg, trainer) LOG.info(f"Training Completed!!! Saving pre-trained model to {cfg.output_dir}") diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 1e7a6aa8b7..38204d02d2 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -1,7 +1,4 @@ -""" -Module for pydantic models for configuration -""" - +"""Module with Pydantic models for configuration.""" # pylint: disable=too-many-lines import logging @@ -810,6 +807,10 @@ class Config: unsloth_rms_norm: Optional[bool] = None unsloth_rope: Optional[bool] = None + lora_mlp_kernel: Optional[bool] = None + lora_qkv_kernel: Optional[bool] = None + lora_o_kernel: Optional[bool] = None + deepspeed: Optional[Union[str, Dict[str, Any]]] = None fsdp: Optional[List[str]] = None fsdp_config: Optional[Dict[str, Any]] = None @@ -1534,12 +1535,42 @@ def check_qlora_unsloth(cls, data): or data.get("unsloth_lora_qkv") or data.get("unsloth_lora_o") ): - if data.get("adapter") == "lora" or data.get("load_in_8bit"): + if data.get("adapter") == "lora" and data.get("load_in_8bit"): raise ValueError( "unsloth_lora_mlp, unsloth_lora_qkv, and unsloth_lora_o are not compatible with 8-bit LoRA" ) return data + @model_validator(mode="before") + @classmethod + def check_lora_8bit(cls, data): + if ( + data.get("lora_mlp_kernel") + or data.get("lora_qkv_kernel") + or data.get("lora_o_kernel") + ): + if data.get("adapter") == "lora" and data.get("load_in_8bit"): + raise ValueError( + "lora_mlp_kernel, lora_mlp_kernel, and lora_mlp_kernel are not compatible with 8-bit LoRA" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_lora_axolotl_unsloth(cls, data): + is_lora_kernel = any( + data.get(k) for k in ["lora_mlp_kernel", "lora_qkv_kernel", "lora_o_kernel"] + ) + is_unsloth_lora = any( + data.get(k) + for k in ["unsloth_lora_mlp", "unsloth_lora_qkv", "unsloth_lora_o"] + ) + if is_lora_kernel and is_unsloth_lora: + raise ValueError( + "both lora_mlp_kernel and unsloth_lora_mlp cannot be true (similarly for lora_qkv_kernel, lora_o_kernel)" + ) + return data + @model_validator(mode="before") @classmethod def check_torch_compile_deepspeed(cls, data): @@ -1672,6 +1703,29 @@ def check_multigpu_unsloth(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_multigpu_lora_kernels(cls, data): + if ( + data.get("lora_mlp_kernel") + or data.get("lora_qkv_kernel") + or data.get("lora_o_kernel") + ): + capabilities = data.get("capabilities") + is_fsdp = data.get("fsdp") is not None + is_deepspeed = data.get("deepspeed") is not None + + if capabilities and capabilities.get("n_gpu", 0) > 1: + if is_fsdp: + raise ValueError( + "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with FSDP." + ) + if is_deepspeed: + raise ValueError( + "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with DeepSpeed." + ) + return data + @model_validator(mode="before") @classmethod def check_adopt_torch_version(cls, data): diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index a96ecb0cf6..377f086052 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -414,6 +414,7 @@ def apply_patches(self) -> None: has_remote_code = "AutoModelForCausalLM" in auto_map_config else: has_remote_code = False + if has_remote_code and self.cfg.trust_remote_code is False: # if explicitly set in the YAML, we should prefer that, for example if explicitly disabled has_remote_code = self.cfg.trust_remote_code @@ -425,10 +426,6 @@ def apply_patches(self) -> None: if self.cfg.is_llama_derived_model: self.patch_loss_llama() - if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: - from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora - - patch_self_attn_lora() elif self.cfg.is_llama_derived_model: self.patch_llama_derived_model() @@ -442,6 +439,11 @@ def apply_patches(self) -> None: patch_mistral_cross_entropy() + if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: + from axolotl.monkeypatch.lora_kernels import patch_self_attn_lora + + patch_self_attn_lora(self.cfg) + def patch_attention(self) -> None: if hasattr(self.model_config, "model_type"): if self.model_config.model_type == "mllama" and self.cfg.flash_attention: @@ -472,9 +474,7 @@ def has_flash_attn(self) -> bool: return importlib.util.find_spec("flash_attn") is not None def patch_loss_llama(self) -> None: - """ - Patch loss functions - """ + """Patch loss functions and other optimizations""" if self.has_flash_attn: from axolotl.monkeypatch.llama_attn_hijack_flash import ( patch_fa_llama_cross_entropy, @@ -494,15 +494,14 @@ def patch_loss_llama(self) -> None: from axolotl.monkeypatch.unsloth_ import patch_unsloth_layernorm patch_unsloth_layernorm() + if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora patch_self_attn_lora() def patch_llama_derived_model(self) -> None: - """ - Modify all llama derived models in one block - """ + """Modify all llama derived models in one block""" self.patch_loss_llama() if self.cfg.flash_attention: @@ -1013,7 +1012,8 @@ def convert_embedding_modules_dtype( if hasattr(module, "weight"): module.to(dist_dtype) - def apply_lora_patch(self) -> None: + # TODO: Deprecate this. + def apply_unsloth_lora_patch(self) -> None: if self.cfg.unsloth_lora_mlp: from axolotl.monkeypatch.unsloth_ import integrate_lora_mlp_patch @@ -1027,6 +1027,16 @@ def apply_lora_patch(self) -> None: integrate_rope_embeddings() + def apply_lora_patch(self) -> None: + if ( + self.cfg.lora_mlp_kernel + or self.cfg.lora_qkv_kernel + or self.cfg.lora_o_kernel + ): + from axolotl.monkeypatch.lora_kernels import apply_lora_kernel_patches + + apply_lora_kernel_patches(self.model, self.cfg) + def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: self.apply_patches() self.set_auto_model_loader() @@ -1171,6 +1181,7 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: if self.cfg.adapter is not None: log_gpu_memory_usage(LOG, "after adapters", self.model.device) + self.apply_unsloth_lora_patch() self.apply_lora_patch() for _ in range(3): diff --git a/tests/e2e/kernels/test_geglu.py b/tests/e2e/kernels/test_geglu.py new file mode 100644 index 0000000000..c720bbce7b --- /dev/null +++ b/tests/e2e/kernels/test_geglu.py @@ -0,0 +1,76 @@ +"""Tests for GEGLU activation function Triton kernels.""" +# pylint: disable=duplicate-code + +import torch +import torch.nn.functional as F + +from axolotl.kernels.geglu import geglu_backward, geglu_forward + + +def test_geglu_forward_shape(): + """Test that GEGLU forward pass preserves expected shapes.""" + batch, seq_len, hidden_dim = 2, 3, 64 + gate = torch.randn(batch, seq_len, hidden_dim, device="cuda") + up = torch.randn(batch, seq_len, hidden_dim, device="cuda") + + out = geglu_forward(gate, up) + assert out.shape == (batch, seq_len, hidden_dim) + assert out.dtype == gate.dtype + assert out.device == gate.device + + +def test_geglu_forward_values(): + """Test GEGLU forward pass matches PyTorch reference implementation.""" + gate = torch.randn(2, 3, 64, device="cuda") + up = torch.randn(2, 3, 64, device="cuda") + + # Custom implementation + triton_out = geglu_forward(gate.clone(), up.clone()) + + # PyTorch reference + torch_out = F.gelu(gate) * up + + assert torch.allclose(triton_out, torch_out, rtol=1e-3) + + +def test_geglu_backward(): + """Test GEGLU backward pass matches PyTorch autograd.""" + gate = torch.randn(2, 3, 64, device="cuda", requires_grad=True) + up = torch.randn(2, 3, 64, device="cuda", requires_grad=True) + grad_output = torch.randn(2, 3, 64, device="cuda") + + # PyTorch reference - compute intermediates + gelu_gate = F.gelu(gate) + torch_out = gelu_gate * up + torch_out.backward(grad_output) + + # Custom backward pass + gate_clone = gate.clone().detach() + up_clone = up.clone().detach() + grad_output_clone = grad_output.clone() + + h, grad_gate, grad_up = geglu_backward(grad_output_clone, gate_clone, up_clone) + + # Compare outputs and gradients + assert torch.allclose(h, torch_out, rtol=1e-3) + assert torch.allclose(grad_gate, gate.grad, rtol=1e-3) + assert torch.allclose(grad_up, up.grad, rtol=1e-3) + + +def test_geglu_inplace_preservation(): + """Test that GEGLU backward doesn't modify original tensors unexpectedly.""" + gate = torch.randn(2, 3, 64, device="cuda") + up = torch.randn(2, 3, 64, device="cuda") + grad_output = torch.randn(2, 3, 64, device="cuda") + + gate_copy = gate.clone() + up_copy = up.clone() + grad_copy = grad_output.clone() + + geglu_backward(grad_output, gate, up) + + assert not torch.equal(gate, gate_copy), "Gate should be modified in-place" + assert not torch.equal(up, up_copy), "Up should be modified in-place" + assert not torch.equal( + grad_output, grad_copy + ), "Grad output should be modified in-place" diff --git a/tests/e2e/kernels/test_lora.py b/tests/e2e/kernels/test_lora.py new file mode 100644 index 0000000000..c8becf2da8 --- /dev/null +++ b/tests/e2e/kernels/test_lora.py @@ -0,0 +1,531 @@ +"""Tests for LoRA custom autograd.""" +# pylint: disable=invalid-name,redefined-outer-name + +import pytest +import torch +from bitsandbytes.functional import QuantState +from torch import nn + +from axolotl.kernels.geglu import geglu_backward, geglu_forward +from axolotl.kernels.lora import ( + LoRA_MLP, + LoRA_O, + LoRA_QKV, + apply_lora_mlp_geglu, + apply_lora_mlp_swiglu, + get_lora_parameters, + matmul_lora, +) +from axolotl.kernels.swiglu import swiglu_backward, swiglu_forward + + +@pytest.fixture +def mock_quantstate(): + """Creates a mock QuantState for testing""" + shape = (64, 64) + n_blocks = shape[0] # Assuming blockwise quantization along first dimension + + # Create nested state first + nested_state = QuantState( + absmax=torch.ones(n_blocks, device="cuda"), # One value per block + shape=shape, + code=torch.randint(0, 15, shape, device="cuda"), # NF4 range is 0-15 + dtype=torch.float16, + blocksize=64, + quant_type="nf4", + offset=None, + state2=None, + ) + + # Create main state with nested state + return QuantState( + absmax=torch.ones(n_blocks, device="cuda"), + shape=shape, + code=torch.randint(0, 15, shape, device="cuda"), + dtype=torch.float16, + blocksize=64, + quant_type="nf4", + offset=torch.zeros(n_blocks, dtype=torch.int32, device="cuda"), + state2=nested_state, + ) + + +@pytest.fixture +def sample_tensors(): + """Creates sample tensors for testing""" + torch.manual_seed(42) + batch_size, seq_len, hidden_dim = 2, 3, 64 + rank = 8 + out_dim = hidden_dim + + return { + "X": torch.randn( + batch_size, seq_len, hidden_dim, device="cuda", dtype=torch.float16 + ), + "W": torch.randn(out_dim, hidden_dim, device="cuda", dtype=torch.float16), + "scale": 0.5, + "shapes": { + "batch": batch_size, + "seq": seq_len, + "hidden": hidden_dim, + "out": out_dim, + "rank": rank, + }, + } + + +@pytest.fixture +def mock_proj(): + """Creates a mock projection module for testing.""" + + class MockProj(nn.Module): + """Mock projection class.""" + + def __init__(self, in_features=64, out_features=128, rank=8): + super().__init__() + self.base_layer = nn.Linear(in_features, out_features) + self.base_layer.to("cuda") + self.lora_A = nn.ModuleDict( + {"default": nn.Linear(in_features, rank, bias=False).to("cuda")} + ) + self.lora_B = nn.ModuleDict( + {"default": nn.Linear(rank, out_features, bias=False).to("cuda")} + ) + self.scaling = {"default": 0.5} + self.active_adapter = "default" + self.disable_adapters = False + self.merged = False + + return MockProj() + + +def test_get_lora_parameters(mock_proj): + """Tests get_lora_parameters function""" + # Test with LoRA enabled + W, _, A, B, s = get_lora_parameters(mock_proj) + + assert isinstance(W, torch.Tensor) + assert W.shape == (128, 64) + assert A.shape == (8, 64) + assert B.shape == (128, 8) + assert s == 0.5 + + # Test with LoRA disabled + mock_proj.disable_adapters = True + W, _, A, B, s = get_lora_parameters(mock_proj) + assert A is None and B is None and s is None + + # Test with merged state + mock_proj.disable_adapters = False + mock_proj.merged = True + W, _, A, B, s = get_lora_parameters(mock_proj) + assert A is None and B is None and s is None + + +def test_matmul_lora(sample_tensors): + """Tests matmul_lora function""" + X = sample_tensors["X"] + W = sample_tensors["W"] + scale = sample_tensors["scale"] + + shapes = sample_tensors["shapes"] + hidden_dim = shapes["hidden"] + out_dim = shapes["out"] + rank = shapes["rank"] + + A = torch.randn(rank, hidden_dim, device="cuda", dtype=torch.float16) + B = torch.randn(out_dim, rank, device="cuda", dtype=torch.float16) + + # Test base matmul + out1 = matmul_lora(X, W, None, None, None, None) + expected1 = torch.matmul(X, W.t()) + assert torch.allclose(out1, expected1, rtol=1e-3) + + # Test with LoRA + out2 = matmul_lora(X, W, None, A, B, scale) + lora_term = scale * torch.matmul(torch.matmul(X, A.t()), B.t()) + expected2 = expected1 + lora_term + assert torch.allclose(out2, expected2, rtol=1e-3) + + # Test 3D input reshaping + X_3d = X.clone() + out3 = matmul_lora(X_3d, W, None, A, B, scale) + assert out3.shape == (X.shape[0], X.shape[1], W.shape[0]) + + +@pytest.mark.parametrize( + "activation_forward,activation_backward", + [(swiglu_forward, swiglu_backward), (geglu_forward, geglu_backward)], +) +def test_lora_mlp_direct(sample_tensors, activation_forward, activation_backward): + """Tests LoRA_MLP directly with different activation functions""" + X = sample_tensors["X"] + shapes = sample_tensors["shapes"] + hidden_dim = shapes["hidden"] + out_dim = shapes["out"] + + # Create linear layers + gate_proj = nn.Linear(hidden_dim, out_dim).to(device="cuda", dtype=torch.float16) + up_proj = nn.Linear(hidden_dim, out_dim).to(device="cuda", dtype=torch.float16) + down_proj = nn.Linear(out_dim, hidden_dim).to(device="cuda", dtype=torch.float16) + + # Test SwiGLU path + X.requires_grad = True + output = LoRA_MLP.apply( + X, + gate_proj.weight, + None, # gate_quant + None, # gate_A + None, # gate_B + None, # gate_scale + up_proj.weight, + None, # up_quant + None, # up_A + None, # up_B + None, # up_scale + down_proj.weight, + None, # down_quant + None, # down_A + None, # down_B + None, # down_scale + activation_forward, + activation_backward, + True, # inplace + ) + + assert output.shape == X.shape + assert not torch.isnan(output).any() + + # Test backward pass + loss = output.sum() + loss.backward() + assert X.grad is not None + assert not torch.isnan(X.grad).any() + + +@pytest.mark.parametrize( + "activation_forward,activation_backward", + [(swiglu_forward, swiglu_backward), (geglu_forward, geglu_backward)], +) +def test_lora_mlp_with_adapters( + sample_tensors, activation_forward, activation_backward +): + """Tests LoRA_MLP with LoRA adapters""" + X = sample_tensors["X"] + shapes = sample_tensors["shapes"] + hidden_dim = shapes["hidden"] + out_dim = shapes["out"] + rank = shapes["rank"] + + # Create LoRA components + gate_A = torch.randn(rank, hidden_dim, device="cuda", dtype=torch.float16) + gate_B = torch.randn(out_dim, rank, device="cuda", dtype=torch.float16) + up_A = torch.randn(rank, hidden_dim, device="cuda", dtype=torch.float16) + up_B = torch.randn(out_dim, rank, device="cuda", dtype=torch.float16) + down_A = torch.randn(rank, out_dim, device="cuda", dtype=torch.float16) + down_B = torch.randn(hidden_dim, rank, device="cuda", dtype=torch.float16) + scale = 0.5 + + gate_proj = nn.Linear(hidden_dim, out_dim).to(device="cuda", dtype=torch.float16) + up_proj = nn.Linear(hidden_dim, out_dim).to(device="cuda", dtype=torch.float16) + down_proj = nn.Linear(out_dim, hidden_dim).to(device="cuda", dtype=torch.float16) + + X.requires_grad = True + gate_A.requires_grad = True + gate_B.requires_grad = True + up_A.requires_grad = True + up_B.requires_grad = True + down_A.requires_grad = True + down_B.requires_grad = True + + # Forward pass with adapters + output = LoRA_MLP.apply( + X, + gate_proj.weight, + None, + gate_A, + gate_B, + scale, + up_proj.weight, + None, + up_A, + up_B, + scale, + down_proj.weight, + None, + down_A, + down_B, + scale, + activation_forward, + activation_backward, + True, + ) + + assert output.shape == X.shape + assert not torch.isnan(output).any() + + # Test backward pass + loss = output.sum() + loss.backward() + + # Check all gradients + assert X.grad is not None + assert gate_A.grad is not None + assert gate_B.grad is not None + assert up_A.grad is not None + assert up_B.grad is not None + assert down_A.grad is not None + assert down_B.grad is not None + + assert not torch.isnan(X.grad).any() + assert not torch.isnan(gate_A.grad).any() + assert not torch.isnan(gate_B.grad).any() + assert not torch.isnan(up_A.grad).any() + assert not torch.isnan(up_B.grad).any() + assert not torch.isnan(down_A.grad).any() + assert not torch.isnan(down_B.grad).any() + + +def test_lora_qkv(sample_tensors): + """Tests LoRA QKV implementation with and without adapters""" + X = sample_tensors["X"] + shapes = sample_tensors["shapes"] + hidden_dim = shapes["hidden"] + rank = shapes["rank"] + + # Create base weights + q_weight = torch.randn(hidden_dim, hidden_dim, device="cuda", dtype=torch.float16) + k_weight = torch.randn(hidden_dim, hidden_dim, device="cuda", dtype=torch.float16) + v_weight = torch.randn(hidden_dim, hidden_dim, device="cuda", dtype=torch.float16) + + # Create LoRA matrices + q_A = torch.randn( + rank, hidden_dim, device="cuda", dtype=torch.float16, requires_grad=True + ) + q_B = torch.randn( + hidden_dim, rank, device="cuda", dtype=torch.float16, requires_grad=True + ) + k_A = torch.randn( + rank, hidden_dim, device="cuda", dtype=torch.float16, requires_grad=True + ) + k_B = torch.randn( + hidden_dim, rank, device="cuda", dtype=torch.float16, requires_grad=True + ) + v_A = torch.randn( + rank, hidden_dim, device="cuda", dtype=torch.float16, requires_grad=True + ) + v_B = torch.randn( + hidden_dim, rank, device="cuda", dtype=torch.float16, requires_grad=True + ) + scale = 0.5 + + X.requires_grad = True + + # Test without LoRA adapters + Q1, K1, V1 = LoRA_QKV.apply( + X, + q_weight, + None, + None, + None, + None, + k_weight, + None, + None, + None, + None, + v_weight, + None, + None, + None, + None, + True, + ) + + assert Q1.shape == K1.shape == V1.shape == X.shape + loss1 = (Q1 + K1 + V1).sum() + loss1.backward() + assert X.grad is not None + + # Clear gradients + X.grad = None + + # Test with LoRA adapters + Q2, K2, V2 = LoRA_QKV.apply( + X, + q_weight, + None, + q_A, + q_B, + scale, + k_weight, + None, + k_A, + k_B, + scale, + v_weight, + None, + v_A, + v_B, + scale, + True, + ) + + assert Q2.shape == K2.shape == V2.shape == X.shape + loss2 = (Q2 + K2 + V2).sum() + loss2.backward() + + # Check gradients + assert X.grad is not None + assert q_A.grad is not None + assert q_B.grad is not None + assert k_A.grad is not None + assert k_B.grad is not None + assert v_A.grad is not None + assert v_B.grad is not None + + # Check for NaN values + assert not torch.isnan(X.grad).any() + assert not torch.isnan(q_A.grad).any() + assert not torch.isnan(q_B.grad).any() + assert not torch.isnan(k_A.grad).any() + assert not torch.isnan(k_B.grad).any() + assert not torch.isnan(v_A.grad).any() + assert not torch.isnan(v_B.grad).any() + + +def test_lora_o(sample_tensors): + """Tests LoRA output projection""" + X = sample_tensors["X"] + W = sample_tensors["W"] + scale = sample_tensors["scale"] + + shapes = sample_tensors["shapes"] + hidden_dim = shapes["hidden"] + out_dim = shapes["out"] + rank = shapes["rank"] + + A = torch.randn(rank, hidden_dim, device="cuda", dtype=torch.float16) + B = torch.randn(out_dim, rank, device="cuda", dtype=torch.float16) + + # Test forward pass + X.requires_grad = True + output = LoRA_O.apply(X, W, None, A, B, scale) + + assert output.shape == (X.shape[0], X.shape[1], W.shape[0]) + + # Test backward pass + loss = output.sum() + loss.backward() + assert X.grad is not None + + +def test_with_quantization(sample_tensors, mock_quantstate): + """Tests LoRA with quantized weights""" + X = sample_tensors["X"] # [batch, seq, hidden] + W = sample_tensors["W"] # [out, hidden] + scale = 0.5 + + shapes = sample_tensors["shapes"] + hidden_dim = shapes["hidden"] + out_dim = shapes["out"] + rank = shapes["rank"] + + A = torch.randn(rank, hidden_dim, device="cuda", dtype=torch.float16) + B = torch.randn(out_dim, rank, device="cuda", dtype=torch.float16) + + # Test matmul with quantization + out = matmul_lora(X, W, mock_quantstate, A, B, scale) + assert out.shape == (X.shape[0], X.shape[1], W.shape[0]) + assert not torch.isnan(out).any() + + # Test with different batch sizes + X2 = torch.randn(4, 6, hidden_dim, device="cuda", dtype=torch.float16) + out2 = matmul_lora(X2, W, mock_quantstate, A, B, scale) + assert out2.shape == (4, 6, W.shape[0]) + assert not torch.isnan(out2).any() + + +@pytest.mark.parametrize( + "batch,seq,hidden,rank,out", + [ + (1, 1, 32, 4, 64), + (2, 3, 64, 8, 128), + (4, 5, 128, 16, 256), + ], +) +def test_shapes_and_dimensions(batch, seq, hidden, rank, out): + """Tests various input shapes and dimensions""" + X = torch.randn(batch, seq, hidden, device="cuda", dtype=torch.float16) + W = torch.randn(out, hidden, device="cuda", dtype=torch.float16) + A = torch.randn(rank, hidden, device="cuda", dtype=torch.float16) + B = torch.randn(out, rank, device="cuda", dtype=torch.float16) + scale = 0.5 + + result = matmul_lora(X, W, None, A, B, scale) + assert result.shape == (batch, seq, out) + + +def test_gradient_flow(sample_tensors): + """Tests gradient flow through LoRA layers""" + X = sample_tensors["X"].clone() + W = sample_tensors["W"].clone() + scale = sample_tensors["scale"] + + shapes = sample_tensors["shapes"] + hidden_dim = shapes["hidden"] + out_dim = shapes["out"] + rank = shapes["rank"] + + A = torch.randn(rank, hidden_dim, device="cuda", dtype=torch.float16) + B = torch.randn(out_dim, rank, device="cuda", dtype=torch.float16) + + X.requires_grad = True + A.requires_grad = True + B.requires_grad = True + + # Forward pass + out = matmul_lora(X, W, None, A, B, scale) + loss = out.sum() + + # Backward pass + loss.backward() + + assert X.grad is not None + assert A.grad is not None + assert B.grad is not None + assert not torch.isnan(X.grad).any() + assert not torch.isnan(A.grad).any() + assert not torch.isnan(B.grad).any() + + +@pytest.mark.parametrize( + "apply_function", + [apply_lora_mlp_swiglu, apply_lora_mlp_geglu], +) +def test_inplace_operations(sample_tensors, apply_function): + """Tests inplace operation behavior""" + X = sample_tensors["X"] + shapes = sample_tensors["shapes"] + + # Create MLP with both inplace=True and inplace=False + mlp = type( + "MLPModule", + (), + { + "gate_proj": nn.Linear(shapes["hidden"], shapes["out"]).to( + device="cuda", dtype=torch.float16 + ), + "up_proj": nn.Linear(shapes["hidden"], shapes["out"]).to( + device="cuda", dtype=torch.float16 + ), + "down_proj": nn.Linear(shapes["out"], shapes["hidden"]).to( + device="cuda", dtype=torch.float16 + ), + }, + ) + + out1 = apply_function(mlp, X.clone(), inplace=True) + out2 = apply_function(mlp, X.clone(), inplace=False) + + assert torch.allclose(out1, out2, rtol=1e-3) diff --git a/tests/e2e/kernels/test_quantize.py b/tests/e2e/kernels/test_quantize.py new file mode 100644 index 0000000000..e4beb846e9 --- /dev/null +++ b/tests/e2e/kernels/test_quantize.py @@ -0,0 +1,103 @@ +"""Tests for quantization utility functions.""" +# pylint: disable=invalid-name + +import torch +from bitsandbytes.functional import QuantState + +from axolotl.kernels.quantize import dequantize + + +def test_dequantize_null_state(): + """Test that dequantize returns input unchanged when quant_state is None""" + W = torch.randn(32, 32) + assert torch.equal(dequantize(W, None), W) + + +def test_dequantize_shape_preservation(): + """Test that dequantization preserves expected shapes""" + shape = (32, 32) + W = torch.randn(shape, device="cuda") + + quant_state = QuantState( + absmax=torch.ones(shape[0], device="cuda"), + shape=shape, + code=torch.randint(0, 15, shape, device="cuda"), + dtype=torch.float16, + blocksize=32, + quant_type="nf4", + offset=torch.zeros(shape[0], dtype=torch.int32, device="cuda"), + state2=QuantState( + absmax=torch.ones(shape[0], device="cuda"), + shape=shape, + code=torch.randint(0, 15, shape, device="cuda"), + dtype=torch.float16, + blocksize=32, + quant_type="nf4", + offset=None, + state2=None, + ), + ) + + result = dequantize(W, quant_state) + assert result.shape == shape + assert result.dtype == torch.float16 + assert result.device == W.device + + +def test_dequantize_transposed(): + """Test that transposed input produces transposed output""" + shape = (32, 32) + W = torch.randn(1, shape[1], device="cuda") # Transposed input + + quant_state = QuantState( + absmax=torch.ones(1), + shape=shape, + code=torch.randint(0, 15, shape), + dtype=torch.float16, + blocksize=32, + quant_type="nf4", + offset=torch.zeros(1, dtype=torch.int32), + state2=QuantState( + absmax=torch.ones(1), + shape=shape, + code=torch.randint(0, 15, shape), + dtype=torch.float16, + blocksize=32, + quant_type="nf4", + offset=None, + state2=None, + ), + ) + + result = dequantize(W, quant_state) + assert result.shape[0] == shape[0] + + +def test_dequantize_output_tensor(): + """Test dequantization with provided output tensor""" + shape = (32, 32) + W = torch.randn(shape, device="cuda") + out = torch.empty(shape, dtype=torch.float16, device="cuda") + + quant_state = QuantState( + absmax=torch.ones(shape[0]), + shape=shape, + code=torch.randint(0, 15, shape), + dtype=torch.float16, + blocksize=32, + quant_type="nf4", + offset=torch.zeros(shape[0], dtype=torch.int32), + state2=QuantState( + absmax=torch.ones(shape[0]), + shape=shape, + code=torch.randint(0, 15, shape), + dtype=torch.float16, + blocksize=32, + quant_type="nf4", + offset=None, + state2=None, + ), + ) + + result = dequantize(W, quant_state, out=out) + assert result is out diff --git a/tests/e2e/kernels/test_swiglu.py b/tests/e2e/kernels/test_swiglu.py new file mode 100644 index 0000000000..3717402de1 --- /dev/null +++ b/tests/e2e/kernels/test_swiglu.py @@ -0,0 +1,78 @@ +"""Tests for SwiGLU activation function Triton kernels.""" +# pylint: disable=duplicate-code + +import torch +import torch.nn.functional as F + +from axolotl.kernels.swiglu import swiglu_backward, swiglu_forward + + +def test_swiglu_forward_shape(): + """Test that SwiGLU forward pass preserves expected shapes""" + batch, seq_len, hidden_dim = 2, 3, 64 + gate = torch.randn(batch, seq_len, hidden_dim, device="cuda") + up = torch.randn(batch, seq_len, hidden_dim, device="cuda") + + out = swiglu_forward(gate, up) + assert out.shape == (batch, seq_len, hidden_dim) + assert out.dtype == gate.dtype + assert out.device == gate.device + + +def test_swiglu_forward_values(): + """Test SwiGLU forward pass matches PyTorch reference implementation""" + gate = torch.randn(2, 3, 64, device="cuda") + up = torch.randn(2, 3, 64, device="cuda") + + # Custom implementation + triton_out = swiglu_forward(gate.clone(), up.clone()) + + # PyTorch reference + torch_out = F.silu(gate) * up + + assert torch.allclose(triton_out, torch_out, rtol=1e-3) + + +def test_swiglu_backward(): + """Test SwiGLU backward pass matches PyTorch autograd""" + gate = torch.randn(2, 3, 64, device="cuda", requires_grad=True) + up = torch.randn(2, 3, 64, device="cuda", requires_grad=True) + grad_output = torch.randn(2, 3, 64, device="cuda") + + # PyTorch reference - compute intermediates + silu_gate = F.silu(gate) + torch_out = silu_gate * up + torch_out.backward(grad_output) + + # Custom backward pass + gate_clone = gate.clone().detach() + up_clone = up.clone().detach() + grad_output_clone = grad_output.clone() + + h, our_grad_gate, our_grad_up = swiglu_backward( + grad_output_clone, gate_clone, up_clone + ) + + # Compare outputs and gradients + assert torch.allclose(h, torch_out, rtol=1e-3) + assert torch.allclose(our_grad_gate, gate.grad, rtol=1e-3) + assert torch.allclose(our_grad_up, up.grad, rtol=1e-3) + + +def test_swiglu_inplace_preservation(): + """Test that SwiGLU backward doesn't modify original tensors unexpectedly""" + gate = torch.randn(2, 3, 64, device="cuda") + up = torch.randn(2, 3, 64, device="cuda") + grad_output = torch.randn(2, 3, 64, device="cuda") + + gate_copy = gate.clone() + up_copy = up.clone() + grad_copy = grad_output.clone() + + swiglu_backward(grad_output, gate, up) + + assert not torch.equal(gate, gate_copy), "Gate should be modified in-place" + assert not torch.equal(up, up_copy), "Up should be modified in-place" + assert not torch.equal( + grad_output, grad_copy + ), "Grad output should be modified in-place" diff --git a/tests/e2e/patched/lora_kernels/__init__.py b/tests/e2e/patched/lora_kernels/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py new file mode 100644 index 0000000000..4e33733673 --- /dev/null +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -0,0 +1,414 @@ +"""Integration tests for LoRA activation and attention kernels.""" +# pylint: disable=redefined-outer-name + +import pytest +import torch +from accelerate.state import PartialState +from peft import PeftModelForCausalLM, get_peft_config +from transformers import AutoModelForCausalLM, LlamaForCausalLM +from transformers.models.llama.configuration_llama import LlamaConfig +from transformers.models.llama.modeling_llama import LlamaAttention + +from axolotl.kernels.lora import ( + apply_lora_mlp_geglu, + apply_lora_mlp_swiglu, + apply_lora_o, + apply_lora_qkv, +) +from axolotl.monkeypatch.lora_kernels import ( + apply_lora_kernel_patches, + patch_self_attn_lora, +) +from axolotl.utils.dict import DictDefault + +MODEL_CONFIGS = [ + { + "name": "openaccess-ai-collective/tiny-mistral", + "expected_activation": apply_lora_mlp_swiglu, + "dtype": torch.float16, + }, + { + "name": "Qwen/Qwen2-7B", + "expected_activation": apply_lora_mlp_swiglu, + "dtype": torch.float16, + }, + { + "name": "HuggingFaceTB/SmolLM2-135M", + "expected_activation": apply_lora_mlp_swiglu, + "dtype": torch.float32, + }, + { + "name": "mhenrichsen/gemma-2b", + "expected_activation": apply_lora_mlp_geglu, + "dtype": torch.float16, + }, +] + + +@pytest.fixture(autouse=True) +def init_accelerate(): + """Initialize Accelerate state before tests.""" + _ = PartialState() + + +@pytest.fixture +def small_llama_model(): + """Create a small LLaMA model for testing.""" + config = { + "vocab_size": 100, + "hidden_size": 128, + "intermediate_size": 256, + "num_hidden_layers": 2, + "num_attention_heads": 4, + } + + return LlamaForCausalLM(LlamaConfig(**config)) + + +def test_attention_patching_integration(): + """Test attention patching in integration context.""" + cfg = {"base_model": "TinyLlama/TinyLlama-1.1B-Chat-v1.0"} + + # Store the original implementation + original_forward = getattr(LlamaAttention, "forward") + + # Apply patch + patch_self_attn_lora(cfg) + + # Get the new forward method + patched_forward = LlamaAttention.forward + + # Check the forward method was replaced + assert original_forward is not patched_forward + assert patched_forward.__name__ == "axolotl_attn_forward" + + # Check original implementation was stored + assert hasattr(LlamaAttention, "_original_forward") + + # Clean up + setattr(LlamaAttention, "forward", original_forward) + delattr(LlamaAttention, "_original_forward") + + +def test_swiglu_mlp_integration(small_llama_model): + """Test SwiGLU activation in LoRA MLP context.""" + peft_config = get_peft_config( + { + "peft_type": "LORA", + "task_type": "CAUSAL_LM", + "r": 8, + "lora_alpha": 16, + "target_modules": ["gate_proj", "up_proj", "down_proj"], + "lora_dropout": 0, + "bias": "none", + } + ) + model = PeftModelForCausalLM(small_llama_model, peft_config).to("cuda") + cfg = DictDefault({"lora_mlp_kernel": True}) + + # Apply patches + patched_model = apply_lora_kernel_patches(model, cfg) + + # Verify patches + layer = patched_model.model.model.layers[0] + assert layer.mlp.forward.__func__ is apply_lora_mlp_swiglu + + # Test forward pass + batch_size, seq_len = 2, 10 + hidden_states = torch.randn( + batch_size, seq_len, model.config.hidden_size, device=model.device + ) + position_ids = ( + torch.arange(seq_len, device=model.device).unsqueeze(0).expand(batch_size, -1) + ) + cos, sin = model.model.model.rotary_emb(hidden_states, position_ids) + + inputs = { + "hidden_states": hidden_states, + "attention_mask": None, + "position_embeddings": (cos, sin), + "output_attentions": False, + "use_cache": False, + "past_key_value": None, + } + + # Compare outputs + with torch.no_grad(): + original_output = model.model.model.layers[0](**inputs)[0] + patched_output = layer(**inputs)[0] + + assert torch.allclose(original_output, patched_output, rtol=1e-4) + + +def test_geglu_model_integration(): + """Test GeGLU activation with Gemma model.""" + model = AutoModelForCausalLM.from_pretrained( + "mhenrichsen/gemma-2b", torch_dtype=torch.float16, device_map="cuda" + ) + peft_config = get_peft_config( + { + "peft_type": "LORA", + "task_type": "CAUSAL_LM", + "r": 8, + "lora_alpha": 16, + "target_modules": ["gate_proj", "up_proj", "down_proj"], + "lora_dropout": 0, + "bias": "none", + } + ) + model = PeftModelForCausalLM(model, peft_config) + + cfg = DictDefault({"lora_mlp_kernel": True}) + patched_model = apply_lora_kernel_patches(model, cfg) + + # Verify patches + layer = patched_model.model.model.layers[0] + assert layer.mlp.forward.__func__ is apply_lora_mlp_geglu + + # Test end-to-end + inputs = torch.randint(0, 100, (1, 20), device=model.device, dtype=torch.long) + with torch.no_grad(): + original_output = model(inputs).logits + patched_output = patched_model(inputs).logits + + assert torch.allclose(original_output, patched_output, rtol=1e-4) + + +@pytest.mark.parametrize( + "model_name,expected_activation", + [ + ("HuggingFaceTB/SmolLM2-135M", apply_lora_mlp_swiglu), + ("mhenrichsen/gemma-2b", apply_lora_mlp_geglu), + ], +) +def test_model_specific_activation(model_name, expected_activation): + """Test that each model type gets the correct activation function.""" + model = AutoModelForCausalLM.from_pretrained(model_name) + peft_config = get_peft_config( + { + "peft_type": "LORA", + "task_type": "CAUSAL_LM", + "r": 8, + "lora_alpha": 16, + "target_modules": ["gate_proj", "up_proj", "down_proj"], + "lora_dropout": 0, + "bias": "none", + } + ) + model = PeftModelForCausalLM(model, peft_config) + cfg = DictDefault({"lora_mlp_kernel": True}) + + patched_model = apply_lora_kernel_patches(model, cfg) + layer = patched_model.model.model.layers[0] + assert layer.mlp.forward.__func__ is expected_activation + + +def test_kernel_patch_conditions(): + """Test various conditions that should prevent kernel patching.""" + test_configs = [ + # Dropout prevents patching + { + "peft_type": "LORA", + "task_type": "CAUSAL_LM", + "r": 8, + "lora_alpha": 16, + "target_modules": ["gate_proj", "up_proj", "down_proj"], + "lora_dropout": 0.1, + "bias": "none", + }, + # Bias prevents patching + { + "peft_type": "LORA", + "task_type": "CAUSAL_LM", + "r": 8, + "lora_alpha": 16, + "target_modules": ["gate_proj", "up_proj", "down_proj"], + "lora_dropout": 0, + "bias": "lora_only", + }, + ] + + for config in test_configs: + model = AutoModelForCausalLM.from_pretrained("HuggingFaceTB/SmolLM2-135M") + peft_config = get_peft_config(config) + model = PeftModelForCausalLM(model, peft_config) + cfg = DictDefault({"lora_mlp_kernel": True}) + + # Should not patch + patched_model = apply_lora_kernel_patches(model, cfg) + layer = patched_model.model.model.layers[0].mlp + + # Verify no patches applied + assert layer.forward.__func__ is not apply_lora_mlp_swiglu + assert layer.forward.__func__ is not apply_lora_mlp_geglu + + +def test_kernel_config_options(): + """Test that kernel configuration options are respected.""" + # Test different configurations + test_configs = [ + ( + {"lora_mlp_kernel": True, "lora_qkv_kernel": False, "lora_o_kernel": False}, + lambda layer: ( + layer.mlp.forward.__func__ is apply_lora_mlp_swiglu + and layer.self_attn.apply_qkv.__func__ is not apply_lora_qkv + and layer.self_attn.apply_o.__func__ is not apply_lora_o + ), + ), + ( + {"lora_mlp_kernel": False, "lora_qkv_kernel": True, "lora_o_kernel": False}, + lambda layer: ( + layer.mlp.forward.__func__ is not apply_lora_mlp_swiglu + and layer.self_attn.apply_qkv.__func__ is apply_lora_qkv + and layer.self_attn.apply_o.__func__ is not apply_lora_o + ), + ), + ( + {"lora_mlp_kernel": False, "lora_qkv_kernel": False, "lora_o_kernel": True}, + lambda layer: ( + layer.mlp.forward.__func__ is not apply_lora_mlp_swiglu + and layer.self_attn.apply_qkv.__func__ is not apply_lora_qkv + and layer.self_attn.apply_o.__func__ is apply_lora_o + ), + ), + ] + + for config_dict, check_fn in test_configs: + # Create fresh model for each test + config = { + "vocab_size": 100, + "hidden_size": 128, + "intermediate_size": 256, + "num_hidden_layers": 2, + "num_attention_heads": 4, + } + small_llama_model = LlamaForCausalLM(LlamaConfig(**config)) + + peft_config = get_peft_config( + { + "peft_type": "LORA", + "task_type": "CAUSAL_LM", + "r": 8, + "lora_alpha": 16, + "target_modules": [ + "gate_proj", + "up_proj", + "down_proj", + "q_proj", + "k_proj", + "v_proj", + "o_proj", + ], + "lora_dropout": 0, + "bias": "none", + } + ) + model = PeftModelForCausalLM(small_llama_model, peft_config).to("cuda") + cfg = DictDefault(config_dict) + patched_model = apply_lora_kernel_patches(model, cfg) + + # Verify only requested optimizations were applied + for layer in patched_model.model.model.layers: + assert check_fn(layer), f"Failed for config: {config_dict}" + + # Clean up + del model + del small_llama_model + del patched_model + + +def get_lora_config(): + """Get standard LoRA configuration for testing.""" + return { + "peft_type": "LORA", + "task_type": "CAUSAL_LM", + "r": 8, + "lora_alpha": 16, + "target_modules": ["gate_proj", "up_proj", "down_proj"], + "lora_dropout": 0, + "bias": "none", + } + + +def get_test_inputs(model, seq_length=20): + """Generate test inputs for model evaluation.""" + return torch.randint( + 0, + model.config.vocab_size, + (1, seq_length), + device=model.device, + dtype=torch.long, + ) + + +@pytest.mark.parametrize("model_config", MODEL_CONFIGS) +def test_model_architecture(model_config): + """Test LoRA kernel patches across different model architectures.""" + # Load model with appropriate dtype + model = AutoModelForCausalLM.from_pretrained( + model_config["name"], torch_dtype=model_config["dtype"], device_map="cuda" + ) + + # Apply LoRA configuration + peft_config = get_peft_config(get_lora_config()) + model = PeftModelForCausalLM(model, peft_config) + + # Apply kernel patches + cfg = DictDefault({"lora_mlp_kernel": True}) + patched_model = apply_lora_kernel_patches(model, cfg) + + # Verify correct activation function + layer = patched_model.model.model.layers[0] + assert ( + layer.mlp.forward.__func__ is model_config["expected_activation"] + ), f"Wrong activation for {model_config['name']}" + + # Test forward pass + inputs = get_test_inputs(model) + with torch.no_grad(): + original_output = model(inputs).logits + patched_output = patched_model(inputs).logits + + # Check outputs match + assert torch.allclose( + original_output, patched_output, rtol=1e-4 + ), f"Outputs don't match for {model_config['name']}" + + +# pylint: disable=duplicate-code +def test_kernel_training_integration(): + """Test model loading with kernel patches enabled.""" + from axolotl.cli.utils import load_model_and_tokenizer + + # Create minimal config + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_config": "HuggingFaceTB/SmolLM2-135M", + "learning_rate": 0.000001, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + } + ], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.0, + "lora_target_linear": True, + "sequence_len": 1024, + "lora_mlp_kernel": True, + "lora_qkv_kernel": True, + "lora_o_kernel": True, + } + ) + + # Load model + model, _ = load_model_and_tokenizer(cfg=cfg) + + # Verify correct activation function + layer = model.model.model.layers[0] + assert layer.mlp.forward.__func__ is apply_lora_mlp_swiglu From 3aac3b1da9e7b38240f99d28e8ebb2218a8b2ec5 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 17 Feb 2025 15:46:04 -0500 Subject: [PATCH 0424/1405] Move sweeps code to another module (#2338) --- src/axolotl/cli/main.py | 74 +------------------------------------ src/axolotl/cli/sweeps.py | 77 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 73 deletions(-) create mode 100644 src/axolotl/cli/sweeps.py diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index dcdbe895f9..0f132c133c 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -3,11 +3,8 @@ import logging import os -import random import subprocess # nosec B404 import tempfile -from copy import deepcopy -from itertools import product from pathlib import Path from typing import Optional @@ -17,6 +14,7 @@ import axolotl from axolotl.cli.args import EvaluateCliArgs, PreprocessCliArgs, TrainerCliArgs +from axolotl.cli.sweeps import generate_sweep_configs from axolotl.cli.utils import ( add_options_from_config, add_options_from_dataclass, @@ -29,76 +27,6 @@ from axolotl.utils.config.models.input.v0_4_1 import AxolotlInputConfig -def generate_sweep_configs(base_config, sweeps_config): - """ - Recursively generates all possible configurations by applying sweeps to the base config. - - Args: - base_config (dict): The original configuration dictionary - sweeps_config (dict): Dictionary where keys are parameters and values are either: - - lists of values to sweep independently - - or for paired values, a list of dicts under the '_' key - - Returns: - list: List of all possible configuration dictionaries - - Example: - sweeps_config = { - 'learning_rate': [0.1, 0.01], - '_': [ - {'load_in_8bit': True, 'adapter': 'lora'}, - {'load_in_4bit': True, 'adapter': 'qlora'} - ] - } - """ - # Separate paired values from regular sweeps - paired_values = sweeps_config.get("_", []) - regular_sweeps = {k: v for k, v in sweeps_config.items() if k != "_"} - - # Process regular sweeps - param_names = list(regular_sweeps.keys()) - param_values = list(regular_sweeps.values()) - - # Generate combinations for regular sweeps - regular_combinations = list(product(*param_values)) if param_values else [()] - - # Combine regular sweeps with paired values - all_combinations = [] - for reg_combo in regular_combinations: - if paired_values: - for paired_set in paired_values: - new_config = {} - # new_config = deepcopy(base_config) - # Combine regular parameters with paired parameters - full_combo = {**dict(zip(param_names, reg_combo)), **paired_set} - for param_name, param_value in full_combo.items(): - new_config[param_name] = param_value - print(new_config) - all_combinations.append(new_config) - else: - # If no paired values, just use regular combinations - # new_config = deepcopy(base_config) - new_config = {} - for param_name, param_value in zip(param_names, reg_combo): - new_config[param_name] = param_value - print(new_config) - all_combinations.append(new_config) - - # randomize the order of trials - random.seed(42) - random.shuffle(all_combinations) - - # Generate a new config for each combination - result_configs = [] - for combination in all_combinations: - new_config = deepcopy(base_config) - for param_name, param_value in combination.items(): - new_config[param_name] = param_value - result_configs.append(new_config) - - return result_configs - - @click.group() @click.version_option(version=axolotl.__version__, prog_name="axolotl") def cli(): diff --git a/src/axolotl/cli/sweeps.py b/src/axolotl/cli/sweeps.py new file mode 100644 index 0000000000..d21664964c --- /dev/null +++ b/src/axolotl/cli/sweeps.py @@ -0,0 +1,77 @@ +"""Utilities for handling sweeps over configs for axolotl train CLI command""" + +import random +from copy import deepcopy +from itertools import product + + +def generate_sweep_configs( + base_config: dict[str, list], sweeps_config: dict[str, list] +) -> list[dict[str, list]]: + """ + Recursively generates all possible configurations by applying sweeps to the base config. + + Args: + base_config (dict): The original configuration dictionary + sweeps_config (dict): Dictionary where keys are parameters and values are either: + - lists of values to sweep independently + - or for paired values, a list of dicts under the '_' key + + Returns: + list: List of all possible configuration dictionaries + + Example: + sweeps_config = { + 'learning_rate': [0.1, 0.01], + '_': [ + {'load_in_8bit': True, 'adapter': 'lora'}, + {'load_in_4bit': True, 'adapter': 'qlora'} + ] + } + """ + # Separate paired values from regular sweeps + paired_values = sweeps_config.get("_", []) + regular_sweeps = {k: v for k, v in sweeps_config.items() if k != "_"} + + # Process regular sweeps + param_names = list(regular_sweeps.keys()) + param_values = list(regular_sweeps.values()) + + # Generate combinations for regular sweeps + regular_combinations = list(product(*param_values)) if param_values else [()] + + # Combine regular sweeps with paired values + all_combinations = [] + for reg_combo in regular_combinations: + if paired_values: + for paired_set in paired_values: + new_config = {} + # new_config = deepcopy(base_config) + # Combine regular parameters with paired parameters + full_combo = {**dict(zip(param_names, reg_combo)), **paired_set} + for param_name, param_value in full_combo.items(): + new_config[param_name] = param_value + print(new_config) + all_combinations.append(new_config) + else: + # If no paired values, just use regular combinations + # new_config = deepcopy(base_config) + new_config = {} + for param_name, param_value in zip(param_names, reg_combo): + new_config[param_name] = param_value + print(new_config) + all_combinations.append(new_config) + + # randomize the order of trials + random.seed(42) + random.shuffle(all_combinations) + + # Generate a new config for each combination + result_configs = [] + for combination in all_combinations: + new_config = deepcopy(base_config) + for param_name, param_value in combination.items(): + new_config[param_name] = param_value + result_configs.append(new_config) + + return result_configs From b194e17c2847348bb102d24e86775dfb77c9c5fe Mon Sep 17 00:00:00 2001 From: NJordan72 Date: Mon, 17 Feb 2025 21:59:27 -0500 Subject: [PATCH 0425/1405] feat: add config for optional parameters in a chat message (#2260) * feat: add config for optional parameters in a chat message * chore: cleanup * chore: fix nits and add light docs * docs: update docs/dataset-formats/conversation.qmd Co-authored-by: NanoCode012 * feat: configurable message mappings, jinja template analyzer * chore: handle bradley terry * docs: update docs * refactor: change order of mappings, improve message transform * refactor: make chat awware of property mappings * chore: remove .python-version * chore: revert change * chore: add dataset validation to tests where appropriate * chore: add dataset validation to tests where appropriate * chore: clean up handling of ds_cfg * chore: recursively serialize config * make sure to use the return value from validate_config * DefaultDict pickle/unpickle fix * fix super call for override * refactor: message fields * chore: empty commit * tests: validate config before using * chore: add config validation to all e2e tests * chore: add unneeded logging * chore: add missed config validation * chore: pass field_messages to prompter * test: fix borked test * chore: remove uninteded file * chore: add deprecation warning and update chat_datasets script * chore: lint * refactor: message fields * feat: update axolotlinputconfig and test_models - add configdict import in axolotl/utils/config/models/input/v0_4_1/__init__.py - remove unnecessary line breaks in sftdataset, dpodataset, ktodataset, stepwisesuperviseddataset classes - update model_dump method in axolotlinputconfig to exclude none values - correct typo in test_models.py comment * feat: simplify dpodataset and ktodataset classes in config models removed several optional fields from dpodataset and ktodataset classes in axolotl/utils/config/models/input/v0_4_1. this simplifies the configuration subsets for these datasets. * feat: improve readability and structure in dataset configuration models this commit enhances the readability and structure of the dataset configuration models in the `axolotl/utils/config/models/input/v0_4_1` module. it removes unused `configdict` import and adds line breaks to separate class definitions for better clarity. additionally, a minor documentation fix is included to ensure a newline at the end of the `stepwise_supervised.qmd` file. * feat: change log level from info to debug in chattemplatestrategy * feat(prompt_strategies): refactor chattemplateprompter and chattemplatestrategy - Make `chat_template` a required parameter in `ChatTemplatePrompter` constructor - Add default value for `message_property_mappings` in `ChatTemplatePrompter` constructor - Add `messages_array_name` property to `ChatTemplatePrompter` - Change `processor` type to Optional in `ChatTemplatePrompter` - Add TypeError check for `processor` in `ChatTemplatePrompter.build_prompt` - Remove `_messages` property from `ChatTemplateStrategy` - Make `prompter` a required parameter and add type hint in `ChatTemplateStrategy` constructor - Remove `messages` getter and setter from `ChatTemplateStrategy` - Use `prompter.messages_array_name` in `ChatTemplateStrategy.get_conversation_thread` - Remove condition to set `messages` field in `load` function * feat(tests/utils): ignore type check in load_model call in test_models.py * feat: improve type handling and test structure in chat templates - Add return type hint for `get_chat_template` function in `chat_templates.py` - Remove unnecessary assignment of `strategy.messages` in several test cases - Add `messages_array_name` parameter to various test configurations in `test_chat_templates.py` and `test_chat_templates_advanced.py` - Remove redundant `strategy.messages` assignment in `test_chat_templates_advanced.py` * feat(axolotl): enhance chat strategy with datasetconfig support This commit introduces support for DatasetConfig in the ChatTemplateStrategy. It also refines the strategy loader to handle different types of ds_cfg inputs and improves the clarity of the code by formatting and reordering. The key changes include: - Importing Union from typing and BaseModel from pydantic. - Adding DatasetConfig as an optional type for ds_cfg in StrategyLoader. - Adjusting the handling of ds_cfg in StrategyLoader to account for BaseModel instances. - Refactoring the prompter_params and strategy_params for better readability. - Changing the reference from prompt[self.messages] to prompt[self.prompter.messages_array_name] in the is_prompt_batched method. * feat: update message handling in btchattemplatestrategy * Replace `self.messages` with direct string references to "chosen_messages" and "rejected_messages" * Append system, user, and assistant content directly to "chosen_messages" and "rejected_messages" * Add a new attribute "messages_array_name" to the `load` function parameters * Remove the conditional attribute assignment for "field_messages" in the `load` function * feat: add config validation in test_kd.py - Import `validate_config` from `axolotl.utils.config` - Validate the configuration in `test_llama_kd` and another function in `TestKnowledgeDistillation` class * feat: enhance config validation and capabilities handling * Import `EnvCapabilities` and `GPUCapabilities` from `axolotl.utils.config.models.internals` * Update `validate_config` function to create `KTODataset` and `SFTDataset` instances using `dict(ds_cfg)` * Replace `capabilities` and `env_capabilities` with instances of `GPUCapabilities` and `EnvCapabilities` respectively in `AxolotlConfigWCapabilities` model dump * feat: update config validation in axolotl utils - Remove import of `EnvCapabilities` and `GPUCapabilities` from `axolotl.utils.config.models.internals` - Update `validate_config` function to use `capabilities` and `env_capabilities` directly instead of creating new instances of `GPUCapabilities` and `EnvCapabilities` * feat: refactor strategyloader in chat_template.py - Extracted the creation of strategy parameters into a separate function, `_get_strategy_params(cfg, dataset_config)` - Created a new function, `_get_strategy_cls()`, to obtain the strategy class - Replaced `ChatTemplateStrategy` with `strategy_cls` for strategy instantiation * trigger CI * chore: revert dataset config changes for kto/dpo * subject: refactor: rename 'messages_array_name' to 'field_messages' Body: - Renamed 'messages_array_name' to 'field_messages' in 'ChatTemplatePrompter' class and its usages in 'chat_template.py' - Updated 'load' function in 'bradley_terry/chat_template.py' to reflect the change - Adjusted 'get_chat_template_msg_variables' and 'get_message_vars' methods in 'jinja_template_analyzer.py' to use the new variable name - Modified 'StrategyLoader' in 'chat_template.py' to use 'field_messages' - Updated tests in 'test_chat_templates.py' and 'test_chat_templates_advanced.py' to use 'field_messages' instead of 'messages_array_name' * feat: refactor prompt strategies and update config models * Remove redundant 'return None' in `axolotl/prompt_strategies/__init__.py` * Simplify message handling in `axolotl/prompt_strategies/bradley_terry/chat_template.py` by using a single 'messages' list instead of separate 'chosen_messages' and 'rejected_messages' lists * Update default 'message_property_mappings' in `axolotl/prompt_strategies/bradley_terry/chat_template.py` * Add 'field_messages' field to `axolotl/utils/config/models/input/v0_4_1/__init__.py` configuration model * chore: remove unused input * chore: remove redundant type ignore * fix: remove old configs and update examples * fix: type check * fix: remove loading old config in ChatMessage * fix: update faq with potential new undefinederror * fix: add debug if property mapped is not found * chore: improve explanation for unmapped properties * fix: update docs with new config * chore: add note for deprecation config and del old config from dict --------- Co-authored-by: NanoCode012 Co-authored-by: Wing Lian Co-authored-by: NanoCode012 --- docs/config.qmd | 17 +- docs/dataset-formats/conversation.qmd | 15 +- docs/dataset-formats/index.qmd | 24 +- docs/faq.qmd | 4 + docs/rlhf.qmd | 5 +- examples/deepseek-v2/qlora-fsdp-2_5.yaml | 5 +- examples/gemma2/qlora.yml | 5 +- examples/jamba/qlora_fsdp_large.yaml | 5 +- examples/llama-3/fft-8b-liger-fsdp.yaml | 5 +- examples/llama-3/instruct-dpo-lora-8b.yml | 5 +- examples/llama-3/instruct-lora-8b.yml | 5 +- examples/llama-3/lora-1b-deduplicate-dpo.yml | 10 +- examples/mistral/mistral-dpo-qlora.yml | 5 +- examples/phi/lora-3.5.yaml | 5 +- examples/qwen2/dpo.yaml | 5 +- scripts/chat_datasets.py | 15 +- src/axolotl/prompt_strategies/__init__.py | 2 +- .../bradley_terry/chat_template.py | 36 +- .../prompt_strategies/chat_template.py | 147 +++++--- .../prompt_strategies/dpo/chat_template.py | 24 +- .../jinja_template_analyzer.py | 318 ++++++++++++++++++ .../prompt_strategies/messages/chat.py | 9 +- src/axolotl/utils/chat_templates.py | 6 +- src/axolotl/utils/config/__init__.py | 13 +- .../config/models/input/v0_4_1/__init__.py | 122 ++++++- src/axolotl/utils/data/sft.py | 1 + src/axolotl/utils/dict.py | 23 ++ tests/e2e/integrations/test_kd.py | 4 +- tests/e2e/patched/test_fa_xentropy.py | 4 +- tests/e2e/solo/test_relora_llama.py | 4 +- tests/e2e/test_dpo.py | 16 +- tests/e2e/test_embeddings_lr.py | 4 +- tests/e2e/test_falcon.py | 8 +- tests/e2e/test_llama.py | 8 +- tests/e2e/test_llama_pretrain.py | 4 +- tests/e2e/test_llama_vision.py | 4 +- tests/e2e/test_lora_llama.py | 4 +- tests/e2e/test_mamba.py | 4 +- tests/e2e/test_mistral.py | 6 +- tests/e2e/test_mixtral.py | 12 +- tests/e2e/test_optimizers.py | 8 +- tests/e2e/test_packing_loss.py | 4 +- tests/e2e/test_phi.py | 3 +- tests/e2e/test_qwen.py | 6 +- tests/e2e/test_reward_model_smollm2.py | 3 +- tests/prompt_strategies/conftest.py | 30 ++ .../prompt_strategies/test_chat_templates.py | 61 ++-- .../test_chat_templates_advanced.py | 131 +++++--- .../test_jinja_template_analyzer.py | 159 +++++++++ tests/test_validation_dataset.py | 19 ++ tests/utils/test_models.py | 78 ++++- 51 files changed, 1190 insertions(+), 230 deletions(-) create mode 100644 src/axolotl/prompt_strategies/jinja_template_analyzer.py create mode 100644 tests/prompt_strategies/test_jinja_template_analyzer.py diff --git a/docs/config.qmd b/docs/config.qmd index 19e33e7f0b..745a185263 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -142,10 +142,19 @@ datasets: # Key containing the messages (default: "messages") field_messages: messages - # Key for role in each message (default: "role") - message_field_role: role - # Key for content in each message (default: "content") - message_field_content: content + + # Mapping of properties from the input dataset to the chat template. + # (default: message_property_mappings={'role':'role', 'content':'content'}) + # If a property exists in the template but not in this mapping, the system will attempt + # to load it directly from the message using the property name as the key. + # Example: In the mapping below, 'from' is loaded from input dataset and used as 'role', + # while 'value' is loaded and used as 'content' in the chat template. + message_property_mappings: + role: from + content: value + # ... + + message_property_mappings: # Optional[Dict[str, List]]. Roles mapping in the messages. The default is: roles: diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 5f9ac01a33..6866f3f408 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -42,8 +42,9 @@ datasets: type: chat_template field_messages: conversations - message_field_role: from - message_field_content: value + message_property_mappings: + role: from + content: value # new (if setting a new chat_template like chatml, gemma, etc) chat_template: chatml @@ -52,8 +53,9 @@ datasets: type: chat_template field_messages: conversations - message_field_role: from - message_field_content: value + message_property_mappings: + role: from + content: value ``` We recommend checking the below examples for other usecases. @@ -138,8 +140,9 @@ datasets: type: chat_template chat_template: tokenizer_default field_messages: conversations - message_field_role: from - message_field_content: value + message_property_mappings: + role: from + content: value roles_to_train: [] train_on_eos: turn message_field_training: train diff --git a/docs/dataset-formats/index.qmd b/docs/dataset-formats/index.qmd index 158fa79bd2..a46f466048 100644 --- a/docs/dataset-formats/index.qmd +++ b/docs/dataset-formats/index.qmd @@ -114,7 +114,7 @@ A flow chart is as follows: 4. Is your dataset in an "instruct" format, containing `{ instruction, response }`? If yes, check [Instruction Dataset](#instruction-dataset) -If you went through the flow chart and did not find one that matches, it is recommended to preprocess your dataset into one of the above or create a Github Discussion. +If you went through the flow chart and did not find one that matches, it is recommended to preprocess your dataset into one of the above or create a thread on Github Discussion. ::: {.callout-tip} You can mix and match within each approach or across approaches to train a model on a variety of datasets. @@ -289,9 +289,10 @@ If your dataset format is different, here are the keys you should check (with th ```yaml datasets: ... - field_messages: messages - message_field_role: role - message_field_content: content + field_messages: messages # this should point to the key containing the list of conversations + message_property_mappings: # this is a mapping from keys in your dataset to keys in chat_template + role: role + content: content ``` In some `chat_templates` (e.g. [Gemma](https://huggingface.co/google/gemma-2b-it/blob/main/tokenizer_config.json#L1507)), the roles are hardcoded to `user` and `assistant`. Consequently, you may find it necessary to map the roles in your dataset to these above. We currently have some defaults that should work for common datasets, but if you get a `KeyError`, it would be necessary to add mapping for your roles. Here is an example of how it would look like: @@ -348,13 +349,14 @@ datasets: - path: A.jsonl type: chat_template - # step 1 + # step 1 chat_template: chatml - # step 2 - field_messages: messages - message_field_role: role - message_field_content: content + # step 2 + field_messages: messages + message_property_mappings: + role: role + content: content roles: assistant: @@ -365,8 +367,8 @@ datasets: - human - user - # step 3 - roles_to_train: ["assistant"] + # step 3 + roles_to_train: ["assistant"] train_on_eos: "turn" special_tokens: diff --git a/docs/faq.qmd b/docs/faq.qmd index a05624bd15..3f78bde73c 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -23,3 +23,7 @@ description: Frequently asked questions **Q: The codes is stuck on saving preprocessed datasets.** > A: This is usually an issue with the GPU. This can be resolved through setting the os environment variable `CUDA_VISIBLE_DEVICES=0`. If you are on runpod, this is usually a pod issue. Starting a new pod should take care of it. + +**Q: `jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'content' / 'role' / ____`** + +> A: This means that the property mapping for the stated attribute does not exist when building `chat_template` prompt. For example, if `no attribute 'content'`, please check you have added the correct mapping for `content` under `message_property_mappings`. diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 86f874799d..ff150ffbe1 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -229,8 +229,9 @@ datasets: field_messages: "messages" field_chosen: "chosen" field_rejected: "rejected" - message_field_role: "role" - message_field_content: "content" + message_property_mappings: + role: role + content: content roles: user: ["user"] assistant: ["assistant"] diff --git a/examples/deepseek-v2/qlora-fsdp-2_5.yaml b/examples/deepseek-v2/qlora-fsdp-2_5.yaml index 009e46a2fb..005a0c76f8 100644 --- a/examples/deepseek-v2/qlora-fsdp-2_5.yaml +++ b/examples/deepseek-v2/qlora-fsdp-2_5.yaml @@ -21,8 +21,9 @@ datasets: type: chat_template split: train[:20%] field_messages: conversations - message_field_role: from - message_field_content: value + message_property_mappings: + role: from + content: value dataset_prepared_path: last_run_prepared val_set_size: 0.0 diff --git a/examples/gemma2/qlora.yml b/examples/gemma2/qlora.yml index 61a2ad8761..2f18cc76d4 100644 --- a/examples/gemma2/qlora.yml +++ b/examples/gemma2/qlora.yml @@ -16,8 +16,9 @@ datasets: type: chat_template drop_system_message: true field_messages: conversations - message_field_role: from - message_field_content: value + message_property_mappings: + role: from + content: value val_set_size: 0.0 output_dir: ./outputs/out diff --git a/examples/jamba/qlora_fsdp_large.yaml b/examples/jamba/qlora_fsdp_large.yaml index 594847330d..18c0ec086f 100644 --- a/examples/jamba/qlora_fsdp_large.yaml +++ b/examples/jamba/qlora_fsdp_large.yaml @@ -13,8 +13,9 @@ datasets: type: chat_template drop_system_message: true field_messages: conversations - message_field_role: from - message_field_content: value + message_property_mappings: + role: from + content: value dataset_prepared_path: last_run_prepared val_set_size: 0.0 diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml index aa8a7c5e82..652a00e5cd 100644 --- a/examples/llama-3/fft-8b-liger-fsdp.yaml +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -17,8 +17,9 @@ datasets: type: chat_template split: train[:20%] field_messages: conversations - message_field_role: from - message_field_content: value + message_property_mappings: + role: from + content: value dataset_prepared_path: last_run_prepared val_set_size: 0.02 diff --git a/examples/llama-3/instruct-dpo-lora-8b.yml b/examples/llama-3/instruct-dpo-lora-8b.yml index bb58b677a4..c7568dd78b 100644 --- a/examples/llama-3/instruct-dpo-lora-8b.yml +++ b/examples/llama-3/instruct-dpo-lora-8b.yml @@ -17,8 +17,9 @@ datasets: field_messages: conversation field_chosen: chosen field_rejected: rejected - message_field_role: role - message_field_content: content + message_property_mappings: + role: role + content: content roles: system: - system diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml index 853f85d74d..8b1c254cbb 100644 --- a/examples/llama-3/instruct-lora-8b.yml +++ b/examples/llama-3/instruct-lora-8b.yml @@ -14,8 +14,9 @@ datasets: - path: fozziethebeat/alpaca_messages_2k_test type: chat_template field_messages: messages - message_field_role: role - message_field_content: content + message_property_mappings: + role: role + content: content roles: user: - user diff --git a/examples/llama-3/lora-1b-deduplicate-dpo.yml b/examples/llama-3/lora-1b-deduplicate-dpo.yml index 00314da75d..62d6985597 100644 --- a/examples/llama-3/lora-1b-deduplicate-dpo.yml +++ b/examples/llama-3/lora-1b-deduplicate-dpo.yml @@ -17,8 +17,9 @@ datasets: field_messages: conversation field_chosen: chosen field_rejected: rejected - message_field_role: role - message_field_content: content + message_property_mappings: + role: role + content: content roles: system: - system @@ -31,8 +32,9 @@ datasets: field_messages: conversation field_chosen: chosen field_rejected: rejected - message_field_role: role - message_field_content: content + message_property_mappings: + role: role + content: content roles: system: - system diff --git a/examples/mistral/mistral-dpo-qlora.yml b/examples/mistral/mistral-dpo-qlora.yml index e2eb6a2645..168fd3c3bb 100644 --- a/examples/mistral/mistral-dpo-qlora.yml +++ b/examples/mistral/mistral-dpo-qlora.yml @@ -22,8 +22,9 @@ datasets: field_messages: conversation field_chosen: chosen field_rejected: rejected - message_field_role: role - message_field_content: content + message_property_mappings: + role: role + content: content dataset_prepared_path: val_set_size: 0.05 diff --git a/examples/phi/lora-3.5.yaml b/examples/phi/lora-3.5.yaml index 8c0205f4cd..ddc2d77884 100644 --- a/examples/phi/lora-3.5.yaml +++ b/examples/phi/lora-3.5.yaml @@ -14,8 +14,9 @@ datasets: - path: fozziethebeat/alpaca_messages_2k_test type: chat_template field_messages: messages - message_field_role: role - message_field_content: content + message_property_mappings: + role: role + content: content roles: user: - user diff --git a/examples/qwen2/dpo.yaml b/examples/qwen2/dpo.yaml index e924be195c..d6dbe22d79 100644 --- a/examples/qwen2/dpo.yaml +++ b/examples/qwen2/dpo.yaml @@ -12,8 +12,9 @@ datasets: field_messages: conversation field_chosen: chosen field_rejected: rejected - message_field_role: role - message_field_content: content + message_property_mappings: + role: role + content: content roles: system: - system diff --git a/scripts/chat_datasets.py b/scripts/chat_datasets.py index 6210b11388..8ae1e256d3 100644 --- a/scripts/chat_datasets.py +++ b/scripts/chat_datasets.py @@ -31,27 +31,26 @@ def parse_dataset(dataset=None, split="train"): ds_cfg["field_messages"] = field_messages message_fields = features[field_messages][0].keys() - message_field_role = None + + message_property_mappings = {"role": None, "content": None} for key in ["from", "role"]: if key in message_fields: - message_field_role = key + message_property_mappings["role"] = key break - if not message_field_role: + if not message_property_mappings["role"]: raise ValueError( f'No role field found in messages: {", ".join(message_fields)}' ) - ds_cfg["message_field_role"] = message_field_role - message_field_content = None for key in ["content", "text", "value"]: if key in message_fields: - message_field_content = key + message_property_mappings["content"] = key break - if not message_field_content: + if not message_property_mappings["content"]: raise ValueError( f'No content field found in messages: {", ".join(message_fields)}' ) - ds_cfg["message_field_content"] = message_field_content + ds_cfg["message_property_mappings"] = message_property_mappings print(yaml.dump({"datasets": [ds_cfg]})) diff --git a/src/axolotl/prompt_strategies/__init__.py b/src/axolotl/prompt_strategies/__init__.py index 645a9329c1..ba0dad0533 100644 --- a/src/axolotl/prompt_strategies/__init__.py +++ b/src/axolotl/prompt_strategies/__init__.py @@ -41,10 +41,10 @@ def load(strategy, tokenizer, cfg, ds_cfg, processor=None): load_kwargs["ds_cfg"] = ds_cfg if "processor" in sig.parameters: load_kwargs["processor"] = processor + return func(tokenizer, cfg, **load_kwargs) except ModuleNotFoundError: return None except Exception as exc: # pylint: disable=broad-exception-caught LOG.error(f"Failed to load prompt strategy `{strategy}`: {str(exc)}") raise exc - return None diff --git a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py index 61191a47f1..67319f5b41 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py +++ b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py @@ -34,15 +34,12 @@ def _tokenize_single_prompt(self, prompt): max_length = self.prompter.max_length - self.messages = "chosen_messages" # pylint: disable=duplicate-code - prompt[self.messages] = [] + prompt["messages"] = [] if prompt["system"]: - prompt[self.messages].append( - {"role": "system", "content": prompt["system"]} - ) - prompt[self.messages].append({"role": "user", "content": prompt["input"]}) - prompt[self.messages].append({"role": "assistant", "content": prompt["chosen"]}) + prompt["messages"].append({"role": "system", "content": prompt["system"]}) + prompt["messages"].append({"role": "user", "content": prompt["input"]}) + prompt["messages"].append({"role": "assistant", "content": prompt["chosen"]}) chosen_tokenized = super()._tokenize_single_prompt(prompt) if len(chosen_tokenized["input_ids"]) > max_length: @@ -55,17 +52,12 @@ def _tokenize_single_prompt(self, prompt): :max_length ] - self.messages = "rejected_messages" # pylint: disable=duplicate-code - prompt[self.messages] = [] + prompt["messages"] = [] if prompt["system"]: - prompt[self.messages].append( - {"role": "system", "content": prompt["system"]} - ) - prompt[self.messages].append({"role": "user", "content": prompt["input"]}) - prompt[self.messages].append( - {"role": "assistant", "content": prompt["rejected"]} - ) + prompt["messages"].append({"role": "system", "content": prompt["system"]}) + prompt["messages"].append({"role": "user", "content": prompt["input"]}) + prompt["messages"].append({"role": "assistant", "content": prompt["rejected"]}) rejected_tokenized = super()._tokenize_single_prompt(prompt) if len(rejected_tokenized["input_ids"]) > max_length: @@ -99,8 +91,13 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): prompter_params = { "tokenizer": tokenizer, "chat_template": chat_template_string, - "message_field_role": ds_cfg.get("message_field_role", "role"), - "message_field_content": ds_cfg.get("message_field_content", "content"), + "message_property_mappings": ds_cfg.get( + "message_property_mappings", + { + "role": "role", + "content": "content", + }, + ), "message_field_training": ds_cfg.get("message_field_training", None), "message_field_training_detail": ds_cfg.get( "message_field_training_detail", None @@ -124,7 +121,4 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): ChatTemplatePrompter(**prompter_params), tokenizer=tokenizer, **strategy_params ) - if "field_messages" in ds_cfg and hasattr(strategy, "messages"): - strategy.messages = ds_cfg["field_messages"] - return strategy diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index bb87ee45b4..55140f1510 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -4,13 +4,16 @@ import logging from collections import defaultdict -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Set, Union +from pydantic import BaseModel from transformers import ProcessorMixin +from axolotl.prompt_strategies.jinja_template_analyzer import JinjaTemplateAnalyzer from axolotl.prompt_tokenizers import PromptTokenizingStrategy from axolotl.prompters import IGNORE_TOKEN_ID, Prompter from axolotl.utils.chat_templates import get_chat_template_from_config +from axolotl.utils.config.models.input.v0_4_1 import DatasetConfig # Configure the logger LOG = logging.getLogger("axolotl") @@ -23,16 +26,23 @@ class ChatTemplatePrompter(Prompter): def __init__( self, tokenizer, + chat_template: str, processor=None, - chat_template=None, max_length=2048, - message_field_role: str = "role", - message_field_content: str = "content", + message_property_mappings: Optional[Dict[str, str]] = None, message_field_training: Optional[str] = None, message_field_training_detail: Optional[str] = None, + field_messages: str = "messages", roles: Optional[Dict[str, List[str]]] = None, drop_system_message: bool = False, ): + # check if message_property_mappings is None or empty dict + if message_property_mappings is None or (not message_property_mappings): + message_property_mappings = { + "role": "role", + "content": "content", + } + if roles: self.roles = {s: t for t, sources in roles.items() for s in sources} else: @@ -45,18 +55,28 @@ def __init__( "tool": "tool", } - self.message_field_role = message_field_role - self.message_field_content = message_field_content + self._chat_template_msg_variables = self.get_chat_template_msg_variables( + chat_template, field_messages + ) + self.message_property_mappings = message_property_mappings self.message_field_training = message_field_training self.message_field_training_detail = message_field_training_detail + self.field_messages = field_messages self.tokenizer = tokenizer - self.processor: ProcessorMixin = processor + self.processor: Optional[ProcessorMixin] = processor self.chat_template = chat_template self.max_length = max_length self.drop_system_message = drop_system_message + @property + def chat_template_msg_variables(self) -> Set[str]: + return self._chat_template_msg_variables + def build_prompt(self, conversation, add_generation_prompt=False, images=None): if self.processor: + if not callable(self.processor): + raise TypeError("Processor must be callable") + text = self.processor.apply_chat_template( conversation, chat_template=self.chat_template, @@ -184,17 +204,21 @@ def adjust_train_details( return adjusted_details + def get_chat_template_msg_variables( + self, chat_template: str, field_messages: str + ) -> Set[str]: + template_analyzer = JinjaTemplateAnalyzer(chat_template) + return template_analyzer.get_message_vars(field_messages) + class ChatTemplateStrategy(PromptTokenizingStrategy): """ Tokenizing strategy for instruction-based prompts. """ - _messages = "messages" - def __init__( self, - prompter: ChatTemplatePrompter, + prompter: "ChatTemplatePrompter", tokenizer, train_on_inputs, sequence_len, @@ -202,6 +226,7 @@ def __init__( train_on_eos=None, ): super().__init__(prompter, tokenizer, train_on_inputs, sequence_len) + self.prompter: ChatTemplatePrompter = prompter self.roles_to_train = [] if roles_to_train: @@ -213,13 +238,9 @@ def __init__( self.train_on_eos = train_on_eos self.images = "images" - @property - def messages(self): - return self._messages - - @messages.setter - def messages(self, messages): - self._messages = messages + LOG.debug( + f"The chat template uses the following properites on the message: {self.prompter.chat_template_msg_variables}" + ) @property def supports_batched(self) -> bool: @@ -229,7 +250,7 @@ def supports_batched(self) -> bool: def is_prompt_batched(self, prompt: dict[str, Any]) -> bool: try: return all(isinstance(v, list) for v in prompt.values()) and all( - isinstance(v, list) for v in prompt[self.messages] + isinstance(v, list) for v in prompt[self.prompter.field_messages] ) except KeyError: return False @@ -464,30 +485,17 @@ def find_turn(self, turns: list[dict], turn_idx: int): def get_conversation_thread(self, prompt): turns = [] - optional_keys = [ - "tool_calls", # tool that 'assistant' calls - "name", # name of tool given by 'tool' - "tool_call_id", # mistral/mixtral requires this - ] - for message in prompt[self.messages]: + for message in prompt[self.prompter.field_messages]: + transformed_message = self.transform_message(message) + turn = { - "role": self.prompter.roles[message[self.prompter.message_field_role]], + **transformed_message, "training": message.get(self.prompter.message_field_training), "training_detail": message.get( self.prompter.message_field_training_detail ), } - # do not add content if None as it may conflict with some templates due to tools - content = message.get(self.prompter.message_field_content, None) - if content is not None: - turn["content"] = content - - for key in optional_keys: - value = message.get(key, None) - if value is not None: - turn[key] = value - turns.append(turn) if self.prompter.drop_system_message and turns[0]["role"] == "system": @@ -495,6 +503,37 @@ def get_conversation_thread(self, prompt): return turns + def transform_message(self, message): + # Build the initial transformed message from the mappings + transformed_message = {} + for key, value in self.prompter.message_property_mappings.items(): + if message.get(value) is not None: + transformed_message[key] = message[value] + else: + LOG.debug( + f"Could not find value for property {value} in message: {message}" + ) + + # Map the role if necessary + if "role" in transformed_message: + transformed_message["role"] = self.prompter.roles.get( + transformed_message["role"], transformed_message["role"] + ) + + # Determine which keys in the original message were not mapped + mapped_values = set(self.prompter.message_property_mappings.values()) + remaining_keys = set(message) - mapped_values + + # Keep only the properties defined in the chat template + # and not already mapped + for key in self.prompter.chat_template_msg_variables: + if key in remaining_keys: + val = message.get(key) + if val is not None: + transformed_message[key] = val + + return transformed_message + def get_images(self, prompt): return prompt.get(self.images, None) @@ -516,33 +555,46 @@ def _get_strategy_params(self, cfg, ds_cfg: Dict[str, Any]): } def __call__( - self, tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None, processor=None + self, + tokenizer, + cfg, + ds_cfg: Optional[Union[Dict[str, Any], DatasetConfig]] = None, + processor=None, ): - # pylint: disable=duplicate-code - ds_cfg = ds_cfg or {} + if ds_cfg is None: + dataset_config = {} + elif isinstance(ds_cfg, BaseModel): + dataset_config = ds_cfg.model_dump() + else: + dataset_config = ds_cfg + chat_template_string = get_chat_template_from_config( - cfg=cfg, ds_cfg=ds_cfg, tokenizer=tokenizer + cfg=cfg, ds_cfg=dataset_config, tokenizer=tokenizer ) LOG.info(f"Using chat template:\n---\n{chat_template_string!s}\n---") prompter_params = { "tokenizer": tokenizer, "chat_template": chat_template_string, - "message_field_role": ds_cfg.get("message_field_role", "role"), - "message_field_content": ds_cfg.get("message_field_content", "content"), - "message_field_training": ds_cfg.get("message_field_training", None), - "message_field_training_detail": ds_cfg.get( + "message_property_mappings": dataset_config.get( + "message_property_mappings", {} + ), + "message_field_training": dataset_config.get( + "message_field_training", None + ), + "message_field_training_detail": dataset_config.get( "message_field_training_detail", None, ), - "roles": ds_cfg.get("roles"), - "drop_system_message": ds_cfg.get("drop_system_message", False), + "field_messages": dataset_config.get("field_messages", "messages"), + "roles": dataset_config.get("roles"), + "drop_system_message": dataset_config.get("drop_system_message", False), # we need to add one for detecting sequences with exceeding the `sequence_len` limit. "max_length": cfg.sequence_len + 1, "processor": processor, } - strategy_params = self._get_strategy_params(cfg, ds_cfg) + strategy_params = self._get_strategy_params(cfg, dataset_config) strategy_cls = self._get_strategy_cls() strategy = strategy_cls( @@ -551,9 +603,6 @@ def __call__( **strategy_params, ) - if "field_messages" in ds_cfg and hasattr(strategy, "messages"): - strategy.messages = ds_cfg["field_messages"] - return strategy diff --git a/src/axolotl/prompt_strategies/dpo/chat_template.py b/src/axolotl/prompt_strategies/dpo/chat_template.py index 489b864851..9e4986dd4c 100644 --- a/src/axolotl/prompt_strategies/dpo/chat_template.py +++ b/src/axolotl/prompt_strategies/dpo/chat_template.py @@ -3,20 +3,28 @@ """ from axolotl.utils.chat_templates import extract_chat_template_args, get_chat_template +from axolotl.utils.config.models.input.v0_4_1 import handle_legacy_message_fields_logic def default( cfg, dataset_idx=0, **kwargs ): # pylint: disable=possibly-unused-variable,unused-argument ds_cfg = cfg["datasets"][dataset_idx] + ds_cfg = handle_legacy_message_fields_logic(ds_cfg) + chat_template_choice, chat_template_jinja = extract_chat_template_args( cfg=cfg, ds_cfg=ds_cfg ) field_messages = ds_cfg.get("field_messages", "messages") field_chosen = ds_cfg.get("field_chosen", "chosen") field_rejected = ds_cfg.get("field_rejected", "rejected") - field_message_role = ds_cfg.get("message_field_role", "role") - field_message_content = ds_cfg.get("message_field_content", "content") + message_property_mappings = ds_cfg.get( + "message_property_mappings", + { + "role": "role", + "content": "content", + }, + ) role_map_inv = ds_cfg.get( "roles", { @@ -40,18 +48,18 @@ def transform_fn(sample, tokenizer=None): messages = sample[field_messages] messages = [ { - "role": role_map[m[field_message_role]], - "content": m[field_message_content], + "role": role_map[m[message_property_mappings["role"]]], + "content": m[message_property_mappings["content"]], } for m in messages ] chosen = { - "role": role_map[sample[field_chosen][field_message_role]], - "content": sample[field_chosen][field_message_content], + "role": role_map[sample[field_chosen][message_property_mappings["role"]]], + "content": sample[field_chosen][message_property_mappings["content"]], } rejected = { - "role": role_map[sample[field_rejected][field_message_role]], - "content": sample[field_rejected][field_message_content], + "role": role_map[sample[field_rejected][message_property_mappings["role"]]], + "content": sample[field_rejected][message_property_mappings["content"]], } dummy_user_message = {"role": "user", "content": "[[dummy_message]]"} diff --git a/src/axolotl/prompt_strategies/jinja_template_analyzer.py b/src/axolotl/prompt_strategies/jinja_template_analyzer.py new file mode 100644 index 0000000000..01cbd3ad27 --- /dev/null +++ b/src/axolotl/prompt_strategies/jinja_template_analyzer.py @@ -0,0 +1,318 @@ +"""Module for inspect jinja templates for the variables they use""" +from typing import Dict, Optional, Set, TypedDict, Union + +from jinja2 import Environment, meta, nodes + + +class JinjaTemplateAnalysis(TypedDict): + """ + Represents the detailed analysis of a Jinja template variable. + + Attributes: + accessed_properties (Set[str]): A set of properties accessed from the variable + (e.g., `foo.bar` results in 'bar' being accessed for 'foo'). + accessed_indices (Set[Union[int, float]]): A set of indices accessed from the variable. + is_iterated (bool): Indicates if the variable is used as an iteration source in a `for` loop. + is_conditional (bool): Indicates if the variable is referenced within a conditional statement (e.g., an `if` block). + iteration_source (Optional[str]): The name of the variable being iterated over, if applicable. + iteration_target (Optional[Union[str, list[str]]]): The loop target(s) assigned in the iteration. + """ + + accessed_properties: Set[str] + accessed_indices: Set[Union[int, float]] + is_iterated: bool + is_conditional: bool + iteration_source: Optional[str] + iteration_target: Optional[Union[str, list[str]]] + + +class JinjaTemplateAnalyzer: + """ + Analyzes Jinja templates to extract information about variable usage, + including accessed properties, iteration, and conditional references. + + Attributes: + env (jinja2.Environment): The Jinja2 environment used for parsing templates. + property_access (Dict[str, Set[str]]): Tracks accessed properties for variables. + iteration_targets (Dict[str, str]): Maps iteration target variables to their sources. + + Methods: + get_template_variables(template: str) -> Dict[str, Set[str]]: + Parse a Jinja template and return a mapping of variables to their accessed properties. + + analyze_template(template: str) -> Dict[str, JinjaTemplateAnalysis]: + Perform a detailed analysis of the template, including variable usage, + iteration, and conditional references. + + Private Methods: + _visit_node(node) -> None: + Recursively visit AST nodes to detect attribute access and iteration targets. + + _get_base_name(node) -> Optional[str]: + Extract the base variable name from a node. + + _get_target_name(node) -> Optional[Union[str, list[str]]]: + Extract the target name(s) from a `For` node. + """ + + def __init__(self, template: str): + self.env: Environment = Environment(autoescape=True) + self.property_access: Dict[str, Set[str]] = {} + self.iteration_targets: Dict[str, Union[str, list[str]]] = {} + self.index_access: Dict[str, Set[Union[int, float]]] = {} + self.ast: nodes.Node = self.env.parse(template) + self.template: str = template + self.variable_assignments: Dict[str, str] = {} + + def _visit_node(self, node) -> None: + """Recursively visit AST nodes to find attribute access.""" + # Handle attribute access (dot notation) + if isinstance(node, nodes.Getattr): + base_name = self._get_base_name(node.node) + if base_name: + self.property_access.setdefault(base_name, set()).add(node.attr) + + # Handle dictionary access (subscript notation) + elif isinstance(node, nodes.Getitem): + base_name = self._get_base_name(node.node) + if base_name and isinstance(node.arg, nodes.Const): + value = node.arg.value + if isinstance(value, (int, float)): + self.index_access.setdefault(base_name, set()).add(value) + else: + self.property_access.setdefault(base_name, set()).add(value) + + elif isinstance(node, nodes.Test) and node.name == "defined": + base_name = self._get_base_name(node.node) + if base_name: + if isinstance(node.node, nodes.Getattr): + self.property_access.setdefault(base_name, set()).add( + node.node.attr + ) + + # Handle loop variables + elif isinstance(node, nodes.For): + iter_name = self._get_base_name(node.iter) + target_name = self._get_target_name(node.target) + if iter_name and target_name: + self.iteration_targets[target_name] = iter_name + self.property_access.setdefault(iter_name, set()) + + elif isinstance(node, nodes.Assign): + target_name = self._get_target_name(node.target) + source_name = self._get_base_name(node.node) + if target_name and source_name: + self.variable_assignments[target_name] = source_name + + elif isinstance(node, nodes.Filter): + if node.name == "selectattr": + target = self._get_base_name(node.node) + if target: + self.variable_assignments[f"filtered_{target}"] = target + + for child in node.iter_child_nodes(): + self._visit_node(child) + + def _get_target_name(self, node) -> Optional[str]: + """Get the target variable name from a For node. + + Args: + node: A Jinja AST node representing either a Name or Tuple node + + Returns: + - str: For simple variable targets (e.g., "item" in "for item in items") + - None: If the node type is not recognized or is a tuple + """ + if isinstance(node, nodes.Name): + return node.name + return None + + def _get_target_names(self, node) -> list[str]: + """Get all target variable names from a For node, including tuple unpacking. + + Args: + node: A Jinja AST node representing either a Name or Tuple node + + Returns: + List of target variable names + """ + if isinstance(node, nodes.Name): + return [node.name] + + if isinstance(node, nodes.Tuple): + names = [] + for n in node.items: + if isinstance(n, nodes.Name): + names.append(n.name) + return names + + return [] + + def _get_base_name(self, node) -> Optional[str]: + """Get the base variable name from a node.""" + if isinstance(node, nodes.Name): + return node.name + + if isinstance(node, nodes.Getattr): + return self._get_base_name(node.node) + + if isinstance(node, nodes.Getitem): + return self._get_base_name(node.node) + + return None + + def get_template_variables(self) -> Dict[str, Set[str]]: + """ + Parse a Jinja template and return both variables and their accessed properties. + + Args: + template (str): The Jinja template string + + Returns: + Dict[str, Set[str]]: Dictionary mapping variable names to sets of accessed properties + """ + # Parse the template + ast = self.env.parse(self.template) + + # Get all undeclared variables + variables = meta.find_undeclared_variables(ast) + + # Reset property access tracking + self.property_access = {} + + # Visit all nodes to find property access + self._visit_node(ast) + + # Create result dictionary + result: Dict[str, Set[str]] = {var: set() for var in variables} + # Merge in any discovered sub-properties + for var, props in self.property_access.items(): + if var not in result: + result[var] = set() + result[var].update(props) + + return result + + def analyze_template(self) -> Dict[str, JinjaTemplateAnalysis]: + """ + Provide a detailed analysis of template variables and their usage. + """ + variables = self.get_template_variables() + self.iteration_targets = {} + + analysis: Dict[str, JinjaTemplateAnalysis] = { + var: JinjaTemplateAnalysis( + accessed_properties=props, + accessed_indices=set(), + is_iterated=False, + is_conditional=False, + iteration_source=None, + iteration_target=None, + ) + for var, props in variables.items() + } + + for var, indices in self.index_access.items(): + if var in analysis: + analysis[var]["accessed_indices"] = indices + + def visit_node(node): + if isinstance(node, nodes.If): + + def find_test_vars(test_node): + if isinstance(test_node, nodes.Name): + if test_node.name in analysis: + analysis[test_node.name]["is_conditional"] = True + for child in test_node.iter_child_nodes(): + find_test_vars(child) + + find_test_vars(node.test) + + if isinstance(node, nodes.For): + iter_target = self._get_base_name(node.iter) + target_name = self._get_target_name(node.target) + if iter_target in analysis: + analysis[iter_target]["is_iterated"] = True + if target_name: + analysis[iter_target]["iteration_target"] = target_name + if isinstance(target_name, str) and target_name not in analysis: + analysis[target_name] = { + "accessed_properties": set(), + "is_iterated": False, + "is_conditional": False, + "iteration_source": iter_target, + "iteration_target": None, + } + + for child in node.iter_child_nodes(): + visit_node(child) + + visit_node(self.ast) + return analysis + + def get_downstream_properties(self, start_var: str) -> Dict[str, Set[str]]: + """ + Get all properties accessed on a variable and its downstream assignments. + + Args: + start_var: The starting variable to trace + + Returns: + Dict mapping variable names to their accessed properties + """ + visited = set() + properties = {} + + def trace_variable(var_name: str): + if var_name in visited: + return + visited.add(var_name) + + # Get direct properties + if var_name in self.property_access: + properties[var_name] = self.property_access[var_name] + + # Get properties from iteration targets + if var_name in self.iteration_targets: + target = self.iteration_targets[var_name] + if isinstance(target, str): + trace_variable(target) + elif isinstance(target, list): + for t in target: + trace_variable(t) + + # Follow assignments + for target, source in self.variable_assignments.items(): + if source == var_name: + trace_variable(target) + + # Check for array slicing + analysis = self.analyze_template() + if var_name in analysis: + var_info = analysis[var_name] + if var_info["accessed_indices"]: + # If this variable is sliced, follow the resulting assignment + slice_result = f"{var_name}_slice" + if slice_result in self.property_access: + trace_variable(slice_result) + + trace_variable(start_var) + return properties + + def get_message_vars(self, field_messages: str = "messages") -> Set[str]: + """ + Get all properties accessed on messages and derived variables. + """ + all_properties = self.get_downstream_properties(field_messages) + + # Combine all properties from all related variables + combined_properties = set() + for properties in all_properties.values(): + combined_properties.update(properties) + + # Also include properties from the message iteration variable + analysis = self.analyze_template() + if "message" in analysis: + combined_properties.update(analysis["message"]["accessed_properties"]) + + return combined_properties diff --git a/src/axolotl/prompt_strategies/messages/chat.py b/src/axolotl/prompt_strategies/messages/chat.py index 35d7649026..52124407e8 100644 --- a/src/axolotl/prompt_strategies/messages/chat.py +++ b/src/axolotl/prompt_strategies/messages/chat.py @@ -51,8 +51,13 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): ds_cfg = ds_cfg or {} field_messages = ds_cfg.get("field_messages") - message_field_role = ds_cfg.get("message_field_role") - message_field_content = ds_cfg.get("message_field_content") + message_property_mappings = ds_cfg.get("message_property_mappings") + message_field_role = ( + message_property_mappings.get("role") if message_property_mappings else None + ) + message_field_content = ( + message_property_mappings.get("content") if message_property_mappings else None + ) message_field_training = ds_cfg.get("message_field_training") builder_kwargs = {} diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 41385bb3b5..f435bb858e 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -38,7 +38,7 @@ def get_chat_template( user_choice: str, jinja_template: Optional[str] = None, tokenizer: Optional["PreTrainedTokenizerBase"] = None, -): +) -> str: """ Finds the correct chat_template based on the user's choice, jinja_template, and tokenizer. @@ -70,7 +70,7 @@ def get_chat_template( f"`chat_template choice is {_DEFAULT_TEMPLATE_CHOICE} but tokenizer's chat_template is null. " f"Please add a chat_template in tokenizer config" ) - return tokenizer.chat_template + return tokenizer.chat_template # type: ignore if user_choice.startswith(_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX): if not tokenizer: @@ -78,7 +78,7 @@ def get_chat_template( f"`tokenizer` cannot be None when chat_template choice starts with {_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX}" ) if tokenizer.chat_template: - return tokenizer.chat_template + return tokenizer.chat_template # type: ignore user_choice = user_choice[ len(_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX) : diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 767bf659af..421f3b6493 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -18,6 +18,7 @@ from axolotl.utils.config.models.input.v0_4_1 import ( AxolotlInputConfig as AxolotlInputConfigBase, ) +from axolotl.utils.config.models.input.v0_4_1 import DPODataset, KTODataset, SFTDataset from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_model_config @@ -258,7 +259,7 @@ def validate_config( cfg: DictDefault, capabilities: Optional[dict] = None, env_capabilities: Optional[dict] = None, -): +) -> DictDefault: AxolotlConfigWCapabilities = AxolotlConfigWCapabilitiesBase AxolotlInputConfig = AxolotlInputConfigBase @@ -268,6 +269,16 @@ def validate_config( AxolotlInputConfig, # pylint: disable=invalid-name ) = merge_input_args() + # Convert datasets to proper format if needed + if cfg.get("datasets"): + for idx, ds_cfg in enumerate(cfg["datasets"]): + if cfg.get("rl") == "dpo" and not isinstance(ds_cfg, DPODataset): + cfg["datasets"][idx] = DPODataset(**ds_cfg) + elif cfg.get("rl") == "kto" and not isinstance(ds_cfg, KTODataset): + cfg["datasets"][idx] = KTODataset(**dict(ds_cfg)) + elif not isinstance(ds_cfg, SFTDataset): + cfg["datasets"][idx] = SFTDataset(**dict(ds_cfg)) + if capabilities or env_capabilities: if (capabilities and env_capabilities is None) or ( env_capabilities and capabilities is None diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 38204d02d2..ab451e6de5 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -12,6 +12,7 @@ Field, StringConstraints, conlist, + field_serializer, field_validator, model_validator, ) @@ -186,8 +187,13 @@ class SFTDataset(BaseModel): field_human: Optional[str] = None field_model: Optional[str] = None field_messages: Optional[str] = None - message_field_role: Optional[str] = None - message_field_content: Optional[str] = None + message_field_role: Optional[ + str + ] = None # deprecated, use message_property_mappings + message_field_content: Optional[ + str + ] = None # deprecated, use message_property_mappings + message_property_mappings: Optional[Dict[str, str]] = None message_field_training: Optional[str] = None message_field_training_detail: Optional[str] = None logprobs_field: Optional[str] = None @@ -199,9 +205,18 @@ class SFTDataset(BaseModel): trust_remote_code: Optional[bool] = False revision: Optional[str] = None + @model_validator(mode="before") + @classmethod + def handle_legacy_message_fields(cls, data): + """Handle backwards compatibility between legacy message field mapping and new property mapping system.""" + return handle_legacy_message_fields_logic(data) + @model_validator(mode="before") @classmethod def check_chat_template_config(cls, data): + if isinstance(data, BaseModel): + data = data.model_dump() + # Set chat_template to tokenizer_default if not set if data.get("type") == "chat_template" and not data.get("chat_template"): data["chat_template"] = ChatTemplate.tokenizer_default @@ -241,6 +256,7 @@ class DPODataset(BaseModel): type: Optional[Union[UserDefinedDPOType, str]] = None data_files: Optional[List[str]] = None revision: Optional[str] = None + field_messages: Optional[str] = None class StepwiseSupervisedDataset(BaseModel): @@ -277,6 +293,9 @@ class KTODataset(BaseModel): revision: Optional[str] = None +DatasetConfig = Union[SFTDataset, DPODataset, KTODataset, StepwiseSupervisedDataset] + + class LoftQConfig(BaseModel): """LoftQ configuration subset""" @@ -680,17 +699,15 @@ class Config: ] = None # whether to use weighting in DPO trainer. If none, default is false in the trainer. dpo_use_logits_to_keep: Optional[bool] = None - datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset, StepwiseSupervisedDataset], min_length=1)] = None # type: ignore - test_datasets: Optional[conlist(Union[SFTDataset, DPODataset, KTODataset, StepwiseSupervisedDataset], min_length=1)] = None # type: ignore + datasets: Optional[conlist(DatasetConfig, min_length=1)] = None # type: ignore + test_datasets: Optional[conlist(DatasetConfig, min_length=1)] = None # type: ignore shuffle_merged_datasets: Optional[bool] = True dataset_prepared_path: Optional[str] = None dataset_shard_num: Optional[int] = None dataset_shard_idx: Optional[int] = None skip_prepare_dataset: Optional[bool] = False - pretraining_dataset: Optional[ # type: ignore - conlist(Union[PretrainingDataset, SFTDataset], min_length=1) - ] = Field( + pretraining_dataset: Optional[conlist(Union[PretrainingDataset, SFTDataset], min_length=1)] = Field( # type: ignore default=None, json_schema_extra={"description": "streaming dataset to use for pretraining"}, ) @@ -895,10 +912,15 @@ class Config: @classmethod def deprecate_sharegpt_datasets(cls, datasets): for _, ds_cfg in enumerate(datasets): - if not ds_cfg.get("type"): + # Handle both dict and pydantic model cases + ds_type = ( + ds_cfg.get("type") + if isinstance(ds_cfg, dict) + else getattr(ds_cfg, "type", None) + ) + if not ds_type: continue - ds_type = ds_cfg["type"] # skip if it's a dict (for custom user instruction prompt) if isinstance(ds_type, dict): continue @@ -910,6 +932,14 @@ def deprecate_sharegpt_datasets(cls, datasets): return datasets + @field_serializer("datasets") + def datasets_serializer( + self, ds_configs: Optional[List[DatasetConfig]] + ) -> Optional[List[Dict[str, Any]]]: + if ds_configs: + return [ds_config.model_dump(exclude_none=True) for ds_config in ds_configs] + return None + @model_validator(mode="before") @classmethod def check_batch_size_fields(cls, data): @@ -1762,3 +1792,77 @@ def check_torch_compile_auto(cls, data): else: data["torch_compile"] = False return data + + +def handle_legacy_message_fields_logic(data: dict) -> dict: + """ + Handle backwards compatibility between legacy message field mapping and new property mapping system. + + Previously, the config only supported mapping 'role' and 'content' fields via dedicated config options: + - message_field_role: Mapped to the role field + - message_field_content: Mapped to the content field + + The new system uses message_property_mappings to support arbitrary field mappings: + message_property_mappings: + role: source_role_field + content: source_content_field + additional_field: source_field + + Args: + data: Dictionary containing configuration data + + Returns: + Updated dictionary with message field mappings consolidated + + Raises: + ValueError: If there are conflicts between legacy and new mappings + """ + data = data.copy() # Create a copy to avoid modifying the original + + if data.get("message_property_mappings") is None: + data["message_property_mappings"] = {} + + # Check for conflicts and handle role + if "message_field_role" in data: + LOG.warning( + "message_field_role is deprecated, use message_property_mappings instead. " + f"Example: message_property_mappings: {{role: {data['message_field_role']}}}" + ) + if ( + "role" in data["message_property_mappings"] + and data["message_property_mappings"]["role"] != data["message_field_role"] + ): + raise ValueError( + f"Conflicting message role fields: message_field_role='{data['message_field_role']}' " + f"conflicts with message_property_mappings.role='{data['message_property_mappings']['role']}'" + ) + data["message_property_mappings"]["role"] = data["message_field_role"] or "role" + + del data["message_field_role"] + elif "role" not in data["message_property_mappings"]: + data["message_property_mappings"]["role"] = "role" + + # Check for conflicts and handle content + if "message_field_content" in data: + LOG.warning( + "message_field_content is deprecated, use message_property_mappings instead. " + f"Example: message_property_mappings: {{content: {data['message_field_content']}}}" + ) + if ( + "content" in data["message_property_mappings"] + and data["message_property_mappings"]["content"] + != data["message_field_content"] + ): + raise ValueError( + f"Conflicting message content fields: message_field_content='{data['message_field_content']}' " + f"conflicts with message_property_mappings.content='{data['message_property_mappings']['content']}'" + ) + data["message_property_mappings"]["content"] = ( + data["message_field_content"] or "content" + ) + + del data["message_field_content"] + elif "content" not in data["message_property_mappings"]: + data["message_property_mappings"]["content"] = "content" + + return data diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 52ca91365c..704c020219 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -180,6 +180,7 @@ def load_tokenized_prepared_datasets( ) -> Tuple[DatasetDict, List[Prompter]]: cfg_datasets = cfg.test_datasets if split == "test" else cfg.datasets tokenizer_name = cfg.tokenizer_config + ds_hash = str( md5( ( diff --git a/src/axolotl/utils/dict.py b/src/axolotl/utils/dict.py index 409d088e6d..f24f7c4a98 100644 --- a/src/axolotl/utils/dict.py +++ b/src/axolotl/utils/dict.py @@ -13,3 +13,26 @@ def __missing__(self, key): def __or__(self, other): return DictDefault(super().__ror__(other)) + + def __setitem__(self, name, value): + # workaround for pickle/unpickle issues and __frozen not being available + try: + isFrozen = hasattr( # pylint: disable=invalid-name + self, "__frozen" + ) and object.__getattribute__(self, "__frozen") + except AttributeError: + isFrozen = False # pylint: disable=invalid-name + + if isFrozen and name not in super().keys(): + raise KeyError(name) + super(Dict, self).__setitem__(name, value) # pylint: disable=bad-super-call + try: + p = object.__getattribute__(self, "__parent") + key = object.__getattribute__(self, "__key") + except AttributeError: + p = None + key = None + if p is not None: + p[key] = self + object.__delattr__(self, "__parent") + object.__delattr__(self, "__key") diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py index e885d3b48c..a90b48d67c 100644 --- a/tests/e2e/integrations/test_kd.py +++ b/tests/e2e/integrations/test_kd.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config, prepare_plugins +from axolotl.utils.config import normalize_config, prepare_plugins, validate_config from axolotl.utils.dict import DictDefault @@ -79,6 +79,7 @@ class TestKnowledgeDistillation: def test_llama_kd(self, temp_dir, kd_min_cfg): cfg = DictDefault(kd_min_cfg) # pylint: disable=duplicate-code + cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() @@ -109,6 +110,7 @@ def test_llama_lora_kd(self, temp_dir, kd_min_cfg, load_in_8bit): | kd_min_cfg ) # pylint: disable=duplicate-code + cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index 2bfd36d155..f71e4fb4af 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -11,7 +11,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists, check_tensorboard @@ -76,7 +76,9 @@ def test_lora_packing_fa_cross_entropy(self, temp_dir, gradient_accumulation_ste else: cfg.fp16 = True + cfg = validate_config(cfg) normalize_config(cfg) + cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/solo/test_relora_llama.py b/tests/e2e/solo/test_relora_llama.py index 191f76f647..53a239c517 100644 --- a/tests/e2e/solo/test_relora_llama.py +++ b/tests/e2e/solo/test_relora_llama.py @@ -10,7 +10,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists, check_tensorboard, with_temp_dir @@ -73,6 +73,8 @@ def test_relora(self, temp_dir): "use_tensorboard": True, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index 2d0baceeef..cf7335805c 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -12,7 +12,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_preference_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, with_temp_dir @@ -63,6 +63,8 @@ def test_dpo_lora(self, temp_dir): "gradient_checkpointing_kwargs": {"use_reentrant": True}, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) @@ -108,6 +110,8 @@ def test_dpo_nll_lora(self, temp_dir): "gradient_checkpointing_kwargs": {"use_reentrant": True}, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) @@ -153,6 +157,8 @@ def test_dpo_use_weighting(self, temp_dir): "gradient_checkpointing_kwargs": {"use_reentrant": True}, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) @@ -198,6 +204,8 @@ def test_kto_pair_lora(self, temp_dir): "gradient_checkpointing_kwargs": {"use_reentrant": True}, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) @@ -242,6 +250,8 @@ def test_ipo_lora(self, temp_dir): "gradient_checkpointing_kwargs": {"use_reentrant": True}, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) @@ -289,6 +299,8 @@ def test_orpo_lora(self, temp_dir): "gradient_checkpointing_kwargs": {"use_reentrant": True}, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) @@ -353,6 +365,8 @@ def test_kto_lora(self, temp_dir): "gradient_checkpointing_kwargs": {"use_reentrant": True}, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py index a1e9b5d46b..687b6637fc 100644 --- a/tests/e2e/test_embeddings_lr.py +++ b/tests/e2e/test_embeddings_lr.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, check_tensorboard, with_temp_dir @@ -56,6 +56,8 @@ def test_train_w_embedding_lr_scale(self, temp_dir): "use_tensorboard": True, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_falcon.py b/tests/e2e/test_falcon.py index 738a0e0b0c..3c325459b8 100644 --- a/tests/e2e/test_falcon.py +++ b/tests/e2e/test_falcon.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, with_temp_dir @@ -65,6 +65,8 @@ def test_lora(self, temp_dir): "bf16": "auto", } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -118,6 +120,8 @@ def test_lora_added_vocab(self, temp_dir): "bf16": "auto", } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -157,6 +161,8 @@ def test_ft(self, temp_dir): "bf16": "auto", } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index a948284904..77e70d8c24 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -10,7 +10,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault LOG = logging.getLogger("axolotl.tests.e2e") @@ -56,6 +56,8 @@ def test_fft_trust_remote_code(self, temp_dir): "save_safetensors": True, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -99,6 +101,8 @@ def test_fix_untrained_tokens(self, temp_dir): "save_safetensors": True, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -138,6 +142,8 @@ def test_batch_flattening(self, temp_dir): "save_safetensors": True, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index 5cd1c693af..647285e464 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -10,7 +10,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, check_tensorboard @@ -69,6 +69,8 @@ def test_pretrain(self, temp_dir, sample_packing, pretrain_multipack_attn): "use_tensorboard": True, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py index 91f101e44c..c4a41f521d 100644 --- a/tests/e2e/test_llama_vision.py +++ b/tests/e2e/test_llama_vision.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, with_temp_dir @@ -62,6 +62,8 @@ def test_lora_llama_vision_text_only_dataset(self, temp_dir): "bf16": True, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index 8ebedf2763..d314fb1977 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, with_temp_dir @@ -59,6 +59,8 @@ def test_lora(self, temp_dir): "max_steps": 20, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_mamba.py b/tests/e2e/test_mamba.py index 8cadf49320..f49b53987d 100644 --- a/tests/e2e/test_mamba.py +++ b/tests/e2e/test_mamba.py @@ -11,7 +11,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, with_temp_dir @@ -59,6 +59,8 @@ def test_fft(self, temp_dir): "save_safetensors": False, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_mistral.py b/tests/e2e/test_mistral.py index 17732d8798..2468a45e94 100644 --- a/tests/e2e/test_mistral.py +++ b/tests/e2e/test_mistral.py @@ -11,7 +11,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, with_temp_dir @@ -63,6 +63,8 @@ def test_lora(self, temp_dir): "eval_steps": 10, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -106,6 +108,8 @@ def test_ft(self, temp_dir): cfg.bf16 = True else: cfg.fp16 = True + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_mixtral.py b/tests/e2e/test_mixtral.py index 6e06626f6e..5de5ab4036 100644 --- a/tests/e2e/test_mixtral.py +++ b/tests/e2e/test_mixtral.py @@ -12,7 +12,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, with_temp_dir @@ -69,6 +69,8 @@ def test_qlora_w_fa2(self, temp_dir): "eval_steps": 10, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -123,6 +125,8 @@ def test_qlora_wo_fa2(self, temp_dir): "eval_steps": 10, } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -180,6 +184,8 @@ def test_16bit_lora_w_fa2(self, temp_dir): cfg.bf16 = True else: cfg.fp16 = True + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -233,6 +239,8 @@ def test_16bit_lora_wo_fa2(self, temp_dir): "eval_steps": 10, } ) + + cfg = validate_config(cfg) normalize_config(cfg) if is_torch_bf16_gpu_available(): cfg.bf16 = True @@ -281,6 +289,8 @@ def test_ft(self, temp_dir): cfg.bf16 = True else: cfg.fp16 = True + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 453872538a..4b0ad1142a 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, require_torch_2_5_1, with_temp_dir @@ -59,6 +59,8 @@ def test_optimi_adamw(self, temp_dir): "lr_scheduler": "cosine", } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -103,6 +105,8 @@ def test_adopt_adamw(self, temp_dir): "lr_scheduler": "cosine", } ) + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -139,6 +143,8 @@ def test_fft_schedule_free_adamw(self, temp_dir): } ) # pylint: disable=duplicate-code + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index a363b11b2c..30054962c9 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -11,7 +11,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_tensorboard, with_temp_dir @@ -59,6 +59,8 @@ def test_loss_packed(self, temp_dir): cfg.bf16 = True else: cfg.fp16 = True + + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_phi.py b/tests/e2e/test_phi.py index 54f564d0e7..49f9261c9f 100644 --- a/tests/e2e/test_phi.py +++ b/tests/e2e/test_phi.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, with_temp_dir @@ -61,6 +61,7 @@ def test_phi_ft(self, temp_dir): "bf16": "auto", } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_qwen.py b/tests/e2e/test_qwen.py index 7a343f4d3f..39d55603f5 100644 --- a/tests/e2e/test_qwen.py +++ b/tests/e2e/test_qwen.py @@ -40,8 +40,10 @@ def test_dpo(self, base_model, temp_dir): "field_messages": "conversation", "field_chosen": "chosen", "field_rejected": "rejected", - "message_field_role": "role", - "message_field_content": "content", + "message_property_mappings": { + "role": "role", + "content": "content", + }, "roles": { "system": ["system"], "user": ["user"], diff --git a/tests/e2e/test_reward_model_smollm2.py b/tests/e2e/test_reward_model_smollm2.py index d123e60612..240c4b3924 100644 --- a/tests/e2e/test_reward_model_smollm2.py +++ b/tests/e2e/test_reward_model_smollm2.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, check_tensorboard, with_temp_dir @@ -66,6 +66,7 @@ def test_rm_lora(self, temp_dir): "use_tensorboard": True, } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index fdfcbff438..9864a6fecd 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -7,6 +7,7 @@ from huggingface_hub import hf_hub_download from transformers import AutoTokenizer +from axolotl.prompt_strategies.jinja_template_analyzer import JinjaTemplateAnalyzer from axolotl.utils.chat_templates import _CHAT_TEMPLATES @@ -174,3 +175,32 @@ def fixture_llama3_2_vision_with_hardcoded_date() -> str: modified_template = template.replace(old_date_logic, new_date_logic) return modified_template + + +@pytest.fixture(name="chat_template_jinja_with_optional_fields") +def fixture_chat_template_jinja_with_optional_fields() -> str: + return """{% for message in messages %} +{{'<|im_start|>'}}{{ message['role'] }} +{% if message['thoughts'] is defined %}[Thoughts: {{ message['thoughts'] }}]{% endif %} +{% if message['tool_calls'] is defined %}[Tool: {{ message['tool_calls'][0]['type'] }}]{% endif %} +{{ message['content'] }}{{'<|im_end|>'}} +{% endfor %}""" + + +@pytest.fixture(name="basic_jinja_template_analyzer") +def basic_jinja_template_analyzer(): + return JinjaTemplateAnalyzer( + """{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|> +' + message['content'] + '<|end|> +'}}{% elif message['role'] == 'user' %}{{'<|user|> +' + message['content'] + '<|end|> +'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|> +' + message['content'] + '<|end|> +'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|> +' }}{% else %}{{ eos_token }}{% endif %}""" + ) + + +@pytest.fixture(name="mistral_jinja_template_analyzer") +def mistral_jinja_template_analyzer(mistralv03_tokenizer_chat_template_jinja): + return JinjaTemplateAnalyzer(mistralv03_tokenizer_chat_template_jinja) diff --git a/tests/prompt_strategies/test_chat_templates.py b/tests/prompt_strategies/test_chat_templates.py index 8ec4fa1191..68772b56b3 100644 --- a/tests/prompt_strategies/test_chat_templates.py +++ b/tests/prompt_strategies/test_chat_templates.py @@ -38,6 +38,10 @@ def test_llama3_load(self, llama3_tokenizer, assistant_dataset): "chat_template": "llama3", "message_field_role": "role", "message_field_content": "content", + "message_property_mappings": { + "role": "role", + "content": "content", + }, "roles": { "user": ["user"], "assistant": ["assistant"], @@ -74,8 +78,10 @@ def test_llama3(self, llama3_tokenizer, assistant_dataset): ChatTemplatePrompter( llama3_tokenizer, chat_template=get_chat_template("llama3"), - message_field_role="role", - message_field_content="content", + message_property_mappings={ + "role": "role", + "content": "content", + }, roles={ "user": ["user"], "assistant": ["assistant"], @@ -86,7 +92,7 @@ def test_llama3(self, llama3_tokenizer, assistant_dataset): train_on_inputs=False, sequence_len=512, ) - strategy.messages = "messages" + res = strategy.tokenize_prompt(assistant_dataset[0]) input_ids = res["input_ids"] # fmt: off @@ -114,8 +120,10 @@ def test_phi35(self, phi35_tokenizer, assistant_dataset): ChatTemplatePrompter( phi35_tokenizer, chat_template=get_chat_template("phi_35"), - message_field_role="role", - message_field_content="content", + message_property_mappings={ + "role": "role", + "content": "content", + }, roles={ "user": ["user"], "assistant": ["assistant"], @@ -126,7 +134,7 @@ def test_phi35(self, phi35_tokenizer, assistant_dataset): train_on_inputs=False, sequence_len=512, ) - strategy.messages = "messages" + res = strategy.tokenize_prompt(assistant_dataset[0]) input_ids = res["input_ids"] labels = res["labels"] @@ -170,9 +178,11 @@ def test_llama3_with_training_data(self, llama3_tokenizer, assistant_dataset): ChatTemplatePrompter( llama3_tokenizer, chat_template=get_chat_template("llama3"), - message_field_role="role", - message_field_content="content", message_field_training="training", + message_property_mappings={ + "role": "role", + "content": "content", + }, roles={ "user": ["user"], "assistant": ["assistant"], @@ -185,7 +195,7 @@ def test_llama3_with_training_data(self, llama3_tokenizer, assistant_dataset): sequence_len=512, roles_to_train=["assistant"], ) - strategy.messages = "messages" + prompt_tokens = strategy.prompter.build_prompt( assistant_dataset[0]["messages"], False ) @@ -230,8 +240,11 @@ def test_llama3_assistant(self, llama3_tokenizer, sharegpt_dataset): ChatTemplatePrompter( llama3_tokenizer, chat_template=get_chat_template("llama3"), - message_field_role="from", - message_field_content="value", + message_property_mappings={ + "role": "from", + "content": "value", + }, + field_messages="conversations", ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -239,7 +252,7 @@ def test_llama3_assistant(self, llama3_tokenizer, sharegpt_dataset): sequence_len=512, roles_to_train=["gpt"], ) - strategy.messages = "conversations" + res = strategy.tokenize_prompt(sharegpt_dataset[0]) input_ids = res["input_ids"] labels = res["labels"] @@ -287,8 +300,11 @@ def test_llama3_human(self, llama3_tokenizer, sharegpt_dataset): ChatTemplatePrompter( llama3_tokenizer, chat_template=get_chat_template("llama3"), - message_field_role="from", - message_field_content="value", + message_property_mappings={ + "role": "from", + "content": "value", + }, + field_messages="conversations", ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -296,7 +312,7 @@ def test_llama3_human(self, llama3_tokenizer, sharegpt_dataset): sequence_len=512, roles_to_train=["human"], ) - strategy.messages = "conversations" + res = strategy.tokenize_prompt(sharegpt_dataset[0]) input_ids = res["input_ids"] labels = res["labels"] @@ -344,8 +360,11 @@ def test_llama3_system_human(self, llama3_tokenizer, basic_dataset): ChatTemplatePrompter( llama3_tokenizer, chat_template=get_chat_template("llama3"), - message_field_role="from", - message_field_content="value", + message_property_mappings={ + "role": "from", + "content": "value", + }, + field_messages="conversations", ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -353,7 +372,7 @@ def test_llama3_system_human(self, llama3_tokenizer, basic_dataset): sequence_len=512, roles_to_train=["system", "human"], ) - strategy.messages = "conversations" + res = strategy.tokenize_prompt(basic_dataset[0]) input_ids = res["input_ids"] labels = res["labels"] @@ -417,8 +436,7 @@ def test_llama32vision_train_on_assistant( chat_template=get_chat_template( "jinja", jinja_template=llama3_2_vision_chat_template_jinja ), - message_field_role="role", - message_field_content="content", + message_property_mappings={"role": "role", "content": "content"}, ), tokenizer=llama3_tokenizer, train_on_inputs=False, @@ -486,8 +504,7 @@ def test_llama32vision_train_on_tools( chat_template=get_chat_template( "jinja", jinja_template=llama3_2_vision_chat_template_jinja ), - message_field_role="role", - message_field_content="content", + message_property_mappings={"role": "role", "content": "content"}, ), tokenizer=llama3_tokenizer, train_on_inputs=False, diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index 7d09b059cc..7f3096eb06 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -3,7 +3,6 @@ """ import logging -import unittest from copy import deepcopy import pytest @@ -123,15 +122,15 @@ def test_train_on_inputs_true( chat_template=get_chat_template( chat_template, jinja_template=chat_template_jinja ), - message_field_role="from", - message_field_content="value", + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", ), tokenizer=tokenizer, train_on_inputs=True, sequence_len=512, roles_to_train=["assistant"], ) - strategy.messages = "conversations" + res = strategy.tokenize_prompt(basic_dataset[0]) turns = strategy.get_conversation_thread(basic_dataset[0]) labels = res["labels"] @@ -180,15 +179,15 @@ def test_train_on_inputs_false( chat_template=get_chat_template( chat_template, jinja_template=chat_template_jinja ), - message_field_role="from", - message_field_content="value", + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", ), tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, roles_to_train=["assistant"], ) - strategy.messages = "conversations" + res = strategy.tokenize_prompt(basic_dataset[0]) turns = strategy.get_conversation_thread(basic_dataset[0]) labels = res["labels"] @@ -241,20 +240,15 @@ def test_roles_to_train_human_assistant_only( chat_template=get_chat_template( chat_template, jinja_template=chat_template_jinja ), - message_field_role="from", - message_field_content="value", + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", ), tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, roles_to_train=["assistant", "human"], ) - strategy.messages = "conversations" - res = strategy.tokenize_prompt(basic_dataset[0]) - labels = res["labels"] - input_ids = res["input_ids"] - strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) turns = strategy.get_conversation_thread(basic_dataset[0]) labels = res["labels"] @@ -307,15 +301,15 @@ def test_roles_to_train_all( chat_template=get_chat_template( chat_template, jinja_template=chat_template_jinja ), - message_field_role="from", - message_field_content="value", + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", ), tokenizer=tokenizer, train_on_inputs=True, sequence_len=512, roles_to_train=["human", "assistant"], ) - strategy.messages = "conversations" + res = strategy.tokenize_prompt(basic_dataset[0]) turns = strategy.get_conversation_thread(basic_dataset[0]) labels = res["labels"] @@ -360,8 +354,8 @@ def test_empty_roles_to_train( chat_template=get_chat_template( chat_template, jinja_template=chat_template_jinja ), - message_field_role="from", - message_field_content="value", + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", ), tokenizer=tokenizer, train_on_inputs=False, @@ -369,7 +363,7 @@ def test_empty_roles_to_train( roles_to_train=[], train_on_eos="none", # Add this line ) - strategy.messages = "conversations" + res = strategy.tokenize_prompt(basic_dataset[0]) labels = res["labels"] @@ -400,8 +394,8 @@ def test_train_on_eos_all( chat_template=get_chat_template( chat_template, jinja_template=chat_template_jinja ), - message_field_role="from", - message_field_content="value", + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", ), tokenizer=tokenizer, train_on_inputs=False, @@ -409,7 +403,7 @@ def test_train_on_eos_all( roles_to_train=["assistant"], train_on_eos="all", ) - strategy.messages = "conversations" + res = strategy.tokenize_prompt(basic_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] @@ -446,8 +440,8 @@ def test_train_on_eos_turn( chat_template=get_chat_template( chat_template, jinja_template=chat_template_jinja ), - message_field_role="from", - message_field_content="value", + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", ), tokenizer=tokenizer, train_on_inputs=False, @@ -455,7 +449,6 @@ def test_train_on_eos_turn( roles_to_train=["assistant"], train_on_eos="turn", ) - strategy.messages = "conversations" res = strategy.tokenize_prompt(basic_dataset[0]) turns = strategy.get_conversation_thread(basic_dataset[0]) labels = res["labels"] @@ -526,8 +519,8 @@ def test_train_on_eos_last( chat_template=get_chat_template( chat_template, jinja_template=chat_template_jinja ), - message_field_role="from", - message_field_content="value", + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", ), tokenizer=tokenizer, train_on_inputs=False, @@ -535,7 +528,7 @@ def test_train_on_eos_last( roles_to_train=["assistant"], train_on_eos="last", ) - strategy.messages = "conversations" + res = strategy.tokenize_prompt(basic_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] @@ -578,8 +571,8 @@ def test_train_on_eos_none( chat_template=get_chat_template( chat_template, jinja_template=chat_template_jinja ), - message_field_role="from", - message_field_content="value", + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", ), tokenizer=tokenizer, train_on_inputs=False, @@ -587,7 +580,7 @@ def test_train_on_eos_none( roles_to_train=["assistant"], train_on_eos="none", ) - strategy.messages = "conversations" + res = strategy.tokenize_prompt(basic_dataset[0]) labels = res["labels"] input_ids = res["input_ids"] @@ -624,15 +617,15 @@ def test_drop_system_message( chat_template, jinja_template=chat_template_jinja ), drop_system_message=True, - message_field_role="from", - message_field_content="value", + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", ), tokenizer=tokenizer, train_on_inputs=False, sequence_len=512, roles_to_train=["assistant"], ) - strategy.messages = "conversations" + res = strategy.tokenize_prompt(basic_dataset[0]) input_ids = res["input_ids"] @@ -668,8 +661,7 @@ def test_custom_roles( chat_template, jinja_template=chat_template_jinja ), roles=custom_roles, - message_field_role="from", - message_field_content="value", + message_property_mappings={"role": "from", "content": "value"}, ), tokenizer=tokenizer, train_on_inputs=False, @@ -741,8 +733,7 @@ def test_message_field_training( ), message_field_training="train", message_field_training_detail="train_detail", - message_field_role="from", - message_field_content="value", + message_property_mappings={"role": "from", "content": "value"}, ), tokenizer=tokenizer, train_on_inputs=False, @@ -911,6 +902,64 @@ def verify_labels(labels_span, should_train, context_message): LOG.debug(f"Final labels: {labels}") LOG.debug(f"Final input_ids: {input_ids}") + def test_get_chat_template_variables( + self, tokenizer, chat_template, chat_template_jinja, eos_token, request + ): + LOG.info("Testing get_chat_template_variables") -if __name__ == "__main__": - unittest.main() + actual_tokenizer, actual_jinja_template = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + + prompter = ChatTemplatePrompter( + actual_tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=actual_jinja_template + ), + message_property_mappings={"from": "role", "value": "content"}, + ) + + variables = prompter.get_chat_template_msg_variables( + actual_jinja_template + if actual_jinja_template + else actual_tokenizer.get_chat_template(), + "messages", + ) + + if chat_template == "llama3": + assert variables == {"role", "content"}, ( + f"Expected variables: {'role', 'content'} from {tokenizer}/{chat_template}\n" + f"Got: {variables}\n" + f"Chat template: {actual_jinja_template}" + ) + elif chat_template == "chatml": + assert variables == {"role", "content"}, ( + f"Expected variables: {'role', 'content'} from {tokenizer}/{chat_template}\n" + f"Got: {variables}\n" + f"Chat template: {actual_jinja_template}" + ) + elif chat_template == "jinja" and tokenizer == "mistralv03_tokenizer": + assert variables == {"role", "content", "tool_call_id", "tool_calls"}, ( + f"Expected variables: {'role', 'content', 'tool_call_id', 'tool_calls'} from {tokenizer}/{chat_template}\n" + f"Got: {variables}\n" + f"Chat template: {actual_jinja_template}" + ) + elif chat_template == "jinja" and tokenizer == "gemma2_tokenizer": + assert variables == {"role", "content"}, ( + f"Expected variables: {'role', 'content'} from {tokenizer}/{chat_template}\n" + f"Got: {variables}\n" + f"Chat template: {actual_jinja_template}" + ) + elif chat_template == "phi_35": + assert variables == {"role", "content"}, ( + f"Expected variables: {'role', 'content'} from {tokenizer}/{chat_template}\n" + f"Got: {variables}\n" + f"Chat template: {actual_jinja_template}" + ) + else: + LOG.warning( + f"Unsupported chat template: {chat_template} with {chat_template_jinja}" + ) + raise ValueError( + f"Unsupported chat template: {chat_template} with {chat_template_jinja}" + ) diff --git a/tests/prompt_strategies/test_jinja_template_analyzer.py b/tests/prompt_strategies/test_jinja_template_analyzer.py new file mode 100644 index 0000000000..004f810999 --- /dev/null +++ b/tests/prompt_strategies/test_jinja_template_analyzer.py @@ -0,0 +1,159 @@ +""" +tests for jinja_template_analyzer +""" +import logging + +import pytest + +from axolotl.prompt_strategies.jinja_template_analyzer import JinjaTemplateAnalyzer + +logging.basicConfig(level=logging.DEBUG) +LOG = logging.getLogger("axolotl") + + +class TestJinjaTemplateAnalyzer: + """ + tests for jinja_template_analyzer + """ + + def test_basic_variable_extraction(self, basic_jinja_template_analyzer): + """Test that all top-level variables are correctly extracted.""" + LOG.info("Testing with train_on_inputs=True") + + variables = basic_jinja_template_analyzer.get_template_variables() + expected_vars = {"messages", "add_generation_prompt", "eos_token", "message"} + assert set(variables.keys()) == expected_vars + + def test_mixtral_variable_extraction(self, mistral_jinja_template_analyzer): + """Test that all top-level variables are correctly extracted.""" + LOG.info("Testing with train_on_inputs=True") + + variables = mistral_jinja_template_analyzer.get_template_variables() + expected_vars = { + "messages", + "content", + "eos_token", + "message", + "tools", + "system_message", + "loop_messages", + "ns", + "tool_call", + "tool", + "loop", + "bos_token", + "raise_exception", + } + assert set(variables.keys()) == expected_vars + message_vars = variables["message"] + assert message_vars == {"role", "content", "tool_calls", "tool_call_id"} + + def test_message_property_access(self, basic_jinja_template_analyzer): + """Test that properties accessed on 'message' variable are correctly identified.""" + LOG.info("Testing message property access") + + variables = basic_jinja_template_analyzer.get_template_variables() + assert "messages" in variables + assert "message" in variables + assert "role" in variables["message"] + assert "content" in variables["message"] + + def test_detailed_analysis(self, basic_jinja_template_analyzer): + """Test the detailed analysis of variable usage.""" + LOG.info("Testing detailed analysis") + + analysis = basic_jinja_template_analyzer.analyze_template() + + assert analysis["messages"]["is_iterated"] is True + assert "role" in analysis["message"]["accessed_properties"] + assert "content" in analysis["message"]["accessed_properties"] + + assert analysis["add_generation_prompt"]["is_conditional"] is True + assert len(analysis["add_generation_prompt"]["accessed_properties"]) == 0 + + assert not analysis["eos_token"]["is_iterated"] + assert len(analysis["eos_token"]["accessed_properties"]) == 0 + + def test_nested_property_access(self): + """Test handling of nested property access.""" + LOG.info("Testing nested property access") + + template = """{{ user.profile.name }}{{ user.settings['preference'] }}""" + analyzer = JinjaTemplateAnalyzer(template) + variables = analyzer.get_template_variables() + + assert "user" in variables + assert "profile" in variables["user"] + assert "settings" in variables["user"] + + def test_loop_variable_handling(self): + """Test handling of loop variables and their properties.""" + LOG.info("Testing loop variable handling") + + template = """ + {% for item in items %} + {{ item.name }} + {% for subitem in item.subitems %} + {{ subitem.value }} + {% endfor %} + {% endfor %} + """ + analyzer = JinjaTemplateAnalyzer(template) + analysis = analyzer.analyze_template() + + assert analysis["items"]["is_iterated"] + assert "name" in analysis["item"]["accessed_properties"] + assert "subitems" in analysis["item"]["accessed_properties"] + + def test_conditional_variable_usage(self): + """Test detection of variables used in conditional statements.""" + LOG.info("Testing conditional variable usage") + + template = """ + {% if user.is_admin and config.debug_mode %} + {{ debug_info }} + {% endif %} + """ + analyzer = JinjaTemplateAnalyzer(template) + analysis = analyzer.analyze_template() + + assert analysis["user"]["is_conditional"] + assert analysis["config"]["is_conditional"] + assert "is_admin" in analysis["user"]["accessed_properties"] + assert "debug_mode" in analysis["config"]["accessed_properties"] + + def test_complex_expressions(self): + """Test handling of complex expressions and filters.""" + LOG.info("Testing complex expressions and filters") + + template = """ + {{ user.name | upper }} + {{ messages | length > 0 and messages[0].content }} + {{ data['key'].nested['value'] }} + """ + analyzer = JinjaTemplateAnalyzer(template) + variables = analyzer.get_template_variables() + + assert "user" in variables + assert "name" in variables["user"] + assert "messages" in variables + assert "content" in variables["messages"] + assert "data" in variables + + def test_basic_msg_vars(self, basic_jinja_template_analyzer): + """Test that the basic message variables are correctly identified.""" + LOG.info("Testing basic message variables") + + variables = basic_jinja_template_analyzer.get_message_vars() + assert variables == {"role", "content"} + + def test_mixtral_msg_vars(self, mistral_jinja_template_analyzer): + """Test that the mixtral message variables are correctly identified.""" + LOG.info("Testing mixtral message variables") + + variables = mistral_jinja_template_analyzer.get_message_vars() + assert variables == {"role", "content", "tool_calls", "tool_call_id"} + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/tests/test_validation_dataset.py b/tests/test_validation_dataset.py index 89f642051b..5c1b5a1f77 100644 --- a/tests/test_validation_dataset.py +++ b/tests/test_validation_dataset.py @@ -302,3 +302,22 @@ def test_dataset_sharegpt_deprecation(self, minimal_cfg): ) validate_config(cfg) + + def test_message_property_mappings(self, minimal_cfg): + cfg = DictDefault( + minimal_cfg + | { + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + "message_property_mappings": { + "role": "role", + "content": "content", + }, + } + ], + } + ) + + validate_config(cfg) diff --git a/tests/utils/test_models.py b/tests/utils/test_models.py index 31698f05fb..e78cdb5d70 100644 --- a/tests/utils/test_models.py +++ b/tests/utils/test_models.py @@ -76,7 +76,7 @@ def test_cfg_throws_error_with_s2_attention_and_sample_packing(self): mocked_load_model_config.return_value = {} with pytest.raises(ValueError) as exc: # Should error before hitting tokenizer, so we pass in an empty str - load_model(cfg, tokenizer="") + load_model(cfg, tokenizer="") # type: ignore assert ( "shifted-sparse attention does not currently support sample packing" in str(exc.value) @@ -116,3 +116,79 @@ def test_set_quantization_config( assert self.model_loader.model_kwargs.get( "quantization_config", BitsAndBytesConfig ) + + def test_message_property_mapping(self): + """Test message property mapping configuration validation""" + from axolotl.utils.config.models.input.v0_4_1 import SFTDataset + + # Test legacy fields are mapped orrectly + dataset = SFTDataset( + path="test_path", + message_field_role="role_field", + message_field_content="content_field", + ) + assert dataset.message_property_mappings == { + "role": "role_field", + "content": "content_field", + } + + # Test direct message_property_mapping works + dataset = SFTDataset( + path="test_path", + message_property_mappings={ + "role": "custom_role", + "content": "custom_content", + }, + ) + assert dataset.message_property_mappings == { + "role": "custom_role", + "content": "custom_content", + } + + # Test both legacy and new fields work when they match + dataset = SFTDataset( + path="test_path", + message_field_role="same_role", + message_property_mappings={"role": "same_role"}, + ) + assert dataset.message_property_mappings == { + "role": "same_role", + "content": "content", + } + + # Test both legacy and new fields work when they don't overlap + dataset = SFTDataset( + path="test_path", + message_field_role="role_field", + message_property_mappings={"content": "content_field"}, + ) + assert dataset.message_property_mappings == { + "role": "role_field", + "content": "content_field", + } + + # Test no role or content provided + dataset = SFTDataset( + path="test_path", + ) + assert dataset.message_property_mappings == { + "role": "role", + "content": "content", + } + + # Test error when legacy and new fields conflict + with pytest.raises(ValueError) as exc_info: + SFTDataset( + path="test_path", + message_field_role="legacy_role", + message_property_mappings={"role": "different_role"}, + ) + assert "Conflicting message role fields" in str(exc_info.value) + + with pytest.raises(ValueError) as exc_info: + SFTDataset( + path="test_path", + message_field_content="legacy_content", + message_property_mappings={"content": "different_content"}, + ) + assert "Conflicting message content fields" in str(exc_info.value) From 91bb95685af94d24cd7f525e6833ddbe55d36cc2 Mon Sep 17 00:00:00 2001 From: NJordan72 Date: Tue, 18 Feb 2025 03:39:24 -0500 Subject: [PATCH 0426/1405] chore: cleanup deprecated config elements (#2309) * feat: update metadata fields and refactor config class in axolotlinputconfig - Replace `metadata` fields with `json_schema_extra` in RayConfig class. - Replace `Config` class with `ConfigDict` in AxolotlInputConfig. - Set `populate_by_name` to `True` directly in `ConfigDict` instance. * feat: update axolotlinputconfig in utils * Replace `conlist` with `Annotated` for `datasets`, `test_datasets`, and `pretraining_dataset` fields * Change default values for `lr_scheduler` and `optimizer` fields in `HyperparametersConfig` class * Remove unnecessary Union from `evals_per_epoch` field in `AxolotlInputConfig` class * Import `MinLen` from `annotated_types` module * Remove import of `conlist` from `pydantic` module * feat: update modelinputconfig and axolotlinputconfig in v0_4_1 - Removed ConfigDict import from pydantic in `src/axolotl/utils/config/models/input/v0_4_1/__init__.py` - Added `model_config` with `protected_namespaces` to ModelInputConfig - Replaced `config: ConfigDict` with `model_config` in AxolotlInputConfig - Set `populate_by_name` to True in `model_config` for AxolotlInputConfig * chore: get rid of unused import --- .../config/models/input/v0_4_1/__init__.py | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index ab451e6de5..26bfff7dc9 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -6,12 +6,12 @@ from enum import Enum from typing import Annotated, Any, Dict, List, Literal, Optional, Tuple, Union +from annotated_types import MinLen from packaging import version from pydantic import ( BaseModel, Field, StringConstraints, - conlist, field_serializer, field_validator, model_validator, @@ -435,6 +435,8 @@ class ReLoRAConfig(BaseModel): class ModelInputConfig(BaseModel): """model to train on configuration subset""" + model_config = {"protected_namespaces": ()} + base_model: str base_model_config: Optional[str] = None cls_model_config: Optional[str] = None @@ -501,7 +503,7 @@ class HyperparametersConfig(BaseModel): "adopt_adamw", ], ] - ] = OptimizerNames.ADAMW_HF.value + ] = OptimizerNames.ADAMW_HF optim_args: Optional[Union[str, Dict[str, Any]]] = Field( default=None, json_schema_extra={"description": "Optional arguments to supply to optimizer."}, @@ -513,7 +515,9 @@ class HyperparametersConfig(BaseModel): }, ) torchdistx_path: Optional[str] = None - lr_scheduler: Optional[Union[SchedulerType, Literal["one_cycle"]]] = "cosine" + lr_scheduler: Optional[ + Union[SchedulerType, Literal["one_cycle"]] + ] = SchedulerType.COSINE lr_scheduler_kwargs: Optional[Dict[str, Any]] = None lr_quadratic_warmup: Optional[bool] = None cosine_min_lr_ratio: Optional[float] = None @@ -637,19 +641,19 @@ class RayConfig(BaseModel): use_ray: bool = Field(default=False) ray_run_name: Optional[str] = Field( default=None, - metadata={ + json_schema_extra={ "help": "The training results will be saved at `saves/ray_run_name`." }, ) ray_num_workers: int = Field( default=1, - metadata={ + json_schema_extra={ "help": "The number of workers for Ray training. Default is 1 worker." }, ) resources_per_worker: dict = Field( default_factory=lambda: {"GPU": 1}, - metadata={ + json_schema_extra={ "help": "The resources per worker for Ray training. Default is to use 1 GPU per worker." }, ) @@ -674,10 +678,7 @@ class AxolotlInputConfig( ): """wrapper of all config options""" - class Config: - """Config for alias""" - - populate_by_name = True + model_config = {"populate_by_name": True} strict: Optional[bool] = Field(default=False) resume_from_checkpoint: Optional[str] = None @@ -699,15 +700,28 @@ class Config: ] = None # whether to use weighting in DPO trainer. If none, default is false in the trainer. dpo_use_logits_to_keep: Optional[bool] = None - datasets: Optional[conlist(DatasetConfig, min_length=1)] = None # type: ignore - test_datasets: Optional[conlist(DatasetConfig, min_length=1)] = None # type: ignore + datasets: Optional[ + Annotated[ + list[Union[SFTDataset, DPODataset, KTODataset, StepwiseSupervisedDataset]], + MinLen(1), + ] + ] = None + + test_datasets: Optional[ + Annotated[ + list[Union[SFTDataset, DPODataset, KTODataset, StepwiseSupervisedDataset]], + MinLen(1), + ] + ] = None shuffle_merged_datasets: Optional[bool] = True dataset_prepared_path: Optional[str] = None dataset_shard_num: Optional[int] = None dataset_shard_idx: Optional[int] = None skip_prepare_dataset: Optional[bool] = False - pretraining_dataset: Optional[conlist(Union[PretrainingDataset, SFTDataset], min_length=1)] = Field( # type: ignore + pretraining_dataset: Optional[ + Annotated[list[Union[PretrainingDataset, SFTDataset]], MinLen(1)] + ] = Field( default=None, json_schema_extra={"description": "streaming dataset to use for pretraining"}, ) @@ -850,7 +864,7 @@ class Config: warmup_steps: Optional[int] = None warmup_ratio: Optional[float] = None eval_steps: Optional[Union[int, float]] = None - evals_per_epoch: Optional[Union[int]] = None + evals_per_epoch: Optional[int] = None eval_strategy: Optional[str] = None save_steps: Optional[Union[int, float]] = None saves_per_epoch: Optional[int] = None From 3c743c4bfbbcb2538b3867ebd4ac7598db3884d0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 18 Feb 2025 04:26:21 -0500 Subject: [PATCH 0427/1405] v0.7.0 for release (#2341) --- src/axolotl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index 8b0ba05320..8f7e0d85cc 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -4,4 +4,4 @@ __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.6.0" +__version__ = "0.7.0" From 7fa690fac8f2a79aff5e32d780a25fd31ef3aa60 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 18 Feb 2025 04:30:59 -0500 Subject: [PATCH 0428/1405] bump dev version (#2342) --- src/axolotl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index 8f7e0d85cc..d3c03cbea2 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -4,4 +4,4 @@ __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.7.0" +__version__ = "0.8.0.dev0" From c3d4f6e29553385d9c3d653bd61c4611e5fb34d7 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Tue, 18 Feb 2025 10:06:31 -0500 Subject: [PATCH 0429/1405] Doc fix: TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL not necessary to use Triton kernel patches (#2343) * removing note about TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL * suggest using TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL for memory efficient attn --- docs/lora_optims.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/lora_optims.qmd b/docs/lora_optims.qmd index a57aa854b1..3f8276bc5b 100644 --- a/docs/lora_optims.qmd +++ b/docs/lora_optims.qmd @@ -82,7 +82,7 @@ lora_o_kernel: true ## Requirements - One or more NVIDIA or AMD GPUs (in order to use the Triton kernels) - - AMD can be used with experimental Triton support by setting the environment variable `TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1` + - Note: Set `TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1` to enable [memory-efficient attention on AMD GPUs](https://github.com/ROCm/aotriton/issues/16#issuecomment-2346675491) - Targeted LoRA adapters cannot use Dropout - This may limit model expressivity / cause overfitting - Targeted LoRA adapters cannot have bias terms From 23a9fcb0a7610db37c47edf56d01c82c705db05f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 18 Feb 2025 16:08:40 -0500 Subject: [PATCH 0430/1405] make sure chatml dpo dataset loading works (#2333) --- tests/prompt_strategies/conftest.py | 6 +++ tests/prompt_strategies/test_dpo_chatml.py | 61 ++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/prompt_strategies/test_dpo_chatml.py diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index 9864a6fecd..a7e4175160 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -125,6 +125,12 @@ def fixture_llama3_tokenizer(): return tokenizer +@pytest.fixture(name="smollm2_tokenizer", scope="session", autouse=True) +def fixture_smollm2_tokenizer(): + tokenizer = AutoTokenizer.from_pretrained("HuggingFaceTB/SmolLM2-135M") + return tokenizer + + @pytest.fixture(name="mistralv03_tokenizer", scope="session", autouse=True) def fixture_mistralv03_tokenizer(): tokenizer = AutoTokenizer.from_pretrained( diff --git a/tests/prompt_strategies/test_dpo_chatml.py b/tests/prompt_strategies/test_dpo_chatml.py new file mode 100644 index 0000000000..34c29275b8 --- /dev/null +++ b/tests/prompt_strategies/test_dpo_chatml.py @@ -0,0 +1,61 @@ +""" +Tests for loading DPO preference datasets with chatml formatting +""" +import unittest + +import pytest + +from axolotl.prompt_strategies.dpo import load as load_dpo +from axolotl.utils.data.rl import load_prepare_preference_datasets +from axolotl.utils.dict import DictDefault + + +@pytest.fixture(name="minimal_dpo_cfg") +def fixture_cfg(): + return DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_config": "HuggingFaceTB/SmolLM2-135M", + "rl": "dpo", + "learning_rate": 0.000001, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "sequence_len": 2048, + } + ) + + +class TestDPOChatml: + """ + Test loading DPO preference datasets with chatml formatting + """ + + def test_default(self, minimal_dpo_cfg): + cfg = DictDefault( + { + "datasets": [ + { + "path": "argilla/distilabel-intel-orca-dpo-pairs", + "type": "chatml", + "split": "train[:1%]", + } + ] + } + | minimal_dpo_cfg + ) + + # test that dpo.load works + load_dpo("chatml", cfg) + # now actually load the datasets with the strategy + train_ds, _ = load_prepare_preference_datasets(cfg) + assert train_ds[0]["prompt"].startswith("<|im_start|>") + assert train_ds[0]["prompt"].endswith("<|im_start|>assistant\n") + assert "chosen" in train_ds[0] + assert "rejected" in train_ds[0] + + +if __name__ == "__main__": + unittest.main() From 8dfadc2b3c7f85e4208b74eb93c32f440c7e25b4 Mon Sep 17 00:00:00 2001 From: Tobias <66688058+tobmi1@users.noreply.github.com> Date: Wed, 19 Feb 2025 06:02:35 +0100 Subject: [PATCH 0431/1405] Fix sample packing producing longer sequences than specified by `sequence_len` (#2332) * Extend MultiPackBatchSampler test to include shorter sequence length and drop long sequences filter * Fix get_dataset_lengths for datasets that were previously filtered (e.g., with drop_long_seq_in_dataset) * Update src/axolotl/utils/samplers/utils.py Fix get_dataset_lengths for datasets that do not have position_ids or length attributes Co-authored-by: NanoCode012 --------- Co-authored-by: NanoCode012 --- src/axolotl/utils/samplers/utils.py | 12 ++++++------ tests/test_packed_batch_sampler.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/axolotl/utils/samplers/utils.py b/src/axolotl/utils/samplers/utils.py index 4e41c9b44d..8f400c98d4 100755 --- a/src/axolotl/utils/samplers/utils.py +++ b/src/axolotl/utils/samplers/utils.py @@ -5,12 +5,12 @@ def get_dataset_lengths(dataset): - if "length" in dataset.data.column_names: - lengths = np.array(dataset.data.column("length")) - elif "position_ids" in dataset.data.column_names: - position_ids = dataset.data.column("position_ids") + if "length" in dataset.column_names: + lengths = np.array(dataset["length"]) + elif "position_ids" in dataset.column_names: + position_ids = dataset["position_ids"] lengths = np.array([x[-1] + 1 for x in position_ids]) else: - input_ids = dataset.data.column("input_ids") - lengths = np.vectorize(len)(np.array(input_ids, dtype=object)) + input_ids = dataset["input_ids"] + lengths = np.array([len(seq) for seq in input_ids]) return lengths diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index ceff11df94..b52320e2aa 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -7,6 +7,7 @@ from axolotl.datasets import TokenizedPromptDataset from axolotl.prompt_strategies.completion import load from axolotl.utils.collators import V2BatchSamplerDataCollatorForSeq2Seq +from axolotl.utils.data.utils import drop_long_seq_in_dataset from axolotl.utils.dict import DictDefault from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths @@ -18,11 +19,6 @@ def fixture_tokenizer(): return tokenizer -@pytest.fixture(name="max_seq_length") -def fixture_max_seq_length(): - return 4096 - - class TestBatchedSamplerPacking: """ Test class for packing streaming dataset sequences @@ -37,6 +33,7 @@ class TestBatchedSamplerPacking: (2, 2), ], ) + @pytest.mark.parametrize("max_seq_length", [4096, 512]) def test_packing(self, batch_size, num_workers, tokenizer, max_seq_length): import axolotl.monkeypatch.data.batch_dataset_fetcher # pylint: disable=unused-import # noqa: F401 @@ -62,6 +59,9 @@ def test_packing(self, batch_size, num_workers, tokenizer, max_seq_length): dataset, ) train_dataset = concatenate_datasets([dataset_wrapper]) + + train_dataset = drop_long_seq_in_dataset(train_dataset, cfg) + lengths = get_dataset_lengths(train_dataset) batch_sampler = MultipackBatchSampler( sampler=RandomSampler(train_dataset), @@ -90,7 +90,7 @@ def test_packing(self, batch_size, num_workers, tokenizer, max_seq_length): batch_idxs.extend(pack) for batch in loader: - assert len(batch["input_ids"]) <= batch_size * max_seq_length + assert batch["input_ids"].numel() <= batch_size * max_seq_length assert batch["input_ids"].shape[1] == max_seq_length original_idxs = set(range(len(train_dataset))) From 954e192f381e1e78d46eb0e53c3fe3610e052ece Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 19 Feb 2025 09:23:31 -0500 Subject: [PATCH 0432/1405] quick formatting fix for LoRA optims doc (#2349) --- docs/lora_optims.qmd | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/lora_optims.qmd b/docs/lora_optims.qmd index 3f8276bc5b..eaeeed260e 100644 --- a/docs/lora_optims.qmd +++ b/docs/lora_optims.qmd @@ -12,6 +12,7 @@ to leverage operator fusion and tensor re-use in order to improve speed and redu memory usage during the forward and backward passes of these calculations. We currently support several common model architectures, including (but not limited to): + - `llama` - `mistral` - `qwen2` From 02f45e94be5f2846ac327c803de287c1ff521c50 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 20 Feb 2025 14:29:58 -0500 Subject: [PATCH 0433/1405] calculate sample length fixes and SFT splitting fixes (#2351) * fix chat template splitting long samples across multiple rows * make the preprocessing faster --- src/axolotl/prompt_strategies/chat_template.py | 3 +-- src/axolotl/utils/data/utils.py | 9 +++++---- src/axolotl/utils/samplers/utils.py | 10 +++++++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 55140f1510..af1d51a46b 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -272,8 +272,7 @@ def tokenize_prompt(self, prompt: dict[str, Any]): dict(zip(feature_names, row)) ) for key, val in tokenized_prompt.items(): - for i in range(0, len(val), self.sequence_len): - res[key].append(val[i : i + self.sequence_len]) + res[key].append(val) # If there are no examples left, return an empty dictionary if not res: diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index a6abd8d735..a8e19582e7 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -172,10 +172,11 @@ def drop_long_seq_in_dataset(dataset: Dataset, cfg: DictDefault): ) try: - min_input_len = np.min(get_dataset_lengths(dataset)) - LOG.debug(f"min_input_len: {min_input_len}") - max_input_len = np.max(get_dataset_lengths(dataset)) - LOG.debug(f"max_input_len: {max_input_len}") + ds_lengths = get_dataset_lengths(dataset, from_arrow=True) + min_input_len = np.min(ds_lengths) + LOG.info(f"min_input_len: {min_input_len}") + max_input_len = np.max(ds_lengths) + LOG.info(f"max_input_len: {max_input_len}") except AttributeError: pass diff --git a/src/axolotl/utils/samplers/utils.py b/src/axolotl/utils/samplers/utils.py index 8f400c98d4..09f1b081c4 100755 --- a/src/axolotl/utils/samplers/utils.py +++ b/src/axolotl/utils/samplers/utils.py @@ -4,13 +4,17 @@ import numpy as np -def get_dataset_lengths(dataset): +def get_dataset_lengths(dataset, from_arrow=False): if "length" in dataset.column_names: lengths = np.array(dataset["length"]) elif "position_ids" in dataset.column_names: position_ids = dataset["position_ids"] lengths = np.array([x[-1] + 1 for x in position_ids]) else: - input_ids = dataset["input_ids"] - lengths = np.array([len(seq) for seq in input_ids]) + if from_arrow: + input_ids = dataset.data.column("input_ids") + lengths = np.vectorize(len)(np.array(input_ids, dtype=object)) + else: + input_ids = dataset["input_ids"] + lengths = np.array([len(seq) for seq in input_ids]) return lengths From b53a41372f592a7e0d2400d29cc8002d01c23e01 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 21 Feb 2025 09:12:06 +0700 Subject: [PATCH 0434/1405] feat: update transformers version to 4.49.0 (#2340) --- requirements.txt | 2 +- src/axolotl/monkeypatch/relora.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 44f49289a5..b54c6e8d6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ liger-kernel==0.5.2 packaging==23.2 peft==0.14.0 -transformers==4.48.3 +transformers==4.49.0 tokenizers>=0.21.0 accelerate==1.3.0 datasets==3.2.0 diff --git a/src/axolotl/monkeypatch/relora.py b/src/axolotl/monkeypatch/relora.py index 3fda84b929..1dd758ec54 100644 --- a/src/axolotl/monkeypatch/relora.py +++ b/src/axolotl/monkeypatch/relora.py @@ -127,6 +127,8 @@ def on_step_begin( optimizer: torch.optim.Optimizer, **_kwargs, ): + if not optimizer: + optimizer = state.optimizer if state.global_step > 0 and state.global_step % self.relora_steps == 0: checkpoint_folder = os.path.join( args.output_dir, From 29b366b2e1facb0067da5d2d519c896206bc0f6c Mon Sep 17 00:00:00 2001 From: salman Date: Fri, 21 Feb 2025 03:56:04 +0000 Subject: [PATCH 0435/1405] Bumping 0.15.1 TRL version for GRPO+PEFT fix (#2344) * bumping TRL version * apply upstream fixes to our custom fix --------- Co-authored-by: Wing Lian --- requirements.txt | 2 +- src/axolotl/core/trainers/grpo/trainer.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index b54c6e8d6c..535d799339 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ tokenizers>=0.21.0 accelerate==1.3.0 datasets==3.2.0 deepspeed==0.16.1 -trl==0.15.0 +trl==0.15.1 optimum==1.16.2 hf_transfer diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index 8f8b9fcf95..6c8f39ac68 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -78,7 +78,6 @@ def _move_model_to_vllm(self): if is_peft_model(unwrapped_model): unwrapped_model.merge_adapter() state_dict = unwrapped_model.state_dict() - unwrapped_model.unmerge_adapter() # Remove base_model and base_layer prefixes state_dict = { k.removeprefix("base_model.model.") @@ -100,8 +99,10 @@ def _move_model_to_vllm(self): } else: state_dict = unwrapped_model.state_dict() - if self.accelerator.is_main_process: - llm_model = ( - self.llm.llm_engine.model_executor.driver_worker.model_runner.model - ) - llm_model.load_weights(state_dict.items()) + if self.accelerator.is_main_process: + llm_model = ( + self.llm.llm_engine.model_executor.driver_worker.model_runner.model + ) + llm_model.load_weights(state_dict.items()) + if is_peft_model(unwrapped_model): + unwrapped_model.unmerge_adapter() From 1db6ad60a71ab852528762a59c60a4ce4f12717c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 20 Feb 2025 22:56:34 -0500 Subject: [PATCH 0436/1405] support for passing init_lora_weights to lora_config (#2352) --- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 + src/axolotl/utils/models.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 26bfff7dc9..1810413bee 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -342,6 +342,7 @@ class LoraConfig(BaseModel): peft_use_dora: Optional[bool] = None peft_use_rslora: Optional[bool] = None peft_layer_replication: Optional[List[Tuple[int, int]]] = None + peft_init_lora_weights: Optional[Union[bool, str]] = None qlora_sharded_model_loading: Optional[bool] = Field( default=False, diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 377f086052..c4c07dd33d 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -1321,6 +1321,8 @@ def load_lora(model, cfg, inference=False, config_only=False): if loftq_bits: lora_config_kwargs["loftq_config"] = LoftQConfig(loftq_bits=loftq_bits) lora_config_kwargs["init_lora_weights"] = "loftq" + if cfg.peft_init_lora_weights: + lora_config_kwargs["init_lora_weights"] = cfg.peft_init_lora_weights if cfg.peft_use_dora: lora_config_kwargs["use_dora"] = cfg.peft_use_dora LOG.info("Initializing LoRA weights using dora. This might take longer.") From bf842730a5a05ae0aabe3fd79c91673a39f482aa Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 21 Feb 2025 11:56:38 +0700 Subject: [PATCH 0437/1405] fix(doc): add missing auto_find_batch_size (#2339) [skip ci] --- docs/config.qmd | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/config.qmd b/docs/config.qmd index 745a185263..8327e14883 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -407,7 +407,10 @@ save_total_limit: # Checkpoints saved at a time max_steps: # bool of whether to include tokens trainer per second in the training metrics. This iterates over the entire dataset once, so it takes some time. -include_tokens_per_second: +include_tokens_per_second: # Optional[bool] + +# whether to find batch size that fits in memory. Passed to underlying transformers Trainer +auto_find_batch_size: # Optional[bool] eval_table_size: # Approximate number of predictions sent to wandb depending on batch size. Enabled above 0. Default is 0 eval_max_new_tokens: # Total number of tokens generated for predictions sent to wandb. Default is 128 From a4170030ab3807aa7d6a99063ab617c723263439 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 21 Feb 2025 22:06:29 -0500 Subject: [PATCH 0438/1405] don't install extraneous old version of pydantic in ci and make sre to run multigpu ci (#2355) --- .github/workflows/multi-gpu-e2e.yml | 4 ++++ cicd/multigpu.py | 14 +++++--------- src/axolotl/cli/cloud/modal_.py | 2 -- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index ea00d749b7..4934bf9cad 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -4,6 +4,10 @@ on: pull_request: paths: - 'tests/e2e/multigpu/*.py' + - 'requirements.txt' + - 'setup.py' + - 'pyproject.toml' + - '.github/workflows/multi-gpu-e2e.yml' workflow_dispatch: schedule: - cron: '0 0 * * 1,4' # Runs at 00:00 UTC every monday & thursday diff --git a/cicd/multigpu.py b/cicd/multigpu.py index 2c0863034e..eda589dd12 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -37,15 +37,11 @@ with open(pathlib.Path(temp_dir) / "Dockerfile", "w", encoding="utf-8") as f: f.write(dockerfile_contents) -cicd_image = ( - Image.from_dockerfile( - pathlib.Path(temp_dir) / "Dockerfile", - force_build=True, - gpu="A10G", - ) - .env(df_args) - .pip_install("fastapi==0.110.0", "pydantic==2.6.3") -) +cicd_image = Image.from_dockerfile( + pathlib.Path(temp_dir) / "Dockerfile", + force_build=True, + gpu="A10G", +).env(df_args) app = App("Axolotl CI/CD", secrets=[]) diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index 6b724f7328..93d6872b1a 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -123,8 +123,6 @@ def get_image(self): if env := self.get_env(): image = image.env(env) - image = image.pip_install("fastapi==0.110.0", "pydantic==2.6.3") - return image def get_secrets(self): From 2d5826f544a05bc5d909c47915cc853d24ea61ab Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 23 Feb 2025 12:31:35 -0500 Subject: [PATCH 0439/1405] Relicense the logprob KD loss functions as Apache 2.0 (#2358) --- .../integrations/kd/topk_logprob/LICENSE.md | 58 ------------------- .../kd/topk_logprob/forward_kl.py | 14 +++-- 2 files changed, 8 insertions(+), 64 deletions(-) delete mode 100644 src/axolotl/integrations/kd/topk_logprob/LICENSE.md diff --git a/src/axolotl/integrations/kd/topk_logprob/LICENSE.md b/src/axolotl/integrations/kd/topk_logprob/LICENSE.md deleted file mode 100644 index 435d36d75b..0000000000 --- a/src/axolotl/integrations/kd/topk_logprob/LICENSE.md +++ /dev/null @@ -1,58 +0,0 @@ -### AXOLOTL COMMUNITY LICENSE AGREEMENT - -This Axolotl Community License Agreement (“Agreement”) is entered into by and between Axolotl AI Corp. (“Axolotl”) and -any individual or entity (“Licensee”) who wishes to use the Software (as defined below) in accordance with the terms -and conditions set forth in this Agreement. - -1. Definitions - 1.1 “Licensee” refers to any individual or entity who has obtained a copy of the Software under this Agreement. - 1.2 “Plugin Integration” means independent integration software modules which may or may not be offered by Axolotl, - which may be licensed separately by their respective authors and/or licensors. - 1.3 “Software” refers to the specific sub-directory of the Axolotl, Inc. software located at - https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations and its subdirectories which - permits Plugin Integrations to integrate with the Axolotl service. -2. Grant of License - 2.1 Axolotl hereby grants Licensee a worldwide, non-exclusive, royalty-free, license to use, copy, modify, merge, - publish, distribute, sublicense, and/or otherwise exploit the Software, subject to the following conditions: - - Licensee must comply with all the terms and conditions of this Agreement. - - Licensee must include the original copyright notice and disclaimer of warranty in all copies or substantial - portions of the Software. - 2.2 Licensee may use the Software for any lawful purpose, except as restricted in Section 3. -3. Restrictions - 3.1 Licensee shall not use the Software for any activity that constitutes a commercial activity of offering for - free or for sale any services, platform, or equivalent to third parties for the purposes of allowing such - third parties to fine-tune artificial intelligence models. - 3.2 Licensee shall not: - - Use the Software for any illegal or unauthorized purpose. - - Reverse engineer, decompile, or disassemble the Software. - - Remove or modify any copyright, trademark, or other proprietary notices contained in the Software. - - Use the Software in a way that could damage, disable, overburden, or impair the functionality of the - Software or interfere with any third-party use of the Software. - 3.3 Axolotl reserves the right to restrict certain Plugin Integrations for use with the Software. To the extent Licensee integrates a permitted, applicable Plugin Integration with the Software, Licensee shall comply with any additional terms and conditions imposed by the licensors of such Plugin Integration for use of such Plugin Integrations. Licensee shall contact Axolotl if it has questions about whether its use of the Software falls beyond the scope of this Agreement. -4. Intellectual Property Rights - 4.1 Axolotl and its contributors retain all intellectual property rights in and to the Software. Licensee - acknowledges that this Agreement does not transfer any ownership rights or intellectual property rights to - Licensee. -5. Disclaimer of Warranty - 5.1 THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED - TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT SHALL - THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. -6. Termination - 6.1 Axolotl may terminate this Agreement at any time if Licensee fails to comply with any of the terms and - conditions set forth herein. Upon termination, Licensee shall cease all use of the Software and destroy any - copies in its possession. -7. Governing Law - 7.1 This Agreement shall be governed by and construed in accordance with the laws of the State of California, - without regards to conflicts of laws provisions thereof. -8. Entire Agreement - 8.1 This Agreement constitutes the entire agreement between Axolotl and Licensee with respect to the subject matter - hereof and supersedes all prior or contemporaneous understandings or agreements between the parties concerning - the Software, whether written or oral. Axolotl may update the terms of this Agreement from time to time, and - Licensee’s continued use of the Software after any such updates shall constitute acceptance of updated terms - on a go-forward basis. Axolotl will use commercially reasonable efforts to provide Licensee notice of any - material updates. By using the Software, Licensee acknowledges that it has read, understood, and agrees to be - bound by the terms and conditions of this Agreement. - -This Agreement was last updated on August 23, 2024. diff --git a/src/axolotl/integrations/kd/topk_logprob/forward_kl.py b/src/axolotl/integrations/kd/topk_logprob/forward_kl.py index ab9a54d330..3c95150913 100644 --- a/src/axolotl/integrations/kd/topk_logprob/forward_kl.py +++ b/src/axolotl/integrations/kd/topk_logprob/forward_kl.py @@ -1,14 +1,16 @@ # Copyright 2024 Axolotl AI. All rights reserved. # -# This software may be used and distributed according to -# the terms of the Axolotl Community License Agreement (the "License"); +# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations under -# the License. +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. """ loss for top_k KL divergence From 00fc8109e4d780912b44c5f170a51cb512f0868a Mon Sep 17 00:00:00 2001 From: Matt Baker Date: Mon, 24 Feb 2025 08:12:57 -0800 Subject: [PATCH 0440/1405] Correctly reference mount paths (#2347) * Correctly reference mount paths * Also fix mount paths in lm_eval * chore: lint --------- Co-authored-by: Wing Lian --- src/axolotl/cli/cloud/modal_.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index 93d6872b1a..e2095a1e9e 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -258,25 +258,21 @@ def lm_eval(self, config_yaml: str): def _preprocess(config_yaml: str, volumes=None): - Path("/workspace/artifacts/axolotl").mkdir(parents=True, exist_ok=True) - with open( - "/workspace/artifacts/axolotl/config.yaml", "w", encoding="utf-8" - ) as f_out: + Path("/workspace/mounts").mkdir(parents=True, exist_ok=True) + with open("/workspace/mounts/config.yaml", "w", encoding="utf-8") as f_out: f_out.write(config_yaml) - run_folder = "/workspace/artifacts/axolotl" + run_folder = "/workspace/mounts" run_cmd( - "axolotl preprocess /workspace/artifacts/axolotl/config.yaml --dataset-processes=8", + "axolotl preprocess /workspace/mounts/config.yaml --dataset-processes=8", run_folder, volumes, ) def _train(config_yaml: str, accelerate: bool = True, volumes=None, **kwargs): - with open( - "/workspace/artifacts/axolotl/config.yaml", "w", encoding="utf-8" - ) as f_out: + with open("/workspace/mounts/config.yaml", "w", encoding="utf-8") as f_out: f_out.write(config_yaml) - run_folder = "/workspace/artifacts/axolotl" + run_folder = "/workspace/mounts" if accelerate: accelerate_args = "--accelerate" else: @@ -285,20 +281,18 @@ def _train(config_yaml: str, accelerate: bool = True, volumes=None, **kwargs): if num_processes := kwargs.pop("num_processes", None): num_processes_args = f"--num-processes {num_processes}" run_cmd( - f"axolotl train {accelerate_args} {num_processes_args} /workspace/artifacts/axolotl/config.yaml", + f"axolotl train {accelerate_args} {num_processes_args} /workspace/mounts/config.yaml", run_folder, volumes, ) def _lm_eval(config_yaml: str, volumes=None): - with open( - "/workspace/artifacts/axolotl/config.yaml", "w", encoding="utf-8" - ) as f_out: + with open("/workspace/mounts/config.yaml", "w", encoding="utf-8") as f_out: f_out.write(config_yaml) - run_folder = "/workspace/artifacts/axolotl" + run_folder = "/workspace/mounts" run_cmd( - "axolotl lm-eval /workspace/artifacts/axolotl/config.yaml", + "axolotl lm-eval /workspace/mounts/config.yaml", run_folder, volumes, ) From 9850f42204142f3a3f1eaf436f2d868a517e161f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 24 Feb 2025 12:40:54 -0500 Subject: [PATCH 0441/1405] bump liger to 0.5.3 (#2353) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 535d799339..18bb00692e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ mamba-ssm==1.2.0.post1 flash-attn==2.7.4.post1 xformers>=0.0.23.post1 autoawq==0.2.7.post3 -liger-kernel==0.5.2 +liger-kernel==0.5.3 # END section packaging==23.2 From 1110a37e217d07fc865e9c819d8001f3f9a36298 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 25 Feb 2025 03:03:15 +0700 Subject: [PATCH 0442/1405] feat: add deepseek_v3 sample packing (#2230) --- src/axolotl/monkeypatch/multipack.py | 1 + src/axolotl/utils/chat_templates.py | 1 + .../config/models/input/v0_4_1/__init__.py | 1 + tests/e2e/test_deepseekv3.py | 130 ++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 tests/e2e/test_deepseekv3.py diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 3ee89d2e5c..f2a87192a4 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -25,6 +25,7 @@ "gemmoe", "starcoder2", "deepseek_v2", + "deepseek_v3", ] diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index f435bb858e..9f65506892 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -27,6 +27,7 @@ "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", "phi_35": "{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'user' %}{{'<|user|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|>\n' + message['content'] + '<|end|>\n'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% endif %}", "deepseek_v2": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %}", + "deepseek_v3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='', is_first_sp=true) %}{%- for message in messages %}{%- if message['role'] == 'system' %}{%- if ns.is_first_sp %}{% set ns.system_prompt = ns.system_prompt + message['content'] %}{% set ns.is_first_sp = false %}{%- else %}{% set ns.system_prompt = ns.system_prompt + '\\n\\n' + message['content'] %}{%- endif %}{%- endif %}{%- endfor %}{{ bos_token }}{{ ns.system_prompt }}{%- for message in messages %}{%- if message['role'] == 'user' %}{%- set ns.is_tool = false -%}{{'<|User|>' + message['content']}}{%- endif %}{%- if message['role'] == 'assistant' and 'tool_calls' in message %}{%- set ns.is_tool = false -%}{%- for tool in message['tool_calls'] %}{%- if not ns.is_first %}{%- if message['content'] is none %}{{'<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- else %}{{'<|Assistant|>' + message['content'] + '<|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- endif %}{%- set ns.is_first = true -%}{%- else %}{{'\\n' + '<|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- endif %}{%- endfor %}{{'<|tool▁calls▁end|><|end▁of▁sentence|>'}}{%- endif %}{%- if message['role'] == 'assistant' and 'tool_calls' not in message %}{%- if ns.is_tool %}{{'<|tool▁outputs▁end|>' + message['content'] + '<|end▁of▁sentence|>'}}{%- set ns.is_tool = false -%}{%- else %}{% set content = message['content'] %}{% if '' in content %}{% set content = content.split('')[-1] %}{% endif %}{{'<|Assistant|>' + content + '<|end▁of▁sentence|>'}}{%- endif %}{%- endif %}{%- if message['role'] == 'tool' %}{%- set ns.is_tool = true -%}{%- if ns.is_output_first %}{{'<|tool▁outputs▁begin|><|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- set ns.is_output_first = false %}{%- else %}{{'<|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- endif %}{%- endif %}{%- endfor -%}{% if ns.is_tool %}{{'<|tool▁outputs▁end|>'}}{% endif %}{% if add_generation_prompt and not ns.is_tool %}{{'<|Assistant|>'}}{% endif %}", "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', "qwen_25": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n", "exaone": "{% for message in messages %}{% if loop.first and message['role'] != 'system' %}{{ '[|system|][|endofturn|]\n' }}{% endif %}{{ '[|' + message['role'] + '|]' + message['content'] }}{% if message['role'] == 'user' %}{{ '\n' }}{% else %}{{ '[|endofturn|]\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '[|assistant|]' }}{% endif %}", diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 1810413bee..c7803b8ccb 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -55,6 +55,7 @@ class ChatTemplate(str, Enum): phi_3 = "phi_3" # pylint: disable=invalid-name phi_35 = "phi_35" # pylint: disable=invalid-name deepseek_v2 = "deepseek_v2" # pylint: disable=invalid-name + deepseek_v3 = "deepseek_v3" # pylint: disable=invalid-name jamba = "jamba" # pylint: disable=invalid-name jinja = "jinja" # pylint: disable=invalid-name qwen_25 = "qwen_25" # pylint: disable=invalid-name diff --git a/tests/e2e/test_deepseekv3.py b/tests/e2e/test_deepseekv3.py new file mode 100644 index 0000000000..de8513078e --- /dev/null +++ b/tests/e2e/test_deepseekv3.py @@ -0,0 +1,130 @@ +""" +E2E tests for lora llama +""" + +import logging +import os +from pathlib import Path + +import pytest + +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestDeepseekV3: + """ + Test case for DeepseekV3 models + """ + + @pytest.mark.parametrize( + "sample_packing", + [True, False], + ) + def test_lora_deepseekv3(self, temp_dir, sample_packing): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "axolotl-ai-co/DeepSeek-V3-11M", + "trust_remote_code": True, + "sample_packing": sample_packing, + "flash_attention": True, + "sequence_len": 2048, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0, + "datasets": [ + { + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "field_messages": "conversations", + "message_property_mappings": { + "role": "from", + "content": "value", + }, + "drop_system_message": True, + "split": "train[:1%]", + }, + ], + "special_tokens": { + "bos_token": "<|begin▁of▁sentence|>", + "eos_token": "<|end▁of▁sentence|>", + }, + "chat_template": "deepseek_v3", + "num_epochs": 1, + "micro_batch_size": 1, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "max_steps": 5, + "save_safetensors": True, + "bf16": True, + } + ) + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.safetensors").exists() + + @pytest.mark.parametrize( + "sample_packing", + [True, False], + ) + def test_fft_deepseekv3(self, temp_dir, sample_packing): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "axolotl-ai-co/DeepSeek-V3-11M", + "trust_remote_code": True, + "sample_packing": sample_packing, + "flash_attention": True, + "sequence_len": 2048, + "val_set_size": 0, + "datasets": [ + { + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "field_messages": "conversations", + "message_field_role": "from", + "message_field_content": "value", + "split": "train[:1%]", + }, + ], + "chat_template": "deepseek_v3", + "special_tokens": { + "bos_token": "<|begin▁of▁sentence|>", + "eos_token": "<|end▁of▁sentence|>", + }, + "num_epochs": 1, + "micro_batch_size": 1, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "max_steps": 5, + "save_safetensors": True, + "bf16": True, + } + ) + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() From 2efe1b4c0949bc94ec8b7c271ea7a86bdd798e9e Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 25 Feb 2025 16:09:37 +0700 Subject: [PATCH 0443/1405] Feat(doc): Reorganize documentation, fix broken syntax, update notes (#2348) * feat(doc): organize docs, add to menu bar, fix broken formatting * feat: add link to custom integrations * feat: update readme for integrations to include citations and repo link * chore: update lm_eval info * chore: use fullname * Update docs/cli.qmd per suggestion Co-authored-by: Dan Saunders * feat: add sweep doc * feat: add kd doc * fix: remove toc * fix: update deprecation * feat: add more info about chat_template issues * fix: heading level * fix: shell->bash code block * fix: ray link * fix(doc): heading level, header links, formatting * feat: add grpo docs * feat: add style changes * fix: wrong cli arg for lm-eval * fix: remove old run method * feat: load custom integration doc dynamically * fix: remove old cli way * fix: toc * fix: minor formatting --------- Co-authored-by: Dan Saunders --- README.md | 5 +- _quarto.yml | 61 ++-- docs/amd_hpc.qmd | 2 +- docs/cli.qmd | 134 +++++---- docs/custom_integrations.qmd | 57 ++++ docs/dataset-formats/conversation.qmd | 4 +- docs/dataset-formats/index.qmd | 51 +++- docs/dataset-formats/pretraining.qmd | 1 - docs/dataset-formats/template_free.qmd | 234 +++++++++++++++- docs/debugging.qmd | 8 +- docs/faq.qmd | 19 ++ docs/getting-started.qmd | 14 +- docs/inference.qmd | 3 +- docs/input_output.qmd | 261 +----------------- docs/installation.qmd | 3 +- docs/lora_optims.qmd | 3 +- docs/mac.qmd | 1 + docs/multi-gpu.qmd | 28 +- docs/multi-node.qmd | 8 +- docs/nccl.qmd | 8 +- docs/ray-integration.qmd | 6 +- docs/rlhf.qmd | 116 +++++--- docs/torchao.qmd | 6 + docs/unsloth.qmd | 8 +- index.qmd | 6 +- .../integrations/cut_cross_entropy/README.md | 22 +- src/axolotl/integrations/grokfast/README.md | 13 +- src/axolotl/integrations/kd/README.md | 23 ++ src/axolotl/integrations/liger/README.md | 36 +++ src/axolotl/integrations/lm_eval/README.md | 24 +- src/axolotl/integrations/spectrum/README.md | 22 +- styles.css | 194 ++++++++++++- 32 files changed, 939 insertions(+), 442 deletions(-) create mode 100644 docs/custom_integrations.qmd create mode 100644 src/axolotl/integrations/kd/README.md create mode 100644 src/axolotl/integrations/liger/README.md diff --git a/README.md b/README.md index 398e31b6f0..c2e0f4731e 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,14 @@ Features: ## 🚀 Quick Start **Requirements**: + - NVIDIA GPU (Ampere or newer for `bf16` and Flash Attention) or AMD GPU - Python 3.11 - PyTorch ≥2.4.1 ### Installation -```shell +```bash pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] # Download example axolotl configs, deepspeed configs @@ -68,7 +69,7 @@ Other installation approaches are described [here](https://axolotl-ai-cloud.gith ### Your First Fine-tune -```shell +```bash # Fetch axolotl examples axolotl fetch examples diff --git a/_quarto.yml b/_quarto.yml index 3ec1ce75b6..ddd172370f 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -3,10 +3,12 @@ project: website: title: "Axolotl" - description: "Fine-tuning" + description: "We make fine-tuning accessible, scalable, and fun" favicon: favicon.jpg + navbar: - title: Axolotl + logo: image/axolotl_logo_digital_white.svg + title: false background: dark pinned: false collapse: false @@ -25,33 +27,58 @@ website: contents: - text: Home href: index.qmd - - section: "How-To Guides" + + - section: "Getting Started" contents: - # TODO Edit folder structure after we have more docs. - docs/getting-started.qmd - docs/installation.qmd - - docs/debugging.qmd + - docs/cli.qmd - docs/inference.qmd - - docs/multipack.qmd - - docs/fsdp_qlora.qmd - - docs/input_output.qmd - - docs/rlhf.qmd - - docs/nccl.qmd - - docs/mac.qmd + + - section: "Dataset Formats" + contents: docs/dataset-formats/* + + - section: "Deployments" + contents: - docs/multi-gpu.qmd - docs/multi-node.qmd - - docs/unsloth.qmd - - docs/amd_hpc.qmd - docs/ray-integration.qmd - - section: "Dataset Formats" - contents: docs/dataset-formats/* + - docs/amd_hpc.qmd + - docs/mac.qmd + + - section: "How To Guides" + contents: + - docs/multimodal.qmd + - docs/rlhf.qmd + - docs/reward_modelling.qmd + - docs/lr_groups.qmd + - docs/lora_optims.qmd + + - section: "Core Concepts" + contents: + - docs/batch_vs_grad.qmd + - docs/dataset_preprocessing.qmd + - docs/multipack.qmd + + - section: "Advanced Features" + contents: + - docs/fsdp_qlora.qmd + - docs/unsloth.qmd + - docs/torchao.qmd + - docs/custom_integrations.qmd + + - section: "Troubleshooting" + contents: + - docs/faq.qmd + - docs/debugging.qmd + - docs/nccl.qmd + - section: "Reference" contents: - docs/config.qmd - - docs/faq.qmd format: html: - theme: materia + theme: darkly css: styles.css toc: true diff --git a/docs/amd_hpc.qmd b/docs/amd_hpc.qmd index 70fbe88ee3..c6dbe82d07 100644 --- a/docs/amd_hpc.qmd +++ b/docs/amd_hpc.qmd @@ -1,5 +1,5 @@ --- -title: Training with AMD GPUs on HPC Systems +title: AMD GPUs on HPC Systems description: A comprehensive guide for using Axolotl on distributed systems with AMD GPUs --- diff --git a/docs/cli.qmd b/docs/cli.qmd index 5b494ab5de..a57e54d9a9 100644 --- a/docs/cli.qmd +++ b/docs/cli.qmd @@ -1,28 +1,19 @@ -# Axolotl CLI Documentation +--- +title: "CLI Reference" +format: + html: + toc: true + toc-expand: 2 + toc-depth: 3 +execute: + enabled: false +--- The Axolotl CLI provides a streamlined interface for training and fine-tuning large language models. This guide covers the CLI commands, their usage, and common examples. -### Table of Contents -- Basic Commands -- Command Reference - - fetch - - preprocess - - train - - inference - - merge-lora - - merge-sharded-fsdp-weights - - evaluate - - lm-eval -- Legacy CLI Usage -- Remote Compute with Modal Cloud - - Cloud Configuration - - Running on Modal Cloud - - Cloud Configuration Options - - -### Basic Commands +## Basic Commands All Axolotl commands follow this general structure: @@ -32,9 +23,9 @@ axolotl [config.yml] [options] The config file can be local or a URL to a raw YAML file. -### Command Reference +## Command Reference -#### fetch +### fetch Downloads example configurations and deepspeed configs to your local machine. @@ -49,7 +40,7 @@ axolotl fetch deepspeed_configs axolotl fetch examples --dest path/to/folder ``` -#### preprocess +### preprocess Preprocesses and tokenizes your dataset before training. This is recommended for large datasets. @@ -74,7 +65,7 @@ dataset_prepared_path: Local folder for saving preprocessed data push_dataset_to_hub: HuggingFace repo to push preprocessed data (optional) ``` -#### train +### train Trains or fine-tunes a model using the configuration specified in your YAML file. @@ -95,7 +86,38 @@ axolotl train config.yml --no-accelerate axolotl train config.yml --resume-from-checkpoint path/to/checkpoint ``` -#### inference +It is possible to run sweeps over multiple hyperparameters by passing in a sweeps config. + +```bash +# Basic training with sweeps +axolotl train config.yml --sweep path/to/sweep.yaml +``` + +Example sweep config: +```yaml +_: + # This section is for dependent variables we need to fix + - load_in_8bit: false + load_in_4bit: false + adapter: lora + - load_in_8bit: true + load_in_4bit: false + adapter: lora + +# These are independent variables +learning_rate: [0.0003, 0.0006] +lora_r: + - 16 + - 32 +lora_alpha: + - 16 + - 32 + - 64 +``` + + + +### inference Runs inference using your trained model in either CLI or Gradio interface mode. @@ -115,7 +137,7 @@ cat prompt.txt | axolotl inference config.yml \ --base-model="./completed-model" ``` -#### merge-lora +### merge-lora Merges trained LoRA adapters into the base model. @@ -137,7 +159,7 @@ gpu_memory_limit: Limit GPU memory usage lora_on_cpu: Load LoRA weights on CPU ``` -#### merge-sharded-fsdp-weights +### merge-sharded-fsdp-weights Merges sharded FSDP model checkpoints into a single combined checkpoint. @@ -146,7 +168,7 @@ Merges sharded FSDP model checkpoints into a single combined checkpoint. axolotl merge-sharded-fsdp-weights config.yml ``` -#### evaluate +### evaluate Evaluates a model's performance using metrics specified in the config. @@ -155,27 +177,27 @@ Evaluates a model's performance using metrics specified in the config. axolotl evaluate config.yml ``` -#### lm-eval +### lm-eval Runs LM Evaluation Harness on your model. ```bash # Basic evaluation axolotl lm-eval config.yml - -# Evaluate specific tasks -axolotl lm-eval config.yml --tasks arc_challenge,hellaswag ``` Configuration options: ```yaml -lm_eval_tasks: List of tasks to evaluate -lm_eval_batch_size: Batch size for evaluation -output_dir: Directory to save evaluation results +# List of tasks to evaluate +lm_eval_tasks: + - arc_challenge + - hellaswag +lm_eval_batch_size: # Batch size for evaluation +output_dir: # Directory to save evaluation results ``` -### Legacy CLI Usage +## Legacy CLI Usage While the new Click-based CLI is preferred, Axolotl still supports the legacy module-based CLI: @@ -195,12 +217,18 @@ accelerate launch -m axolotl.cli.inference config.yml \ --lora_model_dir="./outputs/lora-out" --gradio ``` -### Remote Compute with Modal Cloud +::: {.callout-important} +When overriding CLI parameters in the legacy CLI, use same notation as in yaml file (e.g., `--lora_model_dir`). + +**Note:** This differs from the new Click-based CLI, which uses dash notation (e.g., `--lora-model-dir`). Keep this in mind if you're referencing newer documentation or switching between CLI versions. +::: + +## Remote Compute with Modal Cloud Axolotl supports running training and inference workloads on Modal cloud infrastructure. This is configured using a cloud YAML file alongside your regular Axolotl config. -#### Cloud Configuration +### Cloud Configuration Create a cloud config YAML with your Modal settings: @@ -215,13 +243,17 @@ branch: main # Git branch to use (optional) volumes: # Persistent storage volumes - name: axolotl-cache mount: /workspace/cache + - name: axolotl-data + mount: /workspace/data + - name: axolotl-artifacts + mount: /workspace/artifacts env: # Environment variables - WANDB_API_KEY - HF_TOKEN ``` -#### Running on Modal Cloud +### Running on Modal Cloud Commands that support the --cloud flag: @@ -239,18 +271,18 @@ axolotl train config.yml --cloud cloud_config.yml --no-accelerate axolotl lm-eval config.yml --cloud cloud_config.yml ``` -#### Cloud Configuration Options +### Cloud Configuration Options ```yaml -provider: compute provider, currently only `modal` is supported -gpu: GPU type to use -gpu_count: Number of GPUs (default: 1) -memory: RAM in GB (default: 128) -timeout: Maximum runtime in seconds -timeout_preprocess: Preprocessing timeout -branch: Git branch to use -docker_tag: Custom Docker image tag -volumes: List of persistent storage volumes -env: Environment variables to pass -secrets: Secrets to inject +provider: # compute provider, currently only `modal` is supported +gpu: # GPU type to use +gpu_count: # Number of GPUs (default: 1) +memory: # RAM in GB (default: 128) +timeout: # Maximum runtime in seconds +timeout_preprocess: # Preprocessing timeout +branch: # Git branch to use +docker_tag: # Custom Docker image tag +volumes: # List of persistent storage volumes +env: # Environment variables to pass +secrets: # Secrets to inject ``` diff --git a/docs/custom_integrations.qmd b/docs/custom_integrations.qmd new file mode 100644 index 0000000000..8d04982986 --- /dev/null +++ b/docs/custom_integrations.qmd @@ -0,0 +1,57 @@ +--- +title: Custom Integrations +toc: true +toc-depth: 3 +--- + +```{python} +#| echo: false + +import re + +def process_readme(integration_name): + try: + path = f'../src/axolotl/integrations/{integration_name}/README.md' + with open(path, 'r') as f: + txt = f.read() + # Remove h1 headings + txt = re.sub(r'^# .*\n?', '', txt, flags=re.MULTILINE) + # Convert h2 to h3 + txt = re.sub(r'^## ', '### ', txt, flags=re.MULTILINE) + return txt + except FileNotFoundError: + return None + +def print_section(name, folder_name): + output = f"\n## {name}\n" + content = process_readme(folder_name) + if content: + output += content + output += f"\nPlease see reference [here](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations/{folder_name})\n" + return output +``` + +```{python} +#| output: asis +#| echo: false + +# Introduction text +print(""" +Axolotl adds custom features through `integrations`. They are located within the `src/axolotl/integrations` directory. + +To enable them, please check the respective documentations. +""") + +# Sections +sections = [ + ("Cut Cross Entropy", "cut_cross_entropy"), + ("Grokfast", "grokfast"), + ("Knowledge Distillation (KD)", "kd"), + ("Liger Kernels", "liger"), + ("Language Model Evaluation Harness (LM Eval)", "lm_eval"), + ("Spectrum", "spectrum") +] + +for section_name, folder_name in sections: + print(print_section(section_name, folder_name)) +``` diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 6866f3f408..d67e35876b 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -6,7 +6,9 @@ order: 3 ## sharegpt -IMPORTANT: ShareGPT is deprecated!. Please see [chat_template](#chat_template) section below. +::: {.callout-important} +ShareGPT is deprecated!. Please see [chat_template](#chat_template) section below. +::: ## pygmalion diff --git a/docs/dataset-formats/index.qmd b/docs/dataset-formats/index.qmd index a46f466048..4275858f62 100644 --- a/docs/dataset-formats/index.qmd +++ b/docs/dataset-formats/index.qmd @@ -13,7 +13,7 @@ As there are a lot of available options in Axolotl, this guide aims to provide a Axolotl supports 3 kinds of training methods: pre-training, supervised fine-tuning, and preference-based post-training (e.g. DPO, ORPO, PRMs). Each method has their own dataset format which are described below. -## [Pre-training](pretraining.qmd) +## Pre-training When aiming to train on large corpora of text datasets, pre-training is your go-to choice. Due to the size of these datasets, downloading the entire-datasets before beginning training would be prohibitively time-consuming. Axolotl supports [streaming](https://huggingface.co/docs/datasets/en/stream) to only load batches into memory at a time. @@ -96,6 +96,10 @@ One step is equal to `sequence_len * micro_batch_size * gradient_accumulation_st It is recommended to leave this off if downloading from Hugging Face hub as it would download the entire dataset which can be very large. +### Reference + +Please see docs [here](pretraining.qmd). + ## Supervised fine-tuning (SFT) Supervised fine-tuning is the process of training models to respond to an instruction or chat input. @@ -120,7 +124,7 @@ If you went through the flow chart and did not find one that matches, it is reco You can mix and match within each approach or across approaches to train a model on a variety of datasets. ::: -### [Pre-Tokenized Dataset](tokenized.qmd) +### Pre-Tokenized Dataset We suggest this approach when you want to bring your own tokenized dataset. @@ -145,7 +149,9 @@ datasets: `type: ` is empty! ::: -### [Template Free Dataset](template_free.qmd) +Reference: [Pre-Tokenized Dataset Documentation](tokenized.qmd). + +### Template Free Dataset We reccomend this approach when you want granular control over the prompt formatting, special tokens, and masking, whilst letting Axolotl handle the tokenization. This is very useful if your dataset has unique prompts that differ across samples and where one single general template wouldn't suffice. @@ -182,7 +188,9 @@ datasets: type: input_output ``` -### [Conversation Dataset](conversation.qmd) +Reference: [Template Free Documentation](template_free.qmd). + +### Conversation Dataset `conversation` messages are a list of messages which usually contain a `role` and `content` key. @@ -258,7 +266,7 @@ Newer conversation datasets usually follow the OpenAI format. Axolotl supports both as well as allowing customization of any kind of key. -#### [Chat Template Usage](conversation.qmd#chat_template) +#### Chat Template Usage To properly use this method, it is important to identify three things: @@ -340,9 +348,19 @@ datasets: narrator: ["narrator"] ``` -#### Applying `chat_template` +::: {.callout-tip} +As chat_templates may use hardcoded EOS/EOT tokens that are different from the tokenizer's EOS, it is highly recommended to set them. For example, `ChatML` uses `<|im_end|>` to end turns. + +```yaml +special_tokens: + eos_token: <|im_end|> +``` + +::: -Once all the above steps are completed, you could combine all these configs together to form a bespoke configuration for your custom dataset. The final step would be to correctly set the EOS token in your config: +##### Applying `chat_template` + +Once all the above steps are completed, you could combine all these configs together to form a bespoke configuration for your custom dataset. ```yaml datasets: @@ -391,7 +409,17 @@ If this config were to be applied to the sample dataset above, the output would The first number refers to the label, the second refers to the `token_id`. For example, `-100` labels appear on non-assistant portions, meaning that they are masked during. For assistant portions, the label is the same as the `token_id`. -### [Instruction Dataset](inst_tune.qmd) +::: {.callout-note} + +If during `preprocess`, there are a lot of warnings of `Could not find content __ boundary`, please check the FAQ section for [chat_templates](../faq.qmd#chat-templates). + +::: + +#### Reference + +Please see docs [here](conversation.qmd). + +### Instruction Dataset Instruction datasets are used to train instruction-following models and comprise a prompt, containing an instruction, and a single response. In contrast to chat datasets which may be multi-turn, instruct datasets are typically single-turn. @@ -423,6 +451,9 @@ datasets: Axolotl supports many kinds of instruction dataset. All of them can be found here (https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/inst_tune.html) with their respective type and sample row format. + +Reference: [Instruction Dataset Documentation](inst_tune.qmd). + #### Custom Instruct Prompt Format Due to the myriad possibilities of instruction formats, Axolotl allows customizing your own instruction format without having to dive into the code directly. @@ -453,6 +484,8 @@ datasets: The config sets that the `field_instruction` is actually named `input`, and the `field_input` is empty as we don't have an `input` in this sample. Generally, `instruction` can be thought as the question to the model, and `input` as the additional information with `output` being the response. It is not necessary to have an `input` nor `system`. In the end, the most important part is to understand what format you want it to look like and how you can customize this to your use case. +Reference: [Custom Instruct Prompt Format Documentation](inst_tune.qmd#how-to-add-custom-prompt-format). + ## Reinforcement Learning from Human Feedback (RLHF) -As there are multiple RLHF methods with their own dataset requirements. Please see [RLHF datasets](../rlhf.qmd) documentation for more detail. +As there are multiple RLHF methods with their own dataset requirements. Please see [RLHF documentation](../rlhf.qmd) for more detail. diff --git a/docs/dataset-formats/pretraining.qmd b/docs/dataset-formats/pretraining.qmd index 600fb63e09..b51b0e0b38 100644 --- a/docs/dataset-formats/pretraining.qmd +++ b/docs/dataset-formats/pretraining.qmd @@ -27,7 +27,6 @@ pretraining_dataset: type: pretrain trust_remote_code: skip: # number of rows of data to skip over from the beginning -... ``` ::: diff --git a/docs/dataset-formats/template_free.qmd b/docs/dataset-formats/template_free.qmd index 5087d6a013..c75c5931e8 100644 --- a/docs/dataset-formats/template_free.qmd +++ b/docs/dataset-formats/template_free.qmd @@ -1,7 +1,239 @@ --- title: Template-Free description: Construct prompts without a template. +toc: true +toc-depth: 3 order: 4 --- -See [these docs](../input_output.qmd). +## Background {#sec-background} + +### Masking Inputs {#masking-inputs} + +One of the most popular features of +[axolotl](https://github.com/axolotl-ai-cloud/axolotl) is +setting the following configuration value: + + +```yaml +train_on_inputs: false +``` + +If you declare a [dataset formats](https://github.com/axolotl-ai-cloud/axolotl?tab=readme-ov-file#dataset) +such as `alpaca` or `chatml`, axolotl knows what is an input +(i.e. human) vs. an output (i.e. the assistant) and masks the input +labels so that your model can focus on predicting the outputs only. + +### You may not want prompt templates {#sec-you-may-not-want-prompt-templates} + +However, there are many situations where you don't want to use one of +these formats or templates. This is because they can: + +- Add unnecessary boilerplate to your prompts. +- Create artifacts like special delimiters `<|im_start|>` that can + quickly become footguns if you don't include them correctly at + inference time. +- Enforce a *chat* interface when you do not want one. Sometimes you + just want to fine-tune a model to a very specific task and do NOT + want multi-turn conversations, roles, etc. +- Limit you to only certain roles that the template allows. + +### The `input_output` format {#sec-the-inputoutput-format} + +You can construct your prompts without a template by using the +`input_output` format, by setting `type: input_output` in your +configuration file like this: + +**config.yml** + +```yaml +train_on_inputs: false # Mask segments of your data +datasets: + - path: output.jsonl + type: input_output # use template free prompt construction +``` + +Unlike `type: completion`, which is also template-free, +`type: input_output` allows you to mask segments of your text. More +details on how this works are described below. + +## Usage {#sec-usage} + +This is how you can use the `input_output` format: + +### 1. Prepare Data {#sec-1-prepare-data} + +To use the `input_output` format, collect your data in the following +format into a jsonl file (below is the first row from the file +`output`.jsonl` pretty printed): + +```bash +$ head -n1 output.jsonl | python -m json.tool +``` + +:::{.cell-output .cell-output-stdout} + { + "segments": [ + { + "label": true, + "text": "Hello\n" + }, + { + "label": true, + "text": "hi there!. " + }, + { + "label": false, + "text": "goodbye " + }, + { + "label": true, + "text": "farewell" + } + ] + } +::: + +Set `label:false` when you want to mask a segment of text so that the +model isn't trained on it. Some things to keep in mind: + +> [!IMPORTANT] +> 1. **EOS, BOS, spaces, newlines etc. are entirely up to you. Axolotl + concatenates all the segments as-is.** The tokenizer doesn't add + anything additional. Notice how I added spaces, newlines, `` + (BOS), and `` (EOS) myself. +> 2. Make sure you check the materialized output to validate that the + prompt is getting assembled how you like. + +### 2. Use `type: input_output` {#sec-2-use-type-inputoutput} + +Let's materialize data with our `output.jsonl` file by setting +`type: input_output` in our axolotl config: + +```yaml +# training_config.yaml +base_model: mistralai/Mistral-7B-v0.1 +data_seed: 49 +seed: 49 + +datasets: + - path: output.jsonl + type: input_output +val_set_size: 0.1 + +sequence_len: 896 +sample_packing: false + +micro_batch_size: 2 +gradient_accumulation_steps: 3 +eval_batch_size: 2 +num_epochs: 1 +learning_rate: 0.0002 + +train_on_inputs: false +special_tokens: + bos_token: "" + eos_token: "" + unk_token: "" +``` + +You can use the following command to materialize your data. The +`--debug` flag will print the tokens, along with the labels so you can +verify that the correct items are being ignored: + +```bash +axolotl preprocess training_config.yaml --debug + +... +[2024-03-05 23:36:46,969] [INFO] [axolotl.check_example_labels:35] [PID:607731] [RANK:0] (1, 1) Hello(22557, 22557) +(13, 13) hi(12014, 12014) there(736, 736) !(28808, 28808) .(28723, 28723) (28705, 28705) good(-100, 1179) bye(-100, 17664) (-100, 28705) fare(19111, 19111) well(5458, 5458) (2, 2) + +``` + +The format is `decoded_token`(`label`, `token_id`), for example, +`(1, 1)` means that the token is ``, the label is `1` and the +token_id is `1`. When the label is `-100` then that token is ignored for +training. + +### 3. Check the prompts {#sec-3-check-the-prompts} + +Here is another way to check the materialized output: + +```python +from transformers import AutoTokenizer +from datasets import load_from_disk +import yaml + +directory = !ls last_run_prepared/ +with open('training_config.yaml', 'r') as f: + cfg = yaml.safe_load(f) +model_id = cfg['base_model'] +tok = AutoTokenizer.from_pretrained(model_id) +ds = load_from_disk(f'last_run_prepared/{directory[0]}/') +``` + +```python +>>> row = ds[0] +>>> print(tok.decode(row['input_ids'])) + Hello + hi there!. goodbye farewell +``` + +We can check that the right tokens are ignored by comparing the labels +to each token: + +```python +import pandas as pd +pd.DataFrame([{'token': tok.decode(i), 'label': l, 'id':i} for i,l in + zip(row['input_ids'], row['labels'])]) +``` + +| token | label | id | +|-------|-------|-------| +| 0 | \ | 1 | +| 1 | Hello | 22557 | +| 2 | \\n | 13 | +| 3 | hi | 12014 | +| 4 | there | 736 | +| 5 | ! | 28808 | +| 6 | . | 28723 | +| 7 | | 28705 | +| 8 | good | -100 | +| 9 | bye | -100 | +| 10 | | -100 | +| 11 | fare | 19111 | +| 12 | well | 5458 | +| 13 | \| 2 | + + + +If we look at the input data, the above table seems correct! (The jsonl +version is repeated below for reference): + + +```bash +$ head -n1 output.jsonl | python -m json.tool +``` + +:::{.cell-output .cell-output-stdout} + { + "segments": [ + { + "label": true, + "text": "Hello\n" + }, + { + "label": true, + "text": "hi there!. " + }, + { + "label": false, + "text": "goodbye " + }, + { + "label": true, + "text": "farewell" + } + ] + } +::: diff --git a/docs/debugging.qmd b/docs/debugging.qmd index 4eaa609272..bf3c6fe7e8 100644 --- a/docs/debugging.qmd +++ b/docs/debugging.qmd @@ -31,11 +31,13 @@ While debugging it's helpful to simplify your test scenario as much as possible. - Set `CUDA_VISIBLE_DEVICES` to a single GPU, ex: `export CUDA_VISIBLE_DEVICES=0`. - Set `dataset_processes: 1` in your axolotl config or run the training command with `--dataset_processes=1`. 2. **Use a small dataset**: Construct or use a small dataset from HF Hub. When using a small dataset, you will often have to make sure `sample_packing: False` and `eval_sample_packing: False` to avoid errors. If you are in a pinch and don't have time to construct a small dataset but want to use from the HF Hub, you can shard the data (this will still tokenize the entire dataset, but will only use a fraction of the data for training. For example, to shard the dataset into 20 pieces, add the following to your axolotl config): + ```yaml - dataset: + datasets: ... shards: 20 ``` + 3. **Use a small model**: A good example of a small model is [TinyLlama/TinyLlama-1.1B-Chat-v1.0](https://huggingface.co/TinyLlama/TinyLlama-1.1B-Chat-v1.0). 4. **Minimize iteration time**: Make sure the training loop finishes as fast as possible, with these settings. - `micro_batch_size: 1` @@ -85,7 +87,7 @@ The easiest way to get started is to modify the [.vscode/launch.json](../.vscode For example, to mimic the command `cd devtools && CUDA_VISIBLE_DEVICES=0 accelerate launch -m axolotl.cli.train dev_chat_template.yml`, you would use the below configuration[^1]. Note that we add additional flags that override the axolotl config and incorporate the tips above (see the comments). We also set the working directory to `devtools` and set the `env` variable `HF_HOME` to a temporary folder that is later partially deleted. This is because we want to delete the HF dataset cache before each run in order to ensure that the data preprocessing code is run from scratch. -```jsonc +```json // .vscode/launch.json { "version": "0.2.0", @@ -132,7 +134,7 @@ For example, to mimic the command `cd devtools && CUDA_VISIBLE_DEVICES=0 acceler Below is the [./vscode/tasks.json](../.vscode/tasks.json) file that defines the `cleanup-for-dataprep` task. This task is run before each debugging session when you use the above configuration. Note how there are two tasks that delete the two folders mentioned above. The third task `cleanup-for-dataprep` is a composite task that combines the two tasks. A composite task is necessary because VSCode does not allow you to specify multiple tasks in the `preLaunchTask` argument of the `launch.json` file. -```jsonc +```json // .vscode/tasks.json // this file is used by launch.json { diff --git a/docs/faq.qmd b/docs/faq.qmd index 3f78bde73c..0a181e022a 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -3,6 +3,7 @@ title: FAQ description: Frequently asked questions --- +### General **Q: The trainer stopped and hasn't progressed in several minutes.** @@ -24,6 +25,24 @@ description: Frequently asked questions > A: This is usually an issue with the GPU. This can be resolved through setting the os environment variable `CUDA_VISIBLE_DEVICES=0`. If you are on runpod, this is usually a pod issue. Starting a new pod should take care of it. +### Chat templates + **Q: `jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'content' / 'role' / ____`** > A: This means that the property mapping for the stated attribute does not exist when building `chat_template` prompt. For example, if `no attribute 'content'`, please check you have added the correct mapping for `content` under `message_property_mappings`. + +**Q: `Empty template generated for turn ___`** + +> A: The `content` is empty for that turn. + +**Q: `Could not find content start/end boundary for turn __`** + +> A: The specific turn's start/end could not be detected. Please ensure you have set the `eos_token` following your `chat_template`. Otherwise, this could be a `chat_template` which doesn't use proper boundaries for each turn (like system). On the rare occurrence, make sure your content is not `[[dummy_message]]`. Please let us know about this. + +**Q: `Content end boundary is before start boundary for turn ___`** + +> A: This is an edge case which should not occur. Please create an Issue if this happens. + +**Q: `Content end boundary is the same as start boundary for turn ___. This is likely an empty turn.`** + +> A: This is likely an empty turn. diff --git a/docs/getting-started.qmd b/docs/getting-started.qmd index 2292cde151..8e826b9592 100644 --- a/docs/getting-started.qmd +++ b/docs/getting-started.qmd @@ -1,5 +1,5 @@ --- -title: "Getting Started with Axolotl" +title: "Quickstart" format: html: toc: true @@ -17,12 +17,12 @@ Let's start by fine-tuning a small language model using LoRA. This example uses Assuming `axolotl` is installed (if not, see our [Installation Guide](installation.qmd)) 1. Download example configs: -```shell +```bash axolotl fetch examples ``` 2. Run the training: -```shell +```bash axolotl train examples/llama-3/lora-1b.yml ``` @@ -108,7 +108,7 @@ Please consult the supported [Dataset Formats](dataset-formats/) for more detail 3. Run the training: -```shell +```bash axolotl train my_training.yml ``` @@ -118,7 +118,7 @@ axolotl train my_training.yml After training, test your model: -```shell +```bash axolotl inference my_training.yml --lora-model-dir="./outputs/lora-out" ``` @@ -126,7 +126,7 @@ axolotl inference my_training.yml --lora-model-dir="./outputs/lora-out" For large datasets, preprocess first: -```shell +```bash axolotl preprocess my_training.yml ``` @@ -134,7 +134,7 @@ axolotl preprocess my_training.yml Launch a Gradio interface: -```shell +```bash axolotl inference my_training.yml --lora-model-dir="./outputs/lora-out" --gradio ``` diff --git a/docs/inference.qmd b/docs/inference.qmd index 59e352c181..aded400d04 100644 --- a/docs/inference.qmd +++ b/docs/inference.qmd @@ -1,11 +1,10 @@ --- -title: "Inference Guide" +title: "Inference" format: html: toc: true toc-depth: 3 number-sections: true - code-tools: true execute: enabled: false --- diff --git a/docs/input_output.qmd b/docs/input_output.qmd index 6559578d18..f9d2df2336 100644 --- a/docs/input_output.qmd +++ b/docs/input_output.qmd @@ -3,263 +3,4 @@ title: Template-free prompt construction description: "Template-free prompt construction with the `input_output` format" --- - - -- [Background](#background) - - [Masking Inputs](#masking-inputs) - - [You may not want prompt templates](#you-may-not-want-prompt-templates) - - [The `input_output` format](#the-input_output-format) -- [Usage](#usage) - - [1. Prepare Data](#1-prepare-data) - - [2. Use `type: input_output`](#2-use-type-input_output) - - [3. Check the prompts](#3-check-the-prompts) - - - - - -## Background - - - -### Masking Inputs - -One of the most popular features of -[axolotl](https://github.com/axolotl-ai-cloud/axolotl) is -setting the following configuration value: - - -```yaml -train_on_inputs: false -``` - -If you declare a [dataset formats](https://github.com/axolotl-ai-cloud/axolotl?tab=readme-ov-file#dataset) -such as `alpaca` or `chatml`, axolotl knows what is an input -(i.e. human) vs. an output (i.e. the assistant) and masks the input -labels so that your model can focus on predicting the outputs only. - - - -### You may not want prompt templates - -However, there are many situations where you don't want to use one of -these formats or templates. This is because they can: - -- Add unnecessary boilerplate to your prompts. -- Create artifacts like special delimiters `<|im_start|>` that can - quickly become footguns if you don't include them correctly at - inference time. -- Enforce a *chat* interface when you do not want one. Sometimes you - just want to fine-tune a model to a very specific task and do NOT - want multi-turn conversations, roles, etc. -- Limit you to only certain roles that the template allows. - - - -### The `input_output` format - -You can construct your prompts without a template by using the -`input_output` format, by setting `type: input_output` in your -configuration file like this: - -**config.yml** - -```yaml -train_on_inputs: false # Mask segments of your data -datasets: - - path: output.jsonl - type: input_output # use template free prompt construction -``` - -Unlike `type: completion`, which is also template-free, -`type: input_output` allows you to mask segments of your text. More -details on how this works are described below. - - - -## Usage - -This is how you can use the `input_output` format: - - - -### 1. Prepare Data - -To use the `input_output` format, collect your data in the following -format into a jsonl file (below is the first row from the file -`output`.jsonl` pretty printed): - -```bash -$ head -n1 output.jsonl | python -m json.tool -``` - -:::{.cell-output .cell-output-stdout} - { - "segments": [ - { - "label": true, - "text": "Hello\n" - }, - { - "label": true, - "text": "hi there!. " - }, - { - "label": false, - "text": "goodbye " - }, - { - "label": true, - "text": "farewell" - } - ] - } -::: - -Set `label:false` when you want to mask a segment of text so that the -model isn't trained on it. Some things to keep in mind: - -> [!IMPORTANT] -> 1. **EOS, BOS, spaces, newlines etc. are entirely up to you. Axolotl - concatenates all the segments as-is.** The tokenizer doesn't add - anything additional. Notice how I added spaces, newlines, `` - (BOS), and `` (EOS) myself. -> 2. Make sure you check the materialized output to validate that the - prompt is getting assembled how you like. - - - -### 2. Use `type: input_output` - -Let's materialize data with our `output.jsonl` file by setting -`type: input_output` in our axolotl config: - -```yaml -# training_config.yaml -base_model: mistralai/Mistral-7B-v0.1 -data_seed: 49 -seed: 49 - -datasets: - - path: output.jsonl - type: input_output -val_set_size: 0.1 - -sequence_len: 896 -sample_packing: false - -micro_batch_size: 2 -gradient_accumulation_steps: 3 -eval_batch_size: 2 -num_epochs: 1 -learning_rate: 0.0002 - -train_on_inputs: false -special_tokens: - bos_token: "" - eos_token: "" - unk_token: "" -``` - -You can use the following command to materialize your data. The -`--debug` flag will print the tokens, along with the labels so you can -verify that the correct items are being ignored: - -```bash -$ python -m axolotl.cli.preprocess training_config.yaml --debug - -... -[2024-03-05 23:36:46,969] [INFO] [axolotl.check_example_labels:35] [PID:607731] [RANK:0] (1, 1) Hello(22557, 22557) -(13, 13) hi(12014, 12014) there(736, 736) !(28808, 28808) .(28723, 28723) (28705, 28705) good(-100, 1179) bye(-100, 17664) (-100, 28705) fare(19111, 19111) well(5458, 5458) (2, 2) - -``` - -The format is `decoded_token`(`label`, `token_id`), for example, -`(1, 1)` means that the token is ``, the label is `1` and the -token_id is `1`. When the label is `-100` then that token is ignored for -training. - - - -### 3. Check the prompts - -Here is another way to check the materialized output: - -```python -from transformers import AutoTokenizer -from datasets import load_from_disk -import yaml - -directory = !ls last_run_prepared/ -with open('training_config.yaml', 'r') as f: - cfg = yaml.safe_load(f) -model_id = cfg['base_model'] -tok = AutoTokenizer.from_pretrained(model_id) -ds = load_from_disk(f'last_run_prepared/{directory[0]}/') -``` - -```python ->>> row = ds[0] ->>> print(tok.decode(row['input_ids'])) - Hello - hi there!. goodbye farewell -``` - -We can check that the right tokens are ignored by comparing the labels -to each token: - -```python -import pandas as pd -pd.DataFrame([{'token': tok.decode(i), 'label': l, 'id':i} for i,l in - zip(row['input_ids'], row['labels'])]) -``` - -| token | label | id | -|-------|-------|-------| -| 0 | \ | 1 | -| 1 | Hello | 22557 | -| 2 | \\n | 13 | -| 3 | hi | 12014 | -| 4 | there | 736 | -| 5 | ! | 28808 | -| 6 | . | 28723 | -| 7 | | 28705 | -| 8 | good | -100 | -| 9 | bye | -100 | -| 10 | | -100 | -| 11 | fare | 19111 | -| 12 | well | 5458 | -| 13 | \| 2 | - - - -If we look at the input data, the above table seems correct! (The jsonl -version is repeated below for reference): - - -```bash -$ head -n1 output.jsonl | python -m json.tool -``` - -:::{.cell-output .cell-output-stdout} - { - "segments": [ - { - "label": true, - "text": "Hello\n" - }, - { - "label": true, - "text": "hi there!. " - }, - { - "label": false, - "text": "goodbye " - }, - { - "label": true, - "text": "farewell" - } - ] - } -::: +The documentation moved to [here](dataset-formats/template_free.qmd). diff --git a/docs/installation.qmd b/docs/installation.qmd index f16e814ccc..2be74be0f4 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -1,11 +1,10 @@ --- -title: "Installation Guide" +title: "Installation" format: html: toc: true toc-depth: 3 number-sections: true - code-tools: true execute: enabled: false --- diff --git a/docs/lora_optims.qmd b/docs/lora_optims.qmd index eaeeed260e..8bee20402e 100644 --- a/docs/lora_optims.qmd +++ b/docs/lora_optims.qmd @@ -1,7 +1,6 @@ --- title: "LoRA Optimizations" -description: "Custom autograd functions and Triton kernels in Axolotl for optimized -LoRA fine-tuning" +description: "Custom autograd functions and Triton kernels in Axolotl for optimized LoRA fine-tuning" --- Inspired by [Unsloth](https://github.com/unslothai/unsloth), we've implemented two diff --git a/docs/mac.qmd b/docs/mac.qmd index 2a83035381..2e6c5c429e 100644 --- a/docs/mac.qmd +++ b/docs/mac.qmd @@ -19,4 +19,5 @@ Current support: - [ ] DeepSpeed Untested: + - FSDP diff --git a/docs/multi-gpu.qmd b/docs/multi-gpu.qmd index fe293b750b..19293bb5b7 100644 --- a/docs/multi-gpu.qmd +++ b/docs/multi-gpu.qmd @@ -1,5 +1,5 @@ --- -title: "Multi-GPU Training Guide" +title: "Multi-GPU" format: html: toc: true @@ -35,7 +35,11 @@ deepspeed: deepspeed_configs/zero1.json ### Usage {#sec-deepspeed-usage} ```{.bash} -accelerate launch -m axolotl.cli.train examples/llama-2/config.yml --deepspeed deepspeed_configs/zero1.json +# Passing arg via config +axolotl train config.yml + +# Passing arg via cli +axolotl train config.yml --deepspeed deepspeed_configs/zero1.json ``` ### ZeRO Stages {#sec-zero-stages} @@ -70,25 +74,7 @@ For combining FSDP with QLoRA, see our [dedicated guide](fsdp_qlora.qmd). ### Liger Kernel Integration {#sec-liger} -::: {.callout-note} -Liger Kernel provides efficient Triton kernels for LLM training, offering: - -- 20% increase in multi-GPU training throughput -- 60% reduction in memory usage -- Compatibility with both FSDP and DeepSpeed -::: - -Configuration: - -```{.yaml} -plugins: - - axolotl.integrations.liger.LigerPlugin -liger_rope: true -liger_rms_norm: true -liger_glu_activation: true -liger_layer_norm: true -liger_fused_linear_cross_entropy: true -``` +Please see [docs](custom_integrations.qmd#liger) for more info. ## Troubleshooting {#sec-troubleshooting} diff --git a/docs/multi-node.qmd b/docs/multi-node.qmd index aa6704ab9e..cec8ff45df 100644 --- a/docs/multi-node.qmd +++ b/docs/multi-node.qmd @@ -13,7 +13,7 @@ You will also need to have the same configuration file for your model on each ma Make sure the main machine is reachable by other machines. ::: -# Accelerate +## Accelerate You will need to create a configuration for accelerate, either by using `accelerate config` and follow the instructions or you can use one of the preset below: @@ -51,17 +51,17 @@ fsdp_config: All you have to do now is launch using accelerate as you would usually do on each machine and voila, the processes will start once you have launched accelerate on every machine. -# Raytrain +## Raytrain Please see ray train doc [here](ray-integration.qmd). -# Torchrun +## Torchrun If you are using Infiniband, we recommend torchrun to utilize the full bandwidth. Set the following env (change buffersize/socketname depending on your system): -```yaml +```bash export NCCL_IB_DISABLE=0 export NCCL_SOCKET_IFNAME="eth0,en,eth,em,bond" export NCCL_BUFFSIZE=2097152 diff --git a/docs/nccl.qmd b/docs/nccl.qmd index 3b616aa665..bd2a744124 100644 --- a/docs/nccl.qmd +++ b/docs/nccl.qmd @@ -13,13 +13,13 @@ Often, this timeout will happen after 30 minutes (the default setting) and is ac Forcing cross-GPU communication via [NVLink](https://en.wikipedia.org/wiki/NVLink) may help without increasing timeouts. To verify that your configuration is leveraging NVLink run the following command: -```shell +```bash nvidia-smi nvlink --status ``` To force NCCL to use NVLink, simply set this in the environment: -```shell +```bash export NCCL_P2P_LEVEL=NVL ``` @@ -33,13 +33,13 @@ If NVLink is not available in your environment there are other options for ``NCC To validate that acceptable data transfer speeds exist for your training job, running [NCCL Tests](https://github.com/NVIDIA/nccl-tests/blob/master/README.md) can help pinpoint bottlenecks, for example: -```shell +```bash ./build/all_reduce_perf -b 8 -e 128M -f 2 -g 3 ``` It can be useful when debugging NCCL communication timeouts to activate additional logging in both PyTorch and NCCL: -```shell +```bash export NCCL_DEBUG=INFO export NCCL_DEBUG_SUBSYS=ALL export TORCH_DISTRIBUTED_DEBUG=INFO diff --git a/docs/ray-integration.qmd b/docs/ray-integration.qmd index 0a2b45ef5b..edf9e2dafd 100644 --- a/docs/ray-integration.qmd +++ b/docs/ray-integration.qmd @@ -1,5 +1,5 @@ --- -title: Ray Train integration +title: Ray Train description: How to use Axolotl with Ray Train --- @@ -9,7 +9,7 @@ With the `--use-ray` CLI flag, Axolotl will use Ray Train's [`TorchTrainer`](htt ## Ray cluster setup -A prerequisite using the Ray Train integration is to setup a Ray cluster on your desired node(s). For a detailed guide on how you can get started with ray clusters, check the official Ray docs here: https://docs.ray.io/en/latest/cluster/getting-started.html +A prerequisite using the Ray Train integration is to setup a Ray cluster on your desired node(s). For a detailed guide on how you can get started with ray clusters, check the official Ray docs [here](https://docs.ray.io/en/latest/cluster/getting-started.html). Every Ray cluster has one _head_ node and a set of worker nodes. The head node is just like any other worker node, but it also runs certain special processes related to scheduling and orchestration. Ray-enabled scripts are run on the head node and depending on the resources (number of CPUs, GPUs, etc) they request, will be scheduled to run certain tasks on the worker nodes. For more on key concepts behind a Ray cluster, you can refer this [doc](https://docs.ray.io/en/latest/cluster/key-concepts.html#cluster-key-concepts). @@ -58,13 +58,11 @@ You can find an example configuration at `configs/llama-3/lora-1b-ray.yaml`. The key parameters to note here are: ```yaml -... use_ray: true ray_num_workers: 4 # optional resources_per_worker: GPU: 1 -... ``` - `use_ray`: This is the flag that enables the Ray Train integration. You can either use the corresponding `--use-ray` flag in the CLI or set `use_ray` in the config file. diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index ff150ffbe1..741976bc68 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -3,22 +3,22 @@ title: "RLHF (Beta)" description: "Reinforcement Learning from Human Feedback is a method whereby a language model is optimized from data using human feedback." back-to-top-navigation: true toc: true -toc-depth: 3 +toc-depth: 4 --- -# Overview +## Overview Reinforcement Learning from Human Feedback is a method whereby a language model is optimized from data using human feedback. Various methods include, but not limited to: -- Proximal Policy Optimization (PPO) (not yet supported in axolotl) - [Direct Preference Optimization (DPO)](#dpo) - [Identity Preference Optimization (IPO)](#ipo) - [Kahneman-Tversky Optimization (KTO)](#kto) - [Odds Ratio Preference Optimization (ORPO)](#orpo) +- Proximal Policy Optimization (PPO) (not yet supported in axolotl) -# RLHF using Axolotl +## RLHF using Axolotl ::: {.callout-important} This is a BETA feature and many features are not fully implemented. You are encouraged to open new PRs to improve the integration and functionality. @@ -30,7 +30,7 @@ We rely on the [TRL](https://github.com/huggingface/trl) library for implementat You can find what each method supports by going into `src/axolotl/prompt_strategies/{method}` where `{method}` is one of our supported methods. The `type: ` can be retrieved from `{method}.{function_name}`. ::: -## DPO +### DPO Example config: @@ -47,7 +47,7 @@ datasets: DPO supports the following types with the following dataset format: -### chatml.argilla +#### chatml.argilla ```json { @@ -58,7 +58,7 @@ DPO supports the following types with the following dataset format: } ``` -### chatml.argilla_chat +#### chatml.argilla_chat ```json { @@ -73,7 +73,7 @@ DPO supports the following types with the following dataset format: } ``` -### chatml.icr +#### chatml.icr ```json { @@ -84,7 +84,7 @@ DPO supports the following types with the following dataset format: } ``` -### chatml.intel +#### chatml.intel ```json { @@ -95,7 +95,7 @@ DPO supports the following types with the following dataset format: } ``` -### chatml.prompt_pairs +#### chatml.prompt_pairs ```json { @@ -106,7 +106,7 @@ DPO supports the following types with the following dataset format: } ``` -### chatml.ultra +#### chatml.ultra ```json { @@ -123,7 +123,7 @@ DPO supports the following types with the following dataset format: } ``` -### llama3.argilla +#### llama3.argilla ```json { @@ -134,7 +134,7 @@ DPO supports the following types with the following dataset format: } ``` -### llama3.argilla_chat +#### llama3.argilla_chat ```json { @@ -149,7 +149,7 @@ DPO supports the following types with the following dataset format: } ``` -### llama3.icr +#### llama3.icr ```json { @@ -160,7 +160,7 @@ DPO supports the following types with the following dataset format: } ``` -### llama3.intel +#### llama3.intel ```json { @@ -171,7 +171,7 @@ DPO supports the following types with the following dataset format: } ``` -### llama3.prompt_pairs +#### llama3.prompt_pairs ```json { @@ -182,7 +182,7 @@ DPO supports the following types with the following dataset format: } ``` -### llama3.ultra +#### llama3.ultra ```json { @@ -199,7 +199,7 @@ DPO supports the following types with the following dataset format: } ``` -### zephyr.nectar +#### zephyr.nectar ```json { @@ -218,7 +218,7 @@ DPO supports the following types with the following dataset format: } ``` -### chat_template.default +#### chat_template.default ```yaml rl: dpo @@ -264,7 +264,7 @@ Sample input format: } ``` -### user_defined.default +#### user_defined.default For custom behaviors, @@ -295,7 +295,7 @@ The input format is a simple JSON input with customizable fields based on the ab } ``` -## IPO +### IPO As IPO is just DPO with a different loss function, all supported options for DPO works here. @@ -303,7 +303,7 @@ As IPO is just DPO with a different loss function, all supported options for DPO rl: ipo ``` -## ORPO +### ORPO Paper: https://arxiv.org/abs/2403.07691 @@ -320,7 +320,7 @@ datasets: ORPO supports the following types with the following dataset format: -### chat_template.argilla +#### chat_template.argilla ```json { @@ -339,7 +339,7 @@ ORPO supports the following types with the following dataset format: } ``` -## KTO +### KTO ```yaml rl: kto @@ -360,7 +360,7 @@ gradient_checkpointing_kwargs: KTO supports the following types with the following dataset format: -### chatml.argilla +#### chatml.argilla ```json { @@ -370,7 +370,7 @@ KTO supports the following types with the following dataset format: } ``` -### chatml.argilla_chat +#### chatml.argilla_chat ```json { @@ -383,7 +383,7 @@ KTO supports the following types with the following dataset format: } ``` -### chatml.intel +#### chatml.intel ```json { @@ -393,7 +393,7 @@ KTO supports the following types with the following dataset format: } ``` -### chatml.prompt_pairs +#### chatml.prompt_pairs ```json { @@ -403,7 +403,7 @@ KTO supports the following types with the following dataset format: } ``` -### chatml.ultra +#### chatml.ultra ```json { @@ -413,7 +413,7 @@ KTO supports the following types with the following dataset format: } ``` -### llama3.argilla +#### llama3.argilla ```json { @@ -423,7 +423,7 @@ KTO supports the following types with the following dataset format: } ``` -### llama3.argilla_chat +#### llama3.argilla_chat ```json { @@ -434,7 +434,7 @@ KTO supports the following types with the following dataset format: } ``` -### llama3.intel +#### llama3.intel ```json { @@ -444,7 +444,7 @@ KTO supports the following types with the following dataset format: } ``` -### llama3.prompt_pairs +#### llama3.prompt_pairs ```json { @@ -454,7 +454,7 @@ KTO supports the following types with the following dataset format: } ``` -### llama3.ultra +#### llama3.ultra ```json { @@ -464,7 +464,7 @@ KTO supports the following types with the following dataset format: } ``` -### user_defined.default +#### user_defined.default For custom behaviors, @@ -494,7 +494,49 @@ The input format is a simple JSON input with customizable fields based on the ab } ``` -## Using local dataset files +### GRPO + +GRPO uses custom reward functions and transformations. Please have them ready locally. + +For ex, to load OpenAI's GSM8K and use a random reward for completions: + +```python +# rewards.py +import random + +def rand_reward_func(completions, **kwargs) -> list[float]: + return [random.uniform(0, 1) for _ in completions] + +def oai_gsm8k_transform(cfg, *args, **kwargs): + def transform_fn(example, tokenizer=None): + label = example["answer"].split("####")[-1].strip().replace(",", "") + return { + "prompt": [{"role": "user", "content": example["question"]},], + "answer": label, + } + return transform_fn, {"remove_columns": ["question"]} +``` + +```yaml +rl: grpo + +trl: + beta: 0.001 + max_completion_length: 256 + use_vllm: True + vllm_device: auto + vllm_gpu_memory_utilization: 0.15 + num_generations: 4 + reward_funcs: ["rewards.rand_reward_func"] # format: '{file_name}.{fn_name}' +datasets: + - path: openai/gsm8k + name: main + type: rewards.oai_gsm8k_transform # format: '{file_name}.{fn_name}' +``` + +To see other examples of custom reward functions, please see [TRL GRPO Docs](https://github.com/huggingface/trl/blob/main/docs/source/grpo_trainer.md#using-a-custom-reward-function). + +### Using local dataset files ```yaml datasets: @@ -505,7 +547,7 @@ datasets: type: chatml.intel ``` -## TRL auto-unwrapping for PEFT +### TRL auto-unwrapping for PEFT TRL supports auto-unwrapping PEFT models for RL training paradigms which rely on a reference model. This significantly reduces memory pressure as an additional refreference model does not need to be loaded, and reference model log-probabilities can be obtained by disabling PEFT adapters. This is enabled by default. To turn it off, pass the following config: diff --git a/docs/torchao.qmd b/docs/torchao.qmd index 2dc9117fbf..f50fc9ee13 100644 --- a/docs/torchao.qmd +++ b/docs/torchao.qmd @@ -3,6 +3,12 @@ title: "PyTorch ao" description: "Custom data types and layouts for training and inference" --- +To use experimental optimizers (`AdamWFp8`, `AdamW4bit`, `AdamW8bit`) from Pytorch Ao, please install the package as shown below. + +::: {.callout-tip} +Some experimental optimizers are already present in regular Pytorch, so please re-check if you actually need this package! +::: + ### Installation Stable Release from the PyTorch index diff --git a/docs/unsloth.qmd b/docs/unsloth.qmd index 73b2f03036..fd87f7bde0 100644 --- a/docs/unsloth.qmd +++ b/docs/unsloth.qmd @@ -8,6 +8,12 @@ description: "Hyper-optimized QLoRA finetuning for single GPUs" Unsloth provides hand-written optimized kernels for LLM finetuning that slightly improve speed and VRAM over standard industry baselines. +::: {.callout-important} +Due to breaking changes in transformers `v4.48.0`, users will need to downgrade to `<=v4.47.1` to use this patch. + +This will later be deprecated in favor of [LoRA Optimizations](lora_optims.qmd). +::: + ### Installation @@ -17,7 +23,7 @@ The following will install the correct unsloth and extras from source. python scripts/unsloth_install.py | sh ``` -### Using unsloth w Axolotl +### Usage Axolotl exposes a few configuration options to try out unsloth and get most of the performance gains. diff --git a/index.qmd b/index.qmd index ed23d235ad..01572a8bec 100644 --- a/index.qmd +++ b/index.qmd @@ -1,7 +1,7 @@ --- -toc-location: right-body -toc-title: Table Of Contents -toc-expand: 2 +# toc-location: right-body +# toc-title: Table Of Contents +# toc-expand: 2 --- ```{python} diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index c67d7440b9..625cbc0ecc 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -1,6 +1,10 @@ # Cut Cross Entropy -### Usage +Cut Cross Entropy reduces VRAM usage through optimization on the cross-entropy operation during loss calculation. + +See https://github.com/apple/ml-cross-entropy + +## Usage ```yaml plugins: @@ -8,3 +12,19 @@ plugins: cut_cross_entropy: true ``` + +## Citation + +```bib +@article{wijmans2024cut, + author = {Erik Wijmans and + Brody Huval and + Alexander Hertzberg and + Vladlen Koltun and + Philipp Kr\"ahenb\"uhl}, + title = {Cut Your Losses in Large-Vocabulary Language Models}, + journal = {arXiv}, + year = {2024}, + url = {https://arxiv.org/abs/2411.09009}, +} +``` diff --git a/src/axolotl/integrations/grokfast/README.md b/src/axolotl/integrations/grokfast/README.md index 4950dde87a..7c678b07e7 100644 --- a/src/axolotl/integrations/grokfast/README.md +++ b/src/axolotl/integrations/grokfast/README.md @@ -2,7 +2,7 @@ See https://github.com/ironjr/grokfast -### Usage +## Usage ```yaml plugins: @@ -11,3 +11,14 @@ plugins: grokfast_alpha: 2.0 grokfast_lamb: 0.98 ``` + +## Citation + +```bib +@article{lee2024grokfast, + title={{Grokfast}: Accelerated Grokking by Amplifying Slow Gradients}, + author={Lee, Jaerin and Kang, Bong Gyun and Kim, Kihoon and Lee, Kyoung Mu}, + journal={arXiv preprint arXiv:2405.20233}, + year={2024} +} +``` diff --git a/src/axolotl/integrations/kd/README.md b/src/axolotl/integrations/kd/README.md new file mode 100644 index 0000000000..4b15ad31dd --- /dev/null +++ b/src/axolotl/integrations/kd/README.md @@ -0,0 +1,23 @@ +# Knowledge Distillation + +## Usage + +```yaml +plugins: + - "axolotl.integrations.kd.KDPlugin" + +kd_trainer: True +kd_ce_alpha: 0.1 +kd_alpha: 0.9 +kd_temperature: 1.0 + +torch_compile: True # torch>=2.5.1, recommended to reduce vram + +datasets: + - path: ... + type: "axolotl.integrations.kd.chat_template" + field_messages: "messages_combined" + logprobs_field: "llm_text_generation_vllm_logprobs" # for kd only, field of logprobs +``` + +An example dataset can be found at [`axolotl-ai-co/evolkit-logprobs-pipeline-75k-v2-sample`](https://huggingface.co/datasets/axolotl-ai-co/evolkit-logprobs-pipeline-75k-v2-sample) diff --git a/src/axolotl/integrations/liger/README.md b/src/axolotl/integrations/liger/README.md new file mode 100644 index 0000000000..16164d72f2 --- /dev/null +++ b/src/axolotl/integrations/liger/README.md @@ -0,0 +1,36 @@ +# Liger Kernel Integration + +Liger Kernel provides efficient Triton kernels for LLM training, offering: + +- 20% increase in multi-GPU training throughput +- 60% reduction in memory usage +- Compatibility with both FSDP and DeepSpeed + +See https://github.com/linkedin/Liger-Kernel + +## Usage + +```yaml +plugins: + - axolotl.integrations.liger.LigerPlugin +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true +``` + +## Citation + +```bib +@article{hsu2024ligerkernelefficienttriton, + title={Liger Kernel: Efficient Triton Kernels for LLM Training}, + author={Pin-Lun Hsu and Yun Dai and Vignesh Kothapalli and Qingquan Song and Shao Tang and Siyu Zhu and Steven Shimizu and Shivam Sahni and Haowen Ning and Yanning Chen}, + year={2024}, + eprint={2410.10989}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2410.10989}, + journal={arXiv preprint arXiv:2410.10989}, +} +``` diff --git a/src/axolotl/integrations/lm_eval/README.md b/src/axolotl/integrations/lm_eval/README.md index 3724c49ccf..f6ed5416ec 100644 --- a/src/axolotl/integrations/lm_eval/README.md +++ b/src/axolotl/integrations/lm_eval/README.md @@ -1,6 +1,10 @@ # LM Eval Harness -### Usage +Run evaluation on model using the popular lm-evaluation-harness library. + +See https://github.com/EleutherAI/lm-evaluation-harness + +## Usage ```yaml plugins: @@ -10,4 +14,22 @@ lm_eval_tasks: - gsm8k - hellaswag - arc_easy + +lm_eval_batch_size: # Batch size for evaluation +output_dir: # Directory to save evaluation results +``` + +## Citation + +```bib +@misc{eval-harness, + author = {Gao, Leo and Tow, Jonathan and Abbasi, Baber and Biderman, Stella and Black, Sid and DiPofi, Anthony and Foster, Charles and Golding, Laurence and Hsu, Jeffrey and Le Noac'h, Alain and Li, Haonan and McDonell, Kyle and Muennighoff, Niklas and Ociepa, Chris and Phang, Jason and Reynolds, Laria and Schoelkopf, Hailey and Skowron, Aviya and Sutawika, Lintang and Tang, Eric and Thite, Anish and Wang, Ben and Wang, Kevin and Zou, Andy}, + title = {A framework for few-shot language model evaluation}, + month = 07, + year = 2024, + publisher = {Zenodo}, + version = {v0.4.3}, + doi = {10.5281/zenodo.12608602}, + url = {https://zenodo.org/records/12608602} +} ``` diff --git a/src/axolotl/integrations/spectrum/README.md b/src/axolotl/integrations/spectrum/README.md index 192918060e..0f78a511b5 100644 --- a/src/axolotl/integrations/spectrum/README.md +++ b/src/axolotl/integrations/spectrum/README.md @@ -1,15 +1,17 @@ -## Spectrum: Targeted Training on Signal to Noise Ratio +# Spectrum: Targeted Training on Signal to Noise Ratio by Eric Hartford, Lucas Atkins, Fernando Fernandes, David Golchinfar This plugin contains code to freeze the bottom fraction of modules in a model, based on the Signal-to-Noise Ratio (SNR). -### Overview +See https://github.com/cognitivecomputations/spectrum + +## Overview Spectrum is a tool for scanning and evaluating the Signal-to-Noise Ratio (SNR) of layers in large language models. By identifying the top n% of layers with the highest SNR, you can optimize training efficiency. -### Usage +## Usage ```yaml plugins: @@ -19,3 +21,17 @@ spectrum_top_fraction: 0.5 # Optional if using a pre-scanned model as your base_model. Useful if using a model mirror spectrum_model_name: meta-llama/Meta-Llama-3.1-8B ``` + +## Citation + +```bib +@misc{hartford2024spectrumtargetedtrainingsignal, + title={Spectrum: Targeted Training on Signal to Noise Ratio}, + author={Eric Hartford and Lucas Atkins and Fernando Fernandes Neto and David Golchinfar}, + year={2024}, + eprint={2406.06623}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2406.06623}, +} +``` diff --git a/styles.css b/styles.css index 2e5aa6de8f..891349b4b0 100644 --- a/styles.css +++ b/styles.css @@ -1,5 +1,193 @@ -/* css styles */ +/* TYPOGRAPHY SECTION */ -img[alt="Axolotl"] { - content: url("https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/887513285d98132142bf5db2a74eb5e0928787f1/image/axolotl_logo_digital_black.svg") !important; +/* Import fonts */ +@import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&display=swap'); + +/* Typography hierarchy */ +:root { + --font-title: 'Be Vietnam Pro', sans-serif; + --font-body: 'JetBrains Mono', monospace; +} + +/* Title (h1) */ +h1 { + font-family: var(--font-title); + font-weight: 400; + font-size: 6rem; + line-height: 1.1; + letter-spacing: -0.05em; + font-feature-settings: "ss01" on; +} + +/* Heading (h2) */ +h2 { + font-family: var(--font-title); + font-weight: 500; + font-size: 2rem; + line-height: 1.2; + letter-spacing: -0.03em; + font-feature-settings: "ss01" on; +} + +/* Subtitle/Preamble */ +h3, +h4 { + font-family: var(--font-body); + font-weight: 400; + font-size: 1.5rem; + line-height: 1.5; + letter-spacing: -0.02em; +} + +/* Body text */ +body { + font-family: var(--font-body); + font-weight: 400; + font-size: 1rem; + line-height: 1.5; + letter-spacing: -0.02em; +} + +/* Links */ +a { + font-family: var(--font-body); + font-weight: 400; + font-size: 0.875rem; + line-height: 1; + letter-spacing: -0.02em; +} + +/* NAV BAR SECTION */ + +/* Navbar logo styling */ +.navbar-brand img { + height: 32px; + margin-right: 10px; +} + +/* COLORS SECTION */ + +/* Brand colors */ +:root { + --white: #ffffff; + --greige-300: #EEEEE7; + --greige-600: #CCCAC0; + --black: #141310; + --lime: #E3F8A8; + --cyan: #A0F4EA; + --purple: #C8D0F8; +} + +/* Base styles */ +body { + background-color: var(--black); + color: var(--greige-300); +} + +/* Navigation */ +.navbar { + background-color: var(--black) !important; +} + +.navbar-dark .navbar-nav .nav-link { + color: var(--greige-300); +} + +.navbar-dark .navbar-nav .nav-link:hover { + color: var(--lime); +} + +/* Sidebar */ +.sidebar-navigation { + background-color: var(--black); + border-right: 1px solid var(--greige-600); +} + +.sidebar nav[role="doc-toc"] ul>li>a { + color: var(--greige-300); +} + +.sidebar nav[role="doc-toc"] ul>li>a:hover { + color: var(--lime); +} + +/* Links */ +a { + color: var(--lime); +} + +a:hover { + color: var(--cyan); +} + +/* Headers */ +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--white); +} + +/* Code blocks */ +pre { + background-color: #1a1a1a !important; + border: 1px solid var(--greige-600); +} + +/* Tables */ +.table { + color: var(--greige-300); +} + +/* TOC */ +#toc-title { + color: var(--white); +} + +.toc-active { + color: var(--lime) !important; +} + +/* Buttons */ +.btn-primary { + background-color: var(--lime); + color: var(--black); + border: none; +} + +.btn-primary:hover { + background-color: var(--cyan); + color: var(--black); +} + +/* For inline code (single backtick) */ +code { + background-color: #1a1a1a !important; + color: var(--lime) !important; + padding: 2px 4px; + border-radius: 4px; +} + +/* For inline code that is also a link */ +a code { + color: var(--cyan) !important; +} + +/* For code blocks (triple backtick) */ +pre.sourceCode { + background-color: #1a1a1a !important; +} + +/* Make comments in bash/shell scripts green */ +code span.co { + color: #5cb85c !important; +} + +/* Remove underlines from JSON comments and make them green */ +code span.er { + color: #5cb85c !important; + text-decoration: none !important; } From 75cbd1530172de318f7847250b75785f83c2007b Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 26 Feb 2025 01:50:02 +0700 Subject: [PATCH 0444/1405] Fix(doc): address missing doc changes (#2362) * fix: add multiple tips about eos_token masking * fix: format dataset preprocessing doc * Update docs/dataset-formats/conversation.qmd Co-authored-by: salman --------- Co-authored-by: salman --- docs/config.qmd | 3 ++- docs/dataset-formats/conversation.qmd | 8 +++++++- docs/dataset_preprocessing.qmd | 19 ++++++++++++++----- docs/faq.qmd | 4 ++++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index 8327e14883..3a11666a5a 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -166,7 +166,7 @@ datasets: # IMPORTANT: The following fields determine which parts of the conversation to train on. # Priority order: message_field_training > message_field_training_detail > train_on_inputs or role in roles_to_train # See examples at `docs/dataset-formats/conversation.qmd` - # Note: If the below 4 fields are empty, defaults to training only on the last message. + # Note: If the below 4 fields are set to empty, defaults to training only on the last message. # Optional[List[str]]. Roles to train on. The tokens from these roles will be considered for the loss. roles_to_train: ["assistant"] # default @@ -174,6 +174,7 @@ datasets: # - all: train on all EOS tokens # - turn (default): train on the EOS token at the end of each trainable turn # - last: train on the last EOS token in the conversation + # TIP: Please make sure that your `tokenizer.eos_token` is same as EOS/EOT token in template. Otherwise, set `eos_token` under `special_tokens`. train_on_eos: last # The key in the message turn that indicates via boolean whether tokens of a turn should be considered for training. Useful to selectively train on certain turns besides the `roles_to_train`. message_field_training: training diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index d67e35876b..8ce95b7b06 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -104,6 +104,10 @@ datasets: type: chat_template ``` +::: {.callout-important} +Please make sure that your `tokenizer.eos_token` is same as EOS/EOT token in template. Otherwise, set `eos_token` under `special_tokens`. +::: + 5. (Advanced) Using fine-grained control over tokens and turns to train in a conversation For a data sample that looks like: @@ -151,4 +155,6 @@ datasets: message_field_training_detail: train_detail ``` -Tip: It is not necessary to use both `message_field_training` and `message_field_training_detail` at a time. +::: {.callout-tip} +It is not necessary to set both `message_field_training` and `message_field_training_detail` at once. +::: diff --git a/docs/dataset_preprocessing.qmd b/docs/dataset_preprocessing.qmd index c99fce444e..1075dc8e56 100644 --- a/docs/dataset_preprocessing.qmd +++ b/docs/dataset_preprocessing.qmd @@ -3,8 +3,11 @@ title: Dataset Preprocessing description: How datasets are processed --- +## Overview + Dataset pre-processing is the step where Axolotl takes each dataset you've configured alongside -the (dataset format)[../dataset-formats/] and prompt strategies to: +the [dataset format](docs/dataset-formats) and prompt strategies to: + - parse the dataset based on the *dataset format* - transform the dataset to how you would interact with the model based on the *prompt strategy* - tokenize the dataset based on the configured model & tokenizer @@ -12,10 +15,12 @@ the (dataset format)[../dataset-formats/] and prompt strategies to: The processing of the datasets can happen one of two ways: -1. Before kicking off training by calling `python -m axolotl.cli.preprocess /path/to/your.yaml --debug` +1. Before kicking off training by calling `axolotl preprocess config.yaml --debug` 2. When training is started -What are the benefits of pre-processing? When training interactively or for sweeps +### What are the benefits of pre-processing? + +When training interactively or for sweeps (e.g. you are restarting the trainer often), processing the datasets can oftentimes be frustratingly slow. Pre-processing will cache the tokenized/formatted datasets according to a hash of dependent training parameters so that it will intelligently pull from its cache when possible. @@ -28,8 +33,12 @@ default path of `./last_run_prepared/`, but will ignore anything already cached setting `dataset_prepared_path: ./last_run_prepared`, the trainer will use whatever pre-processed data is in the cache. -What are the edge cases? Let's say you are writing a custom prompt strategy or using a user-defined +### What are the edge cases? + +Let's say you are writing a custom prompt strategy or using a user-defined prompt template. Because the trainer cannot readily detect these changes, we cannot change the -calculated hash value for the pre-processed dataset. If you have `dataset_prepared_path: ...` set +calculated hash value for the pre-processed dataset. + +If you have `dataset_prepared_path: ...` set and change your prompt templating logic, it may not pick up the changes you made and you will be training over the old prompt. diff --git a/docs/faq.qmd b/docs/faq.qmd index 0a181e022a..1b5037db93 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -46,3 +46,7 @@ description: Frequently asked questions **Q: `Content end boundary is the same as start boundary for turn ___. This is likely an empty turn.`** > A: This is likely an empty turn. + +**Q: The EOS/EOT token is incorrectly being masked or not being masked.** + +> A: This is because of the mismatch between `tokenizer.eos_token` and EOS/EOT token in template. Please make sure to set `eos_token` under `special_tokens` to the same EOS/EOT token as in template. From f4910dd2ea4321d1fa617c753c546ace3b081dc1 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 5 Mar 2025 08:58:33 -0500 Subject: [PATCH 0445/1405] `train.py` refactor (#2371) * refactor train.py * updates * update * combine like functions * review comments --- src/axolotl/common/datasets.py | 4 +- src/axolotl/core/trainer_builder.py | 18 +- src/axolotl/train.py | 449 ++++++++++++++++++++-------- src/axolotl/utils/trainer.py | 36 ++- 4 files changed, 365 insertions(+), 142 deletions(-) diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index db07eb43b0..3e712f7726 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -24,8 +24,8 @@ class TrainDatasetMeta: """Dataclass with fields for training and validation datasets and metadata.""" train_dataset: Dataset - eval_dataset: Optional[Dataset] = None - total_num_steps: Optional[int] = None + eval_dataset: Dataset | None = None + total_num_steps: int | None = None def sample_dataset(dataset: Dataset, num_samples: int) -> Dataset: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 12346b8a27..d4ddc9bf37 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -91,13 +91,11 @@ except ImportError: pass -LOG = logging.getLogger("axolotl.core.trainer_builder") +LOG = logging.getLogger(__name__) class TrainerBuilderBase(abc.ABC): - """ - Base class for trainer builder - """ + """Base class for trainer builder.""" _train_dataset = None _eval_dataset = None @@ -110,9 +108,9 @@ def __init__(self, cfg, model, tokenizer, processor=None): self.tokenizer = tokenizer self.processor = processor - # in case the model supports tagging, add the axolotl tag. + # If the model supports tagging, add the axolotl tag. # This makes sure the tag is correctly pushed even if a user calls - # model.push_to_hub instad of trainer.push_to_hub. + # model.push_to_hub instead of trainer.push_to_hub. if hasattr(model, "add_model_tags"): model.add_model_tags(["axolotl"]) @@ -227,8 +225,8 @@ def hook_post_create_trainer(self, trainer): class HFCausalTrainerBuilder(TrainerBuilderBase): """ - Build the HuggingFace training args/trainer for causal models - and reward modelling using TRL. + Build the HuggingFace training args/trainer for causal models and reward modeling + using TRL. """ def get_callbacks(self): @@ -872,9 +870,7 @@ def build_collator( class HFRLTrainerBuilder(TrainerBuilderBase): - """ - Trainer factory class for TRL-based RLHF trainers (e.g. DPO) - """ + """Trainer factory class for TRL-based RLHF trainers (e.g. DPO)""" def get_callbacks(self): callbacks = super().get_callbacks() diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 515248fced..b2f4bf1e99 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -1,26 +1,29 @@ """Prepare and train a model on a dataset. Can also infer from a model or merge lora""" +import importlib import inspect import os import signal import sys import weakref from pathlib import Path -from typing import Tuple, Union +from typing import Any import torch import transformers.modelcard from accelerate.logging import get_logger from accelerate.utils import save_fsdp_model -from peft import PeftModel -from pkg_resources import get_distribution # type: ignore -from transformers import PreTrainedModel, PreTrainedTokenizer +from datasets import Dataset +from peft import PeftConfig, PeftModel +from transformers import PreTrainedModel, PreTrainedTokenizer, ProcessorMixin from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled +from transformers.trainer import Trainer from axolotl.common.datasets import TrainDatasetMeta from axolotl.contribs.lgpl.unsloth import ( # pylint: disable = no-name-in-module fix_untrained_tokens, ) +from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder from axolotl.logging_config import configure_logging from axolotl.utils.dict import DictDefault from axolotl.utils.freeze import freeze_layers_except @@ -32,17 +35,25 @@ except ImportError: BetterTransformer = None -project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -src_dir = os.path.join(project_root, "src") -sys.path.insert(0, src_dir) - configure_logging() LOG = get_logger(__name__) -def train( - *, cfg: DictDefault, dataset_meta: TrainDatasetMeta -) -> Tuple[Union[PeftModel, PreTrainedModel], PreTrainedTokenizer]: +def setup_model_and_tokenizer( + cfg: DictDefault, +) -> tuple[ + PreTrainedModel, PreTrainedTokenizer, PeftConfig | None, ProcessorMixin | None +]: + """ + Load the tokenizer, processor (for multimodal models), and model based on configuration. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + + Returns: + Tuple containing model, tokenizer, `peft_config` (if LoRA / QLoRA, else + `None`), and processor (if multimodal, else `None`). + """ # Load tokenizer LOG.debug( f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}", @@ -55,35 +66,36 @@ def train( if cfg.is_multimodal: processor = load_processor(cfg, tokenizer) - # Get datasets - train_dataset = dataset_meta.train_dataset - eval_dataset = dataset_meta.eval_dataset - total_num_steps = dataset_meta.total_num_steps - - if cfg.resume_from_checkpoint is None and cfg.auto_resume_from_checkpoints: - possible_checkpoints = [ - str(cp) for cp in Path(cfg.output_dir).glob("checkpoint-*") - ] - if len(possible_checkpoints) > 0: - sorted_paths = sorted( - possible_checkpoints, - key=lambda path: int(path.split("-")[-1]), - ) - cfg.resume_from_checkpoint = sorted_paths[-1] - LOG.info( - f"Using Auto-resume functionality to start with checkpoint at {cfg.resume_from_checkpoint}" - ) - resume_from_checkpoint = cfg.resume_from_checkpoint - - # Load the model and tokenizer + # Load the model and peft_config msg = "loading model" if cfg.adapter: msg += " and peft_config..." LOG.debug(msg) + model, peft_config = load_model(cfg, tokenizer, processor=processor) if model.generation_config is not None: model.generation_config.do_sample = True + # Apply freezing if specified + if cfg.unfrozen_parameters: + freeze_layers_except(model, cfg.unfrozen_parameters) + + return model, tokenizer, peft_config, processor + + +def setup_reference_model( + cfg: DictDefault, tokenizer: PreTrainedTokenizer +) -> PreTrainedModel | None: + """ + Set up the reference model for RL training if needed. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + tokenizer: The tokenizer to use for the reference model. + + Returns: + Reference model if needed for RL training, `None` otherwise. + """ model_ref = None if cfg.rl and cfg.rl != "orpo": if cfg.adapter and not cfg.rl_adapter_ref_model: @@ -93,57 +105,48 @@ def train( else: # load the model again for model_ref/baseline model_ref, _ = load_model(cfg, tokenizer, reference_model=True) + return model_ref - safe_serialization = cfg.save_safetensors is True - if cfg.unfrozen_parameters: - freeze_layers_except(model, cfg.unfrozen_parameters) +def determine_resume_checkpoint(cfg: DictDefault) -> str | None: + """ + Determine the checkpoint to resume from based on configuration. - trainer = setup_trainer( - cfg, - train_dataset, - eval_dataset, - (model, model_ref, peft_config), - tokenizer, - processor, - total_num_steps, - ) + Args: + cfg: Dictionary mapping `axolotl` config keys to values. - if cfg.fix_untrained_tokens: - # check if the `token_ids_to_fix` kwarg exists in the fix_untrained_tokens args - sig = inspect.signature(fix_untrained_tokens) - # if the function has the `token_ids_to_fix` arg, and fix_untrained_tokens is a list - if "token_ids_to_fix" in sig.parameters and isinstance( - cfg.fix_untrained_tokens, list - ): - fix_untrained_tokens( - model, - tokenizer, - train_dataset, - token_ids_to_fix=cfg.fix_untrained_tokens, + Returns: + Path to the checkpoint to resume from, or `None` if not resuming. + """ + if cfg.resume_from_checkpoint is None and cfg.auto_resume_from_checkpoints: + possible_checkpoints = [ + str(cp) for cp in Path(cfg.output_dir).glob("checkpoint-*") + ] + if len(possible_checkpoints) > 0: + sorted_paths = sorted( + possible_checkpoints, + key=lambda path: int(path.split("-")[-1]), ) - else: - fix_untrained_tokens(model, tokenizer, train_dataset) - if cfg.local_rank == 0: - model.save_pretrained( - str(Path(cfg.output_dir)), safe_serialization=safe_serialization + cfg.resume_from_checkpoint = sorted_paths[-1] + LOG.info( + f"Using Auto-resume functionality to start with checkpoint at {cfg.resume_from_checkpoint}" ) + return cfg.resume_from_checkpoint - # go ahead and presave, so we have the adapter config available to inspect - if peft_config: - LOG.info(f"Pre-saving adapter config to {cfg.output_dir}") - peft_config.save_pretrained(cfg.output_dir) - # additionally presave the tokenizer and model configs - if not Path(cfg.output_dir).is_dir(): - os.makedirs(cfg.output_dir, exist_ok=True) - tokenizer.save_pretrained(str(Path(cfg.output_dir))) - if hasattr(model, "config"): - model.config.save_pretrained(str(Path(cfg.output_dir))) - # In case we want to stop early with ctrl+c, this is a nice to have to save the pretrained model - if ( - cfg.local_rank == 0 and not cfg.use_ray - ): # ray workers don't have access to this signal +def setup_signal_handler( + cfg: DictDefault, model: PreTrainedModel, safe_serialization: bool +): + """ + Set up signal handler for graceful termination. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + model: The model to save on termination + safe_serialization: Whether to use safe serialization when saving + """ + # ray workers don't have access to this signal + if cfg.local_rank == 0 and not cfg.use_ray: def terminate_handler(_, __, model_weakref): if model_weakref() is not None: @@ -161,21 +164,22 @@ def terminate_handler(_, __, model_weakref): lambda signum, frame: terminate_handler(signum, frame, _model_weakref), ) - badge_markdown = """[Built with Axolotl](https://github.com/axolotl-ai-cloud/axolotl)""" - transformers.modelcard.AUTOGENERATED_TRAINER_COMMENT += f"\n{badge_markdown}" - if getattr(cfg, "axolotl_config_path"): - raw_axolotl_cfg = Path(cfg.axolotl_config_path) - version = get_distribution("axolotl").version - if raw_axolotl_cfg.is_file(): - transformers.modelcard.AUTOGENERATED_TRAINER_COMMENT += f"\n
See axolotl config\n\naxolotl version: `{version}`\n```yaml\n{raw_axolotl_cfg.read_text(encoding='utf-8')}\n```\n\n

\n" +def execute_training( + cfg: DictDefault, trainer: Any, resume_from_checkpoint: str | None +): + """ + Execute the training process with appropriate backend configurations. + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + trainer: The configured trainer object. + resume_from_checkpoint: Path to checkpoint to resume from, if applicable. + """ LOG.info("Starting trainer...") if cfg.group_by_length: LOG.info("hang tight... sorting dataset for group_by_length") - pretrain_hooks(cfg, trainer) - if cfg.flash_optimum: with torch.backends.cuda.sdp_kernel( # TODO configure these from the YAML w/ sdp_kernel_kwargs: ... @@ -187,15 +191,30 @@ def terminate_handler(_, __, model_weakref): else: trainer.train(resume_from_checkpoint=resume_from_checkpoint) - post_train_hooks(cfg, trainer) - LOG.info(f"Training Completed!!! Saving pre-trained model to {cfg.output_dir}") +def save_trained_model( + cfg: DictDefault, + trainer: Any, + model: PreTrainedModel, + safe_serialization: bool, +): + """ + Save the trained model according to configuration and training setup. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + trainer: The trainer object. + model: The trained model to save. + safe_serialization: Whether to use safe serialization. + """ + LOG.info(f"Training completed! Saving pre-trained model to {cfg.output_dir}.") - # post training + # Post training module hooks for name, module in model.named_modules(): if hasattr(module, "_post_training"): module._post_training(model, name) # pylint: disable=protected-access + # Handle FSDP state dict type state_dict_type = "FULL_STATE_DICT" if trainer.is_fsdp_enabled: if cfg.fsdp_final_state_dict_type: @@ -203,16 +222,18 @@ def terminate_handler(_, __, model_weakref): trainer.accelerator.state.fsdp_plugin.set_state_dict_type(state_dict_type) LOG.info(f"Set FSDP state dict type to {state_dict_type} for saving.") + # Handle ReLoRA early return case if cfg.relora_steps: if cfg.adapter == "lora" and not (cfg.load_in_4bit or cfg.load_in_8bit): model = model.merge_and_unload() else: # final model weights have already been saved by `ReLoRACallback.on_train_end` - return model, tokenizer + return - # TODO do we need this fix? https://huggingface.co/docs/accelerate/usage_guides/fsdp#saving-and-loading - # only save on rank 0, otherwise it corrupts output on multi-GPU when multiple processes attempt to write the same file if cfg.fsdp: + # TODO: do we need this fix? https://huggingface.co/docs/accelerate/usage_guides/fsdp#saving-and-loading + # only save on rank 0, otherwise it corrupts output on multi-GPU when multiple + # processes attempt to write the same file if ( state_dict_type == "SHARDED_STATE_DICT" and cfg.fsdp_config.fsdp_state_dict_type == "SHARDED_STATE_DICT" @@ -244,7 +265,6 @@ def terminate_handler(_, __, model_weakref): os.remove(os.path.join(cfg.output_dir, "model.safetensors")) except FileNotFoundError: pass - elif cfg.local_rank == 0: if cfg.flash_optimum and BetterTransformer: model = BetterTransformer.reverse(model) @@ -255,58 +275,239 @@ def terminate_handler(_, __, model_weakref): ) model.save_pretrained(cfg.output_dir, safe_serialization=safe_serialization) + +def create_model_card(cfg: DictDefault, trainer: Trainer): + """ + Create a model card for the trained model if needed. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + trainer: The trainer object with model card creation capabilities. + """ if not cfg.hub_model_id: + # Guard since create_model_card may fail if dataset_tags is empty list try: model_card_kwarg = { "model_name": cfg.output_dir.lstrip("./") .encode("utf-8") .decode("utf-8") } - if cfg.datasets is not None: - if cfg.rl is not None or cfg.reward_model or cfg.process_reward_model: - dataset_tags = [ - d["path"] for d in cfg.datasets if not Path(d["path"]).is_dir() - ] - dataset_tags = [ - d for d in dataset_tags if not d.startswith("https://") - ] - if dataset_tags: - # guard as create_model_card may fail if dataset_tags is empty list - model_card_kwarg["dataset_name"] = dataset_tags - else: - dataset_tags = [ - d["path"] for d in cfg.datasets if not Path(d["path"]).is_dir() - ] - dataset_tags = [ - d for d in dataset_tags if not d.startswith("https://") - ] - if dataset_tags: - # guard as create_model_card may fail if dataset_tags is empty list - model_card_kwarg["dataset_tags"] = dataset_tags + + # We check if we're using a TRL trainer; if so, `dataset_tags` is not consumed. + rl = cfg.rl is not None or cfg.reward_model or cfg.process_reward_model + if cfg.datasets is not None and not rl: + dataset_tags = [ + d["path"] for d in cfg.datasets if not Path(d["path"]).is_dir() + ] + dataset_tags = [d for d in dataset_tags if not d.startswith("https://")] + + if dataset_tags: + model_card_kwarg["dataset_tags"] = dataset_tags trainer.create_model_card(**model_card_kwarg) except (AttributeError, UnicodeDecodeError): pass elif cfg.hub_model_id: - # defensively push to the hub to ensure the model card is updated + # Defensively push to the hub to ensure the model card is updated trainer.push_to_hub() - return model, tokenizer +def save_initial_configs( + cfg: DictDefault, + tokenizer: PreTrainedTokenizer, + model: PreTrainedModel, + peft_config: PeftConfig | None, +): + """ + Save initial configurations before training. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + tokenizer: The tokenizer to save. + model: The model to save configuration for. + peft_config: The PEFT configuration to save if applicable. + """ + # Create output_dir if it doesn't already exist + output_dir = Path(cfg.output_dir) + if not output_dir.is_dir(): + os.makedirs(cfg.output_dir, exist_ok=True) + + # Pre-save adapter config so it's available to inspect + if peft_config: + LOG.info(f"Pre-saving adapter config to {cfg.output_dir}...") + peft_config.save_pretrained(cfg.output_dir) + + # Pre-save the tokenizer and model configs + LOG.info(f"Pre-saving tokenizer to {cfg.output_dir}...") + tokenizer.save_pretrained(str(output_dir)) + if hasattr(model, "config"): + LOG.info(f"Pre-saving model config to {cfg.output_dir}...") + model.config.save_pretrained(str(output_dir)) + + +def setup_model_card(cfg: DictDefault): + """ + Set up the Axolotl badge and add the Axolotl config to the model card if available. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + """ + badge_markdown = """[Built with Axolotl](https://github.com/axolotl-ai-cloud/axolotl)""" + transformers.modelcard.AUTOGENERATED_TRAINER_COMMENT += f"\n{badge_markdown}" + + if getattr(cfg, "axolotl_config_path"): + raw_axolotl_cfg = Path(cfg.axolotl_config_path) + version = importlib.metadata.version("axolotl") + if raw_axolotl_cfg.is_file(): + transformers.modelcard.AUTOGENERATED_TRAINER_COMMENT += f"\n
See axolotl config\n\naxolotl version: `{version}`\n```yaml\n{raw_axolotl_cfg.read_text(encoding='utf-8')}\n```\n\n

\n" + + +def handle_untrained_tokens_fix( + cfg: DictDefault, + model: PreTrainedModel, + tokenizer: PreTrainedTokenizer, + train_dataset: Dataset, + safe_serialization: bool, +): + """ + Apply fixes for untrained tokens if configured. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + model: The model to apply fixes to. + tokenizer: The tokenizer for token identification. + train_dataset: The training dataset to use. + safe_serialization: Whether to use safe serialization when saving. + """ + if not cfg.fix_untrained_tokens: + return + + # Check if the `token_ids_to_fix` kwarg exists in the fix_untrained_tokens args + sig = inspect.signature(fix_untrained_tokens) + + # If the function has the `token_ids_to_fix` arg, and fix_untrained_tokens is a list + if "token_ids_to_fix" in sig.parameters and isinstance( + cfg.fix_untrained_tokens, list + ): + fix_untrained_tokens( + model, + tokenizer, + train_dataset, + token_ids_to_fix=cfg.fix_untrained_tokens, + ) + else: + fix_untrained_tokens(model, tokenizer, train_dataset) + + if cfg.local_rank == 0: + model.save_pretrained( + str(Path(cfg.output_dir)), safe_serialization=safe_serialization + ) -def pretrain_hooks(_cfg, _trainer): + +def setup_model_and_trainer( + cfg: DictDefault, dataset_meta: TrainDatasetMeta +) -> tuple[ + HFRLTrainerBuilder | HFCausalTrainerBuilder, + PeftModel | PreTrainedModel, + PreTrainedTokenizer, + PeftConfig | None, +]: """ - Run hooks right before kicking off the training - :param cfg: - :param trainer: - :return: + Load model, tokenizer, trainer, etc. Helper function to encapsulate the full + trainer setup. + + Args: + cfg: The configuration dictionary with training parameters. + dataset_meta: Object with training, validation datasets and metadata. + + Returns: + Tuple of: + - Trainer (Causal or RLHF) + - Model + - Tokenizer + - PEFT config """ + # Load tokenizer, processor and model + model, tokenizer, peft_config, processor = setup_model_and_tokenizer(cfg) + + # Set up reference model for RL if needed + model_ref = setup_reference_model(cfg, tokenizer) + + # Get datasets from metadata + train_dataset = dataset_meta.train_dataset + eval_dataset = dataset_meta.eval_dataset + total_num_steps = dataset_meta.total_num_steps + # Set up trainer + trainer = setup_trainer( + cfg=cfg, + train_dataset=train_dataset, + eval_dataset=eval_dataset, + model=model, + tokenizer=tokenizer, + processor=processor, + total_num_steps=total_num_steps, + model_ref=model_ref, + peft_config=peft_config, + ) -def post_train_hooks(_cfg, _trainer): + return ( + trainer, + model, + tokenizer, + peft_config, + ) + + +def train( + cfg: DictDefault, dataset_meta: TrainDatasetMeta +) -> tuple[PeftModel | PreTrainedModel, PreTrainedTokenizer]: """ - Run hooks right after training completes - :param cfg: - :param trainer: - :return: + Train a model on the given dataset. + + Args: + cfg: The configuration dictionary with training parameters + dataset_meta: Object with training, validation datasets and metadata + + Returns: + Tuple of (model, tokenizer) after training """ + # Setup model, tokenizer, (causal or RLHF) trainer etc. + ( + trainer, + model, + tokenizer, + peft_config, + ) = setup_model_and_trainer(cfg, dataset_meta) + + # Determine if we need to resume from a checkpoint + resume_from_checkpoint = determine_resume_checkpoint(cfg) + + # Configuration for saving + safe_serialization = cfg.save_safetensors is True + + # Handle untrained tokens if configured + train_dataset = dataset_meta.train_dataset + handle_untrained_tokens_fix( + cfg, model, tokenizer, train_dataset, safe_serialization + ) + + # Save initial configs + save_initial_configs(cfg, tokenizer, model, peft_config) + + # Set up signal handler for graceful termination + setup_signal_handler(cfg, model, safe_serialization) + + # Set up badges and config info for model card + setup_model_card(cfg) + + # Execute the training + execute_training(cfg, trainer, resume_from_checkpoint) + + # Save the trained model + save_trained_model(cfg, trainer, model, safe_serialization) + + # Create model card + create_model_card(cfg, trainer) + + return model, tokenizer diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 8553339b99..8cee3d1242 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -574,14 +574,40 @@ def prepare_opinionated_env(cfg): def setup_trainer( - cfg, train_dataset, eval_dataset, model, tokenizer, processor, total_num_steps + cfg, + train_dataset, + eval_dataset, + model, + tokenizer, + processor, + total_num_steps, + model_ref=None, + peft_config=None, ): + """ + Helper method for instantiating and building a (causal or RLHF) trainer. + + Args: + cfg: Axolotl config object containing training parameters. + train_dataset: Dataset to use for training. + eval_dataset: Dataset to use for evaluation. + model: The model to train. + tokenizer: Tokenizer for processing text input. + processor: Processor for data preparation. + total_num_steps: The total number of training steps. + model_ref: Optional reference model for RLHF training. Default is None. + peft_config: Optional PEFT (Parameter-Efficient Fine-Tuning) configuration. Default is None. + + Returns: + A trainer instance (either `HFRLTrainer` or `HFCausalTrainer`) configured based + on the provided parameters. + """ if cfg.rl: - trainer_builder = HFRLTrainerBuilder(cfg, model[0], tokenizer, processor) - trainer_builder.model_ref = model[1] - trainer_builder.peft_config = model[2] + trainer_builder = HFRLTrainerBuilder(cfg, model, tokenizer, processor) + trainer_builder.model_ref = model_ref + trainer_builder.peft_config = peft_config else: - trainer_builder = HFCausalTrainerBuilder(cfg, model[0], tokenizer, processor) + trainer_builder = HFCausalTrainerBuilder(cfg, model, tokenizer, processor) trainer_builder.train_dataset = train_dataset trainer_builder.eval_dataset = eval_dataset From d883b11b6f0ef42d90595db256087f7c1fdd592d Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 5 Mar 2025 22:00:39 +0700 Subject: [PATCH 0446/1405] fix(doc): add installation for cce to docs (#2375) [skip ci] * fix(doc): add installation for cce to docs * fix: format --- .../integrations/cut_cross_entropy/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 625cbc0ecc..14449a1442 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -4,6 +4,22 @@ Cut Cross Entropy reduces VRAM usage through optimization on the cross-entropy o See https://github.com/apple/ml-cross-entropy +## Requirements + +- PyTorch 2.4.0 or higher + +## Installation + +Run the following command to install `cut_cross_entropy[transformers]` if you don't have it already. + +```bash +# if you are in dev environment +python scripts/cutcrossentropy_install.py | sh + +# if you are not in dev environment +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy @ git+https://github.com/apple/ml-cross-entropy.git@9c297c905f55b73594b5d650722d1e78183b77bd"' +``` + ## Usage ```yaml From 8e309174407d4f272da5aa7929a565cdc40b59ef Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 5 Mar 2025 22:00:50 +0700 Subject: [PATCH 0447/1405] chore(docs): remove phorm (#2378) [skip ci] --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index c2e0f4731e..5d9a29a835 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,6 @@
tests-nightly multigpu-semi-weekly tests - - phorm.ai -

Axolotl is a tool designed to streamline post-training for various AI models. From 05dddfc41d542e357d698fde0a3c6efe55ec81e0 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 5 Mar 2025 22:01:00 +0700 Subject: [PATCH 0448/1405] feat(doc): add docker images explanation (#2379) [skip ci] * feat(doc): add docker images explanation * chore: add link to dockerhub --- _quarto.yml | 1 + docs/docker.qmd | 140 ++++++++++++++++++++++++++++++++++++++++++ docs/installation.qmd | 2 + 3 files changed, 143 insertions(+) create mode 100644 docs/docker.qmd diff --git a/_quarto.yml b/_quarto.yml index ddd172370f..c0536e730e 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -40,6 +40,7 @@ website: - section: "Deployments" contents: + - docs/docker.qmd - docs/multi-gpu.qmd - docs/multi-node.qmd - docs/ray-integration.qmd diff --git a/docs/docker.qmd b/docs/docker.qmd new file mode 100644 index 0000000000..2908592fa7 --- /dev/null +++ b/docs/docker.qmd @@ -0,0 +1,140 @@ +--- +title: "Docker" +format: + html: + toc: true + toc-depth: 4 +--- + +This section describes the different Docker images that are released by AxolotlAI at [Docker Hub](https://hub.docker.com/u/axolotlai). + +## Base + +The base image is the most minimal image that can install Axolotl. It is based on the `nvidia/cuda` image. It includes python, torch, git, git-lfs, awscli, pydantic, and more. + +#### Image + +``` +axolotlai/axolotl-base +``` + +Link: [Docker Hub](https://hub.docker.com/r/axolotlai/axolotl-base) + +#### Tags format + +```bash +main-base-py{python_version}-cu{cuda_version}-{pytorch_version} +``` + +Tags examples: + +- `main-base-py3.11-cu124-2.6.0` +- `main-base-py3.11-cu124-2.5.1` +- `main-base-py3.11-cu124-2.4.1` + +## Main + +The main image is the image that is used to run Axolotl. It is based on the `axolotlai/axolotl-base` image and includes the Axolotl codebase, dependencies, and more. + +#### Image + +``` +axolotlai/axolotl +``` + +Link: [Docker Hub](https://hub.docker.com/r/axolotlai/axolotl) + +#### Tags format {#sec-main-tags} + +```bash +# on push to main +main-py{python_version}-cu{cuda_version}-{pytorch_version} + +# latest main (currently torch 2.5.1, python 3.11, cuda 12.4) +main-latest + +# nightly build +{branch}-{date_in_YYYYMMDD}-py{python_version}-cu{cuda_version}-{pytorch_version} + +# tagged release +{version} +``` + +:::{.callout-tip} + +There may be some extra tags appended to the image, like `-vllm` which installs those packages. + +::: + +Tags examples: + +- `main-py3.11-cu124-2.6.0` +- `main-py3.11-cu124-2.5.1` +- `main-py3.11-cu124-2.4.1` +- `main-latest` +- `main-20250303-py3.11-cu124-2.6.0` +- `main-20250303-py3.11-cu124-2.5.1` +- `main-20250303-py3.11-cu124-2.4.1` +- `0.7.1` + +## Cloud + +The cloud image is the image that is used to run Axolotl in the cloud. It is based on the `axolotlai/axolotl` image and sets ENV variables like HuggingFace cache directories for volume mounts, tmux, and more for different cloud providers. + +:::{.callout-tip} + +Jupyter lab is run by default. Set `JUPYTER_DISABLE=1` in the environment variables to disable it. + +::: + +#### Image + +``` +axolotlai/axolotl-cloud +``` + +Link: [Docker Hub](https://hub.docker.com/r/axolotlai/axolotl-cloud) + +#### Tags format + +This uses the same tags as the [`main` image](#sec-main-tags). + +#### Environment variables + +- `JUPYTER_DISABLE`: Disable Jupyter lab. +- `JUPYTER_PASSWORD`: Set a password for the Jupyter lab. +- `PUBLIC_KEY`: Add a public key for the SSH service. +- `SSH_KEY`: Add a private key for the SSH service. + +#### Volume mounts + +:::{.callout-tip} + +We recommend mounting volumes to `/workspace/data` for data persistence. `/workspace/axolotl` contains the source code and is ephemeral. + +::: + +- `/workspace/data/axolotl-artifacts`: Directory to store Axolotl artifacts. +- `/workspace/data/huggingface-cache`: Directory to store HuggingFace cache. + +## Cloud-no-tmux + +This is the same as the [`cloud` image](#sec-cloud) but without tmux. + +#### Image + +``` +axolotlai/axolotl-cloud-term +``` + +Link: [Docker Hub](https://hub.docker.com/r/axolotlai/axolotl-cloud-term) + +:::{.callout-note} + +The naming may be a bit confusing as it has `-term` appended to the end. + +::: + +#### Tags format + +This uses the same tags as the [`cloud` image](#sec-cloud-tags). diff --git a/docs/installation.qmd b/docs/installation.qmd index 2be74be0f4..95f15e78e0 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -65,6 +65,8 @@ docker run --privileged --gpus '"all"' --shm-size 10g --rm -it \ ``` ::: +Please refer to the [Docker documentation](docker.qmd) for more information on the different Docker images that are available. + ## Cloud Environments {#sec-cloud} ### Cloud GPU Providers {#sec-cloud-gpu} From 9ed4f6b3aac8c95a7c050cddbbace82dc199a5d3 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 5 Mar 2025 22:01:16 +0700 Subject: [PATCH 0449/1405] feat(doc): document drop_system_message and clarify limitation (#2381) [skip ci] --- docs/config.qmd | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index 3a11666a5a..788d4f9691 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -163,6 +163,12 @@ datasets: system: ["system"] tool: ["tool"] + # Optional[bool]. Whether to drop the system turn from the dataset. Only works with chat_template. + # This does not drop the default system message from chat_template if it exists. If you wish to, + # we recommend using a custom jinja template with the default system message removed or + # adding a system turn with empty content. + drop_system_message: + # IMPORTANT: The following fields determine which parts of the conversation to train on. # Priority order: message_field_training > message_field_training_detail > train_on_inputs or role in roles_to_train # See examples at `docs/dataset-formats/conversation.qmd` @@ -222,8 +228,8 @@ process_reward_model: chat_template: tokenizer_default # custom jinja template for chat template. This will be only used if chat_template is set to `jinja` or `null` (in which case chat_template is automatically set to `jinja`). Default is null. chat_template_jinja: null -# Changes the default system message -default_system_message: You are a helpful assistant. Please give a long and detailed answer. # Currently only supports chatml. +# Changes the default system message. Currently only supports chatml. +default_system_message: You are a helpful assistant. Please give a long and detailed answer. # Axolotl attempts to save the dataset as an arrow after packing the data together so # subsequent training attempts load faster, relative path dataset_prepared_path: data/last_run_prepared From f18231c6537e746eda9c9092f5180263f9fd9438 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 5 Mar 2025 22:01:28 +0700 Subject: [PATCH 0450/1405] chore(doc): add clarification about mpi4py error on single gpu deepspeed (#2383) [skip ci] * chore(doc): add clarification about mpi4py error on single gpu deepspeed * fix: lint --- docs/dataset-formats/index.qmd | 1 + docs/faq.qmd | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/dataset-formats/index.qmd b/docs/dataset-formats/index.qmd index 4275858f62..121341e552 100644 --- a/docs/dataset-formats/index.qmd +++ b/docs/dataset-formats/index.qmd @@ -129,6 +129,7 @@ You can mix and match within each approach or across approaches to train a model We suggest this approach when you want to bring your own tokenized dataset. Axolotl expects the dataset to have three keys: + - `input_ids`: from tokenizing formatted prompt - `attention_mask`: for masking padding. If you don't add padding, it would be equal to `len(input_ids) * [1]` - `labels`: this is the same as `input_ids`, however, if you want to mask certain tokens, you would set those indices to `-100`. diff --git a/docs/faq.qmd b/docs/faq.qmd index 1b5037db93..7e5dd3d746 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -19,7 +19,9 @@ description: Frequently asked questions **Q: AttributeError: 'DummyOptim' object has no attribute 'step'** -> A: You may be using deepspeed with single gpu. Please don't set `deepspeed:` in yaml or cli. +**Q: ModuleNotFoundError: No module named 'mpi4py' using single GPU with deepspeed** + +> A: You may be using deepspeed with single gpu. Please remove the `deepspeed:` section in the yaml file or `--deepspeed` CLI flag. **Q: The codes is stuck on saving preprocessed datasets.** From c8191394e916cc9e811d3eab3ddf4d9659675a2b Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 5 Mar 2025 22:01:44 +0700 Subject: [PATCH 0451/1405] fix(doc): add missing low_cpu_mem_usage config to docs (#2369) [skip ci] --- docs/config.qmd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/config.qmd b/docs/config.qmd index 788d4f9691..1f3a50b2e9 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -534,6 +534,8 @@ flash_attn_fuse_mlp: # Whether to fuse part of the MLP into a single operation sdp_attention: # Shifted-sparse attention (only llama) - https://arxiv.org/pdf/2309.12307.pdf s2_attention: +# Optional[bool]. Whether to use low_cpu_mem_usage +low_cpu_mem_usage: # Resume from a specific checkpoint dir resume_from_checkpoint: # If resume_from_checkpoint isn't set and you simply want it to start where it left off. From d4de93a7bbcee39bb90f51dab726983fe6a1c88f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 5 Mar 2025 22:02:08 +0700 Subject: [PATCH 0452/1405] feat(grpo): add reward_weights config and refactor (#2365) --- src/axolotl/core/trainers/grpo/__init__.py | 59 ++++++++++++------- .../utils/config/models/input/v0_4_1/trl.py | 1 + 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 5202cb09dc..ecfc123092 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -9,6 +9,7 @@ from trl.trainer.grpo_trainer import RewardFunc from axolotl.core.trainers.grpo.trainer import AxolotlGRPOTrainer +from axolotl.utils.config.models.input.v0_4_1.trl import TRLConfig LOG = logging.getLogger("axolotl") @@ -31,30 +32,44 @@ def get_training_args_class(cls): @classmethod def set_training_args_kwargs(cls, cfg): grpo_args_kwargs = {} - if cfg.trl and cfg.trl.use_vllm: - grpo_args_kwargs["use_vllm"] = cfg.trl.use_vllm - if cfg.trl and cfg.trl.vllm_device: - grpo_args_kwargs["vllm_device"] = cfg.trl.vllm_device - else: - grpo_args_kwargs["vllm_device"] = "auto" - if cfg.trl and cfg.trl.vllm_gpu_memory_utilization: + + if not hasattr(cfg, "trl") or not cfg.trl: + return grpo_args_kwargs + + trl: TRLConfig = cfg.trl # type: ignore + + if trl.use_vllm: + grpo_args_kwargs["use_vllm"] = trl.use_vllm + grpo_args_kwargs["vllm_device"] = ( + trl.vllm_device if trl.vllm_device else "auto" + ) + + if trl.vllm_gpu_memory_utilization: grpo_args_kwargs[ "vllm_gpu_memory_utilization" - ] = cfg.trl.vllm_gpu_memory_utilization - if cfg.trl and cfg.trl.vllm_max_model_len: - grpo_args_kwargs["vllm_max_model_len"] = cfg.trl.vllm_max_model_len - if cfg.trl and cfg.trl.num_generations: - grpo_args_kwargs["num_generations"] = cfg.trl.num_generations - if cfg.trl and cfg.trl.sync_ref_model: - grpo_args_kwargs["sync_ref_model"] = cfg.trl.sync_ref_model - if cfg.trl and cfg.trl.ref_model_mixup_alpha: - grpo_args_kwargs[ - "ref_model_mixup_alpha" - ] = cfg.trl.ref_model_mixup_alpha - if cfg.trl and cfg.trl.ref_model_sync_steps: - grpo_args_kwargs["ref_model_sync_steps"] = cfg.trl.ref_model_sync_steps - grpo_args_kwargs["max_completion_length"] = cfg.trl.max_completion_length - grpo_args_kwargs["log_completions"] = cfg.trl.log_completions + ] = trl.vllm_gpu_memory_utilization + + if trl.vllm_max_model_len: + grpo_args_kwargs["vllm_max_model_len"] = trl.vllm_max_model_len + + if trl.num_generations: + grpo_args_kwargs["num_generations"] = trl.num_generations + + if trl.sync_ref_model: + grpo_args_kwargs["sync_ref_model"] = trl.sync_ref_model + + if trl.ref_model_mixup_alpha: + grpo_args_kwargs["ref_model_mixup_alpha"] = trl.ref_model_mixup_alpha + + if trl.ref_model_sync_steps: + grpo_args_kwargs["ref_model_sync_steps"] = trl.ref_model_sync_steps + + grpo_args_kwargs["max_completion_length"] = trl.max_completion_length + grpo_args_kwargs["log_completions"] = trl.log_completions + + if trl.reward_weights: + grpo_args_kwargs["reward_weights"] = trl.reward_weights + return grpo_args_kwargs @classmethod diff --git a/src/axolotl/utils/config/models/input/v0_4_1/trl.py b/src/axolotl/utils/config/models/input/v0_4_1/trl.py index 6361bb2492..ae26e634ca 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/trl.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/trl.py @@ -27,6 +27,7 @@ class TRLConfig(BaseModel): vllm_dtype: Optional[str] = "auto" reward_funcs: Optional[List[str]] = None + reward_weights: Optional[List[float]] = None num_generations: Optional[int] = None log_completions: Optional[bool] = False From 0134093acc6b78f5a5cde0570472f114a2c432de Mon Sep 17 00:00:00 2001 From: xzuyn <16216325+xzuyn@users.noreply.github.com> Date: Wed, 5 Mar 2025 10:26:11 -0500 Subject: [PATCH 0453/1405] Add REX LR Scheduler (#2380) * Update trainer_builder.py * Update base.py * Update __init__.py * Update base.py * Update base.py * Update config.qmd * Update base.py * Update base.py * Update base.py * Update base.py * Update base.py * Update base.py * Update base.py * lint * lint * lint * lint * lint * lint * Update base.py * Update base.py * lint * Update base.py * Update base.py * Move RexLR to `schedulers.py` * Remove RexLR from `base.py` * Fix tooltip formatting * lint * Create test_schedulers.py * Use a default optimizer in test * lint * lint * Add `warmup_steps` and `cosine_min_lr_ratio` to test * lint --- docs/config.qmd | 2 +- src/axolotl/core/trainer_builder.py | 2 +- src/axolotl/core/trainers/base.py | 12 +++ .../config/models/input/v0_4_1/__init__.py | 2 +- src/axolotl/utils/schedulers.py | 74 +++++++++++++++++++ tests/e2e/test_schedulers.py | 71 ++++++++++++++++++ 6 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/test_schedulers.py diff --git a/docs/config.qmd b/docs/config.qmd index 1f3a50b2e9..cfd137ff0d 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -451,7 +451,7 @@ gradient_checkpointing: false early_stopping_patience: 3 # Specify a scheduler and kwargs to use with the optimizer -lr_scheduler: # 'one_cycle' | 'log_sweep' | empty for cosine +lr_scheduler: # 'one_cycle' | 'rex' | 'log_sweep' | empty for cosine lr_scheduler_kwargs: cosine_min_lr_ratio: # decay lr to some percentage of the peak lr, e.g. cosine_min_lr_ratio=0.1 for 10% of peak lr cosine_constant_lr_ratio: # freeze lr at some percentage of the step, e.g. cosine_constant_lr_ratio=0.8 means start cosine_min_lr at 80% of training step (https://arxiv.org/pdf/2308.04014.pdf) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index d4ddc9bf37..fe9c8bcae1 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -572,7 +572,7 @@ def build(self, total_num_steps): training_arguments_kwargs["embedding_lr_scale"] = self.cfg.embedding_lr_scale training_arguments_kwargs["lr_groups"] = self.cfg.lr_groups - if self.cfg.lr_scheduler in ["one_cycle", "log_sweep"]: + if self.cfg.lr_scheduler in ["one_cycle", "rex", "log_sweep"]: training_arguments_kwargs["lr_scheduler_type"] = "cosine" training_arguments_kwargs[ "alternate_lr_scheduler_type" diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index ee2545b213..27f00f1fd9 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -25,6 +25,7 @@ from axolotl.monkeypatch.relora import ReLoRAScheduler from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths from axolotl.utils.schedulers import ( + RexLR, get_cosine_schedule_with_min_lr, get_cosine_schedule_with_quadratic_warmup, get_cosine_schedule_with_warmup_decay_constant, @@ -115,6 +116,17 @@ def create_scheduler( **extra_lr_kwargs, **self.args.lr_scheduler_kwargs, ) + elif self.args.alternate_lr_scheduler_type == "rex": + if use_cosine_min_lr: + assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" + + self.lr_scheduler = RexLR( + optimizer=optimizer, + max_lr=self.args.learning_rate, + min_lr=0 if not use_cosine_min_lr else (self.args.learning_rate * self.args.cosine_min_lr_ratio), + total_steps=num_training_steps, + num_warmup_steps=self.args.get_warmup_steps(num_training_steps), + ) elif use_cosine_quadratic: if use_cosine_min_lr: LOG.warning("Both cosine quadratic warmup and min lr detected. Using quadratic warmup.") diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index c7803b8ccb..180e02823f 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -518,7 +518,7 @@ class HyperparametersConfig(BaseModel): ) torchdistx_path: Optional[str] = None lr_scheduler: Optional[ - Union[SchedulerType, Literal["one_cycle"]] + Union[SchedulerType, Literal["one_cycle"], Literal["rex"]] ] = SchedulerType.COSINE lr_scheduler_kwargs: Optional[Dict[str, Any]] = None lr_quadratic_warmup: Optional[bool] = None diff --git a/src/axolotl/utils/schedulers.py b/src/axolotl/utils/schedulers.py index 94387e5ab8..6f057fbd9a 100644 --- a/src/axolotl/utils/schedulers.py +++ b/src/axolotl/utils/schedulers.py @@ -6,6 +6,80 @@ from torch.optim.lr_scheduler import LambdaLR, LRScheduler +class RexLR(LRScheduler): + """ + Reflected Exponential (REX) learning rate scheduler. + + - Original implementation: https://github.com/IvanVassi/REX_LR + - Original license: Apache 2.0 + - Based on: https://arxiv.org/abs/2107.04197 + + Args: + optimizer (torch.optim.Optimizer): The optimizer to schedule the learning rate for. + max_lr (float): The maximum learning rate. + min_lr (float): The minimum learning rate. + total_steps (int): The total number of training steps. + num_warmup_steps (int): The number of warmup steps. + last_step (int): The index of last step. + """ + + def __init__( + self, optimizer, max_lr, min_lr, total_steps=0, num_warmup_steps=0, last_step=0 + ): + if min_lr > max_lr: + raise ValueError( + f'Value of "min_lr" should be less than value of "max_lr". Got min_lr={min_lr} and max_lr={max_lr}' + ) + if num_warmup_steps > total_steps: + raise ValueError( + f"num_warmup_steps ({num_warmup_steps}) must be less than or equal to total_steps ({total_steps})." + ) + + self.min_lr = min_lr + self.max_lr = max_lr + self.total_steps = total_steps + self.num_warmup_steps = num_warmup_steps + self.last_step = last_step - 1 + + # Ensure each parameter group has an "initial_lr" key to avoid issues when resuming. + for group in optimizer.param_groups: + group.setdefault("initial_lr", group["lr"]) + + # Pass self.last_step as last_epoch to the parent. + super().__init__(optimizer, last_epoch=self.last_step) + + @property + def last_step(self): + return self.last_epoch + + @last_step.setter + def last_step(self, value): + self.last_epoch = value + + def get_lr(self): + # Warmup phase: if defined, increase lr linearly from 0 to max_lr. + if 1 <= self.last_step <= self.num_warmup_steps: + return [ + base_lr * self.last_step / self.num_warmup_steps + for base_lr in self.base_lrs + ] + + # Post-warmup phase: adjust step relative to the end of warmup. + step_after = self.last_step - self.num_warmup_steps + remaining_steps = self.total_steps - self.num_warmup_steps + + # Avoid LR spiking + if step_after >= remaining_steps or step_after == -1 or remaining_steps <= 0: + return [self.min_lr for _ in self.base_lrs] + + mod_iter = step_after % remaining_steps + z = (remaining_steps - mod_iter) / remaining_steps + rex_factor = self.min_lr / self.max_lr + (1.0 - self.min_lr / self.max_lr) * ( + z / (0.1 + 0.9 * z) + ) + return [base_lr * rex_factor for base_lr in self.base_lrs] + + class InterpolatingLogScheduler(LRScheduler): """ A scheduler that interpolates learning rates in a logarithmic fashion diff --git a/tests/e2e/test_schedulers.py b/tests/e2e/test_schedulers.py new file mode 100644 index 0000000000..c492fdccfa --- /dev/null +++ b/tests/e2e/test_schedulers.py @@ -0,0 +1,71 @@ +""" +E2E tests for custom schedulers using Llama +""" + +import logging +import os +import unittest + +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +from .utils import check_model_output_exists, with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestCustomSchedulers(unittest.TestCase): + """ + Test case for Llama models using LoRA + """ + + @with_temp_dir + def test_rex_scheduler(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 1024, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.1, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_hf", + "max_steps": 20, + "lr_scheduler": "rex", + "warmup_steps": 5, + "cosine_min_lr_ratio": 0.05, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) From 575e5f28ec0815654756d18be2cf6cf8a0224c20 Mon Sep 17 00:00:00 2001 From: mhenrichsen Date: Wed, 5 Mar 2025 17:15:12 +0100 Subject: [PATCH 0454/1405] Update Tokenizer Overrides Handling in models.py (#1549) * override special tokens mock code * fix(doc): remove duplicate config * feat: replace added_tokens in tokenizer and add test * make sure to run tokenizer modification on rank 0 only * use is local main process instead * feat: rename config --------- Co-authored-by: NanoCode012 Co-authored-by: Wing Lian --- docs/config.qmd | 9 +- .../config/models/input/v0_4_1/__init__.py | 1 + src/axolotl/utils/distributed.py | 2 +- src/axolotl/utils/models.py | 108 +++++++++++++++++- tests/test_tokenizers.py | 45 +++++++- 5 files changed, 156 insertions(+), 9 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index cfd137ff0d..fb0c4b59bc 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -154,8 +154,6 @@ datasets: content: value # ... - message_property_mappings: - # Optional[Dict[str, List]]. Roles mapping in the messages. The default is: roles: user: ["human", "user"] @@ -556,6 +554,13 @@ special_tokens: # Add extra tokens. tokens: +# Mapping token_id to new_token_string to override reserved added_tokens in the tokenizer. +# Only works for tokens that are not part of the base vocab (aka are added_tokens). +# Can be checked if they exist in tokenizer.json added_tokens. +added_tokens_overrides: # Dict[int, str] +# 128041: "<|im_start|>" +# 128042: "<|im_end|>" + # FSDP fsdp: fsdp_config: diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 180e02823f..ce2586afb5 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -855,6 +855,7 @@ class AxolotlInputConfig( special_tokens: Optional[SpecialTokensConfig] = None tokens: Optional[List[str]] = None + added_tokens_overrides: Optional[Dict[int, str]] = None torch_compile: Optional[Union[Literal["auto"], bool]] = None torch_compile_backend: Optional[str] = None diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index 81a928b6ec..7d6cd597a6 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -79,7 +79,7 @@ def is_main_process(): def is_local_main_process(): - return PartialState().is_main_process + return PartialState().is_local_main_process def get_world_size(): diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index c4c07dd33d..add690d9db 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -57,7 +57,13 @@ from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.chat_templates import get_chat_template_from_config from axolotl.utils.dict import DictDefault -from axolotl.utils.distributed import get_device_count, get_device_type, zero_only +from axolotl.utils.distributed import ( + barrier, + get_device_count, + get_device_type, + is_local_main_process, + zero_only, +) from axolotl.utils.gradient_checkpointing import hf_grad_checkpoint_unsloth_wrapper from axolotl.utils.lora_embeddings import get_linear_embedding_layers from axolotl.utils.model_shard_quant import load_sharded_model, load_sharded_model_quant @@ -165,7 +171,95 @@ def load_model_config(cfg): return model_config +def modify_tokenizer_files( + tokenizer_path: str, token_mappings: Dict[int, str], output_dir: str +) -> str: + """ + Modify tokenizer files to replace added_tokens strings, save to output directory, and return the path to the modified tokenizer. + + This only works with reserved tokens that were added to the tokenizer, not tokens already part of the vocab. + + Args: + tokenizer_path: Path or name of the original tokenizer + token_mappings: Dict mapping {token_id (int): new_token_string} + output_dir: Directory to save the modified tokenizer + + Returns: + Path to the modified tokenizer directory + + Ref: https://github.com/huggingface/transformers/issues/27974#issuecomment-1854188941 + """ + + import json + + # Create the tokenizer directory in output_dir if it doesn't exist + tokenizer_dir = os.path.join(output_dir, "tokenizer") + os.makedirs(tokenizer_dir, exist_ok=True) + + if is_local_main_process(): # pylint: disable=too-many-nested-blocks + # Load the tokenizer + temp_tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, use_fast=True) + + # Save the tokenizer to the output directory + temp_tokenizer.save_pretrained(tokenizer_dir) + + # Get the token IDs and map them to their new values + token_id_mappings = { + int(token_id): new_value for token_id, new_value in token_mappings.items() + } + + # 1. Update tokenizer_config.json - added_tokens_decoder + config_path = os.path.join(tokenizer_dir, "tokenizer_config.json") + if os.path.exists(config_path): + with open(config_path, "r", encoding="utf-8") as f: + config_data = json.load(f) + + # Update added_tokens_decoder + if "added_tokens_decoder" in config_data: + for token_id, new_value in token_id_mappings.items(): + token_id_str = str(token_id) + if token_id_str in config_data["added_tokens_decoder"]: + config_data["added_tokens_decoder"][token_id_str][ + "content" + ] = new_value + else: + raise ValueError( + f"Token ID {token_id_str} not found in added_tokens_decoder" + ) + + # Write the updated config back + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config_data, f, indent=2) + + # 2. Update tokenizer.json - added_tokens + tokenizer_path = os.path.join(tokenizer_dir, "tokenizer.json") + if os.path.exists(tokenizer_path): + with open(tokenizer_path, "r", encoding="utf-8") as f: + tokenizer_data = json.load(f) + + # Update added_tokens + if "added_tokens" in tokenizer_data: + for token_id, new_value in token_id_mappings.items(): + for i, token_entry in enumerate(tokenizer_data["added_tokens"]): + if token_entry["id"] == token_id: + tokenizer_data["added_tokens"][i]["content"] = new_value + break + else: + # Reaching this section means the token_id was not found in tokenizer.json added_tokens + raise ValueError( + f"Token ID {token_id} not found in added_tokens" + ) + + # Write the updated tokenizer data back + with open(tokenizer_path, "w", encoding="utf-8") as f: + json.dump(tokenizer_data, f, indent=2) + + barrier() + return tokenizer_dir + + def load_tokenizer(cfg): + """Load and configure the tokenizer based on the provided config.""" model_config = load_model_config(cfg) tokenizer_kwargs = {} use_fast = True # this is the default @@ -180,8 +274,18 @@ def load_tokenizer(cfg): if cfg.tokenizer_type: tokenizer_cls = getattr(transformers, cfg.tokenizer_type) + # Set base tokenizer path + tokenizer_path = cfg.tokenizer_config + + # Apply token string overrides if specified + if cfg.added_tokens_overrides: + # Modify tokenizer files and get path to modified tokenizer + tokenizer_path = modify_tokenizer_files( + tokenizer_path, cfg.added_tokens_overrides, output_dir=cfg.output_dir + ) + tokenizer = tokenizer_cls.from_pretrained( - cfg.tokenizer_config, + tokenizer_path, trust_remote_code=cfg.trust_remote_code or False, use_fast=use_fast, **tokenizer_kwargs, diff --git a/tests/test_tokenizers.py b/tests/test_tokenizers.py index 69c441f8c6..3d568ab19f 100644 --- a/tests/test_tokenizers.py +++ b/tests/test_tokenizers.py @@ -1,6 +1,7 @@ """ Test cases for the tokenizer loading """ + import unittest import pytest @@ -9,7 +10,7 @@ from axolotl.utils.models import load_tokenizer -class TestTokenizers(unittest.TestCase): +class TestTokenizers: """ test class for the load_tokenizer fn """ @@ -75,12 +76,48 @@ def test_add_additional_special_tokens(self): } ) tokenizer = load_tokenizer(cfg) - self.assertEqual(tokenizer("<|im_start|>user")["input_ids"], [1, 32000, 1404]) - self.assertEqual(len(tokenizer), 32001) + assert tokenizer("<|im_start|>user")["input_ids"] == [1, 32000, 1404] + assert len(tokenizer) == 32001 # ensure reloading the tokenizer again from cfg results in same vocab length tokenizer = load_tokenizer(cfg) - self.assertEqual(len(tokenizer), 32001) + assert len(tokenizer) == 32001 + + def test_added_tokens_overrides(self, temp_dir): + cfg = DictDefault( + { + # use with tokenizer that has reserved_tokens in added_tokens + "tokenizer_config": "NousResearch/Llama-3.2-1B", + "added_tokens_overrides": { + 128041: "RANDOM_OVERRIDE_1", + 128042: "RANDOM_OVERRIDE_2", + }, + "output_dir": temp_dir, + } + ) + + tokenizer = load_tokenizer(cfg) + assert tokenizer.encode("RANDOM_OVERRIDE_1", add_special_tokens=False) == [ + 128041 + ] + assert tokenizer.encode("RANDOM_OVERRIDE_2", add_special_tokens=False) == [ + 128042 + ] + + def test_added_tokens_overrides_with_toolargeid(self, temp_dir): + cfg = DictDefault( + { + # use with tokenizer that has reserved_tokens in added_tokens + "tokenizer_config": "NousResearch/Llama-3.2-1B", + "added_tokens_overrides": {1000000: "BROKEN_RANDOM_OVERRIDE_1"}, + "output_dir": temp_dir, + } + ) + + with pytest.raises( + ValueError, match=r".*Token ID 1000000 not found in added_tokens.*" + ): + load_tokenizer(cfg) if __name__ == "__main__": From 5e21b1a9daee7bba2b06a1f181909eb4a9a834ec Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 6 Mar 2025 11:48:44 -0500 Subject: [PATCH 0455/1405] various fixes 20250305 (#2384) * various validation fixes * fix check for non-truthy value --- src/axolotl/integrations/spectrum/args.py | 19 ++++++++++++++++++- .../config/models/input/v0_4_1/__init__.py | 15 ++++++++++++--- .../utils/gradient_checkpointing/__init__.py | 2 +- src/axolotl/utils/models.py | 6 +++--- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/axolotl/integrations/spectrum/args.py b/src/axolotl/integrations/spectrum/args.py index 03426d8413..df57560380 100644 --- a/src/axolotl/integrations/spectrum/args.py +++ b/src/axolotl/integrations/spectrum/args.py @@ -17,7 +17,7 @@ """ from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, model_validator class SpectrumArgs(BaseModel): @@ -27,3 +27,20 @@ class SpectrumArgs(BaseModel): spectrum_top_fraction: Optional[float] = 0.5 spectrum_model_name: Optional[str] = None + + @model_validator(mode="before") + @classmethod + def check_fsdp_use_orig_params(cls, data): + if ( + data.get("fsdp") + and data.get("fsdp_config") + and not data["fsdp_config"].get("use_orig_params") + and data.get("plugins") + and any("SpectrumPlugin" in plugin for plugin in data["plugins"]) + ): + # would otherwise raise + # ValueError: Must flatten tensors with uniform `requires_grad` when `use_orig_params=False` + raise ValueError( + "FSDP + SpectrumPlugin cannot be used together when `use_orig_params=False` is set" + ) + return data diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index ce2586afb5..5143469be1 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -778,9 +778,9 @@ class AxolotlInputConfig( # torch_dtype: Optional[torch.dtype] - gradient_checkpointing: Optional[Union[Literal["unsloth"], bool]] = Field( - default=False - ) + gradient_checkpointing: Optional[ + Union[Literal["unsloth", "offload"], bool] + ] = Field(default=False) gradient_checkpointing_kwargs: Optional[Dict[str, Any]] = None unfrozen_parameters: Optional[List[str]] = None @@ -1154,6 +1154,15 @@ def check_mpt_checkpointing(self): raise ValueError("gradient_checkpointing is not supported for MPT models") return self + @model_validator(mode="after") + def check_offload_grad_checkpointing(self): + if self.gradient_checkpointing and self.gradient_checkpointing == "unsloth": + LOG.warning( + "`unsloth` is deprecated for gradient_checkpointing, use `offload`" + ) + self.gradient_checkpointing = "offload" + return self + @model_validator(mode="after") def check_better_transformers(self): if self.flash_optimum is True: diff --git a/src/axolotl/utils/gradient_checkpointing/__init__.py b/src/axolotl/utils/gradient_checkpointing/__init__.py index 4639fc266c..8bbf878ad6 100644 --- a/src/axolotl/utils/gradient_checkpointing/__init__.py +++ b/src/axolotl/utils/gradient_checkpointing/__init__.py @@ -4,7 +4,7 @@ ) -def hf_grad_checkpoint_unsloth_wrapper( +def hf_grad_checkpoint_offload_wrapper( decoder_layer, *args, use_reentrant=None ): # pylint: disable=unused-argument return Unsloth_Offloaded_Gradient_Checkpointer.apply( diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index add690d9db..1805a749a6 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -64,7 +64,7 @@ is_local_main_process, zero_only, ) -from axolotl.utils.gradient_checkpointing import hf_grad_checkpoint_unsloth_wrapper +from axolotl.utils.gradient_checkpointing import hf_grad_checkpoint_offload_wrapper from axolotl.utils.lora_embeddings import get_linear_embedding_layers from axolotl.utils.model_shard_quant import load_sharded_model, load_sharded_model_quant @@ -493,8 +493,8 @@ def apply_patches(self) -> None: patch_fa_peft_integration() - if self.cfg.gradient_checkpointing == "unsloth": - transformers.modeling_utils.checkpoint = hf_grad_checkpoint_unsloth_wrapper + if self.cfg.gradient_checkpointing in ["unsloth", "offload"]: + transformers.modeling_utils.checkpoint = hf_grad_checkpoint_offload_wrapper if self.cfg.flash_attention: self.patch_attention() From ae663741566ab27bb25af25ef3d2f16b6334e492 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 6 Mar 2025 11:49:19 -0500 Subject: [PATCH 0456/1405] Optimizer refactor and add Muon support (#2367) * add muon optimizer optimizer_cls_and_kwargs is on trainer_kwargs only add adamw_kwargs if they're non-null fix mocks better handling of override and check the optimizer unwrap optimizer * fix import --- requirements.txt | 1 + src/axolotl/cli/train.py | 3 +- src/axolotl/core/trainer_builder.py | 150 +++++++++----- src/axolotl/core/trainers/base.py | 190 ++++++++++-------- src/axolotl/integrations/base.py | 13 ++ src/axolotl/train.py | 4 +- .../config/models/input/v0_4_1/__init__.py | 31 ++- tests/cli/test_cli_train.py | 4 +- tests/e2e/test_mixtral.py | 8 +- tests/e2e/test_optimizers.py | 54 ++++- 10 files changed, 298 insertions(+), 160 deletions(-) diff --git a/requirements.txt b/requirements.txt index 18bb00692e..cd5690f0b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -63,3 +63,4 @@ torchao==0.7.0 schedulefree==1.3.0 axolotl-contribs-lgpl==0.0.3 +axolotl-contribs-mit==0.0.3 diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 7ac15e04f6..032f12b667 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -41,11 +41,12 @@ def do_train(cfg: DictDefault, cli_args: TrainerCliArgs) -> None: else: dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - model, tokenizer = train(cfg=cfg, dataset_meta=dataset_meta) + model, tokenizer, trainer = train(cfg=cfg, dataset_meta=dataset_meta) plugin_manager = PluginManager.get_instance() del model del tokenizer + del trainer plugin_manager.post_train_unload(cfg) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index fe9c8bcae1..0c92047471 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -35,6 +35,7 @@ EarlyStoppingCallback, TrainerCallback, ) +from transformers.training_args import OptimizerNames from trl.trainer.utils import RewardDataCollatorWithPadding from axolotl.core.trainers.base import ( @@ -84,6 +85,7 @@ V2BatchSamplerDataCollatorForSeq2Seq, ) from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator +from axolotl.utils.config.models.input.v0_4_1 import CustomSupportedOptimizers from axolotl.utils.models import ensure_dtype try: @@ -549,28 +551,6 @@ def build(self, total_num_steps): training_arguments_kwargs["run_name"] = self.cfg.mlflow_run_name else: training_arguments_kwargs["run_name"] = None - training_arguments_kwargs["optim"] = ( - self.cfg.optimizer if self.cfg.optimizer else "adamw_hf" - ) - if self.cfg.optim_args: - if isinstance(self.cfg.optim_args, dict): - optim_args = ",".join( - [f"{key}={value}" for key, value in self.cfg.optim_args.items()] - ) - else: - optim_args = self.cfg.optim_args - training_arguments_kwargs["optim_args"] = optim_args - if self.cfg.optim_target_modules: - training_arguments_kwargs[ - "optim_target_modules" - ] = self.cfg.optim_target_modules - training_arguments_kwargs["loraplus_lr_ratio"] = self.cfg.loraplus_lr_ratio - training_arguments_kwargs[ - "loraplus_lr_embedding" - ] = self.cfg.loraplus_lr_embedding - training_arguments_kwargs["embedding_lr"] = self.cfg.embedding_lr - training_arguments_kwargs["embedding_lr_scale"] = self.cfg.embedding_lr_scale - training_arguments_kwargs["lr_groups"] = self.cfg.lr_groups if self.cfg.lr_scheduler in ["one_cycle", "rex", "log_sweep"]: training_arguments_kwargs["lr_scheduler_type"] = "cosine" @@ -656,46 +636,114 @@ def build(self, total_num_steps): if self.cfg.reward_model: training_arguments_kwargs["max_length"] = self.cfg.sequence_len - # pylint: disable=duplicate-code - if self.cfg.optimizer in [ - "optimi_adamw", - "ao_adamw_4bit", - "ao_adamw_8bit", - "ao_adamw_fp8", - "adopt_adamw", - ]: - # Set default so transformers doesn't throw - training_arguments_kwargs["optim"] = "adamw_hf" - training_arguments_kwargs["alternate_optimizer"] = self.cfg.optimizer + # Handle custom optimizer + custom_supported_optimizers = [opt.value for opt in CustomSupportedOptimizers] + if self.cfg.optimizer in custom_supported_optimizers: + # Common optimizer kwargs + optimizer_kwargs = { + "lr": training_arguments_kwargs.get("learning_rate"), + "weight_decay": training_arguments_kwargs.get("weight_decay"), + } + + # Adam-specific kwargs + adam_kwargs = {} + if training_arguments_kwargs.get( + "adam_beta1" + ) and training_arguments_kwargs.get("adam_beta2"): + adam_kwargs["betas"] = ( + training_arguments_kwargs.get("adam_beta1"), + training_arguments_kwargs.get("adam_beta2"), + ) + if training_arguments_kwargs.get("adam_epsilon"): + adam_kwargs["eps"] = training_arguments_kwargs.get("adam_epsilon") - if self.cfg.optimizer == "lion_pytorch": - from lion_pytorch import Lion + if self.cfg.optimizer == "muon": + from axolotl.contribs.mit.muon import ( # pylint: disable=no-name-in-module + MuonOptimizerFactory, + ) - lion_kwargs = {"lr": training_arguments_kwargs["learning_rate"]} - if "weight_decay" in training_arguments_kwargs: - lion_kwargs["weight_decay"] = training_arguments_kwargs["weight_decay"] + optimizer_cls = MuonOptimizerFactory + optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "optimi_adamw": + from optimi import AdamW - if ( - "adam_beta1" in training_arguments_kwargs - and "adam_beta2" in training_arguments_kwargs - ): - lion_kwargs["betas"] = ( - training_arguments_kwargs["adam_beta1"], - training_arguments_kwargs["adam_beta2"], - ) + optimizer_kwargs["foreach"] = False + optimizer_cls = AdamW + optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "ao_adamw_4bit": + # TODO remove 20250401 + from torchao.prototype.low_bit_optim import AdamW4bit + + optimizer_cls = AdamW4bit + optimizer_kwargs.update(adam_kwargs) - trainer_kwargs["optimizers"] = ( - Lion(params=self.model.parameters(), **lion_kwargs), - None, + LOG.warning( + f"`ao_adamw_4bit` will be deprecated soon. Please use `{OptimizerNames.ADAMW_TORCH_4BIT}` instead." + ) + elif self.cfg.optimizer == "ao_adamw_8bit": + from torchao.prototype.low_bit_optim import AdamW8bit + + optimizer_cls = AdamW8bit + optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "ao_adamw_fp8": + from torchao.prototype.low_bit_optim import AdamWFp8 + + optimizer_cls = AdamWFp8 + optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "adopt_adamw": + from axolotl.utils.optimizers.adopt import ADOPT + + optimizer_cls = ADOPT + adam_kwargs["decouple"] = True + optimizer_kwargs.update(adam_kwargs) + + # Parse any additional optimizer args from config + if self.cfg.optim_args: + if isinstance(self.cfg.optim_args, dict): + optimizer_kwargs.update(self.cfg.optim_args) + else: + # Parse string format "key1=value1,key2=value2" + for mapping in self.cfg.optim_args.replace(" ", "").split(","): + key, value = mapping.split("=") + optimizer_kwargs[key] = value + + trainer_kwargs["optimizer_cls_and_kwargs"] = ( + optimizer_cls, + optimizer_kwargs, ) - # Set default so transformers doesn't throw - training_arguments_kwargs["optim"] = "adamw_hf" + else: + # Use transformers' optimizer + training_arguments_kwargs["optim"] = self.cfg.optimizer + + # Parse any additional optimizer args from config + if self.cfg.optim_args: + if isinstance(self.cfg.optim_args, dict): + optim_args = ",".join( + [f"{key}={value}" for key, value in self.cfg.optim_args.items()] + ) + else: + optim_args = self.cfg.optim_args + training_arguments_kwargs["optim_args"] = optim_args if self.cfg.optimizer == "adamw_anyprecision": if Path(self.cfg.torchdistx_path).exists(): sys.path.append(self.cfg.torchdistx_path) importlib.import_module("torchdistx") + if self.cfg.optim_target_modules: + training_arguments_kwargs[ + "optim_target_modules" + ] = self.cfg.optim_target_modules + + training_arguments_kwargs["embedding_lr"] = self.cfg.embedding_lr + training_arguments_kwargs["embedding_lr_scale"] = self.cfg.embedding_lr_scale + + training_arguments_kwargs["loraplus_lr_ratio"] = self.cfg.loraplus_lr_ratio + training_arguments_kwargs[ + "loraplus_lr_embedding" + ] = self.cfg.loraplus_lr_embedding + training_arguments_kwargs["lr_groups"] = self.cfg.lr_groups + if self.cfg.accelerator_config: training_arguments_kwargs[ "accelerator_config" diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 27f00f1fd9..c14ed59b54 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -14,6 +14,7 @@ import torch from datasets import Dataset from peft.optimizers import create_loraplus_optimizer +from torch import nn from torch.optim.lr_scheduler import OneCycleLR from torch.utils.data import BatchSampler, DataLoader, RandomSampler, SequentialSampler from transformers import Trainer @@ -22,6 +23,7 @@ from trl import CPOTrainer, KTOTrainer, ORPOTrainer, PRMTrainer, RewardTrainer from trl.trainer.utils import pad_to_length +from axolotl.integrations.base import BaseOptimizerFactory from axolotl.monkeypatch.relora import ReLoRAScheduler from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths from axolotl.utils.schedulers import ( @@ -166,47 +168,18 @@ def create_scheduler( return self.lr_scheduler -class AxolotlTrainer(SchedulerMixin, Trainer): +class OptimizerMixin(Trainer): """ - Extend the base Trainer for axolotl helpers + Mixin class for shared handling of building custom optimizers """ args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] - tag_names = ["axolotl"] - - def __init__( - self, - *_args, - bench_data_collator=None, - eval_data_collator=None, - dataset_tags=None, - **kwargs, - ): - self.bench_data_collator = bench_data_collator - self.eval_data_collator = eval_data_collator - self.dataset_tags = dataset_tags - self._signature_columns = None # workaround for pylint - super().__init__(*_args, **kwargs) - self.train_data_collator = self.data_collator - self._stored_metrics = defaultdict(lambda: defaultdict(list)) - if self.args.orpo_alpha: - self.loss_fct = torch.nn.CrossEntropyLoss(reduction="none") - - def _wrap_model(self, model, training=True, dataloader=None): - if self.args.torch_compile: - torch._dynamo.config.accumulated_cache_size_limit = ( # pylint: disable=protected-access - 256 - ) - model = torch.compile( - model, - backend=self.args.torch_compile_backend, - mode=self.args.torch_compile_mode, - ) - return super()._wrap_model(model, training=training, dataloader=dataloader) - def create_optimizer_grouped_parameters(self, opt_model, optimizer_kwargs): + def create_optimizer_grouped_parameters( + self, opt_model, optimizer_kwargs + ) -> list[dict]: decay_parameters = self.get_decay_parameter_names(opt_model) - params = { + params: dict = { "to_weight_decay": {}, # LayerNorm and bias "embeddings": {}, # lm_head, embed_tokens, "no_weight_decay": {}, @@ -293,23 +266,30 @@ def create_optimizer(self): and self.args.embedding_lr_scale is None and self.args.embedding_lr is None and self.args.lr_groups is None - and self.args.alternate_optimizer - not in [ - "optimi_adamw", - "ao_adamw_8bit", - "ao_adamw_4bit", - "ao_adamw_fp8", - "adopt_adamw", - ] + and self.optimizer_cls_and_kwargs is None ): return super().create_optimizer() opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model - if self.optimizer is None: # pylint: disable=access-member-before-definition - optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs( - self.args, - opt_model, + + if ( + not self.optimizer + and self.optimizer_cls_and_kwargs is not None + and issubclass(self.optimizer_cls_and_kwargs[0], BaseOptimizerFactory) + ): + optimizer_factory_cls, optimizer_kwargs = self.optimizer_cls_and_kwargs + self.optimizer = optimizer_factory_cls()( + opt_model, self.args, **optimizer_kwargs ) + + if not self.optimizer: + if self.optimizer_cls_and_kwargs is not None: + optimizer_cls, optimizer_kwargs = self.optimizer_cls_and_kwargs + else: + optimizer_cls, optimizer_kwargs = self.get_optimizer_cls_and_kwargs( + self.args, opt_model + ) + optimizer_grouped_parameters = self.create_optimizer_grouped_parameters( opt_model, optimizer_kwargs ) @@ -326,50 +306,47 @@ def create_optimizer(self): loraplus_lr_embedding=loraplus_lr_embedding, **optimizer_kwargs, ) - elif ( - self.args.embedding_lr_scale is not None - or self.args.embedding_lr is not None - or self.args.lr_groups is not None - ): - self.optimizer = ( # pylint: disable=attribute-defined-outside-init - optimizer_cls(optimizer_grouped_parameters, **optimizer_kwargs) - ) - elif self.args.alternate_optimizer == "optimi_adamw": - from optimi import AdamW - - self.optimizer = ( # pylint: disable=attribute-defined-outside-init - AdamW( - optimizer_grouped_parameters, foreach=False, **optimizer_kwargs + else: + # Overwrite `params` in case it's created by `get_optimizer_cls_and_kwargs` + # e.g. for GaLore optimizer. + if "params" in optimizer_kwargs: + optimizer_grouped_parameters = optimizer_kwargs.pop("params") + + # Overwrite `model` in case it's created by `get_optimizer_cls_and_kwargs` + # e.g. for LOMO optimizer. + if "model" in optimizer_kwargs: + optimizer_grouped_parameters = optimizer_kwargs.pop("model") + + # For layer-wise dummy optimizers we overwrite optimizer_grouped_parameters with `optimizer_dict` + # to avoid arguments conflicts. + if "optimizer_dict" in optimizer_kwargs: + optimizer_grouped_parameters = optimizer_kwargs.pop( + "optimizer_dict" ) - ) - elif self.args.alternate_optimizer == "ao_adamw_4bit": - from torchao.prototype.low_bit_optim import AdamW4bit - self.optimizer = ( # pylint: disable=attribute-defined-outside-init - AdamW4bit(optimizer_grouped_parameters, **optimizer_kwargs) + self.optimizer = optimizer_cls( + optimizer_grouped_parameters, **optimizer_kwargs ) - elif self.args.alternate_optimizer == "ao_adamw_8bit": - from torchao.prototype.low_bit_optim import AdamW8bit - self.optimizer = ( # pylint: disable=attribute-defined-outside-init - AdamW8bit(optimizer_grouped_parameters, **optimizer_kwargs) - ) - elif self.args.alternate_optimizer == "ao_adamw_fp8": - from torchao.prototype.low_bit_optim import AdamWFp8 - - self.optimizer = ( # pylint: disable=attribute-defined-outside-init - AdamWFp8(optimizer_grouped_parameters, **optimizer_kwargs) - ) - elif self.args.alternate_optimizer == "adopt_adamw": - from axolotl.utils.optimizers.adopt import ADOPT - - self.optimizer = ( # pylint: disable=attribute-defined-outside-init - ADOPT( - optimizer_grouped_parameters, - decouple=True, - **optimizer_kwargs, - ) - ) + if optimizer_cls.__name__ == "Adam8bit": + import bitsandbytes + + manager = bitsandbytes.optim.GlobalOptimManager.get_instance() + + skipped = 0 + for module in opt_model.modules(): + if isinstance(module, nn.Embedding): + skipped += sum( + { + p.data_ptr(): p.numel() for p in module.parameters() + }.values() + ) + LOG.info(f"skipped {module}: {skipped/2**20}M params") + manager.register_module_override( + module, "weight", {"optim_bits": 32} + ) + LOG.debug(f"bitsandbytes: will optimize {module} in fp32") + LOG.info(f"skipped: {skipped/2**20}M params") if is_sagemaker_mp_enabled(): self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init @@ -378,6 +355,45 @@ def create_optimizer(self): return self.optimizer + +class AxolotlTrainer(SchedulerMixin, OptimizerMixin, Trainer): + """ + Extend the base Trainer for axolotl helpers + """ + + args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] + tag_names = ["axolotl"] + + def __init__( + self, + *_args, + bench_data_collator=None, + eval_data_collator=None, + dataset_tags=None, + **kwargs, + ): + self.bench_data_collator = bench_data_collator + self.eval_data_collator = eval_data_collator + self.dataset_tags = dataset_tags + self._signature_columns = None # workaround for pylint + super().__init__(*_args, **kwargs) + self.train_data_collator = self.data_collator + self._stored_metrics = defaultdict(lambda: defaultdict(list)) + if self.args.orpo_alpha: + self.loss_fct = torch.nn.CrossEntropyLoss(reduction="none") + + def _wrap_model(self, model, training=True, dataloader=None): + if self.args.torch_compile: + torch._dynamo.config.accumulated_cache_size_limit = ( # pylint: disable=protected-access + 256 + ) + model = torch.compile( + model, + backend=self.args.torch_compile_backend, + mode=self.args.torch_compile_mode, + ) + return super()._wrap_model(model, training=training, dataloader=dataloader) + def _get_train_sampler(self) -> Optional[torch.utils.data.Sampler]: if self.args.sample_packing and not self.args.pretraining: if self.args.multipack_real_batches: diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 211d5e51b4..11015e31ae 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -23,6 +23,8 @@ import logging from typing import OrderedDict +import torch + class BasePlugin: """ @@ -469,3 +471,14 @@ def post_train_unload(self, cfg): """ for plugin in self.plugins.values(): plugin.post_train_unload(cfg) + + +class BaseOptimizerFactory: + """ + Base class for factories to create custom optimizers + """ + + def __call__( + self, opt_model, training_args, **optimizer_kwargs + ) -> "torch.optim.Optimizer": + pass diff --git a/src/axolotl/train.py b/src/axolotl/train.py index b2f4bf1e99..178b90f7b6 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -461,7 +461,7 @@ def setup_model_and_trainer( def train( cfg: DictDefault, dataset_meta: TrainDatasetMeta -) -> tuple[PeftModel | PreTrainedModel, PreTrainedTokenizer]: +) -> tuple[PeftModel | PreTrainedModel, PreTrainedTokenizer, Trainer]: """ Train a model on the given dataset. @@ -510,4 +510,4 @@ def train( # Create model card create_model_card(cfg, trainer) - return model, tokenizer + return model, tokenizer, trainer diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 5143469be1..14e8a62858 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -64,6 +64,18 @@ class ChatTemplate(str, Enum): metharme = "metharme" # pylint: disable=invalid-name +class CustomSupportedOptimizers(str, Enum): + """Custom supported optimizers""" + + optimi_adamw = "optimi_adamw" # pylint: disable=invalid-name + ao_adamw_4bit = "ao_adamw_4bit" # pylint: disable=invalid-name + ao_adamw_8bit = "ao_adamw_8bit" # pylint: disable=invalid-name + ao_adamw_fp8 = "ao_adamw_fp8" # pylint: disable=invalid-name + adopt_adamw = "adopt_adamw" # pylint: disable=invalid-name + lion_pytorch = "lion_pytorch" # pylint: disable=invalid-name + muon = "muon" # pylint: disable=invalid-name + + class DeprecatedParameters(BaseModel): """configurations that are deprecated""" @@ -494,17 +506,7 @@ class HyperparametersConfig(BaseModel): embedding_lr_scale: Optional[float] = None weight_decay: Optional[float] = 0.0 optimizer: Optional[ - Union[ - OptimizerNames, - Literal[ - "lion_pytorch", - "optimi_adamw", - "ao_adamw_4bit", - "ao_adamw_8bit", - "ao_adamw_fp8", - "adopt_adamw", - ], - ] + Union[OptimizerNames, CustomSupportedOptimizers] ] = OptimizerNames.ADAMW_HF optim_args: Optional[Union[str, Dict[str, Any]]] = Field( default=None, @@ -1187,6 +1189,13 @@ def check_adamw_optimizer_params(self): LOG.warning("adamw hyperparameters found, but no adamw optimizer set") return self + @model_validator(mode="before") + @classmethod + def check_lr_groups(cls, data): + if data.get("lr_groups") and data.get("loraplus_lr_ratio"): + raise ValueError("lr_groups and loraplus_lr_ratio cannot be used together.") + return data + @model_validator(mode="before") @classmethod def check_saves(cls, data): diff --git a/tests/cli/test_cli_train.py b/tests/cli/test_cli_train.py index 560f3caf58..a512510335 100644 --- a/tests/cli/test_cli_train.py +++ b/tests/cli/test_cli_train.py @@ -28,7 +28,7 @@ def test_train_basic_execution_no_accelerate( config_path.write_text(valid_test_config) with patch("axolotl.cli.train.train") as mock_train: - mock_train.return_value = (MagicMock(), MagicMock()) + mock_train.return_value = (MagicMock(), MagicMock(), MagicMock()) result = cli_runner.invoke( cli, @@ -48,7 +48,7 @@ def test_train_cli_overrides(self, cli_runner, tmp_path, valid_test_config): config_path = self._test_cli_overrides(tmp_path, valid_test_config) with patch("axolotl.cli.train.train") as mock_train: - mock_train.return_value = (MagicMock(), MagicMock()) + mock_train.return_value = (MagicMock(), MagicMock(), MagicMock()) result = cli_runner.invoke( cli, diff --git a/tests/e2e/test_mixtral.py b/tests/e2e/test_mixtral.py index 5de5ab4036..f31920be6f 100644 --- a/tests/e2e/test_mixtral.py +++ b/tests/e2e/test_mixtral.py @@ -75,7 +75,7 @@ def test_qlora_w_fa2(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - model, _ = train(cfg=cfg, dataset_meta=dataset_meta) + model, _, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype == torch.float32 @@ -131,7 +131,7 @@ def test_qlora_wo_fa2(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - model, _ = train(cfg=cfg, dataset_meta=dataset_meta) + model, _, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype == torch.float32 @@ -190,7 +190,7 @@ def test_16bit_lora_w_fa2(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - model, _ = train(cfg=cfg, dataset_meta=dataset_meta) + model, _, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype == torch.float32 @@ -249,7 +249,7 @@ def test_16bit_lora_wo_fa2(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - model, _ = train(cfg=cfg, dataset_meta=dataset_meta) + model, _, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype == torch.float32 diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 4b0ad1142a..43a4735aa9 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -65,8 +65,9 @@ def test_optimi_adamw(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, dataset_meta=dataset_meta) + _, _, trainer = train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + assert trainer.optimizer.optimizer.__class__.__name__ == "AdamW" @with_temp_dir @require_torch_2_5_1 @@ -111,8 +112,57 @@ def test_adopt_adamw(self, temp_dir): cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) - train(cfg=cfg, dataset_meta=dataset_meta) + _, _, trainer = train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) + assert "ADOPT" in trainer.optimizer.optimizer.__class__.__name__ + + @with_temp_dir + @require_torch_2_5_1 + def test_muon(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 1024, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.1, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "muon", + "lr_scheduler": "cosine", + "weight_decay": 0.01, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + _, _, trainer = train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + assert "Muon" in trainer.optimizer.optimizer.__class__.__name__ @with_temp_dir def test_fft_schedule_free_adamw(self, temp_dir): From fa7c79b3b9f9ad2bf4c47a140dff7e3090996f52 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 7 Mar 2025 08:58:15 -0500 Subject: [PATCH 0457/1405] remove lion-pytorch as it's already handled upstream (#2389) --- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 14e8a62858..5cc3fbc4f7 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -72,7 +72,6 @@ class CustomSupportedOptimizers(str, Enum): ao_adamw_8bit = "ao_adamw_8bit" # pylint: disable=invalid-name ao_adamw_fp8 = "ao_adamw_fp8" # pylint: disable=invalid-name adopt_adamw = "adopt_adamw" # pylint: disable=invalid-name - lion_pytorch = "lion_pytorch" # pylint: disable=invalid-name muon = "muon" # pylint: disable=invalid-name From 16dc6ee68d05bfbef607a8ee06e00fca1d8c4739 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 7 Mar 2025 20:58:53 +0700 Subject: [PATCH 0458/1405] refactor: trl grpo configs to have descriptions (#2386) * refactor: trl grpo configs to have descriptions * chore: caps --- docs/rlhf.qmd | 4 + .../utils/config/models/input/v0_4_1/trl.py | 85 +++++++++++++++---- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 741976bc68..773b159e84 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -3,6 +3,7 @@ title: "RLHF (Beta)" description: "Reinforcement Learning from Human Feedback is a method whereby a language model is optimized from data using human feedback." back-to-top-navigation: true toc: true +toc-expand: 2 toc-depth: 4 --- @@ -528,6 +529,7 @@ trl: vllm_gpu_memory_utilization: 0.15 num_generations: 4 reward_funcs: ["rewards.rand_reward_func"] # format: '{file_name}.{fn_name}' + reward_weights: [1.0] datasets: - path: openai/gsm8k name: main @@ -536,6 +538,8 @@ datasets: To see other examples of custom reward functions, please see [TRL GRPO Docs](https://github.com/huggingface/trl/blob/main/docs/source/grpo_trainer.md#using-a-custom-reward-function). +To see description of the configs, please see [TRLConfig](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/utils/config/models/input/v0_4_1/trl.py). + ### Using local dataset files ```yaml diff --git a/src/axolotl/utils/config/models/input/v0_4_1/trl.py b/src/axolotl/utils/config/models/input/v0_4_1/trl.py index ae26e634ca..f408acdba3 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/trl.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/trl.py @@ -1,7 +1,8 @@ """ GRPO specific configuration args """ -from typing import List, Optional + +from typing import Optional from pydantic import BaseModel, Field @@ -11,7 +12,10 @@ class TRLConfig(BaseModel): Input args for TRL. """ - beta: Optional[float] = None + beta: Optional[float] = Field( + default=None, + json_schema_extra={"description": "Beta for RL training"}, + ) max_completion_length: Optional[int] = Field( default=None, json_schema_extra={ @@ -20,17 +24,68 @@ class TRLConfig(BaseModel): ) # GRPO specific args - use_vllm: Optional[bool] = False - vllm_device: Optional[str] = "auto" - vllm_gpu_memory_utilization: Optional[float] = 0.9 - vllm_max_model_len: Optional[int] = None - vllm_dtype: Optional[str] = "auto" - - reward_funcs: Optional[List[str]] = None - reward_weights: Optional[List[float]] = None - num_generations: Optional[int] = None - log_completions: Optional[bool] = False + # Ref: https://github.com/huggingface/trl/blob/e3244d2d096ff1e2e248c931d06d39e165e20623/trl/trainer/grpo_config.py#L22 + use_vllm: Optional[bool] = Field( + default=False, + json_schema_extra={"description": "Whether to use VLLM for RL training"}, + ) + vllm_device: Optional[str] = Field( + default="auto", + json_schema_extra={"description": "Device to use for VLLM"}, + ) + vllm_gpu_memory_utilization: Optional[float] = Field( + default=0.9, + json_schema_extra={"description": "GPU memory utilization for VLLM"}, + ) + vllm_dtype: Optional[str] = Field( + default="auto", + json_schema_extra={"description": "Data type for VLLM"}, + ) + vllm_max_model_len: Optional[int] = Field( + default=None, + json_schema_extra={ + "description": "Maximum length of the model context for VLLM" + }, + ) - sync_ref_model: Optional[bool] = False - ref_model_mixup_alpha: Optional[float] = 0.9 - ref_model_sync_steps: Optional[int] = 64 + reward_funcs: Optional[list[str]] = Field( + default=None, + json_schema_extra={"description": "List of reward functions to load"}, + ) + reward_weights: Optional[list[float]] = Field( + default=None, + json_schema_extra={ + "description": "Weights for each reward function. Must match the number of reward functions." + }, + ) + num_generations: Optional[int] = Field( + default=None, + json_schema_extra={ + "description": "Number of generations to sample. The global batch size (num_processes * per_device_batch_size) must be divisible by this value." + }, + ) + log_completions: Optional[bool] = Field( + default=False, + json_schema_extra={"description": "Whether to log completions"}, + ) + sync_ref_model: Optional[bool] = Field( + default=False, + json_schema_extra={ + "description": ( + "Whether to sync the reference model every `ref_model_sync_steps` " + "steps, using the `ref_model_mixup_alpha` parameter." + ) + }, + ) + ref_model_mixup_alpha: Optional[float] = Field( + default=0.9, + json_schema_extra={ + "description": "Mixup alpha for the reference model. Requires `sync_ref_model=True`." + }, + ) + ref_model_sync_steps: Optional[int] = Field( + default=64, + json_schema_extra={ + "description": "Sync steps for the reference model. Requires `sync_ref_model=True`." + }, + ) From 3b477e08a0e67d405daf29acac97ed358183af89 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 10 Mar 2025 16:25:31 +0700 Subject: [PATCH 0459/1405] feat(doc): add more info on RewardModel datasets (#2391) * fix: reduce title size * feat(doc): add rm dataset info * Update docs/reward_modelling.qmd following suggestion Co-authored-by: salman --------- Co-authored-by: salman --- docs/reward_modelling.qmd | 13 +++++++++++++ styles.css | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/reward_modelling.qmd b/docs/reward_modelling.qmd index 8baa934241..c9ac5f8019 100644 --- a/docs/reward_modelling.qmd +++ b/docs/reward_modelling.qmd @@ -28,6 +28,17 @@ val_set_size: 0.1 eval_steps: 100 ``` +Bradley-Terry chat templates expect single-turn conversations in the following format: + +```json +{ + "system": "...", // optional + "input": "...", + "chosen": "...", + "rejected": "..." +} +``` + ### Process Reward Models (PRM) Process reward models are trained using data which contains preference annotations for each step in a series of interactions. Typically, PRMs are trained to provide reward signals over each step of a reasoning trace and are used for downstream reinforcement learning. @@ -45,3 +56,5 @@ datasets: val_set_size: 0.1 eval_steps: 100 ``` + +Please see [stepwise_supervised](dataset-formats/stepwise_supervised.qmd) for more details on the dataset format. diff --git a/styles.css b/styles.css index 891349b4b0..749ff4366b 100644 --- a/styles.css +++ b/styles.css @@ -14,7 +14,7 @@ h1 { font-family: var(--font-title); font-weight: 400; - font-size: 6rem; + font-size: 5rem; line-height: 1.1; letter-spacing: -0.05em; font-feature-settings: "ss01" on; From 46a045e52807a4108b21043b6041b88be2f19912 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 10 Mar 2025 16:25:50 +0700 Subject: [PATCH 0460/1405] chore(doc): add faq when having no default chat_template (#2398) * chore(doc): add faq when having no default chat_template * Update docs/dataset-formats/conversation.qmd Co-authored-by: salman * Update docs/faq.qmd Co-authored-by: salman --------- Co-authored-by: salman --- docs/dataset-formats/conversation.qmd | 4 ++++ docs/faq.qmd | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 8ce95b7b06..81c902afdc 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -74,6 +74,10 @@ datasets: train_on_eos: ``` +::: {.callout-tip} +If you receive an error like "`chat_template` choice is `tokenizer_default` but tokenizer's `chat_template` is null.", it means the tokenizer does not have a default `chat_template`. Follow the examples below instead to set a custom `chat_template`. +::: + 2. Using the `gemma` chat template to override the tokenizer_config.json's chat template on OpenAI messages format, training on all assistant messages. ```yaml diff --git a/docs/faq.qmd b/docs/faq.qmd index 7e5dd3d746..ba7ac1265b 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -52,3 +52,7 @@ description: Frequently asked questions **Q: The EOS/EOT token is incorrectly being masked or not being masked.** > A: This is because of the mismatch between `tokenizer.eos_token` and EOS/EOT token in template. Please make sure to set `eos_token` under `special_tokens` to the same EOS/EOT token as in template. + +**Q: "`chat_template` choice is `tokenizer_default` but tokenizer's `chat_template` is null. Please add a `chat_template` in tokenizer config"** + +> A: This is because the tokenizer does not have a chat template. Please add a chat template in the tokenizer config. See [chat_template](dataset-formats/conversation.qmd#chat-template) for more details. From 60a11a64104aca8946c8f17249ed59d22552a3c0 Mon Sep 17 00:00:00 2001 From: xzuyn <16216325+xzuyn@users.noreply.github.com> Date: Mon, 10 Mar 2025 05:26:40 -0400 Subject: [PATCH 0461/1405] Use Latest Cut Cross Entropy (#2392) * Update __init__.py * Update README.md * Update cutcrossentropy_install.py * add test --- scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 2 +- .../cut_cross_entropy/__init__.py | 2 +- .../integrations/test_cut_cross_entropy.py | 45 +++++++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index d51e2dd99c..71edc8d0c1 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -24,5 +24,5 @@ print( UNINSTALL_PREFIX - + 'pip install "cut-cross-entropy @ git+https://github.com/apple/ml-cross-entropy.git@9c297c905f55b73594b5d650722d1e78183b77bd"' + + 'pip install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@24fbe4b5dab9a6c250a014573613c1890190536c"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 14449a1442..b166a30049 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -17,7 +17,7 @@ Run the following command to install `cut_cross_entropy[transformers]` if you do python scripts/cutcrossentropy_install.py | sh # if you are not in dev environment -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy @ git+https://github.com/apple/ml-cross-entropy.git@9c297c905f55b73594b5d650722d1e78183b77bd"' +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@24fbe4b5dab9a6c250a014573613c1890190536c" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 97517bccdb..516e9a2ae0 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -33,7 +33,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers]==24.11.4"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@24fbe4b5dab9a6c250a014573613c1890190536c"`' ) diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py index f65d65ee40..25e36b5ebd 100644 --- a/tests/e2e/integrations/test_cut_cross_entropy.py +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -69,6 +69,51 @@ def test_llama_w_cce(self, min_cfg, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + # pylint: disable=redefined-outer-name + def test_qwen2_w_cce(self, temp_dir): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "plugins": [ + "axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin", + ], + "cut_cross_entropy": True, + "sequence_len": 1024, + "val_set_size": 0.1, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 1, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "output_dir": temp_dir, + "lr_scheduler": "cosine", + "save_safetensors": True, + "max_steps": 10, + "bf16": "auto", + } + ) + prepare_plugins(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + major, minor, _ = get_pytorch_version() + if (major, minor) < (2, 4): + with pytest.raises(ImportError): + train(cfg=cfg, dataset_meta=dataset_meta) + else: + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) + @pytest.mark.parametrize( "attention_type", [ From 83f8698b8adaf6c56f10e8f1f8895c29f7c0593d Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 10 Mar 2025 16:27:42 +0700 Subject: [PATCH 0462/1405] fix: create mount folder on modal if not exist (#2390) --- src/axolotl/cli/cloud/modal_.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index e2095a1e9e..c350331eb5 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -270,6 +270,7 @@ def _preprocess(config_yaml: str, volumes=None): def _train(config_yaml: str, accelerate: bool = True, volumes=None, **kwargs): + Path("/workspace/mounts").mkdir(parents=True, exist_ok=True) with open("/workspace/mounts/config.yaml", "w", encoding="utf-8") as f_out: f_out.write(config_yaml) run_folder = "/workspace/mounts" @@ -288,6 +289,7 @@ def _train(config_yaml: str, accelerate: bool = True, volumes=None, **kwargs): def _lm_eval(config_yaml: str, volumes=None): + Path("/workspace/mounts").mkdir(parents=True, exist_ok=True) with open("/workspace/mounts/config.yaml", "w", encoding="utf-8") as f_out: f_out.write(config_yaml) run_folder = "/workspace/mounts" From 5d0f110a3b6bac0a52c699f334ba6fc006414ef3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 10 Mar 2025 15:13:38 -0400 Subject: [PATCH 0463/1405] include iproute2 and nvtop in cloud image (#2393) --- docker/Dockerfile-cloud | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile-cloud b/docker/Dockerfile-cloud index 735afa4dd7..c84ea1dca9 100644 --- a/docker/Dockerfile-cloud +++ b/docker/Dockerfile-cloud @@ -14,7 +14,7 @@ COPY scripts/motd /etc/motd RUN pip install jupyterlab notebook ipywidgets && \ jupyter lab clean -RUN apt install --yes --no-install-recommends openssh-server tmux && \ +RUN apt install --yes --no-install-recommends openssh-server tmux iproute2 nvtop && \ mkdir -p ~/.ssh && \ chmod 700 ~/.ssh && \ printf "\n[[ -z \"\$TMUX\" ]] && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> ~/.bashrc && \ From 4a736986fa6f3387f21391f186f472926754dac1 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 11 Mar 2025 02:14:41 +0700 Subject: [PATCH 0464/1405] fix(modal): add git pull when getting branch files (#2399) --- src/axolotl/cli/cloud/modal_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index c350331eb5..47bc3221ab 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -113,7 +113,7 @@ def get_image(self): [ # Random id for cache busting of branch commits f"RUN echo '{str(randint(0, 1000000))}'", # nosec B311 - f"RUN cd /workspace/axolotl && git fetch && git checkout {self.config.branch}", + f"RUN cd /workspace/axolotl && git fetch && git checkout {self.config.branch} && git pull", ] ) From 59899b9817c0eb8ee2b0dbd8e35ff31de268af26 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 11 Mar 2025 12:02:43 -0400 Subject: [PATCH 0465/1405] pass additional info for fix untrained tokens when using distributed + offloading (#2388) * pass additional info for fix untrained tokens when using distributed + offloading * use latest version of vendored lib * use v0.0.5 of contribs lgpl * fix for no bad tokens and add tests * use release * add multigpu test too * make sure the multigpu zero3 test actually uses zero3 --- requirements.txt | 2 +- src/axolotl/train.py | 22 ++++++----- tests/e2e/multigpu/test_llama.py | 63 ++++++++++++++++++++++++++++++++ tests/e2e/test_llama.py | 48 ++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index cd5690f0b2..c3a9eb2f38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,5 +62,5 @@ antlr4-python3-runtime==4.13.2 torchao==0.7.0 schedulefree==1.3.0 -axolotl-contribs-lgpl==0.0.3 +axolotl-contribs-lgpl==0.0.6 axolotl-contribs-mit==0.0.3 diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 178b90f7b6..1ceb5babd0 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -7,7 +7,7 @@ import sys import weakref from pathlib import Path -from typing import Any +from typing import Any, Dict import torch import transformers.modelcard @@ -20,7 +20,7 @@ from transformers.trainer import Trainer from axolotl.common.datasets import TrainDatasetMeta -from axolotl.contribs.lgpl.unsloth import ( # pylint: disable = no-name-in-module +from axolotl.contribs.lgpl import ( # pylint: disable = no-name-in-module fix_untrained_tokens, ) from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder @@ -382,21 +382,23 @@ def handle_untrained_tokens_fix( if not cfg.fix_untrained_tokens: return + is_ds_zero3: bool = False + if os.environ.get("ACCELERATE_DEEPSPEED_ZERO_STAGE") == "3": + is_ds_zero3 = True + # Check if the `token_ids_to_fix` kwarg exists in the fix_untrained_tokens args sig = inspect.signature(fix_untrained_tokens) + fix_kwargs: Dict[str, Any] = {} # If the function has the `token_ids_to_fix` arg, and fix_untrained_tokens is a list if "token_ids_to_fix" in sig.parameters and isinstance( cfg.fix_untrained_tokens, list ): - fix_untrained_tokens( - model, - tokenizer, - train_dataset, - token_ids_to_fix=cfg.fix_untrained_tokens, - ) - else: - fix_untrained_tokens(model, tokenizer, train_dataset) + fix_kwargs["token_ids_to_fix"] = cfg.fix_untrained_tokens + if "is_ds_zero3" in sig.parameters: + fix_kwargs["is_ds_zero3"] = is_ds_zero3 + + fix_untrained_tokens(model, tokenizer, train_dataset, **fix_kwargs) if cfg.local_rank == 0: model.save_pretrained( diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 0f91fe056f..60b1940907 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -750,3 +750,66 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): check_tensorboard( temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" ) + + def test_fix_untrained_tokens(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "fix_untrained_tokens": True, + "sequence_len": 512, + "val_set_size": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>", + "bos_token": "<|custom_im_start|>", + "eos_token": "<|custom_im_end|>", + }, + "datasets": [ + { + "chat_template": "jinja", + "chat_template_jinja": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|custom_im_start|>' + message['role'] + '\n' + message['content'] + '<|custom_im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|custom_im_start|>assistant\n' }}{% endif %}", + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "split": "train[:10%]", + "field_messages": "conversations", + "message_field_role": "from", + "message_field_content": "value", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": True, + "bf16": True, + "save_safetensors": True, + "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero3_bf16.json"), + "use_tensorboard": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 4.0, "Train Loss is too high" + ) diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index 77e70d8c24..6447442404 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -66,6 +66,54 @@ def test_fft_trust_remote_code(self, temp_dir): check_model_output_exists(temp_dir, cfg) def test_fix_untrained_tokens(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "fix_untrained_tokens": True, + "sequence_len": 512, + "val_set_size": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>", + "bos_token": "<|custom_im_start|>", + "eos_token": "<|custom_im_end|>", + }, + "datasets": [ + { + "chat_template": "jinja", + "chat_template_jinja": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|custom_im_start|>' + message['role'] + '\n' + message['content'] + '<|custom_im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|custom_im_start|>assistant\n' }}{% endif %}", + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "split": "train[:10%]", + "field_messages": "conversations", + "message_field_role": "from", + "message_field_content": "value", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": True, + "bf16": True, + "save_safetensors": True, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) + + def test_fix_untrained_tokens_already_trained(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { From f0072f3b9d580e8e967e8189f8a270494ace7f85 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 11 Mar 2025 12:02:58 -0400 Subject: [PATCH 0466/1405] use max of 32 dataset processes if not explicit (#2403) * use max of 32 dataset processes if not explicit * change alternate min val for consistency --- src/axolotl/core/datasets/chat.py | 2 +- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/axolotl/core/datasets/chat.py b/src/axolotl/core/datasets/chat.py index e74c247d2c..ba257071dc 100644 --- a/src/axolotl/core/datasets/chat.py +++ b/src/axolotl/core/datasets/chat.py @@ -43,7 +43,7 @@ def map_fn(ex): process_or_cpu_count: int = ( process_count or os.cpu_count() # type: ignore[assignment] ) - num_proc = min(64, process_or_cpu_count) + num_proc = min(32, process_or_cpu_count) features = data.features.keys() tokenized_data = data.map( map_fn, diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 5cc3fbc4f7..921a015d33 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -728,7 +728,7 @@ class AxolotlInputConfig( default=None, json_schema_extra={"description": "streaming dataset to use for pretraining"}, ) - dataset_processes: Optional[int] = Field(default=os.cpu_count()) + dataset_processes: Optional[int] = Field(default=min(32, os.cpu_count())) # type: ignore[type-var] dataset_exact_deduplication: Optional[bool] = None dataset_keep_in_memory: Optional[bool] = None dataloader_pin_memory: Optional[bool] = None From 04f6324833715d30fd928c3b8d3564d9835f3b0c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 13 Mar 2025 23:28:51 -0400 Subject: [PATCH 0467/1405] build cloud images with torch 2.6.0 (#2413) * build cloud images with torch 2.6.0 * nightlies too --- .github/workflows/main.yml | 5 +++++ .github/workflows/nightlies.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7fb97b9d93..4a7112041b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -88,6 +88,11 @@ jobs: pytorch: 2.5.1 axolotl_extras: is_latest: true + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.6.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index c501064d25..bc3c1a1916 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -80,6 +80,11 @@ jobs: python_version: "3.11" pytorch: 2.5.1 axolotl_extras: + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.6.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout From fbe54be6b84679979cdfc9cb8970e8cc3f266cd8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 13 Mar 2025 23:29:06 -0400 Subject: [PATCH 0468/1405] only validate hf user token on rank 0 (#2408) --- src/axolotl/cli/train.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 032f12b667..e991105e6b 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -1,6 +1,7 @@ """CLI to run training on a model.""" import logging +import os from pathlib import Path from typing import Union @@ -34,7 +35,8 @@ def do_train(cfg: DictDefault, cli_args: TrainerCliArgs) -> None: """ print_axolotl_text_art() check_accelerate_default_config() - check_user_token() + if int(os.getenv("LOCAL_RANK", "0")) == 0: + check_user_token() if cfg.rl: dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) From 4f5eb42a735d45dfffedf149b7cabd10d6803d9b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 15 Mar 2025 08:49:41 -0400 Subject: [PATCH 0469/1405] remove reference to deprecated import (#2407) --- src/axolotl/utils/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 1805a749a6..93d0f13c0d 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -24,7 +24,6 @@ PeftModelForCausalLM, prepare_model_for_kbit_training, ) -from peft.tuners.lora import QuantLinear from torch import nn from transformers import ( # noqa: F401 AddedToken, @@ -1360,7 +1359,7 @@ def load_llama_adapter(model, cfg): def find_all_linear_names(model): - cls = (bnb.nn.Linear4bit, bnb.nn.Linear8bitLt, torch.nn.Linear, QuantLinear) + cls = (bnb.nn.Linear4bit, bnb.nn.Linear8bitLt, torch.nn.Linear) lora_module_names = set() for name, module in model.named_modules(): if ( From 7235123d449a8ea7e0e98300a1fc2e3bf82ecc95 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 17 Mar 2025 19:38:19 +0700 Subject: [PATCH 0470/1405] chore(docs): add cookbook/blog link to docs (#2410) [skip ci] --- docs/lora_optims.qmd | 4 ++++ docs/reward_modelling.qmd | 4 ++++ docs/rlhf.qmd | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/docs/lora_optims.qmd b/docs/lora_optims.qmd index 8bee20402e..a7555a0a33 100644 --- a/docs/lora_optims.qmd +++ b/docs/lora_optims.qmd @@ -66,6 +66,10 @@ logic to be compatible with more of them. +::: {.callout-tip} +Check out our [LoRA optimizations blog](https://axolotlai.substack.com/p/accelerating-lora-fine-tuning-with). +::: + ## Usage These optimizations can be enabled in your Axolotl config YAML file. The diff --git a/docs/reward_modelling.qmd b/docs/reward_modelling.qmd index c9ac5f8019..386dc1f572 100644 --- a/docs/reward_modelling.qmd +++ b/docs/reward_modelling.qmd @@ -41,6 +41,10 @@ Bradley-Terry chat templates expect single-turn conversations in the following f ### Process Reward Models (PRM) +::: {.callout-tip} +Check out our [PRM blog](https://axolotlai.substack.com/p/process-reward-models). +::: + Process reward models are trained using data which contains preference annotations for each step in a series of interactions. Typically, PRMs are trained to provide reward signals over each step of a reasoning trace and are used for downstream reinforcement learning. ```yaml base_model: Qwen/Qwen2.5-3B diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 773b159e84..ac1cf03938 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -497,6 +497,10 @@ The input format is a simple JSON input with customizable fields based on the ab ### GRPO +::: {.callout-tip} +Check out our [GRPO cookbook](https://github.com/axolotl-ai-cloud/axolotl-cookbook/tree/main/grpo#training-an-r1-style-large-language-model-using-grpo). +::: + GRPO uses custom reward functions and transformations. Please have them ready locally. For ex, to load OpenAI's GSM8K and use a random reward for completions: From 51cd4094883b44ea9521237c089fc86a5eb98ef6 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 17 Mar 2025 19:39:04 +0700 Subject: [PATCH 0471/1405] Feat: minor docs improvements for RLHF and faq on embeddings (#2401) [skip ci] * feat: add doc on shrink_embeddings and custom calling * chore: rename inference doc * fix: clarify same config is used for all cli * chore: rearrange order inference qmd * feat: add simpo to doc * fix: update defaults * feat: add rl configs to doc * fix: ensure beta consistent with trl.beta * fix: clarify about lora/fft * chore: rename title * chore: fix language * feat: move config reference higher * Update docs/getting-started.qmd Co-authored-by: salman * Update docs/rlhf.qmd Co-authored-by: salman --------- Co-authored-by: salman --- _quarto.yml | 7 +-- docs/config.qmd | 48 +++++++++++++++++-- docs/faq.qmd | 10 ++++ docs/getting-started.qmd | 20 +++++--- docs/inference.qmd | 8 +++- docs/rlhf.qmd | 20 ++++++-- .../config/models/input/v0_4_1/__init__.py | 9 ++++ 7 files changed, 100 insertions(+), 22 deletions(-) diff --git a/_quarto.yml b/_quarto.yml index c0536e730e..943ed5293e 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -32,8 +32,9 @@ website: contents: - docs/getting-started.qmd - docs/installation.qmd - - docs/cli.qmd - docs/inference.qmd + - docs/cli.qmd + - docs/config.qmd - section: "Dataset Formats" contents: docs/dataset-formats/* @@ -74,10 +75,6 @@ website: - docs/debugging.qmd - docs/nccl.qmd - - section: "Reference" - contents: - - docs/config.qmd - format: html: theme: darkly diff --git a/docs/config.qmd b/docs/config.qmd index fb0c4b59bc..38ec368a18 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -1,5 +1,5 @@ --- -title: Config options +title: Config Reference description: A complete list of all configuration options. --- @@ -30,6 +30,8 @@ tokenizer_legacy: # Resize the model embeddings when new tokens are added to multiples of 32 # This is reported to improve training speed on some models resize_token_embeddings_to_32x: +# Optional[bool] Whether to shrink the embeddings to len(tokenizer). By default, we won't shrink. +shrink_embeddings: # (Internal use only) # Used to identify which the model is based on @@ -205,10 +207,46 @@ test_datasets: data_files: - /workspace/data/eval.jsonl -# use RL training: 'dpo', 'ipo', 'kto' +# use RL training: 'dpo', 'ipo', 'kto', 'simpo', 'orpo', 'grpo' rl: -# whether to perform weighting if doing DPO training. Boolean. -dpo_use_weighting: +rl_beta: # Optional[float]. The beta parameter for the RL training. + +# dpo +dpo_use_weighting: # Optional[bool]. Whether to perform weighting. +rpo_alpha: # Optional[float]. Weighting of NLL term in loss from RPO paper. + +# orpo +orpo_alpha: 0.1 # Parameter controlling the relative ratio loss weight in the ORPO loss. Passed to `beta` in `ORPOConfig` due to trl mapping. + +# kto +kto_desirable_weight: # Optional[float]. Factor for desirable loss term in KTO loss. +kto_undesirable_weight: # Optional[float]. Factor for undesirable loss term in KTO loss. + +# simpo +cpo_alpha: 1.0 # Weight of the BC regularizer +simpo_gamma: 0.5 # Target reward margin for the SimPO loss + +# grpo +trl: + use_vllm: # Optional[bool]. Whether to use VLLM for RL training. + vllm_device: # Optional[str]. Device to use for VLLM. + vllm_gpu_memory_utilization: # Optional[float]. GPU memory utilization for VLLM. + vllm_max_model_len: # Optional[int]. Maximum length of the model for VLLM. + vllm_dtype: # Optional[str]. Data type for VLLM. + + beta: # Optional[float]. Beta parameter for the RL training. Same as `rl_beta`. Use + max_completion_length: # Optional[int]. Maximum length of the completion for RL training. + + reward_funcs: # Optional[list[str]]. List of reward functions to load. Paths must be importable from current dir. + reward_weights: # Optional[list[float]]. List of reward weights for the reward functions. + + num_generations: # Optional[int]. Number of generations to sample. + log_completions: # Optional[bool]. Whether to log completions. + + sync_ref_model: # Optional[bool]. Whether to sync the reference model. + ref_model_mixup_alpha: # Optional[float]. Mixup alpha for the reference model. + ref_model_sync_steps: # Optional[int]. Sync steps for the reference model. + # reward modelling: `True` or `False` reward_model: @@ -232,7 +270,7 @@ default_system_message: You are a helpful assistant. Please give a long and deta # subsequent training attempts load faster, relative path dataset_prepared_path: data/last_run_prepared # Push prepared dataset to hub -push_dataset_to_hub: # repo path +push_dataset_to_hub: # Optional[str] repo_org/repo_name # The maximum number of processes to use while preprocessing your input dataset. This defaults to `os.cpu_count()` # if not set. dataset_processes: # defaults to os.cpu_count() if not set diff --git a/docs/faq.qmd b/docs/faq.qmd index ba7ac1265b..acec1886ed 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -27,6 +27,16 @@ description: Frequently asked questions > A: This is usually an issue with the GPU. This can be resolved through setting the os environment variable `CUDA_VISIBLE_DEVICES=0`. If you are on runpod, this is usually a pod issue. Starting a new pod should take care of it. +**Q: Received mismatch error on merge adapters / loading adapters between torch.Size of checkpoint and model.** + +> A: This is likely due to vocab size mismatch. By default, Axolotl expands the model's embeddings if the tokenizer has more tokens than the model. Please use the `axolotl merge-lora` command to merge the adapters instead of using your own scripts. + +> On the other hand, if the model has more tokens than the tokenizer, Axolotl does not shrink the model's embeddings unless `shrink_embeddings: true` is set in the config. + +**Q: How to call Axolotl via custom python scripts?** + +> A: Yes, since Axolotl is just Python, please see `src/axolotl/cli/main.py` on how each command is called. + ### Chat templates **Q: `jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'content' / 'role' / ____`** diff --git a/docs/getting-started.qmd b/docs/getting-started.qmd index 8e826b9592..a0501ad21e 100644 --- a/docs/getting-started.qmd +++ b/docs/getting-started.qmd @@ -36,7 +36,9 @@ The YAML configuration file controls everything about your training. Here's what ```yaml base_model: NousResearch/Llama-3.2-1B -# hub_model_id: username/custom_model_name + +load_in_8bit: true +adapter: lora datasets: - path: teknium/GPT4-LLM-Cleaned @@ -44,11 +46,15 @@ datasets: dataset_prepared_path: last_run_prepared val_set_size: 0.1 output_dir: ./outputs/lora-out - -adapter: lora -lora_model_dir: ``` +::: {.callout-tip} +`load_in_8bit: true` and `adapter: lora` enables LoRA adapter finetuning. + +- To perform Full finetuning, remove these two lines. +- To perform QLoRA finetuning, replace with `load_in_4bit: true` and `adapter: qlora`. +::: + See our [Config options](config.qmd) for more details. ### Training {#sec-training} @@ -56,7 +62,7 @@ See our [Config options](config.qmd) for more details. When you run `axolotl train`, Axolotl: 1. Downloads the base model -2. (If specified) applies LoRA adapter layers +2. (If specified) applies QLoRA/LoRA adapter layers 3. Loads and processes the dataset 4. Runs the training loop 5. Saves the trained model and / or LoRA weights @@ -69,6 +75,8 @@ Let's modify the example for your own data: ```yaml base_model: NousResearch/Nous-Hermes-llama-1b-v1 + +load_in_8bit: true adapter: lora # Training settings @@ -104,8 +112,6 @@ format): {"instruction": "Classify this text", "input": "Not good at all", "output": "negative"} ``` -Please consult the supported [Dataset Formats](dataset-formats/) for more details. - 3. Run the training: ```bash diff --git a/docs/inference.qmd b/docs/inference.qmd index aded400d04..6917d3c330 100644 --- a/docs/inference.qmd +++ b/docs/inference.qmd @@ -1,5 +1,5 @@ --- -title: "Inference" +title: "Inference and Merging" format: html: toc: true @@ -9,10 +9,14 @@ execute: enabled: false --- -This guide covers how to use your trained models for inference, including model loading, interactive testing, and common troubleshooting steps. +This guide covers how to use your trained models for inference, including model loading, interactive testing, merging adapters, and common troubleshooting steps. ## Quick Start {#sec-quickstart} +::: {.callout-tip} +Use the same config used for training on inference/merging. +::: + ### Basic Inference {#sec-basic} ::: {.panel-tabset} diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index ac1cf03938..6bef7c8318 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -298,7 +298,7 @@ The input format is a simple JSON input with customizable fields based on the ab ### IPO -As IPO is just DPO with a different loss function, all supported options for DPO works here. +As IPO is just DPO with a different loss function, all supported dataset formats for [DPO](#dpo) are also supported for IPO. ```yaml rl: ipo @@ -344,8 +344,9 @@ ORPO supports the following types with the following dataset format: ```yaml rl: kto -rl_beta: 0.5 -kto_desirable_weight: 0.2 +rl_beta: 0.1 # default +kto_desirable_weight: 1.0 # default +kto_undesirable_weight: 1.0 # default remove_unused_columns: false @@ -544,6 +545,19 @@ To see other examples of custom reward functions, please see [TRL GRPO Docs](htt To see description of the configs, please see [TRLConfig](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/utils/config/models/input/v0_4_1/trl.py). +### SimPO + +SimPO uses [CPOTrainer](https://huggingface.co/docs/trl/main/en/cpo_trainer) but with alternative loss function. + +```yaml +rl: simpo +rl_beta: 0.1 # default in CPOTrainer +cpo_alpha: 1.0 # default in CPOTrainer +simpo_gamma: 0.5 # default in CPOTrainer +``` + +This method uses the same dataset format as [DPO](#dpo). + ### Using local dataset files ```yaml diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 921a015d33..b2584cfcd5 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -1,4 +1,5 @@ """Module with Pydantic models for configuration.""" + # pylint: disable=too-many-lines import logging @@ -1827,6 +1828,14 @@ def check_torch_compile_auto(cls, data): data["torch_compile"] = False return data + @model_validator(mode="before") + @classmethod + def check_beta_and_trl_beta_match(cls, data): + if data.get("beta") and data.get("trl", {}).get("beta"): + if data["beta"] != data["trl"]["beta"]: + raise ValueError("beta and trl.beta must match or one must be removed") + return data + def handle_legacy_message_fields_logic(data: dict) -> dict: """ From 85147ec430e83ea3fcee4703d85bb591445567e2 Mon Sep 17 00:00:00 2001 From: SicariusSicariiStuff <144873641+SicariusSicariiStuff@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:39:17 +0200 Subject: [PATCH 0472/1405] Update README.md (#2360) * Update README.md wheel is needed * feat: add ninja, setuptools, packing to installation steps * fix: add missing instruction --------- Co-authored-by: NanoCode012 --- README.md | 1 + docs/installation.qmd | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5d9a29a835..953bc0be5a 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Features: ### Installation ```bash +pip3 install -U packaging setuptools wheel ninja pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] # Download example axolotl configs, deepspeed configs diff --git a/docs/installation.qmd b/docs/installation.qmd index 95f15e78e0..3cff0bd32f 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -22,6 +22,7 @@ This guide covers all the ways you can install and set up Axolotl for your envir ### PyPI Installation (Recommended) {#sec-pypi} ```{.bash} +pip3 install -U packaging setuptools wheel ninja pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] ``` @@ -37,7 +38,7 @@ For the latest features between releases: ```{.bash} git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -pip3 install packaging ninja +pip3 install -U packaging setuptools wheel ninja pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' ``` @@ -107,7 +108,7 @@ We recommend using WSL2 (Windows Subsystem for Linux) or Docker. 2. Install PyTorch: https://pytorch.org/get-started/locally/ 3. Install Axolotl: ```{.bash} - pip3 install packaging + pip3 install -U packaging setuptools wheel ninja pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' ``` 4. (Optional) Login to Hugging Face: From 4d92a68a9682a9e1e24635136ae9fe8e15cc0492 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 19 Mar 2025 23:58:33 -0400 Subject: [PATCH 0473/1405] use default torch fused adamw optimizer as default as adamw_hf is deprecated (#2425) * use default torch fused adamw optimizer as default as adamw_hf is deprecated * make sure to have latest packaging installed * bump packagingin requirements.txt too --- pyproject.toml | 2 +- requirements.txt | 2 +- src/axolotl/utils/config/models/input/v0_4_1/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 016dbe4171..4a18516b0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=64", "wheel", "setuptools_scm>=8"] +requires = ["setuptools>=64", "wheel", "setuptools_scm>=8", "packaging>=24.2"] build-backend = "setuptools.build_meta" [project] diff --git a/requirements.txt b/requirements.txt index c3a9eb2f38..ce2fac6e09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ autoawq==0.2.7.post3 liger-kernel==0.5.3 # END section -packaging==23.2 +packaging==24.2 peft==0.14.0 transformers==4.49.0 diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index b2584cfcd5..49f26ac0c5 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -507,7 +507,7 @@ class HyperparametersConfig(BaseModel): weight_decay: Optional[float] = 0.0 optimizer: Optional[ Union[OptimizerNames, CustomSupportedOptimizers] - ] = OptimizerNames.ADAMW_HF + ] = OptimizerNames.ADAMW_TORCH_FUSED optim_args: Optional[Union[str, Dict[str, Any]]] = Field( default=None, json_schema_extra={"description": "Optional arguments to supply to optimizer."}, From 38df5a36ea7c40787c95886d01fafe117dfadc62 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 20 Mar 2025 10:22:05 -0400 Subject: [PATCH 0474/1405] bump HF versions except for trl (#2427) --- .pre-commit-config.yaml | 4 ++-- cicd/Dockerfile.jinja | 1 + docker/Dockerfile-base | 2 +- pyproject.toml | 1 + requirements.txt | 8 ++++---- scripts/cutcrossentropy_install.py | 6 +++--- src/axolotl/utils/data/sft.py | 2 ++ tests/conftest.py | 6 ++++++ tests/e2e/solo/test_relora_llama.py | 8 ++++---- tests/test_datasets.py | 10 +++++----- 10 files changed, 29 insertions(+), 19 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f91b9554d7..95a6e99a02 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,8 +22,8 @@ repos: rev: 6.1.0 hooks: - id: flake8 -- repo: https://github.com/PyCQA/pylint - rev: v3.3.0 +- repo: https://github.com/pylint-dev/pylint + rev: c8c96d20cde3552a79858c7456bb1483bf83d633 hooks: - id: pylint - repo: https://github.com/pre-commit/mirrors-mypy diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index a90016ee43..0fb9e90ba1 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -31,6 +31,7 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt; \ fi +RUN pip3 install -U packaging setuptools wheel RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 4b24bfc3ae..0146fd9fe5 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -28,7 +28,7 @@ ENV PATH="/root/miniconda3/envs/py${PYTHON_VERSION}/bin:${PATH}" WORKDIR /workspace -RUN python3 -m pip install --upgrade pip && pip3 install packaging && \ +RUN python3 -m pip install --upgrade pip && pip3 install -U packaging setuptools wheel && \ python3 -m pip install --no-cache-dir -U torch==${PYTORCH_VERSION}+cu${CUDA} --extra-index-url https://download.pytorch.org/whl/cu$CUDA && \ python3 -m pip install --no-cache-dir "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" && \ python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" diff --git a/pyproject.toml b/pyproject.toml index 4a18516b0b..e3c6cae69d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ dynamic = ["version", "dependencies", "optional-dependencies"] description = "LLM Trainer" readme = "README.md" requires-python = ">=3.10" +license-files = ["LICENSE"] [project.scripts] axolotl = "axolotl.cli.main:main" diff --git a/requirements.txt b/requirements.txt index ce2fac6e09..ebefc7ad41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,11 +12,11 @@ liger-kernel==0.5.3 packaging==24.2 -peft==0.14.0 +peft==0.15.0 transformers==4.49.0 -tokenizers>=0.21.0 -accelerate==1.3.0 -datasets==3.2.0 +tokenizers>=0.21.1 +accelerate==1.5.2 +datasets==3.4.1 deepspeed==0.16.1 trl==0.15.1 diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 71edc8d0c1..87f87e5750 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -17,12 +17,12 @@ cce_spec = importlib.util.find_spec("cut_cross_entropy") -UNINSTALL_PREFIX = "" +uninstall_prefix = "" if cce_spec: if not importlib.util.find_spec("cut_cross_entropy.transformers"): - UNINSTALL_PREFIX = "pip uninstall -y cut-cross-entropy && " + uninstall_prefix = "pip uninstall -y cut-cross-entropy && " print( - UNINSTALL_PREFIX + uninstall_prefix + 'pip install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@24fbe4b5dab9a6c250a014573613c1890190536c"' ) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 704c020219..726ec48582 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -2,6 +2,7 @@ import functools import logging +import os from pathlib import Path from typing import List, Optional, Tuple, Union @@ -344,6 +345,7 @@ def gen_from_iter_ds(_ds, _=None): ) ds_from_iter.save_to_disk(str(prepared_ds_path)) else: + os.makedirs(prepared_ds_path, exist_ok=True) dataset.save_to_disk(str(prepared_ds_path)) if cfg.push_dataset_to_hub: LOG.info( diff --git a/tests/conftest.py b/tests/conftest.py index 85e2767223..2505e6de91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -108,6 +108,12 @@ def download_arcee_ai_distilabel_intel_orca_dpo_pairs_dataset(): ) +@pytest.fixture(scope="session", autouse=True) +def download_tiny_shakespeare_dataset(): + # download the dataset + snapshot_download_w_retry("Trelis/tiny-shakespeare", repo_type="dataset") + + @pytest.fixture def temp_dir(): # Create a temporary directory diff --git a/tests/e2e/solo/test_relora_llama.py b/tests/e2e/solo/test_relora_llama.py index 53a239c517..504466b90c 100644 --- a/tests/e2e/solo/test_relora_llama.py +++ b/tests/e2e/solo/test_relora_llama.py @@ -40,8 +40,8 @@ def test_relora(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_modules": ["q_proj", "v_proj"], - "relora_steps": 100, - "relora_warmup_steps": 20, + "relora_steps": 50, + "relora_warmup_steps": 10, "relora_anneal_steps": 10, "relora_prune_ratio": 0.9, "relora_cpu_offload": True, @@ -60,9 +60,9 @@ def test_relora(self, temp_dir): "message_field_content": "value", }, ], - "warmup_steps": 20, + "warmup_steps": 10, "num_epochs": 2, - "max_steps": 205, # at least 2x relora_steps + "max_steps": 105, # at least 2x relora_steps "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, diff --git a/tests/test_datasets.py b/tests/test_datasets.py index 49554d3700..77c50d5586 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -7,13 +7,13 @@ import unittest from pathlib import Path +from conftest import snapshot_download_w_retry from constants import ( ALPACA_MESSAGES_CONFIG_OG, ALPACA_MESSAGES_CONFIG_REVISION, SPECIAL_TOKENS, ) from datasets import Dataset -from huggingface_hub import snapshot_download from transformers import AutoTokenizer from axolotl.utils.data import load_tokenized_prepared_datasets @@ -69,7 +69,7 @@ def test_load_local_hub(self): with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_path = Path(tmp_dir) / "mhenrichsen/alpaca_2k_test" tmp_ds_path.mkdir(parents=True, exist_ok=True) - snapshot_download( + snapshot_download_w_retry( repo_id="mhenrichsen/alpaca_2k_test", repo_type="dataset", local_dir=tmp_ds_path, @@ -81,7 +81,7 @@ def test_load_local_hub(self): # how to load it. cfg = DictDefault( { - "tokenizer_config": "huggyllama/llama-7b", + "tokenizer_config": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 1024, "datasets": [ { @@ -339,7 +339,7 @@ def test_load_local_hub_with_revision(self): with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_path = Path(tmp_dir) / "mhenrichsen/alpaca_2k_test" tmp_ds_path.mkdir(parents=True, exist_ok=True) - snapshot_download( + snapshot_download_w_retry( repo_id="mhenrichsen/alpaca_2k_test", repo_type="dataset", local_dir=tmp_ds_path, @@ -381,7 +381,7 @@ def test_loading_local_dataset_folder(self): with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_path = Path(tmp_dir) / "mhenrichsen/alpaca_2k_test" tmp_ds_path.mkdir(parents=True, exist_ok=True) - snapshot_download( + snapshot_download_w_retry( repo_id="mhenrichsen/alpaca_2k_test", repo_type="dataset", local_dir=tmp_ds_path, From aae4337f4066be8c74319c337858ee8f4e4334a5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 21 Mar 2025 10:17:25 -0400 Subject: [PATCH 0475/1405] add 12.8.1 cuda to the base matrix (#2426) * add 12.8.1 cuda to the base matrix * use nightly * bump deepspeed and set no binary * deepspeed binary fixes hopefully * install deepspeed by itself * multiline fix * make sure ninja is installed * try with reversion of packaging/setuptools/wheel install * use license instead of license-file * try rolling back packaging and setuptools versions * comment out license for validation for now * make sure packaging version is consistent * more parity across tests and docker images for packaging/setuptools --- .github/workflows/base.yml | 8 +++++- .github/workflows/pypi.yml | 2 +- .github/workflows/tests-nightly.yml | 4 +-- .github/workflows/tests.yml | 4 +-- README.md | 2 +- cicd/Dockerfile.jinja | 2 +- docker/Dockerfile-base | 2 +- docker/Dockerfile-base-nightly | 39 +++++++++++++++++++++++++++++ pyproject.toml | 4 +-- requirements.txt | 6 ++--- setup.py | 2 +- 11 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 docker/Dockerfile-base-nightly diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index e45436e930..cf5c1d45d7 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -40,6 +40,12 @@ jobs: python_version: "3.11" pytorch: 2.6.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + - cuda: "128" + cuda_version: 12.8.1 + cudnn_version: "" + python_version: "3.11" + pytorch: nightly + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" steps: - name: Checkout uses: actions/checkout@v4 @@ -61,7 +67,7 @@ jobs: uses: docker/build-push-action@v4 with: context: . - file: ./docker/Dockerfile-base + file: ${{ matrix.pytorch == 'nightly' && './docker/Dockerfile-base-nightly' || './docker/Dockerfile-base' }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.metadata.outputs.tags }}-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} labels: ${{ steps.metadata.outputs.labels }} diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index c64afc0c91..24e3c497d7 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -40,7 +40,7 @@ jobs: - name: Install dependencies run: | - pip3 install wheel packaging + pip3 install wheel packaging==23.2 pip3 install --no-build-isolation -e . pip3 install -r requirements-dev.txt -r requirements-tests.txt diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 2733b8605e..efad7cc379 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -42,7 +42,7 @@ jobs: - name: upgrade pip run: | pip3 install --upgrade pip - pip3 install --upgrade packaging setuptools wheel + pip3 install --upgrade packaging==23.2 setuptools==75.8.0 wheel - name: Install PyTorch run: | @@ -59,7 +59,7 @@ jobs: - name: Install dependencies run: | pip3 install --upgrade pip - pip3 install --upgrade packaging + pip3 install --upgrade packaging==23.2 pip3 install --no-build-isolation -U -e . python scripts/unsloth_install.py | sh python scripts/cutcrossentropy_install.py | sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8893390054..32bb428211 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -74,7 +74,7 @@ jobs: - name: upgrade pip run: | pip3 install --upgrade pip - pip3 install --upgrade packaging setuptools wheel + pip3 install --upgrade packaging==23.2 setuptools==75.8.0 wheel - name: Install PyTorch run: | @@ -147,7 +147,7 @@ jobs: - name: upgrade pip run: | pip3 install --upgrade pip - pip3 install --upgrade packaging setuptools setuptools_scm build wheel + pip3 install --upgrade packaging==23.2 setuptools==75.8.0 setuptools_scm build wheel - name: Install PyTorch run: | diff --git a/README.md b/README.md index 953bc0be5a..343816aff4 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Features: ### Installation ```bash -pip3 install -U packaging setuptools wheel ninja +pip3 install -U packaging==23.2 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] # Download example axolotl configs, deepspeed configs diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 0fb9e90ba1..b212a00657 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -31,7 +31,7 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt; \ fi -RUN pip3 install -U packaging setuptools wheel +RUN pip install packaging==23.2 setuptools==75.8.0 RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 0146fd9fe5..e989152ec5 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -28,7 +28,7 @@ ENV PATH="/root/miniconda3/envs/py${PYTHON_VERSION}/bin:${PATH}" WORKDIR /workspace -RUN python3 -m pip install --upgrade pip && pip3 install -U packaging setuptools wheel && \ +RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==23.2 setuptools==75.8.0 wheel && \ python3 -m pip install --no-cache-dir -U torch==${PYTORCH_VERSION}+cu${CUDA} --extra-index-url https://download.pytorch.org/whl/cu$CUDA && \ python3 -m pip install --no-cache-dir "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" && \ python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" diff --git a/docker/Dockerfile-base-nightly b/docker/Dockerfile-base-nightly new file mode 100644 index 0000000000..85805ea415 --- /dev/null +++ b/docker/Dockerfile-base-nightly @@ -0,0 +1,39 @@ +ARG CUDA_VERSION="12.8.1" +ARG CUDNN_VERSION="8" +ARG UBUNTU_VERSION="22.04" +ARG MAX_JOBS=4 + +FROM nvidia/cuda:$CUDA_VERSION-cudnn$CUDNN_VERSION-devel-ubuntu$UBUNTU_VERSION AS base-builder + +ENV PATH="/root/miniconda3/bin:${PATH}" + +ARG PYTHON_VERSION="3.11" +ARG PYTORCH_VERSION="nightly" +ARG CUDA="128" +ARG TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6 9.0+PTX" + +ENV PYTHON_VERSION=$PYTHON_VERSION +ENV TORCH_CUDA_ARCH_LIST=$TORCH_CUDA_ARCH_LIST + +RUN apt-get update \ + && apt-get install -y wget git build-essential ninja-build git-lfs libaio-dev pkg-config && rm -rf /var/lib/apt/lists/* \ + && wget \ + https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \ + && mkdir /root/.conda \ + && bash Miniconda3-latest-Linux-x86_64.sh -b \ + && rm -f Miniconda3-latest-Linux-x86_64.sh \ + && conda create -n "py${PYTHON_VERSION}" python="${PYTHON_VERSION}" + +ENV PATH="/root/miniconda3/envs/py${PYTHON_VERSION}/bin:${PATH}" + +WORKDIR /workspace + +RUN python3 -m pip install --upgrade pip && pip3 install packaging && \ + python3 -m pip install --no-cache-dir -U torch --extra-index-url https://download.pytorch.org/whl/nightly/cu$CUDA && \ + python3 -m pip install --no-cache-dir "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" && \ + python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" + +RUN git lfs install --skip-repo && \ + pip3 install awscli && \ + # The base image ships with `pydantic==1.8.2` which is not working + pip3 install -U --no-cache-dir pydantic==1.10.10 diff --git a/pyproject.toml b/pyproject.toml index e3c6cae69d..eb85691dd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=64", "wheel", "setuptools_scm>=8", "packaging>=24.2"] +requires = ["setuptools>=64", "wheel", "setuptools_scm>=8", "packaging==23.2"] build-backend = "setuptools.build_meta" [project] @@ -8,7 +8,7 @@ dynamic = ["version", "dependencies", "optional-dependencies"] description = "LLM Trainer" readme = "README.md" requires-python = ">=3.10" -license-files = ["LICENSE"] +# license = "Apache-2.0" [project.scripts] axolotl = "axolotl.cli.main:main" diff --git a/requirements.txt b/requirements.txt index ebefc7ad41..495f43af62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ # START section of dependencies that don't install on Darwin/MacOS -bitsandbytes==0.45.2 +bitsandbytes==0.45.3 triton>=3.0.0 mamba-ssm==1.2.0.post1 flash-attn==2.7.4.post1 @@ -10,14 +10,14 @@ autoawq==0.2.7.post3 liger-kernel==0.5.3 # END section -packaging==24.2 +packaging==23.2 peft==0.15.0 transformers==4.49.0 tokenizers>=0.21.1 accelerate==1.5.2 datasets==3.4.1 -deepspeed==0.16.1 +deepspeed==0.16.4 trl==0.15.1 optimum==1.16.2 diff --git a/setup.py b/setup.py index b84c34525c..c4ffcdaebf 100644 --- a/setup.py +++ b/setup.py @@ -128,7 +128,7 @@ def get_package_version(): "flash-attn==2.7.4.post1", ], "deepspeed": [ - "deepspeed==0.16.1", + "deepspeed==0.16.4", "deepspeed-kernels", ], "mamba-ssm": [ From 8e604848a434904f97372cd71ade08f5c4d1bc25 Mon Sep 17 00:00:00 2001 From: hugo Date: Fri, 21 Mar 2025 22:17:47 +0800 Subject: [PATCH 0476/1405] add run on novita ai (#2421) [skip ci] * add run on novita ai * Revert "add run on novita ai" This reverts commit 4d5df1ac6bd5b999130310a212031c78e213a606. * add run axolotl on novita ai --- docs/installation.qmd | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/installation.qmd b/docs/installation.qmd index 3cff0bd32f..6edec4610c 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -79,6 +79,7 @@ For providers supporting Docker: - [Latitude.sh](https://latitude.sh/blueprint/989e0e79-3bf6-41ea-a46b-1f246e309d5c) - [JarvisLabs.ai](https://jarvislabs.ai/templates/axolotl) - [RunPod](https://runpod.io/gsc?template=v2ickqhz9s&ref=6i7fkpdz) + - [Novita](https://novita.ai/gpus-console?templateId=311) ### Google Colab {#sec-colab} From f8de8bb4f25f8b225db13949919da484d371a39e Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 21 Mar 2025 21:18:01 +0700 Subject: [PATCH 0477/1405] chore(doc): add instructions on adding custom integrations (#2422) [skip ci] * chore(doc): add instructions on adding custom integrations * chore: add warning help * feat: add note about integration path * fix: adjust text per suggestion --- docs/config.qmd | 6 +++++ docs/custom_integrations.qmd | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/docs/config.qmd b/docs/config.qmd index 38ec368a18..ea7ea22939 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -85,6 +85,12 @@ gpu_memory_limit: 20GiB # Do the LoRA/PEFT loading on CPU -- this is required if the base model is so large it takes up most or all of the available GPU VRAM, e.g. during a model and LoRA merge lora_on_cpu: true +# List[str]. Add plugins to extend the pipeline. +# See `src/axolotl/integrations` for the available plugins or doc below for more details. +# https://axolotl-ai-cloud.github.io/axolotl/docs/custom_integrations.html +plugins: + # - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + # A list of one or more datasets to finetune the model with datasets: # HuggingFace dataset repo | s3://,gs:// path | "json" for local dataset, make sure to fill data_files diff --git a/docs/custom_integrations.qmd b/docs/custom_integrations.qmd index 8d04982986..cb4aef9ca2 100644 --- a/docs/custom_integrations.qmd +++ b/docs/custom_integrations.qmd @@ -55,3 +55,47 @@ sections = [ for section_name, folder_name in sections: print(print_section(section_name, folder_name)) ``` + +## Adding a new integration + +Plugins can be used to customize the behavior of the training pipeline through [hooks](https://en.wikipedia.org/wiki/Hooking). See [`axolotl.integrations.BasePlugin`](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/integrations/base.py) for the possible hooks. + +To add a new integration, please follow these steps: + +1. Create a new folder in the `src/axolotl/integrations` directory. +2. Add any relevant files (`LICENSE`, `README.md`, `ACKNOWLEDGEMENTS.md`, etc.) to the new folder. +3. Add `__init__.py` and `args.py` files to the new folder. + - `__init__.py` should import the integration and hook into the appropriate functions. + - `args.py` should define the arguments for the integration. +4. (If applicable) Add CPU tests under `tests/integrations` or GPU tests under `tests/e2e/integrations`. + +::: {.callout-tip} + +See [src/axolotl/integrations/cut_cross_entropy](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations/cut_cross_entropy) for a minimal integration example. + +::: + +::: {.callout-warning} + +If you could not load your integration, please ensure you are pip installing in editable mode. + +```bash +pip install -e . +``` + +and correctly spelled the integration name in the config file. + +```yaml +plugins: + - axolotl.integrations.your_integration_name.YourIntegrationPlugin +``` + +::: + +::: {.callout-note} + +It is not necessary to place your integration in the `integrations` folder. It can be in any location, so long as it's installed in a package in your python env. + +See this repo for an example: [https://github.com/axolotl-ai-cloud/diff-transformer](https://github.com/axolotl-ai-cloud/diff-transformer) + +::: From 187227d8371b8a656145c9c6b92884a842a59f94 Mon Sep 17 00:00:00 2001 From: salman Date: Fri, 21 Mar 2025 14:18:28 +0000 Subject: [PATCH 0478/1405] Fixing KTO+QLoRA+multi-GPU (#2420) * WIP * removing artifacts * adding error * adding adapter check * linting * simplifying check * linting v2 * config fix -___- --- examples/llama-3/qlora-1b-kto.yaml | 2 +- .../config/models/input/v0_4_1/__init__.py | 33 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/examples/llama-3/qlora-1b-kto.yaml b/examples/llama-3/qlora-1b-kto.yaml index df4a084897..32168dc371 100644 --- a/examples/llama-3/qlora-1b-kto.yaml +++ b/examples/llama-3/qlora-1b-kto.yaml @@ -55,7 +55,7 @@ tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: - use_reentrant: true + use_reentrant: false early_stopping_patience: resume_from_checkpoint: local_rank: diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 49f26ac0c5..2fa86eced8 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -1679,6 +1679,30 @@ def check_npu_config(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_rl_config_gradient_checkpointing(cls, data): + # TODO: SalmanMohammadi + # Distributed RL with QLoRA + gradient checkpointing + # and use_reentrant = True is broken upstream in TRL + # pylint: disable=too-many-boolean-expressions + if ( + data.get("rl") + and data.get("gradient_checkpointing") + and data.get("gradient_checkpointing_kwargs") + and data.get("gradient_checkpointing_kwargs").get("use_reentrant") + and data.get("load_in_4bit") + and data.get("adapter") == "qlora" + and data.get("capabilities") + and data.get("capabilities").get("n_gpu", 1) > 1 + ): + raise ValueError( + "The `use_reentrant: True` implementation of gradient checkpointing " + "is not supported for distributed RL training with QLoRA. Please set " + "`use_reentrant: False` in `gradient_checkpointing_kwargs`." + ) + return data + @model_validator(mode="before") @classmethod def check_kto_config(cls, data): @@ -1689,15 +1713,6 @@ def check_kto_config(cls, data): if data.get("remove_unused_columns") is not False: raise ValueError("Set `remove_unused_columns: False` when using kto") - if data.get("gradient_checkpointing") and not ( - data.get("gradient_checkpointing_kwargs") - and isinstance(data.get("gradient_checkpointing_kwargs"), dict) - and data["gradient_checkpointing_kwargs"].get("use_reentrant") - ): - raise ValueError( - "Set `gradient_checkpointing_kwargs: {use_reentrant: true}` for when kto is enabled" - ) - return data From c907ac173e586e759da9c71aa2a51e294648d1f0 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 21 Mar 2025 11:02:43 -0400 Subject: [PATCH 0479/1405] adding pre-commit auto-update GH action and bumping plugin versions (#2428) * adding pre-commit auto-update GH action and bumping plugin versions * running updated pre-commit plugins * sorry to revert, but pylint complained * Update .pre-commit-config.yaml Co-authored-by: Wing Lian --------- Co-authored-by: Dan Saunders Co-authored-by: Wing Lian --- .github/workflows/precommit-autoupdate.yml | 49 ++++ .pre-commit-config.yaml | 14 +- cicd/multigpu.py | 5 +- cicd/tests.py | 1 + scripts/chat_datasets.py | 1 + scripts/cutcrossentropy_install.py | 7 +- src/axolotl/cli/cloud/__init__.py | 1 + src/axolotl/cli/cloud/base.py | 1 + src/axolotl/cli/cloud/modal_.py | 1 + src/axolotl/cli/main.py | 1 + src/axolotl/cli/utils.py | 3 +- src/axolotl/convert.py | 1 - src/axolotl/core/chat/format/chatml.py | 1 + src/axolotl/core/chat/format/llama3x.py | 1 + src/axolotl/core/chat/format/shared.py | 1 + src/axolotl/core/chat/messages.py | 1 + src/axolotl/core/datasets/chat.py | 1 + .../core/datasets/transforms/chat_builder.py | 1 + src/axolotl/core/trainer_builder.py | 252 +++++++++--------- src/axolotl/core/trainers/base.py | 12 +- src/axolotl/core/trainers/dpo/__init__.py | 1 + src/axolotl/core/trainers/dpo/args.py | 1 + src/axolotl/core/trainers/dpo/trainer.py | 1 + src/axolotl/core/trainers/grpo/__init__.py | 12 +- src/axolotl/core/trainers/grpo/args.py | 1 + src/axolotl/core/trainers/grpo/trainer.py | 1 + src/axolotl/core/trainers/trl.py | 1 + src/axolotl/core/training_args.py | 1 + src/axolotl/integrations/grokfast/__init__.py | 1 + src/axolotl/integrations/grokfast/args.py | 1 + src/axolotl/integrations/kd/args.py | 12 +- src/axolotl/integrations/liger/__init__.py | 6 +- .../integrations/liger/models/deepseekv2.py | 1 + .../integrations/liger/models/jamba.py | 1 + src/axolotl/integrations/lm_eval/__init__.py | 1 + src/axolotl/integrations/lm_eval/args.py | 1 + src/axolotl/integrations/lm_eval/cli.py | 1 + src/axolotl/kernels/geglu.py | 1 + src/axolotl/kernels/lora.py | 1 + src/axolotl/kernels/quantize.py | 1 + src/axolotl/kernels/swiglu.py | 1 + .../models/mamba/configuration_mamba.py | 1 + src/axolotl/monkeypatch/attention/mllama.py | 13 +- .../monkeypatch/data/batch_dataset_fetcher.py | 1 + .../monkeypatch/llama_attn_hijack_flash.py | 20 +- src/axolotl/monkeypatch/llama_expand_mask.py | 1 + .../monkeypatch/mistral_attn_hijack_flash.py | 22 +- src/axolotl/monkeypatch/mixtral/__init__.py | 1 + src/axolotl/monkeypatch/relora.py | 6 +- .../monkeypatch/stablelm_attn_hijack_flash.py | 2 +- src/axolotl/monkeypatch/trainer_fsdp_optim.py | 1 + src/axolotl/monkeypatch/utils.py | 1 + src/axolotl/monkeypatch/xformers_/__init__.py | 1 + .../prompt_strategies/alpaca_w_system.py | 1 + src/axolotl/prompt_strategies/completion.py | 1 + src/axolotl/prompt_strategies/context_qa.py | 1 + src/axolotl/prompt_strategies/dpo/__init__.py | 1 + src/axolotl/prompt_strategies/dpo/chatml.py | 36 +-- src/axolotl/prompt_strategies/dpo/llama3.py | 36 +-- src/axolotl/prompt_strategies/input_output.py | 1 + .../jinja_template_analyzer.py | 1 + src/axolotl/prompt_strategies/kto/chatml.py | 31 +-- src/axolotl/prompt_strategies/kto/llama3.py | 31 +-- .../prompt_strategies/kto/user_defined.py | 1 + .../prompt_strategies/messages/chat.py | 1 + src/axolotl/prompt_strategies/orcamini.py | 1 + .../prompt_strategies/orpo/chat_template.py | 1 + src/axolotl/prompt_strategies/pretrain.py | 1 + src/axolotl/train.py | 4 +- src/axolotl/utils/__init__.py | 6 +- src/axolotl/utils/bench.py | 1 + src/axolotl/utils/callbacks/__init__.py | 6 +- src/axolotl/utils/callbacks/mlflow_.py | 1 + src/axolotl/utils/callbacks/perplexity.py | 1 + src/axolotl/utils/callbacks/profiler.py | 1 + src/axolotl/utils/chat_templates.py | 1 + src/axolotl/utils/collators/__init__.py | 1 + src/axolotl/utils/collators/core.py | 1 + src/axolotl/utils/collators/mamba.py | 1 + src/axolotl/utils/config/__init__.py | 6 +- .../config/models/input/v0_4_1/__init__.py | 36 +-- .../utils/config/models/internals/__init__.py | 1 + src/axolotl/utils/data/__init__.py | 1 + src/axolotl/utils/distributed.py | 1 + src/axolotl/utils/environment.py | 5 +- src/axolotl/utils/freeze.py | 1 + .../utils/gradient_checkpointing/__init__.py | 1 + src/axolotl/utils/model_shard_quant.py | 15 +- src/axolotl/utils/models.py | 6 +- src/axolotl/utils/optimizers/adopt.py | 1 + src/axolotl/utils/samplers/__init__.py | 1 + src/axolotl/utils/samplers/utils.py | 1 + src/axolotl/utils/schedulers.py | 1 + src/axolotl/utils/trainer.py | 6 +- ...setuptools_axolotl_dynamic_dependencies.py | 1 + .../test_cli_merge_sharded_fsdp_weights.py | 1 + tests/cli/test_cli_sweeps.py | 1 + tests/cli/test_utils.py | 1 + tests/conftest.py | 1 + tests/core/chat/test_messages.py | 1 + tests/e2e/integrations/test_kd.py | 1 + tests/e2e/kernels/test_geglu.py | 1 + tests/e2e/kernels/test_lora.py | 1 + tests/e2e/kernels/test_quantize.py | 1 + tests/e2e/kernels/test_swiglu.py | 1 + tests/e2e/multigpu/test_eval.py | 1 + tests/e2e/multigpu/test_grpo.py | 1 + .../lora_kernels/test_lora_kernel_patching.py | 1 + tests/e2e/patched/test_cli_integrations.py | 1 + tests/e2e/patched/test_unsloth_integration.py | 1 + tests/e2e/patched/test_unsloth_qlora.py | 1 + tests/e2e/test_imports.py | 1 + tests/e2e/utils.py | 1 + tests/integrations/test_liger.py | 1 + .../test_llama_attn_hijack_flash.py | 1 + tests/prompt_strategies/messages/test_chat.py | 1 + tests/prompt_strategies/test_alpaca.py | 1 + .../test_chat_template_utils.py | 1 + .../test_chat_templates_advanced.py | 8 +- tests/prompt_strategies/test_dpo_chatml.py | 1 + .../test_jinja_template_analyzer.py | 1 + tests/prompt_strategies/test_raw_io.py | 1 + tests/test_data.py | 1 + tests/test_dict.py | 1 - tests/test_exact_deduplication.py | 11 +- tests/test_expand_mask.py | 1 + tests/test_lora.py | 1 + tests/test_normalize_config.py | 1 + tests/test_packed_batch_sampler.py | 1 + tests/test_packed_pretraining.py | 1 + tests/test_perplexity.py | 1 + tests/test_schedulers.py | 1 + 132 files changed, 479 insertions(+), 301 deletions(-) create mode 100644 .github/workflows/precommit-autoupdate.yml diff --git a/.github/workflows/precommit-autoupdate.yml b/.github/workflows/precommit-autoupdate.yml new file mode 100644 index 0000000000..921742211c --- /dev/null +++ b/.github/workflows/precommit-autoupdate.yml @@ -0,0 +1,49 @@ +name: Pre-commit auto-update + +on: + schedule: + - cron: '0 0 * * 0' # Run weekly + workflow_dispatch: # Manual kickoff + +jobs: + auto-update: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Update pre-commit hooks + id: update + run: | + pip install pre-commit + pre-commit autoupdate + if [[ -n $(git status --porcelain) ]]; then + echo "changes=true" >> $GITHUB_OUTPUT + git diff .pre-commit-config.yaml > pre-commit-update.diff + fi + + - name: Create Pull Request + if: steps.update.outputs.changes == 'true' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: update/pre-commit-hooks + delete-branch: true + title: "chore: update pre-commit hooks" + commit-message: "chore: update pre-commit hooks" + body: | + Automated PR to update pre-commit hooks to their latest versions. + +
+ Changes: + + ```diff + ${{ steps.update.outputs.diff }} + ``` +
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95a6e99a02..f627ec13f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ default_language_version: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - id: check-yaml - id: end-of-file-fixer @@ -11,23 +11,23 @@ repos: - id: no-commit-to-branch args: ['--branch', 'main'] - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 25.1.0 hooks: - id: black - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 6.0.1 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + rev: 7.1.2 hooks: - id: flake8 - repo: https://github.com/pylint-dev/pylint - rev: c8c96d20cde3552a79858c7456bb1483bf83d633 + rev: v3.3.6 hooks: - id: pylint - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.3.0 + rev: v1.15.0 hooks: - id: mypy additional_dependencies: @@ -36,7 +36,7 @@ repos: 'pydantic>=2.5.3', ] - repo: https://github.com/PyCQA/bandit - rev: 1.7.5 + rev: 1.8.3 hooks: - id: bandit args: [ diff --git a/cicd/multigpu.py b/cicd/multigpu.py index eda589dd12..453e8daee4 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -1,6 +1,7 @@ """ - modal application to run axolotl gpu tests in Modal - """ +modal application to run axolotl gpu tests in Modal +""" + # pylint: disable=duplicate-code import os diff --git a/cicd/tests.py b/cicd/tests.py index 41ae2306fa..d7cdcc4738 100644 --- a/cicd/tests.py +++ b/cicd/tests.py @@ -1,4 +1,5 @@ """Modal app to run axolotl GPU tests""" + # pylint: disable=duplicate-code import os diff --git a/scripts/chat_datasets.py b/scripts/chat_datasets.py index 8ae1e256d3..1a85fcef92 100644 --- a/scripts/chat_datasets.py +++ b/scripts/chat_datasets.py @@ -1,6 +1,7 @@ """ helper script to parse chat datasets into a usable yaml """ + import click import yaml from datasets import load_dataset diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 87f87e5750..396f4a655a 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -1,4 +1,5 @@ """Script to output the correct installation command for cut-cross-entropy.""" + import importlib.util import sys @@ -17,12 +18,12 @@ cce_spec = importlib.util.find_spec("cut_cross_entropy") -uninstall_prefix = "" +UNINSTALL_PREFIX = "" if cce_spec: if not importlib.util.find_spec("cut_cross_entropy.transformers"): - uninstall_prefix = "pip uninstall -y cut-cross-entropy && " + UNINSTALL_PREFIX = "pip uninstall -y cut-cross-entropy && " print( - uninstall_prefix + UNINSTALL_PREFIX + 'pip install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@24fbe4b5dab9a6c250a014573613c1890190536c"' ) diff --git a/src/axolotl/cli/cloud/__init__.py b/src/axolotl/cli/cloud/__init__.py index b879601bed..5d6900d3eb 100644 --- a/src/axolotl/cli/cloud/__init__.py +++ b/src/axolotl/cli/cloud/__init__.py @@ -1,6 +1,7 @@ """ launch axolotl in supported cloud platforms """ + from pathlib import Path from typing import Union diff --git a/src/axolotl/cli/cloud/base.py b/src/axolotl/cli/cloud/base.py index 44d1b0c17f..eba8be49a4 100644 --- a/src/axolotl/cli/cloud/base.py +++ b/src/axolotl/cli/cloud/base.py @@ -1,6 +1,7 @@ """ base class for cloud platforms from cli """ + from abc import ABC, abstractmethod diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index 47bc3221ab..ef59ed3d41 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -1,6 +1,7 @@ """ Modal Cloud support from CLI """ + import copy import json import os diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index 0f132c133c..f46c8efe25 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -1,4 +1,5 @@ """Click CLI definitions for various axolotl commands.""" + # pylint: disable=redefined-outer-name import logging diff --git a/src/axolotl/cli/utils.py b/src/axolotl/cli/utils.py index addfa0ab9c..cb61fa3711 100644 --- a/src/axolotl/cli/utils.py +++ b/src/axolotl/cli/utils.py @@ -5,7 +5,6 @@ import hashlib import json import logging -import typing from functools import wraps from pathlib import Path from types import NoneType @@ -24,7 +23,7 @@ LOG = logging.getLogger(__name__) -def strip_optional_type(field_type: type | typing._SpecialForm | None): +def strip_optional_type(field_type: type | str | None): """ Extracts the non-`None` type from an `Optional` / `Union` type. diff --git a/src/axolotl/convert.py b/src/axolotl/convert.py index 357e0ec50e..d1bdb34db1 100644 --- a/src/axolotl/convert.py +++ b/src/axolotl/convert.py @@ -1,6 +1,5 @@ """Module containing File Reader, File Writer, Json Parser, and Jsonl Serializer classes""" - import json import sys diff --git a/src/axolotl/core/chat/format/chatml.py b/src/axolotl/core/chat/format/chatml.py index 315d101a86..04c398fe81 100644 --- a/src/axolotl/core/chat/format/chatml.py +++ b/src/axolotl/core/chat/format/chatml.py @@ -1,6 +1,7 @@ """ ChatML transformation functions for MessageContents """ + from typing import Optional from ..messages import MessageContents, Messages diff --git a/src/axolotl/core/chat/format/llama3x.py b/src/axolotl/core/chat/format/llama3x.py index 17fa7aa8d4..a0ce053e51 100644 --- a/src/axolotl/core/chat/format/llama3x.py +++ b/src/axolotl/core/chat/format/llama3x.py @@ -1,6 +1,7 @@ """ Llama 3.x chat formatting functions for MessageContents """ + from typing import Optional from ..messages import MessageContents, Messages diff --git a/src/axolotl/core/chat/format/shared.py b/src/axolotl/core/chat/format/shared.py index 9efa2353db..0a0f56f3ad 100644 --- a/src/axolotl/core/chat/format/shared.py +++ b/src/axolotl/core/chat/format/shared.py @@ -1,6 +1,7 @@ """ shared functions for format transforms """ + from axolotl.core.chat.messages import MessageContents, Messages diff --git a/src/axolotl/core/chat/messages.py b/src/axolotl/core/chat/messages.py index c879bf477b..88ff2b7ad0 100644 --- a/src/axolotl/core/chat/messages.py +++ b/src/axolotl/core/chat/messages.py @@ -1,6 +1,7 @@ """ internal message representations of chat messages """ + import json from enum import Enum from typing import Any, Callable, List, Optional, Union diff --git a/src/axolotl/core/datasets/chat.py b/src/axolotl/core/datasets/chat.py index ba257071dc..724f128666 100644 --- a/src/axolotl/core/datasets/chat.py +++ b/src/axolotl/core/datasets/chat.py @@ -1,6 +1,7 @@ """ chat dataset module """ + import os from typing import Callable, Optional, Union diff --git a/src/axolotl/core/datasets/transforms/chat_builder.py b/src/axolotl/core/datasets/transforms/chat_builder.py index 98d5f171a7..692fe3ebb5 100644 --- a/src/axolotl/core/datasets/transforms/chat_builder.py +++ b/src/axolotl/core/datasets/transforms/chat_builder.py @@ -1,6 +1,7 @@ """ This module contains a function that builds a transform that takes a row from the dataset and converts it to a Chat. """ + from typing import Any, Mapping, Union diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 0c92047471..19b947fb2e 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -332,9 +332,9 @@ def build(self, total_num_steps): training_arguments_kwargs = {} if self.cfg.include_tokens_per_second is not None: - training_arguments_kwargs[ - "include_tokens_per_second" - ] = self.cfg.include_tokens_per_second + training_arguments_kwargs["include_tokens_per_second"] = ( + self.cfg.include_tokens_per_second + ) if self.cfg.bf16 == "full": training_arguments_kwargs["bf16_full_eval"] = True @@ -351,13 +351,13 @@ def build(self, total_num_steps): training_arguments_kwargs["seed"] = self.cfg.seed if self.cfg.gradient_checkpointing: - training_arguments_kwargs[ - "gradient_checkpointing" - ] = self.cfg.gradient_checkpointing + training_arguments_kwargs["gradient_checkpointing"] = ( + self.cfg.gradient_checkpointing + ) if self.cfg.gradient_checkpointing_kwargs is not None: - training_arguments_kwargs[ - "gradient_checkpointing_kwargs" - ] = self.cfg.gradient_checkpointing_kwargs + training_arguments_kwargs["gradient_checkpointing_kwargs"] = ( + self.cfg.gradient_checkpointing_kwargs + ) if self.cfg.fsdp: training_arguments_kwargs["fsdp"] = self.cfg.fsdp if self.cfg.fsdp_config: @@ -373,9 +373,9 @@ def build(self, total_num_steps): training_arguments_kwargs["deepspeed"] = self.cfg.deepspeed if self.cfg.lr_quadratic_warmup is not None: - training_arguments_kwargs[ - "lr_quadratic_warmup" - ] = self.cfg.lr_quadratic_warmup + training_arguments_kwargs["lr_quadratic_warmup"] = ( + self.cfg.lr_quadratic_warmup + ) if self.cfg.adam_beta1: training_arguments_kwargs["adam_beta1"] = self.cfg.adam_beta1 @@ -399,28 +399,28 @@ def build(self, total_num_steps): training_arguments_kwargs["save_safetensors"] = self.cfg.save_safetensors if self.cfg.dataloader_pin_memory is not None: - training_arguments_kwargs[ - "dataloader_pin_memory" - ] = self.cfg.dataloader_pin_memory + training_arguments_kwargs["dataloader_pin_memory"] = ( + self.cfg.dataloader_pin_memory + ) if self.cfg.dataloader_num_workers is not None: - training_arguments_kwargs[ - "dataloader_num_workers" - ] = self.cfg.dataloader_num_workers + training_arguments_kwargs["dataloader_num_workers"] = ( + self.cfg.dataloader_num_workers + ) if self.cfg.dataloader_prefetch_factor is not None: - training_arguments_kwargs[ - "dataloader_prefetch_factor" - ] = self.cfg.dataloader_prefetch_factor + training_arguments_kwargs["dataloader_prefetch_factor"] = ( + self.cfg.dataloader_prefetch_factor + ) if self.cfg.dataloader_drop_last is not None: - training_arguments_kwargs[ - "dataloader_drop_last" - ] = self.cfg.dataloader_drop_last + training_arguments_kwargs["dataloader_drop_last"] = ( + self.cfg.dataloader_drop_last + ) elif self.cfg.sample_packing and self.cfg.eval_sample_packing is False: training_arguments_kwargs["dataloader_drop_last"] = True if self.cfg.remove_unused_columns is not None: - training_arguments_kwargs[ - "remove_unused_columns" - ] = self.cfg.remove_unused_columns + training_arguments_kwargs["remove_unused_columns"] = ( + self.cfg.remove_unused_columns + ) if not self.cfg.test_datasets and self.cfg.val_set_size == 0: # no eval set, so don't eval @@ -452,9 +452,9 @@ def build(self, total_num_steps): if self.cfg.do_causal_lm_eval: training_arguments_kwargs["do_causal_lm_eval"] = self.cfg.do_causal_lm_eval if self.cfg.metric_for_best_model: - training_arguments_kwargs[ - "metric_for_best_model" - ] = self.cfg.metric_for_best_model + training_arguments_kwargs["metric_for_best_model"] = ( + self.cfg.metric_for_best_model + ) if self.cfg.greater_is_better: training_arguments_kwargs["greater_is_better"] = self.cfg.greater_is_better @@ -467,13 +467,13 @@ def build(self, total_num_steps): ) training_arguments_kwargs["torch_compile"] = self.cfg.torch_compile if self.cfg.torch_compile_backend: - training_arguments_kwargs[ - "torch_compile_backend" - ] = self.cfg.torch_compile_backend + training_arguments_kwargs["torch_compile_backend"] = ( + self.cfg.torch_compile_backend + ) if self.cfg.torch_compile_mode: - training_arguments_kwargs[ - "torch_compile_mode" - ] = self.cfg.torch_compile_mode + training_arguments_kwargs["torch_compile_mode"] = ( + self.cfg.torch_compile_mode + ) # DDP Config if self.cfg.ddp_timeout: @@ -482,32 +482,32 @@ def build(self, total_num_steps): if self.cfg.ddp_bucket_cap_mb: training_arguments_kwargs["ddp_bucket_cap_mb"] = self.cfg.ddp_bucket_cap_mb if self.cfg.ddp_broadcast_buffers is not None: - training_arguments_kwargs[ - "ddp_broadcast_buffers" - ] = self.cfg.ddp_broadcast_buffers + training_arguments_kwargs["ddp_broadcast_buffers"] = ( + self.cfg.ddp_broadcast_buffers + ) # these are all the "standard" kwargs that are def used training_arguments_kwargs["max_steps"] = ( total_num_steps if self.cfg.max_steps else -1 ) training_arguments_kwargs["max_seq_length"] = self.cfg.sequence_len - training_arguments_kwargs[ - "per_device_train_batch_size" - ] = self.cfg.micro_batch_size + training_arguments_kwargs["per_device_train_batch_size"] = ( + self.cfg.micro_batch_size + ) if self.cfg.eval_batch_size: - training_arguments_kwargs[ - "per_device_eval_batch_size" - ] = self.cfg.eval_batch_size + training_arguments_kwargs["per_device_eval_batch_size"] = ( + self.cfg.eval_batch_size + ) if self.cfg.auto_find_batch_size is not None: - training_arguments_kwargs[ - "auto_find_batch_size" - ] = self.cfg.auto_find_batch_size - training_arguments_kwargs[ - "gradient_accumulation_steps" - ] = self.cfg.gradient_accumulation_steps - training_arguments_kwargs[ - "eval_accumulation_steps" - ] = self.cfg.gradient_accumulation_steps + training_arguments_kwargs["auto_find_batch_size"] = ( + self.cfg.auto_find_batch_size + ) + training_arguments_kwargs["gradient_accumulation_steps"] = ( + self.cfg.gradient_accumulation_steps + ) + training_arguments_kwargs["eval_accumulation_steps"] = ( + self.cfg.gradient_accumulation_steps + ) training_arguments_kwargs["num_train_epochs"] = self.cfg.num_epochs training_arguments_kwargs["learning_rate"] = self.cfg.learning_rate training_arguments_kwargs["output_dir"] = self.cfg.output_dir @@ -554,9 +554,9 @@ def build(self, total_num_steps): if self.cfg.lr_scheduler in ["one_cycle", "rex", "log_sweep"]: training_arguments_kwargs["lr_scheduler_type"] = "cosine" - training_arguments_kwargs[ - "alternate_lr_scheduler_type" - ] = self.cfg.lr_scheduler + training_arguments_kwargs["alternate_lr_scheduler_type"] = ( + self.cfg.lr_scheduler + ) else: training_arguments_kwargs["lr_scheduler_type"] = ( self.cfg.lr_scheduler if self.cfg.lr_scheduler else "cosine" @@ -565,9 +565,9 @@ def build(self, total_num_steps): self.cfg.lr_scheduler_kwargs if self.cfg.lr_scheduler_kwargs else {} ) training_arguments_kwargs["cosine_min_lr_ratio"] = self.cfg.cosine_min_lr_ratio - training_arguments_kwargs[ - "cosine_constant_lr_ratio" - ] = self.cfg.cosine_constant_lr_ratio + training_arguments_kwargs["cosine_constant_lr_ratio"] = ( + self.cfg.cosine_constant_lr_ratio + ) training_arguments_kwargs["weight_decay"] = ( self.cfg.weight_decay if self.cfg.weight_decay is not None else 0.0 ) @@ -580,40 +580,40 @@ def build(self, total_num_steps): self.cfg.eval_sample_packing ) if self.cfg.sample_packing_bin_size is not None: - training_arguments_kwargs[ - "sample_packing_bin_size" - ] = self.cfg.sample_packing_bin_size + training_arguments_kwargs["sample_packing_bin_size"] = ( + self.cfg.sample_packing_bin_size + ) if self.cfg.sample_packing_group_size is not None: - training_arguments_kwargs[ - "sample_packing_group_size" - ] = self.cfg.sample_packing_group_size + training_arguments_kwargs["sample_packing_group_size"] = ( + self.cfg.sample_packing_group_size + ) if self.cfg.sample_packing_eff_est: - training_arguments_kwargs[ - "sample_packing_efficiency" - ] = self.cfg.sample_packing_eff_est + training_arguments_kwargs["sample_packing_efficiency"] = ( + self.cfg.sample_packing_eff_est + ) if self.cfg.relora_steps: training_arguments_kwargs["relora_steps"] = self.cfg.relora_steps - training_arguments_kwargs[ - "relora_warmup_steps" - ] = self.cfg.relora_warmup_steps + training_arguments_kwargs["relora_warmup_steps"] = ( + self.cfg.relora_warmup_steps + ) if self.cfg.relora_anneal_steps: - training_arguments_kwargs[ - "relora_anneal_steps" - ] = self.cfg.relora_anneal_steps + training_arguments_kwargs["relora_anneal_steps"] = ( + self.cfg.relora_anneal_steps + ) if self.cfg.relora_prune_ratio: - training_arguments_kwargs[ - "relora_prune_ratio" - ] = self.cfg.relora_prune_ratio + training_arguments_kwargs["relora_prune_ratio"] = ( + self.cfg.relora_prune_ratio + ) if self.cfg.lisa_step_interval and self.cfg.lisa_n_layers: training_arguments_kwargs["lisa_n_layers"] = self.cfg.lisa_n_layers - training_arguments_kwargs[ - "lisa_step_interval" - ] = self.cfg.lisa_step_interval - training_arguments_kwargs[ - "lisa_layers_attribute" - ] = self.cfg.lisa_layers_attribute + training_arguments_kwargs["lisa_step_interval"] = ( + self.cfg.lisa_step_interval + ) + training_arguments_kwargs["lisa_layers_attribute"] = ( + self.cfg.lisa_layers_attribute + ) training_arguments_kwargs = self.hook_pre_create_training_args( training_arguments_kwargs @@ -627,9 +627,9 @@ def build(self, total_num_steps): ) if self.cfg.neftune_noise_alpha is not None: - training_arguments_kwargs[ - "neftune_noise_alpha" - ] = self.cfg.neftune_noise_alpha + training_arguments_kwargs["neftune_noise_alpha"] = ( + self.cfg.neftune_noise_alpha + ) trainer_kwargs = {} @@ -731,23 +731,23 @@ def build(self, total_num_steps): importlib.import_module("torchdistx") if self.cfg.optim_target_modules: - training_arguments_kwargs[ - "optim_target_modules" - ] = self.cfg.optim_target_modules + training_arguments_kwargs["optim_target_modules"] = ( + self.cfg.optim_target_modules + ) training_arguments_kwargs["embedding_lr"] = self.cfg.embedding_lr training_arguments_kwargs["embedding_lr_scale"] = self.cfg.embedding_lr_scale training_arguments_kwargs["loraplus_lr_ratio"] = self.cfg.loraplus_lr_ratio - training_arguments_kwargs[ - "loraplus_lr_embedding" - ] = self.cfg.loraplus_lr_embedding + training_arguments_kwargs["loraplus_lr_embedding"] = ( + self.cfg.loraplus_lr_embedding + ) training_arguments_kwargs["lr_groups"] = self.cfg.lr_groups if self.cfg.accelerator_config: - training_arguments_kwargs[ - "accelerator_config" - ] = self.cfg.accelerator_config + training_arguments_kwargs["accelerator_config"] = ( + self.cfg.accelerator_config + ) if self.cfg.kd_ce_alpha is not None: training_arguments_kwargs["kd_ce_alpha"] = self.cfg.kd_ce_alpha @@ -756,13 +756,13 @@ def build(self, total_num_steps): if self.cfg.kd_temperature is not None: training_arguments_kwargs["kd_temperature"] = self.cfg.kd_temperature if self.cfg.kd_zscore_base_temp is not None: - training_arguments_kwargs[ - "kd_zscore_base_temp" - ] = self.cfg.kd_zscore_base_temp + training_arguments_kwargs["kd_zscore_base_temp"] = ( + self.cfg.kd_zscore_base_temp + ) if self.cfg.kd_top_k_before_softmax is not None: - training_arguments_kwargs[ - "kd_top_k_before_softmax" - ] = self.cfg.kd_top_k_before_softmax + training_arguments_kwargs["kd_top_k_before_softmax"] = ( + self.cfg.kd_top_k_before_softmax + ) if self.cfg.reward_model: training_args_cls = AxolotlRewardConfig @@ -972,32 +972,32 @@ def build_training_arguments(self, total_num_steps): self.cfg.lr_scheduler_kwargs if self.cfg.lr_scheduler_kwargs else {} ) if self.cfg.remove_unused_columns is not None: - training_args_kwargs[ - "remove_unused_columns" - ] = self.cfg.remove_unused_columns + training_args_kwargs["remove_unused_columns"] = ( + self.cfg.remove_unused_columns + ) else: training_args_kwargs["remove_unused_columns"] = False if self.cfg.dataloader_pin_memory is not None: - training_args_kwargs[ - "dataloader_pin_memory" - ] = self.cfg.dataloader_pin_memory + training_args_kwargs["dataloader_pin_memory"] = ( + self.cfg.dataloader_pin_memory + ) if self.cfg.dataloader_num_workers is not None: - training_args_kwargs[ - "dataloader_num_workers" - ] = self.cfg.dataloader_num_workers + training_args_kwargs["dataloader_num_workers"] = ( + self.cfg.dataloader_num_workers + ) if self.cfg.dataloader_prefetch_factor is not None: - training_args_kwargs[ - "dataloader_prefetch_factor" - ] = self.cfg.dataloader_prefetch_factor + training_args_kwargs["dataloader_prefetch_factor"] = ( + self.cfg.dataloader_prefetch_factor + ) if self.cfg.gradient_checkpointing: - training_args_kwargs[ - "gradient_checkpointing" - ] = self.cfg.gradient_checkpointing + training_args_kwargs["gradient_checkpointing"] = ( + self.cfg.gradient_checkpointing + ) if self.cfg.gradient_checkpointing_kwargs is not None: - training_args_kwargs[ - "gradient_checkpointing_kwargs" - ] = self.cfg.gradient_checkpointing_kwargs + training_args_kwargs["gradient_checkpointing_kwargs"] = ( + self.cfg.gradient_checkpointing_kwargs + ) else: training_args_kwargs["gradient_checkpointing_kwargs"] = { "use_reentrant": False @@ -1071,9 +1071,9 @@ def build_training_arguments(self, total_num_steps): if self.cfg.dpo_use_weighting is not None: training_args_kwargs["use_weighting"] = self.cfg.dpo_use_weighting if self.cfg.dpo_use_logits_to_keep is not None: - training_args_kwargs[ - "use_logits_to_keep" - ] = self.cfg.dpo_use_logits_to_keep + training_args_kwargs["use_logits_to_keep"] = ( + self.cfg.dpo_use_logits_to_keep + ) for blocklist_key in blocklist_args_kwargs: if blocklist_key in training_args_kwargs: @@ -1108,9 +1108,9 @@ def build(self, total_num_steps): if self.cfg.adapter and self.peft_config: dpo_trainer_kwargs["peft_config"] = self.peft_config if self.cfg.precompute_ref_log_probs is not None: - dpo_trainer_kwargs[ - "precompute_ref_log_probs" - ] = self.cfg.precompute_ref_log_probs + dpo_trainer_kwargs["precompute_ref_log_probs"] = ( + self.cfg.precompute_ref_log_probs + ) if self.cfg.rl == "grpo": trainer_cls = GRPOStrategy.get_trainer_class() trainer_cls_args = [self.model] diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index c14ed59b54..6570db9670 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -462,9 +462,9 @@ def get_train_dataloader(self) -> DataLoader: "pin_memory": self.args.dataloader_pin_memory, } if self.args.dataloader_prefetch_factor: - dataloader_params[ - "prefetch_factor" - ] = self.args.dataloader_prefetch_factor + dataloader_params["prefetch_factor"] = ( + self.args.dataloader_prefetch_factor + ) sampler = self._get_train_sampler() if isinstance(sampler, BatchSampler): @@ -509,9 +509,9 @@ def get_eval_dataloader(self, eval_dataset: Optional[Dataset] = None) -> DataLoa "pin_memory": self.args.dataloader_pin_memory, } if self.args.dataloader_prefetch_factor: - dataloader_params[ - "prefetch_factor" - ] = self.args.dataloader_prefetch_factor + dataloader_params["prefetch_factor"] = ( + self.args.dataloader_prefetch_factor + ) if isinstance(eval_sampler, BatchSampler): dataloader_params["batch_sampler"] = eval_sampler diff --git a/src/axolotl/core/trainers/dpo/__init__.py b/src/axolotl/core/trainers/dpo/__init__.py index 8187a7fb5c..2d6835cf79 100644 --- a/src/axolotl/core/trainers/dpo/__init__.py +++ b/src/axolotl/core/trainers/dpo/__init__.py @@ -1,6 +1,7 @@ """ DPO Specific Strategy for training """ + from axolotl.core.trainers.dpo.trainer import AxolotlDPOTrainer diff --git a/src/axolotl/core/trainers/dpo/args.py b/src/axolotl/core/trainers/dpo/args.py index 4cae67d3ed..de1758ed09 100644 --- a/src/axolotl/core/trainers/dpo/args.py +++ b/src/axolotl/core/trainers/dpo/args.py @@ -1,6 +1,7 @@ """ Axolotl specific DPO args """ + from dataclasses import dataclass from trl import DPOConfig diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index a1de4cc823..38b657260f 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -1,6 +1,7 @@ """ DPO trainer for axolotl """ + import gc from functools import wraps from typing import Any, Dict, Union diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index ecfc123092..52e6363a2e 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -45,9 +45,9 @@ def set_training_args_kwargs(cls, cfg): ) if trl.vllm_gpu_memory_utilization: - grpo_args_kwargs[ - "vllm_gpu_memory_utilization" - ] = trl.vllm_gpu_memory_utilization + grpo_args_kwargs["vllm_gpu_memory_utilization"] = ( + trl.vllm_gpu_memory_utilization + ) if trl.vllm_max_model_len: grpo_args_kwargs["vllm_max_model_len"] = trl.vllm_max_model_len @@ -86,9 +86,9 @@ def set_trainer_args(cls, cfg): def set_trainer_kwargs(cls, cfg): trainer_kwargs = {} if cfg.trl and cfg.trl.reward_processing_classes: - trainer_kwargs[ - "reward_processing_classes" - ] = cfg.trl.reward_processing_classes + trainer_kwargs["reward_processing_classes"] = ( + cfg.trl.reward_processing_classes + ) return trainer_kwargs @classmethod diff --git a/src/axolotl/core/trainers/grpo/args.py b/src/axolotl/core/trainers/grpo/args.py index e14e6b0dc3..5460edca92 100644 --- a/src/axolotl/core/trainers/grpo/args.py +++ b/src/axolotl/core/trainers/grpo/args.py @@ -1,6 +1,7 @@ """ Axolotl Specific Training Args """ + from dataclasses import dataclass from trl import GRPOConfig diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index 6c8f39ac68..663bed094b 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -1,6 +1,7 @@ """ Axolotl GRPO trainer """ + from accelerate.utils import is_peft_model from accelerate.utils.other import is_compiled_module from transformers import PreTrainedModel diff --git a/src/axolotl/core/trainers/trl.py b/src/axolotl/core/trainers/trl.py index 57f014bd6a..7237e792e3 100644 --- a/src/axolotl/core/trainers/trl.py +++ b/src/axolotl/core/trainers/trl.py @@ -1,6 +1,7 @@ """ module for TRL PPO training """ + import torch from tqdm import tqdm from trl import PPOTrainer diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index 7cace7643c..34a79e6462 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -1,6 +1,7 @@ """ extra axolotl specific training args """ + from dataclasses import dataclass, field from typing import Optional diff --git a/src/axolotl/integrations/grokfast/__init__.py b/src/axolotl/integrations/grokfast/__init__.py index 3889e927c2..c8c352bbe0 100644 --- a/src/axolotl/integrations/grokfast/__init__.py +++ b/src/axolotl/integrations/grokfast/__init__.py @@ -1,6 +1,7 @@ """ Grokfast plugin for Axolotl """ + import logging from transformers.trainer_callback import TrainerCallback diff --git a/src/axolotl/integrations/grokfast/args.py b/src/axolotl/integrations/grokfast/args.py index 4776ae60ca..ac91c7395c 100644 --- a/src/axolotl/integrations/grokfast/args.py +++ b/src/axolotl/integrations/grokfast/args.py @@ -1,6 +1,7 @@ """ config args for grokfast plugin """ + from typing import Optional from pydantic import BaseModel diff --git a/src/axolotl/integrations/kd/args.py b/src/axolotl/integrations/kd/args.py index a88a0dc484..2fbba2c6ae 100644 --- a/src/axolotl/integrations/kd/args.py +++ b/src/axolotl/integrations/kd/args.py @@ -26,12 +26,12 @@ class KDArgs(BaseModel): """ kd_trainer: Optional[bool] = None # whether to use KD trainer - kd_ce_alpha: Optional[ - float - ] = None # loss coefficient for cross-entropy loss during KD + kd_ce_alpha: Optional[float] = ( + None # loss coefficient for cross-entropy loss during KD + ) kd_alpha: Optional[float] = None # loss coefficient for KD loss kd_temperature: Optional[float] = None # temperature for sampling during KD kd_zscore_base_temp: Optional[float] = None # base temperature for zscore scaling - kd_top_k_before_softmax: Optional[ - bool - ] = None # whether to sample top k before softmax during KD + kd_top_k_before_softmax: Optional[bool] = ( + None # whether to sample top k before softmax during KD + ) diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index b67dd01e64..b8e1fac529 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -55,9 +55,9 @@ def pre_model_load(self, cfg): if "cross_entropy" in liger_fn_sig.parameters: kwargs["cross_entropy"] = cfg.liger_cross_entropy if "fused_linear_cross_entropy" in liger_fn_sig.parameters: - kwargs[ - "fused_linear_cross_entropy" - ] = cfg.liger_fused_linear_cross_entropy + kwargs["fused_linear_cross_entropy"] = ( + cfg.liger_fused_linear_cross_entropy + ) if "rms_norm" in liger_fn_sig.parameters: kwargs["rms_norm"] = cfg.liger_rms_norm if "layer_norm" in liger_fn_sig.parameters: diff --git a/src/axolotl/integrations/liger/models/deepseekv2.py b/src/axolotl/integrations/liger/models/deepseekv2.py index 79fb274360..c29fd4e798 100644 --- a/src/axolotl/integrations/liger/models/deepseekv2.py +++ b/src/axolotl/integrations/liger/models/deepseekv2.py @@ -1,6 +1,7 @@ """ DeepseekV2 model with LigerFusedLinearCrossEntropyLoss """ + # pylint: disable=duplicate-code from typing import List, Optional, Tuple, Union diff --git a/src/axolotl/integrations/liger/models/jamba.py b/src/axolotl/integrations/liger/models/jamba.py index 40cec63a4f..7ab464c88d 100644 --- a/src/axolotl/integrations/liger/models/jamba.py +++ b/src/axolotl/integrations/liger/models/jamba.py @@ -1,6 +1,7 @@ """ Jamba model with LigerFusedLinearCrossEntropyLoss """ + # pylint: disable=duplicate-code from typing import Optional, Tuple, Union diff --git a/src/axolotl/integrations/lm_eval/__init__.py b/src/axolotl/integrations/lm_eval/__init__.py index 0cbc8a49d1..8db4dc6346 100644 --- a/src/axolotl/integrations/lm_eval/__init__.py +++ b/src/axolotl/integrations/lm_eval/__init__.py @@ -1,6 +1,7 @@ """ Module for the Plugin for LM Eval Harness """ + import subprocess # nosec from axolotl.integrations.base import BasePlugin diff --git a/src/axolotl/integrations/lm_eval/args.py b/src/axolotl/integrations/lm_eval/args.py index 721f560e3f..d022131777 100644 --- a/src/axolotl/integrations/lm_eval/args.py +++ b/src/axolotl/integrations/lm_eval/args.py @@ -1,6 +1,7 @@ """ Module for handling lm eval harness input arguments. """ + from typing import List, Optional from pydantic import BaseModel diff --git a/src/axolotl/integrations/lm_eval/cli.py b/src/axolotl/integrations/lm_eval/cli.py index 4a9bbafe6e..19608e1d95 100644 --- a/src/axolotl/integrations/lm_eval/cli.py +++ b/src/axolotl/integrations/lm_eval/cli.py @@ -1,6 +1,7 @@ """ axolotl CLI for running lm_eval tasks """ + import subprocess # nosec from collections import defaultdict from datetime import datetime diff --git a/src/axolotl/kernels/geglu.py b/src/axolotl/kernels/geglu.py index 4dd70f4cc9..0aa035c941 100644 --- a/src/axolotl/kernels/geglu.py +++ b/src/axolotl/kernels/geglu.py @@ -5,6 +5,7 @@ Credit to `unsloth` (https://unsloth.ai/) for inspiration for this implementation. """ + # pylint: disable=invalid-name,unnecessary-lambda-assignment,duplicate-code import torch diff --git a/src/axolotl/kernels/lora.py b/src/axolotl/kernels/lora.py index 1f8a8e787b..03fca6df4a 100644 --- a/src/axolotl/kernels/lora.py +++ b/src/axolotl/kernels/lora.py @@ -6,6 +6,7 @@ Credit to `unsloth` (https://unsloth.ai/) for inspiration for this implementation. """ + # pylint: disable=invalid-name from typing import Callable diff --git a/src/axolotl/kernels/quantize.py b/src/axolotl/kernels/quantize.py index ea5ecf8e84..b61603fbcf 100644 --- a/src/axolotl/kernels/quantize.py +++ b/src/axolotl/kernels/quantize.py @@ -1,4 +1,5 @@ """Dequantization utilities for `bitsandbytes` integration.""" + # pylint: disable=invalid-name,global-statement import ctypes diff --git a/src/axolotl/kernels/swiglu.py b/src/axolotl/kernels/swiglu.py index 20c6e87a0f..43a798edcc 100644 --- a/src/axolotl/kernels/swiglu.py +++ b/src/axolotl/kernels/swiglu.py @@ -5,6 +5,7 @@ Credit to `unsloth` (https://unsloth.ai/) for inspiration for this implementation. """ + import torch import triton import triton.language as tl diff --git a/src/axolotl/models/mamba/configuration_mamba.py b/src/axolotl/models/mamba/configuration_mamba.py index 5160ee8d7e..d6b77b951a 100644 --- a/src/axolotl/models/mamba/configuration_mamba.py +++ b/src/axolotl/models/mamba/configuration_mamba.py @@ -1,6 +1,7 @@ """ HF Transformers MambaConfig """ + from transformers import PretrainedConfig diff --git a/src/axolotl/monkeypatch/attention/mllama.py b/src/axolotl/monkeypatch/attention/mllama.py index 0b18b716d5..c9e8fb5e17 100644 --- a/src/axolotl/monkeypatch/attention/mllama.py +++ b/src/axolotl/monkeypatch/attention/mllama.py @@ -1,6 +1,7 @@ """ Monkeypatch for Vision Llama for FA2 support """ + # pylint: disable=duplicate-code from typing import Optional, Tuple @@ -220,10 +221,10 @@ def patch_mllama(): True ) MLLAMA_TEXT_ATTENTION_CLASSES["flash_attention_2"] = MllamaTextSelfFlashAttention2 - MLLAMA_TEXT_CROSS_ATTENTION_CLASSES[ - "flash_attention_2" - ] = MllamaTextCrossFlashAttention2 + MLLAMA_TEXT_CROSS_ATTENTION_CLASSES["flash_attention_2"] = ( + MllamaTextCrossFlashAttention2 + ) # fallback to SDPA - MLLAMA_VISION_ATTENTION_CLASSES[ - "flash_attention_2" - ] = MLLAMA_VISION_ATTENTION_CLASSES["sdpa"] + MLLAMA_VISION_ATTENTION_CLASSES["flash_attention_2"] = ( + MLLAMA_VISION_ATTENTION_CLASSES["sdpa"] + ) diff --git a/src/axolotl/monkeypatch/data/batch_dataset_fetcher.py b/src/axolotl/monkeypatch/data/batch_dataset_fetcher.py index 2e9364e3a5..df8d106fd0 100644 --- a/src/axolotl/monkeypatch/data/batch_dataset_fetcher.py +++ b/src/axolotl/monkeypatch/data/batch_dataset_fetcher.py @@ -1,4 +1,5 @@ """monkey patches for the dataset fetcher to handle batches of packed indexes""" + # pylint: disable=protected-access import torch diff --git a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py index ad0459ccc0..998a810279 100644 --- a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py @@ -12,7 +12,9 @@ from einops import rearrange from flash_attn.bert_padding import pad_input, unpad_input from transformers.modeling_outputs import BaseModelOutputWithPast -from transformers.models.llama.modeling_llama import LlamaAttention +from transformers.models.llama.modeling_llama import ( + LlamaAttention, +) from transformers.models.llama.modeling_llama import ( LlamaDecoderLayer as OriginalLlamaDecoderLayer, ) @@ -490,9 +492,11 @@ def flashattn_forward( # We have disabled _prepare_decoder_attention_mask in LlamaModel # the attention_mask should be the same as the key_padding_mask key_padding_mask=attention_mask, - query_padding_mask=attention_mask[:, -query_states.size(1) :] - if attention_mask is not None - else None, + query_padding_mask=( + attention_mask[:, -query_states.size(1) :] + if attention_mask is not None + else None + ), ) output_unpad = flash_attn_varlen_qkvpacked_func( qkv_unpad, @@ -531,9 +535,11 @@ def flashattn_forward( value_states, kvpacked=True, key_padding_mask=attention_mask, - query_padding_mask=attention_mask[:, -query_states.size(1) :] - if attention_mask is not None - else None, + query_padding_mask=( + attention_mask[:, -query_states.size(1) :] + if attention_mask is not None + else None + ), ) if q_unpad.dtype != kv_unpad.dtype: kv_unpad = kv_unpad.to(q_unpad.dtype) diff --git a/src/axolotl/monkeypatch/llama_expand_mask.py b/src/axolotl/monkeypatch/llama_expand_mask.py index 5738bb543c..0277c212a4 100644 --- a/src/axolotl/monkeypatch/llama_expand_mask.py +++ b/src/axolotl/monkeypatch/llama_expand_mask.py @@ -1,6 +1,7 @@ """ expands the binary attention mask per 3.2.2 of https://arxiv.org/pdf/2107.02027.pdf """ + from typing import Optional import torch diff --git a/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py b/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py index 1cbc4278ba..ac9815fce2 100644 --- a/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py @@ -1,4 +1,5 @@ """Flash attention monkey patch for mistral model""" + # pylint: disable=duplicate-code import logging @@ -21,7 +22,10 @@ from transformers.models.mistral.modeling_mistral import ( MistralDecoderLayer as OriginalMistralDecoderLayer, ) -from transformers.models.mistral.modeling_mistral import apply_rotary_pos_emb, repeat_kv +from transformers.models.mistral.modeling_mistral import ( + apply_rotary_pos_emb, + repeat_kv, +) from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids @@ -243,9 +247,11 @@ def flashattn_forward( # We have disabled _prepare_decoder_attention_mask in LlamaModel # the attention_mask should be the same as the key_padding_mask key_padding_mask=attention_mask, - query_padding_mask=attention_mask[:, -query_states.size(1) :] - if attention_mask is not None - else None, + query_padding_mask=( + attention_mask[:, -query_states.size(1) :] + if attention_mask is not None + else None + ), ) output_unpad = flash_attn_varlen_qkvpacked_func( qkv_unpad, @@ -286,9 +292,11 @@ def flashattn_forward( value_states, kvpacked=True, key_padding_mask=attention_mask, - query_padding_mask=attention_mask[:, -query_states.size(1) :] - if attention_mask is not None - else None, + query_padding_mask=( + attention_mask[:, -query_states.size(1) :] + if attention_mask is not None + else None + ), ) if q_unpad.dtype != kv_unpad.dtype: kv_unpad = kv_unpad.to(q_unpad.dtype) diff --git a/src/axolotl/monkeypatch/mixtral/__init__.py b/src/axolotl/monkeypatch/mixtral/__init__.py index bb5afe847c..5b8054000f 100644 --- a/src/axolotl/monkeypatch/mixtral/__init__.py +++ b/src/axolotl/monkeypatch/mixtral/__init__.py @@ -1,6 +1,7 @@ """ Patches to support multipack for mixtral """ + import torch diff --git a/src/axolotl/monkeypatch/relora.py b/src/axolotl/monkeypatch/relora.py index 1dd758ec54..822fd4465e 100644 --- a/src/axolotl/monkeypatch/relora.py +++ b/src/axolotl/monkeypatch/relora.py @@ -1,4 +1,5 @@ """Implements the ReLoRA training procedure from https://arxiv.org/abs/2307.05695, minus the initial full fine-tune.""" + import glob import json import logging @@ -411,7 +412,10 @@ def merge_and_save( if shard_path.endswith(".safetensors"): in_tensors = st.load_file(str(Path(model_src) / shard_path)) else: - in_tensors = torch.load(Path(model_src) / shard_path) + in_tensors = torch.load( + Path(model_src) / shard_path, + weights_only=True, # to prevent arbitrary code execution + ) if "state_dict" in in_tensors: in_tensors = in_tensors["state_dict"] diff --git a/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py b/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py index 67e9337e36..c603021116 100644 --- a/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py @@ -17,7 +17,7 @@ # https://github.com/huggingface/transformers/blob/main/src/transformers/models/llama/modeling_llama.py # https://github.com/huggingface/transformers/blob/main/src/transformers/models/gpt_neox/modeling_gpt_neox.py # pylint: disable=duplicate-code -""" PyTorch StableLM Epoch model. """ +"""PyTorch StableLM Epoch model.""" import importlib import math from typing import Optional, Tuple, Union diff --git a/src/axolotl/monkeypatch/trainer_fsdp_optim.py b/src/axolotl/monkeypatch/trainer_fsdp_optim.py index 00c2dfebcc..1cbfefa5b1 100644 --- a/src/axolotl/monkeypatch/trainer_fsdp_optim.py +++ b/src/axolotl/monkeypatch/trainer_fsdp_optim.py @@ -1,6 +1,7 @@ """ fix for FSDP optimizer save in trainer w 4.47.0 """ + import inspect import logging diff --git a/src/axolotl/monkeypatch/utils.py b/src/axolotl/monkeypatch/utils.py index c2772b4715..43496c7c88 100644 --- a/src/axolotl/monkeypatch/utils.py +++ b/src/axolotl/monkeypatch/utils.py @@ -1,6 +1,7 @@ """ Shared utils for the monkeypatches """ + import re from typing import Optional, Tuple diff --git a/src/axolotl/monkeypatch/xformers_/__init__.py b/src/axolotl/monkeypatch/xformers_/__init__.py index bddc036b24..a052ea49e8 100644 --- a/src/axolotl/monkeypatch/xformers_/__init__.py +++ b/src/axolotl/monkeypatch/xformers_/__init__.py @@ -1,6 +1,7 @@ """ Fused MLP layer for incrementally improved training efficiency """ + import torch from transformers.models.llama.modeling_llama import LlamaMLP from xformers.ops import SwiGLU diff --git a/src/axolotl/prompt_strategies/alpaca_w_system.py b/src/axolotl/prompt_strategies/alpaca_w_system.py index 8c8cc07435..6873c8e087 100644 --- a/src/axolotl/prompt_strategies/alpaca_w_system.py +++ b/src/axolotl/prompt_strategies/alpaca_w_system.py @@ -1,6 +1,7 @@ """ Prompt strategies loader for alpaca instruction datasets with system prompts """ + from typing import Generator, Tuple, Union from axolotl.prompt_tokenizers import PromptTokenizingStrategy diff --git a/src/axolotl/prompt_strategies/completion.py b/src/axolotl/prompt_strategies/completion.py index 3285e667cb..62a4b90b28 100644 --- a/src/axolotl/prompt_strategies/completion.py +++ b/src/axolotl/prompt_strategies/completion.py @@ -1,6 +1,7 @@ """ Basic completion text """ + from collections import defaultdict from typing import Any, Dict, Generator, Optional, Tuple diff --git a/src/axolotl/prompt_strategies/context_qa.py b/src/axolotl/prompt_strategies/context_qa.py index f87dd8b5cd..aac44e0b2c 100644 --- a/src/axolotl/prompt_strategies/context_qa.py +++ b/src/axolotl/prompt_strategies/context_qa.py @@ -1,4 +1,5 @@ """Module containing the classes for Context QA Prompt Tokenization Strategies""" + from typing import Tuple from axolotl.prompt_tokenizers import InstructionPromptTokenizingStrategy diff --git a/src/axolotl/prompt_strategies/dpo/__init__.py b/src/axolotl/prompt_strategies/dpo/__init__.py index 7f5e6eb644..f671256823 100644 --- a/src/axolotl/prompt_strategies/dpo/__init__.py +++ b/src/axolotl/prompt_strategies/dpo/__init__.py @@ -1,6 +1,7 @@ """ module for DPO style dataset transform strategies """ + from functools import partial from ..base import load as load_base diff --git a/src/axolotl/prompt_strategies/dpo/chatml.py b/src/axolotl/prompt_strategies/dpo/chatml.py index 5043a501e6..34a54aaa0b 100644 --- a/src/axolotl/prompt_strategies/dpo/chatml.py +++ b/src/axolotl/prompt_strategies/dpo/chatml.py @@ -33,9 +33,9 @@ def transform_fn(sample): f"<|im_start|>user\n{sample[prompt_key]}<|im_end|>\n<|im_start|>assistant\n" ) else: - sample[ - "prompt" - ] = f"<|im_start|>user\n{sample[prompt_key]}<|im_end|>\n<|im_start|>assistant\n" + sample["prompt"] = ( + f"<|im_start|>user\n{sample[prompt_key]}<|im_end|>\n<|im_start|>assistant\n" + ) sample["chosen"] = f"{sample[chosen_key]}<|im_end|>" sample["rejected"] = f"{sample[rejected_key]}<|im_end|>" return sample @@ -52,9 +52,9 @@ def argilla_chat( """ def transform_fn(sample): - sample[ - "prompt" - ] = f"<|im_start|>user\n{sample['chosen'][0]['content']}<|im_end|>\n<|im_start|>assistant\n" + sample["prompt"] = ( + f"<|im_start|>user\n{sample['chosen'][0]['content']}<|im_end|>\n<|im_start|>assistant\n" + ) sample["chosen"] = f"{sample['chosen'][1]['content']}<|im_end|>" sample["rejected"] = f"{sample['rejected'][1]['content']}<|im_end|>" return sample @@ -78,9 +78,9 @@ def transform_fn(sample): f"<|im_start|>user\n{sample['input']}<|im_end|>\n<|im_start|>assistant\n" ) else: - sample[ - "prompt" - ] = f"<|im_start|>user\n{sample['input']}<|im_end|>\n<|im_start|>assistant\n" + sample["prompt"] = ( + f"<|im_start|>user\n{sample['input']}<|im_end|>\n<|im_start|>assistant\n" + ) sample["chosen"] = f"{sample['chosen']}<|im_end|>" sample["rejected"] = f"{sample['rejected']}<|im_end|>" return sample @@ -100,9 +100,9 @@ def transform_fn(sample): f"<|im_start|>user\n{sample['question']}<|im_end|>\n<|im_start|>assistant\n" ) else: - sample[ - "prompt" - ] = f"<|im_start|>user\n{sample['question']}<|im_end|>\n<|im_start|>assistant\n" + sample["prompt"] = ( + f"<|im_start|>user\n{sample['question']}<|im_end|>\n<|im_start|>assistant\n" + ) sample["chosen"] = f"{sample['chosen']}<|im_end|>" sample["rejected"] = f"{sample['rejected']}<|im_end|>" return sample @@ -120,9 +120,9 @@ def transform_fn(sample): f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" ) else: - sample[ - "prompt" - ] = f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" + sample["prompt"] = ( + f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" + ) sample["chosen"] = f"{sample['chosen']}<|im_end|>" sample["rejected"] = f"{sample['rejected']}<|im_end|>" return sample @@ -142,9 +142,9 @@ def transform_fn(sample): f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" ) else: - sample[ - "prompt" - ] = f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" + sample["prompt"] = ( + f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" + ) sample["chosen"] = f"{sample['chosen'][1]['content']}<|im_end|>" sample["rejected"] = f"{sample['rejected'][1]['content']}<|im_end|>" return sample diff --git a/src/axolotl/prompt_strategies/dpo/llama3.py b/src/axolotl/prompt_strategies/dpo/llama3.py index d10aa223bb..eed420017a 100644 --- a/src/axolotl/prompt_strategies/dpo/llama3.py +++ b/src/axolotl/prompt_strategies/dpo/llama3.py @@ -34,9 +34,9 @@ def transform_fn(sample): f"<|start_header_id|>user<|end_header_id|>\n\n{sample[prompt_key]}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" ) else: - sample[ - "prompt" - ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample[prompt_key]}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["prompt"] = ( + f"<|start_header_id|>user<|end_header_id|>\n\n{sample[prompt_key]}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) sample["chosen"] = f"{sample[chosen_key]}<|eot_id|>" sample["rejected"] = f"{sample[rejected_key]}<|eot_id|>" return sample @@ -53,9 +53,9 @@ def argilla_chat( """ def transform_fn(sample): - sample[ - "prompt" - ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['chosen'][0]['content']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["prompt"] = ( + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['chosen'][0]['content']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) sample["chosen"] = f"{sample['chosen'][1]['content']}<|eot_id|>" sample["rejected"] = f"{sample['rejected'][1]['content']}<|eot_id|>" return sample @@ -79,9 +79,9 @@ def transform_fn(sample): f"<|start_header_id|>user<|end_header_id|>\n\n{sample['input']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" ) else: - sample[ - "prompt" - ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['input']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["prompt"] = ( + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['input']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) sample["chosen"] = f"{sample['chosen']}<|eot_id|>" sample["rejected"] = f"{sample['rejected']}<|eot_id|>" return sample @@ -101,9 +101,9 @@ def transform_fn(sample): f"<|start_header_id|>user<|end_header_id|>\n\n{sample['question']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" ) else: - sample[ - "prompt" - ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['question']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["prompt"] = ( + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['question']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) sample["chosen"] = f"{sample['chosen']}<|eot_id|>" sample["rejected"] = f"{sample['rejected']}<|eot_id|>" return sample @@ -121,9 +121,9 @@ def transform_fn(sample): f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" ) else: - sample[ - "prompt" - ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["prompt"] = ( + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) sample["chosen"] = f"{sample['chosen']}<|eot_id|>" sample["rejected"] = f"{sample['rejected']}<|eot_id|>" return sample @@ -143,9 +143,9 @@ def transform_fn(sample): f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" ) else: - sample[ - "prompt" - ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["prompt"] = ( + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) sample["chosen"] = f"{sample['chosen'][1]['content']}<|eot_id|>" sample["rejected"] = f"{sample['rejected'][1]['content']}<|eot_id|>" return sample diff --git a/src/axolotl/prompt_strategies/input_output.py b/src/axolotl/prompt_strategies/input_output.py index fe14f039cf..8be745b208 100644 --- a/src/axolotl/prompt_strategies/input_output.py +++ b/src/axolotl/prompt_strategies/input_output.py @@ -1,4 +1,5 @@ """Module for plain input/output prompt pairs""" + from typing import Generator, Tuple from axolotl.prompt_tokenizers import PromptTokenizingStrategy diff --git a/src/axolotl/prompt_strategies/jinja_template_analyzer.py b/src/axolotl/prompt_strategies/jinja_template_analyzer.py index 01cbd3ad27..a5f89cfe54 100644 --- a/src/axolotl/prompt_strategies/jinja_template_analyzer.py +++ b/src/axolotl/prompt_strategies/jinja_template_analyzer.py @@ -1,4 +1,5 @@ """Module for inspect jinja templates for the variables they use""" + from typing import Dict, Optional, Set, TypedDict, Union from jinja2 import Environment, meta, nodes diff --git a/src/axolotl/prompt_strategies/kto/chatml.py b/src/axolotl/prompt_strategies/kto/chatml.py index 46c305f831..97ae59ed50 100644 --- a/src/axolotl/prompt_strategies/kto/chatml.py +++ b/src/axolotl/prompt_strategies/kto/chatml.py @@ -1,6 +1,7 @@ """ KTO strategies for chatml """ + # pylint: disable=duplicate-code @@ -15,9 +16,9 @@ def transform_fn(sample): f"<|im_start|>user\n{sample['instruction']}<|im_end|>\n<|im_start|>assistant\n" ) else: - sample[ - "prompt" - ] = f"<|im_start|>user\n{sample['instruction']}<|im_end|>\n<|im_start|>assistant\n" + sample["prompt"] = ( + f"<|im_start|>user\n{sample['instruction']}<|im_end|>\n<|im_start|>assistant\n" + ) sample["completion"] = f"{sample['completion']}<|im_end|>" return sample @@ -33,9 +34,9 @@ def argilla_chat( """ def transform_fn(sample): - sample[ - "prompt" - ] = f"<|im_start|>user\n{sample['chosen'][0]['content']}<|im_end|>\n<|im_start|>assistant\n" + sample["prompt"] = ( + f"<|im_start|>user\n{sample['chosen'][0]['content']}<|im_end|>\n<|im_start|>assistant\n" + ) sample["completion"] = f"{sample['completion'][1]['content']}<|im_end|>" return sample @@ -55,9 +56,9 @@ def transform_fn(sample): f"<|im_start|>user\n{sample['question']}<|im_end|>\n<|im_start|>assistant\n" ) else: - sample[ - "prompt" - ] = f"<|im_start|>user\n{sample['question']}<|im_end|>\n<|im_start|>assistant\n" + sample["prompt"] = ( + f"<|im_start|>user\n{sample['question']}<|im_end|>\n<|im_start|>assistant\n" + ) sample["completion"] = f"{sample['completion']}<|im_end|>" return sample @@ -74,9 +75,9 @@ def transform_fn(sample): f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" ) else: - sample[ - "prompt" - ] = f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" + sample["prompt"] = ( + f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" + ) sample["completion"] = f"{sample['completion']}<|im_end|>" return sample @@ -96,9 +97,9 @@ def transform_fn(sample): f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" ) else: - sample[ - "prompt" - ] = f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" + sample["prompt"] = ( + f"<|im_start|>user\n{sample['prompt']}<|im_end|>\n<|im_start|>assistant\n" + ) sample["completion"] = f"{sample['completion']}<|im_end|>" return sample diff --git a/src/axolotl/prompt_strategies/kto/llama3.py b/src/axolotl/prompt_strategies/kto/llama3.py index 795d343fe3..fde3c2ed4a 100644 --- a/src/axolotl/prompt_strategies/kto/llama3.py +++ b/src/axolotl/prompt_strategies/kto/llama3.py @@ -1,6 +1,7 @@ """ KTO strategies for llama-3 chat template """ + # pylint: disable=duplicate-code @@ -15,9 +16,9 @@ def transform_fn(sample): f"<|start_header_id|>user<|end_header_id|>\n\n{sample['instruction']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" ) else: - sample[ - "prompt" - ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['instruction']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["prompt"] = ( + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['instruction']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) sample["completion"] = f"{sample['completion']}<|eot_id|>" return sample @@ -33,9 +34,9 @@ def argilla_chat( """ def transform_fn(sample): - sample[ - "prompt" - ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['completion'][0]['content']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["prompt"] = ( + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['completion'][0]['content']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) sample["completion"] = f"{sample['completion'][1]['content']}<|eot_id|>" return sample @@ -55,9 +56,9 @@ def transform_fn(sample): f"<|start_header_id|>user<|end_header_id|>\n\n{sample['question']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" ) else: - sample[ - "prompt" - ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['question']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["prompt"] = ( + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['question']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) sample["completion"] = f"{sample['completion']}<|eot_id|>" return sample @@ -74,9 +75,9 @@ def transform_fn(sample): f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" ) else: - sample[ - "prompt" - ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["prompt"] = ( + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) sample["completion"] = f"{sample['completion']}<|eot_id|>" return sample @@ -96,9 +97,9 @@ def transform_fn(sample): f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" ) else: - sample[ - "prompt" - ] = f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + sample["prompt"] = ( + f"<|start_header_id|>user<|end_header_id|>\n\n{sample['prompt']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + ) sample["completion"] = f"{sample['completion']}<|eot_id|>" return sample diff --git a/src/axolotl/prompt_strategies/kto/user_defined.py b/src/axolotl/prompt_strategies/kto/user_defined.py index 7e5458bb70..7c68a30005 100644 --- a/src/axolotl/prompt_strategies/kto/user_defined.py +++ b/src/axolotl/prompt_strategies/kto/user_defined.py @@ -1,6 +1,7 @@ """ User-defined KTO strategies """ + # pylint: disable=duplicate-code diff --git a/src/axolotl/prompt_strategies/messages/chat.py b/src/axolotl/prompt_strategies/messages/chat.py index 52124407e8..eaed2396a6 100644 --- a/src/axolotl/prompt_strategies/messages/chat.py +++ b/src/axolotl/prompt_strategies/messages/chat.py @@ -1,6 +1,7 @@ """ Chat dataset wrapping strategy for new internal messages representations """ + from typing import Any, Callable, Dict, Optional from axolotl.core.datasets.chat import TokenizedChatDataset diff --git a/src/axolotl/prompt_strategies/orcamini.py b/src/axolotl/prompt_strategies/orcamini.py index 04ce5767dd..1a694cf1d7 100644 --- a/src/axolotl/prompt_strategies/orcamini.py +++ b/src/axolotl/prompt_strategies/orcamini.py @@ -9,6 +9,7 @@ Not suited/tested for multiple-turn conversations without further adjustments. """ + from typing import Generator, Union from axolotl.prompt_strategies.alpaca_w_system import OpenOrcaPromptTokenizingStrategy diff --git a/src/axolotl/prompt_strategies/orpo/chat_template.py b/src/axolotl/prompt_strategies/orpo/chat_template.py index e53a547483..fdee28ea1e 100644 --- a/src/axolotl/prompt_strategies/orpo/chat_template.py +++ b/src/axolotl/prompt_strategies/orpo/chat_template.py @@ -1,4 +1,5 @@ """chatml prompt tokenization strategy for ORPO""" + from typing import Any, Dict, Generator, List, Optional, Tuple from pydantic import BaseModel diff --git a/src/axolotl/prompt_strategies/pretrain.py b/src/axolotl/prompt_strategies/pretrain.py index 8430a7fcab..cfb6cb8be4 100644 --- a/src/axolotl/prompt_strategies/pretrain.py +++ b/src/axolotl/prompt_strategies/pretrain.py @@ -1,4 +1,5 @@ """pretraining prompt strategies""" + from typing import Generator from transformers import BatchEncoding diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 1ceb5babd0..ff486db291 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -406,9 +406,7 @@ def handle_untrained_tokens_fix( ) -def setup_model_and_trainer( - cfg: DictDefault, dataset_meta: TrainDatasetMeta -) -> tuple[ +def setup_model_and_trainer(cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> tuple[ HFRLTrainerBuilder | HFCausalTrainerBuilder, PeftModel | PreTrainedModel, PreTrainedTokenizer, diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index 35ea145519..ffa528cc95 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -40,6 +40,6 @@ def set_pytorch_cuda_alloc_conf(): torch_major, torch_minor = int(torch_version[0]), int(torch_version[1]) if torch_major == 2 and torch_minor >= 2: if os.getenv("PYTORCH_CUDA_ALLOC_CONF") is None: - os.environ[ - "PYTORCH_CUDA_ALLOC_CONF" - ] = "expandable_segments:True,roundup_power2_divisions:16" + os.environ["PYTORCH_CUDA_ALLOC_CONF"] = ( + "expandable_segments:True,roundup_power2_divisions:16" + ) diff --git a/src/axolotl/utils/bench.py b/src/axolotl/utils/bench.py index 3d338aff10..d1e972c811 100644 --- a/src/axolotl/utils/bench.py +++ b/src/axolotl/utils/bench.py @@ -1,4 +1,5 @@ """Benchmarking and measurement utilities""" + import functools import torch diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 9ca0e84fe8..47c77619a8 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -343,9 +343,9 @@ def on_evaluate( bench_refs.extend(combined_bench_names[bench_name]["refs"]) bench_preds.extend(combined_bench_names[bench_name]["preds"]) if not pd.isna(bench_score): - results[ - f"{bench_split}_bench_accuracy_{bench_name}" - ] = bench_score + results[f"{bench_split}_bench_accuracy_{bench_name}"] = ( + bench_score + ) bench_scores.append(bench_score) else: results[f"{bench_split}_bench_accuracy_{bench_name}"] = 0.0 diff --git a/src/axolotl/utils/callbacks/mlflow_.py b/src/axolotl/utils/callbacks/mlflow_.py index fcbb88edcd..47679001f5 100644 --- a/src/axolotl/utils/callbacks/mlflow_.py +++ b/src/axolotl/utils/callbacks/mlflow_.py @@ -1,4 +1,5 @@ """MLFlow module for trainer callbacks""" + import logging from shutil import copyfile from tempfile import NamedTemporaryFile diff --git a/src/axolotl/utils/callbacks/perplexity.py b/src/axolotl/utils/callbacks/perplexity.py index d3a362c4cd..a5b39c3046 100644 --- a/src/axolotl/utils/callbacks/perplexity.py +++ b/src/axolotl/utils/callbacks/perplexity.py @@ -1,4 +1,5 @@ """callback to calculate perplexity as an evaluation metric.""" + from typing import Dict, List, Optional import torch diff --git a/src/axolotl/utils/callbacks/profiler.py b/src/axolotl/utils/callbacks/profiler.py index 8616963323..36604813f7 100644 --- a/src/axolotl/utils/callbacks/profiler.py +++ b/src/axolotl/utils/callbacks/profiler.py @@ -1,6 +1,7 @@ """ HF Trainer callback for creating pytorch profiling snapshots """ + from pathlib import Path from pickle import dump # nosec B403 diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 9f65506892..d3c88334b4 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -2,6 +2,7 @@ This module provides functionality for selecting chat templates based on user choices. These templates are used for formatting messages in a conversation. """ + import logging from typing import TYPE_CHECKING, Any, Dict, Optional diff --git a/src/axolotl/utils/collators/__init__.py b/src/axolotl/utils/collators/__init__.py index 93502b67d7..8c60f223cd 100644 --- a/src/axolotl/utils/collators/__init__.py +++ b/src/axolotl/utils/collators/__init__.py @@ -1,6 +1,7 @@ """ shared axolotl collators for multipack, mamba, multimodal """ + from .batching import ( # noqa: F401 BatchSamplerDataCollatorForSeq2Seq, DataCollatorForSeq2Seq, diff --git a/src/axolotl/utils/collators/core.py b/src/axolotl/utils/collators/core.py index 0eae0c3bda..542328127c 100644 --- a/src/axolotl/utils/collators/core.py +++ b/src/axolotl/utils/collators/core.py @@ -1,4 +1,5 @@ """ basic shared collator constants """ + IGNORE_INDEX = -100 diff --git a/src/axolotl/utils/collators/mamba.py b/src/axolotl/utils/collators/mamba.py index 0c4a22fcc0..33f3991cbb 100644 --- a/src/axolotl/utils/collators/mamba.py +++ b/src/axolotl/utils/collators/mamba.py @@ -1,6 +1,7 @@ """ collators for Mamba """ + from dataclasses import dataclass from typing import Dict, Sequence diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 421f3b6493..b7096eeabb 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -18,7 +18,11 @@ from axolotl.utils.config.models.input.v0_4_1 import ( AxolotlInputConfig as AxolotlInputConfigBase, ) -from axolotl.utils.config.models.input.v0_4_1 import DPODataset, KTODataset, SFTDataset +from axolotl.utils.config.models.input.v0_4_1 import ( + DPODataset, + KTODataset, + SFTDataset, +) from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_model_config diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py index 2fa86eced8..f1c514a7c8 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/config/models/input/v0_4_1/__init__.py @@ -200,12 +200,12 @@ class SFTDataset(BaseModel): field_human: Optional[str] = None field_model: Optional[str] = None field_messages: Optional[str] = None - message_field_role: Optional[ - str - ] = None # deprecated, use message_property_mappings - message_field_content: Optional[ - str - ] = None # deprecated, use message_property_mappings + message_field_role: Optional[str] = ( + None # deprecated, use message_property_mappings + ) + message_field_content: Optional[str] = ( + None # deprecated, use message_property_mappings + ) message_property_mappings: Optional[Dict[str, str]] = None message_field_training: Optional[str] = None message_field_training_detail: Optional[str] = None @@ -505,9 +505,9 @@ class HyperparametersConfig(BaseModel): embedding_lr: Optional[float] = None embedding_lr_scale: Optional[float] = None weight_decay: Optional[float] = 0.0 - optimizer: Optional[ - Union[OptimizerNames, CustomSupportedOptimizers] - ] = OptimizerNames.ADAMW_TORCH_FUSED + optimizer: Optional[Union[OptimizerNames, CustomSupportedOptimizers]] = ( + OptimizerNames.ADAMW_TORCH_FUSED + ) optim_args: Optional[Union[str, Dict[str, Any]]] = Field( default=None, json_schema_extra={"description": "Optional arguments to supply to optimizer."}, @@ -699,9 +699,9 @@ class AxolotlInputConfig( reward_model: Optional[bool] = None process_reward_model: Optional[bool] = None num_labels: Optional[int] = None - dpo_use_weighting: Optional[ - bool - ] = None # whether to use weighting in DPO trainer. If none, default is false in the trainer. + dpo_use_weighting: Optional[bool] = ( + None # whether to use weighting in DPO trainer. If none, default is false in the trainer. + ) dpo_use_logits_to_keep: Optional[bool] = None datasets: Optional[ @@ -780,9 +780,9 @@ class AxolotlInputConfig( # torch_dtype: Optional[torch.dtype] - gradient_checkpointing: Optional[ - Union[Literal["unsloth", "offload"], bool] - ] = Field(default=False) + gradient_checkpointing: Optional[Union[Literal["unsloth", "offload"], bool]] = ( + Field(default=False) + ) gradient_checkpointing_kwargs: Optional[Dict[str, Any]] = None unfrozen_parameters: Optional[List[str]] = None @@ -894,9 +894,9 @@ class AxolotlInputConfig( kto_undesirable_weight: Optional[float] = None rl_beta: Optional[float] = None - max_memory: Optional[ - Dict[Union[int, Literal["cpu", "disk"]], Union[int, str]] - ] = None + max_memory: Optional[Dict[Union[int, Literal["cpu", "disk"]], Union[int, str]]] = ( + None + ) gpu_memory_limit: Optional[Union[int, str]] = None low_cpu_mem_usage: Optional[bool] = None diff --git a/src/axolotl/utils/config/models/internals/__init__.py b/src/axolotl/utils/config/models/internals/__init__.py index 7b4a12e035..692dee8334 100644 --- a/src/axolotl/utils/config/models/internals/__init__.py +++ b/src/axolotl/utils/config/models/internals/__init__.py @@ -1,4 +1,5 @@ """module for gpu capabilities""" + from typing import Optional from pydantic import BaseModel, Field diff --git a/src/axolotl/utils/data/__init__.py b/src/axolotl/utils/data/__init__.py index 7f90bf3cb5..8dedcbe690 100644 --- a/src/axolotl/utils/data/__init__.py +++ b/src/axolotl/utils/data/__init__.py @@ -1,6 +1,7 @@ """ Data processing modules """ + from axolotl.utils.data.pretraining import ( # noqa: F401 encode_pretraining, wrap_pretraining_dataset, diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index 7d6cd597a6..7c671abfe5 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -1,6 +1,7 @@ """ utility helpers for distributed checks """ + import os import pickle # nosec from contextlib import contextmanager diff --git a/src/axolotl/utils/environment.py b/src/axolotl/utils/environment.py index 381fec84c5..1cc609a68d 100644 --- a/src/axolotl/utils/environment.py +++ b/src/axolotl/utils/environment.py @@ -1,10 +1,13 @@ """ utils to get GPU info for the current environment """ + from accelerate.utils.environment import ( check_cuda_p2p_ib_support as accelerate_check_cuda_p2p_ib_support, ) -from accelerate.utils.environment import get_gpu_info +from accelerate.utils.environment import ( + get_gpu_info, +) def check_cuda_p2p_ib_support(): diff --git a/src/axolotl/utils/freeze.py b/src/axolotl/utils/freeze.py index f96bc120d5..7199eaa364 100644 --- a/src/axolotl/utils/freeze.py +++ b/src/axolotl/utils/freeze.py @@ -1,6 +1,7 @@ """ module to freeze/unfreeze parameters by name """ + import logging import re from typing import Callable, List, Tuple, Union diff --git a/src/axolotl/utils/gradient_checkpointing/__init__.py b/src/axolotl/utils/gradient_checkpointing/__init__.py index 8bbf878ad6..62fd34b59c 100644 --- a/src/axolotl/utils/gradient_checkpointing/__init__.py +++ b/src/axolotl/utils/gradient_checkpointing/__init__.py @@ -1,4 +1,5 @@ """custom checkpointing utils""" + from axolotl.utils.gradient_checkpointing.unsloth import ( Unsloth_Offloaded_Gradient_Checkpointer, ) diff --git a/src/axolotl/utils/model_shard_quant.py b/src/axolotl/utils/model_shard_quant.py index ecbe86613d..5c5006eda3 100644 --- a/src/axolotl/utils/model_shard_quant.py +++ b/src/axolotl/utils/model_shard_quant.py @@ -1,6 +1,7 @@ """ module to handle loading model on cpu/meta device for FSDP """ + import os import time from typing import List, Optional, Type, Union @@ -45,13 +46,13 @@ def _replace_linear( if isinstance(module, torch.nn.Linear) and name not in skip_modules: if issubclass(linear_replacement, Linear4bit): - model._modules[ # pylint: disable=protected-access - name - ] = linear_replacement( - module.in_features, - module.out_features, - module.bias is not None, - **kwargs, + model._modules[name] = ( # pylint: disable=protected-access + linear_replacement( + module.in_features, + module.out_features, + module.bias is not None, + **kwargs, + ) ) else: raise ValueError( diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 93d0f13c0d..44f570b883 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -741,9 +741,9 @@ def set_quantization_config(self) -> None: ) else: if self.cfg.gptq_disable_exllama is not None: - self.model_config.quantization_config[ - "disable_exllama" - ] = self.cfg.gptq_disable_exllama + self.model_config.quantization_config["disable_exllama"] = ( + self.cfg.gptq_disable_exllama + ) self.model_kwargs["quantization_config"] = GPTQConfig( **self.model_config.quantization_config ) diff --git a/src/axolotl/utils/optimizers/adopt.py b/src/axolotl/utils/optimizers/adopt.py index 36217730b3..6f064abbf5 100644 --- a/src/axolotl/utils/optimizers/adopt.py +++ b/src/axolotl/utils/optimizers/adopt.py @@ -4,6 +4,7 @@ ADOPT: Modified Adam Can Converge with Any β2 with the Optimal Rate (2024) Taniguchi, Shohei and Harada, Keno and Minegishi, Gouki and Oshima, Yuta and Jeong, Seong Cheol and Nagahara, Go and Iiyama, Tomoshi and Suzuki, Masahiro and Iwasawa, Yusuke and Matsuo, Yutaka """ + # mypy: ignore-errors # pylint: skip-file # flake8: noqa diff --git a/src/axolotl/utils/samplers/__init__.py b/src/axolotl/utils/samplers/__init__.py index 96e00a5d26..eb6dc8ab9f 100644 --- a/src/axolotl/utils/samplers/__init__.py +++ b/src/axolotl/utils/samplers/__init__.py @@ -1,5 +1,6 @@ """ axolotl samplers module """ + from .multipack import MultipackBatchSampler # noqa: F401 from .utils import get_dataset_lengths # noqa: F401 diff --git a/src/axolotl/utils/samplers/utils.py b/src/axolotl/utils/samplers/utils.py index 09f1b081c4..a93e847488 100755 --- a/src/axolotl/utils/samplers/utils.py +++ b/src/axolotl/utils/samplers/utils.py @@ -1,6 +1,7 @@ """ helper util to calculate dataset lengths """ + import numpy as np diff --git a/src/axolotl/utils/schedulers.py b/src/axolotl/utils/schedulers.py index 6f057fbd9a..e9989b1afc 100644 --- a/src/axolotl/utils/schedulers.py +++ b/src/axolotl/utils/schedulers.py @@ -1,4 +1,5 @@ """Module for custom LRScheduler class""" + import math from functools import partial diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 8cee3d1242..090e677a67 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -538,9 +538,9 @@ def setup_fsdp_envs(cfg): if cfg.fsdp_config.fsdp_auto_wrap_policy: os.environ["FSDP_AUTO_WRAP_POLICY"] = cfg.fsdp_config.fsdp_auto_wrap_policy if cfg.fsdp_config.fsdp_transformer_layer_cls_to_wrap: - os.environ[ - "FSDP_TRANSFORMER_CLS_TO_WRAP" - ] = cfg.fsdp_config.fsdp_transformer_layer_cls_to_wrap + os.environ["FSDP_TRANSFORMER_CLS_TO_WRAP"] = ( + cfg.fsdp_config.fsdp_transformer_layer_cls_to_wrap + ) def prepare_optim_env(cfg): diff --git a/src/setuptools_axolotl_dynamic_dependencies.py b/src/setuptools_axolotl_dynamic_dependencies.py index 2f10efde68..02a5b8083f 100644 --- a/src/setuptools_axolotl_dynamic_dependencies.py +++ b/src/setuptools_axolotl_dynamic_dependencies.py @@ -1,6 +1,7 @@ """ dynamic requirements for axolotl """ + import platform import re from importlib.metadata import PackageNotFoundError, version diff --git a/tests/cli/test_cli_merge_sharded_fsdp_weights.py b/tests/cli/test_cli_merge_sharded_fsdp_weights.py index 18589a80d9..ec96b4ed4f 100644 --- a/tests/cli/test_cli_merge_sharded_fsdp_weights.py +++ b/tests/cli/test_cli_merge_sharded_fsdp_weights.py @@ -1,4 +1,5 @@ """pytest tests for axolotl CLI merge_sharded_fsdp_weights command.""" + # pylint: disable=duplicate-code from unittest.mock import patch diff --git a/tests/cli/test_cli_sweeps.py b/tests/cli/test_cli_sweeps.py index 61c886e80a..40b3607178 100644 --- a/tests/cli/test_cli_sweeps.py +++ b/tests/cli/test_cli_sweeps.py @@ -1,6 +1,7 @@ """ unit tests for generating sweep configurations """ + from axolotl.cli.main import generate_sweep_configs diff --git a/tests/cli/test_utils.py b/tests/cli/test_utils.py index ecb0025e44..2dab5bba9c 100644 --- a/tests/cli/test_utils.py +++ b/tests/cli/test_utils.py @@ -1,4 +1,5 @@ """pytest tests for axolotl CLI utils.""" + # pylint: disable=redefined-outer-name import json diff --git a/tests/conftest.py b/tests/conftest.py index 2505e6de91..75b12a036e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ """ shared pytest fixtures """ + import functools import importlib import shutil diff --git a/tests/core/chat/test_messages.py b/tests/core/chat/test_messages.py index b3be56c590..6a69d74e75 100644 --- a/tests/core/chat/test_messages.py +++ b/tests/core/chat/test_messages.py @@ -1,6 +1,7 @@ """ Tests for the chat messages module """ + import unittest import pytest diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py index a90b48d67c..4f8cde1d7b 100644 --- a/tests/e2e/integrations/test_kd.py +++ b/tests/e2e/integrations/test_kd.py @@ -1,6 +1,7 @@ """ e2e tests for kd trainer support in Axolotl """ + from pathlib import Path import pytest diff --git a/tests/e2e/kernels/test_geglu.py b/tests/e2e/kernels/test_geglu.py index c720bbce7b..005a1935d6 100644 --- a/tests/e2e/kernels/test_geglu.py +++ b/tests/e2e/kernels/test_geglu.py @@ -1,4 +1,5 @@ """Tests for GEGLU activation function Triton kernels.""" + # pylint: disable=duplicate-code import torch diff --git a/tests/e2e/kernels/test_lora.py b/tests/e2e/kernels/test_lora.py index c8becf2da8..5ad186cbfb 100644 --- a/tests/e2e/kernels/test_lora.py +++ b/tests/e2e/kernels/test_lora.py @@ -1,4 +1,5 @@ """Tests for LoRA custom autograd.""" + # pylint: disable=invalid-name,redefined-outer-name import pytest diff --git a/tests/e2e/kernels/test_quantize.py b/tests/e2e/kernels/test_quantize.py index e4beb846e9..ea91407ef5 100644 --- a/tests/e2e/kernels/test_quantize.py +++ b/tests/e2e/kernels/test_quantize.py @@ -1,4 +1,5 @@ """Tests for quantization utility functions.""" + # pylint: disable=invalid-name import torch diff --git a/tests/e2e/kernels/test_swiglu.py b/tests/e2e/kernels/test_swiglu.py index 3717402de1..60fdafb79b 100644 --- a/tests/e2e/kernels/test_swiglu.py +++ b/tests/e2e/kernels/test_swiglu.py @@ -1,4 +1,5 @@ """Tests for SwiGLU activation function Triton kernels.""" + # pylint: disable=duplicate-code import torch diff --git a/tests/e2e/multigpu/test_eval.py b/tests/e2e/multigpu/test_eval.py index 09561bf265..586da8577c 100644 --- a/tests/e2e/multigpu/test_eval.py +++ b/tests/e2e/multigpu/test_eval.py @@ -1,6 +1,7 @@ """ E2E tests for multigpu eval """ + import logging import os from pathlib import Path diff --git a/tests/e2e/multigpu/test_grpo.py b/tests/e2e/multigpu/test_grpo.py index d2b84994bf..bb99581ad9 100644 --- a/tests/e2e/multigpu/test_grpo.py +++ b/tests/e2e/multigpu/test_grpo.py @@ -1,6 +1,7 @@ """ GRPO test suite """ + import random from pathlib import Path diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index 4e33733673..89f80951b4 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -1,4 +1,5 @@ """Integration tests for LoRA activation and attention kernels.""" + # pylint: disable=redefined-outer-name import pytest diff --git a/tests/e2e/patched/test_cli_integrations.py b/tests/e2e/patched/test_cli_integrations.py index ce9396d5ff..6c908faf10 100644 --- a/tests/e2e/patched/test_cli_integrations.py +++ b/tests/e2e/patched/test_cli_integrations.py @@ -1,6 +1,7 @@ """ test cases to make sure the plugin args are loaded from the config file """ + from pathlib import Path import yaml diff --git a/tests/e2e/patched/test_unsloth_integration.py b/tests/e2e/patched/test_unsloth_integration.py index 403d261478..4cd97c894d 100644 --- a/tests/e2e/patched/test_unsloth_integration.py +++ b/tests/e2e/patched/test_unsloth_integration.py @@ -1,4 +1,5 @@ """Test module for checking whether the integration of Unsloth with Hugging Face Transformers is working as expected.""" + import unittest import pytest diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index da5eaffb66..4cea0d26fa 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -1,6 +1,7 @@ """ e2e tests for unsloth qlora """ + import logging import os diff --git a/tests/e2e/test_imports.py b/tests/e2e/test_imports.py index f186eaac46..fc08434798 100644 --- a/tests/e2e/test_imports.py +++ b/tests/e2e/test_imports.py @@ -1,6 +1,7 @@ """ test module to import various submodules that have historically broken due to dependency issues """ + import unittest diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index ff96f1f58f..2b218fbf5e 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -1,6 +1,7 @@ """ helper utils for tests """ + import os import shutil import tempfile diff --git a/tests/integrations/test_liger.py b/tests/integrations/test_liger.py index c75bc13054..cbe1408b81 100644 --- a/tests/integrations/test_liger.py +++ b/tests/integrations/test_liger.py @@ -1,6 +1,7 @@ """ config validation tests for swiglu args """ + # pylint: disable=duplicate-code import logging from typing import Optional diff --git a/tests/monkeypatch/test_llama_attn_hijack_flash.py b/tests/monkeypatch/test_llama_attn_hijack_flash.py index 4521cd07bc..08425d4dc0 100644 --- a/tests/monkeypatch/test_llama_attn_hijack_flash.py +++ b/tests/monkeypatch/test_llama_attn_hijack_flash.py @@ -1,6 +1,7 @@ """ Unit tests for the monkeypatch utils """ + import unittest import torch diff --git a/tests/prompt_strategies/messages/test_chat.py b/tests/prompt_strategies/messages/test_chat.py index 96c4b6cbbf..2681bb7431 100644 --- a/tests/prompt_strategies/messages/test_chat.py +++ b/tests/prompt_strategies/messages/test_chat.py @@ -1,6 +1,7 @@ """ tests for chat_template prompt strategy """ + # pylint: disable=duplicate-code import logging import unittest diff --git a/tests/prompt_strategies/test_alpaca.py b/tests/prompt_strategies/test_alpaca.py index 51dd5900bb..9e425e0dfb 100644 --- a/tests/prompt_strategies/test_alpaca.py +++ b/tests/prompt_strategies/test_alpaca.py @@ -1,6 +1,7 @@ """ Test module for alpaca integration w chatml """ + import pytest from datasets import Dataset from tokenizers import AddedToken diff --git a/tests/prompt_strategies/test_chat_template_utils.py b/tests/prompt_strategies/test_chat_template_utils.py index b63c9aa179..66bcb547dd 100644 --- a/tests/prompt_strategies/test_chat_template_utils.py +++ b/tests/prompt_strategies/test_chat_template_utils.py @@ -1,6 +1,7 @@ """ Tests for utils in axolotl.utils.chat_templates """ + import unittest import pytest diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index 7f3096eb06..69031bd659 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -920,9 +920,11 @@ def test_get_chat_template_variables( ) variables = prompter.get_chat_template_msg_variables( - actual_jinja_template - if actual_jinja_template - else actual_tokenizer.get_chat_template(), + ( + actual_jinja_template + if actual_jinja_template + else actual_tokenizer.get_chat_template() + ), "messages", ) diff --git a/tests/prompt_strategies/test_dpo_chatml.py b/tests/prompt_strategies/test_dpo_chatml.py index 34c29275b8..93793b2c55 100644 --- a/tests/prompt_strategies/test_dpo_chatml.py +++ b/tests/prompt_strategies/test_dpo_chatml.py @@ -1,6 +1,7 @@ """ Tests for loading DPO preference datasets with chatml formatting """ + import unittest import pytest diff --git a/tests/prompt_strategies/test_jinja_template_analyzer.py b/tests/prompt_strategies/test_jinja_template_analyzer.py index 004f810999..f666c738c9 100644 --- a/tests/prompt_strategies/test_jinja_template_analyzer.py +++ b/tests/prompt_strategies/test_jinja_template_analyzer.py @@ -1,6 +1,7 @@ """ tests for jinja_template_analyzer """ + import logging import pytest diff --git a/tests/prompt_strategies/test_raw_io.py b/tests/prompt_strategies/test_raw_io.py index 967de169f3..082e31ee62 100644 --- a/tests/prompt_strategies/test_raw_io.py +++ b/tests/prompt_strategies/test_raw_io.py @@ -1,6 +1,7 @@ """ Test module for raw i/o data for prompts """ + import pytest from datasets import Dataset from tokenizers import AddedToken diff --git a/tests/test_data.py b/tests/test_data.py index e156e1f3c5..141f3ed213 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1,6 +1,7 @@ """ test module for the axolotl.utils.data module """ + import unittest from transformers import LlamaTokenizer diff --git a/tests/test_dict.py b/tests/test_dict.py index 2007cb085e..0bcf8ca7bf 100644 --- a/tests/test_dict.py +++ b/tests/test_dict.py @@ -1,6 +1,5 @@ """Module for testing DictDefault class""" - import unittest import pytest diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index bc0734ed3c..3fc315b2ea 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -3,6 +3,7 @@ Additionally, this test suite includes tests for functions that indirectly call deduplicate_and_log_datasets during the execution of the preprocess command. """ + import hashlib import unittest from unittest.mock import patch @@ -386,11 +387,11 @@ def setUp(self): @patch( "axolotl.utils.data.utils.sha256", - side_effect=lambda x: hashlib.sha256( - "forced_collision_hash".encode("utf-8") - ).hexdigest() - if "sample 5" in x - else hashlib.sha256(x.encode("utf-8")).hexdigest(), + side_effect=lambda x: ( + hashlib.sha256("forced_collision_hash".encode("utf-8")).hexdigest() + if "sample 5" in x + else hashlib.sha256(x.encode("utf-8")).hexdigest() + ), ) def test_deduplication_wrong_collision_train_eval(self, _mock_sha256): dedup_train, dedup_eval, _ = deduplicate_and_log_datasets( diff --git a/tests/test_expand_mask.py b/tests/test_expand_mask.py index 01241c2958..1c69ca234f 100644 --- a/tests/test_expand_mask.py +++ b/tests/test_expand_mask.py @@ -1,6 +1,7 @@ """ Unit tests for the monkey patch for expand mask to handle packed sequences """ + import unittest import torch diff --git a/tests/test_lora.py b/tests/test_lora.py index b917ff3f95..540371bef4 100644 --- a/tests/test_lora.py +++ b/tests/test_lora.py @@ -1,6 +1,7 @@ """ tests for loading loras """ + from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_model, load_tokenizer diff --git a/tests/test_normalize_config.py b/tests/test_normalize_config.py index 0d663183d4..c8ca3e5506 100644 --- a/tests/test_normalize_config.py +++ b/tests/test_normalize_config.py @@ -1,6 +1,7 @@ """ Test classes for checking functionality of the cfg normalization """ + import unittest from unittest.mock import patch diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index b52320e2aa..55a0afaece 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -1,4 +1,5 @@ """Module for testing streaming dataset sequence packing""" + import pytest from datasets import concatenate_datasets, load_dataset from torch.utils.data import DataLoader, RandomSampler diff --git a/tests/test_packed_pretraining.py b/tests/test_packed_pretraining.py index 9f9ae60fb5..71c9a6861d 100644 --- a/tests/test_packed_pretraining.py +++ b/tests/test_packed_pretraining.py @@ -1,4 +1,5 @@ """Module for testing streaming dataset sequence packing""" + import functools import unittest diff --git a/tests/test_perplexity.py b/tests/test_perplexity.py index b32cd52835..9a1c9b223f 100644 --- a/tests/test_perplexity.py +++ b/tests/test_perplexity.py @@ -1,4 +1,5 @@ """unit tests for perplexity eval callback""" + # pylint: disable=redefined-outer-name from pytest import fixture diff --git a/tests/test_schedulers.py b/tests/test_schedulers.py index bd37bf01df..92664cca81 100644 --- a/tests/test_schedulers.py +++ b/tests/test_schedulers.py @@ -1,6 +1,7 @@ """ test module for the axolotl.utis.data module """ + import unittest import torch From 61825a464aeaea089448dfafc4714916b87e9207 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 21 Mar 2025 22:59:22 +0700 Subject: [PATCH 0480/1405] chore(doc): add explanation on fsdp_transformer_layer_cls_to_wrap (#2429) [skip ci] --- docs/faq.qmd | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/faq.qmd b/docs/faq.qmd index acec1886ed..1ce14681a1 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -37,6 +37,10 @@ description: Frequently asked questions > A: Yes, since Axolotl is just Python, please see `src/axolotl/cli/main.py` on how each command is called. +**Q: How to know the value to use for `fsdp_transformer_layer_cls_to_wrap`?** + +> A: This is the class name of the transformer layer to wrap with FSDP. For example, for `LlamaForCausalLM`, the value is `LlamaDecoderLayer`. To find this for a specific model, check the model's `PreTrainedModel` definition and look for `_no_split_modules` variable in the `modeling_.py` file within `transformers` library. + ### Chat templates **Q: `jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'content' / 'role' / ____`** From 113e9cd19323d66f38013616be54cdb4b3a22393 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 21 Mar 2025 12:26:47 -0400 Subject: [PATCH 0481/1405] Autodoc generation with quartodoc (#2419) * quartodoc integration * quartodoc progress * deletions * Update docs/.gitignore to exclude auto-generated API documentation files * Fix * more autodoc progress * moving reference up near the top of the sidebar * fix broken link * update to reflect recent changes * pydantic models refactor + add to autodoc + fixes * fix * shrinking header sizes * fix accidental change * include quartodoc build step * update pre-commit version * update pylint * pre-commit --------- Co-authored-by: Dan Saunders --- .github/workflows/docs.yml | 6 +- .gitignore | 4 + README.md | 1 + _quarto.yml | 193 +++ docs/.gitignore | 2 + docs/cli.qmd | 2 +- docs/dataset_preprocessing.qmd | 2 +- requirements-dev.txt | 2 + src/axolotl/cli/main.py | 2 +- src/axolotl/core/trainer_builder.py | 6 +- src/axolotl/core/trainers/grpo/__init__.py | 2 +- src/axolotl/evaluate.py | 23 +- src/axolotl/integrations/config.py | 10 +- .../prompt_strategies/chat_template.py | 2 +- .../prompt_strategies/dpo/chat_template.py | 2 +- src/axolotl/utils/callbacks/__init__.py | 2 +- src/axolotl/utils/config/__init__.py | 16 +- .../config/models/input/next/__init__.py | 0 .../models/input => schemas}/__init__.py | 0 .../v0_4_1/__init__.py => schemas/config.py} | 1145 ++++------------- src/axolotl/utils/schemas/datasets.py | 165 +++ src/axolotl/utils/schemas/deprecated.py | 68 + src/axolotl/utils/schemas/enums.py | 49 + src/axolotl/utils/schemas/integrations.py | 108 ++ .../internal}/__init__.py | 0 src/axolotl/utils/schemas/model.py | 55 + src/axolotl/utils/schemas/peft.py | 132 ++ src/axolotl/utils/schemas/training.py | 99 ++ .../models/input/v0_4_1 => schemas}/trl.py | 34 +- src/axolotl/utils/schemas/utils.py | 79 ++ styles.css | 90 +- tests/patched/test_validation.py | 2 +- tests/test_validation_dataset.py | 2 +- tests/utils/test_models.py | 2 +- 34 files changed, 1324 insertions(+), 983 deletions(-) delete mode 100644 src/axolotl/utils/config/models/input/next/__init__.py rename src/axolotl/utils/{config/models/input => schemas}/__init__.py (100%) rename src/axolotl/utils/{config/models/input/v0_4_1/__init__.py => schemas/config.py} (52%) create mode 100644 src/axolotl/utils/schemas/datasets.py create mode 100644 src/axolotl/utils/schemas/deprecated.py create mode 100644 src/axolotl/utils/schemas/enums.py create mode 100644 src/axolotl/utils/schemas/integrations.py rename src/axolotl/utils/{config/models/internals => schemas/internal}/__init__.py (100%) create mode 100644 src/axolotl/utils/schemas/model.py create mode 100644 src/axolotl/utils/schemas/peft.py create mode 100644 src/axolotl/utils/schemas/training.py rename src/axolotl/utils/{config/models/input/v0_4_1 => schemas}/trl.py (76%) create mode 100644 src/axolotl/utils/schemas/utils.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1138d99f10..7ddeccff56 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,9 +20,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.11' - - name: install dependencies + - name: Install dependencies run: | - python3 -m pip install jupyter + python3 -m pip install jupyter quartodoc + - name: Build autodoc + run: quartodoc build - name: Publish to GitHub Pages (and render) uses: quarto-dev/quarto-actions/publish@v2 with: diff --git a/.gitignore b/.gitignore index 7b604d88c7..40084b4086 100644 --- a/.gitignore +++ b/.gitignore @@ -181,6 +181,10 @@ prepared-datasets/ submit.sh *.out* +# Quartodoc generated files +objects.json +site_libs/ + typings/ out/ diff --git a/README.md b/README.md index 343816aff4..ed2c9e6b1f 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ That's it! Check out our [Getting Started Guide](https://axolotl-ai-cloud.github - [Multi-GPU Training](https://axolotl-ai-cloud.github.io/axolotl/docs/multi-gpu.html) - [Multi-Node Training](https://axolotl-ai-cloud.github.io/axolotl/docs/multi-node.html) - [Multipacking](https://axolotl-ai-cloud.github.io/axolotl/docs/multipack.html) +- [API Reference](https://axolotl-ai-cloud.github.io/axolotl/docs/api/) - Auto-generated code documentation - [FAQ](https://axolotl-ai-cloud.github.io/axolotl/docs/faq.html) - Frequently asked questions ## 🤝 Getting Help diff --git a/_quarto.yml b/_quarto.yml index 943ed5293e..c564fb0dd1 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -1,6 +1,178 @@ project: type: website +quartodoc: + dir: docs/api + package: axolotl + title: API Reference + parser: google + + sections: + - title: Core + desc: Core functionality for training + contents: + - train + - evaluate + - datasets + - convert + - prompt_tokenizers + - logging_config + - core.trainer_builder + - core.training_args + - core.chat.messages + - core.chat.format.chatml + - core.chat.format.llama3x + - core.chat.format.shared + - core.datasets.chat + - core.datasets.transforms.chat_builder + - title: CLI + desc: Command-line interface + contents: + - cli.main + - cli.train + - cli.evaluate + - cli.args + - cli.checks + - cli.config + - cli.inference + - cli.merge_lora + - cli.merge_sharded_fsdp_weights + - cli.preprocess + - cli.sweeps + - cli.utils + - cli.cloud.base + - cli.cloud.modal_ + - title: Trainers + desc: Training implementations + contents: + - core.trainers.base + - core.trainers.trl + - core.trainers.dpo.trainer + - core.trainers.grpo.trainer + - title: Prompt Strategies + desc: Prompt formatting strategies + contents: + - prompt_strategies.base + - prompt_strategies.chat_template + - prompt_strategies.alpaca_chat + - prompt_strategies.alpaca_instruct + - prompt_strategies.alpaca_w_system + - prompt_strategies.user_defined + - prompt_strategies.llama2_chat + - prompt_strategies.completion + - prompt_strategies.input_output + - prompt_strategies.stepwise_supervised + - prompt_strategies.metharme + - prompt_strategies.orcamini + - prompt_strategies.pygmalion + - prompt_strategies.messages.chat + - prompt_strategies.dpo.chat_template + - prompt_strategies.dpo.llama3 + - prompt_strategies.dpo.chatml + - prompt_strategies.dpo.zephyr + - prompt_strategies.dpo.user_defined + - prompt_strategies.dpo.passthrough + - prompt_strategies.kto.llama3 + - prompt_strategies.kto.chatml + - prompt_strategies.kto.user_defined + - prompt_strategies.orpo.chat_template + - prompt_strategies.bradley_terry.llama3 + - title: Kernels + desc: Low-level performance optimizations + contents: + - kernels.lora + - kernels.geglu + - kernels.swiglu + - kernels.quantize + - kernels.utils + - title: MonkeyPatches + desc: Runtime patches for model optimizations + contents: + - monkeypatch.llama_attn_hijack_flash + - monkeypatch.llama_attn_hijack_xformers + - monkeypatch.mistral_attn_hijack_flash + - monkeypatch.multipack + - monkeypatch.relora + - monkeypatch.llama_expand_mask + - monkeypatch.lora_kernels + - monkeypatch.utils + - monkeypatch.btlm_attn_hijack_flash + - monkeypatch.llama_patch_multipack + - monkeypatch.stablelm_attn_hijack_flash + - monkeypatch.trainer_fsdp_optim + - monkeypatch.transformers_fa_utils + - monkeypatch.unsloth_ + - monkeypatch.attention.mllama + - monkeypatch.data.batch_dataset_fetcher + - monkeypatch.mixtral + - title: Utils + desc: Utility functions + contents: + - utils.models + - utils.tokenization + - utils.chat_templates + - utils.lora + - utils.lora_embeddings + - utils.model_shard_quant + - utils.bench + - utils.freeze + - utils.trainer + - utils.schedulers + - utils.distributed + - utils.dict + - utils.optimizers.adopt + - utils.data.pretraining + - utils.data.sft + - utils.gradient_checkpointing.unsloth + - title: Schemas + desc: Pydantic data models for Axolotl config + contents: + - utils.schemas.config + - utils.schemas.model + - utils.schemas.training + - utils.schemas.datasets + - utils.schemas.peft + - utils.schemas.trl + - utils.schemas.integrations + - utils.schemas.enums + - utils.schemas.utils + - title: Integrations + desc: Third-party integrations and extensions + contents: + - integrations.base + - integrations.cut_cross_entropy.args + - integrations.grokfast.optimizer + - integrations.kd.trainer + - integrations.liger.args + - integrations.lm_eval.args + - integrations.spectrum.args + - title: Common + desc: Common utilities and shared functionality + contents: + - common.architectures + - common.const + - common.datasets + - title: Models + desc: Custom model implementations + contents: + - models.mamba.modeling_mamba + - title: Data Processing + desc: Data processing utilities + contents: + - utils.collators.core + - utils.collators.batching + - utils.collators.mamba + - utils.collators.mm_chat + - utils.samplers.multipack + - title: Callbacks + desc: Training callbacks + contents: + - utils.callbacks.perplexity + - utils.callbacks.profiler + - utils.callbacks.lisa + - utils.callbacks.mlflow_ + - utils.callbacks.comet_ + website: title: "Axolotl" description: "We make fine-tuning accessible, scalable, and fun" @@ -35,6 +207,8 @@ website: - docs/inference.qmd - docs/cli.qmd - docs/config.qmd + - text: "API Reference" + href: docs/api - section: "Dataset Formats" contents: docs/dataset-formats/* @@ -80,3 +254,22 @@ format: theme: darkly css: styles.css toc: true + # Enable better handling of line breaks in markdown + preserve-tabs: true + html-math-method: mathjax + # Improved markdown processing options + md-extensions: + - markdown_it + - def_list + - attr_list + - fenced_divs + - tables + - html_admonition + - lineblocks + - fancy_lists + # Control whitespace handling + whitespace: preserve + # Process newlines in paragraphs + wrap: preserve + # Better line break handling + preserve-linebreaks: true diff --git a/docs/.gitignore b/docs/.gitignore index 4c23a061fa..6c3cb2070e 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,2 +1,4 @@ /.quarto/ _site/ +/api/*.qmd +/api/*.html diff --git a/docs/cli.qmd b/docs/cli.qmd index a57e54d9a9..a3d5cf939d 100644 --- a/docs/cli.qmd +++ b/docs/cli.qmd @@ -1,5 +1,5 @@ --- -title: "CLI Reference" +title: "Command Line Interface (CLI)" format: html: toc: true diff --git a/docs/dataset_preprocessing.qmd b/docs/dataset_preprocessing.qmd index 1075dc8e56..245723e677 100644 --- a/docs/dataset_preprocessing.qmd +++ b/docs/dataset_preprocessing.qmd @@ -6,7 +6,7 @@ description: How datasets are processed ## Overview Dataset pre-processing is the step where Axolotl takes each dataset you've configured alongside -the [dataset format](docs/dataset-formats) and prompt strategies to: +the [dataset format](dataset-formats) and prompt strategies to: - parse the dataset based on the *dataset format* - transform the dataset to how you would interact with the model based on the *prompt strategy* diff --git a/requirements-dev.txt b/requirements-dev.txt index 4b5df167b6..9f523de54d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,5 @@ pre-commit black mypy types-requests +quartodoc +jupyter diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index f46c8efe25..f53ea825a8 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -25,7 +25,7 @@ ) from axolotl.integrations.lm_eval.cli import lm_eval from axolotl.utils import set_pytorch_cuda_alloc_conf -from axolotl.utils.config.models.input.v0_4_1 import AxolotlInputConfig +from axolotl.utils.schemas.config import AxolotlInputConfig @click.group() diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 19b947fb2e..83bfb1c83a 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -13,9 +13,7 @@ # limitations under the License. # pylint: disable=too-many-lines -""" -Builder for the training args and trainer -""" +"""Builder for the training args and trainer""" import abc import importlib @@ -85,8 +83,8 @@ V2BatchSamplerDataCollatorForSeq2Seq, ) from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator -from axolotl.utils.config.models.input.v0_4_1 import CustomSupportedOptimizers from axolotl.utils.models import ensure_dtype +from axolotl.utils.schemas.enums import CustomSupportedOptimizers try: import torch._dynamo # pylint: disable=ungrouped-imports diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 52e6363a2e..f0c42830d0 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -9,7 +9,7 @@ from trl.trainer.grpo_trainer import RewardFunc from axolotl.core.trainers.grpo.trainer import AxolotlGRPOTrainer -from axolotl.utils.config.models.input.v0_4_1.trl import TRLConfig +from axolotl.utils.schemas.trl import TRLConfig LOG = logging.getLogger("axolotl") diff --git a/src/axolotl/evaluate.py b/src/axolotl/evaluate.py index 8d9ddc6abf..f3be9c2f45 100644 --- a/src/axolotl/evaluate.py +++ b/src/axolotl/evaluate.py @@ -8,6 +8,8 @@ import torch from accelerate.logging import get_logger +from datasets import Dataset +from transformers.trainer import Trainer from axolotl.logging_config import configure_logging from axolotl.train import TrainDatasetMeta @@ -25,18 +27,18 @@ def evaluate_dataset( - trainer, dataset, dataset_type: str, flash_optimum: bool = False + trainer: Trainer, dataset: Dataset, dataset_type: str, flash_optimum: bool = False ) -> Optional[Dict[str, float]]: - """Helper function to evaluate a single dataset safely. + """Helper function to evaluate a single dataset. Args: - trainer: The trainer instance - dataset: Dataset to evaluate - dataset_type: Type of dataset ('train' or 'eval') - flash_optimum: Whether to use flash optimum + trainer: The trainer instance. + dataset: Dataset to evaluate. + dataset_type: Type of dataset ('train' or 'eval'). + flash_optimum: Whether to use flash optimum. Returns: - Dictionary of metrics or None if dataset is None + Dictionary of metrics or None if dataset is None. """ if dataset is None: return None @@ -63,17 +65,14 @@ def evaluate_dataset( def evaluate(*, cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> Dict[str, float]: """ - Evaluate a model on training and validation datasets + Evaluate a model on training and validation datasets. Args: cfg: Dictionary mapping `axolotl` config keys to values. dataset_meta: Dataset metadata containing training and evaluation datasets. Returns: - Tuple containing: - - The model (either PeftModel or PreTrainedModel) - - The tokenizer - - Dictionary of evaluation metrics + Dictionary mapping metric names to their values. """ # pylint: disable=duplicate-code # Enable expandable segments for cuda allocation to improve VRAM usage diff --git a/src/axolotl/integrations/config.py b/src/axolotl/integrations/config.py index b4ffd6758f..b443f228e4 100644 --- a/src/axolotl/integrations/config.py +++ b/src/axolotl/integrations/config.py @@ -11,19 +11,17 @@ # the License. """ -module to handle merging the plugins' input arguments with the base configurations. +Module to handle merging the plugins' input arguments with the base configurations. -this was moved here to prevent circular imports +This was moved here to prevent circular imports. """ from typing import Any, Dict, List -from axolotl.utils.config.models.input.v0_4_1 import ( +from axolotl.utils.schemas.config import ( AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, ) -from axolotl.utils.config.models.input.v0_4_1 import ( - AxolotlInputConfig as AxolotlInputConfigBase, -) +from axolotl.utils.schemas.config import AxolotlInputConfig as AxolotlInputConfigBase def merge_input_args(): diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index af1d51a46b..4266e0c996 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -13,7 +13,7 @@ from axolotl.prompt_tokenizers import PromptTokenizingStrategy from axolotl.prompters import IGNORE_TOKEN_ID, Prompter from axolotl.utils.chat_templates import get_chat_template_from_config -from axolotl.utils.config.models.input.v0_4_1 import DatasetConfig +from axolotl.utils.schemas.datasets import DatasetConfig # Configure the logger LOG = logging.getLogger("axolotl") diff --git a/src/axolotl/prompt_strategies/dpo/chat_template.py b/src/axolotl/prompt_strategies/dpo/chat_template.py index 9e4986dd4c..f04bd7f0d2 100644 --- a/src/axolotl/prompt_strategies/dpo/chat_template.py +++ b/src/axolotl/prompt_strategies/dpo/chat_template.py @@ -3,7 +3,7 @@ """ from axolotl.utils.chat_templates import extract_chat_template_args, get_chat_template -from axolotl.utils.config.models.input.v0_4_1 import handle_legacy_message_fields_logic +from axolotl.utils.schemas.utils import handle_legacy_message_fields_logic def default( diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 47c77619a8..94f180ef46 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -33,7 +33,6 @@ from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.callbacks.perplexity import Perplexity -from axolotl.utils.config.models.input.v0_4_1 import AxolotlInputConfig from axolotl.utils.distributed import ( barrier, broadcast_dict, @@ -43,6 +42,7 @@ is_main_process, zero_first, ) +from axolotl.utils.schemas.config import AxolotlInputConfig if TYPE_CHECKING: from axolotl.core.trainer_builder import AxolotlTrainingArguments diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index b7096eeabb..136acc4a06 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -12,19 +12,13 @@ from axolotl.integrations.base import PluginManager from axolotl.integrations.config import merge_input_args from axolotl.utils.bench import log_gpu_memory_usage -from axolotl.utils.config.models.input.v0_4_1 import ( - AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, -) -from axolotl.utils.config.models.input.v0_4_1 import ( - AxolotlInputConfig as AxolotlInputConfigBase, -) -from axolotl.utils.config.models.input.v0_4_1 import ( - DPODataset, - KTODataset, - SFTDataset, -) from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_model_config +from axolotl.utils.schemas.config import ( + AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, +) +from axolotl.utils.schemas.config import AxolotlInputConfig as AxolotlInputConfigBase +from axolotl.utils.schemas.datasets import DPODataset, KTODataset, SFTDataset LOG = logging.getLogger("axolotl") diff --git a/src/axolotl/utils/config/models/input/next/__init__.py b/src/axolotl/utils/config/models/input/next/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/axolotl/utils/config/models/input/__init__.py b/src/axolotl/utils/schemas/__init__.py similarity index 100% rename from src/axolotl/utils/config/models/input/__init__.py rename to src/axolotl/utils/schemas/__init__.py diff --git a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py b/src/axolotl/utils/schemas/config.py similarity index 52% rename from src/axolotl/utils/config/models/input/v0_4_1/__init__.py rename to src/axolotl/utils/schemas/config.py index f1c514a7c8..7676a50a83 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/__init__.py +++ b/src/axolotl/utils/schemas/config.py @@ -1,11 +1,10 @@ -"""Module with Pydantic models for configuration.""" +"""Main Axolotl input configuration Pydantic models""" # pylint: disable=too-many-lines import logging import os -from enum import Enum -from typing import Annotated, Any, Dict, List, Literal, Optional, Tuple, Union +from typing import Annotated, Any, Literal from annotated_types import MinLen from packaging import version @@ -17,652 +16,41 @@ field_validator, model_validator, ) -from transformers import SchedulerType -from transformers.training_args import OptimizerNames from transformers.utils.import_utils import is_torch_npu_available -from axolotl.utils.config.models.internals import EnvCapabilities, GPUCapabilities - -from .trl import TRLConfig +from axolotl.utils.schemas.datasets import ( + DatasetConfig, + DPODataset, + KTODataset, + PretrainingDataset, + SFTDataset, + StepwiseSupervisedDataset, +) +from axolotl.utils.schemas.deprecated import DeprecatedParameters, RemappedParameters +from axolotl.utils.schemas.enums import ChatTemplate, RLType +from axolotl.utils.schemas.integrations import ( + CometConfig, + GradioConfig, + LISAConfig, + MLFlowConfig, + RayConfig, + WandbConfig, +) +from axolotl.utils.schemas.internal import EnvCapabilities, GPUCapabilities +from axolotl.utils.schemas.model import ( + ModelInputConfig, + ModelOutputConfig, + SpecialTokensConfig, +) +from axolotl.utils.schemas.peft import LoraConfig, ReLoRAConfig +from axolotl.utils.schemas.training import HyperparametersConfig +from axolotl.utils.schemas.trl import TRLConfig -LOG = logging.getLogger("axolotl.utils.config.models.input") +LOG = logging.getLogger(__name__) SUPPORTED_METRICS = {"sacrebleu", "comet", "ter", "chrf", "perplexity"} -class RLType(str, Enum): - """RL trainer type configuration subset""" - - dpo = "dpo" # pylint: disable=invalid-name - grpo = "grpo" # pylint: disable=invalid-name - ipo = "ipo" # pylint: disable=invalid-name - orpo = "orpo" # pylint: disable=invalid-name - kto = "kto" # pylint: disable=invalid-name - simpo = "simpo" # pylint: disable=invalid-name - - -class ChatTemplate(str, Enum): - """Chat templates configuration subset""" - - alpaca = "alpaca" # pylint: disable=invalid-name - chatml = "chatml" # pylint: disable=invalid-name - mistral_v1 = "mistral_v1" # pylint: disable=invalid-name - mistral_v2v3 = "mistral_v2v3" # pylint: disable=invalid-name - mistral_v3_tekken = "mistral_v3_tekken" # pylint: disable=invalid-name - gemma = "gemma" # pylint: disable=invalid-name - cohere = "cohere" # pylint: disable=invalid-name - llama3 = "llama3" # pylint: disable=invalid-name - llama3_2_vision = "llama3_2_vision" # pylint: disable=invalid-name - phi_3 = "phi_3" # pylint: disable=invalid-name - phi_35 = "phi_35" # pylint: disable=invalid-name - deepseek_v2 = "deepseek_v2" # pylint: disable=invalid-name - deepseek_v3 = "deepseek_v3" # pylint: disable=invalid-name - jamba = "jamba" # pylint: disable=invalid-name - jinja = "jinja" # pylint: disable=invalid-name - qwen_25 = "qwen_25" # pylint: disable=invalid-name - tokenizer_default = "tokenizer_default" # pylint: disable=invalid-name - exaone = "exaone" # pylint: disable=invalid-name - metharme = "metharme" # pylint: disable=invalid-name - - -class CustomSupportedOptimizers(str, Enum): - """Custom supported optimizers""" - - optimi_adamw = "optimi_adamw" # pylint: disable=invalid-name - ao_adamw_4bit = "ao_adamw_4bit" # pylint: disable=invalid-name - ao_adamw_8bit = "ao_adamw_8bit" # pylint: disable=invalid-name - ao_adamw_fp8 = "ao_adamw_fp8" # pylint: disable=invalid-name - adopt_adamw = "adopt_adamw" # pylint: disable=invalid-name - muon = "muon" # pylint: disable=invalid-name - - -class DeprecatedParameters(BaseModel): - """configurations that are deprecated""" - - max_packed_sequence_len: Optional[int] = None - rope_scaling: Optional[Any] = None - noisy_embedding_alpha: Optional[float] = None - dpo_beta: Optional[float] = None - evaluation_strategy: Optional[str] = None - - @field_validator("max_packed_sequence_len") - @classmethod - def validate_max_packed_sequence_len(cls, max_packed_sequence_len): - if max_packed_sequence_len: - raise DeprecationWarning("`max_packed_sequence_len` is no longer supported") - return max_packed_sequence_len - - @field_validator("rope_scaling") - @classmethod - def validate_rope_scaling(cls, rope_scaling): - if rope_scaling: - raise DeprecationWarning( - "`rope_scaling` is no longer supported, it should now be be a key under `model_config`" - ) - return rope_scaling - - @field_validator("noisy_embedding_alpha") - @classmethod - def validate_noisy_embedding_alpha(cls, noisy_embedding_alpha): - if noisy_embedding_alpha: - LOG.warning("noisy_embedding_alpha is deprecated, use neftune_noise_alpha") - return noisy_embedding_alpha - - @field_validator("dpo_beta") - @classmethod - def validate_dpo_beta(cls, dpo_beta): - if dpo_beta is not None: - LOG.warning("dpo_beta is deprecated, use rl_beta instead") - return dpo_beta - - @field_validator("evaluation_strategy") - @classmethod - def validate_evaluation_strategy(cls, evaluation_strategy): - if evaluation_strategy is not None: - LOG.warning("evaluation_strategy is deprecated, use eval_strategy instead") - return evaluation_strategy - - -class RemappedParameters(BaseModel): - """parameters that have been remapped to other names""" - - overrides_of_model_config: Optional[Dict[str, Any]] = Field( - default=None, alias="model_config" - ) - overrides_of_model_kwargs: Optional[Dict[str, Any]] = Field( - default=None, alias="model_kwargs" - ) - type_of_model: Optional[str] = Field(default=None, alias="model_type") - revision_of_model: Optional[str] = Field(default=None, alias="model_revision") - - -class PretrainingDataset(BaseModel): - """pretraining dataset configuration subset""" - - name: Optional[str] = None - path: Optional[str] = None - split: Optional[str] = "train" - text_column: Optional[str] = "text" - type: Optional[str] = "pretrain" - trust_remote_code: Optional[bool] = False - data_files: Optional[str] = None - skip: Optional[int] = None - - -class UserDefinedPrompterType(BaseModel): - """structure for user defined prompt types""" - - system_prompt: Optional[str] = None - system_format: Optional[str] = None - field_system: Optional[str] = None - field_instruction: Optional[str] = None - field_input: Optional[str] = None - field_output: Optional[str] = None - - format: Optional[str] = None - no_input_format: Optional[str] = None - field: Optional[str] = None - - -class LrGroup(BaseModel): - """Custom learning rate group configuration""" - - name: str - modules: List[str] - lr: float - - -class SFTDataset(BaseModel): - """SFT configuration subset""" - - path: Optional[str] = None - split: Optional[str] = None - type: Optional[Union[str, UserDefinedPrompterType]] = None - input_transform: Optional[str] = None - shards: Optional[int] = None - shards_idx: Optional[int] = None - preprocess_shards: Optional[int] = None - conversation: Optional[str] = None - # Do not make this too strict or it will break the validator to choose different dataset class - chat_template: Optional[ - Union[ - ChatTemplate, - str, - ] - ] = None - chat_template_jinja: Optional[str] = None - data_files: Optional[Union[str, List[str]]] = None - input_format: Optional[str] = None - name: Optional[str] = None - ds_type: Optional[str] = None - train_on_split: Optional[str] = None - field: Optional[str] = None - field_human: Optional[str] = None - field_model: Optional[str] = None - field_messages: Optional[str] = None - message_field_role: Optional[str] = ( - None # deprecated, use message_property_mappings - ) - message_field_content: Optional[str] = ( - None # deprecated, use message_property_mappings - ) - message_property_mappings: Optional[Dict[str, str]] = None - message_field_training: Optional[str] = None - message_field_training_detail: Optional[str] = None - logprobs_field: Optional[str] = None - temperature: Optional[float] = None - roles_to_train: Optional[List[str]] = None - train_on_eos: Optional[str] = None - roles: Optional[Dict[str, List[str]]] = None - drop_system_message: Optional[bool] = None - trust_remote_code: Optional[bool] = False - revision: Optional[str] = None - - @model_validator(mode="before") - @classmethod - def handle_legacy_message_fields(cls, data): - """Handle backwards compatibility between legacy message field mapping and new property mapping system.""" - return handle_legacy_message_fields_logic(data) - - @model_validator(mode="before") - @classmethod - def check_chat_template_config(cls, data): - if isinstance(data, BaseModel): - data = data.model_dump() - - # Set chat_template to tokenizer_default if not set - if data.get("type") == "chat_template" and not data.get("chat_template"): - data["chat_template"] = ChatTemplate.tokenizer_default - - # if chat_template is set to jinja, chat_template_jinja is required - if data.get("chat_template") == ChatTemplate.jinja and not data.get( - "chat_template_jinja" - ): - raise ValueError( - "chat_template_jinja is required when chat_template is set to jinja" - ) - - # If chat_template_jinja is set, set chat_template to jinja - if data.get("chat_template_jinja") and not data.get("chat_template"): - data["chat_template"] = ChatTemplate.jinja - - return data - - -class UserDefinedDPOType(BaseModel): - """User defined typing for DPO""" - - field_system: Optional[str] = None - field_prompt: Optional[str] = None - field_chosen: Optional[str] = None - field_rejected: Optional[str] = None - prompt_format: Optional[str] = None - chosen_format: Optional[str] = None - rejected_format: Optional[str] = None - - -class DPODataset(BaseModel): - """DPO configuration subset""" - - path: Optional[str] = None - split: Optional[str] = None - type: Optional[Union[UserDefinedDPOType, str]] = None - data_files: Optional[List[str]] = None - revision: Optional[str] = None - field_messages: Optional[str] = None - - -class StepwiseSupervisedDataset(BaseModel): - """Stepwise supervised dataset configuration subset""" - - path: Optional[str] = None - split: Optional[str] = None - data_files: Optional[List[str]] = None - revision: Optional[str] = None - step_separator: Optional[str] = None - max_completion_length: Optional[int] = None - train_on_last_step_only: Optional[bool] = None - - -class UserDefinedKTOType(BaseModel): - """User defined typing for KTO""" - - field_system: Optional[str] = None - field_prompt: Optional[str] = None - field_completion: Optional[str] = None - field_label: Optional[bool] = None - prompt_format: Optional[str] = None - completion_format: Optional[str] = None - - -class KTODataset(BaseModel): - """KTO configuration subset""" - - path: Optional[str] = None - split: Optional[str] = None - type: Optional[Union[UserDefinedKTOType, str]] = None - data_files: Optional[List[str]] = None - trust_remote_code: Optional[bool] = False - revision: Optional[str] = None - - -DatasetConfig = Union[SFTDataset, DPODataset, KTODataset, StepwiseSupervisedDataset] - - -class LoftQConfig(BaseModel): - """LoftQ configuration subset""" - - loftq_bits: int = Field( - default=4, json_schema_extra={"description": "Quantization bits for LoftQ"} - ) - # loftq_iter: int = Field(default=1, json_schema_extra={"description": "Alternating iterations for LoftQ"}) - - -class PeftConfig(BaseModel): - """peftq configuration subset""" - - loftq_config: Optional[LoftQConfig] = None - - -class SpecialTokensConfig(BaseModel): - """Special tokens configuration subset""" - - bos_token: Optional[str] = None - eos_token: Optional[str] = None - pad_token: Optional[str] = None - unk_token: Optional[str] = None - additional_special_tokens: Optional[List[str]] = None - - -class LoraConfig(BaseModel): - """Peft / LoRA configuration subset""" - - load_in_8bit: Optional[bool] = Field(default=False) - load_in_4bit: Optional[bool] = Field(default=False) - - adapter: Optional[str] = None - lora_model_dir: Optional[str] = None - lora_r: Optional[int] = None - lora_alpha: Optional[int] = None - lora_fan_in_fan_out: Optional[bool] = None - lora_target_modules: Optional[Union[str, List[str]]] = None - lora_target_linear: Optional[bool] = None - lora_modules_to_save: Optional[List[str]] = None - lora_dropout: Optional[float] = 0.0 - peft_layers_to_transform: Optional[List[int]] = None - peft_layers_pattern: Optional[List[str]] = None - peft: Optional[PeftConfig] = None - peft_use_dora: Optional[bool] = None - peft_use_rslora: Optional[bool] = None - peft_layer_replication: Optional[List[Tuple[int, int]]] = None - peft_init_lora_weights: Optional[Union[bool, str]] = None - - qlora_sharded_model_loading: Optional[bool] = Field( - default=False, - json_schema_extra={ - "description": "load qlora model in sharded format for FSDP using answer.ai technique." - }, - ) - lora_on_cpu: Optional[bool] = None - gptq: Optional[bool] = None - bnb_config_kwargs: Optional[Dict[str, Any]] = None - - loraplus_lr_ratio: Optional[float] = Field( - default=None, - json_schema_extra={ - "description": "loraplus learning rate ratio lr_B / lr_A. Recommended value is 2^4." - }, - ) - loraplus_lr_embedding: Optional[float] = Field( - default=1e-6, - json_schema_extra={ - "description": "loraplus learning rate for lora embedding layers." - }, - ) - - merge_lora: Optional[bool] = None - - @model_validator(mode="before") - @classmethod - def validate_adapter(cls, data): - if ( - not data.get("adapter") - and not data.get("inference") - and (data.get("load_in_8bit") or data.get("load_in_4bit")) - ): - raise ValueError( - "load_in_8bit and load_in_4bit are not supported without setting an adapter for training." - "If you want to full finetune, please turn off load_in_8bit and load_in_4bit." - ) - return data - - @model_validator(mode="after") - def validate_qlora(self): - if self.adapter == "qlora": - if self.merge_lora: - # can't merge qlora if loaded in 8bit or 4bit - if self.load_in_8bit: - raise ValueError("Can't merge qlora if loaded in 8bit") - - if self.gptq: - raise ValueError("Can't merge qlora if gptq") - - if self.load_in_4bit: - raise ValueError("Can't merge qlora if loaded in 4bit") - - else: - if self.load_in_8bit: - raise ValueError("Can't load qlora in 8bit") - - if self.gptq: - raise ValueError("Can't load qlora if gptq") - - if not self.load_in_4bit: - raise ValueError("Require cfg.load_in_4bit to be True for qlora") - return self - - @field_validator("loraplus_lr_embedding") - @classmethod - def convert_loraplus_lr_embedding(cls, loraplus_lr_embedding): - if loraplus_lr_embedding and isinstance(loraplus_lr_embedding, str): - loraplus_lr_embedding = float(loraplus_lr_embedding) - return loraplus_lr_embedding - - @model_validator(mode="before") - @classmethod - def validate_lora_dropout(cls, data): - if data.get("adapter") is not None and data.get("lora_dropout") is None: - data["lora_dropout"] = 0.0 - return data - - -class ReLoRAConfig(BaseModel): - """ReLoRA configuration subset""" - - relora_steps: Optional[int] = None - relora_warmup_steps: Optional[int] = None - relora_anneal_steps: Optional[int] = None - relora_prune_ratio: Optional[float] = None - relora_cpu_offload: Optional[bool] = None - - -class ModelInputConfig(BaseModel): - """model to train on configuration subset""" - - model_config = {"protected_namespaces": ()} - - base_model: str - base_model_config: Optional[str] = None - cls_model_config: Optional[str] = None - tokenizer_config: Optional[str] = None - tokenizer_use_fast: Optional[bool] = None - tokenizer_legacy: Optional[bool] = None - tokenizer_type: Optional[str] = Field( - default=None, json_schema_extra={"description": "transformers tokenizer class"} - ) - processor_type: Optional[str] = Field( - default=None, json_schema_extra={"description": "transformers processor class"} - ) - trust_remote_code: Optional[bool] = None - - @field_validator("trust_remote_code") - @classmethod - def hint_trust_remote_code(cls, trust_remote_code): - if trust_remote_code: - LOG.warning( - "`trust_remote_code` is set to true. Please make sure that you reviewed the remote code/model." - ) - return trust_remote_code - - -class HyperparametersConfig(BaseModel): - """training hyperparams configuration subset""" - - gradient_accumulation_steps: Optional[int] = Field(default=1) - micro_batch_size: Optional[int] = Field( - default=1, - json_schema_extra={"description": "per gpu micro batch size for training"}, - ) - batch_size: Optional[int] = Field( - default=None, - json_schema_extra={ - "description": "Total batch size, we do not recommended setting this manually" - }, - ) - eval_batch_size: Optional[int] = Field( - default=None, - json_schema_extra={ - "description": "per gpu micro batch size for evals, defaults to value of micro_batch_size" - }, - ) - - auto_find_batch_size: Optional[bool] = None - - train_on_inputs: Optional[bool] = False - group_by_length: Optional[bool] = None - - learning_rate: Union[str, float] - embedding_lr: Optional[float] = None - embedding_lr_scale: Optional[float] = None - weight_decay: Optional[float] = 0.0 - optimizer: Optional[Union[OptimizerNames, CustomSupportedOptimizers]] = ( - OptimizerNames.ADAMW_TORCH_FUSED - ) - optim_args: Optional[Union[str, Dict[str, Any]]] = Field( - default=None, - json_schema_extra={"description": "Optional arguments to supply to optimizer."}, - ) - optim_target_modules: Optional[Union[List[str], Literal["all_linear"]]] = Field( - default=None, - json_schema_extra={ - "description": "The target modules to optimize, i.e. the module names that you would like to train." - }, - ) - torchdistx_path: Optional[str] = None - lr_scheduler: Optional[ - Union[SchedulerType, Literal["one_cycle"], Literal["rex"]] - ] = SchedulerType.COSINE - lr_scheduler_kwargs: Optional[Dict[str, Any]] = None - lr_quadratic_warmup: Optional[bool] = None - cosine_min_lr_ratio: Optional[float] = None - cosine_constant_lr_ratio: Optional[float] = None - lr_div_factor: Optional[float] = None - lr_groups: Optional[List[LrGroup]] = None - - adam_epsilon: Optional[float] = None - adam_beta1: Optional[float] = None - adam_beta2: Optional[float] = None - max_grad_norm: Optional[float] = None - num_epochs: float = Field(default=1.0) - - @field_validator("batch_size") - @classmethod - def hint_batch_size_set(cls, batch_size): - if batch_size: - LOG.warning( - "%s\n%s", - "batch_size is not recommended. Please use gradient_accumulation_steps instead.", - "To calculate the equivalent gradient_accumulation_steps, divide batch_size / micro_batch_size / number of gpus.", - ) - return batch_size - - @field_validator("learning_rate") - @classmethod - def convert_learning_rate(cls, learning_rate): - if learning_rate and isinstance(learning_rate, str): - learning_rate = float(learning_rate) - return learning_rate - - -class ModelOutputConfig(BaseModel): - """model save configuration subset""" - - output_dir: str = Field(default="./model-out") - hub_model_id: Optional[str] = None - hub_strategy: Optional[str] = None - save_safetensors: Optional[bool] = True - - -class MLFlowConfig(BaseModel): - """mlflow configuration subset""" - - use_mlflow: Optional[bool] = None - mlflow_tracking_uri: Optional[str] = None - mlflow_experiment_name: Optional[str] = None - mlflow_run_name: Optional[str] = None - hf_mlflow_log_artifacts: Optional[bool] = None - - -class LISAConfig(BaseModel): - """LISA options""" - - lisa_n_layers: Optional[int] = Field( - default=None, - json_schema_extra={"description": "the number of activate layers in LISA"}, - ) - lisa_step_interval: Optional[int] = Field( - default=None, - json_schema_extra={"description": "how often to switch layers in LISA"}, - ) - lisa_layers_attribute: Optional[str] = Field( - default="model.layers", - json_schema_extra={"description": "path under the model to access the layers"}, - ) - - -class WandbConfig(BaseModel): - """wandb configuration subset""" - - use_wandb: Optional[bool] = None - wandb_name: Optional[str] = None - wandb_run_id: Optional[str] = None - wandb_mode: Optional[str] = None - wandb_project: Optional[str] = None - wandb_entity: Optional[str] = None - wandb_watch: Optional[str] = None - wandb_log_model: Optional[str] = None - - @model_validator(mode="before") - @classmethod - def check_wandb_run(cls, data): - if data.get("wandb_run_id") and not data.get("wandb_name"): - data["wandb_name"] = data.get("wandb_run_id") - - LOG.warning( - "wandb_run_id sets the ID of the run. If you would like to set the name, please use wandb_name instead." - ) - - return data - - -class CometConfig(BaseModel): - """Comet configuration subset""" - - use_comet: Optional[bool] = None - comet_api_key: Optional[str] = None - comet_workspace: Optional[str] = None - comet_project_name: Optional[str] = None - comet_experiment_key: Optional[str] = None - comet_mode: Optional[str] = None - comet_online: Optional[bool] = None - comet_experiment_config: Optional[Dict[str, Any]] = None - - -class GradioConfig(BaseModel): - """Gradio configuration subset""" - - gradio_title: Optional[str] = None - gradio_share: Optional[bool] = None - gradio_server_name: Optional[str] = None - gradio_server_port: Optional[int] = None - gradio_max_new_tokens: Optional[int] = None - gradio_temperature: Optional[float] = None - - -class RayConfig(BaseModel): - """Ray launcher configuration subset""" - - use_ray: bool = Field(default=False) - ray_run_name: Optional[str] = Field( - default=None, - json_schema_extra={ - "help": "The training results will be saved at `saves/ray_run_name`." - }, - ) - ray_num_workers: int = Field( - default=1, - json_schema_extra={ - "help": "The number of workers for Ray training. Default is 1 worker." - }, - ) - resources_per_worker: dict = Field( - default_factory=lambda: {"GPU": 1}, - json_schema_extra={ - "help": "The resources per worker for Ray training. Default is to use 1 GPU per worker." - }, - ) - - # pylint: disable=too-many-public-methods,too-many-ancestors class AxolotlInputConfig( ModelInputConfig, @@ -680,252 +68,250 @@ class AxolotlInputConfig( DeprecatedParameters, BaseModel, ): - """wrapper of all config options""" + """Wrapper of all config options""" model_config = {"populate_by_name": True} - strict: Optional[bool] = Field(default=False) - resume_from_checkpoint: Optional[str] = None - auto_resume_from_checkpoints: Optional[bool] = None - resize_token_embeddings_to_32x: Optional[bool] = None - mean_resizing_embeddings: Optional[bool] = False + strict: bool | None = Field(default=False) + resume_from_checkpoint: str | None = None + auto_resume_from_checkpoints: bool | None = None + resize_token_embeddings_to_32x: bool | None = None + mean_resizing_embeddings: bool | None = False # optionally shrink the embeddings when the tokenizer vocab size is smaller - shrink_embeddings: Optional[bool] = None + shrink_embeddings: bool | None = None - rl: Optional[RLType] = None - trl: Optional[TRLConfig] = Field( + rl: RLType | None = None + trl: TRLConfig | None = Field( default_factory=lambda: TRLConfig(), # pylint: disable=unnecessary-lambda ) - reward_model: Optional[bool] = None - process_reward_model: Optional[bool] = None - num_labels: Optional[int] = None - dpo_use_weighting: Optional[bool] = ( - None # whether to use weighting in DPO trainer. If none, default is false in the trainer. - ) - dpo_use_logits_to_keep: Optional[bool] = None - - datasets: Optional[ + reward_model: bool | None = None + process_reward_model: bool | None = None + num_labels: int | None = None + # Whether to use weighting in DPO trainer. + # If `None`, default is `False` in the trainer. + dpo_use_weighting: bool | None = None + dpo_use_logits_to_keep: bool | None = None + + datasets: ( Annotated[ - list[Union[SFTDataset, DPODataset, KTODataset, StepwiseSupervisedDataset]], + list[SFTDataset | DPODataset | KTODataset | StepwiseSupervisedDataset], MinLen(1), ] - ] = None + | None + ) = None - test_datasets: Optional[ + test_datasets: ( Annotated[ - list[Union[SFTDataset, DPODataset, KTODataset, StepwiseSupervisedDataset]], + list[SFTDataset | DPODataset | KTODataset | StepwiseSupervisedDataset], MinLen(1), ] - ] = None - shuffle_merged_datasets: Optional[bool] = True - dataset_prepared_path: Optional[str] = None - dataset_shard_num: Optional[int] = None - dataset_shard_idx: Optional[int] = None - skip_prepare_dataset: Optional[bool] = False - - pretraining_dataset: Optional[ - Annotated[list[Union[PretrainingDataset, SFTDataset]], MinLen(1)] - ] = Field( + | None + ) = None + shuffle_merged_datasets: bool | None = True + dataset_prepared_path: str | None = None + dataset_shard_num: int | None = None + dataset_shard_idx: int | None = None + skip_prepare_dataset: bool | None = False + + pretraining_dataset: ( + Annotated[list[PretrainingDataset | SFTDataset], MinLen(1)] | None + ) = Field( default=None, json_schema_extra={"description": "streaming dataset to use for pretraining"}, ) - dataset_processes: Optional[int] = Field(default=min(32, os.cpu_count())) # type: ignore[type-var] - dataset_exact_deduplication: Optional[bool] = None - dataset_keep_in_memory: Optional[bool] = None - dataloader_pin_memory: Optional[bool] = None - dataloader_num_workers: Optional[int] = None - dataloader_prefetch_factor: Optional[int] = None - dataloader_drop_last: Optional[bool] = None - - accelerator_config: Optional[Dict[str, Any]] = None - - remove_unused_columns: Optional[bool] = None - - push_dataset_to_hub: Optional[str] = None - hf_use_auth_token: Optional[bool] = None - - device: Optional[Any] = None - device_map: Optional[Any] = None - world_size: Optional[int] = None - local_rank: Optional[int] = None - ddp: Optional[bool] = None - - seed: Optional[int] = None - ddp_timeout: Optional[int] = None - ddp_bucket_cap_mb: Optional[int] = None - ddp_broadcast_buffers: Optional[bool] = None - ddp_find_unused_parameters: Optional[bool] = None - - eval_table_size: Optional[int] = None - eval_max_new_tokens: Optional[int] = None - do_causal_lm_eval: Optional[bool] = None - eval_causal_lm_metrics: Optional[List[str]] = None - do_bench_eval: Optional[bool] = None - bench_dataset: Optional[str] = None - bench_split: Optional[str] = None - metric_for_best_model: Optional[str] = None - greater_is_better: Optional[bool] = None - - loss_watchdog_threshold: Optional[float] = None - loss_watchdog_patience: Optional[int] = None - - gc_steps: Optional[int] = None - - bf16: Optional[Union[Literal["auto"], bool]] = "auto" - fp16: Optional[bool] = None - bfloat16: Optional[bool] = None # for non-AMP cases - float16: Optional[bool] = None # for non-AMP cases - tf32: Optional[bool] = None - float32: Optional[bool] = None - - # torch_dtype: Optional[torch.dtype] - - gradient_checkpointing: Optional[Union[Literal["unsloth", "offload"], bool]] = ( - Field(default=False) + dataset_processes: int | None = Field(default=min(32, os.cpu_count())) # type: ignore[type-var] + dataset_exact_deduplication: bool | None = None + dataset_keep_in_memory: bool | None = None + dataloader_pin_memory: bool | None = None + dataloader_num_workers: int | None = None + dataloader_prefetch_factor: int | None = None + dataloader_drop_last: bool | None = None + + accelerator_config: dict[str, Any] | None = None + + remove_unused_columns: bool | None = None + + push_dataset_to_hub: str | None = None + hf_use_auth_token: bool | None = None + + device: Any | None = None + device_map: Any | None = None + world_size: int | None = None + local_rank: int | None = None + ddp: bool | None = None + + seed: int | None = None + ddp_timeout: int | None = None + ddp_bucket_cap_mb: int | None = None + ddp_broadcast_buffers: bool | None = None + ddp_find_unused_parameters: bool | None = None + + eval_table_size: int | None = None + eval_max_new_tokens: int | None = None + do_causal_lm_eval: bool | None = None + eval_causal_lm_metrics: list[str] | None = None + do_bench_eval: bool | None = None + bench_dataset: str | None = None + bench_split: str | None = None + metric_for_best_model: str | None = None + greater_is_better: bool | None = None + + loss_watchdog_threshold: float | None = None + loss_watchdog_patience: int | None = None + + gc_steps: int | None = None + + bf16: Literal["auto"] | bool | None = "auto" + fp16: bool | None = None + bfloat16: bool | None = None # for non-AMP cases + float16: bool | None = None # for non-AMP cases + tf32: bool | None = None + float32: bool | None = None + + # torch_dtype: torch.dtype | None + + gradient_checkpointing: Literal["unsloth", "offload"] | bool | None = Field( + default=False ) - gradient_checkpointing_kwargs: Optional[Dict[str, Any]] = None + gradient_checkpointing_kwargs: dict[str, Any] | None = None - unfrozen_parameters: Optional[List[str]] = None + unfrozen_parameters: list[str] | None = None sequence_len: int = Field(default=512) - min_sample_len: Optional[int] = None + min_sample_len: int | None = None max_prompt_len: int = Field( default=512, json_schema_extra={"description": "maximum prompt length for RL training"}, ) - sample_packing: Optional[bool] = None - sample_packing_group_size: Optional[int] = 100_000 - sample_packing_bin_size: Optional[int] = 200 - eval_sample_packing: Optional[bool] = None - pad_to_sequence_len: Optional[bool] = None - curriculum_sampling: Optional[bool] = None - multipack_real_batches: Optional[bool] = None - pretraining_sample_concatenation: Optional[bool] = Field( + sample_packing: bool | None = None + sample_packing_group_size: int | None = 100_000 + sample_packing_bin_size: int | None = 200 + eval_sample_packing: bool | None = None + pad_to_sequence_len: bool | None = None + curriculum_sampling: bool | None = None + multipack_real_batches: bool | None = None + pretraining_sample_concatenation: bool | None = Field( default=None, json_schema_extra={ "description": "whether to soft pack/concatenate samples during pretraining", }, ) - batch_flattening: Optional[Union[Literal["auto"], bool]] = None + batch_flattening: Literal["auto"] | bool | None = None # for PoSE context length extension - use_pose: Optional[bool] = None - pose_split_on_token_ids: Optional[List[int]] = None - pose_max_context_len: Optional[int] = None - pose_num_chunks: Optional[int] = None + use_pose: bool | None = None + pose_split_on_token_ids: list[int] | None = None + pose_max_context_len: int | None = None + pose_num_chunks: int | None = None - pretrain_multipack_buffer_size: Optional[int] = 10_000 - pretrain_multipack_attn: Optional[bool] = Field( + pretrain_multipack_buffer_size: int | None = 10_000 + pretrain_multipack_attn: bool | None = Field( default=True, json_schema_extra={ "description": "whether to prevent cross attention for packed sequences during pretraining", }, ) - xformers_attention: Optional[bool] = None - sdp_attention: Optional[bool] = None - s2_attention: Optional[bool] = None - flash_attention: Optional[bool] = None - flash_attn_cross_entropy: Optional[bool] = None - flash_attn_rms_norm: Optional[bool] = None - flash_attn_fuse_qkv: Optional[bool] = None - flash_attn_fuse_mlp: Optional[bool] = None - flash_optimum: Optional[bool] = None - - eager_attention: Optional[bool] = None - - unsloth_cross_entropy_loss: Optional[bool] = None - unsloth_lora_mlp: Optional[bool] = None - unsloth_lora_qkv: Optional[bool] = None - unsloth_lora_o: Optional[bool] = None - unsloth_rms_norm: Optional[bool] = None - unsloth_rope: Optional[bool] = None - - lora_mlp_kernel: Optional[bool] = None - lora_qkv_kernel: Optional[bool] = None - lora_o_kernel: Optional[bool] = None - - deepspeed: Optional[Union[str, Dict[str, Any]]] = None - fsdp: Optional[List[str]] = None - fsdp_config: Optional[Dict[str, Any]] = None - fsdp_final_state_dict_type: Optional[ - Literal["FULL_STATE_DICT", "LOCAL_STATE_DICT", "SHARDED_STATE_DICT"] - ] = None - - val_set_size: Optional[float] = Field(default=0.0) - - special_tokens: Optional[SpecialTokensConfig] = None - tokens: Optional[List[str]] = None - added_tokens_overrides: Optional[Dict[int, str]] = None - - torch_compile: Optional[Union[Literal["auto"], bool]] = None - torch_compile_backend: Optional[str] = None - torch_compile_mode: Optional[ - Literal["default", "reduce-overhead", "max-autotune"] - ] = None - - max_steps: Optional[int] = None - warmup_steps: Optional[int] = None - warmup_ratio: Optional[float] = None - eval_steps: Optional[Union[int, float]] = None - evals_per_epoch: Optional[int] = None - eval_strategy: Optional[str] = None - save_steps: Optional[Union[int, float]] = None - saves_per_epoch: Optional[int] = None - save_strategy: Optional[str] = None - save_total_limit: Optional[int] = None - logging_steps: Optional[int] = None - early_stopping_patience: Optional[int] = None - load_best_model_at_end: Optional[bool] = False - save_only_model: Optional[bool] = False - use_tensorboard: Optional[bool] = None - profiler_steps: Optional[int] = None - include_tokens_per_second: Optional[bool] = None - - neftune_noise_alpha: Optional[float] = None - - orpo_alpha: Optional[float] = None - rpo_alpha: Optional[float] = None - simpo_gamma: Optional[float] = None - cpo_alpha: Optional[float] = None - - kto_desirable_weight: Optional[float] = None - kto_undesirable_weight: Optional[float] = None - rl_beta: Optional[float] = None - - max_memory: Optional[Dict[Union[int, Literal["cpu", "disk"]], Union[int, str]]] = ( + xformers_attention: bool | None = None + sdp_attention: bool | None = None + s2_attention: bool | None = None + flash_attention: bool | None = None + flash_attn_cross_entropy: bool | None = None + flash_attn_rms_norm: bool | None = None + flash_attn_fuse_qkv: bool | None = None + flash_attn_fuse_mlp: bool | None = None + flash_optimum: bool | None = None + + eager_attention: bool | None = None + + unsloth_cross_entropy_loss: bool | None = None + unsloth_lora_mlp: bool | None = None + unsloth_lora_qkv: bool | None = None + unsloth_lora_o: bool | None = None + unsloth_rms_norm: bool | None = None + unsloth_rope: bool | None = None + + lora_mlp_kernel: bool | None = None + lora_qkv_kernel: bool | None = None + lora_o_kernel: bool | None = None + + deepspeed: str | dict[str, Any] | None = None + fsdp: list[str] | None = None + fsdp_config: dict[str, Any] | None = None + fsdp_final_state_dict_type: ( + Literal["FULL_STATE_DICT", "LOCAL_STATE_DICT", "SHARDED_STATE_DICT"] | None + ) = None + + val_set_size: float | None = Field(default=0.0) + + special_tokens: SpecialTokensConfig | None = None + tokens: list[str] | None = None + added_tokens_overrides: dict[int, str] | None = None + + torch_compile: Literal["auto"] | bool | None = None + torch_compile_backend: str | None = None + torch_compile_mode: Literal["default", "reduce-overhead", "max-autotune"] | None = ( None ) - gpu_memory_limit: Optional[Union[int, str]] = None - low_cpu_mem_usage: Optional[bool] = None - - chat_template: Optional[ - Union[ - ChatTemplate, - Annotated[str, StringConstraints(pattern="^tokenizer_default_fallback_")], - ] - ] = None - chat_template_jinja: Optional[str] = None - default_system_message: Optional[str] = None - fix_untrained_tokens: Optional[Union[int, List[int]]] = None + max_steps: int | None = None + warmup_steps: int | None = None + warmup_ratio: float | None = None + eval_steps: int | float | None = None + evals_per_epoch: int | None = None + eval_strategy: str | None = None + save_steps: int | float | None = None + saves_per_epoch: int | None = None + save_strategy: str | None = None + save_total_limit: int | None = None + logging_steps: int | None = None + early_stopping_patience: int | None = None + load_best_model_at_end: bool | None = False + save_only_model: bool | None = False + use_tensorboard: bool | None = None + profiler_steps: int | None = None + include_tokens_per_second: bool | None = None + + neftune_noise_alpha: float | None = None + + orpo_alpha: float | None = None + rpo_alpha: float | None = None + simpo_gamma: float | None = None + cpo_alpha: float | None = None + + kto_desirable_weight: float | None = None + kto_undesirable_weight: float | None = None + rl_beta: float | None = None + + max_memory: dict[int | Literal["cpu", "disk"], int | str] | None = None + gpu_memory_limit: int | str | None = None + low_cpu_mem_usage: bool | None = None + + chat_template: ( + ChatTemplate + | Annotated[str, StringConstraints(pattern="^tokenizer_default_fallback_")] + ) | None = None + chat_template_jinja: str | None = None + default_system_message: str | None = None + + fix_untrained_tokens: int | list[int] | None = None # INTERNALS - document for now, generally not set externally - is_preprocess: Optional[bool] = None - preprocess_iterable: Optional[bool] = None + is_preprocess: bool | None = None + preprocess_iterable: bool | None = None - total_num_tokens: Optional[int] = None - total_supervised_tokens: Optional[int] = None - sample_packing_eff_est: Optional[float] = None - axolotl_config_path: Optional[str] = None + total_num_tokens: int | None = None + total_supervised_tokens: int | None = None + sample_packing_eff_est: float | None = None + axolotl_config_path: str | None = None - is_falcon_derived_model: Optional[bool] = Field(default=None) - is_llama_derived_model: Optional[bool] = Field(default=None) - is_mistral_derived_model: Optional[bool] = Field(default=None) - is_qwen_derived_model: Optional[bool] = Field(default=None) + is_falcon_derived_model: bool | None = Field(default=None) + is_llama_derived_model: bool | None = Field(default=None) + is_mistral_derived_model: bool | None = Field(default=None) + is_qwen_derived_model: bool | None = Field(default=None) - plugins: Optional[List[str]] = Field(default=None) + plugins: list[str] | None = Field(default=None) @field_validator("datasets", mode="before") @classmethod @@ -953,8 +339,8 @@ def deprecate_sharegpt_datasets(cls, datasets): @field_serializer("datasets") def datasets_serializer( - self, ds_configs: Optional[List[DatasetConfig]] - ) -> Optional[List[Dict[str, Any]]]: + self, ds_configs: list[DatasetConfig] | None + ) -> list[dict[str, Any]] | None: if ds_configs: return [ds_config.model_dump(exclude_none=True) for ds_config in ds_configs] return None @@ -1028,6 +414,7 @@ def check_sample_packing_w_xformers(cls, data): @model_validator(mode="before") @classmethod + # pylint: disable=duplicate-code def check_chat_template_config(cls, data): # if chat_template is set to jinja, chat_template_jinja is required if data.get("chat_template") == ChatTemplate.jinja and not data.get( @@ -1850,77 +1237,3 @@ def check_beta_and_trl_beta_match(cls, data): if data["beta"] != data["trl"]["beta"]: raise ValueError("beta and trl.beta must match or one must be removed") return data - - -def handle_legacy_message_fields_logic(data: dict) -> dict: - """ - Handle backwards compatibility between legacy message field mapping and new property mapping system. - - Previously, the config only supported mapping 'role' and 'content' fields via dedicated config options: - - message_field_role: Mapped to the role field - - message_field_content: Mapped to the content field - - The new system uses message_property_mappings to support arbitrary field mappings: - message_property_mappings: - role: source_role_field - content: source_content_field - additional_field: source_field - - Args: - data: Dictionary containing configuration data - - Returns: - Updated dictionary with message field mappings consolidated - - Raises: - ValueError: If there are conflicts between legacy and new mappings - """ - data = data.copy() # Create a copy to avoid modifying the original - - if data.get("message_property_mappings") is None: - data["message_property_mappings"] = {} - - # Check for conflicts and handle role - if "message_field_role" in data: - LOG.warning( - "message_field_role is deprecated, use message_property_mappings instead. " - f"Example: message_property_mappings: {{role: {data['message_field_role']}}}" - ) - if ( - "role" in data["message_property_mappings"] - and data["message_property_mappings"]["role"] != data["message_field_role"] - ): - raise ValueError( - f"Conflicting message role fields: message_field_role='{data['message_field_role']}' " - f"conflicts with message_property_mappings.role='{data['message_property_mappings']['role']}'" - ) - data["message_property_mappings"]["role"] = data["message_field_role"] or "role" - - del data["message_field_role"] - elif "role" not in data["message_property_mappings"]: - data["message_property_mappings"]["role"] = "role" - - # Check for conflicts and handle content - if "message_field_content" in data: - LOG.warning( - "message_field_content is deprecated, use message_property_mappings instead. " - f"Example: message_property_mappings: {{content: {data['message_field_content']}}}" - ) - if ( - "content" in data["message_property_mappings"] - and data["message_property_mappings"]["content"] - != data["message_field_content"] - ): - raise ValueError( - f"Conflicting message content fields: message_field_content='{data['message_field_content']}' " - f"conflicts with message_property_mappings.content='{data['message_property_mappings']['content']}'" - ) - data["message_property_mappings"]["content"] = ( - data["message_field_content"] or "content" - ) - - del data["message_field_content"] - elif "content" not in data["message_property_mappings"]: - data["message_property_mappings"]["content"] = "content" - - return data diff --git a/src/axolotl/utils/schemas/datasets.py b/src/axolotl/utils/schemas/datasets.py new file mode 100644 index 0000000000..57de71da2b --- /dev/null +++ b/src/axolotl/utils/schemas/datasets.py @@ -0,0 +1,165 @@ +"""Pydantic models for datasets-related configuration""" + +from pydantic import BaseModel, model_validator + +from axolotl.utils.schemas.enums import ChatTemplate +from axolotl.utils.schemas.utils import handle_legacy_message_fields_logic + + +class UserDefinedPrompterType(BaseModel): + """Structure for user defined prompt types""" + + system_prompt: str | None = None + system_format: str | None = None + field_system: str | None = None + field_instruction: str | None = None + field_input: str | None = None + field_output: str | None = None + + format: str | None = None + no_input_format: str | None = None + field: str | None = None + + +class SFTDataset(BaseModel): + """SFT configuration subset""" + + path: str | None = None + split: str | None = None + type: str | UserDefinedPrompterType | None = None + input_transform: str | None = None + shards: int | None = None + shards_idx: int | None = None + preprocess_shards: int | None = None + conversation: str | None = None + # Do not make this too strict or it will break the validator to choose different dataset class + chat_template: ChatTemplate | str | None = None + chat_template_jinja: str | None = None + data_files: str | list[str] | None = None + input_format: str | None = None + name: str | None = None + ds_type: str | None = None + train_on_split: str | None = None + field: str | None = None + field_human: str | None = None + field_model: str | None = None + field_messages: str | None = None + # deprecated, use message_property_mappings + message_field_role: str | None = None + # deprecated, use message_property_mappings + message_field_content: str | None = None + message_property_mappings: dict[str, str] | None = None + message_field_training: str | None = None + message_field_training_detail: str | None = None + logprobs_field: str | None = None + temperature: float | None = None + roles_to_train: list[str] | None = None + train_on_eos: str | None = None + roles: dict[str, list[str]] | None = None + drop_system_message: bool | None = None + trust_remote_code: bool | None = False + revision: str | None = None + + @model_validator(mode="before") + @classmethod + def handle_legacy_message_fields(cls, data): + """Handle backwards compatibility between legacy message field mapping and new property mapping system.""" + return handle_legacy_message_fields_logic(data) + + @model_validator(mode="before") + @classmethod + # pylint: disable=duplicate-code + def check_chat_template_config(cls, data): + if isinstance(data, BaseModel): + data = data.model_dump() + + # Set chat_template to tokenizer_default if not set + if data.get("type") == "chat_template" and not data.get("chat_template"): + data["chat_template"] = ChatTemplate.tokenizer_default + + # if chat_template is set to jinja, chat_template_jinja is required + if data.get("chat_template") == ChatTemplate.jinja and not data.get( + "chat_template_jinja" + ): + raise ValueError( + "chat_template_jinja is required when chat_template is set to jinja" + ) + + # If chat_template_jinja is set, set chat_template to jinja + if data.get("chat_template_jinja") and not data.get("chat_template"): + data["chat_template"] = ChatTemplate.jinja + + return data + + +class PretrainingDataset(BaseModel): + """Pretraining dataset configuration subset""" + + name: str | None = None + path: str | None = None + split: str | None = "train" + text_column: str | None = "text" + type: str | None = "pretrain" + trust_remote_code: bool | None = False + data_files: str | None = None + skip: int | None = None + + +class UserDefinedDPOType(BaseModel): + """User defined typing for DPO""" + + field_system: str | None = None + field_prompt: str | None = None + field_chosen: str | None = None + field_rejected: str | None = None + prompt_format: str | None = None + chosen_format: str | None = None + rejected_format: str | None = None + + +class DPODataset(BaseModel): + """DPO configuration subset""" + + path: str | None = None + split: str | None = None + type: UserDefinedDPOType | str | None = None + data_files: list[str] | None = None + revision: str | None = None + field_messages: str | None = None + + +class StepwiseSupervisedDataset(BaseModel): + """Stepwise supervised dataset configuration subset""" + + path: str | None = None + split: str | None = None + data_files: list[str] | None = None + revision: str | None = None + step_separator: str | None = None + max_completion_length: int | None = None + train_on_last_step_only: bool | None = None + + +class UserDefinedKTOType(BaseModel): + """User defined typing for KTO""" + + field_system: str | None = None + field_prompt: str | None = None + field_completion: str | None = None + field_label: bool | None = None + prompt_format: str | None = None + completion_format: str | None = None + + +class KTODataset(BaseModel): + """KTO configuration subset""" + + path: str | None = None + split: str | None = None + type: UserDefinedKTOType | str | None = None + data_files: list[str] | None = None + trust_remote_code: bool | None = False + revision: str | None = None + + +DatasetConfig = SFTDataset | DPODataset | KTODataset | StepwiseSupervisedDataset diff --git a/src/axolotl/utils/schemas/deprecated.py b/src/axolotl/utils/schemas/deprecated.py new file mode 100644 index 0000000000..d42d6ff9eb --- /dev/null +++ b/src/axolotl/utils/schemas/deprecated.py @@ -0,0 +1,68 @@ +"""Pydantic models for deprecated and remapped configuration parameters""" + +import logging +from typing import Any + +from pydantic import BaseModel, Field, field_validator + +LOG = logging.getLogger(__name__) + + +class DeprecatedParameters(BaseModel): + """configurations that are deprecated""" + + max_packed_sequence_len: int | None = None + rope_scaling: Any | None = None + noisy_embedding_alpha: float | None = None + dpo_beta: float | None = None + evaluation_strategy: str | None = None + + @field_validator("max_packed_sequence_len") + @classmethod + def validate_max_packed_sequence_len(cls, max_packed_sequence_len): + if max_packed_sequence_len: + raise DeprecationWarning("`max_packed_sequence_len` is no longer supported") + return max_packed_sequence_len + + @field_validator("rope_scaling") + @classmethod + def validate_rope_scaling(cls, rope_scaling): + if rope_scaling: + raise DeprecationWarning( + "`rope_scaling` is no longer supported, it should now be be a key under `model_config`" + ) + return rope_scaling + + @field_validator("noisy_embedding_alpha") + @classmethod + def validate_noisy_embedding_alpha(cls, noisy_embedding_alpha): + if noisy_embedding_alpha: + LOG.warning("noisy_embedding_alpha is deprecated, use neftune_noise_alpha") + return noisy_embedding_alpha + + @field_validator("dpo_beta") + @classmethod + def validate_dpo_beta(cls, dpo_beta): + if dpo_beta is not None: + LOG.warning("dpo_beta is deprecated, use rl_beta instead") + return dpo_beta + + @field_validator("evaluation_strategy") + @classmethod + def validate_evaluation_strategy(cls, evaluation_strategy): + if evaluation_strategy is not None: + LOG.warning("evaluation_strategy is deprecated, use eval_strategy instead") + return evaluation_strategy + + +class RemappedParameters(BaseModel): + """Parameters that have been remapped to other names""" + + overrides_of_model_config: dict[str, Any] | None = Field( + default=None, alias="model_config" + ) + overrides_of_model_kwargs: dict[str, Any] | None = Field( + default=None, alias="model_kwargs" + ) + type_of_model: str | None = Field(default=None, alias="model_type") + revision_of_model: str | None = Field(default=None, alias="model_revision") diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py new file mode 100644 index 0000000000..f376aca5f7 --- /dev/null +++ b/src/axolotl/utils/schemas/enums.py @@ -0,0 +1,49 @@ +"""Enums for Axolotl input config""" + +from enum import Enum + + +class RLType(str, Enum): + """RL trainer type configuration subset""" + + dpo = "dpo" # pylint: disable=invalid-name + grpo = "grpo" # pylint: disable=invalid-name + ipo = "ipo" # pylint: disable=invalid-name + orpo = "orpo" # pylint: disable=invalid-name + kto = "kto" # pylint: disable=invalid-name + simpo = "simpo" # pylint: disable=invalid-name + + +class ChatTemplate(str, Enum): + """Chat templates configuration subset""" + + alpaca = "alpaca" # pylint: disable=invalid-name + chatml = "chatml" # pylint: disable=invalid-name + mistral_v1 = "mistral_v1" # pylint: disable=invalid-name + mistral_v2v3 = "mistral_v2v3" # pylint: disable=invalid-name + mistral_v3_tekken = "mistral_v3_tekken" # pylint: disable=invalid-name + gemma = "gemma" # pylint: disable=invalid-name + cohere = "cohere" # pylint: disable=invalid-name + llama3 = "llama3" # pylint: disable=invalid-name + llama3_2_vision = "llama3_2_vision" # pylint: disable=invalid-name + phi_3 = "phi_3" # pylint: disable=invalid-name + phi_35 = "phi_35" # pylint: disable=invalid-name + deepseek_v2 = "deepseek_v2" # pylint: disable=invalid-name + deepseek_v3 = "deepseek_v3" # pylint: disable=invalid-name + jamba = "jamba" # pylint: disable=invalid-name + jinja = "jinja" # pylint: disable=invalid-name + qwen_25 = "qwen_25" # pylint: disable=invalid-name + tokenizer_default = "tokenizer_default" # pylint: disable=invalid-name + exaone = "exaone" # pylint: disable=invalid-name + metharme = "metharme" # pylint: disable=invalid-name + + +class CustomSupportedOptimizers(str, Enum): + """Custom supported optimizers""" + + optimi_adamw = "optimi_adamw" # pylint: disable=invalid-name + ao_adamw_4bit = "ao_adamw_4bit" # pylint: disable=invalid-name + ao_adamw_8bit = "ao_adamw_8bit" # pylint: disable=invalid-name + ao_adamw_fp8 = "ao_adamw_fp8" # pylint: disable=invalid-name + adopt_adamw = "adopt_adamw" # pylint: disable=invalid-name + muon = "muon" # pylint: disable=invalid-name diff --git a/src/axolotl/utils/schemas/integrations.py b/src/axolotl/utils/schemas/integrations.py new file mode 100644 index 0000000000..9d8f9c190c --- /dev/null +++ b/src/axolotl/utils/schemas/integrations.py @@ -0,0 +1,108 @@ +"""Pydantic models for Axolotl integrations""" + +import logging +from typing import Any + +from pydantic import BaseModel, Field, model_validator + +LOG = logging.getLogger(__name__) + + +class MLFlowConfig(BaseModel): + """MLFlow configuration subset""" + + use_mlflow: bool | None = None + mlflow_tracking_uri: str | None = None + mlflow_experiment_name: str | None = None + mlflow_run_name: str | None = None + hf_mlflow_log_artifacts: bool | None = None + + +class LISAConfig(BaseModel): + """LISA configuration subset""" + + lisa_n_layers: int | None = Field( + default=None, + json_schema_extra={"description": "the number of activate layers in LISA"}, + ) + lisa_step_interval: int | None = Field( + default=None, + json_schema_extra={"description": "how often to switch layers in LISA"}, + ) + lisa_layers_attribute: str | None = Field( + default="model.layers", + json_schema_extra={"description": "path under the model to access the layers"}, + ) + + +class WandbConfig(BaseModel): + """Wandb configuration subset""" + + use_wandb: bool | None = None + wandb_name: str | None = None + wandb_run_id: str | None = None + wandb_mode: str | None = None + wandb_project: str | None = None + wandb_entity: str | None = None + wandb_watch: str | None = None + wandb_log_model: str | None = None + + @model_validator(mode="before") + @classmethod + def check_wandb_run(cls, data): + if data.get("wandb_run_id") and not data.get("wandb_name"): + data["wandb_name"] = data.get("wandb_run_id") + + LOG.warning( + "wandb_run_id sets the ID of the run. If you would like to set the name, please use wandb_name instead." + ) + + return data + + +class CometConfig(BaseModel): + """Comet configuration subset""" + + use_comet: bool | None = None + comet_api_key: str | None = None + comet_workspace: str | None = None + comet_project_name: str | None = None + comet_experiment_key: str | None = None + comet_mode: str | None = None + comet_online: bool | None = None + comet_experiment_config: dict[str, Any] | None = None + + +class GradioConfig(BaseModel): + """Gradio configuration subset""" + + gradio_title: str | None = None + gradio_share: bool | None = None + gradio_server_name: str | None = None + gradio_server_port: int | None = None + gradio_max_new_tokens: int | None = None + gradio_temperature: float | None = None + + +class RayConfig(BaseModel): + """Ray launcher configuration subset""" + + use_ray: bool = Field(default=False) + ray_run_name: str | None = Field( + default=None, + json_schema_extra={ + "help": "The training results will be saved at `saves/ray_run_name`." + }, + ) + ray_num_workers: int = Field( + default=1, + json_schema_extra={ + "help": "The number of workers for Ray training. Default is 1 worker." + }, + ) + resources_per_worker: dict = Field( + default_factory=lambda: {"GPU": 1}, + json_schema_extra={ + "help": "The resources per worker for Ray training. Default is to use 1 GPU per worker." + }, + ) diff --git a/src/axolotl/utils/config/models/internals/__init__.py b/src/axolotl/utils/schemas/internal/__init__.py similarity index 100% rename from src/axolotl/utils/config/models/internals/__init__.py rename to src/axolotl/utils/schemas/internal/__init__.py diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py new file mode 100644 index 0000000000..5f1d26e84c --- /dev/null +++ b/src/axolotl/utils/schemas/model.py @@ -0,0 +1,55 @@ +"""Pydantic models for model input / output, etc. configuration""" + +import logging + +from pydantic import BaseModel, Field, field_validator + +LOG = logging.getLogger(__name__) + + +class ModelInputConfig(BaseModel): + """Model configuration subset""" + + model_config = {"protected_namespaces": ()} + + base_model: str + base_model_config: str | None = None + cls_model_config: str | None = None + tokenizer_config: str | None = None + tokenizer_use_fast: bool | None = None + tokenizer_legacy: bool | None = None + tokenizer_type: str | None = Field( + default=None, json_schema_extra={"description": "transformers tokenizer class"} + ) + processor_type: str | None = Field( + default=None, json_schema_extra={"description": "transformers processor class"} + ) + trust_remote_code: bool | None = None + + @field_validator("trust_remote_code") + @classmethod + def hint_trust_remote_code(cls, trust_remote_code): + if trust_remote_code: + LOG.warning( + "`trust_remote_code` is set to true. Please make sure that you reviewed the remote code/model." + ) + return trust_remote_code + + +class ModelOutputConfig(BaseModel): + """model save configuration subset""" + + output_dir: str = Field(default="./model-out") + hub_model_id: str | None = None + hub_strategy: str | None = None + save_safetensors: bool | None = True + + +class SpecialTokensConfig(BaseModel): + """Special tokens configuration subset""" + + bos_token: str | None = None + eos_token: str | None = None + pad_token: str | None = None + unk_token: str | None = None + additional_special_tokens: list[str] | None = None diff --git a/src/axolotl/utils/schemas/peft.py b/src/axolotl/utils/schemas/peft.py new file mode 100644 index 0000000000..5d408e1fec --- /dev/null +++ b/src/axolotl/utils/schemas/peft.py @@ -0,0 +1,132 @@ +"""Pydantic models for PEFT-related configuration""" + +from typing import Any + +from pydantic import BaseModel, Field, field_validator, model_validator + + +class LoftQConfig(BaseModel): + """LoftQ configuration subset""" + + loftq_bits: int = Field( + default=4, json_schema_extra={"description": "Quantization bits for LoftQ"} + ) + # loftq_iter: int = Field(default=1, json_schema_extra={"description": "Alternating iterations for LoftQ"}) + + +class PeftConfig(BaseModel): + """peftq configuration subset""" + + loftq_config: LoftQConfig | None = None + + +class LoraConfig(BaseModel): + """Peft / LoRA configuration subset""" + + load_in_8bit: bool | None = Field(default=False) + load_in_4bit: bool | None = Field(default=False) + + adapter: str | None = None + lora_model_dir: str | None = None + lora_r: int | None = None + lora_alpha: int | None = None + lora_fan_in_fan_out: bool | None = None + lora_target_modules: str | list[str] | None = None + lora_target_linear: bool | None = None + lora_modules_to_save: list[str] | None = None + lora_dropout: float | None = 0.0 + peft_layers_to_transform: list[int] | None = None + peft_layers_pattern: list[str] | None = None + peft: PeftConfig | None = None + peft_use_dora: bool | None = None + peft_use_rslora: bool | None = None + peft_layer_replication: list[tuple[int, int]] | None = None + peft_init_lora_weights: bool | str | None = None + + qlora_sharded_model_loading: bool | None = Field( + default=False, + json_schema_extra={ + "description": "load qlora model in sharded format for FSDP using answer.ai technique." + }, + ) + lora_on_cpu: bool | None = None + gptq: bool | None = None + bnb_config_kwargs: dict[str, Any] | None = None + + loraplus_lr_ratio: float | None = Field( + default=None, + json_schema_extra={ + "description": "loraplus learning rate ratio lr_B / lr_A. Recommended value is 2^4." + }, + ) + loraplus_lr_embedding: float | None = Field( + default=1e-6, + json_schema_extra={ + "description": "loraplus learning rate for lora embedding layers." + }, + ) + + merge_lora: bool | None = None + + @model_validator(mode="before") + @classmethod + def validate_adapter(cls, data): + if ( + not data.get("adapter") + and not data.get("inference") + and (data.get("load_in_8bit") or data.get("load_in_4bit")) + ): + raise ValueError( + "load_in_8bit and load_in_4bit are not supported without setting an adapter for training." + "If you want to full finetune, please turn off load_in_8bit and load_in_4bit." + ) + return data + + @model_validator(mode="after") + def validate_qlora(self): + if self.adapter == "qlora": + if self.merge_lora: + # can't merge qlora if loaded in 8bit or 4bit + if self.load_in_8bit: + raise ValueError("Can't merge qlora if loaded in 8bit") + + if self.gptq: + raise ValueError("Can't merge qlora if gptq") + + if self.load_in_4bit: + raise ValueError("Can't merge qlora if loaded in 4bit") + + else: + if self.load_in_8bit: + raise ValueError("Can't load qlora in 8bit") + + if self.gptq: + raise ValueError("Can't load qlora if gptq") + + if not self.load_in_4bit: + raise ValueError("Require cfg.load_in_4bit to be True for qlora") + return self + + @field_validator("loraplus_lr_embedding") + @classmethod + def convert_loraplus_lr_embedding(cls, loraplus_lr_embedding): + if loraplus_lr_embedding and isinstance(loraplus_lr_embedding, str): + loraplus_lr_embedding = float(loraplus_lr_embedding) + return loraplus_lr_embedding + + @model_validator(mode="before") + @classmethod + def validate_lora_dropout(cls, data): + if data.get("adapter") is not None and data.get("lora_dropout") is None: + data["lora_dropout"] = 0.0 + return data + + +class ReLoRAConfig(BaseModel): + """ReLoRA configuration subset""" + + relora_steps: int | None = None + relora_warmup_steps: int | None = None + relora_anneal_steps: int | None = None + relora_prune_ratio: float | None = None + relora_cpu_offload: bool | None = None diff --git a/src/axolotl/utils/schemas/training.py b/src/axolotl/utils/schemas/training.py new file mode 100644 index 0000000000..2ab4b4286d --- /dev/null +++ b/src/axolotl/utils/schemas/training.py @@ -0,0 +1,99 @@ +"""Pydantic models for training hyperparameters""" + +import logging +from typing import Any, Literal + +from pydantic import BaseModel, Field, field_validator +from transformers import SchedulerType +from transformers.training_args import OptimizerNames + +from axolotl.utils.schemas.enums import CustomSupportedOptimizers + +LOG = logging.getLogger(__name__) + + +class LrGroup(BaseModel): + """Custom learning rate group configuration""" + + name: str + modules: list[str] + lr: float + + +class HyperparametersConfig(BaseModel): + """Training hyperparams configuration subset""" + + gradient_accumulation_steps: int | None = Field(default=1) + micro_batch_size: int | None = Field( + default=1, + json_schema_extra={"description": "per gpu micro batch size for training"}, + ) + batch_size: int | None = Field( + default=None, + json_schema_extra={ + "description": "Total batch size, we do not recommended setting this manually" + }, + ) + eval_batch_size: int | None = Field( + default=None, + json_schema_extra={ + "description": "per gpu micro batch size for evals, defaults to value of micro_batch_size" + }, + ) + + auto_find_batch_size: bool | None = None + + train_on_inputs: bool | None = False + group_by_length: bool | None = None + + learning_rate: str | float + embedding_lr: float | None = None + embedding_lr_scale: float | None = None + weight_decay: float | None = 0.0 + optimizer: (OptimizerNames | CustomSupportedOptimizers) | None = ( + OptimizerNames.ADAMW_TORCH_FUSED + ) + optim_args: (str | dict[str, Any]) | None = Field( + default=None, + json_schema_extra={"description": "Optional arguments to supply to optimizer."}, + ) + optim_target_modules: (list[str] | Literal["all_linear"]) | None = Field( + default=None, + json_schema_extra={ + "description": "The target modules to optimize, i.e. the module names that you would like to train." + }, + ) + torchdistx_path: str | None = None + lr_scheduler: (SchedulerType | Literal["one_cycle"] | Literal["rex"]) | None = ( + SchedulerType.COSINE + ) + lr_scheduler_kwargs: dict[str, Any] | None = None + lr_quadratic_warmup: bool | None = None + cosine_min_lr_ratio: float | None = None + cosine_constant_lr_ratio: float | None = None + lr_div_factor: float | None = None + lr_groups: list[LrGroup] | None = None + + adam_epsilon: float | None = None + adam_beta1: float | None = None + adam_beta2: float | None = None + max_grad_norm: float | None = None + num_epochs: float = Field(default=1.0) + + @field_validator("batch_size") + @classmethod + def hint_batch_size_set(cls, batch_size): + if batch_size: + LOG.warning( + "%s\n%s", + "batch_size is not recommended. Please use gradient_accumulation_steps instead.", + "To calculate the equivalent gradient_accumulation_steps, divide batch_size / micro_batch_size / number of gpus.", + ) + return batch_size + + @field_validator("learning_rate") + @classmethod + def convert_learning_rate(cls, learning_rate): + if learning_rate and isinstance(learning_rate, str): + learning_rate = float(learning_rate) + return learning_rate diff --git a/src/axolotl/utils/config/models/input/v0_4_1/trl.py b/src/axolotl/utils/schemas/trl.py similarity index 76% rename from src/axolotl/utils/config/models/input/v0_4_1/trl.py rename to src/axolotl/utils/schemas/trl.py index f408acdba3..60759769dc 100644 --- a/src/axolotl/utils/config/models/input/v0_4_1/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -1,8 +1,4 @@ -""" -GRPO specific configuration args -""" - -from typing import Optional +"""Pydantic models for TRL trainer configuration""" from pydantic import BaseModel, Field @@ -12,11 +8,11 @@ class TRLConfig(BaseModel): Input args for TRL. """ - beta: Optional[float] = Field( + beta: float | None = Field( default=None, json_schema_extra={"description": "Beta for RL training"}, ) - max_completion_length: Optional[int] = Field( + max_completion_length: int | None = Field( default=None, json_schema_extra={ "description": "Maximum length of the completion for RL training" @@ -25,50 +21,50 @@ class TRLConfig(BaseModel): # GRPO specific args # Ref: https://github.com/huggingface/trl/blob/e3244d2d096ff1e2e248c931d06d39e165e20623/trl/trainer/grpo_config.py#L22 - use_vllm: Optional[bool] = Field( + use_vllm: bool | None = Field( default=False, json_schema_extra={"description": "Whether to use VLLM for RL training"}, ) - vllm_device: Optional[str] = Field( + vllm_device: str | None = Field( default="auto", json_schema_extra={"description": "Device to use for VLLM"}, ) - vllm_gpu_memory_utilization: Optional[float] = Field( + vllm_gpu_memory_utilization: float | None = Field( default=0.9, json_schema_extra={"description": "GPU memory utilization for VLLM"}, ) - vllm_dtype: Optional[str] = Field( + vllm_dtype: str | None = Field( default="auto", json_schema_extra={"description": "Data type for VLLM"}, ) - vllm_max_model_len: Optional[int] = Field( + vllm_max_model_len: int | None = Field( default=None, json_schema_extra={ "description": "Maximum length of the model context for VLLM" }, ) - reward_funcs: Optional[list[str]] = Field( + reward_funcs: list[str] | None = Field( default=None, json_schema_extra={"description": "List of reward functions to load"}, ) - reward_weights: Optional[list[float]] = Field( + reward_weights: list[float] | None = Field( default=None, json_schema_extra={ "description": "Weights for each reward function. Must match the number of reward functions." }, ) - num_generations: Optional[int] = Field( + num_generations: int | None = Field( default=None, json_schema_extra={ "description": "Number of generations to sample. The global batch size (num_processes * per_device_batch_size) must be divisible by this value." }, ) - log_completions: Optional[bool] = Field( + log_completions: bool | None = Field( default=False, json_schema_extra={"description": "Whether to log completions"}, ) - sync_ref_model: Optional[bool] = Field( + sync_ref_model: bool | None = Field( default=False, json_schema_extra={ "description": ( @@ -77,13 +73,13 @@ class TRLConfig(BaseModel): ) }, ) - ref_model_mixup_alpha: Optional[float] = Field( + ref_model_mixup_alpha: float | None = Field( default=0.9, json_schema_extra={ "description": "Mixup alpha for the reference model. Requires `sync_ref_model=True`." }, ) - ref_model_sync_steps: Optional[int] = Field( + ref_model_sync_steps: int | None = Field( default=64, json_schema_extra={ "description": "Sync steps for the reference model. Requires `sync_ref_model=True`." diff --git a/src/axolotl/utils/schemas/utils.py b/src/axolotl/utils/schemas/utils.py new file mode 100644 index 0000000000..bf74390f67 --- /dev/null +++ b/src/axolotl/utils/schemas/utils.py @@ -0,0 +1,79 @@ +"""Utilities for Axolotl Pydantic models""" + +import logging + +LOG = logging.getLogger(__name__) + + +def handle_legacy_message_fields_logic(data: dict) -> dict: + """ + Handle backwards compatibility between legacy message field mapping and new property mapping system. + + Previously, the config only supported mapping 'role' and 'content' fields via dedicated config options: + - message_field_role: Mapped to the role field + - message_field_content: Mapped to the content field + + The new system uses message_property_mappings to support arbitrary field mappings: + message_property_mappings: + role: source_role_field + content: source_content_field + additional_field: source_field + + Args: + data: Dictionary containing configuration data + + Returns: + Updated dictionary with message field mappings consolidated + + Raises: + ValueError: If there are conflicts between legacy and new mappings + """ + data = data.copy() # Create a copy to avoid modifying the original + + if data.get("message_property_mappings") is None: + data["message_property_mappings"] = {} + + # Check for conflicts and handle role + if "message_field_role" in data: + LOG.warning( + "message_field_role is deprecated, use message_property_mappings instead. " + f"Example: message_property_mappings: {{role: {data['message_field_role']}}}" + ) + if ( + "role" in data["message_property_mappings"] + and data["message_property_mappings"]["role"] != data["message_field_role"] + ): + raise ValueError( + f"Conflicting message role fields: message_field_role='{data['message_field_role']}' " + f"conflicts with message_property_mappings.role='{data['message_property_mappings']['role']}'" + ) + data["message_property_mappings"]["role"] = data["message_field_role"] or "role" + + del data["message_field_role"] + elif "role" not in data["message_property_mappings"]: + data["message_property_mappings"]["role"] = "role" + + # Check for conflicts and handle content + if "message_field_content" in data: + LOG.warning( + "message_field_content is deprecated, use message_property_mappings instead. " + f"Example: message_property_mappings: {{content: {data['message_field_content']}}}" + ) + if ( + "content" in data["message_property_mappings"] + and data["message_property_mappings"]["content"] + != data["message_field_content"] + ): + raise ValueError( + f"Conflicting message content fields: message_field_content='{data['message_field_content']}' " + f"conflicts with message_property_mappings.content='{data['message_property_mappings']['content']}'" + ) + data["message_property_mappings"]["content"] = ( + data["message_field_content"] or "content" + ) + + del data["message_field_content"] + elif "content" not in data["message_property_mappings"]: + data["message_property_mappings"]["content"] = "content" + + return data diff --git a/styles.css b/styles.css index 749ff4366b..c5b0768faf 100644 --- a/styles.css +++ b/styles.css @@ -14,7 +14,7 @@ h1 { font-family: var(--font-title); font-weight: 400; - font-size: 5rem; + font-size: 3rem; line-height: 1.1; letter-spacing: -0.05em; font-feature-settings: "ss01" on; @@ -24,7 +24,7 @@ h1 { h2 { font-family: var(--font-title); font-weight: 500; - font-size: 2rem; + font-size: 1.5rem; line-height: 1.2; letter-spacing: -0.03em; font-feature-settings: "ss01" on; @@ -35,7 +35,7 @@ h3, h4 { font-family: var(--font-body); font-weight: 400; - font-size: 1.5rem; + font-size: 1.25rem; line-height: 1.5; letter-spacing: -0.02em; } @@ -191,3 +191,87 @@ code span.er { color: #5cb85c !important; text-decoration: none !important; } + +/* API Documentation Styling */ + +/* Improve docstring section rendering */ +.level3 p { + white-space: pre-line !important; +} + +/* Format docstring sections */ +.level3 p strong { + display: block; + margin-top: 1em; + font-weight: bold; + color: var(--cyan); +} + +/* Add spacing after sections */ +.level3 p:has(strong) { + margin-bottom: 0.5em; +} + +/* Format Args and Returns sections */ +p:has(code) { + line-height: 1.6; +} + +/* Function signatures */ +.sourceCode { + margin-bottom: 1.5em; +} + +/* Parameter tables */ +.doc-section-parameters table, +.doc-section-returns table { + margin-top: 1em; + margin-bottom: 1.5em; +} + +/* Make parameter and returns headers smaller */ +h2.anchored[data-anchor-id="parameters"], +h2.anchored[data-anchor-id="returns"], +.doc-section-parameters h4, +.doc-section-returns h4 { + font-size: 1.25rem; + margin-top: 2rem; + margin-bottom: 1rem; + color: var(--lime); + border-bottom: 1px solid var(--lime); + padding-bottom: 0.3rem; + font-family: var(--font-body); + font-weight: 500; + letter-spacing: normal; +} + +/* Style documentation tables */ +table { + width: 100%; + margin-bottom: 1.5rem; + border-collapse: collapse; +} + +table th { + background-color: #1a1a1a; + padding: 0.5rem 1rem; + border-bottom: 2px solid var(--greige-600); + text-align: left; +} + +table td { + padding: 0.5rem 1rem; + border-bottom: 1px solid var(--greige-600); +} + +/* Code in table cells */ +table td code { + background-color: transparent !important; + padding: 0; +} + +/* Improve spacing in parameter and return tables */ +.doc-section-parameters, +.doc-section-returns { + margin-top: 1rem; +} diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 9d41dac761..3262a69815 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -11,10 +11,10 @@ from axolotl.utils import is_comet_available from axolotl.utils.config import validate_config -from axolotl.utils.config.models.input.v0_4_1 import AxolotlConfigWCapabilities from axolotl.utils.dict import DictDefault from axolotl.utils.mlflow_ import setup_mlflow_env_vars from axolotl.utils.models import check_model_config +from axolotl.utils.schemas.config import AxolotlConfigWCapabilities from axolotl.utils.wandb_ import setup_wandb_env_vars warnings.filterwarnings("error") diff --git a/tests/test_validation_dataset.py b/tests/test_validation_dataset.py index 5c1b5a1f77..47d10ee99c 100644 --- a/tests/test_validation_dataset.py +++ b/tests/test_validation_dataset.py @@ -6,8 +6,8 @@ import pytest from axolotl.utils.config import validate_config -from axolotl.utils.config.models.input.v0_4_1 import ChatTemplate from axolotl.utils.dict import DictDefault +from axolotl.utils.schemas.datasets import ChatTemplate warnings.filterwarnings("error") diff --git a/tests/utils/test_models.py b/tests/utils/test_models.py index e78cdb5d70..83678430a5 100644 --- a/tests/utils/test_models.py +++ b/tests/utils/test_models.py @@ -119,7 +119,7 @@ def test_set_quantization_config( def test_message_property_mapping(self): """Test message property mapping configuration validation""" - from axolotl.utils.config.models.input.v0_4_1 import SFTDataset + from axolotl.utils.schemas.datasets import SFTDataset # Test legacy fields are mapped orrectly dataset = SFTDataset( From 23f0c51d88aa1d791d8619b146d92892c27d7ade Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 21 Mar 2025 12:43:55 -0400 Subject: [PATCH 0482/1405] Sequence parallelism (#2412) * adding easy_context as integration for now * progress on ring attn impl * progress on ring attn impl * cleanup * remove errant file * fix req * removing unused code * updates * pytest * update * updates * fixes * precommit fixes * working multi-group SP * fixing sample packing * remove debug logs and simplify * eval dataloader and sampler changes * removing some obvious comments * update config.qmd and rename option * scoping down problematic import * another import scoping change * pernicious Fire CLI bugfix * isolate cli tests * actually isolate CLI tests * gracefully handle no ring-flash-attn * fix * fix * move ring flash attn to extras with flash-attn (#2414) * removing flash-attn from requirements.txt (in setup.py extras already) * rename file, delete another * using field validator instead of model validator * test fix * sampler / dataloader refactor * non-seq2se1 collator fix * removing print statement * bugfix * add SP doc, review comments * small changes * review comments, docstrings * refactors, SP mixin * small updates * fix tests * precommit * precommit --------- Co-authored-by: Wing Lian Co-authored-by: Dan Saunders --- .github/workflows/tests.yml | 6 +- cicd/Dockerfile.jinja | 4 +- cicd/cicd.sh | 5 +- docs/config.qmd | 11 + docs/sequence_parallelism.qmd | 90 ++ requirements.txt | 2 +- setup.py | 12 +- src/axolotl/cli/train.py | 11 +- src/axolotl/core/trainer_builder.py | 19 +- src/axolotl/core/trainers/__init__.py | 18 + src/axolotl/core/trainers/base.py | 819 ++++++------------ src/axolotl/core/trainers/dpo/trainer.py | 12 +- src/axolotl/core/trainers/mamba.py | 32 + src/axolotl/core/trainers/mixins/__init__.py | 8 + src/axolotl/core/trainers/mixins/optimizer.py | 201 +++++ src/axolotl/core/trainers/mixins/scheduler.py | 113 +++ .../core/trainers/mixins/sequence_parallel.py | 131 +++ src/axolotl/core/trainers/relora.py | 43 + src/axolotl/core/trainers/trl.py | 65 +- src/axolotl/core/trainers/utils.py | 33 + src/axolotl/core/training_args.py | 9 +- .../monkeypatch/attention/ring_attn.py | 89 ++ src/axolotl/train.py | 5 +- src/axolotl/utils/collators/batching.py | 96 +- src/axolotl/utils/config/__init__.py | 3 + src/axolotl/utils/models.py | 82 +- src/axolotl/utils/samplers/multipack.py | 4 +- src/axolotl/utils/schemas/config.py | 27 +- src/axolotl/utils/trainer.py | 18 +- tests/e2e/patched/test_sp.py | 207 +++++ tests/test_exact_deduplication.py | 3 + 31 files changed, 1531 insertions(+), 647 deletions(-) create mode 100644 docs/sequence_parallelism.qmd create mode 100644 src/axolotl/core/trainers/mamba.py create mode 100644 src/axolotl/core/trainers/mixins/__init__.py create mode 100644 src/axolotl/core/trainers/mixins/optimizer.py create mode 100644 src/axolotl/core/trainers/mixins/scheduler.py create mode 100644 src/axolotl/core/trainers/mixins/sequence_parallel.py create mode 100644 src/axolotl/core/trainers/relora.py create mode 100644 src/axolotl/core/trainers/utils.py create mode 100644 src/axolotl/monkeypatch/attention/ring_attn.py create mode 100644 tests/e2e/patched/test_sp.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 32bb428211..66d95b3d4d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -98,8 +98,9 @@ jobs: - name: Run tests run: | - pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ + pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ pytest -v tests/patched/ + pytest -v tests/cli/ - name: cleanup pip cache run: | @@ -172,8 +173,9 @@ jobs: - name: Run tests run: | - pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ + pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ pytest -v tests/patched/ + pytest -v tests/cli/ - name: cleanup pip cache run: | diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index b212a00657..6988e092ba 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -33,9 +33,9 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ RUN pip install packaging==23.2 setuptools==75.8.0 RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,ray] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray] $AXOLOTL_ARGS; \ fi RUN python scripts/unsloth_install.py | sh diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 8097e9c56b..1d9ea7fbe7 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -3,9 +3,10 @@ set -e python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__" -pytest -v --durations=10 -n8 --ignore=tests/e2e/ --ignore=tests/patched/ /workspace/axolotl/tests/ +pytest -v --durations=10 -n8 --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli /workspace/axolotl/tests/ pytest -v --durations=10 /workspace/axolotl/tests/e2e/patched/lora_kernels # running these with the other patches causes a failure pytest -v --durations=10 --ignore=tests/e2e/patched/lora_kernels /workspace/axolotl/tests/e2e/patched pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/solo/ pytest -v --durations=10 /workspace/axolotl/tests/e2e/integrations/ -pytest -v --durations=10 --ignore=tests/e2e/solo/ --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ /workspace/axolotl/tests/e2e/ +pytest -v --durations=10 /workspace/axolotl/tests/cli +pytest -v --durations=10 --ignore=tests/e2e/solo/ --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ --ignore=tests/cli /workspace/axolotl/tests/e2e/ diff --git a/docs/config.qmd b/docs/config.qmd index ea7ea22939..9946b5865a 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -32,6 +32,9 @@ tokenizer_legacy: resize_token_embeddings_to_32x: # Optional[bool] Whether to shrink the embeddings to len(tokenizer). By default, we won't shrink. shrink_embeddings: +# Whether to load the model with randomly initialized weights. Useful for +# pre-training a model from scratch or debugging purposes. +random_init_weights: # (Internal use only) # Used to identify which the model is based on @@ -617,6 +620,14 @@ ddp_timeout: ddp_bucket_cap_mb: ddp_broadcast_buffers: +# Sequence parallelism +# Set to a divisor of the number of GPUs available to split sequences into chunks of equal size. +# Use in long context training to prevent OOM when sequences cannot fit into a single GPU's VRAM. +# E.g., if 4 GPUs are available, set this value to 2 to split each sequence into two equal-sized +# subsequences, or set to 4 to split into four equal-sized subsequences. +# See https://axolotl-ai-cloud.github.io/axolotl/docs/sequence_parallelism.html for more details. +sequence_parallel_degree: + # Path to torch distx for optim 'adamw_anyprecision' torchdistx_path: diff --git a/docs/sequence_parallelism.qmd b/docs/sequence_parallelism.qmd new file mode 100644 index 0000000000..cb297c0e0c --- /dev/null +++ b/docs/sequence_parallelism.qmd @@ -0,0 +1,90 @@ +--- +title: Sequence Parallelism +description: Train with long sequences split across multiple GPUs. +--- + +# Sequence Parallelism + +Sequence parallelism is a technique that splits sequences across multiple GPUs, +allowing you to train with very long sequences that wouldn't fit on a single GPU. Each +GPU processes a different portion of the sequence, and the results are aggregated +through a ring communication pattern. + +## When to Use Sequence Parallelism + +Use sequence parallelism when: + +- You need to train with sequence lengths that don't fit into a single GPU's memory +- You have multiple GPUs available +- You're experiencing OOM (Out Of Memory) errors with long sequences + +## Configuration + +To enable sequence parallelism, add the following to your configuration file: + +```yaml +# Set to a divisor (> 1) of the number of GPUs available +sequence_parallel_degree: 4 # Split sequences across 4 GPUs +``` + +The `sequence_parallel_degree` should be a divisor of the total number of GPUs. For example: + +- With 8 GPUs, valid values would be 2, 4, or 8 +- With 4 GPUs, valid values would be 2 or 4 + +## Implementation Details + +When sequence parallelism is enabled: + +1. Each sequence is divided into equal chunks across the GPUs in a sequence parallel group +2. The data collator handles the chunking of input_ids, attention_mask, labels, and position_ids +3. Position IDs are adjusted to maintain proper relative positions, especially for packed sequences +4. The trainer uses special ring communication patterns for attention operations + +## Requirements + +To use sequence parallelism, you need: + +- Multiple GPUs (at least 2) +- The `ring-flash-attn` package. Install with: + - `pip install axolotl[ring-flash-attn]` (preferred) + - `pip install ring-flash-attn>=0.1.4` + +## Limitations + +- Flash attention must be enabled for this to work (`flash_attention: true` in config YAML) +- May have a small performance overhead due to communication between GPUs + +## Example + +```yaml +# Example config with sequence parallelism +base_model: meta-llama/Llama-3-8B-Instruct +sequence_len: 8192 +sequence_parallel_degree: 2 # Split each sequence into 4 parts +flash_attention: true # Required with sequence parallelism +... +``` + +This will train the Llama 3 8B model with 8K context length, with each sequence split +into 2 subsequences of length 4096 across 2 GPUs. + +## Sample Packing with Sequence Parallelism + +Sequence parallelism is compatible with Axolotl's sample packing functionality. When using both features together: + +1. Samples are first packed together +2. The packed sequences are then divided across GPUs in the sequence parallel group +3. Position IDs are automatically adjusted to maintain proper relative positions + +## Effect on Batch Size + +When using sequence parallelism, your effective global batch size is **divided** by the `sequence_parallel_degree`. This happens because: + +- Each group of `sequence_parallel_degree` GPUs works on the same batch (just different parts of each sequence) +- The number of batches processed per step decreases + +For example: +- With 8 GPUs and no sequence parallelism: 8 different batches processed per step +- With 8 GPUs and `sequence_parallel_degree=4`: Only 2 different batches processed per step (each split across 4 GPUs) +- If your per-GPU `micro_batch_size` is 2, the global batch size decreases from 16 to 4 diff --git a/requirements.txt b/requirements.txt index 495f43af62..c8465d23f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,6 @@ bitsandbytes==0.45.3 triton>=3.0.0 mamba-ssm==1.2.0.post1 -flash-attn==2.7.4.post1 xformers>=0.0.23.post1 autoawq==0.2.7.post3 liger-kernel==0.5.3 @@ -36,6 +35,7 @@ einops colorama numba numpy>=1.24.4,<=2.0.1 + # qlora things evaluate==0.4.1 scipy diff --git a/setup.py b/setup.py index c4ffcdaebf..8b2f1b2a59 100644 --- a/setup.py +++ b/setup.py @@ -17,11 +17,7 @@ def parse_requirements(): lines = [r.strip() for r in requirements_file.readlines()] for line in lines: is_extras = ( - "flash-attn" in line - or "flash-attention" in line - or "deepspeed" in line - or "mamba-ssm" in line - or "lion-pytorch" in line + "deepspeed" in line or "mamba-ssm" in line or "lion-pytorch" in line ) if line.startswith("--extra-index-url"): # Handle custom index URLs @@ -39,7 +35,6 @@ def parse_requirements(): "bitsandbytes", "triton", "mamba-ssm", - "flash-attn", "xformers", "autoawq", "liger-kernel", @@ -124,9 +119,8 @@ def get_package_version(): ], }, extras_require={ - "flash-attn": [ - "flash-attn==2.7.4.post1", - ], + "flash-attn": ["flash-attn==2.7.4.post1"], + "ring-flash-attn": ["ring-flash-attn>=0.1.4", "yunchang==0.6.0"], "deepspeed": [ "deepspeed==0.16.4", "deepspeed-kernels", diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index e991105e6b..6cc7c7701c 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -23,7 +23,7 @@ LOG = logging.getLogger(__name__) -def do_train(cfg: DictDefault, cli_args: TrainerCliArgs) -> None: +def do_train(cfg: DictDefault, cli_args: TrainerCliArgs): """ Trains a `transformers` model by first loading the dataset(s) specified in the `axolotl` config, and then calling `axolotl.train.train`. Also runs the plugin @@ -44,16 +44,13 @@ def do_train(cfg: DictDefault, cli_args: TrainerCliArgs) -> None: dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) model, tokenizer, trainer = train(cfg=cfg, dataset_meta=dataset_meta) - plugin_manager = PluginManager.get_instance() - - del model - del tokenizer - del trainer + del model, tokenizer, trainer + plugin_manager = PluginManager.get_instance() plugin_manager.post_train_unload(cfg) -def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: +def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): """ Parses `axolotl` config, CLI args, and calls `do_train`. diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 83bfb1c83a..b151be8fac 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -36,7 +36,7 @@ from transformers.training_args import OptimizerNames from trl.trainer.utils import RewardDataCollatorWithPadding -from axolotl.core.trainers.base import ( +from axolotl.core.trainers import ( AxolotlCPOTrainer, AxolotlKTOTrainer, AxolotlMambaTrainer, @@ -762,6 +762,10 @@ def build(self, total_num_steps): self.cfg.kd_top_k_before_softmax ) + training_arguments_kwargs["sequence_parallel_degree"] = ( + self.cfg.sequence_parallel_degree + ) + if self.cfg.reward_model: training_args_cls = AxolotlRewardConfig elif self.cfg.process_reward_model: @@ -845,9 +849,10 @@ def build_collator( self, training_args: AxolotlTrainingArguments, is_eval=False, **kwargs ): if training_args.pretraining: - if self.cfg.pretraining_sample_concatenation is False: - return DataCollatorForSeq2Seq(self.tokenizer, **kwargs) - if self.cfg.micro_batch_size > 1: + if ( + self.cfg.pretraining_sample_concatenation is False + or self.cfg.micro_batch_size > 1 + ): return DataCollatorForSeq2Seq(self.tokenizer, **kwargs) return None @@ -875,9 +880,7 @@ def build_collator( if "max_length" in kwargs: kwargs.pop("max_length") elif use_batch_sampler_collator: - if self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES: - collator = V2BatchSamplerDataCollatorForSeq2Seq - elif ( + if self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES or ( self.cfg.model_config_type in ["llama"] and self.cfg.flash_attention is not True ): @@ -908,6 +911,8 @@ def build_collator( collator = DataCollatorForSeq2Seq kwargs["return_tensors"] = "pt" + if issubclass(collator, DataCollatorForSeq2Seq): + kwargs["sequence_parallel_degree"] = training_args.sequence_parallel_degree return collator( *collator_args, diff --git a/src/axolotl/core/trainers/__init__.py b/src/axolotl/core/trainers/__init__.py index e69de29bb2..32a889af9a 100644 --- a/src/axolotl/core/trainers/__init__.py +++ b/src/axolotl/core/trainers/__init__.py @@ -0,0 +1,18 @@ +"""Init for axolotl.core.trainers""" + +# pylint: disable=unused-import +# flake8: noqa + +from .base import AxolotlTrainer +from .dpo.trainer import AxolotlDPOTrainer +from .grpo.trainer import AxolotlGRPOTrainer +from .mamba import AxolotlMambaTrainer +from .relora import ReLoRATrainer +from .trl import ( + AxolotlCPOTrainer, + AxolotlKTOTrainer, + AxolotlORPOTrainer, + AxolotlPRMTrainer, + AxolotlRewardTrainer, + TRLPPOTrainer, +) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 6570db9670..9267dd0409 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -1,365 +1,47 @@ -""" -module for customized trainers -""" +"""Module for customized trainers""" + +# pylint: disable=too-many-lines from __future__ import annotations -# pylint: disable=too-many-lines import logging import os from collections import defaultdict from functools import wraps -from typing import Dict, Literal, Optional +from typing import Any, Literal +import datasets import torch from datasets import Dataset -from peft.optimizers import create_loraplus_optimizer from torch import nn -from torch.optim.lr_scheduler import OneCycleLR -from torch.utils.data import BatchSampler, DataLoader, RandomSampler, SequentialSampler +from torch.utils.data import ( + BatchSampler, + DataLoader, + RandomSampler, + Sampler, + SequentialSampler, +) from transformers import Trainer from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, seed_worker -from transformers.utils import is_sagemaker_mp_enabled -from trl import CPOTrainer, KTOTrainer, ORPOTrainer, PRMTrainer, RewardTrainer from trl.trainer.utils import pad_to_length +from typing_extensions import override -from axolotl.integrations.base import BaseOptimizerFactory -from axolotl.monkeypatch.relora import ReLoRAScheduler -from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths -from axolotl.utils.schedulers import ( - RexLR, - get_cosine_schedule_with_min_lr, - get_cosine_schedule_with_quadratic_warmup, - get_cosine_schedule_with_warmup_decay_constant, +from axolotl.core.trainers.mixins import ( + OptimizerMixin, + SchedulerMixin, + SequenceParallelMixin, ) +from axolotl.core.trainers.utils import ( + sanitize_kwargs_for_ds_tagging, + sanitize_kwargs_for_tagging, +) +from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths -if is_sagemaker_mp_enabled(): - import smdistributed.modelparallel.torch as smp - -LOG = logging.getLogger("axolotl.core.trainer_builder") - - -def _sanitize_kwargs_for_tagging(tag_names, kwargs=None): - if isinstance(tag_names, str): - tag_names = [tag_names] - - if kwargs is not None: - if "tags" not in kwargs: - kwargs["tags"] = tag_names - elif "tags" in kwargs and isinstance(kwargs["tags"], list): - kwargs["tags"].extend(tag_names) - elif "tags" in kwargs and isinstance(kwargs["tags"], str): - tag_names.append(kwargs["tags"]) - kwargs["tags"] = tag_names - - return kwargs - - -def _sanitize_kwargs_for_ds_tagging(dataset_tags, kwargs=None): - if isinstance(dataset_tags, str): - dataset_tags = [dataset_tags] - - if (dataset_tags is not None) and (kwargs is not None): - if "dataset_tags" not in kwargs: - kwargs["dataset_tags"] = dataset_tags - elif "dataset_tags" in kwargs and isinstance(kwargs["dataset_tags"], list): - kwargs["dataset_tags"].extend(dataset_tags) - elif "dataset_tags" in kwargs and isinstance(kwargs["dataset_tags"], str): - dataset_tags.append(kwargs["dataset_tags"]) - kwargs["dataset_tags"] = dataset_tags - - return kwargs - - -class SchedulerMixin(Trainer): - """ - Mixin class for scheduler setup in CausalTrainer. - """ - - args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] - - def create_scheduler( - self, num_training_steps: int, optimizer: torch.optim.Optimizer = None - ): - """ - Setup the scheduler. The optimizer of the trainer must have been set up either before this method is called or - passed as an argument. - - Args: - num_training_steps (int): The number of training steps to do. - optimizer (torch.optim.Optimizer): The training optimizer - """ - use_cosine_quadratic = ( - self.args.lr_scheduler_type == "cosine" - and self.args.lr_quadratic_warmup is True - ) - - use_cosine_min_lr = ( - self.args.lr_scheduler_type == "cosine" - and self.args.cosine_min_lr_ratio is not None - ) - - # fmt: off - if self.lr_scheduler is None: # type: ignore # pylint: disable=access-member-before-definition - # fmt: on - if self.args.alternate_lr_scheduler_type == "one_cycle": - num_warmup_steps = self.args.get_warmup_steps(num_training_steps) - pct_start = num_warmup_steps / num_training_steps - extra_lr_kwargs = {} - if "pct_start" not in self.args.lr_scheduler_kwargs: - extra_lr_kwargs["pct_start"] = pct_start - if "anneal_strategy" not in self.args.lr_scheduler_kwargs: - extra_lr_kwargs["anneal_strategy"] = "cos" - - self.lr_scheduler = OneCycleLR( - optimizer, - max_lr=self.args.learning_rate, - total_steps=num_training_steps, - **extra_lr_kwargs, - **self.args.lr_scheduler_kwargs, - ) - elif self.args.alternate_lr_scheduler_type == "rex": - if use_cosine_min_lr: - assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" - - self.lr_scheduler = RexLR( - optimizer=optimizer, - max_lr=self.args.learning_rate, - min_lr=0 if not use_cosine_min_lr else (self.args.learning_rate * self.args.cosine_min_lr_ratio), - total_steps=num_training_steps, - num_warmup_steps=self.args.get_warmup_steps(num_training_steps), - ) - elif use_cosine_quadratic: - if use_cosine_min_lr: - LOG.warning("Both cosine quadratic warmup and min lr detected. Using quadratic warmup.") - - self.lr_scheduler = get_cosine_schedule_with_quadratic_warmup( # pylint: disable=attribute-defined-outside-init - optimizer, - num_warmup_steps=self.args.get_warmup_steps(num_training_steps), - num_training_steps=num_training_steps, - ) - elif self.args.cosine_min_lr_ratio and self.args.cosine_constant_lr_ratio and use_cosine_min_lr: - assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" - assert 0 <= self.args.cosine_constant_lr_ratio <= 1.0, "cosine_constant_lr_ratio must be between 0.0 and 1.0" - self.lr_scheduler = get_cosine_schedule_with_warmup_decay_constant( # pylint: disable=attribute-defined-outside-init - optimizer, - num_warmup_steps=self.args.get_warmup_steps(num_training_steps), - num_training_steps=num_training_steps, - min_lr_ratio=self.args.cosine_min_lr_ratio, - constant_lr_ratio=self.args.cosine_constant_lr_ratio, - ) - elif self.args.cosine_min_lr_ratio and use_cosine_min_lr: - assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" - self.lr_scheduler = get_cosine_schedule_with_min_lr( # pylint: disable=attribute-defined-outside-init - optimizer, - num_warmup_steps=self.args.get_warmup_steps(num_training_steps), - num_training_steps=num_training_steps, - min_lr_ratio=self.args.cosine_min_lr_ratio, - ) - else: - return super().create_scheduler(num_training_steps, optimizer=optimizer) - else: - if use_cosine_quadratic: - LOG.warning("axolotl's cosine scheduler with quadratic warmup not used (e.g., because of deepspeed).") - - if use_cosine_min_lr: - LOG.warning("axolotl's cosine scheduler with min lr not used (e.g., because of deepspeed).") - - return self.lr_scheduler - - -class OptimizerMixin(Trainer): - """ - Mixin class for shared handling of building custom optimizers - """ - - args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] - - def create_optimizer_grouped_parameters( - self, opt_model, optimizer_kwargs - ) -> list[dict]: - decay_parameters = self.get_decay_parameter_names(opt_model) - params: dict = { - "to_weight_decay": {}, # LayerNorm and bias - "embeddings": {}, # lm_head, embed_tokens, - "no_weight_decay": {}, - } - lr_groups_lookup = {} - lr_groups_learning_rates = {} - if self.args.lr_groups: - for lr_group in self.args.lr_groups: - group_name = lr_group["name"] - group_modules = lr_group["modules"] - for module in group_modules: - lr_groups_lookup[module] = group_name - lr_groups_learning_rates[group_name] = lr_group["lr"] - params[f"to_weight_decay_{group_name}"] = {} - - for name, param in opt_model.named_parameters(): - if not param.requires_grad: - continue - if name.endswith("modules_to_save.default.weight") or any( - embed_name in name for embed_name in ["embed_tokens", "lm_head"] - ): - params["embeddings"][name] = param - elif name in decay_parameters: - lr_group_modules = [ - group_modules - for group_modules in lr_groups_lookup - if group_modules in name - ] - if lr_groups_lookup and any(lr_group_modules): - lr_group_module = lr_group_modules[0] - group_name = lr_groups_lookup[lr_group_module] - params[f"to_weight_decay_{group_name}"][name] = param - else: - params["to_weight_decay"][name] = param - else: - params["no_weight_decay"][name] = param - optimizer_grouped_parameters = [] - if params["to_weight_decay"]: - optimizer_grouped_parameters.append( - { - "params": list(params["to_weight_decay"].values()), - "weight_decay": self.args.weight_decay, - "lr": optimizer_kwargs["lr"], - } - ) - if params["embeddings"]: - lr = optimizer_kwargs["lr"] # pylint: disable=invalid-name - if self.args.embedding_lr_scale: - lr *= self.args.embedding_lr_scale # pylint: disable=invalid-name - elif self.args.embedding_lr: - lr = self.args.embedding_lr # pylint: disable=invalid-name - optimizer_grouped_parameters.append( - { - "params": list(params["embeddings"].values()), - "weight_decay": 0.0, - "lr": lr, - } - ) - if params["no_weight_decay"]: - optimizer_grouped_parameters.append( - { - "params": list(params["no_weight_decay"].values()), - "weight_decay": 0.0, - "lr": optimizer_kwargs["lr"], - } - ) - for group_name, group_lr in lr_groups_learning_rates.items(): - if params[f"to_weight_decay_{group_name}"]: - optimizer_grouped_parameters.append( - { - "params": list( - params[f"to_weight_decay_{group_name}"].values() - ), - "weight_decay": self.args.weight_decay, - "lr": group_lr, - } - ) - - return optimizer_grouped_parameters - - def create_optimizer(self): - if ( - self.args.loraplus_lr_ratio is None - and self.args.embedding_lr_scale is None - and self.args.embedding_lr is None - and self.args.lr_groups is None - and self.optimizer_cls_and_kwargs is None - ): - return super().create_optimizer() - - opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model - - if ( - not self.optimizer - and self.optimizer_cls_and_kwargs is not None - and issubclass(self.optimizer_cls_and_kwargs[0], BaseOptimizerFactory) - ): - optimizer_factory_cls, optimizer_kwargs = self.optimizer_cls_and_kwargs - self.optimizer = optimizer_factory_cls()( - opt_model, self.args, **optimizer_kwargs - ) - - if not self.optimizer: - if self.optimizer_cls_and_kwargs is not None: - optimizer_cls, optimizer_kwargs = self.optimizer_cls_and_kwargs - else: - optimizer_cls, optimizer_kwargs = self.get_optimizer_cls_and_kwargs( - self.args, opt_model - ) - - optimizer_grouped_parameters = self.create_optimizer_grouped_parameters( - opt_model, optimizer_kwargs - ) - - if self.args.loraplus_lr_ratio is not None: - loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) - loraplus_lr_embedding = getattr( - self.args, "loraplus_lr_embedding", 1e-6 - ) - self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init - opt_model, - optimizer_cls, - loraplus_lr_ratio=loraplus_lr_ratio, - loraplus_lr_embedding=loraplus_lr_embedding, - **optimizer_kwargs, - ) - else: - # Overwrite `params` in case it's created by `get_optimizer_cls_and_kwargs` - # e.g. for GaLore optimizer. - if "params" in optimizer_kwargs: - optimizer_grouped_parameters = optimizer_kwargs.pop("params") - - # Overwrite `model` in case it's created by `get_optimizer_cls_and_kwargs` - # e.g. for LOMO optimizer. - if "model" in optimizer_kwargs: - optimizer_grouped_parameters = optimizer_kwargs.pop("model") - - # For layer-wise dummy optimizers we overwrite optimizer_grouped_parameters with `optimizer_dict` - # to avoid arguments conflicts. - if "optimizer_dict" in optimizer_kwargs: - optimizer_grouped_parameters = optimizer_kwargs.pop( - "optimizer_dict" - ) - - self.optimizer = optimizer_cls( - optimizer_grouped_parameters, **optimizer_kwargs - ) - - if optimizer_cls.__name__ == "Adam8bit": - import bitsandbytes - - manager = bitsandbytes.optim.GlobalOptimManager.get_instance() - - skipped = 0 - for module in opt_model.modules(): - if isinstance(module, nn.Embedding): - skipped += sum( - { - p.data_ptr(): p.numel() for p in module.parameters() - }.values() - ) - LOG.info(f"skipped {module}: {skipped/2**20}M params") - manager.register_module_override( - module, "weight", {"optim_bits": 32} - ) - LOG.debug(f"bitsandbytes: will optimize {module} in fp32") - LOG.info(f"skipped: {skipped/2**20}M params") - - if is_sagemaker_mp_enabled(): - self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init - self.optimizer - ) - - return self.optimizer +LOG = logging.getLogger(__name__) -class AxolotlTrainer(SchedulerMixin, OptimizerMixin, Trainer): - """ - Extend the base Trainer for axolotl helpers - """ +class AxolotlTrainer(SchedulerMixin, OptimizerMixin, SequenceParallelMixin, Trainer): + """Extend the base Trainer for axolotl helpers""" args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] tag_names = ["axolotl"] @@ -376,12 +58,18 @@ def __init__( self.eval_data_collator = eval_data_collator self.dataset_tags = dataset_tags self._signature_columns = None # workaround for pylint + super().__init__(*_args, **kwargs) + self.train_data_collator = self.data_collator self._stored_metrics = defaultdict(lambda: defaultdict(list)) if self.args.orpo_alpha: self.loss_fct = torch.nn.CrossEntropyLoss(reduction="none") + # Initialize sequence parallelism if enabled + if self.args.sequence_parallel_degree > 1: + self._setup_sequence_parallel() + def _wrap_model(self, model, training=True, dataloader=None): if self.args.torch_compile: torch._dynamo.config.accumulated_cache_size_limit = ( # pylint: disable=protected-access @@ -394,142 +82,247 @@ def _wrap_model(self, model, training=True, dataloader=None): ) return super()._wrap_model(model, training=training, dataloader=dataloader) - def _get_train_sampler(self) -> Optional[torch.utils.data.Sampler]: - if self.args.sample_packing and not self.args.pretraining: - if self.args.multipack_real_batches: - batch_size = self.args.per_device_train_batch_size - batch_max_len = self.args.max_seq_length - else: - batch_size = 1 - train_batch_size = ( - self.state.train_batch_size or self.args.per_device_train_batch_size - ) - batch_max_len = train_batch_size * self.args.max_seq_length + def _create_multipack_sampler( + self, base_sampler: Sampler, dataset: Dataset + ) -> MultipackBatchSampler: + """ + Helper method to create a `MultipackBatchSampler` for multipacking sequences + for training. - if self.args.curriculum_sampling: - sampler = SequentialSampler(self.train_dataset) - else: - sampler = RandomSampler(self.train_dataset) - - return MultipackBatchSampler( - sampler, - lengths=get_dataset_lengths(self.train_dataset), - packing_efficiency_estimate=self.args.sample_packing_efficiency, - batch_max_len=batch_max_len, - batch_size=batch_size, - group_size=self.args.sample_packing_group_size, - bin_size=self.args.sample_packing_bin_size, - drop_last=True, + Args: + base_sampler: Sampler to wrap with `MultipackBatchSampler`. + dataset: Dataset to sample from. + + Returns: + Multipack (sample packing) batch sampler. + """ + if self.args.multipack_real_batches: + batch_size = self.args.per_device_train_batch_size + batch_max_len = self.args.max_seq_length + else: + batch_size = 1 + train_batch_size = ( + self.state.train_batch_size or self.args.per_device_train_batch_size ) - if self.args.curriculum_sampling: - return SequentialSampler(self.train_dataset) - return super()._get_train_sampler() - - def _get_eval_sampler( - self, eval_dataset: Dataset - ) -> Optional[torch.utils.data.Sampler]: - if self.args.sample_packing and self.args.eval_sample_packing is not False: - if self.args.multipack_real_batches: - batch_size = self.args.per_device_eval_batch_size - batch_max_len = self.args.max_seq_length - else: - batch_size = 1 - batch_max_len = ( - self.args.per_device_eval_batch_size * self.args.max_seq_length - ) - return MultipackBatchSampler( - SequentialSampler(eval_dataset), - lengths=get_dataset_lengths(self.eval_dataset), - packing_efficiency_estimate=self.args.sample_packing_efficiency, - batch_max_len=batch_max_len, - batch_size=batch_size, - group_size=self.args.sample_packing_group_size, - bin_size=self.args.sample_packing_bin_size, - drop_last=True, + batch_max_len = train_batch_size * self.args.max_seq_length + + return MultipackBatchSampler( + base_sampler, + lengths=get_dataset_lengths(dataset), + packing_efficiency_estimate=self.args.sample_packing_efficiency, + batch_max_len=batch_max_len, + batch_size=batch_size, + drop_last=True, + ) + + def _get_train_sampler(self) -> Sampler | None: + """ + Helper method to get the sampler for training. Handles cases for sequence + parallelism, sample packing, and curriculum sampling (sequential). + + Returns: + If the dataset is non-empty, a sampler is returned, the type of which + depends on the passed training args. + """ + use_sample_packing = self.args.sample_packing and not self.args.pretraining + + # Determine the base sampler first + if self.args.sequence_parallel_degree > 1: + base_sampler = self._sp_get_train_sampler(self.train_dataset) + elif self.args.curriculum_sampling: + base_sampler = SequentialSampler(self.train_dataset) + elif use_sample_packing: + base_sampler = RandomSampler(self.train_dataset) + else: + # Default to parent class implementation for standard random sampling + return super()._get_train_sampler() + + # Apply multipack wrapper if needed + if use_sample_packing: + return self._create_multipack_sampler( + base_sampler=base_sampler, + dataset=self.train_dataset, ) - return super()._get_eval_sampler(eval_dataset) - def get_train_dataloader(self) -> DataLoader: - if self.args.sample_packing and not self.args.pretraining: - train_dataset = self.train_dataset - if "length" in train_dataset.features.keys(): - train_dataset = train_dataset.remove_columns(["length"]) - data_collator = self.data_collator - dataloader_params = { - "batch_size": self._train_batch_size, - "collate_fn": data_collator, - "num_workers": self.args.dataloader_num_workers, - "pin_memory": self.args.dataloader_pin_memory, - } - if self.args.dataloader_prefetch_factor: - dataloader_params["prefetch_factor"] = ( - self.args.dataloader_prefetch_factor - ) + return base_sampler - sampler = self._get_train_sampler() + def _get_eval_sampler(self, eval_dataset: Dataset | None = None) -> Sampler | None: + """ + Helper method to get the sampler for evaluation. Handles sequence parallelism + and sample packing cases. + + Returns: + If the dataset is non-empty, a sampler is returned, the type of which + depends on the passed training args. + """ + eval_dataset = eval_dataset if eval_dataset is not None else self.eval_dataset + + # Multipacking enabled if training is enabled and eval is not explicitly disabled + use_multipack = ( + self.args.sample_packing and self.args.eval_sample_packing is not False + ) + + # Determine the base sampler + if self.args.sequence_parallel_degree > 1: + base_sampler = self._sp_get_eval_sampler(eval_dataset) + elif use_multipack: + base_sampler = SequentialSampler(eval_dataset) + else: + return super()._get_eval_sampler(eval_dataset) + + # Apply multipack wrapper if needed + if use_multipack: + return self._create_multipack_sampler( + base_sampler=base_sampler, + dataset=eval_dataset, + ) + + return base_sampler + + def _create_dataloader_params(self, is_eval=False, custom_batch_size=None): + """Create common dataloader parameters for train or eval.""" + batch_size = custom_batch_size or ( + self.args.eval_batch_size if is_eval else self._train_batch_size + ) + + params = { + "batch_size": batch_size, + "collate_fn": self.data_collator, + "num_workers": self.args.dataloader_num_workers, + "pin_memory": self.args.dataloader_pin_memory, + } + + # Add persistent workers only for training + if not is_eval and hasattr(self.args, "dataloader_persistent_workers"): + params["persistent_workers"] = self.args.dataloader_persistent_workers + + # Add prefetch factor if specified + if self.args.dataloader_prefetch_factor: + params["prefetch_factor"] = self.args.dataloader_prefetch_factor + + return params + + def _prepare_dataloader( + self, dataset, sampler, is_eval=False, custom_batch_size=None + ): + """Prepare a dataloader with the given dataset and sampler.""" + # Get base parameters + dataloader_params = self._create_dataloader_params(is_eval, custom_batch_size) + + # Add sampler configuration + if not isinstance(dataset, torch.utils.data.IterableDataset): if isinstance(sampler, BatchSampler): + # batch_size and batch_sampler are mutually exclusive dataloader_params["batch_sampler"] = sampler del dataloader_params["batch_size"] else: dataloader_params["sampler"] = sampler dataloader_params["drop_last"] = self.args.dataloader_drop_last - dataloader_params["worker_init_fn"] = seed_worker + if not is_eval: + dataloader_params["worker_init_fn"] = seed_worker + + # Create the dataloader + dataloader = DataLoader(dataset, **dataloader_params) + + if self.args.sample_packing and ( + (not is_eval and not self.args.pretraining) + or (is_eval and self.args.eval_sample_packing is not False) + ): self.accelerator.even_batches = False - return self.accelerator.prepare_data_loader( - DataLoader(train_dataset, **dataloader_params) + + # Return unprepared dataloader if using sequence parallelism + if self.args.sequence_parallel_degree > 1: + return dataloader + + # Otherwise prepare with accelerator + return self.accelerator.prepare_data_loader(dataloader) + + def get_train_dataloader(self) -> DataLoader: + """Get dataloader for training""" + train_dataset = self.train_dataset + data_collator = self.data_collator # type: ignore + + # Handle dataset preprocessing + if isinstance(train_dataset, datasets.Dataset): + if self.args.sample_packing and not self.args.pretraining: + train_dataset = train_dataset.remove_columns(["length"]) + if not self.args.sample_packing or self.args.pretraining: + train_dataset = self._remove_unused_columns( + train_dataset, description="training" + ) + else: + self.data_collator = self._get_collator_with_removed_columns( # pylint: disable=attribute-defined-outside-init + data_collator, + description="training", ) - return super().get_train_dataloader() - def get_eval_dataloader(self, eval_dataset: Optional[Dataset] = None) -> DataLoader: + # Get sampler and create dataloader + sampler = self._get_train_sampler() + return self._prepare_dataloader(train_dataset, sampler, is_eval=False) + + def get_eval_dataloader(self, eval_dataset: Dataset | None = None) -> DataLoader: + """Get dataloader for evaluation""" + eval_dataset = eval_dataset if eval_dataset is not None else self.eval_dataset + + # Handle special case: sample packing is enabled but eval_sample_packing is False if self.args.sample_packing and self.args.eval_sample_packing is False: self.data_collator = ( # pylint: disable=attribute-defined-outside-init self.eval_data_collator ) - if eval_dataset: + if "length" in eval_dataset.column_names: eval_dataset = eval_dataset.remove_columns(["length"]) dataloader = super().get_eval_dataloader(eval_dataset) self.data_collator = ( # pylint: disable=attribute-defined-outside-init self.train_data_collator ) + return dataloader - if self.args.sample_packing and self.args.eval_sample_packing is not False: - eval_dataset = ( - eval_dataset if eval_dataset is not None else self.eval_dataset + # Handle sample packing or sequence parallelism + if ( + self.args.sample_packing + and self.args.eval_sample_packing is not False + or self.args.sequence_parallel_degree > 1 + ): + # Get appropriate data collator + self.data_collator = ( # pylint: disable=attribute-defined-outside-init + self.eval_data_collator + if hasattr(self, "eval_data_collator") and self.eval_data_collator + else self.data_collator ) + if "length" in eval_dataset.column_names: + eval_dataset = eval_dataset.remove_columns(["length"]) - eval_sampler = self._get_eval_sampler(eval_dataset) - eval_dataset = eval_dataset.remove_columns(["length"]) - data_collator = self.data_collator - dataloader_params = { - "batch_size": self.args.eval_batch_size, - "collate_fn": data_collator, - "num_workers": self.args.dataloader_num_workers, - "pin_memory": self.args.dataloader_pin_memory, - } - if self.args.dataloader_prefetch_factor: - dataloader_params["prefetch_factor"] = ( - self.args.dataloader_prefetch_factor - ) - - if isinstance(eval_sampler, BatchSampler): - dataloader_params["batch_sampler"] = eval_sampler - del dataloader_params["batch_size"] - else: - dataloader_params["sampler"] = eval_sampler - dataloader_params["drop_last"] = self.args.dataloader_drop_last + # Handle dataset preprocessing for SP + if self.args.sequence_parallel_degree > 1: + if isinstance(eval_dataset, datasets.Dataset): + eval_dataset = self._remove_unused_columns( + eval_dataset, description="evaluation" + ) + else: + self.data_collator = self._get_collator_with_removed_columns( # pylint: disable=attribute-defined-outside-init + self.data_collator, description="evaluation" + ) - self.accelerator.even_batches = False - return self.accelerator.prepare_data_loader( - DataLoader(eval_dataset, **dataloader_params) + # Use eval_batch_size for sample packing, per_device_eval_batch_size otherwise + batch_size = ( + self.args.eval_batch_size + if self.args.sample_packing + else self.args.per_device_eval_batch_size ) + sampler = self._get_eval_sampler(eval_dataset) + dataloader = self._prepare_dataloader( + eval_dataset, sampler, is_eval=True, custom_batch_size=batch_size + ) + + return dataloader return super().get_eval_dataloader(eval_dataset) def _get_bench_sampler( self, bench_dataset: Dataset - ) -> Optional[torch.utils.data.Sampler]: + ) -> torch.utils.data.Sampler | None: if self.args.world_size <= 1: return SequentialSampler(bench_dataset) return None @@ -554,6 +347,7 @@ def get_bench_dataloader( return DataLoader(bench_dataset, **dataloader_params) # return self.accelerator.prepare(DataLoader(bench_dataset, **dataloader_params)) + @override def compute_loss( self, model, inputs, return_outputs=False, num_items_in_batch=None ): @@ -570,6 +364,7 @@ def compute_loss( return_outputs=return_outputs, num_items_in_batch=num_items_in_batch, ) + return super().compute_loss( model, inputs, @@ -744,10 +539,10 @@ def push_to_hub(self, *args, **kwargs) -> str: Overwrite the `push_to_hub` method in order to force-add the tags when pushing the model on the Hub. Please refer to `~transformers.Trainer.push_to_hub` for more details. """ - kwargs = _sanitize_kwargs_for_ds_tagging( + kwargs = sanitize_kwargs_for_ds_tagging( dataset_tags=self.dataset_tags, kwargs=kwargs ) - kwargs = _sanitize_kwargs_for_tagging(tag_names=self.tag_names, kwargs=kwargs) + kwargs = sanitize_kwargs_for_tagging(tag_names=self.tag_names, kwargs=kwargs) return super().push_to_hub(*args, **kwargs) @@ -764,15 +559,13 @@ def create_accelerator_and_postprocess(self): return res - def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> None: + def log(self, logs: dict[str, float], start_time: float | None = None) -> None: """ Log `logs` on the various objects watching training, including stored metrics. Args: - logs (`Dict[str, float]`): - The values to log. - start_time (`Optional[float]`): - The start of training. + logs: The values to log. + start_time: The start of training. """ # logs either has 'loss' or 'eval_loss' train_eval = "train" if "loss" in logs else "eval" @@ -784,7 +577,7 @@ def log(self, logs: Dict[str, float], start_time: Optional[float] = None) -> Non return super().log(logs, start_time) def store_metrics( - self, metrics: Dict[str, float], train_eval: Literal["train", "eval"] = "train" + self, metrics: dict[str, float], train_eval: Literal["train", "eval"] = "train" ) -> None: for key, value in metrics.items(): self._stored_metrics[train_eval][key].append(value) @@ -797,110 +590,26 @@ def _save_checkpoint(self, model, trial, **kwargs): os.makedirs(output_dir, exist_ok=True) return super()._save_checkpoint(model, trial, **kwargs) - -class AxolotlMambaTrainer(AxolotlTrainer): - """ - Mamba specific trainer to handle loss calculation - """ - - tag_names = ["axolotl", "mamba"] - - def compute_loss( - self, - model, - inputs, - return_outputs=False, # pylint: disable=unused-argument - num_items_in_batch=None, # pylint: disable=unused-argument - ): - input_ids = inputs.pop("input_ids") - lm_logits = model(input_ids).logits - - labels = input_ids.to(lm_logits.device) - shift_logits = lm_logits[:, :-1, :].contiguous() - labels = labels[:, 1:].contiguous() - - loss_fct = torch.nn.CrossEntropyLoss() - lm_loss = loss_fct( - shift_logits.view(-1, shift_logits.size(-1)), labels.view(-1) - ) - - return lm_loss - - -class ReLoRATrainer(AxolotlTrainer): - """ - Trainer subclass that uses the OneCycleLR scheduler - """ - - tag_names = ["axolotl", "relora"] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.lr_scheduler = None - - def create_scheduler( + def training_step( self, - num_training_steps: int, - optimizer: Optional[torch.optim.Optimizer] = None, - ): - optimizer = self.optimizer if optimizer is None else optimizer - lr_scheduler = super().create_scheduler(num_training_steps, optimizer) - - if self.args.relora_steps: - warmup_steps = ( - self.args.relora_warmup_steps if self.args.relora_warmup_steps else 10 - ) - anneal_steps = ( - self.args.relora_anneal_steps if self.args.relora_anneal_steps else 1 - ) - self.lr_scheduler = ReLoRAScheduler( - optimizer, - lr_scheduler, - self.args.relora_steps, - anneal_steps, - warmup_steps, - ) - else: - self.lr_scheduler = lr_scheduler - - return self.lr_scheduler - - -class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): - """ - Extend the base ORPOTrainer for axolotl helpers - """ - - tag_names = ["axolotl", "orpo"] - - -class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): - """ - Extend the base KTOTrainer for axolotl helpers - """ - - tag_names = ["axolotl", "kto"] - - -class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): - """ - Extend the base CPOTrainer for axolotl helpers - """ - - tag_names = ["axolotl", "cpo"] - - -class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): - """ - Extend the base RewardTrainer for axolotl helpers - """ - - tag_names = ["axolotl", "reward"] + model: nn.Module, + inputs: dict[str, torch.Tensor | Any], + num_items_in_batch: int | None = None, + ) -> torch.Tensor: + """ + Perform a training step on a batch of inputs. Overrides the + `transformers.trainer.Trainer` method to handle sequence parallelism if + enabled. + Args: + model: Model to perform training step for. + inputs: Dictionary mapping. + """ + # Set up sequence parallelism for this step if enabled + if self.args.sequence_parallel_degree > 1: + self._update_ring_flash_attn_params(inputs) -class AxolotlPRMTrainer(SchedulerMixin, PRMTrainer): - """ - Extend the base trl.PRMTrainer for axolotl helpers - """ + # Proceed with normal training step + loss = super().training_step(model, inputs, num_items_in_batch) - tag_names = ["axolotl", "prm"] + return loss diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index 38b657260f..9eb870a3a9 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -13,10 +13,10 @@ from transformers.utils import is_sagemaker_mp_enabled from trl import DPOTrainer -from axolotl.core.trainers.base import ( - SchedulerMixin, - _sanitize_kwargs_for_ds_tagging, - _sanitize_kwargs_for_tagging, +from axolotl.core.trainers.mixins import SchedulerMixin +from axolotl.core.trainers.utils import ( + sanitize_kwargs_for_ds_tagging, + sanitize_kwargs_for_tagging, ) if is_sagemaker_mp_enabled(): @@ -74,10 +74,10 @@ def push_to_hub(self, *args, **kwargs) -> str: Overwrite the `push_to_hub` method in order to force-add the tags when pushing the model on the Hub. Please refer to `~transformers.Trainer.push_to_hub` for more details. """ - kwargs = _sanitize_kwargs_for_ds_tagging( + kwargs = sanitize_kwargs_for_ds_tagging( dataset_tags=self.dataset_tags, kwargs=kwargs ) - kwargs = _sanitize_kwargs_for_tagging(tag_names=self.tag_names, kwargs=kwargs) + kwargs = sanitize_kwargs_for_tagging(tag_names=self.tag_names, kwargs=kwargs) return super().push_to_hub(*args, **kwargs) diff --git a/src/axolotl/core/trainers/mamba.py b/src/axolotl/core/trainers/mamba.py new file mode 100644 index 0000000000..38792e3896 --- /dev/null +++ b/src/axolotl/core/trainers/mamba.py @@ -0,0 +1,32 @@ +"""Module for mamba trainer""" + +import torch + +from axolotl.core.trainers.base import AxolotlTrainer + + +class AxolotlMambaTrainer(AxolotlTrainer): + """Mamba specific trainer to handle loss calculation""" + + tag_names = ["axolotl", "mamba"] + + def compute_loss( + self, + model, + inputs, + return_outputs=False, # pylint: disable=unused-argument + num_items_in_batch=None, # pylint: disable=unused-argument + ): + input_ids = inputs.pop("input_ids") + lm_logits = model(input_ids).logits + + labels = input_ids.to(lm_logits.device) + shift_logits = lm_logits[:, :-1, :].contiguous() + labels = labels[:, 1:].contiguous() + + loss_fct = torch.nn.CrossEntropyLoss() + lm_loss = loss_fct( + shift_logits.view(-1, shift_logits.size(-1)), labels.view(-1) + ) + + return lm_loss diff --git a/src/axolotl/core/trainers/mixins/__init__.py b/src/axolotl/core/trainers/mixins/__init__.py new file mode 100644 index 0000000000..12c8277fc3 --- /dev/null +++ b/src/axolotl/core/trainers/mixins/__init__.py @@ -0,0 +1,8 @@ +"""Init for axolotl.core.trainers.mixins""" + +# pylint: disable=unused-import +# flake8: noqa + +from .optimizer import OptimizerMixin +from .scheduler import SchedulerMixin +from .sequence_parallel import SequenceParallelMixin diff --git a/src/axolotl/core/trainers/mixins/optimizer.py b/src/axolotl/core/trainers/mixins/optimizer.py new file mode 100644 index 0000000000..bde58aa1d5 --- /dev/null +++ b/src/axolotl/core/trainers/mixins/optimizer.py @@ -0,0 +1,201 @@ +"""Module for Axolotl trainer optimizer mixin""" + +import logging + +from peft.optimizers import create_loraplus_optimizer +from torch import nn +from transformers.trainer import Trainer +from transformers.utils import is_sagemaker_mp_enabled + +from axolotl.integrations.base import BaseOptimizerFactory + +if is_sagemaker_mp_enabled(): + import smdistributed.modelparallel.torch as smp + +LOG = logging.getLogger(__name__) + + +class OptimizerMixin(Trainer): + """Mixin class for shared handling of building custom optimizers""" + + args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] + + def create_optimizer_grouped_parameters( + self, opt_model, optimizer_kwargs + ) -> list[dict]: + decay_parameters = self.get_decay_parameter_names(opt_model) + params: dict = { + "to_weight_decay": {}, # LayerNorm and bias + "embeddings": {}, # lm_head, embed_tokens, + "no_weight_decay": {}, + } + lr_groups_lookup = {} + lr_groups_learning_rates = {} + if self.args.lr_groups: + for lr_group in self.args.lr_groups: + group_name = lr_group["name"] + group_modules = lr_group["modules"] + for module in group_modules: + lr_groups_lookup[module] = group_name + lr_groups_learning_rates[group_name] = lr_group["lr"] + params[f"to_weight_decay_{group_name}"] = {} + + for name, param in opt_model.named_parameters(): + if not param.requires_grad: + continue + if name.endswith("modules_to_save.default.weight") or any( + embed_name in name for embed_name in ["embed_tokens", "lm_head"] + ): + params["embeddings"][name] = param + elif name in decay_parameters: + lr_group_modules = [ + group_modules + for group_modules in lr_groups_lookup + if group_modules in name + ] + if lr_groups_lookup and any(lr_group_modules): + lr_group_module = lr_group_modules[0] + group_name = lr_groups_lookup[lr_group_module] + params[f"to_weight_decay_{group_name}"][name] = param + else: + params["to_weight_decay"][name] = param + else: + params["no_weight_decay"][name] = param + optimizer_grouped_parameters = [] + if params["to_weight_decay"]: + optimizer_grouped_parameters.append( + { + "params": list(params["to_weight_decay"].values()), + "weight_decay": self.args.weight_decay, + "lr": optimizer_kwargs["lr"], + } + ) + if params["embeddings"]: + lr = optimizer_kwargs["lr"] # pylint: disable=invalid-name + if self.args.embedding_lr_scale: + lr *= self.args.embedding_lr_scale # pylint: disable=invalid-name + elif self.args.embedding_lr: + lr = self.args.embedding_lr # pylint: disable=invalid-name + optimizer_grouped_parameters.append( + { + "params": list(params["embeddings"].values()), + "weight_decay": 0.0, + "lr": lr, + } + ) + if params["no_weight_decay"]: + optimizer_grouped_parameters.append( + { + "params": list(params["no_weight_decay"].values()), + "weight_decay": 0.0, + "lr": optimizer_kwargs["lr"], + } + ) + for group_name, group_lr in lr_groups_learning_rates.items(): + if params[f"to_weight_decay_{group_name}"]: + optimizer_grouped_parameters.append( + { + "params": list( + params[f"to_weight_decay_{group_name}"].values() + ), + "weight_decay": self.args.weight_decay, + "lr": group_lr, + } + ) + + return optimizer_grouped_parameters + + def create_optimizer(self): + if ( + self.args.loraplus_lr_ratio is None + and self.args.embedding_lr_scale is None + and self.args.embedding_lr is None + and self.args.lr_groups is None + and self.optimizer_cls_and_kwargs is None + ): + return super().create_optimizer() + + opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model + + if ( + not self.optimizer + and self.optimizer_cls_and_kwargs is not None + and issubclass(self.optimizer_cls_and_kwargs[0], BaseOptimizerFactory) + ): + optimizer_factory_cls, optimizer_kwargs = self.optimizer_cls_and_kwargs + self.optimizer = optimizer_factory_cls()( + opt_model, self.args, **optimizer_kwargs + ) + + if not self.optimizer: + if self.optimizer_cls_and_kwargs is not None: + optimizer_cls, optimizer_kwargs = self.optimizer_cls_and_kwargs + else: + optimizer_cls, optimizer_kwargs = self.get_optimizer_cls_and_kwargs( + self.args, opt_model + ) + + optimizer_grouped_parameters = self.create_optimizer_grouped_parameters( + opt_model, optimizer_kwargs + ) + + if self.args.loraplus_lr_ratio is not None: + loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) + loraplus_lr_embedding = getattr( + self.args, "loraplus_lr_embedding", 1e-6 + ) + self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init + opt_model, + optimizer_cls, + loraplus_lr_ratio=loraplus_lr_ratio, + loraplus_lr_embedding=loraplus_lr_embedding, + **optimizer_kwargs, + ) + else: + # Overwrite `params` in case it's created by `get_optimizer_cls_and_kwargs` + # e.g. for GaLore optimizer. + if "params" in optimizer_kwargs: + optimizer_grouped_parameters = optimizer_kwargs.pop("params") + + # Overwrite `model` in case it's created by `get_optimizer_cls_and_kwargs` + # e.g. for LOMO optimizer. + if "model" in optimizer_kwargs: + optimizer_grouped_parameters = optimizer_kwargs.pop("model") + + # For layer-wise dummy optimizers we overwrite optimizer_grouped_parameters with `optimizer_dict` + # to avoid arguments conflicts. + if "optimizer_dict" in optimizer_kwargs: + optimizer_grouped_parameters = optimizer_kwargs.pop( + "optimizer_dict" + ) + + self.optimizer = optimizer_cls( + optimizer_grouped_parameters, **optimizer_kwargs + ) + + if optimizer_cls.__name__ == "Adam8bit": + import bitsandbytes + + manager = bitsandbytes.optim.GlobalOptimManager.get_instance() + + skipped = 0 + for module in opt_model.modules(): + if isinstance(module, nn.Embedding): + skipped += sum( + { + p.data_ptr(): p.numel() for p in module.parameters() + }.values() + ) + LOG.info(f"skipped {module}: {skipped/2**20}M params") + manager.register_module_override( + module, "weight", {"optim_bits": 32} + ) + LOG.debug(f"bitsandbytes: will optimize {module} in fp32") + LOG.info(f"skipped: {skipped/2**20}M params") + + if is_sagemaker_mp_enabled(): + self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init + self.optimizer + ) + + return self.optimizer diff --git a/src/axolotl/core/trainers/mixins/scheduler.py b/src/axolotl/core/trainers/mixins/scheduler.py new file mode 100644 index 0000000000..b0a5ee8957 --- /dev/null +++ b/src/axolotl/core/trainers/mixins/scheduler.py @@ -0,0 +1,113 @@ +"""Module for Axolotl trainer scheduler mixin""" + +import logging + +import torch +from torch.optim.lr_scheduler import OneCycleLR +from transformers.trainer import Trainer + +from axolotl.utils.schedulers import ( + RexLR, + get_cosine_schedule_with_min_lr, + get_cosine_schedule_with_quadratic_warmup, + get_cosine_schedule_with_warmup_decay_constant, +) + +LOG = logging.getLogger(__name__) + + +class SchedulerMixin(Trainer): + """ + Mixin class for scheduler setup in CausalTrainer. + """ + + args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] + + def create_scheduler( + self, num_training_steps: int, optimizer: torch.optim.Optimizer = None + ): + """ + Setup the scheduler. The optimizer of the trainer must have been set up either before this method is called or + passed as an argument. + + Args: + num_training_steps (int): The number of training steps to do. + optimizer (torch.optim.Optimizer): The training optimizer + """ + use_cosine_quadratic = ( + self.args.lr_scheduler_type == "cosine" + and self.args.lr_quadratic_warmup is True + ) + + use_cosine_min_lr = ( + self.args.lr_scheduler_type == "cosine" + and self.args.cosine_min_lr_ratio is not None + ) + + # fmt: off + if self.lr_scheduler is None: # type: ignore # pylint: disable=access-member-before-definition + # fmt: on + if self.args.alternate_lr_scheduler_type == "one_cycle": + num_warmup_steps = self.args.get_warmup_steps(num_training_steps) + pct_start = num_warmup_steps / num_training_steps + extra_lr_kwargs = {} + if "pct_start" not in self.args.lr_scheduler_kwargs: + extra_lr_kwargs["pct_start"] = pct_start + if "anneal_strategy" not in self.args.lr_scheduler_kwargs: + extra_lr_kwargs["anneal_strategy"] = "cos" + + self.lr_scheduler = OneCycleLR( + optimizer, + max_lr=self.args.learning_rate, + total_steps=num_training_steps, + **extra_lr_kwargs, + **self.args.lr_scheduler_kwargs, + ) + elif self.args.alternate_lr_scheduler_type == "rex": + if use_cosine_min_lr: + assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" + + self.lr_scheduler = RexLR( + optimizer=optimizer, + max_lr=self.args.learning_rate, + min_lr=0 if not use_cosine_min_lr else (self.args.learning_rate * self.args.cosine_min_lr_ratio), + total_steps=num_training_steps, + num_warmup_steps=self.args.get_warmup_steps(num_training_steps), + ) + elif use_cosine_quadratic: + if use_cosine_min_lr: + LOG.warning("Both cosine quadratic warmup and min lr detected. Using quadratic warmup.") + + self.lr_scheduler = get_cosine_schedule_with_quadratic_warmup( # pylint: disable=attribute-defined-outside-init + optimizer, + num_warmup_steps=self.args.get_warmup_steps(num_training_steps), + num_training_steps=num_training_steps, + ) + elif self.args.cosine_min_lr_ratio and self.args.cosine_constant_lr_ratio and use_cosine_min_lr: + assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" + assert 0 <= self.args.cosine_constant_lr_ratio <= 1.0, "cosine_constant_lr_ratio must be between 0.0 and 1.0" + self.lr_scheduler = get_cosine_schedule_with_warmup_decay_constant( # pylint: disable=attribute-defined-outside-init + optimizer, + num_warmup_steps=self.args.get_warmup_steps(num_training_steps), + num_training_steps=num_training_steps, + min_lr_ratio=self.args.cosine_min_lr_ratio, + constant_lr_ratio=self.args.cosine_constant_lr_ratio, + ) + elif self.args.cosine_min_lr_ratio and use_cosine_min_lr: + assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" + self.lr_scheduler = get_cosine_schedule_with_min_lr( # pylint: disable=attribute-defined-outside-init + optimizer, + num_warmup_steps=self.args.get_warmup_steps(num_training_steps), + num_training_steps=num_training_steps, + min_lr_ratio=self.args.cosine_min_lr_ratio, + ) + else: + return super().create_scheduler(num_training_steps, optimizer=optimizer) + else: + if use_cosine_quadratic: + LOG.warning("axolotl's cosine scheduler with quadratic warmup not used (e.g., because of deepspeed).") + + if use_cosine_min_lr: + LOG.warning("axolotl's cosine scheduler with min lr not used (e.g., because of deepspeed).") + + return self.lr_scheduler diff --git a/src/axolotl/core/trainers/mixins/sequence_parallel.py b/src/axolotl/core/trainers/mixins/sequence_parallel.py new file mode 100644 index 0000000000..f52c044b6f --- /dev/null +++ b/src/axolotl/core/trainers/mixins/sequence_parallel.py @@ -0,0 +1,131 @@ +"""Module for Axolotl trainer sequence parallelism mixin""" + +import logging +from typing import Any + +import torch +import torch.distributed as dist +import torch.nn.functional as F +from datasets import Dataset +from torch.utils.data import DistributedSampler, Sampler + +from axolotl.monkeypatch.attention.ring_attn import get_ring_attn_group + +LOG = logging.getLogger(__name__) + +try: + from ring_flash_attn import update_ring_flash_attn_params +except ImportError: + # We pass silently here, but raise an ImportError in our Axolotl config validation + # if cfg.sequence_parallel_degree > 1 and `ring-flash-attn` is not installed. + pass + + +class SequenceParallelMixin: + """ + Mixin class for sequence parallelism support in trainers. + + This mixin provides functionality for handling sequence parallelism, + including creating appropriate samplers, managing data partitioning, + and updating ring flash attention parameters during training. + """ + + args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] + + def _setup_sequence_parallel(self): + """Set up sequence parallelism environment.""" + self.ring_attn_group = get_ring_attn_group() + + def _create_sequence_parallel_sampler( + self, + dataset: Dataset, + shuffle: bool = True, + is_eval: bool = False, + ) -> DistributedSampler: + """ + Helper method to create sampler for sequence parallelism (SP). + + We create a distributed sampler with rank equal to the SP group ID, which + means that all ranks in the SP group receive the same sample / set of samples + per training step. We also set the number of replicas equal to the number of + SP groups, which is a bit of a hack / unintended use, but works! + + Args: + dataset: Dataset to sample from. + shuffle: Whether to shuffle the dataset. + is_eval: Whether we are creating a sampler for evaluation or training. + + Returns: + Distributed sampler. + """ + num_sp_groups = self.args.world_size // self.args.sequence_parallel_degree + sp_group_id = dist.get_rank() // self.args.sequence_parallel_degree + + return DistributedSampler( + dataset, + num_replicas=num_sp_groups, + rank=sp_group_id, + seed=self.args.seed if shuffle else None, + shuffle=shuffle, + drop_last=not is_eval, + ) + + def _sp_get_train_sampler(self, dataset) -> Sampler | None: + """ + Get a training sampler configured for sequence parallelism. + + Args: + dataset: The training dataset + + Returns: + Configured sequence parallel sampler. + """ + return self._create_sequence_parallel_sampler( + dataset, + shuffle=not self.args.curriculum_sampling, + ) + + def _sp_get_eval_sampler(self, eval_dataset) -> Sampler | None: + """ + Get an evaluation sampler configured for sequence parallelism. + + Args: + eval_dataset: The evaluation dataset. + + Returns: + Configured sequence parallel sampler. + """ + return self._create_sequence_parallel_sampler( + eval_dataset, shuffle=False, is_eval=True + ) + + def _update_ring_flash_attn_params(self, inputs: dict[str, torch.Tensor | Any]): + """ + Calculate the cu_seqlens for the current forward pass and pass the value to + the substituted ring_flash_attn. This is accomplished by using the passed + `input_ids`. + + Args: + inputs: Current batch of inputs. + """ + # At this point, inputs should already be partitioned by the sequence + # parallel data collator + batch_size = inputs["input_ids"].shape[0] + seq_len = inputs["input_ids"].shape[1] + packed_seq_lens = [seq_len] * batch_size + + # Calculate the full sequence length across all GPUs in this SP group + total_seq_len = seq_len * self.args.sequence_parallel_degree + + cu_seqlens = torch.cumsum( + torch.tensor( + packed_seq_lens, device=torch.cuda.current_device(), dtype=torch.int32 + ), + dim=-1, + dtype=torch.int32, + ) + cu_seqlens = F.pad( + F.pad(cu_seqlens, (1, 0), value=0), (0, 1), value=total_seq_len + ) + + update_ring_flash_attn_params(cu_seqlens, self.ring_attn_group) diff --git a/src/axolotl/core/trainers/relora.py b/src/axolotl/core/trainers/relora.py new file mode 100644 index 0000000000..3bcd4a9b8c --- /dev/null +++ b/src/axolotl/core/trainers/relora.py @@ -0,0 +1,43 @@ +"""Module for ReLoRA trainer""" + +import torch + +from axolotl.core.trainers.base import AxolotlTrainer +from axolotl.monkeypatch.relora import ReLoRAScheduler + + +class ReLoRATrainer(AxolotlTrainer): + """Trainer subclass that uses the `OneCycleLR` scheduler""" + + tag_names = ["axolotl", "relora"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.lr_scheduler = None + + def create_scheduler( + self, + num_training_steps: int, + optimizer: torch.optim.Optimizer | None = None, + ): + optimizer = self.optimizer if optimizer is None else optimizer + lr_scheduler = super().create_scheduler(num_training_steps, optimizer) + + if self.args.relora_steps: + warmup_steps = ( + self.args.relora_warmup_steps if self.args.relora_warmup_steps else 10 + ) + anneal_steps = ( + self.args.relora_anneal_steps if self.args.relora_anneal_steps else 1 + ) + self.lr_scheduler = ReLoRAScheduler( + optimizer, + lr_scheduler, + self.args.relora_steps, + anneal_steps, + warmup_steps, + ) + else: + self.lr_scheduler = lr_scheduler + + return self.lr_scheduler diff --git a/src/axolotl/core/trainers/trl.py b/src/axolotl/core/trainers/trl.py index 7237e792e3..1199313e8a 100644 --- a/src/axolotl/core/trainers/trl.py +++ b/src/axolotl/core/trainers/trl.py @@ -1,16 +1,23 @@ -""" -module for TRL PPO training -""" +"""Module for TRL PPO trainer""" import torch from tqdm import tqdm -from trl import PPOTrainer +from trl import ( + CPOTrainer, + KTOTrainer, + ORPOTrainer, + PPOTrainer, + PRMTrainer, + RewardTrainer, +) + +from axolotl.core.trainers.mixins.scheduler import SchedulerMixin class TRLPPOTrainer(PPOTrainer): - """ - wrapper for ppo trainer to handle customizations - """ + """Wrapper for TRL PPO trainer to handle customizations""" + + tag_names = ["axolotl", "ppo"] def train( self, @@ -31,9 +38,7 @@ def train( "batch_size": 16, } - for epoch, batch in tqdm( # pylint: disable=unused-variable - enumerate(self.dataloader) - ): + for _, batch in tqdm(enumerate(self.dataloader)): query_tensors = batch["input_ids"] # generate model response @@ -65,3 +70,43 @@ def train( rewards, columns_to_log=["query", "response", "ref_response", "ref_rewards"], ) + + +class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): + """ + Extend the base ORPOTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "orpo"] + + +class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): + """ + Extend the base KTOTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "kto"] + + +class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): + """ + Extend the base CPOTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "cpo"] + + +class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): + """ + Extend the base RewardTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "reward"] + + +class AxolotlPRMTrainer(SchedulerMixin, PRMTrainer): + """ + Extend the base trl.PRMTrainer for axolotl helpers + """ + + tag_names = ["axolotl", "prm"] diff --git a/src/axolotl/core/trainers/utils.py b/src/axolotl/core/trainers/utils.py new file mode 100644 index 0000000000..c6d40cb617 --- /dev/null +++ b/src/axolotl/core/trainers/utils.py @@ -0,0 +1,33 @@ +"""Utils for Axolotl trainers""" + + +def sanitize_kwargs_for_tagging(tag_names, kwargs=None): + if isinstance(tag_names, str): + tag_names = [tag_names] + + if kwargs is not None: + if "tags" not in kwargs: + kwargs["tags"] = tag_names + elif "tags" in kwargs and isinstance(kwargs["tags"], list): + kwargs["tags"].extend(tag_names) + elif "tags" in kwargs and isinstance(kwargs["tags"], str): + tag_names.append(kwargs["tags"]) + kwargs["tags"] = tag_names + + return kwargs + + +def sanitize_kwargs_for_ds_tagging(dataset_tags, kwargs=None): + if isinstance(dataset_tags, str): + dataset_tags = [dataset_tags] + + if (dataset_tags is not None) and (kwargs is not None): + if "dataset_tags" not in kwargs: + kwargs["dataset_tags"] = dataset_tags + elif "dataset_tags" in kwargs and isinstance(kwargs["dataset_tags"], list): + kwargs["dataset_tags"].extend(dataset_tags) + elif "dataset_tags" in kwargs and isinstance(kwargs["dataset_tags"], str): + dataset_tags.append(kwargs["dataset_tags"]) + kwargs["dataset_tags"] = dataset_tags + + return kwargs diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index 34a79e6462..82a62c0496 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -207,14 +207,19 @@ class AxolotlTrainingMixins: }, ) + sequence_parallel_degree: Optional[int] = field( + default=1, + metadata={"help": "The number of workers to use in sequence parallelism"}, + ) + @dataclass class AxolotlTrainingArguments(AxolotlTrainingMixins, TrainingArguments): """ Training arguments for Causal trainer - This code is duplicated due to HF TrainingArguments not setting output_dir with a defaujlt value - so it can't be used as a mixin. + This code is duplicated due to HF TrainingArguments not setting output_dir with a + default value so it can't be used as a mixin. """ diff --git a/src/axolotl/monkeypatch/attention/ring_attn.py b/src/axolotl/monkeypatch/attention/ring_attn.py new file mode 100644 index 0000000000..95c44a820b --- /dev/null +++ b/src/axolotl/monkeypatch/attention/ring_attn.py @@ -0,0 +1,89 @@ +""" +Ring attention group registration and flash attention patching. + +Make use of the `ring-flash-attn` (https://github.com/zhuzilin/ring-flash-attention) +package, specifically the `hf_adapter.substitute_hf_flash_attn` function to patch in +their sequence parallel version of Flash Attention 2. +""" + +import torch.distributed as dist +from accelerate.logging import get_logger + +from axolotl.logging_config import configure_logging + +configure_logging() +LOG = get_logger(__name__) + +RING_ATTN_GROUP = None + + +def get_ring_attn_group() -> dist.ProcessGroup: + """ + Getter for ring attention group on this rank. + + Returns: + The process group for ring attention for this rank. + """ + return RING_ATTN_GROUP + + +def set_ring_attn_group(ring_attn_group: dist.ProcessGroup): + """ + Setter for ring attention group on this rank. + + Args: + Process group for ring attention. + """ + global RING_ATTN_GROUP # pylint: disable=global-statement + RING_ATTN_GROUP = ring_attn_group + + +def register_ring_attn(sequence_parallel_degree: int): + """ + Create ring attention group and substitute flash attn with ring flash attn. + + Args: + sequence_parallel_degree: Sequence parallelism factor. + """ + LOG.info( + "Enabling ring attention sequence parallelism: " + f"each sequence will be processed across {sequence_parallel_degree} GPUs" + ) + + world_size = dist.get_world_size() + assert sequence_parallel_degree <= world_size, ( + f"sequence_parallel_degree ({sequence_parallel_degree}) " + f"must be less than or equal to world_size ({world_size})" + ) + assert world_size % sequence_parallel_degree == 0, ( + f"sequence_parallel_degree ({sequence_parallel_degree}) " + f"must evenly divide world_size ({world_size})" + ) + + # Detailed logging of group formation + rank = dist.get_rank() + group_assignments = {} + + for i in range(world_size // sequence_parallel_degree): + ring_attn_ranks = list( + range( + i * sequence_parallel_degree, + (i + 1) * sequence_parallel_degree, + ) + ) + group = dist.new_group(ranks=ring_attn_ranks, backend="nccl") + + # Track which GPUs are in which groups + for r in ring_attn_ranks: + group_assignments[r] = i + + if rank in ring_attn_ranks: + set_ring_attn_group(group) + + # Log the GPU group assignments + if rank == 0: + LOG.info(f"Sequence parallel group assignments: {group_assignments}") + + from ring_flash_attn import substitute_hf_flash_attn + + substitute_hf_flash_attn(get_ring_attn_group(), sequence_parallel_degree) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index ff486db291..9ccd2ca0c4 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -169,7 +169,7 @@ def execute_training( cfg: DictDefault, trainer: Any, resume_from_checkpoint: str | None ): """ - Execute the training process with appropriate backend configurations. + Execute the training process with appropriate SDP kernel configurations. Args: cfg: Dictionary mapping `axolotl` config keys to values. @@ -177,9 +177,6 @@ def execute_training( resume_from_checkpoint: Path to checkpoint to resume from, if applicable. """ LOG.info("Starting trainer...") - if cfg.group_by_length: - LOG.info("hang tight... sorting dataset for group_by_length") - if cfg.flash_optimum: with torch.backends.cuda.sdp_kernel( # TODO configure these from the YAML w/ sdp_kernel_kwargs: ... diff --git a/src/axolotl/utils/collators/batching.py b/src/axolotl/utils/collators/batching.py index 7cf771421c..12c8b31d59 100644 --- a/src/axolotl/utils/collators/batching.py +++ b/src/axolotl/utils/collators/batching.py @@ -1,14 +1,59 @@ """ -DataCollator for axolotl to pad labels and position_ids for packed sequences +Data collators for axolotl to pad labels and position_ids for packed sequences. Also +includes logic for handling sequence parallelism collation. """ +import logging from dataclasses import dataclass from typing import Any, Optional, Union import numpy as np +import torch +import torch.distributed as dist from transformers import PreTrainedTokenizerBase from transformers.utils import PaddingStrategy +logger = logging.getLogger(__name__) + + +def adjust_position_ids_for_slice( + position_ids: torch.Tensor, start_idx: int +) -> torch.Tensor: + """ + Adjust position IDs for a sliced sequence to maintain proper relative positions. + This handles the case where position IDs might not be contiguous due to sample + packing. + """ + # Convert to tensor if not already + # Find the boundaries between samples (where position_ids reset) + adjusted_pos_ids = position_ids.clone() + + # Process each sequence in the batch + for i in range(position_ids.shape[0]): + seq = position_ids[i] + + # Find sample boundaries + boundaries = [] + for j in range(1, len(seq)): + if seq[j] < seq[j - 1]: + boundaries.append(j) + + # No need to adjust if there are no boundaries or this is a single sample + if not boundaries: + adjusted_pos_ids[i] = seq - start_idx + continue + + # Adjust each segment separately + prev_boundary = 0 + for boundary in boundaries: + adjusted_pos_ids[i, prev_boundary:boundary] -= start_idx + prev_boundary = boundary + + # Last segment + adjusted_pos_ids[i, prev_boundary:] -= start_idx + + return adjusted_pos_ids + @dataclass class DataCollatorForSeq2Seq: @@ -43,6 +88,8 @@ class DataCollatorForSeq2Seq: The id to use when padding the labels (-100 will be automatically ignored by PyTorch loss functions). return_tensors (`str`): The type of Tensor to return. Allowable values are "np", "pt" and "tf". + sequence_parallel_degree (`int`): + The degree of sequence parallelism. Default to 1 for no sequence parallelism. """ tokenizer: PreTrainedTokenizerBase @@ -53,6 +100,16 @@ class DataCollatorForSeq2Seq: label_pad_token_id: int = -100 position_pad_token_id: int = 0 return_tensors: str = "pt" + sequence_parallel_degree: int = 1 + + def __post_init__(self): + if self.sequence_parallel_degree > 1: + from axolotl.monkeypatch.attention.ring_attn import get_ring_attn_group + + # Get information about our position in the SP group + sp_group = get_ring_attn_group() + self.local_rank = dist.get_rank(group=sp_group) + self.local_world_size = dist.get_world_size(group=sp_group) def __call__(self, features, return_tensors=None): labels = None @@ -119,8 +176,43 @@ def __call__(self, features, return_tensors=None): ) features["decoder_input_ids"] = decoder_input_ids + if self.sequence_parallel_degree > 1: + features = self.apply_sequence_parallelism(features) + return features + def apply_sequence_parallelism( + self, batch: dict[str, torch.Tensor] + ) -> torch.Tensor: + """ + Apply sequence parallelism slicing to a batch. + + Args: + batch: Batch dictionary from parent collator. + + Returns: + Sliced batch dictionary. + """ + keys_to_slice = ["input_ids", "attention_mask", "labels", "position_ids"] + + for key in keys_to_slice: + if key in batch: + seq_len = batch[key].shape[1] + slice_size = seq_len // self.local_world_size + start_idx = self.local_rank * slice_size + end_idx = ( + start_idx + slice_size + if self.local_rank < self.local_world_size - 1 + else seq_len + ) + batch[key] = batch[key][:, start_idx:end_idx] + + # Special handling for position_ids + if key == "position_ids" and self.local_rank > 0: + batch[key] = adjust_position_ids_for_slice(batch[key], start_idx) + + return batch + @dataclass class BatchSamplerDataCollatorForSeq2Seq(DataCollatorForSeq2Seq): @@ -148,6 +240,7 @@ def __call__(self, features, return_tensors=None): np.array(item[feature]) for item in features_ if feature in item ] out_features[i][feature] = np.concatenate(arrays) + return super().__call__(out_features, return_tensors=return_tensors) @@ -177,6 +270,7 @@ def __call__(self, features, return_tensors=None): np.array(item[feature]) for item in features_ if feature in item ] out_features[i][feature] = np.concatenate(arrays) + return super().__call__(out_features, return_tensors=return_tensors) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 136acc4a06..4e956140d1 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -125,6 +125,9 @@ def normalize_config(cfg): with open(ds_config_path, encoding="utf-8") as f: cfg.deepspeed = json.load(f) + if cfg.sequence_parallel_degree is None: + cfg.sequence_parallel_degree = 1 + if cfg.saves_per_epoch: save_steps = 1.0 / (cfg.saves_per_epoch * cfg.num_epochs) if save_steps < 1.0: # prevent saves on every step diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 44f570b883..83f70a022a 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -67,7 +67,12 @@ from axolotl.utils.lora_embeddings import get_linear_embedding_layers from axolotl.utils.model_shard_quant import load_sharded_model, load_sharded_model_quant -LOG = logging.getLogger("axolotl") +LOG = logging.getLogger(__name__) + +MULTIMODEL_AUTO_MODEL_MAPPING = { + "llava": LlavaForConditionalGeneration, + "mllama": MllamaForConditionalGeneration, +} # copied from accelerator.FullyShardedDataParallelPlugin @@ -476,7 +481,7 @@ def __init__( else: self.text_model_config = self.model_config - self.AutoModelLoader = AutoModelForCausalLM # pylint: disable=invalid-name + self.auto_model_loader = AutoModelForCausalLM # pylint: disable=invalid-name def apply_patches(self) -> None: # load any patches from plugins @@ -547,6 +552,14 @@ def apply_patches(self) -> None: patch_self_attn_lora(self.cfg) + if self.cfg.sequence_parallel_degree and self.cfg.sequence_parallel_degree > 1: + from axolotl.monkeypatch.attention.ring_attn import register_ring_attn + + # Initialize ring attn for sequence parallelism. This must be done after + # model init but before the first forward pass, since it modifies flash + # attn to use ring comm for SP training across multiple GPUs. + register_ring_attn(self.cfg.sequence_parallel_degree) + def patch_attention(self) -> None: if hasattr(self.model_config, "model_type"): if self.model_config.model_type == "mllama" and self.cfg.flash_attention: @@ -603,7 +616,7 @@ def patch_loss_llama(self) -> None: patch_self_attn_lora() - def patch_llama_derived_model(self) -> None: + def patch_llama_derived_model(self): """Modify all llama derived models in one block""" self.patch_loss_llama() @@ -653,25 +666,16 @@ def patch_llama_derived_model(self) -> None: "Shifted-sparse attention not currently implemented without flash attention." ) - def set_auto_model_loader(self) -> None: - """set self.AutoModelLoader - - default value: AutoModelForCausalLM (set at __init__) - - when using a multi modality model, self.AutoModelLoader should - be set according to model type of the model + def set_auto_model_loader(self): + """ + Set self.auto_model_loader. Defaults to `transformers.AutoModelForCausalLM` + (set at `__init__`). When using a multimodal model, `self.auto_model_loader` + should be set according to the type of the model. """ if self.cfg.is_multimodal: - if self.model_config.model_type == "llava": - self.AutoModelLoader = ( # pylint: disable=invalid-name - LlavaForConditionalGeneration - ) - elif self.model_config.model_type == "mllama": - self.AutoModelLoader = ( # pylint: disable=invalid-name - MllamaForConditionalGeneration - ) - else: - self.AutoModelLoader = ( - AutoModelForVision2Seq # pylint: disable=invalid-name - ) + self.auto_model_loader = MULTIMODEL_AUTO_MODEL_MAPPING.get( + self.model_config.model_type, AutoModelForVision2Seq + ) def set_device_map_config(self) -> None: device_map = self.cfg.device_map @@ -695,7 +699,7 @@ def set_device_map_config(self) -> None: from accelerate import infer_auto_device_map with init_empty_weights(): - model_canvas = self.AutoModelLoader.from_config( + model_canvas = self.auto_model_loader.from_config( self.model_config, trust_remote_code=self.cfg.trust_remote_code or False, ) @@ -916,11 +920,27 @@ def _configure_zero3_memory_efficient_loading(): if self.cfg.is_multimodal: self.model_config.text_config = self.text_model_config - self.model = self.AutoModelLoader.from_pretrained( - self.base_model, - config=self.model_config, - **self.model_kwargs, - ) + + # Load model with random initialization if specified + if self.cfg.random_init_weights: + # AutoModel classes support the from_config method + if self.auto_model_loader in [ + AutoModelForCausalLM, + AutoModelForVision2Seq, + ]: + self.model = self.auto_model_loader.from_config( + config=self.model_config, + ) + else: + self.model = self.auto_model_loader( + config=self.model_config, + ) + else: + self.model = self.auto_model_loader.from_pretrained( + self.base_model, + config=self.model_config, + **self.model_kwargs, + ) # TODO (MengqingCao) split these patches seperately if self.cfg.flash_attention and not self.inference: @@ -958,7 +978,7 @@ def _configure_zero3_memory_efficient_loading(): if self.cfg.is_multimodal: self.model_config.text_config = self.text_model_config if self.cfg.gptq: - self.model = self.AutoModelLoader.from_pretrained( + self.model = self.auto_model_loader.from_pretrained( self.base_model, config=self.model_config, trust_remote_code=self.cfg.trust_remote_code or False, @@ -991,7 +1011,7 @@ def _configure_zero3_memory_efficient_loading(): if self.cfg.gptq: if self.cfg.is_multimodal: self.model_config.text_config = self.text_model_config - self.model = self.AutoModelLoader.from_pretrained( + self.model = self.auto_model_loader.from_pretrained( self.base_model, config=self.model_config, trust_remote_code=self.cfg.trust_remote_code or False, @@ -1011,7 +1031,7 @@ def _configure_zero3_memory_efficient_loading(): if self.cfg.is_multimodal: self.model_config.text_config = self.text_model_config - self.model = self.AutoModelLoader.from_pretrained( + self.model = self.auto_model_loader.from_pretrained( self.base_model, config=self.model_config, trust_remote_code=self.cfg.trust_remote_code or False, @@ -1307,7 +1327,7 @@ def load_model( """ Load a model for a given configuration and tokenizer. """ - loader = ModelLoader( + model_loader = ModelLoader( cfg, tokenizer, processor=processor, @@ -1315,7 +1335,7 @@ def load_model( reference_model=reference_model, **kwargs, ) - return loader.load_model() + return model_loader.load_model() def load_adapter(model, cfg, adapter, inference=False): diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 6119dff30c..41095152e5 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -104,9 +104,7 @@ def allocate( class MultipackBatchSampler(BatchSampler): - """ - Batch Sampler class for multipack - """ + """Batch sampler class for multipack""" def __init__( self, diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 7676a50a83..7992e65594 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1,4 +1,4 @@ -"""Main Axolotl input configuration Pydantic models""" +"""Module with Pydantic models for configuration.""" # pylint: disable=too-many-lines @@ -245,6 +245,8 @@ class AxolotlInputConfig( val_set_size: float | None = Field(default=0.0) + sequence_parallel_degree: int | None = None + special_tokens: SpecialTokensConfig | None = None tokens: list[str] | None = None added_tokens_overrides: dict[int, str] | None = None @@ -1102,6 +1104,29 @@ def check_kto_config(cls, data): return data + @field_validator("sequence_parallel_degree", mode="before") + @classmethod + def check_sequence_parallel_config(cls, value, info): + if not value: + value = 1 + + if value > 1: + if not info.data.get("flash_attention"): + raise ValueError( + "flash_attention: true must be set with sequence_parallel_degree > 1" + ) + + try: + import ring_flash_attn # noqa: F401 # pylint:disable=unused-import + except ImportError as exception: + raise ImportError( + "sequence_parallel_degree > 1 but ring_flash_attn is not installed. " + "Please install it with `pip install axolotl[ring-flash-attn] " + "or `pip install ring-flash-attn>=0.1.4`." + ) from exception + + return value + class AxolotlConfigWCapabilities(AxolotlInputConfig): """wrapper to valdiate gpu capabilities with the configured options""" diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 090e677a67..d2b211bbc8 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -346,7 +346,7 @@ def drop_no_trainable_tokens(sample): load_from_cache_file=not cfg.is_preprocess, desc="Add position_id column (PoSE)", ) - elif cfg.sample_packing: + elif cfg.sample_packing or cfg.sequence_parallel_degree > 1: drop_long_kwargs = {} if filter_map_kwargs: drop_long_kwargs["desc"] = "Add position_id column (Sample Packing)" @@ -356,7 +356,7 @@ def drop_no_trainable_tokens(sample): **filter_map_kwargs, **drop_long_kwargs, ) - if cfg.eval_sample_packing is not False: + if cfg.eval_sample_packing or cfg.sequence_parallel_degree > 1: if eval_dataset: eval_dataset = eval_dataset.map( add_position_ids, @@ -443,6 +443,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): - 1 ) * cfg.num_epochs + * cfg.sequence_parallel_degree ) LOG.debug( f"total_num_tokens: {cfg.total_num_tokens:_}, total_num_steps: {total_num_steps:_}", @@ -473,7 +474,11 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): LOG.debug(f"data_loader_len: {data_loader_len}", main_process_only=True) # FIXME: is there a bug here somewhere? the total num steps depends # on the agreed on value for sample_packing_eff_est - total_num_steps = int(math.floor(data_loader_len * cfg.num_epochs)) + total_num_steps = int( + math.floor( + data_loader_len * cfg.num_epochs * cfg.sequence_parallel_degree + ) + ) def calc_sample_packing_eff_est(estimates: List[float]): LOG.info(f"sample_packing_eff_est across ranks: {repr(estimates)}") @@ -494,7 +499,12 @@ def calc_sample_packing_eff_est(estimates: List[float]): ) else: total_num_steps = int( - math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) + math.ceil( + len(train_dataset) + * cfg.num_epochs + * cfg.sequence_parallel_degree + / cfg.batch_size + ) ) LOG.debug(f"total_num_steps: {total_num_steps}", main_process_only=True) return total_num_steps diff --git a/tests/e2e/patched/test_sp.py b/tests/e2e/patched/test_sp.py new file mode 100644 index 0000000000..a20ad9ff20 --- /dev/null +++ b/tests/e2e/patched/test_sp.py @@ -0,0 +1,207 @@ +"""Tests for sequence parallelism functionality.""" + +# pylint: disable=redefined-outer-name,unused-argument + +from unittest.mock import MagicMock, patch + +import pytest +import torch +from accelerate.state import PartialState + +from axolotl.utils.dict import DictDefault + +# Use a single patch for ring_flash_attn if it's not available +ring_flash_attn_mock = MagicMock() +with patch.dict("sys.modules", {"ring_flash_attn": ring_flash_attn_mock}): + from axolotl.monkeypatch.attention.ring_attn import get_ring_attn_group + from axolotl.utils.collators.batching import adjust_position_ids_for_slice + + +@pytest.fixture +def partial_state(): + """Create a real PartialState instance for testing.""" + state = PartialState() + return state + + +@pytest.fixture(name="cfg") +def fixture_cfg(): + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 1e-3, + "output_dir": "./model-out", + "sequence_len": 512, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + } + ) + + return cfg + + +class TestSequenceParallelHelpers: + """Test helper functions used in sequence parallelism.""" + + def test_adjust_position_ids_for_slice(self, partial_state): + """Test position_ids adjustment for sequence slices.""" + # Create sample position_ids with multiple sequences + position_ids = torch.tensor( + [ + # First sequence with 2 samples + [0, 1, 2, 3, 4, 0, 1, 2, 3], + # Second sequence with 3 samples + [0, 1, 2, 0, 1, 2, 3, 0, 1], + ] + ) + + # Adjust as if this was the second slice (start_idx = 4) + adjusted = adjust_position_ids_for_slice(position_ids, start_idx=4) + + # For first sequence: [0,1,2,3,4,0,1,2,3] -> [-4,-3,-2,-1,0,-4,-3,-2,-1] + # For second sequence: [0,1,2,0,1,2,3,0,1] -> [-4,-3,-2,-4,-3,-2,-1,-4,-3] + expected_first_seq = torch.tensor([0, 1, 2, 3, 4, 0, 1, 2, 3]) - 4 + expected_second_seq = torch.tensor([0, 1, 2, 0, 1, 2, 3, 0, 1]) - 4 + + assert torch.all(adjusted[0] == expected_first_seq) + assert torch.all(adjusted[1] == expected_second_seq) + + +class TestRingAttention: + """Tests for the ring attention functionality.""" + + @patch("torch.distributed.new_group") + @patch("torch.distributed.get_rank") + @patch("torch.distributed.get_world_size") + def test_register_ring_attn( + self, mock_world_size, mock_rank, mock_new_group, partial_state + ): + """Test that ring attention groups are created correctly.""" + from axolotl.monkeypatch.attention.ring_attn import register_ring_attn + + # Setup mocks + mock_world_size.return_value = 8 # 8 GPUs total + mock_rank.return_value = 3 # GPU #3 + mock_group = MagicMock() + mock_new_group.return_value = mock_group + + # Call register_ring_attn with size 4 + register_ring_attn(sequence_parallel_degree=4) + + # Verify the number of calls without examining the arguments + assert mock_new_group.call_count == 2 + + # Just verify that new_group was called + mock_new_group.assert_called() + + @patch("torch.distributed.get_rank") + @patch("torch.distributed.get_world_size") + def test_get_ring_attn_group_no_registration( + self, mock_world_size, mock_rank, partial_state + ): + """Test that get_ring_attn_group returns None when no group has been registered.""" + # Setup mocks + mock_world_size.return_value = 4 + mock_rank.return_value = 0 + + # Get the group without registration + group = get_ring_attn_group() + + # Verify that None was returned + assert group is None + + +# Mock a simplified DataCollator test +@patch("axolotl.monkeypatch.attention.ring_attn.get_ring_attn_group") +@patch("torch.distributed.get_rank") +@patch("torch.distributed.get_world_size") +def test_sequence_parallel_slicing( + mock_world_size, mock_rank, mock_get_group, partial_state +): + """Test the basic sequence slicing logic without full collator instantiation.""" + # Setup mocks + mock_get_group.return_value = MagicMock() + mock_rank.return_value = 1 # Second GPU + mock_world_size.return_value = 4 # 4 GPUs total + + # Create a sample batch + batch = { + "input_ids": torch.tensor( + [ + [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112], + [201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212], + ] + ), + "attention_mask": torch.ones(2, 12), + } + + # Simplified slicing logic from SequenceParallelDataCollator + def slice_batch(batch, rank, world_size): + result = {} + for key in batch: + seq_len = batch[key].shape[1] + slice_size = seq_len // world_size + start_idx = rank * slice_size + end_idx = start_idx + slice_size if rank < world_size - 1 else seq_len + result[key] = batch[key][:, start_idx:end_idx] + return result + + # Slice the batch + result = slice_batch( + batch, rank=mock_rank.return_value, world_size=mock_world_size.return_value + ) + + # Check slicing + assert result["input_ids"].shape == (2, 3) # 12 tokens / 4 GPUs = 3 tokens per GPU + expected_input_ids = torch.tensor( + [ + [104, 105, 106], # Second slice of first sequence + [204, 205, 206], # Second slice of second sequence + ] + ) + assert torch.all(result["input_ids"] == expected_input_ids) + + +@patch.dict("sys.modules", {"ring_flash_attn": MagicMock()}) +def test_config_validation_with_valid_inputs(cfg): + """Test that valid sequence parallelism configurations pass validation.""" + # Import the actual model class with appropriate mocks + from axolotl.utils.schemas.config import AxolotlInputConfig + + # Valid configuration: sequence_parallel_degree > 1 and flash_attention is True + cfg = cfg | { + "sequence_parallel_degree": 2, + "flash_attention": True, + } + + # Should validate without errors + config = AxolotlInputConfig(**cfg) + assert config.sequence_parallel_degree == 2 + assert config.flash_attention is True + + +def test_config_validation_with_invalid_inputs(cfg): + """Test that invalid sequence parallelism configurations fail validation.""" + from axolotl.utils.schemas.config import AxolotlInputConfig + + # Invalid configuration: sequence_parallel_degree > 1 but flash_attention is False + cfg = cfg | { + "sequence_parallel_degree": 2, + "flash_attention": False, + } + + # Should raise ValidationError + with pytest.raises(ValueError) as excinfo: + AxolotlInputConfig(**cfg) + + # Verify error message + assert "flash_attention: true must be set" in str(excinfo.value) diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index 3fc315b2ea..d32eb3953a 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -12,6 +12,7 @@ from datasets import Dataset from transformers import AutoTokenizer +from axolotl.utils.config import normalize_config from axolotl.utils.data import prepare_dataset from axolotl.utils.data.rl import load_prepare_preference_datasets from axolotl.utils.data.utils import deduplicate_and_log_datasets @@ -262,6 +263,7 @@ def setUp(self) -> None: self.tokenizer.add_special_tokens(SPECIAL_TOKENS) self.cfg_1 = DictDefault( { + "base_model": "huggyllama/llama-7b", "tokenizer_config": "huggyllama/llama-7b", "sequence_len": 1024, "dataset_exact_deduplication": True, @@ -282,6 +284,7 @@ def setUp(self) -> None: "num_epochs": 1, } ) + normalize_config(self.cfg_1) def test_prepare_dataset_with_deduplication_train(self): """Verify that prepare_dataset function processes the dataset correctly with deduplication.""" From e44953d50cdea5cc6a7625065f6313bf3f53d6c5 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 21 Mar 2025 13:28:13 -0400 Subject: [PATCH 0483/1405] installing axolotl prior to quartodoc build (#2434) * installing axolotl prior to quartodoc build * simplify by installing no deps --------- Co-authored-by: Dan Saunders --- .github/workflows/docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7ddeccff56..2d3c209ccc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,6 +23,7 @@ jobs: - name: Install dependencies run: | python3 -m pip install jupyter quartodoc + python3 -m pip install -e . --no-deps - name: Build autodoc run: quartodoc build - name: Publish to GitHub Pages (and render) From 86bac48d149ffd92ec854ddeb0e6547676cd2da1 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Sat, 22 Mar 2025 17:53:29 -0400 Subject: [PATCH 0484/1405] cleanup for failing test (#2436) --- .../monkeypatch/attention/ring_attn.py | 2 +- tests/e2e/patched/test_sp.py | 46 ++++++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/axolotl/monkeypatch/attention/ring_attn.py b/src/axolotl/monkeypatch/attention/ring_attn.py index 95c44a820b..a81b879094 100644 --- a/src/axolotl/monkeypatch/attention/ring_attn.py +++ b/src/axolotl/monkeypatch/attention/ring_attn.py @@ -27,7 +27,7 @@ def get_ring_attn_group() -> dist.ProcessGroup: return RING_ATTN_GROUP -def set_ring_attn_group(ring_attn_group: dist.ProcessGroup): +def set_ring_attn_group(ring_attn_group: dist.ProcessGroup | None): """ Setter for ring attention group on this rank. diff --git a/tests/e2e/patched/test_sp.py b/tests/e2e/patched/test_sp.py index a20ad9ff20..903cb14508 100644 --- a/tests/e2e/patched/test_sp.py +++ b/tests/e2e/patched/test_sp.py @@ -8,14 +8,13 @@ import torch from accelerate.state import PartialState +from axolotl.monkeypatch.attention.ring_attn import ( + get_ring_attn_group, + set_ring_attn_group, +) +from axolotl.utils.collators.batching import adjust_position_ids_for_slice from axolotl.utils.dict import DictDefault -# Use a single patch for ring_flash_attn if it's not available -ring_flash_attn_mock = MagicMock() -with patch.dict("sys.modules", {"ring_flash_attn": ring_flash_attn_mock}): - from axolotl.monkeypatch.attention.ring_attn import get_ring_attn_group - from axolotl.utils.collators.batching import adjust_position_ids_for_slice - @pytest.fixture def partial_state(): @@ -79,6 +78,22 @@ def test_adjust_position_ids_for_slice(self, partial_state): class TestRingAttention: """Tests for the ring attention functionality.""" + @patch("torch.distributed.get_rank") + @patch("torch.distributed.get_world_size") + def test_get_ring_attn_group_no_registration( + self, mock_world_size, mock_rank, partial_state + ): + """Test that get_ring_attn_group returns None when no group has been registered.""" + # Setup mocks + mock_world_size.return_value = 4 + mock_rank.return_value = 0 + + # Get the group without registration + group = get_ring_attn_group() + + # Verify that None was returned + assert group is None + @patch("torch.distributed.new_group") @patch("torch.distributed.get_rank") @patch("torch.distributed.get_world_size") @@ -100,24 +115,11 @@ def test_register_ring_attn( # Verify the number of calls without examining the arguments assert mock_new_group.call_count == 2 - # Just verify that new_group was called + # Verify that new_group was called mock_new_group.assert_called() - @patch("torch.distributed.get_rank") - @patch("torch.distributed.get_world_size") - def test_get_ring_attn_group_no_registration( - self, mock_world_size, mock_rank, partial_state - ): - """Test that get_ring_attn_group returns None when no group has been registered.""" - # Setup mocks - mock_world_size.return_value = 4 - mock_rank.return_value = 0 - - # Get the group without registration - group = get_ring_attn_group() - - # Verify that None was returned - assert group is None + # Clean up + set_ring_attn_group(None) # Mock a simplified DataCollator test From 9f00465a5cb2349c23853c4a020a928709e506f2 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sun, 23 Mar 2025 07:33:21 +0700 Subject: [PATCH 0485/1405] Feat: Add support for gemma3_text and add e2e for gemma2 (#2406) --- docs/config.qmd | 1 - examples/gemma3/qlora.yml | 74 ++++++++++ requirements.txt | 2 +- src/axolotl/integrations/liger/__init__.py | 2 + src/axolotl/monkeypatch/multipack.py | 1 + src/axolotl/utils/chat_templates.py | 1 + src/axolotl/utils/schemas/enums.py | 1 + .../lora_kernels/test_lora_kernel_patching.py | 4 +- tests/e2e/test_deepseekv3.py | 2 +- tests/e2e/test_gemma2.py | 133 ++++++++++++++++++ tests/e2e/test_gemma3_text.py | 131 +++++++++++++++++ tests/e2e/test_schedulers.py | 2 +- 12 files changed, 348 insertions(+), 6 deletions(-) create mode 100644 examples/gemma3/qlora.yml create mode 100644 tests/e2e/test_gemma2.py create mode 100644 tests/e2e/test_gemma3_text.py diff --git a/docs/config.qmd b/docs/config.qmd index 9946b5865a..f166f80501 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -513,7 +513,6 @@ lr_div_factor: # Learning rate div factor # in the examples/ for your model and fine-tuning use case. # # Valid values for 'optimizer' include: -# - adamw_hf # - adamw_torch # - adamw_torch_fused # - adamw_torch_xla diff --git a/examples/gemma3/qlora.yml b/examples/gemma3/qlora.yml new file mode 100644 index 0000000000..50045cc8a3 --- /dev/null +++ b/examples/gemma3/qlora.yml @@ -0,0 +1,74 @@ +base_model: google/gemma-3-1b-it +# optionally might have model_type or tokenizer_type +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: true +strict: false + +# huggingface repo +chat_template: gemma3_text +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: true + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: diff --git a/requirements.txt b/requirements.txt index c8465d23f8..93618ba007 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ liger-kernel==0.5.3 packaging==23.2 peft==0.15.0 -transformers==4.49.0 +transformers==4.50.0 tokenizers>=0.21.1 accelerate==1.5.2 datasets==3.4.1 diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index b8e1fac529..327a051388 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -114,3 +114,5 @@ def pre_model_load(self, cfg): modeling_mod.CrossEntropyLoss = LigerCrossEntropyLoss if cfg.liger_fused_linear_cross_entropy: modeling_mod.DeepseekV2ForCausalLM.forward = deepseekv2_lce_forward + elif cfg.model_config_type in ["gemma3_text", "deepseek_v3"]: + raise ValueError(f"Unsupported model config type: {cfg.model_config_type}") diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index f2a87192a4..d6d209db51 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -22,6 +22,7 @@ "phi3", "gemma", "gemma2", + "gemma3_text", "gemmoe", "starcoder2", "deepseek_v2", diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index d3c88334b4..7dbeda4626 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -22,6 +22,7 @@ "mistral_v3_tekken": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST]' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # V3-Tekken: Nemo, Pixtral... "chatml": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}", "gemma": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\n' + message['content'] | trim + '\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\n'}}{% endif %}", + "gemma3_text": "{{ bos_token }}\n{%- if messages[0]['role'] == 'system' -%}\n {%- if messages[0]['content'] is string -%}\n {%- set first_user_prefix = messages[0]['content'] + '\n\n' -%}\n {%- else -%}\n {%- set first_user_prefix = messages[0]['content'][0]['text'] + '\n\n' -%}\n {%- endif -%}\n {%- set loop_messages = messages[1:] -%}\n{%- else -%}\n {%- set first_user_prefix = \"\" -%}\n {%- set loop_messages = messages -%}\n{%- endif -%}\n{%- for message in loop_messages -%}\n {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) -%}\n {{ raise_exception(\"Conversation roles must alternate user/assistant/user/assistant/...\") }}\n {%- endif -%}\n {%- if (message['role'] == 'assistant') -%}\n {%- set role = \"model\" -%}\n {%- else -%}\n {%- set role = message['role'] -%}\n {%- endif -%}\n {{ '' + role + '\n' + (first_user_prefix if loop.first else \"\") }}\n {%- if message['content'] is string -%}\n {{ message['content'] | trim }}\n {%- elif message['content'] is iterable -%}\n {%- for item in message['content'] -%}\n {%- if item['type'] == 'image' -%}\n {{ '' }}\n {%- elif item['type'] == 'text' -%}\n {{ item['text'] | trim }}\n {%- endif -%}\n {%- endfor -%}\n {%- else -%}\n {{ raise_exception(\"Invalid content type\") }}\n {%- endif -%}\n {{ '\n' }}\n{%- endfor -%}\n{%- if add_generation_prompt -%}\n {{'model\n'}}\n{%- endif -%}\n", "cohere": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", "llama3_2_vision": '{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now("%d %b %Y") %}\n {%- else %}\n {%- set date_string = "26 Jul 2024" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0][\'role\'] == \'system\' %}\n {%- set system_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n{%- else %}\n {%- set system_message = "" %}\n{%- endif %}\n\n{#- Find out if there are any images #}\n{% set image_ns = namespace(has_images=false) %} \n{%- for message in messages %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {%- set image_ns.has_images = true %}\n {%- endif %}\n {%- endfor %}\n{%- endfor %}\n\n{#- Error out if there are images and system message #}\n{%- if image_ns.has_images and not system_message == "" %}\n {{- raise_exception("Prompting with images is incompatible with system messages.") }}\n{%- endif %}\n\n{#- System message if there are no images #}\n{%- if not image_ns.has_images %}\n {{- "<|start_header_id|>system<|end_header_id|>\\n\\n" }}\n {%- if tools is not none %}\n {{- "Environment: ipython\\n" }}\n {%- endif %}\n {{- "Cutting Knowledge Date: December 2023\\n" }}\n {{- "Today Date: " + date_string + "\\n\\n" }}\n {%- if tools is not none and not tools_in_user_message %}\n {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- "<|eot_id|>" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception("Cannot put tools in the first user message when there\'s no first user message!") }}\n{%- endif %}\n {{- \'<|start_header_id|>user<|end_header_id|>\\n\\n\' -}}\n {{- "Given the following functions, please respond with a JSON for a function call " }}\n {{- "with its proper arguments that best answers the given prompt.\\n\\n" }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {{- first_user_message + "<|eot_id|>"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == \'ipython\' or message.role == \'tool\' or \'tool_calls\' in message) %}\n {{- \'<|start_header_id|>\' + message[\'role\'] + \'<|end_header_id|>\\n\\n\' }}\n {%- if message[\'content\'] is string %}\n {{- message[\'content\'] }}\n {%- else %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {{- \'<|image|>\' }}\n {%- elif content[\'type\'] == \'text\' %}\n {{- content[\'text\'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \'<|eot_id|>\' }}\n {%- elif \'tool_calls\' in message %}\n {%- if not message.tool_calls|length == 1 %}\n {{- raise_exception("This model only supports single tool-calls at once!") }}\n {%- endif %}\n {%- set tool_call = message.tool_calls[0].function %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' -}}\n {{- \'{"name": "\' + tool_call.name + \'", \' }}\n {{- \'"parameters": \' }}\n {{- tool_call.arguments | tojson }}\n {{- "}" }}\n {{- "<|eot_id|>" }}\n {%- elif message.role == "tool" or message.role == "ipython" %}\n {{- "<|start_header_id|>ipython<|end_header_id|>\\n\\n" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- "<|eot_id|>" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' }}\n{%- endif %}\n', diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index f376aca5f7..a0c6df7103 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -23,6 +23,7 @@ class ChatTemplate(str, Enum): mistral_v2v3 = "mistral_v2v3" # pylint: disable=invalid-name mistral_v3_tekken = "mistral_v3_tekken" # pylint: disable=invalid-name gemma = "gemma" # pylint: disable=invalid-name + gemma3_text = "gemma3_text" # pylint: disable=invalid-name cohere = "cohere" # pylint: disable=invalid-name llama3 = "llama3" # pylint: disable=invalid-name llama3_2_vision = "llama3_2_vision" # pylint: disable=invalid-name diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index 89f80951b4..ce7a2bf0f7 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -144,7 +144,7 @@ def test_swiglu_mlp_integration(small_llama_model): def test_geglu_model_integration(): """Test GeGLU activation with Gemma model.""" model = AutoModelForCausalLM.from_pretrained( - "mhenrichsen/gemma-2b", torch_dtype=torch.float16, device_map="cuda" + "mhenrichsen/gemma-2b", torch_dtype=torch.float16, device_map="auto" ) peft_config = get_peft_config( { @@ -347,7 +347,7 @@ def test_model_architecture(model_config): """Test LoRA kernel patches across different model architectures.""" # Load model with appropriate dtype model = AutoModelForCausalLM.from_pretrained( - model_config["name"], torch_dtype=model_config["dtype"], device_map="cuda" + model_config["name"], torch_dtype=model_config["dtype"], device_map="auto" ) # Apply LoRA configuration diff --git a/tests/e2e/test_deepseekv3.py b/tests/e2e/test_deepseekv3.py index de8513078e..f8c3d429aa 100644 --- a/tests/e2e/test_deepseekv3.py +++ b/tests/e2e/test_deepseekv3.py @@ -1,5 +1,5 @@ """ -E2E tests for lora llama +E2E tests for deepseekv3 """ import logging diff --git a/tests/e2e/test_gemma2.py b/tests/e2e/test_gemma2.py new file mode 100644 index 0000000000..df777b7099 --- /dev/null +++ b/tests/e2e/test_gemma2.py @@ -0,0 +1,133 @@ +""" +E2E tests for gemma2 +""" + +import logging +import os +from pathlib import Path + +import pytest + +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestGemma2: + """ + Test case for Gemma2 models + """ + + @pytest.mark.parametrize( + "sample_packing", + [True, False], + ) + def test_lora_gemma2(self, temp_dir, sample_packing): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "axolotl-ai-co/gemma-2-33M", + "trust_remote_code": True, + "sample_packing": sample_packing, + "flash_attention": True, + "sequence_len": 2048, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0, + "datasets": [ + { + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "field_messages": "conversations", + "message_property_mappings": { + "role": "from", + "content": "value", + }, + "drop_system_message": True, + "split": "train[:1%]", + }, + ], + "special_tokens": { + "bos_token": "", + "eos_token": "", + }, + "chat_template": "gemma", # gemma2's template is same as gemma + "num_epochs": 1, + "micro_batch_size": 1, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "max_steps": 5, + "save_safetensors": True, + "bf16": True, + } + ) + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.safetensors").exists() + + @pytest.mark.parametrize( + "sample_packing", + [True, False], + ) + def test_fft_gemma2(self, temp_dir, sample_packing): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "axolotl-ai-co/gemma-2-33M", + "trust_remote_code": True, + "sample_packing": sample_packing, + "flash_attention": True, + "sequence_len": 2048, + "val_set_size": 0, + "datasets": [ + { + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "field_messages": "conversations", + "message_property_mappings": { + "role": "from", + "content": "value", + }, + "split": "train[:1%]", + "drop_system_message": True, + }, + ], + "chat_template": "gemma", # gemma2's template is same as gemma + "special_tokens": { + "bos_token": "", + "eos_token": "", + }, + "num_epochs": 1, + "micro_batch_size": 1, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "max_steps": 5, + "save_safetensors": True, + "bf16": True, + } + ) + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() diff --git a/tests/e2e/test_gemma3_text.py b/tests/e2e/test_gemma3_text.py new file mode 100644 index 0000000000..6ce360f688 --- /dev/null +++ b/tests/e2e/test_gemma3_text.py @@ -0,0 +1,131 @@ +""" +E2E tests for gemma3_text +""" + +import logging +import os +from pathlib import Path + +import pytest + +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestGemma3Text: + """ + Test case for Gemma3Text models + """ + + @pytest.mark.parametrize( + "sample_packing", + [True, False], + ) + def test_lora_gemma3_text(self, temp_dir, sample_packing): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "axolotl-ai-co/gemma-3-34M", + "trust_remote_code": True, + "sample_packing": sample_packing, + "flash_attention": True, + "sequence_len": 2048, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0, + "datasets": [ + { + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "field_messages": "conversations", + "message_property_mappings": { + "role": "from", + "content": "value", + }, + "split": "train[:1%]", + }, + ], + "special_tokens": { + "bos_token": "", + "eos_token": "", + }, + "chat_template": "gemma3_text", + "num_epochs": 1, + "micro_batch_size": 1, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "max_steps": 5, + "save_safetensors": True, + "bf16": True, + } + ) + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "adapter_model.safetensors").exists() + + @pytest.mark.parametrize( + "sample_packing", + [True, False], + ) + def test_fft_gemma3_text(self, temp_dir, sample_packing): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "axolotl-ai-co/gemma-3-34M", + "trust_remote_code": True, + "sample_packing": sample_packing, + "flash_attention": True, + "sequence_len": 2048, + "val_set_size": 0, + "datasets": [ + { + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "field_messages": "conversations", + "message_property_mappings": { + "role": "from", + "content": "value", + }, + "split": "train[:1%]", + }, + ], + "chat_template": "gemma3_text", + "special_tokens": { + "bos_token": "", + "eos_token": "", + }, + "num_epochs": 1, + "micro_batch_size": 1, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "max_steps": 5, + "save_safetensors": True, + "bf16": True, + } + ) + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "model.safetensors").exists() diff --git a/tests/e2e/test_schedulers.py b/tests/e2e/test_schedulers.py index c492fdccfa..2d5040ae35 100644 --- a/tests/e2e/test_schedulers.py +++ b/tests/e2e/test_schedulers.py @@ -54,7 +54,7 @@ def test_rex_scheduler(self, temp_dir): "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, - "optimizer": "adamw_hf", + "optimizer": "adamw_torch_fused", "max_steps": 20, "lr_scheduler": "rex", "warmup_steps": 5, From a9b0733f2c6be3a41ce4b8af8481599f0cf78064 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sun, 23 Mar 2025 22:08:51 +0700 Subject: [PATCH 0486/1405] Feat: Rework multimodal support (mllama, llava, pixtral, qwen2, qwen25, gemma3, mistral3) (#2435) --- _quarto.yml | 1 + docs/config.qmd | 8 + docs/multimodal.qmd | 165 ++++++++++- examples/gemma3/gemma-3-4b-lora.yml | 63 ++++ examples/llava/lora-7b.yaml | 63 ++++ .../mistral/mistral-small-3.1-24B-lora.yml | 66 +++++ examples/pixtral/lora-12b.yml | 65 ++++ examples/qwen2-vl/lora-7b.yaml | 63 ++++ src/axolotl/core/trainer_builder.py | 16 +- src/axolotl/core/training_args.py | 15 + src/axolotl/processing_strategies.py | 278 ++++++++++++++++++ src/axolotl/utils/chat_templates.py | 6 +- src/axolotl/utils/collators/mm_chat.py | 216 ++++---------- src/axolotl/utils/config/__init__.py | 5 +- src/axolotl/utils/models.py | 65 +++- src/axolotl/utils/schemas/config.py | 2 + src/axolotl/utils/schemas/enums.py | 6 +- src/axolotl/utils/schemas/multimodal.py | 48 +++ tests/e2e/test_gemma3_text.py | 4 +- 19 files changed, 971 insertions(+), 184 deletions(-) create mode 100644 examples/gemma3/gemma-3-4b-lora.yml create mode 100644 examples/llava/lora-7b.yaml create mode 100644 examples/mistral/mistral-small-3.1-24B-lora.yml create mode 100644 examples/pixtral/lora-12b.yml create mode 100644 examples/qwen2-vl/lora-7b.yaml create mode 100644 src/axolotl/processing_strategies.py create mode 100644 src/axolotl/utils/schemas/multimodal.py diff --git a/_quarto.yml b/_quarto.yml index c564fb0dd1..0a8e023cf6 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -133,6 +133,7 @@ quartodoc: - utils.schemas.datasets - utils.schemas.peft - utils.schemas.trl + - utils.schemas.multimodal - utils.schemas.integrations - utils.schemas.enums - utils.schemas.utils diff --git a/docs/config.qmd b/docs/config.qmd index f166f80501..2a79a0126e 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -586,6 +586,14 @@ resume_from_checkpoint: # Be careful with this being turned on between different models. auto_resume_from_checkpoints: false +## Multimodal section +# int | tuple[int, int] | None . Size to resize images to, width x height. +# Will read from model/processor config if not set. +image_size: +# str. Algorithm to use for image resizing. "bilinear", "bicubic", "lanczos". Default is "bilinear". +image_resize_algorithm: 'bilinear' +## End of multimodal section + # Don't mess with this, it's here for accelerate and torchrun local_rank: diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index 2381566adb..e8e7934826 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -1,28 +1,171 @@ -# MultiModal / Vision Language Models (BETA) +--- +title: MultiModal / Vision Language Models (BETA) +format: + html: + toc: true + toc-depth: 3 +--- -### Supported Models +## Supported Models -- Mllama, i.e. llama with vision models +- [Mllama](#sec-mllama) +- [Pixtral](#sec-pixtral) +- [Llava-1.5](#sec-llava-15) +- [Mistral-Small-3.1](#sec-mistral-small-31) +- [Gemma-3](#sec-gemma-3) +- [Qwen2-VL](#sec-qwen2-vl) +- [Qwen2.5-VL](#sec-qwen25-vl) -### Usage +## Usage -Currently multimodal support is limited and doesn't have full feature parity. To finetune a multimodal Llama w/ LoRA, -you'll need to use the following in YAML in combination with the rest of the required hyperparams. +Multimodal support is limited and doesn't have full feature parity. + +Here are the hyperparams you'll need to use to finetune a multimodal model. ```yaml -base_model: alpindale/Llama-3.2-11B-Vision-Instruct processor_type: AutoProcessor + skip_prepare_dataset: true +remove_unused_columns: false # leave columns in place as they are needed to handle image embeddings during training +sample_packing: false # not yet supported with multimodal -chat_template: llama3_2_vision +chat_template: # see in next section + +# example dataset datasets: - path: HuggingFaceH4/llava-instruct-mix-vsft type: chat_template split: train[:1%] field_messages: messages -remove_unused_columns: false -sample_packing: false -# only finetune the Language model, leave the vision model and vision tower frozen +# (optional) if doing lora, only finetune the Language model, +# leave the vision model and vision tower frozen +# load_in_8bit: true +adapter: lora lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +# (optional) if you want to resize images to a set size +image_size: 512 +image_resize_algorithm: bilinear +``` + +Please see [examples](https://github.com/axolotl-ai/axolotl/tree/main/examples) folder for full configs. + +::: {.callout-warning} +Some of our chat_templates have been extended to support broader dataset types. This should not break any existing configs. +::: + +### Mllama {#sec-mllama} + +```yaml +base_model: meta-llama/Llama-3.2-11B-Vision-Instruct + +chat_template: llama3_2_vision +``` + +### Pixtral {#sec-pixtral} + +```yaml +base_model: mistralai/Pixtral-12B-2409 + +chat_template: pixtral +``` + +### Llava-1.5 {#sec-llava-15} + +```yaml +base_model: llava-hf/llava-1.5-7b-hf + +chat_template: llava +``` + +### Mistral-Small-3.1 {#sec-mistral-small-31} + +```yaml +base_model: mistralai/Mistral-Small-3.1-24B-Instruct-2503 + +chat_template: mistral_v7_tekken +``` + +### Gemma-3 {#sec-gemma-3} + +::: {.callout-tip} +The Gemma3-1B model is a text-only model, so please train as regular text model. +::: + +For multi-modal 4B/12B/27B models, use the following config: + +```yaml +base_model: google/gemma-3-4b-it + +chat_template: gemma3 +``` + +### Qwen2-VL {#sec-qwen2-vl} + +```yaml +base_model: Qwen/Qwen2-VL-7B-Instruct + +chat_template: qwen2_vl +``` + +### Qwen2.5-VL {#sec-qwen25-vl} + +```yaml +base_model: Qwen/Qwen2.5-VL-7B-Instruct + +chat_template: qwen2_vl # same as qwen2-vl +``` + +## Dataset Format + +For multi-modal datasets, we adopt an extended `chat_template` format similar to OpenAI's Message format. + +- A message is a list of `role` and `content`. +- `role` can be `system`, `user`, `assistant`, etc. +- `content` is a list of `type` and (`text` or `image` or `path` or `url` or `base64`). + +::: {.callout-note} +For backwards compatibility: + +- If the dataset has a `images` or `image` column of `list[Image]`, it will be appended to the first `content` list as `{"type": "image", "image": ...}`. However, if the content already has a `{"type": "image"}` but no `image` key, it will be set the `image` key. +- If `content` is a string, it will be converted to a list with `type` as `text`. +::: + +::: {.callout-tip} +For image loading, you can use the following keys within `content` alongside `"type": "image"`: + +- `"path": "/path/to/image.jpg"` +- `"url": "https://example.com/image.jpg"` +- `"base64": "..."` +- `"image": PIL.Image` +::: + +Here is an example of a multi-modal dataset: +```json +[ + { + "messages": [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."} + ] + }, + { + "role": "user", + "content": [ + {"type": "image", "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/bee.jpg"}, + {"type": "text", "text": "Describe this image in detail."} + ] + }, + { + "role": "assistant", + "content": [ + {"type": "text", "text": "The image is a bee."} + ] + } + ] + } +] ``` diff --git a/examples/gemma3/gemma-3-4b-lora.yml b/examples/gemma3/gemma-3-4b-lora.yml new file mode 100644 index 0000000000..b85392982b --- /dev/null +++ b/examples/gemma3/gemma-3-4b-lora.yml @@ -0,0 +1,63 @@ +base_model: google/gemma-3-4b-it +processor_type: AutoProcessor +strict: false + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +chat_template: gemma3 +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + field_messages: messages +dataset_prepared_path: last_run_prepared +val_set_size: 0.01 +output_dir: ./outputs/out + +adapter: lora +lora_model_dir: + +sequence_len: 2048 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +local_rank: +logging_steps: 1 +flash_attention: true +eager_attention: + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: diff --git a/examples/llava/lora-7b.yaml b/examples/llava/lora-7b.yaml new file mode 100644 index 0000000000..9129d01221 --- /dev/null +++ b/examples/llava/lora-7b.yaml @@ -0,0 +1,63 @@ +base_model: llava-hf/llava-1.5-7b-hf +processor_type: AutoProcessor +strict: false + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +chat_template: llava +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + field_messages: messages +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: lora +lora_model_dir: + +sequence_len: 8192 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +local_rank: +logging_steps: 1 +flash_attention: true +eager_attention: + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: diff --git a/examples/mistral/mistral-small-3.1-24B-lora.yml b/examples/mistral/mistral-small-3.1-24B-lora.yml new file mode 100644 index 0000000000..1774847993 --- /dev/null +++ b/examples/mistral/mistral-small-3.1-24B-lora.yml @@ -0,0 +1,66 @@ +base_model: mistralai/Mistral-Small-3.1-24B-Instruct-2503 +processor_type: AutoProcessor +strict: false + +load_in_8bit: true + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +chat_template: mistral_v7_tekken +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + field_messages: messages +dataset_prepared_path: last_run_prepared +val_set_size: 0.01 +output_dir: ./outputs/out + +adapter: lora +lora_model_dir: + +sequence_len: 2048 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +local_rank: +logging_steps: 1 +flash_attention: false # PixtralVisionModel does not support Flash Attention 2.0 yet. +eager_attention: + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: diff --git a/examples/pixtral/lora-12b.yml b/examples/pixtral/lora-12b.yml new file mode 100644 index 0000000000..7336a7ad05 --- /dev/null +++ b/examples/pixtral/lora-12b.yml @@ -0,0 +1,65 @@ +base_model: mistral-community/pixtral-12b +processor_type: AutoProcessor +strict: false + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +chat_template: pixtral +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + field_messages: messages +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: lora +lora_model_dir: + +sequence_len: 8192 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +local_rank: +logging_steps: 1 +flash_attention: false # PixtralVisionModel does not support Flash Attention 2.0 yet +eager_attention: + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: + pad_token: diff --git a/examples/qwen2-vl/lora-7b.yaml b/examples/qwen2-vl/lora-7b.yaml new file mode 100644 index 0000000000..e7ab13ddb3 --- /dev/null +++ b/examples/qwen2-vl/lora-7b.yaml @@ -0,0 +1,63 @@ +base_model: Qwen/Qwen2-VL-7B-Instruct +processor_type: AutoProcessor +strict: false + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +chat_template: qwen2_vl +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + field_messages: messages +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: lora +lora_model_dir: + +sequence_len: 8192 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +local_rank: +logging_steps: 1 +flash_attention: true +eager_attention: + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index b151be8fac..b237b1ef3b 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -60,6 +60,7 @@ from axolotl.integrations.base import PluginManager from axolotl.monkeypatch.multipack import SUPPORTED_MULTIPACK_MODEL_TYPES from axolotl.monkeypatch.relora import ReLoRACallback +from axolotl.processing_strategies import get_processing_strategy from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.callbacks import ( EvalFirstStepCallback, @@ -747,6 +748,12 @@ def build(self, total_num_steps): self.cfg.accelerator_config ) + if self.cfg.image_size: + training_arguments_kwargs["image_size"] = self.cfg.image_size + if self.cfg.image_resize_algorithm: + training_arguments_kwargs["image_resize_algorithm"] = ( + self.cfg.image_resize_algorithm + ) if self.cfg.kd_ce_alpha is not None: training_arguments_kwargs["kd_ce_alpha"] = self.cfg.kd_ce_alpha if self.cfg.kd_alpha is not None: @@ -890,8 +897,13 @@ def build_collator( else: if self.cfg.processor_type and self.processor: collator = MultiModalChatDataCollator - kwargs["processor"] = self.processor - kwargs["chat_template"] = training_args.chat_template + kwargs["processing_strategy"] = get_processing_strategy( + self.processor, + training_args.chat_template, + self.cfg.chat_template, + image_size=training_args.image_size, + image_resize_algorithm=training_args.image_resize_algorithm, + ) elif self.cfg.batch_flattening: collator = DataCollatorWithFlattening collator_args.pop(0) diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index 82a62c0496..fbb3634929 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -5,6 +5,7 @@ from dataclasses import dataclass, field from typing import Optional +from PIL.Image import Resampling from transformers import TrainingArguments from trl import CPOConfig, KTOConfig, ORPOConfig, PRMConfig, RewardConfig @@ -212,6 +213,20 @@ class AxolotlTrainingMixins: metadata={"help": "The number of workers to use in sequence parallelism"}, ) + # multi-modal section + + image_size: int | tuple[int, int] | None = field( + default=None, + metadata={"help": "The size of the image to resize to"}, + ) + + image_resize_algorithm: Resampling | None = field( + default=None, + metadata={"help": "The algorithm to use for image resizing"}, + ) + + # end of multi-modal section + @dataclass class AxolotlTrainingArguments(AxolotlTrainingMixins, TrainingArguments): diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py new file mode 100644 index 0000000000..0b854af8dc --- /dev/null +++ b/src/axolotl/processing_strategies.py @@ -0,0 +1,278 @@ +"""Module containing ProcessingStrategy classes and its derivative for different MultiModal Model types""" + +from copy import deepcopy +from typing import Optional + +from PIL import Image, ImageOps +from PIL.Image import Resampling +from torch import Tensor +from transformers import ProcessorMixin +from transformers.image_utils import load_image + + +class ProcessingStrategy: + """Base Processing Strategy class""" + + def __init__( + self, + processor: ProcessorMixin, + chat_template: Optional[str] = None, + image_size: int | tuple[int, int] | None = None, + image_resize_algorithm: Resampling | None = None, + ): + self.processor = processor + self.chat_template = chat_template + self.image_token = None + self.image_token_id = None + + self.image_size = image_size + self.image_resize_algorithm = ( + image_resize_algorithm or Image.Resampling.BILINEAR + ) + + if hasattr(processor, "image_token"): + self.image_token = processor.image_token + self.image_token_id = processor.tokenizer.convert_tokens_to_ids( + self.image_token + ) + + def __call__(self, examples: list[dict]) -> list[dict]: + """ + Preprocess conversation examples to ensure consistent format. + Converts different conversation formats to OpenAI format with 'messages'. + Supports two formats: + 1. OpenAI format with 'messages' + 2. Legacy format with 'conversations' + + Args: + examples: list of conversation dictionaries + + Returns: + list of dicts in OpenAI format with 'messages' key + + Raises: + ValueError: If the conversation format is not supported + """ + role_mapping = { + "human": "user", + "gpt": "assistant", + } + + def normalize_role(role: str) -> str: + """Normalize role names to OpenAI format. Default to original role if not found.""" + return role_mapping.get(role, role) + + def convert_legacy_format(example: dict) -> dict: + """Convert legacy 'conversations' format to OpenAI 'messages' format.""" + messages = [ + {"role": normalize_role(convo["from"]), "content": convo["value"]} + for convo in example["conversations"] + ] + + # Create new dict without 'conversations' key + result = deepcopy(example) + result.pop("conversations") + result["messages"] = messages + return result + + def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: + """Convert regular messages format to Messages format with content type""" + + new_messages = [] + for message in messages: + if isinstance(message["content"], str): + new_messages.append( + { + "role": message["role"], + "content": [ + { + "type": "text", + "text": message["content"], + } + ], + } + ) + elif isinstance(message["content"], list): + content = message["content"] + + new_messages.append( + { + "role": message["role"], + "content": content, + } + ) + + return new_messages + + processed_examples = [] + for example in examples: + if not ("messages" in example or "conversations" in example): + raise ValueError( + "Only `messages` and `conversations` message keys are currently supported." + ) + + processed_example = None + if "messages" in example: # OpenAI format + processed_example = example + else: # Legacy format + processed_example = convert_legacy_format(example) + + # convert regular messages format to Messages format with content type + # for compatibility with apply_chat_template + processed_example["messages"] = convert_messages_to_multimedia_messages( + processed_example["messages"] + ) + + # find the image key if it exists + possible_image_keys = ["images", "image"] + image_key = None + for key in possible_image_keys: + if key in processed_example: + image_key = key + break + + # if the image key exists, add the image to the first message + if image_key is not None: + # TODO: check if it's normal to be single image only for common datasets + # From observation, it's usually a list of single image but some datasets may have several columns for images + # Temporary solution: take the first image and suggest people convert their datasets to use multi-content Messages + image_value = processed_example[image_key][0] + + # Handle image loading (Image, url, path, base64) + image_value = load_image(image_value) + + if self.image_size is not None: + assert hasattr( + image_value, "resize" + ), "Image does not have a resize method" + + if isinstance(self.image_size, tuple): + image_value = image_value.resize( + self.image_size, self.image_resize_algorithm + ) + else: + # Set the padding value; here we use black (0, 0, 0) for RGB images + padding_color = (0, 0, 0) + + # When image_size is an int (square target), preserve aspect ratio then pad + # This is to prevent aspect ratio distortion when resizing to square + image_value = ImageOps.pad( + image_value, + (self.image_size, self.image_size), + method=self.image_resize_algorithm, + color=padding_color, + ) + + # Look for any image type in the first message + # some dataset have an {type: "image"} in the first message + ind_to_add = None + + for i, content in enumerate( + processed_example["messages"][0]["content"] + ): + # Usually datasets created with image columns, don't have it in the messages itself + if content["type"] == "image" and all( + k not in content for k in ["image", "url", "path", "base64"] + ): + ind_to_add = i + break + + # If an image type is found, add the image to that index + if ind_to_add is not None: + processed_example["messages"][0]["content"][ind_to_add][ + "image" + ] = image_value + else: + # if no image type is found, add it to end of the first message + processed_example["messages"][0]["content"].append( + { + "type": "image", + "image": image_value, + } + ) + + processed_examples.append(processed_example) + + return processed_examples + + def process_labels(self, input_ids: Tensor) -> Tensor: + labels = input_ids.clone() + + # The labels are the input_ids, and we mask the padding tokens in the loss computation + labels[labels == self.processor.tokenizer.pad_token_id] = -100 + + # Ignore the image token index in the loss computation (model specific) + labels[labels == self.image_token_id] = -100 + + return labels + + +class Qwen2VLProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for Qwen2-VL""" + + def __init__( + self, + processor: ProcessorMixin, + chat_template: Optional[str] = None, + image_size: int | tuple[int, int] | None = None, + image_resize_algorithm: Resampling | None = None, + ): + super().__init__(processor, chat_template, image_size, image_resize_algorithm) + self.image_token = "<|image_pad|>" # nosec + self.image_token_id = processor.tokenizer.convert_tokens_to_ids( + self.image_token + ) + + +class Gemma3ProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for Gemma3""" + + def __init__( + self, + processor: ProcessorMixin, + chat_template: Optional[str] = None, + image_size: int | tuple[int, int] | None = None, + image_resize_algorithm: Resampling | None = None, + ): + super().__init__(processor, chat_template, image_size, image_resize_algorithm) + self.image_token = processor.tokenizer.special_tokens_map["boi_token"] + self.image_token_id = processor.tokenizer.convert_tokens_to_ids( + self.image_token + ) + + def process_labels(self, input_ids): + labels = input_ids.clone() + + # Follows https://ai.google.dev/gemma/docs/core/huggingface_vision_finetune_qlora + labels[labels == self.processor.tokenizer.pad_token_id] = -100 + labels[labels == self.image_token_id] = -100 + labels[labels == 262144] = -100 # corresponds to + + return labels + + +def get_processing_strategy( + processor: ProcessorMixin, + chat_template, + chat_template_type, + image_size: int | tuple[int, int] | None = None, + image_resize_algorithm: Resampling | None = None, +): + if chat_template_type == "qwen2_vl": + return Qwen2VLProcessingStrategy( + processor, chat_template, image_size, image_resize_algorithm + ) + if chat_template_type == "gemma3": + return Gemma3ProcessingStrategy( + processor, chat_template, image_size, image_resize_algorithm + ) + if chat_template_type in [ + "llama3_2_vision", + "llava", + "mistral_v7_tekken", + "pixtral", + ]: + return ProcessingStrategy( + processor, chat_template, image_size, image_resize_algorithm + ) + raise ValueError(f"Unsupported chat template type: {chat_template_type}") diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 7dbeda4626..ba0516eb91 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -20,12 +20,14 @@ "mistral_v1": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ ' [INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # Mistral 7B V1, Mistral 7B V2, Mixtral 8x7B V1... "mistral_v2v3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # V3: Mistral 7B V3, Small, Large... "mistral_v3_tekken": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST]' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # V3-Tekken: Nemo, Pixtral... + "mistral_v7_tekken": "{%- set today = strftime_now(\"%Y-%m-%d\") %}\n{%- set default_system_message = \"You are Mistral Small 3, a Large Language Model (LLM) created by Mistral AI, a French startup headquartered in Paris.\\nYour knowledge base was last updated on 2023-10-01. The current date is \" + today + \".\\n\\nWhen you're not sure about some information, you say that you don't have the information and don't make up anything.\\nIf the user's question is not clear, ambiguous, or does not provide enough context for you to accurately answer the question, you do not try to answer it right away and you rather ask the user to clarify their request (e.g. \\\"What are some good restaurants around me?\\\" => \\\"Where are you?\\\" or \\\"When is the next flight to Tokyo\\\" => \\\"Where do you travel from?\\\")\" %}\n\n{{- bos_token }}\n\n{%- if messages[0]['role'] == 'system' %}\n {%- set system_message = messages[0]['content'] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set system_message = default_system_message %}\n {%- set loop_messages = messages %}\n{%- endif %}\n{{- '[SYSTEM_PROMPT]' + system_message + '[/SYSTEM_PROMPT]' }}\n\n{%- for message in loop_messages %}\n {%- if message['role'] == 'user' %}\n\t {%- if message['content'] is string %}\n {{- '[INST]' + message['content'] + '[/INST]' }}\n\t {%- else %}\n\t\t {{- '[INST]' }}\n\t\t {%- for block in message['content'] %}\n\t\t\t {%- if block['type'] == 'text' %}\n\t\t\t\t {{- block['text'] }}\n\t\t\t {%- elif block['type'] == 'image' or block['type'] == 'image_url' %}\n\t\t\t\t {{- '[IMG]' }}\n\t\t\t\t{%- else %}\n\t\t\t\t {{- raise_exception('Only text and image blocks are supported in message content!') }}\n\t\t\t\t{%- endif %}\n\t\t\t{%- endfor %}\n\t\t {{- '[/INST]' }}\n\t\t{%- endif %}\n {%- elif message['role'] == 'system' %}\n {{- '[SYSTEM_PROMPT]' + message['content'] + '[/SYSTEM_PROMPT]' }}\n {%- elif message['role'] == 'assistant' %}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- elif message['content'] is iterable %} \n\t\t {%- for block in message['content'] %}\n\t\t\t {%- if block['type'] == 'text' %}\n\t\t\t\t {{- block['text'] }}\n\t\t\t {%- else %}\n\t\t\t\t {{- raise_exception('Only text blocks are supported in assistant message content!') }} {%- endif %}\n\t\t\t \n\t\t\t{%- endfor %} {{- eos_token }} {%- else %}\n {{- raise_exception('Unsupported assistant message content format!') }} \n{%- endif %} \n{%- else %}\n {{- raise_exception('Only user, system and assistant roles are supported!') }}\n {%- endif %}\n{%- endfor %}", "chatml": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}", "gemma": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\n' + message['content'] | trim + '\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\n'}}{% endif %}", - "gemma3_text": "{{ bos_token }}\n{%- if messages[0]['role'] == 'system' -%}\n {%- if messages[0]['content'] is string -%}\n {%- set first_user_prefix = messages[0]['content'] + '\n\n' -%}\n {%- else -%}\n {%- set first_user_prefix = messages[0]['content'][0]['text'] + '\n\n' -%}\n {%- endif -%}\n {%- set loop_messages = messages[1:] -%}\n{%- else -%}\n {%- set first_user_prefix = \"\" -%}\n {%- set loop_messages = messages -%}\n{%- endif -%}\n{%- for message in loop_messages -%}\n {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) -%}\n {{ raise_exception(\"Conversation roles must alternate user/assistant/user/assistant/...\") }}\n {%- endif -%}\n {%- if (message['role'] == 'assistant') -%}\n {%- set role = \"model\" -%}\n {%- else -%}\n {%- set role = message['role'] -%}\n {%- endif -%}\n {{ '' + role + '\n' + (first_user_prefix if loop.first else \"\") }}\n {%- if message['content'] is string -%}\n {{ message['content'] | trim }}\n {%- elif message['content'] is iterable -%}\n {%- for item in message['content'] -%}\n {%- if item['type'] == 'image' -%}\n {{ '' }}\n {%- elif item['type'] == 'text' -%}\n {{ item['text'] | trim }}\n {%- endif -%}\n {%- endfor -%}\n {%- else -%}\n {{ raise_exception(\"Invalid content type\") }}\n {%- endif -%}\n {{ '\n' }}\n{%- endfor -%}\n{%- if add_generation_prompt -%}\n {{'model\n'}}\n{%- endif -%}\n", + "gemma3": "{{ bos_token }}\n{%- if messages[0]['role'] == 'system' -%}\n {%- if messages[0]['content'] is string -%}\n {%- set first_user_prefix = messages[0]['content'] + '\n\n' -%}\n {%- else -%}\n {%- set first_user_prefix = messages[0]['content'][0]['text'] + '\n\n' -%}\n {%- endif -%}\n {%- set loop_messages = messages[1:] -%}\n{%- else -%}\n {%- set first_user_prefix = \"\" -%}\n {%- set loop_messages = messages -%}\n{%- endif -%}\n{%- for message in loop_messages -%}\n {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) -%}\n {{ raise_exception(\"Conversation roles must alternate user/assistant/user/assistant/...\") }}\n {%- endif -%}\n {%- if (message['role'] == 'assistant') -%}\n {%- set role = \"model\" -%}\n {%- else -%}\n {%- set role = message['role'] -%}\n {%- endif -%}\n {{ '' + role + '\n' + (first_user_prefix if loop.first else \"\") }}\n {%- if message['content'] is string -%}\n {{ message['content'] | trim }}\n {%- elif message['content'] is iterable -%}\n {%- for item in message['content'] -%}\n {%- if item['type'] == 'image' -%}\n {{ '' }}\n {%- elif item['type'] == 'text' -%}\n {{ item['text'] | trim }}\n {%- endif -%}\n {%- endfor -%}\n {%- else -%}\n {{ raise_exception(\"Invalid content type\") }}\n {%- endif -%}\n {{ '\n' }}\n{%- endfor -%}\n{%- if add_generation_prompt -%}\n {{'model\n'}}\n{%- endif -%}\n", "cohere": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", "llama3_2_vision": '{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now("%d %b %Y") %}\n {%- else %}\n {%- set date_string = "26 Jul 2024" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0][\'role\'] == \'system\' %}\n {%- set system_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n{%- else %}\n {%- set system_message = "" %}\n{%- endif %}\n\n{#- Find out if there are any images #}\n{% set image_ns = namespace(has_images=false) %} \n{%- for message in messages %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {%- set image_ns.has_images = true %}\n {%- endif %}\n {%- endfor %}\n{%- endfor %}\n\n{#- Error out if there are images and system message #}\n{%- if image_ns.has_images and not system_message == "" %}\n {{- raise_exception("Prompting with images is incompatible with system messages.") }}\n{%- endif %}\n\n{#- System message if there are no images #}\n{%- if not image_ns.has_images %}\n {{- "<|start_header_id|>system<|end_header_id|>\\n\\n" }}\n {%- if tools is not none %}\n {{- "Environment: ipython\\n" }}\n {%- endif %}\n {{- "Cutting Knowledge Date: December 2023\\n" }}\n {{- "Today Date: " + date_string + "\\n\\n" }}\n {%- if tools is not none and not tools_in_user_message %}\n {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- "<|eot_id|>" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception("Cannot put tools in the first user message when there\'s no first user message!") }}\n{%- endif %}\n {{- \'<|start_header_id|>user<|end_header_id|>\\n\\n\' -}}\n {{- "Given the following functions, please respond with a JSON for a function call " }}\n {{- "with its proper arguments that best answers the given prompt.\\n\\n" }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {{- first_user_message + "<|eot_id|>"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == \'ipython\' or message.role == \'tool\' or \'tool_calls\' in message) %}\n {{- \'<|start_header_id|>\' + message[\'role\'] + \'<|end_header_id|>\\n\\n\' }}\n {%- if message[\'content\'] is string %}\n {{- message[\'content\'] }}\n {%- else %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {{- \'<|image|>\' }}\n {%- elif content[\'type\'] == \'text\' %}\n {{- content[\'text\'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \'<|eot_id|>\' }}\n {%- elif \'tool_calls\' in message %}\n {%- if not message.tool_calls|length == 1 %}\n {{- raise_exception("This model only supports single tool-calls at once!") }}\n {%- endif %}\n {%- set tool_call = message.tool_calls[0].function %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' -}}\n {{- \'{"name": "\' + tool_call.name + \'", \' }}\n {{- \'"parameters": \' }}\n {{- tool_call.arguments | tojson }}\n {{- "}" }}\n {{- "<|eot_id|>" }}\n {%- elif message.role == "tool" or message.role == "ipython" %}\n {{- "<|start_header_id|>ipython<|end_header_id|>\\n\\n" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- "<|eot_id|>" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' }}\n{%- endif %}\n', + "llava": "{% for message in messages %}{% if message['role'] != 'system' %}{{ message['role'].upper() + ': '}}{% endif %}{# Render all images first #}{% for content in message['content'] | selectattr('type', 'equalto', 'image') %}{{ '\n' }}{% endfor %}{# Render all text next #}{% if message['role'] != 'assistant' %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{{ content['text'] + ' '}}{% endfor %}{% else %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{% generation %}{{ content['text'] + ' '}}{% endgeneration %}{% endfor %}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ 'ASSISTANT:' }}{% endif %}", "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", "phi_35": "{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'user' %}{{'<|user|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|>\n' + message['content'] + '<|end|>\n'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% endif %}", "deepseek_v2": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %}", @@ -34,6 +36,8 @@ "qwen_25": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n", "exaone": "{% for message in messages %}{% if loop.first and message['role'] != 'system' %}{{ '[|system|][|endofturn|]\n' }}{% endif %}{{ '[|' + message['role'] + '|]' + message['content'] }}{% if message['role'] == 'user' %}{{ '\n' }}{% else %}{{ '[|endofturn|]\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '[|assistant|]' }}{% endif %}", "metharme": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% else %}{% set loop_messages = messages %}{% set system_message = 'Enter RP mode. You shall reply to the user while staying in character. Your responses must be detailed, creative, immersive, and drive the scenario forward.' %}{% endif %}{{ '<|system|>' + system_message }}{% for message in loop_messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|user|>' + content.strip() }}{% elif message['role'] == 'assistant' %}{{ '<|model|>' + content.strip() }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|model|>' }}{% else %}{{ eos_token }}{% endif %}", + "pixtral": '{%- if messages[0]["role"] == "system" %}\n {%- set system_message = messages[0]["content"] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set loop_messages = messages %}\n{%- endif %}\n\n{{- bos_token }}\n{%- for message in loop_messages %}\n {%- if (message[\'role\'] == \'user\') != (loop.index0 % 2 == 0) %}\n {{- raise_exception(\'After the optional system message, conversation roles must alternate user/assistant/user/assistant/...\') }}\n {%- endif %}\n {%- if message["role"] == "user" %}\n {%- if loop.last and system_message is defined %}\n {{- "[INST]" + system_message + "\n\n" }}\n {%- else %}\n {{- "[INST]" }}\n {%- endif %}\n {%- if message["content"] is not string %}\n {%- for chunk in message["content"] %}\n {%- if chunk["type"] == "text" %}\n {{- chunk["text"] }}\n {%- elif chunk["type"] == "image" %}\n {{- "[IMG]" }}\n {%- else %}\n {{- raise_exception("Unrecognized content type!") }}\n {%- endif %}\n {%- endfor %}\n {%- else %}\n {{- message["content"] }}\n {%- endif %}\n {{- "[/INST]" }}\n {%- elif message["role"] == "assistant" %}\n {%- if message["content"] is not string %}\n {%- for chunk in message["content"] %}\n {%- if chunk["type"] == "text" %}\n {{- chunk["text"] }}\n {%- elif chunk["type"] == "image" %}\n {{- "[IMG]" }}\n {%- else %}\n {{- raise_exception("Unrecognized content type!") }}\n{%- endif %}\n{%- endfor %}\n{{- eos_token }}\n{%- else %}\n{{- message["content"] + eos_token }}\n{%- endif %}\n {%- else %}\n {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }}\n {%- endif %}\n{%- endfor %}', + "qwen2_vl": "{% set image_count = namespace(value=0) %}{% set video_count = namespace(value=0) %}{% for message in messages %}{% if loop.first and message['role'] != 'system' %}<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n{% endif %}<|im_start|>{{ message['role'] }}\n{% if message['content'] is string %}{{ message['content'] }}<|im_end|>\n{% else %}{% for content in message['content'] %}{% if content['type'] == 'image' or 'image' in content or 'image_url' in content %}{% set image_count.value = image_count.value + 1 %}{% if add_vision_id %}Picture {{ image_count.value }}: {% endif %}<|vision_start|><|image_pad|><|vision_end|>{% elif content['type'] == 'video' or 'video' in content %}{% set video_count.value = video_count.value + 1 %}{% if add_vision_id %}Video {{ video_count.value }}: {% endif %}<|vision_start|><|video_pad|><|vision_end|>{% elif 'text' in content %}{{ content['text'] }}{% endif %}{% endfor %}<|im_end|>\n{% endif %}{% endfor %}{% if add_generation_prompt %}<|im_start|>assistant\n{% endif %}", } diff --git a/src/axolotl/utils/collators/mm_chat.py b/src/axolotl/utils/collators/mm_chat.py index 6f8a64ad85..75d72f8dc1 100644 --- a/src/axolotl/utils/collators/mm_chat.py +++ b/src/axolotl/utils/collators/mm_chat.py @@ -2,15 +2,17 @@ Collators for multi-modal chat messages and packing """ -from copy import deepcopy from dataclasses import dataclass from typing import Any, Optional, Union -from PIL import Image -from transformers import PreTrainedTokenizerBase, ProcessorMixin +import torch +from torch import Tensor +from transformers import PreTrainedTokenizerBase from transformers.data.data_collator import DataCollatorMixin from transformers.utils import PaddingStrategy +from axolotl.processing_strategies import ProcessingStrategy + @dataclass class MultiModalChatDataCollator(DataCollatorMixin): @@ -19,11 +21,9 @@ class MultiModalChatDataCollator(DataCollatorMixin): """ tokenizer: PreTrainedTokenizerBase - processor: ProcessorMixin - return_tensors: str = "pt" - chat_template: Optional[str] = None + processing_strategy: ProcessingStrategy packing: bool = False - max_images: int = -1 + return_tensors: str = "pt" padding: Union[bool, str, PaddingStrategy] = True pad_to_multiple_of: Optional[int] = None @@ -31,162 +31,62 @@ def __post_init__(self): if self.packing: raise ValueError("Packing is currently not supported.") - def torch_call( - self, examples: list[Union[list[int], Any, dict[str, Any]]] - ) -> dict[str, Any]: - # Handle dict or lists with proper padding and conversion to tensor. + def torch_call(self, examples: list[dict]) -> dict[str, Any]: + return self.process_rows(examples) - return self.__class__.process_rows( - examples, self.processor, self.chat_template, self.max_images - ) + def process_rows( + self, + examples: list[dict], + ) -> dict[str, Tensor]: + # Preprocess the examples + examples = self.processing_strategy(examples) + + # Initialize batch + batch: dict[str, Any] = {} + + # Process each example + for example in examples: + # Apply chat template to process the example + # This method requires transformers>=4.49.0 + result = self.processing_strategy.processor.apply_chat_template( + example["messages"], + add_generation_prompt=True, + tokenize=True, + return_tensors="pt", + padding=True, + return_dict=True, + chat_template=self.processing_strategy.chat_template, + ) - @staticmethod - def process_rows(examples, processor, chat_template, max_images, length_only=False): - # HINT: use `_torch_collate_batch` to stack and pad tensors - # see also DataCollatorWithFlattening and DefaultDataCollator - - # *** This is COPIED from the trl example sft_vlm.py code *** - # use this as a starting point - - def _preprocess(examples: list[dict]) -> list[dict]: - """ - Preprocess conversation examples to ensure consistent format. - - Converts different conversation formats to OpenAI format with 'messages'. - Supports two formats: - 1. OpenAI format with 'messages' - 2. Legacy format with 'conversations' - - Args: - examples: list of conversation dictionaries - - Returns: - dict in OpenAI format with 'messages' key - - Raises: - ValueError: If the conversation format is not supported - """ - role_mapping = { - "human": "user", - "gpt": "assistant", - } - - def normalize_role(role: str) -> str: - """Normalize role names to OpenAI format. Default to original role if not found.""" - return role_mapping.get(role, role) - - def convert_legacy_format(example: dict) -> dict: - """Convert legacy 'conversations' format to OpenAI 'messages' format.""" - messages = [ - { - "role": normalize_role(convo["from"]), - "content": convo["value"], - } - for convo in example["conversations"] - ] - - # Create new dict without 'conversations' key - result = deepcopy(example) - result.pop("conversations") - return {"messages": messages, **result} - - processed_examples = [] - for example in examples: - # OpenAI format - if "messages" in example: - processed_examples.append(example) - - # Legacy format - elif "conversations" in example: - processed_examples.append(convert_legacy_format(example)) - - else: - raise ValueError( - "Only `messages` and `conversations` message keys are currently supported." - ) - - return processed_examples - - def _process_images(examples, max_images): - """ - Process images from examples, ensuring consistency in image presence and applying max_images limit. - - Args: - examples: List of dictionaries that may contain 'images' key - max_images: Maximum number of images to keep per example (0 means no limit) - - Returns: - Either None (if no images) or List[Image objects] (if all examples have images) - - Raises: - ValueError: If there's a mix of None and non-None images - """ - - def get_image(example): - if "images" not in example: - return None - images = example["images"] - if isinstance(images, str): - return Image.open(images) - return images - - images = [get_image(example) for example in examples] - - # Count None and non-None images - none_count = sum(1 for img in images if img is None) - - # All images are None - if none_count == len(images): - return None - - # Mix of None and non-None images - if none_count > 0: - raise ValueError( - "All images should be either None or not None. " - "Please provide images for all examples or None." - ) - - # Apply max_images limit if specified - if max_images > 0: - images = [ - ( - img_batch[:max_images] - if isinstance(img_batch, (list, tuple)) - else img_batch - ) - for img_batch in images - ] - - return images + # TODO: Check if need handling for len(input_ids) > sequence_len - # Preprocess the examples - examples = _preprocess(examples) + # Add the processed tensors to our batch + for key in result.keys(): + if key not in batch: + batch[key] = [] - # Get the texts and images, and apply the chat template - texts = [ - processor.apply_chat_template( - example["messages"], chat_template=chat_template, tokenize=False - ) - for example in examples - ] + batch[key].append(result[key].squeeze(0)) + + # Pad sequences to the same length + input_ids = torch.nn.utils.rnn.pad_sequence( + batch["input_ids"], + batch_first=True, + padding_value=self.tokenizer.pad_token_id, + ) - images = _process_images(examples, max_images=max_images) + attention_mask = torch.nn.utils.rnn.pad_sequence( + batch["attention_mask"], batch_first=True, padding_value=0 + ) - # Tokenize the texts and process the images - batch = processor(text=texts, images=images, return_tensors="pt", padding=True) + # Create the final batch + final_batch = { + "input_ids": input_ids, + "attention_mask": attention_mask, + } - # The labels are the input_ids, and we mask the padding tokens in the loss computation - labels = batch["input_ids"].clone() - labels[labels == processor.tokenizer.pad_token_id] = -100 # - # Ignore the image token index in the loss computation (model specific) - image_token_id = processor.tokenizer.convert_tokens_to_ids( - processor.image_token + # Process the labels + final_batch["labels"] = self.processing_strategy.process_labels( + final_batch["input_ids"] ) - labels[labels == image_token_id] = -100 - batch["labels"] = labels - - if length_only: - return { - "length": [len(sample["input_ids"]) for sample in batch["input_ids"]] - } - return batch + + return final_batch diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 4e956140d1..634575066f 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -13,7 +13,7 @@ from axolotl.integrations.config import merge_input_args from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.dict import DictDefault -from axolotl.utils.models import load_model_config +from axolotl.utils.models import MULTIMODAL_AUTO_MODEL_MAPPING, load_model_config from axolotl.utils.schemas.config import ( AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, ) @@ -158,7 +158,7 @@ def normalize_config(cfg): cfg.is_multimodal = ( hasattr(model_config, "model_type") - and model_config.model_type in ["llava", "mllama"] + and model_config.model_type in MULTIMODAL_AUTO_MODEL_MAPPING or any( multimodal_name in cfg.base_model.lower() for multimodal_name in [ @@ -171,7 +171,6 @@ def normalize_config(cfg): cfg.processor_config = ( cfg.processor_config or cfg.base_model_config or cfg.base_model ) - model_config = model_config.text_config cfg.model_config_type = model_config.model_type diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 83f70a022a..23a6e102f3 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -34,12 +34,16 @@ AutoTokenizer, AwqConfig, BitsAndBytesConfig, + Gemma3ForConditionalGeneration, GPTQConfig, LlavaForConditionalGeneration, + Mistral3ForConditionalGeneration, MllamaForConditionalGeneration, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, + Qwen2_5_VLForConditionalGeneration, + Qwen2VLForConditionalGeneration, ) from transformers.integrations.deepspeed import ( HfTrainerDeepSpeedConfig, @@ -69,9 +73,13 @@ LOG = logging.getLogger(__name__) -MULTIMODEL_AUTO_MODEL_MAPPING = { - "llava": LlavaForConditionalGeneration, +MULTIMODAL_AUTO_MODEL_MAPPING = { "mllama": MllamaForConditionalGeneration, + "llava": LlavaForConditionalGeneration, + "qwen2_vl": Qwen2VLForConditionalGeneration, + "qwen2_5_vl": Qwen2_5_VLForConditionalGeneration, + "mistral3": Mistral3ForConditionalGeneration, + "gemma3": Gemma3ForConditionalGeneration, } @@ -101,7 +109,21 @@ def get_module_class_from_name(module, name): def check_model_config(cfg: DictDefault, model_config: Union[AutoConfig, DictDefault]): if cfg.is_multimodal: - model_config = model_config.text_config + if hasattr(model_config, "text_config"): + model_config = model_config.text_config + model_config.use_cache = False + elif hasattr(model_config, "get_text_config"): + model_config = model_config.get_text_config() + model_config.use_cache = False + + # check if image_size is not set and load image size from model config if available + if ( + cfg.image_size is None + and hasattr(model_config, "vision_config") + and hasattr(model_config.vision_config, "image_size") + ): + cfg.image_size = model_config.vision_config.image_size + LOG.debug(f"Loaded image size: {cfg.image_size} from model config") quant_config_exists = ( hasattr(model_config, "quantization_config") @@ -440,6 +462,31 @@ def load_processor(cfg: DictDefault, tokenizer: PreTrainedTokenizerBase): **processor_kwargs, ) + # Attempt to load image size from processor if available + if ( + cfg.image_size is None + and hasattr(processor, "size") + and any(dim in processor.size for dim in ["width", "height"]) + ): + im_width = None + im_height = None + if "width" in processor.size: + im_width = processor.size["width"] + if "height" in processor.size: + im_height = processor.size["height"] + + # If both width and height are set, use a tuple + if im_width is not None and im_height is not None: + cfg.image_size = (im_width, im_height) + # If only width is set, use as integer + elif im_width is not None: + cfg.image_size = im_width + # If only height is set, use as integer + elif im_height is not None: + cfg.image_size = im_height + + LOG.debug(f"Loaded image size: {cfg.image_size} from processor") + return processor @@ -477,7 +524,11 @@ def __init__( # init model config self.model_config = load_model_config(cfg) if cfg.is_multimodal: - self.text_model_config = self.model_config.text_config + if hasattr(self.model_config, "text_config"): + self.text_model_config = self.model_config.text_config + else: + # for qwen2_vl + self.text_model_config = self.model_config.get_text_config() else: self.text_model_config = self.model_config @@ -673,7 +724,7 @@ def set_auto_model_loader(self): should be set according to the type of the model. """ if self.cfg.is_multimodal: - self.auto_model_loader = MULTIMODEL_AUTO_MODEL_MAPPING.get( + self.auto_model_loader = MULTIMODAL_AUTO_MODEL_MAPPING.get( self.model_config.model_type, AutoModelForVision2Seq ) @@ -1194,7 +1245,9 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: ) ): resize_kwargs = {} - if self.cfg.mean_resizing_embeddings is not None: + if self.cfg.mean_resizing_embeddings is not None and not ( + self.model_config.model_type == "llava" + ): resize_kwargs["mean_resizing"] = self.cfg.mean_resizing_embeddings self.model.resize_token_embeddings(embeddings_len, **resize_kwargs) else: diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 7992e65594..d52146092f 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -42,6 +42,7 @@ ModelOutputConfig, SpecialTokensConfig, ) +from axolotl.utils.schemas.multimodal import MultiModalConfig from axolotl.utils.schemas.peft import LoraConfig, ReLoRAConfig from axolotl.utils.schemas.training import HyperparametersConfig from axolotl.utils.schemas.trl import TRLConfig @@ -64,6 +65,7 @@ class AxolotlInputConfig( LISAConfig, GradioConfig, RayConfig, + MultiModalConfig, RemappedParameters, DeprecatedParameters, BaseModel, diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index a0c6df7103..ad735afe0e 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -22,8 +22,8 @@ class ChatTemplate(str, Enum): mistral_v1 = "mistral_v1" # pylint: disable=invalid-name mistral_v2v3 = "mistral_v2v3" # pylint: disable=invalid-name mistral_v3_tekken = "mistral_v3_tekken" # pylint: disable=invalid-name + mistral_v7_tekken = "mistral_v7_tekken" # pylint: disable=invalid-name gemma = "gemma" # pylint: disable=invalid-name - gemma3_text = "gemma3_text" # pylint: disable=invalid-name cohere = "cohere" # pylint: disable=invalid-name llama3 = "llama3" # pylint: disable=invalid-name llama3_2_vision = "llama3_2_vision" # pylint: disable=invalid-name @@ -37,6 +37,10 @@ class ChatTemplate(str, Enum): tokenizer_default = "tokenizer_default" # pylint: disable=invalid-name exaone = "exaone" # pylint: disable=invalid-name metharme = "metharme" # pylint: disable=invalid-name + pixtral = "pixtral" # pylint: disable=invalid-name + llava = "llava" # pylint: disable=invalid-name + qwen2_vl = "qwen2_vl" # pylint: disable=invalid-name + gemma3 = "gemma3" # pylint: disable=invalid-name class CustomSupportedOptimizers(str, Enum): diff --git a/src/axolotl/utils/schemas/multimodal.py b/src/axolotl/utils/schemas/multimodal.py new file mode 100644 index 0000000000..a3449199f3 --- /dev/null +++ b/src/axolotl/utils/schemas/multimodal.py @@ -0,0 +1,48 @@ +"""Pydantic models for multimodal-related configuration""" + +from typing import Literal + +from PIL.Image import Resampling +from pydantic import BaseModel, Field, field_validator + + +class MultiModalConfig(BaseModel): + """Multi-modal configuration subset""" + + image_size: int | tuple[int, int] | None = Field( + default=None, + json_schema_extra={ + "description": ( + "The size of the image to resize to. It can be an integer (resized into padded-square image) or a tuple (width, height)." + "If not provided, we will attempt to load from preprocessor.size, otherwise, images won't be resized." + ) + }, + ) + image_resize_algorithm: ( + Literal["bilinear", "bicubic", "lanczos"] | Resampling | None + ) = Field( + default=None, + json_schema_extra={ + "description": "The resampling algorithm to use for image resizing. Default is bilinear. Please refer to PIL.Image.Resampling for more details." + }, + ) + + @field_validator("image_resize_algorithm", mode="before") + @classmethod + def convert_image_resize_algorithm(cls, image_resize_algorithm): + """ + Convert the image resize algorithm to a PIL.Image.Resampling enum. + """ + if isinstance(image_resize_algorithm, str): + image_resize_algorithm = image_resize_algorithm.lower() + if image_resize_algorithm == "bilinear": + image_resize_algorithm = Resampling.BILINEAR + elif image_resize_algorithm == "bicubic": + image_resize_algorithm = Resampling.BICUBIC + elif image_resize_algorithm == "lanczos": + image_resize_algorithm = Resampling.LANCZOS + else: + raise ValueError( + f"Invalid image resize algorithm: {image_resize_algorithm}" + ) + return image_resize_algorithm diff --git a/tests/e2e/test_gemma3_text.py b/tests/e2e/test_gemma3_text.py index 6ce360f688..14423ce733 100644 --- a/tests/e2e/test_gemma3_text.py +++ b/tests/e2e/test_gemma3_text.py @@ -58,7 +58,7 @@ def test_lora_gemma3_text(self, temp_dir, sample_packing): "bos_token": "", "eos_token": "", }, - "chat_template": "gemma3_text", + "chat_template": "gemma3", "num_epochs": 1, "micro_batch_size": 1, "gradient_accumulation_steps": 4, @@ -105,7 +105,7 @@ def test_fft_gemma3_text(self, temp_dir, sample_packing): "split": "train[:1%]", }, ], - "chat_template": "gemma3_text", + "chat_template": "gemma3", "special_tokens": { "bos_token": "", "eos_token": "", From 2c34a4634e1a1059fd07d4e27f10849c6d4c173c Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 27 Mar 2025 05:13:51 +0700 Subject: [PATCH 0487/1405] feat: add CCE for gemma3, cohere, and cohere2 (#2443) * feat: add CCE for gemma3 and cohere1/2 * fix: change from relative import to absolute * feat: add multipack for cohere&cohere2 * chore: improve comments * fix: add gemma3_text * feat: add cohere2 example * fix: cohere forward * fix: patch for cohere2 * feat: add command r v01 qlora sample * chore: lint * feat: upgrade gemma3 and gemma2 patch to use logits_to_keep * chore: lint * fix: add deprecate_kwarg decorator * fix: add cce for gemma3 conditionalgeneration * fix: gemma3 patch to defer logits calculation * fix: patch gemma3 if given as model * fix: remove not working config * fix: update comments to clarify changes * feat(doc): add supported models to readme * fix: address difference in our cohere patch * feat: add mistral3 * feat: add gemma * feat(doc): update README to include gemma and mistral3 in supported models * fix: gemma patch * fix: import * fix: gemma patch to be standalone * fix: gemma3 warn about not support final_logit_softcapping * feat: add mllama CCE * chore: add abbireviation to doc * fix: remove unneeded gemma3 eager warning * fix: save processor if available * fix: enable save processor on merge * fix: wrong env meaning --- docs/docker.qmd | 3 +- examples/cohere/command-r-7b-qlora.yml | 71 +++ src/axolotl/cli/inference.py | 4 +- src/axolotl/cli/merge_lora.py | 5 +- src/axolotl/cli/utils.py | 26 +- .../integrations/cut_cross_entropy/README.md | 16 +- .../cut_cross_entropy/__init__.py | 4 +- .../cut_cross_entropy/monkeypatch/cohere.py | 201 ++++++++ .../cut_cross_entropy/monkeypatch/gemma.py | 175 +++++++ .../cut_cross_entropy/monkeypatch/gemma3.py | 465 ++++++++++++++++++ .../cut_cross_entropy/monkeypatch/mistral3.py | 392 +++++++++++++++ .../cut_cross_entropy/monkeypatch/mllama.py | 379 ++++++++++++++ .../cut_cross_entropy/monkeypatch/patch.py | 85 ++++ src/axolotl/monkeypatch/multipack.py | 2 + src/axolotl/train.py | 11 +- .../lora_kernels/test_lora_kernel_patching.py | 2 +- 16 files changed, 1826 insertions(+), 15 deletions(-) create mode 100644 examples/cohere/command-r-7b-qlora.yml create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/mistral3.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/mllama.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py diff --git a/docs/docker.qmd b/docs/docker.qmd index 2908592fa7..6bdd77a543 100644 --- a/docs/docker.qmd +++ b/docs/docker.qmd @@ -103,8 +103,7 @@ This uses the same tags as the [`main` image](#sec-main-tags). - `JUPYTER_DISABLE`: Disable Jupyter lab. - `JUPYTER_PASSWORD`: Set a password for the Jupyter lab. -- `PUBLIC_KEY`: Add a public key for the SSH service. -- `SSH_KEY`: Add a private key for the SSH service. +- `PUBLIC_KEY` / `SSH_KEY`: Add a public key for the SSH service. #### Volume mounts diff --git a/examples/cohere/command-r-7b-qlora.yml b/examples/cohere/command-r-7b-qlora.yml new file mode 100644 index 0000000000..2ac5c4c09d --- /dev/null +++ b/examples/cohere/command-r-7b-qlora.yml @@ -0,0 +1,71 @@ +base_model: CohereForAI/c4ai-command-r7b-12-2024 +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer + +load_in_8bit: false +load_in_4bit: true +strict: false + +# huggingface repo +chat_template: cohere +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: true + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index e11a39bd62..f4fbdbed4a 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -56,7 +56,7 @@ def do_inference( cfg: Dictionary mapping `axolotl` config keys to values. cli_args: Inference-specific CLI arguments. """ - model, tokenizer = load_model_and_tokenizer(cfg=cfg, inference=True) + model, tokenizer, _ = load_model_and_tokenizer(cfg=cfg, inference=True) prompter = cli_args.prompter prompter_module = None @@ -151,7 +151,7 @@ def do_inference_gradio( """ import gradio as gr - model, tokenizer = load_model_and_tokenizer(cfg=cfg, inference=True) + model, tokenizer, _ = load_model_and_tokenizer(cfg=cfg, inference=True) prompter = cli_args.prompter prompter_module = None diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 595eb3eab7..2a3343b6b9 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -27,7 +27,7 @@ def do_merge_lora(*, cfg: DictDefault) -> None: """ print_axolotl_text_art() - model, tokenizer = load_model_and_tokenizer(cfg=cfg) + model, tokenizer, processor = load_model_and_tokenizer(cfg=cfg) safe_serialization = cfg.save_safetensors is True LOG.info("Running merge of LoRA with base model...") @@ -44,6 +44,9 @@ def do_merge_lora(*, cfg: DictDefault) -> None: ) tokenizer.save_pretrained(str(Path(cfg.output_dir) / "merged")) + if processor: + processor.save_pretrained(str(Path(cfg.output_dir) / "merged")) + def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: """ diff --git a/src/axolotl/cli/utils.py b/src/axolotl/cli/utils.py index cb61fa3711..7cc4d27448 100644 --- a/src/axolotl/cli/utils.py +++ b/src/axolotl/cli/utils.py @@ -13,11 +13,16 @@ import click import requests from pydantic import BaseModel -from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast +from transformers import ( + PreTrainedModel, + PreTrainedTokenizer, + PreTrainedTokenizerFast, + ProcessorMixin, +) from axolotl.logging_config import configure_logging from axolotl.utils.dict import DictDefault -from axolotl.utils.models import load_model, load_tokenizer +from axolotl.utils.models import load_model, load_processor, load_tokenizer configure_logging() LOG = logging.getLogger(__name__) @@ -295,9 +300,13 @@ def load_model_and_tokenizer( *, cfg: DictDefault, inference: bool = False, -) -> tuple[PreTrainedModel, PreTrainedTokenizer | PreTrainedTokenizerFast | Any]: +) -> tuple[ + PreTrainedModel, + PreTrainedTokenizer | PreTrainedTokenizerFast | Any, + ProcessorMixin | None, +]: """ - Helper function for loading a model and tokenizer specified in the given `axolotl` + Helper function for loading a model, tokenizer, and processor specified in the given `axolotl` config. Args: @@ -305,7 +314,7 @@ def load_model_and_tokenizer( inference: Boolean denoting inference mode. Returns: - `transformers` model and tokenizer. + Tuple of (PreTrainedModel, PreTrainedTokenizer, ProcessorMixin). """ LOG.info(f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}") tokenizer = load_tokenizer(cfg) @@ -313,4 +322,9 @@ def load_model_and_tokenizer( LOG.info("loading model...") model, _ = load_model(cfg, tokenizer, inference=inference) - return model, tokenizer + processor = None + if cfg.is_multimodal: + LOG.info("loading processor...") + processor = load_processor(cfg, tokenizer) + + return model, tokenizer, processor diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index b166a30049..7b428eb58c 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -1,6 +1,6 @@ # Cut Cross Entropy -Cut Cross Entropy reduces VRAM usage through optimization on the cross-entropy operation during loss calculation. +Cut Cross Entropy (CCE) reduces VRAM usage through optimization on the cross-entropy operation during loss calculation. See https://github.com/apple/ml-cross-entropy @@ -29,6 +29,20 @@ plugins: cut_cross_entropy: true ``` +## Supported Models + +- llama +- phi3 +- gemma +- gemma2 +- gemma3 +- gemma3_text +- mistral +- mistral3 +- qwen2 +- cohere +- cohere2 + ## Citation ```bib diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 516e9a2ae0..a475cd9f7f 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -72,7 +72,9 @@ def pre_model_load(self, cfg): if cfg.cut_cross_entropy: self._check_requirements() - from cut_cross_entropy.transformers import cce_patch + from axolotl.integrations.cut_cross_entropy.monkeypatch.patch import ( + cce_patch, + ) with zero_only(): LOG.info( diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py new file mode 100644 index 0000000000..5cdc53b0cc --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py @@ -0,0 +1,201 @@ +"""Cohere and Cohere2 CCE patch.""" + +# This patch is based off transformers 4.50.0. +# It patches the forward function for CohereForCausalLM and Cohere2ForCausalLM. +# It scales the hidden states by the logit scale in advance instead of the logits as the +# operation is done internally and should be mathematically equivalent. + +# pylint: disable=duplicate-code + +from types import MethodType +from typing import Optional, Tuple, Union + +import torch +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, + apply_lce, +) +from transformers.cache_utils import Cache +from transformers.modeling_outputs import CausalLMOutputWithPast +from transformers.models.cohere.modeling_cohere import ( + _CONFIG_FOR_DOC, + COHERE_INPUTS_DOCSTRING, + KwargsForCausalLM, +) +from transformers.processing_utils import Unpack +from transformers.utils import ( + add_start_docstrings_to_model_forward, + replace_return_docstrings, +) +from transformers.utils.deprecation import deprecate_kwarg + +_PATCH_OPTS: PatchOptions | None = None + + +@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") +@add_start_docstrings_to_model_forward(COHERE_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def cce_forward( + self, + input_ids: torch.LongTensor | None = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Union[Cache, list[torch.FloatTensor]]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs: Unpack[KwargsForCausalLM], +) -> Union[Tuple, CausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + + Example: + + ```python + >> from transformers import AutoTokenizer, CohereForCausalLM + + >> model = CohereForCausalLM.from_pretrained("CohereForAI/c4ai-command-r-v01") + >> tokenizer = AutoTokenizer.from_pretrained("CohereForAI/c4ai-command-r-v01") + + >> prompt = "Hey, are you conscious? Can you talk to me?" + >> inputs = tokenizer(prompt, return_tensors="pt") + + >> # Generate + >> generate_ids = model.generate(inputs.input_ids, max_length=30) + >> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." + ```""" + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs[0] + loss = None + logits = None + + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + # scale weight by logit_scale in-place of logits + loss = apply_lce( + hidden_states[:, slice_indices, :], + self.lm_head.weight * self.logit_scale, + labels, + _PATCH_OPTS, + **kwargs, + ) + else: + logits = self.lm_head(hidden_states[:, slice_indices, :]) + logits = logits * self.logit_scale # main diff from Llama + + if labels is not None: + loss = self.loss_function( + logits=logits, + labels=labels, + vocab_size=self.config.vocab_size, + **kwargs, + ) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +def patch_cohere( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + from transformers.models.cohere import modeling_cohere + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_cohere.CohereForCausalLM + ), f"Expected a CohereForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward, maybe_model) + return maybe_model + + modeling_cohere.CohereForCausalLM.forward = cce_forward + return None + + +def patch_cohere2( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + from transformers.models.cohere2 import modeling_cohere2 + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_cohere2.Cohere2ForCausalLM + ), f"Expected a Cohere2ForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward, maybe_model) + return maybe_model + + modeling_cohere2.Cohere2ForCausalLM.forward = cce_forward + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma.py new file mode 100644 index 0000000000..4c8d2261a6 --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma.py @@ -0,0 +1,175 @@ +"""Gemma CCE patch""" + +# This patch is based off transformers 4.50.0. + +# pylint: disable=duplicate-code + +from types import MethodType +from typing import Optional, Tuple, Union + +import torch +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, + apply_lce, +) +from transformers.cache_utils import Cache +from transformers.modeling_outputs import CausalLMOutputWithPast +from transformers.models.gemma.modeling_gemma import ( + _CONFIG_FOR_DOC, + GEMMA_INPUTS_DOCSTRING, + KwargsForCausalLM, +) +from transformers.processing_utils import Unpack +from transformers.utils import ( + add_start_docstrings_to_model_forward, + replace_return_docstrings, +) +from transformers.utils.deprecation import deprecate_kwarg + +_PATCH_OPTS: PatchOptions | None = None + + +@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") +@add_start_docstrings_to_model_forward(GEMMA_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def cce_forward( + self, + input_ids: torch.LongTensor | None = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Union[Cache, list[torch.FloatTensor]]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs: Unpack[KwargsForCausalLM], +) -> Union[Tuple, CausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + + Example: + + ```python + >>> from transformers import AutoTokenizer, GemmaForCausalLM + + >>> model = GemmaForCausalLM.from_pretrained("google/gemma-7b") + >>> tokenizer = AutoTokenizer.from_pretrained("google/gemma-7b") + + >>> prompt = "What is your favorite condiment?" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "What is your favorite condiment?" + ```""" + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs[0] + loss = None + logits = None + + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + loss = apply_lce( + hidden_states[:, slice_indices, :], + self.lm_head.weight, + labels, + _PATCH_OPTS, + **kwargs, + ) + else: + logits = self.lm_head(hidden_states[:, slice_indices, :]) + if labels is not None: + loss = self.loss_function( + logits=logits, + labels=labels, + vocab_size=self.config.vocab_size, + **kwargs, + ) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +def patch_gemma( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + from transformers.models.gemma import modeling_gemma + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_gemma.GemmaForCausalLM + ), f"Expected a GemmaForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward, maybe_model) + return maybe_model + + modeling_gemma.GemmaForCausalLM.forward = cce_forward + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py new file mode 100644 index 0000000000..ecbe68085f --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py @@ -0,0 +1,465 @@ +"""Gemma2 and Gemma3 (text and multimodal) CCE patch.""" + +# Implementation originally adapted from https://github.com/apple/ml-cross-entropy/pull/29 +# and updated for transformers 4.50.0. +# This is a modified version of the patch that allows for deferred logits calculation for gemma3 and works +# with both gemma3 (text and multimodal) models. + +# pylint: disable=duplicate-code + +from types import MethodType +from typing import Optional, Tuple, Union + +import torch +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, + apply_lce, +) +from torch import nn +from transformers.cache_utils import Cache, HybridCache +from transformers.modeling_outputs import CausalLMOutputWithPast +from transformers.models.gemma3.modeling_gemma3 import ( + _CONFIG_FOR_DOC, + GEMMA3_INPUTS_DOCSTRING, + Gemma3CausalLMOutputWithPast, + logger, +) +from transformers.utils import ( + add_start_docstrings_to_model_forward, + is_torchdynamo_compiling, + replace_return_docstrings, +) +from transformers.utils.deprecation import deprecate_kwarg + +_PATCH_OPTS: PatchOptions | None = None + + +@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") +@add_start_docstrings_to_model_forward(GEMMA3_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def cce_forward( + self, + input_ids: torch.LongTensor | None = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[HybridCache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + defer_logits_calculation: bool = False, + **loss_kwargs, +) -> Union[Tuple, CausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + defer_logits_calculation (`bool`, *optional*): + If `True`, defer logits calculation to the ConditionalGeneration forward. This is used to avoid the + memory overhead of calculating logits using regular lm_head forward pass and to use CCE. + + Returns: + + Example: + + ```python + >>> from transformers import AutoTokenizer, Gemma3ForCausalLM + + >>> model = Gemma3ForCausalLM.from_pretrained("google/gemma-2-9b") + >>> tokenizer = AutoTokenizer.from_pretrained("google/gemma-2-9b") + + >>> prompt = "What is your favorite condiment?" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "What is your favorite condiment?" + ```""" + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + **loss_kwargs, + ) + + hidden_states = outputs[0] + loss = None + logits = None + + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + if self.config.final_logit_softcapping is not None: + logger.warning_once( + "final_logit_softcapping is not supported for gemma3_text with CCE. Disabling." + ) + loss = apply_lce( + hidden_states[:, slice_indices, :], + self.lm_head.weight, + labels, + _PATCH_OPTS, + **loss_kwargs, + ) + elif _PATCH_OPTS is not None and defer_logits_calculation: + # defer logits calculation to the ConditionalGeneration forward + logits = hidden_states[:, slice_indices, :] + + if self.config.final_logit_softcapping is not None: + logger.warning_once( + "final_logit_softcapping is not supported for gemma3 with CCE. Disabling." + ) + else: + logits = self.lm_head(hidden_states[:, slice_indices, :]) + if self.config.final_logit_softcapping is not None: + logits = logits / self.config.final_logit_softcapping + logits = torch.tanh(logits) + logits = logits * self.config.final_logit_softcapping + + if labels is not None: + loss = self.loss_function(logits, labels, self.vocab_size, **loss_kwargs) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") +@add_start_docstrings_to_model_forward(GEMMA3_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=Gemma3CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def cce_forward_multimodal( + self, + input_ids: torch.LongTensor | None = None, + pixel_values: torch.FloatTensor | None = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Union[list[torch.FloatTensor], Cache]] = None, + token_type_ids: Optional[torch.LongTensor] = None, + cache_position: Optional[torch.LongTensor] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **lm_kwargs, +) -> Union[Tuple, Gemma3CausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.text_config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.text_config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + + Example: + + ```python + >>> from PIL import Image + >>> import requests + >>> from transformers import AutoProcessor, Gemma3ForConditionalGeneration + + >>> model = Gemma3ForConditionalGeneration.from_pretrained("google/Gemma3-test-224px-hf") + >>> processor = AutoProcessor.from_pretrained("google/Gemma3-test-224px-hf") + + >>> prompt = "answer en Where is the cow standing?" + >>> url = "https://huggingface.co/gv-hf/Gemma3-test-224px-hf/resolve/main/cow_beach_1.png" + >>> image = Image.open(requests.get(url, stream=True).raw) + + >>> inputs = processor(images=image, text=prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(**inputs, max_length=30) + >>> processor.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "answer en Where is the cow standing?\nbeach" + ```""" + + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + is_training = token_type_ids is not None and labels is not None + + # Replace image id woth PAD if the image token if OOV, to avoid index-errors + if input_ids is not None and self.config.image_token_index >= self.vocab_size: + special_image_mask = input_ids == self.config.image_token_index + llm_input_ids = input_ids.clone() + llm_input_ids[special_image_mask] = 0 + else: + llm_input_ids = input_ids # type: ignore + + if inputs_embeds is None: + inputs_embeds = self.get_input_embeddings()(llm_input_ids) + + if cache_position is None: + past_seen_tokens = ( + past_key_values.get_seq_length() if past_key_values is not None else 0 # type: ignore + ) + cache_position = torch.arange( # type: ignore + past_seen_tokens, + past_seen_tokens + inputs_embeds.shape[1], + device=inputs_embeds.device, + ) + + # Merge text and images + if pixel_values is not None: + image_features = self.get_image_features(pixel_values) + + if input_ids is None: + special_image_mask = inputs_embeds == self.get_input_embeddings()( + torch.tensor( + self.config.image_token_index, + dtype=torch.long, + device=inputs_embeds.device, + ) + ) + else: + special_image_mask = (input_ids == self.config.image_token_index).unsqueeze( + -1 + ) + special_image_mask = special_image_mask.expand_as(inputs_embeds).to( + inputs_embeds.device + ) + + if ( + not is_torchdynamo_compiling() + and inputs_embeds[special_image_mask].numel() != image_features.numel() + ): + image_tokens_in_text = (special_image_mask).sum(dim=1).sum(dim=0)[0] + raise ValueError( + f"Number of images does not match number of special image tokens in the input text. " + f"Got {image_tokens_in_text} image tokens in the text but {image_features.shape[0] * image_features.shape[1]} " + "tokens from image embeddings." + ) + image_features = image_features.to(inputs_embeds.device, inputs_embeds.dtype) + inputs_embeds = inputs_embeds.masked_scatter(special_image_mask, image_features) # type: ignore + + # mask out pad-token-ids in labels for BC + if labels is not None and self.pad_token_id in labels: + logger.warning_once( + "`labels` contains `pad_token_id` which will be masked with `config.ignore_index`. " + "You have to mask out `pad_token_id` when preparing `labels`, this behavior will be removed in v.4.46.", + ) + labels = torch.where( # type: ignore + input_ids == self.pad_token_id, self.config.ignore_index, labels + ) + + causal_mask = self._update_causal_mask( # pylint: disable=protected-access + attention_mask, + token_type_ids, + past_key_values, + cache_position, + inputs_embeds, + is_training, + ) + outputs = self.language_model( + attention_mask=causal_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + logits_to_keep=logits_to_keep, + defer_logits_calculation=True, # enable deferred logits calculation + **lm_kwargs, + ) + + hidden_states = outputs[0] + loss = None + logits = None + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + loss = apply_lce( + hidden_states, + self.language_model.lm_head.weight, + labels, + _PATCH_OPTS, + **lm_kwargs, + ) + else: + logits = hidden_states + if labels is not None: + # Upcast to float if we need to compute the loss to avoid potential precision issues + logits = logits.float() + shift_logits = logits[..., :-1, :] + shift_labels = labels[..., 1:] + if attention_mask is not None: + # we use the input attention mask to shift the logits and labels, because it is 2D. + # we also crop attn mask in case it is longer, which happens in PrefixTuning with peft + shift_attention_mask = attention_mask[:, -shift_logits.shape[1] :].to( + logits.device + ) + shift_logits = shift_logits[ + shift_attention_mask.to(logits.device) != 0 + ].contiguous() + shift_labels = shift_labels[ + shift_attention_mask.to(shift_labels.device) != 0 + ].contiguous() + else: + shift_logits = shift_logits.contiguous() + shift_labels = shift_labels.contiguous() + # Flatten the tokens + loss_fct = nn.CrossEntropyLoss() + + flat_logits = shift_logits.view(-1, self.config.text_config.vocab_size) + flat_labels = shift_labels.view(-1).to(shift_logits.device) + loss = loss_fct(flat_logits, flat_labels) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return Gemma3CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + image_hidden_states=image_features if pixel_values is not None else None, + ) + + +def patch_gemma2( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + from transformers.models.gemma2 import modeling_gemma2 + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_gemma2.Gemma2ForCausalLM + ), f"Expected a Gemma2ForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward, maybe_model) + return maybe_model + + modeling_gemma2.Gemma2ForCausalLM.forward = cce_forward + return None + + +def patch_gemma3_text( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + from transformers.models.gemma3 import modeling_gemma3 + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_gemma3.Gemma3ForCausalLM + ), f"Expected a Gemma3ForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward, maybe_model) + return maybe_model + + modeling_gemma3.Gemma3ForCausalLM.forward = cce_forward + return None + + +def patch_gemma3( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + from transformers.models.gemma3 import modeling_gemma3 + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_gemma3.Gemma3ForConditionalGeneration + ), f"Expected a Gemma3ForConditionalGeneration model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward_multimodal, maybe_model) + + # patch the causal model to enable deferred logits calculation + maybe_model.language_model.forward = MethodType( + cce_forward, maybe_model.language_model + ) + return maybe_model + + modeling_gemma3.Gemma3ForConditionalGeneration.forward = cce_forward_multimodal + # patch the causal model to enable deferred logits calculation + modeling_gemma3.Gemma3ForCausalLM.forward = cce_forward + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mistral3.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mistral3.py new file mode 100644 index 0000000000..adb65fa8f2 --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mistral3.py @@ -0,0 +1,392 @@ +"""Mistral and Mistral3 CCE patch.""" + +# pylint: disable=duplicate-code + +from types import MethodType +from typing import Optional, Tuple, Union + +import torch +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, + apply_lce, +) +from torch import nn +from transformers.cache_utils import Cache +from transformers.modeling_outputs import CausalLMOutputWithPast +from transformers.models.mistral3.modeling_mistral3 import ( + Mistral3CausalLMOutputWithPast, +) +from transformers.models.mistral.modeling_mistral import ( + _CONFIG_FOR_DOC, + MISTRAL_INPUTS_DOCSTRING, + KwargsForCausalLM, +) +from transformers.processing_utils import Unpack +from transformers.utils import ( + add_start_docstrings_to_model_forward, + is_torchdynamo_compiling, + replace_return_docstrings, +) +from transformers.utils.deprecation import deprecate_kwarg + +_PATCH_OPTS: PatchOptions | None = None + + +@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") +@add_start_docstrings_to_model_forward(MISTRAL_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def cce_forward( + self, + input_ids: torch.LongTensor | None = None, + attention_mask: Optional[torch.Tensor] | None = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Union[Cache, list[torch.FloatTensor]]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + defer_logits_calculation: bool = False, + **kwargs: Unpack[KwargsForCausalLM], +) -> Union[Tuple, CausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + defer_logits_calculation (`bool`, *optional*): + If `True`, defer logits calculation to the ConditionalGeneration forward. This is used to avoid the + memory overhead of calculating logits using regular lm_head forward pass and to use CCE. + + Returns: + + Example: + + ```python + >>> from transformers import AutoTokenizer, MistralForCausalLM + + >>> model = MistralForCausalLM.from_pretrained("meta-mistral/Mistral-2-7b-hf") + >>> tokenizer = AutoTokenizer.from_pretrained("meta-mistral/Mistral-2-7b-hf") + + >>> prompt = "Hey, are you conscious? Can you talk to me?" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." + ```""" + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs[0] + loss = None + logits = None + + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + loss = apply_lce( + hidden_states[:, slice_indices, :], + self.lm_head.weight, + labels, + _PATCH_OPTS, + **kwargs, + ) + elif _PATCH_OPTS is not None and defer_logits_calculation: + # defer logits calculation to the ConditionalGeneration forward + logits = hidden_states[:, slice_indices, :] + else: + logits = self.lm_head(hidden_states[:, slice_indices, :]) + if labels is not None: + loss = self.loss_function( + logits=logits, + labels=labels, + vocab_size=self.config.vocab_size, + **kwargs, + ) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +def cce_forward_multimodal( + self, + input_ids: torch.LongTensor | None = None, + pixel_values: torch.FloatTensor | None = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[list[torch.FloatTensor]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + vision_feature_layer: Optional[Union[int, list[int]]] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + image_sizes: torch.Tensor | None = None, + **lm_kwargs, +) -> Union[Tuple, Mistral3CausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + + Returns: + + Example: + + ```python + >>> from PIL import Image + >>> import requests + >>> from transformers import AutoProcessor, Mistral3ForConditionalGeneration + + >>> model = Mistral3ForConditionalGeneration.from_pretrained("mistralai/Mistral-Small-3.1-24B-Instruct-2503") + >>> processor = AutoProcessor.from_pretrained("mistralai/Mistral-Small-3.1-24B-Instruct-2503") + + >>> prompt = "[INST][IMG]What is the image?[/INST]" + >>> url = "http://images.cocodataset.org/val2017/000000039769.jpg" + >>> image = Image.open(requests.get(url, stream=True).raw) + + >>> inputs = processor(images=image, text=prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(**inputs, max_new_tokens=15) + >>> processor.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "What is the image?The image depicts two cats lying on a pink blanket." + ```""" + + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + vision_feature_layer = ( + vision_feature_layer + if vision_feature_layer is not None + else self.config.vision_feature_layer + ) + + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + if pixel_values is not None and inputs_embeds is not None: + raise ValueError( + "You cannot specify both pixel_values and inputs_embeds at the same time, and must specify either one" + ) + + if inputs_embeds is None: + inputs_embeds = self.get_input_embeddings()(input_ids) + + if pixel_values is not None: + image_features = self.get_image_features( + pixel_values=pixel_values, + vision_feature_layer=vision_feature_layer, + image_sizes=image_sizes, + ) + + special_image_mask = (input_ids == self.config.image_token_index).unsqueeze(-1) + special_image_mask = special_image_mask.expand_as(inputs_embeds).to( + inputs_embeds.device + ) + if ( + not is_torchdynamo_compiling() + and inputs_embeds[special_image_mask].numel() != image_features.numel() + ): + n_image_tokens = (input_ids == self.config.image_token_index).sum() + n_image_features = image_features.shape[0] * image_features.shape[1] + raise ValueError( + f"Image features and image tokens do not match: tokens: {n_image_tokens}, features {n_image_features}" + ) + image_features = image_features.to(inputs_embeds.device, inputs_embeds.dtype) + inputs_embeds = inputs_embeds.masked_scatter(special_image_mask, image_features) # type: ignore + + outputs = self.language_model( + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + logits_to_keep=logits_to_keep, + defer_logits_calculation=True, # enable deferred logits calculation + **lm_kwargs, + ) + + hidden_states = outputs[0] + loss = None + logits = None + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + loss = apply_lce( + hidden_states, + self.language_model.lm_head.weight, + labels, + _PATCH_OPTS, + **lm_kwargs, + ) + else: + logits = hidden_states + if labels is not None: + # Shift so that tokens < n predict n + if attention_mask is not None: + # we use the input attention mask to shift the logits and labels, because it is 2D. + # we also crop attn mask in case it is longer, which happens in PrefixTuning with peft + shift_attention_mask = attention_mask[:, -(logits.shape[1] - 1) :].to( + logits.device + ) + shift_logits = logits[..., :-1, :][ + shift_attention_mask.to(logits.device) != 0 + ].contiguous() + shift_labels = labels[..., 1:][ + shift_attention_mask.to(labels.device) != 0 + ].contiguous() + else: + shift_logits = logits[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + # Flatten the tokens + loss_fct = nn.CrossEntropyLoss() + loss = loss_fct( + shift_logits.view(-1, shift_logits.size(-1)), + shift_labels.view(-1).to(shift_logits.device), + ) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return Mistral3CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + image_hidden_states=image_features if pixel_values is not None else None, + ) + + +def patch_mistral( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + from transformers.models.mistral import modeling_mistral + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_mistral.MistralForCausalLM + ), f"Expected a MistralForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward, maybe_model) + return maybe_model + + modeling_mistral.MistralForCausalLM.forward = cce_forward + return None + + +def patch_mistral3( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + from transformers.models.mistral import modeling_mistral + from transformers.models.mistral3 import modeling_mistral3 + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_mistral3.Mistral3ForConditionalGeneration + ), f"Expected a Mistral3ForConditionalGeneration model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward_multimodal, maybe_model) + + # patch the causal model to enable deferred logits calculation + maybe_model.language_model.forward = MethodType( + cce_forward, maybe_model.language_model + ) + return maybe_model + + modeling_mistral3.Mistral3ForConditionalGeneration.forward = cce_forward_multimodal + # patch the causal model to enable deferred logits calculation + modeling_mistral.MistralForCausalLM.forward = cce_forward + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mllama.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mllama.py new file mode 100644 index 0000000000..850764e10e --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mllama.py @@ -0,0 +1,379 @@ +"""Mllama CCE patch.""" + +# pylint: disable=duplicate-code + +from types import MethodType +from typing import Optional, Tuple, Union + +import torch +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, + apply_lce, +) +from transformers.cache_utils import Cache +from transformers.modeling_outputs import CausalLMOutputWithPast +from transformers.models.mllama.modeling_mllama import ( + MLLAMA_INPUTS_DOCSTRING, + _prepare_cross_attention_mask, +) +from transformers.utils import ( + add_start_docstrings_to_model_forward, + replace_return_docstrings, +) +from transformers.utils.deprecation import deprecate_kwarg + +_PATCH_OPTS: PatchOptions | None = None + + +@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") +@add_start_docstrings_to_model_forward(MLLAMA_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=CausalLMOutputWithPast, config_class="MllamaTextConfig" +) +def cce_forward( + self, + input_ids: torch.LongTensor | None = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + cross_attention_states: Optional[torch.LongTensor] = None, + cross_attention_mask: Optional[torch.LongTensor] = None, + full_text_row_masked_out_mask: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, + past_key_values: Optional[Union[Cache, list[torch.FloatTensor]]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + defer_logits_calculation: bool = False, + **loss_kwargs, +) -> Union[Tuple, CausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + defer_logits_calculation (`bool`, *optional*): + If `True`, defer logits calculation to the ConditionalGeneration forward. This is used to avoid the + memory overhead of calculating logits using regular lm_head forward pass and to use CCE. + + Returns: + + Example: + + ```python + >>> from transformers import AutoTokenizer, MllamaForCausalLM + + >>> model = MllamaForCausalLM.from_pretrained("Llama-3.2-11B-Vision") + >>> tokenizer = AutoTokenizer.from_pretrained("Llama-3.2-11B-Vision") + + >>> prompt = "If I had to write a haiku, it would be:" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=40, do_sample=True, temperature=0.6) + >>> result = tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + >>> print(result) + If I had to write a haiku, it would be: "Snowflakes gently fall" - simple, yet peaceful. + I love the idea of snowflakes gently falling, each one + ``` + """ + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + cross_attention_states=cross_attention_states, + attention_mask=attention_mask, + position_ids=position_ids, + cross_attention_mask=cross_attention_mask, + full_text_row_masked_out_mask=full_text_row_masked_out_mask, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + ) + + hidden_states = outputs[0] + loss = None + logits = None + + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + loss = apply_lce( + hidden_states[:, slice_indices, :], + self.lm_head.weight, + labels, + _PATCH_OPTS, + **loss_kwargs, + ) + elif _PATCH_OPTS is not None and defer_logits_calculation: + # defer logits calculation to the ConditionalGeneration forward + logits = hidden_states[:, slice_indices, :] + else: + logits = self.lm_head(hidden_states[:, slice_indices, :]).float() + + loss = None + if labels is not None: + loss = self.loss_function(logits, labels, self.vocab_size, **loss_kwargs) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") +@add_start_docstrings_to_model_forward(MLLAMA_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=CausalLMOutputWithPast, config_class="MllamaConfig" +) +def cce_forward_multimodal( + self, + input_ids: Optional[torch.LongTensor] = None, + pixel_values: Optional[torch.FloatTensor] = None, + aspect_ratio_mask: Optional[torch.Tensor] = None, + aspect_ratio_ids: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + cross_attention_mask: Optional[torch.Tensor] = None, + cross_attention_states: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[list[torch.FloatTensor]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **loss_kwargs, +) -> Union[Tuple, CausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + + Returns: + + Example: + + ```python + >>> from PIL import Image + >>> import requests + >>> from transformers import AutoProcessor, MllamaForConditionalGeneration + + >>> checkpoint = "meta-llama/Llama-3.2-11B-Vision" + >>> model = MllamaForConditionalGeneration.from_pretrained(checkpoint) + >>> processor = AutoProcessor.from_pretrained(checkpoint) + + >>> prompt = "<|image|>If I had to write a haiku for this one" + >>> url = "https://www.ilankelman.org/stopsigns/australia.jpg" + >>> image = Image.open(requests.get(url, stream=True).raw) + + >>> inputs = processor(text=prompt, images=image, return_tensors="pt") + + >>> # Generate + >>> output = model.generate(**inputs, max_new_tokens=15) + + >>> prompt_len = inputs.input_ids.shape[-1] + >>> generated_ids = output[:, prompt_len:] + >>> generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False) + >>> print(generated_text) + [', it would be:.\\nA stop sign in Chinatown.\\n'] + ``` + """ + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + if pixel_values is not None and inputs_embeds is not None: + raise ValueError( + "You cannot specify both pixel_values and inputs_embeds at the same time, and must specify either one" + ) + + if pixel_values is not None and cross_attention_states is not None: + raise ValueError( + "`pixel_values` and `cross_attention_states` cannot be provided simultaneously" + ) + + if pixel_values is not None: + if aspect_ratio_ids is None: + raise ValueError( + "`aspect_ratio_ids` must be provided if `pixel_values` is provided" + ) + # get vision tokens from vision model + vision_outputs = self.vision_model( + pixel_values=pixel_values, + aspect_ratio_ids=aspect_ratio_ids, + aspect_ratio_mask=aspect_ratio_mask, + output_hidden_states=output_hidden_states, + output_attentions=output_attentions, + return_dict=return_dict, + ) + cross_attention_states = vision_outputs[0] + cross_attention_states = self.multi_modal_projector( + cross_attention_states + ).reshape( + -1, cross_attention_states.shape[-2], self.hidden_size # type: ignore + ) + + if cross_attention_mask is not None: + cross_attention_mask, full_text_row_masked_out_mask = ( + _prepare_cross_attention_mask( + cross_attention_mask, + num_vision_tokens=self.vision_model.num_patches, + dtype=self.dtype, + ) + ) + else: + full_text_row_masked_out_mask = None + + if cross_attention_mask is not None and cache_position is not None: + cross_attention_mask = cross_attention_mask[:, :, cache_position] + full_text_row_masked_out_mask = full_text_row_masked_out_mask[ + :, :, cache_position + ] + + outputs = self.language_model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + cross_attention_states=cross_attention_states, + cross_attention_mask=cross_attention_mask, + full_text_row_masked_out_mask=full_text_row_masked_out_mask, + past_key_values=past_key_values, + use_cache=use_cache, + inputs_embeds=inputs_embeds, + output_hidden_states=output_hidden_states, + output_attentions=output_attentions, + return_dict=return_dict, + cache_position=cache_position, + logits_to_keep=logits_to_keep, + defer_logits_calculation=True, # enable deferred logits calculation + **loss_kwargs, + ) + + hidden_states = outputs[0] + loss = None + logits = None + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + loss = apply_lce( + hidden_states, + self.language_model.lm_head.weight, + labels, + _PATCH_OPTS, + **loss_kwargs, + ) + else: + # Temporary fix to calculate the loss in main class, as the model's vocab size may be resized + logits = hidden_states + + if labels is not None: + loss = self.loss_function( + logits, labels, self.config.get_text_config().vocab_size, **loss_kwargs + ) + + if not return_dict: + return (loss,) + outputs if loss is not None else outputs + + return CausalLMOutputWithPast( + loss=loss, + logits=outputs.logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +def patch_mllama( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + + global _PATCH_OPTS # pylint: disable=global-statement + from transformers.models.mllama import modeling_mllama + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_mllama.MllamaForConditionalGeneration + ), f"Expected a MllamaForConditionalGeneration model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward_multimodal, maybe_model) + + # patch the language model + maybe_model.language_model.forward = MethodType( + cce_forward, maybe_model.language_model + ) + return maybe_model + + modeling_mllama.MllamaForConditionalGeneration.forward = cce_forward_multimodal + + # patch the causal language model + modeling_mllama.MllamaForCausalLM.forward = cce_forward + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py new file mode 100644 index 0000000000..b9c83ff02e --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py @@ -0,0 +1,85 @@ +# Copyright (C) 2024 Apple Inc. All Rights Reserved. + +"""Cut Cross Entropy patcher""" + +import transformers +from cut_cross_entropy.cce_utils import LinearCrossEntropyImpl +from cut_cross_entropy.linear_cross_entropy import LCE_IMPL_DEFAULT +from cut_cross_entropy.transformers.llama import patch_llama +from cut_cross_entropy.transformers.phi3 import patch_phi3 +from cut_cross_entropy.transformers.qwen2 import patch_qwen2 +from cut_cross_entropy.transformers.utils import PatchOptions, TransformersModelT + +from axolotl.integrations.cut_cross_entropy.monkeypatch.cohere import ( + patch_cohere, + patch_cohere2, +) +from axolotl.integrations.cut_cross_entropy.monkeypatch.gemma import patch_gemma +from axolotl.integrations.cut_cross_entropy.monkeypatch.gemma3 import ( + patch_gemma2, + patch_gemma3, + patch_gemma3_text, +) +from axolotl.integrations.cut_cross_entropy.monkeypatch.mistral3 import ( + patch_mistral, + patch_mistral3, +) +from axolotl.integrations.cut_cross_entropy.monkeypatch.mllama import patch_mllama + +CUT_CROSS_ENTROPY_MODEL_MAPPING = { + "llama": patch_llama, + "mllama": patch_mllama, + "phi3": patch_phi3, + "gemma": patch_gemma, + "gemma2": patch_gemma2, + "gemma3": patch_gemma3, + "gemma3_text": patch_gemma3_text, + "mistral": patch_mistral, + "mistral3": patch_mistral3, + "qwen2": patch_qwen2, + "cohere": patch_cohere, + "cohere2": patch_cohere2, +} + + +def cce_patch( + model_type_or_model: str | TransformersModelT | transformers.PretrainedConfig, + impl: str | LinearCrossEntropyImpl = LCE_IMPL_DEFAULT, + reduction: str = "mean", + filter_eps: float | str | None = "auto", + accum_e_fp32: bool = False, + accum_c_fp32: bool = False, + filter_e_grad: bool = True, + filter_c_grad: bool = True, + train_only: bool = False, +) -> TransformersModelT | None: + if isinstance(impl, LinearCrossEntropyImpl): + impl = impl.name.lower() + + if impl not in (v.name.lower() for v in LinearCrossEntropyImpl): + raise ValueError(f"Unknown {impl=}") + + if isinstance(model_type_or_model, transformers.PreTrainedModel): + model_type = model_type_or_model.config.model_type + elif isinstance(model_type_or_model, transformers.PretrainedConfig): + model_type = model_type_or_model.model_type + else: + model_type = model_type_or_model + + patch_options = PatchOptions( + impl=impl, + reduction=reduction, + filter_eps=filter_eps, + accum_e_fp32=accum_e_fp32, + accum_c_fp32=accum_c_fp32, + filter_e_grad=filter_e_grad, + filter_c_grad=filter_c_grad, + train_only=train_only, + ) + + if model_type in CUT_CROSS_ENTROPY_MODEL_MAPPING: + return CUT_CROSS_ENTROPY_MODEL_MAPPING[model_type]( + model_type_or_model, patch_options + ) + + raise RuntimeError(f"Unknown model type {model_type}") diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index d6d209db51..cd819fba48 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -23,6 +23,8 @@ "gemma", "gemma2", "gemma3_text", + "cohere", + "cohere2", "gemmoe", "starcoder2", "deepseek_v2", diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 9ccd2ca0c4..7dbdd0b76f 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -314,6 +314,7 @@ def save_initial_configs( tokenizer: PreTrainedTokenizer, model: PreTrainedModel, peft_config: PeftConfig | None, + processor: ProcessorMixin | None, ): """ Save initial configurations before training. @@ -341,6 +342,10 @@ def save_initial_configs( LOG.info(f"Pre-saving model config to {cfg.output_dir}...") model.config.save_pretrained(str(output_dir)) + if processor: + LOG.info(f"Pre-saving processor to {cfg.output_dir}...") + processor.save_pretrained(str(output_dir)) + def setup_model_card(cfg: DictDefault): """ @@ -408,6 +413,7 @@ def setup_model_and_trainer(cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> PeftModel | PreTrainedModel, PreTrainedTokenizer, PeftConfig | None, + ProcessorMixin | None, ]: """ Load model, tokenizer, trainer, etc. Helper function to encapsulate the full @@ -423,6 +429,7 @@ def setup_model_and_trainer(cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> - Model - Tokenizer - PEFT config + - Processor """ # Load tokenizer, processor and model model, tokenizer, peft_config, processor = setup_model_and_tokenizer(cfg) @@ -453,6 +460,7 @@ def setup_model_and_trainer(cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> model, tokenizer, peft_config, + processor, ) @@ -475,6 +483,7 @@ def train( model, tokenizer, peft_config, + processor, ) = setup_model_and_trainer(cfg, dataset_meta) # Determine if we need to resume from a checkpoint @@ -490,7 +499,7 @@ def train( ) # Save initial configs - save_initial_configs(cfg, tokenizer, model, peft_config) + save_initial_configs(cfg, tokenizer, model, peft_config, processor) # Set up signal handler for graceful termination setup_signal_handler(cfg, model, safe_serialization) diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index ce7a2bf0f7..69c84f4453 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -408,7 +408,7 @@ def test_kernel_training_integration(): ) # Load model - model, _ = load_model_and_tokenizer(cfg=cfg) + model, _, _ = load_model_and_tokenizer(cfg=cfg) # Verify correct activation function layer = model.model.model.layers[0] From e2da821e67072109cfd9d0e83bab524618099d42 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 27 Mar 2025 05:14:07 +0700 Subject: [PATCH 0488/1405] chore: minor optim changes (add apollo, improve docs, remove lion-pytorch) (#2444) * feat: add apollo-torch * chore: update optimizer list * fix: deleted accidental requirements file * fix: remove mention of deprecated lion_pytorch --- docs/config.qmd | 27 +++- requirements_env.txt | 315 ------------------------------------------- setup.py | 12 +- 3 files changed, 30 insertions(+), 324 deletions(-) delete mode 100644 requirements_env.txt diff --git a/docs/config.qmd b/docs/config.qmd index 2a79a0126e..8620ab27d8 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -506,7 +506,7 @@ lr_div_factor: # Learning rate div factor # Specify optimizer # Valid values are driven by the Transformers OptimizerNames class, see: -# https://github.com/huggingface/transformers/blob/95b374952dc27d8511541d6f5a4e22c9ec11fb24/src/transformers/training_args.py#L134 +# https://github.com/huggingface/transformers/blob/cbf924b76c03828101a34069a96d209314114fd5/src/transformers/training_args.py#L144-L189 # # Note that not all optimizers may be available in your environment, ex: 'adamw_anyprecision' is part of # torchdistx, 'adamw_bnb_8bit' is part of bnb.optim.Adam8bit, etc. When in doubt, it is recommended to start with the optimizer used @@ -516,25 +516,48 @@ lr_div_factor: # Learning rate div factor # - adamw_torch # - adamw_torch_fused # - adamw_torch_xla +# - adamw_torch_npu_fused # - adamw_apex_fused -# - adopt_adamw (an EXPERIMENTAL optimizer, only for torch version >= 2.5.1) +# - adopt_adamw (an EXPERIMENTAL optimizer, only for torch version >= 2.5.1) # - adafactor # - adamw_anyprecision +# - adamw_torch_4bit +# - ademamix # - sgd # - adagrad # - adamw_bnb_8bit +# - adamw_8bit # alias for adamw_bnb_8bit +# - ademamix_8bit # - lion_8bit # - lion_32bit # - paged_adamw_32bit # - paged_adamw_8bit +# - paged_ademamix_32bit +# - paged_ademamix_8bit # - paged_lion_32bit # - paged_lion_8bit +# - rmsprop +# - rmsprop_bnb +# - rmsprop_bnb_8bit +# - rmsprop_bnb_32bit # - galore_adamw # - galore_adamw_8bit # - galore_adafactor # - galore_adamw_layerwise # - galore_adamw_8bit_layerwise # - galore_adafactor_layerwise +# - lomo +# - adalomo +# - grokadamw +# - schedule_free_adamw +# - schedule_free_sgd +# - apollo_adamw +# - apollo_adamw_layerwise +# +# Additional custom optimizers include: +# - optimi_adamw +# - ao_adamw_8bit +# - ao_adamw_fp8 optimizer: # Dictionary of arguments to pass to the optimizer optim_args: diff --git a/requirements_env.txt b/requirements_env.txt deleted file mode 100644 index f8acbf73c2..0000000000 --- a/requirements_env.txt +++ /dev/null @@ -1,315 +0,0 @@ -accelerate==0.34.1 -addict==2.4.0 -aiofiles==23.2.1 -aiohttp==3.9.0 -aiosignal==1.3.1 -aiostream==0.5.2 -alembic==1.13.1 -annotated-types==0.6.0 -annoy==1.17.3 -ansible==6.7.0 -ansible-core==2.13.13 -ansible-vault==2.1.0 -anyio==3.7.1 -appdirs==1.4.4 -art==6.0 -asgiref==3.7.2 -async-timeout==4.0.2 -attrdict==2.0.1 -attrs==22.2.0 -awscli==1.32.75 --e git+ssh://git@github.com/OpenAccess-AI-Collective/axolotl.git@6e354682e3c1735d3f7fb9e362280c38e922260f#egg=axolotl -backoff==2.2.1 -base58==2.1.1 -beartype==0.17.2 -bitnet==0.2.1 -bitsandbytes==0.42.0 -bittensor==6.7.0 -black==23.7.0 -blinker==1.7.0 -boto3==1.34.75 -botocore==1.34.75 -cachetools==5.3.3 -cachy==0.1.1 -certifi==2023.7.22 -cffi==1.16.0 -cfgv==3.3.1 -chai-guanaco==1.2.4 -charset-normalizer==3.2.0 -cleo==0.6.8 -click==8.1.7 -cloudpickle==2.0.0 -cohere==4.11.2 -colorama==0.4.4 -coloredlogs==15.0.1 -CoLT5-attention==0.10.20 -contextlib2==21.6.0 -contourpy==1.2.0 -cryptography==41.0.3 -cycler==0.12.1 -cytoolz==0.12.3 -databricks-cli==0.18.0 -dataclasses-json==0.5.7 -datasets==2.11.0 -ddt==1.6.0 -decorator==5.1.1 -deepspeed==0.15.0 -# Editable Git install with no remote (dialogpt==0.1) --e /Users/wing/Projects/ml/dialogpt/src -dill==0.3.6 -distlib==0.3.6 -docker==7.0.0 -docker-pycreds==0.4.0 -docstring-parser==0.15 -docutils==0.16 -ecdsa==0.18.0 -einops==0.7.0 -einops-exts==0.0.4 -einx==0.1.3 -entrypoints==0.4 -eth-hash==0.6.0 -eth-keys==0.5.0 -eth-typing==4.0.0 -eth-utils==2.3.1 -evaluate==0.4.0 -exceptiongroup==1.1.1 -fastapi==0.109.2 -fastcore==1.5.29 -ffmpy==0.4.0 -filelock==3.12.2 --e git+https://github.com/NousResearch/finetuning-subnet.git@24e9407d6b4430a7ca39d344692f89ce5a97d27e#egg=finetuning_subnet -fire==0.5.0 -first==2.0.2 -flake8==7.0.0 -Flask==3.0.1 -fonttools==4.47.2 -frozendict==2.4.1 -frozenlist==1.3.3 -fschat @ git+https://github.com/lm-sys/FastChat.git@27a05b04a35510afb1d767ae7e5990cbd278f8fe -fsspec==2023.6.0 -fuzzywuzzy==0.18.0 -gitdb==4.0.10 -GitPython==3.1.31 -google-pasta==0.2.0 -gradio==4.42.0 -gradio_client==1.3.0 -greenlet==2.0.2 -grpclib==0.4.7 -gunicorn==21.2.0 -h11==0.14.0 -h2==4.1.0 -hpack==4.0.0 -httpcore==0.17.3 -httpx==0.24.1 -huggingface-hub==0.23.4 -humanfriendly==10.0 -hyperframe==6.0.1 -identify==2.5.24 -idna==3.4 -immutables==0.20 -importlib-metadata==6.7.0 -importlib-resources==6.1.1 -inflection==0.5.1 -iniconfig==2.0.0 -itsdangerous==2.1.2 -Jinja2==3.1.2 -jmespath==1.0.1 -joblib==1.3.2 -jsonlines==3.1.0 -jsonschema==2.6.0 -kiwisolver==1.4.5 -langchain==0.0.144 -Levenshtein==0.24.0 -libcst==1.1.0 -liger-kernel==0.0.0 -lion-pytorch==0.1.2 -llama-cpp-python==0.1.36 -llvmlite==0.40.1 -local-attention==1.9.0 -loguru==0.7.0 -Mako==1.3.2 -Markdown==3.5.2 -markdown-it-py==3.0.0 -markdown2==2.4.10 -MarkupSafe==2.1.2 -marshmallow==3.19.0 -marshmallow-enum==1.5.1 -matplotlib==3.8.2 -mccabe==0.7.0 -mdurl==0.1.2 -MEGABYTE-pytorch==0.0.7 --e git+https://github.com/cg123/mergekit.git@53c5f414774a0558b8d84858fb6374bc93a8f1c1#egg=mergekit -mlflow==2.10.0 -modal==0.62.77 -more-itertools==10.2.0 -mpmath==1.2.1 -msgpack==1.0.7 -msgpack-numpy-opentensor==0.5.0 -multidict==6.0.4 -multiprocess==0.70.14 -munch==2.5.0 -mypy==1.3.0 -mypy-extensions==1.0.0 -nest-asyncio==1.6.0 -netaddr==0.10.1 -networkx==3.0rc1 -nh3==0.2.14 -nodeenv==1.8.0 -nomic==2.0.2 -numba==0.57.1 -numexpr==2.8.4 -numpy==1.24.4 -oauthlib==3.2.2 -openai==0.27.4 -openapi==1.1.0 -openapi-schema-pydantic==1.2.4 -optimum==1.8.6 -orjson==3.10.7 -packaging==23.1 -pandas==2.0.0 -parameterized==0.9.0 -password-strength==0.0.3.post2 -pastel==0.1.1 -pathos==0.3.0 -pathspec==0.11.1 -pathtools==0.1.2 -peft==0.11.1 -pendulum==3.0.0 -Pillow==9.5.0 -pip-tools==1.11.0 -platformdirs==3.2.0 -pluggy==1.4.0 -poetry==0.7.1 -pox==0.3.2 -ppft==1.7.6.6 -pre-commit==3.3.2 -prettytable==3.10.0 -prompt-toolkit==3.0.39 -protobuf==3.20.2 -protobuf3-to-dict==0.1.5 -psutil==5.9.5 -psycopg==3.1.18 -PuLP==2.8.0 -py==1.11.0 -py-bip39-bindings==0.1.11 -py-cpuinfo==9.0.0 -py-ed25519-zebra-bindings==1.0.1 -py-sr25519-bindings==0.2.0 -pyarrow==11.0.0 -pyasn1==0.6.0 -pycodestyle==2.11.1 -pycparser==2.21 -pycryptodome==3.20.0 -pydantic==2.5.3 -pydantic_core==2.14.6 -pydub==0.25.1 -pyfiglet==0.8.post1 -pyflakes==3.2.0 -Pygments==2.15.1 -PyJWT==2.8.0 -pylev==1.4.0 -PyNaCl==1.5.0 -pynvml==11.5.0 -pyparsing==2.4.7 -pyrsistent==0.14.11 -pytest==8.0.2 -pytest-asyncio==0.23.4 -python-dateutil==2.8.2 -python-dotenv==1.0.1 -python-Levenshtein==0.24.0 -python-multipart==0.0.9 -pytz==2023.3 -PyYAML==6.0.1 -querystring-parser==1.2.4 -rapidfuzz==3.6.1 -regex==2023.6.3 -requests==2.31.0 -requests-toolbelt==0.8.0 -resolvelib==0.8.1 -responses==0.18.0 -retry==0.9.2 -rich==13.7.0 -rsa==4.7.2 -ruff==0.6.3 -s3transfer==0.10.1 -safetensors==0.4.5 -sagemaker==2.148.0 -scalecodec==1.2.7 -schedulefree==1.2.1 -schema==0.7.5 -scikit-learn==1.4.0 -scipy==1.9.3 -seaborn==0.13.2 -semantic-version==2.10.0 -sentencepiece==0.2.0 -sentry-sdk==1.19.1 -setproctitle==1.3.2 -shellingham==1.5.4 -shortuuid==1.0.11 -shtab==1.6.5 -sigtools==4.0.1 -six==1.16.0 -skypilot==0.4.1 -smdebug-rulesconfig==1.0.1 -smmap==5.0.0 -sniffio==1.3.0 -SQLAlchemy==1.4.47 -sqlparse==0.4.4 -starlette==0.36.3 -substrate-interface==1.5.2 -svgwrite==1.4.3 -sympy==1.11.1 -synchronicity==0.6.7 -tabulate==0.9.0 -tblib==1.7.0 -tenacity==8.2.2 -tensor-parallel==2.0.0 -termcolor==2.2.0 -text2art==0.2.0 -threadpoolctl==3.2.0 -tiktoken==0.6.0 -time-machine==2.14.1 -timm==0.9.16 -tokenizers==0.19.1 -tokenmonster==1.1.12 -toml==0.9.6 -tomli==2.0.1 -tomlkit==0.12.0 -toolz==0.12.1 -torch==2.2.0 -torchdata==0.6.1 -torchdiffeq==0.2.3 -TorchFix==0.4.0 -torchtext==0.15.2 -torchvision==0.17.0 -tqdm==4.66.2 -transformers==4.44.2 -trl==0.9.6 -typer==0.12.5 -types-certifi==2021.10.8.3 -types-requests==2.31.0.20240125 -types-setuptools==69.0.0.20240125 -types-toml==0.10.8.7 -typing==3.7.4.3 -typing-inspect==0.8.0 -typing_extensions==4.9.0 -tyro==0.5.18 -tzdata==2023.3 -unique-names-generator==1.0.2 -urllib3==2.2.2 -uvicorn==0.22.0 -vector_quantize_pytorch==1.14.1 -virtualenv==20.23.0 -voyager==2.0.2 -wandb==0.16.2 -watchfiles==0.21.0 -wavedrom==2.0.3.post3 -wcwidth==0.2.6 -websocket-client==1.7.0 -websockets==12.0 -Werkzeug==3.0.1 -wonderwords==2.2.0 -xxhash==3.2.0 -yarl==1.8.2 -zetascale==2.2.7 -zipp==3.15.0 diff --git a/setup.py b/setup.py index 8b2f1b2a59..4c3024b852 100644 --- a/setup.py +++ b/setup.py @@ -16,9 +16,7 @@ def parse_requirements(): with open("./requirements.txt", encoding="utf-8") as requirements_file: lines = [r.strip() for r in requirements_file.readlines()] for line in lines: - is_extras = ( - "deepspeed" in line or "mamba-ssm" in line or "lion-pytorch" in line - ) + is_extras = "deepspeed" in line or "mamba-ssm" in line if line.startswith("--extra-index-url"): # Handle custom index URLs _, url = line.split() @@ -135,15 +133,15 @@ def get_package_version(): "mlflow": [ "mlflow", ], - "lion-pytorch": [ - "lion-pytorch==0.1.2", - ], "galore": [ "galore_torch", ], + "apollo": [ + "apollo-torch", + ], "optimizers": [ "galore_torch", - "lion-pytorch==0.1.2", + "apollo-torch", "lomo-optim==0.1.1", "torch-optimi==0.2.1", ], From a7811ad4a084afee8f404365046c5c4d849df36c Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 27 Mar 2025 05:14:29 +0700 Subject: [PATCH 0489/1405] fix(doc): document config required to run `eval_causal_lm_metrics` (#2445) [skip ci] --- docs/config.qmd | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/config.qmd b/docs/config.qmd index 8620ab27d8..71ddcbff6e 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -466,6 +466,7 @@ auto_find_batch_size: # Optional[bool] eval_table_size: # Approximate number of predictions sent to wandb depending on batch size. Enabled above 0. Default is 0 eval_max_new_tokens: # Total number of tokens generated for predictions sent to wandb. Default is 128 +do_causal_lm_eval: # Whether to run causal language model evaluation for metrics in `eval_causal_lm_metrics`. eval_causal_lm_metrics: # HF evaluate metrics used during evaluation. Default is ["sacrebleu", "comet", "ter", "chrf", "perplexity"] profiler_steps: # enable the pytorch profiler to capture the first N steps of training to the output_dir. From 6cdcb8ddd5fca39d6f9b32285868eb4f5a869406 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 26 Mar 2025 18:14:43 -0400 Subject: [PATCH 0490/1405] Set the pytorch_cuda_alloc_conf env in the train module (#2447) --- src/axolotl/cli/train.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 6cc7c7701c..e225141b63 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -17,6 +17,7 @@ from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.integrations.base import PluginManager from axolotl.train import train +from axolotl.utils import set_pytorch_cuda_alloc_conf from axolotl.utils.config import normalize_config, resolve_dtype from axolotl.utils.dict import DictDefault @@ -33,6 +34,9 @@ def do_train(cfg: DictDefault, cli_args: TrainerCliArgs): cfg: Dictionary mapping `axolotl` config keys to values. cli_args: Training-specific CLI arguments. """ + # Enable expandable segments for cuda allocation to improve VRAM usage + set_pytorch_cuda_alloc_conf() + print_axolotl_text_art() check_accelerate_default_config() if int(os.getenv("LOCAL_RANK", "0")) == 0: From a4e430e7c4c4577e7b427765a17b93dc6fb0c704 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 26 Mar 2025 18:14:59 -0400 Subject: [PATCH 0491/1405] add override of upstream fix for multi-gpu orpo (#2440) * add override of upstream fix * override batch loss metrics for CPO/Simpo as well --- src/axolotl/core/trainers/trl.py | 148 +++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/src/axolotl/core/trainers/trl.py b/src/axolotl/core/trainers/trl.py index 1199313e8a..ebe46f11d7 100644 --- a/src/axolotl/core/trainers/trl.py +++ b/src/axolotl/core/trainers/trl.py @@ -1,5 +1,7 @@ """Module for TRL PPO trainer""" +from typing import Literal, Union + import torch from tqdm import tqdm from trl import ( @@ -79,6 +81,78 @@ class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): tag_names = ["axolotl", "orpo"] + def get_batch_loss_metrics( + self, + model, + batch: dict[str, Union[list, torch.LongTensor]], + train_eval: Literal["train", "eval"] = "train", + ): + """Compute the ORPO loss and other metrics for the given batch of inputs for train or test.""" + + # TODO remove once https://github.com/huggingface/trl/pull/3069 is included in a trl release + + metrics = {} + + forward_output = self.concatenated_forward(model, batch) + ( + policy_chosen_logps, + policy_rejected_logps, + policy_chosen_logits, + policy_rejected_logits, + policy_nll_loss, + ) = forward_output[:5] + if self.aux_loss_enabled: + aux_loss = forward_output[5] + + losses, chosen_rewards, rejected_rewards, log_odds_ratio, log_odds_chosen = ( + self.odds_ratio_loss(policy_chosen_logps, policy_rejected_logps) + ) + # full ORPO loss + loss = policy_nll_loss - losses.mean() + + reward_accuracies = (chosen_rewards > rejected_rewards).float() + + prefix = "eval_" if train_eval == "eval" else "" + metrics[f"{prefix}rewards/chosen"] = self.accelerator.gather_for_metrics( + chosen_rewards + ).mean() + metrics[f"{prefix}rewards/rejected"] = self.accelerator.gather_for_metrics( + rejected_rewards + ).mean() + metrics[f"{prefix}rewards/accuracies"] = self.accelerator.gather_for_metrics( + reward_accuracies + ).mean() + metrics[f"{prefix}rewards/margins"] = self.accelerator.gather_for_metrics( + chosen_rewards - rejected_rewards + ).mean() + metrics[f"{prefix}logps/rejected"] = ( + self.accelerator.gather_for_metrics(policy_rejected_logps).detach().mean() + ) + metrics[f"{prefix}logps/chosen"] = ( + self.accelerator.gather_for_metrics(policy_chosen_logps).detach().mean() + ) + metrics[f"{prefix}logits/rejected"] = self.accelerator.gather_for_metrics( + policy_rejected_logits.detach().mean() + ).mean() + metrics[f"{prefix}logits/chosen"] = self.accelerator.gather_for_metrics( + policy_chosen_logits.detach().mean() + ).mean() + metrics[f"{prefix}nll_loss"] = ( + self.accelerator.gather_for_metrics(policy_nll_loss).detach().mean() + ) + metrics[f"{prefix}log_odds_ratio"] = ( + self.accelerator.gather_for_metrics(log_odds_ratio).detach().mean() + ) + metrics[f"{prefix}log_odds_chosen"] = ( + self.accelerator.gather_for_metrics(log_odds_chosen).detach().mean() + ) + for k, v in metrics.items(): + metrics[k] = v.item() + if self.aux_loss_enabled: + loss += self.aux_loss_coef * aux_loss + + return loss, metrics + class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): """ @@ -95,6 +169,80 @@ class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): tag_names = ["axolotl", "cpo"] + def get_batch_loss_metrics( + self, + model, + batch: dict[str, Union[list, torch.LongTensor]], + train_eval: Literal["train", "eval"] = "train", + ): + """Compute the CPO loss and other metrics for the given batch of inputs for train or test.""" + metrics = {} + + forward_output = self.concatenated_forward(model, batch) + ( + policy_chosen_logps, + policy_rejected_logps, + policy_chosen_logits, + policy_rejected_logits, + policy_nll_loss, + ) = forward_output[:5] + if self.aux_loss_enabled: + aux_loss = forward_output[5] + + losses, chosen_rewards, rejected_rewards = self.cpo_loss( + policy_chosen_logps, + policy_rejected_logps, + ) + + loss = losses.mean() + self.cpo_alpha * policy_nll_loss + reward_accuracies = (chosen_rewards > rejected_rewards).float() + + prefix = "eval_" if train_eval == "eval" else "" + metrics[f"{prefix}rewards/chosen"] = ( + self.accelerator.gather_for_metrics(chosen_rewards).mean().item() + ) + metrics[f"{prefix}rewards/rejected"] = ( + self.accelerator.gather_for_metrics(rejected_rewards).mean().item() + ) + metrics[f"{prefix}rewards/accuracies"] = ( + self.accelerator.gather_for_metrics(reward_accuracies).mean().item() + ) + metrics[f"{prefix}rewards/margins"] = ( + self.accelerator.gather_for_metrics(chosen_rewards - rejected_rewards) + .mean() + .item() + ) + metrics[f"{prefix}logps/rejected"] = ( + self.accelerator.gather_for_metrics(policy_rejected_logps) + .detach() + .mean() + .item() + ) + metrics[f"{prefix}logps/chosen"] = ( + self.accelerator.gather_for_metrics(policy_chosen_logps) + .detach() + .mean() + .item() + ) + metrics[f"{prefix}logits/rejected"] = ( + self.accelerator.gather_for_metrics(policy_rejected_logits.detach().mean()) + .mean() + .item() + ) + metrics[f"{prefix}logits/chosen"] = ( + self.accelerator.gather_for_metrics(policy_chosen_logits.detach().mean()) + .mean() + .item() + ) + metrics[f"{prefix}nll_loss"] = ( + self.accelerator.gather_for_metrics(policy_nll_loss).detach().mean().item() + ) + + if self.aux_loss_enabled: + loss += self.aux_loss_coef * aux_loss + + return loss, metrics + class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): """ From 05f03b541a2ca4c2a62fa982b294632c574414d5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 28 Mar 2025 19:20:46 -0400 Subject: [PATCH 0492/1405] hf offline decorator for tests to workaround rate limits (#2452) [skip ci] * hf offline decorator for tests to workaround rate limits * fail quicker so we can see logs * try new cache name * limit files downloaded * phi mini predownload * offline decorator for phi tokenizer * handle meta llama 8b offline too * make sure to return fixtures if they are wrapped too * more fixes * more things offline * more offline things * fix the env var * fix the model name * handle gemma also * force reload of modules to recheck offline status * prefetch mistral too * use reset_sessions so hub picks up offline mode * more fixes * rename so it doesn't seem like a context manager * fix backoff * switch out tinyshakespeare dataset since it runs a py script to fetch data and doesn't work offline * include additional dataset * more fixes * more fixes * replace tiny shakespeaere dataset * skip some tests for now * use more robust check using snapshot download to determine if a dataset name is on the hub * typo for skip reason * use local_files_only * more fixtures * remove local only * use tiny shakespeare as pretrain dataset and streaming can't be offline even if precached * make sure fixtures aren't offline improve the offline reset try bumping version of datasets reorder reloading and setting prime a new cache run the tests now with fresh cache try with a static cache * now run all the ci again with hopefully a correct cache * skip wonky tests for now * skip wonky tests for now * handle offline mode for model card creation --- .github/workflows/tests.yml | 4 +- requirements.txt | 2 +- src/axolotl/train.py | 3 +- src/axolotl/utils/data/shared.py | 27 ++- tests/conftest.py | 189 +++++++++++++++++- tests/core/chat/test_messages.py | 4 +- tests/e2e/test_deepseekv3.py | 3 + tests/prompt_strategies/conftest.py | 16 +- tests/prompt_strategies/test_alpaca.py | 2 + .../test_chat_template_utils.py | 2 + .../test_dpo_chat_templates.py | 11 +- tests/prompt_strategies/test_dpo_chatml.py | 3 + tests/test_data.py | 2 + tests/test_datasets.py | 33 ++- tests/test_exact_deduplication.py | 11 + tests/test_packed_batch_sampler.py | 5 +- tests/test_packed_dataset.py | 2 + tests/test_packed_pretraining.py | 11 +- tests/test_prompt_tokenizers.py | 7 + tests/test_tokenizers.py | 9 +- tests/utils/__init__.py | 85 ++++++++ 21 files changed, 381 insertions(+), 50 deletions(-) create mode 100644 tests/utils/__init__.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 66d95b3d4d..4ef8f54f70 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -63,7 +63,7 @@ jobs: path: | /home/runner/.cache/huggingface/hub/datasets--* /home/runner/.cache/huggingface/hub/models--* - key: ${{ runner.os }}-hf-hub-cache-${{ hashFiles('**/conftest.py') }} + key: ${{ runner.os }}-hf-hub-cache-v2 - name: Setup Python uses: actions/setup-python@v5 @@ -137,7 +137,7 @@ jobs: path: | /home/runner/.cache/huggingface/hub/datasets--* /home/runner/.cache/huggingface/hub/models--* - key: ${{ runner.os }}-hf-hub-cache-${{ hashFiles('**/conftest.py') }} + key: ${{ runner.os }}-hf-hub-cache-v2 - name: Setup Python uses: actions/setup-python@v5 diff --git a/requirements.txt b/requirements.txt index 93618ba007..c1d2076fa7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ peft==0.15.0 transformers==4.50.0 tokenizers>=0.21.1 accelerate==1.5.2 -datasets==3.4.1 +datasets==3.5.0 deepspeed==0.16.4 trl==0.15.1 diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 7dbdd0b76f..a6040ebaa9 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -14,6 +14,7 @@ from accelerate.logging import get_logger from accelerate.utils import save_fsdp_model from datasets import Dataset +from huggingface_hub.errors import OfflineModeIsEnabled from peft import PeftConfig, PeftModel from transformers import PreTrainedModel, PreTrainedTokenizer, ProcessorMixin from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled @@ -302,7 +303,7 @@ def create_model_card(cfg: DictDefault, trainer: Trainer): model_card_kwarg["dataset_tags"] = dataset_tags trainer.create_model_card(**model_card_kwarg) - except (AttributeError, UnicodeDecodeError): + except (AttributeError, UnicodeDecodeError, OfflineModeIsEnabled): pass elif cfg.hub_model_id: # Defensively push to the hub to ensure the model card is updated diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 405057efcd..8b3a7541a2 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -6,8 +6,12 @@ from typing import Optional, Union from datasets import Dataset, DatasetDict, load_dataset, load_from_disk -from huggingface_hub import hf_hub_download -from huggingface_hub.errors import HFValidationError +from huggingface_hub import hf_hub_download, snapshot_download +from huggingface_hub.errors import ( + HFValidationError, + RepositoryNotFoundError, + RevisionNotFoundError, +) from axolotl.utils.dict import DictDefault @@ -70,20 +74,25 @@ def load_dataset_w_config( # pylint: disable=invalid-name ds: Optional[Union[Dataset, DatasetDict]] = None # pylint: disable=invalid-name ds_from_hub = False - ds_trust_remote_code = config_dataset.trust_remote_code try: # this is just a basic check to see if the path is a # valid HF dataset that's loadable - load_dataset( - config_dataset.path, - name=config_dataset.name, - streaming=True, + snapshot_download( + repo_id=config_dataset.path, + repo_type="dataset", token=use_auth_token, revision=config_dataset.revision, - trust_remote_code=ds_trust_remote_code, + ignore_patterns=["*"], ) ds_from_hub = True - except (FileNotFoundError, ConnectionError, HFValidationError, ValueError): + except ( + RepositoryNotFoundError, + RevisionNotFoundError, + FileNotFoundError, + ConnectionError, + HFValidationError, + ValueError, + ): pass ds_from_cloud = False diff --git a/tests/conftest.py b/tests/conftest.py index 75b12a036e..7a42ce4286 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ import pytest import requests from huggingface_hub import snapshot_download +from utils import disable_hf_offline def retry_on_request_exceptions(max_retries=3, delay=1): @@ -25,9 +26,11 @@ def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements except ( requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError, + requests.exceptions.HTTPError, ) as exc: if attempt < max_retries - 1: - time.sleep(delay) + wait = 2**attempt * delay # in seconds + time.sleep(wait) else: raise exc @@ -37,41 +40,47 @@ def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements @retry_on_request_exceptions(max_retries=3, delay=5) +@disable_hf_offline def snapshot_download_w_retry(*args, **kwargs): return snapshot_download(*args, **kwargs) @pytest.fixture(scope="session", autouse=True) +@disable_hf_offline def download_smollm2_135m_model(): # download the model - snapshot_download_w_retry("HuggingFaceTB/SmolLM2-135M") + snapshot_download_w_retry("HuggingFaceTB/SmolLM2-135M", repo_type="model") @pytest.fixture(scope="session", autouse=True) def download_llama_68m_random_model(): # download the model - snapshot_download_w_retry("JackFram/llama-68m") + snapshot_download_w_retry("JackFram/llama-68m", repo_type="model") @pytest.fixture(scope="session", autouse=True) +@disable_hf_offline def download_qwen_2_5_half_billion_model(): # download the model - snapshot_download_w_retry("Qwen/Qwen2.5-0.5B") + snapshot_download_w_retry("Qwen/Qwen2.5-0.5B", repo_type="model") @pytest.fixture(scope="session", autouse=True) +@disable_hf_offline def download_tatsu_lab_alpaca_dataset(): # download the dataset snapshot_download_w_retry("tatsu-lab/alpaca", repo_type="dataset") @pytest.fixture(scope="session", autouse=True) +@disable_hf_offline def download_mhenrichsen_alpaca_2k_dataset(): # download the dataset snapshot_download_w_retry("mhenrichsen/alpaca_2k_test", repo_type="dataset") @pytest.fixture(scope="session", autouse=True) +@disable_hf_offline def download_mhenrichsen_alpaca_2k_w_revision_dataset(): # download the dataset snapshot_download_w_retry( @@ -80,6 +89,7 @@ def download_mhenrichsen_alpaca_2k_w_revision_dataset(): @pytest.fixture(scope="session", autouse=True) +@disable_hf_offline def download_mlabonne_finetome_100k_dataset(): # download the dataset snapshot_download_w_retry("mlabonne/FineTome-100k", repo_type="dataset") @@ -101,6 +111,19 @@ def download_argilla_ultrafeedback_binarized_preferences_cleaned_dataset(): ) +@pytest.fixture(scope="session", autouse=True) +def download_fozzie_alpaca_dpo_dataset(): + # download the dataset + snapshot_download_w_retry( + "fozziethebeat/alpaca_messages_2k_dpo_test", repo_type="dataset" + ) + snapshot_download_w_retry( + "fozziethebeat/alpaca_messages_2k_dpo_test", + repo_type="dataset", + revision="ea82cff", + ) + + @pytest.fixture(scope="session", autouse=True) def download_arcee_ai_distilabel_intel_orca_dpo_pairs_dataset(): # download the dataset @@ -109,10 +132,135 @@ def download_arcee_ai_distilabel_intel_orca_dpo_pairs_dataset(): ) +@pytest.fixture(scope="session", autouse=True) +def download_argilla_dpo_pairs_dataset(): + # download the dataset + snapshot_download_w_retry( + "argilla/distilabel-intel-orca-dpo-pairs", repo_type="dataset" + ) + + @pytest.fixture(scope="session", autouse=True) def download_tiny_shakespeare_dataset(): # download the dataset - snapshot_download_w_retry("Trelis/tiny-shakespeare", repo_type="dataset") + snapshot_download_w_retry("winglian/tiny-shakespeare", repo_type="dataset") + + +@pytest.fixture(scope="session", autouse=True) +def download_deepseek_model_fixture(): + snapshot_download_w_retry("axolotl-ai-co/DeepSeek-V3-11M", repo_type="model") + + +@pytest.fixture(scope="session", autouse=True) +@disable_hf_offline +def download_huggyllama_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "huggyllama/llama-7b", + repo_type="model", + allow_patterns=["*token*", "config.json"], + ) + + +@pytest.fixture(scope="session", autouse=True) +@disable_hf_offline +def download_llama_1b_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "NousResearch/Llama-3.2-1B", + repo_type="model", + allow_patterns=["*token*", "config.json"], + ) + + +@pytest.fixture(scope="session", autouse=True) +@disable_hf_offline +def download_llama3_8b_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "NousResearch/Meta-Llama-3-8B", repo_type="model", allow_patterns=["*token*"] + ) + + +@pytest.fixture(scope="session", autouse=True) +@disable_hf_offline +def download_llama3_8b_instruct_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "NousResearch/Meta-Llama-3-8B-Instruct", + repo_type="model", + allow_patterns=["*token*"], + ) + + +@pytest.fixture(scope="session", autouse=True) +@disable_hf_offline +def download_phi_35_mini_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "microsoft/Phi-3.5-mini-instruct", repo_type="model", allow_patterns=["*token*"] + ) + + +@pytest.fixture(scope="session", autouse=True) +def download_phi_3_medium_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "microsoft/Phi-3-medium-128k-instruct", + repo_type="model", + allow_patterns=["*token*"], + ) + + +@pytest.fixture(scope="session", autouse=True) +def download_mistral_7b_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "casperhansen/mistral-7b-instruct-v0.1-awq", + repo_type="model", + allow_patterns=["*token*", "config.json"], + ) + + +@pytest.fixture(scope="session", autouse=True) +def download_gemma_2b_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "unsloth/gemma-2b-it", + revision="703fb4a", + repo_type="model", + allow_patterns=["*token*", "config.json"], + ) + + +@pytest.fixture(scope="session", autouse=True) +def download_gemma2_9b_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "mlx-community/gemma-2-9b-it-4bit", + repo_type="model", + allow_patterns=["*token*", "config.json"], + ) + + +@pytest.fixture(scope="session", autouse=True) +def download_mlx_mistral_7b_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "mlx-community/Mistral-7B-Instruct-v0.3-4bit", + repo_type="model", + allow_patterns=["*token*", "config.json"], + ) + + +@pytest.fixture(scope="session", autouse=True) +def download_llama2_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "NousResearch/Llama-2-7b-hf", + repo_type="model", + allow_patterns=["*token*", "config.json"], + ) @pytest.fixture @@ -178,3 +326,34 @@ def cleanup_monkeypatches(): module_globals = module_name_tuple[1] for module_global in module_globals: globals().pop(module_global, None) + + +# # pylint: disable=redefined-outer-name,unused-argument +# def test_load_fixtures( +# download_smollm2_135m_model, +# download_llama_68m_random_model, +# download_qwen_2_5_half_billion_model, +# download_tatsu_lab_alpaca_dataset, +# download_mhenrichsen_alpaca_2k_dataset, +# download_mhenrichsen_alpaca_2k_w_revision_dataset, +# download_mlabonne_finetome_100k_dataset, +# download_argilla_distilabel_capybara_dpo_7k_binarized_dataset, +# download_argilla_ultrafeedback_binarized_preferences_cleaned_dataset, +# download_fozzie_alpaca_dpo_dataset, +# download_arcee_ai_distilabel_intel_orca_dpo_pairs_dataset, +# download_argilla_dpo_pairs_dataset, +# download_tiny_shakespeare_dataset, +# download_deepseek_model_fixture, +# download_huggyllama_model_fixture, +# download_llama_1b_model_fixture, +# download_llama3_8b_model_fixture, +# download_llama3_8b_instruct_model_fixture, +# download_phi_35_mini_model_fixture, +# download_phi_3_medium_model_fixture, +# download_mistral_7b_model_fixture, +# download_gemma_2b_model_fixture, +# download_gemma2_9b_model_fixture, +# download_mlx_mistral_7b_model_fixture, +# download_llama2_model_fixture, +# ): +# pass diff --git a/tests/core/chat/test_messages.py b/tests/core/chat/test_messages.py index 6a69d74e75..bab77fbcf4 100644 --- a/tests/core/chat/test_messages.py +++ b/tests/core/chat/test_messages.py @@ -6,14 +6,16 @@ import pytest from transformers import AddedToken, AutoTokenizer +from utils import enable_hf_offline from axolotl.core.chat.format.chatml import format_message from axolotl.core.chat.messages import ChatFormattedChats, Chats @pytest.fixture(scope="session", name="llama_tokenizer") +@enable_hf_offline def llama_tokenizer_fixture(): - return AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3.1-8B") + return AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B") @pytest.fixture(scope="session", name="chatml_tokenizer") diff --git a/tests/e2e/test_deepseekv3.py b/tests/e2e/test_deepseekv3.py index f8c3d429aa..41935c6afa 100644 --- a/tests/e2e/test_deepseekv3.py +++ b/tests/e2e/test_deepseekv3.py @@ -7,6 +7,7 @@ from pathlib import Path import pytest +from utils import enable_hf_offline from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets @@ -23,6 +24,7 @@ class TestDeepseekV3: Test case for DeepseekV3 models """ + @enable_hf_offline @pytest.mark.parametrize( "sample_packing", [True, False], @@ -80,6 +82,7 @@ def test_lora_deepseekv3(self, temp_dir, sample_packing): train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.safetensors").exists() + @enable_hf_offline @pytest.mark.parametrize( "sample_packing", [True, False], diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index a7e4175160..d4d0c12f83 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -4,8 +4,8 @@ import pytest from datasets import Dataset -from huggingface_hub import hf_hub_download from transformers import AutoTokenizer +from utils import enable_hf_offline from axolotl.prompt_strategies.jinja_template_analyzer import JinjaTemplateAnalyzer from axolotl.utils.chat_templates import _CHAT_TEMPLATES @@ -108,24 +108,15 @@ def fixture_toolcalling_dataset(): @pytest.fixture(name="llama3_tokenizer", scope="session", autouse=True) +@enable_hf_offline def fixture_llama3_tokenizer(): - hf_hub_download( - repo_id="NousResearch/Meta-Llama-3-8B-Instruct", - filename="special_tokens_map.json", - ) - hf_hub_download( - repo_id="NousResearch/Meta-Llama-3-8B-Instruct", - filename="tokenizer_config.json", - ) - hf_hub_download( - repo_id="NousResearch/Meta-Llama-3-8B-Instruct", filename="tokenizer.json" - ) tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B-Instruct") return tokenizer @pytest.fixture(name="smollm2_tokenizer", scope="session", autouse=True) +@enable_hf_offline def fixture_smollm2_tokenizer(): tokenizer = AutoTokenizer.from_pretrained("HuggingFaceTB/SmolLM2-135M") return tokenizer @@ -140,6 +131,7 @@ def fixture_mistralv03_tokenizer(): @pytest.fixture(name="phi35_tokenizer", scope="session", autouse=True) +@enable_hf_offline def fixture_phi35_tokenizer(): tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3.5-mini-instruct") return tokenizer diff --git a/tests/prompt_strategies/test_alpaca.py b/tests/prompt_strategies/test_alpaca.py index 9e425e0dfb..366663c137 100644 --- a/tests/prompt_strategies/test_alpaca.py +++ b/tests/prompt_strategies/test_alpaca.py @@ -6,6 +6,7 @@ from datasets import Dataset from tokenizers import AddedToken from transformers import AutoTokenizer +from utils import enable_hf_offline from axolotl.datasets import TokenizedPromptDataset from axolotl.prompt_tokenizers import AlpacaPromptTokenizingStrategy @@ -26,6 +27,7 @@ def fixture_alpaca_dataset(): @pytest.fixture(name="tokenizer") +@enable_hf_offline def fixture_tokenizer(): # pylint: disable=all tokenizer = AutoTokenizer.from_pretrained( diff --git a/tests/prompt_strategies/test_chat_template_utils.py b/tests/prompt_strategies/test_chat_template_utils.py index 66bcb547dd..ec0c484ee5 100644 --- a/tests/prompt_strategies/test_chat_template_utils.py +++ b/tests/prompt_strategies/test_chat_template_utils.py @@ -6,6 +6,7 @@ import pytest from transformers import AutoTokenizer +from utils import enable_hf_offline from axolotl.utils.chat_templates import ( _CHAT_TEMPLATES, @@ -15,6 +16,7 @@ @pytest.fixture(name="llama3_tokenizer") +@enable_hf_offline def fixture_llama3_tokenizer(): tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B") diff --git a/tests/prompt_strategies/test_dpo_chat_templates.py b/tests/prompt_strategies/test_dpo_chat_templates.py index 740edc22f2..b8e58a8d37 100644 --- a/tests/prompt_strategies/test_dpo_chat_templates.py +++ b/tests/prompt_strategies/test_dpo_chat_templates.py @@ -7,6 +7,7 @@ import pytest from datasets import Dataset from transformers import AutoTokenizer +from utils import enable_hf_offline from axolotl.prompt_strategies.dpo.chat_template import default from axolotl.utils.dict import DictDefault @@ -78,15 +79,8 @@ def fixture_custom_assistant_dataset(): ) -@pytest.fixture(name="llama3_tokenizer") -def fixture_llama3_tokenizer(): - tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B") - tokenizer.eos_token = "<|eot_id|>" - - return tokenizer - - @pytest.fixture(name="phi3_tokenizer") +@enable_hf_offline def fixture_phi3_tokenizer(): tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-medium-128k-instruct") @@ -94,6 +88,7 @@ def fixture_phi3_tokenizer(): @pytest.fixture(name="gemma_tokenizer") +@enable_hf_offline def fixture_gemma_tokenizer(): tokenizer = AutoTokenizer.from_pretrained("unsloth/gemma-2b-it", revision="703fb4a") diff --git a/tests/prompt_strategies/test_dpo_chatml.py b/tests/prompt_strategies/test_dpo_chatml.py index 93793b2c55..1212bf4119 100644 --- a/tests/prompt_strategies/test_dpo_chatml.py +++ b/tests/prompt_strategies/test_dpo_chatml.py @@ -5,6 +5,7 @@ import unittest import pytest +from utils import enable_hf_offline from axolotl.prompt_strategies.dpo import load as load_dpo from axolotl.utils.data.rl import load_prepare_preference_datasets @@ -34,6 +35,8 @@ class TestDPOChatml: Test loading DPO preference datasets with chatml formatting """ + @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") + @enable_hf_offline def test_default(self, minimal_dpo_cfg): cfg = DictDefault( { diff --git a/tests/test_data.py b/tests/test_data.py index 141f3ed213..ddfa96b822 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -5,6 +5,7 @@ import unittest from transformers import LlamaTokenizer +from utils import enable_hf_offline from axolotl.utils.data import encode_pretraining, md5 @@ -14,6 +15,7 @@ class TestEncodePretraining(unittest.TestCase): test class for encode pretraining and md5 helper """ + @enable_hf_offline def setUp(self): self.tokenizer = LlamaTokenizer.from_pretrained("huggyllama/llama-7b") self.tokenizer.add_special_tokens( diff --git a/tests/test_datasets.py b/tests/test_datasets.py index 77c50d5586..4a64074d7a 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -7,14 +7,16 @@ import unittest from pathlib import Path -from conftest import snapshot_download_w_retry +import pytest from constants import ( ALPACA_MESSAGES_CONFIG_OG, ALPACA_MESSAGES_CONFIG_REVISION, SPECIAL_TOKENS, ) from datasets import Dataset +from huggingface_hub import snapshot_download from transformers import AutoTokenizer +from utils import enable_hf_offline from axolotl.utils.data import load_tokenized_prepared_datasets from axolotl.utils.data.rl import load_prepare_preference_datasets @@ -24,6 +26,7 @@ class TestDatasetPreparation(unittest.TestCase): """Test a configured dataloader.""" + @enable_hf_offline def setUp(self) -> None: self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") self.tokenizer.add_special_tokens(SPECIAL_TOKENS) @@ -38,6 +41,8 @@ def setUp(self) -> None: ] ) + @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") + @enable_hf_offline def test_load_hub(self): """Core use case. Verify that processing data from the hub works""" with tempfile.TemporaryDirectory() as tmp_dir: @@ -64,16 +69,21 @@ def test_load_hub(self): assert "attention_mask" in dataset.features assert "labels" in dataset.features + @enable_hf_offline + @pytest.mark.skip("datasets bug with local datasets when offline") def test_load_local_hub(self): """Niche use case. Verify that a local copy of a hub dataset can be loaded""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_path = Path(tmp_dir) / "mhenrichsen/alpaca_2k_test" tmp_ds_path.mkdir(parents=True, exist_ok=True) - snapshot_download_w_retry( + snapshot_path = snapshot_download( repo_id="mhenrichsen/alpaca_2k_test", repo_type="dataset", local_dir=tmp_ds_path, ) + # offline mode doesn't actually copy it to local_dir, so we + # have to copy all the contents in the dir manually from the returned snapshot_path + shutil.copytree(snapshot_path, tmp_ds_path, dirs_exist_ok=True) prepared_path = Path(tmp_dir) / "prepared" # Right now a local copy that doesn't fully conform to a dataset @@ -106,6 +116,7 @@ def test_load_local_hub(self): assert "labels" in dataset.features shutil.rmtree(tmp_ds_path) + @enable_hf_offline def test_load_from_save_to_disk(self): """Usual use case. Verify datasets saved via `save_to_disk` can be loaded.""" with tempfile.TemporaryDirectory() as tmp_dir: @@ -135,6 +146,7 @@ def test_load_from_save_to_disk(self): assert "attention_mask" in dataset.features assert "labels" in dataset.features + @enable_hf_offline def test_load_from_dir_of_parquet(self): """Usual use case. Verify a directory of parquet files can be loaded.""" with tempfile.TemporaryDirectory() as tmp_dir: @@ -171,6 +183,7 @@ def test_load_from_dir_of_parquet(self): assert "attention_mask" in dataset.features assert "labels" in dataset.features + @enable_hf_offline def test_load_from_dir_of_json(self): """Standard use case. Verify a directory of json files can be loaded.""" with tempfile.TemporaryDirectory() as tmp_dir: @@ -207,6 +220,7 @@ def test_load_from_dir_of_json(self): assert "attention_mask" in dataset.features assert "labels" in dataset.features + @enable_hf_offline def test_load_from_single_parquet(self): """Standard use case. Verify a single parquet file can be loaded.""" with tempfile.TemporaryDirectory() as tmp_dir: @@ -237,6 +251,7 @@ def test_load_from_single_parquet(self): assert "attention_mask" in dataset.features assert "labels" in dataset.features + @enable_hf_offline def test_load_from_single_json(self): """Standard use case. Verify a single json file can be loaded.""" with tempfile.TemporaryDirectory() as tmp_dir: @@ -267,6 +282,8 @@ def test_load_from_single_json(self): assert "attention_mask" in dataset.features assert "labels" in dataset.features + @pytest.mark.skip(reason="TODO: fix hf offline mode for CI rate limits") + @enable_hf_offline def test_load_hub_with_dpo(self): """Verify that processing dpo data from the hub works""" @@ -285,6 +302,8 @@ def test_load_hub_with_dpo(self): assert len(train_dataset) == 1800 assert "conversation" in train_dataset.features + @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") + @enable_hf_offline def test_load_hub_with_revision(self): """Verify that processing data from the hub works with a specific revision""" with tempfile.TemporaryDirectory() as tmp_dir: @@ -316,6 +335,7 @@ def test_load_hub_with_revision(self): assert "attention_mask" in dataset.features assert "labels" in dataset.features + @enable_hf_offline def test_load_hub_with_revision_with_dpo(self): """Verify that processing dpo data from the hub works with a specific revision""" @@ -334,17 +354,20 @@ def test_load_hub_with_revision_with_dpo(self): assert len(train_dataset) == 1800 assert "conversation" in train_dataset.features + @enable_hf_offline + @pytest.mark.skip("datasets bug with local datasets when offline") def test_load_local_hub_with_revision(self): """Verify that a local copy of a hub dataset can be loaded with a specific revision""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_path = Path(tmp_dir) / "mhenrichsen/alpaca_2k_test" tmp_ds_path.mkdir(parents=True, exist_ok=True) - snapshot_download_w_retry( + snapshot_path = snapshot_download( repo_id="mhenrichsen/alpaca_2k_test", repo_type="dataset", local_dir=tmp_ds_path, revision="d05c1cb", ) + shutil.copytree(snapshot_path, tmp_ds_path, dirs_exist_ok=True) prepared_path = Path(tmp_dir) / "prepared" cfg = DictDefault( @@ -375,17 +398,19 @@ def test_load_local_hub_with_revision(self): assert "labels" in dataset.features shutil.rmtree(tmp_ds_path) + @enable_hf_offline def test_loading_local_dataset_folder(self): """Verify that a dataset downloaded to a local folder can be loaded""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_path = Path(tmp_dir) / "mhenrichsen/alpaca_2k_test" tmp_ds_path.mkdir(parents=True, exist_ok=True) - snapshot_download_w_retry( + snapshot_path = snapshot_download( repo_id="mhenrichsen/alpaca_2k_test", repo_type="dataset", local_dir=tmp_ds_path, ) + shutil.copytree(snapshot_path, tmp_ds_path, dirs_exist_ok=True) prepared_path = Path(tmp_dir) / "prepared" cfg = DictDefault( diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index d32eb3953a..865ff030cf 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -8,9 +8,11 @@ import unittest from unittest.mock import patch +import pytest from constants import ALPACA_MESSAGES_CONFIG_REVISION, SPECIAL_TOKENS from datasets import Dataset from transformers import AutoTokenizer +from utils import enable_hf_offline from axolotl.utils.config import normalize_config from axolotl.utils.data import prepare_dataset @@ -234,6 +236,8 @@ def setUp(self) -> None: } ) + @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") + @enable_hf_offline def test_load_with_deduplication(self): """Verify that loading with deduplication removes duplicates.""" @@ -258,6 +262,7 @@ def test_load_without_deduplication(self): class TestDeduplicateNonRL(unittest.TestCase): """Test prepare_dataset function with different configurations.""" + @enable_hf_offline def setUp(self) -> None: self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") self.tokenizer.add_special_tokens(SPECIAL_TOKENS) @@ -286,6 +291,8 @@ def setUp(self) -> None: ) normalize_config(self.cfg_1) + @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") + @enable_hf_offline def test_prepare_dataset_with_deduplication_train(self): """Verify that prepare_dataset function processes the dataset correctly with deduplication.""" self.cfg_1.dataset_exact_deduplication = True @@ -311,6 +318,8 @@ def test_prepare_dataset_with_deduplication_train(self): "Train dataset should have 2000 samples after deduplication.", ) + @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") + @enable_hf_offline def test_prepare_dataset_with_deduplication_eval(self): """Verify that prepare_dataset function processes the dataset correctly with deduplication.""" self.cfg_1.dataset_exact_deduplication = True @@ -336,6 +345,8 @@ def test_prepare_dataset_with_deduplication_eval(self): "Eval dataset should have 2000 samples after deduplication.", ) + @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") + @enable_hf_offline def test_prepare_dataset_without_deduplication(self): """Verify that prepare_dataset function processes the dataset correctly without deduplication.""" self.cfg_1.dataset_exact_deduplication = False diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index 55a0afaece..7964d1e326 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -4,6 +4,7 @@ from datasets import concatenate_datasets, load_dataset from torch.utils.data import DataLoader, RandomSampler from transformers import AutoTokenizer +from utils import enable_hf_offline from axolotl.datasets import TokenizedPromptDataset from axolotl.prompt_strategies.completion import load @@ -25,6 +26,7 @@ class TestBatchedSamplerPacking: Test class for packing streaming dataset sequences """ + @pytest.mark.skip(reason="TODO: fix hf offline mode for CI rate limits") @pytest.mark.parametrize( "batch_size, num_workers", [ @@ -35,11 +37,12 @@ class TestBatchedSamplerPacking: ], ) @pytest.mark.parametrize("max_seq_length", [4096, 512]) + @enable_hf_offline def test_packing(self, batch_size, num_workers, tokenizer, max_seq_length): import axolotl.monkeypatch.data.batch_dataset_fetcher # pylint: disable=unused-import # noqa: F401 dataset = load_dataset( - "Trelis/tiny-shakespeare", + "winglian/tiny-shakespeare", split="train", ) diff --git a/tests/test_packed_dataset.py b/tests/test_packed_dataset.py index da8fb7a937..47b429384d 100644 --- a/tests/test_packed_dataset.py +++ b/tests/test_packed_dataset.py @@ -5,6 +5,7 @@ from datasets import Dataset, load_dataset from transformers import AutoTokenizer +from utils import enable_hf_offline from axolotl.datasets import ConstantLengthDataset, TokenizedPromptDataset from axolotl.prompt_tokenizers import AlpacaPromptTokenizingStrategy @@ -16,6 +17,7 @@ class TestPacking(unittest.TestCase): Test class for packing dataset sequences """ + @enable_hf_offline def setUp(self) -> None: # pylint: disable=duplicate-code self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") diff --git a/tests/test_packed_pretraining.py b/tests/test_packed_pretraining.py index 71c9a6861d..bd8d81dcc1 100644 --- a/tests/test_packed_pretraining.py +++ b/tests/test_packed_pretraining.py @@ -8,6 +8,7 @@ from datasets import load_dataset from torch.utils.data import DataLoader from transformers import AutoTokenizer +from utils import disable_hf_offline, enable_hf_offline from axolotl.utils.data import get_dataset_wrapper, wrap_pretraining_dataset from axolotl.utils.dict import DictDefault @@ -18,17 +19,18 @@ class TestPretrainingPacking(unittest.TestCase): Test class for packing streaming dataset sequences """ + @enable_hf_offline def setUp(self) -> None: # pylint: disable=duplicate-code self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") self.tokenizer.pad_token = "" - @pytest.mark.flaky(retries=3, delay=5) + @pytest.mark.flaky(retries=1, delay=5) + @disable_hf_offline def test_packing_stream_dataset(self): # pylint: disable=duplicate-code dataset = load_dataset( - "allenai/c4", - "en", + "winglian/tiny-shakespeare", streaming=True, )["train"] @@ -36,8 +38,7 @@ def test_packing_stream_dataset(self): { "pretraining_dataset": [ { - "path": "allenai/c4", - "name": "en", + "path": "winglian/tiny-shakespeare", "type": "pretrain", } ], diff --git a/tests/test_prompt_tokenizers.py b/tests/test_prompt_tokenizers.py index c085df4630..ab33502346 100644 --- a/tests/test_prompt_tokenizers.py +++ b/tests/test_prompt_tokenizers.py @@ -5,8 +5,10 @@ import unittest from pathlib import Path +import pytest from datasets import load_dataset from transformers import AddedToken, AutoTokenizer, LlamaTokenizer +from utils import enable_hf_offline from axolotl.prompt_strategies.alpaca_chat import NoSystemPrompter from axolotl.prompt_strategies.alpaca_w_system import ( @@ -63,6 +65,7 @@ class TestPromptTokenizationStrategies(unittest.TestCase): Test class for prompt tokenization strategies. """ + @enable_hf_offline def setUp(self) -> None: # pylint: disable=duplicate-code self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") @@ -119,6 +122,7 @@ class InstructionWSystemPromptTokenizingStrategyTest(unittest.TestCase): Test class for prompt tokenization strategies with sys prompt from the dataset """ + @enable_hf_offline def setUp(self) -> None: # pylint: disable=duplicate-code self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") @@ -160,6 +164,7 @@ class Llama2ChatTokenizationTest(unittest.TestCase): Test class for prompt tokenization strategies with sys prompt from the dataset """ + @enable_hf_offline def setUp(self) -> None: # pylint: disable=duplicate-code self.tokenizer = LlamaTokenizer.from_pretrained("NousResearch/Llama-2-7b-hf") @@ -238,6 +243,7 @@ def compare_with_transformers_integration(self): class OrpoTokenizationTest(unittest.TestCase): """test case for the ORPO tokenization""" + @enable_hf_offline def setUp(self) -> None: # pylint: disable=duplicate-code tokenizer = LlamaTokenizer.from_pretrained( @@ -262,6 +268,7 @@ def setUp(self) -> None: "argilla/ultrafeedback-binarized-preferences-cleaned", split="train" ).select([0]) + @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") def test_orpo_integration(self): strat = load( self.tokenizer, diff --git a/tests/test_tokenizers.py b/tests/test_tokenizers.py index 3d568ab19f..6e612e7e80 100644 --- a/tests/test_tokenizers.py +++ b/tests/test_tokenizers.py @@ -5,6 +5,7 @@ import unittest import pytest +from utils import enable_hf_offline from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_tokenizer @@ -15,6 +16,7 @@ class TestTokenizers: test class for the load_tokenizer fn """ + @enable_hf_offline def test_default_use_fast(self): cfg = DictDefault( { @@ -24,6 +26,7 @@ def test_default_use_fast(self): tokenizer = load_tokenizer(cfg) assert "Fast" in tokenizer.__class__.__name__ + @enable_hf_offline def test_dont_use_fast(self): cfg = DictDefault( { @@ -34,6 +37,7 @@ def test_dont_use_fast(self): tokenizer = load_tokenizer(cfg) assert "Fast" not in tokenizer.__class__.__name__ + @enable_hf_offline def test_special_tokens_modules_to_save(self): # setting special_tokens to new token cfg = DictDefault( @@ -68,6 +72,7 @@ def test_special_tokens_modules_to_save(self): ) load_tokenizer(cfg) + @enable_hf_offline def test_add_additional_special_tokens(self): cfg = DictDefault( { @@ -83,6 +88,7 @@ def test_add_additional_special_tokens(self): tokenizer = load_tokenizer(cfg) assert len(tokenizer) == 32001 + @enable_hf_offline def test_added_tokens_overrides(self, temp_dir): cfg = DictDefault( { @@ -104,11 +110,12 @@ def test_added_tokens_overrides(self, temp_dir): 128042 ] + @enable_hf_offline def test_added_tokens_overrides_with_toolargeid(self, temp_dir): cfg = DictDefault( { # use with tokenizer that has reserved_tokens in added_tokens - "tokenizer_config": "NousResearch/Llama-3.2-1B", + "tokenizer_config": "HuggingFaceTB/SmolLM2-135M", "added_tokens_overrides": {1000000: "BROKEN_RANDOM_OVERRIDE_1"}, "output_dir": temp_dir, } diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000000..bc49206711 --- /dev/null +++ b/tests/utils/__init__.py @@ -0,0 +1,85 @@ +""" +test utils for helpers and decorators +""" + +import os +from functools import wraps + +from huggingface_hub.utils import reset_sessions + + +def reload_modules(hf_hub_offline): + # Force reload of the modules that check this variable + import importlib + + import datasets + import huggingface_hub.constants + + # Reload the constants module first, as others depend on it + importlib.reload(huggingface_hub.constants) + huggingface_hub.constants.HF_HUB_OFFLINE = hf_hub_offline + importlib.reload(datasets.config) + datasets.config.HF_HUB_OFFLINE = hf_hub_offline + reset_sessions() + + +def enable_hf_offline(test_func): + """ + test decorator that sets HF_HUB_OFFLINE environment variable to True and restores it after the test even if the test fails. + :param test_func: + :return: + """ + + @wraps(test_func) + def wrapper(*args, **kwargs): + # Save the original value of HF_HUB_OFFLINE environment variable + original_hf_offline = os.getenv("HF_HUB_OFFLINE") + + # Set HF_OFFLINE environment variable to True + os.environ["HF_HUB_OFFLINE"] = "1" + + reload_modules(True) + try: + # Run the test function + return test_func(*args, **kwargs) + finally: + # Restore the original value of HF_HUB_OFFLINE environment variable + if original_hf_offline is not None: + os.environ["HF_HUB_OFFLINE"] = original_hf_offline + reload_modules(bool(original_hf_offline)) + else: + del os.environ["HF_HUB_OFFLINE"] + reload_modules(False) + + return wrapper + + +def disable_hf_offline(test_func): + """ + test decorator that sets HF_HUB_OFFLINE environment variable to False and restores it after the wrapped func + :param test_func: + :return: + """ + + @wraps(test_func) + def wrapper(*args, **kwargs): + # Save the original value of HF_HUB_OFFLINE environment variable + original_hf_offline = os.getenv("HF_HUB_OFFLINE") + + # Set HF_OFFLINE environment variable to True + os.environ["HF_HUB_OFFLINE"] = "0" + + reload_modules(False) + try: + # Run the test function + return test_func(*args, **kwargs) + finally: + # Restore the original value of HF_HUB_OFFLINE environment variable + if original_hf_offline is not None: + os.environ["HF_HUB_OFFLINE"] = original_hf_offline + reload_modules(bool(original_hf_offline)) + else: + del os.environ["HF_HUB_OFFLINE"] + reload_modules(False) + + return wrapper From e46239f8d3a69e307d7a6fc754fffcac7dfbc8bb Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 28 Mar 2025 19:21:03 -0400 Subject: [PATCH 0493/1405] bump liger to 0.5.5 (#2448) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c1d2076fa7..096237f192 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 autoawq==0.2.7.post3 -liger-kernel==0.5.3 +liger-kernel==0.5.5 # END section packaging==23.2 From c49682132b44613d35e224ecbcb554db07dda85a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 28 Mar 2025 23:39:09 -0400 Subject: [PATCH 0494/1405] use offline for precached stream dataset (#2453) --- .github/workflows/tests.yml | 3 + tests/conftest.py | 44 +++++-- tests/prompt_strategies/conftest.py | 9 +- .../test_chat_templates_advanced.py | 2 + tests/test_datasets.py | 113 ++++++++---------- tests/test_exact_deduplication.py | 79 ++++++++---- tests/test_packed_pretraining.py | 55 +++++---- tests/utils/__init__.py | 2 +- 8 files changed, 181 insertions(+), 126 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4ef8f54f70..632731a2d0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -171,6 +171,9 @@ jobs: run: | axolotl --help + - name: Show HF cache + run: huggingface-cli scan-cache + - name: Run tests run: | pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ diff --git a/tests/conftest.py b/tests/conftest.py index 7a42ce4286..8cf0832901 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,8 +11,10 @@ import pytest import requests +from datasets import load_dataset from huggingface_hub import snapshot_download -from utils import disable_hf_offline +from transformers import AutoTokenizer +from utils import disable_hf_offline, enable_hf_offline def retry_on_request_exceptions(max_retries=3, delay=1): @@ -46,7 +48,6 @@ def snapshot_download_w_retry(*args, **kwargs): @pytest.fixture(scope="session", autouse=True) -@disable_hf_offline def download_smollm2_135m_model(): # download the model snapshot_download_w_retry("HuggingFaceTB/SmolLM2-135M", repo_type="model") @@ -59,28 +60,24 @@ def download_llama_68m_random_model(): @pytest.fixture(scope="session", autouse=True) -@disable_hf_offline def download_qwen_2_5_half_billion_model(): # download the model snapshot_download_w_retry("Qwen/Qwen2.5-0.5B", repo_type="model") @pytest.fixture(scope="session", autouse=True) -@disable_hf_offline def download_tatsu_lab_alpaca_dataset(): # download the dataset snapshot_download_w_retry("tatsu-lab/alpaca", repo_type="dataset") @pytest.fixture(scope="session", autouse=True) -@disable_hf_offline def download_mhenrichsen_alpaca_2k_dataset(): # download the dataset snapshot_download_w_retry("mhenrichsen/alpaca_2k_test", repo_type="dataset") @pytest.fixture(scope="session", autouse=True) -@disable_hf_offline def download_mhenrichsen_alpaca_2k_w_revision_dataset(): # download the dataset snapshot_download_w_retry( @@ -89,7 +86,6 @@ def download_mhenrichsen_alpaca_2k_w_revision_dataset(): @pytest.fixture(scope="session", autouse=True) -@disable_hf_offline def download_mlabonne_finetome_100k_dataset(): # download the dataset snapshot_download_w_retry("mlabonne/FineTome-100k", repo_type="dataset") @@ -124,6 +120,24 @@ def download_fozzie_alpaca_dpo_dataset(): ) +@pytest.fixture(scope="session") +@disable_hf_offline +def dataset_fozzie_alpaca_dpo_dataset( + download_fozzie_alpaca_dpo_dataset, +): # pylint: disable=unused-argument,redefined-outer-name + return load_dataset("fozziethebeat/alpaca_messages_2k_dpo_test", split="train") + + +@pytest.fixture(scope="session") +@disable_hf_offline +def dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff( + download_fozzie_alpaca_dpo_dataset, +): # pylint: disable=unused-argument,redefined-outer-name + return load_dataset( + "fozziethebeat/alpaca_messages_2k_dpo_test", split="train", revision="ea82cff" + ) + + @pytest.fixture(scope="session", autouse=True) def download_arcee_ai_distilabel_intel_orca_dpo_pairs_dataset(): # download the dataset @@ -152,7 +166,6 @@ def download_deepseek_model_fixture(): @pytest.fixture(scope="session", autouse=True) -@disable_hf_offline def download_huggyllama_model_fixture(): # download the tokenizer only snapshot_download_w_retry( @@ -163,7 +176,6 @@ def download_huggyllama_model_fixture(): @pytest.fixture(scope="session", autouse=True) -@disable_hf_offline def download_llama_1b_model_fixture(): # download the tokenizer only snapshot_download_w_retry( @@ -174,7 +186,6 @@ def download_llama_1b_model_fixture(): @pytest.fixture(scope="session", autouse=True) -@disable_hf_offline def download_llama3_8b_model_fixture(): # download the tokenizer only snapshot_download_w_retry( @@ -183,7 +194,6 @@ def download_llama3_8b_model_fixture(): @pytest.fixture(scope="session", autouse=True) -@disable_hf_offline def download_llama3_8b_instruct_model_fixture(): # download the tokenizer only snapshot_download_w_retry( @@ -194,7 +204,6 @@ def download_llama3_8b_instruct_model_fixture(): @pytest.fixture(scope="session", autouse=True) -@disable_hf_offline def download_phi_35_mini_model_fixture(): # download the tokenizer only snapshot_download_w_retry( @@ -263,6 +272,17 @@ def download_llama2_model_fixture(): ) +@pytest.fixture(scope="session", autouse=True) +@enable_hf_offline +def tokenizer_huggyllama( + download_huggyllama_model_fixture, +): # pylint: disable=unused-argument,redefined-outer-name + tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") + tokenizer.pad_token = "
" + + return tokenizer + + @pytest.fixture def temp_dir(): # Create a temporary directory diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index d4d0c12f83..44914e617e 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -109,7 +109,9 @@ def fixture_toolcalling_dataset(): @pytest.fixture(name="llama3_tokenizer", scope="session", autouse=True) @enable_hf_offline -def fixture_llama3_tokenizer(): +def fixture_llama3_tokenizer( + download_llama3_8b_instruct_model_fixture, +): # pylint: disable=unused-argument,redefined-outer-name tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B-Instruct") return tokenizer @@ -123,7 +125,10 @@ def fixture_smollm2_tokenizer(): @pytest.fixture(name="mistralv03_tokenizer", scope="session", autouse=True) -def fixture_mistralv03_tokenizer(): +@enable_hf_offline +def fixture_mistralv03_tokenizer( + download_mlx_mistral_7b_model_fixture, +): # pylint: disable=unused-argument,redefined-outer-name tokenizer = AutoTokenizer.from_pretrained( "mlx-community/Mistral-7B-Instruct-v0.3-4bit" ) diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index 69031bd659..f316e6ec3a 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -9,6 +9,7 @@ from datasets import Dataset from tokenizers import AddedToken from transformers import PreTrainedTokenizer +from utils import enable_hf_offline from axolotl.prompt_strategies.chat_template import ( ChatTemplatePrompter, @@ -101,6 +102,7 @@ def _should_skip_turn(self, tokenizer, turn, turn_idx, start_idx, end_idx): return True return False + @enable_hf_offline def test_train_on_inputs_true( self, tokenizer, diff --git a/tests/test_datasets.py b/tests/test_datasets.py index 4a64074d7a..71d2854970 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -4,8 +4,8 @@ import shutil import tempfile -import unittest from pathlib import Path +from unittest.mock import patch import pytest from constants import ( @@ -15,7 +15,7 @@ ) from datasets import Dataset from huggingface_hub import snapshot_download -from transformers import AutoTokenizer +from transformers import PreTrainedTokenizer from utils import enable_hf_offline from axolotl.utils.data import load_tokenized_prepared_datasets @@ -23,15 +23,17 @@ from axolotl.utils.dict import DictDefault -class TestDatasetPreparation(unittest.TestCase): +class TestDatasetPreparation: """Test a configured dataloader.""" - @enable_hf_offline - def setUp(self) -> None: - self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") - self.tokenizer.add_special_tokens(SPECIAL_TOKENS) - # Alpaca dataset. - self.dataset = Dataset.from_list( + @pytest.fixture + def tokenizer(self, tokenizer_huggyllama) -> PreTrainedTokenizer: + tokenizer_huggyllama.add_special_tokens(SPECIAL_TOKENS) + yield tokenizer_huggyllama + + @pytest.fixture + def dataset_fixture(self): + yield Dataset.from_list( [ { "instruction": "Evaluate this sentence for spelling and grammar mistakes", @@ -43,7 +45,7 @@ def setUp(self) -> None: @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") @enable_hf_offline - def test_load_hub(self): + def test_load_hub(self, tokenizer): """Core use case. Verify that processing data from the hub works""" with tempfile.TemporaryDirectory() as tmp_dir: prepared_path = Path(tmp_dir) / "prepared" @@ -60,9 +62,7 @@ def test_load_hub(self): } ) - dataset, _ = load_tokenized_prepared_datasets( - self.tokenizer, cfg, prepared_path - ) + dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) assert len(dataset) == 2000 assert "input_ids" in dataset.features @@ -71,7 +71,7 @@ def test_load_hub(self): @enable_hf_offline @pytest.mark.skip("datasets bug with local datasets when offline") - def test_load_local_hub(self): + def test_load_local_hub(self, tokenizer): """Niche use case. Verify that a local copy of a hub dataset can be loaded""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_path = Path(tmp_dir) / "mhenrichsen/alpaca_2k_test" @@ -106,9 +106,7 @@ def test_load_local_hub(self): } ) - dataset, _ = load_tokenized_prepared_datasets( - self.tokenizer, cfg, prepared_path - ) + dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) assert len(dataset) == 2000 assert "input_ids" in dataset.features @@ -117,11 +115,11 @@ def test_load_local_hub(self): shutil.rmtree(tmp_ds_path) @enable_hf_offline - def test_load_from_save_to_disk(self): + def test_load_from_save_to_disk(self, tokenizer, dataset_fixture): """Usual use case. Verify datasets saved via `save_to_disk` can be loaded.""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_name = Path(tmp_dir) / "tmp_dataset" - self.dataset.save_to_disk(str(tmp_ds_name)) + dataset_fixture.save_to_disk(str(tmp_ds_name)) prepared_path = Path(tmp_dir) / "prepared" cfg = DictDefault( @@ -137,9 +135,7 @@ def test_load_from_save_to_disk(self): } ) - dataset, _ = load_tokenized_prepared_datasets( - self.tokenizer, cfg, prepared_path - ) + dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) assert len(dataset) == 1 assert "input_ids" in dataset.features @@ -147,13 +143,13 @@ def test_load_from_save_to_disk(self): assert "labels" in dataset.features @enable_hf_offline - def test_load_from_dir_of_parquet(self): + def test_load_from_dir_of_parquet(self, tokenizer, dataset_fixture): """Usual use case. Verify a directory of parquet files can be loaded.""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_dir = Path(tmp_dir) / "tmp_dataset" tmp_ds_dir.mkdir() tmp_ds_path = tmp_ds_dir / "shard1.parquet" - self.dataset.to_parquet(tmp_ds_path) + dataset_fixture.to_parquet(tmp_ds_path) prepared_path: Path = Path(tmp_dir) / "prepared" cfg = DictDefault( @@ -174,9 +170,7 @@ def test_load_from_dir_of_parquet(self): } ) - dataset, _ = load_tokenized_prepared_datasets( - self.tokenizer, cfg, prepared_path - ) + dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) assert len(dataset) == 1 assert "input_ids" in dataset.features @@ -184,13 +178,13 @@ def test_load_from_dir_of_parquet(self): assert "labels" in dataset.features @enable_hf_offline - def test_load_from_dir_of_json(self): + def test_load_from_dir_of_json(self, tokenizer, dataset_fixture): """Standard use case. Verify a directory of json files can be loaded.""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_dir = Path(tmp_dir) / "tmp_dataset" tmp_ds_dir.mkdir() tmp_ds_path = tmp_ds_dir / "shard1.json" - self.dataset.to_json(tmp_ds_path) + dataset_fixture.to_json(tmp_ds_path) prepared_path: Path = Path(tmp_dir) / "prepared" cfg = DictDefault( @@ -211,9 +205,7 @@ def test_load_from_dir_of_json(self): } ) - dataset, _ = load_tokenized_prepared_datasets( - self.tokenizer, cfg, prepared_path - ) + dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) assert len(dataset) == 1 assert "input_ids" in dataset.features @@ -221,11 +213,11 @@ def test_load_from_dir_of_json(self): assert "labels" in dataset.features @enable_hf_offline - def test_load_from_single_parquet(self): + def test_load_from_single_parquet(self, tokenizer, dataset_fixture): """Standard use case. Verify a single parquet file can be loaded.""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_path = Path(tmp_dir) / "tmp_dataset.parquet" - self.dataset.to_parquet(tmp_ds_path) + dataset_fixture.to_parquet(tmp_ds_path) prepared_path: Path = Path(tmp_dir) / "prepared" cfg = DictDefault( @@ -242,9 +234,7 @@ def test_load_from_single_parquet(self): } ) - dataset, _ = load_tokenized_prepared_datasets( - self.tokenizer, cfg, prepared_path - ) + dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) assert len(dataset) == 1 assert "input_ids" in dataset.features @@ -252,11 +242,11 @@ def test_load_from_single_parquet(self): assert "labels" in dataset.features @enable_hf_offline - def test_load_from_single_json(self): + def test_load_from_single_json(self, tokenizer, dataset_fixture): """Standard use case. Verify a single json file can be loaded.""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_path = Path(tmp_dir) / "tmp_dataset.json" - self.dataset.to_json(tmp_ds_path) + dataset_fixture.to_json(tmp_ds_path) prepared_path: Path = Path(tmp_dir) / "prepared" cfg = DictDefault( @@ -273,9 +263,7 @@ def test_load_from_single_json(self): } ) - dataset, _ = load_tokenized_prepared_datasets( - self.tokenizer, cfg, prepared_path - ) + dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) assert len(dataset) == 1 assert "input_ids" in dataset.features @@ -304,7 +292,7 @@ def test_load_hub_with_dpo(self): @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") @enable_hf_offline - def test_load_hub_with_revision(self): + def test_load_hub_with_revision(self, tokenizer): """Verify that processing data from the hub works with a specific revision""" with tempfile.TemporaryDirectory() as tmp_dir: prepared_path = Path(tmp_dir) / "prepared" @@ -326,9 +314,7 @@ def test_load_hub_with_revision(self): } ) - dataset, _ = load_tokenized_prepared_datasets( - self.tokenizer, cfg, prepared_path - ) + dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) assert len(dataset) == 2000 assert "input_ids" in dataset.features @@ -336,7 +322,9 @@ def test_load_hub_with_revision(self): assert "labels" in dataset.features @enable_hf_offline - def test_load_hub_with_revision_with_dpo(self): + def test_load_hub_with_revision_with_dpo( + self, dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff + ): """Verify that processing dpo data from the hub works with a specific revision""" cfg = DictDefault( @@ -349,14 +337,23 @@ def test_load_hub_with_revision_with_dpo(self): } ) - train_dataset, _ = load_prepare_preference_datasets(cfg) + # pylint: disable=duplicate-code + with patch( + "axolotl.utils.data.shared.load_dataset_w_config" + ) as mock_load_dataset: + # Set up the mock to return different values on successive calls + mock_load_dataset.return_value = ( + dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff + ) - assert len(train_dataset) == 1800 - assert "conversation" in train_dataset.features + train_dataset, _ = load_prepare_preference_datasets(cfg) + + assert len(train_dataset) == 1800 + assert "conversation" in train_dataset.features @enable_hf_offline @pytest.mark.skip("datasets bug with local datasets when offline") - def test_load_local_hub_with_revision(self): + def test_load_local_hub_with_revision(self, tokenizer): """Verify that a local copy of a hub dataset can be loaded with a specific revision""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_path = Path(tmp_dir) / "mhenrichsen/alpaca_2k_test" @@ -388,9 +385,7 @@ def test_load_local_hub_with_revision(self): } ) - dataset, _ = load_tokenized_prepared_datasets( - self.tokenizer, cfg, prepared_path - ) + dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) assert len(dataset) == 2000 assert "input_ids" in dataset.features @@ -399,7 +394,7 @@ def test_load_local_hub_with_revision(self): shutil.rmtree(tmp_ds_path) @enable_hf_offline - def test_loading_local_dataset_folder(self): + def test_loading_local_dataset_folder(self, tokenizer): """Verify that a dataset downloaded to a local folder can be loaded""" with tempfile.TemporaryDirectory() as tmp_dir: @@ -426,16 +421,10 @@ def test_loading_local_dataset_folder(self): } ) - dataset, _ = load_tokenized_prepared_datasets( - self.tokenizer, cfg, prepared_path - ) + dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) assert len(dataset) == 2000 assert "input_ids" in dataset.features assert "attention_mask" in dataset.features assert "labels" in dataset.features shutil.rmtree(tmp_ds_path) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index 865ff030cf..9549860f7a 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -9,9 +9,8 @@ from unittest.mock import patch import pytest -from constants import ALPACA_MESSAGES_CONFIG_REVISION, SPECIAL_TOKENS +from constants import ALPACA_MESSAGES_CONFIG_REVISION from datasets import Dataset -from transformers import AutoTokenizer from utils import enable_hf_offline from axolotl.utils.config import normalize_config @@ -216,13 +215,12 @@ def test_combined_duplicates_one(self): verify_deduplication(eval_dataset, expected_dataset_eval, "eval_dataset") -class TestDeduplicateRLDataset(unittest.TestCase): +class TestDeduplicateRLDataset: """Test a configured dataloader with deduplication.""" - def setUp(self) -> None: - self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") - self.tokenizer.add_special_tokens(SPECIAL_TOKENS) - self.cfg = DictDefault( + @pytest.fixture + def cfg(self): + fixture = DictDefault( { "tokenizer_config": "huggyllama/llama-7b", "sequence_len": 1024, @@ -235,28 +233,59 @@ def setUp(self) -> None: ], } ) + yield fixture - @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") @enable_hf_offline - def test_load_with_deduplication(self): + def test_load_with_deduplication( + self, cfg, dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff, tokenizer_huggyllama + ): """Verify that loading with deduplication removes duplicates.""" - # Load the dataset using the deduplication setting - train_dataset, _ = load_prepare_preference_datasets(self.cfg) - - # Verify that the dataset has been deduplicated - assert len(train_dataset) == 1800, "Dataset was not properly deduplicated" + # pylint: disable=duplicate-code + with ( + patch( + "axolotl.utils.data.shared.load_dataset_w_config" + ) as mock_load_dataset, + patch("axolotl.utils.models.load_tokenizer") as mock_load_tokenizer, + ): + # Set up the mock to return different values on successive calls + mock_load_dataset.side_effect = [ + dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff, + dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff, + ] + mock_load_tokenizer.return_value = tokenizer_huggyllama + + train_dataset, _ = load_prepare_preference_datasets(cfg) + + # Verify that the dataset has been deduplicated + assert len(train_dataset) == 1800, "Dataset was not properly deduplicated" - def test_load_without_deduplication(self): - """Verify that loading without deduplication retains duplicates.""" - self.cfg.dataset_exact_deduplication = False - # Load the dataset without deduplication - train_dataset, _ = load_prepare_preference_datasets(self.cfg) - - # Verify that the dataset retains duplicates - assert ( - len(train_dataset) == 1800 * 2 - ), "Dataset deduplication occurred when it should not have" + @enable_hf_offline + def test_load_without_deduplication( + self, cfg, dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff, tokenizer_huggyllama + ): + # pylint: disable=duplicate-code + with ( + patch( + "axolotl.utils.data.shared.load_dataset_w_config" + ) as mock_load_dataset, + patch("axolotl.utils.models.load_tokenizer") as mock_load_tokenizer, + ): + # Set up the mock to return different values on successive calls + mock_load_dataset.side_effect = [ + dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff, + dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff, + ] + mock_load_tokenizer.return_value = tokenizer_huggyllama + + cfg.dataset_exact_deduplication = False + # Load the dataset without deduplication + train_dataset, _ = load_prepare_preference_datasets(cfg) + + # Verify that the dataset retains duplicates + assert ( + len(train_dataset) == 1800 * 2 + ), "Dataset deduplication occurred when it should not have" class TestDeduplicateNonRL(unittest.TestCase): @@ -264,8 +293,6 @@ class TestDeduplicateNonRL(unittest.TestCase): @enable_hf_offline def setUp(self) -> None: - self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") - self.tokenizer.add_special_tokens(SPECIAL_TOKENS) self.cfg_1 = DictDefault( { "base_model": "huggyllama/llama-7b", diff --git a/tests/test_packed_pretraining.py b/tests/test_packed_pretraining.py index bd8d81dcc1..f783af9ccd 100644 --- a/tests/test_packed_pretraining.py +++ b/tests/test_packed_pretraining.py @@ -1,38 +1,50 @@ """Module for testing streaming dataset sequence packing""" import functools -import unittest +import random +import string import pytest import torch -from datasets import load_dataset +from datasets import IterableDataset from torch.utils.data import DataLoader -from transformers import AutoTokenizer -from utils import disable_hf_offline, enable_hf_offline from axolotl.utils.data import get_dataset_wrapper, wrap_pretraining_dataset from axolotl.utils.dict import DictDefault -class TestPretrainingPacking(unittest.TestCase): +class TestPretrainingPacking: """ Test class for packing streaming dataset sequences """ - @enable_hf_offline - def setUp(self) -> None: - # pylint: disable=duplicate-code - self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") - self.tokenizer.pad_token = "
" + @pytest.fixture + def random_text(self): + # seed with random.seed(0) for reproducibility + random.seed(0) + + # generate 20 rows of random text with "words" of between 2 and 10 characters and + # between 400 to 1200 characters per line + data = [ + "".join(random.choices(string.ascii_lowercase, k=random.randint(2, 10))) + for _ in range(20) + ] + [ + " ".join( + random.choices(string.ascii_lowercase, k=random.randint(400, 1200)) + ) + for _ in range(20) + ] + + # Create an IterableDataset + def generator(): + for text in data: + yield {"text": text} + + return IterableDataset.from_generator(generator) @pytest.mark.flaky(retries=1, delay=5) - @disable_hf_offline - def test_packing_stream_dataset(self): - # pylint: disable=duplicate-code - dataset = load_dataset( - "winglian/tiny-shakespeare", - streaming=True, - )["train"] + def test_packing_stream_dataset(self, tokenizer_huggyllama, random_text): + dataset = random_text cfg = DictDefault( { @@ -55,15 +67,16 @@ def test_packing_stream_dataset(self): ds_wrapper_partial = functools.partial( get_dataset_wrapper, cfg.pretraining_dataset[0], - self.tokenizer, + tokenizer_huggyllama, cfg, cfg.pretraining_dataset[0]["type"] or "pretrain", ) + # pylint: disable=duplicate-code original_bsz = cfg.micro_batch_size train_dataset = wrap_pretraining_dataset( dataset, - self.tokenizer, + tokenizer_huggyllama, cfg, ds_wrapper_partial, max_tokens=cfg.sequence_len, @@ -96,7 +109,3 @@ def test_packing_stream_dataset(self): # [1, original_bsz * cfg.sequence_len] # ) idx += 1 - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index bc49206711..0ce8785774 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -19,7 +19,7 @@ def reload_modules(hf_hub_offline): importlib.reload(huggingface_hub.constants) huggingface_hub.constants.HF_HUB_OFFLINE = hf_hub_offline importlib.reload(datasets.config) - datasets.config.HF_HUB_OFFLINE = hf_hub_offline + setattr(datasets.config, "HF_HUB_OFFLINE", hf_hub_offline) reset_sessions() From 4ba80a0e5acae8fd4df5962f2a2c12cf75fcd2a5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 29 Mar 2025 08:30:06 -0400 Subject: [PATCH 0495/1405] fix streaming packing test (#2454) * fix streaming packing test * constrain amount of text generated --- tests/test_packed_pretraining.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/test_packed_pretraining.py b/tests/test_packed_pretraining.py index f783af9ccd..115813df2d 100644 --- a/tests/test_packed_pretraining.py +++ b/tests/test_packed_pretraining.py @@ -23,22 +23,26 @@ def random_text(self): # seed with random.seed(0) for reproducibility random.seed(0) - # generate 20 rows of random text with "words" of between 2 and 10 characters and + # generate row of random text with "words" of between 2 and 10 characters and # between 400 to 1200 characters per line - data = [ - "".join(random.choices(string.ascii_lowercase, k=random.randint(2, 10))) - for _ in range(20) - ] + [ - " ".join( - random.choices(string.ascii_lowercase, k=random.randint(400, 1200)) + def rand_txt(): + return " ".join( + [ + "".join( + random.choices(string.ascii_lowercase, k=random.randint(2, 10)) + ) + for _ in range(random.randint(50, 200)) + ] ) - for _ in range(20) - ] + + # Create a list of 2000 random texts rather than just using it within the + # generator so the test runs faster + data = [rand_txt() for _ in range(500)] # Create an IterableDataset def generator(): - for text in data: - yield {"text": text} + for row in data: + yield {"text": row} return IterableDataset.from_generator(generator) @@ -92,7 +96,7 @@ def test_packing_stream_dataset(self, tokenizer_huggyllama, random_text): ) idx = 0 for data in trainer_loader: - if idx > 10: + if idx > 3: break assert data["input_ids"].shape == torch.Size( [1, original_bsz * cfg.sequence_len] From cf0c79d52e430681659cfe8c9794b1182c9426f7 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 31 Mar 2025 13:40:12 +0700 Subject: [PATCH 0496/1405] fix: minor patches for multimodal (#2441) * fix: update chat_template * fix: handle gemma3 showing a lot of no content for turn 0 * fix: remove unknown config from examples * fix: test * fix: temporary disable gemma2 test * fix: stop overwriting config.text_config unnecessarily * fix: handling of set cache to the text_config section * feat: add liger gemma support and bump liger to 0.5.5 * fix: add double use_cache setting * fix: add support for final_logit_softcap in CCE for gemma2/3 * fix: set use_cache before model load * feat: add missing layernorm override * fix: handle gemma3 rmsnorm * fix: use wrapper to pass dim as hidden_size * fix: change dim to positional * fix: patch with wrong mlp * chore: refactor use_cache handling * fix import issues * fix tests.e2e.utils import --------- Co-authored-by: Wing Lian --- .github/workflows/tests-nightly.yml | 2 +- .github/workflows/tests.yml | 4 +- .isort.cfg | 1 + cicd/{tests.py => e2e_tests.py} | 0 .../{qlora.yml => gemma-3-1b-qlora.yml} | 2 +- examples/llama-3/lora-1b-deduplicate-sft.yml | 1 - .../cut_cross_entropy/__init__.py | 2 +- .../cut_cross_entropy/monkeypatch/gemma3.py | 14 +-- .../cut_cross_entropy/monkeypatch/utils.py | 40 +++++++++ src/axolotl/integrations/liger/README.md | 20 +++++ src/axolotl/integrations/liger/__init__.py | 48 ++++++++++- .../prompt_strategies/chat_template.py | 8 +- src/axolotl/utils/models.py | 63 ++++---------- tests/__init__.py | 0 tests/conftest.py | 3 +- tests/core/chat/test_messages.py | 3 +- tests/e2e/integrations/test_kd.py | 3 +- tests/e2e/integrations/test_liger.py | 4 +- tests/e2e/multigpu/test_grpo.py | 3 +- tests/e2e/multigpu/test_llama.py | 3 +- tests/e2e/multigpu/test_ray.py | 3 +- tests/e2e/test_deepseekv3.py | 3 +- tests/e2e/test_llama.py | 4 +- tests/hf_offline_utils.py | 85 +++++++++++++++++++ tests/prompt_strategies/conftest.py | 3 +- tests/prompt_strategies/test_alpaca.py | 3 +- .../test_chat_template_utils.py | 3 +- .../test_chat_templates_advanced.py | 23 +++-- .../test_dpo_chat_templates.py | 3 +- tests/prompt_strategies/test_dpo_chatml.py | 3 +- tests/test_data.py | 3 +- tests/test_datasets.py | 13 +-- tests/test_exact_deduplication.py | 5 +- tests/test_packed_batch_sampler.py | 3 +- tests/test_packed_dataset.py | 3 +- tests/test_prompt_tokenizers.py | 3 +- tests/test_tokenizers.py | 3 +- tests/utils/__init__.py | 85 ------------------- 38 files changed, 287 insertions(+), 188 deletions(-) rename cicd/{tests.py => e2e_tests.py} (100%) rename examples/gemma3/{qlora.yml => gemma-3-1b-qlora.yml} (97%) create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/utils.py create mode 100644 tests/__init__.py create mode 100644 tests/hf_offline_utils.py diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index efad7cc379..0b91d0c01c 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -136,4 +136,4 @@ jobs: echo "NIGHTLY_BUILD=${{ matrix.nightly_build }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | - modal run cicd.tests + modal run cicd.e2e_tests diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 632731a2d0..ad6305e8f3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -232,7 +232,7 @@ jobs: echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | - modal run cicd.tests + modal run cicd.e2e_tests docker-e2e-tests: if: github.repository_owner == 'axolotl-ai-cloud' @@ -279,4 +279,4 @@ jobs: echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | - modal run cicd.tests + modal run cicd.e2e_tests diff --git a/.isort.cfg b/.isort.cfg index e487797321..bf9afe3192 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,3 +1,4 @@ [settings] profile=black known_third_party=wandb,comet_ml +known_local_folder=src,tests diff --git a/cicd/tests.py b/cicd/e2e_tests.py similarity index 100% rename from cicd/tests.py rename to cicd/e2e_tests.py diff --git a/examples/gemma3/qlora.yml b/examples/gemma3/gemma-3-1b-qlora.yml similarity index 97% rename from examples/gemma3/qlora.yml rename to examples/gemma3/gemma-3-1b-qlora.yml index 50045cc8a3..669ffacdc5 100644 --- a/examples/gemma3/qlora.yml +++ b/examples/gemma3/gemma-3-1b-qlora.yml @@ -10,7 +10,7 @@ load_in_4bit: true strict: false # huggingface repo -chat_template: gemma3_text +chat_template: gemma3 datasets: - path: cgato/SlimOrcaDedupCleaned type: chat_template diff --git a/examples/llama-3/lora-1b-deduplicate-sft.yml b/examples/llama-3/lora-1b-deduplicate-sft.yml index 4516964655..bc748807b2 100644 --- a/examples/llama-3/lora-1b-deduplicate-sft.yml +++ b/examples/llama-3/lora-1b-deduplicate-sft.yml @@ -19,7 +19,6 @@ val_set_size: 0.0 output_dir: ./outputs/lora-out dataset_exact_deduplication: true -test_value: true sequence_len: 4096 sample_packing: true diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index a475cd9f7f..19faf85e67 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -25,8 +25,8 @@ from axolotl.integrations.base import BasePlugin from axolotl.utils import get_pytorch_version +from axolotl.utils.distributed import zero_only -from ...utils.distributed import zero_only from .args import CutCrossEntropyArgs # pylint: disable=unused-import. # noqa: F401 LOG = logging.getLogger("axolotl.integrations.cut_cross_entropy") diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py index ecbe68085f..ccf0c160df 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py @@ -15,7 +15,6 @@ from cut_cross_entropy.transformers.utils import ( PatchOptions, TransformersModelT, - apply_lce, ) from torch import nn from transformers.cache_utils import Cache, HybridCache @@ -33,6 +32,8 @@ ) from transformers.utils.deprecation import deprecate_kwarg +from axolotl.integrations.cut_cross_entropy.monkeypatch.utils import apply_lce + _PATCH_OPTS: PatchOptions | None = None @@ -134,25 +135,17 @@ def cce_forward( if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): assert labels is not None - if self.config.final_logit_softcapping is not None: - logger.warning_once( - "final_logit_softcapping is not supported for gemma3_text with CCE. Disabling." - ) loss = apply_lce( hidden_states[:, slice_indices, :], self.lm_head.weight, labels, _PATCH_OPTS, + softcap=getattr(self.config, "final_logit_softcapping", None), **loss_kwargs, ) elif _PATCH_OPTS is not None and defer_logits_calculation: # defer logits calculation to the ConditionalGeneration forward logits = hidden_states[:, slice_indices, :] - - if self.config.final_logit_softcapping is not None: - logger.warning_once( - "final_logit_softcapping is not supported for gemma3 with CCE. Disabling." - ) else: logits = self.lm_head(hidden_states[:, slice_indices, :]) if self.config.final_logit_softcapping is not None: @@ -353,6 +346,7 @@ def cce_forward_multimodal( self.language_model.lm_head.weight, labels, _PATCH_OPTS, + softcap=getattr(self.config, "final_logit_softcapping", None), **lm_kwargs, ) else: diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/utils.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/utils.py new file mode 100644 index 0000000000..b808b9f0d8 --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/utils.py @@ -0,0 +1,40 @@ +# Copyright (C) 2024 Apple Inc. All Rights Reserved. + +"""Monkeypatch for apply_lce to add softcap.""" + +import torch +from cut_cross_entropy import linear_cross_entropy +from cut_cross_entropy.transformers.utils import PatchOptions + + +def apply_lce( + e: torch.Tensor, + c: torch.Tensor, + labels: torch.Tensor, + opts: PatchOptions, + bias: torch.Tensor | None = None, + softcap: float | None = None, + **loss_kwargs, +) -> torch.Tensor: + """Monkey patch for apply_lce to support softcap kwarg.""" + num_items_in_batch = loss_kwargs.get("num_items_in_batch", None) + cce_kwargs = opts.to_kwargs() + if num_items_in_batch is not None and cce_kwargs["reduction"] == "mean": + cce_kwargs["reduction"] = "sum" + else: + num_items_in_batch = None + + loss = linear_cross_entropy( + e, + c, + labels.to(e.device), + bias=bias, + shift=True, + softcap=softcap, + **cce_kwargs, + ) + + if num_items_in_batch is not None: + loss = loss / num_items_in_batch + + return loss diff --git a/src/axolotl/integrations/liger/README.md b/src/axolotl/integrations/liger/README.md index 16164d72f2..03422f8899 100644 --- a/src/axolotl/integrations/liger/README.md +++ b/src/axolotl/integrations/liger/README.md @@ -20,6 +20,26 @@ liger_layer_norm: true liger_fused_linear_cross_entropy: true ``` +## Supported Models + +- deepseek_v2 +- gemma +- gemma2 +- gemma3 (partial support, no support for FLCE yet) +- granite +- jamba +- llama +- mistral +- mixtral +- mllama +- mllama_text_model +- olmo2 +- paligemma +- phi3 +- qwen2 +- qwen2_5_vl +- qwen2_vl + ## Citation ```bib diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index 327a051388..d6e423fa94 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -21,6 +21,7 @@ import inspect import logging import sys +from functools import partial from axolotl.integrations.base import BasePlugin @@ -41,11 +42,18 @@ def get_input_args(self): def pre_model_load(self, cfg): from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss from liger_kernel.transformers.functional import liger_cross_entropy + from liger_kernel.transformers.geglu import LigerGEGLUMLP + from liger_kernel.transformers.layer_norm import LigerLayerNorm from liger_kernel.transformers.monkey_patch import MODEL_TYPE_TO_APPLY_LIGER_FN from liger_kernel.transformers.rms_norm import LigerRMSNorm from liger_kernel.transformers.rope import liger_rotary_pos_emb from liger_kernel.transformers.swiglu import LigerSwiGLUMLP + if cfg.liger_cross_entropy and cfg.liger_fused_linear_cross_entropy: + raise ValueError( + "Cannot have both `liger_cross_entropy` and `liger_fused_linear_cross_entropy` set." + ) + if cfg.model_config_type in MODEL_TYPE_TO_APPLY_LIGER_FN: apply_liger_fn = MODEL_TYPE_TO_APPLY_LIGER_FN[cfg.model_config_type] liger_fn_sig = inspect.signature(apply_liger_fn) @@ -82,6 +90,8 @@ def pre_model_load(self, cfg): modeling_jamba.JambaRMSNorm = LigerRMSNorm if cfg.liger_glu_activation: modeling_jamba.JambaMLP = LigerSwiGLUMLP + if cfg.liger_layer_norm: + modeling_jamba.nn.LayerNorm = LigerLayerNorm if cfg.liger_cross_entropy: from transformers.loss.loss_utils import nn @@ -104,15 +114,51 @@ def pre_model_load(self, cfg): # The DeepseekV2 version of RoPE is different than upstream LLaMA. # See https://github.com/linkedin/Liger-Kernel/issues/129#issuecomment-2313763528 logging.warning("Fused liger_rope is not supported for DeepseekV2.") + if cfg.liger_glu_activation: + logging.warning("liger_glu_activation is not supported for DeepseekV2.") if cfg.liger_rms_norm: modeling_mod.DeepseekV2RMSNorm = LigerRMSNorm if cfg.liger_glu_activation: modeling_mod.DeepseekV2MLP.forward = LigerSwiGLUMLP.forward + if cfg.liger_layer_norm: + modeling_mod.DeepseekV2MLP.forward = LigerLayerNorm.forward if cfg.liger_cross_entropy: # We do not patch `nn.functional.cross_entropy` for DeepseekV2 as it still uses # nn.CrossEntropyLoss in the forward method. modeling_mod.CrossEntropyLoss = LigerCrossEntropyLoss if cfg.liger_fused_linear_cross_entropy: modeling_mod.DeepseekV2ForCausalLM.forward = deepseekv2_lce_forward - elif cfg.model_config_type in ["gemma3_text", "deepseek_v3"]: + elif cfg.model_config_type in ["gemma3", "gemma3_text"]: + from transformers.models.gemma3 import modeling_gemma3 + + if cfg.liger_rope: + modeling_gemma3.apply_rotary_pos_emb = liger_rotary_pos_emb + if cfg.liger_rms_norm: + + def _liger_rms_norm_wrapper(dim, **kwargs): + "Convert 'dim' keyword to 'hidden_size' to pass to LigerRMSNorm" + return LigerRMSNorm(hidden_size=dim, **kwargs) + + modeling_gemma3.Gemma3RMSNorm = partial( + _liger_rms_norm_wrapper, + offset=1.0, + casting_mode="gemma", + init_fn="zeros", + in_place=False, + ) + if cfg.liger_glu_activation: + modeling_gemma3.Gemma3MLP = LigerGEGLUMLP + if cfg.liger_layer_norm: + modeling_gemma3.nn.LayerNorm = LigerLayerNorm + + if cfg.liger_cross_entropy: + from transformers.loss.loss_utils import nn + + nn.functional.cross_entropy = liger_cross_entropy + + if cfg.liger_fused_linear_cross_entropy: + raise NotImplementedError( + "Fused linear cross entropy is not yet supported for Gemma3." + ) + elif cfg.model_config_type in ["deepseek_v3"]: raise ValueError(f"Unsupported model config type: {cfg.model_config_type}") diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 4266e0c996..918c563296 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -411,11 +411,15 @@ def find_turn(self, turns: list[dict], turn_idx: int): if turn_idx >= len(turns): raise ValueError(f"Turn index {turn_idx} out of range") - # mistral does not output message if it contains only system message + # mistral/gemma3 does not output message if it contains only system message if ( turn_idx == 0 and turns[0].get("role") == "system" - and "mistral" in self.tokenizer.name_or_path.lower() + and ( + "mistral" in self.tokenizer.name_or_path.lower() + # gemma3 uses gemma tokenizer + or "gemma" in self.tokenizer.name_or_path.lower() + ) ): return -1, -1 diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 23a6e102f3..10c171d83e 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -8,7 +8,7 @@ import os import types from functools import cached_property -from typing import Any, Dict, Optional, Tuple, Union # noqa: F401 +from typing import Any, Dict, Optional, Tuple import addict import bitsandbytes as bnb @@ -25,7 +25,7 @@ prepare_model_for_kbit_training, ) from torch import nn -from transformers import ( # noqa: F401 +from transformers import ( AddedToken, AutoConfig, AutoModelForCausalLM, @@ -39,6 +39,7 @@ LlavaForConditionalGeneration, Mistral3ForConditionalGeneration, MllamaForConditionalGeneration, + PretrainedConfig, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, @@ -107,14 +108,21 @@ def get_module_class_from_name(module, name): return None -def check_model_config(cfg: DictDefault, model_config: Union[AutoConfig, DictDefault]): +def check_model_config(cfg: DictDefault, model_config: PretrainedConfig): + # Set use_cache to False + if hasattr(model_config, "use_cache"): + model_config.use_cache = False + if cfg.is_multimodal: - if hasattr(model_config, "text_config"): - model_config = model_config.text_config - model_config.use_cache = False - elif hasattr(model_config, "get_text_config"): - model_config = model_config.get_text_config() - model_config.use_cache = False + # For multimodal configs, use_cache is set in the text_config + if hasattr(model_config, "get_text_config"): + text_config = model_config.get_text_config() + if hasattr(text_config, "use_cache"): + text_config.use_cache = False + else: + raise ValueError( + "No text config found for multimodal model. Please raise an Issue with model details." + ) # check if image_size is not set and load image size from model config if available if ( @@ -523,14 +531,6 @@ def __init__( # init model config self.model_config = load_model_config(cfg) - if cfg.is_multimodal: - if hasattr(self.model_config, "text_config"): - self.text_model_config = self.model_config.text_config - else: - # for qwen2_vl - self.text_model_config = self.model_config.get_text_config() - else: - self.text_model_config = self.model_config self.auto_model_loader = AutoModelForCausalLM # pylint: disable=invalid-name @@ -947,8 +947,6 @@ def _configure_zero3_memory_efficient_loading(): quantization_config = ( quantization_config or self.model_kwargs["quantization_config"] ) - if self.cfg.is_multimodal: - self.model_config.text_config = self.text_model_config self.model = load_sharded_model_quant( self.base_model, self.model_config, @@ -969,9 +967,6 @@ def _configure_zero3_memory_efficient_loading(): _ = _configure_zero3_memory_efficient_loading() - if self.cfg.is_multimodal: - self.model_config.text_config = self.text_model_config - # Load model with random initialization if specified if self.cfg.random_init_weights: # AutoModel classes support the from_config method @@ -1026,8 +1021,6 @@ def _configure_zero3_memory_efficient_loading(): and self.model_type != "AutoModelForCausalLM" and not self.cfg.trust_remote_code ): - if self.cfg.is_multimodal: - self.model_config.text_config = self.text_model_config if self.cfg.gptq: self.model = self.auto_model_loader.from_pretrained( self.base_model, @@ -1043,25 +1036,7 @@ def _configure_zero3_memory_efficient_loading(): **self.model_kwargs, ) else: - # Shouldn't be a problem most of the time. will obviously error if the model doesn't support this - # when training starts - if ( - hasattr(self.text_model_config, "max_seq_len") - and self.text_model_config.max_seq_len - and self.cfg.sequence_len > self.text_model_config.max_seq_len - ): - self.text_model_config.max_seq_len = self.cfg.sequence_len - LOG.warning(f"increasing context length to {self.cfg.sequence_len}") - elif ( - hasattr(self.text_model_config, "max_sequence_length") - and self.text_model_config.max_sequence_length - and self.cfg.sequence_len > self.text_model_config.max_sequence_length - ): - self.text_model_config.max_sequence_length = self.cfg.sequence_len - LOG.warning(f"increasing context length to {self.cfg.sequence_len}") if self.cfg.gptq: - if self.cfg.is_multimodal: - self.model_config.text_config = self.text_model_config self.model = self.auto_model_loader.from_pretrained( self.base_model, config=self.model_config, @@ -1080,8 +1055,6 @@ def _configure_zero3_memory_efficient_loading(): _ = _configure_zero3_memory_efficient_loading() - if self.cfg.is_multimodal: - self.model_config.text_config = self.text_model_config self.model = self.auto_model_loader.from_pretrained( self.base_model, config=self.model_config, @@ -1346,8 +1319,6 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: requires_grad.append(f"{name}: {param.requires_grad}") if len(requires_grad) == 0: LOG.warning("there are no parameters that require gradient updates") - if hasattr(self.model, "config"): - self.model.config.use_cache = False if self.cfg.flash_optimum: from optimum.bettertransformer import BetterTransformer diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/conftest.py b/tests/conftest.py index 8cf0832901..aa867ecb94 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,8 @@ from datasets import load_dataset from huggingface_hub import snapshot_download from transformers import AutoTokenizer -from utils import disable_hf_offline, enable_hf_offline + +from tests.hf_offline_utils import disable_hf_offline, enable_hf_offline def retry_on_request_exceptions(max_retries=3, delay=1): diff --git a/tests/core/chat/test_messages.py b/tests/core/chat/test_messages.py index bab77fbcf4..c1d5cbcbe7 100644 --- a/tests/core/chat/test_messages.py +++ b/tests/core/chat/test_messages.py @@ -6,11 +6,12 @@ import pytest from transformers import AddedToken, AutoTokenizer -from utils import enable_hf_offline from axolotl.core.chat.format.chatml import format_message from axolotl.core.chat.messages import ChatFormattedChats, Chats +from tests.hf_offline_utils import enable_hf_offline # noqa + @pytest.fixture(scope="session", name="llama_tokenizer") @enable_hf_offline diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py index 4f8cde1d7b..9bfe5aaefd 100644 --- a/tests/e2e/integrations/test_kd.py +++ b/tests/e2e/integrations/test_kd.py @@ -5,7 +5,6 @@ from pathlib import Path import pytest -from e2e.utils import check_tensorboard, require_torch_2_5_1 from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets @@ -13,6 +12,8 @@ from axolotl.utils.config import normalize_config, prepare_plugins, validate_config from axolotl.utils.dict import DictDefault +from tests.e2e.utils import check_tensorboard, require_torch_2_5_1 + @pytest.fixture(name="kd_min_cfg") def min_cfg(temp_dir): diff --git a/tests/e2e/integrations/test_liger.py b/tests/e2e/integrations/test_liger.py index 226ed46f80..03c83083d2 100644 --- a/tests/e2e/integrations/test_liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -2,15 +2,13 @@ Simple end-to-end test for Liger integration """ -from e2e.utils import require_torch_2_4_1 - from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, prepare_plugins from axolotl.utils.dict import DictDefault -from ..utils import check_model_output_exists +from tests.e2e.utils import check_model_output_exists, require_torch_2_4_1 class LigerIntegrationTestCase: diff --git a/tests/e2e/multigpu/test_grpo.py b/tests/e2e/multigpu/test_grpo.py index bb99581ad9..a879a77501 100644 --- a/tests/e2e/multigpu/test_grpo.py +++ b/tests/e2e/multigpu/test_grpo.py @@ -8,11 +8,12 @@ import pytest import yaml from accelerate.test_utils import execute_subprocess_async -from e2e.utils import require_vllm from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault +from tests.e2e.utils import require_vllm + class TestGRPO: """ diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 60b1940907..8a16ff096e 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -9,12 +9,13 @@ import pytest import yaml from accelerate.test_utils import execute_subprocess_async -from e2e.utils import check_tensorboard from huggingface_hub import snapshot_download from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault +from tests.e2e.utils import check_tensorboard + LOG = logging.getLogger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index 72ec69aa85..8e7916728c 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -9,10 +9,11 @@ import pytest import yaml from accelerate.test_utils import execute_subprocess_async -from e2e.utils import check_tensorboard, require_torch_lt_2_6_0 from axolotl.utils.dict import DictDefault +from tests.e2e.utils import check_tensorboard, require_torch_lt_2_6_0 + LOG = logging.getLogger(__name__) os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_deepseekv3.py b/tests/e2e/test_deepseekv3.py index 41935c6afa..cdaa2c4161 100644 --- a/tests/e2e/test_deepseekv3.py +++ b/tests/e2e/test_deepseekv3.py @@ -7,7 +7,6 @@ from pathlib import Path import pytest -from utils import enable_hf_offline from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets @@ -15,6 +14,8 @@ from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from tests.hf_offline_utils import enable_hf_offline + LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index 6447442404..8d6483ea42 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -5,14 +5,14 @@ import logging import os -from e2e.utils import check_model_output_exists - from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from tests.e2e.utils import check_model_output_exists + LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/hf_offline_utils.py b/tests/hf_offline_utils.py new file mode 100644 index 0000000000..0ce8785774 --- /dev/null +++ b/tests/hf_offline_utils.py @@ -0,0 +1,85 @@ +""" +test utils for helpers and decorators +""" + +import os +from functools import wraps + +from huggingface_hub.utils import reset_sessions + + +def reload_modules(hf_hub_offline): + # Force reload of the modules that check this variable + import importlib + + import datasets + import huggingface_hub.constants + + # Reload the constants module first, as others depend on it + importlib.reload(huggingface_hub.constants) + huggingface_hub.constants.HF_HUB_OFFLINE = hf_hub_offline + importlib.reload(datasets.config) + setattr(datasets.config, "HF_HUB_OFFLINE", hf_hub_offline) + reset_sessions() + + +def enable_hf_offline(test_func): + """ + test decorator that sets HF_HUB_OFFLINE environment variable to True and restores it after the test even if the test fails. + :param test_func: + :return: + """ + + @wraps(test_func) + def wrapper(*args, **kwargs): + # Save the original value of HF_HUB_OFFLINE environment variable + original_hf_offline = os.getenv("HF_HUB_OFFLINE") + + # Set HF_OFFLINE environment variable to True + os.environ["HF_HUB_OFFLINE"] = "1" + + reload_modules(True) + try: + # Run the test function + return test_func(*args, **kwargs) + finally: + # Restore the original value of HF_HUB_OFFLINE environment variable + if original_hf_offline is not None: + os.environ["HF_HUB_OFFLINE"] = original_hf_offline + reload_modules(bool(original_hf_offline)) + else: + del os.environ["HF_HUB_OFFLINE"] + reload_modules(False) + + return wrapper + + +def disable_hf_offline(test_func): + """ + test decorator that sets HF_HUB_OFFLINE environment variable to False and restores it after the wrapped func + :param test_func: + :return: + """ + + @wraps(test_func) + def wrapper(*args, **kwargs): + # Save the original value of HF_HUB_OFFLINE environment variable + original_hf_offline = os.getenv("HF_HUB_OFFLINE") + + # Set HF_OFFLINE environment variable to True + os.environ["HF_HUB_OFFLINE"] = "0" + + reload_modules(False) + try: + # Run the test function + return test_func(*args, **kwargs) + finally: + # Restore the original value of HF_HUB_OFFLINE environment variable + if original_hf_offline is not None: + os.environ["HF_HUB_OFFLINE"] = original_hf_offline + reload_modules(bool(original_hf_offline)) + else: + del os.environ["HF_HUB_OFFLINE"] + reload_modules(False) + + return wrapper diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index 44914e617e..fe59e00d8f 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -5,11 +5,12 @@ import pytest from datasets import Dataset from transformers import AutoTokenizer -from utils import enable_hf_offline from axolotl.prompt_strategies.jinja_template_analyzer import JinjaTemplateAnalyzer from axolotl.utils.chat_templates import _CHAT_TEMPLATES +from tests.hf_offline_utils import enable_hf_offline + @pytest.fixture(name="assistant_dataset") def fixture_assistant_dataset(): diff --git a/tests/prompt_strategies/test_alpaca.py b/tests/prompt_strategies/test_alpaca.py index 366663c137..78f7837477 100644 --- a/tests/prompt_strategies/test_alpaca.py +++ b/tests/prompt_strategies/test_alpaca.py @@ -6,12 +6,13 @@ from datasets import Dataset from tokenizers import AddedToken from transformers import AutoTokenizer -from utils import enable_hf_offline from axolotl.datasets import TokenizedPromptDataset from axolotl.prompt_tokenizers import AlpacaPromptTokenizingStrategy from axolotl.prompters import AlpacaPrompter, PromptStyle +from tests.hf_offline_utils import enable_hf_offline + @pytest.fixture(name="alpaca_dataset") def fixture_alpaca_dataset(): diff --git a/tests/prompt_strategies/test_chat_template_utils.py b/tests/prompt_strategies/test_chat_template_utils.py index ec0c484ee5..10c84f4320 100644 --- a/tests/prompt_strategies/test_chat_template_utils.py +++ b/tests/prompt_strategies/test_chat_template_utils.py @@ -6,7 +6,6 @@ import pytest from transformers import AutoTokenizer -from utils import enable_hf_offline from axolotl.utils.chat_templates import ( _CHAT_TEMPLATES, @@ -14,6 +13,8 @@ get_chat_template, ) +from tests.hf_offline_utils import enable_hf_offline + @pytest.fixture(name="llama3_tokenizer") @enable_hf_offline diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index f316e6ec3a..ce55b871f5 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -9,7 +9,6 @@ from datasets import Dataset from tokenizers import AddedToken from transformers import PreTrainedTokenizer -from utils import enable_hf_offline from axolotl.prompt_strategies.chat_template import ( ChatTemplatePrompter, @@ -18,6 +17,8 @@ from axolotl.prompters import IGNORE_TOKEN_ID from axolotl.utils.chat_templates import get_chat_template +from tests.hf_offline_utils import enable_hf_offline + logging.basicConfig(level=logging.DEBUG) LOG = logging.getLogger("axolotl") @@ -31,12 +32,14 @@ "mistralv03_tokenizer_chat_template_jinja", "[/INST]", ), - ( - "gemma2_tokenizer", - "jinja", - "gemma2_tokenizer_chat_template_jinja", - "", - ), + # TODO: temporarily skip gemma due to gemma3 template + # Re-enable on new chat_template implementation for perf + # ( + # "gemma2_tokenizer", + # "jinja", + # "gemma2_tokenizer_chat_template_jinja", + # "", + # ), ("phi35_tokenizer", "phi_35", None, "<|end|>"), ] @@ -94,7 +97,11 @@ def _should_skip_turn(self, tokenizer, turn, turn_idx, start_idx, end_idx): if ( turn_idx == 0 and turn.get("from") in ["system", "context"] - and "mistral" in tokenizer.name_or_path.lower() + and ( + "mistral" in tokenizer.name_or_path.lower() + or "gemma" + in tokenizer.name_or_path.lower() # temporarily skip gemma due to gemma3 template + ) ): assert ( start_idx == -1 and end_idx == -1 diff --git a/tests/prompt_strategies/test_dpo_chat_templates.py b/tests/prompt_strategies/test_dpo_chat_templates.py index b8e58a8d37..b1802faa03 100644 --- a/tests/prompt_strategies/test_dpo_chat_templates.py +++ b/tests/prompt_strategies/test_dpo_chat_templates.py @@ -7,11 +7,12 @@ import pytest from datasets import Dataset from transformers import AutoTokenizer -from utils import enable_hf_offline from axolotl.prompt_strategies.dpo.chat_template import default from axolotl.utils.dict import DictDefault +from tests.hf_offline_utils import enable_hf_offline + @pytest.fixture(name="assistant_dataset") def fixture_assistant_dataset(): diff --git a/tests/prompt_strategies/test_dpo_chatml.py b/tests/prompt_strategies/test_dpo_chatml.py index 1212bf4119..b313a4b640 100644 --- a/tests/prompt_strategies/test_dpo_chatml.py +++ b/tests/prompt_strategies/test_dpo_chatml.py @@ -5,12 +5,13 @@ import unittest import pytest -from utils import enable_hf_offline from axolotl.prompt_strategies.dpo import load as load_dpo from axolotl.utils.data.rl import load_prepare_preference_datasets from axolotl.utils.dict import DictDefault +from tests.hf_offline_utils import enable_hf_offline + @pytest.fixture(name="minimal_dpo_cfg") def fixture_cfg(): diff --git a/tests/test_data.py b/tests/test_data.py index ddfa96b822..6d583cfd31 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -5,10 +5,11 @@ import unittest from transformers import LlamaTokenizer -from utils import enable_hf_offline from axolotl.utils.data import encode_pretraining, md5 +from tests.hf_offline_utils import enable_hf_offline + class TestEncodePretraining(unittest.TestCase): """ diff --git a/tests/test_datasets.py b/tests/test_datasets.py index 71d2854970..a82f2f3817 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -8,20 +8,21 @@ from unittest.mock import patch import pytest -from constants import ( - ALPACA_MESSAGES_CONFIG_OG, - ALPACA_MESSAGES_CONFIG_REVISION, - SPECIAL_TOKENS, -) from datasets import Dataset from huggingface_hub import snapshot_download from transformers import PreTrainedTokenizer -from utils import enable_hf_offline from axolotl.utils.data import load_tokenized_prepared_datasets from axolotl.utils.data.rl import load_prepare_preference_datasets from axolotl.utils.dict import DictDefault +from tests.constants import ( + ALPACA_MESSAGES_CONFIG_OG, + ALPACA_MESSAGES_CONFIG_REVISION, + SPECIAL_TOKENS, +) +from tests.hf_offline_utils import enable_hf_offline + class TestDatasetPreparation: """Test a configured dataloader.""" diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index 9549860f7a..7430352a40 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -9,9 +9,7 @@ from unittest.mock import patch import pytest -from constants import ALPACA_MESSAGES_CONFIG_REVISION from datasets import Dataset -from utils import enable_hf_offline from axolotl.utils.config import normalize_config from axolotl.utils.data import prepare_dataset @@ -20,6 +18,9 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_processor, load_tokenizer +from tests.constants import ALPACA_MESSAGES_CONFIG_REVISION +from tests.hf_offline_utils import enable_hf_offline + def verify_deduplication(actual_dataset, expected_dataset, dataset_name): """ diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index 7964d1e326..061b64b09b 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -4,7 +4,6 @@ from datasets import concatenate_datasets, load_dataset from torch.utils.data import DataLoader, RandomSampler from transformers import AutoTokenizer -from utils import enable_hf_offline from axolotl.datasets import TokenizedPromptDataset from axolotl.prompt_strategies.completion import load @@ -13,6 +12,8 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths +from tests.hf_offline_utils import enable_hf_offline + @pytest.fixture(name="tokenizer") def fixture_tokenizer(): diff --git a/tests/test_packed_dataset.py b/tests/test_packed_dataset.py index 47b429384d..45fc752827 100644 --- a/tests/test_packed_dataset.py +++ b/tests/test_packed_dataset.py @@ -5,12 +5,13 @@ from datasets import Dataset, load_dataset from transformers import AutoTokenizer -from utils import enable_hf_offline from axolotl.datasets import ConstantLengthDataset, TokenizedPromptDataset from axolotl.prompt_tokenizers import AlpacaPromptTokenizingStrategy from axolotl.prompters import AlpacaPrompter +from tests.hf_offline_utils import enable_hf_offline + class TestPacking(unittest.TestCase): """ diff --git a/tests/test_prompt_tokenizers.py b/tests/test_prompt_tokenizers.py index ab33502346..65eee7ddba 100644 --- a/tests/test_prompt_tokenizers.py +++ b/tests/test_prompt_tokenizers.py @@ -8,7 +8,6 @@ import pytest from datasets import load_dataset from transformers import AddedToken, AutoTokenizer, LlamaTokenizer -from utils import enable_hf_offline from axolotl.prompt_strategies.alpaca_chat import NoSystemPrompter from axolotl.prompt_strategies.alpaca_w_system import ( @@ -24,6 +23,8 @@ from axolotl.prompters import AlpacaPrompter, PromptStyle from axolotl.utils.dict import DictDefault +from tests.hf_offline_utils import enable_hf_offline + LOG = logging.getLogger("axolotl") test_data = { diff --git a/tests/test_tokenizers.py b/tests/test_tokenizers.py index 6e612e7e80..ef0cb14d1c 100644 --- a/tests/test_tokenizers.py +++ b/tests/test_tokenizers.py @@ -5,11 +5,12 @@ import unittest import pytest -from utils import enable_hf_offline from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_tokenizer +from tests.hf_offline_utils import enable_hf_offline + class TestTokenizers: """ diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index 0ce8785774..e69de29bb2 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -1,85 +0,0 @@ -""" -test utils for helpers and decorators -""" - -import os -from functools import wraps - -from huggingface_hub.utils import reset_sessions - - -def reload_modules(hf_hub_offline): - # Force reload of the modules that check this variable - import importlib - - import datasets - import huggingface_hub.constants - - # Reload the constants module first, as others depend on it - importlib.reload(huggingface_hub.constants) - huggingface_hub.constants.HF_HUB_OFFLINE = hf_hub_offline - importlib.reload(datasets.config) - setattr(datasets.config, "HF_HUB_OFFLINE", hf_hub_offline) - reset_sessions() - - -def enable_hf_offline(test_func): - """ - test decorator that sets HF_HUB_OFFLINE environment variable to True and restores it after the test even if the test fails. - :param test_func: - :return: - """ - - @wraps(test_func) - def wrapper(*args, **kwargs): - # Save the original value of HF_HUB_OFFLINE environment variable - original_hf_offline = os.getenv("HF_HUB_OFFLINE") - - # Set HF_OFFLINE environment variable to True - os.environ["HF_HUB_OFFLINE"] = "1" - - reload_modules(True) - try: - # Run the test function - return test_func(*args, **kwargs) - finally: - # Restore the original value of HF_HUB_OFFLINE environment variable - if original_hf_offline is not None: - os.environ["HF_HUB_OFFLINE"] = original_hf_offline - reload_modules(bool(original_hf_offline)) - else: - del os.environ["HF_HUB_OFFLINE"] - reload_modules(False) - - return wrapper - - -def disable_hf_offline(test_func): - """ - test decorator that sets HF_HUB_OFFLINE environment variable to False and restores it after the wrapped func - :param test_func: - :return: - """ - - @wraps(test_func) - def wrapper(*args, **kwargs): - # Save the original value of HF_HUB_OFFLINE environment variable - original_hf_offline = os.getenv("HF_HUB_OFFLINE") - - # Set HF_OFFLINE environment variable to True - os.environ["HF_HUB_OFFLINE"] = "0" - - reload_modules(False) - try: - # Run the test function - return test_func(*args, **kwargs) - finally: - # Restore the original value of HF_HUB_OFFLINE environment variable - if original_hf_offline is not None: - os.environ["HF_HUB_OFFLINE"] = original_hf_offline - reload_modules(bool(original_hf_offline)) - else: - del os.environ["HF_HUB_OFFLINE"] - reload_modules(False) - - return wrapper From 5410195e0b1d5cc807c5fc4d61d4668d3258aff3 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 31 Mar 2025 09:13:42 -0400 Subject: [PATCH 0497/1405] Sequence parallelism quick follow-ups; remove ModelCallback (#2450) * guard return if ring attn alrady registered * add docs link, bits in multi-gpu docs, remove save model callback (subsumed by HF trainers) * configurable heads_k_stride from ring-flash-attn hf adapter --- _quarto.yml | 1 + docs/config.qmd | 3 +++ docs/multi-gpu.qmd | 23 +++++++++++++++++++ docs/sequence_parallelism.qmd | 11 +++++++-- src/axolotl/core/trainer_builder.py | 3 --- .../monkeypatch/attention/ring_attn.py | 15 ++++++++++-- src/axolotl/utils/callbacks/__init__.py | 21 ----------------- src/axolotl/utils/models.py | 5 +++- src/axolotl/utils/schemas/config.py | 3 ++- tests/e2e/patched/test_sp.py | 2 +- 10 files changed, 56 insertions(+), 31 deletions(-) diff --git a/_quarto.yml b/_quarto.yml index 0a8e023cf6..804fc5e840 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -243,6 +243,7 @@ website: - docs/unsloth.qmd - docs/torchao.qmd - docs/custom_integrations.qmd + - docs/sequence_parallelism.qmd - section: "Troubleshooting" contents: diff --git a/docs/config.qmd b/docs/config.qmd index 71ddcbff6e..753cf47e17 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -658,6 +658,9 @@ ddp_broadcast_buffers: # subsequences, or set to 4 to split into four equal-sized subsequences. # See https://axolotl-ai-cloud.github.io/axolotl/docs/sequence_parallelism.html for more details. sequence_parallel_degree: +# Optional; strides across the key dimension. Larger values use more memory but should make training faster. +# Must evenly divide the number of KV heads in your model. +heads_k_stride: 1 # Path to torch distx for optim 'adamw_anyprecision' torchdistx_path: diff --git a/docs/multi-gpu.qmd b/docs/multi-gpu.qmd index 19293bb5b7..5aec89763e 100644 --- a/docs/multi-gpu.qmd +++ b/docs/multi-gpu.qmd @@ -18,6 +18,7 @@ Axolotl supports several methods for multi-GPU training: - DeepSpeed (recommended) - FSDP (Fully Sharded Data Parallel) +- Sequence parallelism - FSDP + QLoRA ## DeepSpeed {#sec-deepspeed} @@ -66,6 +67,28 @@ fsdp_config: fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer ``` +## Sequence parallelism {#sec-sequence-parallelism} + +We support sequence parallelism (SP) via the +[ring-flash-attention](https://github.com/zhuzilin/ring-flash-attention) project. This +allows one to split up sequences across GPUs, which is useful in the event that a +single sequence causes OOM errors during model training. + +First, install `ring-flash-attn`, recommended via `pip install axolotl[ring-flash-attn]`, +or from source with `pip install .[ring-flash-attn]`. + +Your Axolotl YAML config should contain the following lines: + +```{.yaml} +sequence_parallel_degree: 4 # Split each sequence into 4 parts, one per GPU +flash_attention: true # Required with sequence parallelism + +# Optional; strides across the key dimension. Larger values use more memory but will make training faster. +heads_k_stride: 1 +``` + +See our [dedicated guide](sequence_parallelism.qmd) for more details. + ### FSDP + QLoRA {#sec-fsdp-qlora} For combining FSDP with QLoRA, see our [dedicated guide](fsdp_qlora.qmd). diff --git a/docs/sequence_parallelism.qmd b/docs/sequence_parallelism.qmd index cb297c0e0c..98ca4d746b 100644 --- a/docs/sequence_parallelism.qmd +++ b/docs/sequence_parallelism.qmd @@ -25,6 +25,8 @@ To enable sequence parallelism, add the following to your configuration file: ```yaml # Set to a divisor (> 1) of the number of GPUs available sequence_parallel_degree: 4 # Split sequences across 4 GPUs +# Optional; strides across the key dimension. Larger values use more memory but should make training faster. +heads_k_stride: 1 ``` The `sequence_parallel_degree` should be a divisor of the total number of GPUs. For example: @@ -58,11 +60,16 @@ To use sequence parallelism, you need: ## Example ```yaml -# Example config with sequence parallelism base_model: meta-llama/Llama-3-8B-Instruct sequence_len: 8192 -sequence_parallel_degree: 2 # Split each sequence into 4 parts + +... + +sequence_parallel_degree: 4 # Split each sequence into 4 parts, one per GPU flash_attention: true # Required with sequence parallelism +# Optional; strides across the key dimension. Larger values use more memory but should make training faster. +heads_k_stride: 1 + ... ``` diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index b237b1ef3b..2349932ba7 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -69,7 +69,6 @@ LossWatchDogCallback, SaveAxolotlConfigtoWandBCallback, SaveBetterTransformerModelCallback, - SaveModelCallback, bench_eval_callback_factory, causal_lm_bench_eval_callback_factory, log_prediction_callback_factory, @@ -249,7 +248,6 @@ def get_callbacks(self): if self.cfg.gc_steps: callbacks.append(GCCallback(gc_steps=self.cfg.gc_steps)) - callbacks.append(SaveModelCallback()) return callbacks @@ -937,7 +935,6 @@ class HFRLTrainerBuilder(TrainerBuilderBase): def get_callbacks(self): callbacks = super().get_callbacks() - callbacks.append(SaveModelCallback()) return callbacks diff --git a/src/axolotl/monkeypatch/attention/ring_attn.py b/src/axolotl/monkeypatch/attention/ring_attn.py index a81b879094..6c9d0b429e 100644 --- a/src/axolotl/monkeypatch/attention/ring_attn.py +++ b/src/axolotl/monkeypatch/attention/ring_attn.py @@ -38,13 +38,19 @@ def set_ring_attn_group(ring_attn_group: dist.ProcessGroup | None): RING_ATTN_GROUP = ring_attn_group -def register_ring_attn(sequence_parallel_degree: int): +def register_ring_attn(sequence_parallel_degree: int, heads_k_stride: int | None): """ Create ring attention group and substitute flash attn with ring flash attn. Args: sequence_parallel_degree: Sequence parallelism factor. + heads_k_stride: Sequence parallelism K head stride size. Passed + through to `ring_flash_attn.substitute_hf_flash_attn`. """ + if get_ring_attn_group() is not None: + LOG.info("Ring attention already registered, exiting early...") + return + LOG.info( "Enabling ring attention sequence parallelism: " f"each sequence will be processed across {sequence_parallel_degree} GPUs" @@ -84,6 +90,11 @@ def register_ring_attn(sequence_parallel_degree: int): if rank == 0: LOG.info(f"Sequence parallel group assignments: {group_assignments}") + if heads_k_stride is None: + heads_k_stride = 1 + from ring_flash_attn import substitute_hf_flash_attn - substitute_hf_flash_attn(get_ring_attn_group(), sequence_parallel_degree) + substitute_hf_flash_attn( + process_group=get_ring_attn_group(), heads_k_stride=heads_k_stride + ) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 94f180ef46..ffe4699f8f 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -816,27 +816,6 @@ def on_train_begin( return control -class SaveModelCallback(TrainerCallback): - """Callback to save model on train end""" - - def on_step_end( # pylint: disable=unused-argument - self, - args: TrainingArguments, - state: TrainerState, - control: TrainerControl, - **kwargs, - ): - # Save - if state.global_step >= state.max_steps: - control.should_save = True - - def on_train_end( # pylint: disable=unused-argument - self, args, state, control, **kwargs - ): - control.should_save = True - return control - - class GCCallback(TrainerCallback): """Callback to garbage collect torch cache""" diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 10c171d83e..9611ffca23 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -609,7 +609,10 @@ def apply_patches(self) -> None: # Initialize ring attn for sequence parallelism. This must be done after # model init but before the first forward pass, since it modifies flash # attn to use ring comm for SP training across multiple GPUs. - register_ring_attn(self.cfg.sequence_parallel_degree) + register_ring_attn( + sequence_parallel_degree=self.cfg.sequence_parallel_degree, + heads_k_stride=self.cfg.heads_k_stride, + ) def patch_attention(self) -> None: if hasattr(self.model_config, "model_type"): diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index d52146092f..51c5cf08e3 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -248,6 +248,7 @@ class AxolotlInputConfig( val_set_size: float | None = Field(default=0.0) sequence_parallel_degree: int | None = None + heads_k_stride: int | None = None special_tokens: SpecialTokensConfig | None = None tokens: list[str] | None = None @@ -1108,7 +1109,7 @@ def check_kto_config(cls, data): @field_validator("sequence_parallel_degree", mode="before") @classmethod - def check_sequence_parallel_config(cls, value, info): + def check_sequence_parallel_degree(cls, value, info): if not value: value = 1 diff --git a/tests/e2e/patched/test_sp.py b/tests/e2e/patched/test_sp.py index 903cb14508..70beb8a543 100644 --- a/tests/e2e/patched/test_sp.py +++ b/tests/e2e/patched/test_sp.py @@ -110,7 +110,7 @@ def test_register_ring_attn( mock_new_group.return_value = mock_group # Call register_ring_attn with size 4 - register_ring_attn(sequence_parallel_degree=4) + register_ring_attn(sequence_parallel_degree=4, heads_k_stride=1) # Verify the number of calls without examining the arguments assert mock_new_group.call_count == 2 From ef6eb77cc89728e765c2b6fc75a958347708907d Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 31 Mar 2025 12:36:47 -0400 Subject: [PATCH 0498/1405] destroy process group on Ctrl+C / training or eval run (#2457) * fix nccl pg destroy warning * update --- src/axolotl/evaluate.py | 3 +++ src/axolotl/train.py | 24 +++++++++--------------- src/axolotl/utils/distributed.py | 16 ++++++++++++++-- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/axolotl/evaluate.py b/src/axolotl/evaluate.py index f3be9c2f45..216ff0110e 100644 --- a/src/axolotl/evaluate.py +++ b/src/axolotl/evaluate.py @@ -15,6 +15,7 @@ from axolotl.train import TrainDatasetMeta from axolotl.utils import set_pytorch_cuda_alloc_conf from axolotl.utils.dict import DictDefault +from axolotl.utils.distributed import cleanup_distributed from axolotl.utils.models import load_model, load_processor, load_tokenizer from axolotl.utils.trainer import setup_trainer @@ -159,4 +160,6 @@ def evaluate(*, cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> Dict[str, f del model del tokenizer + cleanup_distributed() + return all_metrics diff --git a/src/axolotl/train.py b/src/axolotl/train.py index a6040ebaa9..56e378c7ff 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -27,6 +27,7 @@ from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder from axolotl.logging_config import configure_logging from axolotl.utils.dict import DictDefault +from axolotl.utils.distributed import cleanup_distributed from axolotl.utils.freeze import freeze_layers_except from axolotl.utils.models import load_model, load_processor, load_tokenizer from axolotl.utils.trainer import setup_trainer @@ -157,6 +158,8 @@ def terminate_handler(_, __, model_weakref): _model.save_pretrained( cfg.output_dir, safe_serialization=safe_serialization ) + + cleanup_distributed() sys.exit(0) _model_weakref = weakref.ref(model) @@ -478,7 +481,7 @@ def train( Returns: Tuple of (model, tokenizer) after training """ - # Setup model, tokenizer, (causal or RLHF) trainer etc. + # Setup model, tokenizer, (causal or RLHF) trainer, etc. ( trainer, model, @@ -487,34 +490,25 @@ def train( processor, ) = setup_model_and_trainer(cfg, dataset_meta) - # Determine if we need to resume from a checkpoint - resume_from_checkpoint = determine_resume_checkpoint(cfg) - - # Configuration for saving - safe_serialization = cfg.save_safetensors is True - # Handle untrained tokens if configured + safe_serialization = cfg.save_safetensors is True train_dataset = dataset_meta.train_dataset handle_untrained_tokens_fix( cfg, model, tokenizer, train_dataset, safe_serialization ) - # Save initial configs + # Additional setup save_initial_configs(cfg, tokenizer, model, peft_config, processor) - - # Set up signal handler for graceful termination setup_signal_handler(cfg, model, safe_serialization) - - # Set up badges and config info for model card setup_model_card(cfg) # Execute the training + resume_from_checkpoint = determine_resume_checkpoint(cfg) execute_training(cfg, trainer, resume_from_checkpoint) - # Save the trained model + # Save the trained model and cleanup save_trained_model(cfg, trainer, model, safe_serialization) - - # Create model card create_model_card(cfg, trainer) + cleanup_distributed() return model, tokenizer, trainer diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index 7c671abfe5..acd2566d0d 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -71,8 +71,8 @@ def barrier(): def is_main_process(): """ - Check if the current process is the main process. - If not in distributed mode, always return True. + Check if the current process is the main process. If not in distributed mode, + always return `True`. """ if not is_distributed(): return True @@ -87,6 +87,18 @@ def get_world_size(): return int(os.getenv("WORLD_SIZE", "1")) +def cleanup_distributed(): + """ + Destroy process group if torch distributed is initialized. Called in training early + termination or when training successfully completes. + """ + # Ensure that all operations are completed before destroying the process group + torch.cuda.synchronize() + # Destroy the process group + if torch.distributed.is_initialized(): + torch.distributed.destroy_process_group() + + @contextmanager def zero_only(): """ From b35992262e88dd6babeddc2c5ba5786264044a9f Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 31 Mar 2025 15:17:43 -0400 Subject: [PATCH 0499/1405] Ray train bugfix (#2458) * fix nccl pg destroy warning * update * ray bugfix --- src/axolotl/train.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 56e378c7ff..89f35d7eb0 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -509,6 +509,7 @@ def train( # Save the trained model and cleanup save_trained_model(cfg, trainer, model, safe_serialization) create_model_card(cfg, trainer) - cleanup_distributed() + if not cfg.use_ray: + cleanup_distributed() return model, tokenizer, trainer From b6fc46ada8ee41c18a37c9faf1ebd820c5fe1b9b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 31 Mar 2025 15:47:11 -0400 Subject: [PATCH 0500/1405] Updates for trl 0.16.0 - mostly for GRPO (#2437) [skip ci] * add grpo scale_rewards config for trl#3135 * options to connect to vllm server directly w grpo trl#3094 * temperature support trl#3029 * sampling/generation kwargs for grpo trl#2989 * make vllm_enable_prefix_caching a config param trl#2900 * grpo multi-step optimizeations trl#2899 * remove overrides for grpo trainer * bump trl to 0.16.0 * add cli to start vllm-serve via trl * call the python module directly * update to use vllm with 2.6.0 too now and call trl vllm serve from module * vllm 0.8.1 * use python3 * use sys.executable * remove context and wait for start * fixes to make it actually work * fixes so the grpo tests pass with new vllm paradigm * explicit host/port and check in start vllm * make sure that vllm doesn't hang by setting quiet so outouts go to dev null * also bump bnb to latest release * add option for wait from cli and nccl debugging for ci * grpo + vllm test on separate devices for now * make sure grpo + vllm tests runs single worker since pynccl comms would conflict * fix cli * remove wait and add caching for argilla dataset * refactoring configs * chore: lint * add vllm config * fixup vllm grpo args * fix one more incorrect schema/config path * fix another vlllm reference and increase timeout * make the tests run a bit faster * change mbsz back so it is correct for grpo * another change mbsz back so it is correct for grpo * fixing cli args * nits * adding docs * docs * include tensor parallel size for vllm in pydantic schema * moving start_vllm, more docs * limit output len for grpo vllm * vllm enable_prefix_caching isn't a bool cli arg * fix env ordering in tests and also use pid check when looking for vllm --------- Co-authored-by: Salman Mohammadi --- .github/workflows/multi-gpu-e2e.yml | 3 +- .github/workflows/tests.yml | 2 +- _quarto.yml | 1 + cicd/multigpu.sh | 3 +- docs/config.qmd | 8 +- docs/rlhf.qmd | 43 ++- requirements.txt | 4 +- setup.py | 83 +++--- src/axolotl/cli/args.py | 49 ++++ src/axolotl/cli/main.py | 16 +- src/axolotl/cli/vllm_serve.py | 55 ++++ src/axolotl/core/trainers/grpo/__init__.py | 36 ++- src/axolotl/core/trainers/grpo/trainer.py | 136 ++++------ src/axolotl/utils/schemas/config.py | 4 + src/axolotl/utils/schemas/trl.py | 74 +++++- src/axolotl/utils/schemas/vllm.py | 38 +++ tests/conftest.py | 8 + tests/e2e/multigpu/solo/__init__.py | 0 tests/e2e/multigpu/solo/test_grpo.py | 294 +++++++++++++++++++++ tests/e2e/multigpu/test_eval.py | 8 +- tests/e2e/multigpu/test_grpo.py | 175 ------------ tests/e2e/multigpu/test_llama.py | 6 +- tests/e2e/multigpu/test_qwen2.py | 2 +- tests/e2e/multigpu/test_ray.py | 2 +- 24 files changed, 702 insertions(+), 348 deletions(-) create mode 100644 src/axolotl/cli/vllm_serve.py create mode 100644 src/axolotl/utils/schemas/vllm.py create mode 100644 tests/e2e/multigpu/solo/__init__.py create mode 100644 tests/e2e/multigpu/solo/test_grpo.py delete mode 100644 tests/e2e/multigpu/test_grpo.py diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 4934bf9cad..dfa3156188 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -42,8 +42,7 @@ jobs: cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.6.0 - # awaiting vllm#12721 - axolotl_extras: + axolotl_extras: vllm num_gpus: 2 nightly_build: "true" runs-on: [self-hosted, modal] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ad6305e8f3..a1a7214eca 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -256,7 +256,7 @@ jobs: python_version: "3.11" pytorch: 2.6.0 num_gpus: 1 - axolotl_extras: + axolotl_extras: vllm steps: - name: Checkout uses: actions/checkout@v4 diff --git a/_quarto.yml b/_quarto.yml index 804fc5e840..cf85da473a 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -40,6 +40,7 @@ quartodoc: - cli.preprocess - cli.sweeps - cli.utils + - cli.vllm_serve - cli.cloud.base - cli.cloud.modal_ - title: Trainers diff --git a/cicd/multigpu.sh b/cicd/multigpu.sh index 05d1bbbf2a..84dfc6f717 100755 --- a/cicd/multigpu.sh +++ b/cicd/multigpu.sh @@ -2,4 +2,5 @@ set -e # only run one test at a time so as not to OOM the GPU -pytest -v -n2 /workspace/axolotl/tests/e2e/multigpu/ +pytest -v -n2 /workspace/axolotl/tests/e2e/multigpu/ --ignore=/workspace/axolotl/tests/e2e/multigpu/solo/ +pytest -v -n1 /workspace/axolotl/tests/e2e/multigpu/solo/ diff --git a/docs/config.qmd b/docs/config.qmd index 753cf47e17..b0c8616a23 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -238,10 +238,10 @@ simpo_gamma: 0.5 # Target reward margin for the SimPO loss # grpo trl: use_vllm: # Optional[bool]. Whether to use VLLM for RL training. - vllm_device: # Optional[str]. Device to use for VLLM. - vllm_gpu_memory_utilization: # Optional[float]. GPU memory utilization for VLLM. - vllm_max_model_len: # Optional[int]. Maximum length of the model for VLLM. - vllm_dtype: # Optional[str]. Data type for VLLM. + vllm_server_host: # Optional[str]. Host of the vLLM server to connect to. + vllm_server_port: # Optional[int]. Port of the vLLM server to connect to. + vllm_server_timeout: # Optional[int]. Total timeout (in seconds) to wait for the vLLM server to respond. + vllm_guided_decoding_regex: # Optional[str]. Regex for vLLM guided decoding. beta: # Optional[float]. Beta parameter for the RL training. Same as `rl_beta`. Use max_completion_length: # Optional[int]. Maximum length of the completion for RL training. diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 6bef7c8318..b3adb5937c 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -502,9 +502,48 @@ The input format is a simple JSON input with customizable fields based on the ab Check out our [GRPO cookbook](https://github.com/axolotl-ai-cloud/axolotl-cookbook/tree/main/grpo#training-an-r1-style-large-language-model-using-grpo). ::: +If you have multiple GPUs available, we reccomend using `vLLM` with the `GRPOTrainer` to significantly speedup trajectory generation during training. +First, launch a `vLLM` server using `trl vllm-serve` - you may use a config file or CLI overrides to configure your vLLM server. In this example, we're +using 4 GPUs - 2 for training, and 2 for vLLM: + +::: {.callout-important} +Make sure you've installed the correct version of vLLM by including it as an extra when installing axolotl, e.g. `pip install axolotl[vllm]`. +::: + +```yaml +base_model: Qwen/Qwen2.5-1.5B-Instruct + +vllm: + host: 0.0.0.0 + port: 8000 + tensor_parallel_size: 2 + gpu_memory_utilization: 0.85 + dtype: auto + # max_model_len: # you may find it useful to set the vLLM model context length if you know this beforehand + +rl: grpo +trl: + use_vllm: true + vllm_server_host: 0.0.0.0 + vllm_server_port: 8000 + vllm_server_timeout: 300 +``` + +```bash +CUDA_VISIBLE_DEVICES=2,3 axolotl vllm_serve grpo.yaml +``` + +Your `vLLM` instance will now attempt to spin up, and it's time to kick off training utilizing our remaining two GPUs. In another terminal, execute: + +```bash +CUDA_VISIBLE_DEVICES=0,1 axolotl train grpo.yaml --num-processes 2 +``` + +#### Reward functions + GRPO uses custom reward functions and transformations. Please have them ready locally. -For ex, to load OpenAI's GSM8K and use a random reward for completions: +For example, to load OpenAI's GSM8K and use a random reward for completions: ```python # rewards.py @@ -530,8 +569,6 @@ trl: beta: 0.001 max_completion_length: 256 use_vllm: True - vllm_device: auto - vllm_gpu_memory_utilization: 0.15 num_generations: 4 reward_funcs: ["rewards.rand_reward_func"] # format: '{file_name}.{fn_name}' reward_weights: [1.0] diff --git a/requirements.txt b/requirements.txt index 096237f192..9aff0ccfe4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ # START section of dependencies that don't install on Darwin/MacOS -bitsandbytes==0.45.3 +bitsandbytes==0.45.4 triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 @@ -17,7 +17,7 @@ tokenizers>=0.21.1 accelerate==1.5.2 datasets==3.5.0 deepspeed==0.16.4 -trl==0.15.1 +trl==0.16.0 optimum==1.16.2 hf_transfer diff --git a/setup.py b/setup.py index 4c3024b852..d19c14828c 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ from setuptools import find_packages, setup -def parse_requirements(): +def parse_requirements(extras_require_map): _install_requires = [] _dependency_links = [] with open("./requirements.txt", encoding="utf-8") as requirements_file: @@ -67,6 +67,7 @@ def parse_requirements(): if (major, minor) >= (2, 6): _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers==0.0.29.post2") + extras_require_map["vllm"] = ["vllm==0.8.1"] elif (major, minor) >= (2, 5): _install_requires.pop(_install_requires.index(xformers_version)) if patch == 0: @@ -86,7 +87,7 @@ def parse_requirements(): except PackageNotFoundError: pass - return _install_requires, _dependency_links + return _install_requires, _dependency_links, extras_require_map def get_package_version(): @@ -103,7 +104,46 @@ def get_package_version(): return version_ -install_requires, dependency_links = parse_requirements() +extras_require = { + "flash-attn": ["flash-attn==2.7.4.post1"], + "ring-flash-attn": ["ring-flash-attn>=0.1.4", "yunchang==0.6.0"], + "deepspeed": [ + "deepspeed==0.16.4", + "deepspeed-kernels", + ], + "mamba-ssm": [ + "mamba-ssm==1.2.0.post1", + "causal_conv1d", + ], + "auto-gptq": [ + "auto-gptq==0.5.1", + ], + "mlflow": [ + "mlflow", + ], + "galore": [ + "galore_torch", + ], + "apollo": [ + "apollo-torch", + ], + "optimizers": [ + "galore_torch", + "apollo-torch", + "lomo-optim==0.1.1", + "torch-optimi==0.2.1", + ], + "ray": [ + "ray[train]", + ], + "vllm": [ + "vllm==0.7.2", + ], +} + +install_requires, dependency_links, extras_require_build = parse_requirements( + extras_require +) setup( version=get_package_version(), @@ -116,40 +156,5 @@ def get_package_version(): "axolotl=axolotl.cli.main:main", ], }, - extras_require={ - "flash-attn": ["flash-attn==2.7.4.post1"], - "ring-flash-attn": ["ring-flash-attn>=0.1.4", "yunchang==0.6.0"], - "deepspeed": [ - "deepspeed==0.16.4", - "deepspeed-kernels", - ], - "mamba-ssm": [ - "mamba-ssm==1.2.0.post1", - "causal_conv1d", - ], - "auto-gptq": [ - "auto-gptq==0.5.1", - ], - "mlflow": [ - "mlflow", - ], - "galore": [ - "galore_torch", - ], - "apollo": [ - "apollo-torch", - ], - "optimizers": [ - "galore_torch", - "apollo-torch", - "lomo-optim==0.1.1", - "torch-optimi==0.2.1", - ], - "ray": [ - "ray[train]", - ], - "vllm": [ - "vllm==0.7.2", - ], - }, + extras_require=extras_require_build, ) diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py index a39ffc3080..72e61d1bb8 100644 --- a/src/axolotl/cli/args.py +++ b/src/axolotl/cli/args.py @@ -35,6 +35,55 @@ class TrainerCliArgs: num_processes: Optional[int] = field(default=None) +@dataclass +class VllmServeCliArgs: + """Dataclass with CLI arguments for `axolotl vllm-serve` command.""" + + tensor_parallel_size: int = field( + default=1, + metadata={"help": "Number of tensor parallel workers to use."}, + ) + host: str = field( + default="0.0.0.0", # nosec B104 + metadata={"help": "Host address to run the server on."}, + ) + port: int = field( + default=8000, + metadata={"help": "Port to run the server on."}, + ) + gpu_memory_utilization: Optional[float] = field( + default=None, + metadata={ + "help": "Ratio (between 0 and 1) of GPU memory to reserve for the model weights, activations, and KV " + "cache on the device dedicated to generation powered by vLLM. Higher values will increase the KV cache " + "size and thus improve the model's throughput. However, if the value is too high, it may cause " + "out-of-memory (OOM) errors during initialization." + }, + ) + dtype: Optional[str] = field( + default=None, + metadata={ + "help": "Data type to use for vLLM generation. If set to 'auto', the data type will be automatically " + "determined based on the model configuration. Find the supported values in the vLLM documentation." + }, + ) + max_model_len: Optional[int] = field( + default=None, + metadata={ + "help": "If set, the `max_model_len` to use for vLLM. This can be useful when running with reduced " + "`vllm_gpu_memory_utilization`, leading to a reduced KV cache size. If not set, vLLM will use the model " + "context size, which might be much larger than the KV cache, leading to inefficiencies." + }, + ) + enable_prefix_caching: Optional[bool] = field( + default=None, + metadata={ + "help": "Whether to enable prefix caching in vLLM. If set to `True`, ensure that the model and the " + "hardware support this feature." + }, + ) + + @dataclass class EvaluateCliArgs: """Dataclass with CLI arguments for `axolotl evaluate` command.""" diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index f53ea825a8..7532a9689a 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -14,7 +14,12 @@ from dotenv import load_dotenv import axolotl -from axolotl.cli.args import EvaluateCliArgs, PreprocessCliArgs, TrainerCliArgs +from axolotl.cli.args import ( + EvaluateCliArgs, + PreprocessCliArgs, + TrainerCliArgs, + VllmServeCliArgs, +) from axolotl.cli.sweeps import generate_sweep_configs from axolotl.cli.utils import ( add_options_from_config, @@ -23,6 +28,7 @@ fetch_from_github, filter_none_kwargs, ) +from axolotl.cli.vllm_serve import do_vllm_serve from axolotl.integrations.lm_eval.cli import lm_eval from axolotl.utils import set_pytorch_cuda_alloc_conf from axolotl.utils.schemas.config import AxolotlInputConfig @@ -316,6 +322,14 @@ def fetch(directory: str, dest: Optional[str]) -> None: fetch_from_github(f"{directory}/", dest) +@cli.command() +@click.argument("config", type=click.Path(exists=True, path_type=str)) +@add_options_from_dataclass(VllmServeCliArgs) +@filter_none_kwargs +def vllm_serve(config: str, **cli_args: VllmServeCliArgs): + do_vllm_serve(config, cli_args) + + cli.add_command(lm_eval) diff --git a/src/axolotl/cli/vllm_serve.py b/src/axolotl/cli/vllm_serve.py new file mode 100644 index 0000000000..552f33e9e0 --- /dev/null +++ b/src/axolotl/cli/vllm_serve.py @@ -0,0 +1,55 @@ +""" +CLI to start the vllm server for online RL +""" + +from pathlib import Path +from typing import Union + +from trl.scripts.vllm_serve import ScriptArguments +from trl.scripts.vllm_serve import main as vllm_serve_main + +from axolotl.cli.config import load_cfg + + +def do_vllm_serve( + config: Union[Path, str], + cli_args: dict, +): + """ + Starts the VLLM server for serving LLM models used for online RL + + Args + :param cfg: Parsed doct of the YAML config + :param cli_args: dict of additional command-line arguments of type VllmServeCliArgs + + Returns: + process_id: the process id of the started VLLM server + """ + cfg = load_cfg(config) + model = cfg.base_model + + tensor_parallel_size = ( + cli_args.get("tensor_parallel_size") or cfg.vllm.tensor_parallel_size + ) + host = cli_args.get("host") or cfg.vllm.host + port = cli_args.get("port") or cfg.vllm.port + gpu_memory_utilization = ( + cli_args.get("gpu_memory_utilization") or cfg.vllm.gpu_memory_utilization + ) + dtype = cli_args.get("dtype") or cfg.vllm.dtype + max_model_len = cli_args.get("max_model_len") or cfg.vllm.max_model_len + enable_prefix_caching = ( + cli_args.get("enable_prefix_caching") or cfg.vllm.enable_prefix_caching + ) + + vllm_script_args = ScriptArguments( + model, + tensor_parallel_size=tensor_parallel_size, + host=host, + port=port, + gpu_memory_utilization=gpu_memory_utilization, + dtype=dtype, + max_model_len=max_model_len, + enable_prefix_caching=enable_prefix_caching, + ) + vllm_serve_main(vllm_script_args) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index f0c42830d0..219eced69d 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -40,18 +40,15 @@ def set_training_args_kwargs(cls, cfg): if trl.use_vllm: grpo_args_kwargs["use_vllm"] = trl.use_vllm - grpo_args_kwargs["vllm_device"] = ( - trl.vllm_device if trl.vllm_device else "auto" - ) - - if trl.vllm_gpu_memory_utilization: - grpo_args_kwargs["vllm_gpu_memory_utilization"] = ( - trl.vllm_gpu_memory_utilization + grpo_args_kwargs["vllm_server_host"] = trl.vllm_server_host + grpo_args_kwargs["vllm_server_port"] = trl.vllm_server_port + if trl.vllm_server_timeout: + grpo_args_kwargs["vllm_server_timeout"] = trl.vllm_server_timeout + if trl.vllm_guided_decoding_regex: + grpo_args_kwargs["vllm_guided_decoding_regex"] = ( + trl.vllm_guided_decoding_regex ) - if trl.vllm_max_model_len: - grpo_args_kwargs["vllm_max_model_len"] = trl.vllm_max_model_len - if trl.num_generations: grpo_args_kwargs["num_generations"] = trl.num_generations @@ -70,6 +67,25 @@ def set_training_args_kwargs(cls, cfg): if trl.reward_weights: grpo_args_kwargs["reward_weights"] = trl.reward_weights + if trl.scale_rewards is not None: + grpo_args_kwargs["scale_rewards"] = trl.scale_rewards + + if trl.temperature is not None: + grpo_args_kwargs["temperature"] = trl.temperature + if trl.top_p is not None: + grpo_args_kwargs["top_p"] = trl.top_p + if trl.top_k is not None: + grpo_args_kwargs["top_k"] = trl.top_k + if trl.min_p is not None: + grpo_args_kwargs["min_p"] = trl.min_p + if trl.repetition_penalty is not None: + grpo_args_kwargs["repetition_penalty"] = trl.repetition_penalty + + if trl.num_iterations is not None: + grpo_args_kwargs["num_iterations"] = trl.num_iterations + if trl.epsilon is not None: + grpo_args_kwargs["epsilon"] = trl.epsilon + return grpo_args_kwargs @classmethod diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index 663bed094b..e8a142945c 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -2,16 +2,18 @@ Axolotl GRPO trainer """ -from accelerate.utils import is_peft_model -from accelerate.utils.other import is_compiled_module -from transformers import PreTrainedModel -from trl import GRPOConfig, GRPOTrainer -from trl.models import unwrap_model_for_generation +from contextlib import nullcontext + +from accelerate.utils import is_deepspeed_available, is_peft_model +from trl import GRPOTrainer +from trl.extras.profiling import profiling_decorator from axolotl.core.trainers.base import SchedulerMixin +if is_deepspeed_available(): + import deepspeed + -# mypy: ignore-errors class AxolotlGRPOTrainer(SchedulerMixin, GRPOTrainer): """ Extend the base GRPOTrainer for axolotl helpers @@ -19,91 +21,49 @@ class AxolotlGRPOTrainer(SchedulerMixin, GRPOTrainer): _tag_names = ["trl", "grpo", "axolotl"] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # pylint: disable=access-member-before-definition - # Enable gradient checkpointing if requested - if kwargs["args"].gradient_checkpointing: - # Ensure use_cache is disabled - if hasattr(self.model, "config"): - self.model.config.use_cache = False - - # Enable gradient checkpointing on the base model for PEFT - if is_peft_model(self.model) and hasattr( - self.model.base_model, "gradient_checkpointing_enable" - ): - self.model.base_model.gradient_checkpointing_enable() - # Enable gradient checkpointing for non-PEFT models - elif hasattr(self.model, "gradient_checkpointing_enable"): - self.model.gradient_checkpointing_enable() - self.model = self._enable_gradient_checkpointing(self.model, kwargs["args"]) - # pylint: enable=access-member-before-definition - - def _enable_gradient_checkpointing( - self, model: PreTrainedModel, args: GRPOConfig - ) -> PreTrainedModel: - """Enables gradient checkpointing for the model.""" - # pylint: disable=unused-argument,redefined-builtin - gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} - use_reentrant = ( - "use_reentrant" not in gradient_checkpointing_kwargs - or gradient_checkpointing_kwargs["use_reentrant"] + @profiling_decorator + def _move_model_to_vllm(self): + # For DeepSpeed ZeRO-3, we need to gather all parameters before operations + deepspeed_plugin = self.accelerator.state.deepspeed_plugin + zero_stage_3 = deepspeed_plugin is not None and deepspeed_plugin.zero_stage == 3 + gather_if_zero3 = ( + deepspeed.zero.GatheredParameters if zero_stage_3 else nullcontext ) - if use_reentrant: - if hasattr(model, "enable_input_require_grads"): - model.enable_input_require_grads() - else: + if is_peft_model(self.model): + # With PEFT and DeepSpeed ZeRO Stage 3, we must gather the full model at once before merging, as merging + # adapters in a sharded manner is not supported. + with gather_if_zero3(list(self.model.parameters())): + self.model.merge_adapter() - def make_inputs_require_grad(module, input, output): - output.requires_grad_(True) + # Update vLLM weights while parameters are gathered + for name, param in self.model.named_parameters(): + # When using PEFT, we need to recover the original parameter name and discard some parameters + name = ( + name.removeprefix("base_model.model.") + .removeprefix("base_model.model.") + .replace(".base_layer", "") + ) + if self.model.prefix in name: + continue + # When module to save, remove its prefix and discard the original module + if "original_module" in name: + continue + name = name.replace("modules_to_save.default.", "") - model.get_input_embeddings().register_forward_hook( - make_inputs_require_grad - ) + if self.accelerator.is_main_process: + self.vllm_client.update_named_param(name, param.data) - return model - # pylint: enable=unused-argument,redefined-builtin + # Unmerge adapters while parameters are still gathered + self.model.unmerge_adapter() + # Parameters will automatically be repartitioned when exiting the context + else: + # For non-PEFT models, simply gather and update each parameter individually. + for name, param in self.model.named_parameters(): + with gather_if_zero3([param]): + if self.accelerator.is_main_process: + self.vllm_client.update_named_param(name, param.data) - def _move_model_to_vllm(self): - with unwrap_model_for_generation( - self.model, - self.accelerator, - gather_deepspeed3_params=self.args.ds3_gather_for_generation, - ) as unwrapped_model: - if is_compiled_module(unwrapped_model): - unwrapped_model = ( - unwrapped_model._orig_mod # pylint: disable=protected-access - ) - if is_peft_model(unwrapped_model): - unwrapped_model.merge_adapter() - state_dict = unwrapped_model.state_dict() - # Remove base_model and base_layer prefixes - state_dict = { - k.removeprefix("base_model.model.") - .removeprefix("base_model.model.") - .replace(".base_layer", ""): v - for k, v in state_dict.items() - } - # Remove values with adapter prefix (example: "_lora") - state_dict = { - k: v - for k, v in state_dict.items() - if unwrapped_model.prefix not in k - } - # When module to save, remove its prefix and discard the original module - state_dict = { - k.replace("modules_to_save.default.", ""): v - for k, v in state_dict.items() - if "original_module" not in k - } - else: - state_dict = unwrapped_model.state_dict() - if self.accelerator.is_main_process: - llm_model = ( - self.llm.llm_engine.model_executor.driver_worker.model_runner.model - ) - llm_model.load_weights(state_dict.items()) - if is_peft_model(unwrapped_model): - unwrapped_model.unmerge_adapter() + # Reset cache on main process + if self.accelerator.is_main_process: + self.vllm_client.reset_prefix_cache() diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 51c5cf08e3..c7be33ab3b 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -46,6 +46,7 @@ from axolotl.utils.schemas.peft import LoraConfig, ReLoRAConfig from axolotl.utils.schemas.training import HyperparametersConfig from axolotl.utils.schemas.trl import TRLConfig +from axolotl.utils.schemas.vllm import VllmConfig LOG = logging.getLogger(__name__) @@ -86,6 +87,9 @@ class AxolotlInputConfig( trl: TRLConfig | None = Field( default_factory=lambda: TRLConfig(), # pylint: disable=unnecessary-lambda ) + vllm: VllmConfig | None = Field( + default_factory=lambda: VllmConfig(), # pylint: disable=unnecessary-lambda + ) reward_model: bool | None = None process_reward_model: bool | None = None num_labels: int | None = None diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index 60759769dc..a051fb0abf 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -20,27 +20,30 @@ class TRLConfig(BaseModel): ) # GRPO specific args - # Ref: https://github.com/huggingface/trl/blob/e3244d2d096ff1e2e248c931d06d39e165e20623/trl/trainer/grpo_config.py#L22 - use_vllm: bool | None = Field( + # Ref: https://github.com/huggingface/trl/blob/26d86757a7c7e24e397ea44f57ecce6031dfac01/trl/trainer/grpo_config.py#L23 + use_vllm: bool = Field( default=False, json_schema_extra={"description": "Whether to use VLLM for RL training"}, ) - vllm_device: str | None = Field( - default="auto", - json_schema_extra={"description": "Device to use for VLLM"}, + vllm_server_host: str | None = Field( + default="0.0.0.0", # nosec B104 + json_schema_extra={"description": "Host of the vLLM server to connect to"}, ) - vllm_gpu_memory_utilization: float | None = Field( - default=0.9, - json_schema_extra={"description": "GPU memory utilization for VLLM"}, + vllm_server_port: int | None = Field( + default=8000, + json_schema_extra={"description": "Port of the vLLM server to connect to"}, ) - vllm_dtype: str | None = Field( - default="auto", - json_schema_extra={"description": "Data type for VLLM"}, + vllm_server_timeout: int | None = Field( + default=None, + json_schema_extra={ + "description": "Total timeout duration in seconds to wait for the vLLM server to be up. If the server is not up " + "after the timeout, a `ConnectionError` is raised." + }, ) - vllm_max_model_len: int | None = Field( + vllm_guided_decoding_regex: str | None = Field( default=None, json_schema_extra={ - "description": "Maximum length of the model context for VLLM" + "description": "Regex for vLLM guided decoding. If `None` (default), guided decoding is disabled." }, ) @@ -85,3 +88,48 @@ class TRLConfig(BaseModel): "description": "Sync steps for the reference model. Requires `sync_ref_model=True`." }, ) + scale_rewards: bool = Field( + default=True, + json_schema_extra={ + "description": "Whether to scale the rewards for GRPO by dividing them by their standard deviation." + }, + ) + + temperature: float | None = Field( + default=None, + json_schema_extra={"description": "Sampling temperature for the GRPO policy."}, + ) + top_p: float | None = Field( + default=None, + json_schema_extra={ + "description": "Top-p sampling probability for the generation policy." + }, + ) + top_k: int | None = Field( + default=None, + json_schema_extra={"description": "Top-k sampling for the generation policy."}, + ) + min_p: float | None = Field( + default=None, + json_schema_extra={ + "description": "Minimum probability for the generation policy." + }, + ) + repetition_penalty: float | None = Field( + default=None, + json_schema_extra={ + "description": "Float that penalizes new tokens based on whether they appear in the prompt and the generated text so far." + }, + ) + num_iterations: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of iterations per batch (denoted as μ in the algorithm) for GRPO." + }, + ) + epsilon: float | None = Field( + default=None, + json_schema_extra={ + "description": "Epsilon value for clipping in the GRPO algorithm." + }, + ) diff --git a/src/axolotl/utils/schemas/vllm.py b/src/axolotl/utils/schemas/vllm.py new file mode 100644 index 0000000000..bb1a4ba265 --- /dev/null +++ b/src/axolotl/utils/schemas/vllm.py @@ -0,0 +1,38 @@ +""" +Pydantic models for VLLM configuration, used primarily for RL training with TRL + grpo +""" + +from pydantic import BaseModel, Field + + +class VllmConfig(BaseModel): + """ + Configuration for VLLM server + """ + + device: str | None = Field( + default="auto", + json_schema_extra={"description": "Device to use for VLLM"}, + ) + tensor_parallel_size: int | None = Field( + default=None, + json_schema_extra={"description": "Tensor parallel size for VLLM"}, + ) + gpu_memory_utilization: float | None = Field( + default=0.9, + json_schema_extra={"description": "GPU memory utilization for VLLM"}, + ) + dtype: str | None = Field( + default="auto", + json_schema_extra={"description": "Data type for VLLM"}, + ) + max_model_len: int | None = Field( + default=None, + json_schema_extra={ + "description": "Maximum length of the model context for VLLM" + }, + ) + enable_prefix_caching: bool | None = Field( + default=None, + json_schema_extra={"description": "Enable prefix caching for VLLM"}, + ) diff --git a/tests/conftest.py b/tests/conftest.py index aa867ecb94..b86b714afd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,6 +100,14 @@ def download_argilla_distilabel_capybara_dpo_7k_binarized_dataset(): ) +@pytest.fixture(scope="session", autouse=True) +def download_argilla_distilabel_intel_orca_dpo_dataset(): + # download the dataset + snapshot_download_w_retry( + "argilla/distilabel-intel-orca-dpo-pairs", repo_type="dataset" + ) + + @pytest.fixture(scope="session", autouse=True) def download_argilla_ultrafeedback_binarized_preferences_cleaned_dataset(): # download the dataset diff --git a/tests/e2e/multigpu/solo/__init__.py b/tests/e2e/multigpu/solo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py new file mode 100644 index 0000000000..bd999e2f3b --- /dev/null +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -0,0 +1,294 @@ +""" +GRPO test suite +""" + +import os +import random +import subprocess # nosec B404 +import sys +import time +from pathlib import Path + +import pytest +import requests +import yaml +from accelerate.test_utils import execute_subprocess_async +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import require_vllm + + +def start_vllm( + model: str, env: dict | None = None, wait: int | None = None, quiet=False, **kwargs +) -> int: + """ + helper function to start the VLLM server in the background, mostly for testing purposes + """ + cmd = [sys.executable, "-m", "trl.scripts.vllm_serve", "--model", model] + + if tensor_parallel_size := kwargs.get("tensor_parallel_size"): + cmd.extend(["--tensor-parallel-size", str(tensor_parallel_size)]) + if host := kwargs.get("host"): + cmd.extend(["--host", host]) + if port := kwargs.get("port"): + cmd.extend(["--port", str(port)]) + if gpu_memory_utilization := kwargs.get("gpu_memory_utilization"): + cmd.extend(["--gpu-memory-utilization", str(gpu_memory_utilization)]) + if dtype := kwargs.get("dtype"): + cmd.extend(["--dtype", dtype]) + if max_model_len := kwargs.get("max_model_len"): + cmd.extend(["--max-model-len", str(max_model_len)]) + if kwargs.get("enable_prefix_caching"): + cmd.extend(["--enable-prefix-caching", "True"]) + + # print out the command to be executed + print(" ".join(cmd)) + + # start `trl vllm-serve` command in the background and capture the process id + process = subprocess.Popen( # pylint: disable=consider-using-with + cmd, + env=env, + stdout=subprocess.DEVNULL if quiet else subprocess.PIPE, + stderr=subprocess.DEVNULL if quiet else subprocess.PIPE, + ) # nosec B603 + + # print out the process id so the user can easily kill it later + print(f"VLLM server process started (PID: {process.pid})") + + # wait until the http server is ready, even if it 404s, but timeout after 60 seconds + started = False + if wait and host and port: + for _ in range(int(wait)): + try: + response = requests.get(f"http://{host}:{port}", timeout=1) + if int(response.status_code) in [200, 404]: + started = True + break + except requests.exceptions.RequestException: + pass + + # also check if the process.pid is still running + if not process.poll() is None: + break + + time.sleep(1) + + if wait and not started: + print( + f"VLLM server process did not start within {wait} seconds. Please check your server logs." + ) + process.kill() + raise RuntimeError(f"VLLM server process did not start within {wait} seconds.") + + # return the process id + return process.pid + + +class TestGRPO: + """ + Test case for GRPO training using multilpe GPUs + """ + + def _utils_write_yaml_and_rewards(self, cfg, temp_dir, suffix=""): + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + with open(f"rewards_{suffix}.py", "w", encoding="utf-8") as fout: + fout.write( + """import random +def rand_reward_func(completions, **kwargs) -> list[float]: + return [random.uniform(0, 1) for _ in completions] + +def oai_gsm8k_transform(cfg, *args, **kwargs): + def transform_fn(example, tokenizer=None): + label = example["answer"].split("####")[-1].strip().replace(",", "") + return { + "prompt": [{"role": "user", "content": example["question"]},], + "answer": label, + } + return transform_fn, {"remove_columns": ["question"]} +""" + ) + + @pytest.mark.parametrize( + "num_gpus", + [1, 2], + ) + @require_vllm + def test_llama_dora(self, temp_dir, num_gpus): + rnd_reward_suffix = str(random.randint(1000, 9999)) + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "chat_template": "llama3", + "rl": "grpo", + "trl": { + "beta": 0.001, + "max_completion_length": 256, + "use_vllm": True, + "num_generations": 4, + "reward_funcs": [f"rewards_{rnd_reward_suffix}.rand_reward_func"], + }, + "vllm": { + "max_model_len": 800, + "enable_prefix_caching": True, + }, + "datasets": [ + { + "path": "openai/gsm8k", + "name": "main", + "type": f"rewards_{rnd_reward_suffix}.oai_gsm8k_transform", + }, + ], + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "peft_use_dora": True, + "flash_attention": True, + "sequence_len": 1024, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "max_steps": 3, + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "warmup_steps": 10, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + "use_tensorboard": True, + } + ) + + self._utils_write_yaml_and_rewards(cfg, temp_dir, suffix=rnd_reward_suffix) + + current_env = os.environ.copy() + env = { + "NCCL_P2P_LEVEL": "LOC", + **current_env, + "CUDA_VISIBLE_DEVICES": "1", + } + vllm_process_id = start_vllm( + cfg.base_model, + env=env, + quiet=True, + wait=120, + gpu_memory_utilization=0.15, + max_model_len=cfg.vllm.max_model_len, + enable_prefix_caching=cfg.vllm.enable_prefix_caching, + host="0.0.0.0", + port=8000, + ) + + try: + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + str(num_gpus), + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ], + env={"NCCL_P2P_LEVEL": "LOC", "NCCL_DEBUG": "INFO", **current_env}, + ) + finally: + os.kill(vllm_process_id, 9) + + @pytest.mark.parametrize( + "num_gpus", + [1, 2], + ) + @require_vllm + def test_llama_fft(self, temp_dir, num_gpus): + rnd_reward_suffix = str(random.randint(1000, 9999)) + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "chat_template": "llama3", + "rl": "grpo", + "trl": { + "beta": 0.001, + "max_completion_length": 256, + "use_vllm": True, + "num_generations": 4, + "reward_funcs": [f"rewards_{rnd_reward_suffix}.rand_reward_func"], + }, + "vllm": { + "max_model_len": 800, + "enable_prefix_caching": True, + }, + "datasets": [ + { + "path": "openai/gsm8k", + "name": "main", + "type": f"rewards_{rnd_reward_suffix}.oai_gsm8k_transform", + }, + ], + "flash_attention": True, + "sequence_len": 1024, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "max_steps": 3, + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "warmup_steps": 10, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + "use_tensorboard": True, + } + ) + + self._utils_write_yaml_and_rewards(cfg, temp_dir, suffix=rnd_reward_suffix) + + current_env = os.environ.copy() + env = { + "NCCL_P2P_LEVEL": "LOC", # nccl can be brittle, assume P2P isn't reliable + **current_env, + "CUDA_VISIBLE_DEVICES": "1", + } + vllm_process_id = start_vllm( + cfg.base_model, + env=env, + quiet=True, + wait=120, + gpu_memory_utilization=0.15, + max_model_len=cfg.vllm.max_model_len, + enable_prefix_caching=cfg.vllm.enable_prefix_caching, + host="0.0.0.0", + port=8000, + ) + + try: + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + str(num_gpus), + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ], + env={"NCCL_P2P_LEVEL": "LOC", "NCCL_DEBUG": "INFO", **current_env}, + ) + finally: + os.kill(vllm_process_id, 9) diff --git a/tests/e2e/multigpu/test_eval.py b/tests/e2e/multigpu/test_eval.py index 586da8577c..4989b81df7 100644 --- a/tests/e2e/multigpu/test_eval.py +++ b/tests/e2e/multigpu/test_eval.py @@ -52,9 +52,9 @@ def test_eval_sample_packing(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 5, + "max_steps": 2, "micro_batch_size": 2, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_8bit", @@ -121,9 +121,9 @@ def test_eval(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 5, + "max_steps": 2, "micro_batch_size": 2, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_8bit", diff --git a/tests/e2e/multigpu/test_grpo.py b/tests/e2e/multigpu/test_grpo.py deleted file mode 100644 index a879a77501..0000000000 --- a/tests/e2e/multigpu/test_grpo.py +++ /dev/null @@ -1,175 +0,0 @@ -""" -GRPO test suite -""" - -import random -from pathlib import Path - -import pytest -import yaml -from accelerate.test_utils import execute_subprocess_async -from transformers.testing_utils import get_torch_dist_unique_port - -from axolotl.utils.dict import DictDefault - -from tests.e2e.utils import require_vllm - - -class TestGRPO: - """ - Test case for GRPO training using multilpe GPUs - """ - - def _utils_write_yaml_and_rewards(self, cfg, temp_dir, suffix=""): - # write cfg to yaml file - Path(temp_dir).mkdir(parents=True, exist_ok=True) - with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: - fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) - with open(f"rewards_{suffix}.py", "w", encoding="utf-8") as fout: - fout.write( - """import random -def rand_reward_func(completions, **kwargs) -> list[float]: - return [random.uniform(0, 1) for _ in completions] - -def oai_gsm8k_transform(cfg, *args, **kwargs): - def transform_fn(example, tokenizer=None): - label = example["answer"].split("####")[-1].strip().replace(",", "") - return { - "prompt": [{"role": "user", "content": example["question"]},], - "answer": label, - } - return transform_fn, {"remove_columns": ["question"]} -""" - ) - - @pytest.mark.parametrize( - "num_gpus", - [1, 2], - ) - @require_vllm - def test_llama_dora(self, temp_dir, num_gpus): - rnd_reward_suffix = str(random.randint(1000, 9999)) - cfg = DictDefault( - { - "base_model": "HuggingFaceTB/SmolLM2-135M", - "chat_template": "llama3", - "rl": "grpo", - "trl": { - "beta": 0.001, - "max_completion_length": 256, - "use_vllm": True, - "vllm_device": "auto" if num_gpus == 1 else "cuda:1", - "vllm_gpu_memory_utilization": 0.15, - "num_generations": 4, - "reward_funcs": [f"rewards_{rnd_reward_suffix}.rand_reward_func"], - }, - "datasets": [ - { - "path": "openai/gsm8k", - "name": "main", - "type": f"rewards_{rnd_reward_suffix}.oai_gsm8k_transform", - }, - ], - "adapter": "lora", - "lora_r": 8, - "lora_alpha": 16, - "lora_dropout": 0.05, - "lora_target_linear": True, - "peft_use_dora": True, - "flash_attention": True, - "sequence_len": 1024, - "special_tokens": { - "pad_token": "<|endoftext|>", - }, - "max_steps": 5, - "num_epochs": 1, - "micro_batch_size": 4, - "gradient_accumulation_steps": 2, - "warmup_steps": 10, - "val_set_size": 0.0, - "output_dir": temp_dir, - "learning_rate": 0.0001, - "optimizer": "adamw_torch_fused", - "lr_scheduler": "cosine", - "save_safetensors": True, - "bf16": "auto", - "use_tensorboard": True, - } - ) - - self._utils_write_yaml_and_rewards(cfg, temp_dir, suffix=rnd_reward_suffix) - - execute_subprocess_async( - [ - "axolotl", - "train", - str(Path(temp_dir) / "config.yaml"), - "--num-processes", - str(num_gpus), - "--main-process-port", - f"{get_torch_dist_unique_port()}", - ] - ) - - @pytest.mark.parametrize( - "num_gpus", - [1, 2], - ) - @require_vllm - def test_llama_fft(self, temp_dir, num_gpus): - rnd_reward_suffix = str(random.randint(1000, 9999)) - cfg = DictDefault( - { - "base_model": "HuggingFaceTB/SmolLM2-135M", - "chat_template": "llama3", - "rl": "grpo", - "trl": { - "beta": 0.001, - "max_completion_length": 256, - "use_vllm": True, - "vllm_device": "auto" if num_gpus == 1 else "cuda:1", - "vllm_gpu_memory_utilization": 0.15, - "num_generations": 4, - "reward_funcs": [f"rewards_{rnd_reward_suffix}.rand_reward_func"], - }, - "datasets": [ - { - "path": "openai/gsm8k", - "name": "main", - "type": f"rewards_{rnd_reward_suffix}.oai_gsm8k_transform", - }, - ], - "flash_attention": True, - "sequence_len": 1024, - "special_tokens": { - "pad_token": "<|endoftext|>", - }, - "max_steps": 5, - "num_epochs": 1, - "micro_batch_size": 4, - "gradient_accumulation_steps": 2, - "warmup_steps": 10, - "val_set_size": 0.0, - "output_dir": temp_dir, - "learning_rate": 0.0001, - "optimizer": "adamw_torch_fused", - "lr_scheduler": "cosine", - "save_safetensors": True, - "bf16": "auto", - "use_tensorboard": True, - } - ) - - self._utils_write_yaml_and_rewards(cfg, temp_dir, suffix=rnd_reward_suffix) - - execute_subprocess_async( - [ - "axolotl", - "train", - str(Path(temp_dir) / "config.yaml"), - "--num-processes", - str(num_gpus), - "--main-process-port", - f"{get_torch_dist_unique_port()}", - ] - ) diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 8a16ff096e..432d89b1fe 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -399,7 +399,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): "num_epochs": 1, "max_steps": 2, "micro_batch_size": 4, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", @@ -478,7 +478,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "num_epochs": 1, "max_steps": 2, "micro_batch_size": 4, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", @@ -778,7 +778,7 @@ def test_fix_untrained_tokens(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 5, + "max_steps": 2, "micro_batch_size": 1, "gradient_accumulation_steps": 1, "output_dir": temp_dir, diff --git a/tests/e2e/multigpu/test_qwen2.py b/tests/e2e/multigpu/test_qwen2.py index 1895e1ee8b..af39c6361f 100644 --- a/tests/e2e/multigpu/test_qwen2.py +++ b/tests/e2e/multigpu/test_qwen2.py @@ -46,7 +46,7 @@ def test_qlora_fsdp_dpo(self, base_model, temp_dir): }, ], "num_epochs": 1, - "max_steps": 5, + "max_steps": 2, "warmup_steps": 20, "micro_batch_size": 2, "gradient_accumulation_steps": 2, diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index 8e7916728c..14b1c0a86f 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -50,7 +50,7 @@ def test_lora_ddp(self, temp_dir): "num_epochs": 1, "max_steps": 2, "micro_batch_size": 4, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_8bit", From 7acf93b59f380408f28bf824d5798f42d4f3839b Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 1 Apr 2025 02:47:28 +0700 Subject: [PATCH 0501/1405] Fix(doc): Clarify doc on attention configs and missing pad_token (#2455) [skip ci] * fix: clarify input type * fix: handling of error message if data_files not available * fix: clarify attention handling * fix: add doc on missing pad token --- docs/config.qmd | 27 ++++++++++++++++----------- docs/faq.qmd | 12 +++++++++++- src/axolotl/utils/data/shared.py | 3 ++- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index b0c8616a23..208d1b739a 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -587,26 +587,31 @@ max_grad_norm: # currently only supported on Llama and Mistral neftune_noise_alpha: -# Whether to bettertransformers +# Optional[bool]. Whether to bettertransformers flash_optimum: -# Whether to use xformers attention patch https://github.com/facebookresearch/xformers: + +# Note: Only one of the following attention patches can be used at a time. +# For example, if you set `xformers_attention` to `true`, do not set `flash_attention` to `true`. + +# Optional[bool]. Whether to use xformers attention patch https://github.com/facebookresearch/xformers: xformers_attention: -# Whether to use flash attention patch https://github.com/Dao-AILab/flash-attention: +# Optional[bool]. Whether to use flash attention patch https://github.com/Dao-AILab/flash-attention: flash_attention: -flash_attn_cross_entropy: # Whether to use flash-attention cross entropy implementation - advanced use only -flash_attn_rms_norm: # Whether to use flash-attention rms norm implementation - advanced use only -flash_attn_fuse_qkv: # Whether to fuse QKV into a single operation -flash_attn_fuse_mlp: # Whether to fuse part of the MLP into a single operation -# Whether to use scaled-dot-product attention +flash_attn_cross_entropy: # Optional[bool]. Whether to use flash-attention cross entropy implementation - advanced use only +flash_attn_rms_norm: # Optional[bool]. Whether to use flash-attention rms norm implementation - advanced use only +flash_attn_fuse_qkv: # Optional[bool]. Whether to fuse QKV into a single operation +flash_attn_fuse_mlp: # Optional[bool]. Whether to fuse part of the MLP into a single operation +# Optional[bool]. Whether to use scaled-dot-product attention # https://pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html sdp_attention: -# Shifted-sparse attention (only llama) - https://arxiv.org/pdf/2309.12307.pdf +# Optional[bool]. Shifted-sparse attention (only llama) - https://arxiv.org/pdf/2309.12307.pdf s2_attention: + # Optional[bool]. Whether to use low_cpu_mem_usage low_cpu_mem_usage: -# Resume from a specific checkpoint dir +# Optional[str]. Resume from a specific checkpoint dir resume_from_checkpoint: -# If resume_from_checkpoint isn't set and you simply want it to start where it left off. +# Optional[bool]. If resume_from_checkpoint isn't set and you simply want it to start where it left off. # Be careful with this being turned on between different models. auto_resume_from_checkpoints: false diff --git a/docs/faq.qmd b/docs/faq.qmd index 1ce14681a1..664359cb81 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -35,12 +35,22 @@ description: Frequently asked questions **Q: How to call Axolotl via custom python scripts?** -> A: Yes, since Axolotl is just Python, please see `src/axolotl/cli/main.py` on how each command is called. +> A: Since Axolotl is just Python, please see `src/axolotl/cli/main.py` on how each command is called. **Q: How to know the value to use for `fsdp_transformer_layer_cls_to_wrap`?** > A: This is the class name of the transformer layer to wrap with FSDP. For example, for `LlamaForCausalLM`, the value is `LlamaDecoderLayer`. To find this for a specific model, check the model's `PreTrainedModel` definition and look for `_no_split_modules` variable in the `modeling_.py` file within `transformers` library. +**Q: ValueError: Asking to pad but the tokenizer does not have a padding token. Please select a token to use as pad_token** + +> A: This is because the tokenizer does not have a padding token. Please add a padding token to the tokenizer via: + +> ```yaml +> special_tokens: +> # str. If you're not sure, set to same as `eos_token`. +> pad_token: "..." +> ``` + ### Chat templates **Q: `jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'content' / 'role' / ____`** diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 8b3a7541a2..1bb83efd5c 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -238,7 +238,8 @@ def load_dataset_w_config( trust_remote_code=config_dataset.trust_remote_code, **load_ds_kwargs, ) - else: + elif config_dataset.data_files: + fp: str | list[str] | None = None if isinstance(config_dataset.data_files, str): fp = hf_hub_download( repo_id=config_dataset.path, From 4d36ecc724ffcd40325559cdf2a168c92e8e437f Mon Sep 17 00:00:00 2001 From: DreamGenX <157678800+DreamGenX@users.noreply.github.com> Date: Mon, 31 Mar 2025 21:48:20 +0200 Subject: [PATCH 0502/1405] Sequential sample packing (#2404) [skip ci] * add sequential sample packing * chore: lint --------- Co-authored-by: Wing Lian --- .../lora-1b-sample-packing-sequentially.yml | 80 ++++++++++++++++++ src/axolotl/core/trainers/base.py | 1 + src/axolotl/core/training_args.py | 6 ++ src/axolotl/utils/samplers/multipack.py | 82 +++++++++++++++++-- src/axolotl/utils/schemas/config.py | 1 + src/axolotl/utils/trainer.py | 9 +- tests/test_packed_batch_sampler.py | 6 +- 7 files changed, 174 insertions(+), 11 deletions(-) create mode 100644 examples/llama-3/lora-1b-sample-packing-sequentially.yml diff --git a/examples/llama-3/lora-1b-sample-packing-sequentially.yml b/examples/llama-3/lora-1b-sample-packing-sequentially.yml new file mode 100644 index 0000000000..79f5a2ba19 --- /dev/null +++ b/examples/llama-3/lora-1b-sample-packing-sequentially.yml @@ -0,0 +1,80 @@ +base_model: meta-llama/Llama-3.2-1B +# optionally might have model_type or tokenizer_type +model_type: LlamaForCausalLM +tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: true +load_in_4bit: false +strict: false + +datasets: + - path: mhenrichsen/alpaca_2k_test + type: alpaca + - path: mhenrichsen/alpaca_2k_test + type: alpaca +dataset_prepared_path: +val_set_size: 0.0 +output_dir: ./outputs/lora-out + +test_value: true + +sequence_len: 4096 +sample_packing: true +sample_packing_sequentially: true +curriculum_sampling: true +eval_sample_packing: false +pad_to_sequence_len: true + +adapter: lora +lora_model_dir: +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_fan_in_fan_out: +lora_modules_to_save: + - embed_tokens + - lm_head + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true +s2_attention: + +warmup_steps: 10 +evals_per_epoch: 4 +eval_table_size: +eval_max_new_tokens: 128 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: + pad_token: <|end_of_text|> diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 9267dd0409..f5679431af 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -112,6 +112,7 @@ def _create_multipack_sampler( packing_efficiency_estimate=self.args.sample_packing_efficiency, batch_max_len=batch_max_len, batch_size=batch_size, + sequential=self.args.sample_packing_sequentially, drop_last=True, ) diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index fbb3634929..18843abb47 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -34,6 +34,12 @@ class AxolotlTrainingMixins: default=False, metadata={"help": "Use sample packing for efficient training."}, ) + sample_packing_sequentially: bool = field( + default=False, + metadata={ + "help": "Use next-fit sample packing that preserves the order of samples coming from the sampler. Use in combination with curriculum_sampling for fully sequential packing." + }, + ) multipack_real_batches: bool = field( default=False, metadata={"help": "Use real batches for efficient training."}, diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 41095152e5..ef47aca877 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -8,7 +8,7 @@ import numba import numpy as np -from torch.utils.data import BatchSampler, Sampler +from torch.utils.data import BatchSampler, Sampler, SequentialSampler from axolotl.utils.distributed import reduce_and_broadcast @@ -103,6 +103,55 @@ def allocate( return result, s, len(result) * c * n +@numba.njit +def allocate_sequentially(lengths: np.ndarray, rank: int, c: int, n: int): + """ + Sequential allocator that preserves example order + + Parameters: + - lengths: The lengths of all examples + - rank: The current rank (for distributed training) + - c: The capacity of each bin (maximum sequence length) + - n: Number of ranks + + Returns: + - result: List of batches for the current rank + - total_used: Number of actual example tokens + - total_slots: Maximum theoretical number of example tokens (number of bins * bin capacity) + """ + result = [] + total_used = 0 + + # First, do sequential packing into bins + all_bins = [] + current_bin = [0 for i in range(0)] # numba hint + remaining_capacity = c + + for idx, size in enumerate(lengths): + if size <= remaining_capacity: + # Example fits in current bin + current_bin.append(idx) + remaining_capacity -= size + total_used += size + else: + # Example doesn't fit, start a new bin + if current_bin: # Add non-empty bin to all_bins + all_bins.append(current_bin) + current_bin = [idx] + remaining_capacity = c - size + total_used += size + + # Add the last bin if not empty + if current_bin: + all_bins.append(current_bin) + + # Assign bins to ranks - each rank gets every n-th bin + for bin_idx in range(rank, len(all_bins), n): + result.append(all_bins[bin_idx]) + + return result, total_used, len(all_bins) * c + + class MultipackBatchSampler(BatchSampler): """Batch sampler class for multipack""" @@ -115,6 +164,7 @@ def __init__( packing_efficiency_estimate: float = 1.0, drop_last: bool = False, num_count_samples: int = 16, + sequential: bool = False, **kwargs, ): super().__init__(sampler, batch_size, drop_last) @@ -122,6 +172,7 @@ def __init__( self.batch_max_len = batch_max_len self.lengths: np.ndarray = lengths self.packing_efficiency_estimate = packing_efficiency_estimate or 1.0 + self.sequential = sequential assert isinstance(self.lengths, np.ndarray) @@ -136,6 +187,11 @@ def __init__( # the minimum packed dataset length across all ranks determined by a gather/broadcast self.len_across_ranks = None + if self.sequential and not isinstance(sampler, SequentialSampler): + LOG.warn( + "using sequential sample packing with non-sequential sampler, did you want to also enable curriculum_sampling?" + ) + def set_epoch(self, epoch: int): self.epoch = epoch @@ -145,13 +201,23 @@ def generate_batches(self, set_stats=False): lengths = self.lengths[indices] lengths_cumsum = np.cumsum(lengths) - batches, total_used, total_slots = allocate( - lengths=lengths, - lengths_cumsum=lengths_cumsum, - rank=0, - c=self.batch_max_len, - n=1, - ) + if self.sequential: + LOG.debug("using sequential sample packing algorithm") + batches, total_used, total_slots = allocate_sequentially( + lengths=lengths, + rank=0, + c=self.batch_max_len, + n=1, + ) + else: + LOG.debug("using non-sequential sample packing algorithm") + batches, total_used, total_slots = allocate( + lengths=lengths, + lengths_cumsum=lengths_cumsum, + rank=0, + c=self.batch_max_len, + n=1, + ) batches = [ [ diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index c7be33ab3b..837d2ca696 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -192,6 +192,7 @@ class AxolotlInputConfig( sample_packing: bool | None = None sample_packing_group_size: int | None = 100_000 sample_packing_bin_size: int | None = 200 + sample_packing_sequentially: bool | None = None eval_sample_packing: bool | None = None pad_to_sequence_len: bool | None = None curriculum_sampling: bool | None = None diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index d2b211bbc8..646fb4c87b 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -13,7 +13,7 @@ import torch.cuda from accelerate.logging import get_logger from datasets import IterableDataset, disable_caching, enable_caching -from torch.utils.data import DataLoader, RandomSampler +from torch.utils.data import DataLoader, RandomSampler, SequentialSampler from transformers.utils import is_torch_bf16_gpu_available from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder @@ -456,13 +456,18 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): else: sampler_batch_size = cfg.micro_batch_size batch_max_len = cfg.sequence_len + if cfg.curriculum_sampling: + sampler = SequentialSampler(train_dataset) + else: + sampler = RandomSampler(train_dataset) sampler = MultipackBatchSampler( - sampler=RandomSampler(train_dataset), + sampler=sampler, lengths=get_dataset_lengths(train_dataset), batch_size=sampler_batch_size, batch_max_len=batch_max_len, group_size=cfg.sample_packing_group_size, bin_size=cfg.sample_packing_bin_size, + sequential=cfg.sample_packing_sequentially, drop_last=True, ) diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index 061b64b09b..faba869318 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -38,8 +38,11 @@ class TestBatchedSamplerPacking: ], ) @pytest.mark.parametrize("max_seq_length", [4096, 512]) + @pytest.mark.parametrize("sequential", [True, False]) @enable_hf_offline - def test_packing(self, batch_size, num_workers, tokenizer, max_seq_length): + def test_packing( + self, batch_size, num_workers, tokenizer, max_seq_length, sequential + ): import axolotl.monkeypatch.data.batch_dataset_fetcher # pylint: disable=unused-import # noqa: F401 dataset = load_dataset( @@ -75,6 +78,7 @@ def test_packing(self, batch_size, num_workers, tokenizer, max_seq_length): batch_max_len=max_seq_length, group_size=100000, bin_size=200, + sequential=sequential, ) loader = DataLoader( From 328d59811488cf30f7e2ecf92afdf380246ee05f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 31 Mar 2025 17:15:23 -0400 Subject: [PATCH 0503/1405] gemma3 packing fixes (#2449) * make gemma3 work with packing * multi-gpu e2e for ci * update gemma3 model namespace to use mirror * add gradient checkpointing to multigpu e2e ci * update gemma3 examples for use_reentrant and fix ddp find unused params * fix tests for gemma3 * fix import for test utils * set correct train loss for gemma3 e2e --- examples/gemma3/gemma-3-1b-qlora.yml | 5 ++ examples/gemma3/gemma-3-4b-lora.yml | 5 ++ src/axolotl/core/trainer_builder.py | 8 +- src/axolotl/monkeypatch/multipack.py | 1 + src/axolotl/utils/collators/batching.py | 3 + src/axolotl/utils/trainer.py | 2 +- tests/e2e/multigpu/test_gemma3.py | 100 ++++++++++++++++++++++++ tests/e2e/multigpu/test_llama.py | 8 ++ 8 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/multigpu/test_gemma3.py diff --git a/examples/gemma3/gemma-3-1b-qlora.yml b/examples/gemma3/gemma-3-1b-qlora.yml index 669ffacdc5..8852b34694 100644 --- a/examples/gemma3/gemma-3-1b-qlora.yml +++ b/examples/gemma3/gemma-3-1b-qlora.yml @@ -5,6 +5,9 @@ tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name +# gemma3 doesn't seem to play nice with ddp +ddp_find_unused_parameters: true + load_in_8bit: false load_in_4bit: true strict: false @@ -54,6 +57,8 @@ fp16: tf32: true gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false early_stopping_patience: resume_from_checkpoint: local_rank: diff --git a/examples/gemma3/gemma-3-4b-lora.yml b/examples/gemma3/gemma-3-4b-lora.yml index b85392982b..0e7422bd47 100644 --- a/examples/gemma3/gemma-3-4b-lora.yml +++ b/examples/gemma3/gemma-3-4b-lora.yml @@ -7,6 +7,9 @@ skip_prepare_dataset: true remove_unused_columns: false sample_packing: false +# gemma3 doesn't seem to play nice with ddp +ddp_find_unused_parameters: true + chat_template: gemma3 datasets: - path: HuggingFaceH4/llava-instruct-mix-vsft @@ -48,6 +51,8 @@ fp16: tf32: true gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false local_rank: logging_steps: 1 flash_attention: true diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 2349932ba7..7d0df8a45a 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -524,9 +524,15 @@ def build(self, total_num_steps): and self.cfg.eval_steps and self.cfg.save_steps % self.cfg.eval_steps == 0 ) or False + + # handle ddp + ddp_find_unused_parameters = None + if self.cfg.ddp: + ddp_find_unused_parameters = bool(self.cfg.ddp_find_unused_parameters) training_arguments_kwargs["ddp_find_unused_parameters"] = ( - False if self.cfg.ddp else None + ddp_find_unused_parameters ) + training_arguments_kwargs["group_by_length"] = self.cfg.group_by_length training_arguments_kwargs["curriculum_sampling"] = self.cfg.curriculum_sampling report_to = [] diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index cd819fba48..0157433291 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -22,6 +22,7 @@ "phi3", "gemma", "gemma2", + "gemma3", "gemma3_text", "cohere", "cohere2", diff --git a/src/axolotl/utils/collators/batching.py b/src/axolotl/utils/collators/batching.py index 12c8b31d59..33bb4b4cc0 100644 --- a/src/axolotl/utils/collators/batching.py +++ b/src/axolotl/utils/collators/batching.py @@ -112,6 +112,7 @@ def __post_init__(self): self.local_world_size = dist.get_world_size(group=sp_group) def __call__(self, features, return_tensors=None): + has_attn_mask = "attention_mask" in features[0].keys() labels = None if return_tensors is None: return_tensors = self.return_tensors @@ -164,6 +165,8 @@ def __call__(self, features, return_tensors=None): pad_to_multiple_of=self.pad_to_multiple_of, return_tensors=return_tensors, ) + if not has_attn_mask: + del features["attention_mask"] # prepare decoder_input_ids if ( diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 646fb4c87b..c370707b6a 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -235,7 +235,7 @@ def drop_long_seq(sample, sequence_len=2048, min_sequence_len=2): def process_datasets_for_packing(cfg, train_dataset, eval_dataset): - if cfg.model_config_type == "mamba": + if cfg.model_config_type in ["mamba", "gemma3"]: LOG.info("dropping attention_mask column") train_dataset = train_dataset.remove_columns("attention_mask") if eval_dataset: diff --git a/tests/e2e/multigpu/test_gemma3.py b/tests/e2e/multigpu/test_gemma3.py new file mode 100644 index 0000000000..9de3ed82f8 --- /dev/null +++ b/tests/e2e/multigpu/test_gemma3.py @@ -0,0 +1,100 @@ +""" +E2E tests for multigpu lora tinyllama +""" + +import logging +import os +from pathlib import Path + +import pytest +import yaml +from accelerate.test_utils import execute_subprocess_async +from huggingface_hub import snapshot_download +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import check_tensorboard + +LOG = logging.getLogger("axolotl.tests.e2e.multigpu") +os.environ["WANDB_DISABLED"] = "true" + +AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent + + +@pytest.fixture(scope="session", autouse=True) +def download_model(): + # download the model + snapshot_download("axolotl-mirrors/gemma-3-4b-pt", repo_type="model") + + +class TestMultiGPUGemma3: + """ + Test case for Gemma3 models using LoRA + """ + + def test_lora_ddp_packed(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "axolotl-mirrors/gemma-3-4b-pt", + "sequence_len": 2048, + "ddp_find_unused_parameters": True, + "sample_packing": True, + "eval_sample_packing": False, + "pad_to_sequence_len": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.0, + "chat_template": "gemma3", + "datasets": [ + { + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "split": "train[:10%]", + "field_messages": "conversations", + "message_field_role": "from", + "message_field_content": "value", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 4, + "gradient_checkpointing": True, + "gradient_checkpointing_kwargs": { + "use_reentrant": False, + }, + "gradient_accumulation_steps": 2, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "use_tensorboard": True, + "bf16": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 1.8, "Train Loss is too high" + ) diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 432d89b1fe..0e228aef02 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -58,6 +58,7 @@ def test_lora_ddp(self, temp_dir): "max_steps": 2, "micro_batch_size": 4, "gradient_accumulation_steps": 4, + "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_8bit", @@ -121,6 +122,7 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): "max_steps": 2, "micro_batch_size": 1, "gradient_accumulation_steps": gradient_accumulation_steps, + "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_8bit", @@ -193,6 +195,7 @@ def test_dpo_lora_ddp(self, temp_dir): "max_steps": 2, "micro_batch_size": 4, "gradient_accumulation_steps": 4, + "gradient_checkpointing": True, "output_dir": temp_dir, "warmup_steps": 0, "learning_rate": 0.00001, @@ -270,6 +273,7 @@ def test_dpo_qlora_ddp(self, temp_dir): "max_steps": 2, "micro_batch_size": 2, "gradient_accumulation_steps": 4, + "gradient_checkpointing": True, "output_dir": temp_dir, "warmup_steps": 0, "learning_rate": 0.00001, @@ -330,6 +334,7 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): "max_steps": 2, "micro_batch_size": 2, "gradient_accumulation_steps": gradient_accumulation_steps, + "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", @@ -400,6 +405,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): "max_steps": 2, "micro_batch_size": 4, "gradient_accumulation_steps": 2, + "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", @@ -479,6 +485,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "max_steps": 2, "micro_batch_size": 4, "gradient_accumulation_steps": 2, + "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", @@ -781,6 +788,7 @@ def test_fix_untrained_tokens(self, temp_dir): "max_steps": 2, "micro_batch_size": 1, "gradient_accumulation_steps": 1, + "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", From e0aba74dd0e0325f9d89e27bc0d3c76b53c94f9b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 1 Apr 2025 08:47:50 -0400 Subject: [PATCH 0504/1405] Release update 20250331 (#2460) [skip ci] * make torch 2.6.0 the default image * fix tests against upstream main * fix attribute access * use fixture dataset * fix dataset load * correct the fixtures + tests * more fixtures * add accidentally removed shakespeare fixture * fix conversion from unittest to pytest class * nightly main ci caches * build 12.6.3 cuda base image * override for fix from huggingface/transformers#37162 * address PR feedback --- .github/workflows/base.yml | 6 + .github/workflows/main.yml | 4 +- .github/workflows/tests-nightly.yml | 23 ++- .github/workflows/tests.yml | 4 + requirements.txt | 2 +- src/axolotl/core/trainers/base.py | 5 +- src/axolotl/core/trainers/dpo/trainer.py | 4 +- src/axolotl/core/trainers/grpo/trainer.py | 4 +- src/axolotl/core/trainers/mixins/__init__.py | 1 + .../core/trainers/mixins/rng_state_loader.py | 67 +++++++ src/axolotl/core/trainers/trl.py | 11 +- src/axolotl/utils/schemas/config.py | 9 + tests/conftest.py | 187 ++++++++++++++---- tests/test_datasets.py | 36 ++-- tests/test_exact_deduplication.py | 26 +-- tests/test_packed_batch_sampler.py | 18 +- tests/test_prompt_tokenizers.py | 111 +++-------- 17 files changed, 348 insertions(+), 170 deletions(-) create mode 100644 src/axolotl/core/trainers/mixins/rng_state_loader.py diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index cf5c1d45d7..01b898310b 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -40,6 +40,12 @@ jobs: python_version: "3.11" pytorch: 2.6.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + - cuda: "126" + cuda_version: 12.6.3 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.6.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4a7112041b..350b04cca9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,12 +25,12 @@ jobs: python_version: "3.11" pytorch: 2.5.1 axolotl_extras: vllm - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.6.0 axolotl_extras: + is_latest: true runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -87,12 +87,12 @@ jobs: python_version: "3.11" pytorch: 2.5.1 axolotl_extras: - is_latest: true - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.6.0 axolotl_extras: + is_latest: true runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 0b91d0c01c..9d3b056e14 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -33,6 +33,15 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 + - name: Restore HF cache + id: hf-cache-restore + uses: actions/cache/restore@v4 + with: + path: | + /home/runner/.cache/huggingface/hub/datasets--* + /home/runner/.cache/huggingface/hub/models--* + key: ${{ runner.os }}-hf-hub-cache-v2 + - name: Setup Python uses: actions/setup-python@v5 with: @@ -46,7 +55,7 @@ jobs: - name: Install PyTorch run: | - pip3 install torch==${{ matrix.pytorch_version }} --index-url https://download.pytorch.org/whl/cpu + pip3 install torch==${{ matrix.pytorch_version }} - name: Update requirements.txt run: | @@ -58,8 +67,7 @@ jobs: - name: Install dependencies run: | - pip3 install --upgrade pip - pip3 install --upgrade packaging==23.2 + pip3 show torch pip3 install --no-build-isolation -U -e . python scripts/unsloth_install.py | sh python scripts/cutcrossentropy_install.py | sh @@ -73,10 +81,15 @@ jobs: run: | axolotl --help + - name: Pre-Download dataset fixture + run: | + huggingface-cli download --repo-type=dataset axolotl-ai-internal/axolotl-oss-dataset-fixtures + - name: Run tests run: | - pytest -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ tests/ - pytest tests/patched/ + pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ + pytest -v tests/patched/ + pytest -v tests/cli/ - name: cleanup pip cache run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a1a7214eca..434803d2c5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -96,6 +96,10 @@ jobs: run: | axolotl --help + - name: Pre-Download dataset fixture + run: | + huggingface-cli download --repo-type=dataset axolotl-ai-internal/axolotl-oss-dataset-fixtures + - name: Run tests run: | pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ diff --git a/requirements.txt b/requirements.txt index 9aff0ccfe4..567b446dda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ liger-kernel==0.5.5 packaging==23.2 peft==0.15.0 -transformers==4.50.0 +transformers==4.50.3 tokenizers>=0.21.1 accelerate==1.5.2 datasets==3.5.0 diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index f5679431af..436d3a0731 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -28,6 +28,7 @@ from axolotl.core.trainers.mixins import ( OptimizerMixin, + RngLoaderMixin, SchedulerMixin, SequenceParallelMixin, ) @@ -40,7 +41,9 @@ LOG = logging.getLogger(__name__) -class AxolotlTrainer(SchedulerMixin, OptimizerMixin, SequenceParallelMixin, Trainer): +class AxolotlTrainer( + SchedulerMixin, OptimizerMixin, RngLoaderMixin, SequenceParallelMixin, Trainer +): """Extend the base Trainer for axolotl helpers""" args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index 9eb870a3a9..89c77dca4a 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -13,7 +13,7 @@ from transformers.utils import is_sagemaker_mp_enabled from trl import DPOTrainer -from axolotl.core.trainers.mixins import SchedulerMixin +from axolotl.core.trainers.mixins import RngLoaderMixin, SchedulerMixin from axolotl.core.trainers.utils import ( sanitize_kwargs_for_ds_tagging, sanitize_kwargs_for_tagging, @@ -23,7 +23,7 @@ import smdistributed.modelparallel.torch as smp -class AxolotlDPOTrainer(SchedulerMixin, DPOTrainer): +class AxolotlDPOTrainer(RngLoaderMixin, SchedulerMixin, DPOTrainer): """ Extend the base DPOTrainer for axolotl helpers """ diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index e8a142945c..25aafa6a7c 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -8,13 +8,13 @@ from trl import GRPOTrainer from trl.extras.profiling import profiling_decorator -from axolotl.core.trainers.base import SchedulerMixin +from axolotl.core.trainers.mixins import RngLoaderMixin, SchedulerMixin if is_deepspeed_available(): import deepspeed -class AxolotlGRPOTrainer(SchedulerMixin, GRPOTrainer): +class AxolotlGRPOTrainer(RngLoaderMixin, SchedulerMixin, GRPOTrainer): """ Extend the base GRPOTrainer for axolotl helpers """ diff --git a/src/axolotl/core/trainers/mixins/__init__.py b/src/axolotl/core/trainers/mixins/__init__.py index 12c8277fc3..44751b4653 100644 --- a/src/axolotl/core/trainers/mixins/__init__.py +++ b/src/axolotl/core/trainers/mixins/__init__.py @@ -4,5 +4,6 @@ # flake8: noqa from .optimizer import OptimizerMixin +from .rng_state_loader import RngLoaderMixin from .scheduler import SchedulerMixin from .sequence_parallel import SequenceParallelMixin diff --git a/src/axolotl/core/trainers/mixins/rng_state_loader.py b/src/axolotl/core/trainers/mixins/rng_state_loader.py new file mode 100644 index 0000000000..0e101dabb9 --- /dev/null +++ b/src/axolotl/core/trainers/mixins/rng_state_loader.py @@ -0,0 +1,67 @@ +""" +Temporary fix/override for bug in resume from checkpoint + +See https://github.com/huggingface/transformers/pull/37162 + +TODO: Remove when upstream added PR to release +""" + +import logging +import os +import random + +import numpy as np +import torch +from transformers import Trainer, is_torch_npu_available +from transformers.trainer import safe_globals +from transformers.trainer_pt_utils import set_rng_state_for_device +from transformers.training_args import ParallelMode + +LOG = logging.getLogger(__name__) + + +class RngLoaderMixin(Trainer): + """ + mixin for method override to load RNG states from a checkpoint + """ + + def _load_rng_state(self, checkpoint): + # Load RNG states from `checkpoint` + if checkpoint is None: + return + + if self.args.world_size > 1: + process_index = self.args.process_index + rng_file = os.path.join(checkpoint, f"rng_state_{process_index}.pth") + if not os.path.isfile(rng_file): + LOG.info( + f"Didn't find an RNG file for process {process_index}, if you are resuming a training that " + "wasn't launched in a distributed fashion, reproducibility is not guaranteed." + ) + return + else: + rng_file = os.path.join(checkpoint, "rng_state.pth") + if not os.path.isfile(rng_file): + LOG.info( + "Didn't find an RNG file, if you are resuming a training that was launched in a distributed " + "fashion, reproducibility is not guaranteed." + ) + return + + # Use safe_globals to ensure numpy RNG states can be deserialized safely under PyTorch 2.6+, + # which requires allowlisted classes when loading with weights_only=True. + with safe_globals(): + checkpoint_rng_state = torch.load(rng_file) # nosec B614 + random.setstate(checkpoint_rng_state["python"]) + np.random.set_state(checkpoint_rng_state["numpy"]) + torch.random.set_rng_state(checkpoint_rng_state["cpu"]) + + is_distributed = self.args.parallel_mode == ParallelMode.DISTRIBUTED + if torch.cuda.is_available(): + set_rng_state_for_device( + "CUDA", torch.cuda, checkpoint_rng_state, is_distributed + ) + if is_torch_npu_available(): + set_rng_state_for_device( + "NPU", torch.npu, checkpoint_rng_state, is_distributed + ) diff --git a/src/axolotl/core/trainers/trl.py b/src/axolotl/core/trainers/trl.py index ebe46f11d7..b2c5c54ca3 100644 --- a/src/axolotl/core/trainers/trl.py +++ b/src/axolotl/core/trainers/trl.py @@ -13,6 +13,7 @@ RewardTrainer, ) +from axolotl.core.trainers.mixins import RngLoaderMixin from axolotl.core.trainers.mixins.scheduler import SchedulerMixin @@ -74,7 +75,7 @@ def train( ) -class AxolotlORPOTrainer(SchedulerMixin, ORPOTrainer): +class AxolotlORPOTrainer(RngLoaderMixin, SchedulerMixin, ORPOTrainer): """ Extend the base ORPOTrainer for axolotl helpers """ @@ -154,7 +155,7 @@ def get_batch_loss_metrics( return loss, metrics -class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): +class AxolotlKTOTrainer(RngLoaderMixin, SchedulerMixin, KTOTrainer): """ Extend the base KTOTrainer for axolotl helpers """ @@ -162,7 +163,7 @@ class AxolotlKTOTrainer(SchedulerMixin, KTOTrainer): tag_names = ["axolotl", "kto"] -class AxolotlCPOTrainer(SchedulerMixin, CPOTrainer): +class AxolotlCPOTrainer(RngLoaderMixin, SchedulerMixin, CPOTrainer): """ Extend the base CPOTrainer for axolotl helpers """ @@ -244,7 +245,7 @@ def get_batch_loss_metrics( return loss, metrics -class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): +class AxolotlRewardTrainer(RngLoaderMixin, SchedulerMixin, RewardTrainer): """ Extend the base RewardTrainer for axolotl helpers """ @@ -252,7 +253,7 @@ class AxolotlRewardTrainer(SchedulerMixin, RewardTrainer): tag_names = ["axolotl", "reward"] -class AxolotlPRMTrainer(SchedulerMixin, PRMTrainer): +class AxolotlPRMTrainer(RngLoaderMixin, SchedulerMixin, PRMTrainer): """ Extend the base trl.PRMTrainer for axolotl helpers """ diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 837d2ca696..3e072b6b43 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1270,3 +1270,12 @@ def check_beta_and_trl_beta_match(cls, data): if data["beta"] != data["trl"]["beta"]: raise ValueError("beta and trl.beta must match or one must be removed") return data + + @model_validator(mode="after") + def check_min_torch_version(self): + if self.env_capabilities and self.env_capabilities.torch_version: + torch_version = self.env_capabilities.torch_version + if version.parse(torch_version) < version.parse("2.5.1"): + LOG.warning( + f"torch=={torch_version} may not be supported in future versions. Please consider upgrading to torch>=2.5.1." + ) diff --git a/tests/conftest.py b/tests/conftest.py index b86b714afd..4d05d3a269 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,11 +8,13 @@ import sys import tempfile import time +from pathlib import Path +import datasets import pytest import requests -from datasets import load_dataset from huggingface_hub import snapshot_download +from tokenizers import AddedToken from transformers import AutoTokenizer from tests.hf_offline_utils import disable_hf_offline, enable_hf_offline @@ -48,6 +50,14 @@ def snapshot_download_w_retry(*args, **kwargs): return snapshot_download(*args, **kwargs) +@pytest.fixture(scope="session", autouse=True) +def download_ds_fixture_bundle(): + ds_dir = snapshot_download_w_retry( + "axolotl-ai-internal/axolotl-oss-dataset-fixtures", repo_type="dataset" + ) + return Path(ds_dir) + + @pytest.fixture(scope="session", autouse=True) def download_smollm2_135m_model(): # download the model @@ -108,43 +118,43 @@ def download_argilla_distilabel_intel_orca_dpo_dataset(): ) -@pytest.fixture(scope="session", autouse=True) -def download_argilla_ultrafeedback_binarized_preferences_cleaned_dataset(): - # download the dataset - snapshot_download_w_retry( - "argilla/ultrafeedback-binarized-preferences-cleaned", repo_type="dataset" - ) - - -@pytest.fixture(scope="session", autouse=True) -def download_fozzie_alpaca_dpo_dataset(): - # download the dataset - snapshot_download_w_retry( - "fozziethebeat/alpaca_messages_2k_dpo_test", repo_type="dataset" - ) - snapshot_download_w_retry( - "fozziethebeat/alpaca_messages_2k_dpo_test", - repo_type="dataset", - revision="ea82cff", - ) +# @pytest.fixture(scope="session", autouse=True) +# def download_argilla_ultrafeedback_binarized_preferences_cleaned_dataset(): +# # download the dataset +# snapshot_download_w_retry( +# "argilla/ultrafeedback-binarized-preferences-cleaned", repo_type="dataset" +# ) -@pytest.fixture(scope="session") -@disable_hf_offline -def dataset_fozzie_alpaca_dpo_dataset( - download_fozzie_alpaca_dpo_dataset, -): # pylint: disable=unused-argument,redefined-outer-name - return load_dataset("fozziethebeat/alpaca_messages_2k_dpo_test", split="train") +# @pytest.fixture(scope="session", autouse=True) +# def download_fozzie_alpaca_dpo_dataset(): +# # download the dataset +# snapshot_download_w_retry( +# "fozziethebeat/alpaca_messages_2k_dpo_test", repo_type="dataset" +# ) +# snapshot_download_w_retry( +# "fozziethebeat/alpaca_messages_2k_dpo_test", +# repo_type="dataset", +# revision="ea82cff", +# ) -@pytest.fixture(scope="session") -@disable_hf_offline -def dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff( - download_fozzie_alpaca_dpo_dataset, -): # pylint: disable=unused-argument,redefined-outer-name - return load_dataset( - "fozziethebeat/alpaca_messages_2k_dpo_test", split="train", revision="ea82cff" - ) +# @pytest.fixture(scope="session") +# @disable_hf_offline +# def dataset_fozzie_alpaca_dpo_dataset( +# download_fozzie_alpaca_dpo_dataset, +# ): # pylint: disable=unused-argument,redefined-outer-name +# return load_dataset("fozziethebeat/alpaca_messages_2k_dpo_test", split="train") +# +# +# @pytest.fixture(scope="session") +# @disable_hf_offline +# def dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff( +# download_fozzie_alpaca_dpo_dataset, +# ): # pylint: disable=unused-argument,redefined-outer-name +# return load_dataset( +# "fozziethebeat/alpaca_messages_2k_dpo_test", split="train", revision="ea82cff" +# ) @pytest.fixture(scope="session", autouse=True) @@ -271,7 +281,7 @@ def download_mlx_mistral_7b_model_fixture(): ) -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture def download_llama2_model_fixture(): # download the tokenizer only snapshot_download_w_retry( @@ -281,7 +291,7 @@ def download_llama2_model_fixture(): ) -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture @enable_hf_offline def tokenizer_huggyllama( download_huggyllama_model_fixture, @@ -292,6 +302,57 @@ def tokenizer_huggyllama( return tokenizer +@pytest.fixture +@enable_hf_offline +def tokenizer_huggyllama_w_special_tokens( + tokenizer_huggyllama, +): # pylint: disable=redefined-outer-name + tokenizer_huggyllama.add_special_tokens( + { + "bos_token": "", + "eos_token": "", + "unk_token": "", + } + ) + + return tokenizer_huggyllama + + +@pytest.fixture +@enable_hf_offline +def tokenizer_llama2_7b( + download_llama2_model_fixture, +): # pylint: disable=unused-argument,redefined-outer-name + tokenizer = AutoTokenizer.from_pretrained("NousResearch/Llama-2-7b-hf") + + return tokenizer + + +@pytest.fixture +@enable_hf_offline +def tokenizer_mistral_7b_instruct( + download_mlx_mistral_7b_model_fixture, +): # pylint: disable=unused-argument,redefined-outer-name + return AutoTokenizer.from_pretrained("casperhansen/mistral-7b-instruct-v0.1-awq") + + +@pytest.fixture +def tokenizer_mistral_7b_instruct_chatml(tokenizer_mistral_7b_instruct): + tokenizer_mistral_7b_instruct.add_special_tokens( + { + "eos_token": AddedToken( + "<|im_end|>", rstrip=False, lstrip=False, normalized=False + ) + } + ) + tokenizer_mistral_7b_instruct.add_tokens( + [ + AddedToken("<|im_start|>", rstrip=False, lstrip=False, normalized=False), + ] + ) + return tokenizer_mistral_7b_instruct + + @pytest.fixture def temp_dir(): # Create a temporary directory @@ -357,6 +418,60 @@ def cleanup_monkeypatches(): globals().pop(module_global, None) +@pytest.fixture +def dataset_winglian_tiny_shakespeare( + download_ds_fixture_bundle: Path, +): # pylint: disable=redefined-outer-name + ds_path = download_ds_fixture_bundle / "winglian__tiny-shakespeare" + return datasets.load_from_disk(ds_path) + + +@pytest.fixture +def dataset_tatsu_lab_alpaca( + download_ds_fixture_bundle: Path, +): # pylint: disable=redefined-outer-name + ds_path = download_ds_fixture_bundle / "tatsu-lab__alpaca" + return datasets.load_from_disk(ds_path)["train"] + + +@pytest.fixture +def dataset_mhenrichsen_alpaca_2k_test( + download_ds_fixture_bundle: Path, +): # pylint: disable=redefined-outer-name + ds_path = download_ds_fixture_bundle / "mhenrichsen__alpaca_2k_test" + return datasets.load_from_disk(ds_path)["train"] + + +@pytest.fixture +def dataset_argilla_ultrafeedback_binarized_preferences_cleaned( + download_ds_fixture_bundle: Path, +): # pylint: disable=redefined-outer-name + ds_path = ( + download_ds_fixture_bundle + / "argilla__ultrafeedback-binarized-preferences-cleaned" + ) + return datasets.load_from_disk(ds_path)["train"] + + +@pytest.fixture +def dataset_fozziethebeat_alpaca_messages_2k_dpo_test( + download_ds_fixture_bundle: Path, +): # pylint: disable=redefined-outer-name + ds_path = download_ds_fixture_bundle / "fozziethebeat__alpaca_messages_2k_dpo_test" + return datasets.load_from_disk(ds_path)["train"] + + +@pytest.fixture +def dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff( + download_ds_fixture_bundle: Path, +): # pylint: disable=redefined-outer-name + ds_path = ( + download_ds_fixture_bundle + / "fozziethebeat__alpaca_messages_2k_dpo_test__rev_ea82cff" + ) + return datasets.load_from_disk(ds_path)["train"] + + # # pylint: disable=redefined-outer-name,unused-argument # def test_load_fixtures( # download_smollm2_135m_model, diff --git a/tests/test_datasets.py b/tests/test_datasets.py index a82f2f3817..ded82869f5 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -324,7 +324,7 @@ def test_load_hub_with_revision(self, tokenizer): @enable_hf_offline def test_load_hub_with_revision_with_dpo( - self, dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff + self, dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff ): """Verify that processing dpo data from the hub works with a specific revision""" @@ -339,12 +339,10 @@ def test_load_hub_with_revision_with_dpo( ) # pylint: disable=duplicate-code - with patch( - "axolotl.utils.data.shared.load_dataset_w_config" - ) as mock_load_dataset: + with patch("axolotl.utils.data.rl.load_dataset_w_config") as mock_load_dataset: # Set up the mock to return different values on successive calls mock_load_dataset.return_value = ( - dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff + dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff ) train_dataset, _ = load_prepare_preference_datasets(cfg) @@ -354,7 +352,9 @@ def test_load_hub_with_revision_with_dpo( @enable_hf_offline @pytest.mark.skip("datasets bug with local datasets when offline") - def test_load_local_hub_with_revision(self, tokenizer): + def test_load_local_hub_with_revision( + self, dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff, tokenizer + ): """Verify that a local copy of a hub dataset can be loaded with a specific revision""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_path = Path(tmp_dir) / "mhenrichsen/alpaca_2k_test" @@ -386,13 +386,23 @@ def test_load_local_hub_with_revision(self, tokenizer): } ) - dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) - - assert len(dataset) == 2000 - assert "input_ids" in dataset.features - assert "attention_mask" in dataset.features - assert "labels" in dataset.features - shutil.rmtree(tmp_ds_path) + with patch( + "axolotl.utils.data.shared.load_dataset_w_config" + ) as mock_load_dataset: + # Set up the mock to return different values on successive calls + mock_load_dataset.return_value = ( + dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff + ) + + dataset, _ = load_tokenized_prepared_datasets( + tokenizer, cfg, prepared_path + ) + + assert len(dataset) == 2000 + assert "input_ids" in dataset.features + assert "attention_mask" in dataset.features + assert "labels" in dataset.features + shutil.rmtree(tmp_ds_path) @enable_hf_offline def test_loading_local_dataset_folder(self, tokenizer): diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index 7430352a40..a75f97f78a 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -238,21 +238,22 @@ def cfg(self): @enable_hf_offline def test_load_with_deduplication( - self, cfg, dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff, tokenizer_huggyllama + self, + cfg, + dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff, + tokenizer_huggyllama, ): """Verify that loading with deduplication removes duplicates.""" # pylint: disable=duplicate-code with ( - patch( - "axolotl.utils.data.shared.load_dataset_w_config" - ) as mock_load_dataset, + patch("axolotl.utils.data.rl.load_dataset_w_config") as mock_load_dataset, patch("axolotl.utils.models.load_tokenizer") as mock_load_tokenizer, ): # Set up the mock to return different values on successive calls mock_load_dataset.side_effect = [ - dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff, - dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff, + dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff, + dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff, ] mock_load_tokenizer.return_value = tokenizer_huggyllama @@ -263,19 +264,20 @@ def test_load_with_deduplication( @enable_hf_offline def test_load_without_deduplication( - self, cfg, dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff, tokenizer_huggyllama + self, + cfg, + dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff, + tokenizer_huggyllama, ): # pylint: disable=duplicate-code with ( - patch( - "axolotl.utils.data.shared.load_dataset_w_config" - ) as mock_load_dataset, + patch("axolotl.utils.data.rl.load_dataset_w_config") as mock_load_dataset, patch("axolotl.utils.models.load_tokenizer") as mock_load_tokenizer, ): # Set up the mock to return different values on successive calls mock_load_dataset.side_effect = [ - dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff, - dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff, + dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff, + dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff, ] mock_load_tokenizer.return_value = tokenizer_huggyllama diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index faba869318..dd0386e58c 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -1,7 +1,7 @@ """Module for testing streaming dataset sequence packing""" import pytest -from datasets import concatenate_datasets, load_dataset +from datasets import concatenate_datasets from torch.utils.data import DataLoader, RandomSampler from transformers import AutoTokenizer @@ -27,7 +27,6 @@ class TestBatchedSamplerPacking: Test class for packing streaming dataset sequences """ - @pytest.mark.skip(reason="TODO: fix hf offline mode for CI rate limits") @pytest.mark.parametrize( "batch_size, num_workers", [ @@ -41,14 +40,17 @@ class TestBatchedSamplerPacking: @pytest.mark.parametrize("sequential", [True, False]) @enable_hf_offline def test_packing( - self, batch_size, num_workers, tokenizer, max_seq_length, sequential + self, + dataset_winglian_tiny_shakespeare, + batch_size, + num_workers, + tokenizer, + max_seq_length, + sequential, ): import axolotl.monkeypatch.data.batch_dataset_fetcher # pylint: disable=unused-import # noqa: F401 - dataset = load_dataset( - "winglian/tiny-shakespeare", - split="train", - ) + dataset = dataset_winglian_tiny_shakespeare["train"] cfg = DictDefault( { @@ -58,7 +60,7 @@ def test_packing( ) ds_cfg = DictDefault( { - "field": "Text", + "field": "text", } ) completion_strategy = load(tokenizer, cfg, ds_cfg) diff --git a/tests/test_prompt_tokenizers.py b/tests/test_prompt_tokenizers.py index 65eee7ddba..3f16bc9177 100644 --- a/tests/test_prompt_tokenizers.py +++ b/tests/test_prompt_tokenizers.py @@ -2,13 +2,8 @@ import json import logging -import unittest from pathlib import Path -import pytest -from datasets import load_dataset -from transformers import AddedToken, AutoTokenizer, LlamaTokenizer - from axolotl.prompt_strategies.alpaca_chat import NoSystemPrompter from axolotl.prompt_strategies.alpaca_w_system import ( InstructionWSystemPromptTokenizingStrategy, @@ -61,24 +56,13 @@ } -class TestPromptTokenizationStrategies(unittest.TestCase): +class TestPromptTokenizationStrategies: """ Test class for prompt tokenization strategies. """ @enable_hf_offline - def setUp(self) -> None: - # pylint: disable=duplicate-code - self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") - self.tokenizer.add_special_tokens( - { - "bos_token": "", - "eos_token": "", - "unk_token": "", - } - ) - - def test_no_sys_prompt(self): + def test_no_sys_prompt(self, tokenizer_huggyllama_w_special_tokens): """ tests the interface between the user and assistant parts """ @@ -86,7 +70,7 @@ def test_no_sys_prompt(self): # pylint: disable=duplicate-code strat = AlpacaPromptTokenizingStrategy( prompter, - self.tokenizer, + tokenizer_huggyllama_w_special_tokens, False, 2048, ) @@ -99,7 +83,8 @@ def test_no_sys_prompt(self): assert example["labels"][world_idx] == 3186 assert example["labels"][world_idx - 1] == -100 - def test_alpaca(self): + @enable_hf_offline + def test_alpaca(self, tokenizer_huggyllama_w_special_tokens): """ tests the interface between the user and assistant parts """ @@ -107,7 +92,7 @@ def test_alpaca(self): prompter = AlpacaPrompter() strat = AlpacaPromptTokenizingStrategy( prompter, - self.tokenizer, + tokenizer_huggyllama_w_special_tokens, False, 2048, ) @@ -118,28 +103,17 @@ def test_alpaca(self): assert example["labels"][world_idx - 1] == -100 -class InstructionWSystemPromptTokenizingStrategyTest(unittest.TestCase): +class TestInstructionWSystemPromptTokenizingStrategy: """ Test class for prompt tokenization strategies with sys prompt from the dataset """ @enable_hf_offline - def setUp(self) -> None: - # pylint: disable=duplicate-code - self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") - self.tokenizer.add_special_tokens( - { - "bos_token": "", - "eos_token": "", - "unk_token": "", - } - ) - - def test_system_alpaca(self): + def test_system_alpaca(self, tokenizer_huggyllama_w_special_tokens): prompter = SystemDataPrompter(PromptStyle.CHAT.value) strat = InstructionWSystemPromptTokenizingStrategy( prompter, - self.tokenizer, + tokenizer_huggyllama_w_special_tokens, False, 2048, ) @@ -160,18 +134,13 @@ def test_system_alpaca(self): assert example["input_ids"][8] == 11889 # USER -class Llama2ChatTokenizationTest(unittest.TestCase): +class Llama2ChatTokenizationTest: """ Test class for prompt tokenization strategies with sys prompt from the dataset """ @enable_hf_offline - def setUp(self) -> None: - # pylint: disable=duplicate-code - self.tokenizer = LlamaTokenizer.from_pretrained("NousResearch/Llama-2-7b-hf") - # woraround because official Meta repos are not open - - def test_llama2_chat_integration(self): + def test_llama2_chat_integration(self, tokenizer_llama2_7b): with open( Path(__file__).parent / "fixtures/conversation.json", encoding="utf-8" ) as fin: @@ -186,16 +155,18 @@ def test_llama2_chat_integration(self): prompter = Llama2ChatPrompter() strat = LLama2ChatTokenizingStrategy( prompter, - self.tokenizer, + tokenizer_llama2_7b, False, 4096, ) example = strat.tokenize_prompt(conversation) for fields in ["input_ids", "attention_mask", "labels"]: - self.assertEqual(len(example[fields]), len(tokenized_conversation[fields])) - self.assertEqual(example[fields], tokenized_conversation[fields]) + # pytest assert equals - def compare_with_transformers_integration(self): + assert len(example[fields]) == len(tokenized_conversation[fields]) + assert example[fields] == tokenized_conversation[fields] + + def compare_with_transformers_integration(self, tokenizer_llama2_7b): # this needs transformers >= v4.31.0 from transformers.models.llama.tokenization_llama import B_SYS, E_SYS from transformers.pipelines.conversational import Conversation @@ -234,49 +205,27 @@ def compare_with_transformers_integration(self): generated_responses=answers, ) # pylint: disable=W0212 - hf_tokens = self.tokenizer._build_conversation_input_ids(hf_conf) + hf_tokens = tokenizer_llama2_7b._build_conversation_input_ids(hf_conf) - self.assertEqual( - hf_tokens, tokenized_conversation["input_ids"][: len(hf_tokens)] - ) + assert hf_tokens == tokenized_conversation["input_ids"][: len(hf_tokens)] -class OrpoTokenizationTest(unittest.TestCase): +class OrpoTokenizationTest: """test case for the ORPO tokenization""" @enable_hf_offline - def setUp(self) -> None: - # pylint: disable=duplicate-code - tokenizer = LlamaTokenizer.from_pretrained( - "casperhansen/mistral-7b-instruct-v0.1-awq" - ) - tokenizer.add_special_tokens( - { - "eos_token": AddedToken( - "<|im_end|>", rstrip=False, lstrip=False, normalized=False - ) - } - ) - tokenizer.add_tokens( - [ - AddedToken( - "<|im_start|>", rstrip=False, lstrip=False, normalized=False - ), - ] - ) - self.tokenizer = tokenizer - self.dataset = load_dataset( - "argilla/ultrafeedback-binarized-preferences-cleaned", split="train" - ).select([0]) - - @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") - def test_orpo_integration(self): + def test_orpo_integration( + self, + tokenizer_mistral_7b_instruct_chatml, + dataset_argilla_ultrafeedback_binarized_preferences_cleaned, + ): + ds = dataset_argilla_ultrafeedback_binarized_preferences_cleaned.select([0]) strat = load( - self.tokenizer, + tokenizer_mistral_7b_instruct_chatml, DictDefault({"train_on_inputs": False}), DictDefault({"chat_template": "chatml"}), ) - res = strat.tokenize_prompt(self.dataset[0]) + res = strat.tokenize_prompt(ds[0]) assert "rejected_input_ids" in res assert "rejected_labels" in res assert "input_ids" in res @@ -295,7 +244,3 @@ def test_orpo_integration(self): assert res["prompt_attention_mask"][0] == 1 assert res["prompt_attention_mask"][-1] == 0 - - -if __name__ == "__main__": - unittest.main() From 9b95e06cbbcd7dc6007b4bb4b82aff70c543db94 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 1 Apr 2025 19:48:36 +0700 Subject: [PATCH 0505/1405] Fix(doc): Minor doc changes for peft and modal (#2462) [skip ci] * fix(doc): document peft configs * fix(doc): explain modal env vs secrets difference * fix(doc): clarify evaluate vs lm-eval * fix: clarify what is performance --- docs/cli.qmd | 40 +++++++++++++++++++++++++++------------- docs/config.qmd | 22 +++++++++++++++++++++- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/docs/cli.qmd b/docs/cli.qmd index a3d5cf939d..79892fc5a7 100644 --- a/docs/cli.qmd +++ b/docs/cli.qmd @@ -170,7 +170,7 @@ axolotl merge-sharded-fsdp-weights config.yml ### evaluate -Evaluates a model's performance using metrics specified in the config. +Evaluates a model's performance (loss etc) on the train and eval datasets. ```bash # Basic evaluation @@ -197,6 +197,8 @@ lm_eval_batch_size: # Batch size for evaluation output_dir: # Directory to save evaluation results ``` +See [LM Eval Harness](https://github.com/EleutherAI/lm-evaluation-harness) for more details. + ## Legacy CLI Usage While the new Click-based CLI is preferred, Axolotl still supports the legacy module-based CLI: @@ -235,7 +237,7 @@ Create a cloud config YAML with your Modal settings: ```yaml # cloud_config.yml provider: modal -gpu: a100 # Supported: l40s, a100-40gb, a100-80gb, a10g, h100, t4, l4 +gpu: a100 # Supported: l40s, a100-40gb, a100-80gb, a10g, h100, t4, l4 gpu_count: 1 # Number of GPUs to use timeout: 86400 # Maximum runtime in seconds (24 hours) branch: main # Git branch to use (optional) @@ -248,7 +250,7 @@ volumes: # Persistent storage volumes - name: axolotl-artifacts mount: /workspace/artifacts -env: # Environment variables +secrets: # Secrets to inject - WANDB_API_KEY - HF_TOKEN ``` @@ -274,15 +276,27 @@ axolotl lm-eval config.yml --cloud cloud_config.yml ### Cloud Configuration Options ```yaml -provider: # compute provider, currently only `modal` is supported -gpu: # GPU type to use -gpu_count: # Number of GPUs (default: 1) -memory: # RAM in GB (default: 128) -timeout: # Maximum runtime in seconds +provider: # compute provider, currently only `modal` is supported +gpu: # GPU type to use +gpu_count: # Number of GPUs (default: 1) +memory: # RAM in GB (default: 128) +timeout: # Maximum runtime in seconds timeout_preprocess: # Preprocessing timeout -branch: # Git branch to use -docker_tag: # Custom Docker image tag -volumes: # List of persistent storage volumes -env: # Environment variables to pass -secrets: # Secrets to inject +branch: # Git branch to use +docker_tag: # Custom Docker image tag +volumes: # List of persistent storage volumes + +# Environment variables to pass. Can be specified in two ways: +# 1. As a string: Will load the value from the host computer's environment variables +# 2. As a key-value pair: Will use the specified value directly +# Example: +# env: +# - CUSTOM_VAR # Loads from host's $CUSTOM_VAR +# - {CUSTOM_VAR: "value"} # Uses "value" directly +env: + +# Secrets to inject. Same input format as `env` but for sensitive data. +secrets: + # - HF_TOKEN + # - WANDB_API_KEY ``` diff --git a/docs/config.qmd b/docs/config.qmd index 208d1b739a..2ed365d75f 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -354,7 +354,27 @@ lora_target_modules: # - down_proj # - up_proj lora_target_linear: # If true, will target all linear modules -peft_layers_to_transform: # The layer indices to transform, otherwise, apply to all layers + +# List[int] | int. # The layer indices to transform, otherwise, apply to all layers +# https://huggingface.co/docs/peft/v0.15.0/en/package_reference/lora#peft.LoraConfig.layers_to_transform +peft_layers_to_transform: + +# Optional[bool]. Whether to use DoRA. +# https://huggingface.co/docs/peft/v0.15.0/en/developer_guides/lora#weight-decomposed-low-rank-adaptation-dora +peft_use_dora: + +# Optional[bool]. Whether to use RSLoRA. +# https://huggingface.co/docs/peft/v0.15.0/en/developer_guides/lora#rank-stabilized-lora +peft_use_rslora: + +# Optional[list[tuple[int, int]]]. List of layer indices to replicate. +# https://huggingface.co/docs/peft/v0.15.0/en/developer_guides/lora#memory-efficient-layer-replication-with-lora +peft_layer_replication: + +# bool | Literal["gaussian", "eva", "olora", "pissa", "pissa_niter_[number of iters]", "corda", "loftq"] +# How to initialize LoRA weights. Default to True which is MS original implementation. +# https://huggingface.co/docs/peft/v0.15.0/en/developer_guides/lora#initialization +peft_init_lora_weights: # If you added new tokens to the tokenizer, you may need to save some LoRA modules because they need to know the new tokens. # For LLaMA and Mistral, you need to save `embed_tokens` and `lm_head`. It may vary for other models. From f4ae8816bbaed25547bbd6809c36c9ca7e2ba0d0 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 1 Apr 2025 20:20:00 +0700 Subject: [PATCH 0506/1405] Fix: remove the numerous sequential log (#2461) * fix: remove sequential logs * feat(doc): add for sample pack sequentially and curriculum sampling --- docs/config.qmd | 4 ++++ src/axolotl/utils/samplers/multipack.py | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index 2ed365d75f..7c41c5126c 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -320,9 +320,13 @@ total_num_tokens: sample_packing_group_size: 100000 # The number of samples which can be packed into one sequence. Increase if using a large sequence_len with many short samples. sample_packing_bin_size: 200 +sample_pack_sequentially: # Optional[bool]. Whether to pack samples sequentially. + # whether to concatenate samples during pretraining pretraining_sample_concatenation: +curriculum_sampling: # Optional[bool]. Whether to use sequential sampling for curriculum learning + # Use batch flattening for speedups when not using sample_packing batch_flattening: diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index ef47aca877..0d8806d8b5 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -12,7 +12,9 @@ from axolotl.utils.distributed import reduce_and_broadcast -LOG = logging.getLogger("axolotl.utils.samplers.multipack") +LOG = logging.getLogger(__name__) + +LOG.setLevel(logging.INFO) @numba.njit @@ -202,7 +204,6 @@ def generate_batches(self, set_stats=False): lengths_cumsum = np.cumsum(lengths) if self.sequential: - LOG.debug("using sequential sample packing algorithm") batches, total_used, total_slots = allocate_sequentially( lengths=lengths, rank=0, @@ -210,7 +211,6 @@ def generate_batches(self, set_stats=False): n=1, ) else: - LOG.debug("using non-sequential sample packing algorithm") batches, total_used, total_slots = allocate( lengths=lengths, lengths_cumsum=lengths_cumsum, From df119e372418d3ce118b8c9ece04bda51489b566 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 1 Apr 2025 09:39:12 -0400 Subject: [PATCH 0507/1405] Validation for Muon optimizer with DS/FSDP (#2464) --- src/axolotl/utils/schemas/config.py | 11 +++++++ tests/test_validation_dataset.py | 45 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 3e072b6b43..1ec9e296d4 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1135,6 +1135,17 @@ def check_sequence_parallel_degree(cls, value, info): return value + @model_validator(mode="before") + @classmethod + def check_muon_deepspeed_fsdp(cls, data): + if data.get("optimizer") == "muon" and ( + data.get("deepspeed") or data.get("fsdp") or data.get("fsdp_config") + ): + raise ValueError( + "Muon optimizer is currently incompatible with DeepSpeed and FSDP" + ) + return data + class AxolotlConfigWCapabilities(AxolotlInputConfig): """wrapper to valdiate gpu capabilities with the configured options""" diff --git a/tests/test_validation_dataset.py b/tests/test_validation_dataset.py index 47d10ee99c..ba142f3bf0 100644 --- a/tests/test_validation_dataset.py +++ b/tests/test_validation_dataset.py @@ -321,3 +321,48 @@ def test_message_property_mappings(self, minimal_cfg): ) validate_config(cfg) + + +class TestOptimizerValidation(BaseValidation): + """ + Test muon optimizer validation + """ + + def test_muon_deepspeed(self, minimal_cfg): + cfg = DictDefault( + minimal_cfg + | { + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + } + ], + "optimizer": "muon", + "deepspeed": "deepspeed_configs/zero3.json", + } + ) + + with pytest.raises(ValueError, match=r".*is currently incompatible with*"): + validate_config(cfg) + + def test_muon_fsdp(self, minimal_cfg): + cfg = DictDefault( + minimal_cfg + | { + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + } + ], + "optimizer": "muon", + "fsdp": ["full_shard"], + "fsdp_config": { + "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + }, + } + ) + + with pytest.raises(ValueError, match=r".*is currently incompatible with*"): + validate_config(cfg) From 7d0eb66b54b243e0a91ae76af4dd89749b9217e9 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Tue, 1 Apr 2025 11:59:08 -0400 Subject: [PATCH 0508/1405] fixing eval for SP (#2468) --- setup.py | 6 ++- src/axolotl/core/trainers/base.py | 27 +--------- .../core/trainers/mixins/sequence_parallel.py | 51 +++++++++++++++++++ 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/setup.py b/setup.py index d19c14828c..aef8182af8 100644 --- a/setup.py +++ b/setup.py @@ -106,7 +106,11 @@ def get_package_version(): extras_require = { "flash-attn": ["flash-attn==2.7.4.post1"], - "ring-flash-attn": ["ring-flash-attn>=0.1.4", "yunchang==0.6.0"], + "ring-flash-attn": [ + "flash-attn==2.7.4.post1", + "ring-flash-attn>=0.1.4", + "yunchang==0.6.0", + ], "deepspeed": [ "deepspeed==0.16.4", "deepspeed-kernels", diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 436d3a0731..9fed78eb72 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -8,12 +8,11 @@ import os from collections import defaultdict from functools import wraps -from typing import Any, Literal +from typing import Literal import datasets import torch from datasets import Dataset -from torch import nn from torch.utils.data import ( BatchSampler, DataLoader, @@ -593,27 +592,3 @@ def _save_checkpoint(self, model, trial, **kwargs): output_dir = os.path.join(run_dir, checkpoint_folder) os.makedirs(output_dir, exist_ok=True) return super()._save_checkpoint(model, trial, **kwargs) - - def training_step( - self, - model: nn.Module, - inputs: dict[str, torch.Tensor | Any], - num_items_in_batch: int | None = None, - ) -> torch.Tensor: - """ - Perform a training step on a batch of inputs. Overrides the - `transformers.trainer.Trainer` method to handle sequence parallelism if - enabled. - - Args: - model: Model to perform training step for. - inputs: Dictionary mapping. - """ - # Set up sequence parallelism for this step if enabled - if self.args.sequence_parallel_degree > 1: - self._update_ring_flash_attn_params(inputs) - - # Proceed with normal training step - loss = super().training_step(model, inputs, num_items_in_batch) - - return loss diff --git a/src/axolotl/core/trainers/mixins/sequence_parallel.py b/src/axolotl/core/trainers/mixins/sequence_parallel.py index f52c044b6f..9bcd5db578 100644 --- a/src/axolotl/core/trainers/mixins/sequence_parallel.py +++ b/src/axolotl/core/trainers/mixins/sequence_parallel.py @@ -7,6 +7,7 @@ import torch.distributed as dist import torch.nn.functional as F from datasets import Dataset +from torch import nn from torch.utils.data import DistributedSampler, Sampler from axolotl.monkeypatch.attention.ring_attn import get_ring_attn_group @@ -129,3 +130,53 @@ def _update_ring_flash_attn_params(self, inputs: dict[str, torch.Tensor | Any]): ) update_ring_flash_attn_params(cu_seqlens, self.ring_attn_group) + + def training_step( + self, + model: nn.Module, + inputs: dict[str, torch.Tensor | Any], + num_items_in_batch: int | None = None, + ) -> torch.Tensor: + """ + Perform a training step on a batch of inputs. Overrides the + `transformers.trainer.Trainer` method to handle sequence parallelism if + enabled. + + Args: + model: Model to perform training step for. + inputs: Dictionary mapping. + """ + # Set up sequence parallelism for this step if enabled + if self.args.sequence_parallel_degree > 1: + self._update_ring_flash_attn_params(inputs) + + # Proceed with normal training step + return super().training_step(model, inputs, num_items_in_batch) # type: ignore + + def prediction_step( + self, + model: nn.Module, + inputs: dict[str, torch.Tensor | Any], + prediction_loss_only: bool, + ignore_keys: list[str] | None = None, + ) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]: + """ + Perform a prediction step on a batch of inputs. Overrides the + `transformers.trainer.Trainer` method to handle sequence parallelism if + enabled. + + Args: + model: Model to perform prediction step for. + inputs: Dictionary mapping of inputs. + prediction_loss_only: Whether to return only the loss. + ignore_keys: Keys to ignore in the inputs. + + Returns: + Tuple of (loss, logits, labels). + """ + # Set up sequence parallelism for this prediction step if enabled + if self.args.sequence_parallel_degree > 1: + self._update_ring_flash_attn_params(inputs) + + # Proceed with normal prediction step + return super().prediction_step(model, inputs, prediction_loss_only, ignore_keys) # type: ignore From 990b5896bc8abe11b6cce0eee0edf2629f85d9b6 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 1 Apr 2025 23:25:05 +0700 Subject: [PATCH 0509/1405] fix: downgrade deepspeed to fix grad checkpoint oom (#2465) [skip ci] --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 567b446dda..dde64f3929 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ transformers==4.50.3 tokenizers>=0.21.1 accelerate==1.5.2 datasets==3.5.0 -deepspeed==0.16.4 +deepspeed==0.15.4 trl==0.16.0 optimum==1.16.2 diff --git a/setup.py b/setup.py index aef8182af8..29719b1f32 100644 --- a/setup.py +++ b/setup.py @@ -112,7 +112,7 @@ def get_package_version(): "yunchang==0.6.0", ], "deepspeed": [ - "deepspeed==0.16.4", + "deepspeed==0.15.4", "deepspeed-kernels", ], "mamba-ssm": [ From 9e22c4ca6aed4e94727d1c665bd7aa5b11f8b499 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 1 Apr 2025 23:25:53 +0700 Subject: [PATCH 0510/1405] fix: set rl=None during inference (#2463) --- src/axolotl/cli/inference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index f4fbdbed4a..a4906bbf36 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -256,7 +256,7 @@ def do_cli( """ # pylint: disable=duplicate-code print_axolotl_text_art() - parsed_cfg = load_cfg(config, inference=True, **kwargs) + parsed_cfg = load_cfg(config, inference=True, rl=None, **kwargs) parsed_cfg.sample_packing = False parser = transformers.HfArgumentParser(InferenceCliArgs) parsed_cli_args, _ = parser.parse_args_into_dataclasses( From 0bfa180f7d90b1483b676b4dd4f6bf2caf36891b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 1 Apr 2025 15:38:26 -0400 Subject: [PATCH 0511/1405] torch 2.7.0 base image for testing (#2467) --- .github/workflows/base.yml | 8 +++++++- docker/Dockerfile-base-next | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 docker/Dockerfile-base-next diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 01b898310b..30859a3749 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -52,6 +52,12 @@ jobs: python_version: "3.11" pytorch: nightly torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + - cuda: "128" + cuda_version: 12.8.1 + cudnn_version: "" + python_version: "3.11" + pytorch: next + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" steps: - name: Checkout uses: actions/checkout@v4 @@ -73,7 +79,7 @@ jobs: uses: docker/build-push-action@v4 with: context: . - file: ${{ matrix.pytorch == 'nightly' && './docker/Dockerfile-base-nightly' || './docker/Dockerfile-base' }} + file: ${{ matrix.pytorch == 'nightly' && './docker/Dockerfile-base-nightly' || matrix.pytorch == 'next' && './docker/Dockerfile-base-next' || './docker/Dockerfile-base' }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.metadata.outputs.tags }}-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} labels: ${{ steps.metadata.outputs.labels }} diff --git a/docker/Dockerfile-base-next b/docker/Dockerfile-base-next new file mode 100644 index 0000000000..a968b5913c --- /dev/null +++ b/docker/Dockerfile-base-next @@ -0,0 +1,38 @@ +ARG CUDA_VERSION="12.8.1" +ARG CUDNN_VERSION="8" +ARG UBUNTU_VERSION="22.04" +ARG MAX_JOBS=4 + +FROM nvidia/cuda:$CUDA_VERSION-cudnn$CUDNN_VERSION-devel-ubuntu$UBUNTU_VERSION AS base-builder + +ENV PATH="/root/miniconda3/bin:${PATH}" + +ARG PYTHON_VERSION="3.11" +ARG PYTORCH_VERSION="next" +ARG CUDA="128" +ARG TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6 9.0+PTX" + +ENV PYTHON_VERSION=$PYTHON_VERSION +ENV TORCH_CUDA_ARCH_LIST=$TORCH_CUDA_ARCH_LIST + +RUN apt-get update \ + && apt-get install -y wget git build-essential ninja-build git-lfs libaio-dev pkg-config && rm -rf /var/lib/apt/lists/* \ + && wget \ + https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \ + && mkdir /root/.conda \ + && bash Miniconda3-latest-Linux-x86_64.sh -b \ + && rm -f Miniconda3-latest-Linux-x86_64.sh \ + && conda create -n "py${PYTHON_VERSION}" python="${PYTHON_VERSION}" + +ENV PATH="/root/miniconda3/envs/py${PYTHON_VERSION}/bin:${PATH}" + +WORKDIR /workspace + +RUN python3 -m pip install --upgrade pip && pip3 install packaging && \ + python3 -m pip install --no-cache-dir -U torch==2.7.0 --extra-index-url https://download.pytorch.org/whl/test/cu$CUDA && \ + python3 -m pip install --no-cache-dir "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" && \ + python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" + +RUN git lfs install --skip-repo && \ + pip3 install awscli && \ + pip3 install -U --no-cache-dir pydantic==2.10.6 From 80ba4b69f154969357df1cd0949b7395ffdfb1c5 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 2 Apr 2025 18:40:49 +0700 Subject: [PATCH 0512/1405] fix: pydantic warning validator not returning self (#2474) --- src/axolotl/utils/schemas/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 1ec9e296d4..a0d04ccc22 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1290,3 +1290,5 @@ def check_min_torch_version(self): LOG.warning( f"torch=={torch_version} may not be supported in future versions. Please consider upgrading to torch>=2.5.1." ) + + return self From 45bf634d17efb5db57843d28259400f3af370f36 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 2 Apr 2025 20:33:46 +0700 Subject: [PATCH 0513/1405] feat: add support for multimodal in lora kernels (#2472) [skip ci] * feat: add support for multimodal in lora kernels * fix: improve multimodal checks * fix: add fallback for model config * chor: add gemma3 to docs --- docs/lora_optims.qmd | 1 + src/axolotl/monkeypatch/lora_kernels.py | 30 +++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/lora_optims.qmd b/docs/lora_optims.qmd index a7555a0a33..56d56e9fc8 100644 --- a/docs/lora_optims.qmd +++ b/docs/lora_optims.qmd @@ -17,6 +17,7 @@ We currently support several common model architectures, including (but not limi - `qwen2` - `gemma` - `gemma2` +- `gemma3`
diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index d59fd22c93..96cfb1b692 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -252,12 +252,38 @@ def apply_lora_kernel_patches( LOG.setLevel(logging.INFO) # Choose activation based on model type - activation = model.config.hidden_act + activation = None + text_config = ( + model.config.get_text_config() + if hasattr(model.config, "get_text_config") + else model.config + ) + if hasattr(text_config, "hidden_act"): + activation = text_config.hidden_act + elif hasattr(text_config, "hidden_activation"): + activation = text_config.hidden_activation + + # map activation to supported activation + if "gelu" in activation: + # gemma3 uses gelu_pytorch_tanh + activation = "gelu" + if activation not in SUPPORTED_ACTIVATIONS: raise NotImplementedError(f"Activation {activation} is not supported") + layers = [] + # check for multimodal models first + if hasattr(model, "language_model"): + layers = model.language_model.model.layers + elif hasattr(model, "model"): + layers = model.model.model.layers + else: + raise NotImplementedError( + f"Model type {model.config.model_type} is not supported yet. Please create an Issue." + ) + # Patch each layer - for layer in model.model.model.layers: + for layer in layers: # Add QKV, O fallback implementations to start # These will be overwritten later (if some conditions apply) layer.self_attn.apply_qkv = types.MethodType( From 7abc71dc0b98963a98151913ccf856b62ad46b97 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 2 Apr 2025 20:34:41 +0700 Subject: [PATCH 0514/1405] fix: gemma3 loss in forward pass (#2473) [skip ci] * fix: gemma3 loss in forward pass * fix: lint * fix: move patch before plugins * Update src/axolotl/monkeypatch/gemma3.py Co-authored-by: salman --------- Co-authored-by: Wing Lian Co-authored-by: salman --- src/axolotl/monkeypatch/gemma3.py | 238 ++++++++++++++++++++++++++++++ src/axolotl/utils/models.py | 9 ++ 2 files changed, 247 insertions(+) create mode 100644 src/axolotl/monkeypatch/gemma3.py diff --git a/src/axolotl/monkeypatch/gemma3.py b/src/axolotl/monkeypatch/gemma3.py new file mode 100644 index 0000000000..38183fa0ed --- /dev/null +++ b/src/axolotl/monkeypatch/gemma3.py @@ -0,0 +1,238 @@ +"""Monkeypatch for gemma3 conditional generation forward to fix loss exploding""" + +# pylint: disable=duplicate-code + +from typing import Optional, Tuple, Union + +import torch +from transformers.cache_utils import Cache +from transformers.models.gemma3.modeling_gemma3 import ( + _CONFIG_FOR_DOC, + GEMMA3_INPUTS_DOCSTRING, + Gemma3CausalLMOutputWithPast, + logger, +) +from transformers.utils import ( + add_start_docstrings_to_model_forward, + is_torchdynamo_compiling, + replace_return_docstrings, +) +from transformers.utils.deprecation import deprecate_kwarg + + +@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") +@add_start_docstrings_to_model_forward(GEMMA3_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=Gemma3CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def new_forward( + self, + input_ids: torch.LongTensor = None, + pixel_values: torch.FloatTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Union[list[torch.FloatTensor], Cache]] = None, + token_type_ids: Optional[torch.LongTensor] = None, + cache_position: Optional[torch.LongTensor] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **lm_kwargs, +) -> Union[Tuple, Gemma3CausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.text_config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.text_config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + + Example: + + ```python + >>> from PIL import Image + >>> import requests + >>> from transformers import AutoProcessor, Gemma3ForConditionalGeneration + + >>> model = Gemma3ForConditionalGeneration.from_pretrained("google/Gemma3-test-224px-hf") + >>> processor = AutoProcessor.from_pretrained("google/Gemma3-test-224px-hf") + + >>> prompt = "answer en Where is the cow standing?" + >>> url = "https://huggingface.co/gv-hf/Gemma3-test-224px-hf/resolve/main/cow_beach_1.png" + >>> image = Image.open(requests.get(url, stream=True).raw) + + >>> inputs = processor(images=image, text=prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(**inputs, max_length=30) + >>> processor.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "answer en Where is the cow standing?\nbeach" + ```""" + + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + is_training = token_type_ids is not None and labels is not None + + # Replace image id with PAD if the image token is OOV, to avoid index-errors + if input_ids is not None and self.config.image_token_index >= self.vocab_size: + special_image_mask = input_ids == self.config.image_token_index + llm_input_ids = input_ids.clone() + llm_input_ids[special_image_mask] = 0 + else: + llm_input_ids = input_ids + + if inputs_embeds is None: + inputs_embeds = self.get_input_embeddings()(llm_input_ids) + + if cache_position is None: + past_seen_tokens = ( + past_key_values.get_seq_length() if past_key_values is not None else 0 + ) + cache_position = torch.arange( + past_seen_tokens, + past_seen_tokens + inputs_embeds.shape[1], + device=inputs_embeds.device, + ) + + # Merge text and images + if pixel_values is not None: + image_features = self.get_image_features(pixel_values) + + if input_ids is None: + special_image_mask = inputs_embeds == self.get_input_embeddings()( + torch.tensor( + self.config.image_token_index, + dtype=torch.long, + device=inputs_embeds.device, + ) + ) + else: + special_image_mask = (input_ids == self.config.image_token_index).unsqueeze( + -1 + ) + special_image_mask = special_image_mask.expand_as(inputs_embeds).to( + inputs_embeds.device + ) + + if ( + not is_torchdynamo_compiling() + and inputs_embeds[special_image_mask].numel() != image_features.numel() + ): + image_tokens_in_text = (special_image_mask).sum(dim=1).sum(dim=0)[0] + raise ValueError( + f"Number of images does not match number of special image tokens in the input text. " + f"Got {image_tokens_in_text} image tokens in the text but {image_features.shape[0] * image_features.shape[1]} " + "tokens from image embeddings." + ) + image_features = image_features.to(inputs_embeds.device, inputs_embeds.dtype) + inputs_embeds = inputs_embeds.masked_scatter(special_image_mask, image_features) + + # mask out pad-token-ids in labels for BC + if labels is not None and self.pad_token_id in labels: + logger.warning_once( + "`labels` contains `pad_token_id` which will be masked with `config.ignore_index`. " + "You have to mask out `pad_token_id` when preparing `labels`, this behavior will be removed in v.4.46.", + ) + labels = torch.where( + input_ids == self.pad_token_id, self.config.ignore_index, labels + ) + + causal_mask = self._update_causal_mask( # pylint: disable=protected-access + attention_mask, + token_type_ids, + past_key_values, + cache_position, + inputs_embeds, + is_training, + ) + outputs = self.language_model( + attention_mask=causal_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + logits_to_keep=logits_to_keep, + **lm_kwargs, + ) + + logits = outputs[0] + loss = None + if labels is not None: + if attention_mask is not None: + # Get the shifted attention mask + shift_attention_mask = attention_mask[:, -logits.shape[1] + 1 :].to( + logits.device + ) # +1 for shift + + # Filter logits and labels based on attention mask + valid_indices = shift_attention_mask != 0 + filtered_logits = logits[..., :-1, :][valid_indices] + filtered_labels = labels[..., 1:][valid_indices.to(labels.device)] + + # TODO: do we need to handle num_items_in_batch given we filter the logits and labels? + + loss = self.loss_function( + logits=filtered_logits, + labels=None, # we pass shift_labels + shift_labels=filtered_labels, + vocab_size=self.config.text_config.vocab_size, + **lm_kwargs, + ) + else: + # Standard case without filtering + loss = self.loss_function( + logits=logits, + labels=labels, + vocab_size=self.config.text_config.vocab_size, + **lm_kwargs, + ) + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return Gemma3CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + image_hidden_states=image_features if pixel_values is not None else None, + ) + + +def patch_gemma3conditionalgeneration_forward(): + from transformers.models.gemma3.modeling_gemma3 import ( + Gemma3ForConditionalGeneration, + ) + + Gemma3ForConditionalGeneration.forward = new_forward diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 9611ffca23..9df63231b8 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -535,6 +535,15 @@ def __init__( self.auto_model_loader = AutoModelForCausalLM # pylint: disable=invalid-name def apply_patches(self) -> None: + # patch gemma3 conditional generation forward before loading plugins + # as it could be overridden by plugins + if self.cfg.model_config_type == "gemma3": + from axolotl.monkeypatch.gemma3 import ( + patch_gemma3conditionalgeneration_forward, + ) + + patch_gemma3conditionalgeneration_forward() + # load any patches from plugins from axolotl.integrations.base import PluginManager From e6cfb093d2877b44a354ed4399c635a55a9d7000 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 2 Apr 2025 20:35:00 +0700 Subject: [PATCH 0515/1405] fix: disable SP during merge (#2470) [skip ci] --- src/axolotl/cli/merge_lora.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 2a3343b6b9..1059218fa2 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -74,8 +74,10 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: load_in_8bit=False, load_in_4bit=False, flash_attention=False, + sequence_parallel_degree=None, deepspeed=None, fsdp=None, + fsdp_config=None, **kwargs, ) @@ -86,13 +88,6 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: f"Target directory for merge: `{parsed_cfg.lora_model_dir}` does not exist." ) - parsed_cfg.load_in_4bit = False - parsed_cfg.load_in_8bit = False - parsed_cfg.flash_attention = False - parsed_cfg.deepspeed = None - parsed_cfg.fsdp = None - parsed_cfg.fsdp_config = None - do_merge_lora(cfg=parsed_cfg) From a0117c9bcec3809f54576df3ff8a1c28ec672b49 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 2 Apr 2025 20:35:29 +0700 Subject: [PATCH 0516/1405] fix: separate gemma3 text and vision example config (#2471) [skip ci] * fix: separate gemma3 text and vision example config * fix: update to use a text-only dataset * fix: typo --- examples/gemma3/gemma-3-4b-qlora.yml | 66 +++++++++++++++++++ ...b-lora.yml => gemma-3-4b-vision-qlora.yml} | 2 +- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 examples/gemma3/gemma-3-4b-qlora.yml rename examples/gemma3/{gemma-3-4b-lora.yml => gemma-3-4b-vision-qlora.yml} (98%) diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml new file mode 100644 index 0000000000..28b7bdacf2 --- /dev/null +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -0,0 +1,66 @@ +base_model: google/gemma-3-4b-it +strict: false + +# gemma3 doesn't seem to play nice with ddp +ddp_find_unused_parameters: true + +chat_template: gemma3 +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +dataset_prepared_path: last_run_prepared +val_set_size: 0.01 +output_dir: ./outputs/out + +adapter: lora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true +pad_to_sequence_len: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +local_rank: +logging_steps: 1 +flash_attention: true +eager_attention: + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: diff --git a/examples/gemma3/gemma-3-4b-lora.yml b/examples/gemma3/gemma-3-4b-vision-qlora.yml similarity index 98% rename from examples/gemma3/gemma-3-4b-lora.yml rename to examples/gemma3/gemma-3-4b-vision-qlora.yml index 0e7422bd47..d51dd88b13 100644 --- a/examples/gemma3/gemma-3-4b-lora.yml +++ b/examples/gemma3/gemma-3-4b-vision-qlora.yml @@ -20,7 +20,7 @@ dataset_prepared_path: last_run_prepared val_set_size: 0.01 output_dir: ./outputs/out -adapter: lora +adapter: qlora lora_model_dir: sequence_len: 2048 From adb593abac7ce755e2d7b78005c8d0bfcf308523 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 2 Apr 2025 20:35:42 +0700 Subject: [PATCH 0517/1405] fix: document offload gradient_checkpointing option (#2475) --- docs/config.qmd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/config.qmd b/docs/config.qmd index 7c41c5126c..6414b63a5a 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -510,7 +510,8 @@ train_on_inputs: false # Note that training loss may have an oscillating pattern with this enabled. group_by_length: false -# Whether to use gradient checkpointing https://huggingface.co/docs/transformers/v4.18.0/en/performance#gradient-checkpointing +# Whether to use gradient checkpointing. Available options are: true, false, "offload". +# https://huggingface.co/docs/transformers/v4.18.0/en/performance#gradient-checkpointing gradient_checkpointing: false # additional kwargs to pass to the trainer for gradient checkpointing # gradient_checkpointing_kwargs: From 3877c5c69da81a7914ea02bbb36fcf101c409771 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 2 Apr 2025 09:50:56 -0400 Subject: [PATCH 0518/1405] set release version 0.8.0 (#2476) * set release version 0.8.0 * make sure to include ring-flash-attn in docker image build --- docker/Dockerfile | 4 ++-- src/axolotl/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index aaaff23eff..e23a729d4d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,9 +20,9 @@ WORKDIR /workspace/axolotl # If AXOLOTL_EXTRAS is set, append it in brackets RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,optimizers,ray] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray] $AXOLOTL_ARGS; \ fi RUN python scripts/unsloth_install.py | sh diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index d3c03cbea2..b4d8b75b73 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -4,4 +4,4 @@ __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.8.0.dev0" +__version__ = "0.8.0" From 5249e98058bbc032c925b4dffc75c703ae39657e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 3 Apr 2025 08:47:52 -0400 Subject: [PATCH 0519/1405] add additional tf32 opt for cudnn (#2477) [skip ci] --- src/axolotl/utils/config/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 634575066f..b527dce08b 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -78,6 +78,7 @@ def resolve_dtype(cfg): cfg.bf16 = False else: torch.backends.cuda.matmul.allow_tf32 = cfg.tf32 or False + torch.backends.cudnn.allow_tf32 = cfg.tf32 or False if cfg.bf16: cfg.fp16 = False From 64d8035f50e8c1c5515f705badaa685c9754d1f3 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 3 Apr 2025 19:48:14 +0700 Subject: [PATCH 0520/1405] fix(example): align example to correct adapter (#2478) * fix(example): align example to correct adapter * fix: add missing load in 4 bit --- examples/gemma3/gemma-3-4b-qlora.yml | 4 +++- examples/gemma3/gemma-3-4b-vision-qlora.yml | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml index 28b7bdacf2..b4be17c3cb 100644 --- a/examples/gemma3/gemma-3-4b-qlora.yml +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -1,6 +1,8 @@ base_model: google/gemma-3-4b-it strict: false +load_in_4bit: true + # gemma3 doesn't seem to play nice with ddp ddp_find_unused_parameters: true @@ -17,7 +19,7 @@ dataset_prepared_path: last_run_prepared val_set_size: 0.01 output_dir: ./outputs/out -adapter: lora +adapter: qlora lora_model_dir: sequence_len: 2048 diff --git a/examples/gemma3/gemma-3-4b-vision-qlora.yml b/examples/gemma3/gemma-3-4b-vision-qlora.yml index d51dd88b13..6e711d6f61 100644 --- a/examples/gemma3/gemma-3-4b-vision-qlora.yml +++ b/examples/gemma3/gemma-3-4b-vision-qlora.yml @@ -2,6 +2,8 @@ base_model: google/gemma-3-4b-it processor_type: AutoProcessor strict: false +load_in_4bit: true + # these 3 lines are needed for now to handle vision chat templates w images skip_prepare_dataset: true remove_unused_columns: false From e0cc4f1a87824a42bcb75a58a438d1802b6179a6 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Thu, 3 Apr 2025 14:50:56 -0400 Subject: [PATCH 0521/1405] removing deepspeed guard for LoRA Triton kernels (#2480) --- src/axolotl/utils/schemas/config.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index a0d04ccc22..c9a208ba22 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1224,17 +1224,12 @@ def check_multigpu_lora_kernels(cls, data): ): capabilities = data.get("capabilities") is_fsdp = data.get("fsdp") is not None - is_deepspeed = data.get("deepspeed") is not None if capabilities and capabilities.get("n_gpu", 0) > 1: if is_fsdp: raise ValueError( "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with FSDP." ) - if is_deepspeed: - raise ValueError( - "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with DeepSpeed." - ) return data @model_validator(mode="before") From dd66fb163c5df4a9c928e74f34262b22137527d1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 4 Apr 2025 13:47:01 -0400 Subject: [PATCH 0522/1405] check if fixture exists in the cache already (#2485) * check if fixture exists in the cache already * add docstring explaining what is going on --- tests/conftest.py | 17 ++++++++++++++++- tests/hf_offline_utils.py | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4d05d3a269..c71ea1e8c4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,10 +14,15 @@ import pytest import requests from huggingface_hub import snapshot_download +from huggingface_hub.errors import LocalEntryNotFoundError from tokenizers import AddedToken from transformers import AutoTokenizer -from tests.hf_offline_utils import disable_hf_offline, enable_hf_offline +from tests.hf_offline_utils import ( + disable_hf_offline, + enable_hf_offline, + hf_offline_context, +) def retry_on_request_exceptions(max_retries=3, delay=1): @@ -47,6 +52,16 @@ def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements @retry_on_request_exceptions(max_retries=3, delay=5) @disable_hf_offline def snapshot_download_w_retry(*args, **kwargs): + """ + download a model or dataset from HF Hub, retrying in requests failures. We also try to fetch it from the local + cache first using hf_hub_offline to avoid hitting HF Hub API rate limits. If it doesn't exist in the cache, + disable hf_hub_offline and actually fetch from the hub + """ + with hf_offline_context(True): + try: + return snapshot_download(*args, **kwargs) + except LocalEntryNotFoundError: + pass return snapshot_download(*args, **kwargs) diff --git a/tests/hf_offline_utils.py b/tests/hf_offline_utils.py index 0ce8785774..0c7b5d4a46 100644 --- a/tests/hf_offline_utils.py +++ b/tests/hf_offline_utils.py @@ -3,6 +3,7 @@ """ import os +from contextlib import contextmanager from functools import wraps from huggingface_hub.utils import reset_sessions @@ -83,3 +84,23 @@ def wrapper(*args, **kwargs): reload_modules(False) return wrapper + + +@contextmanager +def hf_offline_context(hf_hub_offline): + """ + Context manager that sets HF_HUB_OFFLINE environment variable to the given value. + :param hf_hub_offline: The new value for HF_HUB_OFFLINE. + :return: A context manager. + """ + original_hf_offline = os.getenv("HF_HUB_OFFLINE") + os.environ["HF_HUB_OFFLINE"] = str(hf_hub_offline) + reload_modules(True) + yield + # Restore the original value of HF_HUB_OFFLINE environment variable + if original_hf_offline is not None: + os.environ["HF_HUB_OFFLINE"] = original_hf_offline + reload_modules(bool(original_hf_offline)) + else: + del os.environ["HF_HUB_OFFLINE"] + reload_modules(False) From 9f824ef76af48eff9f69f3a93ede03a6a2b4313a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 4 Apr 2025 13:47:26 -0400 Subject: [PATCH 0523/1405] simplify the example configs to be more minimal and less daunting (#2486) [skip ci] * simplify the example configs to be more minimal and less daunting * drop empty s2_attention from example yamls --- examples/cerebras/btlm-ft.yml | 10 ---------- examples/cerebras/qlora.yml | 10 ---------- examples/code-llama/13b/lora.yml | 12 ------------ examples/code-llama/13b/qlora.yml | 12 ------------ examples/code-llama/34b/lora.yml | 12 ------------ examples/code-llama/34b/qlora.yml | 12 ------------ examples/code-llama/7b/lora.yml | 12 ------------ examples/code-llama/7b/qlora.yml | 12 ------------ examples/cohere/command-r-7b-qlora.yml | 12 ------------ examples/dbrx/16bit-lora.yaml | 11 +---------- examples/dbrx/8bit-lora.yaml | 8 +------- examples/dbrx/fft-ds-zero3.yaml | 11 +---------- examples/deepseek-v2/fft-fsdp-16b.yaml | 11 ----------- examples/deepseek-v2/qlora-fsdp-2_5.yaml | 8 -------- examples/falcon/config-7b-lora.yml | 11 ----------- examples/falcon/config-7b-qlora.yml | 10 ---------- examples/falcon/config-7b.yml | 14 -------------- examples/gemma/qlora.yml | 12 ------------ examples/gemma2/qlora.yml | 12 ------------ examples/gemma2/reward-model.yaml | 14 -------------- examples/gemma3/gemma-3-1b-qlora.yml | 12 ------------ examples/gemma3/gemma-3-4b-qlora.yml | 7 ------- examples/gemma3/gemma-3-4b-vision-qlora.yml | 7 ------- examples/gptj/qlora.yml | 11 ----------- examples/jamba/qlora.yaml | 8 -------- examples/jamba/qlora_deepspeed.yaml | 8 +------- examples/jamba/qlora_fsdp_large.yaml | 2 -- examples/jeopardy-bot/config.yml | 8 -------- examples/llama-2/fft_optimized.yml | 15 +-------------- examples/llama-2/gptq-lora.yml | 10 ---------- examples/llama-2/lisa.yml | 15 --------------- examples/llama-2/loftq.yml | 17 ----------------- examples/llama-2/lora.yml | 14 -------------- examples/llama-2/qlora-fsdp.yml | 11 ----------- examples/llama-2/qlora.yml | 13 ------------- examples/llama-2/relora.yml | 12 ------------ examples/llama-3-vision/lora-11b.yaml | 7 ------- examples/llama-3/fft-8b-liger-fsdp.yaml | 8 -------- examples/llama-3/fft-8b.yaml | 13 ------------- examples/llama-3/instruct-dpo-lora-8b.yml | 14 -------------- examples/llama-3/instruct-lora-8b.yml | 14 -------------- examples/llama-3/lora-1b-deduplicate-dpo.yml | 14 -------------- examples/llama-3/lora-1b-deduplicate-sft.yml | 14 -------------- examples/llama-3/lora-1b-kernels.yml | 14 -------------- examples/llama-3/lora-1b-ray.yml | 14 +------------- .../lora-1b-sample-packing-sequentially.yml | 14 -------------- examples/llama-3/lora-1b.yml | 14 -------------- examples/llama-3/lora-8b.yml | 14 -------------- examples/llama-3/qlora-1b-kto.yaml | 13 ------------- examples/llama-3/qlora-1b.yml | 13 ------------- examples/llama-3/qlora-fsdp-405b.yaml | 3 --- examples/llama-3/qlora-fsdp-70b.yaml | 11 ----------- examples/llama-3/qlora.yml | 13 ------------- examples/llava/lora-7b.yaml | 7 ------- examples/mamba/config.yml | 13 ------------- examples/mistral/bigstral-ds-zero3.yaml | 13 +------------ examples/mistral/config.yml | 15 --------------- examples/mistral/lora-mps.yml | 15 --------------- examples/mistral/lora.yml | 13 ------------- examples/mistral/mistral-dpo-qlora.yml | 14 -------------- examples/mistral/mistral-qlora-fsdp.yml | 11 +---------- examples/mistral/mistral-qlora-orpo.yml | 13 ------------- examples/mistral/mistral-small-3.1-24B-lora.yml | 7 ------- examples/mistral/mixtral-8x22b-qlora-fsdp.yml | 11 +---------- examples/mistral/mixtral-qlora-fsdp.yml | 11 +---------- examples/mistral/mixtral.yml | 13 +------------ examples/mistral/mixtral_22.yml | 13 +------------ examples/mistral/qlora.yml | 13 ------------- examples/mpt-7b/config.yml | 9 --------- examples/openllama-3b/config.yml | 13 ------------- examples/openllama-3b/lora.yml | 11 ----------- examples/openllama-3b/qlora.yml | 11 ----------- examples/phi/lora-3.5.yaml | 13 ------------- examples/phi/phi-ft.yml | 14 -------------- examples/phi/phi-qlora.yml | 11 ----------- examples/phi/phi2-ft.yml | 14 -------------- examples/phi/phi3-ft-fsdp.yml | 12 ------------ examples/phi/phi3-ft.yml | 8 +------- examples/pixtral/lora-12b.yml | 7 ------- examples/pythia-12b/config.yml | 10 ---------- examples/pythia/lora.yml | 4 ---- examples/qwen/lora.yml | 13 ------------- examples/qwen/qlora.yml | 13 ------------- examples/qwen/qwen2-moe-lora.yaml | 14 -------------- examples/qwen/qwen2-moe-qlora.yaml | 11 ----------- examples/qwen2-vl/lora-7b.yaml | 7 ------- examples/qwen2/dpo.yaml | 12 ------------ examples/qwen2/prm.yaml | 14 -------------- examples/qwen2/qlora-fsdp.yaml | 9 --------- examples/qwen2/reward-model.yaml | 14 -------------- examples/redpajama/config-3b.yml | 9 --------- examples/replit-3b/config-lora.yml | 10 ---------- examples/stablelm-2/1.6b/fft.yml | 15 +-------------- examples/stablelm-2/1.6b/lora.yml | 11 ----------- examples/starcoder2/qlora.yml | 12 ------------ examples/tiny-llama/lora-mps.yml | 10 ---------- examples/tiny-llama/lora.yml | 11 ----------- examples/tiny-llama/pretrain.yml | 14 -------------- examples/tiny-llama/qlora.yml | 12 ------------ examples/xgen-7b/xgen-7b-8k-qlora.yml | 8 -------- examples/yi-34B-chat/qlora.yml | 13 ------------- 101 files changed, 14 insertions(+), 1140 deletions(-) diff --git a/examples/cerebras/btlm-ft.yml b/examples/cerebras/btlm-ft.yml index 44be539966..6190714b49 100644 --- a/examples/cerebras/btlm-ft.yml +++ b/examples/cerebras/btlm-ft.yml @@ -8,9 +8,6 @@ tokenizer_type: GPT2Tokenizer trust_remote_code: true tokenizer_use_fast: true tokenizer_legacy: true - -load_in_8bit: false -load_in_4bit: false strict: false push_dataset_to_hub: hf_use_auth_token: true @@ -34,7 +31,6 @@ lora_alpha: lora_dropout: lora_target_modules: lora_target_linear: -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -58,16 +54,12 @@ learning_rate: 0.000085 train_on_inputs: true group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true sdp_attention: flash_optimum: @@ -80,8 +72,6 @@ evals_per_epoch: 4 saves_per_epoch: 1 save_total_limit: -debug: -deepspeed: weight_decay: 0.1 special_tokens: pad_token: "<|endoftext|>" diff --git a/examples/cerebras/qlora.yml b/examples/cerebras/qlora.yml index 866b4ab58c..e74b2d675a 100644 --- a/examples/cerebras/qlora.yml +++ b/examples/cerebras/qlora.yml @@ -22,7 +22,6 @@ lora_target_modules: - c_attn - c_proj lora_target_linear: -lora_fan_in_fan_out: wandb_project: wandb_entity: wandb_watch: @@ -36,15 +35,10 @@ optimizer: paged_adamw_8bit torchdistx_path: lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 xformers_attention: true flash_attention: @@ -53,10 +47,6 @@ gptq_model_v1: warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.1 -fsdp: -fsdp_config: special_tokens: pad_token: "<|endoftext|>" diff --git a/examples/code-llama/13b/lora.yml b/examples/code-llama/13b/lora.yml index 2b8a720b24..6c205ae879 100644 --- a/examples/code-llama/13b/lora.yml +++ b/examples/code-llama/13b/lora.yml @@ -26,7 +26,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -41,29 +40,18 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true -s2_attention: warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: bos_token: "" eos_token: "" diff --git a/examples/code-llama/13b/qlora.yml b/examples/code-llama/13b/qlora.yml index 92aa6ac972..28f0275d35 100644 --- a/examples/code-llama/13b/qlora.yml +++ b/examples/code-llama/13b/qlora.yml @@ -26,9 +26,7 @@ pad_to_sequence_len: true lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -43,28 +41,18 @@ optimizer: paged_adamw_32bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: bos_token: "" eos_token: "" diff --git a/examples/code-llama/34b/lora.yml b/examples/code-llama/34b/lora.yml index af343e389a..6024ce3f7d 100644 --- a/examples/code-llama/34b/lora.yml +++ b/examples/code-llama/34b/lora.yml @@ -26,7 +26,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -41,29 +40,18 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true -s2_attention: warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: bos_token: "" eos_token: "" diff --git a/examples/code-llama/34b/qlora.yml b/examples/code-llama/34b/qlora.yml index f45e9205f4..56c276cc90 100644 --- a/examples/code-llama/34b/qlora.yml +++ b/examples/code-llama/34b/qlora.yml @@ -26,9 +26,7 @@ pad_to_sequence_len: true lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -43,28 +41,18 @@ optimizer: paged_adamw_32bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: bos_token: "" eos_token: "" diff --git a/examples/code-llama/7b/lora.yml b/examples/code-llama/7b/lora.yml index 6c385dbcb4..0eb20c2449 100644 --- a/examples/code-llama/7b/lora.yml +++ b/examples/code-llama/7b/lora.yml @@ -26,7 +26,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -41,29 +40,18 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true -s2_attention: warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: bos_token: "" eos_token: "" diff --git a/examples/code-llama/7b/qlora.yml b/examples/code-llama/7b/qlora.yml index ccd256406c..f078f13984 100644 --- a/examples/code-llama/7b/qlora.yml +++ b/examples/code-llama/7b/qlora.yml @@ -26,9 +26,7 @@ pad_to_sequence_len: true lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -43,28 +41,18 @@ optimizer: paged_adamw_32bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: bos_token: "" eos_token: "" diff --git a/examples/cohere/command-r-7b-qlora.yml b/examples/cohere/command-r-7b-qlora.yml index 2ac5c4c09d..8a2b6eacda 100644 --- a/examples/cohere/command-r-7b-qlora.yml +++ b/examples/cohere/command-r-7b-qlora.yml @@ -44,28 +44,16 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_ratio: 0.1 evals_per_epoch: -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/dbrx/16bit-lora.yaml b/examples/dbrx/16bit-lora.yaml index 645ba1d59f..1724c14265 100644 --- a/examples/dbrx/16bit-lora.yaml +++ b/examples/dbrx/16bit-lora.yaml @@ -3,9 +3,6 @@ base_model: LnL-AI/dbrx-base-converted-v2 # hub_model_id: username/custom_model_name trust_remote_code: true - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -48,26 +45,20 @@ optimizer: paged_adamw_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: false # don't use with fsdp_activation_checkpointing gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: saves_per_epoch: 1 -debug: + weight_decay: 0.0 fsdp: - full_shard diff --git a/examples/dbrx/8bit-lora.yaml b/examples/dbrx/8bit-lora.yaml index 4b9f607562..308483adf0 100644 --- a/examples/dbrx/8bit-lora.yaml +++ b/examples/dbrx/8bit-lora.yaml @@ -48,26 +48,20 @@ optimizer: paged_adamw_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: false # don't use with fsdp_activation_checkpointing gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: saves_per_epoch: 1 -debug: + weight_decay: 0.0 fsdp: - full_shard diff --git a/examples/dbrx/fft-ds-zero3.yaml b/examples/dbrx/fft-ds-zero3.yaml index e42b636700..0fbb5b0680 100644 --- a/examples/dbrx/fft-ds-zero3.yaml +++ b/examples/dbrx/fft-ds-zero3.yaml @@ -3,9 +3,6 @@ base_model: LnL-AI/dbrx-base-converted-v2 # hub_model_id: username/custom_model_name trust_remote_code: true - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -35,25 +32,19 @@ optimizer: paged_adamw_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: saves_per_epoch: 1 -debug: + weight_decay: 0.0 deepspeed: deepspeed_configs/zero3_bf16.json diff --git a/examples/deepseek-v2/fft-fsdp-16b.yaml b/examples/deepseek-v2/fft-fsdp-16b.yaml index 649317494a..3fe8691a36 100644 --- a/examples/deepseek-v2/fft-fsdp-16b.yaml +++ b/examples/deepseek-v2/fft-fsdp-16b.yaml @@ -2,9 +2,6 @@ base_model: deepseek-ai/DeepSeek-V2-Lite # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name trust_remote_code: true - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -31,27 +28,19 @@ optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 2e-5 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 100 evals_per_epoch: 2 -eval_table_size: saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 special_tokens: fsdp: diff --git a/examples/deepseek-v2/qlora-fsdp-2_5.yaml b/examples/deepseek-v2/qlora-fsdp-2_5.yaml index 005a0c76f8..a554970b6e 100644 --- a/examples/deepseek-v2/qlora-fsdp-2_5.yaml +++ b/examples/deepseek-v2/qlora-fsdp-2_5.yaml @@ -52,27 +52,19 @@ optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 2e-5 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 100 evals_per_epoch: 2 -eval_table_size: saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 special_tokens: fsdp: diff --git a/examples/falcon/config-7b-lora.yml b/examples/falcon/config-7b-lora.yml index efbe38d4a5..2d9240e8bd 100644 --- a/examples/falcon/config-7b-lora.yml +++ b/examples/falcon/config-7b-lora.yml @@ -25,9 +25,7 @@ max_packed_sequence_len: lora_r: 16 lora_alpha: 32 lora_dropout: 0.0 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: wandb_watch: @@ -41,15 +39,10 @@ optimizer: adamw_bnb_8bit torchdistx_path: lr_scheduler: cosine learning_rate: 0.00003 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 xformers_attention: true flash_attention: @@ -58,11 +51,7 @@ gptq_model_v1: warmup_steps: 40 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: "<|endoftext|>" bos_token: "<|endoftext|>" diff --git a/examples/falcon/config-7b-qlora.yml b/examples/falcon/config-7b-qlora.yml index b9829db5f7..78323db5fa 100644 --- a/examples/falcon/config-7b-qlora.yml +++ b/examples/falcon/config-7b-qlora.yml @@ -38,9 +38,7 @@ lora_alpha: 16 # 0.05 for 33B and 65B models lora_dropout: 0.05 # add LoRA modules on all linear layers of the base model -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -67,10 +65,7 @@ lr_scheduler: cosine # - 2e-4 for 7b & 13b # - 1e-4 for 33b & 64b learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true # stop training after this many evaluation losses have increased in a row @@ -78,7 +73,6 @@ gradient_checkpointing: true early_stopping_patience: 3 resume_from_checkpoint: auto_resume_from_checkpoints: true -local_rank: logging_steps: 1 xformers_attention: true flash_attention: @@ -87,11 +81,7 @@ gptq_model_v1: warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.000001 -fsdp: -fsdp_config: special_tokens: pad_token: "<|endoftext|>" bos_token: "<|endoftext|>" diff --git a/examples/falcon/config-7b.yml b/examples/falcon/config-7b.yml index 5e41a1e336..a796b89dd0 100644 --- a/examples/falcon/config-7b.yml +++ b/examples/falcon/config-7b.yml @@ -7,9 +7,6 @@ tokenizer_type: AutoTokenizer # required by falcon custom model code: https://huggingface.co/tiiuae/falcon-7b/tree/main trust_remote_code: true - -load_in_8bit: false -load_in_4bit: false gptq: false strict: false push_dataset_to_hub: @@ -25,9 +22,7 @@ max_packed_sequence_len: lora_r: 64 lora_alpha: 32 lora_dropout: 0.0 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: wandb_watch: @@ -41,15 +36,10 @@ optimizer: adamw_bnb_8bit torchdistx_path: lr_scheduler: cosine learning_rate: 0.00003 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 xformers_attention: true flash_attention: @@ -58,11 +48,7 @@ gptq_model_v1: warmup_steps: 40 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: "<|endoftext|>" bos_token: "<|endoftext|>" diff --git a/examples/gemma/qlora.yml b/examples/gemma/qlora.yml index 80a9fe62fc..505564269d 100644 --- a/examples/gemma/qlora.yml +++ b/examples/gemma/qlora.yml @@ -42,28 +42,16 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_ratio: 0.1 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/gemma2/qlora.yml b/examples/gemma2/qlora.yml index 2f18cc76d4..afba83552e 100644 --- a/examples/gemma2/qlora.yml +++ b/examples/gemma2/qlora.yml @@ -48,28 +48,16 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_ratio: 0.1 evals_per_epoch: -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/gemma2/reward-model.yaml b/examples/gemma2/reward-model.yaml index ada42ec28a..d828af939f 100644 --- a/examples/gemma2/reward-model.yaml +++ b/examples/gemma2/reward-model.yaml @@ -5,9 +5,6 @@ num_labels: 1 tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false reward_model: true @@ -38,8 +35,6 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: true fp16: tf32: true @@ -47,21 +42,12 @@ tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_ratio: 0.1 evals_per_epoch: -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/gemma3/gemma-3-1b-qlora.yml b/examples/gemma3/gemma-3-1b-qlora.yml index 8852b34694..732b914a88 100644 --- a/examples/gemma3/gemma-3-1b-qlora.yml +++ b/examples/gemma3/gemma-3-1b-qlora.yml @@ -50,30 +50,18 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_ratio: 0.1 evals_per_epoch: -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml index b4be17c3cb..85e5dce685 100644 --- a/examples/gemma3/gemma-3-4b-qlora.yml +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -44,8 +44,6 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: true fp16: tf32: true @@ -53,7 +51,6 @@ tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -local_rank: logging_steps: 1 flash_attention: true eager_attention: @@ -61,8 +58,4 @@ eager_attention: warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: diff --git a/examples/gemma3/gemma-3-4b-vision-qlora.yml b/examples/gemma3/gemma-3-4b-vision-qlora.yml index 6e711d6f61..92273380c6 100644 --- a/examples/gemma3/gemma-3-4b-vision-qlora.yml +++ b/examples/gemma3/gemma-3-4b-vision-qlora.yml @@ -46,8 +46,6 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: true fp16: tf32: true @@ -55,7 +53,6 @@ tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -local_rank: logging_steps: 1 flash_attention: true eager_attention: @@ -63,8 +60,4 @@ eager_attention: warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: diff --git a/examples/gptj/qlora.yml b/examples/gptj/qlora.yml index ddd6d24c0d..086d425b51 100644 --- a/examples/gptj/qlora.yml +++ b/examples/gptj/qlora.yml @@ -18,9 +18,7 @@ max_packed_sequence_len: lora_r: 8 lora_alpha: 32 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: wandb_watch: @@ -34,15 +32,10 @@ optimizer: paged_adamw_8bit torchdistx_path: lr_scheduler: cosine learning_rate: 0.0001 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 xformers_attention: true flash_attention: @@ -51,10 +44,6 @@ gptq_model_v1: warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.1 -fsdp: -fsdp_config: special_tokens: pad_token: "<|endoftext|>" diff --git a/examples/jamba/qlora.yaml b/examples/jamba/qlora.yaml index cab62513c1..7d642cb0a1 100644 --- a/examples/jamba/qlora.yaml +++ b/examples/jamba/qlora.yaml @@ -40,26 +40,18 @@ optimizer: paged_adamw_8bit lr_scheduler: cosine learning_rate: 0.00001 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 special_tokens: diff --git a/examples/jamba/qlora_deepspeed.yaml b/examples/jamba/qlora_deepspeed.yaml index 7ac7bfac5d..d983dc3916 100644 --- a/examples/jamba/qlora_deepspeed.yaml +++ b/examples/jamba/qlora_deepspeed.yaml @@ -39,26 +39,20 @@ optimizer: paged_adamw_8bit lr_scheduler: cosine learning_rate: 0.00001 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: saves_per_epoch: 1 -debug: + deepspeed: deepspeed_configs/zero2.json weight_decay: 0.0 special_tokens: diff --git a/examples/jamba/qlora_fsdp_large.yaml b/examples/jamba/qlora_fsdp_large.yaml index 18c0ec086f..a968d99d7a 100644 --- a/examples/jamba/qlora_fsdp_large.yaml +++ b/examples/jamba/qlora_fsdp_large.yaml @@ -39,8 +39,6 @@ optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.00001 -train_on_inputs: false -group_by_length: false bf16: true tf32: true diff --git a/examples/jeopardy-bot/config.yml b/examples/jeopardy-bot/config.yml index 04f92a8dc8..3609bd97eb 100644 --- a/examples/jeopardy-bot/config.yml +++ b/examples/jeopardy-bot/config.yml @@ -33,13 +33,9 @@ optimizer: adamw_bnb_8bit torchdistx_path: lr_scheduler: cosine learning_rate: 0.00003 -train_on_inputs: false -group_by_length: false bf16: auto tf32: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 5 xformers_attention: true flash_attention: @@ -48,11 +44,7 @@ gptq_model_v1: warmup_steps: 20 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.1 -fsdp: -fsdp_config: tokens: bos_token: "" eos_token: "" diff --git a/examples/llama-2/fft_optimized.yml b/examples/llama-2/fft_optimized.yml index 3475fcd9ac..fd78fbfeee 100644 --- a/examples/llama-2/fft_optimized.yml +++ b/examples/llama-2/fft_optimized.yml @@ -4,9 +4,6 @@ model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -26,7 +23,6 @@ lora_r: lora_alpha: lora_dropout: lora_target_linear: -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -41,18 +37,12 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true flash_attn_cross_entropy: false flash_attn_rms_norm: true @@ -61,11 +51,8 @@ flash_attn_fuse_mlp: true warmup_steps: 100 evals_per_epoch: 4 -eval_table_size: saves_per_epoch: 1 -debug: + deepspeed: #deepspeed_configs/zero2.json # multi-gpu only weight_decay: 0.1 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/llama-2/gptq-lora.yml b/examples/llama-2/gptq-lora.yml index 7d6b90ee3a..ad2dbd9cf7 100644 --- a/examples/llama-2/gptq-lora.yml +++ b/examples/llama-2/gptq-lora.yml @@ -10,8 +10,6 @@ gptq_disable_exllama: true tokenizer_use_fast: true tokenizer_legacy: true -load_in_8bit: false -load_in_4bit: false strict: false push_dataset_to_hub: hf_use_auth_token: true @@ -33,7 +31,6 @@ lora_target_modules: - q_proj - v_proj lora_target_linear: -lora_fan_in_fan_out: wandb_project: wandb_watch: wandb_name: @@ -50,26 +47,19 @@ torchdistx_path: lr_scheduler: cosine lr_quadratic_warmup: true learning_rate: 0.000017 -train_on_inputs: false -group_by_length: false bf16: false fp16: false float16: true tf32: true gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: sdp_attention: flash_optimum: warmup_steps: 100 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.1 special_tokens: bos_token: "" diff --git a/examples/llama-2/lisa.yml b/examples/llama-2/lisa.yml index 40391204ce..585fa94285 100644 --- a/examples/llama-2/lisa.yml +++ b/examples/llama-2/lisa.yml @@ -4,9 +4,6 @@ model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -26,7 +23,6 @@ lora_r: lora_alpha: lora_dropout: lora_target_linear: -lora_fan_in_fan_out: lisa_n_layers: 4 lisa_step_interval: 20 @@ -45,18 +41,12 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 5e-5 # recommendation from lisa paper for 7b -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true flash_attn_cross_entropy: false flash_attn_rms_norm: true @@ -65,13 +55,8 @@ flash_attn_fuse_mlp: true warmup_steps: 100 evals_per_epoch: 4 -eval_table_size: saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.1 -fsdp: -fsdp_config: special_tokens: bos_token: "" eos_token: "" diff --git a/examples/llama-2/loftq.yml b/examples/llama-2/loftq.yml index a5108e70f5..bf32c7b275 100644 --- a/examples/llama-2/loftq.yml +++ b/examples/llama-2/loftq.yml @@ -4,9 +4,6 @@ model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -26,7 +23,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: peft: loftq_config: loftq_bits: 4 @@ -44,29 +40,16 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true -s2_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/llama-2/lora.yml b/examples/llama-2/lora.yml index ec0c800124..3ef607ab48 100644 --- a/examples/llama-2/lora.yml +++ b/examples/llama-2/lora.yml @@ -26,7 +26,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -41,29 +40,16 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true -s2_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/llama-2/qlora-fsdp.yml b/examples/llama-2/qlora-fsdp.yml index c2db26b81a..759f080246 100644 --- a/examples/llama-2/qlora-fsdp.yml +++ b/examples/llama-2/qlora-fsdp.yml @@ -26,9 +26,7 @@ pad_to_sequence_len: true lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -43,28 +41,19 @@ optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.00001 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 fsdp: - full_shard diff --git a/examples/llama-2/qlora.yml b/examples/llama-2/qlora.yml index 81d1acbecd..c678a00423 100644 --- a/examples/llama-2/qlora.yml +++ b/examples/llama-2/qlora.yml @@ -26,9 +26,7 @@ pad_to_sequence_len: true lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -43,27 +41,16 @@ optimizer: paged_adamw_32bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/llama-2/relora.yml b/examples/llama-2/relora.yml index 93247ce068..6c943d009f 100644 --- a/examples/llama-2/relora.yml +++ b/examples/llama-2/relora.yml @@ -24,9 +24,7 @@ pad_to_sequence_len: true lora_r: 8 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: relora_steps: 150 relora_warmup_steps: 10 @@ -45,28 +43,18 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: bos_token: "" eos_token: "" diff --git a/examples/llama-3-vision/lora-11b.yaml b/examples/llama-3-vision/lora-11b.yaml index 22dc3a9afc..4431878fa3 100644 --- a/examples/llama-3-vision/lora-11b.yaml +++ b/examples/llama-3-vision/lora-11b.yaml @@ -45,14 +45,11 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: true fp16: tf32: true gradient_checkpointing: true -local_rank: logging_steps: 1 flash_attention: true eager_attention: @@ -60,8 +57,4 @@ eager_attention: warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml index 652a00e5cd..50169879c8 100644 --- a/examples/llama-3/fft-8b-liger-fsdp.yaml +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -42,27 +42,19 @@ optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 2e-5 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 100 evals_per_epoch: 2 -eval_table_size: saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 fsdp: - full_shard diff --git a/examples/llama-3/fft-8b.yaml b/examples/llama-3/fft-8b.yaml index a129c6e5b4..4452a6e3d6 100644 --- a/examples/llama-3/fft-8b.yaml +++ b/examples/llama-3/fft-8b.yaml @@ -1,9 +1,6 @@ base_model: NousResearch/Meta-Llama-3.1-8B # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -30,29 +27,19 @@ optimizer: paged_adamw_8bit lr_scheduler: cosine learning_rate: 2e-5 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 100 evals_per_epoch: 2 -eval_table_size: saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: <|end_of_text|> diff --git a/examples/llama-3/instruct-dpo-lora-8b.yml b/examples/llama-3/instruct-dpo-lora-8b.yml index c7568dd78b..a1b923fb60 100644 --- a/examples/llama-3/instruct-dpo-lora-8b.yml +++ b/examples/llama-3/instruct-dpo-lora-8b.yml @@ -42,7 +42,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -57,28 +56,15 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true -s2_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml index 8b1c254cbb..362bda9aa3 100644 --- a/examples/llama-3/instruct-lora-8b.yml +++ b/examples/llama-3/instruct-lora-8b.yml @@ -37,7 +37,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -52,30 +51,17 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true -s2_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: <|end_of_text|> diff --git a/examples/llama-3/lora-1b-deduplicate-dpo.yml b/examples/llama-3/lora-1b-deduplicate-dpo.yml index 62d6985597..e4b2a5244b 100644 --- a/examples/llama-3/lora-1b-deduplicate-dpo.yml +++ b/examples/llama-3/lora-1b-deduplicate-dpo.yml @@ -58,7 +58,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -73,28 +72,15 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true -s2_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: diff --git a/examples/llama-3/lora-1b-deduplicate-sft.yml b/examples/llama-3/lora-1b-deduplicate-sft.yml index bc748807b2..b8c21fafb3 100644 --- a/examples/llama-3/lora-1b-deduplicate-sft.yml +++ b/examples/llama-3/lora-1b-deduplicate-sft.yml @@ -31,7 +31,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: lora_modules_to_save: - embed_tokens - lm_head @@ -49,30 +48,17 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true -s2_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: <|end_of_text|> diff --git a/examples/llama-3/lora-1b-kernels.yml b/examples/llama-3/lora-1b-kernels.yml index 9c47f266f1..b76f03801e 100644 --- a/examples/llama-3/lora-1b-kernels.yml +++ b/examples/llama-3/lora-1b-kernels.yml @@ -1,9 +1,6 @@ base_model: NousResearch/Llama-3.2-1B # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -24,7 +21,6 @@ lora_r: 16 lora_alpha: 32 # Currently, we don't support dropout with our custom Triton kernels # lora_dropout: 0.05 -lora_fan_in_fan_out: lora_target_modules: - gate_proj - down_proj @@ -53,18 +49,12 @@ optimizer: adamw_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true loss_watchdog_threshold: 5.0 @@ -73,10 +63,6 @@ loss_watchdog_patience: 3 warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: "<|end_of_text|>" diff --git a/examples/llama-3/lora-1b-ray.yml b/examples/llama-3/lora-1b-ray.yml index 0e597a2049..199fe3b5d3 100644 --- a/examples/llama-3/lora-1b-ray.yml +++ b/examples/llama-3/lora-1b-ray.yml @@ -1,9 +1,6 @@ base_model: NousResearch/Llama-3.2-1B # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -24,7 +21,6 @@ pad_to_sequence_len: true lora_r: 16 lora_alpha: 32 lora_dropout: 0.05 -lora_fan_in_fan_out: lora_target_modules: - gate_proj - down_proj @@ -47,18 +43,12 @@ optimizer: adamw_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true loss_watchdog_threshold: 5.0 @@ -67,11 +57,9 @@ loss_watchdog_patience: 3 warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: + deepspeed: deepspeed_configs/zero3.json weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: "<|end_of_text|>" diff --git a/examples/llama-3/lora-1b-sample-packing-sequentially.yml b/examples/llama-3/lora-1b-sample-packing-sequentially.yml index 79f5a2ba19..a027673ab2 100644 --- a/examples/llama-3/lora-1b-sample-packing-sequentially.yml +++ b/examples/llama-3/lora-1b-sample-packing-sequentially.yml @@ -33,7 +33,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: lora_modules_to_save: - embed_tokens - lm_head @@ -51,30 +50,17 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true -s2_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: <|end_of_text|> diff --git a/examples/llama-3/lora-1b.yml b/examples/llama-3/lora-1b.yml index a1c3afa872..8a536260a9 100644 --- a/examples/llama-3/lora-1b.yml +++ b/examples/llama-3/lora-1b.yml @@ -1,9 +1,6 @@ base_model: NousResearch/Llama-3.2-1B # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -24,7 +21,6 @@ pad_to_sequence_len: true lora_r: 16 lora_alpha: 32 lora_dropout: 0.05 -lora_fan_in_fan_out: lora_target_modules: - gate_proj - down_proj @@ -47,18 +43,12 @@ optimizer: adamw_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true loss_watchdog_threshold: 5.0 @@ -67,10 +57,6 @@ loss_watchdog_patience: 3 warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: "<|end_of_text|>" diff --git a/examples/llama-3/lora-8b.yml b/examples/llama-3/lora-8b.yml index 7921857ceb..700dd46143 100644 --- a/examples/llama-3/lora-8b.yml +++ b/examples/llama-3/lora-8b.yml @@ -27,7 +27,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: lora_modules_to_save: - embed_tokens - lm_head @@ -45,30 +44,17 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true -s2_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: <|end_of_text|> diff --git a/examples/llama-3/qlora-1b-kto.yaml b/examples/llama-3/qlora-1b-kto.yaml index 32168dc371..0dc37b40a8 100644 --- a/examples/llama-3/qlora-1b-kto.yaml +++ b/examples/llama-3/qlora-1b-kto.yaml @@ -32,7 +32,6 @@ lora_r: 32 lora_alpha: 64 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -47,31 +46,19 @@ optimizer: adamw_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 20 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: "<|end_of_text|>" diff --git a/examples/llama-3/qlora-1b.yml b/examples/llama-3/qlora-1b.yml index 226bbb2379..c42dd22384 100644 --- a/examples/llama-3/qlora-1b.yml +++ b/examples/llama-3/qlora-1b.yml @@ -24,7 +24,6 @@ pad_to_sequence_len: true lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_fan_in_fan_out: lora_target_modules: - gate_proj - down_proj @@ -47,18 +46,12 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true loss_watchdog_threshold: 5.0 @@ -66,13 +59,7 @@ loss_watchdog_patience: 3 warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: "<|end_of_text|>" diff --git a/examples/llama-3/qlora-fsdp-405b.yaml b/examples/llama-3/qlora-fsdp-405b.yaml index 434ee67c42..75c8f59735 100644 --- a/examples/llama-3/qlora-fsdp-405b.yaml +++ b/examples/llama-3/qlora-fsdp-405b.yaml @@ -24,7 +24,6 @@ pad_to_sequence_len: true lora_r: 16 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true gradient_accumulation_steps: 4 @@ -34,8 +33,6 @@ optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.00001 -train_on_inputs: false -group_by_length: false bf16: true tf32: true diff --git a/examples/llama-3/qlora-fsdp-70b.yaml b/examples/llama-3/qlora-fsdp-70b.yaml index ceb2d8567c..c4889d6438 100644 --- a/examples/llama-3/qlora-fsdp-70b.yaml +++ b/examples/llama-3/qlora-fsdp-70b.yaml @@ -26,9 +26,7 @@ pad_to_sequence_len: true lora_r: 8 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -43,28 +41,19 @@ optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.00001 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 fsdp: - full_shard diff --git a/examples/llama-3/qlora.yml b/examples/llama-3/qlora.yml index 64268a2054..607deb896c 100644 --- a/examples/llama-3/qlora.yml +++ b/examples/llama-3/qlora.yml @@ -26,9 +26,7 @@ pad_to_sequence_len: true lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -43,28 +41,17 @@ optimizer: paged_adamw_32bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: "<|end_of_text|>" diff --git a/examples/llava/lora-7b.yaml b/examples/llava/lora-7b.yaml index 9129d01221..68e463585c 100644 --- a/examples/llava/lora-7b.yaml +++ b/examples/llava/lora-7b.yaml @@ -41,14 +41,11 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: true fp16: tf32: true gradient_checkpointing: true -local_rank: logging_steps: 1 flash_attention: true eager_attention: @@ -56,8 +53,4 @@ eager_attention: warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: diff --git a/examples/mamba/config.yml b/examples/mamba/config.yml index ca96fbfc3f..3a114bac77 100644 --- a/examples/mamba/config.yml +++ b/examples/mamba/config.yml @@ -5,9 +5,6 @@ tokenizer_type: AutoTokenizer tokenizer_config: EleutherAI/gpt-neox-20b # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -38,27 +35,17 @@ train_on_inputs: false group_by_length: true bf16: auto -fp16: tf32: true gradient_checkpointing: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: tokens: save_safetensors: False diff --git a/examples/mistral/bigstral-ds-zero3.yaml b/examples/mistral/bigstral-ds-zero3.yaml index 5ee214c1b4..bef9899326 100644 --- a/examples/mistral/bigstral-ds-zero3.yaml +++ b/examples/mistral/bigstral-ds-zero3.yaml @@ -6,9 +6,6 @@ tokenizer_type: LlamaTokenizer # hub_model_id: username/custom_model_name trust_remote_code: true - -load_in_8bit: false -load_in_4bit: false strict: false unfrozen_parameters: @@ -40,27 +37,19 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0001 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true save_total_limit: 1 save_steps: -debug: + deepspeed: deepspeed_configs/zero3_bf16_cpuoffload_params.json weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: eos_token: "<|im_end|>" tokens: diff --git a/examples/mistral/config.yml b/examples/mistral/config.yml index 8902033391..c58e2c9543 100644 --- a/examples/mistral/config.yml +++ b/examples/mistral/config.yml @@ -4,9 +4,6 @@ model_type: MistralForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -34,28 +31,16 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.000005 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/mistral/lora-mps.yml b/examples/mistral/lora-mps.yml index a62990e561..ba61cac11d 100644 --- a/examples/mistral/lora-mps.yml +++ b/examples/mistral/lora-mps.yml @@ -4,9 +4,6 @@ model_type: MistralForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -28,7 +25,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: lora_target_modules: - gate_proj - down_proj @@ -51,18 +47,13 @@ optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto fp16: false tf32: true gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: false sdp_attention: true @@ -71,12 +62,6 @@ loss_watchdog_patience: 3 warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_table_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/mistral/lora.yml b/examples/mistral/lora.yml index 11c1e0ee7b..b55c66715a 100644 --- a/examples/mistral/lora.yml +++ b/examples/mistral/lora.yml @@ -27,7 +27,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: lora_target_modules: - gate_proj - down_proj @@ -50,18 +49,12 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true loss_watchdog_threshold: 5.0 @@ -69,12 +62,6 @@ loss_watchdog_patience: 3 warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/mistral/mistral-dpo-qlora.yml b/examples/mistral/mistral-dpo-qlora.yml index 168fd3c3bb..6446cacb84 100644 --- a/examples/mistral/mistral-dpo-qlora.yml +++ b/examples/mistral/mistral-dpo-qlora.yml @@ -40,7 +40,6 @@ lora_r: 8 lora_alpha: 16 lora_dropout: 0.2 lora_target_linear: true -lora_fan_in_fan_out: lora_target_modules: - gate_proj @@ -67,31 +66,18 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0001 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: false -s2_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: bos_token: "<|im_start|>" eos_token: "<|im_end|>" diff --git a/examples/mistral/mistral-qlora-fsdp.yml b/examples/mistral/mistral-qlora-fsdp.yml index 521f4de5f0..5825ac749c 100644 --- a/examples/mistral/mistral-qlora-fsdp.yml +++ b/examples/mistral/mistral-qlora-fsdp.yml @@ -32,7 +32,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -47,18 +46,12 @@ optimizer: paged_adamw_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true loss_watchdog_threshold: 5.0 @@ -66,10 +59,8 @@ loss_watchdog_patience: 3 warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: + weight_decay: 0.0 fsdp: - full_shard diff --git a/examples/mistral/mistral-qlora-orpo.yml b/examples/mistral/mistral-qlora-orpo.yml index 82f30dc172..9c6ae74ef6 100644 --- a/examples/mistral/mistral-qlora-orpo.yml +++ b/examples/mistral/mistral-qlora-orpo.yml @@ -32,7 +32,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: lora_target_modules: - gate_proj - down_proj @@ -55,18 +54,12 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true loss_watchdog_threshold: 5.0 @@ -74,12 +67,6 @@ loss_watchdog_patience: 3 warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/mistral/mistral-small-3.1-24B-lora.yml b/examples/mistral/mistral-small-3.1-24B-lora.yml index 1774847993..0e6b4402d4 100644 --- a/examples/mistral/mistral-small-3.1-24B-lora.yml +++ b/examples/mistral/mistral-small-3.1-24B-lora.yml @@ -43,14 +43,11 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: true fp16: tf32: true gradient_checkpointing: true -local_rank: logging_steps: 1 flash_attention: false # PixtralVisionModel does not support Flash Attention 2.0 yet. eager_attention: @@ -58,9 +55,5 @@ eager_attention: warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml index 353c08d85c..e29b6392ad 100644 --- a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml +++ b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml @@ -30,7 +30,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -45,18 +44,12 @@ optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true loss_watchdog_threshold: 5.0 @@ -64,10 +57,8 @@ loss_watchdog_patience: 3 warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: + weight_decay: 0.0 fsdp: - full_shard diff --git a/examples/mistral/mixtral-qlora-fsdp.yml b/examples/mistral/mixtral-qlora-fsdp.yml index f9b5ab606b..40bb5d5d14 100644 --- a/examples/mistral/mixtral-qlora-fsdp.yml +++ b/examples/mistral/mixtral-qlora-fsdp.yml @@ -32,7 +32,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -47,18 +46,12 @@ optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true loss_watchdog_threshold: 5.0 @@ -66,10 +59,8 @@ loss_watchdog_patience: 3 warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: + weight_decay: 0.0 fsdp: - full_shard diff --git a/examples/mistral/mixtral.yml b/examples/mistral/mixtral.yml index ac80ec9336..eefd2456d0 100644 --- a/examples/mistral/mixtral.yml +++ b/examples/mistral/mixtral.yml @@ -41,7 +41,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: #lora_target_modules: # - gate # - q_proj @@ -65,18 +64,12 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true loss_watchdog_threshold: 5.0 @@ -84,12 +77,8 @@ loss_watchdog_patience: 3 warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: + deepspeed: deepspeed_configs/zero2.json weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/mistral/mixtral_22.yml b/examples/mistral/mixtral_22.yml index 7f2a72212e..1debd793ad 100644 --- a/examples/mistral/mixtral_22.yml +++ b/examples/mistral/mixtral_22.yml @@ -6,9 +6,6 @@ tokenizer_type: LlamaTokenizer # hub_model_id: username/custom_model_name trust_remote_code: true - -load_in_8bit: false -load_in_4bit: false strict: false unfrozen_parameters: @@ -38,27 +35,19 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0001 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true save_total_limit: 1 save_steps: -debug: + deepspeed: deepspeed_configs/zero3_bf16_cpuoffload_all.json weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: eos_token: "<|im_end|>" tokens: diff --git a/examples/mistral/qlora.yml b/examples/mistral/qlora.yml index 5f3fa10b8a..921f3fe7bc 100644 --- a/examples/mistral/qlora.yml +++ b/examples/mistral/qlora.yml @@ -27,7 +27,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: lora_target_modules: - gate_proj - down_proj @@ -50,18 +49,12 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true loss_watchdog_threshold: 5.0 @@ -69,12 +62,6 @@ loss_watchdog_patience: 3 warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/mpt-7b/config.yml b/examples/mpt-7b/config.yml index cf4b433fe5..e7485fad76 100644 --- a/examples/mpt-7b/config.yml +++ b/examples/mpt-7b/config.yml @@ -35,26 +35,17 @@ optimizer: adamw_bnb_8bit torchdistx_path: lr_scheduler: cosine learning_rate: 0.0000002 -train_on_inputs: false -group_by_length: false bf16: auto tf32: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 5 -xformers_attention: flash_attention: gptq_groupsize: gptq_model_v1: warmup_steps: 20 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0001 -fsdp: -fsdp_config: tokens: pad_token: "<|padding|>" bos_token: "<|endoftext|>" diff --git a/examples/openllama-3b/config.yml b/examples/openllama-3b/config.yml index ec66014b43..7a1e2d9a57 100644 --- a/examples/openllama-3b/config.yml +++ b/examples/openllama-3b/config.yml @@ -4,9 +4,6 @@ model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false push_dataset_to_hub: datasets: @@ -23,7 +20,6 @@ lora_alpha: lora_dropout: lora_target_modules: lora_target_linear: -lora_fan_in_fan_out: wandb_project: wandb_entity: wandb_watch: @@ -37,29 +33,20 @@ optimizer: adamw_bnb_8bit torchdistx_path: lr_scheduler: cosine learning_rate: 0.000003 -train_on_inputs: false -group_by_length: false float16: true bf16: false fp16: false tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true gptq_groupsize: gptq_model_v1: warmup_steps: 20 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.1 -fsdp: -fsdp_config: special_tokens: bos_token: "" eos_token: "" diff --git a/examples/openllama-3b/lora.yml b/examples/openllama-3b/lora.yml index b449df9ae7..c1c597b73c 100644 --- a/examples/openllama-3b/lora.yml +++ b/examples/openllama-3b/lora.yml @@ -29,7 +29,6 @@ lora_target_modules: - v_proj - k_proj - o_proj -lora_fan_in_fan_out: wandb_project: wandb_entity: wandb_watch: @@ -43,29 +42,19 @@ optimizer: adamw_bnb_8bit torchdistx_path: lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: false fp16: true tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true gptq_groupsize: -s2_attention: gptq_model_v1: warmup_steps: 20 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.1 -fsdp: -fsdp_config: special_tokens: bos_token: "" eos_token: "" diff --git a/examples/openllama-3b/qlora.yml b/examples/openllama-3b/qlora.yml index 3efcdabc6c..e9c71efd10 100644 --- a/examples/openllama-3b/qlora.yml +++ b/examples/openllama-3b/qlora.yml @@ -21,9 +21,7 @@ sample_packing: true lora_r: 8 lora_alpha: 32 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: wandb_watch: @@ -37,28 +35,19 @@ optimizer: paged_adamw_32bit torchdistx_path: lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: false fp16: true tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true gptq_groupsize: gptq_model_v1: warmup_steps: 20 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.1 -fsdp: -fsdp_config: special_tokens: bos_token: "" eos_token: "" diff --git a/examples/phi/lora-3.5.yaml b/examples/phi/lora-3.5.yaml index ddc2d77884..2ecb9d28db 100644 --- a/examples/phi/lora-3.5.yaml +++ b/examples/phi/lora-3.5.yaml @@ -37,7 +37,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -52,28 +51,16 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bfloat16: true bf16: true fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: -s2_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 4 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: diff --git a/examples/phi/phi-ft.yml b/examples/phi/phi-ft.yml index 29fad3094b..886671a60c 100644 --- a/examples/phi/phi-ft.yml +++ b/examples/phi/phi-ft.yml @@ -4,9 +4,6 @@ model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -27,7 +24,6 @@ lora_r: lora_alpha: lora_dropout: lora_target_linear: -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -45,30 +41,20 @@ max_grad_norm: 1.0 lr_scheduler: cosine learning_rate: 0.000003 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: True -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 100 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.1 -fsdp: -fsdp_config: resize_token_embeddings_to_32x: true special_tokens: pad_token: "<|endoftext|>" diff --git a/examples/phi/phi-qlora.yml b/examples/phi/phi-qlora.yml index d9f23ff264..a1cbf8a52c 100644 --- a/examples/phi/phi-qlora.yml +++ b/examples/phi/phi-qlora.yml @@ -27,7 +27,6 @@ lora_r: 64 lora_alpha: 32 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -45,30 +44,20 @@ max_grad_norm: 1.0 lr_scheduler: cosine learning_rate: 0.000003 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: True -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 100 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.1 -fsdp: -fsdp_config: resize_token_embeddings_to_32x: true special_tokens: pad_token: "<|endoftext|>" diff --git a/examples/phi/phi2-ft.yml b/examples/phi/phi2-ft.yml index 1b7ac89ec8..480017a39d 100644 --- a/examples/phi/phi2-ft.yml +++ b/examples/phi/phi2-ft.yml @@ -4,9 +4,6 @@ model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -27,7 +24,6 @@ lora_r: lora_alpha: lora_dropout: lora_target_linear: -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -45,30 +41,20 @@ max_grad_norm: 1.0 lr_scheduler: cosine learning_rate: 0.000003 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: True -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 100 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.1 -fsdp: -fsdp_config: resize_token_embeddings_to_32x: true special_tokens: pad_token: "<|endoftext|>" diff --git a/examples/phi/phi3-ft-fsdp.yml b/examples/phi/phi3-ft-fsdp.yml index 1479bb97f3..766db76f6b 100644 --- a/examples/phi/phi3-ft-fsdp.yml +++ b/examples/phi/phi3-ft-fsdp.yml @@ -4,9 +4,6 @@ model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -28,7 +25,6 @@ lora_r: lora_alpha: lora_dropout: lora_target_linear: -lora_fan_in_fan_out: wandb_project: phi3 wandb_entity: @@ -46,27 +42,19 @@ max_grad_norm: 1.0 lr_scheduler: cosine learning_rate: 0.000003 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 100 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.1 fsdp: - full_shard diff --git a/examples/phi/phi3-ft.yml b/examples/phi/phi3-ft.yml index 58afd940ee..0a5da2e232 100644 --- a/examples/phi/phi3-ft.yml +++ b/examples/phi/phi3-ft.yml @@ -7,9 +7,6 @@ tokenizer_type: AutoTokenizer # hub_model_id: username/custom_model_name chat_template: phi_3 - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -30,7 +27,6 @@ lora_r: 64 lora_alpha: 32 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: gradient_accumulation_steps: 1 micro_batch_size: 2 @@ -42,8 +38,6 @@ max_grad_norm: 1.0 lr_scheduler: cosine learning_rate: 5.0e-6 -train_on_inputs: false -group_by_length: false bf16: auto gradient_checkpointing: true @@ -55,9 +49,9 @@ flash_attention: true eval_steps: 1000 save_steps: 5000 -eval_table_size: 2 eval_batch_size: 2 eval_sample_packing: false +eval_table_size: 2 eval_max_new_tokens: 32 eval_causal_lm_metrics: ["perplexity"] do_causal_lm_eval: true diff --git a/examples/pixtral/lora-12b.yml b/examples/pixtral/lora-12b.yml index 7336a7ad05..d3b3efd70f 100644 --- a/examples/pixtral/lora-12b.yml +++ b/examples/pixtral/lora-12b.yml @@ -41,14 +41,11 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: true fp16: tf32: true gradient_checkpointing: true -local_rank: logging_steps: 1 flash_attention: false # PixtralVisionModel does not support Flash Attention 2.0 yet eager_attention: @@ -56,10 +53,6 @@ eager_attention: warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: pad_token: diff --git a/examples/pythia-12b/config.yml b/examples/pythia-12b/config.yml index 52ab770556..170d6ac595 100644 --- a/examples/pythia-12b/config.yml +++ b/examples/pythia-12b/config.yml @@ -5,9 +5,6 @@ model_type: GPTNeoXForCausalLM tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false gptq: false device_map: auto datasets: @@ -22,7 +19,6 @@ max_packed_sequence_len: 2048 lora_r: 64 lora_alpha: 32 lora_dropout: 0.0 -lora_target_modules: lora_target_linear: true lora_fan_in_fan_out: true # pythia/GPTNeoX lora specific wandb_project: @@ -37,16 +33,10 @@ num_epochs: 5 learning_rate: 0.00003 optimizer: adamw_bnb_8bit lr_scheduler: cosine -train_on_inputs: false -group_by_length: false bf16: false fp16: false float16: true tf32: true flash_optimum: true -early_stopping_patience: resume_from_checkpoint: -local_rank: gradient_checkpointing: true -fsdp: -fsdp_config: diff --git a/examples/pythia/lora.yml b/examples/pythia/lora.yml index 203652f6b7..0549967eca 100644 --- a/examples/pythia/lora.yml +++ b/examples/pythia/lora.yml @@ -28,13 +28,9 @@ gradient_accumulation_steps: 1 micro_batch_size: 4 num_epochs: 4 learning_rate: 0.00001 -train_on_inputs: false -group_by_length: false bf16: auto tf32: true -early_stopping_patience: resume_from_checkpoint: -local_rank: weight_decay: 0.1 evals_per_epoch: 4 logging_steps: 1 diff --git a/examples/qwen/lora.yml b/examples/qwen/lora.yml index 961125a511..23385d236f 100644 --- a/examples/qwen/lora.yml +++ b/examples/qwen/lora.yml @@ -28,7 +28,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -43,28 +42,16 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/qwen/qlora.yml b/examples/qwen/qlora.yml index e7159eaa5f..854a682fe4 100644 --- a/examples/qwen/qlora.yml +++ b/examples/qwen/qlora.yml @@ -28,7 +28,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -43,28 +42,16 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/qwen/qwen2-moe-lora.yaml b/examples/qwen/qwen2-moe-lora.yaml index b357b93443..a2a1e4d257 100644 --- a/examples/qwen/qwen2-moe-lora.yaml +++ b/examples/qwen/qwen2-moe-lora.yaml @@ -3,9 +3,6 @@ base_model: Qwen/Qwen1.5-MoE-A2.7B # hub_model_id: username/custom_model_name trust_remote_code: true - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -25,7 +22,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -40,28 +36,18 @@ optimizer: paged_adamw_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/qwen/qwen2-moe-qlora.yaml b/examples/qwen/qwen2-moe-qlora.yaml index d45e4c89f2..b1ab131a66 100644 --- a/examples/qwen/qwen2-moe-qlora.yaml +++ b/examples/qwen/qwen2-moe-qlora.yaml @@ -25,7 +25,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -40,28 +39,18 @@ optimizer: paged_adamw_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/qwen2-vl/lora-7b.yaml b/examples/qwen2-vl/lora-7b.yaml index e7ab13ddb3..3d0b10adfa 100644 --- a/examples/qwen2-vl/lora-7b.yaml +++ b/examples/qwen2-vl/lora-7b.yaml @@ -41,14 +41,11 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: true fp16: tf32: true gradient_checkpointing: true -local_rank: logging_steps: 1 flash_attention: true eager_attention: @@ -56,8 +53,4 @@ eager_attention: warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: diff --git a/examples/qwen2/dpo.yaml b/examples/qwen2/dpo.yaml index d6dbe22d79..8df7f9dc49 100644 --- a/examples/qwen2/dpo.yaml +++ b/examples/qwen2/dpo.yaml @@ -44,27 +44,15 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: diff --git a/examples/qwen2/prm.yaml b/examples/qwen2/prm.yaml index 071e2d0f30..669f8e2db8 100644 --- a/examples/qwen2/prm.yaml +++ b/examples/qwen2/prm.yaml @@ -5,9 +5,6 @@ num_labels: 2 tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false process_reward_model: true @@ -43,30 +40,19 @@ optimizer: adamw_torch lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: true fp16: tf32: gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_ratio: 0.1 evals_per_epoch: -eval_table_size: -eval_max_new_tokens: 128 eval_steps: 100 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/qwen2/qlora-fsdp.yaml b/examples/qwen2/qlora-fsdp.yaml index c537d32445..1f2ed83b10 100644 --- a/examples/qwen2/qlora-fsdp.yaml +++ b/examples/qwen2/qlora-fsdp.yaml @@ -26,7 +26,6 @@ lora_r: 32 lora_alpha: 64 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -41,27 +40,19 @@ optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 fsdp: - full_shard diff --git a/examples/qwen2/reward-model.yaml b/examples/qwen2/reward-model.yaml index bbd6e66ced..fcbb9867ea 100644 --- a/examples/qwen2/reward-model.yaml +++ b/examples/qwen2/reward-model.yaml @@ -5,9 +5,6 @@ num_labels: 1 tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false reward_model: true @@ -38,8 +35,6 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: true fp16: tf32: true @@ -47,21 +42,12 @@ tf32: true gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_ratio: 0.1 evals_per_epoch: -eval_table_size: -eval_max_new_tokens: 128 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/redpajama/config-3b.yml b/examples/redpajama/config-3b.yml index d716727a34..3e2999df96 100644 --- a/examples/redpajama/config-3b.yml +++ b/examples/redpajama/config-3b.yml @@ -36,26 +36,17 @@ optimizer: adamw_bnb_8bit torchdistx_path: lr_scheduler: cosine learning_rate: 0.0000002 -train_on_inputs: false -group_by_length: false bf16: auto tf32: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 5 -xformers_attention: flash_attention: gptq_groupsize: gptq_model_v1: warmup_steps: 20 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0001 -fsdp: -fsdp_config: tokens: pad_token: "<|padding|>" bos_token: "<|endoftext|>" diff --git a/examples/replit-3b/config-lora.yml b/examples/replit-3b/config-lora.yml index bb2a6aace4..5a02ba10ce 100644 --- a/examples/replit-3b/config-lora.yml +++ b/examples/replit-3b/config-lora.yml @@ -20,7 +20,6 @@ lora_target_modules: - Wqkv - mlp_up - mlp_down -lora_fan_in_fan_out: wandb_project: lora-replit wandb_entity: wandb_watch: @@ -34,25 +33,16 @@ optimizer: torchdistx_path: lr_scheduler: learning_rate: 0.00001 -train_on_inputs: false -group_by_length: false bf16: auto tf32: true gradient_checkpointing: -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: gptq_groupsize: gptq_model_v1: warmup_steps: 20 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0 -fsdp: -fsdp_config: #special_tokens: diff --git a/examples/stablelm-2/1.6b/fft.yml b/examples/stablelm-2/1.6b/fft.yml index 3ecb1581bc..f26714856e 100644 --- a/examples/stablelm-2/1.6b/fft.yml +++ b/examples/stablelm-2/1.6b/fft.yml @@ -6,9 +6,6 @@ tokenizer_type: AutoTokenizer # hub_model_id: username/custom_model_name trust_remote_code: true - -load_in_8bit: false -load_in_4bit: false strict: false datasets: @@ -28,7 +25,6 @@ lora_r: lora_alpha: lora_dropout: lora_target_linear: -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -43,18 +39,12 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true flash_attn_cross_entropy: false flash_attn_rms_norm: true @@ -63,11 +53,8 @@ flash_attn_fuse_mlp: true warmup_steps: 100 evals_per_epoch: 4 -eval_table_size: saves_per_epoch: 1 -debug: + deepspeed: #deepspeed_configs/zero2.json # multi-gpu only weight_decay: 0.1 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/stablelm-2/1.6b/lora.yml b/examples/stablelm-2/1.6b/lora.yml index 8597de6a28..aaa9908d1a 100644 --- a/examples/stablelm-2/1.6b/lora.yml +++ b/examples/stablelm-2/1.6b/lora.yml @@ -28,7 +28,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -43,18 +42,12 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true flash_attn_cross_entropy: false flash_attn_rms_norm: true @@ -62,9 +55,5 @@ flash_attn_rms_norm: true warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/starcoder2/qlora.yml b/examples/starcoder2/qlora.yml index d1db71d6d0..8b21d71450 100644 --- a/examples/starcoder2/qlora.yml +++ b/examples/starcoder2/qlora.yml @@ -25,9 +25,7 @@ pad_to_sequence_len: true lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -42,30 +40,20 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 2e-5 -train_on_inputs: false -group_by_length: false bf16: auto fp16: false tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 20 evals_per_epoch: 4 eval_steps: -eval_table_size: saves_per_epoch: 4 save_steps: save_total_limit: 2 -debug: -deepspeed: weight_decay: -fsdp: -fsdp_config: special_tokens: diff --git a/examples/tiny-llama/lora-mps.yml b/examples/tiny-llama/lora-mps.yml index c777a4d7b4..8654a39bba 100644 --- a/examples/tiny-llama/lora-mps.yml +++ b/examples/tiny-llama/lora-mps.yml @@ -27,7 +27,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -42,26 +41,17 @@ optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto fp16: false tf32: true gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: false warmup_steps: 10 evals_per_epoch: 0 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/tiny-llama/lora.yml b/examples/tiny-llama/lora.yml index 54aa5ec275..64b2360d30 100644 --- a/examples/tiny-llama/lora.yml +++ b/examples/tiny-llama/lora.yml @@ -26,7 +26,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -41,26 +40,16 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/tiny-llama/pretrain.yml b/examples/tiny-llama/pretrain.yml index fd6d2c9c1f..2984c52ae9 100644 --- a/examples/tiny-llama/pretrain.yml +++ b/examples/tiny-llama/pretrain.yml @@ -4,9 +4,6 @@ model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - -load_in_8bit: false -load_in_4bit: false strict: false max_steps: 200 @@ -34,27 +31,16 @@ optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: -eval_table_size: saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/tiny-llama/qlora.yml b/examples/tiny-llama/qlora.yml index 694ab3a15e..79e3164a50 100644 --- a/examples/tiny-llama/qlora.yml +++ b/examples/tiny-llama/qlora.yml @@ -27,9 +27,7 @@ pad_to_sequence_len: true lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -44,26 +42,16 @@ optimizer: paged_adamw_32bit lr_scheduler: cosine learning_rate: 0.0002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: flash_attention: true warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 -fsdp: -fsdp_config: special_tokens: diff --git a/examples/xgen-7b/xgen-7b-8k-qlora.yml b/examples/xgen-7b/xgen-7b-8k-qlora.yml index d798e326d7..f4ff589e06 100644 --- a/examples/xgen-7b/xgen-7b-8k-qlora.yml +++ b/examples/xgen-7b/xgen-7b-8k-qlora.yml @@ -36,9 +36,7 @@ lora_alpha: 16 # 0.05 for 33B and 65B models lora_dropout: 0.05 # add LoRA modules on all linear layers of the base model -lora_target_modules: lora_target_linear: true -lora_fan_in_fan_out: wandb_project: wandb_entity: @@ -65,10 +63,7 @@ lr_scheduler: cosine # - 2e-4 for 7b & 13b # - 1e-4 for 33b & 64b learning_rate: 0.00002 -train_on_inputs: false -group_by_length: false bf16: auto -fp16: tf32: false gradient_checkpointing: true # stop training after this many evaluation losses have increased in a row @@ -76,7 +71,6 @@ gradient_checkpointing: true early_stopping_patience: 3 resume_from_checkpoint: auto_resume_from_checkpoints: true -local_rank: logging_steps: 1 xformers_attention: true flash_attention: @@ -85,8 +79,6 @@ gptq_model_v1: warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 -debug: -deepspeed: weight_decay: 0.0 special_tokens: eos_token: "<|endoftext|>" diff --git a/examples/yi-34B-chat/qlora.yml b/examples/yi-34B-chat/qlora.yml index b68d008836..de79ed6cee 100644 --- a/examples/yi-34B-chat/qlora.yml +++ b/examples/yi-34B-chat/qlora.yml @@ -10,7 +10,6 @@ load_in_4bit: true strict: false sequence_len: 1024 bf16: auto -fp16: tf32: false flash_attention: true special_tokens: @@ -30,8 +29,6 @@ num_epochs: 1 # Evaluation val_set_size: 0.1 evals_per_epoch: 5 -eval_table_size: -eval_max_new_tokens: 128 eval_sample_packing: false eval_batch_size: 1 @@ -43,7 +40,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -lora_fan_in_fan_out: lora_target_modules: # Sampling @@ -64,15 +60,6 @@ lr_scheduler: cosine learning_rate: 0.0002 # Misc -train_on_inputs: false -group_by_length: false -early_stopping_patience: resume_from_checkpoint: -local_rank: logging_steps: 1 -xformers_attention: -debug: -deepspeed: weight_decay: 0 -fsdp: -fsdp_config: From de451f99a594fbde02fbac952f402caeb28171cf Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 5 Apr 2025 00:47:44 +0700 Subject: [PATCH 0524/1405] fix: cohere cce scaling wrong tensor (#2483) --- .../integrations/cut_cross_entropy/monkeypatch/cohere.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py index 5cdc53b0cc..99e17910e6 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py @@ -128,10 +128,10 @@ def cce_forward( if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): assert labels is not None - # scale weight by logit_scale in-place of logits + # scale hidden_states by logit_scale in-place of logits loss = apply_lce( - hidden_states[:, slice_indices, :], - self.lm_head.weight * self.logit_scale, + hidden_states[:, slice_indices, :] * self.logit_scale, + self.lm_head.weight, labels, _PATCH_OPTS, **kwargs, From 949471039fd56dc33a5485605d15610848f3cc48 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 5 Apr 2025 01:25:44 -0400 Subject: [PATCH 0525/1405] fix tokenizer overrides w gemma3 (#2488) * fix tokenizer overrides w gemma3 * fix offline wrapping --- src/axolotl/utils/models.py | 7 +++++++ tests/conftest.py | 15 ++++++++++++--- tests/hf_offline_utils.py | 2 +- tests/test_tokenizers.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 9df63231b8..3016078654 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -283,6 +283,13 @@ def modify_tokenizer_files( raise ValueError( f"Token ID {token_id} not found in added_tokens" ) + if "model" in tokenizer_data and "vocab" in tokenizer_data["model"]: + for token_id, new_value in token_id_mappings.items(): + for entry_val, entry_id in tokenizer_data["model"]["vocab"].items(): + if entry_id == token_id: + del tokenizer_data["model"]["vocab"][entry_val] + tokenizer_data["model"]["vocab"][new_value] = token_id + break # Write the updated tokenizer data back with open(tokenizer_path, "w", encoding="utf-8") as f: diff --git a/tests/conftest.py b/tests/conftest.py index c71ea1e8c4..97c48db41d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,6 @@ from transformers import AutoTokenizer from tests.hf_offline_utils import ( - disable_hf_offline, enable_hf_offline, hf_offline_context, ) @@ -50,7 +49,6 @@ def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements @retry_on_request_exceptions(max_retries=3, delay=5) -@disable_hf_offline def snapshot_download_w_retry(*args, **kwargs): """ download a model or dataset from HF Hub, retrying in requests failures. We also try to fetch it from the local @@ -62,7 +60,8 @@ def snapshot_download_w_retry(*args, **kwargs): return snapshot_download(*args, **kwargs) except LocalEntryNotFoundError: pass - return snapshot_download(*args, **kwargs) + with hf_offline_context(False): + return snapshot_download(*args, **kwargs) @pytest.fixture(scope="session", autouse=True) @@ -265,6 +264,16 @@ def download_mistral_7b_model_fixture(): ) +@pytest.fixture(scope="session", autouse=True) +def download_gemma3_4b_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "mlx-community/gemma-3-4b-it-8bit", + repo_type="model", + allow_patterns=["*token*", "config.json"], + ) + + @pytest.fixture(scope="session", autouse=True) def download_gemma_2b_model_fixture(): # download the tokenizer only diff --git a/tests/hf_offline_utils.py b/tests/hf_offline_utils.py index 0c7b5d4a46..385e61f181 100644 --- a/tests/hf_offline_utils.py +++ b/tests/hf_offline_utils.py @@ -95,7 +95,7 @@ def hf_offline_context(hf_hub_offline): """ original_hf_offline = os.getenv("HF_HUB_OFFLINE") os.environ["HF_HUB_OFFLINE"] = str(hf_hub_offline) - reload_modules(True) + reload_modules(bool(hf_hub_offline)) yield # Restore the original value of HF_HUB_OFFLINE environment variable if original_hf_offline is not None: diff --git a/tests/test_tokenizers.py b/tests/test_tokenizers.py index ef0cb14d1c..ffd51bc29b 100644 --- a/tests/test_tokenizers.py +++ b/tests/test_tokenizers.py @@ -110,6 +110,34 @@ def test_added_tokens_overrides(self, temp_dir): assert tokenizer.encode("RANDOM_OVERRIDE_2", add_special_tokens=False) == [ 128042 ] + assert ( + tokenizer.decode([128041, 128042]) == "RANDOM_OVERRIDE_1RANDOM_OVERRIDE_2" + ) + + @enable_hf_offline + def test_added_tokens_overrides_gemma3(self, temp_dir): + cfg = DictDefault( + { + # use with tokenizer that has reserved_tokens in added_tokens + "tokenizer_config": "mlx-community/gemma-3-4b-it-8bit", + "added_tokens_overrides": { + 256001: "RANDOM_OVERRIDE_1", + 256002: "RANDOM_OVERRIDE_2", + }, + "output_dir": temp_dir, + } + ) + + tokenizer = load_tokenizer(cfg) + assert tokenizer.encode("RANDOM_OVERRIDE_1", add_special_tokens=False) == [ + 256001 + ] + assert tokenizer.encode("RANDOM_OVERRIDE_2", add_special_tokens=False) == [ + 256002 + ] + assert ( + tokenizer.decode([256001, 256002]) == "RANDOM_OVERRIDE_1RANDOM_OVERRIDE_2" + ) @enable_hf_offline def test_added_tokens_overrides_with_toolargeid(self, temp_dir): From e7e0cd97ce660d77498757791cb40ecff3217dd1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 5 Apr 2025 17:41:31 -0400 Subject: [PATCH 0526/1405] Update dependencies and show slow tests in CI (#2492) * use latest torchao, gradio, schedule-free * get info on slow tests * speed up tests by avoiding gradient checkpointing and reducing eval size --- cicd/multigpu.sh | 4 ++-- requirements.txt | 6 +++--- tests/e2e/multigpu/test_llama.py | 34 ++++++++++++++++---------------- tests/e2e/multigpu/test_qwen2.py | 4 ++-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/cicd/multigpu.sh b/cicd/multigpu.sh index 84dfc6f717..008a74bfff 100755 --- a/cicd/multigpu.sh +++ b/cicd/multigpu.sh @@ -2,5 +2,5 @@ set -e # only run one test at a time so as not to OOM the GPU -pytest -v -n2 /workspace/axolotl/tests/e2e/multigpu/ --ignore=/workspace/axolotl/tests/e2e/multigpu/solo/ -pytest -v -n1 /workspace/axolotl/tests/e2e/multigpu/solo/ +pytest -v --durations=10 -n2 /workspace/axolotl/tests/e2e/multigpu/ --ignore=/workspace/axolotl/tests/e2e/multigpu/solo/ +pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/multigpu/solo/ diff --git a/requirements.txt b/requirements.txt index dde64f3929..78ced57284 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ trl==0.16.0 optimum==1.16.2 hf_transfer sentencepiece -gradio==3.50.2 +gradio==5.23.3 modal==0.70.5 pydantic==2.10.6 @@ -59,8 +59,8 @@ langdetect==1.0.9 immutabledict==4.2.0 antlr4-python3-runtime==4.13.2 -torchao==0.7.0 -schedulefree==1.3.0 +torchao==0.9.0 +schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.6 axolotl-contribs-mit==0.0.3 diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 0e228aef02..ee1869f7da 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -44,7 +44,7 @@ def test_lora_ddp(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.05, + "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -58,7 +58,7 @@ def test_lora_ddp(self, temp_dir): "max_steps": 2, "micro_batch_size": 4, "gradient_accumulation_steps": 4, - "gradient_checkpointing": True, + # "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_8bit", @@ -108,7 +108,7 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.05, + "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -122,7 +122,7 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): "max_steps": 2, "micro_batch_size": 1, "gradient_accumulation_steps": gradient_accumulation_steps, - "gradient_checkpointing": True, + # "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_8bit", @@ -169,7 +169,7 @@ def test_dpo_lora_ddp(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.05, + "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -195,7 +195,7 @@ def test_dpo_lora_ddp(self, temp_dir): "max_steps": 2, "micro_batch_size": 4, "gradient_accumulation_steps": 4, - "gradient_checkpointing": True, + # "gradient_checkpointing": True, "output_dir": temp_dir, "warmup_steps": 0, "learning_rate": 0.00001, @@ -247,7 +247,7 @@ def test_dpo_qlora_ddp(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.05, + "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -273,7 +273,7 @@ def test_dpo_qlora_ddp(self, temp_dir): "max_steps": 2, "micro_batch_size": 2, "gradient_accumulation_steps": 4, - "gradient_checkpointing": True, + # "gradient_checkpointing": True, "output_dir": temp_dir, "warmup_steps": 0, "learning_rate": 0.00001, @@ -334,7 +334,7 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): "max_steps": 2, "micro_batch_size": 2, "gradient_accumulation_steps": gradient_accumulation_steps, - "gradient_checkpointing": True, + # "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", @@ -391,7 +391,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): "sample_packing": True, "pad_to_sequence_len": True, "sequence_len": 2048, - "val_set_size": 0.05, + "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -405,7 +405,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): "max_steps": 2, "micro_batch_size": 4, "gradient_accumulation_steps": 2, - "gradient_checkpointing": True, + # "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", @@ -470,7 +470,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "eval_sample_packing": False, "pad_to_sequence_len": True, "sequence_len": 2048, - "val_set_size": 0.05, + "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -485,7 +485,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "max_steps": 2, "micro_batch_size": 4, "gradient_accumulation_steps": 2, - "gradient_checkpointing": True, + # "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", @@ -567,7 +567,7 @@ def test_ds_zero3_packed( "sample_packing": True, "pad_to_sequence_len": True, "sequence_len": 2048, - "val_set_size": 0.05, + "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -640,7 +640,7 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): "sample_packing": True, "pad_to_sequence_len": True, "sequence_len": 2048, - "val_set_size": 0.05, + "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -713,7 +713,7 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): "sample_packing": True, "pad_to_sequence_len": True, "sequence_len": 2048, - "val_set_size": 0.05, + "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -788,7 +788,7 @@ def test_fix_untrained_tokens(self, temp_dir): "max_steps": 2, "micro_batch_size": 1, "gradient_accumulation_steps": 1, - "gradient_checkpointing": True, + # "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", diff --git a/tests/e2e/multigpu/test_qwen2.py b/tests/e2e/multigpu/test_qwen2.py index af39c6361f..9599c3abf8 100644 --- a/tests/e2e/multigpu/test_qwen2.py +++ b/tests/e2e/multigpu/test_qwen2.py @@ -37,7 +37,7 @@ def test_qlora_fsdp_dpo(self, base_model, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.05, + "val_set_size": 0.01, "datasets": [ { "path": "Intel/orca_dpo_pairs", @@ -57,7 +57,7 @@ def test_qlora_fsdp_dpo(self, base_model, temp_dir): "flash_attention": True, "bf16": "auto", "tf32": True, - "gradient_checkpointing": True, + # "gradient_checkpointing": True, "gradient_checkpointing_kwargs": { "use_reentrant": False, }, From a8f38c367c551225afaab251d89b7eee79e79373 Mon Sep 17 00:00:00 2001 From: Sung Ching Liu <22844540+bursteratom@users.noreply.github.com> Date: Sat, 5 Apr 2025 18:02:57 -0400 Subject: [PATCH 0527/1405] Flex Attention + Packing with BlockMask support (#2363) --- src/axolotl/core/trainer_builder.py | 6 +- .../monkeypatch/attention/flex_attn.py | 48 ++++++++++ src/axolotl/utils/models.py | 18 +++- src/axolotl/utils/schemas/config.py | 35 +++++++ tests/e2e/multigpu/solo/test_flex.py | 92 +++++++++++++++++++ tests/e2e/solo/test_flex.py | 73 +++++++++++++++ tests/e2e/utils.py | 14 ++- 7 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 src/axolotl/monkeypatch/attention/flex_attn.py create mode 100644 tests/e2e/multigpu/solo/test_flex.py create mode 100644 tests/e2e/solo/test_flex.py diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 7d0df8a45a..40dedb4567 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -891,7 +891,11 @@ def build_collator( if "max_length" in kwargs: kwargs.pop("max_length") elif use_batch_sampler_collator: - if self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES or ( + if self.cfg.flex_attention: + collator = V2BatchSamplerDataCollatorForSeq2Seq + elif self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES: + collator = V2BatchSamplerDataCollatorForSeq2Seq + elif ( self.cfg.model_config_type in ["llama"] and self.cfg.flash_attention is not True ): diff --git a/src/axolotl/monkeypatch/attention/flex_attn.py b/src/axolotl/monkeypatch/attention/flex_attn.py new file mode 100644 index 0000000000..8b69c2c491 --- /dev/null +++ b/src/axolotl/monkeypatch/attention/flex_attn.py @@ -0,0 +1,48 @@ +"""Flex attention monkey patch""" + +import torch +import transformers + + +def patch_flex(): + is_torch_2_6 = torch.__version__.startswith("2.6") + is_transformers_below_4_51 = transformers.__version__ < "4.51.0" + + if is_torch_2_6 and is_transformers_below_4_51: + from torch.nn.attention.flex_attention import flex_attention + + class WrappedFlexAttention: + """ + We are doing a singleton class so that flex attention is compiled once when it's first called. + """ + + _instance = None + _is_flex_compiled = False + _compiled_flex_attention = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + # Create a new instance if one doesn't already exist + cls._instance = super().__new__(cls) + return cls._instance + + @torch.compiler.disable(recursive=False) + def __init__(self): + """ + Initialize or update the singleton instance. + """ + if not self._is_flex_compiled: + self._compiled_flex_attention = torch.compile( + flex_attention, + dynamic=False, + mode="max-autotune-no-cudagraphs", + fullgraph=True, + ) + self._is_flex_compiled = True + + def __call__(self): + return self._compiled_flex_attention + + transformers.integrations.flex_attention.WrappedFlexAttention = ( + WrappedFlexAttention + ) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 3016078654..663aa17408 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -578,7 +578,7 @@ def apply_patches(self) -> None: if ( self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES - and self.cfg.flash_attention + and (self.cfg.flash_attention or self.cfg.flex_attention) and self.cfg.sample_packing ): if "auto_map" in self.model_config: @@ -884,7 +884,16 @@ def set_attention_config(self) -> None: """ sample packing uses custom FA2 patch """ - if self.cfg.flash_attention: + if self.cfg.flex_attention: + self.model_kwargs["attn_implementation"] = "flex_attention" + self.model_config._attn_implementation = ( # pylint: disable=protected-access + "flex_attention" + ) + from axolotl.monkeypatch.attention.flex_attn import patch_flex + + patch_flex() + + elif self.cfg.flash_attention: if not self.cfg.sample_packing and self.cfg.s2_attention: pass self.model_kwargs["attn_implementation"] = "flash_attention_2" @@ -1281,7 +1290,10 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: should_convert = ( # LlamaRMSNorm layers are in fp32 after kbit_training or full finetune, so we need to # convert them back to fp16/bf16 for flash-attn compatibility. - ((needs_fa2_dtype or self.cfg.flash_attention) and not qlora_fsdp) + ( + (needs_fa2_dtype or self.cfg.flash_attention or self.cfg.flex_attention) + and not qlora_fsdp + ) or self.cfg.cut_cross_entropy # Cut cross entropy requires embedding layers to be in fp16/bf16 for backward pass ) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index c9a208ba22..cf98f7f02a 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -223,6 +223,7 @@ class AxolotlInputConfig( xformers_attention: bool | None = None sdp_attention: bool | None = None s2_attention: bool | None = None + flex_attention: bool | None = None flash_attention: bool | None = None flash_attn_cross_entropy: bool | None = None flash_attn_rms_norm: bool | None = None @@ -355,6 +356,22 @@ def datasets_serializer( return [ds_config.model_dump(exclude_none=True) for ds_config in ds_configs] return None + @model_validator(mode="before") + @classmethod + def check_attention_fields(cls, data): + fields = ( + "xformers_attention", + "sdp_attention", + "s2_attention", + "flash_attention", + "flex_attention", + ) + non_empty_count = sum(1 for field in fields if data.get(field)) + + if non_empty_count > 1: + raise ValueError(f"Only one of {', '.join(fields)} must be set") + return data + @model_validator(mode="before") @classmethod def check_batch_size_fields(cls, data): @@ -1250,6 +1267,24 @@ def check_adopt_torch_version(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_flex_torch_version(cls, data): + if (data.get("flex_attention") is not None) and (data.get("flex_attention")): + env_capabilities = data.get("env_capabilities", {}) + torch_version = env_capabilities.get("torch_version") + + if torch_version is None: + import torch + + torch_version = str(torch.__version__).split("+", maxsplit=1)[0] + + if version.parse(torch_version) < version.parse("2.6.0"): + raise ValueError( + "Flex attention is not supported on torch version < 2.6.0" + ) + return data + @model_validator(mode="before") @classmethod def check_torch_compile_auto(cls, data): diff --git a/tests/e2e/multigpu/solo/test_flex.py b/tests/e2e/multigpu/solo/test_flex.py new file mode 100644 index 0000000000..5e2c9e7cc4 --- /dev/null +++ b/tests/e2e/multigpu/solo/test_flex.py @@ -0,0 +1,92 @@ +""" +E2E tests for multigpu lora tinyllama +""" + +import logging +import os +from pathlib import Path + +import pytest +import yaml +from accelerate.test_utils import execute_subprocess_async +from huggingface_hub import snapshot_download +from transformers.testing_utils import get_torch_dist_unique_port +from transformers.utils import is_torch_bf16_gpu_available + +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import check_tensorboard, require_torch_2_6_0 + +LOG = logging.getLogger("axolotl.tests.e2e.multigpu") +os.environ["WANDB_DISABLED"] = "true" + +AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent + + +@pytest.fixture(scope="session", autouse=True) +def download_model(): + # download the model + snapshot_download("HuggingFaceTB/SmolLM2-135M") + + +class TestPackedFlex: + """ + Test case for Packed training of llama models + """ + + @require_torch_2_6_0 + def test_loss_llama(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 1024, + "sample_packing": True, + "flex_attention": True, + "val_set_size": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "vicgalle/alpaca-gpt4", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 2, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "max_steps": 5, + "use_tensorboard": True, + "save_strategy": "no", + } + ) + if is_torch_bf16_gpu_available(): + cfg.bf16 = True + else: + cfg.fp16 = True + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" + ) diff --git a/tests/e2e/solo/test_flex.py b/tests/e2e/solo/test_flex.py new file mode 100644 index 0000000000..5e52204a69 --- /dev/null +++ b/tests/e2e/solo/test_flex.py @@ -0,0 +1,73 @@ +""" +E2E tests for packed training w/ flex attention +""" + +import logging +import os +import unittest + +from transformers.utils import is_torch_bf16_gpu_available + +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +from ..utils import check_tensorboard, require_torch_2_6_0, with_temp_dir + +LOG = logging.getLogger("axolotl.tests.e2e") +os.environ["WANDB_DISABLED"] = "true" + + +class TestPackedFlex(unittest.TestCase): + """ + Test case for Packed training of llama models + """ + + @require_torch_2_6_0 + @with_temp_dir + def test_loss_llama(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 1024, + "sample_packing": True, + "flex_attention": True, + "val_set_size": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "vicgalle/alpaca-gpt4", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 2, + "gradient_accumulation_steps": 4, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "max_steps": 5, + "use_tensorboard": True, + } + ) + if is_torch_bf16_gpu_available(): + cfg.bf16 = True + else: + cfg.fp16 = True + + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" + ) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 2b218fbf5e..2fbf333c4a 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -67,9 +67,21 @@ def is_min_2_5_1(): return unittest.skipUnless(is_min_2_5_1(), "test requires torch>=2.5.1")(test_case) +def require_torch_2_6_0(test_case): + """ + Decorator marking a test that requires torch >= 2.6.0 + """ + + def is_min_2_6_0(): + torch_version = version.parse(torch.__version__) + return torch_version >= version.parse("2.6.0") + + return unittest.skipUnless(is_min_2_6_0(), "test requires torch>=2.6.0")(test_case) + + def require_torch_lt_2_6_0(test_case): """ - Decorator marking a test that requires torch >= 2.5.1 + Decorator marking a test that requires torch < 2.6.0 """ def is_max_2_6_0(): From 5f4af3665d5293c9fbfe727183ae36099ff9c950 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 6 Apr 2025 17:08:01 -0400 Subject: [PATCH 0528/1405] FSDP2 support (#2469) * fsdp2 support * use accelerate release 1.6.0 * allow 8bit optims with fsdp2 * liger + torch compile fix * add fsdp2 e2e tests * use transformers commit with fsdp2 support * skip zero3 tests for this PR for now * fix fsdp2 config for ci * make sure both flex and flash attn work with fsdp2, skip fix untrained tokens * okay, actually use fdsp2... * more fixes to flex for fsdp2 * make sure to patch all the loaded models * additional validation for fsdp2, bump dep versions --- requirements.txt | 8 +- src/axolotl/integrations/liger/__init__.py | 13 ++ src/axolotl/integrations/liger/utils.py | 29 +++ .../monkeypatch/attention/flex_attn.py | 192 ++++++++++++++---- src/axolotl/train.py | 2 +- src/axolotl/utils/models.py | 8 +- src/axolotl/utils/schemas/config.py | 13 ++ src/axolotl/utils/trainer.py | 6 + tests/e2e/multigpu/test_llama.py | 92 ++++++++- 9 files changed, 320 insertions(+), 43 deletions(-) create mode 100644 src/axolotl/integrations/liger/utils.py diff --git a/requirements.txt b/requirements.txt index 78ced57284..d824892032 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,12 +12,12 @@ liger-kernel==0.5.5 packaging==23.2 peft==0.15.0 -transformers==4.50.3 +transformers==4.51.0 tokenizers>=0.21.1 -accelerate==1.5.2 +accelerate==1.6.0 datasets==3.5.0 -deepspeed==0.15.4 -trl==0.16.0 +deepspeed>=0.15.4 +trl==0.16.1 optimum==1.16.2 hf_transfer diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index d6e423fa94..82a46d9cfa 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -27,6 +27,7 @@ from ...utils.distributed import zero_only from .args import LigerArgs # pylint: disable=unused-import. # noqa: F401 +from .utils import patch_with_compile_disable LOG = logging.getLogger("axolotl.integrations.liger") @@ -40,6 +41,18 @@ def get_input_args(self): return "axolotl.integrations.liger.LigerArgs" def pre_model_load(self, cfg): + if cfg.torch_compile: + # torch compile will unnecessarily attempt to optimize the triton kernel unless explicitly disabled + import liger_kernel.ops.fused_linear_cross_entropy + + patch_with_compile_disable( + liger_kernel.ops.fused_linear_cross_entropy, + "fused_linear_cross_entropy_forward", + ) + patch_with_compile_disable( + liger_kernel.ops.fused_linear_cross_entropy, + "fused_linear_cross_entropy_backward", + ) from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss from liger_kernel.transformers.functional import liger_cross_entropy from liger_kernel.transformers.geglu import LigerGEGLUMLP diff --git a/src/axolotl/integrations/liger/utils.py b/src/axolotl/integrations/liger/utils.py new file mode 100644 index 0000000000..bf9fc58e77 --- /dev/null +++ b/src/axolotl/integrations/liger/utils.py @@ -0,0 +1,29 @@ +""" +utils to patch liger kernel ops to disable torch.compile +""" + +from functools import wraps + +import torch + + +def patch_with_compile_disable(module, function_name): + """ + Patch a function in a module by wrapping it with torch.compile.disable + + Args: + module: The module containing the function to patch + function_name: The name of the function to patch + """ + original_function = getattr(module, function_name) + + @wraps(original_function) + @torch.compiler.disable + def wrapped_function(*args, **kwargs): + return original_function(*args, **kwargs) + + # Replace the original function with the wrapped one + setattr(module, function_name, wrapped_function) + + # Return the original function in case you need to restore it later + return original_function diff --git a/src/axolotl/monkeypatch/attention/flex_attn.py b/src/axolotl/monkeypatch/attention/flex_attn.py index 8b69c2c491..2ca5b09a62 100644 --- a/src/axolotl/monkeypatch/attention/flex_attn.py +++ b/src/axolotl/monkeypatch/attention/flex_attn.py @@ -1,48 +1,172 @@ """Flex attention monkey patch""" +import sys +from typing import Optional, Tuple, Union + import torch import transformers -def patch_flex(): +def patch_flex_wrapper(): + # TODO remove this patch when transformers#37285 is merged and in a release is_torch_2_6 = torch.__version__.startswith("2.6") is_transformers_below_4_51 = transformers.__version__ < "4.51.0" - if is_torch_2_6 and is_transformers_below_4_51: - from torch.nn.attention.flex_attention import flex_attention + if not (is_torch_2_6 and is_transformers_below_4_51): + return + + from torch.nn.attention.flex_attention import flex_attention + + class WrappedFlexAttention: + """ + We are doing a singleton class so that flex attention is compiled once when it's first called. + """ + + _instance = None + _is_flex_compiled = False + _compiled_flex_attention = None - class WrappedFlexAttention: + def __new__(cls, *args, **kwargs): + if cls._instance is None: + # Create a new instance if one doesn't already exist + cls._instance = super().__new__(cls) + return cls._instance + + @torch.compiler.disable(recursive=False) + def __init__(self): """ - We are doing a singleton class so that flex attention is compiled once when it's first called. + Initialize or update the singleton instance. """ + if not self._is_flex_compiled: + self._compiled_flex_attention = torch.compile( + flex_attention, + dynamic=False, + mode="max-autotune-no-cudagraphs", + fullgraph=True, + ) + self._is_flex_compiled = True + + def __call__(self): + return self._compiled_flex_attention + + transformers.integrations.flex_attention.WrappedFlexAttention = WrappedFlexAttention + + +def patch_flex_make_mask(): + is_torch_2_6 = torch.__version__.startswith("2.6") + is_transformers_eq_4_51 = transformers.__version__ == "4.51.0" + + if not (is_torch_2_6 and is_transformers_eq_4_51): + return + + from torch.nn.attention.flex_attention import ( + BlockMask, + ) + from torch.nn.attention.flex_attention import ( + create_block_mask as create_block_causal_mask_flex, + ) + + Offset = Union[torch.Tensor, int] + + def patched_make_flex_block_causal_mask( + attention_mask_2d: torch.Tensor, + attention_chunk_size: Optional[int] = None, + query_length=None, + key_length=None, + offsets: Optional[Tuple[Offset, Offset]] = None, + ) -> "BlockMask": + """ + Create a block causal document mask for a batch of sequences, both packed and unpacked. + Create Block causal logic and passing it into :func:`torch.nn.attention.flex_attention.create_block_mask`. + The resultant BlockMask is a compressed representation of the full block causal + mask. BlockMask is essential for performant computation of flex attention. + See: https://pytorch.org/blog/flexattention/ + + Args: + attention_mask_2d (torch.Tensor): Attention mask for packed and padded sequences + of shape (batch_size, total_seq_len). e.g. - _instance = None - _is_flex_compiled = False - _compiled_flex_attention = None - - def __new__(cls, *args, **kwargs): - if cls._instance is None: - # Create a new instance if one doesn't already exist - cls._instance = super().__new__(cls) - return cls._instance - - @torch.compiler.disable(recursive=False) - def __init__(self): - """ - Initialize or update the singleton instance. - """ - if not self._is_flex_compiled: - self._compiled_flex_attention = torch.compile( - flex_attention, - dynamic=False, - mode="max-autotune-no-cudagraphs", - fullgraph=True, - ) - self._is_flex_compiled = True - - def __call__(self): - return self._compiled_flex_attention - - transformers.integrations.flex_attention.WrappedFlexAttention = ( - WrappedFlexAttention + For unpacked sequence: + [[1, 1, 1, 1, 0, 0, 0], + [1, 1, 1, 1, 1, 0, 0]] + + For packed sequence: + [[1, 1, 1, 2, 2, 2, 0], + [1, 1, 2, 2, 2, 3, 3]] + + Returns: + BlockMask + """ + + batch_size, total_seq_len = attention_mask_2d.shape + if not key_length: + key_length = total_seq_len + if not query_length: + query_length = total_seq_len + attention_mask_2d = torch.nn.functional.pad( + attention_mask_2d, value=0, pad=(0, key_length) + ) + device = attention_mask_2d.device + document_ids = attention_mask_2d.clone() + + if attention_chunk_size is not None: + # we create an arange, then we just // by chunk size to get [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3] + document_ids = (document_ids.fill_(1).cumsum(-1) - 1) // ( + attention_chunk_size + ) + + # Instead of passing a tensor mask, flex attention requires a mask_mod function + # that determines which elements of QK^T should be included in the attention + # computation prior to the softmax. For sample packing, we need both the + # logic for both causal mask and document mask. See PyTorch's official + # blog post for more details: https://pytorch.org/blog/flexattention/#mask-mods + def causal_mask_mod( + batch_idx, head_idx, q_idx, kv_idx + ): # pylint: disable=unused-argument + """ + Defines the logic of a block causal mask by combining both a standard causal mask + and a block diagonal document mask. + + See :func:`~torchtune.modules.attention_utils.create_block_causal_mask` + for an illustration. + """ + causal_mask = q_idx >= kv_idx # not valid when decoding + document_mask = ( + document_ids[batch_idx, q_idx] == document_ids[batch_idx, kv_idx] + ) + padding_mask = attention_mask_2d[batch_idx, q_idx] > 0 + final_mask = causal_mask & padding_mask & document_mask + return final_mask + + if offsets is not None: + q_offset = offsets[0] + kv_offset = offsets[1] + + def mask_mod(batch_idx, head_idx, q_idx, kv_idx): + offset_q = q_idx + q_offset + offset_kv = kv_idx + kv_offset + return causal_mask_mod(batch_idx, head_idx, offset_q, offset_kv) + + else: + mask_mod = causal_mask_mod + return create_block_causal_mask_flex( + mask_mod=mask_mod, + B=batch_size, + H=None, # attention head + Q_LEN=query_length, + KV_LEN=key_length, + device=device, + _compile=True, ) + + for n in tuple(sys.modules): + if ".modeling_" in n and "llama4" not in n: + if hasattr(sys.modules[n], "make_flex_block_causal_mask"): + print(n) + sys.modules[n].make_flex_block_causal_mask = ( + patched_make_flex_block_causal_mask + ) + + transformers.integrations.flex_attention.make_flex_block_causal_mask = ( + patched_make_flex_block_causal_mask + ) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 89f35d7eb0..c2bddeeec4 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -217,7 +217,7 @@ def save_trained_model( # Handle FSDP state dict type state_dict_type = "FULL_STATE_DICT" - if trainer.is_fsdp_enabled: + if trainer.is_fsdp_enabled and str(cfg.fsdp_config.fsdp_version) != "2": if cfg.fsdp_final_state_dict_type: state_dict_type = cfg.fsdp_final_state_dict_type trainer.accelerator.state.fsdp_plugin.set_state_dict_type(state_dict_type) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 663aa17408..0e1329b973 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -889,9 +889,13 @@ def set_attention_config(self) -> None: self.model_config._attn_implementation = ( # pylint: disable=protected-access "flex_attention" ) - from axolotl.monkeypatch.attention.flex_attn import patch_flex + from axolotl.monkeypatch.attention.flex_attn import ( + patch_flex_make_mask, + patch_flex_wrapper, + ) - patch_flex() + patch_flex_wrapper() + patch_flex_make_mask() elif self.cfg.flash_attention: if not self.cfg.sample_packing and self.cfg.s2_attention: diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index cf98f7f02a..3ceae42738 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -950,10 +950,23 @@ def check_fsdp_offload_w_8bit_optimizer(cls, data): and "8bit" in data.get("optimizer", "") and data.get("fsdp_config") and data["fsdp_config"].get("fsdp_offload_params") + and str(data["fsdp_config"].get("fsdp_version")) != "2" ): raise ValueError( f"FSDP Offload not compatible with {data.get('optimizer')}" ) + if ( + data.get("fsdp") + and "8bit" in data.get("optimizer", "") + and data.get("fsdp_config") + and str(data["fsdp_config"].get("fsdp_version")) == "2" + ): + if data.get("optimizer", "") in ["adamw_8bit", "adamw_bnb_8bit"]: + # CUDA ops errors with bnb 8bit optimizer + FSDP2 + raise ValueError( + f"FSDP2 not compatible with {data.get('optimizer')}, use `adamw_torch_8bit` instead" + ) + return data @model_validator(mode="before") diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index c370707b6a..c5c9e5599f 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -538,6 +538,8 @@ def setup_deepspeed_env(cfg, stage=None): def setup_fsdp_envs(cfg): os.environ["ACCELERATE_USE_FSDP"] = "true" + if str(cfg.fsdp_config.fsdp_version) == "2": + os.environ["FSDP_VERSION"] = "2" if cfg.fsdp_config.fsdp_activation_checkpointing: os.environ["FSDP_ACTIVATION_CHECKPOINTING"] = "true" if cfg.fsdp_config.fsdp_offload_params: @@ -556,6 +558,10 @@ def setup_fsdp_envs(cfg): os.environ["FSDP_TRANSFORMER_CLS_TO_WRAP"] = ( cfg.fsdp_config.fsdp_transformer_layer_cls_to_wrap ) + if cfg.fsdp_config.fsdp_reshard_after_forward is not None: + os.environ["FSDP_RESHARD_AFTER_FORWARD"] = ( + "true" if cfg.fsdp_config.fsdp_reshard_after_forward else "false" + ) def prepare_optim_env(cfg): diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index ee1869f7da..d71fa25c89 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -14,7 +14,7 @@ from axolotl.utils.dict import DictDefault -from tests.e2e.utils import check_tensorboard +from tests.e2e.utils import check_tensorboard, require_torch_2_6_0 LOG = logging.getLogger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" @@ -450,6 +450,88 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" ) + @require_torch_2_6_0 + @pytest.mark.parametrize( + "attention_backend", + ["flash", "flex"], + ) + @pytest.mark.parametrize( + "fsdp_reshard_after_forward", + [True, False], + ) + def test_fsdp2_packed( + self, temp_dir, attention_backend, fsdp_reshard_after_forward + ): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sample_packing": True, + "pad_to_sequence_len": True, + "sequence_len": 2048, + "val_set_size": 0.05, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "gradient_checkpointing": True, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_8bit", + "lr_scheduler": "cosine", + "fsdp": [ + "auto_wrap", + ], + "fsdp_config": { + "fsdp_version": 2, + "fsdp_forward_prefetch": True, + "fsdp_sync_module_states": True, + "fsdp_use_orig_params": True, + "fsdp_offload_params": False, + "fsdp_cpu_ram_efficient_loading": False, + "fsdp_transformer_layer_cls_to_wrap": "LlamaDecoderLayer", + "fsdp_state_dict_type": "SHARDED_STATE_DICT", + "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "fsdp_reshard_after_forward": fsdp_reshard_after_forward, + }, + "use_tensorboard": True, + } + ) + if attention_backend == "flash": + cfg.flash_attention = True + elif attention_backend == "flex": + cfg.flex_attention = True + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.1, "Train Loss is too high" + ) + def test_fsdp_qlora_prequant_packed(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -530,6 +612,9 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" ) + @pytest.mark.skip( + reason="ds-zero3 broken in main until transformers#37281 resolved" + ) @pytest.mark.parametrize( "gradient_accumulation_steps", [1, 2], @@ -759,6 +844,9 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" ) + @pytest.mark.skip( + reason="fix untrained tokens brittle with lots of edge cases in latest transformers" + ) def test_fix_untrained_tokens(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -797,7 +885,7 @@ def test_fix_untrained_tokens(self, temp_dir): "sample_packing": True, "bf16": True, "save_safetensors": True, - "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero3_bf16.json"), + "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero1.json"), "use_tensorboard": True, } ) From 8bbad21bfdbd312aede4d7277adc5a7448d7e941 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 7 Apr 2025 10:49:15 -0400 Subject: [PATCH 0529/1405] llama4 support (#2493) * llama4 support * add xet support [skip ci] * be flexible on transformers version and skip test on version * don't use deepspeed for the fix_untrained_tokens test * reordering to trigger torch 2.6.0 tests first * slightly smaller train set * use 4.51.0 for now * remove stray print, add llama4 chat template to schema, bump peft to 0.15.1 * patches to make llama4 performant * add preliminary fp8 support --- .github/workflows/multi-gpu-e2e.yml | 10 +- .github/workflows/tests.yml | 4 +- examples/llama4/scout-lora.yaml | 75 ++++++++ requirements.txt | 5 +- src/axolotl/core/trainers/base.py | 13 ++ src/axolotl/integrations/liger/__init__.py | 12 ++ .../integrations/liger/models/llama4.py | 171 ++++++++++++++++++ .../monkeypatch/attention/flex_attn.py | 1 - src/axolotl/monkeypatch/multipack.py | 1 + .../monkeypatch/trainer_accelerator_args.py | 80 ++++++++ src/axolotl/utils/chat_templates.py | 1 + src/axolotl/utils/models.py | 11 +- src/axolotl/utils/schemas/config.py | 4 +- src/axolotl/utils/schemas/enums.py | 1 + src/axolotl/utils/trainer.py | 4 +- tests/e2e/multigpu/test_llama.py | 40 ++-- tests/e2e/multigpu/test_ray.py | 6 +- 17 files changed, 407 insertions(+), 32 deletions(-) create mode 100644 examples/llama4/scout-lora.yaml create mode 100644 src/axolotl/integrations/liger/models/llama4.py create mode 100644 src/axolotl/monkeypatch/trainer_accelerator_args.py diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index dfa3156188..f89de494d2 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -27,21 +27,21 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.4.1 - axolotl_extras: # no vllm support for 2.4.1 + pytorch: 2.6.0 + axolotl_extras: vllm num_gpus: 2 nightly_build: "true" - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.5.1 - axolotl_extras: vllm + pytorch: 2.4.1 + axolotl_extras: # no vllm support for 2.4.1 num_gpus: 2 nightly_build: "true" - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.6.0 + pytorch: 2.5.1 axolotl_extras: vllm num_gpus: 2 nightly_build: "true" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 434803d2c5..9eb85a5b13 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -211,7 +211,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.5.1 + pytorch: 2.6.0 num_gpus: 1 axolotl_extras: vllm steps: @@ -258,7 +258,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.6.0 + pytorch: 2.5.1 num_gpus: 1 axolotl_extras: vllm steps: diff --git a/examples/llama4/scout-lora.yaml b/examples/llama4/scout-lora.yaml new file mode 100644 index 0000000000..26534b560b --- /dev/null +++ b/examples/llama4/scout-lora.yaml @@ -0,0 +1,75 @@ +base_model: meta-llama/Llama-4-Scout-17B-16E +model_type: Llama4ForConditionalGeneration + # Automatically upload checkpoint and final model to HF + # hub_model_id: username/custom_model_name + +strict: false + + # torch_compile: true + +adapter: lora +lora_r: 32 +lora_alpha: 64 +lora_target_modules: + - self_attn.q_proj + - self_attn.k_proj + - self_attn.v_proj + - self_attn.o_proj +lora_modules_to_save: + - lm_head + - embed_tokens + +chat_template: llama4 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value + +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +sequence_len: 4096 +sample_packing: true +pad_to_sequence_len: true + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 2e-5 + +bf16: true +tf32: true + +# gradient_checkpointing: true +# gradient_checkpointing_kwargs: +# use_reentrant: false +logging_steps: 1 +flash_attention: true + +warmup_steps: 100 +evals_per_epoch: 2 +saves_per_epoch: 1 +weight_decay: 0.0 +fsdp: + - auto_wrap + - full_shard +fsdp_config: + fsdp_version: 2 + fsdp_offload_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: Llama4TextDecoderLayer + fsdp_state_dict_type: SHARDED_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD + fsdp_reshard_after_forward: true + fsdp_activation_checkpointing: true +special_tokens: + pad_token: <|finetune_right_pad_id|> + eos_token: <|eot|> diff --git a/requirements.txt b/requirements.txt index d824892032..f2b2df5fb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,18 +6,19 @@ triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 autoawq==0.2.7.post3 -liger-kernel==0.5.5 +liger-kernel==0.5.6 # END section packaging==23.2 -peft==0.15.0 +peft==0.15.1 transformers==4.51.0 tokenizers>=0.21.1 accelerate==1.6.0 datasets==3.5.0 deepspeed>=0.15.4 trl==0.16.1 +hf_xet==1.0.0 optimum==1.16.2 hf_transfer diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 9fed78eb72..bc3a200d4a 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -562,6 +562,19 @@ def create_accelerator_and_postprocess(self): return res + def additional_accelerator_args( + self, fp8=None, **kwargs + ): # pylint: disable=unused-argument + ret_kwargs = {} + if fp8: + from accelerate.utils import AORecipeKwargs + + ret_kwargs["mixed_precision"] = "fp8" + ret_kwargs["kwargs_handlers"] = [AORecipeKwargs()] + os.environ["ACCELERATE_MIXED_PRECISION"] = "fp8" + + return ret_kwargs + def log(self, logs: dict[str, float], start_time: float | None = None) -> None: """ Log `logs` on the various objects watching training, including stored metrics. diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index 82a46d9cfa..8d737175e7 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -173,5 +173,17 @@ def _liger_rms_norm_wrapper(dim, **kwargs): raise NotImplementedError( "Fused linear cross entropy is not yet supported for Gemma3." ) + elif cfg.model_config_type == "llama4": + from axolotl.integrations.liger.models.llama4 import ( + apply_liger_kernel_to_llama4, + ) + + apply_liger_kernel_to_llama4( + cross_entropy=cfg.liger_cross_entropy, + fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, + glu_activation=cfg.liger_glu_activation, + rms_norm=cfg.liger_rms_norm, + layer_norm=cfg.liger_layer_norm, + ) elif cfg.model_config_type in ["deepseek_v3"]: raise ValueError(f"Unsupported model config type: {cfg.model_config_type}") diff --git a/src/axolotl/integrations/liger/models/llama4.py b/src/axolotl/integrations/liger/models/llama4.py new file mode 100644 index 0000000000..da35b114c4 --- /dev/null +++ b/src/axolotl/integrations/liger/models/llama4.py @@ -0,0 +1,171 @@ +""" +Liger FLCE for llama4 +""" + +import sys +from typing import List, Optional, Tuple, Union + +import torch +from liger_kernel.transformers.model.loss_utils import LigerForCausalLMLoss +from transformers.modeling_outputs import CausalLMOutputWithPast + + +def lce_forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[ + Union["Cache", List[torch.FloatTensor]] # noqa: F821 + ] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **loss_kwargs, +) -> Union[Tuple, CausalLMOutputWithPast]: + r""" + Args: + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + """ + + # pylint: disable=duplicate-code + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + ) + + hidden_states = outputs[0] + + if hasattr(self.config, "pretraining_tp") and self.config.pretraining_tp > 1: + raise Exception( # pylint: disable=broad-exception-raised + "Liger Kernel does not support pretraining_tp!!" + ) + + logits = None + loss = None + # if in training mode, don't materialize logits + if self.training and (labels is not None): + loss = LigerForCausalLMLoss( + hidden_states=hidden_states, + lm_head_weight=self.lm_head.weight, + labels=labels, + hidden_size=self.config.hidden_size, + **loss_kwargs, + ) + + else: # if in inference mode materialize logits + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + logits = self.lm_head(hidden_states[:, slice_indices, :]) + if labels is not None: + loss = self.loss_function( + logits=logits, + labels=labels, + vocab_size=self.config.vocab_size, + **loss_kwargs, + ) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +def apply_liger_kernel_to_llama4( + cross_entropy: bool = False, + fused_linear_cross_entropy: bool = False, + rms_norm: bool = False, + glu_activation: bool = False, + layer_norm: bool = False, + **kwargs, # pylint: disable=unused-argument +) -> None: + """ + Apply Liger kernels to replace original implementation in HuggingFace Llama models (2 and 3) + + Args: + cross_entropy (bool): Whether to apply Liger's cross entropy loss. Default is False. + fused_linear_cross_entropy (bool): + Whether to apply Liger's fused linear cross entropy loss. Default is False. + `cross_entropy` and `fused_linear_cross_entropy` cannot both be False. + If `fused_linear_cross_entropy` is True, the logits will not be materialized but more memory efficient. + rms_norm (bool): Whether to apply Liger's RMSNorm. Default is False. + glu_activation (bool): Whether to apply Liger's SwiGLU MLP. Default is False. + layer_norm (bool): Whether to apply Liger's LayerNorm. Default is False. + """ + + import transformers.models.llama4.modeling_llama4 # noqa: F401 # pylint: disable=unused-import + from liger_kernel.transformers.functional import liger_cross_entropy + from liger_kernel.transformers.layer_norm import LigerLayerNorm + from liger_kernel.transformers.rms_norm import LigerRMSNorm + from liger_kernel.transformers.swiglu import LigerSwiGLUMLP + + assert not ( + cross_entropy and fused_linear_cross_entropy + ), "cross_entropy and fused_linear_cross_entropy cannot both be True." + + modeling_llama4 = sys.modules["transformers.models.llama4.modeling_llama4"] + + if rms_norm: + modeling_llama4.Llama4TextRMSNorm = LigerRMSNorm + if glu_activation: + modeling_llama4.Llama4TextMLP = LigerSwiGLUMLP + if layer_norm: + modeling_llama4.nn.LayerNorm = LigerLayerNorm + + if cross_entropy: + from transformers.loss.loss_utils import nn + + nn.functional.cross_entropy = liger_cross_entropy + + if fused_linear_cross_entropy: + modeling_llama4.Llama4ForCausalLM.forward = lce_forward diff --git a/src/axolotl/monkeypatch/attention/flex_attn.py b/src/axolotl/monkeypatch/attention/flex_attn.py index 2ca5b09a62..d65ee706f8 100644 --- a/src/axolotl/monkeypatch/attention/flex_attn.py +++ b/src/axolotl/monkeypatch/attention/flex_attn.py @@ -162,7 +162,6 @@ def mask_mod(batch_idx, head_idx, q_idx, kv_idx): for n in tuple(sys.modules): if ".modeling_" in n and "llama4" not in n: if hasattr(sys.modules[n], "make_flex_block_causal_mask"): - print(n) sys.modules[n].make_flex_block_causal_mask = ( patched_make_flex_block_causal_mask ) diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 0157433291..2b02699bdf 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -13,6 +13,7 @@ SUPPORTED_MULTIPACK_MODEL_TYPES = [ "mllama_text_model", "llama", + "llama4", "mistral", "mixtral", "qwen2", diff --git a/src/axolotl/monkeypatch/trainer_accelerator_args.py b/src/axolotl/monkeypatch/trainer_accelerator_args.py new file mode 100644 index 0000000000..d87812c9f8 --- /dev/null +++ b/src/axolotl/monkeypatch/trainer_accelerator_args.py @@ -0,0 +1,80 @@ +""" +allow adding additional kwargs to Accelerator init +""" + +import inspect +import logging + +from transformers import Trainer + +from axolotl.monkeypatch.utils import detab_code + +LOG = logging.getLogger(__name__) + +ORIGINAL_TRAINER_CODE = """ + # create accelerator object + self.accelerator = Accelerator(**args) +""" + +PATCHED_TRAINER_CODE = """ + if hasattr(self, "additional_accelerator_args"): + additional_args = self.additional_accelerator_args(fp8=True, **args) + if additional_args: + args.update(additional_args) + + # create accelerator object + self.accelerator = Accelerator(**args) +""" + + +def get_create_accelerate_code() -> str: + training_loop = inspect.getsource(Trainer.create_accelerator_and_postprocess) + return training_loop + + +def check_create_accelerate_code_is_patchable() -> bool: + create_code = get_create_accelerate_code() + create_code, _ = detab_code(create_code) + return ORIGINAL_TRAINER_CODE in create_code + + +def patch_create_accelerate_code_for_fp8(): + """ + monkeypatch create_accelerator_and_postprocess so it checks for additional kwargs + """ + + try: + create_code = get_create_accelerate_code() + except OSError: + return + Trainer._original_create_accelerator_and_postprocess = ( # pylint: disable=protected-access + create_code + ) + create_code, _ = detab_code(create_code) + if ORIGINAL_TRAINER_CODE not in create_code: + return + + create_code = create_code.replace(ORIGINAL_TRAINER_CODE, PATCHED_TRAINER_CODE) + create_code = create_code.replace( + "def create_accelerator_and_postprocess(", + "def fixed_create_accelerator_and_postprocess(", + 1, + ) + + # load imports necessary + import transformers.trainer + + items_to_import = [] + for item in dir(transformers.trainer): + if item in create_code: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + "from transformers.trainer import (" + + ", ".join(x for x in items_to_import) + + ")", + globals(), + ) + exec(create_code, globals()) # pylint: disable=exec-used # nosec B102 + LOG.info("patching create_accelerator_and_postprocess to allow for overrides") + Trainer.create_accelerator_and_postprocess = fixed_create_accelerator_and_postprocess # pylint: disable=protected-access # pylint: disable=undefined-variable # noqa: F821 diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index ba0516eb91..234b42d8dc 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -26,6 +26,7 @@ "gemma3": "{{ bos_token }}\n{%- if messages[0]['role'] == 'system' -%}\n {%- if messages[0]['content'] is string -%}\n {%- set first_user_prefix = messages[0]['content'] + '\n\n' -%}\n {%- else -%}\n {%- set first_user_prefix = messages[0]['content'][0]['text'] + '\n\n' -%}\n {%- endif -%}\n {%- set loop_messages = messages[1:] -%}\n{%- else -%}\n {%- set first_user_prefix = \"\" -%}\n {%- set loop_messages = messages -%}\n{%- endif -%}\n{%- for message in loop_messages -%}\n {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) -%}\n {{ raise_exception(\"Conversation roles must alternate user/assistant/user/assistant/...\") }}\n {%- endif -%}\n {%- if (message['role'] == 'assistant') -%}\n {%- set role = \"model\" -%}\n {%- else -%}\n {%- set role = message['role'] -%}\n {%- endif -%}\n {{ '' + role + '\n' + (first_user_prefix if loop.first else \"\") }}\n {%- if message['content'] is string -%}\n {{ message['content'] | trim }}\n {%- elif message['content'] is iterable -%}\n {%- for item in message['content'] -%}\n {%- if item['type'] == 'image' -%}\n {{ '' }}\n {%- elif item['type'] == 'text' -%}\n {{ item['text'] | trim }}\n {%- endif -%}\n {%- endfor -%}\n {%- else -%}\n {{ raise_exception(\"Invalid content type\") }}\n {%- endif -%}\n {{ '\n' }}\n{%- endfor -%}\n{%- if add_generation_prompt -%}\n {{'model\n'}}\n{%- endif -%}\n", "cohere": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", + "llama4": "{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now(\"%d %b %Y\") %}\n {%- else %}\n {%- set date_string = \"26 Jul 2024\" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0]['role'] == 'system' %} \n {%- if messages[0]['content'] is string %}\n {%- set system_message = messages[0]['content']|trim %}\n {%- else %}\n {#- FIXME: The processor requires an array, always. #}\n {%- set system_message = messages[0]['content'][0]['text']|trim %}\n {%- endif %}\n {%- set messages = messages[1:] %}\n {%- set user_supplied_system_message = true %}\n{%- else %}\n {%- set system_message = \"\" %}\n {%- set user_supplied_system_message = false %}\n{%- endif %}\n\n{#- System message if the user supplied one #}\n{%- if user_supplied_system_message %}\n {{- \"<|header_start|>system<|header_end|>\\n\\n\" }}\n {%- if tools is not none %}\n {{- \"Environment: ipython\\n\" }}\n {%- endif %}\n {%- if tools is not none and not tools_in_user_message %}\n {{- \"You have access to the following functions. To call a function, please respond with JSON for a function call.\" }}\n {{- 'Respond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}.' }}\n {{- \"Do not use variables.\\n\\n\" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- \"\\n\\n\" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- \"<|eot|>\" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0]['content']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception(\"Cannot put tools in the first user message when there's no first user message!\") }}\n{%- endif %}\n {{- '<|header_start|>user<|header_end|>\\n\\n' -}}\n {{- \"Given the following functions, please respond with a JSON for a function call \" }}\n {{- \"with its proper arguments that best answers the given prompt.\\n\\n\" }}\n {{- 'Respond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}.' }}\n {{- \"Do not use variables.\\n\\n\" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- \"\\n\\n\" }}\n {%- endfor %}\n {{- first_user_message + \"<|eot|>\"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}\n {{- '<|header_start|>' + message['role'] + '<|header_end|>\\n\\n' }}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- else %}\n {%- for content in message['content'] %}\n {%- if content['type'] == 'image' %}\n {{- '<|image|>' }}\n {%- elif content['type'] == 'text' %}\n {{- content['text'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \"<|eot|>\" }}\n {%- elif 'tool_calls' in message and message.tool_calls|length > 0 %}\n {{- '<|header_start|>assistant<|header_end|>\\n\\n' -}}\n {{- '<|python_start|>' }}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- else %}\n {%- for content in message['content'] %}\n {%- if content['type'] == 'image' %}\n {{- '<|image|>' }}\n {%- elif content['type'] == 'text' %}\n {{- content['text'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '<|python_end|>' }}\n {%- for tool_call in message.tool_calls %}\n {{- '{\"name\": \"' + tool_call.function.name + '\", ' }}\n {{- '\"parameters\": ' }}\n {{- tool_call.function.arguments | tojson }}\n {{- \"}\" }}\n {%- endfor %}\n {{- \"<|eot|>\" }}\n {%- elif message.role == \"tool\" or message.role == \"ipython\" %}\n {{- \"<|header_start|>ipython<|header_end|>\\n\\n\" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- \"<|eot|>\" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|header_start|>assistant<|header_end|>\\n\\n' }}\n{%- endif %}\n", "llama3_2_vision": '{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now("%d %b %Y") %}\n {%- else %}\n {%- set date_string = "26 Jul 2024" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0][\'role\'] == \'system\' %}\n {%- set system_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n{%- else %}\n {%- set system_message = "" %}\n{%- endif %}\n\n{#- Find out if there are any images #}\n{% set image_ns = namespace(has_images=false) %} \n{%- for message in messages %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {%- set image_ns.has_images = true %}\n {%- endif %}\n {%- endfor %}\n{%- endfor %}\n\n{#- Error out if there are images and system message #}\n{%- if image_ns.has_images and not system_message == "" %}\n {{- raise_exception("Prompting with images is incompatible with system messages.") }}\n{%- endif %}\n\n{#- System message if there are no images #}\n{%- if not image_ns.has_images %}\n {{- "<|start_header_id|>system<|end_header_id|>\\n\\n" }}\n {%- if tools is not none %}\n {{- "Environment: ipython\\n" }}\n {%- endif %}\n {{- "Cutting Knowledge Date: December 2023\\n" }}\n {{- "Today Date: " + date_string + "\\n\\n" }}\n {%- if tools is not none and not tools_in_user_message %}\n {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- "<|eot_id|>" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception("Cannot put tools in the first user message when there\'s no first user message!") }}\n{%- endif %}\n {{- \'<|start_header_id|>user<|end_header_id|>\\n\\n\' -}}\n {{- "Given the following functions, please respond with a JSON for a function call " }}\n {{- "with its proper arguments that best answers the given prompt.\\n\\n" }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {{- first_user_message + "<|eot_id|>"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == \'ipython\' or message.role == \'tool\' or \'tool_calls\' in message) %}\n {{- \'<|start_header_id|>\' + message[\'role\'] + \'<|end_header_id|>\\n\\n\' }}\n {%- if message[\'content\'] is string %}\n {{- message[\'content\'] }}\n {%- else %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {{- \'<|image|>\' }}\n {%- elif content[\'type\'] == \'text\' %}\n {{- content[\'text\'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \'<|eot_id|>\' }}\n {%- elif \'tool_calls\' in message %}\n {%- if not message.tool_calls|length == 1 %}\n {{- raise_exception("This model only supports single tool-calls at once!") }}\n {%- endif %}\n {%- set tool_call = message.tool_calls[0].function %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' -}}\n {{- \'{"name": "\' + tool_call.name + \'", \' }}\n {{- \'"parameters": \' }}\n {{- tool_call.arguments | tojson }}\n {{- "}" }}\n {{- "<|eot_id|>" }}\n {%- elif message.role == "tool" or message.role == "ipython" %}\n {{- "<|start_header_id|>ipython<|end_header_id|>\\n\\n" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- "<|eot_id|>" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' }}\n{%- endif %}\n', "llava": "{% for message in messages %}{% if message['role'] != 'system' %}{{ message['role'].upper() + ': '}}{% endif %}{# Render all images first #}{% for content in message['content'] | selectattr('type', 'equalto', 'image') %}{{ '\n' }}{% endfor %}{# Render all text next #}{% if message['role'] != 'assistant' %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{{ content['text'] + ' '}}{% endfor %}{% else %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{% generation %}{{ content['text'] + ' '}}{% endgeneration %}{% endfor %}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ 'ASSISTANT:' }}{% endif %}", "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 0e1329b973..367e698502 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -557,6 +557,14 @@ def apply_patches(self) -> None: plugin_manager = PluginManager.get_instance() plugin_manager.pre_model_load(self.cfg) + # monkey patch to allow additional Accelerator init kwargs + if self.cfg.fp8: + from axolotl.monkeypatch.trainer_accelerator_args import ( + patch_create_accelerate_code_for_fp8, + ) + + patch_create_accelerate_code_for_fp8() + if self.cfg.adapter: from axolotl.monkeypatch.transformers_fa_utils import ( patch_fa_peft_integration, @@ -988,10 +996,11 @@ def _configure_zero3_memory_efficient_loading(): ) skip_move_to_device = True elif ( - self.model_config.model_type == "llama" + self.model_config.model_type in ["llama", "llama4"] and not self.cfg.trust_remote_code and not self.cfg.gptq ): + # TODO do we need to open this up for all models? if self.cfg.fsdp and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: skip_move_to_device = True if "device_map" in self.model_kwargs: diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 3ceae42738..0f9a3a1f97 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -169,6 +169,7 @@ class AxolotlInputConfig( bf16: Literal["auto"] | bool | None = "auto" fp16: bool | None = None + fp8: bool | None = None bfloat16: bool | None = None # for non-AMP cases float16: bool | None = None # for non-AMP cases tf32: bool | None = None @@ -464,9 +465,10 @@ def check_sample_packing_wo_flash(cls, data): data.get("sample_packing") and not data.get("flash_attention") and not data.get("sdp_attention") + and not data.get("flex_attention") ): LOG.warning( - "sample_packing without flash_attention or sdp_attention does not handle cross-attention." + "sample_packing without flash, sdp or flex attention does not handle cross sample decontamination." ) return data diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index ad735afe0e..16b91ec412 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -26,6 +26,7 @@ class ChatTemplate(str, Enum): gemma = "gemma" # pylint: disable=invalid-name cohere = "cohere" # pylint: disable=invalid-name llama3 = "llama3" # pylint: disable=invalid-name + llama4 = "llama4" # pylint: disable=invalid-name llama3_2_vision = "llama3_2_vision" # pylint: disable=invalid-name phi_3 = "phi_3" # pylint: disable=invalid-name phi_35 = "phi_35" # pylint: disable=invalid-name diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index c5c9e5599f..964b170869 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -582,7 +582,9 @@ def prepare_optim_env(cfg): setup_torch_compile_env(cfg) - if (cfg.bf16 == "auto" and is_torch_bf16_gpu_available()) or cfg.bf16 is True: + if cfg.fp8: + os.environ["ACCELERATE_MIXED_PRECISION"] = "fp8" + elif (cfg.bf16 == "auto" and is_torch_bf16_gpu_available()) or cfg.bf16 is True: os.environ["ACCELERATE_MIXED_PRECISION"] = "bf16" elif cfg.fp16: os.environ["ACCELERATE_MIXED_PRECISION"] = "fp16" diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index d71fa25c89..f44c775c8d 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -7,9 +7,11 @@ from pathlib import Path import pytest +import transformers import yaml from accelerate.test_utils import execute_subprocess_async from huggingface_hub import snapshot_download +from packaging import version from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault @@ -28,6 +30,10 @@ def download_model(): snapshot_download("HuggingFaceTB/SmolLM2-135M") +def transformers_version_eq(required_version): + return version.parse(transformers.__version__) == version.parse(required_version) + + class TestMultiGPULlama: """ Test case for Llama models using LoRA @@ -56,7 +62,7 @@ def test_lora_ddp(self, temp_dir): ], "num_epochs": 1, "max_steps": 2, - "micro_batch_size": 4, + "micro_batch_size": 1, "gradient_accumulation_steps": 4, # "gradient_checkpointing": True, "output_dir": temp_dir, @@ -108,7 +114,7 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.01, + "val_set_size": 0.05, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -116,6 +122,7 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): { "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:20%]", }, ], "num_epochs": 1, @@ -193,7 +200,7 @@ def test_dpo_lora_ddp(self, temp_dir): ], "num_epochs": 1, "max_steps": 2, - "micro_batch_size": 4, + "micro_batch_size": 2, "gradient_accumulation_steps": 4, # "gradient_checkpointing": True, "output_dir": temp_dir, @@ -390,7 +397,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): "base_model": "HuggingFaceTB/SmolLM2-135M", "sample_packing": True, "pad_to_sequence_len": True, - "sequence_len": 2048, + "sequence_len": 1024, "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", @@ -403,7 +410,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): ], "num_epochs": 1, "max_steps": 2, - "micro_batch_size": 4, + "micro_batch_size": 2, "gradient_accumulation_steps": 2, # "gradient_checkpointing": True, "output_dir": temp_dir, @@ -493,9 +500,7 @@ def test_fsdp2_packed( ], "fsdp_config": { "fsdp_version": 2, - "fsdp_forward_prefetch": True, - "fsdp_sync_module_states": True, - "fsdp_use_orig_params": True, + # "fsdp_forward_prefetch": True, # not yet implemented in accelerate "fsdp_offload_params": False, "fsdp_cpu_ram_efficient_loading": False, "fsdp_transformer_layer_cls_to_wrap": "LlamaDecoderLayer", @@ -551,7 +556,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "sample_packing": True, "eval_sample_packing": False, "pad_to_sequence_len": True, - "sequence_len": 2048, + "sequence_len": 1024, "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", @@ -565,7 +570,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): ], "num_epochs": 1, "max_steps": 2, - "micro_batch_size": 4, + "micro_batch_size": 2, "gradient_accumulation_steps": 2, # "gradient_checkpointing": True, "output_dir": temp_dir, @@ -612,8 +617,11 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" ) - @pytest.mark.skip( - reason="ds-zero3 broken in main until transformers#37281 resolved" + # TODO: remove skip once deepspeed regression is fixed + # see https://github.com/huggingface/transformers/pull/37324 + @pytest.mark.skipif( + transformers_version_eq("4.51.0"), + reason="zero3 is not supported with transformers==4.51.0", ) @pytest.mark.parametrize( "gradient_accumulation_steps", @@ -651,7 +659,7 @@ def test_ds_zero3_packed( "base_model": "HuggingFaceTB/SmolLM2-135M", "sample_packing": True, "pad_to_sequence_len": True, - "sequence_len": 2048, + "sequence_len": 1024, "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", @@ -724,7 +732,7 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): "base_model": "HuggingFaceTB/SmolLM2-135M", "sample_packing": True, "pad_to_sequence_len": True, - "sequence_len": 2048, + "sequence_len": 1024, "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", @@ -797,7 +805,7 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): "base_model": "HuggingFaceTB/SmolLM2-135M", "sample_packing": True, "pad_to_sequence_len": True, - "sequence_len": 2048, + "sequence_len": 1024, "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", @@ -885,7 +893,7 @@ def test_fix_untrained_tokens(self, temp_dir): "sample_packing": True, "bf16": True, "save_safetensors": True, - "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero1.json"), + # "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero1.json"), "use_tensorboard": True, } ) diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index 14b1c0a86f..9be7c6f503 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -31,7 +31,7 @@ def test_lora_ddp(self, temp_dir): cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", - "sequence_len": 2048, + "sequence_len": 1024, "adapter": "lora", "lora_r": 8, "lora_alpha": 16, @@ -94,8 +94,8 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps): "base_model": "HuggingFaceTB/SmolLM2-135M", "sample_packing": True, "pad_to_sequence_len": True, - "sequence_len": 2048, - "val_set_size": 0.05, + "sequence_len": 1024, + "val_set_size": 0.01, "special_tokens": { "pad_token": "<|endoftext|>", }, From e0e5d9b1d672ce5650b2fb4f9779d0f01cf17abc Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 7 Apr 2025 21:49:29 +0700 Subject: [PATCH 0530/1405] feat: add llama4 multimodal (#2499) * feat: add llama4 multimodal * feat: add torchvision to base docker * just use latest torchvision --------- Co-authored-by: Wing Lian --- docker/Dockerfile-base | 2 +- docs/multimodal.qmd | 9 +++++++++ src/axolotl/processing_strategies.py | 1 + src/axolotl/utils/chat_templates.py | 1 + src/axolotl/utils/models.py | 2 ++ src/axolotl/utils/schemas/enums.py | 1 + 6 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index e989152ec5..52201f276e 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -29,7 +29,7 @@ ENV PATH="/root/miniconda3/envs/py${PYTHON_VERSION}/bin:${PATH}" WORKDIR /workspace RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==23.2 setuptools==75.8.0 wheel && \ - python3 -m pip install --no-cache-dir -U torch==${PYTORCH_VERSION}+cu${CUDA} --extra-index-url https://download.pytorch.org/whl/cu$CUDA && \ + python3 -m pip install --no-cache-dir -U torch==${PYTORCH_VERSION}+cu${CUDA} torchvision --extra-index-url https://download.pytorch.org/whl/cu$CUDA && \ python3 -m pip install --no-cache-dir "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" && \ python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index e8e7934826..5ddd91f60f 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -9,6 +9,7 @@ format: ## Supported Models - [Mllama](#sec-mllama) +- [Llama4](#sec-llama4) - [Pixtral](#sec-pixtral) - [Llava-1.5](#sec-llava-15) - [Mistral-Small-3.1](#sec-mistral-small-31) @@ -63,6 +64,14 @@ base_model: meta-llama/Llama-3.2-11B-Vision-Instruct chat_template: llama3_2_vision ``` +### Llama4 {#sec-llama4} + +```yaml +base_model: meta-llama/Llama-4-Scout-17B-16E-Instruct + +chat_template: llama4 +``` + ### Pixtral {#sec-pixtral} ```yaml diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 0b854af8dc..4dee4f8a28 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -268,6 +268,7 @@ def get_processing_strategy( ) if chat_template_type in [ "llama3_2_vision", + "llama4", "llava", "mistral_v7_tekken", "pixtral", diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 234b42d8dc..f0cead09b3 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -28,6 +28,7 @@ "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", "llama4": "{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now(\"%d %b %Y\") %}\n {%- else %}\n {%- set date_string = \"26 Jul 2024\" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0]['role'] == 'system' %} \n {%- if messages[0]['content'] is string %}\n {%- set system_message = messages[0]['content']|trim %}\n {%- else %}\n {#- FIXME: The processor requires an array, always. #}\n {%- set system_message = messages[0]['content'][0]['text']|trim %}\n {%- endif %}\n {%- set messages = messages[1:] %}\n {%- set user_supplied_system_message = true %}\n{%- else %}\n {%- set system_message = \"\" %}\n {%- set user_supplied_system_message = false %}\n{%- endif %}\n\n{#- System message if the user supplied one #}\n{%- if user_supplied_system_message %}\n {{- \"<|header_start|>system<|header_end|>\\n\\n\" }}\n {%- if tools is not none %}\n {{- \"Environment: ipython\\n\" }}\n {%- endif %}\n {%- if tools is not none and not tools_in_user_message %}\n {{- \"You have access to the following functions. To call a function, please respond with JSON for a function call.\" }}\n {{- 'Respond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}.' }}\n {{- \"Do not use variables.\\n\\n\" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- \"\\n\\n\" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- \"<|eot|>\" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0]['content']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception(\"Cannot put tools in the first user message when there's no first user message!\") }}\n{%- endif %}\n {{- '<|header_start|>user<|header_end|>\\n\\n' -}}\n {{- \"Given the following functions, please respond with a JSON for a function call \" }}\n {{- \"with its proper arguments that best answers the given prompt.\\n\\n\" }}\n {{- 'Respond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}.' }}\n {{- \"Do not use variables.\\n\\n\" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- \"\\n\\n\" }}\n {%- endfor %}\n {{- first_user_message + \"<|eot|>\"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}\n {{- '<|header_start|>' + message['role'] + '<|header_end|>\\n\\n' }}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- else %}\n {%- for content in message['content'] %}\n {%- if content['type'] == 'image' %}\n {{- '<|image|>' }}\n {%- elif content['type'] == 'text' %}\n {{- content['text'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \"<|eot|>\" }}\n {%- elif 'tool_calls' in message and message.tool_calls|length > 0 %}\n {{- '<|header_start|>assistant<|header_end|>\\n\\n' -}}\n {{- '<|python_start|>' }}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- else %}\n {%- for content in message['content'] %}\n {%- if content['type'] == 'image' %}\n {{- '<|image|>' }}\n {%- elif content['type'] == 'text' %}\n {{- content['text'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '<|python_end|>' }}\n {%- for tool_call in message.tool_calls %}\n {{- '{\"name\": \"' + tool_call.function.name + '\", ' }}\n {{- '\"parameters\": ' }}\n {{- tool_call.function.arguments | tojson }}\n {{- \"}\" }}\n {%- endfor %}\n {{- \"<|eot|>\" }}\n {%- elif message.role == \"tool\" or message.role == \"ipython\" %}\n {{- \"<|header_start|>ipython<|header_end|>\\n\\n\" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- \"<|eot|>\" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|header_start|>assistant<|header_end|>\\n\\n' }}\n{%- endif %}\n", "llama3_2_vision": '{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now("%d %b %Y") %}\n {%- else %}\n {%- set date_string = "26 Jul 2024" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0][\'role\'] == \'system\' %}\n {%- set system_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n{%- else %}\n {%- set system_message = "" %}\n{%- endif %}\n\n{#- Find out if there are any images #}\n{% set image_ns = namespace(has_images=false) %} \n{%- for message in messages %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {%- set image_ns.has_images = true %}\n {%- endif %}\n {%- endfor %}\n{%- endfor %}\n\n{#- Error out if there are images and system message #}\n{%- if image_ns.has_images and not system_message == "" %}\n {{- raise_exception("Prompting with images is incompatible with system messages.") }}\n{%- endif %}\n\n{#- System message if there are no images #}\n{%- if not image_ns.has_images %}\n {{- "<|start_header_id|>system<|end_header_id|>\\n\\n" }}\n {%- if tools is not none %}\n {{- "Environment: ipython\\n" }}\n {%- endif %}\n {{- "Cutting Knowledge Date: December 2023\\n" }}\n {{- "Today Date: " + date_string + "\\n\\n" }}\n {%- if tools is not none and not tools_in_user_message %}\n {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- "<|eot_id|>" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception("Cannot put tools in the first user message when there\'s no first user message!") }}\n{%- endif %}\n {{- \'<|start_header_id|>user<|end_header_id|>\\n\\n\' -}}\n {{- "Given the following functions, please respond with a JSON for a function call " }}\n {{- "with its proper arguments that best answers the given prompt.\\n\\n" }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {{- first_user_message + "<|eot_id|>"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == \'ipython\' or message.role == \'tool\' or \'tool_calls\' in message) %}\n {{- \'<|start_header_id|>\' + message[\'role\'] + \'<|end_header_id|>\\n\\n\' }}\n {%- if message[\'content\'] is string %}\n {{- message[\'content\'] }}\n {%- else %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {{- \'<|image|>\' }}\n {%- elif content[\'type\'] == \'text\' %}\n {{- content[\'text\'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \'<|eot_id|>\' }}\n {%- elif \'tool_calls\' in message %}\n {%- if not message.tool_calls|length == 1 %}\n {{- raise_exception("This model only supports single tool-calls at once!") }}\n {%- endif %}\n {%- set tool_call = message.tool_calls[0].function %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' -}}\n {{- \'{"name": "\' + tool_call.name + \'", \' }}\n {{- \'"parameters": \' }}\n {{- tool_call.arguments | tojson }}\n {{- "}" }}\n {{- "<|eot_id|>" }}\n {%- elif message.role == "tool" or message.role == "ipython" %}\n {{- "<|start_header_id|>ipython<|end_header_id|>\\n\\n" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- "<|eot_id|>" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' }}\n{%- endif %}\n', + "llama4": "{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now(\"%d %b %Y\") %}\n {%- else %}\n {%- set date_string = \"26 Jul 2024\" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0]['role'] == 'system' %} \n {%- if messages[0]['content'] is string %}\n {%- set system_message = messages[0]['content']|trim %}\n {%- else %}\n {#- FIXME: The processor requires an array, always. #}\n {%- set system_message = messages[0]['content'][0]['text']|trim %}\n {%- endif %}\n {%- set messages = messages[1:] %}\n {%- set user_supplied_system_message = true %}\n{%- else %}\n {%- set system_message = \"\" %}\n {%- set user_supplied_system_message = false %}\n{%- endif %}\n\n{#- System message if the user supplied one #}\n{%- if user_supplied_system_message %}\n {{- \"<|header_start|>system<|header_end|>\\n\\n\" }}\n {%- if tools is not none %}\n {{- \"Environment: ipython\\n\" }}\n {%- endif %}\n {%- if tools is not none and not tools_in_user_message %}\n {{- \"You have access to the following functions. To call a function, please respond with JSON for a function call.\" }}\n {{- 'Respond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}.' }}\n {{- \"Do not use variables.\\n\\n\" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- \"\\n\\n\" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- \"<|eot|>\" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0]['content']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception(\"Cannot put tools in the first user message when there's no first user message!\") }}\n{%- endif %}\n {{- '<|header_start|>user<|header_end|>\\n\\n' -}}\n {{- \"Given the following functions, please respond with a JSON for a function call \" }}\n {{- \"with its proper arguments that best answers the given prompt.\\n\\n\" }}\n {{- 'Respond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}.' }}\n {{- \"Do not use variables.\\n\\n\" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- \"\\n\\n\" }}\n {%- endfor %}\n {{- first_user_message + \"<|eot|>\"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}\n {{- '<|header_start|>' + message['role'] + '<|header_end|>\\n\\n' }}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- else %}\n {%- for content in message['content'] %}\n {%- if content['type'] == 'image' %}\n {{- '<|image|>' }}\n {%- elif content['type'] == 'text' %}\n {{- content['text'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \"<|eot|>\" }}\n {%- elif 'tool_calls' in message and message.tool_calls|length > 0 %}\n {{- '<|header_start|>assistant<|header_end|>\\n\\n' -}}\n {{- '<|python_start|>' }}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- else %}\n {%- for content in message['content'] %}\n {%- if content['type'] == 'image' %}\n {{- '<|image|>' }}\n {%- elif content['type'] == 'text' %}\n {{- content['text'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '<|python_end|>' }}\n {%- for tool_call in message.tool_calls %}\n {{- '{\"name\": \"' + tool_call.function.name + '\", ' }}\n {{- '\"parameters\": ' }}\n {{- tool_call.function.arguments | tojson }}\n {{- \"}\" }}\n {%- endfor %}\n {{- \"<|eot|>\" }}\n {%- elif message.role == \"tool\" or message.role == \"ipython\" %}\n {{- \"<|header_start|>ipython<|header_end|>\\n\\n\" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- \"<|eot|>\" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|header_start|>assistant<|header_end|>\\n\\n' }}\n{%- endif %}\n", "llava": "{% for message in messages %}{% if message['role'] != 'system' %}{{ message['role'].upper() + ': '}}{% endif %}{# Render all images first #}{% for content in message['content'] | selectattr('type', 'equalto', 'image') %}{{ '\n' }}{% endfor %}{# Render all text next #}{% if message['role'] != 'assistant' %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{{ content['text'] + ' '}}{% endfor %}{% else %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{% generation %}{{ content['text'] + ' '}}{% endgeneration %}{% endfor %}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ 'ASSISTANT:' }}{% endif %}", "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", "phi_35": "{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'user' %}{{'<|user|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|>\n' + message['content'] + '<|end|>\n'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% endif %}", diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 367e698502..024673b8ec 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -36,6 +36,7 @@ BitsAndBytesConfig, Gemma3ForConditionalGeneration, GPTQConfig, + Llama4ForConditionalGeneration, LlavaForConditionalGeneration, Mistral3ForConditionalGeneration, MllamaForConditionalGeneration, @@ -76,6 +77,7 @@ MULTIMODAL_AUTO_MODEL_MAPPING = { "mllama": MllamaForConditionalGeneration, + "llama4": Llama4ForConditionalGeneration, "llava": LlavaForConditionalGeneration, "qwen2_vl": Qwen2VLForConditionalGeneration, "qwen2_5_vl": Qwen2_5_VLForConditionalGeneration, diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 16b91ec412..d56d7534a6 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -28,6 +28,7 @@ class ChatTemplate(str, Enum): llama3 = "llama3" # pylint: disable=invalid-name llama4 = "llama4" # pylint: disable=invalid-name llama3_2_vision = "llama3_2_vision" # pylint: disable=invalid-name + llama4 = "llama4" # pylint: disable=invalid-name phi_3 = "phi_3" # pylint: disable=invalid-name phi_35 = "phi_35" # pylint: disable=invalid-name deepseek_v2 = "deepseek_v2" # pylint: disable=invalid-name From d25daebea972d144d33b0006a6b978512c14e25b Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 7 Apr 2025 23:39:19 +0700 Subject: [PATCH 0531/1405] fix: duplicate llama4 chattemplate enum (#2500) * fix: duplicate llama4 chattemplate enum * fix: duplicate chat_template string --- src/axolotl/utils/chat_templates.py | 1 - src/axolotl/utils/schemas/enums.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index f0cead09b3..5220abf844 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -26,7 +26,6 @@ "gemma3": "{{ bos_token }}\n{%- if messages[0]['role'] == 'system' -%}\n {%- if messages[0]['content'] is string -%}\n {%- set first_user_prefix = messages[0]['content'] + '\n\n' -%}\n {%- else -%}\n {%- set first_user_prefix = messages[0]['content'][0]['text'] + '\n\n' -%}\n {%- endif -%}\n {%- set loop_messages = messages[1:] -%}\n{%- else -%}\n {%- set first_user_prefix = \"\" -%}\n {%- set loop_messages = messages -%}\n{%- endif -%}\n{%- for message in loop_messages -%}\n {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) -%}\n {{ raise_exception(\"Conversation roles must alternate user/assistant/user/assistant/...\") }}\n {%- endif -%}\n {%- if (message['role'] == 'assistant') -%}\n {%- set role = \"model\" -%}\n {%- else -%}\n {%- set role = message['role'] -%}\n {%- endif -%}\n {{ '' + role + '\n' + (first_user_prefix if loop.first else \"\") }}\n {%- if message['content'] is string -%}\n {{ message['content'] | trim }}\n {%- elif message['content'] is iterable -%}\n {%- for item in message['content'] -%}\n {%- if item['type'] == 'image' -%}\n {{ '' }}\n {%- elif item['type'] == 'text' -%}\n {{ item['text'] | trim }}\n {%- endif -%}\n {%- endfor -%}\n {%- else -%}\n {{ raise_exception(\"Invalid content type\") }}\n {%- endif -%}\n {{ '\n' }}\n{%- endfor -%}\n{%- if add_generation_prompt -%}\n {{'model\n'}}\n{%- endif -%}\n", "cohere": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", - "llama4": "{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now(\"%d %b %Y\") %}\n {%- else %}\n {%- set date_string = \"26 Jul 2024\" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0]['role'] == 'system' %} \n {%- if messages[0]['content'] is string %}\n {%- set system_message = messages[0]['content']|trim %}\n {%- else %}\n {#- FIXME: The processor requires an array, always. #}\n {%- set system_message = messages[0]['content'][0]['text']|trim %}\n {%- endif %}\n {%- set messages = messages[1:] %}\n {%- set user_supplied_system_message = true %}\n{%- else %}\n {%- set system_message = \"\" %}\n {%- set user_supplied_system_message = false %}\n{%- endif %}\n\n{#- System message if the user supplied one #}\n{%- if user_supplied_system_message %}\n {{- \"<|header_start|>system<|header_end|>\\n\\n\" }}\n {%- if tools is not none %}\n {{- \"Environment: ipython\\n\" }}\n {%- endif %}\n {%- if tools is not none and not tools_in_user_message %}\n {{- \"You have access to the following functions. To call a function, please respond with JSON for a function call.\" }}\n {{- 'Respond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}.' }}\n {{- \"Do not use variables.\\n\\n\" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- \"\\n\\n\" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- \"<|eot|>\" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0]['content']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception(\"Cannot put tools in the first user message when there's no first user message!\") }}\n{%- endif %}\n {{- '<|header_start|>user<|header_end|>\\n\\n' -}}\n {{- \"Given the following functions, please respond with a JSON for a function call \" }}\n {{- \"with its proper arguments that best answers the given prompt.\\n\\n\" }}\n {{- 'Respond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}.' }}\n {{- \"Do not use variables.\\n\\n\" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- \"\\n\\n\" }}\n {%- endfor %}\n {{- first_user_message + \"<|eot|>\"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}\n {{- '<|header_start|>' + message['role'] + '<|header_end|>\\n\\n' }}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- else %}\n {%- for content in message['content'] %}\n {%- if content['type'] == 'image' %}\n {{- '<|image|>' }}\n {%- elif content['type'] == 'text' %}\n {{- content['text'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \"<|eot|>\" }}\n {%- elif 'tool_calls' in message and message.tool_calls|length > 0 %}\n {{- '<|header_start|>assistant<|header_end|>\\n\\n' -}}\n {{- '<|python_start|>' }}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- else %}\n {%- for content in message['content'] %}\n {%- if content['type'] == 'image' %}\n {{- '<|image|>' }}\n {%- elif content['type'] == 'text' %}\n {{- content['text'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '<|python_end|>' }}\n {%- for tool_call in message.tool_calls %}\n {{- '{\"name\": \"' + tool_call.function.name + '\", ' }}\n {{- '\"parameters\": ' }}\n {{- tool_call.function.arguments | tojson }}\n {{- \"}\" }}\n {%- endfor %}\n {{- \"<|eot|>\" }}\n {%- elif message.role == \"tool\" or message.role == \"ipython\" %}\n {{- \"<|header_start|>ipython<|header_end|>\\n\\n\" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- \"<|eot|>\" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|header_start|>assistant<|header_end|>\\n\\n' }}\n{%- endif %}\n", "llama3_2_vision": '{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now("%d %b %Y") %}\n {%- else %}\n {%- set date_string = "26 Jul 2024" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0][\'role\'] == \'system\' %}\n {%- set system_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n{%- else %}\n {%- set system_message = "" %}\n{%- endif %}\n\n{#- Find out if there are any images #}\n{% set image_ns = namespace(has_images=false) %} \n{%- for message in messages %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {%- set image_ns.has_images = true %}\n {%- endif %}\n {%- endfor %}\n{%- endfor %}\n\n{#- Error out if there are images and system message #}\n{%- if image_ns.has_images and not system_message == "" %}\n {{- raise_exception("Prompting with images is incompatible with system messages.") }}\n{%- endif %}\n\n{#- System message if there are no images #}\n{%- if not image_ns.has_images %}\n {{- "<|start_header_id|>system<|end_header_id|>\\n\\n" }}\n {%- if tools is not none %}\n {{- "Environment: ipython\\n" }}\n {%- endif %}\n {{- "Cutting Knowledge Date: December 2023\\n" }}\n {{- "Today Date: " + date_string + "\\n\\n" }}\n {%- if tools is not none and not tools_in_user_message %}\n {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- "<|eot_id|>" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception("Cannot put tools in the first user message when there\'s no first user message!") }}\n{%- endif %}\n {{- \'<|start_header_id|>user<|end_header_id|>\\n\\n\' -}}\n {{- "Given the following functions, please respond with a JSON for a function call " }}\n {{- "with its proper arguments that best answers the given prompt.\\n\\n" }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {{- first_user_message + "<|eot_id|>"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == \'ipython\' or message.role == \'tool\' or \'tool_calls\' in message) %}\n {{- \'<|start_header_id|>\' + message[\'role\'] + \'<|end_header_id|>\\n\\n\' }}\n {%- if message[\'content\'] is string %}\n {{- message[\'content\'] }}\n {%- else %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {{- \'<|image|>\' }}\n {%- elif content[\'type\'] == \'text\' %}\n {{- content[\'text\'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \'<|eot_id|>\' }}\n {%- elif \'tool_calls\' in message %}\n {%- if not message.tool_calls|length == 1 %}\n {{- raise_exception("This model only supports single tool-calls at once!") }}\n {%- endif %}\n {%- set tool_call = message.tool_calls[0].function %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' -}}\n {{- \'{"name": "\' + tool_call.name + \'", \' }}\n {{- \'"parameters": \' }}\n {{- tool_call.arguments | tojson }}\n {{- "}" }}\n {{- "<|eot_id|>" }}\n {%- elif message.role == "tool" or message.role == "ipython" %}\n {{- "<|start_header_id|>ipython<|end_header_id|>\\n\\n" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- "<|eot_id|>" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' }}\n{%- endif %}\n', "llama4": "{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now(\"%d %b %Y\") %}\n {%- else %}\n {%- set date_string = \"26 Jul 2024\" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0]['role'] == 'system' %} \n {%- if messages[0]['content'] is string %}\n {%- set system_message = messages[0]['content']|trim %}\n {%- else %}\n {#- FIXME: The processor requires an array, always. #}\n {%- set system_message = messages[0]['content'][0]['text']|trim %}\n {%- endif %}\n {%- set messages = messages[1:] %}\n {%- set user_supplied_system_message = true %}\n{%- else %}\n {%- set system_message = \"\" %}\n {%- set user_supplied_system_message = false %}\n{%- endif %}\n\n{#- System message if the user supplied one #}\n{%- if user_supplied_system_message %}\n {{- \"<|header_start|>system<|header_end|>\\n\\n\" }}\n {%- if tools is not none %}\n {{- \"Environment: ipython\\n\" }}\n {%- endif %}\n {%- if tools is not none and not tools_in_user_message %}\n {{- \"You have access to the following functions. To call a function, please respond with JSON for a function call.\" }}\n {{- 'Respond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}.' }}\n {{- \"Do not use variables.\\n\\n\" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- \"\\n\\n\" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- \"<|eot|>\" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0]['content']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception(\"Cannot put tools in the first user message when there's no first user message!\") }}\n{%- endif %}\n {{- '<|header_start|>user<|header_end|>\\n\\n' -}}\n {{- \"Given the following functions, please respond with a JSON for a function call \" }}\n {{- \"with its proper arguments that best answers the given prompt.\\n\\n\" }}\n {{- 'Respond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}.' }}\n {{- \"Do not use variables.\\n\\n\" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- \"\\n\\n\" }}\n {%- endfor %}\n {{- first_user_message + \"<|eot|>\"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}\n {{- '<|header_start|>' + message['role'] + '<|header_end|>\\n\\n' }}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- else %}\n {%- for content in message['content'] %}\n {%- if content['type'] == 'image' %}\n {{- '<|image|>' }}\n {%- elif content['type'] == 'text' %}\n {{- content['text'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \"<|eot|>\" }}\n {%- elif 'tool_calls' in message and message.tool_calls|length > 0 %}\n {{- '<|header_start|>assistant<|header_end|>\\n\\n' -}}\n {{- '<|python_start|>' }}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- else %}\n {%- for content in message['content'] %}\n {%- if content['type'] == 'image' %}\n {{- '<|image|>' }}\n {%- elif content['type'] == 'text' %}\n {{- content['text'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '<|python_end|>' }}\n {%- for tool_call in message.tool_calls %}\n {{- '{\"name\": \"' + tool_call.function.name + '\", ' }}\n {{- '\"parameters\": ' }}\n {{- tool_call.function.arguments | tojson }}\n {{- \"}\" }}\n {%- endfor %}\n {{- \"<|eot|>\" }}\n {%- elif message.role == \"tool\" or message.role == \"ipython\" %}\n {{- \"<|header_start|>ipython<|header_end|>\\n\\n\" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- \"<|eot|>\" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|header_start|>assistant<|header_end|>\\n\\n' }}\n{%- endif %}\n", "llava": "{% for message in messages %}{% if message['role'] != 'system' %}{{ message['role'].upper() + ': '}}{% endif %}{# Render all images first #}{% for content in message['content'] | selectattr('type', 'equalto', 'image') %}{{ '\n' }}{% endfor %}{# Render all text next #}{% if message['role'] != 'assistant' %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{{ content['text'] + ' '}}{% endfor %}{% else %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{% generation %}{{ content['text'] + ' '}}{% endgeneration %}{% endfor %}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ 'ASSISTANT:' }}{% endif %}", diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index d56d7534a6..d8d9f2834c 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -26,7 +26,6 @@ class ChatTemplate(str, Enum): gemma = "gemma" # pylint: disable=invalid-name cohere = "cohere" # pylint: disable=invalid-name llama3 = "llama3" # pylint: disable=invalid-name - llama4 = "llama4" # pylint: disable=invalid-name llama3_2_vision = "llama3_2_vision" # pylint: disable=invalid-name llama4 = "llama4" # pylint: disable=invalid-name phi_3 = "phi_3" # pylint: disable=invalid-name From 31498d02301a35990557f37e2c56c9d7a8933cba Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 7 Apr 2025 23:40:32 +0700 Subject: [PATCH 0532/1405] fix(doc): clarify roles mapping in chat_template (#2490) [skip ci] --- docs/config.qmd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/config.qmd b/docs/config.qmd index 6414b63a5a..0e49d1992b 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -165,7 +165,9 @@ datasets: content: value # ... - # Optional[Dict[str, List]]. Roles mapping in the messages. The default is: + # Optional[Dict[str, List]]. Roles mapping in the messages. + # The format is {target_role: [source_roles]}. All source roles will be mapped to the target role. + # The default is: roles: user: ["human", "user"] assistant: ["gpt", "assistant"] From 9b89591eadf62b9ab6688a7cc41c0bb168dde773 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 7 Apr 2025 23:41:13 +0700 Subject: [PATCH 0533/1405] Feat: Add doc on loading datasets and support for Azure/OCI (#2482) * fix: remove unused config * feat: add doc on dataset loading * feat: enable azure and oci remote file system * feat: add adlfs and ocifs to requirements * fix: add links between dataset formats and dataset loading * fix: remove unused condition * Revert "fix: remove unused condition" This reverts commit 5fe13be73e49d34afdcae072ac4b8f04bba4079f. --- _quarto.yml | 1 + docs/config.qmd | 2 +- docs/dataset-formats/index.qmd | 7 + docs/dataset_loading.qmd | 276 ++++++++++++++++++++++++++ requirements.txt | 3 +- src/axolotl/utils/data/shared.py | 69 ++++--- src/axolotl/utils/schemas/datasets.py | 1 - 7 files changed, 328 insertions(+), 31 deletions(-) create mode 100644 docs/dataset_loading.qmd diff --git a/_quarto.yml b/_quarto.yml index cf85da473a..17e121c158 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -231,6 +231,7 @@ website: - docs/reward_modelling.qmd - docs/lr_groups.qmd - docs/lora_optims.qmd + - docs/dataset_loading.qmd - section: "Core Concepts" contents: diff --git a/docs/config.qmd b/docs/config.qmd index 0e49d1992b..4099730c5d 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -109,7 +109,7 @@ datasets: preprocess_shards: # Optional[int] process dataset in N sequential chunks for memory efficiency (exclusive with `shards`) name: # Optional[str] name of dataset configuration to load - train_on_split: train # Optional[str] name of dataset split to load from + split: train # Optional[str] name of dataset split to load from revision: # Optional[str] The specific revision of the dataset to use when loading from the Hugging Face Hub. This can be a commit hash, tag, or branch name. If not specified, the latest version will be used. This parameter is ignored for local datasets. trust_remote_code: # Optional[bool] Trust remote code for untrusted source diff --git a/docs/dataset-formats/index.qmd b/docs/dataset-formats/index.qmd index 121341e552..a071f1d56d 100644 --- a/docs/dataset-formats/index.qmd +++ b/docs/dataset-formats/index.qmd @@ -13,6 +13,13 @@ As there are a lot of available options in Axolotl, this guide aims to provide a Axolotl supports 3 kinds of training methods: pre-training, supervised fine-tuning, and preference-based post-training (e.g. DPO, ORPO, PRMs). Each method has their own dataset format which are described below. +::: {.callout-tip} + +This guide will mainly use JSONL as an introduction. Please refer to the [dataset loading docs](../dataset_loading.qmd) to understand how to load datasets from other sources. + +For `pretraining_dataset:` specifically, please refer to the [Pre-training section](#pre-training). +::: + ## Pre-training When aiming to train on large corpora of text datasets, pre-training is your go-to choice. Due to the size of these datasets, downloading the entire-datasets before beginning training would be prohibitively time-consuming. Axolotl supports [streaming](https://huggingface.co/docs/datasets/en/stream) to only load batches into memory at a time. diff --git a/docs/dataset_loading.qmd b/docs/dataset_loading.qmd new file mode 100644 index 0000000000..09c8b0098e --- /dev/null +++ b/docs/dataset_loading.qmd @@ -0,0 +1,276 @@ +--- +title: Dataset Loading +description: Understanding how to load datasets from different sources +back-to-top-navigation: true +toc: true +toc-depth: 5 +--- + +## Overview + +Datasets can be loaded in a number of different ways depending on the how it is saved (the extension of the file) and where it is stored. + +## Loading Datasets + +We use the `datasets` library to load datasets and a mix of `load_dataset` and `load_from_disk` to load them. + +You may recognize the similar named configs between `load_dataset` and the `datasets` section of the config file. + +```yaml +datasets: + - path: + name: + data_files: + split: + revision: + trust_remote_code: +``` + +::: {.callout-tip} + +Do not feel overwhelmed by the number of options here. A lot of them are optional. In fact, the most common config to use would be `path` and sometimes `data_files`. + +::: + +This matches the API of [`datasets.load_dataset`](https://github.com/huggingface/datasets/blob/0b5998ac62f08e358f8dcc17ec6e2f2a5e9450b6/src/datasets/load.py#L1838-L1858), so if you're familiar with that, you will feel right at home. + +For HuggingFace's guide to load different dataset types, see [here](https://huggingface.co/docs/datasets/loading). + +For full details on the config, see [config.qmd](config.qmd). + +::: {.callout-note} + +You can set multiple datasets in the config file by more than one entry under `datasets`. + +```yaml +datasets: + - path: /path/to/your/dataset + - path: /path/to/your/other/dataset +``` + +::: + +### Local dataset + +#### Files + +Usually, to load a JSON file, you would do something like this: + +```python +from datasets import load_dataset + +dataset = load_dataset("json", data_files="data.json") +``` + +Which translates to the following config: + +```yaml +datasets: + - path: json + data_files: /path/to/your/file.jsonl +``` + +However, to make things easier, we have added a few shortcuts for loading local dataset files. + +You can just point the `path` to the file or directory along with the `ds_type` to load the dataset. The below example shows for a JSON file: + +```yaml +datasets: + - path: /path/to/your/file.jsonl + ds_type: json +``` + +This works for CSV, JSON, Parquet, and Arrow files. + +::: {.callout-tip} + +If `path` points to a file and `ds_type` is not specified, we will automatically infer the dataset type from the file extension, so you could omit `ds_type` if you'd like. + +::: + +#### Directory + +If you're loading a directory, you can point the `path` to the directory. + +Then, you have two options: + +##### Loading entire directory + +You do not need any additional configs. + +We will attempt to load in the following order: +- datasets saved with `datasets.save_to_disk` +- loading entire directory of files (such as with parquet/arrow files) + +```yaml +datasets: + - path: /path/to/your/directory +``` + +##### Loading specific files in directory + +Provide `data_files` with a list of files to load. + +```yaml +datasets: + # single file + - path: /path/to/your/directory + ds_type: csv + data_files: file1.csv + + # multiple files + - path: /path/to/your/directory + ds_type: json + data_files: + - file1.jsonl + - file2.jsonl + + # multiple files for parquet + - path: /path/to/your/directory + ds_type: parquet + data_files: + - file1.parquet + - file2.parquet + +``` + +### HuggingFace Hub + +The method you use to load the dataset depends on how the dataset was created, whether a folder was uploaded directly or a HuggingFace Dataset was pushed. + +::: {.callout-note} + +If you're using a private dataset, you will need to enable the `hf_use_auth_token` flag in the root-level of the config file. + +::: + +#### Folder uploaded + +This would mean that the dataset is a single file or file(s) uploaded to the Hub. + +```yaml +datasets: + - path: org/dataset-name + data_files: + - file1.jsonl + - file2.jsonl +``` + +#### HuggingFace Dataset + +This means that the dataset is created as a HuggingFace Dataset and pushed to the Hub via `datasets.push_to_hub`. + +```yaml +datasets: + - path: org/dataset-name +``` + +::: {.callout-note} + +There are some other configs which may be required like `name`, `split`, `revision`, `trust_remote_code`, etc depending on the dataset. + +::: + +### Remote Filesystems + +Via the `storage_options` config under `load_dataset`, you can load datasets from remote filesystems like S3, GCS, Azure, and OCI. + +::: {.callout-warning} + +This is currently experimental. Please let us know if you run into any issues! + +::: + +The only difference between the providers is that you need to prepend the path with the respective protocols. + +```yaml +datasets: + # Single file + - path: s3://bucket-name/path/to/your/file.jsonl + + # Directory + - path: s3://bucket-name/path/to/your/directory +``` + +For directory, we load via `load_from_disk`. + +#### S3 + +Prepend the path with `s3://`. + +The credentials are pulled in the following order: + +- `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` environment variables +- from the `~/.aws/credentials` file +- for nodes on EC2, the IAM metadata provider + +::: {.callout-note} + +We assume you have credentials setup and not using anonymous access. If you want to use anonymous access, let us know! We may have to open a config option for this. + +::: + +Other environment variables that can be set can be found in [boto3 docs](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#using-environment-variables) + +#### GCS + +Prepend the path with `gs://` or `gcs://`. + +The credentials are loaded in the following order: + +- gcloud credentials +- for nodes on GCP, the google metadata service +- anonymous access + +#### Azure + +##### Gen 1 + +Prepend the path with `adl://`. + +Ensure you have the following environment variables set: + +- `AZURE_STORAGE_TENANT_ID` +- `AZURE_STORAGE_CLIENT_ID` +- `AZURE_STORAGE_CLIENT_SECRET` + +##### Gen 2 + +Prepend the path with `abfs://` or `az://`. + +Ensure you have the following environment variables set: + +- `AZURE_STORAGE_ACCOUNT_NAME` +- `AZURE_STORAGE_ACCOUNT_KEY` + +Other environment variables that can be set can be found in [adlfs docs](https://github.com/fsspec/adlfs?tab=readme-ov-file#setting-credentials) + +#### OCI + +Prepend the path with `oci://`. + +It would attempt to read in the following order: + +- `OCIFS_IAM_TYPE`, `OCIFS_CONFIG_LOCATION`, and `OCIFS_CONFIG_PROFILE` environment variables +- when on OCI resource, resource principal + +Other environment variables: + +- `OCI_REGION_METADATA` + +Please see the [ocifs docs](https://ocifs.readthedocs.io/en/latest/getting-connected.html#Using-Environment-Variables). + +### HTTPS + +The path should start with `https://`. + +```yaml +datasets: + - path: https://path/to/your/dataset/file.jsonl +``` + +This must be publically accessible. + +## Next steps + +Now that you know how to load datasets, you can learn more on how to load your specific dataset format into your target output format [dataset formats docs](dataset-formats). diff --git a/requirements.txt b/requirements.txt index f2b2df5fb0..3a839d8a9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -49,7 +49,8 @@ python-dotenv==1.0.1 # remote filesystems s3fs>=2024.5.0 gcsfs>=2024.5.0 -# adlfs +adlfs>=2024.5.0 +ocifs==1.3.2 zstandard==0.22.0 fastcore diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 1bb83efd5c..e657262b9a 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -96,20 +96,17 @@ def load_dataset_w_config( pass ds_from_cloud = False - storage_options = {} + storage_options: dict = {} remote_file_system = None if config_dataset.path.startswith("s3://"): try: - import aiobotocore.session # type: ignore import s3fs # type: ignore except ImportError as exc: - raise ImportError( - "s3:// paths require aiobotocore and s3fs to be installed" - ) from exc + raise ImportError("s3:// paths require s3fs to be installed") from exc - # Takes credentials from ~/.aws/credentials for default profile - s3_session = aiobotocore.session.AioSession(profile="default") - storage_options = {"session": s3_session} + # Reads env, credentials from ~/.aws/credentials, or IAM metadata provider + # https://s3fs.readthedocs.io/en/latest/index.html?highlight=storage_options#credentials + storage_options = {"anon": False} remote_file_system = s3fs.S3FileSystem(**storage_options) elif config_dataset.path.startswith("gs://") or config_dataset.path.startswith( "gcs://" @@ -125,28 +122,44 @@ def load_dataset_w_config( # https://gcsfs.readthedocs.io/en/latest/#credentials storage_options = {"token": None} remote_file_system = gcsfs.GCSFileSystem(**storage_options) - # TODO: Figure out how to get auth creds passed - # elif config_dataset.path.startswith("adl://") or config_dataset.path.startswith("abfs://"): - # try: - # import adlfs - # except ImportError as exc: - # raise ImportError( - # "adl:// or abfs:// paths require adlfs to be installed" - # ) from exc + elif ( + config_dataset.path.startswith("adl://") + or config_dataset.path.startswith("abfs://") + or config_dataset.path.startswith("az://") + ): + try: + import adlfs + except ImportError as exc: + raise ImportError( + "adl:// or abfs:// paths require adlfs to be installed" + ) from exc + + # # Ensure you have the following environment variables set: + # # Gen 1 + # storage_options = { + # "tenant_id": AZURE_STORAGE_TENANT_ID, + # "client_id": AZURE_STORAGE_CLIENT_ID, + # "client_secret": AZURE_STORAGE_CLIENT_SECRET, + # } + # # Gen 2 + # storage_options = { + # "account_name": AZURE_STORAGE_ACCOUNT_NAME, + # "account_key": AZURE_STORAGE_ACCOUNT_KEY, + # } + + # Reads env + # https://github.com/fsspec/adlfs?tab=readme-ov-file#setting-credentials + storage_options = {"anon": False} + remote_file_system = adlfs.AzureBlobFileSystem(**storage_options) + elif config_dataset.path.startswith("oci://"): + try: + import ocifs + except ImportError as exc: + raise ImportError("oci:// paths require ocifs to be installed") from exc - # # Gen 1 - # storage_options = { - # "tenant_id": TENANT_ID, - # "client_id": CLIENT_ID, - # "client_secret": CLIENT_SECRET, - # } - # # Gen 2 - # storage_options = { - # "account_name": ACCOUNT_NAME, - # "account_key": ACCOUNT_KEY, - # } + # https://ocifs.readthedocs.io/en/latest/getting-connected.html#Using-Environment-Variables + remote_file_system = ocifs.OCIFileSystem(**storage_options) - # remote_file_system = adlfs.AzureBlobFileSystem(**storage_options) try: if remote_file_system and remote_file_system.exists(config_dataset.path): ds_from_cloud = True diff --git a/src/axolotl/utils/schemas/datasets.py b/src/axolotl/utils/schemas/datasets.py index 57de71da2b..f9b694da13 100644 --- a/src/axolotl/utils/schemas/datasets.py +++ b/src/axolotl/utils/schemas/datasets.py @@ -39,7 +39,6 @@ class SFTDataset(BaseModel): input_format: str | None = None name: str | None = None ds_type: str | None = None - train_on_split: str | None = None field: str | None = None field_human: str | None = None field_model: str | None = None From 59cd4725040e4a9d2b465c4cc4582d9870e914c4 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 7 Apr 2025 14:47:57 -0400 Subject: [PATCH 0534/1405] SP cu_seqlens fix, refactor (#2495) * working on masking fix * refactor and fix multipack seqlens * pre-commit fix * adding smoke test * using existing packed seqlens util * log warning re: logged losses / gradient scaling per rank --- src/axolotl/core/trainers/base.py | 3 + .../core/trainers/mixins/sequence_parallel.py | 95 +------------------ .../monkeypatch/attention/ring_attn.py | 26 +++++ src/axolotl/monkeypatch/utils.py | 4 +- src/axolotl/utils/collators/batching.py | 67 +++---------- src/axolotl/utils/schemas/config.py | 18 ++++ tests/e2e/multigpu/test_sp.py | 87 +++++++++++++++++ tests/e2e/patched/test_sp.py | 28 ------ 8 files changed, 150 insertions(+), 178 deletions(-) create mode 100644 tests/e2e/multigpu/test_sp.py diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index bc3a200d4a..fd72cd6dbc 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -235,6 +235,9 @@ def _prepare_dataloader( self.accelerator.even_batches = False # Return unprepared dataloader if using sequence parallelism + # TODO(djsaunde): We might be able to use `accelerate`'s dataloader preparation + # if we use `dispatch_batches` and `slice_fn_for_dispatch` properly (i.e., + # slice each batch along the sequence dimension). if self.args.sequence_parallel_degree > 1: return dataloader diff --git a/src/axolotl/core/trainers/mixins/sequence_parallel.py b/src/axolotl/core/trainers/mixins/sequence_parallel.py index 9bcd5db578..3930c6cb3c 100644 --- a/src/axolotl/core/trainers/mixins/sequence_parallel.py +++ b/src/axolotl/core/trainers/mixins/sequence_parallel.py @@ -1,34 +1,22 @@ """Module for Axolotl trainer sequence parallelism mixin""" import logging -from typing import Any -import torch import torch.distributed as dist -import torch.nn.functional as F from datasets import Dataset -from torch import nn from torch.utils.data import DistributedSampler, Sampler from axolotl.monkeypatch.attention.ring_attn import get_ring_attn_group LOG = logging.getLogger(__name__) -try: - from ring_flash_attn import update_ring_flash_attn_params -except ImportError: - # We pass silently here, but raise an ImportError in our Axolotl config validation - # if cfg.sequence_parallel_degree > 1 and `ring-flash-attn` is not installed. - pass - class SequenceParallelMixin: """ Mixin class for sequence parallelism support in trainers. This mixin provides functionality for handling sequence parallelism, - including creating appropriate samplers, managing data partitioning, - and updating ring flash attention parameters during training. + specifically for creating appropriate data samplers. """ args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] @@ -99,84 +87,3 @@ def _sp_get_eval_sampler(self, eval_dataset) -> Sampler | None: return self._create_sequence_parallel_sampler( eval_dataset, shuffle=False, is_eval=True ) - - def _update_ring_flash_attn_params(self, inputs: dict[str, torch.Tensor | Any]): - """ - Calculate the cu_seqlens for the current forward pass and pass the value to - the substituted ring_flash_attn. This is accomplished by using the passed - `input_ids`. - - Args: - inputs: Current batch of inputs. - """ - # At this point, inputs should already be partitioned by the sequence - # parallel data collator - batch_size = inputs["input_ids"].shape[0] - seq_len = inputs["input_ids"].shape[1] - packed_seq_lens = [seq_len] * batch_size - - # Calculate the full sequence length across all GPUs in this SP group - total_seq_len = seq_len * self.args.sequence_parallel_degree - - cu_seqlens = torch.cumsum( - torch.tensor( - packed_seq_lens, device=torch.cuda.current_device(), dtype=torch.int32 - ), - dim=-1, - dtype=torch.int32, - ) - cu_seqlens = F.pad( - F.pad(cu_seqlens, (1, 0), value=0), (0, 1), value=total_seq_len - ) - - update_ring_flash_attn_params(cu_seqlens, self.ring_attn_group) - - def training_step( - self, - model: nn.Module, - inputs: dict[str, torch.Tensor | Any], - num_items_in_batch: int | None = None, - ) -> torch.Tensor: - """ - Perform a training step on a batch of inputs. Overrides the - `transformers.trainer.Trainer` method to handle sequence parallelism if - enabled. - - Args: - model: Model to perform training step for. - inputs: Dictionary mapping. - """ - # Set up sequence parallelism for this step if enabled - if self.args.sequence_parallel_degree > 1: - self._update_ring_flash_attn_params(inputs) - - # Proceed with normal training step - return super().training_step(model, inputs, num_items_in_batch) # type: ignore - - def prediction_step( - self, - model: nn.Module, - inputs: dict[str, torch.Tensor | Any], - prediction_loss_only: bool, - ignore_keys: list[str] | None = None, - ) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]: - """ - Perform a prediction step on a batch of inputs. Overrides the - `transformers.trainer.Trainer` method to handle sequence parallelism if - enabled. - - Args: - model: Model to perform prediction step for. - inputs: Dictionary mapping of inputs. - prediction_loss_only: Whether to return only the loss. - ignore_keys: Keys to ignore in the inputs. - - Returns: - Tuple of (loss, logits, labels). - """ - # Set up sequence parallelism for this prediction step if enabled - if self.args.sequence_parallel_degree > 1: - self._update_ring_flash_attn_params(inputs) - - # Proceed with normal prediction step - return super().prediction_step(model, inputs, prediction_loss_only, ignore_keys) # type: ignore diff --git a/src/axolotl/monkeypatch/attention/ring_attn.py b/src/axolotl/monkeypatch/attention/ring_attn.py index 6c9d0b429e..30aa78f011 100644 --- a/src/axolotl/monkeypatch/attention/ring_attn.py +++ b/src/axolotl/monkeypatch/attention/ring_attn.py @@ -6,10 +6,12 @@ their sequence parallel version of Flash Attention 2. """ +import torch import torch.distributed as dist from accelerate.logging import get_logger from axolotl.logging_config import configure_logging +from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids configure_logging() LOG = get_logger(__name__) @@ -98,3 +100,27 @@ def register_ring_attn(sequence_parallel_degree: int, heads_k_stride: int | None substitute_hf_flash_attn( process_group=get_ring_attn_group(), heads_k_stride=heads_k_stride ) + + +def update_ring_attn_params(batch: dict[str, torch.Tensor]): + """ + Calculate the cumulative sequence lengths for the current forward pass and pass the + value to the substituted `ring_flash_attn`. + + Args: + batch: A dictionary with a batch of data. May or may not contain `position_ids` + data; if not, we compute it. + """ + from ring_flash_attn import update_ring_flash_attn_params + + input_ids = batch["input_ids"] + position_ids = batch.get("position_ids") + if position_ids is None: + seq_len = input_ids.shape[1] + position_ids = torch.arange( + 0, seq_len, dtype=torch.long, device=input_ids.device + ).unsqueeze(0) + + cu_seqlens, _ = get_cu_seqlens_from_pos_ids(position_ids) + cu_seqlens = cu_seqlens.squeeze().to(device=torch.cuda.current_device()) + update_ring_flash_attn_params(cu_seqlens, get_ring_attn_group()) diff --git a/src/axolotl/monkeypatch/utils.py b/src/axolotl/monkeypatch/utils.py index 43496c7c88..4c6a4de11c 100644 --- a/src/axolotl/monkeypatch/utils.py +++ b/src/axolotl/monkeypatch/utils.py @@ -96,7 +96,9 @@ def get_cu_seqlens(attn_mask): return torch.stack(results).to(dtype=torch.int32), torch.stack(max_seq_lens) -def get_cu_seqlens_from_pos_ids(position_ids): +def get_cu_seqlens_from_pos_ids( + position_ids: torch.Tensor, +) -> tuple[torch.Tensor, torch.Tensor]: """generate a cumulative sequence length mask for flash attention using pos ids""" if len(position_ids.shape) == 1: position_ids = position_ids.unsqueeze(0) diff --git a/src/axolotl/utils/collators/batching.py b/src/axolotl/utils/collators/batching.py index 33bb4b4cc0..ed445ae565 100644 --- a/src/axolotl/utils/collators/batching.py +++ b/src/axolotl/utils/collators/batching.py @@ -3,7 +3,6 @@ includes logic for handling sequence parallelism collation. """ -import logging from dataclasses import dataclass from typing import Any, Optional, Union @@ -13,46 +12,7 @@ from transformers import PreTrainedTokenizerBase from transformers.utils import PaddingStrategy -logger = logging.getLogger(__name__) - - -def adjust_position_ids_for_slice( - position_ids: torch.Tensor, start_idx: int -) -> torch.Tensor: - """ - Adjust position IDs for a sliced sequence to maintain proper relative positions. - This handles the case where position IDs might not be contiguous due to sample - packing. - """ - # Convert to tensor if not already - # Find the boundaries between samples (where position_ids reset) - adjusted_pos_ids = position_ids.clone() - - # Process each sequence in the batch - for i in range(position_ids.shape[0]): - seq = position_ids[i] - - # Find sample boundaries - boundaries = [] - for j in range(1, len(seq)): - if seq[j] < seq[j - 1]: - boundaries.append(j) - - # No need to adjust if there are no boundaries or this is a single sample - if not boundaries: - adjusted_pos_ids[i] = seq - start_idx - continue - - # Adjust each segment separately - prev_boundary = 0 - for boundary in boundaries: - adjusted_pos_ids[i, prev_boundary:boundary] -= start_idx - prev_boundary = boundary - - # Last segment - adjusted_pos_ids[i, prev_boundary:] -= start_idx - - return adjusted_pos_ids +from axolotl.monkeypatch.attention.ring_attn import update_ring_attn_params @dataclass @@ -196,23 +156,20 @@ def apply_sequence_parallelism( Returns: Sliced batch dictionary. """ - keys_to_slice = ["input_ids", "attention_mask", "labels", "position_ids"] + # Get local (start, end) for sequence parallelism slicing + total_seq_len = batch["input_ids"].shape[1] + slice_size = total_seq_len // self.local_world_size + start = self.local_rank * slice_size + end = start + slice_size + + # Update params for ring attention calculation + update_ring_attn_params(batch=batch) + # Slice batch for sequence parallel processing + keys_to_slice = ["input_ids", "attention_mask", "labels", "position_ids"] for key in keys_to_slice: if key in batch: - seq_len = batch[key].shape[1] - slice_size = seq_len // self.local_world_size - start_idx = self.local_rank * slice_size - end_idx = ( - start_idx + slice_size - if self.local_rank < self.local_world_size - 1 - else seq_len - ) - batch[key] = batch[key][:, start_idx:end_idx] - - # Special handling for position_ids - if key == "position_ids" and self.local_rank > 0: - batch[key] = adjust_position_ids_for_slice(batch[key], start_idx) + batch[key] = batch[key][:, start:end] return batch diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 0f9a3a1f97..4083fcc22a 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1156,6 +1156,12 @@ def check_sequence_parallel_degree(cls, value, info): "flash_attention: true must be set with sequence_parallel_degree > 1" ) + if not info.data["micro_batch_size"] == 1: + raise ValueError( + "micro_batch_size must be set to 1 " + "due to a `ring-flash-attn` requirement" + ) + try: import ring_flash_attn # noqa: F401 # pylint:disable=unused-import except ImportError as exception: @@ -1165,6 +1171,18 @@ def check_sequence_parallel_degree(cls, value, info): "or `pip install ring-flash-attn>=0.1.4`." ) from exception + # TODO: monkeypatch / callback to average losses correctly across SP ranks + # / fix gradient scaling across SP ranks. Losses, grads should be scaled + # according to the proportion of non-padding tokens per rank. + LOG.warning( + "Sequence parallelism (SP) is enabled with " + f"sequence_parallel_degree={value}. Please note that logged losses may " + "differ slightly to the non-SP losses due to transformers Trainer " + "implementation details. Please see " + "https://github.com/axolotl-ai-cloud/axolotl/pull/2495#issuecomment-2784022042 " + "for more details." + ) + return value @model_validator(mode="before") diff --git a/tests/e2e/multigpu/test_sp.py b/tests/e2e/multigpu/test_sp.py new file mode 100644 index 0000000000..2bd10beb51 --- /dev/null +++ b/tests/e2e/multigpu/test_sp.py @@ -0,0 +1,87 @@ +"""E2E tests for sequence parallelism""" + +import os +from pathlib import Path + +import yaml +from accelerate.test_utils import execute_subprocess_async +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +from ..utils import check_tensorboard + +os.environ["WANDB_DISABLED"] = "true" + + +class TestSequenceParallelism: + """Test case for training with sequence parallelism enabled""" + + def test_sequence_parallel_training(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "load_in_8bit": False, + "load_in_4bit": True, + "strict": False, + "sequence_len": 2048, + "adapter": "qlora", + "sample_packing": True, + "eval_sample_packing": True, + "pad_to_sequence_len": True, + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "lora_modules_to_save": ["embed_tokens", "lm_head"], + "special_tokens": {"pad_token": "<|endoftext|>"}, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 8, + "micro_batch_size": 1, + "gradient_accumulation_steps": 2, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "loss_watchdog_threshold": 5.0, + "loss_watchdog_patience": 3, + "bf16": "auto", + "warmup_steps": 1, + "saves_per_epoch": 1, + "logging_steps": 1, + "weight_decay": 0.0, + "use_tensorboard": True, + "sequence_parallel_degree": 2, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", + "-m", + "axolotl.cli.train", + str(Path(temp_dir) / "config.yaml"), + ] + ) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.6, "Train Loss is too high" + ) diff --git a/tests/e2e/patched/test_sp.py b/tests/e2e/patched/test_sp.py index 70beb8a543..1361a85221 100644 --- a/tests/e2e/patched/test_sp.py +++ b/tests/e2e/patched/test_sp.py @@ -12,7 +12,6 @@ get_ring_attn_group, set_ring_attn_group, ) -from axolotl.utils.collators.batching import adjust_position_ids_for_slice from axolotl.utils.dict import DictDefault @@ -48,33 +47,6 @@ def fixture_cfg(): return cfg -class TestSequenceParallelHelpers: - """Test helper functions used in sequence parallelism.""" - - def test_adjust_position_ids_for_slice(self, partial_state): - """Test position_ids adjustment for sequence slices.""" - # Create sample position_ids with multiple sequences - position_ids = torch.tensor( - [ - # First sequence with 2 samples - [0, 1, 2, 3, 4, 0, 1, 2, 3], - # Second sequence with 3 samples - [0, 1, 2, 0, 1, 2, 3, 0, 1], - ] - ) - - # Adjust as if this was the second slice (start_idx = 4) - adjusted = adjust_position_ids_for_slice(position_ids, start_idx=4) - - # For first sequence: [0,1,2,3,4,0,1,2,3] -> [-4,-3,-2,-1,0,-4,-3,-2,-1] - # For second sequence: [0,1,2,0,1,2,3,0,1] -> [-4,-3,-2,-4,-3,-2,-1,-4,-3] - expected_first_seq = torch.tensor([0, 1, 2, 3, 4, 0, 1, 2, 3]) - 4 - expected_second_seq = torch.tensor([0, 1, 2, 0, 1, 2, 3, 0, 1]) - 4 - - assert torch.all(adjusted[0] == expected_first_seq) - assert torch.all(adjusted[1] == expected_second_seq) - - class TestRingAttention: """Tests for the ring attention functionality.""" From a6c03217f57b90efd2374500bc88a19fbeeb655f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 8 Apr 2025 04:12:28 +0700 Subject: [PATCH 0535/1405] feat: add llama4 CCE (#2498) * feat: add llama4 CCE * fix: update model support list doc * feat: include llama4_text --- .../integrations/cut_cross_entropy/README.md | 3 + .../cut_cross_entropy/monkeypatch/llama4.py | 414 ++++++++++++++++++ .../cut_cross_entropy/monkeypatch/patch.py | 15 +- 3 files changed, 431 insertions(+), 1 deletion(-) create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 7b428eb58c..29b91bc8d0 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -32,6 +32,9 @@ cut_cross_entropy: true ## Supported Models - llama +- llama4_text +- llama4 +- mllama - phi3 - gemma - gemma2 diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py new file mode 100644 index 0000000000..f08663f996 --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py @@ -0,0 +1,414 @@ +"""Llama4 CCE patch. Adapted from transformers 4.51.0.""" + +# pylint: disable=duplicate-code + +from types import MethodType +from typing import Optional, Tuple, Union + +import torch +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, + apply_lce, +) +from torch import nn +from transformers.cache_utils import Cache +from transformers.modeling_outputs import CausalLMOutputWithPast +from transformers.models.llama4.modeling_llama4 import ( + _CONFIG_FOR_DOC, + LLAMA4_INPUTS_DOCSTRING, + Llama4CausalLMOutputWithPast, +) +from transformers.utils import ( + add_start_docstrings_to_model_forward, + replace_return_docstrings, +) + +_PATCH_OPTS: PatchOptions | None = None + + +@add_start_docstrings_to_model_forward(LLAMA4_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def cce_forward( + self, + input_ids: torch.LongTensor | None = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Union[Cache, list[torch.FloatTensor]]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + defer_logits_calculation: bool = False, + **kwargs, +) -> Union[Tuple, CausalLMOutputWithPast]: + r""" + Args: + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + defer_logits_calculation (`bool`, *optional*, defaults to `False`): + If `True`, defer logits calculation to the ConditionalGeneration forward. This is used to avoid the + memory overhead of calculating logits using regular lm_head forward pass and to use CCE. + + Returns: + + Example: + + ```python + >>> from transformers import AutoTokenizer, Llama4ForCausalLM + + >>> model = Llama4ForCausalLM.from_pretrained("meta-llama4/Llama4-2-7b-hf") + >>> tokenizer = AutoTokenizer.from_pretrained("meta-llama4/Llama4-2-7b-hf") + + >>> prompt = "Hey, are you conscious? Can you talk to me?" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." + ```""" + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs[0] + loss = None + logits = None + + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + loss = apply_lce( + hidden_states[:, slice_indices, :], + self.lm_head.weight, + labels, + _PATCH_OPTS, + **kwargs, + ) + elif _PATCH_OPTS is not None and defer_logits_calculation: + # defer logits calculation to the ConditionalGeneration forward + logits = hidden_states[:, slice_indices, :] + else: + logits = self.lm_head(hidden_states[:, slice_indices, :]) + + if labels is not None: + loss = self.loss_function( + logits=logits, + labels=labels, + vocab_size=self.config.vocab_size, + **kwargs, + ) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +@replace_return_docstrings( + output_type=Llama4CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def cce_forward_multimodal( + self, + input_ids: torch.LongTensor | None = None, + pixel_values: torch.FloatTensor | None = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[list[torch.FloatTensor]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + vision_feature_layer: Optional[Union[int, list[int]]] = None, + vision_feature_select_strategy: Optional[str] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + image_sizes: torch.Tensor | None = None, + **lm_kwargs, +) -> Union[Tuple, Llama4CausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + + Returns: + + Example: + + ```python + >>> from PIL import Image + >>> import requests + >>> from transformers import AutoProcessor, LlavaForConditionalGeneration + + >>> model = LlavaForConditionalGeneration.from_pretrained("llava-hf/llava-1.5-7b-hf") + >>> processor = AutoProcessor.from_pretrained("llava-hf/llava-1.5-7b-hf") + + >>> prompt = "USER: \nWhat's the content of the image? ASSISTANT:" + >>> url = "https://www.ilankelman.org/stopsigns/australia.jpg" + >>> image = Image.open(requests.get(url, stream=True).raw) + + >>> inputs = processor(images=image, text=prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(**inputs, max_new_tokens=15) + >>> processor.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "USER: \nWhat's the content of the image? ASSISTANT: The image features a busy city street with a stop sign prominently displayed" + ```""" + + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + vision_feature_layer = ( + vision_feature_layer + if vision_feature_layer is not None + else self.config.vision_config.vision_feature_layer + ) + vision_feature_select_strategy = ( + vision_feature_select_strategy + if vision_feature_select_strategy is not None + else self.config.vision_config.vision_feature_select_strategy + ) + + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + if pixel_values is not None and inputs_embeds is not None: + raise ValueError( + "You cannot specify both pixel_values and inputs_embeds at the same time, and must specify either one" + ) + + if inputs_embeds is None: + inputs_embeds = self.get_input_embeddings()(input_ids) + + if pixel_values is not None: + image_features = self.get_image_features( + pixel_values=pixel_values, + vision_feature_layer=vision_feature_layer, + vision_feature_select_strategy=vision_feature_select_strategy, + image_sizes=image_sizes, + ) + original_inputs_embeds_shape = inputs_embeds.shape + + vision_flat = image_features.view(-1, image_features.size(-1)) + projected_vision_flat = self.multi_modal_projector(vision_flat) + + special_image_mask = (input_ids == self.config.image_token_index).unsqueeze(-1) + final_mask = special_image_mask.to(inputs_embeds.device) + inputs_embeds = inputs_embeds.view(-1, inputs_embeds.size(-1)) # type: ignore + + final_mask_1d = final_mask[..., 0].reshape(-1) + num_tokens_to_fill = final_mask_1d.sum() + + if num_tokens_to_fill != projected_vision_flat.size(0): + raise ValueError( + f"Mismatch: final_mask wants {num_tokens_to_fill} embeddings, " + f"but multi_modal_projector returned {projected_vision_flat.size(0)}" + ) + + expanded_mask = final_mask_1d.unsqueeze(-1).expand(-1, inputs_embeds.size(-1)) + inputs_embeds = inputs_embeds.masked_scatter( + expanded_mask, projected_vision_flat + ) # type: ignore + inputs_embeds = inputs_embeds.view(original_inputs_embeds_shape) # type: ignore + + outputs = self.language_model( + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + logits_to_keep=logits_to_keep, + defer_logits_calculation=True, # enable deferred logits calculation + **lm_kwargs, + ) + + hidden_states = outputs[0] + loss = None + logits = None + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + # TODO: check if need to handle attention_mask + loss = apply_lce( + hidden_states, + self.language_model.lm_head.weight, + labels, + _PATCH_OPTS, + **lm_kwargs, + ) + else: + logits = hidden_states + if labels is not None: + # Shift so that tokens < n predict n + if attention_mask is not None: + # we use the input attention mask to shift the logits and labels, because it is 2D. + # we also crop attn mask in case it is longer, which happens in PrefixTuning with peft + shift_attention_mask = attention_mask[:, -(logits.shape[1] - 1) :].to( + logits.device + ) + shift_logits = logits[..., :-1, :][ + shift_attention_mask.to(logits.device) != 0 + ].contiguous() + shift_labels = labels[..., 1:][ + shift_attention_mask.to(labels.device) != 0 + ].contiguous() + else: + shift_logits = logits[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + # Flatten the tokens + loss_fct = nn.CrossEntropyLoss() + loss = loss_fct( + shift_logits.view(-1, shift_logits.size(-1)), + shift_labels.view(-1).to(shift_logits.device), + ) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return Llama4CausalLMOutputWithPast( + loss=loss, + logits=logits, # type: ignore # TODO: check if need to create dummy logits + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + image_hidden_states=image_features if pixel_values is not None else None, + ) + + +def patch_llama4_text( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + from transformers.models.llama4 import modeling_llama4 + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_llama4.Llama4ForCausalLM + ), f"Expected a Llama4ForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward, maybe_model) + + return maybe_model + + setattr( + modeling_llama4.Llama4ForCausalLM, + "forward", + cce_forward, + ) + return None + + +def patch_llama4( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + + global _PATCH_OPTS # pylint: disable=global-statement + from transformers.models.llama4 import modeling_llama4 + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_llama4.Llama4ForConditionalGeneration + ), f"Expected a Llama4ForConditionalGeneration model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward_multimodal, maybe_model) + + # patch the language model + maybe_model.language_model.forward = MethodType( + cce_forward, maybe_model.language_model + ) + return maybe_model + + setattr( + modeling_llama4.Llama4ForConditionalGeneration, + "forward", + cce_forward_multimodal, + ) + + # patch the causal language model + setattr(modeling_llama4.Llama4ForCausalLM, "forward", cce_forward) + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py index b9c83ff02e..5263956ce3 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py @@ -20,6 +20,10 @@ patch_gemma3, patch_gemma3_text, ) +from axolotl.integrations.cut_cross_entropy.monkeypatch.llama4 import ( + patch_llama4, + patch_llama4_text, +) from axolotl.integrations.cut_cross_entropy.monkeypatch.mistral3 import ( patch_mistral, patch_mistral3, @@ -28,6 +32,8 @@ CUT_CROSS_ENTROPY_MODEL_MAPPING = { "llama": patch_llama, + "llama4": patch_llama4, + "llama4_text": patch_llama4_text, "mllama": patch_mllama, "phi3": patch_phi3, "gemma": patch_gemma, @@ -60,7 +66,14 @@ def cce_patch( raise ValueError(f"Unknown {impl=}") if isinstance(model_type_or_model, transformers.PreTrainedModel): - model_type = model_type_or_model.config.model_type + if hasattr(model_type_or_model, "config"): + model_type = getattr( + getattr(model_type_or_model, "config", None), "model_type", None + ) + else: + raise ValueError( + "model_type_or_model is a PreTrainedModel but does not have a config attribute" + ) elif isinstance(model_type_or_model, transformers.PretrainedConfig): model_type = model_type_or_model.model_type else: From 0dac2ddeacf06dac8d4fbadcdde3d02d6ef5e2b0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 7 Apr 2025 20:47:00 -0400 Subject: [PATCH 0536/1405] Llama4 linearized (#2502) * llama4 support for linearized experts * clean up fsdp2 sharding to prevent hang * add yaml config * cleanup example [skip ci] --- examples/{llama4 => llama-4}/scout-lora.yaml | 0 examples/llama-4/scout-qlora-fsdp1.yaml | 93 +++++++++ requirements-dev.txt | 2 + .../monkeypatch/accelerate/__init__.py | 0 src/axolotl/monkeypatch/accelerate/fsdp2.py | 63 +++++++ src/axolotl/monkeypatch/lora_kernels.py | 178 +++++++++++------- .../monkeypatch/models/llama4/__init__.py | 0 .../monkeypatch/models/llama4/modeling.py | 101 ++++++++++ src/axolotl/utils/models.py | 12 ++ src/axolotl/utils/schemas/config.py | 2 + 10 files changed, 386 insertions(+), 65 deletions(-) rename examples/{llama4 => llama-4}/scout-lora.yaml (100%) create mode 100644 examples/llama-4/scout-qlora-fsdp1.yaml create mode 100644 src/axolotl/monkeypatch/accelerate/__init__.py create mode 100644 src/axolotl/monkeypatch/accelerate/fsdp2.py create mode 100644 src/axolotl/monkeypatch/models/llama4/__init__.py create mode 100644 src/axolotl/monkeypatch/models/llama4/modeling.py diff --git a/examples/llama4/scout-lora.yaml b/examples/llama-4/scout-lora.yaml similarity index 100% rename from examples/llama4/scout-lora.yaml rename to examples/llama-4/scout-lora.yaml diff --git a/examples/llama-4/scout-qlora-fsdp1.yaml b/examples/llama-4/scout-qlora-fsdp1.yaml new file mode 100644 index 0000000000..ad2e46786e --- /dev/null +++ b/examples/llama-4/scout-qlora-fsdp1.yaml @@ -0,0 +1,93 @@ +base_model: axolotl-quants/Llama-4-Scout-17B-16E-Linearized-bnb-nf4-bf16 +model_type: Llama4ForConditionalGeneration + # Automatically upload checkpoint and final model to HF + # hub_model_id: username/custom_model_name + +strict: false + +# torch_compile: true +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_glu_activation: true +liger_rms_norm: true +liger_layer_norm: true + +llama4_linearized_experts: true +load_in_4bit: true +adapter: qlora +lora_r: 32 +lora_alpha: 64 +lora_target_modules: + - self_attn.q_proj + - self_attn.k_proj + - self_attn.v_proj + - self_attn.o_proj + - shared_expert.gate_proj + - shared_expert.up_proj + - shared_expert.down_proj + # - experts.gate_projs.[0-9]+$ + # - experts.up_projs.[0-9]+$ + # - experts.down_projs.[0-9]+$ +lora_modules_to_save: + - lm_head + - embed_tokens + +chat_template: llama4 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value + +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +sequence_len: 4096 +sample_packing: true +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 2e-5 + +bf16: true +tf32: true + +logging_steps: 1 +flash_attention: true + +warmup_steps: 100 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +fsdp: + - auto_wrap + - full_shard +fsdp_config: + fsdp_transformer_layer_cls_to_wrap: Llama4TextDecoderLayer + fsdp_limit_all_gathers: true + fsdp_sync_module_states: true + fsdp_offload_params: true + fsdp_use_orig_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD + fsdp_activation_checkpointing: true +special_tokens: + pad_token: <|finetune_right_pad_id|> + eos_token: <|eot|> diff --git a/requirements-dev.txt b/requirements-dev.txt index 9f523de54d..1dce5df5f2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,3 +4,5 @@ mypy types-requests quartodoc jupyter +blobfile +tiktoken diff --git a/src/axolotl/monkeypatch/accelerate/__init__.py b/src/axolotl/monkeypatch/accelerate/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py new file mode 100644 index 0000000000..2a5d2151dc --- /dev/null +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -0,0 +1,63 @@ +""" +monkeypatch for accelerate fsdp2 fix when modifying ordereddict during interation +""" + +import logging +import sys + +import torch + +LOG = logging.getLogger(__name__) + + +def fsdp2_load_full_state_dict(accelerator, model: torch.nn.Module, full_sd: dict): + """ + Loads the full state dict (could be only on rank 0) into the sharded model. This is done by broadcasting the + parameters from rank 0 to all other ranks. This function modifies the model in-place. + + Args: + accelerator (`Accelerator`): The accelerator instance + model (`torch.nn.Module`): The model to load the state dict into + full_sd (`dict`): The full state dict to load, can only be on rank 0 + """ + import torch.distributed as dist + from torch.distributed.tensor import distribute_tensor + + LOG.info("Broadcasting full state dict to all ranks...") + sharded_sd = model.state_dict() + param_names = sorted(sharded_sd.keys()) + for param_name in param_names: + mesh = sharded_sd[param_name].device_mesh + if accelerator.is_main_process: + # Use the corresponding tensor from full_sd (assuming the key exists in full_sd) + full_param = full_sd[param_name].detach().cuda() + dist.broadcast(full_param, src=0, group=mesh.get_group()) + sharded_tensor = distribute_tensor( + full_param, mesh, sharded_sd[param_name].placements + ) + sharded_sd[param_name] = sharded_tensor + else: + # Prepare a tensor of matching shape and dtype + full_tensor = torch.empty( + sharded_sd[param_name].size(), + device="cuda", + dtype=sharded_sd[param_name].dtype, + ) + dist.broadcast(full_tensor, src=0, group=mesh.get_group()) + sharded_tensor = distribute_tensor( + full_tensor, mesh, sharded_sd[param_name].placements + ) + sharded_sd[param_name] = sharded_tensor + + model.load_state_dict(sharded_sd) + + +def patch_accelerate_fsdp_utils(): + from accelerate.utils import fsdp_utils + + fsdp_utils.fsdp2_load_full_state_dict = fsdp2_load_full_state_dict + setattr( + sys.modules["accelerate.utils.fsdp_utils"], + "fsdp2_load_full_state_dict", + fsdp2_load_full_state_dict, + ) diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 96cfb1b692..0036fe0039 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -4,7 +4,7 @@ import inspect import logging import types -from typing import Type +from typing import Generator, Tuple, Type import torch from accelerate.logging import get_logger @@ -200,6 +200,46 @@ def patch_self_attn_lora(cfg: DictDefault): ) +def find_self_attn_in_layer( + layer: nn.Module, +) -> Generator[Tuple[nn.Module], None, None]: + # general case of most models + if hasattr(layer, "self_attn"): + if all( + hasattr(layer.self_attn, proj) + for proj in ["q_proj", "k_proj", "v_proj", "o_proj"] + ): + yield layer.self_attn + + +def find_mlp_in_layer( + layer: nn.Module, +) -> Generator[Tuple[nn.Module, nn.Module, nn.Module, nn.Module], None, None]: + # general case of most models + if hasattr(layer, "mlp"): + if all( + hasattr(layer.mlp, proj) for proj in ["gate_proj", "up_proj", "down_proj"] + ): + yield layer.mlp.gate_proj, layer.mlp.up_proj, layer.mlp.down_proj, layer.mlp + # llama4 linearized experts + if hasattr(layer, "feedforward") and hasattr(layer.feedforward, "shared_expert"): + mlp = layer.feedforward.shared_expert + yield mlp.gate_proj, mlp.up_proj, mlp.down_proj, mlp + if hasattr(layer, "feedforward") and hasattr(layer.feedforward, "experts"): + if all( + hasattr(layer.feedforward.experts, proj) + for proj in ["gate_projs", "up_projs", "down_projs"] + ): + for gate_proj, up_proj, down_proj in zip( + layer.feedforward.experts.gate_projs, + layer.feedforward.experts.up_projs, + layer.feedforward.experts.down_projs, + ): + yield gate_proj, up_proj, down_proj, FakeMLP( + gate_proj, up_proj, down_proj + ) + + def apply_lora_kernel_patches( model: PeftModelForCausalLM, cfg: DictDefault ) -> PeftModelForCausalLM: @@ -286,74 +326,82 @@ def apply_lora_kernel_patches( for layer in layers: # Add QKV, O fallback implementations to start # These will be overwritten later (if some conditions apply) - layer.self_attn.apply_qkv = types.MethodType( - original_apply_qkv, layer.self_attn - ) - layer.self_attn.apply_o = types.MethodType(original_apply_o, layer.self_attn) - - if cfg.lora_mlp_kernel: - # MLP patching - gate_proj = layer.mlp.gate_proj - up_proj = layer.mlp.up_proj - down_proj = layer.mlp.down_proj - - can_patch_mlp = all( - hasattr(proj, "lora_A") - and getattr(proj, "base_layer", proj).bias is None - and len(getattr(proj, "lora_magnitude_vector", []) or []) == 0 - for proj in (gate_proj, up_proj, down_proj) - ) - - if can_patch_mlp: - apply_fn = APPLY_FN_MAPPING[activation] - layer.mlp.forward = types.MethodType(apply_fn, layer.mlp) - else: - LOG.warning_once( - "Cannot patch some MLP layers - requires LoRA adapters with no bias" + for self_attn in find_self_attn_in_layer(layer): + self_attn.apply_qkv = types.MethodType(original_apply_qkv, self_attn) + self_attn.apply_o = types.MethodType(original_apply_o, self_attn) + + if cfg.lora_qkv_kernel: + # Query, key, value patching + layer_modules = [ + getattr(self_attn, linear_proj) + for linear_proj in ["q_proj", "k_proj", "v_proj"] + ] + can_patch_qkv = all( + hasattr(module, "lora_A") + and getattr(module, "base_layer", module).bias is None + and len(getattr(module, "lora_magnitude_vector", []) or []) == 0 + for module in layer_modules ) - if cfg.lora_qkv_kernel: - # Query, key, value patching - layer_modules = [ - getattr(layer.self_attn, linear_proj) - for linear_proj in ["q_proj", "k_proj", "v_proj"] - ] - can_patch_qkv = all( - hasattr(module, "lora_A") - and getattr(module, "base_layer", module).bias is None - and len(getattr(module, "lora_magnitude_vector", []) or []) == 0 - for module in layer_modules - ) - - if can_patch_qkv: - # Add optimized implementation - layer.self_attn.apply_qkv = types.MethodType( - apply_lora_qkv, layer.self_attn - ) - else: - LOG.warning_once( - "Cannot patch some attention QKV projections - requires LoRA adapters with no bias" - ) - if cfg.lora_o_kernel: - # Output patching - layer_modules = [ - getattr(layer.self_attn, linear_proj) for linear_proj in ["o_proj"] - ] - can_patch_o = all( - hasattr(module, "lora_A") - and getattr(module, "base_layer", module).bias is None - and len(getattr(module, "lora_magnitude_vector", []) or []) == 0 - for module in layer_modules - ) - - if can_patch_o: - layer.self_attn.apply_o = types.MethodType( - apply_lora_o, layer.self_attn + + if can_patch_qkv: + # Add optimized implementation + self_attn.apply_qkv = types.MethodType(apply_lora_qkv, self_attn) + else: + LOG.warning_once( + "Cannot patch some attention QKV projections - requires LoRA adapters with no bias" + ) + if cfg.lora_o_kernel: + # Output patching + layer_modules = [ + getattr(self_attn, linear_proj) for linear_proj in ["o_proj"] + ] + can_patch_o = all( + hasattr(module, "lora_A") + and getattr(module, "base_layer", module).bias is None + and len(getattr(module, "lora_magnitude_vector", []) or []) == 0 + for module in layer_modules ) - else: - LOG.warning_once( - "Cannot patch some attention output projection - requires LoRA adapters with no bias" + + if can_patch_o: + self_attn.apply_o = types.MethodType(apply_lora_o, self_attn) + else: + LOG.warning_once( + "Cannot patch some attention output projection - requires LoRA adapters with no bias" + ) + for gate_proj, up_proj, down_proj, mlp in find_mlp_in_layer(layer): + if cfg.lora_mlp_kernel: + # MLP patching + can_patch_mlp = all( + hasattr(proj, "lora_A") + and getattr(proj, "base_layer", proj).bias is None + and len(getattr(proj, "lora_magnitude_vector", []) or []) == 0 + for proj in (gate_proj, up_proj, down_proj) ) + if can_patch_mlp: + apply_fn = APPLY_FN_MAPPING[activation] + layer.mlp.forward = types.MethodType(apply_fn, mlp) + else: + LOG.warning_once( + "Cannot patch some MLP layers - requires LoRA adapters with no bias" + ) + LOG.setLevel(original_level) return model + + +class FakeMLP(nn.Module): + """ + placeholder MLP for triton patching + """ + + gate_proj: nn.Linear + up_proj: nn.Linear + down_proj: nn.Linear + + def __init__(self, gate_proj, up_proj, down_proj): + super().__init__() + self.gate_proj = gate_proj + self.up_proj = up_proj + self.down_proj = down_proj diff --git a/src/axolotl/monkeypatch/models/llama4/__init__.py b/src/axolotl/monkeypatch/models/llama4/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/llama4/modeling.py b/src/axolotl/monkeypatch/models/llama4/modeling.py new file mode 100644 index 0000000000..b2a46ab862 --- /dev/null +++ b/src/axolotl/monkeypatch/models/llama4/modeling.py @@ -0,0 +1,101 @@ +""" +Modified Llama-4 text experts modeling for linearized experts for improved LoRA support +""" + +import sys + +import torch +from torch import nn +from transformers import Llama4Config +from transformers.activations import ACT2FN + + +class Llama4TextExperts(nn.Module): + """ + Modified Llama-4 text experts modeling for linearized experts + """ + + def __init__(self, config: Llama4Config): + super().__init__() + self.num_experts = config.num_local_experts + self.intermediate_size = config.intermediate_size + self.hidden_size = config.hidden_size + self.expert_dim = self.intermediate_size + + # Replace fused gate_up_proj with separate Linear modules + self.gate_projs = nn.ModuleList( + [ + nn.Linear(self.hidden_size, self.expert_dim, bias=False) + for _ in range(self.num_experts) + ] + ) + + self.up_projs = nn.ModuleList( + [ + nn.Linear(self.hidden_size, self.expert_dim, bias=False) + for _ in range(self.num_experts) + ] + ) + + # Replace down_proj Parameter with Linear modules + self.down_projs = nn.ModuleList( + [ + nn.Linear(self.expert_dim, self.hidden_size, bias=False) + for _ in range(self.num_experts) + ] + ) + + self.act_fn = ACT2FN[config.hidden_act] + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + """ + Forward method using separate Linear layers for each expert. + + Args: + hidden_states (torch.Tensor): (num_experts * batch_size, hidden_size) + The input should be organized by expert + + Returns: + torch.Tensor: (num_experts * batch_size, hidden_size) + """ + # Reshape to separate by expert + hidden_states = hidden_states.view(self.num_experts, -1, self.hidden_size) + # batch_size_per_expert = hidden_states.size(1) + + # Initialize output tensor + next_states = torch.zeros_like(hidden_states) + + # Process each expert separately + for i in range(self.num_experts): + # Get input for this expert + expert_input = hidden_states[ + i + ] # Shape: (batch_size_per_expert, hidden_size) + + # Apply gate and up projections + gate = self.gate_projs[i]( + expert_input + ) # Shape: (batch_size_per_expert, expert_dim) + up = self.up_projs[i]( + expert_input + ) # Shape: (batch_size_per_expert, expert_dim) + + # Apply activation and down projection + next_states[i] = self.down_projs[i](up * self.act_fn(gate)) + + # Flatten back to original shape + return next_states.view(-1, self.hidden_size) + + +def patch_llama4_linearized_modeling(): + """ + Patch Llama4TextExperts to use separate Linear layers for each expert. + """ + from transformers.models.llama4 import modeling_llama4 + + modeling_llama4.Llama4TextExperts = Llama4TextExperts + setattr( + sys.modules["transformers.models.llama4"], + "Llama4TextExperts", + Llama4TextExperts, + ) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 024673b8ec..f808f4bdd4 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -544,8 +544,20 @@ def __init__( self.auto_model_loader = AutoModelForCausalLM # pylint: disable=invalid-name def apply_patches(self) -> None: + if self.cfg.fsdp_config and str(self.cfg.fsdp_config.fsdp_version) == "2": + from axolotl.monkeypatch.accelerate.fsdp2 import patch_accelerate_fsdp_utils + + patch_accelerate_fsdp_utils() # patch gemma3 conditional generation forward before loading plugins # as it could be overridden by plugins + if self.cfg.model_config_type == "llama4": + if self.cfg.llama4_linearized_experts: + from axolotl.monkeypatch.models.llama4.modeling import ( + patch_llama4_linearized_modeling, + ) + + patch_llama4_linearized_modeling() + if self.cfg.model_config_type == "gemma3": from axolotl.monkeypatch.gemma3 import ( patch_gemma3conditionalgeneration_forward, diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 4083fcc22a..882c9a2486 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -245,6 +245,8 @@ class AxolotlInputConfig( lora_qkv_kernel: bool | None = None lora_o_kernel: bool | None = None + llama4_linearized_experts: bool | None = None + deepspeed: str | dict[str, Any] | None = None fsdp: list[str] | None = None fsdp_config: dict[str, Any] | None = None From bf9efe2a09dc75779f347627083d5640cc295650 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 9 Apr 2025 02:52:45 -0400 Subject: [PATCH 0537/1405] [llama4] fix the mm yaml, add scout single gpu yaml (#2510) * [llama4] fix the mm yaml, add scout single gpu yaml * add README for llama4 * rename to specify fsdp --- examples/llama-4/README.md | 10 +++ examples/llama-4/scout-qlora-single-h100.yaml | 86 +++++++++++++++++++ ...lora.yaml => scout-vision-qlora-fsdp.yaml} | 60 ++++++++----- 3 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 examples/llama-4/README.md create mode 100644 examples/llama-4/scout-qlora-single-h100.yaml rename examples/llama-4/{scout-lora.yaml => scout-vision-qlora-fsdp.yaml} (51%) diff --git a/examples/llama-4/README.md b/examples/llama-4/README.md new file mode 100644 index 0000000000..53448da2b1 --- /dev/null +++ b/examples/llama-4/README.md @@ -0,0 +1,10 @@ +# Llama 4 by Meta AI + +## Available Examples + +### Llama 4 Scout 17Bx16Experts (109B) +- [Multi-Modal/Vision QLoRA w/ FSDP1](./scout-vision-qlora-fsdp.yaml) +- [Text Single GPU (H100) QLoRA](./scout-qlora-single-h100.yaml) +- [Text Multi GPU QLoRA w/ FSDP1](./scout-qlora-fsdp1.yaml) + +Our Single GPU implementation for Llama 4 Scout uses only 68.5GB VRAM for post-training with 4k context length @ 546 tokens/second. diff --git a/examples/llama-4/scout-qlora-single-h100.yaml b/examples/llama-4/scout-qlora-single-h100.yaml new file mode 100644 index 0000000000..23a3a2195c --- /dev/null +++ b/examples/llama-4/scout-qlora-single-h100.yaml @@ -0,0 +1,86 @@ +base_model: axolotl-quants/Llama-4-Scout-17B-16E-Linearized-bnb-nf4-bf16 +model_type: Llama4ForConditionalGeneration +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_glu_activation: true +liger_rms_norm: true +liger_layer_norm: true + +llama4_linearized_experts: true +load_in_4bit: true +adapter: qlora +lora_r: 32 +lora_alpha: 64 +lora_target_modules: + - self_attn.q_proj + - self_attn.k_proj + - self_attn.v_proj + - self_attn.o_proj + - shared_expert.gate_proj + - shared_expert.up_proj + - shared_expert.down_proj + # - experts.gate_projs.[0-9]+$ + # - experts.up_projs.[0-9]+$ + # - experts.down_projs.[0-9]+$ +lora_modules_to_save: + # - lm_head + # - embed_tokens + +lora_mlp_kernel: true +lora_qkv_kernel: true +lora_o_kernel: true + +chat_template: llama4 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value + +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +sequence_len: 4096 # up to 8k will work on a single H100 +sample_packing: true +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 1e-4 + +bf16: true +tf32: true + +logging_steps: 1 +flash_attention: true + +gradient_checkpointing: offload +gradient_checkpointing_kwargs: + use_reentrant: false + +warmup_steps: 20 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: + pad_token: <|finetune_right_pad_id|> + eos_token: <|eot|> diff --git a/examples/llama-4/scout-lora.yaml b/examples/llama-4/scout-vision-qlora-fsdp.yaml similarity index 51% rename from examples/llama-4/scout-lora.yaml rename to examples/llama-4/scout-vision-qlora-fsdp.yaml index 26534b560b..8b8c9abd10 100644 --- a/examples/llama-4/scout-lora.yaml +++ b/examples/llama-4/scout-vision-qlora-fsdp.yaml @@ -1,13 +1,28 @@ -base_model: meta-llama/Llama-4-Scout-17B-16E +base_model: axolotl-quants/Llama-4-Scout-17B-16E-Linearized-bnb-nf4-bf16 model_type: Llama4ForConditionalGeneration +processor_type: Llama4Processor # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name strict: false - # torch_compile: true +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false -adapter: lora +sequence_len: 4096 + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_glu_activation: true +liger_rms_norm: true +liger_layer_norm: true + +llama4_linearized_experts: true # use Axolotl's customized model +load_in_4bit: true +adapter: qlora lora_r: 32 lora_alpha: 64 lora_target_modules: @@ -15,60 +30,59 @@ lora_target_modules: - self_attn.k_proj - self_attn.v_proj - self_attn.o_proj + - shared_expert.gate_proj + - shared_expert.up_proj + - shared_expert.down_proj + - vision_adapter.mlp.fc1 + - vision_adapter.mlp.fc2 + # - experts.gate_projs.[0-9]+$ + # - experts.up_projs.[0-9]+$ + # - experts.down_projs.[0-9]+$ lora_modules_to_save: - lm_head - embed_tokens chat_template: llama4 datasets: - - path: mlabonne/FineTome-100k + - path: HuggingFaceH4/llava-instruct-mix-vsft type: chat_template - split: train[:20%] - field_messages: conversations - message_property_mappings: - role: from - content: value + split: train[:1%] + field_messages: messages dataset_prepared_path: last_run_prepared val_set_size: 0.0 output_dir: ./outputs/out -sequence_len: 4096 -sample_packing: true -pad_to_sequence_len: true - gradient_accumulation_steps: 1 micro_batch_size: 1 num_epochs: 1 -optimizer: adamw_torch_8bit +optimizer: adamw_torch_4bit lr_scheduler: cosine learning_rate: 2e-5 bf16: true tf32: true -# gradient_checkpointing: true -# gradient_checkpointing_kwargs: -# use_reentrant: false logging_steps: 1 flash_attention: true warmup_steps: 100 -evals_per_epoch: 2 +evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 fsdp: - auto_wrap - full_shard fsdp_config: - fsdp_version: 2 - fsdp_offload_params: false + fsdp_transformer_layer_cls_to_wrap: Llama4TextDecoderLayer + fsdp_limit_all_gathers: true + fsdp_sync_module_states: true + fsdp_offload_params: true + fsdp_use_orig_params: false fsdp_cpu_ram_efficient_loading: true fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP - fsdp_transformer_layer_cls_to_wrap: Llama4TextDecoderLayer - fsdp_state_dict_type: SHARDED_STATE_DICT + fsdp_state_dict_type: FULL_STATE_DICT fsdp_sharding_strategy: FULL_SHARD - fsdp_reshard_after_forward: true fsdp_activation_checkpointing: true special_tokens: pad_token: <|finetune_right_pad_id|> From 630e40dd134c2bfd1fb1344e63a5022d2a6685a6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 9 Apr 2025 02:53:00 -0400 Subject: [PATCH 0538/1405] upgrade transformers to 4.51.1 (#2508) * upgrade transformers to 4.51.1 * multigpu longer timeout --- cicd/multigpu.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cicd/multigpu.py b/cicd/multigpu.py index 453e8daee4..66d4d99902 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -68,7 +68,7 @@ def run_cmd(cmd: str, run_folder: str): @app.function( image=cicd_image, gpu=GPU_CONFIG, - timeout=60 * 60, + timeout=90 * 60, cpu=8.0, memory=131072 * N_GPUS, volumes=VOLUME_CONFIG, diff --git a/requirements.txt b/requirements.txt index 3a839d8a9a..e8377b880a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ liger-kernel==0.5.6 packaging==23.2 peft==0.15.1 -transformers==4.51.0 +transformers==4.51.1 tokenizers>=0.21.1 accelerate==1.6.0 datasets==3.5.0 From f85861a0b222222b9203b0bb201975594e079292 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 9 Apr 2025 13:53:17 +0700 Subject: [PATCH 0539/1405] fix: liger swiglu for llama4 (#2504) * fix: liger swiglu for llama4 * feat: add liger to deepseek v3 * fix: unpack not found * fix: spelling * fix: comment out deepseek v3 * fix: retest deepseek * fix: map glu * fix: patch model forward * chore: add temp code to save * fix: remove deepseek to move into separate PR --- src/axolotl/integrations/liger/__init__.py | 6 ++++-- src/axolotl/integrations/liger/models/llama4.py | 12 +++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index 8d737175e7..8e305e0f33 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -185,5 +185,7 @@ def _liger_rms_norm_wrapper(dim, **kwargs): rms_norm=cfg.liger_rms_norm, layer_norm=cfg.liger_layer_norm, ) - elif cfg.model_config_type in ["deepseek_v3"]: - raise ValueError(f"Unsupported model config type: {cfg.model_config_type}") + else: + logging.warning( + f"Unsupported model config type: {cfg.model_config_type}. Liger not applied." + ) diff --git a/src/axolotl/integrations/liger/models/llama4.py b/src/axolotl/integrations/liger/models/llama4.py index da35b114c4..689823bb67 100644 --- a/src/axolotl/integrations/liger/models/llama4.py +++ b/src/axolotl/integrations/liger/models/llama4.py @@ -3,6 +3,7 @@ """ import sys +from copy import deepcopy from typing import List, Optional, Tuple, Union import torch @@ -158,7 +159,16 @@ def apply_liger_kernel_to_llama4( if rms_norm: modeling_llama4.Llama4TextRMSNorm = LigerRMSNorm if glu_activation: - modeling_llama4.Llama4TextMLP = LigerSwiGLUMLP + + def _liger_swiglu_mlp_wrapper(config, intermediate_size=None, **kwargs): + "Accepts intermediate_size to pass to LigerSwiGLUMLP" + # clone config to avoid modifying the original + config = deepcopy(config) + if intermediate_size: + setattr(config, "intermediate_size", intermediate_size) + return LigerSwiGLUMLP(config, **kwargs) + + modeling_llama4.Llama4TextMLP = _liger_swiglu_mlp_wrapper if layer_norm: modeling_llama4.nn.LayerNorm = LigerLayerNorm From 9f986f5e71530557b59182df38d2b19858e7b440 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 9 Apr 2025 14:01:28 -0400 Subject: [PATCH 0540/1405] Add Llama4 maverick examples (#2512) --- examples/llama-4/README.md | 8 +- examples/llama-4/maverick-qlora-fsdp1.yaml | 89 ++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 examples/llama-4/maverick-qlora-fsdp1.yaml diff --git a/examples/llama-4/README.md b/examples/llama-4/README.md index 53448da2b1..a0ec1c70ea 100644 --- a/examples/llama-4/README.md +++ b/examples/llama-4/README.md @@ -7,4 +7,10 @@ - [Text Single GPU (H100) QLoRA](./scout-qlora-single-h100.yaml) - [Text Multi GPU QLoRA w/ FSDP1](./scout-qlora-fsdp1.yaml) -Our Single GPU implementation for Llama 4 Scout uses only 68.5GB VRAM for post-training with 4k context length @ 546 tokens/second. +Our Single H100 implementation for Llama 4 Scout uses only 68.5GB VRAM for post-training with 4k context length @ 546 tokens/second. [WandB logs here](https://wandb.ai/axolotl-ai/llama4-sft/runs/zic56rhd) + +### Llama 4 Maverick 17Bx128Experts (400B) + +- [Text Multi GPU QLoRA w/FSDP1](./maverick-qlora-fsdp1.yaml) + +Our 4xH100 implementation for Llama 4 Maverick uses 79.5GB VRAM/GPU for post-training with 4k context length @ 206 tokens/second. [WandB logs here.](https://wandb.ai/axolotl-ai/llama-sft/runs/siyvwuxc?nw=nwuserwinglian) diff --git a/examples/llama-4/maverick-qlora-fsdp1.yaml b/examples/llama-4/maverick-qlora-fsdp1.yaml new file mode 100644 index 0000000000..232afc73eb --- /dev/null +++ b/examples/llama-4/maverick-qlora-fsdp1.yaml @@ -0,0 +1,89 @@ +base_model: axolotl-quants/Llama-4-Maverick-17B-128E-Linearized-bnb-nf4-bf16 +model_type: Llama4ForConditionalGeneration +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_glu_activation: true +liger_rms_norm: true +liger_layer_norm: true + +llama4_linearized_experts: true +load_in_4bit: true +adapter: qlora +lora_r: 32 +lora_alpha: 64 +lora_target_modules: + - self_attn.q_proj + - self_attn.k_proj + - self_attn.v_proj + - self_attn.o_proj + - shared_expert.gate_proj + - shared_expert.up_proj + - shared_expert.down_proj + # - experts.gate_projs.[0-9]+$ + # - experts.up_projs.[0-9]+$ + # - experts.down_projs.[0-9]+$ +lora_modules_to_save: +# - lm_head +# - embed_tokens + +chat_template: llama4 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value + +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +sequence_len: 4096 +sample_packing: true +pad_to_sequence_len: true + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 1e-4 + +bf16: true +tf32: true + +logging_steps: 1 +flash_attention: true + +gradient_checkpointing: offload +gradient_checkpointing_kwargs: + use_reentrant: false + +warmup_steps: 20 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +fsdp: + - auto_wrap + - full_shard +fsdp_config: + fsdp_transformer_layer_cls_to_wrap: Llama4TextDecoderLayer + fsdp_limit_all_gathers: true + fsdp_sync_module_states: true + fsdp_offload_params: true + fsdp_use_orig_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD +special_tokens: + pad_token: <|finetune_right_pad_id|> + eos_token: <|eot|> From e0420b3528ad3120f3bace81d7dff69c657ab91b Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 10 Apr 2025 01:01:42 +0700 Subject: [PATCH 0541/1405] fix: allow merge lora on pre-quantized model (#2511) * fix: allow merge lora on pre-quantized model * fix: remove unused sections per comment --- src/axolotl/cli/merge_lora.py | 8 +++++++- src/axolotl/utils/models.py | 6 ------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 1059218fa2..c7a3a32259 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -32,7 +32,13 @@ def do_merge_lora(*, cfg: DictDefault) -> None: LOG.info("Running merge of LoRA with base model...") model = model.merge_and_unload(progressbar=True) - model.to(dtype=cfg.torch_dtype) + try: + model.to(dtype=cfg.torch_dtype) + except ValueError as e: + LOG.warning("Failed to convert model to dtype %s", cfg.torch_dtype) + LOG.warning("Ignore this if the base_model is pre-quantized.") + LOG.warning("Error raised: %s", e) + model.generation_config.do_sample = True if cfg.local_rank == 0: diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index f808f4bdd4..c5e569f130 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -151,12 +151,6 @@ def check_model_config(cfg: DictDefault, model_config: PretrainedConfig): "Please make sure to point to a GPTQ model." ) - if not cfg.gptq and quant_config_exists and not cfg.load_in_4bit: - raise ValueError( - "model_config.quantization_config is set but `gptq` flag is not. " - "Please use the `gptq` flag to train quantized model or point to a non-quantized model." - ) - lora_modules_to_save = get_linear_embedding_layers(model_config.model_type) if ( cfg.adapter From 16823e1de678098a5d084e52f70e3ddee1051412 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 10 Apr 2025 12:34:25 +0700 Subject: [PATCH 0542/1405] feat: add CNAME (#2513) --- CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 CNAME diff --git a/CNAME b/CNAME new file mode 100644 index 0000000000..153ba56c3e --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +docs.axolotl.ai From 22c562533d0e106601a41b4d048f3ca0119d3d0b Mon Sep 17 00:00:00 2001 From: Sung Ching Liu <22844540+bursteratom@users.noreply.github.com> Date: Thu, 10 Apr 2025 11:33:09 -0400 Subject: [PATCH 0543/1405] Update rlhf.qmd (#2519) Fix typo in command that spawns a vllm server, should be `axolotl vllm-serve` not `axolotl vllm_serve` --- docs/rlhf.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index b3adb5937c..490d281265 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -530,7 +530,7 @@ trl: ``` ```bash -CUDA_VISIBLE_DEVICES=2,3 axolotl vllm_serve grpo.yaml +CUDA_VISIBLE_DEVICES=2,3 axolotl vllm-serve grpo.yaml ``` Your `vLLM` instance will now attempt to spin up, and it's time to kick off training utilizing our remaining two GPUs. In another terminal, execute: From 7e7180fa10a6627c0963abb3f36bc0d9fd31668a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 11 Apr 2025 09:51:59 -0400 Subject: [PATCH 0544/1405] add mocks for loading datasets in cli train tests (#2497) [skip ci] * add mocks for loading datasets in cli train tests * Apply suggestions from code review to fix patched module for preprocess Co-authored-by: NanoCode012 --------- Co-authored-by: NanoCode012 --- tests/cli/test_cli_preprocess.py | 50 +++++++++++++---------- tests/cli/test_cli_train.py | 70 +++++++++++++++++--------------- 2 files changed, 65 insertions(+), 55 deletions(-) diff --git a/tests/cli/test_cli_preprocess.py b/tests/cli/test_cli_preprocess.py index e2dd3a6c35..b213e43da3 100644 --- a/tests/cli/test_cli_preprocess.py +++ b/tests/cli/test_cli_preprocess.py @@ -2,7 +2,7 @@ import shutil from pathlib import Path -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest @@ -26,12 +26,15 @@ def test_preprocess_config_not_found(cli_runner): def test_preprocess_basic(cli_runner, config_path): """Test basic preprocessing with minimal config""" with patch("axolotl.cli.preprocess.do_cli") as mock_do_cli: - result = cli_runner.invoke(cli, ["preprocess", str(config_path)]) - assert result.exit_code == 0 + with patch("axolotl.cli.preprocess.load_datasets") as mock_load_datasets: + mock_load_datasets.return_value = MagicMock() - mock_do_cli.assert_called_once() - assert mock_do_cli.call_args.kwargs["config"] == str(config_path) - assert mock_do_cli.call_args.kwargs["download"] is True + result = cli_runner.invoke(cli, ["preprocess", str(config_path)]) + assert result.exit_code == 0 + + mock_do_cli.assert_called_once() + assert mock_do_cli.call_args.kwargs["config"] == str(config_path) + assert mock_do_cli.call_args.kwargs["download"] is True def test_preprocess_without_download(cli_runner, config_path): @@ -54,19 +57,22 @@ def test_preprocess_custom_path(cli_runner, tmp_path, valid_test_config): config_path.write_text(valid_test_config) with patch("axolotl.cli.preprocess.do_cli") as mock_do_cli: - result = cli_runner.invoke( - cli, - [ - "preprocess", - str(config_path), - "--dataset-prepared-path", - str(custom_path.absolute()), - ], - ) - assert result.exit_code == 0 - - mock_do_cli.assert_called_once() - assert mock_do_cli.call_args.kwargs["config"] == str(config_path) - assert mock_do_cli.call_args.kwargs["dataset_prepared_path"] == str( - custom_path.absolute() - ) + with patch("axolotl.cli.preprocess.load_datasets") as mock_load_datasets: + mock_load_datasets.return_value = MagicMock() + + result = cli_runner.invoke( + cli, + [ + "preprocess", + str(config_path), + "--dataset-prepared-path", + str(custom_path.absolute()), + ], + ) + assert result.exit_code == 0 + + mock_do_cli.assert_called_once() + assert mock_do_cli.call_args.kwargs["config"] == str(config_path) + assert mock_do_cli.call_args.kwargs["dataset_prepared_path"] == str( + custom_path.absolute() + ) diff --git a/tests/cli/test_cli_train.py b/tests/cli/test_cli_train.py index a512510335..4739135993 100644 --- a/tests/cli/test_cli_train.py +++ b/tests/cli/test_cli_train.py @@ -29,19 +29,21 @@ def test_train_basic_execution_no_accelerate( with patch("axolotl.cli.train.train") as mock_train: mock_train.return_value = (MagicMock(), MagicMock(), MagicMock()) - - result = cli_runner.invoke( - cli, - [ - "train", - str(config_path), - "--no-accelerate", - ], - catch_exceptions=False, - ) - - assert result.exit_code == 0 - mock_train.assert_called_once() + with patch("axolotl.cli.train.load_datasets") as mock_load_datasets: + mock_load_datasets.return_value = MagicMock() + + result = cli_runner.invoke( + cli, + [ + "train", + str(config_path), + "--no-accelerate", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_train.assert_called_once() def test_train_cli_overrides(self, cli_runner, tmp_path, valid_test_config): """Test CLI arguments properly override config values""" @@ -49,23 +51,25 @@ def test_train_cli_overrides(self, cli_runner, tmp_path, valid_test_config): with patch("axolotl.cli.train.train") as mock_train: mock_train.return_value = (MagicMock(), MagicMock(), MagicMock()) - - result = cli_runner.invoke( - cli, - [ - "train", - str(config_path), - "--learning-rate", - "1e-4", - "--micro-batch-size", - "2", - "--no-accelerate", - ], - catch_exceptions=False, - ) - - assert result.exit_code == 0 - mock_train.assert_called_once() - cfg = mock_train.call_args[1]["cfg"] - assert cfg["learning_rate"] == 1e-4 - assert cfg["micro_batch_size"] == 2 + with patch("axolotl.cli.train.load_datasets") as mock_load_datasets: + mock_load_datasets.return_value = MagicMock() + + result = cli_runner.invoke( + cli, + [ + "train", + str(config_path), + "--learning-rate", + "1e-4", + "--micro-batch-size", + "2", + "--no-accelerate", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_train.assert_called_once() + cfg = mock_train.call_args[1]["cfg"] + assert cfg["learning_rate"] == 1e-4 + assert cfg["micro_batch_size"] == 2 From 9a8e3e9c7bdb01e3730438c49b4cf5d4c4403c87 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 11 Apr 2025 20:52:23 +0700 Subject: [PATCH 0545/1405] Feat(examples): add deepcogito (#2516) [skip ci] * feat: add examples for deepcogito * fix: reduce num evals per epoch * fix: reduce num epochs --- .../cogito-v1-preview-llama-3B-lora.yml | 58 +++++++++++++++++++ .../cogito-v1-preview-qwen-14B-lora.yml | 58 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml create mode 100644 examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml diff --git a/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml b/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml new file mode 100644 index 0000000000..2c0495ceda --- /dev/null +++ b/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml @@ -0,0 +1,58 @@ +base_model: deepcogito/cogito-v1-preview-llama-3B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: true +load_in_4bit: false +strict: false + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + field_messages: messages + message_property_mappings: + role: role + content: content + +dataset_prepared_path: +val_set_size: 0.05 +output_dir: ./outputs/lora-out + +sequence_len: 4096 +sample_packing: true +eval_sample_packing: false +pad_to_sequence_len: true + +adapter: lora +lora_model_dir: +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_steps: 10 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml b/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml new file mode 100644 index 0000000000..de9c956e0c --- /dev/null +++ b/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml @@ -0,0 +1,58 @@ +base_model: deepcogito/cogito-v1-preview-qwen-14B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: true +load_in_4bit: false +strict: false + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + field_messages: messages + message_property_mappings: + role: role + content: content + +dataset_prepared_path: +val_set_size: 0.05 +output_dir: ./outputs/lora-out + +sequence_len: 4096 +sample_packing: true +eval_sample_packing: false +pad_to_sequence_len: true + +adapter: lora +lora_model_dir: +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_steps: 10 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: From 756a0559c1bdadb8c833d9de13e728b616972a56 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 11 Apr 2025 20:52:43 +0700 Subject: [PATCH 0546/1405] feat(doc): explain deepspeed configs (#2514) [skip ci] * feat(doc): explain deepspeed configs * fix: add fetch configs --- docs/multi-gpu.qmd | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/multi-gpu.qmd b/docs/multi-gpu.qmd index 5aec89763e..55eaca6c35 100644 --- a/docs/multi-gpu.qmd +++ b/docs/multi-gpu.qmd @@ -36,6 +36,9 @@ deepspeed: deepspeed_configs/zero1.json ### Usage {#sec-deepspeed-usage} ```{.bash} +# Fetch deepspeed configs (if not already present) +axolotl fetch deepspeed_configs + # Passing arg via config axolotl train config.yml @@ -48,10 +51,20 @@ axolotl train config.yml --deepspeed deepspeed_configs/zero1.json We provide default configurations for: - ZeRO Stage 1 (`zero1.json`) +- ZeRO Stage 1 with torch compile (`zero1_torch_compile.json`) - ZeRO Stage 2 (`zero2.json`) - ZeRO Stage 3 (`zero3.json`) +- ZeRO Stage 3 with bf16 (`zero3_bf16.json`) +- ZeRO Stage 3 with bf16 and CPU offload params(`zero3_bf16_cpuoffload_params.json`) +- ZeRO Stage 3 with bf16 and CPU offload params and optimizer (`zero3_bf16_cpuoffload_all.json`) + +::: {.callout-tip} + +Choose the configuration that offloads the least amount to memory while still being able to fit on VRAM for best performance. -Choose based on your memory requirements and performance needs. +Start from Stage 1 -> Stage 2 -> Stage 3. + +::: ## FSDP {#sec-fsdp} From 51267ded04f1e2590a6c6b773dae2c888771677c Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 11 Apr 2025 20:53:18 +0700 Subject: [PATCH 0547/1405] chore: update doc links (#2509) * chore: update doc links * fix: address pr feedback --- README.md | 22 +++++++++++----------- docs/config.qmd | 6 +++--- docs/dataset-formats/index.qmd | 5 +---- pyproject.toml | 3 ++- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ed2c9e6b1f..0f2907df44 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ axolotl fetch examples axolotl fetch deepspeed_configs # OPTIONAL ``` -Other installation approaches are described [here](https://axolotl-ai-cloud.github.io/axolotl/docs/installation.html). +Other installation approaches are described [here](https://docs.axolotl.ai/docs/installation.html). ### Your First Fine-tune @@ -78,7 +78,7 @@ axolotl fetch examples --dest path/to/folder axolotl train examples/llama-3/lora-1b.yml ``` -That's it! Check out our [Getting Started Guide](https://axolotl-ai-cloud.github.io/axolotl/docs/getting-started.html) for a more detailed walkthrough. +That's it! Check out our [Getting Started Guide](https://docs.axolotl.ai/docs/getting-started.html) for a more detailed walkthrough. ## ✨ Key Features @@ -91,20 +91,20 @@ That's it! Check out our [Getting Started Guide](https://axolotl-ai-cloud.github ## 📚 Documentation -- [Installation Options](https://axolotl-ai-cloud.github.io/axolotl/docs/installation.html) - Detailed setup instructions for different environments -- [Configuration Guide](https://axolotl-ai-cloud.github.io/axolotl/docs/config.html) - Full configuration options and examples -- [Dataset Guide](https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/) - Supported formats and how to use them -- [Multi-GPU Training](https://axolotl-ai-cloud.github.io/axolotl/docs/multi-gpu.html) -- [Multi-Node Training](https://axolotl-ai-cloud.github.io/axolotl/docs/multi-node.html) -- [Multipacking](https://axolotl-ai-cloud.github.io/axolotl/docs/multipack.html) -- [API Reference](https://axolotl-ai-cloud.github.io/axolotl/docs/api/) - Auto-generated code documentation -- [FAQ](https://axolotl-ai-cloud.github.io/axolotl/docs/faq.html) - Frequently asked questions +- [Installation Options](https://docs.axolotl.ai/docs/installation.html) - Detailed setup instructions for different environments +- [Configuration Guide](https://docs.axolotl.ai/docs/config.html) - Full configuration options and examples +- [Dataset Guide](https://docs.axolotl.ai/docs/dataset-formats/) - Supported formats and how to use them +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [Multipacking](https://docs.axolotl.ai/docs/multipack.html) +- [API Reference](https://docs.axolotl.ai/docs/api/) - Auto-generated code documentation +- [FAQ](https://docs.axolotl.ai/docs/faq.html) - Frequently asked questions ## 🤝 Getting Help - Join our [Discord community](https://discord.gg/HhrNrHJPRb) for support - Check out our [Examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/) directory -- Read our [Debugging Guide](https://axolotl-ai-cloud.github.io/axolotl/docs/debugging.html) +- Read our [Debugging Guide](https://docs.axolotl.ai/docs/debugging.html) - Need dedicated support? Please contact [✉️wing@axolotl.ai](mailto:wing@axolotl.ai) for options ## 🌟 Contributing diff --git a/docs/config.qmd b/docs/config.qmd index 4099730c5d..0f55c10772 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -90,7 +90,7 @@ lora_on_cpu: true # List[str]. Add plugins to extend the pipeline. # See `src/axolotl/integrations` for the available plugins or doc below for more details. -# https://axolotl-ai-cloud.github.io/axolotl/docs/custom_integrations.html +# https://docs.axolotl.ai/docs/custom_integrations.html plugins: # - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin @@ -394,7 +394,7 @@ lora_fan_in_fan_out: false # Apply custom LoRA autograd functions and activation function Triton kernels for # speed and memory savings -# See: https://axolotl-ai-cloud.github.io/axolotl/docs/lora_optims.html +# See: https://docs.axolotl.ai/docs/lora_optims.html lora_mlp_kernel: true lora_qkv_kernel: true lora_o_kernel: true @@ -688,7 +688,7 @@ ddp_broadcast_buffers: # Use in long context training to prevent OOM when sequences cannot fit into a single GPU's VRAM. # E.g., if 4 GPUs are available, set this value to 2 to split each sequence into two equal-sized # subsequences, or set to 4 to split into four equal-sized subsequences. -# See https://axolotl-ai-cloud.github.io/axolotl/docs/sequence_parallelism.html for more details. +# See https://docs.axolotl.ai/docs/sequence_parallelism.html for more details. sequence_parallel_degree: # Optional; strides across the key dimension. Larger values use more memory but should make training faster. # Must evenly divide the number of KV heads in your model. diff --git a/docs/dataset-formats/index.qmd b/docs/dataset-formats/index.qmd index a071f1d56d..9898bbc9b6 100644 --- a/docs/dataset-formats/index.qmd +++ b/docs/dataset-formats/index.qmd @@ -457,10 +457,7 @@ datasets: type: alpaca ``` -Axolotl supports many kinds of instruction dataset. All of them can be found here (https://axolotl-ai-cloud.github.io/axolotl/docs/dataset-formats/inst_tune.html) with their respective type and sample row format. - - -Reference: [Instruction Dataset Documentation](inst_tune.qmd). +Axolotl supports many kinds of instruction dataset. All of them can be found in the [Instruction Dataset Documentation](inst_tune.qmd) with their respective type and sample row format. #### Custom Instruct Prompt Format diff --git a/pyproject.toml b/pyproject.toml index eb85691dd3..36138c65d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,8 @@ requires-python = ">=3.10" axolotl = "axolotl.cli.main:main" [project.urls] -Homepage = "https://axolotl-ai-cloud.github.io/axolotl/" +Homepage = "https://axolotl.ai/" +Documentation = "https://docs.axolotl.ai/" Repository = "https://github.com/axolotl-ai-cloud/axolotl.git" [tool.setuptools_scm] From de8a625dd7205c335f8201597d741e6de95de06d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 12 Apr 2025 07:24:43 -0700 Subject: [PATCH 0548/1405] make e2e tests a bit faster by reducing test split size (#2522) [skip ci] * [ci] make e2e tests a bit faster by reducing test split size * use 10% split of alpaca dataset to speed up dataset loading/tokenization * reduce gas 4->2 for most e2e tests * increase val set size for packing --- .../integrations/test_cut_cross_entropy.py | 4 ++-- tests/e2e/multigpu/solo/test_flex.py | 2 +- tests/e2e/multigpu/test_llama.py | 19 +++++++++++++------ tests/e2e/multigpu/test_ray.py | 2 ++ tests/e2e/multigpu/test_sp.py | 1 + tests/e2e/patched/test_llama_s2_attention.py | 4 ++-- tests/e2e/patched/test_model_patches.py | 4 ++-- tests/e2e/patched/test_phi_multipack.py | 2 +- tests/e2e/solo/test_flex.py | 2 +- tests/e2e/test_deepseekv3.py | 4 ++-- tests/e2e/test_falcon.py | 6 +++--- tests/e2e/test_gemma2.py | 4 ++-- tests/e2e/test_gemma3_text.py | 4 ++-- tests/e2e/test_llama.py | 2 +- tests/e2e/test_llama_vision.py | 4 ++-- tests/e2e/test_load_model.py | 2 +- tests/e2e/test_lora_llama.py | 2 +- tests/e2e/test_mistral.py | 4 ++-- tests/e2e/test_mixtral.py | 10 +++++----- tests/e2e/test_optimizers.py | 6 +++--- tests/e2e/test_packing_loss.py | 2 +- tests/e2e/test_phi.py | 4 ++-- tests/e2e/test_schedulers.py | 2 +- tests/test_exact_deduplication.py | 2 +- 24 files changed, 54 insertions(+), 44 deletions(-) diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py index 25e36b5ebd..7539345634 100644 --- a/tests/e2e/integrations/test_cut_cross_entropy.py +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -25,7 +25,7 @@ def min_cfg(temp_dir): ], "cut_cross_entropy": True, "sequence_len": 1024, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -79,7 +79,7 @@ def test_qwen2_w_cce(self, temp_dir): ], "cut_cross_entropy": True, "sequence_len": 1024, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "pad_token": "<|endoftext|>", }, diff --git a/tests/e2e/multigpu/solo/test_flex.py b/tests/e2e/multigpu/solo/test_flex.py index 5e2c9e7cc4..3af6d5a768 100644 --- a/tests/e2e/multigpu/solo/test_flex.py +++ b/tests/e2e/multigpu/solo/test_flex.py @@ -55,7 +55,7 @@ def test_loss_llama(self, temp_dir): ], "num_epochs": 1, "micro_batch_size": 2, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index f44c775c8d..3bacac821d 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -58,12 +58,13 @@ def test_lora_ddp(self, temp_dir): { "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, "max_steps": 2, "micro_batch_size": 1, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, # "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, @@ -201,7 +202,7 @@ def test_dpo_lora_ddp(self, temp_dir): "num_epochs": 1, "max_steps": 2, "micro_batch_size": 2, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, # "gradient_checkpointing": True, "output_dir": temp_dir, "warmup_steps": 0, @@ -279,7 +280,7 @@ def test_dpo_qlora_ddp(self, temp_dir): "num_epochs": 1, "max_steps": 2, "micro_batch_size": 2, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, # "gradient_checkpointing": True, "output_dir": temp_dir, "warmup_steps": 0, @@ -335,6 +336,7 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): { "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, @@ -398,7 +400,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): "sample_packing": True, "pad_to_sequence_len": True, "sequence_len": 1024, - "val_set_size": 0.01, + "val_set_size": 0.05, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -406,6 +408,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): { "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, @@ -484,6 +487,7 @@ def test_fsdp2_packed( { "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, @@ -565,7 +569,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): { "path": "tatsu-lab/alpaca", "type": "alpaca", - "split": "train[:25%]", + "split": "train[:10%]", }, ], "num_epochs": 1, @@ -660,7 +664,7 @@ def test_ds_zero3_packed( "sample_packing": True, "pad_to_sequence_len": True, "sequence_len": 1024, - "val_set_size": 0.01, + "val_set_size": 0.05, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -668,6 +672,7 @@ def test_ds_zero3_packed( { "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, @@ -741,6 +746,7 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): { "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, @@ -814,6 +820,7 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): { "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index 9be7c6f503..843adac912 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -45,6 +45,7 @@ def test_lora_ddp(self, temp_dir): { "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, @@ -103,6 +104,7 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps): { "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, diff --git a/tests/e2e/multigpu/test_sp.py b/tests/e2e/multigpu/test_sp.py index 2bd10beb51..288720eec2 100644 --- a/tests/e2e/multigpu/test_sp.py +++ b/tests/e2e/multigpu/test_sp.py @@ -40,6 +40,7 @@ def test_sequence_parallel_training(self, temp_dir): { "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py index cfa70fd73c..b8ddf10dae 100644 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ b/tests/e2e/patched/test_llama_s2_attention.py @@ -43,7 +43,7 @@ def test_lora_s2_attn(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": {}, "datasets": [ { @@ -83,7 +83,7 @@ def test_fft_s2_attn(self, temp_dir): "sample_packing": False, "flash_attention": True, "s2_attention": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": {}, "datasets": [ { diff --git a/tests/e2e/patched/test_model_patches.py b/tests/e2e/patched/test_model_patches.py index c6a13af19b..ec09e0c81a 100644 --- a/tests/e2e/patched/test_model_patches.py +++ b/tests/e2e/patched/test_model_patches.py @@ -27,7 +27,7 @@ def test_mixtral_multipack(self, temp_dir): "flash_attention": True, "sample_packing": True, "sequence_len": 2048, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": {}, "datasets": [ { @@ -59,7 +59,7 @@ def test_mistral_multipack(self, temp_dir): "flash_attention": True, "sample_packing": True, "sequence_len": 2048, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": {}, "datasets": [ { diff --git a/tests/e2e/patched/test_phi_multipack.py b/tests/e2e/patched/test_phi_multipack.py index ce466460eb..70b3ea124f 100644 --- a/tests/e2e/patched/test_phi_multipack.py +++ b/tests/e2e/patched/test_phi_multipack.py @@ -88,7 +88,7 @@ def test_qlora_packed(self, temp_dir): "lora_alpha": 32, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "pad_token": "<|endoftext|>", }, diff --git a/tests/e2e/solo/test_flex.py b/tests/e2e/solo/test_flex.py index 5e52204a69..6de813e375 100644 --- a/tests/e2e/solo/test_flex.py +++ b/tests/e2e/solo/test_flex.py @@ -47,7 +47,7 @@ def test_loss_llama(self, temp_dir): ], "num_epochs": 1, "micro_batch_size": 2, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", diff --git a/tests/e2e/test_deepseekv3.py b/tests/e2e/test_deepseekv3.py index cdaa2c4161..2afda640f1 100644 --- a/tests/e2e/test_deepseekv3.py +++ b/tests/e2e/test_deepseekv3.py @@ -65,7 +65,7 @@ def test_lora_deepseekv3(self, temp_dir, sample_packing): "chat_template": "deepseek_v3", "num_epochs": 1, "micro_batch_size": 1, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_bnb_8bit", @@ -115,7 +115,7 @@ def test_fft_deepseekv3(self, temp_dir, sample_packing): }, "num_epochs": 1, "micro_batch_size": 1, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_bnb_8bit", diff --git a/tests/e2e/test_falcon.py b/tests/e2e/test_falcon.py index 3c325459b8..a1641a9971 100644 --- a/tests/e2e/test_falcon.py +++ b/tests/e2e/test_falcon.py @@ -41,7 +41,7 @@ def test_lora(self, temp_dir): "word_embeddings", "lm_head", ], - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "bos_token": "<|endoftext|>", "pad_token": "<|endoftext|>", @@ -92,7 +92,7 @@ def test_lora_added_vocab(self, temp_dir): "word_embeddings", "lm_head", ], - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "bos_token": "<|endoftext|>", "pad_token": "<|endoftext|>", @@ -137,7 +137,7 @@ def test_ft(self, temp_dir): "base_model": "illuin/tiny-random-FalconForCausalLM", "flash_attention": True, "sequence_len": 1024, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "bos_token": "<|endoftext|>", "pad_token": "<|endoftext|>", diff --git a/tests/e2e/test_gemma2.py b/tests/e2e/test_gemma2.py index df777b7099..68dc4855d9 100644 --- a/tests/e2e/test_gemma2.py +++ b/tests/e2e/test_gemma2.py @@ -62,7 +62,7 @@ def test_lora_gemma2(self, temp_dir, sample_packing): "chat_template": "gemma", # gemma2's template is same as gemma "num_epochs": 1, "micro_batch_size": 1, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_bnb_8bit", @@ -114,7 +114,7 @@ def test_fft_gemma2(self, temp_dir, sample_packing): }, "num_epochs": 1, "micro_batch_size": 1, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_bnb_8bit", diff --git a/tests/e2e/test_gemma3_text.py b/tests/e2e/test_gemma3_text.py index 14423ce733..5cbde04d10 100644 --- a/tests/e2e/test_gemma3_text.py +++ b/tests/e2e/test_gemma3_text.py @@ -61,7 +61,7 @@ def test_lora_gemma3_text(self, temp_dir, sample_packing): "chat_template": "gemma3", "num_epochs": 1, "micro_batch_size": 1, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_bnb_8bit", @@ -112,7 +112,7 @@ def test_fft_gemma3_text(self, temp_dir, sample_packing): }, "num_epochs": 1, "micro_batch_size": 1, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_bnb_8bit", diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index 8d6483ea42..b84e432b5b 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -30,7 +30,7 @@ def test_fft_trust_remote_code(self, temp_dir): "tokenizer_type": "LlamaTokenizer", "trust_remote_code": True, "sequence_len": 512, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "unk_token": "", "bos_token": "", diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py index c4a41f521d..3fc12afcce 100644 --- a/tests/e2e/test_llama_vision.py +++ b/tests/e2e/test_llama_vision.py @@ -52,7 +52,7 @@ def test_lora_llama_vision_text_only_dataset(self, temp_dir): ], "num_epochs": 1, "micro_batch_size": 1, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_bnb_8bit", @@ -99,7 +99,7 @@ def test_lora_llama_vision_multimodal_dataset(self, temp_dir): ], "num_epochs": 1, "micro_batch_size": 1, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_bnb_8bit", diff --git a/tests/e2e/test_load_model.py b/tests/e2e/test_load_model.py index 255b096b0f..2128dbef2d 100644 --- a/tests/e2e/test_load_model.py +++ b/tests/e2e/test_load_model.py @@ -36,7 +36,7 @@ def setup_method(self): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "unk_token": "", "bos_token": "", diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index d314fb1977..8328d5b90b 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -37,7 +37,7 @@ def test_lora(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "unk_token": "", "bos_token": "", diff --git a/tests/e2e/test_mistral.py b/tests/e2e/test_mistral.py index 2468a45e94..740fa6eed9 100644 --- a/tests/e2e/test_mistral.py +++ b/tests/e2e/test_mistral.py @@ -39,7 +39,7 @@ def test_lora(self, temp_dir): "lora_alpha": 64, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "unk_token": "", "bos_token": "", @@ -80,7 +80,7 @@ def test_ft(self, temp_dir): "base_model": "openaccess-ai-collective/tiny-mistral", "flash_attention": True, "sequence_len": 1024, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "unk_token": "", "bos_token": "", diff --git a/tests/e2e/test_mixtral.py b/tests/e2e/test_mixtral.py index f31920be6f..4e0693b949 100644 --- a/tests/e2e/test_mixtral.py +++ b/tests/e2e/test_mixtral.py @@ -49,7 +49,7 @@ def test_qlora_w_fa2(self, temp_dir): "q_proj", "w2", ], - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": {}, "datasets": [ { @@ -105,7 +105,7 @@ def test_qlora_wo_fa2(self, temp_dir): "q_proj", "w2", ], - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": {}, "datasets": [ { @@ -160,7 +160,7 @@ def test_16bit_lora_w_fa2(self, temp_dir): "q_proj", "w2", ], - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": {}, "datasets": [ { @@ -219,7 +219,7 @@ def test_16bit_lora_wo_fa2(self, temp_dir): "q_proj", "w2", ], - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": {}, "datasets": [ { @@ -265,7 +265,7 @@ def test_ft(self, temp_dir): "tokenizer_config": "LoneStriker/Mixtral-8x7B-v0.1-HF", "flash_attention": True, "sequence_len": 1024, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": {}, "datasets": [ { diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 43a4735aa9..8a82e34695 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -37,7 +37,7 @@ def test_optimi_adamw(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "unk_token": "", "bos_token": "", @@ -84,7 +84,7 @@ def test_adopt_adamw(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "unk_token": "", "bos_token": "", @@ -131,7 +131,7 @@ def test_muon(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "unk_token": "", "bos_token": "", diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index 30054962c9..4e8e704194 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -46,7 +46,7 @@ def test_loss_packed(self, temp_dir): ], "num_epochs": 1, "micro_batch_size": 2, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", diff --git a/tests/e2e/test_phi.py b/tests/e2e/test_phi.py index 49f9261c9f..268646432c 100644 --- a/tests/e2e/test_phi.py +++ b/tests/e2e/test_phi.py @@ -35,7 +35,7 @@ def test_phi_ft(self, temp_dir): "sample_packing": False, "load_in_8bit": False, "adapter": None, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "pad_token": "<|endoftext|>", }, @@ -85,7 +85,7 @@ def test_phi_qlora(self, temp_dir): "lora_alpha": 32, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "pad_token": "<|endoftext|>", }, diff --git a/tests/e2e/test_schedulers.py b/tests/e2e/test_schedulers.py index 2d5040ae35..c20cebf4e6 100644 --- a/tests/e2e/test_schedulers.py +++ b/tests/e2e/test_schedulers.py @@ -37,7 +37,7 @@ def test_rex_scheduler(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "val_set_size": 0.1, + "val_set_size": 0.02, "special_tokens": { "unk_token": "", "bos_token": "", diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index a75f97f78a..4d069a11d9 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -313,7 +313,7 @@ def setUp(self) -> None: }, ], "val_set_size": 0.0, - "gradient_accumulation_steps": 4, + "gradient_accumulation_steps": 2, "batch_size": 10, "micro_batch_size": 10, "num_epochs": 1, From dd8bad06d0b4849a5a32dcb7f0f91b4a5be869f7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 12 Apr 2025 07:25:11 -0700 Subject: [PATCH 0549/1405] remove strict=false from example yamls [skip ci] (#2523) [skip ci] --- examples/cerebras/btlm-ft.yml | 1 - examples/cerebras/qlora.yml | 1 - examples/code-llama/13b/lora.yml | 1 - examples/code-llama/13b/qlora.yml | 1 - examples/code-llama/34b/lora.yml | 1 - examples/code-llama/34b/qlora.yml | 1 - examples/code-llama/7b/lora.yml | 1 - examples/code-llama/7b/qlora.yml | 1 - examples/cohere/command-r-7b-qlora.yml | 1 - examples/dbrx/16bit-lora.yaml | 1 - examples/dbrx/8bit-lora.yaml | 1 - examples/dbrx/fft-ds-zero3.yaml | 1 - examples/deepseek-v2/fft-fsdp-16b.yaml | 1 - examples/deepseek-v2/qlora-fsdp-2_5.yaml | 1 - examples/falcon/config-7b-lora.yml | 1 - examples/falcon/config-7b-qlora.yml | 1 - examples/falcon/config-7b.yml | 1 - examples/gemma/qlora.yml | 1 - examples/gemma2/qlora.yml | 1 - examples/gemma2/reward-model.yaml | 1 - examples/gemma3/gemma-3-1b-qlora.yml | 1 - examples/gemma3/gemma-3-4b-qlora.yml | 1 - examples/gemma3/gemma-3-4b-vision-qlora.yml | 1 - examples/gptj/qlora.yml | 1 - examples/jamba/qlora.yaml | 1 - examples/jamba/qlora_deepspeed.yaml | 1 - examples/jamba/qlora_fsdp_large.yaml | 1 - examples/llama-2/fft_optimized.yml | 1 - examples/llama-2/gptq-lora.yml | 1 - examples/llama-2/lisa.yml | 1 - examples/llama-2/loftq.yml | 1 - examples/llama-2/lora.yml | 1 - examples/llama-2/qlora-fsdp.yml | 1 - examples/llama-2/qlora.yml | 1 - examples/llama-2/relora.yml | 1 - examples/llama-3-vision/lora-11b.yaml | 1 - examples/llama-3/fft-8b-liger-fsdp.yaml | 1 - examples/llama-3/fft-8b.yaml | 1 - examples/llama-3/instruct-dpo-lora-8b.yml | 1 - examples/llama-3/instruct-lora-8b.yml | 1 - examples/llama-3/lora-1b-deduplicate-dpo.yml | 1 - examples/llama-3/lora-1b-deduplicate-sft.yml | 1 - examples/llama-3/lora-1b-kernels.yml | 1 - examples/llama-3/lora-1b-ray.yml | 1 - examples/llama-3/lora-1b-sample-packing-sequentially.yml | 1 - examples/llama-3/lora-1b.yml | 1 - examples/llama-3/lora-8b.yml | 1 - examples/llama-3/qlora-1b-kto.yaml | 1 - examples/llama-3/qlora-1b.yml | 1 - examples/llama-3/qlora-fsdp-405b.yaml | 1 - examples/llama-3/qlora-fsdp-70b.yaml | 1 - examples/llama-3/qlora.yml | 1 - examples/llama-4/maverick-qlora-fsdp1.yaml | 1 - examples/llama-4/scout-qlora-fsdp1.yaml | 1 - examples/llama-4/scout-qlora-single-h100.yaml | 1 - examples/llama-4/scout-vision-qlora-fsdp.yaml | 1 - examples/llava/lora-7b.yaml | 1 - examples/mamba/config.yml | 1 - examples/mistral/bigstral-ds-zero3.yaml | 1 - examples/mistral/config.yml | 1 - examples/mistral/lora-mps.yml | 1 - examples/mistral/lora.yml | 1 - examples/mistral/mistral-dpo-qlora.yml | 1 - examples/mistral/mistral-qlora-fsdp.yml | 1 - examples/mistral/mistral-qlora-orpo.yml | 1 - examples/mistral/mistral-small-3.1-24B-lora.yml | 1 - examples/mistral/mixtral-8x22b-qlora-fsdp.yml | 1 - examples/mistral/mixtral-qlora-fsdp.yml | 1 - examples/mistral/mixtral.yml | 1 - examples/mistral/mixtral_22.yml | 1 - examples/mistral/qlora.yml | 1 - examples/openllama-3b/config.yml | 1 - examples/openllama-3b/lora.yml | 1 - examples/openllama-3b/qlora.yml | 1 - examples/phi/lora-3.5.yaml | 1 - examples/phi/phi-ft.yml | 1 - examples/phi/phi-qlora.yml | 1 - examples/phi/phi2-ft.yml | 1 - examples/phi/phi3-ft-fsdp.yml | 1 - examples/phi/phi3-ft.yml | 1 - examples/pixtral/lora-12b.yml | 1 - examples/qwen/lora.yml | 1 - examples/qwen/qlora.yml | 1 - examples/qwen/qwen2-moe-lora.yaml | 1 - examples/qwen/qwen2-moe-qlora.yaml | 1 - examples/qwen2-vl/lora-7b.yaml | 1 - examples/qwen2/dpo.yaml | 1 - examples/qwen2/prm.yaml | 1 - examples/qwen2/qlora-fsdp.yaml | 1 - examples/qwen2/reward-model.yaml | 1 - examples/stablelm-2/1.6b/fft.yml | 1 - examples/stablelm-2/1.6b/lora.yml | 1 - examples/starcoder2/qlora.yml | 1 - examples/tiny-llama/lora-mps.yml | 1 - examples/tiny-llama/lora.yml | 1 - examples/tiny-llama/pretrain.yml | 1 - examples/tiny-llama/qlora.yml | 1 - examples/xgen-7b/xgen-7b-8k-qlora.yml | 1 - examples/yi-34B-chat/qlora.yml | 1 - 99 files changed, 99 deletions(-) diff --git a/examples/cerebras/btlm-ft.yml b/examples/cerebras/btlm-ft.yml index 6190714b49..c9878779d1 100644 --- a/examples/cerebras/btlm-ft.yml +++ b/examples/cerebras/btlm-ft.yml @@ -8,7 +8,6 @@ tokenizer_type: GPT2Tokenizer trust_remote_code: true tokenizer_use_fast: true tokenizer_legacy: true -strict: false push_dataset_to_hub: hf_use_auth_token: true datasets: diff --git a/examples/cerebras/qlora.yml b/examples/cerebras/qlora.yml index e74b2d675a..55cc597f17 100644 --- a/examples/cerebras/qlora.yml +++ b/examples/cerebras/qlora.yml @@ -4,7 +4,6 @@ base_model: cerebras/Cerebras-GPT-1.3B load_in_8bit: false load_in_4bit: true -strict: false push_dataset_to_hub: datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/code-llama/13b/lora.yml b/examples/code-llama/13b/lora.yml index 6c205ae879..0ed2382ba6 100644 --- a/examples/code-llama/13b/lora.yml +++ b/examples/code-llama/13b/lora.yml @@ -7,7 +7,6 @@ tokenizer_type: CodeLlamaTokenizer load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/code-llama/13b/qlora.yml b/examples/code-llama/13b/qlora.yml index 28f0275d35..22bd1691b4 100644 --- a/examples/code-llama/13b/qlora.yml +++ b/examples/code-llama/13b/qlora.yml @@ -7,7 +7,6 @@ tokenizer_type: CodeLlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/code-llama/34b/lora.yml b/examples/code-llama/34b/lora.yml index 6024ce3f7d..25dc9f421e 100644 --- a/examples/code-llama/34b/lora.yml +++ b/examples/code-llama/34b/lora.yml @@ -7,7 +7,6 @@ tokenizer_type: CodeLlamaTokenizer load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/code-llama/34b/qlora.yml b/examples/code-llama/34b/qlora.yml index 56c276cc90..0e33e2a45f 100644 --- a/examples/code-llama/34b/qlora.yml +++ b/examples/code-llama/34b/qlora.yml @@ -7,7 +7,6 @@ tokenizer_type: CodeLlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/code-llama/7b/lora.yml b/examples/code-llama/7b/lora.yml index 0eb20c2449..d288b9f65a 100644 --- a/examples/code-llama/7b/lora.yml +++ b/examples/code-llama/7b/lora.yml @@ -7,7 +7,6 @@ tokenizer_type: CodeLlamaTokenizer load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/code-llama/7b/qlora.yml b/examples/code-llama/7b/qlora.yml index f078f13984..de41c01232 100644 --- a/examples/code-llama/7b/qlora.yml +++ b/examples/code-llama/7b/qlora.yml @@ -7,7 +7,6 @@ tokenizer_type: CodeLlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/cohere/command-r-7b-qlora.yml b/examples/cohere/command-r-7b-qlora.yml index 8a2b6eacda..4a30e9a776 100644 --- a/examples/cohere/command-r-7b-qlora.yml +++ b/examples/cohere/command-r-7b-qlora.yml @@ -4,7 +4,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: false load_in_4bit: true -strict: false # huggingface repo chat_template: cohere diff --git a/examples/dbrx/16bit-lora.yaml b/examples/dbrx/16bit-lora.yaml index 1724c14265..852654d496 100644 --- a/examples/dbrx/16bit-lora.yaml +++ b/examples/dbrx/16bit-lora.yaml @@ -3,7 +3,6 @@ base_model: LnL-AI/dbrx-base-converted-v2 # hub_model_id: username/custom_model_name trust_remote_code: true -strict: false datasets: - path: tatsu-lab/alpaca diff --git a/examples/dbrx/8bit-lora.yaml b/examples/dbrx/8bit-lora.yaml index 308483adf0..0b9402194f 100644 --- a/examples/dbrx/8bit-lora.yaml +++ b/examples/dbrx/8bit-lora.yaml @@ -6,7 +6,6 @@ trust_remote_code: true load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: tatsu-lab/alpaca diff --git a/examples/dbrx/fft-ds-zero3.yaml b/examples/dbrx/fft-ds-zero3.yaml index 0fbb5b0680..e42c166734 100644 --- a/examples/dbrx/fft-ds-zero3.yaml +++ b/examples/dbrx/fft-ds-zero3.yaml @@ -3,7 +3,6 @@ base_model: LnL-AI/dbrx-base-converted-v2 # hub_model_id: username/custom_model_name trust_remote_code: true -strict: false datasets: - path: tatsu-lab/alpaca diff --git a/examples/deepseek-v2/fft-fsdp-16b.yaml b/examples/deepseek-v2/fft-fsdp-16b.yaml index 3fe8691a36..0ed97db369 100644 --- a/examples/deepseek-v2/fft-fsdp-16b.yaml +++ b/examples/deepseek-v2/fft-fsdp-16b.yaml @@ -2,7 +2,6 @@ base_model: deepseek-ai/DeepSeek-V2-Lite # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name trust_remote_code: true -strict: false datasets: - path: tatsu-lab/alpaca diff --git a/examples/deepseek-v2/qlora-fsdp-2_5.yaml b/examples/deepseek-v2/qlora-fsdp-2_5.yaml index a554970b6e..34dbeaafed 100644 --- a/examples/deepseek-v2/qlora-fsdp-2_5.yaml +++ b/examples/deepseek-v2/qlora-fsdp-2_5.yaml @@ -6,7 +6,6 @@ trust_remote_code: true load_in_8bit: false load_in_4bit: true -strict: false plugins: diff --git a/examples/falcon/config-7b-lora.yml b/examples/falcon/config-7b-lora.yml index 2d9240e8bd..391d4dd94f 100644 --- a/examples/falcon/config-7b-lora.yml +++ b/examples/falcon/config-7b-lora.yml @@ -11,7 +11,6 @@ trust_remote_code: true load_in_8bit: true load_in_4bit: false gptq: false -strict: false push_dataset_to_hub: datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/falcon/config-7b-qlora.yml b/examples/falcon/config-7b-qlora.yml index 78323db5fa..a9af8574c3 100644 --- a/examples/falcon/config-7b-qlora.yml +++ b/examples/falcon/config-7b-qlora.yml @@ -15,7 +15,6 @@ load_in_8bit: false # enable 4bit for QLoRA load_in_4bit: true gptq: false -strict: false push_dataset_to_hub: datasets: - path: QingyiSi/Alpaca-CoT diff --git a/examples/falcon/config-7b.yml b/examples/falcon/config-7b.yml index a796b89dd0..3cc553daab 100644 --- a/examples/falcon/config-7b.yml +++ b/examples/falcon/config-7b.yml @@ -8,7 +8,6 @@ tokenizer_type: AutoTokenizer # required by falcon custom model code: https://huggingface.co/tiiuae/falcon-7b/tree/main trust_remote_code: true gptq: false -strict: false push_dataset_to_hub: datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/gemma/qlora.yml b/examples/gemma/qlora.yml index 505564269d..2738112b41 100644 --- a/examples/gemma/qlora.yml +++ b/examples/gemma/qlora.yml @@ -8,7 +8,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: false load_in_4bit: true -strict: false # huggingface repo datasets: diff --git a/examples/gemma2/qlora.yml b/examples/gemma2/qlora.yml index afba83552e..cb96a32c1d 100644 --- a/examples/gemma2/qlora.yml +++ b/examples/gemma2/qlora.yml @@ -7,7 +7,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: false load_in_4bit: true -strict: false # huggingface repo chat_template: gemma diff --git a/examples/gemma2/reward-model.yaml b/examples/gemma2/reward-model.yaml index d828af939f..ce01a4572e 100644 --- a/examples/gemma2/reward-model.yaml +++ b/examples/gemma2/reward-model.yaml @@ -5,7 +5,6 @@ num_labels: 1 tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false reward_model: true chat_template: gemma diff --git a/examples/gemma3/gemma-3-1b-qlora.yml b/examples/gemma3/gemma-3-1b-qlora.yml index 732b914a88..44310558c2 100644 --- a/examples/gemma3/gemma-3-1b-qlora.yml +++ b/examples/gemma3/gemma-3-1b-qlora.yml @@ -10,7 +10,6 @@ ddp_find_unused_parameters: true load_in_8bit: false load_in_4bit: true -strict: false # huggingface repo chat_template: gemma3 diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml index 85e5dce685..29f8cc1e1d 100644 --- a/examples/gemma3/gemma-3-4b-qlora.yml +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -1,5 +1,4 @@ base_model: google/gemma-3-4b-it -strict: false load_in_4bit: true diff --git a/examples/gemma3/gemma-3-4b-vision-qlora.yml b/examples/gemma3/gemma-3-4b-vision-qlora.yml index 92273380c6..3fd9eb5f0b 100644 --- a/examples/gemma3/gemma-3-4b-vision-qlora.yml +++ b/examples/gemma3/gemma-3-4b-vision-qlora.yml @@ -1,6 +1,5 @@ base_model: google/gemma-3-4b-it processor_type: AutoProcessor -strict: false load_in_4bit: true diff --git a/examples/gptj/qlora.yml b/examples/gptj/qlora.yml index 086d425b51..c3cf9f9738 100644 --- a/examples/gptj/qlora.yml +++ b/examples/gptj/qlora.yml @@ -4,7 +4,6 @@ base_model: EleutherAI/gpt-j-6b load_in_8bit: false load_in_4bit: true -strict: false push_dataset_to_hub: datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/jamba/qlora.yaml b/examples/jamba/qlora.yaml index 7d642cb0a1..2cb0eea411 100644 --- a/examples/jamba/qlora.yaml +++ b/examples/jamba/qlora.yaml @@ -6,7 +6,6 @@ trust_remote_code: true load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/jamba/qlora_deepspeed.yaml b/examples/jamba/qlora_deepspeed.yaml index d983dc3916..d13ce64839 100644 --- a/examples/jamba/qlora_deepspeed.yaml +++ b/examples/jamba/qlora_deepspeed.yaml @@ -5,7 +5,6 @@ trust_remote_code: true load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/jamba/qlora_fsdp_large.yaml b/examples/jamba/qlora_fsdp_large.yaml index a968d99d7a..6badaba19b 100644 --- a/examples/jamba/qlora_fsdp_large.yaml +++ b/examples/jamba/qlora_fsdp_large.yaml @@ -5,7 +5,6 @@ tokenizer_type: AutoTokenizer # hub_model_id: username/custom_model_name load_in_4bit: true -strict: false use_tensorboard: true chat_template: jamba datasets: diff --git a/examples/llama-2/fft_optimized.yml b/examples/llama-2/fft_optimized.yml index fd78fbfeee..86b1b6a218 100644 --- a/examples/llama-2/fft_optimized.yml +++ b/examples/llama-2/fft_optimized.yml @@ -4,7 +4,6 @@ model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/llama-2/gptq-lora.yml b/examples/llama-2/gptq-lora.yml index ad2dbd9cf7..0f1b34016c 100644 --- a/examples/llama-2/gptq-lora.yml +++ b/examples/llama-2/gptq-lora.yml @@ -10,7 +10,6 @@ gptq_disable_exllama: true tokenizer_use_fast: true tokenizer_legacy: true -strict: false push_dataset_to_hub: hf_use_auth_token: true datasets: diff --git a/examples/llama-2/lisa.yml b/examples/llama-2/lisa.yml index 585fa94285..a76a792aef 100644 --- a/examples/llama-2/lisa.yml +++ b/examples/llama-2/lisa.yml @@ -4,7 +4,6 @@ model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/llama-2/loftq.yml b/examples/llama-2/loftq.yml index bf32c7b275..22dbf2d992 100644 --- a/examples/llama-2/loftq.yml +++ b/examples/llama-2/loftq.yml @@ -4,7 +4,6 @@ model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/llama-2/lora.yml b/examples/llama-2/lora.yml index 3ef607ab48..679aed3a99 100644 --- a/examples/llama-2/lora.yml +++ b/examples/llama-2/lora.yml @@ -7,7 +7,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/llama-2/qlora-fsdp.yml b/examples/llama-2/qlora-fsdp.yml index 759f080246..a42eabd4b7 100644 --- a/examples/llama-2/qlora-fsdp.yml +++ b/examples/llama-2/qlora-fsdp.yml @@ -7,7 +7,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: yahma/alpaca-cleaned diff --git a/examples/llama-2/qlora.yml b/examples/llama-2/qlora.yml index c678a00423..de65928bc8 100644 --- a/examples/llama-2/qlora.yml +++ b/examples/llama-2/qlora.yml @@ -7,7 +7,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/llama-2/relora.yml b/examples/llama-2/relora.yml index 6c943d009f..e0a5f7068c 100644 --- a/examples/llama-2/relora.yml +++ b/examples/llama-2/relora.yml @@ -5,7 +5,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/llama-3-vision/lora-11b.yaml b/examples/llama-3-vision/lora-11b.yaml index 4431878fa3..f4883e903d 100644 --- a/examples/llama-3-vision/lora-11b.yaml +++ b/examples/llama-3-vision/lora-11b.yaml @@ -4,7 +4,6 @@ processor_type: AutoProcessor # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false # these 3 lines are needed for now to handle vision chat templates w images skip_prepare_dataset: true diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml index 50169879c8..eccfa6d8c1 100644 --- a/examples/llama-3/fft-8b-liger-fsdp.yaml +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -9,7 +9,6 @@ liger_rms_norm: true liger_glu_activation: true liger_fused_linear_cross_entropy: true -strict: false chat_template: llama3 datasets: diff --git a/examples/llama-3/fft-8b.yaml b/examples/llama-3/fft-8b.yaml index 4452a6e3d6..fdae3e6c4d 100644 --- a/examples/llama-3/fft-8b.yaml +++ b/examples/llama-3/fft-8b.yaml @@ -1,7 +1,6 @@ base_model: NousResearch/Meta-Llama-3.1-8B # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: tatsu-lab/alpaca diff --git a/examples/llama-3/instruct-dpo-lora-8b.yml b/examples/llama-3/instruct-dpo-lora-8b.yml index a1b923fb60..13082294f8 100644 --- a/examples/llama-3/instruct-dpo-lora-8b.yml +++ b/examples/llama-3/instruct-dpo-lora-8b.yml @@ -7,7 +7,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: true load_in_4bit: false -strict: false chat_template: llama3 rl: dpo diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml index 362bda9aa3..acab862f64 100644 --- a/examples/llama-3/instruct-lora-8b.yml +++ b/examples/llama-3/instruct-lora-8b.yml @@ -7,7 +7,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: true load_in_4bit: false -strict: false chat_template: llama3 datasets: diff --git a/examples/llama-3/lora-1b-deduplicate-dpo.yml b/examples/llama-3/lora-1b-deduplicate-dpo.yml index e4b2a5244b..10e9747cb1 100644 --- a/examples/llama-3/lora-1b-deduplicate-dpo.yml +++ b/examples/llama-3/lora-1b-deduplicate-dpo.yml @@ -7,7 +7,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: true load_in_4bit: false -strict: false chat_template: llama3 rl: dpo diff --git a/examples/llama-3/lora-1b-deduplicate-sft.yml b/examples/llama-3/lora-1b-deduplicate-sft.yml index b8c21fafb3..630ec92f6a 100644 --- a/examples/llama-3/lora-1b-deduplicate-sft.yml +++ b/examples/llama-3/lora-1b-deduplicate-sft.yml @@ -7,7 +7,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/llama-3/lora-1b-kernels.yml b/examples/llama-3/lora-1b-kernels.yml index b76f03801e..a2d07ca491 100644 --- a/examples/llama-3/lora-1b-kernels.yml +++ b/examples/llama-3/lora-1b-kernels.yml @@ -1,7 +1,6 @@ base_model: NousResearch/Llama-3.2-1B # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/llama-3/lora-1b-ray.yml b/examples/llama-3/lora-1b-ray.yml index 199fe3b5d3..bb23164ebb 100644 --- a/examples/llama-3/lora-1b-ray.yml +++ b/examples/llama-3/lora-1b-ray.yml @@ -1,7 +1,6 @@ base_model: NousResearch/Llama-3.2-1B # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/llama-3/lora-1b-sample-packing-sequentially.yml b/examples/llama-3/lora-1b-sample-packing-sequentially.yml index a027673ab2..769dd32e60 100644 --- a/examples/llama-3/lora-1b-sample-packing-sequentially.yml +++ b/examples/llama-3/lora-1b-sample-packing-sequentially.yml @@ -7,7 +7,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/llama-3/lora-1b.yml b/examples/llama-3/lora-1b.yml index 8a536260a9..c31a9f39a4 100644 --- a/examples/llama-3/lora-1b.yml +++ b/examples/llama-3/lora-1b.yml @@ -1,7 +1,6 @@ base_model: NousResearch/Llama-3.2-1B # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/llama-3/lora-8b.yml b/examples/llama-3/lora-8b.yml index 700dd46143..ad50cd38a3 100644 --- a/examples/llama-3/lora-8b.yml +++ b/examples/llama-3/lora-8b.yml @@ -7,7 +7,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/llama-3/qlora-1b-kto.yaml b/examples/llama-3/qlora-1b-kto.yaml index 0dc37b40a8..89a51ea68f 100644 --- a/examples/llama-3/qlora-1b-kto.yaml +++ b/examples/llama-3/qlora-1b-kto.yaml @@ -4,7 +4,6 @@ base_model: meta-llama/Llama-3.2-1B load_in_8bit: false load_in_4bit: true -strict: false rl: kto rl_beta: 0.5 diff --git a/examples/llama-3/qlora-1b.yml b/examples/llama-3/qlora-1b.yml index c42dd22384..5c8fe66289 100644 --- a/examples/llama-3/qlora-1b.yml +++ b/examples/llama-3/qlora-1b.yml @@ -4,7 +4,6 @@ base_model: NousResearch/Llama-3.2-1B load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/llama-3/qlora-fsdp-405b.yaml b/examples/llama-3/qlora-fsdp-405b.yaml index 75c8f59735..2b7d51925c 100644 --- a/examples/llama-3/qlora-fsdp-405b.yaml +++ b/examples/llama-3/qlora-fsdp-405b.yaml @@ -5,7 +5,6 @@ tokenizer_type: AutoTokenizer # hub_model_id: username/custom_model_name load_in_4bit: true -strict: false datasets: - path: tatsu-lab/alpaca diff --git a/examples/llama-3/qlora-fsdp-70b.yaml b/examples/llama-3/qlora-fsdp-70b.yaml index c4889d6438..412b6721ca 100644 --- a/examples/llama-3/qlora-fsdp-70b.yaml +++ b/examples/llama-3/qlora-fsdp-70b.yaml @@ -7,7 +7,6 @@ tokenizer_type: AutoTokenizer # PreTrainedTokenizerFast load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: tatsu-lab/alpaca diff --git a/examples/llama-3/qlora.yml b/examples/llama-3/qlora.yml index 607deb896c..4cc9fc3dba 100644 --- a/examples/llama-3/qlora.yml +++ b/examples/llama-3/qlora.yml @@ -7,7 +7,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: aaditya/alpaca_subset_1 diff --git a/examples/llama-4/maverick-qlora-fsdp1.yaml b/examples/llama-4/maverick-qlora-fsdp1.yaml index 232afc73eb..2be94f4efa 100644 --- a/examples/llama-4/maverick-qlora-fsdp1.yaml +++ b/examples/llama-4/maverick-qlora-fsdp1.yaml @@ -3,7 +3,6 @@ model_type: Llama4ForConditionalGeneration # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false plugins: - axolotl.integrations.liger.LigerPlugin diff --git a/examples/llama-4/scout-qlora-fsdp1.yaml b/examples/llama-4/scout-qlora-fsdp1.yaml index ad2e46786e..eeae872a6b 100644 --- a/examples/llama-4/scout-qlora-fsdp1.yaml +++ b/examples/llama-4/scout-qlora-fsdp1.yaml @@ -3,7 +3,6 @@ model_type: Llama4ForConditionalGeneration # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false # torch_compile: true plugins: diff --git a/examples/llama-4/scout-qlora-single-h100.yaml b/examples/llama-4/scout-qlora-single-h100.yaml index 23a3a2195c..17ad706344 100644 --- a/examples/llama-4/scout-qlora-single-h100.yaml +++ b/examples/llama-4/scout-qlora-single-h100.yaml @@ -3,7 +3,6 @@ model_type: Llama4ForConditionalGeneration # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false plugins: - axolotl.integrations.liger.LigerPlugin diff --git a/examples/llama-4/scout-vision-qlora-fsdp.yaml b/examples/llama-4/scout-vision-qlora-fsdp.yaml index 8b8c9abd10..eff708e4db 100644 --- a/examples/llama-4/scout-vision-qlora-fsdp.yaml +++ b/examples/llama-4/scout-vision-qlora-fsdp.yaml @@ -4,7 +4,6 @@ processor_type: Llama4Processor # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false # these 3 lines are needed for now to handle vision chat templates w images skip_prepare_dataset: true diff --git a/examples/llava/lora-7b.yaml b/examples/llava/lora-7b.yaml index 68e463585c..54edd04dcc 100644 --- a/examples/llava/lora-7b.yaml +++ b/examples/llava/lora-7b.yaml @@ -1,6 +1,5 @@ base_model: llava-hf/llava-1.5-7b-hf processor_type: AutoProcessor -strict: false # these 3 lines are needed for now to handle vision chat templates w images skip_prepare_dataset: true diff --git a/examples/mamba/config.yml b/examples/mamba/config.yml index 3a114bac77..3d4583932e 100644 --- a/examples/mamba/config.yml +++ b/examples/mamba/config.yml @@ -5,7 +5,6 @@ tokenizer_type: AutoTokenizer tokenizer_config: EleutherAI/gpt-neox-20b # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/mistral/bigstral-ds-zero3.yaml b/examples/mistral/bigstral-ds-zero3.yaml index bef9899326..f626a92a17 100644 --- a/examples/mistral/bigstral-ds-zero3.yaml +++ b/examples/mistral/bigstral-ds-zero3.yaml @@ -6,7 +6,6 @@ tokenizer_type: LlamaTokenizer # hub_model_id: username/custom_model_name trust_remote_code: true -strict: false unfrozen_parameters: - ^lm_head.weight$ diff --git a/examples/mistral/config.yml b/examples/mistral/config.yml index c58e2c9543..15edffb44e 100644 --- a/examples/mistral/config.yml +++ b/examples/mistral/config.yml @@ -4,7 +4,6 @@ model_type: MistralForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/mistral/lora-mps.yml b/examples/mistral/lora-mps.yml index ba61cac11d..e6f46affb1 100644 --- a/examples/mistral/lora-mps.yml +++ b/examples/mistral/lora-mps.yml @@ -4,7 +4,6 @@ model_type: MistralForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/mistral/lora.yml b/examples/mistral/lora.yml index b55c66715a..9af4274fdf 100644 --- a/examples/mistral/lora.yml +++ b/examples/mistral/lora.yml @@ -7,7 +7,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/mistral/mistral-dpo-qlora.yml b/examples/mistral/mistral-dpo-qlora.yml index 6446cacb84..af707973ff 100644 --- a/examples/mistral/mistral-dpo-qlora.yml +++ b/examples/mistral/mistral-dpo-qlora.yml @@ -12,7 +12,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false chat_template: chatml rl: dpo diff --git a/examples/mistral/mistral-qlora-fsdp.yml b/examples/mistral/mistral-qlora-fsdp.yml index 5825ac749c..e234b19a24 100644 --- a/examples/mistral/mistral-qlora-fsdp.yml +++ b/examples/mistral/mistral-qlora-fsdp.yml @@ -9,7 +9,6 @@ trust_remote_code: true load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: tatsu-lab/alpaca diff --git a/examples/mistral/mistral-qlora-orpo.yml b/examples/mistral/mistral-qlora-orpo.yml index 9c6ae74ef6..6c0212b7cb 100644 --- a/examples/mistral/mistral-qlora-orpo.yml +++ b/examples/mistral/mistral-qlora-orpo.yml @@ -7,7 +7,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false rl: orpo orpo_alpha: 0.1 diff --git a/examples/mistral/mistral-small-3.1-24B-lora.yml b/examples/mistral/mistral-small-3.1-24B-lora.yml index 0e6b4402d4..198b3f3737 100644 --- a/examples/mistral/mistral-small-3.1-24B-lora.yml +++ b/examples/mistral/mistral-small-3.1-24B-lora.yml @@ -1,6 +1,5 @@ base_model: mistralai/Mistral-Small-3.1-24B-Instruct-2503 processor_type: AutoProcessor -strict: false load_in_8bit: true diff --git a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml index e29b6392ad..af6ba5a769 100644 --- a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml +++ b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml @@ -7,7 +7,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: tatsu-lab/alpaca diff --git a/examples/mistral/mixtral-qlora-fsdp.yml b/examples/mistral/mixtral-qlora-fsdp.yml index 40bb5d5d14..b1843a138d 100644 --- a/examples/mistral/mixtral-qlora-fsdp.yml +++ b/examples/mistral/mixtral-qlora-fsdp.yml @@ -9,7 +9,6 @@ trust_remote_code: true load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: tatsu-lab/alpaca diff --git a/examples/mistral/mixtral.yml b/examples/mistral/mixtral.yml index eefd2456d0..4c256420cf 100644 --- a/examples/mistral/mixtral.yml +++ b/examples/mistral/mixtral.yml @@ -9,7 +9,6 @@ trust_remote_code: true load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: tatsu-lab/alpaca diff --git a/examples/mistral/mixtral_22.yml b/examples/mistral/mixtral_22.yml index 1debd793ad..25e1d71551 100644 --- a/examples/mistral/mixtral_22.yml +++ b/examples/mistral/mixtral_22.yml @@ -6,7 +6,6 @@ tokenizer_type: LlamaTokenizer # hub_model_id: username/custom_model_name trust_remote_code: true -strict: false unfrozen_parameters: - ^lm_head.weight$ diff --git a/examples/mistral/qlora.yml b/examples/mistral/qlora.yml index 921f3fe7bc..607e337014 100644 --- a/examples/mistral/qlora.yml +++ b/examples/mistral/qlora.yml @@ -7,7 +7,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/openllama-3b/config.yml b/examples/openllama-3b/config.yml index 7a1e2d9a57..17eeb73aec 100644 --- a/examples/openllama-3b/config.yml +++ b/examples/openllama-3b/config.yml @@ -4,7 +4,6 @@ model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false push_dataset_to_hub: datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/openllama-3b/lora.yml b/examples/openllama-3b/lora.yml index c1c597b73c..073117f114 100644 --- a/examples/openllama-3b/lora.yml +++ b/examples/openllama-3b/lora.yml @@ -7,7 +7,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: true load_in_4bit: false -strict: false push_dataset_to_hub: datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/openllama-3b/qlora.yml b/examples/openllama-3b/qlora.yml index e9c71efd10..b4fca2c073 100644 --- a/examples/openllama-3b/qlora.yml +++ b/examples/openllama-3b/qlora.yml @@ -7,7 +7,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false push_dataset_to_hub: datasets: - path: teknium/GPT4-LLM-Cleaned diff --git a/examples/phi/lora-3.5.yaml b/examples/phi/lora-3.5.yaml index 2ecb9d28db..ad4ce9cd44 100644 --- a/examples/phi/lora-3.5.yaml +++ b/examples/phi/lora-3.5.yaml @@ -7,7 +7,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: true load_in_4bit: false -strict: false chat_template: phi_3 datasets: diff --git a/examples/phi/phi-ft.yml b/examples/phi/phi-ft.yml index 886671a60c..1562a73536 100644 --- a/examples/phi/phi-ft.yml +++ b/examples/phi/phi-ft.yml @@ -4,7 +4,6 @@ model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: garage-bAInd/Open-Platypus diff --git a/examples/phi/phi-qlora.yml b/examples/phi/phi-qlora.yml index a1cbf8a52c..4cd53db979 100644 --- a/examples/phi/phi-qlora.yml +++ b/examples/phi/phi-qlora.yml @@ -7,7 +7,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: garage-bAInd/Open-Platypus diff --git a/examples/phi/phi2-ft.yml b/examples/phi/phi2-ft.yml index 480017a39d..ca733cc71b 100644 --- a/examples/phi/phi2-ft.yml +++ b/examples/phi/phi2-ft.yml @@ -4,7 +4,6 @@ model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: garage-bAInd/Open-Platypus diff --git a/examples/phi/phi3-ft-fsdp.yml b/examples/phi/phi3-ft-fsdp.yml index 766db76f6b..d0d14fea67 100644 --- a/examples/phi/phi3-ft-fsdp.yml +++ b/examples/phi/phi3-ft-fsdp.yml @@ -4,7 +4,6 @@ model_type: AutoModelForCausalLM tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/phi/phi3-ft.yml b/examples/phi/phi3-ft.yml index 0a5da2e232..17c48da6f7 100644 --- a/examples/phi/phi3-ft.yml +++ b/examples/phi/phi3-ft.yml @@ -7,7 +7,6 @@ tokenizer_type: AutoTokenizer # hub_model_id: username/custom_model_name chat_template: phi_3 -strict: false datasets: - path: garage-bAInd/Open-Platypus diff --git a/examples/pixtral/lora-12b.yml b/examples/pixtral/lora-12b.yml index d3b3efd70f..dec8e4b5e3 100644 --- a/examples/pixtral/lora-12b.yml +++ b/examples/pixtral/lora-12b.yml @@ -1,6 +1,5 @@ base_model: mistral-community/pixtral-12b processor_type: AutoProcessor -strict: false # these 3 lines are needed for now to handle vision chat templates w images skip_prepare_dataset: true diff --git a/examples/qwen/lora.yml b/examples/qwen/lora.yml index 23385d236f..9a28432360 100644 --- a/examples/qwen/lora.yml +++ b/examples/qwen/lora.yml @@ -9,7 +9,6 @@ trust_remote_code: true load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/qwen/qlora.yml b/examples/qwen/qlora.yml index 854a682fe4..5f85b44dd1 100644 --- a/examples/qwen/qlora.yml +++ b/examples/qwen/qlora.yml @@ -9,7 +9,6 @@ trust_remote_code: true load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/qwen/qwen2-moe-lora.yaml b/examples/qwen/qwen2-moe-lora.yaml index a2a1e4d257..afce443a01 100644 --- a/examples/qwen/qwen2-moe-lora.yaml +++ b/examples/qwen/qwen2-moe-lora.yaml @@ -3,7 +3,6 @@ base_model: Qwen/Qwen1.5-MoE-A2.7B # hub_model_id: username/custom_model_name trust_remote_code: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/qwen/qwen2-moe-qlora.yaml b/examples/qwen/qwen2-moe-qlora.yaml index b1ab131a66..92a6842cfc 100644 --- a/examples/qwen/qwen2-moe-qlora.yaml +++ b/examples/qwen/qwen2-moe-qlora.yaml @@ -6,7 +6,6 @@ trust_remote_code: true load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/qwen2-vl/lora-7b.yaml b/examples/qwen2-vl/lora-7b.yaml index 3d0b10adfa..55773bc3d6 100644 --- a/examples/qwen2-vl/lora-7b.yaml +++ b/examples/qwen2-vl/lora-7b.yaml @@ -1,6 +1,5 @@ base_model: Qwen/Qwen2-VL-7B-Instruct processor_type: AutoProcessor -strict: false # these 3 lines are needed for now to handle vision chat templates w images skip_prepare_dataset: true diff --git a/examples/qwen2/dpo.yaml b/examples/qwen2/dpo.yaml index 8df7f9dc49..3547c6c98d 100644 --- a/examples/qwen2/dpo.yaml +++ b/examples/qwen2/dpo.yaml @@ -2,7 +2,6 @@ base_model: Qwen/Qwen2.5-0.5B # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false chat_template: qwen_25 rl: dpo diff --git a/examples/qwen2/prm.yaml b/examples/qwen2/prm.yaml index 669f8e2db8..4afa24f3ce 100644 --- a/examples/qwen2/prm.yaml +++ b/examples/qwen2/prm.yaml @@ -5,7 +5,6 @@ num_labels: 2 tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false process_reward_model: true chat_template: diff --git a/examples/qwen2/qlora-fsdp.yaml b/examples/qwen2/qlora-fsdp.yaml index 1f2ed83b10..ed2670ab61 100644 --- a/examples/qwen2/qlora-fsdp.yaml +++ b/examples/qwen2/qlora-fsdp.yaml @@ -6,7 +6,6 @@ trust_remote_code: true load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: tatsu-lab/alpaca diff --git a/examples/qwen2/reward-model.yaml b/examples/qwen2/reward-model.yaml index fcbb9867ea..822407a1fe 100644 --- a/examples/qwen2/reward-model.yaml +++ b/examples/qwen2/reward-model.yaml @@ -5,7 +5,6 @@ num_labels: 1 tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false reward_model: true chat_template: qwen_25 diff --git a/examples/stablelm-2/1.6b/fft.yml b/examples/stablelm-2/1.6b/fft.yml index f26714856e..9b45b399f8 100644 --- a/examples/stablelm-2/1.6b/fft.yml +++ b/examples/stablelm-2/1.6b/fft.yml @@ -6,7 +6,6 @@ tokenizer_type: AutoTokenizer # hub_model_id: username/custom_model_name trust_remote_code: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/stablelm-2/1.6b/lora.yml b/examples/stablelm-2/1.6b/lora.yml index aaa9908d1a..31e5ad9336 100644 --- a/examples/stablelm-2/1.6b/lora.yml +++ b/examples/stablelm-2/1.6b/lora.yml @@ -9,7 +9,6 @@ trust_remote_code: true load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/starcoder2/qlora.yml b/examples/starcoder2/qlora.yml index 8b21d71450..18d85f9c38 100644 --- a/examples/starcoder2/qlora.yml +++ b/examples/starcoder2/qlora.yml @@ -4,7 +4,6 @@ base_model: bigcode/starcoder2-3b load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/tiny-llama/lora-mps.yml b/examples/tiny-llama/lora-mps.yml index 8654a39bba..66cf7cfb3e 100644 --- a/examples/tiny-llama/lora-mps.yml +++ b/examples/tiny-llama/lora-mps.yml @@ -7,7 +7,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/tiny-llama/lora.yml b/examples/tiny-llama/lora.yml index 64b2360d30..90998880f7 100644 --- a/examples/tiny-llama/lora.yml +++ b/examples/tiny-llama/lora.yml @@ -6,7 +6,6 @@ tokenizer_type: AutoTokenizer load_in_8bit: true load_in_4bit: false -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/tiny-llama/pretrain.yml b/examples/tiny-llama/pretrain.yml index 2984c52ae9..5b3706bcbe 100644 --- a/examples/tiny-llama/pretrain.yml +++ b/examples/tiny-llama/pretrain.yml @@ -4,7 +4,6 @@ model_type: LlamaForCausalLM tokenizer_type: LlamaTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -strict: false max_steps: 200 pretraining_dataset: diff --git a/examples/tiny-llama/qlora.yml b/examples/tiny-llama/qlora.yml index 79e3164a50..8b2a4565a7 100644 --- a/examples/tiny-llama/qlora.yml +++ b/examples/tiny-llama/qlora.yml @@ -7,7 +7,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false datasets: - path: mhenrichsen/alpaca_2k_test diff --git a/examples/xgen-7b/xgen-7b-8k-qlora.yml b/examples/xgen-7b/xgen-7b-8k-qlora.yml index f4ff589e06..48066b1304 100644 --- a/examples/xgen-7b/xgen-7b-8k-qlora.yml +++ b/examples/xgen-7b/xgen-7b-8k-qlora.yml @@ -13,7 +13,6 @@ load_in_8bit: false # enable 4bit for QLoRA load_in_4bit: true gptq: false -strict: false push_dataset_to_hub: datasets: - path: timdettmers/openassistant-guanaco diff --git a/examples/yi-34B-chat/qlora.yml b/examples/yi-34B-chat/qlora.yml index de79ed6cee..a0a95d86fe 100644 --- a/examples/yi-34B-chat/qlora.yml +++ b/examples/yi-34B-chat/qlora.yml @@ -7,7 +7,6 @@ tokenizer_type: LlamaTokenizer load_in_8bit: false load_in_4bit: true -strict: false sequence_len: 1024 bf16: auto tf32: false From e4307fb7d796b33ccf8ff45a1a92ea6bf83a1890 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 12 Apr 2025 21:25:23 +0700 Subject: [PATCH 0550/1405] feat: add examples for deepcoder (#2517) --- .../deepcoder/deepcoder-14B-preview-lora.yml | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 examples/deepcoder/deepcoder-14B-preview-lora.yml diff --git a/examples/deepcoder/deepcoder-14B-preview-lora.yml b/examples/deepcoder/deepcoder-14B-preview-lora.yml new file mode 100644 index 0000000000..9e92c0a072 --- /dev/null +++ b/examples/deepcoder/deepcoder-14B-preview-lora.yml @@ -0,0 +1,58 @@ +base_model: agentica-org/DeepCoder-14B-Preview +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: true +load_in_4bit: false +strict: false + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + field_messages: messages + message_property_mappings: + role: role + content: content + +dataset_prepared_path: +val_set_size: 0.05 +output_dir: ./outputs/lora-out + +sequence_len: 4096 +sample_packing: true +eval_sample_packing: false +pad_to_sequence_len: true + +adapter: lora +lora_model_dir: +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 2 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_steps: 10 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: From 198d775d6d0307a8c168ff762339c4904e233a3f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 15 Apr 2025 22:15:42 -0700 Subject: [PATCH 0551/1405] make sure the all of the model is on the same device, so this test will pass on multigpu (#2524) [skip ci] --- tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index 69c84f4453..bada305b31 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -144,7 +144,7 @@ def test_swiglu_mlp_integration(small_llama_model): def test_geglu_model_integration(): """Test GeGLU activation with Gemma model.""" model = AutoModelForCausalLM.from_pretrained( - "mhenrichsen/gemma-2b", torch_dtype=torch.float16, device_map="auto" + "mhenrichsen/gemma-2b", torch_dtype=torch.float16, device_map="cuda:0" ) peft_config = get_peft_config( { @@ -347,7 +347,7 @@ def test_model_architecture(model_config): """Test LoRA kernel patches across different model architectures.""" # Load model with appropriate dtype model = AutoModelForCausalLM.from_pretrained( - model_config["name"], torch_dtype=model_config["dtype"], device_map="auto" + model_config["name"], torch_dtype=model_config["dtype"], device_map="cuda:0" ) # Apply LoRA configuration From 271b24cccceb8f62e4706bd4f8c43748276c7545 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 16 Apr 2025 12:17:10 +0700 Subject: [PATCH 0552/1405] feat: update cce to latest (#2521) --- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/integrations/cut_cross_entropy/README.md | 2 +- src/axolotl/integrations/cut_cross_entropy/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 396f4a655a..bc6213dd97 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -25,5 +25,5 @@ print( UNINSTALL_PREFIX - + 'pip install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@24fbe4b5dab9a6c250a014573613c1890190536c"' + + 'pip install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@bad6f7b49c75fdec69471abb71b4cddd0f0c6438"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 29b91bc8d0..618af466f8 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -17,7 +17,7 @@ Run the following command to install `cut_cross_entropy[transformers]` if you do python scripts/cutcrossentropy_install.py | sh # if you are not in dev environment -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@24fbe4b5dab9a6c250a014573613c1890190536c" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@bad6f7b49c75fdec69471abb71b4cddd0f0c6438" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 19faf85e67..4792adb6f7 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -33,7 +33,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@24fbe4b5dab9a6c250a014573613c1890190536c"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@bad6f7b49c75fdec69471abb71b4cddd0f0c6438"`' ) From 682a9cf79bc6a5fc4a699f2b63b00c7b2e59376d Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 16 Apr 2025 13:31:39 +0700 Subject: [PATCH 0553/1405] Fix: add delinearization and make qlora work with fsdp2 (#2515) * fixes for delinearization, and make qlora work with fsdp2 * Add back mistakenly removed lm_eval * typo [skip ci] * patch evals for torch.compile + fsdp2 * also check torch_compile w fsdp2 * lots of fixes for flex attn with llama4 * fix patch check and patch llama4 too * attempt to make the patches stick * use transformers 4.51.2 * update configs and README for llama4 * remove torch.compile for CI test * cleanup any existing singletons * set singleton cache to None instead of deleting * use importlib reload with monkeypatch * don't worry about transformers version, mark inputs with grads, fix regex * make sure embeds aren't on cpu * logging and mem improvements * vllm version and add to docker, make sure to save processor on conversion * fix ambiguous tensor bool check * fix vllm to not use v1, upgrade hf transformers * fix tests * make flex_attn_compile_kwargs configurable, since this depends on model params --------- Co-authored-by: Wing Lian Co-authored-by: Salman Mohammadi --- .github/workflows/main.yml | 2 +- examples/llama-4/README.md | 26 ++- .../maverick-qlora-fsdp1.yaml | 0 .../scout-qlora-fsdp1.yaml | 0 .../scout-qlora-single-h100.yaml | 0 .../scout-vision-qlora-fsdp.yaml | 0 .../llama-4/scout-qlora-flexattn-fsdp2.yaml | 86 ++++++++++ .../llama-4/scout-qlora-single-h100-flex.yaml | 85 ++++++++++ .../scout-vision-qlora-fsdp2-flex.yaml | 89 ++++++++++ requirements.txt | 2 +- setup.py | 2 +- src/axolotl/cli/delinearize_llama4.py | 156 ++++++++++++++++++ src/axolotl/cli/main.py | 9 + src/axolotl/cli/merge_lora.py | 1 + .../cut_cross_entropy/monkeypatch/llama4.py | 8 +- src/axolotl/monkeypatch/accelerate/fsdp2.py | 2 +- .../monkeypatch/attention/flex_attn.py | 62 +++++-- .../monkeypatch/models/llama4/modeling.py | 11 ++ src/axolotl/monkeypatch/trainer_eval_guard.py | 78 +++++++++ src/axolotl/train.py | 5 + src/axolotl/utils/freeze.py | 7 +- src/axolotl/utils/models.py | 18 +- src/axolotl/utils/schemas/config.py | 10 +- src/axolotl/utils/trainer.py | 10 +- tests/e2e/multigpu/solo/test_flex.py | 3 +- tests/e2e/multigpu/solo/test_grpo.py | 2 + 26 files changed, 629 insertions(+), 45 deletions(-) rename examples/llama-4/{ => do-no-use-fa2}/maverick-qlora-fsdp1.yaml (100%) rename examples/llama-4/{ => do-no-use-fa2}/scout-qlora-fsdp1.yaml (100%) rename examples/llama-4/{ => do-no-use-fa2}/scout-qlora-single-h100.yaml (100%) rename examples/llama-4/{ => do-no-use-fa2}/scout-vision-qlora-fsdp.yaml (100%) create mode 100644 examples/llama-4/scout-qlora-flexattn-fsdp2.yaml create mode 100644 examples/llama-4/scout-qlora-single-h100-flex.yaml create mode 100644 examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml create mode 100644 src/axolotl/cli/delinearize_llama4.py create mode 100644 src/axolotl/monkeypatch/trainer_eval_guard.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 350b04cca9..df12b3c893 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ jobs: cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.6.0 - axolotl_extras: + axolotl_extras: vllm is_latest: true runs-on: axolotl-gpu-runner steps: diff --git a/examples/llama-4/README.md b/examples/llama-4/README.md index a0ec1c70ea..b33f8ae3c2 100644 --- a/examples/llama-4/README.md +++ b/examples/llama-4/README.md @@ -1,16 +1,28 @@ # Llama 4 by Meta AI +## Flash Attention vs Flex Attention + +While Flash Attention to support is "enabled" for Llama-4, the upstream implementation is not correct and usage of Flex Attention is recommended. + ## Available Examples ### Llama 4 Scout 17Bx16Experts (109B) -- [Multi-Modal/Vision QLoRA w/ FSDP1](./scout-vision-qlora-fsdp.yaml) -- [Text Single GPU (H100) QLoRA](./scout-qlora-single-h100.yaml) -- [Text Multi GPU QLoRA w/ FSDP1](./scout-qlora-fsdp1.yaml) -Our Single H100 implementation for Llama 4 Scout uses only 68.5GB VRAM for post-training with 4k context length @ 546 tokens/second. [WandB logs here](https://wandb.ai/axolotl-ai/llama4-sft/runs/zic56rhd) +Flex Attention +- [Text Single GPU (H100) QLoRA](./scout-qlora-single-h100-flex.yaml) +- [Text Multi GPU QLoRA w/ FSDP2](./scout-qlora-flexattn-fsdp2.yaml) -### Llama 4 Maverick 17Bx128Experts (400B) +[//]: # (Flash Attention (Do not use)) + +[//]: # (- [Multi-Modal/Vision QLoRA w/ FSDP1](./scout-vision-qlora-fsdp.yaml)) -- [Text Multi GPU QLoRA w/FSDP1](./maverick-qlora-fsdp1.yaml) +[//]: # (- [Text Single GPU (H100) QLoRA](./scout-qlora-single-h100.yaml)) + +[//]: # (- [Text Multi GPU QLoRA w/ FSDP1](./scout-qlora-fsdp1.yaml)) + +Our Single H100 implementation for Llama 4 Scout uses only 64.5GB VRAM for post-training with 4k context length @ 519 tokens/second. [WandB logs here](https://wandb.ai/axolotl-ai/llama4-flexattn-qlora/runs/wpie7dkj) +Multi-GPU (4xH100) for Llama 4 Scout uses 62.8GB VRAM/GPU @ 4k contenxt length @ 280tps/gpu, [WandB logs here](https://wandb.ai/axolotl-ai/llama4-flexattn-qlora/runs/2lkezdj8) + +### Llama 4 Maverick 17Bx128Experts (400B) -Our 4xH100 implementation for Llama 4 Maverick uses 79.5GB VRAM/GPU for post-training with 4k context length @ 206 tokens/second. [WandB logs here.](https://wandb.ai/axolotl-ai/llama-sft/runs/siyvwuxc?nw=nwuserwinglian) +Coming Soon diff --git a/examples/llama-4/maverick-qlora-fsdp1.yaml b/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml similarity index 100% rename from examples/llama-4/maverick-qlora-fsdp1.yaml rename to examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml diff --git a/examples/llama-4/scout-qlora-fsdp1.yaml b/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml similarity index 100% rename from examples/llama-4/scout-qlora-fsdp1.yaml rename to examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml diff --git a/examples/llama-4/scout-qlora-single-h100.yaml b/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml similarity index 100% rename from examples/llama-4/scout-qlora-single-h100.yaml rename to examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml diff --git a/examples/llama-4/scout-vision-qlora-fsdp.yaml b/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml similarity index 100% rename from examples/llama-4/scout-vision-qlora-fsdp.yaml rename to examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml diff --git a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml new file mode 100644 index 0000000000..9a411883e4 --- /dev/null +++ b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml @@ -0,0 +1,86 @@ +base_model: axolotl-quants/Llama-4-Scout-17B-16E-Linearized-bnb-nf4-bf16 +model_type: Llama4ForConditionalGeneration +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_glu_activation: true +liger_rms_norm: true +liger_layer_norm: true + +llama4_linearized_experts: true +load_in_4bit: true +adapter: qlora +lora_r: 32 +lora_alpha: 64 +lora_target_modules: + - self_attn.q_proj + - self_attn.k_proj + - self_attn.v_proj + - self_attn.o_proj + - shared_expert.gate_proj + - shared_expert.up_proj + - shared_expert.down_proj + # - experts.gate_projs.[0-9]+$ + # - experts.up_projs.[0-9]+$ + # - experts.down_projs.[0-9]+$ +lora_modules_to_save: + # - lm_head + # - embed_tokens + +chat_template: llama4 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value + +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +sequence_len: 4096 +sample_packing: true +pad_to_sequence_len: true + +gradient_accumulation_steps: 1 +micro_batch_size: 2 +num_epochs: 3 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 1e-4 + +bf16: true +tf32: true + +logging_steps: 1 +flex_attention: true +flex_attn_compile_kwargs: + dynamic: false + mode: max-autotune-no-cudagraphs + +warmup_steps: 10 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +fsdp: + - auto_wrap + - full_shard +fsdp_config: + fsdp_version: 2 + fsdp_offload_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: Llama4TextDecoderLayer + fsdp_state_dict_type: SHARDED_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD + fsdp_reshard_after_forward: true + fsdp_activation_checkpointing: true +special_tokens: + pad_token: <|finetune_right_pad_id|> + eos_token: <|eot|> diff --git a/examples/llama-4/scout-qlora-single-h100-flex.yaml b/examples/llama-4/scout-qlora-single-h100-flex.yaml new file mode 100644 index 0000000000..c7a3b28d08 --- /dev/null +++ b/examples/llama-4/scout-qlora-single-h100-flex.yaml @@ -0,0 +1,85 @@ +base_model: axolotl-quants/Llama-4-Scout-17B-16E-Linearized-bnb-nf4-bf16 +model_type: Llama4ForConditionalGeneration +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.liger.LigerPlugin + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +liger_glu_activation: true +liger_rms_norm: true +liger_layer_norm: true +cut_cross_entropy: true + +llama4_linearized_experts: true # needed with custom linearized experts model +load_in_4bit: true +adapter: qlora +lora_r: 32 +lora_alpha: 64 +lora_target_modules: + - self_attn.q_proj + - self_attn.k_proj + - self_attn.v_proj + - self_attn.o_proj + - shared_expert.gate_proj + - shared_expert.up_proj + - shared_expert.down_proj + # - experts.gate_projs.[0-9]+$ # optionally train the moe experts + # - experts.up_projs.[0-9]+$ + # - experts.down_projs.[0-9]+$ +lora_modules_to_save: + # - lm_head # needed if modifying vocabulary + # - embed_tokens + +lora_mlp_kernel: true +lora_qkv_kernel: true +lora_o_kernel: true + +chat_template: llama4 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value + +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +sequence_len: 4096 # up to 8k will work on a single H100 +sample_packing: true +pad_to_sequence_len: true + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 1e-4 + +bf16: true +tf32: true + +torch_compile: true +flex_attention: true +flex_attn_compile_kwargs: + dynamic: false + mode: max-autotune-no-cudagraphs + +gradient_checkpointing: offload +gradient_checkpointing_kwargs: + use_reentrant: false + +logging_steps: 1 +warmup_steps: 20 +evals_per_epoch: 1 +saves_per_epoch: 1 + +weight_decay: 0.0 +special_tokens: + pad_token: <|finetune_right_pad_id|> + eos_token: <|eot|> diff --git a/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml b/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml new file mode 100644 index 0000000000..9fbd34107b --- /dev/null +++ b/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml @@ -0,0 +1,89 @@ +base_model: axolotl-quants/Llama-4-Scout-17B-16E-Linearized-bnb-nf4-bf16 +model_type: Llama4ForConditionalGeneration +processor_type: Llama4Processor +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +sequence_len: 4096 + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_glu_activation: true +liger_rms_norm: true +liger_layer_norm: true + +llama4_linearized_experts: true # use Axolotl's customized model +load_in_4bit: true +adapter: qlora +lora_r: 32 +lora_alpha: 64 +lora_target_modules: + - self_attn.q_proj + - self_attn.k_proj + - self_attn.v_proj + - self_attn.o_proj + - shared_expert.gate_proj + - shared_expert.up_proj + - shared_expert.down_proj + - vision_adapter.mlp.fc1 + - vision_adapter.mlp.fc2 + # - experts.gate_projs.[0-9]+$ + # - experts.up_projs.[0-9]+$ + # - experts.down_projs.[0-9]+$ +lora_modules_to_save: + - lm_head + - embed_tokens + +chat_template: llama4 +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + field_messages: messages + +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 1e-4 + +bf16: true +tf32: true + +logging_steps: 1 +flex_attention: true +flex_attn_compile_kwargs: + dynamic: false + mode: max-autotune-no-cudagraphs + +warmup_steps: 10 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +fsdp: + - auto_wrap + - full_shard +fsdp_config: + fsdp_version: 2 + fsdp_offload_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: Llama4TextDecoderLayer + fsdp_state_dict_type: SHARDED_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD + fsdp_reshard_after_forward: true + fsdp_activation_checkpointing: true +special_tokens: + pad_token: <|finetune_right_pad_id|> + eos_token: <|eot|> diff --git a/requirements.txt b/requirements.txt index e8377b880a..76e8be8c16 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ liger-kernel==0.5.6 packaging==23.2 peft==0.15.1 -transformers==4.51.1 +transformers==4.51.3 tokenizers>=0.21.1 accelerate==1.6.0 datasets==3.5.0 diff --git a/setup.py b/setup.py index 29719b1f32..6c911d8f7d 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ def parse_requirements(extras_require_map): if (major, minor) >= (2, 6): _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers==0.0.29.post2") - extras_require_map["vllm"] = ["vllm==0.8.1"] + extras_require_map["vllm"] = ["vllm==0.8.3"] elif (major, minor) >= (2, 5): _install_requires.pop(_install_requires.index(xformers_version)) if patch == 0: diff --git a/src/axolotl/cli/delinearize_llama4.py b/src/axolotl/cli/delinearize_llama4.py new file mode 100644 index 0000000000..c92bae9302 --- /dev/null +++ b/src/axolotl/cli/delinearize_llama4.py @@ -0,0 +1,156 @@ +""" +CLI tool to delinearize quantized/Linearized Llama-4 models. +""" + +import os +from pathlib import Path +from typing import Generator, Union + +import fire +import torch +from accelerate import init_empty_weights +from dotenv import load_dotenv +from transformers import AutoProcessor + + +def iter_convert_patched_to_hf(model_state_dict, num_experts) -> Generator: + keys = list(model_state_dict.keys()) + for key in keys: + if ".feed_forward.experts." not in key: + yield key, model_state_dict[key] + if ".feed_forward.experts.gate_projs" in key: + # gate gets fused with up so skip the yield on this and we'll fuse it when asking for the up + continue + if ".feed_forward.experts.up_projs" in key: + if ".feed_forward.experts.up_projs.0." in key: + # handle the re-shape and fusing of gate and up, and conversion from linear to parameter + prefix = key.split(".up_projs.0.")[0] + key = f"{prefix}.gate_up_proj" + # grab all the up_projs and gate_projs across all experts + gate_stacked = torch.stack( + [ + model_state_dict[ + f"{prefix}.gate_projs.{expert_idx}.weight" + ].transpose(0, 1) + for expert_idx in range(num_experts) + ] + ) + up_stacked = torch.stack( + [ + model_state_dict[ + f"{prefix}.up_projs.{expert_idx}.weight" + ].transpose(0, 1) + for expert_idx in range(num_experts) + ] + ) + gate_up_proj = torch.cat((gate_stacked, up_stacked), dim=-1) + del gate_stacked, up_stacked + yield key, gate_up_proj + else: + del model_state_dict[key] + continue + if ".feed_forward.experts.down_projs" in key: + if ".feed_forward.experts.down_projs.0." in key: + # handle the re-shape and fusing of gate and up, and conversion from linear to parameter + prefix = key.split(".down_projs.0.")[0] + key = f"{prefix}.down_proj" + # grab all the down_projs across all experts + down_stacked = torch.stack( + [ + model_state_dict[ + f"{prefix}.down_projs.{expert_idx}.weight" + ].transpose(0, 1) + for expert_idx in range(num_experts) + ] + ) + yield key, down_stacked + else: + del model_state_dict[key] + continue + + +def do_cli(model: Union[Path, str], output: Union[Path, str]) -> None: + """ + Convert a patched HF format Llama4 model (with separated projections) + back to the original HF format (with fused projections). + + Args: + model: Path to the patched HF model + output: Path to save the converted model + """ + print(f"Loading model from {model}") + from axolotl.monkeypatch.models.llama4.modeling import ( + patch_llama4_linearized_modeling, + ) + + unpatch_llama4 = patch_llama4_linearized_modeling() + from transformers import Llama4ForConditionalGeneration + + model_ = Llama4ForConditionalGeneration.from_pretrained( + model, torch_dtype=torch.bfloat16 + ) + processor = AutoProcessor.from_pretrained(model) + processor.save_pretrained(output) + + device = model_.device.type + if device == "cuda": + print( + f"peak memory allocated: {torch.cuda.max_memory_allocated() / 1024**2} MB" + ) + print(f"peak memory reserved: {torch.cuda.max_memory_reserved() / 1024**2} MB") + model_config = model_.config + config = model_.config.get_text_config() + + # Get key dimensions from the config + hidden_size = config.hidden_size + intermediate_size = config.intermediate_size + num_experts = config.num_local_experts + + print( + f"Model dimensions: hidden_size={hidden_size}, intermediate_size={intermediate_size}, num_experts={num_experts}" + ) + + # Create output directory if it doesn't exist + os.makedirs(output, exist_ok=True) + + # Get state dict + state_dict = model_.state_dict() + del model_ + + # Create a new state dict for the converted model + converted_state_dict = {} + + # First, copy all keys that don't need modification + for key, value in iter_convert_patched_to_hf(state_dict, num_experts): + converted_state_dict[key] = value + + del state_dict + if device == "cuda": + torch.cuda.empty_cache() + print("State dict converted.") + print( + f"peak memory allocated: {torch.cuda.max_memory_allocated() / 1024**2} MB" + ) + print(f"peak memory reserved: {torch.cuda.max_memory_reserved() / 1024**2} MB") + # Ideally re-load the model import to load the converted state dict + # Save the converted model + with init_empty_weights(): + unpatch_llama4() + model_ = Llama4ForConditionalGeneration(model_config) + + if device == "cuda": + print("State dict loaded into model.") + print( + f"peak memory allocated: {torch.cuda.max_memory_allocated() / 1024**2} MB" + ) + print(f"peak memory reserved: {torch.cuda.max_memory_reserved() / 1024**2} MB") + model_.load_state_dict(converted_state_dict, strict=False, assign=True) + print(f"Saving converted model to {output}...") + model_.save_pretrained(output) + + print(f"Model successfully converted and saved to {output}") + + +if __name__ == "__main__": + load_dotenv() + fire.Fire(do_cli) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index 7532a9689a..5936147333 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -330,6 +330,15 @@ def vllm_serve(config: str, **cli_args: VllmServeCliArgs): do_vllm_serve(config, cli_args) +@cli.command() +@click.argument("model", type=click.Path(exists=True, path_type=str)) +@click.argument("output", type=click.Path(exists=False, path_type=str)) +def delinearize_llama4(model: str, output: str) -> None: + from axolotl.cli.delinearize_llama4 import do_cli as do_delinearize_llama4 + + do_delinearize_llama4(model, output) + + cli.add_command(lm_eval) diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index c7a3a32259..5c8802dd11 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -40,6 +40,7 @@ def do_merge_lora(*, cfg: DictDefault) -> None: LOG.warning("Error raised: %s", e) model.generation_config.do_sample = True + model.config.use_cache = True if cfg.local_rank == 0: LOG.info(f"Saving merged model to: {str(Path(cfg.output_dir) / 'merged')}...") diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py index f08663f996..7204f5c900 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py @@ -165,7 +165,7 @@ def cce_forward( ) def cce_forward_multimodal( self, - input_ids: torch.LongTensor | None = None, + input_ids: torch.LongTensor | None = None, # type: ignore pixel_values: torch.FloatTensor | None = None, attention_mask: Optional[torch.Tensor] = None, position_ids: Optional[torch.LongTensor] = None, @@ -254,7 +254,7 @@ def cce_forward_multimodal( ) if inputs_embeds is None: - inputs_embeds = self.get_input_embeddings()(input_ids) + inputs_embeds = self.get_input_embeddings()(input_ids) # type: ignore if pixel_values is not None: image_features = self.get_image_features( @@ -263,13 +263,13 @@ def cce_forward_multimodal( vision_feature_select_strategy=vision_feature_select_strategy, image_sizes=image_sizes, ) - original_inputs_embeds_shape = inputs_embeds.shape + original_inputs_embeds_shape = inputs_embeds.shape # type: ignore vision_flat = image_features.view(-1, image_features.size(-1)) projected_vision_flat = self.multi_modal_projector(vision_flat) special_image_mask = (input_ids == self.config.image_token_index).unsqueeze(-1) - final_mask = special_image_mask.to(inputs_embeds.device) + final_mask = special_image_mask.to(inputs_embeds.device) # type: ignore inputs_embeds = inputs_embeds.view(-1, inputs_embeds.size(-1)) # type: ignore final_mask_1d = final_mask[..., 0].reshape(-1) diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index 2a5d2151dc..d8ec00c696 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -49,7 +49,7 @@ def fsdp2_load_full_state_dict(accelerator, model: torch.nn.Module, full_sd: dic ) sharded_sd[param_name] = sharded_tensor - model.load_state_dict(sharded_sd) + model.load_state_dict(sharded_sd, assign=True) def patch_accelerate_fsdp_utils(): diff --git a/src/axolotl/monkeypatch/attention/flex_attn.py b/src/axolotl/monkeypatch/attention/flex_attn.py index d65ee706f8..3652a30b33 100644 --- a/src/axolotl/monkeypatch/attention/flex_attn.py +++ b/src/axolotl/monkeypatch/attention/flex_attn.py @@ -7,12 +7,11 @@ import transformers -def patch_flex_wrapper(): +def patch_flex_wrapper(**flex_attn_compile_kwargs): # TODO remove this patch when transformers#37285 is merged and in a release is_torch_2_6 = torch.__version__.startswith("2.6") - is_transformers_below_4_51 = transformers.__version__ < "4.51.0" - if not (is_torch_2_6 and is_transformers_below_4_51): + if not is_torch_2_6: return from torch.nn.attention.flex_attention import flex_attention @@ -32,17 +31,24 @@ def __new__(cls, *args, **kwargs): cls._instance = super().__new__(cls) return cls._instance + @classmethod + def del_singleton(cls): + cls._instance = None + @torch.compiler.disable(recursive=False) - def __init__(self): + def __init__(self, training): """ Initialize or update the singleton instance. """ - if not self._is_flex_compiled: + self.training = None + if not self._is_flex_compiled or training != self.training: + # In PyTorch 2.6.0, there's a known issue with flex attention compilation which may + # cause errors. The suggested fix is to compile with "max-autotune-no-cudagraphs" + # see https://github.com/pytorch/pytorch/issues/146260 for training + self.training = training self._compiled_flex_attention = torch.compile( flex_attention, - dynamic=False, - mode="max-autotune-no-cudagraphs", - fullgraph=True, + **flex_attn_compile_kwargs, ) self._is_flex_compiled = True @@ -50,15 +56,22 @@ def __call__(self): return self._compiled_flex_attention transformers.integrations.flex_attention.WrappedFlexAttention = WrappedFlexAttention + setattr( + sys.modules["transformers.integrations.flex_attention"], + "WrappedFlexAttention", + WrappedFlexAttention, + ) def patch_flex_make_mask(): is_torch_2_6 = torch.__version__.startswith("2.6") - is_transformers_eq_4_51 = transformers.__version__ == "4.51.0" - if not (is_torch_2_6 and is_transformers_eq_4_51): + if not is_torch_2_6: return + from torch.nn.attention.flex_attention import ( + _DEFAULT_SPARSE_BLOCK_SIZE as flex_default_block_size, + ) from torch.nn.attention.flex_attention import ( BlockMask, ) @@ -104,14 +117,16 @@ def patched_make_flex_block_causal_mask( if not query_length: query_length = total_seq_len attention_mask_2d = torch.nn.functional.pad( - attention_mask_2d, value=0, pad=(0, key_length) + attention_mask_2d, + value=0, + pad=(0, abs(total_seq_len - max(key_length, flex_default_block_size))), ) device = attention_mask_2d.device document_ids = attention_mask_2d.clone() if attention_chunk_size is not None: # we create an arange, then we just // by chunk size to get [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3] - document_ids = (document_ids.fill_(1).cumsum(-1) - 1) // ( + chunk_idxs = (document_ids.clone().fill_(1).cumsum(-1) - 1) // ( attention_chunk_size ) @@ -138,6 +153,18 @@ def causal_mask_mod( final_mask = causal_mask & padding_mask & document_mask return final_mask + def chunk_causal_mask_mod(batch_idx, head_idx, q_idx, kv_idx): + """ + Combines the chunk mask with the causal mask for chunked attention. + """ + chunk_mask = chunk_idxs[batch_idx, q_idx] == chunk_idxs[batch_idx, kv_idx] + causal_doc_mask = causal_mask_mod(batch_idx, head_idx, q_idx, kv_idx) + return chunk_mask & causal_doc_mask + + mask_mod_maybe_combined = ( + causal_mask_mod if attention_chunk_size is None else chunk_causal_mask_mod + ) + if offsets is not None: q_offset = offsets[0] kv_offset = offsets[1] @@ -145,10 +172,10 @@ def causal_mask_mod( def mask_mod(batch_idx, head_idx, q_idx, kv_idx): offset_q = q_idx + q_offset offset_kv = kv_idx + kv_offset - return causal_mask_mod(batch_idx, head_idx, offset_q, offset_kv) + return mask_mod_maybe_combined(batch_idx, head_idx, offset_q, offset_kv) else: - mask_mod = causal_mask_mod + mask_mod = mask_mod_maybe_combined return create_block_causal_mask_flex( mask_mod=mask_mod, B=batch_size, @@ -160,11 +187,16 @@ def mask_mod(batch_idx, head_idx, q_idx, kv_idx): ) for n in tuple(sys.modules): - if ".modeling_" in n and "llama4" not in n: + if ".modeling_" in n: if hasattr(sys.modules[n], "make_flex_block_causal_mask"): sys.modules[n].make_flex_block_causal_mask = ( patched_make_flex_block_causal_mask ) + setattr( + sys.modules[n], + "make_flex_block_causal_mask", + patched_make_flex_block_causal_mask, + ) transformers.integrations.flex_attention.make_flex_block_causal_mask = ( patched_make_flex_block_causal_mask diff --git a/src/axolotl/monkeypatch/models/llama4/modeling.py b/src/axolotl/monkeypatch/models/llama4/modeling.py index b2a46ab862..4127793e75 100644 --- a/src/axolotl/monkeypatch/models/llama4/modeling.py +++ b/src/axolotl/monkeypatch/models/llama4/modeling.py @@ -93,9 +93,20 @@ def patch_llama4_linearized_modeling(): """ from transformers.models.llama4 import modeling_llama4 + old_lamma_4_text_experts = modeling_llama4.Llama4TextExperts modeling_llama4.Llama4TextExperts = Llama4TextExperts setattr( sys.modules["transformers.models.llama4"], "Llama4TextExperts", Llama4TextExperts, ) + + def unpatch(): + modeling_llama4.Llama4TextExperts = old_lamma_4_text_experts + setattr( + sys.modules["transformers.models.llama4"], + "Llama4TextExperts", + old_lamma_4_text_experts, + ) + + return unpatch diff --git a/src/axolotl/monkeypatch/trainer_eval_guard.py b/src/axolotl/monkeypatch/trainer_eval_guard.py new file mode 100644 index 0000000000..e929ac766a --- /dev/null +++ b/src/axolotl/monkeypatch/trainer_eval_guard.py @@ -0,0 +1,78 @@ +""" +fix for FSDP2 evals when using torch.compile +""" + +import inspect +import logging + +from transformers import Trainer + +from axolotl.monkeypatch.utils import detab_code + +LOG = logging.getLogger(__name__) + +ORIGINAL_TRAINER_CODE = """ + model.eval() +""" + +PATCHED_TRAINER_CODE = """ + if hasattr(model, "eval") and callable(model.eval): + self.model.eval() +""" + + +def get_evaluation_loop_code() -> str: + training_loop = inspect.getsource(Trainer.evaluation_loop) + return training_loop + + +def check_evaluation_loop_is_patchable() -> bool: + eval_loop = get_evaluation_loop_code() + eval_loop, _ = detab_code(eval_loop) + return ORIGINAL_TRAINER_CODE in eval_loop + + +def patch_evaluation_loop_for_fsdp2(): + """ + monkeypatch for fixing the eval loop for fsdp2 with torch.compile + """ + + try: + evaluation_loop = get_evaluation_loop_code() + except OSError: + return + Trainer._original_evaluation_loop = ( # pylint: disable=protected-access + evaluation_loop + ) + evaluation_loop, _ = detab_code(evaluation_loop) + if ORIGINAL_TRAINER_CODE not in evaluation_loop: + return + + evaluation_loop = evaluation_loop.replace( + ORIGINAL_TRAINER_CODE, PATCHED_TRAINER_CODE + ) + evaluation_loop = evaluation_loop.replace( + "def evaluation_loop(", + "def _fixed_evaluation_loop(", + 1, + ) + + # load imports necessary + import transformers.trainer + + items_to_import = [] + for item in dir(transformers.trainer): + if item in evaluation_loop: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + "from transformers.trainer import (" + + ", ".join(x for x in items_to_import) + + ")", + globals(), + ) + exec(evaluation_loop, globals()) # pylint: disable=exec-used # nosec B102 + LOG.info("patching _inner_training_loop for fsdp optimizer save") + Trainer.evaluation_loop = ( # pylint: disable=protected-access + _fixed_evaluation_loop # pylint: disable=undefined-variable # noqa: F821 + ) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index c2bddeeec4..e003c8b674 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -81,6 +81,11 @@ def setup_model_and_tokenizer( # Apply freezing if specified if cfg.unfrozen_parameters: freeze_layers_except(model, cfg.unfrozen_parameters) + if any( + any(embed in param for embed in ["lm_head", "embed_tokens"]) + for param in cfg.unfrozen_parameters + ): + model.enable_input_require_grads() return model, tokenizer, peft_config, processor diff --git a/src/axolotl/utils/freeze.py b/src/axolotl/utils/freeze.py index 7199eaa364..65ca621374 100644 --- a/src/axolotl/utils/freeze.py +++ b/src/axolotl/utils/freeze.py @@ -2,13 +2,14 @@ module to freeze/unfreeze parameters by name """ -import logging import re from typing import Callable, List, Tuple, Union +from accelerate.logging import get_logger + from axolotl.utils.distributed import is_main_process -LOG = logging.getLogger("axolotl.utils.freeze") +LOG = get_logger(__name__) def freeze_layers_except(model, regex_patterns): @@ -184,7 +185,7 @@ def __init__(self, pattern: str): """ self.raw_pattern = pattern name_pattern, self.range = self._parse_pattern(pattern) - self.name_regex = re.compile(name_pattern.replace(".", "\\.")) + self.name_regex = re.compile(re.sub(r"\.(?!\+)", "\\.", name_pattern)) def match(self, name: str) -> bool: """ diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index c5e569f130..4d43669942 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -542,6 +542,17 @@ def apply_patches(self) -> None: from axolotl.monkeypatch.accelerate.fsdp2 import patch_accelerate_fsdp_utils patch_accelerate_fsdp_utils() + + if self.cfg.flex_attention: + from axolotl.monkeypatch.attention.flex_attn import ( + patch_flex_make_mask, + patch_flex_wrapper, + ) + + flex_attn_compile_kwargs = self.cfg.flex_attn_compile_kwargs or {} + patch_flex_wrapper(**flex_attn_compile_kwargs) + patch_flex_make_mask() + # patch gemma3 conditional generation forward before loading plugins # as it could be overridden by plugins if self.cfg.model_config_type == "llama4": @@ -905,13 +916,6 @@ def set_attention_config(self) -> None: self.model_config._attn_implementation = ( # pylint: disable=protected-access "flex_attention" ) - from axolotl.monkeypatch.attention.flex_attn import ( - patch_flex_make_mask, - patch_flex_wrapper, - ) - - patch_flex_wrapper() - patch_flex_make_mask() elif self.cfg.flash_attention: if not self.cfg.sample_packing and self.cfg.s2_attention: diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 882c9a2486..8a76c4eb48 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -225,6 +225,7 @@ class AxolotlInputConfig( sdp_attention: bool | None = None s2_attention: bool | None = None flex_attention: bool | None = None + flex_attn_compile_kwargs: dict[str, Any] | None = None flash_attention: bool | None = None flash_attn_cross_entropy: bool | None = None flash_attn_rms_norm: bool | None = None @@ -1276,11 +1277,14 @@ def check_multigpu_lora_kernels(cls, data): ): capabilities = data.get("capabilities") is_fsdp = data.get("fsdp") is not None - - if capabilities and capabilities.get("n_gpu", 0) > 1: + is_fsdp2 = ( + data.get("fsdp_config") is not None + and str(data.get("fsdp_config").get("fsdp_version")) == "2" + ) + if capabilities and capabilities.get("n_gpu", 0) > 1 and not is_fsdp2: if is_fsdp: raise ValueError( - "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with FSDP." + "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with FSDP1." ) return data diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 964b170869..c1154be68d 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -17,6 +17,7 @@ from transformers.utils import is_torch_bf16_gpu_available from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder +from axolotl.monkeypatch.trainer_eval_guard import patch_evaluation_loop_for_fsdp2 from axolotl.utils.distributed import reduce_and_broadcast from axolotl.utils.environment import check_cuda_p2p_ib_support from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths @@ -235,7 +236,8 @@ def drop_long_seq(sample, sequence_len=2048, min_sequence_len=2): def process_datasets_for_packing(cfg, train_dataset, eval_dataset): - if cfg.model_config_type in ["mamba", "gemma3"]: + drop_attn_mask = cfg.model_config_type in ["mamba", "gemma3"] + if drop_attn_mask: LOG.info("dropping attention_mask column") train_dataset = train_dataset.remove_columns("attention_mask") if eval_dataset: @@ -625,6 +627,12 @@ def setup_trainer( A trainer instance (either `HFRLTrainer` or `HFCausalTrainer`) configured based on the provided parameters. """ + if ( + cfg.torch_compile + and cfg.fsdp_config + and str(cfg.fsdp_config.fsdp_version) == "2" + ): + patch_evaluation_loop_for_fsdp2() if cfg.rl: trainer_builder = HFRLTrainerBuilder(cfg, model, tokenizer, processor) trainer_builder.model_ref = model_ref diff --git a/tests/e2e/multigpu/solo/test_flex.py b/tests/e2e/multigpu/solo/test_flex.py index 3af6d5a768..cbe3794b35 100644 --- a/tests/e2e/multigpu/solo/test_flex.py +++ b/tests/e2e/multigpu/solo/test_flex.py @@ -56,11 +56,12 @@ def test_loss_llama(self, temp_dir): "num_epochs": 1, "micro_batch_size": 2, "gradient_accumulation_steps": 2, + "gradient_checkpointing": True, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 5, + "max_steps": 2, "use_tensorboard": True, "save_strategy": "no", } diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index bd999e2f3b..f4914ed1ab 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -177,6 +177,7 @@ def test_llama_dora(self, temp_dir, num_gpus): "NCCL_P2P_LEVEL": "LOC", **current_env, "CUDA_VISIBLE_DEVICES": "1", + "VLLM_USE_V1": "0", } vllm_process_id = start_vllm( cfg.base_model, @@ -264,6 +265,7 @@ def test_llama_fft(self, temp_dir, num_gpus): "NCCL_P2P_LEVEL": "LOC", # nccl can be brittle, assume P2P isn't reliable **current_env, "CUDA_VISIBLE_DEVICES": "1", + "VLLM_USE_V1": "0", } vllm_process_id = start_vllm( cfg.base_model, From b8c633aa978f1c47092cbc1ee8d86040ddccef51 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 16 Apr 2025 13:50:48 -0400 Subject: [PATCH 0554/1405] batch api HF adapter for ring-flash-attn; cleanup and improvements (#2520) * batch api HF adapter for ring-flash-attn; cleanup and improvements * update * adding all batch ring-flash-attn methods via single adapter * removing pad_to_sequence_len=False for now * fix * updating docs to include batch SP * review comments * fixes for batch API funcs, simplify * fixes * fix * updates * add batch_zigzag smoke test --- docs/config.qmd | 3 + docs/sequence_parallelism.qmd | 3 + src/axolotl/core/trainer_builder.py | 2 + src/axolotl/core/training_args.py | 8 + .../attention/ring_attn/__init__.py | 12 ++ .../attention/ring_attn/adapters/__init__.py | 0 .../attention/ring_attn/adapters/batch.py | 192 ++++++++++++++++++ .../{ring_attn.py => ring_attn/patch.py} | 65 ++++-- src/axolotl/utils/collators/batching.py | 54 +++-- src/axolotl/utils/models.py | 5 +- src/axolotl/utils/schemas/config.py | 38 +++- tests/e2e/multigpu/test_sp.py | 53 ++++- tests/e2e/patched/test_sp.py | 11 +- 13 files changed, 397 insertions(+), 49 deletions(-) create mode 100644 src/axolotl/monkeypatch/attention/ring_attn/__init__.py create mode 100644 src/axolotl/monkeypatch/attention/ring_attn/adapters/__init__.py create mode 100644 src/axolotl/monkeypatch/attention/ring_attn/adapters/batch.py rename src/axolotl/monkeypatch/attention/{ring_attn.py => ring_attn/patch.py} (68%) diff --git a/docs/config.qmd b/docs/config.qmd index 0f55c10772..a677344980 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -693,6 +693,9 @@ sequence_parallel_degree: # Optional; strides across the key dimension. Larger values use more memory but should make training faster. # Must evenly divide the number of KV heads in your model. heads_k_stride: 1 +# One of "varlen_llama3", "batch_ring", "batch_zigzag", "batch_stripe". Defaults to "varlen_llama3" +# in the sample packing case, and "batch_ring" in the non-sample packing case. +ring_attn_func: # Path to torch distx for optim 'adamw_anyprecision' torchdistx_path: diff --git a/docs/sequence_parallelism.qmd b/docs/sequence_parallelism.qmd index 98ca4d746b..20739333ae 100644 --- a/docs/sequence_parallelism.qmd +++ b/docs/sequence_parallelism.qmd @@ -27,6 +27,9 @@ To enable sequence parallelism, add the following to your configuration file: sequence_parallel_degree: 4 # Split sequences across 4 GPUs # Optional; strides across the key dimension. Larger values use more memory but should make training faster. heads_k_stride: 1 +# Optional; one of "varlen_llama3", "batch_ring", "batch_zigzag", "batch_stripe". Defaults to +# "varlen_llama3" when `sample_packing: true`, and "batch_ring" otherwise. +ring_attn_func: ``` The `sequence_parallel_degree` should be a divisor of the total number of GPUs. For example: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 40dedb4567..7c2be49568 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -776,6 +776,7 @@ def build(self, total_num_steps): training_arguments_kwargs["sequence_parallel_degree"] = ( self.cfg.sequence_parallel_degree ) + training_arguments_kwargs["ring_attn_func"] = self.cfg.ring_attn_func if self.cfg.reward_model: training_args_cls = AxolotlRewardConfig @@ -933,6 +934,7 @@ def build_collator( kwargs["return_tensors"] = "pt" if issubclass(collator, DataCollatorForSeq2Seq): kwargs["sequence_parallel_degree"] = training_args.sequence_parallel_degree + kwargs["ring_attn_func"] = training_args.ring_attn_func return collator( *collator_args, diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index 18843abb47..3fe32f5074 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -9,6 +9,8 @@ from transformers import TrainingArguments from trl import CPOConfig, KTOConfig, ORPOConfig, PRMConfig, RewardConfig +from axolotl.monkeypatch.attention.ring_attn.patch import RingAttnFunc + @dataclass class AxolotlTrainingMixins: @@ -218,6 +220,12 @@ class AxolotlTrainingMixins: default=1, metadata={"help": "The number of workers to use in sequence parallelism"}, ) + ring_attn_func: Optional[RingAttnFunc] = field( + default=None, + metadata={ + "help": "The ring-flash-attn function to use in sequence parallelism" + }, + ) # multi-modal section diff --git a/src/axolotl/monkeypatch/attention/ring_attn/__init__.py b/src/axolotl/monkeypatch/attention/ring_attn/__init__.py new file mode 100644 index 0000000000..055607e928 --- /dev/null +++ b/src/axolotl/monkeypatch/attention/ring_attn/__init__.py @@ -0,0 +1,12 @@ +"""Init for ring attention monkeypatch module""" + +# pylint: disable=unused-import +# flake8: noqa + +from .patch import ( + RingAttnFunc, + get_ring_attn_group, + register_ring_attn, + set_ring_attn_group, + update_ring_attn_params, +) diff --git a/src/axolotl/monkeypatch/attention/ring_attn/adapters/__init__.py b/src/axolotl/monkeypatch/attention/ring_attn/adapters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/attention/ring_attn/adapters/batch.py b/src/axolotl/monkeypatch/attention/ring_attn/adapters/batch.py new file mode 100644 index 0000000000..a88c9f6f17 --- /dev/null +++ b/src/axolotl/monkeypatch/attention/ring_attn/adapters/batch.py @@ -0,0 +1,192 @@ +""" +HuggingFace flash attention adapter for basic ring attention (batch API). + +Inspired by +https://github.com/zhuzilin/ring-flash-attention/blob/ce9fd3935ca0e5f0592bb0826cbed18ec69da729/ring_flash_attn/adapters/hf_adapter.py. +Our implementation closely follows the structure of that module, but we've minified it +somewhat to support only the latest versions of transformers. +""" + +# pylint: disable=protected-access,cyclic-import + +import os +from typing import Callable + +import torch +import torch.distributed as dist +import transformers +import transformers.modeling_flash_attention_utils +from ring_flash_attn import ( + ring_flash_attn_func, + stripe_flash_attn_func, + zigzag_ring_flash_attn_func, +) +from ring_flash_attn.adapters.hf_adapter import check_params +from transformers.modeling_flash_attention_utils import ( + _flash_supports_window_size, + is_flash_attn_greater_or_equal, +) +from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + +from axolotl.monkeypatch.attention.ring_attn.patch import RingAttnFunc + +RING_ATTN_FUNC_MAPPING = { + RingAttnFunc.BATCH_RING: ring_flash_attn_func, + RingAttnFunc.BATCH_ZIGZAG: zigzag_ring_flash_attn_func, + RingAttnFunc.BATCH_STRIPE: stripe_flash_attn_func, +} + + +def create_flash_attn_forward( + process_group: dist.ProcessGroup, ring_attn_func: RingAttnFunc +) -> Callable: + """ + Create a ring flash attention forward function compatible with HuggingFace's + interface. + + Args: + process_group: A PyTorch distributed process group. + ring_attn_func: Function from `ring_flash_attention` to replace HF flash + attention with. + + Returns: + A function that implements the ring flash attention forward pass with the + signature expected by HuggingFace Transformers. + """ + + # transformers 4.48+ + # pylint: disable=unused-argument + def _flash_attention_forward( + query_states: torch.Tensor, + key_states: torch.Tensor, + value_states: torch.Tensor, + attention_mask: torch.Tensor, + query_length: int, + is_causal: bool, + dropout: float = 0.0, + position_ids: torch.Tensor | None = None, + softmax_scale: float | None = None, + sliding_window: int | None = None, + use_top_left_mask: bool = False, + softcap: float | None = None, + deterministic: bool = None, + cu_seq_lens_q: torch.LongTensor | None = None, + cu_seq_lens_k: torch.LongTensor | None = None, + max_length_q: int | None = None, + max_length_k: int | None = None, + target_dtype: torch.dtype | None = None, + **kwargs, + ): + """ + Calls the forward method of Ring Flash Attention. + + Args: + query_states: Tensor containing the query vectors. + key_states: Tensor containing the key vectors. + value_states: Tensor containing the value vectors. + attention_mask: Not used in this implementation. + query_length: Integer representing the length of the query sequence. + is_causal: Boolean indicating whether to apply a causal mask to the attention. + dropout: Float representing the dropout probability. Default is 0.0. + position_ids: Not used in this implementation. + softmax_scale: Optional float value for the softmax scaling factor. Default is None. + sliding_window: Optional integer defining the size of the sliding attention window. + Default is None. + use_top_left_mask: Boolean indicating whether to use a top-left mask for the attention. + Default is False. + softcap: Not used in this implementation. + deterministic: Optional boolean to enforce deterministic computation. Default is None. + cu_seq_lens_q: Not used in this implementation. + cu_seq_lens_k: Not used in this implementation. + max_length_q: Not used in this implementation. + max_length_k: Not used in this implementation. + target_dtype: Not used in this implementation. + **kwargs: Additional keyword arguments. Not used in this implementation. + + Returns: + torch.Tensor: The output of the attention mechanism, with shape + `[batch_size, query_length, num_heads, head_dim]`. + """ + if not use_top_left_mask: + causal = is_causal + else: + causal = is_causal and query_length != 1 + + # Handle sliding window + use_sliding_windows = ( + _flash_supports_window_size + and sliding_window is not None + and key_states.shape[1] > sliding_window + ) + window_size = ( + (sliding_window, sliding_window) if use_sliding_windows else (-1, -1) + ) + + # Handle deterministic mode + if is_flash_attn_greater_or_equal("2.4.1"): + if deterministic is None: + deterministic = ( + os.environ.get("FLASH_ATTENTION_DETERMINISTIC", "0") == "1" + ) + + # Call ring flash attention function + attn_output = RING_ATTN_FUNC_MAPPING[ring_attn_func]( + query_states, + key_states, + value_states, + dropout_p=dropout, + softmax_scale=softmax_scale, + causal=causal, + window_size=window_size, + alibi_slopes=None, + deterministic=deterministic, + return_attn_probs=False, + group=process_group, + ) + + return attn_output + + return _flash_attention_forward + + +def substitute_hf_flash_attn( + process_group: dist.ProcessGroup, ring_attn_func: RingAttnFunc +): + """ + Substitute HuggingFace's flash attention implementation with ring-based implementation. + + Args: + process_group: PyTorch distributed process group for communication. + ring_attn_func: Function from `ring_flash_attention` to replace HF flash + attention with. + """ + try: + # Substitute flash attention + old_flash_attention_forward = ( + transformers.modeling_flash_attention_utils._flash_attention_forward + ) + new_flash_attention_forward = create_flash_attn_forward( + process_group=process_group, ring_attn_func=ring_attn_func + ) + + if check_params(old_flash_attention_forward, new_flash_attention_forward): + transformers.modeling_flash_attention_utils._flash_attention_forward = ( + new_flash_attention_forward + ) + else: + raise ValueError( + "The signature of the new flash attention forward function does not match the old one." + ) + except Exception as exception: + raise ValueError( + f"The current transformer version {transformers.__version__} is not supported. " + "Please use pip install -U transformers to upgrade to the latest version. " + "If the code failed with the latest version, " + f"please file an issue." + ) from exception + + # Register with ALL_ATTENTION_FUNCTIONS if available + if ALL_ATTENTION_FUNCTIONS is not None: + from ring_flash_attn.adapters.hf_adapter import flash_attention_forward + + ALL_ATTENTION_FUNCTIONS["flash_attention_2"] = flash_attention_forward diff --git a/src/axolotl/monkeypatch/attention/ring_attn.py b/src/axolotl/monkeypatch/attention/ring_attn/patch.py similarity index 68% rename from src/axolotl/monkeypatch/attention/ring_attn.py rename to src/axolotl/monkeypatch/attention/ring_attn/patch.py index 30aa78f011..b5587ddca6 100644 --- a/src/axolotl/monkeypatch/attention/ring_attn.py +++ b/src/axolotl/monkeypatch/attention/ring_attn/patch.py @@ -6,6 +6,8 @@ their sequence parallel version of Flash Attention 2. """ +from enum import Enum + import torch import torch.distributed as dist from accelerate.logging import get_logger @@ -16,6 +18,7 @@ configure_logging() LOG = get_logger(__name__) + RING_ATTN_GROUP = None @@ -40,7 +43,22 @@ def set_ring_attn_group(ring_attn_group: dist.ProcessGroup | None): RING_ATTN_GROUP = ring_attn_group -def register_ring_attn(sequence_parallel_degree: int, heads_k_stride: int | None): +class RingAttnFunc(str, Enum): + """Enum class for supported `ring-flash-attn` implementations""" + + # VARLEN_RING = "varlen_ring" + # VARLEN_ZIGZAG = "varlen_zigzag" + VARLEN_LLAMA3 = "varlen_llama3" + BATCH_RING = "batch_ring" + BATCH_ZIGZAG = "batch_zigzag" + BATCH_STRIPE = "batch_stripe" + + +def register_ring_attn( + sequence_parallel_degree: int, + heads_k_stride: int | None, + ring_attn_func: RingAttnFunc | None, +): """ Create ring attention group and substitute flash attn with ring flash attn. @@ -48,6 +66,9 @@ def register_ring_attn(sequence_parallel_degree: int, heads_k_stride: int | None sequence_parallel_degree: Sequence parallelism factor. heads_k_stride: Sequence parallelism K head stride size. Passed through to `ring_flash_attn.substitute_hf_flash_attn`. + ring_attn_func: `ring_flash_attn` ring attention implemention. If sample + packing is enabled, it must be a `varlen` function; otherwise, it must be a + `batch` function. """ if get_ring_attn_group() is not None: LOG.info("Ring attention already registered, exiting early...") @@ -58,7 +79,9 @@ def register_ring_attn(sequence_parallel_degree: int, heads_k_stride: int | None f"each sequence will be processed across {sequence_parallel_degree} GPUs" ) + rank = dist.get_rank() world_size = dist.get_world_size() + assert sequence_parallel_degree <= world_size, ( f"sequence_parallel_degree ({sequence_parallel_degree}) " f"must be less than or equal to world_size ({world_size})" @@ -68,10 +91,8 @@ def register_ring_attn(sequence_parallel_degree: int, heads_k_stride: int | None f"must evenly divide world_size ({world_size})" ) - # Detailed logging of group formation - rank = dist.get_rank() + # Assign ranks to sequence parallel groups group_assignments = {} - for i in range(world_size // sequence_parallel_degree): ring_attn_ranks = list( range( @@ -92,35 +113,37 @@ def register_ring_attn(sequence_parallel_degree: int, heads_k_stride: int | None if rank == 0: LOG.info(f"Sequence parallel group assignments: {group_assignments}") - if heads_k_stride is None: - heads_k_stride = 1 + if ring_attn_func is RingAttnFunc.VARLEN_LLAMA3: + from ring_flash_attn import substitute_hf_flash_attn - from ring_flash_attn import substitute_hf_flash_attn + substitute_hf_flash_attn( + process_group=get_ring_attn_group(), heads_k_stride=heads_k_stride or 1 + ) + elif ring_attn_func in [ + RingAttnFunc.BATCH_RING, + RingAttnFunc.BATCH_ZIGZAG, + RingAttnFunc.BATCH_STRIPE, + ]: + from axolotl.monkeypatch.attention.ring_attn.adapters.batch import ( + substitute_hf_flash_attn, + ) - substitute_hf_flash_attn( - process_group=get_ring_attn_group(), heads_k_stride=heads_k_stride - ) + substitute_hf_flash_attn( + process_group=get_ring_attn_group(), + ring_attn_func=ring_attn_func, + ) -def update_ring_attn_params(batch: dict[str, torch.Tensor]): +def update_ring_attn_params(position_ids: torch.Tensor | None): """ Calculate the cumulative sequence lengths for the current forward pass and pass the value to the substituted `ring_flash_attn`. Args: - batch: A dictionary with a batch of data. May or may not contain `position_ids` - data; if not, we compute it. + position_ids: Optional tensor of position IDs (for sample packed data). """ from ring_flash_attn import update_ring_flash_attn_params - input_ids = batch["input_ids"] - position_ids = batch.get("position_ids") - if position_ids is None: - seq_len = input_ids.shape[1] - position_ids = torch.arange( - 0, seq_len, dtype=torch.long, device=input_ids.device - ).unsqueeze(0) - cu_seqlens, _ = get_cu_seqlens_from_pos_ids(position_ids) cu_seqlens = cu_seqlens.squeeze().to(device=torch.cuda.current_device()) update_ring_flash_attn_params(cu_seqlens, get_ring_attn_group()) diff --git a/src/axolotl/utils/collators/batching.py b/src/axolotl/utils/collators/batching.py index ed445ae565..738ef0dc52 100644 --- a/src/axolotl/utils/collators/batching.py +++ b/src/axolotl/utils/collators/batching.py @@ -4,7 +4,7 @@ """ from dataclasses import dataclass -from typing import Any, Optional, Union +from typing import Any import numpy as np import torch @@ -13,6 +13,7 @@ from transformers.utils import PaddingStrategy from axolotl.monkeypatch.attention.ring_attn import update_ring_attn_params +from axolotl.monkeypatch.attention.ring_attn.patch import RingAttnFunc @dataclass @@ -53,14 +54,15 @@ class DataCollatorForSeq2Seq: """ tokenizer: PreTrainedTokenizerBase - model: Optional[Any] = None - padding: Union[bool, str, PaddingStrategy] = True - max_length: Optional[int] = None - pad_to_multiple_of: Optional[int] = None + model: Any | None = None + padding: bool | str | PaddingStrategy = True + max_length: int | None = None + pad_to_multiple_of: int | None = None label_pad_token_id: int = -100 position_pad_token_id: int = 0 return_tensors: str = "pt" sequence_parallel_degree: int = 1 + ring_attn_func: RingAttnFunc | None = None def __post_init__(self): if self.sequence_parallel_degree > 1: @@ -157,19 +159,41 @@ def apply_sequence_parallelism( Sliced batch dictionary. """ # Get local (start, end) for sequence parallelism slicing - total_seq_len = batch["input_ids"].shape[1] - slice_size = total_seq_len // self.local_world_size - start = self.local_rank * slice_size - end = start + slice_size + total_seq_len = batch["input_ids"].size(1) - # Update params for ring attention calculation - update_ring_attn_params(batch=batch) + # Update params for varlen ring attention calculation + if batch.get("position_ids") is not None: + update_ring_attn_params(position_ids=batch["position_ids"]) # Slice batch for sequence parallel processing - keys_to_slice = ["input_ids", "attention_mask", "labels", "position_ids"] - for key in keys_to_slice: - if key in batch: - batch[key] = batch[key][:, start:end] + for key in batch: + if batch[key].size(1) == total_seq_len: + if self.ring_attn_func in [ + RingAttnFunc.VARLEN_LLAMA3, + RingAttnFunc.BATCH_RING, + ]: + batch[key] = ( + batch[key] + .chunk(self.local_world_size, dim=1)[self.local_rank] + .contiguous() + ) + elif self.ring_attn_func is RingAttnFunc.BATCH_ZIGZAG: + chunks = batch[key].chunk(2 * self.local_world_size, dim=1) + + # Take rank's chunk and opposing chunk for zigzag pattern + selected_chunks = [ + chunks[self.local_rank], + chunks[2 * self.local_world_size - self.local_rank - 1], + ] + batch[key] = torch.cat(selected_chunks, dim=1).contiguous() + elif self.ring_attn_func is RingAttnFunc.BATCH_STRIPE: + # TODO(djsaunde): This doesn't seem to work as expected + # Split into striped data and stack + tensor = torch.stack( + batch[key].split(self.local_world_size, dim=1), + dim=1, + ).transpose(1, 2) + batch[key] = tensor[:, self.local_rank].contiguous() return batch diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 4d43669942..d7105daba5 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -655,6 +655,7 @@ def apply_patches(self) -> None: register_ring_attn( sequence_parallel_degree=self.cfg.sequence_parallel_degree, heads_k_stride=self.cfg.heads_k_stride, + ring_attn_func=self.cfg.ring_attn_func, ) def patch_attention(self) -> None: @@ -1119,7 +1120,7 @@ def _configure_zero3_memory_efficient_loading(): return skip_move_to_device - def ajust_model_config(self) -> None: + def adjust_model_config(self) -> None: if ( hasattr(self.model, "config") and hasattr(self.model.config, "max_position_embeddings") @@ -1279,7 +1280,7 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: else: self.model.tie_weights() - self.ajust_model_config() + self.adjust_model_config() # log device memory usage if hasattr(self.model, "device") and self.model.device.type in ( diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 8a76c4eb48..a0fc2c7d3f 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -259,6 +259,7 @@ class AxolotlInputConfig( sequence_parallel_degree: int | None = None heads_k_stride: int | None = None + ring_attn_func: str | None = None special_tokens: SpecialTokensConfig | None = None tokens: list[str] | None = None @@ -1147,7 +1148,7 @@ def check_kto_config(cls, data): return data - @field_validator("sequence_parallel_degree", mode="before") + @field_validator("sequence_parallel_degree", mode="after") @classmethod def check_sequence_parallel_degree(cls, value, info): if not value: @@ -1159,9 +1160,12 @@ def check_sequence_parallel_degree(cls, value, info): "flash_attention: true must be set with sequence_parallel_degree > 1" ) - if not info.data["micro_batch_size"] == 1: + if ( + info.data.get("sample_packing") + and not info.data["micro_batch_size"] == 1 + ): raise ValueError( - "micro_batch_size must be set to 1 " + "micro_batch_size must be set to 1 when sample_packing is enabled" "due to a `ring-flash-attn` requirement" ) @@ -1188,6 +1192,34 @@ def check_sequence_parallel_degree(cls, value, info): return value + @field_validator("ring_attn_func", mode="after") + @classmethod + def check_ring_attn_func(cls, value, info): + if not info.data.get("sequence_parallel_degree", 1) > 1: + return value + + from axolotl.monkeypatch.attention.ring_attn.patch import RingAttnFunc + + if value is not None: + # Set the ring attention function if passed in config + valid_funcs = list(RingAttnFunc) + if value in valid_funcs: + value = RingAttnFunc(value) + else: + raise ValueError( + f"ring_attn_func: {value} must be one of {valid_funcs}" + ) + else: + # Default ring attention function selection + sample_packing = info.data.get("sample_packing") + value = ( + RingAttnFunc.VARLEN_LLAMA3 + if sample_packing + else RingAttnFunc.BATCH_RING + ) + + return value + @model_validator(mode="before") @classmethod def check_muon_deepspeed_fsdp(cls, data): diff --git a/tests/e2e/multigpu/test_sp.py b/tests/e2e/multigpu/test_sp.py index 288720eec2..72e5cb88c4 100644 --- a/tests/e2e/multigpu/test_sp.py +++ b/tests/e2e/multigpu/test_sp.py @@ -3,6 +3,7 @@ import os from pathlib import Path +import pytest import yaml from accelerate.test_utils import execute_subprocess_async from transformers.testing_utils import get_torch_dist_unique_port @@ -17,8 +18,15 @@ class TestSequenceParallelism: """Test case for training with sequence parallelism enabled""" - def test_sequence_parallel_training(self, temp_dir): - # pylint: disable=duplicate-code + def _run_sequence_parallel_test( + self, + temp_dir, + sample_packing=True, + micro_batch_size=1, + pad_to_sequence_len=True, + ring_attn_func=None, + ): + """Helper method to run sequence parallel tests with different configurations""" cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -27,9 +35,9 @@ def test_sequence_parallel_training(self, temp_dir): "strict": False, "sequence_len": 2048, "adapter": "qlora", - "sample_packing": True, - "eval_sample_packing": True, - "pad_to_sequence_len": True, + "sample_packing": sample_packing, + "eval_sample_packing": sample_packing, + "pad_to_sequence_len": pad_to_sequence_len, "lora_r": 8, "lora_alpha": 16, "lora_dropout": 0.05, @@ -45,7 +53,7 @@ def test_sequence_parallel_training(self, temp_dir): ], "num_epochs": 1, "max_steps": 8, - "micro_batch_size": 1, + "micro_batch_size": micro_batch_size, "gradient_accumulation_steps": 2, "output_dir": temp_dir, "learning_rate": 0.00001, @@ -61,6 +69,7 @@ def test_sequence_parallel_training(self, temp_dir): "weight_decay": 0.0, "use_tensorboard": True, "sequence_parallel_degree": 2, + "ring_attn_func": ring_attn_func, } ) @@ -86,3 +95,35 @@ def test_sequence_parallel_training(self, temp_dir): check_tensorboard( temp_dir + "/runs", "train/train_loss", 2.6, "Train Loss is too high" ) + + @pytest.mark.parametrize( + "sample_packing, micro_batch_size, pad_to_sequence_len, ring_attn_func", + [ + (True, 1, True, None), # defaults to varlen_llama3 ring_attn_func + (False, 2, True, None), # defaults to batch_ring ring_attn_func + (False, 2, True, "batch_zigzag"), + # (False, 2, False), # not yet working + ], + ids=[ + "sample_packing, varlen_llama3 ring_attn_func", + "no sample_packing, no pad_to_sequence_len, batch_ring ring_attn_func", + "no sample_packing, no pad_to_sequence_len, batch_zigzag ring_attn_func", + # "no sample_packing, pad_to_sequence_len", # not yet working + ], + ) + def test_sequence_parallel_training( + self, + temp_dir, + sample_packing, + micro_batch_size, + pad_to_sequence_len, + ring_attn_func, + ): + """Test sequence parallel training with different configurations""" + self._run_sequence_parallel_test( + temp_dir, + sample_packing=sample_packing, + micro_batch_size=micro_batch_size, + pad_to_sequence_len=pad_to_sequence_len, + ring_attn_func=ring_attn_func, + ) diff --git a/tests/e2e/patched/test_sp.py b/tests/e2e/patched/test_sp.py index 1361a85221..70a601f639 100644 --- a/tests/e2e/patched/test_sp.py +++ b/tests/e2e/patched/test_sp.py @@ -73,7 +73,10 @@ def test_register_ring_attn( self, mock_world_size, mock_rank, mock_new_group, partial_state ): """Test that ring attention groups are created correctly.""" - from axolotl.monkeypatch.attention.ring_attn import register_ring_attn + from axolotl.monkeypatch.attention.ring_attn import ( + RingAttnFunc, + register_ring_attn, + ) # Setup mocks mock_world_size.return_value = 8 # 8 GPUs total @@ -82,7 +85,11 @@ def test_register_ring_attn( mock_new_group.return_value = mock_group # Call register_ring_attn with size 4 - register_ring_attn(sequence_parallel_degree=4, heads_k_stride=1) + register_ring_attn( + sequence_parallel_degree=4, + heads_k_stride=1, + ring_attn_func=RingAttnFunc.VARLEN_LLAMA3, + ) # Verify the number of calls without examining the arguments assert mock_new_group.call_count == 2 From 69eda209a6a7d9b04ed54719c5e45e3e9313f431 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 16 Apr 2025 14:48:40 -0700 Subject: [PATCH 0555/1405] re-enable DS zero3 ci with updated transformers (#2533) --- tests/e2e/multigpu/test_llama.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 3bacac821d..1ff795bd65 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -621,12 +621,6 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" ) - # TODO: remove skip once deepspeed regression is fixed - # see https://github.com/huggingface/transformers/pull/37324 - @pytest.mark.skipif( - transformers_version_eq("4.51.0"), - reason="zero3 is not supported with transformers==4.51.0", - ) @pytest.mark.parametrize( "gradient_accumulation_steps", [1, 2], From f776f889a164b4440debd3c7d2f35f8356aaea44 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 16 Apr 2025 18:02:17 -0400 Subject: [PATCH 0556/1405] adding codecov reporting (#2372) [skip ci] * adding codecov reporting * update codecov-action to v5 * fix --------- Co-authored-by: Dan Saunders --- .coveragerc | 14 +++++++++ .github/workflows/tests.yml | 13 ++++++-- README.md | 1 + cicd/cicd.sh | 63 ++++++++++++++++++++++++++++++++----- cicd/multigpu.sh | 19 +++++++++++ codecov.yml | 51 ++++++++++++++++++++++++++++++ requirements-dev.txt | 2 +- requirements-tests.txt | 4 ++- 8 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 .coveragerc create mode 100644 codecov.yml diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..c5670410c7 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,14 @@ +[run] +source = axolotl +omit = + */tests/* + setup.py + +[report] +exclude_lines = + pragma: no cover + def __repr__ + raise NotImplementedError + if __name__ == .__main__.: + pass + raise ImportError diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9eb85a5b13..863687128c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -102,9 +102,16 @@ jobs: - name: Run tests run: | - pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ - pytest -v tests/patched/ - pytest -v tests/cli/ + pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ --cov=axolotl --cov-report=xml + pytest -v tests/patched/ --cov=axolotl --cov-append --cov-report=xml + pytest -v tests/cli/ --cov=axolotl --cov-append --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + files: ./coverage.xml + flags: unittests,pytorch-${{ matrix.pytorch_version }} + fail_ci_if_error: false - name: cleanup pip cache run: | diff --git a/README.md b/README.md index 0f2907df44..56e45e3fed 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@

GitHub License tests + codecov Releases
contributors diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 1d9ea7fbe7..3ae4896aca 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -3,10 +3,59 @@ set -e python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__" -pytest -v --durations=10 -n8 --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli /workspace/axolotl/tests/ -pytest -v --durations=10 /workspace/axolotl/tests/e2e/patched/lora_kernels # running these with the other patches causes a failure -pytest -v --durations=10 --ignore=tests/e2e/patched/lora_kernels /workspace/axolotl/tests/e2e/patched -pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/solo/ -pytest -v --durations=10 /workspace/axolotl/tests/e2e/integrations/ -pytest -v --durations=10 /workspace/axolotl/tests/cli -pytest -v --durations=10 --ignore=tests/e2e/solo/ --ignore=tests/e2e/patched/ --ignore=tests/e2e/multigpu/ --ignore=tests/e2e/integrations/ --ignore=tests/cli /workspace/axolotl/tests/e2e/ +# Run unit tests with initial coverage report +pytest -v --durations=10 -n8 \ + --ignore=tests/e2e/ \ + --ignore=tests/patched/ \ + --ignore=tests/cli \ + /workspace/axolotl/tests/ \ + --cov=axolotl \ + --cov-report=xml:coverage.xml + +# Run lora kernels tests with coverage append +pytest -v --durations=10 \ + /workspace/axolotl/tests/e2e/patched/lora_kernels \ + --cov=axolotl \ + --cov-append + +# Run patched tests excluding lora kernels with coverage append +pytest -v --durations=10 \ + --ignore=tests/e2e/patched/lora_kernels \ + /workspace/axolotl/tests/e2e/patched \ + --cov=axolotl \ + --cov-append + +# Run solo tests with coverage append +pytest -v --durations=10 -n1 \ + /workspace/axolotl/tests/e2e/solo/ \ + --cov=axolotl \ + --cov-append + +# Run integration tests with coverage append +pytest -v --durations=10 \ + /workspace/axolotl/tests/e2e/integrations/ \ + --cov=axolotl \ + --cov-append + +pytest -v --durations=10 /workspace/axolotl/tests/cli \ + --cov=axolotl \ + --cov-append + +# Run remaining e2e tests with coverage append and final report +pytest -v --durations=10 \ + --ignore=tests/e2e/solo/ \ + --ignore=tests/e2e/patched/ \ + --ignore=tests/e2e/multigpu/ \ + --ignore=tests/e2e/integrations/ \ + --ignore=tests/cli \ + /workspace/axolotl/tests/e2e/ \ + --cov=axolotl \ + --cov-append \ + --cov-report=xml:coverage.xml + +# Upload coverage to Codecov +if [ -f e2e-coverage.xml ]; then + codecov -f e2e-coverage.xml -F e2e,pytorch-${PYTORCH_VERSION} +else + echo "Coverage file not found. Coverage report may have failed." +fi diff --git a/cicd/multigpu.sh b/cicd/multigpu.sh index 008a74bfff..898cf11559 100755 --- a/cicd/multigpu.sh +++ b/cicd/multigpu.sh @@ -4,3 +4,22 @@ set -e # only run one test at a time so as not to OOM the GPU pytest -v --durations=10 -n2 /workspace/axolotl/tests/e2e/multigpu/ --ignore=/workspace/axolotl/tests/e2e/multigpu/solo/ pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/multigpu/solo/ + +# Only run two tests at a time to avoid OOM on GPU (with coverage collection) +pytest -v -n2 \ + --ignore=/workspace/axolotl/tests/e2e/multigpu/solo/ + /workspace/axolotl/tests/e2e/multigpu/ \ + --cov=axolotl \ + --cov-report=xml:multigpu-coverage.xml + +pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/multigpu/solo/ \ + --cov=axolotl \ + --cov-append \ + --cov-report=xml:multigpu-coverage.xml + +# Upload coverage to Codecov +if [ -f multigpu-coverage.xml ]; then + codecov -f multigpu-coverage.xml -F multigpu,docker-tests,pytorch-${PYTORCH_VERSION} +else + echo "Coverage file not found. Coverage report may have failed." +fi diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..6d47379e84 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,51 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + status: + project: + default: + # basic + target: auto + threshold: 0% + base: auto + # advanced + branches: null + if_no_uploads: error + if_not_found: success + if_ci_failed: error + only_pulls: false + flags: null + paths: null + patch: + default: + # basic + target: auto + threshold: 0% + base: auto + # advanced + branches: null + if_no_uploads: error + if_not_found: success + if_ci_failed: error + only_pulls: false + flags: null + paths: null + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "reach,diff,flags,files,footer" + behavior: default + require_changes: no + require_base: no + require_head: yes diff --git a/requirements-dev.txt b/requirements-dev.txt index 1dce5df5f2..5c42d96d4f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ -pre-commit black mypy +pre-commit types-requests quartodoc jupyter diff --git a/requirements-tests.txt b/requirements-tests.txt index a13f739231..43e3f88f45 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,7 @@ +codecov pytest -pytest-xdist +pytest-cov pytest-retry pytest-sugar +pytest-xdist tbparse From 32637fad0035ddc6839d517d5b9b6b468825ab13 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 17 Apr 2025 05:02:35 +0700 Subject: [PATCH 0557/1405] fix: preprocess yielding whole dataset to each worker (#2503) [skip ci] --- src/axolotl/utils/data/sft.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 726ec48582..35eab30c5f 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -332,16 +332,23 @@ def load_tokenized_prepared_datasets( if cfg.local_rank == 0 and not cfg.skip_prepare_dataset: LOG.info(f"Saving merged prepared dataset to disk... {prepared_ds_path}") if isinstance(dataset, IterableDataset): + num_workers = cfg.dataset_processes - def gen_from_iter_ds(_ds, _=None): - yield from _ds + def gen_from_iter_ds(_ds, worker_id: List[int], num_workers: List[int]): + """Generator function to correctly splice the dataset for each worker""" + for i, item in enumerate(_ds): + if i % num_workers[0] == worker_id[0]: + yield item ds_from_iter = Dataset.from_generator( functools.partial(gen_from_iter_ds, dataset), features=dataset.features, - num_proc=cfg.dataset_processes, + num_proc=num_workers, split=split, - gen_kwargs={"_": list(range(cfg.dataset_processes))}, + gen_kwargs={ + "worker_id": list(range(num_workers)), + "num_workers": [num_workers] * num_workers, + }, ) ds_from_iter.save_to_disk(str(prepared_ds_path)) else: From 9da730d6a465ffaf753e26a303120af3013d78ee Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 17 Apr 2025 05:02:51 +0700 Subject: [PATCH 0558/1405] fix(doc): cut cross entropy installation instructions broken in qmd (#2532) --- src/axolotl/integrations/cut_cross_entropy/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 618af466f8..e18d7df06b 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -12,11 +12,13 @@ See https://github.com/apple/ml-cross-entropy Run the following command to install `cut_cross_entropy[transformers]` if you don't have it already. +- If you are in dev environment ```bash -# if you are in dev environment python scripts/cutcrossentropy_install.py | sh +``` -# if you are not in dev environment +- If you are installing from pip +```bash pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@bad6f7b49c75fdec69471abb71b4cddd0f0c6438" ``` From 60a8f0958ddc2334c6ffc7b56a70a7726241cb2c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 17 Apr 2025 17:27:19 -0700 Subject: [PATCH 0559/1405] zero val fix for beta (#2538) --- src/axolotl/core/trainer_builder.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 7c2be49568..a51211263f 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1040,9 +1040,11 @@ def build_training_arguments(self, total_num_steps): if self.cfg.dataset_processes: training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes - if (self.cfg.trl and self.cfg.trl.beta) or self.cfg.rl_beta: - training_args_kwargs["beta"] = self.cfg.trl.beta or self.cfg.rl_beta - if self.cfg.orpo_alpha: + if self.cfg.trl and self.cfg.trl.beta is not None: + training_args_kwargs["beta"] = self.cfg.trl.beta + elif self.cfg.rl_beta is not None: + training_args_kwargs["beta"] = self.cfg.rl_beta + elif self.cfg.orpo_alpha is not None: # trl does some odd mapping of alpha to beta to reuse the beta parameter ??? training_args_kwargs["beta"] = self.cfg.orpo_alpha From 4ce469d32e1fb7dd16da8ff9253de76ca31faabd Mon Sep 17 00:00:00 2001 From: Chiwan Park Date: Sat, 19 Apr 2025 01:57:40 +0900 Subject: [PATCH 0560/1405] fix: upgrade liger to 0.5.8 and use native Gemma3 patches (#2527) * fix: upgrade liger to 0.5.8 and use native Gemma3 patches * fix: make lint happy * doc: update Liger Kernel FLCE support for Gemma 3 --- requirements.txt | 2 +- src/axolotl/integrations/liger/README.md | 2 +- src/axolotl/integrations/liger/__init__.py | 34 ---------------------- 3 files changed, 2 insertions(+), 36 deletions(-) diff --git a/requirements.txt b/requirements.txt index 76e8be8c16..2827c2ca1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 autoawq==0.2.7.post3 -liger-kernel==0.5.6 +liger-kernel==0.5.8 # END section packaging==23.2 diff --git a/src/axolotl/integrations/liger/README.md b/src/axolotl/integrations/liger/README.md index 03422f8899..c5cce8282b 100644 --- a/src/axolotl/integrations/liger/README.md +++ b/src/axolotl/integrations/liger/README.md @@ -25,7 +25,7 @@ liger_fused_linear_cross_entropy: true - deepseek_v2 - gemma - gemma2 -- gemma3 (partial support, no support for FLCE yet) +- gemma3 - granite - jamba - llama diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index 8e305e0f33..4e8d00552c 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -21,7 +21,6 @@ import inspect import logging import sys -from functools import partial from axolotl.integrations.base import BasePlugin @@ -55,7 +54,6 @@ def pre_model_load(self, cfg): ) from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss from liger_kernel.transformers.functional import liger_cross_entropy - from liger_kernel.transformers.geglu import LigerGEGLUMLP from liger_kernel.transformers.layer_norm import LigerLayerNorm from liger_kernel.transformers.monkey_patch import MODEL_TYPE_TO_APPLY_LIGER_FN from liger_kernel.transformers.rms_norm import LigerRMSNorm @@ -141,38 +139,6 @@ def pre_model_load(self, cfg): modeling_mod.CrossEntropyLoss = LigerCrossEntropyLoss if cfg.liger_fused_linear_cross_entropy: modeling_mod.DeepseekV2ForCausalLM.forward = deepseekv2_lce_forward - elif cfg.model_config_type in ["gemma3", "gemma3_text"]: - from transformers.models.gemma3 import modeling_gemma3 - - if cfg.liger_rope: - modeling_gemma3.apply_rotary_pos_emb = liger_rotary_pos_emb - if cfg.liger_rms_norm: - - def _liger_rms_norm_wrapper(dim, **kwargs): - "Convert 'dim' keyword to 'hidden_size' to pass to LigerRMSNorm" - return LigerRMSNorm(hidden_size=dim, **kwargs) - - modeling_gemma3.Gemma3RMSNorm = partial( - _liger_rms_norm_wrapper, - offset=1.0, - casting_mode="gemma", - init_fn="zeros", - in_place=False, - ) - if cfg.liger_glu_activation: - modeling_gemma3.Gemma3MLP = LigerGEGLUMLP - if cfg.liger_layer_norm: - modeling_gemma3.nn.LayerNorm = LigerLayerNorm - - if cfg.liger_cross_entropy: - from transformers.loss.loss_utils import nn - - nn.functional.cross_entropy = liger_cross_entropy - - if cfg.liger_fused_linear_cross_entropy: - raise NotImplementedError( - "Fused linear cross entropy is not yet supported for Gemma3." - ) elif cfg.model_config_type == "llama4": from axolotl.integrations.liger.models.llama4 import ( apply_liger_kernel_to_llama4, From b640db1dbce079dc592fd389f4d784f622ca2b1a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 21 Apr 2025 10:24:13 -0400 Subject: [PATCH 0561/1405] don't run multigpu tests twice, run SP in separate test (#2542) * don't run multigpu tests twice, run SP in separate test * fix multiline --- cicd/multigpu.sh | 12 +++++++----- tests/e2e/multigpu/patched/__init__.py | 0 tests/e2e/multigpu/{ => patched}/test_sp.py | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/multigpu/patched/__init__.py rename tests/e2e/multigpu/{ => patched}/test_sp.py (99%) diff --git a/cicd/multigpu.sh b/cicd/multigpu.sh index 898cf11559..519ac01339 100755 --- a/cicd/multigpu.sh +++ b/cicd/multigpu.sh @@ -1,13 +1,10 @@ #!/bin/bash set -e -# only run one test at a time so as not to OOM the GPU -pytest -v --durations=10 -n2 /workspace/axolotl/tests/e2e/multigpu/ --ignore=/workspace/axolotl/tests/e2e/multigpu/solo/ -pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/multigpu/solo/ - # Only run two tests at a time to avoid OOM on GPU (with coverage collection) pytest -v -n2 \ - --ignore=/workspace/axolotl/tests/e2e/multigpu/solo/ + --ignore=/workspace/axolotl/tests/e2e/multigpu/solo/ \ + --ignore=/workspace/axolotl/tests/e2e/multigpu/patched/ \ /workspace/axolotl/tests/e2e/multigpu/ \ --cov=axolotl \ --cov-report=xml:multigpu-coverage.xml @@ -17,6 +14,11 @@ pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/multigpu/solo/ \ --cov-append \ --cov-report=xml:multigpu-coverage.xml +pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/multigpu/patched/ \ + --cov=axolotl \ + --cov-append \ + --cov-report=xml:multigpu-coverage.xml + # Upload coverage to Codecov if [ -f multigpu-coverage.xml ]; then codecov -f multigpu-coverage.xml -F multigpu,docker-tests,pytorch-${PYTORCH_VERSION} diff --git a/tests/e2e/multigpu/patched/__init__.py b/tests/e2e/multigpu/patched/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/multigpu/test_sp.py b/tests/e2e/multigpu/patched/test_sp.py similarity index 99% rename from tests/e2e/multigpu/test_sp.py rename to tests/e2e/multigpu/patched/test_sp.py index 72e5cb88c4..1667408f49 100644 --- a/tests/e2e/multigpu/test_sp.py +++ b/tests/e2e/multigpu/patched/test_sp.py @@ -10,7 +10,7 @@ from axolotl.utils.dict import DictDefault -from ..utils import check_tensorboard +from ...utils import check_tensorboard os.environ["WANDB_DISABLED"] = "true" From b882dfb63fa3103eec35c754cd52dbba42091da6 Mon Sep 17 00:00:00 2001 From: Catgat <145307970+Catgat@users.noreply.github.com> Date: Mon, 21 Apr 2025 10:30:55 -0400 Subject: [PATCH 0562/1405] Fixed Rex Scheduler Warm Up (#2535) [skip ci] * Fixed Rex Scheduler Warm Up * chore: lint --------- Co-authored-by: Wing Lian --- src/axolotl/utils/schedulers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/schedulers.py b/src/axolotl/utils/schedulers.py index e9989b1afc..b550ac02c6 100644 --- a/src/axolotl/utils/schedulers.py +++ b/src/axolotl/utils/schedulers.py @@ -40,7 +40,7 @@ def __init__( self.max_lr = max_lr self.total_steps = total_steps self.num_warmup_steps = num_warmup_steps - self.last_step = last_step - 1 + self.last_step = max(last_step - 1, 0) # Ensure each parameter group has an "initial_lr" key to avoid issues when resuming. for group in optimizer.param_groups: From 341e95aac9d95a06dbc821250e6081e771e5d649 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 21 Apr 2025 10:31:35 -0400 Subject: [PATCH 0563/1405] prevent rate limiting to hf when using dispatch batches (#2536) [skip ci] --- src/axolotl/utils/data/sft.py | 25 ++++++++++++++++++++++--- src/axolotl/utils/schemas/config.py | 1 + 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 35eab30c5f..413f6d144f 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -3,6 +3,7 @@ import functools import logging import os +import tempfile from pathlib import Path from typing import List, Optional, Tuple, Union @@ -117,9 +118,27 @@ def prepare_dataset(cfg, tokenizer, processor=None, preprocess_iterable=None): cfg.pretraining_dataset[0]["type"] or "pretrain", ) - iter_ds = load_dataset( - path, streaming=True, split=split, name=name, data_files=data_files - ) + # when letting accelerator dispatch batches from the main process, we don't need to load the dataset from + # other ranks, we just need to present a fake dataset + if ( + cfg.accelerator_config + and cfg.accelerator_config.dispatch_batches + and not is_local_main_process() + ): + with tempfile.NamedTemporaryFile(mode="w+", delete=False) as f: + f.write("text\n") + f.write("lorem ipsum dolor sit amet\n") + # rewind the file pointer to the beginning so we can read it again + f.seek(0) + iter_ds = load_dataset( + "csv", data_files=f.name, split="train", streaming=True + ) + else: + if is_local_main_process(): + iter_ds = load_dataset( + path, streaming=True, split=split, name=name, data_files=data_files + ) + if skip: LOG.info(f"Skipping {skip} samples from the dataset") iter_ds = iter_ds.skip(skip) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index a0fc2c7d3f..732ae60cf1 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -660,6 +660,7 @@ def check_evals(cls, data): data.get("val_set_size") == 0 and (data.get("eval_steps") or data.get("eval_strategy")) and not data.get("test_datasets") + and data.get("eval_strategy") != "no" ): raise ValueError( "eval_steps and eval_strategy are not supported with val_set_size == 0" From 7651550850b7c8d0420bbf8354b6e3dd2728493b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 21 Apr 2025 10:31:50 -0400 Subject: [PATCH 0564/1405] make sure to download fixtures for kd test (#2541) * make sure to download fixtures for kd test * use same alpaca dataset --- tests/conftest.py | 26 ++++++++++++++++++++++++++ tests/e2e/multigpu/solo/__init__.py | 2 ++ tests/e2e/multigpu/solo/test_flex.py | 3 ++- tests/e2e/patched/test_resume.py | 3 ++- tests/e2e/solo/test_flex.py | 3 ++- tests/e2e/test_packing_loss.py | 3 ++- 6 files changed, 36 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 97c48db41d..3f3cc2732e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -193,6 +193,14 @@ def download_tiny_shakespeare_dataset(): snapshot_download_w_retry("winglian/tiny-shakespeare", repo_type="dataset") +@pytest.fixture(scope="session", autouse=True) +def download_evolkit_kd_sample_dataset(): + # download the dataset + snapshot_download_w_retry( + "axolotl-ai-co/evolkit-logprobs-pipeline-75k-v2-sample", repo_type="dataset" + ) + + @pytest.fixture(scope="session", autouse=True) def download_deepseek_model_fixture(): snapshot_download_w_retry("axolotl-ai-co/DeepSeek-V3-11M", repo_type="model") @@ -208,6 +216,16 @@ def download_huggyllama_model_fixture(): ) +@pytest.fixture(scope="session", autouse=True) +def download_llama33_70b_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "axolotl-ai-co/Llama-3.3-70B-Instruct-tokenizer", + repo_type="model", + allow_patterns=["*token*", "config.json"], + ) + + @pytest.fixture(scope="session", autouse=True) def download_llama_1b_model_fixture(): # download the tokenizer only @@ -315,6 +333,14 @@ def download_llama2_model_fixture(): ) +@pytest.fixture(scope="session", autouse=True) +def download_llama32_1b_model_fixture(): + snapshot_download_w_retry( + "osllmai-community/Llama-3.2-1B", + repo_type="model", + ) + + @pytest.fixture @enable_hf_offline def tokenizer_huggyllama( diff --git a/tests/e2e/multigpu/solo/__init__.py b/tests/e2e/multigpu/solo/__init__.py index e69de29bb2..ed1ba7dc62 100644 --- a/tests/e2e/multigpu/solo/__init__.py +++ b/tests/e2e/multigpu/solo/__init__.py @@ -0,0 +1,2 @@ +# Tests under this directory should get run "solo" on their own as they +# seem to cause issues when run in the same batch as other tests. diff --git a/tests/e2e/multigpu/solo/test_flex.py b/tests/e2e/multigpu/solo/test_flex.py index cbe3794b35..471b112c10 100644 --- a/tests/e2e/multigpu/solo/test_flex.py +++ b/tests/e2e/multigpu/solo/test_flex.py @@ -49,8 +49,9 @@ def test_loss_llama(self, temp_dir): }, "datasets": [ { - "path": "vicgalle/alpaca-gpt4", + "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index f6a3e0109e..68489ed034 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -46,8 +46,9 @@ def test_resume_lora_packed(self, temp_dir): }, "datasets": [ { - "path": "vicgalle/alpaca-gpt4", + "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 2, diff --git a/tests/e2e/solo/test_flex.py b/tests/e2e/solo/test_flex.py index 6de813e375..71da795f89 100644 --- a/tests/e2e/solo/test_flex.py +++ b/tests/e2e/solo/test_flex.py @@ -41,8 +41,9 @@ def test_loss_llama(self, temp_dir): }, "datasets": [ { - "path": "vicgalle/alpaca-gpt4", + "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index 4e8e704194..73716f44bb 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -40,8 +40,9 @@ def test_loss_packed(self, temp_dir): }, "datasets": [ { - "path": "vicgalle/alpaca-gpt4", + "path": "tatsu-lab/alpaca", "type": "alpaca", + "split": "train[:10%]", }, ], "num_epochs": 1, From 32e335dd51fa310e39f01dcaf58fcb90c2c94be1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 22 Apr 2025 10:16:48 -0400 Subject: [PATCH 0565/1405] fix missing host/port for vllm (#2543) * fix missing host/port for vllm * set tensor parallel size so it doesn't always default to cli override --- src/axolotl/cli/args.py | 12 ++++++------ src/axolotl/core/trainers/grpo/__init__.py | 4 ++-- src/axolotl/utils/schemas/vllm.py | 8 ++++++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py index 72e61d1bb8..83febd7f4c 100644 --- a/src/axolotl/cli/args.py +++ b/src/axolotl/cli/args.py @@ -39,16 +39,16 @@ class TrainerCliArgs: class VllmServeCliArgs: """Dataclass with CLI arguments for `axolotl vllm-serve` command.""" - tensor_parallel_size: int = field( - default=1, + tensor_parallel_size: Optional[int] = field( + default=None, metadata={"help": "Number of tensor parallel workers to use."}, ) - host: str = field( - default="0.0.0.0", # nosec B104 + host: Optional[str] = field( + default=None, # nosec B104 metadata={"help": "Host address to run the server on."}, ) - port: int = field( - default=8000, + port: Optional[int] = field( + default=None, metadata={"help": "Port to run the server on."}, ) gpu_memory_utilization: Optional[float] = field( diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 219eced69d..0d3e7b7d7e 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -40,8 +40,8 @@ def set_training_args_kwargs(cls, cfg): if trl.use_vllm: grpo_args_kwargs["use_vllm"] = trl.use_vllm - grpo_args_kwargs["vllm_server_host"] = trl.vllm_server_host - grpo_args_kwargs["vllm_server_port"] = trl.vllm_server_port + grpo_args_kwargs["vllm_server_host"] = trl.vllm_server_host or trl.vllm.host + grpo_args_kwargs["vllm_server_port"] = trl.vllm_server_port or trl.vllm.port if trl.vllm_server_timeout: grpo_args_kwargs["vllm_server_timeout"] = trl.vllm_server_timeout if trl.vllm_guided_decoding_regex: diff --git a/src/axolotl/utils/schemas/vllm.py b/src/axolotl/utils/schemas/vllm.py index bb1a4ba265..48441de5e7 100644 --- a/src/axolotl/utils/schemas/vllm.py +++ b/src/axolotl/utils/schemas/vllm.py @@ -36,3 +36,11 @@ class VllmConfig(BaseModel): default=None, json_schema_extra={"description": "Enable prefix caching for VLLM"}, ) + host: str | None = Field( + default="0.0.0.0", # nosec B104 + json_schema_extra={"description": "Host for the vLLM server to start on"}, + ) + port: int | None = Field( + default=8000, + json_schema_extra={"description": "Port of the vLLM server to start on"}, + ) From a6d28d19b1bd96a8bb9d730720b3960f6d9d13a9 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 23 Apr 2025 21:27:51 +0700 Subject: [PATCH 0566/1405] feat: add glm and glm4 multipack and cce (#2546) * feat: add glm and glm4 multipack * feat: add glm4 example * feat: add cce for glm --- examples/glm4/qlora-32b.yaml | 62 +++++++++++++++++++ .../integrations/cut_cross_entropy/README.md | 2 + .../cut_cross_entropy/monkeypatch/glm4.py | 57 +++++++++++++++++ .../cut_cross_entropy/monkeypatch/patch.py | 6 ++ src/axolotl/monkeypatch/multipack.py | 2 + 5 files changed, 129 insertions(+) create mode 100644 examples/glm4/qlora-32b.yaml create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/glm4.py diff --git a/examples/glm4/qlora-32b.yaml b/examples/glm4/qlora-32b.yaml new file mode 100644 index 0000000000..86d9b43f8b --- /dev/null +++ b/examples/glm4/qlora-32b.yaml @@ -0,0 +1,62 @@ +base_model: THUDM/GLM-4-32B-0414 +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_4bit: true + +datasets: + - path: teknium/GPT4-LLM-Cleaned + type: alpaca +dataset_prepared_path: last_run_prepared +val_set_size: 0 +output_dir: ./outputs/qlora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: true +pad_to_sequence_len: true + +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.05 +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +loss_watchdog_threshold: 5.0 +loss_watchdog_patience: 3 + +warmup_steps: 10 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index e18d7df06b..724e0688db 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -47,6 +47,8 @@ cut_cross_entropy: true - qwen2 - cohere - cohere2 +- glm +- glm4 ## Citation diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/glm4.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/glm4.py new file mode 100644 index 0000000000..3df909f884 --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/glm4.py @@ -0,0 +1,57 @@ +"""GLM 4 patch. GLM family inherits from Llama.""" + +from types import MethodType + +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, +) + + +def patch_glm( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + + # Set the _PATCH_OPTS in the llama patch file + import cut_cross_entropy.transformers.llama as llama_patch + + llama_patch._PATCH_OPTS = patch_options # pylint: disable=protected-access + + from cut_cross_entropy.transformers.llama import cce_forward + from transformers.models.glm import modeling_glm + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_glm.GlmForCausalLM + ), f"Expected a GlmForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward, maybe_model) + return maybe_model + + modeling_glm.GlmForCausalLM.forward = cce_forward + return None + + +def patch_glm4( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + + # Set the _PATCH_OPTS in the llama patch file + import cut_cross_entropy.transformers.llama as llama_patch + + llama_patch._PATCH_OPTS = patch_options # pylint: disable=protected-access + + from cut_cross_entropy.transformers.llama import cce_forward + from transformers.models.glm4 import modeling_glm4 + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_glm4.Glm4ForCausalLM + ), f"Expected a Glm4ForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward, maybe_model) + return maybe_model + + modeling_glm4.Glm4ForCausalLM.forward = cce_forward + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py index 5263956ce3..9e18c6b0b6 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py @@ -20,6 +20,10 @@ patch_gemma3, patch_gemma3_text, ) +from axolotl.integrations.cut_cross_entropy.monkeypatch.glm4 import ( + patch_glm, + patch_glm4, +) from axolotl.integrations.cut_cross_entropy.monkeypatch.llama4 import ( patch_llama4, patch_llama4_text, @@ -45,6 +49,8 @@ "qwen2": patch_qwen2, "cohere": patch_cohere, "cohere2": patch_cohere2, + "glm": patch_glm, + "glm4": patch_glm4, } diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 2b02699bdf..a2459ec5ad 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -31,6 +31,8 @@ "starcoder2", "deepseek_v2", "deepseek_v3", + "glm", + "glm4", ] From c4053481ff0f6739f1f9d513f1d02c55e6746a38 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 23 Apr 2025 10:33:30 -0400 Subject: [PATCH 0567/1405] Codecov fixes / improvements (#2549) * adding codecov reporting * random change * codecov fixes * adding missing dependency * fix --------- Co-authored-by: Dan Saunders --- .github/workflows/multi-gpu-e2e.yml | 1 + .github/workflows/tests-nightly.yml | 1 + .github/workflows/tests.yml | 3 +++ cicd/cicd.sh | 14 ++++---------- cicd/e2e_tests.py | 1 + cicd/multigpu.py | 1 + cicd/multigpu.sh | 16 ++++++---------- requirements-tests.txt | 1 + 8 files changed, 18 insertions(+), 20 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index f89de494d2..fcc6ee0218 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -67,6 +67,7 @@ jobs: echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV echo "NIGHTLY_BUILD=${{ matrix.nightly_build }}" >> $GITHUB_ENV + echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | modal run cicd.multigpu diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 9d3b056e14..77cefc992a 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -147,6 +147,7 @@ jobs: echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV echo "NIGHTLY_BUILD=${{ matrix.nightly_build }}" >> $GITHUB_ENV + echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | modal run cicd.e2e_tests diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 863687128c..ba1c837cbc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -109,6 +109,7 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: + token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml flags: unittests,pytorch-${{ matrix.pytorch_version }} fail_ci_if_error: false @@ -241,6 +242,7 @@ jobs: echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV + echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | modal run cicd.e2e_tests @@ -288,6 +290,7 @@ jobs: echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV + echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | modal run cicd.e2e_tests diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 3ae4896aca..89d746e5b6 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -9,8 +9,7 @@ pytest -v --durations=10 -n8 \ --ignore=tests/patched/ \ --ignore=tests/cli \ /workspace/axolotl/tests/ \ - --cov=axolotl \ - --cov-report=xml:coverage.xml + --cov=axolotl # Run lora kernels tests with coverage append pytest -v --durations=10 \ @@ -51,11 +50,6 @@ pytest -v --durations=10 \ /workspace/axolotl/tests/e2e/ \ --cov=axolotl \ --cov-append \ - --cov-report=xml:coverage.xml - -# Upload coverage to Codecov -if [ -f e2e-coverage.xml ]; then - codecov -f e2e-coverage.xml -F e2e,pytorch-${PYTORCH_VERSION} -else - echo "Coverage file not found. Coverage report may have failed." -fi + --cov-report=xml:e2e-coverage.xml + +codecov upload-process -t $CODECOV_TOKEN -f e2e-coverage.xml -F e2e,pytorch-${PYTORCH_VERSION} diff --git a/cicd/e2e_tests.py b/cicd/e2e_tests.py index d7cdcc4738..998f8c35da 100644 --- a/cicd/e2e_tests.py +++ b/cicd/e2e_tests.py @@ -28,6 +28,7 @@ "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), "NIGHTLY_BUILD": os.environ.get("NIGHTLY_BUILD", ""), + "CODECOV_TOKEN": os.environ.get("CODECOV_TOKEN", ""), "HF_HOME": "/workspace/data/huggingface-cache/hub", } diff --git a/cicd/multigpu.py b/cicd/multigpu.py index 66d4d99902..90d4ce1ee3 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -29,6 +29,7 @@ "CUDA": os.environ.get("CUDA", "121"), "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), + "CODECOV_TOKEN": os.environ.get("CODECOV_TOKEN", ""), "HF_HOME": "/workspace/data/huggingface-cache/hub", } diff --git a/cicd/multigpu.sh b/cicd/multigpu.sh index 519ac01339..1e6f014717 100755 --- a/cicd/multigpu.sh +++ b/cicd/multigpu.sh @@ -6,13 +6,13 @@ pytest -v -n2 \ --ignore=/workspace/axolotl/tests/e2e/multigpu/solo/ \ --ignore=/workspace/axolotl/tests/e2e/multigpu/patched/ \ /workspace/axolotl/tests/e2e/multigpu/ \ - --cov=axolotl \ - --cov-report=xml:multigpu-coverage.xml + --cov=axolotl -pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/multigpu/solo/ \ +# Run solo tests with coverage append +pytest -v --durations=10 -n1 \ + /workspace/axolotl/tests/e2e/multigpu/solo/ \ --cov=axolotl \ - --cov-append \ - --cov-report=xml:multigpu-coverage.xml + --cov-append pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/multigpu/patched/ \ --cov=axolotl \ @@ -20,8 +20,4 @@ pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/multigpu/patched/ \ --cov-report=xml:multigpu-coverage.xml # Upload coverage to Codecov -if [ -f multigpu-coverage.xml ]; then - codecov -f multigpu-coverage.xml -F multigpu,docker-tests,pytorch-${PYTORCH_VERSION} -else - echo "Coverage file not found. Coverage report may have failed." -fi +codecov upload-process -t $CODECOV_TOKEN -f multigpu-coverage.xml -F multigpu,docker-tests,pytorch-${PYTORCH_VERSION} diff --git a/requirements-tests.txt b/requirements-tests.txt index 43e3f88f45..93b2ceee59 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,4 +1,5 @@ codecov +codecov-cli pytest pytest-cov pytest-retry From 0d691cc2a7b8a10602cafaf88bf3b9f44bd4c878 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 23 Apr 2025 14:59:03 -0400 Subject: [PATCH 0568/1405] add base docker image with pytorch 2.7.0 and variant for cuda 12.8 (#2551) * add base docker image with pytorch 2.7.0 and variant for cuda 12.8 * my bash is terrible --- .github/workflows/base.yml | 12 ++++++++++++ docker/Dockerfile-base | 4 ++++ requirements.txt | 1 + 3 files changed, 17 insertions(+) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 30859a3749..d6850a0de0 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -46,6 +46,18 @@ jobs: python_version: "3.11" pytorch: 2.6.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + - cuda: "126" + cuda_version: 12.6.3 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.7.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + - cuda: "128" + cuda_version: 12.6.3 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.7.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 52201f276e..cf1af96829 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -37,3 +37,7 @@ RUN git lfs install --skip-repo && \ pip3 install awscli && \ # The base image ships with `pydantic==1.8.2` which is not working pip3 install -U --no-cache-dir pydantic==1.10.10 + +RUN if [ "$PYTORCH_VERSION" = "2.7.0" ] ; then \ + pip3 install flash-attn==2.7.4.post1; \ + fi diff --git a/requirements.txt b/requirements.txt index 2827c2ca1b..02da0abf4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ datasets==3.5.0 deepspeed>=0.15.4 trl==0.16.1 hf_xet==1.0.0 +hqq==0.2.5 optimum==1.16.2 hf_transfer From a4d5112ae1b2a89c778ee1df1ee638812b885946 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 24 Apr 2025 00:39:31 -0400 Subject: [PATCH 0569/1405] builds for torch 2.7.0 (#2552) * builds for torch==2.7.0 * use xformers==0.0.29.post3 * no vllm support with torch 2.7 * update default, fix conditional * no xformers for 270 * no vllm on 2.7.0 for multigpu test too * remove deprecated verbose arg from scheduler * 2.7.0 tests on cpu --- .github/workflows/main.yml | 12 +++++++++++- .github/workflows/multi-gpu-e2e.yml | 7 +++++++ .github/workflows/tests.yml | 8 +++++++- setup.py | 12 +++++++++--- src/axolotl/monkeypatch/relora.py | 2 +- 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index df12b3c893..14e30f20f0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,6 +31,11 @@ jobs: pytorch: 2.6.0 axolotl_extras: vllm is_latest: true + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.0 + axolotl_extras: vllm runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -93,6 +98,11 @@ jobs: pytorch: 2.6.0 axolotl_extras: is_latest: true + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -138,7 +148,7 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.4.1 + pytorch: 2.6.0 axolotl_extras: runs-on: axolotl-gpu-runner steps: diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index fcc6ee0218..aee4ddba68 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -45,6 +45,13 @@ jobs: axolotl_extras: vllm num_gpus: 2 nightly_build: "true" + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.0 + axolotl_extras: + num_gpus: 2 + nightly_build: "true" runs-on: [self-hosted, modal] timeout-minutes: 120 steps: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ba1c837cbc..825277ce08 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,7 +49,7 @@ jobs: max-parallel: 2 matrix: python_version: ["3.11"] - pytorch_version: ["2.4.1", "2.5.1", "2.6.0"] + pytorch_version: ["2.4.1", "2.5.1", "2.6.0", "2.7.0"] timeout-minutes: 20 steps: @@ -270,6 +270,12 @@ jobs: pytorch: 2.5.1 num_gpus: 1 axolotl_extras: vllm + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.0 + num_gpus: 1 + axolotl_extras: steps: - name: Checkout uses: actions/checkout@v4 diff --git a/setup.py b/setup.py index 6c911d8f7d..0f3892c3bb 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ def parse_requirements(extras_require_map): try: torch_version = version("torch") except PackageNotFoundError: - torch_version = "2.5.1" + torch_version = "2.6.0" # default to torch 2.6 _install_requires.append(f"torch=={torch_version}") version_match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", torch_version) @@ -64,9 +64,15 @@ def parse_requirements(extras_require_map): else: raise ValueError("Invalid version format") - if (major, minor) >= (2, 6): + if (major, minor) >= (2, 7): _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers==0.0.29.post2") + # _install_requires.append("xformers==0.0.29.post3") # xformers seems to be hard pinned to 2.6.0 + extras_require_map["vllm"] = ["vllm==0.8.3"] + elif (major, minor) >= (2, 6): + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append( + "xformers==0.0.29.post2" + ) # vllm needs post2 w torch 2.6 extras_require_map["vllm"] = ["vllm==0.8.3"] elif (major, minor) >= (2, 5): _install_requires.pop(_install_requires.index(xformers_version)) diff --git a/src/axolotl/monkeypatch/relora.py b/src/axolotl/monkeypatch/relora.py index 822fd4465e..4a27dde81f 100644 --- a/src/axolotl/monkeypatch/relora.py +++ b/src/axolotl/monkeypatch/relora.py @@ -272,7 +272,7 @@ def __init__( self.warmup_steps = warmup_steps self.anneal_steps = anneal_steps self.min_lr_scale = min_lr_scale - super().__init__(optimizer, inner_schedule.last_epoch, inner_schedule.verbose) + super().__init__(optimizer, inner_schedule.last_epoch) def get_lr(self) -> float: self.inner_schedule.last_epoch = self.last_epoch From 85053f4bd41396cd31c524b7e2bf432649344cdc Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 24 Apr 2025 12:03:43 +0700 Subject: [PATCH 0570/1405] Fix(doc): add delinearize instruction (#2545) * fix: mention to install pytorch before axolotl * feat(doc): include instruction to delinearize * fix: update instruction for delinearize with adapter --- docs/cli.qmd | 11 +++++++++++ docs/installation.qmd | 6 ++++++ examples/llama-4/README.md | 8 ++++++++ 3 files changed, 25 insertions(+) diff --git a/docs/cli.qmd b/docs/cli.qmd index 79892fc5a7..1003a210c0 100644 --- a/docs/cli.qmd +++ b/docs/cli.qmd @@ -199,6 +199,17 @@ output_dir: # Directory to save evaluation results See [LM Eval Harness](https://github.com/EleutherAI/lm-evaluation-harness) for more details. +### delinearize-llama4 + +Delinearizes a Llama 4 linearized model into a regular HuggingFace Llama 4 model. This only works with the non-quantized linearized model. + +```bash +axolotl delinearize-llama4 --model path/to/model_dir --output path/to/output_dir +``` + +This would be necessary to use with other frameworks. If you have an adapter, merge it with the non-quantized linearized model before delinearizing. + + ## Legacy CLI Usage While the new Click-based CLI is preferred, Axolotl still supports the legacy module-based CLI: diff --git a/docs/installation.qmd b/docs/installation.qmd index 6edec4610c..0cf5ffcebd 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -19,6 +19,12 @@ This guide covers all the ways you can install and set up Axolotl for your envir ## Installation Methods {#sec-installation-methods} +::: {.callout-important} +Please make sure to have Pytorch installed before installing Axolotl in your local environment. + +Follow the instructions at: [https://pytorch.org/get-started/locally/](https://pytorch.org/get-started/locally/) +::: + ### PyPI Installation (Recommended) {#sec-pypi} ```{.bash} diff --git a/examples/llama-4/README.md b/examples/llama-4/README.md index b33f8ae3c2..f5fc908a64 100644 --- a/examples/llama-4/README.md +++ b/examples/llama-4/README.md @@ -26,3 +26,11 @@ Multi-GPU (4xH100) for Llama 4 Scout uses 62.8GB VRAM/GPU @ 4k contenxt length @ ### Llama 4 Maverick 17Bx128Experts (400B) Coming Soon + +## Delinearized Llama 4 Models + +We provide a script to delinearize Llama 4 linearized models into regular HuggingFace Llama 4 models. + +```bash +axolotl delinearize-llama4 --model path/to/model_dir --output path/to/output_dir +``` From 66f41ec6f1e43819bec3fb2a845f7a7e5d873564 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Thu, 24 Apr 2025 08:51:51 -0400 Subject: [PATCH 0571/1405] disable codecov pr annotations (#2556) --- codecov.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/codecov.yml b/codecov.yml index 6d47379e84..b4810bfa44 100644 --- a/codecov.yml +++ b/codecov.yml @@ -49,3 +49,6 @@ comment: require_changes: no require_base: no require_head: yes + +github_checks: + annotations: false From 1447beb1328f8298ae0b7333091609f895fc596e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 24 Apr 2025 13:01:43 -0400 Subject: [PATCH 0572/1405] make sure to validate the config before normalizing so defaults get set (#2554) * make sure to validate the config before normalizing so defaults get set * validation not needed for particular test * remove duplicate validations * set qlora correctly --- tests/e2e/integrations/test_cut_cross_entropy.py | 5 ++++- tests/e2e/integrations/test_liger.py | 4 +++- tests/e2e/patched/test_4d_multipack_llama.py | 4 +++- tests/e2e/patched/test_falcon_samplepack.py | 4 +++- tests/e2e/patched/test_fused_llama.py | 3 ++- tests/e2e/patched/test_llama_s2_attention.py | 4 +++- tests/e2e/patched/test_lora_llama_multipack.py | 4 +++- tests/e2e/patched/test_mistral_samplepack.py | 4 +++- tests/e2e/patched/test_mixtral_samplepack.py | 3 ++- tests/e2e/patched/test_model_patches.py | 4 +++- tests/e2e/patched/test_phi_multipack.py | 6 ++++-- tests/e2e/patched/test_resume.py | 3 ++- tests/e2e/patched/test_unsloth_qlora.py | 5 ++++- tests/e2e/test_embeddings_lr.py | 1 + tests/e2e/test_llama_vision.py | 1 + tests/e2e/test_phi.py | 3 ++- tests/e2e/test_process_reward_model_smollm2.py | 3 ++- tests/test_exact_deduplication.py | 3 ++- 18 files changed, 47 insertions(+), 17 deletions(-) diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py index 7539345634..2ae59a15a2 100644 --- a/tests/e2e/integrations/test_cut_cross_entropy.py +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -8,7 +8,7 @@ from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils import get_pytorch_version -from axolotl.utils.config import normalize_config, prepare_plugins +from axolotl.utils.config import normalize_config, prepare_plugins, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists @@ -56,6 +56,7 @@ class TestCutCrossEntropyIntegration: # pylint: disable=redefined-outer-name def test_llama_w_cce(self, min_cfg, temp_dir): cfg = DictDefault(min_cfg) + cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() @@ -101,6 +102,7 @@ def test_qwen2_w_cce(self, temp_dir): "bf16": "auto", } ) + cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() @@ -129,6 +131,7 @@ def test_llama_w_cce_and_attention(self, min_cfg, temp_dir, attention_type): attention_type: True, } ) + cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() diff --git a/tests/e2e/integrations/test_liger.py b/tests/e2e/integrations/test_liger.py index 03c83083d2..8ecfc4746e 100644 --- a/tests/e2e/integrations/test_liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -5,7 +5,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config, prepare_plugins +from axolotl.utils.config import normalize_config, prepare_plugins, validate_config from axolotl.utils.dict import DictDefault from tests.e2e.utils import check_model_output_exists, require_torch_2_4_1 @@ -54,6 +54,7 @@ def test_llama_wo_flce(self, temp_dir): } ) # pylint: disable=duplicate-code + cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() @@ -100,6 +101,7 @@ def test_llama_w_flce(self, temp_dir): } ) # pylint: disable=duplicate-code + cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index 7beb71145b..33ba47abda 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists, with_temp_dir @@ -60,6 +60,7 @@ def test_sdp_lora_packing(self, temp_dir): "fp16": True, } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -104,6 +105,7 @@ def test_torch_lora_packing(self, temp_dir): "fp16": True, } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/patched/test_falcon_samplepack.py b/tests/e2e/patched/test_falcon_samplepack.py index 62ee4f717a..0034169af1 100644 --- a/tests/e2e/patched/test_falcon_samplepack.py +++ b/tests/e2e/patched/test_falcon_samplepack.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists, with_temp_dir @@ -63,6 +63,7 @@ def test_qlora(self, temp_dir): "bf16": "auto", } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -103,6 +104,7 @@ def test_ft(self, temp_dir): "bf16": "auto", } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index f8f2455141..51dfec5f49 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -12,7 +12,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists, with_temp_dir @@ -67,6 +67,7 @@ def test_fft_packing(self, temp_dir): cfg.bf16 = True else: cfg.fp16 = True + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py index b8ddf10dae..3aa36772ae 100644 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ b/tests/e2e/patched/test_llama_s2_attention.py @@ -11,7 +11,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists, with_temp_dir @@ -65,6 +65,7 @@ def test_lora_s2_attn(self, temp_dir): } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -105,6 +106,7 @@ def test_fft_s2_attn(self, temp_dir): } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index e544eb4fda..ab6e87e2ad 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -12,7 +12,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists, with_temp_dir @@ -70,6 +70,7 @@ def test_lora_packing(self, temp_dir): else: cfg.fp16 = True + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -120,6 +121,7 @@ def test_lora_gptq_packed(self, temp_dir): "lr_scheduler": "cosine", } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index f9e5236794..3bc0fcfbcf 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists, with_temp_dir @@ -63,6 +63,7 @@ def test_lora_packing(self, temp_dir): "bf16": "auto", } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -104,6 +105,7 @@ def test_ft_packing(self, temp_dir): "bf16": "auto", } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index 8746c923b0..2d4f97084f 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists, with_temp_dir @@ -60,6 +60,7 @@ def test_qlora(self, temp_dir): "bf16": "auto", } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/patched/test_model_patches.py b/tests/e2e/patched/test_model_patches.py index ec09e0c81a..8a75db52e4 100644 --- a/tests/e2e/patched/test_model_patches.py +++ b/tests/e2e/patched/test_model_patches.py @@ -6,7 +6,7 @@ import transformers -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_model, load_tokenizer @@ -47,6 +47,7 @@ def test_mixtral_multipack(self, temp_dir): "eval_steps": 10, } ) + cfg = validate_config(cfg) normalize_config(cfg) tokenizer = load_tokenizer(cfg) load_model(cfg, tokenizer, inference=False) @@ -79,6 +80,7 @@ def test_mistral_multipack(self, temp_dir): "eval_steps": 10, } ) + cfg = validate_config(cfg) normalize_config(cfg) tokenizer = load_tokenizer(cfg) load_model(cfg, tokenizer, inference=False) diff --git a/tests/e2e/patched/test_phi_multipack.py b/tests/e2e/patched/test_phi_multipack.py index 70b3ea124f..c42ed8baf8 100644 --- a/tests/e2e/patched/test_phi_multipack.py +++ b/tests/e2e/patched/test_phi_multipack.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists, with_temp_dir @@ -63,6 +63,7 @@ def test_ft_packed(self, temp_dir): } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -82,7 +83,7 @@ def test_qlora_packed(self, temp_dir): "sample_packing": True, "flash_attention": True, "pad_to_sequence_len": True, - "load_in_8bit": False, + "load_in_4bit": True, "adapter": "qlora", "lora_r": 64, "lora_alpha": 32, @@ -114,6 +115,7 @@ def test_qlora_packed(self, temp_dir): } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index 68489ed034..a84759baea 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -12,7 +12,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists, most_recent_subdir @@ -68,6 +68,7 @@ def test_resume_lora_packed(self, temp_dir): cfg.bf16 = True else: cfg.fp16 = True + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index 4cea0d26fa..5f8fde6b4d 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -10,7 +10,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from ..utils import check_model_output_exists, check_tensorboard @@ -72,6 +72,7 @@ def test_unsloth_llama_qlora_fa2(self, temp_dir, sample_packing): } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -122,6 +123,7 @@ def test_unsloth_llama_qlora_unpacked(self, temp_dir): } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) @@ -177,6 +179,7 @@ def test_unsloth_llama_qlora_unpacked_no_fa2_fp16(self, temp_dir, sdp_attention) } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py index 687b6637fc..82b822ad60 100644 --- a/tests/e2e/test_embeddings_lr.py +++ b/tests/e2e/test_embeddings_lr.py @@ -102,6 +102,7 @@ def test_train_w_embedding_lr(self, temp_dir): "use_tensorboard": True, } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py index 3fc12afcce..e1e496ccf8 100644 --- a/tests/e2e/test_llama_vision.py +++ b/tests/e2e/test_llama_vision.py @@ -109,6 +109,7 @@ def test_lora_llama_vision_multimodal_dataset(self, temp_dir): "bf16": True, } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_phi.py b/tests/e2e/test_phi.py index 268646432c..f531a17c50 100644 --- a/tests/e2e/test_phi.py +++ b/tests/e2e/test_phi.py @@ -79,7 +79,7 @@ def test_phi_qlora(self, temp_dir): "tokenizer_type": "AutoTokenizer", "sequence_len": 2048, "sample_packing": False, - "load_in_8bit": False, + "load_in_4bit": True, "adapter": "qlora", "lora_r": 64, "lora_alpha": 32, @@ -111,6 +111,7 @@ def test_phi_qlora(self, temp_dir): "bf16": "auto", } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/test_process_reward_model_smollm2.py b/tests/e2e/test_process_reward_model_smollm2.py index 19347cf928..446facdb0d 100644 --- a/tests/e2e/test_process_reward_model_smollm2.py +++ b/tests/e2e/test_process_reward_model_smollm2.py @@ -9,7 +9,7 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault from .utils import check_model_output_exists, check_tensorboard, with_temp_dir @@ -57,6 +57,7 @@ def test_prm(self, temp_dir): "seed": 42, } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index 4d069a11d9..1d41a248d7 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -11,7 +11,7 @@ import pytest from datasets import Dataset -from axolotl.utils.config import normalize_config +from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.data import prepare_dataset from axolotl.utils.data.rl import load_prepare_preference_datasets from axolotl.utils.data.utils import deduplicate_and_log_datasets @@ -319,6 +319,7 @@ def setUp(self) -> None: "num_epochs": 1, } ) + self.cfg_1 = validate_config(self.cfg_1) normalize_config(self.cfg_1) @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") From ae1c7ace63f00112d3fd7152203b53324a16790b Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 25 Apr 2025 10:33:54 -0400 Subject: [PATCH 0573/1405] Sequence parallel training context manager (#2553) * ctx manager for SP * updates * update * further simplifying * accommodate both training context managers * simplifying * simplifying * nit * reorg * tweak codecov yaml * add gather post hook, simplify, fixes * pytest * pytest fix --- codecov.yml | 2 + src/axolotl/core/trainer_builder.py | 3 - src/axolotl/core/trainers/base.py | 4 +- src/axolotl/core/trainers/mixins/__init__.py | 2 +- .../core/trainers/mixins/sequence_parallel.py | 228 +++++++++- src/axolotl/train.py | 30 +- src/axolotl/utils/collators/batching.py | 77 +--- src/axolotl/utils/config/__init__.py | 3 - src/axolotl/utils/schemas/config.py | 70 ++-- src/axolotl/utils/trainer.py | 4 +- tests/e2e/patched/test_mixtral_samplepack.py | 1 + tests/e2e/patched/test_sp.py | 395 ++++++++++++++---- 12 files changed, 610 insertions(+), 209 deletions(-) diff --git a/codecov.yml b/codecov.yml index b4810bfa44..c85268b4cb 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,5 +1,7 @@ codecov: require_ci_to_pass: yes + notify: + wait_for_ci: true coverage: precision: 2 diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index a51211263f..44f8c5d2b8 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -932,9 +932,6 @@ def build_collator( collator = DataCollatorForSeq2Seq kwargs["return_tensors"] = "pt" - if issubclass(collator, DataCollatorForSeq2Seq): - kwargs["sequence_parallel_degree"] = training_args.sequence_parallel_degree - kwargs["ring_attn_func"] = training_args.ring_attn_func return collator( *collator_args, diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index fd72cd6dbc..3864903a5a 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -371,13 +371,15 @@ def compute_loss( num_items_in_batch=num_items_in_batch, ) - return super().compute_loss( + loss = super().compute_loss( model, inputs, return_outputs=return_outputs, num_items_in_batch=num_items_in_batch, ) + return loss + @staticmethod def orpo_concatenate_inputs(inputs, label_pad_token=-100, pad_token=0, device=None): concatenated_batch = {} diff --git a/src/axolotl/core/trainers/mixins/__init__.py b/src/axolotl/core/trainers/mixins/__init__.py index 44751b4653..6e4b3e4d03 100644 --- a/src/axolotl/core/trainers/mixins/__init__.py +++ b/src/axolotl/core/trainers/mixins/__init__.py @@ -6,4 +6,4 @@ from .optimizer import OptimizerMixin from .rng_state_loader import RngLoaderMixin from .scheduler import SchedulerMixin -from .sequence_parallel import SequenceParallelMixin +from .sequence_parallel import SequenceParallelContextManager, SequenceParallelMixin diff --git a/src/axolotl/core/trainers/mixins/sequence_parallel.py b/src/axolotl/core/trainers/mixins/sequence_parallel.py index 3930c6cb3c..362acb88e0 100644 --- a/src/axolotl/core/trainers/mixins/sequence_parallel.py +++ b/src/axolotl/core/trainers/mixins/sequence_parallel.py @@ -1,16 +1,86 @@ -"""Module for Axolotl trainer sequence parallelism mixin""" +""" +Module for Axolotl trainer sequence parallelism mixin and training context manager +""" +import functools import logging +import torch import torch.distributed as dist from datasets import Dataset +from torch import nn from torch.utils.data import DistributedSampler, Sampler +from torch.utils.hooks import RemovableHandle -from axolotl.monkeypatch.attention.ring_attn import get_ring_attn_group +from axolotl.monkeypatch.attention.ring_attn import ( + RingAttnFunc, + get_ring_attn_group, + update_ring_attn_params, +) LOG = logging.getLogger(__name__) +def apply_sequence_parallelism( + batch: dict[str, torch.Tensor], + local_rank: int, + local_world_size: int, + ring_attn_func: RingAttnFunc, +) -> dict[str, torch.Tensor]: + """ + Apply sequence parallelism slicing to a batch. + + Args: + batch: Batch dictionary (e.g., input_ids, attention_mask, etc.) + local_rank: Local rank in the sequence parallel group + local_world_size: World size of the sequence parallel group + ring_attn_func: The ring attention function to use + + Returns: + Sliced batch dictionary. + """ + # Update ring attention params if needed + if batch.get("position_ids") is not None: + update_ring_attn_params(position_ids=batch["position_ids"]) + + # Slice batch for sequence parallel processing + total_seq_len = batch["input_ids"].size(1) + for key in batch: + if ( + key in batch + and isinstance(batch[key], torch.Tensor) + and batch[key].dim() > 1 + and batch[key].size(1) == total_seq_len + ): + + if ring_attn_func in [ + RingAttnFunc.VARLEN_LLAMA3, + RingAttnFunc.BATCH_RING, + ]: + # Split in sequential fashion and grab this rank's chunk + batch[key] = ( + batch[key].chunk(local_world_size, dim=1)[local_rank].contiguous() + ) + elif ring_attn_func is RingAttnFunc.BATCH_ZIGZAG: + chunks = batch[key].chunk(2 * local_world_size, dim=1) + + # Take rank's chunk and opposing chunk for zigzag pattern + selected_chunks = [ + chunks[local_rank], + chunks[2 * local_world_size - local_rank - 1], + ] + batch[key] = torch.cat(selected_chunks, dim=1).contiguous() + elif ring_attn_func is RingAttnFunc.BATCH_STRIPE: + # Split into striped data and stack + tensor = torch.stack( + batch[key].split(local_world_size, dim=1), + dim=1, + ).transpose(1, 2) + batch[key] = tensor[:, local_rank].contiguous() + + return batch + + class SequenceParallelMixin: """ Mixin class for sequence parallelism support in trainers. @@ -87,3 +157,157 @@ def _sp_get_eval_sampler(self, eval_dataset) -> Sampler | None: return self._create_sequence_parallel_sampler( eval_dataset, shuffle=False, is_eval=True ) + + +class SequenceParallelContextManager: + """ + Context manager for sequence parallelism operations. + + This class provides a context that will automatically apply sequence parallelism + during model forward passes using a pre-forward hook, and gather outputs from + across the sequence parallelism group using a post-forward hook. + """ + + def __init__( + self, + model: nn.Module, + sequence_parallel_degree: int, + ring_attn_func: RingAttnFunc, + ): + self.model = model + self.sequence_parallel_degree = sequence_parallel_degree + self.ring_attn_func = ring_attn_func + self.process_group = get_ring_attn_group() + + # Initialize sequence parallel group details + self.local_rank = dist.get_rank(self.process_group) + self.local_world_size = dist.get_world_size(self.process_group) + + # Will store hook handles for removal + self.hook_handles: list[RemovableHandle] = [] + + # Create a partially applied version of the apply_sequence_parallelism function + # with pre-configured params + self.apply_sequence_parallelism = functools.partial( + apply_sequence_parallelism, + local_rank=self.local_rank, + local_world_size=self.local_world_size, + ring_attn_func=self.ring_attn_func, + ) + + def __enter__(self): + # Forward pre-hook to apply sequence parallelism + def sequence_parallel_pre_hook(_, args, kwargs): + # Apply sequence parallelism to kwargs + kwargs = self.apply_sequence_parallelism(batch=kwargs) + return args, kwargs + + # Forward post-hook to gather outputs + def sequence_parallel_post_hook(_, __, output): + # Gather the sharded outputs + return self.gather_outputs(output) + + # Register both hooks + self.hook_handles.append( + self.model.register_forward_pre_hook( + sequence_parallel_pre_hook, with_kwargs=True + ) + ) + self.hook_handles.append( + self.model.register_forward_hook(sequence_parallel_post_hook) + ) + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # Remove all hooks + for handle in self.hook_handles: + handle.remove() + self.hook_handles = [] + + def gather_outputs(self, output): + """Gather sharded outputs from all ranks and reconstruct the full tensor.""" + # Handle different output formats (dict, tensor, etc.) + if isinstance(output, dict): + gathered_output = {} + for key, value in output.items(): + if isinstance(value, torch.Tensor) and value.dim() > 1: + # Gather logits or other sequence-sharded tensors + gathered_value = self.gather_tensor(value) + gathered_output[key] = gathered_value + else: + gathered_value = value.clone() + dist.all_reduce( + gathered_value, op=dist.ReduceOp.SUM, group=self.process_group + ) + gathered_output[key] = gathered_value + return gathered_output + if isinstance(output, torch.Tensor): + return self.gather_tensor(output) + + return output + + def gather_tensor(self, tensor): + """Gather a sharded tensor from all ranks.""" + # Prepare tensors for all_gather + world_size = self.local_world_size + + # Create list to store tensors from all ranks + gathered_tensors = [torch.zeros_like(tensor) for _ in range(world_size)] + + # All-gather operation + dist.all_gather(gathered_tensors, tensor, group=self.process_group) + + # Concatenate along sequence dimension (typically dim=1) + if self.ring_attn_func in [RingAttnFunc.VARLEN_LLAMA3, RingAttnFunc.BATCH_RING]: + # Simple concatenation for standard sharding + return torch.cat(gathered_tensors, dim=1) + + if self.ring_attn_func is RingAttnFunc.BATCH_ZIGZAG: + # Each rank has a pattern of (rank, world_size*2-rank-1) + reconstituted_tensors = [None] * (world_size * 2) + + # First, split each gathered tensor into its two chunks + for rank, gathered_tensor in enumerate(gathered_tensors): + # Each tensor contains two chunks in the sequence dimension + chunk_size = gathered_tensor.size(1) // 2 + chunk1, chunk2 = gathered_tensor.split(chunk_size, dim=1) + + # Place chunks in their original positions + reconstituted_tensors[rank] = chunk1 + reconstituted_tensors[world_size * 2 - rank - 1] = chunk2 + + # Concatenate the reconstituted tensors in the correct order + return torch.cat(reconstituted_tensors, dim=1) + + # Otherwise, RingAttnFunc.BATCH_STRIPE + # In striping, each rank has every world_size-th slice + batch_size = tensor.size(0) + hidden_dim = tensor.size(-1) + + # First, determine the full sequence length + total_seq_len = 0 + for t in gathered_tensors: + total_seq_len += t.size(1) + + # Create a tensor to hold the unstriped result + result = torch.zeros( + batch_size, + total_seq_len, + hidden_dim, + dtype=tensor.dtype, + device=tensor.device, + ) + + # For each rank's tensor, distribute its slices to the correct positions + for rank, gathered_tensor in enumerate(gathered_tensors): + # The rank's tensor contains every world_size-th slice + # starting from its rank position + seq_len = gathered_tensor.size(1) + for i in range(seq_len): + # Calculate the position in the full tensor + pos = i * world_size + rank + if pos < total_seq_len: + result[:, pos] = gathered_tensor[:, i] + + return result diff --git a/src/axolotl/train.py b/src/axolotl/train.py index e003c8b674..d116ea4fd9 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -6,6 +6,7 @@ import signal import sys import weakref +from contextlib import nullcontext from pathlib import Path from typing import Any, Dict @@ -25,6 +26,9 @@ fix_untrained_tokens, ) from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder +from axolotl.core.trainers.mixins.sequence_parallel import ( + SequenceParallelContextManager, +) from axolotl.logging_config import configure_logging from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import cleanup_distributed @@ -185,16 +189,28 @@ def execute_training( trainer: The configured trainer object. resume_from_checkpoint: Path to checkpoint to resume from, if applicable. """ - LOG.info("Starting trainer...") - if cfg.flash_optimum: - with torch.backends.cuda.sdp_kernel( - # TODO configure these from the YAML w/ sdp_kernel_kwargs: ... + # Define the context managers to use + flash_context = ( + torch.backends.cuda.sdp_kernel( enable_flash=True, enable_math=True, enable_mem_efficient=True, - ): - trainer.train(resume_from_checkpoint=resume_from_checkpoint) - else: + ) + if cfg.flash_optimum + else nullcontext() + ) + sequence_parallel_context = ( + SequenceParallelContextManager( + model=trainer.model, + sequence_parallel_degree=cfg.sequence_parallel_degree, + ring_attn_func=cfg.ring_attn_func, + ) + if cfg.sequence_parallel_degree > 1 + else nullcontext() + ) + + LOG.info("Starting trainer...") + with flash_context, sequence_parallel_context: trainer.train(resume_from_checkpoint=resume_from_checkpoint) diff --git a/src/axolotl/utils/collators/batching.py b/src/axolotl/utils/collators/batching.py index 738ef0dc52..45facf832a 100644 --- a/src/axolotl/utils/collators/batching.py +++ b/src/axolotl/utils/collators/batching.py @@ -1,20 +1,12 @@ -""" -Data collators for axolotl to pad labels and position_ids for packed sequences. Also -includes logic for handling sequence parallelism collation. -""" +"""Data collators for axolotl to pad labels and position_ids for packed sequences""" from dataclasses import dataclass from typing import Any import numpy as np -import torch -import torch.distributed as dist from transformers import PreTrainedTokenizerBase from transformers.utils import PaddingStrategy -from axolotl.monkeypatch.attention.ring_attn import update_ring_attn_params -from axolotl.monkeypatch.attention.ring_attn.patch import RingAttnFunc - @dataclass class DataCollatorForSeq2Seq: @@ -49,8 +41,6 @@ class DataCollatorForSeq2Seq: The id to use when padding the labels (-100 will be automatically ignored by PyTorch loss functions). return_tensors (`str`): The type of Tensor to return. Allowable values are "np", "pt" and "tf". - sequence_parallel_degree (`int`): - The degree of sequence parallelism. Default to 1 for no sequence parallelism. """ tokenizer: PreTrainedTokenizerBase @@ -61,17 +51,6 @@ class DataCollatorForSeq2Seq: label_pad_token_id: int = -100 position_pad_token_id: int = 0 return_tensors: str = "pt" - sequence_parallel_degree: int = 1 - ring_attn_func: RingAttnFunc | None = None - - def __post_init__(self): - if self.sequence_parallel_degree > 1: - from axolotl.monkeypatch.attention.ring_attn import get_ring_attn_group - - # Get information about our position in the SP group - sp_group = get_ring_attn_group() - self.local_rank = dist.get_rank(group=sp_group) - self.local_world_size = dist.get_world_size(group=sp_group) def __call__(self, features, return_tensors=None): has_attn_mask = "attention_mask" in features[0].keys() @@ -141,62 +120,8 @@ def __call__(self, features, return_tensors=None): ) features["decoder_input_ids"] = decoder_input_ids - if self.sequence_parallel_degree > 1: - features = self.apply_sequence_parallelism(features) - return features - def apply_sequence_parallelism( - self, batch: dict[str, torch.Tensor] - ) -> torch.Tensor: - """ - Apply sequence parallelism slicing to a batch. - - Args: - batch: Batch dictionary from parent collator. - - Returns: - Sliced batch dictionary. - """ - # Get local (start, end) for sequence parallelism slicing - total_seq_len = batch["input_ids"].size(1) - - # Update params for varlen ring attention calculation - if batch.get("position_ids") is not None: - update_ring_attn_params(position_ids=batch["position_ids"]) - - # Slice batch for sequence parallel processing - for key in batch: - if batch[key].size(1) == total_seq_len: - if self.ring_attn_func in [ - RingAttnFunc.VARLEN_LLAMA3, - RingAttnFunc.BATCH_RING, - ]: - batch[key] = ( - batch[key] - .chunk(self.local_world_size, dim=1)[self.local_rank] - .contiguous() - ) - elif self.ring_attn_func is RingAttnFunc.BATCH_ZIGZAG: - chunks = batch[key].chunk(2 * self.local_world_size, dim=1) - - # Take rank's chunk and opposing chunk for zigzag pattern - selected_chunks = [ - chunks[self.local_rank], - chunks[2 * self.local_world_size - self.local_rank - 1], - ] - batch[key] = torch.cat(selected_chunks, dim=1).contiguous() - elif self.ring_attn_func is RingAttnFunc.BATCH_STRIPE: - # TODO(djsaunde): This doesn't seem to work as expected - # Split into striped data and stack - tensor = torch.stack( - batch[key].split(self.local_world_size, dim=1), - dim=1, - ).transpose(1, 2) - batch[key] = tensor[:, self.local_rank].contiguous() - - return batch - @dataclass class BatchSamplerDataCollatorForSeq2Seq(DataCollatorForSeq2Seq): diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index b527dce08b..e5ea44aa08 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -126,9 +126,6 @@ def normalize_config(cfg): with open(ds_config_path, encoding="utf-8") as f: cfg.deepspeed = json.load(f) - if cfg.sequence_parallel_degree is None: - cfg.sequence_parallel_degree = 1 - if cfg.saves_per_epoch: save_steps = 1.0 / (cfg.saves_per_epoch * cfg.num_epochs) if save_steps < 1.0: # prevent saves on every step diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 732ae60cf1..f68d160dfc 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -18,6 +18,7 @@ ) from transformers.utils.import_utils import is_torch_npu_available +from axolotl.utils.distributed import is_main_process from axolotl.utils.schemas.datasets import ( DatasetConfig, DPODataset, @@ -718,9 +719,10 @@ def check_eval_packing(cls, data): and data.get("eval_sample_packing") is None and not data.get("eval_table_size") ): - LOG.info( - "explicitly setting `eval_sample_packing` to match `sample_packing`" - ) + if is_main_process(): + LOG.info( + "explicitly setting `eval_sample_packing` to match `sample_packing`" + ) data["eval_sample_packing"] = True if ( @@ -1149,22 +1151,17 @@ def check_kto_config(cls, data): return data - @field_validator("sequence_parallel_degree", mode="after") - @classmethod - def check_sequence_parallel_degree(cls, value, info): - if not value: - value = 1 - - if value > 1: - if not info.data.get("flash_attention"): + @model_validator(mode="after") + def check_sequence_parallel_degree(self): + if not self.sequence_parallel_degree: + self.sequence_parallel_degree = 1 + elif self.sequence_parallel_degree > 1: + if not self.flash_attention: raise ValueError( "flash_attention: true must be set with sequence_parallel_degree > 1" ) - if ( - info.data.get("sample_packing") - and not info.data["micro_batch_size"] == 1 - ): + if self.sample_packing and self.micro_batch_size > 1: raise ValueError( "micro_batch_size must be set to 1 when sample_packing is enabled" "due to a `ring-flash-attn` requirement" @@ -1182,44 +1179,43 @@ def check_sequence_parallel_degree(cls, value, info): # TODO: monkeypatch / callback to average losses correctly across SP ranks # / fix gradient scaling across SP ranks. Losses, grads should be scaled # according to the proportion of non-padding tokens per rank. - LOG.warning( - "Sequence parallelism (SP) is enabled with " - f"sequence_parallel_degree={value}. Please note that logged losses may " - "differ slightly to the non-SP losses due to transformers Trainer " - "implementation details. Please see " - "https://github.com/axolotl-ai-cloud/axolotl/pull/2495#issuecomment-2784022042 " - "for more details." - ) + if is_main_process(): + LOG.warning( + "Sequence parallelism (SP) is enabled with " + f"sequence_parallel_degree={self.sequence_parallel_degree}. " + "Please note that logged losses may differ slightly to the non-SP " + "losses due to transformers Trainer implementation details. " + "Please see https://github.com/axolotl-ai-cloud/axolotl/pull/2495#issuecomment-2784022042 " + "for more details." + ) - return value + return self - @field_validator("ring_attn_func", mode="after") - @classmethod - def check_ring_attn_func(cls, value, info): - if not info.data.get("sequence_parallel_degree", 1) > 1: - return value + @model_validator(mode="after") + def validate_ring_attn_func(self): + if getattr(self, "sequence_parallel_degree", 1) == 1: + return self from axolotl.monkeypatch.attention.ring_attn.patch import RingAttnFunc - if value is not None: - # Set the ring attention function if passed in config + if self.ring_attn_func is not None: valid_funcs = list(RingAttnFunc) - if value in valid_funcs: - value = RingAttnFunc(value) + if self.ring_attn_func in valid_funcs: + self.ring_attn_func = RingAttnFunc(self.ring_attn_func) else: raise ValueError( - f"ring_attn_func: {value} must be one of {valid_funcs}" + f"ring_attn_func: {self.ring_attn_func} must be in {valid_funcs}" ) else: # Default ring attention function selection - sample_packing = info.data.get("sample_packing") - value = ( + sample_packing = getattr(self, "sample_packing", False) + self.ring_attn_func = ( RingAttnFunc.VARLEN_LLAMA3 if sample_packing else RingAttnFunc.BATCH_RING ) - return value + return self @model_validator(mode="before") @classmethod diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index c1154be68d..3dc9ae3f66 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -348,7 +348,7 @@ def drop_no_trainable_tokens(sample): load_from_cache_file=not cfg.is_preprocess, desc="Add position_id column (PoSE)", ) - elif cfg.sample_packing or cfg.sequence_parallel_degree > 1: + elif cfg.sample_packing: drop_long_kwargs = {} if filter_map_kwargs: drop_long_kwargs["desc"] = "Add position_id column (Sample Packing)" @@ -358,7 +358,7 @@ def drop_no_trainable_tokens(sample): **filter_map_kwargs, **drop_long_kwargs, ) - if cfg.eval_sample_packing or cfg.sequence_parallel_degree > 1: + if cfg.eval_sample_packing: if eval_dataset: eval_dataset = eval_dataset.map( add_position_ids, diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index 2d4f97084f..f035b1f288 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -99,6 +99,7 @@ def test_ft(self, temp_dir): "bf16": "auto", } ) + cfg = validate_config(cfg) normalize_config(cfg) cli_args = TrainerCliArgs() dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/tests/e2e/patched/test_sp.py b/tests/e2e/patched/test_sp.py index 70a601f639..6e1e2f2cba 100644 --- a/tests/e2e/patched/test_sp.py +++ b/tests/e2e/patched/test_sp.py @@ -2,14 +2,19 @@ # pylint: disable=redefined-outer-name,unused-argument +import functools +import sys from unittest.mock import MagicMock, patch import pytest import torch from accelerate.state import PartialState +from axolotl.core.trainers.mixins.sequence_parallel import apply_sequence_parallelism from axolotl.monkeypatch.attention.ring_attn import ( + RingAttnFunc, get_ring_attn_group, + register_ring_attn, set_ring_attn_group, ) from axolotl.utils.dict import DictDefault @@ -47,6 +52,27 @@ def fixture_cfg(): return cfg +@pytest.fixture +def sequence_parallel_batch(): + """Create a test batch for sequence parallelism tests.""" + batch_size = 1 + seq_len = 8 + + # Create test tensors + input_ids = torch.arange(batch_size * seq_len).reshape(batch_size, seq_len) + attention_mask = torch.ones(batch_size, seq_len) + position_ids = torch.arange(seq_len).expand(batch_size, seq_len) + + # Create test batch + batch = { + "input_ids": input_ids, + "attention_mask": attention_mask, + "position_ids": position_ids, + } + + return batch + + class TestRingAttention: """Tests for the ring attention functionality.""" @@ -73,11 +99,6 @@ def test_register_ring_attn( self, mock_world_size, mock_rank, mock_new_group, partial_state ): """Test that ring attention groups are created correctly.""" - from axolotl.monkeypatch.attention.ring_attn import ( - RingAttnFunc, - register_ring_attn, - ) - # Setup mocks mock_world_size.return_value = 8 # 8 GPUs total mock_rank.return_value = 3 # GPU #3 @@ -101,88 +122,308 @@ def test_register_ring_attn( set_ring_attn_group(None) -# Mock a simplified DataCollator test -@patch("axolotl.monkeypatch.attention.ring_attn.get_ring_attn_group") -@patch("torch.distributed.get_rank") -@patch("torch.distributed.get_world_size") -def test_sequence_parallel_slicing( - mock_world_size, mock_rank, mock_get_group, partial_state -): - """Test the basic sequence slicing logic without full collator instantiation.""" - # Setup mocks - mock_get_group.return_value = MagicMock() - mock_rank.return_value = 1 # Second GPU - mock_world_size.return_value = 4 # 4 GPUs total - - # Create a sample batch - batch = { - "input_ids": torch.tensor( - [ - [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112], - [201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212], - ] - ), - "attention_mask": torch.ones(2, 12), - } +class TestConfigValidation: + """Tests for validating sequence parallelism configurations.""" - # Simplified slicing logic from SequenceParallelDataCollator - def slice_batch(batch, rank, world_size): - result = {} - for key in batch: - seq_len = batch[key].shape[1] - slice_size = seq_len // world_size - start_idx = rank * slice_size - end_idx = start_idx + slice_size if rank < world_size - 1 else seq_len - result[key] = batch[key][:, start_idx:end_idx] - return result - - # Slice the batch - result = slice_batch( - batch, rank=mock_rank.return_value, world_size=mock_world_size.return_value - ) + @pytest.fixture(autouse=True) + def setup_mocks(self, monkeypatch): + """Set up mocks for all tests in this class.""" + # Mock the ring_flash_attn module + monkeypatch.setitem(sys.modules, "ring_flash_attn", MagicMock()) + + # Mock the is_main_process function to return True + monkeypatch.setattr( + "axolotl.utils.schemas.config.is_main_process", lambda: True + ) - # Check slicing - assert result["input_ids"].shape == (2, 3) # 12 tokens / 4 GPUs = 3 tokens per GPU - expected_input_ids = torch.tensor( + @pytest.fixture + def base_cfg(self): + """Create a base configuration for testing.""" + return DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "datasets": [{"path": "mhenrichsen/alpaca_2k_test", "type": "alpaca"}], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 1e-3, + "output_dir": "./model-out", + "sequence_len": 512, + "special_tokens": {"pad_token": "<|endoftext|>"}, + } + ) + + @pytest.mark.parametrize( + "config_updates, expected_values, should_pass, error_msg", + [ + # Valid configuration + ( + {"sequence_parallel_degree": 2, "flash_attention": True}, + {"sequence_parallel_degree": 2, "flash_attention": True}, + True, + None, + ), + # Default sequence_parallel_degree + ({}, {"sequence_parallel_degree": 1}, True, None), + # Invalid: sequence_parallel_degree > 1 without flash_attention + ( + {"sequence_parallel_degree": 2, "flash_attention": False}, + None, + False, + "flash_attention: true must be set", + ), + # Invalid: sequence_parallel_degree > 1 with sample_packing and micro_batch_size > 1 + ( + { + "sequence_parallel_degree": 2, + "flash_attention": True, + "sample_packing": True, + "micro_batch_size": 2, + "pad_to_sequence_len": True, + }, + None, + False, + "micro_batch_size must be set to 1", + ), + ], + ids=[ + "valid_config", + "default_sp_degree", + "without_flash_attention", + "sample_packing_with_large_batch", + ], + ) + def test_sequence_parallel_config_validation( + self, base_cfg, config_updates, expected_values, should_pass, error_msg + ): + """Test various sequence parallelism configuration scenarios.""" + from axolotl.utils.schemas.config import AxolotlInputConfig + + # Apply updates to base config + cfg = base_cfg + cfg.update(config_updates) + + if should_pass: + # Should validate without errors + config = AxolotlInputConfig(**cfg) + + # Check expected values + for key, value in expected_values.items(): + assert getattr(config, key) == value + else: + # Should raise exception + with pytest.raises(ValueError) as excinfo: + AxolotlInputConfig(**cfg) + assert error_msg in str(excinfo.value) + + @pytest.mark.parametrize( + "ring_attn_func, sample_packing, expected_func", [ - [104, 105, 106], # Second slice of first sequence - [204, 205, 206], # Second slice of second sequence - ] + (None, True, RingAttnFunc.VARLEN_LLAMA3), + (None, False, RingAttnFunc.BATCH_RING), + ], + ids=["default_with_sample_packing", "default_without_sample_packing"], ) - assert torch.all(result["input_ids"] == expected_input_ids) + def test_ring_attn_func_validation( + self, base_cfg, ring_attn_func, sample_packing, expected_func + ): + """Test ring_attn_func validation and defaults.""" + from axolotl.utils.schemas.config import AxolotlInputConfig + + # Apply updates to base config + cfg = base_cfg | { + "sequence_parallel_degree": 2, + "flash_attention": True, + "sample_packing": sample_packing, + } + if ring_attn_func is not None: + cfg["ring_attn_func"] = ring_attn_func -@patch.dict("sys.modules", {"ring_flash_attn": MagicMock()}) -def test_config_validation_with_valid_inputs(cfg): - """Test that valid sequence parallelism configurations pass validation.""" - # Import the actual model class with appropriate mocks - from axolotl.utils.schemas.config import AxolotlInputConfig + # Should validate without errors + config = AxolotlInputConfig(**cfg) - # Valid configuration: sequence_parallel_degree > 1 and flash_attention is True - cfg = cfg | { - "sequence_parallel_degree": 2, - "flash_attention": True, - } + # Check ring_attn_func value + assert config.ring_attn_func.value == expected_func - # Should validate without errors - config = AxolotlInputConfig(**cfg) - assert config.sequence_parallel_degree == 2 - assert config.flash_attention is True + def test_invalid_ring_attn_func(self, base_cfg): + """Test that an invalid ring_attn_func is rejected.""" + from axolotl.utils.schemas.config import AxolotlInputConfig + # Invalid configuration with invalid ring_attn_func + cfg = base_cfg | { + "sequence_parallel_degree": 2, + "flash_attention": True, + "ring_attn_func": "INVALID_FUNC", + } -def test_config_validation_with_invalid_inputs(cfg): - """Test that invalid sequence parallelism configurations fail validation.""" - from axolotl.utils.schemas.config import AxolotlInputConfig + # Should raise ValidationError + with pytest.raises(ValueError) as excinfo: + AxolotlInputConfig(**cfg) - # Invalid configuration: sequence_parallel_degree > 1 but flash_attention is False - cfg = cfg | { - "sequence_parallel_degree": 2, - "flash_attention": False, - } + # Verify error message + assert "ring_attn_func: INVALID_FUNC must be in" in str(excinfo.value) + + +class TestApplySequenceParallelism: + """Tests for the apply_sequence_parallelism function.""" + + @pytest.fixture(autouse=True) + def mock_distributed(self, monkeypatch): + """Mock torch.distributed functions for testing.""" + # Mock is_initialized to return True + monkeypatch.setattr(torch.distributed, "is_initialized", lambda: True) - # Should raise ValidationError - with pytest.raises(ValueError) as excinfo: - AxolotlInputConfig(**cfg) + # Mock get_rank to return 0 by default + monkeypatch.setattr(torch.distributed, "get_rank", lambda *args, **kwargs: 0) + + # Mock get_world_size to return 2 by default + monkeypatch.setattr( + torch.distributed, "get_world_size", lambda *args, **kwargs: 2 + ) + + # Mock the process group + monkeypatch.setattr( + "axolotl.monkeypatch.attention.ring_attn.get_ring_attn_group", + MagicMock, + ) + + # Mock update_ring_attn_params + monkeypatch.setattr( + "axolotl.monkeypatch.attention.ring_attn.update_ring_attn_params", + lambda **kwargs: None, + ) + + def test_world_size_one(self, sequence_parallel_batch): + """Test that function returns original batch when world size is 1.""" + result = apply_sequence_parallelism( + batch=sequence_parallel_batch, + local_rank=0, + local_world_size=1, + ring_attn_func=RingAttnFunc.BATCH_RING, + ) + + # Should return the original batch unchanged + assert result == sequence_parallel_batch + + def test_batch_ring_rank0(self, sequence_parallel_batch): + """Test BATCH_RING sharding for rank 0 in a 2-process group.""" + batch = sequence_parallel_batch + seq_len = batch["input_ids"].size(1) + + result = apply_sequence_parallelism( + batch=batch, + local_rank=0, + local_world_size=2, + ring_attn_func=RingAttnFunc.BATCH_RING, + ) + + # Check that sequence dimension was sharded correctly + assert result["input_ids"].shape[1] == seq_len // 2 + assert result["attention_mask"].shape[1] == seq_len // 2 + + # Verify content: rank 0 should get the first half of the sequence + assert torch.equal(result["input_ids"], batch["input_ids"][:, : seq_len // 2]) + assert torch.equal( + result["position_ids"], batch["position_ids"][:, : seq_len // 2] + ) + + def test_batch_ring_rank1(self, sequence_parallel_batch): + """Test BATCH_RING sharding for rank 1 in a 2-process group.""" + batch = sequence_parallel_batch + seq_len = batch["input_ids"].size(1) + original_input_ids = batch["input_ids"].clone() + + result = apply_sequence_parallelism( + batch=batch, + local_rank=1, + local_world_size=2, + ring_attn_func=RingAttnFunc.BATCH_RING, + ) + + # Verify content: rank 1 should get the second half of the sequence + assert torch.equal(result["input_ids"], original_input_ids[:, seq_len // 2 :]) + + def test_batch_zigzag(self, sequence_parallel_batch): + """Test BATCH_ZIGZAG sharding pattern.""" + batch = sequence_parallel_batch + original_input_ids = batch["input_ids"].clone() + seq_len = batch["input_ids"].size(1) + + # Test rank 0 + result_rank0 = apply_sequence_parallelism( + batch={k: v.clone() for k, v in batch.items()}, + local_rank=0, + local_world_size=2, + ring_attn_func=RingAttnFunc.BATCH_ZIGZAG, + ) + + # Test rank 1 + result_rank1 = apply_sequence_parallelism( + batch={k: v.clone() for k, v in batch.items()}, + local_rank=1, + local_world_size=2, + ring_attn_func=RingAttnFunc.BATCH_ZIGZAG, + ) + + # Checks for both ranks + assert result_rank0["input_ids"].shape[1] == seq_len // 2 + assert result_rank1["input_ids"].shape[1] == seq_len // 2 + + # For a 2-rank system with 8 tokens, check specific zigzag pattern + # Rank 0 should get chunks [0, 1] and [6, 7] + # Rank 1 should get chunks [2, 3] and [4, 5] + if seq_len == 8: + # Create expected tensors for comparison + rank0_expected = torch.cat( + [original_input_ids[:, :2], original_input_ids[:, 6:8]], dim=1 + ) + + rank1_expected = torch.cat( + [original_input_ids[:, 2:4], original_input_ids[:, 4:6]], dim=1 + ) + + assert torch.equal(result_rank0["input_ids"], rank0_expected) + assert torch.equal(result_rank1["input_ids"], rank1_expected) + + def test_partial_application(self, sequence_parallel_batch): + """Test that we can create a partially applied version of the function.""" + batch = sequence_parallel_batch + original_input_ids = batch["input_ids"].clone() + + # Create a partially applied function + rank0_ring_parallel = functools.partial( + apply_sequence_parallelism, + local_rank=0, + local_world_size=2, + ring_attn_func=RingAttnFunc.BATCH_RING, + ) + + # Use the partially applied function + result = rank0_ring_parallel(batch=batch) + + # Verify it works as expected + assert result["input_ids"].shape[1] == original_input_ids.shape[1] // 2 + assert torch.equal( + result["input_ids"], + original_input_ids[:, : original_input_ids.shape[1] // 2], + ) + + def test_missing_position_ids(self, sequence_parallel_batch): + """Test handling of batch without position_ids.""" + # Create a batch without position_ids + batch = { + k: v for k, v in sequence_parallel_batch.items() if k != "position_ids" + } + original_input_ids = batch["input_ids"].clone() + + # This should run without error even though position_ids is missing + result = apply_sequence_parallelism( + batch=batch, + local_rank=0, + local_world_size=2, + ring_attn_func=RingAttnFunc.BATCH_RING, + ) - # Verify error message - assert "flash_attention: true must be set" in str(excinfo.value) + # Verification should pass + assert "position_ids" not in result + assert result["input_ids"].shape[1] == original_input_ids.shape[1] // 2 From 5cb33984609b363b4c6c725760007d0f2ba80eda Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 25 Apr 2025 15:10:55 -0400 Subject: [PATCH 0574/1405] don't fail on codecov upload for external contributor PRs (#2564) [skip ci] --- cicd/cicd.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 89d746e5b6..86cc4fa96f 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -52,4 +52,4 @@ pytest -v --durations=10 \ --cov-append \ --cov-report=xml:e2e-coverage.xml -codecov upload-process -t $CODECOV_TOKEN -f e2e-coverage.xml -F e2e,pytorch-${PYTORCH_VERSION} +codecov upload-process -t $CODECOV_TOKEN -f e2e-coverage.xml -F e2e,pytorch-${PYTORCH_VERSION} || true From 2c2563bc341ab8896322011d04a32b81ec1daa41 Mon Sep 17 00:00:00 2001 From: Eko Julianto Salim Date: Sat, 26 Apr 2025 04:02:37 +0700 Subject: [PATCH 0575/1405] fix: gradient checkpointing functools.partial object has no attribute __self__ (#2563) [skip ci] * fix: gradient checkpointing causing functools.partial error * lint * chore: lint --------- Co-authored-by: Wing Lian --- src/axolotl/utils/gradient_checkpointing/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/gradient_checkpointing/__init__.py b/src/axolotl/utils/gradient_checkpointing/__init__.py index 62fd34b59c..0da5c83a22 100644 --- a/src/axolotl/utils/gradient_checkpointing/__init__.py +++ b/src/axolotl/utils/gradient_checkpointing/__init__.py @@ -1,5 +1,7 @@ """custom checkpointing utils""" +from functools import partial + from axolotl.utils.gradient_checkpointing.unsloth import ( Unsloth_Offloaded_Gradient_Checkpointer, ) @@ -9,6 +11,10 @@ def hf_grad_checkpoint_offload_wrapper( decoder_layer, *args, use_reentrant=None ): # pylint: disable=unused-argument return Unsloth_Offloaded_Gradient_Checkpointer.apply( - decoder_layer.__self__, + ( + decoder_layer.func.__self__ + if isinstance(decoder_layer, partial) + else decoder_layer.__self__ + ), *args, ) From 53dbf97d85287c9763b3baf3ae26dbc9ca96a399 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 25 Apr 2025 17:14:26 -0400 Subject: [PATCH 0576/1405] make cce default to true when using the plugin (#2562) [skip ci] --- examples/llama-4/scout-qlora-single-h100-flex.yaml | 1 - src/axolotl/integrations/cut_cross_entropy/README.md | 2 -- src/axolotl/integrations/cut_cross_entropy/args.py | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/llama-4/scout-qlora-single-h100-flex.yaml b/examples/llama-4/scout-qlora-single-h100-flex.yaml index c7a3b28d08..20352f81eb 100644 --- a/examples/llama-4/scout-qlora-single-h100-flex.yaml +++ b/examples/llama-4/scout-qlora-single-h100-flex.yaml @@ -10,7 +10,6 @@ plugins: liger_glu_activation: true liger_rms_norm: true liger_layer_norm: true -cut_cross_entropy: true llama4_linearized_experts: true # needed with custom linearized experts model load_in_4bit: true diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 724e0688db..462bcbedc8 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -27,8 +27,6 @@ pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transform ```yaml plugins: - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin - -cut_cross_entropy: true ``` ## Supported Models diff --git a/src/axolotl/integrations/cut_cross_entropy/args.py b/src/axolotl/integrations/cut_cross_entropy/args.py index c16d91ede2..da1db73976 100644 --- a/src/axolotl/integrations/cut_cross_entropy/args.py +++ b/src/axolotl/integrations/cut_cross_entropy/args.py @@ -28,7 +28,7 @@ class CutCrossEntropyArgs(BaseModel): Input args for Cut Cross Entropy. """ - cut_cross_entropy: Optional[bool] = None + cut_cross_entropy: Optional[bool] = True @model_validator(mode="before") @classmethod From 9eba0ad1180a599db23aeccd166b0e2be5b0d726 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 26 Apr 2025 04:14:48 +0700 Subject: [PATCH 0577/1405] chore(doc): update docker tags on doc (#2559) [skip ci] --- docs/docker.qmd | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/docker.qmd b/docs/docker.qmd index 6bdd77a543..e208d3222d 100644 --- a/docs/docker.qmd +++ b/docs/docker.qmd @@ -28,6 +28,8 @@ main-base-py{python_version}-cu{cuda_version}-{pytorch_version} Tags examples: +- `main-base-py3.11-cu128-2.7.0` +- `main-base-py3.11-cu126-2.7.0` - `main-base-py3.11-cu124-2.6.0` - `main-base-py3.11-cu124-2.5.1` - `main-base-py3.11-cu124-2.4.1` @@ -50,7 +52,7 @@ Link: [Docker Hub](https://hub.docker.com/r/axolotlai/axolotl) # on push to main main-py{python_version}-cu{cuda_version}-{pytorch_version} -# latest main (currently torch 2.5.1, python 3.11, cuda 12.4) +# latest main (currently torch 2.6.0, python 3.11, cuda 12.4) main-latest # nightly build @@ -68,6 +70,7 @@ There may be some extra tags appended to the image, like `-vllm` which installs Tags examples: +- `main-py3.11-cu126-2.7.0` - `main-py3.11-cu124-2.6.0` - `main-py3.11-cu124-2.5.1` - `main-py3.11-cu124-2.4.1` From e3c9d541a7b7816abcaa24a12616daa074f3c1dc Mon Sep 17 00:00:00 2001 From: Chiwan Park Date: Sat, 26 Apr 2025 06:15:03 +0900 Subject: [PATCH 0578/1405] fix: crash when pretraining_dataset with dispatch_batches is false (#2558) --- src/axolotl/utils/data/sft.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 413f6d144f..12f0701f06 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -134,10 +134,9 @@ def prepare_dataset(cfg, tokenizer, processor=None, preprocess_iterable=None): "csv", data_files=f.name, split="train", streaming=True ) else: - if is_local_main_process(): - iter_ds = load_dataset( - path, streaming=True, split=split, name=name, data_files=data_files - ) + iter_ds = load_dataset( + path, streaming=True, split=split, name=name, data_files=data_files + ) if skip: LOG.info(f"Skipping {skip} samples from the dataset") From 5dba5c82a80279331b96b65b15e8707234bbcd98 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 25 Apr 2025 21:10:54 -0400 Subject: [PATCH 0579/1405] fix support for wandb run_name for rl trainers (#2566) [skip ci] * fix support for wandb run_name for rl trainers * prefer to use wandb random names for run_name --- src/axolotl/core/trainer_builder.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 44f8c5d2b8..970b020755 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1048,6 +1048,9 @@ def build_training_arguments(self, total_num_steps): if self.cfg.rpo_alpha is not None: training_args_kwargs["rpo_alpha"] = self.cfg.rpo_alpha + if self.cfg.use_wandb: + training_args_kwargs["run_name"] = self.cfg.wandb_name + training_args_cls = None blocklist_args_kwargs = [] if self.cfg.rl == "simpo": @@ -1118,6 +1121,12 @@ def build_training_arguments(self, total_num_steps): **training_args_kwargs, ) + # unset run_name so wandb sets up experiment names + if self.cfg.use_wandb and training_args.run_name == training_args.output_dir: + training_args.run_name = ( # pylint: disable=attribute-defined-outside-init + None + ) + return training_args def build(self, total_num_steps): From caf5cb63ea3591126f2dda4346f63ac5fdea74ce Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 25 Apr 2025 21:11:17 -0400 Subject: [PATCH 0580/1405] add e2e smoke test for using activation/gradient checkpointing with offload (#2565) * add e2e smoke test for using activation/gradient checkpointing with offload * disable duplicate code check for the test * fix relative import * seq len too small to test this dataset with packing * Fix checkpoint ptaching for tests --- .../patched/test_activation_checkpointing.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/e2e/patched/test_activation_checkpointing.py diff --git a/tests/e2e/patched/test_activation_checkpointing.py b/tests/e2e/patched/test_activation_checkpointing.py new file mode 100644 index 0000000000..cbabab6fd0 --- /dev/null +++ b/tests/e2e/patched/test_activation_checkpointing.py @@ -0,0 +1,77 @@ +""" +E2E tests for activation checkpointing +""" + +import pytest +import transformers +from torch.utils.checkpoint import checkpoint + +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +from ..utils import check_model_output_exists + + +@pytest.fixture() +def fix_checkpoint_after_test(): + yield + transformers.modeling_utils.checkpoint = checkpoint + + +class TestActivationCheckpointing: + """ + E2E tests for activation checkpointing + """ + + def test_activation_checkpointing_offload( + self, + temp_dir, + fix_checkpoint_after_test, # pylint: disable=unused-argument,redefined-outer-name + ): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 1024, + "val_set_size": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>", + "eos_token": "<|im_end|>", + }, + "datasets": [ + { + "chat_template": "chatml", + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "split": "train[:10%]", + "field_messages": "conversations", + "message_field_role": "from", + "message_field_content": "value", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": True, + "bf16": True, + "save_safetensors": True, + "gradient_checkpointing": "offload", + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) From f9c7c3bb72a10a4fbedbe42359b65851dc76f66a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 26 Apr 2025 14:14:52 -0400 Subject: [PATCH 0581/1405] don't use is_main_process during config validation (#2569) --- .github/workflows/multi-gpu-e2e.yml | 1 + src/axolotl/utils/schemas/config.py | 25 +++++++++++-------------- src/axolotl/utils/trainer.py | 7 +++++++ tests/e2e/patched/test_sp.py | 5 ----- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index aee4ddba68..2221bcfd4c 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -8,6 +8,7 @@ on: - 'setup.py' - 'pyproject.toml' - '.github/workflows/multi-gpu-e2e.yml' + - 'src/axolotl/core/trainers/mixins/sequence_parallel.py' workflow_dispatch: schedule: - cron: '0 0 * * 1,4' # Runs at 00:00 UTC every monday & thursday diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index f68d160dfc..2e0a6027c7 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -18,7 +18,6 @@ ) from transformers.utils.import_utils import is_torch_npu_available -from axolotl.utils.distributed import is_main_process from axolotl.utils.schemas.datasets import ( DatasetConfig, DPODataset, @@ -719,10 +718,9 @@ def check_eval_packing(cls, data): and data.get("eval_sample_packing") is None and not data.get("eval_table_size") ): - if is_main_process(): - LOG.info( - "explicitly setting `eval_sample_packing` to match `sample_packing`" - ) + LOG.info( + "explicitly setting `eval_sample_packing` to match `sample_packing`" + ) data["eval_sample_packing"] = True if ( @@ -1179,15 +1177,14 @@ def check_sequence_parallel_degree(self): # TODO: monkeypatch / callback to average losses correctly across SP ranks # / fix gradient scaling across SP ranks. Losses, grads should be scaled # according to the proportion of non-padding tokens per rank. - if is_main_process(): - LOG.warning( - "Sequence parallelism (SP) is enabled with " - f"sequence_parallel_degree={self.sequence_parallel_degree}. " - "Please note that logged losses may differ slightly to the non-SP " - "losses due to transformers Trainer implementation details. " - "Please see https://github.com/axolotl-ai-cloud/axolotl/pull/2495#issuecomment-2784022042 " - "for more details." - ) + LOG.warning( + "Sequence parallelism (SP) is enabled with " + f"sequence_parallel_degree={self.sequence_parallel_degree}. " + "Please note that logged losses may differ slightly to the non-SP " + "losses due to transformers Trainer implementation details. " + "Please see https://github.com/axolotl-ai-cloud/axolotl/pull/2495#issuecomment-2784022042 " + "for more details." + ) return self diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 3dc9ae3f66..69aaabfa68 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -528,6 +528,13 @@ def setup_torch_compile_env(cfg): def setup_deepspeed_env(cfg, stage=None): from transformers.integrations.deepspeed import HfTrainerDeepSpeedConfig + from axolotl.utils.distributed import distributed_state + + if distributed_state and distributed_state.initialized: + raise RuntimeError( + "Distributed State already initialized before Deepspeed setup" + ) + os.environ["ACCELERATE_USE_DEEPSPEED"] = "true" os.environ["ACCELERATE_DEEPSPEED_CONFIG_FILE"] = cfg.deepspeed if stage: diff --git a/tests/e2e/patched/test_sp.py b/tests/e2e/patched/test_sp.py index 6e1e2f2cba..046c482e3e 100644 --- a/tests/e2e/patched/test_sp.py +++ b/tests/e2e/patched/test_sp.py @@ -131,11 +131,6 @@ def setup_mocks(self, monkeypatch): # Mock the ring_flash_attn module monkeypatch.setitem(sys.modules, "ring_flash_attn", MagicMock()) - # Mock the is_main_process function to return True - monkeypatch.setattr( - "axolotl.utils.schemas.config.is_main_process", lambda: True - ) - @pytest.fixture def base_cfg(self): """Create a base configuration for testing.""" From dc4da4a7e202f1a68e9bb190b27ce96e85659643 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 27 Apr 2025 19:19:53 -0400 Subject: [PATCH 0582/1405] update trl to 0.17.0 (#2560) * update trl to 0.17.0 * grpo + vllm no longer supported with 2.5.1 due to vllm constraints * disable VLLM_USE_V1 for ci * imporve handle killing off of multiprocessing vllm service * debug why this doesn't run in CI * increase vllm wait time * increase timeout to 5min * upgrade to vllm 0.8.4 * dump out the vllm log for debugging * use debug logging * increase vllm start timeout * use NVL instead * disable torch compile cache * revert some commented checks now that grpo tests are fixed * increase vllm timeoout back to 5min --- .github/workflows/main.yml | 2 +- .github/workflows/multi-gpu-e2e.yml | 2 +- .github/workflows/tests.yml | 2 +- cicd/multigpu.sh | 2 +- requirements.txt | 4 +- setup.py | 4 +- tests/e2e/multigpu/solo/test_grpo.py | 107 +++++++++++++++++++++------ 7 files changed, 93 insertions(+), 30 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 14e30f20f0..61b2fc14a2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.5.1 - axolotl_extras: vllm + axolotl_extras: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 2221bcfd4c..78238bb974 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -43,7 +43,7 @@ jobs: cuda_version: 12.4.1 python_version: "3.11" pytorch: 2.5.1 - axolotl_extras: vllm + axolotl_extras: num_gpus: 2 nightly_build: "true" - cuda: 126 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 825277ce08..b149254376 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -269,7 +269,7 @@ jobs: python_version: "3.11" pytorch: 2.5.1 num_gpus: 1 - axolotl_extras: vllm + axolotl_extras: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" diff --git a/cicd/multigpu.sh b/cicd/multigpu.sh index 1e6f014717..1f74cd67df 100755 --- a/cicd/multigpu.sh +++ b/cicd/multigpu.sh @@ -20,4 +20,4 @@ pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/multigpu/patched/ \ --cov-report=xml:multigpu-coverage.xml # Upload coverage to Codecov -codecov upload-process -t $CODECOV_TOKEN -f multigpu-coverage.xml -F multigpu,docker-tests,pytorch-${PYTORCH_VERSION} +codecov upload-process -t "${CODECOV_TOKEN}" -f multigpu-coverage.xml -F multigpu,docker-tests,pytorch-${PYTORCH_VERSION} || true diff --git a/requirements.txt b/requirements.txt index 02da0abf4c..931dec3452 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,13 +11,13 @@ liger-kernel==0.5.8 packaging==23.2 -peft==0.15.1 +peft==0.15.2 transformers==4.51.3 tokenizers>=0.21.1 accelerate==1.6.0 datasets==3.5.0 deepspeed>=0.15.4 -trl==0.16.1 +trl==0.17.0 hf_xet==1.0.0 hqq==0.2.5 diff --git a/setup.py b/setup.py index 0f3892c3bb..3a494d1071 100644 --- a/setup.py +++ b/setup.py @@ -67,13 +67,13 @@ def parse_requirements(extras_require_map): if (major, minor) >= (2, 7): _install_requires.pop(_install_requires.index(xformers_version)) # _install_requires.append("xformers==0.0.29.post3") # xformers seems to be hard pinned to 2.6.0 - extras_require_map["vllm"] = ["vllm==0.8.3"] + extras_require_map["vllm"] = ["vllm==0.8.4"] elif (major, minor) >= (2, 6): _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append( "xformers==0.0.29.post2" ) # vllm needs post2 w torch 2.6 - extras_require_map["vllm"] = ["vllm==0.8.3"] + extras_require_map["vllm"] = ["vllm==0.8.4"] elif (major, minor) >= (2, 5): _install_requires.pop(_install_requires.index(xformers_version)) if patch == 0: diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index f4914ed1ab..a34d4b3f80 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -4,11 +4,14 @@ import os import random +import shutil import subprocess # nosec B404 import sys +import tempfile import time from pathlib import Path +import psutil import pytest import requests import yaml @@ -21,8 +24,8 @@ def start_vllm( - model: str, env: dict | None = None, wait: int | None = None, quiet=False, **kwargs -) -> int: + model: str, env: dict, wait: int | None = None, quiet=False, **kwargs +) -> subprocess.Popen: """ helper function to start the VLLM server in the background, mostly for testing purposes """ @@ -46,10 +49,41 @@ def start_vllm( # print out the command to be executed print(" ".join(cmd)) + vllm_logging_json = Path(tempfile.mkdtemp()) / "vllm_logging.json" + with open(vllm_logging_json, "w", encoding="utf-8") as temp_file: + temp_file.write( + """{ + "formatters": { + "json": { + "class": "pythonjsonlogger.jsonlogger.JsonFormatter" + } + }, + "handlers": { + "file": { + "class": "logging.FileHandler", + "formatter": "json", + "level": "DEBUG", + "filename": "/tmp/vllm.log", + "mode": "a" + } + }, + "loggers": { + "vllm": { + "handlers": ["file"], + "level": "DEBUG", + "propagate": false + } + }, + "version": 1 +}""" + ) + + cmd_env = env.copy() + cmd_env.update({"VLLM_LOGGING_CONFIG_PATH": vllm_logging_json}) # start `trl vllm-serve` command in the background and capture the process id process = subprocess.Popen( # pylint: disable=consider-using-with cmd, - env=env, + env=cmd_env, stdout=subprocess.DEVNULL if quiet else subprocess.PIPE, stderr=subprocess.DEVNULL if quiet else subprocess.PIPE, ) # nosec B603 @@ -58,32 +92,51 @@ def start_vllm( print(f"VLLM server process started (PID: {process.pid})") # wait until the http server is ready, even if it 404s, but timeout after 60 seconds + period_seconds = 5 started = False if wait and host and port: - for _ in range(int(wait)): + for i in range(0, int(wait), period_seconds): try: response = requests.get(f"http://{host}:{port}", timeout=1) + print(f"{i}: VLLM server (status: {response.status_code})") if int(response.status_code) in [200, 404]: started = True break - except requests.exceptions.RequestException: - pass + except requests.exceptions.RequestException as exc: + print(f"{i}: VLLM server failed to start: {str(exc)}") # also check if the process.pid is still running if not process.poll() is None: break - time.sleep(1) + time.sleep(period_seconds) if wait and not started: print( f"VLLM server process did not start within {wait} seconds. Please check your server logs." ) - process.kill() + recursive_kill(process) + with open("/tmp/vllm.log", "r", encoding="utf-8") as log_file: + print(log_file.read()) + shutil.rmtree("/tmp/vllm.log") raise RuntimeError(f"VLLM server process did not start within {wait} seconds.") - # return the process id - return process.pid + # return the process + return process + + +def recursive_kill(process: subprocess.Popen): + """ + Recursively kill a process and its children + """ + process = psutil.Process(process.pid) + for child in psutil.Process(process.pid).children(recursive=True): + child.terminate() + child.kill() + os.kill(child.pid, 9) + process.terminate() + process.kill() + os.kill(process.pid, 9) class TestGRPO: @@ -174,16 +227,17 @@ def test_llama_dora(self, temp_dir, num_gpus): current_env = os.environ.copy() env = { - "NCCL_P2P_LEVEL": "LOC", + "NCCL_P2P_LEVEL": "NVL", **current_env, "CUDA_VISIBLE_DEVICES": "1", - "VLLM_USE_V1": "0", + "VLLM_DISABLE_COMPILE_CACHE": "1", + # "VLLM_USE_V1": "0", } - vllm_process_id = start_vllm( + vllm_process = start_vllm( cfg.base_model, env=env, quiet=True, - wait=120, + wait=300, gpu_memory_utilization=0.15, max_model_len=cfg.vllm.max_model_len, enable_prefix_caching=cfg.vllm.enable_prefix_caching, @@ -202,10 +256,14 @@ def test_llama_dora(self, temp_dir, num_gpus): "--main-process-port", f"{get_torch_dist_unique_port()}", ], - env={"NCCL_P2P_LEVEL": "LOC", "NCCL_DEBUG": "INFO", **current_env}, + env={ + "NCCL_P2P_LEVEL": "NVL", + "NCCL_DEBUG": "INFO", + **current_env, + }, ) finally: - os.kill(vllm_process_id, 9) + recursive_kill(vllm_process) @pytest.mark.parametrize( "num_gpus", @@ -262,16 +320,17 @@ def test_llama_fft(self, temp_dir, num_gpus): current_env = os.environ.copy() env = { - "NCCL_P2P_LEVEL": "LOC", # nccl can be brittle, assume P2P isn't reliable + "NCCL_P2P_LEVEL": "NVL", # nccl can be brittle, assume P2P isn't reliable **current_env, "CUDA_VISIBLE_DEVICES": "1", - "VLLM_USE_V1": "0", + "VLLM_DISABLE_COMPILE_CACHE": "1", + # "VLLM_USE_V1": "0", } - vllm_process_id = start_vllm( + vllm_process = start_vllm( cfg.base_model, env=env, quiet=True, - wait=120, + wait=300, gpu_memory_utilization=0.15, max_model_len=cfg.vllm.max_model_len, enable_prefix_caching=cfg.vllm.enable_prefix_caching, @@ -290,7 +349,11 @@ def test_llama_fft(self, temp_dir, num_gpus): "--main-process-port", f"{get_torch_dist_unique_port()}", ], - env={"NCCL_P2P_LEVEL": "LOC", "NCCL_DEBUG": "INFO", **current_env}, + env={ + "NCCL_P2P_LEVEL": "NVL", + "NCCL_DEBUG": "INFO", + **current_env, + }, ) finally: - os.kill(vllm_process_id, 9) + recursive_kill(vllm_process) From 8b33ae1c4f5078e0b3d2cb31bb6856692aa3d993 Mon Sep 17 00:00:00 2001 From: Dhruv Mullick Date: Sun, 27 Apr 2025 22:31:56 -0600 Subject: [PATCH 0583/1405] Fix bug in grpo reward module import (#2571) --- src/axolotl/core/trainers/grpo/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 0d3e7b7d7e..c82fe69f2e 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -135,7 +135,9 @@ def get_reward_func(cls, reward_func_fqn: str) -> RewardFunc: try: # use importlib to dynamically load the reward function from the module reward_func_module_name = reward_func_fqn.split(".")[-1] - reward_func_module = importlib.import_module(reward_func_fqn.split(".")[-2]) + reward_func_module = importlib.import_module( + ".".join(reward_func_fqn.split(".")[:-1]) + ) reward_func = getattr(reward_func_module, reward_func_module_name) if not len(inspect.signature(reward_func).parameters) >= 2: raise ValueError( From f1df73a798c4a72c74be058ee9c306f58bdaf356 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 28 Apr 2025 21:07:45 +0700 Subject: [PATCH 0584/1405] fix(doc): clarify vllm usage with grpo (#2573) [skip ci] * fix(doc): clarify vllm usage with grpo * nit Co-authored-by: salman * Update docs/rlhf.qmd --------- Co-authored-by: Wing Lian Co-authored-by: salman --- docs/rlhf.qmd | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 490d281265..3a8f87d71b 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -502,9 +502,7 @@ The input format is a simple JSON input with customizable fields based on the ab Check out our [GRPO cookbook](https://github.com/axolotl-ai-cloud/axolotl-cookbook/tree/main/grpo#training-an-r1-style-large-language-model-using-grpo). ::: -If you have multiple GPUs available, we reccomend using `vLLM` with the `GRPOTrainer` to significantly speedup trajectory generation during training. -First, launch a `vLLM` server using `trl vllm-serve` - you may use a config file or CLI overrides to configure your vLLM server. In this example, we're -using 4 GPUs - 2 for training, and 2 for vLLM: +In the latest GRPO implementation, `vLLM` is used to significantly speedup trajectory generation during training. In this example, we're using 4 GPUs - 2 for training, and 2 for vLLM: ::: {.callout-important} Make sure you've installed the correct version of vLLM by including it as an extra when installing axolotl, e.g. `pip install axolotl[vllm]`. @@ -539,6 +537,10 @@ Your `vLLM` instance will now attempt to spin up, and it's time to kick off trai CUDA_VISIBLE_DEVICES=0,1 axolotl train grpo.yaml --num-processes 2 ``` +::: {.callout-note} +Due to TRL's implementation with vLLM, the vLLM instance must use the last N GPUs instead of the first N GPUs. This is why in the example above, we use `CUDA_VISIBLE_DEVICES=2,3` for the vLLM instance. +::: + #### Reward functions GRPO uses custom reward functions and transformations. Please have them ready locally. From 40f4ea23ab788c14cabf3f7bb5d53744cfe94198 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 28 Apr 2025 10:08:07 -0400 Subject: [PATCH 0585/1405] replace references to random 68m model w 135m smollm2 (#2570) [skip ci] * replace references to random 68m model w 135m smollm2 * use AutoTokenizer for smollm2 --- tests/conftest.py | 4 +- tests/e2e/patched/test_4d_multipack_llama.py | 10 +++- tests/e2e/patched/test_fused_llama.py | 6 +-- tests/e2e/patched/test_llama_s2_attention.py | 16 +++--- .../e2e/patched/test_lora_llama_multipack.py | 16 +++--- tests/e2e/test_dpo.py | 54 ++++++++++++------- tests/e2e/test_llama.py | 8 ++- tests/e2e/test_load_model.py | 10 ++-- tests/e2e/test_lora_llama.py | 8 ++- tests/e2e/test_optimizers.py | 28 +++++----- tests/e2e/test_schedulers.py | 8 ++- tests/test_normalize_config.py | 6 +-- tests/utils/test_models.py | 8 +-- 13 files changed, 95 insertions(+), 87 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3f3cc2732e..82e4e911bf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,9 +79,9 @@ def download_smollm2_135m_model(): @pytest.fixture(scope="session", autouse=True) -def download_llama_68m_random_model(): +def download_smollm2_135m_gptq_model(): # download the model - snapshot_download_w_retry("JackFram/llama-68m", repo_type="model") + snapshot_download_w_retry("lilmeaty/SmolLM2-135M-Instruct-GPTQ", repo_type="model") @pytest.fixture(scope="session", autouse=True) diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index 33ba47abda..2709568839 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -28,7 +28,7 @@ def test_sdp_lora_packing(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", + "base_model": "HuggingFaceTB/SmolLM2-135M", "flash_attention": False, "sdp_attention": True, "sample_packing": True, @@ -41,6 +41,9 @@ def test_sdp_lora_packing(self, temp_dir): "lora_target_linear": True, "sequence_len": 1024, "val_set_size": 0.02, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, "datasets": [ { "path": "mhenrichsen/alpaca_2k_test", @@ -73,7 +76,7 @@ def test_torch_lora_packing(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", + "base_model": "HuggingFaceTB/SmolLM2-135M", "flash_attention": False, "sdp_attention": False, "sample_packing": True, @@ -86,6 +89,9 @@ def test_torch_lora_packing(self, temp_dir): "lora_dropout": 0.05, "lora_target_linear": True, "val_set_size": 0.02, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, "datasets": [ { "path": "mhenrichsen/alpaca_2k_test", diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index 51dfec5f49..7725e095d1 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -32,7 +32,7 @@ def test_fft_packing(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", + "base_model": "HuggingFaceTB/SmolLM2-135M", "flash_attention": True, "pad_to_sequence_len": True, "flash_attn_fuse_qkv": True, @@ -41,9 +41,7 @@ def test_fft_packing(self, temp_dir): "sequence_len": 1024, "val_set_size": 0.02, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py index 3aa36772ae..3cf43ba9d3 100644 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ b/tests/e2e/patched/test_llama_s2_attention.py @@ -31,8 +31,8 @@ def test_lora_s2_attn(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "sequence_len": 16384, "sample_packing": False, "flash_attention": True, @@ -44,7 +44,9 @@ def test_lora_s2_attn(self, temp_dir): "lora_dropout": 0.05, "lora_target_linear": True, "val_set_size": 0.02, - "special_tokens": {}, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, "datasets": [ { "path": "Yukang/LongAlpaca-12k", @@ -78,14 +80,16 @@ def test_fft_s2_attn(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "sequence_len": 16384, "sample_packing": False, "flash_attention": True, "s2_attention": True, "val_set_size": 0.02, - "special_tokens": {}, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, "datasets": [ { "path": "Yukang/LongAlpaca-12k", diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index ab6e87e2ad..ca989f241e 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -31,8 +31,8 @@ def test_lora_packing(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "sample_packing": True, "flash_attention": True, @@ -44,9 +44,7 @@ def test_lora_packing(self, temp_dir): "lora_target_linear": True, "val_set_size": 0.2, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -84,9 +82,9 @@ def test_lora_gptq_packed(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "TheBlokeAI/jackfram_llama-68m-GPTQ", + "base_model": "lilmeaty/SmolLM2-135M-Instruct-GPTQ", "model_type": "AutoModelForCausalLM", - "tokenizer_type": "LlamaTokenizer", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "sample_packing": True, "flash_attention": True, @@ -100,9 +98,7 @@ def test_lora_gptq_packed(self, temp_dir): "lora_target_linear": True, "val_set_size": 0.02, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index cf7335805c..84d723ec08 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -31,8 +31,8 @@ def test_dpo_lora(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -40,7 +40,9 @@ def test_dpo_lora(self, temp_dir): "lora_alpha": 32, "lora_dropout": 0.1, "lora_target_linear": True, - "special_tokens": {}, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, "rl": "dpo", "datasets": [ { @@ -77,8 +79,8 @@ def test_dpo_nll_lora(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -86,7 +88,9 @@ def test_dpo_nll_lora(self, temp_dir): "lora_alpha": 32, "lora_dropout": 0.1, "lora_target_linear": True, - "special_tokens": {}, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, "rl": "dpo", "rpo_alpha": 0.5, "datasets": [ @@ -124,8 +128,8 @@ def test_dpo_use_weighting(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -133,7 +137,9 @@ def test_dpo_use_weighting(self, temp_dir): "lora_alpha": 32, "lora_dropout": 0.1, "lora_target_linear": True, - "special_tokens": {}, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, "rl": "dpo", "dpo_use_weighting": True, "datasets": [ @@ -172,8 +178,8 @@ def test_kto_pair_lora(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -181,7 +187,9 @@ def test_kto_pair_lora(self, temp_dir): "lora_alpha": 32, "lora_dropout": 0.1, "lora_target_linear": True, - "special_tokens": {}, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, "rl": "kto_pair", "datasets": [ { @@ -218,8 +226,8 @@ def test_ipo_lora(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -227,7 +235,9 @@ def test_ipo_lora(self, temp_dir): "lora_alpha": 32, "lora_dropout": 0.1, "lora_target_linear": True, - "special_tokens": {}, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, "rl": "ipo", "datasets": [ { @@ -264,8 +274,8 @@ def test_orpo_lora(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -273,7 +283,9 @@ def test_orpo_lora(self, temp_dir): "lora_alpha": 32, "lora_dropout": 0.1, "lora_target_linear": True, - "special_tokens": {}, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, "rl": "orpo", "orpo_alpha": 0.1, "remove_unused_columns": False, @@ -314,7 +326,7 @@ def test_kto_lora(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", + "base_model": "HuggingFaceTB/SmolLM2-135M", "tokenizer_type": "LlamaTokenizer", "sequence_len": 1024, "load_in_8bit": True, @@ -323,7 +335,9 @@ def test_kto_lora(self, temp_dir): "lora_alpha": 32, "lora_dropout": 0.1, "lora_target_linear": True, - "special_tokens": {}, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, "rl": "kto", "rl_beta": 0.5, "kto_desirable_weight": 1.0, diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index b84e432b5b..d3e37fb3fc 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -26,15 +26,13 @@ def test_fft_trust_remote_code(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "trust_remote_code": True, "sequence_len": 512, "val_set_size": 0.02, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { diff --git a/tests/e2e/test_load_model.py b/tests/e2e/test_load_model.py index 2128dbef2d..96745c0403 100644 --- a/tests/e2e/test_load_model.py +++ b/tests/e2e/test_load_model.py @@ -26,9 +26,9 @@ def setup_method(self): # load config self.cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", - "tokenizer_config": "JackFram/llama-68m", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", + "tokenizer_config": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 1024, "load_in_8bit": False, "adapter": "lora", @@ -38,9 +38,7 @@ def setup_method(self): "lora_target_linear": True, "val_set_size": 0.02, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index 8328d5b90b..e5a734b332 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -28,8 +28,8 @@ def test_lora(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -39,9 +39,7 @@ def test_lora(self, temp_dir): "lora_target_linear": True, "val_set_size": 0.02, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 8a82e34695..d3ff27ca5a 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -28,8 +28,9 @@ def test_optimi_adamw(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "model_type": "AutoModelForCausalLM", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -39,9 +40,7 @@ def test_optimi_adamw(self, temp_dir): "lora_target_linear": True, "val_set_size": 0.02, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -75,8 +74,9 @@ def test_adopt_adamw(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "model_type": "AutoModelForCausalLM", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -86,9 +86,7 @@ def test_adopt_adamw(self, temp_dir): "lora_target_linear": True, "val_set_size": 0.02, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -122,8 +120,9 @@ def test_muon(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "model_type": "AutoModelForCausalLM", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -133,9 +132,7 @@ def test_muon(self, temp_dir): "lora_target_linear": True, "val_set_size": 0.02, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -170,6 +167,7 @@ def test_fft_schedule_free_adamw(self, temp_dir): cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", + "model_type": "AutoModelForCausalLM", "sequence_len": 1024, "val_set_size": 0.01, "special_tokens": { diff --git a/tests/e2e/test_schedulers.py b/tests/e2e/test_schedulers.py index c20cebf4e6..694bb21e81 100644 --- a/tests/e2e/test_schedulers.py +++ b/tests/e2e/test_schedulers.py @@ -28,8 +28,8 @@ def test_rex_scheduler(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -39,9 +39,7 @@ def test_rex_scheduler(self, temp_dir): "lora_target_linear": True, "val_set_size": 0.02, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { diff --git a/tests/test_normalize_config.py b/tests/test_normalize_config.py index c8ca3e5506..ea98bf97d7 100644 --- a/tests/test_normalize_config.py +++ b/tests/test_normalize_config.py @@ -17,9 +17,9 @@ class NormalizeConfigTestCase(unittest.TestCase): def _get_base_cfg(self): return DictDefault( { - "base_model": "JackFram/llama-68m", - "base_model_config": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "base_model_config": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", "num_epochs": 1, "micro_batch_size": 1, "gradient_accumulation_steps": 1, diff --git a/tests/utils/test_models.py b/tests/utils/test_models.py index 83678430a5..bcc1ba5d1e 100644 --- a/tests/utils/test_models.py +++ b/tests/utils/test_models.py @@ -18,9 +18,9 @@ def setup_method(self) -> None: # load config self.cfg = DictDefault( # pylint: disable=attribute-defined-outside-init { - "base_model": "JackFram/llama-68m", - "model_type": "LlamaForCausalLM", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", + "model_type": "AutoModelForCausalLM", + "tokenizer_type": "AutoTokenizer", "load_in_8bit": True, "load_in_4bit": False, "adapter": "lora", @@ -65,7 +65,7 @@ def test_cfg_throws_error_with_s2_attention_and_sample_packing(self): "s2_attention": True, "sample_packing": True, "base_model": "", - "model_type": "LlamaForCausalLM", + "model_type": "AutoModelForCausalLM", } ) From 5d182a105660a48b5e2cca679a9d82f3c23c87cc Mon Sep 17 00:00:00 2001 From: Ezekiel Wotring <40004347+KAJdev@users.noreply.github.com> Date: Mon, 28 Apr 2025 06:08:32 -0800 Subject: [PATCH 0586/1405] Add runpod sls handler (#2530) [skip ci] * Add runpod sls handler * remove LICENSE and fix README * chore: lint * use axolotl cloud image as base and various fixes * fix: trim allowed cuda versions * restore dockerfile * chore: update title * use axolotl cloud image --------- Co-authored-by: Wing Lian Co-authored-by: NanoCode012 --- .runpod/.gitignore | 161 +++++++++ .runpod/Dockerfile | 18 + .runpod/README.md | 335 +++++++++++++++++++ .runpod/hub.json | 93 ++++++ .runpod/requirements.txt | 7 + .runpod/src/config/config.yaml | 577 +++++++++++++++++++++++++++++++++ .runpod/src/handler.py | 64 ++++ .runpod/src/test_input.json | 61 ++++ .runpod/src/train.py | 45 +++ .runpod/src/utils.py | 89 +++++ .runpod/tests.json | 85 +++++ 11 files changed, 1535 insertions(+) create mode 100644 .runpod/.gitignore create mode 100644 .runpod/Dockerfile create mode 100644 .runpod/README.md create mode 100644 .runpod/hub.json create mode 100644 .runpod/requirements.txt create mode 100644 .runpod/src/config/config.yaml create mode 100644 .runpod/src/handler.py create mode 100644 .runpod/src/test_input.json create mode 100644 .runpod/src/train.py create mode 100644 .runpod/src/utils.py create mode 100644 .runpod/tests.json diff --git a/.runpod/.gitignore b/.runpod/.gitignore new file mode 100644 index 0000000000..383570cfc6 --- /dev/null +++ b/.runpod/.gitignore @@ -0,0 +1,161 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +pod/scripts/config.yaml diff --git a/.runpod/Dockerfile b/.runpod/Dockerfile new file mode 100644 index 0000000000..107caf5f3c --- /dev/null +++ b/.runpod/Dockerfile @@ -0,0 +1,18 @@ +FROM axolotlai/axolotl-cloud:main-py3.11-cu124-2.6.0 + +COPY .runpod/requirements.txt /requirements.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + python3 -m pip install --upgrade pip && \ + python3 -m pip install --upgrade -r /requirements.txt + +# Environment settings +ARG BASE_VOLUME="/runpod-volume" +ENV BASE_VOLUME=$BASE_VOLUME +ENV HF_DATASETS_CACHE="${BASE_VOLUME}/huggingface-cache/datasets" +ENV HUGGINGFACE_HUB_CACHE="${BASE_VOLUME}/huggingface-cache/hub" +ENV TRANSFORMERS_CACHE="${BASE_VOLUME}/huggingface-cache/hub" + +COPY .runpod/src /src + +WORKDIR /src +CMD ["python3", "/src/handler.py"] diff --git a/.runpod/README.md b/.runpod/README.md new file mode 100644 index 0000000000..a631c3937a --- /dev/null +++ b/.runpod/README.md @@ -0,0 +1,335 @@ +

LLM Post Training- Full fine-tune, LoRA, QLoRa etc. Llama/Mistral/Gemma and more

+ +# Configuration Options + +This document outlines all available configuration options for training models. The configuration can be provided as a JSON request. + +## Usage + +You can use these configuration Options: + +1. As a JSON request body: + +```json +{ + "input": { + "user_id": "user", + "model_id": "model-name", + "run_id": "run-id", + "credentials": { + "wandb_api_key": "", # add your Weights & biases key. TODO: you will be able to set this in Enviornment variables. + "hf_token": "", # add your HF_token. TODO: you will be able to set this in Enviornment variables. + }, + "args": { + "base_model": "NousResearch/Llama-3.2-1B", + // ... other options + } + } +} +``` + +## Configuration Options + +### Model Configuration + +| Option | Description | Default | +| ------------------- | --------------------------------------------------------------------------------------------- | -------------------- | +| `base_model` | Path to the base model (local or HuggingFace) | Required | +| `base_model_config` | Configuration path for the base model | Same as base_model | +| `revision_of_model` | Specific model revision from HuggingFace hub | Latest | +| `tokenizer_config` | Custom tokenizer configuration path | Optional | +| `model_type` | Type of model to load | AutoModelForCausalLM | +| `tokenizer_type` | Type of tokenizer to use | AutoTokenizer | +| `hub_model_id` | Repository ID where the model will be pushed on Hugging Face Hub (format: username/repo-name) | Optional | + +## Model Family Identification + +| Option | Default | Description | +| -------------------------- | ------- | ------------------------------ | +| `is_falcon_derived_model` | `false` | Whether model is Falcon-based | +| `is_llama_derived_model` | `false` | Whether model is LLaMA-based | +| `is_qwen_derived_model` | `false` | Whether model is Qwen-based | +| `is_mistral_derived_model` | `false` | Whether model is Mistral-based | + +## Model Configuration Overrides + +| Option | Default | Description | +| ----------------------------------------------- | ---------- | ---------------------------------- | +| `overrides_of_model_config.rope_scaling.type` | `"linear"` | RoPE scaling type (linear/dynamic) | +| `overrides_of_model_config.rope_scaling.factor` | `1.0` | RoPE scaling factor | + +### Model Loading Options + +| Option | Description | Default | +| -------------- | ----------------------------- | ------- | +| `load_in_8bit` | Load model in 8-bit precision | false | +| `load_in_4bit` | Load model in 4-bit precision | false | +| `bf16` | Use bfloat16 precision | false | +| `fp16` | Use float16 precision | false | +| `tf32` | Use tensor float 32 precision | false | + +## Memory and Device Settings + +| Option | Default | Description | +| ------------------ | --------- | ----------------------- | +| `gpu_memory_limit` | `"20GiB"` | GPU memory limit | +| `lora_on_cpu` | `false` | Load LoRA on CPU | +| `device_map` | `"auto"` | Device mapping strategy | +| `max_memory` | `null` | Max memory per device | + +## Training Hyperparameters + +| Option | Default | Description | +| ----------------------------- | --------- | --------------------------- | +| `gradient_accumulation_steps` | `1` | Gradient accumulation steps | +| `micro_batch_size` | `2` | Batch size per GPU | +| `eval_batch_size` | `null` | Evaluation batch size | +| `num_epochs` | `4` | Number of training epochs | +| `warmup_steps` | `100` | Warmup steps | +| `warmup_ratio` | `0.05` | Warmup ratio | +| `learning_rate` | `0.00003` | Learning rate | +| `lr_quadratic_warmup` | `false` | Quadratic warmup | +| `logging_steps` | `null` | Logging frequency | +| `eval_steps` | `null` | Evaluation frequency | +| `evals_per_epoch` | `null` | Evaluations per epoch | +| `save_strategy` | `"epoch"` | Checkpoint saving strategy | +| `save_steps` | `null` | Saving frequency | +| `saves_per_epoch` | `null` | Saves per epoch | +| `save_total_limit` | `null` | Maximum checkpoints to keep | +| `max_steps` | `null` | Maximum training steps | + +### Dataset Configuration + +```yaml +datasets: + - path: vicgalle/alpaca-gpt4 # HuggingFace dataset or TODO: You will be able to add the local path. + type: alpaca # Format type (alpaca, gpteacher, oasst, etc.) + ds_type: json # Dataset type + data_files: path/to/data # Source data files + train_on_split: train # Dataset split to use +``` + +## Chat Template Settings + +| Option | Default | Description | +| ------------------------ | -------------------------------- | ---------------------- | +| `chat_template` | `"tokenizer_default"` | Chat template type | +| `chat_template_jinja` | `null` | Custom Jinja template | +| `default_system_message` | `"You are a helpful assistant."` | Default system message | + +## Dataset Processing + +| Option | Default | Description | +| ----------------------------- | -------------------------- | --------------------------------- | +| `dataset_prepared_path` | `"data/last_run_prepared"` | Path for prepared dataset | +| `push_dataset_to_hub` | `""` | Push dataset to HF hub | +| `dataset_processes` | `4` | Number of preprocessing processes | +| `dataset_keep_in_memory` | `false` | Keep dataset in memory | +| `shuffle_merged_datasets` | `true` | Shuffle merged datasets | +| `dataset_exact_deduplication` | `true` | Deduplicate datasets | + +## LoRA Configuration + +| Option | Default | Description | +| -------------------------- | ---------------------- | ------------------------------ | +| `adapter` | `"lora"` | Adapter type (lora/qlora) | +| `lora_model_dir` | `""` | Directory with pretrained LoRA | +| `lora_r` | `8` | LoRA attention dimension | +| `lora_alpha` | `16` | LoRA alpha parameter | +| `lora_dropout` | `0.05` | LoRA dropout | +| `lora_target_modules` | `["q_proj", "v_proj"]` | Modules to apply LoRA | +| `lora_target_linear` | `false` | Target all linear modules | +| `peft_layers_to_transform` | `[]` | Layers to transform | +| `lora_modules_to_save` | `[]` | Modules to save | +| `lora_fan_in_fan_out` | `false` | Fan in/out structure | + +## Optimization Settings + +| Option | Default | Description | +| ------------------------- | ------- | -------------------------- | +| `train_on_inputs` | `false` | Train on input prompts | +| `group_by_length` | `false` | Group by sequence length | +| `gradient_checkpointing` | `false` | Use gradient checkpointing | +| `early_stopping_patience` | `3` | Early stopping patience | + +## Learning Rate Scheduling + +| Option | Default | Description | +| -------------------------- | ---------- | -------------------- | +| `lr_scheduler` | `"cosine"` | Scheduler type | +| `lr_scheduler_kwargs` | `{}` | Scheduler parameters | +| `cosine_min_lr_ratio` | `null` | Minimum LR ratio | +| `cosine_constant_lr_ratio` | `null` | Constant LR ratio | +| `lr_div_factor` | `null` | LR division factor | + +## Optimizer Settings + +| Option | Default | Description | +| ---------------------- | ------------ | ------------------- | +| `optimizer` | `"adamw_hf"` | Optimizer choice | +| `optim_args` | `{}` | Optimizer arguments | +| `optim_target_modules` | `[]` | Target modules | +| `weight_decay` | `null` | Weight decay | +| `adam_beta1` | `null` | Adam beta1 | +| `adam_beta2` | `null` | Adam beta2 | +| `adam_epsilon` | `null` | Adam epsilon | +| `max_grad_norm` | `null` | Gradient clipping | + +## Attention Implementations + +| Option | Default | Description | +| -------------------------- | ------- | ----------------------------- | +| `flash_optimum` | `false` | Use better transformers | +| `xformers_attention` | `false` | Use xformers | +| `flash_attention` | `false` | Use flash attention | +| `flash_attn_cross_entropy` | `false` | Flash attention cross entropy | +| `flash_attn_rms_norm` | `false` | Flash attention RMS norm | +| `flash_attn_fuse_qkv` | `false` | Fuse QKV operations | +| `flash_attn_fuse_mlp` | `false` | Fuse MLP operations | +| `sdp_attention` | `false` | Use scaled dot product | +| `s2_attention` | `false` | Use shifted sparse attention | + +## Tokenizer Modifications + +| Option | Default | Description | +| ---------------- | ------- | ---------------------------- | +| `special_tokens` | - | Special tokens to add/modify | +| `tokens` | `[]` | Additional tokens | + +## Distributed Training + +| Option | Default | Description | +| ----------------------- | ------- | --------------------- | +| `fsdp` | `null` | FSDP configuration | +| `fsdp_config` | `null` | FSDP config options | +| `deepspeed` | `null` | Deepspeed config path | +| `ddp_timeout` | `null` | DDP timeout | +| `ddp_bucket_cap_mb` | `null` | DDP bucket capacity | +| `ddp_broadcast_buffers` | `null` | DDP broadcast buffers | + +
+

Example Configuration Request:

+ +Here's a complete example for fine-tuning a LLaMA model using LoRA: + +```json +{ + "input": { + "user_id": "user", + "model_id": "llama-test", + "run_id": "test-run", + "credentials": { + "wandb_api_key": "", + "hf_token": "" + }, + "args": { + "base_model": "NousResearch/Llama-3.2-1B", + "load_in_8bit": false, + "load_in_4bit": false, + "strict": false, + "datasets": [ + { + "path": "teknium/GPT4-LLM-Cleaned", + "type": "alpaca" + } + ], + "dataset_prepared_path": "last_run_prepared", + "val_set_size": 0.1, + "output_dir": "./outputs/lora-out", + "adapter": "lora", + "sequence_len": 2048, + "sample_packing": true, + "eval_sample_packing": true, + "pad_to_sequence_len": true, + "lora_r": 16, + "lora_alpha": 32, + "lora_dropout": 0.05, + "lora_target_modules": [ + "gate_proj", + "down_proj", + "up_proj", + "q_proj", + "v_proj", + "k_proj", + "o_proj" + ], + "gradient_accumulation_steps": 2, + "micro_batch_size": 2, + "num_epochs": 1, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "learning_rate": 0.0002, + "train_on_inputs": false, + "group_by_length": false, + "bf16": "auto", + "tf32": false, + "gradient_checkpointing": true, + "logging_steps": 1, + "flash_attention": true, + "loss_watchdog_threshold": 5, + "loss_watchdog_patience": 3, + "warmup_steps": 10, + "evals_per_epoch": 4, + "saves_per_epoch": 1, + "weight_decay": 0, + "hub_model_id": "runpod/llama-fr-lora", + "wandb_name": "test-run-1", + "wandb_project": "test-run-1", + "wandb_entity": "axo-test", + "special_tokens": { + "pad_token": "<|end_of_text|>" + } + } + } +} +``` + +
+ +### Advanced Features + +#### Wandb Integration + +- `wandb_project`: Project name for Weights & Biases +- `wandb_entity`: Team name in W&B +- `wandb_watch`: Monitor model with W&B +- `wandb_name`: Name of the W&B run +- `wandb_run_id`: ID for the W&B run + +#### Performance Optimization + +- `sample_packing`: Enable efficient sequence packing +- `eval_sample_packing`: Use sequence packing during evaluation +- `torch_compile`: Enable PyTorch 2.0 compilation +- `flash_attention`: Use Flash Attention implementation +- `xformers_attention`: Use xFormers attention implementation + +### Available Optimizers + +The following optimizers are supported: + +- `adamw_hf`: HuggingFace's AdamW implementation +- `adamw_torch`: PyTorch's AdamW +- `adamw_torch_fused`: Fused AdamW implementation +- `adamw_torch_xla`: XLA-optimized AdamW +- `adamw_apex_fused`: NVIDIA Apex fused AdamW +- `adafactor`: Adafactor optimizer +- `adamw_anyprecision`: Anyprecision AdamW +- `adamw_bnb_8bit`: 8-bit AdamW from bitsandbytes +- `lion_8bit`: 8-bit Lion optimizer +- `lion_32bit`: 32-bit Lion optimizer +- `sgd`: Stochastic Gradient Descent +- `adagrad`: Adagrad optimizer + +## Notes + +- Set `load_in_8bit: true` or `load_in_4bit: true` for memory-efficient training +- Enable `flash_attention: true` for faster training on modern GPUs +- Use `gradient_checkpointing: true` to reduce memory usage +- Adjust `micro_batch_size` and `gradient_accumulation_steps` based on your GPU memory + +For more detailed information, please refer to the [documentation](https://axolotl-ai-cloud.github.io/axolotl/docs/config.html). + +### Errors: + +- if you face any issues with the Flash Attention-2, Delete yoor worker and Re-start. diff --git a/.runpod/hub.json b/.runpod/hub.json new file mode 100644 index 0000000000..a243a27d80 --- /dev/null +++ b/.runpod/hub.json @@ -0,0 +1,93 @@ +{ + "title": "Axolotl Fine-Tuning", + "description": "Serverless fine-tuning of open-source LLMs with Axolotl. Supports LoRA, QLoRA, DPO, and more using Hugging Face models and datasets.", + "type": "serverless", + "category": "language", + "iconUrl": "https://avatars.githubusercontent.com/u/167502477", + "config": { + "runsOn": "GPU", + "containerDiskInGb": 200, + "gpuCount": 1, + "allowedCudaVersions": [ + "12.8", + "12.7", + "12.6", + "12.5", + "12.4" + ], + "presets": [], + "env": [ + { + "key": "TOKENIZER", + "input": { + "name": "Tokenizer", + "type": "string", + "description": "Name or path of the Hugging Face tokenizer to use.", + "default": "", + "advanced": true + } + }, + { + "key": "MAX_NUM_SEQS", + "input": { + "name": "Max Num Seqs", + "type": "number", + "description": "Maximum number of sequences per iteration.", + "default": 256, + "advanced": true + } + }, + { + "key": "DISABLE_LOG_STATS", + "input": { + "name": "Disable Log Stats", + "type": "boolean", + "description": "Disable logging statistics.", + "default": false, + "trueValue": "true", + "falseValue": "false" + } + }, + { + "key": "LOAD_FORMAT", + "input": { + "name": "Load Format", + "type": "string", + "description": "The format of the model weights to load.", + "default": "auto", + "options": [ + { + "label": "auto", + "value": "auto" + }, + { + "label": "pt", + "value": "pt" + }, + { + "label": "safetensors", + "value": "safetensors" + }, + { + "label": "npcache", + "value": "npcache" + }, + { + "label": "dummy", + "value": "dummy" + }, + { + "label": "tensorizer", + "value": "tensorizer" + }, + { + "label": "bitsandbytes", + "value": "bitsandbytes" + } + ], + "advanced": true + } + } + ] + } +} diff --git a/.runpod/requirements.txt b/.runpod/requirements.txt new file mode 100644 index 0000000000..345bdda350 --- /dev/null +++ b/.runpod/requirements.txt @@ -0,0 +1,7 @@ +# Required Python packages get listed here, one per line. +# Reccomended to lock the version number to avoid unexpected changes. + +# You can also install packages from a git repository, e.g.: +# git+https://github.com/runpod/runpod-python.git +# To learn more, see https://pip.pypa.io/en/stable/reference/requirements-file-format/ +runpod~=1.7.0 diff --git a/.runpod/src/config/config.yaml b/.runpod/src/config/config.yaml new file mode 100644 index 0000000000..4dff37caef --- /dev/null +++ b/.runpod/src/config/config.yaml @@ -0,0 +1,577 @@ +# # This is the huggingface model that contains *.pt, *.safetensors, or *.bin files +# # This can also be a relative path to a model on disk +# base_model: ./llama-7b-hf +# # You can specify an ignore pattern if the model repo contains more than 1 model type (*.pt, etc) +# base_model_ignore_patterns: +# # If the base_model repo on hf hub doesn't include configuration .json files, +# # You can set that here, or leave this empty to default to base_model +# base_model_config: ./llama-7b-hf +# # You can specify to choose a specific model revision from huggingface hub +# model_revision: +# # Optional tokenizer configuration override in case you want to use a different tokenizer +# # than the one defined in the base model +# tokenizer_config: +# # If you want to specify the type of model to load, AutoModelForCausalLM is a good choice too +# model_type: AutoModelForCausalLM +# # Corresponding tokenizer for the model AutoTokenizer is a good choice +# tokenizer_type: AutoTokenizer +# # Trust remote code for untrusted source +# trust_remote_code: +# # use_fast option for tokenizer loading from_pretrained, default to True +# tokenizer_use_fast: +# # Whether to use the legacy tokenizer setting, defaults to True +# tokenizer_legacy: +# # Resize the model embeddings when new tokens are added to multiples of 32 +# # This is reported to improve training speed on some models +# resize_token_embeddings_to_32x: + +# # Used to identify which the model is based on +# is_falcon_derived_model: +# is_llama_derived_model: +# # Please note that if you set this to true, `padding_side` will be set to "left" by default +# is_mistral_derived_model: +# is_qwen_derived_model: + +# # optional overrides to the base model configuration +# model_config: +# # RoPE Scaling https://github.com/huggingface/transformers/pull/24653 +# rope_scaling: +# type: # linear | dynamic +# factor: # float + + +# # Whether you are training a 4-bit GPTQ quantized model +# gptq: true +# gptq_groupsize: 128 # group size +# gptq_model_v1: false # v1 or v2 + +# # This will attempt to quantize the model down to 8 bits and use adam 8 bit optimizer +# load_in_8bit: true +# # Use bitsandbytes 4 bit +# load_in_4bit: + +# # Use CUDA bf16 +# bf16: true # bool or 'full' for `bf16_full_eval`. require >=ampere +# # Use CUDA fp16 +# fp16: true +# # Use CUDA tf32 +# tf32: true # require >=ampere + +# # No AMP (automatic mixed precision) +# bfloat16: true # require >=ampere +# float16: true + +# # A list of one or more datasets to finetune the model with +# datasets: +# # HuggingFace dataset repo | s3://,gs:// path | "json" for local dataset, make sure to fill data_files +# - path: vicgalle/alpaca-gpt4 +# # The type of prompt to use for training. [alpaca, sharegpt, gpteacher, oasst, reflection] +# type: alpaca # format | format: (chat/instruct) | .load_ +# ds_type: # Optional[str] (json|arrow|parquet|text|csv) defines the datatype when path is a file +# data_files: # Optional[str] path to source data files +# shards: # Optional[int] number of shards to split data into +# name: # Optional[str] name of dataset configuration to load +# train_on_split: train # Optional[str] name of dataset split to load from + +# # Optional[str] fastchat conversation type, only used with type: sharegpt +# conversation: # Options (see Conversation 'name'): https://github.com/lm-sys/FastChat/blob/main/fastchat/conversation.py +# field_human: # Optional[str]. Human key to use for conversation. +# field_model: # Optional[str]. Assistant key to use for conversation. + +# # Custom user prompt +# - path: repo +# type: +# # The below are defaults. only set what's needed. +# system_prompt: "" +# system_format: "{system}" +# field_system: system +# field_instruction: instruction +# field_input: input +# field_output: output + +# # Customizable to be single line or multi-line +# # 'format' can include {input} +# format: |- +# User: {instruction} {input} +# Assistant: +# # 'no_input_format' cannot include {input} +# no_input_format: "{instruction} " + +# # For `completion` datsets only, uses the provided field instead of `text` column +# field: + +# # Axolotl attempts to save the dataset as an arrow after packing the data together so +# # subsequent training attempts load faster, relative path +# dataset_prepared_path: data/last_run_prepared +# # Push prepared dataset to hub +# push_dataset_to_hub: # repo path +# # The maximum number of processes to use while preprocessing your input dataset. This defaults to `os.cpu_count()` +# # if not set. +# dataset_processes: # defaults to os.cpu_count() if not set +# # push checkpoints to hub +# hub_model_id: # repo path to push finetuned model +# # how to push checkpoints to hub +# # https://huggingface.co/docs/transformers/v4.31.0/en/main_classes/trainer#transformers.TrainingArguments.hub_strategy +# hub_strategy: +# # Whether to use hf `use_auth_token` for loading datasets. Useful for fetching private datasets +# # Required to be true when used in combination with `push_dataset_to_hub` +# hf_use_auth_token: # boolean +# # How much of the dataset to set aside as evaluation. 1 = 100%, 0.50 = 50%, etc. 0 for no eval. +# val_set_size: 0.04 +# # Num shards for whole dataset +# dataset_shard_num: +# # Index of shard to use for whole dataset +# dataset_shard_idx: + +# # The maximum length of an input to train with, this should typically be less than 2048 +# # as most models have a token/context limit of 2048 +# sequence_len: 2048 +# # Pad inputs so each step uses constant sized buffers +# # This will reduce memory fragmentation and may prevent OOMs, by re-using memory more efficiently +# pad_to_sequence_len: +# # Max sequence length to concatenate training samples together up to +# # Inspired by StackLLaMA. see https://huggingface.co/blog/stackllama#supervised-fine-tuning +# # FutureWarning: This will soon be DEPRECATED +# max_packed_sequence_len: 1024 +# # Use efficient multi-packing with block diagonal attention and per sequence position_ids. Recommend set to 'true' +# sample_packing: +# # Set to 'false' if getting errors during eval with sample_packing on. +# eval_sample_packing: +# # You can set these packing optimizations AFTER starting a training at least once. +# # The trainer will provide recommended values for these values. +# sample_packing_eff_est: +# total_num_tokens: + +# # If you want to use 'lora' or 'qlora' or leave blank to train all parameters in original model +# adapter: lora +# # If you already have a lora model trained that you want to load, put that here. +# # This means after training, if you want to test the model, you should set this to the value of `lora_out_dir`. +# lora_model_dir: + +# # LoRA hyperparameters +# # For more details about the following options, see: +# # https://www.anyscale.com/blog/fine-tuning-llms-lora-or-full-parameter-an-in-depth-analysis-with-llama-2 +# lora_r: 8 +# lora_alpha: 16 +# lora_dropout: 0.05 +# lora_target_modules: +# - q_proj +# - v_proj +# # - k_proj +# # - o_proj +# # - gate_proj +# # - down_proj +# # - up_proj +# lora_target_linear: # If true, will target all linear layers + +# # If you added new tokens to the tokenizer, you may need to save some LoRA modules because they need to know the new tokens. +# # For LLaMA and Mistral, you need to save `embed_tokens` and `lm_head`. It may vary for other models. +# # `embed_tokens` converts tokens to embeddings, and `lm_head` converts embeddings to token probabilities. +# # https://github.com/huggingface/peft/issues/334#issuecomment-1561727994 +# lora_modules_to_save: +# # - embed_tokens +# # - lm_head + +# # Once you complete training, the model will be saved to the following directory. +# # If you merge the adapter to the base model, a subdirectory `merged` will be created under this directory. +# # Make sure `lora_model_dir` points to this directory if you want to use the trained model. +# lora_out_dir: +# lora_fan_in_fan_out: false + +# # ReLoRA configuration +# # Must use either 'lora' or 'qlora' adapter, and does not support fsdp or deepspeed +# relora_steps: # Number of steps per ReLoRA restart +# relora_warmup_steps: # Number of per-restart warmup steps +# relora_cpu_offload: # True to perform lora weight merges on cpu during restarts, for modest gpu memory savings + +# # wandb configuration if you're using it +# wandb_mode: # "offline" to save run metadata locally and not sync to the server, "disabled" to turn off wandb +# wandb_project: # Your wandb project name +# wandb_entity: # A wandb Team name if using a Team +# wandb_watch: +# wandb_run_id: # Set the name of your wandb run +# wandb_log_model: # "checkpoint" to log model to wandb Artifacts every `save_steps` or "end" to log only at the end of training + +# # Where to save the full-finetuned model to +# output_dir: ./completed-model + +# # Whether to use torch.compile and which backend to use +# torch_compile: # bool +# torch_compile_backend: # Optional[str] + +# # Training hyperparameters + +# # If greater than 1, backpropagation will be skipped and the gradients will be accumulated for the given number of steps. +# gradient_accumulation_steps: 1 +# # The number of samples to include in each batch. This is the number of samples sent to each GPU. +# micro_batch_size: 2 +# eval_batch_size: +# num_epochs: 4 +# warmup_steps: 100 # cannot use with warmup_ratio +# warmup_ratio: 0.05 # cannot use with warmup_steps +# learning_rate: 0.00003 +# lr_quadratic_warmup: +# logging_steps: +# save_strategy: # Set to `no` to skip checkpoint saves +# save_steps: # Leave empty to save at each epoch +# eval_steps: # Leave empty to eval at each epoch, integers for every N steps. decimal for fraction of total steps +# save_total_limit: # Checkpoints saved at a time +# # Maximum number of iterations to train for. It precedes num_epochs which means that +# # if both are set, num_epochs will not be guaranteed. +# # e.g., when 1 epoch is 1000 steps => `num_epochs: 2` and `max_steps: 100` will train for 100 steps +# max_steps: + +# eval_table_size: # Approximate number of predictions sent to wandb depending on batch size. Enabled above 0. Default is 0 +# eval_table_max_new_tokens: # Total number of tokens generated for predictions sent to wandb. Default is 128 + +# # Save model as safetensors (require safetensors package) +# save_safetensors: + +# # Whether to mask out or include the human's prompt from the training labels +# train_on_inputs: false +# # Group similarly sized data to minimize padding. +# # May be slower to start, as it must download and sort the entire dataset. +# # Note that training loss may have an oscillating pattern with this enabled. +# group_by_length: false + +# # Whether to use gradient checkpointing https://huggingface.co/docs/transformers/v4.18.0/en/performance#gradient-checkpointing +# gradient_checkpointing: false + +# # Stop training after this many evaluation losses have increased in a row +# # https://huggingface.co/transformers/v4.2.2/_modules/transformers/trainer_callback.html#EarlyStoppingCallback +# early_stopping_patience: 3 + +# # Specify a scheduler and kwargs to use with the optimizer +# lr_scheduler: # 'one_cycle' | 'log_sweep' | empty for cosine +# lr_scheduler_kwargs: + +# # For one_cycle optim +# lr_div_factor: # Learning rate div factor + +# # For log_sweep optim +# log_sweep_min_lr: +# log_sweep_max_lr: + +# # Specify optimizer +# # Valid values are driven by the Transformers OptimizerNames class, see: +# # https://github.com/huggingface/transformers/blob/95b374952dc27d8511541d6f5a4e22c9ec11fb24/src/transformers/training_args.py#L134 +# # +# # Note that not all optimizers may be available in your environment, ex: 'adamw_anyprecision' is part of +# # torchdistx, 'adamw_bnb_8bit' is part of bnb.optim.Adam8bit, etc. When in doubt, it is recommended to start with the optimizer used +# # in the examples/ for your model and fine-tuning use case. +# # +# # Valid values for 'optimizer' include: +# # - adamw_hf +# # - adamw_torch +# # - adamw_torch_fused +# # - adamw_torch_xla +# # - adamw_apex_fused +# # - adafactor +# # - adamw_anyprecision +# # - sgd +# # - adagrad +# # - adamw_bnb_8bit +# # - lion_8bit +# # - lion_32bit +# # - paged_adamw_32bit +# # - paged_adamw_8bit +# # - paged_lion_32bit +# # - paged_lion_8bit +# optimizer: +# # Specify weight decay +# weight_decay: +# # adamw hyperparams +# adam_beta1: +# adam_beta2: +# adam_epsilon: +# # Gradient clipping max norm +# max_grad_norm: + +# # Augmentation techniques +# # NEFT https://arxiv.org/abs/2310.05914, set this to a number (paper default is 5) to add noise to embeddings +# # currently only supported on Llama and Mistral +# noisy_embedding_alpha: + +# # Whether to bettertransformers +# flash_optimum: +# # Whether to use xformers attention patch https://github.com/facebookresearch/xformers: +# xformers_attention: +# # Whether to use flash attention patch https://github.com/Dao-AILab/flash-attention: +# flash_attention: +# flash_attn_cross_entropy: # Whether to use flash-attention cross entropy implementation - advanced use only +# flash_attn_rms_norm: # Whether to use flash-attention rms norm implementation - advanced use only +# flash_attn_fuse_qkv: # Whether to fuse QKV into a single operation +# flash_attn_fuse_mlp: # Whether to fuse part of the MLP into a single operation +# # Whether to use scaled-dot-product attention +# # https://pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html +# sdp_attention: +# # Landmark attention (only llama) +# landmark_attention: +# # xpos RoPE see https://github.com/kaiokendev/cutoff-len-is-context-len/blob/main/util/xpos_rope_llama_monkey_patch.py +# # LLaMA only +# xpos_rope: + +# # Resume from a specific checkpoint dir +# resume_from_checkpoint: +# # If resume_from_checkpoint isn't set and you simply want it to start where it left off. +# # Be careful with this being turned on between different models. +# auto_resume_from_checkpoints: false + +# # Don't mess with this, it's here for accelerate and torchrun +# local_rank: + +# # Add or change special tokens. +# # If you add tokens here, you don't need to add them to the `tokens` list. +# special_tokens: +# # bos_token: "" +# # eos_token: "" +# # unk_token: "" + +# # Add extra tokens. +# tokens: + +# # FSDP +# fsdp: +# fsdp_config: + +# # Deepspeed config path. e.g., deepspeed/zero3.json +# deepspeed: + +# # Advanced DDP Arguments +# ddp_timeout: +# ddp_bucket_cap_mb: +# ddp_broadcast_buffers: + +# # Path to torch distx for optim 'adamw_anyprecision' +# torchdistx_path: + +# # Set to HF dataset for type: 'completion' for streaming instead of pre-tokenize +# pretraining_dataset: + +# # Debug mode +# debug: + +# # Seed +# seed: + +# # Allow overwrite yml config using from cli +# strict: + + + +base_model: ${BASE_MODEL} +base_model_ignore_patterns: ${BASE_MODEL_IGNORE_PATTERNS} +base_model_config: ${BASE_MODEL_CONFIG} +revision_of_model: ${REVISION_OF_MODEL} +tokenizer_config: ${TOKENIZER_CONFIG} +model_type: ${MODEL_TYPE} +tokenizer_type: ${TOKENIZER_TYPE} +trust_remote_code: ${TRUST_REMOTE_CODE} +tokenizer_use_fast: ${TOKENIZER_USE_FAST} +tokenizer_legacy: ${TOKENIZER_LEGACY} +resize_token_embeddings_to_32x: ${RESIZE_TOKEN_EMBEDDINGS_TO_32X} + +is_falcon_derived_model: ${IS_FALCON_DERIVED_MODEL} +is_llama_derived_model: ${IS_LLAMA_DERIVED_MODEL} +is_qwen_derived_model: ${IS_QWEN_DERIVED_MODEL} +is_mistral_derived_model: ${IS_MISTRAL_DERIVED_MODEL} + +overrides_of_model_config: + rope_scaling: + type: ${ROPE_SCALING_TYPE} + factor: ${ROPE_SCALING_FACTOR} + +bnb_config_kwargs: + llm_int8_has_fp16_weight: ${BNB_LLM_INT8_HAS_FP16_WEIGHT} + bnb_4bit_quant_type: ${BNB_4BIT_QUANT_TYPE} + bnb_4bit_use_double_quant: ${BNB_4BIT_USE_DOUBLE_QUANT} + +gptq: ${GPTQ} +load_in_8bit: ${LOAD_IN_8BIT} +load_in_4bit: ${LOAD_IN_4BIT} +bf16: ${BF16} +fp16: ${FP16} +tf32: ${TF32} +bfloat16: ${BFLOAT16} +float16: ${FLOAT16} + +gpu_memory_limit: ${GPU_MEMORY_LIMIT} +lora_on_cpu: ${LORA_ON_CPU} + +datasets: + - path: ${DATASET_PATH} + type: ${DATASET_TYPE} + ds_type: ${DATASET_DS_TYPE} + data_files: ${DATASET_DATA_FILES} + shards: ${DATASET_SHARDS} + name: ${DATASET_NAME} + train_on_split: ${DATASET_TRAIN_ON_SPLIT} + revision: ${DATASET_REVISION} + trust_remote_code: ${DATASET_TRUST_REMOTE_CODE} + +rl: ${RL} +dpo_use_weighting: ${DPO_USE_WEIGHTING} + +chat_template: ${CHAT_TEMPLATE} +chat_template_jinja: ${CHAT_TEMPLATE_JINJA} +default_system_message: ${DEFAULT_SYSTEM_MESSAGE} +dataset_prepared_path: ${DATASET_PREPARED_PATH} +push_dataset_to_hub: ${PUSH_DATASET_TO_HUB} +dataset_processes: ${DATASET_PROCESSES} +dataset_keep_in_memory: ${DATASET_KEEP_IN_MEMORY} +hub_model_id: ${HUB_MODEL_ID} +hub_strategy: ${HUB_STRATEGY} +hf_use_auth_token: ${HF_USE_AUTH_TOKEN} +val_set_size: ${VAL_SET_SIZE} +dataset_shard_num: ${DATASET_SHARD_NUM} +dataset_shard_idx: ${DATASET_SHARD_IDX} + +sequence_len: ${SEQUENCE_LEN} +pad_to_sequence_len: ${PAD_TO_SEQUENCE_LEN} +sample_packing: ${SAMPLE_PACKING} +eval_sample_packing: ${EVAL_SAMPLE_PACKING} +sample_packing_eff_est: ${SAMPLE_PACKING_EFF_EST} +total_num_tokens: ${TOTAL_NUM_TOKENS} +sample_packing_group_size: ${SAMPLE_PACKING_GROUP_SIZE} +sample_packing_bin_size: ${SAMPLE_PACKING_BIN_SIZE} + +batch_flattening: ${BATCH_FLATTENING} +device_map: ${DEVICE_MAP} +max_memory: ${MAX_MEMORY} + +adapter: ${ADAPTER} +lora_model_dir: ${LORA_MODEL_DIR} + +lora_r: ${LORA_R} +lora_alpha: ${LORA_ALPHA} +lora_dropout: ${LORA_DROPOUT} +lora_target_modules: + - ${LORA_TARGET_MODULES} +lora_target_linear: ${LORA_TARGET_LINEAR} +peft_layers_to_transform: ${PEFT_LAYERS_TO_TRANSFORM} +lora_modules_to_save: ${LORA_MODULES_TO_SAVE} +lora_fan_in_fan_out: ${LORA_FAN_IN_FAN_OUT} + +loraplus_lr_ratio: ${LORAPLUS_LR_RATIO} +loraplus_lr_embedding: ${LORAPLUS_LR_EMBEDDING} + +peft: + loftq_config: + loftq_bits: ${LOFTQ_BITS} + +relora_steps: ${RELORA_STEPS} +relora_warmup_steps: ${RELORA_WARMUP_STEPS} +relora_anneal_steps: ${RELORA_ANNEAL_STEPS} +relora_prune_ratio: ${RELORA_PRUNE_RATIO} +relora_cpu_offload: ${RELORA_CPU_OFFLOAD} + +wandb_mode: ${WANDB_MODE} +wandb_project: ${WANDB_PROJECT} +wandb_entity: ${WANDB_ENTITY} +wandb_watch: ${WANDB_WATCH} +wandb_name: ${WANDB_NAME} +wandb_run_id: ${WANDB_RUN_ID} +wandb_log_model: ${WANDB_LOG_MODEL} + +mlflow_tracking_uri: ${MLFLOW_TRACKING_URI} +mlflow_experiment_name: ${MLFLOW_EXPERIMENT_NAME} +mlflow_run_name: ${MLFLOW_RUN_NAME} +hf_mlflow_log_artifacts: ${HF_MLFLOW_LOG_ARTIFACTS} + +use_comet: ${USE_COMET} +comet_api_key: ${COMET_API_KEY} +comet_workspace: ${COMET_WORKSPACE} +comet_project_name: ${COMET_PROJECT_NAME} +comet_experiment_key: ${COMET_EXPERIMENT_KEY} +comet_mode: ${COMET_MODE} +comet_online: ${COMET_ONLINE} +comet_experiment_config: ${COMET_EXPERIMENT_CONFIG} + +output_dir: ${OUTPUT_DIR} + +torch_compile: ${TORCH_COMPILE} +torch_compile_backend: ${TORCH_COMPILE_BACKEND} + +gradient_accumulation_steps: ${GRADIENT_ACCUMULATION_STEPS} +micro_batch_size: ${MICRO_BATCH_SIZE} +eval_batch_size: ${EVAL_BATCH_SIZE} +num_epochs: ${NUM_EPOCHS} +warmup_steps: ${WARMUP_STEPS} +warmup_ratio: ${WARMUP_RATIO} +learning_rate: ${LEARNING_RATE} +lr_quadratic_warmup: ${LR_QUADRATIC_WARMUP} +logging_steps: ${LOGGING_STEPS} +eval_steps: ${EVAL_STEPS} +evals_per_epoch: ${EVALS_PER_EPOCH} +save_strategy: ${SAVE_STRATEGY} +save_steps: ${SAVE_STEPS} +saves_per_epoch: ${SAVES_PER_EPOCH} +save_total_limit: ${SAVE_TOTAL_LIMIT} +max_steps: ${MAX_STEPS} + +eval_table_size: ${EVAL_TABLE_SIZE} +eval_max_new_tokens: ${EVAL_MAX_NEW_TOKENS} +eval_causal_lm_metrics: ${EVAL_CAUSAL_LM_METRICS} + +profiler_steps: ${PROFILER_STEPS} +loss_watchdog_threshold: ${LOSS_WATCHDOG_THRESHOLD} +loss_watchdog_patience: ${LOSS_WATCHDOG_PATIENCE} + +save_safetensors: ${SAVE_SAFETENSORS} +train_on_inputs: ${TRAIN_ON_INPUTS} +group_by_length: ${GROUP_BY_LENGTH} +gradient_checkpointing: ${GRADIENT_CHECKPOINTING} +early_stopping_patience: ${EARLY_STOPPING_PATIENCE} + +lr_scheduler: ${LR_SCHEDULER} +lr_scheduler_kwargs: ${LR_SCHEDULER_KWARGS} +cosine_min_lr_ratio: ${COSINE_MIN_LR_RATIO} +cosine_constant_lr_ratio: ${COSINE_CONSTANT_LR_RATIO} +lr_div_factor: ${LR_DIV_FACTOR} + +optimizer: ${OPTIMIZER} +optim_args: ${OPTIM_ARGS} +optim_target_modules: ${OPTIM_TARGET_MODULES} +weight_decay: ${WEIGHT_DECAY} +adam_beta1: ${ADAM_BETA1} +adam_beta2: ${ADAM_BETA2} +adam_epsilon: ${ADAM_EPSILON} +max_grad_norm: ${MAX_GRAD_NORM} + +neftune_noise_alpha: ${NEFTUNE_NOISE_ALPHA} + +flash_optimum: ${FLASH_OPTIMUM} +xformers_attention: ${XFORMERS_ATTENTION} +flash_attention: ${FLASH_ATTENTION} +flash_attn_cross_entropy: ${FLASH_ATTN_CROSS_ENTROPY} +flash_attn_rms_norm: ${FLASH_ATTN_RMS_NORM} +flash_attn_fuse_qkv: ${FLASH_ATTN_FUSE_QKV} +flash_attn_fuse_mlp: ${FLASH_ATTN_FUSE_MLP} +sdp_attention: ${SDP_ATTENTION} +s2_attention: ${S2_ATTENTION} +resume_from_checkpoint: ${RESUME_FROM_CHECKPOINT} +auto_resume_from_checkpoints: ${AUTO_RESUME_FROM_CHECKPOINTS} + +local_rank: ${LOCAL_RANK} + +special_tokens: + bos_token: ${SPECIAL_TOKEN_BOS} + eos_token: ${SPECIAL_TOKEN_EOS} + unk_token: ${SPECIAL_TOKEN_UNK} + pad_token: ${SPECIAL_TOKEN_PAD} + +tokens: ${TOKENS} + +fsdp: ${FSDP} +fsdp_config: ${FSDP_CONFIG} +deepspeed: ${DEEPSPEED} + +ddp_timeout: ${DDP_TIMEOUT} +ddp_bucket_cap_mb: ${DDP_BUCKET_CAP_MB} +ddp_broadcast_buffers: ${DDP_BROADCAST_BUFFERS} + +torchdistx_path: ${TORCHDISTX_PATH} +pretraining_dataset: ${PRETRAINING_DATASET} +debug: ${DEBUG} +seed: ${SEED} +strict: ${STRICT} diff --git a/.runpod/src/handler.py b/.runpod/src/handler.py new file mode 100644 index 0000000000..21073dff4d --- /dev/null +++ b/.runpod/src/handler.py @@ -0,0 +1,64 @@ +""" +Runpod serverless entrypoint handler +""" + +import os + +import runpod +import yaml +from huggingface_hub._login import login +from train import train +from utils import get_output_dir + +BASE_VOLUME = os.environ.get("BASE_VOLUME", "/runpod-volume") +if not os.path.exists(BASE_VOLUME): + os.makedirs(BASE_VOLUME) + +logger = runpod.RunPodLogger() + + +async def handler(job): + runpod_job_id = job["id"] + inputs = job["input"] + run_id = inputs.get("run_id", "default_run_id") + args = inputs.get("args", {}) + + # Set output directory + output_dir = os.path.join(BASE_VOLUME, get_output_dir(run_id)) + args["output_dir"] = output_dir + + # First save args to a temporary config file + config_path = "/workspace/test_config.yaml" + + # Add run_name and job_id to args before saving + args["run_name"] = run_id + args["runpod_job_id"] = runpod_job_id + + yaml_data = yaml.dump(args, default_flow_style=False) + with open(config_path, "w", encoding="utf-8") as file: + file.write(yaml_data) + + # Handle credentials + credentials = inputs.get("credentials", {}) + + if "wandb_api_key" in credentials: + os.environ["WANDB_API_KEY"] = credentials["wandb_api_key"] + if "hf_token" in credentials: + os.environ["HF_TOKEN"] = credentials["hf_token"] + + if os.environ.get("HF_TOKEN"): + login(token=os.environ["HF_TOKEN"]) + else: + logger.info("No HF_TOKEN provided. Skipping login.") + + logger.info("Starting Training.") + async for result in train(config_path): # Pass the config path instead of args + logger.info(result) + logger.info("Training Complete.") + + # Cleanup + del os.environ["WANDB_API_KEY"] + del os.environ["HF_TOKEN"] + + +runpod.serverless.start({"handler": handler, "return_aggregate_stream": True}) diff --git a/.runpod/src/test_input.json b/.runpod/src/test_input.json new file mode 100644 index 0000000000..889e8ee273 --- /dev/null +++ b/.runpod/src/test_input.json @@ -0,0 +1,61 @@ +{ + "input": { + "user_id": "user", + "model_id": "llama-test", + "run_id": "llama-test", + "credentials": { + "wandb_api_key": "", + "hf_token": "" + }, + "args": { + "base_model": "NousResearch/Meta-Llama-3-8B", + "model_type": "LlamaForCausalLM", + "tokenizer_type": "AutoTokenizer", + "load_in_8bit": true, + "load_in_4bit": false, + "strict": false, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca" + } + ], + "val_set_size": 0.05, + "output_dir": "./outputs/lora-out", + "sequence_len": 4096, + "sample_packing": true, + "eval_sample_packing": false, + "pad_to_sequence_len": true, + "adapter": "lora", + "lora_r": 32, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": true, + "lora_modules_to_save": [ + "embed_tokens", + "lm_head" + ], + "gradient_accumulation_steps": 4, + "micro_batch_size": 2, + "num_epochs": 1, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "learning_rate": 0.0002, + "train_on_inputs": false, + "group_by_length": false, + "bf16": "auto", + "tf32": false, + "gradient_checkpointing": true, + "logging_steps": 1, + "flash_attention": true, + "warmup_steps": 1, + "evals_per_epoch": 1, + "eval_max_new_tokens": 128, + "saves_per_epoch": 1, + "weight_decay": 0.0, + "special_tokens": { + "pad_token": "<|end_of_text|>" + } + } + } +} diff --git a/.runpod/src/train.py b/.runpod/src/train.py new file mode 100644 index 0000000000..72edda9405 --- /dev/null +++ b/.runpod/src/train.py @@ -0,0 +1,45 @@ +""" +Runpod train entrypoint +""" + +import asyncio + + +async def train(config_path: str, gpu_id: str = "0", preprocess: bool = True): + """ + Run preprocessing (if enabled) and training with the given config file + :param config_path: Path to the YAML config file + :param gpu_id: GPU ID to use (default: "0") + :param preprocess: Whether to run preprocessing (default: True) + + """ + # First check if preprocessing is needed + if preprocess: + # Preprocess command + preprocess_cmd = ( + f"CUDA_VISIBLE_DEVICES={gpu_id} axolotl preprocess {config_path}" + ) + process = await asyncio.create_subprocess_shell( + preprocess_cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + ) + + if process.stdout is not None: + async for line in process.stdout: + yield f"Preprocessing: {line.decode().strip()}" + await process.wait() + yield "Preprocessing completed." + else: + yield "Skipping preprocessing step." + + # Training command + train_cmd = f"axolotl train {config_path}" + process = await asyncio.create_subprocess_shell( + train_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT + ) + + if process.stdout is not None: + async for line in process.stdout: + yield f"Training: {line.decode().strip()}" + await process.wait() diff --git a/.runpod/src/utils.py b/.runpod/src/utils.py new file mode 100644 index 0000000000..8245aecf43 --- /dev/null +++ b/.runpod/src/utils.py @@ -0,0 +1,89 @@ +""" +Runpod launcher utils +""" + +import os + +import yaml + + +def get_output_dir(run_id): + path = f"fine-tuning/{run_id}" + return path + + +def make_valid_config(input_args): + """ + Creates and saves updated config file, returns the path to the new config + :param input_args: dict of input args + :return: str, path to the updated config file + """ + # Load default config + with open("config/config.yaml", "r", encoding="utf-8") as fin: + all_args = yaml.safe_load(fin) + + if not input_args: + print("No args provided, using defaults") + else: + all_args.update(input_args) + + # Create updated config path + updated_config_path = "config/updated_config.yaml" + + # Save updated config to new file + with open(updated_config_path, "w", encoding="utf-8") as f: + yaml.dump(all_args, f) + + return updated_config_path + + +def set_config_env_vars(args: dict): + """ + Convert API arguments into environment variables. + Handles nested dictionaries, lists, and special values. + + Args: + args (dict): The arguments dictionary from the API request + """ + + def process_value(value): + """Convert Python values to string format for environment variables""" + if value is None: + return "" + if isinstance(value, bool): + return str(value).lower() + if isinstance(value, (list, dict)): + return str(value) + return str(value) + + def set_env_vars(data, prefix=""): + """Recursively set environment variables from nested dictionary""" + for key, value in data.items(): + env_key = prefix + key.upper() + + # Handle special cases + if isinstance(value, dict): + # For nested dictionaries (like special_tokens) + set_env_vars(value, f"{env_key}_") + elif isinstance(value, list): + # Handle list of dictionaries (like datasets) + if value and isinstance(value[0], dict): + for i, item in enumerate(value): + set_env_vars(item, f"{env_key}_{i}_") + else: + # For simple lists (like lora_target_modules) + os.environ[env_key] = process_value(value) + else: + # Handle all other cases + os.environ[env_key] = process_value(value) + + # Clear any existing related environment variables + # This prevents old values from persisting + for key in list(os.environ.keys()): + if key.startswith( + ("BASE_MODEL", "MODEL_TYPE", "TOKENIZER_TYPE", "DATASET", "LORA_", "WANDB_") + ): + del os.environ[key] + + # Set new environment variables + set_env_vars(args) diff --git a/.runpod/tests.json b/.runpod/tests.json new file mode 100644 index 0000000000..1d1e0287b5 --- /dev/null +++ b/.runpod/tests.json @@ -0,0 +1,85 @@ +{ + "input": { + "name": "quick_smoke_test_sft", + "user_id": "user", + "model_id": "llama-test", + "run_id": "llama-test", + "credentials": { + "wandb_api_key": "", + "hf_token": "" + }, + "args": { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "model_type": "AutoModelForCausalLM", + "tokenizer_type": "AutoTokenizer", + "load_in_8bit": true, + "load_in_4bit": false, + "strict": false, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca" + } + ], + "val_set_size": 0.05, + "output_dir": "./outputs/lora-out", + "sequence_len": 4096, + "sample_packing": true, + "eval_sample_packing": false, + "pad_to_sequence_len": true, + "adapter": "lora", + "lora_r": 32, + "lora_alpha": 64, + "lora_dropout": 0.05, + "lora_target_linear": true, + "lora_modules_to_save": [ + "embed_tokens", + "lm_head" + ], + "gradient_accumulation_steps": 4, + "micro_batch_size": 2, + "num_epochs": 1, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "learning_rate": 0.0002, + "train_on_inputs": false, + "group_by_length": false, + "bf16": "auto", + "tf32": true, + "gradient_checkpointing": true, + "logging_steps": 1, + "flash_attention": true, + "warmup_steps": 1, + "evals_per_epoch": 1, + "eval_max_new_tokens": 128, + "saves_per_epoch": 1, + "weight_decay": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>" + } + }, + "timeout": 100000 + }, + "config": { + "gpuTypeId": "NVIDIA GeForce RTX 4090", + "gpuCount": 1, + "containerDiskInGb": 200, + "env": [ + { + "key": "TOKENIZER", + "value": "" + }, + { + "key": "DISABLE_LOG_STATS", + "value": "true" + } + ], + "allowedCudaVersions": [ + "12.8", + "12.7", + "12.6", + "12.5", + "12.4" + ] + } +} From 170cdb5be94809b871d2f5aea2352b7d94966308 Mon Sep 17 00:00:00 2001 From: divyanshuaggarwal Date: Mon, 28 Apr 2025 19:40:28 +0530 Subject: [PATCH 0587/1405] Add Post_model_load, post_lora_load, post_train, post_train_unload function calls (#2539) * Update train.py add post_model_load and post_lora_load model calss. * Update train.py add post_train and post_train_unload function calls * Update train.py * Update base.py * Update train.py * chore: lint * clarify plugin hooks * Update src/axolotl/integrations/base.py Co-authored-by: Dan Saunders * Update src/axolotl/utils/models.py Co-authored-by: Dan Saunders * Update src/axolotl/utils/models.py Co-authored-by: Dan Saunders * Update src/axolotl/integrations/base.py Co-authored-by: Dan Saunders * Update models.py * Update models.py * remove extra call to post_model_load * chore: lint * add test for hooks and gc trainer * disable duplicated code check for test * fix the path and add better handling --------- Co-authored-by: Wing Lian Co-authored-by: Dan Saunders --- src/axolotl/cli/train.py | 4 + src/axolotl/integrations/base.py | 40 +++++- src/axolotl/train.py | 4 + src/axolotl/utils/models.py | 19 ++- tests/e2e/integrations/test_hooks.py | 184 +++++++++++++++++++++++++++ tests/e2e/test_lora_llama.py | 4 +- 6 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 tests/e2e/integrations/test_hooks.py diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index e225141b63..4f258313d2 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -1,5 +1,6 @@ """CLI to run training on a model.""" +import gc import logging import os from pathlib import Path @@ -48,8 +49,11 @@ def do_train(cfg: DictDefault, cli_args: TrainerCliArgs): dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) model, tokenizer, trainer = train(cfg=cfg, dataset_meta=dataset_meta) + del model, tokenizer, trainer + gc.collect() + plugin_manager = PluginManager.get_instance() plugin_manager.post_train_unload(cfg) diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 11015e31ae..cb65f96dd0 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -36,9 +36,10 @@ class BasePlugin: Methods: register(cfg): Registers the plugin with the given configuration. pre_model_load(cfg): Performs actions before the model is loaded. - post_model_load(cfg, model): Performs actions after the model is loaded. + post_model_build(cfg, model): Performs actions after the model is loaded, but before LoRA adapters are applied. pre_lora_load(cfg, model): Performs actions before LoRA weights are loaded. post_lora_load(cfg, model): Performs actions after LoRA weights are loaded. + post_model_load(cfg, model): Performs actions after the model is loaded, inclusive of any adapters. create_optimizer(cfg, trainer): Creates and returns an optimizer for training. create_lr_scheduler(cfg, trainer, optimizer): Creates and returns a learning rate scheduler. add_callbacks_pre_trainer(cfg, model): Adds callbacks to the trainer before training. @@ -77,6 +78,14 @@ def pre_model_load(self, cfg): # pylint: disable=unused-argument None """ + def post_model_build(self, cfg, model): # pylint: disable=unused-argument + """ + Performs actions after the model is built/loaded, but before any adapters are applied. + + Args: + cfg (dict): The configuration for the plugin. + """ + def post_model_load(self, cfg, model): # pylint: disable=unused-argument """ Performs actions after the model is loaded. @@ -329,9 +338,22 @@ def pre_model_load(self, cfg): for plugin in self.plugins.values(): plugin.pre_model_load(cfg) + def post_model_build(self, cfg, model): + """ + Calls the post_model_build method of all registered plugins after the model has been built/loaded, + but before any adapters have been applied. + + Args: + cfg (dict): The configuration for the plugins. + model (object): The loaded model. + """ + for plugin in self.plugins.values(): + plugin.post_model_build(cfg, model) + def post_model_load(self, cfg, model): """ - Calls the post_model_load method of all registered plugins. + Calls the post_model_load method of all registered plugins after the model has been loaded + inclusive of any adapters Parameters: cfg (dict): The configuration for the plugins. @@ -458,6 +480,20 @@ def add_callbacks_post_trainer(self, cfg, trainer): callbacks.extend(plugin_callbacks) return callbacks + def post_train(self, cfg, model): + """ + Calls the post_train method of all registered plugins. + + Parameters: + cfg (dict): The configuration for the plugins. + model (object): The loaded model. + + Returns: + None + """ + for plugin in self.plugins.values(): + plugin.post_train(cfg, model) + def post_train_unload(self, cfg): """ Calls the post_train_unload method of all registered plugins. diff --git a/src/axolotl/train.py b/src/axolotl/train.py index d116ea4fd9..7896239de1 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -29,6 +29,7 @@ from axolotl.core.trainers.mixins.sequence_parallel import ( SequenceParallelContextManager, ) +from axolotl.integrations.base import PluginManager from axolotl.logging_config import configure_logging from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import cleanup_distributed @@ -533,4 +534,7 @@ def train( if not cfg.use_ray: cleanup_distributed() + plugin_manager = PluginManager.get_instance() + plugin_manager.post_train(cfg, model) + return model, tokenizer, trainer diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index d7105daba5..ab4cc19bb5 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -53,6 +53,7 @@ ) from axolotl.common.architectures import MOE_ARCH_BLOCK +from axolotl.integrations.base import PluginManager from axolotl.models.mamba import fix_mamba_attn_for_loss from axolotl.monkeypatch.multipack import ( SUPPORTED_MULTIPACK_MODEL_TYPES, @@ -74,6 +75,7 @@ from axolotl.utils.model_shard_quant import load_sharded_model, load_sharded_model_quant LOG = logging.getLogger(__name__) +PLUGIN_MANAGER = PluginManager.get_instance() MULTIMODAL_AUTO_MODEL_MAPPING = { "mllama": MllamaForConditionalGeneration, @@ -571,10 +573,8 @@ def apply_patches(self) -> None: patch_gemma3conditionalgeneration_forward() # load any patches from plugins - from axolotl.integrations.base import PluginManager - plugin_manager = PluginManager.get_instance() - plugin_manager.pre_model_load(self.cfg) + PLUGIN_MANAGER.pre_model_load(self.cfg) # monkey patch to allow additional Accelerator init kwargs if self.cfg.fp8: @@ -1252,6 +1252,7 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: try: skip_move_to_device = self.build_model(qlora_fsdp) + PLUGIN_MANAGER.post_model_build(self.cfg, self.model) except Exception as err: # pylint: disable=broad-exception-caught LOG.exception(err) raise err @@ -1331,6 +1332,8 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: before_kbit_train_or_finetune=False, ) + PLUGIN_MANAGER.pre_lora_load(self.cfg, self.model) + # --------------------------------------------------------- # load lora or adapter # --------------------------------------------------------- @@ -1392,7 +1395,7 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: gc.collect() torch.cuda.empty_cache() - # TODO resume_from_checkpoint handling + PLUGIN_MANAGER.post_model_load(self.cfg, self.model) return self.model, lora_config @@ -1427,9 +1430,13 @@ def load_adapter(model, cfg, adapter, inference=False): if hasattr(model, "enable_input_require_grads"): model.enable_input_require_grads() if adapter in ["lora", "qlora"]: - return load_lora(model, cfg, inference=inference) + model, lora_config = load_lora(model, cfg, inference=inference) + PLUGIN_MANAGER.post_lora_load(cfg, model) + return model, lora_config if adapter == "llama-adapter": - return load_llama_adapter(model, cfg) + model, lora_config = load_llama_adapter(model, cfg) + PLUGIN_MANAGER.post_lora_load(cfg, model) + return model, lora_config raise NotImplementedError(f"{adapter} peft adapter not available") diff --git a/tests/e2e/integrations/test_hooks.py b/tests/e2e/integrations/test_hooks.py new file mode 100644 index 0000000000..e51334dfe8 --- /dev/null +++ b/tests/e2e/integrations/test_hooks.py @@ -0,0 +1,184 @@ +""" +e2e tests to make sure all the hooks are fired on the plugin +""" + +import os +from pathlib import Path + +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets +from axolotl.integrations.base import BasePlugin +from axolotl.train import train +from axolotl.utils.config import normalize_config, prepare_plugins, validate_config +from axolotl.utils.dict import DictDefault + +from ..utils import check_model_output_exists + + +class LogHooksPlugin(BasePlugin): + """ + fixture to capture in a log file each hook that was fired + """ + + base_dir = Path("/tmp/axolotl-log-hooks") + + def __init__(self): + self.base_dir.mkdir(parents=True, exist_ok=True) + try: + os.remove(self.base_dir.joinpath("plugin_hooks.log")) + except FileNotFoundError: + pass + + def pre_model_load(self, cfg): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("pre_model_load\n") + + def post_model_build(self, cfg, model): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("post_model_build\n") + + def pre_lora_load(self, cfg, model): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("pre_lora_load\n") + + def post_lora_load(self, cfg, model): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("post_lora_load\n") + + def post_model_load(self, cfg, model): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("post_model_load\n") + + def create_optimizer(self, cfg, trainer): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("create_optimizer\n") + + def get_trainer_cls(self, cfg): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("get_trainer_cls\n") + + def create_lr_scheduler( + self, cfg, trainer, optimizer + ): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("create_lr_scheduler\n") + + def add_callbacks_pre_trainer(self, cfg, model): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("add_callbacks_pre_trainer\n") + return [] + + def add_callbacks_post_trainer( + self, cfg, trainer + ): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("add_callbacks_post_trainer\n") + return [] + + def post_train(self, cfg, model): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("post_train\n") + + def post_train_unload(self, cfg): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("post_train_unload\n") + + +class TestPluginHooks: + """ + e2e tests to make sure all the hooks are fired during the training + """ + + def test_plugin_hooks(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "plugins": [ + "tests.e2e.integrations.test_hooks.LogHooksPlugin", + ], + "tokenizer_type": "AutoTokenizer", + "sequence_len": 1024, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.02, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "max_steps": 5, + "flash_attention": True, + "bf16": "auto", + } + ) + + cfg = validate_config(cfg) + prepare_plugins(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) + + with open( + "/tmp/axolotl-log-hooks" + "/plugin_hooks.log", "r", encoding="utf-8" + ) as f: + file_contents = f.readlines() + file_contents = "\n".join(file_contents) + assert "pre_model_load" in file_contents + assert "post_model_build" in file_contents + assert "pre_lora_load" in file_contents + assert "post_lora_load" in file_contents + assert "post_model_load" in file_contents + # assert "create_optimizer" in file_contents # not implemented yet + assert "get_trainer_cls" in file_contents + # assert "create_lr_scheduler" in file_contents # not implemented yet + assert "add_callbacks_pre_trainer" in file_contents + assert "add_callbacks_post_trainer" in file_contents + assert "post_train" in file_contents + # assert "post_train_unload" in file_contents # not called from test train call + + try: + os.remove("/tmp/axolotl-log-hooks" + "/plugin_hooks.log") + except FileNotFoundError: + pass diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index e5a734b332..b02fe3d447 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -48,13 +48,13 @@ def test_lora(self, temp_dir): }, ], "num_epochs": 1, - "micro_batch_size": 8, + "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 20, + "max_steps": 5, } ) From 5000cb3fe76b53128a374ad16a44630e0101b506 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 28 Apr 2025 10:11:06 -0400 Subject: [PATCH 0588/1405] grab sys prompt too from dataset (#2397) [skip ci] * grab sys prompt too from dataset * chore: add field_system to docs --------- Co-authored-by: NanoCode012 --- docs/config.qmd | 4 ++++ src/axolotl/prompt_strategies/chat_template.py | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/docs/config.qmd b/docs/config.qmd index a677344980..cb39e1d54d 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -154,6 +154,10 @@ datasets: # Key containing the messages (default: "messages") field_messages: messages + # Key containing the system message (default: "system") + # If the system message is not present in the dataset sample, it will be loaded from the field_system property. + field_system: system + # Mapping of properties from the input dataset to the chat template. # (default: message_property_mappings={'role':'role', 'content':'content'}) # If a property exists in the template but not in this mapping, the system will attempt diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 918c563296..d16eb34e18 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -33,6 +33,7 @@ def __init__( message_field_training: Optional[str] = None, message_field_training_detail: Optional[str] = None, field_messages: str = "messages", + field_system: str = "system", roles: Optional[Dict[str, List[str]]] = None, drop_system_message: bool = False, ): @@ -62,6 +63,7 @@ def __init__( self.message_field_training = message_field_training self.message_field_training_detail = message_field_training_detail self.field_messages = field_messages + self.field_system = field_system self.tokenizer = tokenizer self.processor: Optional[ProcessorMixin] = processor self.chat_template = chat_template @@ -488,6 +490,17 @@ def find_turn(self, turns: list[dict], turn_idx: int): def get_conversation_thread(self, prompt): turns = [] + + possible_sys_turn = self.transform_message( + prompt[self.prompter.field_messages][0] + ) + if ( + possible_sys_turn["role"] != "system" + and self.prompter.field_system in prompt + ): + turn = {"role": "system", "content": prompt[self.prompter.field_system]} + turns.append(turn) + for message in prompt[self.prompter.field_messages]: transformed_message = self.transform_message(message) From 7099343c56e0c48932c198f030e877a46970f0b9 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 28 Apr 2025 21:11:20 +0700 Subject: [PATCH 0589/1405] feat: add eos_tokens and train_on_eot for chat_template EOT parsing (#2364) * feat: add eos_tokens and train_on_eot for chat_template EOT parsing * fix: comments * chore: add some examples of tokens * feat: add new potential errors for chat_template to faq * feat: add examples for EOT handling * fix: change error to warning for missing EOS * fix: warning typo * feat: add tests for eot token handling * fix: remove broken caplog capture in test * fix: chattemplate strategy with kd missing eot changes --- docs/config.qmd | 27 +- docs/dataset-formats/conversation.qmd | 75 +++- docs/faq.qmd | 34 +- src/axolotl/integrations/kd/chat_template.py | 4 + .../prompt_strategies/chat_template.py | 161 +++++++-- src/axolotl/utils/schemas/config.py | 1 + .../test_chat_templates_advanced.py | 323 +++++++++++++++++- 7 files changed, 575 insertions(+), 50 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index cb39e1d54d..8795fa4ab8 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -187,7 +187,7 @@ datasets: # IMPORTANT: The following fields determine which parts of the conversation to train on. # Priority order: message_field_training > message_field_training_detail > train_on_inputs or role in roles_to_train # See examples at `docs/dataset-formats/conversation.qmd` - # Note: If the below 4 fields are set to empty, defaults to training only on the last message. + # Note: If the below 5 fields are empty, defaults to training only on the last message. # Optional[List[str]]. Roles to train on. The tokens from these roles will be considered for the loss. roles_to_train: ["assistant"] # default @@ -196,7 +196,13 @@ datasets: # - turn (default): train on the EOS token at the end of each trainable turn # - last: train on the last EOS token in the conversation # TIP: Please make sure that your `tokenizer.eos_token` is same as EOS/EOT token in template. Otherwise, set `eos_token` under `special_tokens`. - train_on_eos: last + train_on_eos: turn + # Optional[str]. Which EOT (End-of-Turn) tokens to train on in the conversation. Possible values are: + # - all: train on all EOT tokens + # - turn: train on the EOT token at the end of each trainable turn + # - last: train on the last EOT token in the conversation + # If not specified, defaults to the value of train_on_eos for backward compatibility. + train_on_eot: # The key in the message turn that indicates via boolean whether tokens of a turn should be considered for training. Useful to selectively train on certain turns besides the `roles_to_train`. message_field_training: training # The key in the message turn that contains the training details. Useful to selectively train on certain tokens in a turn. @@ -279,8 +285,17 @@ process_reward_model: chat_template: tokenizer_default # custom jinja template for chat template. This will be only used if chat_template is set to `jinja` or `null` (in which case chat_template is automatically set to `jinja`). Default is null. chat_template_jinja: null -# Changes the default system message. Currently only supports chatml. -default_system_message: You are a helpful assistant. Please give a long and detailed answer. +# Optional[List[str]]. Custom EOT (End-of-Turn) tokens to mask/unmask during training. +# These tokens mark the boundaries between conversation turns. +# For example: ["/INST", "
", "[/SYSTEM_PROMPT]"] +# If not specified, defaults to just the model's eos_token. +# This is useful for templates that use multiple delimiter tokens. +eot_tokens: + # - "
" + # - "[/INST]" + # - "[/SYSTEM_PROMPT]" +# Changes the default system message +default_system_message: You are a helpful assistant. Please give a long and detailed answer. # Currently only supports chatml. # Axolotl attempts to save the dataset as an arrow after packing the data together so # subsequent training attempts load faster, relative path dataset_prepared_path: data/last_run_prepared @@ -665,8 +680,10 @@ special_tokens: # unk_token: "" # pad_token: "[PAD]" -# Add extra tokens. +# Optional[list[str]]. Add extra tokens to the tokenizer. tokens: + # - "<|startoftext|>" + # - "<|endoftext|>" # Mapping token_id to new_token_string to override reserved added_tokens in the tokenizer. # Only works for tokens that are not part of the base vocab (aka are added_tokens). diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 81c902afdc..ee9f7391b8 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -4,18 +4,6 @@ description: Conversation format for supervised fine-tuning. order: 3 --- -## sharegpt - -::: {.callout-important} -ShareGPT is deprecated!. Please see [chat_template](#chat_template) section below. -::: - -## pygmalion - -```{.json filename="data.jsonl"} -{"conversations": [{"role": "...", "value": "..."}]} -``` - ## chat_template Chat Template strategy uses a jinja2 template that converts a list of messages into a prompt. Support using tokenizer's template, a supported template, or custom jinja2. @@ -64,7 +52,7 @@ We recommend checking the below examples for other usecases. ### Examples -1. Using the default chat template in the tokenizer_config.json on OpenAI messages format, training on only last message. +1. (Legacy) Using the default chat template in the tokenizer_config.json on OpenAI messages format, training on only last message. ```yaml datasets: @@ -109,10 +97,55 @@ datasets: ``` ::: {.callout-important} -Please make sure that your `tokenizer.eos_token` is same as EOS/EOT token in template. Otherwise, set `eos_token` under `special_tokens`. +Please make sure that your `tokenizer.eos_token` is same as EOS (End-of-Sequence) token in template. Otherwise, set `eos_token` under `special_tokens: `. +::: + +5. If you are using a template that has a different EOT (End-of-Turn) token from EOS token or multiple EOT tokens (like Mistral V7 Tekken), set the `eot_tokens: ` config. The handling of EOT tokens follows `train_on_eos: ` which defaults to turn. + +```yaml +eot_tokens: + - "[/INST]" + # - "[/SYSTEM_PROMPT]" + +datasets: + - path: ... + type: chat_template + + # optional + train_on_eot: turn # defaults read from train_on_eos (which defaults to turn) +``` + +::: {.callout-tip} +See [config documentation](../config.qmd) for detailed explanations of "turn", "last", and "all" options for training on tokens. +::: + +::: {.callout-note} +Using `eot_tokens` requires each token that exists in `chat_template` to be a single token in the tokenizer. Otherwise, the tokenizer will split the token and cause unexpected behavior. + +You can add those tokens as new tokens under `tokens: ` or (recommended) override unused added_tokens via `added_tokens_overrides: `. See [config](../config.qmd) for more details. +::: + +6. Continuing from the previous example, if you want to train on all EOT token trainable turns but only last EOS token, set `train_on_eos: last`. + +```yaml +eot_tokens: + - "[/INST]" + # ... + +datasets: + - path: ... + type: chat_template + + train_on_eos: last + train_on_eot: turn +``` + +::: {.callout-tip} +If EOS token only appears at the end of a prompt, `train_on_eos: last` is equivalent to `train_on_eos: turn`. Therefore, generally, you can leave them to their defaults and omit them. ::: -5. (Advanced) Using fine-grained control over tokens and turns to train in a conversation + +7. (Advanced) Using fine-grained control over tokens and turns to train in a conversation For a data sample that looks like: @@ -162,3 +195,15 @@ datasets: ::: {.callout-tip} It is not necessary to set both `message_field_training` and `message_field_training_detail` at once. ::: + +## sharegpt + +::: {.callout-important} +ShareGPT is deprecated!. Please see [chat_template](#chat_template) section. +::: + +## pygmalion + +```{.json filename="data.jsonl"} +{"conversations": [{"role": "...", "value": "..."}]} +``` diff --git a/docs/faq.qmd b/docs/faq.qmd index 664359cb81..f586099e78 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -73,10 +73,40 @@ description: Frequently asked questions > A: This is likely an empty turn. -**Q: The EOS/EOT token is incorrectly being masked or not being masked.** +**Q: The EOS token is incorrectly being masked or not being masked / `EOS token __ not found in chat template`.** -> A: This is because of the mismatch between `tokenizer.eos_token` and EOS/EOT token in template. Please make sure to set `eos_token` under `special_tokens` to the same EOS/EOT token as in template. +> A: There can be two reasons: + +> 1. This is because of the mismatch between `tokenizer.eos_token` and EOS token in template. Please make sure to set `eos_token: ` under `special_tokens: ` to the same EOS token as in template. + +> 2. The EOS token is not in the template. Please check if your template is correct. As an example, `phi_35` template does not use its dedicated EOS token `<|endoftext|>` at the end. **Q: "`chat_template` choice is `tokenizer_default` but tokenizer's `chat_template` is null. Please add a `chat_template` in tokenizer config"** > A: This is because the tokenizer does not have a chat template. Please add a chat template in the tokenizer config. See [chat_template](dataset-formats/conversation.qmd#chat-template) for more details. + +**Q: The EOT token(s) are incorrectly being masked or not being masked / `EOT token __ not found in chat template`.** + +> A: There can be two reasons: + +> 1. The EOT token is different from the EOS token and was not specified under `eot_tokens: `. Please set `eot_tokens: ` to the same EOT token(s) as in template. + +> 2. There is more than one EOT token per turn in the template. Please raise an issue with examples as we recognize this as an edge case. + +**Q: `EOT token encoding failed. Please check if the token is valid and can be encoded.`** + +> A: There could be some issue with the tokenizer or unicode encoding. Please raise an issue with examples with the EOT token & tokenizer causing the issue. + +**Q: `EOT token __ is encoded as multiple tokens.`** + +> A: This is because the EOT token is encoded as multiple tokens which can cause unexpected behavior. Please add it under `tokens: ` or (recommended) override unused added_tokens via `added_tokens_overrides: `. + +**Q: `Conflict between train_on_eos and train_on_eot. eos_token is in eot_tokens and train_on_eos != train_on_eot`** + +> A: This is because the EOS token is in the `eot_tokens: ` while mismatch between `train_on_eos: ` and `train_on_eot: `. This will cause one to override the other. Please ensure that `train_on_eos: ` and `train_on_eot: ` are the same or remove the EOS token from `eot_tokens: `. + +**Q: If `eot_tokens: ` is not provided, what happens?** + +> A: If `eot_tokens: ` is not provided, the default behavior is the same as before. EOS tokens used to delimit turns are masked/unmasked depending on whether the turn is trainable. + +> Internally, `eot_tokens: tokenizer.eos_token` and `train_on_eot: train_on_eos` (which defaults to `turn`). This transition helps clarify the naming and behavior of EOT/EOS tokens. diff --git a/src/axolotl/integrations/kd/chat_template.py b/src/axolotl/integrations/kd/chat_template.py index 699728e9f4..131570aea7 100644 --- a/src/axolotl/integrations/kd/chat_template.py +++ b/src/axolotl/integrations/kd/chat_template.py @@ -35,6 +35,8 @@ def __init__( sequence_len, roles_to_train=None, train_on_eos=None, + train_on_eot=None, + eot_tokens=None, logprobs_field="logprobs", gen_temperature=1.0, kd_temperature=1.0, @@ -50,6 +52,8 @@ def __init__( sequence_len, roles_to_train=roles_to_train, train_on_eos=train_on_eos, + train_on_eot=train_on_eot, + eot_tokens=eot_tokens, ) @property diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index d16eb34e18..076ddac1fe 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -222,10 +222,12 @@ def __init__( self, prompter: "ChatTemplatePrompter", tokenizer, - train_on_inputs, - sequence_len, - roles_to_train=None, - train_on_eos=None, + train_on_inputs: bool, + sequence_len: int, + roles_to_train: Optional[List[str]] = None, + train_on_eos: Optional[str] = None, + train_on_eot: Optional[str] = None, + eot_tokens: Optional[List[str]] = None, ): super().__init__(prompter, tokenizer, train_on_inputs, sequence_len) self.prompter: ChatTemplatePrompter = prompter @@ -238,12 +240,87 @@ def __init__( ] self.train_on_eos = train_on_eos + # Backward compatibility, load from train_on_eos + self.train_on_eot = train_on_eot if train_on_eot is not None else train_on_eos + + # Default to eos_token if eot_tokens not provided + self.eot_tokens = ( + eot_tokens if eot_tokens is not None else [self.tokenizer.eos_token] + ) + self.images = "images" LOG.debug( f"The chat template uses the following properites on the message: {self.prompter.chat_template_msg_variables}" ) + self._validate_eot_and_eos_tokens() + + def _validate_eot_and_eos_tokens(self): + """ + - Validates that EOT tokens (or eos_token) are in the chat_template + - Checks if EOT tokens are encoded as multiple tokens in the tokenizer. + - Checks for potential conflicts between train_on_eos and train_on_eot. + """ + if self.prompter.chat_template is None: + # Usually this should not happen + LOG.warning( + "No chat template provided, skipping EOT and EOS token validation" + ) + return + + # If the EOT token is the same as the EOS token, we need to check differently + if len(self.eot_tokens) == 1 and self.eot_tokens[0] == self.tokenizer.eos_token: + # Check if the eos_token is in the chat_template or as a variable `eos_token` + # Note: we check for `eos_token` in the string, but it could possibly not be a variable + if ( + self.tokenizer.eos_token not in self.prompter.chat_template + and "eos_token" not in self.prompter.chat_template + ): + LOG.warning( + f"EOS token '{self.tokenizer.eos_token}' not found in chat_template. Please check if your template/EOS token is correct." + ) + return + + # Create a new list to store tokens that should be kept + valid_eot_tokens = [] + for token in self.eot_tokens: + # Check if EOT token is in the chat_template + if token not in self.prompter.chat_template: + LOG.warning(f"EOT token '{token}' not found in chat_template.") + # Don't add to the valid tokens list + continue + + valid_eot_tokens.append(token) + + # Replace the original list with the filtered one + self.eot_tokens = valid_eot_tokens + + for token in self.eot_tokens: + # If token in template, check if EOT token is in tokenizer and not encoded as multiple tokens + token_ids = self.tokenizer.encode(token, add_special_tokens=False) + if not token_ids: + raise ValueError( + "EOT token encoding failed. Please check if the token is valid and can be encoded." + ) + if token_ids and len(token_ids) > 1: + raise ValueError( + f"EOT token '{token}' is encoded as multiple tokens: {token_ids}. Please add it under `tokens: ` in the config " + "or (recommended) override unused added_tokens via `added_tokens_overrides: `." + ) + + # If eos_token is in eot_tokens and conflict between train_on_eos and train_on_eot, raise an error + if ( + self.tokenizer.eos_token in self.eot_tokens + and self.train_on_eos != self.train_on_eot + ): + raise ValueError( + "Conflict between train_on_eos and train_on_eot. eos_token is in eot_tokens and train_on_eos != train_on_eot" + f"train_on_eos: {self.train_on_eos}, train_on_eot: {self.train_on_eot}" + f"eot_tokens: {self.eot_tokens}" + f"eos_token: {self.tokenizer.eos_token}" + ) + @property def supports_batched(self) -> bool: # Let calling code know we can handle lists of examples @@ -287,6 +364,7 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: if ( not self.roles_to_train and not self.train_on_eos + and not self.train_on_eot and not self.prompter.message_field_training # type: ignore and not self.prompter.message_field_training_detail # type: ignore ): @@ -322,6 +400,7 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: labels = [IGNORE_TOKEN_ID] * len(input_ids) last_eos_idx = -1 + last_eot_idx = -1 for index, turn in enumerate(turns): role = turn.get("role") content = turn.get("content") @@ -370,25 +449,46 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: LOG.debug(f"Labels after processing turn {index}: {labels}") - # Handle EOS token - eos_idx = self.find_first_eos_token(input_ids, start_idx=turn_end_idx) - if abs(eos_idx - turn_end_idx) <= 3: # Allow for some template padding - last_eos_idx = eos_idx - if self.train_on_eos == "all" or ( - self.train_on_eos == "turn" and should_train - ): - labels[eos_idx] = input_ids[eos_idx] - LOG.debug(f"EOS token set for training at index {eos_idx}") - else: + # Handle special tokens (EOT and EOS) + for token_type, find_func, train_option in [ + ("EOT", self.find_first_eot_token, self.train_on_eot), + ("EOS", self.find_first_eos_token, self.train_on_eos), + ]: + token_idx = find_func(input_ids, start_idx=turn_end_idx) + + if ( + token_idx != -1 and abs(token_idx - turn_end_idx) <= 3 + ): # Allow for some template padding + # Update the last token index + if token_type == "EOT": # nosec B105 + last_eot_idx = token_idx + else: + last_eos_idx = token_idx + + # Set labels if needed for this turn + if train_option == "all" or ( + train_option == "turn" and should_train + ): + labels[token_idx] = input_ids[token_idx] + LOG.debug( + f"{token_type} token set for training at index {token_idx}" + ) + else: + LOG.debug( + f"{token_type} token missing after turn {turn}. {token_type.lower()}_idx: {token_idx}, turn_end_idx: {turn_end_idx}" + ) + + # Handle 'last' option for special tokens + for token_type, last_idx, train_option in [ + ("EOT", last_eot_idx, self.train_on_eot), + ("EOS", last_eos_idx, self.train_on_eos), + ]: + if train_option == "last" and last_idx != -1: + labels[last_idx] = input_ids[last_idx] LOG.debug( - f"EOS token missing after turn {turn}. eos_idx: {eos_idx}, turn_end_idx: {turn_end_idx}" + f"Last {token_type} token set for training at index {last_idx}" ) - # Handle 'last' option for train_on_eos - if self.train_on_eos == "last" and last_eos_idx != -1: - labels[last_eos_idx] = input_ids[last_eos_idx] - LOG.debug(f"Last EOS token set for training at index {last_eos_idx}") - LOG.debug(f"Final labels: {labels}") return { @@ -404,6 +504,25 @@ def find_first_eos_token(self, input_ids, start_idx): return i return -1 + def find_first_eot_token(self, input_ids, start_idx): + """Find the first EOT token in the input_ids starting from start_idx.""" + # Get token IDs for all EOT tokens + eot_token_ids = [] + for token in self.eot_tokens: + token_ids = self.tokenizer.encode(token, add_special_tokens=False) + if len(token_ids) != 1: + raise ValueError( + f"EOT token '{token}' is encoded as multiple tokens: {token_ids}. Please add it under `tokens: ` in the config." + ) + + eot_token_ids.append(token_ids[0]) # Use the last token ID if multiple + + # Search for any of the EOT token IDs + for i in range(start_idx, len(input_ids)): + if input_ids[i] in eot_token_ids: + return i + return -1 + def find_turn(self, turns: list[dict], turn_idx: int): """ Locate the starting and ending indices of the specified turn in a conversation. @@ -568,6 +687,8 @@ def _get_strategy_params(self, cfg, ds_cfg: Dict[str, Any]): "sequence_len": cfg.sequence_len, "roles_to_train": ds_cfg.get("roles_to_train", ["assistant"]), "train_on_eos": ds_cfg.get("train_on_eos", "turn"), + "train_on_eot": ds_cfg.get("train_on_eot", None), + "eot_tokens": cfg.get("eot_tokens", None), # loads from cfg, not ds_cfg } def __call__( diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 2e0a6027c7..36c18fd3c8 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -309,6 +309,7 @@ class AxolotlInputConfig( | Annotated[str, StringConstraints(pattern="^tokenizer_default_fallback_")] ) | None = None chat_template_jinja: str | None = None + eot_tokens: list[str] | None = None default_system_message: str | None = None fix_untrained_tokens: int | list[int] | None = None diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index ce55b871f5..38a5b6c432 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -2,6 +2,8 @@ tests for chat_template prompt strategy """ +# pylint: disable=too-many-lines + import logging from copy import deepcopy @@ -53,14 +55,6 @@ class TestChatTemplateConfigurations: Test class for various configurations of ChatTemplateStrategy. """ - @staticmethod - def find_sublist(full_list, sub_list): - token_count = len(sub_list) - for index in range(len(full_list) - token_count + 1): - if full_list[index : index + token_count] == sub_list: - return index - return -1 - @staticmethod def setup_tokenizer( tokenizer_name, @@ -68,6 +62,7 @@ def setup_tokenizer( chat_template_jinja=None, eos_token=None, request=None, + eot_token=None, ) -> tuple[PreTrainedTokenizer, str]: """ Helper function to set up the tokenizer and chat template for the test. @@ -88,6 +83,10 @@ def setup_tokenizer( "CodeLlamaTokenizerFast", ): tokenizer.update_post_processor() + + if eot_token: + tokenizer.add_special_tokens({"additional_special_tokens": [eot_token]}) + return tokenizer, chat_template_jinja def _should_skip_turn(self, tokenizer, turn, turn_idx, start_idx, end_idx): @@ -974,3 +973,311 @@ def test_get_chat_template_variables( raise ValueError( f"Unsupported chat template: {chat_template} with {chat_template_jinja}" ) + + def test_eot_tokens_conflict_with_eos_token( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, # pylint: disable=unused-argument + request, + ): + """Test that an error is raised when eot_tokens contains eos_token and train_on_eot/train_on_eos conflict""" + LOG.info( + "Testing conflict between eot_tokens containing eos_token and train_on_eot/train_on_eos mismatch" + ) + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + + # Create a situation where eot_tokens contains eos_token + eot_tokens = [ + tokenizer.eos_token, + "[/INST]", + ] # Deliberately including eos_token + + # Create conflicting train_on_eos and train_on_eot settings + with pytest.raises( + ValueError, + match=".*eos_token is in eot_tokens and train_on_eos != train_on_eot.*", + ): + ChatTemplateStrategy( + ChatTemplatePrompter( + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", + ), + tokenizer=tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + train_on_eos="none", # Setting to none + train_on_eot="turn", # Different from train_on_eos + eot_tokens=eot_tokens, + ) + + def test_eot_token_backward_compatibility( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, # pylint: disable=unused-argument + request, + ): + """Test that eot_tokens inherits from eos_token when not specified""" + LOG.info("Testing backward compatibility that eot_token inherits eos_token") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", + ), + tokenizer=tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + train_on_eos="turn", # Setting train_on_eos to "turn" + ) + + # In backward compatibility mode, eot_tokens should be derived from eos_token + assert strategy.eot_tokens == [ + tokenizer.eos_token + ], f"Expected eot_tokens to inherit from eos_token, got {strategy.eot_tokens}" + assert ( + strategy.train_on_eot == "turn" + ), f"Expected train_on_eot to inherit from train_on_eos, got {strategy.train_on_eot}" + + def test_token_not_in_template( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, + request, + ): + """Test runs even when tokens are not found in the template""" + LOG.info("Testing runs even when tokens are not found in template") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + + # Create a non-existent token that definitely won't be in the template + non_existent_token = "[DEFINITELY_NOT_IN_TEMPLATE]" + tokenizer.add_special_tokens( + {"additional_special_tokens": [non_existent_token]} + ) + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", + ), + tokenizer=tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + eot_tokens=[non_existent_token], + ) + + # Force template check by calling tokenize_prompt + strategy.tokenize_prompt(basic_dataset[0]) + + # We can also check that a warning was logged, but there's + # caplog conflicts when running with other tests + # assert any( + # "not found in chat_template" in record.message for record in self._caplog.records + # ), "Expected warning about token not found in template was not logged" + + def test_custom_eot_tokens( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, # pylint: disable=unused-argument + basic_dataset, + request, + ): + """Test with custom EOT tokens to ensure proper masking and training""" + LOG.info("Testing with custom EOT tokens") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, None, request + ) + + # Add custom EOT tokens to the tokenizer + custom_eot = "[EOT]" + tokenizer.add_special_tokens({"additional_special_tokens": [custom_eot]}) + + # Create a custom chat template that uses our EOT token + custom_template = """{% for message in messages %}{% if message['role'] == 'system' %}{{ message['content'] }}{% elif message['role'] == 'user' %}User: {{ message['content'] }}{% elif message['role'] == 'assistant' %}Assistant: {{ message['content'] }}[EOT]{% endif %}{% endfor %}""" + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + tokenizer, + chat_template=custom_template, + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", + ), + tokenizer=tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + train_on_eot="turn", # Train on EOT token after each turn + eot_tokens=[custom_eot], + ) + + res = strategy.tokenize_prompt(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Find indices of the EOT token + eot_token_id = tokenizer.convert_tokens_to_ids(custom_eot) + eot_indices = [ + i for i, token_id in enumerate(input_ids) if token_id == eot_token_id + ] + + assert len(eot_indices) > 0, "Expected at least one EOT token in the input" + + # Verify labeling for EOT tokens based on role + turns = strategy.get_conversation_thread(basic_dataset[0]) + assistant_turn_indices = [] + non_assistant_turn_indices = [] + + for i, turn in enumerate(basic_dataset[0]["conversations"]): + start_idx, end_idx = strategy.find_turn(turns=turns, turn_idx=i) + if start_idx != -1 and end_idx != -1: # If turn is found + if turn["from"] == "assistant": + assistant_turn_indices.append((start_idx, end_idx)) + else: + non_assistant_turn_indices.append((start_idx, end_idx)) + + # Check EOT tokens after assistant turns are labeled + for eot_idx in eot_indices: + is_after_assistant = any( + start_idx <= eot_idx <= end_idx + 1 # +1 to include the EOT token + for start_idx, end_idx in assistant_turn_indices + ) + + if is_after_assistant: + assert ( + labels[eot_idx] != IGNORE_TOKEN_ID + ), f"Expected EOT token after assistant turn at index {eot_idx} to be labeled" + else: + assert ( + labels[eot_idx] == IGNORE_TOKEN_ID + ), f"Expected EOT token not after assistant turn at index {eot_idx} to not be labeled" + + def test_multiple_train_on_eot_settings( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + basic_dataset, + request, + ): + """Test different train_on_eot settings""" + LOG.info("Testing different train_on_eot settings") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + + # Create a list to test different train_on_eot settings + test_settings = [ + ("none", lambda idx, is_assistant: False), # Never train on EOT + ("all", lambda idx, is_assistant: True), # Always train on EOT + ( + "turn", + lambda idx, is_assistant: is_assistant, + ), # Train on EOT after assistant turns + ("last", lambda idx, is_last: is_last), # Only train on last EOT + ] + + for setting, expected_train_func in test_settings: + LOG.info(f"Testing train_on_eot='{setting}'") + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_property_mappings={"role": "from", "content": "value"}, + field_messages="conversations", + ), + tokenizer=tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + train_on_eot=setting, + eot_tokens=[ + tokenizer.eos_token + ], # Use eos_token as the EOT token for simplicity + ) + + res = strategy.tokenize_prompt(basic_dataset[0]) + turns = strategy.get_conversation_thread(basic_dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + eos_token_id = tokenizer.eos_token_id + eos_indices = [ + i for i, token_id in enumerate(input_ids) if token_id == eos_token_id + ] + + assert ( + len(eos_indices) > 0 + ), "Expected at least one EOS/EOT token in the input" + + # Check labeling for each EOS/EOT token + for idx, eos_idx in enumerate(eos_indices): + # Find which turn this EOS token belongs to + preceding_turn = None + for i, turn in enumerate(basic_dataset[0]["conversations"]): + start_idx, end_idx = strategy.find_turn(turns=turns, turn_idx=i) + if ( + start_idx != -1 + and end_idx != -1 + and start_idx <= eos_idx <= end_idx + 1 + ): + preceding_turn = turn + break + + is_assistant = ( + preceding_turn is not None and preceding_turn["from"] == "assistant" + ) + is_last = idx == len(eos_indices) - 1 + + expected_label = not expected_train_func( + idx, is_assistant if setting != "last" else is_last + ) + + if expected_label: + assert ( + labels[eos_idx] == IGNORE_TOKEN_ID + ), f"Expected EOT token at index {eos_idx} to not be labeled with train_on_eot='{setting}'" + else: + assert ( + labels[eos_idx] != IGNORE_TOKEN_ID + ), f"Expected EOT token at index {eos_idx} to be labeled with train_on_eot='{setting}'" From dda95e6c406670024be592aaf372551dd09ba70f Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 28 Apr 2025 11:20:46 -0400 Subject: [PATCH 0590/1405] add preview-docs workflow (#2432) * add preview-docs workflow * update preview-docs workflow * use correct publish-dir * install deps prior to docs build * use correct publish-dir * use quarto publish with netlify target * adding _publish.yml * fix * fix * fix * remove unused file * fix naming --------- Co-authored-by: Dan Saunders --- .github/workflows/preview-docs.yml | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/preview-docs.yml diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml new file mode 100644 index 0000000000..dbf8d9a000 --- /dev/null +++ b/.github/workflows/preview-docs.yml @@ -0,0 +1,55 @@ +name: Preview +on: + workflow_dispatch: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + checks: write + contents: write + deployments: write + issues: write + discussions: write + pages: write + pull-requests: write + statuses: write + +jobs: + preview: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Quarto + uses: quarto-dev/quarto-actions/setup@v2 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python3 -m pip install jupyter quartodoc + python3 -m pip install -e . --no-deps + + - name: Build autodoc + run: quartodoc build + + - name: Quarto render + run: quarto render + + - name: Netlify Publish + uses: nwtgck/actions-netlify@v3.0 + with: + publish-dir: './_site' + enable-pull-request-comment: true + enable-github-deployment: true + github-token: ${{ secrets.GITHUB_TOKEN }} + deploy-message: "Deployed On Netlify" + github-deployment-environment: 'preview' + github-deployment-description: 'Preview Deployment' + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} From c513487d1aa71fe9753d4e1ee8ac3de6c8af8e19 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 28 Apr 2025 12:12:15 -0400 Subject: [PATCH 0591/1405] support val_set_size for splitting test split from train with DPO (#2572) --- src/axolotl/core/trainers/dpo/trainer.py | 142 ++++++++++++++++++++++- src/axolotl/utils/data/rl.py | 32 ++++- 2 files changed, 170 insertions(+), 4 deletions(-) diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index 89c77dca4a..3520aff109 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -3,15 +3,29 @@ """ import gc +import random from functools import wraps -from typing import Any, Dict, Union +from typing import Any, Dict, Optional, Union +import pandas as pd import torch +import wandb +from accelerate import PartialState +from datasets import Dataset, IterableDataset from peft.optimizers import create_loraplus_optimizer from torch import nn -from transformers import Trainer +from torch.utils.data import DataLoader +from transformers import ( + BaseImageProcessor, + FeatureExtractionMixin, + PreTrainedTokenizerBase, + ProcessorMixin, + Trainer, +) +from transformers.trainer_utils import EvalLoopOutput from transformers.utils import is_sagemaker_mp_enabled -from trl import DPOTrainer +from trl import DPOConfig, DPOTrainer, maybe_apply_chat_template, maybe_extract_prompt +from trl.trainer.utils import log_table_to_comet_experiment from axolotl.core.trainers.mixins import RngLoaderMixin, SchedulerMixin from axolotl.core.trainers.utils import ( @@ -81,6 +95,64 @@ def push_to_hub(self, *args, **kwargs) -> str: return super().push_to_hub(*args, **kwargs) + # TODO: remove this once https://github.com/huggingface/trl/pull/3377 is in a release + def _prepare_dataset( + self, + dataset: Union[Dataset, IterableDataset], + processing_class: Union[ + PreTrainedTokenizerBase, + BaseImageProcessor, + FeatureExtractionMixin, + ProcessorMixin, + ], + args: DPOConfig, + dataset_name: str, + ) -> Union[Dataset, IterableDataset]: + # Build the kwargs for the `map` function + map_kwargs: Dict[str, Any] = {"writer_batch_size": 10} + if isinstance(dataset, Dataset): # IterableDataset does not support num_proc + map_kwargs["num_proc"] = args.dataset_num_proc + + with PartialState().main_process_first(): + # Extract prompt if needed + if isinstance( + dataset, Dataset + ): # `IterableDataset.map` does not support `desc` + map_kwargs["desc"] = f"Extracting prompt in {dataset_name} dataset" + dataset = dataset.map(maybe_extract_prompt, **map_kwargs) + + # Apply the chat template if needed + if isinstance( + dataset, Dataset + ): # `IterableDataset.map` does not support `desc` + map_kwargs["desc"] = f"Applying chat template to {dataset_name} dataset" + dataset = dataset.map( + maybe_apply_chat_template, + fn_kwargs={"tokenizer": processing_class, "tools": args.tools}, + **map_kwargs, + ) + + # Tokenize the dataset + if isinstance( + dataset, Dataset + ): # `IterableDataset.map` does not support `desc` + map_kwargs["desc"] = f"Tokenizing {dataset_name} dataset" + + dataset = dataset.map( + self.tokenize_row if not self.is_vision_model else self.process_row, + remove_columns=["chosen", "rejected"], + fn_kwargs={ + "processing_class": processing_class, + "max_prompt_length": args.max_prompt_length, + "max_completion_length": args.max_completion_length, + # for enc-dec, we add the special tokens ([bos_token] + prompt + [eos_token]; completion + [eos_token]) + "add_special_tokens": False, + }, + **map_kwargs, + ) + + return dataset + @staticmethod def tokenize_row( features, @@ -124,3 +196,67 @@ def training_step( gc.collect() torch.cuda.empty_cache() return loss + + # TODO: remove this once https://github.com/huggingface/trl/pull/3377 is in a release + def evaluation_loop( + self, + dataloader: DataLoader, + description: str, + prediction_loss_only: Optional[bool] = None, + ignore_keys: Optional[list[str]] = None, + metric_key_prefix: str = "eval", + ) -> EvalLoopOutput: + """ + Overriding built-in evaluation loop to store metrics for each batch. + Prediction/evaluation loop, shared by `Trainer.evaluate()` and `Trainer.predict()`. + + Works both with or without labels. + """ + + # Sample and save to game log if requested (for one batch to save time) + if self.generate_during_eval: + # Generate random indices within the range of the total number of samples + num_samples = len(dataloader.dataset) + random_indices = random.sample( + range(num_samples), k=self.args.eval_batch_size + ) + + # Use dataloader.dataset.select to get the random batch without iterating over the DataLoader + random_batch_dataset = dataloader.dataset.select(random_indices) + random_batch = self.data_collator(random_batch_dataset) + random_batch = self._prepare_inputs(random_batch) + + policy_output_decoded, ref_output_decoded = ( + self.generate_from_model_and_ref(self.model, random_batch) + ) + + table = pd.DataFrame( + columns=["Prompt", "Policy", "Ref Model"], + data=[ + [prompt, pol[len(prompt) :], ref[len(prompt) :]] + for prompt, pol, ref in zip( + random_batch_dataset["prompt"], + policy_output_decoded, + ref_output_decoded, + ) + ], + ) + if "wandb" in self.args.report_to and self.accelerator.is_main_process: + wandb.log({"game_log": wandb.Table(data=table)}) + + if "comet_ml" in self.args.report_to: + log_table_to_comet_experiment( + name="game_log.csv", + table=table, + ) + + # Base evaluation + initial_output = super().evaluation_loop( + dataloader, + description, + prediction_loss_only, + ignore_keys, + metric_key_prefix, + ) + + return initial_output diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index 4c7b712924..135de61a30 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -204,7 +204,37 @@ def load_split(dataset_cfgs, _cfg): else: eval_dataset = load_split(cfg.test_datasets, cfg) if not eval_dataset: - eval_dataset = None + if cfg.val_set_size: + # ensure we end up with the same fingerprint by doing rank0 first and being able to cache + to_hash_train = ( + train_dataset._fingerprint # pylint: disable=protected-access + + "|" + + str(cfg.val_set_size) + + "|" + + "train" + + "|" + + str(cfg.seed or 42) + ) + to_hash_test = ( + train_dataset._fingerprint # pylint: disable=protected-access + + "|" + + str(cfg.val_set_size) + + "|" + + "test" + + "|" + + str(cfg.seed or 42) + ) + train_fingerprint = md5(to_hash_train) + test_fingerprint = md5(to_hash_test) + ds_w_test_split = train_dataset.train_test_split( + test_size=cfg.val_set_size, + seed=cfg.seed, + shuffle=False, + train_new_fingerprint=train_fingerprint, + test_new_fingerprint=test_fingerprint, + ) + eval_dataset = ds_w_test_split["test"] + train_dataset = ds_w_test_split["train"] if not train_is_preprocessed: _save_preprocessed_ds(cfg, cfg.datasets, train_dataset) From 1178a15ede612df0fdfb6dc65cb274c94a57e713 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 28 Apr 2025 23:18:46 +0700 Subject: [PATCH 0592/1405] Feat: Add qwen3 and CCE for qwen family (#2518) --- examples/qwen3/qlora-fsdp.yaml | 68 +++++ .../integrations/cut_cross_entropy/README.md | 7 +- .../cut_cross_entropy/monkeypatch/llama.py | 174 ++++++++++++ .../cut_cross_entropy/monkeypatch/patch.py | 26 +- .../cut_cross_entropy/monkeypatch/qwen2.py | 37 +++ .../monkeypatch/qwen2_5_vl.py | 246 +++++++++++++++++ .../monkeypatch/qwen2_moe.py | 188 +++++++++++++ .../cut_cross_entropy/monkeypatch/qwen2_vl.py | 249 ++++++++++++++++++ .../cut_cross_entropy/monkeypatch/qwen3.py | 35 +++ .../monkeypatch/qwen3_moe.py | 194 ++++++++++++++ 10 files changed, 1221 insertions(+), 3 deletions(-) create mode 100644 examples/qwen3/qlora-fsdp.yaml create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_5_vl.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_moe.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_vl.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3.py create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3_moe.py diff --git a/examples/qwen3/qlora-fsdp.yaml b/examples/qwen3/qlora-fsdp.yaml new file mode 100644 index 0000000000..dc3377b4f2 --- /dev/null +++ b/examples/qwen3/qlora-fsdp.yaml @@ -0,0 +1,68 @@ +base_model: Qwen/Qwen3-8B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: true +strict: false + +datasets: + - path: tatsu-lab/alpaca + type: alpaca +dataset_prepared_path: +val_set_size: 0.05 +output_dir: ./outputs/out + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: true +pad_to_sequence_len: true + +adapter: qlora +lora_model_dir: +lora_r: 32 +lora_alpha: 64 +lora_dropout: 0.05 +lora_target_linear: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_steps: 10 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_limit_all_gathers: true + fsdp_sync_module_states: true + fsdp_offload_params: true + fsdp_use_orig_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: Qwen3DecoderLayer + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD +special_tokens: diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 462bcbedc8..627ebd9358 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -32,8 +32,8 @@ plugins: ## Supported Models - llama -- llama4_text - llama4 +- llama4_text - mllama - phi3 - gemma @@ -43,6 +43,11 @@ plugins: - mistral - mistral3 - qwen2 +- qwen2_moe +- qwen2_vl +- qwen2_5_vl +- qwen3 +- qwen3_moe - cohere - cohere2 - glm diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama.py new file mode 100644 index 0000000000..42ab996b9c --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama.py @@ -0,0 +1,174 @@ +"""Llama CCE patch. Adapted from transformers v4.51.2""" + +# pylint: disable=duplicate-code + + +from types import MethodType +from typing import Optional, Union + +import torch +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, + apply_lce, +) +from transformers.cache_utils import Cache +from transformers.modeling_outputs import ( + BaseModelOutputWithPast, + CausalLMOutputWithPast, +) +from transformers.models.llama.modeling_llama import ( + _CONFIG_FOR_DOC, + LLAMA_INPUTS_DOCSTRING, + KwargsForCausalLM, +) +from transformers.processing_utils import Unpack +from transformers.utils import ( + add_start_docstrings_to_model_forward, + replace_return_docstrings, +) +from transformers.utils.deprecation import deprecate_kwarg +from transformers.utils.generic import can_return_tuple + +_PATCH_OPTS: PatchOptions | None = None + + +@can_return_tuple +@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") +@add_start_docstrings_to_model_forward(LLAMA_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def cce_forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs: Unpack[KwargsForCausalLM], +) -> CausalLMOutputWithPast: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + + Example: + + ```python + >>> from transformers import AutoTokenizer, LlamaForCausalLM + + >>> model = LlamaForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf") + >>> tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf") + + >>> prompt = "Hey, are you conscious? Can you talk to me?" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." + ```""" + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs: BaseModelOutputWithPast = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs.last_hidden_state + if hidden_states is None: + raise ValueError("hidden_states is None") + + loss = None + logits = None + + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + loss = apply_lce( + hidden_states[:, slice_indices, :], + self.lm_head.weight, + labels, + _PATCH_OPTS, + **kwargs, + ) + else: + logits = self.lm_head(hidden_states[:, slice_indices, :]) + + if labels is not None: + loss = self.loss_function( + logits=logits, + labels=labels, + vocab_size=self.config.vocab_size, + **kwargs, + ) + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +def patch_llama( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + """Patch Llama for CCE.""" + global _PATCH_OPTS # pylint: disable=global-statement + from transformers.models.llama import modeling_llama + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_llama.LlamaForCausalLM + ), f"Expected a LlamaForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward, maybe_model) + return maybe_model + + modeling_llama.LlamaForCausalLM.forward = cce_forward + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py index 9e18c6b0b6..8176a1f0c7 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py @@ -5,9 +5,7 @@ import transformers from cut_cross_entropy.cce_utils import LinearCrossEntropyImpl from cut_cross_entropy.linear_cross_entropy import LCE_IMPL_DEFAULT -from cut_cross_entropy.transformers.llama import patch_llama from cut_cross_entropy.transformers.phi3 import patch_phi3 -from cut_cross_entropy.transformers.qwen2 import patch_qwen2 from cut_cross_entropy.transformers.utils import PatchOptions, TransformersModelT from axolotl.integrations.cut_cross_entropy.monkeypatch.cohere import ( @@ -24,6 +22,9 @@ patch_glm, patch_glm4, ) +from axolotl.integrations.cut_cross_entropy.monkeypatch.llama import ( + patch_llama, +) from axolotl.integrations.cut_cross_entropy.monkeypatch.llama4 import ( patch_llama4, patch_llama4_text, @@ -33,6 +34,22 @@ patch_mistral3, ) from axolotl.integrations.cut_cross_entropy.monkeypatch.mllama import patch_mllama +from axolotl.integrations.cut_cross_entropy.monkeypatch.qwen2 import ( + patch_qwen2, +) +from axolotl.integrations.cut_cross_entropy.monkeypatch.qwen2_5_vl import ( + patch_qwen2_5_vl, +) +from axolotl.integrations.cut_cross_entropy.monkeypatch.qwen2_moe import ( + patch_qwen2_moe, +) +from axolotl.integrations.cut_cross_entropy.monkeypatch.qwen2_vl import ( + patch_qwen2_vl, +) +from axolotl.integrations.cut_cross_entropy.monkeypatch.qwen3 import patch_qwen3 +from axolotl.integrations.cut_cross_entropy.monkeypatch.qwen3_moe import ( + patch_qwen3_moe, +) CUT_CROSS_ENTROPY_MODEL_MAPPING = { "llama": patch_llama, @@ -47,6 +64,11 @@ "mistral": patch_mistral, "mistral3": patch_mistral3, "qwen2": patch_qwen2, + "qwen2_moe": patch_qwen2_moe, + "qwen2_vl": patch_qwen2_vl, + "qwen2_5_vl": patch_qwen2_5_vl, + "qwen3": patch_qwen3, + "qwen3_moe": patch_qwen3_moe, "cohere": patch_cohere, "cohere2": patch_cohere2, "glm": patch_glm, diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2.py new file mode 100644 index 0000000000..3f6d2b3e9b --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2.py @@ -0,0 +1,37 @@ +"""Qwen2 CCE patch. The model inherits Llama's modeling code and uses the same forward method.""" + +# pylint: disable=duplicate-code + +from types import MethodType + +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, +) + + +def patch_qwen2( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + from transformers.models.qwen2 import modeling_qwen2 + + # Set the _PATCH_OPTS in the llama patch file + import axolotl.integrations.cut_cross_entropy.monkeypatch.llama as llama_patch + + llama_patch._PATCH_OPTS = patch_options # pylint: disable=protected-access + + from axolotl.integrations.cut_cross_entropy.monkeypatch.llama import ( + cce_forward, + ) + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_qwen2.Qwen2ForCausalLM + ), f"Expected a Qwen2ForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward, maybe_model) + return maybe_model + + modeling_qwen2.Qwen2ForCausalLM.forward = cce_forward + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_5_vl.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_5_vl.py new file mode 100644 index 0000000000..16206006fc --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_5_vl.py @@ -0,0 +1,246 @@ +"""Qwen2.5 VL CCE patch. Adapted from transformers v4.51.2""" + +# pylint: disable=duplicate-code + + +from types import MethodType +from typing import Optional, Tuple, Union + +import torch +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, + apply_lce, +) +from torch.nn import CrossEntropyLoss +from transformers.models.qwen2_5_vl.modeling_qwen2_5_vl import ( + Qwen2_5_VLCausalLMOutputWithPast, +) + +_PATCH_OPTS: PatchOptions | None = None + + +def cce_forward_multimodal( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[list[torch.FloatTensor]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + pixel_values: Optional[torch.Tensor] = None, + pixel_values_videos: Optional[torch.FloatTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + video_grid_thw: Optional[torch.LongTensor] = None, + rope_deltas: Optional[torch.LongTensor] = None, + cache_position: Optional[torch.LongTensor] = None, + second_per_grid_ts: Optional[torch.Tensor] = None, +) -> Union[Tuple, Qwen2_5_VLCausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + Returns: + + Example: + + ```python + >>> from PIL import Image + >>> import requests + >>> from transformers import AutoProcessor, Qwen2_5_VLForConditionalGeneration + + >>> model = Qwen2_5_VLForConditionalGeneration.from_pretrained("Qwen/Qwen2.5-VL-7B-Instruct") + >>> processor = AutoProcessor.from_pretrained("Qwen/Qwen2.5-VL-7B-Instruct") + + >>> messages = [ + { + "role": "user", + "content": [ + {"type": "image"}, + {"type": "text", "text": "What is shown in this image?"}, + ], + }, + ] + >>> url = "https://www.ilankelman.org/stopsigns/australia.jpg" + >>> image = Image.open(requests.get(url, stream=True).raw) + + >>> text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) + >>> inputs = processor(text=[text], images=[image], vision_infos=[vision_infos]) + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "The image shows a street scene with a red stop sign in the foreground. In the background, there is a large red gate with Chinese characters ..." + ```""" + + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + if inputs_embeds is None: + inputs_embeds = self.model.embed_tokens(input_ids) + if pixel_values is not None: + pixel_values = pixel_values.type(self.visual.dtype) + image_embeds = self.visual(pixel_values, grid_thw=image_grid_thw) + n_image_tokens = (input_ids == self.config.image_token_id).sum().item() + n_image_features = image_embeds.shape[0] + if n_image_tokens != n_image_features: + raise ValueError( + f"Image features and image tokens do not match: tokens: {n_image_tokens}, features {n_image_features}" + ) + + mask = input_ids == self.config.image_token_id + mask_unsqueezed = mask.unsqueeze(-1) + mask_expanded = mask_unsqueezed.expand_as(inputs_embeds) + image_mask = mask_expanded.to(inputs_embeds.device) + + image_embeds = image_embeds.to(inputs_embeds.device, inputs_embeds.dtype) + inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds) # type: ignore + + if pixel_values_videos is not None: + pixel_values_videos = pixel_values_videos.type(self.visual.dtype) + video_embeds = self.visual(pixel_values_videos, grid_thw=video_grid_thw) + n_video_tokens = (input_ids == self.config.video_token_id).sum().item() + n_video_features = video_embeds.shape[0] + if n_video_tokens != n_video_features: + raise ValueError( + f"Video features and video tokens do not match: tokens: {n_video_tokens}, features {n_video_features}" + ) + + mask = input_ids == self.config.video_token_id + mask_unsqueezed = mask.unsqueeze(-1) + mask_expanded = mask_unsqueezed.expand_as(inputs_embeds) + video_mask = mask_expanded.to(inputs_embeds.device) + + video_embeds = video_embeds.to(inputs_embeds.device, inputs_embeds.dtype) + inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds) # type: ignore + + if attention_mask is not None: + attention_mask = attention_mask.to(inputs_embeds.device) + + # if we get 4D attention mask we cannot calculate rope deltas anymore. TODO @raushan fixme + if position_ids is None and (attention_mask is None or attention_mask.ndim == 2): + # calculate RoPE index once per generation in the pre-fill stage only + if ( + (cache_position is not None and cache_position[0] == 0) + or self.rope_deltas is None + or (past_key_values is None or past_key_values.get_seq_length() == 0) # type: ignore + ): + position_ids, rope_deltas = self.get_rope_index( + input_ids, + image_grid_thw, + video_grid_thw, + second_per_grid_ts, + attention_mask, + ) + self.rope_deltas = rope_deltas + # then use the prev pre-calculated rope-deltas to get the correct position ids + else: + batch_size, seq_length, _ = inputs_embeds.shape + delta = ( + (cache_position[0] + self.rope_deltas).to(inputs_embeds.device) + if cache_position is not None + else 0 + ) + position_ids = torch.arange(seq_length, device=inputs_embeds.device) # type: ignore + position_ids = position_ids.view(1, -1).expand(batch_size, -1) # type: ignore + if cache_position is not None: # otherwise `deltas` is an int `0` + delta = delta.repeat_interleave(batch_size // delta.shape[0], dim=0) # type: ignore + position_ids = position_ids.add(delta) # type: ignore + position_ids = position_ids.unsqueeze(0).expand(3, -1, -1) # type: ignore + + outputs = self.model( + input_ids=None, + position_ids=position_ids, + attention_mask=attention_mask, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + ) + + hidden_states = outputs[0] + logits = None + loss = None + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + loss = apply_lce( + hidden_states, + self.lm_head.weight, + labels, + _PATCH_OPTS, + ) + else: + logits = self.lm_head(hidden_states) + + if labels is not None: + # Upcast to float if we need to compute the loss to avoid potential precision issues + logits = logits.float() + # Shift so that tokens < n predict n + shift_logits = logits[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + # Flatten the tokens + loss_fct = CrossEntropyLoss() + shift_logits = shift_logits.view(-1, self.config.vocab_size) + shift_labels = shift_labels.view(-1) + # Enable model parallelism + shift_labels = shift_labels.to(shift_logits.device) + loss = loss_fct(shift_logits, shift_labels) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return Qwen2_5_VLCausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + rope_deltas=self.rope_deltas, + ) + + +def patch_qwen2_5_vl( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + + from transformers.models.qwen2_5_vl import modeling_qwen2_5_vl + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_qwen2_5_vl.Qwen2_5_VLForConditionalGeneration + ), f"Expected a Qwen2_5_VLForConditionalGeneration model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward_multimodal, maybe_model) + + return maybe_model + + modeling_qwen2_5_vl.Qwen2_5_VLForConditionalGeneration.forward = ( + cce_forward_multimodal + ) + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_moe.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_moe.py new file mode 100644 index 0000000000..0811bf55a6 --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_moe.py @@ -0,0 +1,188 @@ +"""Qwen2 MoE CCE patch. Adapted from transformers v4.51.2""" + +# pylint: disable=duplicate-code + +from types import MethodType +from typing import Optional, Union + +import torch +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, + apply_lce, +) +from transformers.models.qwen2_moe.modeling_qwen2_moe import ( + _CONFIG_FOR_DOC, + QWEN2MOE_INPUTS_DOCSTRING, + MoeCausalLMOutputWithPast, + MoeModelOutputWithPast, + load_balancing_loss_func, +) +from transformers.utils import ( + add_start_docstrings_to_model_forward, + replace_return_docstrings, +) +from transformers.utils.deprecation import deprecate_kwarg +from transformers.utils.generic import can_return_tuple + +_PATCH_OPTS: PatchOptions | None = None + + +@can_return_tuple +@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") +@add_start_docstrings_to_model_forward(QWEN2MOE_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=MoeCausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[list[torch.FloatTensor]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + output_router_logits: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **loss_kwargs, +) -> MoeCausalLMOutputWithPast: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + + Example: + + ```python + >>> from transformers import AutoTokenizer, Qwen2MoeForCausalLM + + >>> model = Qwen2MoeForCausalLM.from_pretrained(PATH_TO_CONVERTED_WEIGHTS) + >>> tokenizer = AutoTokenizer.from_pretrained(PATH_TO_CONVERTED_TOKENIZER) + + >>> prompt = "Hey, are you conscious? Can you talk to me?" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." + ```""" + + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_router_logits = ( + output_router_logits + if output_router_logits is not None + else self.config.output_router_logits + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs: MoeModelOutputWithPast = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + output_router_logits=output_router_logits, + cache_position=cache_position, + ) + + hidden_states = outputs.last_hidden_state + loss = None + logits = None + + if hidden_states is None: + raise ValueError("hidden_states is None") + + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + loss = apply_lce( + hidden_states[:, slice_indices, :], + self.lm_head.weight, + labels, + _PATCH_OPTS, + **loss_kwargs, + ) + else: + logits = self.lm_head(hidden_states[:, slice_indices, :]) + + if labels is not None: + loss = self.loss_function(logits, labels, self.vocab_size, **loss_kwargs) + + aux_loss = None + if output_router_logits: + aux_loss = load_balancing_loss_func( + outputs.router_logits, + self.num_experts, + self.num_experts_per_tok, + attention_mask, + ) + if labels is not None: + loss += self.router_aux_loss_coef * aux_loss.to( # type: ignore + loss.device # type: ignore + ) # make sure to reside in the same device + + return MoeCausalLMOutputWithPast( + loss=loss, + aux_loss=aux_loss, # type: ignore + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + router_logits=outputs.router_logits, + ) + + +def patch_qwen2_moe( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + + from transformers.models.qwen2_moe import modeling_qwen2_moe + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_qwen2_moe.Qwen2MoeForCausalLM + ), f"Expected a Qwen3MoeForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(forward, maybe_model) + + return maybe_model + + modeling_qwen2_moe.Qwen2MoeForCausalLM.forward = forward + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_vl.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_vl.py new file mode 100644 index 0000000000..250c3ab6bd --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_vl.py @@ -0,0 +1,249 @@ +"""Qwen2 VL CCE patch. Adapted from transformers v4.51.2""" + +# pylint: disable=duplicate-code + +from types import MethodType +from typing import Optional, Tuple, Union + +import torch +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, + apply_lce, +) +from torch.nn import CrossEntropyLoss +from transformers.models.qwen2_vl.modeling_qwen2_vl import ( + _CONFIG_FOR_DOC, + QWEN2_VL_INPUTS_DOCSTRING, + Qwen2VLCausalLMOutputWithPast, +) +from transformers.utils import ( + add_start_docstrings_to_model_forward, + replace_return_docstrings, +) + +_PATCH_OPTS: PatchOptions | None = None + + +@add_start_docstrings_to_model_forward(QWEN2_VL_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=Qwen2VLCausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def cce_forward_multimodal( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[list[torch.FloatTensor]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + pixel_values: Optional[torch.Tensor] = None, + pixel_values_videos: Optional[torch.FloatTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + video_grid_thw: Optional[torch.LongTensor] = None, + rope_deltas: Optional[torch.LongTensor] = None, + cache_position: Optional[torch.LongTensor] = None, +) -> Union[Tuple, Qwen2VLCausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + Returns: + + Example: + + ```python + >>> from PIL import Image + >>> import requests + >>> from transformers import AutoProcessor, Qwen2VLForConditionalGeneration + + >>> model = Qwen2VLForConditionalGeneration.from_pretrained("Qwen/Qwen2-VL-7B-Instruct") + >>> processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-7B-Instruct") + + >>> messages = [ + { + "role": "user", + "content": [ + {"type": "image"}, + {"type": "text", "text": "What is shown in this image?"}, + ], + }, + ] + >>> url = "https://www.ilankelman.org/stopsigns/australia.jpg" + >>> image = Image.open(requests.get(url, stream=True).raw) + + >>> text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) + >>> inputs = processor(text=[text], images=[image], vision_infos=[vision_infos]) + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "The image shows a street scene with a red stop sign in the foreground. In the background, there is a large red gate with Chinese characters ..." + ```""" + + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + if inputs_embeds is None: + inputs_embeds = self.model.embed_tokens(input_ids) + if pixel_values is not None: + pixel_values = pixel_values.type(self.visual.get_dtype()) + image_embeds = self.visual(pixel_values, grid_thw=image_grid_thw) + n_image_tokens = (input_ids == self.config.image_token_id).sum().item() + n_image_features = image_embeds.shape[0] + if n_image_tokens != n_image_features: + raise ValueError( + f"Image features and image tokens do not match: tokens: {n_image_tokens}, features {n_image_features}" + ) + image_mask = ( + (input_ids == self.config.image_token_id) + .unsqueeze(-1) + .expand_as(inputs_embeds) + .to(inputs_embeds.device) + ) + image_embeds = image_embeds.to(inputs_embeds.device, inputs_embeds.dtype) + inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds) # type: ignore + + if pixel_values_videos is not None: + pixel_values_videos = pixel_values_videos.type(self.visual.get_dtype()) + video_embeds = self.visual(pixel_values_videos, grid_thw=video_grid_thw) + n_video_tokens = (input_ids == self.config.video_token_id).sum().item() + n_video_features = video_embeds.shape[0] + if n_video_tokens != n_video_features: + raise ValueError( + f"Video features and video tokens do not match: tokens: {n_video_tokens}, features {n_video_features}" + ) + video_mask = ( + (input_ids == self.config.video_token_id) + .unsqueeze(-1) + .expand_as(inputs_embeds) + .to(inputs_embeds.device) + ) + video_embeds = video_embeds.to(inputs_embeds.device, inputs_embeds.dtype) + inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds) # type: ignore + + if attention_mask is not None: + attention_mask = attention_mask.to(inputs_embeds.device) + + # if we get 4D attention mask we cannot calculate rope deltas anymore. TODO @raushan fixme + if position_ids is None and (attention_mask is None or attention_mask.ndim == 2): + # calculate RoPE index once per generation in the pre-fill stage only + if ( + (cache_position is not None and cache_position[0] == 0) + or self.rope_deltas is None + or (past_key_values is None or past_key_values.get_seq_length() == 0) # type: ignore + ): + position_ids, rope_deltas = self.get_rope_index( + input_ids, image_grid_thw, video_grid_thw, attention_mask + ) + self.rope_deltas = rope_deltas + # then use the prev pre-calculated rope-deltas to get the correct position ids + else: + batch_size, seq_length, _ = inputs_embeds.shape + delta = ( + cache_position[0] + self.rope_deltas + if cache_position is not None + else 0 + ) + position_ids = torch.arange(seq_length, device=inputs_embeds.device) # type: ignore + position_ids = position_ids.view(1, -1).expand(batch_size, -1) # type: ignore + if cache_position is not None: # otherwise `deltas` is an int `0` + delta = delta.repeat_interleave(batch_size // delta.shape[0], dim=0) # type: ignore + delta = delta.to(position_ids.device) # type: ignore + position_ids = position_ids.add(delta) # type: ignore + position_ids = position_ids.unsqueeze(0).expand(3, -1, -1) # type: ignore + + outputs = self.model( + input_ids=None, + position_ids=position_ids, + attention_mask=attention_mask, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + ) + + hidden_states = outputs[0] + logits = None + loss = None + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + loss = apply_lce( + hidden_states, + self.lm_head.weight, + labels, + _PATCH_OPTS, + ) + else: + logits = self.lm_head(hidden_states) + + if labels is not None: + # Upcast to float if we need to compute the loss to avoid potential precision issues + logits = logits.float() + # Shift so that tokens < n predict n + shift_logits = logits[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + # Flatten the tokens + loss_fct = CrossEntropyLoss() + shift_logits = shift_logits.view(-1, self.config.vocab_size) + shift_labels = shift_labels.view(-1) + # Enable model parallelism + shift_labels = shift_labels.to(shift_logits.device) + loss = loss_fct(shift_logits, shift_labels) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return Qwen2VLCausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + rope_deltas=self.rope_deltas, + ) + + +def patch_qwen2_vl( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + + from transformers.models.qwen2_vl import modeling_qwen2_vl + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_qwen2_vl.Qwen2VLForConditionalGeneration + ), f"Expected a Qwen2VLForConditionalGeneration model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward_multimodal, maybe_model) + + return maybe_model + + modeling_qwen2_vl.Qwen2VLForConditionalGeneration.forward = cce_forward_multimodal + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3.py new file mode 100644 index 0000000000..799a4f357e --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3.py @@ -0,0 +1,35 @@ +"""Qwen3 CCE patch. The model inherits Llama's modeling code and uses the same forward method.""" + +# pylint: disable=duplicate-code + +from types import MethodType + +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, +) + + +def patch_qwen3( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + from transformers.models.qwen3 import modeling_qwen3 + + # Set the _PATCH_OPTS in the llama patch file + import axolotl.integrations.cut_cross_entropy.monkeypatch.llama as llama_patch + + llama_patch._PATCH_OPTS = patch_options # pylint: disable=protected-access + + from axolotl.integrations.cut_cross_entropy.monkeypatch.llama import cce_forward + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_qwen3.Qwen3ForCausalLM + ), f"Expected a Qwen3ForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(cce_forward, maybe_model) + return maybe_model + + modeling_qwen3.Qwen3ForCausalLM.forward = cce_forward + return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3_moe.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3_moe.py new file mode 100644 index 0000000000..c5cd76f944 --- /dev/null +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3_moe.py @@ -0,0 +1,194 @@ +"""Qwen3 MoE CCE patch. Adapted from transformers v4.51.2""" + +# pylint: disable=duplicate-code + +from types import MethodType +from typing import Optional, Union + +import torch +import transformers +from cut_cross_entropy.transformers.utils import ( + PatchOptions, + TransformersModelT, + apply_lce, +) +from transformers.modeling_outputs import CausalLMOutputWithPast +from transformers.models.qwen3_moe.modeling_qwen3_moe import ( + _CONFIG_FOR_DOC, + QWEN3_MOE_INPUTS_DOCSTRING, + KwargsForCausalLM, + MoeCausalLMOutputWithPast, + MoeModelOutputWithPast, + load_balancing_loss_func, +) +from transformers.processing_utils import Unpack +from transformers.utils import ( + add_start_docstrings_to_model_forward, + replace_return_docstrings, +) +from transformers.utils.deprecation import deprecate_kwarg +from transformers.utils.generic import can_return_tuple + +_PATCH_OPTS: PatchOptions | None = None + + +@can_return_tuple +@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") +@add_start_docstrings_to_model_forward(QWEN3_MOE_INPUTS_DOCSTRING) +@replace_return_docstrings( + output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC +) +def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[list[torch.FloatTensor]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + output_router_logits: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs: Unpack[KwargsForCausalLM], +) -> MoeCausalLMOutputWithPast: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + + Example: + + ```python + >>> from transformers import AutoTokenizer, Qwen3MoeForCausalLM + + >>> model = Qwen3MoeForCausalLM.from_pretrained("Qwen/Qwen3-MoE-15B-A2B") + >>> tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-MoE-15B-A2B") + + >>> prompt = "Hey, are you conscious? Can you talk to me?" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." + ```""" + + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_router_logits = ( + output_router_logits + if output_router_logits is not None + else self.config.output_router_logits + ) + + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs: MoeModelOutputWithPast = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + output_router_logits=output_router_logits, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs.last_hidden_state + + if hidden_states is None: + raise ValueError("hidden_states is None") + + loss = None + logits = None + + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + + if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): + assert labels is not None + loss = apply_lce( + hidden_states[:, slice_indices, :], + self.lm_head.weight, + labels, + _PATCH_OPTS, + **kwargs, + ) + else: + logits = self.lm_head(hidden_states[:, slice_indices, :]) + + if labels is not None: + loss = self.loss_function(logits, labels, self.vocab_size, **kwargs) + + aux_loss = None + if output_router_logits: + aux_loss = load_balancing_loss_func( + outputs.router_logits, + self.num_experts, + self.num_experts_per_tok, + attention_mask, + ) + if labels is not None: + loss += self.router_aux_loss_coef * aux_loss.to( # type: ignore + loss.device # type: ignore + ) # make sure to reside in the same device + + return MoeCausalLMOutputWithPast( + loss=loss, + aux_loss=aux_loss, # type: ignore + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + router_logits=outputs.router_logits, + ) + + +def patch_qwen3_moe( + maybe_model: TransformersModelT | str | transformers.PretrainedConfig, + patch_options: PatchOptions, +) -> TransformersModelT | None: + global _PATCH_OPTS # pylint: disable=global-statement + + from transformers.models.qwen3_moe import modeling_qwen3_moe + + _PATCH_OPTS = patch_options + + if isinstance(maybe_model, transformers.PreTrainedModel): + assert isinstance( + maybe_model, modeling_qwen3_moe.Qwen3MoeForCausalLM + ), f"Expected a Qwen3MoeForCausalLM model. Got {type(maybe_model)}." + maybe_model.forward = MethodType(forward, maybe_model) + + return maybe_model + + modeling_qwen3_moe.Qwen3MoeForCausalLM.forward = forward + return None From 63b17e3109654d7455ef4b554acf798e6f0657f5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 28 Apr 2025 15:09:41 -0400 Subject: [PATCH 0593/1405] chat template and example for qwen3 (#2577) --- examples/qwen3/32b-qlora.yaml | 69 +++++++++++++++++++++++++++++ src/axolotl/utils/chat_templates.py | 1 + src/axolotl/utils/schemas/enums.py | 1 + 3 files changed, 71 insertions(+) create mode 100644 examples/qwen3/32b-qlora.yaml diff --git a/examples/qwen3/32b-qlora.yaml b/examples/qwen3/32b-qlora.yaml new file mode 100644 index 0000000000..45a4395ac1 --- /dev/null +++ b/examples/qwen3/32b-qlora.yaml @@ -0,0 +1,69 @@ +base_model: Qwen/Qwen3-32B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +strict: false + +chat_template: qwen3 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.0 +output_dir: ./outputs/out +dataset_prepared_path: last_run_prepared + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: true +pad_to_sequence_len: true + +load_in_4bit: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - down_proj + - up_proj +lora_mlp_kernel: true +lora_qkv_kernel: true +lora_o_kernel: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: offload +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_steps: 10 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 5220abf844..fb21348528 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -35,6 +35,7 @@ "deepseek_v3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='', is_first_sp=true) %}{%- for message in messages %}{%- if message['role'] == 'system' %}{%- if ns.is_first_sp %}{% set ns.system_prompt = ns.system_prompt + message['content'] %}{% set ns.is_first_sp = false %}{%- else %}{% set ns.system_prompt = ns.system_prompt + '\\n\\n' + message['content'] %}{%- endif %}{%- endif %}{%- endfor %}{{ bos_token }}{{ ns.system_prompt }}{%- for message in messages %}{%- if message['role'] == 'user' %}{%- set ns.is_tool = false -%}{{'<|User|>' + message['content']}}{%- endif %}{%- if message['role'] == 'assistant' and 'tool_calls' in message %}{%- set ns.is_tool = false -%}{%- for tool in message['tool_calls'] %}{%- if not ns.is_first %}{%- if message['content'] is none %}{{'<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- else %}{{'<|Assistant|>' + message['content'] + '<|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- endif %}{%- set ns.is_first = true -%}{%- else %}{{'\\n' + '<|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- endif %}{%- endfor %}{{'<|tool▁calls▁end|><|end▁of▁sentence|>'}}{%- endif %}{%- if message['role'] == 'assistant' and 'tool_calls' not in message %}{%- if ns.is_tool %}{{'<|tool▁outputs▁end|>' + message['content'] + '<|end▁of▁sentence|>'}}{%- set ns.is_tool = false -%}{%- else %}{% set content = message['content'] %}{% if '' in content %}{% set content = content.split('')[-1] %}{% endif %}{{'<|Assistant|>' + content + '<|end▁of▁sentence|>'}}{%- endif %}{%- endif %}{%- if message['role'] == 'tool' %}{%- set ns.is_tool = true -%}{%- if ns.is_output_first %}{{'<|tool▁outputs▁begin|><|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- set ns.is_output_first = false %}{%- else %}{{'<|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- endif %}{%- endif %}{%- endfor -%}{% if ns.is_tool %}{{'<|tool▁outputs▁end|>'}}{% endif %}{% if add_generation_prompt and not ns.is_tool %}{{'<|Assistant|>'}}{% endif %}", "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', "qwen_25": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n", + "qwen3": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].role == 'system' %}\n {{- messages[0].content + '\\n\\n' }}\n {%- endif %}\n {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}\n {%- set index = (messages|length - 1) - loop.index0 %}\n {%- if ns.multi_step_tool and message.role == \"user\" and not(message.content.startswith('') and message.content.endswith('')) %}\n {%- set ns.multi_step_tool = false %}\n {%- set ns.last_query_index = index %}\n {%- endif %}\n{%- endfor %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set content = message.content %}\n {%- set reasoning_content = '' %}\n {%- if message.reasoning_content is defined and message.reasoning_content is not none %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if '' in message.content %}\n {%- set content = message.content.split('')[-1].lstrip('\\n') %}\n {%- set reasoning_content = message.content.split('')[0].rstrip('\\n').split('')[-1].lstrip('\\n') %}\n {%- endif %}\n {%- endif %}\n {%- if loop.index0 > ns.last_query_index %}\n {%- if loop.last or (not loop.last and reasoning_content) %}\n {{- '<|im_start|>' + message.role + '\\n\\n' + reasoning_content.strip('\\n') + '\\n\\n\\n' + content.lstrip('\\n') }}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- if message.tool_calls %}\n {%- for tool_call in message.tool_calls %}\n {%- if (loop.first and content) or (not loop.first) %}\n {{- '\\n' }}\n {%- endif %}\n {%- if tool_call.function %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {%- if tool_call.arguments is string %}\n {{- tool_call.arguments }}\n {%- else %}\n {{- tool_call.arguments | tojson }}\n {%- endif %}\n {{- '}\\n' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n {%- if enable_thinking is defined and enable_thinking is false %}\n {{- '\\n\\n\\n\\n' }}\n {%- endif %}\n{%- endif %}", "exaone": "{% for message in messages %}{% if loop.first and message['role'] != 'system' %}{{ '[|system|][|endofturn|]\n' }}{% endif %}{{ '[|' + message['role'] + '|]' + message['content'] }}{% if message['role'] == 'user' %}{{ '\n' }}{% else %}{{ '[|endofturn|]\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '[|assistant|]' }}{% endif %}", "metharme": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% else %}{% set loop_messages = messages %}{% set system_message = 'Enter RP mode. You shall reply to the user while staying in character. Your responses must be detailed, creative, immersive, and drive the scenario forward.' %}{% endif %}{{ '<|system|>' + system_message }}{% for message in loop_messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|user|>' + content.strip() }}{% elif message['role'] == 'assistant' %}{{ '<|model|>' + content.strip() }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|model|>' }}{% else %}{{ eos_token }}{% endif %}", "pixtral": '{%- if messages[0]["role"] == "system" %}\n {%- set system_message = messages[0]["content"] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set loop_messages = messages %}\n{%- endif %}\n\n{{- bos_token }}\n{%- for message in loop_messages %}\n {%- if (message[\'role\'] == \'user\') != (loop.index0 % 2 == 0) %}\n {{- raise_exception(\'After the optional system message, conversation roles must alternate user/assistant/user/assistant/...\') }}\n {%- endif %}\n {%- if message["role"] == "user" %}\n {%- if loop.last and system_message is defined %}\n {{- "[INST]" + system_message + "\n\n" }}\n {%- else %}\n {{- "[INST]" }}\n {%- endif %}\n {%- if message["content"] is not string %}\n {%- for chunk in message["content"] %}\n {%- if chunk["type"] == "text" %}\n {{- chunk["text"] }}\n {%- elif chunk["type"] == "image" %}\n {{- "[IMG]" }}\n {%- else %}\n {{- raise_exception("Unrecognized content type!") }}\n {%- endif %}\n {%- endfor %}\n {%- else %}\n {{- message["content"] }}\n {%- endif %}\n {{- "[/INST]" }}\n {%- elif message["role"] == "assistant" %}\n {%- if message["content"] is not string %}\n {%- for chunk in message["content"] %}\n {%- if chunk["type"] == "text" %}\n {{- chunk["text"] }}\n {%- elif chunk["type"] == "image" %}\n {{- "[IMG]" }}\n {%- else %}\n {{- raise_exception("Unrecognized content type!") }}\n{%- endif %}\n{%- endfor %}\n{{- eos_token }}\n{%- else %}\n{{- message["content"] + eos_token }}\n{%- endif %}\n {%- else %}\n {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }}\n {%- endif %}\n{%- endfor %}', diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index d8d9f2834c..118176d344 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -35,6 +35,7 @@ class ChatTemplate(str, Enum): jamba = "jamba" # pylint: disable=invalid-name jinja = "jinja" # pylint: disable=invalid-name qwen_25 = "qwen_25" # pylint: disable=invalid-name + qwen3 = "qwen3" # pylint: disable=invalid-name tokenizer_default = "tokenizer_default" # pylint: disable=invalid-name exaone = "exaone" # pylint: disable=invalid-name metharme = "metharme" # pylint: disable=invalid-name From 2d77165dc063155e6abf957e1bb077065b573927 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 28 Apr 2025 18:23:03 -0400 Subject: [PATCH 0594/1405] automatically split out reasoning trace from dataset (#2579) * automatically split out reasoning trace from dataset * chore: lint * fix import --- .../prompt_strategies/chat_template.py | 19 +++ src/axolotl/utils/schemas/datasets.py | 1 + tests/conftest.py | 6 + .../test_chat_templates_thinking.py | 118 ++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 tests/prompt_strategies/test_chat_templates_thinking.py diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 076ddac1fe..399bb378af 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -228,6 +228,7 @@ def __init__( train_on_eos: Optional[str] = None, train_on_eot: Optional[str] = None, eot_tokens: Optional[List[str]] = None, + split_thinking: Optional[bool] = False, ): super().__init__(prompter, tokenizer, train_on_inputs, sequence_len) self.prompter: ChatTemplatePrompter = prompter @@ -247,6 +248,7 @@ def __init__( self.eot_tokens = ( eot_tokens if eot_tokens is not None else [self.tokenizer.eos_token] ) + self.split_thinking = split_thinking self.images = "images" @@ -655,6 +657,22 @@ def transform_message(self, message): transformed_message["role"], transformed_message["role"] ) + # TODO handle reasoning_content with split_thinking + # if the role is assistant that we want to use reasoning_content + if self.split_thinking and transformed_message["role"] == "assistant": + content = transformed_message["content"] + pairs = [("", ""), ("", "")] + for pair in pairs: + if pair[0] in content and pair[1] in content: + start_idx = content.find(pair[0]) + end_idx = content.find(pair[1]) + thinking_content = content[start_idx + len(pair[0]) : end_idx] + transformed_message["reasoning_content"] = thinking_content.strip() + transformed_message["content"] = content[ + end_idx + len(pair[1]) : + ].lstrip() + break + # Determine which keys in the original message were not mapped mapped_values = set(self.prompter.message_property_mappings.values()) remaining_keys = set(message) - mapped_values @@ -689,6 +707,7 @@ def _get_strategy_params(self, cfg, ds_cfg: Dict[str, Any]): "train_on_eos": ds_cfg.get("train_on_eos", "turn"), "train_on_eot": ds_cfg.get("train_on_eot", None), "eot_tokens": cfg.get("eot_tokens", None), # loads from cfg, not ds_cfg + "split_thinking": ds_cfg.get("split_thinking", False), } def __call__( diff --git a/src/axolotl/utils/schemas/datasets.py b/src/axolotl/utils/schemas/datasets.py index f9b694da13..cc5d6daba2 100644 --- a/src/axolotl/utils/schemas/datasets.py +++ b/src/axolotl/utils/schemas/datasets.py @@ -50,6 +50,7 @@ class SFTDataset(BaseModel): message_property_mappings: dict[str, str] | None = None message_field_training: str | None = None message_field_training_detail: str | None = None + split_thinking: bool | None = None logprobs_field: str | None = None temperature: float | None = None roles_to_train: list[str] | None = None diff --git a/tests/conftest.py b/tests/conftest.py index 82e4e911bf..7fc9a62afa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -90,6 +90,12 @@ def download_qwen_2_5_half_billion_model(): snapshot_download_w_retry("Qwen/Qwen2.5-0.5B", repo_type="model") +@pytest.fixture(scope="session", autouse=True) +def download_qwen3_half_billion_model(): + # download the model + snapshot_download_w_retry("Qwen/Qwen3-0.6B", repo_type="model") + + @pytest.fixture(scope="session", autouse=True) def download_tatsu_lab_alpaca_dataset(): # download the dataset diff --git a/tests/prompt_strategies/test_chat_templates_thinking.py b/tests/prompt_strategies/test_chat_templates_thinking.py new file mode 100644 index 0000000000..236a7ec39f --- /dev/null +++ b/tests/prompt_strategies/test_chat_templates_thinking.py @@ -0,0 +1,118 @@ +""" +Tests for splitting reasoning/thinking from content into separate field +""" + +import logging + +import pytest +from datasets import Dataset +from transformers import AutoTokenizer + +from axolotl.prompt_strategies.chat_template import ( + load, +) +from axolotl.utils.dict import DictDefault + +from tests.hf_offline_utils import enable_hf_offline + +logging.basicConfig(level=logging.DEBUG) +LOG = logging.getLogger("axolotl") + + +@pytest.fixture(name="messages_w_reasoning") +def messages_w_reasoning_fixture(): + return Dataset.from_list( + [ + { + "messages": [ + { + "role": "user", + "content": "hello", + }, + { + "role": "assistant", + "content": "lorem\nwelcome", + }, + ] + } + ] + ) + + +@pytest.fixture(name="qwen3_tokenizer") +@enable_hf_offline +def qwen3_tokenizer_fixture( + download_qwen3_half_billion_model, +): # pylint: disable=unused-argument + tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B") + + return tokenizer + + +class TestSplitThinking: + """ + test class to make sure datasets with reasoning content conforms to the chat_template strategy + """ + + def test_splits_think(self, messages_w_reasoning, qwen3_tokenizer): + # pylint: disable=duplicate-code + strategy = load( + qwen3_tokenizer, + DictDefault( + { + "train_on_inputs": False, + "sequence_len": 512, + } + ), + DictDefault( + { + "chat_template": "qwen3", + "message_field_role": "role", + "message_field_content": "content", + "message_property_mappings": { + "role": "role", + "content": "content", + }, + "roles": { + "user": ["user"], + "assistant": ["assistant"], + "system": ["system"], + }, + "field_messages": "messages", + "split_thinking": True, + } + ), + ) + transformed_prompt = strategy.get_conversation_thread(messages_w_reasoning[0]) + assert transformed_prompt[0]["role"] == "user" + assert transformed_prompt[1]["role"] == "assistant" + assert transformed_prompt[1]["reasoning_content"] == "lorem" + assert transformed_prompt[1]["content"] == "welcome" + + res = strategy.tokenize_prompt(messages_w_reasoning[0]) + input_ids = res["input_ids"] + # fmt: off + expected_input_ids = [ + 151644, # im_start + 872, # user + 198, # \n + 14990, # hello + 151645, # im_end + 198, # \n + 151644, # im_start + 77091, # assistant + 198, # \n + 151667, # think + 198, # \n + 385, 1826, # lorem + 198, # \n + 151668, # /think + 271, # \n + 34084, # welcome + 151645, # im_end + 198, # \n + ] + # fmt: on + assert ( + input_ids == expected_input_ids + ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" From 14d670dbf0b91fee8cf4c3a463e803c06e169aef Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 28 Apr 2025 18:23:17 -0400 Subject: [PATCH 0595/1405] v0.9.0 release (#2578) --- src/axolotl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index b4d8b75b73..7ff225b897 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -4,4 +4,4 @@ __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.8.0" +__version__ = "0.9.0" From 8175896adaa077640b7b91678f853b7d0b97bf03 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 28 Apr 2025 20:30:14 -0400 Subject: [PATCH 0596/1405] add dev tag for v0.10.0.dev0 (#2580) --- src/axolotl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index 7ff225b897..63f28adda4 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -4,4 +4,4 @@ __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.9.0" +__version__ = "0.10.0.dev0" From fedbcc0254fc59afcbfb0c8bb0c6419ae17e128f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 29 Apr 2025 08:28:32 -0400 Subject: [PATCH 0597/1405] remove torch 2.4.1 CI as part of support deprecation (#2582) --- .github/workflows/base.yml | 6 ------ .github/workflows/main.yml | 10 ---------- .github/workflows/multi-gpu-e2e.yml | 7 ------- .github/workflows/nightlies.yml | 10 ---------- .github/workflows/tests-nightly.yml | 9 +-------- .github/workflows/tests.yml | 10 ++-------- 6 files changed, 3 insertions(+), 49 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index d6850a0de0..9e19114d71 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -22,12 +22,6 @@ jobs: fail-fast: false matrix: include: - - cuda: "124" - cuda_version: 12.4.1 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.4.1 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - cuda: "124" cuda_version: 12.4.1 cudnn_version: "" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 61b2fc14a2..891e31dba3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,11 +15,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.4.1 - axolotl_extras: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -82,11 +77,6 @@ jobs: strategy: matrix: include: - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.4.1 - axolotl_extras: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 78238bb974..edc1c7d792 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -32,13 +32,6 @@ jobs: axolotl_extras: vllm num_gpus: 2 nightly_build: "true" - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.4.1 - axolotl_extras: # no vllm support for 2.4.1 - num_gpus: 2 - nightly_build: "true" - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index bc3c1a1916..4e61984fb3 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -12,11 +12,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.4.1 - axolotl_extras: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -70,11 +65,6 @@ jobs: strategy: matrix: include: - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.4.1 - axolotl_extras: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 77cefc992a..23eb25f569 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -26,7 +26,7 @@ jobs: max-parallel: 2 matrix: python_version: ["3.11"] - pytorch_version: ["2.4.1", "2.5.1", "2.6.0"] + pytorch_version: ["2.5.1", "2.6.0", "2.7.0"] timeout-minutes: 20 steps: @@ -106,13 +106,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.4.1 - num_gpus: 1 - axolotl_extras: - nightly_build: "true" - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b149254376..e54850af74 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,7 +49,7 @@ jobs: max-parallel: 2 matrix: python_version: ["3.11"] - pytorch_version: ["2.4.1", "2.5.1", "2.6.0", "2.7.0"] + pytorch_version: ["2.5.1", "2.6.0", "2.7.0"] timeout-minutes: 20 steps: @@ -135,7 +135,7 @@ jobs: max-parallel: 1 matrix: python_version: ["3.11"] - pytorch_version: ["2.4.1", "2.5.1", "2.6.0"] + pytorch_version: ["2.5.1", "2.6.0", "2.7.0"] timeout-minutes: 20 steps: @@ -258,12 +258,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.4.1 - num_gpus: 1 - axolotl_extras: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" From 80b4edb4a765fa7967b60b7eba93732016564870 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 29 Apr 2025 10:01:38 -0400 Subject: [PATCH 0598/1405] Post release fixes (#2581) * fix missing kwarg on child * make the runpod test shorter * update docs * rename runpod test json file * typing fixes and ordering of doc --- .runpod/{tests.json => test-input.json} | 17 ++++++------- docs/config.qmd | 4 ++++ src/axolotl/integrations/kd/chat_template.py | 2 ++ .../prompt_strategies/chat_template.py | 24 +++++++++---------- 4 files changed, 27 insertions(+), 20 deletions(-) rename .runpod/{tests.json => test-input.json} (87%) diff --git a/.runpod/tests.json b/.runpod/test-input.json similarity index 87% rename from .runpod/tests.json rename to .runpod/test-input.json index 1d1e0287b5..52bc905e3e 100644 --- a/.runpod/tests.json +++ b/.runpod/test-input.json @@ -12,22 +12,22 @@ "base_model": "HuggingFaceTB/SmolLM2-135M", "model_type": "AutoModelForCausalLM", "tokenizer_type": "AutoTokenizer", - "load_in_8bit": true, - "load_in_4bit": false, + "load_in_4bit": true, "strict": false, "datasets": [ { "path": "mhenrichsen/alpaca_2k_test", - "type": "alpaca" + "type": "alpaca", + "split": "train[:10%]" } ], - "val_set_size": 0.05, + "val_set_size": 0.02, "output_dir": "./outputs/lora-out", "sequence_len": 4096, "sample_packing": true, "eval_sample_packing": false, "pad_to_sequence_len": true, - "adapter": "lora", + "adapter": "qlora", "lora_r": 32, "lora_alpha": 64, "lora_dropout": 0.05, @@ -36,8 +36,8 @@ "embed_tokens", "lm_head" ], - "gradient_accumulation_steps": 4, - "micro_batch_size": 2, + "gradient_accumulation_steps": 2, + "micro_batch_size": 1, "num_epochs": 1, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", @@ -56,7 +56,8 @@ "weight_decay": 0.0, "special_tokens": { "pad_token": "<|endoftext|>" - } + }, + "max_steps": 20 }, "timeout": 100000 }, diff --git a/docs/config.qmd b/docs/config.qmd index 8795fa4ab8..7b0d404621 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -184,6 +184,10 @@ datasets: # adding a system turn with empty content. drop_system_message: + # Optional[bool]. Whether to split the assistant turn based on a reasoning trace inside delimited tags + # defaults to False + split_thinking: + # IMPORTANT: The following fields determine which parts of the conversation to train on. # Priority order: message_field_training > message_field_training_detail > train_on_inputs or role in roles_to_train # See examples at `docs/dataset-formats/conversation.qmd` diff --git a/src/axolotl/integrations/kd/chat_template.py b/src/axolotl/integrations/kd/chat_template.py index 131570aea7..eb067cd04e 100644 --- a/src/axolotl/integrations/kd/chat_template.py +++ b/src/axolotl/integrations/kd/chat_template.py @@ -37,6 +37,7 @@ def __init__( train_on_eos=None, train_on_eot=None, eot_tokens=None, + split_thinking: bool | None = False, logprobs_field="logprobs", gen_temperature=1.0, kd_temperature=1.0, @@ -54,6 +55,7 @@ def __init__( train_on_eos=train_on_eos, train_on_eot=train_on_eot, eot_tokens=eot_tokens, + split_thinking=split_thinking, ) @property diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 399bb378af..c2948fc11c 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -4,7 +4,7 @@ import logging from collections import defaultdict -from typing import Any, Dict, List, Optional, Set, Union +from typing import Any, Dict, List, Set, Union from pydantic import BaseModel from transformers import ProcessorMixin @@ -29,12 +29,12 @@ def __init__( chat_template: str, processor=None, max_length=2048, - message_property_mappings: Optional[Dict[str, str]] = None, - message_field_training: Optional[str] = None, - message_field_training_detail: Optional[str] = None, + message_property_mappings: Dict[str, str] | None = None, + message_field_training: str | None = None, + message_field_training_detail: str | None = None, field_messages: str = "messages", field_system: str = "system", - roles: Optional[Dict[str, List[str]]] = None, + roles: Dict[str, List[str]] | None = None, drop_system_message: bool = False, ): # check if message_property_mappings is None or empty dict @@ -65,7 +65,7 @@ def __init__( self.field_messages = field_messages self.field_system = field_system self.tokenizer = tokenizer - self.processor: Optional[ProcessorMixin] = processor + self.processor: ProcessorMixin | None = processor self.chat_template = chat_template self.max_length = max_length self.drop_system_message = drop_system_message @@ -224,11 +224,11 @@ def __init__( tokenizer, train_on_inputs: bool, sequence_len: int, - roles_to_train: Optional[List[str]] = None, - train_on_eos: Optional[str] = None, - train_on_eot: Optional[str] = None, - eot_tokens: Optional[List[str]] = None, - split_thinking: Optional[bool] = False, + roles_to_train: list[str] | None = None, + train_on_eos: str | None = None, + train_on_eot: str | None = None, + eot_tokens: list[str] | None = None, + split_thinking: bool | None = False, ): super().__init__(prompter, tokenizer, train_on_inputs, sequence_len) self.prompter: ChatTemplatePrompter = prompter @@ -714,7 +714,7 @@ def __call__( self, tokenizer, cfg, - ds_cfg: Optional[Union[Dict[str, Any], DatasetConfig]] = None, + ds_cfg: Union[Dict[str, Any], DatasetConfig] | None = None, processor=None, ): if ds_cfg is None: From 6565ae85d8b6842a1c8acf7cedde476accdfb691 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 29 Apr 2025 12:05:44 -0400 Subject: [PATCH 0599/1405] set config on the PluginManager for callback access (#2587) --- src/axolotl/cli/config.py | 7 +++++++ src/axolotl/integrations/base.py | 23 ++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index 166a676703..64bf402b9f 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -152,6 +152,12 @@ def prepare_plugins(cfg: DictDefault): plugin_manager.register(plugin_name) +def plugin_set_cfg(cfg: DictDefault): + if cfg.get("plugins"): + plugin_manager = PluginManager.get_instance() + plugin_manager.cfg = cfg + + def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs) -> DictDefault: """ Loads the `axolotl` configuration stored at `config`, validates it, and performs @@ -213,5 +219,6 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs) -> DictDefa setup_wandb_env_vars(cfg) setup_mlflow_env_vars(cfg) setup_comet_env_vars(cfg) + plugin_set_cfg(cfg) return cfg diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index cb65f96dd0..7d6491478a 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -270,6 +270,7 @@ class PluginManager: plugins: OrderedDict[str, BasePlugin] = collections.OrderedDict() _instance = None + _cfg = None def __new__(cls): """ @@ -277,7 +278,9 @@ def __new__(cls): """ if cls._instance is None: cls._instance = super(PluginManager, cls).__new__(cls) - cls._instance.plugins = collections.OrderedDict() + cls._instance.plugins: OrderedDict[str, BasePlugin] = ( + collections.OrderedDict() + ) return cls._instance @staticmethod @@ -290,6 +293,14 @@ def get_instance() -> "PluginManager": PluginManager() return PluginManager._instance # type: ignore + @property + def cfg(self): + return self._cfg + + @cfg.setter + def cfg(self, cfg): + self._cfg = cfg + def register(self, plugin_name: str): """ Registers a new plugin by its name. @@ -409,29 +420,27 @@ def get_trainer_cls(self, cfg): return trainer_cls return None - def create_optimizer(self, cfg, trainer): + def create_optimizer(self, trainer): """ Calls the create_optimizer method of all registered plugins and returns the first non-None optimizer. Parameters: - cfg (dict): The configuration for the plugins. trainer (object): The trainer object for training. Returns: object: The created optimizer, or None if none was found. """ for plugin in self.plugins.values(): - optimizer = plugin.create_optimizer(cfg, trainer) + optimizer = plugin.create_optimizer(self.cfg, trainer) if optimizer is not None: return optimizer return None - def create_lr_scheduler(self, cfg, trainer, optimizer): + def create_lr_scheduler(self, trainer, optimizer): """ Calls the create_lr_scheduler method of all registered plugins and returns the first non-None scheduler. Parameters: - cfg (dict): The configuration for the plugins. trainer (object): The trainer object for training. optimizer (object): The optimizer for training. @@ -439,7 +448,7 @@ def create_lr_scheduler(self, cfg, trainer, optimizer): object: The created learning rate scheduler, or None if none was found. """ for plugin in self.plugins.values(): - scheduler = plugin.create_lr_scheduler(cfg, trainer, optimizer) + scheduler = plugin.create_lr_scheduler(self.cfg, trainer, optimizer) if scheduler is not None: return scheduler return None From c7d07de6b47b1b11d2098589e4bb15c6ed1066c3 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Tue, 29 Apr 2025 12:58:54 -0400 Subject: [PATCH 0600/1405] Fix eval + add smoke test (#2586) * fix evaluate CLI * add smoke test * fix naming * lint --- src/axolotl/cli/evaluate.py | 8 ++++- src/axolotl/evaluate.py | 36 +++++++------------- tests/e2e/test_evaluate.py | 65 +++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 tests/e2e/test_evaluate.py diff --git a/src/axolotl/cli/evaluate.py b/src/axolotl/cli/evaluate.py index c89715719e..a1859f3156 100644 --- a/src/axolotl/cli/evaluate.py +++ b/src/axolotl/cli/evaluate.py @@ -1,6 +1,7 @@ """CLI to run evaluation on a model.""" import logging +import os from pathlib import Path from typing import Union @@ -14,6 +15,7 @@ from axolotl.cli.config import load_cfg from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.evaluate import evaluate +from axolotl.utils import set_pytorch_cuda_alloc_conf from axolotl.utils.dict import DictDefault LOG = logging.getLogger(__name__) @@ -29,10 +31,14 @@ def do_evaluate(cfg: DictDefault, cli_args: TrainerCliArgs) -> None: cfg: Dictionary mapping `axolotl` config keys to values. cli_args: CLI arguments. """ + # Enable expandable segments for cuda allocation to improve VRAM usage + set_pytorch_cuda_alloc_conf() + # pylint: disable=duplicate-code print_axolotl_text_art() check_accelerate_default_config() - check_user_token() + if int(os.getenv("LOCAL_RANK", "0")) == 0: + check_user_token() if cfg.rl: dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) diff --git a/src/axolotl/evaluate.py b/src/axolotl/evaluate.py index 216ff0110e..a6a192bc7f 100644 --- a/src/axolotl/evaluate.py +++ b/src/axolotl/evaluate.py @@ -12,11 +12,12 @@ from transformers.trainer import Trainer from axolotl.logging_config import configure_logging -from axolotl.train import TrainDatasetMeta -from axolotl.utils import set_pytorch_cuda_alloc_conf +from axolotl.train import ( + TrainDatasetMeta, + setup_model_and_tokenizer, +) from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import cleanup_distributed -from axolotl.utils.models import load_model, load_processor, load_tokenizer from axolotl.utils.trainer import setup_trainer project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) @@ -24,7 +25,7 @@ sys.path.insert(0, src_dir) configure_logging() -LOG = get_logger("axolotl.evaluate") +LOG = get_logger(__name__) def evaluate_dataset( @@ -75,37 +76,22 @@ def evaluate(*, cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> Dict[str, f Returns: Dictionary mapping metric names to their values. """ - # pylint: disable=duplicate-code - # Enable expandable segments for cuda allocation to improve VRAM usage - set_pytorch_cuda_alloc_conf() - - # Load tokenizer - LOG.debug( - f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}", - main_process_only=True, - ) - tokenizer = load_tokenizer(cfg) - - # Load processor for multimodal models if needed - processor = None - if cfg.is_multimodal: - processor = load_processor(cfg, tokenizer) + # Load tokenizer, processor and model + LOG.debug("loading model for evaluation...") + model, tokenizer, _, processor = setup_model_and_tokenizer(cfg) # Get datasets + # pylint: disable=duplicate-code train_dataset = dataset_meta.train_dataset eval_dataset = dataset_meta.eval_dataset total_num_steps = dataset_meta.total_num_steps - # Load model - LOG.debug("loading model for evaluation...") - model, _ = load_model(cfg, tokenizer, processor=processor) - # Set up trainer trainer = setup_trainer( - cfg, + cfg=cfg, train_dataset=train_dataset, eval_dataset=eval_dataset, - model=(model, None, None), # No need for model_ref or peft_config + model=model, tokenizer=tokenizer, processor=processor, total_num_steps=total_num_steps, diff --git a/tests/e2e/test_evaluate.py b/tests/e2e/test_evaluate.py new file mode 100644 index 0000000000..b2d7d02cad --- /dev/null +++ b/tests/e2e/test_evaluate.py @@ -0,0 +1,65 @@ +"""E2E smoke test for evaluate CLI command""" + +import os +from pathlib import Path + +import yaml +from accelerate.test_utils import execute_subprocess_async +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +os.environ["WANDB_DISABLED"] = "true" + + +class TestE2eEvaluate: + """Test cases for evaluate CLI""" + + def test_evaluate(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 1024, + "val_set_size": 0.02, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "max_steps": 20, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "accelerate", + "launch", + "--num-processes", + "2", + "--main_process_port", + f"{get_torch_dist_unique_port()}", + "-m", + "axolotl.cli.evaluate", + str(Path(temp_dir) / "config.yaml"), + ] + ) From 07e4f2e25b5d63ab84216ffb7c473cbe4b0b9582 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 29 Apr 2025 15:02:49 -0400 Subject: [PATCH 0601/1405] support for qwen3 with lora kernels (#2588) * support for qwen3 with lora kernels * fix patch * typo --- src/axolotl/monkeypatch/lora_kernels.py | 53 ++++++++++++++----- .../lora_kernels/test_lora_kernel_patching.py | 22 +++++--- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 0036fe0039..6c920dcc86 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -23,22 +23,42 @@ LOG = get_logger(__name__) -ORIGINAL_QKV_CODE = """ +QKV_PATCHES = [ + ( + """ query_states = self.q_proj(hidden_states).view(hidden_shape).transpose(1, 2) key_states = self.k_proj(hidden_states).view(hidden_shape).transpose(1, 2) value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) """.lstrip( - "\n" -) - -PATCHED_QKV_CODE = """ + "\n" + ), + """ query_states, key_states, value_states = self.apply_qkv(hidden_states) query_states = query_states.view(hidden_shape).transpose(1, 2) key_states = key_states.view(hidden_shape).transpose(1, 2) value_states = value_states.view(hidden_shape).transpose(1, 2) """.lstrip( - "\n" -) + "\n" + ), + ), + ( + """ + query_states = self.q_norm(self.q_proj(hidden_states).view(hidden_shape)).transpose(1, 2) + key_states = self.k_norm(self.k_proj(hidden_states).view(hidden_shape)).transpose(1, 2) + value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) +""".lstrip( + "\n" + ), + """ + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states = self.q_norm(query_states.view(hidden_shape)).transpose(1, 2) + key_states = self.k_norm(key_states.view(hidden_shape)).transpose(1, 2) + value_states = value_states.view(hidden_shape).transpose(1, 2) +""".lstrip( + "\n" + ), + ), +] ORIGINAL_O_CODE = """ attn_output = self.o_proj(attn_output) @@ -128,10 +148,11 @@ def get_attention_cls_from_config(cfg: DictDefault) -> Type[nn.Module]: try: # Dynamically import the module and attention class module_path = f"transformers.models.{model_type}.modeling_{model_type}" - module = __import__( - module_path, fromlist=[f"{model_type.capitalize()}Attention"] + model_cls_prefix = "".join( + [part.capitalize() for part in model_type.split("_")] ) - attention_cls = getattr(module, f"{model_type.capitalize()}Attention") + module = __import__(module_path, fromlist=[f"{model_cls_prefix}Attention"]) + attention_cls = getattr(module, f"{model_cls_prefix}Attention") return attention_cls except (ImportError, AttributeError) as e: @@ -168,10 +189,18 @@ def patch_self_attn_lora(cfg: DictDefault): attention_cls._original_forward = self_attn_forward self_attn_forward, _ = detab_code(self_attn_forward) - assert ORIGINAL_QKV_CODE in self_attn_forward, "Original QKV code not found" + assert any( + qkv_options[0] in self_attn_forward for qkv_options in QKV_PATCHES + ), "Original QKV code not found" assert ORIGINAL_O_CODE in self_attn_forward, "Original O code not found" - self_attn_forward = self_attn_forward.replace(ORIGINAL_QKV_CODE, PATCHED_QKV_CODE) + for qkv_orig, qkv_patched in QKV_PATCHES: + if qkv_orig in self_attn_forward: + self_attn_forward = self_attn_forward.replace( + qkv_orig, + qkv_patched, + ) + break self_attn_forward = self_attn_forward.replace(ORIGINAL_O_CODE, PATCHED_O_CODE) self_attn_forward = self_attn_forward.replace( "def forward(", diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index bada305b31..eb0c73225d 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -9,6 +9,7 @@ from transformers import AutoModelForCausalLM, LlamaForCausalLM from transformers.models.llama.configuration_llama import LlamaConfig from transformers.models.llama.modeling_llama import LlamaAttention +from transformers.models.qwen3_moe.modeling_qwen3_moe import Qwen3MoeAttention from axolotl.kernels.lora import ( apply_lora_mlp_geglu, @@ -66,29 +67,36 @@ def small_llama_model(): return LlamaForCausalLM(LlamaConfig(**config)) -def test_attention_patching_integration(): +@pytest.mark.parametrize( + "model_name,attention_cls", + [ + ("HuggingFaceTB/SmolLM2-135M", LlamaAttention), + ("Qwen/Qwen3-30B-A3B", Qwen3MoeAttention), + ], +) +def test_attention_patching_integration(model_name, attention_cls): """Test attention patching in integration context.""" - cfg = {"base_model": "TinyLlama/TinyLlama-1.1B-Chat-v1.0"} + cfg = {"base_model": model_name} # Store the original implementation - original_forward = getattr(LlamaAttention, "forward") + original_forward = getattr(attention_cls, "forward") # Apply patch patch_self_attn_lora(cfg) # Get the new forward method - patched_forward = LlamaAttention.forward + patched_forward = attention_cls.forward # Check the forward method was replaced assert original_forward is not patched_forward assert patched_forward.__name__ == "axolotl_attn_forward" # Check original implementation was stored - assert hasattr(LlamaAttention, "_original_forward") + assert hasattr(attention_cls, "_original_forward") # Clean up - setattr(LlamaAttention, "forward", original_forward) - delattr(LlamaAttention, "_original_forward") + setattr(attention_cls, "forward", original_forward) + delattr(attention_cls, "_original_forward") def test_swiglu_mlp_integration(small_llama_model): From a39caf8824ed65567a8b751db50949a13a0d9a8a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 29 Apr 2025 15:10:40 -0400 Subject: [PATCH 0602/1405] bump vllm==0.8.5 for qwen3 support (#2583) [skip ci] --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3a494d1071..5b66a2ea79 100644 --- a/setup.py +++ b/setup.py @@ -67,13 +67,13 @@ def parse_requirements(extras_require_map): if (major, minor) >= (2, 7): _install_requires.pop(_install_requires.index(xformers_version)) # _install_requires.append("xformers==0.0.29.post3") # xformers seems to be hard pinned to 2.6.0 - extras_require_map["vllm"] = ["vllm==0.8.4"] + extras_require_map["vllm"] = ["vllm==0.8.5"] elif (major, minor) >= (2, 6): _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append( "xformers==0.0.29.post2" ) # vllm needs post2 w torch 2.6 - extras_require_map["vllm"] = ["vllm==0.8.4"] + extras_require_map["vllm"] = ["vllm==0.8.5"] elif (major, minor) >= (2, 5): _install_requires.pop(_install_requires.index(xformers_version)) if patch == 0: From 742fef42001bc28b4fda7cbddf3363789eac8ced Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 30 Apr 2025 02:10:59 +0700 Subject: [PATCH 0603/1405] fix(doc): key used to point to url in multimodal doc (#2575) [skip ci] --- docs/multimodal.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index 5ddd91f60f..3506db3409 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -164,7 +164,7 @@ Here is an example of a multi-modal dataset: { "role": "user", "content": [ - {"type": "image", "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/bee.jpg"}, + {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/bee.jpg"}, {"type": "text", "text": "Describe this image in detail."} ] }, From ecac731922a43825158512286ac21f85ca9f953a Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Tue, 29 Apr 2025 16:18:49 -0400 Subject: [PATCH 0604/1405] auto-enable lora kernels where possible (#2589) * auto-enable lora kernels where possible * test * revert change to example yaml * naming * remove print * slight logic change --- src/axolotl/utils/schemas/config.py | 48 +++++++++++++++++++ .../lora_kernels/test_lora_kernel_patching.py | 43 +++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 36c18fd3c8..bc25b1ab0d 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1315,6 +1315,54 @@ def check_multigpu_lora_kernels(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_auto_enable_lora_kernels(cls, data): + # Only proceed if using LoRA or QLoRA adapter + if data.get("adapter") in ["lora", "qlora"]: + # Skip if already set, using unsloth optimizations, or using 8-bit + unsloth_fields = ["unsloth_lora_mlp", "unsloth_lora_qkv", "unsloth_lora_o"] + kernel_fields = ["lora_mlp_kernel", "lora_qkv_kernel", "lora_o_kernel"] + if ( + any(data.get(k) is not None for k in kernel_fields) + or any(data.get(k) for k in unsloth_fields) + or data.get("adapter") == "lora" + and data.get("load_in_8bit") + ): + return data + + # Check multi-GPU compatibility + capabilities = data.get("capabilities") + is_multi_gpu = capabilities and capabilities.get("n_gpu", 0) > 1 + is_fsdp = data.get("fsdp") is not None + is_fsdp2 = ( + data.get("fsdp_config") is not None + and str(data.get("fsdp_config").get("fsdp_version")) == "2" + ) + + if ( + not is_multi_gpu + or (is_multi_gpu and not is_fsdp) + or (is_multi_gpu and is_fsdp2) + ): + # Auto-enable kernels if not explicitly set by user + if data.get("lora_mlp_kernel") is None: + data["lora_mlp_kernel"] = True + + if data.get("lora_qkv_kernel") is None: + data["lora_qkv_kernel"] = True + + if data.get("lora_o_kernel") is None: + data["lora_o_kernel"] = True + + LOG.warning( + "Auto-enabling LoRA kernel optimizations for faster training. " + + "Please explicitly set `lora_*_kernel` config values to `false` to disable. " + + "See https://docs.axolotl.ai/docs/lora_optims.html for more info." + ) + + return data + @model_validator(mode="before") @classmethod def check_adopt_torch_version(cls, data): diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index eb0c73225d..f3e59b3734 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -2,8 +2,11 @@ # pylint: disable=redefined-outer-name +from pathlib import Path + import pytest import torch +import yaml from accelerate.state import PartialState from peft import PeftModelForCausalLM, get_peft_config from transformers import AutoModelForCausalLM, LlamaForCausalLM @@ -11,6 +14,7 @@ from transformers.models.llama.modeling_llama import LlamaAttention from transformers.models.qwen3_moe.modeling_qwen3_moe import Qwen3MoeAttention +from axolotl.cli.config import load_cfg from axolotl.kernels.lora import ( apply_lora_mlp_geglu, apply_lora_mlp_swiglu, @@ -421,3 +425,42 @@ def test_kernel_training_integration(): # Verify correct activation function layer = model.model.model.layers[0] assert layer.mlp.forward.__func__ is apply_lora_mlp_swiglu + + +def test_kernel_training_integration_auto_enable(temp_dir): + """Test model loading with auto-enabled kernel patches.""" + # Create minimal config without explicitly setting kernel options + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_config": "HuggingFaceTB/SmolLM2-135M", + "learning_rate": 0.000001, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + } + ], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.0, + "lora_target_linear": True, + "sequence_len": 1024, + } + ) + + # Write cfg to yaml file + path = Path(temp_dir) / "config.yaml" + with open(path, "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + # Load config + cfg = load_cfg(str(path)) + + # Verify kernel options were auto-enabled in the config + assert cfg.lora_mlp_kernel is True + assert cfg.lora_qkv_kernel is True + assert cfg.lora_o_kernel is True From 41a1ec0c9593d8533daa057c09155afa762ced61 Mon Sep 17 00:00:00 2001 From: Aleksandr Dremov Date: Tue, 29 Apr 2025 23:08:30 +0200 Subject: [PATCH 0605/1405] Plugins create_lr_scheduler support (#2584) * lr_scheduler support * fix * Update scheduler.py * Update scheduler.py * cfg handling * black * remove debug * remove adding the axolotl cfg to the scheduler mixin --------- Co-authored-by: Wing Lian --- src/axolotl/core/trainers/mixins/scheduler.py | 20 +++++++++++++----- src/axolotl/core/trainers/relora.py | 13 +++++++----- src/axolotl/integrations/base.py | 21 +++++++++++++------ tests/e2e/integrations/test_hooks.py | 4 ++-- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/axolotl/core/trainers/mixins/scheduler.py b/src/axolotl/core/trainers/mixins/scheduler.py index b0a5ee8957..0c36f9f95d 100644 --- a/src/axolotl/core/trainers/mixins/scheduler.py +++ b/src/axolotl/core/trainers/mixins/scheduler.py @@ -3,9 +3,10 @@ import logging import torch -from torch.optim.lr_scheduler import OneCycleLR +from torch.optim.lr_scheduler import LRScheduler, OneCycleLR from transformers.trainer import Trainer +from axolotl.integrations.base import PluginManager from axolotl.utils.schedulers import ( RexLR, get_cosine_schedule_with_min_lr, @@ -25,9 +26,9 @@ class SchedulerMixin(Trainer): def create_scheduler( self, num_training_steps: int, optimizer: torch.optim.Optimizer = None - ): + ) -> LRScheduler: """ - Setup the scheduler. The optimizer of the trainer must have been set up either before this method is called or + Set up the scheduler. The optimizer of the trainer must have been set up either before this method is called or passed as an argument. Args: @@ -47,7 +48,16 @@ def create_scheduler( # fmt: off if self.lr_scheduler is None: # type: ignore # pylint: disable=access-member-before-definition # fmt: on - if self.args.alternate_lr_scheduler_type == "one_cycle": + plugin_manager = PluginManager.get_instance() + lr_scheduler: LRScheduler | None = plugin_manager.create_lr_scheduler( + trainer=self, + optimizer=optimizer, + num_training_steps=num_training_steps + ) + if lr_scheduler is not None: + LOG.info(f"Using plugin-created lr_scheduler: {lr_scheduler}") + self.lr_scheduler = lr_scheduler + elif self.args.alternate_lr_scheduler_type == "one_cycle": num_warmup_steps = self.args.get_warmup_steps(num_training_steps) pct_start = num_warmup_steps / num_training_steps extra_lr_kwargs = {} @@ -110,4 +120,4 @@ def create_scheduler( if use_cosine_min_lr: LOG.warning("axolotl's cosine scheduler with min lr not used (e.g., because of deepspeed).") - return self.lr_scheduler + return self.lr_scheduler # type: ignore diff --git a/src/axolotl/core/trainers/relora.py b/src/axolotl/core/trainers/relora.py index 3bcd4a9b8c..890278f494 100644 --- a/src/axolotl/core/trainers/relora.py +++ b/src/axolotl/core/trainers/relora.py @@ -1,6 +1,7 @@ """Module for ReLoRA trainer""" import torch +from torch.optim.lr_scheduler import LRScheduler from axolotl.core.trainers.base import AxolotlTrainer from axolotl.monkeypatch.relora import ReLoRAScheduler @@ -19,9 +20,11 @@ def create_scheduler( self, num_training_steps: int, optimizer: torch.optim.Optimizer | None = None, - ): + ) -> LRScheduler: optimizer = self.optimizer if optimizer is None else optimizer - lr_scheduler = super().create_scheduler(num_training_steps, optimizer) + lr_scheduler: LRScheduler = super().create_scheduler( + num_training_steps, optimizer + ) if self.args.relora_steps: warmup_steps = ( @@ -30,7 +33,7 @@ def create_scheduler( anneal_steps = ( self.args.relora_anneal_steps if self.args.relora_anneal_steps else 1 ) - self.lr_scheduler = ReLoRAScheduler( + self.lr_scheduler = ReLoRAScheduler( # type: ignore optimizer, lr_scheduler, self.args.relora_steps, @@ -38,6 +41,6 @@ def create_scheduler( warmup_steps, ) else: - self.lr_scheduler = lr_scheduler + self.lr_scheduler = lr_scheduler # type: ignore - return self.lr_scheduler + return self.lr_scheduler # type: ignore diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 7d6491478a..efe542af74 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -24,6 +24,7 @@ from typing import OrderedDict import torch +from torch.optim.lr_scheduler import LRScheduler class BasePlugin: @@ -41,7 +42,7 @@ class BasePlugin: post_lora_load(cfg, model): Performs actions after LoRA weights are loaded. post_model_load(cfg, model): Performs actions after the model is loaded, inclusive of any adapters. create_optimizer(cfg, trainer): Creates and returns an optimizer for training. - create_lr_scheduler(cfg, trainer, optimizer): Creates and returns a learning rate scheduler. + create_lr_scheduler(cfg, trainer, optimizer, num_training_steps): Creates and returns a learning rate scheduler. add_callbacks_pre_trainer(cfg, model): Adds callbacks to the trainer before training. add_callbacks_post_trainer(cfg, trainer): Adds callbacks to the trainer after training. """ @@ -146,8 +147,8 @@ def create_optimizer(self, cfg, trainer): # pylint: disable=unused-argument """ def create_lr_scheduler( - self, cfg, trainer, optimizer - ): # pylint: disable=unused-argument + self, cfg, trainer, optimizer, num_training_steps + ) -> LRScheduler | None: # pylint: disable=unused-argument """ Creates and returns a learning rate scheduler. @@ -155,9 +156,10 @@ def create_lr_scheduler( cfg (dict): The configuration for the plugin. trainer (object): The trainer object for training. optimizer (object): The optimizer for training. + num_training_steps (int): Total number of training steps Returns: - object: The created learning rate scheduler. + object (LRScheduler): The created learning rate scheduler. """ def add_callbacks_pre_trainer(self, cfg, model): # pylint: disable=unused-argument @@ -436,7 +438,9 @@ def create_optimizer(self, trainer): return optimizer return None - def create_lr_scheduler(self, trainer, optimizer): + def create_lr_scheduler( + self, trainer, optimizer, num_training_steps + ) -> LRScheduler | None: """ Calls the create_lr_scheduler method of all registered plugins and returns the first non-None scheduler. @@ -448,7 +452,12 @@ def create_lr_scheduler(self, trainer, optimizer): object: The created learning rate scheduler, or None if none was found. """ for plugin in self.plugins.values(): - scheduler = plugin.create_lr_scheduler(self.cfg, trainer, optimizer) + scheduler: LRScheduler | None = plugin.create_lr_scheduler( + self.cfg, + trainer=trainer, + optimizer=optimizer, + num_training_steps=num_training_steps, + ) if scheduler is not None: return scheduler return None diff --git a/tests/e2e/integrations/test_hooks.py b/tests/e2e/integrations/test_hooks.py index e51334dfe8..9b12e6d4ef 100644 --- a/tests/e2e/integrations/test_hooks.py +++ b/tests/e2e/integrations/test_hooks.py @@ -72,7 +72,7 @@ def get_trainer_cls(self, cfg): # pylint: disable=unused-argument f.write("get_trainer_cls\n") def create_lr_scheduler( - self, cfg, trainer, optimizer + self, cfg, trainer, optimizer, num_training_steps ): # pylint: disable=unused-argument with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" @@ -172,7 +172,7 @@ def test_plugin_hooks(self, temp_dir): assert "post_model_load" in file_contents # assert "create_optimizer" in file_contents # not implemented yet assert "get_trainer_cls" in file_contents - # assert "create_lr_scheduler" in file_contents # not implemented yet + assert "create_lr_scheduler" in file_contents assert "add_callbacks_pre_trainer" in file_contents assert "add_callbacks_post_trainer" in file_contents assert "post_train" in file_contents From a21b9cc472f9d7736db72e7f5dc4d8df962c1c28 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 30 Apr 2025 03:31:57 -0400 Subject: [PATCH 0606/1405] patch to convert LR from tensor to float when using DS (#2595) [skip ci] --- src/axolotl/core/trainer_builder.py | 3 ++ src/axolotl/monkeypatch/trainer/lr.py | 42 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/axolotl/monkeypatch/trainer/lr.py diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 970b020755..358058f692 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -60,6 +60,7 @@ from axolotl.integrations.base import PluginManager from axolotl.monkeypatch.multipack import SUPPORTED_MULTIPACK_MODEL_TYPES from axolotl.monkeypatch.relora import ReLoRACallback +from axolotl.monkeypatch.trainer.lr import patch_trainer_get_lr from axolotl.processing_strategies import get_processing_strategy from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.callbacks import ( @@ -114,6 +115,8 @@ def __init__(self, cfg, model, tokenizer, processor=None): if hasattr(model, "add_model_tags"): model.add_model_tags(["axolotl"]) + patch_trainer_get_lr() + @property def model_ref(self): return self._model_ref diff --git a/src/axolotl/monkeypatch/trainer/lr.py b/src/axolotl/monkeypatch/trainer/lr.py new file mode 100644 index 0000000000..0176093d6a --- /dev/null +++ b/src/axolotl/monkeypatch/trainer/lr.py @@ -0,0 +1,42 @@ +""" +monkeypatch for Trainer _get_learning_rate method +""" + +import logging + +import torch + +LOG = logging.getLogger(__name__) + + +# TODO remove this patch once https://github.com/huggingface/transformers/pull/37881 is included in a release +def _get_learning_rate(self): + if self.is_deepspeed_enabled: + # with deepspeed's fp16 and dynamic loss scale enabled the optimizer/scheduler steps may + # not run for the first few dozen steps while loss scale is too large, and thus during + # that time `get_last_lr` will fail if called during that warm up stage, so work around it: + try: + last_lr = self.lr_scheduler.get_last_lr()[0] + except AssertionError as e: + if "need to call step" in str(e): + LOG.warning( + "tried to get lr value before scheduler/optimizer started stepping, returning lr=0" + ) + last_lr = 0 + else: + raise + else: + if isinstance(self.lr_scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau): + last_lr = self.optimizer.param_groups[0]["lr"] + else: + last_lr = self.lr_scheduler.get_last_lr()[0] + + if torch.is_tensor(last_lr): + last_lr = last_lr.item() + return last_lr + + +def patch_trainer_get_lr(): + from transformers.trainer import Trainer + + Trainer._get_learning_rate = _get_learning_rate # pylint: disable=protected-access From 5bb1f3da5605548eac9e4bf338a351e29b2ea02c Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 30 Apr 2025 14:32:23 +0700 Subject: [PATCH 0607/1405] feat: add qwen3 moe block for ds3 (#2596) [skip ci] --- src/axolotl/common/architectures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/axolotl/common/architectures.py b/src/axolotl/common/architectures.py index 827a63c070..2f77b613eb 100644 --- a/src/axolotl/common/architectures.py +++ b/src/axolotl/common/architectures.py @@ -11,5 +11,6 @@ ], "mixtral": "MixtralSparseMoeBlock", "qwen2_moe": "Qwen2MoeSparseMoeBlock", + "qwen3_moe": "Qwen3MoeSparseMoeBlock", "deepseek_v2": "DeepseekV2MoE", } From 2413688b084ac97eb7d328341694686f736b18c2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 30 Apr 2025 03:32:44 -0400 Subject: [PATCH 0608/1405] upload the deepspeed json to wandb (#2593) [skip ci] --- src/axolotl/utils/callbacks/__init__.py | 40 +++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index ffe4699f8f..21b14d9860 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import gc +import json import logging import os import traceback @@ -808,11 +809,44 @@ def on_train_begin( artifact.add_file(temp_file.name) wandb.log_artifact(artifact) wandb.save(temp_file.name) - LOG.info( - "The Axolotl config has been saved to the WandB run under files." - ) + LOG.info( + "The Axolotl config has been saved to the WandB run under files." + ) except (FileNotFoundError, ConnectionError) as err: LOG.warning(f"Error while saving Axolotl config to WandB: {err}") + + if args.deepspeed: + try: + # sync config to top level in run, cannot delete file right away because wandb schedules it to be synced even w/policy = 'now', so let OS delete it later. + with NamedTemporaryFile( + mode="w", + delete=False, + suffix=".json", + prefix="deepspeed_config_", + ) as temp_file: + skip_upload = False + if isinstance(args.deepspeed, dict): + json.dump(args.deepspeed, temp_file, indent=4) + elif isinstance(args.deepspeed, str) and os.path.exists( + args.deepspeed + ): + copyfile(args.deepspeed, temp_file.name) + else: + skip_upload = True + if not skip_upload: + artifact = wandb.Artifact( + f"deepspeed-config-{wandb.run.id}", + type="deepspeed-config", + ) + artifact.add_file(temp_file.name) + wandb.log_artifact(artifact) + wandb.save(temp_file.name) + LOG.info( + "The DeepSpeed config has been saved to the WandB run under files." + ) + except (FileNotFoundError, ConnectionError) as err: + LOG.warning(f"Error while saving DeepSpeed config to WandB: {err}") + return control From baeb00231b90c65bfe71d4faeb72d31969b7da49 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 30 Apr 2025 03:32:55 -0400 Subject: [PATCH 0609/1405] Handle other reasoning trace dataset formats (#2591) * Handle other reasoning trace dataset formats * rename var to improve readability * chore: refactor with comments --------- Co-authored-by: NanoCode012 --- .../prompt_strategies/chat_template.py | 49 ++++++++-- .../test_chat_templates_thinking.py | 91 ++++++++++++------- 2 files changed, 98 insertions(+), 42 deletions(-) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index c2948fc11c..638cee559e 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -42,6 +42,7 @@ def __init__( message_property_mappings = { "role": "role", "content": "content", + "reasoning_content": "reasoning_content", } if roles: @@ -661,16 +662,46 @@ def transform_message(self, message): # if the role is assistant that we want to use reasoning_content if self.split_thinking and transformed_message["role"] == "assistant": content = transformed_message["content"] - pairs = [("", ""), ("", "")] - for pair in pairs: - if pair[0] in content and pair[1] in content: - start_idx = content.find(pair[0]) - end_idx = content.find(pair[1]) - thinking_content = content[start_idx + len(pair[0]) : end_idx] + thinking_pairs = [ + ("", ""), + ("", ""), + ("<|begin_of_thought|>", "<|end_of_thought|>"), + ] + content_pairs = [("<|begin_of_solution|>", "<|end_of_solution|>")] + for tpair in thinking_pairs: + # check if the thinking pair is in the content + if tpair[0] in content and tpair[1] in content: + # find the start and end index of the thinking pair + t_start_idx = content.find(tpair[0]) + t_end_idx = content.find(tpair[1]) + + # get the thinking content + thinking_content = content[t_start_idx + len(tpair[0]) : t_end_idx] transformed_message["reasoning_content"] = thinking_content.strip() - transformed_message["content"] = content[ - end_idx + len(pair[1]) : - ].lstrip() + + # take remainder of the content + # strip whitespace from beginning of the remainder (thinking tokens) + remainder = content[t_end_idx + len(tpair[1]) :].lstrip() + + # check if the content pair is in the remainder + cpair_found = False + for cpair in content_pairs: + if cpair[0] in remainder and cpair[1] in remainder: + # find the start and end index of the content pair + c_start_idx = remainder.find(cpair[0]) + c_end_idx = remainder.find(cpair[1]) + + # get the content content + content_content = remainder[ + c_start_idx + len(cpair[0]) : c_end_idx + ] + transformed_message["content"] = content_content.strip() + cpair_found = True + break + + # else, the content is the remainder + if not cpair_found: + transformed_message["content"] = remainder break # Determine which keys in the original message were not mapped diff --git a/tests/prompt_strategies/test_chat_templates_thinking.py b/tests/prompt_strategies/test_chat_templates_thinking.py index 236a7ec39f..9fe292317d 100644 --- a/tests/prompt_strategies/test_chat_templates_thinking.py +++ b/tests/prompt_strategies/test_chat_templates_thinking.py @@ -34,7 +34,31 @@ def messages_w_reasoning_fixture(): "content": "lorem\nwelcome", }, ] - } + }, + { + "messages": [ + { + "role": "user", + "content": "hello", + }, + { + "role": "assistant", + "content": "<|begin_of_thought|>lorem<|end_of_thought|>\n<|begin_of_solution|>welcome\n<|end_of_solution|>", + }, + ] + }, + { + "messages": [ + { + "role": "user", + "content": "hello", + }, + { + "role": "assistant", + "content": "lorem\nwelcome", + }, + ] + }, ] ) @@ -83,36 +107,37 @@ def test_splits_think(self, messages_w_reasoning, qwen3_tokenizer): } ), ) - transformed_prompt = strategy.get_conversation_thread(messages_w_reasoning[0]) - assert transformed_prompt[0]["role"] == "user" - assert transformed_prompt[1]["role"] == "assistant" - assert transformed_prompt[1]["reasoning_content"] == "lorem" - assert transformed_prompt[1]["content"] == "welcome" + for conversation in messages_w_reasoning: + transformed_prompt = strategy.get_conversation_thread(conversation) + assert transformed_prompt[0]["role"] == "user" + assert transformed_prompt[1]["role"] == "assistant" + assert transformed_prompt[1]["reasoning_content"] == "lorem" + assert transformed_prompt[1]["content"] == "welcome" - res = strategy.tokenize_prompt(messages_w_reasoning[0]) - input_ids = res["input_ids"] - # fmt: off - expected_input_ids = [ - 151644, # im_start - 872, # user - 198, # \n - 14990, # hello - 151645, # im_end - 198, # \n - 151644, # im_start - 77091, # assistant - 198, # \n - 151667, # think - 198, # \n - 385, 1826, # lorem - 198, # \n - 151668, # /think - 271, # \n - 34084, # welcome - 151645, # im_end - 198, # \n - ] - # fmt: on - assert ( - input_ids == expected_input_ids - ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + res = strategy.tokenize_prompt(conversation) + input_ids = res["input_ids"] + # fmt: off + expected_input_ids = [ + 151644, # im_start + 872, # user + 198, # \n + 14990, # hello + 151645, # im_end + 198, # \n + 151644, # im_start + 77091, # assistant + 198, # \n + 151667, # think + 198, # \n + 385, 1826, # lorem + 198, # \n + 151668, # /think + 271, # \n + 34084, # welcome + 151645, # im_end + 198, # \n + ] + # fmt: on + assert ( + input_ids == expected_input_ids + ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" From fc79606b6de80474acfc70e5da04f1ce73de7458 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 30 Apr 2025 09:11:25 -0400 Subject: [PATCH 0610/1405] only import vllm serve cli if its being called (#2597) [skip ci] --- src/axolotl/cli/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index 5936147333..de4fb6cbee 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -28,7 +28,6 @@ fetch_from_github, filter_none_kwargs, ) -from axolotl.cli.vllm_serve import do_vllm_serve from axolotl.integrations.lm_eval.cli import lm_eval from axolotl.utils import set_pytorch_cuda_alloc_conf from axolotl.utils.schemas.config import AxolotlInputConfig @@ -327,6 +326,8 @@ def fetch(directory: str, dest: Optional[str]) -> None: @add_options_from_dataclass(VllmServeCliArgs) @filter_none_kwargs def vllm_serve(config: str, **cli_args: VllmServeCliArgs): + from axolotl.cli.vllm_serve import do_vllm_serve + do_vllm_serve(config, cli_args) From 8446b4ad28409438805ba780e3fd7d766011ea2c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 30 Apr 2025 11:06:50 -0400 Subject: [PATCH 0611/1405] don't automatically enable lora kernels for RL training (#2600) --- src/axolotl/utils/schemas/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index bc25b1ab0d..7f35de81d6 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1319,6 +1319,9 @@ def check_multigpu_lora_kernels(cls, data): @classmethod def check_auto_enable_lora_kernels(cls, data): # Only proceed if using LoRA or QLoRA adapter + if data.get("rl"): + # RL trainers not tested so don't enable kernels by default + return data if data.get("adapter") in ["lora", "qlora"]: # Skip if already set, using unsloth optimizations, or using 8-bit unsloth_fields = ["unsloth_lora_mlp", "unsloth_lora_qkv", "unsloth_lora_o"] From 89ca14d9a089d8a1e1735415592863c44f4241e4 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 30 Apr 2025 11:35:45 -0400 Subject: [PATCH 0612/1405] ensure we pass axolotl extras to the Dockerfile so vllm is included in shipped images (#2599) --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 891e31dba3..756b288303 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,6 +62,7 @@ jobs: CUDA=${{ matrix.cuda }} PYTORCH_VERSION=${{ matrix.pytorch }} AXOLOTL_ARGS=${{ matrix.axolotl_args }} + AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}} file: ./docker/Dockerfile push: ${{ github.event_name != 'pull_request' }} tags: | From 5e949eaa07c80980d03b11d675027e3b4cd7f41b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 30 Apr 2025 13:11:03 -0400 Subject: [PATCH 0613/1405] replace zero_only with simpler if statement (#2592) --- .github/workflows/multi-gpu-e2e.yml | 1 + .github/workflows/tests.yml | 3 +++ .../cut_cross_entropy/__init__.py | 4 +-- src/axolotl/integrations/liger/__init__.py | 4 +-- src/axolotl/utils/distributed.py | 25 +++++++++---------- src/axolotl/utils/models.py | 4 +-- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index edc1c7d792..ffb3577ea6 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -9,6 +9,7 @@ on: - 'pyproject.toml' - '.github/workflows/multi-gpu-e2e.yml' - 'src/axolotl/core/trainers/mixins/sequence_parallel.py' + - 'src/axolotl/utils/distributed.py' workflow_dispatch: schedule: - cron: '0 0 * * 1,4' # Runs at 00:00 UTC every monday & thursday diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e54850af74..e633215e42 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,6 +27,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +env: + TRANSFORMERS_IS_CI: "yes" + jobs: pre-commit: name: pre-commit diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 4792adb6f7..7420674fa9 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -25,7 +25,7 @@ from axolotl.integrations.base import BasePlugin from axolotl.utils import get_pytorch_version -from axolotl.utils.distributed import zero_only +from axolotl.utils.distributed import is_main_process from .args import CutCrossEntropyArgs # pylint: disable=unused-import. # noqa: F401 @@ -76,7 +76,7 @@ def pre_model_load(self, cfg): cce_patch, ) - with zero_only(): + if is_main_process(use_environ=True): LOG.info( f"Applying Cut Cross Entropy to model type: {cfg.model_config_type}" ) diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index 4e8d00552c..50df7a408d 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -23,8 +23,8 @@ import sys from axolotl.integrations.base import BasePlugin +from axolotl.utils.distributed import is_main_process -from ...utils.distributed import zero_only from .args import LigerArgs # pylint: disable=unused-import. # noqa: F401 from .utils import patch_with_compile_disable @@ -85,7 +85,7 @@ def pre_model_load(self, cfg): kwargs["geglu"] = cfg.liger_glu_activation elif "swiglu" in liger_fn_sig.parameters: kwargs["swiglu"] = cfg.liger_glu_activation - with zero_only(): + if is_main_process(use_environ=True): LOG.info( f"Applying LIGER to {cfg.model_config_type} with kwargs: {kwargs}" ) diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index acd2566d0d..8c52102c89 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -69,17 +69,27 @@ def barrier(): dist.barrier() -def is_main_process(): +def is_main_process(use_environ=False): """ Check if the current process is the main process. If not in distributed mode, always return `True`. + + Args: + - use_environ (bool, optional): Use environment variable to determine main process. + + Returns: + - bool: `True` if the current process is the main process, `False` otherwise. """ + if use_environ: + return os.environ.get("LOCAL_RANK", "0") == "0" if not is_distributed(): return True return dist.get_rank() == 0 -def is_local_main_process(): +def is_local_main_process(use_environ=False): + if use_environ: + return os.environ.get("LOCAL_RANK", "0") == "0" return PartialState().is_local_main_process @@ -99,17 +109,6 @@ def cleanup_distributed(): torch.distributed.destroy_process_group() -@contextmanager -def zero_only(): - """ - Context manager that only runs the enclosed block on the main rank. - """ - if is_main_process(): - yield - else: - yield None - - @contextmanager def zero_first(is_main): """ diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index ab4cc19bb5..e88de1bad4 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -68,7 +68,7 @@ get_device_count, get_device_type, is_local_main_process, - zero_only, + is_main_process, ) from axolotl.utils.gradient_checkpointing import hf_grad_checkpoint_offload_wrapper from axolotl.utils.lora_embeddings import get_linear_embedding_layers @@ -437,7 +437,7 @@ def load_tokenizer(cfg): {"additional_special_tokens": additional_special_tokens} ) - with zero_only(): + if is_main_process(use_environ=True): LOG.debug(f"EOS: {tokenizer.eos_token_id} / {tokenizer.eos_token}") LOG.debug(f"BOS: {tokenizer.bos_token_id} / {tokenizer.bos_token}") LOG.debug(f"PAD: {tokenizer.pad_token_id} / {tokenizer.pad_token}") From 24ff5f53f8797a84f6785540489356dea2cec864 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 30 Apr 2025 13:11:12 -0400 Subject: [PATCH 0614/1405] additional args for grpo config/trainer (#2598) --- src/axolotl/core/trainers/grpo/__init__.py | 12 ++++++++++++ src/axolotl/utils/schemas/config.py | 12 ++++++++++++ src/axolotl/utils/schemas/trl.py | 22 ++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index c82fe69f2e..b64d087b0a 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -70,6 +70,13 @@ def set_training_args_kwargs(cls, cfg): if trl.scale_rewards is not None: grpo_args_kwargs["scale_rewards"] = trl.scale_rewards + if trl.loss_type is not None: + grpo_args_kwargs["loss_type"] = trl.loss_type + if trl.mask_truncated_completions is not None: + grpo_args_kwargs["mask_truncated_completions"] = ( + trl.mask_truncated_completions + ) + if trl.temperature is not None: grpo_args_kwargs["temperature"] = trl.temperature if trl.top_p is not None: @@ -85,6 +92,11 @@ def set_training_args_kwargs(cls, cfg): grpo_args_kwargs["num_iterations"] = trl.num_iterations if trl.epsilon is not None: grpo_args_kwargs["epsilon"] = trl.epsilon + if trl.epsilon_high is not None: + grpo_args_kwargs["epsilon_high"] = trl.epsilon_high + + if trl.use_liger_loss is not None: + grpo_args_kwargs["use_liger_loss"] = trl.use_liger_loss return grpo_args_kwargs diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 7f35de81d6..bc7ee7e724 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1150,6 +1150,18 @@ def check_kto_config(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_grpo_peft_liger(cls, data): + if ( + data.get("rl") == "grpo" + and data.get("trl", {}) + and data.get("trl").get("use_liger_loss") + and data.get("adapter") + ): + raise ValueError("PEFT + GRPO + Liger is not yet supported") + return data + @model_validator(mode="after") def check_sequence_parallel_degree(self): if not self.sequence_parallel_degree: diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index a051fb0abf..c581b265e9 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -133,3 +133,25 @@ class TRLConfig(BaseModel): "description": "Epsilon value for clipping in the GRPO algorithm." }, ) + epsilon_high: float | None = Field( + default=None, + json_schema_extra={ + "description": "Upper-bound epsilon value for clipping in the GRPO algorithm." + }, + ) + use_liger_loss: bool | None = Field( + default=None, + json_schema_extra={"description": "Whether to use Liger loss for GRPO."}, + ) + loss_type: str | None = Field( + default=None, + json_schema_extra={ + "description": "Specifies the loss formulation to use. Supported values are `grpo`, `bnpo`, and `dr_grpo`." + }, + ) + mask_truncated_completions: bool = Field( + default=False, + json_schema_extra={ + "description": "When enabled, truncated completions are excluded from the loss calculation." + }, + ) From 6ba5c0ed2c42a0e069b28c83646ee5a2a6904430 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 30 Apr 2025 18:27:39 -0400 Subject: [PATCH 0615/1405] use latest hf-xet and don't install vllm for torch 2.7.0 (#2603) * use latest hf-xet and don't install vllm for torch 2.7.0 * fix runpod hub tests --- .github/workflows/main.yml | 2 +- .runpod/tests.json | 90 ++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 +- 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 .runpod/tests.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 756b288303..4fcf083523 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.7.0 - axolotl_extras: vllm + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/.runpod/tests.json b/.runpod/tests.json new file mode 100644 index 0000000000..6cc18daec9 --- /dev/null +++ b/.runpod/tests.json @@ -0,0 +1,90 @@ +{ + "tests": [ + { + "name": "quick_smoke_test_sft", + "input": { + "user_id": "user", + "model_id": "llama-test", + "run_id": "llama-test", + "credentials": { + "wandb_api_key": "", + "hf_token": "" + }, + "args": { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "model_type": "AutoModelForCausalLM", + "tokenizer_type": "AutoTokenizer", + "load_in_4bit": true, + "strict": false, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + "split": "train[:10%]" + } + ], + "val_set_size": 0.02, + "output_dir": "./outputs/lora-out", + "sequence_len": 4096, + "sample_packing": true, + "eval_sample_packing": false, + "pad_to_sequence_len": true, + "adapter": "qlora", + "lora_r": 32, + "lora_alpha": 64, + "lora_dropout": 0.05, + "lora_target_linear": true, + "lora_modules_to_save": [ + "embed_tokens", + "lm_head" + ], + "gradient_accumulation_steps": 2, + "micro_batch_size": 1, + "num_epochs": 1, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "learning_rate": 0.0002, + "train_on_inputs": false, + "group_by_length": false, + "bf16": "auto", + "tf32": true, + "gradient_checkpointing": true, + "logging_steps": 1, + "flash_attention": true, + "warmup_steps": 1, + "evals_per_epoch": 1, + "eval_max_new_tokens": 128, + "saves_per_epoch": 1, + "weight_decay": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>" + }, + "max_steps": 20 + } + }, + "timeout": 100000 + } + ], + "config": { + "gpuTypeId": "NVIDIA GeForce RTX 4090", + "gpuCount": 1, + "containerDiskInGb": 200, + "env": [ + { + "key": "TOKENIZER", + "value": "" + }, + { + "key": "DISABLE_LOG_STATS", + "value": "true" + } + ], + "allowedCudaVersions": [ + "12.8", + "12.7", + "12.6", + "12.5", + "12.4" + ] + } +} diff --git a/requirements.txt b/requirements.txt index 931dec3452..327b5ee2d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ accelerate==1.6.0 datasets==3.5.0 deepspeed>=0.15.4 trl==0.17.0 -hf_xet==1.0.0 +hf_xet==1.1.0 hqq==0.2.5 optimum==1.16.2 From c3f2b1c5c2aad469443ae37cce7af475ec70525f Mon Sep 17 00:00:00 2001 From: Dhruv Mullick Date: Wed, 30 Apr 2025 19:00:30 -0600 Subject: [PATCH 0616/1405] Add num_completions_to_print for trl and grpo (#2604) --- src/axolotl/core/trainers/grpo/__init__.py | 1 + src/axolotl/utils/schemas/trl.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index b64d087b0a..078fdcb227 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -63,6 +63,7 @@ def set_training_args_kwargs(cls, cfg): grpo_args_kwargs["max_completion_length"] = trl.max_completion_length grpo_args_kwargs["log_completions"] = trl.log_completions + grpo_args_kwargs["num_completions_to_print"] = trl.num_completions_to_print if trl.reward_weights: grpo_args_kwargs["reward_weights"] = trl.reward_weights diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index c581b265e9..37b71dba89 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -67,6 +67,12 @@ class TRLConfig(BaseModel): default=False, json_schema_extra={"description": "Whether to log completions"}, ) + num_completions_to_print: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of completions to print. If `log_completions` is `True`, this will be the number of completions logged." + }, + ) sync_ref_model: bool | None = Field( default=False, json_schema_extra={ From e963990ad7bdf27476f097003f8360a5762035c7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 1 May 2025 09:41:32 -0400 Subject: [PATCH 0617/1405] add missing __init__ for lr monkeypatch fix (#2609) --- src/axolotl/monkeypatch/trainer/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/axolotl/monkeypatch/trainer/__init__.py diff --git a/src/axolotl/monkeypatch/trainer/__init__.py b/src/axolotl/monkeypatch/trainer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 996fc124e5ed535e498495f6abe814b3a23620aa Mon Sep 17 00:00:00 2001 From: Rahul Tuli Date: Thu, 1 May 2025 11:25:16 -0500 Subject: [PATCH 0618/1405] Add: Sparse Finetuning Integration with llmcompressor (#2479) * Add: SFTPlugin with llmcompressor * Update: review comments! * Add:llmcompressor instalable * pre commit hooks * Use: warning over warn * Revert: TODO's * Update llmcompressor version to latest * Apply suggestions from @markurtz Co-authored-by: Mark Kurtz * Address review comments from @markurtz * Add: llcompressor installable * Rename: sft.yaml to sparse-finetuning.yaml * Use: absolute import * Update model config * Move: LLMCompressorPlugin into it's own submodule * Add: `llm_compressor` integration documentation * Rebase and updates! * Tests, Style, Updates * Add: .qmd file * Address Review Comments: * deleted redundant docs/llm_compressor.qmd * incorporated feedback in integration README.md * added llmcompressor integration to docs/custom_integrations.qmd Signed-off-by: Rahul Tuli * Add: line about further optimizations using llmcompressor Signed-off-by: Rahul Tuli * Apply patch from @winglian Signed-off-by: Rahul Tuli * Fix: Test Signed-off-by: Rahul Tuli * additional fixes for docker and saving compressed * split llmcompressor from vllm checks * Reset session between tests Signed-off-by: Rahul Tuli * move decorator to test method instead of class * make sure to reset the session after each test * move import of llmcompressor to reset session inside test --------- Signed-off-by: Rahul Tuli Co-authored-by: Mark Kurtz Co-authored-by: Wing Lian --- .github/workflows/tests.yml | 12 ++ docs/custom_integrations.qmd | 3 +- examples/llama-3/sparse-finetuning.yaml | 77 ++++++++ setup.py | 3 + .../integrations/llm_compressor/README.md | 108 +++++++++++ .../integrations/llm_compressor/__init__.py | 5 + .../integrations/llm_compressor/args.py | 40 ++++ .../integrations/llm_compressor/plugin.py | 171 ++++++++++++++++++ .../integrations/llm_compressor/utils.py | 40 ++++ src/axolotl/train.py | 15 ++ src/axolotl/utils/models.py | 16 ++ tests/e2e/integrations/test_llm_compressor.py | 111 ++++++++++++ tests/e2e/utils.py | 20 +- 13 files changed, 619 insertions(+), 2 deletions(-) create mode 100644 examples/llama-3/sparse-finetuning.yaml create mode 100644 src/axolotl/integrations/llm_compressor/README.md create mode 100644 src/axolotl/integrations/llm_compressor/__init__.py create mode 100644 src/axolotl/integrations/llm_compressor/args.py create mode 100644 src/axolotl/integrations/llm_compressor/plugin.py create mode 100644 src/axolotl/integrations/llm_compressor/utils.py create mode 100644 tests/e2e/integrations/test_llm_compressor.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e633215e42..c2c085fa0a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -261,6 +261,18 @@ jobs: fail-fast: false matrix: include: + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.6.0 + num_gpus: 1 + axolotl_extras: llmcompressor + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.4.1 + num_gpus: 1 + axolotl_extras: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" diff --git a/docs/custom_integrations.qmd b/docs/custom_integrations.qmd index cb4aef9ca2..023f09732a 100644 --- a/docs/custom_integrations.qmd +++ b/docs/custom_integrations.qmd @@ -49,7 +49,8 @@ sections = [ ("Knowledge Distillation (KD)", "kd"), ("Liger Kernels", "liger"), ("Language Model Evaluation Harness (LM Eval)", "lm_eval"), - ("Spectrum", "spectrum") + ("Spectrum", "spectrum"), + ("LLMCompressor", "llm_compressor") ] for section_name, folder_name in sections: diff --git a/examples/llama-3/sparse-finetuning.yaml b/examples/llama-3/sparse-finetuning.yaml new file mode 100644 index 0000000000..1bbb880289 --- /dev/null +++ b/examples/llama-3/sparse-finetuning.yaml @@ -0,0 +1,77 @@ +base_model: neuralmagic/Sparse-Llama-3.1-8B-2of4 + +plugins: + - axolotl.integrations.llm_compressor.LLMCompressorPlugin + +load_in_8bit: false +load_in_4bit: false +strict: false + +datasets: + - path: tatsu-lab/alpaca + type: alpaca +dataset_prepared_path: last_run_prepared +val_set_size: 0.05 +output_dir: ./outputs/out + +sequence_len: 4096 +sample_packing: true +pad_to_sequence_len: true +eval_sample_packing: false + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 8 +micro_batch_size: 1 +num_epochs: 1 +optimizer: paged_adamw_8bit +lr_scheduler: cosine +learning_rate: 2e-5 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +early_stopping_patience: +resume_from_checkpoint: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_steps: 100 +evals_per_epoch: 2 +eval_table_size: +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: + pad_token: <|end_of_text|> + +llmcompressor: + recipe: + finetuning_stage: + finetuning_modifiers: + ConstantPruningModifier: + targets: [ + 're:.*q_proj.weight', + 're:.*k_proj.weight', + 're:.*v_proj.weight', + 're:.*o_proj.weight', + 're:.*gate_proj.weight', + 're:.*up_proj.weight', + 're:.*down_proj.weight', + ] + start: 0 + save_compressed: true diff --git a/setup.py b/setup.py index 5b66a2ea79..d6009b8d5c 100644 --- a/setup.py +++ b/setup.py @@ -149,6 +149,9 @@ def get_package_version(): "vllm": [ "vllm==0.7.2", ], + "llmcompressor": [ + "llmcompressor==0.5.1", + ], } install_requires, dependency_links, extras_require_build = parse_requirements( diff --git a/src/axolotl/integrations/llm_compressor/README.md b/src/axolotl/integrations/llm_compressor/README.md new file mode 100644 index 0000000000..16eff804dd --- /dev/null +++ b/src/axolotl/integrations/llm_compressor/README.md @@ -0,0 +1,108 @@ +# LLMCompressor Integration + +Fine-tune sparsified models in Axolotl using Neural Magic's [LLMCompressor](https://github.com/vllm-project/llm-compressor). + +This integration enables fine-tuning of models sparsified using LLMCompressor within the Axolotl training framework. By combining LLMCompressor's model compression capabilities with Axolotl's distributed training pipelines, users can efficiently fine-tune sparse models at scale. + +It uses Axolotl’s plugin system to hook into the fine-tuning flows while maintaining sparsity throughout training. + +--- + +## Requirements + +- Axolotl with `llmcompressor` extras: + + ```bash + pip install "axolotl[llmcompressor]" + ``` + +- Requires `llmcompressor >= 0.5.1` + +This will install all necessary dependencies to fine-tune sparsified models using the integration. + +--- + +## Usage + +To enable sparse fine-tuning with this integration, include the plugin in your Axolotl config: + +```yaml +plugins: + - axolotl.integrations.llm_compressor.LLMCompressorPlugin + +llmcompressor: + recipe: + finetuning_stage: + finetuning_modifiers: + ConstantPruningModifier: + targets: [ + 're:.*q_proj.weight', + 're:.*k_proj.weight', + 're:.*v_proj.weight', + 're:.*o_proj.weight', + 're:.*gate_proj.weight', + 're:.*up_proj.weight', + 're:.*down_proj.weight', + ] + start: 0 + save_compressed: true +# ... (other training arguments) +``` + +This plugin **does not apply pruning or sparsification itself** — it is intended for **fine-tuning models that have already been sparsified**. + +Pre-sparsified checkpoints can be: +- Generated using [LLMCompressor](https://github.com/vllm-project/llm-compressor) +- Downloaded from [Neural Magic's Hugging Face page](https://huggingface.co/neuralmagic) +- Any custom LLM with compatible sparsity patterns that you've created yourself + +To learn more about writing and customizing LLMCompressor recipes, refer to the official documentation: +[https://github.com/vllm-project/llm-compressor/blob/main/README.md](https://github.com/vllm-project/llm-compressor/blob/main/README.md) + +### Storage Optimization with save_compressed + +Setting `save_compressed: true` in your configuration enables saving models in a compressed format, which: +- Reduces disk space usage by approximately 40% +- Maintains compatibility with vLLM for accelerated inference +- Maintains compatibility with llmcompressor for further optimization (example: quantization) + +This option is highly recommended when working with sparse models to maximize the benefits of model compression. + +### Example Config + +See [`examples/llama-3/sparse-finetuning.yaml`](examples/llama-3/sparse-finetuning.yaml) for a complete example. + +--- + +## Inference with vLLM + +After fine-tuning your sparse model, you can leverage vLLM for efficient inference. +You can also use LLMCompressor to apply additional quantization to your fine-tuned +sparse model before inference for even greater performance benefits.: + +```python +from vllm import LLM, SamplingParams + +prompts = [ + "Hello, my name is", + "The president of the United States is", + "The capital of France is", + "The future of AI is", +] +sampling_params = SamplingParams(temperature=0.8, top_p=0.95) +llm = LLM("path/to/your/sparse/model") +outputs = llm.generate(prompts, sampling_params) + +for output in outputs: + prompt = output.prompt + generated_text = output.outputs[0].text + print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}") +``` + +For more details on vLLM's capabilities and advanced configuration options, see the [official vLLM documentation](https://docs.vllm.ai/). + +## Learn More + +For details on available sparsity and quantization schemes, fine-tuning recipes, and usage examples, visit the official LLMCompressor repository: + +[https://github.com/vllm-project/llm-compressor](https://github.com/vllm-project/llm-compressor) diff --git a/src/axolotl/integrations/llm_compressor/__init__.py b/src/axolotl/integrations/llm_compressor/__init__.py new file mode 100644 index 0000000000..fe799d3c0f --- /dev/null +++ b/src/axolotl/integrations/llm_compressor/__init__.py @@ -0,0 +1,5 @@ +"""Integration entry point for the LLMCompressor plugin.""" + +from .plugin import LLMCompressorPlugin + +__all__ = ["LLMCompressorPlugin"] diff --git a/src/axolotl/integrations/llm_compressor/args.py b/src/axolotl/integrations/llm_compressor/args.py new file mode 100644 index 0000000000..4c0e4cac31 --- /dev/null +++ b/src/axolotl/integrations/llm_compressor/args.py @@ -0,0 +1,40 @@ +""" +LLMCompressor and Sparse Finetuning config models. +""" + +from typing import Any + +from pydantic import BaseModel, Field +from typing_extensions import Annotated + + +class CompressionArgs(BaseModel): + """Sparse Finetuning config for LLMCompressor.""" + + # Typing for recipe is set to Any due to: + # https://github.com/vllm-project/llm-compressor/issues/1319 + recipe: Annotated[ + Any, + Field( + description="The recipe containing the compression algorithms and hyperparameters to apply." + ), + ] + + save_compressed: Annotated[ + bool, + Field( + default=False, + description="Whether to save the compressed model after training.", + ), + ] + + +class LLMCompressorArgs(BaseModel): + """LLMCompressor configuration BaseModel.""" + + llmcompressor: Annotated[ + CompressionArgs, + Field( + description="Arguments enabling compression pathways through the LLM Compressor plugins" + ), + ] diff --git a/src/axolotl/integrations/llm_compressor/plugin.py b/src/axolotl/integrations/llm_compressor/plugin.py new file mode 100644 index 0000000000..d986d51f47 --- /dev/null +++ b/src/axolotl/integrations/llm_compressor/plugin.py @@ -0,0 +1,171 @@ +""" +Sparse Finetuning plugin for Axolotl — enables handling of sparse neural networks +by maintaining masks for zero weights during training. +""" + +import logging +from functools import wraps +from typing import Any, Callable, Concatenate, ParamSpec, TypeVar + +from llmcompressor import active_session, create_session +from llmcompressor.core import callbacks as session_callbacks +from llmcompressor.recipe import Recipe +from torch.nn import Module +from transformers.trainer import Trainer +from transformers.trainer_callback import TrainerCallback, TrainerControl, TrainerState +from transformers.training_args import TrainingArguments + +from axolotl.integrations.base import BasePlugin + +P = ParamSpec("P") # Params for generic function signatures +R = TypeVar("R") # Return type for generic function signatures + +LOG = logging.getLogger("axolotl.integrations.llm_compressor") + + +class LLMCompressorCallbackHandler(TrainerCallback): + """ + Trainer callback for Sparse Finetuning. + Maintains sparsity patterns during training by applying masks after optimization steps, + ensuring zero-weight updates are canceled out. + """ + + def __init__(self, trainer: Trainer, recipe: Any): + """ + Initialize the Sparse Finetuning callback handler. + + Args: + trainer (Trainer): Huggingface Trainer instance. + recipe (Recipe | dict): Sparse finetuning recipe to apply. + """ + super().__init__() + self.trainer = trainer + self.recipe = ( + Recipe.model_validate(recipe) if not isinstance(recipe, Recipe) else recipe + ) + self.original_compute_loss = trainer.compute_loss + self.trainer.compute_loss = compute_loss_wrapper(self.trainer.compute_loss) + create_session() + + def on_train_begin( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ) -> None: + """ + Called at the beginning of training. Initializes the compression session. + + Args: + args (TrainingArguments): Training arguments. + state (TrainerState): Trainer state. + control (TrainerControl): Trainer control. + """ + super().on_train_begin(args, state, control, **kwargs) + self.trainer.accelerator.wait_for_everyone() + active_session().initialize( + model=self.trainer.model, + optimizer=self.trainer.optimizer, + start=state.epoch, + recipe=self.recipe, + ) + self.trainer.accelerator.wait_for_everyone() + + def on_step_begin( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ) -> None: + """ + Called at the beginning of a training step. Triggers batch_start callback. + """ + super().on_step_begin(args, state, control, **kwargs) + session_callbacks.batch_start() + + def on_step_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ) -> None: + """ + Called at the end of a training step. Triggers optimizer and batch_end callbacks. + """ + super().on_step_end(args, state, control, **kwargs) + session_callbacks.optim_pre_step() + session_callbacks.optim_post_step() + session_callbacks.batch_end() + + def on_train_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ) -> None: + """ + Called at the end of training. Finalizes the compression session. + """ + super().on_train_end(args, state, control, **kwargs) + active_session().finalize() + self.trainer.compute_loss_func = self.original_compute_loss + + +class LLMCompressorPlugin(BasePlugin): + """ + Sparse Finetuning plugin for Axolotl integration. + """ + + def get_input_args(self) -> str: + """ + Returns the path to the plugin's argument definition. + + Returns: + str: Dotted path to the LLMCompressorArgs class. + """ + return "axolotl.integrations.llm_compressor.args.LLMCompressorArgs" + + def add_callbacks_post_trainer(self, cfg: Any, trainer: Trainer) -> list: + """ + Adds Sparse Finetuning callback to the Trainer instance. + + Args: + cfg (Any): Configuration object containing the sparse recipe. + trainer (Trainer): Huggingface Trainer instance. + + Returns: + list: List containing the configured callback instances. + """ + LOG.info("Adding Sparse Finetuning callback to the trainer") + callback = LLMCompressorCallbackHandler( + trainer=trainer, + recipe=cfg.llmcompressor.recipe, + ) + return [callback] + + +def compute_loss_wrapper( + compute_loss_func: Callable[Concatenate[Module, P], R], +) -> Callable[Concatenate[Module, P], R]: + """ + Wraps the loss computation function to trigger the loss_calculated callback. + + Args: + compute_loss_func (Callable): Original loss computation function. + + Returns: + Callable: Wrapped function that also invokes the loss_calculated callback. + """ + + @wraps(compute_loss_func) + def compute_and_notify(model: Module, *args: P.args, **kwargs: P.kwargs) -> R: + loss = compute_loss_func(model, *args, **kwargs) + if active_session().lifecycle.initialized_ and model.training: + session_callbacks.loss_calculated(loss=loss) + return loss + + return compute_and_notify diff --git a/src/axolotl/integrations/llm_compressor/utils.py b/src/axolotl/integrations/llm_compressor/utils.py new file mode 100644 index 0000000000..f04454e5b3 --- /dev/null +++ b/src/axolotl/integrations/llm_compressor/utils.py @@ -0,0 +1,40 @@ +"""Utilities for llmcompressor integration with axolotl.""" + +from typing import Union + +from llmcompressor.transformers.sparsification.compressed_tensors_utils import ( + modify_save_pretrained, +) +from transformers import PreTrainedModel, Trainer + + +def save_compressed_model( + model: PreTrainedModel, + output_dir: Union[str, bytes], + trainer: Trainer, + safe_serialization: bool = False, + save_compressed: bool = False, +) -> None: + """ + Synchronize processes, apply compression hooks, and save the model. + + Args: + model (PreTrainedModel): The model to be saved. + output_dir (str or bytes): Path where the model files will be written. + trainer (Trainer): Hugging Face Trainer for process synchronization. + safe_serialization (bool): Use safe serialization if True. + save_compressed (bool): Write compressed tensors if True. + """ + trainer.accelerator.wait_for_everyone() + + # Only the main process writes the files + if not trainer.accelerator.is_main_process: + return + + modify_save_pretrained(model) + model.save_pretrained( + output_dir, + safe_serialization=safe_serialization, + save_compressed=save_compressed, + skip_sparsity_compression_stats=not save_compressed, + ) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 7896239de1..808d3af64b 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -296,8 +296,23 @@ def save_trained_model( trainer.model.save_pretrained( cfg.output_dir, safe_serialization=safe_serialization ) + model.save_pretrained(cfg.output_dir, safe_serialization=safe_serialization) + if hasattr(cfg, "llmcompressor") and cfg.llmcompressor: + # TODO: add integration support so this can be implemented completely within the plugin + from axolotl.integrations.llm_compressor.utils import ( + save_compressed_model, + ) + + save_compressed_model( + model=model, + output_dir=cfg.output_dir, + trainer=trainer, + safe_serialization=safe_serialization, + save_compressed=cfg.llmcompressor.save_compressed, + ) + def create_model_card(cfg: DictDefault, trainer: Trainer): """ diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index e88de1bad4..ba71ea4598 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -141,6 +141,22 @@ def check_model_config(cfg: DictDefault, model_config: PretrainedConfig): hasattr(model_config, "quantization_config") and model_config.quantization_config ) + + # Detect compressed-tensors config + is_compressed_tensors_config = ( + quant_config_exists + and model_config.quantization_config.get("quant_method") == "compressed-tensors" + ) + + if is_compressed_tensors_config: + if model_config.quantization_config.get("config_groups"): + LOG.warning( + "Found `config_groups` in a compressed-tensors config. " + "QAT integration with llmcompressor is not tested." + ) + # Skip further quant checks for compressed-tensors + return + quant_config_method_is_gptq = ( quant_config_exists and "quant_method" in model_config.quantization_config diff --git a/tests/e2e/integrations/test_llm_compressor.py b/tests/e2e/integrations/test_llm_compressor.py new file mode 100644 index 0000000000..20bf821bfd --- /dev/null +++ b/tests/e2e/integrations/test_llm_compressor.py @@ -0,0 +1,111 @@ +""" +E2E smoke tests for LLMCompressorPlugin integration +""" + +from pathlib import Path + +import pytest + +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, prepare_plugins, validate_config +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import ( + check_model_output_exists, + require_llmcompressor, + require_torch_2_4_1, +) + +MODELS = [ + "nm-testing/llama2.c-stories42M-pruned2.4-compressed", + "nm-testing/llama2.c-stories42M-gsm8k-sparse-only-compressed", +] + + +@pytest.mark.parametrize( + "base_model", MODELS, ids=["no-checkpoint-recipe", "with-checkpoint-recipe"] +) +@pytest.mark.parametrize( + "save_compressed", [True, False], ids=["save_compressed", "save_uncompressed"] +) +class TestLLMCompressorIntegration: + """ + e2e tests for axolotl.integrations.llm_compressor.LLMCompressorPlugin + """ + + @require_llmcompressor + @require_torch_2_4_1 + def test_llmcompressor_plugin( + self, temp_dir, base_model: str, save_compressed: bool + ): + from llmcompressor import active_session + + # core cfg + cfg = DictDefault( + { + "base_model": base_model, + "plugins": ["axolotl.integrations.llm_compressor.LLMCompressorPlugin"], + "sequence_len": 1024, + "val_set_size": 0.05, + "special_tokens": {"pad_token": "<|endoftext|>"}, + "datasets": [{"path": "mhenrichsen/alpaca_2k_test", "type": "alpaca"}], + "num_epochs": 1, + "micro_batch_size": 2, + "gradient_accumulation_steps": 2, + "output_dir": temp_dir, + "learning_rate": 1e-5, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + "max_steps": 5, + "llmcompressor": { + "recipe": { + "finetuning_stage": { + "finetuning_modifiers": { + "ConstantPruningModifier": { + "targets": [ + "re:.*q_proj.weight", + "re:.*k_proj.weight", + "re:.*v_proj.weight", + "re:.*o_proj.weight", + "re:.*gate_proj.weight", + "re:.*up_proj.weight", + "re:.*down_proj.weight", + ], + "start": 0, + }, + }, + }, + }, + "save_compressed": save_compressed, + }, + } + ) + + prepare_plugins(cfg) + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + try: + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) + _check_llmcompressor_model_outputs(temp_dir, save_compressed) + finally: + active_session().reset() + + +def _check_llmcompressor_model_outputs(temp_dir, save_compressed): + if save_compressed: + assert (Path(temp_dir) / "recipe.yaml").exists() + + from compressed_tensors import ModelCompressor + from compressed_tensors.config import Sparse24BitMaskConfig + + compressor = ModelCompressor.from_pretrained(temp_dir) + assert compressor is not None + assert isinstance(compressor.sparsity_config, Sparse24BitMaskConfig) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 2fbf333c4a..61df1d8fef 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -105,7 +105,25 @@ def is_vllm_installed(): return False return unittest.skipUnless( - is_vllm_installed(), "test requires a vllm to be installed" + is_vllm_installed(), "test requires vllm to be installed" + )(test_case) + + +def require_llmcompressor(test_case): + """ + Decorator marking a test that requires a llmcompressor to be installed + """ + + def is_llmcompressor_installed(): + try: + import llmcompressor # pylint: disable=unused-import # noqa: F401 + + return True + except ImportError: + return False + + return unittest.skipUnless( + is_llmcompressor_installed(), "test requires llmcompressor to be installed" )(test_case) From fee3c13bb59f1f6be490e05dc2b77bb1f98a9d21 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 1 May 2025 12:58:00 -0400 Subject: [PATCH 0619/1405] Logging config for colab (#2611) * only configure logging on cli to play nicely with colab * allow reloading the config on the fly from a dict * make sure to use dict for yaml * reuse existing function for load * make cli args optional * mps fix and respect max_steps --- src/axolotl/cli/__init__.py | 3 ++ src/axolotl/cli/checks.py | 3 -- src/axolotl/cli/config.py | 32 +++++++++++++------ src/axolotl/cli/utils.py | 2 -- src/axolotl/common/datasets.py | 7 ++-- src/axolotl/core/trainer_builder.py | 2 +- src/axolotl/evaluate.py | 2 -- .../monkeypatch/attention/ring_attn/patch.py | 2 -- src/axolotl/train.py | 2 -- src/axolotl/utils/config/__init__.py | 2 +- src/axolotl/utils/trainer.py | 2 ++ 11 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index b20e4f085a..8955eca3e9 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -2,4 +2,7 @@ import os +from axolotl.logging_config import configure_logging + os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1" +configure_logging() diff --git a/src/axolotl/cli/checks.py b/src/axolotl/cli/checks.py index cc3ed0d9f1..47348240eb 100644 --- a/src/axolotl/cli/checks.py +++ b/src/axolotl/cli/checks.py @@ -8,9 +8,6 @@ from huggingface_hub import HfApi from huggingface_hub.utils import LocalTokenNotFoundError -from axolotl.logging_config import configure_logging - -configure_logging() LOG = logging.getLogger(__name__) diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index 64bf402b9f..8f1fe7185d 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -5,6 +5,7 @@ import os import tempfile from pathlib import Path +from tempfile import NamedTemporaryFile from typing import Union from urllib.parse import urlparse @@ -158,7 +159,9 @@ def plugin_set_cfg(cfg: DictDefault): plugin_manager.cfg = cfg -def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs) -> DictDefault: +def load_cfg( + config: str | Path | DictDefault = Path("examples/"), **kwargs +) -> DictDefault: """ Loads the `axolotl` configuration stored at `config`, validates it, and performs various setup. @@ -170,13 +173,24 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs) -> DictDefa Returns: `DictDefault` mapping configuration keys to values. """ - config = check_remote_config(config) - if Path(config).is_dir(): - config = choose_config(Path(config)) - - # Load the config from the yaml file - with open(config, encoding="utf-8") as file: - cfg: DictDefault = DictDefault(yaml.safe_load(file)) + if isinstance(config, (str, Path)): + config = check_remote_config(config) + if Path(config).is_dir(): + config = choose_config(Path(config)) + + # Load the config from the yaml file + with open(config, encoding="utf-8") as file: + cfg: DictDefault = DictDefault(yaml.safe_load(file)) + + cfg.axolotl_config_path = config + else: + cfg = config + with NamedTemporaryFile( + mode="w", delete=False, suffix=".yml", prefix="axolotl_config_" + ) as temp_file: + temp_file.write(yaml.dump(config.to_dict())) + temp_file.close() + cfg.axolotl_config_path = temp_file.name # If there are any options passed in the cli, if it is something that seems valid # from the yaml, then overwrite the value @@ -190,8 +204,6 @@ def load_cfg(config: Union[str, Path] = Path("examples/"), **kwargs) -> DictDefa else: cfg[k] = kwargs[k] - cfg.axolotl_config_path = config - try: device_props = torch.cuda.get_device_properties("cuda") gpu_version = "sm_" + str(device_props.major) + str(device_props.minor) diff --git a/src/axolotl/cli/utils.py b/src/axolotl/cli/utils.py index 7cc4d27448..ee00db39d8 100644 --- a/src/axolotl/cli/utils.py +++ b/src/axolotl/cli/utils.py @@ -20,11 +20,9 @@ ProcessorMixin, ) -from axolotl.logging_config import configure_logging from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_model, load_processor, load_tokenizer -configure_logging() LOG = logging.getLogger(__name__) diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index 3e712f7726..2ab405ef1f 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -47,7 +47,7 @@ def sample_dataset(dataset: Dataset, num_samples: int) -> Dataset: def load_datasets( *, cfg: DictDefault, - cli_args: Union[PreprocessCliArgs, TrainerCliArgs], + cli_args: PreprocessCliArgs | TrainerCliArgs | None = None, ) -> TrainDatasetMeta: """ Loads one or more training or evaluation datasets, calling @@ -64,7 +64,8 @@ def load_datasets( tokenizer = load_tokenizer(cfg) processor = load_processor(cfg, tokenizer=tokenizer) if cfg.processor_type else None preprocess_iterable = ( - hasattr(cli_args, "iterable") + cli_args + and hasattr(cli_args, "iterable") and cli_args.iterable is not None and cli_args.iterable ) @@ -76,7 +77,7 @@ def load_datasets( preprocess_iterable=preprocess_iterable, ) - if ( + if cli_args and ( cli_args.debug or cfg.debug or cli_args.debug_text_only diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 358058f692..31ee3cccfb 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -488,7 +488,7 @@ def build(self, total_num_steps): # these are all the "standard" kwargs that are def used training_arguments_kwargs["max_steps"] = ( - total_num_steps if self.cfg.max_steps else -1 + self.cfg.max_steps if self.cfg.max_steps else -1 ) training_arguments_kwargs["max_seq_length"] = self.cfg.sequence_len training_arguments_kwargs["per_device_train_batch_size"] = ( diff --git a/src/axolotl/evaluate.py b/src/axolotl/evaluate.py index a6a192bc7f..6d68137308 100644 --- a/src/axolotl/evaluate.py +++ b/src/axolotl/evaluate.py @@ -11,7 +11,6 @@ from datasets import Dataset from transformers.trainer import Trainer -from axolotl.logging_config import configure_logging from axolotl.train import ( TrainDatasetMeta, setup_model_and_tokenizer, @@ -24,7 +23,6 @@ src_dir = os.path.join(project_root, "src") sys.path.insert(0, src_dir) -configure_logging() LOG = get_logger(__name__) diff --git a/src/axolotl/monkeypatch/attention/ring_attn/patch.py b/src/axolotl/monkeypatch/attention/ring_attn/patch.py index b5587ddca6..fa03bd1741 100644 --- a/src/axolotl/monkeypatch/attention/ring_attn/patch.py +++ b/src/axolotl/monkeypatch/attention/ring_attn/patch.py @@ -12,10 +12,8 @@ import torch.distributed as dist from accelerate.logging import get_logger -from axolotl.logging_config import configure_logging from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids -configure_logging() LOG = get_logger(__name__) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 808d3af64b..30d26b7063 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -30,7 +30,6 @@ SequenceParallelContextManager, ) from axolotl.integrations.base import PluginManager -from axolotl.logging_config import configure_logging from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import cleanup_distributed from axolotl.utils.freeze import freeze_layers_except @@ -42,7 +41,6 @@ except ImportError: BetterTransformer = None -configure_logging() LOG = get_logger(__name__) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index e5ea44aa08..35e742a89a 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -67,7 +67,7 @@ def resolve_dtype(cfg): else: LOG.debug("bf16 support not detected, disabling for this configuration.") cfg.bf16 = False - if cfg.fp16 is None: + if cfg.fp16 is None and not cfg.float16: cfg.fp16 = True if cfg.device == "mps": diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 69aaabfa68..96f54b39d1 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -597,6 +597,8 @@ def prepare_optim_env(cfg): os.environ["ACCELERATE_MIXED_PRECISION"] = "bf16" elif cfg.fp16: os.environ["ACCELERATE_MIXED_PRECISION"] = "fp16" + else: + os.environ["ACCELERATE_MIXED_PRECISION"] = "no" def prepare_opinionated_env(cfg): From 6a3e6f8c532033c3a60d272584359982d06c106b Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 2 May 2025 00:21:28 +0700 Subject: [PATCH 0620/1405] fix: run preview-docs only when md/qmd changes (#2606) * fix: run preview-docs only when md/qmd changes * feat: add quarto yaml based on PR feedback --- .github/workflows/preview-docs.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index dbf8d9a000..5af70b0dc5 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -4,6 +4,12 @@ on: pull_request: types: [opened, synchronize, reopened] + # Run the workflow only when one of these files changes + paths: + - '**/*.md' # any Markdown file + - '**/*.qmd' # any Quarto file + - '_quarto.yaml' + permissions: checks: write contents: write From bcb59c70e246692cb90196c719006b4479366da3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 1 May 2025 13:24:38 -0400 Subject: [PATCH 0621/1405] automatically set pad_to_sequence_len when use packing (#2607) * automatically set pad_to_sequence_len when use packing * update tests --- src/axolotl/utils/schemas/config.py | 15 +++++++++++---- tests/patched/test_validation.py | 22 +++++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index bc7ee7e724..02308695c0 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -512,10 +512,17 @@ def check_sample_packing_w_rl(cls, data): @model_validator(mode="before") @classmethod def hint_sample_packing_padding(cls, data): - if data.get("sample_packing") and not data.get("pad_to_sequence_len"): - LOG.warning( - "`pad_to_sequence_len: true` is recommended when using sample_packing" - ) + if data.get("sample_packing"): + pad_to_sequence_len = data.get("pad_to_sequence_len") + if pad_to_sequence_len is False: + LOG.warning( + "`pad_to_sequence_len: true` is recommended when using sample_packing" + ) + elif pad_to_sequence_len is None: + LOG.info( + "Setting `pad_to_sequence_len: true` to prevent memory leaks when sample_packing" + ) + data["pad_to_sequence_len"] = True return data @model_validator(mode="before") diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 3262a69815..683db61b28 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -648,7 +648,7 @@ def test_packing(self, minimal_cfg): DictDefault( { "sample_packing": True, - "pad_to_sequence_len": None, + "pad_to_sequence_len": False, "flash_attention": True, } ) @@ -662,6 +662,26 @@ def test_packing(self, minimal_cfg): for record in self._caplog.records ) + def test_packing_autoset(self, minimal_cfg): + cfg = ( + DictDefault( + { + "sample_packing": True, + "pad_to_sequence_len": None, + "flash_attention": True, + } + ) + | minimal_cfg + ) + with self._caplog.at_level(logging.INFO): + cfg = validate_config(cfg) + assert any( + "Setting `pad_to_sequence_len: true` to prevent memory leaks when sample_packing" + in record.message + for record in self._caplog.records + ) + assert cfg.pad_to_sequence_len is True + def test_merge_lora_no_bf16_fail(self, minimal_cfg): """ This is assumed to be run on a CPU machine, so bf16 is not supported. From e4f73bc98eada3df1ce48afaf460fe53d1b14ad8 Mon Sep 17 00:00:00 2001 From: aitechguy <153626981+aitechguy0105@users.noreply.github.com> Date: Fri, 2 May 2025 20:47:42 +0800 Subject: [PATCH 0622/1405] remove keys to incoporate changes for the trl update (#2616) --- src/axolotl/core/trainers/dpo/trainer.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index 3520aff109..20bcc8dd63 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -177,12 +177,8 @@ def tokenize_row( # dpo trainer may incorrectly prepend the bos_token_id to the dpo outputs if res["chosen_input_ids"][0] == processing_class.bos_token_id: res["chosen_input_ids"] = res["chosen_input_ids"][1:] - res["chosen_labels"] = res["chosen_labels"][1:] - res["chosen_attention_mask"] = res["chosen_attention_mask"][1:] if res["rejected_input_ids"][0] == processing_class.bos_token_id: res["rejected_input_ids"] = res["rejected_input_ids"][1:] - res["rejected_labels"] = res["rejected_labels"][1:] - res["rejected_attention_mask"] = res["rejected_attention_mask"][1:] return res From 0ba7d362fa32a89bf3767d185c8623e917af674d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 2 May 2025 09:29:55 -0400 Subject: [PATCH 0623/1405] qwen3 and qwen3_moe support for liger kernels (#2612) * qwen3 and qwen3_moe support for liger kernels * fix moe module path * fix: qwen3 liger input args and mlp * fix: qwen3 input args and output class --------- Co-authored-by: NanoCode012 --- src/axolotl/integrations/liger/__init__.py | 24 +++ .../integrations/liger/models/qwen3.py | 160 +++++++++++++++ .../integrations/liger/models/qwen3_moe.py | 191 ++++++++++++++++++ 3 files changed, 375 insertions(+) create mode 100644 src/axolotl/integrations/liger/models/qwen3.py create mode 100644 src/axolotl/integrations/liger/models/qwen3_moe.py diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index 50df7a408d..22d9886333 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -151,6 +151,30 @@ def pre_model_load(self, cfg): rms_norm=cfg.liger_rms_norm, layer_norm=cfg.liger_layer_norm, ) + elif cfg.model_config_type == "qwen3": + from axolotl.integrations.liger.models.qwen3 import ( + apply_liger_kernel_to_qwen3, + ) + + apply_liger_kernel_to_qwen3( + cross_entropy=cfg.liger_cross_entropy, + fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, + glu_activation=cfg.liger_glu_activation, + rms_norm=cfg.liger_rms_norm, + layer_norm=cfg.liger_layer_norm, + ) + elif cfg.model_config_type == "qwen3_moe": + from axolotl.integrations.liger.models.qwen3_moe import ( + apply_liger_kernel_to_qwen3_moe, + ) + + apply_liger_kernel_to_qwen3_moe( + cross_entropy=cfg.liger_cross_entropy, + fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, + glu_activation=cfg.liger_glu_activation, + rms_norm=cfg.liger_rms_norm, + layer_norm=cfg.liger_layer_norm, + ) else: logging.warning( f"Unsupported model config type: {cfg.model_config_type}. Liger not applied." diff --git a/src/axolotl/integrations/liger/models/qwen3.py b/src/axolotl/integrations/liger/models/qwen3.py new file mode 100644 index 0000000000..1dc19eaf96 --- /dev/null +++ b/src/axolotl/integrations/liger/models/qwen3.py @@ -0,0 +1,160 @@ +""" +Liger FLCE for Qwen3. Based on transformers v4.51.3. +""" + +import sys +from typing import Optional, Tuple, Union + +import torch +from liger_kernel.transformers.model.loss_utils import LigerForCausalLMLoss +from transformers.cache_utils import Cache +from transformers.modeling_outputs import CausalLMOutputWithPast + + +def lce_forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs, +) -> Union[Tuple, CausalLMOutputWithPast]: + r""" + Args: + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + """ + + # pylint: disable=duplicate-code + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs[0] + + logits = None + loss = None + # if in training mode, don't materialize logits + if self.training and (labels is not None): + loss = LigerForCausalLMLoss( + hidden_states=hidden_states, + lm_head_weight=self.lm_head.weight, + labels=labels, + hidden_size=self.config.hidden_size, + **kwargs, + ) + + else: # if in inference mode materialize logits + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + logits = self.lm_head(hidden_states[:, slice_indices, :]) + if labels is not None: + loss = self.loss_function( + logits=logits, + labels=labels, + vocab_size=self.config.vocab_size, + **kwargs, + ) + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +def apply_liger_kernel_to_qwen3( + cross_entropy: bool = False, + fused_linear_cross_entropy: bool = False, + rms_norm: bool = False, + glu_activation: bool = False, + layer_norm: bool = False, + **kwargs, # pylint: disable=unused-argument +) -> None: + # pylint: disable=duplicate-code + """ + Apply Liger kernels to replace original implementation in HuggingFace Llama models (2 and 3) + + Args: + cross_entropy (bool): Whether to apply Liger's cross entropy loss. Default is False. + fused_linear_cross_entropy (bool): + Whether to apply Liger's fused linear cross entropy loss. Default is False. + `cross_entropy` and `fused_linear_cross_entropy` cannot both be False. + If `fused_linear_cross_entropy` is True, the logits will not be materialized but more memory efficient. + rms_norm (bool): Whether to apply Liger's RMSNorm. Default is False. + glu_activation (bool): Whether to apply Liger's SwiGLU MLP. Default is False. + layer_norm (bool): Whether to apply Liger's LayerNorm. Default is False. + """ + + import transformers.models.qwen3.modeling_qwen3 # noqa: F401 # pylint: disable=unused-import + from liger_kernel.transformers.functional import liger_cross_entropy + from liger_kernel.transformers.layer_norm import LigerLayerNorm + from liger_kernel.transformers.rms_norm import LigerRMSNorm + from liger_kernel.transformers.swiglu import LigerSwiGLUMLP + + assert not ( + cross_entropy and fused_linear_cross_entropy + ), "cross_entropy and fused_linear_cross_entropy cannot both be True." + + modeling_qwen3 = sys.modules["transformers.models.qwen3.modeling_qwen3"] + + if rms_norm: + modeling_qwen3.Qwen3RMSNorm = LigerRMSNorm + + if glu_activation: + modeling_qwen3.Qwen3MLP = LigerSwiGLUMLP + + if layer_norm: + modeling_qwen3.nn.LayerNorm = LigerLayerNorm + + if cross_entropy: + from transformers.loss.loss_utils import nn + + nn.functional.cross_entropy = liger_cross_entropy + + if fused_linear_cross_entropy: + modeling_qwen3.Qwen3ForCausalLM.forward = lce_forward diff --git a/src/axolotl/integrations/liger/models/qwen3_moe.py b/src/axolotl/integrations/liger/models/qwen3_moe.py new file mode 100644 index 0000000000..89bdc5bcc1 --- /dev/null +++ b/src/axolotl/integrations/liger/models/qwen3_moe.py @@ -0,0 +1,191 @@ +""" +Liger FLCE for Qwen3 MoE. Based on transformers v4.51.3. +""" + +import sys +from copy import deepcopy +from typing import List, Optional, Union + +import torch +from liger_kernel.transformers.model.loss_utils import LigerForCausalLMLoss +from transformers.modeling_outputs import MoeCausalLMOutputWithPast +from transformers.models.qwen3_moe.modeling_qwen3_moe import load_balancing_loss_func + + +def lce_forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[List[torch.FloatTensor]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + output_router_logits: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs, +) -> MoeCausalLMOutputWithPast: + r""" + Args: + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + """ + + # pylint: disable=duplicate-code + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_router_logits = ( + output_router_logits + if output_router_logits is not None + else self.config.output_router_logits + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + output_router_logits=output_router_logits, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs[0] + + logits = None + loss = None + # if in training mode, don't materialize logits + if self.training and (labels is not None): + loss = LigerForCausalLMLoss( + hidden_states=hidden_states, + lm_head_weight=self.lm_head.weight, + labels=labels, + hidden_size=self.config.hidden_size, + **kwargs, + ) + + else: # if in inference mode materialize logits + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + logits = self.lm_head(hidden_states[:, slice_indices, :]) + if labels is not None: + loss = self.loss_function( + logits=logits, + labels=labels, + vocab_size=self.config.vocab_size, + **kwargs, + ) + + aux_loss = None + if output_router_logits: + aux_loss = load_balancing_loss_func( + outputs.router_logits, + self.num_experts, + self.num_experts_per_tok, + attention_mask, + ) + if labels is not None: + loss += self.router_aux_loss_coef * aux_loss.to( + loss.device + ) # make sure to reside in the same device + + return MoeCausalLMOutputWithPast( + loss=loss, + aux_loss=aux_loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +def apply_liger_kernel_to_qwen3_moe( + cross_entropy: bool = False, + fused_linear_cross_entropy: bool = False, + rms_norm: bool = False, + glu_activation: bool = False, + layer_norm: bool = False, + **kwargs, # pylint: disable=unused-argument +) -> None: + # pylint: disable=duplicate-code + """ + Apply Liger kernels to replace original implementation in HuggingFace Llama models (2 and 3) + + Args: + cross_entropy (bool): Whether to apply Liger's cross entropy loss. Default is False. + fused_linear_cross_entropy (bool): + Whether to apply Liger's fused linear cross entropy loss. Default is False. + `cross_entropy` and `fused_linear_cross_entropy` cannot both be False. + If `fused_linear_cross_entropy` is True, the logits will not be materialized but more memory efficient. + rms_norm (bool): Whether to apply Liger's RMSNorm. Default is False. + glu_activation (bool): Whether to apply Liger's SwiGLU MLP. Default is False. + layer_norm (bool): Whether to apply Liger's LayerNorm. Default is False. + """ + + import transformers.models.qwen3_moe.modeling_qwen3_moe # noqa: F401 # pylint: disable=unused-import + from liger_kernel.transformers.functional import liger_cross_entropy + from liger_kernel.transformers.layer_norm import LigerLayerNorm + from liger_kernel.transformers.rms_norm import LigerRMSNorm + from liger_kernel.transformers.swiglu import LigerSwiGLUMLP + + assert not ( + cross_entropy and fused_linear_cross_entropy + ), "cross_entropy and fused_linear_cross_entropy cannot both be True." + + modeling_qwen3_moe = sys.modules["transformers.models.qwen3_moe.modeling_qwen3_moe"] + + if rms_norm: + modeling_qwen3_moe.Qwen3MoeRMSNorm = LigerRMSNorm + + if glu_activation: + + def _liger_swiglu_mlp_wrapper(config, intermediate_size=None, **kwargs): + "Accepts intermediate_size to pass to LigerSwiGLUMLP" + # clone config to avoid modifying the original + config = deepcopy(config) + if intermediate_size: + setattr(config, "intermediate_size", intermediate_size) + return LigerSwiGLUMLP(config, **kwargs) + + modeling_qwen3_moe.Qwen3MoeMLP = _liger_swiglu_mlp_wrapper + + if layer_norm: + modeling_qwen3_moe.nn.LayerNorm = LigerLayerNorm + + if cross_entropy: + from transformers.loss.loss_utils import nn + + nn.functional.cross_entropy = liger_cross_entropy + + if fused_linear_cross_entropy: + modeling_qwen3_moe.Qwen3MoeForCausalLM.forward = lce_forward From 3dd9c3bf3f45df23110bf5bb84993f16c0a627d9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 3 May 2025 12:02:26 -0400 Subject: [PATCH 0624/1405] setup hf transfer too and fix auto bf16 when fp16 enabled (#2620) [skip ci] --- src/axolotl/cli/evaluate.py | 4 ++-- src/axolotl/cli/main.py | 6 ++++-- src/axolotl/cli/train.py | 4 ++-- src/axolotl/utils/__init__.py | 9 +++++++++ src/axolotl/utils/config/__init__.py | 2 +- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/axolotl/cli/evaluate.py b/src/axolotl/cli/evaluate.py index a1859f3156..e52da66b72 100644 --- a/src/axolotl/cli/evaluate.py +++ b/src/axolotl/cli/evaluate.py @@ -15,7 +15,7 @@ from axolotl.cli.config import load_cfg from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.evaluate import evaluate -from axolotl.utils import set_pytorch_cuda_alloc_conf +from axolotl.utils import patch_optimized_env from axolotl.utils.dict import DictDefault LOG = logging.getLogger(__name__) @@ -32,7 +32,7 @@ def do_evaluate(cfg: DictDefault, cli_args: TrainerCliArgs) -> None: cli_args: CLI arguments. """ # Enable expandable segments for cuda allocation to improve VRAM usage - set_pytorch_cuda_alloc_conf() + patch_optimized_env() # pylint: disable=duplicate-code print_axolotl_text_art() diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index de4fb6cbee..601add7090 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -29,7 +29,7 @@ filter_none_kwargs, ) from axolotl.integrations.lm_eval.cli import lm_eval -from axolotl.utils import set_pytorch_cuda_alloc_conf +from axolotl.utils import patch_optimized_env from axolotl.utils.schemas.config import AxolotlInputConfig @@ -55,6 +55,8 @@ def preprocess(config: str, cloud: Optional[str] = None, **kwargs) -> None: kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` config options. """ + patch_optimized_env() + if cloud: from axolotl.cli.cloud import do_cli_preprocess @@ -100,7 +102,7 @@ def train( config options. """ # Enable expandable segments for cuda allocation to improve VRAM usage - set_pytorch_cuda_alloc_conf() + patch_optimized_env() if "use_ray" in kwargs and kwargs["use_ray"]: accelerate = False diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 4f258313d2..9e90cede38 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -18,7 +18,7 @@ from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.integrations.base import PluginManager from axolotl.train import train -from axolotl.utils import set_pytorch_cuda_alloc_conf +from axolotl.utils import patch_optimized_env from axolotl.utils.config import normalize_config, resolve_dtype from axolotl.utils.dict import DictDefault @@ -36,7 +36,7 @@ def do_train(cfg: DictDefault, cli_args: TrainerCliArgs): cli_args: Training-specific CLI arguments. """ # Enable expandable segments for cuda allocation to improve VRAM usage - set_pytorch_cuda_alloc_conf() + patch_optimized_env() print_axolotl_text_art() check_accelerate_default_config() diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index ffa528cc95..3d0ba7c9c6 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -43,3 +43,12 @@ def set_pytorch_cuda_alloc_conf(): os.environ["PYTORCH_CUDA_ALLOC_CONF"] = ( "expandable_segments:True,roundup_power2_divisions:16" ) + + +def patch_optimized_env(): + """ + Patch environment variables to improve VRAM usage and increase download speed + """ + if os.getenv("HF_HUB_ENABLE_HF_TRANSFER") is None: + os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1" + set_pytorch_cuda_alloc_conf() diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 35e742a89a..0de87fa5cf 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -59,7 +59,7 @@ def get_device(): def resolve_dtype(cfg): if ( - cfg.bf16 == "auto" and not cfg.use_ray + not cfg.fp16 and cfg.bf16 == "auto" and not cfg.use_ray ): # if we use ray we want to defer this check to the worker node if is_torch_bf16_gpu_available(): LOG.debug("bf16 support detected, enabling for this configuration.") From ed922796b71bc38c2504b676b445da14fa932ada Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 3 May 2025 12:02:39 -0400 Subject: [PATCH 0625/1405] include multipack support for qwen3 family (#2622) --- src/axolotl/monkeypatch/multipack.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index a2459ec5ad..1467f9e295 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -18,6 +18,8 @@ "mixtral", "qwen2", "qwen2_moe", + "qwen3", + "qwen3_moe", "falcon", "phi", "phi3", From 54960d4de0b3a1bfafeb2656a7b5bbf7875e232f Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Sun, 4 May 2025 15:22:45 +0300 Subject: [PATCH 0626/1405] Fix logging deprecation warnings (#2623) Signed-off-by: Emmanuel Ferdman --- src/axolotl/utils/samplers/multipack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 0d8806d8b5..1e1172cb55 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -190,7 +190,7 @@ def __init__( self.len_across_ranks = None if self.sequential and not isinstance(sampler, SequentialSampler): - LOG.warn( + LOG.warning( "using sequential sample packing with non-sequential sampler, did you want to also enable curriculum_sampling?" ) From a980618fd0e95fb241280a6079f35042042d67d9 Mon Sep 17 00:00:00 2001 From: mhenrichsen Date: Tue, 6 May 2025 10:11:06 +0200 Subject: [PATCH 0627/1405] Adds example for training a TTS model on top of a LLM. (#2614) * Adds example for training a TTS model on top of a LLM. * Update examples/orpheus/finetune.yml Co-authored-by: NanoCode012 * Update examples/orpheus/finetune.yml Co-authored-by: NanoCode012 * Update README.md to clarify GPU requirements for finetuning Orpheus TTS model * Update finetune.yml to use the new base model canopylabs/orpheus-3b-0.1-pretrained * Update finetune.yml and README.md for consistency and clarity --------- Co-authored-by: NanoCode012 --- examples/orpheus/README.md | 341 ++++++++++++++++++++++++++++++++++ examples/orpheus/finetune.yml | 52 ++++++ 2 files changed, 393 insertions(+) create mode 100644 examples/orpheus/README.md create mode 100644 examples/orpheus/finetune.yml diff --git a/examples/orpheus/README.md b/examples/orpheus/README.md new file mode 100644 index 0000000000..5fea05ecf4 --- /dev/null +++ b/examples/orpheus/README.md @@ -0,0 +1,341 @@ +# Finetuning LLMs to output audio + +In this example, we finetune Orpcanopylabs/orpheus-tts-0.1-pretrained (a LLaMA 3.2 3b model) to output audio. + +The `finetune.yml` withe current settings will run on any Nvidia GPU with 45GB VRAM or more. If you adjust the batch size it can easily run on any GPU under 24GB. + +## Dataset pre-processing for pre-training +If you are adding another voice in English, please jump ahead to finetuning pre-processing. + +For this to work, we need to preprocess our dataset. Since we are expecting to output audio, we will need to add tokens to the tokenizer. + +Using this code, it will download the SNAC model and add the correct tokens and upload the final dataset. + +```python +import torch +from snac import SNAC +from datasets import load_dataset +from huggingface_hub import snapshot_download +from datasets import load_dataset +import random +import torchaudio.transforms as T +from transformers import AutoTokenizer +import os + +my_original_dataset_name = "" +name_to_push_dataset_to = "" + +dsn = my_original_dataset_name + +snapshot_download( + repo_id=dsn, + repo_type="dataset", + revision="main", + max_workers=64, +) + + +ds = load_dataset(dsn, split="train") +ds_sample_rate = ds[0]["audio"]["sampling_rate"] + +model = SNAC.from_pretrained("hubertsiuzdak/snac_24khz") +model = model.to("mps") + +def tokenise_audio(waveform): + waveform = torch.from_numpy(waveform).unsqueeze(0) + waveform = waveform.to(dtype=torch.float32) + resample_transform = T.Resample(orig_freq=ds_sample_rate, new_freq=24000) + waveform = resample_transform(waveform) + + waveform = waveform.unsqueeze(0).to("cuda") + + #generate the codes from snac + with torch.inference_mode(): + codes = model.encode(waveform) + + all_codes = [] + for i in range(codes[0].shape[1]): + all_codes.append(codes[0][0][i].item()+128266) + all_codes.append(codes[1][0][2*i].item()+128266+4096) + all_codes.append(codes[2][0][4*i].item()+128266+(2*4096)) + all_codes.append(codes[2][0][(4*i)+1].item()+128266+(3*4096)) + all_codes.append(codes[1][0][(2*i)+1].item()+128266+(4*4096)) + all_codes.append(codes[2][0][(4*i)+2].item()+128266+(5*4096)) + all_codes.append(codes[2][0][(4*i)+3].item()+128266+(6*4096)) + + + return all_codes + +def add_codes(example): + # Always initialize codes_list to None + codes_list = None + + try: + answer_audio = example.get("audio") + # If there's a valid audio array, tokenise it + if answer_audio and "array" in answer_audio: + audio_array = answer_audio["array"] + codes_list = tokenise_audio(audio_array) + except Exception as e: + print(f"Skipping row due to error: {e}") + # Keep codes_list as None if we fail + example["codes_list"] = codes_list + + return example + +ds = ds.map(add_codes, remove_columns=["audio"]) + +#@title Load Tokenizer +tokeniser_length = 128256 +start_of_text = 128000 +end_of_text = 128009 + +start_of_speech = tokeniser_length + 1 +end_of_speech = tokeniser_length + 2 + +start_of_human = tokeniser_length + 3 +end_of_human = tokeniser_length + 4 + +start_of_ai = tokeniser_length + 5 +end_of_ai = tokeniser_length + 6 +pad_token = tokeniser_length + 7 + +audio_tokens_start = tokeniser_length + 10 + +tokenizer_name = "canopylabs/orpheus-3b-0.1-pretrained" + + +tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) +num_proc = os.cpu_count() - 2 + +ds = ds.filter(lambda x: x["codes_list"] is not None) +ds = ds.filter(lambda x: len(x["codes_list"]) > 0) + +#@title Create Input Ids +def remove_duplicate_frames(example): + vals = example["codes_list"] + if len(vals) % 7 != 0: + raise ValueError("Input list length must be divisible by 7") + + result = vals[:7] + + removed_frames = 0 + + for i in range(7, len(vals), 7): + current_first = vals[i] + previous_first = result[-7] + + if current_first != previous_first: + result.extend(vals[i:i+7]) + else: + removed_frames += 1 + + example["codes_list"] = result + + return example + +ds = ds.map(remove_duplicate_frames, num_proc=num_proc) + + +def create_input_ids(example): + text_ids = tokenizer.encode({example['text']}, add_special_tokens=True) + text_ids.append(end_of_text) + example["text_tokens"] = text_ids + input_ids = ( + [start_of_human] + + example["text_tokens"] + + [end_of_human] + + [start_of_ai] + + [start_of_speech] + + example["codes_list"] + + [end_of_speech] + + [end_of_ai] + ) + example["input_ids"] = input_ids + example["labels"] = input_ids + example["attention_mask"] = [1] * len(input_ids) + + return example + +ds = ds.map(create_input_ids, num_proc=num_proc, remove_columns=["text", "codes_list"]) + +#@title Remove unnecessary columns +columns_to_keep = ["input_ids", "labels", "attention_mask"] +columns_to_remove = [col for col in ds.column_names if col not in columns_to_keep] + +ds = ds.remove_columns(columns_to_remove) + +ds.push_to_hub(name_to_push_dataset_to) +``` + + +## Finetune pre-processing +Use this code to add a new voice. + +```python +import torch +from snac import SNAC +from datasets import load_dataset +from huggingface_hub import snapshot_download +from datasets import load_dataset +import random +import torchaudio.transforms as T +from transformers import AutoTokenizer +import os + +my_original_dataset_name = "" +name_to_push_dataset_to = "" + +dsn = my_original_dataset_name + +snapshot_download( + repo_id=dsn, + repo_type="dataset", + revision="main", + max_workers=64, +) + + +ds = load_dataset(dsn, split="train") +ds_sample_rate = ds[0]["audio"]["sampling_rate"] + +model = SNAC.from_pretrained("hubertsiuzdak/snac_24khz") +model = model.to("mps") + +def tokenise_audio(waveform): + waveform = torch.from_numpy(waveform).unsqueeze(0) + waveform = waveform.to(dtype=torch.float32) + resample_transform = T.Resample(orig_freq=ds_sample_rate, new_freq=24000) + waveform = resample_transform(waveform) + + waveform = waveform.unsqueeze(0).to("cuda") + + #generate the codes from snac + with torch.inference_mode(): + codes = model.encode(waveform) + + all_codes = [] + for i in range(codes[0].shape[1]): + all_codes.append(codes[0][0][i].item()+128266) + all_codes.append(codes[1][0][2*i].item()+128266+4096) + all_codes.append(codes[2][0][4*i].item()+128266+(2*4096)) + all_codes.append(codes[2][0][(4*i)+1].item()+128266+(3*4096)) + all_codes.append(codes[1][0][(2*i)+1].item()+128266+(4*4096)) + all_codes.append(codes[2][0][(4*i)+2].item()+128266+(5*4096)) + all_codes.append(codes[2][0][(4*i)+3].item()+128266+(6*4096)) + + + return all_codes + +def add_codes(example): + # Always initialize codes_list to None + codes_list = None + + try: + answer_audio = example.get("audio") + # If there's a valid audio array, tokenise it + if answer_audio and "array" in answer_audio: + audio_array = answer_audio["array"] + codes_list = tokenise_audio(audio_array) + except Exception as e: + print(f"Skipping row due to error: {e}") + # Keep codes_list as None if we fail + example["codes_list"] = codes_list + + return example + +ds = ds.map(add_codes, remove_columns=["audio"]) + +#@title Load Tokenizer +tokeniser_length = 128256 +start_of_text = 128000 +end_of_text = 128009 + +start_of_speech = tokeniser_length + 1 +end_of_speech = tokeniser_length + 2 + +start_of_human = tokeniser_length + 3 +end_of_human = tokeniser_length + 4 + +start_of_ai = tokeniser_length + 5 +end_of_ai = tokeniser_length + 6 +pad_token = tokeniser_length + 7 + +audio_tokens_start = tokeniser_length + 10 + +tokenizer_name = "canopylabs/orpheus-3b-0.1-pretrained" + + +tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) +num_proc = os.cpu_count() - 2 + +ds = ds.filter(lambda x: x["codes_list"] is not None) +ds = ds.filter(lambda x: len(x["codes_list"]) > 0) + +#@title Create Input Ids +def remove_duplicate_frames(example): + vals = example["codes_list"] + if len(vals) % 7 != 0: + raise ValueError("Input list length must be divisible by 7") + + result = vals[:7] + + removed_frames = 0 + + for i in range(7, len(vals), 7): + current_first = vals[i] + previous_first = result[-7] + + if current_first != previous_first: + result.extend(vals[i:i+7]) + else: + removed_frames += 1 + + example["codes_list"] = result + + return example + +ds = ds.map(remove_duplicate_frames, num_proc=num_proc) + +tok_info = '''*** HERE you can modify the text prompt +i.e. if you wanted a multispeaker model like canopylabs/orpheus-3b-0.1-ft, you can pass: +f"{example["source"]}: {example["text"]}", as is passed. +''' +print(tok_info) + +def create_input_ids(example): + text_ids = tokenizer.encode(f"{example['speaker_id']}: {example['text']}", add_special_tokens=True) + text_ids.append(end_of_text) + example["text_tokens"] = text_ids + input_ids = ( + [start_of_human] + + example["text_tokens"] + + [end_of_human] + + [start_of_ai] + + [start_of_speech] + + example["codes_list"] + + [end_of_speech] + + [end_of_ai] + ) + example["input_ids"] = input_ids + example["labels"] = input_ids + example["attention_mask"] = [1] * len(input_ids) + + return example + +ds = ds.map(create_input_ids, num_proc=num_proc, remove_columns=["text", "codes_list"]) + +#@title Remove unnecessary columns +columns_to_keep = ["input_ids", "labels", "attention_mask"] +columns_to_remove = [col for col in ds.column_names if col not in columns_to_keep] + +ds = ds.remove_columns(columns_to_remove) + +ds.push_to_hub(name_to_push_dataset_to) +``` + +## Training +After preprocessing is done, fill out the blanks in finetune.yml and simply run `axolotl train finetune.yml` + +## Inference +For inference, please refer to the original [orpheus github](https://github.com/canopyai/Orpheus-TTS/tree/main). diff --git a/examples/orpheus/finetune.yml b/examples/orpheus/finetune.yml new file mode 100644 index 0000000000..9bcbbeee0d --- /dev/null +++ b/examples/orpheus/finetune.yml @@ -0,0 +1,52 @@ +base_model: canopylabs/orpheus-3b-0.1-pretrained + +hub_model_id: + +plugins: + - axolotl.integrations.liger.LigerPlugin +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_fused_linear_cross_entropy: true + +datasets: + - path: + type: # leave empty to load pre-tokenized +dataset_prepared_path: last_run_prepared +val_set_size: 0.01 +output_dir: ./outputs/out + +sequence_len: 8192 +sample_packing: true +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 8 +micro_batch_size: 4 +num_epochs: 3 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 2e-5 + +bf16: auto +tf32: false + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_steps: 20 +evals_per_epoch: 5 +saves_per_epoch: 5 +weight_decay: 0.05 + +special_tokens: + pad_token: From f720b6e72d486a7b6281fcf94130f817b403f449 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 May 2025 11:09:07 -0400 Subject: [PATCH 0628/1405] repop cache (#2639) * repop cache * pre-cache as a step * fix the name * add reason for pytest skipif * restore pytorch matrix * remove max-parallel now that we've optimized this a bit --- .github/workflows/tests.yml | 102 ++++++++++++++++++++++++++++++------ tests/conftest.py | 58 ++++++++++---------- 2 files changed, 115 insertions(+), 45 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c2c085fa0a..2671cfd339 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,17 +44,19 @@ jobs: env: SKIP: no-commit-to-branch - pytest: - name: PyTest + preload-cache: + name: Preload HF cache runs-on: ubuntu-latest strategy: fail-fast: false - max-parallel: 2 matrix: python_version: ["3.11"] - pytorch_version: ["2.5.1", "2.6.0", "2.7.0"] + pytorch_version: ["2.6.0"] timeout-minutes: 20 + env: + AXOLOTL_IS_CI_CACHE_PRELOAD: "1" + steps: - name: Check out repository code uses: actions/checkout@v4 @@ -105,9 +107,7 @@ jobs: - name: Run tests run: | - pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ --cov=axolotl --cov-report=xml - pytest -v tests/patched/ --cov=axolotl --cov-append --cov-report=xml - pytest -v tests/cli/ --cov=axolotl --cov-append --cov-report=xml + pytest -v tests/conftest.py - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 @@ -130,12 +130,89 @@ jobs: /home/runner/.cache/huggingface/hub/models--* key: ${{ steps.hf-cache-restore.outputs.cache-primary-key }} + pytest: + name: PyTest + runs-on: ubuntu-latest + needs: [preload-cache] + strategy: + fail-fast: false + matrix: + python_version: ["3.11"] + pytorch_version: ["2.5.1", "2.6.0", "2.7.0"] + timeout-minutes: 20 + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Restore HF cache + id: hf-cache-restore + uses: actions/cache/restore@v4 + with: + path: | + /home/runner/.cache/huggingface/hub/datasets--* + /home/runner/.cache/huggingface/hub/models--* + key: ${{ runner.os }}-hf-hub-cache-v2 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python_version }} + cache: 'pip' # caching pip dependencies + + - name: upgrade pip + run: | + pip3 install --upgrade pip + pip3 install --upgrade packaging==23.2 setuptools==75.8.0 wheel + + - name: Install PyTorch + run: | + pip3 install torch==${{ matrix.pytorch_version }} + + - name: Install dependencies + run: | + pip3 show torch + pip3 install --no-build-isolation -U -e . + python scripts/unsloth_install.py | sh + python scripts/cutcrossentropy_install.py | sh + pip3 install -r requirements-dev.txt -r requirements-tests.txt + + - name: Make sure PyTorch version wasn't clobbered + run: | + python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" + + - name: Ensure axolotl CLI was installed + run: | + axolotl --help + + - name: Pre-Download dataset fixture + run: | + huggingface-cli download --repo-type=dataset axolotl-ai-internal/axolotl-oss-dataset-fixtures + + - name: Run tests + run: | + pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ --cov=axolotl --cov-report=xml + pytest -v tests/patched/ --cov=axolotl --cov-append --cov-report=xml + pytest -v tests/cli/ --cov=axolotl --cov-append --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + flags: unittests,pytorch-${{ matrix.pytorch_version }} + fail_ci_if_error: false + + - name: cleanup pip cache + run: | + find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + pytest-sdist: name: PyTest from Source Dist runs-on: ubuntu-latest + needs: [preload-cache] strategy: fail-fast: false - max-parallel: 1 matrix: python_version: ["3.11"] pytorch_version: ["2.5.1", "2.6.0", "2.7.0"] @@ -199,15 +276,6 @@ jobs: run: | find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; - - name: Save HF cache - id: hf-cache - uses: actions/cache/save@v4 - with: - path: | - /home/runner/.cache/huggingface/hub/datasets--* - /home/runner/.cache/huggingface/hub/models--* - key: ${{ steps.hf-cache-restore.outputs.cache-primary-key }} - docker-e2e-tests-1st: if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... diff --git a/tests/conftest.py b/tests/conftest.py index 7fc9a62afa..8ab8fd6a46 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import functools import importlib +import os import shutil import sys import tempfile @@ -529,31 +530,32 @@ def dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff( # # pylint: disable=redefined-outer-name,unused-argument -# def test_load_fixtures( -# download_smollm2_135m_model, -# download_llama_68m_random_model, -# download_qwen_2_5_half_billion_model, -# download_tatsu_lab_alpaca_dataset, -# download_mhenrichsen_alpaca_2k_dataset, -# download_mhenrichsen_alpaca_2k_w_revision_dataset, -# download_mlabonne_finetome_100k_dataset, -# download_argilla_distilabel_capybara_dpo_7k_binarized_dataset, -# download_argilla_ultrafeedback_binarized_preferences_cleaned_dataset, -# download_fozzie_alpaca_dpo_dataset, -# download_arcee_ai_distilabel_intel_orca_dpo_pairs_dataset, -# download_argilla_dpo_pairs_dataset, -# download_tiny_shakespeare_dataset, -# download_deepseek_model_fixture, -# download_huggyllama_model_fixture, -# download_llama_1b_model_fixture, -# download_llama3_8b_model_fixture, -# download_llama3_8b_instruct_model_fixture, -# download_phi_35_mini_model_fixture, -# download_phi_3_medium_model_fixture, -# download_mistral_7b_model_fixture, -# download_gemma_2b_model_fixture, -# download_gemma2_9b_model_fixture, -# download_mlx_mistral_7b_model_fixture, -# download_llama2_model_fixture, -# ): -# pass +@pytest.mark.skipif( + os.environ.get("AXOLOTL_IS_CI_CACHE_PRELOAD", "-1") != "1", + reason="Not running in CI cache preload", +) +def test_load_fixtures( + download_smollm2_135m_model, + download_qwen_2_5_half_billion_model, + download_tatsu_lab_alpaca_dataset, + download_mhenrichsen_alpaca_2k_dataset, + download_mhenrichsen_alpaca_2k_w_revision_dataset, + download_mlabonne_finetome_100k_dataset, + download_argilla_distilabel_capybara_dpo_7k_binarized_dataset, + download_arcee_ai_distilabel_intel_orca_dpo_pairs_dataset, + download_argilla_dpo_pairs_dataset, + download_tiny_shakespeare_dataset, + download_deepseek_model_fixture, + download_huggyllama_model_fixture, + download_llama_1b_model_fixture, + download_llama3_8b_model_fixture, + download_llama3_8b_instruct_model_fixture, + download_phi_35_mini_model_fixture, + download_phi_3_medium_model_fixture, + download_mistral_7b_model_fixture, + download_gemma_2b_model_fixture, + download_gemma2_9b_model_fixture, + download_mlx_mistral_7b_model_fixture, + download_llama2_model_fixture, +): + pass From 679743087a438380a6717ef1242cae6b03d65def Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 May 2025 11:18:00 -0400 Subject: [PATCH 0629/1405] make sure gc_steps is used for all trainers (#2638) --- src/axolotl/core/trainer_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 31ee3cccfb..99d97800c8 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -168,6 +168,9 @@ def get_callbacks(self) -> List[TrainerCallback]: ) ) + if self.cfg.gc_steps: + callbacks.append(GCCallback(gc_steps=self.cfg.gc_steps)) + if self.cfg.use_wandb: callbacks.append( SaveAxolotlConfigtoWandBCallback(self.cfg.axolotl_config_path) @@ -249,9 +252,6 @@ def get_callbacks(self): if self.cfg.loss_watchdog_threshold is not None: callbacks.append(LossWatchDogCallback(self.cfg)) - if self.cfg.gc_steps: - callbacks.append(GCCallback(gc_steps=self.cfg.gc_steps)) - return callbacks def get_post_trainer_create_callbacks(self, trainer): From ddaebf8309c2bbb08c19ee8d09150753b5e0570b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 May 2025 11:18:25 -0400 Subject: [PATCH 0630/1405] fix dpo eval override to call grandparent instead of the broken super (#2628) [skip ci] --- src/axolotl/core/trainers/dpo/trainer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index 20bcc8dd63..1ce7deea75 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -247,7 +247,9 @@ def evaluation_loop( ) # Base evaluation - initial_output = super().evaluation_loop( + initial_output = super( # pylint: disable=bad-super-call + DPOTrainer, self + ).evaluation_loop( dataloader, description, prediction_loss_only, From b71c0e344730ba21383de4f05e0c7c671522ccaa Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 May 2025 11:18:45 -0400 Subject: [PATCH 0631/1405] Print axolotl art if train is called outside of cli: (#2627) [skip ci] --- src/axolotl/cli/art.py | 7 +++++++ src/axolotl/common/datasets.py | 25 ++++++++++++++++--------- src/axolotl/train.py | 3 +++ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/axolotl/cli/art.py b/src/axolotl/cli/art.py index 6ed22a52d7..2051784e98 100644 --- a/src/axolotl/cli/art.py +++ b/src/axolotl/cli/art.py @@ -16,8 +16,15 @@ @@@@ @@@@@@@@@@@@@@@@ """ +HAS_PRINTED_LOGO = False + def print_axolotl_text_art(): """Prints axolotl ASCII art.""" + + global HAS_PRINTED_LOGO # pylint: disable=global-statement + if HAS_PRINTED_LOGO: + return if is_main_process(): + HAS_PRINTED_LOGO = True print(AXOLOTL_LOGO) diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index 2ab405ef1f..9dd62f0f7d 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -48,6 +48,7 @@ def load_datasets( *, cfg: DictDefault, cli_args: PreprocessCliArgs | TrainerCliArgs | None = None, + debug: bool = False, ) -> TrainDatasetMeta: """ Loads one or more training or evaluation datasets, calling @@ -56,6 +57,7 @@ def load_datasets( Args: cfg: Dictionary mapping `axolotl` config keys to values. cli_args: Command-specific CLI arguments. + debug: Whether to print out tokenization of sample Returns: Dataclass with fields for training and evaluation datasets and the computed @@ -77,20 +79,25 @@ def load_datasets( preprocess_iterable=preprocess_iterable, ) - if cli_args and ( - cli_args.debug - or cfg.debug - or cli_args.debug_text_only - or int(cli_args.debug_num_examples) > 0 - ): + if ( # pylint: disable=too-many-boolean-expressions + cli_args + and ( + cli_args.debug + or cfg.debug + or cli_args.debug_text_only + or int(cli_args.debug_num_examples) > 0 + ) + ) or debug: LOG.info("check_dataset_labels...") - train_samples = sample_dataset(train_dataset, cli_args.debug_num_examples) + num_examples = cli_args.debug_num_examples if cli_args else 1 + text_only = cli_args.debug_text_only if cli_args else False + train_samples = sample_dataset(train_dataset, num_examples) check_dataset_labels( train_samples, tokenizer, - num_examples=cli_args.debug_num_examples, - text_only=cli_args.debug_text_only, + num_examples=num_examples, + text_only=text_only, ) LOG.info("printing prompters...") diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 30d26b7063..a5fd5e2e06 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -21,6 +21,7 @@ from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled from transformers.trainer import Trainer +from axolotl.cli.art import print_axolotl_text_art from axolotl.common.datasets import TrainDatasetMeta from axolotl.contribs.lgpl import ( # pylint: disable = no-name-in-module fix_untrained_tokens, @@ -516,6 +517,8 @@ def train( Returns: Tuple of (model, tokenizer) after training """ + print_axolotl_text_art() + # Setup model, tokenizer, (causal or RLHF) trainer, etc. ( trainer, From a6cac5dd32c273594d89d5f4d82bd88ddc0443d7 Mon Sep 17 00:00:00 2001 From: mhenrichsen Date: Tue, 6 May 2025 17:24:07 +0200 Subject: [PATCH 0632/1405] Update lr_scheduler options in config.qmd to include additional scheduling strategies for improved training flexibility. (#2636) [skip ci] --- docs/config.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.qmd b/docs/config.qmd index 7b0d404621..04d1320cad 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -547,7 +547,7 @@ gradient_checkpointing: false early_stopping_patience: 3 # Specify a scheduler and kwargs to use with the optimizer -lr_scheduler: # 'one_cycle' | 'rex' | 'log_sweep' | empty for cosine +lr_scheduler: # 'one_cycle' | 'rex' | 'log_sweep' | 'linear' | 'cosine_with_restarts' | 'polynomial' | 'constant' | 'constant_with_warmup' | 'inverse_sqrt' | 'reduce_lr_on_plateau' | 'cosine_with_min_lr' | 'warmup_stable_decay' | empty for cosine lr_scheduler_kwargs: cosine_min_lr_ratio: # decay lr to some percentage of the peak lr, e.g. cosine_min_lr_ratio=0.1 for 10% of peak lr cosine_constant_lr_ratio: # freeze lr at some percentage of the step, e.g. cosine_constant_lr_ratio=0.8 means start cosine_min_lr at 80% of training step (https://arxiv.org/pdf/2308.04014.pdf) From e4cfebe9955f3fc4d2dec5c46e27a3f6323f2a01 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 May 2025 20:05:19 -0400 Subject: [PATCH 0633/1405] bump liger dep to 0.5.9 (#2640) [skip ci] * bump liger dep to 0.5.9 * also upgrade vllm to post1, and datasets to 3.5.1 --- requirements.txt | 4 ++-- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 327b5ee2d2..dc495bedd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 autoawq==0.2.7.post3 -liger-kernel==0.5.8 +liger-kernel==0.5.9 # END section packaging==23.2 @@ -15,7 +15,7 @@ peft==0.15.2 transformers==4.51.3 tokenizers>=0.21.1 accelerate==1.6.0 -datasets==3.5.0 +datasets==3.5.1 deepspeed>=0.15.4 trl==0.17.0 hf_xet==1.1.0 diff --git a/setup.py b/setup.py index d6009b8d5c..51a97e4d9c 100644 --- a/setup.py +++ b/setup.py @@ -67,13 +67,13 @@ def parse_requirements(extras_require_map): if (major, minor) >= (2, 7): _install_requires.pop(_install_requires.index(xformers_version)) # _install_requires.append("xformers==0.0.29.post3") # xformers seems to be hard pinned to 2.6.0 - extras_require_map["vllm"] = ["vllm==0.8.5"] + extras_require_map["vllm"] = ["vllm==0.8.5.post1"] elif (major, minor) >= (2, 6): _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append( "xformers==0.0.29.post2" ) # vllm needs post2 w torch 2.6 - extras_require_map["vllm"] = ["vllm==0.8.5"] + extras_require_map["vllm"] = ["vllm==0.8.5.post1"] elif (major, minor) >= (2, 5): _install_requires.pop(_install_requires.index(xformers_version)) if patch == 0: From 0b140fef83462455a7b00ef18bdd62b26c61da95 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 7 May 2025 07:05:32 +0700 Subject: [PATCH 0634/1405] feat(doc): add split_thinking docs (#2613) [skip ci] * feat(doc): add split_thinking docs * fix: link config.qmd to conversation.qmd for split_thinking example * update thinking => reasoning_content in messages format --------- Co-authored-by: Wing Lian --- docs/config.qmd | 4 ++-- docs/dataset-formats/conversation.qmd | 28 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index 04d1320cad..18fc9dcf8a 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -184,8 +184,8 @@ datasets: # adding a system turn with empty content. drop_system_message: - # Optional[bool]. Whether to split the assistant turn based on a reasoning trace inside delimited tags - # defaults to False + # Optional[bool]. (for Qwen3 template only) Whether to split the assistant content based on a reasoning trace inside delimited tags + # See example at `docs/dataset-formats/conversation.qmd` split_thinking: # IMPORTANT: The following fields determine which parts of the conversation to train on. diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index ee9f7391b8..87c2941e63 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -196,6 +196,34 @@ datasets: It is not necessary to set both `message_field_training` and `message_field_training_detail` at once. ::: +8. (For Qwen3 template only) Enable reasoning split, where the reasoning is split from the content and passed as a separate field into the template. + +```yaml +datasets: + - path: ... + type: chat_template + chat_template: qwen3 + split_thinking: true +``` + +For example, a content can look like: + +```json +{ + "content": "Some thinking outputsOutput after thinking." +} +``` + +After split, it will look like: + +```json +{ + "reasoning_content": "Some thinking outputs", + "content": "Output after thinking..." +} +``` + + ## sharegpt ::: {.callout-important} From cd84325253fdd0ac4c9e218cdf3309be0ea4b53a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 May 2025 20:05:51 -0400 Subject: [PATCH 0635/1405] allow plugins to return their own dataset (#2617) [skip ci] * allow plugins to return their own dataset * add post_trainer_create and wire up * add hook check * address PR feedback: * remove annotation causing circular import --- src/axolotl/cli/preprocess.py | 6 +- src/axolotl/cli/train.py | 11 +- src/axolotl/integrations/base.py | 151 +++++++++++++++++++-------- src/axolotl/train.py | 4 +- tests/e2e/integrations/test_hooks.py | 7 ++ 5 files changed, 129 insertions(+), 50 deletions(-) diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index 5585c88a7a..2a4dcd2886 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -18,6 +18,7 @@ from axolotl.cli.config import load_cfg from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH from axolotl.common.datasets import load_datasets, load_preference_datasets +from axolotl.integrations.base import PluginManager from axolotl.utils.dict import DictDefault from axolotl.utils.trainer import disable_datasets_caching @@ -47,7 +48,10 @@ def do_preprocess(cfg: DictDefault, cli_args: PreprocessCliArgs) -> None: cfg.dataset_prepared_path = DEFAULT_DATASET_PREPARED_PATH with disable_datasets_caching(): - if cfg.rl: + plugin_manager = PluginManager.get_instance() + if plugin_manager.load_datasets(cfg, preprocess=True): + pass + elif cfg.rl: load_preference_datasets(cfg=cfg, cli_args=cli_args) else: load_datasets(cfg=cfg, cli_args=cli_args) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 9e90cede38..777d848853 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -43,10 +43,13 @@ def do_train(cfg: DictDefault, cli_args: TrainerCliArgs): if int(os.getenv("LOCAL_RANK", "0")) == 0: check_user_token() - if cfg.rl: - dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) - else: - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + plugin_manager = PluginManager.get_instance() + dataset_meta = plugin_manager.load_datasets(cfg, preprocess=False) + if not dataset_meta: + if cfg.rl: + dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) + else: + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) model, tokenizer, trainer = train(cfg=cfg, dataset_meta=dataset_meta) diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index efe542af74..97cbac6938 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -26,6 +26,8 @@ import torch from torch.optim.lr_scheduler import LRScheduler +from axolotl.utils.dict import DictDefault + class BasePlugin: """ @@ -36,11 +38,13 @@ class BasePlugin: Methods: register(cfg): Registers the plugin with the given configuration. + load_datasets(cfg): Loads and preprocesses the dataset for training. pre_model_load(cfg): Performs actions before the model is loaded. post_model_build(cfg, model): Performs actions after the model is loaded, but before LoRA adapters are applied. pre_lora_load(cfg, model): Performs actions before LoRA weights are loaded. post_lora_load(cfg, model): Performs actions after LoRA weights are loaded. post_model_load(cfg, model): Performs actions after the model is loaded, inclusive of any adapters. + post_trainer_create(cfg, trainer): Performs actions after the trainer is created. create_optimizer(cfg, trainer): Creates and returns an optimizer for training. create_lr_scheduler(cfg, trainer, optimizer, num_training_steps): Creates and returns a learning rate scheduler. add_callbacks_pre_trainer(cfg, model): Adds callbacks to the trainer before training. @@ -63,20 +67,32 @@ def register(self, cfg): # pylint: disable=unused-argument None """ - def get_input_args(self): + def get_input_args(self) -> str | None: """ Returns a pydantic model for the plugin's input arguments. """ + def load_datasets(self, cfg: DictDefault, preprocess: bool = False): + """ + Loads and preprocesses the dataset for training. + + Args: + cfg: The configuration for the plugin. + preprocess: Whether this is the preprocess step of the datasets. + + Returns: + dataset_meta: The metadata for the training dataset. + """ + def pre_model_load(self, cfg): # pylint: disable=unused-argument """ Performs actions before the model is loaded. - Parameters: - cfg (dict): The configuration for the plugin. + Args: + cfg (dict): The configuration for the plugin. Returns: - None + None """ def post_model_build(self, cfg, model): # pylint: disable=unused-argument @@ -91,59 +107,71 @@ def post_model_load(self, cfg, model): # pylint: disable=unused-argument """ Performs actions after the model is loaded. - Parameters: - cfg (dict): The configuration for the plugin. - model (object): The loaded model. + Args: + cfg (dict): The configuration for the plugin. + model (object): The loaded model. Returns: - None + None """ def pre_lora_load(self, cfg, model): # pylint: disable=unused-argument """ Performs actions before LoRA weights are loaded. - Parameters: - cfg (dict): The configuration for the plugin. - model (object): The loaded model. + Args: + cfg (dict): The configuration for the plugin. + model (object): The loaded model. Returns: - None + None """ def post_lora_load(self, cfg, model): # pylint: disable=unused-argument """ Performs actions after LoRA weights are loaded. - Parameters: - cfg (dict): The configuration for the plugin. - model (object): The loaded model. + Args: + cfg (dict): The configuration for the plugin. + model (object): The loaded model. Returns: - None + None """ def get_trainer_cls(self, cfg): # pylint: disable=unused-argument): """ Returns a custom class for the trainer. - Parameters: - cfg (dict): The global axolotl configuration. + Args: + cfg (dict): The global axolotl configuration. + + Returns: + class: The class for the trainer. + """ + + def post_trainer_create(self, cfg, trainer): # pylint: disable=unused-argument + """ + Performs actions after the trainer is created. + + Args: + cfg (dict): The configuration for the plugin. + trainer (object): The trainer object for training. Returns: - class: The class for the trainer. + None """ def create_optimizer(self, cfg, trainer): # pylint: disable=unused-argument """ Creates and returns an optimizer for training. - Parameters: - cfg (dict): The configuration for the plugin. - trainer (object): The trainer object for training. + Args: + cfg (dict): The configuration for the plugin. + trainer (object): The trainer object for training. Returns: - object: The created optimizer. + object: The created optimizer. """ def create_lr_scheduler( @@ -152,26 +180,26 @@ def create_lr_scheduler( """ Creates and returns a learning rate scheduler. - Parameters: - cfg (dict): The configuration for the plugin. - trainer (object): The trainer object for training. - optimizer (object): The optimizer for training. - num_training_steps (int): Total number of training steps + Args: + cfg (dict): The configuration for the plugin. + trainer (object): The trainer object for training. + optimizer (object): The optimizer for training. + num_training_steps (int): Total number of training steps Returns: - object (LRScheduler): The created learning rate scheduler. + object (LRScheduler): The created learning rate scheduler. """ def add_callbacks_pre_trainer(self, cfg, model): # pylint: disable=unused-argument """ setup callbacks before creating the trainer. - Parameters: - cfg (dict): The configuration for the plugin. - model (object): The loaded model. + Args: + cfg (dict): The configuration for the plugin. + model (object): The loaded model. Returns: - List[callable]: A list of callback functions to be added to the TrainingArgs + List[callable]: A list of callback functions to be added to the TrainingArgs """ return [] @@ -182,12 +210,12 @@ def add_callbacks_post_trainer( Adds callbacks to the trainer after creating the trainer. This is useful for callbacks that require access to the model or trainer. - Parameters: - cfg (dict): The configuration for the plugin. - trainer (object): The trainer object for training. + Args: + cfg (dict): The configuration for the plugin. + trainer (object): The trainer object for training. Returns: - List[callable]: A list of callback functions to be added + List[callable]: A list of callback functions to be added """ return [] @@ -195,23 +223,23 @@ def post_train(self, cfg, model): # pylint: disable=unused-argument """ Performs actions after training is complete. - Parameters: - cfg (dict): The axolotl configuration - model (object): The loaded model. + Args: + cfg (dict): The axolotl configuration + model (object): The loaded model. Returns: - None + None """ def post_train_unload(self, cfg): # pylint: disable=unused-argument """ Performs actions after training is complete and the model is unloaded. - Parameters: - cfg (dict): The configuration for the plugin. + Args: + cfg (dict): The configuration for the plugin. Returns: - None + None """ @@ -338,6 +366,27 @@ def get_input_args(self): input_args.append(input_args_from_plugin) return input_args + def load_datasets(self, cfg, preprocess: bool = False): + """ + Calls the load_datasets method of each registered plugin. + + Args: + cfg: The configuration for the plugins. + preprocess : Whether this is preprocess step of the datasets. + + Returns: + dataset_meta: The dataset metadata loaded from all registered plugins. + """ + return_ds_meta = None + for plugin in self.plugins.values(): + dataset_meta = plugin.load_datasets(cfg, preprocess) + if dataset_meta is not None: + if return_ds_meta is None: + return_ds_meta = dataset_meta + else: + raise RuntimeError("Multiple plugins loaded datasets") + return return_ds_meta + def pre_model_load(self, cfg): """ Calls the pre_model_load method of all registered plugins. @@ -422,6 +471,20 @@ def get_trainer_cls(self, cfg): return trainer_cls return None + def post_trainer_create(self, cfg, trainer): + """ + Calls the post_trainer_create method of all registered plugins. + + Parameters: + cfg (dict): The configuration for the plugins. + trainer (object): The trainer object for training. + + Returns: + None + """ + for plugin in self.plugins.values(): + plugin.post_trainer_create(cfg, trainer) + def create_optimizer(self, trainer): """ Calls the create_optimizer method of all registered plugins and returns the first non-None optimizer. diff --git a/src/axolotl/train.py b/src/axolotl/train.py index a5fd5e2e06..67a80b410b 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -528,6 +528,9 @@ def train( processor, ) = setup_model_and_trainer(cfg, dataset_meta) + plugin_manager = PluginManager.get_instance() + plugin_manager.post_trainer_create(cfg, trainer) + # Handle untrained tokens if configured safe_serialization = cfg.save_safetensors is True train_dataset = dataset_meta.train_dataset @@ -550,7 +553,6 @@ def train( if not cfg.use_ray: cleanup_distributed() - plugin_manager = PluginManager.get_instance() plugin_manager.post_train(cfg, model) return model, tokenizer, trainer diff --git a/tests/e2e/integrations/test_hooks.py b/tests/e2e/integrations/test_hooks.py index 9b12e6d4ef..45d7200fb6 100644 --- a/tests/e2e/integrations/test_hooks.py +++ b/tests/e2e/integrations/test_hooks.py @@ -29,6 +29,12 @@ def __init__(self): except FileNotFoundError: pass + def post_trainer_create(self, cfg, trainer): # pylint: disable=unused-argument + with open( + self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" + ) as f: + f.write("post_trainer_create\n") + def pre_model_load(self, cfg): # pylint: disable=unused-argument with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" @@ -165,6 +171,7 @@ def test_plugin_hooks(self, temp_dir): ) as f: file_contents = f.readlines() file_contents = "\n".join(file_contents) + assert "post_trainer_create" in file_contents assert "pre_model_load" in file_contents assert "post_model_build" in file_contents assert "pre_lora_load" in file_contents From 8e4158cc0b295f713594c918685068cec305d421 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 May 2025 20:08:08 -0400 Subject: [PATCH 0636/1405] Multipack parallel bin packing (#2631) * improve readability of multipack sampler * parallel bin packing fix error with lambda and pickling make sure things are in float instead of np.float * annotations and comments update * support for configurable group and bin size for sample packing * fix missing map back to original indices --- src/axolotl/core/trainers/base.py | 2 + src/axolotl/utils/samplers/multipack.py | 407 ++++++++++++++++-------- 2 files changed, 284 insertions(+), 125 deletions(-) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 3864903a5a..ab9735adcd 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -114,6 +114,8 @@ def _create_multipack_sampler( packing_efficiency_estimate=self.args.sample_packing_efficiency, batch_max_len=batch_max_len, batch_size=batch_size, + group_size=self.args.sample_packing_group_size, + bin_size=self.args.sample_packing_bin_size, sequential=self.args.sample_packing_sequentially, drop_last=True, ) diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 1e1172cb55..a0c30b0d4c 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -1,10 +1,13 @@ -# pylint: skip-file """ -Multipack Batch Sampler +Multipack Batch Sampler - An efficient batch sampler for packing variable-length sequences +into fixed-capacity batches to optimize memory usage and training throughput. """ + import logging import math -from typing import Any, Iterable, List, Union +from concurrent.futures import ProcessPoolExecutor +from multiprocessing import cpu_count +from typing import Iterable, Union import numba import numpy as np @@ -13,26 +16,39 @@ from axolotl.utils.distributed import reduce_and_broadcast LOG = logging.getLogger(__name__) - LOG.setLevel(logging.INFO) @numba.njit -def ffd_check(a: np.ndarray, c: int, n: int): - # First-fit-decreasing bin packing - # Check if a[] could fit in n bins with capacity c - # https://en.wikipedia.org/wiki/First-fit-decreasing_bin_packing - - a = np.sort(a)[::-1] - bins = np.full((n,), c, dtype=a.dtype) - for size in a: +def ffd_check(sequence_lengths: np.ndarray, bin_capacity: int, num_bins: int): + """ + First-fit-decreasing bin packing algorithm check + + Checks if sequences with the given lengths could fit in the specified number of bins + + Args: + sequence_lengths: Array of sequence lengths + bin_capacity: Maximum capacity of each bin + num_bins: Number of bins available + + Returns: + True if all sequences can be packed, False otherwise + """ + # Sort sequence lengths in descending order for optimal packing + sequence_lengths = np.sort(sequence_lengths)[::-1] + # Initialize all bins with full capacity + bins = np.full((num_bins,), bin_capacity, dtype=sequence_lengths.dtype) + + # Try to place each sequence in the first bin it fits + for size in sequence_lengths: not_found = True - for idx in range(n): + for idx in range(num_bins): if bins[idx] >= size: bins[idx] -= size not_found = False break + # If no bin could fit this sequence, packing failed if not_found: return False @@ -40,86 +56,132 @@ def ffd_check(a: np.ndarray, c: int, n: int): @numba.njit -def ffd_with_result(a: np.ndarray, c: int, start_index: int): - # First-fit-decreasing bin packing (with result return) +def pack_group( + sequence_lengths: np.ndarray, + group_offset: int, + bin_capacity: int, + max_bins: int, + bin_size: int, + safe_mode: bool = True, +): + """ + Pack a group of sequences into bins using First-Fit Decreasing algorithm - indices = np.argsort(a)[::-1] - a = a[indices] + Args: + sequence_lengths: Array of sequence lengths + group_offset: Offset to apply to indices when returning results + bin_capacity: Maximum capacity of each bin + max_bins: Maximum number of bins to use + bin_size: Maximum number of sequences per bin + safe_mode: If True, use a more conservative packing approach - bins: List[Any] = [] - bins_result: List[Any] = [] - for a_id, size in enumerate(a): - add_new = True - for idx in range(len(bins)): - if bins[idx] >= size: - bins[idx] -= size - bins_result[idx].append(indices[a_id] + start_index) - add_new = False + Returns: + List of bins, where each bin contains indices of sequences assigned to it + """ + # Get sorting indices and sort lengths in descending order + indices = np.argsort(sequence_lengths)[::-1] + sorted_lengths = sequence_lengths[indices] + + bins_remaining_space: list = [] # Tracks remaining capacity in each bin + bins_assigned_sequences: list = [] # Tracks sequence indices assigned to each bin + + for seq_id, size in enumerate(sorted_lengths): + global_idx = indices[seq_id] + group_offset + + # Try to place sequence in existing bins + add_new_bin = True + for bin_idx, _ in enumerate(bins_remaining_space): + if ( + bins_remaining_space[bin_idx] >= size + and len(bins_assigned_sequences[bin_idx]) < bin_size + ): + bins_remaining_space[bin_idx] -= size + bins_assigned_sequences[bin_idx].append(global_idx) + add_new_bin = False break - if add_new: - bins.append(c - size) - bins_result.append([indices[a_id] + start_index]) + # Create a new bin if needed and if we haven't reached the limit + if add_new_bin: + if len(bins_remaining_space) >= max_bins and safe_mode: + # In safe mode, skip items that would exceed max_bins + continue + bins_remaining_space.append(bin_capacity - size) + bins_assigned_sequences.append([global_idx]) - return bins_result + # Safety check to avoid infinite bins + if len(bins_remaining_space) > len(sequence_lengths): + break + return bins_assigned_sequences -@numba.njit -def allocate( - lengths: np.ndarray, lengths_cumsum: np.ndarray, rank: int, c: int, n: int -): - # Dynamic batch allocator, similar to Multifit - # https://en.wikipedia.org/wiki/Multifit_algorithm - # ~99.5% efficiency on OpenChat training set (12 * 2048 ctx len) - s = 0 - start_index = 0 - result = [] +# Define a standalone function for multiprocessing +def _process_group(args): + group_lengths, start_idx, bin_capacity, max_bins, bin_size, safe_mode = args + return pack_group( + group_lengths, start_idx, bin_capacity, max_bins, bin_size, safe_mode + ) - while True: - # binary search [l, r) - left = 1 - right = 1 + np.searchsorted(lengths_cumsum[start_index:], s + c * n, "right") - - while right - left > 1: - mid = (left + right) // 2 - if ffd_check(lengths[start_index : start_index + mid], c, n): - left = mid - else: - right = mid - - # use length l - batch = ffd_with_result( - lengths[start_index : start_index + left], c, start_index - ) - assert len(batch) <= n - if len(batch) < n: - break - start_index += left - s = lengths_cumsum[start_index - 1] +def pack_parallel( + sequence_lengths: np.ndarray, + bin_capacity: int, + group_size: int, + bin_size: int, + num_processes: int | None = None, + safe_mode: bool = True, +): + """ + Pack sequences into bins using parallel processing + + Args: + sequence_lengths: Array of sequence lengths + bin_capacity: Maximum capacity of each bin as total number of tokens + group_size: Number of sequences to process in each group + bin_size: Maximum number of bins to use + num_processes: Number of parallel processes to use + safe_mode: If True, use a more conservative packing approach - # add local rank - result.append(batch[rank]) + Returns: + List of bins, where each bin contains indices of sequences assigned to it + """ + num_items = len(sequence_lengths) + if num_processes is None: + num_processes = max(1, min(num_items // group_size, cpu_count())) + + # Create tasks for parallel processing + tasks = [] + for i in range(0, num_items, group_size): + group_lengths = sequence_lengths[i : i + group_size] + max_bins = len(group_lengths) # Allow as many bins as items in the group + tasks.append((group_lengths, i, bin_capacity, max_bins, bin_size, safe_mode)) + + # Process groups in parallel + all_bins = [] + with ProcessPoolExecutor(max_workers=num_processes) as executor: + for group_bins in executor.map(_process_group, tasks): + all_bins.extend(group_bins) - return result, s, len(result) * c * n + return all_bins @numba.njit -def allocate_sequentially(lengths: np.ndarray, rank: int, c: int, n: int): +def allocate_sequentially( + sequence_lengths: np.ndarray, rank: int, bin_capacity: int, num_ranks: int +): """ Sequential allocator that preserves example order - Parameters: - - lengths: The lengths of all examples - - rank: The current rank (for distributed training) - - c: The capacity of each bin (maximum sequence length) - - n: Number of ranks + Args: + sequence_lengths: The lengths of all examples + rank: The current rank (for distributed training) + bin_capacity: The capacity of each bin (maximum sequence length) + num_ranks: Number of ranks (processes/GPUs) Returns: - - result: List of batches for the current rank - - total_used: Number of actual example tokens - - total_slots: Maximum theoretical number of example tokens (number of bins * bin capacity) + rank_batches: List of batches for the current rank + total_tokens_used: Number of actual example tokens + total_token_slots: Maximum theoretical number of example tokens (number of bins * bin capacity) """ result = [] total_used = 0 @@ -127,9 +189,9 @@ def allocate_sequentially(lengths: np.ndarray, rank: int, c: int, n: int): # First, do sequential packing into bins all_bins = [] current_bin = [0 for i in range(0)] # numba hint - remaining_capacity = c + remaining_capacity = bin_capacity - for idx, size in enumerate(lengths): + for idx, size in enumerate(sequence_lengths): if size <= remaining_capacity: # Example fits in current bin current_bin.append(idx) @@ -140,7 +202,7 @@ def allocate_sequentially(lengths: np.ndarray, rank: int, c: int, n: int): if current_bin: # Add non-empty bin to all_bins all_bins.append(current_bin) current_bin = [idx] - remaining_capacity = c - size + remaining_capacity = bin_capacity - size total_used += size # Add the last bin if not empty @@ -148,132 +210,227 @@ def allocate_sequentially(lengths: np.ndarray, rank: int, c: int, n: int): all_bins.append(current_bin) # Assign bins to ranks - each rank gets every n-th bin - for bin_idx in range(rank, len(all_bins), n): + for bin_idx in range(rank, len(all_bins), num_ranks): result.append(all_bins[bin_idx]) - return result, total_used, len(all_bins) * c + return result, total_used, len(all_bins) * bin_capacity class MultipackBatchSampler(BatchSampler): - """Batch sampler class for multipack""" + """ + Batch sampler class for efficient packing of variable-length sequences + + This sampler packs sequences into fixed-capacity bins (batches) to maximize + GPU memory utilization and training throughput by reducing padding. + + It supports both parallel packing (using FFD algorithm) and + sequential packing (preserving original sequence order). + """ def __init__( self, sampler: Union[Sampler[int], Iterable[int]], - batch_size: int, - batch_max_len: int, - lengths: np.ndarray, - packing_efficiency_estimate: float = 1.0, - drop_last: bool = False, - num_count_samples: int = 16, - sequential: bool = False, - **kwargs, + batch_size: int, # Number of bins per batch + batch_max_len: int, # Maximum sequence length (bin capacity) + lengths: np.ndarray, # Sequence lengths + packing_efficiency_estimate: float = 1.0, # Initial efficiency estimate + drop_last: bool = False, # Whether to drop final batches (might be incomplete) + num_count_samples: int = 16, # Number of times to estimate batch count + sequential: bool = False, # Whether to use sequential packing + group_size: int = 100_000, # Size of groups for parallel packing + bin_size: int = 200, # The max number of samples that can be packed in a single bin + num_processes: int | None = None, # Number of processes for parallel packing + safe_mode: bool = True, # Conservative packing to prevent training instability + **kwargs, # pylint: disable=unused-argument ): super().__init__(sampler, batch_size, drop_last) self.batch_size = batch_size self.batch_max_len = batch_max_len - self.lengths: np.ndarray = lengths + self.lengths = np.array(lengths, dtype=np.int32) self.packing_efficiency_estimate = packing_efficiency_estimate or 1.0 self.sequential = sequential + self.group_size = group_size + self.bin_size = bin_size + self.num_processes = num_processes + self.safe_mode = safe_mode assert isinstance(self.lengths, np.ndarray) self.epoch = 0 - # statistics - self.eff_total_used = 0 - self.eff_total_slots = 0 + # Efficiency statistics tracking + self.total_tokens_used = 0 + self.total_token_slots = 0 - # The number of times to calculate the batches to determine the minimum packed dataset length for the local rank + # The number of times to calculate batches to determine minimum packed dataset length self.num_count_samples = num_count_samples - # the minimum packed dataset length across all ranks determined by a gather/broadcast + # Minimum packed dataset length across all ranks (determined by gather/broadcast) self.len_across_ranks = None + # Cache for batches + self._batches = None + if self.sequential and not isinstance(sampler, SequentialSampler): LOG.warning( "using sequential sample packing with non-sequential sampler, did you want to also enable curriculum_sampling?" ) def set_epoch(self, epoch: int): + """Set the epoch number, used for reproducible shuffling across epochs""" self.epoch = epoch + self._batches = None # Invalidate batch cache def generate_batches(self, set_stats=False): - indices = [idx for idx in self.sampler] + """ + Generate packed batches for training + + Args: + set_stats: Whether to update efficiency statistics + + Returns: + List of batches, where each batch contains multiple bins, + and each bin contains multiple sequence indices + """ + if self._batches is not None: + return self._batches + + # Get indices from the sampler + indices = [ # pylint: disable=unnecessary-comprehension + idx for idx in self.sampler + ] + # Get lengths of the selected sequences lengths = self.lengths[indices] - lengths_cumsum = np.cumsum(lengths) + # Pack sequences into bins using either sequential or parallel packing if self.sequential: - batches, total_used, total_slots = allocate_sequentially( - lengths=lengths, + bins, total_used, total_slots = allocate_sequentially( + lengths, rank=0, - c=self.batch_max_len, - n=1, + bin_capacity=self.batch_max_len, + num_ranks=1, ) + # Map bin indices back to original indices + bins = [[indices[b_idx] for b_idx in bin_indices] for bin_indices in bins] else: - batches, total_used, total_slots = allocate( - lengths=lengths, - lengths_cumsum=lengths_cumsum, - rank=0, - c=self.batch_max_len, - n=1, + # Use parallel packing + all_bins = pack_parallel( + lengths, + bin_capacity=self.batch_max_len, + group_size=self.group_size, + bin_size=self.bin_size, + num_processes=self.num_processes, + safe_mode=self.safe_mode, ) - batches = [ - [ - [indices[b_idx] for b_idx in batch] - for batch in batches[i : i + self.batch_size] + # Map bin indices back to original indices + bins = [ + [indices[b_idx] for b_idx in bin_indices] for bin_indices in all_bins ] - for i in range(0, len(batches), self.batch_size) + + # Calculate efficiency statistics + total_used = lengths.sum() + total_slots = len(all_bins) * self.batch_max_len + + # Group bins into batches (each batch contains batch_size bins) + batches = [ + bins[i : i + self.batch_size] for i in range(0, len(bins), self.batch_size) ] - # statistics + # Drop last batch if requested and it's incomplete + if self.drop_last and len(batches[-1]) < self.batch_size: + batches = batches[:-1] + # Adjust total_slots if we dropped a batch + if not self.sequential: + total_slots -= (self.batch_size - len(batches[-1])) * self.batch_max_len + + # Update statistics if requested if set_stats: - self.eff_total_used += total_used - self.eff_total_slots += total_slots + self.total_tokens_used += total_used + self.total_token_slots += total_slots + self._batches = batches return batches def __iter__(self): + """ + Return an iterator over batches + + The batches are truncated to match the minimum number of batches across all ranks + to ensure distributed training balance + """ batches = self.generate_batches(set_stats=True) if self.len_across_ranks: - # make sure the batches we iterate over is truncated to the same min length across all ranks + # Truncate batches to ensure all ranks have the same number of batches batches = batches[: self.len_across_ranks] return iter(batches) - def num_batches(self): - batches = self.generate_batches(set_stats=True) - return len(batches) - def efficiency(self): - return self.eff_total_used / self.eff_total_slots + """ + Calculate the packing efficiency (ratio of tokens used to total token slots) + Higher is better - 1.0 would mean perfect packing with no wasted space + """ + if self.total_token_slots == 0: + self.generate_batches(set_stats=True) + if self.total_token_slots == 0: + return 0.0 + # Return a Python float instead of potentially a numpy float + return float(self.total_tokens_used / self.total_token_slots) def gather_efficiency(self): - def calc_sample_packing_eff_est(estimates: List[float]): + """ + Gather and synchronize packing efficiency estimates across all distributed ranks + Returns a conservative efficiency estimate based on the measurements + """ + + def calc_sample_packing_eff_est(estimates: list[float]): LOG.debug(f"sample_packing_eff_est across ranks: {repr(estimates)}") - return math.floor(0.997 * max(estimates)) + # Use 99.7% of max observed efficiency as a safe estimate + max_eff = max(float(eff) for eff in estimates) + return math.floor(0.997 * max_eff) + # Gather efficiency from all ranks and apply the calculation function sample_packing_actual_eff_all = reduce_and_broadcast( - lambda: self.efficiency(), # pylint: disable=unnecessary-lambda + lambda: float(self.efficiency()), # pylint: disable=unnecessary-lambda calc_sample_packing_eff_est, ) + + # Quantize to 0.5% intervals for stability sample_packing_eff_est = ( math.ceil(sample_packing_actual_eff_all * 200.0) / 200.0 ) return sample_packing_eff_est def gather_len_batches(self, num): + """ + Gather and synchronize batch counts across all distributed ranks + Returns the minimum number of batches available on any rank + """ + def calc_min_len(estimates: list[(int, float)]): LOG.info(f"gather_len_batches: {repr(estimates)}") return math.floor(min(estimates)) + # Find minimum batch count across ranks to ensure balance min_len_batches = reduce_and_broadcast(lambda: num, calc_min_len) return min_len_batches def __len__(self): - if not self.len_across_ranks: - len_batches = min( - [self.num_batches() for _ in range(self.num_count_samples)] + """ + Return the total number of batches that will be yielded by this sampler + + This is calculated as the minimum number of batches available on any rank + to ensure balanced distributed training + """ + if self._batches is None: + self._batches = self.generate_batches(set_stats=True) + + if self.len_across_ranks is None: + # Sample multiple times to get stable estimate + len_batches = min( # pylint: disable=consider-using-generator + [len(self._batches) for _ in range(self.num_count_samples)] ) + # Gather minimum across all ranks self.len_across_ranks = self.gather_len_batches(len_batches) + return self.len_across_ranks From ff0fe767c8e58f2e74a7234a853c253367eaea34 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 May 2025 22:49:22 -0400 Subject: [PATCH 0637/1405] xformers attention with packing (#2619) * xformers attention with packing * wire up the patch * fix xformers + packing validation * fix warning * reorder the packing check * fix fp16 / bf16 reset when using fp16 with bf16 auto * fix seq lens calc to drop hanging sequences * handle xformers patch for inference too * fix batch size setter * fix xformers inference * add colab callback to fix inference post train * PR feedback --- docs/config.qmd | 3 +- src/axolotl/core/trainer_builder.py | 6 + src/axolotl/monkeypatch/attention/__init__.py | 19 +++ src/axolotl/monkeypatch/attention/xformers.py | 160 ++++++++++++++++++ src/axolotl/utils/callbacks/__init__.py | 25 +++ src/axolotl/utils/config/__init__.py | 3 + src/axolotl/utils/models.py | 5 + src/axolotl/utils/schemas/config.py | 13 +- 8 files changed, 222 insertions(+), 12 deletions(-) create mode 100644 src/axolotl/monkeypatch/attention/xformers.py diff --git a/docs/config.qmd b/docs/config.qmd index 18fc9dcf8a..fae64501af 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -73,11 +73,12 @@ load_in_8bit: true load_in_4bit: # Use CUDA bf16 -bf16: true # bool or 'full' for `bf16_full_eval`. require >=ampere +bf16: true # bool or 'full' for `bf16_full_eval`, or 'auto' for automatic detection. require >=ampere # Use CUDA fp16 fp16: true # Use CUDA tf32 tf32: true # require >=ampere +# Note: if bf16 is set to 'auto', and fp16 is set to true, we will prefer the explict fp16 setting # No AMP (automatic mixed precision) bfloat16: true # require >=ampere diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 99d97800c8..af9a43db32 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -21,6 +21,7 @@ import inspect import logging import math +import os import sys from abc import abstractmethod from pathlib import Path @@ -72,6 +73,7 @@ SaveBetterTransformerModelCallback, bench_eval_callback_factory, causal_lm_bench_eval_callback_factory, + colab_inference_post_train_callback, log_prediction_callback_factory, ) from axolotl.utils.callbacks.lisa import lisa_callback_factory @@ -293,6 +295,10 @@ def get_post_trainer_create_callbacks(self, trainer): if self.cfg.lisa_step_interval and self.cfg.lisa_n_layers: callbacks.append(lisa_callback_factory(trainer)) + if any("COLAB_" in key for key in os.environ): + ColabCallback = colab_inference_post_train_callback(trainer) + callbacks.append(ColabCallback(self.cfg)) + callbacks.extend(super().get_post_trainer_create_callbacks(trainer=trainer)) return callbacks diff --git a/src/axolotl/monkeypatch/attention/__init__.py b/src/axolotl/monkeypatch/attention/__init__.py index e69de29bb2..15ed764f40 100644 --- a/src/axolotl/monkeypatch/attention/__init__.py +++ b/src/axolotl/monkeypatch/attention/__init__.py @@ -0,0 +1,19 @@ +""" +attention module for attention monkeypatches +""" + +from transformers.integrations.flash_attention import flash_attention_forward + + +def patch_xformers_attn_over_fa2(): + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + from .xformers import xformers_attention_forward + + ALL_ATTENTION_FUNCTIONS["flash_attention_2"] = xformers_attention_forward + + +def unpatch_xformers_attn_over_fa2(): + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + ALL_ATTENTION_FUNCTIONS["flash_attention_2"] = flash_attention_forward() diff --git a/src/axolotl/monkeypatch/attention/xformers.py b/src/axolotl/monkeypatch/attention/xformers.py new file mode 100644 index 0000000000..5901963f05 --- /dev/null +++ b/src/axolotl/monkeypatch/attention/xformers.py @@ -0,0 +1,160 @@ +""" +xformers attention implementation for packing +""" + +from typing import Optional + +import torch +import xformers +import xformers.ops.fmha +from transformers.modeling_flash_attention_utils import ( + _upad_input, +) + +from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids + +xformers_attention = xformers.ops.fmha.memory_efficient_attention + + +def xformers_attention_forward( + module: torch.nn.Module, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + dropout: float = 0.0, # pylint: disable=unused-argument + scaling: Optional[float] = None, # pylint: disable=unused-argument + sliding_window: Optional[int] = None, # pylint: disable=unused-argument + softcap: Optional[float] = None, # pylint: disable=unused-argument + cu_seq_lens_q: Optional[torch.LongTensor] = None, + cu_seq_lens_k: Optional[torch.LongTensor] = None, + max_length_q: Optional[int] = None, + max_length_k: Optional[int] = None, # pylint: disable=unused-argument + **kwargs, # pylint: disable=unused-argument +): + # Get dimensions + # query: [batch, heads, seq_len, hidden_dim] + batch_size = query.size(0) + query_length = query.shape[2] + key_length = key.shape[2] + + # Default causal mask + attn_bias = xformers.ops.LowerTriangularMask() + + # Check if we have sliding window attention + has_sliding_window = sliding_window is not None and sliding_window < query_length + + # Transpose dimensions for xformers (Q: [b, h, s, d] -> [b, s, h, d]) + query = query.transpose(1, 2) + key = key.transpose(1, 2) + value = value.transpose(1, 2) + + # Get GQA parameters + num_attention_heads = module.config.num_attention_heads + num_key_value_heads = module.config.num_key_value_heads + head_dim = query.size(-1) + is_gqa = num_attention_heads != num_key_value_heads + n_groups = num_attention_heads // num_key_value_heads if is_gqa else 1 + + # If position_ids is provided and check all examples do not contain only 1 sequence, If tensor in increasing + # then we probably have one sequence, otherwise it is packed. Additionally check we are in pre-fill/training stage. + # Use `flash_attn_varlen_func` to prevent cross-example attention and also allow padding free approach + if position_ids is not None and ( + max_length_q is not None + or (query_length != 1 and not (torch.diff(position_ids, dim=-1) >= 0).all()) + ): + if cu_seq_lens_q is None or cu_seq_lens_k is None: + cu_seq_lens_q = get_cu_seqlens_from_pos_ids(position_ids)[0] + cu_seq_lens_q = cu_seq_lens_q.squeeze() + seq_lengths = cu_seq_lens_q[1:] - cu_seq_lens_q[:-1] + attn_bias = ( + xformers.ops.fmha.attn_bias.BlockDiagonalCausalMask.from_seqlens( + q_seqlen=seq_lengths.tolist(), + ) + ) + else: + query = query.reshape(-1, query.size(-2), query.size(-1)) + key = key.reshape(-1, key.size(-2), key.size(-1)) + value = value.reshape(-1, value.size(-2), value.size(-1)) + + # Handle GQA + if is_gqa: + key = key.repeat_interleave(n_groups, dim=2) + value = value.repeat_interleave(n_groups, dim=2) + + elif attention_mask is not None: + query, key, value, _, cu_seq_lens, _ = _upad_input( + query, key, value, attention_mask, query_length + ) + cu_seq_lens_q, cu_seq_lens_k = cu_seq_lens + seq_lengths = [] + for i in range(len(cu_seq_lens_q) - 1): + seq_lengths.append(cu_seq_lens_q[i + 1] - cu_seq_lens_q[i]) + attn_bias = xformers.ops.fmha.attn_bias.BlockDiagonalCausalMask.from_seqlens( + q_seqlen=seq_lengths, + kv_seqlen=seq_lengths, + ) + + # Handle GQA + if is_gqa: + key = key.repeat_interleave(n_groups, dim=2) + value = value.repeat_interleave(n_groups, dim=2) + else: + # Handle Group Query Attention (GQA) using view/expand approach from reference + key = key.view(batch_size, key_length, num_key_value_heads, 1, head_dim) + value = value.view(batch_size, key_length, num_key_value_heads, 1, head_dim) + key = key.expand( + batch_size, key_length, num_key_value_heads, n_groups, head_dim + ) + value = value.expand( + batch_size, key_length, num_key_value_heads, n_groups, head_dim + ) + + if module.training: + key = key.reshape(batch_size, key_length, num_attention_heads, head_dim) + value = value.reshape(batch_size, key_length, num_attention_heads, head_dim) + + if has_sliding_window: + query = query.view( + 1, batch_size * query_length, num_attention_heads, head_dim + ) + key = key.view( + 1, batch_size * key_length, num_attention_heads, head_dim + ) + value = value.view( + 1, batch_size * key_length, num_attention_heads, head_dim + ) + else: + query = query.view( + batch_size, query_length, num_key_value_heads, n_groups, head_dim + ) + + # If we need a sliding window attention + if has_sliding_window: + query = query.view( + 1, + batch_size * query_length, + num_key_value_heads, + n_groups, + head_dim, + ) + key = key.view( + 1, batch_size * key_length, num_key_value_heads, n_groups, head_dim + ) + value = value.view( + 1, batch_size * key_length, num_key_value_heads, n_groups, head_dim + ) + + # Run the xformers attention + attn_output = xformers_attention( + query, + key, + value, + attn_bias=attn_bias, + ) + + attn_output = attn_output.view( + batch_size, -1, attn_output.size(-2), attn_output.size(-1) + ) + return attn_output, None diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 21b14d9860..0e7b060939 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -868,3 +868,28 @@ def on_epoch_end( ): torch.cuda.empty_cache() gc.collect() + + +def colab_inference_post_train_callback(trainer: Trainer): + class ColabCallback(TrainerCallback): + """Callback to prep model for inference on Google Colab""" + + def __init__(self, cfg): + self.gpu_name = torch.cuda.get_device_name(0) + self.cfg = cfg + + def on_train_end( + self, args, state, control, **kwargs + ): # pylint: disable=unused-argument + """ + handle T4 gpu, we need to convert attention to eager for inference + """ + if "Tesla T4" in self.gpu_name and self.cfg.xformers_attention: + trainer.model.config._attn_implementation = ( # pylint: disable=protected-access + "eager" + ) + trainer.model.gradient_checkpointing_disable() + trainer.model.config.use_cache = True + trainer.model.eval() + + return ColabCallback diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 0de87fa5cf..a96cc1286b 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -70,6 +70,9 @@ def resolve_dtype(cfg): if cfg.fp16 is None and not cfg.float16: cfg.fp16 = True + if cfg.fp16 and cfg.bf16 == "auto": + cfg.bf16 = False + if cfg.device == "mps": cfg.load_in_8bit = False cfg.tf32 = False diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index ba71ea4598..92766d44ca 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -556,6 +556,11 @@ def __init__( self.auto_model_loader = AutoModelForCausalLM # pylint: disable=invalid-name def apply_patches(self) -> None: + if self.cfg.xformers_attention and self.cfg.sample_packing: + from axolotl.monkeypatch.attention import patch_xformers_attn_over_fa2 + + patch_xformers_attn_over_fa2() + self.cfg.flash_attention = True if self.cfg.fsdp_config and str(self.cfg.fsdp_config.fsdp_version) == "2": from axolotl.monkeypatch.accelerate.fsdp2 import patch_accelerate_fsdp_utils diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 02308695c0..3527ec56ea 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -435,16 +435,6 @@ def check_gptq_w_revision(cls, data): ) return data - @model_validator(mode="before") - @classmethod - def check_sample_packing_w_xformers(cls, data): - if data.get("sample_packing") and data.get("xformers_attention"): - raise ValueError( - "sample_packing not compatible with xformers_attention. Use flash_attention" - ) - - return data - @model_validator(mode="before") @classmethod # pylint: disable=duplicate-code @@ -471,9 +461,10 @@ def check_sample_packing_wo_flash(cls, data): and not data.get("flash_attention") and not data.get("sdp_attention") and not data.get("flex_attention") + and not data.get("xformers_attention") ): LOG.warning( - "sample_packing without flash, sdp or flex attention does not handle cross sample decontamination." + "sample_packing without flash, sdp, xformers or flex attention does not handle cross sample decontamination." ) return data From 63aaccf85b5ee51c4ff0b911415273c3dfd0c227 Mon Sep 17 00:00:00 2001 From: Eric Meier Date: Tue, 6 May 2025 19:56:00 -0700 Subject: [PATCH 0638/1405] Fix cut_cross_entropy plugin install (#2642) [skip ci] --- .../integrations/cut_cross_entropy/monkeypatch/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/__init__.py diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/__init__.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 0d71b0aa5fd4a35a011a574a9233eecb4f2793bb Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 6 May 2025 23:40:44 -0400 Subject: [PATCH 0639/1405] Configurable embeddings upcast (#2621) * fsdp embeddings should be float32 per comment * patch peft to not upcast everything * add tabs back to code check * fix import * add configurable option and fix check * add check for dtypes * move embeddings test to patch dir * fix test * fix comment and logic --- docs/config.qmd | 2 + src/axolotl/monkeypatch/peft/__init__.py | 0 src/axolotl/monkeypatch/peft/utils.py | 78 +++++++++++++++++++++++ src/axolotl/utils/models.py | 12 +++- src/axolotl/utils/schemas/config.py | 1 + tests/e2e/patched/test_peft_embeddings.py | 63 ++++++++++++++++++ 6 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 src/axolotl/monkeypatch/peft/__init__.py create mode 100644 src/axolotl/monkeypatch/peft/utils.py create mode 100644 tests/e2e/patched/test_peft_embeddings.py diff --git a/docs/config.qmd b/docs/config.qmd index fae64501af..857d0eb030 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -32,6 +32,8 @@ tokenizer_legacy: resize_token_embeddings_to_32x: # Optional[bool] Whether to shrink the embeddings to len(tokenizer). By default, we won't shrink. shrink_embeddings: +# Optional[bool] Don't upcast the embeddings to float32 when using PEFT. Useful for low-VRAM GPUs +embeddings_skip_upcast: # Whether to load the model with randomly initialized weights. Useful for # pre-training a model from scratch or debugging purposes. random_init_weights: diff --git a/src/axolotl/monkeypatch/peft/__init__.py b/src/axolotl/monkeypatch/peft/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/peft/utils.py b/src/axolotl/monkeypatch/peft/utils.py new file mode 100644 index 0000000000..b3703d398f --- /dev/null +++ b/src/axolotl/monkeypatch/peft/utils.py @@ -0,0 +1,78 @@ +""" +Patch prepare_model_for_kbit_training to not upcast everything +""" + +import inspect +import logging + +import peft + +import axolotl +from axolotl.monkeypatch.utils import detab_code + +LOG = logging.getLogger(__name__) + +ORIGINAL_PREPARE_CODE = """ + for param in model.parameters(): + if ( + (param.dtype == torch.float16) or (param.dtype == torch.bfloat16) + ) and param.__class__.__name__ != "Params4bit": + param.data = param.data.to(torch.float32) +""" + +PATCHED_PREPARE_CODE = """ + for name, param in model.named_parameters(): + if ( + (param.dtype == torch.float16) or (param.dtype == torch.bfloat16) + ) and param.__class__.__name__ != "Params4bit" and all(embed_name not in name for embed_name in ["embed_tokens", "lm_head"]): + param.data = param.data.to(torch.float32) +""" + + +def get_peft_prep_code() -> str: + prepare = inspect.getsource(peft.utils.other.prepare_model_for_kbit_training) + return prepare + + +def check_peft_prep_code_is_patchable() -> bool: + prep_code = get_peft_prep_code() + prep_code, _ = detab_code(prep_code) + return ORIGINAL_PREPARE_CODE in prep_code + + +def patch_peft_prep_code(): + """ + monkeypatch create_accelerator_and_postprocess so it checks for additional kwargs + """ + + try: + prep_code = get_peft_prep_code() + except OSError: + return + peft.utils.other._original_create_accelerator_and_postprocess = ( # pylint: disable=protected-access + prep_code + ) + prep_code, _ = detab_code(prep_code) + if ORIGINAL_PREPARE_CODE not in prep_code: + return + + prep_code = prep_code.replace(ORIGINAL_PREPARE_CODE, PATCHED_PREPARE_CODE) + prep_code = prep_code.replace( + "def prepare_model_for_kbit_training(", + "def fixed_prepare_model_for_kbit_training(", + 1, + ) + + items_to_import = [] + for item in dir(peft.utils.other): + if item in prep_code: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + "from peft.utils.other import (" + ", ".join(x for x in items_to_import) + ")", + globals(), + ) + exec(prep_code, globals()) # pylint: disable=exec-used # nosec B102 + LOG.info("patching prepare_model_for_kbit_training to allow for overrides") + peft.utils.other.prepare_model_for_kbit_training = fixed_prepare_model_for_kbit_training # pylint: disable=protected-access # pylint: disable=undefined-variable # noqa: F821 + axolotl.utils.models.prepare_model_for_kbit_training = fixed_prepare_model_for_kbit_training # pylint: disable=protected-access # pylint: disable=undefined-variable # noqa: F821 diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 92766d44ca..6aa4dd162a 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -566,6 +566,11 @@ def apply_patches(self) -> None: patch_accelerate_fsdp_utils() + if self.cfg.adapter and self.cfg.embeddings_skip_upcast: + from axolotl.monkeypatch.peft.utils import patch_peft_prep_code + + patch_peft_prep_code() + if self.cfg.flex_attention: from axolotl.monkeypatch.attention.flex_attn import ( patch_flex_make_mask, @@ -1185,7 +1190,7 @@ def set_z3_leaf_modules(self) -> None: ], ) - def prepare_model(self, qlora_fsdp) -> None: + def prepare_model(self, qlora_fsdp: bool) -> None: skip_prepare_model_for_kbit_training = False if self.cfg.model_config_type == "qwen" and self.cfg.adapter == "lora": # Qwen doesn't play nicely with LoRA if this is enabled @@ -1315,7 +1320,10 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: # make sure these are fp32 per Ramesh et al. (2021) embedding_modules = get_linear_embedding_layers(self.cfg.model_config_type) if not self.cfg.fsdp: - # FSDP doesn't like mixed Float and BFloat16 + # we don't run this during FSDP because this will leave mixed + # float and bfloat16 dtypes in the model which FSDP doesn't like + if self.cfg.load_in_4bit and self.cfg.embeddings_skip_upcast: + embedding_modules = [] self.convert_embedding_modules_dtype( embedding_modules, dist_dtype=torch.float32, diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 3527ec56ea..9db3744097 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -82,6 +82,7 @@ class AxolotlInputConfig( mean_resizing_embeddings: bool | None = False # optionally shrink the embeddings when the tokenizer vocab size is smaller shrink_embeddings: bool | None = None + embeddings_skip_upcast: bool | None = None rl: RLType | None = None trl: TRLConfig | None = Field( diff --git a/tests/e2e/patched/test_peft_embeddings.py b/tests/e2e/patched/test_peft_embeddings.py new file mode 100644 index 0000000000..d4f59a128f --- /dev/null +++ b/tests/e2e/patched/test_peft_embeddings.py @@ -0,0 +1,63 @@ +""" +Test case for handling embeddings when using peft +""" + +import torch + +from axolotl.train import setup_model_and_tokenizer +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + + +class TestLlamaPeftEmbeddings: + """ + test class for handling embeddings when using peft + """ + + def test_peft_embeddings_upcast(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "load_in_4bit": True, + "adapter": "qlora", + "lora_r": 8, + "lora_alpha": 16, + "lora_target_linear": True, + "trust_remote_code": True, + "sequence_len": 512, + "val_set_size": 0.01, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": False, + "bf16": "auto", + "save_safetensors": True, + "embeddings_skip_upcast": True, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + + model, _, _, _ = setup_model_and_tokenizer(cfg) + + # Check if the embeddings are upcast correctly + # only embed_tokens is a parameter that may be upcast + assert model.base_model.model.model.embed_tokens.weight.dtype == torch.bfloat16 + assert model.base_model.model.lm_head.weight.dtype == torch.bfloat16 From 9daa04da907558d2a232d0d9b9d01c47d9a768f3 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 7 May 2025 21:29:05 +0700 Subject: [PATCH 0640/1405] Fix: improve error message on failed dataset load (#2637) [skip ci] * fix(log): clarify error on dataset loading failed * fix: add path for easy tracking of broken config * fix: improve error message based on pr feedback --- src/axolotl/utils/data/shared.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index e657262b9a..d2e119f77b 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -281,6 +281,10 @@ def load_dataset_w_config( **load_ds_kwargs, ) if not ds: - raise ValueError("unhandled dataset load") + raise ValueError( + "The dataset could not be loaded. This could be due to a misconfigured dataset path " + f"({config_dataset.path}). Try double-check your path / name / data_files. " + "This is not caused by the dataset type." + ) return ds From 32f51bca359fe4609c4ae7fe541a0da163b7774b Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 7 May 2025 21:29:47 +0700 Subject: [PATCH 0641/1405] fix(doc): clarify instruction to delinearize llama4 similar to cli doc (#2644) [skip ci] --- examples/llama-4/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/llama-4/README.md b/examples/llama-4/README.md index f5fc908a64..653da155cd 100644 --- a/examples/llama-4/README.md +++ b/examples/llama-4/README.md @@ -34,3 +34,5 @@ We provide a script to delinearize Llama 4 linearized models into regular Huggin ```bash axolotl delinearize-llama4 --model path/to/model_dir --output path/to/output_dir ``` + +Note: This only works with the non-quantized linearized model. If you have an adapter, merge it with the *non-quantized linearized* model before delinearizing. From 25e6c5f9bd33738cbab07a2f333304c10815c78f Mon Sep 17 00:00:00 2001 From: xzuyn <16216325+xzuyn@users.noreply.github.com> Date: Wed, 7 May 2025 10:31:46 -0400 Subject: [PATCH 0642/1405] Add CAME Optimizer (#2385) --- docs/config.qmd | 1 + setup.py | 1 + src/axolotl/core/trainer_builder.py | 14 ++++++++ src/axolotl/utils/schemas/enums.py | 1 + src/axolotl/utils/schemas/training.py | 2 ++ tests/e2e/test_optimizers.py | 47 +++++++++++++++++++++++++++ 6 files changed, 66 insertions(+) diff --git a/docs/config.qmd b/docs/config.qmd index 857d0eb030..1cff9e6f42 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -612,6 +612,7 @@ lr_div_factor: # Learning rate div factor # - optimi_adamw # - ao_adamw_8bit # - ao_adamw_fp8 +# - came_pytorch optimizer: # Dictionary of arguments to pass to the optimizer optim_args: diff --git a/setup.py b/setup.py index 51a97e4d9c..97e7f5ff5c 100644 --- a/setup.py +++ b/setup.py @@ -142,6 +142,7 @@ def get_package_version(): "apollo-torch", "lomo-optim==0.1.1", "torch-optimi==0.2.1", + "came_pytorch==0.1.3", ], "ray": [ "ray[train]", diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index af9a43db32..5cb397b28a 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -708,6 +708,20 @@ def build(self, total_num_steps): optimizer_cls = ADOPT adam_kwargs["decouple"] = True optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "came_pytorch": + from came_pytorch import CAME + + optimizer_cls = CAME + + beta1 = training_arguments_kwargs.get("adam_beta1", 0.9) + beta2 = training_arguments_kwargs.get("adam_beta2", 0.999) + beta3 = training_arguments_kwargs.get("adam_beta2", 0.9999) + eps1 = training_arguments_kwargs.get("adam_epsilon", 1e-30) + eps2 = training_arguments_kwargs.get("adam_epsilon2", 1e-16) + adam_kwargs["betas"] = (beta1, beta2, beta3) + adam_kwargs["eps"] = (eps1, eps2) + + optimizer_kwargs.update(adam_kwargs) # Parse any additional optimizer args from config if self.cfg.optim_args: diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 118176d344..fe5cf62bab 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -53,4 +53,5 @@ class CustomSupportedOptimizers(str, Enum): ao_adamw_8bit = "ao_adamw_8bit" # pylint: disable=invalid-name ao_adamw_fp8 = "ao_adamw_fp8" # pylint: disable=invalid-name adopt_adamw = "adopt_adamw" # pylint: disable=invalid-name + came_pytorch = "came_pytorch" # pylint: disable=invalid-name muon = "muon" # pylint: disable=invalid-name diff --git a/src/axolotl/utils/schemas/training.py b/src/axolotl/utils/schemas/training.py index 2ab4b4286d..69547c17f3 100644 --- a/src/axolotl/utils/schemas/training.py +++ b/src/axolotl/utils/schemas/training.py @@ -75,8 +75,10 @@ class HyperparametersConfig(BaseModel): lr_groups: list[LrGroup] | None = None adam_epsilon: float | None = None + adam_epsilon2: float | None = None adam_beta1: float | None = None adam_beta2: float | None = None + adam_beta3: float | None = None max_grad_norm: float | None = None num_epochs: float = Field(default=1.0) diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index d3ff27ca5a..91f45b762f 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -199,3 +199,50 @@ def test_fft_schedule_free_adamw(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + + @with_temp_dir + def test_came_pytorch(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "JackFram/llama-68m", + "tokenizer_type": "LlamaTokenizer", + "sequence_len": 1024, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.1, + "special_tokens": { + "unk_token": "", + "bos_token": "", + "eos_token": "", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "came_pytorch", + "adam_beta3": 0.9999, + "adam_epsilon2": 1e-16, + "max_steps": 5, + "lr_scheduler": "cosine", + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) From 0f3587174d28678ae1a2d481f30743fa8581bca4 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 7 May 2025 15:06:07 -0400 Subject: [PATCH 0643/1405] swap tinymodels that have safetensors for some ci tests (#2641) --- .github/workflows/tests-nightly.yml | 87 +++++++++++++++++++ requirements.txt | 1 + src/axolotl/train.py | 5 +- .../utils/gradient_checkpointing/__init__.py | 21 +++++ tests/e2e/multigpu/test_llama.py | 2 +- .../lora_kernels/test_lora_kernel_patching.py | 10 ++- tests/e2e/patched/test_falcon_samplepack.py | 4 + tests/e2e/patched/test_mistral_samplepack.py | 4 +- tests/e2e/patched/test_model_patches.py | 2 +- tests/e2e/patched/test_resume.py | 4 +- tests/e2e/test_evaluate.py | 7 +- tests/e2e/test_falcon.py | 5 ++ tests/e2e/test_mistral.py | 4 +- tests/test_datasets.py | 1 - 14 files changed, 137 insertions(+), 20 deletions(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 23eb25f569..539f7f71b6 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -18,9 +18,96 @@ jobs: env: SKIP: no-commit-to-branch + preload-cache: + name: Preload HF cache + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python_version: ["3.11"] + pytorch_version: ["2.6.0"] + timeout-minutes: 20 + + env: + AXOLOTL_IS_CI_CACHE_PRELOAD: "1" + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Restore HF cache + id: hf-cache-restore + uses: actions/cache/restore@v4 + with: + path: | + /home/runner/.cache/huggingface/hub/datasets--* + /home/runner/.cache/huggingface/hub/models--* + key: ${{ runner.os }}-hf-hub-cache-v2 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python_version }} + cache: 'pip' # caching pip dependencies + + - name: upgrade pip + run: | + pip3 install --upgrade pip + pip3 install --upgrade packaging==23.2 setuptools==75.8.0 wheel + + - name: Install PyTorch + run: | + pip3 install torch==${{ matrix.pytorch_version }} + + - name: Install dependencies + run: | + pip3 show torch + pip3 install --no-build-isolation -U -e . + python scripts/unsloth_install.py | sh + python scripts/cutcrossentropy_install.py | sh + pip3 install -r requirements-dev.txt -r requirements-tests.txt + + - name: Make sure PyTorch version wasn't clobbered + run: | + python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" + + - name: Ensure axolotl CLI was installed + run: | + axolotl --help + + - name: Pre-Download dataset fixture + run: | + huggingface-cli download --repo-type=dataset axolotl-ai-internal/axolotl-oss-dataset-fixtures + + - name: Run tests + run: | + pytest -v tests/conftest.py + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + flags: unittests,pytorch-${{ matrix.pytorch_version }} + fail_ci_if_error: false + + - name: cleanup pip cache + run: | + find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + + - name: Save HF cache + id: hf-cache + uses: actions/cache/save@v4 + with: + path: | + /home/runner/.cache/huggingface/hub/datasets--* + /home/runner/.cache/huggingface/hub/models--* + key: ${{ steps.hf-cache-restore.outputs.cache-primary-key }} + pytest: name: PyTest runs-on: ubuntu-latest + needs: [preload-cache] strategy: fail-fast: false max-parallel: 2 diff --git a/requirements.txt b/requirements.txt index dc495bedd4..4ae82dd496 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ liger-kernel==0.5.9 packaging==23.2 +huggingface_hub==0.31.0 peft==0.15.2 transformers==4.51.3 tokenizers>=0.21.1 diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 67a80b410b..2adf28fdf5 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -2,6 +2,7 @@ import importlib import inspect +import logging import os import signal import sys @@ -12,7 +13,6 @@ import torch import transformers.modelcard -from accelerate.logging import get_logger from accelerate.utils import save_fsdp_model from datasets import Dataset from huggingface_hub.errors import OfflineModeIsEnabled @@ -42,7 +42,7 @@ except ImportError: BetterTransformer = None -LOG = get_logger(__name__) +LOG = logging.getLogger(__name__) def setup_model_and_tokenizer( @@ -63,7 +63,6 @@ def setup_model_and_tokenizer( # Load tokenizer LOG.debug( f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}", - main_process_only=True, ) tokenizer = load_tokenizer(cfg) diff --git a/src/axolotl/utils/gradient_checkpointing/__init__.py b/src/axolotl/utils/gradient_checkpointing/__init__.py index 0da5c83a22..f84f76d80b 100644 --- a/src/axolotl/utils/gradient_checkpointing/__init__.py +++ b/src/axolotl/utils/gradient_checkpointing/__init__.py @@ -1,15 +1,36 @@ """custom checkpointing utils""" +import importlib from functools import partial +from packaging import version + from axolotl.utils.gradient_checkpointing.unsloth import ( Unsloth_Offloaded_Gradient_Checkpointer, ) +transformers_version = version.parse(importlib.metadata.version("transformers")) +if transformers_version > version.parse("4.51.3"): + from transformers.modeling_layers import GradientCheckpointingLayer + + def uses_gc_layers(decoder_layer): + return isinstance(decoder_layer.func.__self__, GradientCheckpointingLayer) + +else: + + def uses_gc_layers(_): + return False + def hf_grad_checkpoint_offload_wrapper( decoder_layer, *args, use_reentrant=None ): # pylint: disable=unused-argument + if uses_gc_layers(decoder_layer): + return Unsloth_Offloaded_Gradient_Checkpointer.apply( + decoder_layer, + *args, + ) + return Unsloth_Offloaded_Gradient_Checkpointer.apply( ( decoder_layer.func.__self__ diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 1ff795bd65..38e6e741a1 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -479,7 +479,7 @@ def test_fsdp2_packed( "sample_packing": True, "pad_to_sequence_len": True, "sequence_len": 2048, - "val_set_size": 0.05, + "val_set_size": 0.1, "special_tokens": { "pad_token": "<|endoftext|>", }, diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index f3e59b3734..f6b7ee9b98 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -29,12 +29,12 @@ MODEL_CONFIGS = [ { - "name": "openaccess-ai-collective/tiny-mistral", + "name": "trl-internal-testing/tiny-MistralForCausalLM-0.2", "expected_activation": apply_lora_mlp_swiglu, "dtype": torch.float16, }, { - "name": "Qwen/Qwen2-7B", + "name": "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "expected_activation": apply_lora_mlp_swiglu, "dtype": torch.float16, }, @@ -44,7 +44,7 @@ "dtype": torch.float32, }, { - "name": "mhenrichsen/gemma-2b", + "name": "trl-internal-testing/tiny-Gemma2ForCausalLM", "expected_activation": apply_lora_mlp_geglu, "dtype": torch.float16, }, @@ -156,7 +156,9 @@ def test_swiglu_mlp_integration(small_llama_model): def test_geglu_model_integration(): """Test GeGLU activation with Gemma model.""" model = AutoModelForCausalLM.from_pretrained( - "mhenrichsen/gemma-2b", torch_dtype=torch.float16, device_map="cuda:0" + "trl-internal-testing/tiny-Gemma2ForCausalLM", + torch_dtype=torch.float16, + device_map="cuda:0", ) peft_config = get_peft_config( { diff --git a/tests/e2e/patched/test_falcon_samplepack.py b/tests/e2e/patched/test_falcon_samplepack.py index 0034169af1..667b62ffba 100644 --- a/tests/e2e/patched/test_falcon_samplepack.py +++ b/tests/e2e/patched/test_falcon_samplepack.py @@ -6,6 +6,8 @@ import os import unittest +import pytest + from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train @@ -23,6 +25,7 @@ class TestFalconPatched(unittest.TestCase): Test case for Falcon models """ + @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_qlora(self, temp_dir): # pylint: disable=duplicate-code @@ -71,6 +74,7 @@ def test_qlora(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_ft(self, temp_dir): # pylint: disable=duplicate-code diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index 3bc0fcfbcf..ccfeb3d634 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -28,7 +28,7 @@ def test_lora_packing(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "openaccess-ai-collective/tiny-mistral", + "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", "flash_attention": True, "sample_packing": True, "sequence_len": 1024, @@ -76,7 +76,7 @@ def test_ft_packing(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "openaccess-ai-collective/tiny-mistral", + "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", "flash_attention": True, "sample_packing": True, "sequence_len": 1024, diff --git a/tests/e2e/patched/test_model_patches.py b/tests/e2e/patched/test_model_patches.py index 8a75db52e4..26090e697c 100644 --- a/tests/e2e/patched/test_model_patches.py +++ b/tests/e2e/patched/test_model_patches.py @@ -56,7 +56,7 @@ def test_mixtral_multipack(self, temp_dir): def test_mistral_multipack(self, temp_dir): cfg = DictDefault( { - "base_model": "openaccess-ai-collective/tiny-mistral", + "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", "flash_attention": True, "sample_packing": True, "sequence_len": 2048, diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index a84759baea..61e4a0e03d 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -15,7 +15,7 @@ from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from ..utils import check_model_output_exists, most_recent_subdir +from ..utils import check_model_output_exists, most_recent_subdir, require_torch_2_6_0 LOG = logging.getLogger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" @@ -26,6 +26,7 @@ class TestResumeLlama: Test case for resuming training of llama models """ + @require_torch_2_6_0 def test_resume_lora_packed(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( @@ -62,6 +63,7 @@ def test_resume_lora_packed(self, temp_dir): "save_total_limit": 5, "max_steps": 15, "use_tensorboard": True, + "save_safetensors": True, } ) if is_torch_bf16_gpu_available(): diff --git a/tests/e2e/test_evaluate.py b/tests/e2e/test_evaluate.py index b2d7d02cad..0278113b7a 100644 --- a/tests/e2e/test_evaluate.py +++ b/tests/e2e/test_evaluate.py @@ -19,14 +19,11 @@ def test_evaluate(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "HuggingFaceTB/SmolLM2-135M", "sequence_len": 1024, "val_set_size": 0.02, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { diff --git a/tests/e2e/test_falcon.py b/tests/e2e/test_falcon.py index a1641a9971..24afab0b3f 100644 --- a/tests/e2e/test_falcon.py +++ b/tests/e2e/test_falcon.py @@ -6,6 +6,8 @@ import os import unittest +import pytest + from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train @@ -23,6 +25,7 @@ class TestFalcon(unittest.TestCase): Test case for falcon """ + @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_lora(self, temp_dir): # pylint: disable=duplicate-code @@ -74,6 +77,7 @@ def test_lora(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_lora_added_vocab(self, temp_dir): # pylint: disable=duplicate-code @@ -129,6 +133,7 @@ def test_lora_added_vocab(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_ft(self, temp_dir): # pylint: disable=duplicate-code diff --git a/tests/e2e/test_mistral.py b/tests/e2e/test_mistral.py index 740fa6eed9..ba8cf28962 100644 --- a/tests/e2e/test_mistral.py +++ b/tests/e2e/test_mistral.py @@ -30,7 +30,7 @@ def test_lora(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "openaccess-ai-collective/tiny-mistral", + "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", "flash_attention": True, "sequence_len": 1024, "load_in_8bit": True, @@ -77,7 +77,7 @@ def test_ft(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { - "base_model": "openaccess-ai-collective/tiny-mistral", + "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", "flash_attention": True, "sequence_len": 1024, "val_set_size": 0.02, diff --git a/tests/test_datasets.py b/tests/test_datasets.py index ded82869f5..88d196ad16 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -414,7 +414,6 @@ def test_loading_local_dataset_folder(self, tokenizer): snapshot_path = snapshot_download( repo_id="mhenrichsen/alpaca_2k_test", repo_type="dataset", - local_dir=tmp_ds_path, ) shutil.copytree(snapshot_path, tmp_ds_path, dirs_exist_ok=True) From 47e0e71bc8f3b8bc9800e05c6516710c3aac05c2 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 9 May 2025 20:28:58 -0400 Subject: [PATCH 0644/1405] don't sort multipack sampler (#2657) * don't sort multipack sampler * increased packing efficiency increases loss --------- Co-authored-by: Wing Lian --- src/axolotl/utils/samplers/multipack.py | 8 ++------ tests/e2e/integrations/test_kd.py | 4 ++-- tests/test_packed_batch_sampler.py | 1 + 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index a0c30b0d4c..c38313c7c1 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -78,15 +78,11 @@ def pack_group( Returns: List of bins, where each bin contains indices of sequences assigned to it """ - # Get sorting indices and sort lengths in descending order - indices = np.argsort(sequence_lengths)[::-1] - sorted_lengths = sequence_lengths[indices] - bins_remaining_space: list = [] # Tracks remaining capacity in each bin bins_assigned_sequences: list = [] # Tracks sequence indices assigned to each bin - for seq_id, size in enumerate(sorted_lengths): - global_idx = indices[seq_id] + group_offset + for seq_id, size in enumerate(sequence_lengths): + global_idx = seq_id + group_offset # Try to place sequence in existing bins add_new_bin = True diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py index 9bfe5aaefd..f36eef9538 100644 --- a/tests/e2e/integrations/test_kd.py +++ b/tests/e2e/integrations/test_kd.py @@ -90,7 +90,7 @@ def test_llama_kd(self, temp_dir, kd_min_cfg): train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "model.safetensors").exists() check_tensorboard( - temp_dir + "/runs", "train/loss", 1.0, "Train Loss is too high" + temp_dir + "/runs", "train/loss", 1.2, "Train Loss (%s) is too high" ) @pytest.mark.parametrize( @@ -121,5 +121,5 @@ def test_llama_lora_kd(self, temp_dir, kd_min_cfg, load_in_8bit): train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.safetensors").exists() check_tensorboard( - temp_dir + "/runs", "train/loss", 1.0, "Train Loss is too high" + temp_dir + "/runs", "train/loss", 1.2, "Train Loss (%s) is too high" ) diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index dd0386e58c..2b03c62f82 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -106,3 +106,4 @@ def test_packing( original_idxs = set(range(len(train_dataset))) assert original_idxs == set(batch_idxs) + assert len(batch_idxs) == len(set(batch_idxs)) From c7b67906149d22f707b60ca8162007a1e4d48fa5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 12 May 2025 10:51:18 -0400 Subject: [PATCH 0645/1405] Various fixes for CI, save_only_model for RL, prevent packing multiprocessing deadlocks (#2661) * lean mistral ft tests, remove e2e torch 2.4.1 test * make sure to pass save_only_model for RL * more tests to make ci leaner, add cleanup to modal ci * fix module for import in e2e tests * use mp spawn to prevent deadlocks with packing * make sure cleanup shell script is executable when cloned out --- .github/workflows/tests.yml | 46 ++++++++++++-- cicd/__init__.py | 0 cicd/cicd.sh | 2 +- cicd/cleanup.py | 19 ++++++ cicd/cleanup.sh | 6 ++ cicd/e2e_tests.py | 65 +------------------ cicd/single_gpu.py | 66 ++++++++++++++++++++ src/axolotl/core/trainer_builder.py | 2 + src/axolotl/utils/samplers/multipack.py | 35 +++++++++-- tests/e2e/patched/test_4d_multipack_llama.py | 12 ++-- tests/e2e/patched/test_mistral_samplepack.py | 12 ++-- tests/e2e/patched/test_mixtral_samplepack.py | 12 ++-- tests/e2e/patched/test_phi_multipack.py | 12 ++-- 13 files changed, 190 insertions(+), 99 deletions(-) create mode 100644 cicd/__init__.py create mode 100644 cicd/cleanup.py create mode 100755 cicd/cleanup.sh create mode 100644 cicd/single_gpu.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2671cfd339..56d41a0d29 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -335,12 +335,6 @@ jobs: pytorch: 2.6.0 num_gpus: 1 axolotl_extras: llmcompressor - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.4.1 - num_gpus: 1 - axolotl_extras: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -377,3 +371,43 @@ jobs: - name: Run tests job on Modal run: | modal run cicd.e2e_tests + + docker-e2e-cleanup: + runs-on: [self-hosted, modal] + timeout-minutes: 90 + needs: [docker-e2e-tests] + + strategy: + fail-fast: false + matrix: + include: + - cuda: 124 + cuda_version: 12.4.1 + python_version: "3.11" + pytorch: 2.6.0 + num_gpus: 1 + axolotl_extras: vllm + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install Modal + run: | + python -m pip install --upgrade pip + pip install modal==0.71.8 jinja2 + - name: Update env vars + run: | + echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV + echo "PYTORCH_VERSION=${{ matrix.pytorch}}" >> $GITHUB_ENV + echo "AXOLOTL_ARGS=${{ matrix.axolotl_args}}" >> $GITHUB_ENV + echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV + echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV + echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV + echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV + echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV + - name: Run tests job on Modal + run: | + modal run cicd.cleanup diff --git a/cicd/__init__.py b/cicd/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 86cc4fa96f..65ee8699de 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -18,7 +18,7 @@ pytest -v --durations=10 \ --cov-append # Run patched tests excluding lora kernels with coverage append -pytest -v --durations=10 \ +pytest --full-trace -vvv --durations=10 \ --ignore=tests/e2e/patched/lora_kernels \ /workspace/axolotl/tests/e2e/patched \ --cov=axolotl \ diff --git a/cicd/cleanup.py b/cicd/cleanup.py new file mode 100644 index 0000000000..0074899938 --- /dev/null +++ b/cicd/cleanup.py @@ -0,0 +1,19 @@ +"""Modal app to run axolotl GPU cleanup""" + +from .single_gpu import VOLUME_CONFIG, app, cicd_image, run_cmd + + +@app.function( + image=cicd_image, + timeout=60 * 60, + cpu=8.0, + memory=131072, + volumes=VOLUME_CONFIG, +) +def cleanup(): + run_cmd("./cicd/cleanup.sh", "/workspace/axolotl") + + +@app.local_entrypoint() +def main(): + cleanup.remote() diff --git a/cicd/cleanup.sh b/cicd/cleanup.sh new file mode 100755 index 0000000000..4ea851bb4e --- /dev/null +++ b/cicd/cleanup.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +# cleanup old cache files for datasets processing and intermediate mappings +find /workspace/data/huggingface-cache/hub/datasets -name "cache-*" -type f -mtime +1 -exec rm {} \; +find /workspace/data/huggingface-cache/hub/datasets -name "*.lock" -type f -mtime +1 -exec rm {} \; diff --git a/cicd/e2e_tests.py b/cicd/e2e_tests.py index 998f8c35da..2bc8ca072a 100644 --- a/cicd/e2e_tests.py +++ b/cicd/e2e_tests.py @@ -1,69 +1,6 @@ """Modal app to run axolotl GPU tests""" -# pylint: disable=duplicate-code - -import os -import pathlib -import tempfile - -import jinja2 -import modal -from jinja2 import select_autoescape -from modal import App, Image - -cicd_path = pathlib.Path(__file__).parent.resolve() - -template_loader = jinja2.FileSystemLoader(searchpath=cicd_path) -template_env = jinja2.Environment( - loader=template_loader, autoescape=select_autoescape() -) -df_template = template_env.get_template("Dockerfile.jinja") - -df_args = { - "AXOLOTL_EXTRAS": os.environ.get("AXOLOTL_EXTRAS", ""), - "AXOLOTL_ARGS": os.environ.get("AXOLOTL_ARGS", ""), - "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.4.1"), - "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu121-2.4.1"), - "CUDA": os.environ.get("CUDA", "121"), - "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), - "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), - "NIGHTLY_BUILD": os.environ.get("NIGHTLY_BUILD", ""), - "CODECOV_TOKEN": os.environ.get("CODECOV_TOKEN", ""), - "HF_HOME": "/workspace/data/huggingface-cache/hub", -} - -dockerfile_contents = df_template.render(**df_args) - -temp_dir = tempfile.mkdtemp() -with open(pathlib.Path(temp_dir) / "Dockerfile", "w", encoding="utf-8") as f: - f.write(dockerfile_contents) - -cicd_image = Image.from_dockerfile( - pathlib.Path(temp_dir) / "Dockerfile", - context_mount=None, - force_build=True, - gpu="A10G", -).env(df_args) - -app = App("Axolotl CI/CD", secrets=[]) - -hf_cache_volume = modal.Volume.from_name( - "axolotl-ci-hf-hub-cache", create_if_missing=True -) -VOLUME_CONFIG = { - "/workspace/data/huggingface-cache/hub": hf_cache_volume, -} - -N_GPUS = int(os.environ.get("N_GPUS", 1)) -GPU_CONFIG = modal.gpu.L40S(count=N_GPUS) - - -def run_cmd(cmd: str, run_folder: str): - import subprocess # nosec - - # Propagate errors from subprocess. - if exit_code := subprocess.call(cmd.split(), cwd=run_folder): # nosec - exit(exit_code) # pylint: disable=consider-using-sys-exit +from .single_gpu import GPU_CONFIG, VOLUME_CONFIG, app, cicd_image, run_cmd @app.function( diff --git a/cicd/single_gpu.py b/cicd/single_gpu.py new file mode 100644 index 0000000000..d46d970cfe --- /dev/null +++ b/cicd/single_gpu.py @@ -0,0 +1,66 @@ +"""Modal app to run axolotl GPU tests""" + +# pylint: disable=duplicate-code + +import os +import pathlib +import tempfile + +import jinja2 +import modal +from jinja2 import select_autoescape +from modal import App, Image + +cicd_path = pathlib.Path(__file__).parent.resolve() + +template_loader = jinja2.FileSystemLoader(searchpath=cicd_path) +template_env = jinja2.Environment( + loader=template_loader, autoescape=select_autoescape() +) +df_template = template_env.get_template("Dockerfile.jinja") + +df_args = { + "AXOLOTL_EXTRAS": os.environ.get("AXOLOTL_EXTRAS", ""), + "AXOLOTL_ARGS": os.environ.get("AXOLOTL_ARGS", ""), + "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.4.1"), + "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu121-2.4.1"), + "CUDA": os.environ.get("CUDA", "121"), + "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), + "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), + "NIGHTLY_BUILD": os.environ.get("NIGHTLY_BUILD", ""), + "CODECOV_TOKEN": os.environ.get("CODECOV_TOKEN", ""), + "HF_HOME": "/workspace/data/huggingface-cache/hub", +} + +dockerfile_contents = df_template.render(**df_args) + +temp_dir = tempfile.mkdtemp() +with open(pathlib.Path(temp_dir) / "Dockerfile", "w", encoding="utf-8") as f: + f.write(dockerfile_contents) + +cicd_image = Image.from_dockerfile( + pathlib.Path(temp_dir) / "Dockerfile", + context_mount=None, + force_build=True, + gpu="A10G", +).env(df_args) + +app = App("Axolotl CI/CD", secrets=[]) + +hf_cache_volume = modal.Volume.from_name( + "axolotl-ci-hf-hub-cache", create_if_missing=True +) +VOLUME_CONFIG = { + "/workspace/data/huggingface-cache/hub": hf_cache_volume, +} + +N_GPUS = int(os.environ.get("N_GPUS", 1)) +GPU_CONFIG = modal.gpu.L40S(count=N_GPUS) + + +def run_cmd(cmd: str, run_folder: str): + import subprocess # nosec + + # Propagate errors from subprocess. + if exit_code := subprocess.call(cmd.split(), cwd=run_folder): # nosec + exit(exit_code) # pylint: disable=consider-using-sys-exit diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 5cb397b28a..670561edec 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1057,6 +1057,8 @@ def build_training_arguments(self, total_num_steps): # default to saving each epoch if not defined training_args_kwargs["save_strategy"] = "epoch" + training_args_kwargs["save_only_model"] = self.cfg.save_only_model + if self.cfg.dataset_processes: training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index c38313c7c1..2df2d9e191 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -6,7 +6,7 @@ import logging import math from concurrent.futures import ProcessPoolExecutor -from multiprocessing import cpu_count +from multiprocessing import cpu_count, get_context from typing import Iterable, Union import numba @@ -126,6 +126,7 @@ def pack_parallel( bin_size: int, num_processes: int | None = None, safe_mode: bool = True, + mp_start_method: str | None = "spawn", ): """ Pack sequences into bins using parallel processing @@ -137,7 +138,9 @@ def pack_parallel( bin_size: Maximum number of bins to use num_processes: Number of parallel processes to use safe_mode: If True, use a more conservative packing approach - + mp_start_method: Multiprocessing start method ('fork', 'spawn', 'forkserver'). + 'spawn' is often safer with Numba/PyTorch. + Set to None to use system default. Returns: List of bins, where each bin contains indices of sequences assigned to it """ @@ -154,9 +157,33 @@ def pack_parallel( # Process groups in parallel all_bins = [] - with ProcessPoolExecutor(max_workers=num_processes) as executor: - for group_bins in executor.map(_process_group, tasks): + + mp_ctx = None + if mp_start_method: + try: + mp_ctx = get_context(mp_start_method) + except ValueError: + LOG.warning( + f"Failed to get multiprocessing context '{mp_start_method}'. " + f"Falling back to default. Available: {get_context().get_all_start_methods()}" + ) + mp_ctx = ( + None # Fallback to default context if specified one is not available + ) + + if num_processes == 1: + LOG.debug("Using single process for pack_parallel, running sequentially.") + for task_args in tasks: + group_bins = _process_group(task_args) all_bins.extend(group_bins) + else: + # Use ProcessPoolExecutor only if num_processes > 1 + # Pass mp_context if available + with ProcessPoolExecutor( + max_workers=num_processes, mp_context=mp_ctx + ) as executor: + for group_bins in executor.map(_process_group, tasks): + all_bins.extend(group_bins) return all_bins diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index 2709568839..12dd51c134 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -57,9 +57,9 @@ def test_sdp_lora_packing(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 5, + "save_steps": 3, + "eval_steps": 4, "fp16": True, } ) @@ -105,9 +105,9 @@ def test_torch_lora_packing(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 5, + "save_steps": 3, + "eval_steps": 4, "fp16": True, } ) diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index ccfeb3d634..fe8fafb19d 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -57,9 +57,9 @@ def test_lora_packing(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 5, + "save_steps": 3, + "eval_steps": 4, "bf16": "auto", } ) @@ -99,9 +99,9 @@ def test_ft_packing(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 5, + "save_steps": 3, + "eval_steps": 4, "bf16": "auto", } ) diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index f035b1f288..ebc2ba0927 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -54,9 +54,9 @@ def test_qlora(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 5, + "save_steps": 3, + "eval_steps": 4, "bf16": "auto", } ) @@ -93,9 +93,9 @@ def test_ft(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 5, + "save_steps": 3, + "eval_steps": 4, "bf16": "auto", } ) diff --git a/tests/e2e/patched/test_phi_multipack.py b/tests/e2e/patched/test_phi_multipack.py index c42ed8baf8..d8130d1190 100644 --- a/tests/e2e/patched/test_phi_multipack.py +++ b/tests/e2e/patched/test_phi_multipack.py @@ -56,9 +56,9 @@ def test_ft_packed(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 20, - "eval_steps": 10, - "save_steps": 10, + "max_steps": 5, + "eval_steps": 3, + "save_steps": 4, "bf16": "auto", } ) @@ -108,9 +108,9 @@ def test_qlora_packed(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 20, - "eval_steps": 10, - "save_steps": 10, + "max_steps": 5, + "eval_steps": 3, + "save_steps": 4, "bf16": "auto", } ) From f34eef546a9171d5de1d4779c3ca75e275f45b1e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 12 May 2025 14:17:25 -0400 Subject: [PATCH 0646/1405] update doc and use P2P=LOC for brittle grpo test (#2649) * update doc and skip brittle grpo test * fix the path to run the multigpu tests * increase timeout, use LOC instead of NVL * typo * use hf cache from s3 backed cloudfront * mark grpo as flaky test dues to vllm start --- .github/workflows/multi-gpu-e2e.yml | 2 +- .github/workflows/tests.yml | 224 +++++++++++++++------------ cicd/e2e_tests.py | 2 +- codecov.yml | 2 +- docs/config.qmd | 1 + tests/e2e/multigpu/solo/test_grpo.py | 10 +- 6 files changed, 131 insertions(+), 110 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index ffb3577ea6..8c7692d138 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -3,7 +3,7 @@ name: docker-multigpu-tests-biweekly on: pull_request: paths: - - 'tests/e2e/multigpu/*.py' + - 'tests/e2e/multigpu/**.py' - 'requirements.txt' - 'setup.py' - 'pyproject.toml' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 56d41a0d29..c296e23145 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,96 +44,102 @@ jobs: env: SKIP: no-commit-to-branch - preload-cache: - name: Preload HF cache - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python_version: ["3.11"] - pytorch_version: ["2.6.0"] - timeout-minutes: 20 - - env: - AXOLOTL_IS_CI_CACHE_PRELOAD: "1" - - steps: - - name: Check out repository code - uses: actions/checkout@v4 - - - name: Restore HF cache - id: hf-cache-restore - uses: actions/cache/restore@v4 - with: - path: | - /home/runner/.cache/huggingface/hub/datasets--* - /home/runner/.cache/huggingface/hub/models--* - key: ${{ runner.os }}-hf-hub-cache-v2 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python_version }} - cache: 'pip' # caching pip dependencies - - - name: upgrade pip - run: | - pip3 install --upgrade pip - pip3 install --upgrade packaging==23.2 setuptools==75.8.0 wheel - - - name: Install PyTorch - run: | - pip3 install torch==${{ matrix.pytorch_version }} - - - name: Install dependencies - run: | - pip3 show torch - pip3 install --no-build-isolation -U -e . - python scripts/unsloth_install.py | sh - python scripts/cutcrossentropy_install.py | sh - pip3 install -r requirements-dev.txt -r requirements-tests.txt - - - name: Make sure PyTorch version wasn't clobbered - run: | - python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" - - - name: Ensure axolotl CLI was installed - run: | - axolotl --help - - - name: Pre-Download dataset fixture - run: | - huggingface-cli download --repo-type=dataset axolotl-ai-internal/axolotl-oss-dataset-fixtures - - - name: Run tests - run: | - pytest -v tests/conftest.py - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.xml - flags: unittests,pytorch-${{ matrix.pytorch_version }} - fail_ci_if_error: false - - - name: cleanup pip cache - run: | - find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; - - - name: Save HF cache - id: hf-cache - uses: actions/cache/save@v4 - with: - path: | - /home/runner/.cache/huggingface/hub/datasets--* - /home/runner/.cache/huggingface/hub/models--* - key: ${{ steps.hf-cache-restore.outputs.cache-primary-key }} +# preload-cache: +# name: Preload HF cache +# runs-on: ubuntu-latest +# strategy: +# fail-fast: false +# matrix: +# python_version: ["3.11"] +# pytorch_version: ["2.6.0"] +# timeout-minutes: 20 +# +# env: +# AXOLOTL_IS_CI_CACHE_PRELOAD: "1" +# +# steps: +# - name: Check out repository code +# uses: actions/checkout@v4 +# +# - name: Restore HF cache +# id: hf-cache-restore +# uses: actions/cache/restore@v4 +# with: +# path: | +# /home/runner/.cache/huggingface/hub/datasets--* +# /home/runner/.cache/huggingface/hub/models--* +# key: ${{ runner.os }}-hf-hub-cache-v2 +# +# - name: Restore Cache from S3 +# id: hf-cache-restore-s3 +# run: | +# mkdir -p /home/runner/.cache/huggingface/hub +# curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xf - -C /home/runner/.cache/huggingface/hub/ --use-compress-program unzstd +# +# - name: Setup Python +# uses: actions/setup-python@v5 +# with: +# python-version: ${{ matrix.python_version }} +# cache: 'pip' # caching pip dependencies +# +# - name: upgrade pip +# run: | +# pip3 install --upgrade pip +# pip3 install --upgrade packaging==23.2 setuptools==75.8.0 wheel +# +# - name: Install PyTorch +# run: | +# pip3 install torch==${{ matrix.pytorch_version }} +# +# - name: Install dependencies +# run: | +# pip3 show torch +# pip3 install --no-build-isolation -U -e . +# python scripts/unsloth_install.py | sh +# python scripts/cutcrossentropy_install.py | sh +# pip3 install -r requirements-dev.txt -r requirements-tests.txt +# +# - name: Make sure PyTorch version wasn't clobbered +# run: | +# python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" +# +# - name: Ensure axolotl CLI was installed +# run: | +# axolotl --help +# +# - name: Pre-Download dataset fixture +# run: | +# huggingface-cli download --repo-type=dataset axolotl-ai-internal/axolotl-oss-dataset-fixtures +# +# - name: Run tests +# run: | +# pytest -v tests/conftest.py +# +# - name: Upload coverage to Codecov +# uses: codecov/codecov-action@v5 +# with: +# token: ${{ secrets.CODECOV_TOKEN }} +# files: ./coverage.xml +# flags: unittests,pytorch-${{ matrix.pytorch_version }} +# fail_ci_if_error: false +# +# - name: cleanup pip cache +# run: | +# find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; +# +# - name: Save HF cache +# id: hf-cache +# uses: actions/cache/save@v4 +# with: +# path: | +# /home/runner/.cache/huggingface/hub/datasets--* +# /home/runner/.cache/huggingface/hub/models--* +# key: ${{ steps.hf-cache-restore.outputs.cache-primary-key }} pytest: name: PyTest runs-on: ubuntu-latest - needs: [preload-cache] +# needs: [preload-cache] strategy: fail-fast: false matrix: @@ -145,14 +151,20 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 - - name: Restore HF cache - id: hf-cache-restore - uses: actions/cache/restore@v4 - with: - path: | - /home/runner/.cache/huggingface/hub/datasets--* - /home/runner/.cache/huggingface/hub/models--* - key: ${{ runner.os }}-hf-hub-cache-v2 +# - name: Restore HF cache +# id: hf-cache-restore +# uses: actions/cache/restore@v4 +# with: +# path: | +# /home/runner/.cache/huggingface/hub/datasets--* +# /home/runner/.cache/huggingface/hub/models--* +# key: ${{ runner.os }}-hf-hub-cache-v2 + + - name: Restore Cache from S3 + id: hf-cache-restore-s3 + run: | + mkdir -p /home/runner/.cache/huggingface/hub + curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xf - -C /home/runner/.cache/huggingface/hub/ --use-compress-program unzstd - name: Setup Python uses: actions/setup-python@v5 @@ -210,7 +222,7 @@ jobs: pytest-sdist: name: PyTest from Source Dist runs-on: ubuntu-latest - needs: [preload-cache] +# needs: [preload-cache] strategy: fail-fast: false matrix: @@ -222,14 +234,20 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 - - name: Restore HF cache - id: hf-cache-restore - uses: actions/cache/restore@v4 - with: - path: | - /home/runner/.cache/huggingface/hub/datasets--* - /home/runner/.cache/huggingface/hub/models--* - key: ${{ runner.os }}-hf-hub-cache-v2 +# - name: Restore HF cache +# id: hf-cache-restore +# uses: actions/cache/restore@v4 +# with: +# path: | +# /home/runner/.cache/huggingface/hub/datasets--* +# /home/runner/.cache/huggingface/hub/models--* +# key: ${{ runner.os }}-hf-hub-cache-v2 + + - name: Restore Cache from S3 + id: hf-cache-restore-s3 + run: | + mkdir -p /home/runner/.cache/huggingface/hub + curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xf - -C /home/runner/.cache/huggingface/hub/ --use-compress-program unzstd - name: Setup Python uses: actions/setup-python@v5 diff --git a/cicd/e2e_tests.py b/cicd/e2e_tests.py index 2bc8ca072a..ce9c605c70 100644 --- a/cicd/e2e_tests.py +++ b/cicd/e2e_tests.py @@ -6,7 +6,7 @@ @app.function( image=cicd_image, gpu=GPU_CONFIG, - timeout=60 * 60, + timeout=90 * 60, # 90 min cpu=8.0, memory=131072, volumes=VOLUME_CONFIG, diff --git a/codecov.yml b/codecov.yml index c85268b4cb..2741b17582 100644 --- a/codecov.yml +++ b/codecov.yml @@ -19,7 +19,7 @@ coverage: if_no_uploads: error if_not_found: success if_ci_failed: error - only_pulls: false + only_pulls: true flags: null paths: null patch: diff --git a/docs/config.qmd b/docs/config.qmd index 1cff9e6f42..eba9f4881b 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -505,6 +505,7 @@ save_strategy: # Set to `"no"` to skip checkpoint saves, `"epoch"` at end of eac save_steps: # Leave empty to save at each epoch, integer for every N steps. float for fraction of total steps saves_per_epoch: # number of times per epoch to save a checkpoint, mutually exclusive with save_steps save_total_limit: # Checkpoints saved at a time +save_only_model: # Save only the model weights, skipping the optimizer. Using this means you can't resume from checkpoints. # Maximum number of iterations to train for. It precedes num_epochs which means that # if both are set, num_epochs will not be guaranteed. # e.g., when 1 epoch is 1000 steps => `num_epochs: 2` and `max_steps: 100` will train for 100 steps diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index a34d4b3f80..a1eade5311 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -166,6 +166,7 @@ def transform_fn(example, tokenizer=None): """ ) + @pytest.mark.skip(reason="flaky test") @pytest.mark.parametrize( "num_gpus", [1, 2], @@ -227,7 +228,7 @@ def test_llama_dora(self, temp_dir, num_gpus): current_env = os.environ.copy() env = { - "NCCL_P2P_LEVEL": "NVL", + "NCCL_P2P_LEVEL": "LOC", **current_env, "CUDA_VISIBLE_DEVICES": "1", "VLLM_DISABLE_COMPILE_CACHE": "1", @@ -257,7 +258,7 @@ def test_llama_dora(self, temp_dir, num_gpus): f"{get_torch_dist_unique_port()}", ], env={ - "NCCL_P2P_LEVEL": "NVL", + "NCCL_P2P_LEVEL": "LOC", "NCCL_DEBUG": "INFO", **current_env, }, @@ -265,6 +266,7 @@ def test_llama_dora(self, temp_dir, num_gpus): finally: recursive_kill(vllm_process) + @pytest.mark.skip(reason="flaky test") @pytest.mark.parametrize( "num_gpus", [1, 2], @@ -320,7 +322,7 @@ def test_llama_fft(self, temp_dir, num_gpus): current_env = os.environ.copy() env = { - "NCCL_P2P_LEVEL": "NVL", # nccl can be brittle, assume P2P isn't reliable + "NCCL_P2P_LEVEL": "LOC", # nccl can be brittle, assume P2P isn't reliable **current_env, "CUDA_VISIBLE_DEVICES": "1", "VLLM_DISABLE_COMPILE_CACHE": "1", @@ -350,7 +352,7 @@ def test_llama_fft(self, temp_dir, num_gpus): f"{get_torch_dist_unique_port()}", ], env={ - "NCCL_P2P_LEVEL": "NVL", + "NCCL_P2P_LEVEL": "LOC", "NCCL_DEBUG": "INFO", **current_env, }, From 526ddb886d2f01e9482c69face7550fb052e4ad2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 12 May 2025 14:18:42 -0400 Subject: [PATCH 0647/1405] guard on deleting secrets from env (#2653) [skip ci] --- .runpod/src/handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.runpod/src/handler.py b/.runpod/src/handler.py index 21073dff4d..740c1ed1fb 100644 --- a/.runpod/src/handler.py +++ b/.runpod/src/handler.py @@ -57,8 +57,10 @@ async def handler(job): logger.info("Training Complete.") # Cleanup - del os.environ["WANDB_API_KEY"] - del os.environ["HF_TOKEN"] + if "WANDB_API_KEY" in os.environ: + del os.environ["WANDB_API_KEY"] + if "HF_TOKEN" in os.environ: + del os.environ["HF_TOKEN"] runpod.serverless.start({"handler": handler, "return_aggregate_stream": True}) From 67c4ea9c7c55873070a9ec81f8fda708df6314b1 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 13 May 2025 03:23:53 +0700 Subject: [PATCH 0648/1405] fix: disable auto lora kernel if dropout nonzero (#2655) [skip ci] * fix: disable auto lora kernel if dropout nonzero * Add comment from PR feedback --------- Co-authored-by: Wing Lian --- src/axolotl/utils/schemas/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 9db3744097..cd9891e04d 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1345,6 +1345,10 @@ def check_auto_enable_lora_kernels(cls, data): ): return data + # Skip if dropout is not 0, as auto enabling it would just disable it during runtime patch checks + if data.get("lora_dropout") != 0: + return data + # Check multi-GPU compatibility capabilities = data.get("capabilities") is_multi_gpu = capabilities and capabilities.get("n_gpu", 0) > 1 From 80304c26a70e21ed8522fdbd53bcb290f9c6b7d3 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 12 May 2025 17:52:40 -0400 Subject: [PATCH 0649/1405] SP GRPO support + batch SP fixes (#2643) * ctx manager for SP * updates * update * further simplifying * simplifying * simplifying * reorg * batch api HF adapter for ring-flash-attn; cleanup and improvements * update * adding all batch ring-flash-attn methods via single adapter * fix * fixes for batch API funcs, simplify * fix * grpo sp support * progress * stronger subclassing of TRL GRPO trainer; custom distributed sampler * subclassing constructor * progress * finalizing SP + GRPO trainer * minimize diffs to GRPO trainer * remove (most of) the custom GRPO trainer logic * debug * debug * update * update * update * progress * cleanup * cleanup * minor changes * update * update * update * small changes * updates * cleanup; torch.compile ring_flash_attn functions to prevent numerical instability; lint * spacing * cleanup; log in pydantic model config only on main process * remove comment * fix sp sampler, update to latest upstream code, doc * add docs * update quartodoc autodoc contents * fix, simplifications * fixes + simplifications * review comments * lint * removing main process only logs in favor of #2608 * fixes, additional smoke test * updatse * more tests * update * fix grad accum bug (sort of) * lint, tests * todo --- _quarto.yml | 17 +- docs/sequence_parallelism.qmd | 4 +- src/axolotl/common/datasets.py | 3 +- src/axolotl/core/trainer_builder.py | 83 ++- src/axolotl/core/trainers/__init__.py | 2 +- src/axolotl/core/trainers/base.py | 4 +- src/axolotl/core/trainers/dpo/__init__.py | 11 +- src/axolotl/core/trainers/grpo/__init__.py | 47 +- src/axolotl/core/trainers/grpo/args.py | 4 +- src/axolotl/core/trainers/grpo/sampler.py | 172 +++++ src/axolotl/core/trainers/grpo/trainer.py | 653 +++++++++++++++++- src/axolotl/core/trainers/mixins/__init__.py | 2 +- .../core/trainers/mixins/sequence_parallel.py | 228 +----- src/axolotl/core/training_args.py | 2 +- .../attention/ring_attn/__init__.py | 1 - .../attention/ring_attn/adapters/batch.py | 14 +- .../monkeypatch/attention/ring_attn/patch.py | 20 +- src/axolotl/train.py | 55 +- src/axolotl/utils/ctx_managers/__init__.py | 6 + .../utils/ctx_managers/sequence_parallel.py | 335 +++++++++ src/axolotl/utils/data/rl.py | 25 +- src/axolotl/utils/data/sft.py | 10 +- src/axolotl/utils/models.py | 3 +- src/axolotl/utils/schemas/config.py | 30 +- src/axolotl/utils/schemas/enums.py | 23 +- tests/e2e/multigpu/patched/test_sp.py | 19 +- tests/e2e/patched/test_sp.py | 142 ++-- 27 files changed, 1454 insertions(+), 461 deletions(-) create mode 100644 src/axolotl/core/trainers/grpo/sampler.py create mode 100644 src/axolotl/utils/ctx_managers/__init__.py create mode 100644 src/axolotl/utils/ctx_managers/sequence_parallel.py diff --git a/_quarto.yml b/_quarto.yml index 17e121c158..463f76b342 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -48,8 +48,23 @@ quartodoc: contents: - core.trainers.base - core.trainers.trl + - core.trainers.mamba + - core.trainers.relora - core.trainers.dpo.trainer - core.trainers.grpo.trainer + - core.trainers.grpo.sampler + - core.trainers.utils + - title: Mixins + desc: Mixin classes for augmenting trainers + contents: + - core.trainers.mixins.optimizer + - core.trainers.mixins.rng_state_loader + - core.trainers.mixins.scheduler + - core.trainers.mixins.sequence_parallel + - title: Context Managers + desc: Context managers for altering trainer behaviors + contents: + - utils.ctx_managers.sequence_parallel - title: Prompt Strategies desc: Prompt formatting strategies contents: @@ -86,7 +101,7 @@ quartodoc: - kernels.swiglu - kernels.quantize - kernels.utils - - title: MonkeyPatches + - title: Monkey Patches desc: Runtime patches for model optimizations contents: - monkeypatch.llama_attn_hijack_flash diff --git a/docs/sequence_parallelism.qmd b/docs/sequence_parallelism.qmd index 20739333ae..1bff17ce9b 100644 --- a/docs/sequence_parallelism.qmd +++ b/docs/sequence_parallelism.qmd @@ -3,8 +3,6 @@ title: Sequence Parallelism description: Train with long sequences split across multiple GPUs. --- -# Sequence Parallelism - Sequence parallelism is a technique that splits sequences across multiple GPUs, allowing you to train with very long sequences that wouldn't fit on a single GPU. Each GPU processes a different portion of the sequence, and the results are aggregated @@ -27,7 +25,7 @@ To enable sequence parallelism, add the following to your configuration file: sequence_parallel_degree: 4 # Split sequences across 4 GPUs # Optional; strides across the key dimension. Larger values use more memory but should make training faster. heads_k_stride: 1 -# Optional; one of "varlen_llama3", "batch_ring", "batch_zigzag", "batch_stripe". Defaults to +# Optional; one of "varlen_llama3" or "batch_ring". Defaults to # "varlen_llama3" when `sample_packing: true`, and "batch_ring" otherwise. ring_attn_func: ``` diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index 9dd62f0f7d..f944cbd6a8 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -14,6 +14,7 @@ from axolotl.utils.data.rl import load_prepare_preference_datasets from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_processor, load_tokenizer +from axolotl.utils.schemas.enums import RLType from axolotl.utils.tokenization import check_dataset_labels LOG = logging.getLogger(__name__) @@ -133,7 +134,7 @@ def load_preference_datasets( total_num_steps: Optional[int] = int( math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) ) - if cfg.rl == "grpo": + if cfg.rl is RLType.GRPO: total_num_steps = None if cli_args.debug or cfg.debug: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 670561edec..99ab397c7e 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -87,7 +87,7 @@ ) from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator from axolotl.utils.models import ensure_dtype -from axolotl.utils.schemas.enums import CustomSupportedOptimizers +from axolotl.utils.schemas.enums import CustomSupportedOptimizers, RLType try: import torch._dynamo # pylint: disable=ungrouped-imports @@ -353,7 +353,7 @@ def build(self, total_num_steps): training_arguments_kwargs["warmup_steps"] = warmup_steps training_arguments_kwargs["logging_steps"] = logging_steps - if self.cfg.seed: + if self.cfg.seed is not None: training_arguments_kwargs["seed"] = self.cfg.seed if self.cfg.gradient_checkpointing: @@ -547,8 +547,6 @@ def build(self, total_num_steps): report_to = [] if self.cfg.use_wandb: report_to.append("wandb") - if self.cfg.wandb_name: - training_arguments_kwargs["run_name"] = self.cfg.wandb_name if self.cfg.use_mlflow: report_to.append("mlflow") if self.cfg.use_tensorboard: @@ -821,14 +819,15 @@ def build(self, total_num_steps): data_collator_kwargs = { "padding": True, # True/"longest" is the default } + multiple = 64 if self.cfg.pad_to_sequence_len: - data_collator_kwargs["pad_to_multiple_of"] = 64 * math.ceil( - self.cfg.sequence_len / 64 + data_collator_kwargs["pad_to_multiple_of"] = multiple * math.ceil( + self.cfg.sequence_len / multiple ) else: # A100 is best at 64, while others at 8. Let's use the larger so we don't have to check # https://docs.nvidia.com/deeplearning/performance/dl-performance-matrix-multiplication/index.html - data_collator_kwargs["pad_to_multiple_of"] = 64 + data_collator_kwargs["pad_to_multiple_of"] = multiple if self.cfg.reward_model: data_collator_kwargs["max_length"] = self.cfg.sequence_len @@ -1034,6 +1033,10 @@ def build_training_arguments(self, total_num_steps): training_args_kwargs["dataloader_prefetch_factor"] = ( self.cfg.dataloader_prefetch_factor ) + + if self.cfg.seed is not None: + training_args_kwargs["seed"] = self.cfg.seed + if self.cfg.gradient_checkpointing: training_args_kwargs["gradient_checkpointing"] = ( self.cfg.gradient_checkpointing @@ -1076,9 +1079,13 @@ def build_training_arguments(self, total_num_steps): if self.cfg.use_wandb: training_args_kwargs["run_name"] = self.cfg.wandb_name + training_args_kwargs["sequence_parallel_degree"] = ( + self.cfg.sequence_parallel_degree + ) + training_args_cls = None blocklist_args_kwargs = [] - if self.cfg.rl == "simpo": + if self.cfg.rl is RLType.SIMPO: training_args_cls = AxolotlCPOConfig training_args_kwargs["loss_type"] = "simpo" training_args_kwargs["max_length"] = self.cfg.sequence_len @@ -1086,13 +1093,13 @@ def build_training_arguments(self, total_num_steps): if self.cfg.cpo_alpha is not None: training_args_kwargs["cpo_alpha"] = self.cfg.cpo_alpha - elif self.cfg.rl == "orpo": + elif self.cfg.rl is RLType.ORPO: training_args_cls = AxolotlORPOConfig training_args_kwargs["max_length"] = self.cfg.sequence_len if self.cfg.max_prompt_len: training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len - elif self.cfg.rl == "kto": + elif self.cfg.rl is RLType.KTO: training_args_cls = AxolotlKTOConfig training_args_kwargs["desirable_weight"] = ( @@ -1106,14 +1113,14 @@ def build_training_arguments(self, total_num_steps): if self.cfg.max_prompt_len: training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len - elif self.cfg.rl == "grpo": + elif self.cfg.rl is RLType.GRPO: training_args_cls = GRPOStrategy.get_training_args_class() training_args_kwargs.update(GRPOStrategy.set_training_args_kwargs(self.cfg)) blocklist_args_kwargs = GRPOStrategy.get_blocklist_args_kwargs() else: training_args_cls = AxolotlDPOConfig - if self.cfg.rl == "ipo": + if self.cfg.rl is RLType.IPO: training_args_kwargs["loss_type"] = "ipo" training_args_kwargs["max_length"] = self.cfg.sequence_len training_args_kwargs["max_completion_length"] = None @@ -1156,33 +1163,35 @@ def build_training_arguments(self, total_num_steps): def build(self, total_num_steps): training_args = self.build_training_arguments(total_num_steps) - dpo_trainer_kwargs = {} - if self.cfg.rl == "ipo": + trainer_kwargs = {} + if self.cfg.rl is RLType.IPO: if self.cfg.dpo_label_smoothing: - dpo_trainer_kwargs["label_smoothing"] = self.cfg.dpo_label_smoothing + trainer_kwargs["label_smoothing"] = self.cfg.dpo_label_smoothing if self.eval_dataset: - dpo_trainer_kwargs["eval_dataset"] = self.eval_dataset + trainer_kwargs["eval_dataset"] = self.eval_dataset if self.cfg.adapter and self.peft_config: - dpo_trainer_kwargs["peft_config"] = self.peft_config + trainer_kwargs["peft_config"] = self.peft_config if self.cfg.precompute_ref_log_probs is not None: - dpo_trainer_kwargs["precompute_ref_log_probs"] = ( + trainer_kwargs["precompute_ref_log_probs"] = ( self.cfg.precompute_ref_log_probs ) - if self.cfg.rl == "grpo": - trainer_cls = GRPOStrategy.get_trainer_class() + if self.cfg.rl is RLType.GRPO: + trainer_cls = GRPOStrategy.get_trainer_class( + sequence_parallel=self.cfg.sequence_parallel_degree > 1 + ) trainer_cls_args = [self.model] trainer_cls_args.extend(GRPOStrategy.set_trainer_args(self.cfg)) - dpo_trainer_kwargs.update(GRPOStrategy.set_trainer_kwargs(self.cfg)) - elif self.cfg.rl in ["dpo", "ipo"]: + trainer_kwargs.update(GRPOStrategy.set_trainer_kwargs(self.cfg)) + elif self.cfg.rl in [RLType.DPO, RLType.IPO]: trainer_cls = DPOStrategy.get_trainer_class() trainer_cls_args = [self.model, self.model_ref] - elif self.cfg.rl == "orpo": + elif self.cfg.rl is RLType.ORPO: trainer_cls = AxolotlORPOTrainer trainer_cls_args = [self.model] - elif self.cfg.rl in ["kto"]: + elif self.cfg.rl is RLType.KTO: trainer_cls = AxolotlKTOTrainer trainer_cls_args = [self.model] - elif self.cfg.rl in ["simpo"]: + elif self.cfg.rl is RLType.SIMPO: trainer_cls = AxolotlCPOTrainer trainer_cls_args = [self.model] else: @@ -1190,33 +1199,33 @@ def build(self, total_num_steps): sig = inspect.signature(trainer_cls) if "tokenizer" in sig.parameters.keys(): - dpo_trainer_kwargs["tokenizer"] = self.tokenizer + trainer_kwargs["tokenizer"] = self.tokenizer else: - dpo_trainer_kwargs["processing_class"] = self.tokenizer + trainer_kwargs["processing_class"] = self.tokenizer if self.cfg.datasets is not None and ( trainer_cls is DPOStrategy.get_trainer_class() ): - dpo_trainer_kwargs["dataset_tags"] = [ + trainer_kwargs["dataset_tags"] = [ d["path"] for d in self.cfg.datasets if not Path(d["path"]).is_dir() ] - dpo_trainer = trainer_cls( + trainer = trainer_cls( *trainer_cls_args, args=training_args, train_dataset=self.train_dataset, callbacks=self.get_callbacks(), - **dpo_trainer_kwargs, + **trainer_kwargs, ) if self.cfg.fsdp: - ensure_dtype(dpo_trainer.model, dtype=self.cfg.torch_dtype) - if self.cfg.rl in ["dpo", "ipo"] and dpo_trainer.ref_model: - ensure_dtype(dpo_trainer.ref_model, dtype=self.cfg.torch_dtype) + ensure_dtype(trainer.model, dtype=self.cfg.torch_dtype) + if self.cfg.rl in [RLType.DPO, RLType.IPO] and trainer.ref_model: + ensure_dtype(trainer.ref_model, dtype=self.cfg.torch_dtype) - dpo_trainer = self.hook_post_create_trainer(dpo_trainer) - for callback in self.get_post_trainer_create_callbacks(dpo_trainer): - dpo_trainer.add_callback(callback) + trainer = self.hook_post_create_trainer(trainer) + for callback in self.get_post_trainer_create_callbacks(trainer): + trainer.add_callback(callback) - return dpo_trainer + return trainer class HFPPOTrainerBuilder(TrainerBuilderBase): diff --git a/src/axolotl/core/trainers/__init__.py b/src/axolotl/core/trainers/__init__.py index 32a889af9a..2cdc9c195e 100644 --- a/src/axolotl/core/trainers/__init__.py +++ b/src/axolotl/core/trainers/__init__.py @@ -5,7 +5,7 @@ from .base import AxolotlTrainer from .dpo.trainer import AxolotlDPOTrainer -from .grpo.trainer import AxolotlGRPOTrainer +from .grpo.trainer import AxolotlGRPOSequenceParallelTrainer, AxolotlGRPOTrainer from .mamba import AxolotlMambaTrainer from .relora import ReLoRATrainer from .trl import ( diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index ab9735adcd..2f0ce68949 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -373,15 +373,13 @@ def compute_loss( num_items_in_batch=num_items_in_batch, ) - loss = super().compute_loss( + return super().compute_loss( model, inputs, return_outputs=return_outputs, num_items_in_batch=num_items_in_batch, ) - return loss - @staticmethod def orpo_concatenate_inputs(inputs, label_pad_token=-100, pad_token=0, device=None): concatenated_batch = {} diff --git a/src/axolotl/core/trainers/dpo/__init__.py b/src/axolotl/core/trainers/dpo/__init__.py index 2d6835cf79..603fdf0b69 100644 --- a/src/axolotl/core/trainers/dpo/__init__.py +++ b/src/axolotl/core/trainers/dpo/__init__.py @@ -1,14 +1,11 @@ -""" -DPO Specific Strategy for training -""" +"""DPO Specific Strategy for training""" from axolotl.core.trainers.dpo.trainer import AxolotlDPOTrainer +from axolotl.utils.schemas.enums import RLType class DPOStrategy: - """ - Strategy for DPO training - """ + """Strategy for DPO training""" @classmethod def get_trainer_class(cls): @@ -23,7 +20,7 @@ def get_training_args_class(cls): @classmethod def set_training_args_kwargs(cls, cfg): training_args_kwargs = {} - if cfg.rl == "ipo": + if cfg.rl is RLType.IPO: training_args_kwargs["loss_type"] = "ipo" training_args_kwargs["max_length"] = cfg.sequence_len training_args_kwargs["max_completion_length"] = None diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 078fdcb227..f4685893b0 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -1,37 +1,41 @@ -""" -GRPO Specific Strategy for training -""" +"""GRPO Specific Strategy for training""" import importlib import inspect import logging +from typing import Any from trl.trainer.grpo_trainer import RewardFunc -from axolotl.core.trainers.grpo.trainer import AxolotlGRPOTrainer +from axolotl.core.trainers.grpo.args import AxolotlGRPOConfig +from axolotl.core.trainers.grpo.trainer import ( + AxolotlGRPOSequenceParallelTrainer, + AxolotlGRPOTrainer, +) +from axolotl.utils.dict import DictDefault from axolotl.utils.schemas.trl import TRLConfig -LOG = logging.getLogger("axolotl") +LOG = logging.getLogger(__name__) class GRPOStrategy: - """ - Strategy for GRPO training - """ + """Strategy for GRPO training""" @classmethod - def get_trainer_class(cls): + def get_trainer_class( + cls, sequence_parallel: bool + ) -> type[AxolotlGRPOTrainer] | type[AxolotlGRPOSequenceParallelTrainer]: + if sequence_parallel: + return AxolotlGRPOSequenceParallelTrainer return AxolotlGRPOTrainer @classmethod - def get_training_args_class(cls): - from axolotl.core.trainers.grpo.args import AxolotlGRPOConfig - + def get_training_args_class(cls) -> type[AxolotlGRPOConfig]: return AxolotlGRPOConfig @classmethod - def set_training_args_kwargs(cls, cfg): - grpo_args_kwargs = {} + def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: + grpo_args_kwargs: dict[str, Any] = {} if not hasattr(cfg, "trl") or not cfg.trl: return grpo_args_kwargs @@ -40,8 +44,8 @@ def set_training_args_kwargs(cls, cfg): if trl.use_vllm: grpo_args_kwargs["use_vllm"] = trl.use_vllm - grpo_args_kwargs["vllm_server_host"] = trl.vllm_server_host or trl.vllm.host - grpo_args_kwargs["vllm_server_port"] = trl.vllm_server_port or trl.vllm.port + grpo_args_kwargs["vllm_server_host"] = trl.vllm_server_host or trl.vllm.host # type: ignore[attr-defined] + grpo_args_kwargs["vllm_server_port"] = trl.vllm_server_port or trl.vllm.port # type: ignore[attr-defined] if trl.vllm_server_timeout: grpo_args_kwargs["vllm_server_timeout"] = trl.vllm_server_timeout if trl.vllm_guided_decoding_regex: @@ -102,17 +106,18 @@ def set_training_args_kwargs(cls, cfg): return grpo_args_kwargs @classmethod - def set_trainer_args(cls, cfg): + def set_trainer_args(cls, cfg: DictDefault) -> list[Any]: trainer_args = [] if cfg.trl and cfg.trl.reward_funcs: reward_funcs = [] for reward_func_fqn in cfg.trl.reward_funcs: reward_funcs.append(cls.get_reward_func(reward_func_fqn)) trainer_args.append(reward_funcs) + return trainer_args @classmethod - def set_trainer_kwargs(cls, cfg): + def set_trainer_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: trainer_kwargs = {} if cfg.trl and cfg.trl.reward_processing_classes: trainer_kwargs["reward_processing_classes"] = ( @@ -126,7 +131,7 @@ def get_collator(cls, *args, **kwargs): # pylint: disable=unused-argument return None @classmethod - def get_blocklist_args_kwargs(cls): + def get_blocklist_args_kwargs(cls) -> list[str]: return ["dataset_num_proc"] @classmethod @@ -137,13 +142,13 @@ def get_reward_func(cls, reward_func_fqn: str) -> RewardFunc: Args: reward_func_fqn (str): Fully qualified name of the reward function (e.g. r1_grpo.gsm8k_transform), or a HF hub path to the reward model. - Raises: - ValueError: If the reward function does not accept at least two arguments. Returns: RewardFunc: A callable that accepts prompts and completions and returns rewards, or a path to a reward model. + Raises: + ValueError: If the reward function does not accept at least two arguments. """ try: # use importlib to dynamically load the reward function from the module diff --git a/src/axolotl/core/trainers/grpo/args.py b/src/axolotl/core/trainers/grpo/args.py index 5460edca92..76be88c896 100644 --- a/src/axolotl/core/trainers/grpo/args.py +++ b/src/axolotl/core/trainers/grpo/args.py @@ -11,6 +11,4 @@ @dataclass class AxolotlGRPOConfig(AxolotlTrainingMixins, GRPOConfig): - """ - Axolotl GRPO Config for GRPO training - """ + """Axolotl GRPO Config for GRPO training""" diff --git a/src/axolotl/core/trainers/grpo/sampler.py b/src/axolotl/core/trainers/grpo/sampler.py new file mode 100644 index 0000000000..ebc6e19e2a --- /dev/null +++ b/src/axolotl/core/trainers/grpo/sampler.py @@ -0,0 +1,172 @@ +"""Repeat random sampler (similar to the one implemented in +https://github.com/huggingface/trl/blob/main/trl/trainer/grpo_trainer.py) that adds +sequence parallelism functionality; i.e., duplicating data across ranks in the same +sequence parallel group. +""" + +from typing import Iterator, Sized + +import torch +from torch.utils.data import Sampler + + +class SequenceParallelRepeatRandomSampler(Sampler): + """Sampler for GRPO training with sequence parallelism. + + This sampler ensures: + - Ranks in the same sequence parallel (SP) group receive identical data. + - Each index is repeated multiple times for sampling different completions. + - Entire batches are repeated for reuse in multiple updates. + - Data is properly distributed across SP groups. + + In the table below, the values represent dataset indices. Each SP group has + `sequence_parallel_degree = 2` GPUs working together on the same data. There are 2 + SP groups (SP0 and SP1), with `world_size = 4` total GPUs. + + Sequence Parallel Groups + | SP0 | SP1 | + | GPU 0 | GPU 1 | GPU 2 | GPU 3 | + global_step step <---> mini_repeat_count=3 + <----------> batch_size=2 per SP group + grad_accum=2 ▲ ▲ 0 0 [0 0 0 1 1 1] [2 2 2 3 3 3] <- SP groups get different data + ▼ | 0 1 [0 0 0 1 1 1] [2 2 2 3 3 3] <- Same data for each SP group GPU + | + | 1 2 [0 0 0 1 1 1] [2 2 2 3 3 3] <- Repeat same indices for iterations + num_iterations=2 ▼ 1 3 [0 0 0 1 1 1] [2 2 2 3 3 3] <- When using gradient accumulation + + 2 4 [4 4 4 5 5 5] [6 6 6 7 7 7] <- New batch of data indices + 2 5 [4 4 4 5 5 5] [6 6 6 7 7 7] + ... + + Args: + dataset: Dataset to sample from. + mini_repeat_count: How many times to repeat each sample immediately. + world_size: Total number of processes. + rank: Rank of current process. + batch_size: Number of samples per batch. + repeat_count: How many times to repeat the full sampling process. + sequence_parallel_degree: Number of ranks in a sequence parallel group. + shuffle: Whether to shuffle the dataset. + seed: Random seed for shuffling. + drop_last: Whether to drop the last incomplete batch. + """ + + def __init__( + self, + dataset: Sized, + mini_repeat_count: int, + world_size: int, + rank: int, + batch_size: int = 1, + repeat_count: int = 1, + sequence_parallel_degree: int = 1, + shuffle: bool = True, + seed: int = 0, + drop_last: bool = False, + ): + self.dataset = dataset + self.mini_repeat_count = mini_repeat_count + self.batch_size = batch_size + self.repeat_count = repeat_count + self.shuffle = shuffle + self.seed = seed + self.drop_last = drop_last + self.epoch = 0 + + self.world_size = world_size + self.rank = rank + + # Sequence parallelism parameters + self.sequence_parallel_degree = sequence_parallel_degree + self.num_sp_groups = world_size // sequence_parallel_degree + self.sp_group_id = rank // sequence_parallel_degree + + # Adjust dataset size for distributed sampling + self.num_samples = len(self.dataset) + self.total_size = self.num_samples + + # Calculate effective number of samples per SP group + if ( + self.drop_last + and self.total_size % (self.num_sp_groups * self.batch_size) != 0 + ): + # Drop last incomplete batch if drop_last is True + self.num_samples_per_sp_group = ( + self.total_size // self.batch_size // self.num_sp_groups + ) * self.batch_size + else: + # Round up to include last batch if drop_last is False + self.num_samples_per_sp_group = ( + (self.total_size + self.batch_size * self.num_sp_groups - 1) + // (self.batch_size * self.num_sp_groups) + * self.batch_size + ) + + if shuffle: + self.generator = torch.Generator() + self.generator.manual_seed(seed) + + def __iter__(self) -> Iterator[int]: + """Creates iterator over dataset indices. + + Returns: + Iterator that yields indices into the dataset. + """ + # Deterministically shuffle based on epoch and seed + if self.shuffle: + indices = torch.randperm( + self.num_samples, generator=self.generator + ).tolist() + else: + indices = list(range(self.num_samples)) + + # Add extra samples to make it evenly divisible by batch_size + if len(indices) % self.batch_size != 0: + padding = indices[: self.batch_size - len(indices) % self.batch_size] + indices += padding + + # Subsample based on SP group ID + # Each SP group gets distinct batches of data + batch_indices = [] + for i in range(0, len(indices), self.batch_size * self.num_sp_groups): + start_idx = i + self.sp_group_id * self.batch_size + end_idx = min(start_idx + self.batch_size, len(indices)) + if start_idx < len(indices): + for j in range(self.batch_size): + if start_idx + j < end_idx: + batch_indices.append(indices[start_idx + j]) + + # Make sure batch_indices is exactly batch_size * num_batches_per_sp_group + if self.drop_last: + num_batches_per_sp_group = self.num_samples_per_sp_group // self.batch_size + target_len = self.batch_size * num_batches_per_sp_group + if len(batch_indices) > target_len: + batch_indices = batch_indices[:target_len] + + # Apply the GRPO repeat pattern + final_indices = [] + for _ in range(self.repeat_count): + for idx in batch_indices: + for _ in range(self.mini_repeat_count): + final_indices.append(idx) + + return iter(final_indices) + + def __len__(self) -> int: + """Returns the total length of the iterable including repetitions. + + Returns: + Total number of samples. + """ + # Total length including all repetitions + return ( + self.num_samples_per_sp_group * self.mini_repeat_count * self.repeat_count + ) + + def set_epoch(self, epoch: int) -> None: + """Sets the epoch for this sampler. + + Args: + epoch: Epoch number to use for shuffling. + """ + self.epoch = epoch diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index 25aafa6a7c..bc3d140b10 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -1,23 +1,63 @@ -""" -Axolotl GRPO trainer -""" +"""Axolotl GRPO trainers (with and without sequence parallelism handling)""" +# pylint: disable=too-many-lines,duplicate-code,protected-access,no-member + +import warnings from contextlib import nullcontext +from typing import Any -from accelerate.utils import is_deepspeed_available, is_peft_model +import datasets +import torch +import torch.distributed as dist +import torch.utils.data +from accelerate.utils import ( + broadcast_object_list, + gather, + gather_object, + is_peft_model, +) +from datasets import Dataset, IterableDataset +from torch import nn +from torch.utils.data import ( + BatchSampler, + DataLoader, + Sampler, +) +from transformers import ( + PreTrainedModel, + PreTrainedTokenizerBase, + Trainer, + TrainerCallback, +) +from transformers.trainer_utils import seed_worker +from transformers.utils import is_peft_available from trl import GRPOTrainer -from trl.extras.profiling import profiling_decorator +from trl.data_utils import ( + apply_chat_template, + is_conversational, + maybe_apply_chat_template, +) +from trl.extras.profiling import profiling_context, profiling_decorator +from trl.import_utils import is_deepspeed_available +from trl.models import unwrap_model_for_generation +from trl.trainer.grpo_config import GRPOConfig +from trl.trainer.grpo_trainer import RewardFunc, nanstd +from trl.trainer.utils import pad +from axolotl.core.trainers.grpo.sampler import SequenceParallelRepeatRandomSampler from axolotl.core.trainers.mixins import RngLoaderMixin, SchedulerMixin +from axolotl.monkeypatch.attention.ring_attn.patch import get_ring_attn_group + +if is_peft_available(): + # pylint: disable=unused-import + from peft import PeftConfig if is_deepspeed_available(): import deepspeed class AxolotlGRPOTrainer(RngLoaderMixin, SchedulerMixin, GRPOTrainer): - """ - Extend the base GRPOTrainer for axolotl helpers - """ + """Extend the base GRPOTrainer for axolotl helpers""" _tag_names = ["trl", "grpo", "axolotl"] @@ -67,3 +107,600 @@ def _move_model_to_vllm(self): # Reset cache on main process if self.accelerator.is_main_process: self.vllm_client.reset_prefix_cache() + + +class AxolotlGRPOSequenceParallelTrainer(AxolotlGRPOTrainer): + """Extend the base GRPOTrainer for sequence parallelism handling""" + + def __init__( + self, + model: str | PreTrainedModel, + reward_funcs: RewardFunc | list[RewardFunc], + args: GRPOConfig | None = None, + train_dataset: Dataset | IterableDataset | None = None, + eval_dataset: ( + Dataset | IterableDataset | dict[str, Dataset | IterableDataset] | None + ) = None, + processing_class: PreTrainedTokenizerBase | None = None, + reward_processing_classes: ( + PreTrainedTokenizerBase | list[PreTrainedTokenizerBase] | None + ) = None, + callbacks: list[TrainerCallback] | None = None, + optimizers: tuple[ + torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None + ] = (None, None), + peft_config: "PeftConfig | None" = None, + ): + # First call the superclass constructor with all arguments + super().__init__( + model=model, + reward_funcs=reward_funcs, + args=args, + train_dataset=train_dataset, + eval_dataset=eval_dataset, + processing_class=processing_class, + reward_processing_classes=reward_processing_classes, + callbacks=callbacks, + optimizers=optimizers, + peft_config=peft_config, + ) + + # Get number of SP groups (number of processes divided by SP degree) + num_processes = self.accelerator.num_processes + num_sp_groups = num_processes // self.args.sequence_parallel_degree + + # Calculate batch size per SP group (not per process) + sp_group_batch_size = self.args.per_device_train_batch_size * num_sp_groups + possible_values = [ + n_gen + for n_gen in range(2, sp_group_batch_size + 1) + if (sp_group_batch_size) % n_gen == 0 + ] + + if self.num_generations not in possible_values: + raise ValueError( + f"The batch size per SP group ({num_sp_groups} x " + f"{self.args.per_device_train_batch_size}) must be evenly divisible by " + f"the number of generations per prompt ({self.num_generations}). Given " + "the current configuration, the valid values for the number of " + f"generations are: {possible_values}." + ) + + if self.args.eval_strategy != "no": + # If sequence parallelism is enabled, calculate batch size per SP group + sp_group_eval_batch_size = args.per_device_eval_batch_size * num_sp_groups # type: ignore[union-attr] + possible_values = [ + n_gen + for n_gen in range(2, sp_group_eval_batch_size + 1) + if (sp_group_eval_batch_size) % n_gen == 0 + ] + + if self.num_generations not in possible_values: + raise ValueError( + f"With sequence parallelism (degree {self.args.sequence_parallel_degree}), " + f"the eval batch size per SP group ({num_sp_groups} x {self.args.per_device_eval_batch_size}) " + f"must be evenly divisible by the number of generations per prompt " + f"({self.num_generations}). Given the current eval batch size, " + f"the valid values for the number of generations are: {possible_values}." + ) + + # Initialize the SP group + self.sp_group = get_ring_attn_group() + self.rank = dist.get_rank() + self.world_size = dist.get_world_size() + self.local_rank = dist.get_rank(group=self.sp_group) + self.local_world_size = dist.get_world_size(group=self.sp_group) + + def _get_train_sampler(self) -> Sampler: + effective_batch_size = ( + self.args.per_device_train_batch_size + * self.world_size + * self.args.gradient_accumulation_steps + ) + + return SequenceParallelRepeatRandomSampler( + dataset=self.train_dataset, + mini_repeat_count=self.num_generations, + world_size=self.world_size, + rank=self.rank, + batch_size=effective_batch_size + // self.num_generations + // self.args.sequence_parallel_degree, + repeat_count=self.num_iterations * self.args.gradient_accumulation_steps, + sequence_parallel_degree=self.args.sequence_parallel_degree, + shuffle=True, + seed=self.args.seed, + drop_last=True, + ) + + def _create_dataloader_params(self, is_eval=False, custom_batch_size=None): + """Create common dataloader parameters for train or eval.""" + batch_size = custom_batch_size or ( + self.args.eval_batch_size if is_eval else self._train_batch_size + ) + + params = { + "batch_size": batch_size, + "collate_fn": self.data_collator, + "num_workers": self.args.dataloader_num_workers, + "pin_memory": self.args.dataloader_pin_memory, + } + + # Add persistent workers only for training + if not is_eval and hasattr(self.args, "dataloader_persistent_workers"): + params["persistent_workers"] = self.args.dataloader_persistent_workers + + # Add prefetch factor if specified + if self.args.dataloader_prefetch_factor: + params["prefetch_factor"] = self.args.dataloader_prefetch_factor + + return params + + def _prepare_dataloader( + self, dataset, sampler, is_eval=False, custom_batch_size=None + ): + """Prepare a dataloader with the given dataset and sampler.""" + # Get base parameters + dataloader_params = self._create_dataloader_params(is_eval, custom_batch_size) + + # Add sampler configuration + if not isinstance(dataset, torch.utils.data.IterableDataset): + if isinstance(sampler, BatchSampler): + # batch_size and batch_sampler are mutually exclusive + dataloader_params["batch_sampler"] = sampler + del dataloader_params["batch_size"] + else: + dataloader_params["sampler"] = sampler + dataloader_params["drop_last"] = self.args.dataloader_drop_last + + if not is_eval: + dataloader_params["worker_init_fn"] = seed_worker + + # Create the dataloader + dataloader = DataLoader(dataset, **dataloader_params) + + if self.args.sample_packing and ( + (not is_eval and not self.args.pretraining) + or (is_eval and self.args.eval_sample_packing is not False) + ): + self.accelerator.even_batches = False + + # Return unprepared dataloader if using sequence parallelism + # TODO(djsaunde): We might be able to use `accelerate`'s dataloader preparation + # if we use `dispatch_batches` and `slice_fn_for_dispatch` properly (i.e., + # slice each batch along the sequence dimension). + if self.args.sequence_parallel_degree > 1: + return dataloader + + # Otherwise prepare with accelerator + return self.accelerator.prepare_data_loader(dataloader) + + def get_train_dataloader(self) -> DataLoader: + """Get dataloader for training""" + train_dataset = self.train_dataset + # pylint: disable=access-member-before-definition + data_collator = self.data_collator # type: ignore + + # Handle dataset preprocessing + if isinstance(train_dataset, datasets.Dataset): + # Add debug print before any modifications + if self.args.sample_packing and not self.args.pretraining: + train_dataset = train_dataset.remove_columns(["length"]) + if not self.args.sample_packing or self.args.pretraining: + train_dataset = self._remove_unused_columns( + train_dataset, description="training" + ) + else: + self.data_collator = self._get_collator_with_removed_columns( # pylint: disable=attribute-defined-outside-init + data_collator, + description="training", + ) + + # Get sampler and create dataloader + sampler = self._get_train_sampler() + dataloader = self._prepare_dataloader(train_dataset, sampler, is_eval=False) + + return dataloader + + def _generate_and_score_completions( + self, inputs: list[dict[str, torch.Tensor | Any]] + ) -> dict[str, torch.Tensor | Any]: + device = self.accelerator.device + mode = "eval" if self.control.should_evaluate else "train" + + prompts = [x["prompt"] for x in inputs] + prompts_text = [ + maybe_apply_chat_template(example, self.processing_class)["prompt"] + for example in inputs + ] + prompt_inputs = self.processing_class( + text=prompts_text, + return_tensors="pt", + padding=True, + padding_side="left", + add_special_tokens=False, + ) + prompt_inputs = Trainer._prepare_inputs(self, prompt_inputs) + prompt_ids, prompt_mask = ( + prompt_inputs["input_ids"], + prompt_inputs["attention_mask"], + ) + + if self.max_prompt_length is not None: + prompt_ids = prompt_ids[:, -self.max_prompt_length :] + prompt_mask = prompt_mask[:, -self.max_prompt_length :] + + # Generate completions using either vLLM or regular generation + if self.args.use_vllm: + # First, have main process load weights if needed + # pylint: disable=access-member-before-definition + if self.state.global_step != self._last_loaded_step: # type: ignore[has-type] + self._move_model_to_vllm() + # pylint: disable=attribute-defined-outside-init + self._last_loaded_step = self.state.global_step + + # Generate completions using vLLM: gather all prompts and use them in a single call in the main process + all_prompts_text = gather_object(prompts_text) + if self.accelerator.is_main_process: + if self.args.sequence_parallel_degree > 1: + # Calculate sequence parallel group information + world_size = self.accelerator.num_processes + sequence_parallel_degree = self.args.sequence_parallel_degree + num_sp_groups = world_size // sequence_parallel_degree + + # Since processes in the same SP group have the same prompts, we need to ensure + # we only take one copy of each prompt from each SP group + ordered_set_of_prompts = [] + for sp_group_id in range(num_sp_groups): + # Get the first process from each SP group (typically the group leader) + group_leader_rank = sp_group_id * sequence_parallel_degree + + # Extract prompts from this SP group, accounting for num_generations duplicates + # We only need prompts from one rank in each SP group + group_prompts = all_prompts_text[ + group_leader_rank + * len(prompts_text) : (group_leader_rank + 1) + * len(prompts_text) : self.num_generations + ] + + ordered_set_of_prompts.extend(group_prompts) + else: + # Since 'prompts' contains 'num_generations' duplicates, we first take unique prompts, and generate + # num_generations outputs for each one. This is faster than generating outputs for each duplicate + # prompt individually. + ordered_set_of_prompts = all_prompts_text[ + :: self.num_generations * self.args.sequence_parallel_degree + ] + + with profiling_context(self, "vLLM.generate"): + completion_ids = self.vllm_client.generate( + prompts=ordered_set_of_prompts, + n=self.num_generations, + repetition_penalty=self.repetition_penalty, + temperature=self.temperature, + top_p=self.top_p, + top_k=-1 if self.top_k is None else self.top_k, + min_p=0.0 if self.min_p is None else self.min_p, + max_tokens=self.max_completion_length, + guided_decoding_regex=self.guided_decoding_regex, + ) + else: + completion_ids = [None] * ( + len(all_prompts_text) // self.args.sequence_parallel_degree + ) + + # Broadcast the completions from the main process to all processes + completion_ids = broadcast_object_list(completion_ids, from_process=0) + + # Determine the appropriate slice based on sequence parallelism + if self.args.sequence_parallel_degree > 1: + # Calculate SP group ID (which group of ranks this rank belongs to) + sp_group_id = self.accelerator.process_index // self.local_world_size + + # Calculate the start index for this SP group + sp_group_start = sp_group_id * len(prompts) * self.local_world_size + + # All ranks in the same SP group get the same data slice + process_slice = slice( + sp_group_start, + sp_group_start + len(prompts), + ) + completion_ids = completion_ids[process_slice] + else: + # Original behavior for non-sequence parallel case + process_slice = slice( + self.accelerator.process_index * len(prompts), + (self.accelerator.process_index + 1) * len(prompts), + ) + completion_ids = completion_ids[process_slice] + + # Pad the completions, and concatenate them with the prompts + completion_ids = [ + torch.tensor(ids, device=device) for ids in completion_ids + ] + completion_ids = pad( + completion_ids, padding_value=self.processing_class.pad_token_id + ) + prompt_completion_ids = torch.cat([prompt_ids, completion_ids], dim=1) + else: + # Regular generation path + with unwrap_model_for_generation( + self.model_wrapped, + self.accelerator, + gather_deepspeed3_params=self.args.ds3_gather_for_generation, + ) as unwrapped_model: + prompt_completion_ids = unwrapped_model.generate( + prompt_ids, + attention_mask=prompt_mask, + generation_config=self.generation_config, + ) + + # Compute prompt length and extract completion ids + prompt_length = prompt_ids.size(1) + prompt_ids = prompt_completion_ids[:, :prompt_length] + completion_ids = prompt_completion_ids[:, prompt_length:] + + # Mask everything after the first EOS token + is_eos = completion_ids == self.processing_class.eos_token_id + eos_idx = torch.full( + (is_eos.size(0),), is_eos.size(1), dtype=torch.long, device=device + ) + eos_idx[is_eos.any(dim=1)] = is_eos.int().argmax(dim=1)[is_eos.any(dim=1)] + sequence_indices = torch.arange(is_eos.size(1), device=device).expand( + is_eos.size(0), -1 + ) + completion_mask = (sequence_indices <= eos_idx.unsqueeze(1)).int() + + # If mask_truncated_completions is enabled, zero out truncated completions in completion_mask + if self.args.mask_truncated_completions: + truncated_completions = ~is_eos.any(dim=1) + completion_mask = ( + completion_mask * (~truncated_completions).unsqueeze(1).int() + ) + + # Concatenate prompt_mask with completion_mask for logit computation + attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) # (B, P+C) + + logits_to_keep = completion_ids.size( + 1 + ) # we only need to compute the logits for the completion tokens + batch_size = ( + self.args.per_device_train_batch_size + if mode == "train" + else self.args.per_device_eval_batch_size + ) + + with torch.no_grad(): + # When using num_iterations == 1, old_per_token_logps == per_token_logps, so we can skip it's + # computation here, and use per_token_logps.detach() instead. + if self.num_iterations > 1: + old_per_token_logps = self._get_per_token_logps( + self.model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + batch_size, + ) + else: + old_per_token_logps = None + + if self.beta == 0.0: + ref_per_token_logps = None + elif self.ref_model is not None: + ref_per_token_logps = self._get_per_token_logps( + self.ref_model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + batch_size, + ) + else: + with self.accelerator.unwrap_model(self.model).disable_adapter(): + ref_per_token_logps = self._get_per_token_logps( + self.model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + batch_size, + ) + + # Decode the generated completions + completions_text = self.processing_class.batch_decode( + completion_ids, skip_special_tokens=True + ) + if is_conversational(inputs[0]): + completions = [] + for prompt, completion in zip(prompts, completions_text): + bootstrap = ( + prompt.pop()["content"] if prompt[-1]["role"] == "assistant" else "" + ) + completions.append( + [{"role": "assistant", "content": bootstrap + completion}] + ) + else: + completions = completions_text + + rewards_per_func = torch.zeros( + len(prompts), len(self.reward_funcs), device=device + ) + for i, (reward_func, reward_processing_class, reward_func_name) in enumerate( + zip( + self.reward_funcs, + self.reward_processing_classes, + self.reward_func_names, + ) + ): + with profiling_context(self, reward_func_name): + if isinstance( + reward_func, nn.Module + ): # Module instead of PretrainedModel for compat with compiled models + if is_conversational(inputs[0]): + messages = [ + {"messages": p + c} for p, c in zip(prompts, completions) + ] + texts = [ + apply_chat_template(x, reward_processing_class)["text"] + for x in messages + ] + else: + texts = [p + c for p, c in zip(prompts, completions)] + reward_inputs = reward_processing_class( + text=texts, + return_tensors="pt", + padding=True, + padding_side="right", + add_special_tokens=False, + ) + reward_inputs = Trainer._prepare_inputs(self, reward_inputs) + with torch.inference_mode(): + rewards_per_func[:, i] = reward_func(**reward_inputs).logits[ + :, 0 + ] # Shape (B*G,) + else: + # Repeat all input columns (but "prompt" and "completion") to match the number of generations + keys = [ + key for key in inputs[0] if key not in ["prompt", "completion"] + ] + reward_kwargs = { + key: [example[key] for example in inputs] for key in keys + } + output_reward_func = reward_func( + prompts=prompts, completions=completions, **reward_kwargs + ) + # Convert None values to NaN + output_reward_func = [ + reward if reward is not None else torch.nan + for reward in output_reward_func + ] + + rewards_per_func[:, i] = torch.tensor( + output_reward_func, dtype=torch.float32, device=device + ) + + # If all reward functions return None for a given row, issue a detailed warning + if torch.isnan(rewards_per_func).all(dim=1).any(): + nan_row_idx = ( + torch.isnan(rewards_per_func).all(dim=1).nonzero(as_tuple=True)[0][0] + ) + row_reward_kwargs = { + key: value[nan_row_idx] for key, value in reward_kwargs.items() + } + row_reward_kwargs["prompt"] = prompts[nan_row_idx] + row_reward_kwargs["completion"] = completions[nan_row_idx] + warnings.warn( + f"All reward functions returned None for the following kwargs: {row_reward_kwargs}. " + "Please ensure that at least one reward function returns a valid reward." + ) + + # Gather the reward per function: this part is crucial, because the rewards are normalized per group and the + # completions may be distributed across processes + rewards_per_func = gather(rewards_per_func) + + # Apply weights to each reward function's output and sum + rewards = ( + rewards_per_func * self.reward_weights.to(device).unsqueeze(0) + ).nansum(dim=1) + + # Compute grouped-wise rewards + mean_grouped_rewards = rewards.view(-1, self.num_generations).mean(dim=1) + std_grouped_rewards = rewards.view(-1, self.num_generations).std(dim=1) + + # Normalize the rewards to compute the advantages + mean_grouped_rewards = mean_grouped_rewards.repeat_interleave( + self.num_generations, dim=0 + ) + std_grouped_rewards = std_grouped_rewards.repeat_interleave( + self.num_generations, dim=0 + ) + advantages = rewards - mean_grouped_rewards + if self.args.scale_rewards: + advantages = advantages / (std_grouped_rewards + 1e-4) + + # Slice to keep only the local part of the data + if self.args.sequence_parallel_degree > 1: + # Calculate SP group ID (which group of ranks this rank belongs to) + sp_group_id = self.accelerator.process_index // self.local_world_size + + # Calculate the start index for this SP group + sp_group_start = sp_group_id * len(prompts) * self.local_world_size + + # All ranks in the same SP group get the same data slice + process_slice = slice( + sp_group_start, + sp_group_start + len(prompts), + ) + else: + # Original behavior for non-sequence parallel case + process_slice = slice( + self.accelerator.process_index * len(prompts), + (self.accelerator.process_index + 1) * len(prompts), + ) + advantages = advantages[process_slice] + + # Log the metrics + if mode == "train": + self._total_train_tokens += ( + self.accelerator.gather_for_metrics(attention_mask.sum()).sum().item() + ) + self._metrics[mode]["num_tokens"] = [self._total_train_tokens] + + # log completion lengths, mean, min, max + agg_completion_mask = self.accelerator.gather_for_metrics( + completion_mask.sum(1) + ) + self._metrics[mode]["completions/mean_length"].append( + agg_completion_mask.float().mean().item() + ) + self._metrics[mode]["completions/min_length"].append( + agg_completion_mask.float().min().item() + ) + self._metrics[mode]["completions/max_length"].append( + agg_completion_mask.float().max().item() + ) + + # identify sequences that terminated with EOS and log their lengths + agg_terminated_with_eos = self.accelerator.gather_for_metrics(is_eos.any(dim=1)) + term_completion_mask = agg_completion_mask[agg_terminated_with_eos] + clipped_completions_ratio = 1 - len(term_completion_mask) / len( + agg_completion_mask + ) + self._metrics[mode]["completions/clipped_ratio"].append( + clipped_completions_ratio + ) + if len(term_completion_mask) == 0: + # edge case where no completed sequences are found + term_completion_mask = torch.zeros(1, device=device) + self._metrics[mode]["completions/mean_terminated_length"].append( + term_completion_mask.float().mean().item() + ) + self._metrics[mode]["completions/min_terminated_length"].append( + term_completion_mask.float().min().item() + ) + self._metrics[mode]["completions/max_terminated_length"].append( + term_completion_mask.float().max().item() + ) + + # Calculate mean reward per function, but only for samples where the function was applied (non-NaN values) + for i, reward_func_name in enumerate(self.reward_func_names): + mean_rewards = torch.nanmean(rewards_per_func[:, i]).item() + self._metrics[mode][f"rewards/{reward_func_name}/mean"].append(mean_rewards) + std_rewards = nanstd(rewards_per_func[:, i]).item() + self._metrics[mode][f"rewards/{reward_func_name}/std"].append(std_rewards) + self._metrics[mode]["reward"].append(mean_grouped_rewards.mean().item()) + self._metrics[mode]["reward_std"].append(std_grouped_rewards.mean().item()) + + # Log prompt and completion texts + self._textual_logs["prompt"].extend(gather_object(prompts_text)) + self._textual_logs["completion"].extend(gather_object(completions_text)) + for i, name in enumerate(self.reward_func_names): + self._textual_logs["rewards"][name].extend(rewards_per_func[:, i].tolist()) + + return { + "prompt_ids": prompt_ids, + "prompt_mask": prompt_mask, + "completion_ids": completion_ids, + "completion_mask": completion_mask, + "advantages": advantages, + "old_per_token_logps": old_per_token_logps, + "ref_per_token_logps": ref_per_token_logps, + } diff --git a/src/axolotl/core/trainers/mixins/__init__.py b/src/axolotl/core/trainers/mixins/__init__.py index 6e4b3e4d03..44751b4653 100644 --- a/src/axolotl/core/trainers/mixins/__init__.py +++ b/src/axolotl/core/trainers/mixins/__init__.py @@ -6,4 +6,4 @@ from .optimizer import OptimizerMixin from .rng_state_loader import RngLoaderMixin from .scheduler import SchedulerMixin -from .sequence_parallel import SequenceParallelContextManager, SequenceParallelMixin +from .sequence_parallel import SequenceParallelMixin diff --git a/src/axolotl/core/trainers/mixins/sequence_parallel.py b/src/axolotl/core/trainers/mixins/sequence_parallel.py index 362acb88e0..0f30458cdc 100644 --- a/src/axolotl/core/trainers/mixins/sequence_parallel.py +++ b/src/axolotl/core/trainers/mixins/sequence_parallel.py @@ -1,85 +1,13 @@ -""" -Module for Axolotl trainer sequence parallelism mixin and training context manager -""" +"""Module for Axolotl trainer sequence parallelism mixin""" -import functools -import logging - -import torch import torch.distributed as dist from datasets import Dataset -from torch import nn from torch.utils.data import DistributedSampler, Sampler -from torch.utils.hooks import RemovableHandle from axolotl.monkeypatch.attention.ring_attn import ( - RingAttnFunc, get_ring_attn_group, - update_ring_attn_params, ) -LOG = logging.getLogger(__name__) - - -def apply_sequence_parallelism( - batch: dict[str, torch.Tensor], - local_rank: int, - local_world_size: int, - ring_attn_func: RingAttnFunc, -) -> dict[str, torch.Tensor]: - """ - Apply sequence parallelism slicing to a batch. - - Args: - batch: Batch dictionary (e.g., input_ids, attention_mask, etc.) - local_rank: Local rank in the sequence parallel group - local_world_size: World size of the sequence parallel group - ring_attn_func: The ring attention function to use - - Returns: - Sliced batch dictionary. - """ - # Update ring attention params if needed - if batch.get("position_ids") is not None: - update_ring_attn_params(position_ids=batch["position_ids"]) - - # Slice batch for sequence parallel processing - total_seq_len = batch["input_ids"].size(1) - for key in batch: - if ( - key in batch - and isinstance(batch[key], torch.Tensor) - and batch[key].dim() > 1 - and batch[key].size(1) == total_seq_len - ): - - if ring_attn_func in [ - RingAttnFunc.VARLEN_LLAMA3, - RingAttnFunc.BATCH_RING, - ]: - # Split in sequential fashion and grab this rank's chunk - batch[key] = ( - batch[key].chunk(local_world_size, dim=1)[local_rank].contiguous() - ) - elif ring_attn_func is RingAttnFunc.BATCH_ZIGZAG: - chunks = batch[key].chunk(2 * local_world_size, dim=1) - - # Take rank's chunk and opposing chunk for zigzag pattern - selected_chunks = [ - chunks[local_rank], - chunks[2 * local_world_size - local_rank - 1], - ] - batch[key] = torch.cat(selected_chunks, dim=1).contiguous() - elif ring_attn_func is RingAttnFunc.BATCH_STRIPE: - # Split into striped data and stack - tensor = torch.stack( - batch[key].split(local_world_size, dim=1), - dim=1, - ).transpose(1, 2) - batch[key] = tensor[:, local_rank].contiguous() - - return batch - class SequenceParallelMixin: """ @@ -157,157 +85,3 @@ def _sp_get_eval_sampler(self, eval_dataset) -> Sampler | None: return self._create_sequence_parallel_sampler( eval_dataset, shuffle=False, is_eval=True ) - - -class SequenceParallelContextManager: - """ - Context manager for sequence parallelism operations. - - This class provides a context that will automatically apply sequence parallelism - during model forward passes using a pre-forward hook, and gather outputs from - across the sequence parallelism group using a post-forward hook. - """ - - def __init__( - self, - model: nn.Module, - sequence_parallel_degree: int, - ring_attn_func: RingAttnFunc, - ): - self.model = model - self.sequence_parallel_degree = sequence_parallel_degree - self.ring_attn_func = ring_attn_func - self.process_group = get_ring_attn_group() - - # Initialize sequence parallel group details - self.local_rank = dist.get_rank(self.process_group) - self.local_world_size = dist.get_world_size(self.process_group) - - # Will store hook handles for removal - self.hook_handles: list[RemovableHandle] = [] - - # Create a partially applied version of the apply_sequence_parallelism function - # with pre-configured params - self.apply_sequence_parallelism = functools.partial( - apply_sequence_parallelism, - local_rank=self.local_rank, - local_world_size=self.local_world_size, - ring_attn_func=self.ring_attn_func, - ) - - def __enter__(self): - # Forward pre-hook to apply sequence parallelism - def sequence_parallel_pre_hook(_, args, kwargs): - # Apply sequence parallelism to kwargs - kwargs = self.apply_sequence_parallelism(batch=kwargs) - return args, kwargs - - # Forward post-hook to gather outputs - def sequence_parallel_post_hook(_, __, output): - # Gather the sharded outputs - return self.gather_outputs(output) - - # Register both hooks - self.hook_handles.append( - self.model.register_forward_pre_hook( - sequence_parallel_pre_hook, with_kwargs=True - ) - ) - self.hook_handles.append( - self.model.register_forward_hook(sequence_parallel_post_hook) - ) - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - # Remove all hooks - for handle in self.hook_handles: - handle.remove() - self.hook_handles = [] - - def gather_outputs(self, output): - """Gather sharded outputs from all ranks and reconstruct the full tensor.""" - # Handle different output formats (dict, tensor, etc.) - if isinstance(output, dict): - gathered_output = {} - for key, value in output.items(): - if isinstance(value, torch.Tensor) and value.dim() > 1: - # Gather logits or other sequence-sharded tensors - gathered_value = self.gather_tensor(value) - gathered_output[key] = gathered_value - else: - gathered_value = value.clone() - dist.all_reduce( - gathered_value, op=dist.ReduceOp.SUM, group=self.process_group - ) - gathered_output[key] = gathered_value - return gathered_output - if isinstance(output, torch.Tensor): - return self.gather_tensor(output) - - return output - - def gather_tensor(self, tensor): - """Gather a sharded tensor from all ranks.""" - # Prepare tensors for all_gather - world_size = self.local_world_size - - # Create list to store tensors from all ranks - gathered_tensors = [torch.zeros_like(tensor) for _ in range(world_size)] - - # All-gather operation - dist.all_gather(gathered_tensors, tensor, group=self.process_group) - - # Concatenate along sequence dimension (typically dim=1) - if self.ring_attn_func in [RingAttnFunc.VARLEN_LLAMA3, RingAttnFunc.BATCH_RING]: - # Simple concatenation for standard sharding - return torch.cat(gathered_tensors, dim=1) - - if self.ring_attn_func is RingAttnFunc.BATCH_ZIGZAG: - # Each rank has a pattern of (rank, world_size*2-rank-1) - reconstituted_tensors = [None] * (world_size * 2) - - # First, split each gathered tensor into its two chunks - for rank, gathered_tensor in enumerate(gathered_tensors): - # Each tensor contains two chunks in the sequence dimension - chunk_size = gathered_tensor.size(1) // 2 - chunk1, chunk2 = gathered_tensor.split(chunk_size, dim=1) - - # Place chunks in their original positions - reconstituted_tensors[rank] = chunk1 - reconstituted_tensors[world_size * 2 - rank - 1] = chunk2 - - # Concatenate the reconstituted tensors in the correct order - return torch.cat(reconstituted_tensors, dim=1) - - # Otherwise, RingAttnFunc.BATCH_STRIPE - # In striping, each rank has every world_size-th slice - batch_size = tensor.size(0) - hidden_dim = tensor.size(-1) - - # First, determine the full sequence length - total_seq_len = 0 - for t in gathered_tensors: - total_seq_len += t.size(1) - - # Create a tensor to hold the unstriped result - result = torch.zeros( - batch_size, - total_seq_len, - hidden_dim, - dtype=tensor.dtype, - device=tensor.device, - ) - - # For each rank's tensor, distribute its slices to the correct positions - for rank, gathered_tensor in enumerate(gathered_tensors): - # The rank's tensor contains every world_size-th slice - # starting from its rank position - seq_len = gathered_tensor.size(1) - for i in range(seq_len): - # Calculate the position in the full tensor - pos = i * world_size + rank - if pos < total_seq_len: - result[:, pos] = gathered_tensor[:, i] - - return result diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index 3fe32f5074..0b14e76612 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -9,7 +9,7 @@ from transformers import TrainingArguments from trl import CPOConfig, KTOConfig, ORPOConfig, PRMConfig, RewardConfig -from axolotl.monkeypatch.attention.ring_attn.patch import RingAttnFunc +from axolotl.utils.schemas.enums import RingAttnFunc @dataclass diff --git a/src/axolotl/monkeypatch/attention/ring_attn/__init__.py b/src/axolotl/monkeypatch/attention/ring_attn/__init__.py index 055607e928..a50ad456ea 100644 --- a/src/axolotl/monkeypatch/attention/ring_attn/__init__.py +++ b/src/axolotl/monkeypatch/attention/ring_attn/__init__.py @@ -4,7 +4,6 @@ # flake8: noqa from .patch import ( - RingAttnFunc, get_ring_attn_group, register_ring_attn, set_ring_attn_group, diff --git a/src/axolotl/monkeypatch/attention/ring_attn/adapters/batch.py b/src/axolotl/monkeypatch/attention/ring_attn/adapters/batch.py index a88c9f6f17..e556ba5e33 100644 --- a/src/axolotl/monkeypatch/attention/ring_attn/adapters/batch.py +++ b/src/axolotl/monkeypatch/attention/ring_attn/adapters/batch.py @@ -16,11 +16,7 @@ import torch.distributed as dist import transformers import transformers.modeling_flash_attention_utils -from ring_flash_attn import ( - ring_flash_attn_func, - stripe_flash_attn_func, - zigzag_ring_flash_attn_func, -) +from ring_flash_attn import ring_flash_attn_func from ring_flash_attn.adapters.hf_adapter import check_params from transformers.modeling_flash_attention_utils import ( _flash_supports_window_size, @@ -28,12 +24,12 @@ ) from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS -from axolotl.monkeypatch.attention.ring_attn.patch import RingAttnFunc +from axolotl.utils.schemas.enums import RingAttnFunc RING_ATTN_FUNC_MAPPING = { - RingAttnFunc.BATCH_RING: ring_flash_attn_func, - RingAttnFunc.BATCH_ZIGZAG: zigzag_ring_flash_attn_func, - RingAttnFunc.BATCH_STRIPE: stripe_flash_attn_func, + RingAttnFunc.BATCH_RING: torch.compile(ring_flash_attn_func), + # RingAttnFunc.BATCH_ZIGZAG: torch.compile(zigzag_ring_flash_attn_func), + # RingAttnFunc.BATCH_STRIPE: torch.compile(stripe_flash_attn_func), } diff --git a/src/axolotl/monkeypatch/attention/ring_attn/patch.py b/src/axolotl/monkeypatch/attention/ring_attn/patch.py index fa03bd1741..8cbba338a1 100644 --- a/src/axolotl/monkeypatch/attention/ring_attn/patch.py +++ b/src/axolotl/monkeypatch/attention/ring_attn/patch.py @@ -6,13 +6,12 @@ their sequence parallel version of Flash Attention 2. """ -from enum import Enum - import torch import torch.distributed as dist from accelerate.logging import get_logger from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids +from axolotl.utils.schemas.enums import RingAttnFunc LOG = get_logger(__name__) @@ -41,17 +40,6 @@ def set_ring_attn_group(ring_attn_group: dist.ProcessGroup | None): RING_ATTN_GROUP = ring_attn_group -class RingAttnFunc(str, Enum): - """Enum class for supported `ring-flash-attn` implementations""" - - # VARLEN_RING = "varlen_ring" - # VARLEN_ZIGZAG = "varlen_zigzag" - VARLEN_LLAMA3 = "varlen_llama3" - BATCH_RING = "batch_ring" - BATCH_ZIGZAG = "batch_zigzag" - BATCH_STRIPE = "batch_stripe" - - def register_ring_attn( sequence_parallel_degree: int, heads_k_stride: int | None, @@ -117,11 +105,7 @@ def register_ring_attn( substitute_hf_flash_attn( process_group=get_ring_attn_group(), heads_k_stride=heads_k_stride or 1 ) - elif ring_attn_func in [ - RingAttnFunc.BATCH_RING, - RingAttnFunc.BATCH_ZIGZAG, - RingAttnFunc.BATCH_STRIPE, - ]: + elif ring_attn_func is RingAttnFunc.BATCH_RING: from axolotl.monkeypatch.attention.ring_attn.adapters.batch import ( substitute_hf_flash_attn, ) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 2adf28fdf5..90ab10e9f9 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -7,7 +7,7 @@ import signal import sys import weakref -from contextlib import nullcontext +from contextlib import ExitStack from pathlib import Path from typing import Any, Dict @@ -27,14 +27,13 @@ fix_untrained_tokens, ) from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder -from axolotl.core.trainers.mixins.sequence_parallel import ( - SequenceParallelContextManager, -) from axolotl.integrations.base import PluginManager +from axolotl.utils.ctx_managers.sequence_parallel import SequenceParallelContextManager from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import cleanup_distributed from axolotl.utils.freeze import freeze_layers_except from axolotl.utils.models import load_model, load_processor, load_tokenizer +from axolotl.utils.schemas.enums import RLType from axolotl.utils.trainer import setup_trainer try: @@ -107,7 +106,7 @@ def setup_reference_model( Reference model if needed for RL training, `None` otherwise. """ model_ref = None - if cfg.rl and cfg.rl != "orpo": + if cfg.rl and cfg.rl != RLType.ORPO: if cfg.adapter and not cfg.rl_adapter_ref_model: # use built-in trl autounwrap LOG.debug("Passing model_ref: None to RL trainer") @@ -188,28 +187,32 @@ def execute_training( trainer: The configured trainer object. resume_from_checkpoint: Path to checkpoint to resume from, if applicable. """ - # Define the context managers to use - flash_context = ( - torch.backends.cuda.sdp_kernel( - enable_flash=True, - enable_math=True, - enable_mem_efficient=True, - ) - if cfg.flash_optimum - else nullcontext() - ) - sequence_parallel_context = ( - SequenceParallelContextManager( - model=trainer.model, - sequence_parallel_degree=cfg.sequence_parallel_degree, - ring_attn_func=cfg.ring_attn_func, - ) - if cfg.sequence_parallel_degree > 1 - else nullcontext() - ) + with ExitStack() as stack: + # Define the context managers to use + if cfg.flash_optimum: + stack.enter_context( + torch.backends.cuda.sdp_kernel( + enable_flash=True, + enable_math=True, + enable_mem_efficient=True, + ) + ) + + if cfg.sequence_parallel_degree > 1: + models = [trainer.model] + if hasattr(trainer, "ref_model"): + models.append(trainer.ref_model) + + stack.enter_context( + SequenceParallelContextManager( + models=models, + sequence_parallel_degree=cfg.sequence_parallel_degree, + gradient_accumulation_steps=cfg.gradient_accumulation_steps, + ring_attn_func=cfg.ring_attn_func, + ) + ) - LOG.info("Starting trainer...") - with flash_context, sequence_parallel_context: + LOG.info("Starting trainer...") trainer.train(resume_from_checkpoint=resume_from_checkpoint) diff --git a/src/axolotl/utils/ctx_managers/__init__.py b/src/axolotl/utils/ctx_managers/__init__.py new file mode 100644 index 0000000000..e544621b58 --- /dev/null +++ b/src/axolotl/utils/ctx_managers/__init__.py @@ -0,0 +1,6 @@ +"""Init for context manager submodule""" + +# pylint: disable=unused-import +# flake8: noqa + +from .sequence_parallel import SequenceParallelContextManager diff --git a/src/axolotl/utils/ctx_managers/sequence_parallel.py b/src/axolotl/utils/ctx_managers/sequence_parallel.py new file mode 100644 index 0000000000..66044f7f03 --- /dev/null +++ b/src/axolotl/utils/ctx_managers/sequence_parallel.py @@ -0,0 +1,335 @@ +"""Module for Axolotl trainer sequence parallelism manager and utilities""" + +import functools + +import torch +import torch.distributed as dist +from torch import nn +from torch.utils.hooks import RemovableHandle +from transformers.modeling_outputs import CausalLMOutputWithPast +from transformers.utils import ModelOutput + +from axolotl.monkeypatch.attention.ring_attn.patch import ( + get_ring_attn_group, + update_ring_attn_params, +) +from axolotl.utils.schemas.enums import RingAttnFunc + + +# TODO(djsaunde): implement zigzag, stripe patterns here (and elsewhere) in this +# module. Currently, we just focus on batch ring and varlen llama3 for simplicity. +def apply_sequence_parallelism( + batch: dict[str, torch.Tensor], + local_rank: int, + local_world_size: int, + gradient_accumulation_steps: int, + ring_attn_func: RingAttnFunc, # pylint: disable=unused-argument +) -> tuple[dict[str, torch.Tensor], int, int]: + """ + Apply sequence parallelism slicing to a batch. + + Special handling is implemented for integer logits_to_keep, which indicates + to only keep the last N tokens in the sequence during generation. + + Args: + batch: Batch dictionary (e.g., input_ids, attention_mask, etc.). + local_rank: Local rank in the sequence parallel group. + local_world_size: World size of the sequence parallel group. + gradient_accumulation_steps: Number of steps to accumulate gradients over. + ring_attn_func: Which ring attention function to use. Currently unused, but + related to above TODO. + + Returns: + tuple of: + - Batch dictionary with sliced tensors. + - The original sequence length before padding. + - The number of padding tokens added. + """ + original_seq_len = batch["input_ids"].size(1) + + # Update ring attention params if needed + if batch.get("position_ids") is not None: + update_ring_attn_params(position_ids=batch["position_ids"]) + else: + # If position_ids aren't already in the batch, create them + batch["position_ids"] = torch.arange( + 0, + original_seq_len, + dtype=torch.long, + device=batch["input_ids"].device, + ).expand(batch["input_ids"].size(0), -1) + + if "logits_to_keep" in batch and isinstance(batch["logits_to_keep"], int): + logits_to_keep = batch["logits_to_keep"] + + # Calculate which positions in the full sequence contain the last N tokens + start_position = max(0, original_seq_len - logits_to_keep) + chunk_size = original_seq_len // local_world_size + rank_start = local_rank * chunk_size + rank_end = rank_start + chunk_size + + # Create a boolean mask tensor for this rank's chunk + mask = torch.zeros( + chunk_size, + dtype=torch.bool, + device=batch["input_ids"].device, + ) + + if rank_end > start_position: + # Calculate how many of the last N tokens fall within this rank's range + tokens_in_rank = min(rank_end, original_seq_len) - max( + rank_start, start_position + ) + + # Calculate where these tokens start in the local chunk + local_start_idx = max(0, start_position - rank_start) + + # Set the appropriate positions in the mask to True + mask[local_start_idx : local_start_idx + tokens_in_rank] = True + + # Replace the integer with the boolean mask + batch["logits_to_keep"] = mask + + # Add padding to make sequence length divisible by local_world_size + total_seq_len = original_seq_len + pad_len = 0 + divisor = min(local_world_size, 64) + if total_seq_len % divisor != 0: + pad_len = divisor - (total_seq_len % divisor) + + # Apply padding to all relevant tensors + for key in batch: + if ( + isinstance(batch[key], torch.Tensor) + and batch[key].dim() > 1 + and batch[key].size(1) == total_seq_len + ): + # Create padding tensor + pad_value = -100 if key == "labels" else 0 + padding = torch.full( + (batch[key].size(0), pad_len, *batch[key].shape[2:]), + pad_value, + dtype=batch[key].dtype, + device=batch[key].device, + ) + + # Concatenate padding to the right side of the tensor + batch[key] = torch.cat([batch[key], padding], dim=1) + if key == "logits_to_keep": + # Create padding tensor + padding = torch.ones( + 1, + dtype=batch[key].dtype, + device=batch[key].device, + ) + + # Concatenate padding to the right side of the tensor + batch[key] = torch.cat([batch[key], padding], dim=0) + + # Update the total sequence length after padding + total_seq_len = batch["input_ids"].size(1) + + # Slice batch for sequence parallel + for key in batch: + if not isinstance(batch[key], torch.Tensor) or batch[key].dim() <= 1: + continue + + # Split in sequential fashion and grab this rank's chunk + if batch[key].size(1) == total_seq_len: + batch[key] = ( + batch[key].chunk(local_world_size, dim=1)[local_rank].contiguous() + ) + elif key == "logits_to_keep": + batch[key] = ( + batch[key].chunk(local_world_size, dim=0)[local_rank].contiguous() + ) + + # Handle num_items_in_batch + if "num_items_in_batch" in batch: + # Approximation; this needed since num_items_in_batch may be counted across + # all samples in a gradient accumulated batch, not on a per-step basis. + batch["num_items_in_batch"] = ( + batch["labels"] != -100 + ).sum() * gradient_accumulation_steps + + return batch, original_seq_len, pad_len + + +class SequenceParallelContextManager: + """Context manager for sequence parallelism operations. + + This class provides a context that will automatically apply sequence parallelism + during model forward passes using a pre-forward hook, and gather outputs from + across the sequence parallelism group using a post-forward hook. + + Args: + models: List of models to apply sequence parallelism to pre- and post- forward + hooks. + sequence_parallel_degree: Number of processes to split sequences over. + gradient_accumulation_steps: Number of steps to accumulate gradients over. + ring_attn_func: Which ring attention function to use. Currently unused. + """ + + def __init__( + self, + models: list[nn.Module], + sequence_parallel_degree: int, + gradient_accumulation_steps: int, + ring_attn_func: RingAttnFunc, + ): + self.models = models + self.sequence_parallel_degree = sequence_parallel_degree + self.gradient_accumulation_steps = gradient_accumulation_steps + self.ring_attn_func = ring_attn_func + self.process_group = get_ring_attn_group() + + # Initialize sequence parallel group details + self.local_rank = dist.get_rank(self.process_group) + self.local_world_size = dist.get_world_size(self.process_group) + + # Will store hook handles for removal + self.hook_handles: list[RemovableHandle] = [] + + # Store original sequence length and padding information + self.original_seq_len = 0 + self.pad_len = 0 + + # Create a partially applied version of the apply_sequence_parallelism function + self.apply_sequence_parallelism = functools.partial( + apply_sequence_parallelism, + local_rank=self.local_rank, + local_world_size=self.local_world_size, + gradient_accumulation_steps=self.gradient_accumulation_steps, + ring_attn_func=self.ring_attn_func, + ) + + def __enter__(self): + # Forward pre-hook to apply sequence parallelism + def sequence_parallel_pre_hook(_, args, kwargs): + # Apply sequence parallelism to kwargs and get original sequence length and padding info + kwargs, self.original_seq_len, self.pad_len = ( + self.apply_sequence_parallelism(batch=kwargs) + ) + + return args, kwargs + + # Forward post-hook to gather outputs + def sequence_parallel_post_hook(_, __, output: ModelOutput) -> ModelOutput: + # Gather the sharded outputs + output = self.gather_outputs(output) + + # Remove padding if it was added + if self.pad_len > 0: + for key, value in output.items(): + if isinstance(value, torch.Tensor) and value.dim() > 1: + if value.size(1) == self.original_seq_len + self.pad_len: + # Slice to remove padding + output[key] = value[:, : self.original_seq_len].contiguous() + + return output + + # Register both hooks + for model in self.models: + self.hook_handles.append( + model.register_forward_pre_hook( + sequence_parallel_pre_hook, with_kwargs=True + ) + ) + self.hook_handles.append( + model.register_forward_hook(sequence_parallel_post_hook) + ) + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # Remove all hooks + for handle in self.hook_handles: + handle.remove() + self.hook_handles = [] + + def gather_outputs(self, output: CausalLMOutputWithPast) -> CausalLMOutputWithPast: + """Gather sharded outputs from all ranks and reconstruct the full tensor.""" + for key, value in output.items(): + if isinstance(value, torch.Tensor) and value.dim() > 1: + output[key] = AllGatherWithGrad.apply(value, self.process_group) + + return output + + +class AllGatherWithGrad(torch.autograd.Function): + """Custom autograd function for all-gather to preserve gradients.""" + + @staticmethod + def forward( + ctx: torch.autograd.function.FunctionCtx, + input_tensor: torch.Tensor, + group: dist.ProcessGroup, + ) -> torch.Tensor: + """ + Forward pass of all-gather of data with sequence dimension. + + Args: + ctx: `torch.autograd` function context. + input_tensor: Tensor from model output with sequence dimension. + group: `torch.distributed` process group. + + Returns: + Tensor from gathering the `input_tensor` from across the process group and + concatenating along the sequence dimension. + """ + ctx.group = group + ctx.rank = dist.get_rank(group) + world_size = dist.get_world_size(group) + + # Gather shape metadata + local_shape = torch.tensor(list(input_tensor.shape), device=input_tensor.device) + all_shapes = [torch.zeros_like(local_shape) for _ in range(world_size)] + dist.all_gather(all_shapes, local_shape, group=group) + + # Store sequence lengths for backward pass + seq_lens = [int(shape[1].item()) for shape in all_shapes] + ctx.seq_lens = seq_lens + + # Perform all_gather operation + gathered = [ + torch.zeros( + tuple(shape.tolist()), + dtype=input_tensor.dtype, + device=input_tensor.device, + ) + for shape in all_shapes + ] + dist.all_gather(gathered, input_tensor, group=group) + + # Concatenate tensors along sequence dimension + result = torch.cat(gathered, dim=1) + + return result + + @staticmethod + def backward( + ctx: torch.autograd.function.FunctionCtx, grad_output: torch.Tensor + ) -> tuple[torch.Tensor, None]: + """ + Backward pass for all-gather operation. + + Extracts the gradient slice corresponding to this rank's original input + from the full gradient tensor. + + Args: + ctx: `torch.autograd` function context. + grad_output: Gradient from subsequent layers with respect to the + concatenated output tensor. + + Returns: + Tuple containing the gradient slice for this rank's input tensor and `None` + for the process group parameter which doesn't require gradients. + """ + rank = ctx.rank + seq_lens = ctx.seq_lens + + # Extract gradient for this rank's chunk + offset = sum(seq_lens[:rank]) + grad_slice = grad_output[:, offset : offset + seq_lens[rank]].contiguous() + + return grad_slice, None diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index 135de61a30..eaa8348226 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -18,8 +18,9 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import is_main_process, zero_first from axolotl.utils.models import load_tokenizer +from axolotl.utils.schemas.enums import RLType -LOG = logging.getLogger("axolotl") +LOG = logging.getLogger(__name__) def _get_path(ds_hash, cfg): @@ -80,7 +81,7 @@ def map_dataset(cfg, data_set, ds_transform_fn, tokenizer, **map_kwargs): def drop_long_rl_seq( sample, rl, tokenizer, sequence_len # pylint: disable=invalid-name ): - if rl in ("dpo", "ipo", "orpo", "simpo"): + if rl in (RLType.DPO, RLType.IPO, RLType.ORPO, RLType.SIMPO): if not ( sample.get("prompt") and sample.get("chosen") and sample.get("rejected") ): @@ -100,7 +101,7 @@ def drop_long_rl_seq( len_prompt + len_rejected ) <= sequence_len - if rl == "kto": + if rl is RLType.KTO: if not (sample.get("prompt") and sample.get("completion")): raise ValueError("Prompt and completion keys are required for KTO datasets") @@ -114,7 +115,7 @@ def drop_long_rl_seq( return (len_prompt + len_completion) <= sequence_len - if rl == "grpo": + if rl is RLType.GRPO: return True raise ValueError("Unknown RL type") @@ -137,9 +138,9 @@ def load_split(dataset_cfgs, _cfg): if _type: if isinstance(_type, DictDefault): _type = "user_defined.default" - if _cfg.rl == "orpo": + if _cfg.rl is RLType.ORPO: ds_transform_fn = load_orpo(_type, _cfg, dataset_idx=i) - elif _cfg.rl == "kto": + elif _cfg.rl is RLType.KTO: ds_transform_fn = load_kto(_type, _cfg, dataset_idx=i) else: ds_transform_fn = load_dpo(_type, _cfg, dataset_idx=i) @@ -150,7 +151,7 @@ def load_split(dataset_cfgs, _cfg): split_datasets[i] = map_dataset( cfg, data_set, ds_transform_fn, tokenizer, **map_kwargs ) - elif _cfg.rl == "kto": + elif _cfg.rl is RLType.KTO: ds_transform_fn = load_kto(_type, _cfg, dataset_idx=i) map_kwargs = {} if isinstance(ds_transform_fn, tuple): @@ -185,7 +186,7 @@ def load_split(dataset_cfgs, _cfg): ) combined_datasets = concatenate_datasets(split_datasets) - combined_datasets = combined_datasets.shuffle(seed=cfg.seed) + combined_datasets = combined_datasets.shuffle(seed=cfg.seed or 42) return combined_datasets @@ -205,6 +206,8 @@ def load_split(dataset_cfgs, _cfg): eval_dataset = load_split(cfg.test_datasets, cfg) if not eval_dataset: if cfg.val_set_size: + seed = cfg.seed if cfg.seed is not None else 42 + # ensure we end up with the same fingerprint by doing rank0 first and being able to cache to_hash_train = ( train_dataset._fingerprint # pylint: disable=protected-access @@ -213,7 +216,7 @@ def load_split(dataset_cfgs, _cfg): + "|" + "train" + "|" - + str(cfg.seed or 42) + + str(seed) ) to_hash_test = ( train_dataset._fingerprint # pylint: disable=protected-access @@ -222,13 +225,13 @@ def load_split(dataset_cfgs, _cfg): + "|" + "test" + "|" - + str(cfg.seed or 42) + + str(seed) ) train_fingerprint = md5(to_hash_train) test_fingerprint = md5(to_hash_test) ds_w_test_split = train_dataset.train_test_split( test_size=cfg.val_set_size, - seed=cfg.seed, + seed=seed, shuffle=False, train_new_fingerprint=train_fingerprint, test_new_fingerprint=test_fingerprint, diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 12f0701f06..5fa0cb60d6 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -148,7 +148,7 @@ def prepare_dataset(cfg, tokenizer, processor=None, preprocess_iterable=None): ds_wrapper_partial, max_tokens=cfg.sequence_len, batch_size=cfg.micro_batch_size, - seed=cfg.seed or 42, + seed=cfg.seed if cfg.seed is not None else 42, buffer_size=cfg.pretrain_multipack_buffer_size or 10_000, ) # https://discuss.huggingface.co/t/how-to-use-huggingface-trainer-streaming-datasets-without-wrapping-it-with-torchdatas-iterablewrapper/25230 @@ -416,6 +416,8 @@ def load_prepare_datasets( ) if split == "train" and val_set_size: + seed = cfg.seed if cfg.seed is not None else 42 + # ensure we end up with the same fingerprint by doing rank0 first and being able to cache to_hash_train = ( dataset._fingerprint # pylint: disable=protected-access @@ -424,7 +426,7 @@ def load_prepare_datasets( + "|" + "train" + "|" - + str(cfg.seed or 42) + + str(seed) ) to_hash_test = ( dataset._fingerprint # pylint: disable=protected-access @@ -433,7 +435,7 @@ def load_prepare_datasets( + "|" + "test" + "|" - + str(cfg.seed or 42) + + str(seed) ) train_fingerprint = md5(to_hash_train) test_fingerprint = md5(to_hash_test) @@ -442,7 +444,7 @@ def load_prepare_datasets( dataset = dataset.train_test_split( test_size=val_set_size, shuffle=False, - seed=cfg.seed or 42, + seed=seed, train_new_fingerprint=train_fingerprint, test_new_fingerprint=test_fingerprint, ) diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 6aa4dd162a..dff6d854b3 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -73,6 +73,7 @@ from axolotl.utils.gradient_checkpointing import hf_grad_checkpoint_offload_wrapper from axolotl.utils.lora_embeddings import get_linear_embedding_layers from axolotl.utils.model_shard_quant import load_sharded_model, load_sharded_model_quant +from axolotl.utils.schemas.enums import RLType LOG = logging.getLogger(__name__) PLUGIN_MANAGER = PluginManager.get_instance() @@ -1372,7 +1373,7 @@ def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: # then the dpo trainer doesn't want the peft model loaded over it, it just wants the lora/peft config if ( self.cfg.adapter - and self.cfg.rl in ["dpo", "ipo", "kto"] + and self.cfg.rl in [RLType.DPO, RLType.IPO, RLType.KTO] and not self.cfg.merge_lora ): _, lora_config = load_lora( diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index cd9891e04d..34f084f100 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -27,7 +27,7 @@ StepwiseSupervisedDataset, ) from axolotl.utils.schemas.deprecated import DeprecatedParameters, RemappedParameters -from axolotl.utils.schemas.enums import ChatTemplate, RLType +from axolotl.utils.schemas.enums import ChatTemplate, RingAttnFunc, RLType from axolotl.utils.schemas.integrations import ( CometConfig, GradioConfig, @@ -260,7 +260,7 @@ class AxolotlInputConfig( sequence_parallel_degree: int | None = None heads_k_stride: int | None = None - ring_attn_func: str | None = None + ring_attn_func: RingAttnFunc | None = None special_tokens: SpecialTokensConfig | None = None tokens: list[str] | None = None @@ -782,7 +782,7 @@ def check_rl_beta(self): @model_validator(mode="after") def check_simpo_warmup(self): - if self.rl == "simpo" and self.warmup_ratio: + if self.rl is RLType.SIMPO and self.warmup_ratio: raise ValueError( "warmup_ratio is not supported with the simpo trainer. Please use `warmup_steps` instead" ) @@ -1161,6 +1161,18 @@ def check_grpo_peft_liger(cls, data): raise ValueError("PEFT + GRPO + Liger is not yet supported") return data + @model_validator(mode="before") + @classmethod + def check_grpo_liger_sequence_parallel(cls, data): + if ( + data.get("rl") == "grpo" + and data.get("trl", {}) + and data.get("trl").get("use_liger_loss") + and data.get("sequence_parallel_degree", 1) > 1 + ): + raise ValueError("GRPO + SP + Liger not currently supported") + return data + @model_validator(mode="after") def check_sequence_parallel_degree(self): if not self.sequence_parallel_degree: @@ -1173,7 +1185,7 @@ def check_sequence_parallel_degree(self): if self.sample_packing and self.micro_batch_size > 1: raise ValueError( - "micro_batch_size must be set to 1 when sample_packing is enabled" + "micro_batch_size must be set to 1 when sample_packing is enabled " "due to a `ring-flash-attn` requirement" ) @@ -1205,16 +1217,8 @@ def validate_ring_attn_func(self): if getattr(self, "sequence_parallel_degree", 1) == 1: return self - from axolotl.monkeypatch.attention.ring_attn.patch import RingAttnFunc - if self.ring_attn_func is not None: - valid_funcs = list(RingAttnFunc) - if self.ring_attn_func in valid_funcs: - self.ring_attn_func = RingAttnFunc(self.ring_attn_func) - else: - raise ValueError( - f"ring_attn_func: {self.ring_attn_func} must be in {valid_funcs}" - ) + self.ring_attn_func = RingAttnFunc(self.ring_attn_func) else: # Default ring attention function selection sample_packing = getattr(self, "sample_packing", False) diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index fe5cf62bab..ff8471dfd5 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -6,12 +6,12 @@ class RLType(str, Enum): """RL trainer type configuration subset""" - dpo = "dpo" # pylint: disable=invalid-name - grpo = "grpo" # pylint: disable=invalid-name - ipo = "ipo" # pylint: disable=invalid-name - orpo = "orpo" # pylint: disable=invalid-name - kto = "kto" # pylint: disable=invalid-name - simpo = "simpo" # pylint: disable=invalid-name + DPO = "dpo" # pylint: disable=invalid-name + GRPO = "grpo" # pylint: disable=invalid-name + IPO = "ipo" # pylint: disable=invalid-name + ORPO = "orpo" # pylint: disable=invalid-name + KTO = "kto" # pylint: disable=invalid-name + SIMPO = "simpo" # pylint: disable=invalid-name class ChatTemplate(str, Enum): @@ -55,3 +55,14 @@ class CustomSupportedOptimizers(str, Enum): adopt_adamw = "adopt_adamw" # pylint: disable=invalid-name came_pytorch = "came_pytorch" # pylint: disable=invalid-name muon = "muon" # pylint: disable=invalid-name + + +class RingAttnFunc(str, Enum): + """Enum class for supported `ring-flash-attn` implementations""" + + # VARLEN_RING = "varlen_ring" + # VARLEN_ZIGZAG = "varlen_zigzag" + VARLEN_LLAMA3 = "varlen_llama3" + BATCH_RING = "batch_ring" + # BATCH_ZIGZAG = "batch_zigzag" + # BATCH_STRIPE = "batch_stripe" diff --git a/tests/e2e/multigpu/patched/test_sp.py b/tests/e2e/multigpu/patched/test_sp.py index 1667408f49..1170f5eee8 100644 --- a/tests/e2e/multigpu/patched/test_sp.py +++ b/tests/e2e/multigpu/patched/test_sp.py @@ -25,6 +25,7 @@ def _run_sequence_parallel_test( micro_batch_size=1, pad_to_sequence_len=True, ring_attn_func=None, + threshold=2.0, ): """Helper method to run sequence parallel tests with different configurations""" cfg = DictDefault( @@ -93,22 +94,22 @@ def _run_sequence_parallel_test( ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.6, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", threshold, "Train Loss is too high" ) @pytest.mark.parametrize( - "sample_packing, micro_batch_size, pad_to_sequence_len, ring_attn_func", + "sample_packing, micro_batch_size, pad_to_sequence_len, ring_attn_func, threshold", [ - (True, 1, True, None), # defaults to varlen_llama3 ring_attn_func - (False, 2, True, None), # defaults to batch_ring ring_attn_func - (False, 2, True, "batch_zigzag"), - # (False, 2, False), # not yet working + (True, 1, True, None, 2.5), # defaults to varlen_llama3 ring_attn_func + (False, 2, True, None, 2.5), # defaults to batch_ring ring_attn_func + # (False, 2, True, "batch_zigzag", 2.5), + (False, 2, False, None, 2.5), # defaults to batch_ring ring_attn_func ], ids=[ "sample_packing, varlen_llama3 ring_attn_func", + "no sample_packing, pad_to_sequence_len, batch_ring ring_attn_func", + # "no sample_packing, no pad_to_sequence_len, batch_zigzag ring_attn_func", "no sample_packing, no pad_to_sequence_len, batch_ring ring_attn_func", - "no sample_packing, no pad_to_sequence_len, batch_zigzag ring_attn_func", - # "no sample_packing, pad_to_sequence_len", # not yet working ], ) def test_sequence_parallel_training( @@ -118,6 +119,7 @@ def test_sequence_parallel_training( micro_batch_size, pad_to_sequence_len, ring_attn_func, + threshold, ): """Test sequence parallel training with different configurations""" self._run_sequence_parallel_test( @@ -126,4 +128,5 @@ def test_sequence_parallel_training( micro_batch_size=micro_batch_size, pad_to_sequence_len=pad_to_sequence_len, ring_attn_func=ring_attn_func, + threshold=threshold, ) diff --git a/tests/e2e/patched/test_sp.py b/tests/e2e/patched/test_sp.py index 046c482e3e..8efe629409 100644 --- a/tests/e2e/patched/test_sp.py +++ b/tests/e2e/patched/test_sp.py @@ -10,14 +10,15 @@ import torch from accelerate.state import PartialState -from axolotl.core.trainers.mixins.sequence_parallel import apply_sequence_parallelism from axolotl.monkeypatch.attention.ring_attn import ( - RingAttnFunc, get_ring_attn_group, register_ring_attn, set_ring_attn_group, ) +from axolotl.utils.ctx_managers.sequence_parallel import apply_sequence_parallelism from axolotl.utils.dict import DictDefault +from axolotl.utils.schemas.enums import RingAttnFunc +from axolotl.utils.schemas.trl import TRLConfig @pytest.fixture @@ -62,12 +63,14 @@ def sequence_parallel_batch(): input_ids = torch.arange(batch_size * seq_len).reshape(batch_size, seq_len) attention_mask = torch.ones(batch_size, seq_len) position_ids = torch.arange(seq_len).expand(batch_size, seq_len) + labels = input_ids.clone() # Create test batch batch = { "input_ids": input_ids, "attention_mask": attention_mask, "position_ids": position_ids, + "labels": labels, } return batch @@ -179,12 +182,44 @@ def base_cfg(self): False, "micro_batch_size must be set to 1", ), + # Valid: Basic GRPO config + ( + { + "sequence_parallel_degree": 2, + "flash_attention": True, + "micro_batch_size": 2, + "trl": {"use_liger_loss": True}, + }, + { + "sequence_parallel_degree": 2, + "flash_attention": True, + "micro_batch_size": 2, + "trl": TRLConfig(use_liger_loss=True), + }, + True, + "GRPO + SP + Liger not currently supported", + ), + # Invalid: GRPO config with Liger loss + ( + { + "rl": "grpo", + "sequence_parallel_degree": 2, + "flash_attention": True, + "micro_batch_size": 2, + "trl": {"use_liger_loss": True}, + }, + None, + False, + "GRPO + SP + Liger not currently supported", + ), ], ids=[ "valid_config", "default_sp_degree", "without_flash_attention", "sample_packing_with_large_batch", + "valid_grpo", + "grpo_with_liger_loss", ], ) def test_sequence_parallel_config_validation( @@ -256,7 +291,7 @@ def test_invalid_ring_attn_func(self, base_cfg): AxolotlInputConfig(**cfg) # Verify error message - assert "ring_attn_func: INVALID_FUNC must be in" in str(excinfo.value) + assert "Input should be 'varlen_llama3' or 'batch_ring'" in str(excinfo.value) class TestApplySequenceParallelism: @@ -290,10 +325,11 @@ def mock_distributed(self, monkeypatch): def test_world_size_one(self, sequence_parallel_batch): """Test that function returns original batch when world size is 1.""" - result = apply_sequence_parallelism( + result, _, _ = apply_sequence_parallelism( batch=sequence_parallel_batch, local_rank=0, local_world_size=1, + gradient_accumulation_steps=1, ring_attn_func=RingAttnFunc.BATCH_RING, ) @@ -305,10 +341,11 @@ def test_batch_ring_rank0(self, sequence_parallel_batch): batch = sequence_parallel_batch seq_len = batch["input_ids"].size(1) - result = apply_sequence_parallelism( + result, _, _ = apply_sequence_parallelism( batch=batch, local_rank=0, local_world_size=2, + gradient_accumulation_steps=1, ring_attn_func=RingAttnFunc.BATCH_RING, ) @@ -328,57 +365,59 @@ def test_batch_ring_rank1(self, sequence_parallel_batch): seq_len = batch["input_ids"].size(1) original_input_ids = batch["input_ids"].clone() - result = apply_sequence_parallelism( + result, _, _ = apply_sequence_parallelism( batch=batch, local_rank=1, local_world_size=2, + gradient_accumulation_steps=1, ring_attn_func=RingAttnFunc.BATCH_RING, ) # Verify content: rank 1 should get the second half of the sequence assert torch.equal(result["input_ids"], original_input_ids[:, seq_len // 2 :]) - def test_batch_zigzag(self, sequence_parallel_batch): - """Test BATCH_ZIGZAG sharding pattern.""" - batch = sequence_parallel_batch - original_input_ids = batch["input_ids"].clone() - seq_len = batch["input_ids"].size(1) - - # Test rank 0 - result_rank0 = apply_sequence_parallelism( - batch={k: v.clone() for k, v in batch.items()}, - local_rank=0, - local_world_size=2, - ring_attn_func=RingAttnFunc.BATCH_ZIGZAG, - ) - - # Test rank 1 - result_rank1 = apply_sequence_parallelism( - batch={k: v.clone() for k, v in batch.items()}, - local_rank=1, - local_world_size=2, - ring_attn_func=RingAttnFunc.BATCH_ZIGZAG, - ) - - # Checks for both ranks - assert result_rank0["input_ids"].shape[1] == seq_len // 2 - assert result_rank1["input_ids"].shape[1] == seq_len // 2 - - # For a 2-rank system with 8 tokens, check specific zigzag pattern - # Rank 0 should get chunks [0, 1] and [6, 7] - # Rank 1 should get chunks [2, 3] and [4, 5] - if seq_len == 8: - # Create expected tensors for comparison - rank0_expected = torch.cat( - [original_input_ids[:, :2], original_input_ids[:, 6:8]], dim=1 - ) - - rank1_expected = torch.cat( - [original_input_ids[:, 2:4], original_input_ids[:, 4:6]], dim=1 - ) - - assert torch.equal(result_rank0["input_ids"], rank0_expected) - assert torch.equal(result_rank1["input_ids"], rank1_expected) + # TODO(djsaunde): add back once implemented. + # def test_batch_zigzag(self, sequence_parallel_batch): + # """Test BATCH_ZIGZAG sharding pattern.""" + # batch = sequence_parallel_batch + # original_input_ids = batch["input_ids"].clone() + # seq_len = batch["input_ids"].size(1) + + # # Test rank 0 + # result_rank0 = apply_sequence_parallelism( + # batch={k: v.clone() for k, v in batch.items()}, + # local_rank=0, + # local_world_size=2, + # ring_attn_func=RingAttnFunc.BATCH_ZIGZAG, + # ) + + # # Test rank 1 + # result_rank1 = apply_sequence_parallelism( + # batch={k: v.clone() for k, v in batch.items()}, + # local_rank=1, + # local_world_size=2, + # ring_attn_func=RingAttnFunc.BATCH_ZIGZAG, + # ) + + # # Checks for both ranks + # assert result_rank0["input_ids"].shape[1] == seq_len // 2 + # assert result_rank1["input_ids"].shape[1] == seq_len // 2 + + # # For a 2-rank system with 8 tokens, check specific zigzag pattern + # # Rank 0 should get chunks [0, 1] and [6, 7] + # # Rank 1 should get chunks [2, 3] and [4, 5] + # if seq_len == 8: + # # Create expected tensors for comparison + # rank0_expected = torch.cat( + # [original_input_ids[:, :2], original_input_ids[:, 6:8]], dim=1 + # ) + + # rank1_expected = torch.cat( + # [original_input_ids[:, 2:4], original_input_ids[:, 4:6]], dim=1 + # ) + + # assert torch.equal(result_rank0["input_ids"], rank0_expected) + # assert torch.equal(result_rank1["input_ids"], rank1_expected) def test_partial_application(self, sequence_parallel_batch): """Test that we can create a partially applied version of the function.""" @@ -390,11 +429,12 @@ def test_partial_application(self, sequence_parallel_batch): apply_sequence_parallelism, local_rank=0, local_world_size=2, + gradient_accumulation_steps=1, ring_attn_func=RingAttnFunc.BATCH_RING, ) # Use the partially applied function - result = rank0_ring_parallel(batch=batch) + result, _, _ = rank0_ring_parallel(batch=batch) # Verify it works as expected assert result["input_ids"].shape[1] == original_input_ids.shape[1] // 2 @@ -412,13 +452,15 @@ def test_missing_position_ids(self, sequence_parallel_batch): original_input_ids = batch["input_ids"].clone() # This should run without error even though position_ids is missing - result = apply_sequence_parallelism( + result, _, _ = apply_sequence_parallelism( batch=batch, local_rank=0, local_world_size=2, + gradient_accumulation_steps=1, ring_attn_func=RingAttnFunc.BATCH_RING, ) # Verification should pass - assert "position_ids" not in result + assert "position_ids" in result + assert result["input_ids"].shape[1] == result["position_ids"].shape[1] assert result["input_ids"].shape[1] == original_input_ids.shape[1] // 2 From 7fa1089cea4e81d2724dee23dae5ad23dfb10399 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 13 May 2025 08:30:58 -0400 Subject: [PATCH 0650/1405] Atropos support (#2666) [skip ci] * allow peft+liger+grpo and custom vllm serve for atropos support * set trainer class for RL --- src/axolotl/cli/args.py | 6 ++++++ src/axolotl/cli/vllm_serve.py | 4 +++- src/axolotl/core/trainer_builder.py | 4 ++++ src/axolotl/utils/schemas/config.py | 24 ++++++++++++------------ 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py index 83febd7f4c..088e337e46 100644 --- a/src/axolotl/cli/args.py +++ b/src/axolotl/cli/args.py @@ -82,6 +82,12 @@ class VllmServeCliArgs: "hardware support this feature." }, ) + serve_module: Optional[str] = field( + default=None, + metadata={ + "help": "Module to serve. If not set, the default module will be used." + }, + ) @dataclass diff --git a/src/axolotl/cli/vllm_serve.py b/src/axolotl/cli/vllm_serve.py index 552f33e9e0..d3c4ad68d3 100644 --- a/src/axolotl/cli/vllm_serve.py +++ b/src/axolotl/cli/vllm_serve.py @@ -6,7 +6,6 @@ from typing import Union from trl.scripts.vllm_serve import ScriptArguments -from trl.scripts.vllm_serve import main as vllm_serve_main from axolotl.cli.config import load_cfg @@ -28,6 +27,9 @@ def do_vllm_serve( cfg = load_cfg(config) model = cfg.base_model + serve_module = cli_args.get("serve_module", "trl.scripts.vllm_serve") + vllm_serve_main = getattr(__import__(serve_module, fromlist=["main"]), "main") + tensor_parallel_size = ( cli_args.get("tensor_parallel_size") or cfg.vllm.tensor_parallel_size ) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 99ab397c7e..25d327dcde 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1197,6 +1197,10 @@ def build(self, total_num_steps): else: raise ValueError(f"Unsupported RL: {self.cfg.rl}") + if self.cfg.plugins: + plugin_manager = PluginManager.get_instance() + trainer_cls = plugin_manager.get_trainer_cls(self.cfg) + sig = inspect.signature(trainer_cls) if "tokenizer" in sig.parameters.keys(): trainer_kwargs["tokenizer"] = self.tokenizer diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 34f084f100..25c8029590 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1149,18 +1149,18 @@ def check_kto_config(cls, data): return data - @model_validator(mode="before") - @classmethod - def check_grpo_peft_liger(cls, data): - if ( - data.get("rl") == "grpo" - and data.get("trl", {}) - and data.get("trl").get("use_liger_loss") - and data.get("adapter") - ): - raise ValueError("PEFT + GRPO + Liger is not yet supported") - return data - + # @model_validator(mode="before") + # @classmethod + # def check_grpo_peft_liger(cls, data): + # if ( + # data.get("rl") == "grpo" + # and data.get("trl", {}) + # and data.get("trl").get("use_liger_loss") + # and data.get("adapter") + # ): + # raise ValueError("PEFT + GRPO + Liger is not yet supported") + # return data + # @model_validator(mode="before") @classmethod def check_grpo_liger_sequence_parallel(cls, data): From c0a0c7534cc3a9a2b4c0ad6235fd5756f35ec17b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 13 May 2025 16:39:39 -0400 Subject: [PATCH 0651/1405] Activation checkpointing with offloading to disk with prefetch (#2663) * offload activations to disk instead of CPU RAM * add prefetch * Disco :dance: * include offload_disk in e2e test for AC * document and make sure to cleanup * fix annotation to match docs * fix docs build * address PR feedback --- _quarto.yml | 3 +- docs/config.qmd | 2 +- .../utils/gradient_checkpointing/__init__.py | 30 +- .../{unsloth.py => offload_cpu.py} | 4 +- .../gradient_checkpointing/offload_disk.py | 531 ++++++++++++++++++ src/axolotl/utils/models.py | 9 +- src/axolotl/utils/schemas/config.py | 2 +- .../patched/test_activation_checkpointing.py | 7 +- 8 files changed, 577 insertions(+), 11 deletions(-) rename src/axolotl/utils/gradient_checkpointing/{unsloth.py => offload_cpu.py} (95%) create mode 100644 src/axolotl/utils/gradient_checkpointing/offload_disk.py diff --git a/_quarto.yml b/_quarto.yml index 463f76b342..dc5071838f 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -139,7 +139,8 @@ quartodoc: - utils.optimizers.adopt - utils.data.pretraining - utils.data.sft - - utils.gradient_checkpointing.unsloth + - utils.gradient_checkpointing.offload_cpu + - utils.gradient_checkpointing.offload_disk - title: Schemas desc: Pydantic data models for Axolotl config contents: diff --git a/docs/config.qmd b/docs/config.qmd index eba9f4881b..10e5a5895e 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -539,7 +539,7 @@ train_on_inputs: false # Note that training loss may have an oscillating pattern with this enabled. group_by_length: false -# Whether to use gradient checkpointing. Available options are: true, false, "offload". +# Whether to use gradient checkpointing. Available options are: true, false, "offload", "offload_disk". # https://huggingface.co/docs/transformers/v4.18.0/en/performance#gradient-checkpointing gradient_checkpointing: false # additional kwargs to pass to the trainer for gradient checkpointing diff --git a/src/axolotl/utils/gradient_checkpointing/__init__.py b/src/axolotl/utils/gradient_checkpointing/__init__.py index f84f76d80b..ae0c559e96 100644 --- a/src/axolotl/utils/gradient_checkpointing/__init__.py +++ b/src/axolotl/utils/gradient_checkpointing/__init__.py @@ -5,8 +5,11 @@ from packaging import version -from axolotl.utils.gradient_checkpointing.unsloth import ( - Unsloth_Offloaded_Gradient_Checkpointer, +from axolotl.utils.gradient_checkpointing.offload_cpu import ( + CPU_Offloaded_Gradient_Checkpointer, +) +from axolotl.utils.gradient_checkpointing.offload_disk import ( + Disco, ) transformers_version = version.parse(importlib.metadata.version("transformers")) @@ -26,12 +29,31 @@ def hf_grad_checkpoint_offload_wrapper( decoder_layer, *args, use_reentrant=None ): # pylint: disable=unused-argument if uses_gc_layers(decoder_layer): - return Unsloth_Offloaded_Gradient_Checkpointer.apply( + return CPU_Offloaded_Gradient_Checkpointer.apply( + decoder_layer, + *args, + ) + + return CPU_Offloaded_Gradient_Checkpointer.apply( + ( + decoder_layer.func.__self__ + if isinstance(decoder_layer, partial) + else decoder_layer.__self__ + ), + *args, + ) + + +def hf_grad_checkpoint_disk_offload_wrapper( + decoder_layer, *args, use_reentrant=None +): # pylint: disable=unused-argument + if uses_gc_layers(decoder_layer): + return Disco.apply( decoder_layer, *args, ) - return Unsloth_Offloaded_Gradient_Checkpointer.apply( + return Disco.apply( ( decoder_layer.func.__self__ if isinstance(decoder_layer, partial) diff --git a/src/axolotl/utils/gradient_checkpointing/unsloth.py b/src/axolotl/utils/gradient_checkpointing/offload_cpu.py similarity index 95% rename from src/axolotl/utils/gradient_checkpointing/unsloth.py rename to src/axolotl/utils/gradient_checkpointing/offload_cpu.py index 7a14614b18..bbb5ad40d3 100644 --- a/src/axolotl/utils/gradient_checkpointing/unsloth.py +++ b/src/axolotl/utils/gradient_checkpointing/offload_cpu.py @@ -1,4 +1,4 @@ -"""Unsloth checkpointing""" +"""CPU offloaded checkpointing""" # Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. # @@ -26,7 +26,7 @@ torch_cuda_amp_custom_bwd = torch.amp.custom_bwd(device_type="cuda") -class Unsloth_Offloaded_Gradient_Checkpointer( # pylint: disable=invalid-name +class CPU_Offloaded_Gradient_Checkpointer( # pylint: disable=invalid-name torch.autograd.Function ): """ diff --git a/src/axolotl/utils/gradient_checkpointing/offload_disk.py b/src/axolotl/utils/gradient_checkpointing/offload_disk.py new file mode 100644 index 0000000000..90e70f504a --- /dev/null +++ b/src/axolotl/utils/gradient_checkpointing/offload_disk.py @@ -0,0 +1,531 @@ +""" +DISCO - DIsk-based Storage and Checkpointing with Optimized prefetching +""" + +# Copyright 2025 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import atexit +import concurrent.futures +import logging +import os +import queue +import shutil +import tempfile +import threading +import time +import uuid +from collections import deque +from concurrent.futures import Future +from typing import Dict + +import torch + +torch_cuda_amp_custom_fwd = torch.amp.custom_fwd(device_type="cuda") +torch_cuda_amp_custom_bwd = torch.amp.custom_bwd(device_type="cuda") + +# Setup logger +logger = logging.getLogger(__name__) + + +class DiskOffloadManager: + """ + Manages offloaded tensors and handles prefetching in a separate thread. + Includes synchronization to prevent race conditions. + """ + + def __init__( + self, + prefetch_size: int = 3, + prefetch_to_gpu: bool = True, + save_workers: int = 4, + ): + """ + Args: + prefetch_size: Maximum number of tensors to prefetch in the background. + prefetch_to_gpu: Whether to prefetch tensors directly to GPU memory. + save_workers: Maximum number of concurrent save operations. + """ + self.temp_dir = tempfile.mkdtemp(prefix="disco_") + + # Track tensor paths and their status + self.tensor_paths: deque = deque() # Ordered history of tensor paths (LIFO) + self.file_locks: Dict[str, threading.Lock] = ( + {} + ) # Maps file_path -> threading.Lock() + # Maps file_path -> status ("saving", "ready", "prefetching", "loaded", "deleted") + self.file_status: Dict[str, str] = {} + + self.max_prefetch = prefetch_size + self.prefetch_to_gpu = prefetch_to_gpu + + # Thread synchronization + self.manager_lock = threading.RLock() # Used for thread-safe operations + + # Prefetch queue and cache + self.prefetch_queue: queue.Queue = queue.Queue() + self.prefetch_cache: Dict[str, torch.Tensor] = {} # Maps file_path -> tensor + + # Save queue and thread pool + self.save_queue: queue.Queue = queue.Queue() + self.save_pool = concurrent.futures.ThreadPoolExecutor(max_workers=save_workers) + self.save_futures: Dict[str, Future] = {} + self.save_semaphore = threading.Semaphore( + save_workers * 2 + ) # Limit concurrent save operations + + # Start prefetch worker thread + self.stop_event = threading.Event() + # start multiple threads for prefetching + self.prefetch_worker_count = 2 + self.prefetch_workers = [] + for _ in range(self.prefetch_worker_count): + worker = threading.Thread(target=self._prefetch_worker, daemon=True) + worker.start() + self.prefetch_workers.append(worker) + + # Start save worker thread + self.save_worker = threading.Thread(target=self._save_worker, daemon=True) + self.save_worker.start() + self.idx = 0 + + atexit.register(self.cleanup) + + def _save_worker(self): + """Background thread that processes the save queue""" + while not self.stop_event.is_set(): + try: + save_item = self.save_queue.get(timeout=0.5) + if save_item is None: + continue + + tensor, file_path = save_item + + # Submit the save task to the thread pool + future = self.save_pool.submit( + self._save_tensor_to_disk, tensor, file_path + ) + with self.manager_lock: + self.save_futures[file_path] = future + + self.save_queue.task_done() + + except queue.Empty: + time.sleep(0.01) # Small sleep to prevent CPU spinning + continue + + def _save_tensor_to_disk(self, tensor: torch.Tensor, file_path: str): + """Actually save the tensor to disk""" + try: + # Save tensor to disk + cpu_tensor = tensor.detach().cpu() + torch.save(cpu_tensor, file_path) + del cpu_tensor + + with self.manager_lock: + # Mark file as ready + self.file_status[file_path] = "ready" + + # Release semaphore + self.save_semaphore.release() + + return True + except FileNotFoundError as e: + logger.error(f"Error saving tensor to {file_path}: {e}") + with self.manager_lock: + self.file_status[file_path] = "error" + + # Release semaphore + self.save_semaphore.release() + + return False + + def _prefetch_worker(self): + """Background thread that loads tensors from disk ahead of time""" + while not self.stop_event.is_set(): + try: + file_path = self.prefetch_queue.get(timeout=0.5) + if file_path is None: + continue + + # Check if file is available and not already in cache + with self.manager_lock: + if ( + file_path not in self.file_status + or self.file_status[file_path] == "deleted" + ): + self.prefetch_queue.task_done() + if file_path in self.prefetch_cache: + self.prefetch_queue.task_done() + continue + + # If file is still being saved, wait for it + if ( + self.file_status[file_path] == "saving" + and file_path in self.save_futures + ): + # Re-queue this prefetch request with a little delay + self.prefetch_queue.task_done() + time.sleep(0.1) + self.prefetch_queue.put(file_path) + continue + + # Mark file as being prefetched + self.file_status[file_path] = "prefetching" + + # Load tensor from disk and store in cache + try: + if os.path.exists(file_path): + if self.prefetch_to_gpu: + tensor = torch.load( + file_path, + map_location=torch.device("cuda"), + weights_only=True, + ) + else: + tensor = torch.load(file_path, weights_only=True) + + with self.manager_lock: + self.prefetch_cache[file_path] = tensor + self.file_status[file_path] = "ready" + else: + with self.manager_lock: + if self.file_status.get(file_path) != "deleted": + logger.warning( + f"Prefetch error: File not found {file_path}" + ) + self.file_status[file_path] = "missing" + + except FileNotFoundError as e: + with self.manager_lock: + if self.file_status.get(file_path) != "deleted": + logger.warning(f"Prefetch error for {file_path}: {e}") + self.file_status[file_path] = "error" + + self.prefetch_queue.task_done() + + except queue.Empty: + time.sleep(0.01) # Small sleep to prevent CPU spinning + continue + + def save_tensor(self, tensor: torch.Tensor): + """Save tensor to disk asynchronously and return file path with thread-safe operations""" + # Generate unique file path + self.idx += 1 + file_path: str = os.path.join( + self.temp_dir, f"{self.idx:06d}-{uuid.uuid4()}.pt" + ) + + with self.manager_lock: + # Mark file as being saved + self.file_locks[file_path] = threading.Lock() + self.file_status[file_path] = "saving" + # Add to history + self.tensor_paths.append(file_path) + + # Acquire semaphore to limit concurrent save operations + self.save_semaphore.acquire() # pylint: disable=consider-using-with + # Queue tensor for saving in background + self.save_queue.put((tensor.detach(), file_path)) + + return file_path + + def wait_for_save(self, file_path, timeout=None) -> None: + """Wait for a tensor to be saved to disk""" + start_time = time.time() + while timeout is None or time.time() - start_time < timeout: + with self.manager_lock: + if self.file_status.get(file_path) == "ready": + return + if self.file_status.get(file_path) in ["error", "missing", "deleted"]: + return + + if file_path in self.save_futures: + future = self.save_futures[file_path] + if future.done(): + return + + # Small sleep to prevent CPU spinning + time.sleep(0.01) + + # Timeout + logger.warning(f"Timeout waiting for tensor to be saved: {file_path}") + return + + def load_tensor(self, file_path, target_device="cuda"): + """Load tensor from disk or prefetch cache with proper synchronization""" + # Wait for tensor to be saved if it's still in progress + self.wait_for_save(file_path) + + tensor = None + + # Try to get from cache first + with self.manager_lock: + # Check if tensor is already in cache + if file_path in self.prefetch_cache: + tensor = self.prefetch_cache[file_path] + del self.prefetch_cache[file_path] + self.file_status[file_path] = "loaded" + + if tensor is not None: + # Ensure tensor is on correct device + if target_device != "cpu" and tensor.device.type == "cpu": + tensor = tensor.to(target_device, non_blocking=True) + return tensor + + # If not in cache, load directly from disk + try: + if not os.path.exists(file_path): + logger.error(f"File not found for loading: {file_path}") + raise FileNotFoundError(f"File not found: {file_path}") + + tensor = torch.load(file_path, weights_only=True) + + with self.manager_lock: + self.file_status[file_path] = "loaded" + + if target_device != "cpu": + tensor = tensor.to(target_device, non_blocking=True) + + return tensor + + except Exception as e: + logger.error(f"Error loading tensor from {file_path}: {e}") + raise + + def _safe_delete_file(self, file_path): + """Safely delete a file with proper synchronization""" + with self.manager_lock: + # Make sure any save operation is completed + if file_path in self.save_futures: + future = self.save_futures[file_path] + try: + if not future.done(): + future.cancel() + del self.save_futures[file_path] + except FileNotFoundError as e: + logger.warning( + f"Error canceling save operation for {file_path}: {e}" + ) + + # Only delete if file exists and is not being prefetched + status = self.file_status.get(file_path) + if status in ["ready", "loaded", "error", "missing"]: + try: + if os.path.exists(file_path): + os.remove(file_path) + self.file_status[file_path] = "deleted" + return True + except FileNotFoundError as e: + logger.warning(f"Error deleting file {file_path}: {e}") + return False + + def trigger_prefetch(self, n=None): + """Trigger prefetching of the next N tensors with proper synchronization""" + if n is None: + n = self.max_prefetch + + prefetch_paths = [] + with self.manager_lock: + # Find files that are ready to be prefetched (not already in cache or being prefetched) + for path in reversed(self.tensor_paths): + if ( + path not in self.prefetch_cache + and self.file_status.get(path) == "ready" + ): + prefetch_paths.append(path) + if len(prefetch_paths) >= n: + break + + # Queue files for prefetching + for path in prefetch_paths: + self.prefetch_queue.put(path) + + def cleanup_tensor(self, file_path: str): + """Clean up a specific tensor file after it's been used""" + with self.manager_lock: + if file_path in self.tensor_paths: + self.tensor_paths.remove(file_path) + + # Remove from prefetch cache if present + if file_path in self.prefetch_cache: + del self.prefetch_cache[file_path] + + # Remove from save futures if present + if file_path in self.save_futures: + future = self.save_futures[file_path] + if not future.done(): + future.cancel() + del self.save_futures[file_path] + + # Try to delete the file + self._safe_delete_file(file_path) + + def cleanup(self): + """Clean up all temp files and stop prefetch thread with proper synchronization""" + self.stop_event.set() + + # Cancel all pending save operations + with self.manager_lock: + for _, future in self.save_futures.items(): + if not future.done(): + future.cancel() + self.save_futures.clear() + + # Drain the save queue + while not self.save_queue.empty(): + try: + self.save_queue.get_nowait() + self.save_queue.task_done() + except queue.Empty: + break + + # Shutdown the save pool + self.save_pool.shutdown(wait=False) + + # Join the save worker thread + if self.save_worker.is_alive(): + self.save_worker.join(timeout=2.0) + + # Join the prefetch worker threads + for thread in self.prefetch_workers: + if thread.is_alive(): + thread.join(timeout=2.0) + + # Clear cache and remove all temporary files + with self.manager_lock: + self.prefetch_cache.clear() + paths_to_delete = list(self.tensor_paths) + self.tensor_paths.clear() + + # Delete all temporary files + for path in paths_to_delete: + self._safe_delete_file(path) + + # Remove temp directory + try: + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir, ignore_errors=True) + except FileNotFoundError as e: + logger.warning(f"Error removing temporary directory {self.temp_dir}: {e}") + + +class Disco(torch.autograd.Function): + """ + Disco: DIsk-based Storage and Checkpointing with Optimized prefetching + Advanced disk-based gradient checkpointer with prefetching. + """ + + # Shared manager instance across all checkpointing operations + _manager = None + + @staticmethod + def get_instance(prefetch_size=1, prefetch_to_gpu=True, save_workers=4): + """Get or create the offload manager""" + if Disco._manager is None: + Disco._manager = DiskOffloadManager( + prefetch_size=prefetch_size, + prefetch_to_gpu=prefetch_to_gpu, + save_workers=save_workers, + ) + return Disco._manager + + @staticmethod + @torch_cuda_amp_custom_fwd + def forward( + ctx, + forward_function, + hidden_states, + *args, + prefetch_size=1, + prefetch_to_gpu=True, + save_workers=4, + ): + """Forward pass that offloads activations to disk asynchronously""" + # Get or create the manager + manager = Disco.get_instance( + prefetch_size=prefetch_size, + prefetch_to_gpu=prefetch_to_gpu, + save_workers=save_workers, + ) + + # Save tensor to disk asynchronously + file_path = manager.save_tensor(hidden_states) + + # Run forward pass immediately without waiting for save to complete + with torch.no_grad(): + output = forward_function(hidden_states, *args) + + # Store what we need for backward + ctx.save_for_backward(torch.tensor([0])) # Dummy tensor + ctx.file_path = file_path + ctx.forward_function = forward_function + ctx.args = args + + return output + + @staticmethod + @torch_cuda_amp_custom_bwd + def backward(ctx, *grad_outputs): + """Backward pass that loads activations from disk with prefetching""" + # Get the manager + manager = Disco._manager + + # Trigger prefetching for future tensors + # This happens at the start of backward, so should have time to complete + manager.trigger_prefetch() + + # Load hidden states from disk or prefetch cache + file_path = ctx.file_path + try: + # Ensure the file is saved before we try to load it + manager.wait_for_save(file_path) + + hidden_states = manager.load_tensor(file_path) + hidden_states.requires_grad = True + + # Compute gradients + with torch.enable_grad(): + output = ctx.forward_function(hidden_states, *ctx.args) + + # Handle tuple outputs properly + if isinstance(output, tuple): + if len(grad_outputs) == len(output): + torch.autograd.backward(output, grad_outputs) + else: + torch.autograd.backward(output, grad_outputs[0]) + else: + torch.autograd.backward(output, grad_outputs[0]) + + # Clean up the file after we're done with it + manager.cleanup_tensor(file_path) + + return ( + ( + None, # forward_function + hidden_states.grad, # hidden_states grad + ) + + (None,) * len(ctx.args) # for each arg + + ( + None, # prefetch_size + None, # prefetch_to_gpu + None, # save_workers + ) + ) + + except Exception as e: + logger.error(f"Error in backward pass: {e}") + # Clean up the file even on error + manager.cleanup_tensor(file_path) + raise diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index dff6d854b3..316fbec8ce 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -70,7 +70,10 @@ is_local_main_process, is_main_process, ) -from axolotl.utils.gradient_checkpointing import hf_grad_checkpoint_offload_wrapper +from axolotl.utils.gradient_checkpointing import ( + hf_grad_checkpoint_disk_offload_wrapper, + hf_grad_checkpoint_offload_wrapper, +) from axolotl.utils.lora_embeddings import get_linear_embedding_layers from axolotl.utils.model_shard_quant import load_sharded_model, load_sharded_model_quant from axolotl.utils.schemas.enums import RLType @@ -620,6 +623,10 @@ def apply_patches(self) -> None: if self.cfg.gradient_checkpointing in ["unsloth", "offload"]: transformers.modeling_utils.checkpoint = hf_grad_checkpoint_offload_wrapper + if self.cfg.gradient_checkpointing == "offload_disk": + transformers.modeling_utils.checkpoint = ( + hf_grad_checkpoint_disk_offload_wrapper + ) if self.cfg.flash_attention: self.patch_attention() diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 25c8029590..8ae9d5c04b 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -178,7 +178,7 @@ class AxolotlInputConfig( # torch_dtype: torch.dtype | None - gradient_checkpointing: Literal["unsloth", "offload"] | bool | None = Field( + gradient_checkpointing: Literal["offload", "offload_disk"] | bool | None = Field( default=False ) gradient_checkpointing_kwargs: dict[str, Any] | None = None diff --git a/tests/e2e/patched/test_activation_checkpointing.py b/tests/e2e/patched/test_activation_checkpointing.py index cbabab6fd0..45107b871a 100644 --- a/tests/e2e/patched/test_activation_checkpointing.py +++ b/tests/e2e/patched/test_activation_checkpointing.py @@ -26,10 +26,15 @@ class TestActivationCheckpointing: E2E tests for activation checkpointing """ + @pytest.mark.parametrize( + "gradient_checkpointing", + ["offload", "offload_disk"], + ) def test_activation_checkpointing_offload( self, temp_dir, fix_checkpoint_after_test, # pylint: disable=unused-argument,redefined-outer-name + gradient_checkpointing, ): # pylint: disable=duplicate-code cfg = DictDefault( @@ -64,7 +69,7 @@ def test_activation_checkpointing_offload( "sample_packing": True, "bf16": True, "save_safetensors": True, - "gradient_checkpointing": "offload", + "gradient_checkpointing": gradient_checkpointing, } ) From 86472715dabf6be53e4f2dccce746c2344f455a5 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 17 May 2025 00:05:55 +0700 Subject: [PATCH 0652/1405] fix: remove doc string imports in monkeypatches (#2671) [skip ci] --- .../cut_cross_entropy/monkeypatch/cohere.py | 10 ---------- .../cut_cross_entropy/monkeypatch/gemma.py | 10 ---------- .../cut_cross_entropy/monkeypatch/gemma3.py | 12 ------------ .../cut_cross_entropy/monkeypatch/llama.py | 10 ---------- .../cut_cross_entropy/monkeypatch/llama4.py | 13 ------------- .../cut_cross_entropy/monkeypatch/mistral3.py | 8 -------- .../cut_cross_entropy/monkeypatch/qwen2_moe.py | 10 ---------- .../cut_cross_entropy/monkeypatch/qwen2_vl.py | 10 ---------- .../cut_cross_entropy/monkeypatch/qwen3_moe.py | 11 ----------- src/axolotl/integrations/liger/models/deepseekv2.py | 4 ---- src/axolotl/integrations/liger/models/jamba.py | 10 ---------- src/axolotl/monkeypatch/gemma3.py | 8 -------- 12 files changed, 116 deletions(-) diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py index 99e17910e6..ea9e107247 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py @@ -20,25 +20,15 @@ from transformers.cache_utils import Cache from transformers.modeling_outputs import CausalLMOutputWithPast from transformers.models.cohere.modeling_cohere import ( - _CONFIG_FOR_DOC, - COHERE_INPUTS_DOCSTRING, KwargsForCausalLM, ) from transformers.processing_utils import Unpack -from transformers.utils import ( - add_start_docstrings_to_model_forward, - replace_return_docstrings, -) from transformers.utils.deprecation import deprecate_kwarg _PATCH_OPTS: PatchOptions | None = None @deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -@add_start_docstrings_to_model_forward(COHERE_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def cce_forward( self, input_ids: torch.LongTensor | None = None, diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma.py index 4c8d2261a6..ae3d8c6ef3 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma.py @@ -17,25 +17,15 @@ from transformers.cache_utils import Cache from transformers.modeling_outputs import CausalLMOutputWithPast from transformers.models.gemma.modeling_gemma import ( - _CONFIG_FOR_DOC, - GEMMA_INPUTS_DOCSTRING, KwargsForCausalLM, ) from transformers.processing_utils import Unpack -from transformers.utils import ( - add_start_docstrings_to_model_forward, - replace_return_docstrings, -) from transformers.utils.deprecation import deprecate_kwarg _PATCH_OPTS: PatchOptions | None = None @deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -@add_start_docstrings_to_model_forward(GEMMA_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def cce_forward( self, input_ids: torch.LongTensor | None = None, diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py index ccf0c160df..644e5cce7f 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py @@ -20,15 +20,11 @@ from transformers.cache_utils import Cache, HybridCache from transformers.modeling_outputs import CausalLMOutputWithPast from transformers.models.gemma3.modeling_gemma3 import ( - _CONFIG_FOR_DOC, - GEMMA3_INPUTS_DOCSTRING, Gemma3CausalLMOutputWithPast, logger, ) from transformers.utils import ( - add_start_docstrings_to_model_forward, is_torchdynamo_compiling, - replace_return_docstrings, ) from transformers.utils.deprecation import deprecate_kwarg @@ -38,10 +34,6 @@ @deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -@add_start_docstrings_to_model_forward(GEMMA3_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def cce_forward( self, input_ids: torch.LongTensor | None = None, @@ -170,10 +162,6 @@ def cce_forward( @deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -@add_start_docstrings_to_model_forward(GEMMA3_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=Gemma3CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def cce_forward_multimodal( self, input_ids: torch.LongTensor | None = None, diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama.py index 42ab996b9c..bed411ace3 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama.py @@ -19,15 +19,9 @@ CausalLMOutputWithPast, ) from transformers.models.llama.modeling_llama import ( - _CONFIG_FOR_DOC, - LLAMA_INPUTS_DOCSTRING, KwargsForCausalLM, ) from transformers.processing_utils import Unpack -from transformers.utils import ( - add_start_docstrings_to_model_forward, - replace_return_docstrings, -) from transformers.utils.deprecation import deprecate_kwarg from transformers.utils.generic import can_return_tuple @@ -36,10 +30,6 @@ @can_return_tuple @deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -@add_start_docstrings_to_model_forward(LLAMA_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def cce_forward( self, input_ids: Optional[torch.LongTensor] = None, diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py index 7204f5c900..3143e9c8da 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py @@ -16,22 +16,12 @@ from transformers.cache_utils import Cache from transformers.modeling_outputs import CausalLMOutputWithPast from transformers.models.llama4.modeling_llama4 import ( - _CONFIG_FOR_DOC, - LLAMA4_INPUTS_DOCSTRING, Llama4CausalLMOutputWithPast, ) -from transformers.utils import ( - add_start_docstrings_to_model_forward, - replace_return_docstrings, -) _PATCH_OPTS: PatchOptions | None = None -@add_start_docstrings_to_model_forward(LLAMA4_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def cce_forward( self, input_ids: torch.LongTensor | None = None, @@ -160,9 +150,6 @@ def cce_forward( ) -@replace_return_docstrings( - output_type=Llama4CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def cce_forward_multimodal( self, input_ids: torch.LongTensor | None = None, # type: ignore diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mistral3.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mistral3.py index adb65fa8f2..aa252701ec 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mistral3.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mistral3.py @@ -19,15 +19,11 @@ Mistral3CausalLMOutputWithPast, ) from transformers.models.mistral.modeling_mistral import ( - _CONFIG_FOR_DOC, - MISTRAL_INPUTS_DOCSTRING, KwargsForCausalLM, ) from transformers.processing_utils import Unpack from transformers.utils import ( - add_start_docstrings_to_model_forward, is_torchdynamo_compiling, - replace_return_docstrings, ) from transformers.utils.deprecation import deprecate_kwarg @@ -35,10 +31,6 @@ @deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -@add_start_docstrings_to_model_forward(MISTRAL_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def cce_forward( self, input_ids: torch.LongTensor | None = None, diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_moe.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_moe.py index 0811bf55a6..afe56266e8 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_moe.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_moe.py @@ -13,16 +13,10 @@ apply_lce, ) from transformers.models.qwen2_moe.modeling_qwen2_moe import ( - _CONFIG_FOR_DOC, - QWEN2MOE_INPUTS_DOCSTRING, MoeCausalLMOutputWithPast, MoeModelOutputWithPast, load_balancing_loss_func, ) -from transformers.utils import ( - add_start_docstrings_to_model_forward, - replace_return_docstrings, -) from transformers.utils.deprecation import deprecate_kwarg from transformers.utils.generic import can_return_tuple @@ -31,10 +25,6 @@ @can_return_tuple @deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -@add_start_docstrings_to_model_forward(QWEN2MOE_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=MoeCausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def forward( self, input_ids: Optional[torch.LongTensor] = None, diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_vl.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_vl.py index 250c3ab6bd..79af01cfad 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_vl.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_vl.py @@ -14,22 +14,12 @@ ) from torch.nn import CrossEntropyLoss from transformers.models.qwen2_vl.modeling_qwen2_vl import ( - _CONFIG_FOR_DOC, - QWEN2_VL_INPUTS_DOCSTRING, Qwen2VLCausalLMOutputWithPast, ) -from transformers.utils import ( - add_start_docstrings_to_model_forward, - replace_return_docstrings, -) _PATCH_OPTS: PatchOptions | None = None -@add_start_docstrings_to_model_forward(QWEN2_VL_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=Qwen2VLCausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def cce_forward_multimodal( self, input_ids: Optional[torch.LongTensor] = None, diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3_moe.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3_moe.py index c5cd76f944..90466e64ba 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3_moe.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3_moe.py @@ -12,20 +12,13 @@ TransformersModelT, apply_lce, ) -from transformers.modeling_outputs import CausalLMOutputWithPast from transformers.models.qwen3_moe.modeling_qwen3_moe import ( - _CONFIG_FOR_DOC, - QWEN3_MOE_INPUTS_DOCSTRING, KwargsForCausalLM, MoeCausalLMOutputWithPast, MoeModelOutputWithPast, load_balancing_loss_func, ) from transformers.processing_utils import Unpack -from transformers.utils import ( - add_start_docstrings_to_model_forward, - replace_return_docstrings, -) from transformers.utils.deprecation import deprecate_kwarg from transformers.utils.generic import can_return_tuple @@ -34,10 +27,6 @@ @can_return_tuple @deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -@add_start_docstrings_to_model_forward(QWEN3_MOE_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def forward( self, input_ids: Optional[torch.LongTensor] = None, diff --git a/src/axolotl/integrations/liger/models/deepseekv2.py b/src/axolotl/integrations/liger/models/deepseekv2.py index c29fd4e798..2f0d2a7048 100644 --- a/src/axolotl/integrations/liger/models/deepseekv2.py +++ b/src/axolotl/integrations/liger/models/deepseekv2.py @@ -14,10 +14,6 @@ from transformers.modeling_outputs import CausalLMOutputWithPast -# @add_start_docstrings_to_model_forward(DeepseekV2_INPUTS_DOCSTRING) -# @replace_return_docstrings( -# output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -# ) def lce_forward( self, input_ids: torch.LongTensor = None, diff --git a/src/axolotl/integrations/liger/models/jamba.py b/src/axolotl/integrations/liger/models/jamba.py index 7ab464c88d..d255299707 100644 --- a/src/axolotl/integrations/liger/models/jamba.py +++ b/src/axolotl/integrations/liger/models/jamba.py @@ -13,21 +13,11 @@ from torch.nn import CrossEntropyLoss from transformers.modeling_outputs import MoeCausalLMOutputWithPast from transformers.models.jamba.modeling_jamba import ( - _CONFIG_FOR_DOC, - JAMBA_INPUTS_DOCSTRING, HybridMambaAttentionDynamicCache, load_balancing_loss_func, ) -from transformers.utils import ( - add_start_docstrings_to_model_forward, - replace_return_docstrings, -) -@add_start_docstrings_to_model_forward(JAMBA_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=MoeCausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def lce_forward( self, input_ids: torch.LongTensor = None, diff --git a/src/axolotl/monkeypatch/gemma3.py b/src/axolotl/monkeypatch/gemma3.py index 38183fa0ed..36f591efd0 100644 --- a/src/axolotl/monkeypatch/gemma3.py +++ b/src/axolotl/monkeypatch/gemma3.py @@ -7,24 +7,16 @@ import torch from transformers.cache_utils import Cache from transformers.models.gemma3.modeling_gemma3 import ( - _CONFIG_FOR_DOC, - GEMMA3_INPUTS_DOCSTRING, Gemma3CausalLMOutputWithPast, logger, ) from transformers.utils import ( - add_start_docstrings_to_model_forward, is_torchdynamo_compiling, - replace_return_docstrings, ) from transformers.utils.deprecation import deprecate_kwarg @deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -@add_start_docstrings_to_model_forward(GEMMA3_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=Gemma3CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC -) def new_forward( self, input_ids: torch.LongTensor = None, From 8f8a7afb05aec84ed6bf03e187a745088e2563b0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 16 May 2025 13:06:08 -0400 Subject: [PATCH 0653/1405] Add ci and images for CUDA 12.8 for B200s (#2683) [skip ci] * Add ci and images for CUDA 12.8 for B200s * add comments explaining CI [skip e2e] --- .github/workflows/main.yml | 10 ++++++++++ .github/workflows/tests.yml | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4fcf083523..01606f9020 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,6 +31,11 @@ jobs: python_version: "3.11" pytorch: 2.7.0 axolotl_extras: + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.7.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -94,6 +99,11 @@ jobs: python_version: "3.11" pytorch: 2.7.0 axolotl_extras: + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.7.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c296e23145..69f0a030d5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -295,6 +295,7 @@ jobs: find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; docker-e2e-tests-1st: + # Run this job first as a gate for running the remainder of the test matrix if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] @@ -341,6 +342,8 @@ jobs: # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] timeout-minutes: 90 + # Only run the remainder of the matrix if the first e2e check passed; + # this is to save on wasted compute costs for known failures that get caught in the first run needs: [pre-commit, pytest, docker-e2e-tests-1st] strategy: @@ -365,6 +368,12 @@ jobs: pytorch: 2.7.0 num_gpus: 1 axolotl_extras: + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.7.0 + num_gpus: 1 + axolotl_extras: steps: - name: Checkout uses: actions/checkout@v4 From c9797de6bb208dc95eb7374e76fefaa9f00a58c8 Mon Sep 17 00:00:00 2001 From: michelyang Date: Fri, 16 May 2025 10:06:20 -0700 Subject: [PATCH 0654/1405] Add num_proc to fix data set slow processing issue (#2681) [skip ci] --- src/axolotl/utils/data/rl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index eaa8348226..dc59200992 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -72,6 +72,7 @@ def map_dataset(cfg, data_set, ds_transform_fn, tokenizer, **map_kwargs): data_set = data_set.map( ds_transform_fn, desc="Mapping RL Dataset", + num_proc=cfg.dataset_processes, **map_kwargs, ) From c837c4a424a60dd34ea44e34a65076fd6a5782d4 Mon Sep 17 00:00:00 2001 From: Eric Meier Date: Fri, 16 May 2025 10:06:46 -0700 Subject: [PATCH 0655/1405] Add missing init file to liger plugin (#2670) [skip ci] --- src/axolotl/integrations/liger/models/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/axolotl/integrations/liger/models/__init__.py diff --git a/src/axolotl/integrations/liger/models/__init__.py b/src/axolotl/integrations/liger/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From f661858fc4d6c0d2329f5b0c1c965a7fc694c8fc Mon Sep 17 00:00:00 2001 From: xzuyn <16216325+xzuyn@users.noreply.github.com> Date: Fri, 16 May 2025 13:06:58 -0400 Subject: [PATCH 0656/1405] Print dataset name (#2668) [skip ci] --- src/axolotl/utils/data/sft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 5fa0cb60d6..6de2d2cf74 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -484,7 +484,7 @@ def get_dataset_wrapper( } LOG.info( - f"Loading dataset with base_type: {d_base_type} and prompt_style: {d_prompt_style}" + f"Loading dataset: {config_dataset['path']} with base_type: {d_base_type} and prompt_style: {d_prompt_style}" ) if ( From 3a5b495a740a85fc8dc638ae4a78620dadc59721 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 17 May 2025 00:07:40 +0700 Subject: [PATCH 0657/1405] Fix: improve doc on merge/inference cli visibility (#2674) * feat: improve visibility for merge doc * feat: add tip on reuse config between modes --- docs/getting-started.qmd | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/getting-started.qmd b/docs/getting-started.qmd index a0501ad21e..064985e35a 100644 --- a/docs/getting-started.qmd +++ b/docs/getting-started.qmd @@ -104,7 +104,7 @@ the `alpaca` dataset format, which has the following format: Please see our [Dataset Formats](dataset-formats) for more dataset formats and how to format them. -2. Prepare your JSONL data in the specified format (in this case, the expected `alpaca +2. Prepare your JSONL data in the specified format (in this case, the expected `alpaca` format): ```json @@ -120,6 +120,12 @@ axolotl train my_training.yml ## Common Tasks {#sec-common-tasks} +::: {.callout-tip} + +The same yaml file is used for training, inference, and merging. + +::: + ### Testing Your Model {#sec-testing} After training, test your model: @@ -128,6 +134,16 @@ After training, test your model: axolotl inference my_training.yml --lora-model-dir="./outputs/lora-out" ``` +More details can be found in [Inference](inference.qmd). + +### Using a UI {#sec-ui} + +Launch a Gradio interface: + +```bash +axolotl inference my_training.yml --lora-model-dir="./outputs/lora-out" --gradio +``` + ### Preprocessing Data {#sec-preprocessing} For large datasets, preprocess first: @@ -136,14 +152,22 @@ For large datasets, preprocess first: axolotl preprocess my_training.yml ``` -### Using a UI {#sec-ui} +Please make sure to set `dataset_prepared_path: ` in your config to set the path to save the prepared dataset. -Launch a Gradio interface: +More details can be found in [Dataset Preprocessing](dataset_preprocessing.qmd). + +### Merging LoRA weights {#sec-merging-lora} + +To merge the LoRA weights back into the base model, run: ```bash -axolotl inference my_training.yml --lora-model-dir="./outputs/lora-out" --gradio +axolotl merge-lora my_training.yml --lora-model-dir="./outputs/lora-out" ``` +The merged model will be saved in the `{output_dir}/merged` directory. + +More details can be found in [Merging LoRA weights](inference.qmd#sec-merging). + ## Next Steps {#sec-next-steps} Now that you have the basics, you might want to: @@ -156,6 +180,7 @@ Now that you have the basics, you might want to: Check our other guides for details on these topics: - [Configuration Guide](config.qmd) - Full configuration options +- [Dataset Loading](dataset-loading.qmd) - Loading datasets from various sources - [Dataset Formats](dataset-formats) - Working with different data formats - [Multi-GPU Training](multi-gpu.qmd) - [Multi-Node Training](multi-node.qmd) From 288653adb6decba7ff71f6f4b5c460542f29f1a7 Mon Sep 17 00:00:00 2001 From: C080 <54465490+C080@users.noreply.github.com> Date: Fri, 16 May 2025 21:46:31 +0200 Subject: [PATCH 0658/1405] =?UTF-8?q?Fix:=20Make=20MLflow=20config=20artif?= =?UTF-8?q?act=20logging=20respect=20hf=5Fmlflow=5Flog=5Fartifa=E2=80=A6?= =?UTF-8?q?=20(#2675)=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: Make MLflow config artifact logging respect hf_mlflow_log_artifacts setting * cleanup and lint --------- Co-authored-by: Wing Lian --- src/axolotl/utils/callbacks/mlflow_.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/axolotl/utils/callbacks/mlflow_.py b/src/axolotl/utils/callbacks/mlflow_.py index 47679001f5..15ca1ca475 100644 --- a/src/axolotl/utils/callbacks/mlflow_.py +++ b/src/axolotl/utils/callbacks/mlflow_.py @@ -1,6 +1,7 @@ """MLFlow module for trainer callbacks""" import logging +import os from shutil import copyfile from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING @@ -16,6 +17,11 @@ LOG = logging.getLogger("axolotl.callbacks") +def should_log_artifacts() -> bool: + truths = ["TRUE", "1", "YES"] + return os.getenv("HF_MLFLOW_LOG_ARTIFACTS", "FALSE").upper() in truths + + class SaveAxolotlConfigtoMlflowCallback(TrainerCallback): # pylint: disable=duplicate-code """Callback to save axolotl config to mlflow""" @@ -32,13 +38,18 @@ def on_train_begin( ): if is_main_process(): try: - with NamedTemporaryFile( - mode="w", delete=False, suffix=".yml", prefix="axolotl_config_" - ) as temp_file: - copyfile(self.axolotl_config_path, temp_file.name) - mlflow.log_artifact(temp_file.name, artifact_path="") + if should_log_artifacts(): + with NamedTemporaryFile( + mode="w", delete=False, suffix=".yml", prefix="axolotl_config_" + ) as temp_file: + copyfile(self.axolotl_config_path, temp_file.name) + mlflow.log_artifact(temp_file.name, artifact_path="") + LOG.info( + "The Axolotl config has been saved to the MLflow artifacts." + ) + else: LOG.info( - "The Axolotl config has been saved to the MLflow artifacts." + "Skipping logging artifacts to MLflow (hf_mlflow_log_artifacts is false)" ) except (FileNotFoundError, ConnectionError) as err: LOG.warning(f"Error while saving Axolotl config to MLflow: {err}") From 6cb07b9d12984198b8feb0a0abda922be8108e2c Mon Sep 17 00:00:00 2001 From: xzuyn <16216325+xzuyn@users.noreply.github.com> Date: Fri, 16 May 2025 15:46:50 -0400 Subject: [PATCH 0659/1405] Fix for setting `adam_beta3` and `adam_epsilon2` for CAME Optimizer (#2654) [skip ci] * make setting `adam_beta3` and `adam_epsilon2` work correctly * update config docs so users know args are specific to CAME optim --------- Co-authored-by: Wing Lian --- docs/config.qmd | 2 ++ src/axolotl/core/trainer_builder.py | 6 +++++- src/axolotl/core/training_args.py | 13 +++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/config.qmd b/docs/config.qmd index 10e5a5895e..ac4c3fa4fb 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -633,7 +633,9 @@ weight_decay: # adamw hyperparams adam_beta1: adam_beta2: +adam_beta3: # only used for CAME Optimizer adam_epsilon: +adam_epsilon2: # only used for CAME Optimizer # Gradient clipping max norm max_grad_norm: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 25d327dcde..6bd4ef996c 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -387,8 +387,12 @@ def build(self, total_num_steps): training_arguments_kwargs["adam_beta1"] = self.cfg.adam_beta1 if self.cfg.adam_beta2: training_arguments_kwargs["adam_beta2"] = self.cfg.adam_beta2 + if self.cfg.adam_beta3: + training_arguments_kwargs["adam_beta3"] = self.cfg.adam_beta3 if self.cfg.adam_epsilon: training_arguments_kwargs["adam_epsilon"] = self.cfg.adam_epsilon + if self.cfg.adam_epsilon2: + training_arguments_kwargs["adam_epsilon2"] = self.cfg.adam_epsilon2 if self.cfg.max_grad_norm: training_arguments_kwargs["max_grad_norm"] = self.cfg.max_grad_norm @@ -713,7 +717,7 @@ def build(self, total_num_steps): beta1 = training_arguments_kwargs.get("adam_beta1", 0.9) beta2 = training_arguments_kwargs.get("adam_beta2", 0.999) - beta3 = training_arguments_kwargs.get("adam_beta2", 0.9999) + beta3 = training_arguments_kwargs.get("adam_beta3", 0.9999) eps1 = training_arguments_kwargs.get("adam_epsilon", 1e-30) eps2 = training_arguments_kwargs.get("adam_epsilon2", 1e-16) adam_kwargs["betas"] = (beta1, beta2, beta3) diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index 0b14e76612..a81c33801c 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -227,6 +227,19 @@ class AxolotlTrainingMixins: }, ) + adam_beta3: Optional[float] = field( + default=None, + metadata={ + "help": "The beta3 hyperparameter used in some optimizers such as CAME" + }, + ) + adam_epsilon2: Optional[float] = field( + default=None, + metadata={ + "help": "The epsilon2 hyperparameter used in some optimizers such as CAME" + }, + ) + # multi-modal section image_size: int | tuple[int, int] | None = field( From a27b909c5c1c2c561a8d503024b89afcce15226f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 16 May 2025 15:47:03 -0400 Subject: [PATCH 0660/1405] GRPO fixes (peft) (#2676) * don't set peft_config on grpo to prevent double peft wrap * remove overrides needed to support bug * fix grpo tests * require more CPU for multigpu to help with torch compile for vllm --- cicd/multigpu.py | 2 +- src/axolotl/core/trainer_builder.py | 3 +- src/axolotl/core/trainers/grpo/trainer.py | 57 +---------------------- tests/e2e/multigpu/solo/test_grpo.py | 6 --- 4 files changed, 5 insertions(+), 63 deletions(-) diff --git a/cicd/multigpu.py b/cicd/multigpu.py index 90d4ce1ee3..7de4ae0a7d 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -70,7 +70,7 @@ def run_cmd(cmd: str, run_folder: str): image=cicd_image, gpu=GPU_CONFIG, timeout=90 * 60, - cpu=8.0, + cpu=16.0, memory=131072 * N_GPUS, volumes=VOLUME_CONFIG, ) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 6bd4ef996c..d82e4d20b2 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1174,7 +1174,8 @@ def build(self, total_num_steps): if self.eval_dataset: trainer_kwargs["eval_dataset"] = self.eval_dataset if self.cfg.adapter and self.peft_config: - trainer_kwargs["peft_config"] = self.peft_config + if self.cfg.rl is not RLType.GRPO: + trainer_kwargs["peft_config"] = self.peft_config if self.cfg.precompute_ref_log_probs is not None: trainer_kwargs["precompute_ref_log_probs"] = ( self.cfg.precompute_ref_log_probs diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index bc3d140b10..8a89de333a 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -3,7 +3,6 @@ # pylint: disable=too-many-lines,duplicate-code,protected-access,no-member import warnings -from contextlib import nullcontext from typing import Any import datasets @@ -14,7 +13,7 @@ broadcast_object_list, gather, gather_object, - is_peft_model, + is_peft_available, ) from datasets import Dataset, IterableDataset from torch import nn @@ -30,15 +29,13 @@ TrainerCallback, ) from transformers.trainer_utils import seed_worker -from transformers.utils import is_peft_available from trl import GRPOTrainer from trl.data_utils import ( apply_chat_template, is_conversational, maybe_apply_chat_template, ) -from trl.extras.profiling import profiling_context, profiling_decorator -from trl.import_utils import is_deepspeed_available +from trl.extras.profiling import profiling_context from trl.models import unwrap_model_for_generation from trl.trainer.grpo_config import GRPOConfig from trl.trainer.grpo_trainer import RewardFunc, nanstd @@ -52,62 +49,12 @@ # pylint: disable=unused-import from peft import PeftConfig -if is_deepspeed_available(): - import deepspeed - class AxolotlGRPOTrainer(RngLoaderMixin, SchedulerMixin, GRPOTrainer): """Extend the base GRPOTrainer for axolotl helpers""" _tag_names = ["trl", "grpo", "axolotl"] - @profiling_decorator - def _move_model_to_vllm(self): - # For DeepSpeed ZeRO-3, we need to gather all parameters before operations - deepspeed_plugin = self.accelerator.state.deepspeed_plugin - zero_stage_3 = deepspeed_plugin is not None and deepspeed_plugin.zero_stage == 3 - gather_if_zero3 = ( - deepspeed.zero.GatheredParameters if zero_stage_3 else nullcontext - ) - - if is_peft_model(self.model): - # With PEFT and DeepSpeed ZeRO Stage 3, we must gather the full model at once before merging, as merging - # adapters in a sharded manner is not supported. - with gather_if_zero3(list(self.model.parameters())): - self.model.merge_adapter() - - # Update vLLM weights while parameters are gathered - for name, param in self.model.named_parameters(): - # When using PEFT, we need to recover the original parameter name and discard some parameters - name = ( - name.removeprefix("base_model.model.") - .removeprefix("base_model.model.") - .replace(".base_layer", "") - ) - if self.model.prefix in name: - continue - # When module to save, remove its prefix and discard the original module - if "original_module" in name: - continue - name = name.replace("modules_to_save.default.", "") - - if self.accelerator.is_main_process: - self.vllm_client.update_named_param(name, param.data) - - # Unmerge adapters while parameters are still gathered - self.model.unmerge_adapter() - # Parameters will automatically be repartitioned when exiting the context - else: - # For non-PEFT models, simply gather and update each parameter individually. - for name, param in self.model.named_parameters(): - with gather_if_zero3([param]): - if self.accelerator.is_main_process: - self.vllm_client.update_named_param(name, param.data) - - # Reset cache on main process - if self.accelerator.is_main_process: - self.vllm_client.reset_prefix_cache() - class AxolotlGRPOSequenceParallelTrainer(AxolotlGRPOTrainer): """Extend the base GRPOTrainer for sequence parallelism handling""" diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index a1eade5311..575b7a620a 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -166,7 +166,6 @@ def transform_fn(example, tokenizer=None): """ ) - @pytest.mark.skip(reason="flaky test") @pytest.mark.parametrize( "num_gpus", [1, 2], @@ -231,8 +230,6 @@ def test_llama_dora(self, temp_dir, num_gpus): "NCCL_P2P_LEVEL": "LOC", **current_env, "CUDA_VISIBLE_DEVICES": "1", - "VLLM_DISABLE_COMPILE_CACHE": "1", - # "VLLM_USE_V1": "0", } vllm_process = start_vllm( cfg.base_model, @@ -266,7 +263,6 @@ def test_llama_dora(self, temp_dir, num_gpus): finally: recursive_kill(vllm_process) - @pytest.mark.skip(reason="flaky test") @pytest.mark.parametrize( "num_gpus", [1, 2], @@ -325,8 +321,6 @@ def test_llama_fft(self, temp_dir, num_gpus): "NCCL_P2P_LEVEL": "LOC", # nccl can be brittle, assume P2P isn't reliable **current_env, "CUDA_VISIBLE_DEVICES": "1", - "VLLM_DISABLE_COMPILE_CACHE": "1", - # "VLLM_USE_V1": "0", } vllm_process = start_vllm( cfg.base_model, From 6aa41740df7623d1f8995a1efd3b668f4a57c5cf Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 21 May 2025 11:20:20 -0400 Subject: [PATCH 0661/1405] SP dataloader patching + removing custom sampler / dataloader logic (#2686) * utilize accelerate prepare_data_loader with patching * lint * cleanup, fix * update to support DPO quirk * small change * coderabbit commits, cleanup, remove dead code * quarto fix * patch fix * review comments * moving monkeypatch up one level * fix --- _quarto.yml | 1 - docs/multi-gpu.qmd | 15 +- docs/sequence_parallelism.qmd | 6 +- examples/qwen2/dpo.yaml | 1 - src/axolotl/core/trainer_builder.py | 9 - src/axolotl/core/trainers/base.py | 50 +--- src/axolotl/core/trainers/dpo/trainer.py | 157 +----------- src/axolotl/core/trainers/grpo/trainer.py | 2 +- src/axolotl/core/trainers/mixins/__init__.py | 1 - .../core/trainers/mixins/sequence_parallel.py | 87 ------- src/axolotl/core/training_args.py | 13 - .../attention/ring_attn/__init__.py | 11 - .../monkeypatch/attention/ring_attn/patch.py | 131 ---------- src/axolotl/monkeypatch/ring_attn/__init__.py | 22 ++ .../ring_attn/adapters/__init__.py | 0 .../ring_attn/adapters/batch.py | 0 src/axolotl/monkeypatch/ring_attn/patch.py | 223 ++++++++++++++++++ .../utils/ctx_managers/sequence_parallel.py | 24 +- src/axolotl/utils/models.py | 22 +- tests/e2e/patched/test_sp.py | 6 +- 20 files changed, 304 insertions(+), 477 deletions(-) delete mode 100644 src/axolotl/core/trainers/mixins/sequence_parallel.py delete mode 100644 src/axolotl/monkeypatch/attention/ring_attn/__init__.py delete mode 100644 src/axolotl/monkeypatch/attention/ring_attn/patch.py create mode 100644 src/axolotl/monkeypatch/ring_attn/__init__.py rename src/axolotl/monkeypatch/{attention => }/ring_attn/adapters/__init__.py (100%) rename src/axolotl/monkeypatch/{attention => }/ring_attn/adapters/batch.py (100%) create mode 100644 src/axolotl/monkeypatch/ring_attn/patch.py diff --git a/_quarto.yml b/_quarto.yml index dc5071838f..c09aecaeab 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -60,7 +60,6 @@ quartodoc: - core.trainers.mixins.optimizer - core.trainers.mixins.rng_state_loader - core.trainers.mixins.scheduler - - core.trainers.mixins.sequence_parallel - title: Context Managers desc: Context managers for altering trainer behaviors contents: diff --git a/docs/multi-gpu.qmd b/docs/multi-gpu.qmd index 55eaca6c35..fee7d17e5f 100644 --- a/docs/multi-gpu.qmd +++ b/docs/multi-gpu.qmd @@ -87,20 +87,7 @@ We support sequence parallelism (SP) via the allows one to split up sequences across GPUs, which is useful in the event that a single sequence causes OOM errors during model training. -First, install `ring-flash-attn`, recommended via `pip install axolotl[ring-flash-attn]`, -or from source with `pip install .[ring-flash-attn]`. - -Your Axolotl YAML config should contain the following lines: - -```{.yaml} -sequence_parallel_degree: 4 # Split each sequence into 4 parts, one per GPU -flash_attention: true # Required with sequence parallelism - -# Optional; strides across the key dimension. Larger values use more memory but will make training faster. -heads_k_stride: 1 -``` - -See our [dedicated guide](sequence_parallelism.qmd) for more details. +See our [dedicated guide](sequence_parallelism.qmd) for more information. ### FSDP + QLoRA {#sec-fsdp-qlora} diff --git a/docs/sequence_parallelism.qmd b/docs/sequence_parallelism.qmd index 1bff17ce9b..b98206135a 100644 --- a/docs/sequence_parallelism.qmd +++ b/docs/sequence_parallelism.qmd @@ -41,7 +41,7 @@ When sequence parallelism is enabled: 1. Each sequence is divided into equal chunks across the GPUs in a sequence parallel group 2. The data collator handles the chunking of input_ids, attention_mask, labels, and position_ids -3. Position IDs are adjusted to maintain proper relative positions, especially for packed sequences +3. Position IDs are adjusted to maintain proper relative positions 4. The trainer uses special ring communication patterns for attention operations ## Requirements @@ -67,9 +67,11 @@ sequence_len: 8192 ... sequence_parallel_degree: 4 # Split each sequence into 4 parts, one per GPU -flash_attention: true # Required with sequence parallelism # Optional; strides across the key dimension. Larger values use more memory but should make training faster. heads_k_stride: 1 +# Optional; one of "varlen_llama3" or "batch_ring". Defaults to +# "varlen_llama3" when `sample_packing: true`, and "batch_ring" otherwise. +ring_attn_func: ... ``` diff --git a/examples/qwen2/dpo.yaml b/examples/qwen2/dpo.yaml index 3547c6c98d..bd896c2b3d 100644 --- a/examples/qwen2/dpo.yaml +++ b/examples/qwen2/dpo.yaml @@ -2,7 +2,6 @@ base_model: Qwen/Qwen2.5-0.5B # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name - chat_template: qwen_25 rl: dpo datasets: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index d82e4d20b2..878dd176a9 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -798,11 +798,6 @@ def build(self, total_num_steps): self.cfg.kd_top_k_before_softmax ) - training_arguments_kwargs["sequence_parallel_degree"] = ( - self.cfg.sequence_parallel_degree - ) - training_arguments_kwargs["ring_attn_func"] = self.cfg.ring_attn_func - if self.cfg.reward_model: training_args_cls = AxolotlRewardConfig elif self.cfg.process_reward_model: @@ -1083,10 +1078,6 @@ def build_training_arguments(self, total_num_steps): if self.cfg.use_wandb: training_args_kwargs["run_name"] = self.cfg.wandb_name - training_args_kwargs["sequence_parallel_degree"] = ( - self.cfg.sequence_parallel_degree - ) - training_args_cls = None blocklist_args_kwargs = [] if self.cfg.rl is RLType.SIMPO: diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 2f0ce68949..d5cfc23df0 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -29,7 +29,6 @@ OptimizerMixin, RngLoaderMixin, SchedulerMixin, - SequenceParallelMixin, ) from axolotl.core.trainers.utils import ( sanitize_kwargs_for_ds_tagging, @@ -40,9 +39,7 @@ LOG = logging.getLogger(__name__) -class AxolotlTrainer( - SchedulerMixin, OptimizerMixin, RngLoaderMixin, SequenceParallelMixin, Trainer -): +class AxolotlTrainer(SchedulerMixin, OptimizerMixin, RngLoaderMixin, Trainer): """Extend the base Trainer for axolotl helpers""" args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] @@ -68,10 +65,6 @@ def __init__( if self.args.orpo_alpha: self.loss_fct = torch.nn.CrossEntropyLoss(reduction="none") - # Initialize sequence parallelism if enabled - if self.args.sequence_parallel_degree > 1: - self._setup_sequence_parallel() - def _wrap_model(self, model, training=True, dataloader=None): if self.args.torch_compile: torch._dynamo.config.accumulated_cache_size_limit = ( # pylint: disable=protected-access @@ -122,8 +115,8 @@ def _create_multipack_sampler( def _get_train_sampler(self) -> Sampler | None: """ - Helper method to get the sampler for training. Handles cases for sequence - parallelism, sample packing, and curriculum sampling (sequential). + Helper method to get the sampler for training. Handles cases for sample packing + and curriculum sampling (sequential). Returns: If the dataset is non-empty, a sampler is returned, the type of which @@ -132,9 +125,7 @@ def _get_train_sampler(self) -> Sampler | None: use_sample_packing = self.args.sample_packing and not self.args.pretraining # Determine the base sampler first - if self.args.sequence_parallel_degree > 1: - base_sampler = self._sp_get_train_sampler(self.train_dataset) - elif self.args.curriculum_sampling: + if self.args.curriculum_sampling: base_sampler = SequentialSampler(self.train_dataset) elif use_sample_packing: base_sampler = RandomSampler(self.train_dataset) @@ -153,8 +144,7 @@ def _get_train_sampler(self) -> Sampler | None: def _get_eval_sampler(self, eval_dataset: Dataset | None = None) -> Sampler | None: """ - Helper method to get the sampler for evaluation. Handles sequence parallelism - and sample packing cases. + Helper method to get the sampler for evaluation. Handles sample packing case. Returns: If the dataset is non-empty, a sampler is returned, the type of which @@ -168,9 +158,7 @@ def _get_eval_sampler(self, eval_dataset: Dataset | None = None) -> Sampler | No ) # Determine the base sampler - if self.args.sequence_parallel_degree > 1: - base_sampler = self._sp_get_eval_sampler(eval_dataset) - elif use_multipack: + if use_multipack: base_sampler = SequentialSampler(eval_dataset) else: return super()._get_eval_sampler(eval_dataset) @@ -236,14 +224,6 @@ def _prepare_dataloader( ): self.accelerator.even_batches = False - # Return unprepared dataloader if using sequence parallelism - # TODO(djsaunde): We might be able to use `accelerate`'s dataloader preparation - # if we use `dispatch_batches` and `slice_fn_for_dispatch` properly (i.e., - # slice each batch along the sequence dimension). - if self.args.sequence_parallel_degree > 1: - return dataloader - - # Otherwise prepare with accelerator return self.accelerator.prepare_data_loader(dataloader) def get_train_dataloader(self) -> DataLoader: @@ -287,12 +267,7 @@ def get_eval_dataloader(self, eval_dataset: Dataset | None = None) -> DataLoader return dataloader - # Handle sample packing or sequence parallelism - if ( - self.args.sample_packing - and self.args.eval_sample_packing is not False - or self.args.sequence_parallel_degree > 1 - ): + if self.args.sample_packing and self.args.eval_sample_packing is not False: # Get appropriate data collator self.data_collator = ( # pylint: disable=attribute-defined-outside-init self.eval_data_collator @@ -302,17 +277,6 @@ def get_eval_dataloader(self, eval_dataset: Dataset | None = None) -> DataLoader if "length" in eval_dataset.column_names: eval_dataset = eval_dataset.remove_columns(["length"]) - # Handle dataset preprocessing for SP - if self.args.sequence_parallel_degree > 1: - if isinstance(eval_dataset, datasets.Dataset): - eval_dataset = self._remove_unused_columns( - eval_dataset, description="evaluation" - ) - else: - self.data_collator = self._get_collator_with_removed_columns( # pylint: disable=attribute-defined-outside-init - self.data_collator, description="evaluation" - ) - # Use eval_batch_size for sample packing, per_device_eval_batch_size otherwise batch_size = ( self.args.eval_batch_size diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index 1ce7deea75..c2c80c0bcd 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -1,31 +1,15 @@ -""" -DPO trainer for axolotl -""" +"""DPO trainer for axolotl""" import gc -import random from functools import wraps -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Union -import pandas as pd import torch -import wandb -from accelerate import PartialState -from datasets import Dataset, IterableDataset from peft.optimizers import create_loraplus_optimizer from torch import nn -from torch.utils.data import DataLoader -from transformers import ( - BaseImageProcessor, - FeatureExtractionMixin, - PreTrainedTokenizerBase, - ProcessorMixin, - Trainer, -) -from transformers.trainer_utils import EvalLoopOutput +from transformers import Trainer from transformers.utils import is_sagemaker_mp_enabled -from trl import DPOConfig, DPOTrainer, maybe_apply_chat_template, maybe_extract_prompt -from trl.trainer.utils import log_table_to_comet_experiment +from trl import DPOTrainer from axolotl.core.trainers.mixins import RngLoaderMixin, SchedulerMixin from axolotl.core.trainers.utils import ( @@ -38,9 +22,7 @@ class AxolotlDPOTrainer(RngLoaderMixin, SchedulerMixin, DPOTrainer): - """ - Extend the base DPOTrainer for axolotl helpers - """ + """Extend the base DPOTrainer for axolotl helpers.""" tag_names = ["axolotl", "dpo"] @@ -85,8 +67,9 @@ def create_optimizer(self): @wraps(DPOTrainer.push_to_hub) def push_to_hub(self, *args, **kwargs) -> str: """ - Overwrite the `push_to_hub` method in order to force-add the tags when pushing the - model on the Hub. Please refer to `~transformers.Trainer.push_to_hub` for more details. + Overwrite the `push_to_hub` method in order to force-add the tags when pushing + the model on the Hub. Please refer to `~transformers.Trainer.push_to_hub` + for more details. """ kwargs = sanitize_kwargs_for_ds_tagging( dataset_tags=self.dataset_tags, kwargs=kwargs @@ -95,64 +78,6 @@ def push_to_hub(self, *args, **kwargs) -> str: return super().push_to_hub(*args, **kwargs) - # TODO: remove this once https://github.com/huggingface/trl/pull/3377 is in a release - def _prepare_dataset( - self, - dataset: Union[Dataset, IterableDataset], - processing_class: Union[ - PreTrainedTokenizerBase, - BaseImageProcessor, - FeatureExtractionMixin, - ProcessorMixin, - ], - args: DPOConfig, - dataset_name: str, - ) -> Union[Dataset, IterableDataset]: - # Build the kwargs for the `map` function - map_kwargs: Dict[str, Any] = {"writer_batch_size": 10} - if isinstance(dataset, Dataset): # IterableDataset does not support num_proc - map_kwargs["num_proc"] = args.dataset_num_proc - - with PartialState().main_process_first(): - # Extract prompt if needed - if isinstance( - dataset, Dataset - ): # `IterableDataset.map` does not support `desc` - map_kwargs["desc"] = f"Extracting prompt in {dataset_name} dataset" - dataset = dataset.map(maybe_extract_prompt, **map_kwargs) - - # Apply the chat template if needed - if isinstance( - dataset, Dataset - ): # `IterableDataset.map` does not support `desc` - map_kwargs["desc"] = f"Applying chat template to {dataset_name} dataset" - dataset = dataset.map( - maybe_apply_chat_template, - fn_kwargs={"tokenizer": processing_class, "tools": args.tools}, - **map_kwargs, - ) - - # Tokenize the dataset - if isinstance( - dataset, Dataset - ): # `IterableDataset.map` does not support `desc` - map_kwargs["desc"] = f"Tokenizing {dataset_name} dataset" - - dataset = dataset.map( - self.tokenize_row if not self.is_vision_model else self.process_row, - remove_columns=["chosen", "rejected"], - fn_kwargs={ - "processing_class": processing_class, - "max_prompt_length": args.max_prompt_length, - "max_completion_length": args.max_completion_length, - # for enc-dec, we add the special tokens ([bos_token] + prompt + [eos_token]; completion + [eos_token]) - "add_special_tokens": False, - }, - **map_kwargs, - ) - - return dataset - @staticmethod def tokenize_row( features, @@ -192,69 +117,3 @@ def training_step( gc.collect() torch.cuda.empty_cache() return loss - - # TODO: remove this once https://github.com/huggingface/trl/pull/3377 is in a release - def evaluation_loop( - self, - dataloader: DataLoader, - description: str, - prediction_loss_only: Optional[bool] = None, - ignore_keys: Optional[list[str]] = None, - metric_key_prefix: str = "eval", - ) -> EvalLoopOutput: - """ - Overriding built-in evaluation loop to store metrics for each batch. - Prediction/evaluation loop, shared by `Trainer.evaluate()` and `Trainer.predict()`. - - Works both with or without labels. - """ - - # Sample and save to game log if requested (for one batch to save time) - if self.generate_during_eval: - # Generate random indices within the range of the total number of samples - num_samples = len(dataloader.dataset) - random_indices = random.sample( - range(num_samples), k=self.args.eval_batch_size - ) - - # Use dataloader.dataset.select to get the random batch without iterating over the DataLoader - random_batch_dataset = dataloader.dataset.select(random_indices) - random_batch = self.data_collator(random_batch_dataset) - random_batch = self._prepare_inputs(random_batch) - - policy_output_decoded, ref_output_decoded = ( - self.generate_from_model_and_ref(self.model, random_batch) - ) - - table = pd.DataFrame( - columns=["Prompt", "Policy", "Ref Model"], - data=[ - [prompt, pol[len(prompt) :], ref[len(prompt) :]] - for prompt, pol, ref in zip( - random_batch_dataset["prompt"], - policy_output_decoded, - ref_output_decoded, - ) - ], - ) - if "wandb" in self.args.report_to and self.accelerator.is_main_process: - wandb.log({"game_log": wandb.Table(data=table)}) - - if "comet_ml" in self.args.report_to: - log_table_to_comet_experiment( - name="game_log.csv", - table=table, - ) - - # Base evaluation - initial_output = super( # pylint: disable=bad-super-call - DPOTrainer, self - ).evaluation_loop( - dataloader, - description, - prediction_loss_only, - ignore_keys, - metric_key_prefix, - ) - - return initial_output diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index 8a89de333a..a603ed8606 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -43,7 +43,7 @@ from axolotl.core.trainers.grpo.sampler import SequenceParallelRepeatRandomSampler from axolotl.core.trainers.mixins import RngLoaderMixin, SchedulerMixin -from axolotl.monkeypatch.attention.ring_attn.patch import get_ring_attn_group +from axolotl.monkeypatch.ring_attn.patch import get_ring_attn_group if is_peft_available(): # pylint: disable=unused-import diff --git a/src/axolotl/core/trainers/mixins/__init__.py b/src/axolotl/core/trainers/mixins/__init__.py index 44751b4653..a71cb321ab 100644 --- a/src/axolotl/core/trainers/mixins/__init__.py +++ b/src/axolotl/core/trainers/mixins/__init__.py @@ -6,4 +6,3 @@ from .optimizer import OptimizerMixin from .rng_state_loader import RngLoaderMixin from .scheduler import SchedulerMixin -from .sequence_parallel import SequenceParallelMixin diff --git a/src/axolotl/core/trainers/mixins/sequence_parallel.py b/src/axolotl/core/trainers/mixins/sequence_parallel.py deleted file mode 100644 index 0f30458cdc..0000000000 --- a/src/axolotl/core/trainers/mixins/sequence_parallel.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Module for Axolotl trainer sequence parallelism mixin""" - -import torch.distributed as dist -from datasets import Dataset -from torch.utils.data import DistributedSampler, Sampler - -from axolotl.monkeypatch.attention.ring_attn import ( - get_ring_attn_group, -) - - -class SequenceParallelMixin: - """ - Mixin class for sequence parallelism support in trainers. - - This mixin provides functionality for handling sequence parallelism, - specifically for creating appropriate data samplers. - """ - - args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] - - def _setup_sequence_parallel(self): - """Set up sequence parallelism environment.""" - self.ring_attn_group = get_ring_attn_group() - - def _create_sequence_parallel_sampler( - self, - dataset: Dataset, - shuffle: bool = True, - is_eval: bool = False, - ) -> DistributedSampler: - """ - Helper method to create sampler for sequence parallelism (SP). - - We create a distributed sampler with rank equal to the SP group ID, which - means that all ranks in the SP group receive the same sample / set of samples - per training step. We also set the number of replicas equal to the number of - SP groups, which is a bit of a hack / unintended use, but works! - - Args: - dataset: Dataset to sample from. - shuffle: Whether to shuffle the dataset. - is_eval: Whether we are creating a sampler for evaluation or training. - - Returns: - Distributed sampler. - """ - num_sp_groups = self.args.world_size // self.args.sequence_parallel_degree - sp_group_id = dist.get_rank() // self.args.sequence_parallel_degree - - return DistributedSampler( - dataset, - num_replicas=num_sp_groups, - rank=sp_group_id, - seed=self.args.seed if shuffle else None, - shuffle=shuffle, - drop_last=not is_eval, - ) - - def _sp_get_train_sampler(self, dataset) -> Sampler | None: - """ - Get a training sampler configured for sequence parallelism. - - Args: - dataset: The training dataset - - Returns: - Configured sequence parallel sampler. - """ - return self._create_sequence_parallel_sampler( - dataset, - shuffle=not self.args.curriculum_sampling, - ) - - def _sp_get_eval_sampler(self, eval_dataset) -> Sampler | None: - """ - Get an evaluation sampler configured for sequence parallelism. - - Args: - eval_dataset: The evaluation dataset. - - Returns: - Configured sequence parallel sampler. - """ - return self._create_sequence_parallel_sampler( - eval_dataset, shuffle=False, is_eval=True - ) diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index a81c33801c..9c93f77c7e 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -9,8 +9,6 @@ from transformers import TrainingArguments from trl import CPOConfig, KTOConfig, ORPOConfig, PRMConfig, RewardConfig -from axolotl.utils.schemas.enums import RingAttnFunc - @dataclass class AxolotlTrainingMixins: @@ -216,17 +214,6 @@ class AxolotlTrainingMixins: }, ) - sequence_parallel_degree: Optional[int] = field( - default=1, - metadata={"help": "The number of workers to use in sequence parallelism"}, - ) - ring_attn_func: Optional[RingAttnFunc] = field( - default=None, - metadata={ - "help": "The ring-flash-attn function to use in sequence parallelism" - }, - ) - adam_beta3: Optional[float] = field( default=None, metadata={ diff --git a/src/axolotl/monkeypatch/attention/ring_attn/__init__.py b/src/axolotl/monkeypatch/attention/ring_attn/__init__.py deleted file mode 100644 index a50ad456ea..0000000000 --- a/src/axolotl/monkeypatch/attention/ring_attn/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Init for ring attention monkeypatch module""" - -# pylint: disable=unused-import -# flake8: noqa - -from .patch import ( - get_ring_attn_group, - register_ring_attn, - set_ring_attn_group, - update_ring_attn_params, -) diff --git a/src/axolotl/monkeypatch/attention/ring_attn/patch.py b/src/axolotl/monkeypatch/attention/ring_attn/patch.py deleted file mode 100644 index 8cbba338a1..0000000000 --- a/src/axolotl/monkeypatch/attention/ring_attn/patch.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -Ring attention group registration and flash attention patching. - -Make use of the `ring-flash-attn` (https://github.com/zhuzilin/ring-flash-attention) -package, specifically the `hf_adapter.substitute_hf_flash_attn` function to patch in -their sequence parallel version of Flash Attention 2. -""" - -import torch -import torch.distributed as dist -from accelerate.logging import get_logger - -from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids -from axolotl.utils.schemas.enums import RingAttnFunc - -LOG = get_logger(__name__) - - -RING_ATTN_GROUP = None - - -def get_ring_attn_group() -> dist.ProcessGroup: - """ - Getter for ring attention group on this rank. - - Returns: - The process group for ring attention for this rank. - """ - return RING_ATTN_GROUP - - -def set_ring_attn_group(ring_attn_group: dist.ProcessGroup | None): - """ - Setter for ring attention group on this rank. - - Args: - Process group for ring attention. - """ - global RING_ATTN_GROUP # pylint: disable=global-statement - RING_ATTN_GROUP = ring_attn_group - - -def register_ring_attn( - sequence_parallel_degree: int, - heads_k_stride: int | None, - ring_attn_func: RingAttnFunc | None, -): - """ - Create ring attention group and substitute flash attn with ring flash attn. - - Args: - sequence_parallel_degree: Sequence parallelism factor. - heads_k_stride: Sequence parallelism K head stride size. Passed - through to `ring_flash_attn.substitute_hf_flash_attn`. - ring_attn_func: `ring_flash_attn` ring attention implemention. If sample - packing is enabled, it must be a `varlen` function; otherwise, it must be a - `batch` function. - """ - if get_ring_attn_group() is not None: - LOG.info("Ring attention already registered, exiting early...") - return - - LOG.info( - "Enabling ring attention sequence parallelism: " - f"each sequence will be processed across {sequence_parallel_degree} GPUs" - ) - - rank = dist.get_rank() - world_size = dist.get_world_size() - - assert sequence_parallel_degree <= world_size, ( - f"sequence_parallel_degree ({sequence_parallel_degree}) " - f"must be less than or equal to world_size ({world_size})" - ) - assert world_size % sequence_parallel_degree == 0, ( - f"sequence_parallel_degree ({sequence_parallel_degree}) " - f"must evenly divide world_size ({world_size})" - ) - - # Assign ranks to sequence parallel groups - group_assignments = {} - for i in range(world_size // sequence_parallel_degree): - ring_attn_ranks = list( - range( - i * sequence_parallel_degree, - (i + 1) * sequence_parallel_degree, - ) - ) - group = dist.new_group(ranks=ring_attn_ranks, backend="nccl") - - # Track which GPUs are in which groups - for r in ring_attn_ranks: - group_assignments[r] = i - - if rank in ring_attn_ranks: - set_ring_attn_group(group) - - # Log the GPU group assignments - if rank == 0: - LOG.info(f"Sequence parallel group assignments: {group_assignments}") - - if ring_attn_func is RingAttnFunc.VARLEN_LLAMA3: - from ring_flash_attn import substitute_hf_flash_attn - - substitute_hf_flash_attn( - process_group=get_ring_attn_group(), heads_k_stride=heads_k_stride or 1 - ) - elif ring_attn_func is RingAttnFunc.BATCH_RING: - from axolotl.monkeypatch.attention.ring_attn.adapters.batch import ( - substitute_hf_flash_attn, - ) - - substitute_hf_flash_attn( - process_group=get_ring_attn_group(), - ring_attn_func=ring_attn_func, - ) - - -def update_ring_attn_params(position_ids: torch.Tensor | None): - """ - Calculate the cumulative sequence lengths for the current forward pass and pass the - value to the substituted `ring_flash_attn`. - - Args: - position_ids: Optional tensor of position IDs (for sample packed data). - """ - from ring_flash_attn import update_ring_flash_attn_params - - cu_seqlens, _ = get_cu_seqlens_from_pos_ids(position_ids) - cu_seqlens = cu_seqlens.squeeze().to(device=torch.cuda.current_device()) - update_ring_flash_attn_params(cu_seqlens, get_ring_attn_group()) diff --git a/src/axolotl/monkeypatch/ring_attn/__init__.py b/src/axolotl/monkeypatch/ring_attn/__init__.py new file mode 100644 index 0000000000..5833b9ce43 --- /dev/null +++ b/src/axolotl/monkeypatch/ring_attn/__init__.py @@ -0,0 +1,22 @@ +"""Init for ring attention monkeypatch module""" + +# pylint: disable=unused-import +# flake8: noqa + +from .patch import ( + get_ring_attn_group, + patch_prepare_data_loader, + patch_prepare_device_mesh, + register_ring_attn, + set_ring_attn_group, + update_ring_attn_params, +) + +__all__ = ( + "get_ring_attn_group", + "patch_prepare_data_loader", + "patch_prepare_device_mesh", + "register_ring_attn", + "set_ring_attn_group", + "update_ring_attn_params", +) diff --git a/src/axolotl/monkeypatch/attention/ring_attn/adapters/__init__.py b/src/axolotl/monkeypatch/ring_attn/adapters/__init__.py similarity index 100% rename from src/axolotl/monkeypatch/attention/ring_attn/adapters/__init__.py rename to src/axolotl/monkeypatch/ring_attn/adapters/__init__.py diff --git a/src/axolotl/monkeypatch/attention/ring_attn/adapters/batch.py b/src/axolotl/monkeypatch/ring_attn/adapters/batch.py similarity index 100% rename from src/axolotl/monkeypatch/attention/ring_attn/adapters/batch.py rename to src/axolotl/monkeypatch/ring_attn/adapters/batch.py diff --git a/src/axolotl/monkeypatch/ring_attn/patch.py b/src/axolotl/monkeypatch/ring_attn/patch.py new file mode 100644 index 0000000000..4329d9f138 --- /dev/null +++ b/src/axolotl/monkeypatch/ring_attn/patch.py @@ -0,0 +1,223 @@ +"""Ring attention group registration and flash attention patching. + +Make use of the `ring-flash-attn` (https://github.com/zhuzilin/ring-flash-attention) +package, specifically the `hf_adapter.substitute_hf_flash_attn` function to patch in +their sequence parallel version of Flash Attention 2. + +We also provide some patches for accelerate functions to prepare the dataloader for +sequence parallelism training. +""" + +import inspect + +import accelerate +import torch +import torch.distributed as dist +from accelerate.logging import get_logger + +from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids +from axolotl.utils.schemas.enums import RingAttnFunc + +LOG = get_logger(__name__) + + +RING_ATTN_GROUP = None + +ORIGINAL_PREPARE_DATALOADER_CODE = """ submesh_fsdp_size = 1 + submesh_dp_size = 1 + submesh_tp_size = 1 + if "tp" in torch_device_mesh.mesh_dim_names: + submesh_tp_size = torch_device_mesh["tp"].size() + if "dp" in torch_device_mesh.mesh_dim_names: + submesh_dp_size = torch_device_mesh["dp"].size() + if "fsdp" in torch_device_mesh.mesh_dim_names: + submesh_fsdp_size = torch_device_mesh["fsdp"].size() + process_index = process_index // submesh_tp_size""" + +NEW_PREPARE_DATALOADER_CODE = """ submesh_fsdp_size = 1 + submesh_dp_size = 1 + submesh_tp_size = 1 + submesh_cp_size = 1 + if "cp" in torch_device_mesh.mesh_dim_names: + submesh_cp_size = torch_device_mesh["cp"].size() + if "tp" in torch_device_mesh.mesh_dim_names: + submesh_tp_size = torch_device_mesh["tp"].size() + if "dp" in torch_device_mesh.mesh_dim_names: + submesh_dp_size = torch_device_mesh["dp"].size() + if "fsdp" in torch_device_mesh.mesh_dim_names: + submesh_fsdp_size = torch_device_mesh["fsdp"].size() + process_index = process_index // (submesh_tp_size * submesh_cp_size)""" + + +def get_ring_attn_group() -> dist.ProcessGroup: + """Getter for ring attention group on this rank.""" + return RING_ATTN_GROUP + + +def set_ring_attn_group(ring_attn_group: dist.ProcessGroup | None): + """Setter for ring attention group on this rank.""" + global RING_ATTN_GROUP # pylint: disable=global-statement + RING_ATTN_GROUP = ring_attn_group + + +def register_ring_attn( + sequence_parallel_degree: int, + heads_k_stride: int | None, + ring_attn_func: RingAttnFunc | None, +): + """Create ring attention group and substitute flash attn with ring flash attn. + + Args: + sequence_parallel_degree: Sequence parallelism factor. + heads_k_stride: Sequence parallelism K head stride size. Passed + through to `ring_flash_attn.substitute_hf_flash_attn`. + ring_attn_func: `ring_flash_attn` ring attention implemention. If sample + packing is enabled, it must be a `varlen` function; otherwise, it must be a + `batch` function. + """ + rank = dist.get_rank() + world_size = dist.get_world_size() + + if rank == 0: + LOG.info( + "Enabling ring attention sequence parallelism: " + f"each sequence will be processed across {sequence_parallel_degree} GPUs" + ) + + assert sequence_parallel_degree <= world_size, ( + f"sequence_parallel_degree ({sequence_parallel_degree}) " + f"must be less than or equal to world_size ({world_size})" + ) + assert world_size % sequence_parallel_degree == 0, ( + f"sequence_parallel_degree ({sequence_parallel_degree}) " + f"must evenly divide world_size ({world_size})" + ) + + # Assign ranks to sequence parallel groups + group_assignments = {} + for i in range(world_size // sequence_parallel_degree): + ring_attn_ranks = list( + range( + i * sequence_parallel_degree, + (i + 1) * sequence_parallel_degree, + ) + ) + group = dist.new_group(ranks=ring_attn_ranks, backend="nccl") + + # Track which GPUs are in which groups + for r in ring_attn_ranks: + group_assignments[r] = i + + if rank in ring_attn_ranks: + set_ring_attn_group(group) + + # Log the GPU group assignments + if rank == 0: + LOG.info(f"Sequence parallel group assignments: {group_assignments}") + + if ring_attn_func is RingAttnFunc.VARLEN_LLAMA3: + from ring_flash_attn import substitute_hf_flash_attn + + substitute_hf_flash_attn( + process_group=get_ring_attn_group(), heads_k_stride=heads_k_stride or 1 + ) + elif ring_attn_func is RingAttnFunc.BATCH_RING: + from axolotl.monkeypatch.ring_attn.adapters.batch import ( + substitute_hf_flash_attn, + ) + + substitute_hf_flash_attn( + process_group=get_ring_attn_group(), + ring_attn_func=ring_attn_func, + ) + + +def update_ring_attn_params(position_ids: torch.Tensor | None): + """ + Calculate the cumulative sequence lengths for the current forward pass and pass the + value to the substituted `ring_flash_attn`. + + Args: + position_ids: Optional tensor of position IDs (for sample packed data). + """ + from ring_flash_attn import update_ring_flash_attn_params + + cu_seqlens, _ = get_cu_seqlens_from_pos_ids(position_ids) + cu_seqlens = cu_seqlens.squeeze().to(device=torch.cuda.current_device()) + update_ring_flash_attn_params(cu_seqlens, get_ring_attn_group()) + + +def patch_prepare_data_loader(): + """Patch `accelerate.data_loader.prepare_data_loader` to respect the SP degree. + + Raies: + RuntimeError: If source code to patch does not exist. + """ + original_fn = accelerate.data_loader.prepare_data_loader + original_source = inspect.getsource(original_fn) + + if ORIGINAL_PREPARE_DATALOADER_CODE not in original_source: + raise RuntimeError( + "SP patch failed - target snippet not found. " + "Check accelerate's version or update the patch." + ) + + patched_source = original_source.replace( + ORIGINAL_PREPARE_DATALOADER_CODE, NEW_PREPARE_DATALOADER_CODE + ) + + # Create a new function from the patched source + namespace = {} + exec( # pylint: disable=exec-used # nosec B102 + patched_source, accelerate.data_loader.__dict__, namespace + ) + patched_function = namespace["prepare_data_loader"] + + accelerate.data_loader.prepare_data_loader = patched_function + LOG.info("Patched accelerate.data_loader.prepare_data_loader for SP support") + + +def patch_prepare_device_mesh(sequence_parallel_degree: int): + """Patches the `Accelerator._prepare_device_mesh` method to create a device mesh + that includes sequence parallelism with the specified degree. + + Args: + sequence_parallel_degree (int): The degree of sequence parallelism to use. + """ + + def _prepare_device_mesh(self): + """Prepare the device mesh for distributed training. The dataloader will + determine how to load data based on the device mesh. + """ + if self.state.torch_tp_plugin: + return self.state.torch_tp_plugin.torch_device_mesh + if ( + self.distributed_type == accelerate.accelerator.DistributedType.DEEPSPEED + and hasattr(self.state, "ds_device_mesh") + ): + return self.state.ds_device_mesh + + # Create device mesh with sequence parallelism + world_size = dist.get_world_size() + mesh_shape = ( + world_size // sequence_parallel_degree, + sequence_parallel_degree, + ) + device_ids = list(range(world_size)) + + # Note that we use "cp" instead of "sp" to match the PyTorch native "context + # parallelism" implementation naming + return dist.DeviceMesh( + "cuda", + torch.tensor(device_ids).reshape(mesh_shape), + mesh_dim_names=("dp", "cp"), + ) + + # Replace the original method with our new method + # pylint: disable=protected-access + accelerate.accelerator.Accelerator._prepare_device_mesh = _prepare_device_mesh + + LOG.info( + "Successfully patched Accelerator._prepare_device_mesh " + f"with sequence_parallel_degree={sequence_parallel_degree}" + ) diff --git a/src/axolotl/utils/ctx_managers/sequence_parallel.py b/src/axolotl/utils/ctx_managers/sequence_parallel.py index 66044f7f03..6e4f9badad 100644 --- a/src/axolotl/utils/ctx_managers/sequence_parallel.py +++ b/src/axolotl/utils/ctx_managers/sequence_parallel.py @@ -1,6 +1,7 @@ """Module for Axolotl trainer sequence parallelism manager and utilities""" import functools +import inspect import torch import torch.distributed as dist @@ -9,7 +10,7 @@ from transformers.modeling_outputs import CausalLMOutputWithPast from transformers.utils import ModelOutput -from axolotl.monkeypatch.attention.ring_attn.patch import ( +from axolotl.monkeypatch.ring_attn.patch import ( get_ring_attn_group, update_ring_attn_params, ) @@ -206,12 +207,25 @@ def __init__( def __enter__(self): # Forward pre-hook to apply sequence parallelism def sequence_parallel_pre_hook(_, args, kwargs): - # Apply sequence parallelism to kwargs and get original sequence length and padding info - kwargs, self.original_seq_len, self.pad_len = ( - self.apply_sequence_parallelism(batch=kwargs) + # Get parameter names from the model's forward function + forward_params = list( + inspect.signature(self.models[0].forward).parameters.keys() ) - return args, kwargs + updated_kwargs = kwargs.copy() + for i, arg in enumerate(args): + if i < len(forward_params): + updated_kwargs[forward_params[i]] = arg + + # Any excess positional arguments are kept as-is + remaining_args = args[len(forward_params) :] + + # Apply sequence parallelism to updated kwargs + updated_kwargs, self.original_seq_len, self.pad_len = ( + self.apply_sequence_parallelism(updated_kwargs) + ) + + return remaining_args, updated_kwargs # Forward post-hook to gather outputs def sequence_parallel_post_hook(_, __, output: ModelOutput) -> ModelOutput: diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 316fbec8ce..6236f78e84 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -59,6 +59,7 @@ SUPPORTED_MULTIPACK_MODEL_TYPES, patch_for_multipack, ) +from axolotl.monkeypatch.ring_attn.patch import get_ring_attn_group from axolotl.prompt_tokenizers import LLAMA_DEFAULT_EOS_TOKEN from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.chat_templates import get_chat_template_from_config @@ -681,16 +682,25 @@ def apply_patches(self) -> None: patch_self_attn_lora(self.cfg) if self.cfg.sequence_parallel_degree and self.cfg.sequence_parallel_degree > 1: - from axolotl.monkeypatch.attention.ring_attn import register_ring_attn + from axolotl.monkeypatch.ring_attn import ( + patch_prepare_data_loader, + patch_prepare_device_mesh, + register_ring_attn, + ) # Initialize ring attn for sequence parallelism. This must be done after # model init but before the first forward pass, since it modifies flash # attn to use ring comm for SP training across multiple GPUs. - register_ring_attn( - sequence_parallel_degree=self.cfg.sequence_parallel_degree, - heads_k_stride=self.cfg.heads_k_stride, - ring_attn_func=self.cfg.ring_attn_func, - ) + if get_ring_attn_group() is None: # If already set, this is already patched + register_ring_attn( + sequence_parallel_degree=self.cfg.sequence_parallel_degree, + heads_k_stride=self.cfg.heads_k_stride, + ring_attn_func=self.cfg.ring_attn_func, + ) + patch_prepare_data_loader() + patch_prepare_device_mesh( + sequence_parallel_degree=self.cfg.sequence_parallel_degree + ) def patch_attention(self) -> None: if hasattr(self.model_config, "model_type"): diff --git a/tests/e2e/patched/test_sp.py b/tests/e2e/patched/test_sp.py index 8efe629409..83faa779f3 100644 --- a/tests/e2e/patched/test_sp.py +++ b/tests/e2e/patched/test_sp.py @@ -10,7 +10,7 @@ import torch from accelerate.state import PartialState -from axolotl.monkeypatch.attention.ring_attn import ( +from axolotl.monkeypatch.ring_attn import ( get_ring_attn_group, register_ring_attn, set_ring_attn_group, @@ -313,13 +313,13 @@ def mock_distributed(self, monkeypatch): # Mock the process group monkeypatch.setattr( - "axolotl.monkeypatch.attention.ring_attn.get_ring_attn_group", + "axolotl.monkeypatch.ring_attn.get_ring_attn_group", MagicMock, ) # Mock update_ring_attn_params monkeypatch.setattr( - "axolotl.monkeypatch.attention.ring_attn.update_ring_attn_params", + "axolotl.monkeypatch.ring_attn.update_ring_attn_params", lambda **kwargs: None, ) From 1c83a1a02081834eb84afa7292eef479133541fa Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 22 May 2025 19:18:27 +0700 Subject: [PATCH 0662/1405] feat(doc): clarify minimum pytorch and cuda to use blackwell (#2704) [skip ci] --- docs/docker.qmd | 4 ++++ docs/installation.qmd | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/docs/docker.qmd b/docs/docker.qmd index e208d3222d..d665eaf5b8 100644 --- a/docs/docker.qmd +++ b/docs/docker.qmd @@ -8,6 +8,10 @@ format: This section describes the different Docker images that are released by AxolotlAI at [Docker Hub](https://hub.docker.com/u/axolotlai). +::: {.callout-important} +For Blackwell GPUs, please use the tags with Pytorch 2.7.0 and CUDA 12.8. +::: + ## Base The base image is the most minimal image that can install Axolotl. It is based on the `nvidia/cuda` image. It includes python, torch, git, git-lfs, awscli, pydantic, and more. diff --git a/docs/installation.qmd b/docs/installation.qmd index 0cf5ffcebd..b429992b6a 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -25,6 +25,10 @@ Please make sure to have Pytorch installed before installing Axolotl in your loc Follow the instructions at: [https://pytorch.org/get-started/locally/](https://pytorch.org/get-started/locally/) ::: +::: {.callout-important} +For Blackwell GPUs, please use Pytorch 2.7.0 and CUDA 12.8. +::: + ### PyPI Installation (Recommended) {#sec-pypi} ```{.bash} @@ -72,6 +76,10 @@ docker run --privileged --gpus '"all"' --shm-size 10g --rm -it \ ``` ::: +::: {.callout-important} +For Blackwell GPUs, please use `axolotlai/axolotl:main-py3.11-cu128-2.7.0` or the cloud variant `axolotlai/axolotl-cloud:main-py3.11-cu128-2.7.0`. +::: + Please refer to the [Docker documentation](docker.qmd) for more information on the different Docker images that are available. ## Cloud Environments {#sec-cloud} From 798b5f5cfdc3478b51cd48d53c38a3b5a0d387f2 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 22 May 2025 19:19:12 +0700 Subject: [PATCH 0663/1405] fix(RL): address plugin rl overwriting trainer_cls (#2697) [skip ci] * fix: plugin rl overwrite trainer_cls * feat(test): add test to catch trainer_cls is not None --- src/axolotl/core/trainer_builder.py | 4 +++- tests/core/test_trainer_builder.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 878dd176a9..863b065e6f 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -1195,7 +1195,9 @@ def build(self, total_num_steps): if self.cfg.plugins: plugin_manager = PluginManager.get_instance() - trainer_cls = plugin_manager.get_trainer_cls(self.cfg) + temp_trainer_cls = plugin_manager.get_trainer_cls(self.cfg) + if temp_trainer_cls is not None: + trainer_cls = temp_trainer_cls sig = inspect.signature(trainer_cls) if "tokenizer" in sig.parameters.keys(): diff --git a/tests/core/test_trainer_builder.py b/tests/core/test_trainer_builder.py index fbfd7a87c7..d1ad273ea3 100644 --- a/tests/core/test_trainer_builder.py +++ b/tests/core/test_trainer_builder.py @@ -8,6 +8,7 @@ from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault from axolotl.utils.models import load_model, load_tokenizer +from axolotl.utils.schemas.enums import RLType @pytest.fixture(name="cfg") @@ -65,3 +66,27 @@ def test_build_training_arguments(self, cfg, model, tokenizer): assert training_arguments.adam_epsilon == 0.00001 assert training_arguments.dataloader_num_workers == 1 assert training_arguments.dataloader_pin_memory is True + + +class TestTrainerClsPlugin: + """ + TestCase class for trainer builder with plugin + """ + + def test_trainer_cls_is_not_none_with_plugin(self, cfg, model, tokenizer): + """ + Test that the trainer cls is not none with plugin + + Fixes #2693 + """ + cfg.plugins = ["axolotl.integrations.liger.LigerPlugin"] + cfg.rl = RLType.KTO + + # Expected AttributeError as we don't pass regular model configs to RL trainer builder + # If it throws `TypeError: None is not a callable object`, trainer_cls could be None + with pytest.raises( + AttributeError, match=r".*'tuple' object has no attribute 'config'.*" + ): + builder = HFRLTrainerBuilder(cfg, model, tokenizer) + + builder.build(100) From aa0492c366d32645481c80b5c60a86f53f7670d7 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 22 May 2025 19:19:59 +0700 Subject: [PATCH 0664/1405] feat: do not find turn indices if turn is not trainable (#2696) * feat: do not find turn indices if turn is not trainable * fix: handle edge case where train on eos/eot is all * fix: improve warning message --- src/axolotl/prompt_strategies/chat_template.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 638cee559e..047a66e947 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -424,6 +424,20 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: LOG.debug(f"Should train: {should_train}") + # turn not trainable, skip having to find the turn indices + # unless last turn and train_on_eos/train_on_eot is all + if not should_train and ( + self.train_on_eos != "all" and self.train_on_eot != "all" + ): + if index == len(turns) - 1: + LOG.warning( + "Last turn is not trainable, skipping having to find the turn indices. " + "This may cause incorrect last EOT/EOS token to be unmasked." + "This is likely a dataset design issue. Please ensure last turn is trainable." + ) + + continue + turn_start_idx, turn_end_idx = self.find_turn(turns=turns, turn_idx=index) LOG.debug(f"Turn indices: start={turn_start_idx}, end={turn_end_idx}") From 5f8f8172005543e2fff728c81fd7314be7883d6f Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Thu, 22 May 2025 11:18:32 -0400 Subject: [PATCH 0665/1405] SP context manager update (#2699) * utilize accelerate prepare_data_loader with patching * lint * cleanup, fix * update to support DPO quirk * coderabbit commits, cleanup, remove dead code * fix * move ring attn patching to sp ctx manager * lint * lint * test fix * test fix --- src/axolotl/monkeypatch/ring_attn/patch.py | 6 ++- src/axolotl/train.py | 1 + .../utils/ctx_managers/sequence_parallel.py | 51 ++++++++++++++----- src/axolotl/utils/models.py | 22 -------- tests/e2e/patched/test_sp.py | 34 +++++++++---- 5 files changed, 68 insertions(+), 46 deletions(-) diff --git a/src/axolotl/monkeypatch/ring_attn/patch.py b/src/axolotl/monkeypatch/ring_attn/patch.py index 4329d9f138..7d733cfc1b 100644 --- a/src/axolotl/monkeypatch/ring_attn/patch.py +++ b/src/axolotl/monkeypatch/ring_attn/patch.py @@ -51,6 +51,8 @@ def get_ring_attn_group() -> dist.ProcessGroup: """Getter for ring attention group on this rank.""" + if RING_ATTN_GROUP is None: + raise RuntimeError("register_ring_attn() not yet called") return RING_ATTN_GROUP @@ -69,8 +71,8 @@ def register_ring_attn( Args: sequence_parallel_degree: Sequence parallelism factor. - heads_k_stride: Sequence parallelism K head stride size. Passed - through to `ring_flash_attn.substitute_hf_flash_attn`. + heads_k_stride: Sequence parallelism K head stride size. Passed through to + `varlen_llama3` `ring_flash_attn` implementation. ring_attn_func: `ring_flash_attn` ring attention implemention. If sample packing is enabled, it must be a `varlen` function; otherwise, it must be a `batch` function. diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 90ab10e9f9..46f722eebc 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -209,6 +209,7 @@ def execute_training( sequence_parallel_degree=cfg.sequence_parallel_degree, gradient_accumulation_steps=cfg.gradient_accumulation_steps, ring_attn_func=cfg.ring_attn_func, + heads_k_stride=cfg.heads_k_stride, ) ) diff --git a/src/axolotl/utils/ctx_managers/sequence_parallel.py b/src/axolotl/utils/ctx_managers/sequence_parallel.py index 6e4f9badad..2ae93acad7 100644 --- a/src/axolotl/utils/ctx_managers/sequence_parallel.py +++ b/src/axolotl/utils/ctx_managers/sequence_parallel.py @@ -12,6 +12,9 @@ from axolotl.monkeypatch.ring_attn.patch import ( get_ring_attn_group, + patch_prepare_data_loader, + patch_prepare_device_mesh, + register_ring_attn, update_ring_attn_params, ) from axolotl.utils.schemas.enums import RingAttnFunc @@ -169,6 +172,8 @@ class SequenceParallelContextManager: sequence_parallel_degree: Number of processes to split sequences over. gradient_accumulation_steps: Number of steps to accumulate gradients over. ring_attn_func: Which ring attention function to use. Currently unused. + heads_k_stride: Sequence parallelism K head stride size. Passed through to + `varlen_llama3` `ring_flash_attn` implementation. """ def __init__( @@ -177,14 +182,17 @@ def __init__( sequence_parallel_degree: int, gradient_accumulation_steps: int, ring_attn_func: RingAttnFunc, + heads_k_stride: int | None, ): self.models = models self.sequence_parallel_degree = sequence_parallel_degree self.gradient_accumulation_steps = gradient_accumulation_steps self.ring_attn_func = ring_attn_func - self.process_group = get_ring_attn_group() + self.heads_k_stride = heads_k_stride + self._register_ring_attn() - # Initialize sequence parallel group details + # Set distributed info for local rank + self.process_group = get_ring_attn_group() self.local_rank = dist.get_rank(self.process_group) self.local_world_size = dist.get_world_size(self.process_group) @@ -205,6 +213,33 @@ def __init__( ) def __enter__(self): + self._register_model_hooks() + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # Remove all hooks + for handle in self.hook_handles: + handle.remove() + self.hook_handles = [] + + # TODO(djsaunde): Un-patch attention and accelerate functions (low priority) + + def _register_ring_attn(self): + # Initialize ring attn for sequence parallelism + register_ring_attn( + sequence_parallel_degree=self.sequence_parallel_degree, + heads_k_stride=self.heads_k_stride, + ring_attn_func=self.ring_attn_func, + ) + + # Patches for accelerate functionality + patch_prepare_data_loader() + patch_prepare_device_mesh( + sequence_parallel_degree=self.sequence_parallel_degree + ) + + def _register_model_hooks(self): # Forward pre-hook to apply sequence parallelism def sequence_parallel_pre_hook(_, args, kwargs): # Get parameter names from the model's forward function @@ -230,7 +265,7 @@ def sequence_parallel_pre_hook(_, args, kwargs): # Forward post-hook to gather outputs def sequence_parallel_post_hook(_, __, output: ModelOutput) -> ModelOutput: # Gather the sharded outputs - output = self.gather_outputs(output) + output = self._gather_outputs(output) # Remove padding if it was added if self.pad_len > 0: @@ -253,15 +288,7 @@ def sequence_parallel_post_hook(_, __, output: ModelOutput) -> ModelOutput: model.register_forward_hook(sequence_parallel_post_hook) ) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - # Remove all hooks - for handle in self.hook_handles: - handle.remove() - self.hook_handles = [] - - def gather_outputs(self, output: CausalLMOutputWithPast) -> CausalLMOutputWithPast: + def _gather_outputs(self, output: CausalLMOutputWithPast) -> CausalLMOutputWithPast: """Gather sharded outputs from all ranks and reconstruct the full tensor.""" for key, value in output.items(): if isinstance(value, torch.Tensor) and value.dim() > 1: diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py index 6236f78e84..cd7499869e 100644 --- a/src/axolotl/utils/models.py +++ b/src/axolotl/utils/models.py @@ -59,7 +59,6 @@ SUPPORTED_MULTIPACK_MODEL_TYPES, patch_for_multipack, ) -from axolotl.monkeypatch.ring_attn.patch import get_ring_attn_group from axolotl.prompt_tokenizers import LLAMA_DEFAULT_EOS_TOKEN from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.chat_templates import get_chat_template_from_config @@ -681,27 +680,6 @@ def apply_patches(self) -> None: patch_self_attn_lora(self.cfg) - if self.cfg.sequence_parallel_degree and self.cfg.sequence_parallel_degree > 1: - from axolotl.monkeypatch.ring_attn import ( - patch_prepare_data_loader, - patch_prepare_device_mesh, - register_ring_attn, - ) - - # Initialize ring attn for sequence parallelism. This must be done after - # model init but before the first forward pass, since it modifies flash - # attn to use ring comm for SP training across multiple GPUs. - if get_ring_attn_group() is None: # If already set, this is already patched - register_ring_attn( - sequence_parallel_degree=self.cfg.sequence_parallel_degree, - heads_k_stride=self.cfg.heads_k_stride, - ring_attn_func=self.cfg.ring_attn_func, - ) - patch_prepare_data_loader() - patch_prepare_device_mesh( - sequence_parallel_degree=self.cfg.sequence_parallel_degree - ) - def patch_attention(self) -> None: if hasattr(self.model_config, "model_type"): if self.model_config.model_type == "mllama" and self.cfg.flash_attention: diff --git a/tests/e2e/patched/test_sp.py b/tests/e2e/patched/test_sp.py index 83faa779f3..2b4d11b30c 100644 --- a/tests/e2e/patched/test_sp.py +++ b/tests/e2e/patched/test_sp.py @@ -84,16 +84,16 @@ class TestRingAttention: def test_get_ring_attn_group_no_registration( self, mock_world_size, mock_rank, partial_state ): - """Test that get_ring_attn_group returns None when no group has been registered.""" + """Test that get_ring_attn_group raises RuntimeError when no group has been registered.""" # Setup mocks mock_world_size.return_value = 4 mock_rank.return_value = 0 - # Get the group without registration - group = get_ring_attn_group() - - # Verify that None was returned - assert group is None + # Verify that RuntimeError is raised when no group is registered + with pytest.raises( + RuntimeError, match="register_ring_attn\\(\\) not yet called" + ): + get_ring_attn_group() @patch("torch.distributed.new_group") @patch("torch.distributed.get_rank") @@ -323,8 +323,11 @@ def mock_distributed(self, monkeypatch): lambda **kwargs: None, ) - def test_world_size_one(self, sequence_parallel_batch): + @patch("axolotl.monkeypatch.ring_attn.patch.get_ring_attn_group") + def test_world_size_one(self, mock_get_ring_attn_group, sequence_parallel_batch): """Test that function returns original batch when world size is 1.""" + mock_get_ring_attn_group.return_value = 0 + result, _, _ = apply_sequence_parallelism( batch=sequence_parallel_batch, local_rank=0, @@ -336,8 +339,11 @@ def test_world_size_one(self, sequence_parallel_batch): # Should return the original batch unchanged assert result == sequence_parallel_batch - def test_batch_ring_rank0(self, sequence_parallel_batch): + @patch("axolotl.monkeypatch.ring_attn.patch.get_ring_attn_group") + def test_batch_ring_rank0(self, mock_get_ring_attn_group, sequence_parallel_batch): """Test BATCH_RING sharding for rank 0 in a 2-process group.""" + mock_get_ring_attn_group.return_value = 0 + batch = sequence_parallel_batch seq_len = batch["input_ids"].size(1) @@ -359,8 +365,11 @@ def test_batch_ring_rank0(self, sequence_parallel_batch): result["position_ids"], batch["position_ids"][:, : seq_len // 2] ) - def test_batch_ring_rank1(self, sequence_parallel_batch): + @patch("axolotl.monkeypatch.ring_attn.patch.get_ring_attn_group") + def test_batch_ring_rank1(self, mock_get_ring_attn_group, sequence_parallel_batch): """Test BATCH_RING sharding for rank 1 in a 2-process group.""" + mock_get_ring_attn_group.return_value = 0 + batch = sequence_parallel_batch seq_len = batch["input_ids"].size(1) original_input_ids = batch["input_ids"].clone() @@ -419,8 +428,13 @@ def test_batch_ring_rank1(self, sequence_parallel_batch): # assert torch.equal(result_rank0["input_ids"], rank0_expected) # assert torch.equal(result_rank1["input_ids"], rank1_expected) - def test_partial_application(self, sequence_parallel_batch): + @patch("axolotl.monkeypatch.ring_attn.patch.get_ring_attn_group") + def test_partial_application( + self, mock_get_ring_attn_group, sequence_parallel_batch + ): """Test that we can create a partially applied version of the function.""" + mock_get_ring_attn_group.return_value = 0 + batch = sequence_parallel_batch original_input_ids = batch["input_ids"].clone() From 8cde256db2964f14398e861317431afedffc7a26 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 23 May 2025 12:27:38 -0400 Subject: [PATCH 0666/1405] Remove unused const (#2714) * remove unused const * accidentally commited benchmark plot --- src/axolotl/kernels/geglu.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/axolotl/kernels/geglu.py b/src/axolotl/kernels/geglu.py index 0aa035c941..6acbea0d47 100644 --- a/src/axolotl/kernels/geglu.py +++ b/src/axolotl/kernels/geglu.py @@ -1,5 +1,4 @@ -""" -Module for definition of GEGLU Triton kernels. +"""Module for definition of GEGLU Triton kernels. See "GLU Variants Improve Transformer" (https://arxiv.org/abs/2002.05202). @@ -12,8 +11,6 @@ import triton import triton.language as tl -SQRT_2_PI: tl.constexpr = 0.7978845608028654 # sqrt(2/π) - @triton.jit def _geglu_fwd_kernel( From b5f1e53a0fbb43528c753f017bf099fa99f42c3e Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 23 May 2025 15:51:11 -0400 Subject: [PATCH 0667/1405] models.py -> loaders/ module refactor (#2680) * models.py -> loaders/ module refactor * refactor ModelLoader class * plugin manager changes * circular import fix * pytest * pytest * minor improvements * fix * minor changes * fix test * remove dead code * coderabbit comments * lint * fix * coderabbit suggestion I liked * more coderabbit * review comments, yak shaving * lint * updating in light of SP ctx manager changes * review comment * review comment 2 --- src/axolotl/cli/utils.py | 6 +- src/axolotl/common/datasets.py | 2 +- src/axolotl/core/trainer_builder.py | 2 +- src/axolotl/core/trainers/grpo/trainer.py | 2 +- src/axolotl/integrations/base.py | 506 +++-- src/axolotl/loaders/__init__.py | 10 + src/axolotl/loaders/adapter.py | 206 +++ src/axolotl/loaders/constants.py | 21 + src/axolotl/loaders/model.py | 754 ++++++++ src/axolotl/loaders/patch_manager.py | 380 ++++ src/axolotl/loaders/processor.py | 56 + src/axolotl/loaders/tokenizer.py | 281 +++ src/axolotl/loaders/utils.py | 211 +++ .../gradient_checkpointing/__init__.py | 4 +- .../gradient_checkpointing/offload_cpu.py | 0 .../gradient_checkpointing/offload_disk.py | 0 src/axolotl/monkeypatch/peft/utils.py | 2 +- src/axolotl/train.py | 12 +- src/axolotl/utils/config/__init__.py | 3 +- .../utils/ctx_managers/sequence_parallel.py | 2 +- src/axolotl/utils/data/rl.py | 2 +- src/axolotl/utils/lora_embeddings.py | 14 - src/axolotl/utils/models.py | 1648 ----------------- src/axolotl/utils/schemas/config.py | 10 + tests/core/test_trainer_builder.py | 8 +- tests/e2e/patched/test_model_patches.py | 6 +- tests/e2e/test_load_model.py | 13 +- tests/patched/test_validation.py | 16 +- tests/test_exact_deduplication.py | 22 +- .../{utils/test_models.py => test_loaders.py} | 37 +- tests/test_lora.py | 6 +- tests/test_tokenizers.py | 2 +- tests/utils/__init__.py | 0 33 files changed, 2227 insertions(+), 2017 deletions(-) create mode 100644 src/axolotl/loaders/__init__.py create mode 100644 src/axolotl/loaders/adapter.py create mode 100644 src/axolotl/loaders/constants.py create mode 100644 src/axolotl/loaders/model.py create mode 100644 src/axolotl/loaders/patch_manager.py create mode 100644 src/axolotl/loaders/processor.py create mode 100644 src/axolotl/loaders/tokenizer.py create mode 100644 src/axolotl/loaders/utils.py rename src/axolotl/{utils => monkeypatch}/gradient_checkpointing/__init__.py (91%) rename src/axolotl/{utils => monkeypatch}/gradient_checkpointing/offload_cpu.py (100%) rename src/axolotl/{utils => monkeypatch}/gradient_checkpointing/offload_disk.py (100%) delete mode 100644 src/axolotl/utils/lora_embeddings.py delete mode 100644 src/axolotl/utils/models.py rename tests/{utils/test_models.py => test_loaders.py} (83%) delete mode 100644 tests/utils/__init__.py diff --git a/src/axolotl/cli/utils.py b/src/axolotl/cli/utils.py index ee00db39d8..e681589f3b 100644 --- a/src/axolotl/cli/utils.py +++ b/src/axolotl/cli/utils.py @@ -20,8 +20,9 @@ ProcessorMixin, ) +from axolotl.loaders import load_processor, load_tokenizer +from axolotl.loaders.model import ModelLoader from axolotl.utils.dict import DictDefault -from axolotl.utils.models import load_model, load_processor, load_tokenizer LOG = logging.getLogger(__name__) @@ -318,7 +319,8 @@ def load_model_and_tokenizer( tokenizer = load_tokenizer(cfg) LOG.info("loading model...") - model, _ = load_model(cfg, tokenizer, inference=inference) + model_loader = ModelLoader(cfg, tokenizer, inference=inference) + model, _ = model_loader.load() processor = None if cfg.is_multimodal: diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index f944cbd6a8..e3ffb7ae9a 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -10,10 +10,10 @@ import axolotl.monkeypatch.data.batch_dataset_fetcher # pylint: disable=unused-import # noqa: F401 from axolotl.cli.args import PreprocessCliArgs, TrainerCliArgs +from axolotl.loaders import load_processor, load_tokenizer from axolotl.utils.data import prepare_dataset from axolotl.utils.data.rl import load_prepare_preference_datasets from axolotl.utils.dict import DictDefault -from axolotl.utils.models import load_processor, load_tokenizer from axolotl.utils.schemas.enums import RLType from axolotl.utils.tokenization import check_dataset_labels diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 863b065e6f..9709f0fd4a 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -59,6 +59,7 @@ AxolotlTrainingArguments, ) from axolotl.integrations.base import PluginManager +from axolotl.loaders.utils import ensure_dtype from axolotl.monkeypatch.multipack import SUPPORTED_MULTIPACK_MODEL_TYPES from axolotl.monkeypatch.relora import ReLoRACallback from axolotl.monkeypatch.trainer.lr import patch_trainer_get_lr @@ -86,7 +87,6 @@ V2BatchSamplerDataCollatorForSeq2Seq, ) from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator -from axolotl.utils.models import ensure_dtype from axolotl.utils.schemas.enums import CustomSupportedOptimizers, RLType try: diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index a603ed8606..b5b3912cf5 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -43,7 +43,7 @@ from axolotl.core.trainers.grpo.sampler import SequenceParallelRepeatRandomSampler from axolotl.core.trainers.mixins import RngLoaderMixin, SchedulerMixin -from axolotl.monkeypatch.ring_attn.patch import get_ring_attn_group +from axolotl.monkeypatch.ring_attn import get_ring_attn_group if is_peft_available(): # pylint: disable=unused-import diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 97cbac6938..2beaf667ac 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -10,71 +10,73 @@ # License for the specific language governing permissions and limitations under # the License. -""" -Base class for all plugins. +"""Base class for all plugins. A plugin is a reusable, modular, and self-contained piece of code that extends the functionality of Axolotl. Plugins can be used to integrate third-party models, modify the training process, or add new features. To create a new plugin, you need to inherit from the BasePlugin class and implement the required methods. """ + +from __future__ import annotations + import collections import importlib import logging -from typing import OrderedDict +from typing import TYPE_CHECKING, Callable, OrderedDict, Union -import torch +from peft import PeftModel +from torch.optim import Optimizer from torch.optim.lr_scheduler import LRScheduler +from transformers import PreTrainedModel, Trainer from axolotl.utils.dict import DictDefault +if TYPE_CHECKING: + from axolotl.common.datasets import TrainDatasetMeta -class BasePlugin: - """ - Base class for all plugins. Defines the interface for plugin methods. - Attributes: - None +class BasePlugin: + """Base class for all plugins. Defines the interface for plugin methods. Methods: - register(cfg): Registers the plugin with the given configuration. - load_datasets(cfg): Loads and preprocesses the dataset for training. - pre_model_load(cfg): Performs actions before the model is loaded. - post_model_build(cfg, model): Performs actions after the model is loaded, but before LoRA adapters are applied. - pre_lora_load(cfg, model): Performs actions before LoRA weights are loaded. - post_lora_load(cfg, model): Performs actions after LoRA weights are loaded. - post_model_load(cfg, model): Performs actions after the model is loaded, inclusive of any adapters. - post_trainer_create(cfg, trainer): Performs actions after the trainer is created. - create_optimizer(cfg, trainer): Creates and returns an optimizer for training. - create_lr_scheduler(cfg, trainer, optimizer, num_training_steps): Creates and returns a learning rate scheduler. - add_callbacks_pre_trainer(cfg, model): Adds callbacks to the trainer before training. - add_callbacks_post_trainer(cfg, trainer): Adds callbacks to the trainer after training. + register(cfg): Registers the plugin with the given configuration. + load_datasets(cfg): Loads and preprocesses the dataset for training. + pre_model_load(cfg): Performs actions before the model is loaded. + post_model_build(cfg, model): Performs actions after the model is loaded, but + before LoRA adapters are applied. + pre_lora_load(cfg, model): Performs actions before LoRA weights are loaded. + post_lora_load(cfg, model): Performs actions after LoRA weights are loaded. + post_model_load(cfg, model): Performs actions after the model is loaded, + inclusive of any adapters. + post_trainer_create(cfg, trainer): Performs actions after the trainer is + created. + create_optimizer(cfg, trainer): Creates and returns an optimizer for training. + create_lr_scheduler(cfg, trainer, optimizer, num_training_steps): Creates and + returns a learning rate scheduler. + add_callbacks_pre_trainer(cfg, model): Adds callbacks to the trainer before + training. + add_callbacks_post_trainer(cfg, trainer): Adds callbacks to the trainer after + training. """ def __init__(self): - """ - Initializes the BasePlugin. - """ + """Initializes the BasePlugin.""" def register(self, cfg): # pylint: disable=unused-argument - """ - Registers the plugin with the given configuration. + """Registers the plugin with the given configuration. - Parameters: - cfg (dict): The configuration for the plugin. - - Returns: - None + Args: + cfg: The configuration for the plugin. """ def get_input_args(self) -> str | None: - """ - Returns a pydantic model for the plugin's input arguments. - """ + """Returns a pydantic model for the plugin's input arguments.""" - def load_datasets(self, cfg: DictDefault, preprocess: bool = False): - """ - Loads and preprocesses the dataset for training. + def load_datasets( + self, cfg: DictDefault, preprocess: bool = False + ) -> Union["TrainDatasetMeta", None]: + """Loads and preprocesses the dataset for training. Args: cfg: The configuration for the plugin. @@ -84,181 +86,164 @@ def load_datasets(self, cfg: DictDefault, preprocess: bool = False): dataset_meta: The metadata for the training dataset. """ - def pre_model_load(self, cfg): # pylint: disable=unused-argument - """ - Performs actions before the model is loaded. + def pre_model_load(self, cfg: DictDefault): # pylint: disable=unused-argument + """Performs actions before the model is loaded. Args: - cfg (dict): The configuration for the plugin. - - Returns: - None + cfg: The configuration for the plugin. """ - def post_model_build(self, cfg, model): # pylint: disable=unused-argument - """ - Performs actions after the model is built/loaded, but before any adapters are applied. + # pylint: disable=unused-argument + def post_model_build(self, cfg: DictDefault, model: PreTrainedModel): + """Performs actions after the model is built/loaded, but before any adapters are applied. Args: - cfg (dict): The configuration for the plugin. + cfg: The configuration for the plugin. """ - def post_model_load(self, cfg, model): # pylint: disable=unused-argument - """ - Performs actions after the model is loaded. + # pylint: disable=unused-argument + def pre_lora_load(self, cfg: DictDefault, model: PreTrainedModel): + """Performs actions before LoRA weights are loaded. Args: - cfg (dict): The configuration for the plugin. - model (object): The loaded model. - - Returns: - None + cfg: The configuration for the plugin. + model: The loaded model. """ - def pre_lora_load(self, cfg, model): # pylint: disable=unused-argument - """ - Performs actions before LoRA weights are loaded. + # pylint: disable=unused-argument + def post_lora_load(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): + """Performs actions after LoRA weights are loaded. Args: - cfg (dict): The configuration for the plugin. - model (object): The loaded model. - - Returns: - None + cfg: The configuration for the plugin. + model: The loaded model. """ - def post_lora_load(self, cfg, model): # pylint: disable=unused-argument - """ - Performs actions after LoRA weights are loaded. + # pylint: disable=unused-argument + def post_model_load(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): + """Performs actions after the model is loaded. Args: - cfg (dict): The configuration for the plugin. - model (object): The loaded model. - - Returns: - None + cfg: The configuration for the plugin. + model: The loaded model. """ - def get_trainer_cls(self, cfg): # pylint: disable=unused-argument): - """ - Returns a custom class for the trainer. + # pylint: disable=unused-argument + def get_trainer_cls(self, cfg: DictDefault) -> Trainer | None: + """Returns a custom class for the trainer. Args: - cfg (dict): The global axolotl configuration. + cfg: The global axolotl configuration. Returns: - class: The class for the trainer. + The first non-`None` trainer class returned by a plugin. """ - def post_trainer_create(self, cfg, trainer): # pylint: disable=unused-argument - """ - Performs actions after the trainer is created. + # pylint: disable=unused-argument + def post_trainer_create(self, cfg: DictDefault, trainer: Trainer): + """Performs actions after the trainer is created. Args: - cfg (dict): The configuration for the plugin. - trainer (object): The trainer object for training. - - Returns: - None + cfg: The configuration for the plugin. + trainer: The trainer object for training. """ - def create_optimizer(self, cfg, trainer): # pylint: disable=unused-argument - """ - Creates and returns an optimizer for training. + # pylint: disable=unused-argument + def create_optimizer(self, cfg: DictDefault, trainer: Trainer) -> Optimizer | None: + """Creates and returns an optimizer for training. Args: - cfg (dict): The configuration for the plugin. - trainer (object): The trainer object for training. + cfg: The configuration for the plugin. + trainer: The trainer object for training. Returns: - object: The created optimizer. + The created optimizer. """ + # pylint: disable=unused-argument def create_lr_scheduler( - self, cfg, trainer, optimizer, num_training_steps - ) -> LRScheduler | None: # pylint: disable=unused-argument - """ - Creates and returns a learning rate scheduler. + self, + cfg: DictDefault, + trainer: Trainer, + optimizer: Optimizer, + num_training_steps: int, + ) -> LRScheduler | None: + """Creates and returns a learning rate scheduler. Args: - cfg (dict): The configuration for the plugin. - trainer (object): The trainer object for training. - optimizer (object): The optimizer for training. - num_training_steps (int): Total number of training steps + cfg: The configuration for the plugin. + trainer: The trainer object for training. + optimizer: The optimizer for training. + num_training_steps: Total number of training steps Returns: - object (LRScheduler): The created learning rate scheduler. + The created learning rate scheduler. """ - def add_callbacks_pre_trainer(self, cfg, model): # pylint: disable=unused-argument - """ - setup callbacks before creating the trainer. + # pylint: disable=unused-argument + def add_callbacks_pre_trainer( + self, cfg: DictDefault, model: PreTrainedModel + ) -> list[Callable]: + """Set up callbacks before creating the trainer. Args: - cfg (dict): The configuration for the plugin. - model (object): The loaded model. + cfg: The configuration for the plugin. + model: The loaded model. Returns: - List[callable]: A list of callback functions to be added to the TrainingArgs + A list of callback functions to be added to the `TrainingArgs`. """ return [] + # pylint: disable=unused-argument def add_callbacks_post_trainer( - self, cfg, trainer - ): # pylint: disable=unused-argument - """ - Adds callbacks to the trainer after creating the trainer. - This is useful for callbacks that require access to the model or trainer. + self, cfg: DictDefault, trainer: Trainer + ) -> list[Callable]: + """Adds callbacks to the trainer after creating the trainer. This is useful for + callbacks that require access to the model or trainer. Args: - cfg (dict): The configuration for the plugin. - trainer (object): The trainer object for training. + cfg: The configuration for the plugin. + trainer: The trainer object for training. Returns: - List[callable]: A list of callback functions to be added + A list of callback functions to be added """ return [] - def post_train(self, cfg, model): # pylint: disable=unused-argument - """ - Performs actions after training is complete. + # pylint: disable=unused-argument + def post_train(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): + """Performs actions after training is complete. Args: - cfg (dict): The axolotl configuration - model (object): The loaded model. - - Returns: - None + cfg: The axolotl configuration. + model: The loaded model. """ - def post_train_unload(self, cfg): # pylint: disable=unused-argument - """ - Performs actions after training is complete and the model is unloaded. + def post_train_unload(self, cfg: DictDefault): # pylint: disable=unused-argument + """Performs actions after training is complete and the model is unloaded. Args: - cfg (dict): The configuration for the plugin. - - Returns: - None + cfg: The configuration for the plugin. """ def load_plugin(plugin_name: str) -> BasePlugin: - """ - Loads a plugin based on the given plugin name. + """Loads a plugin based on the given plugin name. - The plugin name should be in the format "module_name.class_name". - This function splits the plugin name into module and class, imports the module, - retrieves the class from the module, and creates an instance of the class. + The plugin name should be in the format "module_name.class_name". This function + splits the plugin name into module and class, imports the module, retrieves the + class from the module, and creates an instance of the class. - Parameters: - plugin_name (str): The name of the plugin to be loaded. The name should be in the format "module_name.class_name". + Args: + plugin_name: The name of the plugin to be loaded. The name should be in the + format "module_name.class_name". Returns: - BasePlugin: An instance of the loaded plugin. + An instance of the loaded plugin. Raises: - ImportError: If the plugin module cannot be imported. + ImportError: If the plugin module cannot be imported. """ # split the plugin name into module and class module_name, class_name = plugin_name.rsplit(".", 1) @@ -284,28 +269,25 @@ def load_plugin(plugin_name: str) -> BasePlugin: class PluginManager: - """ - The PluginManager class is responsible for loading and managing plugins. - It should be a singleton so it can be accessed from anywhere in the codebase. + """The `PluginManager` class is responsible for loading and managing plugins. It + should be a singleton so it can be accessed from anywhere in the codebase. Attributes: - plugins (List[BasePlugin]): A list of loaded plugins. + plugins: A list of loaded plugins. Methods: - get_instance(): Static method to get the singleton instance of PluginManager. - register(plugin_name: str): Registers a new plugin by its name. - pre_model_load(cfg): Calls the pre_model_load method of all registered plugins. + get_instance(): Static method to get the singleton instance of `PluginManager`. + register(plugin_name: str): Registers a new plugin by its name. + pre_model_load(cfg): Calls the pre_model_load method of all registered plugins. """ plugins: OrderedDict[str, BasePlugin] = collections.OrderedDict() - _instance = None - _cfg = None + _instance: PluginManager | None = None + _cfg: DictDefault | None = None def __new__(cls): - """ - Creates a new instance of PluginManager if it doesn't exist yet. - """ + """Creates a new instance of PluginManager if it doesn't exist yet.""" if cls._instance is None: cls._instance = super(PluginManager, cls).__new__(cls) cls._instance.plugins: OrderedDict[str, BasePlugin] = ( @@ -315,9 +297,8 @@ def __new__(cls): @staticmethod def get_instance() -> "PluginManager": - """ - Returns the singleton instance of PluginManager. - If the instance doesn't exist, it creates a new one. + """Returns the singleton instance of PluginManager. If the instance doesn't + exist, it creates a new one. """ if PluginManager._instance is None: PluginManager() @@ -332,17 +313,13 @@ def cfg(self, cfg): self._cfg = cfg def register(self, plugin_name: str): - """ - Registers a new plugin by its name. - - Parameters: - plugin_name (str): The name of the plugin to be registered. + """Registers a new plugin by its name. - Returns: - None + Args: + plugin_name: The name of the plugin to be registered. Raises: - ImportError: If the plugin module cannot be imported. + ImportError: If the plugin module cannot be imported. """ try: logging.info(f"Attempting to load plugin: {plugin_name}") @@ -352,12 +329,11 @@ def register(self, plugin_name: str): except ImportError: logging.error(f"Failed to load plugin: {plugin_name}") - def get_input_args(self): - """ - Returns a list of Pydantic classes for all registered plugins' input arguments.' + def get_input_args(self) -> list[str]: + """Returns a list of Pydantic classes for all registered plugins' input arguments.' Returns: - list[str]: A list of Pydantic classes for all registered plugins' input arguments.' + A list of Pydantic classes for all registered plugins' input arguments.' """ input_args = [] for plugin in self.plugins.values(): @@ -366,16 +342,17 @@ def get_input_args(self): input_args.append(input_args_from_plugin) return input_args - def load_datasets(self, cfg, preprocess: bool = False): - """ - Calls the load_datasets method of each registered plugin. + def load_datasets( + self, cfg: DictDefault, preprocess: bool = False + ) -> Union["TrainDatasetMeta", None]: + """Calls the load_datasets method of each registered plugin. Args: cfg: The configuration for the plugins. - preprocess : Whether this is preprocess step of the datasets. + preprocess: Whether this is preprocess step of the datasets. Returns: - dataset_meta: The dataset metadata loaded from all registered plugins. + The dataset metadata loaded from all registered plugins. """ return_ds_meta = None for plugin in self.plugins.values(): @@ -387,83 +364,66 @@ def load_datasets(self, cfg, preprocess: bool = False): raise RuntimeError("Multiple plugins loaded datasets") return return_ds_meta - def pre_model_load(self, cfg): - """ - Calls the pre_model_load method of all registered plugins. - - Parameters: - cfg (dict): The configuration for the plugins. + def pre_model_load(self, cfg: DictDefault): + """Calls the pre_model_load method of all registered plugins. - Returns: - None + Args: + cfg: The configuration for the plugins. """ for plugin in self.plugins.values(): plugin.pre_model_load(cfg) - def post_model_build(self, cfg, model): - """ - Calls the post_model_build method of all registered plugins after the model has been built/loaded, - but before any adapters have been applied. + def post_model_build(self, cfg: DictDefault, model: PreTrainedModel): + """Calls the `post_model_build` method of all registered plugins after the + model has been built / loaded, but before any adapters have been applied. Args: - cfg (dict): The configuration for the plugins. - model (object): The loaded model. + cfg: The configuration for the plugins. + model: The loaded model. """ for plugin in self.plugins.values(): plugin.post_model_build(cfg, model) - def post_model_load(self, cfg, model): - """ - Calls the post_model_load method of all registered plugins after the model has been loaded - inclusive of any adapters - - Parameters: - cfg (dict): The configuration for the plugins. - model (object): The loaded model. + def pre_lora_load(self, cfg: DictDefault, model: PreTrainedModel): + """Calls the `pre_lora_load` method of all registered plugins. - Returns: - None + Args: + cfg: The configuration for the plugins. + model: The loaded model. """ for plugin in self.plugins.values(): - plugin.post_model_load(cfg, model) - - def pre_lora_load(self, cfg, model): - """ - Calls the pre_lora_load method of all registered plugins. + plugin.pre_lora_load(cfg, model) - Parameters: - cfg (dict): The configuration for the plugins. - model (object): The loaded model. + def post_lora_load(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): + """Calls the `post_lora_load` method of all registered plugins. - Returns: - None + Args: + cfg: The configuration for the plugins. + model: The loaded model. """ for plugin in self.plugins.values(): - plugin.pre_lora_load(cfg, model) - - def post_lora_load(self, cfg, model): - """ - Calls the post_lora_load method of all registered plugins. + plugin.post_lora_load(cfg, model) - Parameters: - cfg (dict): The configuration for the plugins. - model (object): The loaded model. + def post_model_load(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): + """Calls the `post_model_load` method of all registered plugins after the model + has been loaded inclusive of any adapters. - Returns: - None + Args: + cfg: The configuration for the plugins. + model: The loaded model. """ for plugin in self.plugins.values(): - plugin.post_lora_load(cfg, model) + plugin.post_model_load(cfg, model) - def get_trainer_cls(self, cfg): - """ - Calls the get_trainer_cls method of all registered plugins and returns the first non-None trainer class. + def get_trainer_cls(self, cfg: DictDefault) -> Trainer | None: + """Calls the `get_trainer_cls` method of all registered plugins and returns the + first non-`None` trainer class. - Parameters: - cfg (dict): The configuration for the plugins. + Args: + cfg: The configuration for the plugins. Returns: - object: The trainer class, or None if none was found. + The first non-`None` trainer class returned by a plugin. """ for plugin in self.plugins.values(): trainer_cls = plugin.get_trainer_cls(cfg) @@ -471,29 +431,25 @@ def get_trainer_cls(self, cfg): return trainer_cls return None - def post_trainer_create(self, cfg, trainer): - """ - Calls the post_trainer_create method of all registered plugins. - - Parameters: - cfg (dict): The configuration for the plugins. - trainer (object): The trainer object for training. + def post_trainer_create(self, cfg: DictDefault, trainer: Trainer): + """Calls the `post_trainer_create` method of all registered plugins. - Returns: - None + Args: + cfg: The configuration for the plugins. + trainer: The trainer object for training. """ for plugin in self.plugins.values(): plugin.post_trainer_create(cfg, trainer) - def create_optimizer(self, trainer): - """ - Calls the create_optimizer method of all registered plugins and returns the first non-None optimizer. + def create_optimizer(self, trainer: Trainer) -> Optimizer | None: + """Calls the `create_optimizer` method of all registered plugins and returns + the first non-`None` optimizer. - Parameters: - trainer (object): The trainer object for training. + Args: + trainer: The trainer object for training. Returns: - object: The created optimizer, or None if none was found. + The created optimizer, or `None` if none was found. """ for plugin in self.plugins.values(): optimizer = plugin.create_optimizer(self.cfg, trainer) @@ -502,17 +458,17 @@ def create_optimizer(self, trainer): return None def create_lr_scheduler( - self, trainer, optimizer, num_training_steps + self, trainer: Trainer, optimizer: Optimizer, num_training_steps: int ) -> LRScheduler | None: - """ - Calls the create_lr_scheduler method of all registered plugins and returns the first non-None scheduler. + """Calls the `create_lr_scheduler` method of all registered plugins and returns + the first non-`None` scheduler. - Parameters: - trainer (object): The trainer object for training. - optimizer (object): The optimizer for training. + Args: + trainer: The trainer object for training. + optimizer: The optimizer for training. Returns: - object: The created learning rate scheduler, or None if none was found. + The created learning rate scheduler, or `None` if not found. """ for plugin in self.plugins.values(): scheduler: LRScheduler | None = plugin.create_lr_scheduler( @@ -525,16 +481,17 @@ def create_lr_scheduler( return scheduler return None - def add_callbacks_pre_trainer(self, cfg, model): - """ - Calls the add_callbacks_pre_trainer method of all registered plugins. + def add_callbacks_pre_trainer( + self, cfg: DictDefault, model: PreTrainedModel + ) -> list[Callable]: + """Calls the add_callbacks_pre_trainer method of all registered plugins. - Parameters: - cfg (dict): The configuration for the plugins. - model (object): The loaded model. + Args: + cfg: The configuration for the plugins. + model: The loaded model. Returns: - List[callable]: A list of callback functions to be added to the TrainingArgs. + A list of callback functions to be added to the `TrainingArgs`. """ callbacks = [] for plugin in self.plugins.values(): @@ -543,16 +500,17 @@ def add_callbacks_pre_trainer(self, cfg, model): callbacks.extend(plugin_callbacks) return callbacks - def add_callbacks_post_trainer(self, cfg, trainer): - """ - Calls the add_callbacks_post_trainer method of all registered plugins. + def add_callbacks_post_trainer( + self, cfg: DictDefault, trainer: Trainer + ) -> list[Callable]: + """Calls the `add_callbacks_post_trainer` method of all registered plugins. - Parameters: - cfg (dict): The configuration for the plugins. - trainer (object): The trainer object for training. + Args: + cfg: The configuration for the plugins. + trainer: The trainer object for training. Returns: - List[callable]: A list of callback functions to be added to the TrainingArgs. + A list of callback functions to be added to the `TrainingArgs`. """ callbacks = [] for plugin in self.plugins.values(): @@ -561,41 +519,31 @@ def add_callbacks_post_trainer(self, cfg, trainer): callbacks.extend(plugin_callbacks) return callbacks - def post_train(self, cfg, model): - """ - Calls the post_train method of all registered plugins. - - Parameters: - cfg (dict): The configuration for the plugins. - model (object): The loaded model. + def post_train(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): + """Calls the post_train method of all registered plugins. - Returns: - None + Args: + cfg: The configuration for the plugins. + model: The loaded model. """ for plugin in self.plugins.values(): plugin.post_train(cfg, model) - def post_train_unload(self, cfg): - """ - Calls the post_train_unload method of all registered plugins. - - Parameters: - cfg (dict): The configuration for the plugins. - model (object): The loaded model. + def post_train_unload(self, cfg: DictDefault): + """Calls the post_train_unload method of all registered plugins. - Returns: - None + Args: + cfg: The configuration for the plugins. + model: The loaded model. """ for plugin in self.plugins.values(): plugin.post_train_unload(cfg) class BaseOptimizerFactory: - """ - Base class for factories to create custom optimizers - """ + """Base class for factories to create custom optimizers""" def __call__( self, opt_model, training_args, **optimizer_kwargs - ) -> "torch.optim.Optimizer": + ) -> Optimizer | None: pass diff --git a/src/axolotl/loaders/__init__.py b/src/axolotl/loaders/__init__.py new file mode 100644 index 0000000000..3eef75e58b --- /dev/null +++ b/src/axolotl/loaders/__init__.py @@ -0,0 +1,10 @@ +"""Init for axolotl.loaders module""" + +# pylint: disable=unused-import +# flake8: noqa + +from .adapter import load_adapter, load_lora +from .constants import MULTIMODAL_AUTO_MODEL_MAPPING +from .model import ModelLoader +from .processor import load_processor +from .tokenizer import load_tokenizer diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py new file mode 100644 index 0000000000..f7a484e9bc --- /dev/null +++ b/src/axolotl/loaders/adapter.py @@ -0,0 +1,206 @@ +"""Adapter loading functionality, including LoRA / QLoRA and associated utils""" + +import logging +import os +import types +from typing import Any + +import bitsandbytes as bnb +import torch +from bitsandbytes.nn import Params4bit +from peft import ( + AdaptionPromptConfig, + LoftQConfig, + LoraConfig, + PeftConfig, + PeftMixedModel, + PeftModel, + get_peft_model, +) +from transformers import PreTrainedModel + +from axolotl.loaders.utils import get_linear_embedding_layers +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger(__name__) + + +def setup_quantized_meta_for_peft(model: torch.nn.Module): + """Replaces `quant_state.to` with a dummy function to prevent PEFT from moving `quant_state` to meta device""" + + def temp_to_method(self, *args, **kwargs): # pylint: disable=unused-argument + return self + + for param in model.parameters(): + if isinstance(param, Params4bit): + param.quant_state._orig_to = ( # pylint: disable=protected-access + param.quant_state.to + ) + param.quant_state.to = types.MethodType(temp_to_method, param.quant_state) + + +def setup_quantized_peft_meta_for_training(model: torch.nn.Module): + """Replaces dummy `quant_state.to` method with the original function to allow training to continue""" + for param in model.parameters(): + if isinstance(param, Params4bit) and hasattr(param.quant_state, "_orig_to"): + param.quant_state.to = ( + param.quant_state._orig_to # pylint: disable=protected-access + ) + param.quant_state._orig_to = None # pylint: disable=protected-access + + +def find_all_linear_names(model): + cls = (bnb.nn.Linear4bit, bnb.nn.Linear8bitLt, torch.nn.Linear) + lora_module_names = set() + for name, module in model.named_modules(): + if ( + isinstance(module, cls) + or "Linear" in module.__class__.__name__ + and module.__class__.__name__ not in ("LlamaLinearScalingRotaryEmbedding",) + ): + names = name.split(".") + lora_module_names.add(names[0] if len(names) == 1 else names[-1]) + + embedding_modules = get_linear_embedding_layers(model.config.model_type) + output_embedding = embedding_modules[1] + if output_embedding in lora_module_names: # needed for 16-bit + lora_module_names.remove(output_embedding) + + return list(lora_module_names) + + +def load_lora( + model: PreTrainedModel, + cfg: DictDefault, + inference: bool = False, + config_only: bool = False, +) -> tuple[PreTrainedModel | PeftModel | PeftMixedModel | None, PeftConfig | None]: + lora_target_modules = cfg.lora_target_modules or [] + + if cfg.lora_target_linear: + linear_names = find_all_linear_names(model) + LOG.info(f"found linear modules: {repr(sorted(linear_names))}") + lora_target_modules_as_list = ( + lora_target_modules + if isinstance(lora_target_modules, list) + else [lora_target_modules] + ) + lora_target_modules = list(set(lora_target_modules_as_list + linear_names)) + + lora_config_kwargs = {} + loftq_bits = cfg.peft and cfg.peft.loftq_config and cfg.peft.loftq_config.loftq_bits + if loftq_bits: + lora_config_kwargs["loftq_config"] = LoftQConfig(loftq_bits=loftq_bits) + lora_config_kwargs["init_lora_weights"] = "loftq" + if cfg.peft_init_lora_weights: + lora_config_kwargs["init_lora_weights"] = cfg.peft_init_lora_weights + if cfg.peft_use_dora: + lora_config_kwargs["use_dora"] = cfg.peft_use_dora + LOG.info("Initializing LoRA weights using dora. This might take longer.") + if cfg.peft_use_rslora: + lora_config_kwargs["use_rslora"] = cfg.peft_use_rslora + if cfg.peft_layer_replication: + lora_config_kwargs["layer_replication"] = cfg.peft_layer_replication + + lora_config = LoraConfig( + r=cfg.lora_r, + lora_alpha=cfg.lora_alpha, + target_modules=lora_target_modules, + layers_to_transform=cfg.peft_layers_to_transform, + layers_pattern=cfg.peft_layers_pattern, + lora_dropout=cfg.lora_dropout, + fan_in_fan_out=cfg.lora_fan_in_fan_out, + modules_to_save=cfg.lora_modules_to_save if cfg.lora_modules_to_save else None, + bias="none", + task_type="CAUSAL_LM", + **lora_config_kwargs, + ) + + if config_only: + return None, lora_config + + rank = int(os.environ.get("LOCAL_RANK", 0)) + + if ( + cfg.fsdp + and cfg.adapter + and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading + and rank != 0 + ): + setup_quantized_meta_for_peft(model) + + if cfg.lora_model_dir: + LOG.debug("Loading pretrained PEFT - LoRA") + model_kwargs: Any = {} + if cfg.lora_on_cpu: + model_kwargs["max_memory"] = {"cpu": "256GiB"} + model_kwargs["device_map"] = {"": "cpu"} + model = PeftModel.from_pretrained( + model, + cfg.lora_model_dir, + is_trainable=(not inference), + **model_kwargs, + ) + else: + model = get_peft_model(model, lora_config) + + if rank == 0: + try: + model.print_trainable_parameters() + except AttributeError as exc: + LOG.warning( + "Exception caught during model.print_trainable_parameters(): %s", exc + ) + elif ( + cfg.fsdp + and cfg.adapter + and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading + and rank != 0 + ): + setup_quantized_peft_meta_for_training(model) + + return model, lora_config + + +def load_adapter( + model: PreTrainedModel, + cfg: DictDefault, + adapter: str | None, + inference: bool = False, +) -> tuple[PreTrainedModel | PeftModel | PeftMixedModel, PeftConfig | None]: + if adapter is None: + return model, None + if hasattr(model, "enable_input_require_grads"): + model.enable_input_require_grads() + if adapter in ["lora", "qlora"]: + peft_model, lora_config = load_lora(model, cfg, inference=inference) + return peft_model, lora_config + if adapter == "llama-adapter": + peft_model, lora_config = load_llama_adapter(model, cfg) + return peft_model, lora_config + + raise NotImplementedError(f"{adapter} PEFT adapter not available") + + +def load_llama_adapter( + model: PreTrainedModel, cfg: DictDefault +) -> tuple[PeftModel | PeftMixedModel, PeftConfig]: + peft_config = AdaptionPromptConfig( + adapter_layers=cfg.peft_adapter.layers, # layers (L) + adapter_len=cfg.peft_adapter.len, # prompt length (K) + task_type="CAUSAL_LM", + ) + + if cfg.lora_model_dir: + LOG.debug("Loading pretrained PEFT - llama_adapter") + peft_model = PeftModel.from_pretrained( + model, + cfg.lora_model_dir, + torch_dtype=torch.float16, + ) + else: + peft_model = get_peft_model(model, peft_config) + + peft_model.print_trainable_parameters() + + return peft_model, peft_config diff --git a/src/axolotl/loaders/constants.py b/src/axolotl/loaders/constants.py new file mode 100644 index 0000000000..c08518dd60 --- /dev/null +++ b/src/axolotl/loaders/constants.py @@ -0,0 +1,21 @@ +"""Shared constants for axolotl.loaders module""" + +from transformers import ( + Gemma3ForConditionalGeneration, + Llama4ForConditionalGeneration, + LlavaForConditionalGeneration, + Mistral3ForConditionalGeneration, + MllamaForConditionalGeneration, + Qwen2_5_VLForConditionalGeneration, + Qwen2VLForConditionalGeneration, +) + +MULTIMODAL_AUTO_MODEL_MAPPING = { + "mllama": MllamaForConditionalGeneration, + "llama4": Llama4ForConditionalGeneration, + "llava": LlavaForConditionalGeneration, + "qwen2_vl": Qwen2VLForConditionalGeneration, + "qwen2_5_vl": Qwen2_5_VLForConditionalGeneration, + "mistral3": Mistral3ForConditionalGeneration, + "gemma3": Gemma3ForConditionalGeneration, +} diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py new file mode 100644 index 0000000000..d7ac84a6d8 --- /dev/null +++ b/src/axolotl/loaders/model.py @@ -0,0 +1,754 @@ +"""Model loader class implementation for loading, configuring, and patching various +models. +""" + +import gc +import logging +import math +import os +from functools import cached_property +from importlib.util import find_spec +from typing import Any + +import peft +import torch +import transformers +import transformers.modeling_utils +from accelerate import init_empty_weights +from peft import PeftConfig, PeftMixedModel, PeftModel, prepare_model_for_kbit_training +from transformers import ( + AutoModelForCausalLM, + AutoModelForVision2Seq, + AwqConfig, + BitsAndBytesConfig, + GPTQConfig, + PreTrainedModel, + PreTrainedTokenizerBase, +) +from transformers.integrations.deepspeed import ( + HfTrainerDeepSpeedConfig, + is_deepspeed_zero3_enabled, +) + +from axolotl.common.architectures import MOE_ARCH_BLOCK +from axolotl.integrations.base import PluginManager +from axolotl.loaders.adapter import load_adapter, load_lora +from axolotl.loaders.constants import MULTIMODAL_AUTO_MODEL_MAPPING +from axolotl.loaders.patch_manager import PatchManager +from axolotl.loaders.utils import ( + get_linear_embedding_layers, + get_module_class_from_name, + load_model_config, +) +from axolotl.models.mamba import fix_mamba_attn_for_loss +from axolotl.utils.bench import log_gpu_memory_usage +from axolotl.utils.dict import DictDefault +from axolotl.utils.distributed import ( + get_device_count, + get_device_type, +) +from axolotl.utils.model_shard_quant import load_sharded_model_quant +from axolotl.utils.schemas.enums import RLType + +LOG = logging.getLogger(__name__) +PLUGIN_MANAGER = PluginManager.get_instance() + + +class ModelLoader: + """Manages model configuration, initialization and application of patches during + model loading. + + This class orchestrates the entire process of loading a model from configuration to + final preparation. It handles device mapping, quantization, attention mechanisms, + adapter integration, and various optimizations. + + The loading process includes: + - Loading and validating model configuration + - Applying monkey patches for optimizations / fixes + - Setting up device mapping (including multi-GPU configurations) + - Configuring quantization + - Setting attention mechanisms (Flash Attention, SDPA, etc.) + - Loading and initializing the model + - Applying adapters (LoRA, QLoRA, etc.) + + Attributes: + model: The loaded model instance (available after load() is called). + model_kwargs: Dictionary of keyword arguments passed to model initialization. + base_model: Name or path of the base model to load. + model_type: Type of model to load (e.g., `AutoModelForCausalLM`). + model_config: Configuration object for the model. + auto_model_loader: class used for loading the model (default: + `AutoModelForCausalLM`). + """ + + def __init__( + self, + cfg: DictDefault, + tokenizer: PreTrainedTokenizerBase, + *, + inference: bool = False, + reference_model: bool = False, + **kwargs, # pylint: disable=unused-argument + ): + """Initializes the ModelLoader. + + Args: + cfg: Configuration dictionary with model and training settings. + tokenizer: Tokenizer instance associated with the model. + processor: Optional processor for multimodal models. Defaults to None. + inference: Whether the model is being loaded for inference mode. Defaults + to False. + reference_model: Whether this is a reference model (used in setups like DPO + training). Defaults to False. + **kwargs: Additional keyword arguments (ignored). + """ + self.cfg = cfg + self.tokenizer = tokenizer + self.inference: bool = inference + self.reference_model: bool = reference_model + + # Init model kwargs + self.model_kwargs: dict[str, Any] = {} + if cfg.overrides_of_model_kwargs: + for key, val in cfg.overrides_of_model_kwargs.items(): + self.model_kwargs[key] = val + + # Init model + self.model: PreTrainedModel | PeftModel | PeftMixedModel + self.base_model = cfg.base_model + self.model_type = cfg.type_of_model + + # Init model config + self.model_config = load_model_config(cfg) + self.auto_model_loader = AutoModelForCausalLM # pylint: disable=invalid-name + + # Initialize the patch manager + self.patch_manager = PatchManager( + cfg=cfg, + model_config=self.model_config, + inference=inference, + ) + + @cached_property + def has_flash_attn(self) -> bool: + """Check if flash attention is installed.""" + return find_spec("flash_attn") is not None + + @cached_property + def qlora_fsdp(self): + """Property that determines if FSDP with QLoRA is enabled.""" + return self.cfg.fsdp and self.cfg.adapter == "qlora" + + def load(self) -> tuple[PreTrainedModel, PeftConfig | None]: + """Load and prepare the model with all configurations and patches. + + Returns: + A tuple with the loaded model and its LoRA configuration (if applicable). + """ + # Initial setup and patches + self.patch_manager.apply_pre_model_load_patches() + self._apply_pre_model_load_setup() + + # Build the model + PLUGIN_MANAGER.pre_model_load(self.cfg) + skip_move_to_device = self._build_model() + PLUGIN_MANAGER.post_model_build(self.cfg, self.model) + + # Post-build model configuration + self._apply_post_model_load_setup() + + # Load adapters (LoRA, etc.) + PLUGIN_MANAGER.pre_lora_load(self.cfg, self.model) + lora_config = self._load_adapters() + PLUGIN_MANAGER.post_lora_load(self.cfg, self.model) + + # Apply remaining patches and finalize + self._apply_post_lora_load_setup(skip_move_to_device) + self.patch_manager.apply_post_model_load_patches(self.model) + PLUGIN_MANAGER.post_model_load(self.cfg, self.model) + + return self.model, lora_config + + def _apply_pre_model_load_setup(self): + """Apply patches and setup configurations before model loading.""" + self._set_auto_model_loader() + self._set_device_map_config() + if self.cfg.revision_of_model: + self.model_kwargs["revision"] = self.cfg.revision_of_model + self._set_quantization_config() + self._set_attention_config() + + def _apply_post_model_load_setup(self): + """Configure the model after it has been loaded.""" + # Handle PeftModel if needed + if ( + isinstance(self.model, (peft.PeftModel, peft.PeftModelForCausalLM)) + and not self.qlora_fsdp + ): + self.model = self.model.merge_and_unload() + + self._resize_token_embeddings() + self._adjust_model_config() + self._log_memory_usage() + self._configure_embedding_dtypes() + + def _resize_token_embeddings(self): + """Resize token embeddings if needed.""" + embeddings_len = ( + math.ceil(len(self.tokenizer) / 32) * 32 + if self.cfg.resize_token_embeddings_to_32x + else len(self.tokenizer) + ) + if hasattr(self.model, "get_input_embeddings") and ( + self.model.get_input_embeddings().num_embeddings < embeddings_len + or ( + self.model.get_input_embeddings().num_embeddings > embeddings_len + and self.cfg.shrink_embeddings + ) + ): + resize_kwargs = {} + if self.cfg.mean_resizing_embeddings is not None and ( + self.model_config.model_type != "llava" + ): + resize_kwargs["mean_resizing"] = self.cfg.mean_resizing_embeddings + self.model.resize_token_embeddings(embeddings_len, **resize_kwargs) + else: + self.model.tie_weights() + + def _adjust_model_config(self): + if ( + hasattr(self.model, "config") + and hasattr(self.model.config, "max_position_embeddings") + and self.model.config.max_position_embeddings + and self.cfg.sequence_len > self.model.config.max_position_embeddings + ): + LOG.warning( + "increasing model.config.max_position_embeddings from " + f"{self.model.config.max_position_embeddings} to {self.cfg.sequence_len}" + ) + self.model.config.max_position_embeddings = self.cfg.sequence_len + + if ( + hasattr(self.model, "config") + and hasattr(self.model.config, "bos_token_id") + and self.model.config.bos_token_id + and self.model.config.bos_token_id != self.tokenizer.bos_token_id + ): + self.model.config.bos_token_id = self.tokenizer.bos_token_id + + if ( + hasattr(self.model, "config") + and hasattr(self.model.config, "eos_token_id") + and self.model.config.eos_token_id + and self.model.config.eos_token_id != self.tokenizer.eos_token_id + ): + self.model.config.eos_token_id = self.tokenizer.eos_token_id + + def _log_memory_usage(self): + """Log device memory usage after model load.""" + if hasattr(self.model, "device") and self.model.device.type in ( + "cuda", + "mps", + "npu", + ): + log_gpu_memory_usage(LOG, "after model load", self.model.device) + + def _configure_embedding_dtypes(self): + """Configure embedding module dtypes.""" + # Get embedding modules + embedding_modules = get_linear_embedding_layers(self.cfg.model_config_type) + + # Initial dtype conversion + if not self.cfg.fsdp: + # We don't run this during FSDP because this will leave mixed and bfloat16 + # dtypes in the model which FSDP doesn't like + if self.cfg.load_in_4bit and self.cfg.embeddings_skip_upcast: + embedding_modules = [] + self._convert_embedding_modules_dtype( + embedding_modules, + dist_dtype=torch.float32, + before_kbit_train_or_finetune=True, + ) + + # Handle DeepSpeed Zero3 + if is_deepspeed_zero3_enabled(): + self._set_z3_leaf_modules() + + # Apply gradient checkpointing if needed + needs_fa2_dtype = self.cfg.adapter or self.cfg.fsdp + if self.cfg.adapter in ["lora", "qlora"]: + needs_fa2_dtype = True + if self.cfg.gradient_checkpointing: + self.model.gradient_checkpointing_enable( + gradient_checkpointing_kwargs=self.cfg.gradient_checkpointing_kwargs + ) + + self._prepare_model_for_quantization() + + # Convert dtypes if needed + should_convert = ( + # LlamaRMSNorm layers are in fp32 after kbit_training or full finetune, so + # we need to convert them back to fp16/bf16 for flash-attn compatibility. + ( + (needs_fa2_dtype or self.cfg.flash_attention or self.cfg.flex_attention) + and not self.qlora_fsdp + ) + # CCE requires embedding layers to be in fp16/bf16 for backward pass + or self.cfg.cut_cross_entropy + ) + + if should_convert: + LOG.info("Converting modules to %s", self.cfg.torch_dtype) + self._convert_embedding_modules_dtype( + embedding_modules=embedding_modules, + dist_dtype=self.cfg.torch_dtype, + before_kbit_train_or_finetune=False, + ) + + def _load_adapters(self) -> PeftConfig | None: + """Load LoRA or other adapters.""" + # Load LoRA or adapter + lora_config = None + if not self.reference_model or self.cfg.lora_model_dir: + # If we're not loading the reference model, then we're loading the model + # for training. Then, the DPO trainer doesn't want the PEFT model loaded + # over it, it just wants the LoRA / PEFT config. + if ( + self.cfg.adapter + and self.cfg.rl in [RLType.DPO, RLType.IPO, RLType.KTO] + and not self.cfg.merge_lora + ): + _, lora_config = load_lora( + self.model, self.cfg, inference=False, config_only=True + ) + else: + self.model, lora_config = load_adapter( + self.model, self.cfg, self.cfg.adapter + ) + + return lora_config + + def _apply_post_lora_load_setup(self, skip_move_to_device: bool): + """Apply final optimizations and patches.""" + # Place model on accelerator + if ( + self.cfg.ddp + and not self.cfg.load_in_8bit + and not (self.cfg.rl and self.cfg.load_in_4bit) + and not skip_move_to_device + ): + # TODO: validate this conditional + self.model.to(f"{str(get_device_type())}:{self.cfg.local_rank}") + + if get_device_count() > 1 and int(os.getenv("WORLD_SIZE", "1")) == 1: + self.model.is_parallelizable = True + self.model.model_parallel = True + + if not any( + param.requires_grad + for _, param in self.model.named_parameters(recurse=True) + ): + LOG.warning("There are no parameters that require gradient updates") + + if self.cfg.flash_optimum: + from optimum.bettertransformer import BetterTransformer + + self.model = BetterTransformer.transform(self.model) + + if self.cfg.adapter is not None: + log_gpu_memory_usage(LOG, "after adapters", self.model.device) + + for _ in range(3): + gc.collect() + torch.cuda.empty_cache() + + def _set_auto_model_loader(self): + """Set `self.auto_model_loader`. Defaults to `transformers.AutoModelForCausalLM` + (set at `__init__`). When using a multimodal model, `self.auto_model_loader` + should be set according to the type of the model. + """ + if self.cfg.is_multimodal: + self.auto_model_loader = MULTIMODAL_AUTO_MODEL_MAPPING.get( + self.model_config.model_type, AutoModelForVision2Seq + ) + + def _set_device_map_config(self): + """Setup `device_map` according to config""" + device_map = self.cfg.device_map + max_memory = self.cfg.max_memory + + if self.cfg.gpu_memory_limit: + gpu_memory_limit = ( + str(self.cfg.gpu_memory_limit) + "GiB" + if isinstance(self.cfg.gpu_memory_limit, int) + else self.cfg.gpu_memory_limit + ) + + max_memory = {} + num_device = get_device_count() + for i in range(num_device): + max_memory[i] = gpu_memory_limit + max_memory["cpu"] = "256GiB" # something sufficiently large to fit anything + + if max_memory is not None: + # Based on https://github.com/togethercomputer/OpenChatKit/blob/main/inference/bot.py + from accelerate import infer_auto_device_map + + with init_empty_weights(): + model_canvas = self.auto_model_loader.from_config( + self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + ) + model_canvas.tie_weights() + device_map = infer_auto_device_map( + model_canvas, + max_memory=max_memory, + dtype=self.cfg.torch_dtype, + ) + # We can discard max_memory now as we have a device map set up + max_memory = None + + self.model_kwargs["torch_dtype"] = self.cfg.torch_dtype + + if not is_deepspeed_zero3_enabled(): + self.model_kwargs["device_map"] = device_map + + cur_device = get_device_type() + if "mps" in str(cur_device): + self.model_kwargs["device_map"] = "mps:0" + elif "npu" in str(cur_device): + self.model_kwargs["device_map"] = "npu:0" + + # TODO: can we put the reference model on it's own gpu? I think we have to move + # logits around to calculate loss + # if cfg.rl: + # if torch.cuda.device_count() > 1: + # if reference_model: + # model_kwargs["device_map"] = "cuda:" + str( + # torch.cuda.current_device() + 1 + # ) + # else: + # model_kwargs["device_map"] = "cuda:" + str(torch.cuda.current_device()) + + def _set_quantization_config(self): + """Set up quantization config (bitsandbytes, awq, gptq, etc.)""" + self.model_kwargs["load_in_8bit"] = self.cfg.load_in_8bit + self.model_kwargs["load_in_4bit"] = self.cfg.load_in_4bit + + if self.cfg.gptq: + if not hasattr(self.model_config, "quantization_config"): + LOG.warning( + "model config does not contain quantization_config information" + ) + else: + if self.cfg.gptq_disable_exllama is not None: + self.model_config.quantization_config["disable_exllama"] = ( + self.cfg.gptq_disable_exllama + ) + self.model_kwargs["quantization_config"] = GPTQConfig( + **self.model_config.quantization_config + ) + if ( + self.cfg.adapter in ["qlora", "lora"] + and hasattr(self.model_config, "quantization_config") + and self.model_config.quantization_config["quant_method"] + in ["gptq", "awq", "bitsandbytes"] + ): + if self.model_config.quantization_config["quant_method"] == "gptq": + self.model_kwargs["quantization_config"] = GPTQConfig( + **self.model_config.quantization_config + ) + elif self.model_config.quantization_config["quant_method"] == "awq": + self.model_kwargs["quantization_config"] = AwqConfig( + **self.model_config.quantization_config + ) + elif ( + self.model_config.quantization_config["quant_method"] == "bitsandbytes" + ): + self.model_kwargs["quantization_config"] = BitsAndBytesConfig( + **self.model_config.quantization_config + ) + elif self.cfg.adapter == "qlora" and self.model_kwargs["load_in_4bit"]: + bnb_config = { + "load_in_4bit": True, + "llm_int8_threshold": 6.0, + "llm_int8_has_fp16_weight": False, + "bnb_4bit_compute_dtype": self.cfg.torch_dtype, + "bnb_4bit_use_double_quant": True, + "bnb_4bit_quant_type": "nf4", + "bnb_4bit_quant_storage": torch.bfloat16, + } + if self.cfg.model_config_type in ["jamba", "qwen2_moe"] and not ( + self.cfg.deepspeed or self.cfg.fsdp + ): + # for some reason, this causes the loss to be off by an order of magnitude + # but deepspeed needs this still in bfloat16 + bnb_config["bnb_4bit_quant_storage"] = torch.float32 + + if self.cfg.bnb_config_kwargs: + bnb_config.update(self.cfg.bnb_config_kwargs) + + self.model_kwargs["quantization_config"] = BitsAndBytesConfig( + **bnb_config, + ) + elif self.cfg.adapter == "lora" and self.model_kwargs["load_in_8bit"]: + bnb_config = { + "load_in_8bit": True, + } + # Exclude mamba blocks from int8 quantization for jamba + if self.cfg.model_config_type == "jamba": + bnb_config["llm_int8_skip_modules"] = ["mamba"] + self.model_kwargs["quantization_config"] = BitsAndBytesConfig( + **bnb_config, + ) + + # no longer needed per https://github.com/huggingface/transformers/pull/26610 + if "quantization_config" in self.model_kwargs or self.cfg.gptq: + self.model_kwargs.pop("load_in_8bit", None) + self.model_kwargs.pop("load_in_4bit", None) + + def _set_attention_config(self): + """Sample packing uses custom FA2 patch""" + if self.cfg.flex_attention: + self.model_kwargs["attn_implementation"] = "flex_attention" + self.model_config._attn_implementation = ( # pylint: disable=protected-access + "flex_attention" + ) + + elif self.cfg.flash_attention: + if not self.cfg.sample_packing and self.cfg.s2_attention: + pass + self.model_kwargs["attn_implementation"] = "flash_attention_2" + self.model_config._attn_implementation = ( # pylint: disable=protected-access + "flash_attention_2" + ) + elif self.cfg.sdp_attention: + self.model_kwargs["attn_implementation"] = "sdpa" + self.model_config._attn_implementation = ( # pylint: disable=protected-access + "sdpa" + ) + elif self.cfg.eager_attention: + self.model_kwargs["attn_implementation"] = "eager" + self.model_config._attn_implementation = ( # pylint: disable=protected-access + "eager" + ) + + if self.cfg.low_cpu_mem_usage: + self.model_kwargs["low_cpu_mem_usage"] = True + + def _configure_zero3_memory_efficient_loading(self): + """Set the deepspeed config to load the model into RAM first before moving + to VRAM. + + We need to return `hf_ds_cfg` as it needs to exist before model loading. + """ + hf_ds_cfg = None + + if os.getenv("ACCELERATE_DEEPSPEED_ZERO_STAGE") == "3": + hf_ds_cfg = HfTrainerDeepSpeedConfig(self.cfg.deepspeed) + hf_ds_cfg.fill_match( + "train_micro_batch_size_per_gpu", self.cfg.micro_batch_size + ) + hf_ds_cfg.fill_match( + "gradient_accumulation_steps", self.cfg.gradient_accumulation_steps + ) + hf_ds_cfg.fill_match( + "train_batch_size", + int(os.getenv("WORLD_SIZE", "1")) + * self.cfg.micro_batch_size + * self.cfg.gradient_accumulation_steps, + ) + if "device_map" in self.model_kwargs: + del self.model_kwargs["device_map"] + + transformers.modeling_utils.is_deepspeed_zero3_enabled = lambda: True + transformers.integrations.deepspeed.is_deepspeed_zero3_enabled = ( + lambda: True + ) + + return hf_ds_cfg + + def _build_model(self) -> bool: + """Load model, with load strategy depending on config.""" + skip_move_to_device = False + if ( + self.qlora_fsdp + and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading + and ( + self.cfg.model_config_type == "dbrx" + or self.cfg.qlora_sharded_model_loading + ) + ): + quant_storage = self.cfg.torch_dtype + quantization_config = getattr( + self.model_config, "quantization_config", None + ) + quantization_config = ( + quantization_config or self.model_kwargs["quantization_config"] + ) + self.model = load_sharded_model_quant( + self.base_model, + self.model_config, + self.cfg, + quant_storage=quant_storage, + quantization_config=quantization_config, + ) + skip_move_to_device = True + elif ( + self.model_config.model_type in ["llama", "llama4"] + and not self.cfg.trust_remote_code + and not self.cfg.gptq + ): + # TODO: Do we need to open this up for all models? + if self.cfg.fsdp and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: + skip_move_to_device = True + if "device_map" in self.model_kwargs: + del self.model_kwargs["device_map"] + + self._configure_zero3_memory_efficient_loading() + + # Load model with random initialization if specified + if self.cfg.random_init_weights: + # AutoModel classes support the from_config method + if self.auto_model_loader in [ + AutoModelForCausalLM, + AutoModelForVision2Seq, + ]: + self.model = self.auto_model_loader.from_config( + config=self.model_config, + ) + else: + self.model = self.auto_model_loader(config=self.model_config) + else: + self.model = self.auto_model_loader.from_pretrained( + self.base_model, + config=self.model_config, + **self.model_kwargs, + ) + elif self.model_type == "MambaLMHeadModel": + # FIXME this is janky at best and hacked together to make it work + MambaLMHeadModel = fix_mamba_attn_for_loss() # pylint: disable=invalid-name + + self.model_kwargs["dtype"] = self.model_kwargs["torch_dtype"] + self.model_kwargs["device"] = torch.cuda.current_device() + self.model_kwargs.pop("torch_dtype", None) + self.model_kwargs.pop("device_map", None) + + self.model = MambaLMHeadModel.from_pretrained( + self.base_model, + **self.model_kwargs, + ) + elif ( + self.model_type + and self.model_type != "AutoModelForCausalLM" + and not self.cfg.trust_remote_code + ): + if self.cfg.gptq: + self.model = self.auto_model_loader.from_pretrained( + self.base_model, + config=self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + **self.model_kwargs, + ) + else: + self.model = getattr(transformers, self.model_type).from_pretrained( + self.base_model, + config=self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + **self.model_kwargs, + ) + else: + if self.cfg.gptq: + self.model = self.auto_model_loader.from_pretrained( + self.base_model, + config=self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + **self.model_kwargs, + ) + else: + if ( + self.cfg.fsdp + and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading + ): + # disabling either of these two still leads to VRAM spike before setting back down + skip_move_to_device = True + if "device_map" in self.model_kwargs: + del self.model_kwargs["device_map"] + + self._configure_zero3_memory_efficient_loading() + + self.model = self.auto_model_loader.from_pretrained( + self.base_model, + config=self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + **self.model_kwargs, + ) + if is_deepspeed_zero3_enabled(): + skip_move_to_device = True + + return skip_move_to_device + + def _set_z3_leaf_modules(self): + from deepspeed.utils import set_z3_leaf_modules + + if self.cfg.model_config_type in MOE_ARCH_BLOCK: + moe_blocks = MOE_ARCH_BLOCK[self.cfg.model_config_type] + moe_blocks = [moe_blocks] if isinstance(moe_blocks, str) else moe_blocks + set_z3_leaf_modules( + self.model, + [ + get_module_class_from_name(self.model, module_name) + for module_name in moe_blocks + ], + ) + + def _prepare_model_for_quantization(self): + """Prepare loaded model for quantization.""" + skip_prepare_model_for_kbit_training = False + if self.cfg.model_config_type == "qwen" and self.cfg.adapter == "lora": + # Qwen doesn't play nicely with LoRA if this is enabled + skip_prepare_model_for_kbit_training = True + + loftq_bits = ( + self.cfg.peft + and self.cfg.peft.loftq_config + and self.cfg.peft.loftq_config.loftq_bits + ) + if self.cfg.adapter == "lora" and loftq_bits: + skip_prepare_model_for_kbit_training = True + + if ( + self.qlora_fsdp + or (self.cfg.fsdp and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading) + or is_deepspeed_zero3_enabled() + ): + # Make sure everything is in the same dtype + skip_prepare_model_for_kbit_training = True + + if ( + not skip_prepare_model_for_kbit_training + and self.cfg.adapter in ["lora", "qlora"] + and (self.cfg.load_in_8bit or self.cfg.load_in_4bit) + ): + LOG.info("converting PEFT model w/ prepare_model_for_kbit_training") + self.model = prepare_model_for_kbit_training( + self.model, use_gradient_checkpointing=self.cfg.gradient_checkpointing + ) + + def _convert_embedding_modules_dtype( + self, + embedding_modules: list[str], + dist_dtype: torch.dtype, + before_kbit_train_or_finetune: bool, + ): + for name, module in self.model.named_modules(): + if "norm" in name: + module.to(dist_dtype) + if before_kbit_train_or_finetune: + if name.endswith(".gate"): + module.to(dist_dtype) + if self.model_config.model_type == "btlm": + # don't upcast lm_head for btlm + continue + if any(m in name for m in embedding_modules) and hasattr(module, "weight"): + module.to(dist_dtype) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py new file mode 100644 index 0000000000..f251f958da --- /dev/null +++ b/src/axolotl/loaders/patch_manager.py @@ -0,0 +1,380 @@ +"""Patch manager class implementation to complement `axolotl.loaders.ModelLoader`. + +Applies pre- and post-model load patches for various fixes and optimizations. +""" + +import importlib.util +import logging +from functools import cached_property + +import addict +import transformers +from transformers import PretrainedConfig, PreTrainedModel + +from axolotl.integrations.base import PluginManager +from axolotl.monkeypatch.multipack import ( + SUPPORTED_MULTIPACK_MODEL_TYPES, + patch_for_multipack, +) +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger(__name__) +PLUGIN_MANAGER = PluginManager.get_instance() + + +class PatchManager: + """Manages the application of patches during the model loading process.""" + + def __init__( + self, + cfg: DictDefault, + model_config: PretrainedConfig | addict.Dict, + inference: bool = False, + ): + """Initialize the `PatchManager`. + + Args: + cfg: Configuration dictionary with model and training settings. + model_config: Configuration object for the model. + inference: Whether the model is being loaded for inference mode. + """ + self.cfg = cfg + self.model_config = model_config + self.inference = inference + + @cached_property + def has_flash_attn(self) -> bool: + """Check if flash attention is installed.""" + return importlib.util.find_spec("flash_attn") is not None + + def apply_pre_model_load_patches(self): + """Apply pre-model load patches based on config.""" + self._apply_flash_attention_patches() + self._apply_fsdp_patches() + self._apply_adapter_patches() + self._apply_flex_attention_patches() + self._apply_model_specific_patches() + self._apply_fp8_patches() + self._apply_flash_attention_peft_patches() + self._apply_gradient_checkpointing_patches() + self._patch_attention() + self._apply_multipack_patches() + self._patch_llama_derived_model() + self._apply_mistral_cross_entropy_patch() + self._apply_unsloth_self_attention_patch() + + def apply_post_model_load_patches(self, model: PreTrainedModel): + """Apply patches that require the model instance.""" + self._apply_llama_flash_attn_patches(model) + self._apply_unsloth_patches(model) + self._apply_lora_kernel_patch(model) + + def _apply_flash_attention_patches(self): + """Apply patches related to Flash Attention.""" + if self.cfg.xformers_attention and self.cfg.sample_packing: + from axolotl.monkeypatch.attention import patch_xformers_attn_over_fa2 + + patch_xformers_attn_over_fa2() + self.cfg.flash_attention = True + + def _apply_fsdp_patches(self): + """Apply patches for FSDP configurations.""" + if self.cfg.fsdp_config and str(self.cfg.fsdp_config.fsdp_version) == "2": + from axolotl.monkeypatch.accelerate.fsdp2 import patch_accelerate_fsdp_utils + + patch_accelerate_fsdp_utils() + + def _apply_adapter_patches(self): + """Apply patches for adapter configurations.""" + if self.cfg.adapter and self.cfg.embeddings_skip_upcast: + from axolotl.monkeypatch.peft.utils import patch_peft_prep_code + + patch_peft_prep_code() + + def _apply_flex_attention_patches(self): + """Apply patches for flexible attention.""" + if self.cfg.flex_attention: + from axolotl.monkeypatch.attention.flex_attn import ( + patch_flex_make_mask, + patch_flex_wrapper, + ) + + flex_attn_compile_kwargs = self.cfg.flex_attn_compile_kwargs or {} + patch_flex_wrapper(**flex_attn_compile_kwargs) + patch_flex_make_mask() + + def _apply_model_specific_patches(self): + """Apply patches specific to model architectures.""" + if ( + self.cfg.model_config_type == "llama4" + and self.cfg.llama4_linearized_experts + ): + from axolotl.monkeypatch.models.llama4.modeling import ( + patch_llama4_linearized_modeling, + ) + + patch_llama4_linearized_modeling() + + if self.cfg.model_config_type == "gemma3": + from axolotl.monkeypatch.gemma3 import ( + patch_gemma3conditionalgeneration_forward, + ) + + patch_gemma3conditionalgeneration_forward() + + def _apply_fp8_patches(self): + """Apply patches for FP8 support.""" + if self.cfg.fp8: + from axolotl.monkeypatch.trainer_accelerator_args import ( + patch_create_accelerate_code_for_fp8, + ) + + patch_create_accelerate_code_for_fp8() + + def _apply_flash_attention_peft_patches(self): + """Apply patches for Flash Attention with PEFT.""" + if self.cfg.adapter: + from axolotl.monkeypatch.transformers_fa_utils import ( + patch_fa_peft_integration, + ) + + patch_fa_peft_integration() + + def _apply_gradient_checkpointing_patches(self): + """Apply patches for gradient checkpointing.""" + if self.cfg.gradient_checkpointing in ["unsloth", "offload"]: + from axolotl.monkeypatch.gradient_checkpointing import ( + hf_grad_checkpoint_offload_wrapper, + ) + + transformers.modeling_utils.checkpoint = hf_grad_checkpoint_offload_wrapper + if self.cfg.gradient_checkpointing == "offload_disk": + from axolotl.monkeypatch.gradient_checkpointing import ( + hf_grad_checkpoint_disk_offload_wrapper, + ) + + transformers.modeling_utils.checkpoint = ( + hf_grad_checkpoint_disk_offload_wrapper + ) + + def _apply_mistral_cross_entropy_patch(self): + """Apply Mistral cross entropy patch if configured.""" + if ( + self.cfg.model_config_type == "mistral" + and self.cfg.flash_attn_cross_entropy_loss + ): + from axolotl.monkeypatch.mistral_attn_hijack_flash import ( + patch_mistral_cross_entropy, + ) + + patch_mistral_cross_entropy() + + def _apply_unsloth_self_attention_patch(self): + """Apply Unsloth self-attention patches if configured.""" + if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: + from axolotl.monkeypatch.lora_kernels import patch_self_attn_lora + + patch_self_attn_lora(self.cfg) + + def _apply_multipack_patches(self): + """Apply multipack patches if necessary.""" + if ( + self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES + and (self.cfg.flash_attention or self.cfg.flex_attention) + and self.cfg.sample_packing + ): + # Get automap config if it exists + auto_map_config = None + if isinstance(self.model_config, dict) and "auto_map" in self.model_config: + auto_map_config = self.model_config["auto_map"] + elif hasattr(self.model_config, "auto_map"): + auto_map_config = self.model_config.auto_map + + # Determine if the model has remote code + if auto_map_config is not None: + has_remote_code = "AutoModelForCausalLM" in auto_map_config + else: + has_remote_code = False + + if has_remote_code and self.cfg.trust_remote_code is False: + # If explicitly set in YAML, prefer that + has_remote_code = self.cfg.trust_remote_code + + patch_for_multipack( + self.cfg.model_config_type, + model_name=self.cfg.base_model, + has_remote_code=has_remote_code, + ) + + if self.cfg.is_llama_derived_model: + self._patch_loss_llama() + + def _patch_attention(self): + """Apply attention-specific patches based on model type.""" + if not (self.cfg.flash_attention and hasattr(self.model_config, "model_type")): + return + + if self.model_config.model_type == "mllama" and self.cfg.flash_attention: + from axolotl.monkeypatch.attention.mllama import patch_mllama + + patch_mllama() + + if self.model_config.model_type == "btlm": + from axolotl.monkeypatch.btlm_attn_hijack_flash import ( + replace_btlm_attn_with_flash_attn, + ) + + replace_btlm_attn_with_flash_attn(self.cfg.base_model) + + if self.model_config.model_type == "stablelm_epoch" and self.cfg.sample_packing: + from axolotl.monkeypatch.stablelm_attn_hijack_flash import ( + replace_stablelm_attn_with_flash_attn, + ) + + replace_stablelm_attn_with_flash_attn(self.cfg.base_model) + + def _patch_loss_llama(self): + """Patch loss functions and other optimizations for LLaMA models.""" + if self.cfg.flash_attn_cross_entropy and self.has_flash_attn: + from axolotl.monkeypatch.llama_attn_hijack_flash import ( + patch_fa_llama_cross_entropy, + ) + + patch_fa_llama_cross_entropy() + elif self.cfg.unsloth_cross_entropy_loss: + from axolotl.monkeypatch.unsloth_ import integrate_cross_entropy_loss_patch + + integrate_cross_entropy_loss_patch(model_type="llama") + + if self.cfg.flash_attn_rms_norm and self.has_flash_attn: + from axolotl.monkeypatch.llama_attn_hijack_flash import patch_llama_rms_norm + + patch_llama_rms_norm() + elif self.cfg.unsloth_rms_norm: + from axolotl.monkeypatch.unsloth_ import patch_unsloth_layernorm + + patch_unsloth_layernorm() + + if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: + from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora + + patch_self_attn_lora() + + def _patch_llama_flash_attention(self, packed=False): + """Apply Flash Attention patches for LLaMA models.""" + from axolotl.monkeypatch.llama_attn_hijack_flash import ( + replace_llama_attn_with_flash_attn, + ) + + if packed: + if self.cfg.device not in ["mps", "cpu"] and not self.inference: + LOG.info("patching with flash attention for sample packing") + replace_llama_attn_with_flash_attn( + packed=True, + cross_entropy=self.cfg.flash_attn_cross_entropy, + rms_norm=self.cfg.flash_attn_rms_norm, + ) + elif self.cfg.s2_attention: + LOG.info("patching w/ flash-enabled, shifted-sparse attention") + replace_llama_attn_with_flash_attn( + packed=False, + cross_entropy=self.cfg.flash_attn_cross_entropy, + rms_norm=self.cfg.flash_attn_rms_norm, + use_shifted_sparse_attn=True, + ) + elif self.cfg.flash_attn_cross_entropy or self.cfg.flash_attn_rms_norm: + replace_llama_attn_with_flash_attn( + packed=False, + cross_entropy=self.cfg.flash_attn_cross_entropy, + rms_norm=self.cfg.flash_attn_rms_norm, + ) + + def _patch_llama_xformers_attention(self): + """Apply xformers attention patches for LLaMA models.""" + from axolotl.monkeypatch.llama_attn_hijack_xformers import ( + hijack_llama_attention, + ) + + LOG.info("Patching with xformers attention...") + hijack_llama_attention() + + def _patch_llama_sample_packing(self): + """Apply sample packing patches for LLaMA models.""" + from axolotl.monkeypatch.llama_patch_multipack import ( + hijack_llama_prepare_4d_mask, + ) + + LOG.info("Patching llama _prepare_4d_causal_attention_mask*...") + hijack_llama_prepare_4d_mask() + + def _patch_llama_derived_model(self): + """Modify all llama derived models in one block.""" + if self.cfg.is_llama_derived_model and not ( + self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES + and (self.cfg.flash_attention or self.cfg.flex_attention) + and self.cfg.sample_packing + ): + self._patch_loss_llama() + + if self.cfg.flash_attention: + self._patch_llama_flash_attention(packed=self.cfg.sample_packing) + elif self.cfg.xformers_attention: + self._patch_llama_xformers_attention() + elif self.cfg.sample_packing: + self._patch_llama_sample_packing() + elif self.cfg.s2_attention: + raise NotImplementedError( + "Shifted-sparse attention not currently implemented without flash attention." + ) + + def _apply_llama_flash_attn_patches(self, model): + """Apply LLaMA-specific flash attention patches.""" + if ( + self.model_config.model_type in ["llama", "llama4"] + and not self.cfg.trust_remote_code + and not self.cfg.gptq + and self.cfg.flash_attention + and not self.inference + ): + # TODO(MengqingCao): split these patches seperately + from axolotl.monkeypatch.llama_attn_hijack_flash import ( + is_xformers_swiglu_available, + replace_llama_mlp_with_swiglu, + replace_llama_qkv_with_fused, + ) + + if self.cfg.flash_attn_fuse_mlp and is_xformers_swiglu_available(): + LOG.info("Patching with SwiGLU...") + replace_llama_mlp_with_swiglu(model) + + if self.cfg.flash_attn_fuse_qkv: + LOG.info("Patching with fused QKV...") + replace_llama_qkv_with_fused(model) + + def _apply_unsloth_patches(self, model): + """Apply unsloth optimization patches.""" + if self.cfg.unsloth_lora_mlp: + from axolotl.monkeypatch.unsloth_ import integrate_lora_mlp_patch + + integrate_lora_mlp_patch(peft_model=model) + + if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: + from axolotl.monkeypatch.unsloth_ import integrate_lora_patch + + integrate_lora_patch(peft_model=model, cfg=self.cfg) + + if self.cfg.unsloth_rope: + from axolotl.monkeypatch.unsloth_ import integrate_rope_embeddings + + integrate_rope_embeddings() + + def _apply_lora_kernel_patch(self, model): + """Apply LoRA kernel patches.""" + if ( + self.cfg.lora_mlp_kernel + or self.cfg.lora_qkv_kernel + or self.cfg.lora_o_kernel + ): + from axolotl.monkeypatch.lora_kernels import apply_lora_kernel_patches + + apply_lora_kernel_patches(model=model, cfg=self.cfg) diff --git a/src/axolotl/loaders/processor.py b/src/axolotl/loaders/processor.py new file mode 100644 index 0000000000..57394bc670 --- /dev/null +++ b/src/axolotl/loaders/processor.py @@ -0,0 +1,56 @@ +"""Processor loading functionality for multi-modal models""" + +import logging +from typing import Any + +import transformers +from transformers import ( + AutoProcessor, + PreTrainedTokenizerBase, +) + +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger(__name__) + + +def load_processor(cfg: DictDefault, tokenizer: PreTrainedTokenizerBase): + processor_kwargs: dict[str, Any] = {} # Do we actually need this? + + processor_cls = AutoProcessor + if cfg.processor_type: + processor_cls = getattr(transformers, cfg.processor_type) + + processor = processor_cls.from_pretrained( + cfg.processor_config, + trust_remote_code=cfg.trust_remote_code or False, + tokenizer=tokenizer, + **processor_kwargs, + ) + + # Attempt to load image size from processor if available + if ( + cfg.image_size is None + and hasattr(processor, "size") + and any(dim in processor.size for dim in ["width", "height"]) + ): + im_width = None + im_height = None + if "width" in processor.size: + im_width = processor.size["width"] + if "height" in processor.size: + im_height = processor.size["height"] + + # If both width and height are set, use a tuple + if im_width is not None and im_height is not None: + cfg.image_size = (im_width, im_height) + # If only width is set, use as integer + elif im_width is not None: + cfg.image_size = im_width + # If only height is set, use as integer + elif im_height is not None: + cfg.image_size = im_height + + LOG.debug(f"Loaded image size: {cfg.image_size} from processor") + + return processor diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py new file mode 100644 index 0000000000..ec9d69e8a1 --- /dev/null +++ b/src/axolotl/loaders/tokenizer.py @@ -0,0 +1,281 @@ +"""Tokenizer loading functionality and associated utils""" + +import json +import logging +import os + +import transformers +from transformers import ( + AddedToken, + AutoTokenizer, +) + +from axolotl.integrations.base import PluginManager +from axolotl.loaders.utils import get_linear_embedding_layers, load_model_config +from axolotl.prompt_tokenizers import LLAMA_DEFAULT_EOS_TOKEN +from axolotl.utils.chat_templates import get_chat_template_from_config +from axolotl.utils.distributed import ( + barrier, + is_local_main_process, + is_main_process, +) + +LOG = logging.getLogger(__name__) +PLUGIN_MANAGER = PluginManager.get_instance() + + +def modify_tokenizer_files( + tokenizer_path: str, token_mappings: dict[int, str], output_dir: str +) -> str: + """ + Modify tokenizer files to replace added_tokens strings, save to output directory, + and return the path to the modified tokenizer. + + This only works with reserved tokens that were added to the tokenizer, not tokens + already part of the vocab. + + Args: + tokenizer_path: Path or name of the original tokenizer + token_mappings: Dict mapping {token_id (int): new_token_string} + output_dir: Directory to save the modified tokenizer + + Returns: + Path to the modified tokenizer directory + + Ref: https://github.com/huggingface/transformers/issues/27974#issuecomment-1854188941 + """ + # Create the tokenizer directory in output_dir if it doesn't exist + tokenizer_dir = os.path.join(output_dir, "tokenizer") + os.makedirs(tokenizer_dir, exist_ok=True) + + if is_local_main_process(): # pylint: disable=too-many-nested-blocks + # Load the tokenizer + temp_tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, use_fast=True) + + # Save the tokenizer to the output directory + temp_tokenizer.save_pretrained(tokenizer_dir) + + # Get the token IDs and map them to their new values + token_id_mappings = { + int(token_id): new_value for token_id, new_value in token_mappings.items() + } + + # 1. Update tokenizer_config.json - added_tokens_decoder + config_path = os.path.join(tokenizer_dir, "tokenizer_config.json") + if os.path.exists(config_path): + with open(config_path, "r", encoding="utf-8") as f: + config_data = json.load(f) + + # Update added_tokens_decoder + if "added_tokens_decoder" in config_data: + for token_id, new_value in token_id_mappings.items(): + token_id_str = str(token_id) + if token_id_str in config_data["added_tokens_decoder"]: + config_data["added_tokens_decoder"][token_id_str][ + "content" + ] = new_value + else: + raise ValueError( + f"Token ID {token_id_str} not found in added_tokens_decoder" + ) + + # Write the updated config back + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config_data, f, indent=2) + + # 2. Update tokenizer.json - added_tokens + tokenizer_path = os.path.join(tokenizer_dir, "tokenizer.json") + if os.path.exists(tokenizer_path): + with open(tokenizer_path, "r", encoding="utf-8") as f: + tokenizer_data = json.load(f) + + # Update added_tokens + if "added_tokens" in tokenizer_data: + for token_id, new_value in token_id_mappings.items(): + for i, token_entry in enumerate(tokenizer_data["added_tokens"]): + if token_entry["id"] == token_id: + tokenizer_data["added_tokens"][i]["content"] = new_value + break + else: + # Reaching this section means the token_id was not found in tokenizer.json added_tokens + raise ValueError( + f"Token ID {token_id} not found in added_tokens" + ) + if "model" in tokenizer_data and "vocab" in tokenizer_data["model"]: + for token_id, new_value in token_id_mappings.items(): + for entry_val, entry_id in tokenizer_data["model"]["vocab"].items(): + if entry_id == token_id: + del tokenizer_data["model"]["vocab"][entry_val] + tokenizer_data["model"]["vocab"][new_value] = token_id + break + + # Write the updated tokenizer data back + with open(tokenizer_path, "w", encoding="utf-8") as f: + json.dump(tokenizer_data, f, indent=2) + + barrier() + return tokenizer_dir + + +def load_tokenizer(cfg): + """Load and configure the tokenizer based on the provided config.""" + model_config = load_model_config(cfg) + tokenizer_kwargs = {} + use_fast = True # this is the default + + if cfg.tokenizer_use_fast is not None: + use_fast = cfg.tokenizer_use_fast + if cfg.tokenizer_legacy is not None: + # True is the default w/ https://github.com/huggingface/transformers/pull/25224 + tokenizer_kwargs["legacy"] = cfg.tokenizer_legacy + + tokenizer_cls = AutoTokenizer + if cfg.tokenizer_type: + tokenizer_cls = getattr(transformers, cfg.tokenizer_type) + + # Set base tokenizer path + tokenizer_path = cfg.tokenizer_config + + # Apply token string overrides if specified + if cfg.added_tokens_overrides: + # Modify tokenizer files and get path to modified tokenizer + tokenizer_path = modify_tokenizer_files( + tokenizer_path, cfg.added_tokens_overrides, output_dir=cfg.output_dir + ) + + tokenizer = tokenizer_cls.from_pretrained( + tokenizer_path, + trust_remote_code=cfg.trust_remote_code or False, + use_fast=use_fast, + **tokenizer_kwargs, + ) + + if ( + tokenizer.__class__.__name__ + in [ + "LlamaTokenizer", + "LlamaTokenizerFast", + "CodeLlamaTokenizer", + "CodeLlamaTokenizerFast", + ] + and hasattr(tokenizer, "pad_token") + and not tokenizer.pad_token + ): + # set a pad_token, but use eos_token so we don't add a new token + tokenizer.pad_token = LLAMA_DEFAULT_EOS_TOKEN + + if tokenizer.__class__.__name__ == "GPTNeoXTokenizerFast": + tokenizer.add_special_tokens({"pad_token": "[PAD]"}) + os.environ["TOKENIZERS_PARALLELISM"] = "false" + + # Mistral's official FA implementation requires left padding + if cfg.is_mistral_derived_model and cfg.flash_attention and not cfg.sample_packing: + tokenizer.padding_side = "left" + + # Qwen base only has single token, so we need to set the special tokens + if cfg.is_qwen_derived_model: + token_ids = ["bos_token_id", "eos_token_id", "pad_token_id", "unk_token_id"] + for attr_name in token_ids: + if getattr(tokenizer, attr_name) is None: + setattr(tokenizer, attr_name, tokenizer.eod_id) + + token_names = ["bos_token", "eos_token", "pad_token", "unk_token"] + for attr_name in token_names: + if getattr(tokenizer, attr_name) is None: + setattr(tokenizer, attr_name, "<|endoftext|>") + + additional_special_tokens = None + if cfg.special_tokens: + special_tokens = cfg.special_tokens.to_dict() + additional_special_tokens = special_tokens.pop( + "additional_special_tokens", None + ) + lora_modules_to_save = get_linear_embedding_layers(model_config.model_type) + for k, val in special_tokens.items(): + # check if new special token is not already in tokenizer and + # is adapter training to make sure lora_modules_to_save is set + # pylint: disable=too-many-boolean-expressions + if ( + (getattr(tokenizer, k) is None or getattr(tokenizer, k) != val) + and (len(tokenizer.encode(val, add_special_tokens=False)) > 2) + and cfg.adapter + and ( + not cfg.lora_modules_to_save + or not all( + x in cfg.lora_modules_to_save for x in lora_modules_to_save + ) + ) + and k != "pad_token" + ): + lora_modules_to_save = ", ".join( + [f"`{x}`" for x in lora_modules_to_save] + ) + raise ValueError( + f"Please set lora_modules_to_save to [{lora_modules_to_save}] when using an adapter and changing the special tokens." + ) + + tokenizer.add_special_tokens( + {k: AddedToken(val, rstrip=False, lstrip=False, normalized=False)} + ) + + # If we add bos_token and eos_token, we need to update the post processor to + # handle them correctly. + # https://github.com/huggingface/transformers/pull/24132 + bos_or_eos_in_special_tokens = ( + "bos_token" in cfg.special_tokens and "eos_token" in cfg.special_tokens + ) + if ( + tokenizer.__class__.__name__ + in ( + "LlamaTokenizerFast", + "CodeLlamaTokenizerFast", + ) + and bos_or_eos_in_special_tokens + ): + tokenizer.update_post_processor() + + if cfg.tokens: + tokenizer.add_tokens( + [ + AddedToken(token, rstrip=False, lstrip=False, normalized=False) + for token in cfg.tokens + ] + ) + + # Additional special tokens are a List, and need to be treated differently than regular special + # tokens. We add them after we have called `add_tokens` in case these additional special tokens + # are new tokens. + # + # Usage: + # + # ```py + # special_tokens: + # additional_special_tokens: ["<|im_start|>", "<|im_end|>"] + # ``` + if additional_special_tokens is not None: + tokenizer.add_special_tokens( + {"additional_special_tokens": additional_special_tokens} + ) + + if is_main_process(use_environ=True): + LOG.debug(f"EOS: {tokenizer.eos_token_id} / {tokenizer.eos_token}") + LOG.debug(f"BOS: {tokenizer.bos_token_id} / {tokenizer.bos_token}") + LOG.debug(f"PAD: {tokenizer.pad_token_id} / {tokenizer.pad_token}") + LOG.debug(f"UNK: {tokenizer.unk_token_id} / {tokenizer.unk_token}") + + if cfg.chat_template: + chat_template_string = get_chat_template_from_config( + cfg=cfg, + tokenizer=tokenizer, + ) + if cfg.default_system_message and cfg.chat_template == "chatml": + chat_template_string = chat_template_string.replace( + "You are a helpful assistant.", cfg.default_system_message + ) + + tokenizer.chat_template = chat_template_string + else: + LOG.info( + "No Chat template selected. Consider adding a chat template for easier inference." + ) + return tokenizer diff --git a/src/axolotl/loaders/utils.py b/src/axolotl/loaders/utils.py new file mode 100644 index 0000000000..1aae4834d1 --- /dev/null +++ b/src/axolotl/loaders/utils.py @@ -0,0 +1,211 @@ +"""Utilities for axolotl.loaders module""" + +import contextlib +import logging +from typing import Type + +import addict +import torch +from transformers import AutoConfig, PretrainedConfig, PreTrainedModel + +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger(__name__) + + +def get_module_class_from_name( + module: torch.nn.Module, name: str +) -> Type[torch.nn.Module] | None: + """Gets a class from a module by its name. Copied from `accelerate.utils.dataclasses` + (https://github.com/huggingface/accelerate/blob/main/src/accelerate/utils/dataclasses.py#L2805). + + Args: + module: The module to get the class from. + name: The name of the class. + + Returns: + The class type of the matching module, or `None` if no match is found. + """ + modules_children = list(module.children()) + if module.__class__.__name__ == name: + return module.__class__ + + if len(modules_children) == 0: + return None + + for child_module in modules_children: + module_class = get_module_class_from_name(child_module, name) + if module_class is not None: + return module_class + + return None + + +def check_model_config(cfg: DictDefault, model_config: PretrainedConfig): + """Validates and adjusts model config based on `axolotl` config. + + This function performs several important checks and adjustments: + - Disables model caching for better memory efficiency + - Handles multimodal model-specific configurations + - Validates quantization settings + - Ensures proper LoRA configuration when using adapters with new tokens + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + model_config: The model's configuration object from `transformers`. + + Raises: + ValueError: If a multimodal model lacks text configuration, if GPTQ settings + are inconsistent, or if LoRA `modules_to_save` is improperly configured + with new tokens. + """ + if hasattr(model_config, "use_cache"): + model_config.use_cache = False + + if cfg.is_multimodal: + # For multimodal configs, use_cache is set in the text_config + if hasattr(model_config, "get_text_config"): + text_config = model_config.get_text_config() + if hasattr(text_config, "use_cache"): + text_config.use_cache = False + else: + raise ValueError( + "No text config found for multimodal model. Please raise an Issue with model details." + ) + + # Check if image_size is not set and load image size from model config if available + if ( + cfg.image_size is None + and hasattr(model_config, "vision_config") + and hasattr(model_config.vision_config, "image_size") + ): + cfg.image_size = model_config.vision_config.image_size + LOG.debug(f"Loaded image size: {cfg.image_size} from model config") + + quant_config_exists = ( + hasattr(model_config, "quantization_config") + and model_config.quantization_config + ) + + # Detect compressed-tensors config + is_compressed_tensors_config = ( + quant_config_exists + and model_config.quantization_config.get("quant_method") == "compressed-tensors" + ) + + if is_compressed_tensors_config: + if model_config.quantization_config.get("config_groups"): + LOG.warning( + "Found `config_groups` in a compressed-tensors config. " + "QAT integration with llmcompressor is not tested." + ) + # Skip further quant checks for compressed-tensors + return + + quant_config_method_is_gptq = ( + quant_config_exists + and "quant_method" in model_config.quantization_config + and model_config.quantization_config["quant_method"] == "gptq" + ) + + if cfg.gptq and not quant_config_method_is_gptq: + raise ValueError( + "model_config.quantization_config is not set or quant_method is not set to gptq. " + "Please make sure to point to a GPTQ model." + ) + + lora_modules_to_save = get_linear_embedding_layers(model_config.model_type) + if ( + cfg.adapter + and cfg.tokens + and ( + not cfg.lora_modules_to_save + or not all(x in cfg.lora_modules_to_save for x in lora_modules_to_save) + ) + ): + lora_modules_to_save_joined = ", ".join( + map(lambda x: f"`{x}`", lora_modules_to_save) + ) + raise ValueError( + "`lora_modules_to_save` not properly set when adding new tokens. " + f"Please include [{lora_modules_to_save_joined}] in `lora_modules_to_save`." + ) + + +def load_model_config(cfg: DictDefault) -> PretrainedConfig | addict.Dict: + """Loads and configures a model configuration from HuggingFace or local sources. + + This function determines the appropriate model config source, loads it, applies any + necessary overrides, and validates it for compatibility with the `axolotl` config. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + + Returns: + A configured model configuration object (`AutoConfig` instance), or a simple + dictionary configuration for special cases like Mamba models. + + Raises: + ValueError: If configuration loading fails for reasons other than special cases + that are handled (e.g., Mamba models). + """ + model_config_name = cfg.base_model_config or cfg.base_model + if not model_config_name and cfg.tokenizer_config: + model_config_name = cfg.tokenizer_config + trust_remote_code = cfg.trust_remote_code is True + config_kwargs = {} + if cfg.revision_of_model: + config_kwargs["revision"] = cfg.revision_of_model + if cfg.num_labels: + # num_labels is used to initialize classifier models + config_kwargs["num_labels"] = cfg.num_labels + try: + model_config = AutoConfig.from_pretrained( + model_config_name, + trust_remote_code=trust_remote_code, + **config_kwargs, + ) + except ValueError as error: + if "mamba" in model_config_name: + return addict.Dict( + { + "model_type": "mamba", + } + ) + raise error + + if cfg.overrides_of_model_config: + for key, val in cfg.overrides_of_model_config.items(): + setattr(model_config, key, val) + + check_model_config(cfg, model_config) + + return model_config + + +def ensure_dtype(model: PreTrainedModel, dtype: torch.dtype = torch.bfloat16): + """Ensures all modules in the model are converted to the specified data type.""" + for name, module in model.named_modules(): + weight_mismatch = False + with contextlib.suppress(AttributeError): + weight_mismatch = module.weight.dtype != dtype + + bias_mismatch = False + with contextlib.suppress(AttributeError): + bias_mismatch = module.bias.dtype != dtype + + if weight_mismatch: + print(f"Converting module {name}.weight: {module.weight.dtype} -> {dtype}") + if bias_mismatch: + print(f"Converting module {name}.bias: {module.bias.dtype} -> {dtype}") + if weight_mismatch or bias_mismatch: + module.to(dtype) + + +def get_linear_embedding_layers(model_type: str) -> list[str]: + """Returns layer names of linear embeddings needed for LoRA based on model type.""" + if model_type == "gpt_neox": + return ["embed_in", "embed_out"] + if model_type == "falcon": + return ["word_embeddings", "lm_head"] + return ["embed_tokens", "lm_head"] diff --git a/src/axolotl/utils/gradient_checkpointing/__init__.py b/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py similarity index 91% rename from src/axolotl/utils/gradient_checkpointing/__init__.py rename to src/axolotl/monkeypatch/gradient_checkpointing/__init__.py index ae0c559e96..5d631776b2 100644 --- a/src/axolotl/utils/gradient_checkpointing/__init__.py +++ b/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py @@ -5,10 +5,10 @@ from packaging import version -from axolotl.utils.gradient_checkpointing.offload_cpu import ( +from axolotl.monkeypatch.gradient_checkpointing.offload_cpu import ( CPU_Offloaded_Gradient_Checkpointer, ) -from axolotl.utils.gradient_checkpointing.offload_disk import ( +from axolotl.monkeypatch.gradient_checkpointing.offload_disk import ( Disco, ) diff --git a/src/axolotl/utils/gradient_checkpointing/offload_cpu.py b/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py similarity index 100% rename from src/axolotl/utils/gradient_checkpointing/offload_cpu.py rename to src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py diff --git a/src/axolotl/utils/gradient_checkpointing/offload_disk.py b/src/axolotl/monkeypatch/gradient_checkpointing/offload_disk.py similarity index 100% rename from src/axolotl/utils/gradient_checkpointing/offload_disk.py rename to src/axolotl/monkeypatch/gradient_checkpointing/offload_disk.py diff --git a/src/axolotl/monkeypatch/peft/utils.py b/src/axolotl/monkeypatch/peft/utils.py index b3703d398f..fdc49c5f6a 100644 --- a/src/axolotl/monkeypatch/peft/utils.py +++ b/src/axolotl/monkeypatch/peft/utils.py @@ -75,4 +75,4 @@ def patch_peft_prep_code(): exec(prep_code, globals()) # pylint: disable=exec-used # nosec B102 LOG.info("patching prepare_model_for_kbit_training to allow for overrides") peft.utils.other.prepare_model_for_kbit_training = fixed_prepare_model_for_kbit_training # pylint: disable=protected-access # pylint: disable=undefined-variable # noqa: F821 - axolotl.utils.models.prepare_model_for_kbit_training = fixed_prepare_model_for_kbit_training # pylint: disable=protected-access # pylint: disable=undefined-variable # noqa: F821 + axolotl.loaders.model.prepare_model_for_kbit_training = fixed_prepare_model_for_kbit_training # pylint: disable=protected-access # pylint: disable=undefined-variable # noqa: F821 diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 46f722eebc..52ec8f22ba 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -28,11 +28,15 @@ ) from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder from axolotl.integrations.base import PluginManager +from axolotl.loaders import ( + ModelLoader, + load_processor, + load_tokenizer, +) from axolotl.utils.ctx_managers.sequence_parallel import SequenceParallelContextManager from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import cleanup_distributed from axolotl.utils.freeze import freeze_layers_except -from axolotl.utils.models import load_model, load_processor, load_tokenizer from axolotl.utils.schemas.enums import RLType from axolotl.utils.trainer import setup_trainer @@ -76,7 +80,8 @@ def setup_model_and_tokenizer( msg += " and peft_config..." LOG.debug(msg) - model, peft_config = load_model(cfg, tokenizer, processor=processor) + model_loader = ModelLoader(cfg, tokenizer, processor=processor) + model, peft_config = model_loader.load() if model.generation_config is not None: model.generation_config.do_sample = True @@ -113,7 +118,8 @@ def setup_reference_model( model_ref = None # explicit setting to None else: # load the model again for model_ref/baseline - model_ref, _ = load_model(cfg, tokenizer, reference_model=True) + model_loader = ModelLoader(cfg, tokenizer, reference_model=True) + model_ref, _ = model_loader.load() return model_ref diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index a96cc1286b..49e4cfc6fb 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -11,9 +11,10 @@ from axolotl.integrations.base import PluginManager from axolotl.integrations.config import merge_input_args +from axolotl.loaders import MULTIMODAL_AUTO_MODEL_MAPPING +from axolotl.loaders.utils import load_model_config from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.dict import DictDefault -from axolotl.utils.models import MULTIMODAL_AUTO_MODEL_MAPPING, load_model_config from axolotl.utils.schemas.config import ( AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, ) diff --git a/src/axolotl/utils/ctx_managers/sequence_parallel.py b/src/axolotl/utils/ctx_managers/sequence_parallel.py index 2ae93acad7..491cb98771 100644 --- a/src/axolotl/utils/ctx_managers/sequence_parallel.py +++ b/src/axolotl/utils/ctx_managers/sequence_parallel.py @@ -10,7 +10,7 @@ from transformers.modeling_outputs import CausalLMOutputWithPast from transformers.utils import ModelOutput -from axolotl.monkeypatch.ring_attn.patch import ( +from axolotl.monkeypatch.ring_attn import ( get_ring_attn_group, patch_prepare_data_loader, patch_prepare_device_mesh, diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index dc59200992..15744d4c60 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -10,6 +10,7 @@ from datasets import Dataset, DatasetDict, concatenate_datasets, load_from_disk from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH +from axolotl.loaders import load_tokenizer from axolotl.prompt_strategies.dpo import load as load_dpo from axolotl.prompt_strategies.kto import load as load_kto from axolotl.prompt_strategies.orpo import load as load_orpo @@ -17,7 +18,6 @@ from axolotl.utils.data.utils import deduplicate_and_log_datasets, md5 from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import is_main_process, zero_first -from axolotl.utils.models import load_tokenizer from axolotl.utils.schemas.enums import RLType LOG = logging.getLogger(__name__) diff --git a/src/axolotl/utils/lora_embeddings.py b/src/axolotl/utils/lora_embeddings.py deleted file mode 100644 index 70f56655ea..0000000000 --- a/src/axolotl/utils/lora_embeddings.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -helpers for lora embeddings -""" - - -def get_linear_embedding_layers(model_type): - """ - returns the linear embedding layers needed for loras, dependent on the model arch - """ - if model_type == "gpt_neox": - return ["embed_in", "embed_out"] - if model_type == "falcon": - return ["word_embeddings", "lm_head"] - return ["embed_tokens", "lm_head"] diff --git a/src/axolotl/utils/models.py b/src/axolotl/utils/models.py deleted file mode 100644 index cd7499869e..0000000000 --- a/src/axolotl/utils/models.py +++ /dev/null @@ -1,1648 +0,0 @@ -"""Module for models and model loading""" - -# pylint: disable=too-many-lines -import gc -import importlib -import logging -import math -import os -import types -from functools import cached_property -from typing import Any, Dict, Optional, Tuple - -import addict -import bitsandbytes as bnb -import torch -import transformers -import transformers.modeling_utils -from accelerate import init_empty_weights -from bitsandbytes.nn import Params4bit -from peft import ( - LoftQConfig, - PeftConfig, - PeftModel, - PeftModelForCausalLM, - prepare_model_for_kbit_training, -) -from torch import nn -from transformers import ( - AddedToken, - AutoConfig, - AutoModelForCausalLM, - AutoModelForVision2Seq, - AutoProcessor, - AutoTokenizer, - AwqConfig, - BitsAndBytesConfig, - Gemma3ForConditionalGeneration, - GPTQConfig, - Llama4ForConditionalGeneration, - LlavaForConditionalGeneration, - Mistral3ForConditionalGeneration, - MllamaForConditionalGeneration, - PretrainedConfig, - PreTrainedModel, - PreTrainedTokenizerBase, - ProcessorMixin, - Qwen2_5_VLForConditionalGeneration, - Qwen2VLForConditionalGeneration, -) -from transformers.integrations.deepspeed import ( - HfTrainerDeepSpeedConfig, - is_deepspeed_zero3_enabled, -) - -from axolotl.common.architectures import MOE_ARCH_BLOCK -from axolotl.integrations.base import PluginManager -from axolotl.models.mamba import fix_mamba_attn_for_loss -from axolotl.monkeypatch.multipack import ( - SUPPORTED_MULTIPACK_MODEL_TYPES, - patch_for_multipack, -) -from axolotl.prompt_tokenizers import LLAMA_DEFAULT_EOS_TOKEN -from axolotl.utils.bench import log_gpu_memory_usage -from axolotl.utils.chat_templates import get_chat_template_from_config -from axolotl.utils.dict import DictDefault -from axolotl.utils.distributed import ( - barrier, - get_device_count, - get_device_type, - is_local_main_process, - is_main_process, -) -from axolotl.utils.gradient_checkpointing import ( - hf_grad_checkpoint_disk_offload_wrapper, - hf_grad_checkpoint_offload_wrapper, -) -from axolotl.utils.lora_embeddings import get_linear_embedding_layers -from axolotl.utils.model_shard_quant import load_sharded_model, load_sharded_model_quant -from axolotl.utils.schemas.enums import RLType - -LOG = logging.getLogger(__name__) -PLUGIN_MANAGER = PluginManager.get_instance() - -MULTIMODAL_AUTO_MODEL_MAPPING = { - "mllama": MllamaForConditionalGeneration, - "llama4": Llama4ForConditionalGeneration, - "llava": LlavaForConditionalGeneration, - "qwen2_vl": Qwen2VLForConditionalGeneration, - "qwen2_5_vl": Qwen2_5_VLForConditionalGeneration, - "mistral3": Mistral3ForConditionalGeneration, - "gemma3": Gemma3ForConditionalGeneration, -} - - -# copied from accelerator.FullyShardedDataParallelPlugin -def get_module_class_from_name(module, name): - """ - Gets a class from a module by its name. - - Args: - module (`torch.nn.Module`): The module to get the class from. - name (`str`): The name of the class. - """ - modules_children = list(module.children()) - if module.__class__.__name__ == name: - return module.__class__ - - if len(modules_children) == 0: - return None - - for child_module in modules_children: - module_class = get_module_class_from_name(child_module, name) - if module_class is not None: - return module_class - - return None - - -def check_model_config(cfg: DictDefault, model_config: PretrainedConfig): - # Set use_cache to False - if hasattr(model_config, "use_cache"): - model_config.use_cache = False - - if cfg.is_multimodal: - # For multimodal configs, use_cache is set in the text_config - if hasattr(model_config, "get_text_config"): - text_config = model_config.get_text_config() - if hasattr(text_config, "use_cache"): - text_config.use_cache = False - else: - raise ValueError( - "No text config found for multimodal model. Please raise an Issue with model details." - ) - - # check if image_size is not set and load image size from model config if available - if ( - cfg.image_size is None - and hasattr(model_config, "vision_config") - and hasattr(model_config.vision_config, "image_size") - ): - cfg.image_size = model_config.vision_config.image_size - LOG.debug(f"Loaded image size: {cfg.image_size} from model config") - - quant_config_exists = ( - hasattr(model_config, "quantization_config") - and model_config.quantization_config - ) - - # Detect compressed-tensors config - is_compressed_tensors_config = ( - quant_config_exists - and model_config.quantization_config.get("quant_method") == "compressed-tensors" - ) - - if is_compressed_tensors_config: - if model_config.quantization_config.get("config_groups"): - LOG.warning( - "Found `config_groups` in a compressed-tensors config. " - "QAT integration with llmcompressor is not tested." - ) - # Skip further quant checks for compressed-tensors - return - - quant_config_method_is_gptq = ( - quant_config_exists - and "quant_method" in model_config.quantization_config - and model_config.quantization_config["quant_method"] == "gptq" - ) - - if cfg.gptq and not quant_config_method_is_gptq: - raise ValueError( - "model_config.quantization_config is not set or quant_method is not set to gptq. " - "Please make sure to point to a GPTQ model." - ) - - lora_modules_to_save = get_linear_embedding_layers(model_config.model_type) - if ( - cfg.adapter - and cfg.tokens - and ( - not cfg.lora_modules_to_save - or not all(x in cfg.lora_modules_to_save for x in lora_modules_to_save) - ) - ): - lora_modules_to_save = ", ".join(map(lambda x: f"`{x}`", lora_modules_to_save)) - raise ValueError( - f"`lora_modules_to_save` not properly set when adding new tokens. Please include [{lora_modules_to_save}] in `lora_modules_to_save`." - ) - - -def load_model_config(cfg): - model_config_name = cfg.base_model_config or cfg.base_model - if not model_config_name and cfg.tokenizer_config: - model_config_name = cfg.tokenizer_config - trust_remote_code = cfg.trust_remote_code is True - config_kwargs = {} - if cfg.revision_of_model: - config_kwargs["revision"] = cfg.revision_of_model - if cfg.num_labels: - # num_labels is used to initialize classifier models - config_kwargs["num_labels"] = cfg.num_labels - try: - model_config = AutoConfig.from_pretrained( - model_config_name, - trust_remote_code=trust_remote_code, - **config_kwargs, - ) - except ValueError as err: - if "mamba" in model_config_name: - return addict.Dict( - { - "model_type": "mamba", - } - ) - raise err - - if cfg.overrides_of_model_config: - for key, val in cfg.overrides_of_model_config.items(): - setattr(model_config, key, val) - - check_model_config(cfg, model_config) - - return model_config - - -def modify_tokenizer_files( - tokenizer_path: str, token_mappings: Dict[int, str], output_dir: str -) -> str: - """ - Modify tokenizer files to replace added_tokens strings, save to output directory, and return the path to the modified tokenizer. - - This only works with reserved tokens that were added to the tokenizer, not tokens already part of the vocab. - - Args: - tokenizer_path: Path or name of the original tokenizer - token_mappings: Dict mapping {token_id (int): new_token_string} - output_dir: Directory to save the modified tokenizer - - Returns: - Path to the modified tokenizer directory - - Ref: https://github.com/huggingface/transformers/issues/27974#issuecomment-1854188941 - """ - - import json - - # Create the tokenizer directory in output_dir if it doesn't exist - tokenizer_dir = os.path.join(output_dir, "tokenizer") - os.makedirs(tokenizer_dir, exist_ok=True) - - if is_local_main_process(): # pylint: disable=too-many-nested-blocks - # Load the tokenizer - temp_tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, use_fast=True) - - # Save the tokenizer to the output directory - temp_tokenizer.save_pretrained(tokenizer_dir) - - # Get the token IDs and map them to their new values - token_id_mappings = { - int(token_id): new_value for token_id, new_value in token_mappings.items() - } - - # 1. Update tokenizer_config.json - added_tokens_decoder - config_path = os.path.join(tokenizer_dir, "tokenizer_config.json") - if os.path.exists(config_path): - with open(config_path, "r", encoding="utf-8") as f: - config_data = json.load(f) - - # Update added_tokens_decoder - if "added_tokens_decoder" in config_data: - for token_id, new_value in token_id_mappings.items(): - token_id_str = str(token_id) - if token_id_str in config_data["added_tokens_decoder"]: - config_data["added_tokens_decoder"][token_id_str][ - "content" - ] = new_value - else: - raise ValueError( - f"Token ID {token_id_str} not found in added_tokens_decoder" - ) - - # Write the updated config back - with open(config_path, "w", encoding="utf-8") as f: - json.dump(config_data, f, indent=2) - - # 2. Update tokenizer.json - added_tokens - tokenizer_path = os.path.join(tokenizer_dir, "tokenizer.json") - if os.path.exists(tokenizer_path): - with open(tokenizer_path, "r", encoding="utf-8") as f: - tokenizer_data = json.load(f) - - # Update added_tokens - if "added_tokens" in tokenizer_data: - for token_id, new_value in token_id_mappings.items(): - for i, token_entry in enumerate(tokenizer_data["added_tokens"]): - if token_entry["id"] == token_id: - tokenizer_data["added_tokens"][i]["content"] = new_value - break - else: - # Reaching this section means the token_id was not found in tokenizer.json added_tokens - raise ValueError( - f"Token ID {token_id} not found in added_tokens" - ) - if "model" in tokenizer_data and "vocab" in tokenizer_data["model"]: - for token_id, new_value in token_id_mappings.items(): - for entry_val, entry_id in tokenizer_data["model"]["vocab"].items(): - if entry_id == token_id: - del tokenizer_data["model"]["vocab"][entry_val] - tokenizer_data["model"]["vocab"][new_value] = token_id - break - - # Write the updated tokenizer data back - with open(tokenizer_path, "w", encoding="utf-8") as f: - json.dump(tokenizer_data, f, indent=2) - - barrier() - return tokenizer_dir - - -def load_tokenizer(cfg): - """Load and configure the tokenizer based on the provided config.""" - model_config = load_model_config(cfg) - tokenizer_kwargs = {} - use_fast = True # this is the default - - if cfg.tokenizer_use_fast is not None: - use_fast = cfg.tokenizer_use_fast - if cfg.tokenizer_legacy is not None: - # True is the default w/ https://github.com/huggingface/transformers/pull/25224 - tokenizer_kwargs["legacy"] = cfg.tokenizer_legacy - - tokenizer_cls = AutoTokenizer - if cfg.tokenizer_type: - tokenizer_cls = getattr(transformers, cfg.tokenizer_type) - - # Set base tokenizer path - tokenizer_path = cfg.tokenizer_config - - # Apply token string overrides if specified - if cfg.added_tokens_overrides: - # Modify tokenizer files and get path to modified tokenizer - tokenizer_path = modify_tokenizer_files( - tokenizer_path, cfg.added_tokens_overrides, output_dir=cfg.output_dir - ) - - tokenizer = tokenizer_cls.from_pretrained( - tokenizer_path, - trust_remote_code=cfg.trust_remote_code or False, - use_fast=use_fast, - **tokenizer_kwargs, - ) - - if ( - tokenizer.__class__.__name__ - in [ - "LlamaTokenizer", - "LlamaTokenizerFast", - "CodeLlamaTokenizer", - "CodeLlamaTokenizerFast", - ] - and hasattr(tokenizer, "pad_token") - and not tokenizer.pad_token - ): - # set a pad_token, but use eos_token so we don't add a new token - tokenizer.pad_token = LLAMA_DEFAULT_EOS_TOKEN - - if tokenizer.__class__.__name__ == "GPTNeoXTokenizerFast": - tokenizer.add_special_tokens({"pad_token": "[PAD]"}) - os.environ["TOKENIZERS_PARALLELISM"] = "false" - - # Mistral's official FA implementation requires left padding - if cfg.is_mistral_derived_model and cfg.flash_attention and not cfg.sample_packing: - tokenizer.padding_side = "left" - - # Qwen base only has single token, so we need to set the special tokens - if cfg.is_qwen_derived_model: - token_ids = ["bos_token_id", "eos_token_id", "pad_token_id", "unk_token_id"] - for attr_name in token_ids: - if getattr(tokenizer, attr_name) is None: - setattr(tokenizer, attr_name, tokenizer.eod_id) - - token_names = ["bos_token", "eos_token", "pad_token", "unk_token"] - for attr_name in token_names: - if getattr(tokenizer, attr_name) is None: - setattr(tokenizer, attr_name, "<|endoftext|>") - - additional_special_tokens = None - if cfg.special_tokens: - special_tokens = cfg.special_tokens.to_dict() - additional_special_tokens = special_tokens.pop( - "additional_special_tokens", None - ) - lora_modules_to_save = get_linear_embedding_layers(model_config.model_type) - for k, val in special_tokens.items(): - # check if new special token is not already in tokenizer and - # is adapter training to make sure lora_modules_to_save is set - # pylint: disable=too-many-boolean-expressions - if ( - (getattr(tokenizer, k) is None or getattr(tokenizer, k) != val) - and (len(tokenizer.encode(val, add_special_tokens=False)) > 2) - and cfg.adapter - and ( - not cfg.lora_modules_to_save - or not all( - x in cfg.lora_modules_to_save for x in lora_modules_to_save - ) - ) - and k != "pad_token" - ): - lora_modules_to_save = ", ".join( - [f"`{x}`" for x in lora_modules_to_save] - ) - raise ValueError( - f"Please set lora_modules_to_save to [{lora_modules_to_save}] when using an adapter and changing the special tokens." - ) - - tokenizer.add_special_tokens( - {k: AddedToken(val, rstrip=False, lstrip=False, normalized=False)} - ) - - # If we add bos_token and eos_token, we need to update the post processor to - # handle them correctly. - # https://github.com/huggingface/transformers/pull/24132 - bos_or_eos_in_special_tokens = ( - "bos_token" in cfg.special_tokens and "eos_token" in cfg.special_tokens - ) - if ( - tokenizer.__class__.__name__ - in ( - "LlamaTokenizerFast", - "CodeLlamaTokenizerFast", - ) - and bos_or_eos_in_special_tokens - ): - tokenizer.update_post_processor() - - if cfg.tokens: - tokenizer.add_tokens( - [ - AddedToken(token, rstrip=False, lstrip=False, normalized=False) - for token in cfg.tokens - ] - ) - - # Additional special tokens are a List, and need to be treated differently than regular special - # tokens. We add them after we have called `add_tokens` in case these additional special tokens - # are new tokens. - # - # Usage: - # - # ```py - # special_tokens: - # additional_special_tokens: ["<|im_start|>", "<|im_end|>"] - # ``` - if additional_special_tokens is not None: - tokenizer.add_special_tokens( - {"additional_special_tokens": additional_special_tokens} - ) - - if is_main_process(use_environ=True): - LOG.debug(f"EOS: {tokenizer.eos_token_id} / {tokenizer.eos_token}") - LOG.debug(f"BOS: {tokenizer.bos_token_id} / {tokenizer.bos_token}") - LOG.debug(f"PAD: {tokenizer.pad_token_id} / {tokenizer.pad_token}") - LOG.debug(f"UNK: {tokenizer.unk_token_id} / {tokenizer.unk_token}") - - if cfg.chat_template: - chat_template_string = get_chat_template_from_config( - cfg=cfg, - tokenizer=tokenizer, - ) - if cfg.default_system_message and cfg.chat_template == "chatml": - chat_template_string = chat_template_string.replace( - "You are a helpful assistant.", cfg.default_system_message - ) - - tokenizer.chat_template = chat_template_string - else: - LOG.info( - "No Chat template selected. Consider adding a chat template for easier inference." - ) - return tokenizer - - -def load_processor(cfg: DictDefault, tokenizer: PreTrainedTokenizerBase): - processor_kwargs: Dict[str, Any] = {} # do we actually need this? - - processor_cls = AutoProcessor - if cfg.processor_type: - processor_cls = getattr(transformers, cfg.processor_type) - - processor = processor_cls.from_pretrained( - cfg.processor_config, - trust_remote_code=cfg.trust_remote_code or False, - tokenizer=tokenizer, - **processor_kwargs, - ) - - # Attempt to load image size from processor if available - if ( - cfg.image_size is None - and hasattr(processor, "size") - and any(dim in processor.size for dim in ["width", "height"]) - ): - im_width = None - im_height = None - if "width" in processor.size: - im_width = processor.size["width"] - if "height" in processor.size: - im_height = processor.size["height"] - - # If both width and height are set, use a tuple - if im_width is not None and im_height is not None: - cfg.image_size = (im_width, im_height) - # If only width is set, use as integer - elif im_width is not None: - cfg.image_size = im_width - # If only height is set, use as integer - elif im_height is not None: - cfg.image_size = im_height - - LOG.debug(f"Loaded image size: {cfg.image_size} from processor") - - return processor - - -class ModelLoader: - """ - ModelLoader: managing all the config and monkey patches while loading model - """ - - def __init__( - self, - cfg: DictDefault, - tokenizer: PreTrainedTokenizerBase, - *, - processor: ProcessorMixin = None, # pylint: disable=unused-argument - inference: bool = False, - reference_model: bool = False, - **kwargs, # pylint: disable=unused-argument - ) -> None: - self.cfg = cfg - self.tokenizer = tokenizer - self.inference: bool = inference - self.reference_model: bool = reference_model - - # init model kwargs - self.model_kwargs: Dict[str, Any] = {} - if cfg.overrides_of_model_kwargs: - for key, val in cfg.overrides_of_model_kwargs.items(): - self.model_kwargs[key] = val - - # init model - self.model: PreTrainedModel - self.base_model = cfg.base_model - self.model_type = cfg.type_of_model - - # init model config - self.model_config = load_model_config(cfg) - - self.auto_model_loader = AutoModelForCausalLM # pylint: disable=invalid-name - - def apply_patches(self) -> None: - if self.cfg.xformers_attention and self.cfg.sample_packing: - from axolotl.monkeypatch.attention import patch_xformers_attn_over_fa2 - - patch_xformers_attn_over_fa2() - self.cfg.flash_attention = True - if self.cfg.fsdp_config and str(self.cfg.fsdp_config.fsdp_version) == "2": - from axolotl.monkeypatch.accelerate.fsdp2 import patch_accelerate_fsdp_utils - - patch_accelerate_fsdp_utils() - - if self.cfg.adapter and self.cfg.embeddings_skip_upcast: - from axolotl.monkeypatch.peft.utils import patch_peft_prep_code - - patch_peft_prep_code() - - if self.cfg.flex_attention: - from axolotl.monkeypatch.attention.flex_attn import ( - patch_flex_make_mask, - patch_flex_wrapper, - ) - - flex_attn_compile_kwargs = self.cfg.flex_attn_compile_kwargs or {} - patch_flex_wrapper(**flex_attn_compile_kwargs) - patch_flex_make_mask() - - # patch gemma3 conditional generation forward before loading plugins - # as it could be overridden by plugins - if self.cfg.model_config_type == "llama4": - if self.cfg.llama4_linearized_experts: - from axolotl.monkeypatch.models.llama4.modeling import ( - patch_llama4_linearized_modeling, - ) - - patch_llama4_linearized_modeling() - - if self.cfg.model_config_type == "gemma3": - from axolotl.monkeypatch.gemma3 import ( - patch_gemma3conditionalgeneration_forward, - ) - - patch_gemma3conditionalgeneration_forward() - - # load any patches from plugins - - PLUGIN_MANAGER.pre_model_load(self.cfg) - - # monkey patch to allow additional Accelerator init kwargs - if self.cfg.fp8: - from axolotl.monkeypatch.trainer_accelerator_args import ( - patch_create_accelerate_code_for_fp8, - ) - - patch_create_accelerate_code_for_fp8() - - if self.cfg.adapter: - from axolotl.monkeypatch.transformers_fa_utils import ( - patch_fa_peft_integration, - ) - - patch_fa_peft_integration() - - if self.cfg.gradient_checkpointing in ["unsloth", "offload"]: - transformers.modeling_utils.checkpoint = hf_grad_checkpoint_offload_wrapper - if self.cfg.gradient_checkpointing == "offload_disk": - transformers.modeling_utils.checkpoint = ( - hf_grad_checkpoint_disk_offload_wrapper - ) - - if self.cfg.flash_attention: - self.patch_attention() - - if self.cfg.sample_packing and self.cfg.s2_attention: - raise ValueError( - "Received `sample_packing=true` and `s2_attention=true`; however, \ - shifted-sparse attention does not currently support sample packing." - ) - - if ( - self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES - and (self.cfg.flash_attention or self.cfg.flex_attention) - and self.cfg.sample_packing - ): - if "auto_map" in self.model_config: - try: - auto_map_config = self.model_config["auto_map"] - except TypeError: - auto_map_config = self.model_config.auto_map - has_remote_code = "AutoModelForCausalLM" in auto_map_config - else: - has_remote_code = False - - if has_remote_code and self.cfg.trust_remote_code is False: - # if explicitly set in the YAML, we should prefer that, for example if explicitly disabled - has_remote_code = self.cfg.trust_remote_code - patch_for_multipack( - self.cfg.model_config_type, - model_name=self.cfg.base_model, - has_remote_code=has_remote_code, - ) - - if self.cfg.is_llama_derived_model: - self.patch_loss_llama() - elif self.cfg.is_llama_derived_model: - self.patch_llama_derived_model() - - if ( - self.cfg.model_config_type == "mistral" - and self.cfg.flash_attn_cross_entropy_loss - ): - from axolotl.monkeypatch.mistral_attn_hijack_flash import ( - patch_mistral_cross_entropy, - ) - - patch_mistral_cross_entropy() - - if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: - from axolotl.monkeypatch.lora_kernels import patch_self_attn_lora - - patch_self_attn_lora(self.cfg) - - def patch_attention(self) -> None: - if hasattr(self.model_config, "model_type"): - if self.model_config.model_type == "mllama" and self.cfg.flash_attention: - from axolotl.monkeypatch.attention.mllama import patch_mllama - - patch_mllama() - - if self.model_config.model_type == "btlm": - from axolotl.monkeypatch.btlm_attn_hijack_flash import ( - replace_btlm_attn_with_flash_attn, - ) - - replace_btlm_attn_with_flash_attn(self.cfg.base_model) - - if ( - self.model_config.model_type == "stablelm_epoch" - and self.cfg.sample_packing - ): - from axolotl.monkeypatch.stablelm_attn_hijack_flash import ( - replace_stablelm_attn_with_flash_attn, - ) - - replace_stablelm_attn_with_flash_attn(self.cfg.base_model) - - @cached_property - def has_flash_attn(self) -> bool: - """Check if flash attention is installed""" - return importlib.util.find_spec("flash_attn") is not None - - def patch_loss_llama(self) -> None: - """Patch loss functions and other optimizations""" - if self.has_flash_attn: - from axolotl.monkeypatch.llama_attn_hijack_flash import ( - patch_fa_llama_cross_entropy, - patch_llama_rms_norm, - ) - - if self.cfg.flash_attn_cross_entropy and self.has_flash_attn: - patch_fa_llama_cross_entropy() - elif self.cfg.unsloth_cross_entropy_loss: - from axolotl.monkeypatch.unsloth_ import integrate_cross_entropy_loss_patch - - integrate_cross_entropy_loss_patch(model_type="llama") - - if self.cfg.flash_attn_rms_norm and self.has_flash_attn: - patch_llama_rms_norm() - elif self.cfg.unsloth_rms_norm: - from axolotl.monkeypatch.unsloth_ import patch_unsloth_layernorm - - patch_unsloth_layernorm() - - if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: - from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora - - patch_self_attn_lora() - - def patch_llama_derived_model(self): - """Modify all llama derived models in one block""" - self.patch_loss_llama() - - if self.cfg.flash_attention: - from axolotl.monkeypatch.llama_attn_hijack_flash import ( - replace_llama_attn_with_flash_attn, - ) - - if self.cfg.sample_packing: - if self.cfg.device not in ["mps", "cpu"] and not self.inference: - LOG.info("patching with flash attention for sample packing") - replace_llama_attn_with_flash_attn( - packed=True, - cross_entropy=self.cfg.flash_attn_cross_entropy, - rms_norm=self.cfg.flash_attn_rms_norm, - ) - elif self.cfg.s2_attention: - LOG.info("patching w/ flash-enabled, shifted-sparse attention") - replace_llama_attn_with_flash_attn( - packed=False, - cross_entropy=self.cfg.flash_attn_cross_entropy, - rms_norm=self.cfg.flash_attn_rms_norm, - use_shifted_sparse_attn=True, - ) - elif self.cfg.flash_attn_cross_entropy or self.cfg.flash_attn_rms_norm: - replace_llama_attn_with_flash_attn( - packed=False, - cross_entropy=self.cfg.flash_attn_cross_entropy, - rms_norm=self.cfg.flash_attn_rms_norm, - ) - elif self.cfg.xformers_attention: - from axolotl.monkeypatch.llama_attn_hijack_xformers import ( - hijack_llama_attention, - ) - - LOG.info("patching with xformers attention") - hijack_llama_attention() - elif self.cfg.sample_packing: - from axolotl.monkeypatch.llama_patch_multipack import ( - hijack_llama_prepare_4d_mask, - ) - - LOG.info("patching llama _prepare_4d_causal_attention_mask*") - hijack_llama_prepare_4d_mask() - elif self.cfg.s2_attention: - raise NotImplementedError( - "Shifted-sparse attention not currently implemented without flash attention." - ) - - def set_auto_model_loader(self): - """ - Set self.auto_model_loader. Defaults to `transformers.AutoModelForCausalLM` - (set at `__init__`). When using a multimodal model, `self.auto_model_loader` - should be set according to the type of the model. - """ - if self.cfg.is_multimodal: - self.auto_model_loader = MULTIMODAL_AUTO_MODEL_MAPPING.get( - self.model_config.model_type, AutoModelForVision2Seq - ) - - def set_device_map_config(self) -> None: - device_map = self.cfg.device_map - max_memory = self.cfg.max_memory - - if self.cfg.gpu_memory_limit: - gpu_memory_limit = ( - str(self.cfg.gpu_memory_limit) + "GiB" - if isinstance(self.cfg.gpu_memory_limit, int) - else self.cfg.gpu_memory_limit - ) - - max_memory = {} - num_device = get_device_count() - for i in range(num_device): - max_memory[i] = gpu_memory_limit - max_memory["cpu"] = "256GiB" # something sufficiently large to fit anything - - if max_memory is not None: - # Based on https://github.com/togethercomputer/OpenChatKit/blob/main/inference/bot.py - from accelerate import infer_auto_device_map - - with init_empty_weights(): - model_canvas = self.auto_model_loader.from_config( - self.model_config, - trust_remote_code=self.cfg.trust_remote_code or False, - ) - model_canvas.tie_weights() - device_map = infer_auto_device_map( - model_canvas, - max_memory=max_memory, - dtype=self.cfg.torch_dtype, - ) - # We can discard max_memory now as we have a device map set up for us - max_memory = None - - self.model_kwargs["device_map"] = device_map - self.model_kwargs["torch_dtype"] = self.cfg.torch_dtype - - cur_device = get_device_type() - if "mps" in str(cur_device): - self.model_kwargs["device_map"] = "mps:0" - elif "npu" in str(cur_device): - self.model_kwargs["device_map"] = "npu:0" - - # TODO can we put the reference model on it's own gpu? I think we have to move logits around to calculate loss - # if cfg.rl: - # if torch.cuda.device_count() > 1: - # if reference_model: - # model_kwargs["device_map"] = "cuda:" + str( - # torch.cuda.current_device() + 1 - # ) - # else: - # model_kwargs["device_map"] = "cuda:" + str(torch.cuda.current_device()) - - if is_deepspeed_zero3_enabled(): - del self.model_kwargs["device_map"] - - def set_quantization_config(self) -> None: - self.model_kwargs["load_in_8bit"] = self.cfg.load_in_8bit - self.model_kwargs["load_in_4bit"] = self.cfg.load_in_4bit - - if self.cfg.gptq: - if not hasattr(self.model_config, "quantization_config"): - LOG.warning( - "model config does not contain quantization_config information" - ) - else: - if self.cfg.gptq_disable_exllama is not None: - self.model_config.quantization_config["disable_exllama"] = ( - self.cfg.gptq_disable_exllama - ) - self.model_kwargs["quantization_config"] = GPTQConfig( - **self.model_config.quantization_config - ) - if ( - self.cfg.adapter in ["qlora", "lora"] - and hasattr(self.model_config, "quantization_config") - and self.model_config.quantization_config["quant_method"] - in ["gptq", "awq", "bitsandbytes"] - ): - if self.model_config.quantization_config["quant_method"] == "gptq": - self.model_kwargs["quantization_config"] = GPTQConfig( - **self.model_config.quantization_config - ) - elif self.model_config.quantization_config["quant_method"] == "awq": - self.model_kwargs["quantization_config"] = AwqConfig( - **self.model_config.quantization_config - ) - elif ( - self.model_config.quantization_config["quant_method"] == "bitsandbytes" - ): - self.model_kwargs["quantization_config"] = BitsAndBytesConfig( - **self.model_config.quantization_config - ) - elif self.cfg.adapter == "qlora" and self.model_kwargs["load_in_4bit"]: - bnb_config = { - "load_in_4bit": True, - "llm_int8_threshold": 6.0, - "llm_int8_has_fp16_weight": False, - "bnb_4bit_compute_dtype": self.cfg.torch_dtype, - "bnb_4bit_use_double_quant": True, - "bnb_4bit_quant_type": "nf4", - "bnb_4bit_quant_storage": torch.bfloat16, - } - if self.cfg.model_config_type in ["jamba", "qwen2_moe"] and not ( - self.cfg.deepspeed or self.cfg.fsdp - ): - # for some reason, this causes the loss to be off by an order of magnitude - # but deepspeed needs this still in bfloat16 - bnb_config["bnb_4bit_quant_storage"] = torch.float32 - - if self.cfg.bnb_config_kwargs: - bnb_config.update(self.cfg.bnb_config_kwargs) - - self.model_kwargs["quantization_config"] = BitsAndBytesConfig( - **bnb_config, - ) - elif self.cfg.adapter == "lora" and self.model_kwargs["load_in_8bit"]: - bnb_config = { - "load_in_8bit": True, - } - # Exclude mamba blocks from int8 quantization for jamba - if self.cfg.model_config_type == "jamba": - bnb_config["llm_int8_skip_modules"] = ["mamba"] - self.model_kwargs["quantization_config"] = BitsAndBytesConfig( - **bnb_config, - ) - - # no longer needed per https://github.com/huggingface/transformers/pull/26610 - if "quantization_config" in self.model_kwargs or self.cfg.gptq: - self.model_kwargs.pop("load_in_8bit", None) - self.model_kwargs.pop("load_in_4bit", None) - - def set_attention_config(self) -> None: - """ - sample packing uses custom FA2 patch - """ - if self.cfg.flex_attention: - self.model_kwargs["attn_implementation"] = "flex_attention" - self.model_config._attn_implementation = ( # pylint: disable=protected-access - "flex_attention" - ) - - elif self.cfg.flash_attention: - if not self.cfg.sample_packing and self.cfg.s2_attention: - pass - self.model_kwargs["attn_implementation"] = "flash_attention_2" - self.model_config._attn_implementation = ( # pylint: disable=protected-access - "flash_attention_2" - ) - elif self.cfg.sdp_attention: - self.model_kwargs["attn_implementation"] = "sdpa" - self.model_config._attn_implementation = ( # pylint: disable=protected-access - "sdpa" - ) - elif self.cfg.eager_attention: - self.model_kwargs["attn_implementation"] = "eager" - self.model_config._attn_implementation = ( # pylint: disable=protected-access - "eager" - ) - - if self.cfg.low_cpu_mem_usage: - self.model_kwargs["low_cpu_mem_usage"] = True - - def build_model(self, qlora_fsdp) -> bool: - def _configure_zero3_memory_efficient_loading(): - """ - Set the deepspeed config to load the model into RAM first before moving to VRAM. - - We need to return hf_ds_cfg as it needs to exist before model loading. - """ - hf_ds_cfg = None - - if os.getenv("ACCELERATE_DEEPSPEED_ZERO_STAGE") == "3": - hf_ds_cfg = HfTrainerDeepSpeedConfig(self.cfg.deepspeed) - hf_ds_cfg.fill_match( - "train_micro_batch_size_per_gpu", self.cfg.micro_batch_size - ) - hf_ds_cfg.fill_match( - "gradient_accumulation_steps", self.cfg.gradient_accumulation_steps - ) - hf_ds_cfg.fill_match( - "train_batch_size", - int(os.getenv("WORLD_SIZE", "1")) - * self.cfg.micro_batch_size - * self.cfg.gradient_accumulation_steps, - ) - if "device_map" in self.model_kwargs: - del self.model_kwargs["device_map"] - - transformers.modeling_utils.is_deepspeed_zero3_enabled = lambda: True - transformers.integrations.deepspeed.is_deepspeed_zero3_enabled = ( - lambda: True - ) - - return hf_ds_cfg - - skip_move_to_device = False - if ( # pylint: disable=condition-evals-to-constant) - (self.cfg.fsdp and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading) - and not qlora_fsdp - and False - ): - self.model = load_sharded_model( - self.base_model, - self.model_config, - self.cfg, - torch_dtype=self.cfg.torch_dtype, - ) - skip_move_to_device = True - elif ( - qlora_fsdp - and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading - and ( - self.cfg.model_config_type == "dbrx" - or self.cfg.qlora_sharded_model_loading - ) - ): - quant_storage = self.cfg.torch_dtype - quantization_config = hasattr( - self.model_config, "quantization_config" - ) and getattr(self.model_config, "quantization_config") - quantization_config = ( - quantization_config or self.model_kwargs["quantization_config"] - ) - self.model = load_sharded_model_quant( - self.base_model, - self.model_config, - self.cfg, - quant_storage=quant_storage, - quantization_config=quantization_config, - ) - skip_move_to_device = True - elif ( - self.model_config.model_type in ["llama", "llama4"] - and not self.cfg.trust_remote_code - and not self.cfg.gptq - ): - # TODO do we need to open this up for all models? - if self.cfg.fsdp and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: - skip_move_to_device = True - if "device_map" in self.model_kwargs: - del self.model_kwargs["device_map"] - - _ = _configure_zero3_memory_efficient_loading() - - # Load model with random initialization if specified - if self.cfg.random_init_weights: - # AutoModel classes support the from_config method - if self.auto_model_loader in [ - AutoModelForCausalLM, - AutoModelForVision2Seq, - ]: - self.model = self.auto_model_loader.from_config( - config=self.model_config, - ) - else: - self.model = self.auto_model_loader( - config=self.model_config, - ) - else: - self.model = self.auto_model_loader.from_pretrained( - self.base_model, - config=self.model_config, - **self.model_kwargs, - ) - - # TODO (MengqingCao) split these patches seperately - if self.cfg.flash_attention and not self.inference: - from axolotl.monkeypatch.llama_attn_hijack_flash import ( - is_xformers_swiglu_available, - replace_llama_mlp_with_swiglu, - replace_llama_qkv_with_fused, - ) - - if self.cfg.flash_attn_fuse_mlp and is_xformers_swiglu_available(): - LOG.info("patching with SwiGLU") - replace_llama_mlp_with_swiglu(self.model) - - if self.cfg.flash_attn_fuse_qkv: - LOG.info("patching with fused QKV") - replace_llama_qkv_with_fused(self.model) - elif self.model_type == "MambaLMHeadModel": - # FIXME this is janky at best and hacked together to make it work - MambaLMHeadModel = fix_mamba_attn_for_loss() # pylint: disable=invalid-name - - self.model_kwargs["dtype"] = self.model_kwargs["torch_dtype"] - self.model_kwargs["device"] = torch.cuda.current_device() - del self.model_kwargs["torch_dtype"] - del self.model_kwargs["device_map"] - - self.model = MambaLMHeadModel.from_pretrained( - self.base_model, - **self.model_kwargs, - ) - elif ( - self.model_type - and self.model_type != "AutoModelForCausalLM" - and not self.cfg.trust_remote_code - ): - if self.cfg.gptq: - self.model = self.auto_model_loader.from_pretrained( - self.base_model, - config=self.model_config, - trust_remote_code=self.cfg.trust_remote_code or False, - **self.model_kwargs, - ) - else: - self.model = getattr(transformers, self.model_type).from_pretrained( - self.base_model, - config=self.model_config, - trust_remote_code=self.cfg.trust_remote_code or False, - **self.model_kwargs, - ) - else: - if self.cfg.gptq: - self.model = self.auto_model_loader.from_pretrained( - self.base_model, - config=self.model_config, - trust_remote_code=self.cfg.trust_remote_code or False, - **self.model_kwargs, - ) - else: - if ( - self.cfg.fsdp - and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading - ): - # disabling either of these two still leads to VRAM spike before setting back down - skip_move_to_device = True - if "device_map" in self.model_kwargs: - del self.model_kwargs["device_map"] - - _ = _configure_zero3_memory_efficient_loading() - - self.model = self.auto_model_loader.from_pretrained( - self.base_model, - config=self.model_config, - trust_remote_code=self.cfg.trust_remote_code or False, - **self.model_kwargs, - ) - if is_deepspeed_zero3_enabled(): - skip_move_to_device = True - - return skip_move_to_device - - def adjust_model_config(self) -> None: - if ( - hasattr(self.model, "config") - and hasattr(self.model.config, "max_position_embeddings") - and self.model.config.max_position_embeddings - and self.cfg.sequence_len > self.model.config.max_position_embeddings - ): - LOG.warning( - f"increasing model.config.max_position_embeddings from {self.model.config.max_position_embeddings} to {self.cfg.sequence_len}" - ) - self.model.config.max_position_embeddings = self.cfg.sequence_len - - if ( - hasattr(self.model, "config") - and hasattr(self.model.config, "bos_token_id") - and self.model.config.bos_token_id - and self.model.config.bos_token_id != self.tokenizer.bos_token_id - ): - self.model.config.bos_token_id = self.tokenizer.bos_token_id - - if ( - hasattr(self.model, "config") - and hasattr(self.model.config, "eos_token_id") - and self.model.config.eos_token_id - and self.model.config.eos_token_id != self.tokenizer.eos_token_id - ): - self.model.config.eos_token_id = self.tokenizer.eos_token_id - - def set_z3_leaf_modules(self) -> None: - from deepspeed.utils import ( # pylint: disable=no-name-in-module - set_z3_leaf_modules, - ) - - if self.cfg.model_config_type in MOE_ARCH_BLOCK: - moe_blocks = MOE_ARCH_BLOCK[self.cfg.model_config_type] - moe_blocks = [moe_blocks] if isinstance(moe_blocks, str) else moe_blocks - set_z3_leaf_modules( - self.model, - [ - get_module_class_from_name(self.model, module_name) - for module_name in moe_blocks - ], - ) - - def prepare_model(self, qlora_fsdp: bool) -> None: - skip_prepare_model_for_kbit_training = False - if self.cfg.model_config_type == "qwen" and self.cfg.adapter == "lora": - # Qwen doesn't play nicely with LoRA if this is enabled - skip_prepare_model_for_kbit_training = True - - loftq_bits = ( - self.cfg.peft - and self.cfg.peft.loftq_config - and self.cfg.peft.loftq_config.loftq_bits - ) - if self.cfg.adapter == "lora" and loftq_bits: - skip_prepare_model_for_kbit_training = True - - if qlora_fsdp or ( - self.cfg.fsdp and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading - ): - # make sure everything is in the same dtype - skip_prepare_model_for_kbit_training = True - - if is_deepspeed_zero3_enabled(): - skip_prepare_model_for_kbit_training = True - - if ( - not skip_prepare_model_for_kbit_training - and self.cfg.adapter in ["lora", "qlora"] - and (self.cfg.load_in_8bit or self.cfg.load_in_4bit) - ): - LOG.info("converting PEFT model w/ prepare_model_for_kbit_training") - self.model = prepare_model_for_kbit_training( - self.model, use_gradient_checkpointing=self.cfg.gradient_checkpointing - ) - - def convert_embedding_modules_dtype( - self, embedding_modules, dist_dtype, before_kbit_train_or_finetune - ) -> None: - for name, module in self.model.named_modules(): - if "norm" in name: - module.to(dist_dtype) - if before_kbit_train_or_finetune: - if name.endswith(".gate"): - module.to(dist_dtype) - if self.model_config.model_type == "btlm": - # don't upcast lm_head for btlm - continue - if any(m in name for m in embedding_modules): - if hasattr(module, "weight"): - module.to(dist_dtype) - - # TODO: Deprecate this. - def apply_unsloth_lora_patch(self) -> None: - if self.cfg.unsloth_lora_mlp: - from axolotl.monkeypatch.unsloth_ import integrate_lora_mlp_patch - - integrate_lora_mlp_patch(self.model) - if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: - from axolotl.monkeypatch.unsloth_ import integrate_lora_patch - - integrate_lora_patch(self.model, self.cfg) - if self.cfg.unsloth_rope: - from axolotl.monkeypatch.unsloth_ import integrate_rope_embeddings - - integrate_rope_embeddings() - - def apply_lora_patch(self) -> None: - if ( - self.cfg.lora_mlp_kernel - or self.cfg.lora_qkv_kernel - or self.cfg.lora_o_kernel - ): - from axolotl.monkeypatch.lora_kernels import apply_lora_kernel_patches - - apply_lora_kernel_patches(self.model, self.cfg) - - def load_model(self) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: - self.apply_patches() - self.set_auto_model_loader() - self.set_device_map_config() - if self.cfg.revision_of_model: - self.model_kwargs["revision"] = self.cfg.revision_of_model - self.set_quantization_config() - self.set_attention_config() - - qlora_fsdp = self.cfg.fsdp and self.cfg.adapter == "qlora" - skip_move_to_device = False - - try: - skip_move_to_device = self.build_model(qlora_fsdp) - PLUGIN_MANAGER.post_model_build(self.cfg, self.model) - except Exception as err: # pylint: disable=broad-exception-caught - LOG.exception(err) - raise err - - if isinstance(self.model, (PeftModel, PeftModelForCausalLM)) and not qlora_fsdp: - self.model = self.model.merge_and_unload() - - embeddings_len = ( - math.ceil(len(self.tokenizer) / 32) * 32 - if self.cfg.resize_token_embeddings_to_32x - else len(self.tokenizer) - ) - if hasattr(self.model, "get_input_embeddings") and ( - self.model.get_input_embeddings().num_embeddings < embeddings_len - or ( - self.model.get_input_embeddings().num_embeddings > embeddings_len - and self.cfg.shrink_embeddings - ) - ): - resize_kwargs = {} - if self.cfg.mean_resizing_embeddings is not None and not ( - self.model_config.model_type == "llava" - ): - resize_kwargs["mean_resizing"] = self.cfg.mean_resizing_embeddings - self.model.resize_token_embeddings(embeddings_len, **resize_kwargs) - else: - self.model.tie_weights() - - self.adjust_model_config() - - # log device memory usage - if hasattr(self.model, "device") and self.model.device.type in ( - "cuda", - "mps", - "npu", - ): - log_gpu_memory_usage(LOG, "after model load", self.model.device) - - # make sure these are fp32 per Ramesh et al. (2021) - embedding_modules = get_linear_embedding_layers(self.cfg.model_config_type) - if not self.cfg.fsdp: - # we don't run this during FSDP because this will leave mixed - # float and bfloat16 dtypes in the model which FSDP doesn't like - if self.cfg.load_in_4bit and self.cfg.embeddings_skip_upcast: - embedding_modules = [] - self.convert_embedding_modules_dtype( - embedding_modules, - dist_dtype=torch.float32, - before_kbit_train_or_finetune=True, - ) - - if is_deepspeed_zero3_enabled(): - self.set_z3_leaf_modules() - - needs_fa2_dtype = self.cfg.adapter or self.cfg.fsdp - if self.cfg.adapter in ["lora", "qlora"]: - needs_fa2_dtype = True - if self.cfg.gradient_checkpointing: - self.model.gradient_checkpointing_enable( - gradient_checkpointing_kwargs=self.cfg.gradient_checkpointing_kwargs - ) - - self.prepare_model(qlora_fsdp) - - should_convert = ( - # LlamaRMSNorm layers are in fp32 after kbit_training or full finetune, so we need to - # convert them back to fp16/bf16 for flash-attn compatibility. - ( - (needs_fa2_dtype or self.cfg.flash_attention or self.cfg.flex_attention) - and not qlora_fsdp - ) - or self.cfg.cut_cross_entropy # Cut cross entropy requires embedding layers to be in fp16/bf16 for backward pass - ) - - if should_convert: - LOG.info("Converting modules to %s", self.cfg.torch_dtype) - self.convert_embedding_modules_dtype( - embedding_modules=embedding_modules, - dist_dtype=self.cfg.torch_dtype, - before_kbit_train_or_finetune=False, - ) - - PLUGIN_MANAGER.pre_lora_load(self.cfg, self.model) - - # --------------------------------------------------------- - # load lora or adapter - # --------------------------------------------------------- - lora_config = None - if not self.reference_model or self.cfg.lora_model_dir: - # if we're not loading the reference model, then we're loading the model for training - # then the dpo trainer doesn't want the peft model loaded over it, it just wants the lora/peft config - if ( - self.cfg.adapter - and self.cfg.rl in [RLType.DPO, RLType.IPO, RLType.KTO] - and not self.cfg.merge_lora - ): - _, lora_config = load_lora( - self.model, self.cfg, inference=False, config_only=True - ) - else: - self.model, lora_config = load_adapter( - self.model, self.cfg, self.cfg.adapter - ) - - # --------------------------------------------------------- - # put model to accelerator - # --------------------------------------------------------- - if ( - self.cfg.ddp - and not self.cfg.load_in_8bit - and not (self.cfg.rl and self.cfg.load_in_4bit) - and not skip_move_to_device - ): - # TODO revaldate this conditional - self.model.to(f"{str(get_device_type())}:{self.cfg.local_rank}") - - if get_device_count() > 1 and int(os.getenv("WORLD_SIZE", "1")) == 1: - setattr(self.model, "is_parallelizable", True) - setattr(self.model, "model_parallel", True) - - # --------------------------------------------------------- - # parameters that require gradient updates - # --------------------------------------------------------- - requires_grad = [] - for name, param in self.model.named_parameters(recurse=True): - if param.requires_grad: - requires_grad.append(f"{name}: {param.requires_grad}") - if len(requires_grad) == 0: - LOG.warning("there are no parameters that require gradient updates") - - if self.cfg.flash_optimum: - from optimum.bettertransformer import BetterTransformer - - self.model = BetterTransformer.transform(self.model) - - if self.cfg.adapter is not None: - log_gpu_memory_usage(LOG, "after adapters", self.model.device) - - self.apply_unsloth_lora_patch() - self.apply_lora_patch() - - for _ in range(3): - gc.collect() - torch.cuda.empty_cache() - - PLUGIN_MANAGER.post_model_load(self.cfg, self.model) - return self.model, lora_config - - -def load_model( - cfg: DictDefault, - tokenizer: PreTrainedTokenizerBase, - *, - processor: ProcessorMixin = None, # pylint: disable=unused-argument - inference: bool = False, - reference_model: bool = False, - **kwargs, # pylint: disable=unused-argument -) -> Tuple[PreTrainedModel, Optional[PeftConfig]]: - """ - Load a model for a given configuration and tokenizer. - """ - model_loader = ModelLoader( - cfg, - tokenizer, - processor=processor, - inference=inference, - reference_model=reference_model, - **kwargs, - ) - return model_loader.load_model() - - -def load_adapter(model, cfg, adapter, inference=False): - # type: (PreTrainedModel, DictDefault, Optional[str], bool) -> Tuple[PreTrainedModel, Optional[PeftConfig]] - - if adapter is None: - return model, None - if hasattr(model, "enable_input_require_grads"): - model.enable_input_require_grads() - if adapter in ["lora", "qlora"]: - model, lora_config = load_lora(model, cfg, inference=inference) - PLUGIN_MANAGER.post_lora_load(cfg, model) - return model, lora_config - if adapter == "llama-adapter": - model, lora_config = load_llama_adapter(model, cfg) - PLUGIN_MANAGER.post_lora_load(cfg, model) - return model, lora_config - - raise NotImplementedError(f"{adapter} peft adapter not available") - - -def load_llama_adapter(model, cfg): - # type: (PreTrainedModel, DictDefault) -> Tuple[PreTrainedModel, Optional[PeftConfig]] - from peft import AdaptionPromptConfig, get_peft_model - - peft_config = AdaptionPromptConfig( - adapter_layers=cfg.peft_adapter.layers, # layers (L) - adapter_len=cfg.peft_adapter.len, # prompt length (K) - task_type="CAUSAL_LM", - ) - - if cfg.lora_model_dir: - LOG.debug("Loading pretrained PEFT - llama_adapter") - model = PeftModel.from_pretrained( - model, - cfg.lora_model_dir, - torch_dtype=torch.float16, - ) - else: - model = get_peft_model(model, peft_config) - - model.print_trainable_parameters() - - return model, peft_config - - -def find_all_linear_names(model): - cls = (bnb.nn.Linear4bit, bnb.nn.Linear8bitLt, torch.nn.Linear) - lora_module_names = set() - for name, module in model.named_modules(): - if ( - isinstance(module, cls) - or "Linear" in module.__class__.__name__ - and module.__class__.__name__ not in ("LlamaLinearScalingRotaryEmbedding",) - ): - names = name.split(".") - lora_module_names.add(names[0] if len(names) == 1 else names[-1]) - - embedding_modules = get_linear_embedding_layers(model.config.model_type) - output_embedding = embedding_modules[1] - if output_embedding in lora_module_names: # needed for 16-bit - lora_module_names.remove(output_embedding) - - return list(lora_module_names) - - -def setup_quantized_meta_for_peft(model: nn.Module): - """Replaces `quant_state.to` with a dummy function to prevent PEFT from moving `quant_state` to meta device""" - - def temp_to_method(self, *args, **kwargs): # pylint: disable=unused-argument - return self - - for param in model.parameters(): - if isinstance(param, Params4bit): - param.quant_state._orig_to = ( # pylint: disable=protected-access - param.quant_state.to - ) - param.quant_state.to = types.MethodType(temp_to_method, param.quant_state) - - -def setup_quantized_peft_meta_for_training(model: nn.Module): - """Replaces dummy `quant_state.to` method with the original function to allow training to continue""" - for param in model.parameters(): - if isinstance(param, Params4bit) and hasattr(param.quant_state, "_orig_to"): - param.quant_state.to = ( - param.quant_state._orig_to # pylint: disable=protected-access - ) - param.quant_state._orig_to = None # pylint: disable=protected-access - - -def load_lora(model, cfg, inference=False, config_only=False): - # type: (PreTrainedModel, DictDefault, bool, bool) -> Tuple[Optional[PreTrainedModel], Optional[PeftConfig]] - - from peft import LoraConfig, get_peft_model - - lora_target_modules = cfg.lora_target_modules or [] - - if cfg.lora_target_linear: - linear_names = find_all_linear_names(model) - LOG.info(f"found linear modules: {repr(sorted(linear_names))}") - lora_target_modules_as_list = ( - lora_target_modules - if isinstance(lora_target_modules, list) - else [lora_target_modules] - ) - lora_target_modules = list(set(lora_target_modules_as_list + linear_names)) - - lora_config_kwargs = {} - loftq_bits = cfg.peft and cfg.peft.loftq_config and cfg.peft.loftq_config.loftq_bits - if loftq_bits: - lora_config_kwargs["loftq_config"] = LoftQConfig(loftq_bits=loftq_bits) - lora_config_kwargs["init_lora_weights"] = "loftq" - if cfg.peft_init_lora_weights: - lora_config_kwargs["init_lora_weights"] = cfg.peft_init_lora_weights - if cfg.peft_use_dora: - lora_config_kwargs["use_dora"] = cfg.peft_use_dora - LOG.info("Initializing LoRA weights using dora. This might take longer.") - if cfg.peft_use_rslora: - lora_config_kwargs["use_rslora"] = cfg.peft_use_rslora - if cfg.peft_layer_replication: - lora_config_kwargs["layer_replication"] = cfg.peft_layer_replication - - lora_config = LoraConfig( - r=cfg.lora_r, - lora_alpha=cfg.lora_alpha, - target_modules=lora_target_modules, - layers_to_transform=cfg.peft_layers_to_transform, - layers_pattern=cfg.peft_layers_pattern, - lora_dropout=cfg.lora_dropout, - fan_in_fan_out=cfg.lora_fan_in_fan_out, - modules_to_save=cfg.lora_modules_to_save if cfg.lora_modules_to_save else None, - bias="none", - task_type="CAUSAL_LM", - **lora_config_kwargs, - ) - - if config_only: - return None, lora_config - - rank = int(os.environ.get("LOCAL_RANK", 0)) - - if ( - cfg.fsdp - and cfg.adapter - and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading - and rank != 0 - ): - setup_quantized_meta_for_peft(model) - - if cfg.lora_model_dir: - LOG.debug("Loading pretrained PEFT - LoRA") - model_kwargs: Any = {} - if cfg.lora_on_cpu: - model_kwargs["max_memory"] = {"cpu": "256GiB"} - model_kwargs["device_map"] = {"": "cpu"} - model = PeftModel.from_pretrained( - model, - cfg.lora_model_dir, - is_trainable=(not inference), - **model_kwargs, - ) - else: - model = get_peft_model(model, lora_config) - - if rank == 0: - try: - model.print_trainable_parameters() - except AttributeError as exc: - LOG.warning( - "Exception caught during model.print_trainable_parameters(): %s", exc - ) - elif ( - cfg.fsdp - and cfg.adapter - and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading - and rank != 0 - ): - setup_quantized_peft_meta_for_training(model) - - return model, lora_config - - -def ensure_dtype(model, dtype=torch.bfloat16): - for name, module in model.named_modules(): - weight_mismatch = False - bias_mismatch = False - try: - weight_mismatch = module.weight.dtype != dtype - except AttributeError: - pass - try: - bias_mismatch = module.bias.dtype != dtype - except AttributeError: - pass - - if weight_mismatch: - print(f"Converting module {name}.weight: {module.weight.dtype} -> {dtype}") - if bias_mismatch: - print(f"Converting module {name}.bias: {module.bias.dtype} -> {dtype}") - if weight_mismatch or bias_mismatch: - module.to(dtype) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 8ae9d5c04b..cc5f54ac45 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -470,6 +470,16 @@ def check_sample_packing_wo_flash(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_sample_packing_with_s2attn(cls, data): + if data.get("sample_packing") and data.get("s2_attention"): + raise ValueError( + "Received `sample_packing=true` and `s2_attention=true`; however, \ + shifted-sparse attention does not currently support sample packing." + ) + return data + @model_validator(mode="before") @classmethod def check_batch_flattening_fa(cls, data): diff --git a/tests/core/test_trainer_builder.py b/tests/core/test_trainer_builder.py index d1ad273ea3..492578c40f 100644 --- a/tests/core/test_trainer_builder.py +++ b/tests/core/test_trainer_builder.py @@ -1,13 +1,11 @@ -""" -unit tests for axolotl.core.trainer_builder -""" +"""Unit tests for axolotl.core.trainer_builder""" import pytest from axolotl.core.trainer_builder import HFRLTrainerBuilder +from axolotl.loaders import ModelLoader, load_tokenizer from axolotl.utils.config import normalize_config from axolotl.utils.dict import DictDefault -from axolotl.utils.models import load_model, load_tokenizer from axolotl.utils.schemas.enums import RLType @@ -50,7 +48,7 @@ def fixture_tokenizer(cfg): @pytest.fixture(name="model") def fixture_model(cfg, tokenizer): - return load_model(cfg, tokenizer) + return ModelLoader(cfg, tokenizer).load() class TestHFRLTrainerBuilder: diff --git a/tests/e2e/patched/test_model_patches.py b/tests/e2e/patched/test_model_patches.py index 26090e697c..5ea88b001b 100644 --- a/tests/e2e/patched/test_model_patches.py +++ b/tests/e2e/patched/test_model_patches.py @@ -6,9 +6,9 @@ import transformers +from axolotl.loaders import ModelLoader, load_tokenizer from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.models import load_model, load_tokenizer from ..utils import with_temp_dir @@ -50,7 +50,7 @@ def test_mixtral_multipack(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) tokenizer = load_tokenizer(cfg) - load_model(cfg, tokenizer, inference=False) + ModelLoader(cfg, tokenizer, inference=False).load() @with_temp_dir def test_mistral_multipack(self, temp_dir): @@ -83,7 +83,7 @@ def test_mistral_multipack(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) tokenizer = load_tokenizer(cfg) - load_model(cfg, tokenizer, inference=False) + ModelLoader(cfg, tokenizer, inference=False).load() assert ( "torch.jit" diff --git a/tests/e2e/test_load_model.py b/tests/e2e/test_load_model.py index 96745c0403..5061945b44 100644 --- a/tests/e2e/test_load_model.py +++ b/tests/e2e/test_load_model.py @@ -6,8 +6,8 @@ import pytest import torch +from axolotl.loaders import ModelLoader, load_tokenizer from axolotl.utils.dict import DictDefault -from axolotl.utils.models import ModelLoader, load_model, load_tokenizer @pytest.fixture(name="temp_dir") @@ -58,6 +58,8 @@ def setup_method(self): ModelLoader( cfg=self.cfg, tokenizer="", + inference=False, + reference_model=True, ) ) @@ -71,13 +73,8 @@ def test_convert_embedding_modules_dtype( ): self.cfg.output_dir = temp_dir self.model_loader.tokenizer = load_tokenizer(self.cfg) # pylint: disable=all - self.model_loader.model, _ = load_model( - self.cfg, - self.model_loader.tokenizer, - inference=False, - reference_model=True, - ) - self.model_loader.convert_embedding_modules_dtype( + self.model_loader.load() + self.model_loader._convert_embedding_modules_dtype( embedding_modules, dist_dtype, before_kbit_train_or_finetune ) for name, module in self.model_loader.model.named_modules(): diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 683db61b28..1c7325dffb 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -9,11 +9,11 @@ import pytest from pydantic import ValidationError +from axolotl.loaders.utils import check_model_config from axolotl.utils import is_comet_available from axolotl.utils.config import validate_config from axolotl.utils.dict import DictDefault from axolotl.utils.mlflow_ import setup_mlflow_env_vars -from axolotl.utils.models import check_model_config from axolotl.utils.schemas.config import AxolotlConfigWCapabilities from axolotl.utils.wandb_ import setup_wandb_env_vars @@ -1215,6 +1215,20 @@ def test_torch_version_adopt_req(self, minimal_cfg): cfg, capabilities=capabilities, env_capabilities=env_capabilities ) + def test_cfg_throws_error_with_s2_attention_and_sample_packing(self, minimal_cfg): + test_cfg = DictDefault( + { + "s2_attention": True, + "sample_packing": True, + } + | minimal_cfg + ) + with pytest.raises( + ValidationError, + match=r".*shifted-sparse attention does not currently support sample packing*", + ): + validate_config(test_cfg) + class TestTorchCompileValidation(BaseValidation): """ diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index 1d41a248d7..29672c9e5d 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -1,7 +1,8 @@ -""" -Test suite for functions in the axolotl.utils.data.utils module, focusing on the deduplicate_and_log_datasets function. +"""Test suite for functions in the `axolotl.utils.data.utils` module, focusing on the +`deduplicate_and_log_datasets` function. -Additionally, this test suite includes tests for functions that indirectly call deduplicate_and_log_datasets during the execution of the preprocess command. +Additionally, this test suite includes tests for functions that indirectly call +`deduplicate_and_log_datasets` during the execution of the preprocess command. """ import hashlib @@ -11,20 +12,19 @@ import pytest from datasets import Dataset +from axolotl.loaders import load_processor, load_tokenizer from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.data import prepare_dataset from axolotl.utils.data.rl import load_prepare_preference_datasets from axolotl.utils.data.utils import deduplicate_and_log_datasets from axolotl.utils.dict import DictDefault -from axolotl.utils.models import load_processor, load_tokenizer from tests.constants import ALPACA_MESSAGES_CONFIG_REVISION from tests.hf_offline_utils import enable_hf_offline def verify_deduplication(actual_dataset, expected_dataset, dataset_name): - """ - Validates deduplication results and size consistency. + """Validates deduplication results and size consistency. Parameters: - actual_dataset: Deduplicated dataset. @@ -49,9 +49,7 @@ def verify_deduplication(actual_dataset, expected_dataset, dataset_name): class TestDeduplicateIndividualFunctions(unittest.TestCase): - """ - test class for deduplication function in data utils - """ + """Test class for deduplication function in data utils""" def setUp(self): # Sample data with duplicates @@ -248,7 +246,7 @@ def test_load_with_deduplication( # pylint: disable=duplicate-code with ( patch("axolotl.utils.data.rl.load_dataset_w_config") as mock_load_dataset, - patch("axolotl.utils.models.load_tokenizer") as mock_load_tokenizer, + patch("axolotl.loaders.load_tokenizer") as mock_load_tokenizer, ): # Set up the mock to return different values on successive calls mock_load_dataset.side_effect = [ @@ -272,7 +270,7 @@ def test_load_without_deduplication( # pylint: disable=duplicate-code with ( patch("axolotl.utils.data.rl.load_dataset_w_config") as mock_load_dataset, - patch("axolotl.utils.models.load_tokenizer") as mock_load_tokenizer, + patch("axolotl.loaders.load_tokenizer") as mock_load_tokenizer, ): # Set up the mock to return different values on successive calls mock_load_dataset.side_effect = [ @@ -411,7 +409,7 @@ def test_prepare_dataset_without_deduplication(self): class TestWrongCollisions(unittest.TestCase): - """Creating mock datasets for testing wrong collisions""" + """Creating mock datasets for testing wrong collisions.""" def setUp(self): self.train_data = {"text": ["sample 5", "sample 6"], "label": [1, 2]} diff --git a/tests/utils/test_models.py b/tests/test_loaders.py similarity index 83% rename from tests/utils/test_models.py rename to tests/test_loaders.py index bcc1ba5d1e..7313a82670 100644 --- a/tests/utils/test_models.py +++ b/tests/test_loaders.py @@ -1,18 +1,18 @@ -"""Module for testing models utils file.""" +"""Module for `axolotl.loaders`.""" -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest from transformers import BitsAndBytesConfig, PreTrainedTokenizerBase from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled from transformers.utils.import_utils import is_torch_mps_available +from axolotl.loaders import ModelLoader from axolotl.utils.dict import DictDefault -from axolotl.utils.models import ModelLoader, load_model class TestModelsUtils: - """Testing module for models utils.""" + """Testing module for `axolotl.loaders`.""" def setup_method(self) -> None: # load config @@ -50,7 +50,8 @@ def test_set_device_map_config(self): device_map = self.cfg.device_map if is_torch_mps_available(): device_map = "mps" - self.model_loader.set_device_map_config() + # pylint: disable=protected-access + self.model_loader._set_device_map_config() if is_deepspeed_zero3_enabled(): assert "device_map" not in self.model_loader.model_kwargs else: @@ -59,29 +60,6 @@ def test_set_device_map_config(self): # check torch_dtype assert self.cfg.torch_dtype == self.model_loader.model_kwargs["torch_dtype"] - def test_cfg_throws_error_with_s2_attention_and_sample_packing(self): - cfg = DictDefault( - { - "s2_attention": True, - "sample_packing": True, - "base_model": "", - "model_type": "AutoModelForCausalLM", - } - ) - - # Mock out call to HF hub - with patch( - "axolotl.utils.models.load_model_config" - ) as mocked_load_model_config: - mocked_load_model_config.return_value = {} - with pytest.raises(ValueError) as exc: - # Should error before hitting tokenizer, so we pass in an empty str - load_model(cfg, tokenizer="") # type: ignore - assert ( - "shifted-sparse attention does not currently support sample packing" - in str(exc.value) - ) - @pytest.mark.parametrize("adapter", ["lora", "qlora", None]) @pytest.mark.parametrize("load_in_8bit", [True, False]) @pytest.mark.parametrize("load_in_4bit", [True, False]) @@ -99,7 +77,8 @@ def test_set_quantization_config( self.cfg.gptq = gptq self.cfg.adapter = adapter - self.model_loader.set_quantization_config() + # pylint: disable=protected-access + self.model_loader._set_quantization_config() if "quantization_config" in self.model_loader.model_kwargs or self.cfg.gptq: assert not ( hasattr(self.model_loader.model_kwargs, "load_in_8bit") diff --git a/tests/test_lora.py b/tests/test_lora.py index 540371bef4..6edcdd88e4 100644 --- a/tests/test_lora.py +++ b/tests/test_lora.py @@ -2,9 +2,9 @@ tests for loading loras """ +from axolotl.loaders import ModelLoader, load_tokenizer from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.models import load_model, load_tokenizer # pylint: disable=duplicate-code minimal_config = DictDefault( @@ -46,7 +46,7 @@ def test_load_lora_weights(self): cfg = validate_config(cfg) normalize_config(cfg) tokenizer = load_tokenizer(cfg) - load_model(cfg, tokenizer) + ModelLoader(cfg, tokenizer).load() def test_load_lora_weights_empty_dropout(self): cfg = DictDefault( @@ -67,4 +67,4 @@ def test_load_lora_weights_empty_dropout(self): normalize_config(cfg) assert cfg.lora_dropout == 0.0 tokenizer = load_tokenizer(cfg) - load_model(cfg, tokenizer) + ModelLoader(cfg, tokenizer).load() diff --git a/tests/test_tokenizers.py b/tests/test_tokenizers.py index ffd51bc29b..4064620387 100644 --- a/tests/test_tokenizers.py +++ b/tests/test_tokenizers.py @@ -6,8 +6,8 @@ import pytest +from axolotl.loaders import load_tokenizer from axolotl.utils.dict import DictDefault -from axolotl.utils.models import load_tokenizer from tests.hf_offline_utils import enable_hf_offline diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From a535b68043d25c892ab554622621c927f53027e6 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 23 May 2025 16:28:31 -0400 Subject: [PATCH 0668/1405] update quarto for model loading refactor (#2716) * update quarto for model loading refactor * fix desc --- _quarto.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/_quarto.yml b/_quarto.yml index c09aecaeab..a530e380ab 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -54,6 +54,15 @@ quartodoc: - core.trainers.grpo.trainer - core.trainers.grpo.sampler - core.trainers.utils + - title: Model Loading + desc: Functionality for loading and patching models, tokenizers, etc. + contents: + - loaders.model + - loaders.tokenizer + - loaders.processor + - loaders.adapter + - loaders.patch_manager + - loaders.constants - title: Mixins desc: Mixin classes for augmenting trainers contents: @@ -123,11 +132,9 @@ quartodoc: - title: Utils desc: Utility functions contents: - - utils.models - utils.tokenization - utils.chat_templates - utils.lora - - utils.lora_embeddings - utils.model_shard_quant - utils.bench - utils.freeze From d27c35ac445e7da68981006b2437abdba01b1f90 Mon Sep 17 00:00:00 2001 From: xzuyn <16216325+xzuyn@users.noreply.github.com> Date: Fri, 23 May 2025 18:40:43 -0400 Subject: [PATCH 0669/1405] Liger GraniteMoE (#2715) --- src/axolotl/integrations/liger/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index 22d9886333..c7ac423729 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -175,6 +175,16 @@ def pre_model_load(self, cfg): rms_norm=cfg.liger_rms_norm, layer_norm=cfg.liger_layer_norm, ) + elif cfg.model_config_type == "granitemoe": + from liger_kernel.transformers import apply_liger_kernel_to_granite + + apply_liger_kernel_to_granite( + rope=cfg.liger_rope, + cross_entropy=cfg.liger_cross_entropy, + fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, + rms_norm=cfg.liger_rms_norm, + swiglu=cfg.liger_glu_activation, + ) else: logging.warning( f"Unsupported model config type: {cfg.model_config_type}. Liger not applied." From 5eb01f3df194ce6d663cebf2de8f3fb8fe7ec8e0 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 23 May 2025 21:16:51 -0400 Subject: [PATCH 0670/1405] Fix quarto (#2717) * missing modules * fix quarto complaints --- _quarto.yml | 4 +- docs/getting-started.qmd | 2 +- src/axolotl/integrations/base.py | 46 ++++--- src/axolotl/utils/samplers/multipack.py | 158 ++++++++++++------------ 4 files changed, 106 insertions(+), 104 deletions(-) diff --git a/_quarto.yml b/_quarto.yml index a530e380ab..df6992d92d 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -129,6 +129,8 @@ quartodoc: - monkeypatch.attention.mllama - monkeypatch.data.batch_dataset_fetcher - monkeypatch.mixtral + - monkeypatch.gradient_checkpointing.offload_cpu + - monkeypatch.gradient_checkpointing.offload_disk - title: Utils desc: Utility functions contents: @@ -145,8 +147,6 @@ quartodoc: - utils.optimizers.adopt - utils.data.pretraining - utils.data.sft - - utils.gradient_checkpointing.offload_cpu - - utils.gradient_checkpointing.offload_disk - title: Schemas desc: Pydantic data models for Axolotl config contents: diff --git a/docs/getting-started.qmd b/docs/getting-started.qmd index 064985e35a..6f1b543488 100644 --- a/docs/getting-started.qmd +++ b/docs/getting-started.qmd @@ -180,7 +180,7 @@ Now that you have the basics, you might want to: Check our other guides for details on these topics: - [Configuration Guide](config.qmd) - Full configuration options -- [Dataset Loading](dataset-loading.qmd) - Loading datasets from various sources +- [Dataset Loading](dataset_loading.qmd) - Loading datasets from various sources - [Dataset Formats](dataset-formats) - Working with different data formats - [Multi-GPU Training](multi-gpu.qmd) - [Multi-Node Training](multi-node.qmd) diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 2beaf667ac..eb2b29cbe7 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -39,31 +39,39 @@ class BasePlugin: """Base class for all plugins. Defines the interface for plugin methods. - Methods: - register(cfg): Registers the plugin with the given configuration. - load_datasets(cfg): Loads and preprocesses the dataset for training. - pre_model_load(cfg): Performs actions before the model is loaded. - post_model_build(cfg, model): Performs actions after the model is loaded, but + A plugin is a reusable, modular, and self-contained piece of code that extends + the functionality of Axolotl. Plugins can be used to integrate third-party models, + modify the training process, or add new features. + + To create a new plugin, you need to inherit from the BasePlugin class and + implement the required methods. + + Note: + Plugin methods include: + - register(cfg): Registers the plugin with the given configuration. + - load_datasets(cfg): Loads and preprocesses the dataset for training. + - pre_model_load(cfg): Performs actions before the model is loaded. + - post_model_build(cfg, model): Performs actions after the model is loaded, but before LoRA adapters are applied. - pre_lora_load(cfg, model): Performs actions before LoRA weights are loaded. - post_lora_load(cfg, model): Performs actions after LoRA weights are loaded. - post_model_load(cfg, model): Performs actions after the model is loaded, + - pre_lora_load(cfg, model): Performs actions before LoRA weights are loaded. + - post_lora_load(cfg, model): Performs actions after LoRA weights are loaded. + - post_model_load(cfg, model): Performs actions after the model is loaded, inclusive of any adapters. - post_trainer_create(cfg, trainer): Performs actions after the trainer is + - post_trainer_create(cfg, trainer): Performs actions after the trainer is created. - create_optimizer(cfg, trainer): Creates and returns an optimizer for training. - create_lr_scheduler(cfg, trainer, optimizer, num_training_steps): Creates and + - create_optimizer(cfg, trainer): Creates and returns an optimizer for training. + - create_lr_scheduler(cfg, trainer, optimizer, num_training_steps): Creates and returns a learning rate scheduler. - add_callbacks_pre_trainer(cfg, model): Adds callbacks to the trainer before + - add_callbacks_pre_trainer(cfg, model): Adds callbacks to the trainer before training. - add_callbacks_post_trainer(cfg, trainer): Adds callbacks to the trainer after + - add_callbacks_post_trainer(cfg, trainer): Adds callbacks to the trainer after training. """ def __init__(self): """Initializes the BasePlugin.""" - def register(self, cfg): # pylint: disable=unused-argument + def register(self, cfg: DictDefault): # pylint: disable=unused-argument """Registers the plugin with the given configuration. Args: @@ -275,10 +283,11 @@ class PluginManager: Attributes: plugins: A list of loaded plugins. - Methods: - get_instance(): Static method to get the singleton instance of `PluginManager`. - register(plugin_name: str): Registers a new plugin by its name. - pre_model_load(cfg): Calls the pre_model_load method of all registered plugins. + Note: + Key methods include: + - get_instance(): Static method to get the singleton instance of `PluginManager`. + - register(plugin_name: str): Registers a new plugin by its name. + - pre_model_load(cfg): Calls the pre_model_load method of all registered plugins. """ plugins: OrderedDict[str, BasePlugin] = collections.OrderedDict() @@ -534,7 +543,6 @@ def post_train_unload(self, cfg: DictDefault): Args: cfg: The configuration for the plugins. - model: The loaded model. """ for plugin in self.plugins.values(): plugin.post_train_unload(cfg) diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 2df2d9e191..1bfa2ec6e0 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -7,7 +7,7 @@ import math from concurrent.futures import ProcessPoolExecutor from multiprocessing import cpu_count, get_context -from typing import Iterable, Union +from typing import Iterable, Iterator, Union import numba import numpy as np @@ -20,19 +20,19 @@ @numba.njit -def ffd_check(sequence_lengths: np.ndarray, bin_capacity: int, num_bins: int): - """ - First-fit-decreasing bin packing algorithm check +def ffd_check(sequence_lengths: np.ndarray, bin_capacity: int, num_bins: int) -> bool: + """First-fit-decreasing bin packing algorithm check. - Checks if sequences with the given lengths could fit in the specified number of bins + Checks if sequences with the given lengths could fit in the specified number of + bins. Args: - sequence_lengths: Array of sequence lengths - bin_capacity: Maximum capacity of each bin - num_bins: Number of bins available + sequence_lengths: Array of sequence lengths. + bin_capacity: Maximum capacity of each bin. + num_bins: Number of bins available. Returns: - True if all sequences can be packed, False otherwise + `True` if all sequences can be packed, `False` otherwise. """ # Sort sequence lengths in descending order for optimal packing sequence_lengths = np.sort(sequence_lengths)[::-1] @@ -63,20 +63,19 @@ def pack_group( max_bins: int, bin_size: int, safe_mode: bool = True, -): - """ - Pack a group of sequences into bins using First-Fit Decreasing algorithm +) -> list[list[int]]: + """Pack a group of sequences into bins using First-Fit Decreasing algorithm. Args: - sequence_lengths: Array of sequence lengths - group_offset: Offset to apply to indices when returning results - bin_capacity: Maximum capacity of each bin - max_bins: Maximum number of bins to use - bin_size: Maximum number of sequences per bin - safe_mode: If True, use a more conservative packing approach + sequence_lengths: Array of sequence lengths. + group_offset: Offset to apply to indices when returning results. + bin_capacity: Maximum capacity of each bin. + max_bins: Maximum number of bins to use. + bin_size: Maximum number of sequences per bin. + safe_mode: If True, use a more conservative packing approach. Returns: - List of bins, where each bin contains indices of sequences assigned to it + List of bins, where each bin contains indices of sequences assigned to it. """ bins_remaining_space: list = [] # Tracks remaining capacity in each bin bins_assigned_sequences: list = [] # Tracks sequence indices assigned to each bin @@ -111,8 +110,10 @@ def pack_group( return bins_assigned_sequences -# Define a standalone function for multiprocessing -def _process_group(args): +def _process_group( + args: tuple[np.ndarray, int, int, int, int, bool], +) -> list[list[int]]: + """Standalone function for multiprocessing.""" group_lengths, start_idx, bin_capacity, max_bins, bin_size, safe_mode = args return pack_group( group_lengths, start_idx, bin_capacity, max_bins, bin_size, safe_mode @@ -127,22 +128,21 @@ def pack_parallel( num_processes: int | None = None, safe_mode: bool = True, mp_start_method: str | None = "spawn", -): - """ - Pack sequences into bins using parallel processing +) -> list[list[int]]: + """Pack sequences into bins using parallel processing. Args: - sequence_lengths: Array of sequence lengths - bin_capacity: Maximum capacity of each bin as total number of tokens - group_size: Number of sequences to process in each group - bin_size: Maximum number of bins to use - num_processes: Number of parallel processes to use - safe_mode: If True, use a more conservative packing approach + sequence_lengths: Array of sequence lengths. + bin_capacity: Maximum capacity of each bin as total number of tokens. + group_size: Number of sequences to process in each group. + bin_size: Maximum number of bins to use. + num_processes: Number of parallel processes to use. + safe_mode: If True, use a more conservative packing approach. mp_start_method: Multiprocessing start method ('fork', 'spawn', 'forkserver'). 'spawn' is often safer with Numba/PyTorch. Set to None to use system default. Returns: - List of bins, where each bin contains indices of sequences assigned to it + List of bins, where each bin contains indices of sequences assigned to it. """ num_items = len(sequence_lengths) if num_processes is None: @@ -191,20 +191,20 @@ def pack_parallel( @numba.njit def allocate_sequentially( sequence_lengths: np.ndarray, rank: int, bin_capacity: int, num_ranks: int -): - """ - Sequential allocator that preserves example order +) -> tuple[list[list[int]], int, int]: + """Sequential allocator that preserves example order. Args: - sequence_lengths: The lengths of all examples - rank: The current rank (for distributed training) - bin_capacity: The capacity of each bin (maximum sequence length) - num_ranks: Number of ranks (processes/GPUs) + sequence_lengths: The lengths of all examples. + rank: The current rank (for distributed training). + bin_capacity: The capacity of each bin (maximum sequence length). + num_ranks: Number of ranks (processes / GPUs). Returns: - rank_batches: List of batches for the current rank - total_tokens_used: Number of actual example tokens - total_token_slots: Maximum theoretical number of example tokens (number of bins * bin capacity) + rank_batches: List of batches for the current rank. + total_tokens_used: Number of actual example tokens. + total_token_slots: Maximum theoretical number of example tokens (number of bins + * bin capacity). """ result = [] total_used = 0 @@ -240,8 +240,7 @@ def allocate_sequentially( class MultipackBatchSampler(BatchSampler): - """ - Batch sampler class for efficient packing of variable-length sequences + """Batch sampler class for efficient packing of variable-length sequences This sampler packs sequences into fixed-capacity bins (batches) to maximize GPU memory utilization and training throughput by reducing padding. @@ -250,6 +249,9 @@ class MultipackBatchSampler(BatchSampler): sequential packing (preserving original sequence order). """ + _batches: list[list[list[int]]] | None = None + _len_across_ranks: int | None = None + def __init__( self, sampler: Union[Sampler[int], Iterable[int]], @@ -287,11 +289,6 @@ def __init__( # The number of times to calculate batches to determine minimum packed dataset length self.num_count_samples = num_count_samples - # Minimum packed dataset length across all ranks (determined by gather/broadcast) - self.len_across_ranks = None - - # Cache for batches - self._batches = None if self.sequential and not isinstance(sampler, SequentialSampler): LOG.warning( @@ -303,16 +300,15 @@ def set_epoch(self, epoch: int): self.epoch = epoch self._batches = None # Invalidate batch cache - def generate_batches(self, set_stats=False): - """ - Generate packed batches for training + def generate_batches(self, set_stats: bool = False) -> list[list[list[int]]]: + """Generate packed batches for training. Args: - set_stats: Whether to update efficiency statistics + set_stats: Whether to update efficiency statistics. Returns: - List of batches, where each batch contains multiple bins, - and each bin contains multiple sequence indices + List of batches, where each batch contains multiple bins, and each bin + contains multiple sequence indices. """ if self._batches is not None: return self._batches @@ -375,23 +371,21 @@ def generate_batches(self, set_stats=False): self._batches = batches return batches - def __iter__(self): - """ - Return an iterator over batches + def __iter__(self) -> Iterator[list[list[int]]]: + """Return an iterator over batches. - The batches are truncated to match the minimum number of batches across all ranks - to ensure distributed training balance + The batches are truncated to match the minimum number of batches across all + ranks to ensure distributed training balance. """ batches = self.generate_batches(set_stats=True) - if self.len_across_ranks: + if self._len_across_ranks: # Truncate batches to ensure all ranks have the same number of batches - batches = batches[: self.len_across_ranks] + batches = batches[: self._len_across_ranks] return iter(batches) - def efficiency(self): - """ - Calculate the packing efficiency (ratio of tokens used to total token slots) - Higher is better - 1.0 would mean perfect packing with no wasted space + def efficiency(self) -> float: + """Calculate the packing efficiency (ratio of tokens used to total token slots). + Higher is better - 1.0 would mean perfect packing with no wasted space. """ if self.total_token_slots == 0: self.generate_batches(set_stats=True) @@ -400,10 +394,12 @@ def efficiency(self): # Return a Python float instead of potentially a numpy float return float(self.total_tokens_used / self.total_token_slots) - def gather_efficiency(self): - """ - Gather and synchronize packing efficiency estimates across all distributed ranks - Returns a conservative efficiency estimate based on the measurements + def gather_efficiency(self) -> float: + """Gather and synchronize packing efficiency estimates across all distributed + ranks. + + Returns: + A conservative efficiency estimate based on the measurements. """ def calc_sample_packing_eff_est(estimates: list[float]): @@ -424,13 +420,12 @@ def calc_sample_packing_eff_est(estimates: list[float]): ) return sample_packing_eff_est - def gather_len_batches(self, num): - """ - Gather and synchronize batch counts across all distributed ranks - Returns the minimum number of batches available on any rank + def gather_len_batches(self, num: int) -> int: + """Gather and synchronize batch counts across all distributed ranks. Returns + the minimum number of batches available on any rank. """ - def calc_min_len(estimates: list[(int, float)]): + def calc_min_len(estimates: list[int]) -> int: LOG.info(f"gather_len_batches: {repr(estimates)}") return math.floor(min(estimates)) @@ -438,22 +433,21 @@ def calc_min_len(estimates: list[(int, float)]): min_len_batches = reduce_and_broadcast(lambda: num, calc_min_len) return min_len_batches - def __len__(self): - """ - Return the total number of batches that will be yielded by this sampler + def __len__(self) -> int: + """Return the total number of batches that will be yielded by this sampler. - This is calculated as the minimum number of batches available on any rank - to ensure balanced distributed training + This is calculated as the minimum number of batches available on any rank to + ensure balanced distributed training. """ if self._batches is None: self._batches = self.generate_batches(set_stats=True) - if self.len_across_ranks is None: + if self._len_across_ranks is None: # Sample multiple times to get stable estimate len_batches = min( # pylint: disable=consider-using-generator [len(self._batches) for _ in range(self.num_count_samples)] ) # Gather minimum across all ranks - self.len_across_ranks = self.gather_len_batches(len_batches) + self._len_across_ranks = self.gather_len_batches(len_batches) - return self.len_across_ranks + return self._len_across_ranks From a0941a92710504c0662ae49bb3562d3901540e3e Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Tue, 27 May 2025 11:44:06 -0400 Subject: [PATCH 0671/1405] no need to generate diff file (#2728) --- .github/workflows/precommit-autoupdate.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/precommit-autoupdate.yml b/.github/workflows/precommit-autoupdate.yml index 921742211c..10330f9555 100644 --- a/.github/workflows/precommit-autoupdate.yml +++ b/.github/workflows/precommit-autoupdate.yml @@ -25,7 +25,6 @@ jobs: pre-commit autoupdate if [[ -n $(git status --porcelain) ]]; then echo "changes=true" >> $GITHUB_OUTPUT - git diff .pre-commit-config.yaml > pre-commit-update.diff fi - name: Create Pull Request @@ -39,11 +38,3 @@ jobs: commit-message: "chore: update pre-commit hooks" body: | Automated PR to update pre-commit hooks to their latest versions. - -
- Changes: - - ```diff - ${{ steps.update.outputs.diff }} - ``` -
From 4a8af60d34d5761811ee3a4420bc7c8658ccf29a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 11:45:31 -0400 Subject: [PATCH 0672/1405] chore: update pre-commit hooks (#2729) Co-authored-by: djsaunde <1245942+djsaunde@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f627ec13f2..be78d0d3e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,11 +19,11 @@ repos: hooks: - id: isort - repo: https://github.com/PyCQA/flake8 - rev: 7.1.2 + rev: 7.2.0 hooks: - id: flake8 - repo: https://github.com/pylint-dev/pylint - rev: v3.3.6 + rev: v3.3.7 hooks: - id: pylint - repo: https://github.com/pre-commit/mirrors-mypy From 3e6948be975b13746537929d0cfd391eb7000e2f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 28 May 2025 15:48:22 +0700 Subject: [PATCH 0673/1405] Fix(doc): clarify data loading for local datasets and splitting samples (#2726) [skip ci] * fix(doc): remove incorrect json dataset loading method * fix(doc): clarify splitting only happens in completion mode * fix: update local file loading on config doc * fix: typo --- docs/config.qmd | 6 ++++-- docs/dataset-formats/index.qmd | 13 ++++++------- docs/dataset_loading.qmd | 16 ++++------------ 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index ac4c3fa4fb..eab8d28caa 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -98,8 +98,10 @@ plugins: # - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin # A list of one or more datasets to finetune the model with +# See https://docs.axolotl.ai/docs/dataset_loading.html for guide on loading datasets +# See https://docs.axolotl.ai/docs/dataset-formats/ for guide on dataset formats datasets: - # HuggingFace dataset repo | s3://,gs:// path | "json" for local dataset, make sure to fill data_files + # HuggingFace dataset repo | s3:// | gs:// | path to local file or directory - path: vicgalle/alpaca-gpt4 # The type of prompt to use for training. [alpaca, gpteacher, oasst, reflection] type: alpaca # format | format: (chat/instruct) | .load_ @@ -221,7 +223,7 @@ datasets: # The same applies to the `test_datasets` option and the `pretraining_dataset` option. Default is true. shuffle_merged_datasets: true -Deduplicates datasets and test_datasets with identical entries. +# Deduplicates datasets and test_datasets with identical entries. dataset_exact_deduplication: true # A list of one or more datasets to eval the model with. diff --git a/docs/dataset-formats/index.qmd b/docs/dataset-formats/index.qmd index 9898bbc9b6..a0113db07e 100644 --- a/docs/dataset-formats/index.qmd +++ b/docs/dataset-formats/index.qmd @@ -36,10 +36,6 @@ It is typically recommended to save your dataset as `.jsonl` due to its flexibil Axolotl supports loading from a Hugging Face hub repo or from local files. -::: {.callout-important} -For pre-training only, Axolotl would split texts if it exceeds the context length into multiple smaller prompts. -::: - ### Pre-training from Hugging Face hub datasets As an example, to train using a Hugging Face dataset `hf_org/name`, you can pass the following config: @@ -77,18 +73,21 @@ datasets: type: completion ``` -From local files (either example works): +From local files: ```yaml datasets: - path: A.jsonl type: completion - - path: json - data_files: ["A.jsonl", "B.jsonl", "C.jsonl"] + - path: B.jsonl type: completion ``` +::: {.callout-important} +For `completion` only, Axolotl would split texts if it exceeds the context length into multiple smaller prompts. If you are interested in having this for `pretraining_dataset` too, please let us know or help make a PR! +::: + ### Pre-training dataset configuration tips #### Setting max_steps diff --git a/docs/dataset_loading.qmd b/docs/dataset_loading.qmd index 09c8b0098e..b78f86a983 100644 --- a/docs/dataset_loading.qmd +++ b/docs/dataset_loading.qmd @@ -54,7 +54,7 @@ datasets: #### Files -Usually, to load a JSON file, you would do something like this: +To load a JSON file, you would do something like this: ```python from datasets import load_dataset @@ -66,20 +66,12 @@ Which translates to the following config: ```yaml datasets: - - path: json - data_files: /path/to/your/file.jsonl -``` - -However, to make things easier, we have added a few shortcuts for loading local dataset files. - -You can just point the `path` to the file or directory along with the `ds_type` to load the dataset. The below example shows for a JSON file: - -```yaml -datasets: - - path: /path/to/your/file.jsonl + - path: data.json ds_type: json ``` +In the example above, it can be seen that we can just point the `path` to the file or directory along with the `ds_type` to load the dataset. + This works for CSV, JSON, Parquet, and Arrow files. ::: {.callout-tip} From e33f22543473853a0a8f3d59183b496e3faf7640 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 28 May 2025 15:48:40 +0700 Subject: [PATCH 0674/1405] feat(doc): note lora kernel incompat with RLHF (#2706) [skip ci] * feat(doc): note lora kernel incompat with RLHF * fix: add validation following comments * chore: fix typo following suggestion --- docs/lora_optims.qmd | 4 ++++ src/axolotl/utils/schemas/config.py | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/lora_optims.qmd b/docs/lora_optims.qmd index 56d56e9fc8..7cdf539755 100644 --- a/docs/lora_optims.qmd +++ b/docs/lora_optims.qmd @@ -84,6 +84,10 @@ lora_qkv_kernel: true lora_o_kernel: true ``` +::: {.callout-note} +Currently, LoRA kernels are not supported for RLHF training, only SFT. +::: + ## Requirements - One or more NVIDIA or AMD GPUs (in order to use the Triton kernels) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index cc5f54ac45..e68185323f 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1052,7 +1052,7 @@ def check_qlora_unsloth(cls, data): @model_validator(mode="before") @classmethod - def check_lora_8bit(cls, data): + def check_lora_kernel_8bit(cls, data): if ( data.get("lora_mlp_kernel") or data.get("lora_qkv_kernel") @@ -1060,10 +1060,23 @@ def check_lora_8bit(cls, data): ): if data.get("adapter") == "lora" and data.get("load_in_8bit"): raise ValueError( - "lora_mlp_kernel, lora_mlp_kernel, and lora_mlp_kernel are not compatible with 8-bit LoRA" + "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with 8-bit LoRA" ) return data + @model_validator(mode="before") + @classmethod + def check_lora_kernel_rl(cls, data): + if ( + data.get("lora_mlp_kernel") + or data.get("lora_qkv_kernel") + or data.get("lora_o_kernel") + ) and data.get("rl"): + raise ValueError( + "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with RL at the moment." + ) + return data + @model_validator(mode="before") @classmethod def check_lora_axolotl_unsloth(cls, data): From 4a80d309e8e7d15bab374d417539bc5cf8c04bfb Mon Sep 17 00:00:00 2001 From: "NOHHYEOB, BAE" Date: Wed, 28 May 2025 17:49:16 +0900 Subject: [PATCH 0675/1405] Add chat templates for command-a and aya-23-8B models (#2731) [skip ci] * Add chat templates for command-a and aya model * Fix: isolate for-loop update and remove unintended changes --- src/axolotl/utils/chat_templates.py | 4 ++++ src/axolotl/utils/schemas/enums.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index fb21348528..7a7aedc1de 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -40,6 +40,10 @@ "metharme": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% else %}{% set loop_messages = messages %}{% set system_message = 'Enter RP mode. You shall reply to the user while staying in character. Your responses must be detailed, creative, immersive, and drive the scenario forward.' %}{% endif %}{{ '<|system|>' + system_message }}{% for message in loop_messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|user|>' + content.strip() }}{% elif message['role'] == 'assistant' %}{{ '<|model|>' + content.strip() }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|model|>' }}{% else %}{{ eos_token }}{% endif %}", "pixtral": '{%- if messages[0]["role"] == "system" %}\n {%- set system_message = messages[0]["content"] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set loop_messages = messages %}\n{%- endif %}\n\n{{- bos_token }}\n{%- for message in loop_messages %}\n {%- if (message[\'role\'] == \'user\') != (loop.index0 % 2 == 0) %}\n {{- raise_exception(\'After the optional system message, conversation roles must alternate user/assistant/user/assistant/...\') }}\n {%- endif %}\n {%- if message["role"] == "user" %}\n {%- if loop.last and system_message is defined %}\n {{- "[INST]" + system_message + "\n\n" }}\n {%- else %}\n {{- "[INST]" }}\n {%- endif %}\n {%- if message["content"] is not string %}\n {%- for chunk in message["content"] %}\n {%- if chunk["type"] == "text" %}\n {{- chunk["text"] }}\n {%- elif chunk["type"] == "image" %}\n {{- "[IMG]" }}\n {%- else %}\n {{- raise_exception("Unrecognized content type!") }}\n {%- endif %}\n {%- endfor %}\n {%- else %}\n {{- message["content"] }}\n {%- endif %}\n {{- "[/INST]" }}\n {%- elif message["role"] == "assistant" %}\n {%- if message["content"] is not string %}\n {%- for chunk in message["content"] %}\n {%- if chunk["type"] == "text" %}\n {{- chunk["text"] }}\n {%- elif chunk["type"] == "image" %}\n {{- "[IMG]" }}\n {%- else %}\n {{- raise_exception("Unrecognized content type!") }}\n{%- endif %}\n{%- endfor %}\n{{- eos_token }}\n{%- else %}\n{{- message["content"] + eos_token }}\n{%- endif %}\n {%- else %}\n {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }}\n {%- endif %}\n{%- endfor %}', "qwen2_vl": "{% set image_count = namespace(value=0) %}{% set video_count = namespace(value=0) %}{% for message in messages %}{% if loop.first and message['role'] != 'system' %}<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n{% endif %}<|im_start|>{{ message['role'] }}\n{% if message['content'] is string %}{{ message['content'] }}<|im_end|>\n{% else %}{% for content in message['content'] %}{% if content['type'] == 'image' or 'image' in content or 'image_url' in content %}{% set image_count.value = image_count.value + 1 %}{% if add_vision_id %}Picture {{ image_count.value }}: {% endif %}<|vision_start|><|image_pad|><|vision_end|>{% elif content['type'] == 'video' or 'video' in content %}{% set video_count.value = video_count.value + 1 %}{% if add_vision_id %}Video {{ video_count.value }}: {% endif %}<|vision_start|><|video_pad|><|vision_end|>{% elif 'text' in content %}{{ content['text'] }}{% endif %}{% endfor %}<|im_end|>\n{% endif %}{% endfor %}{% if add_generation_prompt %}<|im_start|>assistant\n{% endif %}", + "command_a": "{{ bos_token }}{% if documents %}\n{% set tools = [] %}\n{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {\"tool_call_id\": \"0\", \"tool_name\": \"direct-injected-document\", \"parameters\": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n \"tool_call_id\": \"0\",\n \"results\": {\n{% for doc in documents %}\n \"{{ loop.index0 }}\": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n \"is_error\": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n \"tool_call_id\": \"{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}\",\n \"results\": {\n \"0\": {{ tool_msg.content|tojson }}\n },\n \"is_error\": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0]['role']|lower == 'system' %}{%- set developer_preamble = messages[0]['content'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing \"tool_name\" and \"parameters\" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its \"tool_call_id\".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that \"Reflection\" and \"Response\" above can be grounded.\nGrounding means you associate pieces of texts (called \"spans\") with those specific tool results that support them (called \"sources\"). And you use a pair of tags \"\" and \"\" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as \"{tool_call_id}:[{list of result indices}]\", before they are joined together by \",\". E.g., \"span\" means that \"span\" is supported by result 1 and 2 from \"tool_call_id=0\" as well as result 0 from \"tool_call_id=1\".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like \"name\", \"description\", \"parameters\" (per JSON Schema), and optionally, \"responses\" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {\"name\": \"direct-injected-document\", \"description\": \"This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!\", \"parameters\": {\"type\": \"object\", \"properties\": {}, \"required\": []}, \"responses\": {\"200\": {\"description\": \"Successfully returned a list of chunked text snippets from the directly uploaded documents.\", \"content\": {\"application/json\": {\"schema\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"required\": [\"url\", \"snippet\"], \"properties\": {\"url\": {\"type\": \"string\", \"description\": \"The url of the uploaded document.\"}, \"snippet\": {\"type\": \"string\", \"description\": \"The text snippet for the returned document chunk.\"}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {\"name\": \"{{ tool['function']['name'] }}\", \"description\": \"{{tool['function']['description']}}\", \"parameters\": {{ tool['function']['parameters']|tojson }}, \"responses\": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == 'system' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == 'user' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {\"tool_call_id\": \"{{ tool_idx.value }}\", \"tool_name\": \"{{ tc['function']['name'] }}\", \"parameters\": {{ tc['function']['arguments']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == 'tool' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == 'tool' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>\n{%- else -%}\n{%- if messages and messages[0]['role']|lower == 'system' %}{%- set developer_preamble = messages[0]['content'] %}{% endif %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\n{% if safety_mode|upper == 'STRICT' -%}\nYou are in strict safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will reject requests to generate content related to violence, hate, misinformation or sex to any amount. You will avoid using profanity. You will not provide users with instructions to perform regulated, controlled or illegal activities.\n{%- else -%}\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n{%- endif %}\n\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == 'system' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == 'user' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{%- if add_generation_prompt -%}<|START_RESPONSE|>{%- endif %}\n{% endif %}", + "command_a_tool_use": "{{ bos_token }}{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {\"tool_call_id\": \"0\", \"tool_name\": \"direct-injected-document\", \"parameters\": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n \"tool_call_id\": \"0\",\n \"results\": {\n{% for doc in documents %}\n \"{{ loop.index0 }}\": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n \"is_error\": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n \"tool_call_id\": \"{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}\",\n \"results\": {\n \"0\": {{ tool_msg.content|tojson }}\n },\n \"is_error\": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0]['role']|lower == 'system' %}{%- set developer_preamble = messages[0]['content'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing \"tool_name\" and \"parameters\" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its \"tool_call_id\".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that \"Reflection\" and \"Response\" above can be grounded.\nGrounding means you associate pieces of texts (called \"spans\") with those specific tool results that support them (called \"sources\"). And you use a pair of tags \"\" and \"\" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as \"{tool_call_id}:[{list of result indices}]\", before they are joined together by \",\". E.g., \"span\" means that \"span\" is supported by result 1 and 2 from \"tool_call_id=0\" as well as result 0 from \"tool_call_id=1\".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like \"name\", \"description\", \"parameters\" (per JSON Schema), and optionally, \"responses\" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {\"name\": \"direct-injected-document\", \"description\": \"This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!\", \"parameters\": {\"type\": \"object\", \"properties\": {}, \"required\": []}, \"responses\": {\"200\": {\"description\": \"Successfully returned a list of chunked text snippets from the directly uploaded documents.\", \"content\": {\"application/json\": {\"schema\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"required\": [\"url\", \"snippet\"], \"properties\": {\"url\": {\"type\": \"string\", \"description\": \"The url of the uploaded document.\"}, \"snippet\": {\"type\": \"string\", \"description\": \"The text snippet for the returned document chunk.\"}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {\"name\": \"{{ tool['function']['name'] }}\", \"description\": \"{{tool['function']['description']}}\", \"parameters\": {{ tool['function']['parameters']|tojson }}, \"responses\": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == 'system' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == 'user' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {\"tool_call_id\": \"{{ tool_idx.value }}\", \"tool_name\": \"{{ tc['function']['name'] }}\", \"parameters\": {{ tc['function']['arguments']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == 'tool' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == 'tool' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>", + "command_a_rag": "{{ bos_token }}{% set tools = [] %}\n{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {\"tool_call_id\": \"0\", \"tool_name\": \"direct-injected-document\", \"parameters\": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n \"tool_call_id\": \"0\",\n \"results\": {\n{% for doc in documents %}\n \"{{ loop.index0 }}\": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n \"is_error\": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n \"tool_call_id\": \"{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}\",\n \"results\": {\n \"0\": {{ tool_msg.content|tojson }}\n },\n \"is_error\": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0]['role']|lower == 'system' %}{%- set developer_preamble = messages[0]['content'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing \"tool_name\" and \"parameters\" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its \"tool_call_id\".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that \"Reflection\" and \"Response\" above can be grounded.\nGrounding means you associate pieces of texts (called \"spans\") with those specific tool results that support them (called \"sources\"). And you use a pair of tags \"\" and \"\" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as \"{tool_call_id}:[{list of result indices}]\", before they are joined together by \",\". E.g., \"span\" means that \"span\" is supported by result 1 and 2 from \"tool_call_id=0\" as well as result 0 from \"tool_call_id=1\".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like \"name\", \"description\", \"parameters\" (per JSON Schema), and optionally, \"responses\" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {\"name\": \"direct-injected-document\", \"description\": \"This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!\", \"parameters\": {\"type\": \"object\", \"properties\": {}, \"required\": []}, \"responses\": {\"200\": {\"description\": \"Successfully returned a list of chunked text snippets from the directly uploaded documents.\", \"content\": {\"application/json\": {\"schema\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"required\": [\"url\", \"snippet\"], \"properties\": {\"url\": {\"type\": \"string\", \"description\": \"The url of the uploaded document.\"}, \"snippet\": {\"type\": \"string\", \"description\": \"The text snippet for the returned document chunk.\"}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {\"name\": \"{{ tool['function']['name'] }}\", \"description\": \"{{tool['function']['description']}}\", \"parameters\": {{ tool['function']['parameters']|tojson }}, \"responses\": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == 'system' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == 'user' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {\"tool_call_id\": \"{{ tool_idx.value }}\", \"tool_name\": \"{{ tc['function']['name'] }}\", \"parameters\": {{ tc['function']['arguments']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == 'tool' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == 'tool' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>", + "aya": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Aya, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", } diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index ff8471dfd5..5268724128 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -43,7 +43,10 @@ class ChatTemplate(str, Enum): llava = "llava" # pylint: disable=invalid-name qwen2_vl = "qwen2_vl" # pylint: disable=invalid-name gemma3 = "gemma3" # pylint: disable=invalid-name - + command_a = "command_a" # pylint: disable=invalid-name + command_a_tool_use = "command_a_tool_use" # pylint: disable=invalid-name + command_a_rag = "command_a_rag" # pylint: disable=invalid-name + aya = "aya" # pylint: disable=invalid-name class CustomSupportedOptimizers(str, Enum): """Custom supported optimizers""" From a703560a1015383150689eea50958277998fa74b Mon Sep 17 00:00:00 2001 From: artem <42897193+sumo43@users.noreply.github.com> Date: Wed, 28 May 2025 01:49:43 -0700 Subject: [PATCH 0676/1405] add two checks to handle legacy format interleaved multimodal ds (#2721) [skip ci] * add two checks to handle legacy format interleaved ds * fix: add warning about multiple image using legacy format --------- Co-authored-by: NanoCode012 --- src/axolotl/processing_strategies.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 4dee4f8a28..1cb6ed0648 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -1,5 +1,6 @@ """Module containing ProcessingStrategy classes and its derivative for different MultiModal Model types""" +import logging from copy import deepcopy from typing import Optional @@ -9,6 +10,8 @@ from transformers import ProcessorMixin from transformers.image_utils import load_image +LOG = logging.getLogger(__name__) + class ProcessingStrategy: """Base Processing Strategy class""" @@ -112,7 +115,9 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: ) processed_example = None - if "messages" in example: # OpenAI format + if ( + "messages" in example and example["messages"] is not None + ): # OpenAI format processed_example = example else: # Legacy format processed_example = convert_legacy_format(example) @@ -132,10 +137,17 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: break # if the image key exists, add the image to the first message - if image_key is not None: + if image_key is not None and processed_example[image_key] is not None: # TODO: check if it's normal to be single image only for common datasets # From observation, it's usually a list of single image but some datasets may have several columns for images # Temporary solution: take the first image and suggest people convert their datasets to use multi-content Messages + if len(processed_example[image_key]) > 0: + LOG.warning( + f"Found {len(processed_example[image_key])} images in a sample. Using the first one." + "If you are using a dataset with multiple images per sample, please convert it to use multi-content Messages." + "See https://docs.axolotl.ai/docs/multimodal.html#dataset-format" + ) + image_value = processed_example[image_key][0] # Handle image loading (Image, url, path, base64) From add2025253a182b5e7db4181357d5f428499f5c3 Mon Sep 17 00:00:00 2001 From: mashdragon <122402293+mashdragon@users.noreply.github.com> Date: Wed, 28 May 2025 08:50:47 +0000 Subject: [PATCH 0677/1405] Fix Mistral chat template (mistral_v7_tekken) (#2710) [skip ci] Per https://huggingface.co/mistralai/Mistral-Small-3.1-24B-Instruct-2503/commit/4b8dd8aae705887db5295fcbff4aedbb92d682eb#d2h-482763 --- src/axolotl/utils/chat_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 7a7aedc1de..72ebffbcdb 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -20,7 +20,7 @@ "mistral_v1": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ ' [INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # Mistral 7B V1, Mistral 7B V2, Mixtral 8x7B V1... "mistral_v2v3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # V3: Mistral 7B V3, Small, Large... "mistral_v3_tekken": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST]' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # V3-Tekken: Nemo, Pixtral... - "mistral_v7_tekken": "{%- set today = strftime_now(\"%Y-%m-%d\") %}\n{%- set default_system_message = \"You are Mistral Small 3, a Large Language Model (LLM) created by Mistral AI, a French startup headquartered in Paris.\\nYour knowledge base was last updated on 2023-10-01. The current date is \" + today + \".\\n\\nWhen you're not sure about some information, you say that you don't have the information and don't make up anything.\\nIf the user's question is not clear, ambiguous, or does not provide enough context for you to accurately answer the question, you do not try to answer it right away and you rather ask the user to clarify their request (e.g. \\\"What are some good restaurants around me?\\\" => \\\"Where are you?\\\" or \\\"When is the next flight to Tokyo\\\" => \\\"Where do you travel from?\\\")\" %}\n\n{{- bos_token }}\n\n{%- if messages[0]['role'] == 'system' %}\n {%- set system_message = messages[0]['content'] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set system_message = default_system_message %}\n {%- set loop_messages = messages %}\n{%- endif %}\n{{- '[SYSTEM_PROMPT]' + system_message + '[/SYSTEM_PROMPT]' }}\n\n{%- for message in loop_messages %}\n {%- if message['role'] == 'user' %}\n\t {%- if message['content'] is string %}\n {{- '[INST]' + message['content'] + '[/INST]' }}\n\t {%- else %}\n\t\t {{- '[INST]' }}\n\t\t {%- for block in message['content'] %}\n\t\t\t {%- if block['type'] == 'text' %}\n\t\t\t\t {{- block['text'] }}\n\t\t\t {%- elif block['type'] == 'image' or block['type'] == 'image_url' %}\n\t\t\t\t {{- '[IMG]' }}\n\t\t\t\t{%- else %}\n\t\t\t\t {{- raise_exception('Only text and image blocks are supported in message content!') }}\n\t\t\t\t{%- endif %}\n\t\t\t{%- endfor %}\n\t\t {{- '[/INST]' }}\n\t\t{%- endif %}\n {%- elif message['role'] == 'system' %}\n {{- '[SYSTEM_PROMPT]' + message['content'] + '[/SYSTEM_PROMPT]' }}\n {%- elif message['role'] == 'assistant' %}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- elif message['content'] is iterable %} \n\t\t {%- for block in message['content'] %}\n\t\t\t {%- if block['type'] == 'text' %}\n\t\t\t\t {{- block['text'] }}\n\t\t\t {%- else %}\n\t\t\t\t {{- raise_exception('Only text blocks are supported in assistant message content!') }} {%- endif %}\n\t\t\t \n\t\t\t{%- endfor %} {{- eos_token }} {%- else %}\n {{- raise_exception('Unsupported assistant message content format!') }} \n{%- endif %} \n{%- else %}\n {{- raise_exception('Only user, system and assistant roles are supported!') }}\n {%- endif %}\n{%- endfor %}", + "mistral_v7_tekken": "{%- set today = strftime_now(\"%Y-%m-%d\") %}\n{%- set default_system_message = \"You are Mistral Small 3, a Large Language Model (LLM) created by Mistral AI, a French startup headquartered in Paris.\\nYour knowledge base was last updated on 2023-10-01. The current date is \" + today + \".\\n\\nWhen you're not sure about some information, you say that you don't have the information and don't make up anything.\\nIf the user's question is not clear, ambiguous, or does not provide enough context for you to accurately answer the question, you do not try to answer it right away and you rather ask the user to clarify their request (e.g. \\\"What are some good restaurants around me?\\\" => \\\"Where are you?\\\" or \\\"When is the next flight to Tokyo\\\" => \\\"Where do you travel from?\\\")\" %}\n\n{{- bos_token }}\n\n{%- if messages[0]['role'] == 'system' %}\n {%- if messages[0]['content'] is string %}\n {%- set system_message = messages[0]['content'] %}\n {%- else %}\n {%- set system_message = messages[0]['content'][0]['text'] %}\n {%- endif %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set system_message = default_system_message %}\n {%- set loop_messages = messages %}\n{%- endif %}\n{{- '[SYSTEM_PROMPT]' + system_message + '[/SYSTEM_PROMPT]' }}\n\n{%- for message in loop_messages %}\n {%- if message['role'] == 'user' %}\n {%- if message['content'] is string %}\n {{- '[INST]' + message['content'] + '[/INST]' }}\n {%- else %}\n {{- '[INST]' }}\n {%- for block in message['content'] %}\n {%- if block['type'] == 'text' %}\n {{- block['text'] }}\n {%- elif block['type'] in ['image', 'image_url'] %}\n {{- '[IMG]' }}\n {%- else %}\n {{- raise_exception('Only text and image blocks are supported in message content!') }}\n {%- endif %}\n {%- endfor %}\n {{- '[/INST]' }}\n {%- endif %}\n {%- elif message['role'] == 'system' %}\n {%- if message['content'] is string %}\n {{- '[SYSTEM_PROMPT]' + message['content'] + '[/SYSTEM_PROMPT]' }}\n {%- else %}\n {{- '[SYSTEM_PROMPT]' + message['content'][0]['text'] + '[/SYSTEM_PROMPT]' }}\n {%- endif %}\n {%- elif message['role'] == 'assistant' %}\n {%- if message['content'] is string %}\n {{- message['content'] + eos_token }}\n {%- else %}\n {{- message['content'][0]['text'] + eos_token }}\n {%- endif %}\n {%- else %}\n {{- raise_exception('Only user, system and assistant roles are supported!') }}\n {%- endif %}\n{%- endfor %}", "chatml": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}", "gemma": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\n' + message['content'] | trim + '\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\n'}}{% endif %}", "gemma3": "{{ bos_token }}\n{%- if messages[0]['role'] == 'system' -%}\n {%- if messages[0]['content'] is string -%}\n {%- set first_user_prefix = messages[0]['content'] + '\n\n' -%}\n {%- else -%}\n {%- set first_user_prefix = messages[0]['content'][0]['text'] + '\n\n' -%}\n {%- endif -%}\n {%- set loop_messages = messages[1:] -%}\n{%- else -%}\n {%- set first_user_prefix = \"\" -%}\n {%- set loop_messages = messages -%}\n{%- endif -%}\n{%- for message in loop_messages -%}\n {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) -%}\n {{ raise_exception(\"Conversation roles must alternate user/assistant/user/assistant/...\") }}\n {%- endif -%}\n {%- if (message['role'] == 'assistant') -%}\n {%- set role = \"model\" -%}\n {%- else -%}\n {%- set role = message['role'] -%}\n {%- endif -%}\n {{ '' + role + '\n' + (first_user_prefix if loop.first else \"\") }}\n {%- if message['content'] is string -%}\n {{ message['content'] | trim }}\n {%- elif message['content'] is iterable -%}\n {%- for item in message['content'] -%}\n {%- if item['type'] == 'image' -%}\n {{ '' }}\n {%- elif item['type'] == 'text' -%}\n {{ item['text'] | trim }}\n {%- endif -%}\n {%- endfor -%}\n {%- else -%}\n {{ raise_exception(\"Invalid content type\") }}\n {%- endif -%}\n {{ '\n' }}\n{%- endfor -%}\n{%- if add_generation_prompt -%}\n {{'model\n'}}\n{%- endif -%}\n", From 6b6370f4e3d5d657a42676185ba76034c09294c3 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 28 May 2025 15:51:04 +0700 Subject: [PATCH 0678/1405] feat(doc): add info on how to use dapo / dr grpo and misc doc fixes (#2673) [skip ci] * feat(doc): add info on how to use dapo / dr grpo * chore: add missing config to docs * fix: missing comment * fix: add missing scheduler from schema * chore: refactor lr scheduler docs * fix: remove log_sweep --- .runpod/src/config/config.yaml | 6 +----- docs/config.qmd | 36 ++++++++++++++++++++++++++++++++-- docs/rlhf.qmd | 17 ++++++++++++++-- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/.runpod/src/config/config.yaml b/.runpod/src/config/config.yaml index 4dff37caef..42c5978d57 100644 --- a/.runpod/src/config/config.yaml +++ b/.runpod/src/config/config.yaml @@ -242,16 +242,12 @@ # early_stopping_patience: 3 # # Specify a scheduler and kwargs to use with the optimizer -# lr_scheduler: # 'one_cycle' | 'log_sweep' | empty for cosine +# lr_scheduler: # 'one_cycle' | empty for cosine # lr_scheduler_kwargs: # # For one_cycle optim # lr_div_factor: # Learning rate div factor -# # For log_sweep optim -# log_sweep_min_lr: -# log_sweep_max_lr: - # # Specify optimizer # # Valid values are driven by the Transformers OptimizerNames class, see: # # https://github.com/huggingface/transformers/blob/95b374952dc27d8511541d6f5a4e22c9ec11fb24/src/transformers/training_args.py#L134 diff --git a/docs/config.qmd b/docs/config.qmd index eab8d28caa..369d3db430 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -272,10 +272,25 @@ trl: num_generations: # Optional[int]. Number of generations to sample. log_completions: # Optional[bool]. Whether to log completions. + num_completions_to_print: # Optional[int]. Number of completions to print when log_completions is True. sync_ref_model: # Optional[bool]. Whether to sync the reference model. ref_model_mixup_alpha: # Optional[float]. Mixup alpha for the reference model. ref_model_sync_steps: # Optional[int]. Sync steps for the reference model. + scale_rewards: # Optional[bool]. Whether to scale rewards by their standard deviation. + + temperature: # Optional[float]. Sampling temperature for the GRPO policy. + top_p: # Optional[float]. Top-p sampling probability for the generation policy. + top_k: # Optional[int]. Top-k sampling for the generation policy. + min_p: # Optional[float]. Minimum probability for the generation policy. + repetition_penalty: # Optional[float]. Penalty for tokens that appear in prompt and generated text. + + num_iterations: # Optional[int]. Number of iterations per batch (μ) for GRPO. + epsilon: # Optional[float]. Epsilon value for clipping in the GRPO algorithm. + epsilon_high: # Optional[float]. Upper-bound epsilon value for clipping in the GRPO algorithm. + use_liger_loss: # Optional[bool]. Whether to use Liger loss for GRPO. + loss_type: # Optional[str]. Loss formulation to use. Supported values: grpo, bnpo, dr_grpo. + mask_truncated_completions: # Optional[bool]. Whether to exclude truncated completions from loss calculation. # reward modelling: `True` or `False` @@ -553,7 +568,24 @@ gradient_checkpointing: false early_stopping_patience: 3 # Specify a scheduler and kwargs to use with the optimizer -lr_scheduler: # 'one_cycle' | 'rex' | 'log_sweep' | 'linear' | 'cosine_with_restarts' | 'polynomial' | 'constant' | 'constant_with_warmup' | 'inverse_sqrt' | 'reduce_lr_on_plateau' | 'cosine_with_min_lr' | 'warmup_stable_decay' | empty for cosine +# Valid values are driven by the Transformers SchedulerType class, see: +# https://github.com/huggingface/transformers/blob/5f4ecf2d9f867a1255131d2461d75793c0cf1db2/src/transformers/trainer_utils.py#L420 +# Valid values include +# - 'linear' +# - 'cosine' (default) +# - 'cosine_with_restarts' +# - 'polynomial' +# - 'constant' +# - 'constant_with_warmup' +# - 'inverse_sqrt' +# - 'reduce_lr_on_plateau' +# - 'cosine_with_min_lr' +# - 'warmup_stable_decay' + +# Additional schedulers include: +# - 'one_cycle' +# - 'rex' +lr_scheduler: lr_scheduler_kwargs: cosine_min_lr_ratio: # decay lr to some percentage of the peak lr, e.g. cosine_min_lr_ratio=0.1 for 10% of peak lr cosine_constant_lr_ratio: # freeze lr at some percentage of the step, e.g. cosine_constant_lr_ratio=0.8 means start cosine_min_lr at 80% of training step (https://arxiv.org/pdf/2308.04014.pdf) @@ -571,7 +603,7 @@ lr_div_factor: # Learning rate div factor # # Valid values for 'optimizer' include: # - adamw_torch -# - adamw_torch_fused +# - adamw_torch_fused (default) # - adamw_torch_xla # - adamw_torch_npu_fused # - adamw_apex_fused diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 3a8f87d71b..e0d3b55e4f 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -16,7 +16,7 @@ feedback. Various methods include, but not limited to: - [Identity Preference Optimization (IPO)](#ipo) - [Kahneman-Tversky Optimization (KTO)](#kto) - [Odds Ratio Preference Optimization (ORPO)](#orpo) -- Proximal Policy Optimization (PPO) (not yet supported in axolotl) +- Proximal Policy Optimization (PPO) (not yet supported in axolotl, if you're interested in contributing, please reach out!) ## RLHF using Axolotl @@ -582,7 +582,20 @@ datasets: To see other examples of custom reward functions, please see [TRL GRPO Docs](https://github.com/huggingface/trl/blob/main/docs/source/grpo_trainer.md#using-a-custom-reward-function). -To see description of the configs, please see [TRLConfig](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/utils/config/models/input/v0_4_1/trl.py). +To see all configs, please see [TRLConfig](https://github.com/axolotl-ai-cloud/axolotl/blob/v0.9.2/src/axolotl/utils/schemas/trl.py). + +#### GRPO with DAPO/Dr. GRPO loss + +The DAPO paper and subsequently Dr. GRPO paper proposed an alternative loss function for GRPO to remediate the penalty in longer responses. + +```yaml +trl: + loss_type: dr_grpo + # Normalizes loss based on max completion length (default: 256) + max_completion_length: +``` + +For more information, see [GRPO docs](https://huggingface.co/docs/trl/v0.17.0/en/grpo_trainer#loss-types). ### SimPO From 20fda75917b41b8997b2ff1fe8e1a8ef110ba433 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 28 May 2025 15:51:21 +0700 Subject: [PATCH 0679/1405] feat(doc): add google analytics to docs (#2708) --- _quarto.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_quarto.yml b/_quarto.yml index df6992d92d..696cedd51a 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -202,6 +202,8 @@ website: description: "We make fine-tuning accessible, scalable, and fun" favicon: favicon.jpg + google-analytics: "G-9KYCVJBNMQ" + navbar: logo: image/axolotl_logo_digital_white.svg title: false From 5fca214108af6ae51f2dfc2cc035e8accde94e9b Mon Sep 17 00:00:00 2001 From: salman Date: Wed, 28 May 2025 12:35:47 +0100 Subject: [PATCH 0680/1405] QAT (#2590) QAT and quantization w/torchao --- _quarto.yml | 6 +- docs/cli.qmd | 10 + docs/config.qmd | 14 + docs/qat.qmd | 32 ++ docs/quantize.qmd | 53 +++ examples/llama-3/3b-qat-fsdp2.yaml | 79 +++++ examples/qwen3/8b-qat-fsdp2.yml | 78 +++++ requirements.txt | 2 +- src/axolotl/cli/args.py | 12 + src/axolotl/cli/main.py | 11 + src/axolotl/cli/quantize.py | 90 +++++ src/axolotl/core/trainer_builder.py | 4 + src/axolotl/loaders/model.py | 14 + src/axolotl/loaders/patch_manager.py | 4 +- src/axolotl/monkeypatch/accelerate/fsdp2.py | 159 ++++++++- src/axolotl/train.py | 18 +- src/axolotl/utils/callbacks/qat.py | 50 +++ src/axolotl/utils/distributed.py | 7 +- src/axolotl/utils/quantization.py | 189 +++++++++++ src/axolotl/utils/schemas/config.py | 44 ++- src/axolotl/utils/schemas/enums.py | 16 + src/axolotl/utils/schemas/quantization.py | 64 ++++ tests/e2e/multigpu/solo/test_grpo.py | 6 +- tests/e2e/test_qat.py | 71 ++++ tests/e2e/test_quantization.py | 350 ++++++++++++++++++++ tests/e2e/utils.py | 2 - 26 files changed, 1372 insertions(+), 13 deletions(-) create mode 100644 docs/qat.qmd create mode 100644 docs/quantize.qmd create mode 100644 examples/llama-3/3b-qat-fsdp2.yaml create mode 100644 examples/qwen3/8b-qat-fsdp2.yml create mode 100644 src/axolotl/cli/quantize.py create mode 100644 src/axolotl/utils/callbacks/qat.py create mode 100644 src/axolotl/utils/quantization.py create mode 100644 src/axolotl/utils/schemas/quantization.py create mode 100644 tests/e2e/test_qat.py create mode 100644 tests/e2e/test_quantization.py diff --git a/_quarto.yml b/_quarto.yml index 696cedd51a..e05a1c35c3 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -43,6 +43,7 @@ quartodoc: - cli.vllm_serve - cli.cloud.base - cli.cloud.modal_ + - cli.quantize - title: Trainers desc: Training implementations contents: @@ -147,6 +148,7 @@ quartodoc: - utils.optimizers.adopt - utils.data.pretraining - utils.data.sft + - utils.quantization - title: Schemas desc: Pydantic data models for Axolotl config contents: @@ -196,7 +198,7 @@ quartodoc: - utils.callbacks.lisa - utils.callbacks.mlflow_ - utils.callbacks.comet_ - + - utils.callbacks.qat website: title: "Axolotl" description: "We make fine-tuning accessible, scalable, and fun" @@ -256,6 +258,8 @@ website: - docs/lr_groups.qmd - docs/lora_optims.qmd - docs/dataset_loading.qmd + - docs/qat.qmd + - docs/quantize.qmd - section: "Core Concepts" contents: diff --git a/docs/cli.qmd b/docs/cli.qmd index 1003a210c0..f6f9b34810 100644 --- a/docs/cli.qmd +++ b/docs/cli.qmd @@ -209,6 +209,16 @@ axolotl delinearize-llama4 --model path/to/model_dir --output path/to/output_dir This would be necessary to use with other frameworks. If you have an adapter, merge it with the non-quantized linearized model before delinearizing. +### quantize + +Quantizes a model using the quantization configuration specified in your YAML file. + +```bash +axolotl quantize config.yml +``` + +See [Quantization](./quantize.qmd) for more details. + ## Legacy CLI Usage diff --git a/docs/config.qmd b/docs/config.qmd index 369d3db430..5a36ca8aa4 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -65,6 +65,20 @@ bnb_config_kwargs: bnb_4bit_quant_type: nf4 bnb_4bit_use_double_quant: true +# quantization aware training +qat: + activation_dtype: # Optional[str] = "int8". Fake quantization layout to use for activation quantization. Valid options are "int4" and "int8" + weight_dtype: # Optional[str] = "int8". Fake quantization layout to use for weight quantization. Valid options are "int4" and "int8" + group_size: # Optional[int] = 32. The number of elements in each group for per-group fake quantization + fake_quant_after_n_steps: # Optional[int] = None. The number of steps to apply fake quantization after + +# post-training quantization +quantization: + weight_dtype: # Optional[str] = "int8". Fake quantization layout to use for weight quantization. Valid options are uintX for X in [1, 2, 3, 4, 5, 6, 7], or int4, or int8 + activation_dtype: # Optional[str] = "int8". Fake quantization layout to use for activation quantization. Valid options are "int4" and "int8" + group_size: # Optional[int] = 32. The number of elements in each group for per-group fake quantization + quantize_embedding: # Optional[bool] = False. Whether to quantize the embedding layer. + # Whether you are training a 4-bit GPTQ quantized model gptq: true diff --git a/docs/qat.qmd b/docs/qat.qmd new file mode 100644 index 0000000000..0531388de6 --- /dev/null +++ b/docs/qat.qmd @@ -0,0 +1,32 @@ +--- +title: "Quantization Aware Training (QAT)" +back-to-top-navigation: true +toc: true +toc-expand: 2 +toc-depth: 4 +--- + +## Overview + +[Quantization Aware Training](https://pytorch.org/blog/introduction-to-quantization-on-pytorch/#quantization-aware-training) (QAT) is a technique for improving the accuracy of models which are quantized +by applying "fake" quantizations to the model's weights (and optionally, activations) during training. This fake +quantization allows for the model to adjust for noise introduced by the quantization, so when the model is eventually +quantized, the accuracy loss is minimized. We use the quantization techniques implemented in [torchao](https://github.com/pytorch/ao) to provide +support for QAT and post-training quantization (PTQ) in axolotl. + +We recommend reviewing the excellent QAT tutorial in the [torchtune library](https://pytorch.org/torchtune/main/tutorials/qat_finetune.html#quantizing-the-qat-model), +and the QAT documentation in the [torchao library](https://github.com/pytorch/ao/tree/main/torchao/quantization/qat), for more details. + +## Configuring QAT in Axolotl + +To enable QAT in axolotl, add the following to your configuration file: + +```yaml +qat: + activation_dtype: # Optional[str] = "int8". Fake quantization layout to use for activation quantization. Valid options are "int4" and "int8" + weight_dtype: # Optional[str] = "int8". Fake quantization layout to use for weight quantization. Valid options are "int4" and "int8" + group_size: # Optional[int] = 32. The number of elements in each group for per-group fake quantization + fake_quant_after_n_steps: # Optional[int] = None. The number of steps to apply fake quantization after +``` + +Once you have finished training, you must quantize your model by using the same quantization configuration which you used to train the model with. You can use the [`quantize` command](./quantize.md) to do this. diff --git a/docs/quantize.qmd b/docs/quantize.qmd new file mode 100644 index 0000000000..294efda8b4 --- /dev/null +++ b/docs/quantize.qmd @@ -0,0 +1,53 @@ +--- +title: "Quantization with torchao" +back-to-top-navigation: true +toc: true +toc-expand: 2 +toc-depth: 4 +--- + +Quantization is a technique to lower the memory footprint of your model, potentially at the cost of accuracy or model performance. We support quantizing your model using the [torchao](https://github.com/pytorch/ao) library. Quantization is supported for both post-training quantization (PTQ) and quantization-aware training (QAT). + + +::: {.callout-note} + +We do not currently support quantization techniques such as GGUF/GPTQ,EXL2 at the moment. + +::: + +## Configuring Quantization in Axolotl + +Quantization is configured using the `quantization` key in your configuration file. + +```yaml +base_model: # The path to the model to quantize. +quantization: + weight_dtype: # Optional[str] = "int8". Fake quantization layout to use for weight quantization. Valid options are uintX for X in [1, 2, 3, 4, 5, 6, 7], or int4, or int8 + activation_dtype: # Optional[str] = "int8". Fake quantization layout to use for activation quantization. Valid options are "int4" and "int8" + group_size: # Optional[int] = 32. The number of elements in each group for per-group fake quantization + quantize_embedding: # Optional[bool] = False. Whether to quantize the embedding layer. + +output_dir: # The path to the output directory. +``` + +Once quantization is complete, your quantized model will be saved in the `{output_dir}/quantized` directory. + +You may also use the `quantize` command to quantize a model which has been trained with [QAT](./qat.md) - you can do this by using the existing QAT configuration file which +you used to train the model: + +```yaml +# qat.yml +qat: + activation_dtype: int8 + weight_dtype: int8 + group_size: 256 + quantize_embedding: true + +output_dir: # The path to the output directory used during training where the final checkpoint has been saved. +``` + +```bash +axolotl quantize qat.yml +``` + +This ensures that an identical quantization configuration is used to quantize the model as was used to train it. diff --git a/examples/llama-3/3b-qat-fsdp2.yaml b/examples/llama-3/3b-qat-fsdp2.yaml new file mode 100644 index 0000000000..5d979c96c2 --- /dev/null +++ b/examples/llama-3/3b-qat-fsdp2.yaml @@ -0,0 +1,79 @@ +base_model: meta-llama/Llama-3.2-3B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true + +datasets: + - path: yahma/alpaca-cleaned + type: alpaca + +output_dir: ./outputs/qat_out/ + +sample_packing: true +pad_to_sequence_len: true +sequence_len: 512 + +flex_attention: true +flex_attn_compile_kwargs: + dynamic: false + mode: max-autotune-no-cudagraphs + +qat: + activation_dtype: int8 + weight_dtype: int4 + group_size: 32 + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 16 +num_epochs: 1 +optimizer: adamw_torch_fused + +cosine_constant_lr_ratio: 0 +cosine_min_lr_ratio: 1.0 +learning_rate: 2e-5 +save_only_model: true +bf16: true + +resume_from_checkpoint: +logging_steps: 1 + +evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_steps: 10 +weight_decay: 0.0 +fsdp: + - full_shard + - auto_wrap + +fsdp_config: + fsdp_version: 2 + fsdp_offload_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD + fsdp_reshard_after_forward: true + fsdp_activation_checkpointing: true + +special_tokens: + pad_token: <|end_of_text|> diff --git a/examples/qwen3/8b-qat-fsdp2.yml b/examples/qwen3/8b-qat-fsdp2.yml new file mode 100644 index 0000000000..6832b6af75 --- /dev/null +++ b/examples/qwen3/8b-qat-fsdp2.yml @@ -0,0 +1,78 @@ +base_model: Qwen/Qwen3-8B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true + +datasets: + - path: tatsu-lab/alpaca + type: alpaca + +output_dir: ./outputs/qat_out/ + +sequence_len: 2048 +sample_packing: true +flex_attention: true +pad_to_sequence_len: true + +flex_attn_compile_kwargs: + dynamic: false + mode: max-autotune-no-cudagraphs + +qat: + activation_dtype: int8 + weight_dtype: int4 + group_size: 256 + fake_quant_after_n_steps: 1000 + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 2 +max_steps: 2000 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 2e-5 + +bf16: true +tf32: true + +resume_from_checkpoint: +logging_steps: 1 + +evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_steps: 10 +weight_decay: 0.0 +fsdp: + - full_shard + - auto_wrap + +fsdp_config: + fsdp_version: 2 + fsdp_offload_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: Qwen3DecoderLayer + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD + fsdp_reshard_after_forward: true + fsdp_activation_checkpointing: true + +special_tokens: diff --git a/requirements.txt b/requirements.txt index 4ae82dd496..4e632b0f3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -63,7 +63,7 @@ langdetect==1.0.9 immutabledict==4.2.0 antlr4-python3-runtime==4.13.2 -torchao==0.9.0 +torchao==0.10.0 schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.6 diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py index 088e337e46..4be3704acd 100644 --- a/src/axolotl/cli/args.py +++ b/src/axolotl/cli/args.py @@ -90,6 +90,18 @@ class VllmServeCliArgs: ) +@dataclass +class QuantizeCliArgs: + """Dataclass with CLI arguments for `axolotl quantize` command.""" + + base_model: Optional[str] = field(default=None) + weight_dtype: Optional[str] = field(default=None) + activation_dtype: Optional[str] = field(default=None) + quantize_embedding: Optional[bool] = field(default=None) + group_size: Optional[int] = field(default=None) + output_dir: Optional[str] = field(default=None) + + @dataclass class EvaluateCliArgs: """Dataclass with CLI arguments for `axolotl evaluate` command.""" diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index 601add7090..e61dad5d63 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -17,6 +17,7 @@ from axolotl.cli.args import ( EvaluateCliArgs, PreprocessCliArgs, + QuantizeCliArgs, TrainerCliArgs, VllmServeCliArgs, ) @@ -333,6 +334,16 @@ def vllm_serve(config: str, **cli_args: VllmServeCliArgs): do_vllm_serve(config, cli_args) +@cli.command() +@click.argument("config", type=click.Path(exists=True, path_type=str)) +@add_options_from_dataclass(QuantizeCliArgs) +@filter_none_kwargs +def quantize(config: str, **cli_args: QuantizeCliArgs): + from axolotl.cli.quantize import do_quantize + + do_quantize(config, cli_args) + + @cli.command() @click.argument("model", type=click.Path(exists=True, path_type=str)) @click.argument("output", type=click.Path(exists=False, path_type=str)) diff --git a/src/axolotl/cli/quantize.py b/src/axolotl/cli/quantize.py new file mode 100644 index 0000000000..2036fddeac --- /dev/null +++ b/src/axolotl/cli/quantize.py @@ -0,0 +1,90 @@ +""" +CLI to post-training quantize a model using torchao +""" + +import logging +from pathlib import Path +from typing import Union + +from transformers import AutoModelForCausalLM + +from axolotl.cli.art import print_axolotl_text_art +from axolotl.cli.config import load_cfg +from axolotl.loaders import load_tokenizer +from axolotl.utils.quantization import TorchIntDType, quantize_model_for_ptq + +LOG = logging.getLogger(__name__) + + +def do_quantize( + config: Union[Path, str], + cli_args: dict, +): + """ + Quantizes a model's model's weights + + Args: + config (Union[Path, str]): The path to the config file + cli_args (dict): Additional command-line arguments + """ + print_axolotl_text_art() + + cfg = load_cfg(config) + + if cfg.qat and cfg.quantization: + raise ValueError( + "QAT and quantization cannot be used together. Please specify only one of qat or quantization in your config file." + ) + + if cfg.qat: + quantize_cfg = cfg.qat + elif cfg.quantization: + quantize_cfg = cfg.quantization + else: + raise ValueError( + "No quantization configuration found. Please specify either qat or quantization in your config file." + ) + + model_path = cli_args.get("model_path") or cfg.output_dir + if weight_dtype := cli_args.get("weight_dtype"): + weight_dtype = TorchIntDType[weight_dtype] + else: + weight_dtype = quantize_cfg.weight_dtype + if activation_dtype := cli_args.get("activation_dtype"): + activation_dtype = TorchIntDType[activation_dtype] + else: + activation_dtype = quantize_cfg.activation_dtype + group_size = cli_args.get("group_size") or quantize_cfg.group_size + quantize_embedding = ( + cli_args.get("quantize_embedding") or quantize_cfg.quantize_embedding + ) + output_dir = cli_args.get("output_dir") or cfg.output_dir + + LOG.info(f"Loading model from {model_path}...") + tokenizer = load_tokenizer(cfg) + model = AutoModelForCausalLM.from_pretrained(model_path, device_map="auto") + + LOG.info( + f"Quantizing model with configuration: \n" + f"\tweight_dtype: {weight_dtype}\n" + f"\tactivation_dtype: {activation_dtype}\n" + f"\tgroup_size: {group_size}\n" + f"\tquantize_embedding: {quantize_embedding}" + ) + + quantize_model_for_ptq( + model, weight_dtype, group_size, activation_dtype, quantize_embedding + ) + + LOG.info(f"Saving quantized model to: {str(Path(output_dir) / 'quantized')}...") + model.save_pretrained( + str(Path(output_dir) / "quantized"), + safe_serialization=False, + progressbar=True, + ) + tokenizer.save_pretrained( + str(Path(output_dir) / "quantized"), + safe_serialization=False, + progressbar=True, + ) + LOG.info(f"Quantized model saved to: {str(Path(output_dir) / 'quantized')}...") diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 9709f0fd4a..08759d9f9f 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -79,6 +79,7 @@ ) from axolotl.utils.callbacks.lisa import lisa_callback_factory from axolotl.utils.callbacks.profiler import PytorchProfilerCallback +from axolotl.utils.callbacks.qat import QATCallback from axolotl.utils.chat_templates import get_chat_template_from_config from axolotl.utils.collators import ( BatchSamplerDataCollatorForSeq2Seq, @@ -254,6 +255,9 @@ def get_callbacks(self): if self.cfg.loss_watchdog_threshold is not None: callbacks.append(LossWatchDogCallback(self.cfg)) + if self.cfg.qat: + callbacks.append(QATCallback(self.cfg.qat)) + return callbacks def get_post_trainer_create_callbacks(self, trainer): diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index d7ac84a6d8..8d8f927a78 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -191,6 +191,7 @@ def _apply_post_model_load_setup(self): self._adjust_model_config() self._log_memory_usage() self._configure_embedding_dtypes() + self._configure_qat() def _resize_token_embeddings(self): """Resize token embeddings if needed.""" @@ -305,6 +306,19 @@ def _configure_embedding_dtypes(self): before_kbit_train_or_finetune=False, ) + def _configure_qat(self): + """Configure QAT.""" + if self.cfg.qat: + from axolotl.utils.quantization import prepare_model_for_qat + + prepare_model_for_qat( + self.model, + self.cfg.qat.weight_dtype, + self.cfg.qat.group_size, + self.cfg.qat.activation_dtype, + self.cfg.qat.quantize_embedding, + ) + def _load_adapters(self) -> PeftConfig | None: """Load LoRA or other adapters.""" # Load LoRA or adapter diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index f251f958da..36813bafd8 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -80,9 +80,9 @@ def _apply_flash_attention_patches(self): def _apply_fsdp_patches(self): """Apply patches for FSDP configurations.""" if self.cfg.fsdp_config and str(self.cfg.fsdp_config.fsdp_version) == "2": - from axolotl.monkeypatch.accelerate.fsdp2 import patch_accelerate_fsdp_utils + from axolotl.monkeypatch.accelerate.fsdp2 import patch_accelerate_fsdp2 - patch_accelerate_fsdp_utils() + patch_accelerate_fsdp2() def _apply_adapter_patches(self): """Apply patches for adapter configurations.""" diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index d8ec00c696..ffde17aebd 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -1,5 +1,5 @@ """ -monkeypatch for accelerate fsdp2 fix when modifying ordereddict during interation +monkeypatch for accelerate fsdp2 fix when modifying ordereddict during interation, and saving full state dicts """ import logging @@ -52,7 +52,146 @@ def fsdp2_load_full_state_dict(accelerator, model: torch.nn.Module, full_sd: dic model.load_state_dict(sharded_sd, assign=True) -def patch_accelerate_fsdp_utils(): +def set_state_dict_type(self, state_dict_type=None): + """ + Set the state dict config based on the `StateDictType`. + """ + import os + + from torch.distributed.fsdp.fully_sharded_data_parallel import ( + FullOptimStateDictConfig, + FullStateDictConfig, + ShardedOptimStateDictConfig, + ShardedStateDictConfig, + StateDictType, + ) + + # Override the state_dict_type if provided, typical use case: + # user trains with sharded, but final save is with full + if state_dict_type is not None: + self.state_dict_type = state_dict_type + + if self.state_dict_type is None: + self.state_dict_type = os.environ.get( + "FSDP_STATE_DICT_TYPE", + "FULL_STATE_DICT" if self.fsdp_version == 1 else "SHARDED_STATE_DICT", + ) + if isinstance(self.state_dict_type, str): + if self.state_dict_type.isdigit(): + self.state_dict_type = StateDictType(int(self.state_dict_type)) + else: + self.state_dict_type = StateDictType[self.state_dict_type.upper()] + + if self.state_dict_type == StateDictType.FULL_STATE_DICT: + if self.state_dict_config is None: + self.state_dict_config = FullStateDictConfig( + offload_to_cpu=True, rank0_only=True + ) + if self.optim_state_dict_config is None: + self.optim_state_dict_config = FullOptimStateDictConfig( + offload_to_cpu=True, rank0_only=True + ) + elif self.state_dict_type == StateDictType.SHARDED_STATE_DICT: + if self.state_dict_config is None: + self.state_dict_config = ShardedStateDictConfig(offload_to_cpu=True) + if self.optim_state_dict_config is None: + self.optim_state_dict_config = ShardedOptimStateDictConfig( + offload_to_cpu=True + ) + + +def get_state_dict(self, model, unwrap=True): + """ + Returns the state dictionary of a model sent through [`Accelerator.prepare`] potentially without full + precision. + + Args: + model (`torch.nn.Module`): + A PyTorch model sent through [`Accelerator.prepare`] + unwrap (`bool`, *optional*, defaults to `True`): + Whether to return the original underlying state_dict of `model` or to return the wrapped state_dict + + Returns: + `dict`: The state dictionary of the model potentially without full precision. + + Example: + + ```python + >>> import torch + >>> from accelerate import Accelerator + + >>> accelerator = Accelerator() + >>> net = torch.nn.Linear(2, 2) + >>> net = accelerator.prepare(net) + >>> state_dict = accelerator.get_state_dict(net) + ``` + """ + from accelerate import DistributedType + from accelerate.utils import compare_versions + + if self.distributed_type == DistributedType.DEEPSPEED: + zero3_sharding = self.deepspeed_config["zero_optimization"]["stage"] == 3 + tp_sharding = ( + self.deepspeed_config.get("tensor_parallel", {}).get("autotp_size", 0) > 1 + ) + if zero3_sharding or tp_sharding: + if model.zero_gather_16bit_weights_on_model_save(): + if tp_sharding and not compare_versions("deepspeed", ">=", "0.16.4"): + raise ImportError( + "Deepspeed TP requires deepspeed >= 0.16.4, Please update DeepSpeed via `pip install deepspeed -U`." + ) + state_dict = ( + model._consolidated_16bit_state_dict() # pylint: disable=protected-access + if tp_sharding + else model._zero3_consolidated_16bit_state_dict() # pylint: disable=protected-access + ) + else: + raise ValueError( + "Cannot get 16bit model weights because `stage3_gather_16bit_weights_on_model_save` in DeepSpeed config is False. " + "To save the model weights in 16bit, set `stage3_gather_16bit_weights_on_model_save` to True in DeepSpeed config file or " + "set `zero3_save_16bit_model` to True when using `accelerate config`. " + "To save the full checkpoint, run `model.save_checkpoint(save_dir)` and use `zero_to_fp32.py` to recover weights." + ) + else: + from deepspeed.checkpoint.utils import clone_tensors_for_torch_save + + state_dict = clone_tensors_for_torch_save( + self.unwrap_model(model).state_dict() + ) + elif self.is_fsdp2: + # https://github.com/pytorch/torchtune/blob/main/torchtune/training/_distributed.py#L465 + state_dict = {} + sharded_state_dict = model.state_dict() + for param_name, param in sharded_state_dict.items(): + if param.is_cpu: + param = param.to(torch.device("cuda")) + + param = param.full_tensor() + if torch.distributed.get_rank() == 0: + state_dict[param_name] = param.cpu() + torch.distributed.barrier() + elif self.distributed_type == DistributedType.FSDP: + from torch.distributed.fsdp import FullStateDictConfig + from torch.distributed.fsdp import FullyShardedDataParallel as FSDP + from torch.distributed.fsdp import StateDictType + + full_state_dict_config = FullStateDictConfig( + offload_to_cpu=True, rank0_only=True + ) + with FSDP.state_dict_type( + model, StateDictType.FULL_STATE_DICT, full_state_dict_config + ): + state_dict = model.state_dict() + else: + if unwrap: + model = self.unwrap_model(model) + state_dict = model.state_dict() + + return state_dict + + +def patch_accelerate_fsdp2(): + import accelerate from accelerate.utils import fsdp_utils fsdp_utils.fsdp2_load_full_state_dict = fsdp2_load_full_state_dict @@ -61,3 +200,19 @@ def patch_accelerate_fsdp_utils(): "fsdp2_load_full_state_dict", fsdp2_load_full_state_dict, ) + + accelerate.Accelerator.get_state_dict = get_state_dict + setattr( + sys.modules["accelerate"], + "Accelerator.get_state_dict", + get_state_dict, + ) + + accelerate.utils.dataclasses.FullyShardedDataParallelPlugin.set_state_dict_type = ( + set_state_dict_type + ) + setattr( + sys.modules["accelerate.utils.dataclasses"], + "FullyShardedDataParallelPlugin.set_state_dict_type", + set_state_dict_type, + ) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 52ec8f22ba..8a4c0040d9 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -238,13 +238,27 @@ def save_trained_model( model: The trained model to save. safe_serialization: Whether to use safe serialization. """ - LOG.info(f"Training completed! Saving pre-trained model to {cfg.output_dir}.") + LOG.info(f"Training completed! Saving trained model to {cfg.output_dir}.") # Post training module hooks for name, module in model.named_modules(): if hasattr(module, "_post_training"): module._post_training(model, name) # pylint: disable=protected-access + # handle QAT + if cfg.qat: + from axolotl.utils.quantization import convert_qat_model_for_ptq + + LOG.info("Processing QAT model for saving...") + convert_qat_model_for_ptq( + model, + quantize_embedding=cfg.qat.quantize_embedding, + ) + LOG.info( + "QAT modules have been converted for PTQ. Please ensure you quantize " + "your model weights with `axolotl quantize`." + ) + # Handle FSDP state dict type state_dict_type = "FULL_STATE_DICT" if trainer.is_fsdp_enabled and str(cfg.fsdp_config.fsdp_version) != "2": @@ -321,6 +335,8 @@ def save_trained_model( save_compressed=cfg.llmcompressor.save_compressed, ) + LOG.info(f"Model successfully saved to {cfg.output_dir}") + def create_model_card(cfg: DictDefault, trainer: Trainer): """ diff --git a/src/axolotl/utils/callbacks/qat.py b/src/axolotl/utils/callbacks/qat.py new file mode 100644 index 0000000000..da4f2612be --- /dev/null +++ b/src/axolotl/utils/callbacks/qat.py @@ -0,0 +1,50 @@ +"""QAT Callback for HF Causal Trainer""" + +import logging +from functools import partial + +from torch import nn +from torchao.quantization.qat.embedding import FakeQuantizedEmbedding +from torchao.quantization.qat.linear import FakeQuantizedLinear +from transformers import TrainerCallback + +from axolotl.utils.schemas.quantization import QATConfig + +LOG = logging.getLogger(__name__) + + +def toggle_fake_quant(mod: nn.Module, enable: bool): + """ + Toggle fake quantization for any fake quantized linear or embedding layers in the model. + + Args: + mod: The module to toggle fake quantization for. + enable: Whether to enable or disable fake quantization. + """ + if isinstance(mod, (FakeQuantizedLinear, FakeQuantizedEmbedding)): + if ( + isinstance(mod, FakeQuantizedLinear) + and mod.activation_fake_quantizer is not None + ): + mod.activation_fake_quantizer.enabled = enable + mod.weight_fake_quantizer.enabled = enable + + +class QATCallback(TrainerCallback): + """ + Callback to toggle fake quantization for the model. + """ + + def __init__(self, cfg: QATConfig): + self.cfg = cfg + + def on_step_begin( + self, args, state, control, model, **kwargs + ): # pylint: disable=unused-argument + if self.cfg.fake_quant_after_n_steps is not None: + if state.global_step == 0: + LOG.info(f"Disabling fake quantization at step {state.global_step}") + model.apply(partial(toggle_fake_quant, enable=False)) + elif state.global_step == self.cfg.fake_quant_after_n_steps: + LOG.info(f"Enabling fake quantization at step {state.global_step}") + model.apply(partial(toggle_fake_quant, enable=True)) diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index 8c52102c89..0673c6e951 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -103,7 +103,12 @@ def cleanup_distributed(): termination or when training successfully completes. """ # Ensure that all operations are completed before destroying the process group - torch.cuda.synchronize() + if torch.cuda.is_available(): + torch.cuda.synchronize() + + if torch.xpu.is_available(): + torch.xpu.synchronize() + # Destroy the process group if torch.distributed.is_initialized(): torch.distributed.destroy_process_group() diff --git a/src/axolotl/utils/quantization.py b/src/axolotl/utils/quantization.py new file mode 100644 index 0000000000..612b1d44e6 --- /dev/null +++ b/src/axolotl/utils/quantization.py @@ -0,0 +1,189 @@ +""" +Utilities for quantization including QAT and PTQ using torchao. +""" + +import logging + +import torch +from torch import nn +from torchao.core.config import AOBaseConfig +from torchao.quantization import quantize_ +from torchao.quantization.qat import ( + FakeQuantizeConfig, + FromIntXQuantizationAwareTrainingConfig, + IntXQuantizationAwareTrainingConfig, +) +from torchao.quantization.quant_api import ( + Int4DynamicActivationInt4WeightConfig, + Int4WeightOnlyConfig, + Int8DynamicActivationInt4WeightConfig, + Int8DynamicActivationInt8WeightConfig, + Int8WeightOnlyConfig, + UIntXWeightOnlyConfig, + _is_linear, +) + +from axolotl.utils.schemas.enums import TorchIntDType + +LOG = logging.getLogger(__name__) + + +def get_ptq_config( + weight_dtype: TorchIntDType, + activation_dtype: TorchIntDType | None = None, + group_size: int | None = None, +) -> AOBaseConfig: + """ + This function is used to build a post-training quantization config. + + Args: + weight_dtype: The dtype to use for weight quantization. + activation_dtype: The dtype to use for activation quantization. + group_size: The group size to use for weight quantization. + + Returns: + The post-training quantization config. + + Raises: + ValueError: If the activation dtype is not specified and the weight dtype is not int8 or int4, + or if the group size is not specified for int8 or int4 weight only quantization. + """ + if activation_dtype is None: + if not weight_dtype.value.is_signed: # type: ignore[attr-defined,union-attr] + return UIntXWeightOnlyConfig( + dtype=weight_dtype.value, + group_size=group_size, + set_inductor_config=False, + ) + if weight_dtype == TorchIntDType.int8: + if group_size is None: + raise ValueError( + "group_size must be specified for int8 weight only quantization" + ) + return Int8WeightOnlyConfig( + group_size=group_size, + ) + if weight_dtype == TorchIntDType.int4: + if group_size is None: + raise ValueError( + "group_size must be specified for int4 weight only quantization" + ) + return Int4WeightOnlyConfig( + group_size=group_size, + ) + if activation_dtype == TorchIntDType.int4 and weight_dtype == TorchIntDType.int4: + return Int4DynamicActivationInt4WeightConfig() + if activation_dtype == TorchIntDType.int8 and weight_dtype == TorchIntDType.int8: + return Int8DynamicActivationInt8WeightConfig() + if activation_dtype == TorchIntDType.int8 and weight_dtype == TorchIntDType.int4: + return Int8DynamicActivationInt4WeightConfig() + raise ValueError( + f"Invalid activation/weight dtype combination: {activation_dtype}/{weight_dtype}" + ) + + +def prepare_model_for_qat( + model, + weight_dtype: TorchIntDType, + group_size: int, + activation_dtype: TorchIntDType | None = None, + quantize_embedding: bool = False, +): + """ + This function is used to prepare a model for QAT by swapping the model's linear + layers with fake quantized linear layers, and optionally the embedding weights with + fake quantized embedding weights. + + Args: + model: The model to quantize. + weight_dtype: The dtype to use for weight quantization. + group_size: The group size to use for weight quantization. + activation_dtype: The dtype to use for activation quantization. + quantize_embedding: Whether to quantize the model's embedding weights. + + Raises: + ValueError: If the activation/weight dtype combination is invalid. + """ + if activation_dtype: + activation_config = FakeQuantizeConfig( + dtype=activation_dtype.value, granularity="per_token", is_symmetric=False + ) + weight_config = FakeQuantizeConfig(dtype=weight_dtype.value, group_size=group_size) + linear_quantize_config = IntXQuantizationAwareTrainingConfig( + activation_config=None if activation_dtype is None else activation_config, + weight_config=weight_config, + ) + quantize_(model, linear_quantize_config) + if quantize_embedding: + # activation fake quantization is not supported for embedding layers + embedding_quantize_config = IntXQuantizationAwareTrainingConfig( + activation_config=None, + weight_config=weight_config, + ) + quantize_( + model, + embedding_quantize_config, + filter_fn=lambda m, _: isinstance(m, torch.nn.Embedding), + ) + + +def quantize_model_for_ptq( + model, + weight_dtype: TorchIntDType, + group_size: int | None = None, + activation_dtype: TorchIntDType | None = None, + quantize_embedding: bool | None = None, +): + """ + This function is used to quantize a model for post-training quantization. + It swaps the model's linear layers with fake quantized linear layers. + If `quantize_embedding` is True, it will also swap the model's embedding weights with fake quantized embedding weights. + + Args: + model: The model to quantize. + weight_dtype: The dtype to use for weight quantization. + group_size: The group size to use for weight quantization. + activation_dtype: The dtype to use for activation quantization. + quantize_embedding: Whether to quantize the model's embedding weights. + + """ + linear_ptq_config = get_ptq_config( + weight_dtype=weight_dtype, + activation_dtype=activation_dtype, + group_size=group_size, + ) + quantize_(model, linear_ptq_config) + if quantize_embedding: + embedding_quantize_config = get_ptq_config( + weight_dtype=weight_dtype, + activation_dtype=None, + group_size=group_size, + ) + quantize_( + model, + embedding_quantize_config, + filter_fn=lambda m, _: isinstance(m, torch.nn.Embedding), + ) + + +def convert_qat_model_for_ptq( + model, + *, + quantize_embedding: bool | None = None, +): + """ + This function is used to convert a swap fake-quantized modules in a model + which has been trained with QAT back to the original modules, ready for PTQ. + + Args: + model: The model to convert. + quantize_embedding: Whether to quantize the model's embedding weights. + """ + if quantize_embedding: + + def filter_fn(m, _): + return isinstance(m, nn.Embedding) or _is_linear(m) + + else: + filter_fn = _is_linear + quantize_(model, FromIntXQuantizationAwareTrainingConfig(), filter_fn=filter_fn) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index e68185323f..8a4d6d63fa 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -44,6 +44,7 @@ ) from axolotl.utils.schemas.multimodal import MultiModalConfig from axolotl.utils.schemas.peft import LoraConfig, ReLoRAConfig +from axolotl.utils.schemas.quantization import PTQConfig, QATConfig from axolotl.utils.schemas.training import HyperparametersConfig from axolotl.utils.schemas.trl import TRLConfig from axolotl.utils.schemas.vllm import VllmConfig @@ -91,6 +92,8 @@ class AxolotlInputConfig( vllm: VllmConfig | None = Field( default_factory=lambda: VllmConfig(), # pylint: disable=unnecessary-lambda ) + qat: QATConfig | None = None + quantization: PTQConfig | None = None reward_model: bool | None = None process_reward_model: bool | None = None num_labels: int | None = None @@ -126,7 +129,7 @@ class AxolotlInputConfig( default=None, json_schema_extra={"description": "streaming dataset to use for pretraining"}, ) - dataset_processes: int | None = Field(default=min(32, os.cpu_count())) # type: ignore[type-var] + dataset_processes: int | None = Field(default=min(32, os.cpu_count() or 1)) dataset_exact_deduplication: bool | None = None dataset_keep_in_memory: bool | None = None dataloader_pin_memory: bool | None = None @@ -1481,3 +1484,42 @@ def check_min_torch_version(self): ) return self + + @model_validator(mode="before") + @classmethod + def check_qat_config(cls, data): + qat_cfg = data.get("qat", {}) + if not qat_cfg: + return data + + if data.get("peft"): + raise ValueError("QAT and PEFT cannot be used together.") + + if data.get("load_in_8bit"): + raise ValueError("QAT and load_in_8bit cannot be used together.") + + if data.get("load_in_4bit"): + raise ValueError("QAT and load_in_4bit cannot be used together.") + + env_capabilities = data.get("env_capabilities", {}) + torch_version = env_capabilities.get("torch_version") + + if torch_version is None: + import torch + + torch_version = str(torch.__version__).split("+", maxsplit=1)[0] + + if ( + data.get("fsdp") + and data.get("fsdp_config") + and str(data["fsdp_config"].get("fsdp_version")) == "2" + ): + if version.parse(torch_version) < version.parse("2.7.0"): + raise ValueError( + "FSDP2 and QAT are not supported on torch version < 2.7.0" + ) + + if version.parse(torch_version) < version.parse("2.6.0"): + raise ValueError("QAT is not supported on torch version < 2.6.0") + + return data diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 5268724128..91fdce1614 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -2,6 +2,22 @@ from enum import Enum +import torch + + +class TorchIntDType(Enum): + """Torch integer data types - `getattr` guards against torch < 2.6 which does not support int4""" + + uint1 = getattr(torch, "uint1", None) # pylint: disable=invalid-name + uint2 = getattr(torch, "uint2", None) # pylint: disable=invalid-name + uint3 = getattr(torch, "uint3", None) # pylint: disable=invalid-name + uint4 = getattr(torch, "uint4", None) # pylint: disable=invalid-name + uint5 = getattr(torch, "uint5", None) # pylint: disable=invalid-name + uint6 = getattr(torch, "uint6", None) # pylint: disable=invalid-name + uint7 = getattr(torch, "uint7", None) # pylint: disable=invalid-name + int4 = getattr(torch, "int4", None) # pylint: disable=invalid-name + int8 = getattr(torch, "int8", None) # pylint: disable=invalid-name + class RLType(str, Enum): """RL trainer type configuration subset""" diff --git a/src/axolotl/utils/schemas/quantization.py b/src/axolotl/utils/schemas/quantization.py new file mode 100644 index 0000000000..fe2cdb1fe3 --- /dev/null +++ b/src/axolotl/utils/schemas/quantization.py @@ -0,0 +1,64 @@ +""" +QAT Config Schema +""" + +from typing import Any + +from pydantic import BaseModel, Field, field_validator + +from axolotl.utils.schemas.enums import TorchIntDType + + +class QATConfig(BaseModel): + """ + QAT Config Schema + """ + + activation_dtype: TorchIntDType | None = Field( + default=None, description="Activation dtype" + ) + weight_dtype: TorchIntDType = Field( + default=TorchIntDType.int8, description="Weight dtype" + ) + quantize_embedding: bool | None = Field( + default=False, description="Quantize embedding" + ) + group_size: int | None = Field(default=32, description="Group size") + fake_quant_after_n_steps: int | None = Field( + default=None, description="Fake quant after n steps" + ) + + @field_validator("activation_dtype", "weight_dtype", mode="before") + @classmethod + def validate_dtype(cls, v: Any) -> TorchIntDType | None: + if v == "int4": + return TorchIntDType.int4 + if v == "int8": + return TorchIntDType.int8 + raise ValueError(f"Invalid dtype: '{v}'. Must be one of: ['int4', 'int8']") + + +class PTQConfig(BaseModel): + """ + PTQ Config Schema + """ + + weight_dtype: TorchIntDType = Field( + default=TorchIntDType.int8, description="Weight dtype" + ) + activation_dtype: TorchIntDType | None = Field( + default=None, description="Activation dtype" + ) + quantize_embedding: bool | None = Field( + default=None, description="Quantize embedding" + ) + group_size: int | None = Field(default=32, description="Group size") + + @field_validator("activation_dtype", "weight_dtype", mode="before") + @classmethod + def validate_dtype(cls, v: Any) -> TorchIntDType | None: + if v == "int4": + return TorchIntDType.int4 + if v == "int8": + return TorchIntDType.int8 + raise ValueError(f"Invalid dtype: '{v}'. Must be one of: ['int4', 'int8']") diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index 575b7a620a..6c7a9b2e46 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -4,7 +4,6 @@ import os import random -import shutil import subprocess # nosec B404 import sys import tempfile @@ -118,7 +117,10 @@ def start_vllm( recursive_kill(process) with open("/tmp/vllm.log", "r", encoding="utf-8") as log_file: print(log_file.read()) - shutil.rmtree("/tmp/vllm.log") + try: + os.remove("/tmp/vllm.log") + except FileNotFoundError: + pass raise RuntimeError(f"VLLM server process did not start within {wait} seconds.") # return the process diff --git a/tests/e2e/test_qat.py b/tests/e2e/test_qat.py new file mode 100644 index 0000000000..f9e7993be6 --- /dev/null +++ b/tests/e2e/test_qat.py @@ -0,0 +1,71 @@ +""" +E2E tests for QAT +""" + +import unittest +from pathlib import Path + +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +from .utils import check_model_output_exists, with_temp_dir + + +class TestQATLlama(unittest.TestCase): + """ + Test case for QAT Llama models + """ + + @with_temp_dir + def test_qat_lora(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", + "sequence_len": 1024, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "field_messages": "conversations", + "message_property_mappings": { + "role": "from", + "content": "value", + }, + "drop_system_message": True, + "split": "train[:1%]", + }, + ], + "chat_template": "chatml", + "qat": { + "quantize_embedding": True, + "activation_dtype": "int8", + "weight_dtype": "int8", + "group_size": 8, + }, + "num_epochs": 1, + "micro_batch_size": 1, + "gradient_accumulation_steps": 2, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "max_steps": 5, + "save_safetensors": True, + "bf16": True, + } + ) + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(Path(temp_dir) / "checkpoint-5", cfg) diff --git a/tests/e2e/test_quantization.py b/tests/e2e/test_quantization.py new file mode 100644 index 0000000000..500b7e556b --- /dev/null +++ b/tests/e2e/test_quantization.py @@ -0,0 +1,350 @@ +""" +Tests for axolotl.utils.quantization +""" + +import pytest +import torch +from torch import nn +from torchao.dtypes.affine_quantized_tensor import AffineQuantizedTensor +from torchao.quantization.granularity import PerAxis, PerGroup +from torchao.quantization.linear_activation_quantized_tensor import ( + LinearActivationQuantizedTensor, +) +from torchao.quantization.qat.embedding import FakeQuantizedEmbedding +from torchao.quantization.qat.linear import FakeQuantizedLinear +from torchao.quantization.quant_api import ( + Int4DynamicActivationInt4WeightConfig, + Int4WeightOnlyConfig, + Int8DynamicActivationInt8WeightConfig, + Int8WeightOnlyConfig, + UIntXWeightOnlyConfig, +) +from transformers import AutoModelForCausalLM +from transformers.trainer_callback import TrainerState + +from axolotl.utils.callbacks.qat import QATCallback +from axolotl.utils.quantization import ( + convert_qat_model_for_ptq, + get_ptq_config, + prepare_model_for_qat, + quantize_model_for_ptq, +) +from axolotl.utils.schemas.enums import TorchIntDType +from axolotl.utils.schemas.quantization import QATConfig + +from tests.e2e.utils import require_torch_2_6_0 + + +@pytest.fixture() +def model(): + dummy_model = AutoModelForCausalLM.from_pretrained( + "HuggingFaceTB/SmolLM2-135M", + device_map="cuda", + torch_dtype=torch.bfloat16, + ) + with torch.device(dummy_model.device): + dummy_model.model.embed_tokens = torch.nn.Embedding( + dummy_model.model.embed_tokens.weight.shape[0], + dummy_model.model.embed_tokens.weight.shape[1], + dtype=dummy_model.model.embed_tokens.weight.dtype, + ) + return dummy_model + + +ptq_config_test_cases = [ + # weight_dtype, activation_dtype, group_size, expected_type, expected_params + ( + TorchIntDType.uint4, + None, + None, + UIntXWeightOnlyConfig, + {"dtype": torch.uint4, "group_size": None}, + ), + (TorchIntDType.int8, None, 32, Int8WeightOnlyConfig, {"group_size": 32}), + (TorchIntDType.int4, None, 4, Int4WeightOnlyConfig, {"group_size": 4}), + ( + TorchIntDType.int4, + TorchIntDType.int4, + None, + Int4DynamicActivationInt4WeightConfig, + {}, + ), + ( + TorchIntDType.int8, + TorchIntDType.int8, + None, + Int8DynamicActivationInt8WeightConfig, + {}, + ), +] + +ptq_test_cases = [ + # weight_dtype, activation_dtype, group_size, quantize_embedding, expected_exception + (TorchIntDType.int8, None, 8, False, None), + (TorchIntDType.int4, None, 4, True, None), + (TorchIntDType.uint4, None, 8, False, None), + (TorchIntDType.int4, TorchIntDType.int4, 8, False, None), + (TorchIntDType.int8, TorchIntDType.int8, 8, True, None), + (TorchIntDType.int8, None, None, False, ValueError), + (TorchIntDType.int4, None, None, False, ValueError), +] + + +class TestQuantization: + """ + Test quantization utilities + """ + + @pytest.mark.parametrize( + "weight_dtype,activation_dtype,group_size,expected_type,expected_params", + ptq_config_test_cases, + ) + @require_torch_2_6_0 + def test_get_ptq_config( + self, weight_dtype, activation_dtype, group_size, expected_type, expected_params + ): + config = get_ptq_config(weight_dtype, activation_dtype, group_size) + + assert isinstance(config, expected_type) + + for param_name, param_value in expected_params.items(): + if isinstance(param_value, (PerAxis, PerGroup)): + if isinstance(param_value, PerAxis): + assert isinstance(getattr(config, param_name), PerAxis) + assert getattr(config, param_name).axis == param_value.axis + else: + assert isinstance(getattr(config, param_name), PerGroup) + assert ( + getattr(config, param_name).group_size == param_value.group_size + ) + else: + assert getattr(config, param_name) == param_value + + @pytest.mark.parametrize( + "weight_dtype", [TorchIntDType.int8, TorchIntDType.int4, TorchIntDType.uint4] + ) + @pytest.mark.parametrize( + "activation_dtype", [None, TorchIntDType.int4, TorchIntDType.int8] + ) + @pytest.mark.parametrize("group_size", [4, 8]) + @pytest.mark.parametrize("quantize_embedding", [False, True]) + @require_torch_2_6_0 + def test_prepare_model_for_qat( + self, model, weight_dtype, activation_dtype, group_size, quantize_embedding + ): # pylint: disable=redefined-outer-name + prepare_model_for_qat( + model, weight_dtype, group_size, activation_dtype, quantize_embedding + ) + if quantize_embedding: + assert isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) + assert hasattr(model.model.embed_tokens, "weight_fake_quantizer") + assert ( + model.model.embed_tokens.weight_fake_quantizer.config.dtype + == weight_dtype.value + ) + assert ( + model.model.embed_tokens.weight_fake_quantizer.config.group_size + == group_size + ) + + for child in list(model.children()): + if isinstance(child, torch.nn.Linear): + assert isinstance(child, FakeQuantizedLinear) + assert hasattr(child, "weight_fake_quantizer") + assert child.weight_fake_quantizer.config.dtype == weight_dtype.value + assert child.weight_fake_quantizer.config.group_size == group_size + if activation_dtype: + assert hasattr(child, "activation_fake_quantizer") + assert ( + child.activation_fake_quantizer.config.dtype + == activation_dtype.value + ) + else: + assert child.activation_fake_quantizer is None + + @pytest.mark.parametrize( + "weight_dtype,activation_dtype,group_size,quantize_embedding,expected_exception", + ptq_test_cases, + ) + @require_torch_2_6_0 + def test_quantize_model_for_ptq( + self, + model, + weight_dtype, + activation_dtype, + group_size, + quantize_embedding, + expected_exception, + ): # pylint: disable=redefined-outer-name + if expected_exception: + with pytest.raises(expected_exception): + quantize_model_for_ptq( + model, + weight_dtype, + group_size, + activation_dtype, + quantize_embedding, + ) + else: + quantize_model_for_ptq( + model, weight_dtype, group_size, activation_dtype, quantize_embedding + ) + if quantize_embedding: + assert isinstance( + model.model.embed_tokens.weight, AffineQuantizedTensor + ), "Embedding weight should be quantized" + for child in list(model.children()): + if isinstance(child, torch.nn.Linear): + if activation_dtype: + assert isinstance( + child.weight, LinearActivationQuantizedTensor + ), "Linear weight should be quantized with activation quantization" + else: + assert isinstance( + child.weight, AffineQuantizedTensor + ), "Linear weight should be quantized without activation quantization" + + +class TestQuantizationCallback: + """ + Test QATCallback + """ + + @pytest.fixture() + def trainer_state(self): + return TrainerState( + global_step=0, + ) + + @require_torch_2_6_0 + def test_qat_callback_fake_quant_after_n_steps( + self, model, trainer_state + ): # pylint: disable=redefined-outer-name + cfg = QATConfig( + weight_dtype="int8", + activation_dtype="int8", + group_size=8, + quantize_embedding=True, + fake_quant_after_n_steps=100, + ) + + prepare_model_for_qat( + model, + cfg.weight_dtype, + cfg.group_size, + cfg.activation_dtype, + cfg.quantize_embedding, + ) + + # ensure model has been quantized + assert isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) + assert model.model.embed_tokens.weight_fake_quantizer.enabled + assert isinstance(model.lm_head, FakeQuantizedLinear) + assert model.lm_head.weight_fake_quantizer.enabled + + qat_callback = QATCallback(cfg) + + # simulate first training step + qat_callback.on_step_begin( + args=None, + state=trainer_state, + control=None, + model=model, + ) + + # quantization should have been disabled + assert not model.model.embed_tokens.weight_fake_quantizer.enabled + assert not model.lm_head.weight_fake_quantizer.enabled + + trainer_state.global_step = 100 + qat_callback.on_step_begin( + args=None, + state=trainer_state, + control=None, + model=model, + ) + + # quantization should have been enabled + assert model.model.embed_tokens.weight_fake_quantizer.enabled + assert model.lm_head.weight_fake_quantizer.enabled + + @require_torch_2_6_0 + def test_qat_callback_fake_quant_after_n_steps_is_none( + self, model, trainer_state + ): # pylint: disable=redefined-outer-name + cfg = QATConfig( + weight_dtype="int8", + activation_dtype="int8", + group_size=8, + quantize_embedding=True, + fake_quant_after_n_steps=None, + ) + + prepare_model_for_qat( + model, + cfg.weight_dtype, + cfg.group_size, + cfg.activation_dtype, + cfg.quantize_embedding, + ) + + # ensure model has been quantized + assert isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) + assert model.model.embed_tokens.weight_fake_quantizer.enabled + assert isinstance(model.lm_head, FakeQuantizedLinear) + assert model.lm_head.weight_fake_quantizer.enabled + + qat_callback = QATCallback(cfg) + # simulate first training step + qat_callback.on_step_begin( + args=None, + state=trainer_state, + control=None, + model=model, + ) + + # quantization should be enabled from the get-go + assert model.model.embed_tokens.weight_fake_quantizer.enabled + assert model.lm_head.weight_fake_quantizer.enabled + + +class TestConvertQATModelForPTQ: + """ + Test convert_qat_model_for_ptq + """ + + @require_torch_2_6_0 + def test_convert_qat_model_for_ptq( + self, model + ): # pylint: disable=redefined-outer-name + config = QATConfig( + weight_dtype="int8", + activation_dtype="int8", + group_size=8, + quantize_embedding=True, + ) + + # quantize model for qat + prepare_model_for_qat( + model, + config.weight_dtype, + config.group_size, + config.activation_dtype, + config.quantize_embedding, + ) + + assert isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) + assert isinstance(model.lm_head, FakeQuantizedLinear) + + # apply conversion + convert_qat_model_for_ptq( + model, + quantize_embedding=config.quantize_embedding, + ) + # ensure modules have been swapped out + assert not isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) + assert not isinstance(model.lm_head, FakeQuantizedLinear) + + # ensure weights have been quantized + assert isinstance(model.model.embed_tokens.weight, nn.Parameter) + assert isinstance(model.lm_head.weight, nn.Parameter) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 61df1d8fef..65069eb164 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -10,8 +10,6 @@ from pathlib import Path import torch - -# from importlib.metadata import version from packaging import version from tbparse import SummaryReader From 65c5481120c4afde03ca8a7ae229161806844c8c Mon Sep 17 00:00:00 2001 From: salman Date: Wed, 28 May 2025 14:57:30 +0100 Subject: [PATCH 0681/1405] Rank 0-only logging (#2608) Co-authored-by: Wing Lian --- examples/llama-3/lora-1b.yml | 3 +- src/axolotl/cli/checks.py | 5 +- src/axolotl/cli/config.py | 14 ++--- src/axolotl/cli/evaluate.py | 4 +- src/axolotl/cli/inference.py | 4 +- src/axolotl/cli/main.py | 6 +- src/axolotl/cli/merge_lora.py | 4 +- src/axolotl/cli/merge_sharded_fsdp_weights.py | 4 +- src/axolotl/cli/preprocess.py | 4 +- src/axolotl/cli/quantize.py | 4 +- src/axolotl/cli/train.py | 3 - src/axolotl/cli/utils.py | 4 +- src/axolotl/common/datasets.py | 4 +- src/axolotl/core/chat/messages.py | 1 - src/axolotl/core/trainer_builder.py | 4 +- src/axolotl/core/trainers/base.py | 4 +- src/axolotl/core/trainers/grpo/__init__.py | 4 +- src/axolotl/core/trainers/mixins/optimizer.py | 5 +- .../core/trainers/mixins/rng_state_loader.py | 5 +- src/axolotl/core/trainers/mixins/scheduler.py | 17 ++--- src/axolotl/datasets.py | 5 +- src/axolotl/integrations/base.py | 10 +-- .../cut_cross_entropy/__init__.py | 12 ++-- .../integrations/cut_cross_entropy/args.py | 5 +- src/axolotl/integrations/grokfast/__init__.py | 6 +- src/axolotl/integrations/liger/__init__.py | 16 ++--- src/axolotl/integrations/liger/args.py | 5 +- .../integrations/llm_compressor/plugin.py | 4 +- src/axolotl/integrations/spectrum/__init__.py | 10 +-- src/axolotl/loaders/adapter.py | 4 +- src/axolotl/loaders/model.py | 4 +- src/axolotl/loaders/patch_manager.py | 4 +- src/axolotl/loaders/processor.py | 4 +- src/axolotl/loaders/tokenizer.py | 4 +- src/axolotl/loaders/utils.py | 4 +- src/axolotl/monkeypatch/accelerate/fsdp2.py | 5 +- .../monkeypatch/btlm_attn_hijack_flash.py | 5 +- .../gradient_checkpointing/offload_disk.py | 5 +- .../monkeypatch/llama_attn_hijack_flash.py | 18 +++--- .../monkeypatch/llama_attn_hijack_xformers.py | 7 ++- src/axolotl/monkeypatch/lora_kernels.py | 2 +- .../monkeypatch/mistral_attn_hijack_flash.py | 18 +++--- src/axolotl/monkeypatch/peft/utils.py | 4 +- src/axolotl/monkeypatch/relora.py | 4 +- .../monkeypatch/stablelm_attn_hijack_flash.py | 4 +- src/axolotl/monkeypatch/trainer/lr.py | 6 +- .../monkeypatch/trainer_accelerator_args.py | 4 +- src/axolotl/monkeypatch/trainer_eval_guard.py | 4 +- src/axolotl/monkeypatch/trainer_fsdp_optim.py | 4 +- .../monkeypatch/transformers_fa_utils.py | 5 +- src/axolotl/monkeypatch/unsloth_.py | 14 ++--- src/axolotl/processing_strategies.py | 5 +- src/axolotl/prompt_strategies/__init__.py | 4 +- src/axolotl/prompt_strategies/base.py | 5 +- .../bradley_terry/__init__.py | 4 +- .../bradley_terry/chat_template.py | 10 +-- .../prompt_strategies/chat_template.py | 14 +++-- src/axolotl/prompt_strategies/llama2_chat.py | 6 +- .../prompt_strategies/messages/__init__.py | 5 +- src/axolotl/prompt_strategies/metharme.py | 4 +- src/axolotl/prompt_strategies/pygmalion.py | 4 +- src/axolotl/prompt_tokenizers.py | 4 +- src/axolotl/prompters.py | 5 +- src/axolotl/train.py | 8 +-- src/axolotl/utils/callbacks/__init__.py | 13 +++- src/axolotl/utils/callbacks/comet_.py | 4 +- src/axolotl/utils/callbacks/lisa.py | 5 +- src/axolotl/utils/callbacks/mlflow_.py | 4 +- src/axolotl/utils/callbacks/qat.py | 4 +- src/axolotl/utils/chat_templates.py | 11 ++-- src/axolotl/utils/comet_.py | 4 +- src/axolotl/utils/config/__init__.py | 4 +- src/axolotl/utils/data/pretraining.py | 4 +- src/axolotl/utils/data/rl.py | 8 +-- src/axolotl/utils/data/sft.py | 15 +++-- src/axolotl/utils/data/utils.py | 4 +- src/axolotl/utils/logging.py | 62 +++++++++++++++++++ src/axolotl/utils/quantization.py | 4 -- src/axolotl/utils/samplers/multipack.py | 5 +- src/axolotl/utils/schemas/config.py | 4 +- src/axolotl/utils/schemas/deprecated.py | 5 +- src/axolotl/utils/schemas/enums.py | 1 + src/axolotl/utils/schemas/integrations.py | 5 +- src/axolotl/utils/schemas/model.py | 6 +- src/axolotl/utils/schemas/training.py | 4 +- src/axolotl/utils/schemas/utils.py | 4 +- src/axolotl/utils/tokenization.py | 6 +- src/axolotl/utils/trainer.py | 21 +++---- tests/e2e/multigpu/solo/test_flex.py | 4 +- tests/e2e/multigpu/test_eval.py | 4 +- tests/e2e/multigpu/test_gemma3.py | 4 +- tests/e2e/multigpu/test_llama.py | 4 +- tests/e2e/multigpu/test_qwen2.py | 4 +- tests/e2e/multigpu/test_ray.py | 4 +- tests/e2e/patched/test_4d_multipack_llama.py | 4 +- tests/e2e/patched/test_fa_xentropy.py | 4 +- tests/e2e/patched/test_falcon_samplepack.py | 4 +- tests/e2e/patched/test_fused_llama.py | 4 +- tests/e2e/patched/test_llama_s2_attention.py | 4 +- .../e2e/patched/test_lora_llama_multipack.py | 4 +- tests/e2e/patched/test_mistral_samplepack.py | 4 +- tests/e2e/patched/test_mixtral_samplepack.py | 4 +- tests/e2e/patched/test_phi_multipack.py | 4 +- tests/e2e/patched/test_resume.py | 4 +- tests/e2e/patched/test_unsloth_qlora.py | 4 +- tests/e2e/solo/test_flex.py | 4 +- tests/e2e/solo/test_relora_llama.py | 4 +- tests/e2e/test_deepseekv3.py | 4 +- tests/e2e/test_dpo.py | 4 +- tests/e2e/test_embeddings_lr.py | 4 +- tests/e2e/test_falcon.py | 4 +- tests/e2e/test_gemma2.py | 4 +- tests/e2e/test_gemma3_text.py | 4 +- tests/e2e/test_llama.py | 4 +- tests/e2e/test_llama_pretrain.py | 4 +- tests/e2e/test_llama_vision.py | 4 +- tests/e2e/test_lora_llama.py | 4 +- tests/e2e/test_mamba.py | 4 +- tests/e2e/test_mistral.py | 4 +- tests/e2e/test_mixtral.py | 4 +- tests/e2e/test_optimizers.py | 4 +- tests/e2e/test_packing_loss.py | 4 +- tests/e2e/test_phi.py | 4 +- .../e2e/test_process_reward_model_smollm2.py | 4 +- tests/e2e/test_qwen.py | 4 +- tests/e2e/test_reward_model_smollm2.py | 4 +- tests/e2e/test_schedulers.py | 4 +- tests/integrations/test_liger.py | 13 ++-- tests/patched/test_validation.py | 38 ++++++------ tests/prompt_strategies/messages/test_chat.py | 5 +- .../prompt_strategies/test_chat_templates.py | 5 +- .../test_chat_templates_advanced.py | 5 +- .../test_chat_templates_thinking.py | 6 +- .../test_jinja_template_analyzer.py | 6 +- tests/test_prompt_tokenizers.py | 4 +- 135 files changed, 454 insertions(+), 378 deletions(-) create mode 100644 src/axolotl/utils/logging.py diff --git a/examples/llama-3/lora-1b.yml b/examples/llama-3/lora-1b.yml index c31a9f39a4..acc17e21f2 100644 --- a/examples/llama-3/lora-1b.yml +++ b/examples/llama-3/lora-1b.yml @@ -5,7 +5,7 @@ base_model: NousResearch/Llama-3.2-1B datasets: - path: teknium/GPT4-LLM-Cleaned type: alpaca -dataset_prepared_path: last_run_prepared + val_set_size: 0.1 output_dir: ./outputs/lora-out @@ -38,6 +38,7 @@ wandb_log_model: gradient_accumulation_steps: 2 micro_batch_size: 2 num_epochs: 1 + optimizer: adamw_8bit lr_scheduler: cosine learning_rate: 0.0002 diff --git a/src/axolotl/cli/checks.py b/src/axolotl/cli/checks.py index 47348240eb..10086c2a4c 100644 --- a/src/axolotl/cli/checks.py +++ b/src/axolotl/cli/checks.py @@ -1,6 +1,5 @@ """Various checks for Axolotl CLI.""" -import logging import os from pathlib import Path @@ -8,7 +7,9 @@ from huggingface_hub import HfApi from huggingface_hub.utils import LocalTokenNotFoundError -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) def check_accelerate_default_config() -> None: diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index 8f1fe7185d..d55448da4d 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -1,7 +1,6 @@ """Configuration loading and processing.""" import json -import logging import os import tempfile from pathlib import Path @@ -22,11 +21,12 @@ validate_config, ) from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from axolotl.utils.mlflow_ import setup_mlflow_env_vars from axolotl.utils.trainer import prepare_opinionated_env, prepare_optim_env from axolotl.utils.wandb_ import setup_wandb_env_vars -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__, use_environ=True) def check_remote_config(config: Union[str, Path]) -> Union[str, Path]: @@ -119,12 +119,12 @@ def choose_config(path: Path) -> str: ) if len(yaml_files) == 1: - print(f"Using default YAML file '{yaml_files[0]}'") + LOG.info(f"Using default YAML file '{yaml_files[0]}'") return str(yaml_files[0]) - print("Choose a YAML file:") + LOG.info("Choose a YAML file:") for idx, file in enumerate(yaml_files): - print(f"{idx + 1}. {file}") + LOG.info(f"{idx + 1}. {file}") chosen_file = None while chosen_file is None: @@ -133,9 +133,9 @@ def choose_config(path: Path) -> str: if 1 <= choice <= len(yaml_files): chosen_file = str(yaml_files[choice - 1]) else: - print("Invalid choice. Please choose a number from the list.") + LOG.info("Invalid choice. Please choose a number from the list.") except ValueError: - print("Invalid input. Please enter a number.") + LOG.info("Invalid input. Please enter a number.") return chosen_file diff --git a/src/axolotl/cli/evaluate.py b/src/axolotl/cli/evaluate.py index e52da66b72..f131f70830 100644 --- a/src/axolotl/cli/evaluate.py +++ b/src/axolotl/cli/evaluate.py @@ -1,6 +1,5 @@ """CLI to run evaluation on a model.""" -import logging import os from pathlib import Path from typing import Union @@ -17,8 +16,9 @@ from axolotl.evaluate import evaluate from axolotl.utils import patch_optimized_env from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def do_evaluate(cfg: DictDefault, cli_args: TrainerCliArgs) -> None: diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index a4906bbf36..b5bc158fa1 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -1,7 +1,6 @@ """CLI to run inference on a trained model.""" import importlib -import logging import sys from pathlib import Path from threading import Thread @@ -22,8 +21,9 @@ get_chat_template_from_config, ) from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def get_multi_line_input() -> str: diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index e61dad5d63..3dafa552bd 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -2,7 +2,6 @@ # pylint: disable=redefined-outer-name -import logging import os import subprocess # nosec B404 import tempfile @@ -31,8 +30,11 @@ ) from axolotl.integrations.lm_eval.cli import lm_eval from axolotl.utils import patch_optimized_env +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.config import AxolotlInputConfig +LOG = get_logger(__name__) + @click.group() @click.version_option(version=axolotl.__version__, prog_name="axolotl") @@ -177,7 +179,7 @@ def iter_configs(): do_cli(config=cfg_file, **kwargs) except subprocess.CalledProcessError as exc: - logging.error(f"Failed to train/fine-tune config '{cfg_file}': {exc}") + LOG.error(f"Failed to train/fine-tune config '{cfg_file}': {exc}") if not sweep: raise exc diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 5c8802dd11..2e59d25374 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -1,6 +1,5 @@ """CLI to merge a trained LoRA into a base model.""" -import logging from pathlib import Path from typing import Union @@ -13,8 +12,9 @@ from axolotl.cli.config import load_cfg from axolotl.cli.utils import load_model_and_tokenizer from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def do_merge_lora(*, cfg: DictDefault) -> None: diff --git a/src/axolotl/cli/merge_sharded_fsdp_weights.py b/src/axolotl/cli/merge_sharded_fsdp_weights.py index d4b36d92c6..297d7946e4 100644 --- a/src/axolotl/cli/merge_sharded_fsdp_weights.py +++ b/src/axolotl/cli/merge_sharded_fsdp_weights.py @@ -1,7 +1,6 @@ """CLI to merge sharded FSDP model checkpoints into a single combined checkpoint.""" import json -import logging import os import shutil from pathlib import Path @@ -27,8 +26,9 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.config import load_cfg +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) class BFloat16CastPlanner(_EmptyStateDictLoadPlanner): diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index 2a4dcd2886..9f96f5cc17 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -1,6 +1,5 @@ """CLI to run preprocessing of a dataset.""" -import logging import warnings from pathlib import Path from typing import Union @@ -20,9 +19,10 @@ from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.integrations.base import PluginManager from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from axolotl.utils.trainer import disable_datasets_caching -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def do_preprocess(cfg: DictDefault, cli_args: PreprocessCliArgs) -> None: diff --git a/src/axolotl/cli/quantize.py b/src/axolotl/cli/quantize.py index 2036fddeac..63d51fadf0 100644 --- a/src/axolotl/cli/quantize.py +++ b/src/axolotl/cli/quantize.py @@ -2,7 +2,6 @@ CLI to post-training quantize a model using torchao """ -import logging from pathlib import Path from typing import Union @@ -11,9 +10,10 @@ from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.config import load_cfg from axolotl.loaders import load_tokenizer +from axolotl.utils.logging import get_logger from axolotl.utils.quantization import TorchIntDType, quantize_model_for_ptq -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def do_quantize( diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 777d848853..fef80fdbaf 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -1,7 +1,6 @@ """CLI to run training on a model.""" import gc -import logging import os from pathlib import Path from typing import Union @@ -22,8 +21,6 @@ from axolotl.utils.config import normalize_config, resolve_dtype from axolotl.utils.dict import DictDefault -LOG = logging.getLogger(__name__) - def do_train(cfg: DictDefault, cli_args: TrainerCliArgs): """ diff --git a/src/axolotl/cli/utils.py b/src/axolotl/cli/utils.py index e681589f3b..d287953613 100644 --- a/src/axolotl/cli/utils.py +++ b/src/axolotl/cli/utils.py @@ -4,7 +4,6 @@ import dataclasses import hashlib import json -import logging from functools import wraps from pathlib import Path from types import NoneType @@ -23,8 +22,9 @@ from axolotl.loaders import load_processor, load_tokenizer from axolotl.loaders.model import ModelLoader from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def strip_optional_type(field_type: type | str | None): diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index e3ffb7ae9a..d9c3841124 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -1,6 +1,5 @@ """Dataset loading utilities.""" -import logging import math import random from dataclasses import dataclass @@ -14,10 +13,11 @@ from axolotl.utils.data import prepare_dataset from axolotl.utils.data.rl import load_prepare_preference_datasets from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import RLType from axolotl.utils.tokenization import check_dataset_labels -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) @dataclass diff --git a/src/axolotl/core/chat/messages.py b/src/axolotl/core/chat/messages.py index 88ff2b7ad0..923b177c1f 100644 --- a/src/axolotl/core/chat/messages.py +++ b/src/axolotl/core/chat/messages.py @@ -156,7 +156,6 @@ def tokenized( len(input_ids) : len(input_ids) + len(pending_input_ids) ] if new_pending_inputs != pending_input_ids: - # logging.warning("tokenization mismatch from concatenation.") pending_input_ids = new_pending_inputs input_ids.extend(pending_input_ids) if pending_weight: diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py index 08759d9f9f..46ec12ccb6 100755 --- a/src/axolotl/core/trainer_builder.py +++ b/src/axolotl/core/trainer_builder.py @@ -19,7 +19,6 @@ import importlib import importlib.util import inspect -import logging import math import os import sys @@ -88,6 +87,7 @@ V2BatchSamplerDataCollatorForSeq2Seq, ) from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import CustomSupportedOptimizers, RLType try: @@ -95,7 +95,7 @@ except ImportError: pass -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) class TrainerBuilderBase(abc.ABC): diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index d5cfc23df0..25e9f9f0ae 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -4,7 +4,6 @@ from __future__ import annotations -import logging import os from collections import defaultdict from functools import wraps @@ -34,9 +33,10 @@ sanitize_kwargs_for_ds_tagging, sanitize_kwargs_for_tagging, ) +from axolotl.utils.logging import get_logger from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) class AxolotlTrainer(SchedulerMixin, OptimizerMixin, RngLoaderMixin, Trainer): diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index f4685893b0..196cdb56a5 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -2,7 +2,6 @@ import importlib import inspect -import logging from typing import Any from trl.trainer.grpo_trainer import RewardFunc @@ -13,9 +12,10 @@ AxolotlGRPOTrainer, ) from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.trl import TRLConfig -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) class GRPOStrategy: diff --git a/src/axolotl/core/trainers/mixins/optimizer.py b/src/axolotl/core/trainers/mixins/optimizer.py index bde58aa1d5..abb662706a 100644 --- a/src/axolotl/core/trainers/mixins/optimizer.py +++ b/src/axolotl/core/trainers/mixins/optimizer.py @@ -1,18 +1,17 @@ """Module for Axolotl trainer optimizer mixin""" -import logging - from peft.optimizers import create_loraplus_optimizer from torch import nn from transformers.trainer import Trainer from transformers.utils import is_sagemaker_mp_enabled from axolotl.integrations.base import BaseOptimizerFactory +from axolotl.utils.logging import get_logger if is_sagemaker_mp_enabled(): import smdistributed.modelparallel.torch as smp -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) class OptimizerMixin(Trainer): diff --git a/src/axolotl/core/trainers/mixins/rng_state_loader.py b/src/axolotl/core/trainers/mixins/rng_state_loader.py index 0e101dabb9..f248394b2e 100644 --- a/src/axolotl/core/trainers/mixins/rng_state_loader.py +++ b/src/axolotl/core/trainers/mixins/rng_state_loader.py @@ -6,7 +6,6 @@ TODO: Remove when upstream added PR to release """ -import logging import os import random @@ -17,7 +16,9 @@ from transformers.trainer_pt_utils import set_rng_state_for_device from transformers.training_args import ParallelMode -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) class RngLoaderMixin(Trainer): diff --git a/src/axolotl/core/trainers/mixins/scheduler.py b/src/axolotl/core/trainers/mixins/scheduler.py index 0c36f9f95d..90070ab78a 100644 --- a/src/axolotl/core/trainers/mixins/scheduler.py +++ b/src/axolotl/core/trainers/mixins/scheduler.py @@ -1,12 +1,11 @@ """Module for Axolotl trainer scheduler mixin""" -import logging - import torch from torch.optim.lr_scheduler import LRScheduler, OneCycleLR from transformers.trainer import Trainer from axolotl.integrations.base import PluginManager +from axolotl.utils.logging import get_logger from axolotl.utils.schedulers import ( RexLR, get_cosine_schedule_with_min_lr, @@ -14,7 +13,7 @@ get_cosine_schedule_with_warmup_decay_constant, ) -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) class SchedulerMixin(Trainer): @@ -80,13 +79,15 @@ def create_scheduler( self.lr_scheduler = RexLR( optimizer=optimizer, max_lr=self.args.learning_rate, - min_lr=0 if not use_cosine_min_lr else (self.args.learning_rate * self.args.cosine_min_lr_ratio), + min_lr=0 if not use_cosine_min_lr else ( + self.args.learning_rate * self.args.cosine_min_lr_ratio), total_steps=num_training_steps, num_warmup_steps=self.args.get_warmup_steps(num_training_steps), ) elif use_cosine_quadratic: if use_cosine_min_lr: - LOG.warning("Both cosine quadratic warmup and min lr detected. Using quadratic warmup.") + LOG.warning( + "Both cosine quadratic warmup and min lr detected. Using quadratic warmup.") self.lr_scheduler = get_cosine_schedule_with_quadratic_warmup( # pylint: disable=attribute-defined-outside-init optimizer, @@ -115,9 +116,11 @@ def create_scheduler( return super().create_scheduler(num_training_steps, optimizer=optimizer) else: if use_cosine_quadratic: - LOG.warning("axolotl's cosine scheduler with quadratic warmup not used (e.g., because of deepspeed).") + LOG.warning( + "axolotl's cosine scheduler with quadratic warmup not used (e.g., because of deepspeed).") if use_cosine_min_lr: - LOG.warning("axolotl's cosine scheduler with min lr not used (e.g., because of deepspeed).") + LOG.warning( + "axolotl's cosine scheduler with min lr not used (e.g., because of deepspeed).") return self.lr_scheduler # type: ignore diff --git a/src/axolotl/datasets.py b/src/axolotl/datasets.py index 143928019b..9f1d9500d6 100644 --- a/src/axolotl/datasets.py +++ b/src/axolotl/datasets.py @@ -1,12 +1,13 @@ """Module containing Dataset functionality""" -import logging import os from typing import List, Optional, Union import torch from datasets import Dataset, IterableDataset +from axolotl.utils.logging import get_logger + from .prompt_tokenizers import PromptTokenizingStrategy # We want this to be a wrapper for an existing dataset that we have loaded @@ -15,7 +16,7 @@ # let's check to ensure we don't truncate an item in the middle, we'll use # the collators later on to pad the datasets -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__) class TokenizedPromptDataset(Dataset): diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index eb2b29cbe7..11d85f8f81 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -22,7 +22,6 @@ import collections import importlib -import logging from typing import TYPE_CHECKING, Callable, OrderedDict, Union from peft import PeftModel @@ -31,6 +30,9 @@ from transformers import PreTrainedModel, Trainer from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) if TYPE_CHECKING: from axolotl.common.datasets import TrainDatasetMeta @@ -331,12 +333,12 @@ def register(self, plugin_name: str): ImportError: If the plugin module cannot be imported. """ try: - logging.info(f"Attempting to load plugin: {plugin_name}") + LOG.info(f"Attempting to load plugin: {plugin_name}") plugin = load_plugin(plugin_name) self.plugins[plugin_name] = plugin - logging.info(f"Plugin loaded successfully: {plugin_name}") + LOG.info(f"Plugin loaded successfully: {plugin_name}") except ImportError: - logging.error(f"Failed to load plugin: {plugin_name}") + LOG.error(f"Failed to load plugin: {plugin_name}") def get_input_args(self) -> list[str]: """Returns a list of Pydantic classes for all registered plugins' input arguments.' diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 7420674fa9..a7e94e3637 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -19,17 +19,16 @@ from Apple's ML team. """ import importlib -import logging import torch from axolotl.integrations.base import BasePlugin from axolotl.utils import get_pytorch_version -from axolotl.utils.distributed import is_main_process +from axolotl.utils.logging import get_logger from .args import CutCrossEntropyArgs # pylint: disable=unused-import. # noqa: F401 -LOG = logging.getLogger("axolotl.integrations.cut_cross_entropy") +LOG = get_logger(__name__, use_environ=True) _CCE_INSTALL_MESSAGE = ( "Please install cut_cross_entropy with transformers support using " @@ -76,10 +75,9 @@ def pre_model_load(self, cfg): cce_patch, ) - if is_main_process(use_environ=True): - LOG.info( - f"Applying Cut Cross Entropy to model type: {cfg.model_config_type}" - ) + LOG.info( + f"Applying Cut Cross Entropy to model type: {cfg.model_config_type}" + ) # The patch checks model_type internally cce_patch(cfg.model_config_type) diff --git a/src/axolotl/integrations/cut_cross_entropy/args.py b/src/axolotl/integrations/cut_cross_entropy/args.py index da1db73976..2729ebe2e3 100644 --- a/src/axolotl/integrations/cut_cross_entropy/args.py +++ b/src/axolotl/integrations/cut_cross_entropy/args.py @@ -15,12 +15,13 @@ """ Module for handling Cut Cross Entropy input arguments. """ -import logging from typing import Optional from pydantic import BaseModel, model_validator -LOG = logging.getLogger("axolotl.integrations.cut_cross_entropy.args") +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) class CutCrossEntropyArgs(BaseModel): diff --git a/src/axolotl/integrations/grokfast/__init__.py b/src/axolotl/integrations/grokfast/__init__.py index c8c352bbe0..234d27226a 100644 --- a/src/axolotl/integrations/grokfast/__init__.py +++ b/src/axolotl/integrations/grokfast/__init__.py @@ -2,15 +2,15 @@ Grokfast plugin for Axolotl """ -import logging - from transformers.trainer_callback import TrainerCallback +from axolotl.utils.logging import get_logger + from ..base import BasePlugin from .args import GrokfastArgs # pylint: disable=unused-import. # noqa: F401 from .optimizer import gradfilter_ema -LOG = logging.getLogger("axolotl.integrations.grokfast") +LOG = get_logger(__name__) class GrokfastCallbackHandler(TrainerCallback): diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index c7ac423729..1c17ab2b59 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -19,16 +19,15 @@ It is designed to be performant, correct, and light-weight. """ import inspect -import logging import sys from axolotl.integrations.base import BasePlugin -from axolotl.utils.distributed import is_main_process +from axolotl.utils.logging import get_logger from .args import LigerArgs # pylint: disable=unused-import. # noqa: F401 from .utils import patch_with_compile_disable -LOG = logging.getLogger("axolotl.integrations.liger") +LOG = get_logger(__name__, use_environ=True) class LigerPlugin(BasePlugin): @@ -85,10 +84,7 @@ def pre_model_load(self, cfg): kwargs["geglu"] = cfg.liger_glu_activation elif "swiglu" in liger_fn_sig.parameters: kwargs["swiglu"] = cfg.liger_glu_activation - if is_main_process(use_environ=True): - LOG.info( - f"Applying LIGER to {cfg.model_config_type} with kwargs: {kwargs}" - ) + LOG.info(f"Applying LIGER to {cfg.model_config_type} with kwargs: {kwargs}") apply_liger_fn(**kwargs) elif cfg.model_config_type == "jamba": from transformers.models.jamba import modeling_jamba @@ -124,9 +120,9 @@ def pre_model_load(self, cfg): if cfg.liger_rope: # The DeepseekV2 version of RoPE is different than upstream LLaMA. # See https://github.com/linkedin/Liger-Kernel/issues/129#issuecomment-2313763528 - logging.warning("Fused liger_rope is not supported for DeepseekV2.") + LOG.warning("Fused liger_rope is not supported for DeepseekV2.") if cfg.liger_glu_activation: - logging.warning("liger_glu_activation is not supported for DeepseekV2.") + LOG.warning("liger_glu_activation is not supported for DeepseekV2.") if cfg.liger_rms_norm: modeling_mod.DeepseekV2RMSNorm = LigerRMSNorm if cfg.liger_glu_activation: @@ -186,6 +182,6 @@ def pre_model_load(self, cfg): swiglu=cfg.liger_glu_activation, ) else: - logging.warning( + LOG.warning( f"Unsupported model config type: {cfg.model_config_type}. Liger not applied." ) diff --git a/src/axolotl/integrations/liger/args.py b/src/axolotl/integrations/liger/args.py index 02ece31432..7c9eb23d56 100644 --- a/src/axolotl/integrations/liger/args.py +++ b/src/axolotl/integrations/liger/args.py @@ -15,12 +15,13 @@ """ Module for handling LIGER input arguments. """ -import logging from typing import Optional from pydantic import BaseModel, model_validator -LOG = logging.getLogger("axolotl.integrations.liger.args") +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) class LigerArgs(BaseModel): diff --git a/src/axolotl/integrations/llm_compressor/plugin.py b/src/axolotl/integrations/llm_compressor/plugin.py index d986d51f47..57d506a573 100644 --- a/src/axolotl/integrations/llm_compressor/plugin.py +++ b/src/axolotl/integrations/llm_compressor/plugin.py @@ -3,7 +3,6 @@ by maintaining masks for zero weights during training. """ -import logging from functools import wraps from typing import Any, Callable, Concatenate, ParamSpec, TypeVar @@ -16,11 +15,12 @@ from transformers.training_args import TrainingArguments from axolotl.integrations.base import BasePlugin +from axolotl.utils.logging import get_logger P = ParamSpec("P") # Params for generic function signatures R = TypeVar("R") # Return type for generic function signatures -LOG = logging.getLogger("axolotl.integrations.llm_compressor") +LOG = get_logger(__name__) class LLMCompressorCallbackHandler(TrainerCallback): diff --git a/src/axolotl/integrations/spectrum/__init__.py b/src/axolotl/integrations/spectrum/__init__.py index 6059e7951c..9f66aef97f 100644 --- a/src/axolotl/integrations/spectrum/__init__.py +++ b/src/axolotl/integrations/spectrum/__init__.py @@ -17,14 +17,16 @@ """ import json -import logging import requests from axolotl.integrations.base import BasePlugin +from axolotl.utils.logging import get_logger from .args import SpectrumArgs # pylint: disable=unused-import. # noqa: F401 +LOG = get_logger(__name__) + def _generate_unfrozen_params_yaml(snr_data, top_fraction=0.5): unfrozen_parameters = {} @@ -83,17 +85,17 @@ def pre_model_load(self, cfg): except FileNotFoundError: pass except Exception as exc: # pylint: disable=broad-exception-caught - logging.warning(f"Failed to read SNR data from {snr_path}: {exc}") + LOG.warning(f"Failed to read SNR data from {snr_path}: {exc}") if not snr_data: try: snr_data = requests.get(snr_url, timeout=60).json() except requests.exceptions.RequestException as exc: - logging.warning(f"Failed to fetch SNR data from {snr_url}: {exc}") + LOG.warning(f"Failed to fetch SNR data from {snr_url}: {exc}") return # also catch json parsing errors except json.JSONDecodeError as exc: - logging.warning(f"Failed to parse SNR data from {snr_url}: {exc}") + LOG.warning(f"Failed to parse SNR data from {snr_url}: {exc}") return unfrozen_parameters = _generate_unfrozen_params_yaml( diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index f7a484e9bc..16d8daac8e 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -1,6 +1,5 @@ """Adapter loading functionality, including LoRA / QLoRA and associated utils""" -import logging import os import types from typing import Any @@ -21,8 +20,9 @@ from axolotl.loaders.utils import get_linear_embedding_layers from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def setup_quantized_meta_for_peft(model: torch.nn.Module): diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 8d8f927a78..681e5d3352 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -3,7 +3,6 @@ """ import gc -import logging import math import os from functools import cached_property @@ -47,10 +46,11 @@ get_device_count, get_device_type, ) +from axolotl.utils.logging import get_logger from axolotl.utils.model_shard_quant import load_sharded_model_quant from axolotl.utils.schemas.enums import RLType -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) PLUGIN_MANAGER = PluginManager.get_instance() diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 36813bafd8..ce1f5cf709 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -4,7 +4,6 @@ """ import importlib.util -import logging from functools import cached_property import addict @@ -17,8 +16,9 @@ patch_for_multipack, ) from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) PLUGIN_MANAGER = PluginManager.get_instance() diff --git a/src/axolotl/loaders/processor.py b/src/axolotl/loaders/processor.py index 57394bc670..2e3ec8d7fe 100644 --- a/src/axolotl/loaders/processor.py +++ b/src/axolotl/loaders/processor.py @@ -1,6 +1,5 @@ """Processor loading functionality for multi-modal models""" -import logging from typing import Any import transformers @@ -10,8 +9,9 @@ ) from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def load_processor(cfg: DictDefault, tokenizer: PreTrainedTokenizerBase): diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index ec9d69e8a1..c311d52472 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -1,7 +1,6 @@ """Tokenizer loading functionality and associated utils""" import json -import logging import os import transformers @@ -19,8 +18,9 @@ is_local_main_process, is_main_process, ) +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) PLUGIN_MANAGER = PluginManager.get_instance() diff --git a/src/axolotl/loaders/utils.py b/src/axolotl/loaders/utils.py index 1aae4834d1..28c9350853 100644 --- a/src/axolotl/loaders/utils.py +++ b/src/axolotl/loaders/utils.py @@ -1,7 +1,6 @@ """Utilities for axolotl.loaders module""" import contextlib -import logging from typing import Type import addict @@ -9,8 +8,9 @@ from transformers import AutoConfig, PretrainedConfig, PreTrainedModel from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def get_module_class_from_name( diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index ffde17aebd..6a7d48236c 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -2,12 +2,13 @@ monkeypatch for accelerate fsdp2 fix when modifying ordereddict during interation, and saving full state dicts """ -import logging import sys import torch -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) def fsdp2_load_full_state_dict(accelerator, model: torch.nn.Module, full_sd: dict): diff --git a/src/axolotl/monkeypatch/btlm_attn_hijack_flash.py b/src/axolotl/monkeypatch/btlm_attn_hijack_flash.py index 1275906804..589980c8b9 100644 --- a/src/axolotl/monkeypatch/btlm_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/btlm_attn_hijack_flash.py @@ -3,7 +3,6 @@ """ import importlib -import logging from typing import Optional, Tuple import torch @@ -11,7 +10,9 @@ from flash_attn.flash_attn_interface import flash_attn_func from transformers import AutoConfig, AutoModelForCausalLM -LOG = logging.getLogger("axolotl") +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) def replace_btlm_attn_with_flash_attn(model_name="cerebras/btlm-3b-8k-base"): diff --git a/src/axolotl/monkeypatch/gradient_checkpointing/offload_disk.py b/src/axolotl/monkeypatch/gradient_checkpointing/offload_disk.py index 90e70f504a..792d3c6efc 100644 --- a/src/axolotl/monkeypatch/gradient_checkpointing/offload_disk.py +++ b/src/axolotl/monkeypatch/gradient_checkpointing/offload_disk.py @@ -18,7 +18,6 @@ import atexit import concurrent.futures -import logging import os import queue import shutil @@ -32,11 +31,13 @@ import torch +from axolotl.utils.logging import get_logger + torch_cuda_amp_custom_fwd = torch.amp.custom_fwd(device_type="cuda") torch_cuda_amp_custom_bwd = torch.amp.custom_bwd(device_type="cuda") # Setup logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class DiskOffloadManager: diff --git a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py index 998a810279..70e36714c8 100644 --- a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py @@ -2,7 +2,6 @@ # copied from https://github.com/lm-sys/FastChat/blob/main/fastchat/train/llama_flash_attn_monkey_patch.py -import logging import warnings from typing import List, Optional, Tuple, Union @@ -25,6 +24,7 @@ ) from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids, set_module_name +from axolotl.utils.logging import get_logger try: from flash_attn.flash_attn_interface import ( # pylint: disable=ungrouped-imports @@ -41,7 +41,7 @@ ) -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__) def is_xformers_available() -> bool: @@ -612,9 +612,10 @@ def generate_qkv( q, query_padding_mask ) - output_pad_fn = lambda output_unpad: pad_input( # noqa: E731 - output_unpad, indices_q, batch_size, seqlen_q - ) + def output_pad_fn(output_unpad): + return pad_input( # noqa: E731 + output_unpad, indices_q, batch_size, seqlen_q + ) else: q_unpad = rearrange(q, "b s h d -> (b s) h d") @@ -627,9 +628,10 @@ def generate_qkv( ) max_seqlen_q = seqlen_q - output_pad_fn = lambda output_unpad: rearrange( # noqa: E731 - output_unpad, "(b s) h d -> b s h d", b=batch_size - ) + def output_pad_fn(output_unpad): + return rearrange( # noqa: E731 + output_unpad, "(b s) h d -> b s h d", b=batch_size + ) if key_padding_mask is not None: k_unpad, _, cu_seqlens_k, max_seqlen_k = unpad_input(k, key_padding_mask) diff --git a/src/axolotl/monkeypatch/llama_attn_hijack_xformers.py b/src/axolotl/monkeypatch/llama_attn_hijack_xformers.py index 0c1a4e8224..28223eee36 100644 --- a/src/axolotl/monkeypatch/llama_attn_hijack_xformers.py +++ b/src/axolotl/monkeypatch/llama_attn_hijack_xformers.py @@ -2,7 +2,6 @@ Directly copied the code from https://raw.githubusercontent.com/oobabooga/text-generation-webui/main/modules/llama_attn_hijack.py and made some adjustments """ -import logging import warnings from typing import Optional, Tuple @@ -11,10 +10,14 @@ import transformers.models.llama.modeling_llama from transformers.models.llama.modeling_llama import apply_rotary_pos_emb, repeat_kv +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + try: import xformers.ops except ImportError: - logging.error("xformers not found! Please install it before trying to use it.") + LOG.error("xformers not found! Please install it before trying to use it.") def hijack_llama_attention(): diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 6c920dcc86..11e0989cf5 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -7,7 +7,6 @@ from typing import Generator, Tuple, Type import torch -from accelerate.logging import get_logger from peft import PeftModelForCausalLM from torch import nn from transformers import AutoConfig @@ -20,6 +19,7 @@ ) from axolotl.monkeypatch.utils import detab_code from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger LOG = get_logger(__name__) diff --git a/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py b/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py index ac9815fce2..3fc22917fb 100644 --- a/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py @@ -2,7 +2,6 @@ # pylint: disable=duplicate-code -import logging from functools import partial from typing import List, Optional, Tuple, Union @@ -28,8 +27,9 @@ ) from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl.monkeypatch.mistral") +LOG = get_logger(__name__) def replace_mistral_attn_with_flash_attn( @@ -359,9 +359,10 @@ def generate_qkv( q, query_padding_mask ) - output_pad_fn = lambda output_unpad: pad_input( # noqa: E731 - output_unpad, indices_q, batch_size, seqlen_q - ) + def output_pad_fn(output_unpad): + return pad_input( # noqa: E731 + output_unpad, indices_q, batch_size, seqlen_q + ) else: q_unpad = rearrange(q, "b s h d -> (b s) h d") @@ -374,9 +375,10 @@ def generate_qkv( ) max_seqlen_q = seqlen_q - output_pad_fn = lambda output_unpad: rearrange( # noqa: E731 - output_unpad, "(b s) h d -> b s h d", b=batch_size - ) + def output_pad_fn(output_unpad): + return rearrange( # noqa: E731 + output_unpad, "(b s) h d -> b s h d", b=batch_size + ) if key_padding_mask is not None: k_unpad, _, cu_seqlens_k, max_seqlen_k = unpad_input(k, key_padding_mask) diff --git a/src/axolotl/monkeypatch/peft/utils.py b/src/axolotl/monkeypatch/peft/utils.py index fdc49c5f6a..0c571fbd2f 100644 --- a/src/axolotl/monkeypatch/peft/utils.py +++ b/src/axolotl/monkeypatch/peft/utils.py @@ -3,14 +3,14 @@ """ import inspect -import logging import peft import axolotl from axolotl.monkeypatch.utils import detab_code +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) ORIGINAL_PREPARE_CODE = """ for param in model.parameters(): diff --git a/src/axolotl/monkeypatch/relora.py b/src/axolotl/monkeypatch/relora.py index 4a27dde81f..5b7418e39d 100644 --- a/src/axolotl/monkeypatch/relora.py +++ b/src/axolotl/monkeypatch/relora.py @@ -2,7 +2,6 @@ import glob import json -import logging import os.path import shutil from functools import partial @@ -27,8 +26,9 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import barrier, is_main_process +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl.relora") +LOG = get_logger(__name__) @torch.no_grad() diff --git a/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py b/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py index c603021116..85454fe2e3 100644 --- a/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py @@ -32,11 +32,11 @@ from torch import nn from transformers import AutoConfig, AutoModelForCausalLM from transformers.modeling_outputs import BaseModelOutputWithPast -from transformers.utils import logging from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids +from axolotl.utils.logging import get_logger -logger = logging.get_logger(__name__) +logger = get_logger(__name__) def replace_stablelm_attn_with_flash_attn(model_name="stabilityai/stablelm-3b-4e1t"): diff --git a/src/axolotl/monkeypatch/trainer/lr.py b/src/axolotl/monkeypatch/trainer/lr.py index 0176093d6a..9afc23c466 100644 --- a/src/axolotl/monkeypatch/trainer/lr.py +++ b/src/axolotl/monkeypatch/trainer/lr.py @@ -2,11 +2,11 @@ monkeypatch for Trainer _get_learning_rate method """ -import logging - import torch -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) # TODO remove this patch once https://github.com/huggingface/transformers/pull/37881 is included in a release diff --git a/src/axolotl/monkeypatch/trainer_accelerator_args.py b/src/axolotl/monkeypatch/trainer_accelerator_args.py index d87812c9f8..0a5b27c13e 100644 --- a/src/axolotl/monkeypatch/trainer_accelerator_args.py +++ b/src/axolotl/monkeypatch/trainer_accelerator_args.py @@ -3,13 +3,13 @@ """ import inspect -import logging from transformers import Trainer from axolotl.monkeypatch.utils import detab_code +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) ORIGINAL_TRAINER_CODE = """ # create accelerator object diff --git a/src/axolotl/monkeypatch/trainer_eval_guard.py b/src/axolotl/monkeypatch/trainer_eval_guard.py index e929ac766a..8488a16df9 100644 --- a/src/axolotl/monkeypatch/trainer_eval_guard.py +++ b/src/axolotl/monkeypatch/trainer_eval_guard.py @@ -3,13 +3,13 @@ """ import inspect -import logging from transformers import Trainer from axolotl.monkeypatch.utils import detab_code +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) ORIGINAL_TRAINER_CODE = """ model.eval() diff --git a/src/axolotl/monkeypatch/trainer_fsdp_optim.py b/src/axolotl/monkeypatch/trainer_fsdp_optim.py index 1cbfefa5b1..4ce5b8ecd3 100644 --- a/src/axolotl/monkeypatch/trainer_fsdp_optim.py +++ b/src/axolotl/monkeypatch/trainer_fsdp_optim.py @@ -3,13 +3,13 @@ """ import inspect -import logging from transformers import Trainer from axolotl.monkeypatch.utils import detab_code +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl.monkeypatch.trainer_fsdp_save") +LOG = get_logger(__name__) ORIGINAL_TRAINER_CODE = """ diff --git a/src/axolotl/monkeypatch/transformers_fa_utils.py b/src/axolotl/monkeypatch/transformers_fa_utils.py index f34ecb8c07..e372dc3f85 100644 --- a/src/axolotl/monkeypatch/transformers_fa_utils.py +++ b/src/axolotl/monkeypatch/transformers_fa_utils.py @@ -2,13 +2,14 @@ see https://github.com/huggingface/transformers/pull/35834 """ -import logging from functools import partial from typing import Optional import torch -logger = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +logger = get_logger(__name__) def fixed_fa_peft_integration_check( diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py index c81bacbfcb..61f4eeea03 100644 --- a/src/axolotl/monkeypatch/unsloth_.py +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -11,7 +11,7 @@ from axolotl.monkeypatch.utils import detab_code -LOG = get_logger("axolotl.monkeypatch.unsloth") +LOG = get_logger(__name__) ORIGINAL_QKV_CODE = """ query_states = self.q_proj(hidden_states) @@ -133,7 +133,7 @@ def patch_self_attn_lora(): ) exec(self_attn_forward, globals()) # pylint: disable=exec-used # nosec B102 self_attn_lora_patched = True - LOG.info("patching unsloth attn lora", main_process_only=True) + LOG.info("patching unsloth attn lora") LlamaFlashAttention2.forward = ( unsloth_attn_forward # pylint: disable=undefined-variable # noqa: F821 ) @@ -153,7 +153,7 @@ def apply_rotary_pos_emb( # pylint: disable=unused-argument ): return fast_rope_embedding(q, k, cos, sin) - LOG.info("patching unsloth RoPE embeddings", main_process_only=True) + LOG.info("patching unsloth RoPE embeddings") transformers.models.llama.modeling_llama.apply_rotary_pos_emb = apply_rotary_pos_emb @@ -189,7 +189,7 @@ def integrate_lora_mlp_patch(peft_model: PeftModelForCausalLM): if is_mlp_lora and mlp_no_bias and mlp_not_dora: layer.mlp.forward = types.MethodType(apply_lora_mlp, layer.mlp) else: - LOG.warning("unable to apply unsloth lora mlp patch to layer %d", idx) + LOG.warning(f"unable to apply unsloth lora mlp patch to layer {idx}") def integrate_lora_patch(peft_model: PeftModelForCausalLM, cfg): @@ -215,7 +215,7 @@ def integrate_lora_patch(peft_model: PeftModelForCausalLM, cfg): layer.self_attn.apply_qkv = apply_lora_qkv else: layer.self_attn.apply_qkv = original_apply_qkv - LOG.warning("unable to apply unsloth lora qkv patch to layer %d", idx) + LOG.warning(f"unable to apply unsloth lora qkv patch to layer {idx}") if cfg.unsloth_lora_o: layer_modules = [ getattr(layer.self_attn, linear_proj) for linear_proj in ["o_proj"] @@ -234,9 +234,7 @@ def integrate_lora_patch(peft_model: PeftModelForCausalLM, cfg): layer.self_attn.apply_o = apply_lora_o else: layer.self_attn.apply_o = original_apply_o - LOG.warning( - "unable to apply unsloth lora o_proj patch to layer %d", idx - ) + LOG.warning(f"unable to apply unsloth lora o_proj patch to layer {idx}") def patch_unsloth_layernorm(): diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 1cb6ed0648..ce9b6a838d 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -1,6 +1,5 @@ """Module containing ProcessingStrategy classes and its derivative for different MultiModal Model types""" -import logging from copy import deepcopy from typing import Optional @@ -10,7 +9,9 @@ from transformers import ProcessorMixin from transformers.image_utils import load_image -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) class ProcessingStrategy: diff --git a/src/axolotl/prompt_strategies/__init__.py b/src/axolotl/prompt_strategies/__init__.py index ba0dad0533..3cdbbb6f33 100644 --- a/src/axolotl/prompt_strategies/__init__.py +++ b/src/axolotl/prompt_strategies/__init__.py @@ -2,11 +2,11 @@ import importlib import inspect -import logging from axolotl.prompt_strategies.user_defined import UserDefinedDatasetConfig +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl.prompt_strategies") +LOG = get_logger(__name__) def load(strategy, tokenizer, cfg, ds_cfg, processor=None): diff --git a/src/axolotl/prompt_strategies/base.py b/src/axolotl/prompt_strategies/base.py index c146133fbd..370a51a95a 100644 --- a/src/axolotl/prompt_strategies/base.py +++ b/src/axolotl/prompt_strategies/base.py @@ -3,9 +3,10 @@ """ import importlib -import logging -LOG = logging.getLogger("axolotl") +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) def load(strategy, cfg, module_base=None, **kwargs): diff --git a/src/axolotl/prompt_strategies/bradley_terry/__init__.py b/src/axolotl/prompt_strategies/bradley_terry/__init__.py index 4457c50be5..7530aee192 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/__init__.py +++ b/src/axolotl/prompt_strategies/bradley_terry/__init__.py @@ -2,11 +2,11 @@ import importlib import inspect -import logging from axolotl.prompt_strategies.user_defined import UserDefinedDatasetConfig +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl.prompt_strategies.bradley_terry") +LOG = get_logger(__name__) def load(strategy, tokenizer, cfg, ds_cfg): diff --git a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py index 67319f5b41..e655f85a1f 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py +++ b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py @@ -2,7 +2,6 @@ Bradley-Terry model with chat template prompt strategy. """ -import logging from typing import Any, Dict, Optional from axolotl.prompt_strategies.chat_template import ( @@ -10,10 +9,11 @@ ChatTemplateStrategy, ) from axolotl.utils.chat_templates import get_chat_template_from_config +from axolotl.utils.logging import get_logger # Configure the logger -LOG = logging.getLogger("axolotl.prompt_strategies.bradley_terry.chat_template") -LOG.setLevel(logging.INFO) +LOG = get_logger(__name__) +LOG.setLevel("INFO") class BTChatTemplateStrategy(ChatTemplateStrategy): @@ -44,7 +44,7 @@ def _tokenize_single_prompt(self, prompt): if len(chosen_tokenized["input_ids"]) > max_length: LOG.warning( - f"To-be-trimmed chosen sequence exceeds max sequence length: {len(chosen_tokenized['input_ids'])}", + f"To-be-trimmed chosen sequence exceeds max sequence length: {len(chosen_tokenized['input_ids'])}" ) chosen_tokenized["input_ids"] = chosen_tokenized["input_ids"][:max_length] @@ -62,7 +62,7 @@ def _tokenize_single_prompt(self, prompt): if len(rejected_tokenized["input_ids"]) > max_length: LOG.warning( - f"To-be-trimmed rejected sequence exceeds max sequence length: {len(rejected_tokenized['input_ids'])}", + f"To-be-trimmed rejected sequence exceeds max sequence length: {len(rejected_tokenized['input_ids'])}" ) rejected_tokenized["input_ids"] = rejected_tokenized["input_ids"][ diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 047a66e947..ebb151876c 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -2,7 +2,6 @@ HF Chat Templates prompt strategy """ -import logging from collections import defaultdict from typing import Any, Dict, List, Set, Union @@ -13,11 +12,12 @@ from axolotl.prompt_tokenizers import PromptTokenizingStrategy from axolotl.prompters import IGNORE_TOKEN_ID, Prompter from axolotl.utils.chat_templates import get_chat_template_from_config +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.datasets import DatasetConfig # Configure the logger -LOG = logging.getLogger("axolotl") -LOG.setLevel(logging.INFO) +LOG = get_logger(__name__) +LOG.setLevel("INFO") class ChatTemplatePrompter(Prompter): @@ -378,7 +378,9 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: add_generation_prompt=True, images=images, ) - tokenized_res = self.prompter.build_prompt(turns, images=images) # type: ignore + tokenized_res = self.prompter.build_prompt( + turns, images=images + ) # type: ignore tokenized_prompt = {} if isinstance(tokenized_res, list): input_ids = prompt_ids + tokenized_res[len(prompt_ids) :] @@ -555,8 +557,8 @@ def find_turn(self, turns: list[dict], turn_idx: int): and turns[0].get("role") == "system" and ( "mistral" in self.tokenizer.name_or_path.lower() - # gemma3 uses gemma tokenizer - or "gemma" in self.tokenizer.name_or_path.lower() + or "gemma" + in self.tokenizer.name_or_path.lower() # gemma3 uses gemma tokenizer ) ): return -1, -1 diff --git a/src/axolotl/prompt_strategies/llama2_chat.py b/src/axolotl/prompt_strategies/llama2_chat.py index 29e091bfd0..eef2e1d4d3 100644 --- a/src/axolotl/prompt_strategies/llama2_chat.py +++ b/src/axolotl/prompt_strategies/llama2_chat.py @@ -24,12 +24,14 @@ Important: Don't use "special_tokens:" in your config.yml if you are not sure what you are doing! """ -import logging from dataclasses import dataclass, field from typing import Generator, List, Sequence from axolotl.prompt_tokenizers import PromptTokenizingStrategy from axolotl.prompters import ALTERNATING_ASSERTION_FAILED_ROLE, IGNORE_TOKEN_ID +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) @dataclass @@ -129,7 +131,7 @@ def tokenize_prompt(self, prompt): if cur_len < self.sequence_len: if cur_len != total_len: target[:] = IGNORE_TOKEN_ID - logging.warning( + LOG.warning( f"WARNING: tokenization mismatch: {cur_len} vs. {total_len}." f" (ignored)" ) diff --git a/src/axolotl/prompt_strategies/messages/__init__.py b/src/axolotl/prompt_strategies/messages/__init__.py index d014d93a6b..cc7b84da18 100644 --- a/src/axolotl/prompt_strategies/messages/__init__.py +++ b/src/axolotl/prompt_strategies/messages/__init__.py @@ -2,9 +2,10 @@ import importlib import inspect -import logging -LOG = logging.getLogger("axolotl.prompt_strategies.messages") +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) def load(tokenizer, cfg, ds_cfg, processor=None): diff --git a/src/axolotl/prompt_strategies/metharme.py b/src/axolotl/prompt_strategies/metharme.py index 52d77c00cf..66da723893 100644 --- a/src/axolotl/prompt_strategies/metharme.py +++ b/src/axolotl/prompt_strategies/metharme.py @@ -1,12 +1,12 @@ """Module containing the MetharmenPromptTokenizingStrategy and MetharmePrompter class""" -import logging from typing import Tuple from axolotl.prompt_tokenizers import InstructionPromptTokenizingStrategy from axolotl.prompters import AlpacaPrompter +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__) IGNORE_TOKEN_ID = -100 diff --git a/src/axolotl/prompt_strategies/pygmalion.py b/src/axolotl/prompt_strategies/pygmalion.py index 88208f6ec4..51f92f3970 100644 --- a/src/axolotl/prompt_strategies/pygmalion.py +++ b/src/axolotl/prompt_strategies/pygmalion.py @@ -1,7 +1,6 @@ """Module containing the PygmalionPromptTokenizingStrategy and PygmalionPrompter class""" import copy -import logging from collections import defaultdict from typing import Generator, List, Tuple @@ -10,8 +9,9 @@ parse_tokenized_to_result, tokenize_prompt_default, ) +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__) IGNORE_TOKEN_ID = -100 diff --git a/src/axolotl/prompt_tokenizers.py b/src/axolotl/prompt_tokenizers.py index c29fd05a4c..cb1a1ba4ed 100644 --- a/src/axolotl/prompt_tokenizers.py +++ b/src/axolotl/prompt_tokenizers.py @@ -1,14 +1,14 @@ """Module containing PromptTokenizingStrategy and Prompter classes""" import abc -import logging from typing import Callable, Dict, List, Optional, Tuple, Union from transformers import BatchEncoding, PreTrainedTokenizer from axolotl.prompters import Prompter +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__) IGNORE_INDEX = -100 LLAMA_DEFAULT_PAD_TOKEN = "" # nosec diff --git a/src/axolotl/prompters.py b/src/axolotl/prompters.py index ec680702dc..d29da075e0 100644 --- a/src/axolotl/prompters.py +++ b/src/axolotl/prompters.py @@ -1,12 +1,13 @@ """Module containing prompters""" -import logging from enum import Enum from typing import Generator, Optional, Union from colorama import Fore -LOG = logging.getLogger("axolotl") +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) IGNORE_TOKEN_ID = -100 REPR_TEMPLATE = "\n\n" + Fore.CYAN + "{full_prompt}" + Fore.RESET + "\n\n" diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 8a4c0040d9..68ba3a124e 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -2,7 +2,6 @@ import importlib import inspect -import logging import os import signal import sys @@ -37,6 +36,7 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import cleanup_distributed from axolotl.utils.freeze import freeze_layers_except +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import RLType from axolotl.utils.trainer import setup_trainer @@ -45,7 +45,7 @@ except ImportError: BetterTransformer = None -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def setup_model_and_tokenizer( @@ -64,9 +64,7 @@ def setup_model_and_tokenizer( `None`), and processor (if multimodal, else `None`). """ # Load tokenizer - LOG.debug( - f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}", - ) + LOG.debug(f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}") tokenizer = load_tokenizer(cfg) # Load processor for multimodal models if needed diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 0e7b060939..d94f4be74d 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -4,7 +4,6 @@ import gc import json -import logging import os import traceback from shutil import copyfile @@ -43,6 +42,7 @@ is_main_process, zero_first, ) +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.config import AxolotlInputConfig if TYPE_CHECKING: @@ -50,7 +50,7 @@ IGNORE_INDEX = -100 -LOG = logging.getLogger("axolotl.callbacks") +LOG = get_logger(__name__) class EvalFirstStepCallback( @@ -753,7 +753,14 @@ def log_table_from_dataloader(name: str, table_dataloader): ].append(pred_step_text) row_index += 1 if logger == "wandb": - wandb.run.log({f"{name} - Predictions vs Ground Truth": pd.DataFrame(table_data)}) # type: ignore[attr-defined] + # type: ignore[attr-defined] + wandb.run.log( + { + f"{name} - Predictions vs Ground Truth": pd.DataFrame( + table_data + ) + } + ) elif logger == "mlflow" and is_mlflow_available(): import mlflow diff --git a/src/axolotl/utils/callbacks/comet_.py b/src/axolotl/utils/callbacks/comet_.py index b29f997a86..b7e9034b0e 100644 --- a/src/axolotl/utils/callbacks/comet_.py +++ b/src/axolotl/utils/callbacks/comet_.py @@ -1,17 +1,17 @@ """Comet module for trainer callbacks""" -import logging from typing import TYPE_CHECKING import comet_ml from transformers import TrainerCallback, TrainerControl, TrainerState from axolotl.utils.distributed import is_main_process +from axolotl.utils.logging import get_logger if TYPE_CHECKING: from axolotl.core.trainer_builder import AxolotlTrainingArguments -LOG = logging.getLogger("axolotl.callbacks") +LOG = get_logger(__name__) class SaveAxolotlConfigtoCometCallback(TrainerCallback): diff --git a/src/axolotl/utils/callbacks/lisa.py b/src/axolotl/utils/callbacks/lisa.py index e226471b1a..ad7e23144a 100644 --- a/src/axolotl/utils/callbacks/lisa.py +++ b/src/axolotl/utils/callbacks/lisa.py @@ -6,17 +6,18 @@ License: Apache 2.0 """ -import logging from functools import reduce from typing import TYPE_CHECKING import numpy as np from transformers import TrainerCallback +from axolotl.utils.logging import get_logger + if TYPE_CHECKING: from axolotl.core.trainer_builder import AxolotlTrainer -LOG = logging.getLogger("axolotl.callbacks.lisa") +LOG = get_logger(__name__) def lisa_callback_factory(trainer: "AxolotlTrainer"): diff --git a/src/axolotl/utils/callbacks/mlflow_.py b/src/axolotl/utils/callbacks/mlflow_.py index 15ca1ca475..15f8ef0697 100644 --- a/src/axolotl/utils/callbacks/mlflow_.py +++ b/src/axolotl/utils/callbacks/mlflow_.py @@ -1,6 +1,5 @@ """MLFlow module for trainer callbacks""" -import logging import os from shutil import copyfile from tempfile import NamedTemporaryFile @@ -10,11 +9,12 @@ from transformers import TrainerCallback, TrainerControl, TrainerState from axolotl.utils.distributed import is_main_process +from axolotl.utils.logging import get_logger if TYPE_CHECKING: from axolotl.core.trainer_builder import AxolotlTrainingArguments -LOG = logging.getLogger("axolotl.callbacks") +LOG = get_logger(__name__) def should_log_artifacts() -> bool: diff --git a/src/axolotl/utils/callbacks/qat.py b/src/axolotl/utils/callbacks/qat.py index da4f2612be..cf4d9a9373 100644 --- a/src/axolotl/utils/callbacks/qat.py +++ b/src/axolotl/utils/callbacks/qat.py @@ -1,6 +1,5 @@ """QAT Callback for HF Causal Trainer""" -import logging from functools import partial from torch import nn @@ -8,9 +7,10 @@ from torchao.quantization.qat.linear import FakeQuantizedLinear from transformers import TrainerCallback +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.quantization import QATConfig -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def toggle_fake_quant(mod: nn.Module, enable: bool): diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 72ebffbcdb..bf496d2c51 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -3,13 +3,14 @@ These templates are used for formatting messages in a conversation. """ -import logging from typing import TYPE_CHECKING, Any, Dict, Optional +from axolotl.utils.logging import get_logger + if TYPE_CHECKING: from transformers import PreTrainedTokenizerBase -LOG = logging.getLogger("axolotl.utils.chat_templates") +LOG = get_logger("axolotl.utils.chat_templates") _JINJA_TEMPALTE_CHOICE = "jinja" _DEFAULT_TEMPLATE_CHOICE = "tokenizer_default" @@ -40,9 +41,9 @@ "metharme": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% else %}{% set loop_messages = messages %}{% set system_message = 'Enter RP mode. You shall reply to the user while staying in character. Your responses must be detailed, creative, immersive, and drive the scenario forward.' %}{% endif %}{{ '<|system|>' + system_message }}{% for message in loop_messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|user|>' + content.strip() }}{% elif message['role'] == 'assistant' %}{{ '<|model|>' + content.strip() }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|model|>' }}{% else %}{{ eos_token }}{% endif %}", "pixtral": '{%- if messages[0]["role"] == "system" %}\n {%- set system_message = messages[0]["content"] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set loop_messages = messages %}\n{%- endif %}\n\n{{- bos_token }}\n{%- for message in loop_messages %}\n {%- if (message[\'role\'] == \'user\') != (loop.index0 % 2 == 0) %}\n {{- raise_exception(\'After the optional system message, conversation roles must alternate user/assistant/user/assistant/...\') }}\n {%- endif %}\n {%- if message["role"] == "user" %}\n {%- if loop.last and system_message is defined %}\n {{- "[INST]" + system_message + "\n\n" }}\n {%- else %}\n {{- "[INST]" }}\n {%- endif %}\n {%- if message["content"] is not string %}\n {%- for chunk in message["content"] %}\n {%- if chunk["type"] == "text" %}\n {{- chunk["text"] }}\n {%- elif chunk["type"] == "image" %}\n {{- "[IMG]" }}\n {%- else %}\n {{- raise_exception("Unrecognized content type!") }}\n {%- endif %}\n {%- endfor %}\n {%- else %}\n {{- message["content"] }}\n {%- endif %}\n {{- "[/INST]" }}\n {%- elif message["role"] == "assistant" %}\n {%- if message["content"] is not string %}\n {%- for chunk in message["content"] %}\n {%- if chunk["type"] == "text" %}\n {{- chunk["text"] }}\n {%- elif chunk["type"] == "image" %}\n {{- "[IMG]" }}\n {%- else %}\n {{- raise_exception("Unrecognized content type!") }}\n{%- endif %}\n{%- endfor %}\n{{- eos_token }}\n{%- else %}\n{{- message["content"] + eos_token }}\n{%- endif %}\n {%- else %}\n {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }}\n {%- endif %}\n{%- endfor %}', "qwen2_vl": "{% set image_count = namespace(value=0) %}{% set video_count = namespace(value=0) %}{% for message in messages %}{% if loop.first and message['role'] != 'system' %}<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n{% endif %}<|im_start|>{{ message['role'] }}\n{% if message['content'] is string %}{{ message['content'] }}<|im_end|>\n{% else %}{% for content in message['content'] %}{% if content['type'] == 'image' or 'image' in content or 'image_url' in content %}{% set image_count.value = image_count.value + 1 %}{% if add_vision_id %}Picture {{ image_count.value }}: {% endif %}<|vision_start|><|image_pad|><|vision_end|>{% elif content['type'] == 'video' or 'video' in content %}{% set video_count.value = video_count.value + 1 %}{% if add_vision_id %}Video {{ video_count.value }}: {% endif %}<|vision_start|><|video_pad|><|vision_end|>{% elif 'text' in content %}{{ content['text'] }}{% endif %}{% endfor %}<|im_end|>\n{% endif %}{% endfor %}{% if add_generation_prompt %}<|im_start|>assistant\n{% endif %}", - "command_a": "{{ bos_token }}{% if documents %}\n{% set tools = [] %}\n{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {\"tool_call_id\": \"0\", \"tool_name\": \"direct-injected-document\", \"parameters\": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n \"tool_call_id\": \"0\",\n \"results\": {\n{% for doc in documents %}\n \"{{ loop.index0 }}\": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n \"is_error\": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n \"tool_call_id\": \"{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}\",\n \"results\": {\n \"0\": {{ tool_msg.content|tojson }}\n },\n \"is_error\": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0]['role']|lower == 'system' %}{%- set developer_preamble = messages[0]['content'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing \"tool_name\" and \"parameters\" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its \"tool_call_id\".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that \"Reflection\" and \"Response\" above can be grounded.\nGrounding means you associate pieces of texts (called \"spans\") with those specific tool results that support them (called \"sources\"). And you use a pair of tags \"\" and \"\" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as \"{tool_call_id}:[{list of result indices}]\", before they are joined together by \",\". E.g., \"span\" means that \"span\" is supported by result 1 and 2 from \"tool_call_id=0\" as well as result 0 from \"tool_call_id=1\".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like \"name\", \"description\", \"parameters\" (per JSON Schema), and optionally, \"responses\" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {\"name\": \"direct-injected-document\", \"description\": \"This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!\", \"parameters\": {\"type\": \"object\", \"properties\": {}, \"required\": []}, \"responses\": {\"200\": {\"description\": \"Successfully returned a list of chunked text snippets from the directly uploaded documents.\", \"content\": {\"application/json\": {\"schema\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"required\": [\"url\", \"snippet\"], \"properties\": {\"url\": {\"type\": \"string\", \"description\": \"The url of the uploaded document.\"}, \"snippet\": {\"type\": \"string\", \"description\": \"The text snippet for the returned document chunk.\"}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {\"name\": \"{{ tool['function']['name'] }}\", \"description\": \"{{tool['function']['description']}}\", \"parameters\": {{ tool['function']['parameters']|tojson }}, \"responses\": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == 'system' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == 'user' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {\"tool_call_id\": \"{{ tool_idx.value }}\", \"tool_name\": \"{{ tc['function']['name'] }}\", \"parameters\": {{ tc['function']['arguments']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == 'tool' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == 'tool' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>\n{%- else -%}\n{%- if messages and messages[0]['role']|lower == 'system' %}{%- set developer_preamble = messages[0]['content'] %}{% endif %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\n{% if safety_mode|upper == 'STRICT' -%}\nYou are in strict safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will reject requests to generate content related to violence, hate, misinformation or sex to any amount. You will avoid using profanity. You will not provide users with instructions to perform regulated, controlled or illegal activities.\n{%- else -%}\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n{%- endif %}\n\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == 'system' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == 'user' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{%- if add_generation_prompt -%}<|START_RESPONSE|>{%- endif %}\n{% endif %}", - "command_a_tool_use": "{{ bos_token }}{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {\"tool_call_id\": \"0\", \"tool_name\": \"direct-injected-document\", \"parameters\": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n \"tool_call_id\": \"0\",\n \"results\": {\n{% for doc in documents %}\n \"{{ loop.index0 }}\": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n \"is_error\": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n \"tool_call_id\": \"{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}\",\n \"results\": {\n \"0\": {{ tool_msg.content|tojson }}\n },\n \"is_error\": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0]['role']|lower == 'system' %}{%- set developer_preamble = messages[0]['content'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing \"tool_name\" and \"parameters\" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its \"tool_call_id\".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that \"Reflection\" and \"Response\" above can be grounded.\nGrounding means you associate pieces of texts (called \"spans\") with those specific tool results that support them (called \"sources\"). And you use a pair of tags \"\" and \"\" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as \"{tool_call_id}:[{list of result indices}]\", before they are joined together by \",\". E.g., \"span\" means that \"span\" is supported by result 1 and 2 from \"tool_call_id=0\" as well as result 0 from \"tool_call_id=1\".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like \"name\", \"description\", \"parameters\" (per JSON Schema), and optionally, \"responses\" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {\"name\": \"direct-injected-document\", \"description\": \"This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!\", \"parameters\": {\"type\": \"object\", \"properties\": {}, \"required\": []}, \"responses\": {\"200\": {\"description\": \"Successfully returned a list of chunked text snippets from the directly uploaded documents.\", \"content\": {\"application/json\": {\"schema\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"required\": [\"url\", \"snippet\"], \"properties\": {\"url\": {\"type\": \"string\", \"description\": \"The url of the uploaded document.\"}, \"snippet\": {\"type\": \"string\", \"description\": \"The text snippet for the returned document chunk.\"}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {\"name\": \"{{ tool['function']['name'] }}\", \"description\": \"{{tool['function']['description']}}\", \"parameters\": {{ tool['function']['parameters']|tojson }}, \"responses\": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == 'system' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == 'user' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {\"tool_call_id\": \"{{ tool_idx.value }}\", \"tool_name\": \"{{ tc['function']['name'] }}\", \"parameters\": {{ tc['function']['arguments']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == 'tool' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == 'tool' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>", - "command_a_rag": "{{ bos_token }}{% set tools = [] %}\n{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {\"tool_call_id\": \"0\", \"tool_name\": \"direct-injected-document\", \"parameters\": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n \"tool_call_id\": \"0\",\n \"results\": {\n{% for doc in documents %}\n \"{{ loop.index0 }}\": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n \"is_error\": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n \"tool_call_id\": \"{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}\",\n \"results\": {\n \"0\": {{ tool_msg.content|tojson }}\n },\n \"is_error\": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0]['role']|lower == 'system' %}{%- set developer_preamble = messages[0]['content'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing \"tool_name\" and \"parameters\" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its \"tool_call_id\".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that \"Reflection\" and \"Response\" above can be grounded.\nGrounding means you associate pieces of texts (called \"spans\") with those specific tool results that support them (called \"sources\"). And you use a pair of tags \"\" and \"\" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as \"{tool_call_id}:[{list of result indices}]\", before they are joined together by \",\". E.g., \"span\" means that \"span\" is supported by result 1 and 2 from \"tool_call_id=0\" as well as result 0 from \"tool_call_id=1\".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like \"name\", \"description\", \"parameters\" (per JSON Schema), and optionally, \"responses\" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {\"name\": \"direct-injected-document\", \"description\": \"This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!\", \"parameters\": {\"type\": \"object\", \"properties\": {}, \"required\": []}, \"responses\": {\"200\": {\"description\": \"Successfully returned a list of chunked text snippets from the directly uploaded documents.\", \"content\": {\"application/json\": {\"schema\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"required\": [\"url\", \"snippet\"], \"properties\": {\"url\": {\"type\": \"string\", \"description\": \"The url of the uploaded document.\"}, \"snippet\": {\"type\": \"string\", \"description\": \"The text snippet for the returned document chunk.\"}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {\"name\": \"{{ tool['function']['name'] }}\", \"description\": \"{{tool['function']['description']}}\", \"parameters\": {{ tool['function']['parameters']|tojson }}, \"responses\": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == 'system' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == 'user' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {\"tool_call_id\": \"{{ tool_idx.value }}\", \"tool_name\": \"{{ tc['function']['name'] }}\", \"parameters\": {{ tc['function']['arguments']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == 'tool' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == 'tool' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>", + "command_a": '{{ bos_token }}{% if documents %}\n{% set tools = [] %}\n{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {"tool_call_id": "0", "tool_name": "direct-injected-document", "parameters": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n "tool_call_id": "0",\n "results": {\n{% for doc in documents %}\n "{{ loop.index0 }}": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n "is_error": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n "tool_call_id": "{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}",\n "results": {\n "0": {{ tool_msg.content|tojson }}\n },\n "is_error": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0][\'role\']|lower == \'system\' %}{%- set developer_preamble = messages[0][\'content\'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user\'s requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user\'s request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it\'ll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you\'ve figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it\'s time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user\'s last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that "Reflection" and "Response" above can be grounded.\nGrounding means you associate pieces of texts (called "spans") with those specific tool results that support them (called "sources"). And you use a pair of tags "" and "" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as "{tool_call_id}:[{list of result indices}]", before they are joined together by ",". E.g., "span" means that "span" is supported by result 1 and 2 from "tool_call_id=0" as well as result 0 from "tool_call_id=1".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {"name": "direct-injected-document", "description": "This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!", "parameters": {"type": "object", "properties": {}, "required": []}, "responses": {"200": {"description": "Successfully returned a list of chunked text snippets from the directly uploaded documents.", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "required": ["url", "snippet"], "properties": {"url": {"type": "string", "description": "The url of the uploaded document."}, "snippet": {"type": "string", "description": "The text snippet for the returned document chunk."}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {"name": "{{ tool[\'function\'][\'name\'] }}", "description": "{{tool[\'function\'][\'description\']}}", "parameters": {{ tool[\'function\'][\'parameters\']|tojson }}, "responses": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == \'system\' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == \'user\' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == \'assistant\' or message.role|lower == \'chatbot\' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {"tool_call_id": "{{ tool_idx.value }}", "tool_name": "{{ tc[\'function\'][\'name\'] }}", "parameters": {{ tc[\'function\'][\'arguments\']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == \'tool\' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == \'tool\' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>\n{%- else -%}\n{%- if messages and messages[0][\'role\']|lower == \'system\' %}{%- set developer_preamble = messages[0][\'content\'] %}{% endif %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\n{% if safety_mode|upper == \'STRICT\' -%}\nYou are in strict safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will reject requests to generate content related to violence, hate, misinformation or sex to any amount. You will avoid using profanity. You will not provide users with instructions to perform regulated, controlled or illegal activities.\n{%- else -%}\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n{%- endif %}\n\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == \'system\' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == \'user\' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == \'assistant\' or message.role|lower == \'chatbot\' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{%- if add_generation_prompt -%}<|START_RESPONSE|>{%- endif %}\n{% endif %}', + "command_a_tool_use": '{{ bos_token }}{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {"tool_call_id": "0", "tool_name": "direct-injected-document", "parameters": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n "tool_call_id": "0",\n "results": {\n{% for doc in documents %}\n "{{ loop.index0 }}": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n "is_error": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n "tool_call_id": "{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}",\n "results": {\n "0": {{ tool_msg.content|tojson }}\n },\n "is_error": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0][\'role\']|lower == \'system\' %}{%- set developer_preamble = messages[0][\'content\'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user\'s requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user\'s request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it\'ll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you\'ve figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it\'s time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user\'s last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that "Reflection" and "Response" above can be grounded.\nGrounding means you associate pieces of texts (called "spans") with those specific tool results that support them (called "sources"). And you use a pair of tags "" and "" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as "{tool_call_id}:[{list of result indices}]", before they are joined together by ",". E.g., "span" means that "span" is supported by result 1 and 2 from "tool_call_id=0" as well as result 0 from "tool_call_id=1".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {"name": "direct-injected-document", "description": "This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!", "parameters": {"type": "object", "properties": {}, "required": []}, "responses": {"200": {"description": "Successfully returned a list of chunked text snippets from the directly uploaded documents.", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "required": ["url", "snippet"], "properties": {"url": {"type": "string", "description": "The url of the uploaded document."}, "snippet": {"type": "string", "description": "The text snippet for the returned document chunk."}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {"name": "{{ tool[\'function\'][\'name\'] }}", "description": "{{tool[\'function\'][\'description\']}}", "parameters": {{ tool[\'function\'][\'parameters\']|tojson }}, "responses": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == \'system\' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == \'user\' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == \'assistant\' or message.role|lower == \'chatbot\' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {"tool_call_id": "{{ tool_idx.value }}", "tool_name": "{{ tc[\'function\'][\'name\'] }}", "parameters": {{ tc[\'function\'][\'arguments\']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == \'tool\' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == \'tool\' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>', + "command_a_rag": '{{ bos_token }}{% set tools = [] %}\n{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {"tool_call_id": "0", "tool_name": "direct-injected-document", "parameters": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n "tool_call_id": "0",\n "results": {\n{% for doc in documents %}\n "{{ loop.index0 }}": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n "is_error": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n "tool_call_id": "{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}",\n "results": {\n "0": {{ tool_msg.content|tojson }}\n },\n "is_error": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0][\'role\']|lower == \'system\' %}{%- set developer_preamble = messages[0][\'content\'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user\'s requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user\'s request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it\'ll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you\'ve figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it\'s time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user\'s last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that "Reflection" and "Response" above can be grounded.\nGrounding means you associate pieces of texts (called "spans") with those specific tool results that support them (called "sources"). And you use a pair of tags "" and "" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as "{tool_call_id}:[{list of result indices}]", before they are joined together by ",". E.g., "span" means that "span" is supported by result 1 and 2 from "tool_call_id=0" as well as result 0 from "tool_call_id=1".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {"name": "direct-injected-document", "description": "This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!", "parameters": {"type": "object", "properties": {}, "required": []}, "responses": {"200": {"description": "Successfully returned a list of chunked text snippets from the directly uploaded documents.", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "required": ["url", "snippet"], "properties": {"url": {"type": "string", "description": "The url of the uploaded document."}, "snippet": {"type": "string", "description": "The text snippet for the returned document chunk."}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {"name": "{{ tool[\'function\'][\'name\'] }}", "description": "{{tool[\'function\'][\'description\']}}", "parameters": {{ tool[\'function\'][\'parameters\']|tojson }}, "responses": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == \'system\' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == \'user\' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == \'assistant\' or message.role|lower == \'chatbot\' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {"tool_call_id": "{{ tool_idx.value }}", "tool_name": "{{ tc[\'function\'][\'name\'] }}", "parameters": {{ tc[\'function\'][\'arguments\']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == \'tool\' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == \'tool\' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>', "aya": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Aya, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", } diff --git a/src/axolotl/utils/comet_.py b/src/axolotl/utils/comet_.py index b4ecc80ad9..9eeb6a2801 100644 --- a/src/axolotl/utils/comet_.py +++ b/src/axolotl/utils/comet_.py @@ -1,11 +1,11 @@ """Module for wandb utilities""" -import logging import os from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl.utils.comet_") +LOG = get_logger(__name__) COMET_ENV_MAPPING_OVERRIDE = { "comet_mode": "COMET_START_MODE", diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 49e4cfc6fb..e0eaf9ac92 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -1,7 +1,6 @@ """Module for working with config dicts""" import json -import logging import os from typing import Optional @@ -15,13 +14,14 @@ from axolotl.loaders.utils import load_model_config from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.config import ( AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, ) from axolotl.utils.schemas.config import AxolotlInputConfig as AxolotlInputConfigBase from axolotl.utils.schemas.datasets import DPODataset, KTODataset, SFTDataset -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__, use_environ=True) def choose_device(cfg): diff --git a/src/axolotl/utils/data/pretraining.py b/src/axolotl/utils/data/pretraining.py index f20ced221a..44d8d6fed0 100644 --- a/src/axolotl/utils/data/pretraining.py +++ b/src/axolotl/utils/data/pretraining.py @@ -1,7 +1,6 @@ """data handling specific to pretraining""" import functools -import logging from collections import defaultdict from typing import Callable, Dict, List, Optional @@ -11,10 +10,11 @@ from transformers import PreTrainedTokenizerBase from axolotl.utils.collators import PretrainingBatchSamplerDataCollatorForSeq2Seq +from axolotl.utils.logging import get_logger from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths from axolotl.utils.trainer import process_pretraining_datasets_for_packing -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__) def encode_pretraining( diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index 15744d4c60..eeea6f2072 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -1,7 +1,6 @@ """data handling specific to DPO""" import inspect -import logging from functools import partial from pathlib import Path from typing import Any, List, Union @@ -18,9 +17,10 @@ from axolotl.utils.data.utils import deduplicate_and_log_datasets, md5 from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import is_main_process, zero_first +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import RLType -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def _get_path(ds_hash, cfg): @@ -217,7 +217,7 @@ def load_split(dataset_cfgs, _cfg): + "|" + "train" + "|" - + str(seed) + + str(cfg.seed or 42) ) to_hash_test = ( train_dataset._fingerprint # pylint: disable=protected-access @@ -226,7 +226,7 @@ def load_split(dataset_cfgs, _cfg): + "|" + "test" + "|" - + str(seed) + + str(cfg.seed or 42) ) train_fingerprint = md5(to_hash_train) test_fingerprint = md5(to_hash_test) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 6de2d2cf74..88c78174bc 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -1,7 +1,6 @@ """data handling specific to SFT""" import functools -import logging import os import tempfile from pathlib import Path @@ -54,12 +53,13 @@ ) from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import is_local_main_process, zero_first +from axolotl.utils.logging import get_logger from axolotl.utils.trainer import ( calculate_total_num_steps, process_datasets_for_packing, ) -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) @retry_on_request_exceptions(max_retries=3, delay=5) @@ -182,10 +182,9 @@ def prepare_dataset(cfg, tokenizer, processor=None, preprocess_iterable=None): total_num_steps = min( calculate_total_num_steps(cfg, train_dataset), cfg.max_steps ) - LOG.info(f"Maximum number of steps set at {total_num_steps}") else: total_num_steps = calculate_total_num_steps(cfg, train_dataset) - + LOG.info(f"Maximum number of steps set at {total_num_steps}") return train_dataset, eval_dataset, total_num_steps, prompters @@ -331,12 +330,12 @@ def load_tokenized_prepared_datasets( if len(datasets) == 1: dataset = datasets[0] else: - LOG.info("merging datasets") + LOG.info("Merging datasets...") dataset = concatenate_datasets(datasets) if len(datasets) > 1: if cfg.shuffle_merged_datasets: - LOG.debug("shuffle merged datasets") + LOG.debug("Shuffling merged datasets...") dataset = dataset.shuffle(seed=seed) else: LOG.debug("NOT shuffling merged datasets") @@ -426,7 +425,7 @@ def load_prepare_datasets( + "|" + "train" + "|" - + str(seed) + + str(cfg.seed or 42) ) to_hash_test = ( dataset._fingerprint # pylint: disable=protected-access @@ -435,7 +434,7 @@ def load_prepare_datasets( + "|" + "test" + "|" - + str(seed) + + str(cfg.seed or 42) ) train_fingerprint = md5(to_hash_train) test_fingerprint = md5(to_hash_test) diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index a8e19582e7..5f3b8d3cc6 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -2,7 +2,6 @@ import functools import hashlib -import logging import time from enum import Enum @@ -12,10 +11,11 @@ from datasets import Dataset, IterableDataset from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from axolotl.utils.samplers.utils import get_dataset_lengths from axolotl.utils.trainer import drop_long_seq -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) class RetryStrategy(Enum): diff --git a/src/axolotl/utils/logging.py b/src/axolotl/utils/logging.py new file mode 100644 index 0000000000..80daab4eaa --- /dev/null +++ b/src/axolotl/utils/logging.py @@ -0,0 +1,62 @@ +""" +logging helpers to only log on main process +""" + +import functools +import logging +import os + +from axolotl.utils.distributed import is_main_process + +# Adapted from Accelerate +# https://github.com/huggingface/accelerate/blob/main/src/accelerate/logging.py + + +class MultiProcessAdapter(logging.LoggerAdapter): + """ + logger adapter for distributed logging, specifically to only log on main process + """ + + def __init__(self, logger, use_environ=False, extra=None): + super().__init__(logger, extra) + self.use_environ = use_environ + + @staticmethod + def _should_log(main_process_only, use_environ=False): + return not main_process_only or ( + main_process_only and is_main_process(use_environ=use_environ) + ) + + def log(self, level, msg, *args, **kwargs): + use_environ = kwargs.pop("use_environ", self.use_environ) + main_process_only = kwargs.pop("main_process_only", True) + kwargs.setdefault("stacklevel", 2) + + if self.isEnabledFor(level) and self._should_log( + main_process_only, use_environ=use_environ + ): + msg, kwargs = self.process(msg, kwargs) + self.logger.log(level, msg, *args, **kwargs) + + @functools.lru_cache(maxsize=10) + def warning_once(self, *args, **kwargs): + """ + This method is identical to `logger.warning()`, but will emit the warning with the same message only once + + Note: The cache is for the function arguments, so 2 different callers using the same arguments will hit the + cache. The assumption here is that all warning messages are unique across the code. If they aren't then need to + switch to another type of cache that includes the caller frame information in the hashing function. + """ + self.warning(*args, **kwargs) + + +def get_logger( + name: str, log_level: str | None = None, use_environ: bool = False +) -> MultiProcessAdapter: + if log_level is None: + log_level = os.environ.get("AXOLOTL_LOG_LEVEL", None) + logger = logging.getLogger(name) + if log_level is not None: + logger.setLevel(log_level.upper()) + logger.root.setLevel(log_level.upper()) + return MultiProcessAdapter(logger, use_environ=use_environ, extra={}) diff --git a/src/axolotl/utils/quantization.py b/src/axolotl/utils/quantization.py index 612b1d44e6..f9a30b660e 100644 --- a/src/axolotl/utils/quantization.py +++ b/src/axolotl/utils/quantization.py @@ -2,8 +2,6 @@ Utilities for quantization including QAT and PTQ using torchao. """ -import logging - import torch from torch import nn from torchao.core.config import AOBaseConfig @@ -25,8 +23,6 @@ from axolotl.utils.schemas.enums import TorchIntDType -LOG = logging.getLogger(__name__) - def get_ptq_config( weight_dtype: TorchIntDType, diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 1bfa2ec6e0..e488ed7d5c 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -3,7 +3,6 @@ into fixed-capacity batches to optimize memory usage and training throughput. """ -import logging import math from concurrent.futures import ProcessPoolExecutor from multiprocessing import cpu_count, get_context @@ -14,9 +13,9 @@ from torch.utils.data import BatchSampler, Sampler, SequentialSampler from axolotl.utils.distributed import reduce_and_broadcast +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) -LOG.setLevel(logging.INFO) +LOG = get_logger(__name__) @numba.njit diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 8a4d6d63fa..75551085b6 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -2,7 +2,6 @@ # pylint: disable=too-many-lines -import logging import os from typing import Annotated, Any, Literal @@ -18,6 +17,7 @@ ) from transformers.utils.import_utils import is_torch_npu_available +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.datasets import ( DatasetConfig, DPODataset, @@ -49,7 +49,7 @@ from axolotl.utils.schemas.trl import TRLConfig from axolotl.utils.schemas.vllm import VllmConfig -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__, use_environ=True) SUPPORTED_METRICS = {"sacrebleu", "comet", "ter", "chrf", "perplexity"} diff --git a/src/axolotl/utils/schemas/deprecated.py b/src/axolotl/utils/schemas/deprecated.py index d42d6ff9eb..b8904136e4 100644 --- a/src/axolotl/utils/schemas/deprecated.py +++ b/src/axolotl/utils/schemas/deprecated.py @@ -1,11 +1,12 @@ """Pydantic models for deprecated and remapped configuration parameters""" -import logging from typing import Any from pydantic import BaseModel, Field, field_validator -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) class DeprecatedParameters(BaseModel): diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 91fdce1614..d09ab63877 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -64,6 +64,7 @@ class ChatTemplate(str, Enum): command_a_rag = "command_a_rag" # pylint: disable=invalid-name aya = "aya" # pylint: disable=invalid-name + class CustomSupportedOptimizers(str, Enum): """Custom supported optimizers""" diff --git a/src/axolotl/utils/schemas/integrations.py b/src/axolotl/utils/schemas/integrations.py index 9d8f9c190c..4843e3592d 100644 --- a/src/axolotl/utils/schemas/integrations.py +++ b/src/axolotl/utils/schemas/integrations.py @@ -1,11 +1,12 @@ """Pydantic models for Axolotl integrations""" -import logging from typing import Any from pydantic import BaseModel, Field, model_validator -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) class MLFlowConfig(BaseModel): diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index 5f1d26e84c..57f5ae309c 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -1,10 +1,10 @@ """Pydantic models for model input / output, etc. configuration""" -import logging - from pydantic import BaseModel, Field, field_validator -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__, use_environ=True) class ModelInputConfig(BaseModel): diff --git a/src/axolotl/utils/schemas/training.py b/src/axolotl/utils/schemas/training.py index 69547c17f3..ad7f899aca 100644 --- a/src/axolotl/utils/schemas/training.py +++ b/src/axolotl/utils/schemas/training.py @@ -1,15 +1,15 @@ """Pydantic models for training hyperparameters""" -import logging from typing import Any, Literal from pydantic import BaseModel, Field, field_validator from transformers import SchedulerType from transformers.training_args import OptimizerNames +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import CustomSupportedOptimizers -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) class LrGroup(BaseModel): diff --git a/src/axolotl/utils/schemas/utils.py b/src/axolotl/utils/schemas/utils.py index bf74390f67..b46c8f8475 100644 --- a/src/axolotl/utils/schemas/utils.py +++ b/src/axolotl/utils/schemas/utils.py @@ -1,8 +1,8 @@ """Utilities for Axolotl Pydantic models""" -import logging +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def handle_legacy_message_fields_logic(data: dict) -> dict: diff --git a/src/axolotl/utils/tokenization.py b/src/axolotl/utils/tokenization.py index e0b21a9f02..3526bd5b58 100644 --- a/src/axolotl/utils/tokenization.py +++ b/src/axolotl/utils/tokenization.py @@ -1,10 +1,10 @@ """Module for tokenization utilities""" -import logging - from termcolor import colored -LOG = logging.getLogger("axolotl") +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) def check_dataset_labels( diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 96f54b39d1..c08504d73c 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -22,7 +22,7 @@ from axolotl.utils.environment import check_cuda_p2p_ib_support from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths -LOG = get_logger("axolotl") +LOG = get_logger(__name__) @torch.jit.script @@ -402,7 +402,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): .apply(len) .values ) - LOG.debug(f"total_num_tokens: {total_num_tokens:_}", main_process_only=True) + LOG.debug(f"total_num_tokens: {total_num_tokens:_}") if update: cfg.total_num_tokens = total_num_tokens @@ -420,10 +420,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): .apply(lambda x: np.sum(np.array(x) != -100)) .sum() ) - LOG.debug( - f"`total_supervised_tokens: {total_supervised_tokens:_}`", - main_process_only=True, - ) + LOG.debug(f"`total_supervised_tokens: {total_supervised_tokens:_}`") if update: cfg.total_supervised_tokens = total_supervised_tokens @@ -448,8 +445,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): * cfg.sequence_parallel_degree ) LOG.debug( - f"total_num_tokens: {cfg.total_num_tokens:_}, total_num_steps: {total_num_steps:_}", - main_process_only=True, + f"total_num_tokens: {cfg.total_num_tokens:_}, total_num_steps: {total_num_steps:_}" ) else: if cfg.flash_attention and not cfg.multipack_real_batches: @@ -478,7 +474,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): batch_sampler=sampler, ) data_loader_len = len(data_loader) * cfg.micro_batch_size // cfg.batch_size - LOG.debug(f"data_loader_len: {data_loader_len}", main_process_only=True) + LOG.debug(f"data_loader_len: {data_loader_len}") # FIXME: is there a bug here somewhere? the total num steps depends # on the agreed on value for sample_packing_eff_est total_num_steps = int( @@ -500,10 +496,7 @@ def calc_sample_packing_eff_est(estimates: List[float]): ) if update: cfg.sample_packing_eff_est = sample_packing_eff_est - LOG.debug( - f"sample_packing_eff_est: {cfg.sample_packing_eff_est}", - main_process_only=True, - ) + LOG.debug(f"sample_packing_eff_est: {cfg.sample_packing_eff_est}") else: total_num_steps = int( math.ceil( @@ -513,7 +506,7 @@ def calc_sample_packing_eff_est(estimates: List[float]): / cfg.batch_size ) ) - LOG.debug(f"total_num_steps: {total_num_steps}", main_process_only=True) + LOG.debug(f"total_num_steps: {total_num_steps}") return total_num_steps diff --git a/tests/e2e/multigpu/solo/test_flex.py b/tests/e2e/multigpu/solo/test_flex.py index 471b112c10..080ea4c97a 100644 --- a/tests/e2e/multigpu/solo/test_flex.py +++ b/tests/e2e/multigpu/solo/test_flex.py @@ -2,7 +2,6 @@ E2E tests for multigpu lora tinyllama """ -import logging import os from pathlib import Path @@ -14,10 +13,11 @@ from transformers.utils import is_torch_bf16_gpu_available from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from tests.e2e.utils import check_tensorboard, require_torch_2_6_0 -LOG = logging.getLogger("axolotl.tests.e2e.multigpu") +LOG = get_logger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent diff --git a/tests/e2e/multigpu/test_eval.py b/tests/e2e/multigpu/test_eval.py index 4989b81df7..45a961b7a4 100644 --- a/tests/e2e/multigpu/test_eval.py +++ b/tests/e2e/multigpu/test_eval.py @@ -2,7 +2,6 @@ E2E tests for multigpu eval """ -import logging import os from pathlib import Path @@ -11,10 +10,11 @@ from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_tensorboard -LOG = logging.getLogger("axolotl.tests.e2e.multigpu") +LOG = get_logger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent diff --git a/tests/e2e/multigpu/test_gemma3.py b/tests/e2e/multigpu/test_gemma3.py index 9de3ed82f8..8540ec91fb 100644 --- a/tests/e2e/multigpu/test_gemma3.py +++ b/tests/e2e/multigpu/test_gemma3.py @@ -2,7 +2,6 @@ E2E tests for multigpu lora tinyllama """ -import logging import os from pathlib import Path @@ -13,10 +12,11 @@ from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from tests.e2e.utils import check_tensorboard -LOG = logging.getLogger("axolotl.tests.e2e.multigpu") +LOG = get_logger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 38e6e741a1..e383c54413 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -2,7 +2,6 @@ E2E tests for multigpu lora tinyllama """ -import logging import os from pathlib import Path @@ -15,10 +14,11 @@ from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from tests.e2e.utils import check_tensorboard, require_torch_2_6_0 -LOG = logging.getLogger("axolotl.tests.e2e.multigpu") +LOG = get_logger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent diff --git a/tests/e2e/multigpu/test_qwen2.py b/tests/e2e/multigpu/test_qwen2.py index 9599c3abf8..23650b10dc 100644 --- a/tests/e2e/multigpu/test_qwen2.py +++ b/tests/e2e/multigpu/test_qwen2.py @@ -2,7 +2,6 @@ E2E tests for multigpu qwen2 """ -import logging import os from pathlib import Path @@ -12,8 +11,9 @@ from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl.tests.e2e.multigpu") +LOG = get_logger("axolotl.tests.e2e.multigpu") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index 843adac912..64c2d501ff 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -2,7 +2,6 @@ E2E tests for multigpu post-training use Ray Train """ -import logging import os from pathlib import Path @@ -11,10 +10,11 @@ from accelerate.test_utils import execute_subprocess_async from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from tests.e2e.utils import check_tensorboard, require_torch_lt_2_6_0 -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) os.environ["WANDB_DISABLED"] = "true" AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index 12dd51c134..27b2b2ca04 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -2,7 +2,6 @@ E2E tests for multipack fft llama using 4d attention masks """ -import logging import os import unittest @@ -11,10 +10,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index f71e4fb4af..2581d39a6e 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import logging import os import pytest @@ -13,10 +12,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, check_tensorboard -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/patched/test_falcon_samplepack.py b/tests/e2e/patched/test_falcon_samplepack.py index 667b62ffba..61689ca1fc 100644 --- a/tests/e2e/patched/test_falcon_samplepack.py +++ b/tests/e2e/patched/test_falcon_samplepack.py @@ -2,7 +2,6 @@ E2E tests for falcon """ -import logging import os import unittest @@ -13,10 +12,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index 7725e095d1..20fd2acb53 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import logging import os import unittest @@ -14,10 +13,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py index 3cf43ba9d3..3c81a274a7 100644 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ b/tests/e2e/patched/test_llama_s2_attention.py @@ -2,7 +2,6 @@ E2E tests for llama w/ S2 attn """ -import logging import os import unittest @@ -13,10 +12,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index ca989f241e..894742a7e8 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import logging import os import unittest @@ -14,10 +13,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index fe8fafb19d..5ae5a6dc5a 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import logging import os import unittest @@ -11,10 +10,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index ebc2ba0927..38a5d6b658 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -2,7 +2,6 @@ E2E tests for mixtral """ -import logging import os import unittest @@ -11,10 +10,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/patched/test_phi_multipack.py b/tests/e2e/patched/test_phi_multipack.py index d8130d1190..54cac15dcd 100644 --- a/tests/e2e/patched/test_phi_multipack.py +++ b/tests/e2e/patched/test_phi_multipack.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import logging import os import unittest @@ -11,10 +10,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index 61e4a0e03d..8ba6b7c540 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -2,7 +2,6 @@ E2E tests for resuming training """ -import logging import os import re import subprocess @@ -14,10 +13,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, most_recent_subdir, require_torch_2_6_0 -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index 5f8fde6b4d..3b429279f5 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -2,7 +2,6 @@ e2e tests for unsloth qlora """ -import logging import os import pytest @@ -12,10 +11,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, check_tensorboard -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/solo/test_flex.py b/tests/e2e/solo/test_flex.py index 71da795f89..431afd55ba 100644 --- a/tests/e2e/solo/test_flex.py +++ b/tests/e2e/solo/test_flex.py @@ -2,7 +2,6 @@ E2E tests for packed training w/ flex attention """ -import logging import os import unittest @@ -13,10 +12,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_tensorboard, require_torch_2_6_0, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/solo/test_relora_llama.py b/tests/e2e/solo/test_relora_llama.py index 504466b90c..6e9f403d0e 100644 --- a/tests/e2e/solo/test_relora_llama.py +++ b/tests/e2e/solo/test_relora_llama.py @@ -2,7 +2,6 @@ E2E tests for relora llama """ -import logging import os import unittest from pathlib import Path @@ -12,10 +11,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, check_tensorboard, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_deepseekv3.py b/tests/e2e/test_deepseekv3.py index 2afda640f1..0a228aa052 100644 --- a/tests/e2e/test_deepseekv3.py +++ b/tests/e2e/test_deepseekv3.py @@ -2,7 +2,6 @@ E2E tests for deepseekv3 """ -import logging import os from pathlib import Path @@ -13,10 +12,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from tests.hf_offline_utils import enable_hf_offline -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index 84d723ec08..b039893849 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import logging import os import unittest from pathlib import Path @@ -14,10 +13,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py index 82b822ad60..fe6a507449 100644 --- a/tests/e2e/test_embeddings_lr.py +++ b/tests/e2e/test_embeddings_lr.py @@ -2,7 +2,6 @@ E2E tests for llama pretrain """ -import logging import os import unittest @@ -11,10 +10,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, check_tensorboard, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_falcon.py b/tests/e2e/test_falcon.py index 24afab0b3f..4f15867caf 100644 --- a/tests/e2e/test_falcon.py +++ b/tests/e2e/test_falcon.py @@ -2,7 +2,6 @@ E2E tests for falcon """ -import logging import os import unittest @@ -13,10 +12,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_gemma2.py b/tests/e2e/test_gemma2.py index 68dc4855d9..8b9b0d11d4 100644 --- a/tests/e2e/test_gemma2.py +++ b/tests/e2e/test_gemma2.py @@ -2,7 +2,6 @@ E2E tests for gemma2 """ -import logging import os from pathlib import Path @@ -13,8 +12,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_gemma3_text.py b/tests/e2e/test_gemma3_text.py index 5cbde04d10..9873de6279 100644 --- a/tests/e2e/test_gemma3_text.py +++ b/tests/e2e/test_gemma3_text.py @@ -2,7 +2,6 @@ E2E tests for gemma3_text """ -import logging import os from pathlib import Path @@ -13,8 +12,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index d3e37fb3fc..352372e1ec 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -2,7 +2,6 @@ E2E tests for llama """ -import logging import os from axolotl.cli.args import TrainerCliArgs @@ -10,10 +9,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from tests.e2e.utils import check_model_output_exists -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index 647285e464..9d0e4d7a6f 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -2,7 +2,6 @@ E2E tests for llama pretrain """ -import logging import os import pytest @@ -12,10 +11,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, check_tensorboard -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py index e1e496ccf8..890f275698 100644 --- a/tests/e2e/test_llama_vision.py +++ b/tests/e2e/test_llama_vision.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import logging import os import unittest @@ -11,10 +10,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index b02fe3d447..02d2868dac 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import logging import os import unittest @@ -11,10 +10,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_mamba.py b/tests/e2e/test_mamba.py index f49b53987d..92397ab88f 100644 --- a/tests/e2e/test_mamba.py +++ b/tests/e2e/test_mamba.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import logging import os import unittest @@ -13,10 +12,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_mistral.py b/tests/e2e/test_mistral.py index ba8cf28962..ac57848435 100644 --- a/tests/e2e/test_mistral.py +++ b/tests/e2e/test_mistral.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import logging import os import unittest @@ -13,10 +12,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_mixtral.py b/tests/e2e/test_mixtral.py index 4e0693b949..329428473f 100644 --- a/tests/e2e/test_mixtral.py +++ b/tests/e2e/test_mixtral.py @@ -2,7 +2,6 @@ E2E tests for mixtral """ -import logging import os import unittest @@ -14,10 +13,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 91f45b762f..291ed3d6a1 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -2,7 +2,6 @@ E2E tests for custom optimizers using Llama """ -import logging import os import unittest @@ -11,10 +10,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, require_torch_2_5_1, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index 73716f44bb..52e27a2c17 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -2,7 +2,6 @@ E2E tests for packed training """ -import logging import os import unittest @@ -13,10 +12,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_tensorboard, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_phi.py b/tests/e2e/test_phi.py index f531a17c50..349ae9efba 100644 --- a/tests/e2e/test_phi.py +++ b/tests/e2e/test_phi.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import logging import os import unittest @@ -11,10 +10,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_process_reward_model_smollm2.py b/tests/e2e/test_process_reward_model_smollm2.py index 446facdb0d..0673409ab2 100644 --- a/tests/e2e/test_process_reward_model_smollm2.py +++ b/tests/e2e/test_process_reward_model_smollm2.py @@ -2,7 +2,6 @@ E2E tests for process reward model w/ lora llama """ -import logging import os import unittest @@ -11,10 +10,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, check_tensorboard, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_qwen.py b/tests/e2e/test_qwen.py index 39d55603f5..1f57c6ae18 100644 --- a/tests/e2e/test_qwen.py +++ b/tests/e2e/test_qwen.py @@ -2,7 +2,6 @@ E2E tests for qwen """ -import logging import os from pathlib import Path @@ -12,8 +11,9 @@ from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger("axolotl.tests.qwen") +LOG = get_logger("axolotl.tests.qwen") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_reward_model_smollm2.py b/tests/e2e/test_reward_model_smollm2.py index 240c4b3924..31938ea589 100644 --- a/tests/e2e/test_reward_model_smollm2.py +++ b/tests/e2e/test_reward_model_smollm2.py @@ -2,7 +2,6 @@ E2E tests for reward model lora llama """ -import logging import os import unittest @@ -11,10 +10,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, check_tensorboard, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/test_schedulers.py b/tests/e2e/test_schedulers.py index 694bb21e81..12783cfb7d 100644 --- a/tests/e2e/test_schedulers.py +++ b/tests/e2e/test_schedulers.py @@ -2,7 +2,6 @@ E2E tests for custom schedulers using Llama """ -import logging import os import unittest @@ -11,10 +10,11 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = logging.getLogger("axolotl.tests.e2e") +LOG = get_logger("axolotl.tests.e2e") os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/integrations/test_liger.py b/tests/integrations/test_liger.py index cbe1408b81..2d6abe311b 100644 --- a/tests/integrations/test_liger.py +++ b/tests/integrations/test_liger.py @@ -2,8 +2,6 @@ config validation tests for swiglu args """ -# pylint: disable=duplicate-code -import logging from typing import Optional import pytest @@ -11,6 +9,11 @@ from axolotl.utils.config import prepare_plugins, validate_config from axolotl.utils.dict import DictDefault +# pylint: disable=duplicate-code +from axolotl.utils.logging import get_logger + +LOG = get_logger("axolotl.integrations.test_liger") + @pytest.fixture(name="minimal_liger_cfg") def fixture_cfg(): @@ -41,7 +44,7 @@ class TestValidation: @pytest.fixture(autouse=True) def inject_fixtures(self, caplog): - caplog.set_level(logging.WARNING) + caplog.set_level("WARNING") self._caplog = caplog def test_deprecated_swiglu(self, minimal_liger_cfg): @@ -52,9 +55,7 @@ def test_deprecated_swiglu(self, minimal_liger_cfg): | minimal_liger_cfg ) - with self._caplog.at_level( - logging.WARNING, logger="axolotl.integrations.liger.args" - ): + with self._caplog.at_level("WARNING", logger="axolotl.integrations.liger.args"): prepare_plugins(test_cfg) updated_cfg = validate_config(test_cfg) # TODO this test is brittle in CI diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 1c7325dffb..93347e2a4f 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -1,7 +1,6 @@ # pylint: disable=too-many-lines """Module for testing the validation module""" -import logging import os import warnings from typing import Optional @@ -13,12 +12,15 @@ from axolotl.utils import is_comet_available from axolotl.utils.config import validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from axolotl.utils.mlflow_ import setup_mlflow_env_vars from axolotl.utils.schemas.config import AxolotlConfigWCapabilities from axolotl.utils.wandb_ import setup_wandb_env_vars warnings.filterwarnings("error") +LOG = get_logger(__name__) + @pytest.fixture(name="minimal_cfg") def fixture_cfg(): @@ -80,7 +82,7 @@ def test_zero3_qlora_use_reentrant_false(self, minimal_cfg): | minimal_cfg ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(test_cfg) assert ( "qlora + zero3 with use_reentrant: false may result in a CheckpointError about recomputed values" @@ -218,7 +220,7 @@ def test_batch_size_unused_warning(self): } ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(cfg) assert "batch_size is not recommended" in self._caplog.records[0].message @@ -513,7 +515,7 @@ def test_flash_optimum(self, minimal_cfg): | minimal_cfg ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(cfg) assert any( "BetterTransformers probably doesn't work with PEFT adapters" @@ -531,7 +533,7 @@ def test_flash_optimum(self, minimal_cfg): | minimal_cfg ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(cfg) assert any( "probably set bfloat16 or float16" in record.message @@ -577,7 +579,7 @@ def test_adamw_hyperparams(self, minimal_cfg): | minimal_cfg ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(cfg) assert any( "adamw hyperparameters found, but no adamw optimizer set" @@ -595,7 +597,7 @@ def test_adamw_hyperparams(self, minimal_cfg): | minimal_cfg ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(cfg) assert any( "adamw hyperparameters found, but no adamw optimizer set" @@ -654,7 +656,7 @@ def test_packing(self, minimal_cfg): ) | minimal_cfg ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(cfg) assert any( "`pad_to_sequence_len: true` is recommended when using sample_packing" @@ -673,7 +675,7 @@ def test_packing_autoset(self, minimal_cfg): ) | minimal_cfg ) - with self._caplog.at_level(logging.INFO): + with self._caplog.at_level("INFO"): cfg = validate_config(cfg) assert any( "Setting `pad_to_sequence_len: true` to prevent memory leaks when sample_packing" @@ -1109,7 +1111,7 @@ def test_unfrozen_parameters_w_peft_layers_to_transform(self, minimal_cfg): def test_hub_model_id_save_value_warns_save_stragey_no(self, minimal_cfg): cfg = DictDefault({"hub_model_id": "test", "save_strategy": "no"}) | minimal_cfg - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(cfg) assert len(self._caplog.records) == 1 @@ -1118,7 +1120,7 @@ def test_hub_model_id_save_value_warns_random_value(self, minimal_cfg): DictDefault({"hub_model_id": "test", "save_strategy": "test"}) | minimal_cfg ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(cfg) assert len(self._caplog.records) == 1 @@ -1128,7 +1130,7 @@ def test_hub_model_id_save_value_steps(self, minimal_cfg): | minimal_cfg ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(cfg) assert len(self._caplog.records) == 0 @@ -1138,28 +1140,28 @@ def test_hub_model_id_save_value_epochs(self, minimal_cfg): | minimal_cfg ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(cfg) assert len(self._caplog.records) == 0 def test_hub_model_id_save_value_none(self, minimal_cfg): cfg = DictDefault({"hub_model_id": "test", "save_strategy": None}) | minimal_cfg - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(cfg) assert len(self._caplog.records) == 0 def test_hub_model_id_save_value_no_set_save_strategy(self, minimal_cfg): cfg = DictDefault({"hub_model_id": "test"}) | minimal_cfg - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): validate_config(cfg) assert len(self._caplog.records) == 0 def test_dpo_beta_deprecation(self, minimal_cfg): cfg = DictDefault({"dpo_beta": 0.2}) | minimal_cfg - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): new_cfg = validate_config(cfg) assert new_cfg["rl_beta"] == 0.2 assert new_cfg["dpo_beta"] is None @@ -1175,7 +1177,7 @@ def test_eval_strategy_remap(self, minimal_cfg): | minimal_cfg ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): new_cfg = validate_config(cfg) assert new_cfg.eval_strategy == "steps" assert ( @@ -1455,7 +1457,7 @@ def test_wandb_set_run_id_to_name(self, minimal_cfg): | minimal_cfg ) - with self._caplog.at_level(logging.WARNING): + with self._caplog.at_level("WARNING"): new_cfg = validate_config(cfg) assert any( "wandb_run_id sets the ID of the run. If you would like to set the name, please use wandb_name instead." diff --git a/tests/prompt_strategies/messages/test_chat.py b/tests/prompt_strategies/messages/test_chat.py index 2681bb7431..a4c2ae67fd 100644 --- a/tests/prompt_strategies/messages/test_chat.py +++ b/tests/prompt_strategies/messages/test_chat.py @@ -3,14 +3,13 @@ """ # pylint: disable=duplicate-code -import logging import unittest from axolotl.prompt_strategies.messages.chat import load from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -logging.basicConfig(level=logging.DEBUG) -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__, log_level="DEBUG") class TestMessagesChatLlama3: diff --git a/tests/prompt_strategies/test_chat_templates.py b/tests/prompt_strategies/test_chat_templates.py index 68772b56b3..371ccf6161 100644 --- a/tests/prompt_strategies/test_chat_templates.py +++ b/tests/prompt_strategies/test_chat_templates.py @@ -2,7 +2,6 @@ tests for chat_template prompt strategy """ -import logging import unittest from axolotl.prompt_strategies.chat_template import ( @@ -13,9 +12,9 @@ from axolotl.prompters import IGNORE_TOKEN_ID from axolotl.utils.chat_templates import get_chat_template from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -logging.basicConfig(level=logging.DEBUG) -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__) class TestAssistantChatTemplateLlama3: diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index 38a5b6c432..7f011f9543 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -4,7 +4,6 @@ # pylint: disable=too-many-lines -import logging from copy import deepcopy import pytest @@ -18,11 +17,11 @@ ) from axolotl.prompters import IGNORE_TOKEN_ID from axolotl.utils.chat_templates import get_chat_template +from axolotl.utils.logging import get_logger from tests.hf_offline_utils import enable_hf_offline -logging.basicConfig(level=logging.DEBUG) -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__) PARAMETRIZE_KEYS = "tokenizer, chat_template, chat_template_jinja, eos_token" PARAMETRIZE_PARAMS = [ diff --git a/tests/prompt_strategies/test_chat_templates_thinking.py b/tests/prompt_strategies/test_chat_templates_thinking.py index 9fe292317d..21d8c4d5ea 100644 --- a/tests/prompt_strategies/test_chat_templates_thinking.py +++ b/tests/prompt_strategies/test_chat_templates_thinking.py @@ -2,8 +2,6 @@ Tests for splitting reasoning/thinking from content into separate field """ -import logging - import pytest from datasets import Dataset from transformers import AutoTokenizer @@ -12,11 +10,11 @@ load, ) from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from tests.hf_offline_utils import enable_hf_offline -logging.basicConfig(level=logging.DEBUG) -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__) @pytest.fixture(name="messages_w_reasoning") diff --git a/tests/prompt_strategies/test_jinja_template_analyzer.py b/tests/prompt_strategies/test_jinja_template_analyzer.py index f666c738c9..41b9a0203a 100644 --- a/tests/prompt_strategies/test_jinja_template_analyzer.py +++ b/tests/prompt_strategies/test_jinja_template_analyzer.py @@ -2,14 +2,12 @@ tests for jinja_template_analyzer """ -import logging - import pytest from axolotl.prompt_strategies.jinja_template_analyzer import JinjaTemplateAnalyzer +from axolotl.utils.logging import get_logger -logging.basicConfig(level=logging.DEBUG) -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__, log_level="DEBUG") class TestJinjaTemplateAnalyzer: diff --git a/tests/test_prompt_tokenizers.py b/tests/test_prompt_tokenizers.py index 3f16bc9177..d34b774b36 100644 --- a/tests/test_prompt_tokenizers.py +++ b/tests/test_prompt_tokenizers.py @@ -1,7 +1,6 @@ """Module for testing prompt tokenizers.""" import json -import logging from pathlib import Path from axolotl.prompt_strategies.alpaca_chat import NoSystemPrompter @@ -17,10 +16,11 @@ from axolotl.prompt_tokenizers import AlpacaPromptTokenizingStrategy from axolotl.prompters import AlpacaPrompter, PromptStyle from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger from tests.hf_offline_utils import enable_hf_offline -LOG = logging.getLogger("axolotl") +LOG = get_logger(__name__) test_data = { "multi_turn_sys": { From 2962a398b7939fbb43d90f56a4323896e3e1148b Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 28 May 2025 10:03:43 -0400 Subject: [PATCH 0682/1405] Lora kernels fix (#2732) * fix lora kernel patching and improve test * simplification --- src/axolotl/loaders/model.py | 10 ++++- src/axolotl/loaders/patch_manager.py | 17 ++++----- .../lora_kernels/test_lora_kernel_patching.py | 37 ++++++++++++++++++- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 681e5d3352..1d26a99dd0 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -14,7 +14,13 @@ import transformers import transformers.modeling_utils from accelerate import init_empty_weights -from peft import PeftConfig, PeftMixedModel, PeftModel, prepare_model_for_kbit_training +from peft import ( + PeftConfig, + PeftMixedModel, + PeftModel, + PeftModelForCausalLM, + prepare_model_for_kbit_training, +) from transformers import ( AutoModelForCausalLM, AutoModelForVision2Seq, @@ -139,7 +145,7 @@ def qlora_fsdp(self): """Property that determines if FSDP with QLoRA is enabled.""" return self.cfg.fsdp and self.cfg.adapter == "qlora" - def load(self) -> tuple[PreTrainedModel, PeftConfig | None]: + def load(self) -> tuple[PreTrainedModel | PeftModelForCausalLM, PeftConfig | None]: """Load and prepare the model with all configurations and patches. Returns: diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index ce1f5cf709..56888b6072 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -59,9 +59,10 @@ def apply_pre_model_load_patches(self): self._apply_gradient_checkpointing_patches() self._patch_attention() self._apply_multipack_patches() + self._patch_loss_llama() self._patch_llama_derived_model() self._apply_mistral_cross_entropy_patch() - self._apply_unsloth_self_attention_patch() + self._apply_self_attention_lora_patch() def apply_post_model_load_patches(self, model: PreTrainedModel): """Apply patches that require the model instance.""" @@ -169,9 +170,9 @@ def _apply_mistral_cross_entropy_patch(self): patch_mistral_cross_entropy() - def _apply_unsloth_self_attention_patch(self): - """Apply Unsloth self-attention patches if configured.""" - if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: + def _apply_self_attention_lora_patch(self): + """Apply self-attention LoRA patches if configured.""" + if self.cfg.lora_qkv_kernel or self.cfg.lora_o_kernel: from axolotl.monkeypatch.lora_kernels import patch_self_attn_lora patch_self_attn_lora(self.cfg) @@ -206,9 +207,6 @@ def _apply_multipack_patches(self): has_remote_code=has_remote_code, ) - if self.cfg.is_llama_derived_model: - self._patch_loss_llama() - def _patch_attention(self): """Apply attention-specific patches based on model type.""" if not (self.cfg.flash_attention and hasattr(self.model_config, "model_type")): @@ -235,6 +233,9 @@ def _patch_attention(self): def _patch_loss_llama(self): """Patch loss functions and other optimizations for LLaMA models.""" + if not self.cfg.is_llama_derived_model: + return + if self.cfg.flash_attn_cross_entropy and self.has_flash_attn: from axolotl.monkeypatch.llama_attn_hijack_flash import ( patch_fa_llama_cross_entropy, @@ -314,8 +315,6 @@ def _patch_llama_derived_model(self): and (self.cfg.flash_attention or self.cfg.flex_attention) and self.cfg.sample_packing ): - self._patch_loss_llama() - if self.cfg.flash_attention: self._patch_llama_flash_attention(packed=self.cfg.sample_packing) elif self.cfg.xformers_attention: diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index f6b7ee9b98..76c383a921 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -21,8 +21,11 @@ apply_lora_o, apply_lora_qkv, ) +from axolotl.loaders.model import ModelLoader +from axolotl.loaders.tokenizer import load_tokenizer from axolotl.monkeypatch.lora_kernels import ( apply_lora_kernel_patches, + get_attention_cls_from_config, patch_self_attn_lora, ) from axolotl.utils.dict import DictDefault @@ -80,7 +83,7 @@ def small_llama_model(): ) def test_attention_patching_integration(model_name, attention_cls): """Test attention patching in integration context.""" - cfg = {"base_model": model_name} + cfg = DictDefault({"base_model": model_name}) # Store the original implementation original_forward = getattr(attention_cls, "forward") @@ -466,3 +469,35 @@ def test_kernel_training_integration_auto_enable(temp_dir): assert cfg.lora_mlp_kernel is True assert cfg.lora_qkv_kernel is True assert cfg.lora_o_kernel is True + + # Get the attention class before patching to check for side effects + attention_cls = get_attention_cls_from_config(cfg) + + # Store original state before patching + original_forward_method = attention_cls.forward + + # Load the model (this should trigger the patches) + tokenizer = load_tokenizer(cfg) + model, _ = ModelLoader(cfg, tokenizer).load() + + # Test side effects of patch_self_attn_lora + assert hasattr(attention_cls, "_original_forward") + assert attention_cls.forward != original_forward_method + + # Find at least one self-attention module and verify it has the patched methods + found_patched_attn = False + for layer in model.model.model.layers: + if hasattr(layer, "self_attn"): + self_attn = layer.self_attn + if all( + hasattr(self_attn, proj) + for proj in ["q_proj", "k_proj", "v_proj", "o_proj"] + ): + # These methods should be added by apply_lora_kernel_patches + assert hasattr(self_attn, "apply_qkv") and callable(self_attn.apply_qkv) + assert hasattr(self_attn, "apply_o") and callable(self_attn.apply_o) + + found_patched_attn = True + break + + assert found_patched_attn From bde8b5b6bda0233699aba8ca85949a0b4ac7409d Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 28 May 2025 14:59:57 -0400 Subject: [PATCH 0683/1405] fix dist state init before deepspeed setup (#2737) --- src/axolotl/integrations/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 11d85f8f81..0edc9fdea2 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -32,7 +32,7 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger -LOG = get_logger(__name__) +LOG = get_logger(__name__, use_environ=True) if TYPE_CHECKING: from axolotl.common.datasets import TrainDatasetMeta From ec4ebfd99787577bff10d85beb7174aba3489236 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 28 May 2025 16:20:19 -0400 Subject: [PATCH 0684/1405] Add a few items to faq (#2734) * Add a few items to faq * formatting * chore: lint --- docs/faq.qmd | 14 ++++++++++++++ src/axolotl/utils/schemas/config.py | 12 ------------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/faq.qmd b/docs/faq.qmd index f586099e78..f2744caba8 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -110,3 +110,17 @@ description: Frequently asked questions > A: If `eot_tokens: ` is not provided, the default behavior is the same as before. EOS tokens used to delimit turns are masked/unmasked depending on whether the turn is trainable. > Internally, `eot_tokens: tokenizer.eos_token` and `train_on_eot: train_on_eos` (which defaults to `turn`). This transition helps clarify the naming and behavior of EOT/EOS tokens. + +**Q: `Data processing error: CAS service error`** + +> A: Try disabling XET with `export HF_HUB_DISABLE_XET=1` + +**Q: `torch._inductor.exc.LoweringException: NoValidChoicesError: No choices to select, please consider adding ATEN into max_autotune_gemm_backends config (defined in torch/_inductor/config.py) to allow at least one choice. `** + +> A: Depending on the version of torch, you may need to include this in your YAML: + +> ```yaml +> flex_attn_compile_kwargs: +> dynamic: false +> mode: max-autotune-no-cudagraphs +> ``` diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 75551085b6..698befa194 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1175,18 +1175,6 @@ def check_kto_config(cls, data): return data - # @model_validator(mode="before") - # @classmethod - # def check_grpo_peft_liger(cls, data): - # if ( - # data.get("rl") == "grpo" - # and data.get("trl", {}) - # and data.get("trl").get("use_liger_loss") - # and data.get("adapter") - # ): - # raise ValueError("PEFT + GRPO + Liger is not yet supported") - # return data - # @model_validator(mode="before") @classmethod def check_grpo_liger_sequence_parallel(cls, data): From 6778856804e2c6d5f9903006916f9cd4bfb27756 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 30 May 2025 11:21:47 +0700 Subject: [PATCH 0685/1405] Fix: RL base feature parity (#2133) * feat: add num_proc and load from cache for rl mapping * fix: refactor sft and rl trainer to set same base args * feat: add report_to to set run name * fix: consolidate handling of fp16, bf16, tf32 kwarg * chore: consolidate eval_strat, loraplus, lr sched, max_length * fix: deprecate old types * fix: adding missing Any * fix: max_steps incorrectly set * fix: remove unnecessary datacollator kwarg insert and pop * fix: update default max_steps * fix: add missing weight_decay handling * fix: ignore max_length for grpo * feat: update CI on trainer_builder * fix: comments * improve handling of warmup/logging steps * use transformers default for logging steps, not None * fix: remove redundant override * fix: lint * feat: allow custom optim for rl methods * fix: duplicate optim setting * fix(test): set sequence_parallel_degree default in base cfg * feat: add handling for seed and SP/ring-attn config * chore: add back return typing from rebase * fix(test): use RLType directly to skip needing to validate * feat: split training builder into sub modules * fix: remove deprecated clause * chore: add missing config to doc * fix: update quarto autodoc * fix: import path for trainer builder and submodules * fix: remove redundant configs from rebase mistake * chore: simplify dynamo check * fix: optimizer_cls_and_kwargs to be passed into trainer_kwargs * fix: add missing rex from rebase * fix: move pop optimizer_cls_and_kwargs * fix: pop optimizer cls in rl too * fix: leftover bug from rebase * fix: update handling of trainer_cls in RL * fix: address pr feedback * feat: call hook_pre_create_trainer for rl * chore: lint * fix: return notimplemented for ppo * feat: moved torch compile to base and refactor collator setting * chore: remove unused importlib.util import * fix: optimizer cls not being popped * feat: move epoch setting to base * fix: catch unhandled custom optimizer * fix: remove duplicate lora plus setting * chore: refactor if condition * chore: refactor set_base_training_args into smaller modules * fix: address TrainerBuilderBase class variables to instance var * fix: add handling for beta3 and episilon2 * fix: change to pass dict via arg instead of updating dict * chore: simplify if condition * fix: force access to lr & weight decay in case not provided to early error * fix: remove log sweep * chore: refactor if condition * fix: address renamed cfg * fix: improve handling of cosine hyp * fix: remove unused params * chore: refactor * chore: clarify doc safetensors * fix: update import path to be unified following comments * fix: duplicate kwargs passed * feat: return separate trainer_kwargs * chore: refactor * chore: refactor based on comments * chore: refactor based on comments * fix: move gpustats callback to base * chore: create trainer_cls_args first based on comments * fix: ipo label smoothing passed incorrectly * feat: add optimizer parity for RL methods with test * feat: add parity for optimizer in RM/PRM and add test * fix: remove redundant function override for orpo/cpo batch metrics * fix: improve handling of dpo_label_smoothing and merge issue * fix: test fixture returning wrong field * fix: address avoid direct modify fixture * chore: minor refactor * Revert "chore: refactor" This reverts commit 99c8859eb02d1c4bde465d1fb0290a8024d2fd87. * feat: rename trainer_builder to builders --------- Co-authored-by: Wing Lian --- _quarto.yml | 4 +- docs/config.qmd | 3 +- src/axolotl/core/builders/__init__.py | 6 + src/axolotl/core/builders/base.py | 503 +++++++ src/axolotl/core/builders/causal.py | 487 +++++++ src/axolotl/core/builders/rl.py | 246 ++++ src/axolotl/core/trainer_builder.py | 1252 ----------------- src/axolotl/core/trainers/dpo/trainer.py | 44 +- src/axolotl/core/trainers/grpo/__init__.py | 4 +- src/axolotl/core/trainers/grpo/trainer.py | 7 +- src/axolotl/core/trainers/mixins/optimizer.py | 17 + src/axolotl/core/trainers/trl.py | 169 +-- src/axolotl/core/training_args.py | 6 - src/axolotl/train.py | 2 +- src/axolotl/utils/callbacks/__init__.py | 2 +- src/axolotl/utils/callbacks/comet_.py | 2 +- src/axolotl/utils/callbacks/lisa.py | 2 +- src/axolotl/utils/callbacks/mlflow_.py | 2 +- src/axolotl/utils/data/rl.py | 3 +- src/axolotl/utils/schemas/config.py | 1 + src/axolotl/utils/trainer.py | 2 +- tests/core/test_builders.py | 595 ++++++++ tests/core/test_trainer_builder.py | 90 -- tests/e2e/test_imports.py | 4 +- 24 files changed, 1899 insertions(+), 1554 deletions(-) create mode 100644 src/axolotl/core/builders/__init__.py create mode 100644 src/axolotl/core/builders/base.py create mode 100644 src/axolotl/core/builders/causal.py create mode 100644 src/axolotl/core/builders/rl.py delete mode 100755 src/axolotl/core/trainer_builder.py create mode 100644 tests/core/test_builders.py delete mode 100644 tests/core/test_trainer_builder.py diff --git a/_quarto.yml b/_quarto.yml index e05a1c35c3..a970cd08ba 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -17,7 +17,9 @@ quartodoc: - convert - prompt_tokenizers - logging_config - - core.trainer_builder + - core.builders.base + - core.builders.causal + - core.builders.rl - core.training_args - core.chat.messages - core.chat.format.chatml diff --git a/docs/config.qmd b/docs/config.qmd index 5a36ca8aa4..519065554c 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -514,6 +514,7 @@ output_dir: ./completed-model # setting to `auto` will enable torch compile when torch>=2.5.1 torch_compile: # Optional[Union[Literal["auto"], bool]] torch_compile_backend: # Optional[str] +torch_compile_mode: # 'default' | 'reduce-overhead' | 'max-autotune' # Training hyperparameters @@ -560,7 +561,7 @@ profiler_steps: # enable the pytorch profiler to capture the first N steps of tr loss_watchdog_threshold: # High loss value, indicating the learning has broken down (a good estimate is ~2 times the loss at the start of training) loss_watchdog_patience: # Number of high-loss steps in a row before the trainer aborts (default: 3) -# Save model as safetensors (require safetensors package) +# Save model as safetensors (require safetensors package). Default True save_safetensors: # Whether to mask out or include the human's prompt from the training labels diff --git a/src/axolotl/core/builders/__init__.py b/src/axolotl/core/builders/__init__.py new file mode 100644 index 0000000000..5bd2444341 --- /dev/null +++ b/src/axolotl/core/builders/__init__.py @@ -0,0 +1,6 @@ +"""Trainer builder classes""" + +from .causal import HFCausalTrainerBuilder +from .rl import HFRLTrainerBuilder + +__all__ = ["HFCausalTrainerBuilder", "HFRLTrainerBuilder"] diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py new file mode 100644 index 0000000000..907de056b3 --- /dev/null +++ b/src/axolotl/core/builders/base.py @@ -0,0 +1,503 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base class for trainer builder""" + +import abc +import importlib +import logging +import sys +from abc import abstractmethod +from contextlib import suppress +from pathlib import Path +from typing import Any + +import torch +from transformers import ( + TrainerCallback, +) +from transformers.training_args import OptimizerNames + +from axolotl.integrations.base import PluginManager +from axolotl.monkeypatch.trainer.lr import patch_trainer_get_lr +from axolotl.utils import is_comet_available, is_mlflow_available +from axolotl.utils.callbacks import ( + GCCallback, + GPUStatsCallback, + SaveAxolotlConfigtoWandBCallback, +) +from axolotl.utils.callbacks.profiler import PytorchProfilerCallback +from axolotl.utils.schemas.enums import CustomSupportedOptimizers + +LOG = logging.getLogger(__name__) + +with suppress(ImportError): + import torch._dynamo # pylint: disable=ungrouped-imports + + +class TrainerBuilderBase(abc.ABC): + """Base class for trainer builder.""" + + def __init__(self, cfg, model, tokenizer, processor=None): + self.cfg = cfg + self.model = model + self.tokenizer = tokenizer + self.processor = processor + + self._train_dataset = None + self._eval_dataset = None + self._model_ref = None + self._peft_config = None + + # If the model supports tagging, add the axolotl tag. + # This makes sure the tag is correctly pushed even if a user calls + # model.push_to_hub instead of trainer.push_to_hub. + if hasattr(model, "add_model_tags"): + model.add_model_tags(["axolotl"]) + + patch_trainer_get_lr() + + @property + def model_ref(self): + return self._model_ref + + @model_ref.setter + def model_ref(self, model): + self._model_ref = model + + @property + def train_dataset(self): + return self._train_dataset + + @train_dataset.setter + def train_dataset(self, dataset): + self._train_dataset = dataset + + @property + def eval_dataset(self): + return self._eval_dataset + + @eval_dataset.setter + def eval_dataset(self, dataset): + self._eval_dataset = dataset + + @property + def peft_config(self): + return self._peft_config + + @peft_config.setter + def peft_config(self, peft_config): + self._peft_config = peft_config + + @abstractmethod + def build(self, total_num_steps): + pass + + def get_callbacks(self) -> list[TrainerCallback]: + callbacks = [] + + plugin_manager = PluginManager.get_instance() + callbacks.extend( + plugin_manager.add_callbacks_pre_trainer(cfg=self.cfg, model=self.model) + ) + + if self.cfg.profiler_steps: + callbacks.append( + PytorchProfilerCallback( + steps_to_profile=self.cfg.profiler_steps, + ) + ) + + if self.cfg.gc_steps: + callbacks.append(GCCallback(gc_steps=self.cfg.gc_steps)) + + if self.cfg.use_wandb: + callbacks.append( + SaveAxolotlConfigtoWandBCallback(self.cfg.axolotl_config_path) + ) + if self.cfg.use_mlflow and is_mlflow_available(): + from axolotl.utils.callbacks.mlflow_ import ( + SaveAxolotlConfigtoMlflowCallback, + ) + + callbacks.extend( + [ + SaveAxolotlConfigtoMlflowCallback(self.cfg.axolotl_config_path), + ] + ) + if self.cfg.use_comet and is_comet_available(): + from axolotl.utils.callbacks.comet_ import SaveAxolotlConfigtoCometCallback + + callbacks.append( + SaveAxolotlConfigtoCometCallback(self.cfg.axolotl_config_path) + ) + + callbacks.append(GPUStatsCallback(cfg=self.cfg)) + + return callbacks + + def get_post_trainer_create_callbacks(self, trainer): + """ + Callbacks added after the trainer is created, usually b/c these need access to the trainer + """ + callbacks = [] + if self.cfg.plugins: + plugin_manager = PluginManager.get_instance() + callbacks.extend( + [ + cb + for cb in plugin_manager.add_callbacks_post_trainer( + self.cfg, trainer + ) + if cb + ] + ) + return callbacks + + def hook_pre_create_training_args(self, training_arguments_kwargs): + # TODO + return training_arguments_kwargs + + def hook_post_create_training_args(self, training_arguments): + # TODO + return training_arguments + + def hook_pre_create_trainer(self, trainer_kwargs, trainer_cls): + # TODO + return trainer_kwargs, trainer_cls + + def hook_post_create_trainer(self, trainer): + # TODO + return trainer + + def _configure_warmup_and_logging( + self, total_num_steps: int, training_args_kwargs: dict + ): + warmup_steps = 0 + warmup_ratio = 0.0 + if self.cfg.warmup_steps: + warmup_steps = self.cfg.warmup_steps + elif self.cfg.warmup_ratio: + if total_num_steps: + warmup_steps = max(int(self.cfg.warmup_ratio * total_num_steps), 0) + else: + warmup_ratio = self.cfg.warmup_ratio + elif total_num_steps: + warmup_steps = min(int(0.03 * total_num_steps), 100) + else: + warmup_ratio = 0.03 + + if warmup_steps == 1: + warmup_steps = 2 + + if self.cfg.logging_steps is not None: + training_args_kwargs["logging_steps"] = self.cfg.logging_steps + else: + training_args_kwargs["logging_steps"] = ( + 500 # transformers defaults to 500 + if not total_num_steps + else max(min(int(0.005 * total_num_steps), 10), 1) + ) + + training_args_kwargs["warmup_ratio"] = warmup_ratio + training_args_kwargs["warmup_steps"] = warmup_steps + + def _configure_precision_settings(self, training_args_kwargs: dict): + training_args_kwargs["fp16"] = (self.cfg.fp16 and not self.cfg.bf16) or False + training_args_kwargs["tf32"] = self.cfg.tf32 + if self.cfg.bf16 == "full": + training_args_kwargs["bf16_full_eval"] = True + else: + training_args_kwargs["bf16"] = self.cfg.bf16 or self.cfg.bfloat16 + + def _configure_scheduler(self, training_args_kwargs: dict): + if self.cfg.lr_scheduler in ["one_cycle", "rex"]: + training_args_kwargs["lr_scheduler_type"] = "cosine" + training_args_kwargs["alternate_lr_scheduler_type"] = self.cfg.lr_scheduler + else: + training_args_kwargs["lr_scheduler_type"] = ( + self.cfg.lr_scheduler if self.cfg.lr_scheduler else "cosine" + ) + training_args_kwargs["lr_scheduler_kwargs"] = ( + self.cfg.lr_scheduler_kwargs if self.cfg.lr_scheduler_kwargs else {} + ) + + def _configure_optimizer(self, training_args_kwargs: dict, trainer_kwargs: dict): + def _configure_custom_optimizer( + training_args_kwargs: dict, trainer_kwargs: dict + ): + # Common optimizer kwargs + optimizer_kwargs = { + "lr": training_args_kwargs["learning_rate"], + "weight_decay": training_args_kwargs["weight_decay"], + } + + # Adam-specific kwargs + adam_kwargs: dict = {} + if training_args_kwargs.get("adam_beta1") and training_args_kwargs.get( + "adam_beta2" + ): + adam_kwargs["betas"] = ( + training_args_kwargs.get("adam_beta1"), + training_args_kwargs.get("adam_beta2"), + ) + if training_args_kwargs.get("adam_epsilon"): + adam_kwargs["eps"] = training_args_kwargs.get("adam_epsilon") + + if self.cfg.optimizer == "muon": + from axolotl.contribs.mit.muon import ( # pylint: disable=no-name-in-module + MuonOptimizerFactory, + ) + + optimizer_cls = MuonOptimizerFactory + optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "optimi_adamw": + from optimi import AdamW + + optimizer_kwargs["foreach"] = False + optimizer_cls = AdamW + optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "ao_adamw_4bit": + # TODO remove 20250401 + from torchao.prototype.low_bit_optim import AdamW4bit + + optimizer_cls = AdamW4bit + optimizer_kwargs.update(adam_kwargs) + + LOG.warning( + f"`ao_adamw_4bit` will be deprecated soon. Please use `{OptimizerNames.ADAMW_TORCH_4BIT}` instead." + ) + elif self.cfg.optimizer == "ao_adamw_8bit": + from torchao.prototype.low_bit_optim import AdamW8bit + + optimizer_cls = AdamW8bit + optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "ao_adamw_fp8": + from torchao.prototype.low_bit_optim import AdamWFp8 + + optimizer_cls = AdamWFp8 + optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "adopt_adamw": + from axolotl.utils.optimizers.adopt import ADOPT + + optimizer_cls = ADOPT + adam_kwargs["decouple"] = True + optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "came_pytorch": + from came_pytorch import CAME + + optimizer_cls = CAME + + beta1 = training_args_kwargs.get("adam_beta1", 0.9) + beta2 = training_args_kwargs.get("adam_beta2", 0.999) + beta3 = training_args_kwargs.get("adam_beta3", 0.9999) + eps1 = training_args_kwargs.get("adam_epsilon", 1e-30) + eps2 = training_args_kwargs.get("adam_epsilon2", 1e-16) + adam_kwargs["betas"] = (beta1, beta2, beta3) + adam_kwargs["eps"] = (eps1, eps2) + + optimizer_kwargs.update(adam_kwargs) + else: + raise ValueError( + f"Unhandled optimizer: {self.cfg.optimizer}. Please raise an Issue." + ) + + # Parse any additional optimizer args from config + if self.cfg.optim_args: + if isinstance(self.cfg.optim_args, dict): + optimizer_kwargs.update(self.cfg.optim_args) + else: + # Parse string format "key1=value1,key2=value2" + for mapping in self.cfg.optim_args.replace(" ", "").split(","): + key, value = mapping.split("=") + optimizer_kwargs[key] = value + + # Note: This is not used in training_args_kwargs, but in trainer_kwargs + trainer_kwargs["optimizer_cls_and_kwargs"] = ( + optimizer_cls, + optimizer_kwargs, + ) + + # Handle custom optimizer + custom_supported_optimizers = [opt.value for opt in CustomSupportedOptimizers] + if self.cfg.optimizer in custom_supported_optimizers: + _configure_custom_optimizer(training_args_kwargs, trainer_kwargs) + else: + # Use transformers' optimizer + training_args_kwargs["optim"] = self.cfg.optimizer + + # Parse any additional optimizer args from config + if self.cfg.optim_args: + if isinstance(self.cfg.optim_args, dict): + optim_args = ",".join( + [f"{key}={value}" for key, value in self.cfg.optim_args.items()] + ) + else: + optim_args = self.cfg.optim_args + training_args_kwargs["optim_args"] = optim_args + + if ( + self.cfg.optimizer == "adamw_anyprecision" + and Path(self.cfg.torchdistx_path).exists() + ): + sys.path.append(self.cfg.torchdistx_path) + importlib.import_module("torchdistx") + + def _configure_hub_parameters(self, training_args_kwargs: dict): + if self.cfg.hub_model_id: + training_args_kwargs["hub_model_id"] = self.cfg.hub_model_id + training_args_kwargs["push_to_hub"] = True + training_args_kwargs["hub_private_repo"] = True + training_args_kwargs["hub_always_push"] = True + + if self.cfg.hub_strategy: + training_args_kwargs["hub_strategy"] = self.cfg.hub_strategy + + def _configure_save_and_eval_strategy(self, training_args_kwargs: dict): + # save_strategy and save_steps + if self.cfg.save_steps: + training_args_kwargs["save_strategy"] = "steps" + training_args_kwargs["save_steps"] = self.cfg.save_steps + elif self.cfg.save_strategy: + training_args_kwargs["save_strategy"] = self.cfg.save_strategy + else: + # default to saving each epoch if not defined + training_args_kwargs["save_strategy"] = "epoch" + + training_args_kwargs["save_total_limit"] = ( + self.cfg.save_total_limit if self.cfg.save_total_limit else 4 + ) + + # eval_strategy and eval_steps + if not self.eval_dataset or self.cfg.val_set_size == 0: + # do not eval if no eval_dataset or val_set_size=0 + training_args_kwargs["eval_strategy"] = "no" + elif self.cfg.eval_steps: + training_args_kwargs["eval_strategy"] = "steps" + training_args_kwargs["eval_steps"] = self.cfg.eval_steps + elif self.cfg.eval_strategy: + training_args_kwargs["eval_strategy"] = self.cfg.eval_strategy + + def _configure_reporting(self, training_args_kwargs: dict): + report_to = [] + if self.cfg.use_wandb: + report_to.append("wandb") + if self.cfg.use_mlflow: + report_to.append("mlflow") + if self.cfg.use_tensorboard: + report_to.append("tensorboard") + if self.cfg.use_comet: + report_to.append("comet_ml") + + training_args_kwargs["report_to"] = report_to + + if self.cfg.use_wandb: + training_args_kwargs["run_name"] = self.cfg.wandb_name + elif self.cfg.use_mlflow: + training_args_kwargs["run_name"] = self.cfg.mlflow_run_name + else: + training_args_kwargs["run_name"] = None + + def _configure_torch_compile(self, training_args_kwargs: dict): + if self.cfg.torch_compile and getattr(torch, "_dynamo", None): + torch._dynamo.config.suppress_errors = ( # pylint: disable=protected-access + True + ) + training_args_kwargs["torch_compile"] = self.cfg.torch_compile + if self.cfg.torch_compile_backend: + training_args_kwargs["torch_compile_backend"] = ( + self.cfg.torch_compile_backend + ) + if self.cfg.torch_compile_mode: + training_args_kwargs["torch_compile_mode"] = self.cfg.torch_compile_mode + + def _configure_gradient_checkpointing(self, training_args_kwargs: dict): + if self.cfg.gradient_checkpointing: + training_args_kwargs["gradient_checkpointing"] = ( + self.cfg.gradient_checkpointing + ) + if self.cfg.gradient_checkpointing_kwargs is not None: + training_args_kwargs["gradient_checkpointing_kwargs"] = ( + self.cfg.gradient_checkpointing_kwargs + ) + else: + training_args_kwargs["gradient_checkpointing_kwargs"] = { + "use_reentrant": False + } + + def _set_base_training_args( + self, total_num_steps + ) -> tuple[dict[str, Any], dict[str, Any]]: + training_args_kwargs: dict[str, Any] = {} + trainer_kwargs: dict[str, Any] = {} + + self._configure_warmup_and_logging(total_num_steps, training_args_kwargs) + self._configure_precision_settings(training_args_kwargs) + self._configure_save_and_eval_strategy(training_args_kwargs) + self._configure_gradient_checkpointing(training_args_kwargs) + + # set arg into trainer_args_kwargs with same name if value not None + for arg in [ + # optim/scheduler + "adam_beta1", + "adam_beta2", + "adam_beta3", + "adam_epsilon", + "adam_epsilon2", + "cosine_min_lr_ratio", + "cosine_constant_lr_ratio", + "optim_target_modules", + # trainer + "max_grad_norm", + "dataloader_num_workers", + "dataloader_pin_memory", + "dataloader_prefetch_factor", + "gradient_accumulation_steps", + "learning_rate", + "embedding_lr", + "embedding_lr_scale", + "lr_groups", + "loraplus_lr_ratio", + "loraplus_lr_embedding", + "output_dir", + "save_safetensors", + "save_only_model", + "include_tokens_per_second", + "weight_decay", + "seed", + ]: + if hasattr(self.cfg, arg) and getattr(self.cfg, arg) is not None: + training_args_kwargs[arg] = getattr(self.cfg, arg) + + training_args_kwargs["per_device_train_batch_size"] = self.cfg.micro_batch_size + + if self.cfg.eval_batch_size: + training_args_kwargs["per_device_eval_batch_size"] = ( + self.cfg.eval_batch_size + ) + + training_args_kwargs["max_steps"] = self.cfg.max_steps or total_num_steps or -1 + training_args_kwargs["num_train_epochs"] = self.cfg.num_epochs + + # max_length is not used in CausalTrainer + if self.cfg.reward_model or self.cfg.rl: + training_args_kwargs["max_length"] = self.cfg.sequence_len + + self._configure_reporting(training_args_kwargs) + self._configure_hub_parameters(training_args_kwargs) + self._configure_scheduler(training_args_kwargs) + self._configure_optimizer(training_args_kwargs, trainer_kwargs) + self._configure_torch_compile(training_args_kwargs) + + return training_args_kwargs, trainer_kwargs diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py new file mode 100644 index 0000000000..3b1d0b3c2d --- /dev/null +++ b/src/axolotl/core/builders/causal.py @@ -0,0 +1,487 @@ +"""Builder for causal trainers""" + +import inspect +import math +import os +from pathlib import Path +from typing import Type, Union + +import transformers +from transformers import ( + DataCollatorWithFlattening, + EarlyStoppingCallback, +) +from trl.trainer.utils import RewardDataCollatorWithPadding + +from axolotl.core.builders.base import TrainerBuilderBase +from axolotl.core.trainers import ( + AxolotlMambaTrainer, + AxolotlPRMTrainer, + AxolotlRewardTrainer, + AxolotlTrainer, + ReLoRATrainer, +) +from axolotl.core.training_args import ( + AxolotlPRMConfig, + AxolotlRewardConfig, + AxolotlTrainingArguments, +) +from axolotl.integrations.base import PluginManager +from axolotl.monkeypatch.multipack import SUPPORTED_MULTIPACK_MODEL_TYPES +from axolotl.monkeypatch.relora import ReLoRACallback +from axolotl.processing_strategies import get_processing_strategy +from axolotl.utils import is_comet_available, is_mlflow_available +from axolotl.utils.callbacks import ( + EvalFirstStepCallback, + LossWatchDogCallback, + SaveBetterTransformerModelCallback, + bench_eval_callback_factory, + causal_lm_bench_eval_callback_factory, + colab_inference_post_train_callback, + log_prediction_callback_factory, +) +from axolotl.utils.callbacks.lisa import lisa_callback_factory +from axolotl.utils.callbacks.qat import QATCallback +from axolotl.utils.chat_templates import get_chat_template_from_config +from axolotl.utils.collators import ( + BatchSamplerDataCollatorForSeq2Seq, + DataCollatorForSeq2Seq, + MambaDataCollator, + V2BatchSamplerDataCollatorForSeq2Seq, +) +from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +class HFCausalTrainerBuilder(TrainerBuilderBase): + """ + Build the HuggingFace training args/trainer for causal models and reward modeling + using TRL. + """ + + def get_callbacks(self): + callbacks = super().get_callbacks() + callbacks.append(EvalFirstStepCallback()) + + if self.cfg.relora_steps: + callbacks.append(ReLoRACallback(self.cfg)) + + if ( + hasattr(self.model, "use_bettertransformer") + and self.model.use_bettertransformer is True + ): + callbacks.append(SaveBetterTransformerModelCallback()) + + # TODO: check if can move to base class + if self.cfg.loss_watchdog_threshold is not None: + callbacks.append(LossWatchDogCallback(self.cfg)) + + if self.cfg.qat: + callbacks.append(QATCallback(self.cfg.qat)) + + return callbacks + + def get_post_trainer_create_callbacks(self, trainer): + callbacks = [] + if self.cfg.use_wandb and self.cfg.eval_table_size > 0: + LogPredictionCallback = log_prediction_callback_factory( + trainer, self.tokenizer, "wandb" + ) + callbacks.append(LogPredictionCallback(self.cfg)) + if ( + self.cfg.use_mlflow + and is_mlflow_available() + and self.cfg.eval_table_size > 0 + ): + LogPredictionCallback = log_prediction_callback_factory( + trainer, self.tokenizer, "mlflow" + ) + callbacks.append(LogPredictionCallback(self.cfg)) + if self.cfg.use_comet and is_comet_available() and self.cfg.eval_table_size > 0: + LogPredictionCallback = log_prediction_callback_factory( + trainer, self.tokenizer, "comet_ml" + ) + callbacks.append(LogPredictionCallback(self.cfg)) + + if self.cfg.do_bench_eval: + callbacks.append(bench_eval_callback_factory(trainer, self.tokenizer)) + if self.cfg.do_causal_lm_eval: + CausalLMBenchEvalCallback = causal_lm_bench_eval_callback_factory( + trainer, self.tokenizer + ) + callbacks.append(CausalLMBenchEvalCallback(self.cfg)) + + if self.cfg.early_stopping_patience: + early_stop_cb = EarlyStoppingCallback( + self.cfg.early_stopping_patience, + ) + callbacks.append(early_stop_cb) + + if self.cfg.lisa_step_interval and self.cfg.lisa_n_layers: + callbacks.append(lisa_callback_factory(trainer)) + + if any("COLAB_" in key for key in os.environ): + ColabCallback = colab_inference_post_train_callback(trainer) + callbacks.append(ColabCallback(self.cfg)) + + callbacks.extend(super().get_post_trainer_create_callbacks(trainer=trainer)) + return callbacks + + def _get_trainer_cls(self): + if self.cfg.plugins: + plugin_manager = PluginManager.get_instance() + trainer_cls = plugin_manager.get_trainer_cls(self.cfg) + if trainer_cls: + return trainer_cls + if self.cfg.relora_steps: + return ReLoRATrainer + if self.cfg.model_config_type == "mamba": + return AxolotlMambaTrainer + if self.cfg.reward_model: + return AxolotlRewardTrainer + if self.cfg.process_reward_model: + return AxolotlPRMTrainer + return AxolotlTrainer + + def build(self, total_num_steps): + training_arguments_kwargs, trainer_kwargs = self._set_base_training_args( + total_num_steps + ) + + if self.cfg.fsdp: + training_arguments_kwargs["fsdp"] = self.cfg.fsdp + if self.cfg.fsdp_config: + training_arguments_kwargs["fsdp_config"] = { + k.lstrip("fsdp_"): v for k, v in dict(self.cfg.fsdp_config).items() + } + + if self.cfg.adapter == "qlora": + training_arguments_kwargs["qlora"] = True + + # deepspeed + if self.cfg.deepspeed: + training_arguments_kwargs["deepspeed"] = self.cfg.deepspeed + + if self.cfg.lr_quadratic_warmup is not None: + training_arguments_kwargs["lr_quadratic_warmup"] = ( + self.cfg.lr_quadratic_warmup + ) + + if self.cfg.dataloader_drop_last is not None: + training_arguments_kwargs["dataloader_drop_last"] = ( + self.cfg.dataloader_drop_last + ) + elif self.cfg.sample_packing and self.cfg.eval_sample_packing is False: + training_arguments_kwargs["dataloader_drop_last"] = True + + if self.cfg.remove_unused_columns is not None: + training_arguments_kwargs["remove_unused_columns"] = ( + self.cfg.remove_unused_columns + ) + + if self.cfg.do_bench_eval: + training_arguments_kwargs["do_bench_eval"] = self.cfg.do_bench_eval + if self.cfg.bench_dataset: + training_arguments_kwargs["bench_dataset"] = self.cfg.bench_dataset + if self.cfg.do_causal_lm_eval: + training_arguments_kwargs["do_causal_lm_eval"] = self.cfg.do_causal_lm_eval + if self.cfg.metric_for_best_model: + training_arguments_kwargs["metric_for_best_model"] = ( + self.cfg.metric_for_best_model + ) + if self.cfg.greater_is_better: + training_arguments_kwargs["greater_is_better"] = self.cfg.greater_is_better + + # DDP Config + if self.cfg.ddp_timeout: + training_arguments_kwargs["ddp_timeout"] = self.cfg.ddp_timeout + # see https://pytorch.org/docs/stable/generated/torch.nn.parallel.DistributedDataParallel.html + if self.cfg.ddp_bucket_cap_mb: + training_arguments_kwargs["ddp_bucket_cap_mb"] = self.cfg.ddp_bucket_cap_mb + if self.cfg.ddp_broadcast_buffers is not None: + training_arguments_kwargs["ddp_broadcast_buffers"] = ( + self.cfg.ddp_broadcast_buffers + ) + + # these are all the "standard" kwargs that are def used + training_arguments_kwargs["max_seq_length"] = self.cfg.sequence_len + + if self.cfg.auto_find_batch_size is not None: + training_arguments_kwargs["auto_find_batch_size"] = ( + self.cfg.auto_find_batch_size + ) + + training_arguments_kwargs["eval_accumulation_steps"] = ( + self.cfg.gradient_accumulation_steps + ) + + training_arguments_kwargs["load_best_model_at_end"] = ( + ( + self.cfg.load_best_model_at_end is not False + or self.cfg.early_stopping_patience + ) + and ( + (not self.cfg.test_datasets and self.cfg.val_set_size > 0) + or (self.cfg.test_datasets and self.cfg.val_set_size == 0) + ) + and self.cfg.save_steps + and self.cfg.eval_steps + and self.cfg.save_steps % self.cfg.eval_steps == 0 + ) or False + + # handle ddp + ddp_find_unused_parameters = None + if self.cfg.ddp: + ddp_find_unused_parameters = bool(self.cfg.ddp_find_unused_parameters) + training_arguments_kwargs["ddp_find_unused_parameters"] = ( + ddp_find_unused_parameters + ) + + training_arguments_kwargs["group_by_length"] = self.cfg.group_by_length + training_arguments_kwargs["curriculum_sampling"] = self.cfg.curriculum_sampling + + training_arguments_kwargs["sample_packing"] = bool(self.cfg.sample_packing) + training_arguments_kwargs["multipack_real_batches"] = ( + not self.cfg.flash_attention or self.cfg.multipack_real_batches + ) + training_arguments_kwargs["eval_sample_packing"] = bool( + self.cfg.eval_sample_packing + ) + if self.cfg.sample_packing_bin_size is not None: + training_arguments_kwargs["sample_packing_bin_size"] = ( + self.cfg.sample_packing_bin_size + ) + if self.cfg.sample_packing_group_size is not None: + training_arguments_kwargs["sample_packing_group_size"] = ( + self.cfg.sample_packing_group_size + ) + if self.cfg.sample_packing_eff_est: + training_arguments_kwargs["sample_packing_efficiency"] = ( + self.cfg.sample_packing_eff_est + ) + + if self.cfg.relora_steps: + training_arguments_kwargs["relora_steps"] = self.cfg.relora_steps + training_arguments_kwargs["relora_warmup_steps"] = ( + self.cfg.relora_warmup_steps + ) + if self.cfg.relora_anneal_steps: + training_arguments_kwargs["relora_anneal_steps"] = ( + self.cfg.relora_anneal_steps + ) + if self.cfg.relora_prune_ratio: + training_arguments_kwargs["relora_prune_ratio"] = ( + self.cfg.relora_prune_ratio + ) + + if self.cfg.lisa_step_interval and self.cfg.lisa_n_layers: + training_arguments_kwargs["lisa_n_layers"] = self.cfg.lisa_n_layers + training_arguments_kwargs["lisa_step_interval"] = ( + self.cfg.lisa_step_interval + ) + training_arguments_kwargs["lisa_layers_attribute"] = ( + self.cfg.lisa_layers_attribute + ) + + training_arguments_kwargs = self.hook_pre_create_training_args( + training_arguments_kwargs + ) + training_arguments_kwargs["model_type"] = self.cfg.model_config_type + training_arguments_kwargs["pretraining"] = bool(self.cfg.pretraining_dataset) + if self.cfg.chat_template: + training_arguments_kwargs["chat_template"] = get_chat_template_from_config( + cfg=self.cfg, + tokenizer=self.tokenizer, + ) + + if self.cfg.neftune_noise_alpha is not None: + training_arguments_kwargs["neftune_noise_alpha"] = ( + self.cfg.neftune_noise_alpha + ) + + if self.cfg.accelerator_config: + training_arguments_kwargs["accelerator_config"] = ( + self.cfg.accelerator_config + ) + + if self.cfg.image_size: + training_arguments_kwargs["image_size"] = self.cfg.image_size + if self.cfg.image_resize_algorithm: + training_arguments_kwargs["image_resize_algorithm"] = ( + self.cfg.image_resize_algorithm + ) + if self.cfg.kd_ce_alpha is not None: + training_arguments_kwargs["kd_ce_alpha"] = self.cfg.kd_ce_alpha + if self.cfg.kd_alpha is not None: + training_arguments_kwargs["kd_alpha"] = self.cfg.kd_alpha + if self.cfg.kd_temperature is not None: + training_arguments_kwargs["kd_temperature"] = self.cfg.kd_temperature + if self.cfg.kd_zscore_base_temp is not None: + training_arguments_kwargs["kd_zscore_base_temp"] = ( + self.cfg.kd_zscore_base_temp + ) + if self.cfg.kd_top_k_before_softmax is not None: + training_arguments_kwargs["kd_top_k_before_softmax"] = ( + self.cfg.kd_top_k_before_softmax + ) + + if self.cfg.reward_model: + training_args_cls = AxolotlRewardConfig + elif self.cfg.process_reward_model: + training_args_cls = AxolotlPRMConfig + else: + training_args_cls = AxolotlTrainingArguments + training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg + **training_arguments_kwargs, + ) + training_args = self.hook_post_create_training_args(training_args) + + # unset run_name so wandb sets up experiment names + if self.cfg.use_wandb and training_args.run_name == training_args.output_dir: + training_args.run_name = ( # pylint: disable=attribute-defined-outside-init + None + ) + + data_collator_kwargs = { + "padding": True, # True/"longest" is the default + } + multiple = 64 + if self.cfg.pad_to_sequence_len: + data_collator_kwargs["pad_to_multiple_of"] = multiple * math.ceil( + self.cfg.sequence_len / multiple + ) + else: + # A100 is best at 64, while others at 8. Let's use the larger so we don't have to check + # https://docs.nvidia.com/deeplearning/performance/dl-performance-matrix-multiplication/index.html + data_collator_kwargs["pad_to_multiple_of"] = multiple + + trainer_cls = self._get_trainer_cls() + + trainer_kwargs, trainer_cls = self.hook_pre_create_trainer( + trainer_kwargs, trainer_cls + ) + if eval_data_collator := self.build_collator( + training_args, is_eval=True, **data_collator_kwargs + ): + if not (self.cfg.reward_model or self.cfg.process_reward_model): + trainer_kwargs["eval_data_collator"] = eval_data_collator + if not (self.cfg.reward_model or self.cfg.process_reward_model): + trainer_kwargs["bench_data_collator"] = transformers.DataCollatorForSeq2Seq( + self.tokenizer, + return_tensors="pt", + **data_collator_kwargs, + ) + sig = inspect.signature(trainer_cls) + if "processing_class" in sig.parameters: + trainer_kwargs["processing_class"] = self.tokenizer + elif "tokenizer" in sig.parameters: + trainer_kwargs["tokenizer"] = self.tokenizer + if ( + not (trainer_cls in [AxolotlRewardTrainer, AxolotlPRMTrainer]) + and self.cfg.datasets is not None + ): + trainer_kwargs["dataset_tags"] = [ + d["path"] for d in self.cfg.datasets if not Path(d["path"]).is_dir() + ] + trainer = trainer_cls( + model=self.model, + train_dataset=self.train_dataset, + eval_dataset=self.eval_dataset, + args=training_args, + data_collator=self.build_collator(training_args, **data_collator_kwargs), + callbacks=self.get_callbacks(), + **trainer_kwargs, + ) + trainer = self.hook_post_create_trainer(trainer) + for callback in self.get_post_trainer_create_callbacks(trainer): + trainer.add_callback(callback) + + if self.cfg.deepspeed and self.cfg.sample_packing: + trainer.accelerator.state.deepspeed_plugin.deepspeed_config[ + "train_micro_batch_size_per_gpu" + ] = self.cfg.micro_batch_size + + return trainer + + def build_collator( + self, training_args: AxolotlTrainingArguments, is_eval=False, **kwargs + ): + if training_args.pretraining: + if ( + self.cfg.pretraining_sample_concatenation is False + or self.cfg.micro_batch_size > 1 + ): + return DataCollatorForSeq2Seq(self.tokenizer, **kwargs) + return None + + if self.cfg.model_config_type == "mamba": + return MambaDataCollator(tokenizer=self.tokenizer) + + use_batch_sampler_collator = False + if is_eval is False and training_args.sample_packing: + use_batch_sampler_collator = True + if is_eval and training_args.eval_sample_packing: + use_batch_sampler_collator = True + + collator: Type[ + Union[ + V2BatchSamplerDataCollatorForSeq2Seq, + BatchSamplerDataCollatorForSeq2Seq, + DataCollatorForSeq2Seq, + DataCollatorWithFlattening, + RewardDataCollatorWithPadding, + ] + ] + collator_args = [self.tokenizer] + if self.cfg.reward_model: + collator = RewardDataCollatorWithPadding + elif use_batch_sampler_collator: + # Use V2BatchSamplerDataCollatorForSeq2Seq for flex attention, + # supported multipack models, or non-flash-attention llama + if ( + self.cfg.flex_attention + or self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES + or ( + self.cfg.model_config_type in ["llama"] + and self.cfg.flash_attention is not True + ) + ): + collator = V2BatchSamplerDataCollatorForSeq2Seq + else: + collator = BatchSamplerDataCollatorForSeq2Seq + else: + if self.cfg.processor_type and self.processor: + collator = MultiModalChatDataCollator + kwargs["processing_strategy"] = get_processing_strategy( + self.processor, + training_args.chat_template, + self.cfg.chat_template, + image_size=training_args.image_size, + image_resize_algorithm=training_args.image_resize_algorithm, + ) + elif self.cfg.batch_flattening: + collator = DataCollatorWithFlattening + collator_args.pop(0) + kwargs.pop("pad_to_multiple_of", None) + kwargs.pop("padding", None) + elif self.cfg.kd_trainer: + from axolotl.integrations.kd.collator import ( + DataCollatorForKD, + KDBatchSamplerDataCollatorForSeq2Seq, + ) + + if self.cfg.sample_packing: + collator = KDBatchSamplerDataCollatorForSeq2Seq + else: + collator = DataCollatorForKD + else: + collator = DataCollatorForSeq2Seq + + kwargs["return_tensors"] = "pt" + + return collator( + *collator_args, + **kwargs, + ) diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py new file mode 100644 index 0000000000..14dbfa715b --- /dev/null +++ b/src/axolotl/core/builders/rl.py @@ -0,0 +1,246 @@ +"""Builder for RLHF trainers""" + +import inspect +from pathlib import Path + +from axolotl.core.builders.base import TrainerBuilderBase +from axolotl.core.trainers import ( + AxolotlCPOTrainer, + AxolotlKTOTrainer, + AxolotlORPOTrainer, +) +from axolotl.core.trainers.dpo import DPOStrategy +from axolotl.core.trainers.dpo.args import AxolotlDPOConfig +from axolotl.core.trainers.grpo import GRPOStrategy +from axolotl.core.training_args import ( + AxolotlCPOConfig, + AxolotlKTOConfig, + AxolotlORPOConfig, +) +from axolotl.integrations.base import PluginManager +from axolotl.loaders.utils import ensure_dtype +from axolotl.utils.logging import get_logger +from axolotl.utils.schemas.enums import RLType + +LOG = get_logger(__name__) + + +class HFRLTrainerBuilder(TrainerBuilderBase): + """Trainer factory class for TRL-based RLHF trainers (e.g. DPO)""" + + def get_callbacks(self): + callbacks = super().get_callbacks() + + return callbacks + + def get_post_trainer_create_callbacks(self, trainer): + callbacks = super().get_post_trainer_create_callbacks(trainer=trainer) + return callbacks + + def _get_trainer_cls(self, trainer_kwargs: dict): + """ + Returns trainer_cls and trainer_cls_args + """ + if self.cfg.plugins: + plugin_manager = PluginManager.get_instance() + trainer_cls = plugin_manager.get_trainer_cls(self.cfg) + trainer_cls_args = [] # type: ignore + + if trainer_cls is not None: + return trainer_cls, trainer_cls_args + + trainer_cls = None + trainer_cls_args = [self.model] + + if self.cfg.rl is RLType.GRPO: + trainer_cls = GRPOStrategy.get_trainer_class( + sequence_parallel=self.cfg.sequence_parallel_degree > 1 + ) + trainer_cls_args.extend(GRPOStrategy.set_trainer_args(self.cfg)) + + trainer_kwargs.update(GRPOStrategy.set_trainer_kwargs(self.cfg)) + + elif self.cfg.rl in [RLType.DPO, RLType.IPO]: + trainer_cls = DPOStrategy.get_trainer_class() + trainer_cls_args.append(self.model_ref) + + elif self.cfg.rl is RLType.ORPO: + trainer_cls = AxolotlORPOTrainer + elif self.cfg.rl is RLType.KTO: + trainer_cls = AxolotlKTOTrainer + elif self.cfg.rl is RLType.SIMPO: + trainer_cls = AxolotlCPOTrainer + else: + raise ValueError(f"Unsupported RL: {self.cfg.rl}") + + return trainer_cls, trainer_cls_args + + def _build_training_arguments(self, total_num_steps): + """ + Returns training_args and trainer_kwargs + """ + training_args_kwargs, trainer_kwargs = self._set_base_training_args( + total_num_steps=total_num_steps + ) + + if self.cfg.remove_unused_columns is not None: + training_args_kwargs["remove_unused_columns"] = ( + self.cfg.remove_unused_columns + ) + else: + training_args_kwargs["remove_unused_columns"] = False + + # only rlhf + if self.cfg.dataset_processes: + training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes + + if self.cfg.trl and self.cfg.trl.beta is not None: + training_args_kwargs["beta"] = self.cfg.trl.beta + elif self.cfg.rl_beta is not None: + training_args_kwargs["beta"] = self.cfg.rl_beta + elif self.cfg.orpo_alpha is not None: + # trl does some odd mapping of alpha to beta to reuse the beta parameter ??? + training_args_kwargs["beta"] = self.cfg.orpo_alpha + + if self.cfg.rpo_alpha is not None: + training_args_kwargs["rpo_alpha"] = self.cfg.rpo_alpha + + if self.cfg.use_wandb: + training_args_kwargs["run_name"] = self.cfg.wandb_name + + training_args_cls = None + blocklist_args_kwargs = [] + if self.cfg.rl is RLType.SIMPO: + training_args_cls = AxolotlCPOConfig + training_args_kwargs["loss_type"] = "simpo" + training_args_kwargs["simpo_gamma"] = self.cfg.simpo_gamma + if self.cfg.cpo_alpha is not None: + training_args_kwargs["cpo_alpha"] = self.cfg.cpo_alpha + + elif self.cfg.rl is RLType.ORPO: + training_args_cls = AxolotlORPOConfig + if self.cfg.max_prompt_len: + training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len + + elif self.cfg.rl is RLType.KTO: + training_args_cls = AxolotlKTOConfig + + training_args_kwargs["desirable_weight"] = ( + self.cfg.kto_desirable_weight or 1.0 + ) + training_args_kwargs["undesirable_weight"] = ( + self.cfg.kto_undesirable_weight or 1.0 + ) + + if self.cfg.max_prompt_len: + training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len + + elif self.cfg.rl is RLType.GRPO: + training_args_cls = GRPOStrategy.get_training_args_class() + training_args_kwargs.update(GRPOStrategy.set_training_args_kwargs(self.cfg)) + blocklist_args_kwargs = GRPOStrategy.get_blocklist_args_kwargs() + + elif self.cfg.rl in [RLType.DPO, RLType.IPO]: + training_args_cls = AxolotlDPOConfig + if self.cfg.rl is RLType.IPO: + training_args_kwargs["loss_type"] = "ipo" + + # Not compatible with IPO + if self.cfg.rl is RLType.DPO and self.cfg.dpo_label_smoothing: + training_args_kwargs["label_smoothing"] = self.cfg.dpo_label_smoothing + + training_args_kwargs["max_completion_length"] = None + training_args_kwargs["max_prompt_length"] = self.cfg.sequence_len + training_args_kwargs["generate_during_eval"] = self.cfg.use_wandb + if self.cfg.dpo_use_weighting is not None: + training_args_kwargs["use_weighting"] = self.cfg.dpo_use_weighting + if self.cfg.dpo_use_logits_to_keep is not None: + training_args_kwargs["use_logits_to_keep"] = ( + self.cfg.dpo_use_logits_to_keep + ) + else: + raise ValueError(f"Unsupported RL: {self.cfg.rl}") + + for blocklist_key in blocklist_args_kwargs: + if blocklist_key in training_args_kwargs: + del training_args_kwargs[blocklist_key] + + training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg + logging_first_step=True, + **training_args_kwargs, + ) + + # unset run_name so wandb sets up experiment names + if self.cfg.use_wandb and training_args.run_name == training_args.output_dir: + training_args.run_name = ( # pylint: disable=attribute-defined-outside-init + None + ) + + return training_args, trainer_kwargs + + def build(self, total_num_steps): + training_args, trainer_kwargs = self._build_training_arguments(total_num_steps) + + if self.eval_dataset: + trainer_kwargs["eval_dataset"] = self.eval_dataset + if self.cfg.adapter and self.peft_config and self.cfg.rl is not RLType.GRPO: + trainer_kwargs["peft_config"] = self.peft_config + if self.cfg.precompute_ref_log_probs is not None: + trainer_kwargs["precompute_ref_log_probs"] = ( + self.cfg.precompute_ref_log_probs + ) + + trainer_cls, trainer_cls_args = self._get_trainer_cls(trainer_kwargs) + + sig = inspect.signature(trainer_cls) + if "tokenizer" in sig.parameters: + trainer_kwargs["tokenizer"] = self.tokenizer + else: + trainer_kwargs["processing_class"] = self.tokenizer + + if self.cfg.datasets is not None and ( + trainer_cls is DPOStrategy.get_trainer_class() + ): + trainer_kwargs["dataset_tags"] = [ + d["path"] for d in self.cfg.datasets if not Path(d["path"]).is_dir() + ] + + trainer_kwargs, trainer_cls = self.hook_pre_create_trainer( + trainer_kwargs, trainer_cls + ) + + trainer = trainer_cls( + *trainer_cls_args, + args=training_args, + train_dataset=self.train_dataset, + callbacks=self.get_callbacks(), + **trainer_kwargs, + ) + if self.cfg.fsdp: + ensure_dtype(trainer.model, dtype=self.cfg.torch_dtype) + if self.cfg.rl in [RLType.DPO, RLType.IPO] and trainer.ref_model: + ensure_dtype(trainer.ref_model, dtype=self.cfg.torch_dtype) + + trainer = self.hook_post_create_trainer(trainer) + for callback in self.get_post_trainer_create_callbacks(trainer): + trainer.add_callback(callback) + + return trainer + + +class HFPPOTrainerBuilder(TrainerBuilderBase): + """ + HF Factory class for PPO Trainer + """ + + def get_callbacks(self): + callbacks = super().get_callbacks() + return callbacks + + def get_post_trainer_create_callbacks(self, trainer): + callbacks = super().get_post_trainer_create_callbacks(trainer=trainer) + return callbacks + + def build(self, total_num_steps): + # TODO: build PPOConfig + raise NotImplementedError("PPO trainer builder is not implemented yet.") diff --git a/src/axolotl/core/trainer_builder.py b/src/axolotl/core/trainer_builder.py deleted file mode 100755 index 46ec12ccb6..0000000000 --- a/src/axolotl/core/trainer_builder.py +++ /dev/null @@ -1,1252 +0,0 @@ -# Copyright 2024 Axolotl AI. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=too-many-lines -"""Builder for the training args and trainer""" - -import abc -import importlib -import importlib.util -import inspect -import math -import os -import sys -from abc import abstractmethod -from pathlib import Path -from typing import List, Type, Union - -import torch -import transformers -from transformers import ( - DataCollatorWithFlattening, - EarlyStoppingCallback, - TrainerCallback, -) -from transformers.training_args import OptimizerNames -from trl.trainer.utils import RewardDataCollatorWithPadding - -from axolotl.core.trainers import ( - AxolotlCPOTrainer, - AxolotlKTOTrainer, - AxolotlMambaTrainer, - AxolotlORPOTrainer, - AxolotlPRMTrainer, - AxolotlRewardTrainer, - AxolotlTrainer, - ReLoRATrainer, -) -from axolotl.core.trainers.dpo import DPOStrategy -from axolotl.core.trainers.dpo.args import AxolotlDPOConfig -from axolotl.core.trainers.grpo import GRPOStrategy -from axolotl.core.training_args import ( - AxolotlCPOConfig, - AxolotlKTOConfig, - AxolotlORPOConfig, - AxolotlPRMConfig, - AxolotlRewardConfig, - AxolotlTrainingArguments, -) -from axolotl.integrations.base import PluginManager -from axolotl.loaders.utils import ensure_dtype -from axolotl.monkeypatch.multipack import SUPPORTED_MULTIPACK_MODEL_TYPES -from axolotl.monkeypatch.relora import ReLoRACallback -from axolotl.monkeypatch.trainer.lr import patch_trainer_get_lr -from axolotl.processing_strategies import get_processing_strategy -from axolotl.utils import is_comet_available, is_mlflow_available -from axolotl.utils.callbacks import ( - EvalFirstStepCallback, - GCCallback, - GPUStatsCallback, - LossWatchDogCallback, - SaveAxolotlConfigtoWandBCallback, - SaveBetterTransformerModelCallback, - bench_eval_callback_factory, - causal_lm_bench_eval_callback_factory, - colab_inference_post_train_callback, - log_prediction_callback_factory, -) -from axolotl.utils.callbacks.lisa import lisa_callback_factory -from axolotl.utils.callbacks.profiler import PytorchProfilerCallback -from axolotl.utils.callbacks.qat import QATCallback -from axolotl.utils.chat_templates import get_chat_template_from_config -from axolotl.utils.collators import ( - BatchSamplerDataCollatorForSeq2Seq, - DataCollatorForSeq2Seq, - MambaDataCollator, - V2BatchSamplerDataCollatorForSeq2Seq, -) -from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator -from axolotl.utils.logging import get_logger -from axolotl.utils.schemas.enums import CustomSupportedOptimizers, RLType - -try: - import torch._dynamo # pylint: disable=ungrouped-imports -except ImportError: - pass - -LOG = get_logger(__name__) - - -class TrainerBuilderBase(abc.ABC): - """Base class for trainer builder.""" - - _train_dataset = None - _eval_dataset = None - _model_ref = None - _peft_config = None - - def __init__(self, cfg, model, tokenizer, processor=None): - self.cfg = cfg - self.model = model - self.tokenizer = tokenizer - self.processor = processor - - # If the model supports tagging, add the axolotl tag. - # This makes sure the tag is correctly pushed even if a user calls - # model.push_to_hub instead of trainer.push_to_hub. - if hasattr(model, "add_model_tags"): - model.add_model_tags(["axolotl"]) - - patch_trainer_get_lr() - - @property - def model_ref(self): - return self._model_ref - - @model_ref.setter - def model_ref(self, model): - self._model_ref = model - - @property - def train_dataset(self): - return self._train_dataset - - @train_dataset.setter - def train_dataset(self, dataset): - self._train_dataset = dataset - - @property - def eval_dataset(self): - return self._eval_dataset - - @eval_dataset.setter - def eval_dataset(self, dataset): - self._eval_dataset = dataset - - @property - def peft_config(self): - return self._peft_config - - @peft_config.setter - def peft_config(self, peft_config): - self._peft_config = peft_config - - @abstractmethod - def build(self, total_num_steps): - pass - - def get_callbacks(self) -> List[TrainerCallback]: - callbacks = [] - - plugin_manager = PluginManager.get_instance() - callbacks.extend( - plugin_manager.add_callbacks_pre_trainer(cfg=self.cfg, model=self.model) - ) - - if self.cfg.profiler_steps: - callbacks.append( - PytorchProfilerCallback( - steps_to_profile=self.cfg.profiler_steps, - ) - ) - - if self.cfg.gc_steps: - callbacks.append(GCCallback(gc_steps=self.cfg.gc_steps)) - - if self.cfg.use_wandb: - callbacks.append( - SaveAxolotlConfigtoWandBCallback(self.cfg.axolotl_config_path) - ) - if self.cfg.use_mlflow and is_mlflow_available(): - from axolotl.utils.callbacks.mlflow_ import ( - SaveAxolotlConfigtoMlflowCallback, - ) - - callbacks.extend( - [ - SaveAxolotlConfigtoMlflowCallback(self.cfg.axolotl_config_path), - ] - ) - if self.cfg.use_comet and is_comet_available(): - from axolotl.utils.callbacks.comet_ import SaveAxolotlConfigtoCometCallback - - callbacks.append( - SaveAxolotlConfigtoCometCallback(self.cfg.axolotl_config_path) - ) - - return callbacks - - def get_post_trainer_create_callbacks(self, trainer): - """ - Callbacks added after the trainer is created, usually b/c these need access to the trainer - """ - callbacks = [] - if self.cfg.plugins: - plugin_manager = PluginManager.get_instance() - callbacks.extend( - [ - cb - for cb in plugin_manager.add_callbacks_post_trainer( - self.cfg, trainer - ) - if cb - ] - ) - return callbacks - - def hook_pre_create_training_args(self, training_arguments_kwargs): - # TODO - return training_arguments_kwargs - - def hook_post_create_training_args(self, training_arguments): - # TODO - return training_arguments - - def hook_pre_create_trainer(self, trainer_kwargs, trainer_cls): - # TODO - return trainer_kwargs, trainer_cls - - def hook_post_create_trainer(self, trainer): - # TODO - return trainer - - -class HFCausalTrainerBuilder(TrainerBuilderBase): - """ - Build the HuggingFace training args/trainer for causal models and reward modeling - using TRL. - """ - - def get_callbacks(self): - callbacks = super().get_callbacks() - callbacks.append(GPUStatsCallback(self.cfg)) - callbacks.append(EvalFirstStepCallback()) - - if self.cfg.relora_steps: - callbacks.append(ReLoRACallback(self.cfg)) - - if ( - hasattr(self.model, "use_bettertransformer") - and self.model.use_bettertransformer is True - ): - callbacks.append(SaveBetterTransformerModelCallback()) - - if self.cfg.loss_watchdog_threshold is not None: - callbacks.append(LossWatchDogCallback(self.cfg)) - - if self.cfg.qat: - callbacks.append(QATCallback(self.cfg.qat)) - - return callbacks - - def get_post_trainer_create_callbacks(self, trainer): - callbacks = [] - if self.cfg.use_wandb and self.cfg.eval_table_size > 0: - LogPredictionCallback = log_prediction_callback_factory( - trainer, self.tokenizer, "wandb" - ) - callbacks.append(LogPredictionCallback(self.cfg)) - if ( - self.cfg.use_mlflow - and is_mlflow_available() - and self.cfg.eval_table_size > 0 - ): - LogPredictionCallback = log_prediction_callback_factory( - trainer, self.tokenizer, "mlflow" - ) - callbacks.append(LogPredictionCallback(self.cfg)) - if self.cfg.use_comet and is_comet_available() and self.cfg.eval_table_size > 0: - LogPredictionCallback = log_prediction_callback_factory( - trainer, self.tokenizer, "comet_ml" - ) - callbacks.append(LogPredictionCallback(self.cfg)) - - if self.cfg.do_bench_eval: - callbacks.append(bench_eval_callback_factory(trainer, self.tokenizer)) - if self.cfg.do_causal_lm_eval: - CausalLMBenchEvalCallback = causal_lm_bench_eval_callback_factory( - trainer, self.tokenizer - ) - callbacks.append(CausalLMBenchEvalCallback(self.cfg)) - - if self.cfg.early_stopping_patience: - early_stop_cb = EarlyStoppingCallback( - self.cfg.early_stopping_patience, - ) - callbacks.append(early_stop_cb) - - if self.cfg.lisa_step_interval and self.cfg.lisa_n_layers: - callbacks.append(lisa_callback_factory(trainer)) - - if any("COLAB_" in key for key in os.environ): - ColabCallback = colab_inference_post_train_callback(trainer) - callbacks.append(ColabCallback(self.cfg)) - - callbacks.extend(super().get_post_trainer_create_callbacks(trainer=trainer)) - return callbacks - - def _get_trainer_cls(self): - if self.cfg.plugins: - plugin_manager = PluginManager.get_instance() - trainer_cls = plugin_manager.get_trainer_cls(self.cfg) - if trainer_cls: - return trainer_cls - if self.cfg.relora_steps: - return ReLoRATrainer - if self.cfg.model_config_type == "mamba": - return AxolotlMambaTrainer - if self.cfg.reward_model: - return AxolotlRewardTrainer - if self.cfg.process_reward_model: - return AxolotlPRMTrainer - return AxolotlTrainer - - def build(self, total_num_steps): - warmup_steps = None - if self.cfg.warmup_steps is not None: - warmup_steps = self.cfg.warmup_steps - elif self.cfg.warmup_ratio is not None: - warmup_steps = max(int(self.cfg.warmup_ratio * total_num_steps), 0) - else: - warmup_steps = min(int(0.03 * total_num_steps), 100) - if warmup_steps == 1: - warmup_steps = 2 - - logging_steps = ( - self.cfg.logging_steps - if self.cfg.logging_steps is not None - else max(min(int(0.005 * total_num_steps), 10), 1) - ) - - training_arguments_kwargs = {} - - if self.cfg.include_tokens_per_second is not None: - training_arguments_kwargs["include_tokens_per_second"] = ( - self.cfg.include_tokens_per_second - ) - - if self.cfg.bf16 == "full": - training_arguments_kwargs["bf16_full_eval"] = True - else: - training_arguments_kwargs["bf16"] = self.cfg.bf16 - training_arguments_kwargs["fp16"] = ( - self.cfg.fp16 and not self.cfg.bf16 - ) or False - training_arguments_kwargs["tf32"] = self.cfg.tf32 - training_arguments_kwargs["warmup_steps"] = warmup_steps - training_arguments_kwargs["logging_steps"] = logging_steps - - if self.cfg.seed is not None: - training_arguments_kwargs["seed"] = self.cfg.seed - - if self.cfg.gradient_checkpointing: - training_arguments_kwargs["gradient_checkpointing"] = ( - self.cfg.gradient_checkpointing - ) - if self.cfg.gradient_checkpointing_kwargs is not None: - training_arguments_kwargs["gradient_checkpointing_kwargs"] = ( - self.cfg.gradient_checkpointing_kwargs - ) - if self.cfg.fsdp: - training_arguments_kwargs["fsdp"] = self.cfg.fsdp - if self.cfg.fsdp_config: - training_arguments_kwargs["fsdp_config"] = { - k.lstrip("fsdp_"): v for k, v in dict(self.cfg.fsdp_config).items() - } - - if self.cfg.adapter == "qlora": - training_arguments_kwargs["qlora"] = True - - # deepspeed - if self.cfg.deepspeed: - training_arguments_kwargs["deepspeed"] = self.cfg.deepspeed - - if self.cfg.lr_quadratic_warmup is not None: - training_arguments_kwargs["lr_quadratic_warmup"] = ( - self.cfg.lr_quadratic_warmup - ) - - if self.cfg.adam_beta1: - training_arguments_kwargs["adam_beta1"] = self.cfg.adam_beta1 - if self.cfg.adam_beta2: - training_arguments_kwargs["adam_beta2"] = self.cfg.adam_beta2 - if self.cfg.adam_beta3: - training_arguments_kwargs["adam_beta3"] = self.cfg.adam_beta3 - if self.cfg.adam_epsilon: - training_arguments_kwargs["adam_epsilon"] = self.cfg.adam_epsilon - if self.cfg.adam_epsilon2: - training_arguments_kwargs["adam_epsilon2"] = self.cfg.adam_epsilon2 - if self.cfg.max_grad_norm: - training_arguments_kwargs["max_grad_norm"] = self.cfg.max_grad_norm - - if self.cfg.hub_model_id: - training_arguments_kwargs["hub_model_id"] = self.cfg.hub_model_id - training_arguments_kwargs["push_to_hub"] = True - training_arguments_kwargs["hub_private_repo"] = True - training_arguments_kwargs["hub_always_push"] = True - - if self.cfg.hub_strategy: - training_arguments_kwargs["hub_strategy"] = self.cfg.hub_strategy - - if self.cfg.save_safetensors is not None: - training_arguments_kwargs["save_safetensors"] = self.cfg.save_safetensors - - if self.cfg.dataloader_pin_memory is not None: - training_arguments_kwargs["dataloader_pin_memory"] = ( - self.cfg.dataloader_pin_memory - ) - if self.cfg.dataloader_num_workers is not None: - training_arguments_kwargs["dataloader_num_workers"] = ( - self.cfg.dataloader_num_workers - ) - if self.cfg.dataloader_prefetch_factor is not None: - training_arguments_kwargs["dataloader_prefetch_factor"] = ( - self.cfg.dataloader_prefetch_factor - ) - if self.cfg.dataloader_drop_last is not None: - training_arguments_kwargs["dataloader_drop_last"] = ( - self.cfg.dataloader_drop_last - ) - elif self.cfg.sample_packing and self.cfg.eval_sample_packing is False: - training_arguments_kwargs["dataloader_drop_last"] = True - - if self.cfg.remove_unused_columns is not None: - training_arguments_kwargs["remove_unused_columns"] = ( - self.cfg.remove_unused_columns - ) - - if not self.cfg.test_datasets and self.cfg.val_set_size == 0: - # no eval set, so don't eval - training_arguments_kwargs["eval_strategy"] = "no" - elif self.cfg.eval_steps: - training_arguments_kwargs["eval_strategy"] = "steps" - training_arguments_kwargs["eval_steps"] = self.cfg.eval_steps - elif self.cfg.eval_strategy: - training_arguments_kwargs["eval_strategy"] = self.cfg.eval_strategy - else: - # we have an eval set, but no steps defined, default to use epoch - training_arguments_kwargs["eval_strategy"] = "epoch" - - if self.cfg.save_steps: - training_arguments_kwargs["save_strategy"] = "steps" - training_arguments_kwargs["save_steps"] = self.cfg.save_steps - elif self.cfg.save_strategy: - training_arguments_kwargs["save_strategy"] = self.cfg.save_strategy - else: - # default to saving each epoch if not defined - training_arguments_kwargs["save_strategy"] = "epoch" - - training_arguments_kwargs["save_only_model"] = self.cfg.save_only_model - - if self.cfg.do_bench_eval: - training_arguments_kwargs["do_bench_eval"] = self.cfg.do_bench_eval - if self.cfg.bench_dataset: - training_arguments_kwargs["bench_dataset"] = self.cfg.bench_dataset - if self.cfg.do_causal_lm_eval: - training_arguments_kwargs["do_causal_lm_eval"] = self.cfg.do_causal_lm_eval - if self.cfg.metric_for_best_model: - training_arguments_kwargs["metric_for_best_model"] = ( - self.cfg.metric_for_best_model - ) - if self.cfg.greater_is_better: - training_arguments_kwargs["greater_is_better"] = self.cfg.greater_is_better - - if self.cfg.torch_compile: - if torch.__version__ < "2.1.0": # pylint: disable=protected-access - LOG.warning("torch>=2.1.0 required for torch_compile to work properly") - elif torch._dynamo: # pylint: disable=protected-access - torch._dynamo.config.suppress_errors = ( # pylint: disable=protected-access - True - ) - training_arguments_kwargs["torch_compile"] = self.cfg.torch_compile - if self.cfg.torch_compile_backend: - training_arguments_kwargs["torch_compile_backend"] = ( - self.cfg.torch_compile_backend - ) - if self.cfg.torch_compile_mode: - training_arguments_kwargs["torch_compile_mode"] = ( - self.cfg.torch_compile_mode - ) - - # DDP Config - if self.cfg.ddp_timeout: - training_arguments_kwargs["ddp_timeout"] = self.cfg.ddp_timeout - # see https://pytorch.org/docs/stable/generated/torch.nn.parallel.DistributedDataParallel.html - if self.cfg.ddp_bucket_cap_mb: - training_arguments_kwargs["ddp_bucket_cap_mb"] = self.cfg.ddp_bucket_cap_mb - if self.cfg.ddp_broadcast_buffers is not None: - training_arguments_kwargs["ddp_broadcast_buffers"] = ( - self.cfg.ddp_broadcast_buffers - ) - - # these are all the "standard" kwargs that are def used - training_arguments_kwargs["max_steps"] = ( - self.cfg.max_steps if self.cfg.max_steps else -1 - ) - training_arguments_kwargs["max_seq_length"] = self.cfg.sequence_len - training_arguments_kwargs["per_device_train_batch_size"] = ( - self.cfg.micro_batch_size - ) - if self.cfg.eval_batch_size: - training_arguments_kwargs["per_device_eval_batch_size"] = ( - self.cfg.eval_batch_size - ) - if self.cfg.auto_find_batch_size is not None: - training_arguments_kwargs["auto_find_batch_size"] = ( - self.cfg.auto_find_batch_size - ) - training_arguments_kwargs["gradient_accumulation_steps"] = ( - self.cfg.gradient_accumulation_steps - ) - training_arguments_kwargs["eval_accumulation_steps"] = ( - self.cfg.gradient_accumulation_steps - ) - training_arguments_kwargs["num_train_epochs"] = self.cfg.num_epochs - training_arguments_kwargs["learning_rate"] = self.cfg.learning_rate - training_arguments_kwargs["output_dir"] = self.cfg.output_dir - training_arguments_kwargs["save_total_limit"] = ( - self.cfg.save_total_limit if self.cfg.save_total_limit else 4 - ) - training_arguments_kwargs["load_best_model_at_end"] = ( - ( - self.cfg.load_best_model_at_end is not False - or self.cfg.early_stopping_patience - ) - and ( - (not self.cfg.test_datasets and self.cfg.val_set_size > 0) - or (self.cfg.test_datasets and self.cfg.val_set_size == 0) - ) - and self.cfg.save_steps - and self.cfg.eval_steps - and self.cfg.save_steps % self.cfg.eval_steps == 0 - ) or False - - # handle ddp - ddp_find_unused_parameters = None - if self.cfg.ddp: - ddp_find_unused_parameters = bool(self.cfg.ddp_find_unused_parameters) - training_arguments_kwargs["ddp_find_unused_parameters"] = ( - ddp_find_unused_parameters - ) - - training_arguments_kwargs["group_by_length"] = self.cfg.group_by_length - training_arguments_kwargs["curriculum_sampling"] = self.cfg.curriculum_sampling - report_to = [] - if self.cfg.use_wandb: - report_to.append("wandb") - if self.cfg.use_mlflow: - report_to.append("mlflow") - if self.cfg.use_tensorboard: - report_to.append("tensorboard") - if self.cfg.use_comet: - report_to.append("comet_ml") - - training_arguments_kwargs["report_to"] = report_to - if self.cfg.use_wandb: - training_arguments_kwargs["run_name"] = self.cfg.wandb_name - elif self.cfg.use_mlflow: - training_arguments_kwargs["run_name"] = self.cfg.mlflow_run_name - else: - training_arguments_kwargs["run_name"] = None - - if self.cfg.lr_scheduler in ["one_cycle", "rex", "log_sweep"]: - training_arguments_kwargs["lr_scheduler_type"] = "cosine" - training_arguments_kwargs["alternate_lr_scheduler_type"] = ( - self.cfg.lr_scheduler - ) - else: - training_arguments_kwargs["lr_scheduler_type"] = ( - self.cfg.lr_scheduler if self.cfg.lr_scheduler else "cosine" - ) - training_arguments_kwargs["lr_scheduler_kwargs"] = ( - self.cfg.lr_scheduler_kwargs if self.cfg.lr_scheduler_kwargs else {} - ) - training_arguments_kwargs["cosine_min_lr_ratio"] = self.cfg.cosine_min_lr_ratio - training_arguments_kwargs["cosine_constant_lr_ratio"] = ( - self.cfg.cosine_constant_lr_ratio - ) - training_arguments_kwargs["weight_decay"] = ( - self.cfg.weight_decay if self.cfg.weight_decay is not None else 0.0 - ) - - training_arguments_kwargs["sample_packing"] = bool(self.cfg.sample_packing) - training_arguments_kwargs["multipack_real_batches"] = ( - not self.cfg.flash_attention or self.cfg.multipack_real_batches - ) - training_arguments_kwargs["eval_sample_packing"] = bool( - self.cfg.eval_sample_packing - ) - if self.cfg.sample_packing_bin_size is not None: - training_arguments_kwargs["sample_packing_bin_size"] = ( - self.cfg.sample_packing_bin_size - ) - if self.cfg.sample_packing_group_size is not None: - training_arguments_kwargs["sample_packing_group_size"] = ( - self.cfg.sample_packing_group_size - ) - if self.cfg.sample_packing_eff_est: - training_arguments_kwargs["sample_packing_efficiency"] = ( - self.cfg.sample_packing_eff_est - ) - - if self.cfg.relora_steps: - training_arguments_kwargs["relora_steps"] = self.cfg.relora_steps - training_arguments_kwargs["relora_warmup_steps"] = ( - self.cfg.relora_warmup_steps - ) - if self.cfg.relora_anneal_steps: - training_arguments_kwargs["relora_anneal_steps"] = ( - self.cfg.relora_anneal_steps - ) - if self.cfg.relora_prune_ratio: - training_arguments_kwargs["relora_prune_ratio"] = ( - self.cfg.relora_prune_ratio - ) - - if self.cfg.lisa_step_interval and self.cfg.lisa_n_layers: - training_arguments_kwargs["lisa_n_layers"] = self.cfg.lisa_n_layers - training_arguments_kwargs["lisa_step_interval"] = ( - self.cfg.lisa_step_interval - ) - training_arguments_kwargs["lisa_layers_attribute"] = ( - self.cfg.lisa_layers_attribute - ) - - training_arguments_kwargs = self.hook_pre_create_training_args( - training_arguments_kwargs - ) - training_arguments_kwargs["model_type"] = self.cfg.model_config_type - training_arguments_kwargs["pretraining"] = bool(self.cfg.pretraining_dataset) - if self.cfg.chat_template: - training_arguments_kwargs["chat_template"] = get_chat_template_from_config( - cfg=self.cfg, - tokenizer=self.tokenizer, - ) - - if self.cfg.neftune_noise_alpha is not None: - training_arguments_kwargs["neftune_noise_alpha"] = ( - self.cfg.neftune_noise_alpha - ) - - trainer_kwargs = {} - - if self.cfg.reward_model: - training_arguments_kwargs["max_length"] = self.cfg.sequence_len - - # Handle custom optimizer - custom_supported_optimizers = [opt.value for opt in CustomSupportedOptimizers] - if self.cfg.optimizer in custom_supported_optimizers: - # Common optimizer kwargs - optimizer_kwargs = { - "lr": training_arguments_kwargs.get("learning_rate"), - "weight_decay": training_arguments_kwargs.get("weight_decay"), - } - - # Adam-specific kwargs - adam_kwargs = {} - if training_arguments_kwargs.get( - "adam_beta1" - ) and training_arguments_kwargs.get("adam_beta2"): - adam_kwargs["betas"] = ( - training_arguments_kwargs.get("adam_beta1"), - training_arguments_kwargs.get("adam_beta2"), - ) - if training_arguments_kwargs.get("adam_epsilon"): - adam_kwargs["eps"] = training_arguments_kwargs.get("adam_epsilon") - - if self.cfg.optimizer == "muon": - from axolotl.contribs.mit.muon import ( # pylint: disable=no-name-in-module - MuonOptimizerFactory, - ) - - optimizer_cls = MuonOptimizerFactory - optimizer_kwargs.update(adam_kwargs) - elif self.cfg.optimizer == "optimi_adamw": - from optimi import AdamW - - optimizer_kwargs["foreach"] = False - optimizer_cls = AdamW - optimizer_kwargs.update(adam_kwargs) - elif self.cfg.optimizer == "ao_adamw_4bit": - # TODO remove 20250401 - from torchao.prototype.low_bit_optim import AdamW4bit - - optimizer_cls = AdamW4bit - optimizer_kwargs.update(adam_kwargs) - - LOG.warning( - f"`ao_adamw_4bit` will be deprecated soon. Please use `{OptimizerNames.ADAMW_TORCH_4BIT}` instead." - ) - elif self.cfg.optimizer == "ao_adamw_8bit": - from torchao.prototype.low_bit_optim import AdamW8bit - - optimizer_cls = AdamW8bit - optimizer_kwargs.update(adam_kwargs) - elif self.cfg.optimizer == "ao_adamw_fp8": - from torchao.prototype.low_bit_optim import AdamWFp8 - - optimizer_cls = AdamWFp8 - optimizer_kwargs.update(adam_kwargs) - elif self.cfg.optimizer == "adopt_adamw": - from axolotl.utils.optimizers.adopt import ADOPT - - optimizer_cls = ADOPT - adam_kwargs["decouple"] = True - optimizer_kwargs.update(adam_kwargs) - elif self.cfg.optimizer == "came_pytorch": - from came_pytorch import CAME - - optimizer_cls = CAME - - beta1 = training_arguments_kwargs.get("adam_beta1", 0.9) - beta2 = training_arguments_kwargs.get("adam_beta2", 0.999) - beta3 = training_arguments_kwargs.get("adam_beta3", 0.9999) - eps1 = training_arguments_kwargs.get("adam_epsilon", 1e-30) - eps2 = training_arguments_kwargs.get("adam_epsilon2", 1e-16) - adam_kwargs["betas"] = (beta1, beta2, beta3) - adam_kwargs["eps"] = (eps1, eps2) - - optimizer_kwargs.update(adam_kwargs) - - # Parse any additional optimizer args from config - if self.cfg.optim_args: - if isinstance(self.cfg.optim_args, dict): - optimizer_kwargs.update(self.cfg.optim_args) - else: - # Parse string format "key1=value1,key2=value2" - for mapping in self.cfg.optim_args.replace(" ", "").split(","): - key, value = mapping.split("=") - optimizer_kwargs[key] = value - - trainer_kwargs["optimizer_cls_and_kwargs"] = ( - optimizer_cls, - optimizer_kwargs, - ) - else: - # Use transformers' optimizer - training_arguments_kwargs["optim"] = self.cfg.optimizer - - # Parse any additional optimizer args from config - if self.cfg.optim_args: - if isinstance(self.cfg.optim_args, dict): - optim_args = ",".join( - [f"{key}={value}" for key, value in self.cfg.optim_args.items()] - ) - else: - optim_args = self.cfg.optim_args - training_arguments_kwargs["optim_args"] = optim_args - - if self.cfg.optimizer == "adamw_anyprecision": - if Path(self.cfg.torchdistx_path).exists(): - sys.path.append(self.cfg.torchdistx_path) - importlib.import_module("torchdistx") - - if self.cfg.optim_target_modules: - training_arguments_kwargs["optim_target_modules"] = ( - self.cfg.optim_target_modules - ) - - training_arguments_kwargs["embedding_lr"] = self.cfg.embedding_lr - training_arguments_kwargs["embedding_lr_scale"] = self.cfg.embedding_lr_scale - - training_arguments_kwargs["loraplus_lr_ratio"] = self.cfg.loraplus_lr_ratio - training_arguments_kwargs["loraplus_lr_embedding"] = ( - self.cfg.loraplus_lr_embedding - ) - training_arguments_kwargs["lr_groups"] = self.cfg.lr_groups - - if self.cfg.accelerator_config: - training_arguments_kwargs["accelerator_config"] = ( - self.cfg.accelerator_config - ) - - if self.cfg.image_size: - training_arguments_kwargs["image_size"] = self.cfg.image_size - if self.cfg.image_resize_algorithm: - training_arguments_kwargs["image_resize_algorithm"] = ( - self.cfg.image_resize_algorithm - ) - if self.cfg.kd_ce_alpha is not None: - training_arguments_kwargs["kd_ce_alpha"] = self.cfg.kd_ce_alpha - if self.cfg.kd_alpha is not None: - training_arguments_kwargs["kd_alpha"] = self.cfg.kd_alpha - if self.cfg.kd_temperature is not None: - training_arguments_kwargs["kd_temperature"] = self.cfg.kd_temperature - if self.cfg.kd_zscore_base_temp is not None: - training_arguments_kwargs["kd_zscore_base_temp"] = ( - self.cfg.kd_zscore_base_temp - ) - if self.cfg.kd_top_k_before_softmax is not None: - training_arguments_kwargs["kd_top_k_before_softmax"] = ( - self.cfg.kd_top_k_before_softmax - ) - - if self.cfg.reward_model: - training_args_cls = AxolotlRewardConfig - elif self.cfg.process_reward_model: - training_args_cls = AxolotlPRMConfig - else: - training_args_cls = AxolotlTrainingArguments - training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg - **training_arguments_kwargs, - ) - training_args = self.hook_post_create_training_args(training_args) - - # unset run_name so wandb sets up experiment names - if self.cfg.use_wandb and training_args.run_name == training_args.output_dir: - training_args.run_name = ( # pylint: disable=attribute-defined-outside-init - None - ) - - data_collator_kwargs = { - "padding": True, # True/"longest" is the default - } - multiple = 64 - if self.cfg.pad_to_sequence_len: - data_collator_kwargs["pad_to_multiple_of"] = multiple * math.ceil( - self.cfg.sequence_len / multiple - ) - else: - # A100 is best at 64, while others at 8. Let's use the larger so we don't have to check - # https://docs.nvidia.com/deeplearning/performance/dl-performance-matrix-multiplication/index.html - data_collator_kwargs["pad_to_multiple_of"] = multiple - - if self.cfg.reward_model: - data_collator_kwargs["max_length"] = self.cfg.sequence_len - - trainer_cls = self._get_trainer_cls() - trainer_kwargs, trainer_cls = self.hook_pre_create_trainer( - trainer_kwargs, trainer_cls - ) - if eval_data_collator := self.build_collator( - training_args, is_eval=True, **data_collator_kwargs - ): - if not (self.cfg.reward_model or self.cfg.process_reward_model): - trainer_kwargs["eval_data_collator"] = eval_data_collator - if not (self.cfg.reward_model or self.cfg.process_reward_model): - trainer_kwargs["bench_data_collator"] = transformers.DataCollatorForSeq2Seq( - self.tokenizer, - return_tensors="pt", - **data_collator_kwargs, - ) - sig = inspect.signature(trainer_cls) - if "processing_class" in sig.parameters.keys(): - trainer_kwargs["processing_class"] = self.tokenizer - else: - trainer_kwargs["tokenizer"] = self.tokenizer - if ( - not (trainer_cls in [AxolotlRewardTrainer, AxolotlPRMTrainer]) - and self.cfg.datasets is not None - ): - trainer_kwargs["dataset_tags"] = [ - d["path"] for d in self.cfg.datasets if not Path(d["path"]).is_dir() - ] - trainer = trainer_cls( - model=self.model, - train_dataset=self.train_dataset, - eval_dataset=self.eval_dataset, - args=training_args, - data_collator=self.build_collator(training_args, **data_collator_kwargs), - callbacks=self.get_callbacks(), - **trainer_kwargs, - ) - trainer = self.hook_post_create_trainer(trainer) - for callback in self.get_post_trainer_create_callbacks(trainer): - trainer.add_callback(callback) - - if self.cfg.deepspeed and self.cfg.sample_packing: - trainer.accelerator.state.deepspeed_plugin.deepspeed_config[ - "train_micro_batch_size_per_gpu" - ] = self.cfg.micro_batch_size - - return trainer - - def build_collator( - self, training_args: AxolotlTrainingArguments, is_eval=False, **kwargs - ): - if training_args.pretraining: - if ( - self.cfg.pretraining_sample_concatenation is False - or self.cfg.micro_batch_size > 1 - ): - return DataCollatorForSeq2Seq(self.tokenizer, **kwargs) - return None - - if self.cfg.model_config_type == "mamba": - return MambaDataCollator(tokenizer=self.tokenizer) - - use_batch_sampler_collator = False - if is_eval is False and training_args.sample_packing: - use_batch_sampler_collator = True - if is_eval and training_args.eval_sample_packing: - use_batch_sampler_collator = True - - collator: Type[ - Union[ - V2BatchSamplerDataCollatorForSeq2Seq, - BatchSamplerDataCollatorForSeq2Seq, - DataCollatorForSeq2Seq, - DataCollatorWithFlattening, - RewardDataCollatorWithPadding, - ] - ] - collator_args = [self.tokenizer] - if self.cfg.reward_model: - collator = RewardDataCollatorWithPadding - if "max_length" in kwargs: - kwargs.pop("max_length") - elif use_batch_sampler_collator: - if self.cfg.flex_attention: - collator = V2BatchSamplerDataCollatorForSeq2Seq - elif self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES: - collator = V2BatchSamplerDataCollatorForSeq2Seq - elif ( - self.cfg.model_config_type in ["llama"] - and self.cfg.flash_attention is not True - ): - collator = V2BatchSamplerDataCollatorForSeq2Seq - else: - collator = BatchSamplerDataCollatorForSeq2Seq - else: - if self.cfg.processor_type and self.processor: - collator = MultiModalChatDataCollator - kwargs["processing_strategy"] = get_processing_strategy( - self.processor, - training_args.chat_template, - self.cfg.chat_template, - image_size=training_args.image_size, - image_resize_algorithm=training_args.image_resize_algorithm, - ) - elif self.cfg.batch_flattening: - collator = DataCollatorWithFlattening - collator_args.pop(0) - kwargs.pop("pad_to_multiple_of", None) - kwargs.pop("padding", None) - elif self.cfg.kd_trainer: - from axolotl.integrations.kd.collator import ( - DataCollatorForKD, - KDBatchSamplerDataCollatorForSeq2Seq, - ) - - if self.cfg.sample_packing: - collator = KDBatchSamplerDataCollatorForSeq2Seq - else: - collator = DataCollatorForKD - else: - collator = DataCollatorForSeq2Seq - - kwargs["return_tensors"] = "pt" - - return collator( - *collator_args, - **kwargs, - ) - - -class HFRLTrainerBuilder(TrainerBuilderBase): - """Trainer factory class for TRL-based RLHF trainers (e.g. DPO)""" - - def get_callbacks(self): - callbacks = super().get_callbacks() - - return callbacks - - def get_post_trainer_create_callbacks(self, trainer): - callbacks = super().get_post_trainer_create_callbacks(trainer=trainer) - return callbacks - - def build_training_arguments(self, total_num_steps): - training_args_kwargs = {} - for arg in [ - "adam_beta1", - "adam_beta2", - "adam_epsilon", - "dataloader_num_workers", - "dataloader_pin_memory", - ]: - if hasattr(self.cfg, arg) and getattr(self.cfg, arg) is not None: - training_args_kwargs[arg] = getattr(self.cfg, arg) - - if self.cfg.hub_model_id: - training_args_kwargs["hub_model_id"] = self.cfg.hub_model_id - training_args_kwargs["push_to_hub"] = True - training_args_kwargs["hub_private_repo"] = True - training_args_kwargs["hub_always_push"] = True - - if self.cfg.hub_strategy: - training_args_kwargs["hub_strategy"] = self.cfg.hub_strategy - - if self.cfg.save_safetensors is not None: - training_args_kwargs["save_safetensors"] = self.cfg.save_safetensors - - if self.eval_dataset: - training_args_kwargs["eval_strategy"] = "steps" - training_args_kwargs["eval_steps"] = self.cfg.eval_steps - else: - training_args_kwargs["eval_strategy"] = "no" - - if self.cfg.bf16 or self.cfg.bfloat16: - training_args_kwargs["bf16"] = True - - training_args_kwargs["loraplus_lr_ratio"] = self.cfg.loraplus_lr_ratio - training_args_kwargs["loraplus_lr_embedding"] = self.cfg.loraplus_lr_embedding - training_args_kwargs["lr_scheduler_type"] = ( - self.cfg.lr_scheduler if self.cfg.lr_scheduler else "cosine" - ) - training_args_kwargs["lr_scheduler_kwargs"] = ( - self.cfg.lr_scheduler_kwargs if self.cfg.lr_scheduler_kwargs else {} - ) - if self.cfg.remove_unused_columns is not None: - training_args_kwargs["remove_unused_columns"] = ( - self.cfg.remove_unused_columns - ) - else: - training_args_kwargs["remove_unused_columns"] = False - - if self.cfg.dataloader_pin_memory is not None: - training_args_kwargs["dataloader_pin_memory"] = ( - self.cfg.dataloader_pin_memory - ) - if self.cfg.dataloader_num_workers is not None: - training_args_kwargs["dataloader_num_workers"] = ( - self.cfg.dataloader_num_workers - ) - if self.cfg.dataloader_prefetch_factor is not None: - training_args_kwargs["dataloader_prefetch_factor"] = ( - self.cfg.dataloader_prefetch_factor - ) - - if self.cfg.seed is not None: - training_args_kwargs["seed"] = self.cfg.seed - - if self.cfg.gradient_checkpointing: - training_args_kwargs["gradient_checkpointing"] = ( - self.cfg.gradient_checkpointing - ) - if self.cfg.gradient_checkpointing_kwargs is not None: - training_args_kwargs["gradient_checkpointing_kwargs"] = ( - self.cfg.gradient_checkpointing_kwargs - ) - else: - training_args_kwargs["gradient_checkpointing_kwargs"] = { - "use_reentrant": False - } - - # set save_strategy and save_steps - if self.cfg.save_steps: - training_args_kwargs["save_strategy"] = "steps" - training_args_kwargs["save_steps"] = self.cfg.save_steps - elif self.cfg.save_strategy: - training_args_kwargs["save_strategy"] = self.cfg.save_strategy - else: - # default to saving each epoch if not defined - training_args_kwargs["save_strategy"] = "epoch" - - training_args_kwargs["save_only_model"] = self.cfg.save_only_model - - if self.cfg.dataset_processes: - training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes - - if self.cfg.trl and self.cfg.trl.beta is not None: - training_args_kwargs["beta"] = self.cfg.trl.beta - elif self.cfg.rl_beta is not None: - training_args_kwargs["beta"] = self.cfg.rl_beta - elif self.cfg.orpo_alpha is not None: - # trl does some odd mapping of alpha to beta to reuse the beta parameter ??? - training_args_kwargs["beta"] = self.cfg.orpo_alpha - - if self.cfg.rpo_alpha is not None: - training_args_kwargs["rpo_alpha"] = self.cfg.rpo_alpha - - if self.cfg.use_wandb: - training_args_kwargs["run_name"] = self.cfg.wandb_name - - training_args_cls = None - blocklist_args_kwargs = [] - if self.cfg.rl is RLType.SIMPO: - training_args_cls = AxolotlCPOConfig - training_args_kwargs["loss_type"] = "simpo" - training_args_kwargs["max_length"] = self.cfg.sequence_len - training_args_kwargs["simpo_gamma"] = self.cfg.simpo_gamma - if self.cfg.cpo_alpha is not None: - training_args_kwargs["cpo_alpha"] = self.cfg.cpo_alpha - - elif self.cfg.rl is RLType.ORPO: - training_args_cls = AxolotlORPOConfig - training_args_kwargs["max_length"] = self.cfg.sequence_len - if self.cfg.max_prompt_len: - training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len - - elif self.cfg.rl is RLType.KTO: - training_args_cls = AxolotlKTOConfig - - training_args_kwargs["desirable_weight"] = ( - self.cfg.kto_desirable_weight or 1.0 - ) - training_args_kwargs["undesirable_weight"] = ( - self.cfg.kto_undesirable_weight or 1.0 - ) - - training_args_kwargs["max_length"] = self.cfg.sequence_len - if self.cfg.max_prompt_len: - training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len - - elif self.cfg.rl is RLType.GRPO: - training_args_cls = GRPOStrategy.get_training_args_class() - training_args_kwargs.update(GRPOStrategy.set_training_args_kwargs(self.cfg)) - blocklist_args_kwargs = GRPOStrategy.get_blocklist_args_kwargs() - - else: - training_args_cls = AxolotlDPOConfig - if self.cfg.rl is RLType.IPO: - training_args_kwargs["loss_type"] = "ipo" - training_args_kwargs["max_length"] = self.cfg.sequence_len - training_args_kwargs["max_completion_length"] = None - training_args_kwargs["max_prompt_length"] = self.cfg.sequence_len - training_args_kwargs["generate_during_eval"] = self.cfg.use_wandb - if self.cfg.dpo_use_weighting is not None: - training_args_kwargs["use_weighting"] = self.cfg.dpo_use_weighting - if self.cfg.dpo_use_logits_to_keep is not None: - training_args_kwargs["use_logits_to_keep"] = ( - self.cfg.dpo_use_logits_to_keep - ) - - for blocklist_key in blocklist_args_kwargs: - if blocklist_key in training_args_kwargs: - del training_args_kwargs[blocklist_key] - - max_steps = self.cfg.max_steps or total_num_steps or -1 - training_args_kwargs["num_train_epochs"] = self.cfg.num_epochs - training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg - self.cfg.output_dir, - per_device_train_batch_size=self.cfg.micro_batch_size, - max_steps=max_steps, - gradient_accumulation_steps=self.cfg.gradient_accumulation_steps, - learning_rate=self.cfg.learning_rate, - warmup_steps=self.cfg.warmup_steps, - logging_first_step=True, - logging_steps=1, - optim=self.cfg.optimizer, - save_total_limit=self.cfg.save_total_limit or 5, - **training_args_kwargs, - ) - - # unset run_name so wandb sets up experiment names - if self.cfg.use_wandb and training_args.run_name == training_args.output_dir: - training_args.run_name = ( # pylint: disable=attribute-defined-outside-init - None - ) - - return training_args - - def build(self, total_num_steps): - training_args = self.build_training_arguments(total_num_steps) - trainer_kwargs = {} - if self.cfg.rl is RLType.IPO: - if self.cfg.dpo_label_smoothing: - trainer_kwargs["label_smoothing"] = self.cfg.dpo_label_smoothing - if self.eval_dataset: - trainer_kwargs["eval_dataset"] = self.eval_dataset - if self.cfg.adapter and self.peft_config: - if self.cfg.rl is not RLType.GRPO: - trainer_kwargs["peft_config"] = self.peft_config - if self.cfg.precompute_ref_log_probs is not None: - trainer_kwargs["precompute_ref_log_probs"] = ( - self.cfg.precompute_ref_log_probs - ) - if self.cfg.rl is RLType.GRPO: - trainer_cls = GRPOStrategy.get_trainer_class( - sequence_parallel=self.cfg.sequence_parallel_degree > 1 - ) - trainer_cls_args = [self.model] - trainer_cls_args.extend(GRPOStrategy.set_trainer_args(self.cfg)) - trainer_kwargs.update(GRPOStrategy.set_trainer_kwargs(self.cfg)) - elif self.cfg.rl in [RLType.DPO, RLType.IPO]: - trainer_cls = DPOStrategy.get_trainer_class() - trainer_cls_args = [self.model, self.model_ref] - elif self.cfg.rl is RLType.ORPO: - trainer_cls = AxolotlORPOTrainer - trainer_cls_args = [self.model] - elif self.cfg.rl is RLType.KTO: - trainer_cls = AxolotlKTOTrainer - trainer_cls_args = [self.model] - elif self.cfg.rl is RLType.SIMPO: - trainer_cls = AxolotlCPOTrainer - trainer_cls_args = [self.model] - else: - raise ValueError(f"Unsupported RL: {self.cfg.rl}") - - if self.cfg.plugins: - plugin_manager = PluginManager.get_instance() - temp_trainer_cls = plugin_manager.get_trainer_cls(self.cfg) - if temp_trainer_cls is not None: - trainer_cls = temp_trainer_cls - - sig = inspect.signature(trainer_cls) - if "tokenizer" in sig.parameters.keys(): - trainer_kwargs["tokenizer"] = self.tokenizer - else: - trainer_kwargs["processing_class"] = self.tokenizer - - if self.cfg.datasets is not None and ( - trainer_cls is DPOStrategy.get_trainer_class() - ): - trainer_kwargs["dataset_tags"] = [ - d["path"] for d in self.cfg.datasets if not Path(d["path"]).is_dir() - ] - trainer = trainer_cls( - *trainer_cls_args, - args=training_args, - train_dataset=self.train_dataset, - callbacks=self.get_callbacks(), - **trainer_kwargs, - ) - if self.cfg.fsdp: - ensure_dtype(trainer.model, dtype=self.cfg.torch_dtype) - if self.cfg.rl in [RLType.DPO, RLType.IPO] and trainer.ref_model: - ensure_dtype(trainer.ref_model, dtype=self.cfg.torch_dtype) - - trainer = self.hook_post_create_trainer(trainer) - for callback in self.get_post_trainer_create_callbacks(trainer): - trainer.add_callback(callback) - - return trainer - - -class HFPPOTrainerBuilder(TrainerBuilderBase): - """ - HF Factory class for PPO Trainer - """ - - def get_callbacks(self): - callbacks = super().get_callbacks() - return callbacks - - def get_post_trainer_create_callbacks(self, trainer): - callbacks = super().get_post_trainer_create_callbacks(trainer=trainer) - return callbacks - - def build(self, total_num_steps): - # build PPOConfig - pass diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index c2c80c0bcd..15af80c020 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -5,65 +5,31 @@ from typing import Any, Dict, Union import torch -from peft.optimizers import create_loraplus_optimizer from torch import nn -from transformers import Trainer -from transformers.utils import is_sagemaker_mp_enabled from trl import DPOTrainer from axolotl.core.trainers.mixins import RngLoaderMixin, SchedulerMixin +from axolotl.core.trainers.mixins.optimizer import OptimizerInitMixin, OptimizerMixin from axolotl.core.trainers.utils import ( sanitize_kwargs_for_ds_tagging, sanitize_kwargs_for_tagging, ) -if is_sagemaker_mp_enabled(): - import smdistributed.modelparallel.torch as smp - -class AxolotlDPOTrainer(RngLoaderMixin, SchedulerMixin, DPOTrainer): +class AxolotlDPOTrainer( + RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, DPOTrainer +): """Extend the base DPOTrainer for axolotl helpers.""" tag_names = ["axolotl", "dpo"] def __init__(self, *args, dataset_tags=None, **kwargs): super().__init__(*args, **kwargs) + self.dataset_tags = dataset_tags self.optimizer = None self.model_accepts_loss_kwargs = False - def create_optimizer(self): - # pylint: disable=duplicate-code - if self.args.loraplus_lr_ratio is None: - return super().create_optimizer() - - opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model - if self.optimizer is None: # pylint: disable=access-member-before-definition - optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs( - self.args, - opt_model, - ) - - loraplus_lr_ratio = getattr(self.args, "loraplus_lr_ratio", None) - if loraplus_lr_ratio: - print("Using lora+") - loraplus_lr_embedding = getattr(self.args, "loraplus_lr_embedding", None) - # pylint: disable=duplicate-code - self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init - opt_model, - optimizer_cls, - loraplus_lr_ratio=loraplus_lr_ratio, - loraplus_lr_embedding=loraplus_lr_embedding, - **optimizer_kwargs, - ) - - if is_sagemaker_mp_enabled(): - self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init - self.optimizer - ) - - return self.optimizer - @wraps(DPOTrainer.push_to_hub) def push_to_hub(self, *args, **kwargs) -> str: """ diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 196cdb56a5..a37c8bacaa 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -132,7 +132,7 @@ def get_collator(cls, *args, **kwargs): # pylint: disable=unused-argument @classmethod def get_blocklist_args_kwargs(cls) -> list[str]: - return ["dataset_num_proc"] + return ["dataset_num_proc", "max_length"] @classmethod def get_reward_func(cls, reward_func_fqn: str) -> RewardFunc: @@ -167,4 +167,4 @@ def get_reward_func(cls, reward_func_fqn: str) -> RewardFunc: LOG.info( f"Reward function {reward_func_fqn} is a pre-trained model path - if this is unexpected, please check the reward function path." ) - return reward_func + return reward_func_fqn diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index b5b3912cf5..5c93c69dfb 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -43,6 +43,7 @@ from axolotl.core.trainers.grpo.sampler import SequenceParallelRepeatRandomSampler from axolotl.core.trainers.mixins import RngLoaderMixin, SchedulerMixin +from axolotl.core.trainers.mixins.optimizer import OptimizerInitMixin, OptimizerMixin from axolotl.monkeypatch.ring_attn import get_ring_attn_group if is_peft_available(): @@ -50,7 +51,9 @@ from peft import PeftConfig -class AxolotlGRPOTrainer(RngLoaderMixin, SchedulerMixin, GRPOTrainer): +class AxolotlGRPOTrainer( + RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, GRPOTrainer +): """Extend the base GRPOTrainer for axolotl helpers""" _tag_names = ["trl", "grpo", "axolotl"] @@ -77,6 +80,7 @@ def __init__( torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None ] = (None, None), peft_config: "PeftConfig | None" = None, + optimizer_cls_and_kwargs: tuple[type, dict] | None = None, ): # First call the superclass constructor with all arguments super().__init__( @@ -90,6 +94,7 @@ def __init__( callbacks=callbacks, optimizers=optimizers, peft_config=peft_config, + optimizer_cls_and_kwargs=optimizer_cls_and_kwargs, ) # Get number of SP groups (number of processes divided by SP degree) diff --git a/src/axolotl/core/trainers/mixins/optimizer.py b/src/axolotl/core/trainers/mixins/optimizer.py index abb662706a..a9a9a3992d 100644 --- a/src/axolotl/core/trainers/mixins/optimizer.py +++ b/src/axolotl/core/trainers/mixins/optimizer.py @@ -198,3 +198,20 @@ def create_optimizer(self): ) return self.optimizer + + +class OptimizerInitMixin: + """ + Mixin to handle common optimizer initialization logic for Trainers (mostly TRL) that do not + accept optimizer_cls_and_kwargs as kwarg in constructor. + """ + + def __init__(self, *args, **kwargs): + optimizer_cls_and_kwargs = kwargs.pop("optimizer_cls_and_kwargs", None) + super().__init__(*args, **kwargs) + if ( + optimizer_cls_and_kwargs + and self.optimizer_cls_and_kwargs is None + and self.optimizer is None + ): + self.optimizer_cls_and_kwargs = optimizer_cls_and_kwargs diff --git a/src/axolotl/core/trainers/trl.py b/src/axolotl/core/trainers/trl.py index b2c5c54ca3..bf0d50deea 100644 --- a/src/axolotl/core/trainers/trl.py +++ b/src/axolotl/core/trainers/trl.py @@ -1,7 +1,5 @@ """Module for TRL PPO trainer""" -from typing import Literal, Union - import torch from tqdm import tqdm from trl import ( @@ -14,6 +12,7 @@ ) from axolotl.core.trainers.mixins import RngLoaderMixin +from axolotl.core.trainers.mixins.optimizer import OptimizerInitMixin, OptimizerMixin from axolotl.core.trainers.mixins.scheduler import SchedulerMixin @@ -75,87 +74,19 @@ def train( ) -class AxolotlORPOTrainer(RngLoaderMixin, SchedulerMixin, ORPOTrainer): +class AxolotlORPOTrainer( + RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, ORPOTrainer +): """ Extend the base ORPOTrainer for axolotl helpers """ tag_names = ["axolotl", "orpo"] - def get_batch_loss_metrics( - self, - model, - batch: dict[str, Union[list, torch.LongTensor]], - train_eval: Literal["train", "eval"] = "train", - ): - """Compute the ORPO loss and other metrics for the given batch of inputs for train or test.""" - - # TODO remove once https://github.com/huggingface/trl/pull/3069 is included in a trl release - - metrics = {} - - forward_output = self.concatenated_forward(model, batch) - ( - policy_chosen_logps, - policy_rejected_logps, - policy_chosen_logits, - policy_rejected_logits, - policy_nll_loss, - ) = forward_output[:5] - if self.aux_loss_enabled: - aux_loss = forward_output[5] - - losses, chosen_rewards, rejected_rewards, log_odds_ratio, log_odds_chosen = ( - self.odds_ratio_loss(policy_chosen_logps, policy_rejected_logps) - ) - # full ORPO loss - loss = policy_nll_loss - losses.mean() - - reward_accuracies = (chosen_rewards > rejected_rewards).float() - - prefix = "eval_" if train_eval == "eval" else "" - metrics[f"{prefix}rewards/chosen"] = self.accelerator.gather_for_metrics( - chosen_rewards - ).mean() - metrics[f"{prefix}rewards/rejected"] = self.accelerator.gather_for_metrics( - rejected_rewards - ).mean() - metrics[f"{prefix}rewards/accuracies"] = self.accelerator.gather_for_metrics( - reward_accuracies - ).mean() - metrics[f"{prefix}rewards/margins"] = self.accelerator.gather_for_metrics( - chosen_rewards - rejected_rewards - ).mean() - metrics[f"{prefix}logps/rejected"] = ( - self.accelerator.gather_for_metrics(policy_rejected_logps).detach().mean() - ) - metrics[f"{prefix}logps/chosen"] = ( - self.accelerator.gather_for_metrics(policy_chosen_logps).detach().mean() - ) - metrics[f"{prefix}logits/rejected"] = self.accelerator.gather_for_metrics( - policy_rejected_logits.detach().mean() - ).mean() - metrics[f"{prefix}logits/chosen"] = self.accelerator.gather_for_metrics( - policy_chosen_logits.detach().mean() - ).mean() - metrics[f"{prefix}nll_loss"] = ( - self.accelerator.gather_for_metrics(policy_nll_loss).detach().mean() - ) - metrics[f"{prefix}log_odds_ratio"] = ( - self.accelerator.gather_for_metrics(log_odds_ratio).detach().mean() - ) - metrics[f"{prefix}log_odds_chosen"] = ( - self.accelerator.gather_for_metrics(log_odds_chosen).detach().mean() - ) - for k, v in metrics.items(): - metrics[k] = v.item() - if self.aux_loss_enabled: - loss += self.aux_loss_coef * aux_loss - - return loss, metrics - -class AxolotlKTOTrainer(RngLoaderMixin, SchedulerMixin, KTOTrainer): +class AxolotlKTOTrainer( + RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, KTOTrainer +): """ Extend the base KTOTrainer for axolotl helpers """ @@ -163,89 +94,19 @@ class AxolotlKTOTrainer(RngLoaderMixin, SchedulerMixin, KTOTrainer): tag_names = ["axolotl", "kto"] -class AxolotlCPOTrainer(RngLoaderMixin, SchedulerMixin, CPOTrainer): +class AxolotlCPOTrainer( + RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, CPOTrainer +): """ Extend the base CPOTrainer for axolotl helpers """ tag_names = ["axolotl", "cpo"] - def get_batch_loss_metrics( - self, - model, - batch: dict[str, Union[list, torch.LongTensor]], - train_eval: Literal["train", "eval"] = "train", - ): - """Compute the CPO loss and other metrics for the given batch of inputs for train or test.""" - metrics = {} - - forward_output = self.concatenated_forward(model, batch) - ( - policy_chosen_logps, - policy_rejected_logps, - policy_chosen_logits, - policy_rejected_logits, - policy_nll_loss, - ) = forward_output[:5] - if self.aux_loss_enabled: - aux_loss = forward_output[5] - - losses, chosen_rewards, rejected_rewards = self.cpo_loss( - policy_chosen_logps, - policy_rejected_logps, - ) - - loss = losses.mean() + self.cpo_alpha * policy_nll_loss - reward_accuracies = (chosen_rewards > rejected_rewards).float() - - prefix = "eval_" if train_eval == "eval" else "" - metrics[f"{prefix}rewards/chosen"] = ( - self.accelerator.gather_for_metrics(chosen_rewards).mean().item() - ) - metrics[f"{prefix}rewards/rejected"] = ( - self.accelerator.gather_for_metrics(rejected_rewards).mean().item() - ) - metrics[f"{prefix}rewards/accuracies"] = ( - self.accelerator.gather_for_metrics(reward_accuracies).mean().item() - ) - metrics[f"{prefix}rewards/margins"] = ( - self.accelerator.gather_for_metrics(chosen_rewards - rejected_rewards) - .mean() - .item() - ) - metrics[f"{prefix}logps/rejected"] = ( - self.accelerator.gather_for_metrics(policy_rejected_logps) - .detach() - .mean() - .item() - ) - metrics[f"{prefix}logps/chosen"] = ( - self.accelerator.gather_for_metrics(policy_chosen_logps) - .detach() - .mean() - .item() - ) - metrics[f"{prefix}logits/rejected"] = ( - self.accelerator.gather_for_metrics(policy_rejected_logits.detach().mean()) - .mean() - .item() - ) - metrics[f"{prefix}logits/chosen"] = ( - self.accelerator.gather_for_metrics(policy_chosen_logits.detach().mean()) - .mean() - .item() - ) - metrics[f"{prefix}nll_loss"] = ( - self.accelerator.gather_for_metrics(policy_nll_loss).detach().mean().item() - ) - - if self.aux_loss_enabled: - loss += self.aux_loss_coef * aux_loss - - return loss, metrics - -class AxolotlRewardTrainer(RngLoaderMixin, SchedulerMixin, RewardTrainer): +class AxolotlRewardTrainer( + RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, RewardTrainer +): """ Extend the base RewardTrainer for axolotl helpers """ @@ -253,7 +114,9 @@ class AxolotlRewardTrainer(RngLoaderMixin, SchedulerMixin, RewardTrainer): tag_names = ["axolotl", "reward"] -class AxolotlPRMTrainer(RngLoaderMixin, SchedulerMixin, PRMTrainer): +class AxolotlPRMTrainer( + RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, PRMTrainer +): """ Extend the base trl.PRMTrainer for axolotl helpers """ diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index 9c93f77c7e..42488e643d 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -164,12 +164,6 @@ class AxolotlTrainingMixins: default=None, metadata={"help": "whether to use sequential sampling for curriculum learning"}, ) - alternate_optimizer: Optional[str] = field( - default=None, - metadata={ - "help": "workaround to pass an alternate optimizer to the HF trainer" - }, - ) alternate_lr_scheduler_type: Optional[str] = field( default=None, metadata={ diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 68ba3a124e..b59bd8a752 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -25,7 +25,7 @@ from axolotl.contribs.lgpl import ( # pylint: disable = no-name-in-module fix_untrained_tokens, ) -from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder +from axolotl.core.builders import HFCausalTrainerBuilder, HFRLTrainerBuilder from axolotl.integrations.base import PluginManager from axolotl.loaders import ( ModelLoader, diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index d94f4be74d..8b8a776110 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -46,7 +46,7 @@ from axolotl.utils.schemas.config import AxolotlInputConfig if TYPE_CHECKING: - from axolotl.core.trainer_builder import AxolotlTrainingArguments + from axolotl.core.training_args import AxolotlTrainingArguments IGNORE_INDEX = -100 diff --git a/src/axolotl/utils/callbacks/comet_.py b/src/axolotl/utils/callbacks/comet_.py index b7e9034b0e..7dce951456 100644 --- a/src/axolotl/utils/callbacks/comet_.py +++ b/src/axolotl/utils/callbacks/comet_.py @@ -9,7 +9,7 @@ from axolotl.utils.logging import get_logger if TYPE_CHECKING: - from axolotl.core.trainer_builder import AxolotlTrainingArguments + from axolotl.core.training_args import AxolotlTrainingArguments LOG = get_logger(__name__) diff --git a/src/axolotl/utils/callbacks/lisa.py b/src/axolotl/utils/callbacks/lisa.py index ad7e23144a..348cdf2da2 100644 --- a/src/axolotl/utils/callbacks/lisa.py +++ b/src/axolotl/utils/callbacks/lisa.py @@ -15,7 +15,7 @@ from axolotl.utils.logging import get_logger if TYPE_CHECKING: - from axolotl.core.trainer_builder import AxolotlTrainer + from axolotl.core.trainers import AxolotlTrainer LOG = get_logger(__name__) diff --git a/src/axolotl/utils/callbacks/mlflow_.py b/src/axolotl/utils/callbacks/mlflow_.py index 15f8ef0697..ac72f5e6de 100644 --- a/src/axolotl/utils/callbacks/mlflow_.py +++ b/src/axolotl/utils/callbacks/mlflow_.py @@ -12,7 +12,7 @@ from axolotl.utils.logging import get_logger if TYPE_CHECKING: - from axolotl.core.trainer_builder import AxolotlTrainingArguments + from axolotl.core.training_args import AxolotlTrainingArguments LOG = get_logger(__name__) diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index eeea6f2072..9264c86abd 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -71,8 +71,9 @@ def map_dataset(cfg, data_set, ds_transform_fn, tokenizer, **map_kwargs): data_set = data_set.map( ds_transform_fn, - desc="Mapping RL Dataset", num_proc=cfg.dataset_processes, + load_from_cache_file=not cfg.is_preprocess, + desc="Mapping RL Dataset", **map_kwargs, ) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 698befa194..e7bd16892d 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -101,6 +101,7 @@ class AxolotlInputConfig( # If `None`, default is `False` in the trainer. dpo_use_weighting: bool | None = None dpo_use_logits_to_keep: bool | None = None + dpo_label_smoothing: float | None = None datasets: ( Annotated[ diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index c08504d73c..67f590a377 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -16,7 +16,7 @@ from torch.utils.data import DataLoader, RandomSampler, SequentialSampler from transformers.utils import is_torch_bf16_gpu_available -from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder +from axolotl.core.builders import HFCausalTrainerBuilder, HFRLTrainerBuilder from axolotl.monkeypatch.trainer_eval_guard import patch_evaluation_loop_for_fsdp2 from axolotl.utils.distributed import reduce_and_broadcast from axolotl.utils.environment import check_cuda_p2p_ib_support diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py new file mode 100644 index 0000000000..cde7b74ce8 --- /dev/null +++ b/tests/core/test_builders.py @@ -0,0 +1,595 @@ +"""Unit tests for axolotl.core.builders""" + +# pylint: disable=protected-access + +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest + +from axolotl.common.datasets import load_datasets +from axolotl.core.builders import HFCausalTrainerBuilder, HFRLTrainerBuilder +from axolotl.loaders import ModelLoader, load_tokenizer +from axolotl.utils.config import normalize_config +from axolotl.utils.data.rl import load_prepare_preference_datasets +from axolotl.utils.dict import DictDefault +from axolotl.utils.schemas.enums import RLType + +from tests.constants import ALPACA_MESSAGES_CONFIG_REVISION + + +@pytest.fixture(name="base_cfg") +def fixture_base_cfg(): + """ + Base config with all common arguments between SFT and RLHF + """ + cfg = DictDefault( + { + # Model and tokenizer settings + "base_model": "HuggingFaceTB/SmolLM2-135M-Instruct", + "sequence_len": 2048, + "model_config_type": "llama", # example type + # Basic training settings + "micro_batch_size": 2, + "eval_batch_size": 2, + "num_epochs": 1, + "gradient_accumulation_steps": 1, + "max_steps": 100, + "val_set_size": 0, + # Optimizer settings + "optimizer": "adamw_torch_fused", + "learning_rate": 0.00005, + "weight_decay": 0.01, + "adam_beta1": 0.998, + "adam_beta2": 0.9, + "adam_epsilon": 0.00001, + "max_grad_norm": 1.0, + # LR scheduler settings + "lr_scheduler": "cosine", + "lr_scheduler_kwargs": {"foo": "bar"}, + "warmup_steps": 10, + "warmup_ratio": None, + "cosine_min_lr_ratio": 0.1, + "cosine_constant_lr_ratio": 0.2, + # Checkpointing and saving + "save_steps": 100, + "output_dir": "./model-out", + "save_safetensors": True, + "save_total_limit": 4, + "save_only_model": False, + # Hardware/performance settings + "gradient_checkpointing": False, + "gradient_checkpointing_kwargs": {"use_reentrant": False}, + "dataloader_num_workers": 1, + "dataloader_pin_memory": True, + "dataloader_prefetch_factor": 2, + "sequence_parallel_degree": 1, + # Dtype + "fp16": False, + "bf16": False, + "tf32": False, + # Logging and evaluation + "logging_steps": 10, + "eval_steps": 50, + "eval_strategy": "steps", + "save_strategy": "steps", + "include_tokens_per_second": True, + # Other common settings + "seed": 42, + "remove_unused_columns": True, + "ddp_timeout": 1800, + "ddp_bucket_cap_mb": 25, + "ddp_broadcast_buffers": False, + } + ) + + normalize_config(cfg) + return cfg + + +@pytest.fixture(name="dpo_cfg") +def fixture_dpo_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": RLType.DPO, + "dpo_use_weighting": True, + "dpo_use_logits_to_keep": True, + "dpo_label_smoothing": 0.1, + "beta": 0.1, # DPO beta + } + ) + return cfg + + +@pytest.fixture(name="orpo_cfg") +def fixture_orpo_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": RLType.ORPO, + "orpo_alpha": 0.1, + "max_prompt_len": 512, + } + ) + return cfg + + +@pytest.fixture(name="kto_cfg") +def fixture_kto_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": RLType.KTO, + "kto_desirable_weight": 1.0, + "kto_undesirable_weight": 1.0, + "max_prompt_len": 512, + } + ) + return cfg + + +@pytest.fixture(name="grpo_cfg") +def fixture_grpo_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": RLType.GRPO, + "trl": DictDefault( + { + "beta": 0.001, + "max_completion_length": 256, + "use_vllm": False, # run on CPU + # "vllm_device": "auto", + # "vllm_gpu_memory_utilization": 0.15, + "num_generations": 4, + "reward_funcs": ["rewards.rand_reward_func"], + } + ), + # Must be evenly divisible by num_generations + "micro_batch_size": 4, + } + ) + return cfg + + +@pytest.fixture(name="ipo_cfg") +def fixture_ipo_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": RLType.IPO, + "dpo_label_smoothing": 0, + "beta": 0.1, + } + ) + return cfg + + +@pytest.fixture(name="simpo_cfg") +def fixture_simpo_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": RLType.SIMPO, + "rl_beta": 0.2, + "cpo_alpha": 0.9, + "simpo_gamma": 0.4, + } + ) + return cfg + + +@pytest.fixture(name="sft_cfg") +def fixture_sft_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": None, + "sample_packing": False, + "eval_sample_packing": False, + "flash_attention": False, + } + ) + return cfg + + +@pytest.fixture(name="rm_cfg") +def fixture_rm_cfg(sft_cfg): + cfg = sft_cfg.copy() + cfg.update( + DictDefault( + { + "reward_model": True, + "datasets": [ + { + "path": "argilla/distilabel-intel-orca-dpo-pairs", + "type": "bradley_terry.chat_template", + "split": "train[:1%]", + } + ], + } + ) + ) + return cfg + + +@pytest.fixture(name="prm_cfg") +def fixture_prm_cfg(sft_cfg): + cfg = sft_cfg.copy() + cfg.update( + DictDefault( + { + "process_reward_model": True, + "datasets": [ + { + "path": "trl-lib/math_shepherd", + "type": "stepwise_supervised", + "split": "train[:1%]", + } + ], + } + ) + ) + return cfg + + +@pytest.fixture(name="tokenizer") +def fixture_tokenizer(base_cfg): + return load_tokenizer(base_cfg) + + +@pytest.fixture(name="model") +def fixture_model(base_cfg, tokenizer): + model, _ = ModelLoader(base_cfg, tokenizer).load() + return model + + +class TestHFRLTrainerBuilder: + """ + TestCase class for RLHF trainer builders + """ + + def _test_common_training_arguments(self, training_arguments, rl: str): + """Helper to test common arguments across all variants""" + # Basic training settings + if rl == "grpo": + # grpo_cfg's micro_batch_size is diff from others + assert training_arguments.per_device_train_batch_size == 4 + else: + assert training_arguments.per_device_train_batch_size == 2 + assert training_arguments.gradient_accumulation_steps == 1 + assert training_arguments.max_steps == 100 + + # Optimizer settings + assert training_arguments.learning_rate == 0.00005 + assert training_arguments.weight_decay == 0.01 + assert training_arguments.adam_beta1 == 0.998 + assert training_arguments.adam_beta2 == 0.9 + assert training_arguments.adam_epsilon == 0.00001 + assert training_arguments.max_grad_norm == 1.0 + + # LR scheduler settings + assert training_arguments.lr_scheduler_type == "cosine" + assert training_arguments.warmup_steps == 10 + assert training_arguments.cosine_min_lr_ratio == 0.1 + assert training_arguments.cosine_constant_lr_ratio == 0.2 + + # Other settings + assert training_arguments.dataloader_num_workers == 1 + assert training_arguments.dataloader_pin_memory is True + assert training_arguments.gradient_checkpointing is False + + def test_dpo_training_arguments(self, dpo_cfg, model, tokenizer): + builder = HFRLTrainerBuilder(dpo_cfg, model, tokenizer) + training_arguments, _ = builder._build_training_arguments(100) + + self._test_common_training_arguments(training_arguments, rl=dpo_cfg.rl) + # DPO specific + assert training_arguments.beta == 0.1 + assert hasattr(training_arguments, "use_weighting") + assert training_arguments.use_weighting is True + assert training_arguments.label_smoothing == 0.1 + + def test_orpo_training_arguments(self, orpo_cfg, model, tokenizer): + builder = HFRLTrainerBuilder(orpo_cfg, model, tokenizer) + training_arguments, _ = builder._build_training_arguments(100) + + self._test_common_training_arguments(training_arguments, rl=orpo_cfg.rl) + # ORPO specific + assert training_arguments.beta == 0.1 # maps from orpo_alpha + assert training_arguments.max_prompt_length == 512 + + def test_kto_training_arguments(self, kto_cfg, model, tokenizer): + builder = HFRLTrainerBuilder(kto_cfg, model, tokenizer) + training_arguments, _ = builder._build_training_arguments(100) + + self._test_common_training_arguments(training_arguments, rl=kto_cfg.rl) + # KTO specific + assert training_arguments.desirable_weight == 1.0 + assert training_arguments.undesirable_weight == 1.0 + assert training_arguments.max_prompt_length == 512 + + def _write_rewards_file(self, rewards_dir: Path): + """ + Writes reward function to local tmp path to be loaded on trainer building + """ + # Create rewards.py in a directory we can import from + rewards_dir.mkdir() + rewards_file = rewards_dir / "rewards.py" + rewards_file.write_text( + """import random +def rand_reward_func(prompts, completions) -> list[float]: + return [random.uniform(0, 1) for _ in completions] +""" + ) + + def test_grpo_training_arguments(self, grpo_cfg, model, tokenizer, tmp_path): + + rewards_dir = tmp_path / "rewards_test" + self._write_rewards_file(rewards_dir) + + # Add the directory to Python path so we can import the module + sys.path.insert(0, str(rewards_dir)) + + try: + builder = HFRLTrainerBuilder(grpo_cfg, model, tokenizer) + training_arguments, _ = builder._build_training_arguments(100) + + self._test_common_training_arguments(training_arguments, rl=grpo_cfg.rl) + # GRPO specific + assert training_arguments.beta == 0.001 + assert training_arguments.max_completion_length == 256 + assert training_arguments.use_vllm is False + # assert training_arguments.vllm_device == "auto" + # assert training_arguments.vllm_gpu_memory_utilization == 0.15 + assert training_arguments.num_generations == 4 + + # Test trainer creation to verify reward_funcs + trainer = builder.build(100) + + # Verify reward functions are properly loaded + assert len(trainer.reward_funcs) == 1 + assert trainer.reward_funcs[0].__module__ == "rewards" + assert trainer.reward_funcs[0].__name__ == "rand_reward_func" + finally: + # remove imported module from path + if str(rewards_dir) in sys.path: + sys.path.remove(str(rewards_dir)) + + def test_ipo_training_arguments(self, ipo_cfg, model, tokenizer): + builder = HFRLTrainerBuilder(ipo_cfg, model, tokenizer) + training_arguments, _ = builder._build_training_arguments(100) + + self._test_common_training_arguments(training_arguments, rl=ipo_cfg.rl) + # IPO specific + assert training_arguments.beta == 0.1 + assert training_arguments.loss_type == "ipo" + assert training_arguments.label_smoothing == 0 + + def test_simpo_training_arguments(self, simpo_cfg, model, tokenizer): + builder = HFRLTrainerBuilder(simpo_cfg, model, tokenizer) + training_arguments, _ = builder._build_training_arguments(100) + + self._test_common_training_arguments(training_arguments, rl=simpo_cfg.rl) + # SIMPO specific + assert training_arguments.beta == 0.2 + assert training_arguments.cpo_alpha == 0.9 + assert training_arguments.simpo_gamma == 0.4 + + @pytest.mark.parametrize( + ("cfg_string", "dataset_name"), + [ + ( + "dpo_cfg", + "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", + ), + ( + "ipo_cfg", + "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", + ), + ( + "grpo_cfg", + "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", + ), + ("orpo_cfg", None), # don't use fixture for orpo to use smaller split + ("kto_cfg", None), # no fixture for kto + ( + "simpo_cfg", + "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", + ), + ], + ) + def test_custom_optimizer_cls_and_kwargs( + self, + request, + cfg_string, + dataset_name, + tmp_path, + model, + tokenizer, + ): + cfg = request.getfixturevalue(cfg_string) + + builder = HFRLTrainerBuilder(cfg, model, tokenizer) + cfg["optimizer"] = "muon" + + if cfg_string in ["dpo_cfg", "ipo_cfg", "grpo_cfg", "simpo_cfg"]: + cfg["datasets"] = [DictDefault(ALPACA_MESSAGES_CONFIG_REVISION)] + elif cfg_string == "kto_cfg": + cfg["datasets"] = [ + DictDefault( + { + "path": "argilla/ultrafeedback-binarized-preferences-cleaned-kto", + "type": "llama3.ultra", + "split": "train[:1%]", + } + ) + ] + elif cfg_string == "orpo_cfg": + cfg["datasets"] = [ + DictDefault( + { + "path": "argilla/ultrafeedback-binarized-preferences-cleaned", + "type": "chat_template.argilla", + "split": "train[:1%]", + } + ) + ] + else: + raise ValueError(f"Unhandled cfg_string: {cfg_string}") + + if cfg_string == "grpo_cfg": + rewards_dir = tmp_path / "rewards_test" + self._write_rewards_file(rewards_dir) + + # Add the directory to Python path so we can import the module + sys.path.insert(0, str(rewards_dir)) + + try: + # Only use mock for the commented out configs + if dataset_name is not None: + with patch( + "axolotl.utils.data.rl.load_dataset_w_config" + ) as mock_load_dataset: + mock_load_dataset.return_value = request.getfixturevalue( + dataset_name + ) + train_dataset, eval_dataset = load_prepare_preference_datasets(cfg) + else: + # Load actual datasets for orpo_cfg and kto_cfg + train_dataset, eval_dataset = load_prepare_preference_datasets(cfg) + + builder.train_dataset = train_dataset + builder.eval_dataset = eval_dataset + + trainer = builder.build(100) + + assert trainer.optimizer_cls_and_kwargs is not None + + from axolotl.contribs.mit.muon import ( # pylint: disable=no-name-in-module + Muon, + MuonOptimizerFactory, + ) + + optimizer_cls, optimizer_kwargs = trainer.optimizer_cls_and_kwargs + assert optimizer_cls is MuonOptimizerFactory + assert optimizer_kwargs["lr"] == 0.00005 + assert optimizer_kwargs["weight_decay"] == 0.01 + assert optimizer_kwargs["betas"] == (0.998, 0.9) + assert optimizer_kwargs["eps"] == 0.00001 + + # Ensure optimizer is created with correct class + optim = trainer.create_optimizer() + assert isinstance(optim, Muon) + + finally: + # remove imported module from path + if cfg_string == "grpo_cfg" and str(rewards_dir) in sys.path: + sys.path.remove(str(rewards_dir)) + + +class TestHFCausalTrainerBuilder: + """ + TestCase class for SFT trainer builder + """ + + def test_training_arguments(self, sft_cfg, model, tokenizer): + builder = HFCausalTrainerBuilder(sft_cfg, model, tokenizer) + trainer = builder.build(100) + training_arguments = trainer.args + + # Test common arguments + assert training_arguments.per_device_train_batch_size == 2 + assert training_arguments.gradient_accumulation_steps == 1 + assert training_arguments.max_steps == 100 + + assert training_arguments.learning_rate == 0.00005 + assert training_arguments.weight_decay == 0.01 + assert training_arguments.adam_beta1 == 0.998 + assert training_arguments.adam_beta2 == 0.9 + assert training_arguments.adam_epsilon == 0.00001 + assert training_arguments.max_grad_norm == 1.0 + + assert training_arguments.lr_scheduler_type == "cosine" + assert training_arguments.warmup_steps == 10 + assert training_arguments.cosine_min_lr_ratio == 0.1 + + assert training_arguments.dataloader_num_workers == 1 + assert training_arguments.dataloader_pin_memory is True + assert training_arguments.gradient_checkpointing is False + + # SFT specific + assert training_arguments.sample_packing is False + assert training_arguments.eval_sample_packing is False + + @pytest.mark.parametrize( + "cfg_string", + [ + "sft_cfg", + "rm_cfg", + "prm_cfg", + ], + ) + def test_custom_optimizer_cls_and_kwargs( + self, request, cfg_string, model, tokenizer + ): + cfg = request.getfixturevalue(cfg_string) + builder = HFCausalTrainerBuilder(cfg, model, tokenizer) + cfg["optimizer"] = "muon" + + # need to load datasets for reward model and process reward model trainer + if cfg_string in ["rm_cfg", "prm_cfg"]: + dataset_meta = load_datasets(cfg=cfg) + + builder.train_dataset = dataset_meta.train_dataset + builder.eval_dataset = dataset_meta.eval_dataset + + trainer = builder.build(100) + + assert trainer.optimizer_cls_and_kwargs is not None + + from axolotl.contribs.mit.muon import ( # pylint: disable=no-name-in-module + Muon, + MuonOptimizerFactory, + ) + + optimizer_cls, optimizer_kwargs = trainer.optimizer_cls_and_kwargs + assert optimizer_cls is MuonOptimizerFactory + assert optimizer_kwargs["lr"] == 0.00005 + assert optimizer_kwargs["weight_decay"] == 0.01 + assert optimizer_kwargs["betas"] == (0.998, 0.9) + assert optimizer_kwargs["eps"] == 0.00001 + + # Ensure optimizer is created with correct class + optim = trainer.create_optimizer() + assert isinstance(optim, Muon) + + +class TestTrainerClsPlugin: + """ + TestCase class for trainer builder with plugin + """ + + def test_trainer_cls_is_not_none_with_plugin(self, kto_cfg, model, tokenizer): + """ + Test that the trainer cls is not none with plugin + + Fixes #2693 + """ + cfg = kto_cfg.copy() + cfg.plugins = ["axolotl.integrations.liger.LigerPlugin"] + + # Expected AttributeError as we don't pass regular model configs to RL trainer builder + # If it throws `TypeError: None is not a callable object`, trainer_cls could be None + try: + builder = HFRLTrainerBuilder(cfg, model, tokenizer) + + builder.build(100) + except TypeError as e: + # Error raised if trainer_cls is None + assert "'tuple' object has no attribute 'config'" not in str(e) + except Exception: # pylint: disable=broad-exception-caught + # Another error happens, so we passed trainer_cls to builder + pass diff --git a/tests/core/test_trainer_builder.py b/tests/core/test_trainer_builder.py deleted file mode 100644 index 492578c40f..0000000000 --- a/tests/core/test_trainer_builder.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Unit tests for axolotl.core.trainer_builder""" - -import pytest - -from axolotl.core.trainer_builder import HFRLTrainerBuilder -from axolotl.loaders import ModelLoader, load_tokenizer -from axolotl.utils.config import normalize_config -from axolotl.utils.dict import DictDefault -from axolotl.utils.schemas.enums import RLType - - -@pytest.fixture(name="cfg") -def fixture_cfg(): - cfg = DictDefault( - { - "base_model": "HuggingFaceTB/SmolLM2-135M", - "micro_batch_size": 1, - "gradient_accumulation_steps": 1, - "learning_rate": 0.00005, - "save_steps": 100, - "output_dir": "./model-out", - "warmup_steps": 10, - "gradient_checkpointing": False, - "optimizer": "adamw_torch_fused", - "sequence_len": 2048, - "rl": True, - "adam_beta1": 0.998, - "adam_beta2": 0.9, - "adam_epsilon": 0.00001, - "dataloader_num_workers": 1, - "dataloader_pin_memory": True, - "model_config_type": "llama", - "special_tokens": { - "pad_token": "<|endoftext|>", - }, - } - ) - - normalize_config(cfg) - - return cfg - - -@pytest.fixture(name="tokenizer") -def fixture_tokenizer(cfg): - return load_tokenizer(cfg) - - -@pytest.fixture(name="model") -def fixture_model(cfg, tokenizer): - return ModelLoader(cfg, tokenizer).load() - - -class TestHFRLTrainerBuilder: - """ - TestCase class for DPO trainer builder - """ - - def test_build_training_arguments(self, cfg, model, tokenizer): - builder = HFRLTrainerBuilder(cfg, model, tokenizer) - training_arguments = builder.build_training_arguments(100) - assert training_arguments.adam_beta1 == 0.998 - assert training_arguments.adam_beta2 == 0.9 - assert training_arguments.adam_epsilon == 0.00001 - assert training_arguments.dataloader_num_workers == 1 - assert training_arguments.dataloader_pin_memory is True - - -class TestTrainerClsPlugin: - """ - TestCase class for trainer builder with plugin - """ - - def test_trainer_cls_is_not_none_with_plugin(self, cfg, model, tokenizer): - """ - Test that the trainer cls is not none with plugin - - Fixes #2693 - """ - cfg.plugins = ["axolotl.integrations.liger.LigerPlugin"] - cfg.rl = RLType.KTO - - # Expected AttributeError as we don't pass regular model configs to RL trainer builder - # If it throws `TypeError: None is not a callable object`, trainer_cls could be None - with pytest.raises( - AttributeError, match=r".*'tuple' object has no attribute 'config'.*" - ): - builder = HFRLTrainerBuilder(cfg, model, tokenizer) - - builder.build(100) diff --git a/tests/e2e/test_imports.py b/tests/e2e/test_imports.py index fc08434798..050e4dfb30 100644 --- a/tests/e2e/test_imports.py +++ b/tests/e2e/test_imports.py @@ -11,11 +11,11 @@ class TestImports(unittest.TestCase): """ def test_import_causal_trainer(self): - from axolotl.core.trainer_builder import ( # pylint: disable=unused-import # noqa: F401 + from axolotl.core.builders import ( # pylint: disable=unused-import # noqa: F401 HFCausalTrainerBuilder, ) def test_import_rl_trainer(self): - from axolotl.core.trainer_builder import ( # pylint: disable=unused-import # noqa: F401 + from axolotl.core.builders import ( # pylint: disable=unused-import # noqa: F401 HFRLTrainerBuilder, ) From 5e86c353222d83896ca3b68181935d17605b7358 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 31 May 2025 12:13:31 +0700 Subject: [PATCH 0686/1405] fix(log): remove duplicate merge_lora param (#2742) [skip ci] --- src/axolotl/cli/args.py | 1 - src/axolotl/cli/merge_lora.py | 8 -------- src/axolotl/cli/merge_sharded_fsdp_weights.py | 7 ------- 3 files changed, 16 deletions(-) diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py index 4be3704acd..b859b99c87 100644 --- a/src/axolotl/cli/args.py +++ b/src/axolotl/cli/args.py @@ -28,7 +28,6 @@ class TrainerCliArgs: debug: bool = field(default=False) debug_text_only: bool = field(default=False) debug_num_examples: int = field(default=0) - merge_lora: bool = field(default=False) prompter: Optional[str] = field(default=None) shard: bool = field(default=False) main_process_port: Optional[int] = field(default=None) diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 2e59d25374..36cfdec4e6 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -4,10 +4,8 @@ from typing import Union import fire -import transformers from dotenv import load_dotenv -from axolotl.cli.args import TrainerCliArgs from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.config import load_cfg from axolotl.cli.utils import load_model_and_tokenizer @@ -68,12 +66,6 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: Raises: ValueError: If target directory for LoRA merged model does not exist. """ - # pylint: disable=duplicate-code - parser = transformers.HfArgumentParser(TrainerCliArgs) - parsed_cli_args, _ = parser.parse_args_into_dataclasses( - return_remaining_strings=True - ) - parsed_cli_args.merge_lora = True parsed_cfg = load_cfg( config, diff --git a/src/axolotl/cli/merge_sharded_fsdp_weights.py b/src/axolotl/cli/merge_sharded_fsdp_weights.py index 297d7946e4..2480b551d2 100644 --- a/src/axolotl/cli/merge_sharded_fsdp_weights.py +++ b/src/axolotl/cli/merge_sharded_fsdp_weights.py @@ -10,7 +10,6 @@ import torch import torch.distributed.checkpoint as dist_cp import torch.distributed.checkpoint.format_utils as dist_cp_format_utils -import transformers from accelerate.utils import ( SAFE_WEIGHTS_INDEX_NAME, SAFE_WEIGHTS_NAME, @@ -23,7 +22,6 @@ from safetensors.torch import save_file as safe_save_file from torch.distributed.checkpoint.format_utils import _EmptyStateDictLoadPlanner -from axolotl.cli.args import TrainerCliArgs from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.config import load_cfg from axolotl.utils.logging import get_logger @@ -197,11 +195,6 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): """ # pylint: disable=duplicate-code print_axolotl_text_art() - parser = transformers.HfArgumentParser(TrainerCliArgs) - parsed_cli_args, _ = parser.parse_args_into_dataclasses( - return_remaining_strings=True - ) - parsed_cli_args.merge_lora = True parsed_cfg = load_cfg(config, **kwargs) fsdp_dir = Path(parsed_cfg.output_dir) / "pytorch_model_fsdp_0" From d5d0dc5938a2366314f934db2468ccb84e2165ea Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 31 May 2025 12:13:43 +0700 Subject: [PATCH 0687/1405] fix: suppress non-axolotl logs unless it's warning or higher (#2724) * fix: increase log level for root loggers and axolotl's * fix: BasePlugin using wrong logger * fix: update logger to take name from module * feat: change logger class to AxolotlLogger to filter non-axolotl infos or below * fix: change behavior to not disable existing loggers * fix: update logging to respect correct env * chore: fix comment * fix: suppress accelerate log to LOG_LEVEL if not set --------- Co-authored-by: salman --- src/axolotl/logging_config.py | 57 +++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/src/axolotl/logging_config.py b/src/axolotl/logging_config.py index 2ddf89a8c4..b8dc6479d9 100644 --- a/src/axolotl/logging_config.py +++ b/src/axolotl/logging_config.py @@ -2,14 +2,56 @@ Common logging module for axolotl """ +import logging import os import sys -from logging import Formatter +from logging import Formatter, Logger, LogRecord from logging.config import dictConfig from typing import Any, Dict from colorama import Fore, Style, init +DEFAULT_AXOLOTL_LOG_LEVEL = "INFO" +DEFAULT_LOG_LEVEL = "WARNING" + + +class AxolotlOrWarnErrorFilter(logging.Filter): + """ + Allows ANY WARNING or higher (unless overridden by LOG_LEVEL) + Allows axolotl.* at INFO or higher (unless overridden by AXOLOTL_LOG_LEVEL) + Drops all other records (i.e. non-axolotl.INFO, DEBUG, etc. by default) + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.axolotl_level = logging.getLevelNamesMapping()[ + os.getenv("AXOLOTL_LOG_LEVEL", DEFAULT_AXOLOTL_LOG_LEVEL) + ] + self.other_level = logging.getLevelNamesMapping()[ + os.getenv("LOG_LEVEL", DEFAULT_LOG_LEVEL) + ] + + def filter(self, record: LogRecord) -> bool: + # General filter + if record.levelno >= self.other_level: + return True + + # Axolotl filter + return ( + record.name.startswith("axolotl") and record.levelno >= self.axolotl_level + ) + + +class AxolotlLogger(Logger): + """A Logger that automatically rejects non-axolotl INFOs.""" + + def __init__(self, name: str, level: int = logging.NOTSET): + super().__init__(name, level) + + # set global filter on the logger itself + self.addFilter(AxolotlOrWarnErrorFilter()) + class ColorfulFormatter(Formatter): """ @@ -55,11 +97,15 @@ def format(self, record): "stream": sys.stdout, }, }, - "root": {"handlers": ["console"], "level": os.getenv("LOG_LEVEL", "INFO")}, + # log level will be superseded by the AxolotlLogger + "root": { + "handlers": ["console"], + "level": os.getenv("LOG_LEVEL", DEFAULT_LOG_LEVEL), + }, "loggers": { "axolotl": { "handlers": ["color_console"], - "level": "DEBUG", + "level": os.getenv("AXOLOTL_LOG_LEVEL", DEFAULT_AXOLOTL_LOG_LEVEL), "propagate": False, }, }, @@ -70,3 +116,8 @@ def configure_logging(): """Configure with default logging""" init() # Initialize colorama dictConfig(DEFAULT_LOGGING_CONFIG) + logging.setLoggerClass(AxolotlLogger) + + # set default `ACCELERATE_LOG_LEVEL` to `LOG_LEVEL` if available and not set + if "ACCELERATE_LOG_LEVEL" not in os.environ: + os.environ["ACCELERATE_LOG_LEVEL"] = os.getenv("LOG_LEVEL", DEFAULT_LOG_LEVEL) From ecc719f5c73e8a1a17c6a2d34d72d59d01a5bfc6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 2 Jun 2025 12:48:55 -0700 Subject: [PATCH 0688/1405] add support for base image with uv (#2691) --- .github/workflows/base.yml | 70 +++++++++++++++++++++++++++++++++++--- README.md | 2 +- cicd/multigpu.py | 6 ++-- cicd/single_gpu.py | 6 ++-- docker/Dockerfile-uv-base | 36 ++++++++++++++++++++ docs/docker.qmd | 5 +-- docs/installation.qmd | 36 +++++++++++++++++++- scripts/motd | 2 +- 8 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 docker/Dockerfile-uv-base diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 9e19114d71..6b750fc5a5 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -17,7 +17,7 @@ jobs: build-base: if: github.repository_owner == 'axolotl-ai-cloud' # this job needs to be run on self-hosted GPU runners... - runs-on: axolotl-gpu-runner + runs-on: ubuntu-latest-m strategy: fail-fast: false matrix: @@ -28,42 +28,103 @@ jobs: python_version: "3.11" pytorch: 2.5.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-base" - cuda: "124" cuda_version: 12.4.1 cudnn_version: "" python_version: "3.11" pytorch: 2.6.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-base" - cuda: "126" cuda_version: 12.6.3 cudnn_version: "" python_version: "3.11" pytorch: 2.6.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-base" - cuda: "126" cuda_version: 12.6.3 cudnn_version: "" python_version: "3.11" pytorch: 2.7.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-base" - cuda: "128" cuda_version: 12.6.3 cudnn_version: "" python_version: "3.11" pytorch: 2.7.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-base" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" python_version: "3.11" pytorch: nightly torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-base-nightly" +# # "next" is for release candidates of pytorch +# - cuda: "128" +# cuda_version: 12.8.1 +# cudnn_version: "" +# python_version: "3.11" +# pytorch: next +# torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" +# dockerfile: "Dockerfile-base-next" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Docker metadata + id: metadata + uses: docker/metadata-action@v5 + with: + images: | + winglian/axolotl-base + axolotlai/axolotl-base + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build + uses: docker/build-push-action@v4 + with: + context: . + file: ./docker/${{ matrix.dockerfile }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.metadata.outputs.tags }}-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} + labels: ${{ steps.metadata.outputs.labels }} + build-args: | + CUDA_VERSION=${{ matrix.cuda_version }} + CUDNN_VERSION=${{ matrix.cudnn_version }} + CUDA=${{ matrix.cuda }} + PYTHON_VERSION=${{ matrix.python_version }} + PYTORCH_VERSION=${{ matrix.pytorch }} + TORCH_CUDA_ARCH_LIST=${{ matrix.torch_cuda_arch_list }} + build-base-uv: + if: github.repository_owner == 'axolotl-ai-cloud' + runs-on: ubuntu-latest-m + strategy: + fail-fast: false + matrix: + include: + - cuda: "126" + cuda_version: 12.6.3 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.6.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-uv-base" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" python_version: "3.11" - pytorch: next + pytorch: 2.7.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-uv-base" steps: - name: Checkout uses: actions/checkout@v4 @@ -72,8 +133,7 @@ jobs: uses: docker/metadata-action@v5 with: images: | - winglian/axolotl-base - axolotlai/axolotl-base + axolotlai/axolotl-base-uv - name: Login to Docker Hub uses: docker/login-action@v2 with: @@ -85,7 +145,7 @@ jobs: uses: docker/build-push-action@v4 with: context: . - file: ${{ matrix.pytorch == 'nightly' && './docker/Dockerfile-base-nightly' || matrix.pytorch == 'next' && './docker/Dockerfile-base-next' || './docker/Dockerfile-base' }} + file: ./docker/${{ matrix.dockerfile }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.metadata.outputs.tags }}-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} labels: ${{ steps.metadata.outputs.labels }} diff --git a/README.md b/README.md index 56e45e3fed..d5e8f08a1c 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Features: - NVIDIA GPU (Ampere or newer for `bf16` and Flash Attention) or AMD GPU - Python 3.11 -- PyTorch ≥2.4.1 +- PyTorch ≥2.5.1 ### Installation diff --git a/cicd/multigpu.py b/cicd/multigpu.py index 7de4ae0a7d..9819d3760c 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -24,9 +24,9 @@ df_args = { "AXOLOTL_EXTRAS": os.environ.get("AXOLOTL_EXTRAS", ""), "AXOLOTL_ARGS": os.environ.get("AXOLOTL_ARGS", ""), - "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.4.1"), - "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu121-2.4.1"), - "CUDA": os.environ.get("CUDA", "121"), + "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.5.1"), + "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu124-2.5.1"), + "CUDA": os.environ.get("CUDA", "124"), "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), "CODECOV_TOKEN": os.environ.get("CODECOV_TOKEN", ""), diff --git a/cicd/single_gpu.py b/cicd/single_gpu.py index d46d970cfe..35dd0de594 100644 --- a/cicd/single_gpu.py +++ b/cicd/single_gpu.py @@ -22,9 +22,9 @@ df_args = { "AXOLOTL_EXTRAS": os.environ.get("AXOLOTL_EXTRAS", ""), "AXOLOTL_ARGS": os.environ.get("AXOLOTL_ARGS", ""), - "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.4.1"), - "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu121-2.4.1"), - "CUDA": os.environ.get("CUDA", "121"), + "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.5.1"), + "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu124-2.5.1"), + "CUDA": os.environ.get("CUDA", "124"), "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), "NIGHTLY_BUILD": os.environ.get("NIGHTLY_BUILD", ""), diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base new file mode 100644 index 0000000000..5ac8d86c7d --- /dev/null +++ b/docker/Dockerfile-uv-base @@ -0,0 +1,36 @@ +ARG CUDA_VERSION="12.6.3" +ARG CUDNN_VERSION="" +ARG UBUNTU_VERSION="22.04" +ARG MAX_JOBS=4 + +FROM nvidia/cuda:$CUDA_VERSION-cudnn$CUDNN_VERSION-devel-ubuntu$UBUNTU_VERSION AS base-builder + +ARG PYTHON_VERSION="3.11" +ARG PYTORCH_VERSION="2.6.0" +ARG CUDA="126" +ARG TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6 9.0+PTX" + +ENV PYTHON_VERSION=$PYTHON_VERSION +ENV TORCH_CUDA_ARCH_LIST=$TORCH_CUDA_ARCH_LIST +ENV UV_TORCH_BACKEND="cu${CUDA}" + +RUN apt-get update \ + && apt-get install -y wget git build-essential ninja-build git-lfs libaio-dev pkg-config curl && rm -rf /var/lib/apt/lists/* \ + && git lfs install --skip-repo \ + && curl -LsSf https://astral.sh/uv/install.sh | sh + +ENV PATH="/root/.local/bin:${PATH}" + +RUN uv python install ${PYTHON_VERSION} + +WORKDIR /workspace + +RUN uv venv --no-project --relocatable axolotl-venv + +ENV PATH="/workspace/axolotl-venv/bin:${PATH}" + +RUN uv pip install packaging setuptools wheel \ + && uv pip install torch==${PYTORCH_VERSION} \ + && uv pip install --no-build-isolation "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" \ + && uv pip install "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" \ + && uv pip install awscli pydantic diff --git a/docs/docker.qmd b/docs/docker.qmd index d665eaf5b8..7b236b9604 100644 --- a/docs/docker.qmd +++ b/docs/docker.qmd @@ -36,7 +36,6 @@ Tags examples: - `main-base-py3.11-cu126-2.7.0` - `main-base-py3.11-cu124-2.6.0` - `main-base-py3.11-cu124-2.5.1` -- `main-base-py3.11-cu124-2.4.1` ## Main @@ -77,12 +76,10 @@ Tags examples: - `main-py3.11-cu126-2.7.0` - `main-py3.11-cu124-2.6.0` - `main-py3.11-cu124-2.5.1` -- `main-py3.11-cu124-2.4.1` - `main-latest` - `main-20250303-py3.11-cu124-2.6.0` - `main-20250303-py3.11-cu124-2.5.1` -- `main-20250303-py3.11-cu124-2.4.1` -- `0.7.1` +- `0.9.2` ## Cloud diff --git a/docs/installation.qmd b/docs/installation.qmd index b429992b6a..15f2db57b8 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -15,7 +15,7 @@ This guide covers all the ways you can install and set up Axolotl for your envir - NVIDIA GPU (Ampere architecture or newer for `bf16` and Flash Attention) or AMD GPU - Python ≥3.10 -- PyTorch ≥2.4.1 +- PyTorch ≥2.5.1 ## Installation Methods {#sec-installation-methods} @@ -41,6 +41,40 @@ installed) in order not to clobber it, and so that we set the correct version of dependencies that are specific to the PyTorch version or other installed co-dependencies. +### uv Installation {#sec-uv} + +uv is a fast, reliable Python package installer and resolver built in Rust. It offers significant performance improvements over pip and provides better dependency resolution, making it an excellent choice for complex environments. + +Install uv if not already installed +```{.bash} +curl -LsSf https://astral.sh/uv/install.sh | sh +source $HOME/.local/bin/env +``` + +Choose your CUDA version to use with PyTorch; e.g. `cu124`, `cu126`, `cu128`, +then create the venv and activate +```{.bash} +export UV_TORCH_BACKEND=cu126 +uv venv --no-project --relocatable +source .venv/bin/activate +``` + +Install PyTorch +- PyTorch 2.6.0 recommended +```{.bash} +uv pip install packaging setuptools wheel +uv pip install torch==2.6.0 +uv pip install awscli pydantic +``` + +Install axolotl from PyPi +```{.bash} +uv pip install --no-build-isolation axolotl[deepspeed,flash-attn] + +# optionally install with vLLM if you're using torch==2.6.0 and want to train w/ GRPO +uv pip install --no-build-isolation axolotl[deepspeed,flash-attn,vllm] +``` + ### Edge/Development Build {#sec-edge-build} For the latest features between releases: diff --git a/scripts/motd b/scripts/motd index bc123c312d..f842bd0764 100644 --- a/scripts/motd +++ b/scripts/motd @@ -11,7 +11,7 @@ =@# @# #@= #@ =#@@@@#= +#@@= +#@@@@#= .##@@+ @@ @@@@ @@@@@@@@@@@@@@@@ -Welcome to the axolotl cloud image! If the you've mounted a disk to /workspace and the axolotl directory ie empty, run the following commands: +Welcome to the axolotl cloud image! If the you've mounted a disk to /workspace and the axolotl directory is empty, run the following commands: ``` cd /workspace From 94219f6ee8a4e4b6d28d768c94379406392efe24 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:54:29 -0700 Subject: [PATCH 0689/1405] chore: update pre-commit hooks (#2745) * chore: update pre-commit hooks * trigger linter when pre commit hooks are updated * fix type checks from upgraded pre-commit --------- Co-authored-by: djsaunde <1245942+djsaunde@users.noreply.github.com> Co-authored-by: Wing Lian --- .github/workflows/lint.yml | 1 + .pre-commit-config.yaml | 2 +- src/axolotl/kernels/lora.py | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d85892b43a..d044504281 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,6 +9,7 @@ on: - '.github/workflows/*.yml' - "*.[q]md" - "examples/**/*.y[a]?ml" + - ".pre-commit-config.yaml" workflow_dispatch: jobs: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be78d0d3e4..195746d2d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: hooks: - id: pylint - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v1.16.0 hooks: - id: mypy additional_dependencies: diff --git a/src/axolotl/kernels/lora.py b/src/axolotl/kernels/lora.py index 03fca6df4a..63c9e57bd6 100644 --- a/src/axolotl/kernels/lora.py +++ b/src/axolotl/kernels/lora.py @@ -280,19 +280,19 @@ def backward( # Initialize and compute LoRA gradients d_down_A = d_down_B = d_up_A = d_up_B = d_gate_A = d_gate_B = None - if down_A is not None: + if down_A is not None and down_B is not None: d_down_A = h.t() @ (grad_output @ down_B.t()) d_down_B = (down_A.t() @ h.t()) @ grad_output d_down_A *= down_scale d_down_B *= down_scale - if up_A is not None: + if up_A is not None and up_B is not None: d_up_A = X.t() @ (grad_up @ up_B.t()) d_up_B = (up_A.t() @ X.t()) @ grad_up d_up_A *= up_scale d_up_B *= up_scale - if gate_A is not None: + if gate_A is not None and gate_B is not None: d_gate_A = X.t() @ (grad_gate @ gate_B.t()) d_gate_B = (gate_A.t() @ X.t()) @ grad_gate d_gate_A *= gate_scale @@ -311,7 +311,7 @@ def backward( del up_weight # Note the .to(dtype) only where mixing LoRA with base weights - if up_A is not None: + if up_A is not None and up_B is not None: dX += grad_up @ up_B.to(dtype).t() @ (up_scale * up_A.to(dtype).t()) # Gate projection gradients @@ -319,7 +319,7 @@ def backward( dX += grad_gate @ gate_weight.t() del gate_weight - if gate_A is not None: + if gate_A is not None and gate_B is not None: dX += ( grad_gate @ gate_B.to(dtype).t() From 68788e419e336521eb3543aa1b6a297ea595b849 Mon Sep 17 00:00:00 2001 From: mhenrhcsen Date: Sun, 1 Jun 2025 22:42:03 +0200 Subject: [PATCH 0690/1405] feat: add Group Relative Policy Optimization (GPRO) to RLHF documentation --- docs/rlhf.qmd | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index e0d3b55e4f..af3fe8767a 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -16,6 +16,7 @@ feedback. Various methods include, but not limited to: - [Identity Preference Optimization (IPO)](#ipo) - [Kahneman-Tversky Optimization (KTO)](#kto) - [Odds Ratio Preference Optimization (ORPO)](#orpo) +- [Group Relative Policy Optimization (GPRO)](#grpo) - Proximal Policy Optimization (PPO) (not yet supported in axolotl, if you're interested in contributing, please reach out!) From 2bf61d8e25cc1bc47522ccf87b924181eafe1422 Mon Sep 17 00:00:00 2001 From: mhenrhcsen Date: Sun, 1 Jun 2025 22:50:17 +0200 Subject: [PATCH 0691/1405] fix abbriviatation spelling error --- docs/rlhf.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index af3fe8767a..b2687a8f90 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -16,7 +16,7 @@ feedback. Various methods include, but not limited to: - [Identity Preference Optimization (IPO)](#ipo) - [Kahneman-Tversky Optimization (KTO)](#kto) - [Odds Ratio Preference Optimization (ORPO)](#orpo) -- [Group Relative Policy Optimization (GPRO)](#grpo) +- [Group Relative Policy Optimization (GRPO)](#grpo) - Proximal Policy Optimization (PPO) (not yet supported in axolotl, if you're interested in contributing, please reach out!) From 1d91d905c9e8960fe7f453e1529a0577f64042b3 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Tue, 3 Jun 2025 16:04:15 -0500 Subject: [PATCH 0692/1405] remove deprecated wandb env var (#2751) * remove deprecated wandb env var * remove os.environ wandb setting; unused loggers * remove os.environ wandb setting; unused loggers --- src/axolotl/utils/wandb_.py | 3 --- tests/e2e/multigpu/patched/test_sp.py | 3 --- tests/e2e/multigpu/solo/test_flex.py | 5 ----- tests/e2e/multigpu/test_eval.py | 5 ----- tests/e2e/multigpu/test_gemma3.py | 5 ----- tests/e2e/multigpu/test_llama.py | 5 ----- tests/e2e/multigpu/test_qwen2.py | 5 ----- tests/e2e/multigpu/test_ray.py | 5 ----- tests/e2e/patched/test_4d_multipack_llama.py | 5 ----- tests/e2e/patched/test_fa_xentropy.py | 6 ------ tests/e2e/patched/test_falcon_samplepack.py | 5 ----- tests/e2e/patched/test_fused_llama.py | 5 ----- tests/e2e/patched/test_llama_s2_attention.py | 5 ----- tests/e2e/patched/test_lora_llama_multipack.py | 5 ----- tests/e2e/patched/test_mistral_samplepack.py | 5 ----- tests/e2e/patched/test_mixtral_samplepack.py | 5 ----- tests/e2e/patched/test_phi_multipack.py | 5 ----- tests/e2e/patched/test_resume.py | 5 ----- tests/e2e/patched/test_unsloth_qlora.py | 6 ------ tests/e2e/solo/test_flex.py | 5 ----- tests/e2e/solo/test_relora_llama.py | 5 ----- tests/e2e/test_deepseekv3.py | 5 ----- tests/e2e/test_dpo.py | 5 ----- tests/e2e/test_embeddings_lr.py | 5 ----- tests/e2e/test_evaluate.py | 3 --- tests/e2e/test_falcon.py | 5 ----- tests/e2e/test_gemma2.py | 5 ----- tests/e2e/test_gemma3_text.py | 5 ----- tests/e2e/test_llama.py | 6 ------ tests/e2e/test_llama_pretrain.py | 6 ------ tests/e2e/test_llama_vision.py | 5 ----- tests/e2e/test_lora_llama.py | 5 ----- tests/e2e/test_mamba.py | 5 ----- tests/e2e/test_mistral.py | 5 ----- tests/e2e/test_mixtral.py | 5 ----- tests/e2e/test_optimizers.py | 5 ----- tests/e2e/test_packing_loss.py | 5 ----- tests/e2e/test_phi.py | 5 ----- tests/e2e/test_process_reward_model_smollm2.py | 5 ----- tests/e2e/test_qwen.py | 5 ----- tests/e2e/test_reward_model_smollm2.py | 5 ----- tests/e2e/test_schedulers.py | 5 ----- tests/integrations/test_liger.py | 6 +----- tests/patched/test_validation.py | 15 ++------------- .../test_chat_templates_thinking.py | 3 --- tests/test_prompt_tokenizers.py | 3 --- 46 files changed, 3 insertions(+), 232 deletions(-) diff --git a/src/axolotl/utils/wandb_.py b/src/axolotl/utils/wandb_.py index 327dd9b634..6484d435ae 100644 --- a/src/axolotl/utils/wandb_.py +++ b/src/axolotl/utils/wandb_.py @@ -16,6 +16,3 @@ def setup_wandb_env_vars(cfg: DictDefault): # Enable wandb if project name is present if cfg.wandb_project and len(cfg.wandb_project) > 0: cfg.use_wandb = True - os.environ.pop("WANDB_DISABLED", None) # Remove if present - else: - os.environ["WANDB_DISABLED"] = "true" diff --git a/tests/e2e/multigpu/patched/test_sp.py b/tests/e2e/multigpu/patched/test_sp.py index 1170f5eee8..e90def2b78 100644 --- a/tests/e2e/multigpu/patched/test_sp.py +++ b/tests/e2e/multigpu/patched/test_sp.py @@ -1,6 +1,5 @@ """E2E tests for sequence parallelism""" -import os from pathlib import Path import pytest @@ -12,8 +11,6 @@ from ...utils import check_tensorboard -os.environ["WANDB_DISABLED"] = "true" - class TestSequenceParallelism: """Test case for training with sequence parallelism enabled""" diff --git a/tests/e2e/multigpu/solo/test_flex.py b/tests/e2e/multigpu/solo/test_flex.py index 080ea4c97a..42c3c00c8e 100644 --- a/tests/e2e/multigpu/solo/test_flex.py +++ b/tests/e2e/multigpu/solo/test_flex.py @@ -2,7 +2,6 @@ E2E tests for multigpu lora tinyllama """ -import os from pathlib import Path import pytest @@ -13,13 +12,9 @@ from transformers.utils import is_torch_bf16_gpu_available from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from tests.e2e.utils import check_tensorboard, require_torch_2_6_0 -LOG = get_logger("axolotl.tests.e2e.multigpu") -os.environ["WANDB_DISABLED"] = "true" - AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent diff --git a/tests/e2e/multigpu/test_eval.py b/tests/e2e/multigpu/test_eval.py index 45a961b7a4..379562e40f 100644 --- a/tests/e2e/multigpu/test_eval.py +++ b/tests/e2e/multigpu/test_eval.py @@ -2,7 +2,6 @@ E2E tests for multigpu eval """ -import os from pathlib import Path import yaml @@ -10,13 +9,9 @@ from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_tensorboard -LOG = get_logger("axolotl.tests.e2e.multigpu") -os.environ["WANDB_DISABLED"] = "true" - AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent diff --git a/tests/e2e/multigpu/test_gemma3.py b/tests/e2e/multigpu/test_gemma3.py index 8540ec91fb..9bff25f408 100644 --- a/tests/e2e/multigpu/test_gemma3.py +++ b/tests/e2e/multigpu/test_gemma3.py @@ -2,7 +2,6 @@ E2E tests for multigpu lora tinyllama """ -import os from pathlib import Path import pytest @@ -12,13 +11,9 @@ from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from tests.e2e.utils import check_tensorboard -LOG = get_logger("axolotl.tests.e2e.multigpu") -os.environ["WANDB_DISABLED"] = "true" - AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index e383c54413..9c4bf50543 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -2,7 +2,6 @@ E2E tests for multigpu lora tinyllama """ -import os from pathlib import Path import pytest @@ -14,13 +13,9 @@ from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from tests.e2e.utils import check_tensorboard, require_torch_2_6_0 -LOG = get_logger("axolotl.tests.e2e.multigpu") -os.environ["WANDB_DISABLED"] = "true" - AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent diff --git a/tests/e2e/multigpu/test_qwen2.py b/tests/e2e/multigpu/test_qwen2.py index 23650b10dc..fa4efa32b2 100644 --- a/tests/e2e/multigpu/test_qwen2.py +++ b/tests/e2e/multigpu/test_qwen2.py @@ -2,7 +2,6 @@ E2E tests for multigpu qwen2 """ -import os from pathlib import Path import pytest @@ -11,10 +10,6 @@ from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger - -LOG = get_logger("axolotl.tests.e2e.multigpu") -os.environ["WANDB_DISABLED"] = "true" class TestMultiGPUQwen2: diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index 64c2d501ff..f2c812eb56 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -2,7 +2,6 @@ E2E tests for multigpu post-training use Ray Train """ -import os from pathlib import Path import pytest @@ -10,13 +9,9 @@ from accelerate.test_utils import execute_subprocess_async from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from tests.e2e.utils import check_tensorboard, require_torch_lt_2_6_0 -LOG = get_logger(__name__) -os.environ["WANDB_DISABLED"] = "true" - AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index 27b2b2ca04..490ce77fbb 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -2,7 +2,6 @@ E2E tests for multipack fft llama using 4d attention masks """ -import os import unittest from axolotl.cli.args import TrainerCliArgs @@ -10,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class Test4dMultipackLlama(unittest.TestCase): """ diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index 2581d39a6e..e66b67e6df 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -2,8 +2,6 @@ E2E tests for lora llama """ -import os - import pytest from transformers.utils import is_torch_bf16_gpu_available @@ -12,13 +10,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, check_tensorboard -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestFAXentropyLlama: """ diff --git a/tests/e2e/patched/test_falcon_samplepack.py b/tests/e2e/patched/test_falcon_samplepack.py index 61689ca1fc..bd80221cea 100644 --- a/tests/e2e/patched/test_falcon_samplepack.py +++ b/tests/e2e/patched/test_falcon_samplepack.py @@ -2,7 +2,6 @@ E2E tests for falcon """ -import os import unittest import pytest @@ -12,13 +11,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestFalconPatched(unittest.TestCase): """ diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index 20fd2acb53..49478f10c8 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import os import unittest import pytest @@ -13,13 +12,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - @pytest.mark.skip("FIXME, mostly underused functionality") class TestFusedLlama(unittest.TestCase): diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py index 3c81a274a7..327bb13f8a 100644 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ b/tests/e2e/patched/test_llama_s2_attention.py @@ -2,7 +2,6 @@ E2E tests for llama w/ S2 attn """ -import os import unittest import pytest @@ -12,13 +11,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - @pytest.mark.skip(reason="FIXME?") class TestLlamaShiftedSparseAttention(unittest.TestCase): diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index 894742a7e8..1bad677b91 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import os import unittest import pytest @@ -13,13 +12,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestLoraLlama(unittest.TestCase): """ diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index 5ae5a6dc5a..994b9dfcaf 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import os import unittest from axolotl.cli.args import TrainerCliArgs @@ -10,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestMistral(unittest.TestCase): """ diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index 38a5d6b658..6a84069ef4 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -2,7 +2,6 @@ E2E tests for mixtral """ -import os import unittest from axolotl.cli.args import TrainerCliArgs @@ -10,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestMixtral(unittest.TestCase): """ diff --git a/tests/e2e/patched/test_phi_multipack.py b/tests/e2e/patched/test_phi_multipack.py index 54cac15dcd..ee2a3ffb45 100644 --- a/tests/e2e/patched/test_phi_multipack.py +++ b/tests/e2e/patched/test_phi_multipack.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import os import unittest from axolotl.cli.args import TrainerCliArgs @@ -10,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestPhiMultipack(unittest.TestCase): """ diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index 8ba6b7c540..cc1f3ddeeb 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -2,7 +2,6 @@ E2E tests for resuming training """ -import os import re import subprocess @@ -13,13 +12,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, most_recent_subdir, require_torch_2_6_0 -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestResumeLlama: """ diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index 3b429279f5..46f5b66148 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -2,8 +2,6 @@ e2e tests for unsloth qlora """ -import os - import pytest from axolotl.cli.args import TrainerCliArgs @@ -11,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, check_tensorboard -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - # pylint: disable=duplicate-code @pytest.mark.skip( diff --git a/tests/e2e/solo/test_flex.py b/tests/e2e/solo/test_flex.py index 431afd55ba..b33869b1c6 100644 --- a/tests/e2e/solo/test_flex.py +++ b/tests/e2e/solo/test_flex.py @@ -2,7 +2,6 @@ E2E tests for packed training w/ flex attention """ -import os import unittest from transformers.utils import is_torch_bf16_gpu_available @@ -12,13 +11,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_tensorboard, require_torch_2_6_0, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestPackedFlex(unittest.TestCase): """ diff --git a/tests/e2e/solo/test_relora_llama.py b/tests/e2e/solo/test_relora_llama.py index 6e9f403d0e..cff8313f3b 100644 --- a/tests/e2e/solo/test_relora_llama.py +++ b/tests/e2e/solo/test_relora_llama.py @@ -2,7 +2,6 @@ E2E tests for relora llama """ -import os import unittest from pathlib import Path @@ -11,13 +10,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from ..utils import check_model_output_exists, check_tensorboard, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestReLoraLlama(unittest.TestCase): """ diff --git a/tests/e2e/test_deepseekv3.py b/tests/e2e/test_deepseekv3.py index 0a228aa052..d882286ccd 100644 --- a/tests/e2e/test_deepseekv3.py +++ b/tests/e2e/test_deepseekv3.py @@ -2,7 +2,6 @@ E2E tests for deepseekv3 """ -import os from pathlib import Path import pytest @@ -12,13 +11,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from tests.hf_offline_utils import enable_hf_offline -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestDeepseekV3: """ diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index b039893849..e9f70758bc 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import os import unittest from pathlib import Path @@ -13,13 +12,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestDPOLlamaLora(unittest.TestCase): """ diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py index fe6a507449..f1297fcf34 100644 --- a/tests/e2e/test_embeddings_lr.py +++ b/tests/e2e/test_embeddings_lr.py @@ -2,7 +2,6 @@ E2E tests for llama pretrain """ -import os import unittest from axolotl.cli.args import TrainerCliArgs @@ -10,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, check_tensorboard, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestEmbeddingsLrScale(unittest.TestCase): """ diff --git a/tests/e2e/test_evaluate.py b/tests/e2e/test_evaluate.py index 0278113b7a..6271bba289 100644 --- a/tests/e2e/test_evaluate.py +++ b/tests/e2e/test_evaluate.py @@ -1,6 +1,5 @@ """E2E smoke test for evaluate CLI command""" -import os from pathlib import Path import yaml @@ -9,8 +8,6 @@ from axolotl.utils.dict import DictDefault -os.environ["WANDB_DISABLED"] = "true" - class TestE2eEvaluate: """Test cases for evaluate CLI""" diff --git a/tests/e2e/test_falcon.py b/tests/e2e/test_falcon.py index 4f15867caf..7ea7e30f42 100644 --- a/tests/e2e/test_falcon.py +++ b/tests/e2e/test_falcon.py @@ -2,7 +2,6 @@ E2E tests for falcon """ -import os import unittest import pytest @@ -12,13 +11,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestFalcon(unittest.TestCase): """ diff --git a/tests/e2e/test_gemma2.py b/tests/e2e/test_gemma2.py index 8b9b0d11d4..65732a7374 100644 --- a/tests/e2e/test_gemma2.py +++ b/tests/e2e/test_gemma2.py @@ -2,7 +2,6 @@ E2E tests for gemma2 """ -import os from pathlib import Path import pytest @@ -12,10 +11,6 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger - -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" class TestGemma2: diff --git a/tests/e2e/test_gemma3_text.py b/tests/e2e/test_gemma3_text.py index 9873de6279..d790fa1560 100644 --- a/tests/e2e/test_gemma3_text.py +++ b/tests/e2e/test_gemma3_text.py @@ -2,7 +2,6 @@ E2E tests for gemma3_text """ -import os from pathlib import Path import pytest @@ -12,10 +11,6 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger - -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" class TestGemma3Text: diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index 352372e1ec..455e175325 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -2,20 +2,14 @@ E2E tests for llama """ -import os - from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from tests.e2e.utils import check_model_output_exists -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestLlama: """ diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index 9d0e4d7a6f..ec1e164a40 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -2,8 +2,6 @@ E2E tests for llama pretrain """ -import os - import pytest from axolotl.cli.args import TrainerCliArgs @@ -11,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, check_tensorboard -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestPretrainLlama: """ diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py index 890f275698..b93947f0d0 100644 --- a/tests/e2e/test_llama_vision.py +++ b/tests/e2e/test_llama_vision.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import os import unittest from axolotl.cli.args import TrainerCliArgs @@ -10,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestLlamaVision(unittest.TestCase): """ diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index 02d2868dac..9996250702 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import os import unittest from axolotl.cli.args import TrainerCliArgs @@ -10,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestLoraLlama(unittest.TestCase): """ diff --git a/tests/e2e/test_mamba.py b/tests/e2e/test_mamba.py index 92397ab88f..efffb4547a 100644 --- a/tests/e2e/test_mamba.py +++ b/tests/e2e/test_mamba.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import os import unittest import pytest @@ -12,13 +11,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - @pytest.mark.skip(reason="skipping until upstreamed into transformers") class TestMamba(unittest.TestCase): diff --git a/tests/e2e/test_mistral.py b/tests/e2e/test_mistral.py index ac57848435..98a82a5f09 100644 --- a/tests/e2e/test_mistral.py +++ b/tests/e2e/test_mistral.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import os import unittest from transformers.utils import is_torch_bf16_gpu_available @@ -12,13 +11,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestMistral(unittest.TestCase): """ diff --git a/tests/e2e/test_mixtral.py b/tests/e2e/test_mixtral.py index 329428473f..b551e431a8 100644 --- a/tests/e2e/test_mixtral.py +++ b/tests/e2e/test_mixtral.py @@ -2,7 +2,6 @@ E2E tests for mixtral """ -import os import unittest import torch @@ -13,13 +12,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestMixtral(unittest.TestCase): """ diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 291ed3d6a1..d0837f191f 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -2,7 +2,6 @@ E2E tests for custom optimizers using Llama """ -import os import unittest from axolotl.cli.args import TrainerCliArgs @@ -10,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, require_torch_2_5_1, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestCustomOptimizers(unittest.TestCase): """ diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index 52e27a2c17..12e272888b 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -2,7 +2,6 @@ E2E tests for packed training """ -import os import unittest from transformers.utils import is_torch_bf16_gpu_available @@ -12,13 +11,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_tensorboard, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestPackedLlama(unittest.TestCase): """ diff --git a/tests/e2e/test_phi.py b/tests/e2e/test_phi.py index 349ae9efba..f8b43ad32f 100644 --- a/tests/e2e/test_phi.py +++ b/tests/e2e/test_phi.py @@ -2,7 +2,6 @@ E2E tests for lora llama """ -import os import unittest from axolotl.cli.args import TrainerCliArgs @@ -10,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestPhi(unittest.TestCase): """ diff --git a/tests/e2e/test_process_reward_model_smollm2.py b/tests/e2e/test_process_reward_model_smollm2.py index 0673409ab2..eb81959a21 100644 --- a/tests/e2e/test_process_reward_model_smollm2.py +++ b/tests/e2e/test_process_reward_model_smollm2.py @@ -2,7 +2,6 @@ E2E tests for process reward model w/ lora llama """ -import os import unittest from axolotl.cli.args import TrainerCliArgs @@ -10,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, check_tensorboard, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestProcessRewardSmolLM2(unittest.TestCase): """ diff --git a/tests/e2e/test_qwen.py b/tests/e2e/test_qwen.py index 1f57c6ae18..aa8b9f6c05 100644 --- a/tests/e2e/test_qwen.py +++ b/tests/e2e/test_qwen.py @@ -2,7 +2,6 @@ E2E tests for qwen """ -import os from pathlib import Path import pytest @@ -11,10 +10,6 @@ from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger - -LOG = get_logger("axolotl.tests.qwen") -os.environ["WANDB_DISABLED"] = "true" class TestE2eQwen: diff --git a/tests/e2e/test_reward_model_smollm2.py b/tests/e2e/test_reward_model_smollm2.py index 31938ea589..55405d58c4 100644 --- a/tests/e2e/test_reward_model_smollm2.py +++ b/tests/e2e/test_reward_model_smollm2.py @@ -2,7 +2,6 @@ E2E tests for reward model lora llama """ -import os import unittest from axolotl.cli.args import TrainerCliArgs @@ -10,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, check_tensorboard, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestRewardModelLoraSmolLM2(unittest.TestCase): """ diff --git a/tests/e2e/test_schedulers.py b/tests/e2e/test_schedulers.py index 12783cfb7d..e468081b1d 100644 --- a/tests/e2e/test_schedulers.py +++ b/tests/e2e/test_schedulers.py @@ -2,7 +2,6 @@ E2E tests for custom schedulers using Llama """ -import os import unittest from axolotl.cli.args import TrainerCliArgs @@ -10,13 +9,9 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from .utils import check_model_output_exists, with_temp_dir -LOG = get_logger("axolotl.tests.e2e") -os.environ["WANDB_DISABLED"] = "true" - class TestCustomSchedulers(unittest.TestCase): """ diff --git a/tests/integrations/test_liger.py b/tests/integrations/test_liger.py index 2d6abe311b..5c4bd10281 100644 --- a/tests/integrations/test_liger.py +++ b/tests/integrations/test_liger.py @@ -9,12 +9,8 @@ from axolotl.utils.config import prepare_plugins, validate_config from axolotl.utils.dict import DictDefault -# pylint: disable=duplicate-code -from axolotl.utils.logging import get_logger - -LOG = get_logger("axolotl.integrations.test_liger") - +# pylint: disable=duplicate-code @pytest.fixture(name="minimal_liger_cfg") def fixture_cfg(): return DictDefault( diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 93347e2a4f..2c28a71ea3 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -12,15 +12,12 @@ from axolotl.utils import is_comet_available from axolotl.utils.config import validate_config from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from axolotl.utils.mlflow_ import setup_mlflow_env_vars from axolotl.utils.schemas.config import AxolotlConfigWCapabilities from axolotl.utils.wandb_ import setup_wandb_env_vars warnings.filterwarnings("error") -LOG = get_logger(__name__) - @pytest.fixture(name="minimal_cfg") def fixture_cfg(): @@ -1507,7 +1504,6 @@ def test_wandb_sets_env(self, minimal_cfg): assert os.environ.get("WANDB_MODE", "") == "online" assert os.environ.get("WANDB_WATCH", "") == "false" assert os.environ.get("WANDB_LOG_MODEL", "") == "checkpoint" - assert os.environ.get("WANDB_DISABLED", "") != "true" os.environ.pop("WANDB_PROJECT", None) os.environ.pop("WANDB_NAME", None) @@ -1516,16 +1512,12 @@ def test_wandb_sets_env(self, minimal_cfg): os.environ.pop("WANDB_MODE", None) os.environ.pop("WANDB_WATCH", None) os.environ.pop("WANDB_LOG_MODEL", None) - os.environ.pop("WANDB_DISABLED", None) def test_wandb_set_disabled(self, minimal_cfg): cfg = DictDefault({}) | minimal_cfg - new_cfg = validate_config(cfg) - setup_wandb_env_vars(new_cfg) - - assert os.environ.get("WANDB_DISABLED", "") == "true" + assert new_cfg.use_wandb is None cfg = ( DictDefault( @@ -1537,13 +1529,10 @@ def test_wandb_set_disabled(self, minimal_cfg): ) new_cfg = validate_config(cfg) - setup_wandb_env_vars(new_cfg) - - assert os.environ.get("WANDB_DISABLED", "") != "true" + assert new_cfg.use_wandb is True os.environ.pop("WANDB_PROJECT", None) - os.environ.pop("WANDB_DISABLED", None) @pytest.mark.skipif(is_comet_available() is False, reason="comet_ml is not installed") diff --git a/tests/prompt_strategies/test_chat_templates_thinking.py b/tests/prompt_strategies/test_chat_templates_thinking.py index 21d8c4d5ea..79429b7314 100644 --- a/tests/prompt_strategies/test_chat_templates_thinking.py +++ b/tests/prompt_strategies/test_chat_templates_thinking.py @@ -10,12 +10,9 @@ load, ) from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from tests.hf_offline_utils import enable_hf_offline -LOG = get_logger(__name__) - @pytest.fixture(name="messages_w_reasoning") def messages_w_reasoning_fixture(): diff --git a/tests/test_prompt_tokenizers.py b/tests/test_prompt_tokenizers.py index d34b774b36..5e5de4ff8e 100644 --- a/tests/test_prompt_tokenizers.py +++ b/tests/test_prompt_tokenizers.py @@ -16,12 +16,9 @@ from axolotl.prompt_tokenizers import AlpacaPromptTokenizingStrategy from axolotl.prompters import AlpacaPrompter, PromptStyle from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger from tests.hf_offline_utils import enable_hf_offline -LOG = get_logger(__name__) - test_data = { "multi_turn_sys": { "conversations": [ From d7fa60662ea1b65d53d5ff5d3f4fcf4a590dd9ea Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 3 Jun 2025 14:25:26 -0700 Subject: [PATCH 0693/1405] feat: add chat_template kwargs (#2694) [skip ci] --- src/axolotl/prompt_strategies/chat_template.py | 9 +++++++-- src/axolotl/utils/schemas/config.py | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index ebb151876c..a0fd8d911e 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -29,12 +29,13 @@ def __init__( chat_template: str, processor=None, max_length=2048, - message_property_mappings: Dict[str, str] | None = None, + message_property_mappings: dict[str, str] | None = None, message_field_training: str | None = None, message_field_training_detail: str | None = None, field_messages: str = "messages", field_system: str = "system", - roles: Dict[str, List[str]] | None = None, + roles: dict[str, list[str]] | None = None, + chat_template_kwargs: dict[str, Any] | None = None, drop_system_message: bool = False, ): # check if message_property_mappings is None or empty dict @@ -68,6 +69,7 @@ def __init__( self.tokenizer = tokenizer self.processor: ProcessorMixin | None = processor self.chat_template = chat_template + self.chat_template_kwargs = chat_template_kwargs or {} self.max_length = max_length self.drop_system_message = drop_system_message @@ -85,6 +87,7 @@ def build_prompt(self, conversation, add_generation_prompt=False, images=None): chat_template=self.chat_template, tokenize=False, add_generation_prompt=add_generation_prompt, + **self.chat_template_kwargs, ) batch = self.processor( text=text, @@ -103,6 +106,7 @@ def build_prompt(self, conversation, add_generation_prompt=False, images=None): conversation, add_generation_prompt=add_generation_prompt, chat_template=self.chat_template, + **self.chat_template_kwargs, ) def get_offsets_for_train_detail( @@ -779,6 +783,7 @@ def __call__( prompter_params = { "tokenizer": tokenizer, "chat_template": chat_template_string, + "chat_template_kwargs": cfg.get("chat_template_kwargs", {}), "message_property_mappings": dataset_config.get( "message_property_mappings", {} ), diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index e7bd16892d..e5f105053c 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -314,6 +314,7 @@ class AxolotlInputConfig( | Annotated[str, StringConstraints(pattern="^tokenizer_default_fallback_")] ) | None = None chat_template_jinja: str | None = None + chat_template_kwargs: dict[str, Any] | None = None eot_tokens: list[str] | None = None default_system_message: str | None = None From 4b1a29c6940879d67eb32d5f167b68d6262ece44 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 3 Jun 2025 14:26:07 -0700 Subject: [PATCH 0694/1405] feat(modal): update docker tag to use torch2.6 from torch2.5 (#2749) [skip ci] --- src/axolotl/cli/cloud/modal_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index ef59ed3d41..83cdd7b721 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -82,7 +82,7 @@ def get_env(self): return res def get_image(self): - docker_tag = "main-py3.11-cu124-2.5.1" + docker_tag = "main-py3.11-cu124-2.6.0" if self.config.docker_tag: docker_tag = self.config.docker_tag docker_image = f"axolotlai/axolotl:{docker_tag}" From 787880215b3ab32ccaf81c1b2e9588c6f3e6e764 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 3 Jun 2025 14:27:09 -0700 Subject: [PATCH 0695/1405] fix(deepspeed): deepspeed config not being set for z3 (#2754) * fix(deepspeed): deepspeed config not being set for z3 * fix: comments --- src/axolotl/loaders/model.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 1d26a99dd0..3b2a455ca9 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -556,11 +556,18 @@ def _set_attention_config(self): if self.cfg.low_cpu_mem_usage: self.model_kwargs["low_cpu_mem_usage"] = True - def _configure_zero3_memory_efficient_loading(self): - """Set the deepspeed config to load the model into RAM first before moving - to VRAM. + def _configure_zero3_memory_efficient_loading( + self, + ) -> HfTrainerDeepSpeedConfig | None: + """ + Set the deepspeed config to load the model into RAM first before moving to VRAM. + + IMPORTANT + ========== - We need to return `hf_ds_cfg` as it needs to exist before model loading. + We need to return `hf_ds_cfg` as it needs to exist before model loading for zero3. + HfTrainerDeepSpeedConfig is a class that is used to configure the DeepSpeed training. + It is not passed anywhere in the model loading function, just need to exist. """ hf_ds_cfg = None @@ -625,7 +632,8 @@ def _build_model(self) -> bool: if "device_map" in self.model_kwargs: del self.model_kwargs["device_map"] - self._configure_zero3_memory_efficient_loading() + # Please don't remove underscore binding without reading the fn docstring. + _ = self._configure_zero3_memory_efficient_loading() # Load model with random initialization if specified if self.cfg.random_init_weights: @@ -695,7 +703,8 @@ def _build_model(self) -> bool: if "device_map" in self.model_kwargs: del self.model_kwargs["device_map"] - self._configure_zero3_memory_efficient_loading() + # Please don't remove underscore binding without reading the fn docstring. + _ = self._configure_zero3_memory_efficient_loading() self.model = self.auto_model_loader.from_pretrained( self.base_model, From c67910fa6f11ee69659779bc7009ccd40f34f322 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 5 Jun 2025 07:20:33 -0700 Subject: [PATCH 0696/1405] bump hf deps (#2735) [skip ci] * bump hf deps * upgrade liger-kernel too * install cce from fork for transformers fix * fix reference to vocab size in gemma3 patch * use padding_idx instead of pad_token_id * remove fixed gemma3 patch * use updated cce fork * fix local mllama cce patches w docstring * add test for multipack with trainer setup and fix trainer for trainer refactor upstream * bump modal version * guard for iterable datasetS * mllama model arch layout changed in latest transformers * fix batch sampler with drop_last * fix: address upstream vlm changes for lora * fix: update references to old lora target path * fix: remove mllama fa2 patch due to upstream fix * fix: lora kernel patch path for multimodal models * fix: removed mllama from quarto * run test for came optim on 2.6.0+ * fix fsdp2 patch and remove deprecated patch * make sure to set sequence_parallel_degree for grpo * Add SP test for GRPO * add sp to grpo config for trainer * use reward_funcs as kwarg to grpo trainer * fix the comprehension for reward funcs * reward funcs already passed in as args * init sp_group right before training * fix check for adding models to SP context * make sure to pass args to super * upgrade deepspeed * use updated trl and add reasoning flags for vllm * patch the worker --------- Co-authored-by: NanoCode012 --- .github/workflows/multi-gpu-e2e.yml | 2 +- .github/workflows/tests.yml | 6 +- _quarto.yml | 1 - docs/multimodal.qmd | 2 +- examples/gemma3/gemma-3-4b-qlora.yml | 2 +- examples/gemma3/gemma-3-4b-vision-qlora.yml | 2 +- examples/llama-3-vision/lora-11b.yaml | 2 +- examples/llava/lora-7b.yaml | 2 +- .../mistral/mistral-small-3.1-24B-lora.yml | 2 +- examples/pixtral/lora-12b.yml | 2 +- requirements.txt | 16 +- scripts/cutcrossentropy_install.py | 2 +- setup.py | 2 +- src/axolotl/cli/args.py | 8 + src/axolotl/cli/vllm_serve.py | 87 ++++++- src/axolotl/core/builders/causal.py | 4 +- src/axolotl/core/trainers/base.py | 188 ++++++-------- src/axolotl/core/trainers/grpo/__init__.py | 8 +- src/axolotl/core/trainers/grpo/args.py | 2 + src/axolotl/core/trainers/grpo/trainer.py | 9 + .../cut_cross_entropy/monkeypatch/mllama.py | 13 - src/axolotl/loaders/patch_manager.py | 12 - src/axolotl/monkeypatch/accelerate/fsdp2.py | 119 +++++---- src/axolotl/monkeypatch/attention/mllama.py | 230 ------------------ src/axolotl/monkeypatch/gemma3.py | 230 ------------------ src/axolotl/monkeypatch/lora_kernels.py | 9 +- src/axolotl/train.py | 2 +- src/axolotl/utils/schemas/vllm.py | 9 + tests/e2e/integrations/test_kd.py | 2 +- tests/e2e/multigpu/solo/test_grpo.py | 93 +++++++ tests/e2e/test_llama_vision.py | 4 +- tests/e2e/test_optimizers.py | 8 +- tests/test_packed_dataset.py | 85 +++++++ 33 files changed, 470 insertions(+), 695 deletions(-) delete mode 100644 src/axolotl/monkeypatch/attention/mllama.py delete mode 100644 src/axolotl/monkeypatch/gemma3.py diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 8c7692d138..0167df67a3 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -59,7 +59,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==0.71.8 jinja2 + pip install modal==1.0.2 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 69f0a030d5..29c5bef38d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -322,7 +322,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==0.71.8 jinja2 + pip install modal==1.0.2 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV @@ -384,7 +384,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==0.71.8 jinja2 + pip install modal==1.0.2 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV @@ -424,7 +424,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==0.71.8 jinja2 + pip install modal==1.0.2 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV diff --git a/_quarto.yml b/_quarto.yml index a970cd08ba..9b97095ce6 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -129,7 +129,6 @@ quartodoc: - monkeypatch.trainer_fsdp_optim - monkeypatch.transformers_fa_utils - monkeypatch.unsloth_ - - monkeypatch.attention.mllama - monkeypatch.data.batch_dataset_fetcher - monkeypatch.mixtral - monkeypatch.gradient_checkpointing.offload_cpu diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index 3506db3409..ec51a8ec3f 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -43,7 +43,7 @@ datasets: # leave the vision model and vision tower frozen # load_in_8bit: true adapter: lora -lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' # (optional) if you want to resize images to a set size image_size: 512 diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml index 29f8cc1e1d..0d89d9ffb8 100644 --- a/examples/gemma3/gemma-3-4b-qlora.yml +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -28,7 +28,7 @@ pad_to_sequence_len: true lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' wandb_project: wandb_entity: diff --git a/examples/gemma3/gemma-3-4b-vision-qlora.yml b/examples/gemma3/gemma-3-4b-vision-qlora.yml index 3fd9eb5f0b..339df92e5e 100644 --- a/examples/gemma3/gemma-3-4b-vision-qlora.yml +++ b/examples/gemma3/gemma-3-4b-vision-qlora.yml @@ -30,7 +30,7 @@ pad_to_sequence_len: false lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' wandb_project: wandb_entity: diff --git a/examples/llama-3-vision/lora-11b.yaml b/examples/llama-3-vision/lora-11b.yaml index f4883e903d..2b0ae2c70b 100644 --- a/examples/llama-3-vision/lora-11b.yaml +++ b/examples/llama-3-vision/lora-11b.yaml @@ -29,7 +29,7 @@ pad_to_sequence_len: false lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' wandb_project: wandb_entity: diff --git a/examples/llava/lora-7b.yaml b/examples/llava/lora-7b.yaml index 54edd04dcc..5198c8e744 100644 --- a/examples/llava/lora-7b.yaml +++ b/examples/llava/lora-7b.yaml @@ -25,7 +25,7 @@ pad_to_sequence_len: false lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' wandb_project: wandb_entity: diff --git a/examples/mistral/mistral-small-3.1-24B-lora.yml b/examples/mistral/mistral-small-3.1-24B-lora.yml index 198b3f3737..3e3b45862d 100644 --- a/examples/mistral/mistral-small-3.1-24B-lora.yml +++ b/examples/mistral/mistral-small-3.1-24B-lora.yml @@ -27,7 +27,7 @@ pad_to_sequence_len: false lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' wandb_project: wandb_entity: diff --git a/examples/pixtral/lora-12b.yml b/examples/pixtral/lora-12b.yml index dec8e4b5e3..6ad0a5e999 100644 --- a/examples/pixtral/lora-12b.yml +++ b/examples/pixtral/lora-12b.yml @@ -25,7 +25,7 @@ pad_to_sequence_len: false lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' wandb_project: wandb_entity: diff --git a/requirements.txt b/requirements.txt index 4e632b0f3a..5c5bb00300 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,20 +6,20 @@ triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 autoawq==0.2.7.post3 -liger-kernel==0.5.9 +liger-kernel==0.5.10 # END section packaging==23.2 -huggingface_hub==0.31.0 +huggingface_hub==0.32.2 peft==0.15.2 -transformers==4.51.3 +transformers==4.52.3 tokenizers>=0.21.1 -accelerate==1.6.0 -datasets==3.5.1 -deepspeed>=0.15.4 -trl==0.17.0 -hf_xet==1.1.0 +accelerate==1.7.0 +datasets==3.6.0 +deepspeed>=0.17.0 +trl==0.18.1 +hf_xet==1.1.2 hqq==0.2.5 optimum==1.16.2 diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index bc6213dd97..3ff6dfa8fa 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -25,5 +25,5 @@ print( UNINSTALL_PREFIX - + 'pip install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@bad6f7b49c75fdec69471abb71b4cddd0f0c6438"' + + 'pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@a1174ca"' ) diff --git a/setup.py b/setup.py index 97e7f5ff5c..28f71f789c 100644 --- a/setup.py +++ b/setup.py @@ -118,7 +118,7 @@ def get_package_version(): "yunchang==0.6.0", ], "deepspeed": [ - "deepspeed==0.15.4", + "deepspeed==0.17.0", "deepspeed-kernels", ], "mamba-ssm": [ diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py index b859b99c87..e8571a900c 100644 --- a/src/axolotl/cli/args.py +++ b/src/axolotl/cli/args.py @@ -88,6 +88,14 @@ class VllmServeCliArgs: }, ) + enable_reasoning: Optional[bool] = field( + default=None, + ) + + reasoning_parser: Optional[str] = field( + default=None, + ) + @dataclass class QuantizeCliArgs: diff --git a/src/axolotl/cli/vllm_serve.py b/src/axolotl/cli/vllm_serve.py index d3c4ad68d3..448b25a7e6 100644 --- a/src/axolotl/cli/vllm_serve.py +++ b/src/axolotl/cli/vllm_serve.py @@ -2,14 +2,27 @@ CLI to start the vllm server for online RL """ +import os +from dataclasses import dataclass, field from pathlib import Path from typing import Union +import trl from trl.scripts.vllm_serve import ScriptArguments from axolotl.cli.config import load_cfg +@dataclass +class AxolotlScriptArguments(ScriptArguments): + """ + Additional arguments for the VLLM server + """ + + reasoning_parser: str = field(default="", kw_only=True) + enable_reasoning: bool | None = field(default=None, kw_only=True) + + def do_vllm_serve( config: Union[Path, str], cli_args: dict, @@ -24,6 +37,7 @@ def do_vllm_serve( Returns: process_id: the process id of the started VLLM server """ + patch_vllm_worker() cfg = load_cfg(config) model = cfg.base_model @@ -43,9 +57,16 @@ def do_vllm_serve( enable_prefix_caching = ( cli_args.get("enable_prefix_caching") or cfg.vllm.enable_prefix_caching ) + reasoning_parser = ( + cli_args.get("reasoning_parser") or cfg.vllm.reasoning_parser or "" + ) + enable_reasoning = ( + cli_args.get("enable_reasoning") or cfg.vllm.enable_reasoning or False + ) - vllm_script_args = ScriptArguments( - model, + # pylint: disable=unexpected-keyword-arg + vllm_script_args = AxolotlScriptArguments( + model=model, tensor_parallel_size=tensor_parallel_size, host=host, port=port, @@ -53,5 +74,67 @@ def do_vllm_serve( dtype=dtype, max_model_len=max_model_len, enable_prefix_caching=enable_prefix_caching, + reasoning_parser=reasoning_parser, + enable_reasoning=enable_reasoning, ) vllm_serve_main(vllm_script_args) + + +def patch_vllm_worker(): + from multiprocessing.connection import Connection + + from vllm import LLM + + def llm_worker( + script_args: AxolotlScriptArguments, + data_parallel_rank: int, + master_port: int, + connection: Connection, + ) -> None: + # Set required environment variables for DP to work with vLLM + os.environ["VLLM_DP_RANK"] = str(data_parallel_rank) + os.environ["VLLM_DP_RANK_LOCAL"] = str(data_parallel_rank) + os.environ["VLLM_DP_SIZE"] = str(script_args.data_parallel_size) + os.environ["VLLM_DP_MASTER_PORT"] = str(master_port) + + llm = LLM( + model=script_args.model, + revision=script_args.revision, + tensor_parallel_size=script_args.tensor_parallel_size, + gpu_memory_utilization=script_args.gpu_memory_utilization, + enforce_eager=script_args.enforce_eager, + dtype=script_args.dtype, + # Automatic Prefix Caching caches the KV cache of existing queries, so that a new query can + # directly reuse the KV cache if it shares the same prefix with one of the existing queries. + # This is particularly useful here because we generate completions from the same prompts. + enable_prefix_caching=script_args.enable_prefix_caching, + kv_cache_dtype=script_args.kv_cache_dtype, + max_model_len=script_args.max_model_len, + worker_extension_cls="trl.scripts.vllm_serve.WeightSyncWorkerExtension", + enable_reasoning=script_args.enable_reasoning, + reasoning_parser=script_args.reasoning_parser, + ) + + # Send ready signal to parent process + connection.send({"status": "ready"}) + + while True: + # Wait for commands from the parent process + try: + command = connection.recv() + except KeyboardInterrupt: + llm.collective_rpc(method="close_communicator") + break + + # Handle commands + if command["type"] in ["call", "fire_and_forget"]: + method_name = command["method"] + args, kwargs = command.get("args", ()), command.get("kwargs", {}) + method = getattr(llm, method_name) + result = method(*args, **kwargs) + if command["type"] == "call": + connection.send(result) + elif command["type"] == "shutdown": + break + + trl.scripts.vllm_serve.llm_worker = llm_worker diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 3b1d0b3c2d..7a81616ba7 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -244,7 +244,9 @@ def build(self, total_num_steps): training_arguments_kwargs["sample_packing"] = bool(self.cfg.sample_packing) training_arguments_kwargs["multipack_real_batches"] = ( - not self.cfg.flash_attention or self.cfg.multipack_real_batches + self.cfg.multipack_real_batches + if self.cfg.multipack_real_batches is not None + else not self.cfg.flash_attention ) training_arguments_kwargs["eval_sample_packing"] = bool( self.cfg.eval_sample_packing diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 25e9f9f0ae..70e443cb30 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -6,8 +6,8 @@ import os from collections import defaultdict -from functools import wraps -from typing import Literal +from functools import partial, wraps +from typing import Callable, Literal, Optional import datasets import torch @@ -113,7 +113,9 @@ def _create_multipack_sampler( drop_last=True, ) - def _get_train_sampler(self) -> Sampler | None: + def _get_train_sampler( + self, train_dataset: Optional[Dataset] = None + ) -> Optional[Sampler]: """ Helper method to get the sampler for training. Handles cases for sample packing and curriculum sampling (sequential). @@ -137,7 +139,7 @@ def _get_train_sampler(self) -> Sampler | None: if use_sample_packing: return self._create_multipack_sampler( base_sampler=base_sampler, - dataset=self.train_dataset, + dataset=train_dataset, ) return base_sampler @@ -150,8 +152,6 @@ def _get_eval_sampler(self, eval_dataset: Dataset | None = None) -> Sampler | No If the dataset is non-empty, a sampler is returned, the type of which depends on the passed training args. """ - eval_dataset = eval_dataset if eval_dataset is not None else self.eval_dataset - # Multipacking enabled if training is enabled and eval is not explicitly disabled use_multipack = ( self.args.sample_packing and self.args.eval_sample_packing is not False @@ -172,125 +172,91 @@ def _get_eval_sampler(self, eval_dataset: Dataset | None = None) -> Sampler | No return base_sampler - def _create_dataloader_params(self, is_eval=False, custom_batch_size=None): - """Create common dataloader parameters for train or eval.""" - batch_size = custom_batch_size or ( - self.args.eval_batch_size if is_eval else self._train_batch_size - ) + def _get_dataloader( + self, + dataset: Dataset, + description: str, + batch_size: int, + sampler_fn: Optional[Callable[[Dataset], torch.utils.data.Sampler]] = None, + is_training: bool = False, + dataloader_key: Optional[str] = None, + ) -> DataLoader: + """Create a [`~torch.utils.data.DataLoader`] from the given dataset.""" + + data_collator = self.data_collator if is_training else self.eval_data_collator + + if dataset.column_names and "length" in dataset.column_names: + dataset = dataset.remove_columns(["length"]) + + if isinstance(dataset, datasets.Dataset): + if is_training: + if not self.args.sample_packing or self.args.pretraining: + dataset = self._remove_unused_columns( + dataset, description="training" + ) + elif ( + not is_training + and self.args.sample_packing + and self.args.eval_sample_packing is not False + ): + batch_size = ( + batch_size + if self.args.sample_packing + else self.args.per_device_eval_batch_size + ) + else: + dataset = self._remove_unused_columns(dataset, description=description) + else: + data_collator = self._get_collator_with_removed_columns( + self.data_collator, description=description + ) - params = { + dataloader_params = { "batch_size": batch_size, - "collate_fn": self.data_collator, + "collate_fn": data_collator, "num_workers": self.args.dataloader_num_workers, "pin_memory": self.args.dataloader_pin_memory, + "persistent_workers": self.args.dataloader_persistent_workers, } - # Add persistent workers only for training - if not is_eval and hasattr(self.args, "dataloader_persistent_workers"): - params["persistent_workers"] = self.args.dataloader_persistent_workers - - # Add prefetch factor if specified - if self.args.dataloader_prefetch_factor: - params["prefetch_factor"] = self.args.dataloader_prefetch_factor - - return params - - def _prepare_dataloader( - self, dataset, sampler, is_eval=False, custom_batch_size=None - ): - """Prepare a dataloader with the given dataset and sampler.""" - # Get base parameters - dataloader_params = self._create_dataloader_params(is_eval, custom_batch_size) - - # Add sampler configuration if not isinstance(dataset, torch.utils.data.IterableDataset): - if isinstance(sampler, BatchSampler): - # batch_size and batch_sampler are mutually exclusive - dataloader_params["batch_sampler"] = sampler - del dataloader_params["batch_size"] - else: - dataloader_params["sampler"] = sampler - dataloader_params["drop_last"] = self.args.dataloader_drop_last - - if not is_eval: - dataloader_params["worker_init_fn"] = seed_worker - - # Create the dataloader - dataloader = DataLoader(dataset, **dataloader_params) + dataloader_params["drop_last"] = self.args.dataloader_drop_last + if sampler_fn is not None: + sampler = sampler_fn(dataset) + if isinstance(sampler, BatchSampler): + # batch_size and batch_sampler are mutually exclusive + dataloader_params["batch_sampler"] = sampler + del dataloader_params["batch_size"] + del dataloader_params["drop_last"] + else: + dataloader_params["sampler"] = sampler + dataloader_params["prefetch_factor"] = self.args.dataloader_prefetch_factor + if is_training: + dataloader_params["worker_init_fn"] = partial( + seed_worker, + num_workers=self.args.dataloader_num_workers, + rank=self.args.process_index, + ) if self.args.sample_packing and ( - (not is_eval and not self.args.pretraining) - or (is_eval and self.args.eval_sample_packing is not False) + (is_training and not self.args.pretraining) + or (not is_training and self.args.eval_sample_packing is not False) ): self.accelerator.even_batches = False - return self.accelerator.prepare_data_loader(dataloader) - - def get_train_dataloader(self) -> DataLoader: - """Get dataloader for training""" - train_dataset = self.train_dataset - data_collator = self.data_collator # type: ignore - - # Handle dataset preprocessing - if isinstance(train_dataset, datasets.Dataset): - if self.args.sample_packing and not self.args.pretraining: - train_dataset = train_dataset.remove_columns(["length"]) - if not self.args.sample_packing or self.args.pretraining: - train_dataset = self._remove_unused_columns( - train_dataset, description="training" - ) - else: - self.data_collator = self._get_collator_with_removed_columns( # pylint: disable=attribute-defined-outside-init - data_collator, - description="training", - ) - - # Get sampler and create dataloader - sampler = self._get_train_sampler() - return self._prepare_dataloader(train_dataset, sampler, is_eval=False) - - def get_eval_dataloader(self, eval_dataset: Dataset | None = None) -> DataLoader: - """Get dataloader for evaluation""" - eval_dataset = eval_dataset if eval_dataset is not None else self.eval_dataset - - # Handle special case: sample packing is enabled but eval_sample_packing is False - if self.args.sample_packing and self.args.eval_sample_packing is False: - self.data_collator = ( # pylint: disable=attribute-defined-outside-init - self.eval_data_collator - ) - if "length" in eval_dataset.column_names: - eval_dataset = eval_dataset.remove_columns(["length"]) - dataloader = super().get_eval_dataloader(eval_dataset) - self.data_collator = ( # pylint: disable=attribute-defined-outside-init - self.train_data_collator - ) - - return dataloader - - if self.args.sample_packing and self.args.eval_sample_packing is not False: - # Get appropriate data collator - self.data_collator = ( # pylint: disable=attribute-defined-outside-init - self.eval_data_collator - if hasattr(self, "eval_data_collator") and self.eval_data_collator - else self.data_collator - ) - if "length" in eval_dataset.column_names: - eval_dataset = eval_dataset.remove_columns(["length"]) - - # Use eval_batch_size for sample packing, per_device_eval_batch_size otherwise - batch_size = ( - self.args.eval_batch_size - if self.args.sample_packing - else self.args.per_device_eval_batch_size - ) - sampler = self._get_eval_sampler(eval_dataset) - dataloader = self._prepare_dataloader( - eval_dataset, sampler, is_eval=True, custom_batch_size=batch_size - ) + dataloader = DataLoader(dataset, **dataloader_params) - return dataloader + # Accelerator.free_memory() will destroy the references, so + # we need to store the non-prepared version for eval dataloaders. + # fmt: off + if dataloader_key is not None and self.args.dataloader_persistent_workers: + if hasattr(self, "_eval_dataloaders"): + self._eval_dataloaders[dataloader_key] = dataloader # type: ignore # pylint: disable=access-member-before-definition + else: + self._eval_dataloaders = {dataloader_key: dataloader} # pylint: disable=attribute-defined-outside-init + # fmt: on - return super().get_eval_dataloader(eval_dataset) + return self.accelerator.prepare(dataloader) def _get_bench_sampler( self, bench_dataset: Dataset diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index a37c8bacaa..c0f10be23f 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -69,6 +69,9 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: grpo_args_kwargs["log_completions"] = trl.log_completions grpo_args_kwargs["num_completions_to_print"] = trl.num_completions_to_print + if cfg.sequence_parallel_degree > 1: + grpo_args_kwargs["sequence_parallel_degree"] = cfg.sequence_parallel_degree + if trl.reward_weights: grpo_args_kwargs["reward_weights"] = trl.reward_weights @@ -106,7 +109,9 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: return grpo_args_kwargs @classmethod - def set_trainer_args(cls, cfg: DictDefault) -> list[Any]: + def set_trainer_args( + cls, cfg: DictDefault + ) -> list[Any]: # pylint: disable=unused-argument trainer_args = [] if cfg.trl and cfg.trl.reward_funcs: reward_funcs = [] @@ -123,6 +128,7 @@ def set_trainer_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: trainer_kwargs["reward_processing_classes"] = ( cfg.trl.reward_processing_classes ) + return trainer_kwargs @classmethod diff --git a/src/axolotl/core/trainers/grpo/args.py b/src/axolotl/core/trainers/grpo/args.py index 76be88c896..5c8b1a33b6 100644 --- a/src/axolotl/core/trainers/grpo/args.py +++ b/src/axolotl/core/trainers/grpo/args.py @@ -12,3 +12,5 @@ @dataclass class AxolotlGRPOConfig(AxolotlTrainingMixins, GRPOConfig): """Axolotl GRPO Config for GRPO training""" + + sequence_parallel_degree: int | None = None diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index 5c93c69dfb..dccc85d80d 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -136,6 +136,13 @@ def __init__( f"the valid values for the number of generations are: {possible_values}." ) + self.sp_group = None + self.rank = dist.get_rank() + self.world_size = dist.get_world_size() + self.local_rank = 0 + self.local_world_size = 1 + + def train(self, *args, **kwargs): # Initialize the SP group self.sp_group = get_ring_attn_group() self.rank = dist.get_rank() @@ -143,6 +150,8 @@ def __init__( self.local_rank = dist.get_rank(group=self.sp_group) self.local_world_size = dist.get_world_size(group=self.sp_group) + return super().train(*args, **kwargs) + def _get_train_sampler(self) -> Sampler: effective_batch_size = ( self.args.per_device_train_batch_size diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mllama.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mllama.py index 850764e10e..e82853e6cb 100644 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mllama.py +++ b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mllama.py @@ -15,23 +15,14 @@ from transformers.cache_utils import Cache from transformers.modeling_outputs import CausalLMOutputWithPast from transformers.models.mllama.modeling_mllama import ( - MLLAMA_INPUTS_DOCSTRING, _prepare_cross_attention_mask, ) -from transformers.utils import ( - add_start_docstrings_to_model_forward, - replace_return_docstrings, -) from transformers.utils.deprecation import deprecate_kwarg _PATCH_OPTS: PatchOptions | None = None @deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -@add_start_docstrings_to_model_forward(MLLAMA_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=CausalLMOutputWithPast, config_class="MllamaTextConfig" -) def cce_forward( self, input_ids: torch.LongTensor | None = None, @@ -164,10 +155,6 @@ def cce_forward( @deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -@add_start_docstrings_to_model_forward(MLLAMA_INPUTS_DOCSTRING) -@replace_return_docstrings( - output_type=CausalLMOutputWithPast, config_class="MllamaConfig" -) def cce_forward_multimodal( self, input_ids: Optional[torch.LongTensor] = None, diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 56888b6072..23f79d368a 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -116,13 +116,6 @@ def _apply_model_specific_patches(self): patch_llama4_linearized_modeling() - if self.cfg.model_config_type == "gemma3": - from axolotl.monkeypatch.gemma3 import ( - patch_gemma3conditionalgeneration_forward, - ) - - patch_gemma3conditionalgeneration_forward() - def _apply_fp8_patches(self): """Apply patches for FP8 support.""" if self.cfg.fp8: @@ -212,11 +205,6 @@ def _patch_attention(self): if not (self.cfg.flash_attention and hasattr(self.model_config, "model_type")): return - if self.model_config.model_type == "mllama" and self.cfg.flash_attention: - from axolotl.monkeypatch.attention.mllama import patch_mllama - - patch_mllama() - if self.model_config.model_type == "btlm": from axolotl.monkeypatch.btlm_attn_hijack_flash import ( replace_btlm_attn_with_flash_attn, diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index 6a7d48236c..955c06cbe6 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -18,27 +18,65 @@ def fsdp2_load_full_state_dict(accelerator, model: torch.nn.Module, full_sd: dic Args: accelerator (`Accelerator`): The accelerator instance - model (`torch.nn.Module`): The model to load the state dict into + model (`torch.nn.Module`): + The model to load the state dict into, expected to be on meta device or a VRAM spike can occur full_sd (`dict`): The full state dict to load, can only be on rank 0 """ import torch.distributed as dist from torch.distributed.tensor import distribute_tensor - LOG.info("Broadcasting full state dict to all ranks...") - sharded_sd = model.state_dict() - param_names = sorted(sharded_sd.keys()) + # Model was previously copied to meta device + meta_sharded_sd = model.state_dict() + sharded_sd = {} + + # Rank 0 distributes the full state dict to other ranks + def _infer_parameter_dtype(model, param_name, empty_param): + try: + old_param = model.get_parameter_or_buffer(param_name) + except AttributeError: + # Need this for LORA, as there some params are not *parameters* of sorts + base_param_name, local_param_name = param_name.rsplit(".", 1) + submodule = model.get_submodule(base_param_name) + old_param = getattr(submodule, local_param_name) + + is_torch_e4m3fn_available = hasattr(torch, "float8_e4m3fn") + casting_dtype = None + is_param_float8_e4m3fn = ( + is_torch_e4m3fn_available and empty_param.dtype == torch.float8_e4m3fn + ) + + if empty_param.dtype.is_floating_point and not is_param_float8_e4m3fn: + casting_dtype = old_param.dtype + + return old_param is not None and old_param.is_contiguous(), casting_dtype + + def _cast_and_contiguous(tensor, to_contiguous, dtype): + if dtype is not None: + tensor = tensor.to(dtype=dtype) + if to_contiguous: + tensor = tensor.contiguous() + return tensor + + param_names = sorted(meta_sharded_sd.keys()) + for param_name in param_names: - mesh = sharded_sd[param_name].device_mesh + mesh = meta_sharded_sd[param_name].device_mesh if accelerator.is_main_process: - # Use the corresponding tensor from full_sd (assuming the key exists in full_sd) full_param = full_sd[param_name].detach().cuda() dist.broadcast(full_param, src=0, group=mesh.get_group()) sharded_tensor = distribute_tensor( full_param, mesh, sharded_sd[param_name].placements ) + to_contiguous, casting_dtype = _infer_parameter_dtype( + model, + param_name, + full_param, + ) + sharded_tensor = _cast_and_contiguous( + sharded_tensor, to_contiguous, casting_dtype + ) sharded_sd[param_name] = sharded_tensor else: - # Prepare a tensor of matching shape and dtype full_tensor = torch.empty( sharded_sd[param_name].size(), device="cuda", @@ -48,57 +86,19 @@ def fsdp2_load_full_state_dict(accelerator, model: torch.nn.Module, full_sd: dic sharded_tensor = distribute_tensor( full_tensor, mesh, sharded_sd[param_name].placements ) + to_contiguous, casting_dtype = _infer_parameter_dtype( + model, + param_name, + full_tensor, + ) + sharded_tensor = _cast_and_contiguous( + sharded_tensor, to_contiguous, casting_dtype + ) sharded_sd[param_name] = sharded_tensor + # we set `assign=True` because our params are on meta device model.load_state_dict(sharded_sd, assign=True) - - -def set_state_dict_type(self, state_dict_type=None): - """ - Set the state dict config based on the `StateDictType`. - """ - import os - - from torch.distributed.fsdp.fully_sharded_data_parallel import ( - FullOptimStateDictConfig, - FullStateDictConfig, - ShardedOptimStateDictConfig, - ShardedStateDictConfig, - StateDictType, - ) - - # Override the state_dict_type if provided, typical use case: - # user trains with sharded, but final save is with full - if state_dict_type is not None: - self.state_dict_type = state_dict_type - - if self.state_dict_type is None: - self.state_dict_type = os.environ.get( - "FSDP_STATE_DICT_TYPE", - "FULL_STATE_DICT" if self.fsdp_version == 1 else "SHARDED_STATE_DICT", - ) - if isinstance(self.state_dict_type, str): - if self.state_dict_type.isdigit(): - self.state_dict_type = StateDictType(int(self.state_dict_type)) - else: - self.state_dict_type = StateDictType[self.state_dict_type.upper()] - - if self.state_dict_type == StateDictType.FULL_STATE_DICT: - if self.state_dict_config is None: - self.state_dict_config = FullStateDictConfig( - offload_to_cpu=True, rank0_only=True - ) - if self.optim_state_dict_config is None: - self.optim_state_dict_config = FullOptimStateDictConfig( - offload_to_cpu=True, rank0_only=True - ) - elif self.state_dict_type == StateDictType.SHARDED_STATE_DICT: - if self.state_dict_config is None: - self.state_dict_config = ShardedStateDictConfig(offload_to_cpu=True) - if self.optim_state_dict_config is None: - self.optim_state_dict_config = ShardedOptimStateDictConfig( - offload_to_cpu=True - ) + return model def get_state_dict(self, model, unwrap=True): @@ -208,12 +208,3 @@ def patch_accelerate_fsdp2(): "Accelerator.get_state_dict", get_state_dict, ) - - accelerate.utils.dataclasses.FullyShardedDataParallelPlugin.set_state_dict_type = ( - set_state_dict_type - ) - setattr( - sys.modules["accelerate.utils.dataclasses"], - "FullyShardedDataParallelPlugin.set_state_dict_type", - set_state_dict_type, - ) diff --git a/src/axolotl/monkeypatch/attention/mllama.py b/src/axolotl/monkeypatch/attention/mllama.py deleted file mode 100644 index c9e8fb5e17..0000000000 --- a/src/axolotl/monkeypatch/attention/mllama.py +++ /dev/null @@ -1,230 +0,0 @@ -""" -Monkeypatch for Vision Llama for FA2 support -""" - -# pylint: disable=duplicate-code - -from typing import Optional, Tuple - -import torch -from flash_attn.flash_attn_interface import flash_attn_func -from transformers.cache_utils import Cache -from transformers.modeling_flash_attention_utils import _flash_attention_forward -from transformers.models.mllama.configuration_mllama import MllamaTextConfig -from transformers.models.mllama.modeling_mllama import ( - MllamaTextCrossAttention, - MllamaTextSelfAttention, - apply_rotary_pos_emb, - repeat_kv, -) -from transformers.utils import is_flash_attn_greater_or_equal_2_10 - - -class MllamaTextCrossFlashAttention2(MllamaTextCrossAttention): - """ - Mllama flash cross-attention module. This module inherits from `MllamaTextCrossAttention` and - implements the forward pass using Flash Attention for improved performance. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Check if flash attention version is greater or equal to 2.1 - self._flash_attn_uses_top_left_mask = not is_flash_attn_greater_or_equal_2_10() - - def forward( - self, - hidden_states: torch.Tensor, - cross_attention_states: Optional[torch.Tensor] = None, - past_key_value: Optional[Cache] = None, - attention_mask: Optional[ # pylint: disable=unused-argument - torch.Tensor - ] = None, - output_attentions: bool = False, - use_cache: bool = False, # pylint: disable=unused-argument - cache_position: Optional[torch.LongTensor] = None, - ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: - bsz, q_len, _ = hidden_states.size() - - query_states = self.q_proj(hidden_states) - query_states = query_states.view( - bsz, q_len, self.num_heads, self.head_dim - ).transpose(1, 2) - query_states = self.q_norm(query_states) - - if cross_attention_states is not None: - key_states = self.k_proj(cross_attention_states) - value_states = self.v_proj(cross_attention_states) - key_states = key_states.view( - bsz, -1, self.num_key_value_heads, self.head_dim - ).transpose(1, 2) - value_states = value_states.view( - bsz, -1, self.num_key_value_heads, self.head_dim - ).transpose(1, 2) - key_states = repeat_kv(key_states, self.num_key_value_groups) - value_states = repeat_kv(value_states, self.num_key_value_groups) - - key_states = self.k_norm(key_states) - if past_key_value is not None: - key_states, value_states = past_key_value.update( - key_states, - value_states, - self.layer_idx, - {"cache_position": cache_position}, - ) - elif cache_position[0] != 0: - key_states, value_states = ( - past_key_value.key_cache[self.layer_idx], - past_key_value.value_cache[self.layer_idx], - ) - else: - raise ValueError( - "Cross attention layer can't find neither `cross_attn_states` nor cached values for key/values!" - ) - - # Transpose to get the expected layout for flash attention - query_states = query_states.transpose(1, 2) - key_states = key_states.transpose(1, 2) - value_states = value_states.transpose(1, 2) - - # Apply Flash Attention - dropout_rate = self.dropout if self.training else 0.0 - output = flash_attn_func( - query_states, - key_states, - value_states, - dropout_p=dropout_rate, - softmax_scale=None, - causal=False, - return_attn_probs=output_attentions, - ) - - attn_output = output.contiguous().view(bsz, q_len, -1) - attn_output = self.o_proj(attn_output) - - if not output_attentions: - attn_weights = None - - return attn_output, attn_weights, past_key_value - - -class MllamaTextSelfFlashAttention2(MllamaTextSelfAttention): - """ - Mllama flash self-attention module. This module inherits from `MllamaTextSelfAttention` and - implements the forward pass using Flash Attention for improved performance. - """ - - def __init__(self, config: MllamaTextConfig, layer_idx: int, *args, **kwargs): - super().__init__(config, layer_idx, *args, **kwargs) - - # Check if flash attention version is greater or equal to 2.1 - self._flash_attn_uses_top_left_mask = not is_flash_attn_greater_or_equal_2_10() - - def forward( - self, - hidden_states: torch.Tensor, - attention_mask: Optional[torch.Tensor] = None, - position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, - output_attentions: bool = False, - use_cache: bool = False, # pylint: disable=unused-argument - past_key_value=None, - cache_position: Optional[torch.LongTensor] = None, - **kwargs, # pylint: disable=unused-argument - ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: - output_attentions = False - - bsz, q_len, _ = hidden_states.size() - - query_states = self.q_proj(hidden_states) - key_states = self.k_proj(hidden_states) - value_states = self.v_proj(hidden_states) - - # Flash attention requires the input to have the shape - # batch_size x seq_length x num_heads x head_dim - query_states = query_states.view( - bsz, q_len, self.num_heads, self.head_dim - ).transpose(1, 2) - key_states = key_states.view( - bsz, q_len, self.num_key_value_heads, self.head_dim - ).transpose(1, 2) - value_states = value_states.view( - bsz, q_len, self.num_key_value_heads, self.head_dim - ).transpose(1, 2) - - cos, sin = position_embeddings - query_states, key_states = apply_rotary_pos_emb( - query_states, key_states, cos, sin - ) - - if past_key_value is not None: - # sin and cos are specific to RoPE models; cache_position needed for the static cache - cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position} - key_states, value_states = past_key_value.update( - key_states, value_states, self.layer_idx, cache_kwargs - ) - - key_states = repeat_kv(key_states, self.num_key_value_groups) - value_states = repeat_kv(value_states, self.num_key_value_groups) - - # Transpose to get the expected layout for flash attention - query_states = query_states.transpose(1, 2) - key_states = key_states.transpose(1, 2) - value_states = value_states.transpose(1, 2) - - dropout_rate = self.dropout if self.training else 0.0 - - # Handle potential silent casting to float32 - input_dtype = query_states.dtype - if input_dtype == torch.float32: - if torch.is_autocast_enabled(): - target_dtype = torch.get_autocast_gpu_dtype() - elif hasattr(self.config, "_pre_quantization_dtype"): - target_dtype = ( - self.config._pre_quantization_dtype # pylint: disable=protected-access - ) - else: - target_dtype = self.q_proj.weight.dtype - - query_states = query_states.to(target_dtype) - key_states = key_states.to(target_dtype) - value_states = value_states.to(target_dtype) - - attn_output = _flash_attention_forward( - query_states, - key_states, - value_states, - attention_mask, - q_len, - dropout=dropout_rate, - use_top_left_mask=self._flash_attn_uses_top_left_mask, - is_causal=True, - ) - - attn_output = attn_output.reshape(bsz, q_len, -1).contiguous() - attn_output = self.o_proj(attn_output) - - if not output_attentions: - attn_weights = None - - return attn_output, attn_weights, past_key_value - - -def patch_mllama(): - from transformers.models.mllama.modeling_mllama import ( - MLLAMA_TEXT_ATTENTION_CLASSES, - MLLAMA_TEXT_CROSS_ATTENTION_CLASSES, - MLLAMA_VISION_ATTENTION_CLASSES, - MllamaPreTrainedModel, - ) - - MllamaPreTrainedModel._supports_flash_attn_2 = ( # pylint: disable=protected-access - True - ) - MLLAMA_TEXT_ATTENTION_CLASSES["flash_attention_2"] = MllamaTextSelfFlashAttention2 - MLLAMA_TEXT_CROSS_ATTENTION_CLASSES["flash_attention_2"] = ( - MllamaTextCrossFlashAttention2 - ) - # fallback to SDPA - MLLAMA_VISION_ATTENTION_CLASSES["flash_attention_2"] = ( - MLLAMA_VISION_ATTENTION_CLASSES["sdpa"] - ) diff --git a/src/axolotl/monkeypatch/gemma3.py b/src/axolotl/monkeypatch/gemma3.py deleted file mode 100644 index 36f591efd0..0000000000 --- a/src/axolotl/monkeypatch/gemma3.py +++ /dev/null @@ -1,230 +0,0 @@ -"""Monkeypatch for gemma3 conditional generation forward to fix loss exploding""" - -# pylint: disable=duplicate-code - -from typing import Optional, Tuple, Union - -import torch -from transformers.cache_utils import Cache -from transformers.models.gemma3.modeling_gemma3 import ( - Gemma3CausalLMOutputWithPast, - logger, -) -from transformers.utils import ( - is_torchdynamo_compiling, -) -from transformers.utils.deprecation import deprecate_kwarg - - -@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -def new_forward( - self, - input_ids: torch.LongTensor = None, - pixel_values: torch.FloatTensor = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[Union[list[torch.FloatTensor], Cache]] = None, - token_type_ids: Optional[torch.LongTensor] = None, - cache_position: Optional[torch.LongTensor] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - **lm_kwargs, -) -> Union[Tuple, Gemma3CausalLMOutputWithPast]: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.text_config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.text_config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - Returns: - - Example: - - ```python - >>> from PIL import Image - >>> import requests - >>> from transformers import AutoProcessor, Gemma3ForConditionalGeneration - - >>> model = Gemma3ForConditionalGeneration.from_pretrained("google/Gemma3-test-224px-hf") - >>> processor = AutoProcessor.from_pretrained("google/Gemma3-test-224px-hf") - - >>> prompt = "answer en Where is the cow standing?" - >>> url = "https://huggingface.co/gv-hf/Gemma3-test-224px-hf/resolve/main/cow_beach_1.png" - >>> image = Image.open(requests.get(url, stream=True).raw) - - >>> inputs = processor(images=image, text=prompt, return_tensors="pt") - - >>> # Generate - >>> generate_ids = model.generate(**inputs, max_length=30) - >>> processor.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "answer en Where is the cow standing?\nbeach" - ```""" - - if (input_ids is None) ^ (inputs_embeds is not None): - raise ValueError("You must specify exactly one of input_ids or inputs_embeds") - - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - - is_training = token_type_ids is not None and labels is not None - - # Replace image id with PAD if the image token is OOV, to avoid index-errors - if input_ids is not None and self.config.image_token_index >= self.vocab_size: - special_image_mask = input_ids == self.config.image_token_index - llm_input_ids = input_ids.clone() - llm_input_ids[special_image_mask] = 0 - else: - llm_input_ids = input_ids - - if inputs_embeds is None: - inputs_embeds = self.get_input_embeddings()(llm_input_ids) - - if cache_position is None: - past_seen_tokens = ( - past_key_values.get_seq_length() if past_key_values is not None else 0 - ) - cache_position = torch.arange( - past_seen_tokens, - past_seen_tokens + inputs_embeds.shape[1], - device=inputs_embeds.device, - ) - - # Merge text and images - if pixel_values is not None: - image_features = self.get_image_features(pixel_values) - - if input_ids is None: - special_image_mask = inputs_embeds == self.get_input_embeddings()( - torch.tensor( - self.config.image_token_index, - dtype=torch.long, - device=inputs_embeds.device, - ) - ) - else: - special_image_mask = (input_ids == self.config.image_token_index).unsqueeze( - -1 - ) - special_image_mask = special_image_mask.expand_as(inputs_embeds).to( - inputs_embeds.device - ) - - if ( - not is_torchdynamo_compiling() - and inputs_embeds[special_image_mask].numel() != image_features.numel() - ): - image_tokens_in_text = (special_image_mask).sum(dim=1).sum(dim=0)[0] - raise ValueError( - f"Number of images does not match number of special image tokens in the input text. " - f"Got {image_tokens_in_text} image tokens in the text but {image_features.shape[0] * image_features.shape[1]} " - "tokens from image embeddings." - ) - image_features = image_features.to(inputs_embeds.device, inputs_embeds.dtype) - inputs_embeds = inputs_embeds.masked_scatter(special_image_mask, image_features) - - # mask out pad-token-ids in labels for BC - if labels is not None and self.pad_token_id in labels: - logger.warning_once( - "`labels` contains `pad_token_id` which will be masked with `config.ignore_index`. " - "You have to mask out `pad_token_id` when preparing `labels`, this behavior will be removed in v.4.46.", - ) - labels = torch.where( - input_ids == self.pad_token_id, self.config.ignore_index, labels - ) - - causal_mask = self._update_causal_mask( # pylint: disable=protected-access - attention_mask, - token_type_ids, - past_key_values, - cache_position, - inputs_embeds, - is_training, - ) - outputs = self.language_model( - attention_mask=causal_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - logits_to_keep=logits_to_keep, - **lm_kwargs, - ) - - logits = outputs[0] - loss = None - if labels is not None: - if attention_mask is not None: - # Get the shifted attention mask - shift_attention_mask = attention_mask[:, -logits.shape[1] + 1 :].to( - logits.device - ) # +1 for shift - - # Filter logits and labels based on attention mask - valid_indices = shift_attention_mask != 0 - filtered_logits = logits[..., :-1, :][valid_indices] - filtered_labels = labels[..., 1:][valid_indices.to(labels.device)] - - # TODO: do we need to handle num_items_in_batch given we filter the logits and labels? - - loss = self.loss_function( - logits=filtered_logits, - labels=None, # we pass shift_labels - shift_labels=filtered_labels, - vocab_size=self.config.text_config.vocab_size, - **lm_kwargs, - ) - else: - # Standard case without filtering - loss = self.loss_function( - logits=logits, - labels=labels, - vocab_size=self.config.text_config.vocab_size, - **lm_kwargs, - ) - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return Gemma3CausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - image_hidden_states=image_features if pixel_values is not None else None, - ) - - -def patch_gemma3conditionalgeneration_forward(): - from transformers.models.gemma3.modeling_gemma3 import ( - Gemma3ForConditionalGeneration, - ) - - Gemma3ForConditionalGeneration.forward = new_forward diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 11e0989cf5..a7875eefe4 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -342,10 +342,11 @@ def apply_lora_kernel_patches( layers = [] # check for multimodal models first - if hasattr(model, "language_model"): - layers = model.language_model.model.layers - elif hasattr(model, "model"): - layers = model.model.model.layers + pretrained_model = model.model + if hasattr(pretrained_model, "language_model"): + layers = pretrained_model.language_model.layers + elif hasattr(pretrained_model, "model"): + layers = pretrained_model.model.layers else: raise NotImplementedError( f"Model type {model.config.model_type} is not supported yet. Please create an Issue." diff --git a/src/axolotl/train.py b/src/axolotl/train.py index b59bd8a752..866a9c4548 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -204,7 +204,7 @@ def execute_training( if cfg.sequence_parallel_degree > 1: models = [trainer.model] - if hasattr(trainer, "ref_model"): + if hasattr(trainer, "ref_model") and trainer.ref_model: models.append(trainer.ref_model) stack.enter_context( diff --git a/src/axolotl/utils/schemas/vllm.py b/src/axolotl/utils/schemas/vllm.py index 48441de5e7..0ae6355893 100644 --- a/src/axolotl/utils/schemas/vllm.py +++ b/src/axolotl/utils/schemas/vllm.py @@ -44,3 +44,12 @@ class VllmConfig(BaseModel): default=8000, json_schema_extra={"description": "Port of the vLLM server to start on"}, ) + + enable_reasoning: bool | None = Field( + default=None, + json_schema_extra={"description": "Enable reasoning for VLLM"}, + ) + reasoning_parser: str | None = Field( + default=None, + json_schema_extra={"description": "Reasoning parser for VLLM"}, + ) diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py index f36eef9538..dad7779477 100644 --- a/tests/e2e/integrations/test_kd.py +++ b/tests/e2e/integrations/test_kd.py @@ -90,7 +90,7 @@ def test_llama_kd(self, temp_dir, kd_min_cfg): train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "model.safetensors").exists() check_tensorboard( - temp_dir + "/runs", "train/loss", 1.2, "Train Loss (%s) is too high" + temp_dir + "/runs", "train/loss", 1.4, "Train Loss (%s) is too high" ) @pytest.mark.parametrize( diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index 6c7a9b2e46..8ea2e3ce4d 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -262,6 +262,99 @@ def test_llama_dora(self, temp_dir, num_gpus): **current_env, }, ) + finally: + (recursive_kill(vllm_process)) + + @require_vllm + def test_llama_lora_sp(self, temp_dir): + rnd_reward_suffix = str(random.randint(1000, 9999)) + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "chat_template": "llama3", + "rl": "grpo", + "trl": { + "beta": 0.001, + "max_completion_length": 256, + "use_vllm": True, + "num_generations": 4, + "reward_funcs": [f"rewards_{rnd_reward_suffix}.rand_reward_func"], + }, + "vllm": { + "max_model_len": 800, + "enable_prefix_caching": True, + }, + "datasets": [ + { + "path": "openai/gsm8k", + "name": "main", + "type": f"rewards_{rnd_reward_suffix}.oai_gsm8k_transform", + }, + ], + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "sequence_parallel_degree": 2, + "flash_attention": True, + "sequence_len": 1024, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "max_steps": 3, + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "warmup_steps": 10, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + "use_tensorboard": True, + } + ) + + self._utils_write_yaml_and_rewards(cfg, temp_dir, suffix=rnd_reward_suffix) + + current_env = os.environ.copy() + env = { + "NCCL_P2P_LEVEL": "LOC", + **current_env, + "CUDA_VISIBLE_DEVICES": "1", + } + vllm_process = start_vllm( + cfg.base_model, + env=env, + quiet=True, + wait=300, + gpu_memory_utilization=0.15, + max_model_len=cfg.vllm.max_model_len, + enable_prefix_caching=cfg.vllm.enable_prefix_caching, + host="0.0.0.0", + port=8000, + ) + + try: + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + str(2), + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ], + env={ + "NCCL_P2P_LEVEL": "LOC", + "NCCL_DEBUG": "INFO", + **current_env, + }, + ) finally: recursive_kill(vllm_process) diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py index b93947f0d0..32657c156f 100644 --- a/tests/e2e/test_llama_vision.py +++ b/tests/e2e/test_llama_vision.py @@ -33,7 +33,7 @@ def test_lora_llama_vision_text_only_dataset(self, temp_dir): "lora_r": 8, "lora_alpha": 16, "lora_dropout": 0.05, - "lora_target_modules": r"language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj", + "lora_target_modules": r"model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj", "val_set_size": 0, "chat_template": "llama3_2_vision", "datasets": [ @@ -81,7 +81,7 @@ def test_lora_llama_vision_multimodal_dataset(self, temp_dir): "lora_r": 8, "lora_alpha": 16, "lora_dropout": 0.05, - "lora_target_modules": r"language_model.model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj", + "lora_target_modules": r"model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj", "val_set_size": 0, "chat_template": "llama3_2_vision", "datasets": [ diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index d0837f191f..e812a5f7e4 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -10,7 +10,12 @@ from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from .utils import check_model_output_exists, require_torch_2_5_1, with_temp_dir +from .utils import ( + check_model_output_exists, + require_torch_2_5_1, + require_torch_2_6_0, + with_temp_dir, +) class TestCustomOptimizers(unittest.TestCase): @@ -196,6 +201,7 @@ def test_fft_schedule_free_adamw(self, temp_dir): check_model_output_exists(temp_dir, cfg) @with_temp_dir + @require_torch_2_6_0 def test_came_pytorch(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( diff --git a/tests/test_packed_dataset.py b/tests/test_packed_dataset.py index 45fc752827..8b29eab210 100644 --- a/tests/test_packed_dataset.py +++ b/tests/test_packed_dataset.py @@ -6,10 +6,16 @@ from datasets import Dataset, load_dataset from transformers import AutoTokenizer +from axolotl.cli.args import TrainerCliArgs +from axolotl.common.datasets import load_datasets from axolotl.datasets import ConstantLengthDataset, TokenizedPromptDataset from axolotl.prompt_tokenizers import AlpacaPromptTokenizingStrategy from axolotl.prompters import AlpacaPrompter +from axolotl.train import setup_model_and_trainer +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault +from tests.e2e.utils import with_temp_dir from tests.hf_offline_utils import enable_hf_offline @@ -67,6 +73,85 @@ def test_increments_attention(self): assert example["position_ids"][next_bos_index] == 0 assert example["position_ids"][next_bos_index + 1] == 1 + @with_temp_dir + def test_lora_packing(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", + "sequence_len": 1024, + "sample_packing": True, + "multipack_real_batches": False, + "eval_sample_packing": True, + "adapter": "lora", + "lora_r": 32, + "lora_alpha": 64, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.2, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 20, + "save_steps": 10, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "fp16": False, + "bf16": False, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + + ( + trainer, + _, + _, + _, + _, + ) = setup_model_and_trainer(cfg, dataset_meta) + + sampler = trainer._get_eval_sampler( # pylint: disable=protected-access + trainer.eval_dataset + ) + assert "MultipackBatchSampler" in sampler.__class__.__name__ + assert ( + "V2BatchSamplerDataCollatorForSeq2Seq" + in trainer.eval_data_collator.__class__.__name__ + ) + dataloader = trainer.get_eval_dataloader(trainer.eval_dataset) + dataloader_iter = iter(dataloader) + batch = next(dataloader_iter) + assert batch["input_ids"].shape == (1, 8192) + + sampler = trainer._get_train_sampler( # pylint: disable=protected-access + trainer.train_dataset + ) + assert "MultipackBatchSampler" in sampler.__class__.__name__ + assert ( + "V2BatchSamplerDataCollatorForSeq2Seq" + in trainer.train_data_collator.__class__.__name__ + ) + dataloader = trainer.get_train_dataloader() + dataloader_iter = iter(dataloader) + batch = next(dataloader_iter) + assert batch["input_ids"].shape == (1, 8192) + if __name__ == "__main__": unittest.main() From e8e45b3441be105e475a84acc3e36087a7dbbbd4 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 5 Jun 2025 07:22:23 -0700 Subject: [PATCH 0697/1405] fix: remove hqq (#2759) [skip ci] --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5c5bb00300..3af94421d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,6 @@ datasets==3.6.0 deepspeed>=0.17.0 trl==0.18.1 hf_xet==1.1.2 -hqq==0.2.5 optimum==1.16.2 hf_transfer From 4440b4a1ce9ee0e1702ab17f6e1852985c8220a3 Mon Sep 17 00:00:00 2001 From: Timofey Klyubin Date: Thu, 5 Jun 2025 10:22:58 -0400 Subject: [PATCH 0698/1405] remove unused field for chat_template.default for DPO training (#2755) [skip ci] * remove unused field for chat_template.default "messages" field present in final dataset causes issues with DPO training otherwise * lint and fix tests for new return value * remove unused field for chat_template.default "messages" field present in final dataset causes issues with DPO training otherwise lint and fix tests for new return value fix for updated expected fields for dpo remove unused field for chat_template.default "messages" field present in final dataset causes issues with DPO training otherwise fix test still expecting "messages" field * chore: lint --------- Co-authored-by: Wing Lian --- src/axolotl/prompt_strategies/dpo/chat_template.py | 2 +- tests/prompt_strategies/test_dpo_chat_templates.py | 8 ++++---- tests/test_datasets.py | 10 ++++++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/axolotl/prompt_strategies/dpo/chat_template.py b/src/axolotl/prompt_strategies/dpo/chat_template.py index f04bd7f0d2..f3427022f2 100644 --- a/src/axolotl/prompt_strategies/dpo/chat_template.py +++ b/src/axolotl/prompt_strategies/dpo/chat_template.py @@ -91,4 +91,4 @@ def transform_fn(sample, tokenizer=None): return result - return transform_fn + return transform_fn, {"remove_columns": [field_messages]} diff --git a/tests/prompt_strategies/test_dpo_chat_templates.py b/tests/prompt_strategies/test_dpo_chat_templates.py index b1802faa03..e5f30a6c43 100644 --- a/tests/prompt_strategies/test_dpo_chat_templates.py +++ b/tests/prompt_strategies/test_dpo_chat_templates.py @@ -103,7 +103,7 @@ class TestAssistantDPOChatTemplateLlama3: def test_llama3_defaults(self, llama3_tokenizer, assistant_dataset): # pylint: disable=duplicate-code - transform_fn = default( + transform_fn, _ = default( DictDefault( { "chat_template": "llama3", @@ -128,7 +128,7 @@ def test_llama3_defaults(self, llama3_tokenizer, assistant_dataset): def test_llama3_configured(self, llama3_tokenizer, custom_assistant_dataset): # pylint: disable=duplicate-code - transform_fn = default( + transform_fn, _ = default( DictDefault( { "chat_template": "llama3", @@ -169,7 +169,7 @@ class TestAssistantDPOChatTemplatePhi3: def test_phi3_defaults(self, phi3_tokenizer, assistant_dataset): # pylint: disable=duplicate-code - transform_fn = default( + transform_fn, _ = default( DictDefault( { "chat_template": "tokenizer_default", @@ -199,7 +199,7 @@ class TestAssistantDPOChatTemplateGemma: def test_gemma_defaults(self, gemma_tokenizer, assistant_dataset): # pylint: disable=duplicate-code - transform_fn = default( + transform_fn, _ = default( DictDefault( { "chat_template": "tokenizer_default", diff --git a/tests/test_datasets.py b/tests/test_datasets.py index 88d196ad16..bd77591cf2 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -289,7 +289,10 @@ def test_load_hub_with_dpo(self): train_dataset, _ = load_prepare_preference_datasets(cfg) assert len(train_dataset) == 1800 - assert "conversation" in train_dataset.features + assert "conversation" not in train_dataset.features + assert "chosen" in train_dataset.features + assert "rejected" in train_dataset.features + assert "prompt" in train_dataset.features @pytest.mark.skip(reason="TODO: fix hf hub offline to work with HF rate limits") @enable_hf_offline @@ -348,7 +351,10 @@ def test_load_hub_with_revision_with_dpo( train_dataset, _ = load_prepare_preference_datasets(cfg) assert len(train_dataset) == 1800 - assert "conversation" in train_dataset.features + assert "conversation" not in train_dataset.features + assert "chosen" in train_dataset.features + assert "rejected" in train_dataset.features + assert "prompt" in train_dataset.features @enable_hf_offline @pytest.mark.skip("datasets bug with local datasets when offline") From cb03c765a14fe9b01a39d425f933e6644df84551 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 5 Jun 2025 07:25:06 -0700 Subject: [PATCH 0699/1405] add uv tooling for e2e gpu tests (#2750) * add uv tooling for e2e gpu tests * fixes from PR feedback * simplify check * fix env var * make sure to use uv for other install * use raw_dockerfile_image * Fix import * fix args to experimental dockerfile image call * use updated modal versions --- .github/workflows/tests.yml | 120 +++-------------------------- cicd/Dockerfile-uv.jinja | 52 +++++++++++++ cicd/multigpu.py | 2 +- cicd/single_gpu.py | 14 ++-- scripts/cutcrossentropy_install.py | 6 +- scripts/unsloth_install.py | 7 +- 6 files changed, 81 insertions(+), 120 deletions(-) create mode 100644 cicd/Dockerfile-uv.jinja diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 29c5bef38d..ddbd252917 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,98 +44,6 @@ jobs: env: SKIP: no-commit-to-branch -# preload-cache: -# name: Preload HF cache -# runs-on: ubuntu-latest -# strategy: -# fail-fast: false -# matrix: -# python_version: ["3.11"] -# pytorch_version: ["2.6.0"] -# timeout-minutes: 20 -# -# env: -# AXOLOTL_IS_CI_CACHE_PRELOAD: "1" -# -# steps: -# - name: Check out repository code -# uses: actions/checkout@v4 -# -# - name: Restore HF cache -# id: hf-cache-restore -# uses: actions/cache/restore@v4 -# with: -# path: | -# /home/runner/.cache/huggingface/hub/datasets--* -# /home/runner/.cache/huggingface/hub/models--* -# key: ${{ runner.os }}-hf-hub-cache-v2 -# -# - name: Restore Cache from S3 -# id: hf-cache-restore-s3 -# run: | -# mkdir -p /home/runner/.cache/huggingface/hub -# curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xf - -C /home/runner/.cache/huggingface/hub/ --use-compress-program unzstd -# -# - name: Setup Python -# uses: actions/setup-python@v5 -# with: -# python-version: ${{ matrix.python_version }} -# cache: 'pip' # caching pip dependencies -# -# - name: upgrade pip -# run: | -# pip3 install --upgrade pip -# pip3 install --upgrade packaging==23.2 setuptools==75.8.0 wheel -# -# - name: Install PyTorch -# run: | -# pip3 install torch==${{ matrix.pytorch_version }} -# -# - name: Install dependencies -# run: | -# pip3 show torch -# pip3 install --no-build-isolation -U -e . -# python scripts/unsloth_install.py | sh -# python scripts/cutcrossentropy_install.py | sh -# pip3 install -r requirements-dev.txt -r requirements-tests.txt -# -# - name: Make sure PyTorch version wasn't clobbered -# run: | -# python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" -# -# - name: Ensure axolotl CLI was installed -# run: | -# axolotl --help -# -# - name: Pre-Download dataset fixture -# run: | -# huggingface-cli download --repo-type=dataset axolotl-ai-internal/axolotl-oss-dataset-fixtures -# -# - name: Run tests -# run: | -# pytest -v tests/conftest.py -# -# - name: Upload coverage to Codecov -# uses: codecov/codecov-action@v5 -# with: -# token: ${{ secrets.CODECOV_TOKEN }} -# files: ./coverage.xml -# flags: unittests,pytorch-${{ matrix.pytorch_version }} -# fail_ci_if_error: false -# -# - name: cleanup pip cache -# run: | -# find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; -# -# - name: Save HF cache -# id: hf-cache -# uses: actions/cache/save@v4 -# with: -# path: | -# /home/runner/.cache/huggingface/hub/datasets--* -# /home/runner/.cache/huggingface/hub/models--* -# key: ${{ steps.hf-cache-restore.outputs.cache-primary-key }} - pytest: name: PyTest runs-on: ubuntu-latest @@ -151,15 +59,6 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 -# - name: Restore HF cache -# id: hf-cache-restore -# uses: actions/cache/restore@v4 -# with: -# path: | -# /home/runner/.cache/huggingface/hub/datasets--* -# /home/runner/.cache/huggingface/hub/models--* -# key: ${{ runner.os }}-hf-hub-cache-v2 - - name: Restore Cache from S3 id: hf-cache-restore-s3 run: | @@ -222,7 +121,6 @@ jobs: pytest-sdist: name: PyTest from Source Dist runs-on: ubuntu-latest -# needs: [preload-cache] strategy: fail-fast: false matrix: @@ -234,15 +132,6 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 -# - name: Restore HF cache -# id: hf-cache-restore -# uses: actions/cache/restore@v4 -# with: -# path: | -# /home/runner/.cache/huggingface/hub/datasets--* -# /home/runner/.cache/huggingface/hub/models--* -# key: ${{ runner.os }}-hf-hub-cache-v2 - - name: Restore Cache from S3 id: hf-cache-restore-s3 run: | @@ -312,6 +201,13 @@ jobs: pytorch: 2.6.0 num_gpus: 1 axolotl_extras: vllm + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.6.0 + num_gpus: 1 + axolotl_extras: + dockerfile: "Dockerfile-uv.jinja" steps: - name: Checkout uses: actions/checkout@v4 @@ -333,6 +229,7 @@ jobs: echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV + echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV - name: Run tests job on Modal run: | modal run cicd.e2e_tests @@ -395,6 +292,7 @@ jobs: echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV + echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV - name: Run tests job on Modal run: | modal run cicd.e2e_tests diff --git a/cicd/Dockerfile-uv.jinja b/cicd/Dockerfile-uv.jinja new file mode 100644 index 0000000000..84527274d3 --- /dev/null +++ b/cicd/Dockerfile-uv.jinja @@ -0,0 +1,52 @@ +FROM axolotlai/axolotl-base-uv:{{ BASE_TAG }} + +ENV TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6 9.0+PTX" +ENV AXOLOTL_EXTRAS="{{ AXOLOTL_EXTRAS }}" +ENV AXOLOTL_ARGS="{{ AXOLOTL_ARGS }}" +ENV CUDA="{{ CUDA }}" +ENV PYTORCH_VERSION="{{ PYTORCH_VERSION }}" +ENV GITHUB_REF="{{ GITHUB_REF }}" +ENV GITHUB_SHA="{{ GITHUB_SHA }}" +ENV NIGHTLY_BUILD="{{ NIGHTLY_BUILD }}" +ENV HF_HOME="{{ HF_HOME }}" + +RUN apt-get update && \ + apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev + +WORKDIR /workspace + +RUN git clone --depth=1 https://github.com/axolotl-ai-cloud/axolotl.git + +WORKDIR /workspace/axolotl + +RUN git fetch origin +$GITHUB_REF && \ + git checkout FETCH_HEAD + +# If AXOLOTL_EXTRAS is set, append it in brackets +RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ + sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt; \ + sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt; \ + sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt; \ + sed -i 's#^trl.*#trl @ git+https://github.com/huggingface/trl.git@main#' requirements.txt; \ + sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt; \ + fi + +RUN uv pip install packaging==23.2 setuptools==75.8.0 +RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ + uv pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + else \ + uv pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray] $AXOLOTL_ARGS; \ + fi + +RUN python scripts/unsloth_install.py --uv | sh +RUN python scripts/cutcrossentropy_install.py --uv | sh + +# So we can test the Docker image +RUN uv pip install -r requirements-dev.txt -r requirements-tests.txt + +# fix so that git fetch/pull from remote works +RUN git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && \ + git config --get remote.origin.fetch + +# helper for huggingface-login cli +RUN git config --global credential.helper store diff --git a/cicd/multigpu.py b/cicd/multigpu.py index 9819d3760c..a2dd8d0b32 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -55,7 +55,7 @@ } N_GPUS = int(os.environ.get("N_GPUS", 2)) -GPU_CONFIG = modal.gpu.H100(count=N_GPUS) +GPU_CONFIG = f"H100:{N_GPUS}" def run_cmd(cmd: str, run_folder: str): diff --git a/cicd/single_gpu.py b/cicd/single_gpu.py index 35dd0de594..2ce3b06621 100644 --- a/cicd/single_gpu.py +++ b/cicd/single_gpu.py @@ -8,8 +8,9 @@ import jinja2 import modal +import modal.experimental from jinja2 import select_autoescape -from modal import App, Image +from modal import App cicd_path = pathlib.Path(__file__).parent.resolve() @@ -17,7 +18,8 @@ template_env = jinja2.Environment( loader=template_loader, autoescape=select_autoescape() ) -df_template = template_env.get_template("Dockerfile.jinja") +dockerfile = os.environ.get("E2E_DOCKERFILE", "Dockerfile.jinja") +df_template = template_env.get_template(dockerfile) df_args = { "AXOLOTL_EXTRAS": os.environ.get("AXOLOTL_EXTRAS", ""), @@ -38,11 +40,11 @@ with open(pathlib.Path(temp_dir) / "Dockerfile", "w", encoding="utf-8") as f: f.write(dockerfile_contents) -cicd_image = Image.from_dockerfile( +cicd_image = modal.experimental.raw_dockerfile_image( pathlib.Path(temp_dir) / "Dockerfile", - context_mount=None, + # context_mount=None, force_build=True, - gpu="A10G", + # gpu="A10G", ).env(df_args) app = App("Axolotl CI/CD", secrets=[]) @@ -55,7 +57,7 @@ } N_GPUS = int(os.environ.get("N_GPUS", 1)) -GPU_CONFIG = modal.gpu.L40S(count=N_GPUS) +GPU_CONFIG = f"L40S:{N_GPUS}" def run_cmd(cmd: str, run_folder: str): diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 3ff6dfa8fa..4a92746c19 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -9,6 +9,8 @@ raise ImportError("Install torch via `pip install torch`") from exc from packaging.version import Version as V +USE_UV = "--uv" in sys.argv[1:] + v = V(torch.__version__) # no cut-cross-entropy support for torch < 2.4.0 @@ -23,7 +25,9 @@ if not importlib.util.find_spec("cut_cross_entropy.transformers"): UNINSTALL_PREFIX = "pip uninstall -y cut-cross-entropy && " +UV_PREFIX = "uv " if USE_UV else "" + print( UNINSTALL_PREFIX - + 'pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@a1174ca"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@a1174ca"' ) diff --git a/scripts/unsloth_install.py b/scripts/unsloth_install.py index bffab4670c..acbd05e901 100644 --- a/scripts/unsloth_install.py +++ b/scripts/unsloth_install.py @@ -1,11 +1,15 @@ # noqa # pylint: skip-file +import sys + try: import torch except ImportError: raise ImportError("Install torch via `pip install torch`") from packaging.version import Version as V +use_uv = "--uv" in sys.argv[1:] + v = V(torch.__version__) cuda = str(torch.version.cuda) try: @@ -31,6 +35,7 @@ else: raise RuntimeError(f"Torch = {v} too new!") x = x.format(cuda.replace(".", ""), "-ampere" if is_ampere else "") +uv_prefix = "uv " if use_uv else "" print( - f'pip install unsloth-zoo==2024.12.1 && pip install --no-deps "unsloth[{x}]==2024.12.4"' + f'{uv_prefix}pip install unsloth-zoo==2024.12.1 && {uv_prefix}pip install --no-deps "unsloth[{x}]==2024.12.4"' ) From 7909bfb076a67c31d407f89212e6143afa4aa05d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 5 Jun 2025 09:23:17 -0700 Subject: [PATCH 0700/1405] add manual seed for flaky test_geglu_backward test (#2763) [skip ci] --- tests/e2e/kernels/test_geglu.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/e2e/kernels/test_geglu.py b/tests/e2e/kernels/test_geglu.py index 005a1935d6..90403ab4ab 100644 --- a/tests/e2e/kernels/test_geglu.py +++ b/tests/e2e/kernels/test_geglu.py @@ -1,7 +1,6 @@ """Tests for GEGLU activation function Triton kernels.""" -# pylint: disable=duplicate-code - +import pytest import torch import torch.nn.functional as F @@ -34,8 +33,14 @@ def test_geglu_forward_values(): assert torch.allclose(triton_out, torch_out, rtol=1e-3) -def test_geglu_backward(): +@pytest.mark.parametrize( + "torch_seed", + [0, 42], +) +def test_geglu_backward(torch_seed): """Test GEGLU backward pass matches PyTorch autograd.""" + torch.manual_seed(torch_seed) + gate = torch.randn(2, 3, 64, device="cuda", requires_grad=True) up = torch.randn(2, 3, 64, device="cuda", requires_grad=True) grad_output = torch.randn(2, 3, 64, device="cuda") From 09c685fd2c80f30e0d4a8ee937f8cc9058fce15f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 8 Jun 2025 23:14:10 -0700 Subject: [PATCH 0701/1405] fix worker_init_fn signature handling (#2769) --- src/axolotl/core/trainers/grpo/trainer.py | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index dccc85d80d..c97fccd31f 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -3,6 +3,7 @@ # pylint: disable=too-many-lines,duplicate-code,protected-access,no-member import warnings +from functools import partial from typing import Any import datasets @@ -58,6 +59,42 @@ class AxolotlGRPOTrainer( _tag_names = ["trl", "grpo", "axolotl"] + def get_train_dataloader(self): + if self.train_dataset is None: + raise ValueError("Trainer: training requires a train_dataset.") + + train_dataset = self.train_dataset + data_collator = self.data_collator + if isinstance(train_dataset, datasets.Dataset): + train_dataset = self._remove_unused_columns( + train_dataset, description="training" + ) + else: + data_collator = self._get_collator_with_removed_columns( + data_collator, description="training" + ) + + dataloader_params = { + "batch_size": self._train_batch_size + * self.args.steps_per_generation, # < this is the change + "collate_fn": data_collator, + "num_workers": self.args.dataloader_num_workers, + "pin_memory": self.args.dataloader_pin_memory, + "persistent_workers": self.args.dataloader_persistent_workers, + } + + if not isinstance(train_dataset, torch.utils.data.IterableDataset): + dataloader_params["sampler"] = self._get_train_sampler() + dataloader_params["drop_last"] = self.args.dataloader_drop_last + dataloader_params["worker_init_fn"] = partial( + seed_worker, + num_workers=self.args.dataloader_num_workers, + rank=self.args.process_index, + ) + dataloader_params["prefetch_factor"] = self.args.dataloader_prefetch_factor + + return self.accelerator.prepare(DataLoader(train_dataset, **dataloader_params)) + class AxolotlGRPOSequenceParallelTrainer(AxolotlGRPOTrainer): """Extend the base GRPOTrainer for sequence parallelism handling""" From dd660c2ed046e8715cdf73c23cf14066ac165ce7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 9 Jun 2025 21:26:14 -0700 Subject: [PATCH 0702/1405] handle when unable to save optimizer state when using ao optimizer with FSDP (#2773) [skip ci] * handle when unable to save optimizer state when using ao optimizer with FSDP1 * improve messaging Co-authored-by: salman --------- Co-authored-by: salman --- src/axolotl/core/trainers/base.py | 5 ++++- src/axolotl/core/trainers/mixins/__init__.py | 1 + .../core/trainers/mixins/checkpoints.py | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/axolotl/core/trainers/mixins/checkpoints.py diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 70e443cb30..d6f2c579ae 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -25,6 +25,7 @@ from typing_extensions import override from axolotl.core.trainers.mixins import ( + CheckpointSaveMixin, OptimizerMixin, RngLoaderMixin, SchedulerMixin, @@ -39,7 +40,9 @@ LOG = get_logger(__name__) -class AxolotlTrainer(SchedulerMixin, OptimizerMixin, RngLoaderMixin, Trainer): +class AxolotlTrainer( + SchedulerMixin, OptimizerMixin, RngLoaderMixin, CheckpointSaveMixin, Trainer +): """Extend the base Trainer for axolotl helpers""" args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] diff --git a/src/axolotl/core/trainers/mixins/__init__.py b/src/axolotl/core/trainers/mixins/__init__.py index a71cb321ab..1782320778 100644 --- a/src/axolotl/core/trainers/mixins/__init__.py +++ b/src/axolotl/core/trainers/mixins/__init__.py @@ -3,6 +3,7 @@ # pylint: disable=unused-import # flake8: noqa +from .checkpoints import CheckpointSaveMixin from .optimizer import OptimizerMixin from .rng_state_loader import RngLoaderMixin from .scheduler import SchedulerMixin diff --git a/src/axolotl/core/trainers/mixins/checkpoints.py b/src/axolotl/core/trainers/mixins/checkpoints.py new file mode 100644 index 0000000000..8f994d78cc --- /dev/null +++ b/src/axolotl/core/trainers/mixins/checkpoints.py @@ -0,0 +1,21 @@ +"""Custom handling to not fail training if fsdp optimizer is not savable""" + +from transformers import Trainer + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +class CheckpointSaveMixin(Trainer): + """Mixin to handle saving the optimizer and scheduler if they are not savable.""" + + def _save_optimizer_and_scheduler(self, output_dir): + try: + super()._save_optimizer_and_scheduler(output_dir) + except NotImplementedError as exc: + LOG.warning( + f"Trainer does not support saving optimizer and scheduler: {exc}\n" + "Optimizer and scheduler states were not saved - resuming from checkpoints " + "for this training run will not be possible." + ) From 92afa4fa272c9139c58e8c4527cedb61750cc3e1 Mon Sep 17 00:00:00 2001 From: Qingyang Wu Date: Mon, 9 Jun 2025 21:26:36 -0700 Subject: [PATCH 0703/1405] Fix the bug of position ids padding (#2739) [skip ci] * Update batching.py: fix the bug of position ids padding if position ids is padded with a long sequence of zeros, it will cause flash attention to crash * use alternate calculation for padding position_ids with a range --------- Co-authored-by: Wing Lian --- src/axolotl/utils/collators/batching.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/collators/batching.py b/src/axolotl/utils/collators/batching.py index 45facf832a..d8414d1175 100644 --- a/src/axolotl/utils/collators/batching.py +++ b/src/axolotl/utils/collators/batching.py @@ -81,9 +81,11 @@ def __call__(self, features, return_tensors=None): padding_side = self.tokenizer.padding_side for feature in features: - remainder = [pad_token_id] * ( - max_feature_length - len(feature[feature_name]) - ) + remainder_len = max_feature_length - len(feature[feature_name]) + if feature_name == "position_ids": + remainder = list(range(remainder_len)) + else: + remainder = [pad_token_id] * remainder_len if isinstance(feature[feature_name], list): feature[feature_name] = ( feature[feature_name] + remainder From 83632f71d82422819d4bd0020523a52624b1cb6b Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 9 Jun 2025 21:42:05 -0700 Subject: [PATCH 0704/1405] Feat: add tool calling support via tools column (#2774) * feat: add tool_calling field support * fix: add tests --- docs/config.qmd | 4 + docs/dataset-formats/conversation.qmd | 98 ++++++++++- .../prompt_strategies/chat_template.py | 92 +++++++--- src/axolotl/utils/schemas/datasets.py | 1 + .../test_chat_templates_advanced.py | 159 ++++++++++++++++++ 5 files changed, 327 insertions(+), 27 deletions(-) diff --git a/docs/config.qmd b/docs/config.qmd index 519065554c..2ca2367082 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -173,6 +173,10 @@ datasets: # Key containing the messages (default: "messages") field_messages: messages + # Key containing the tools (default: "tools") + # Must be a list[dict] and follow [JSON schema](https://json-schema.org/learn/getting-started-step-by-step). + field_tools: tools + # Key containing the system message (default: "system") # If the system message is not present in the dataset sample, it will be loaded from the field_system property. field_system: system diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 87c2941e63..290841c085 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -52,7 +52,9 @@ We recommend checking the below examples for other usecases. ### Examples -1. (Legacy) Using the default chat template in the tokenizer_config.json on OpenAI messages format, training on only last message. +#### Training on last message + +(Legacy) Using the default chat template in the tokenizer_config.json on OpenAI messages format, training on only last message. ```yaml datasets: @@ -66,7 +68,9 @@ datasets: If you receive an error like "`chat_template` choice is `tokenizer_default` but tokenizer's `chat_template` is null.", it means the tokenizer does not have a default `chat_template`. Follow the examples below instead to set a custom `chat_template`. ::: -2. Using the `gemma` chat template to override the tokenizer_config.json's chat template on OpenAI messages format, training on all assistant messages. +#### Overriding default chat template + +Using the `gemma` chat template to override the tokenizer_config.json's chat template on OpenAI messages format, training on all assistant messages. ```yaml chat_template: gemma # this overwrites the tokenizer's chat_template @@ -76,7 +80,13 @@ datasets: roles_to_train: ["assistant"] # default value ``` -3. Using the tokenizer_config.json's chat template or `chatml` as fallback if the former's chat template does not exist, on OpenAI messages format, training on all assistant messages. +::: {.callout-note} +If you want to use built-in chat_template, use `chat_template: tokenizer_default` (this is set by default). +::: + +#### Using default chat template with fallback + +Using the tokenizer_config.json's chat template or `chatml` as fallback if the former's chat template does not exist, on OpenAI messages format, training on all assistant messages. ```yaml chat_template: tokenizer_default_fallback_chatml # this overwrites the tokenizer's chat_template @@ -85,7 +95,9 @@ datasets: type: chat_template ``` -4. Using a custom jinja template on OpenAI messages format, training on all assistant messages. +#### Custom Jinja template + +Using a custom jinja template on OpenAI messages format, training on all assistant messages. ```yaml # chat_template: jinja # `jinja` will be implied if the `chat_template_jinja` is set and this field is empty @@ -100,7 +112,9 @@ datasets: Please make sure that your `tokenizer.eos_token` is same as EOS (End-of-Sequence) token in template. Otherwise, set `eos_token` under `special_tokens: `. ::: -5. If you are using a template that has a different EOT (End-of-Turn) token from EOS token or multiple EOT tokens (like Mistral V7 Tekken), set the `eot_tokens: ` config. The handling of EOT tokens follows `train_on_eos: ` which defaults to turn. +#### Using template with different token for EOT and EOS + +- If you are using a template that has a different EOT (End-of-Turn) token from EOS token or multiple EOT tokens (like Mistral V7 Tekken), set the `eot_tokens: ` config. The handling of EOT tokens follows `train_on_eos: ` which defaults to turn. ```yaml eot_tokens: @@ -125,7 +139,7 @@ Using `eot_tokens` requires each token that exists in `chat_template` to be a si You can add those tokens as new tokens under `tokens: ` or (recommended) override unused added_tokens via `added_tokens_overrides: `. See [config](../config.qmd) for more details. ::: -6. Continuing from the previous example, if you want to train on all EOT token trainable turns but only last EOS token, set `train_on_eos: last`. +- Continuing from the previous example, if you want to train on all EOT token trainable turns but only last EOS token, set `train_on_eos: last`. ```yaml eot_tokens: @@ -145,7 +159,73 @@ If EOS token only appears at the end of a prompt, `train_on_eos: last` is equiva ::: -7. (Advanced) Using fine-grained control over tokens and turns to train in a conversation +#### Using tool use + +Instead of passing `tools` via the system prompt, an alternative method would be to have the `tools` in a separate column and loaded via `chat_template` to let the template dynamically build it. + +```json +{ + "tools": [ + { + "type": "...", + "function": { + "name": "...", + "description": "...", + "parameters": { + "type": "...", + "properties": { + // ... + }, + "required": ["..."], + }, + }, + }, + ], + "messages": [ + // ... + { + "role": "assistant", // call the function via assistant + "tool_calls": [ + { + "type": "function", + "function": { + "name": "...", + "arguments": { + "...": "...", + } + } + } + ] + }, + { + "role": "tool", + "name": "...", + "content": "..." + }, + ], +} +``` + +::: {.callout-note} +Tools need to follow [JSON schema](https://json-schema.org/learn/getting-started-step-by-step). +::: + +```yaml +chat_template: llama4 +datasets: + - path: ... + type: chat_template + # field_tools: tools # default is `tools` +``` + +::: {.callout-tip} +Look into the `chat_template` you are using to see if it supports `tools` and what the expected role is for the tool answer. In the example above, the tool answer is expected to be in the `tool` or `ipython` role for `llama4` template. +::: + + +#### Using fine-grained control over token masking + +(Advanced) Using fine-grained control over tokens and turns to train in a conversation For a data sample that looks like: @@ -196,7 +276,9 @@ datasets: It is not necessary to set both `message_field_training` and `message_field_training_detail` at once. ::: -8. (For Qwen3 template only) Enable reasoning split, where the reasoning is split from the content and passed as a separate field into the template. +#### Reasoning split + +(For Qwen3 template only) Enable reasoning split, where the reasoning is split from the content and passed as a separate field into the template. ```yaml datasets: diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index a0fd8d911e..1fee0f7f60 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -34,6 +34,7 @@ def __init__( message_field_training_detail: str | None = None, field_messages: str = "messages", field_system: str = "system", + field_tools: str = "tools", roles: dict[str, list[str]] | None = None, chat_template_kwargs: dict[str, Any] | None = None, drop_system_message: bool = False, @@ -66,6 +67,7 @@ def __init__( self.message_field_training_detail = message_field_training_detail self.field_messages = field_messages self.field_system = field_system + self.field_tools = field_tools self.tokenizer = tokenizer self.processor: ProcessorMixin | None = processor self.chat_template = chat_template @@ -77,17 +79,38 @@ def __init__( def chat_template_msg_variables(self) -> Set[str]: return self._chat_template_msg_variables - def build_prompt(self, conversation, add_generation_prompt=False, images=None): + def build_prompt( + self, + conversation, + add_generation_prompt=False, + images=None, + tools=None, + ): + """ + Build a prompt from a conversation. + + Args: + conversation: A list of messages. + add_generation_prompt: Whether to add a generation prompt. + images: A list of images. (optional) + tools: A list of tools. (optional) + """ + chat_template_kwargs = { + "chat_template": self.chat_template, + "add_generation_prompt": add_generation_prompt, + } + + if tools: + chat_template_kwargs["tools"] = tools + if self.processor: if not callable(self.processor): raise TypeError("Processor must be callable") text = self.processor.apply_chat_template( conversation, - chat_template=self.chat_template, tokenize=False, - add_generation_prompt=add_generation_prompt, - **self.chat_template_kwargs, + **chat_template_kwargs, ) batch = self.processor( text=text, @@ -104,9 +127,7 @@ def build_prompt(self, conversation, add_generation_prompt=False, images=None): return self.tokenizer.apply_chat_template( conversation, - add_generation_prompt=add_generation_prompt, - chat_template=self.chat_template, - **self.chat_template_kwargs, + **chat_template_kwargs, ) def get_offsets_for_train_detail( @@ -376,7 +397,7 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: and not self.prompter.message_field_training_detail # type: ignore ): turns = self.get_conversation_thread(prompt) - images = self.get_images(prompt) + images = self._get_images(prompt) prompt_ids = self.prompter.build_prompt( # type: ignore turns[:-1], add_generation_prompt=True, @@ -405,7 +426,8 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: return tokenized_prompt turns = self.get_conversation_thread(prompt) - input_ids = self.prompter.build_prompt(turns) # type: ignore + tools = self._get_tools(prompt) + input_ids = self.prompter.build_prompt(turns, tools=tools) # type: ignore labels = [IGNORE_TOKEN_ID] * len(input_ids) last_eos_idx = -1 @@ -444,7 +466,9 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: continue - turn_start_idx, turn_end_idx = self.find_turn(turns=turns, turn_idx=index) + turn_start_idx, turn_end_idx = self.find_turn( + turns=turns, turn_idx=index, tools=tools + ) LOG.debug(f"Turn indices: start={turn_start_idx}, end={turn_end_idx}") @@ -546,7 +570,9 @@ def find_first_eot_token(self, input_ids, start_idx): return i return -1 - def find_turn(self, turns: list[dict], turn_idx: int): + def find_turn( + self, turns: list[dict], turn_idx: int, tools: list[dict] | None = None + ): """ Locate the starting and ending indices of the specified turn in a conversation. """ @@ -577,10 +603,10 @@ def find_turn(self, turns: list[dict], turn_idx: int): turns_with_content = turns[: turn_idx + 1] # Generate the conversation up to the turn, with final turn replaced with dummy content - dummy_ids = self.prompter.build_prompt(turns_with_empty) # type: ignore + dummy_ids = self.prompter.build_prompt(turns_with_empty, tools=tools) # type: ignore # Generate the conversation up to the turn, with final turn included - full_ids = self.prompter.build_prompt(turns_with_content) # type: ignore + full_ids = self.prompter.build_prompt(turns_with_content, tools=tools) # type: ignore if not full_ids or not dummy_ids: LOG.warning(f"Empty template generated for turn {turn_idx}") @@ -633,9 +659,10 @@ def find_turn(self, turns: list[dict], turn_idx: int): def get_conversation_thread(self, prompt): turns = [] - possible_sys_turn = self.transform_message( - prompt[self.prompter.field_messages][0] - ) + messages = self._get_messages(prompt) + + possible_sys_turn = self.transform_message(messages[0]) + if ( possible_sys_turn["role"] != "system" and self.prompter.field_system in prompt @@ -643,7 +670,7 @@ def get_conversation_thread(self, prompt): turn = {"role": "system", "content": prompt[self.prompter.field_system]} turns.append(turn) - for message in prompt[self.prompter.field_messages]: + for message in messages: transformed_message = self.transform_message(message) turn = { @@ -661,7 +688,7 @@ def get_conversation_thread(self, prompt): return turns - def transform_message(self, message): + def transform_message(self, message: dict) -> dict: # Build the initial transformed message from the mappings transformed_message = {} for key, value in self.prompter.message_property_mappings.items(): @@ -738,9 +765,36 @@ def transform_message(self, message): return transformed_message - def get_images(self, prompt): + def _get_images(self, prompt): return prompt.get(self.images, None) + def _get_tools(self, prompt) -> list[dict] | None: + """Get tools from prompt if available.""" + tools = prompt.get(self.prompter.field_tools, None) + if tools is None: + return None + + if isinstance(tools, list): + return tools + + raise ValueError( + "Unknown tools format. Please convert it into a list[dict].\n" + f"Current format: {type(tools)}" + ) + + def _get_messages(self, prompt): + messages = prompt.get(self.prompter.field_messages, None) + if messages is None: + raise ValueError("Messages is null. Please check `field_messages`.") + + if isinstance(messages, list): + return messages + + raise ValueError( + "Unknown messages format. Please convert it into a list[dict].\n" + f"Current format: {type(messages)}" + ) + class StrategyLoader: """ diff --git a/src/axolotl/utils/schemas/datasets.py b/src/axolotl/utils/schemas/datasets.py index cc5d6daba2..c71f9be77f 100644 --- a/src/axolotl/utils/schemas/datasets.py +++ b/src/axolotl/utils/schemas/datasets.py @@ -43,6 +43,7 @@ class SFTDataset(BaseModel): field_human: str | None = None field_model: str | None = None field_messages: str | None = None + field_tools: str | None = None # deprecated, use message_property_mappings message_field_role: str | None = None # deprecated, use message_property_mappings diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index 7f011f9543..fcf860f815 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -1280,3 +1280,162 @@ def test_multiple_train_on_eot_settings( assert ( labels[eos_idx] != IGNORE_TOKEN_ID ), f"Expected EOT token at index {eos_idx} to be labeled with train_on_eot='{setting}'" + + +class TestChatTemplateToolCalling: + """ + Test class for tool calling functionality with chat templates. + """ + + def test_tool_calling_with_llama4_template( + self, + llama3_tokenizer, + ): + LOG.info("Testing tool calling with llama3 tokenizer and llama4 chat template") + + # Create tool calling dataset + tool_calling_dataset = [ + { + "tools": [ + { + "type": "function", + "function": { + "name": "xml_escape", + "description": 'Replaces any "<", ">", or "&" characters in the input string with their corresponding XML entities.', + "parameters": { + "type": "object", + "properties": { + "s": { + "type": "string", + "description": "The input string to be XML-escaped.", + } + }, + "required": ["s"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "multiples", + "description": "Generates a list of all the multiples of a number that are less than a given limit.", + "parameters": { + "type": "object", + "properties": { + "number": { + "type": "integer", + "description": "The number to find multiples of.", + }, + "limit": { + "type": "integer", + "description": "The upper limit for the multiples.", + }, + }, + "required": ["number", "limit"], + }, + }, + }, + ], + "messages": [ + { + "role": "user", + "content": "Can you help me find multiples of 5 that are less than 20?", + }, + { + "role": "assistant", + "tool_calls": [ + { + "type": "function", + "function": { + "name": "multiples", + "arguments": { + "number": 5, + "limit": 20, + }, + }, + } + ], + }, + {"role": "tool", "name": "multiples", "content": "5,10,15"}, + { + "role": "assistant", + "content": "The multiples of 5 less than 20 are: 5, 10, and 15.", + }, + ], + } + ] + + # Setup tokenizer with llama4 chat template + tokenizer = deepcopy(llama3_tokenizer) + + # Add EOS token to the tokenizer + eot_token = "<|eot_id|>" + tokenizer.add_special_tokens({"additional_special_tokens": [eot_token]}) + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + tokenizer, + chat_template=get_chat_template("llama4"), + message_property_mappings={"role": "role", "content": "content"}, + field_messages="messages", + field_tools="tools", + ), + tokenizer=tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + eot_tokens=[eot_token], + ) + + res = strategy.tokenize_prompt(tool_calling_dataset[0]) + input_ids = res["input_ids"] + labels = res["labels"] + + # Verify that the input_ids contain expected tokens + assert len(input_ids) > 0, "Input IDs should not be empty" + assert len(labels) == len(input_ids), "Labels should match input_ids length" + + # Decode the full conversation to verify structure + decoded_conversation = tokenizer.decode(input_ids) + + # Verify tool calling structure is present in the decoded conversation + assert ( + '"type": "function",' in decoded_conversation + ), "Tool type function should be in conversation" + assert ( + '"name": "multiples",' in decoded_conversation + ), "Tool function name should be in conversation" + + assert ( + '<|python_start|><|python_end|>{"name": "multiples", "parameters": {"number": 5, "limit": 20}}<|eot|>' + in decoded_conversation + ), "Assistant tool call should be in conversation" + assert ( + "<|header_start|>ipython<|header_end|>" in decoded_conversation + ), "IPython header should be in conversation" + assert ( + '"5,10,15"' in decoded_conversation + ), "Tool response should be in conversation" + + # Get conversation turns to verify labeling + turns = strategy.get_conversation_thread(tool_calling_dataset[0]) + tools = strategy._get_tools( # pylint: disable=protected-access + tool_calling_dataset[0] + ) + + # Check that assistant responses are properly labeled + for i, turn in enumerate(tool_calling_dataset[0]["messages"]): + if turn["role"] == "assistant": + start_idx, end_idx = strategy.find_turn( + turns=turns, turn_idx=i, tools=tools + ) + + assert ( + start_idx != -1 and end_idx != -1 + ), f"Assistant turn {i} should be found" + + # Verify that assistant responses have proper labels + turn_labels = labels[start_idx:end_idx] + assert all( + label != IGNORE_TOKEN_ID for label in turn_labels + ), f"Assistant turn {i} should be unmasked" From 52a0452acb9453bba48b25d182fa1b6a23b3c0f2 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Tue, 10 Jun 2025 10:03:41 -0700 Subject: [PATCH 0705/1405] magistral small placeholder (#2777) --- examples/magistral/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/magistral/README.md diff --git a/examples/magistral/README.md b/examples/magistral/README.md new file mode 100644 index 0000000000..172d9ac933 --- /dev/null +++ b/examples/magistral/README.md @@ -0,0 +1,3 @@ +# Coming Soon! + +Watch this space for configs for fine-tuning [Magistral Small 2506](https://huggingface.co/mistralai/Magistral-Small-2506). From 00cda8cc70ca6a2f501cef7e843ae87931faece7 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Tue, 10 Jun 2025 19:53:07 -0400 Subject: [PATCH 0706/1405] Data loader refactor (#2707) * data loading refactor (wip) * updates * progress * pytest * pytest fix * lint * zero_first -> filelock, more simplifications * small simplification * import change * nit * lint * simplify dedup * couldnt resist * review comments WIP * continued wip * minor changes * fix; remove contrived test * further refactor * set default seed in pydantic config * lint * continued simplication * lint * renaming and nits * filelock tests * fix * fix * lint * remove nullable arg * remove unnecessary code * moving dataset save fn to shared module * remove debug print * matching var naming * fn name change * coderabbit comments * naming nit * fix test --- src/axolotl/common/const.py | 4 +- src/axolotl/common/datasets.py | 77 +- src/axolotl/core/builders/causal.py | 2 +- src/axolotl/datasets.py | 43 +- src/axolotl/loaders/tokenizer.py | 9 +- .../prompt_strategies/messages/__init__.py | 1 - src/axolotl/prompt_tokenizers.py | 11 + src/axolotl/train.py | 4 +- src/axolotl/utils/data/__init__.py | 25 +- src/axolotl/utils/data/lock.py | 66 ++ src/axolotl/utils/data/pretraining.py | 2 +- src/axolotl/utils/data/rl.py | 373 ++++--- src/axolotl/utils/data/sft.py | 994 ++++++++---------- src/axolotl/utils/data/shared.py | 682 ++++++++---- src/axolotl/utils/data/utils.py | 161 ++- src/axolotl/utils/data/wrappers.py | 425 ++++++++ src/axolotl/utils/schemas/config.py | 10 +- tests/core/test_builders.py | 12 +- .../integrations/test_cut_cross_entropy.py | 10 +- tests/e2e/integrations/test_hooks.py | 4 +- tests/e2e/integrations/test_kd.py | 7 +- tests/e2e/integrations/test_liger.py | 7 +- tests/e2e/integrations/test_llm_compressor.py | 4 +- tests/e2e/multigpu/solo/test_grpo.py | 2 +- tests/e2e/multigpu/test_locking.py | 192 ++++ tests/e2e/patched/test_4d_multipack_llama.py | 7 +- .../patched/test_activation_checkpointing.py | 4 +- tests/e2e/patched/test_fa_xentropy.py | 4 +- tests/e2e/patched/test_falcon_samplepack.py | 7 +- tests/e2e/patched/test_fused_llama.py | 4 +- tests/e2e/patched/test_llama_s2_attention.py | 7 +- .../e2e/patched/test_lora_llama_multipack.py | 7 +- tests/e2e/patched/test_mistral_samplepack.py | 7 +- tests/e2e/patched/test_mixtral_samplepack.py | 7 +- tests/e2e/patched/test_phi_multipack.py | 7 +- tests/e2e/patched/test_resume.py | 5 +- tests/e2e/patched/test_unsloth_qlora.py | 10 +- tests/e2e/solo/test_flex.py | 4 +- tests/e2e/solo/test_relora_llama.py | 4 +- tests/e2e/test_deepseekv3.py | 7 +- tests/e2e/test_dpo.py | 4 +- tests/e2e/test_embeddings_lr.py | 7 +- tests/e2e/test_falcon.py | 10 +- tests/e2e/test_gemma2.py | 7 +- tests/e2e/test_gemma3_text.py | 7 +- tests/e2e/test_llama.py | 13 +- tests/e2e/test_llama_pretrain.py | 12 +- tests/e2e/test_llama_vision.py | 7 +- tests/e2e/test_lora_llama.py | 4 +- tests/e2e/test_mamba.py | 4 +- tests/e2e/test_mistral.py | 7 +- tests/e2e/test_mixtral.py | 16 +- tests/e2e/test_optimizers.py | 16 +- tests/e2e/test_packing_loss.py | 4 +- tests/e2e/test_phi.py | 7 +- .../e2e/test_process_reward_model_smollm2.py | 4 +- tests/e2e/test_qat.py | 4 +- tests/e2e/test_reward_model_smollm2.py | 4 +- tests/e2e/test_schedulers.py | 4 +- tests/prompt_strategies/test_dpo_chatml.py | 6 +- tests/test_datasets.py | 81 +- tests/test_exact_deduplication.py | 111 +- 62 files changed, 2123 insertions(+), 1434 deletions(-) create mode 100644 src/axolotl/utils/data/lock.py create mode 100644 src/axolotl/utils/data/wrappers.py create mode 100644 tests/e2e/multigpu/test_locking.py diff --git a/src/axolotl/common/const.py b/src/axolotl/common/const.py index fd34ad4694..8aae06e997 100644 --- a/src/axolotl/common/const.py +++ b/src/axolotl/common/const.py @@ -1,5 +1,3 @@ -""" -Various shared constants -""" +"""Various shared constants""" DEFAULT_DATASET_PREPARED_PATH = "last_run_prepared" diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index d9c3841124..4d64958b67 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -3,15 +3,13 @@ import math import random from dataclasses import dataclass -from typing import Optional, Union from datasets import Dataset import axolotl.monkeypatch.data.batch_dataset_fetcher # pylint: disable=unused-import # noqa: F401 from axolotl.cli.args import PreprocessCliArgs, TrainerCliArgs from axolotl.loaders import load_processor, load_tokenizer -from axolotl.utils.data import prepare_dataset -from axolotl.utils.data.rl import load_prepare_preference_datasets +from axolotl.utils.data import prepare_datasets, prepare_preference_datasets from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import RLType @@ -30,16 +28,7 @@ class TrainDatasetMeta: def sample_dataset(dataset: Dataset, num_samples: int) -> Dataset: - """ - Randomly sample `num_samples` samples from `dataset`. - - Args: - dataset: Dataset. - num_samples: Number of samples to return. - - Returns: - Random sample (with replacement) of examples in `dataset`. - """ + """Randomly sample `num_samples` samples with replacement from `dataset`.""" return dataset.select( [random.randrange(0, len(dataset) - 1) for _ in range(num_samples)] # nosec ) @@ -51,44 +40,37 @@ def load_datasets( cli_args: PreprocessCliArgs | TrainerCliArgs | None = None, debug: bool = False, ) -> TrainDatasetMeta: - """ - Loads one or more training or evaluation datasets, calling - `axolotl.utils.data.prepare_dataset`. Optionally, logs out debug information. + """Loads one or more training or evaluation datasets, calling + `axolotl.utils.data.prepare_datasets`. Optionally, logs out debug information. Args: cfg: Dictionary mapping `axolotl` config keys to values. cli_args: Command-specific CLI arguments. - debug: Whether to print out tokenization of sample + debug: Whether to print out tokenization of sample. This is duplicated in + `cfg` and `cli_args`, but is kept due to use in our Colab notebooks. Returns: Dataclass with fields for training and evaluation datasets and the computed - `total_num_steps`. + `total_num_steps`. """ tokenizer = load_tokenizer(cfg) processor = load_processor(cfg, tokenizer=tokenizer) if cfg.processor_type else None - preprocess_iterable = ( - cli_args - and hasattr(cli_args, "iterable") - and cli_args.iterable is not None - and cli_args.iterable - ) + preprocess_iterable = getattr(cli_args, "iterable", False) - train_dataset, eval_dataset, total_num_steps, prompters = prepare_dataset( + train_dataset, eval_dataset, total_num_steps, prompters = prepare_datasets( cfg, tokenizer, processor=processor, preprocess_iterable=preprocess_iterable, ) - if ( # pylint: disable=too-many-boolean-expressions - cli_args - and ( - cli_args.debug - or cfg.debug - or cli_args.debug_text_only - or int(cli_args.debug_num_examples) > 0 - ) - ) or debug: + if ( + cfg.debug + or getattr(cli_args, "debug", False) + or getattr(cli_args, "debug_text_only", False) + or getattr(cli_args, "debug_num_examples", 0) > 0 + or debug + ): LOG.info("check_dataset_labels...") num_examples = cli_args.debug_num_examples if cli_args else 1 @@ -113,13 +95,10 @@ def load_datasets( def load_preference_datasets( - *, - cfg: DictDefault, - cli_args: Union[PreprocessCliArgs, TrainerCliArgs], + *, cfg: DictDefault, cli_args: PreprocessCliArgs | TrainerCliArgs ) -> TrainDatasetMeta: - """ - Loads one or more training or evaluation datasets for RL training using paired - preference data, calling `axolotl.utils.data.rl.load_prepare_preference_datasets`. + """Loads one or more training or evaluation datasets for RL training using paired + preference data, calling `axolotl.utils.data.rl.prepare_preference_datasets`. Optionally, logs out debug information. Args: @@ -130,12 +109,14 @@ def load_preference_datasets( Dataclass with fields for training and evaluation datasets and the computed `total_num_steps`. """ - train_dataset, eval_dataset = load_prepare_preference_datasets(cfg) - total_num_steps: Optional[int] = int( - math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) - ) - if cfg.rl is RLType.GRPO: - total_num_steps = None + tokenizer = load_tokenizer(cfg) + train_dataset, eval_dataset = prepare_preference_datasets(cfg, tokenizer) + + total_num_steps: int | None = None + if cfg.rl is not RLType.GRPO: + total_num_steps = int( + math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) + ) if cli_args.debug or cfg.debug: LOG.info("check_dataset_labels...") @@ -143,8 +124,8 @@ def load_preference_datasets( tokenizer = load_tokenizer(cfg) train_samples = sample_dataset(train_dataset, cli_args.debug_num_examples) check_dataset_labels( - train_samples, - tokenizer, + dataset=train_samples, + tokenizer=tokenizer, num_examples=cli_args.debug_num_examples, text_only=cli_args.debug_text_only, rl_mode=True, diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 7a81616ba7..8ff565dbb6 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -381,7 +381,7 @@ def build(self, total_num_steps): elif "tokenizer" in sig.parameters: trainer_kwargs["tokenizer"] = self.tokenizer if ( - not (trainer_cls in [AxolotlRewardTrainer, AxolotlPRMTrainer]) + trainer_cls not in [AxolotlRewardTrainer, AxolotlPRMTrainer] and self.cfg.datasets is not None ): trainer_kwargs["dataset_tags"] = [ diff --git a/src/axolotl/datasets.py b/src/axolotl/datasets.py index 9f1d9500d6..7c112c59e7 100644 --- a/src/axolotl/datasets.py +++ b/src/axolotl/datasets.py @@ -1,7 +1,6 @@ """Module containing Dataset functionality""" import os -from typing import List, Optional, Union import torch from datasets import Dataset, IterableDataset @@ -20,21 +19,21 @@ class TokenizedPromptDataset(Dataset): - """ - Dataset that returns tokenized prompts from a stream of text files. - Args: - prompt_tokenizer (PromptTokenizingStrategy): The prompt tokenizing method for processing the data. - dataset (dataset.Dataset): Dataset with text files. - process_count (int): Number of processes to use for tokenizing. - keep_in_memory (bool): Whether to keep the tokenized dataset in memory. + """Dataset that returns tokenized prompts from a stream of text files. + + Args: + prompt_tokenizer: The prompt tokenizing method for processing the data. + dataset: Dataset with text files. + process_count: Number of processes to use for tokenizing. + keep_in_memory: Whether to keep the tokenized dataset in memory. """ def __init__( # pylint: disable=super-init-not-called self, prompt_tokenizer: PromptTokenizingStrategy, dataset: Dataset, - process_count: Optional[int] = None, - keep_in_memory: Optional[bool] = False, + process_count: int | None = None, + keep_in_memory: bool | None = False, **kwargs, ): self.prompt_tokenizer = prompt_tokenizer @@ -76,14 +75,14 @@ def process(self, dataset): def wrap_dataset_for_tokenized_prompt( prompt_tokenizer: PromptTokenizingStrategy, - dataset: Union[Dataset, IterableDataset], + dataset: Dataset | IterableDataset, **kwargs, ): if isinstance(dataset, IterableDataset): map_kwargs = {} if prompt_tokenizer.supports_batched: map_kwargs["batched"] = True - features = dataset.features.keys() + features = list(dataset.features.keys()) return dataset.map( prompt_tokenizer.tokenize_prompt, remove_columns=features, @@ -94,12 +93,13 @@ def wrap_dataset_for_tokenized_prompt( # TODO this isn't the best since it can't interleave datasets class ConstantLengthDataset(IterableDataset): - """ - Iterable dataset that returns constant length chunks of tokens from stream of text files. - Args: - tokenizer (Tokenizer): The processor used for processing the data. - dataset (dataset.Dataset): Dataset with text files. - seq_length (int): Length of token sequences to return. + """Iterable dataset that returns constant length chunks of tokens from stream of + text files. + + Args: + tokenizer: The processor used for processing the data. + dataset: Dataset with text files. + seq_length: Length of token sequences to return. """ def __init__( # pylint: disable=super-init-not-called @@ -110,7 +110,7 @@ def __init__( # pylint: disable=super-init-not-called ): self.tokenizer = tokenizer self.concat_token_id = tokenizer.eos_token_id - self.datasets: List[IterableDataset] = datasets + self.datasets: list[IterableDataset] = datasets self.seq_length = seq_length vocab_size = len(tokenizer.get_vocab()) @@ -174,7 +174,10 @@ def __iter__(self): } else: LOG.warning( - f"dropping batch due to tensor size mismatch input_ids: {input_ids.size()}, labels: {labels.size()}, attention_mask: {attention_mask.size()}" + "Dropping batch due to tensor size mismatch " + f"input_ids: {input_ids.size()}, " + f"labels: {labels.size()}, " + f"attention_mask: {attention_mask.size()}" ) buffer = { "input_ids": [], diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index c311d52472..5a174186d9 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -7,12 +7,14 @@ from transformers import ( AddedToken, AutoTokenizer, + PreTrainedTokenizer, ) from axolotl.integrations.base import PluginManager from axolotl.loaders.utils import get_linear_embedding_layers, load_model_config from axolotl.prompt_tokenizers import LLAMA_DEFAULT_EOS_TOKEN from axolotl.utils.chat_templates import get_chat_template_from_config +from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import ( barrier, is_local_main_process, @@ -117,7 +119,7 @@ def modify_tokenizer_files( return tokenizer_dir -def load_tokenizer(cfg): +def load_tokenizer(cfg: DictDefault) -> PreTrainedTokenizer: """Load and configure the tokenizer based on the provided config.""" model_config = load_model_config(cfg) tokenizer_kwargs = {} @@ -207,11 +209,12 @@ def load_tokenizer(cfg): ) and k != "pad_token" ): - lora_modules_to_save = ", ".join( + lora_modules_to_save_str = ", ".join( [f"`{x}`" for x in lora_modules_to_save] ) raise ValueError( - f"Please set lora_modules_to_save to [{lora_modules_to_save}] when using an adapter and changing the special tokens." + f"Please set lora_modules_to_save to [{lora_modules_to_save_str}] " + "when using an adapter and changing the special tokens." ) tokenizer.add_special_tokens( diff --git a/src/axolotl/prompt_strategies/messages/__init__.py b/src/axolotl/prompt_strategies/messages/__init__.py index cc7b84da18..6eae9dfd8a 100644 --- a/src/axolotl/prompt_strategies/messages/__init__.py +++ b/src/axolotl/prompt_strategies/messages/__init__.py @@ -32,4 +32,3 @@ def load(tokenizer, cfg, ds_cfg, processor=None): except Exception as exc: # pylint: disable=broad-exception-caught LOG.error(f"Failed to load prompt strategy `{strategy}`: {str(exc)}") raise exc - return None diff --git a/src/axolotl/prompt_tokenizers.py b/src/axolotl/prompt_tokenizers.py index cb1a1ba4ed..9ca645de3c 100644 --- a/src/axolotl/prompt_tokenizers.py +++ b/src/axolotl/prompt_tokenizers.py @@ -3,6 +3,7 @@ import abc from typing import Callable, Dict, List, Optional, Tuple, Union +from datasets import Dataset from transformers import BatchEncoding, PreTrainedTokenizer from axolotl.prompters import Prompter @@ -28,6 +29,16 @@ class DatasetWrappingStrategy(abc.ABC): Abstract class for wrapping datasets for Chat Messages """ + @abc.abstractmethod + def wrap_dataset( + self, + dataset, + process_count: int | None = None, + keep_in_memory: bool | None = False, + **kwargs, + ) -> Dataset: + pass + class PromptTokenizingStrategy(abc.ABC): """ diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 866a9c4548..13ac8ec0d3 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -53,8 +53,8 @@ def setup_model_and_tokenizer( ) -> tuple[ PreTrainedModel, PreTrainedTokenizer, PeftConfig | None, ProcessorMixin | None ]: - """ - Load the tokenizer, processor (for multimodal models), and model based on configuration. + """Load the tokenizer, processor (for multimodal models), and model based on + configuration. Args: cfg: Dictionary mapping `axolotl` config keys to values. diff --git a/src/axolotl/utils/data/__init__.py b/src/axolotl/utils/data/__init__.py index 8dedcbe690..d162a7d0bb 100644 --- a/src/axolotl/utils/data/__init__.py +++ b/src/axolotl/utils/data/__init__.py @@ -1,16 +1,21 @@ -""" -Data processing modules -""" +"""Init for `axolotl.utils.data` module.""" -from axolotl.utils.data.pretraining import ( # noqa: F401 +from axolotl.utils.data.pretraining import ( encode_pretraining, wrap_pretraining_dataset, ) -from axolotl.utils.data.rl import load_prepare_preference_datasets # noqa: F401 -from axolotl.utils.data.sft import ( # noqa: F401 +from axolotl.utils.data.rl import prepare_preference_datasets +from axolotl.utils.data.sft import ( get_dataset_wrapper, - load_prepare_datasets, - load_tokenized_prepared_datasets, - prepare_dataset, + prepare_datasets, ) -from axolotl.utils.data.utils import md5 # noqa: F401 +from axolotl.utils.data.utils import md5 + +__all__ = [ + "encode_pretraining", + "wrap_pretraining_dataset", + "prepare_preference_datasets", + "get_dataset_wrapper", + "prepare_datasets", + "md5", +] diff --git a/src/axolotl/utils/data/lock.py b/src/axolotl/utils/data/lock.py new file mode 100644 index 0000000000..f5ec1679b3 --- /dev/null +++ b/src/axolotl/utils/data/lock.py @@ -0,0 +1,66 @@ +"""Logic for loading / preparing a dataset once over all processes.""" + +import time +from pathlib import Path +from typing import Any, Callable + +from filelock import FileLock + +from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH +from axolotl.utils.dict import DictDefault + +LOCK_FILE_NAME = "datasets_prep.lock" +READY_FILE_NAME = "datasets_ready.flag" +PROCESS_COUNTER_FILE_NAME = "process_counter.txt" + + +class FileLockLoader: + """ + Simple class for abstracting single process data loading / processing. The first + process that creates a lock file does the work; the remaining procesees simply load + the preprocessed dataset once the first process is done. + """ + + def __init__(self, cfg: DictDefault): + self.cfg = cfg + self.dataset_prepared_path = ( + cfg.dataset_prepared_path or DEFAULT_DATASET_PREPARED_PATH + ) + self.lock_file_path = Path(self.dataset_prepared_path) / LOCK_FILE_NAME + self.ready_flag_path = Path(self.dataset_prepared_path) / READY_FILE_NAME + self.counter_path = Path(self.dataset_prepared_path) / PROCESS_COUNTER_FILE_NAME + + def load(self, load_fn: Callable[[], Any]) -> Any: + with FileLock(str(self.lock_file_path)): + self._increment_counter() + + if not self.ready_flag_path.exists(): + result = load_fn() + self.ready_flag_path.touch() + return result + + while not self.ready_flag_path.exists(): + time.sleep(1) + return load_fn() + + def _increment_counter(self): + """Safely increment the process counter.""" + if self.counter_path.exists(): + count = int(self.counter_path.read_text().strip()) + else: + count = 0 + self.counter_path.write_text(str(count + 1)) + + def cleanup(self): + """Clean up ready flag when last process is done.""" + with FileLock(str(self.lock_file_path)): + count = int(self.counter_path.read_text().strip()) + count -= 1 + + if count == 0: + # Last process cleans everything up + self.ready_flag_path.unlink(missing_ok=True) + self.counter_path.unlink(missing_ok=True) + else: + # Still have active processes + self.counter_path.write_text(str(count)) diff --git a/src/axolotl/utils/data/pretraining.py b/src/axolotl/utils/data/pretraining.py index 44d8d6fed0..4ff108aeef 100644 --- a/src/axolotl/utils/data/pretraining.py +++ b/src/axolotl/utils/data/pretraining.py @@ -250,7 +250,7 @@ def encode_packed_pretraining( # pylint: disable=duplicate-code # tokenize all the examples # rows get split with stride (overlap) - train_dataset = ds_wrapper(Dataset.from_dict(examples))[0] + train_dataset = ds_wrapper(dataset=Dataset.from_dict(examples))[0] train_dataset = process_pretraining_datasets_for_packing( train_dataset, diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index 9264c86abd..6fd5397586 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -1,75 +1,117 @@ -"""data handling specific to DPO""" +"""Data handling specific to RL trainers.""" import inspect from functools import partial -from pathlib import Path -from typing import Any, List, Union +from typing import Any, Callable, Literal -import yaml -from datasets import Dataset, DatasetDict, concatenate_datasets, load_from_disk +from datasets import Dataset, DatasetDict +from transformers import PreTrainedTokenizer -from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH from axolotl.loaders import load_tokenizer from axolotl.prompt_strategies.dpo import load as load_dpo from axolotl.prompt_strategies.kto import load as load_kto from axolotl.prompt_strategies.orpo import load as load_orpo -from axolotl.utils.data.shared import datasets_w_name_generator, load_dataset_w_config -from axolotl.utils.data.utils import deduplicate_and_log_datasets, md5 +from axolotl.utils.data.lock import FileLockLoader +from axolotl.utils.data.shared import ( + create_train_validation_split, + datasets_with_name_generator, + generate_dataset_hash_from_config, + load_dataset_with_config, + load_preprocessed_dataset, + merge_datasets, + save_preprocessed_dataset, + try_load_from_hub, +) +from axolotl.utils.data.utils import ( + deduplicate_and_log_datasets, + retry_on_request_exceptions, +) from axolotl.utils.dict import DictDefault -from axolotl.utils.distributed import is_main_process, zero_first from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import RLType LOG = get_logger(__name__) -def _get_path(ds_hash, cfg): - prepared_ds_path = ( - Path(cfg.dataset_prepared_path) / ds_hash - if cfg.dataset_prepared_path - else Path(DEFAULT_DATASET_PREPARED_PATH) / ds_hash - ) +@retry_on_request_exceptions(max_retries=3, delay=5) +def prepare_preference_datasets( + cfg: DictDefault, tokenizer: PreTrainedTokenizer +) -> tuple[Dataset, Dataset | None]: + """Load and prepare preference datasets for RL training. - return prepared_ds_path + Loads training and evaluation datasets, handling preprocessing, caching, and + deduplication as configured. Uses FileLock for distributed coordination. + Args: + cfg: Configuration object containing dataset and training settings. + tokenizer: Tokenizer to use for processing text. -def _load_preprocessed_ds(cfg, sub_cfg): - ds_hash = md5(yaml.dump(sub_cfg, Dumper=yaml.Dumper)) - prepared_ds_path = _get_path(ds_hash, cfg) - dataset = None + Returns: + Tuple of (train_dataset, eval_dataset). eval_dataset may be None + if no evaluation dataset is configured. + """ - # pylint: disable=duplicate-code - if ( - cfg.dataset_prepared_path - and any(prepared_ds_path.glob("*")) - and not cfg.is_preprocess - ): - LOG.info(f"Loading prepared dataset from disk at {prepared_ds_path}...") - dataset = load_from_disk(str(prepared_ds_path)) + def _load_datasets(): + # Load training dataset + train_dataset = _load_or_create_dataset_split(cfg, tokenizer, split="train") - return dataset + # Load or create evaluation dataset + eval_dataset: Dataset | None = None + if cfg.test_datasets: + eval_dataset = _load_or_create_dataset_split(cfg, tokenizer, split="test") + elif cfg.val_set_size: + # Create validation split from training data + train_dataset, eval_dataset = create_train_validation_split( + train_dataset, cfg, cfg.val_set_size + ) + return train_dataset, eval_dataset -def _save_preprocessed_ds(cfg, sub_cfg, dataset): - ds_hash = md5(yaml.dump(sub_cfg, Dumper=yaml.Dumper)) - prepared_ds_path = _get_path(ds_hash, cfg) + # Prepare datasets (with file locking logic for multiple ranks) + loader = FileLockLoader(cfg) + try: + train_dataset, eval_dataset = loader.load(_load_datasets) + finally: + loader.cleanup() - if cfg.is_preprocess and is_main_process(): - LOG.info(f"Loading prepared dataset from disk at {prepared_ds_path}...") - dataset.save_to_disk(str(prepared_ds_path)) + # Apply deduplication if configured + if cfg.dataset_exact_deduplication: + train_dataset, eval_dataset = deduplicate_and_log_datasets( + dataset=train_dataset, other_dataset=eval_dataset + ) + return train_dataset, eval_dataset -def map_dataset(cfg, data_set, ds_transform_fn, tokenizer, **map_kwargs): + +def _map_dataset( + cfg: DictDefault, + dataset: Dataset | DatasetDict, + ds_transform_fn: Callable[..., Any], + tokenizer: Any | None = None, + **map_kwargs: Any, +) -> Dataset: + """Apply transformation function to dataset. + + Args: + cfg: Configuration object. + dataset: Dataset to transform. + ds_transform_fn: Transformation function to apply. + tokenizer: Optional tokenizer for transformation. + **map_kwargs: Additional arguments for dataset mapping. + + Returns: + Transformed dataset. + """ sig = inspect.signature(ds_transform_fn) if "tokenizer" in sig.parameters: if not tokenizer: tokenizer = load_tokenizer(cfg) ds_transform_fn = partial(ds_transform_fn, tokenizer=tokenizer) - if isinstance(data_set, DatasetDict): - data_set = data_set["train"] + if isinstance(dataset, DatasetDict): + dataset = dataset["train"] - data_set = data_set.map( + dataset = dataset.map( ds_transform_fn, num_proc=cfg.dataset_processes, load_from_cache_file=not cfg.is_preprocess, @@ -77,13 +119,27 @@ def map_dataset(cfg, data_set, ds_transform_fn, tokenizer, **map_kwargs): **map_kwargs, ) - return data_set + return dataset + + +def _drop_long_sequences( + sample: dict[str, Any], rl: RLType, tokenizer: Any, sequence_len: int +) -> bool: + """Filter out samples that exceed maximum sequence length. + + Args: + sample: Dataset sample to check. + rl: Reinforcement learning type. + tokenizer: Tokenizer for length calculation. + sequence_len: Maximum allowed sequence length. + Returns: + True if sample should be kept, False if it should be dropped. -def drop_long_rl_seq( - sample, rl, tokenizer, sequence_len # pylint: disable=invalid-name -): - if rl in (RLType.DPO, RLType.IPO, RLType.ORPO, RLType.SIMPO): + Raises: + ValueError: If required keys are missing or RL type is unknown. + """ + if rl in {RLType.DPO, RLType.IPO, RLType.ORPO, RLType.SIMPO}: if not ( sample.get("prompt") and sample.get("chosen") and sample.get("rejected") ): @@ -123,132 +179,115 @@ def drop_long_rl_seq( raise ValueError("Unknown RL type") -def load_prepare_preference_datasets(cfg): - def load_split(dataset_cfgs, _cfg): - split_datasets: List[Any] = [] - use_auth_token = _cfg.hf_use_auth_token - for config_dataset in datasets_w_name_generator(dataset_cfgs): - ds: Union[Dataset, DatasetDict] = load_dataset_w_config( - config_dataset, use_auth_token, streaming=False - ) - split_datasets.append(ds) - - tokenizer = load_tokenizer(cfg) - - for i, data_set in enumerate(split_datasets): - _type = dataset_cfgs[i]["type"] - if _type: - if isinstance(_type, DictDefault): - _type = "user_defined.default" - if _cfg.rl is RLType.ORPO: - ds_transform_fn = load_orpo(_type, _cfg, dataset_idx=i) - elif _cfg.rl is RLType.KTO: - ds_transform_fn = load_kto(_type, _cfg, dataset_idx=i) - else: - ds_transform_fn = load_dpo(_type, _cfg, dataset_idx=i) - - map_kwargs = {} - if isinstance(ds_transform_fn, tuple): - ds_transform_fn, map_kwargs = ds_transform_fn - split_datasets[i] = map_dataset( - cfg, data_set, ds_transform_fn, tokenizer, **map_kwargs - ) - elif _cfg.rl is RLType.KTO: - ds_transform_fn = load_kto(_type, _cfg, dataset_idx=i) - map_kwargs = {} - if isinstance(ds_transform_fn, tuple): - ds_transform_fn, map_kwargs = ds_transform_fn - split_datasets[i] = map_dataset( - cfg, data_set, ds_transform_fn, tokenizer, **map_kwargs - ) +def _load_split(cfg: DictDefault, split: Literal["train", "test"]) -> Dataset: + """Load and process dataset split for RL training. + + Args: + cfg: Configuration object containing dataset settings. + split: Dataset split to load ("train" or "test"). + + Returns: + Combined and processed dataset for the specified split. + """ + datasets_configs = cfg.datasets if split == "train" else cfg.test_datasets + split_datasets: list[Dataset | DatasetDict] = [] + + for dataset_config in datasets_with_name_generator(datasets_configs): + dataset: Dataset | DatasetDict = load_dataset_with_config( + dataset_config, cfg.hf_use_auth_token, streaming=False + ) + split_datasets.append(dataset) + + tokenizer = load_tokenizer(cfg) + + for i, dataset in enumerate(split_datasets): + _type = datasets_configs[i]["type"] + if _type: + if isinstance(_type, DictDefault): + _type = "user_defined.default" + if cfg.rl is RLType.ORPO: + ds_transform_fn = load_orpo(_type, cfg, dataset_idx=i) + elif cfg.rl is RLType.KTO: + ds_transform_fn = load_kto(_type, cfg, dataset_idx=i) else: - # If no `type` is provided, assume the dataset is already in the expected format with - # "prompt", "chosen" and "rejected" already preprocessed - split_datasets[i] = data_set - - if not cfg.skip_prepare_dataset: - drop_long = partial( - drop_long_rl_seq, - rl=_cfg.rl, - tokenizer=tokenizer, - sequence_len=cfg.sequence_len, - ) - - prior_len = len(split_datasets[i]) - split_datasets[i] = split_datasets[i].filter( - drop_long, - num_proc=cfg.dataset_processes, - load_from_cache_file=not cfg.is_preprocess, - desc="Dropping Long Sequences", - ) - dropped = prior_len - len(split_datasets[i]) - if dropped: - LOG.warning( - f"Dropped {dropped} long samples from dataset index {i}" - ) - - combined_datasets = concatenate_datasets(split_datasets) - combined_datasets = combined_datasets.shuffle(seed=cfg.seed or 42) - - return combined_datasets - - with zero_first(is_main_process()): - train_is_preprocessed = False - eval_is_preprocessed = False - if train_dataset := _load_preprocessed_ds(cfg, cfg.datasets): - train_is_preprocessed = True + ds_transform_fn = load_dpo(_type, cfg, dataset_idx=i) + + map_kwargs: dict[str, Any] = {} + if isinstance(ds_transform_fn, tuple): + ds_transform_fn, map_kwargs = ds_transform_fn + split_datasets[i] = _map_dataset( + cfg, dataset, ds_transform_fn, tokenizer, **map_kwargs + ) else: - train_dataset = load_split(cfg.datasets, cfg) + # If no `type` is provided, assume the dataset is already in the expected format with + # "prompt", "chosen", and "rejected" already preprocessed + split_datasets[i] = dataset + + if not cfg.skip_prepare_dataset: + drop_long = partial( + _drop_long_sequences, + rl=cfg.rl, + tokenizer=tokenizer, + sequence_len=cfg.sequence_len, + ) - eval_dataset = None - if cfg.test_datasets: - if eval_dataset := _load_preprocessed_ds(cfg, cfg.test_datasets): - eval_is_preprocessed = True - else: - eval_dataset = load_split(cfg.test_datasets, cfg) - if not eval_dataset: - if cfg.val_set_size: - seed = cfg.seed if cfg.seed is not None else 42 - - # ensure we end up with the same fingerprint by doing rank0 first and being able to cache - to_hash_train = ( - train_dataset._fingerprint # pylint: disable=protected-access - + "|" - + str(cfg.val_set_size) - + "|" - + "train" - + "|" - + str(cfg.seed or 42) - ) - to_hash_test = ( - train_dataset._fingerprint # pylint: disable=protected-access - + "|" - + str(cfg.val_set_size) - + "|" - + "test" - + "|" - + str(cfg.seed or 42) - ) - train_fingerprint = md5(to_hash_train) - test_fingerprint = md5(to_hash_test) - ds_w_test_split = train_dataset.train_test_split( - test_size=cfg.val_set_size, - seed=seed, - shuffle=False, - train_new_fingerprint=train_fingerprint, - test_new_fingerprint=test_fingerprint, - ) - eval_dataset = ds_w_test_split["test"] - train_dataset = ds_w_test_split["train"] - - if not train_is_preprocessed: - _save_preprocessed_ds(cfg, cfg.datasets, train_dataset) - if eval_dataset and not eval_is_preprocessed: - _save_preprocessed_ds(cfg, cfg.test_datasets, eval_dataset) + prior_len = len(split_datasets[i]) + split_datasets[i] = split_datasets[i].filter( + drop_long, + num_proc=cfg.dataset_processes, + load_from_cache_file=not cfg.is_preprocess, + desc="Dropping Long Sequences", + ) + dropped = prior_len - len(split_datasets[i]) + if dropped: + LOG.warning(f"Dropped {dropped} long samples from dataset index {i}") - if cfg.dataset_exact_deduplication: - train_dataset, eval_dataset, _ = deduplicate_and_log_datasets( - train_dataset=train_dataset, eval_dataset=eval_dataset + # Merge datasets + dataset = merge_datasets(split_datasets, cfg) + + if not cfg.skip_prepare_dataset: + # Save preprocessed dataset + dataset_hash = generate_dataset_hash_from_config( + cfg, datasets_configs, tokenizer.name_or_path ) + save_preprocessed_dataset(cfg, dataset, dataset_hash, split) - return train_dataset, eval_dataset + return dataset + + +# pylint: disable=duplicate-code +def _load_or_create_dataset_split( + cfg: DictDefault, tokenizer: PreTrainedTokenizer, split: Literal["train", "test"] +) -> Dataset: + """Load preprocessed dataset or create new one for given split. + + Args: + cfg: Configuration object. + tokenizer: Tokenizer to use for processing text. + split: Dataset split to load. + + Returns: + Tuple of (dataset, is_preprocessed). + """ + # Select correct dataset configuration based on split + datasets_config = cfg.datasets if split == "train" else cfg.test_datasets + + # Generate dataset hash for caching + dataset_hash = generate_dataset_hash_from_config( + cfg, datasets_config, tokenizer.name_or_path + ) + + # Try loading from hub if push_dataset_to_hub is configured + dataset = None + if cfg.push_dataset_to_hub: + dataset = try_load_from_hub(cfg, dataset_hash, split) + + # Attempt to load preprocessed dataset + if dataset is None: + dataset = load_preprocessed_dataset(cfg, dataset_hash) + + # Otherwise, load it + if dataset is None: + dataset = _load_split(cfg, split=split) + + return dataset diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 88c78174bc..d0b8ab7432 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -1,58 +1,38 @@ -"""data handling specific to SFT""" +"""Data handling specific to SFT.""" import functools -import os import tempfile -from pathlib import Path -from typing import List, Optional, Tuple, Union +from typing import Literal from datasets import ( Dataset, DatasetDict, IterableDataset, - Sequence, - Value, - concatenate_datasets, load_dataset, - load_from_disk, -) -from transformers import PreTrainedTokenizerBase - -from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH -from axolotl.datasets import TokenizedPromptDataset, wrap_dataset_for_tokenized_prompt -from axolotl.prompt_strategies import load -from axolotl.prompt_strategies.bradley_terry import load as bradley_terry_load -from axolotl.prompt_tokenizers import ( - AlpacaMultipleChoicePromptTokenizingStrategy, - AlpacaPromptTokenizingStrategy, - AlpacaReflectionPTStrategy, - DatasetWrappingStrategy, - GPTeacherPromptTokenizingStrategy, - JeopardyPromptTokenizingStrategy, - OpenAssistantPromptTokenizingStrategy, - SummarizeTLDRPromptTokenizingStrategy, -) -from axolotl.prompters import ( - AlpacaPrompter, - GPTeacherPrompter, - JeopardyPrompter, - MultipleChoiceConcisePrompter, - MultipleChoiceExplainPrompter, - Prompter, - ReflectAlpacaPrompter, - SummarizeTLDRPrompter, - UnsupportedPrompter, ) +from transformers import PreTrainedTokenizer, ProcessorMixin + +from axolotl.prompters import Prompter +from axolotl.utils.data.lock import FileLockLoader from axolotl.utils.data.pretraining import wrap_pretraining_dataset -from axolotl.utils.data.shared import datasets_w_name_generator, load_dataset_w_config +from axolotl.utils.data.shared import ( + create_train_validation_split, + datasets_with_name_generator, + generate_dataset_hash_from_config, + load_dataset_with_config, + load_preprocessed_dataset, + merge_datasets, + save_preprocessed_dataset, + try_load_from_hub, +) from axolotl.utils.data.utils import ( deduplicate_and_log_datasets, drop_long_seq_in_dataset, - md5, retry_on_request_exceptions, ) +from axolotl.utils.data.wrappers import get_dataset_wrapper from axolotl.utils.dict import DictDefault -from axolotl.utils.distributed import is_local_main_process, zero_first +from axolotl.utils.distributed import is_local_main_process from axolotl.utils.logging import get_logger from axolotl.utils.trainer import ( calculate_total_num_steps, @@ -63,121 +43,77 @@ @retry_on_request_exceptions(max_retries=3, delay=5) -def prepare_dataset(cfg, tokenizer, processor=None, preprocess_iterable=None): - prompters = [] - if not cfg.pretraining_dataset: - with zero_first(is_local_main_process()): - if cfg.test_datasets: - train_dataset, _, prompters = load_prepare_datasets( - tokenizer, - cfg, - DEFAULT_DATASET_PREPARED_PATH, - split="train", - processor=processor, - preprocess_iterable=preprocess_iterable, - ) - _, eval_dataset, _ = load_prepare_datasets( - tokenizer, - cfg, - DEFAULT_DATASET_PREPARED_PATH, - split="test", - processor=processor, - preprocess_iterable=preprocess_iterable, - ) - else: - train_dataset, eval_dataset, prompters = load_prepare_datasets( - tokenizer, - cfg, - DEFAULT_DATASET_PREPARED_PATH, - processor=processor, - preprocess_iterable=preprocess_iterable, - ) - else: - # Load streaming dataset if pretraining_dataset is given - path = cfg.pretraining_dataset - split = "train" - name = None - data_files = None - skip = 0 - if isinstance(cfg.pretraining_dataset, list) and isinstance( - cfg.pretraining_dataset[0], dict - ): - path = cfg.pretraining_dataset[0]["path"] - name = cfg.pretraining_dataset[0]["name"] - skip = cfg.pretraining_dataset[0]["skip"] - if "split" in cfg.pretraining_dataset[0]: - split = cfg.pretraining_dataset[0]["split"] - - data_files = cfg.pretraining_dataset[0].get("data_files") - - ds_wrapper_partial = functools.partial( - get_dataset_wrapper, - cfg.pretraining_dataset[0], - tokenizer, - cfg, - cfg.pretraining_dataset[0]["type"] or "pretrain", +def prepare_datasets( + cfg: DictDefault, + tokenizer: PreTrainedTokenizer, + processor: ProcessorMixin | None = None, + preprocess_iterable: bool = False, +) -> tuple[IterableDataset | Dataset, Dataset | None, int, list[Prompter | None]]: + """Prepare training and evaluation datasets based on configuration. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + tokenizer: Tokenizer to use for processing text. + processor: Optional processor for multimodal datasets. + preprocess_iterable: Whether to use iterable preprocessing. + + Returns: + Tuple of (train_dataset, eval_dataset, total_steps, prompters). + """ + if cfg.pretraining_dataset: + return _prepare_pretraining_dataset( + cfg, tokenizer, processor, preprocess_iterable ) + return _prepare_standard_dataset(cfg, tokenizer, processor, preprocess_iterable) - # when letting accelerator dispatch batches from the main process, we don't need to load the dataset from - # other ranks, we just need to present a fake dataset - if ( - cfg.accelerator_config - and cfg.accelerator_config.dispatch_batches - and not is_local_main_process() - ): - with tempfile.NamedTemporaryFile(mode="w+", delete=False) as f: - f.write("text\n") - f.write("lorem ipsum dolor sit amet\n") - # rewind the file pointer to the beginning so we can read it again - f.seek(0) - iter_ds = load_dataset( - "csv", data_files=f.name, split="train", streaming=True - ) - else: - iter_ds = load_dataset( - path, streaming=True, split=split, name=name, data_files=data_files - ) - if skip: - LOG.info(f"Skipping {skip} samples from the dataset") - iter_ds = iter_ds.skip(skip) - train_dataset = wrap_pretraining_dataset( - iter_ds, +def _prepare_standard_dataset( + cfg: DictDefault, + tokenizer: PreTrainedTokenizer, + processor: ProcessorMixin | None, + preprocess_iterable: bool, +) -> tuple[Dataset, Dataset | None, int, list[Prompter | None]]: + """Prepare standard (non-pretraining) datasets.""" + + def _load_datasets(): + # Always load training dataset + train_dataset, eval_dataset, prompters = _load_and_prepare_datasets( tokenizer, cfg, - ds_wrapper_partial, - max_tokens=cfg.sequence_len, - batch_size=cfg.micro_batch_size, - seed=cfg.seed if cfg.seed is not None else 42, - buffer_size=cfg.pretrain_multipack_buffer_size or 10_000, + split="train", + processor=processor, + preprocess_iterable=preprocess_iterable, ) - # https://discuss.huggingface.co/t/how-to-use-huggingface-trainer-streaming-datasets-without-wrapping-it-with-torchdatas-iterablewrapper/25230 - train_dataset = train_dataset.with_format("torch") - # Load eval dataset (non-streaming) if specified - eval_dataset = None + # Overwrite eval_dataset if test data exists if cfg.test_datasets: - _, eval_dataset, _ = load_prepare_datasets( + _, eval_dataset, _ = _load_and_prepare_datasets( tokenizer, cfg, - DEFAULT_DATASET_PREPARED_PATH, split="test", processor=processor, preprocess_iterable=preprocess_iterable, ) - if cfg.dataset_exact_deduplication: - LOG.info("Deduplication not available for pretrained datasets") + return train_dataset, eval_dataset, prompters - return train_dataset, eval_dataset, cfg.max_steps, prompters + # Prepare datasets (with file locking logic for multiple ranks) + loader = FileLockLoader(cfg) + try: + train_dataset, eval_dataset, prompters = loader.load(_load_datasets) + finally: + loader.cleanup() + # Validate sample packing configuration for evaluation if eval_dataset and cfg.sample_packing and cfg.eval_sample_packing is not False: total_eval_steps = calculate_total_num_steps(cfg, eval_dataset, update=False) if total_eval_steps == 0: raise ValueError( - "eval dataset split is too small for sample_packing. You should set `eval_sample_packing: False`. " + "eval dataset split is too small for sample_packing. " + "You should set `eval_sample_packing: False` in your config." ) + # Calculate total number of training steps if cfg.max_steps: total_num_steps = min( calculate_total_num_steps(cfg, train_dataset), cfg.max_steps @@ -188,480 +124,384 @@ def prepare_dataset(cfg, tokenizer, processor=None, preprocess_iterable=None): return train_dataset, eval_dataset, total_num_steps, prompters -def load_tokenized_prepared_datasets( - tokenizer, - cfg, - default_dataset_prepared_path, - split="train", - processor=None, - preprocess_iterable: Optional[bool] = None, -) -> Tuple[DatasetDict, List[Prompter]]: - cfg_datasets = cfg.test_datasets if split == "test" else cfg.datasets - tokenizer_name = cfg.tokenizer_config - - ds_hash = str( - md5( - ( - str(cfg.sequence_len) - + "@" - + str(cfg.sample_packing) - + "@" - + str(cfg.eval_sample_packing) - + "@" - + str(cfg.group_by_length) - + "@" - + str(cfg.kd_temperature or 1.0) - + "|".join( - sorted( - [ - f"{d.path}:{d.type}:{d.shards}:{d.conversation}:{d.split}:{d.temperature or 1.0}" - for d in cfg_datasets - ] - ) - ) - + "|" - + tokenizer_name - ) +def _prepare_pretraining_dataset( + cfg: DictDefault, + tokenizer: PreTrainedTokenizer, + processor: ProcessorMixin | None, + preprocess_iterable: bool, +) -> tuple[IterableDataset, Dataset | None, int, list[Prompter | None]]: + """ + Prepare dataset for pretraining mode. + + Note: Pre-training datasets are streamed from the HuggingFace Hub. + """ + # Extract pretraining dataset configuration + pretraining_config = _extract_pretraining_config(cfg) + + # Load streaming dataset for training + train_dataset = _load_pretraining_dataset(pretraining_config, cfg, tokenizer) + + # Load evaluation dataset if specified + eval_dataset = None + if cfg.test_datasets: + _, eval_dataset, _ = _load_and_prepare_datasets( + tokenizer, + cfg, + split="test", + processor=processor, + preprocess_iterable=preprocess_iterable, ) - ) - prepared_ds_path = ( - Path(cfg.dataset_prepared_path) / ds_hash - if cfg.dataset_prepared_path - else Path(default_dataset_prepared_path) / ds_hash - ) - dataset = None - prompters = [] - use_auth_token = cfg.hf_use_auth_token - try: - if cfg.push_dataset_to_hub: - LOG.info( - f"Attempting to load prepared dataset from Huggingface hub at {cfg.push_dataset_to_hub} (version {ds_hash})..." - ) - dataset = load_dataset( - cfg.push_dataset_to_hub, - ds_hash, - token=use_auth_token, - ) - dataset = dataset[split] - except Exception: # pylint: disable=broad-except # nosec - pass - - # pylint: disable=duplicate-code - if dataset: - # This is for the case where we already loaded a pretokenized dataset from the hub - ... - elif ( - cfg.dataset_prepared_path - and any(prepared_ds_path.glob("*")) - and not cfg.is_preprocess - and not cfg.skip_prepare_dataset - ): - LOG.info(f"Loading prepared dataset from disk at {prepared_ds_path}...") - dataset = load_from_disk(str(prepared_ds_path)) - LOG.info("Prepared dataset loaded from disk...") - else: - if cfg.push_dataset_to_hub: - LOG.info("Unable to find prepared dataset in Huggingface hub") - if cfg.is_preprocess: - LOG.info( - f"Skipping prepared dataset in {prepared_ds_path} for pre-processing..." - ) - else: - LOG.info(f"Unable to find prepared dataset in {prepared_ds_path}") - LOG.info("Loading raw datasets...") - if not cfg.is_preprocess: - LOG.warning( - "Processing datasets during training can lead to VRAM instability. Please pre-process your dataset." - ) - if cfg.seed: - seed = cfg.seed - else: - LOG.info("No seed provided, using default seed of 42") - seed = 42 - - datasets = [] - - streaming_ds = False - if preprocess_iterable: - streaming_ds = True - # pylint: disable=invalid-name - for config_dataset in datasets_w_name_generator(cfg_datasets): - ds: Union[Dataset, DatasetDict] = load_dataset_w_config( - config_dataset, use_auth_token, streaming=streaming_ds - ) + if cfg.dataset_exact_deduplication: + LOG.info("Deduplication not available for pretrained datasets") - d_base_type = d_prompt_style = None - d_type = config_dataset.type - if isinstance(d_type, str): - d_type_split = d_type.split(":") - d_base_type = d_type_split[0] - d_prompt_style = d_type_split[1] if len(d_type_split) > 1 else None - - if isinstance(ds, DatasetDict): - if config_dataset.split and config_dataset.split in ds: - ds = ds[config_dataset.split] - elif split in ds: - ds = ds[split] - else: - raise ValueError( - f"no {split} split found for dataset {config_dataset.path}, you may specify a split with 'split: `" - ) - - # support for using a subset of the data - if config_dataset.shards: - shards_idx = config_dataset.get("shards_idx", 0) - ds = ds.shuffle(seed=seed).shard( - num_shards=config_dataset.shards, index=shards_idx - ) - - dataset_wrapper, dataset_prompter = get_dataset_wrapper( - config_dataset=config_dataset, - tokenizer=tokenizer, - cfg=cfg, - d_base_type=d_base_type, - dataset=ds, - d_prompt_style=d_prompt_style, - processor=processor, - ) - datasets.append(dataset_wrapper) - prompters.append(dataset_prompter) + # For pretraining, we return max_steps directly from config + return train_dataset, eval_dataset, cfg.max_steps, [] - if len(datasets) == 1: - dataset = datasets[0] - else: - LOG.info("Merging datasets...") - dataset = concatenate_datasets(datasets) - - if len(datasets) > 1: - if cfg.shuffle_merged_datasets: - LOG.debug("Shuffling merged datasets...") - dataset = dataset.shuffle(seed=seed) - else: - LOG.debug("NOT shuffling merged datasets") - - if not cfg.skip_prepare_dataset: - dataset = drop_long_seq_in_dataset(dataset, cfg) - - if cfg.sample_packing: - dataset, _ = process_datasets_for_packing(cfg, dataset, None) - - if cfg.local_rank == 0 and not cfg.skip_prepare_dataset: - LOG.info(f"Saving merged prepared dataset to disk... {prepared_ds_path}") - if isinstance(dataset, IterableDataset): - num_workers = cfg.dataset_processes - - def gen_from_iter_ds(_ds, worker_id: List[int], num_workers: List[int]): - """Generator function to correctly splice the dataset for each worker""" - for i, item in enumerate(_ds): - if i % num_workers[0] == worker_id[0]: - yield item - - ds_from_iter = Dataset.from_generator( - functools.partial(gen_from_iter_ds, dataset), - features=dataset.features, - num_proc=num_workers, - split=split, - gen_kwargs={ - "worker_id": list(range(num_workers)), - "num_workers": [num_workers] * num_workers, - }, - ) - ds_from_iter.save_to_disk(str(prepared_ds_path)) - else: - os.makedirs(prepared_ds_path, exist_ok=True) - dataset.save_to_disk(str(prepared_ds_path)) - if cfg.push_dataset_to_hub: - LOG.info( - f"Pushing merged prepared dataset to Huggingface hub at {cfg.push_dataset_to_hub} (version {ds_hash})..." - ) - dataset.push_to_hub( - cfg.push_dataset_to_hub, - ds_hash, - private=True, - ) - return dataset, prompters +def _extract_pretraining_config(cfg: DictDefault) -> DictDefault: + """Extract pretraining configuration from the main config.""" + if isinstance(cfg.pretraining_dataset, list) and isinstance( + cfg.pretraining_dataset[0], dict + ): + config = cfg.pretraining_dataset[0] + return DictDefault( + { + "path": config["path"], + "name": config["name"], + "skip": config["skip"], + "split": config.get("split", "train"), + "data_files": config.get("data_files"), + "type": config.get("type", "pretrain"), + } + ) + # Simple string path case + return DictDefault( + { + "path": cfg.pretraining_dataset, + "name": None, + "skip": 0, + "split": "train", + "data_files": None, + "type": "pretrain", + } + ) + + +def _load_pretraining_dataset( + pretraining_config: DictDefault, cfg: DictDefault, tokenizer: PreTrainedTokenizer +) -> IterableDataset: + """Load and prepare a streaming dataset for pretraining.""" + # Create dataset wrapper partial function + dataset_wrapper_partial = functools.partial( + get_dataset_wrapper, + dataset_config=pretraining_config, + tokenizer=tokenizer, + cfg=cfg, + dataset_base_type=pretraining_config["type"], + ) + + # Load the actual dataset + if ( + cfg.accelerator_config + and cfg.accelerator_config.dispatch_batches + and not is_local_main_process() + ): + iter_dataset = _create_placeholder_dataset() + else: + iter_dataset = load_dataset( + pretraining_config["path"], + streaming=True, + split=pretraining_config["split"], + name=pretraining_config["name"], + data_files=pretraining_config["data_files"], + ) + # Apply skip if specified + if pretraining_config["skip"]: + LOG.info(f"Skipping {pretraining_config['skip']} samples from the dataset") + iter_dataset = iter_dataset.skip(pretraining_config["skip"]) -def load_prepare_datasets( - tokenizer: PreTrainedTokenizerBase, - cfg, - default_dataset_prepared_path, - split="train", - processor=None, - preprocess_iterable: Optional[bool] = False, -) -> Tuple[Dataset, Dataset, List[Prompter]]: - dataset, prompters = load_tokenized_prepared_datasets( + # Wrap the dataset for pretraining + train_dataset = wrap_pretraining_dataset( + iter_dataset, tokenizer, cfg, - default_dataset_prepared_path, - split=split, - processor=processor, - preprocess_iterable=preprocess_iterable, + dataset_wrapper_partial, + max_tokens=cfg.sequence_len, + batch_size=cfg.micro_batch_size, + seed=cfg.seed, + buffer_size=cfg.pretrain_multipack_buffer_size or 10_000, ) - if cfg.dataset_shard_num and cfg.dataset_shard_idx is not None: - LOG.info( - f"Using index #{cfg.dataset_shard_idx} of {cfg.dataset_shard_num} shards" - ) - dataset = dataset.shard( - num_shards=cfg.dataset_shard_num, - index=cfg.dataset_shard_idx, + # Format for PyTorch + return train_dataset.with_format("torch") + + +def _create_placeholder_dataset() -> IterableDataset: + """Create a minimal placeholder dataset for non-main processes.""" + with tempfile.NamedTemporaryFile(mode="w+", delete=False) as f: + f.write("text\n") + f.write("lorem ipsum dolor sit amet\n") + f.seek(0) + return load_dataset("csv", data_files=f.name, split="train", streaming=True) + + +def _load_tokenized_prepared_datasets( + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + split: Literal["train", "test"] = "train", + processor: ProcessorMixin | None = None, + preprocess_iterable: bool = False, +) -> tuple[Dataset | DatasetDict, list[Prompter | None]]: + """Load or create tokenized and prepared datasets for training or testing. + + Args: + tokenizer: Tokenizer for processing text. + cfg: Configuration object. + split: Dataset split to load ('train' or 'test'). + processor: Optional processor for multimodal datasets. + preprocess_iterable: Whether to use iterable preprocessing. + + Returns: + Tuple of (dataset, prompters list). + """ + # Select correct dataset configuration based on split + datasets_configs = cfg.datasets if split == "train" else cfg.test_datasets + + # Generate dataset hash for caching + dataset_hash = generate_dataset_hash_from_config( + cfg, datasets_configs, tokenizer.name_or_path + ) + + # Try loading from hub if push_dataset_to_hub is configured + dataset = None + if cfg.push_dataset_to_hub: + dataset = try_load_from_hub(cfg, dataset_hash, split) + + # If not found on hub, try loading from disk + if dataset is None: + dataset = load_preprocessed_dataset(cfg, dataset_hash) + + # If not found on disk or skipping prepared dataset, load and process raw datasets + prompters: list[Prompter | None] = [] + if dataset is None: + dataset, prompters = _load_raw_datasets( + cfg, + datasets_configs, + tokenizer, + split, + processor, + preprocess_iterable, ) - val_set_size = ( - int(cfg.val_set_size) if cfg.val_set_size > 1 else float(cfg.val_set_size) - ) + return dataset, prompters - if split == "train" and val_set_size: - seed = cfg.seed if cfg.seed is not None else 42 - - # ensure we end up with the same fingerprint by doing rank0 first and being able to cache - to_hash_train = ( - dataset._fingerprint # pylint: disable=protected-access - + "|" - + str(val_set_size) - + "|" - + "train" - + "|" - + str(cfg.seed or 42) + +def _load_raw_datasets( + cfg: DictDefault, + datasets_configs: list, + tokenizer: PreTrainedTokenizer, + split: str, + processor: ProcessorMixin | None = None, + preprocess_iterable: bool = False, +) -> tuple[Dataset, list[Prompter | None]]: + """Load, process, merge, and save raw datasets.""" + LOG.info("Loading raw datasets...", main_process_only=False) + if not cfg.is_preprocess: + LOG.warning( + "Processing datasets during training can lead to VRAM instability. Please " + "pre-process your dataset using `axolotl preprocess path/to/config.yml`." ) - to_hash_test = ( - dataset._fingerprint # pylint: disable=protected-access - + "|" - + str(val_set_size) - + "|" - + "test" - + "|" - + str(cfg.seed or 42) + + # Load and process individual datasets + datasets = [] + prompters = [] + for dataset_config in datasets_with_name_generator(datasets_configs): + dataset_wrapper, dataset_prompter = _load_and_process_single_dataset( + dataset_config=dataset_config, + cfg=cfg, + tokenizer=tokenizer, + split=split, + seed=cfg.seed, + processor=processor, + preprocess_iterable=preprocess_iterable, ) - train_fingerprint = md5(to_hash_train) - test_fingerprint = md5(to_hash_test) - if cfg.dataset_exact_deduplication: - _, _, dataset = deduplicate_and_log_datasets(dataset=dataset) - dataset = dataset.train_test_split( - test_size=val_set_size, - shuffle=False, - seed=seed, - train_new_fingerprint=train_fingerprint, - test_new_fingerprint=test_fingerprint, + datasets.append(dataset_wrapper) + prompters.append(dataset_prompter) + + # Merge datasets + dataset = merge_datasets(datasets, cfg) + + if not cfg.skip_prepare_dataset: + dataset = drop_long_seq_in_dataset(dataset, cfg) + if cfg.sample_packing: + dataset, _ = process_datasets_for_packing(cfg, dataset, None) + + # Save the prepared dataset + dataset_hash = generate_dataset_hash_from_config( + cfg, datasets_configs, tokenizer.name_or_path ) + save_preprocessed_dataset(cfg, dataset, dataset_hash, split) - train_dataset = dataset["train"] - eval_dataset = dataset["test"] - elif split == "test": - if cfg.dataset_exact_deduplication: - _, eval_dataset, _ = deduplicate_and_log_datasets(eval_dataset=dataset) - else: - eval_dataset = dataset - train_dataset = None - else: - if cfg.dataset_exact_deduplication: - train_dataset, _, _ = deduplicate_and_log_datasets(train_dataset=dataset) - else: - train_dataset = dataset - eval_dataset = None - return train_dataset, eval_dataset, prompters + return dataset, prompters -def get_dataset_wrapper( - config_dataset, - tokenizer, - cfg, - d_base_type, - dataset, - d_prompt_style=None, - processor=None, # pylint: disable=unused-argument -): - dataset_wrapper = None - dataset_prompter = None - - ds_kwargs = { - "process_count": cfg.dataset_processes, - "keep_in_memory": cfg.dataset_keep_in_memory is True, - } - - LOG.info( - f"Loading dataset: {config_dataset['path']} with base_type: {d_base_type} and prompt_style: {d_prompt_style}" +def _load_and_process_single_dataset( + dataset_config: DictDefault, + cfg: DictDefault, + tokenizer: PreTrainedTokenizer, + split: str, + seed: int, + processor: ProcessorMixin | None = None, + preprocess_iterable: bool = False, +) -> tuple[Dataset | IterableDataset, Prompter | None]: + """Load and process a single dataset based on the passed config.""" + # Load the dataset + dataset = load_dataset_with_config( + dataset_config, cfg.hf_use_auth_token, streaming=preprocess_iterable ) - if ( - isinstance(dataset, Dataset) - and "input_ids" in dataset.features - and "attention_mask" in dataset.features - and "labels" in dataset.features - ): - # dataset is already tokenized, just drop it straight in - dataset_prompter = UnsupportedPrompter() - dataset_wrapper = dataset - elif isinstance(config_dataset.type, DictDefault): - ds_strategy = load( - "user_defined", tokenizer, cfg, config_dataset.type.to_dict() - ) - dataset_prompter = UnsupportedPrompter() - dataset_wrapper = wrap_dataset_for_tokenized_prompt( - ds_strategy, - dataset, - **ds_kwargs, - ) - elif cfg.skip_prepare_dataset: - dataset_wrapper = dataset - elif ds_strategy := config_dataset.type.startswith( - "bradley_terry" - ) and bradley_terry_load( - config_dataset.type.split(".", 1)[1], tokenizer, cfg, config_dataset - ): - dataset_prompter = UnsupportedPrompter() - dataset_wrapper = wrap_dataset_for_tokenized_prompt( - ds_strategy, - dataset, - **ds_kwargs, - ) - elif config_dataset.type.startswith("stepwise_supervised"): - dataset_prompter = UnsupportedPrompter() - ds_strategy = load(config_dataset.type, tokenizer, cfg, config_dataset) - # we need to explicitly cast boolean labels to int - # for compatibility with how trl's PRMTrainer works - dataset = dataset.cast_column("labels", Sequence(Value("int64"))) - dataset_wrapper = TokenizedPromptDataset( - ds_strategy, - dataset, - **ds_kwargs, - ) - elif ds_strategy := load( - config_dataset.type, tokenizer, cfg, config_dataset, processor=processor - ): - if isinstance(ds_strategy, DatasetWrappingStrategy): - dataset_wrapper = ds_strategy.wrap_dataset(dataset, **ds_kwargs) + # Parse dataset type + d_base_type, d_prompt_style = _parse_dataset_type(dataset_config.type) + + # Select the appropriate split + if isinstance(dataset, DatasetDict): + if dataset_config.split and dataset_config.split in dataset: + dataset = dataset[dataset_config.split] + elif split in dataset: + dataset = dataset[split] else: - dataset_prompter = UnsupportedPrompter() - dataset_wrapper = wrap_dataset_for_tokenized_prompt( - ds_strategy, - dataset, - **ds_kwargs, + raise ValueError( + f"no {split} split found for dataset {dataset_config.path}, you may " + "specify a split with 'split: ...'" ) - elif d_base_type == "alpaca": - dataset_prompter = AlpacaPrompter(d_prompt_style) - ds_strategy = AlpacaPromptTokenizingStrategy( - dataset_prompter, - tokenizer, - cfg.train_on_inputs, - cfg.sequence_len, - ) - ds_wrapper = wrap_dataset_for_tokenized_prompt( - ds_strategy, - dataset, - **ds_kwargs, - ) - dataset_wrapper = ds_wrapper - elif d_base_type == "explainchoice": - dataset_prompter = MultipleChoiceExplainPrompter(d_prompt_style) - ds_strategy = AlpacaMultipleChoicePromptTokenizingStrategy( - dataset_prompter, - tokenizer, - cfg.train_on_inputs, - cfg.sequence_len, - ) - ds_wrapper = wrap_dataset_for_tokenized_prompt( - ds_strategy, - dataset, - **ds_kwargs, - ) - dataset_wrapper = ds_wrapper - elif d_base_type == "concisechoice": - dataset_prompter = MultipleChoiceConcisePrompter(d_prompt_style) - ds_strategy = AlpacaMultipleChoicePromptTokenizingStrategy( - dataset_prompter, - tokenizer, - cfg.train_on_inputs, - cfg.sequence_len, - ) - ds_wrapper = wrap_dataset_for_tokenized_prompt( - ds_strategy, - dataset, - **ds_kwargs, - ) - dataset_wrapper = ds_wrapper - elif d_base_type == "summarizetldr": - dataset_prompter = SummarizeTLDRPrompter(d_prompt_style) - ds_strategy = SummarizeTLDRPromptTokenizingStrategy( - dataset_prompter, - tokenizer, - cfg.train_on_inputs, - cfg.sequence_len, - ) - ds_wrapper = wrap_dataset_for_tokenized_prompt( - ds_strategy, - dataset, - **ds_kwargs, - ) - dataset_wrapper = ds_wrapper - elif d_base_type == "jeopardy": - dataset_prompter = JeopardyPrompter(d_prompt_style) - ds_strategy = JeopardyPromptTokenizingStrategy( - dataset_prompter, - tokenizer, - cfg.train_on_inputs, - cfg.sequence_len, - ) - ds_wrapper = wrap_dataset_for_tokenized_prompt( - ds_strategy, - dataset, - **ds_kwargs, - ) - dataset_wrapper = ds_wrapper - elif d_base_type == "oasst": - dataset_prompter = AlpacaPrompter(d_prompt_style) - ds_strategy = OpenAssistantPromptTokenizingStrategy( - dataset_prompter, - tokenizer, - cfg.train_on_inputs, - cfg.sequence_len, - ) - ds_wrapper = wrap_dataset_for_tokenized_prompt( - ds_strategy, - dataset, - **ds_kwargs, - ) - dataset_wrapper = ds_wrapper - elif d_base_type == "gpteacher": - dataset_prompter = GPTeacherPrompter(d_prompt_style) - ds_strategy = GPTeacherPromptTokenizingStrategy( - dataset_prompter, - tokenizer, - cfg.train_on_inputs, - cfg.sequence_len, - ) - ds_wrapper = wrap_dataset_for_tokenized_prompt( - ds_strategy, - dataset, - **ds_kwargs, - ) - dataset_wrapper = ds_wrapper - elif d_base_type == "reflection": - dataset_prompter = ReflectAlpacaPrompter(d_prompt_style) - ds_strategy = AlpacaReflectionPTStrategy( - dataset_prompter, - tokenizer, - cfg.train_on_inputs, - cfg.sequence_len, + + # Apply sharding if configured + if dataset_config.shards: + shards_idx = dataset_config.get("shards_idx", 0) + dataset = dataset.shuffle(seed=seed).shard( + num_shards=dataset_config.shards, index=shards_idx ) - ds_wrapper = wrap_dataset_for_tokenized_prompt( - ds_strategy, - dataset, - **ds_kwargs, + + # Apply dataset wrapper + dataset_wrapper, dataset_prompter = get_dataset_wrapper( + dataset_config=dataset_config, + tokenizer=tokenizer, + cfg=cfg, + dataset_base_type=d_base_type, + dataset=dataset, + dataset_prompt_style=d_prompt_style, + processor=processor, + ) + + return dataset_wrapper, dataset_prompter + + +def _parse_dataset_type(d_type: str) -> tuple[str | None, str | None]: + """Parse the dataset type string into base type and prompt style.""" + if not isinstance(d_type, str): + return None, None + + d_type_split = d_type.split(":") + d_base_type = d_type_split[0] + d_prompt_style = d_type_split[1] if len(d_type_split) > 1 else None + + return d_base_type, d_prompt_style + + +def _handle_train_dataset_split( + dataset: Dataset, cfg: DictDefault +) -> tuple[Dataset, Dataset | None]: + """Handle processing for train split, including validation set creation.""" + val_set_size = ( + int(cfg.val_set_size) if cfg.val_set_size > 1 else float(cfg.val_set_size) + ) + + if val_set_size: + # Create train/validation split + train_dataset, eval_dataset = create_train_validation_split( + dataset, cfg, val_set_size ) - dataset_wrapper = ds_wrapper + return train_dataset, eval_dataset + + # No validation split - apply deduplication if needed and return as train dataset + if cfg.dataset_exact_deduplication: + train_dataset, _ = deduplicate_and_log_datasets(dataset=dataset) + else: + train_dataset = dataset + + return train_dataset, None + + +def _handle_test_dataset_split( + dataset: Dataset, cfg: DictDefault +) -> tuple[None, Dataset | None]: + """Handle processing for test split.""" + if cfg.dataset_exact_deduplication: + eval_dataset, _ = deduplicate_and_log_datasets(dataset=dataset) else: - suffix = "" - if ":load_" in config_dataset.type: - suffix = f" Did you mean {config_dataset.type.replace(':load_', '.load_')}?" - LOG.error( - f"unhandled prompt tokenization strategy: {config_dataset.type}. {suffix}" + eval_dataset = dataset + + return None, eval_dataset + + +def _apply_dataset_sharding(dataset: Dataset, cfg: DictDefault) -> Dataset: + """Apply dataset sharding if configured. + + Args: + dataset: Dataset to shard. + cfg: Configuration object containing shard settings. + + Returns: + Sharded dataset or original dataset if no sharding configured. + """ + if cfg.dataset_shard_num and cfg.dataset_shard_idx is not None: + LOG.info( + f"Using index #{cfg.dataset_shard_idx} of {cfg.dataset_shard_num} shards" ) - raise ValueError( - f"unhandled prompt tokenization strategy: {config_dataset.type} {suffix}" + dataset = dataset.shard( + num_shards=cfg.dataset_shard_num, + index=cfg.dataset_shard_idx, ) + return dataset + + +def _load_and_prepare_datasets( + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + split: Literal["train", "test"] = "train", + processor: ProcessorMixin | None = None, + preprocess_iterable: bool = False, +) -> tuple[Dataset | None, Dataset | None, list[Prompter | None]]: + """Load and prepare datasets with optional validation split and sharding. + + Args: + tokenizer: Tokenizer for processing text. + cfg: Configuration object. + split: Dataset split to load ('train' or 'test'). + processor: Optional processor for multimodal datasets. + preprocess_iterable: Whether to use iterable preprocessing. + + Returns: + Tuple of (train_dataset, eval_dataset, prompters). + """ + # Load the base dataset + dataset, prompters = _load_tokenized_prepared_datasets( + tokenizer, + cfg, + split=split, + processor=processor, + preprocess_iterable=preprocess_iterable, + ) - return dataset_wrapper, dataset_prompter + # Apply dataset sharding if configured using shared function + dataset = _apply_dataset_sharding(dataset, cfg) + + # Apply deduplication and create train / validation splits based on the split type + if split == "train": + train_dataset, eval_dataset = _handle_train_dataset_split(dataset, cfg) + else: + train_dataset, eval_dataset = _handle_test_dataset_split(dataset, cfg) + + return train_dataset, eval_dataset, prompters diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index d2e119f77b..3c58b4c853 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -1,11 +1,21 @@ -""" -dataset loading shared utils -""" +"""Dataset loading shared utils.""" +from __future__ import annotations + +import functools +import os from pathlib import Path -from typing import Optional, Union +from typing import TYPE_CHECKING, Any, Generator -from datasets import Dataset, DatasetDict, load_dataset, load_from_disk +from datasets import ( + Dataset, + DatasetDict, + IterableDataset, + IterableDatasetDict, + concatenate_datasets, + load_dataset, + load_from_disk, +) from huggingface_hub import hf_hub_download, snapshot_download from huggingface_hub.errors import ( HFValidationError, @@ -13,78 +23,141 @@ RevisionNotFoundError, ) +from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH +from axolotl.utils.data.utils import deduplicate_and_log_datasets, md5 from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger +if TYPE_CHECKING: + from adlfs import AzureBlobFileSystem + from gcsfs import GCSFileSystem + from ocifs import OCIFileSystem + from s3fs import S3FileSystem -def get_ds_type(config_dataset: DictDefault): - """ - Get the dataset type from the path if it's not specified - """ - ds_type = "json" - if config_dataset.ds_type: - ds_type = config_dataset.ds_type - elif ".parquet" in config_dataset.path: - ds_type = "parquet" - elif ".arrow" in config_dataset.path: - ds_type = "arrow" - elif ".csv" in config_dataset.path: - ds_type = "csv" - elif ".txt" in config_dataset.path: - ds_type = "text" - return ds_type - - -def datasets_w_name_generator(dataset_configs: list[DictDefault]): - """ - Yields dataset configs handling multiple names or preprocess_shards +LOG = get_logger(__name__) + +EXTENSIONS_TO_DATASET_TYPES = { + ".parquet": "parquet", + ".arrow": "arrow", + ".csv": "csv", + ".txt": "text", +} + + +def get_dataset_type(dataset_config: DictDefault) -> str: + """Get the dataset type from the path if it's not specified.""" + if dataset_config.ds_type: + return dataset_config.ds_type + + for extension, dataset_type in EXTENSIONS_TO_DATASET_TYPES.items(): + if extension in dataset_config.path: + return dataset_type + + return "json" + + +def datasets_with_name_generator( + dataset_configs: list[DictDefault], +) -> Generator[DictDefault, None, None]: + """Yields expanded dataset configurations based on multiple names or preprocessing + shards. + + When a dataset config has a list of names, it yields separate configs for each + name. When a dataset config specifies preprocessing shards, it yields configs for + each shard. Args: - dataset_configs: list of dataset configs (equivalent to cfg.datasets) + dataset_configs: List of dataset configuration objects. + + Yields: + Individual dataset configurations, expanded as needed for names or shards. """ - for dataset in dataset_configs: - if dataset.name and isinstance(dataset.name, list): - # load_dataset doesn't properly handle multiple named configurations - # at the same time for a given dataset - for name in dataset.name: - yield DictDefault({**dataset, "name": name}) - elif dataset.preprocess_shards and not dataset.shards: - for shard in range(dataset.preprocess_shards): + for config in dataset_configs: + if config.name and isinstance(config.name, list): + for name in config.name: + yield DictDefault({**config, "name": name}) + elif config.preprocess_shards and not config.shards: + for shard_idx in range(config.preprocess_shards): yield DictDefault( { - **dataset, - "shards": dataset.preprocess_shards, - "shards_idx": shard, + **config, + "shards": config.preprocess_shards, + "shards_idx": shard_idx, } ) else: - yield dataset + yield config -def load_dataset_w_config( - config_dataset: DictDefault, use_auth_token: bool, streaming=False -) -> Union[Dataset, DatasetDict]: - """ - Load a dataset from a config +def load_dataset_with_config( + dataset_config: DictDefault, use_auth_token: bool, streaming=False +) -> Dataset | IterableDataset: + """Load a dataset from a config. Handles datasets that are stored locally, in the + HuggingFace Hub, in a remote filesystem (S3, GCS, Azure, OCI), a URL, or + `data_files`. Args: - config_dataset: single dataset config - use_auth_token: whether to use HF auth token - streaming: whether to stream the dataset + dataset_config: Single dataset config. + use_auth_token: Whether to use HF auth token. + streaming: Whether to stream the dataset. + + Returns: + Loaded dataset. """ - # pylint: disable=invalid-name - ds: Optional[Union[Dataset, DatasetDict]] = None # pylint: disable=invalid-name - ds_from_hub = False + # Set up common kwargs for dataset loading + load_dataset_kwargs = { + "split": dataset_config.split if dataset_config.split else None, + "name": dataset_config.name, + "streaming": streaming, + "trust_remote_code": dataset_config.trust_remote_code, + } + + # First check if it's a local path + if Path(dataset_config.path).exists(): + return _load_from_local_path(dataset_config, load_dataset_kwargs) + + # Check if it's a HuggingFace dataset + is_hub_dataset = _check_if_hub_dataset(dataset_config, use_auth_token) + + # Check if it's a cloud storage path and get appropriate filesystem + remote_fs, storage_options = _get_remote_filesystem(dataset_config.path) + is_cloud_dataset = False + if remote_fs: + try: + is_cloud_dataset = remote_fs.exists(dataset_config.path) + except (FileNotFoundError, ConnectionError): + pass + + # Load from appropriate source + if is_hub_dataset: + return _load_from_hub(dataset_config, use_auth_token, load_dataset_kwargs) + if is_cloud_dataset: + return _load_from_cloud( + dataset_config, remote_fs, storage_options, load_dataset_kwargs + ) + if dataset_config.path.startswith("https://"): + return _load_from_url(dataset_config, load_dataset_kwargs) + if dataset_config.data_files: + return _load_from_data_files(dataset_config, load_dataset_kwargs) + + raise ValueError( + f"The dataset could not be loaded. This could be due to a misconfigured dataset path " + f"({dataset_config.path}). Try double-check your path / name / data_files. " + f"This is not caused by the dataset type." + ) + + +def _check_if_hub_dataset(dataset_config: DictDefault, use_auth_token: bool) -> bool: + """Check if a dataset exists on the HuggingFace Hub.""" try: - # this is just a basic check to see if the path is a - # valid HF dataset that's loadable snapshot_download( - repo_id=config_dataset.path, + repo_id=dataset_config.path, repo_type="dataset", token=use_auth_token, - revision=config_dataset.revision, + revision=dataset_config.revision, ignore_patterns=["*"], ) - ds_from_hub = True + return True except ( RepositoryNotFoundError, RevisionNotFoundError, @@ -93,198 +166,373 @@ def load_dataset_w_config( HFValidationError, ValueError, ): - pass + return False + - ds_from_cloud = False - storage_options: dict = {} - remote_file_system = None - if config_dataset.path.startswith("s3://"): +def _get_remote_filesystem( + path: str, +) -> tuple[ + S3FileSystem | GCSFileSystem | AzureBlobFileSystem | OCIFileSystem | None, dict +]: + """Get the appropriate filesystem for a remote path.""" + if path.startswith("s3://"): try: - import s3fs # type: ignore + import s3fs + + storage_options = {"anon": False} + return s3fs.S3FileSystem(**storage_options), storage_options except ImportError as exc: raise ImportError("s3:// paths require s3fs to be installed") from exc - # Reads env, credentials from ~/.aws/credentials, or IAM metadata provider - # https://s3fs.readthedocs.io/en/latest/index.html?highlight=storage_options#credentials - storage_options = {"anon": False} - remote_file_system = s3fs.S3FileSystem(**storage_options) - elif config_dataset.path.startswith("gs://") or config_dataset.path.startswith( - "gcs://" - ): + elif path.startswith(("gs://", "gcs://")): try: - import gcsfs # type: ignore + import gcsfs + + storage_options = {"token": None} # type: ignore + return gcsfs.GCSFileSystem(**storage_options), storage_options except ImportError as exc: raise ImportError( "gs:// or gcs:// paths require gcsfs to be installed" ) from exc - # gcsfs will use default credentials from the environment else anon - # https://gcsfs.readthedocs.io/en/latest/#credentials - storage_options = {"token": None} - remote_file_system = gcsfs.GCSFileSystem(**storage_options) - elif ( - config_dataset.path.startswith("adl://") - or config_dataset.path.startswith("abfs://") - or config_dataset.path.startswith("az://") - ): + elif path.startswith(("adl://", "abfs://", "az://")): try: import adlfs + + storage_options = {"anon": False} + return adlfs.AzureBlobFileSystem(**storage_options), storage_options except ImportError as exc: raise ImportError( "adl:// or abfs:// paths require adlfs to be installed" ) from exc - # # Ensure you have the following environment variables set: - # # Gen 1 - # storage_options = { - # "tenant_id": AZURE_STORAGE_TENANT_ID, - # "client_id": AZURE_STORAGE_CLIENT_ID, - # "client_secret": AZURE_STORAGE_CLIENT_SECRET, - # } - # # Gen 2 - # storage_options = { - # "account_name": AZURE_STORAGE_ACCOUNT_NAME, - # "account_key": AZURE_STORAGE_ACCOUNT_KEY, - # } - - # Reads env - # https://github.com/fsspec/adlfs?tab=readme-ov-file#setting-credentials - storage_options = {"anon": False} - remote_file_system = adlfs.AzureBlobFileSystem(**storage_options) - elif config_dataset.path.startswith("oci://"): + elif path.startswith("oci://"): try: import ocifs + + storage_options = {} + return ocifs.OCIFileSystem(**storage_options), storage_options except ImportError as exc: raise ImportError("oci:// paths require ocifs to be installed") from exc - # https://ocifs.readthedocs.io/en/latest/getting-connected.html#Using-Environment-Variables - remote_file_system = ocifs.OCIFileSystem(**storage_options) + return None, {} - try: - if remote_file_system and remote_file_system.exists(config_dataset.path): - ds_from_cloud = True - except (FileNotFoundError, ConnectionError): - pass - - # gather extra args from the config - load_ds_kwargs = {} - if config_dataset.split: - load_ds_kwargs["split"] = config_dataset.split - else: - load_ds_kwargs["split"] = None - - # prefer local dataset, even if hub exists - local_path = Path(config_dataset.path) - if local_path.exists(): - if local_path.is_dir(): - if config_dataset.data_files: - ds_type = get_ds_type(config_dataset) - ds = load_dataset( # pylint: disable=invalid-name - ds_type, - name=config_dataset.name, - data_files=config_dataset.data_files, - streaming=streaming, - **load_ds_kwargs, - ) - else: - try: - ds = load_from_disk( - config_dataset.path - ) # pylint: disable=invalid-name - except FileNotFoundError: - ds = load_dataset( - config_dataset.path, - name=config_dataset.name, - streaming=False, - **load_ds_kwargs, - ) - elif local_path.is_file(): - ds_type = get_ds_type(config_dataset) - - ds = load_dataset( # pylint: disable=invalid-name - ds_type, - name=config_dataset.name, - data_files=config_dataset.path, - streaming=False, - **load_ds_kwargs, - ) - else: - raise ValueError( - "unhandled dataset load: local path exists, but is neither a directory or a file" + +def _load_from_local_path( + dataset_config: DictDefault, load_dataset_kwargs: dict +) -> Dataset | IterableDataset | DatasetDict | IterableDatasetDict: + """Load a dataset from a local path.""" + local_path = Path(dataset_config.path) + + if local_path.is_dir(): + if dataset_config.data_files: + dataset_type = get_dataset_type(dataset_config) + return load_dataset( + dataset_type, + data_files=dataset_config.data_files, + **load_dataset_kwargs, ) - elif ds_from_hub: - ds = load_dataset( - config_dataset.path, - name=config_dataset.name, - streaming=streaming, - data_files=config_dataset.data_files, - token=use_auth_token, - revision=config_dataset.revision, - trust_remote_code=config_dataset.trust_remote_code, - **load_ds_kwargs, + try: + return load_from_disk(dataset_config.path) + except FileNotFoundError: + load_dataset_kwargs["streaming"] = False + return load_dataset(dataset_config.path, **load_dataset_kwargs) + elif local_path.is_file(): + dataset_type = get_dataset_type(dataset_config) + load_dataset_kwargs["streaming"] = False + return load_dataset( + dataset_type, + data_files=dataset_config.path, + **load_dataset_kwargs, ) - elif ds_from_cloud and remote_file_system: - if remote_file_system.isdir(config_dataset.path): - ds = load_from_disk( - config_dataset.path, - storage_options=storage_options, - ) - elif remote_file_system.isfile(config_dataset.path): - ds_type = get_ds_type(config_dataset) - ds = load_dataset( - ds_type, - name=config_dataset.name, - data_files=config_dataset.path, - streaming=streaming, - storage_options=storage_options, - trust_remote_code=config_dataset.trust_remote_code, - **load_ds_kwargs, - ) - elif config_dataset.path.startswith("https://"): - ds_type = get_ds_type(config_dataset) - ds = load_dataset( - ds_type, - name=config_dataset.name, - data_files=config_dataset.path, - streaming=streaming, + else: + raise ValueError( + "Unhandled dataset load: local path exists, but is neither a directory or a file" + ) + + +def _load_from_hub( + dataset_config: DictDefault, use_auth_token: bool, load_dataset_kwargs: dict +) -> Dataset | IterableDataset | DatasetDict | IterableDatasetDict: + """Load a dataset from the HuggingFace Hub.""" + return load_dataset( + dataset_config.path, + data_files=dataset_config.data_files, + token=use_auth_token, + revision=dataset_config.revision, + **load_dataset_kwargs, + ) + + +def _load_from_cloud( + dataset_config: DictDefault, + remote_fs: S3FileSystem | GCSFileSystem | AzureBlobFileSystem | OCIFileSystem, + storage_options: dict, + load_dataset_kwargs: dict, +) -> Dataset | IterableDataset | DatasetDict | IterableDatasetDict: + """Load a dataset from cloud storage.""" + if remote_fs.isdir(dataset_config.path): + return load_from_disk( + dataset_config.path, + storage_options=storage_options, + ) + + if remote_fs.isfile(dataset_config.path): + dataset_type = get_dataset_type(dataset_config) + return load_dataset( + dataset_type, + data_files=dataset_config.path, storage_options=storage_options, - trust_remote_code=config_dataset.trust_remote_code, - **load_ds_kwargs, + **load_dataset_kwargs, ) - elif config_dataset.data_files: - fp: str | list[str] | None = None - if isinstance(config_dataset.data_files, str): - fp = hf_hub_download( - repo_id=config_dataset.path, + + raise ValueError( + f"Cloud path {dataset_config.path} is neither a directory nor a file" + ) + + +def _load_from_url( + dataset_config: DictDefault, load_dataset_kwargs: dict +) -> Dataset | IterableDataset | DatasetDict | IterableDatasetDict: + """Load a dataset from a URL.""" + dataset_type = get_dataset_type(dataset_config) + return load_dataset( + dataset_type, + data_files=dataset_config.path, + **load_dataset_kwargs, + ) + + +def _load_from_data_files( + dataset_config: DictDefault, load_dataset_kwargs: dict +) -> Dataset | IterableDataset | DatasetDict | IterableDatasetDict: + """Load a dataset from data files.""" + file_path = None + + if isinstance(dataset_config.data_files, str): + file_path = hf_hub_download( + repo_id=dataset_config.path, + repo_type="dataset", + filename=dataset_config.data_files, + revision=dataset_config.revision, + ) + elif isinstance(dataset_config.data_files, list): + file_path = [ + hf_hub_download( + repo_id=dataset_config.path, repo_type="dataset", - filename=config_dataset.data_files, - revision=config_dataset.revision, + filename=file, + revision=dataset_config.revision, ) - elif isinstance(config_dataset.data_files, list): - fp = [] - for file in config_dataset.data_files: - fp.append( - hf_hub_download( - repo_id=config_dataset.path, - repo_type="dataset", - filename=file, - revision=config_dataset.revision, - ) - ) - else: - raise ValueError("data_files must be either a string or list of strings") - ds = load_dataset( - "json", - name=config_dataset.name, - data_files=fp, - streaming=streaming, - **load_ds_kwargs, + for file in dataset_config.data_files + ] + else: + raise ValueError("data_files must be either a string or list of strings") + + return load_dataset("json", data_files=file_path, **load_dataset_kwargs) + + +def generate_split_fingerprints( + dataset: Dataset, val_set_size: int | float, seed: int +) -> tuple[str, str]: + """Generate consistent fingerprints for train/test splits.""" + fingerprint = dataset._fingerprint # pylint: disable=protected-access + + train_hash_input = f"{fingerprint}|{val_set_size}|train|{seed}" + test_hash_input = f"{fingerprint}|{val_set_size}|test|{seed}" + + train_fingerprint = md5(train_hash_input) + test_fingerprint = md5(test_hash_input) + + return train_fingerprint, test_fingerprint + + +def get_prepared_dataset_path(cfg: DictDefault, dataset_hash: str) -> Path: + """Get standardized path for prepared datasets. + + Args: + cfg: Configuration object. + dataset_hash: Hash identifying the specific dataset configuration. + + Returns: + Path where the prepared dataset should be stored. + """ + base_path = cfg.dataset_prepared_path or DEFAULT_DATASET_PREPARED_PATH + return Path(base_path) / dataset_hash + + +def create_train_validation_split( + dataset: Dataset, cfg: DictDefault, val_set_size: int | float +) -> tuple[Dataset, Dataset]: + """Create train/validation split with consistent fingerprinting. + + Args: + dataset: Dataset to split. + cfg: Configuration object containing seed and other settings. + val_set_size: Size of validation set (absolute number or fraction). + + Returns: + Tuple of (train_dataset, eval_dataset). + """ + train_fingerprint, test_fingerprint = generate_split_fingerprints( + dataset, val_set_size, cfg.seed + ) + + # Apply deduplication before splitting if configured + if cfg.dataset_exact_deduplication: + dataset, _ = deduplicate_and_log_datasets(dataset=dataset) + + split_dataset = dataset.train_test_split( + test_size=val_set_size, + shuffle=False, + seed=cfg.seed, + train_new_fingerprint=train_fingerprint, + test_new_fingerprint=test_fingerprint, + ) + + return split_dataset["train"], split_dataset["test"] + + +def _generate_from_iterable_dataset( + dataset: IterableDataset, worker_id: list[int], num_workers: list[int] +) -> Generator[Any, None, None]: + """Generator function to correctly split the dataset for each worker""" + for i, item in enumerate(dataset): + if i % num_workers[0] == worker_id[0]: + yield item + + +def save_preprocessed_dataset( + cfg: DictDefault, + dataset: Dataset, + dataset_hash: str, + split: str, +) -> None: + """Save preprocessed dataset to disk and optionally push to the HF Hub.""" + prepared_ds_path = get_prepared_dataset_path(cfg, dataset_hash) + if isinstance(dataset, IterableDataset): + num_workers = cfg.dataset_processes + + ds_from_iter = Dataset.from_generator( + functools.partial(_generate_from_iterable_dataset, dataset), + features=dataset.features, + num_proc=num_workers, + split=split, + gen_kwargs={ + "worker_id": list(range(num_workers)), + "num_workers": [num_workers] * num_workers, + }, ) - if not ds: - raise ValueError( - "The dataset could not be loaded. This could be due to a misconfigured dataset path " - f"({config_dataset.path}). Try double-check your path / name / data_files. " - "This is not caused by the dataset type." + ds_from_iter.save_to_disk(str(prepared_ds_path)) + else: + os.makedirs(prepared_ds_path, exist_ok=True) + dataset.save_to_disk(str(prepared_ds_path)) + if cfg.push_dataset_to_hub: + LOG.info( + "Pushing merged prepared dataset to Huggingface hub at " + f"{cfg.push_dataset_to_hub} (version {dataset_hash})...", + main_process_only=False, + ) + dataset.push_to_hub( + cfg.push_dataset_to_hub, + dataset_hash, + private=True, ) - return ds + +def load_preprocessed_dataset(cfg: DictDefault, dataset_hash: str) -> Dataset | None: + """Load preprocessed dataset from disk if available. + + Args: + cfg: Configuration object. + dataset_hash: Hash identifying the dataset configuration. + + Returns: + Loaded dataset if found and conditions are met, None otherwise. + """ + prepared_ds_path = get_prepared_dataset_path(cfg, dataset_hash) + + if ( + cfg.dataset_prepared_path + and any(prepared_ds_path.glob("*")) + and not cfg.skip_prepare_dataset + and not cfg.is_preprocess + ): + LOG.info( + f"Loading prepared dataset from disk at {prepared_ds_path}...", + main_process_only=False, + ) + return load_from_disk(str(prepared_ds_path)) + + LOG.info( + f"Unable to find prepared dataset in {prepared_ds_path}", + main_process_only=False, + ) + return None + + +def try_load_from_hub( + cfg: DictDefault, dataset_hash: str, split: str +) -> Dataset | None: + """Try to load the prepared dataset from HuggingFace Hub.""" + try: + LOG.info( + "Attempting to load prepared dataset from HuggingFace Hub at " + f"{cfg.push_dataset_to_hub} (version {dataset_hash})..." + ) + dataset = load_dataset( + cfg.push_dataset_to_hub, + dataset_hash, + token=cfg.hf_use_auth_token, + ) + return dataset[split] + except Exception: # pylint: disable=broad-except # nosec + LOG.info("Unable to find prepared dataset in HuggingFace Hub") + return None + + +def generate_dataset_hash_from_config( + cfg: DictDefault, cfg_datasets: list, tokenizer_name: str +) -> str: + """Generate a hash to uniquely identify a dataset configuration for SFT. + + Args: + cfg: Main configuration object. + cfg_datasets: List of dataset configurations. + tokenizer_name: Name of the tokenizer being used. + + Returns: + MD5 hash string representing the configuration. + """ + config_str = ( + f"{cfg.sequence_len}@{cfg.sample_packing}@{cfg.eval_sample_packing}@" + f"{cfg.group_by_length}@{cfg.kd_temperature or 1.0}|" + f"{'|'.join(sorted([f'{d.path}:{d.type}:{d.shards}:{d.conversation}:{d.split}:{d.temperature or 1.0}' for d in cfg_datasets]))}" + f"|{tokenizer_name}" + ) + return str(md5(config_str)) + + +def merge_datasets(datasets: list[Dataset], cfg: DictDefault) -> Dataset: + """Merge multiple datasets into one with optional shuffling. + + Args: + datasets: List of datasets to merge. + cfg: Configuration object containing shuffle settings. + + Returns: + Merged dataset. + """ + if len(datasets) == 1: + return datasets[0] + + LOG.info("Merging datasets...") + merged_dataset = concatenate_datasets(datasets) + + if cfg.shuffle_merged_datasets: + LOG.debug("Shuffling merged datasets...") + merged_dataset = merged_dataset.shuffle(seed=cfg.seed) + else: + LOG.debug("Not shuffling merged datasets.") + + return merged_dataset diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index 5f3b8d3cc6..0ffaa932fa 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -1,9 +1,11 @@ -"""data handling helpers""" +"""Data handling helpers""" +import contextlib import functools import hashlib import time from enum import Enum +from typing import Callable import huggingface_hub import numpy as np @@ -19,9 +21,7 @@ class RetryStrategy(Enum): - """ - Enum for retry strategies. - """ + """Enum for retry strategies.""" CONSTANT = 1 LINEAR = 2 @@ -30,7 +30,18 @@ class RetryStrategy(Enum): def retry_on_request_exceptions( max_retries=3, delay=1, retry_strategy: RetryStrategy = RetryStrategy.LINEAR -): +) -> Callable: + """Decorator that retries function calls on specific request exceptions. + + Args: + max_retries: Maximum number of retry attempts. + delay: Base delay between retries in seconds. + retry_strategy: Strategy for calculating retry delays. + + Returns: + Decorated function with retry logic. + """ + def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements @@ -59,6 +70,7 @@ def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements def md5(to_hash: str, encoding: str = "utf-8") -> str: + """Generate MD5 hash of a string.""" try: return hashlib.md5(to_hash.encode(encoding), usedforsecurity=False).hexdigest() except TypeError: @@ -66,102 +78,89 @@ def md5(to_hash: str, encoding: str = "utf-8") -> str: def sha256(to_hash: str, encoding: str = "utf-8") -> str: + """Generate SHA256 hash of a string.""" return hashlib.sha256(to_hash.encode(encoding)).hexdigest() -def deduplicate_dataset( - dataset: Dataset, seen_hashes: dict[str, list[int]], other_dataset: Dataset = None -) -> Dataset: - unique_indices = [] +def _deduplicate_dataset( + dataset: Dataset, + seen_hashes: set[str] | None = None, +) -> tuple[Dataset, set[str]]: + """Remove duplicate rows from a dataset using SHA256 hashes. + Args: + dataset: Dataset to deduplicate. + seen_hashes: Set of previously seen row hashes (for cross-deduplication). + + Returns: + Tuple of deduplicated dataset and the set of seen hashes. + """ + if seen_hashes is None: + seen_hashes = set() + + unique_indices = [] for idx, row in enumerate(dataset): - row_hash = sha256(str(row)) # Using SHA256 for collision resistance. + row_hash = sha256(str(row)) # Using SHA256 for collision resistance if row_hash not in seen_hashes: - seen_hashes[row_hash] = [idx] + seen_hashes.add(row_hash) unique_indices.append(idx) - else: - # Check for collision by looking up the original dataset indices - original_indices = seen_hashes[row_hash] - is_duplicate = False - for original_idx in original_indices: - if ( - not idx == original_idx - and original_idx < len(dataset) - and str(dataset[original_idx]) == str(row) - ): - is_duplicate = True - break - # Check in the other dataset if provided - if other_dataset is not None: - if original_idx < len(other_dataset) and str( - other_dataset[original_idx] - ) == str(row): - is_duplicate = True - break - if not is_duplicate: - seen_hashes[row_hash].append(idx) - unique_indices.append(idx) - continue - return dataset.select(unique_indices) + + return dataset.select(unique_indices), seen_hashes def deduplicate_and_log_datasets( - *, - train_dataset: Dataset = None, - eval_dataset: Dataset = None, - dataset: Dataset = None, -) -> tuple[Dataset, Dataset, Dataset]: - """ - Deduplicates train, eval, and an optional dataset if provided, logging original and new sizes. + dataset: Dataset, + other_dataset: Dataset | None = None, + dataset_name: str | None = "train", + other_name: str | None = "eval", +) -> tuple[Dataset, Dataset | None]: + """Deduplicate datasets, with optional cross-dataset deduplication. + + Args: + dataset: Primary dataset to deduplicate. + other_dataset: Optional second dataset to deduplicate against the first. + dataset_name: Name for the primary dataset (for logging). + other_name: Name for the second dataset (for logging). Returns: - tuple: Deduplicated train, eval, and additional datasets. + Tuple of (deduplicated_dataset, deduplicated_other_dataset). """ - seen_hashes: dict[str, list[int]] = {} + # Deduplicate primary dataset + LOG.info( + f"Starting deduplication for {dataset_name} dataset. Original size: {len(dataset)}" + ) + dataset, seen_rows = _deduplicate_dataset(dataset) + LOG.info( + f"Deduplication complete for {dataset_name} dataset. New size: {len(dataset)}" + ) - # Handle cases where datasets are None - if train_dataset is not None: + # Deduplicate second dataset if provided + if other_dataset is not None: LOG.info( - f"Starting deduplication for train dataset. Original size: {len(train_dataset)}" - ) - train_dataset = deduplicate_dataset( - dataset=train_dataset, seen_hashes=seen_hashes + f"Starting deduplication for {other_name} dataset. Original size: {len(other_dataset)}" ) + other_dataset, _ = _deduplicate_dataset(other_dataset, seen_rows) LOG.info( - f"Deduplication complete for train dataset. New size: {len(train_dataset)}" + f"Deduplication complete for {other_name} dataset. New size: {len(other_dataset)}" ) - else: - LOG.info("Train dataset is None. Skipping deduplication.") - if eval_dataset is not None: - LOG.info( - f"Starting deduplication for eval dataset. Original size: {len(eval_dataset)}" - ) - eval_dataset = deduplicate_dataset( - dataset=eval_dataset, seen_hashes=seen_hashes, other_dataset=train_dataset - ) - LOG.info( - f"Deduplication complete for eval dataset. New size: {len(eval_dataset)}" - ) - else: - LOG.info("Eval dataset is None. Skipping deduplication.") + return dataset, other_dataset - if dataset is not None and (eval_dataset is None and train_dataset is None): - LOG.info( - f"Starting deduplication for combined dataset. Original size: {len(dataset)}" - ) - dataset = deduplicate_dataset(dataset=dataset, seen_hashes=seen_hashes) - LOG.info( - f"Deduplication complete for combined dataset. New size: {len(dataset)}" - ) - return train_dataset, eval_dataset, dataset +def drop_long_seq_in_dataset(dataset: Dataset, cfg: DictDefault) -> Dataset: + """Remove sequences longer than configured maximum from dataset. + Args: + dataset: Dataset to filter. + cfg: Dictionary mapping `axolotl` config keys to values. -def drop_long_seq_in_dataset(dataset: Dataset, cfg: DictDefault): + Returns: + Filtered dataset with long sequences removed. + """ if "input_ids" not in dataset.column_names: LOG.warning( - "Dataset does not contain 'input_ids' column. Skip drop long seq. This is expected for RewardModeling." + "Dataset does not contain 'input_ids' column. Skip drop long seq. This is " + "expected for reward modeling." ) return dataset @@ -171,20 +170,14 @@ def drop_long_seq_in_dataset(dataset: Dataset, cfg: DictDefault): min_sequence_len=cfg.min_sample_len, ) - try: + with contextlib.suppress(AttributeError): ds_lengths = get_dataset_lengths(dataset, from_arrow=True) min_input_len = np.min(ds_lengths) LOG.info(f"min_input_len: {min_input_len}") max_input_len = np.max(ds_lengths) LOG.info(f"max_input_len: {max_input_len}") - except AttributeError: - pass - try: - prior_len = len(dataset) - except TypeError: - # handle iterable datasets case - prior_len = None + prior_len = len(dataset) if hasattr(dataset, "__len__") else None filter_map_kwargs = {} if not isinstance(dataset, IterableDataset): diff --git a/src/axolotl/utils/data/wrappers.py b/src/axolotl/utils/data/wrappers.py new file mode 100644 index 0000000000..b6dc42c710 --- /dev/null +++ b/src/axolotl/utils/data/wrappers.py @@ -0,0 +1,425 @@ +"""Data handling specific to SFT.""" + +import logging +from typing import Any, NoReturn, cast + +from datasets import ( + Dataset, + IterableDataset, + Sequence, + Value, +) +from transformers import PreTrainedTokenizer +from transformers.processing_utils import ProcessorMixin + +from axolotl.datasets import TokenizedPromptDataset, wrap_dataset_for_tokenized_prompt +from axolotl.prompt_strategies import load +from axolotl.prompt_strategies.bradley_terry import load as bradley_terry_load +from axolotl.prompt_tokenizers import ( + AlpacaMultipleChoicePromptTokenizingStrategy, + AlpacaPromptTokenizingStrategy, + AlpacaReflectionPTStrategy, + DatasetWrappingStrategy, + GPTeacherPromptTokenizingStrategy, + JeopardyPromptTokenizingStrategy, + OpenAssistantPromptTokenizingStrategy, + PromptTokenizingStrategy, + SummarizeTLDRPromptTokenizingStrategy, +) +from axolotl.prompters import ( + AlpacaPrompter, + GPTeacherPrompter, + JeopardyPrompter, + MultipleChoiceConcisePrompter, + MultipleChoiceExplainPrompter, + Prompter, + ReflectAlpacaPrompter, + SummarizeTLDRPrompter, + UnsupportedPrompter, +) +from axolotl.utils.dict import DictDefault + +LOG = logging.getLogger(__name__) + + +def handle_unknown_dataset_strategy(dataset_config: DictDefault) -> NoReturn: + """Raise error for unknown dataset strategy.""" + ds_type = dataset_config.type + suffix = "" + if ":load_" in ds_type: + suffix = f"Did you mean {ds_type.replace(':load_', '.load_')}?" + + error_message = f"unhandled prompt tokenization strategy: {ds_type}. {suffix}" + LOG.error(error_message) + raise ValueError(error_message) + + +# pylint: disable=too-many-return-statements +def get_dataset_wrapper( + dataset_config: DictDefault, + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + dataset_base_type: str | None, + dataset: Dataset | IterableDataset, + dataset_prompt_style: str | None = None, + processor: ProcessorMixin | None = None, # pylint: disable=unused-argument +) -> tuple[Dataset | IterableDataset, Prompter | None]: + """Create an appropriate dataset wrapper and prompter based on dataset + configuration. + + Args: + dataset_config: Configuration for the dataset. + tokenizer: Tokenizer to use for processing text. + cfg: Global configuration object. + dataset_base_type: The base type of the dataset. + dataset: The actual dataset object. + dataset_prompt_style: Optional prompt style specification. + processor: Optional processor for multimodal datasets. + + Returns: + tuple of (dataset_wrapper, dataset_prompter). + """ + # Common parameters for dataset wrapping + dataset_kwargs: dict[str, Any] = { + "process_count": cfg.dataset_processes, + "keep_in_memory": cfg.dataset_keep_in_memory is True, + } + + LOG.info( + f"Loading dataset: {dataset_config['path']} with base_type: " + f"{dataset_base_type} and prompt_style: {dataset_prompt_style}" + ) + + # Dataset is already tokenized + if _is_dataset_already_tokenized(dataset): + return dataset, UnsupportedPrompter() + + # Custom dataset type definition + if isinstance(dataset_config.type, DictDefault): + return _handle_custom_dataset_type( + dataset_config, tokenizer, cfg, dataset, dataset_kwargs + ) + + # Skip preparation if configured + if cfg.skip_prepare_dataset: + return dataset, None + + # Bradley-Terry dataset + if dataset_config.type.startswith("bradley_terry"): + return _handle_bradley_terry_dataset( + dataset_config, tokenizer, cfg, dataset, dataset_kwargs + ) + + # Stepwise supervised dataset + if dataset_config.type.startswith("stepwise_supervised"): + return _handle_stepwise_supervised_dataset( + dataset_config, tokenizer, cfg, dataset, dataset_kwargs + ) + + # Try to load prompt tokenizer / dataset wrapper strategy from registry + dataset_strategy = load( + dataset_config.type, tokenizer, cfg, dataset_config, processor=processor + ) + if dataset_strategy: + return _handle_loaded_strategy(dataset_strategy, dataset, dataset_kwargs) + + # Known dataset types with specific handling + if dataset_base_type in DATASET_HANDLERS: + handler = DATASET_HANDLERS[dataset_base_type] + return handler(dataset_prompt_style, tokenizer, cfg, dataset, dataset_kwargs) + + # Unhandled dataset type + handle_unknown_dataset_strategy(dataset_config) + + +def _is_dataset_already_tokenized(dataset: Dataset | IterableDataset) -> bool: + """Check if the dataset is already tokenized.""" + return ( + isinstance(dataset, Dataset) + and "input_ids" in dataset.features + and "attention_mask" in dataset.features + and "labels" in dataset.features + ) + + +def _handle_custom_dataset_type( + dataset_config: DictDefault, + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + dataset: Dataset | IterableDataset, + dataset_kwargs: dict[str, Any], +) -> tuple[Dataset | IterableDataset, Prompter]: + """Handle a custom dataset type defined in the configuration.""" + dataset_strategy = cast( + PromptTokenizingStrategy, + load("user_defined", tokenizer, cfg, dataset_config.type.to_dict()), + ) + dataset_prompter = UnsupportedPrompter() + dataset_wrapper = wrap_dataset_for_tokenized_prompt( + dataset_strategy, + dataset, + **dataset_kwargs, + ) + return dataset_wrapper, dataset_prompter + + +def _handle_bradley_terry_dataset( + dataset_config: DictDefault, + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + dataset: Dataset | IterableDataset, + dataset_kwargs: dict[str, Any], +) -> tuple[Dataset | IterableDataset, Prompter | None]: + """Handle a Bradley-Terry dataset.""" + bt_type = dataset_config.type.split(".", 1)[1] + dataset_strategy = bradley_terry_load(bt_type, tokenizer, cfg, dataset_config) + + if not dataset_strategy: + handle_unknown_dataset_strategy(dataset_config) + + dataset_prompter = UnsupportedPrompter() + dataset_wrapper = wrap_dataset_for_tokenized_prompt( + dataset_strategy, + dataset, + **dataset_kwargs, + ) + + return dataset_wrapper, dataset_prompter + + +def _handle_stepwise_supervised_dataset( + dataset_config: DictDefault, + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + dataset: Dataset | IterableDataset, + dataset_kwargs: dict[str, Any], +) -> tuple[Dataset | IterableDataset, Prompter]: + """Handle a stepwise supervised dataset.""" + dataset_prompter = UnsupportedPrompter() + dataset_strategy = load(dataset_config.type, tokenizer, cfg, dataset_config) + + # We need to explicitly cast boolean labels to int + # for compatibility with how trl's PRMTrainer works + if isinstance(dataset, Dataset): + dataset = dataset.cast_column("labels", Sequence(Value("int64"))) + + dataset_wrapper = TokenizedPromptDataset( + dataset_strategy, + dataset, + **dataset_kwargs, + ) + return dataset_wrapper, dataset_prompter + + +def _handle_loaded_strategy( + dataset_strategy: PromptTokenizingStrategy | DatasetWrappingStrategy, + dataset: Dataset | IterableDataset, + dataset_kwargs: dict[str, Any], +) -> tuple[Dataset | IterableDataset, Prompter | None]: + """Handle a dataset with a strategy loaded from the registry.""" + if isinstance(dataset_strategy, DatasetWrappingStrategy): + return dataset_strategy.wrap_dataset(dataset, **dataset_kwargs), None + + dataset_prompter = UnsupportedPrompter() + dataset_wrapper = wrap_dataset_for_tokenized_prompt( + dataset_strategy, + dataset, + **dataset_kwargs, + ) + return dataset_wrapper, dataset_prompter + + +def _handle_alpaca_dataset( + dataset_prompt_style: str | None, + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + dataset: Dataset | IterableDataset, + dataset_kwargs: dict[str, Any], +) -> tuple[Dataset | IterableDataset, Prompter]: + """Handle an Alpaca dataset.""" + dataset_prompter = AlpacaPrompter(dataset_prompt_style) + dataset_strategy = AlpacaPromptTokenizingStrategy( + dataset_prompter, + tokenizer, + cfg.train_on_inputs, + cfg.sequence_len, + ) + dataset_wrapper = wrap_dataset_for_tokenized_prompt( + dataset_strategy, + dataset, + **dataset_kwargs, + ) + return dataset_wrapper, dataset_prompter + + +def _handle_explainchoice_dataset( + dataset_prompt_style: str | None, + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + dataset: Dataset | IterableDataset, + dataset_kwargs: dict[str, Any], +) -> tuple[Dataset | IterableDataset, Prompter]: + """Handle an ExplainChoice dataset.""" + dataset_prompter = MultipleChoiceExplainPrompter(dataset_prompt_style) + dataset_strategy = AlpacaMultipleChoicePromptTokenizingStrategy( + dataset_prompter, + tokenizer, + cfg.train_on_inputs, + cfg.sequence_len, + ) + dataset_wrapper = wrap_dataset_for_tokenized_prompt( + dataset_strategy, + dataset, + **dataset_kwargs, + ) + return dataset_wrapper, dataset_prompter + + +def _handle_concisechoice_dataset( + dataset_prompt_style: str | None, + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + dataset: Dataset | IterableDataset, + dataset_kwargs: dict[str, Any], +) -> tuple[Dataset | IterableDataset, Prompter]: + """Handle a ConciseChoice dataset.""" + dataset_prompter = MultipleChoiceConcisePrompter(dataset_prompt_style) + dataset_strategy = AlpacaMultipleChoicePromptTokenizingStrategy( + dataset_prompter, + tokenizer, + cfg.train_on_inputs, + cfg.sequence_len, + ) + dataset_wrapper = wrap_dataset_for_tokenized_prompt( + dataset_strategy, + dataset, + **dataset_kwargs, + ) + return dataset_wrapper, dataset_prompter + + +def _handle_summarizetldr_dataset( + dataset_prompt_style: str | None, + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + dataset: Dataset | IterableDataset, + dataset_kwargs: dict[str, Any], +) -> tuple[Dataset | IterableDataset, Prompter]: + """Handle a SummarizeTLDR dataset.""" + dataset_prompter = SummarizeTLDRPrompter(dataset_prompt_style) + dataset_strategy = SummarizeTLDRPromptTokenizingStrategy( + dataset_prompter, + tokenizer, + cfg.train_on_inputs, + cfg.sequence_len, + ) + dataset_wrapper = wrap_dataset_for_tokenized_prompt( + dataset_strategy, + dataset, + **dataset_kwargs, + ) + return dataset_wrapper, dataset_prompter + + +def _handle_jeopardy_dataset( + dataset_prompt_style: str | None, + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + dataset: Dataset | IterableDataset, + dataset_kwargs: dict[str, Any], +) -> tuple[Dataset | IterableDataset, Prompter]: + """Handle a Jeopardy dataset.""" + dataset_prompter = JeopardyPrompter(dataset_prompt_style) + dataset_strategy = JeopardyPromptTokenizingStrategy( + dataset_prompter, + tokenizer, + cfg.train_on_inputs, + cfg.sequence_len, + ) + dataset_wrapper = wrap_dataset_for_tokenized_prompt( + dataset_strategy, + dataset, + **dataset_kwargs, + ) + return dataset_wrapper, dataset_prompter + + +def _handle_oasst_dataset( + dataset_prompt_style: str | None, + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + dataset: Dataset | IterableDataset, + dataset_kwargs: dict[str, Any], +) -> tuple[Dataset | IterableDataset, Prompter]: + """Handle an OpenAssistant dataset.""" + dataset_prompter = AlpacaPrompter(dataset_prompt_style) + dataset_strategy = OpenAssistantPromptTokenizingStrategy( + dataset_prompter, + tokenizer, + cfg.train_on_inputs, + cfg.sequence_len, + ) + dataset_wrapper = wrap_dataset_for_tokenized_prompt( + dataset_strategy, + dataset, + **dataset_kwargs, + ) + return dataset_wrapper, dataset_prompter + + +def _handle_gpteacher_dataset( + dataset_prompt_style: str | None, + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + dataset: Dataset | IterableDataset, + dataset_kwargs: dict[str, Any], +) -> tuple[Dataset | IterableDataset, Prompter]: + """Handle a GPTeacher dataset.""" + dataset_prompter = GPTeacherPrompter(dataset_prompt_style) + dataset_strategy = GPTeacherPromptTokenizingStrategy( + dataset_prompter, + tokenizer, + cfg.train_on_inputs, + cfg.sequence_len, + ) + dataset_wrapper = wrap_dataset_for_tokenized_prompt( + dataset_strategy, + dataset, + **dataset_kwargs, + ) + return dataset_wrapper, dataset_prompter + + +def _handle_reflection_dataset( + dataset_prompt_style: str | None, + tokenizer: PreTrainedTokenizer, + cfg: DictDefault, + dataset: Dataset | IterableDataset, + dataset_kwargs: dict[str, Any], +) -> tuple[Dataset | IterableDataset, Prompter]: + """Handle a Reflection dataset.""" + dataset_prompter = ReflectAlpacaPrompter(dataset_prompt_style) + dataset_strategy = AlpacaReflectionPTStrategy( + dataset_prompter, + tokenizer, + cfg.train_on_inputs, + cfg.sequence_len, + ) + dataset_wrapper = wrap_dataset_for_tokenized_prompt( + dataset_strategy, + dataset, + **dataset_kwargs, + ) + return dataset_wrapper, dataset_prompter + + +DATASET_HANDLERS = { + "alpaca": _handle_alpaca_dataset, + "explainchoice": _handle_explainchoice_dataset, + "concisechoice": _handle_concisechoice_dataset, + "summarizetldr": _handle_summarizetldr_dataset, + "jeopardy": _handle_jeopardy_dataset, + "oasst": _handle_oasst_dataset, + "gpteacher": _handle_gpteacher_dataset, + "reflection": _handle_reflection_dataset, +} diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index e5f105053c..dad6aac621 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -336,6 +336,14 @@ class AxolotlInputConfig( plugins: list[str] | None = Field(default=None) + @field_validator("seed", mode="after") + @classmethod + def set_default_seed(cls, seed): + if seed is None: + LOG.info("`seed` not set in config; setting to 42") + seed = 42 + return seed + @field_validator("datasets", mode="before") @classmethod def deprecate_sharegpt_datasets(cls, datasets): @@ -1199,7 +1207,7 @@ def check_sequence_parallel_degree(self): "flash_attention: true must be set with sequence_parallel_degree > 1" ) - if self.sample_packing and self.micro_batch_size > 1: + if self.sample_packing and getattr(self, "micro_batch_size", 1) > 1: raise ValueError( "micro_batch_size must be set to 1 when sample_packing is enabled " "due to a `ring-flash-attn` requirement" diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index cde7b74ce8..e66b8e009d 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -12,7 +12,7 @@ from axolotl.core.builders import HFCausalTrainerBuilder, HFRLTrainerBuilder from axolotl.loaders import ModelLoader, load_tokenizer from axolotl.utils.config import normalize_config -from axolotl.utils.data.rl import load_prepare_preference_datasets +from axolotl.utils.data import prepare_preference_datasets from axolotl.utils.dict import DictDefault from axolotl.utils.schemas.enums import RLType @@ -451,15 +451,19 @@ def test_custom_optimizer_cls_and_kwargs( # Only use mock for the commented out configs if dataset_name is not None: with patch( - "axolotl.utils.data.rl.load_dataset_w_config" + "axolotl.utils.data.rl.load_dataset_with_config" ) as mock_load_dataset: mock_load_dataset.return_value = request.getfixturevalue( dataset_name ) - train_dataset, eval_dataset = load_prepare_preference_datasets(cfg) + train_dataset, eval_dataset = prepare_preference_datasets( + cfg, tokenizer + ) else: # Load actual datasets for orpo_cfg and kto_cfg - train_dataset, eval_dataset = load_prepare_preference_datasets(cfg) + train_dataset, eval_dataset = prepare_preference_datasets( + cfg, tokenizer + ) builder.train_dataset = train_dataset builder.eval_dataset = eval_dataset diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py index 2ae59a15a2..790b34f3e6 100644 --- a/tests/e2e/integrations/test_cut_cross_entropy.py +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -4,7 +4,6 @@ import pytest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils import get_pytorch_version @@ -59,8 +58,7 @@ def test_llama_w_cce(self, min_cfg, temp_dir): cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) major, minor, _ = get_pytorch_version() if (major, minor) < (2, 4): @@ -105,8 +103,7 @@ def test_qwen2_w_cce(self, temp_dir): cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) major, minor, _ = get_pytorch_version() if (major, minor) < (2, 4): @@ -134,8 +131,7 @@ def test_llama_w_cce_and_attention(self, min_cfg, temp_dir, attention_type): cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) major, minor, _ = get_pytorch_version() if (major, minor) < (2, 4): diff --git a/tests/e2e/integrations/test_hooks.py b/tests/e2e/integrations/test_hooks.py index 45d7200fb6..4734449fe2 100644 --- a/tests/e2e/integrations/test_hooks.py +++ b/tests/e2e/integrations/test_hooks.py @@ -5,7 +5,6 @@ import os from pathlib import Path -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.integrations.base import BasePlugin from axolotl.train import train @@ -160,8 +159,7 @@ def test_plugin_hooks(self, temp_dir): cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py index dad7779477..2bd1fbf3d9 100644 --- a/tests/e2e/integrations/test_kd.py +++ b/tests/e2e/integrations/test_kd.py @@ -6,7 +6,6 @@ import pytest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, prepare_plugins, validate_config @@ -84,8 +83,7 @@ def test_llama_kd(self, temp_dir, kd_min_cfg): cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "model.safetensors").exists() @@ -115,8 +113,7 @@ def test_llama_lora_kd(self, temp_dir, kd_min_cfg, load_in_8bit): cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.safetensors").exists() diff --git a/tests/e2e/integrations/test_liger.py b/tests/e2e/integrations/test_liger.py index 8ecfc4746e..6ab3d7ab89 100644 --- a/tests/e2e/integrations/test_liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -2,7 +2,6 @@ Simple end-to-end test for Liger integration """ -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, prepare_plugins, validate_config @@ -57,8 +56,7 @@ def test_llama_wo_flce(self, temp_dir): cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -104,8 +102,7 @@ def test_llama_w_flce(self, temp_dir): cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/integrations/test_llm_compressor.py b/tests/e2e/integrations/test_llm_compressor.py index 20bf821bfd..247ae3bac2 100644 --- a/tests/e2e/integrations/test_llm_compressor.py +++ b/tests/e2e/integrations/test_llm_compressor.py @@ -6,7 +6,6 @@ import pytest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, prepare_plugins, validate_config @@ -88,8 +87,7 @@ def test_llmcompressor_plugin( prepare_plugins(cfg) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) try: train(cfg=cfg, dataset_meta=dataset_meta) diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index 8ea2e3ce4d..1daf584726 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -105,7 +105,7 @@ def start_vllm( print(f"{i}: VLLM server failed to start: {str(exc)}") # also check if the process.pid is still running - if not process.poll() is None: + if process.poll() is not None: break time.sleep(period_seconds) diff --git a/tests/e2e/multigpu/test_locking.py b/tests/e2e/multigpu/test_locking.py new file mode 100644 index 0000000000..42502dfa3c --- /dev/null +++ b/tests/e2e/multigpu/test_locking.py @@ -0,0 +1,192 @@ +"""Tests for FileLockLoader class.""" + +import tempfile +import threading +import time +from pathlib import Path +from unittest.mock import MagicMock, Mock, patch + +import pytest + +from axolotl.utils.data.lock import FileLockLoader +from axolotl.utils.dict import DictDefault + + +class TestFileLockLoader: + """Class with tests for FileLockLoader.""" + + @pytest.fixture + def temp_dir(self): + """Create a temporary directory for testing.""" + with tempfile.TemporaryDirectory() as tmp_dir: + yield Path(tmp_dir) + + @pytest.fixture + def cfg(self, temp_dir): + """Create a test configuration.""" + return DictDefault({"dataset_prepared_path": str(temp_dir)}) + + @pytest.fixture + def loader(self, cfg): + """Create a FileLockLoader instance for testing.""" + return FileLockLoader(cfg) + + def test_load_first_process(self, loader): + """Test load() when no ready flag exists (first process).""" + mock_load_fn = Mock(return_value="test_data") + + result = loader.load(mock_load_fn) + + # Should call the load function + mock_load_fn.assert_called_once() + assert result == "test_data" + + # Should create the ready flag + assert loader.ready_flag_path.exists() + + def test_load_subsequent_process(self, loader): + """Test load() when ready flag already exists (subsequent process).""" + # Create ready flag first + loader.ready_flag_path.touch() + + mock_load_fn = Mock(return_value="loaded_data") + + result = loader.load(mock_load_fn) + + # Should still call load function (to load the prepared data) + mock_load_fn.assert_called_once() + assert result == "loaded_data" + + def test_load_concurrent_processes(self, cfg): + """Test that concurrent processes coordinate correctly.""" + results = [] + call_count = 0 + + def slow_load_fn(): + nonlocal call_count + call_count += 1 + time.sleep(0.1) # Simulate slow loading + return f"data_{call_count}" + + def worker(): + loader = FileLockLoader(cfg) + result = loader.load(slow_load_fn) + results.append(result) + + # Start multiple threads simultaneously + threads = [threading.Thread(target=worker) for _ in range(3)] + for t in threads: + t.start() + for t in threads: + t.join() + + # Only one thread should have done the initial loading + # All should return data, but the load function should be called + # once by the first process and once by each subsequent process + assert len(results) == 3 + assert all(result.startswith("data_") for result in results) + + @patch("time.sleep") + def test_load_waiting_for_ready_flag(self, mock_sleep, loader): + """Test that processes wait for the ready flag to appear.""" + mock_load_fn = Mock(return_value="waiting_data") + mock_ready_flag_path = Mock() + exists_call_count = 0 + + def mock_exists(): + nonlocal exists_call_count + exists_call_count += 1 + + if exists_call_count == 1: + # First check: ready flag exists (not first process) + return True + if exists_call_count <= 3: + # While loop checks: flag doesn't exist yet + return False + return True + + mock_ready_flag_path.exists.side_effect = mock_exists + + # Replace the ready_flag_path with our mock + original_path = loader.ready_flag_path + loader.ready_flag_path = mock_ready_flag_path + + try: + result = loader.load(mock_load_fn) + finally: + # Restore original path + loader.ready_flag_path = original_path + + # Should have slept twice while waiting + assert mock_sleep.call_count == 2 + mock_sleep.assert_called_with(1) + + # Should eventually call load function + mock_load_fn.assert_called_once() + assert result == "waiting_data" + + def test_complete_workflow_with_cleanup(self, loader): + """Test the complete load -> cleanup workflow.""" + mock_load_fn = Mock(return_value="test_data") + + # First process calls load (this should set up counter) + result = loader.load(mock_load_fn) + assert result == "test_data" + assert loader.ready_flag_path.exists() + assert loader.counter_path.exists() + + # Cleanup should remove everything since there's only one process + loader.cleanup() + assert not loader.ready_flag_path.exists() + assert not loader.counter_path.exists() + + def test_multiple_processes_workflow(self, loader): + """Test workflow with multiple processes.""" + # Simulate multiple processes by manually setting up counter + loader.ready_flag_path.touch() + loader.counter_path.write_text("3") # 3 processes + + # First process cleanup + loader.cleanup() + assert loader.ready_flag_path.exists() + assert loader.counter_path.read_text().strip() == "2" + + # Second process cleanup + loader.cleanup() + assert loader.ready_flag_path.exists() + assert loader.counter_path.read_text().strip() == "1" + + # Last process cleanup + loader.cleanup() + assert not loader.ready_flag_path.exists() + assert not loader.counter_path.exists() + + def test_load_exception_handling(self, loader): + """Test behavior when load_fn raises an exception.""" + + def failing_load_fn(): + raise ValueError("Load failed") + + with pytest.raises(ValueError, match="Load failed"): + loader.load(failing_load_fn) + + # Ready flag should not be created on failure + assert not loader.ready_flag_path.exists() + + def test_file_lock_called(self, loader): + """Test that FileLock is properly used.""" + mock_load_fn = Mock(return_value="locked_data") + + with patch("axolotl.utils.data.lock.FileLock") as mock_filelock: + mock_context = MagicMock() + mock_filelock.return_value.__enter__ = Mock(return_value=mock_context) + mock_filelock.return_value.__exit__ = Mock(return_value=None) + + loader.load(mock_load_fn) + + # Verify FileLock was called with correct path + mock_filelock.assert_called_once_with(str(loader.lock_file_path)) + + # Verify context manager was used + mock_filelock.return_value.__enter__.assert_called_once() + mock_filelock.return_value.__exit__.assert_called_once() diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index 490ce77fbb..08b62accc0 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -4,7 +4,6 @@ import unittest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -60,8 +59,7 @@ def test_sdp_lora_packing(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -108,8 +106,7 @@ def test_torch_lora_packing(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_activation_checkpointing.py b/tests/e2e/patched/test_activation_checkpointing.py index 45107b871a..d494ed1ebd 100644 --- a/tests/e2e/patched/test_activation_checkpointing.py +++ b/tests/e2e/patched/test_activation_checkpointing.py @@ -6,7 +6,6 @@ import transformers from torch.utils.checkpoint import checkpoint -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -75,8 +74,7 @@ def test_activation_checkpointing_offload( cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index e66b67e6df..4e3cbc50d3 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -5,7 +5,6 @@ import pytest from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -73,8 +72,7 @@ def test_lora_packing_fa_cross_entropy(self, temp_dir, gradient_accumulation_ste cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_falcon_samplepack.py b/tests/e2e/patched/test_falcon_samplepack.py index bd80221cea..a593b07918 100644 --- a/tests/e2e/patched/test_falcon_samplepack.py +++ b/tests/e2e/patched/test_falcon_samplepack.py @@ -6,7 +6,6 @@ import pytest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -63,8 +62,7 @@ def test_qlora(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -105,8 +103,7 @@ def test_ft(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index 49478f10c8..1bbc82a38a 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -7,7 +7,6 @@ import pytest from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -62,8 +61,7 @@ def test_fft_packing(self, temp_dir): cfg.fp16 = True cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py index 327bb13f8a..d2dcc5e4b7 100644 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ b/tests/e2e/patched/test_llama_s2_attention.py @@ -6,7 +6,6 @@ import pytest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -64,8 +63,7 @@ def test_lora_s2_attn(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -107,8 +105,7 @@ def test_fft_s2_attn(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index 1bad677b91..5df6bfecc6 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -7,7 +7,6 @@ import pytest from transformers.utils import is_auto_gptq_available, is_torch_bf16_gpu_available -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -65,8 +64,7 @@ def test_lora_packing(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -114,8 +112,7 @@ def test_lora_gptq_packed(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index 994b9dfcaf..2de9cc96ff 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -4,7 +4,6 @@ import unittest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -60,8 +59,7 @@ def test_lora_packing(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -102,8 +100,7 @@ def test_ft_packing(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index 6a84069ef4..5f778660bb 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -4,7 +4,6 @@ import unittest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -57,8 +56,7 @@ def test_qlora(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -96,8 +94,7 @@ def test_ft(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_phi_multipack.py b/tests/e2e/patched/test_phi_multipack.py index ee2a3ffb45..d241ce1853 100644 --- a/tests/e2e/patched/test_phi_multipack.py +++ b/tests/e2e/patched/test_phi_multipack.py @@ -4,7 +4,6 @@ import unittest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -60,8 +59,7 @@ def test_ft_packed(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -112,8 +110,7 @@ def test_qlora_packed(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index cc1f3ddeeb..3639567335 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -7,7 +7,6 @@ from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -67,8 +66,7 @@ def test_resume_lora_packed(self, temp_dir): cfg.fp16 = True cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) @@ -78,7 +76,6 @@ def test_resume_lora_packed(self, temp_dir): } ) normalize_config(resume_cfg) - cli_args = TrainerCliArgs() train(cfg=resume_cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index 46f5b66148..9567c0b180 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -4,7 +4,6 @@ import pytest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -68,8 +67,7 @@ def test_unsloth_llama_qlora_fa2(self, temp_dir, sample_packing): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -119,8 +117,7 @@ def test_unsloth_llama_qlora_unpacked(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -175,8 +172,7 @@ def test_unsloth_llama_qlora_unpacked_no_fa2_fp16(self, temp_dir, sdp_attention) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/solo/test_flex.py b/tests/e2e/solo/test_flex.py index b33869b1c6..8d1a0c7d1e 100644 --- a/tests/e2e/solo/test_flex.py +++ b/tests/e2e/solo/test_flex.py @@ -6,7 +6,6 @@ from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -59,8 +58,7 @@ def test_loss_llama(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) diff --git a/tests/e2e/solo/test_relora_llama.py b/tests/e2e/solo/test_relora_llama.py index cff8313f3b..7af5504963 100644 --- a/tests/e2e/solo/test_relora_llama.py +++ b/tests/e2e/solo/test_relora_llama.py @@ -5,7 +5,6 @@ import unittest from pathlib import Path -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -71,8 +70,7 @@ def test_relora(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-100/adapter", cfg) diff --git a/tests/e2e/test_deepseekv3.py b/tests/e2e/test_deepseekv3.py index d882286ccd..7dfc4ae159 100644 --- a/tests/e2e/test_deepseekv3.py +++ b/tests/e2e/test_deepseekv3.py @@ -6,7 +6,6 @@ import pytest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -72,8 +71,7 @@ def test_lora_deepseekv3(self, temp_dir, sample_packing): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.safetensors").exists() @@ -122,8 +120,7 @@ def test_fft_deepseekv3(self, temp_dir, sample_packing): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "model.safetensors").exists() diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index e9f70758bc..2cdb576891 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -1,6 +1,4 @@ -""" -E2E tests for lora llama -""" +"""E2E tests for lora llama""" import unittest from pathlib import Path diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py index f1297fcf34..9b65f8feb6 100644 --- a/tests/e2e/test_embeddings_lr.py +++ b/tests/e2e/test_embeddings_lr.py @@ -4,7 +4,6 @@ import unittest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -54,8 +53,7 @@ def test_train_w_embedding_lr_scale(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -99,8 +97,7 @@ def test_train_w_embedding_lr(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_falcon.py b/tests/e2e/test_falcon.py index 7ea7e30f42..4f88e740c3 100644 --- a/tests/e2e/test_falcon.py +++ b/tests/e2e/test_falcon.py @@ -6,7 +6,6 @@ import pytest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -66,8 +65,7 @@ def test_lora(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -122,8 +120,7 @@ def test_lora_added_vocab(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -164,8 +161,7 @@ def test_ft(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_gemma2.py b/tests/e2e/test_gemma2.py index 65732a7374..c0eba72a78 100644 --- a/tests/e2e/test_gemma2.py +++ b/tests/e2e/test_gemma2.py @@ -6,7 +6,6 @@ import pytest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -69,8 +68,7 @@ def test_lora_gemma2(self, temp_dir, sample_packing): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.safetensors").exists() @@ -121,8 +119,7 @@ def test_fft_gemma2(self, temp_dir, sample_packing): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "model.safetensors").exists() diff --git a/tests/e2e/test_gemma3_text.py b/tests/e2e/test_gemma3_text.py index d790fa1560..3f00a13844 100644 --- a/tests/e2e/test_gemma3_text.py +++ b/tests/e2e/test_gemma3_text.py @@ -6,7 +6,6 @@ import pytest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -68,8 +67,7 @@ def test_lora_gemma3_text(self, temp_dir, sample_packing): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "adapter_model.safetensors").exists() @@ -119,8 +117,7 @@ def test_fft_gemma3_text(self, temp_dir, sample_packing): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "model.safetensors").exists() diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index 455e175325..2b180029ce 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -2,7 +2,6 @@ E2E tests for llama """ -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -51,8 +50,7 @@ def test_fft_trust_remote_code(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -99,8 +97,7 @@ def test_fix_untrained_tokens(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -144,8 +141,7 @@ def test_fix_untrained_tokens_already_trained(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -185,8 +181,7 @@ def test_batch_flattening(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index ec1e164a40..47d4b48395 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -1,10 +1,7 @@ -""" -E2E tests for llama pretrain -""" +"""E2E tests for llama pretrain""" import pytest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -14,9 +11,7 @@ class TestPretrainLlama: - """ - Test case for Llama models w pretraining - """ + """Test case for Llama models w pretraining""" @pytest.mark.parametrize( "sample_packing", @@ -66,8 +61,7 @@ def test_pretrain(self, temp_dir, sample_packing, pretrain_multipack_attn): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py index 32657c156f..ad4a83c6a2 100644 --- a/tests/e2e/test_llama_vision.py +++ b/tests/e2e/test_llama_vision.py @@ -4,7 +4,6 @@ import unittest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -60,8 +59,7 @@ def test_lora_llama_vision_text_only_dataset(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -106,8 +104,7 @@ def test_lora_llama_vision_multimodal_dataset(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index 9996250702..3015653021 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -4,7 +4,6 @@ import unittest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -55,8 +54,7 @@ def test_lora(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_mamba.py b/tests/e2e/test_mamba.py index efffb4547a..1824619a6a 100644 --- a/tests/e2e/test_mamba.py +++ b/tests/e2e/test_mamba.py @@ -6,7 +6,6 @@ import pytest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -57,8 +56,7 @@ def test_fft(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_mistral.py b/tests/e2e/test_mistral.py index 98a82a5f09..5d9b8ba8c1 100644 --- a/tests/e2e/test_mistral.py +++ b/tests/e2e/test_mistral.py @@ -6,7 +6,6 @@ from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -61,8 +60,7 @@ def test_lora(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -106,8 +104,7 @@ def test_ft(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_mixtral.py b/tests/e2e/test_mixtral.py index b551e431a8..761e59391c 100644 --- a/tests/e2e/test_mixtral.py +++ b/tests/e2e/test_mixtral.py @@ -7,7 +7,6 @@ import torch from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -67,8 +66,7 @@ def test_qlora_w_fa2(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) model, _, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( @@ -123,8 +121,7 @@ def test_qlora_wo_fa2(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) model, _, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( @@ -182,8 +179,7 @@ def test_16bit_lora_w_fa2(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) model, _, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( @@ -241,8 +237,7 @@ def test_16bit_lora_wo_fa2(self, temp_dir): cfg.bf16 = True else: cfg.fp16 = True - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) model, _, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( @@ -287,8 +282,7 @@ def test_ft(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index e812a5f7e4..53ef86022f 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -4,7 +4,6 @@ import unittest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -61,8 +60,7 @@ def test_optimi_adamw(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) _, _, trainer = train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -107,8 +105,7 @@ def test_adopt_adamw(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) _, _, trainer = train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -154,8 +151,7 @@ def test_muon(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) _, _, trainer = train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -194,8 +190,7 @@ def test_fft_schedule_free_adamw(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -242,8 +237,7 @@ def test_came_pytorch(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index 12e272888b..463f7c8383 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -6,7 +6,6 @@ from transformers.utils import is_torch_bf16_gpu_available -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -58,8 +57,7 @@ def test_loss_packed(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) diff --git a/tests/e2e/test_phi.py b/tests/e2e/test_phi.py index f8b43ad32f..88fda91915 100644 --- a/tests/e2e/test_phi.py +++ b/tests/e2e/test_phi.py @@ -4,7 +4,6 @@ import unittest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -58,8 +57,7 @@ def test_phi_ft(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) @@ -108,8 +106,7 @@ def test_phi_qlora(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_process_reward_model_smollm2.py b/tests/e2e/test_process_reward_model_smollm2.py index eb81959a21..abfe1b0c55 100644 --- a/tests/e2e/test_process_reward_model_smollm2.py +++ b/tests/e2e/test_process_reward_model_smollm2.py @@ -4,7 +4,6 @@ import unittest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -54,8 +53,7 @@ def test_prm(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_tensorboard( diff --git a/tests/e2e/test_qat.py b/tests/e2e/test_qat.py index f9e7993be6..2a7cd1459c 100644 --- a/tests/e2e/test_qat.py +++ b/tests/e2e/test_qat.py @@ -5,7 +5,6 @@ import unittest from pathlib import Path -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -64,8 +63,7 @@ def test_qat_lora(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-5", cfg) diff --git a/tests/e2e/test_reward_model_smollm2.py b/tests/e2e/test_reward_model_smollm2.py index 55405d58c4..304fda1cc4 100644 --- a/tests/e2e/test_reward_model_smollm2.py +++ b/tests/e2e/test_reward_model_smollm2.py @@ -4,7 +4,6 @@ import unittest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -63,8 +62,7 @@ def test_rm_lora(self, temp_dir): ) cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_tensorboard( diff --git a/tests/e2e/test_schedulers.py b/tests/e2e/test_schedulers.py index e468081b1d..e98378f08a 100644 --- a/tests/e2e/test_schedulers.py +++ b/tests/e2e/test_schedulers.py @@ -4,7 +4,6 @@ import unittest -from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -57,8 +56,7 @@ def test_rex_scheduler(self, temp_dir): cfg = validate_config(cfg) normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_datasets(cfg=cfg, cli_args=cli_args) + dataset_meta = load_datasets(cfg=cfg) train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) diff --git a/tests/prompt_strategies/test_dpo_chatml.py b/tests/prompt_strategies/test_dpo_chatml.py index b313a4b640..2c089067f7 100644 --- a/tests/prompt_strategies/test_dpo_chatml.py +++ b/tests/prompt_strategies/test_dpo_chatml.py @@ -6,8 +6,9 @@ import pytest +from axolotl.loaders.tokenizer import load_tokenizer from axolotl.prompt_strategies.dpo import load as load_dpo -from axolotl.utils.data.rl import load_prepare_preference_datasets +from axolotl.utils.data.rl import prepare_preference_datasets from axolotl.utils.dict import DictDefault from tests.hf_offline_utils import enable_hf_offline @@ -55,7 +56,8 @@ def test_default(self, minimal_dpo_cfg): # test that dpo.load works load_dpo("chatml", cfg) # now actually load the datasets with the strategy - train_ds, _ = load_prepare_preference_datasets(cfg) + tokenizer = load_tokenizer(cfg) + train_ds, _ = prepare_preference_datasets(cfg, tokenizer) assert train_ds[0]["prompt"].startswith("<|im_start|>") assert train_ds[0]["prompt"].endswith("<|im_start|>assistant\n") assert "chosen" in train_ds[0] diff --git a/tests/test_datasets.py b/tests/test_datasets.py index bd77591cf2..f4730f0f1d 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -1,10 +1,9 @@ -""" -Test dataset loading under various conditions. -""" +"""Test dataset loading under various conditions.""" import shutil import tempfile from pathlib import Path +from typing import Any, Generator from unittest.mock import patch import pytest @@ -12,8 +11,9 @@ from huggingface_hub import snapshot_download from transformers import PreTrainedTokenizer -from axolotl.utils.data import load_tokenized_prepared_datasets -from axolotl.utils.data.rl import load_prepare_preference_datasets +from axolotl.loaders.tokenizer import load_tokenizer +from axolotl.utils.data.rl import prepare_preference_datasets +from axolotl.utils.data.sft import _load_tokenized_prepared_datasets from axolotl.utils.dict import DictDefault from tests.constants import ( @@ -28,7 +28,9 @@ class TestDatasetPreparation: """Test a configured dataloader.""" @pytest.fixture - def tokenizer(self, tokenizer_huggyllama) -> PreTrainedTokenizer: + def tokenizer( + self, tokenizer_huggyllama + ) -> Generator[PreTrainedTokenizer, Any, Any]: tokenizer_huggyllama.add_special_tokens(SPECIAL_TOKENS) yield tokenizer_huggyllama @@ -63,7 +65,10 @@ def test_load_hub(self, tokenizer): } ) - dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) + with patch( + "axolotl.common.const.DEFAULT_DATASET_PREPARED_PATH", str(prepared_path) + ): + dataset, _ = _load_tokenized_prepared_datasets(tokenizer, cfg) assert len(dataset) == 2000 assert "input_ids" in dataset.features @@ -107,7 +112,10 @@ def test_load_local_hub(self, tokenizer): } ) - dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) + with patch( + "axolotl.common.const.DEFAULT_DATASET_PREPARED_PATH", str(prepared_path) + ): + dataset, _ = _load_tokenized_prepared_datasets(tokenizer, cfg) assert len(dataset) == 2000 assert "input_ids" in dataset.features @@ -136,7 +144,10 @@ def test_load_from_save_to_disk(self, tokenizer, dataset_fixture): } ) - dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) + with patch( + "axolotl.common.const.DEFAULT_DATASET_PREPARED_PATH", str(prepared_path) + ): + dataset, _ = _load_tokenized_prepared_datasets(tokenizer, cfg) assert len(dataset) == 1 assert "input_ids" in dataset.features @@ -145,7 +156,7 @@ def test_load_from_save_to_disk(self, tokenizer, dataset_fixture): @enable_hf_offline def test_load_from_dir_of_parquet(self, tokenizer, dataset_fixture): - """Usual use case. Verify a directory of parquet files can be loaded.""" + """Usual use case. Verify a directory of parquet files can be loaded.""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_ds_dir = Path(tmp_dir) / "tmp_dataset" tmp_ds_dir.mkdir() @@ -171,7 +182,10 @@ def test_load_from_dir_of_parquet(self, tokenizer, dataset_fixture): } ) - dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) + with patch( + "axolotl.common.const.DEFAULT_DATASET_PREPARED_PATH", str(prepared_path) + ): + dataset, _ = _load_tokenized_prepared_datasets(tokenizer, cfg) assert len(dataset) == 1 assert "input_ids" in dataset.features @@ -206,7 +220,10 @@ def test_load_from_dir_of_json(self, tokenizer, dataset_fixture): } ) - dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) + with patch( + "axolotl.common.const.DEFAULT_DATASET_PREPARED_PATH", str(prepared_path) + ): + dataset, _ = _load_tokenized_prepared_datasets(tokenizer, cfg) assert len(dataset) == 1 assert "input_ids" in dataset.features @@ -235,7 +252,10 @@ def test_load_from_single_parquet(self, tokenizer, dataset_fixture): } ) - dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) + with patch( + "axolotl.common.const.DEFAULT_DATASET_PREPARED_PATH", str(prepared_path) + ): + dataset, _ = _load_tokenized_prepared_datasets(tokenizer, cfg) assert len(dataset) == 1 assert "input_ids" in dataset.features @@ -264,7 +284,10 @@ def test_load_from_single_json(self, tokenizer, dataset_fixture): } ) - dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) + with patch( + "axolotl.common.const.DEFAULT_DATASET_PREPARED_PATH", str(prepared_path) + ): + dataset, _ = _load_tokenized_prepared_datasets(tokenizer, cfg) assert len(dataset) == 1 assert "input_ids" in dataset.features @@ -286,7 +309,8 @@ def test_load_hub_with_dpo(self): } ) - train_dataset, _ = load_prepare_preference_datasets(cfg) + tokenizer = load_tokenizer(cfg) + train_dataset, _ = prepare_preference_datasets(cfg, tokenizer) assert len(train_dataset) == 1800 assert "conversation" not in train_dataset.features @@ -318,7 +342,10 @@ def test_load_hub_with_revision(self, tokenizer): } ) - dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) + with patch( + "axolotl.common.const.DEFAULT_DATASET_PREPARED_PATH", str(prepared_path) + ): + dataset, _ = _load_tokenized_prepared_datasets(tokenizer, cfg) assert len(dataset) == 2000 assert "input_ids" in dataset.features @@ -342,13 +369,16 @@ def test_load_hub_with_revision_with_dpo( ) # pylint: disable=duplicate-code - with patch("axolotl.utils.data.rl.load_dataset_w_config") as mock_load_dataset: + with patch( + "axolotl.utils.data.rl.load_dataset_with_config" + ) as mock_load_dataset: # Set up the mock to return different values on successive calls mock_load_dataset.return_value = ( dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff ) - train_dataset, _ = load_prepare_preference_datasets(cfg) + tokenizer = load_tokenizer(cfg) + train_dataset, _ = prepare_preference_datasets(cfg, tokenizer) assert len(train_dataset) == 1800 assert "conversation" not in train_dataset.features @@ -393,16 +423,18 @@ def test_load_local_hub_with_revision( ) with patch( - "axolotl.utils.data.shared.load_dataset_w_config" + "axolotl.utils.data.shared.load_dataset_with_config" ) as mock_load_dataset: # Set up the mock to return different values on successive calls mock_load_dataset.return_value = ( dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff ) - dataset, _ = load_tokenized_prepared_datasets( - tokenizer, cfg, prepared_path - ) + with patch( + "axolotl.common.const.DEFAULT_DATASET_PREPARED_PATH", + str(prepared_path), + ): + dataset, _ = _load_tokenized_prepared_datasets(tokenizer, cfg) assert len(dataset) == 2000 assert "input_ids" in dataset.features @@ -437,7 +469,10 @@ def test_loading_local_dataset_folder(self, tokenizer): } ) - dataset, _ = load_tokenized_prepared_datasets(tokenizer, cfg, prepared_path) + with patch( + "axolotl.common.const.DEFAULT_DATASET_PREPARED_PATH", str(prepared_path) + ): + dataset, _ = _load_tokenized_prepared_datasets(tokenizer, cfg) assert len(dataset) == 2000 assert "input_ids" in dataset.features diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index 29672c9e5d..45a327a403 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -5,7 +5,6 @@ `deduplicate_and_log_datasets` during the execution of the preprocess command. """ -import hashlib import unittest from unittest.mock import patch @@ -14,8 +13,7 @@ from axolotl.loaders import load_processor, load_tokenizer from axolotl.utils.config import normalize_config, validate_config -from axolotl.utils.data import prepare_dataset -from axolotl.utils.data.rl import load_prepare_preference_datasets +from axolotl.utils.data import prepare_datasets, prepare_preference_datasets from axolotl.utils.data.utils import deduplicate_and_log_datasets from axolotl.utils.dict import DictDefault @@ -71,35 +69,13 @@ def setUp(self): self.expected_dataset = Dataset.from_dict(self.expected_data) def test_deduplication(self): - train_dataset, _, _ = deduplicate_and_log_datasets(train_dataset=self.dataset) - _, eval_dataset, _ = deduplicate_and_log_datasets(eval_dataset=self.dataset) - - verify_deduplication(train_dataset, self.expected_dataset, "train_dataset") - verify_deduplication(eval_dataset, self.expected_dataset, "eval_dataset") - - def test_datasets_are_none(self): - # Test when both datasets are None - train_dataset, eval_dataset, _ = deduplicate_and_log_datasets( - train_dataset=None, eval_dataset=None - ) - self.assertIsNone(train_dataset, "Expected train_dataset to be None") - self.assertIsNone(eval_dataset, "Expected eval_dataset to be None") - - def test_only_train_is_none(self): - # Test when only train_dataset is None - train_dataset, eval_dataset, _ = deduplicate_and_log_datasets( - train_dataset=None, eval_dataset=self.dataset + train_dataset, _ = deduplicate_and_log_datasets(dataset=self.dataset) + eval_dataset, _ = deduplicate_and_log_datasets( + dataset=self.dataset, dataset_name="eval" ) - self.assertIsNone(train_dataset, "Expected train_dataset to be None") - verify_deduplication(eval_dataset, self.expected_dataset, "eval_dataset") - def test_only_eval_is_none(self): - # Test when only eval_dataset is None - train_dataset, eval_dataset, _ = deduplicate_and_log_datasets( - train_dataset=self.dataset, eval_dataset=None - ) - self.assertIsNone(eval_dataset, "Expected eval_dataset to be None") verify_deduplication(train_dataset, self.expected_dataset, "train_dataset") + verify_deduplication(eval_dataset, self.expected_dataset, "eval_dataset") def test_exact_duplicates(self): # Test when datasets are exact duplicates @@ -115,8 +91,10 @@ def test_exact_duplicates(self): expected_dataset = Dataset.from_dict(expected_data) # Run deduplication - train_dataset, _, _ = deduplicate_and_log_datasets(train_dataset=dataset) - _, eval_dataset, _ = deduplicate_and_log_datasets(eval_dataset=dataset) + train_dataset, _ = deduplicate_and_log_datasets(dataset=dataset) + eval_dataset, _ = deduplicate_and_log_datasets( + dataset=dataset, dataset_name="eval" + ) verify_deduplication(train_dataset, expected_dataset, "train_dataset") verify_deduplication(eval_dataset, expected_dataset, "eval_dataset") @@ -139,8 +117,10 @@ def test_partial_duplicates(self): expected_dataset = Dataset.from_dict(expected_data) # Run deduplication - train_dataset, _, _ = deduplicate_and_log_datasets(train_dataset=dataset) - _, eval_dataset, _ = deduplicate_and_log_datasets(eval_dataset=dataset) + train_dataset, _ = deduplicate_and_log_datasets(dataset=dataset) + eval_dataset, _ = deduplicate_and_log_datasets( + dataset=dataset, dataset_name="eval" + ) verify_deduplication(train_dataset, expected_dataset, "train_dataset") verify_deduplication(eval_dataset, expected_dataset, "eval_dataset") @@ -169,8 +149,8 @@ def test_combined_duplicates_empty(self): expected_dataset_eval = Dataset.from_dict(expected_data_eval) # Run deduplication - train_dataset, eval_dataset, _ = deduplicate_and_log_datasets( - train_dataset=dataset, eval_dataset=dataset + train_dataset, eval_dataset = deduplicate_and_log_datasets( + dataset=dataset, other_dataset=dataset ) verify_deduplication(train_dataset, expected_dataset_train, "train_dataset") @@ -206,8 +186,8 @@ def test_combined_duplicates_one(self): expected_dataset_eval = Dataset.from_dict(expected_data_eval) # Run deduplication - train_dataset, eval_dataset, _ = deduplicate_and_log_datasets( - train_dataset=dataset_train, eval_dataset=dataset_eval + train_dataset, eval_dataset = deduplicate_and_log_datasets( + dataset=dataset_train, other_dataset=dataset_eval ) verify_deduplication(train_dataset, expected_dataset_train, "train_dataset") @@ -245,7 +225,9 @@ def test_load_with_deduplication( # pylint: disable=duplicate-code with ( - patch("axolotl.utils.data.rl.load_dataset_w_config") as mock_load_dataset, + patch( + "axolotl.utils.data.rl.load_dataset_with_config" + ) as mock_load_dataset, patch("axolotl.loaders.load_tokenizer") as mock_load_tokenizer, ): # Set up the mock to return different values on successive calls @@ -255,7 +237,8 @@ def test_load_with_deduplication( ] mock_load_tokenizer.return_value = tokenizer_huggyllama - train_dataset, _ = load_prepare_preference_datasets(cfg) + tokenizer = load_tokenizer(cfg) + train_dataset, _ = prepare_preference_datasets(cfg, tokenizer) # Verify that the dataset has been deduplicated assert len(train_dataset) == 1800, "Dataset was not properly deduplicated" @@ -269,7 +252,9 @@ def test_load_without_deduplication( ): # pylint: disable=duplicate-code with ( - patch("axolotl.utils.data.rl.load_dataset_w_config") as mock_load_dataset, + patch( + "axolotl.utils.data.rl.load_dataset_with_config" + ) as mock_load_dataset, patch("axolotl.loaders.load_tokenizer") as mock_load_tokenizer, ): # Set up the mock to return different values on successive calls @@ -279,9 +264,10 @@ def test_load_without_deduplication( ] mock_load_tokenizer.return_value = tokenizer_huggyllama - cfg.dataset_exact_deduplication = False # Load the dataset without deduplication - train_dataset, _ = load_prepare_preference_datasets(cfg) + cfg.dataset_exact_deduplication = False + tokenizer = load_tokenizer(cfg) + train_dataset, _ = prepare_preference_datasets(cfg, tokenizer) # Verify that the dataset retains duplicates assert ( @@ -335,7 +321,7 @@ def test_prepare_dataset_with_deduplication_train(self): ) # Prepare dataset using the prepare_dataset function - train_dataset, _, _, _ = prepare_dataset( + train_dataset, _, _, _ = prepare_datasets( self.cfg_1, tokenizer, processor=processor, @@ -362,7 +348,7 @@ def test_prepare_dataset_with_deduplication_eval(self): ) # Prepare dataset using the prepare_dataset function - _, eval_dataset, _, _ = prepare_dataset( + _, eval_dataset, _, _ = prepare_datasets( self.cfg_1, tokenizer, processor=processor, @@ -389,7 +375,7 @@ def test_prepare_dataset_without_deduplication(self): ) # Prepare dataset using the prepare_dataset function - train_dataset, eval_dataset, _, _ = prepare_dataset( + train_dataset, eval_dataset, _, _ = prepare_datasets( self.cfg_1, tokenizer, processor=processor, @@ -428,41 +414,8 @@ def setUp(self): self.eval_dataset = Dataset.from_dict(self.eval_data) self.dataset = Dataset.from_dict(self.dataset_data) - @patch( - "axolotl.utils.data.utils.sha256", - side_effect=lambda x: ( - hashlib.sha256("forced_collision_hash".encode("utf-8")).hexdigest() - if "sample 5" in x - else hashlib.sha256(x.encode("utf-8")).hexdigest() - ), - ) - def test_deduplication_wrong_collision_train_eval(self, _mock_sha256): - dedup_train, dedup_eval, _ = deduplicate_and_log_datasets( - train_dataset=self.train_dataset, eval_dataset=self.eval_dataset - ) - self.assertEqual( - len(dedup_train), - 2, - "train dataset should not deduplicate rows with forced hash collisions but different labels.", - ) - self.assertEqual( - len(dedup_eval), - 2, - "Eval dataset should not deduplicate rows with forced hash collisions but different labels.", - ) - self.assertEqual( - len(dedup_eval), - len(self.eval_dataset), - "The output eval dataset should have the same number of rows as the input eval dataset.", - ) - self.assertEqual( - str(dedup_eval), - str(self.eval_dataset), - "The string representation of the output eval dataset should be identical to the input eval dataset.", - ) - def test_deduplication_dataset_only(self): - _, _, dedup_dataset = deduplicate_and_log_datasets(dataset=self.dataset) + dedup_dataset, _ = deduplicate_and_log_datasets(dataset=self.dataset) self.assertEqual( len(dedup_dataset), 3, "Dataset should have all original values" ) From 581dd324cc7fee332d0cfe906a3e8665fdac0034 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 11 Jun 2025 17:11:06 -0400 Subject: [PATCH 0707/1405] build base images for torch 2.7.1 (#2764) * build base images for torch 2.7.1 * fix: update base docker to use torch 2.7.1 * fix: update doc for main base to use 2.7.1 * make sure to install fa2 in base uv too * use no build isolation for uv+flashattn * install psutil also for fa2 * longer timeout for flash attn build --------- Co-authored-by: NanoCode012 --- .github/workflows/base.yml | 8 +++++--- docker/Dockerfile-base | 2 +- docker/Dockerfile-base-next | 2 +- docker/Dockerfile-uv-base | 6 +++++- docs/docker.qmd | 6 +++--- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 6b750fc5a5..966bd2f5b5 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -16,6 +16,7 @@ on: jobs: build-base: if: github.repository_owner == 'axolotl-ai-cloud' + timeout-minutes: 480 # this job needs to be run on self-hosted GPU runners... runs-on: ubuntu-latest-m strategy: @@ -47,14 +48,14 @@ jobs: cuda_version: 12.6.3 cudnn_version: "" python_version: "3.11" - pytorch: 2.7.0 + pytorch: 2.7.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" - cuda: "128" cuda_version: 12.6.3 cudnn_version: "" python_version: "3.11" - pytorch: 2.7.0 + pytorch: 2.7.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" - cuda: "128" @@ -106,6 +107,7 @@ jobs: TORCH_CUDA_ARCH_LIST=${{ matrix.torch_cuda_arch_list }} build-base-uv: if: github.repository_owner == 'axolotl-ai-cloud' + timeout-minutes: 480 runs-on: ubuntu-latest-m strategy: fail-fast: false @@ -122,7 +124,7 @@ jobs: cuda_version: 12.8.1 cudnn_version: "" python_version: "3.11" - pytorch: 2.7.0 + pytorch: 2.7.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" steps: diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index cf1af96829..cc9ca2f2d5 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -38,6 +38,6 @@ RUN git lfs install --skip-repo && \ # The base image ships with `pydantic==1.8.2` which is not working pip3 install -U --no-cache-dir pydantic==1.10.10 -RUN if [ "$PYTORCH_VERSION" = "2.7.0" ] ; then \ +RUN if [ "$PYTORCH_VERSION" = "2.7.1" ] ; then \ pip3 install flash-attn==2.7.4.post1; \ fi diff --git a/docker/Dockerfile-base-next b/docker/Dockerfile-base-next index a968b5913c..85bac25165 100644 --- a/docker/Dockerfile-base-next +++ b/docker/Dockerfile-base-next @@ -29,7 +29,7 @@ ENV PATH="/root/miniconda3/envs/py${PYTHON_VERSION}/bin:${PATH}" WORKDIR /workspace RUN python3 -m pip install --upgrade pip && pip3 install packaging && \ - python3 -m pip install --no-cache-dir -U torch==2.7.0 --extra-index-url https://download.pytorch.org/whl/test/cu$CUDA && \ + python3 -m pip install --no-cache-dir -U torch==2.7.1 --extra-index-url https://download.pytorch.org/whl/test/cu$CUDA && \ python3 -m pip install --no-cache-dir "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" && \ python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index 5ac8d86c7d..c612278aec 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -29,8 +29,12 @@ RUN uv venv --no-project --relocatable axolotl-venv ENV PATH="/workspace/axolotl-venv/bin:${PATH}" -RUN uv pip install packaging setuptools wheel \ +RUN uv pip install packaging setuptools wheel psutil \ && uv pip install torch==${PYTORCH_VERSION} \ && uv pip install --no-build-isolation "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" \ && uv pip install "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" \ && uv pip install awscli pydantic + +RUN if [ "$PYTORCH_VERSION" = "2.7.1" ] ; then \ + uv pip install --no-build-isolation flash-attn==2.7.4.post1; \ + fi diff --git a/docs/docker.qmd b/docs/docker.qmd index 7b236b9604..bc26a795f2 100644 --- a/docs/docker.qmd +++ b/docs/docker.qmd @@ -9,7 +9,7 @@ format: This section describes the different Docker images that are released by AxolotlAI at [Docker Hub](https://hub.docker.com/u/axolotlai). ::: {.callout-important} -For Blackwell GPUs, please use the tags with Pytorch 2.7.0 and CUDA 12.8. +For Blackwell GPUs, please use the tags with Pytorch 2.7.1 and CUDA 12.8. ::: ## Base @@ -32,8 +32,8 @@ main-base-py{python_version}-cu{cuda_version}-{pytorch_version} Tags examples: -- `main-base-py3.11-cu128-2.7.0` -- `main-base-py3.11-cu126-2.7.0` +- `main-base-py3.11-cu128-2.7.1` +- `main-base-py3.11-cu126-2.7.1` - `main-base-py3.11-cu124-2.6.0` - `main-base-py3.11-cu124-2.5.1` From bcc108efc1a23b3f4552d4e7b2fbfe029fe53af6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 12 Jun 2025 13:22:20 -0400 Subject: [PATCH 0708/1405] build 2.7.1 images too (#2784) [skip ci] --- .github/workflows/main.yml | 8 ++++---- .github/workflows/multi-gpu-e2e.yml | 2 +- .github/workflows/tests.yml | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 01606f9020..7ff7127574 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,12 +29,12 @@ jobs: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" - pytorch: 2.7.0 + pytorch: 2.7.1 axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.0 + pytorch: 2.7.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: @@ -97,12 +97,12 @@ jobs: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" - pytorch: 2.7.0 + pytorch: 2.7.1 axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.0 + pytorch: 2.7.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 0167df67a3..deea0ed299 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -43,7 +43,7 @@ jobs: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" - pytorch: 2.7.0 + pytorch: 2.7.1 axolotl_extras: num_gpus: 2 nightly_build: "true" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ddbd252917..11fe13713a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,7 +52,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.5.1", "2.6.0", "2.7.0"] + pytorch_version: ["2.5.1", "2.6.0", "2.7.1"] timeout-minutes: 20 steps: @@ -125,7 +125,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.5.1", "2.6.0", "2.7.0"] + pytorch_version: ["2.5.1", "2.6.0", "2.7.1"] timeout-minutes: 20 steps: @@ -262,13 +262,13 @@ jobs: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" - pytorch: 2.7.0 + pytorch: 2.7.1 num_gpus: 1 axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.0 + pytorch: 2.7.1 num_gpus: 1 axolotl_extras: steps: From 3634d8ff9d929a44127fc68de87f09118eee0ed0 Mon Sep 17 00:00:00 2001 From: salman Date: Thu, 12 Jun 2025 10:22:40 -0700 Subject: [PATCH 0709/1405] QAT docfix (#2778) [skip ci] * nits * Update docs/qat.qmd Co-authored-by: NanoCode012 --------- Co-authored-by: NanoCode012 --- docs/qat.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/qat.qmd b/docs/qat.qmd index 0531388de6..e0d000a794 100644 --- a/docs/qat.qmd +++ b/docs/qat.qmd @@ -29,4 +29,4 @@ qat: fake_quant_after_n_steps: # Optional[int] = None. The number of steps to apply fake quantization after ``` -Once you have finished training, you must quantize your model by using the same quantization configuration which you used to train the model with. You can use the [`quantize` command](./quantize.md) to do this. +Once you have finished training, you must quantize your model by using the same quantization configuration which you used to train the model with. You can use the [`quantize`](./quantize.qmd) command to do this. From 468580d18efe5ec55c050e72b3fc3b30ad390641 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 12 Jun 2025 13:22:58 -0400 Subject: [PATCH 0710/1405] limit multipack sampler processes (#2771) [skip ci] * limit to 16 packing processes * make num_processes properly reflect configured dataset_processes --- src/axolotl/core/builders/base.py | 3 +++ src/axolotl/core/builders/rl.py | 4 ---- src/axolotl/core/trainers/base.py | 1 + src/axolotl/core/training_args.py | 4 ++++ src/axolotl/utils/samplers/multipack.py | 7 +++++-- src/axolotl/utils/trainer.py | 1 + 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 907de056b3..ac49b4e88a 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -490,6 +490,9 @@ def _set_base_training_args( training_args_kwargs["max_steps"] = self.cfg.max_steps or total_num_steps or -1 training_args_kwargs["num_train_epochs"] = self.cfg.num_epochs + if self.cfg.dataset_processes: + training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes + # max_length is not used in CausalTrainer if self.cfg.reward_model or self.cfg.rl: training_args_kwargs["max_length"] = self.cfg.sequence_len diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index 14dbfa715b..80c5a9eefe 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -90,10 +90,6 @@ def _build_training_arguments(self, total_num_steps): else: training_args_kwargs["remove_unused_columns"] = False - # only rlhf - if self.cfg.dataset_processes: - training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes - if self.cfg.trl and self.cfg.trl.beta is not None: training_args_kwargs["beta"] = self.cfg.trl.beta elif self.cfg.rl_beta is not None: diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index d6f2c579ae..25ffb4cbf7 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -114,6 +114,7 @@ def _create_multipack_sampler( bin_size=self.args.sample_packing_bin_size, sequential=self.args.sample_packing_sequentially, drop_last=True, + num_processes=self.args.dataset_num_proc, ) def _get_train_sampler( diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index 42488e643d..2b53c67986 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -68,6 +68,10 @@ class AxolotlTrainingMixins: default=2048, metadata={"help": "The maximum sequence length the model can handle"}, ) + dataset_num_proc: int | None = field( + default=None, + metadata={"help": "The number of processes to use for data processing"}, + ) relora_steps: Optional[int] = field( default=None, metadata={"help": "how often to reset for ReLoRA"}, diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index e488ed7d5c..eabfc2d849 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -3,6 +3,7 @@ into fixed-capacity batches to optimize memory usage and training throughput. """ +import gc import math from concurrent.futures import ProcessPoolExecutor from multiprocessing import cpu_count, get_context @@ -145,7 +146,7 @@ def pack_parallel( """ num_items = len(sequence_lengths) if num_processes is None: - num_processes = max(1, min(num_items // group_size, cpu_count())) + num_processes = max(1, min(num_items // group_size, cpu_count(), 16)) # Create tasks for parallel processing tasks = [] @@ -259,7 +260,7 @@ def __init__( lengths: np.ndarray, # Sequence lengths packing_efficiency_estimate: float = 1.0, # Initial efficiency estimate drop_last: bool = False, # Whether to drop final batches (might be incomplete) - num_count_samples: int = 16, # Number of times to estimate batch count + num_count_samples: int = 8, # Number of times to estimate batch count sequential: bool = False, # Whether to use sequential packing group_size: int = 100_000, # Size of groups for parallel packing bin_size: int = 200, # The max number of samples that can be packed in a single bin @@ -349,6 +350,7 @@ def generate_batches(self, set_stats: bool = False) -> list[list[list[int]]]: # Calculate efficiency statistics total_used = lengths.sum() total_slots = len(all_bins) * self.batch_max_len + del all_bins # Group bins into batches (each batch contains batch_size bins) batches = [ @@ -368,6 +370,7 @@ def generate_batches(self, set_stats: bool = False) -> list[list[list[int]]]: self.total_token_slots += total_slots self._batches = batches + gc.collect() return batches def __iter__(self) -> Iterator[list[list[int]]]: diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 67f590a377..ec5360fa33 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -467,6 +467,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): bin_size=cfg.sample_packing_bin_size, sequential=cfg.sample_packing_sequentially, drop_last=True, + num_processes=cfg.dataset_processes, ) data_loader = DataLoader( From 706c677cad094a64575c9de3ec87b4974af99deb Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 12 Jun 2025 10:23:18 -0700 Subject: [PATCH 0711/1405] feat(doc): update readme to include changelog and remove matrix (#2775) [skip ci] * feat(doc): update readme to include changelog and remove matrix * chore: improve wording * chore: wording * Update README.md Co-authored-by: salman * Update README.md Co-authored-by: salman * Update README.md Co-authored-by: salman * Update README.md Co-authored-by: salman * chore: address comment remove muon * chore: address comments * fix: address final comments --------- Co-authored-by: salman --- README.md | 73 +++++++++++++++++-------------------------------------- 1 file changed, 22 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index d5e8f08a1c..f7765c4753 100644 --- a/README.md +++ b/README.md @@ -22,28 +22,31 @@ multigpu-semi-weekly tests

-Axolotl is a tool designed to streamline post-training for various AI models. -Post-training refers to any modifications or additional training performed on -pre-trained models - including full model fine-tuning, parameter-efficient tuning (like -LoRA and QLoRA), supervised fine-tuning (SFT), instruction tuning, and alignment -techniques. With support for multiple model architectures and training configurations, -Axolotl makes it easy to get started with these techniques. -Axolotl is designed to work with YAML config files that contain everything you need to -preprocess a dataset, train or fine-tune a model, run model inference or evaluation, -and much more. +## 🎉 Latest Updates + +- 2025/05: Quantization Aware Training (QAT) support has been added to Axolotl. Explore the [docs](https://docs.axolotl.ai/docs/qat.html) to learn more! +- 2025/04: Llama 4 support has been added in Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/llama-4) to start training your own Llama 4 models with Axolotl's linearized version! +- 2025/03: Axolotl has implemented Sequence Parallelism (SP) support. Read the [blog](https://huggingface.co/blog/axolotl-ai-co/long-context-with-sequence-parallelism-in-axolotl) and [docs](https://docs.axolotl.ai/docs/sequence_parallelism.html) to learn how to scale your context length when fine-tuning. +- 2025/03: (Beta) Fine-tuning Multimodal models is now supported in Axolotl. Check out the [docs](https://docs.axolotl.ai/docs/multimodal.html) to fine-tune your own! +- 2025/02: Axolotl has added LoRA optimizations to reduce memory usage and improve training speed for LoRA and QLoRA in single GPU and multi-GPU training (DDP and DeepSpeed). Jump into the [docs](https://docs.axolotl.ai/docs/lora_optims.html) to give it a try. +- 2025/02: Axolotl has added GRPO support. Dive into our [blog](https://huggingface.co/blog/axolotl-ai-co/training-llms-w-interpreter-feedback-wasm) and [GRPO example](https://github.com/axolotl-ai-cloud/grpo_code) and have some fun! +- 2025/01: Axolotl has added Reward Modelling / Process Reward Modelling fine-tuning support. See [docs](https://docs.axolotl.ai/docs/reward_modelling.html). + +## ✨ Overview + +Axolotl is a tool designed to streamline post-training for various AI models. Features: -- Train various Huggingface models such as llama, pythia, falcon, mpt -- Supports fullfinetune, lora, qlora, relora, and gptq -- Customize configurations using a simple yaml file or CLI overwrite -- Load different dataset formats, use custom formats, or bring your own tokenized datasets -- Integrated with [xformers](https://github.com/facebookresearch/xformers), flash attention, [liger kernel](https://github.com/linkedin/Liger-Kernel), rope scaling, and multipacking -- Works with single GPU or multiple GPUs via FSDP or Deepspeed -- Easily run with Docker locally or on the cloud -- Log results and optionally checkpoints to wandb, mlflow or Comet -- And more! +- **Multiple Model Support**: Train various models like LLaMA, Mistral, Mixtral, Pythia, and more. We are compatible with HuggingFace transformers causal language models. +- **Training Methods**: Full fine-tuning, LoRA, QLoRA, GPTQ, QAT, Preference Tuning (DPO, IPO, KTO, ORPO), RL (GRPO), Multimodal, and Reward Modelling (RM) / Process Reward Modelling (PRM). +- **Easy Configuration**: Re-use a single YAML file between dataset preprocess, training, evaluation, quantization, and inference. +- **Performance Optimizations**: [Multipacking](https://docs.axolotl.ai/docs/multipack.html), [Flash Attention](https://github.com/Dao-AILab/flash-attention), [Xformers](https://github.com/facebookresearch/xformers), [Flex Attention](https://pytorch.org/blog/flexattention/), [Liger Kernel](https://github.com/linkedin/Liger-Kernel), [Cut Cross Entropy](https://github.com/apple/ml-cross-entropy/tree/main), Sequence Parallelism (SP), LoRA optimizations, Multi-GPU training (FSDP1, FSDP2, DeepSpeed), Multi-node training (Torchrun, Ray), and many more! +- **Flexible Dataset Handling**: Load from local, HuggingFace, and cloud (S3, Azure, GCP, OCI) datasets. +- **Cloud Ready**: We ship [Docker images](https://hub.docker.com/u/axolotlai) and also [PyPI packages](https://pypi.org/project/axolotl/) for use on cloud platforms and local hardware. + + ## 🚀 Quick Start @@ -81,19 +84,12 @@ axolotl train examples/llama-3/lora-1b.yml That's it! Check out our [Getting Started Guide](https://docs.axolotl.ai/docs/getting-started.html) for a more detailed walkthrough. -## ✨ Key Features - -- **Multiple Model Support**: Train various models like LLaMA, Mistral, Mixtral, Pythia, and more -- **Training Methods**: Full fine-tuning, LoRA, QLoRA, and more -- **Easy Configuration**: Simple YAML files to control your training setup -- **Performance Optimizations**: Flash Attention, xformers, multi-GPU training -- **Flexible Dataset Handling**: Use various formats and custom datasets -- **Cloud Ready**: Run on cloud platforms or local hardware ## 📚 Documentation - [Installation Options](https://docs.axolotl.ai/docs/installation.html) - Detailed setup instructions for different environments - [Configuration Guide](https://docs.axolotl.ai/docs/config.html) - Full configuration options and examples +- [Dataset Loading](https://docs.axolotl.ai/docs/dataset_loading.html) - Loading datasets from various sources - [Dataset Guide](https://docs.axolotl.ai/docs/dataset-formats/) - Supported formats and how to use them - [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) - [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) @@ -112,31 +108,6 @@ That's it! Check out our [Getting Started Guide](https://docs.axolotl.ai/docs/ge Contributions are welcome! Please see our [Contributing Guide](https://github.com/axolotl-ai-cloud/axolotl/blob/main/.github/CONTRIBUTING.md) for details. -## Supported Models - -| | fp16/fp32 | lora | qlora | gptq | gptq w/flash attn | flash attn | xformers attn | -|-------------|:----------|:-----|-------|------|-------------------|------------|--------------| -| llama | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Mistral | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Mixtral-MoE | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | -| Mixtral8X22 | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | -| Pythia | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❓ | -| cerebras | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❓ | -| btlm | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❓ | -| mpt | ✅ | ❌ | ❓ | ❌ | ❌ | ❌ | ❓ | -| falcon | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❓ | -| gpt-j | ✅ | ✅ | ✅ | ❌ | ❌ | ❓ | ❓ | -| XGen | ✅ | ❓ | ✅ | ❓ | ❓ | ❓ | ✅ | -| phi | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | -| RWKV | ✅ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | -| Qwen | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | ❓ | -| Gemma | ✅ | ✅ | ✅ | ❓ | ❓ | ✅ | ❓ | -| Jamba | ✅ | ✅ | ✅ | ❓ | ❓ | ✅ | ❓ | - -✅: supported -❌: not supported -❓: untested - ## ❤️ Sponsors Thank you to our sponsors who help make Axolotl possible: From f5fbc82f2bd65b9f19d8b7cb2b3702ef80da9714 Mon Sep 17 00:00:00 2001 From: JZacaroli <32681029+JZacaroli@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:23:31 +0100 Subject: [PATCH 0712/1405] Fix logging import in evaluate.py (#2782) (#2783) * Fix logging import in evaluate.py (#2782) * chore: lint --------- Co-authored-by: Joe Zacaroli Co-authored-by: Wing Lian --- src/axolotl/evaluate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/evaluate.py b/src/axolotl/evaluate.py index 6d68137308..2b58699396 100644 --- a/src/axolotl/evaluate.py +++ b/src/axolotl/evaluate.py @@ -7,7 +7,6 @@ from typing import Dict, Optional import torch -from accelerate.logging import get_logger from datasets import Dataset from transformers.trainer import Trainer @@ -17,6 +16,7 @@ ) from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import cleanup_distributed +from axolotl.utils.logging import get_logger from axolotl.utils.trainer import setup_trainer project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) From ace9287c96217179ad8be8db10cc51f0e4922db7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 12 Jun 2025 18:06:14 -0400 Subject: [PATCH 0713/1405] update loss value for flakey e2e test (#2786) [skip ci] * update loss value for flakey e2e test * use pytest skip * parametrize combinations --- tests/e2e/test_llama_pretrain.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index 47d4b48395..6944c6f5ef 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -14,17 +14,14 @@ class TestPretrainLlama: """Test case for Llama models w pretraining""" @pytest.mark.parametrize( - "sample_packing", - [True, False], - ) - @pytest.mark.parametrize( - "pretrain_multipack_attn", - [True, False], + ("sample_packing", "pretrain_multipack_attn"), + [ + (False, False), + (True, True), + (True, False), + ], ) def test_pretrain(self, temp_dir, sample_packing, pretrain_multipack_attn): - if not sample_packing and pretrain_multipack_attn: - return - # pylint: disable=duplicate-code cfg = DictDefault( { @@ -65,7 +62,7 @@ def test_pretrain(self, temp_dir, sample_packing, pretrain_multipack_attn): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) - loss_threshold = 3.5 + loss_threshold = 3.6 if sample_packing and not pretrain_multipack_attn: loss_threshold = 6.5 check_tensorboard( From eac4a61f55a9d92dd7e4c1df05126208d86569f4 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 12 Jun 2025 16:18:33 -0700 Subject: [PATCH 0714/1405] Feat: Add Magistral and mistral-common tokenizer support (#2780) --- README.md | 1 + docs/config.qmd | 2 + examples/magistral/README.md | 72 ++- examples/magistral/magistral-small-qlora.yaml | 63 ++ requirements.txt | 2 + src/axolotl/datasets.py | 7 + src/axolotl/integrations/kd/chat_template.py | 2 +- src/axolotl/loaders/tokenizer.py | 13 + .../prompt_strategies/chat_template.py | 129 +++- src/axolotl/prompt_tokenizers.py | 8 + src/axolotl/utils/mistral_tokenizer.py | 567 ++++++++++++++++++ src/axolotl/utils/schemas/config.py | 62 ++ src/axolotl/utils/schemas/model.py | 1 + tests/prompt_strategies/conftest.py | 8 + .../test_chat_templates_mistral.py | 290 +++++++++ 15 files changed, 1213 insertions(+), 14 deletions(-) create mode 100644 examples/magistral/magistral-small-qlora.yaml create mode 100644 src/axolotl/utils/mistral_tokenizer.py create mode 100644 tests/prompt_strategies/test_chat_templates_mistral.py diff --git a/README.md b/README.md index f7765c4753..ef55238984 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ ## 🎉 Latest Updates +- 2025/06: Magistral with mistral-common tokenizer support has been added to Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral) to start training your own Magistral models with Axolotl! - 2025/05: Quantization Aware Training (QAT) support has been added to Axolotl. Explore the [docs](https://docs.axolotl.ai/docs/qat.html) to learn more! - 2025/04: Llama 4 support has been added in Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/llama-4) to start training your own Llama 4 models with Axolotl's linearized version! - 2025/03: Axolotl has implemented Sequence Parallelism (SP) support. Read the [blog](https://huggingface.co/blog/axolotl-ai-co/long-context-with-sequence-parallelism-in-axolotl) and [docs](https://docs.axolotl.ai/docs/sequence_parallelism.html) to learn how to scale your context length when fine-tuning. diff --git a/docs/config.qmd b/docs/config.qmd index 2ca2367082..d146b4c841 100644 --- a/docs/config.qmd +++ b/docs/config.qmd @@ -27,6 +27,8 @@ trust_remote_code: tokenizer_use_fast: # Whether to use the legacy tokenizer setting, defaults to True tokenizer_legacy: +# Whether to use mistral-common tokenizer. If set to True, it will use the mistral-common tokenizer. +tokenizer_use_mistral_common: # Resize the model embeddings when new tokens are added to multiples of 32 # This is reported to improve training speed on some models resize_token_embeddings_to_32x: diff --git a/examples/magistral/README.md b/examples/magistral/README.md index 172d9ac933..a2b09ab700 100644 --- a/examples/magistral/README.md +++ b/examples/magistral/README.md @@ -1,3 +1,71 @@ -# Coming Soon! +# Finetune Magistral Small with Axolotl -Watch this space for configs for fine-tuning [Magistral Small 2506](https://huggingface.co/mistralai/Magistral-Small-2506). +Magistral Small is a 24B parameter opensource model from MistralAI found on [HuggingFace](https://huggingface.co/mistralai/Magistral-Small-2506). This guide shows how to fine-tune it with Axolotl with multi-turn conversations with proper masking. + +MistralAI has also released a proprietary medium-sized version called Magistral Medium. + +Thanks to the team at MistralAI for giving us early access to prepare for this release. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Magistral is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). + + Here is an example of how to install from main for pip: + +```bash +# Ensure you have Pytorch installed (Pytorch 2.6.0 recommended) +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl + +pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install --no-build-isolation -e '.[flash-attn,mistral]' +``` + +2. Download the example config: + +```bash +axolotl fetch examples +``` + +3. Run the finetuning example: + +```bash +axolotl train examples/magistral/magistral-small-qlora.yaml +``` + +This config uses about 24GB VRAM. + +Let us know how it goes. Happy finetuning! 🚀 + +### TIPS + +- For inference, the official MistralAI team recommends `top_p: 0.95` and `temperature: 0.7` with `max_tokens: 40960`. +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format is the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) + +## Limitations + +We only support the `mistral-common` tokenizer for Supervised Fine-tuning at the moment and for `type: chat_template` only. + +The tokenizer does not work with `dataset.map` with multiprocessing, so we had to disable it. In addition, we do not support overriding tokens yet. + +## Related Resources + +- [MistralAI Magistral Blog](https://mistral.ai/news/magistral/) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) + + +## Future Work + +- Add parity to Preference Tuning, RL, Multi-modal, etc. +- Add parity to other tokenizer configs like overriding tokens. diff --git a/examples/magistral/magistral-small-qlora.yaml b/examples/magistral/magistral-small-qlora.yaml new file mode 100644 index 0000000000..e3e746f224 --- /dev/null +++ b/examples/magistral/magistral-small-qlora.yaml @@ -0,0 +1,63 @@ +base_model: mistralai/Magistral-Small-2506 + +# Enable to use mistral-common tokenizer +tokenizer_use_mistral_common: true + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true +pad_to_sequence_len: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 diff --git a/requirements.txt b/requirements.txt index 3af94421d5..cf8caba002 100644 --- a/requirements.txt +++ b/requirements.txt @@ -67,3 +67,5 @@ schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.6 axolotl-contribs-mit==0.0.3 + +mistral-common==1.6.0 diff --git a/src/axolotl/datasets.py b/src/axolotl/datasets.py index 7c112c59e7..28182b16f5 100644 --- a/src/axolotl/datasets.py +++ b/src/axolotl/datasets.py @@ -48,6 +48,13 @@ def process(self, dataset): features = dataset.features.keys() num_proc = min(64, self.process_count if self.process_count else os.cpu_count()) + # Disable multiprocessing if the tokenizer doesn't support it (e.g., mistral_common) + if not getattr(self.prompt_tokenizer, "supports_multiprocessing", True): + LOG.info( + "Disabling multiprocessing for tokenizer as it doesn't support it (e.g., mistral_common)" + ) + num_proc = 1 + map_kwargs = {} if self.prompt_tokenizer.supports_batched: map_kwargs["batched"] = True diff --git a/src/axolotl/integrations/kd/chat_template.py b/src/axolotl/integrations/kd/chat_template.py index eb067cd04e..7c99a9c3d7 100644 --- a/src/axolotl/integrations/kd/chat_template.py +++ b/src/axolotl/integrations/kd/chat_template.py @@ -189,7 +189,7 @@ class KDStrategyLoader(StrategyLoader): Load ChatTemplateStrategy with KD support using StrategyLoader. """ - def _get_strategy_cls(self): + def _get_strategy_cls(self, cfg): # pylint: disable=unused-argument return ChatTemplateStrategyWithKD def _get_strategy_params(self, cfg, ds_cfg: Dict[str, Any]): diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index 5a174186d9..4f9a60a695 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -121,6 +121,19 @@ def modify_tokenizer_files( def load_tokenizer(cfg: DictDefault) -> PreTrainedTokenizer: """Load and configure the tokenizer based on the provided config.""" + + def _load_mistral_common_tokenizer(cfg: DictDefault): + """Load mistral-common tokenizer""" + from axolotl.utils.mistral_tokenizer import HFMistralTokenizer + + # Load the HF-compatible wrapper around MistralTokenizer + tokenizer = HFMistralTokenizer.from_pretrained(cfg.tokenizer_config) + + return tokenizer + + if cfg.tokenizer_use_mistral_common: + return _load_mistral_common_tokenizer(cfg) + model_config = load_model_config(cfg) tokenizer_kwargs = {} use_fast = True # this is the default diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 1fee0f7f60..4a358928e9 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -2,8 +2,10 @@ HF Chat Templates prompt strategy """ +# pylint: disable=too-many-lines + from collections import defaultdict -from typing import Any, Dict, List, Set, Union +from typing import TYPE_CHECKING, Any, Dict, List, Set, Union from pydantic import BaseModel from transformers import ProcessorMixin @@ -15,6 +17,9 @@ from axolotl.utils.logging import get_logger from axolotl.utils.schemas.datasets import DatasetConfig +if TYPE_CHECKING: + from axolotl.utils.mistral_tokenizer import HFMistralTokenizer + # Configure the logger LOG = get_logger(__name__) LOG.setLevel("INFO") @@ -81,7 +86,7 @@ def chat_template_msg_variables(self) -> Set[str]: def build_prompt( self, - conversation, + conversation: list[dict], add_generation_prompt=False, images=None, tools=None, @@ -271,9 +276,15 @@ def __init__( self.train_on_eot = train_on_eot if train_on_eot is not None else train_on_eos # Default to eos_token if eot_tokens not provided - self.eot_tokens = ( - eot_tokens if eot_tokens is not None else [self.tokenizer.eos_token] - ) + self.eot_tokens = [] + if eot_tokens is not None: + self.eot_tokens = eot_tokens + elif ( + hasattr(self.tokenizer, "eos_token") + and self.tokenizer.eos_token is not None + ): + self.eot_tokens = [self.tokenizer.eos_token] + self.split_thinking = split_thinking self.images = "images" @@ -796,14 +807,104 @@ def _get_messages(self, prompt): ) +class MistralStrategy(ChatTemplateStrategy): + """ + Mistral strategy for chat template. + """ + + def __init__( + self, + prompter: "ChatTemplatePrompter", + tokenizer: "HFMistralTokenizer", + train_on_inputs: bool, + sequence_len: int, + roles_to_train: list[str] | None = None, + train_on_eos: str | None = None, + train_on_eot: str | None = None, + eot_tokens: list[str] | None = None, + split_thinking: bool | None = False, + ): + # Call the parent's parent __init__ (PromptTokenizingStrategy) to skip ChatTemplateStrategy's validation + # pylint: disable=non-parent-init-called,super-init-not-called + PromptTokenizingStrategy.__init__( + self, prompter, tokenizer, train_on_inputs, sequence_len + ) + self.prompter: ChatTemplatePrompter = prompter + + self.roles_to_train = [] + if roles_to_train: + # map roles if exist in prompter.roles else use the role as is + self.roles_to_train = [ + prompter.roles.get(role, role) for role in roles_to_train + ] + + self.train_on_eos = train_on_eos + # Backward compatibility, load from train_on_eos + self.train_on_eot = train_on_eot if train_on_eot is not None else train_on_eos + + # Default to eos_token if eot_tokens not provided + self.eot_tokens = [] + if eot_tokens is not None: + self.eot_tokens = eot_tokens + else: + # set eot_tokens to the eos_token + self.eot_tokens = [self.tokenizer.eos_token] + + self.split_thinking = split_thinking + + self.images = "images" + + LOG.debug( + f"The chat template uses the following properites on the message: {self.prompter.chat_template_msg_variables}" + ) + + # Skip the validation that ChatTemplateStrategy calls + # TODO: address this in the future with mistral-specific checks + # self._validate_eot_and_eos_tokens() + + @property + def supports_multiprocessing(self) -> bool: + """ + Whether this tokenizing strategy supports multiprocessing. + mistral_common tokenizers cannot be pickled for multiprocessing. + """ + + return False + + def find_first_eot_token(self, input_ids, start_idx): + """Find the first EOT token in the input_ids starting from start_idx.""" + # mistral-common tokenizer does not support eot_tokens + return self.find_first_eos_token(input_ids, start_idx) + + +class MistralPrompter(ChatTemplatePrompter): + """ + Mistral prompter for chat template. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._chat_template_msg_variables = set(["tool_call_id", "name", "tool_calls"]) + + class StrategyLoader: """ Load chat template strategy based on configuration. """ - def _get_strategy_cls(self): + def _get_strategy_cls(self, cfg): + if cfg.tokenizer_use_mistral_common: + return MistralStrategy + return ChatTemplateStrategy + def _get_prompter_cls(self, cfg): + if cfg.tokenizer_use_mistral_common: + return MistralPrompter + + return ChatTemplatePrompter + def _get_strategy_params(self, cfg, ds_cfg: Dict[str, Any]): return { "train_on_inputs": cfg.train_on_inputs, @@ -829,9 +930,14 @@ def __call__( else: dataset_config = ds_cfg - chat_template_string = get_chat_template_from_config( - cfg=cfg, ds_cfg=dataset_config, tokenizer=tokenizer - ) + if cfg.tokenizer_use_mistral_common: + # mistral-common does not use this, so we pass an empty string + chat_template_string = "" + else: + chat_template_string = get_chat_template_from_config( + cfg=cfg, ds_cfg=dataset_config, tokenizer=tokenizer + ) + LOG.info(f"Using chat template:\n---\n{chat_template_string!s}\n---") prompter_params = { @@ -857,10 +963,11 @@ def __call__( } strategy_params = self._get_strategy_params(cfg, dataset_config) - strategy_cls = self._get_strategy_cls() + strategy_cls = self._get_strategy_cls(cfg) + prompter_cls = self._get_prompter_cls(cfg) strategy = strategy_cls( - ChatTemplatePrompter(**prompter_params), + prompter_cls(**prompter_params), tokenizer=tokenizer, **strategy_params, ) diff --git a/src/axolotl/prompt_tokenizers.py b/src/axolotl/prompt_tokenizers.py index 9ca645de3c..aae778ae8a 100644 --- a/src/axolotl/prompt_tokenizers.py +++ b/src/axolotl/prompt_tokenizers.py @@ -70,6 +70,14 @@ def tokenize_prompt(self, prompt): def supports_batched(self): return False + @property + def supports_multiprocessing(self): + """ + Whether this tokenizing strategy supports multiprocessing. + Should return False if the tokenizer has unpicklable objects. + """ + return True + def _tokenize( self, prompt: str, add_eos_token: bool = True, strip_bos_token: bool = False ) -> BatchEncoding: diff --git a/src/axolotl/utils/mistral_tokenizer.py b/src/axolotl/utils/mistral_tokenizer.py new file mode 100644 index 0000000000..3ccf39bb0f --- /dev/null +++ b/src/axolotl/utils/mistral_tokenizer.py @@ -0,0 +1,567 @@ +"""Wrapper for MistralTokenizer from mistral-common""" + +import math +import os +from shutil import copyfile +from typing import TYPE_CHECKING, Optional + +import numpy as np +from huggingface_hub import hf_hub_download +from mistral_common.tokens.tokenizers.mistral import MistralTokenizer +from mistral_common.tokens.tokenizers.tekken import Tekkenizer +from torch import Tensor +from transformers.utils import PaddingStrategy + +from axolotl.utils.collators.core import IGNORE_INDEX + +if TYPE_CHECKING: + from mistral_common.protocol.instruct.request import ChatCompletionRequest + + +def _get_file_path(path_or_repo_id: str, filename: str) -> str: + """Get the file path from local or HF Hub""" + if os.path.exists(path_or_repo_id): + maybe_file_path = os.path.join(path_or_repo_id, filename) + if os.path.exists(maybe_file_path): + return maybe_file_path + + raise FileNotFoundError(f"File not found at {path_or_repo_id}") + + return hf_hub_download(repo_id=path_or_repo_id, filename=filename) + + +class HFMistralTokenizer: + """ + Wraps mistral_common.tokens.tokenizers.mistral.MistralTokenizer + and exposes HuggingFace API for special tokens. + """ + + def __init__( + self, mistral: MistralTokenizer, name_or_path: str, tokenizer_path: str + ): + """ + Args: + mistral: The mistral-common tokenizer to wrap. + name_or_path: The name or path to the tokenizer files or the repo id. + """ + self._mistral = mistral + self._padding_side = "right" + self._name_or_path = name_or_path + self._tokenizer_path = tokenizer_path + + # Manual set to training mode + from mistral_common.protocol.instruct.validator import ( + MistralRequestValidator, + ValidationMode, + ) + + # Check if MistralRequestValidator has a _mode attribute. + # This is a private API and may change in the future. + # pylint: disable=protected-access + if not ( + hasattr(self._mistral, "_chat_completion_request_validator") + and isinstance( + self._mistral._chat_completion_request_validator, + MistralRequestValidator, + ) + and hasattr(self._mistral._chat_completion_request_validator, "_mode") + ): + raise RuntimeError( + "Unable to switch mistral tokenizer to finetuning mode – " + "private API `_chat_completion_request_validator._mode` missing." + ) + + self._mistral._chat_completion_request_validator._mode = ( + ValidationMode.finetuning + ) + + def _load_system_prompt(self, path_or_repo_id: str) -> str: + """Load system prompt from local or HF Hub. + + Note: Unused for now as we don't want to explicitly set the system prompt if a user does + not provide one. + + Args: + path_or_repo_id: The path to the tokenizer files or the repo id. + + Returns: + The system prompt. + """ + file_path = _get_file_path(path_or_repo_id, "SYSTEM_PROMPT.txt") + + if not os.path.exists(file_path): + raise FileNotFoundError(f"System prompt file not found at {file_path}") + + with open(file_path, "r", encoding="utf-8") as file: + return file.read() + + @property + def bos_token_id(self) -> int: + return self._mistral.instruct_tokenizer.tokenizer.bos_id + + @property + def eos_token_id(self) -> int: + return self._mistral.instruct_tokenizer.tokenizer.eos_id + + @property + def pad_token_id(self) -> int: + return self._mistral.instruct_tokenizer.tokenizer.pad_id + + @property + def unk_token_id(self) -> int: + return self._mistral.instruct_tokenizer.tokenizer.unk_id + + @property + def bos_token(self) -> str: + return self._mistral.instruct_tokenizer.tokenizer.id_to_piece(self.bos_token_id) + + @property + def eos_token(self) -> str: + return self._mistral.instruct_tokenizer.tokenizer.id_to_piece(self.eos_token_id) + + @property + def pad_token(self) -> str: + return self._mistral.instruct_tokenizer.tokenizer.id_to_piece(self.pad_token_id) + + @property + def unk_token(self) -> str: + return self._mistral.instruct_tokenizer.tokenizer.id_to_piece(self.unk_token_id) + + @property + def padding_side(self) -> str: + return self._padding_side + + @property + def name_or_path(self) -> str: + return self._name_or_path + + @property + def chat_template(self) -> str | None: + """Chat template is not supported. Dummy method to satisfy HuggingFace API.""" + return None + + def __len__(self) -> int: + return self._mistral.instruct_tokenizer.tokenizer.n_words + + @classmethod + def from_pretrained( + cls, + name_or_path: str, + *, + revision: Optional[str] = None, + **kwargs, # pylint: disable=unused-argument + ) -> "HFMistralTokenizer": + """ + Load a mistral tekken tokenizer from a local file or HF Hub and wrap it. + + Args: + path_or_repo_id: The path to the tokenizer files or the repo id. + revision: The revision of the tokenizer to download. + kwargs: Additional keyword arguments. + + Returns: + A HFMistralTokenizer instance. + """ + if revision: + raise NotImplementedError( + "Revision not supported yet for mistral-common tokenizer" + ) + + # only support Tekken tokenizer for now + # downloads from HF Hub if not local + tokenizer_path = _get_file_path(name_or_path, "tekken.json") + + base = MistralTokenizer.from_file(tokenizer_path) + + return cls( + base, + name_or_path=name_or_path, + tokenizer_path=tokenizer_path, + ) + + def save_pretrained(self, save_directory: str) -> None: + """ + Save the Tekken/SentencePiece model file so that from_pretrained can pick it up again. + + Only Tekken models are supported. + + Args: + save_directory: The directory to save the tokenizer files. + """ + inner = self._mistral.instruct_tokenizer.tokenizer + if isinstance(inner, Tekkenizer): + # Create the directory and save the model + try: + os.makedirs(save_directory, exist_ok=True) + + # Verify directory was created + if not os.path.exists(save_directory): + raise RuntimeError(f"Failed to create directory: {save_directory}") + + # Verify source file exists + if not os.path.exists(self._tokenizer_path): + raise FileNotFoundError( + f"Source tokenizer file not found: {self._tokenizer_path}" + ) + + destination_path = os.path.join(save_directory, "tekken.json") + copyfile(self._tokenizer_path, destination_path) + + except Exception as e: + raise RuntimeError( + f"Failed to save tokenizer to {save_directory}: {e}. " + f"Source path: {self._tokenizer_path}, " + f"Directory exists: {os.path.exists(save_directory)}" + ) from e + + else: + raise RuntimeError(f"Unknown tokenizer type: {type(inner)}") + + def encode(self, text: str, add_special_tokens: bool = True) -> list[int]: + """ + Encode a text string into a list of token IDs. + + Args: + text: The text string to encode. + add_special_tokens: Whether to add special tokens to the encoded tokens. + + Returns: + A list of token IDs. + """ + return self._mistral.instruct_tokenizer.tokenizer.encode( + text, + bos=add_special_tokens, + eos=add_special_tokens, + ) + + def decode( + self, token_ids: int | list[int], skip_special_tokens: bool = False + ) -> str: + """ + Decode a list of token IDs into a text string. + + Args: + token_ids: The int or list of token IDs to decode. + skip_special_tokens: Whether to skip special tokens in the decoded text. + + Returns: + The decoded text string. + """ + if isinstance(token_ids, int): + token_ids = [token_ids] + + if skip_special_tokens: + return self._mistral.instruct_tokenizer.tokenizer.decode(token_ids) + + # to_string returns a string with special tokens + return self._mistral.instruct_tokenizer.tokenizer.to_string(token_ids) + + def _create_mistral_chat_completion_request( + self, conversation: list[dict], tools: list[dict] | None = None + ) -> "ChatCompletionRequest": + from mistral_common.protocol.instruct.messages import ( + AssistantMessage, + SystemMessage, + ToolMessage, + UserMessage, + ) + from mistral_common.protocol.instruct.request import ChatCompletionRequest + from mistral_common.protocol.instruct.tool_calls import Function, Tool + + messages: list[UserMessage | AssistantMessage | ToolMessage | SystemMessage] = ( + [] + ) + for turn in conversation: + role = turn.get("role") + + if role == "user": + messages.append(UserMessage(content=turn["content"])) + elif role == "assistant": + messages.append( + AssistantMessage( + content=turn.get("content"), + tool_calls=turn.get("tool_calls"), + ) + ) + elif role == "tool": + messages.append( + ToolMessage( + content=turn.get("content"), + tool_call_id=turn.get("tool_call_id"), + name=turn.get("name"), + ) + ) + elif role == "system": + messages.append(SystemMessage(content=turn["content"])) + else: + raise ValueError( + f"Unknown role for use with mistral-common tokenizer: {turn['role']}" + ) + + tool_calls: list[Tool] = [] + if tools: + # convert to Tool + for tool in tools: + if tool["type"] != "function": + continue + + function = tool["function"] + + tool_calls.append( + Tool( + function=Function( + name=function["name"], + description=function["description"], + # set parameters to empty dict if not provided + parameters=function.get("parameters", {}), + ) + ) + ) + + chat_completion: ChatCompletionRequest = ChatCompletionRequest( + messages=messages, + tools=tool_calls, + ) + + return chat_completion + + def apply_chat_template( + self, + messages: list[dict], + tokenize: bool = True, + tools: list[dict] | None = None, + chat_template: str | None = None, # pylint: disable=unused-argument + add_generation_prompt: bool = False, # pylint: disable=unused-argument + ) -> list[int] | str: + if chat_template: + raise NotImplementedError("chat_template not supported yet") + + if add_generation_prompt: + raise NotImplementedError("add_generation_prompt not supported yet") + + chat_completion: ChatCompletionRequest = ( + self._create_mistral_chat_completion_request(messages, tools) + ) + + tokens: list[int] = self._mistral.encode_chat_completion(chat_completion).tokens + + if tokenize: + return tokens + + return self.decode(tokens) + + def pad( + self, + features: list[dict[str, list[int] | np.ndarray]], + *, + padding: bool | str | PaddingStrategy = True, + max_length: int | None = None, + pad_to_multiple_of: int | None = None, + return_tensors: str | None = None, # "np", "pt", or "tf" + ) -> dict[str, np.ndarray | Tensor]: + """ + HF-style pad method that properly handles all sequence-related features: + - pad 'input_ids' & 'labels' to the longest (or to max_length) + """ + import torch + from torch.nn import functional as F + + # Check for unsupported fields + if any("token_type_ids" in f for f in features): + raise ValueError("token_type_ids is not supported by this tokenizer") + + # Determine desired sequence length + lengths = [len(f["input_ids"]) for f in features] + if padding in (True, "longest", PaddingStrategy.LONGEST): + target_length = max(lengths) + elif padding in ("max_length", PaddingStrategy.MAX_LENGTH): + if max_length is None: + raise ValueError("max_length must be set for 'max_length' padding") + target_length = max_length + elif padding in (False, "do_not_pad", PaddingStrategy.DO_NOT_PAD): + target_length = None + else: + raise ValueError(f"Unknown padding strategy: {padding}") + + # Apply pad_to_multiple_of + if target_length is not None and pad_to_multiple_of is not None: + target_length = ( + math.ceil(target_length / pad_to_multiple_of) * pad_to_multiple_of + ) + + # If no padding requested, just stack tensors + do_pad = target_length is not None + + # Pad sequences using torch.nn.utils.rnn.pad_sequence + input_ids = torch.nn.utils.rnn.pad_sequence( + [torch.tensor(x["input_ids"], dtype=torch.long) for x in features], + batch_first=True, + padding_value=self.pad_token_id if self.pad_token_id is not None else 0, + ) + + labels = torch.nn.utils.rnn.pad_sequence( + [torch.tensor(x["labels"], dtype=torch.long) for x in features], + batch_first=True, + padding_value=IGNORE_INDEX, + ) + + attention_mask = torch.nn.utils.rnn.pad_sequence( + [torch.tensor(x["attention_mask"], dtype=torch.long) for x in features], + batch_first=True, + padding_value=0, + ) + + # Handle position_ids - pad with sequential values for right padding, 0s for left padding + if "position_ids" in features[0]: + if self.padding_side == "left": + # Likely not needed, but keeping for now + # For left padding, we'll pad with 0s using pad_sequence, then handle manually + position_ids = torch.nn.utils.rnn.pad_sequence( + [ + torch.tensor(x["position_ids"], dtype=torch.long) + for x in features + ], + batch_first=True, + padding_value=0, + ) + else: + # For right padding, continue the sequence + max_pos_len = max(len(f["position_ids"]) for f in features) + position_ids_list = [] + for f in features: + pos_seq = torch.tensor(f["position_ids"], dtype=torch.long) + if len(pos_seq) < max_pos_len: + # Continue the sequence + last_pos = pos_seq[-1].item() if len(pos_seq) > 0 else -1 + pad_len = max_pos_len - len(pos_seq) + pad_positions = torch.arange( + last_pos + 1, last_pos + 1 + pad_len, dtype=torch.long + ) + pos_seq = torch.cat([pos_seq, pad_positions]) + position_ids_list.append(pos_seq) + position_ids = torch.stack(position_ids_list) + else: + # Create position_ids if not present + seq_len = input_ids.size(1) + position_ids = ( + torch.arange(seq_len, dtype=torch.long) + .unsqueeze(0) + .expand(input_ids.size(0), -1) + ) + + # Ensure all tensors have the same sequence length + max_seq_len = max( + input_ids.size(1), + labels.size(1), + attention_mask.size(1), + position_ids.size(1), + ) + + # TODO: check if trimming is needed? and correct. + + if do_pad and target_length is not None: + max_seq_len = target_length + + # Pad all tensors to the same length + if input_ids.size(1) < max_seq_len: + pad_len = max_seq_len - input_ids.size(1) + if self.padding_side == "right": + input_ids = F.pad( + input_ids, + (0, pad_len), + value=self.pad_token_id if self.pad_token_id is not None else 0, + ) + else: + input_ids = F.pad( + input_ids, + (pad_len, 0), + value=self.pad_token_id if self.pad_token_id is not None else 0, + ) + elif input_ids.size(1) > max_seq_len: + input_ids = input_ids[:, :max_seq_len] + + if labels.size(1) < max_seq_len: + pad_len = max_seq_len - labels.size(1) + if self.padding_side == "right": + labels = F.pad(labels, (0, pad_len), value=IGNORE_INDEX) + else: + labels = F.pad(labels, (pad_len, 0), value=IGNORE_INDEX) + elif labels.size(1) > max_seq_len: + labels = labels[:, :max_seq_len] + + if attention_mask.size(1) < max_seq_len: + pad_len = max_seq_len - attention_mask.size(1) + if self.padding_side == "right": + attention_mask = F.pad(attention_mask, (0, pad_len), value=0) + else: + attention_mask = F.pad(attention_mask, (pad_len, 0), value=0) + elif attention_mask.size(1) > max_seq_len: + attention_mask = attention_mask[:, :max_seq_len] + + if position_ids.size(1) < max_seq_len: + pad_len = max_seq_len - position_ids.size(1) + if self.padding_side == "right": + batch_size = position_ids.size(0) + new_position_ids = [] + for i in range(batch_size): + seq = position_ids[i] + if len(seq) > 0: + # get last position and pad with sequential values + last_pos = seq[-1].item() + pad_positions = torch.arange( + last_pos + 1, last_pos + 1 + pad_len, dtype=torch.long + ) + new_seq = torch.cat([seq, pad_positions]) + else: + new_seq = torch.arange(pad_len, dtype=torch.long) + new_position_ids.append(new_seq) + position_ids = torch.stack(new_position_ids) + else: + position_ids = F.pad(position_ids, (pad_len, 0), value=0) + elif position_ids.size(1) > max_seq_len: + position_ids = position_ids[:, :max_seq_len] + + final_batch = { + "input_ids": input_ids, + "labels": labels, + "attention_mask": attention_mask, + "position_ids": position_ids, + } + + # Handle non-sequence fields (raise error) + sequence_fields = {"input_ids", "labels", "attention_mask", "position_ids"} + for f in features: + for key in f.keys(): + if key not in sequence_fields: + raise NotImplementedError( + f"Non-sequence field {key} not handled yet" + ) + + # Convert to requested tensor type + if return_tensors is None or return_tensors == "np": + result = {} + for k, v in final_batch.items(): + if isinstance(v, torch.Tensor): + result[k] = v.numpy().astype(np.long) + else: + result[k] = v + return result + + if return_tensors == "pt": + return final_batch + + raise ValueError(f"Unsupported return_tensors='{return_tensors}'") + + def convert_ids_to_tokens(self, ids: list[int]) -> list[str]: + """ + Convert a list of token IDs to a list of tokens. + + Args: + ids: The list of token IDs to convert. + + Returns: + The list of tokens. + """ + return [ + self._mistral.instruct_tokenizer.tokenizer.id_to_piece(id) for id in ids + ] diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index dad6aac621..505d398582 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1265,6 +1265,68 @@ def check_muon_deepspeed_fsdp(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_tokenizer_use_mistral_common(cls, data): + if data.get("tokenizer_use_mistral_common") is None: + if any( + "magistral" in name.lower() + for name in [ + data.get("base_model", ""), + data.get("base_model_config", ""), + data.get("tokenizer_config", ""), + ] + ): + LOG.warning( + "tokenizer_use_mistral_common auto inferred to True for Magistral models. Please set it to True explicitly if you want to use mistral-common tokenizer." + ) + data["tokenizer_use_mistral_common"] = True + + return data + + @field_validator("tokenizer_use_mistral_common", mode="after") + @classmethod + def check_mistral_common_import(cls, tokenizer_use_mistral_common): + if tokenizer_use_mistral_common: + try: + import mistral_common # noqa: F401 # pylint:disable=unused-import + except ImportError as exception: + raise ImportError( + "mistral-common is required for mistral models. Please install it with `pip install axolotl` or `pip install -e .`." + ) from exception + + return tokenizer_use_mistral_common + + @model_validator(mode="before") + @classmethod + def check_mistral_common_incompatible_options(cls, data): + if not data.get("tokenizer_use_mistral_common"): + return data + + # NOTE: mistral-common tokenizer is not compatible with editing tokenizer at the moment + + if data.get("added_tokens_overrides"): + raise ValueError( + "added_tokens_overrides is not supported with mistral-common tokenizer" + ) + + if data.get("special_tokens"): + raise ValueError( + "special_tokens override is not supported with mistral-common tokenizer" + ) + + if data.get("tokens"): + raise ValueError( + "tokens override is not supported with mistral-common tokenizer" + ) + + if data.get("chat_template"): + raise ValueError( + "Setting chat_template is not supported with mistral-common tokenizer" + ) + + return data + class AxolotlConfigWCapabilities(AxolotlInputConfig): """wrapper to valdiate gpu capabilities with the configured options""" diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index 57f5ae309c..aafb52152e 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -18,6 +18,7 @@ class ModelInputConfig(BaseModel): tokenizer_config: str | None = None tokenizer_use_fast: bool | None = None tokenizer_legacy: bool | None = None + tokenizer_use_mistral_common: bool | None = None tokenizer_type: str | None = Field( default=None, json_schema_extra={"description": "transformers tokenizer class"} ) diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index fe59e00d8f..98488a988c 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -150,6 +150,14 @@ def fixture_gemma2_tokenizer(): return tokenizer +@pytest.fixture(name="magistral_tokenizer") +def fixture_magistral_tokenizer(): + from axolotl.utils.mistral_tokenizer import HFMistralTokenizer + + tokenizer = HFMistralTokenizer.from_pretrained("mistralai/Magistral-Small-2506") + return tokenizer + + @pytest.fixture(name="mistralv03_tokenizer_chat_template_jinja") def fixture_mistralv03_chat_template_jinja_w_system() -> str: return '{%- if messages[0]["role"] == "system" %}\n {%- set system_message = messages[0]["content"] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set loop_messages = messages %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n{%- set user_messages = loop_messages | selectattr("role", "equalto", "user") | list %}\n\n{#- This block checks for alternating user/assistant messages, skipping tool calling messages #}\n{%- set ns = namespace() %}\n{%- set ns.index = 0 %}\n{%- for message in loop_messages %}\n {%- if not (message.role == "tool" or message.role == "tool_results" or (message.tool_calls is defined and message.tool_calls is not none)) %}\n {%- if (message["role"] == "user") != (ns.index % 2 == 0) %}\n {{- raise_exception("After the optional system message, conversation roles must alternate user/assistant/user/assistant/...") }}\n {%- endif %}\n {%- set ns.index = ns.index + 1 %}\n {%- endif %}\n{%- endfor %}\n\n{{- bos_token }}\n{%- for message in loop_messages %}\n {%- if message["role"] == "user" %}\n {%- if tools is not none and (message == user_messages[-1]) %}\n {{- "[AVAILABLE_TOOLS] [" }}\n {%- for tool in tools %}\n {%- set tool = tool.function %}\n {{- \'{"type": "function", "function": {\' }}\n {%- for key, val in tool.items() if key != "return" %}\n {%- if val is string %}\n {{- \'"\' + key + \'": "\' + val + \'"\' }}\n {%- else %}\n {{- \'"\' + key + \'": \' + val|tojson }}\n {%- endif %}\n {%- if not loop.last %}\n {{- ", " }}\n {%- endif %}\n {%- endfor %}\n {{- "}}" }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- else %}\n {{- "]" }}\n {%- endif %}\n {%- endfor %}\n {{- "[/AVAILABLE_TOOLS]" }}\n {%- endif %}\n {%- if loop.first and system_message is defined %}\n {{- "[INST] " + system_message + "\\n\\n" + message["content"] + "[/INST]" }}\n {%- else %}\n {{- "[INST] " + message["content"] + "[/INST]" }}\n {%- endif %}\n {%- elif message.tool_calls is defined and message.tool_calls is not none %}\n {{- "[TOOL_CALLS] [" }}\n {%- for tool_call in message.tool_calls %}\n {%- set out = tool_call.function|tojson %}\n {{- out[:-1] }}\n {%- if not tool_call.id is defined or tool_call.id|length != 9 %}\n {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }}\n {%- endif %}\n {{- \', "id": "\' + tool_call.id + \'"}\' }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- else %}\n {{- "]" + eos_token }}\n {%- endif %}\n {%- endfor %}\n {%- elif message["role"] == "assistant" %}\n {{- " " + message["content"]|trim + eos_token}}\n {%- elif message["role"] == "tool_results" or message["role"] == "tool" %}\n {%- if message.content is defined and message.content.content is defined %}\n {%- set content = message.content.content %}\n {%- else %}\n {%- set content = message.content %}\n {%- endif %}\n {{- \'[TOOL_RESULTS] {"content": \' + content|string + ", " }}\n {%- if not message.tool_call_id is defined or message.tool_call_id|length != 9 %}\n {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }}\n {%- endif %}\n {{- \'"call_id": "\' + message.tool_call_id + \'"}[/TOOL_RESULTS]\' }}\n {%- else %}\n {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }}\n {%- endif %}\n{%- endfor %}\n' diff --git a/tests/prompt_strategies/test_chat_templates_mistral.py b/tests/prompt_strategies/test_chat_templates_mistral.py new file mode 100644 index 0000000000..3c60a15c27 --- /dev/null +++ b/tests/prompt_strategies/test_chat_templates_mistral.py @@ -0,0 +1,290 @@ +"""Test chat templates for mistral-common wrapper tokenizer""" + +import unittest +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from axolotl.utils.mistral_tokenizer import HFMistralTokenizer + + +def test_magistral_chat_template(magistral_tokenizer: "HFMistralTokenizer"): + # pylint: disable=duplicate-code + from axolotl.prompt_strategies.chat_template import MistralPrompter, MistralStrategy + + # check bos, eos, pad, unk are accessible properties + assert magistral_tokenizer.bos_token_id == 1 + assert magistral_tokenizer.eos_token_id == 2 + assert magistral_tokenizer.pad_token_id == 11 + assert magistral_tokenizer.unk_token_id == 0 + + assert magistral_tokenizer.pad_token == "" + assert magistral_tokenizer.eos_token == "" + assert magistral_tokenizer.bos_token == "" + assert magistral_tokenizer.unk_token == "" + + strategy = MistralStrategy( + MistralPrompter( + magistral_tokenizer, + chat_template=None, + message_property_mappings={"role": "role", "content": "content"}, + ), + tokenizer=magistral_tokenizer, + train_on_inputs=False, + train_on_eos="turn", + sequence_len=512, + roles_to_train=["assistant"], + ) + + # test chat template masking without system prompt + res = strategy.tokenize_prompt( + { + "messages": [ + {"role": "user", "content": "Hello, how are you?"}, + {"role": "assistant", "content": "I'm doing great, thank you!"}, + ] + } + ) + + assert res["input_ids"] == [ + 1, # bos + 3, # [INST] + 22177, # Hello + 1044, # , + 2606, # how + 1584, # are + 1636, # you + 1063, # ? + 4, # [/INST] + 1073, # I + 4525, # 'm + 6965, # doing + 4824, # great + 1044, # , + 15412, # thank + 1636, # you + 1033, # ! + 2, # + ] + + assert res["labels"] == [ + -100, # bos + -100, # [INST] + -100, # Hello + -100, # , + -100, # how + -100, # are + -100, # you + -100, # ? + -100, # [/INST] + 1073, # I + 4525, # 'm + 6965, # doing + 4824, # great + 1044, # , + 15412, # thank + 1636, # you + 1033, # ! + 2, # + ] + + # test chat template masking with system prompt + res = strategy.tokenize_prompt( + { + "messages": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello, how are you?"}, + {"role": "assistant", "content": "I'm doing great, thank you!"}, + ] + } + ) + + assert res["input_ids"] == [ + 1, # bos + 17, # [SYSTEM_PROMPT] + 4568, # You + 1584, # are + 1261, # a + 20351, # helpful + 27089, # assistant + 1046, # . + 18, # [/SYSTEM_PROMPT] + 3, # [INST] + 22177, # Hello + 1044, # , + 2606, # how + 1584, # are + 1636, # you + 1063, # ? + 4, # [/INST] + 1073, # I + 4525, # 'm + 6965, # doing + 4824, # great + 1044, # , + 15412, # thank + 1636, # you + 1033, # ! + 2, # + ] + + assert res["labels"] == [ + -100, # bos + -100, # [SYSTEM_PROMPT] + -100, # You + -100, # are + -100, # a + -100, # helpful + -100, # assistant + -100, # . + -100, # [/SYSTEM_PROMPT] + -100, # [INST] + -100, # Hello + -100, # , + -100, # how + -100, # are + -100, # you + -100, # ? + -100, # [/INST] + 1073, # I + 4525, # 'm + 6965, # doing + 4824, # great + 1044, # , + 15412, # thank + 1636, # you + 1033, # ! + 2, # + ] + + # test chat template with tools + res = strategy.tokenize_prompt( + { + "tools": [ + { + "type": "function", + "function": { + "name": "multiples", + "description": "Generates a list of all the multiples of a number that are less than a given limit.", + "parameters": { + "type": "object", + "properties": { + "number": { + "type": "integer", + "description": "The number to find multiples of.", + }, + "limit": { + "type": "integer", + "description": "The upper limit for the multiples.", + }, + }, + "required": ["number", "limit"], + }, + }, + }, + ], + "messages": [ + { + "role": "user", + "content": "Hey, can you give me a breakdown of how to throw an awesome themed party? Like, what themes work best, and how can I set everything up to really wow my guests? I want some ideas on decorations, food, and activities that will make the party unforgettable!", + }, + { + "role": "assistant", + "tool_calls": [ + { + "id": "call12345", + "type": "function", + "function": { + "name": "multiples", + "arguments": { + "number": 16, + "limit": 2, + }, + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call12345", + "name": "multiples", + "content": "1,2", + }, + {"role": "assistant", "content": "The multiples of 16 is 1 and 2."}, + ], + } + ) + + # fmt: off + assert res["input_ids"] == [ + 1, # bos + 5, 1091, 19227, 4994, 2811, 1429, 5165, 1897, 1429, 5165, 2811, 16753, 2391, 2811, 1429, 44627, 3684, 1897, 1429, 14653, 2811, 1429, 10639, 2130, 1261, 2951, 1307, 1747, 1278, 60092, 1307, 1261, 2782, 1455, 1584, 4289, 2224, 1261, 4265, 6139, 39249, 1429, 26204, 2811, 16753, 4994, 2811, 1429, 6371, 1897, 1429, 48649, 2811, 16753, 12856, 2811, 16753, 4994, 2811, 1429, 49039, 1897, 1429, 14653, 2811, 1429, 1784, 2782, 1317, 3081, 60092, 1307, 2613, 4179, 1429, 33319, 2811, 16753, 4994, 2811, 1429, 49039, 1897, 1429, 14653, 2811, 1429, 1784, 9229, 6139, 1394, 1278, 60092, 2613, 47579, 1429, 15760, 2811, 12161, 12856, 1897, 1429, 33319, 4964, 2821, 27028, 6, # tool prompt + 3, 46634, 1044, 1710, 1636, 5628, 1639, 1261, 44433, 1307, 2606, 1317, 5388, 1420, 54191, 2424, 1286, 8967, 1063, 15621, 1044, 2549, 30305, 2196, 3560, 1044, 1321, 2606, 1710, 1362, 2016, 8605, 2015, 1317, 5524, 118931, 2036, 32951, 1063, 1362, 2933, 2269, 12106, 1408, 101987, 1044, 6939, 1044, 1321, 9216, 1455, 2084, 3180, 1278, 8967, 119141, 1689, 5935, 1033, 4, # user + 9, 44627, 3684, 33, 19881, 1049, 1050, 1051, 1052, 1053, 32, 19227, 12856, 2811, 1032, 1049, 1054, 1044, 1429, 33319, 2811, 1032, 1050, 1125, 2, # assistant tool calling + 7, 19881, 1049, 1050, 1051, 1052, 1053, 19, 1049, 1044, 1050, 8, # tool result + 1784, 60092, 1307, 1032, 1049, 1054, 1395, 1032, 1049, 1321, 1032, 1050, 1046, # assistant + 2 # eos + ] + + assert res["labels"] == [ + -100, # bos + -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # tool prompt + -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # user prompt + 9, 44627, 3684, 33, 19881, 1049, 1050, 1051, 1052, 1053, 32, 19227, 12856, 2811, 1032, 1049, 1054, 1044, 1429, 33319, 2811, 1032, 1050, 1125, 2, # assistant tool calling + -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # tool result + 1784, 60092, 1307, 1032, 1049, 1054, 1395, 1032, 1049, 1321, 1032, 1050, 1046, # assistant + 2 # eos + ] + # fmt: on + + # test chat template with tokenize=False + res = magistral_tokenizer.apply_chat_template( + [ + {"role": "user", "content": "Hello, how are you?"}, + {"role": "assistant", "content": "I'm doing great, thank you!"}, + ], + tokenize=False, + ) + + assert res == "[INST]Hello, how are you?[/INST]I'm doing great, thank you!" + + # test encode + res = magistral_tokenizer.encode("Hello, how are you?", add_special_tokens=True) + assert res == [ + 1, # bos + 22177, # Hello + 1044, # , + 2606, # how + 1584, # are + 1636, # you + 1063, # ? + 2, # eos + ] + + # test decode no skip special tokens + decoded_res = magistral_tokenizer.decode(res, skip_special_tokens=False) + + assert decoded_res == "Hello, how are you?" + + # test decode skip special tokens + decoded_res = magistral_tokenizer.decode(res, skip_special_tokens=True) + assert decoded_res == "Hello, how are you?" + + # test encode no special tokens + res = magistral_tokenizer.encode("Hello, how are you?", add_special_tokens=False) + assert res == [ + 22177, # Hello + 1044, # , + 2606, # how + 1584, # are + 1636, # you + 1063, # ? + ] + + # test convert ids to tokens + res = magistral_tokenizer.convert_ids_to_tokens(res) + # spacing are needed as we are converting without decoding + assert res == ["Hello", ",", " how", " are", " you", "?"] + + +if __name__ == "__main__": + unittest.main() From b2274d430b650cca6414e5e712d79a8df13c0ad6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 13 Jun 2025 10:00:35 -0400 Subject: [PATCH 0715/1405] support for QAT w RL (DPO) (#2776) --- examples/llama-3/instruct-dpo-lora-8b.yml | 4 + src/axolotl/common/datasets.py | 13 ++-- src/axolotl/core/builders/rl.py | 21 ++--- src/axolotl/core/trainers/dpo/__init__.py | 11 ++- src/axolotl/core/trainers/dpo/args.py | 2 + src/axolotl/core/trainers/dpo/trainer.py | 17 +++++ .../prompt_strategies/dpo/chat_template.py | 38 +++++++++- src/axolotl/utils/schemas/config.py | 2 + tests/e2e/test_qat.py | 76 +++++++++++++++++-- 9 files changed, 152 insertions(+), 32 deletions(-) diff --git a/examples/llama-3/instruct-dpo-lora-8b.yml b/examples/llama-3/instruct-dpo-lora-8b.yml index 13082294f8..51f1c768b1 100644 --- a/examples/llama-3/instruct-dpo-lora-8b.yml +++ b/examples/llama-3/instruct-dpo-lora-8b.yml @@ -5,6 +5,10 @@ tokenizer_type: AutoTokenizer # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name +special_tokens: + pad_token: <|finetune_right_pad_id|> + eos_token: <|eot_id|> + load_in_8bit: true load_in_4bit: false diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index 4d64958b67..96af84c1e5 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -95,7 +95,7 @@ def load_datasets( def load_preference_datasets( - *, cfg: DictDefault, cli_args: PreprocessCliArgs | TrainerCliArgs + *, cfg: DictDefault, cli_args: PreprocessCliArgs | TrainerCliArgs | None = None ) -> TrainDatasetMeta: """Loads one or more training or evaluation datasets for RL training using paired preference data, calling `axolotl.utils.data.rl.prepare_preference_datasets`. @@ -118,16 +118,19 @@ def load_preference_datasets( math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) ) - if cli_args.debug or cfg.debug: + if (cli_args and cli_args.debug) or cfg.debug: LOG.info("check_dataset_labels...") + num_examples = cli_args.debug_num_examples if cli_args else 1 + text_only = cli_args.debug_text_only if cli_args else False + tokenizer = load_tokenizer(cfg) - train_samples = sample_dataset(train_dataset, cli_args.debug_num_examples) + train_samples = sample_dataset(train_dataset, num_examples) check_dataset_labels( dataset=train_samples, tokenizer=tokenizer, - num_examples=cli_args.debug_num_examples, - text_only=cli_args.debug_text_only, + num_examples=num_examples, + text_only=text_only, rl_mode=True, ) diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index 80c5a9eefe..47ace74516 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -19,6 +19,7 @@ ) from axolotl.integrations.base import PluginManager from axolotl.loaders.utils import ensure_dtype +from axolotl.utils.callbacks.qat import QATCallback from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import RLType @@ -31,6 +32,9 @@ class HFRLTrainerBuilder(TrainerBuilderBase): def get_callbacks(self): callbacks = super().get_callbacks() + if self.cfg.qat: + callbacks.append(QATCallback(self.cfg.qat)) + return callbacks def get_post_trainer_create_callbacks(self, trainer): @@ -138,22 +142,7 @@ def _build_training_arguments(self, total_num_steps): elif self.cfg.rl in [RLType.DPO, RLType.IPO]: training_args_cls = AxolotlDPOConfig - if self.cfg.rl is RLType.IPO: - training_args_kwargs["loss_type"] = "ipo" - - # Not compatible with IPO - if self.cfg.rl is RLType.DPO and self.cfg.dpo_label_smoothing: - training_args_kwargs["label_smoothing"] = self.cfg.dpo_label_smoothing - - training_args_kwargs["max_completion_length"] = None - training_args_kwargs["max_prompt_length"] = self.cfg.sequence_len - training_args_kwargs["generate_during_eval"] = self.cfg.use_wandb - if self.cfg.dpo_use_weighting is not None: - training_args_kwargs["use_weighting"] = self.cfg.dpo_use_weighting - if self.cfg.dpo_use_logits_to_keep is not None: - training_args_kwargs["use_logits_to_keep"] = ( - self.cfg.dpo_use_logits_to_keep - ) + training_args_kwargs.update(DPOStrategy.set_training_args_kwargs(self.cfg)) else: raise ValueError(f"Unsupported RL: {self.cfg.rl}") diff --git a/src/axolotl/core/trainers/dpo/__init__.py b/src/axolotl/core/trainers/dpo/__init__.py index 603fdf0b69..8cd9aacf5f 100644 --- a/src/axolotl/core/trainers/dpo/__init__.py +++ b/src/axolotl/core/trainers/dpo/__init__.py @@ -22,10 +22,19 @@ def set_training_args_kwargs(cls, cfg): training_args_kwargs = {} if cfg.rl is RLType.IPO: training_args_kwargs["loss_type"] = "ipo" - training_args_kwargs["max_length"] = cfg.sequence_len + # Label smoothing is not compatible with IPO + if cfg.rl is RLType.DPO and cfg.dpo_label_smoothing: + training_args_kwargs["label_smoothing"] = cfg.dpo_label_smoothing training_args_kwargs["max_completion_length"] = None + training_args_kwargs["max_length"] = cfg.sequence_len training_args_kwargs["max_prompt_length"] = cfg.sequence_len training_args_kwargs["generate_during_eval"] = cfg.use_wandb if cfg.dpo_use_weighting is not None: training_args_kwargs["use_weighting"] = cfg.dpo_use_weighting + if cfg.dpo_padding_free is not None: + training_args_kwargs["padding_free"] = cfg.dpo_padding_free + if cfg.dpo_norm_loss is not None: + training_args_kwargs["dpo_norm_loss"] = cfg.dpo_norm_loss + if cfg.dpo_use_logits_to_keep is not None: + training_args_kwargs["use_logits_to_keep"] = cfg.dpo_use_logits_to_keep return training_args_kwargs diff --git a/src/axolotl/core/trainers/dpo/args.py b/src/axolotl/core/trainers/dpo/args.py index de1758ed09..b1e53236e5 100644 --- a/src/axolotl/core/trainers/dpo/args.py +++ b/src/axolotl/core/trainers/dpo/args.py @@ -14,3 +14,5 @@ class AxolotlDPOConfig(AxolotlTrainingMixins, DPOConfig): """ DPO config for DPO training """ + + dpo_norm_loss: bool | None = False diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index 15af80c020..762e0a331d 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -83,3 +83,20 @@ def training_step( gc.collect() torch.cuda.empty_cache() return loss + + def concatenated_forward( + self, + model: nn.Module, + batch: dict[str, Union[list, torch.LongTensor]], + is_ref_model: bool = False, + ) -> dict[str, torch.Tensor]: + if self.args.dpo_norm_loss: + # fmt: off + loss_type: str = self.loss_type # type: ignore[has-type] # pylint: disable=access-member-before-definition + # fmt: on + # concatenated_forward handles avg token logprob for ipo case already + self.loss_type = "ipo" # pylint: disable=attribute-defined-outside-init + res = super().concatenated_forward(model, batch, is_ref_model=is_ref_model) + self.loss_type = loss_type # pylint: disable=attribute-defined-outside-init + return res + return super().concatenated_forward(model, batch, is_ref_model=is_ref_model) diff --git a/src/axolotl/prompt_strategies/dpo/chat_template.py b/src/axolotl/prompt_strategies/dpo/chat_template.py index f3427022f2..7867708856 100644 --- a/src/axolotl/prompt_strategies/dpo/chat_template.py +++ b/src/axolotl/prompt_strategies/dpo/chat_template.py @@ -46,6 +46,14 @@ def transform_fn(sample, tokenizer=None): ) messages = sample[field_messages] + if isinstance(messages, str): + messages = [ + { + message_property_mappings["role"]: "user", + message_property_mappings["content"]: messages, + } + ] + messages = [ { "role": role_map[m[message_property_mappings["role"]]], @@ -53,13 +61,35 @@ def transform_fn(sample, tokenizer=None): } for m in messages ] + + chosen_raw = sample[field_chosen] + if isinstance(chosen_raw, str): + chosen_msg = { + message_property_mappings["role"]: "assistant", + message_property_mappings["content"]: chosen_raw, + } + elif isinstance(chosen_raw, dict): + chosen_msg = chosen_raw + else: + chosen_msg = chosen_raw[-1] chosen = { - "role": role_map[sample[field_chosen][message_property_mappings["role"]]], - "content": sample[field_chosen][message_property_mappings["content"]], + "role": role_map[chosen_msg[message_property_mappings["role"]]], + "content": chosen_msg[message_property_mappings["content"]], } + + rejected_raw = sample[field_rejected] + if isinstance(rejected_raw, str): + rejected_msg = { + message_property_mappings["role"]: "assistant", + message_property_mappings["content"]: rejected_raw, + } + elif isinstance(rejected_raw, dict): + rejected_msg = rejected_raw + else: + rejected_msg = rejected_raw[-1] rejected = { - "role": role_map[sample[field_rejected][message_property_mappings["role"]]], - "content": sample[field_rejected][message_property_mappings["content"]], + "role": role_map[rejected_msg[message_property_mappings["role"]]], + "content": rejected_msg[message_property_mappings["content"]], } dummy_user_message = {"role": "user", "content": "[[dummy_message]]"} diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 505d398582..33a8f77dbf 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -102,6 +102,8 @@ class AxolotlInputConfig( dpo_use_weighting: bool | None = None dpo_use_logits_to_keep: bool | None = None dpo_label_smoothing: float | None = None + dpo_norm_loss: bool | None = None + dpo_padding_free: bool | None = None datasets: ( Annotated[ diff --git a/tests/e2e/test_qat.py b/tests/e2e/test_qat.py index 2a7cd1459c..964bf3c1ca 100644 --- a/tests/e2e/test_qat.py +++ b/tests/e2e/test_qat.py @@ -2,24 +2,22 @@ E2E tests for QAT """ -import unittest from pathlib import Path -from axolotl.common.datasets import load_datasets +from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from .utils import check_model_output_exists, with_temp_dir +from .utils import check_model_output_exists, check_tensorboard -class TestQATLlama(unittest.TestCase): +class TestQATLlama: """ Test case for QAT Llama models """ - @with_temp_dir - def test_qat_lora(self, temp_dir): + def test_qat(self, temp_dir): # pylint: disable=duplicate-code cfg = DictDefault( { @@ -67,3 +65,69 @@ def test_qat_lora(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-5", cfg) + + def test_qat_dpo(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 2048, + "sample_packing": False, + "eval_sample_packing": False, + "pad_to_sequence_len": True, + "val_set_size": 0.01, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "rl": "dpo", + "chat_template": "chatml", + "datasets": [ + { + "path": "fozziethebeat/alpaca_messages_2k_dpo_test", + "type": "chat_template.default", + "field_messages": "conversation", + "field_chosen": "chosen", + "field_rejected": "rejected", + "message_field_role": "role", + "message_field_content": "content", + "roles": { + "system": ["system"], + "user": ["user"], + "assistant": ["assistant"], + }, + }, + ], + "num_epochs": 1, + "max_steps": 5, + "micro_batch_size": 2, + "gradient_accumulation_steps": 2, + "output_dir": temp_dir, + "warmup_steps": 0, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "use_tensorboard": True, + "bf16": True, + "qat": { + "quantize_embedding": True, + "activation_dtype": "int8", + "weight_dtype": "int8", + "group_size": 8, + }, + } + ) + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_preference_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(Path(temp_dir) / "checkpoint-5", cfg) + + loss_threshold = 2.3 + check_tensorboard( + temp_dir + "/runs", + "train/train_loss", + loss_threshold, + "Train Loss is too high", + ) From a3c82e8cbbe4f8285a7ebb40891cd9289b087ece Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 13 Jun 2025 12:03:47 -0700 Subject: [PATCH 0716/1405] fix: grpo doc link (#2788) [skip ci] --- docs/rlhf.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index b2687a8f90..0a189e3c17 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -500,7 +500,7 @@ The input format is a simple JSON input with customizable fields based on the ab ### GRPO ::: {.callout-tip} -Check out our [GRPO cookbook](https://github.com/axolotl-ai-cloud/axolotl-cookbook/tree/main/grpo#training-an-r1-style-large-language-model-using-grpo). +Check out our [GRPO cookbook](https://github.com/axolotl-ai-cloud/grpo_code). ::: In the latest GRPO implementation, `vLLM` is used to significantly speedup trajectory generation during training. In this example, we're using 4 GPUs - 2 for training, and 2 for vLLM: From 80d5b066ecfc3ae6e97ad70d93942e816b4a9a72 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 14 Jun 2025 11:53:43 -0700 Subject: [PATCH 0717/1405] Fix: adding magistral fsdp config, fixing not eval with test_datasets, handle mllama attention (#2789) [skip ci] * feat: add fsdp config for magistral * fix: add mllama self attention handling for lora kernels * fix: no eval if val_set_size 0 despite having test_datasets * fix: add note for cce for vlm in newer model --- .../magistral/magistral-small-fsdp-qlora.yaml | 72 +++++++++++++++++++ src/axolotl/core/builders/base.py | 4 +- .../integrations/cut_cross_entropy/README.md | 8 +++ src/axolotl/monkeypatch/lora_kernels.py | 5 ++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 examples/magistral/magistral-small-fsdp-qlora.yaml diff --git a/examples/magistral/magistral-small-fsdp-qlora.yaml b/examples/magistral/magistral-small-fsdp-qlora.yaml new file mode 100644 index 0000000000..b10e8baf6a --- /dev/null +++ b/examples/magistral/magistral-small-fsdp-qlora.yaml @@ -0,0 +1,72 @@ +base_model: mistralai/Magistral-Small-2506 + +# Enable to use mistral-common tokenizer +tokenizer_use_mistral_common: true + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: false +pad_to_sequence_len: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_transformer_layer_cls_to_wrap: MistralDecoderLayer + fsdp_activation_checkpointing: true diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index ac49b4e88a..e399cf3c5e 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -380,8 +380,8 @@ def _configure_save_and_eval_strategy(self, training_args_kwargs: dict): ) # eval_strategy and eval_steps - if not self.eval_dataset or self.cfg.val_set_size == 0: - # do not eval if no eval_dataset or val_set_size=0 + if not self.eval_dataset and self.cfg.val_set_size == 0: + # do not eval if no eval_dataset and val_set_size=0 training_args_kwargs["eval_strategy"] = "no" elif self.cfg.eval_steps: training_args_kwargs["eval_strategy"] = "steps" diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 627ebd9358..bddf3ced2b 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -24,6 +24,14 @@ pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transform ## Usage +**NOTE**: If you are training a VLM model, please use older version of Axolotl as upstream has applied a major VLM refactor, and our patches have not been updated yet. + +```bash +git checkout 787880215b3ab32ccaf81c1b2e9588c6f3e6e764 + +pip3 install --no-build-isolation -e . +``` + ```yaml plugins: - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index a7875eefe4..63fbfa359c 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -145,6 +145,11 @@ def get_attention_cls_from_config(cfg: DictDefault) -> Type[nn.Module]: return Qwen2Attention + if model_type == "mllama": + from transformers.models.mllama.modeling_mllama import MllamaTextSelfAttention + + return MllamaTextSelfAttention + try: # Dynamically import the module and attention class module_path = f"transformers.models.{model_type}.modeling_{model_type}" From 21388cf615ddce9cdc39fc1a594b00162fc4959e Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 14 Jun 2025 11:54:06 -0700 Subject: [PATCH 0718/1405] Fix: lora kernel pre-patch applied despite post-patch not applied (#2772) * fix: do not pre-patch self attention if lora dropout non-zero * fix: add test to check patch not applied * fix: test * fix: test config check * fix where we check so that tests don't break * fix: test --------- Co-authored-by: Wing Lian --- src/axolotl/loaders/patch_manager.py | 11 ++++ src/axolotl/monkeypatch/lora_kernels.py | 35 +++++++---- .../lora_kernels/test_lora_kernel_patching.py | 62 +++++++++++++++++++ 3 files changed, 97 insertions(+), 11 deletions(-) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 23f79d368a..ca8fd12583 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -166,6 +166,17 @@ def _apply_mistral_cross_entropy_patch(self): def _apply_self_attention_lora_patch(self): """Apply self-attention LoRA patches if configured.""" if self.cfg.lora_qkv_kernel or self.cfg.lora_o_kernel: + # Only patch if conditions are met + can_patch = ( + self.cfg.lora_dropout == 0 + if hasattr(self.cfg, "lora_dropout") + else True + ) # default to True if lora_dropout is not set + + if not can_patch: + LOG.warning("Cannot patch self-attention - requires no dropout") + return + from axolotl.monkeypatch.lora_kernels import patch_self_attn_lora patch_self_attn_lora(self.cfg) diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 63fbfa359c..586412dd78 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -274,6 +274,29 @@ def find_mlp_in_layer( ) +def get_layers(model: PeftModelForCausalLM) -> list[nn.Module]: + """ + Get the layers of the model. Handles text-only and multimodal models. + + Args: + model: A PEFT model. + + Returns: + A list of layers. + """ + pretrained_model = model.model + + # check for multimodal models first + if hasattr(pretrained_model, "language_model"): + return pretrained_model.language_model.layers + if hasattr(pretrained_model, "model"): + return pretrained_model.model.layers + + raise NotImplementedError( + f"Model type {model.config.model_type} is not supported yet. Please create an Issue." + ) + + def apply_lora_kernel_patches( model: PeftModelForCausalLM, cfg: DictDefault ) -> PeftModelForCausalLM: @@ -345,17 +368,7 @@ def apply_lora_kernel_patches( if activation not in SUPPORTED_ACTIVATIONS: raise NotImplementedError(f"Activation {activation} is not supported") - layers = [] - # check for multimodal models first - pretrained_model = model.model - if hasattr(pretrained_model, "language_model"): - layers = pretrained_model.language_model.layers - elif hasattr(pretrained_model, "model"): - layers = pretrained_model.model.layers - else: - raise NotImplementedError( - f"Model type {model.config.model_type} is not supported yet. Please create an Issue." - ) + layers = get_layers(model) # Patch each layer for layer in layers: diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index 76c383a921..56ce5a8b9a 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -25,7 +25,9 @@ from axolotl.loaders.tokenizer import load_tokenizer from axolotl.monkeypatch.lora_kernels import ( apply_lora_kernel_patches, + find_self_attn_in_layer, get_attention_cls_from_config, + get_layers, patch_self_attn_lora, ) from axolotl.utils.dict import DictDefault @@ -501,3 +503,63 @@ def test_kernel_training_integration_auto_enable(temp_dir): break assert found_patched_attn + + +def test_kernel_training_integration_dropout_non_zero(): + """Test model loading with dropout non-zero should not patch.""" + + from axolotl.cli.utils import load_model_and_tokenizer + + # Create minimal config + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_config": "HuggingFaceTB/SmolLM2-135M", + "learning_rate": 0.000001, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + } + ], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.1, + "lora_target_linear": True, + "sequence_len": 1024, + } + ) + + # Get original attention class + attention_cls = get_attention_cls_from_config(cfg) + + # Store original state before patching + original_forward_method = attention_cls.forward + + # Load model + model, tokenizer, _ = load_model_and_tokenizer(cfg=cfg) + + # We call modelloader as that's where the patches are applied + # despite the fact that we're not using it to load the model + model_loader = ModelLoader(cfg, tokenizer) + + # Apply patch + model_loader.patch_manager._apply_self_attention_lora_patch() # pylint: disable=protected-access + + # Verify patch was not applied + assert attention_cls.forward == original_forward_method + + # Apply apply_lora_kernel_patches + model_loader.patch_manager._apply_lora_kernel_patch( # pylint: disable=protected-access + model + ) + + # Verify patch was not applied + layers = get_layers(model) + for layer in layers: + for self_attn in find_self_attn_in_layer(layer): + assert not hasattr(self_attn, "apply_qkv") + assert not hasattr(self_attn, "apply_o") From ba62aa65ee00fe0af731855c5b3686b1b650cfb7 Mon Sep 17 00:00:00 2001 From: Matt Cummins Date: Sun, 15 Jun 2025 13:47:02 -0700 Subject: [PATCH 0719/1405] fixed the lora_target_modules syntax (#2793) --- examples/qwen2-vl/lora-7b.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/qwen2-vl/lora-7b.yaml b/examples/qwen2-vl/lora-7b.yaml index 55773bc3d6..e8932b9688 100644 --- a/examples/qwen2-vl/lora-7b.yaml +++ b/examples/qwen2-vl/lora-7b.yaml @@ -25,7 +25,7 @@ pad_to_sequence_len: false lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 -lora_target_modules: 'model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' wandb_project: wandb_entity: From ccc94da8ad3e0e99e97357220a720fe095e645b4 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 17 Jun 2025 12:09:13 -0400 Subject: [PATCH 0720/1405] KD fix w/ online distillation (#2700) [skip ci] * kd fixes * fix collator setup * fix input args * better handling to drop string fields for kd with raw dataset * kd trainer has kd temp as part of the init * drop top_k before softmax * simplfy and remove zscore * WIP chunked KD loss with autograd wrapper * more fixes and liger-type chunked loss * collator cls for plugins * remove debugging * additional plugin collator kwargs, don't scale up kd loss by t^2 * don't need temp arg to distill method * online kd wip * add close to comment block * suport sampling params/max new tokens * handle when no custom collator is used in plugins * logsumexp trick: * fix check * shift off the first empty token * fix length of padding * use max not min * temp scale kd loss at end * support for dynamic plugin training args mixins and symmetric kl * chore: lint * fix trainer callback base class * Fix decay * accept compressed responses for smaller wire payload * post-rebase lint * more KD updates * increase hyperparams_count for gradients for added normalize_topk * fix to remove attention_mask * rename vars for consistency * fix rebase issues * default to dropping last batch in multipack batch sampler * improve handling of train len * init collator_cls_and_kwargs * explicit drop_last=False when checking for multipack completeness * use separate v2 loader for kd * fix kd tests to use subprocess so it picks up kd training args * default value for kd_beta arg * use updated dataset for ci * longer timeout for e2e --- .github/workflows/tests.yml | 4 +- deepspeed_configs/zero2_torch_compile.json | 31 + src/axolotl/core/builders/causal.py | 63 +- src/axolotl/core/builders/rl.py | 17 +- src/axolotl/core/trainers/base.py | 10 +- src/axolotl/core/training_args.py | 235 +------- src/axolotl/core/training_args_base.py | 224 +++++++ src/axolotl/integrations/base.py | 88 ++- src/axolotl/integrations/config.py | 42 +- src/axolotl/integrations/kd/__init__.py | 71 +++ src/axolotl/integrations/kd/args.py | 56 +- src/axolotl/integrations/kd/callbacks.py | 36 ++ src/axolotl/integrations/kd/chat_template.py | 134 ++++- src/axolotl/integrations/kd/collator.py | 24 +- .../kd/collator_online_teacher.py | 561 ++++++++++++++++++ .../integrations/kd/kernels/__init__.py | 8 + src/axolotl/integrations/kd/kernels/liger.py | 485 +++++++++++++++ src/axolotl/integrations/kd/kernels/models.py | 98 +++ .../kd/topk_logprob/forward_kl.py | 234 +++----- src/axolotl/integrations/kd/trainer.py | 74 +-- src/axolotl/integrations/kd/utils.py | 100 ++++ src/axolotl/prompt_strategies/__init__.py | 5 +- src/axolotl/train.py | 9 +- src/axolotl/utils/__init__.py | 7 + src/axolotl/utils/chat_templates.py | 2 +- src/axolotl/utils/collators/batching.py | 4 +- src/axolotl/utils/data/utils.py | 1 + src/axolotl/utils/samplers/multipack.py | 18 +- src/axolotl/utils/trainer.py | 6 +- tests/e2e/integrations/test_kd.py | 65 +- tests/test_packed_batch_sampler.py | 1 + 31 files changed, 2182 insertions(+), 531 deletions(-) create mode 100644 deepspeed_configs/zero2_torch_compile.json create mode 100644 src/axolotl/core/training_args_base.py create mode 100644 src/axolotl/integrations/kd/callbacks.py create mode 100644 src/axolotl/integrations/kd/collator_online_teacher.py create mode 100644 src/axolotl/integrations/kd/kernels/liger.py create mode 100644 src/axolotl/integrations/kd/kernels/models.py create mode 100644 src/axolotl/integrations/kd/utils.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 11fe13713a..bb865e98d4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -188,7 +188,7 @@ jobs: if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] - timeout-minutes: 90 + timeout-minutes: 120 needs: [pre-commit, pytest, pytest-sdist] strategy: @@ -238,7 +238,7 @@ jobs: if: github.repository_owner == 'axolotl-ai-cloud' # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] - timeout-minutes: 90 + timeout-minutes: 120 # Only run the remainder of the matrix if the first e2e check passed; # this is to save on wasted compute costs for known failures that get caught in the first run needs: [pre-commit, pytest, docker-e2e-tests-1st] diff --git a/deepspeed_configs/zero2_torch_compile.json b/deepspeed_configs/zero2_torch_compile.json new file mode 100644 index 0000000000..c3bcf98cf5 --- /dev/null +++ b/deepspeed_configs/zero2_torch_compile.json @@ -0,0 +1,31 @@ +{ + "compile": { + "disable": false, + "backend": "inductor" + }, + "zero_optimization": { + "stage": 2, + "offload_optimizer": { + "device": "cpu" + }, + "contiguous_gradients": true, + "overlap_comm": true + }, + "bf16": { + "enabled": "auto" + }, + "fp16": { + "enabled": "auto", + "auto_cast": false, + "loss_scale": 0, + "initial_scale_power": 32, + "loss_scale_window": 1000, + "hysteresis": 2, + "min_loss_scale": 1 + }, + "gradient_accumulation_steps": "auto", + "gradient_clipping": "auto", + "train_batch_size": "auto", + "train_micro_batch_size_per_gpu": "auto", + "wall_clock_breakdown": false +} diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 8ff565dbb6..6ed298d9f8 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -21,11 +21,6 @@ AxolotlTrainer, ReLoRATrainer, ) -from axolotl.core.training_args import ( - AxolotlPRMConfig, - AxolotlRewardConfig, - AxolotlTrainingArguments, -) from axolotl.integrations.base import PluginManager from axolotl.monkeypatch.multipack import SUPPORTED_MULTIPACK_MODEL_TYPES from axolotl.monkeypatch.relora import ReLoRACallback @@ -130,6 +125,9 @@ def get_post_trainer_create_callbacks(self, trainer): return callbacks def _get_trainer_cls(self): + """ + Gets the trainer class for the given configuration. + """ if self.cfg.plugins: plugin_manager = PluginManager.get_instance() trainer_cls = plugin_manager.get_trainer_cls(self.cfg) @@ -146,6 +144,12 @@ def _get_trainer_cls(self): return AxolotlTrainer def build(self, total_num_steps): + from axolotl.core.training_args import ( + AxolotlPRMConfig, + AxolotlRewardConfig, + AxolotlTrainingArguments, + ) + training_arguments_kwargs, trainer_kwargs = self._set_base_training_args( total_num_steps ) @@ -314,20 +318,12 @@ def build(self, total_num_steps): training_arguments_kwargs["image_resize_algorithm"] = ( self.cfg.image_resize_algorithm ) - if self.cfg.kd_ce_alpha is not None: - training_arguments_kwargs["kd_ce_alpha"] = self.cfg.kd_ce_alpha - if self.cfg.kd_alpha is not None: - training_arguments_kwargs["kd_alpha"] = self.cfg.kd_alpha - if self.cfg.kd_temperature is not None: - training_arguments_kwargs["kd_temperature"] = self.cfg.kd_temperature - if self.cfg.kd_zscore_base_temp is not None: - training_arguments_kwargs["kd_zscore_base_temp"] = ( - self.cfg.kd_zscore_base_temp - ) - if self.cfg.kd_top_k_before_softmax is not None: - training_arguments_kwargs["kd_top_k_before_softmax"] = ( - self.cfg.kd_top_k_before_softmax - ) + + if self.cfg.plugins: + plugin_manager = PluginManager.get_instance() + plugin_training_args = plugin_manager.get_training_args(self.cfg) + if plugin_training_args: + training_arguments_kwargs.update(plugin_training_args) if self.cfg.reward_model: training_args_cls = AxolotlRewardConfig @@ -408,7 +404,10 @@ def build(self, total_num_steps): return trainer def build_collator( - self, training_args: AxolotlTrainingArguments, is_eval=False, **kwargs + self, + training_args, # type: "AxolotlTrainingArguments" # type: ignore + is_eval=False, + **kwargs, ): if training_args.pretraining: if ( @@ -437,7 +436,19 @@ def build_collator( ] ] collator_args = [self.tokenizer] - if self.cfg.reward_model: + + collator_cls_and_kwargs = None + if self.cfg.plugins: + plugin_manager = PluginManager.get_instance() + collator_cls_and_kwargs = plugin_manager.get_collator_cls_and_kwargs( + self.cfg, is_eval=is_eval + ) + + if collator_cls_and_kwargs: + collator = collator_cls_and_kwargs[0] + if kwargs and isinstance(kwargs, dict): + kwargs.update(collator_cls_and_kwargs[1]) + elif self.cfg.reward_model: collator = RewardDataCollatorWithPadding elif use_batch_sampler_collator: # Use V2BatchSamplerDataCollatorForSeq2Seq for flex attention, @@ -468,16 +479,6 @@ def build_collator( collator_args.pop(0) kwargs.pop("pad_to_multiple_of", None) kwargs.pop("padding", None) - elif self.cfg.kd_trainer: - from axolotl.integrations.kd.collator import ( - DataCollatorForKD, - KDBatchSamplerDataCollatorForSeq2Seq, - ) - - if self.cfg.sample_packing: - collator = KDBatchSamplerDataCollatorForSeq2Seq - else: - collator = DataCollatorForKD else: collator = DataCollatorForSeq2Seq diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index 47ace74516..c5f01dd418 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -12,11 +12,6 @@ from axolotl.core.trainers.dpo import DPOStrategy from axolotl.core.trainers.dpo.args import AxolotlDPOConfig from axolotl.core.trainers.grpo import GRPOStrategy -from axolotl.core.training_args import ( - AxolotlCPOConfig, - AxolotlKTOConfig, - AxolotlORPOConfig, -) from axolotl.integrations.base import PluginManager from axolotl.loaders.utils import ensure_dtype from axolotl.utils.callbacks.qat import QATCallback @@ -83,6 +78,12 @@ def _build_training_arguments(self, total_num_steps): """ Returns training_args and trainer_kwargs """ + from axolotl.core.training_args import ( + AxolotlCPOConfig, + AxolotlKTOConfig, + AxolotlORPOConfig, + ) + training_args_kwargs, trainer_kwargs = self._set_base_training_args( total_num_steps=total_num_steps ) @@ -150,6 +151,12 @@ def _build_training_arguments(self, total_num_steps): if blocklist_key in training_args_kwargs: del training_args_kwargs[blocklist_key] + if self.cfg.plugins: + plugin_manager = PluginManager.get_instance() + plugin_training_args = plugin_manager.get_training_args(self.cfg) + if plugin_training_args: + training_args_kwargs.update(plugin_training_args) + training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg logging_first_step=True, **training_args_kwargs, diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 25ffb4cbf7..fbae253d66 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -34,6 +34,7 @@ sanitize_kwargs_for_ds_tagging, sanitize_kwargs_for_tagging, ) +from axolotl.utils import get_not_null from axolotl.utils.logging import get_logger from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths @@ -104,7 +105,7 @@ def _create_multipack_sampler( ) batch_max_len = train_batch_size * self.args.max_seq_length - return MultipackBatchSampler( + sampler = MultipackBatchSampler( base_sampler, lengths=get_dataset_lengths(dataset), packing_efficiency_estimate=self.args.sample_packing_efficiency, @@ -117,6 +118,9 @@ def _create_multipack_sampler( num_processes=self.args.dataset_num_proc, ) + len(sampler) + return sampler + def _get_train_sampler( self, train_dataset: Optional[Dataset] = None ) -> Optional[Sampler]: @@ -224,7 +228,9 @@ def _get_dataloader( } if not isinstance(dataset, torch.utils.data.IterableDataset): - dataloader_params["drop_last"] = self.args.dataloader_drop_last + dataloader_params["drop_last"] = get_not_null( + self.args.dataloader_drop_last, True + ) if sampler_fn is not None: sampler = sampler_fn(dataset) if isinstance(sampler, BatchSampler): diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index 2b53c67986..d5be9fc626 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -2,242 +2,17 @@ extra axolotl specific training args """ +from __future__ import annotations + from dataclasses import dataclass, field -from typing import Optional +from typing import Optional, Type -from PIL.Image import Resampling from transformers import TrainingArguments from trl import CPOConfig, KTOConfig, ORPOConfig, PRMConfig, RewardConfig +from axolotl.integrations.config import merge_training_args -@dataclass -class AxolotlTrainingMixins: - """ - Mixin class for the Axolotl training args. - """ - - # pylint: disable=duplicate-code - model_type: Optional[str] = field( - default=None, metadata={"help": "HF model configuration model_type."} - ) - lr_quadratic_warmup: bool = field( - default=False, - metadata={"help": "Use quadratic warmup for cosine scheduling."}, - ) - pretraining: bool = field( - default=False, - metadata={ - "help": "Indicates to trainer whether we are doing continued pretraining." - }, - ) - sample_packing: bool = field( - default=False, - metadata={"help": "Use sample packing for efficient training."}, - ) - sample_packing_sequentially: bool = field( - default=False, - metadata={ - "help": "Use next-fit sample packing that preserves the order of samples coming from the sampler. Use in combination with curriculum_sampling for fully sequential packing." - }, - ) - multipack_real_batches: bool = field( - default=False, - metadata={"help": "Use real batches for efficient training."}, - ) - eval_sample_packing: Optional[bool] = field( - default=None, - metadata={"help": "Use sample packing for efficient evals."}, - ) - sample_packing_efficiency: float = field( - default=1.0, - metadata={"help": "Sample packing efficiency for calculating batch length."}, - ) - sample_packing_bin_size: int = field( - default=200, - metadata={ - "help": "The max number of samples that packed sample can contain after packing. Increase for better packing." - }, - ) - sample_packing_group_size: int = field( - default=100000, - metadata={ - "help": "The number of samples to group together for packing. Increase for better packing." - }, - ) - max_seq_length: int = field( - default=2048, - metadata={"help": "The maximum sequence length the model can handle"}, - ) - dataset_num_proc: int | None = field( - default=None, - metadata={"help": "The number of processes to use for data processing"}, - ) - relora_steps: Optional[int] = field( - default=None, - metadata={"help": "how often to reset for ReLoRA"}, - ) - relora_warmup_steps: Optional[int] = field( - default=None, - metadata={"help": "how many warmup steps to take after reset for ReLoRA"}, - ) - relora_anneal_steps: Optional[int] = field( - default=None, - metadata={"help": "how many warmup steps to take after reset for ReLoRA"}, - ) - relora_prune_ratio: Optional[float] = field( - default=0.9, - metadata={"help": "prune ratio for magnitude pruning of the optimizer"}, - ) - bench_split: Optional[str] = field( - default="eval", metadata={"help": "The benchmark split to run on"} - ) - bench_dataset: Optional[str] = field( - default="pharaouk/dharma-1/dharma_1_mini.json", - metadata={ - "help": "Benchmark dataset to use: options are `mmlu-zs`, `mmlu-fs`, or the full path to the dataset file" - }, - ) - do_bench_eval: Optional[bool] = field( - default=False, metadata={"help": "Whether to run the Benchmark evaluation."} - ) - do_causal_lm_eval: Optional[bool] = field( - default=False, metadata={"help": "Whether to run the Causal LM evaluation."} - ) - max_bench_samples: Optional[int] = field( - default=None, - metadata={ - "help": "If set, only evaluates on `max_bench_samples` of the benchmark dataset." - }, - ) - bench_source_max_len: int = field( - default=2048, metadata={"help": "Maximum source sequence length for bench."} - ) - dataloader_prefetch_factor: Optional[int] = field( - default=None, - metadata={"help": "prefetch_factor argument to the dataloader"}, - ) - cosine_min_lr_ratio: Optional[float] = field( - default=None, - metadata={"help": "Minimum learning rate is min_lr_ratio * learning_rate"}, - ) - cosine_constant_lr_ratio: Optional[float] = field( - default=None, - metadata={ - "help": "Starting constant learning rate step is cosine_constant_lr_ratio * max_steps" - }, - ) - loraplus_lr_ratio: Optional[float] = field( - default=None, metadata={"help": "loraplus learning rate ratio lr_B / lr_A."} - ) - loraplus_lr_embedding: Optional[float] = field( - default=1e-6, - metadata={"help": "loraplus learning rate for lora embedding layers."}, - ) - embedding_lr_scale: Optional[float] = field( - default=None, - metadata={"help": "Scale the learning rate for the embedding layers."}, - ) - lr_groups: Optional[list[dict]] = field( - default=None, - metadata={"help": "Specify learning rate groups for with different LRs."}, - ) - embedding_lr: Optional[float] = field( - default=None, - metadata={"help": "absolute learning rate for the embedding layers."}, - ) - qlora: bool = field( - default=False, - metadata={"help": "whether this is a qlora training"}, - ) - orpo_alpha: Optional[float] = field( - default=None, - ) - lisa_n_layers: Optional[int] = field( - default=None, - metadata={"help": "the number of activate layers in LISA"}, - ) - lisa_step_interval: Optional[int] = field( - default=None, - metadata={"help": "how often to switch layers in LISA"}, - ) - lisa_layers_attribute: Optional[str] = field( - default=None, - metadata={"help": "path under the model to access the layers"}, - ) - curriculum_sampling: Optional[bool] = field( - default=None, - metadata={"help": "whether to use sequential sampling for curriculum learning"}, - ) - alternate_lr_scheduler_type: Optional[str] = field( - default=None, - metadata={ - "help": "workaround to pass an alternate lr scheduler to the HF trainer" - }, - ) - chat_template: Optional[str] = field( - default=None, - metadata={"help": "Chat template converting chat messages to text"}, - ) - - kd_ce_alpha: Optional[float] = field( - default=None, - metadata={ - "help": "The alpha scaling parameter for SFT cross entropy loss when using KD" - }, - ) - - kd_alpha: Optional[float] = field( - default=1.0, - metadata={"help": "The alpha scaling parameter for KD loss"}, - ) - - kd_temperature: Optional[float] = field( - default=1.0, - metadata={ - "help": "the temperature parameter for KL divergence loss when using KD" - }, - ) - - kd_zscore_base_temp: Optional[float] = field( - default=None, - metadata={ - "help": "the base temperature parameter for KL divergence with z-score when using KD" - }, - ) - - kd_top_k_before_softmax: Optional[bool] = field( - default=None, - metadata={ - "help": "Whether to apply top_k_before_softmax to the logits when using KD" - }, - ) - - adam_beta3: Optional[float] = field( - default=None, - metadata={ - "help": "The beta3 hyperparameter used in some optimizers such as CAME" - }, - ) - adam_epsilon2: Optional[float] = field( - default=None, - metadata={ - "help": "The epsilon2 hyperparameter used in some optimizers such as CAME" - }, - ) - - # multi-modal section - - image_size: int | tuple[int, int] | None = field( - default=None, - metadata={"help": "The size of the image to resize to"}, - ) - - image_resize_algorithm: Resampling | None = field( - default=None, - metadata={"help": "The algorithm to use for image resizing"}, - ) - - # end of multi-modal section +AxolotlTrainingMixins: Type = merge_training_args() @dataclass diff --git a/src/axolotl/core/training_args_base.py b/src/axolotl/core/training_args_base.py new file mode 100644 index 0000000000..8fcaff632f --- /dev/null +++ b/src/axolotl/core/training_args_base.py @@ -0,0 +1,224 @@ +""" +Base Axolotl Training Mixins shared across various trainer configs +""" + +from dataclasses import dataclass, field +from typing import Optional + +from PIL.Image import Resampling + + +@dataclass +class AxolotlTrainingMixins: + """ + Mixin class for the Axolotl training args. + """ + + # pylint: disable=duplicate-code + model_type: Optional[str] = field( + default=None, metadata={"help": "HF model configuration model_type."} + ) + lr_quadratic_warmup: bool = field( + default=False, + metadata={"help": "Use quadratic warmup for cosine scheduling."}, + ) + pretraining: bool = field( + default=False, + metadata={ + "help": "Indicates to trainer whether we are doing continued pretraining." + }, + ) + sample_packing: bool = field( + default=False, + metadata={"help": "Use sample packing for efficient training."}, + ) + sample_packing_sequentially: bool = field( + default=False, + metadata={ + "help": "Use next-fit sample packing that preserves the order of samples coming from the sampler. Use in combination with curriculum_sampling for fully sequential packing." + }, + ) + multipack_real_batches: bool = field( + default=False, + metadata={"help": "Use real batches for efficient training."}, + ) + eval_sample_packing: Optional[bool] = field( + default=None, + metadata={"help": "Use sample packing for efficient evals."}, + ) + sample_packing_efficiency: float = field( + default=1.0, + metadata={"help": "Sample packing efficiency for calculating batch length."}, + ) + sample_packing_bin_size: int = field( + default=200, + metadata={ + "help": "The max number of samples that packed sample can contain after packing. Increase for better packing." + }, + ) + sample_packing_group_size: int = field( + default=100000, + metadata={ + "help": "The number of samples to group together for packing. Increase for better packing." + }, + ) + max_seq_length: int = field( + default=2048, + metadata={"help": "The maximum sequence length the model can handle"}, + ) + dataset_num_proc: int | None = field( + default=None, + metadata={"help": "The number of processes to use for data processing"}, + ) + relora_steps: Optional[int] = field( + default=None, + metadata={"help": "how often to reset for ReLoRA"}, + ) + relora_warmup_steps: Optional[int] = field( + default=None, + metadata={"help": "how many warmup steps to take after reset for ReLoRA"}, + ) + relora_anneal_steps: Optional[int] = field( + default=None, + metadata={"help": "how many warmup steps to take after reset for ReLoRA"}, + ) + relora_prune_ratio: Optional[float] = field( + default=0.9, + metadata={"help": "prune ratio for magnitude pruning of the optimizer"}, + ) + bench_split: Optional[str] = field( + default="eval", metadata={"help": "The benchmark split to run on"} + ) + bench_dataset: Optional[str] = field( + default="pharaouk/dharma-1/dharma_1_mini.json", + metadata={ + "help": "Benchmark dataset to use: options are `mmlu-zs`, `mmlu-fs`, or the full path to the dataset file" + }, + ) + do_bench_eval: Optional[bool] = field( + default=False, metadata={"help": "Whether to run the Benchmark evaluation."} + ) + do_causal_lm_eval: Optional[bool] = field( + default=False, metadata={"help": "Whether to run the Causal LM evaluation."} + ) + max_bench_samples: Optional[int] = field( + default=None, + metadata={ + "help": "If set, only evaluates on `max_bench_samples` of the benchmark dataset." + }, + ) + bench_source_max_len: int = field( + default=2048, metadata={"help": "Maximum source sequence length for bench."} + ) + dataloader_prefetch_factor: Optional[int] = field( + default=None, + metadata={"help": "prefetch_factor argument to the dataloader"}, + ) + cosine_min_lr_ratio: Optional[float] = field( + default=None, + metadata={"help": "Minimum learning rate is min_lr_ratio * learning_rate"}, + ) + cosine_constant_lr_ratio: Optional[float] = field( + default=None, + metadata={ + "help": "Starting constant learning rate step is cosine_constant_lr_ratio * max_steps" + }, + ) + loraplus_lr_ratio: Optional[float] = field( + default=None, metadata={"help": "loraplus learning rate ratio lr_B / lr_A."} + ) + loraplus_lr_embedding: Optional[float] = field( + default=1e-6, + metadata={"help": "loraplus learning rate for lora embedding layers."}, + ) + embedding_lr_scale: Optional[float] = field( + default=None, + metadata={"help": "Scale the learning rate for the embedding layers."}, + ) + lr_groups: Optional[list[dict]] = field( + default=None, + metadata={"help": "Specify learning rate groups for with different LRs."}, + ) + embedding_lr: Optional[float] = field( + default=None, + metadata={"help": "absolute learning rate for the embedding layers."}, + ) + qlora: bool = field( + default=False, + metadata={"help": "whether this is a qlora training"}, + ) + orpo_alpha: Optional[float] = field( + default=None, + ) + lisa_n_layers: Optional[int] = field( + default=None, + metadata={"help": "the number of activate layers in LISA"}, + ) + lisa_step_interval: Optional[int] = field( + default=None, + metadata={"help": "how often to switch layers in LISA"}, + ) + lisa_layers_attribute: Optional[str] = field( + default=None, + metadata={"help": "path under the model to access the layers"}, + ) + curriculum_sampling: Optional[bool] = field( + default=None, + metadata={"help": "whether to use sequential sampling for curriculum learning"}, + ) + alternate_lr_scheduler_type: Optional[str] = field( + default=None, + metadata={ + "help": "workaround to pass an alternate lr scheduler to the HF trainer" + }, + ) + chat_template: Optional[str] = field( + default=None, + metadata={"help": "Chat template converting chat messages to text"}, + ) + + # kd_ce_alpha: Optional[float] = field( + # default=None, + # metadata={ + # "help": "The alpha scaling parameter for SFT cross entropy loss when using KD" + # }, + # ) + # + # kd_alpha: Optional[float] = field( + # default=1.0, + # metadata={"help": "The alpha scaling parameter for KD loss"}, + # ) + # + # kd_temperature: Optional[float] = field( + # default=1.0, + # metadata={ + # "help": "the temperature parameter for KL divergence loss when using KD" + # }, + # ) + + adam_beta3: Optional[float] = field( + default=None, + metadata={ + "help": "The beta3 hyperparameter used in some optimizers such as CAME" + }, + ) + adam_epsilon2: Optional[float] = field( + default=None, + metadata={ + "help": "The epsilon2 hyperparameter used in some optimizers such as CAME" + }, + ) + + # multi-modal section + + image_size: int | tuple[int, int] | None = field( + default=None, + metadata={"help": "The size of the image to resize to"}, + ) + + image_resize_algorithm: Resampling | None = field( + default=None, + metadata={"help": "The algorithm to use for image resizing"}, + ) + + # end of multi-modal section diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 0edc9fdea2..9162bc7458 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -22,6 +22,7 @@ import collections import importlib +import traceback from typing import TYPE_CHECKING, Callable, OrderedDict, Union from peft import PeftModel @@ -83,6 +84,11 @@ def register(self, cfg: DictDefault): # pylint: disable=unused-argument def get_input_args(self) -> str | None: """Returns a pydantic model for the plugin's input arguments.""" + def get_training_args_mixin(self) -> str | None: + """ + Returns a dataclass model for the plugin's training arguments. + """ + def load_datasets( self, cfg: DictDefault, preprocess: bool = False ) -> Union["TrainDatasetMeta", None]: @@ -158,6 +164,31 @@ def post_trainer_create(self, cfg: DictDefault, trainer: Trainer): trainer: The trainer object for training. """ + def get_training_args(self, cfg: DictDefault): # pylint: disable=unused-argument): + """ + Returns custom training arguments to set on TrainingArgs. + + Args: + cfg: The global axolotl configuration. + + Returns: + object: dict containing the training arguments. + """ + + def get_collator_cls_and_kwargs( + self, cfg: DictDefault, is_eval: bool = False + ): # pylint: disable=unused-argument): + """ + Returns a custom class for the collator. + + Args: + cfg: The global axolotl configuration. + is_eval: Whether this is an eval split. + + Returns: + class: The class for the collator. + """ + # pylint: disable=unused-argument def create_optimizer(self, cfg: DictDefault, trainer: Trainer) -> Optimizer | None: """Creates and returns an optimizer for training. @@ -278,7 +309,7 @@ class from the module, and creates an instance of the class. return plugin -class PluginManager: +class PluginManager: # pylint: disable=too-many-public-methods """The `PluginManager` class is responsible for loading and managing plugins. It should be a singleton so it can be accessed from anywhere in the codebase. @@ -337,8 +368,11 @@ def register(self, plugin_name: str): plugin = load_plugin(plugin_name) self.plugins[plugin_name] = plugin LOG.info(f"Plugin loaded successfully: {plugin_name}") - except ImportError: + except ImportError as exc: LOG.error(f"Failed to load plugin: {plugin_name}") + # print stacktrace + traceback.print_exc() + print(f"Error: {exc}") def get_input_args(self) -> list[str]: """Returns a list of Pydantic classes for all registered plugins' input arguments.' @@ -353,6 +387,20 @@ def get_input_args(self) -> list[str]: input_args.append(input_args_from_plugin) return input_args + def get_training_args_mixin(self): + """ + Returns a list of dataclasses for all registered plugins' training args mixins' + + Returns: + list[str]: A list of dataclsses + """ + training_args = [] + for plugin in self.plugins.values(): + training_args_from_plugin = plugin.get_training_args_mixin() + if training_args_from_plugin is not None: + training_args.append(training_args_from_plugin) + return training_args + def load_datasets( self, cfg: DictDefault, preprocess: bool = False ) -> Union["TrainDatasetMeta", None]: @@ -442,6 +490,42 @@ def get_trainer_cls(self, cfg: DictDefault) -> Trainer | None: return trainer_cls return None + def get_training_args(self, cfg): + """ + Calls the get_training_args method of all registered plugins and returns the combined training arguments. + + Parameters: + cfg (dict): The configuration for the plugins. + + Returns: + object: The training arguments + """ + training_args_kwargs = {} + for plugin in self.plugins.values(): + training_args = plugin.get_training_args(cfg) + if training_args is not None: + training_args_kwargs.update(training_args) + + return training_args_kwargs + + def get_collator_cls_and_kwargs(self, cfg, is_eval=False): + """ + Calls the get_collator_cls_and_kwargs method of all registered plugins and returns the first non-None collator class. + + Parameters: + cfg (dict): The configuration for the plugins. + is_eval (bool): Whether this is an eval split. + + Returns: + object: The collator class, or None if none was found. + """ + for plugin in self.plugins.values(): + collator = plugin.get_collator_cls_and_kwargs(cfg, is_eval=is_eval) + if collator is not None: + collator_cls, collator_kwargs = collator + return collator_cls, collator_kwargs + return None + def post_trainer_create(self, cfg: DictDefault, trainer: Trainer): """Calls the `post_trainer_create` method of all registered plugins. diff --git a/src/axolotl/integrations/config.py b/src/axolotl/integrations/config.py index b443f228e4..f5fc07e9e2 100644 --- a/src/axolotl/integrations/config.py +++ b/src/axolotl/integrations/config.py @@ -16,7 +16,7 @@ This was moved here to prevent circular imports. """ -from typing import Any, Dict, List +from typing import Any, Dict, List, Type from axolotl.utils.schemas.config import ( AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, @@ -61,3 +61,43 @@ def merge_input_args(): ] return AxolotlConfigWCapabilities, AxolotlInputConfig return AxolotlConfigWCapabilitiesBase, AxolotlInputConfigBase + + +def merge_training_args() -> Type: + """ + Merges training arguments from registered plugins with the base TrainingArguments. + + This function retrieves the training arguments from registered plugins using the PluginManager. + It then dynamically creates new classes, AxolotlTrainingMixins, + that inherit from the base configurations and include the training arguments from the plugins. + + Returns: + tuple: A tuple containing the newly created classes, AxolotlTrainingMixins. + """ + # pylint: disable=duplicate-code + from axolotl.core.training_args_base import ( + AxolotlTrainingMixins as AxolotlTrainingMixinsBase, + ) + from axolotl.integrations.base import PluginManager + + plugin_manager = PluginManager.get_instance() + training_args_mixins: List[str] = plugin_manager.get_training_args_mixin() + mixin_classes = [] + dynamic_input = "" + for plugin_args in training_args_mixins: + plugin_module, plugin_cls = plugin_args.rsplit(".", 1) + dynamic_input += f"from {plugin_module} import {plugin_cls}\n" + mixin_classes.append(plugin_cls) + if dynamic_input: + dynamic_input += f"class AxolotlTrainingMixins(AxolotlTrainingMixinsBase, {', '.join(mixin_classes)}):\n pass\n" + + namespace: Dict[Any, Any] = {} + local_vars = {"AxolotlTrainingMixinsBase": AxolotlTrainingMixinsBase} + exec( # pylint: disable=exec-used # nosec B102 + dynamic_input, {**globals(), **local_vars}, namespace + ) + AxolotlTrainingMixins = namespace[ # pylint: disable=invalid-name + "AxolotlTrainingMixins" + ] + return AxolotlTrainingMixins + return AxolotlTrainingMixinsBase diff --git a/src/axolotl/integrations/kd/__init__.py b/src/axolotl/integrations/kd/__init__.py index 8a6e3eda13..4c8535a0a1 100644 --- a/src/axolotl/integrations/kd/__init__.py +++ b/src/axolotl/integrations/kd/__init__.py @@ -15,7 +15,12 @@ """ Plugin init to add KD support to Axolotl. """ +from typing import Any + +from transformers import Trainer + from axolotl.integrations.base import BasePlugin +from axolotl.integrations.kd.callbacks import KDTemperatureSchedulerCallback from .args import KDArgs # pylint: disable=unused-import. # noqa: F401 @@ -28,9 +33,75 @@ class KDPlugin(BasePlugin): def get_input_args(self): return "axolotl.integrations.kd.KDArgs" + def get_training_args_mixin(self): + return "axolotl.integrations.kd.args.KDTrainingArgsMixin" + def get_trainer_cls(self, cfg): if cfg.kd_trainer: from .trainer import AxolotlKDTrainer return AxolotlKDTrainer return None + + def get_training_args(self, cfg): + return { + "kd_ce_alpha": cfg.kd_ce_alpha, + "kd_alpha": cfg.kd_alpha, + "kd_temperature": cfg.kd_temperature, + "kd_beta": cfg.kd_beta, + "kd_normalize_topk": cfg.kd_normalize_topk, + } + + def get_collator_cls_and_kwargs(self, cfg, is_eval=False): + if not cfg.kd_trainer: + return None, None + + from .collator import DataCollatorForKD, KDBatchSamplerDataCollatorForSeq2Seq + + use_batch_sampler_collator = False + if is_eval is False and cfg.sample_packing: + use_batch_sampler_collator = True + if cfg.eval_sample_packing and is_eval: + use_batch_sampler_collator = True + + if cfg.kd_online_server_base_url: + from .collator_online_teacher import OnlineTeacherCollator + + return OnlineTeacherCollator, { + "kd_online_server_base_url": cfg.kd_online_server_base_url, + "kd_online_topk": cfg.kd_online_topk, + "kd_temperature": cfg.kd_temperature, + "kd_online_server": cfg.kd_online_server, + "kd_online_timeout": cfg.kd_online_timeout, + "kd_normalize_topk": cfg.kd_normalize_topk, + } + + if use_batch_sampler_collator: + return KDBatchSamplerDataCollatorForSeq2Seq, {} + return DataCollatorForKD, {} + + def pre_model_load(self, cfg): + from .kernels.models import apply_kernel + + apply_kernel(cfg.model_config_type) + + def add_callbacks_post_trainer(self, cfg: Any, trainer: Trainer) -> list: + """ + Adds temp scheduler callback to the Trainer instance. + + Args: + cfg (Any): Configuration object containing the sparse recipe. + trainer (Trainer): Huggingface Trainer instance. + + Returns: + list: List containing the configured callback instances. + """ + if cfg.kd_temperature_min is not None and cfg.kd_online_server_base_url: + callback = KDTemperatureSchedulerCallback( + cfg.kd_temperature, + cfg.kd_temperature_min, + trainer, + ) + return [callback] + + return [] diff --git a/src/axolotl/integrations/kd/args.py b/src/axolotl/integrations/kd/args.py index 2fbba2c6ae..758bc89178 100644 --- a/src/axolotl/integrations/kd/args.py +++ b/src/axolotl/integrations/kd/args.py @@ -15,9 +15,19 @@ """ Plugin args for KD support. """ -from typing import Optional +from dataclasses import dataclass +from enum import Enum -from pydantic import BaseModel +from pydantic import BaseModel, Field + + +class InferenceServerType(str, Enum): + """ + Online inferences server types to handle different request args + """ + + vllm = "vllm" # pylint: disable=invalid-name + sglang = "sglang" # pylint: disable=invalid-name class KDArgs(BaseModel): @@ -25,13 +35,41 @@ class KDArgs(BaseModel): Input args for knowledge distillation. """ - kd_trainer: Optional[bool] = None # whether to use KD trainer - kd_ce_alpha: Optional[float] = ( + kd_trainer: float | None = None # whether to use KD trainer + kd_ce_alpha: float | None = ( + None # loss coefficient for cross-entropy loss during KD + ) + kd_alpha: float | None = None # loss coefficient for KD loss + kd_temperature: float | None = None # temperature for sampling during KD + kd_beta: float | None = 0.0 # beta coefficient for ratio of fwd and reverse KL + kd_normalize_topk: bool | None = ( + None # whether to normalize student logits during KD + ) + + # TODO online kd + kd_online_server_base_url: str | None = None + kd_online_topk: int | None = None + kd_online_server: InferenceServerType | None = Field( + default_factory=lambda: InferenceServerType.vllm + ) + kd_online_timeout: int | None = 120 + kd_temperature_min: float | None = ( + None # kd temperature scheduling during online kd + ) + + +@dataclass +class KDTrainingArgsMixin: + """ + Additional args for KD training. + """ + + kd_ce_alpha: float | None = ( None # loss coefficient for cross-entropy loss during KD ) - kd_alpha: Optional[float] = None # loss coefficient for KD loss - kd_temperature: Optional[float] = None # temperature for sampling during KD - kd_zscore_base_temp: Optional[float] = None # base temperature for zscore scaling - kd_top_k_before_softmax: Optional[bool] = ( - None # whether to sample top k before softmax during KD + kd_alpha: float | None = None # loss coefficient for KD loss + kd_temperature: float | None = None # temperature for sampling during KD + kd_beta: float | None = None # beta coefficient for ratio of fwd and reverse KL + kd_normalize_topk: float | None = ( + None # whether to normalize student logits during KD ) diff --git a/src/axolotl/integrations/kd/callbacks.py b/src/axolotl/integrations/kd/callbacks.py new file mode 100644 index 0000000000..911c3d5176 --- /dev/null +++ b/src/axolotl/integrations/kd/callbacks.py @@ -0,0 +1,36 @@ +""" +Transformers trainer callbacks to schedule the KD temperature during training +""" + +import math + +from transformers.trainer_callback import TrainerCallback + + +class KDTemperatureSchedulerCallback(TrainerCallback): + """ + KD temperature scheduler callback for the trainer. + """ + + def __init__(self, temperature_start, temperature_min, trainer): + self.temperature_start = temperature_start + self.temperature_min = temperature_min + self.temperature = temperature_start + + self.trainer = trainer + + def on_step_end( + self, args, state, control, **kwargs + ): # pylint: disable=unused-argument + # cosine decay temperature over the max steps + + progress = state.global_step / state.max_steps + # Cosine decay factor: 0.5 * (1 + cos(pi * progress)) + # This factor goes from 1 (at progress=0) to 0 (at progress=1) + decay_factor = 0.5 * (1.0 + math.cos(math.pi * progress)) + self.temperature = self.temperature_start - ( + (self.temperature_start - self.temperature_min) * (1.0 - decay_factor) + ) + + if hasattr(self.trainer.data_collator, "kd_temperature"): + self.trainer.data_collator.kd_temperature = self.temperature diff --git a/src/axolotl/integrations/kd/chat_template.py b/src/axolotl/integrations/kd/chat_template.py index 7c99a9c3d7..f99dfe4586 100644 --- a/src/axolotl/integrations/kd/chat_template.py +++ b/src/axolotl/integrations/kd/chat_template.py @@ -15,12 +15,15 @@ """ Chat template prompt strategy loader with KD support """ +import logging from typing import Any, Dict import torch from axolotl.prompt_strategies.chat_template import ChatTemplateStrategy, StrategyLoader +LOG = logging.getLogger(__name__) + class ChatTemplateStrategyWithKD(ChatTemplateStrategy): """ @@ -101,10 +104,8 @@ def transform_logprobs(self, sample): # fill with -inf for padding_len tokens for top_k tokens # extend target_logprobs with a padding_len x top_k 2D list filled with -inf - # for causal models, if we start the range at 1, then we don't need to shift in the trainer - # otherwise, we need to shift in the trainer - shift = 0 - for _ in range(shift, input_padding_len): + # we shift for causal models in the trainer, so start the range from 0 + for _ in range(0, input_padding_len): target_logprobs.append([-float("inf")] * top_k) target_token_ids.append(list(range(top_k))) target_mask.append([0] * top_k) @@ -143,6 +144,10 @@ def transform_logprobs(self, sample): # # Convert from log to probability teacher_probs_t1 = position_logprobs_tensor.exp() + # normalize probabilities to sum to 1 in case they aren't already + teacher_probs_t1_sum = teacher_probs_t1.sum(dim=0, keepdim=True) + if teacher_probs_t1_sum > 1e-9: + teacher_probs_t1 = teacher_probs_t1 / teacher_probs_t1_sum if self.kd_temperature != self.gen_temperature: # Exponentiate by factor (T1 / T2) exponent = self.gen_temperature / self.kd_temperature @@ -162,12 +167,115 @@ def transform_logprobs(self, sample): target_logprobs.append(position_logprobs_scaled) target_token_ids.append(position_token_ids) - if shift == 1: - # since we started at index 1 for causal, we need one more padding token + # Update sample with transformed logprobs + sample["target_logprobs"] = target_logprobs + sample["target_token_ids"] = target_token_ids + sample["target_mask"] = target_mask + + return sample + + def _tokenize_single_prompt(self, prompt): + logprobs = prompt.pop(self.logprobs_field) + tokenized_prompt = super()._tokenize_single_prompt(prompt) + tokenized_prompt[self.logprobs_field] = logprobs + tokenized_prompt = self.transform_logprobs(tokenized_prompt) + + return tokenized_prompt + + +class ChatTemplateStrategyWithKDv2(ChatTemplateStrategyWithKD): + """ + Strat for datasets with complete structured KD logprob data + """ + + def transform_logprobs(self, sample): + """ + Transform logprobs to target format for KD training + """ + # pylint: disable=duplicate-code + + logprobs = sample.pop(self.logprobs_field) + target_seq_len = len(logprobs) + input_seq_len = len(sample["input_ids"]) + input_padding_len = input_seq_len - target_seq_len + # get non-zero top-k (prune None logprobs from vllm data step) + top_k_vals = [ + len(logprobs[i]) + for i in range(len(logprobs)) + if logprobs[i] is not None and len(logprobs[i]) + ] + max_top_k = max(set(top_k_vals), key=top_k_vals.count) + min_top_k = min(set(top_k_vals), key=top_k_vals.count) + top_k = min(max_top_k, min_top_k) + if top_k == 0: + raise ValueError("No non-zero top-k logprobs found.") + + target_logprobs = [] + target_token_ids = [] + target_mask = [] + + if input_padding_len < 0: + # logprobs is longer than target_seq_len, + # so we need to slice from the left/beginning of logprobs + logprobs = logprobs[:-input_seq_len] + input_padding_len = 0 + # target_seq_len = input_seq_len + + # truncate the second dimension of the logprobs to top_k + logprobs = [row[:top_k] for row in logprobs] + + # fill with -inf for padding_len tokens for top_k tokens + # extend target_logprobs with a padding_len x top_k 2D list filled with -inf + + # we shift for causal models in the trainer, so start the range from 0 + for _ in range(0, input_padding_len): target_logprobs.append([-float("inf")] * top_k) target_token_ids.append(list(range(top_k))) target_mask.append([0] * top_k) + for position in range(input_padding_len, input_seq_len): + if sample["labels"][position] == -100: + target_mask.append([0] * top_k) + else: + target_mask.append([1] * top_k) + + for token_pos_logprobs, pos_target_token_ids in zip( + logprobs, sample["target_token_ids"] + ): + # Convert to a tensor for easier manipulation + position_logprobs_tensor = torch.tensor( + token_pos_logprobs, dtype=torch.float + ) + + # Now we have distribution at T1 in log form, i.e. log p_{T1}(k). + # Next, re-scale to T2 = self.kd_temperature via exponent-based trick + # p_{T2}(k) = [p_{T1}(k)]^(T1 / T2) / Z + # + # Convert from log to probability + teacher_probs_t1 = position_logprobs_tensor.exp() + # normalize probabilities to sum to 1 in case they aren't already + teacher_probs_t1_sum = teacher_probs_t1.sum(dim=0, keepdim=True) + if teacher_probs_t1_sum > 1e-9: + teacher_probs_t1 = teacher_probs_t1 / teacher_probs_t1_sum + if self.kd_temperature != self.gen_temperature: + # Exponentiate by factor (T1 / T2) + exponent = self.gen_temperature / self.kd_temperature + teacher_probs_t2 = teacher_probs_t1**exponent + else: + teacher_probs_t2 = teacher_probs_t1 + # Re-normalize + teacher_probs_t2 = teacher_probs_t2 / teacher_probs_t2.sum( + dim=0, keepdim=True + ) + # Convert back to log + position_logprobs_tensor = torch.log(teacher_probs_t2) + + # Now we have log p_{teacher, T2}(k) stored in position_logprobs_tensor + position_logprobs_scaled = position_logprobs_tensor.tolist() + + target_logprobs.append(position_logprobs_scaled) + target_token_ids.append(pos_target_token_ids) + # Update sample with transformed logprobs sample["target_logprobs"] = target_logprobs sample["target_token_ids"] = target_token_ids @@ -177,8 +285,10 @@ def transform_logprobs(self, sample): def _tokenize_single_prompt(self, prompt): logprobs = prompt.pop(self.logprobs_field) + target_token_ids = prompt.pop("target_token_ids") tokenized_prompt = super()._tokenize_single_prompt(prompt) tokenized_prompt[self.logprobs_field] = logprobs + tokenized_prompt["target_token_ids"] = target_token_ids tokenized_prompt = self.transform_logprobs(tokenized_prompt) return tokenized_prompt @@ -204,4 +314,14 @@ def _get_strategy_params(self, cfg, ds_cfg: Dict[str, Any]): return strategy_params -load = KDStrategyLoader() +class KDStrategyLoaderV2(KDStrategyLoader): + """ + Load KD chat template datasets with pre-tokenized logprob data + """ + + def _get_strategy_cls(self, cfg): # pylint: disable=unused-argument + return ChatTemplateStrategyWithKDv2 + + +load_legacy = KDStrategyLoader() +load = KDStrategyLoaderV2() diff --git a/src/axolotl/integrations/kd/collator.py b/src/axolotl/integrations/kd/collator.py index de63869c71..0cc745b788 100644 --- a/src/axolotl/integrations/kd/collator.py +++ b/src/axolotl/integrations/kd/collator.py @@ -47,11 +47,16 @@ class DataCollatorForKD(DataCollatorForSeq2Seq): position_pad_token_id: int = 0 return_tensors: str = "pt" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.tokenizer.deprecation_warnings["Asking-to-pad-a-fast-tokenizer"] = True + def __call__(self, features, return_tensors=None): if return_tensors is None: return_tensors = self.return_tensors padding_side = self.tokenizer.padding_side + max_len = 0 # Pad labels and position_ids first for feature_name, pad_token_id in [ @@ -102,7 +107,9 @@ def __call__(self, features, return_tensors=None): target_mask_list.append(f.pop("target_mask")) # Determine max lengths - max_teacher_seq_len = max(len(seq) for seq in target_logprobs_list) + max_teacher_seq_len = max_len or max( + len(seq) for seq in target_logprobs_list + ) max_k = max(len(seq_k) for seq in target_logprobs_list for seq_k in seq) padded_target_logprobs = [] @@ -209,7 +216,9 @@ def __call__(self, features, return_tensors=None): # We want to produce a single "merged" feature dict for each sub-batch. out_features = [{} for _ in features] - for i, sub_features in enumerate(features): + for i, sub_features in enumerate( # pylint: disable=too-many-nested-blocks + features + ): # sub_features is a list of dicts, each dict = one sequence’s features # We'll merge them into out_features[i]. # @@ -243,10 +252,17 @@ def __call__(self, features, return_tensors=None): # For example, input_ids or labels are often arrays. arrays = [] for feat in sub_features: - if field_name in feat: + if field_name in feat and isinstance( + feat[field_name], (list, torch.Tensor) + ): + if isinstance( + feat[field_name][0], (dict, str) + ): # pylint: disable=too-many-nested-blocks + continue arr = np.array(feat[field_name]) arrays.append(arr) - out_features[i][field_name] = np.concatenate(arrays) + if arrays: + out_features[i][field_name] = np.concatenate(arrays) # 3) Now call the parent collator, which will do: # - padding of labels/position_ids diff --git a/src/axolotl/integrations/kd/collator_online_teacher.py b/src/axolotl/integrations/kd/collator_online_teacher.py new file mode 100644 index 0000000000..584ace481d --- /dev/null +++ b/src/axolotl/integrations/kd/collator_online_teacher.py @@ -0,0 +1,561 @@ +""" +Packed data loader for online teacher training supporting vllm and sglang. +""" + +import hashlib +import hmac +import logging +from typing import Any, Dict, List, Optional + +import requests +import torch +from orjson import orjson + +from axolotl.integrations.kd.collator import KDBatchSamplerDataCollatorForSeq2Seq +from axolotl.integrations.kd.utils import normalize_logprobs +from axolotl.utils.data.utils import retry_on_request_exceptions + +LOG = logging.getLogger(__name__) + + +def hmac_sha_from_int_list(int_list, key, hash_func=hashlib.sha256): + """ + Create HMAC-SHA hash from a list of integers + + Args: + int_list: List of integers + key: Secret key (string or bytes) + hash_func: Hash function (default: sha256) + + Returns: + HMAC digest as hex string + """ + # Convert key to bytes if it's a string + if isinstance(key, str): + key = key.encode("utf-8") + + # Convert list of ints to bytes + # Method 1: Convert each int to bytes and concatenate + data = b"".join(i.to_bytes(4, byteorder="big") for i in int_list) + + # Create HMAC + h = hmac.new(key, data, hash_func) + return h.hexdigest() + + +class OnlineTeacherCollator(KDBatchSamplerDataCollatorForSeq2Seq): + """ + Collator for online teacher training. + """ + + DEFAULT_LABEL_PAD_TOKEN_ID: int = -100 + + def __init__( + self, + *args: Any, + kd_online_server_base_url: Optional[str] = None, + kd_online_topk: Optional[int] = None, + kd_temperature: Optional[float] = 1.0, + kd_online_server: Optional[str] = "vllm", + kd_online_timeout: Optional[int] = 120, + kd_cache_dir: Optional[str] = None, + kd_normalize_topk: Optional[bool] = True, + **kwargs: Any, + ): + super().__init__(*args, **kwargs) + + if kd_online_server_base_url is None: + raise ValueError( + "kd_online_server_base_url must be provided for OnlineTeacherDataloader" + ) + if kd_online_topk is None or kd_online_topk <= 0: + raise ValueError( + "kd_online_topk must be a positive integer for OnlineTeacherDataloader" + ) + + self.kd_online_server_base_url = kd_online_server_base_url.rstrip("/") + self.kd_online_topk = kd_online_topk + self.kd_temperature = kd_temperature + self.kd_online_server = kd_online_server + self.http_session = requests.Session() + self.kd_online_timeout = kd_online_timeout + self.kd_cache_dir = kd_cache_dir + self.kd_normalize_topk = kd_normalize_topk + + def _normalize_logprobs(self, raw_logprobs: List[float]) -> List[float]: + """ + Re-normalizes top-k raw logprobs as probabilities, and converts back to logprobs. + """ + if not raw_logprobs or self.kd_online_topk == 0: + return ( + [-float("inf")] * self.kd_online_topk if self.kd_online_topk > 0 else [] + ) + + raw_logprobs_tensor = torch.tensor(raw_logprobs, dtype=torch.float32) + return normalize_logprobs(raw_logprobs_tensor, self.kd_online_topk).tolist() + + @retry_on_request_exceptions(max_retries=10, delay=5) + def fetch_online_logprobs_sglang( + self, batch_input_ids: List[List[int]], labels: List[List[int]] + ): + """ + Fetches logprobs from an online teacher served by sglang for a batch of input_ids. + Assumes API returns token IDs as strings in logprob dictionary keys. + """ + api_endpoint = f"{self.kd_online_server_base_url}/generate" + + payload = { + "input_ids": batch_input_ids, + "return_logprob": True, + "top_logprobs_num": self.kd_online_topk, + "logprob_start_len": 0, + "return_text_in_logprobs": True, + "echo": True, + "sampling_params": { + "max_new_tokens": 0, + "temperature": self.kd_temperature, + "skip_special_tokens": False, + }, + } + + # Initialize with empty lists, so if API call fails, these are returned. + ret_data_target_token_ids: List[List[List[int]]] = [] + ret_data_target_logprobs: List[List[List[float]]] = [] + ret_data_target_mask: List[List[List[int]]] = [] + + try: + response = self.http_session.post( + api_endpoint, json=payload, timeout=self.kd_online_timeout + ) + response.raise_for_status() + api_data: list[dict] = response.json() + + # Ensure api_data is a list, and its length matches batch_input_ids + if not isinstance(api_data, list) or len(api_data) != len(batch_input_ids): + LOG.error( + f"API response format error. Expected a list of {len(batch_input_ids)} " + f"items, got {type(api_data)} with length {len(api_data) if isinstance(api_data, list) else 'N/A'}." + ) + # Return empty data; items processed later will get default empty KD fields + return { + "target_token_ids": ret_data_target_token_ids, + "target_logprobs": ret_data_target_logprobs, + "target_mask": ret_data_target_mask, + } + + for sequence_data, seq_input_ids, seq_labels in zip( + api_data, batch_input_ids, labels + ): + current_target_logprobs = [] + current_target_token_ids = [] + current_target_mask = [] + + meta_info = sequence_data.pop("meta_info", {}) + # Ensure input_top_logprobs is a list + input_top_logprobs: Optional[list[None | list[tuple]]] = meta_info.pop( + "input_top_logprobs", [] + ) + if not isinstance(input_top_logprobs, list): + LOG.warning( + f"Received non-list input_top_logprobs: {input_top_logprobs}. Skipping sequence." + ) + input_top_logprobs = [] # Treat as empty + + # basic check that the logprob data len matches the input len, so no need to handle padding + assert len(seq_input_ids) == len(input_top_logprobs) + + for i, _, label in zip( + range(len(seq_input_ids)), seq_input_ids, seq_labels + ): + if i < len(input_top_logprobs) and input_top_logprobs[i] is None: + # this is always the case for the first token. + # there is never logprob data for the first token since that's a true input + # so we replace the None value with padding data + current_target_logprobs.append( + [-float("inf")] * self.kd_online_topk + ) + current_target_token_ids.append([0] * self.kd_online_topk) + current_target_mask.append([0] * self.kd_online_topk) + elif ( + i < len(input_top_logprobs) + and input_top_logprobs[i] is not None + ): + pos_top_logprobs_data = input_top_logprobs[i] + # Ensure pos_top_logprobs_data is a list of lists as expected + if not ( + isinstance(pos_top_logprobs_data, list) + and all( + isinstance(item, list) for item in pos_top_logprobs_data + ) + and len(pos_top_logprobs_data) > 0 + and len(pos_top_logprobs_data[0]) == 3 + ): # [logprob, token_id, token_str] + LOG.warning( + f"Malformed pos_top_logprobs_data: {pos_top_logprobs_data}. Padding this position." + ) + current_target_logprobs.append( + [-float("inf")] * self.kd_online_topk + ) + current_target_token_ids.append([0] * self.kd_online_topk) + current_target_mask.append([0] * self.kd_online_topk) + continue + + # pos_top_logprobs: list of logprobs, pos_token_ids: list of token_ids + pos_logprobs_raw, pos_token_ids, _ = [ + list(row) for row in zip(*pos_top_logprobs_data) + ] + + # Ensure correct length (top_k) + if len(pos_logprobs_raw) < self.kd_online_topk: + pad_len = self.kd_online_topk - len(pos_logprobs_raw) + pos_logprobs_raw.extend([-float("inf")] * pad_len) + pos_token_ids.extend([0] * pad_len) # Pad with 0 token_id + + # truncate to top_k in case the response was longer + current_target_token_ids.append( + pos_token_ids[: self.kd_online_topk] + ) + + if self.kd_normalize_topk: + normalized_logprobs_for_position = self._normalize_logprobs( + pos_logprobs_raw[: self.kd_online_topk] + ) + current_target_logprobs.append( + normalized_logprobs_for_position + ) + else: + current_target_logprobs.append( + pos_logprobs_raw[: self.kd_online_topk] + ) + + # Mask depends on the corresponding label for the student + if label == self.DEFAULT_LABEL_PAD_TOKEN_ID: + current_target_mask.append([0] * self.kd_online_topk) + else: + current_target_mask.append([1] * self.kd_online_topk) + else: + # Pad if no logprobs for this position (either due to length mismatch or None entry) + current_target_logprobs.append( + [-float("inf")] * self.kd_online_topk + ) + current_target_token_ids.append([0] * self.kd_online_topk) + current_target_mask.append([0] * self.kd_online_topk) + + ret_data_target_token_ids.append(current_target_token_ids) + ret_data_target_logprobs.append(current_target_logprobs) + ret_data_target_mask.append(current_target_mask) + + except requests.exceptions.RequestException as e: + LOG.error(f"Error fetching logprobs from online teacher: {e}") + raise e + # ret_logprobs_data will be returned with empty lists, handled by the caller. + except Exception as e: # Catch other potential errors during processing + LOG.error( + f"Unexpected error processing API response in fetch_online_logprobs: {e}", + exc_info=True, + ) + raise e + + return { + "target_token_ids": ret_data_target_token_ids, + "target_logprobs": ret_data_target_logprobs, + "target_mask": ret_data_target_mask, + } + + @retry_on_request_exceptions(max_retries=10, delay=5) + def fetch_online_logprobs_vllm( + self, batch_input_ids: List[List[int]], labels: List[List[int]] + ): + """ + Fetches logprobs from an online teacher served by vllm for a batch of input_ids. + Assumes API returns token IDs as strings in logprob dictionary keys. + """ + api_endpoint = f"{self.kd_online_server_base_url}/v1/completions" + + payload = { + "prompt": batch_input_ids, + "echo": True, + "logprobs": True, + "prompt_logprobs": self.kd_online_topk, + "top_logprobs": self.kd_online_topk, + "max_new_tokens": 0, + "skip_special_tokens": False, + "temperature": self.kd_temperature, + "sampling_params": { + "max_tokens": 0, + }, + } + + # Initialize with empty lists, so if API call fails, these are returned. + ret_data_target_token_ids: List[List[List[int]]] = [] + ret_data_target_logprobs: List[List[List[float]]] = [] + ret_data_target_mask: List[List[List[int]]] = [] + + try: + headers = {"Accept-Encoding": "deflate, gzip, br, zstd"} + response = self.http_session.post( + api_endpoint, + json=payload, + headers=headers, + timeout=self.kd_online_timeout, + ) + response.raise_for_status() + api_data: dict = orjson.loads(response.content) + choices: list[dict] = api_data["choices"] + + # Ensure api_data is a list, and its length matches batch_input_ids + if not isinstance(choices, list) or len(choices) != len(batch_input_ids): + LOG.error( + f"API response format error. Expected a list of {len(batch_input_ids)} " + f"items, got {type(api_data)} with length {len(api_data) if isinstance(api_data, list) else 'N/A'}." + ) + # Return empty data; items processed later will get default empty KD fields + return { + "target_token_ids": ret_data_target_token_ids, + "target_logprobs": ret_data_target_logprobs, + "target_mask": ret_data_target_mask, + } + + for sequence_data, seq_input_ids, seq_labels in zip( + choices, batch_input_ids, labels + ): + # seq_input_ids: List[int] + # seq_labels: List[int] + + current_target_logprobs = [] + current_target_token_ids = [] + current_target_mask = [] + + # Ensure input_top_logprobs is a list + input_top_logprobs: Optional[list[None | dict[str, dict]]] = ( + sequence_data.pop("prompt_logprobs", []) + ) + + if not isinstance(input_top_logprobs, list): + LOG.warning( + f"Received non-list input_top_logprobs: {input_top_logprobs}. Skipping sequence." + ) + input_top_logprobs = [] # Treat as empty + + # basic check that the logprob data len matches the input len, so no need to handle padding + assert len(seq_input_ids) == len(input_top_logprobs) + + seq_len = len(seq_input_ids) + + for i, _, label in zip(range(seq_len), seq_input_ids, seq_labels): + if i < len(input_top_logprobs) and input_top_logprobs[i] is None: + # this is always the case for the first token. + # there is never logprob data for the first token since that's a true input + continue + if ( + i < len(input_top_logprobs) + and input_top_logprobs[i] is not None + ): + pos_top_logprobs_data: dict[str, dict] = input_top_logprobs[i] # type: ignore[assignment] + # Ensure pos_top_logprobs_data is a list of lists as expected + if not ( + isinstance(pos_top_logprobs_data, dict) + and all( + isinstance(item, dict) + for item in pos_top_logprobs_data.values() + ) + and len(pos_top_logprobs_data.keys()) > 0 + ): # [logprob, token_id, token_str] + LOG.warning( + f"Malformed pos_top_logprobs_data: {pos_top_logprobs_data}. Padding this position." + ) + current_target_logprobs.append( + [-float("inf")] * self.kd_online_topk + ) + current_target_token_ids.append( + list(range(self.kd_online_topk)) + ) + current_target_mask.append([0] * self.kd_online_topk) + continue + + # pos_top_logprobs: list of logprobs, pos_token_ids: list of token_ids + pos_token_ids_str = list(pos_top_logprobs_data.keys()) + pos_logprobs_dict = pos_top_logprobs_data.values() + pos_token_ids = [ + int(token_id) for token_id in pos_token_ids_str + ] + pos_logprobs_raw = [ + float(logprob.get("logprob", -float("inf"))) + for logprob in pos_logprobs_dict + ] + + # Ensure correct length (top_k) + if len(pos_logprobs_raw) < self.kd_online_topk: + pad_len = self.kd_online_topk - len(pos_logprobs_raw) + LOG.warning( + f"Padding position {i} with {pad_len} top-k tokens and logprobs." + ) + pos_logprobs_raw.extend([-float("inf")] * pad_len) + pos_token_ids.extend([0] * pad_len) # Pad with 0 token_id + + # truncate to top_k in case the response was longer + current_target_token_ids.append( + pos_token_ids[: self.kd_online_topk] + ) + + if self.kd_normalize_topk: + normalized_logprobs_for_position = self._normalize_logprobs( + pos_logprobs_raw[: self.kd_online_topk] + ) + current_target_logprobs.append( + normalized_logprobs_for_position + ) + else: + current_target_logprobs.append( + pos_logprobs_raw[: self.kd_online_topk] + ) + + # Mask depends on the corresponding label for the student + if label == self.DEFAULT_LABEL_PAD_TOKEN_ID: + current_target_mask.append([0] * self.kd_online_topk) + else: + current_target_mask.append([1] * self.kd_online_topk) + else: + # Pad if no logprobs for this position (either due to length mismatch or None entry) + current_target_logprobs.append( + [-float("inf")] * self.kd_online_topk + ) + current_target_token_ids.append( + list(range(self.kd_online_topk)) + ) + current_target_mask.append([0] * self.kd_online_topk) + for i in range(max(0, seq_len - len(current_target_logprobs))): + current_target_logprobs.append( + [-float("inf")] * self.kd_online_topk + ) + current_target_token_ids.append(list(range(self.kd_online_topk))) + current_target_mask.append([0] * self.kd_online_topk) + + ret_data_target_token_ids.append(current_target_token_ids) + ret_data_target_logprobs.append(current_target_logprobs) + ret_data_target_mask.append(current_target_mask) + + # TODO save and load targets to disk for caching for next epoch + # generate a hmac SHA256 hash over the list seq_input_ids and convert it to an int + # if self.kd_cache_dir: + # hash_input_ids = hmac_sha_from_int_list( + # seq_input_ids, f"{self.kd_online_server_base_url}:{self.kd_online_topk}" + # ) + # with open(f"{self.kd_cache_dir}/{hash_input_ids}.parquet", "wb") as f: + # pd.DataFrame(ret_logprobs_data).to_parquet(f, index=False) + + except requests.exceptions.RequestException as e: + LOG.error(f"Error fetching logprobs from online teacher: {e}") + raise e + # ret_logprobs_data will be returned with empty lists, handled by the caller. + except Exception as e: # Catch other potential errors during processing + LOG.error( + f"Unexpected error processing API response in fetch_online_logprobs: {e}", + exc_info=True, + ) + raise e + + return { + "target_token_ids": ret_data_target_token_ids, + "target_logprobs": ret_data_target_logprobs, + "target_mask": ret_data_target_mask, + } + + def __call__( + self, features: List[List[Dict[str, Any]]], return_tensors: Optional[str] = None + ) -> Dict[str, Any]: + if not features: + return super().__call__(features, return_tensors=return_tensors) + + for ( + sub_batch_features + ) in features: # sub_batch_features is List[Dict[str, Any]] + if not sub_batch_features: + continue + + input_ids_for_api_call: List[List[int]] = [] + labels_for_api_call: List[List[int]] = [] + # Store references to the original item dictionaries to update them in-place + items_for_api_call: List[Dict[str, Any]] = [] + + for item_dict in sub_batch_features: + if not isinstance(item_dict, dict): + LOG.warning( + f"Skipping non-dict item in sub_batch_features: {item_dict}" + ) + continue + + current_input_ids = item_dict.get("input_ids") + current_labels = item_dict.get("labels") + + if current_input_ids is not None and current_labels is not None: + # Ensure input_ids and labels are lists of ints for JSON serialization + input_ids_list = ( + current_input_ids.tolist() + if hasattr(current_input_ids, "tolist") + else list(current_input_ids) + ) + labels_list = ( + current_labels.tolist() + if hasattr(current_labels, "tolist") + else list(current_labels) + ) + + input_ids_for_api_call.append(input_ids_list) + labels_for_api_call.append(labels_list) + items_for_api_call.append(item_dict) + else: + # This item will not get teacher logprobs from the API. + # Initialize KD fields to empty lists so downstream collators handle them uniformly. + item_dict.setdefault("target_token_ids", []) + item_dict.setdefault("target_logprobs", []) + item_dict.setdefault("target_mask", []) + + # print(items_for_api_call) + if items_for_api_call: # Only call API if there's something to process + if self.kd_online_server == "sglang": + api_responses_for_sub_batch = self.fetch_online_logprobs_sglang( + input_ids_for_api_call, labels_for_api_call + ) + else: + api_responses_for_sub_batch = self.fetch_online_logprobs_vllm( + input_ids_for_api_call, labels_for_api_call + ) + + # api_responses_for_sub_batch has keys: "target_token_ids", "target_logprobs", "target_mask" + # Each value is a list, corresponding to items_for_api_call + for i, item_to_update in enumerate(items_for_api_call): + # TODO make sure to figure out which input in sub_batch_features to update the batch in the original `features` object so the super class can handle it properly. + if api_responses_for_sub_batch and i < len( + api_responses_for_sub_batch["target_token_ids"] + ): # Check bounds + assert len( + api_responses_for_sub_batch["target_token_ids"][i] + ) == len(item_to_update["input_ids"]) + assert len( + api_responses_for_sub_batch["target_logprobs"][i] + ) == len(item_to_update["input_ids"]) + assert len( + api_responses_for_sub_batch["target_mask"][i] + ) == len(item_to_update["labels"]) + item_to_update["target_token_ids"] = ( + api_responses_for_sub_batch["target_token_ids"][i] + ) + item_to_update["target_logprobs"] = api_responses_for_sub_batch[ + "target_logprobs" + ][i] + item_to_update["target_mask"] = api_responses_for_sub_batch[ + "target_mask" + ][i] + else: + # API call failed for this item, or response was shorter than expected. + # Ensure KD fields are initialized as empty lists. + LOG.warning( + f" (index {i}), or API response was too short. " + f"API response keys: {list(api_responses_for_sub_batch.keys()) if api_responses_for_sub_batch else 'None'}" + ) + item_to_update.setdefault("target_token_ids", []) + item_to_update.setdefault("target_logprobs", []) + item_to_update.setdefault("target_mask", []) + + return super().__call__(features, return_tensors=return_tensors) diff --git a/src/axolotl/integrations/kd/kernels/__init__.py b/src/axolotl/integrations/kd/kernels/__init__.py index e69de29bb2..3f1144a45c 100644 --- a/src/axolotl/integrations/kd/kernels/__init__.py +++ b/src/axolotl/integrations/kd/kernels/__init__.py @@ -0,0 +1,8 @@ +""" +Liger Chunked loss optimizations module +""" + +from .liger import LigerFusedLinearKLTopKLogprobLoss +from .models import apply_kernel + +__all__ = ["LigerFusedLinearKLTopKLogprobLoss", "apply_kernel"] diff --git a/src/axolotl/integrations/kd/kernels/liger.py b/src/axolotl/integrations/kd/kernels/liger.py new file mode 100644 index 0000000000..6356643c22 --- /dev/null +++ b/src/axolotl/integrations/kd/kernels/liger.py @@ -0,0 +1,485 @@ +""" +Liger Kernels for Chunked Top-K Log-Prob Distillation +""" + +import torch +import torch.nn.functional as F +from liger_kernel.chunked_loss.fused_linear_distillation import ( + LigerFusedLinearDistillationBase, +) + +from axolotl.integrations.kd.utils import normalize_logprobs + + +class LigerFusedLinearKLTopKLogprobFunction(LigerFusedLinearDistillationBase): + """ + Chunked kl-div loss for top-k logprobs + """ + + @staticmethod + def distillation_loss_fn( + student_logits_temp_scaled: torch.Tensor, # [chunk_size, vocab_size], already temp-scaled + target_token_ids_chunk: torch.Tensor, # [chunk_size, top_k] + target_logprobs_chunk: torch.Tensor, # [chunk_size, top_k], already temp-scaled and normalized logprobs + target_mask_chunk: torch.Tensor, # [chunk_size, top_k] + beta: float = 0.0, + normalize_topk: bool = True, + ) -> torch.Tensor: + """ + Compute Top-K KL divergence loss for a chunk. + Args: + student_logits_temp_scaled: Student logits, scaled by temperature. Shape: (N, V). + target_token_ids_chunk: Top-k teacher token IDs. Shape: (N, K). + target_logprobs_chunk: Top-k teacher log probabilities (temp-scaled, normalized). Shape: (N, K). + target_mask_chunk: Mask for valid top-k tokens. Shape: (N, K). + beta: Controls the type of KL divergence. + 0.0 for Forward KL (P_teacher || P_student). + 1.0 for Reverse KL (P_student || P_teacher). + 0.5 for Symmetric KL (average of Forward and Reverse). + normalize_topk: Whether to normalize the log probabilities + Returns: + Sum of KL divergence losses for the chunk. + """ + topk = target_token_ids_chunk.shape[-1] + student_logits_temp_scaled = ( # [chunk_size, vocab_size] + student_logits_temp_scaled.float() + ) + target_logprobs_chunk = target_logprobs_chunk.float() + + # Gather student logits for the top-k teacher token IDs + # target_token_ids_chunk: [chunk_size, top_k] + # student_logits_topk_temp_scaled: [chunk_size, top_k] + student_logits_topk_temp_scaled = torch.gather( + student_logits_temp_scaled, dim=-1, index=target_token_ids_chunk + ) + + # Student log-probabilities for the gathered top-k tokens + student_lse = torch.logsumexp( + student_logits_temp_scaled, dim=-1, keepdim=True + ) # [chunk_size, 1] + student_logprobs_topk_temp_scaled = ( + student_logits_topk_temp_scaled - student_lse + ) + + # we have the top-k student logprobs, normalize them + if normalize_topk: + student_logprobs_topk_temp_scaled = normalize_logprobs( + student_logprobs_topk_temp_scaled, topk + ) + + valid_mask = target_mask_chunk.to(torch.bool) # [chunk_size, top_k] + + student_logprobs_topk_valid = student_logprobs_topk_temp_scaled[valid_mask] + teacher_logprobs_valid = target_logprobs_chunk[valid_mask] + + # Teacher probabilities P(y|x_teacher) from logprobs + # target_logprobs_valid are already normalized (log(softmax(teacher_logits/T))) + teacher_probs_valid = teacher_logprobs_valid.exp() + # Student probabilities P_student from log P_student + student_probs_topk_valid = student_logprobs_topk_valid.exp() + + # kd_loss_per_token = torch.zeros_like(target_logprobs_valid) + + # KL divergence: sum(P_teacher * (log P_teacher - log P_student)) + # = sum(P_teacher * log P_teacher) - sum(P_teacher * log P_student) + # The distillation loss is often formulated as -sum(P_teacher * log P_student) + # or as sum(P_teacher * (log_softmax_teacher - log_softmax_student)) + # Here, target_logprobs_valid are log_softmax_teacher. + # student_logprobs_topk_valid are log_softmax_student (for the selected K indices). + if beta == 0.0: # Contribution from Forward KL + fwd_kl_per_token = teacher_probs_valid * ( + teacher_logprobs_valid - student_logprobs_topk_valid + ) + kd_loss = fwd_kl_per_token.sum() + elif beta == 1.0: # Contribution from Reverse KL + rev_kl_per_token = student_probs_topk_valid * ( + student_logprobs_topk_valid - teacher_logprobs_valid + ) + kd_loss = rev_kl_per_token.sum() + else: + # JSD - Jensen-Shannon Divergence / Symmetric + mean_probs = ( + 1 - beta + ) * student_probs_topk_valid + beta * teacher_probs_valid + log_mean_probs = mean_probs.log() + student_kl = F.kl_div( + log_mean_probs, + student_logprobs_topk_valid, + reduction="sum", + log_target=True, + ) + teacher_kl = F.kl_div( + log_mean_probs, teacher_logprobs_valid, reduction="sum", log_target=True + ) + jsd_loss = beta * teacher_kl + (1 - beta) * student_kl + kd_loss = jsd_loss + + return kd_loss + + @staticmethod + def _compute_loss_kl_topk( + student_input_chunk: torch.Tensor, + student_weight: torch.Tensor, + # Args for student_bias, target_token_ids_chunk etc. are passed to the lambda wrapped by grad_and_value + # or through `partial`. Let's make them explicit here for clarity. + target_token_ids_chunk: torch.Tensor, + target_logprobs_chunk: torch.Tensor, + target_mask_chunk: torch.Tensor, + target_chunk: torch.Tensor, # For hard loss (true labels) + student_bias: torch.Tensor = None, # This will be one of the grad targets + # Other params passed via `partial` from `forward` + distillation_loss_fn=None, + ignore_index: int = -100, + weight_hard_loss: float = 0.5, + weight_soft_loss: float = 0.5, + compute_ce_loss: bool = True, + temperature: float = 1.0, + beta: float = 0.0, + normalize_topk: bool = True, + ): + # Compute student logits for the chunk from hidden states and LM head + # student_input_chunk: [chunk_size, hidden_dim] + # student_lm_head_weight: [vocab_size, hidden_dim] + # student_logits_chunk: [chunk_size, vocab_size] + student_logits_chunk = F.linear( + student_input_chunk, student_weight, student_bias + ) + + ce_loss = torch.tensor( + 0.0, device=student_logits_chunk.device, dtype=student_logits_chunk.dtype + ) + if compute_ce_loss and weight_hard_loss > 0.0: + ce_loss = F.cross_entropy( + student_logits_chunk.view(-1, student_logits_chunk.shape[-1]), + target_chunk.view(-1), + reduction="sum", + ignore_index=ignore_index, + ) + + soft_loss = torch.tensor( + 0.0, device=student_logits_chunk.device, dtype=student_logits_chunk.dtype + ) + if weight_soft_loss > 0.0: + student_logits_chunk_temp_scaled = student_logits_chunk / temperature + + # Assuming student_weight.shape[0] (vocab_size) is adequate for target_token_ids_chunk.max() + # No explicit padding here; user must ensure vocab alignment or pre-pad student_weight. + + soft_loss = distillation_loss_fn( + student_logits_chunk_temp_scaled, + target_token_ids_chunk, + target_logprobs_chunk, + target_mask_chunk, + beta=beta, + normalize_topk=normalize_topk, + ) + + return soft_loss, ce_loss + + @classmethod + def forward( + cls, + ctx, + student_input: torch.Tensor, # [batch_size, seq_len, dim] + student_lm_head_weight: torch.Tensor, # [dim, vocab_size] + target_token_ids: torch.Tensor, # [batch_size, seq_len, top_k] + target_logprobs: torch.Tensor, # [batch_size, seq_len, top_k] + target_mask: torch.Tensor, # [batch_size, seq_len, top_k] + true_labels: torch.Tensor, # [batch_size, seq_len] + student_lm_head_bias: torch.Tensor = None, + weight_hard_loss: float = 0.5, + weight_soft_loss: float = 0.5, + ignore_index: int = -100, + temperature: float = 1.0, + beta: float = 0.0, + compiled: bool = False, + chunk_size: int = 1024, + compute_ce_loss: bool = True, + normalize_topk: bool = True, + ): + CHUNK_SIZE = chunk_size # pylint: disable=invalid-name + grad_weight_acc = torch.zeros_like(student_lm_head_weight) + grad_inputs_list = [] + grad_bias_acc = ( + torch.zeros_like(student_lm_head_bias) + if student_lm_head_bias is not None + else None + ) + kd_loss_acc = torch.zeros( + (), device=student_input.device, dtype=student_input.dtype + ) + ce_loss_acc = torch.zeros( + (), device=student_input.device, dtype=student_input.dtype + ) + + # This function will be what torch.func.grad_and_value differentiates. + # It takes student_input_chunk, student_weight (full), student_bias (full) as primals. + # Other necessary data (target_*, etc.) are passed as non-differentiable arguments. + def loss_fn_for_grad( + _student_input_chunk, + _student_lm_head_weight, # full weight + _student_lm_head_bias, # full bias + # Fixed arguments for a given chunk, not differentiated: + _target_token_ids_chunk, + _target_logprobs_chunk, + _target_mask_chunk, + _true_labels_chunk, + ): + return cls._compute_loss_kl_topk( + student_input_chunk=_student_input_chunk, + student_weight=_student_lm_head_weight, + target_token_ids_chunk=_target_token_ids_chunk, + target_logprobs_chunk=_target_logprobs_chunk, + target_mask_chunk=_target_mask_chunk, + target_chunk=_true_labels_chunk, + student_bias=_student_lm_head_bias, + distillation_loss_fn=cls.distillation_loss_fn, + ignore_index=ignore_index, + weight_hard_loss=weight_hard_loss, + weight_soft_loss=weight_soft_loss, + compute_ce_loss=compute_ce_loss, + temperature=temperature, + beta=beta, + normalize_topk=normalize_topk, + ) + + def accumulate_chunk_grads( + student_input_chunk_ac, + target_token_ids_chunk_ac, + target_logprobs_chunk_ac, + target_mask_chunk_ac, + true_labels_chunk_ac, + ): + # student_weight and student_bias are closed over from the outer scope (full tensors) + if student_lm_head_bias is not None: + ( + (chunk_grad_input, chunk_grad_weight, chunk_grad_bias), + (chunk_kd_loss, chunk_ce_loss), + ) = torch.func.grad_and_value( + loss_fn_for_grad, argnums=(0, 1, 2), has_aux=True + )( + student_input_chunk_ac, + student_lm_head_weight, + student_lm_head_bias, # primals + target_token_ids_chunk_ac, + target_logprobs_chunk_ac, + target_mask_chunk_ac, + true_labels_chunk_ac, + ) # non-primals + grad_bias_acc.add_(chunk_grad_bias) + else: + argnums_for_grad = (0, 1) # Differentiate wrt input_chunk, weight + ( + (chunk_grad_input, chunk_grad_weight), # No grad for bias + (chunk_kd_loss, chunk_ce_loss), + ) = torch.func.grad_and_value( + loss_fn_for_grad, argnums=argnums_for_grad, has_aux=True + )( + student_input_chunk_ac, + student_lm_head_weight, + None, # Pass None for student_bias primal + target_token_ids_chunk_ac, + target_logprobs_chunk_ac, + target_mask_chunk_ac, + true_labels_chunk_ac, + ) + + grad_weight_acc.add_(chunk_grad_weight) + kd_loss_acc.add_(chunk_kd_loss) + ce_loss_acc.add_(chunk_ce_loss) + + return chunk_grad_input + + if compiled: + accumulate_chunk_grads_compiled = torch.compile( + accumulate_chunk_grads, dynamic=True, backend="inductor" + ) # dynamic=True often helpful + else: + accumulate_chunk_grads_compiled = accumulate_chunk_grads + + # Use the same chunking logic as LigerFusedLinearDistillationBase.forward + B, N, D = student_input.shape # pylint: disable=invalid-name + K = target_token_ids.shape[-1] # pylint: disable=invalid-name + + student_input_flat = student_input.reshape(-1, student_input.shape[-1]) + target_token_ids_flat = target_token_ids.reshape(-1, target_token_ids.shape[-1]) + target_logprobs_flat = target_logprobs.reshape(-1, target_logprobs.shape[-1]) + target_mask_flat = target_mask.reshape(-1, target_mask.shape[-1]) + # pad and shift for cross entropy loss + true_labels = torch.nn.functional.pad(true_labels, (0, 1), value=ignore_index) + true_labels_flat = true_labels[:, 1:].contiguous().view(-1) + + num_chunks = max(1, student_input_flat.shape[0] // CHUNK_SIZE) + + _student_input_chunks = torch.chunk( + student_input_flat, chunks=num_chunks, dim=0 + ) + _target_token_ids_chunks = torch.chunk( + target_token_ids_flat, chunks=num_chunks, dim=0 + ) + _target_logprobs_chunks = torch.chunk( + target_logprobs_flat, chunks=num_chunks, dim=0 + ) + _target_mask_chunks = torch.chunk(target_mask_flat, chunks=num_chunks, dim=0) + _true_labels_chunks = torch.chunk(true_labels_flat, chunks=num_chunks, dim=0) + + for i in range(num_chunks): + grad_input_chunk = accumulate_chunk_grads_compiled( + _student_input_chunks[i], + _target_token_ids_chunks[i], + _target_logprobs_chunks[i], + _target_mask_chunks[i], + _true_labels_chunks[i], + ) + grad_inputs_list.append(grad_input_chunk) + + grad_inputs_combined = torch.cat(grad_inputs_list, dim=0) + ctx.save_for_backward(grad_inputs_combined, grad_weight_acc, grad_bias_acc) + + # For matching None returns in backward for non-tensor/non-grad_requiring inputs + ctx.hyperparams_count = 9 # Corresponds to number of hyperparams after main tensors in fwd signature + ctx.bias_was_none = student_lm_head_bias is None + ctx.orig_dims = (B, N, D, K) + + # since this is packed, there is simply a single batch, so batchmean reduction of kl-div is simply the accumulated sum + # we still need to scale the kd_loss by the temp^2 + kd_loss_acc = kd_loss_acc * (temperature**2) + final_loss = weight_soft_loss * kd_loss_acc + weight_hard_loss * ce_loss_acc + + return final_loss + + @staticmethod + def backward(ctx, grad_output): + grad_input_flat, grad_weight, grad_bias_maybe = ( + ctx.saved_tensors + ) # grad_input_flat is (B*N, D) + + # Scale gradients by grad_output if it's not 1.0 + if not torch.equal( + grad_output, + torch.tensor(1.0, device=grad_output.device, dtype=grad_output.dtype), + ): + grad_input_flat = grad_input_flat * grad_output + grad_weight = grad_weight * grad_output + if grad_bias_maybe is not None: + grad_bias_maybe = grad_bias_maybe * grad_output + + # Reshape grad_input_flat to match original student_input shape (B, N, D) + # ctx.orig_dims stores (B, N, D, K) + # We need the first three dimensions for student_input's shape. + # Ensure that orig_dims are not (0,0,0,K) for empty inputs leading to view errors + if ( + ctx.orig_dims[0] * ctx.orig_dims[1] * ctx.orig_dims[2] == 0 + and grad_input_flat.numel() == 0 + ): + # If original input was empty, gradient should also be empty with correct shape + grad_input_reshaped = torch.zeros( + ctx.orig_dims[0], + ctx.orig_dims[1], + ctx.orig_dims[2], + dtype=grad_input_flat.dtype, + device=grad_input_flat.device, + ) + elif grad_input_flat.numel() == 0 and not ( + ctx.orig_dims[0] * ctx.orig_dims[1] * ctx.orig_dims[2] == 0 + ): + # This case should ideally not happen if forward path is correct (non-empty input -> non-empty flat grad) + # but as a safeguard: + grad_input_reshaped = torch.zeros( + ctx.orig_dims[0], + ctx.orig_dims[1], + ctx.orig_dims[2], + dtype=grad_input_flat.dtype, + device=grad_input_flat.device, + ) + else: + grad_input_reshaped = grad_input_flat.view( + ctx.orig_dims[0], ctx.orig_dims[1], ctx.orig_dims[2] + ) + + nones_for_hyperparams = [None] * ctx.hyperparams_count + grad_bias_return = grad_bias_maybe if not ctx.bias_was_none else None + + return ( + grad_input_reshaped, # Gradient for student_input (reshaped) + grad_weight, # Gradient for student_lm_head_weight + None, # Gradient for target_token_ids + None, # Gradient for target_logprobs + None, # Gradient for target_mask + None, # Gradient for true_labels + grad_bias_return, # Gradient for student_lm_head_bias + *nones_for_hyperparams, # Grads for weight_hard_loss, ..., compute_ce_loss + ) + + +class LigerFusedLinearKLTopKLogprobLoss(torch.nn.Module): + """ + wrapper for chunked top-k logprob kl-d + """ + + def __init__( + self, + weight_hard_loss: float = 0.5, + weight_soft_loss: float = 0.5, + temperature: float = 1.0, # This is the kd_temperature + beta: float = 1.0, + ignore_index: int = -100, + compiled: bool = True, + chunk_size: int = 1024, + compute_ce_loss: bool = True, + normalize_topk: bool = True, + ): + super().__init__() + if not (0.0 <= weight_hard_loss <= 1.0 and 0.0 <= weight_soft_loss <= 1.0): + raise ValueError("Loss weights must be between 0.0 and 1.0.") + if temperature <= 0: + raise ValueError("Temperature must be positive.") + + self.weight_hard_loss = weight_hard_loss + self.weight_soft_loss = weight_soft_loss + self.temperature = temperature + self.beta = beta + self.ignore_index = ignore_index + self.compiled = compiled + self.chunk_size = chunk_size + self.compute_ce_loss = compute_ce_loss + self.normalize_topk = normalize_topk + + if not self.compute_ce_loss and self.weight_hard_loss > 0.0: + print( + f"Warning: compute_ce_loss is False, but weight_hard_loss ({self.weight_hard_loss}) > 0. Hard loss will effectively be zero." + ) + # self.weight_hard_loss = 0.0 # Or let user manage this + if self.weight_soft_loss == 0.0: + print( + "Warning: weight_soft_loss is 0.0. Soft (KD) loss will not be computed." + ) + + def forward( + self, + lm_head_weight: torch.Tensor, # Weights of the linear layer in the LM head + student_hidden_states: torch.Tensor, # student_hidden_states before the lm_head + target_token_ids: torch.Tensor, + target_logprobs: torch.Tensor, + target_mask: torch.Tensor, + true_labels: torch.Tensor, + student_bias: torch.Tensor = None, + ) -> torch.Tensor: + return LigerFusedLinearKLTopKLogprobFunction.apply( + student_hidden_states, + lm_head_weight, + target_token_ids, + target_logprobs, + target_mask, + true_labels, + student_bias, + self.weight_hard_loss, + self.weight_soft_loss, + self.ignore_index, + self.temperature, + self.beta, + self.compiled, + self.chunk_size, + self.compute_ce_loss, + self.normalize_topk, + ) diff --git a/src/axolotl/integrations/kd/kernels/models.py b/src/axolotl/integrations/kd/kernels/models.py new file mode 100644 index 0000000000..bfd7529641 --- /dev/null +++ b/src/axolotl/integrations/kd/kernels/models.py @@ -0,0 +1,98 @@ +""" +model patcher for chunked top-k kl-div +""" + +from types import MethodType +from typing import Optional, Union, Unpack + +import torch +from transformers import Cache +from transformers.modeling_flash_attention_utils import FlashAttentionKwargs +from transformers.modeling_outputs import CausalLMOutputWithPast +from transformers.utils import LossKwargs + + +class KwargsForCausalLM(FlashAttentionKwargs, LossKwargs): + """ + placeholder kwargs for hf model classes + """ + + +def kldiv_forward_llama_like( + self, + input_ids: Optional[torch.LongTensor] = None, + target_logprobs: Optional[torch.Tensor] = None, + target_token_ids: Optional[torch.LongTensor] = None, + target_mask: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, # pylint: disable=unused-argument + **kwargs: Unpack[KwargsForCausalLM], # type: ignore[misc] +) -> CausalLMOutputWithPast: + # pylint: disable=duplicate-code + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs.last_hidden_state + + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + # TODO, we can optimize this further by filtering hidden_states on sequence dimension using labels != -100 + # self.loss_function should be LigerFusedLinearKLTopKLogprobLoss + + loss = self.loss_function( + self.lm_head.weight, + hidden_states, + target_token_ids, + target_logprobs, + target_mask, + true_labels=labels, + ) + num_items_in_batch = kwargs.pop("num_items_in_batch", -1) + if num_items_in_batch is not None and num_items_in_batch > 0: + loss = loss / num_items_in_batch + + return CausalLMOutputWithPast( + loss=loss, + logits=None, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +def apply_kernel(model_type): + # Dynamically import the module and attention class + module_path = f"transformers.models.{model_type}.modeling_{model_type}" + model_cls_prefix = "".join([part.capitalize() for part in model_type.split("_")]) + module = __import__(module_path, fromlist=[f"{model_cls_prefix}ForCausalLM"]) + model_cls = getattr(module, f"{model_cls_prefix}ForCausalLM") + model_cls.forward = MethodType(kldiv_forward_llama_like, model_cls) diff --git a/src/axolotl/integrations/kd/topk_logprob/forward_kl.py b/src/axolotl/integrations/kd/topk_logprob/forward_kl.py index 3c95150913..74184455f5 100644 --- a/src/axolotl/integrations/kd/topk_logprob/forward_kl.py +++ b/src/axolotl/integrations/kd/topk_logprob/forward_kl.py @@ -16,40 +16,7 @@ loss for top_k KL divergence """ import torch - - -def zscore_standardize( - logits: torch.Tensor, - mask: torch.Tensor = None, - base_temperature: float = 1.0, - eps: float = 1e-9, -): - """ - Z-score standardize along the last dimension of `logits`. - i.e., for each [B, seq_len] row, across K entries: - z = (logits - mean) / std, - then scale by 1 / base_temperature if desired. - - mask can be broadcastable or None. If None, we standardize all elements. - """ - if mask is None: - # shape: [B, seq_len, K] - # Mean and std over dim=-1 - mean = logits.mean(dim=-1, keepdim=True) - var = logits.var(dim=-1, unbiased=False, keepdim=True) - else: - # If you have to exclude some tokens, multiply by mask, etc. - float_mask = mask.to(logits.dtype) - count = float_mask.sum(dim=-1, keepdim=True).clamp_min(1.0) - mean = (logits * float_mask).sum(dim=-1, keepdim=True) / count - var = (float_mask * (logits - mean) ** 2).sum(dim=-1, keepdim=True) / count - - std = torch.sqrt(var.clamp_min(eps)) - z = (logits - mean) / std - - # Scale by 1 / base_temperature - z = z / base_temperature - return z +from torch import nn @torch.jit.script @@ -60,7 +27,6 @@ def loss( target_mask: torch.Tensor, num_items_in_batch: int = -1, # Use -1 to indicate "None" kd_temperature: float = 1.0, - top_k_before_softmax: int = 0, ) -> torch.Tensor: """ A KD loss function that is TorchScript-friendly. @@ -77,8 +43,6 @@ def loss( num_items_in_batch (int, optional): The number of items in the batch. kd_temperature (float, optional): The temperature for KD. Default: 1.0 - top_k_before_softmax (int, optional): Flag of whether to apply softmax before gathering student top-k logits - Default: 0 """ target_logprobs = target_logprobs.float() @@ -88,46 +52,24 @@ def loss( # student_logits shape: [B, student_seq_len, vocab_size] teacher_seq_len = target_token_ids.shape[1] - if top_k_before_softmax: - # Slice student logits to match teacher-provided sequence length - student_logits_for_kd = student_logits[ - :, :teacher_seq_len, : - ] # [B, teacher_seq_len, vocab_size] - - # Gather student logits for teacher's top-K tokens - student_logits_topk = torch.gather( - student_logits_for_kd, dim=-1, index=target_token_ids - ) # [B, teacher_seq_len, K] + # Slice student logits to match teacher-provided sequence length + student_logits_for_kd = ( + student_logits[:, :teacher_seq_len, :] / kd_temperature + ) # [B, teacher_seq_len, vocab_size] - student_logits_topk = student_logits_topk.float() + # keep in full precision for numerical stability of loss + student_logits_for_kd = student_logits_for_kd.float() - # Apply KD temperature to student’s logits - if kd_temperature != 1.0: - student_logits_topk = student_logits_topk / kd_temperature - - # Convert student top-k logits to logprobs - student_logprobs_topk = student_logits_topk - torch.logsumexp( - student_logits_topk, dim=-1, keepdim=True - ) # [B, teacher_seq_len, K] - else: - # Slice student logits to match teacher-provided sequence length - student_logits_for_kd = ( - student_logits[:, :teacher_seq_len, :] / kd_temperature - ) # [B, teacher_seq_len, vocab_size] - - # keep in full precision for numerical stability of loss - student_logits_for_kd = student_logits_for_kd.float() - - # Gather student logits for teacher's top-K tokens - student_logits_topk = torch.gather( - student_logits_for_kd, dim=-1, index=target_token_ids - ) # [B, teacher_seq_len, K] + # Gather student logits for teacher's top-K tokens + student_logits_topk = torch.gather( + student_logits_for_kd, dim=-1, index=target_token_ids + ) # [B, teacher_seq_len, K] - # Compute logsumexp across full vocabulary - student_lse = torch.logsumexp(student_logits_for_kd, dim=-1, keepdim=True) + # Compute logsumexp across full vocabulary + student_lse = torch.logsumexp(student_logits_for_kd, dim=-1, keepdim=True) - # Convert just the top-k logits to logprobs - student_logprobs_topk = student_logits_topk - student_lse + # Convert just the top-k logits to logprobs + student_logprobs_topk = student_logits_topk - student_lse # Convert teacher_mask to boolean for indexing # In TorchScript, .bool() is sometimes unsupported, so we do: @@ -144,10 +86,6 @@ def loss( kd_loss_per_token = teacher_probs * (target_logprobs - student_logprobs_topk) kd_loss = kd_loss_per_token.sum() - # Multiply by T^2 (classical KD scaling) - if kd_temperature != 1.0: - kd_loss = kd_loss * (kd_temperature**2) - # Normalize by number of items (if provided) or by valid tokens if num_items_in_batch > 0: kd_loss = kd_loss / float(num_items_in_batch) @@ -158,80 +96,74 @@ def loss( return kd_loss -def topk_kd_loss_with_zscore( - student_logits: torch.Tensor, # [B, seq_len, vocab_size] - target_token_ids: torch.Tensor, # [B, seq_len, K] - target_logprobs: torch.Tensor, # [B, seq_len, K], sums to 1.0 in prob space - target_mask: torch.Tensor, # [B, seq_len, K] or [B, seq_len] - kd_temperature: float = 1.0, # classic KD temperature - zscore_base_temp: float = 1.0, # from the paper - num_items_in_batch: int = -1, -): - """ - A variant of top_k KL divergence with Z-score scaling - from "Logit Standardization in Knowledge Distillation". +class ChunkedTopKKDLoss(nn.Module): """ + A wrapper that chunks (splits) the student and teacher outputs along the time dimension + to reduce peak memory usage when upcasting from bf16 to fp32, especially for large vocabularies. - target_logprobs = target_logprobs.float() - - B, teacher_seq_len, K = target_logprobs.shape # pylint: disable=invalid-name - # 1) Gather the student's top-k logits to match teacher - student_logits_for_kd = student_logits[ - :, :teacher_seq_len, : - ] # [B, seq_len, vocab] - student_topk_logits = torch.gather( - student_logits_for_kd, dim=-1, index=target_token_ids - ) # [B, seq_len, K] - - student_topk_logits = student_topk_logits.float() - - # 2) If you want to keep the "classical" T scaling, apply it first - if kd_temperature != 1.0: - student_topk_logits = student_topk_logits / kd_temperature - - # 3) Convert teacher logprobs -> treat them as “logits” for z-score - # (They differ by +some_constant from real logits, but in z-score - # that constant is subtracted out anyway.) - teacher_logits_for_zscore = target_logprobs # rename variable for clarity - - # 4) Z-score teacher and student - # If target_mask is 2D, expand to 3D for the K dimension - if target_mask.dim() == 2 and target_mask.shape[:2] == (B, teacher_seq_len): - target_mask = target_mask.unsqueeze(-1).expand(-1, -1, K) - - teacher_z = zscore_standardize( - teacher_logits_for_zscore, mask=target_mask, base_temperature=zscore_base_temp - ) - student_z = zscore_standardize( - student_topk_logits, mask=target_mask, base_temperature=zscore_base_temp - ) - - # 5) Convert to log-probs for KL - teacher_logprobs_z = teacher_z - torch.logsumexp(teacher_z, dim=-1, keepdim=True) - student_logprobs_z = student_z - torch.logsumexp(student_z, dim=-1, keepdim=True) - - # 6) Restrict to valid tokens if needed - valid_mask = target_mask.bool() # shape [B, seq_len, K] - teacher_probs_z = teacher_logprobs_z.exp() - teacher_probs_z = teacher_probs_z[valid_mask] - teacher_logprobs_z = teacher_logprobs_z[valid_mask] - student_logprobs_z = student_logprobs_z[valid_mask] - - # 7) forward KL: sum( p_teacher * [log(p_teacher) - log(p_student)] ) - kd_loss_per_token = teacher_probs_z * (teacher_logprobs_z - student_logprobs_z) - kd_loss = kd_loss_per_token.sum() - - # 8) If using classical KD scaling by T^2 - if kd_temperature != 1.0: - kd_loss = kd_loss * (kd_temperature**2) - - # Optionally scale by zscore_base_temp**2 if you want (paper might differ). - # kd_loss = kd_loss * (zscore_base_temp**2) - - # 9) Normalize - if num_items_in_batch is not None and num_items_in_batch > 0: - kd_loss = kd_loss / float(num_items_in_batch) - else: - kd_loss = kd_loss / float(kd_loss_per_token.size(0)) + Usage is analogous to ForwardKLWithChunkedOutputLoss but adapted to top-K teacher logprobs. + """ - return kd_loss + def __init__(self, num_output_chunks: int = 8, kd_temperature: float = 1.0): + super().__init__() + self.num_output_chunks = num_output_chunks + self.kd_temperature = kd_temperature + + def forward( + self, + student_logits: torch.Tensor, # [B, seq_len, vocab_size] + target_token_ids: torch.Tensor, # [B, seq_len, K] + target_logprobs: torch.Tensor, # [B, seq_len, K] + target_mask: torch.Tensor, # [B, seq_len, K] + num_items_in_batch: int = -1, # optional batch size for normalization + ) -> torch.Tensor: + + # 1. Split along the "token" dimension (dim=1). + student_logits_chunks = student_logits.chunk(self.num_output_chunks, dim=1) + token_ids_chunks = target_token_ids.chunk(self.num_output_chunks, dim=1) + logprobs_chunks = target_logprobs.chunk(self.num_output_chunks, dim=1) + mask_chunks = target_mask.chunk(self.num_output_chunks, dim=1) + + # We'll accumulate a global "sum of losses" and "sum of valid tokens" + # so that our final average is consistent with the entire sequence/batch. + total_loss = 0.0 + total_valid_tokens = 0 + + # 2. Loop over each chunk and compute a chunk-specific loss. + for st_chunk, tid_chunk, lp_chunk, msk_chunk in zip( + student_logits_chunks, token_ids_chunks, logprobs_chunks, mask_chunks + ): + # We pass num_items_in_batch=-1 so that the kd_loss + # will average over *this chunk's* valid tokens only. + chunk_loss = loss( + student_logits=st_chunk, + target_token_ids=tid_chunk, + target_logprobs=lp_chunk, + target_mask=msk_chunk, + num_items_in_batch=-1, # ensure per-chunk averaging by valid tokens + kd_temperature=self.kd_temperature, + ) + + # kd_loss returns an average over the chunk's valid tokens. + # We want a global average in the end, so we need to re‐weight + # by the number of valid tokens in this chunk and keep track of the total. + chunk_valid_mask = msk_chunk.to(torch.bool) + chunk_valid_count = chunk_valid_mask.sum() # scalar tensor + + # Re-scale "chunk average" back to "chunk sum" + chunk_loss_sum = chunk_loss * chunk_valid_count + + total_loss += chunk_loss_sum + total_valid_tokens += chunk_valid_count + + # 3. Normalize *once* at the end. + if num_items_in_batch > 0: + # If the user gave us a manual denominator (e.g. total items in batch), + # we divide by it. Typically used if each item is of different length. + final_loss = total_loss / float(num_items_in_batch) + else: + # Otherwise, divide by total valid tokens across all chunks. + # to get the same result as a non-chunked approach. + final_loss = total_loss / float(total_valid_tokens) + + return final_loss diff --git a/src/axolotl/integrations/kd/trainer.py b/src/axolotl/integrations/kd/trainer.py index f99f2ca28b..7ec43333a6 100644 --- a/src/axolotl/integrations/kd/trainer.py +++ b/src/axolotl/integrations/kd/trainer.py @@ -18,8 +18,7 @@ from axolotl.core.trainers.base import AxolotlTrainer -from .topk_logprob.forward_kl import loss as topk_kd_loss -from .topk_logprob.forward_kl import topk_kd_loss_with_zscore +from .kernels.liger import LigerFusedLinearKLTopKLogprobLoss class AxolotlKDTrainer(AxolotlTrainer): @@ -27,6 +26,18 @@ class AxolotlKDTrainer(AxolotlTrainer): Custom trainer subclass for Knowledge Distillation (KD) """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.model_accepts_loss_kwargs = True + self.model._loss_function = LigerFusedLinearKLTopKLogprobLoss( + self.args.kd_ce_alpha, # hard label loss + self.args.kd_alpha, # kd loss + self.args.kd_temperature, + self.args.kd_beta or 0.0, + compute_ce_loss=bool(self.args.kd_ce_alpha), + normalize_topk=self.args.kd_normalize_topk, + ) + def _set_signature_columns_if_needed(self): super()._set_signature_columns_if_needed() columns_to_add = [] @@ -52,12 +63,12 @@ def compute_loss( Subclass and override for custom behavior. """ - - target_logprobs = inputs.pop("target_logprobs") - target_token_ids = inputs.pop("target_token_ids") - target_mask = inputs.pop("target_mask") - - seq_len = target_token_ids.shape[1] + if ( + self.args.sample_packing + and hasattr(inputs, "attention_mask") + and hasattr(inputs, "position_ids") + ): + del inputs["attention_mask"] if self.model_accepts_loss_kwargs: loss_kwargs = {} @@ -65,49 +76,4 @@ def compute_loss( loss_kwargs["num_items_in_batch"] = num_items_in_batch inputs = {**inputs, **loss_kwargs} outputs = model(**inputs) - - # FIXME: account for tokenizer.padding_side - student_logits = outputs["logits"][:, : seq_len - 1, :].contiguous() - - shift_logits = student_logits.contiguous() - target_logprobs_for_loss = target_logprobs[..., 1:, :].contiguous() - target_token_ids_for_loss = target_token_ids[..., 1:, :].contiguous() - target_mask_for_loss = target_mask[..., 1:, :].contiguous() - - if self.args.kd_zscore_base_temp: - loss_kd = topk_kd_loss_with_zscore( - shift_logits, - target_token_ids_for_loss, - target_logprobs_for_loss, - target_mask_for_loss, - kd_temperature=self.args.kd_temperature, - zscore_base_temp=self.args.kd_zscore_base_temp, - num_items_in_batch=num_items_in_batch, - ) - else: - loss_kd = topk_kd_loss( - shift_logits, - target_token_ids_for_loss, - target_logprobs_for_loss, - target_mask_for_loss, - num_items_in_batch=num_items_in_batch, - kd_temperature=self.args.kd_temperature, - top_k_before_softmax=1 if self.args.kd_top_k_before_softmax else 0, - ) - - if self.args.kd_ce_alpha > 0: - kd_alpha = self.args.kd_alpha - loss = self.args.kd_ce_alpha * outputs["loss"] + kd_alpha * loss_kd - else: - loss = loss_kd - # Save past state if it exists - # TODO: this needs to be fixed and made cleaner later. - if self.args.past_index >= 0: - self._past = outputs[ # pylint: disable=attribute-defined-outside-init - self.args.past_index - ] - - if self.args.average_tokens_across_devices and self.model_accepts_loss_kwargs: - loss *= self.accelerator.num_processes - - return (loss, outputs) if return_outputs else loss + return outputs[0] diff --git a/src/axolotl/integrations/kd/utils.py b/src/axolotl/integrations/kd/utils.py new file mode 100644 index 0000000000..ba60694a56 --- /dev/null +++ b/src/axolotl/integrations/kd/utils.py @@ -0,0 +1,100 @@ +"""Helper KD utils""" + +import math +from typing import List, Union + +import numpy as np +import torch +from torch import FloatTensor, Tensor + + +def normalize_logprobs(logprobs: FloatTensor, topk: int) -> FloatTensor: + """ + Re-normalizes top-k raw logprobs as probabilities, and converts back to logprobs. + """ + # Ensure raw_logprobs matches kd_online_topk length for tensor operations + # This should ideally be handled by the caller ensuring correct padding/truncation first + if logprobs.shape[-1] != topk: + # pad last dimension of logprobs to match topk length with -inf + padding_len = topk - logprobs.shape[-1] + padding_tensor = torch.full( + ( + *logprobs.shape[:-1], + padding_len, + ), # Takes all dimensions of logprobs except the last, then appends padding_needed + float("-inf"), + dtype=logprobs.dtype, + device=logprobs.device, + ) + logprobs = torch.cat((logprobs, padding_tensor), dim=-1) + + # Convert logprobs at T_online to probabilities + # use log sum exp trick to avoid underflow + position_logprobs_lse = torch.logsumexp(logprobs, dim=-1, keepdim=True) + teacher_probs_t_online = torch.exp(logprobs - position_logprobs_lse) + + # Normalize probabilities (sum to 1) + # This is important if the top-k from server aren't a full distribution + teacher_probs_t_online_sum = teacher_probs_t_online.sum(dim=-1, keepdim=True) + teacher_probs_t_online = teacher_probs_t_online / teacher_probs_t_online_sum + + final_logprobs_tensor = torch.log(teacher_probs_t_online) + + return final_logprobs_tensor + + +def strided_chunk_views( + tensor: Union[np.ndarray, torch.Tensor], + chunks: int, + dim: int = 0, + stride: int = 1, + chunk_size: int | None = None, +) -> List[Union[np.ndarray, torch.Tensor]]: + """ + Split a tensor into chunks along a dimension with striding, prioritizing views over copies. + + Args: + tensor: Input tensor (numpy array or torch tensor) + chunks: Number of chunks to create + dim: Dimension along which to chunk (default: 0) + stride: Stride between chunk starting positions (default: 1) + chunk_size: Size of each chunk. If None, calculated automatically (default: None) + + Returns: + List of tensor chunks (views when possible, copies when necessary) + """ + + # Get the size of the specified dimension + dim_size = tensor.shape[dim] + + # Calculate chunk size if not provided + if chunk_size is None: + chunk_size = (dim_size + chunks - 1) // chunks # Ceiling division + + chunks_list = [] + + for i in range(chunks): + start_idx = i * stride + end_idx = min(start_idx + chunk_size, dim_size) + + # Break if we've gone beyond the tensor + if start_idx >= dim_size: + break + + # Create slice objects for all dimensions + slices = [slice(None)] * tensor.ndim + slices[dim] = slice(start_idx, end_idx) + + chunk = tensor[tuple(slices)] + chunks_list.append(chunk) + + return chunks_list + + +def chunk_overlap(input_tensor: Tensor, chunks: int, dim: int = 0, overlap: int = 1): + dim_size = input_tensor.shape[dim] + stride = math.ceil(dim_size / chunks) + + return strided_chunk_views( + input_tensor, chunks, dim, stride=stride, chunk_size=stride + overlap + ) diff --git a/src/axolotl/prompt_strategies/__init__.py b/src/axolotl/prompt_strategies/__init__.py index 3cdbbb6f33..cf936481e9 100644 --- a/src/axolotl/prompt_strategies/__init__.py +++ b/src/axolotl/prompt_strategies/__init__.py @@ -17,7 +17,10 @@ def load(strategy, tokenizer, cfg, ds_cfg, processor=None): return messages_load(tokenizer, cfg, ds_cfg, processor=processor) load_fn = "load" package = "axolotl.prompt_strategies" - if strategy.split(".")[-1].startswith("load_"): + if ( + strategy.split(".")[-1].startswith("load_") + or strategy.split(".")[-1] == "load" + ): load_fn = strategy.split(".")[-1] strategy = ".".join(strategy.split(".")[:-1]) elif len(strategy.split(".")) > 1: diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 13ac8ec0d3..fa7d569130 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -1,10 +1,13 @@ """Prepare and train a model on a dataset. Can also infer from a model or merge lora""" +from __future__ import annotations + import importlib import inspect import os import signal import sys +import typing import weakref from contextlib import ExitStack from pathlib import Path @@ -25,7 +28,6 @@ from axolotl.contribs.lgpl import ( # pylint: disable = no-name-in-module fix_untrained_tokens, ) -from axolotl.core.builders import HFCausalTrainerBuilder, HFRLTrainerBuilder from axolotl.integrations.base import PluginManager from axolotl.loaders import ( ModelLoader, @@ -45,6 +47,9 @@ except ImportError: BetterTransformer = None +if typing.TYPE_CHECKING: + from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder + LOG = get_logger(__name__) @@ -472,7 +477,7 @@ def handle_untrained_tokens_fix( def setup_model_and_trainer(cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> tuple[ - HFRLTrainerBuilder | HFCausalTrainerBuilder, + "HFRLTrainerBuilder" | "HFCausalTrainerBuilder", PeftModel | PreTrainedModel, PreTrainedTokenizer, PeftConfig | None, diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index 3d0ba7c9c6..e669413f82 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -52,3 +52,10 @@ def patch_optimized_env(): if os.getenv("HF_HUB_ENABLE_HF_TRANSFER") is None: os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1" set_pytorch_cuda_alloc_conf() + + +def get_not_null(value, default=None): + """ + return the value if it's not None, otherwise return the default value + """ + return value if value is not None else default diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index bf496d2c51..09bfb55763 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -36,7 +36,7 @@ "deepseek_v3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='', is_first_sp=true) %}{%- for message in messages %}{%- if message['role'] == 'system' %}{%- if ns.is_first_sp %}{% set ns.system_prompt = ns.system_prompt + message['content'] %}{% set ns.is_first_sp = false %}{%- else %}{% set ns.system_prompt = ns.system_prompt + '\\n\\n' + message['content'] %}{%- endif %}{%- endif %}{%- endfor %}{{ bos_token }}{{ ns.system_prompt }}{%- for message in messages %}{%- if message['role'] == 'user' %}{%- set ns.is_tool = false -%}{{'<|User|>' + message['content']}}{%- endif %}{%- if message['role'] == 'assistant' and 'tool_calls' in message %}{%- set ns.is_tool = false -%}{%- for tool in message['tool_calls'] %}{%- if not ns.is_first %}{%- if message['content'] is none %}{{'<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- else %}{{'<|Assistant|>' + message['content'] + '<|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- endif %}{%- set ns.is_first = true -%}{%- else %}{{'\\n' + '<|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- endif %}{%- endfor %}{{'<|tool▁calls▁end|><|end▁of▁sentence|>'}}{%- endif %}{%- if message['role'] == 'assistant' and 'tool_calls' not in message %}{%- if ns.is_tool %}{{'<|tool▁outputs▁end|>' + message['content'] + '<|end▁of▁sentence|>'}}{%- set ns.is_tool = false -%}{%- else %}{% set content = message['content'] %}{% if '
' in content %}{% set content = content.split('')[-1] %}{% endif %}{{'<|Assistant|>' + content + '<|end▁of▁sentence|>'}}{%- endif %}{%- endif %}{%- if message['role'] == 'tool' %}{%- set ns.is_tool = true -%}{%- if ns.is_output_first %}{{'<|tool▁outputs▁begin|><|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- set ns.is_output_first = false %}{%- else %}{{'<|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- endif %}{%- endif %}{%- endfor -%}{% if ns.is_tool %}{{'<|tool▁outputs▁end|>'}}{% endif %}{% if add_generation_prompt and not ns.is_tool %}{{'<|Assistant|>'}}{% endif %}", "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', "qwen_25": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n", - "qwen3": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].role == 'system' %}\n {{- messages[0].content + '\\n\\n' }}\n {%- endif %}\n {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}\n {%- set index = (messages|length - 1) - loop.index0 %}\n {%- if ns.multi_step_tool and message.role == \"user\" and not(message.content.startswith('') and message.content.endswith('')) %}\n {%- set ns.multi_step_tool = false %}\n {%- set ns.last_query_index = index %}\n {%- endif %}\n{%- endfor %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set content = message.content %}\n {%- set reasoning_content = '' %}\n {%- if message.reasoning_content is defined and message.reasoning_content is not none %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if '' in message.content %}\n {%- set content = message.content.split('')[-1].lstrip('\\n') %}\n {%- set reasoning_content = message.content.split('')[0].rstrip('\\n').split('')[-1].lstrip('\\n') %}\n {%- endif %}\n {%- endif %}\n {%- if loop.index0 > ns.last_query_index %}\n {%- if loop.last or (not loop.last and reasoning_content) %}\n {{- '<|im_start|>' + message.role + '\\n\\n' + reasoning_content.strip('\\n') + '\\n\\n\\n' + content.lstrip('\\n') }}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- if message.tool_calls %}\n {%- for tool_call in message.tool_calls %}\n {%- if (loop.first and content) or (not loop.first) %}\n {{- '\\n' }}\n {%- endif %}\n {%- if tool_call.function %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {%- if tool_call.arguments is string %}\n {{- tool_call.arguments }}\n {%- else %}\n {{- tool_call.arguments | tojson }}\n {%- endif %}\n {{- '}\\n' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n {%- if enable_thinking is defined and enable_thinking is false %}\n {{- '\\n\\n\\n\\n' }}\n {%- endif %}\n{%- endif %}", + "qwen3": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].role == 'system' %}\n {{- messages[0].content + '\\n\\n' }}\n {%- endif %}\n {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}\n {%- set index = (messages|length - 1) - loop.index0 %}\n {%- if ns.multi_step_tool and message.role == \"user\" and not(message.content.startswith('') and message.content.endswith('')) %}\n {%- set ns.multi_step_tool = false %}\n {%- set ns.last_query_index = index %}\n {%- endif %}\n{%- endfor %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set content = message.content %}\n {%- set reasoning_content = '' %}\n {%- if message.reasoning_content is defined and message.reasoning_content is not none %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if '' in message.content %}\n {%- set content = message.content.split('')[-1].lstrip('\\n') %}\n {%- set reasoning_content = message.content.split('')[0].rstrip('\\n').split('')[-1].lstrip('\\n') %}\n {%- endif %}\n {%- endif %}\n {%- if loop.index0 > ns.last_query_index %}\n {%- if loop.last or (not loop.last and reasoning_content) %}\n {{- '<|im_start|>' + message.role + '\\n\\n' + reasoning_content.strip('\\n') + '\\n\\n\\n' + content.lstrip('\\n') }}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- if message.tool_calls %}\n {%- for tool_call in message.tool_calls %}\n {%- if (loop.first and content) or (not loop.first) %}\n {{- '\\n' }}\n {%- endif %}\n {%- if tool_call.function %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {%- if tool_call.arguments is string %}\n {{- tool_call.arguments }}\n {%- else %}\n {{- tool_call.arguments | tojson }}\n {%- endif %}\n {{- '}\\n' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n {%- if enable_thinking is defined and enable_thinking is false %}\n {{- '\\n\\n\\n\\n' }}\n {%- else %}\n {{- '\\n\\n' }}\n {%- endif %}\n{%- endif %}", "exaone": "{% for message in messages %}{% if loop.first and message['role'] != 'system' %}{{ '[|system|][|endofturn|]\n' }}{% endif %}{{ '[|' + message['role'] + '|]' + message['content'] }}{% if message['role'] == 'user' %}{{ '\n' }}{% else %}{{ '[|endofturn|]\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '[|assistant|]' }}{% endif %}", "metharme": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% else %}{% set loop_messages = messages %}{% set system_message = 'Enter RP mode. You shall reply to the user while staying in character. Your responses must be detailed, creative, immersive, and drive the scenario forward.' %}{% endif %}{{ '<|system|>' + system_message }}{% for message in loop_messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|user|>' + content.strip() }}{% elif message['role'] == 'assistant' %}{{ '<|model|>' + content.strip() }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|model|>' }}{% else %}{{ eos_token }}{% endif %}", "pixtral": '{%- if messages[0]["role"] == "system" %}\n {%- set system_message = messages[0]["content"] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set loop_messages = messages %}\n{%- endif %}\n\n{{- bos_token }}\n{%- for message in loop_messages %}\n {%- if (message[\'role\'] == \'user\') != (loop.index0 % 2 == 0) %}\n {{- raise_exception(\'After the optional system message, conversation roles must alternate user/assistant/user/assistant/...\') }}\n {%- endif %}\n {%- if message["role"] == "user" %}\n {%- if loop.last and system_message is defined %}\n {{- "[INST]" + system_message + "\n\n" }}\n {%- else %}\n {{- "[INST]" }}\n {%- endif %}\n {%- if message["content"] is not string %}\n {%- for chunk in message["content"] %}\n {%- if chunk["type"] == "text" %}\n {{- chunk["text"] }}\n {%- elif chunk["type"] == "image" %}\n {{- "[IMG]" }}\n {%- else %}\n {{- raise_exception("Unrecognized content type!") }}\n {%- endif %}\n {%- endfor %}\n {%- else %}\n {{- message["content"] }}\n {%- endif %}\n {{- "[/INST]" }}\n {%- elif message["role"] == "assistant" %}\n {%- if message["content"] is not string %}\n {%- for chunk in message["content"] %}\n {%- if chunk["type"] == "text" %}\n {{- chunk["text"] }}\n {%- elif chunk["type"] == "image" %}\n {{- "[IMG]" }}\n {%- else %}\n {{- raise_exception("Unrecognized content type!") }}\n{%- endif %}\n{%- endfor %}\n{{- eos_token }}\n{%- else %}\n{{- message["content"] + eos_token }}\n{%- endif %}\n {%- else %}\n {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }}\n {%- endif %}\n{%- endfor %}', diff --git a/src/axolotl/utils/collators/batching.py b/src/axolotl/utils/collators/batching.py index d8414d1175..a28f360be3 100644 --- a/src/axolotl/utils/collators/batching.py +++ b/src/axolotl/utils/collators/batching.py @@ -1,7 +1,7 @@ """Data collators for axolotl to pad labels and position_ids for packed sequences""" from dataclasses import dataclass -from typing import Any +from typing import Any, List import numpy as np from transformers import PreTrainedTokenizerBase @@ -163,7 +163,7 @@ class V2BatchSamplerDataCollatorForSeq2Seq(DataCollatorForSeq2Seq): def __call__(self, features, return_tensors=None): if not isinstance(features[0], list): - features = [features] + features: List[List[dict]] = [features] out_features = [{} for _ in features] for i, features_ in enumerate(features): for feature in features_[0].keys(): diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index 0ffaa932fa..4f7f6f8dd0 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -51,6 +51,7 @@ def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements except ( requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError, + requests.exceptions.HTTPError, huggingface_hub.errors.HfHubHTTPError, ) as exc: if attempt < max_retries - 1: diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index eabfc2d849..7fb5e1b41b 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -259,7 +259,7 @@ def __init__( batch_max_len: int, # Maximum sequence length (bin capacity) lengths: np.ndarray, # Sequence lengths packing_efficiency_estimate: float = 1.0, # Initial efficiency estimate - drop_last: bool = False, # Whether to drop final batches (might be incomplete) + drop_last: bool = True, # Whether to drop final batches (might be incomplete) num_count_samples: int = 8, # Number of times to estimate batch count sequential: bool = False, # Whether to use sequential packing group_size: int = 100_000, # Size of groups for parallel packing @@ -446,10 +446,18 @@ def __len__(self) -> int: if self._len_across_ranks is None: # Sample multiple times to get stable estimate - len_batches = min( # pylint: disable=consider-using-generator - [len(self._batches) for _ in range(self.num_count_samples)] - ) + _sampled_lens = [] + for _ in range(self.num_count_samples): + self._batches = None # Reset cached batches + _sampled_lens.append(len(self.generate_batches(set_stats=False))) + len_batches = min(_sampled_lens) + # Gather minimum across all ranks - self._len_across_ranks = self.gather_len_batches(len_batches) + if self._len_across_ranks is None: + self._len_across_ranks = self.gather_len_batches(len_batches) + else: + self._len_across_ranks = min( + self._len_across_ranks, self.gather_len_batches(len_batches) + ) return self._len_across_ranks diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index ec5360fa33..33ddadf78e 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -16,7 +16,6 @@ from torch.utils.data import DataLoader, RandomSampler, SequentialSampler from transformers.utils import is_torch_bf16_gpu_available -from axolotl.core.builders import HFCausalTrainerBuilder, HFRLTrainerBuilder from axolotl.monkeypatch.trainer_eval_guard import patch_evaluation_loop_for_fsdp2 from axolotl.utils.distributed import reduce_and_broadcast from axolotl.utils.environment import check_cuda_p2p_ib_support @@ -483,6 +482,9 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): data_loader_len * cfg.num_epochs * cfg.sequence_parallel_degree ) ) + if cfg.dataloader_drop_last: + # drop the last batch for each epoch + total_num_steps -= int(math.ceil(cfg.num_epochs)) def calc_sample_packing_eff_est(estimates: List[float]): LOG.info(f"sample_packing_eff_est across ranks: {repr(estimates)}") @@ -630,6 +632,8 @@ def setup_trainer( A trainer instance (either `HFRLTrainer` or `HFCausalTrainer`) configured based on the provided parameters. """ + from axolotl.core.builders import HFCausalTrainerBuilder, HFRLTrainerBuilder + if ( cfg.torch_compile and cfg.fsdp_config diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py index 2bd1fbf3d9..212450e89e 100644 --- a/tests/e2e/integrations/test_kd.py +++ b/tests/e2e/integrations/test_kd.py @@ -5,10 +5,9 @@ from pathlib import Path import pytest +import yaml +from accelerate.test_utils import execute_subprocess_async, get_torch_dist_unique_port -from axolotl.common.datasets import load_datasets -from axolotl.train import train -from axolotl.utils.config import normalize_config, prepare_plugins, validate_config from axolotl.utils.dict import DictDefault from tests.e2e.utils import check_tensorboard, require_torch_2_5_1 @@ -17,8 +16,8 @@ @pytest.fixture(name="kd_min_cfg") def min_cfg(temp_dir): return { - "base_model": "osllmai-community/Llama-3.2-1B", - "tokenizer_config": "axolotl-ai-co/Llama-3.3-70B-Instruct-tokenizer", + "base_model": "Qwen/Qwen3-0.6B", + "tokenizer_config": "winglian/qwen3-14b-math", "plugins": [ "axolotl.integrations.kd.KDPlugin", "axolotl.integrations.liger.LigerPlugin", @@ -31,20 +30,22 @@ def min_cfg(temp_dir): "kd_ce_alpha": 0.1, "kd_alpha": 0.9, "kd_temperature": 1.0, + "kd_beta": 0.0, + "kd_normalize_topk": True, "dataloader_prefetch_factor": 8, "dataloader_num_workers": 4, "dataloader_pin_memory": True, "datasets": [ { - "path": "axolotl-ai-co/evolkit-logprobs-pipeline-75k-v2-sample", - "type": "axolotl.integrations.kd.chat_template", - "field_messages": "messages_combined", + "path": "winglian/OpenThoughts-114k-math-correct-qwen3-14b-math-prepared-topk128-normalized", + "type": "chat_template", "split": "train", - "logprobs_field": "llm_text_generation_vllm_logprobs", - "temperature": 1.0, - "preprocess_shards": 2, + "split_thinking": True, + "eot_tokens": ["<|im_end|>"], + "data_files": ["train/batch-000000.parquet"], }, ], + "skip_prepare_dataset": True, "val_set_size": 0.0, "sequence_len": 2048, "sample_packing": True, @@ -80,17 +81,29 @@ class TestKnowledgeDistillation: def test_llama_kd(self, temp_dir, kd_min_cfg): cfg = DictDefault(kd_min_cfg) # pylint: disable=duplicate-code - cfg = validate_config(cfg) - prepare_plugins(cfg) - normalize_config(cfg) - dataset_meta = load_datasets(cfg=cfg) + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "1", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) - train(cfg=cfg, dataset_meta=dataset_meta) assert (Path(temp_dir) / "model.safetensors").exists() check_tensorboard( temp_dir + "/runs", "train/loss", 1.4, "Train Loss (%s) is too high" ) + @pytest.mark.skip(reason="Chunked KD loss doesn't support PEFT/LoRA") @pytest.mark.parametrize( "load_in_8bit", [True, False], @@ -110,12 +123,22 @@ def test_llama_lora_kd(self, temp_dir, kd_min_cfg, load_in_8bit): | kd_min_cfg ) # pylint: disable=duplicate-code - cfg = validate_config(cfg) - prepare_plugins(cfg) - normalize_config(cfg) - dataset_meta = load_datasets(cfg=cfg) + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) - train(cfg=cfg, dataset_meta=dataset_meta) + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "1", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) assert (Path(temp_dir) / "adapter_model.safetensors").exists() check_tensorboard( temp_dir + "/runs", "train/loss", 1.2, "Train Loss (%s) is too high" diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index 2b03c62f82..d91f63d941 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -81,6 +81,7 @@ def test_packing( group_size=100000, bin_size=200, sequential=sequential, + drop_last=False, ) loader = DataLoader( From d8e8cd855843f10666a87344392dce3c60d6d3eb Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 17 Jun 2025 09:09:33 -0700 Subject: [PATCH 0721/1405] feat: remove evalfirst callback with built-in trainer arg (#2797) --- src/axolotl/core/builders/base.py | 2 ++ src/axolotl/core/builders/causal.py | 2 -- src/axolotl/utils/callbacks/__init__.py | 19 ------------------- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index e399cf3c5e..eed43e542c 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -386,8 +386,10 @@ def _configure_save_and_eval_strategy(self, training_args_kwargs: dict): elif self.cfg.eval_steps: training_args_kwargs["eval_strategy"] = "steps" training_args_kwargs["eval_steps"] = self.cfg.eval_steps + training_args_kwargs["eval_on_start"] = True elif self.cfg.eval_strategy: training_args_kwargs["eval_strategy"] = self.cfg.eval_strategy + training_args_kwargs["eval_on_start"] = True def _configure_reporting(self, training_args_kwargs: dict): report_to = [] diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 6ed298d9f8..47e33a3321 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -27,7 +27,6 @@ from axolotl.processing_strategies import get_processing_strategy from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.callbacks import ( - EvalFirstStepCallback, LossWatchDogCallback, SaveBetterTransformerModelCallback, bench_eval_callback_factory, @@ -58,7 +57,6 @@ class HFCausalTrainerBuilder(TrainerBuilderBase): def get_callbacks(self): callbacks = super().get_callbacks() - callbacks.append(EvalFirstStepCallback()) if self.cfg.relora_steps: callbacks.append(ReLoRACallback(self.cfg)) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 8b8a776110..2a93ceef54 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -53,25 +53,6 @@ LOG = get_logger(__name__) -class EvalFirstStepCallback( - TrainerCallback -): # pylint: disable=too-few-public-methods disable=unused-argument - """ - Callback to trigger evals on the first step - """ - - def on_step_end( - self, - args: TrainingArguments, - state: TrainerState, - control: TrainerControl, - **kwargs, - ): - if args.eval_strategy == IntervalStrategy.STEPS and state.global_step == 1: - control.should_evaluate = True - return control - - class SaveBetterTransformerModelCallback( TrainerCallback ): # pylint: disable=too-few-public-methods From 88c0e8d0486657314e140e4dfd55f3e3ed277e8f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 17 Jun 2025 12:13:27 -0400 Subject: [PATCH 0722/1405] release tag (#2799) --- src/axolotl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index 63f28adda4..8f2b67fc3b 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -4,4 +4,4 @@ __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.10.0.dev0" +__version__ = "0.10.0" From da8f6c32b96aea79c36548b5d51790defffecfab Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 17 Jun 2025 18:09:24 -0400 Subject: [PATCH 0723/1405] update favicon (#2801) * update favicon * correct size favicon --- favicon.jpg | Bin 4638 -> 4805 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/favicon.jpg b/favicon.jpg index 43c69024430555f849ee0077664f194dae8d912f..4ec3587464daf6f9e62b0428eaa9b5ad7e8db54a 100644 GIT binary patch delta 4181 zcma)9c{o(<`#(ivE0RhP)2n(_6D_ui%v-N55|ylJDnv9%A!W?dCW;Ixlr$w0VqVLT zC6k>bOLj41EM=dujA724^F6)4@AdurcVFi^=UmTq-OKa2@B8!I&#SG~+oGFd{UkO5 zoaAr8N#Xt#ssD=fzasNno}AtA8>d*q~X(*IlkfB&F;kR-GM{R}BeLl%&Q z#4-&iPe$_NGO*Z*NV}qtE%}`e z=N=AK`fbY$9Xglrxm>N-0fp|p4CtM{%i>w@4#O`4#_AJ!ypcR^qiW?sVDGgjmx@ha z##YMjSH00|8uMKul@g5Ew?d)2yQr0bSvaAWjI@LN2hTHuVykXV|KkHnsmQSfAEK}LuBfmdu z=kU`#{75(8^X`-4$VI(N=ecihOzO@NYdaCe_r!sND0JW9E16CfX+Dqg10##UIpa}2 z;*%^dT91w3D2B>vr2g!(X37EH65=cSqp$XLk5kY^!s(*mgo&OJtB40l+}X2-mGbJo zesf%M7`lUH)C`3@6}i4bOY<6ErA4;4$ex&YxnOG6=qt#cN1;QROB&)_)_KsKDVXEQ zKjyIoFIuOVj|qQ$0!V9KRFiF@jYrXp3fm6@dvncK{hymUOCpnuOwzNsg$HKO49eNt z#p`hx{$=(P6a$eWhM_Hs^BjeECrFRtiW&Dx4RH7PZdL^!KmL=?KmOPUS8(1`%+D>6 z<&vIV8(*7xcgBps6%!D74{UqB6t_0jf$3UOm@%ORz=r<0T8;fBDNf!?*LGb#QTmHK zR1{LGZsLCK)&*7~O}Jh8vw{@naWCLx>QiyzL@aA+#KY~Sd$Ya9{rsB(q7!7hu10zyo3Q9 zR=4jlCVz$CG56zqcZW;Y1XtgzBIsF%`AT_byP9Qnj3jVwcP~M30br{7NWJ<7aVU%P z=B(De&+Ik+8w1qrr>!lt^`zrEoy&?o=6e(shF6K+N^M^K#r$h#484%ZNuw7wvfwq! z0(ar{42mxAP};-%-lXH5WpbIg%;iLzP;v@-4H9F&v^kUrHvLMas-SFqOg5XsI7RO4QV61S^T7IOD!1evz#`M zwRuqUZLA{DD)^TN{W5YU*iW{M>KF5QoK*9ywz>&C!h6Nm@sF1bPZ9@1tLgFNDH3MO}f$WqlERZrzXr3V2nv1 zG6zryR~}R;7!$5Yp|+Fq6=)ZE_)v25 zp=WYE&%w3WwkxpaP^0d`-rJ~}Ud1-ydQbUbcT1hMNr(B8>$ALFv-F>nuOzSNAlx@| zaLvkf|M5lHP0RlpGrfIc0Cb7JV>Yk=%Fm+^htA52hn0Sov!sC!KD?z%IBQ+fv9;}w zvbprE`7zqpaCQ?zLa9?%7`*vQ9*ycA-$70lZ#SaE!lu0y88j;-4`#5Yn<6 z(!2{^TtDoWR@G6gH_xp8u2)@kWNY`v4U)roGwJ2E+yz(>BqFPX!Xhk#I=m2eEnY3? z8wdmPx_w8;`UkSI%GyDCU+=J|m3z#6MN`QH^XDDbGdPoD+Oq=Mt!!F%7PntGg9$(op=8;#`I;% z14Hm&i%M+GKw`kDk;p4WzQrzD#-3y5@akBBwh)Cx%c+g5)upPURar`&o6k8+_`=$xP}1DqQMynGFYFvqhLwl{ULl5TA+-^|dY<@D zT_&H`kwi{+4KkaxVV1=S3^RQ)H}nAe-{pSh!|>k1NOzS<;=nhogo#V52T|zrLm#|!1Z4uyw2L}qNwQLy_ns}3jBwiU8>e_rXeuhNn zP^9A#x!)Xhn}>TSG8^rQuVk$w#^(6;5yH?WmidSc}Ag-G6) zzjg*PsnXtn=bbj`xeCr1#VZ-D+m?}|s8jyltlH4S%GfxO){&bU`sC3G^U5J7ft<_{ z$IGXWJ5@7LXqDTd&+Gh$D<@_CsQy&by5F!gt>(~dq{F;4;IudOt_F=3Jah39>m8^f zunX|wwKPe<&RpsP=f?dXPM}a$kqwMOr0XcOWhqmfh$WL2O3cHI9^w(rsn@ZtntMa~1gBMJNxqYJDiksumFJd4@`3gXMfN}Tvf7j6f? z@!fOo2BV{%@5l6;4~O{Zc3g9ATAmm*NSm0@)CQ}wFSvi+rGU=_d^v+jFuVwObokKd z0xNIIsEP4>c)CgCk~e+Y23DyQou@6Pt4pI0%VD0;HvTEuE+~-@Z2x~j+hfPDC+iaF zHhmS=M-l8*`3m7@5AYCLR&Sp$&Lu+2{8V8DPkf`Udi@$GHa^FcU;#{tG*W? zmbDS;r3uPSYGF;bLMjS*yB_3TB=QU%Cyq9$rT5%`Rcb{J^&u;r3#%9_YP}t0oDD+` zcWF|#yElEmCN_0w+q=pMn6&%JyS8SC=!^IiBF{%|P+%(j0LEUkkVGDH|Idq&kp zWCamce2d5^4^AozZhW7|hpQ8?8OZ!}X&IAg@@m??O}Qb_HfS~1&_&PqV)d^2m6rQz zwMZB{!NgOD>`fFha>ok#Xy%W!?agT_0R}ld#}C;~4>YpD&d?&4@iY^Fwu;wb$s3?{ zu=25jJ|#LJRHz0yvx!<(C-^%@4eqWYh;09S6{mCX&hp6Hj7{FsHB##Ba2=?e#qiE^ zoi}8_8e16?gMu(|PTB7<@1XIDARUr88$J=T>ju${$7}!Nd+MhbD0K4fcmF54`d_M7 zCToId4q1&bzz~Kr5E;v_%Z0VhL$`_C=WVxgWTlo~4c#VmB}u0PrT9A7NAo3;{Z>q$ z44}?qon7w)x1mtwEa2qf=hhaZP{p5?zA#Y-k=u(xhkr-o&X{r$B#6_>Rw8vQNhZDr zKbQpspoQ;TE@-tnQ<`13G)=2W2J<*`i`2>X~ZAJHwQqW4x5HNnxW8Noeg2i15X? zSR{COO(qhi=7~ZwS>`Ad*?Fe@fZ1Nh`&mC(So3Zj{gyI(zc{wVWnjh`*y~~$e@J4| zCJjYi$hZE3e4ELjk=c{qOD%e=Ung224GL+Wo@yVy^T5J0rY-hm)ImQ|7M}OM3@fna zl6RQ$J`hSN5g8+007w!!$#@BhM>aMDX?}XBRn|E$n_-=1<)QZ=_0YRtLylKYTWz?X zzIkxdg|Z7M^gv$x5ruw@Fj=s2FA9a;^54<(l01Dg)VO>t8MBD}`uw7p`dlCACcl_cEhq?5?-&Jt9;Kv3GzL7;H$B%N?q|_4 z{BZfyI{v|8%4y-9ncO{#`Tg(9;}KWGb$BELV@gh^@1hwObOoYVa?MnbXa&oq7ml}1 zzfp;eDj;eGMVc-Dz)QIG_=r}o?~Jj!_#dJt>kge~>wEjvPH{x3)gV6iP5d#kt(QSu z?pPey?WudiR_bwS6X9k!_z+3?MBpc5ypkudjOj5GX7Zu9t@K1|?2>uq*}~5W5(@2D zElvgF(^!&P-ALKYmQ8v)`{xFS7u5U-9Tn8E$R20nr!9-&C=~9@a`!_aJqNCocmYFw zX4@XDE3@diHsW=paQs3)#`1OGnfSnq$4mD7hO6v4+Q}%lN5O624qQVQ>al)bhLY2< zlTb8{K{;fJ4Jh}{5ZzzVc}ycfTgabAOc(KwYs1Yg z&2$ZnbpR6+0L&jH}$8ysk7?}VV zJiC|czYhPgh5o_6@qmkKkTU>G91Pb5XaB%p2LH`q)39LwJx*sZpR>me7X}wIST>OH zLI!`>^E>_GnLT#@#ne3xw6@e{G{?nYara*=3mEmqoqlchx)FEw2=rlGy2)TkpPM0! zNBq=YFI=v^HdYKP^79V@mOu|&0^-04gaA))69j{^4C~97+W#JR{%4#i@MFX{Ggf~P z0RkaLh&%9L1j{nqAs`62GOQa2WLQ^5(g;Qn_&xsLV-0fqYcCHqfa&0jJf0G?O?nmT`9bSeR$nK5R; zN&g#xH+~jkVXVwg$9aS7D+8uGGl_Lt{(X&=_*gon5{@Cb^x0| z2S?fkLbZt81AdPXbJw{(=tkI(CCSn2i|{X{7I~NKa->}mkq{j?p!62)c0`Iqtrmq% zw3h@pJI5*`YaY#3TrSHNx5do0qUXnrS1**yHOe=5tv;XAn}vs%9o;nJ{xCF&Z-)m+ zr>>~0+IsaJ<&&6y7uFjhh&?iZyy&YQ6yo%nweHmOJ_|>T6#o2~OJU8((2X6o$O~Mq z27{qt26Uj`3pI)MU-p>%#MYgW_jvKEn(bc|3x=$^^S?zoYUl5dFip0I{q)x&H|ALd=GSb`&&$SATE)hg4~voKWj&N39Z6 zG3vD_=PE8*PQJ#q)5qEI!ROYH;}R*Nx8q_l8);Y(PTJLWZi?fVXGf0}+%I}L8u+rv z>mbPNw16FJ!Oq8SSm9k)o8wLh*q;2n5bor8mArC3<{5(7dxz9BWBJ0FtR|zhw6>=8 zFlfKdK)WVHUW9X^2GYF?&ULH`BnH2N8g9BC@Nb5<>62Ywx=Bc@OrKg%5wRsq3e>HV zU*!Bqmu-gWes&6to$^{|!Q<2tliQUvPYFZk72E8bQlF&0J>-5@l^m*($p1q$71@-1 z%4K?qUCbf_KDom}EQOiL6y52uGOfaAx0Zy2xXwXl$FBRov)syOHr zwP7_t$0({eMz)e4)<|y@?ACS)?y%l@*B(_a6|^t6nL=DfX}bG-3b`b*=)>`gSJwtYqKw(k=01FqpaBD1bh=!D)(lWLq} zb=u3w(KXX+M*K+?k8hkH+#N%;d2Wj3kG!!Ckmu2xtgjnHCM zrAcG2hXqtDX^PJuZrO(~H(Bp!gM9p6(qgAmCToOqggAOld32=0mk8x+*&U@+0ui_L zClN{(RiorZ`s5@ACs+SvF(@hC6QI7rRT((Q5RIg?C1^gc9|2h4|@ebuhZ3CcHV0G zTV9W>JKyBJ8Pqb;$wW0H=}b13BUJ`#ZS!J+g(khv27(&Ym45Vb&(;!6>nMy#asFQj*q!MS6`j#(i&j{y8a1yN3IeH-FT&*Q7a%I#}6ZvPVHkeq2 z^4V`v9zzB}3aRmveJM@nm^9c~fD_Ifa)n(x^P>yDcsIPF2U66{VokKZUl@_N`&YWm znI)}ffA^bve|>c3Uw3#*lC~W@T9IiESEV>bsCj4cjtik}U$H)#Fo`YgfvE9a(Bx27zPf@&*Y%vn$>Uh6s{$E?kFxls4kD)X%i z%pJT$`EHm|lp=I<$DQkJxrwi>(eRBdsRUxSdc;1LX*#GA=F(6deUq(9fP1wLUGc(4 zN62xekWqpu2lKrt3-|T5a5+v37F0sr^1ZriO{0EP_HQF%as>zND)O2Bj_`F-w65CAz_@S?se;;r)DmO2n-6)tr}z?PHITA)CcP{9lpWWsY{R@KD+oo;39)tg|8UPz^M4T=2U0 zXxb8V?IBzk;d5#JZRkg|LjL{Qc4W&^?ks7FV;0@BZRnqD5YjyB!(+xmVm(b9f(5M2 zA^Li?#x&`GjL!EAE7LjF*jS|_AvgC2|6=2zTFHVLvh-c=Z}o~XLa>;5~YfPJ)JVcRJ?VCnnV;*PnH#X)rbz%}GczWRD@Qq8mg>>Ks(^`TuQGO_KZ$z*T6-_j0~J$KpXZJIdy7gy(u^<%0|5Dws`v&6V| zgs~2AF)=E^0$*MDR5?34faBl5JJdHE`vl z_oy91Bd=r(s+G329(6-V-+M1Kg@<_cp4g#B&=x`wO6fcj1u9q3jU+l)%?XFoqOah7 ztd6RXp>eV4(l*riK}?wu0`V6gdit$|1v)NU!b9;3<7x&Kq9J+dfMt*ll528Nd4uL91Q{8?-FxZ%9fn3sfPhoq*Q#lK5jOyg+W79rm{zuNtASm!`3@_xx zbNgsd(twqvH$vHeP={0Z50j4boD`}P&Jj0Vgjw^%u+za(87<>1q^2oL2X<2qn|Wym z^aRWZHZdJjrf+vE5>5oy&R<+tRMZGF9Ug;9?|LVOeW?IihKn=-s%_U6x{cY}A@yC{ z5!IqkarQg$ZcN38Y9_V?B;|DVS?*AT<=bT@cJ|YrLY&JzMDe1+C(AA&*9>&btWi}v ztfNw^SxrTLL_%pnuRum&Tc(1Ef}uZppk7S2?p6=eW)OM|TZX*0ZO?PZXd{`M-6RCh z(WEar`GTz>eoXHz;e@0}LEH|Nn50O-yufVoVxh$eT$VePrznaWnPqr5FcYJ_=rx|I zZ&h5C>qZAj-?fxIDfybBN`9D(}vaVxoVJ1{yBtzM^P&4#QHDOUb^UF1Mx Ok04$E From 9d5bfc127e24647e0f7a345a70cef5b86a43c7c4 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 18 Jun 2025 15:36:53 -0400 Subject: [PATCH 0724/1405] Config doc autogen (#2718) * config reference doc autogen * improvements * cleanup; still ugly but working * reformat * remove autogen config ref from git * factor out validations * rewrite * rewrite * cleanup * progress * progress * progress * lint and minifying somewhat * remove unneeded * coderabbit * coderabbit * update preview-docs workflow triggers * installing with deps * coderabbit * update refs * overwrote file accidentally --- .github/workflows/preview-docs.yml | 6 +- .runpod/README.md | 2 +- README.md | 2 +- _quarto.yml | 3 +- docs/.gitignore | 1 + docs/config.qmd | 801 ---------- docs/dataset-formats/conversation.qmd | 6 +- docs/dataset-formats/inst_tune.qmd | 2 +- docs/dataset_loading.qmd | 2 +- docs/getting-started.qmd | 4 +- docs/quantize.qmd | 2 +- docs/scripts/generate_config_docs.py | 752 +++++++++ src/axolotl/utils/schemas/config.py | 1757 ++++++++------------- src/axolotl/utils/schemas/datasets.py | 181 ++- src/axolotl/utils/schemas/deprecated.py | 28 +- src/axolotl/utils/schemas/enums.py | 110 +- src/axolotl/utils/schemas/integrations.py | 105 +- src/axolotl/utils/schemas/model.py | 72 +- src/axolotl/utils/schemas/peft.py | 127 +- src/axolotl/utils/schemas/quantization.py | 26 +- src/axolotl/utils/schemas/training.py | 103 +- src/axolotl/utils/schemas/trl.py | 58 +- src/axolotl/utils/schemas/validation.py | 1073 +++++++++++++ 23 files changed, 3077 insertions(+), 2146 deletions(-) delete mode 100644 docs/config.qmd create mode 100644 docs/scripts/generate_config_docs.py create mode 100644 src/axolotl/utils/schemas/validation.py diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 5af70b0dc5..f93cfa660d 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -8,7 +8,9 @@ on: paths: - '**/*.md' # any Markdown file - '**/*.qmd' # any Quarto file - - '_quarto.yaml' + - '_quarto.yml' + - docs/scripts/generate_config_docs.py + - src/axolotl/utils/schemas/**.py permissions: checks: write @@ -38,7 +40,7 @@ jobs: - name: Install dependencies run: | python3 -m pip install jupyter quartodoc - python3 -m pip install -e . --no-deps + python3 -m pip install -e . - name: Build autodoc run: quartodoc build diff --git a/.runpod/README.md b/.runpod/README.md index a631c3937a..60c661eef0 100644 --- a/.runpod/README.md +++ b/.runpod/README.md @@ -328,7 +328,7 @@ The following optimizers are supported: - Use `gradient_checkpointing: true` to reduce memory usage - Adjust `micro_batch_size` and `gradient_accumulation_steps` based on your GPU memory -For more detailed information, please refer to the [documentation](https://axolotl-ai-cloud.github.io/axolotl/docs/config.html). +For more detailed information, please refer to the [documentation](https://axolotl-ai-cloud.github.io/axolotl/docs/config-reference.html). ### Errors: diff --git a/README.md b/README.md index ef55238984..3bfce8df10 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ That's it! Check out our [Getting Started Guide](https://docs.axolotl.ai/docs/ge ## 📚 Documentation - [Installation Options](https://docs.axolotl.ai/docs/installation.html) - Detailed setup instructions for different environments -- [Configuration Guide](https://docs.axolotl.ai/docs/config.html) - Full configuration options and examples +- [Configuration Guide](https://docs.axolotl.ai/docs/config-reference.html) - Full configuration options and examples - [Dataset Loading](https://docs.axolotl.ai/docs/dataset_loading.html) - Loading datasets from various sources - [Dataset Guide](https://docs.axolotl.ai/docs/dataset-formats/) - Supported formats and how to use them - [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) diff --git a/_quarto.yml b/_quarto.yml index 9b97095ce6..93141aa9e1 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -1,5 +1,6 @@ project: type: website + pre-render: docs/scripts/generate_config_docs.py quartodoc: dir: docs/api @@ -235,7 +236,7 @@ website: - docs/installation.qmd - docs/inference.qmd - docs/cli.qmd - - docs/config.qmd + - docs/config-reference.qmd - text: "API Reference" href: docs/api diff --git a/docs/.gitignore b/docs/.gitignore index 6c3cb2070e..89407326f9 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -2,3 +2,4 @@ _site/ /api/*.qmd /api/*.html +config-reference.qmd diff --git a/docs/config.qmd b/docs/config.qmd deleted file mode 100644 index d146b4c841..0000000000 --- a/docs/config.qmd +++ /dev/null @@ -1,801 +0,0 @@ ---- -title: Config Reference -description: A complete list of all configuration options. ---- - -```yaml -# This is the huggingface model that contains *.pt, *.safetensors, or *.bin files -# This can also be a relative path to a model on disk -base_model: ./llama-7b-hf -# You can specify an ignore pattern if the model repo contains more than 1 model type (*.pt, etc) -base_model_ignore_patterns: -# If the base_model repo on hf hub doesn't include configuration .json files, -# You can set that here, or leave this empty to default to base_model -base_model_config: ./llama-7b-hf -# You can specify to choose a specific model revision from huggingface hub -revision_of_model: -# Optional tokenizer configuration path in case you want to use a different tokenizer -# than the one defined in the base model -tokenizer_config: -# If you want to specify the type of model to load, AutoModelForCausalLM is a good choice too -model_type: AutoModelForCausalLM -# Corresponding tokenizer for the model AutoTokenizer is a good choice -tokenizer_type: AutoTokenizer -# Trust remote code for untrusted source -trust_remote_code: -# use_fast option for tokenizer loading from_pretrained, default to True -tokenizer_use_fast: -# Whether to use the legacy tokenizer setting, defaults to True -tokenizer_legacy: -# Whether to use mistral-common tokenizer. If set to True, it will use the mistral-common tokenizer. -tokenizer_use_mistral_common: -# Resize the model embeddings when new tokens are added to multiples of 32 -# This is reported to improve training speed on some models -resize_token_embeddings_to_32x: -# Optional[bool] Whether to shrink the embeddings to len(tokenizer). By default, we won't shrink. -shrink_embeddings: -# Optional[bool] Don't upcast the embeddings to float32 when using PEFT. Useful for low-VRAM GPUs -embeddings_skip_upcast: -# Whether to load the model with randomly initialized weights. Useful for -# pre-training a model from scratch or debugging purposes. -random_init_weights: - -# (Internal use only) -# Used to identify which the model is based on -is_falcon_derived_model: -is_llama_derived_model: -is_qwen_derived_model: -# Please note that if you set this to true, `padding_side` will be set to "left" by default -is_mistral_derived_model: - -# optional overrides to the base model configuration -overrides_of_model_config: - # RoPE Scaling https://github.com/huggingface/transformers/pull/24653 - rope_scaling: - type: # linear | dynamic - factor: # float - -# optional overrides the base model loading from_pretrained -overrides_of_model_kwargs: - # use_cache: False - -# optional overrides to the bnb 4bit quantization configuration -# https://huggingface.co/docs/transformers/main/main_classes/quantization#transformers.BitsAndBytesConfig -bnb_config_kwargs: - # These are default values - llm_int8_has_fp16_weight: false - bnb_4bit_quant_type: nf4 - bnb_4bit_use_double_quant: true - -# quantization aware training -qat: - activation_dtype: # Optional[str] = "int8". Fake quantization layout to use for activation quantization. Valid options are "int4" and "int8" - weight_dtype: # Optional[str] = "int8". Fake quantization layout to use for weight quantization. Valid options are "int4" and "int8" - group_size: # Optional[int] = 32. The number of elements in each group for per-group fake quantization - fake_quant_after_n_steps: # Optional[int] = None. The number of steps to apply fake quantization after - -# post-training quantization -quantization: - weight_dtype: # Optional[str] = "int8". Fake quantization layout to use for weight quantization. Valid options are uintX for X in [1, 2, 3, 4, 5, 6, 7], or int4, or int8 - activation_dtype: # Optional[str] = "int8". Fake quantization layout to use for activation quantization. Valid options are "int4" and "int8" - group_size: # Optional[int] = 32. The number of elements in each group for per-group fake quantization - quantize_embedding: # Optional[bool] = False. Whether to quantize the embedding layer. - - -# Whether you are training a 4-bit GPTQ quantized model -gptq: true - -# This will attempt to quantize the model down to 8 bits and use adam 8 bit optimizer -load_in_8bit: true -# Use bitsandbytes 4 bit -load_in_4bit: - -# Use CUDA bf16 -bf16: true # bool or 'full' for `bf16_full_eval`, or 'auto' for automatic detection. require >=ampere -# Use CUDA fp16 -fp16: true -# Use CUDA tf32 -tf32: true # require >=ampere -# Note: if bf16 is set to 'auto', and fp16 is set to true, we will prefer the explict fp16 setting - -# No AMP (automatic mixed precision) -bfloat16: true # require >=ampere -float16: true - -# Limit the memory for all available GPUs to this amount (if an integer, expressed in gigabytes); default: unset -gpu_memory_limit: 20GiB -# Do the LoRA/PEFT loading on CPU -- this is required if the base model is so large it takes up most or all of the available GPU VRAM, e.g. during a model and LoRA merge -lora_on_cpu: true - -# List[str]. Add plugins to extend the pipeline. -# See `src/axolotl/integrations` for the available plugins or doc below for more details. -# https://docs.axolotl.ai/docs/custom_integrations.html -plugins: - # - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin - -# A list of one or more datasets to finetune the model with -# See https://docs.axolotl.ai/docs/dataset_loading.html for guide on loading datasets -# See https://docs.axolotl.ai/docs/dataset-formats/ for guide on dataset formats -datasets: - # HuggingFace dataset repo | s3:// | gs:// | path to local file or directory - - path: vicgalle/alpaca-gpt4 - # The type of prompt to use for training. [alpaca, gpteacher, oasst, reflection] - type: alpaca # format | format: (chat/instruct) | .load_ - ds_type: # Optional[str] (json|arrow|parquet|text|csv) defines the datatype when path is a file - data_files: # Optional[str] path to source data files - - shards: # Optional[int] split dataset into N pieces (use with shards_idx) - shards_idx: # Optional[int] = 0 the index of sharded dataset to use - - preprocess_shards: # Optional[int] process dataset in N sequential chunks for memory efficiency (exclusive with `shards`) - - name: # Optional[str] name of dataset configuration to load - split: train # Optional[str] name of dataset split to load from - revision: # Optional[str] The specific revision of the dataset to use when loading from the Hugging Face Hub. This can be a commit hash, tag, or branch name. If not specified, the latest version will be used. This parameter is ignored for local datasets. - trust_remote_code: # Optional[bool] Trust remote code for untrusted source - - # Custom user instruction prompt - - path: repo - type: - # The below are defaults. only set what's needed if you use a different column name. - system_prompt: "" - system_format: "{system}" - field_system: system - field_instruction: instruction - field_input: input - field_output: output - - # Customizable to be single line or multi-line - # Use {instruction}/{input} as key to be replaced - # 'format' can include {input} - format: |- - User: {instruction} {input} - Assistant: - # 'no_input_format' cannot include {input} - no_input_format: "{instruction} " - - # For `completion` datsets only, uses the provided field instead of `text` column - field: - - # Using chat template - - path: ... - # Set type to `chat_template` to use this strategy - type: chat_template - # Specify the name of the chat template to use - # The name of the chat template to use for training, following values are supported: - # - tokenizer_default: Uses the chat template that is available in the tokenizer_config.json. If the chat template is not available in the tokenizer, it will raise an error. This is the default. - # - alpaca/inst/chatml/gemma/cohere/llama3/phi_3/deepseek_v2/jamba: These chat templates are available in the axolotl codebase at src/axolotl/utils/chat_templates.py - # - tokenizer_default_fallback_*: where * is the name of the chat template to fallback to if the tokenizer does not have a chat template else default to tokenizer. E.g. tokenizer_default_fallback_chatml. - # - jinja: Uses a custom jinja template for the chat template. The custom jinja template should be provided in the chat_template_jinja field. - chat_template: tokenizer_default - - # Custom jinja chat template. Used only if `chat_template: jinja` or empty. - chat_template_jinja: - - # Key containing the messages (default: "messages") - field_messages: messages - - # Key containing the tools (default: "tools") - # Must be a list[dict] and follow [JSON schema](https://json-schema.org/learn/getting-started-step-by-step). - field_tools: tools - - # Key containing the system message (default: "system") - # If the system message is not present in the dataset sample, it will be loaded from the field_system property. - field_system: system - - # Mapping of properties from the input dataset to the chat template. - # (default: message_property_mappings={'role':'role', 'content':'content'}) - # If a property exists in the template but not in this mapping, the system will attempt - # to load it directly from the message using the property name as the key. - # Example: In the mapping below, 'from' is loaded from input dataset and used as 'role', - # while 'value' is loaded and used as 'content' in the chat template. - message_property_mappings: - role: from - content: value - # ... - - # Optional[Dict[str, List]]. Roles mapping in the messages. - # The format is {target_role: [source_roles]}. All source roles will be mapped to the target role. - # The default is: - roles: - user: ["human", "user"] - assistant: ["gpt", "assistant"] - system: ["system"] - tool: ["tool"] - - # Optional[bool]. Whether to drop the system turn from the dataset. Only works with chat_template. - # This does not drop the default system message from chat_template if it exists. If you wish to, - # we recommend using a custom jinja template with the default system message removed or - # adding a system turn with empty content. - drop_system_message: - - # Optional[bool]. (for Qwen3 template only) Whether to split the assistant content based on a reasoning trace inside delimited tags - # See example at `docs/dataset-formats/conversation.qmd` - split_thinking: - - # IMPORTANT: The following fields determine which parts of the conversation to train on. - # Priority order: message_field_training > message_field_training_detail > train_on_inputs or role in roles_to_train - # See examples at `docs/dataset-formats/conversation.qmd` - # Note: If the below 5 fields are empty, defaults to training only on the last message. - - # Optional[List[str]]. Roles to train on. The tokens from these roles will be considered for the loss. - roles_to_train: ["assistant"] # default - # Optional[str]. Which EOS tokens to train on in the conversation. Possible values are: - # - all: train on all EOS tokens - # - turn (default): train on the EOS token at the end of each trainable turn - # - last: train on the last EOS token in the conversation - # TIP: Please make sure that your `tokenizer.eos_token` is same as EOS/EOT token in template. Otherwise, set `eos_token` under `special_tokens`. - train_on_eos: turn - # Optional[str]. Which EOT (End-of-Turn) tokens to train on in the conversation. Possible values are: - # - all: train on all EOT tokens - # - turn: train on the EOT token at the end of each trainable turn - # - last: train on the last EOT token in the conversation - # If not specified, defaults to the value of train_on_eos for backward compatibility. - train_on_eot: - # The key in the message turn that indicates via boolean whether tokens of a turn should be considered for training. Useful to selectively train on certain turns besides the `roles_to_train`. - message_field_training: training - # The key in the message turn that contains the training details. Useful to selectively train on certain tokens in a turn. - # The value of the key is a List[Dict] containing `begin_offset` (start character index in content), `end_offset` (end character index in content), and `train` (boolean whether to train). - message_field_training_detail: train_detail - - -# If false, the datasets will not be shuffled and will keep their original order in `datasets`. -# The same applies to the `test_datasets` option and the `pretraining_dataset` option. Default is true. -shuffle_merged_datasets: true - -# Deduplicates datasets and test_datasets with identical entries. -dataset_exact_deduplication: true - -# A list of one or more datasets to eval the model with. -# You can use either test_datasets, or val_set_size, but not both. -test_datasets: - - path: /workspace/data/eval.jsonl - ds_type: json - # You need to specify a split. For "json" datasets the default split is called "train". - split: train - type: completion - data_files: - - /workspace/data/eval.jsonl - -# use RL training: 'dpo', 'ipo', 'kto', 'simpo', 'orpo', 'grpo' -rl: -rl_beta: # Optional[float]. The beta parameter for the RL training. - -# dpo -dpo_use_weighting: # Optional[bool]. Whether to perform weighting. -rpo_alpha: # Optional[float]. Weighting of NLL term in loss from RPO paper. - -# orpo -orpo_alpha: 0.1 # Parameter controlling the relative ratio loss weight in the ORPO loss. Passed to `beta` in `ORPOConfig` due to trl mapping. - -# kto -kto_desirable_weight: # Optional[float]. Factor for desirable loss term in KTO loss. -kto_undesirable_weight: # Optional[float]. Factor for undesirable loss term in KTO loss. - -# simpo -cpo_alpha: 1.0 # Weight of the BC regularizer -simpo_gamma: 0.5 # Target reward margin for the SimPO loss - -# grpo -trl: - use_vllm: # Optional[bool]. Whether to use VLLM for RL training. - vllm_server_host: # Optional[str]. Host of the vLLM server to connect to. - vllm_server_port: # Optional[int]. Port of the vLLM server to connect to. - vllm_server_timeout: # Optional[int]. Total timeout (in seconds) to wait for the vLLM server to respond. - vllm_guided_decoding_regex: # Optional[str]. Regex for vLLM guided decoding. - - beta: # Optional[float]. Beta parameter for the RL training. Same as `rl_beta`. Use - max_completion_length: # Optional[int]. Maximum length of the completion for RL training. - - reward_funcs: # Optional[list[str]]. List of reward functions to load. Paths must be importable from current dir. - reward_weights: # Optional[list[float]]. List of reward weights for the reward functions. - - num_generations: # Optional[int]. Number of generations to sample. - log_completions: # Optional[bool]. Whether to log completions. - num_completions_to_print: # Optional[int]. Number of completions to print when log_completions is True. - - sync_ref_model: # Optional[bool]. Whether to sync the reference model. - ref_model_mixup_alpha: # Optional[float]. Mixup alpha for the reference model. - ref_model_sync_steps: # Optional[int]. Sync steps for the reference model. - scale_rewards: # Optional[bool]. Whether to scale rewards by their standard deviation. - - temperature: # Optional[float]. Sampling temperature for the GRPO policy. - top_p: # Optional[float]. Top-p sampling probability for the generation policy. - top_k: # Optional[int]. Top-k sampling for the generation policy. - min_p: # Optional[float]. Minimum probability for the generation policy. - repetition_penalty: # Optional[float]. Penalty for tokens that appear in prompt and generated text. - - num_iterations: # Optional[int]. Number of iterations per batch (μ) for GRPO. - epsilon: # Optional[float]. Epsilon value for clipping in the GRPO algorithm. - epsilon_high: # Optional[float]. Upper-bound epsilon value for clipping in the GRPO algorithm. - use_liger_loss: # Optional[bool]. Whether to use Liger loss for GRPO. - loss_type: # Optional[str]. Loss formulation to use. Supported values: grpo, bnpo, dr_grpo. - mask_truncated_completions: # Optional[bool]. Whether to exclude truncated completions from loss calculation. - - -# reward modelling: `True` or `False` -reward_model: - -# process reward modelling: `True` or `False` -process_reward_model: - -# The name of the chat template to use for training, following values are supported: -# - tokenizer_default: Uses the chat template that is available in the tokenizer_config.json. If the chat template is not available in the tokenizer, it will raise an error. This is the default value. -# - alpaca/inst/chatml/gemma/cohere/llama3/phi_3/deepseek_v2/jamba: These chat templates are available in the axolotl codebase at src/axolotl/utils/chat_templates.py -# - tokenizer_default_fallback_*: where * is the name of the chat template to fallback to. E.g. tokenizer_default_fallback_chatml. This is useful when the chat template is not available in the tokenizer. -# - jinja: Uses a custom jinja template for the chat template. The custom jinja template should be provided in the chat_template_jinja field. -# The selected chat template will be saved to the tokenizer_config.json for easier inferencing -# Note: It is recommended to set train_on_inputs to true when using a chat template that is different from the model's default chat template. -chat_template: tokenizer_default -# custom jinja template for chat template. This will be only used if chat_template is set to `jinja` or `null` (in which case chat_template is automatically set to `jinja`). Default is null. -chat_template_jinja: null -# Optional[List[str]]. Custom EOT (End-of-Turn) tokens to mask/unmask during training. -# These tokens mark the boundaries between conversation turns. -# For example: ["/INST", "", "[/SYSTEM_PROMPT]"] -# If not specified, defaults to just the model's eos_token. -# This is useful for templates that use multiple delimiter tokens. -eot_tokens: - # - "" - # - "[/INST]" - # - "[/SYSTEM_PROMPT]" -# Changes the default system message -default_system_message: You are a helpful assistant. Please give a long and detailed answer. # Currently only supports chatml. -# Axolotl attempts to save the dataset as an arrow after packing the data together so -# subsequent training attempts load faster, relative path -dataset_prepared_path: data/last_run_prepared -# Push prepared dataset to hub -push_dataset_to_hub: # Optional[str] repo_org/repo_name -# The maximum number of processes to use while preprocessing your input dataset. This defaults to `os.cpu_count()` -# if not set. -dataset_processes: # defaults to os.cpu_count() if not set -# Keep dataset in memory while preprocessing -# Only needed if cached dataset is taking too much storage -dataset_keep_in_memory: -# push checkpoints to hub -hub_model_id: # private repo path to push finetuned model -# how to push checkpoints to hub -# https://huggingface.co/docs/transformers/v4.31.0/en/main_classes/trainer#transformers.TrainingArguments.hub_strategy -hub_strategy: -# Whether to use hf `use_auth_token` for loading datasets. Useful for fetching private datasets -# Required to be true when used in combination with `push_dataset_to_hub` -hf_use_auth_token: # boolean -# How much of the dataset to set aside as evaluation. 1 = 100%, 0.50 = 50%, etc. 0 for no eval. -val_set_size: 0.04 -# Num shards for whole dataset -dataset_shard_num: -# Index of shard to use for whole dataset -dataset_shard_idx: - -# The maximum length of an input to train with, this should typically be less than 2048 -# as most models have a token/context limit of 2048 -sequence_len: 2048 -# Pad inputs so each step uses constant sized buffers -# This will reduce memory fragmentation and may prevent OOMs, by re-using memory more efficiently -pad_to_sequence_len: -# Use efficient multi-packing with block diagonal attention and per sequence position_ids. Recommend set to 'true' -sample_packing: -# Set to 'false' if getting errors during eval with sample_packing on. -eval_sample_packing: -# You can set these packing optimizations AFTER starting a training at least once. -# The trainer will provide recommended values for these values. -sample_packing_eff_est: -total_num_tokens: -# Increasing the following values helps with packing, but usually only slightly (<%1.) -# The number of samples packed at a time. -sample_packing_group_size: 100000 -# The number of samples which can be packed into one sequence. Increase if using a large sequence_len with many short samples. -sample_packing_bin_size: 200 -sample_pack_sequentially: # Optional[bool]. Whether to pack samples sequentially. - -# whether to concatenate samples during pretraining -pretraining_sample_concatenation: - -curriculum_sampling: # Optional[bool]. Whether to use sequential sampling for curriculum learning - -# Use batch flattening for speedups when not using sample_packing -batch_flattening: - -# Passed through to transformers when loading the model when launched without accelerate -# Use `sequential` when training w/ model parallelism to limit memory -device_map: -# Defines the max memory usage per gpu on the system. Passed through to transformers when loading the model. -max_memory: - -# If you want to use 'lora' or 'qlora' or leave blank to train all parameters in original model -adapter: lora -# If you already have a lora model trained that you want to load, put that here. -# This means after training, if you want to test the model, you should set this to the value of `output_dir`. -# Note that if you merge an adapter to the base model, a new subdirectory `merged` will be created under the `output_dir`. -lora_model_dir: - -# LoRA hyperparameters -# For more details about the following options, see: -# https://www.anyscale.com/blog/fine-tuning-llms-lora-or-full-parameter-an-in-depth-analysis-with-llama-2 -lora_r: 8 -lora_alpha: 16 -lora_dropout: 0.05 -lora_target_modules: - - q_proj - - v_proj -# - k_proj -# - o_proj -# - gate_proj -# - down_proj -# - up_proj -lora_target_linear: # If true, will target all linear modules - -# List[int] | int. # The layer indices to transform, otherwise, apply to all layers -# https://huggingface.co/docs/peft/v0.15.0/en/package_reference/lora#peft.LoraConfig.layers_to_transform -peft_layers_to_transform: - -# Optional[bool]. Whether to use DoRA. -# https://huggingface.co/docs/peft/v0.15.0/en/developer_guides/lora#weight-decomposed-low-rank-adaptation-dora -peft_use_dora: - -# Optional[bool]. Whether to use RSLoRA. -# https://huggingface.co/docs/peft/v0.15.0/en/developer_guides/lora#rank-stabilized-lora -peft_use_rslora: - -# Optional[list[tuple[int, int]]]. List of layer indices to replicate. -# https://huggingface.co/docs/peft/v0.15.0/en/developer_guides/lora#memory-efficient-layer-replication-with-lora -peft_layer_replication: - -# bool | Literal["gaussian", "eva", "olora", "pissa", "pissa_niter_[number of iters]", "corda", "loftq"] -# How to initialize LoRA weights. Default to True which is MS original implementation. -# https://huggingface.co/docs/peft/v0.15.0/en/developer_guides/lora#initialization -peft_init_lora_weights: - -# If you added new tokens to the tokenizer, you may need to save some LoRA modules because they need to know the new tokens. -# For LLaMA and Mistral, you need to save `embed_tokens` and `lm_head`. It may vary for other models. -# `embed_tokens` converts tokens to embeddings, and `lm_head` converts embeddings to token probabilities. -# https://github.com/huggingface/peft/issues/334#issuecomment-1561727994 -lora_modules_to_save: -# - embed_tokens -# - lm_head - -lora_fan_in_fan_out: false - -# Apply custom LoRA autograd functions and activation function Triton kernels for -# speed and memory savings -# See: https://docs.axolotl.ai/docs/lora_optims.html -lora_mlp_kernel: true -lora_qkv_kernel: true -lora_o_kernel: true - -# LoRA+ hyperparameters -# For more details about the following options, see: -# https://arxiv.org/abs/2402.12354 and `src/axolotl/core/train_builder.py` -loraplus_lr_ratio: # loraplus learning rate ratio lr_B / lr_A. Recommended value is 2^4. -loraplus_lr_embedding: # loraplus learning rate for lora embedding layers. Default value is 1e-6. - -peft: - # Configuration options for loftq initialization for LoRA - # https://huggingface.co/docs/peft/developer_guides/quantization#loftq-initialization - loftq_config: - loftq_bits: # typically 4 bits - -# ReLoRA configuration -# Must use either 'lora' or 'qlora' adapter, and does not support fsdp or deepspeed -relora_steps: # Number of steps per ReLoRA restart -relora_warmup_steps: # Number of per-restart warmup steps -relora_anneal_steps: # Number of anneal steps for each relora cycle -relora_prune_ratio: # threshold for optimizer magnitude when pruning -relora_cpu_offload: # True to perform lora weight merges on cpu during restarts, for modest gpu memory savings - -# wandb configuration if you're using it -# Make sure your `WANDB_API_KEY` environment variable is set (recommended) or you login to wandb with `wandb login`. -wandb_mode: # "offline" to save run metadata locally and not sync to the server, "disabled" to turn off wandb -wandb_project: # Your wandb project name -wandb_entity: # A wandb Team name if using a Team -wandb_watch: -wandb_name: # Set the name of your wandb run -wandb_run_id: # Set the ID of your wandb run -wandb_log_model: # "checkpoint" to log model to wandb Artifacts every `save_steps` or "end" to log only at the end of training - -# mlflow configuration if you're using it -mlflow_tracking_uri: # URI to mlflow -mlflow_experiment_name: # Your experiment name -mlflow_run_name: # Your run name -hf_mlflow_log_artifacts: # set to true to copy each saved checkpoint on each save to mlflow artifact registry - -# Comet configuration if you're using it -# Make sure your `COMET_API_KEY` environment variable is set (recommended) or you login to Comet with `comet login`. -# Check out our documentation for more details https://www.comet.com/docs/v2/api-and-sdk/python-sdk/reference/Experiment-Creation/#comet_ml.start -use_comet: # Enable or disable Comet integration. -comet_api_key: # API key for Comet. Recommended to set via `comet login`. -comet_workspace: # Workspace name in Comet. Defaults to the user's default workspace. -comet_project_name: # Project name in Comet. Defaults to Uncategorized. -comet_experiment_key: # Identifier for the experiment. Used to append data to an existing experiment or control the key of new experiments. Default to a random key. -comet_mode: # Create a new experiment ("create") or log to an existing one ("get"). Default ("get_or_create") auto-selects based on configuration. -comet_online: # Set to True to log data to Comet server, or False for offline storage. Default is True. -comet_experiment_config: # Dictionary for additional configuration settings, see the doc for more details. - -# Tensorboard -use_tensorboard: # Optional[bool] - -# Where to save the full-finetuned model to -output_dir: ./completed-model - -# Whether to use torch.compile and which backend to use -# setting to `auto` will enable torch compile when torch>=2.5.1 -torch_compile: # Optional[Union[Literal["auto"], bool]] -torch_compile_backend: # Optional[str] -torch_compile_mode: # 'default' | 'reduce-overhead' | 'max-autotune' - -# Training hyperparameters - -# If greater than 1, backpropagation will be skipped and the gradients will be accumulated for the given number of steps. -gradient_accumulation_steps: 1 -# The number of samples to include in each batch. This is the number of samples sent to each GPU. -# Batch size per gpu = micro_batch_size * gradient_accumulation_steps -micro_batch_size: 2 -eval_batch_size: -num_epochs: 4 -warmup_steps: 100 # cannot use with warmup_ratio -warmup_ratio: 0.05 # cannot use with warmup_steps -learning_rate: 0.00003 -lr_quadratic_warmup: -logging_steps: -eval_steps: # Leave empty to eval at each epoch, integer for every N steps. float for fraction of total steps -evals_per_epoch: # number of times per epoch to run evals, mutually exclusive with eval_steps -eval_strategy: # Set to `"no"` to skip evaluation, `"epoch"` at end of each epoch, leave empty to infer from `eval_steps`. -save_strategy: # Set to `"no"` to skip checkpoint saves, `"epoch"` at end of each epoch, `"best"` when better result is achieved, leave empty to infer from `save_steps`. -save_steps: # Leave empty to save at each epoch, integer for every N steps. float for fraction of total steps -saves_per_epoch: # number of times per epoch to save a checkpoint, mutually exclusive with save_steps -save_total_limit: # Checkpoints saved at a time -save_only_model: # Save only the model weights, skipping the optimizer. Using this means you can't resume from checkpoints. -# Maximum number of iterations to train for. It precedes num_epochs which means that -# if both are set, num_epochs will not be guaranteed. -# e.g., when 1 epoch is 1000 steps => `num_epochs: 2` and `max_steps: 100` will train for 100 steps -max_steps: - -# bool of whether to include tokens trainer per second in the training metrics. This iterates over the entire dataset once, so it takes some time. -include_tokens_per_second: # Optional[bool] - -# whether to find batch size that fits in memory. Passed to underlying transformers Trainer -auto_find_batch_size: # Optional[bool] - -eval_table_size: # Approximate number of predictions sent to wandb depending on batch size. Enabled above 0. Default is 0 -eval_max_new_tokens: # Total number of tokens generated for predictions sent to wandb. Default is 128 -do_causal_lm_eval: # Whether to run causal language model evaluation for metrics in `eval_causal_lm_metrics`. -eval_causal_lm_metrics: # HF evaluate metrics used during evaluation. Default is ["sacrebleu", "comet", "ter", "chrf", "perplexity"] - -profiler_steps: # enable the pytorch profiler to capture the first N steps of training to the output_dir. - # see https://pytorch.org/blog/understanding-gpu-memory-1/ for more information - # snapshots can be visualized @ https://pytorch.org/memory_viz - -loss_watchdog_threshold: # High loss value, indicating the learning has broken down (a good estimate is ~2 times the loss at the start of training) -loss_watchdog_patience: # Number of high-loss steps in a row before the trainer aborts (default: 3) - -# Save model as safetensors (require safetensors package). Default True -save_safetensors: - -# Whether to mask out or include the human's prompt from the training labels -train_on_inputs: false -# Group similarly sized data to minimize padding. -# May be slower to start, as it must download and sort the entire dataset. -# Note that training loss may have an oscillating pattern with this enabled. -group_by_length: false - -# Whether to use gradient checkpointing. Available options are: true, false, "offload", "offload_disk". -# https://huggingface.co/docs/transformers/v4.18.0/en/performance#gradient-checkpointing -gradient_checkpointing: false -# additional kwargs to pass to the trainer for gradient checkpointing -# gradient_checkpointing_kwargs: -# use_reentrant: true - -# Stop training after this many evaluation losses have increased in a row -# https://huggingface.co/transformers/v4.2.2/_modules/transformers/trainer_callback.html#EarlyStoppingCallback -early_stopping_patience: 3 - -# Specify a scheduler and kwargs to use with the optimizer -# Valid values are driven by the Transformers SchedulerType class, see: -# https://github.com/huggingface/transformers/blob/5f4ecf2d9f867a1255131d2461d75793c0cf1db2/src/transformers/trainer_utils.py#L420 -# Valid values include -# - 'linear' -# - 'cosine' (default) -# - 'cosine_with_restarts' -# - 'polynomial' -# - 'constant' -# - 'constant_with_warmup' -# - 'inverse_sqrt' -# - 'reduce_lr_on_plateau' -# - 'cosine_with_min_lr' -# - 'warmup_stable_decay' - -# Additional schedulers include: -# - 'one_cycle' -# - 'rex' -lr_scheduler: -lr_scheduler_kwargs: -cosine_min_lr_ratio: # decay lr to some percentage of the peak lr, e.g. cosine_min_lr_ratio=0.1 for 10% of peak lr -cosine_constant_lr_ratio: # freeze lr at some percentage of the step, e.g. cosine_constant_lr_ratio=0.8 means start cosine_min_lr at 80% of training step (https://arxiv.org/pdf/2308.04014.pdf) - -# For one_cycle optim -lr_div_factor: # Learning rate div factor - -# Specify optimizer -# Valid values are driven by the Transformers OptimizerNames class, see: -# https://github.com/huggingface/transformers/blob/cbf924b76c03828101a34069a96d209314114fd5/src/transformers/training_args.py#L144-L189 -# -# Note that not all optimizers may be available in your environment, ex: 'adamw_anyprecision' is part of -# torchdistx, 'adamw_bnb_8bit' is part of bnb.optim.Adam8bit, etc. When in doubt, it is recommended to start with the optimizer used -# in the examples/ for your model and fine-tuning use case. -# -# Valid values for 'optimizer' include: -# - adamw_torch -# - adamw_torch_fused (default) -# - adamw_torch_xla -# - adamw_torch_npu_fused -# - adamw_apex_fused -# - adopt_adamw (an EXPERIMENTAL optimizer, only for torch version >= 2.5.1) -# - adafactor -# - adamw_anyprecision -# - adamw_torch_4bit -# - ademamix -# - sgd -# - adagrad -# - adamw_bnb_8bit -# - adamw_8bit # alias for adamw_bnb_8bit -# - ademamix_8bit -# - lion_8bit -# - lion_32bit -# - paged_adamw_32bit -# - paged_adamw_8bit -# - paged_ademamix_32bit -# - paged_ademamix_8bit -# - paged_lion_32bit -# - paged_lion_8bit -# - rmsprop -# - rmsprop_bnb -# - rmsprop_bnb_8bit -# - rmsprop_bnb_32bit -# - galore_adamw -# - galore_adamw_8bit -# - galore_adafactor -# - galore_adamw_layerwise -# - galore_adamw_8bit_layerwise -# - galore_adafactor_layerwise -# - lomo -# - adalomo -# - grokadamw -# - schedule_free_adamw -# - schedule_free_sgd -# - apollo_adamw -# - apollo_adamw_layerwise -# -# Additional custom optimizers include: -# - optimi_adamw -# - ao_adamw_8bit -# - ao_adamw_fp8 -# - came_pytorch -optimizer: -# Dictionary of arguments to pass to the optimizer -optim_args: -# For Galore Optimizers the following optim_args are available -# rank: # type: int -# update_proj_gap # type: int -# scale # type: float -# proj_type: # type: str, default = std - -# The target modules to optimize, i.e. the module names that you would like to train, right now this is used only for GaLore algorithm -optim_target_modules: -# - self_attn # for llama -# - mlp - -# Specify weight decay -weight_decay: -# adamw hyperparams -adam_beta1: -adam_beta2: -adam_beta3: # only used for CAME Optimizer -adam_epsilon: -adam_epsilon2: # only used for CAME Optimizer -# Gradient clipping max norm -max_grad_norm: - -# Augmentation techniques -# NEFT https://arxiv.org/abs/2310.05914, set this to a number (paper default is 5) to add noise to embeddings -# currently only supported on Llama and Mistral -neftune_noise_alpha: - -# Optional[bool]. Whether to bettertransformers -flash_optimum: - -# Note: Only one of the following attention patches can be used at a time. -# For example, if you set `xformers_attention` to `true`, do not set `flash_attention` to `true`. - -# Optional[bool]. Whether to use xformers attention patch https://github.com/facebookresearch/xformers: -xformers_attention: -# Optional[bool]. Whether to use flash attention patch https://github.com/Dao-AILab/flash-attention: -flash_attention: -flash_attn_cross_entropy: # Optional[bool]. Whether to use flash-attention cross entropy implementation - advanced use only -flash_attn_rms_norm: # Optional[bool]. Whether to use flash-attention rms norm implementation - advanced use only -flash_attn_fuse_qkv: # Optional[bool]. Whether to fuse QKV into a single operation -flash_attn_fuse_mlp: # Optional[bool]. Whether to fuse part of the MLP into a single operation -# Optional[bool]. Whether to use scaled-dot-product attention -# https://pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html -sdp_attention: -# Optional[bool]. Shifted-sparse attention (only llama) - https://arxiv.org/pdf/2309.12307.pdf -s2_attention: - -# Optional[bool]. Whether to use low_cpu_mem_usage -low_cpu_mem_usage: -# Optional[str]. Resume from a specific checkpoint dir -resume_from_checkpoint: -# Optional[bool]. If resume_from_checkpoint isn't set and you simply want it to start where it left off. -# Be careful with this being turned on between different models. -auto_resume_from_checkpoints: false - -## Multimodal section -# int | tuple[int, int] | None . Size to resize images to, width x height. -# Will read from model/processor config if not set. -image_size: -# str. Algorithm to use for image resizing. "bilinear", "bicubic", "lanczos". Default is "bilinear". -image_resize_algorithm: 'bilinear' -## End of multimodal section - -# Don't mess with this, it's here for accelerate and torchrun -local_rank: - -# Add or change special tokens. -# If you add tokens here, you don't need to add them to the `tokens` list. -special_tokens: - # bos_token: "" - # eos_token: "" - # unk_token: "" - # pad_token: "[PAD]" - -# Optional[list[str]]. Add extra tokens to the tokenizer. -tokens: - # - "<|startoftext|>" - # - "<|endoftext|>" - -# Mapping token_id to new_token_string to override reserved added_tokens in the tokenizer. -# Only works for tokens that are not part of the base vocab (aka are added_tokens). -# Can be checked if they exist in tokenizer.json added_tokens. -added_tokens_overrides: # Dict[int, str] -# 128041: "<|im_start|>" -# 128042: "<|im_end|>" - -# FSDP -fsdp: -fsdp_config: - -# Deepspeed config path. e.g., deepspeed_configs/zero3.json -deepspeed: - -# Advanced DDP Arguments -ddp_timeout: -ddp_bucket_cap_mb: -ddp_broadcast_buffers: - -# Sequence parallelism -# Set to a divisor of the number of GPUs available to split sequences into chunks of equal size. -# Use in long context training to prevent OOM when sequences cannot fit into a single GPU's VRAM. -# E.g., if 4 GPUs are available, set this value to 2 to split each sequence into two equal-sized -# subsequences, or set to 4 to split into four equal-sized subsequences. -# See https://docs.axolotl.ai/docs/sequence_parallelism.html for more details. -sequence_parallel_degree: -# Optional; strides across the key dimension. Larger values use more memory but should make training faster. -# Must evenly divide the number of KV heads in your model. -heads_k_stride: 1 -# One of "varlen_llama3", "batch_ring", "batch_zigzag", "batch_stripe". Defaults to "varlen_llama3" -# in the sample packing case, and "batch_ring" in the non-sample packing case. -ring_attn_func: - -# Path to torch distx for optim 'adamw_anyprecision' -torchdistx_path: - -# Set to HF dataset for type: 'completion' for streaming instead of pre-tokenize -pretraining_dataset: - -# Debug mode -debug: - -# Seed -seed: - -# Allow overwrite yml config using from cli -strict: -``` diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 290841c085..d1fca9441e 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -12,7 +12,7 @@ Chat Template strategy uses a jinja2 template that converts a list of messages i {"conversations": [{"role": "...", "content": "..."}]} ``` -See [configs](../config.qmd) for full configs and supported templates. +See [configs](../config-reference.qmd) for full configs and supported templates. ### Migrating from sharegpt @@ -130,13 +130,13 @@ datasets: ``` ::: {.callout-tip} -See [config documentation](../config.qmd) for detailed explanations of "turn", "last", and "all" options for training on tokens. +See [config documentation](../config-reference.qmd) for detailed explanations of "turn", "last", and "all" options for training on tokens. ::: ::: {.callout-note} Using `eot_tokens` requires each token that exists in `chat_template` to be a single token in the tokenizer. Otherwise, the tokenizer will split the token and cause unexpected behavior. -You can add those tokens as new tokens under `tokens: ` or (recommended) override unused added_tokens via `added_tokens_overrides: `. See [config](../config.qmd) for more details. +You can add those tokens as new tokens under `tokens: ` or (recommended) override unused added_tokens via `added_tokens_overrides: `. See [config](../config-reference.qmd) for more details. ::: - Continuing from the previous example, if you want to train on all EOT token trainable turns but only last EOS token, set `train_on_eos: last`. diff --git a/docs/dataset-formats/inst_tune.qmd b/docs/dataset-formats/inst_tune.qmd index d89c6adaf5..f5bd7ab8f1 100644 --- a/docs/dataset-formats/inst_tune.qmd +++ b/docs/dataset-formats/inst_tune.qmd @@ -186,4 +186,4 @@ datasets: no_input_format: "[INST] {instruction} [/INST]" ``` -See full config options under [here](../config.qmd). +See full config options under [here](../config-reference.qmd). diff --git a/docs/dataset_loading.qmd b/docs/dataset_loading.qmd index b78f86a983..bcffe7f0f3 100644 --- a/docs/dataset_loading.qmd +++ b/docs/dataset_loading.qmd @@ -36,7 +36,7 @@ This matches the API of [`datasets.load_dataset`](https://github.com/huggingface For HuggingFace's guide to load different dataset types, see [here](https://huggingface.co/docs/datasets/loading). -For full details on the config, see [config.qmd](config.qmd). +For full details on the config, see [config-reference.qmd](config-reference.qmd). ::: {.callout-note} diff --git a/docs/getting-started.qmd b/docs/getting-started.qmd index 6f1b543488..de059c397f 100644 --- a/docs/getting-started.qmd +++ b/docs/getting-started.qmd @@ -55,7 +55,7 @@ output_dir: ./outputs/lora-out - To perform QLoRA finetuning, replace with `load_in_4bit: true` and `adapter: qlora`. ::: -See our [Config options](config.qmd) for more details. +See our [config options](config-reference.qmd) for more details. ### Training {#sec-training} @@ -179,7 +179,7 @@ Now that you have the basics, you might want to: Check our other guides for details on these topics: -- [Configuration Guide](config.qmd) - Full configuration options +- [Configuration Guide](config-reference.qmd) - Full configuration options - [Dataset Loading](dataset_loading.qmd) - Loading datasets from various sources - [Dataset Formats](dataset-formats) - Working with different data formats - [Multi-GPU Training](multi-gpu.qmd) diff --git a/docs/quantize.qmd b/docs/quantize.qmd index 294efda8b4..113fcafbed 100644 --- a/docs/quantize.qmd +++ b/docs/quantize.qmd @@ -32,7 +32,7 @@ output_dir: # The path to the output directory. Once quantization is complete, your quantized model will be saved in the `{output_dir}/quantized` directory. -You may also use the `quantize` command to quantize a model which has been trained with [QAT](./qat.md) - you can do this by using the existing QAT configuration file which +You may also use the `quantize` command to quantize a model which has been trained with [QAT](./qat.qmd) - you can do this by using the existing QAT configuration file which you used to train the model: ```yaml diff --git a/docs/scripts/generate_config_docs.py b/docs/scripts/generate_config_docs.py new file mode 100644 index 0000000000..e22da7d050 --- /dev/null +++ b/docs/scripts/generate_config_docs.py @@ -0,0 +1,752 @@ +# type: ignore + +""" +Quarto documentation generation from Pydantic models. Uses Pydantic model source code +to automatically group fields, including inherited fields from parent classes. +""" + +import ast +import inspect +import textwrap +import types +import typing +from typing import Any, FrozenSet, Type, Union + +from pydantic import BaseModel + +from axolotl.utils.schemas.config import AxolotlInputConfig + + +class QuartoGenerator: + """Generate Quarto documentation from Pydantic models.""" + + def __init__(self): + self._class_fields_cache = {} + self._inheritance_map_cache = {} + self._nested_models_cache = {} + + def _get_direct_fields(self, cls: Type[BaseModel]) -> FrozenSet[str]: + """Get fields defined directly in a single class (not inherited).""" + if cls in self._class_fields_cache: + return self._class_fields_cache[cls] + + fields = set() + + # Get annotated fields + if hasattr(cls, "__annotations__"): + fields.update(cls.__annotations__.keys()) + + # Filter out private/special methods + fields = {f for f in fields if not f.startswith("_")} + + result = frozenset(fields) + self._class_fields_cache[cls] = result + return result + + def _is_pydantic_model(self, type_obj) -> bool: + """Check if a type is a Pydantic BaseModel.""" + return inspect.isclass(type_obj) and issubclass(type_obj, BaseModel) + + # pylint: disable=too-many-return-statements + def _extract_nested_type(self, field_type) -> Any: + """Extract the actual type from complex type annotations.""" + # Handle Annotated types (Python 3.9+) + if hasattr(typing, "get_origin") and hasattr(typing, "get_args"): + origin = typing.get_origin(field_type) + args = typing.get_args(field_type) + + if origin is not None: + # Handle Annotated[SomeType, ...] - extract the first argument + if hasattr(typing, "Annotated") and origin is typing.Annotated: + if args: + return self._extract_nested_type( + args[0] + ) # Recursively process the actual type + + # Handle list[SomeType], List[SomeType], etc. + elif origin in (list, typing.List): + if args: + return self._extract_nested_type( + args[0] + ) # Extract element type + + # Handle Union types (including | syntax) + elif origin is typing.Union: + # Get non-None types from the Union + non_none_types = [arg for arg in args if arg is not type(None)] + if len(non_none_types) >= 1: + # Prioritize Pydantic models over primitive types + pydantic_models = [ + arg + for arg in non_none_types + if self._is_pydantic_model(arg) + ] + if pydantic_models: + # Return the first Pydantic model found + return self._extract_nested_type(pydantic_models[0]) + + # No Pydantic models, return the first non-None type + return self._extract_nested_type(non_none_types[0]) + + # Handle new Python 3.10+ union syntax (PeftConfig | None) + if hasattr(field_type, "__class__") and field_type.__class__ is types.UnionType: + # Get non-None types from the Union + non_none_types = [ + arg for arg in field_type.__args__ if arg is not type(None) + ] + if len(non_none_types) >= 1: + # Prioritize Pydantic models over primitive types + pydantic_models = [ + arg for arg in non_none_types if self._is_pydantic_model(arg) + ] + if pydantic_models: + return self._extract_nested_type(pydantic_models[0]) + return self._extract_nested_type(non_none_types[0]) + + # Handle old typing.Union syntax (fallback) + if hasattr(field_type, "__origin__"): + if field_type.__origin__ is Union: + # Get non-None types from the Union + non_none_types = [ + arg for arg in field_type.__args__ if arg is not type(None) + ] + if len(non_none_types) >= 1: + # Prioritize Pydantic models over primitive types + pydantic_models = [ + arg for arg in non_none_types if self._is_pydantic_model(arg) + ] + if pydantic_models: + return self._extract_nested_type(pydantic_models[0]) + return self._extract_nested_type(non_none_types[0]) + # Handle other generic types like dict[str, Any], etc. + elif hasattr(field_type, "__args__"): + return field_type + + return field_type + + # pylint: disable=too-many-return-statements + def _extract_all_pydantic_models_from_type( + self, field_type + ) -> list[type[BaseModel]]: + """Extract all Pydantic models from a type annotation, including from Unions.""" + models = [] + + if field_type is None: + return models + + # Handle Annotated types + if hasattr(typing, "get_origin") and hasattr(typing, "get_args"): + origin = typing.get_origin(field_type) + args = typing.get_args(field_type) + + if origin is not None: + # Handle Annotated[SomeType, ...] - extract from the first argument + if hasattr(typing, "Annotated") and origin is typing.Annotated: + if args: + models.extend( + self._extract_all_pydantic_models_from_type(args[0]) + ) + return models + + # Handle list[SomeType], List[SomeType], etc. + if origin in (list, typing.List): + if args: + models.extend( + self._extract_all_pydantic_models_from_type(args[0]) + ) + return models + + # Handle Union types + if origin is typing.Union: + for arg in args: + if arg is not type(None): # Skip None type + models.extend( + self._extract_all_pydantic_models_from_type(arg) + ) + return models + + # Handle new Python 3.10+ union syntax + if hasattr(field_type, "__class__") and field_type.__class__ is types.UnionType: + for arg in field_type.__args__: + if arg is not type(None): # Skip None type + models.extend(self._extract_all_pydantic_models_from_type(arg)) + return models + + # Handle old typing.Union syntax (fallback) + if hasattr(field_type, "__origin__") and field_type.__origin__ is Union: + for arg in field_type.__args__: + if arg is not type(None): # Skip None type + models.extend(self._extract_all_pydantic_models_from_type(arg)) + return models + + # Check if this type itself is a Pydantic model + if self._is_pydantic_model(field_type): + models.append(field_type) + + return models + + def _get_nested_models( + self, model_class: type[BaseModel], visited=None + ) -> dict[str, type[BaseModel]]: + """Get all nested Pydantic models from a model class.""" + if visited is None: + visited = set() + + # Avoid infinite recursion + if model_class in visited: + return {} + + if model_class in self._nested_models_cache: + return self._nested_models_cache[model_class] + + visited.add(model_class) + nested_models = {} + + # Check all fields in the model + for field_info in model_class.model_fields.values(): + field_type = self._extract_nested_type(field_info.annotation) + + if self._is_pydantic_model(field_type): + nested_models[field_type.__name__] = field_type + # Recursively get nested models from this nested model + deeper_nested = self._get_nested_models(field_type, visited.copy()) + nested_models.update(deeper_nested) + + self._nested_models_cache[model_class] = nested_models + return nested_models + + def _build_inheritance_map(self, child_class: Type[BaseModel]): + """Build inheritance map for a class and all its parents.""" + if child_class in self._inheritance_map_cache: + return self._inheritance_map_cache[child_class] + + inheritance_map = {} + + # Get MRO and filter out BaseModel and object + mro_classes = [ + cls + for cls in child_class.__mro__ + if cls not in (BaseModel, object) and hasattr(cls, "__annotations__") + ] + + # Process each class in the MRO + for cls in mro_classes: + inheritance_map[cls] = self._get_direct_fields(cls) + + self._inheritance_map_cache[child_class] = inheritance_map + return inheritance_map + + def _wrap_comment(self, text: str, width: int = 88) -> list[str]: + """Wrap a comment to specified width, accounting for '# ' prefix.""" + if not text.strip(): + return ["#"] + + # Account for "# " prefix (2 characters) + content_width = width - 2 + wrapped_lines = textwrap.wrap(text, width=content_width) + return [f"# {line}" for line in wrapped_lines] + + def _extract_type_from_source( + self, model_class: type[BaseModel], field_name: str + ) -> str: + """Extract the actual type annotation text from source code, checking inheritance chain.""" + # Use inheritance map to check classes efficiently + inheritance_map = self._build_inheritance_map(model_class) + + # Check classes in MRO order + for cls in model_class.__mro__: + if cls in inheritance_map and field_name in inheritance_map[cls]: + type_annotation = self._get_type_from_class_source(cls, field_name) + if type_annotation != "unknown": + return type_annotation + + return "unknown" + + def _get_type_from_class_source(self, class_obj: type, field_name: str) -> str: + """Extract type annotation from a specific class's source code.""" + try: + source = inspect.getsource(class_obj) + tree = ast.parse(source) + except (OSError, TypeError): + return "unknown" + + # Find the class definition + for node in tree.body: + if isinstance(node, ast.ClassDef) and node.name == class_obj.__name__: + # Find the field assignment + for body_node in node.body: + if isinstance(body_node, ast.AnnAssign) and isinstance( + body_node.target, ast.Name + ): + if body_node.target.id == field_name and body_node.annotation: + return ast.unparse(body_node.annotation) + break + + return "unknown" + + def _extract_field_groups_from_all_classes( + self, model_class: type[BaseModel] + ) -> list[dict]: + """Extract field groups from all classes in the inheritance hierarchy.""" + all_groups = [] + inheritance_map = self._build_inheritance_map(model_class) + + # Get all Pydantic base classes in MRO order (most specific first) + # This puts AxolotlInputConfig fields first, then parent class fields + pydantic_classes = [ + cls + for cls in model_class.__mro__ + if cls in inheritance_map and inheritance_map[cls] + ] + + # Extract groups from each class + for cls in pydantic_classes: + class_groups = self._extract_field_groups_from_source(cls) + for group in class_groups: + all_groups.append(group) + + # If no groups found, create a default grouping by class + if not all_groups: + for cls in pydantic_classes: + fields_in_class = inheritance_map[cls] + if fields_in_class: + all_groups.append( + { + "fields": list(fields_in_class), + } + ) + + return all_groups + + # pylint: disable=too-many-return-statements + def _extract_field_groups_from_source( + self, model_class: type[BaseModel] + ) -> list[dict]: + """Extract field groups from source code based on blank lines and comments.""" + try: + source = inspect.getsource(model_class) + tree = ast.parse(source) + except (OSError, TypeError): + # Fallback if we can't get source code + fields_in_class = self._get_direct_fields(model_class) + if fields_in_class: + return [ + { + "fields": list(fields_in_class), + } + ] + return [] + + groups = [] + current_group_fields = [] + current_group_comment = None + + # Find the class definition + class_node = None + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and node.name == model_class.__name__: + class_node = node + break + + if not class_node: + fields_in_class = self._get_direct_fields(model_class) + if fields_in_class: + return [ + { + "fields": list(fields_in_class), + } + ] + return [] + + # Parse the source lines to detect groupings + source_lines = source.split("\n") + + # Get fields that are actually defined in this specific class + fields_in_class = self._get_direct_fields(model_class) + + # Find assignments that correspond to model fields for THIS class only + field_assignments = [] + for node in class_node.body: + if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name): + field_name = node.target.id + if field_name in fields_in_class: + field_assignments.append( + { + "name": field_name, + "lineno": node.lineno, + "end_lineno": getattr(node, "end_lineno", node.lineno), + } + ) + + if not field_assignments: + if fields_in_class: + return [ + { + "fields": list(fields_in_class), + } + ] + return [] + + # Sort by line number + field_assignments.sort(key=lambda x: x["lineno"]) + + # Group fields based on blank lines and comments + for i, field_info in enumerate(field_assignments): + field_name = field_info["name"] + current_line = field_info["lineno"] + + # Check if this starts a new group (blank line before or significant gap) + is_new_group = False + + if i == 0: + is_new_group = True + else: + prev_end_line = field_assignments[i - 1]["end_lineno"] + + # Check for blank lines or comments between fields + lines_between = source_lines[prev_end_line : current_line - 1] + has_blank_line = any(line.strip() == "" for line in lines_between) + has_comment = any( + line.strip().startswith("#") for line in lines_between + ) + + # Start new group if there's a blank line or comment, or significant gap + if has_blank_line or has_comment or (current_line - prev_end_line > 3): + is_new_group = True + + if is_new_group and current_group_fields: + # Save the previous group + groups.append( + { + "fields": current_group_fields.copy(), + "description": current_group_comment, + } + ) + current_group_fields = [] + current_group_comment = None + + current_group_fields.append(field_name) + + # Add the final group + if current_group_fields: + groups.append( + { + "fields": current_group_fields, + "description": current_group_comment, + } + ) + + return groups + + def _generate_field_documentation( + self, + model_class: type[BaseModel], + field_name: str, + field_info: dict, + field_type_str: str, + is_required: bool, + indent_level: int = 0, + visited_models: set = None, + ) -> list[str]: + """Generate documentation for a single field, expanding nested models inline.""" + if visited_models is None: + visited_models = set() + + lines = [] + indent = " " * indent_level + + # Get the actual field type for nested model detection + if field_name in model_class.model_fields: + pydantic_field_info = model_class.model_fields[field_name] + actual_field_type = pydantic_field_info.annotation + else: + actual_field_type = None + + # Add description comment if available + description = field_info.get("description", "") + if description: + wrapped_lines = self._wrap_comment(description, width=88 - len(indent)) + for line in wrapped_lines: + lines.append(f"{indent}{line}") + + # Extract nested Pydantic models from the type annotation + nested_models = self._extract_all_pydantic_models_from_type(actual_field_type) + + # Filter out already visited models to prevent infinite recursion + expandable_models = [ + model for model in nested_models if model not in visited_models + ] + + if expandable_models: + # This field contains Pydantic models that can be expanded + + # Show the field with its full type annotation + field_line = f"{indent}{field_name}: {field_type_str}" + if field_info.get("default") is not None: + field_line += f" = {field_info['default']}" + if is_required: + field_line += " (required)" + lines.append(field_line) + + # Add to visited to prevent infinite recursion + new_visited = visited_models.copy() + new_visited.update(expandable_models) + + # Expand each nested Pydantic model + for i, nested_model in enumerate(expandable_models): + if i > 0: + lines.append("\n") + lines.append(f"{indent} # For {nested_model.__name__}:") + + # Get nested model schema + try: + nested_schema = nested_model.model_json_schema() + nested_properties = nested_schema.get("properties", {}) + nested_required = nested_schema.get("required", []) + except Exception: # pylint: disable=broad-exception-caught + # Fallback: use model fields directly + nested_properties = {} + nested_required = [] + for ( + nested_field_name, + nested_field_info, + ) in nested_model.model_fields.items(): + nested_description = "" + if ( + hasattr(nested_field_info, "json_schema_extra") + and nested_field_info.json_schema_extra + ): + nested_description = ( + nested_field_info.json_schema_extra.get( + "description", "" + ) + ) + elif ( + hasattr(nested_field_info, "description") + and nested_field_info.description + ): + nested_description = nested_field_info.description + + nested_default_val = None + if ( + hasattr(nested_field_info, "default") + and nested_field_info.default is not None + ): + if str(nested_field_info.default) != "PydanticUndefined": + nested_default_val = nested_field_info.default + + nested_properties[nested_field_name] = { + "type": "unknown", + "description": nested_description, + "default": nested_default_val, + } + + if nested_field_info.is_required(): + nested_required.append(nested_field_name) + + # Get field groups for the nested model + nested_field_groups = self._extract_field_groups_from_all_classes( + nested_model + ) + + # Generate nested fields with increased indentation + for i, group in enumerate(nested_field_groups): + if not group["fields"]: + continue + + # Add blank line between groups (except before first group) + if i > 0: + lines.append("") + + # Process nested fields + for nested_field_name in group["fields"]: + if nested_field_name not in nested_properties: + continue + + nested_field_info = nested_properties[nested_field_name] + nested_field_type = self._extract_type_from_source( + nested_model, nested_field_name + ) + nested_is_required = nested_field_name in nested_required + + # Recursively generate documentation for nested field + nested_lines = self._generate_field_documentation( + nested_model, + nested_field_name, + nested_field_info, + nested_field_type, + nested_is_required, + indent_level + 1, + new_visited, + ) + lines.extend(nested_lines) + else: + # Regular field (no expandable nested models) + field_line = f"{indent}{field_name}: {field_type_str}" + if field_info.get("default") is not None: + field_line += f" = {field_info['default']}" + if is_required: + field_line += " (required)" + lines.append(field_line) + + return lines + + def generate_qmd( + self, + model_class: type[BaseModel], + title: str | None = None, + expand_nested: bool = True, + ) -> str: + """Auto-generate config reference documentation including inherited fields.""" + + if title is None: + title = f"{model_class.__name__} Reference" + + # Try to get JSON schema, with fallback for serialization issues + try: + schema = model_class.model_json_schema() + properties = schema.get("properties", {}) + required = schema.get("required", []) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Warning: Could not generate JSON schema ({e}). Using model fields instead." + ) + # Fallback: use model fields directly + properties = {} + required = [] + for field_name, field_info in model_class.model_fields.items(): + # Extract description from json_schema_extra or field info + description = "" + if ( + hasattr(field_info, "json_schema_extra") + and field_info.json_schema_extra + ): + description = field_info.json_schema_extra.get("description", "") + elif hasattr(field_info, "description") and field_info.description: + description = field_info.description + + # Get default value + default_val = None + if hasattr(field_info, "default") and field_info.default is not None: + # Handle special Pydantic default markers + if str(field_info.default) != "PydanticUndefined": + default_val = field_info.default + + properties[field_name] = { + "type": "unknown", + "description": description, + "default": default_val, + } + + if field_info.is_required(): + required.append(field_name) + + # Extract field groups from all classes in inheritance hierarchy + field_groups = self._extract_field_groups_from_all_classes(model_class) + + # Start building QMD content + qmd_lines = [ + "---", + f"title: {title}", + "description: A complete list of all configuration options.", + "---", + "", + ] + + # Generate one big code block with all fields (inline nested expansion) + qmd_lines.append("```yaml") + + for i, group in enumerate(field_groups): + if not group["fields"]: + continue + + # Add blank line between groups (except before first group) + if i > 0: + qmd_lines.append("") + + # Process fields in the order they appear in source + for field_name in group["fields"]: + if field_name not in properties: + continue + + field_info = properties[field_name] + field_type = self._extract_type_from_source(model_class, field_name) + is_required = field_name in required + + if expand_nested: + # Check if this field has nested models + if field_name in model_class.model_fields: + pydantic_field_info = model_class.model_fields[field_name] + nested_models = self._extract_all_pydantic_models_from_type( + pydantic_field_info.annotation + ) + has_nested = bool(nested_models) + else: + has_nested = False + + # Add blank line before nested config + if has_nested: + qmd_lines.append("") + + # Use the new inline generation method + field_lines = self._generate_field_documentation( + model_class, + field_name, + field_info, + field_type, + is_required, + indent_level=0, + visited_models=set(), + ) + qmd_lines.extend(field_lines) + + # Add blank line after nested config + if has_nested: + qmd_lines.append("") + else: + # Original simple approach + description = field_info.get("description", "") + default = field_info.get("default") + + # Add wrapped comment for description + if description: + wrapped_lines = self._wrap_comment(description) + qmd_lines.extend(wrapped_lines) + + line = f"{field_name}: {field_type}" + if default is not None: + line += f" = {default}" + if is_required: + line += " (required)" + qmd_lines.append(line) + + qmd_lines.append("```") + + # Join all lines and clean up any double newlines + content = "\n".join(qmd_lines) + + # Replace multiple consecutive newlines with just two newlines (one blank line) + import re + + content = re.sub(r"\n{3,}", "\n\n", content) + + # Ensure single newline at the very end + content = content.rstrip("\n") + "\n" + + return content + + +def main(): + generator = QuartoGenerator() + + print("Generating config reference content...") + qmd_content = generator.generate_qmd(AxolotlInputConfig, "Config Reference", True) + + print("Writing to file...") + with open("docs/config-reference.qmd", "w", encoding="utf-8") as f: + f.write(qmd_content) + print("Done!") + + +if __name__ == "__main__": + main() diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 33a8f77dbf..259daa56f9 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -12,10 +12,8 @@ Field, StringConstraints, field_serializer, - field_validator, model_validator, ) -from transformers.utils.import_utils import is_torch_npu_available from axolotl.utils.logging import get_logger from axolotl.utils.schemas.datasets import ( @@ -47,14 +45,13 @@ from axolotl.utils.schemas.quantization import PTQConfig, QATConfig from axolotl.utils.schemas.training import HyperparametersConfig from axolotl.utils.schemas.trl import TRLConfig +from axolotl.utils.schemas.validation import ValidationMixin from axolotl.utils.schemas.vllm import VllmConfig LOG = get_logger(__name__, use_environ=True) -SUPPORTED_METRICS = {"sacrebleu", "comet", "ter", "chrf", "perplexity"} - -# pylint: disable=too-many-public-methods,too-many-ancestors +# pylint: disable=too-many-ancestors class AxolotlInputConfig( ModelInputConfig, ModelOutputConfig, @@ -70,22 +67,54 @@ class AxolotlInputConfig( MultiModalConfig, RemappedParameters, DeprecatedParameters, + ValidationMixin, BaseModel, ): - """Wrapper of all config options""" + """Wrapper of all config options.""" model_config = {"populate_by_name": True} - strict: bool | None = Field(default=False) - resume_from_checkpoint: str | None = None - auto_resume_from_checkpoints: bool | None = None - resize_token_embeddings_to_32x: bool | None = None + strict: bool | None = Field( + default=False, + json_schema_extra={"description": "Allow overwrite yml config using from cli"}, + ) + resume_from_checkpoint: str | None = Field( + default=None, + json_schema_extra={"description": "Resume from a specific checkpoint dir"}, + ) + auto_resume_from_checkpoints: bool | None = Field( + default=None, + json_schema_extra={ + "description": "If resume_from_checkpoint isn't set and you simply want it to start where it left off. Be careful with this being turned on between different models." + }, + ) + resize_token_embeddings_to_32x: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Resize the model embeddings when new tokens are added to multiples of 32. This is reported to improve training speed on some models" + }, + ) mean_resizing_embeddings: bool | None = False # optionally shrink the embeddings when the tokenizer vocab size is smaller - shrink_embeddings: bool | None = None - embeddings_skip_upcast: bool | None = None + shrink_embeddings: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to shrink the embeddings to len(tokenizer). By default, we won't shrink." + }, + ) + embeddings_skip_upcast: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Don't upcast the embeddings to float32 when using PEFT. Useful for low-VRAM GPUs" + }, + ) - rl: RLType | None = None + rl: RLType | None = Field( + default=None, + json_schema_extra={ + "description": "Use RL training: 'dpo', 'ipo', 'kto', 'simpo', 'orpo', 'grpo'" + }, + ) trl: TRLConfig | None = Field( default_factory=lambda: TRLConfig(), # pylint: disable=unnecessary-lambda ) @@ -94,12 +123,25 @@ class AxolotlInputConfig( ) qat: QATConfig | None = None quantization: PTQConfig | None = None - reward_model: bool | None = None - process_reward_model: bool | None = None + reward_model: bool | None = Field( + default=None, + json_schema_extra={"description": "Reward modelling: `True` or `False`"}, + ) + process_reward_model: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Process reward modelling: `True` or `False`" + }, + ) num_labels: int | None = None # Whether to use weighting in DPO trainer. # If `None`, default is `False` in the trainer. - dpo_use_weighting: bool | None = None + dpo_use_weighting: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to perform weighting in DPO trainer" + }, + ) dpo_use_logits_to_keep: bool | None = None dpo_label_smoothing: float | None = None dpo_norm_loss: bool | None = None @@ -111,7 +153,12 @@ class AxolotlInputConfig( MinLen(1), ] | None - ) = None + ) = Field( + default=None, + json_schema_extra={ + "description": "A list of one or more datasets to finetune the model with" + }, + ) test_datasets: ( Annotated[ @@ -119,22 +166,59 @@ class AxolotlInputConfig( MinLen(1), ] | None - ) = None - shuffle_merged_datasets: bool | None = True - dataset_prepared_path: str | None = None - dataset_shard_num: int | None = None - dataset_shard_idx: int | None = None + ) = Field( + default=None, + json_schema_extra={ + "description": "A list of one or more datasets to eval the model with. You can use either test_datasets, or val_set_size, but not both." + }, + ) + shuffle_merged_datasets: bool | None = Field( + default=True, + json_schema_extra={ + "description": "If false, the datasets will not be shuffled and will keep their original order in `datasets`. The same applies to the `test_datasets` option and the `pretraining_dataset` option. Default is true." + }, + ) + dataset_prepared_path: str | None = Field( + default=None, + json_schema_extra={ + "description": "Axolotl attempts to save the dataset as an arrow after packing the data together so subsequent training attempts load faster, relative path" + }, + ) + dataset_shard_num: int | None = Field( + default=None, json_schema_extra={"description": "Num shards for whole dataset"} + ) + dataset_shard_idx: int | None = Field( + default=None, + json_schema_extra={"description": "Index of shard to use for whole dataset"}, + ) skip_prepare_dataset: bool | None = False pretraining_dataset: ( Annotated[list[PretrainingDataset | SFTDataset], MinLen(1)] | None ) = Field( default=None, - json_schema_extra={"description": "streaming dataset to use for pretraining"}, + json_schema_extra={ + "description": "Set to HF dataset for type: 'completion' for streaming instead of pre-tokenize" + }, + ) + dataset_processes: int | None = Field( + default=min(32, os.cpu_count()), # type: ignore[type-var] + json_schema_extra={ + "description": "The maximum number of processes to use while preprocessing your input dataset. This defaults to `os.cpu_count()` if not set." + }, + ) + dataset_exact_deduplication: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Deduplicates datasets and test_datasets with identical entries" + }, + ) + dataset_keep_in_memory: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Keep dataset in memory while preprocessing. Only needed if cached dataset is taking too much storage" + }, ) - dataset_processes: int | None = Field(default=min(32, os.cpu_count() or 1)) - dataset_exact_deduplication: bool | None = None - dataset_keep_in_memory: bool | None = None dataloader_pin_memory: bool | None = None dataloader_num_workers: int | None = None dataloader_prefetch_factor: int | None = None @@ -144,75 +228,203 @@ class AxolotlInputConfig( remove_unused_columns: bool | None = None - push_dataset_to_hub: str | None = None - hf_use_auth_token: bool | None = None + push_dataset_to_hub: str | None = Field( + default=None, + json_schema_extra={ + "description": "Push prepared dataset to hub - repo_org/repo_name" + }, + ) + hf_use_auth_token: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use hf `use_auth_token` for loading datasets. Useful for fetching private datasets. Required to be true when used in combination with `push_dataset_to_hub`" + }, + ) device: Any | None = None - device_map: Any | None = None + device_map: Any | None = Field( + default=None, + json_schema_extra={ + "description": "Passed through to transformers when loading the model when launched without accelerate. Use `sequential` when training w/ model parallelism to limit memory" + }, + ) world_size: int | None = None - local_rank: int | None = None + local_rank: int | None = Field( + default=None, + json_schema_extra={ + "description": "Don't mess with this, it's here for accelerate and torchrun" + }, + ) ddp: bool | None = None - seed: int | None = None - ddp_timeout: int | None = None - ddp_bucket_cap_mb: int | None = None - ddp_broadcast_buffers: bool | None = None + seed: int | None = Field( + default=None, json_schema_extra={"description": "Seed for reproducibility"} + ) + ddp_timeout: int | None = Field( + default=None, + json_schema_extra={"description": "Advanced DDP Arguments - timeout"}, + ) + ddp_bucket_cap_mb: int | None = Field( + default=None, + json_schema_extra={"description": "Advanced DDP Arguments - bucket cap in MB"}, + ) + ddp_broadcast_buffers: bool | None = Field( + default=None, + json_schema_extra={"description": "Advanced DDP Arguments - broadcast buffers"}, + ) ddp_find_unused_parameters: bool | None = None - eval_table_size: int | None = None - eval_max_new_tokens: int | None = None - do_causal_lm_eval: bool | None = None - eval_causal_lm_metrics: list[str] | None = None + eval_table_size: int | None = Field( + default=None, + json_schema_extra={ + "description": "Approximate number of predictions sent to wandb depending on batch size. Enabled above 0. Default is 0" + }, + ) + eval_max_new_tokens: int | None = Field( + default=None, + json_schema_extra={ + "description": "Total number of tokens generated for predictions sent to wandb. Default is 128" + }, + ) + do_causal_lm_eval: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to run causal language model evaluation for metrics in `eval_causal_lm_metrics`" + }, + ) + eval_causal_lm_metrics: list[str] | None = Field( + default=None, + json_schema_extra={ + "description": "HF evaluate metrics used during evaluation. Default is ['sacrebleu', 'comet', 'ter', 'chrf', 'perplexity']" + }, + ) do_bench_eval: bool | None = None bench_dataset: str | None = None bench_split: str | None = None metric_for_best_model: str | None = None greater_is_better: bool | None = None - loss_watchdog_threshold: float | None = None - loss_watchdog_patience: int | None = None + loss_watchdog_threshold: float | None = Field( + default=None, + json_schema_extra={ + "description": "High loss value, indicating the learning has broken down (a good estimate is ~2 times the loss at the start of training)" + }, + ) + loss_watchdog_patience: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of high-loss steps in a row before the trainer aborts (default: 3)" + }, + ) gc_steps: int | None = None - bf16: Literal["auto"] | bool | None = "auto" - fp16: bool | None = None + bf16: Literal["auto"] | bool | None = Field( + default="auto", + json_schema_extra={ + "description": "Use CUDA bf16. bool or 'full' for `bf16_full_eval`, or 'auto' for automatic detection. require >=ampere" + }, + ) + fp16: bool | None = Field( + default=None, json_schema_extra={"description": "Use CUDA fp16"} + ) fp8: bool | None = None - bfloat16: bool | None = None # for non-AMP cases - float16: bool | None = None # for non-AMP cases - tf32: bool | None = None + bfloat16: bool | None = Field( + default=None, + json_schema_extra={ + "description": "No AMP (automatic mixed precision) - require >=ampere" + }, + ) # for non-AMP cases + float16: bool | None = Field( + default=None, + json_schema_extra={"description": "No AMP (automatic mixed precision)"}, + ) # for non-AMP cases + tf32: bool | None = Field( + default=None, + json_schema_extra={"description": "Use CUDA tf32 - require >=ampere"}, + ) float32: bool | None = None - # torch_dtype: torch.dtype | None - gradient_checkpointing: Literal["offload", "offload_disk"] | bool | None = Field( - default=False + default=False, + json_schema_extra={ + "description": "Whether to use gradient checkpointing. Available options are: true, false, 'offload', 'offload_disk'. https://huggingface.co/docs/transformers/v4.18.0/en/performance#gradient-checkpointing" + }, + ) + gradient_checkpointing_kwargs: dict[str, Any] | None = Field( + default=None, + json_schema_extra={ + "description": "Additional kwargs to pass to the trainer for gradient checkpointing" + }, ) - gradient_checkpointing_kwargs: dict[str, Any] | None = None unfrozen_parameters: list[str] | None = None - sequence_len: int = Field(default=512) + sequence_len: int = Field( + default=512, + json_schema_extra={ + "description": "The maximum length of an input to train with, this should typically be less than 2048 as most models have a token/context limit of 2048" + }, + ) min_sample_len: int | None = None max_prompt_len: int = Field( default=512, json_schema_extra={"description": "maximum prompt length for RL training"}, ) - sample_packing: bool | None = None - sample_packing_group_size: int | None = 100_000 - sample_packing_bin_size: int | None = 200 - sample_packing_sequentially: bool | None = None - eval_sample_packing: bool | None = None - pad_to_sequence_len: bool | None = None - curriculum_sampling: bool | None = None + sample_packing: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Use efficient multi-packing with block diagonal attention and per sequence position_ids. Recommend set to 'true'" + }, + ) + sample_packing_group_size: int | None = Field( + default=100_000, + json_schema_extra={ + "description": "The number of samples packed at a time. Increasing the following values helps with packing, but usually only slightly (<%1.)" + }, + ) + sample_packing_bin_size: int | None = Field( + default=200, + json_schema_extra={ + "description": "The number of samples which can be packed into one sequence. Increase if using a large sequence_len with many short samples." + }, + ) + sample_packing_sequentially: bool | None = Field( + default=None, + json_schema_extra={"description": "Whether to pack samples sequentially"}, + ) + eval_sample_packing: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Set to 'false' if getting errors during eval with sample_packing on" + }, + ) + pad_to_sequence_len: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Pad inputs so each step uses constant sized buffers. This will reduce memory fragmentation and may prevent OOMs, by re-using memory more efficiently" + }, + ) + curriculum_sampling: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use sequential sampling for curriculum learning" + }, + ) multipack_real_batches: bool | None = None pretraining_sample_concatenation: bool | None = Field( default=None, json_schema_extra={ - "description": "whether to soft pack/concatenate samples during pretraining", + "description": "whether to concatenate samples during pretraining", }, ) - batch_flattening: Literal["auto"] | bool | None = None + batch_flattening: Literal["auto"] | bool | None = Field( + default=None, + json_schema_extra={ + "description": "Use batch flattening for speedups when not using sample_packing" + }, + ) # for PoSE context length extension use_pose: bool | None = None @@ -228,17 +440,60 @@ class AxolotlInputConfig( }, ) - xformers_attention: bool | None = None - sdp_attention: bool | None = None - s2_attention: bool | None = None + xformers_attention: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use xformers attention patch https://github.com/facebookresearch/xformers" + }, + ) + sdp_attention: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use scaled-dot-product attention https://pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html" + }, + ) + s2_attention: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Shifted-sparse attention (only llama) - https://arxiv.org/pdf/2309.12307.pdf" + }, + ) flex_attention: bool | None = None flex_attn_compile_kwargs: dict[str, Any] | None = None - flash_attention: bool | None = None - flash_attn_cross_entropy: bool | None = None - flash_attn_rms_norm: bool | None = None - flash_attn_fuse_qkv: bool | None = None - flash_attn_fuse_mlp: bool | None = None - flash_optimum: bool | None = None + flash_attention: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use flash attention patch https://github.com/Dao-AILab/flash-attention" + }, + ) + flash_attn_cross_entropy: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use flash-attention cross entropy implementation - advanced use only" + }, + ) + flash_attn_rms_norm: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use flash-attention rms norm implementation - advanced use only" + }, + ) + flash_attn_fuse_qkv: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to fuse QKV into a single operation" + }, + ) + flash_attn_fuse_mlp: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to fuse part of the MLP into a single operation" + }, + ) + flash_optimum: bool | None = Field( + default=None, + json_schema_extra={"description": "Whether to use bettertransformers"}, + ) eager_attention: bool | None = None @@ -249,1089 +504,336 @@ class AxolotlInputConfig( unsloth_rms_norm: bool | None = None unsloth_rope: bool | None = None - lora_mlp_kernel: bool | None = None - lora_qkv_kernel: bool | None = None - lora_o_kernel: bool | None = None + lora_mlp_kernel: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Apply custom LoRA autograd functions and activation function Triton kernels for speed and memory savings. See: https://docs.axolotl.ai/docs/lora_optims.html" + }, + ) + lora_qkv_kernel: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Apply custom LoRA autograd functions and activation function Triton kernels for speed and memory savings. See: https://docs.axolotl.ai/docs/lora_optims.html" + }, + ) + lora_o_kernel: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Apply custom LoRA autograd functions and activation function Triton kernels for speed and memory savings. See: https://docs.axolotl.ai/docs/lora_optims.html" + }, + ) llama4_linearized_experts: bool | None = None - deepspeed: str | dict[str, Any] | None = None - fsdp: list[str] | None = None - fsdp_config: dict[str, Any] | None = None + deepspeed: str | dict[str, Any] | None = Field( + default=None, + json_schema_extra={ + "description": "Deepspeed config path. e.g., deepspeed_configs/zero3.json" + }, + ) + fsdp: list[str] | None = Field( + default=None, json_schema_extra={"description": "FSDP configuration"} + ) + fsdp_config: dict[str, Any] | None = Field( + default=None, json_schema_extra={"description": "FSDP configuration options"} + ) fsdp_final_state_dict_type: ( Literal["FULL_STATE_DICT", "LOCAL_STATE_DICT", "SHARDED_STATE_DICT"] | None ) = None - val_set_size: float | None = Field(default=0.0) + val_set_size: float | None = Field( + default=0.0, + json_schema_extra={ + "description": "How much of the dataset to set aside as evaluation. 1 = 100%, 0.50 = 50%, etc. 0 for no eval." + }, + ) - sequence_parallel_degree: int | None = None - heads_k_stride: int | None = None - ring_attn_func: RingAttnFunc | None = None + sequence_parallel_degree: int | None = Field( + default=None, + json_schema_extra={ + "description": "Set to a divisor of the number of GPUs available to split sequences into chunks of equal size. Use in long context training to prevent OOM when sequences cannot fit into a single GPU's VRAM. E.g., if 4 GPUs are available, set this value to 2 to split each sequence into two equal-sized subsequences, or set to 4 to split into four equal-sized subsequences. See https://docs.axolotl.ai/docs/sequence_parallelism.html for more details." + }, + ) + heads_k_stride: int | None = Field( + default=None, + json_schema_extra={ + "description": "Optional; strides across the key dimension. Larger values use more memory but should make training faster. Must evenly divide the number of KV heads in your model." + }, + ) + ring_attn_func: RingAttnFunc | None = Field( + default=None, + json_schema_extra={ + "description": "One of 'varlen_llama3', 'batch_ring', 'batch_zigzag', 'batch_stripe'. Defaults to 'varlen_llama3' in the sample packing case, and 'batch_ring' in the non-sample packing case." + }, + ) - special_tokens: SpecialTokensConfig | None = None - tokens: list[str] | None = None - added_tokens_overrides: dict[int, str] | None = None + special_tokens: SpecialTokensConfig | None = Field( + default=None, + json_schema_extra={ + "description": "Add or change special tokens. If you add tokens here, you don't need to add them to the `tokens` list." + }, + ) + tokens: list[str] | None = Field( + default=None, + json_schema_extra={"description": "Add extra tokens to the tokenizer"}, + ) + added_tokens_overrides: dict[int, str] | None = Field( + default=None, + json_schema_extra={ + "description": "Mapping token_id to new_token_string to override reserved added_tokens in the tokenizer. Only works for tokens that are not part of the base vocab (aka are added_tokens). Can be checked if they exist in tokenizer.json added_tokens." + }, + ) - torch_compile: Literal["auto"] | bool | None = None - torch_compile_backend: str | None = None + torch_compile: Literal["auto"] | bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use torch.compile and which backend to use. setting to `auto` will enable torch compile when torch>=2.5.1" + }, + ) + torch_compile_backend: str | None = Field( + default=None, + json_schema_extra={"description": "Backend to use for torch.compile"}, + ) torch_compile_mode: Literal["default", "reduce-overhead", "max-autotune"] | None = ( None ) - max_steps: int | None = None - warmup_steps: int | None = None - warmup_ratio: float | None = None - eval_steps: int | float | None = None - evals_per_epoch: int | None = None - eval_strategy: str | None = None - save_steps: int | float | None = None - saves_per_epoch: int | None = None - save_strategy: str | None = None - save_total_limit: int | None = None - logging_steps: int | None = None - early_stopping_patience: int | None = None - load_best_model_at_end: bool | None = False - save_only_model: bool | None = False - use_tensorboard: bool | None = None - profiler_steps: int | None = None - include_tokens_per_second: bool | None = None - - neftune_noise_alpha: float | None = None - - orpo_alpha: float | None = None - rpo_alpha: float | None = None - simpo_gamma: float | None = None - cpo_alpha: float | None = None - - kto_desirable_weight: float | None = None - kto_undesirable_weight: float | None = None - rl_beta: float | None = None - - max_memory: dict[int | Literal["cpu", "disk"], int | str] | None = None - gpu_memory_limit: int | str | None = None - low_cpu_mem_usage: bool | None = None - - chat_template: ( - ChatTemplate - | Annotated[str, StringConstraints(pattern="^tokenizer_default_fallback_")] - ) | None = None - chat_template_jinja: str | None = None - chat_template_kwargs: dict[str, Any] | None = None - eot_tokens: list[str] | None = None - default_system_message: str | None = None - - fix_untrained_tokens: int | list[int] | None = None - - # INTERNALS - document for now, generally not set externally - is_preprocess: bool | None = None - preprocess_iterable: bool | None = None - - total_num_tokens: int | None = None - total_supervised_tokens: int | None = None - sample_packing_eff_est: float | None = None - axolotl_config_path: str | None = None - - is_falcon_derived_model: bool | None = Field(default=None) - is_llama_derived_model: bool | None = Field(default=None) - is_mistral_derived_model: bool | None = Field(default=None) - is_qwen_derived_model: bool | None = Field(default=None) - - plugins: list[str] | None = Field(default=None) - - @field_validator("seed", mode="after") - @classmethod - def set_default_seed(cls, seed): - if seed is None: - LOG.info("`seed` not set in config; setting to 42") - seed = 42 - return seed - - @field_validator("datasets", mode="before") - @classmethod - def deprecate_sharegpt_datasets(cls, datasets): - for _, ds_cfg in enumerate(datasets): - # Handle both dict and pydantic model cases - ds_type = ( - ds_cfg.get("type") - if isinstance(ds_cfg, dict) - else getattr(ds_cfg, "type", None) - ) - if not ds_type: - continue - - # skip if it's a dict (for custom user instruction prompt) - if isinstance(ds_type, dict): - continue - - if isinstance(ds_type, str) and ds_type.startswith("sharegpt"): - raise ValueError( - "`type: sharegpt.*` is deprecated. Please use `type: chat_template` instead." - ) - - return datasets - - @field_serializer("datasets") - def datasets_serializer( - self, ds_configs: list[DatasetConfig] | None - ) -> list[dict[str, Any]] | None: - if ds_configs: - return [ds_config.model_dump(exclude_none=True) for ds_config in ds_configs] - return None - - @model_validator(mode="before") - @classmethod - def check_attention_fields(cls, data): - fields = ( - "xformers_attention", - "sdp_attention", - "s2_attention", - "flash_attention", - "flex_attention", - ) - non_empty_count = sum(1 for field in fields if data.get(field)) - - if non_empty_count > 1: - raise ValueError(f"Only one of {', '.join(fields)} must be set") - return data - - @model_validator(mode="before") - @classmethod - def check_batch_size_fields(cls, data): - fields = ("micro_batch_size", "gradient_accumulation_steps", "batch_size") - non_empty_count = sum(1 for field in fields if data.get(field)) - - if non_empty_count < 2: - raise ValueError(f"At least two of {', '.join(fields)} must be set") - return data - - @model_validator(mode="before") - @classmethod - def check_pretraining_w_max_steps(cls, data): - if data.get("pretraining_dataset") and not data.get("max_steps"): - raise ValueError( - "max_steps must be set when using iterable pretraining_dataset, Trainer can't infer length and schedule optimizer/learning rate without it!" - ) - return data - - @model_validator(mode="before") - @classmethod - def check_pretraining_w_group_by_length(cls, data): - if data.get("pretraining_dataset") and data.get("group_by_length"): - LOG.warning( - "You probably want to disable group_by_length as it will force a streamed dataset to download completely." - ) - return data - - @model_validator(mode="before") - @classmethod - def check_pretraining_split_batches_accelerate(cls, data): - # alternatively set ACCELERATE_SPLIT_BATCHES=False - if data.get("pretraining_dataset"): - accelerator_config = data.get("accelerator_config", {}) - if not accelerator_config: - data["accelerator_config"] = { - "split_batches": False, - "dispatch_batches": False, - } - else: - if accelerator_config.get("split_batches") is None: - data["accelerator_config"]["split_batches"] = False - if accelerator_config.get("dispatch_batches") is None: - data["accelerator_config"]["dispatch_batches"] = False - return data - - @model_validator(mode="before") - @classmethod - def check_gptq_w_revision(cls, data): - if data.get("gptq") and data.get("revision_of_model"): - raise ValueError( - "revision_of_model is not supported for GPTQ models. " - + "Please download the model from HuggingFace Hub manually for correct branch, " - + "point to its path, and remove revision_of_model from the config." - ) - return data - - @model_validator(mode="before") - @classmethod - # pylint: disable=duplicate-code - def check_chat_template_config(cls, data): - # if chat_template is set to jinja, chat_template_jinja is required - if data.get("chat_template") == ChatTemplate.jinja and not data.get( - "chat_template_jinja" - ): - raise ValueError( - "chat_template_jinja is required when chat_template is set to jinja" - ) - - # If chat_template_jinja is set, set chat_template to jinja - if data.get("chat_template_jinja") and not data.get("chat_template"): - data["chat_template"] = ChatTemplate.jinja - - return data - - @model_validator(mode="before") - @classmethod - def check_sample_packing_wo_flash(cls, data): - if ( - data.get("sample_packing") - and not data.get("flash_attention") - and not data.get("sdp_attention") - and not data.get("flex_attention") - and not data.get("xformers_attention") - ): - LOG.warning( - "sample_packing without flash, sdp, xformers or flex attention does not handle cross sample decontamination." - ) - - return data - - @model_validator(mode="before") - @classmethod - def check_sample_packing_with_s2attn(cls, data): - if data.get("sample_packing") and data.get("s2_attention"): - raise ValueError( - "Received `sample_packing=true` and `s2_attention=true`; however, \ - shifted-sparse attention does not currently support sample packing." - ) - return data - - @model_validator(mode="before") - @classmethod - def check_batch_flattening_fa(cls, data): - if data.get("batch_flattening"): - batch_flattening_auto = data.get("batch_flattening") == "auto" - if not data.get("flash_attention") and not batch_flattening_auto: - raise ValueError("batch_flattening requires flash attention") - if data.get("sample_packing") and not batch_flattening_auto: - raise ValueError("batch_flattening not compatible with sample_packing") - if data.get("micro_batch_size") == 1 and not batch_flattening_auto: - LOG.warning("batch_flattening has no effect with micro_batch_size == 1") - - if ( - batch_flattening_auto - and data.get("flash_attention") - and not data.get("sample_packing") - and data.get("micro_batch_size") > 1 - ): - data["batch_flattening"] = True - elif batch_flattening_auto: - data["batch_flattening"] = False - - return data - - @model_validator(mode="before") - @classmethod - def check_sample_packing_w_rl(cls, data): - if data.get("sample_packing") and data.get("rl"): - raise ValueError("`sample_packing: true` does not work with RLHF training") - return data - - @model_validator(mode="before") - @classmethod - def hint_sample_packing_padding(cls, data): - if data.get("sample_packing"): - pad_to_sequence_len = data.get("pad_to_sequence_len") - if pad_to_sequence_len is False: - LOG.warning( - "`pad_to_sequence_len: true` is recommended when using sample_packing" - ) - elif pad_to_sequence_len is None: - LOG.info( - "Setting `pad_to_sequence_len: true` to prevent memory leaks when sample_packing" - ) - data["pad_to_sequence_len"] = True - return data - - @model_validator(mode="before") - @classmethod - def hint_reward_model_pad(cls, data): - if data.get("reward_model") and not data.get("pad_to_sequence_len"): - LOG.warning( - "`pad_to_sequence_len: true` is recommended when using reward_model" - ) - if data.get("pad_to_sequence_len") is None: - data["pad_to_sequence_len"] = True - return data - - @model_validator(mode="before") - @classmethod - def check_gas_bsz(cls, data): - if data.get("gradient_accumulation_steps") and data.get("batch_size"): - raise ValueError( - "please set only one of gradient_accumulation_steps or batch_size" - ) - return data - - @model_validator(mode="before") - @classmethod - def hint_eval_train_mbsz(cls, data): - if ( - data.get("eval_batch_size") - and data.get("micro_batch_size") - and data.get("eval_batch_size") != data.get("micro_batch_size") - ): - LOG.warning( - "eval_batch_size != micro_batch_size. This can lead to VRAM instability." - ) - return data - - @model_validator(mode="before") - @classmethod - def check_push_ds_auth(cls, data): - if ( - data.get("push_dataset_to_hub") - and data.get("hf_use_auth_token") is not True - ): - raise ValueError( - "Require cfg.hf_use_auth_token to be True for push_dataset_to_hub" - ) - return data - - @model_validator(mode="after") - def check_falcon_fsdp(self): - if (self.base_model and "falcon" in self.base_model.lower()) and self.fsdp: - raise ValueError("FSDP is not supported for falcon models") - return self - - @model_validator(mode="after") - def check_mpt_checkpointing(self): - if ( - self.base_model and "mpt" in self.base_model.lower() - ) and self.gradient_checkpointing: - raise ValueError("gradient_checkpointing is not supported for MPT models") - return self - - @model_validator(mode="after") - def check_offload_grad_checkpointing(self): - if self.gradient_checkpointing and self.gradient_checkpointing == "unsloth": - LOG.warning( - "`unsloth` is deprecated for gradient_checkpointing, use `offload`" - ) - self.gradient_checkpointing = "offload" - return self - - @model_validator(mode="after") - def check_better_transformers(self): - if self.flash_optimum is True: - if self.adapter: - LOG.warning( - "BetterTransformers probably doesn't work with PEFT adapters" - ) - if self.fp16 or self.bf16: - raise ValueError("AMP is not supported with BetterTransformer") - if self.float16 is not True and self.bfloat16 is not True: - LOG.warning( - "You should probably set bfloat16 or float16 to true to " - "load the model in float16 for BetterTransformers" - ) - return self - - @model_validator(mode="after") - def check_adamw_optimizer_params(self): - if any([self.adam_beta1, self.adam_beta2, self.adam_epsilon]) and ( - not self.optimizer or "adamw" not in str(self.optimizer).lower() - ): - LOG.warning("adamw hyperparameters found, but no adamw optimizer set") - return self - - @model_validator(mode="before") - @classmethod - def check_lr_groups(cls, data): - if data.get("lr_groups") and data.get("loraplus_lr_ratio"): - raise ValueError("lr_groups and loraplus_lr_ratio cannot be used together.") - return data - - @model_validator(mode="before") - @classmethod - def check_saves(cls, data): - if ( - data.get("save_strategy") - and data.get("save_steps") - and data.get("save_strategy") != "steps" - ): - raise ValueError( - "save_strategy and save_steps mismatch. Please set save_strategy to 'steps' or remove save_steps." - ) - if data.get("saves_per_epoch") and data.get("save_steps"): - raise ValueError( - "save_steps and saves_per_epoch are mutually exclusive and cannot be used together." - ) - return data - - @model_validator(mode="before") - @classmethod - def check_push_save(cls, data): - if data.get("hub_model_id") and ( - data.get("save_strategy") not in ["steps", "epoch", None] - ): - LOG.warning( - "hub_model_id is set without any models being saved. To save a model, set save_strategy." - ) - return data - - @model_validator(mode="before") - @classmethod - def check_evals(cls, data): - if ( - data.get("eval_strategy") - and data.get("eval_steps") - and data.get("eval_strategy") != "steps" - ): - raise ValueError( - "eval_strategy and eval_steps mismatch. Please set eval_strategy to 'steps' or remove eval_steps." - ) - - if ( - data.get("val_set_size") == 0 - and (data.get("eval_steps") or data.get("eval_strategy")) - and not data.get("test_datasets") - and data.get("eval_strategy") != "no" - ): - raise ValueError( - "eval_steps and eval_strategy are not supported with val_set_size == 0" - ) - if data.get("evals_per_epoch") and data.get("eval_steps"): - raise ValueError( - "eval_steps and evals_per_epoch are mutually exclusive and cannot be used together." - ) - if ( - data.get("evals_per_epoch") - and data.get("eval_strategy") - and data.get("eval_strategy") != "steps" - ): - raise ValueError( - "eval_strategy must be empty or set to `steps` when used with evals_per_epoch." - ) - - if data.get("do_bench_eval") and not ( - data.get("evals_per_epoch") or data.get("eval_steps") - ): - raise ValueError( - "do_bench_eval requires evals_per_epoch or eval_steps to be set." - ) - return data - - @model_validator(mode="before") - @classmethod - def check_test_datasets_bench(cls, data): - if ( - data.get("do_bench_eval") - and not data.get("test_datasets") - and not data.get("val_set_size") - ): - LOG.warning( - "`do_bench_eval` needs a test dataset to run evals, adding an empty test_dataset." - ) - data["test_datasets"] = [{"path": "axolotl-ai-co/empty-test-ds"}] - return data - - @model_validator(mode="before") - @classmethod - def check_eval_packing(cls, data): - # TODO also should check test_datasets and val_set_size as we can skip - # if there are no eval datasets/splits - if ( - data.get("sample_packing") - and data.get("eval_table_size") - and data.get("eval_sample_packing") is not False - ): - raise ValueError( - "eval_table_size and eval_sample_packing are not supported together with sample_packing. Please set 'eval_sample_packing' to false." - ) - if ( - data.get("sample_packing") - and data.get("eval_sample_packing") is None - and not data.get("eval_table_size") - ): - LOG.info( - "explicitly setting `eval_sample_packing` to match `sample_packing`" - ) - data["eval_sample_packing"] = True - - if ( - data.get("sample_packing") - and data.get("eval_sample_packing") is False - and data.get("remove_unused_columns") is None - ): - LOG.info( - "setting `remove_unused_columns: false` for when sample_packing and eval_sample_packing don't match" - ) - data["remove_unused_columns"] = False - - return data - - @model_validator(mode="before") - @classmethod - def check_mm_prepare(cls, data): - if data.get("skip_prepare_dataset"): - if data.get("remove_unused_columns") is None: - LOG.info( - "setting `remove_unused_columns: false` for skip_prepare_dataset" - ) - data["remove_unused_columns"] = False - - return data - - @model_validator(mode="before") - @classmethod - def check_warmup(cls, data): - if data.get("warmup_steps") and data.get("warmup_ratio"): - raise ValueError("warmup_steps and warmup_ratio are mutually exclusive") - return data - - @model_validator(mode="before") - @classmethod - def check_neftune(cls, data): - if data.get("noisy_embedding_alpha") and not data.get("neftune_noise_alpha"): - data["neftune_noise_alpha"] = data["noisy_embedding_alpha"] - del data["noisy_embedding_alpha"] - elif data.get("noisy_embedding_alpha") and not data.get("neftune_noise_alpha"): - raise ValueError( - "noisy_embedding_alpha is deprecated, use neftune_noise_alpha; both are set, please remove the deprecated noisy_embedding_alpha setting" - ) - return data - - @field_validator("neftune_noise_alpha") - @classmethod - def validate_neftune_noise_alpha(cls, neftune_noise_alpha): - if neftune_noise_alpha is not None and neftune_noise_alpha <= 0.0: - raise ValueError("neftune_noise_alpha must be > 0.0") - return neftune_noise_alpha - - @model_validator(mode="after") - def check_rl_beta(self): - if self.dpo_beta and not self.rl_beta: - self.rl_beta = self.dpo_beta - del self.dpo_beta - return self - - @model_validator(mode="after") - def check_simpo_warmup(self): - if self.rl is RLType.SIMPO and self.warmup_ratio: - raise ValueError( - "warmup_ratio is not supported with the simpo trainer. Please use `warmup_steps` instead" - ) - return self - - @model_validator(mode="before") - @classmethod - def check_frozen(cls, data): - if ( - data.get("adapter") - and data.get("peft_layers_to_transform") - and data.get("unfrozen_parameters") - ): - raise ValueError( - "`unfrozen_parameters` used with `peft_layers_to_transform` can have unexpected behavior." - ) - - return data - - @model_validator(mode="before") - @classmethod - def check_peft_layers_pattern(cls, data): - if data.get("peft_layers_pattern") and not data.get("peft_layers_to_transform"): - raise ValueError( - "peft_layers_pattern requires peft_layers_to_transform to be set" - ) - return data - - @model_validator(mode="after") - def check_fft_possible_bad_config(self): - if ( - # pylint: disable=too-many-boolean-expressions - not (self.bf16 or self.bfloat16) - and (self.fp16 or self.float16) - and not self.adapter - and not self.flash_attention - and self.sample_packing - ): - LOG.warning( - "Full fine tune w/o FA2 w/ sample packing and fp16/float16 is likely to raise errors. Try LoRA." - ) - # ValueError: Attempting to unscale FP16 gradients. - # OR - # RuntimeError: expected mat1 and mat2 to have the same dtype, but got: float != c10::Half - return self - - @model_validator(mode="after") - def check_fused_lora(self): - if self.adapter in ["lora", "qlora"] and ( - self.flash_attn_fuse_qkv or self.flash_attn_fuse_mlp - ): - raise ValueError("Fused modules are not supported with LoRA/QLoRA") - return self - - @model_validator(mode="after") - def hint_lora_8bit(self): - loftq = ( - self.peft and self.peft.loftq_config and self.peft.loftq_config.loftq_bits - ) - if not self.load_in_8bit and self.adapter == "lora" and not loftq: - LOG.warning("We recommend setting `load_in_8bit: true` for LORA finetuning") - return self - - @model_validator(mode="after") - def check_early_stopping(self): - if self.early_stopping_patience: - if not self.save_steps or not self.eval_steps: - raise ValueError( - "`early_stopping_patience` requires save_steps and eval_steps to be set. eval_steps should evenly divide save_steps." - ) - if self.save_steps % self.eval_steps != 0: - raise ValueError( - "`early_stopping_patience` requires that eval_steps should evenly divide save_steps." - ) - return self - - @model_validator(mode="after") - def check_relora(self): - if self.relora_steps: - if self.adapter not in ("lora", "qlora"): - raise ValueError("cfg.adapter must be lora or qlora to use ReLoRA") - - if self.fsdp: - raise ValueError("fsdp not supported with ReLoRA") - - if self.deepspeed: - raise ValueError("deepspeed not supported with ReLoRA") - - if self.lr_scheduler == "one_cycle": - raise ValueError( - "ReLoRA is not compatible with the one_cycle scheduler" - ) - - if self.flash_attn_fuse_qkv or self.flash_attn_fuse_mlp: - raise ValueError("Fused modules are not supported with ReLoRA") - return self - - @model_validator(mode="before") - @classmethod - def check_mem_mismatch(cls, data): - if ( - data.get("max_memory") is not None - and data.get("gpu_memory_limit") is not None - ): - raise ValueError( - "max_memory and gpu_memory_limit are mutually exclusive and cannot be used together." - ) - return data - - @model_validator(mode="before") - @classmethod - def check_use_reentrant_mismatch(cls, data): - if ( - data.get("unfrozen_parameters") - and data.get("gradient_checkpointing_kwargs") - and data.get("gradient_checkpointing_kwargs", {}).get("use_reentrant") - is True - ): - # https://github.com/huggingface/transformers/issues/21381 - raise ValueError( - "`use_reentrant` must be false when used with partially frozen model." - ) - return data - - @model_validator(mode="before") - @classmethod - def warn_qlora_zero3_w_use_reentrant(cls, data): - if ( - data.get("adapter") == "qlora" - and data.get("gradient_checkpointing_kwargs", {}) - and data.get("gradient_checkpointing_kwargs", {}).get("use_reentrant") - is False - and data.get("deepspeed", "") is not None - and "zero3" in data.get("deepspeed", "") - ): - # may result in: - # torch.utils.checkpoint.CheckpointError: torch.utils.checkpoint: - # Recomputed values for the following tensors have different metadata - # than during the forward pass. - LOG.warning( - "qlora + zero3 with use_reentrant: false may result in a CheckpointError about recomputed values" - ) - return data - - @model_validator(mode="before") - @classmethod - def check_val_w_test_datasets(cls, data): - if data.get("test_datasets") and data.get("val_set_size"): - raise ValueError( - "non-zero val_set_size should not be used with test_datasets configuration" - ) - return data - - @model_validator(mode="before") - @classmethod - def check_eval_strategy(cls, data): - if ( - data.get("evaluation_strategy") is not None - and data.get("eval_strategy") is None - ): - LOG.info( - "explicitly setting `eval_strategy` from the `evaluation_strategy`" - ) - data["eval_strategy"] = data.get("evaluation_strategy") - return data - - @model_validator(mode="before") - @classmethod - def check_fsdp_offload_w_8bit_optimizer(cls, data): - if ( - data.get("fsdp") - and "8bit" in data.get("optimizer", "") - and data.get("fsdp_config") - and data["fsdp_config"].get("fsdp_offload_params") - and str(data["fsdp_config"].get("fsdp_version")) != "2" - ): - raise ValueError( - f"FSDP Offload not compatible with {data.get('optimizer')}" - ) - if ( - data.get("fsdp") - and "8bit" in data.get("optimizer", "") - and data.get("fsdp_config") - and str(data["fsdp_config"].get("fsdp_version")) == "2" - ): - if data.get("optimizer", "") in ["adamw_8bit", "adamw_bnb_8bit"]: - # CUDA ops errors with bnb 8bit optimizer + FSDP2 - raise ValueError( - f"FSDP2 not compatible with {data.get('optimizer')}, use `adamw_torch_8bit` instead" - ) - - return data - - @model_validator(mode="before") - @classmethod - def check_fsdp_sharded_state_dict_w_safetensors(cls, data): - if ( - data.get("fsdp") - and data.get("save_safetensors") - and data.get("fsdp_config") - and data["fsdp_config"].get("fsdp_state_dict_type") == "SHARDED_STATE_DICT" - ): - raise ValueError( - "FSDP SHARDED_STATE_DICT not compatible with save_safetensors" - ) - return data - - @model_validator(mode="before") - @classmethod - def check_causal_lm_evals(cls, data): - if data.get("do_causal_lm_eval") and data.get("eval_sample_packing"): - raise ValueError( - "do_causal_lm_eval is enabled, eval_sample_packing must be set to False" - ) - - if data.get("eval_causal_lm_metrics"): - if not isinstance(data.get("eval_causal_lm_metrics"), list): - raise ValueError("eval_causal_lm_metrics must be a list") - # only ["sacrebleu", "comet", "ter", "chrf"] supported - if set(data.get("eval_causal_lm_metrics")) - SUPPORTED_METRICS: - raise ValueError( - f"eval_causal_lm_metrics must be one of {SUPPORTED_METRICS}" - ) - return data - - @model_validator(mode="before") - @classmethod - def check_dataset_or_pretraining_dataset(cls, data): - if data.get("datasets") is None and data.get("pretraining_dataset") is None: - raise ValueError("either datasets or pretraining_dataset is required") - return data - - @model_validator(mode="before") - @classmethod - def check_xentropy_patch_conflicts(cls, data): - if data.get("flash_attn_cross_entropy") and data.get( - "unsloth_cross_entropy_loss" - ): - raise ValueError( - "flash_attn_cross_entropy and unsloth_cross_entropy_loss cannot be both enabled" - ) - return data - - @model_validator(mode="before") - @classmethod - def check_qlora_unsloth(cls, data): - if ( - data.get("unsloth_lora_mlp") - or data.get("unsloth_lora_qkv") - or data.get("unsloth_lora_o") - ): - if data.get("adapter") == "lora" and data.get("load_in_8bit"): - raise ValueError( - "unsloth_lora_mlp, unsloth_lora_qkv, and unsloth_lora_o are not compatible with 8-bit LoRA" - ) - return data - - @model_validator(mode="before") - @classmethod - def check_lora_kernel_8bit(cls, data): - if ( - data.get("lora_mlp_kernel") - or data.get("lora_qkv_kernel") - or data.get("lora_o_kernel") - ): - if data.get("adapter") == "lora" and data.get("load_in_8bit"): - raise ValueError( - "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with 8-bit LoRA" - ) - return data - - @model_validator(mode="before") - @classmethod - def check_lora_kernel_rl(cls, data): - if ( - data.get("lora_mlp_kernel") - or data.get("lora_qkv_kernel") - or data.get("lora_o_kernel") - ) and data.get("rl"): - raise ValueError( - "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with RL at the moment." - ) - return data - - @model_validator(mode="before") - @classmethod - def check_lora_axolotl_unsloth(cls, data): - is_lora_kernel = any( - data.get(k) for k in ["lora_mlp_kernel", "lora_qkv_kernel", "lora_o_kernel"] - ) - is_unsloth_lora = any( - data.get(k) - for k in ["unsloth_lora_mlp", "unsloth_lora_qkv", "unsloth_lora_o"] - ) - if is_lora_kernel and is_unsloth_lora: - raise ValueError( - "both lora_mlp_kernel and unsloth_lora_mlp cannot be true (similarly for lora_qkv_kernel, lora_o_kernel)" - ) - return data - - @model_validator(mode="before") - @classmethod - def check_torch_compile_deepspeed(cls, data): - if data.get("deepspeed") and data.get("torch_compile"): - raise ValueError( - "torch_compile should be set within your deepspeed config file" - ) - return data - - @model_validator(mode="before") - @classmethod - def check_npu_config(cls, data): - if is_torch_npu_available(): - # check attention config - attn_list = ["flash_attention", "sdp_attention", "s2_attention"] - for attn in attn_list: - if data.get(attn): - raise NotImplementedError( - f"{attn} is currently not supported in Ascend npu, please disable this configuration." - ) - - # check quant config - if data.get("optimizer") is not None and "bit" in data.get("optimizer"): - optimizer = data.get("optimizer") - raise NotImplementedError( - f"{optimizer} is currently not supported in Ascend npu, choose another one please." - ) - - quant_list = ["load_in_8bit", "load_in_4bit"] - for quant in quant_list: - if data.get(quant): - raise NotImplementedError( - f"Quantification is currently not supported in Ascend npu, please disable {quant}." - ) - - # check dtype config - if data.get("tf32"): - raise NotImplementedError( - "tf32 dtype is currently not supported in Ascend npu, please disable this configuration" - ) - - return data - - @model_validator(mode="before") - @classmethod - def check_rl_config_gradient_checkpointing(cls, data): - # TODO: SalmanMohammadi - # Distributed RL with QLoRA + gradient checkpointing - # and use_reentrant = True is broken upstream in TRL - # pylint: disable=too-many-boolean-expressions - if ( - data.get("rl") - and data.get("gradient_checkpointing") - and data.get("gradient_checkpointing_kwargs") - and data.get("gradient_checkpointing_kwargs").get("use_reentrant") - and data.get("load_in_4bit") - and data.get("adapter") == "qlora" - and data.get("capabilities") - and data.get("capabilities").get("n_gpu", 1) > 1 - ): - raise ValueError( - "The `use_reentrant: True` implementation of gradient checkpointing " - "is not supported for distributed RL training with QLoRA. Please set " - "`use_reentrant: False` in `gradient_checkpointing_kwargs`." - ) - return data - - @model_validator(mode="before") - @classmethod - def check_kto_config(cls, data): - if data.get("rl") == "kto": - if data.get("sample_packing") or data.get("eval_sample_packing"): - raise ValueError("sample_packing is not supported with kto") - - if data.get("remove_unused_columns") is not False: - raise ValueError("Set `remove_unused_columns: False` when using kto") - - return data - - @model_validator(mode="before") - @classmethod - def check_grpo_liger_sequence_parallel(cls, data): - if ( - data.get("rl") == "grpo" - and data.get("trl", {}) - and data.get("trl").get("use_liger_loss") - and data.get("sequence_parallel_degree", 1) > 1 - ): - raise ValueError("GRPO + SP + Liger not currently supported") - return data - - @model_validator(mode="after") - def check_sequence_parallel_degree(self): - if not self.sequence_parallel_degree: - self.sequence_parallel_degree = 1 - elif self.sequence_parallel_degree > 1: - if not self.flash_attention: - raise ValueError( - "flash_attention: true must be set with sequence_parallel_degree > 1" - ) - - if self.sample_packing and getattr(self, "micro_batch_size", 1) > 1: - raise ValueError( - "micro_batch_size must be set to 1 when sample_packing is enabled " - "due to a `ring-flash-attn` requirement" - ) - - try: - import ring_flash_attn # noqa: F401 # pylint:disable=unused-import - except ImportError as exception: - raise ImportError( - "sequence_parallel_degree > 1 but ring_flash_attn is not installed. " - "Please install it with `pip install axolotl[ring-flash-attn] " - "or `pip install ring-flash-attn>=0.1.4`." - ) from exception - - # TODO: monkeypatch / callback to average losses correctly across SP ranks - # / fix gradient scaling across SP ranks. Losses, grads should be scaled - # according to the proportion of non-padding tokens per rank. - LOG.warning( - "Sequence parallelism (SP) is enabled with " - f"sequence_parallel_degree={self.sequence_parallel_degree}. " - "Please note that logged losses may differ slightly to the non-SP " - "losses due to transformers Trainer implementation details. " - "Please see https://github.com/axolotl-ai-cloud/axolotl/pull/2495#issuecomment-2784022042 " - "for more details." - ) - - return self - - @model_validator(mode="after") - def validate_ring_attn_func(self): - if getattr(self, "sequence_parallel_degree", 1) == 1: - return self - - if self.ring_attn_func is not None: - self.ring_attn_func = RingAttnFunc(self.ring_attn_func) - else: - # Default ring attention function selection - sample_packing = getattr(self, "sample_packing", False) - self.ring_attn_func = ( - RingAttnFunc.VARLEN_LLAMA3 - if sample_packing - else RingAttnFunc.BATCH_RING - ) - - return self - - @model_validator(mode="before") - @classmethod - def check_muon_deepspeed_fsdp(cls, data): - if data.get("optimizer") == "muon" and ( - data.get("deepspeed") or data.get("fsdp") or data.get("fsdp_config") - ): - raise ValueError( - "Muon optimizer is currently incompatible with DeepSpeed and FSDP" - ) - return data + max_steps: int | None = Field( + default=None, + json_schema_extra={ + "description": "Maximum number of iterations to train for. It precedes num_epochs which means that if both are set, num_epochs will not be guaranteed. e.g., when 1 epoch is 1000 steps => `num_epochs: 2` and `max_steps: 100` will train for 100 steps" + }, + ) + warmup_steps: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of warmup steps. Cannot use with warmup_ratio" + }, + ) + warmup_ratio: float | None = Field( + default=None, + json_schema_extra={"description": "Warmup ratio. Cannot use with warmup_steps"}, + ) + eval_steps: int | float | None = Field( + default=None, + json_schema_extra={ + "description": "Leave empty to eval at each epoch, integer for every N steps. float for fraction of total steps" + }, + ) + evals_per_epoch: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of times per epoch to run evals, mutually exclusive with eval_steps" + }, + ) + eval_strategy: str | None = Field( + default=None, + json_schema_extra={ + "description": "Set to `no` to skip evaluation, `epoch` at end of each epoch, leave empty to infer from `eval_steps`" + }, + ) + save_steps: int | float | None = Field( + default=None, + json_schema_extra={ + "description": "Leave empty to save at each epoch, integer for every N steps. float for fraction of total steps" + }, + ) + saves_per_epoch: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of times per epoch to save a checkpoint, mutually exclusive with save_steps" + }, + ) + save_strategy: str | None = Field( + default=None, + json_schema_extra={ + "description": "Set to `no` to skip checkpoint saves, `epoch` at end of each epoch, `best` when better result is achieved, leave empty to infer from `save_steps`" + }, + ) + save_total_limit: int | None = Field( + default=None, json_schema_extra={"description": "Checkpoints saved at a time"} + ) + logging_steps: int | None = Field( + default=None, json_schema_extra={"description": "Logging frequency"} + ) + early_stopping_patience: int | None = Field( + default=None, + json_schema_extra={ + "description": "Stop training after this many evaluation losses have increased in a row. https://huggingface.co/transformers/v4.2.2/_modules/transformers/trainer_callback.html#EarlyStoppingCallback" + }, + ) + load_best_model_at_end: bool | None = False + save_only_model: bool | None = Field( + default=False, + json_schema_extra={ + "description": "Save only the model weights, skipping the optimizer. Using this means you can't resume from checkpoints." + }, + ) + use_tensorboard: bool | None = Field( + default=None, json_schema_extra={"description": "Use tensorboard for logging"} + ) + profiler_steps: int | None = Field( + default=None, + json_schema_extra={ + "description": "Enable the pytorch profiler to capture the first N steps of training to the output_dir. see https://pytorch.org/blog/understanding-gpu-memory-1/ for more information. Snapshots can be visualized @ https://pytorch.org/memory_viz" + }, + ) + include_tokens_per_second: bool | None = Field( + default=None, + json_schema_extra={ + "description": "bool of whether to include tokens trainer per second in the training metrics. This iterates over the entire dataset once, so it takes some time." + }, + ) - @model_validator(mode="before") - @classmethod - def check_tokenizer_use_mistral_common(cls, data): - if data.get("tokenizer_use_mistral_common") is None: - if any( - "magistral" in name.lower() - for name in [ - data.get("base_model", ""), - data.get("base_model_config", ""), - data.get("tokenizer_config", ""), - ] - ): - LOG.warning( - "tokenizer_use_mistral_common auto inferred to True for Magistral models. Please set it to True explicitly if you want to use mistral-common tokenizer." - ) - data["tokenizer_use_mistral_common"] = True + neftune_noise_alpha: float | None = Field( + default=None, + json_schema_extra={ + "description": "NEFT https://arxiv.org/abs/2310.05914, set this to a number (paper default is 5) to add noise to embeddings. Currently only supported on Llama and Mistral" + }, + ) - return data + orpo_alpha: float | None = Field( + default=None, + json_schema_extra={ + "description": "Parameter controlling the relative ratio loss weight in the ORPO loss. Passed to `beta` in `ORPOConfig` due to trl mapping." + }, + ) + rpo_alpha: float | None = Field( + default=None, + json_schema_extra={ + "description": "Weighting of NLL term in loss from RPO paper" + }, + ) + simpo_gamma: float | None = Field( + default=None, + json_schema_extra={"description": "Target reward margin for the SimPO loss"}, + ) + cpo_alpha: float | None = Field( + default=None, json_schema_extra={"description": "Weight of the BC regularizer"} + ) - @field_validator("tokenizer_use_mistral_common", mode="after") - @classmethod - def check_mistral_common_import(cls, tokenizer_use_mistral_common): - if tokenizer_use_mistral_common: - try: - import mistral_common # noqa: F401 # pylint:disable=unused-import - except ImportError as exception: - raise ImportError( - "mistral-common is required for mistral models. Please install it with `pip install axolotl` or `pip install -e .`." - ) from exception + kto_desirable_weight: float | None = Field( + default=None, + json_schema_extra={"description": "Factor for desirable loss term in KTO loss"}, + ) + kto_undesirable_weight: float | None = Field( + default=None, + json_schema_extra={ + "description": "Factor for undesirable loss term in KTO loss" + }, + ) + rl_beta: float | None = Field( + default=None, + json_schema_extra={"description": "The beta parameter for the RL training"}, + ) - return tokenizer_use_mistral_common + max_memory: dict[int | Literal["cpu", "disk"], int | str] | None = Field( + default=None, + json_schema_extra={ + "description": "Defines the max memory usage per gpu on the system. Passed through to transformers when loading the model." + }, + ) + gpu_memory_limit: int | str | None = Field( + default=None, + json_schema_extra={ + "description": "Limit the memory for all available GPUs to this amount (if an integer, expressed in gigabytes); default: unset" + }, + ) + low_cpu_mem_usage: bool | None = Field( + default=None, + json_schema_extra={"description": "Whether to use low_cpu_mem_usage"}, + ) - @model_validator(mode="before") - @classmethod - def check_mistral_common_incompatible_options(cls, data): - if not data.get("tokenizer_use_mistral_common"): - return data + chat_template: ( + ChatTemplate + | Annotated[str, StringConstraints(pattern="^tokenizer_default_fallback_")] + ) | None = Field( + default=None, + json_schema_extra={ + "description": "The name of the chat template to use for training, following values are supported: tokenizer_default: Uses the chat template that is available in the tokenizer_config.json. If the chat template is not available in the tokenizer, it will raise an error. This is the default value. alpaca/inst/chatml/gemma/cohere/llama3/phi_3/deepseek_v2/jamba: These chat templates are available in the axolotl codebase at src/axolotl/utils/chat_templates.py. tokenizer_default_fallback_*: where * is the name of the chat template to fallback to. E.g. tokenizer_default_fallback_chatml. This is useful when the chat template is not available in the tokenizer. jinja: Uses a custom jinja template for the chat template. The custom jinja template should be provided in the chat_template_jinja field. The selected chat template will be saved to the tokenizer_config.json for easier inferencing" + }, + ) + chat_template_jinja: str | None = Field( + default=None, + json_schema_extra={ + "description": "Custom jinja template for chat template. This will be only used if chat_template is set to `jinja` or `null` (in which case chat_template is automatically set to `jinja`). Default is null." + }, + ) + eot_tokens: list[str] | None = Field( + default=None, + json_schema_extra={ + "description": "Custom EOT (End-of-Turn) tokens to mask/unmask during training. These tokens mark the boundaries between conversation turns. For example: ['/INST', '', '[/SYSTEM_PROMPT]']. If not specified, defaults to just the model's eos_token. This is useful for templates that use multiple delimiter tokens." + }, + ) + default_system_message: str | None = Field( + default=None, + json_schema_extra={ + "description": "Changes the default system message. Currently only supports chatml." + }, + ) - # NOTE: mistral-common tokenizer is not compatible with editing tokenizer at the moment + fix_untrained_tokens: int | list[int] | None = None - if data.get("added_tokens_overrides"): - raise ValueError( - "added_tokens_overrides is not supported with mistral-common tokenizer" - ) + # INTERNALS - document for now, generally not set externally + is_preprocess: bool | None = None + preprocess_iterable: bool | None = None - if data.get("special_tokens"): - raise ValueError( - "special_tokens override is not supported with mistral-common tokenizer" - ) + total_num_tokens: int | None = Field( + default=None, + json_schema_extra={"description": "Total number of tokens - internal use"}, + ) + total_supervised_tokens: int | None = None + sample_packing_eff_est: float | None = Field( + default=None, + json_schema_extra={ + "description": "You can set these packing optimizations AFTER starting a training at least once. The trainer will provide recommended values for these values." + }, + ) + axolotl_config_path: str | None = None - if data.get("tokens"): - raise ValueError( - "tokens override is not supported with mistral-common tokenizer" - ) + is_falcon_derived_model: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Internal use only - Used to identify which the model is based on" + }, + ) + is_llama_derived_model: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Internal use only - Used to identify which the model is based on" + }, + ) + is_mistral_derived_model: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Internal use only - Used to identify which the model is based on. Please note that if you set this to true, `padding_side` will be set to 'left' by default" + }, + ) + is_qwen_derived_model: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Internal use only - Used to identify which the model is based on" + }, + ) - if data.get("chat_template"): - raise ValueError( - "Setting chat_template is not supported with mistral-common tokenizer" - ) + plugins: list[str] | None = Field( + default=None, + json_schema_extra={ + "description": "Add plugins to extend the pipeline. See `src/axolotl/integrations` for the available plugins or doc below for more details. https://docs.axolotl.ai/docs/custom_integrations.html" + }, + ) - return data + @field_serializer("datasets") + def datasets_serializer( + self, ds_configs: list[DatasetConfig] | None + ) -> list[dict[str, Any]] | None: + if ds_configs: + return [ds_config.model_dump(exclude_none=True) for ds_config in ds_configs] + return None class AxolotlConfigWCapabilities(AxolotlInputConfig): - """wrapper to valdiate gpu capabilities with the configured options""" + """wrapper to valdiate GPU capabilities with the configured options""" capabilities: GPUCapabilities env_capabilities: EnvCapabilities @@ -1375,13 +877,7 @@ def check_sample_packing_w_sdpa_bf16(cls, data): return data - @model_validator(mode="before") - @classmethod - def check_fsdp_deepspeed(cls, data): - if data.get("deepspeed") and data.get("fsdp"): - raise ValueError("deepspeed and fsdp cannot be used together.") - return data - + # pylint: disable=duplicate-code @model_validator(mode="before") @classmethod def check_multigpu_unsloth(cls, data): @@ -1397,6 +893,7 @@ def check_multigpu_unsloth(cls, data): ) return data + # pylint: disable=duplicate-code @model_validator(mode="before") @classmethod def check_multigpu_lora_kernels(cls, data): diff --git a/src/axolotl/utils/schemas/datasets.py b/src/axolotl/utils/schemas/datasets.py index c71f9be77f..d9459feb95 100644 --- a/src/axolotl/utils/schemas/datasets.py +++ b/src/axolotl/utils/schemas/datasets.py @@ -1,6 +1,8 @@ """Pydantic models for datasets-related configuration""" -from pydantic import BaseModel, model_validator +from typing import Literal + +from pydantic import BaseModel, Field, model_validator from axolotl.utils.schemas.enums import ChatTemplate from axolotl.utils.schemas.utils import handle_legacy_message_fields_logic @@ -9,57 +11,178 @@ class UserDefinedPrompterType(BaseModel): """Structure for user defined prompt types""" - system_prompt: str | None = None - system_format: str | None = None + system_prompt: str | None = Field( + default=None, + json_schema_extra={"description": "Custom user instruction prompt"}, + ) + system_format: str | None = Field( + default=None, + json_schema_extra={"description": "Use {system} as key to be replaced"}, + ) field_system: str | None = None field_instruction: str | None = None field_input: str | None = None field_output: str | None = None - format: str | None = None - no_input_format: str | None = None - field: str | None = None + format: str | None = Field( + default=None, + json_schema_extra={ + "description": "Customizable to be single line or multi-line. Use {instruction}/{input} as key to be replaced. 'format' can include {input}" + }, + ) + no_input_format: str | None = Field( + default=None, + json_schema_extra={"description": "'no_input_format' cannot include {input}"}, + ) + field: str | None = Field( + default=None, + json_schema_extra={ + "description": "For `completion` datsets only, uses the provided field instead of `text` column" + }, + ) class SFTDataset(BaseModel): """SFT configuration subset""" - path: str | None = None - split: str | None = None - type: str | UserDefinedPrompterType | None = None + path: str | None = Field( + default=None, + json_schema_extra={ + "description": "HuggingFace dataset repo | s3:// | gs:// | path to local file or directory" + }, + ) + split: str | None = Field( + default=None, + json_schema_extra={"description": "name of dataset split to load from"}, + ) + type: str | UserDefinedPrompterType | None = Field( + default=None, + json_schema_extra={ + "description": "The type of prompt to use for training. [alpaca, gpteacher, oasst, reflection]" + }, + ) input_transform: str | None = None - shards: int | None = None - shards_idx: int | None = None - preprocess_shards: int | None = None + shards: int | None = Field( + default=None, + json_schema_extra={ + "description": "split dataset into N pieces (use with shards_idx)" + }, + ) + shards_idx: int | None = Field( + default=None, + json_schema_extra={"description": "the index of sharded dataset to use"}, + ) + preprocess_shards: int | None = Field( + default=None, + json_schema_extra={ + "description": "process dataset in N sequential chunks for memory efficiency (exclusive with `shards`)" + }, + ) conversation: str | None = None # Do not make this too strict or it will break the validator to choose different dataset class - chat_template: ChatTemplate | str | None = None - chat_template_jinja: str | None = None - data_files: str | list[str] | None = None + chat_template: ChatTemplate | str | None = Field( + default=None, + json_schema_extra={ + "description": "The name of the chat template to use for training, following values are supported: tokenizer_default: Uses the chat template that is available in the tokenizer_config.json. If the chat template is not available in the tokenizer, it will raise an error. This is the default. alpaca/inst/chatml/gemma/cohere/llama3/phi_3/deepseek_v2/jamba: These chat templates are available in the axolotl codebase at src/axolotl/utils/chat_templates.py. tokenizer_default_fallback_*: where * is the name of the chat template to fallback to if the tokenizer does not have a chat template else default to tokenizer. E.g. tokenizer_default_fallback_chatml. jinja: Uses a custom jinja template for the chat template. The custom jinja template should be provided in the chat_template_jinja field." + }, + ) + chat_template_jinja: str | None = Field( + default=None, + json_schema_extra={ + "description": "Custom jinja chat template. Used only if `chat_template: jinja` or empty." + }, + ) + data_files: str | list[str] | None = Field( + default=None, json_schema_extra={"description": "path to source data files"} + ) input_format: str | None = None - name: str | None = None - ds_type: str | None = None + name: str | None = Field( + default=None, + json_schema_extra={"description": "name of dataset configuration to load"}, + ) + ds_type: str | None = Field( + default=None, + json_schema_extra={"description": "defines the datatype when path is a file"}, + ) field: str | None = None field_human: str | None = None field_model: str | None = None - field_messages: str | None = None - field_tools: str | None = None + field_messages: str | None = Field( + default=None, + json_schema_extra={ + "description": 'Key containing the messages (default: "messages")' + }, + ) + field_tools: str | None = Field( + default=None, + json_schema_extra={ + "description": 'Key containing the tools (default: "tools"). Must be a list[dict] and follow [JSON schema](https://json-schema.org/learn/getting-started-step-by-step).' + }, + ) # deprecated, use message_property_mappings message_field_role: str | None = None # deprecated, use message_property_mappings message_field_content: str | None = None - message_property_mappings: dict[str, str] | None = None - message_field_training: str | None = None - message_field_training_detail: str | None = None - split_thinking: bool | None = None + message_property_mappings: dict[str, str] | None = Field( + default=None, + json_schema_extra={ + "description": "Mapping of properties from the input dataset to the chat template. (default: message_property_mappings={'role':'role', 'content':'content'}) If a property exists in the template but not in this mapping, the system will attempt to load it directly from the message using the property name as the key. Example: In the mapping below, 'from' is loaded from input dataset and used as 'role', while 'value' is loaded and used as 'content' in the chat template." + }, + ) + message_field_training: str | None = Field( + default=None, + json_schema_extra={ + "description": "The key in the message turn that indicates via boolean whether tokens of a turn should be considered for training. Useful to selectively train on certain turns besides the `roles_to_train`." + }, + ) + message_field_training_detail: str | None = Field( + default=None, + json_schema_extra={ + "description": "The key in the message turn that contains the training details. Useful to selectively train on certain tokens in a turn. The value of the key is a List[Dict] containing `begin_offset` (start character index in content), `end_offset` (end character index in content), and `train` (boolean whether to train)." + }, + ) + split_thinking: bool | None = Field( + default=None, + json_schema_extra={ + "description": "(for Qwen3 template only) Whether to split the assistant content based on a reasoning trace inside delimited tags" + }, + ) logprobs_field: str | None = None temperature: float | None = None - roles_to_train: list[str] | None = None - train_on_eos: str | None = None - roles: dict[str, list[str]] | None = None - drop_system_message: bool | None = None - trust_remote_code: bool | None = False - revision: str | None = None + roles_to_train: list[str] | None = Field( + default=None, + json_schema_extra={ + "description": "Roles to train on. The tokens from these roles will be considered for the loss." + }, + ) + train_on_eos: Literal["all", "turn", "last"] | None = Field( + default=None, + json_schema_extra={ + "description": "Which EOS tokens to train on in the conversation. Possible values are: all: train on all EOS tokens, turn (default): train on the EOS token at the end of each trainable turn, last: train on the last EOS token in the conversation" + }, + ) + roles: dict[str, list[str]] | None = Field( + default=None, + json_schema_extra={ + "description": 'Roles mapping in the messages. The format is {target_role: [source_roles]}. All source roles will be mapped to the target role. The default is: user: ["human", "user"], assistant: ["gpt", "assistant"], system: ["system"], tool: ["tool"]' + }, + ) + drop_system_message: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to drop the system turn from the dataset. Only works with chat_template. This does not drop the default system message from chat_template if it exists. If you wish to, we recommend using a custom jinja template with the default system message removed or adding a system turn with empty content." + }, + ) + trust_remote_code: bool | None = Field( + default=False, + json_schema_extra={"description": "Trust remote code for untrusted source"}, + ) + revision: str | None = Field( + default=None, + json_schema_extra={ + "description": "The specific revision of the dataset to use when loading from the Hugging Face Hub. This can be a commit hash, tag, or branch name. If not specified, the latest version will be used. This parameter is ignored for local datasets." + }, + ) @model_validator(mode="before") @classmethod diff --git a/src/axolotl/utils/schemas/deprecated.py b/src/axolotl/utils/schemas/deprecated.py index b8904136e4..972fe0ccfb 100644 --- a/src/axolotl/utils/schemas/deprecated.py +++ b/src/axolotl/utils/schemas/deprecated.py @@ -60,10 +60,30 @@ class RemappedParameters(BaseModel): """Parameters that have been remapped to other names""" overrides_of_model_config: dict[str, Any] | None = Field( - default=None, alias="model_config" + default=None, + alias="model_config", + json_schema_extra={ + "description": "optional overrides to the base model configuration" + }, ) overrides_of_model_kwargs: dict[str, Any] | None = Field( - default=None, alias="model_kwargs" + default=None, + alias="model_kwargs", + json_schema_extra={ + "description": "optional overrides the base model loading from_pretrained" + }, + ) + type_of_model: str | None = Field( + default=None, + alias="model_type", + json_schema_extra={ + "description": "If you want to specify the type of model to load, AutoModelForCausalLM is a good choice too" + }, + ) + revision_of_model: str | None = Field( + default=None, + alias="model_revision", + json_schema_extra={ + "description": "You can specify to choose a specific model revision from huggingface hub" + }, ) - type_of_model: str | None = Field(default=None, alias="model_type") - revision_of_model: str | None = Field(default=None, alias="model_revision") diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index d09ab63877..bfef14d533 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -1,5 +1,7 @@ """Enums for Axolotl input config""" +# pylint: disable=invalid-name + from enum import Enum import torch @@ -8,81 +10,81 @@ class TorchIntDType(Enum): """Torch integer data types - `getattr` guards against torch < 2.6 which does not support int4""" - uint1 = getattr(torch, "uint1", None) # pylint: disable=invalid-name - uint2 = getattr(torch, "uint2", None) # pylint: disable=invalid-name - uint3 = getattr(torch, "uint3", None) # pylint: disable=invalid-name - uint4 = getattr(torch, "uint4", None) # pylint: disable=invalid-name - uint5 = getattr(torch, "uint5", None) # pylint: disable=invalid-name - uint6 = getattr(torch, "uint6", None) # pylint: disable=invalid-name - uint7 = getattr(torch, "uint7", None) # pylint: disable=invalid-name - int4 = getattr(torch, "int4", None) # pylint: disable=invalid-name - int8 = getattr(torch, "int8", None) # pylint: disable=invalid-name + uint1 = getattr(torch, "uint1", None) + uint2 = getattr(torch, "uint2", None) + uint3 = getattr(torch, "uint3", None) + uint4 = getattr(torch, "uint4", None) + uint5 = getattr(torch, "uint5", None) + uint6 = getattr(torch, "uint6", None) + uint7 = getattr(torch, "uint7", None) + int4 = getattr(torch, "int4", None) + int8 = getattr(torch, "int8", None) class RLType(str, Enum): """RL trainer type configuration subset""" - DPO = "dpo" # pylint: disable=invalid-name - GRPO = "grpo" # pylint: disable=invalid-name - IPO = "ipo" # pylint: disable=invalid-name - ORPO = "orpo" # pylint: disable=invalid-name - KTO = "kto" # pylint: disable=invalid-name - SIMPO = "simpo" # pylint: disable=invalid-name + DPO = "dpo" + GRPO = "grpo" + IPO = "ipo" + ORPO = "orpo" + KTO = "kto" + SIMPO = "simpo" class ChatTemplate(str, Enum): """Chat templates configuration subset""" - alpaca = "alpaca" # pylint: disable=invalid-name - chatml = "chatml" # pylint: disable=invalid-name - mistral_v1 = "mistral_v1" # pylint: disable=invalid-name - mistral_v2v3 = "mistral_v2v3" # pylint: disable=invalid-name - mistral_v3_tekken = "mistral_v3_tekken" # pylint: disable=invalid-name - mistral_v7_tekken = "mistral_v7_tekken" # pylint: disable=invalid-name - gemma = "gemma" # pylint: disable=invalid-name - cohere = "cohere" # pylint: disable=invalid-name - llama3 = "llama3" # pylint: disable=invalid-name - llama3_2_vision = "llama3_2_vision" # pylint: disable=invalid-name - llama4 = "llama4" # pylint: disable=invalid-name - phi_3 = "phi_3" # pylint: disable=invalid-name - phi_35 = "phi_35" # pylint: disable=invalid-name - deepseek_v2 = "deepseek_v2" # pylint: disable=invalid-name - deepseek_v3 = "deepseek_v3" # pylint: disable=invalid-name - jamba = "jamba" # pylint: disable=invalid-name - jinja = "jinja" # pylint: disable=invalid-name - qwen_25 = "qwen_25" # pylint: disable=invalid-name - qwen3 = "qwen3" # pylint: disable=invalid-name - tokenizer_default = "tokenizer_default" # pylint: disable=invalid-name - exaone = "exaone" # pylint: disable=invalid-name - metharme = "metharme" # pylint: disable=invalid-name - pixtral = "pixtral" # pylint: disable=invalid-name - llava = "llava" # pylint: disable=invalid-name - qwen2_vl = "qwen2_vl" # pylint: disable=invalid-name - gemma3 = "gemma3" # pylint: disable=invalid-name - command_a = "command_a" # pylint: disable=invalid-name - command_a_tool_use = "command_a_tool_use" # pylint: disable=invalid-name - command_a_rag = "command_a_rag" # pylint: disable=invalid-name - aya = "aya" # pylint: disable=invalid-name + alpaca = "alpaca" + chatml = "chatml" + mistral_v1 = "mistral_v1" + mistral_v2v3 = "mistral_v2v3" + mistral_v3_tekken = "mistral_v3_tekken" + mistral_v7_tekken = "mistral_v7_tekken" + gemma = "gemma" + cohere = "cohere" + llama3 = "llama3" + llama3_2_vision = "llama3_2_vision" + llama4 = "llama4" + phi_3 = "phi_3" + phi_35 = "phi_35" + deepseek_v2 = "deepseek_v2" + deepseek_v3 = "deepseek_v3" + jamba = "jamba" + jinja = "jinja" + qwen_25 = "qwen_25" + qwen3 = "qwen3" + tokenizer_default = "tokenizer_default" + exaone = "exaone" + metharme = "metharme" + pixtral = "pixtral" + llava = "llava" + qwen2_vl = "qwen2_vl" + gemma3 = "gemma3" + command_a = "command_a" + command_a_tool_use = "command_a_tool_use" + command_a_rag = "command_a_rag" + aya = "aya" class CustomSupportedOptimizers(str, Enum): """Custom supported optimizers""" - optimi_adamw = "optimi_adamw" # pylint: disable=invalid-name - ao_adamw_4bit = "ao_adamw_4bit" # pylint: disable=invalid-name - ao_adamw_8bit = "ao_adamw_8bit" # pylint: disable=invalid-name - ao_adamw_fp8 = "ao_adamw_fp8" # pylint: disable=invalid-name - adopt_adamw = "adopt_adamw" # pylint: disable=invalid-name - came_pytorch = "came_pytorch" # pylint: disable=invalid-name - muon = "muon" # pylint: disable=invalid-name + optimi_adamw = "optimi_adamw" + ao_adamw_4bit = "ao_adamw_4bit" + ao_adamw_8bit = "ao_adamw_8bit" + ao_adamw_fp8 = "ao_adamw_fp8" + adopt_adamw = "adopt_adamw" + came_pytorch = "came_pytorch" + muon = "muon" class RingAttnFunc(str, Enum): """Enum class for supported `ring-flash-attn` implementations""" - # VARLEN_RING = "varlen_ring" - # VARLEN_ZIGZAG = "varlen_zigzag" VARLEN_LLAMA3 = "varlen_llama3" BATCH_RING = "batch_ring" + # VARLEN_RING = "varlen_ring" + # VARLEN_ZIGZAG = "varlen_zigzag" # BATCH_ZIGZAG = "batch_zigzag" # BATCH_STRIPE = "batch_stripe" diff --git a/src/axolotl/utils/schemas/integrations.py b/src/axolotl/utils/schemas/integrations.py index 4843e3592d..7332c7d39c 100644 --- a/src/axolotl/utils/schemas/integrations.py +++ b/src/axolotl/utils/schemas/integrations.py @@ -13,10 +13,21 @@ class MLFlowConfig(BaseModel): """MLFlow configuration subset""" use_mlflow: bool | None = None - mlflow_tracking_uri: str | None = None - mlflow_experiment_name: str | None = None - mlflow_run_name: str | None = None - hf_mlflow_log_artifacts: bool | None = None + mlflow_tracking_uri: str | None = Field( + default=None, json_schema_extra={"description": "URI to mlflow"} + ) + mlflow_experiment_name: str | None = Field( + default=None, json_schema_extra={"description": "Your experiment name"} + ) + mlflow_run_name: str | None = Field( + default=None, json_schema_extra={"description": "Your run name"} + ) + hf_mlflow_log_artifacts: bool | None = Field( + default=None, + json_schema_extra={ + "description": "set to true to copy each saved checkpoint on each save to mlflow artifact registry" + }, + ) class LISAConfig(BaseModel): @@ -40,13 +51,33 @@ class WandbConfig(BaseModel): """Wandb configuration subset""" use_wandb: bool | None = None - wandb_name: str | None = None - wandb_run_id: str | None = None - wandb_mode: str | None = None - wandb_project: str | None = None - wandb_entity: str | None = None + wandb_name: str | None = Field( + default=None, + json_schema_extra={"description": "Set the name of your wandb run"}, + ) + wandb_run_id: str | None = Field( + default=None, json_schema_extra={"description": "Set the ID of your wandb run"} + ) + wandb_mode: str | None = Field( + default=None, + json_schema_extra={ + "description": '"offline" to save run metadata locally and not sync to the server, "disabled" to turn off wandb' + }, + ) + wandb_project: str | None = Field( + default=None, json_schema_extra={"description": "Your wandb project name"} + ) + wandb_entity: str | None = Field( + default=None, + json_schema_extra={"description": "A wandb Team name if using a Team"}, + ) wandb_watch: str | None = None - wandb_log_model: str | None = None + wandb_log_model: str | None = Field( + default=None, + json_schema_extra={ + "description": '"checkpoint" to log model to wandb Artifacts every `save_steps` or "end" to log only at the end of training' + }, + ) @model_validator(mode="before") @classmethod @@ -64,14 +95,52 @@ def check_wandb_run(cls, data): class CometConfig(BaseModel): """Comet configuration subset""" - use_comet: bool | None = None - comet_api_key: str | None = None - comet_workspace: str | None = None - comet_project_name: str | None = None - comet_experiment_key: str | None = None - comet_mode: str | None = None - comet_online: bool | None = None - comet_experiment_config: dict[str, Any] | None = None + use_comet: bool | None = Field( + default=None, + json_schema_extra={"description": "Enable or disable Comet integration."}, + ) + comet_api_key: str | None = Field( + default=None, + json_schema_extra={ + "description": "API key for Comet. Recommended to set via `comet login`." + }, + ) + comet_workspace: str | None = Field( + default=None, + json_schema_extra={ + "description": "Workspace name in Comet. Defaults to the user's default workspace." + }, + ) + comet_project_name: str | None = Field( + default=None, + json_schema_extra={ + "description": "Project name in Comet. Defaults to Uncategorized." + }, + ) + comet_experiment_key: str | None = Field( + default=None, + json_schema_extra={ + "description": "Identifier for the experiment. Used to append data to an existing experiment or control the key of new experiments. Default to a random key." + }, + ) + comet_mode: str | None = Field( + default=None, + json_schema_extra={ + "description": 'Create a new experiment ("create") or log to an existing one ("get"). Default ("get_or_create") auto-selects based on configuration.' + }, + ) + comet_online: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Set to True to log data to Comet server, or False for offline storage. Default is True." + }, + ) + comet_experiment_config: dict[str, Any] | None = Field( + default=None, + json_schema_extra={ + "description": "Dictionary for additional configuration settings, see the doc for more details." + }, + ) class GradioConfig(BaseModel): diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index aafb52152e..6f995996d2 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -12,20 +12,55 @@ class ModelInputConfig(BaseModel): model_config = {"protected_namespaces": ()} - base_model: str - base_model_config: str | None = None + base_model: str = Field( + json_schema_extra={ + "description": "This is the huggingface model that contains *.pt, *.safetensors, or *.bin files. This can also be a relative path to a model on disk" + } + ) + base_model_config: str | None = Field( + default=None, + json_schema_extra={ + "description": "If the base_model repo on hf hub doesn't include configuration .json files, You can set that here, or leave this empty to default to base_model" + }, + ) cls_model_config: str | None = None - tokenizer_config: str | None = None - tokenizer_use_fast: bool | None = None - tokenizer_legacy: bool | None = None - tokenizer_use_mistral_common: bool | None = None + tokenizer_config: str | None = Field( + default=None, + json_schema_extra={ + "description": "Optional tokenizer configuration path in case you want to use a different tokenizer than the one defined in the base model" + }, + ) + tokenizer_use_fast: bool | None = Field( + default=None, + json_schema_extra={ + "description": "use_fast option for tokenizer loading from_pretrained, default to True" + }, + ) + tokenizer_legacy: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use the legacy tokenizer setting, defaults to True" + }, + ) + tokenizer_use_mistral_common: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use mistral-common tokenizer. If set to True, it will use the mistral-common tokenizer." + }, + ) tokenizer_type: str | None = Field( - default=None, json_schema_extra={"description": "transformers tokenizer class"} + default=None, + json_schema_extra={ + "description": "Corresponding tokenizer for the model AutoTokenizer is a good choice" + }, ) processor_type: str | None = Field( default=None, json_schema_extra={"description": "transformers processor class"} ) - trust_remote_code: bool | None = None + trust_remote_code: bool | None = Field( + default=None, + json_schema_extra={"description": "Trust remote code for untrusted source"}, + ) @field_validator("trust_remote_code") @classmethod @@ -40,10 +75,23 @@ def hint_trust_remote_code(cls, trust_remote_code): class ModelOutputConfig(BaseModel): """model save configuration subset""" - output_dir: str = Field(default="./model-out") - hub_model_id: str | None = None - hub_strategy: str | None = None - save_safetensors: bool | None = True + output_dir: str = Field( + default="./model-out", + json_schema_extra={"description": "Where to save the full-finetuned model to"}, + ) + hub_model_id: str | None = Field( + default=None, json_schema_extra={"description": "push checkpoints to hub"} + ) + hub_strategy: str | None = Field( + default=None, + json_schema_extra={"description": "how to push checkpoints to hub"}, + ) + save_safetensors: bool | None = Field( + default=True, + json_schema_extra={ + "description": "Save model as safetensors (require safetensors package). Default True" + }, + ) class SpecialTokensConfig(BaseModel): diff --git a/src/axolotl/utils/schemas/peft.py b/src/axolotl/utils/schemas/peft.py index 5d408e1fec..4b31ce0187 100644 --- a/src/axolotl/utils/schemas/peft.py +++ b/src/axolotl/utils/schemas/peft.py @@ -9,7 +9,7 @@ class LoftQConfig(BaseModel): """LoftQ configuration subset""" loftq_bits: int = Field( - default=4, json_schema_extra={"description": "Quantization bits for LoftQ"} + default=4, json_schema_extra={"description": "typically 4 bits"} ) # loftq_iter: int = Field(default=1, json_schema_extra={"description": "Alternating iterations for LoftQ"}) @@ -17,31 +17,78 @@ class LoftQConfig(BaseModel): class PeftConfig(BaseModel): """peftq configuration subset""" - loftq_config: LoftQConfig | None = None + loftq_config: LoftQConfig | None = Field( + default=None, + json_schema_extra={ + "description": "Configuration options for loftq initialization for LoRA" + }, + ) class LoraConfig(BaseModel): """Peft / LoRA configuration subset""" - load_in_8bit: bool | None = Field(default=False) - load_in_4bit: bool | None = Field(default=False) + load_in_8bit: bool | None = Field( + default=False, + json_schema_extra={ + "description": "This will attempt to quantize the model down to 8 bits and use adam 8 bit optimizer" + }, + ) + load_in_4bit: bool | None = Field( + default=False, json_schema_extra={"description": "Use bitsandbytes 4 bit"} + ) - adapter: str | None = None - lora_model_dir: str | None = None + adapter: str | None = Field( + default=None, + json_schema_extra={ + "description": "If you want to use 'lora' or 'qlora' or leave blank to train all parameters in original model" + }, + ) + lora_model_dir: str | None = Field( + default=None, + json_schema_extra={ + "description": "If you already have a lora model trained that you want to load, put that here. This means after training, if you want to test the model, you should set this to the value of `output_dir`. Note that if you merge an adapter to the base model, a new subdirectory `merged` will be created under the `output_dir`." + }, + ) lora_r: int | None = None lora_alpha: int | None = None lora_fan_in_fan_out: bool | None = None lora_target_modules: str | list[str] | None = None - lora_target_linear: bool | None = None - lora_modules_to_save: list[str] | None = None + lora_target_linear: bool | None = Field( + default=None, + json_schema_extra={"description": "If true, will target all linear modules"}, + ) + lora_modules_to_save: list[str] | None = Field( + default=None, + json_schema_extra={ + "description": "If you added new tokens to the tokenizer, you may need to save some LoRA modules because they need to know the new tokens. For LLaMA and Mistral, you need to save `embed_tokens` and `lm_head`. It may vary for other models. `embed_tokens` converts tokens to embeddings, and `lm_head` converts embeddings to token probabilities." + }, + ) lora_dropout: float | None = 0.0 - peft_layers_to_transform: list[int] | None = None + peft_layers_to_transform: list[int] | None = Field( + default=None, + json_schema_extra={ + "description": "The layer indices to transform, otherwise, apply to all layers" + }, + ) peft_layers_pattern: list[str] | None = None peft: PeftConfig | None = None - peft_use_dora: bool | None = None - peft_use_rslora: bool | None = None - peft_layer_replication: list[tuple[int, int]] | None = None - peft_init_lora_weights: bool | str | None = None + peft_use_dora: bool | None = Field( + default=None, json_schema_extra={"description": "Whether to use DoRA."} + ) + peft_use_rslora: bool | None = Field( + default=None, json_schema_extra={"description": "Whether to use RSLoRA."} + ) + peft_layer_replication: list[tuple[int, int]] | None = Field( + default=None, + json_schema_extra={"description": "List of layer indices to replicate."}, + ) + peft_init_lora_weights: bool | str | None = Field( + default=None, + json_schema_extra={ + "description": "How to initialize LoRA weights. Default to True which is MS original implementation." + }, + ) qlora_sharded_model_loading: bool | None = Field( default=False, @@ -49,9 +96,24 @@ class LoraConfig(BaseModel): "description": "load qlora model in sharded format for FSDP using answer.ai technique." }, ) - lora_on_cpu: bool | None = None - gptq: bool | None = None - bnb_config_kwargs: dict[str, Any] | None = None + lora_on_cpu: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Do the LoRA/PEFT loading on CPU -- this is required if the base model is so large it takes up most or all of the available GPU VRAM, e.g. during a model and LoRA merge" + }, + ) + gptq: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether you are training a 4-bit GPTQ quantized model" + }, + ) + bnb_config_kwargs: dict[str, Any] | None = Field( + default=None, + json_schema_extra={ + "description": "optional overrides to the bnb 4bit quantization configuration" + }, + ) loraplus_lr_ratio: float | None = Field( default=None, @@ -62,7 +124,7 @@ class LoraConfig(BaseModel): loraplus_lr_embedding: float | None = Field( default=1e-6, json_schema_extra={ - "description": "loraplus learning rate for lora embedding layers." + "description": "loraplus learning rate for lora embedding layers. Default value is 1e-6." }, ) @@ -125,8 +187,29 @@ def validate_lora_dropout(cls, data): class ReLoRAConfig(BaseModel): """ReLoRA configuration subset""" - relora_steps: int | None = None - relora_warmup_steps: int | None = None - relora_anneal_steps: int | None = None - relora_prune_ratio: float | None = None - relora_cpu_offload: bool | None = None + relora_steps: int | None = Field( + default=None, + json_schema_extra={"description": "Number of steps per ReLoRA restart"}, + ) + relora_warmup_steps: int | None = Field( + default=None, + json_schema_extra={"description": "Number of per-restart warmup steps"}, + ) + relora_anneal_steps: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of anneal steps for each relora cycle" + }, + ) + relora_prune_ratio: float | None = Field( + default=None, + json_schema_extra={ + "description": "threshold for optimizer magnitude when pruning" + }, + ) + relora_cpu_offload: bool | None = Field( + default=None, + json_schema_extra={ + "description": "True to perform lora weight merges on cpu during restarts, for modest gpu memory savings" + }, + ) diff --git a/src/axolotl/utils/schemas/quantization.py b/src/axolotl/utils/schemas/quantization.py index fe2cdb1fe3..090640c7bc 100644 --- a/src/axolotl/utils/schemas/quantization.py +++ b/src/axolotl/utils/schemas/quantization.py @@ -15,17 +15,22 @@ class QATConfig(BaseModel): """ activation_dtype: TorchIntDType | None = Field( - default=None, description="Activation dtype" + default=None, + description='Fake quantization layout to use for activation quantization. Valid options are "int4" and "int8"', ) weight_dtype: TorchIntDType = Field( - default=TorchIntDType.int8, description="Weight dtype" + default=TorchIntDType.int8, + description='Fake quantization layout to use for weight quantization. Valid options are "int4" and "int8"', ) quantize_embedding: bool | None = Field( default=False, description="Quantize embedding" ) - group_size: int | None = Field(default=32, description="Group size") + group_size: int | None = Field( + default=32, + description="The number of elements in each group for per-group fake quantization", + ) fake_quant_after_n_steps: int | None = Field( - default=None, description="Fake quant after n steps" + default=None, description="The number of steps to apply fake quantization after" ) @field_validator("activation_dtype", "weight_dtype", mode="before") @@ -44,15 +49,20 @@ class PTQConfig(BaseModel): """ weight_dtype: TorchIntDType = Field( - default=TorchIntDType.int8, description="Weight dtype" + default=TorchIntDType.int8, + description="Fake quantization layout to use for weight quantization. Valid options are uintX for X in [1, 2, 3, 4, 5, 6, 7], or int4, or int8", ) activation_dtype: TorchIntDType | None = Field( - default=None, description="Activation dtype" + default=None, + description='Fake quantization layout to use for activation quantization. Valid options are "int4" and "int8"', ) quantize_embedding: bool | None = Field( - default=None, description="Quantize embedding" + default=None, description="Whether to quantize the embedding layer." + ) + group_size: int | None = Field( + default=32, + description="The number of elements in each group for per-group fake quantization", ) - group_size: int | None = Field(default=32, description="Group size") @field_validator("activation_dtype", "weight_dtype", mode="before") @classmethod diff --git a/src/axolotl/utils/schemas/training.py b/src/axolotl/utils/schemas/training.py index ad7f899aca..4d88cc9e63 100644 --- a/src/axolotl/utils/schemas/training.py +++ b/src/axolotl/utils/schemas/training.py @@ -23,10 +23,17 @@ class LrGroup(BaseModel): class HyperparametersConfig(BaseModel): """Training hyperparams configuration subset""" - gradient_accumulation_steps: int | None = Field(default=1) + gradient_accumulation_steps: int | None = Field( + default=1, + json_schema_extra={ + "description": "If greater than 1, backpropagation will be skipped and the gradients will be accumulated for the given number of steps." + }, + ) micro_batch_size: int | None = Field( default=1, - json_schema_extra={"description": "per gpu micro batch size for training"}, + json_schema_extra={ + "description": "The number of samples to include in each batch. This is the number of samples sent to each GPU. Batch size per gpu = micro_batch_size * gradient_accumulation_steps" + }, ) batch_size: int | None = Field( default=None, @@ -41,45 +48,99 @@ class HyperparametersConfig(BaseModel): }, ) - auto_find_batch_size: bool | None = None + auto_find_batch_size: bool | None = Field( + default=None, + json_schema_extra={ + "description": "whether to find batch size that fits in memory. Passed to underlying transformers Trainer" + }, + ) - train_on_inputs: bool | None = False - group_by_length: bool | None = None + train_on_inputs: bool | None = Field( + default=False, + json_schema_extra={ + "description": "Whether to mask out or include the human's prompt from the training labels" + }, + ) + group_by_length: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Group similarly sized data to minimize padding. May be slower to start, as it must download and sort the entire dataset. Note that training loss may have an oscillating pattern with this enabled." + }, + ) learning_rate: str | float embedding_lr: float | None = None embedding_lr_scale: float | None = None - weight_decay: float | None = 0.0 - optimizer: (OptimizerNames | CustomSupportedOptimizers) | None = ( - OptimizerNames.ADAMW_TORCH_FUSED + weight_decay: float | None = Field( + default=0.0, json_schema_extra={"description": "Specify weight decay"} + ) + optimizer: (OptimizerNames | CustomSupportedOptimizers) | None = Field( + default=OptimizerNames.ADAMW_TORCH_FUSED, + json_schema_extra={"description": "Specify optimizer"}, ) optim_args: (str | dict[str, Any]) | None = Field( default=None, - json_schema_extra={"description": "Optional arguments to supply to optimizer."}, + json_schema_extra={ + "description": "Dictionary of arguments to pass to the optimizer" + }, ) optim_target_modules: (list[str] | Literal["all_linear"]) | None = Field( default=None, json_schema_extra={ - "description": "The target modules to optimize, i.e. the module names that you would like to train." + "description": "The target modules to optimize, i.e. the module names that you would like to train, right now this is used only for GaLore algorithm" + }, + ) + torchdistx_path: str | None = Field( + default=None, + json_schema_extra={ + "description": "Path to torch distx for optim 'adamw_anyprecision'" }, ) - torchdistx_path: str | None = None lr_scheduler: (SchedulerType | Literal["one_cycle"] | Literal["rex"]) | None = ( SchedulerType.COSINE ) - lr_scheduler_kwargs: dict[str, Any] | None = None + lr_scheduler_kwargs: dict[str, Any] | None = Field( + default=None, + json_schema_extra={ + "description": "Specify a scheduler and kwargs to use with the optimizer" + }, + ) lr_quadratic_warmup: bool | None = None - cosine_min_lr_ratio: float | None = None - cosine_constant_lr_ratio: float | None = None - lr_div_factor: float | None = None + cosine_min_lr_ratio: float | None = Field( + default=None, + json_schema_extra={ + "description": "decay lr to some percentage of the peak lr, e.g. cosine_min_lr_ratio=0.1 for 10% of peak lr" + }, + ) + cosine_constant_lr_ratio: float | None = Field( + default=None, + json_schema_extra={ + "description": "freeze lr at some percentage of the step, e.g. cosine_constant_lr_ratio=0.8 means start cosine_min_lr at 80% of training step" + }, + ) + lr_div_factor: float | None = Field( + default=None, json_schema_extra={"description": "Learning rate div factor"} + ) lr_groups: list[LrGroup] | None = None - adam_epsilon: float | None = None - adam_epsilon2: float | None = None - adam_beta1: float | None = None - adam_beta2: float | None = None - adam_beta3: float | None = None - max_grad_norm: float | None = None + adam_epsilon: float | None = Field( + default=None, json_schema_extra={"description": "adamw hyperparams"} + ) + adam_epsilon2: float | None = Field( + default=None, json_schema_extra={"description": "only used for CAME Optimizer"} + ) + adam_beta1: float | None = Field( + default=None, json_schema_extra={"description": "adamw hyperparams"} + ) + adam_beta2: float | None = Field( + default=None, json_schema_extra={"description": "adamw hyperparams"} + ) + adam_beta3: float | None = Field( + default=None, json_schema_extra={"description": "only used for CAME Optimizer"} + ) + max_grad_norm: float | None = Field( + default=None, json_schema_extra={"description": "Gradient clipping max norm"} + ) num_epochs: float = Field(default=1.0) @field_validator("batch_size") diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index 37b71dba89..d1b18a56e2 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -10,12 +10,14 @@ class TRLConfig(BaseModel): beta: float | None = Field( default=None, - json_schema_extra={"description": "Beta for RL training"}, + json_schema_extra={ + "description": "Beta parameter for the RL training. Same as `rl_beta`. Use" + }, ) max_completion_length: int | None = Field( default=None, json_schema_extra={ - "description": "Maximum length of the completion for RL training" + "description": "Maximum length of the completion for RL training." }, ) @@ -23,81 +25,69 @@ class TRLConfig(BaseModel): # Ref: https://github.com/huggingface/trl/blob/26d86757a7c7e24e397ea44f57ecce6031dfac01/trl/trainer/grpo_config.py#L23 use_vllm: bool = Field( default=False, - json_schema_extra={"description": "Whether to use VLLM for RL training"}, + json_schema_extra={"description": "Whether to use VLLM for RL training."}, ) vllm_server_host: str | None = Field( default="0.0.0.0", # nosec B104 - json_schema_extra={"description": "Host of the vLLM server to connect to"}, + json_schema_extra={"description": "Host of the vLLM server to connect to."}, ) vllm_server_port: int | None = Field( default=8000, - json_schema_extra={"description": "Port of the vLLM server to connect to"}, + json_schema_extra={"description": "Port of the vLLM server to connect to."}, ) vllm_server_timeout: int | None = Field( default=None, json_schema_extra={ - "description": "Total timeout duration in seconds to wait for the vLLM server to be up. If the server is not up " - "after the timeout, a `ConnectionError` is raised." + "description": "Total timeout (in seconds) to wait for the vLLM server to respond." }, ) vllm_guided_decoding_regex: str | None = Field( default=None, - json_schema_extra={ - "description": "Regex for vLLM guided decoding. If `None` (default), guided decoding is disabled." - }, + json_schema_extra={"description": "Regex for vLLM guided decoding."}, ) reward_funcs: list[str] | None = Field( default=None, - json_schema_extra={"description": "List of reward functions to load"}, + json_schema_extra={ + "description": "List of reward functions to load. Paths must be importable from current dir." + }, ) reward_weights: list[float] | None = Field( default=None, json_schema_extra={ - "description": "Weights for each reward function. Must match the number of reward functions." + "description": "List of reward weights for the reward functions." }, ) num_generations: int | None = Field( default=None, - json_schema_extra={ - "description": "Number of generations to sample. The global batch size (num_processes * per_device_batch_size) must be divisible by this value." - }, + json_schema_extra={"description": "Number of generations to sample."}, ) log_completions: bool | None = Field( default=False, - json_schema_extra={"description": "Whether to log completions"}, + json_schema_extra={"description": "Whether to log completions."}, ) num_completions_to_print: int | None = Field( default=None, json_schema_extra={ - "description": "Number of completions to print. If `log_completions` is `True`, this will be the number of completions logged." + "description": "Number of completions to print when log_completions is True." }, ) sync_ref_model: bool | None = Field( default=False, - json_schema_extra={ - "description": ( - "Whether to sync the reference model every `ref_model_sync_steps` " - "steps, using the `ref_model_mixup_alpha` parameter." - ) - }, + json_schema_extra={"description": "Whether to sync the reference model."}, ) ref_model_mixup_alpha: float | None = Field( default=0.9, - json_schema_extra={ - "description": "Mixup alpha for the reference model. Requires `sync_ref_model=True`." - }, + json_schema_extra={"description": "Mixup alpha for the reference model."}, ) ref_model_sync_steps: int | None = Field( default=64, - json_schema_extra={ - "description": "Sync steps for the reference model. Requires `sync_ref_model=True`." - }, + json_schema_extra={"description": "Sync steps for the reference model."}, ) scale_rewards: bool = Field( default=True, json_schema_extra={ - "description": "Whether to scale the rewards for GRPO by dividing them by their standard deviation." + "description": "Whether to scale rewards by their standard deviation." }, ) @@ -124,13 +114,13 @@ class TRLConfig(BaseModel): repetition_penalty: float | None = Field( default=None, json_schema_extra={ - "description": "Float that penalizes new tokens based on whether they appear in the prompt and the generated text so far." + "description": "Penalty for tokens that appear in prompt and generated text." }, ) num_iterations: int | None = Field( default=None, json_schema_extra={ - "description": "Number of iterations per batch (denoted as μ in the algorithm) for GRPO." + "description": "Number of iterations per batch (μ) for GRPO." }, ) epsilon: float | None = Field( @@ -152,12 +142,12 @@ class TRLConfig(BaseModel): loss_type: str | None = Field( default=None, json_schema_extra={ - "description": "Specifies the loss formulation to use. Supported values are `grpo`, `bnpo`, and `dr_grpo`." + "description": "Loss formulation to use. Supported values: grpo, bnpo, dr_grpo." }, ) mask_truncated_completions: bool = Field( default=False, json_schema_extra={ - "description": "When enabled, truncated completions are excluded from the loss calculation." + "description": "Whether to exclude truncated completions from loss calculation." }, ) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py new file mode 100644 index 0000000000..5a6bf43b3b --- /dev/null +++ b/src/axolotl/utils/schemas/validation.py @@ -0,0 +1,1073 @@ +"""Module with validation methods for config pydantic model.""" + +# pylint: disable=too-many-lines + +import logging + +from pydantic import ( + field_validator, + model_validator, +) +from transformers.utils.import_utils import is_torch_npu_available + +from axolotl.utils.schemas.enums import ChatTemplate, RingAttnFunc, RLType + +LOG = logging.getLogger(__name__) + +SUPPORTED_METRICS = {"sacrebleu", "comet", "ter", "chrf", "perplexity"} + + +class DatasetValidationMixin: + """Validation methods related to dataset configuration.""" + + @field_validator("seed", mode="after") + @classmethod + def set_default_seed(cls, seed): + if seed is None: + LOG.info("`seed` not set in config; setting to 42") + seed = 42 + return seed + + @field_validator("datasets", mode="before") + @classmethod + def deprecate_sharegpt_datasets(cls, datasets): + for _, ds_cfg in enumerate(datasets): + ds_type = ( + ds_cfg.get("type") + if isinstance(ds_cfg, dict) + else getattr(ds_cfg, "type", None) + ) + if not ds_type: + continue + + if isinstance(ds_type, dict): + continue + + if isinstance(ds_type, str) and ds_type.startswith("sharegpt"): + raise ValueError( + "`type: sharegpt.*` is deprecated. Please use `type: chat_template` instead." + ) + + return datasets + + @model_validator(mode="before") + @classmethod + def check_dataset_or_pretraining_dataset(cls, data): + if data.get("datasets") is None and data.get("pretraining_dataset") is None: + raise ValueError("either datasets or pretraining_dataset is required") + return data + + @model_validator(mode="before") + @classmethod + def check_push_ds_auth(cls, data): + if ( + data.get("push_dataset_to_hub") + and data.get("hf_use_auth_token") is not True + ): + raise ValueError( + "Require cfg.hf_use_auth_token to be True for push_dataset_to_hub" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_val_w_test_datasets(cls, data): + if data.get("test_datasets") and data.get("val_set_size"): + raise ValueError( + "non-zero val_set_size should not be used with test_datasets configuration" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_test_datasets_bench(cls, data): + if ( + data.get("do_bench_eval") + and not data.get("test_datasets") + and not data.get("val_set_size") + ): + LOG.warning( + "`do_bench_eval` needs a test dataset to run evals, adding an empty test_dataset." + ) + data["test_datasets"] = [{"path": "axolotl-ai-co/empty-test-ds"}] + return data + + @model_validator(mode="before") + @classmethod + def check_eval_packing(cls, data): + # TODO also should check test_datasets and val_set_size as we can skip + # if there are no eval datasets/splits + if ( + data.get("sample_packing") + and data.get("eval_table_size") + and data.get("eval_sample_packing") is not False + ): + raise ValueError( + "eval_table_size and eval_sample_packing are not supported together with sample_packing. Please set 'eval_sample_packing' to false." + ) + if ( + data.get("sample_packing") + and data.get("eval_sample_packing") is None + and not data.get("eval_table_size") + ): + LOG.info( + "explicitly setting `eval_sample_packing` to match `sample_packing`" + ) + data["eval_sample_packing"] = True + + if ( + data.get("sample_packing") + and data.get("eval_sample_packing") is False + and data.get("remove_unused_columns") is None + ): + LOG.info( + "setting `remove_unused_columns: false` for when sample_packing and eval_sample_packing don't match" + ) + data["remove_unused_columns"] = False + + return data + + @model_validator(mode="before") + @classmethod + def check_mm_prepare(cls, data): + if data.get("skip_prepare_dataset"): + if data.get("remove_unused_columns") is None: + LOG.info( + "setting `remove_unused_columns: false` for skip_prepare_dataset" + ) + data["remove_unused_columns"] = False + + return data + + +class AttentionValidationMixin: + """Validation methods related to attention mechanisms.""" + + @model_validator(mode="before") + @classmethod + def check_attention_fields(cls, data): + fields = ( + "xformers_attention", + "sdp_attention", + "s2_attention", + "flash_attention", + "flex_attention", + ) + non_empty_count = sum(1 for field in fields if data.get(field)) + + if non_empty_count > 1: + raise ValueError(f"Only one of {', '.join(fields)} must be set") + return data + + @model_validator(mode="before") + @classmethod + def check_sample_packing_without_attention(cls, data): + if ( + data.get("sample_packing") + and not data.get("flash_attention") + and not data.get("sdp_attention") + and not data.get("flex_attention") + and not data.get("xformers_attention") + ): + LOG.warning( + "sample_packing without flash, sdp, xformers or flex attention does not handle cross sample decontamination." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_sample_packing_with_s2attn(cls, data): + if data.get("sample_packing") and data.get("s2_attention"): + raise ValueError( + "Received `sample_packing=true` and `s2_attention=true`; however, \ + shifted-sparse attention does not currently support sample packing." + ) + return data + + +class TrainingValidationMixin: + """Validation methods related to training configuration.""" + + @model_validator(mode="before") + @classmethod + def check_batch_size_fields(cls, data): + fields = ("micro_batch_size", "gradient_accumulation_steps", "batch_size") + non_empty_count = sum(1 for field in fields if data.get(field)) + + if non_empty_count < 2: + raise ValueError(f"At least two of {', '.join(fields)} must be set") + return data + + @model_validator(mode="before") + @classmethod + def hint_sample_packing_padding(cls, data): + if data.get("sample_packing"): + pad_to_sequence_len = data.get("pad_to_sequence_len") + if pad_to_sequence_len is False: + LOG.warning( + "`pad_to_sequence_len: true` is recommended when using sample_packing" + ) + elif pad_to_sequence_len is None: + LOG.info( + "Setting `pad_to_sequence_len: true` to prevent memory leaks when sample_packing" + ) + data["pad_to_sequence_len"] = True + return data + + @model_validator(mode="before") + @classmethod + def hint_reward_model_pad(cls, data): + if data.get("reward_model") and not data.get("pad_to_sequence_len"): + LOG.warning( + "`pad_to_sequence_len: true` is recommended when using reward_model" + ) + if data.get("pad_to_sequence_len") is None: + data["pad_to_sequence_len"] = True + return data + + @model_validator(mode="before") + @classmethod + def check_gas_bsz(cls, data): + if data.get("gradient_accumulation_steps") and data.get("batch_size"): + raise ValueError( + "please set only one of gradient_accumulation_steps or batch_size" + ) + return data + + @model_validator(mode="before") + @classmethod + def hint_eval_train_mbsz(cls, data): + if ( + data.get("eval_batch_size") + and data.get("micro_batch_size") + and data.get("eval_batch_size") != data.get("micro_batch_size") + ): + LOG.warning( + "eval_batch_size != micro_batch_size. This can lead to VRAM instability." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_warmup(cls, data): + if data.get("warmup_steps") and data.get("warmup_ratio"): + raise ValueError("warmup_steps and warmup_ratio are mutually exclusive") + return data + + @model_validator(mode="before") + @classmethod + def check_saves(cls, data): + if ( + data.get("save_strategy") + and data.get("save_steps") + and data.get("save_strategy") != "steps" + ): + raise ValueError( + "save_strategy and save_steps mismatch. Please set save_strategy to 'steps' or remove save_steps." + ) + if data.get("saves_per_epoch") and data.get("save_steps"): + raise ValueError( + "save_steps and saves_per_epoch are mutually exclusive and cannot be used together." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_push_save(cls, data): + if data.get("hub_model_id") and ( + data.get("save_strategy") not in ["steps", "epoch", None] + ): + LOG.warning( + "hub_model_id is set without any models being saved. To save a model, set save_strategy." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_evals(cls, data): + if ( + data.get("eval_strategy") + and data.get("eval_steps") + and data.get("eval_strategy") != "steps" + ): + raise ValueError( + "eval_strategy and eval_steps mismatch. Please set eval_strategy to 'steps' or remove eval_steps." + ) + + if ( + data.get("val_set_size") == 0 + and (data.get("eval_steps") or data.get("eval_strategy")) + and not data.get("test_datasets") + and data.get("eval_strategy") != "no" + ): + raise ValueError( + "eval_steps and eval_strategy are not supported with val_set_size == 0" + ) + if data.get("evals_per_epoch") and data.get("eval_steps"): + raise ValueError( + "eval_steps and evals_per_epoch are mutually exclusive and cannot be used together." + ) + if ( + data.get("evals_per_epoch") + and data.get("eval_strategy") + and data.get("eval_strategy") != "steps" + ): + raise ValueError( + "eval_strategy must be empty or set to `steps` when used with evals_per_epoch." + ) + + if data.get("do_bench_eval") and not ( + data.get("evals_per_epoch") or data.get("eval_steps") + ): + raise ValueError( + "do_bench_eval requires evals_per_epoch or eval_steps to be set." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_neftune(cls, data): + if data.get("noisy_embedding_alpha") and not data.get("neftune_noise_alpha"): + data["neftune_noise_alpha"] = data["noisy_embedding_alpha"] + del data["noisy_embedding_alpha"] + elif data.get("noisy_embedding_alpha") and data.get("neftune_noise_alpha"): + raise ValueError( + "noisy_embedding_alpha is deprecated, use neftune_noise_alpha; both are set, please remove the deprecated noisy_embedding_alpha setting" + ) + return data + + @model_validator(mode="after") + def check_fft_possible_bad_config(self): + if ( + # pylint: disable=too-many-boolean-expressions + not (self.bf16 or self.bfloat16) + and (self.fp16 or self.float16) + and not self.adapter + and not self.flash_attention + and self.sample_packing + ): + LOG.warning( + "Full fine tune w/o FA2 w/ sample packing and fp16/float16 is likely to raise errors. Try LoRA." + ) + # ValueError: Attempting to unscale FP16 gradients. + # OR + # RuntimeError: expected mat1 and mat2 to have the same dtype, but got: float != c10::Half + return self + + @model_validator(mode="before") + @classmethod + def check_use_reentrant_mismatch(cls, data): + if ( + data.get("unfrozen_parameters") + and data.get("gradient_checkpointing_kwargs") + and data.get("gradient_checkpointing_kwargs", {}).get("use_reentrant") + is True + ): + # https://github.com/huggingface/transformers/issues/21381 + raise ValueError( + "`use_reentrant` must be false when used with partially frozen model." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_eval_strategy(cls, data): + if ( + data.get("evaluation_strategy") is not None + and data.get("eval_strategy") is None + ): + LOG.info( + "explicitly setting `eval_strategy` from the `evaluation_strategy`" + ) + data["eval_strategy"] = data.get("evaluation_strategy") + return data + + @model_validator(mode="before") + @classmethod + def check_causal_lm_evals(cls, data): + if data.get("do_causal_lm_eval") and data.get("eval_sample_packing"): + raise ValueError( + "do_causal_lm_eval is enabled, eval_sample_packing must be set to False" + ) + + if data.get("eval_causal_lm_metrics"): + if not isinstance(data.get("eval_causal_lm_metrics"), list): + raise ValueError("eval_causal_lm_metrics must be a list") + # only ["sacrebleu", "comet", "ter", "chrf"] supported + if set(data.get("eval_causal_lm_metrics")) - SUPPORTED_METRICS: + raise ValueError( + f"eval_causal_lm_metrics must be one of {SUPPORTED_METRICS}" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_tokenizer_use_mistral_common(cls, data): + if data.get("tokenizer_use_mistral_common") is None: + if any( + "magistral" in name.lower() + for name in [ + data.get("base_model", ""), + data.get("base_model_config", ""), + data.get("tokenizer_config", ""), + ] + ): + LOG.warning( + "tokenizer_use_mistral_common auto inferred to True for Magistral models. Please set it to True explicitly if you want to use mistral-common tokenizer." + ) + data["tokenizer_use_mistral_common"] = True + + return data + + @field_validator("tokenizer_use_mistral_common", mode="after") + @classmethod + def check_mistral_common_import(cls, tokenizer_use_mistral_common): + if tokenizer_use_mistral_common: + try: + import mistral_common # noqa: F401 # pylint:disable=unused-import + except ImportError as exception: + raise ImportError( + "mistral-common is required for mistral models. Please install it with `pip install axolotl` or `pip install -e .`." + ) from exception + + return tokenizer_use_mistral_common + + @model_validator(mode="before") + @classmethod + def check_mistral_common_incompatible_options(cls, data): + if not data.get("tokenizer_use_mistral_common"): + return data + + # NOTE: mistral-common tokenizer is not compatible with editing tokenizer at the moment + + if data.get("added_tokens_overrides"): + raise ValueError( + "added_tokens_overrides is not supported with mistral-common tokenizer" + ) + + if data.get("special_tokens"): + raise ValueError( + "special_tokens override is not supported with mistral-common tokenizer" + ) + + if data.get("tokens"): + raise ValueError( + "tokens override is not supported with mistral-common tokenizer" + ) + + if data.get("chat_template"): + raise ValueError( + "Setting chat_template is not supported with mistral-common tokenizer" + ) + + return data + + +class LoRAValidationMixin: + """Validation methods related to LoRA/QLoRA configuration.""" + + @model_validator(mode="before") + @classmethod + def check_lr_groups(cls, data): + if data.get("lr_groups") and data.get("loraplus_lr_ratio"): + raise ValueError("lr_groups and loraplus_lr_ratio cannot be used together.") + return data + + @model_validator(mode="before") + @classmethod + def check_frozen(cls, data): + if ( + data.get("adapter") + and data.get("peft_layers_to_transform") + and data.get("unfrozen_parameters") + ): + raise ValueError( + "`unfrozen_parameters` used with `peft_layers_to_transform` can have unexpected behavior." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_peft_layers_pattern(cls, data): + if data.get("peft_layers_pattern") and not data.get("peft_layers_to_transform"): + raise ValueError( + "peft_layers_pattern requires peft_layers_to_transform to be set" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_qlora_unsloth(cls, data): + if ( + data.get("unsloth_lora_mlp") + or data.get("unsloth_lora_qkv") + or data.get("unsloth_lora_o") + ): + if data.get("adapter") == "lora" and data.get("load_in_8bit"): + raise ValueError( + "unsloth_lora_mlp, unsloth_lora_qkv, and unsloth_lora_o are not compatible with 8-bit LoRA" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_lora_8bit(cls, data): + if ( + data.get("lora_mlp_kernel") + or data.get("lora_qkv_kernel") + or data.get("lora_o_kernel") + ): + if data.get("adapter") == "lora" and data.get("load_in_8bit"): + raise ValueError( + "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with 8-bit LoRA" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_lora_axolotl_unsloth(cls, data): + is_lora_kernel = any( + data.get(k) for k in ["lora_mlp_kernel", "lora_qkv_kernel", "lora_o_kernel"] + ) + is_unsloth_lora = any( + data.get(k) + for k in ["unsloth_lora_mlp", "unsloth_lora_qkv", "unsloth_lora_o"] + ) + if is_lora_kernel and is_unsloth_lora: + raise ValueError( + "both lora_mlp_kernel and unsloth_lora_mlp cannot be true (similarly for lora_qkv_kernel, lora_o_kernel)" + ) + return data + + @model_validator(mode="after") + def check_fused_lora(self): + if self.adapter in ["lora", "qlora"] and ( + self.flash_attn_fuse_qkv or self.flash_attn_fuse_mlp + ): + raise ValueError("Fused modules are not supported with LoRA/QLoRA") + return self + + @model_validator(mode="after") + def hint_lora_8bit(self): + loftq = ( + self.peft and self.peft.loftq_config and self.peft.loftq_config.loftq_bits + ) + if not self.load_in_8bit and self.adapter == "lora" and not loftq: + LOG.warning("We recommend setting `load_in_8bit: true` for LORA finetuning") + return self + + @model_validator(mode="before") + @classmethod + def warn_qlora_zero3_w_use_reentrant(cls, data): + if ( + data.get("adapter") == "qlora" + and data.get("gradient_checkpointing_kwargs", {}) + and data.get("gradient_checkpointing_kwargs", {}).get("use_reentrant") + is False + and data.get("deepspeed", "") is not None + and "zero3" in data.get("deepspeed", "") + ): + # may result in: + # torch.utils.checkpoint.CheckpointError: torch.utils.checkpoint: + # Recomputed values for the following tensors have different metadata + # than during the forward pass. + LOG.warning( + "qlora + zero3 with use_reentrant: false may result in a CheckpointError about recomputed values" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_lora_kernel_8bit(cls, data): + if ( + data.get("lora_mlp_kernel") + or data.get("lora_qkv_kernel") + or data.get("lora_o_kernel") + ): + if data.get("adapter") == "lora" and data.get("load_in_8bit"): + raise ValueError( + "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with 8-bit LoRA" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_lora_kernel_rl(cls, data): + if ( + data.get("lora_mlp_kernel") + or data.get("lora_qkv_kernel") + or data.get("lora_o_kernel") + ) and data.get("rl"): + raise ValueError( + "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with RL at the moment." + ) + return data + + +class RLValidationMixin: + """Validation methods related to RL training configuration.""" + + @model_validator(mode="before") + @classmethod + def check_sample_packing_w_rl(cls, data): + if data.get("sample_packing") and data.get("rl"): + raise ValueError("`sample_packing: true` does not work with RLHF training") + return data + + @model_validator(mode="before") + @classmethod + def check_kto_config(cls, data): + if data.get("rl") == "kto": + if data.get("sample_packing") or data.get("eval_sample_packing"): + raise ValueError("sample_packing is not supported with kto") + + if data.get("remove_unused_columns") is not False: + raise ValueError("Set `remove_unused_columns: False` when using kto") + return data + + @model_validator(mode="before") + @classmethod + def check_grpo_liger_sequence_parallel(cls, data): + if ( + data.get("rl") == "grpo" + and data.get("trl", {}) + and data.get("trl").get("use_liger_loss") + and data.get("sequence_parallel_degree", 1) > 1 + ): + raise ValueError("GRPO + SP + Liger not currently supported") + return data + + @model_validator(mode="before") + @classmethod + def check_rl_config_gradient_checkpointing(cls, data): + # TODO: SalmanMohammadi + # Distributed RL with QLoRA + gradient checkpointing + # and use_reentrant = True is broken upstream in TRL + # pylint: disable=too-many-boolean-expressions + if ( + data.get("rl") + and data.get("gradient_checkpointing") + and data.get("gradient_checkpointing_kwargs") + and data.get("gradient_checkpointing_kwargs").get("use_reentrant") + and data.get("load_in_4bit") + and data.get("adapter") == "qlora" + and data.get("capabilities") + and data.get("capabilities").get("n_gpu", 1) > 1 + ): + raise ValueError( + "The `use_reentrant: True` implementation of gradient checkpointing " + "is not supported for distributed RL training with QLoRA. Please set " + "`use_reentrant: False` in `gradient_checkpointing_kwargs`." + ) + return data + + +class OptimizationValidationMixin: + """Validation methods related to optimization and performance.""" + + @model_validator(mode="after") + def check_adamw_optimizer_params(self): + if any([self.adam_beta1, self.adam_beta2, self.adam_epsilon]) and ( + not self.optimizer or "adamw" not in str(self.optimizer).lower() + ): + LOG.warning("adamw hyperparameters found, but no adamw optimizer set") + return self + + @model_validator(mode="before") + @classmethod + def check_muon_deepspeed_fsdp(cls, data): + if data.get("optimizer") == "muon" and ( + data.get("deepspeed") or data.get("fsdp") or data.get("fsdp_config") + ): + raise ValueError( + "Muon optimizer is currently incompatible with DeepSpeed and FSDP" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_batch_flattening_fa(cls, data): + if data.get("batch_flattening"): + batch_flattening_auto = data.get("batch_flattening") == "auto" + if not data.get("flash_attention") and not batch_flattening_auto: + raise ValueError("batch_flattening requires flash attention") + if data.get("sample_packing") and not batch_flattening_auto: + raise ValueError("batch_flattening not compatible with sample_packing") + if data.get("micro_batch_size") == 1 and not batch_flattening_auto: + LOG.warning("batch_flattening has no effect with micro_batch_size == 1") + + if ( + batch_flattening_auto + and data.get("flash_attention") + and not data.get("sample_packing") + and data.get("micro_batch_size") > 1 + ): + data["batch_flattening"] = True + elif batch_flattening_auto: + data["batch_flattening"] = False + + return data + + @model_validator(mode="before") + @classmethod + def check_torch_compile_deepspeed(cls, data): + if data.get("deepspeed") and data.get("torch_compile"): + raise ValueError( + "torch_compile should be set within your deepspeed config file" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_xentropy_patch_conflicts(cls, data): + if data.get("flash_attn_cross_entropy") and data.get( + "unsloth_cross_entropy_loss" + ): + raise ValueError( + "flash_attn_cross_entropy and unsloth_cross_entropy_loss cannot be both enabled" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_fsdp_offload_w_8bit_optimizer(cls, data): + if ( + data.get("fsdp") + and "8bit" in data.get("optimizer", "") + and data.get("fsdp_config") + and data["fsdp_config"].get("fsdp_offload_params") + and str(data["fsdp_config"].get("fsdp_version")) != "2" + ): + raise ValueError( + f"FSDP Offload not compatible with {data.get('optimizer')}" + ) + if ( + data.get("fsdp") + and "8bit" in data.get("optimizer", "") + and data.get("fsdp_config") + and str(data["fsdp_config"].get("fsdp_version")) == "2" + ): + if data.get("optimizer", "") in ["adamw_8bit", "adamw_bnb_8bit"]: + # CUDA ops errors with bnb 8bit optimizer + FSDP2 + raise ValueError( + f"FSDP2 not compatible with {data.get('optimizer')}, use `adamw_torch_8bit` instead" + ) + + return data + + @model_validator(mode="before") + @classmethod + def check_fsdp_sharded_state_dict_w_safetensors(cls, data): + if ( + data.get("fsdp") + and data.get("save_safetensors") + and data.get("fsdp_config") + and data["fsdp_config"].get("fsdp_state_dict_type") == "SHARDED_STATE_DICT" + ): + raise ValueError( + "FSDP SHARDED_STATE_DICT not compatible with save_safetensors" + ) + return data + + +class SystemValidationMixin: + """Validation methods related to system and hardware configuration.""" + + @model_validator(mode="before") + @classmethod + def check_mem_mismatch(cls, data): + if ( + data.get("max_memory") is not None + and data.get("gpu_memory_limit") is not None + ): + raise ValueError( + "max_memory and gpu_memory_limit are mutually exclusive and cannot be used together." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_fsdp_deepspeed(cls, data): + if data.get("deepspeed") and data.get("fsdp"): + raise ValueError("deepspeed and fsdp cannot be used together.") + return data + + @model_validator(mode="before") + @classmethod + def check_npu_config(cls, data): + if is_torch_npu_available(): + # check attention config + attn_list = ["flash_attention", "sdp_attention", "s2_attention"] + for attn in attn_list: + if data.get(attn): + raise NotImplementedError( + f"{attn} is currently not supported in Ascend npu, please disable this configuration." + ) + + # check quant config + if data.get("optimizer") is not None and "bit" in data.get("optimizer"): + optimizer = data.get("optimizer") + raise NotImplementedError( + f"{optimizer} is currently not supported in Ascend npu, choose another one please." + ) + + quant_list = ["load_in_8bit", "load_in_4bit"] + for quant in quant_list: + if data.get(quant): + raise NotImplementedError( + f"Quantification is currently not supported in Ascend npu, please disable {quant}." + ) + + # check dtype config + if data.get("tf32"): + raise NotImplementedError( + "tf32 dtype is currently not supported in Ascend npu, please disable this configuration" + ) + + return data + + +class ChatTemplateValidationMixin: + """Validation methods related to chat template configuration.""" + + @model_validator(mode="before") + @classmethod + def check_chat_template_config(cls, data): + # if chat_template is set to jinja, chat_template_jinja is required + if data.get("chat_template") == ChatTemplate.jinja and not data.get( + "chat_template_jinja" + ): + raise ValueError( + "chat_template_jinja is required when chat_template is set to jinja" + ) + + # If chat_template_jinja is set, set chat_template to jinja + if data.get("chat_template_jinja") and not data.get("chat_template"): + data["chat_template"] = ChatTemplate.jinja + + return data + + +class PretrainingValidationMixin: + """Validation methods related to pretraining configuration.""" + + @model_validator(mode="before") + @classmethod + def check_pretraining_w_max_steps(cls, data): + if data.get("pretraining_dataset") and not data.get("max_steps"): + raise ValueError( + "max_steps must be set when using iterable pretraining_dataset, Trainer can't infer length and schedule optimizer/learning rate without it!" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_pretraining_w_group_by_length(cls, data): + if data.get("pretraining_dataset") and data.get("group_by_length"): + LOG.warning( + "You probably want to disable group_by_length as it will force a streamed dataset to download completely." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_pretraining_split_batches_accelerate(cls, data): + # alternatively set ACCELERATE_SPLIT_BATCHES=False + if data.get("pretraining_dataset"): + accelerator_config = data.get("accelerator_config", {}) + if not accelerator_config: + data["accelerator_config"] = { + "split_batches": False, + "dispatch_batches": False, + } + else: + if accelerator_config.get("split_batches") is None: + data["accelerator_config"]["split_batches"] = False + if accelerator_config.get("dispatch_batches") is None: + data["accelerator_config"]["dispatch_batches"] = False + return data + + +class ModelCompatibilityValidationMixin: + """Validation methods for specific model compatibility.""" + + @model_validator(mode="after") + def check_falcon_fsdp(self): + if (self.base_model and "falcon" in self.base_model.lower()) and self.fsdp: + raise ValueError("FSDP is not supported for falcon models") + return self + + @model_validator(mode="after") + def check_mpt_checkpointing(self): + if ( + self.base_model and "mpt" in self.base_model.lower() + ) and self.gradient_checkpointing: + raise ValueError("gradient_checkpointing is not supported for MPT models") + return self + + @model_validator(mode="after") + def check_offload_grad_checkpointing(self): + if self.gradient_checkpointing and self.gradient_checkpointing == "unsloth": + LOG.warning( + "`unsloth` is deprecated for gradient_checkpointing, use `offload`" + ) + self.gradient_checkpointing = "offload" + return self + + @model_validator(mode="after") + def check_better_transformers(self): + if self.flash_optimum is True: + if self.adapter: + LOG.warning( + "BetterTransformers probably doesn't work with PEFT adapters" + ) + if self.fp16 or self.bf16: + raise ValueError("AMP is not supported with BetterTransformer") + if self.float16 is not True and self.bfloat16 is not True: + LOG.warning( + "You should probably set bfloat16 or float16 to true to " + "load the model in float16 for BetterTransformers" + ) + return self + + @model_validator(mode="before") + @classmethod + def check_gptq_w_revision(cls, data): + if data.get("gptq") and data.get("revision_of_model"): + raise ValueError( + "revision_of_model is not supported for GPTQ models. " + + "Please download the model from HuggingFace Hub manually for correct branch, " + + "point to its path, and remove revision_of_model from the config." + ) + return data + + +class ComplexValidationMixin: + """Complex validation methods that involve multiple systems.""" + + @field_validator("neftune_noise_alpha") + @classmethod + def validate_neftune_noise_alpha(cls, neftune_noise_alpha): + if neftune_noise_alpha is not None and neftune_noise_alpha <= 0.0: + raise ValueError("neftune_noise_alpha must be > 0.0") + return neftune_noise_alpha + + @model_validator(mode="after") + def check_rl_beta(self): + if self.dpo_beta and not self.rl_beta: + self.rl_beta = self.dpo_beta + del self.dpo_beta + return self + + @model_validator(mode="after") + def check_simpo_warmup(self): + if self.rl is RLType.SIMPO and self.warmup_ratio: + raise ValueError( + "warmup_ratio is not supported with the simpo trainer. Please use `warmup_steps` instead" + ) + return self + + @model_validator(mode="after") + def check_relora(self): + if self.relora_steps: + if self.adapter not in ("lora", "qlora"): + raise ValueError("cfg.adapter must be lora or qlora to use ReLoRA") + + if self.fsdp: + raise ValueError("fsdp not supported with ReLoRA") + + if self.deepspeed: + raise ValueError("deepspeed not supported with ReLoRA") + + if self.lr_scheduler == "one_cycle": + raise ValueError( + "ReLoRA is not compatible with the one_cycle scheduler" + ) + + if self.flash_attn_fuse_qkv or self.flash_attn_fuse_mlp: + raise ValueError("Fused modules are not supported with ReLoRA") + return self + + @model_validator(mode="after") + def check_early_stopping(self): + if self.early_stopping_patience: + if not self.save_steps or not self.eval_steps: + raise ValueError( + "`early_stopping_patience` requires save_steps and eval_steps to be set. eval_steps should evenly divide save_steps." + ) + if self.save_steps % self.eval_steps != 0: + raise ValueError( + "`early_stopping_patience` requires that eval_steps should evenly divide save_steps." + ) + return self + + @model_validator(mode="after") + def check_sequence_parallel_degree(self): + if not self.sequence_parallel_degree: + self.sequence_parallel_degree = 1 + elif self.sequence_parallel_degree > 1: + if not self.flash_attention: + raise ValueError( + "flash_attention: true must be set with sequence_parallel_degree > 1" + ) + + if self.sample_packing and self.micro_batch_size > 1: + raise ValueError( + "micro_batch_size must be set to 1 when sample_packing is enabled " + "due to a `ring-flash-attn` requirement" + ) + + try: + import ring_flash_attn # noqa: F401 # pylint:disable=unused-import + except ImportError as exception: + raise ImportError( + "sequence_parallel_degree > 1 but ring_flash_attn is not installed. " + "Please install it with `pip install axolotl[ring-flash-attn] " + "or `pip install ring-flash-attn>=0.1.4`." + ) from exception + + LOG.warning( + "Sequence parallelism (SP) is enabled with " + f"sequence_parallel_degree={self.sequence_parallel_degree}. " + "Please note that logged losses may differ slightly to the non-SP " + "losses due to transformers Trainer implementation details. " + "Please see https://github.com/axolotl-ai-cloud/axolotl/pull/2495#issuecomment-2784022042 " + "for more details." + ) + + return self + + @model_validator(mode="after") + def validate_ring_attn_func(self): + if getattr(self, "sequence_parallel_degree", 1) == 1: + return self + + if self.ring_attn_func is not None: + self.ring_attn_func = RingAttnFunc(self.ring_attn_func) + else: + # Default ring attention function selection + sample_packing = getattr(self, "sample_packing", False) + self.ring_attn_func = ( + RingAttnFunc.VARLEN_LLAMA3 + if sample_packing + else RingAttnFunc.BATCH_RING + ) + + return self + + +# pylint: disable=too-many-ancestors +class ValidationMixin( + DatasetValidationMixin, + AttentionValidationMixin, + TrainingValidationMixin, + LoRAValidationMixin, + RLValidationMixin, + OptimizationValidationMixin, + SystemValidationMixin, + ChatTemplateValidationMixin, + PretrainingValidationMixin, + ModelCompatibilityValidationMixin, + ComplexValidationMixin, +): + """Full validation mixin for Axolotl configuration.""" From 06a648263ba1c49f5ecc604a2a82d9f6a1348258 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 18 Jun 2025 15:42:54 -0400 Subject: [PATCH 0725/1405] Config doc autogen: follow-up fix docs build (#2806) * config reference doc autogen * improvements * cleanup; still ugly but working * reformat * remove autogen config ref from git * factor out validations * rewrite * rewrite * cleanup * progress * progress * progress * lint and minifying somewhat * remove unneeded * coderabbit * coderabbit * update preview-docs workflow triggers * installing with deps * coderabbit * update refs * overwrote file accidentally * docs install deps --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2d3c209ccc..5b5cc54891 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python3 -m pip install jupyter quartodoc - python3 -m pip install -e . --no-deps + python3 -m pip install -e . - name: Build autodoc run: quartodoc build - name: Publish to GitHub Pages (and render) From a85efffbef716e20f7b51faac7a84d379d8f7474 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 18 Jun 2025 15:46:14 -0400 Subject: [PATCH 0726/1405] bump transformers==4.52.4 (#2800) [skip ci] * bump transformers==4.52.4 * don't use hf offline for qwen tokenizer * increase timeout * don't use methodtype * increase timeout * better assertion logging * upgrade deepspeed version too --- cicd/e2e_tests.py | 2 +- cicd/multigpu.py | 2 +- requirements.txt | 2 +- setup.py | 2 +- src/axolotl/integrations/kd/kernels/models.py | 3 +-- tests/e2e/multigpu/patched/test_sp.py | 5 +++- tests/e2e/multigpu/solo/test_flex.py | 2 +- tests/e2e/multigpu/test_gemma3.py | 2 +- tests/e2e/multigpu/test_llama.py | 24 +++++++++---------- tests/e2e/multigpu/test_ray.py | 4 ++-- tests/e2e/patched/test_fa_xentropy.py | 2 +- tests/e2e/patched/test_unsloth_qlora.py | 6 ++--- tests/e2e/solo/test_flex.py | 2 +- tests/e2e/test_llama_pretrain.py | 2 +- tests/e2e/test_packing_loss.py | 2 +- tests/e2e/test_qat.py | 2 +- tests/e2e/test_reward_model_smollm2.py | 2 +- .../test_chat_templates_thinking.py | 3 --- 18 files changed, 34 insertions(+), 35 deletions(-) diff --git a/cicd/e2e_tests.py b/cicd/e2e_tests.py index ce9c605c70..5d2b6fed17 100644 --- a/cicd/e2e_tests.py +++ b/cicd/e2e_tests.py @@ -6,7 +6,7 @@ @app.function( image=cicd_image, gpu=GPU_CONFIG, - timeout=90 * 60, # 90 min + timeout=120 * 60, # 90 min cpu=8.0, memory=131072, volumes=VOLUME_CONFIG, diff --git a/cicd/multigpu.py b/cicd/multigpu.py index a2dd8d0b32..848110a841 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -69,7 +69,7 @@ def run_cmd(cmd: str, run_folder: str): @app.function( image=cicd_image, gpu=GPU_CONFIG, - timeout=90 * 60, + timeout=120 * 60, cpu=16.0, memory=131072 * N_GPUS, volumes=VOLUME_CONFIG, diff --git a/requirements.txt b/requirements.txt index cf8caba002..8bd77ab5e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ packaging==23.2 huggingface_hub==0.32.2 peft==0.15.2 -transformers==4.52.3 +transformers==4.52.4 tokenizers>=0.21.1 accelerate==1.7.0 datasets==3.6.0 diff --git a/setup.py b/setup.py index 28f71f789c..08c39c71c8 100644 --- a/setup.py +++ b/setup.py @@ -118,7 +118,7 @@ def get_package_version(): "yunchang==0.6.0", ], "deepspeed": [ - "deepspeed==0.17.0", + "deepspeed==0.17.1", "deepspeed-kernels", ], "mamba-ssm": [ diff --git a/src/axolotl/integrations/kd/kernels/models.py b/src/axolotl/integrations/kd/kernels/models.py index bfd7529641..5a7c286bc4 100644 --- a/src/axolotl/integrations/kd/kernels/models.py +++ b/src/axolotl/integrations/kd/kernels/models.py @@ -2,7 +2,6 @@ model patcher for chunked top-k kl-div """ -from types import MethodType from typing import Optional, Union, Unpack import torch @@ -95,4 +94,4 @@ def apply_kernel(model_type): model_cls_prefix = "".join([part.capitalize() for part in model_type.split("_")]) module = __import__(module_path, fromlist=[f"{model_cls_prefix}ForCausalLM"]) model_cls = getattr(module, f"{model_cls_prefix}ForCausalLM") - model_cls.forward = MethodType(kldiv_forward_llama_like, model_cls) + model_cls.forward = kldiv_forward_llama_like diff --git a/tests/e2e/multigpu/patched/test_sp.py b/tests/e2e/multigpu/patched/test_sp.py index e90def2b78..8883e0135d 100644 --- a/tests/e2e/multigpu/patched/test_sp.py +++ b/tests/e2e/multigpu/patched/test_sp.py @@ -91,7 +91,10 @@ def _run_sequence_parallel_test( ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", threshold, "Train Loss is too high" + temp_dir + "/runs", + "train/train_loss", + threshold, + "Train Loss (%s) is too high", ) @pytest.mark.parametrize( diff --git a/tests/e2e/multigpu/solo/test_flex.py b/tests/e2e/multigpu/solo/test_flex.py index 42c3c00c8e..c8f14330d3 100644 --- a/tests/e2e/multigpu/solo/test_flex.py +++ b/tests/e2e/multigpu/solo/test_flex.py @@ -85,5 +85,5 @@ def test_loss_llama(self, temp_dir): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss (%s) is too high" ) diff --git a/tests/e2e/multigpu/test_gemma3.py b/tests/e2e/multigpu/test_gemma3.py index 9bff25f408..b4cb6e59de 100644 --- a/tests/e2e/multigpu/test_gemma3.py +++ b/tests/e2e/multigpu/test_gemma3.py @@ -91,5 +91,5 @@ def test_lora_ddp_packed(self, temp_dir): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 1.8, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 1.8, "Train Loss (%s) is too high" ) diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 9c4bf50543..a8ed6bda07 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -89,7 +89,7 @@ def test_lora_ddp(self, temp_dir): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" ) @pytest.mark.parametrize( @@ -154,7 +154,7 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" ) def test_dpo_lora_ddp(self, temp_dir): @@ -232,7 +232,7 @@ def test_dpo_lora_ddp(self, temp_dir): temp_dir + "/runs", "train/train_loss", loss_threshold, - "Train Loss is too high", + "Train Loss (%s) is too high", ) def test_dpo_qlora_ddp(self, temp_dir): @@ -310,7 +310,7 @@ def test_dpo_qlora_ddp(self, temp_dir): temp_dir + "/runs", "train/train_loss", loss_threshold, - "Train Loss is too high", + "Train Loss (%s) is too high", ) @pytest.mark.parametrize( @@ -380,7 +380,7 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" ) @pytest.mark.parametrize( @@ -452,7 +452,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" ) @require_torch_2_6_0 @@ -533,7 +533,7 @@ def test_fsdp2_packed( ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.1, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.1, "Train Loss (%s) is too high" ) def test_fsdp_qlora_prequant_packed(self, temp_dir): @@ -613,7 +613,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" ) @pytest.mark.parametrize( @@ -697,7 +697,7 @@ def test_ds_zero3_packed( ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.4, "Train Loss (%s) is too high" ) @pytest.mark.parametrize( @@ -771,7 +771,7 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" ) @pytest.mark.parametrize( @@ -845,7 +845,7 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" ) @pytest.mark.skip( @@ -912,5 +912,5 @@ def test_fix_untrained_tokens(self, temp_dir): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 4.0, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 4.0, "Train Loss (%s) is too high" ) diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index f2c812eb56..22023507a5 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -75,7 +75,7 @@ def test_lora_ddp(self, temp_dir): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" ) @require_torch_lt_2_6_0 @@ -133,5 +133,5 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" ) diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index 4e3cbc50d3..ca8b21178f 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -78,5 +78,5 @@ def test_lora_packing_fa_cross_entropy(self, temp_dir, gradient_accumulation_ste check_model_output_exists(temp_dir, cfg) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 1.5, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 1.5, "Train Loss (%s) is too high" ) diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index 9567c0b180..69171481c7 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -73,7 +73,7 @@ def test_unsloth_llama_qlora_fa2(self, temp_dir, sample_packing): check_model_output_exists(temp_dir, cfg) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss (%s) is too high" ) def test_unsloth_llama_qlora_unpacked(self, temp_dir): @@ -123,7 +123,7 @@ def test_unsloth_llama_qlora_unpacked(self, temp_dir): check_model_output_exists(temp_dir, cfg) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss (%s) is too high" ) @pytest.mark.parametrize( @@ -178,5 +178,5 @@ def test_unsloth_llama_qlora_unpacked_no_fa2_fp16(self, temp_dir, sdp_attention) check_model_output_exists(temp_dir, cfg) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss (%s) is too high" ) diff --git a/tests/e2e/solo/test_flex.py b/tests/e2e/solo/test_flex.py index 8d1a0c7d1e..f6b8c6283b 100644 --- a/tests/e2e/solo/test_flex.py +++ b/tests/e2e/solo/test_flex.py @@ -63,5 +63,5 @@ def test_loss_llama(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss (%s) is too high" ) diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index 6944c6f5ef..fdebf2173b 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -69,5 +69,5 @@ def test_pretrain(self, temp_dir, sample_packing, pretrain_multipack_attn): temp_dir + "/runs", "train/train_loss", loss_threshold, - "Train Loss is too high", + "Train Loss (%s) is too high", ) diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index 463f7c8383..cc2db72e0e 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -62,5 +62,5 @@ def test_loss_packed(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss (%s) is too high" ) diff --git a/tests/e2e/test_qat.py b/tests/e2e/test_qat.py index 964bf3c1ca..ef726079d0 100644 --- a/tests/e2e/test_qat.py +++ b/tests/e2e/test_qat.py @@ -129,5 +129,5 @@ def test_qat_dpo(self, temp_dir): temp_dir + "/runs", "train/train_loss", loss_threshold, - "Train Loss is too high", + "Train Loss (%s) is too high", ) diff --git a/tests/e2e/test_reward_model_smollm2.py b/tests/e2e/test_reward_model_smollm2.py index 304fda1cc4..5d52bcc865 100644 --- a/tests/e2e/test_reward_model_smollm2.py +++ b/tests/e2e/test_reward_model_smollm2.py @@ -66,6 +66,6 @@ def test_rm_lora(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.5, "Train Loss is too high" + temp_dir + "/runs", "train/train_loss", 2.5, "Train Loss (%s) is too high" ) check_model_output_exists(temp_dir, cfg) diff --git a/tests/prompt_strategies/test_chat_templates_thinking.py b/tests/prompt_strategies/test_chat_templates_thinking.py index 79429b7314..e807111aa5 100644 --- a/tests/prompt_strategies/test_chat_templates_thinking.py +++ b/tests/prompt_strategies/test_chat_templates_thinking.py @@ -11,8 +11,6 @@ ) from axolotl.utils.dict import DictDefault -from tests.hf_offline_utils import enable_hf_offline - @pytest.fixture(name="messages_w_reasoning") def messages_w_reasoning_fixture(): @@ -59,7 +57,6 @@ def messages_w_reasoning_fixture(): @pytest.fixture(name="qwen3_tokenizer") -@enable_hf_offline def qwen3_tokenizer_fixture( download_qwen3_half_billion_model, ): # pylint: disable=unused-argument From 0bb90775539a5fc8b45c8e665f4ac213760c023d Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 19 Jun 2025 02:46:27 +0700 Subject: [PATCH 0727/1405] Fix: logging on py310 (#2802) * feat: encourage py311 * fix: logging import on py310 * fix: do upper and simplify handling --- docs/installation.qmd | 4 ++-- src/axolotl/logging_config.py | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/installation.qmd b/docs/installation.qmd index 15f2db57b8..c905e93cda 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -14,7 +14,7 @@ This guide covers all the ways you can install and set up Axolotl for your envir ## Requirements {#sec-requirements} - NVIDIA GPU (Ampere architecture or newer for `bf16` and Flash Attention) or AMD GPU -- Python ≥3.10 +- Python ≥3.11 - PyTorch ≥2.5.1 ## Installation Methods {#sec-installation-methods} @@ -153,7 +153,7 @@ We recommend using WSL2 (Windows Subsystem for Linux) or Docker. ### Conda/Pip venv {#sec-conda} -1. Install Python ≥3.10 +1. Install Python ≥3.11 2. Install PyTorch: https://pytorch.org/get-started/locally/ 3. Install Axolotl: ```{.bash} diff --git a/src/axolotl/logging_config.py b/src/axolotl/logging_config.py index b8dc6479d9..f570e013c1 100644 --- a/src/axolotl/logging_config.py +++ b/src/axolotl/logging_config.py @@ -25,12 +25,20 @@ class AxolotlOrWarnErrorFilter(logging.Filter): def __init__(self, **kwargs): super().__init__(**kwargs) - self.axolotl_level = logging.getLevelNamesMapping()[ - os.getenv("AXOLOTL_LOG_LEVEL", DEFAULT_AXOLOTL_LOG_LEVEL) - ] - self.other_level = logging.getLevelNamesMapping()[ - os.getenv("LOG_LEVEL", DEFAULT_LOG_LEVEL) - ] + axolotl_log_level = os.getenv( + "AXOLOTL_LOG_LEVEL", DEFAULT_AXOLOTL_LOG_LEVEL + ).upper() + other_log_level = os.getenv("LOG_LEVEL", DEFAULT_LOG_LEVEL).upper() + + try: + # py311+ only + level_mapping = logging.getLevelNamesMapping() + self.axolotl_level = level_mapping[axolotl_log_level] + self.other_level = level_mapping[other_log_level] + except AttributeError: + # For py310, use getLevelName directly + self.axolotl_level = logging.getLevelName(axolotl_log_level) + self.other_level = logging.getLevelName(other_log_level) def filter(self, record: LogRecord) -> bool: # General filter From 34da391391225ad300b2631a201c43e544b21550 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 18 Jun 2025 15:49:05 -0400 Subject: [PATCH 0728/1405] Set dev version (#2807) [skip ci] --- src/axolotl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index 8f2b67fc3b..314d22279f 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -4,4 +4,4 @@ __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.10.0" +__version__ = "0.11.0.dev" From eb3a57eb17053da3b4437566579d12dbc580e7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carsten=20Kragelund=20J=C3=B8rgensen?= Date: Wed, 18 Jun 2025 21:59:07 +0200 Subject: [PATCH 0729/1405] Ignore generation/endgeneration tags when analyzing Jinja chat template (#2787) * ignore generation/endgeneration tags Axolotl handles calculating the mask for assistant turns on its own, and as such these tags are not needed, however currently the analyzer does not recognize them at all and throws an error. * feat: add phi4 tokenizer test and unblock gemma2 * fix: improve template * chore: refactor * chore: lint --------- Co-authored-by: NanoCode012 Co-authored-by: Wing Lian --- .../prompt_strategies/chat_template.py | 6 +- .../jinja_template_analyzer.py | 17 ++++- src/axolotl/utils/chat_templates.py | 1 + tests/prompt_strategies/conftest.py | 6 ++ .../test_chat_templates_advanced.py | 65 +++++++------------ 5 files changed, 46 insertions(+), 49 deletions(-) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 4a358928e9..0271fca246 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -596,11 +596,7 @@ def find_turn( if ( turn_idx == 0 and turns[0].get("role") == "system" - and ( - "mistral" in self.tokenizer.name_or_path.lower() - or "gemma" - in self.tokenizer.name_or_path.lower() # gemma3 uses gemma tokenizer - ) + and ("mistral" in self.tokenizer.name_or_path.lower()) ): return -1, -1 diff --git a/src/axolotl/prompt_strategies/jinja_template_analyzer.py b/src/axolotl/prompt_strategies/jinja_template_analyzer.py index a5f89cfe54..e16a1e22b9 100644 --- a/src/axolotl/prompt_strategies/jinja_template_analyzer.py +++ b/src/axolotl/prompt_strategies/jinja_template_analyzer.py @@ -3,6 +3,7 @@ from typing import Dict, Optional, Set, TypedDict, Union from jinja2 import Environment, meta, nodes +from jinja2.ext import Extension class JinjaTemplateAnalysis(TypedDict): @@ -27,6 +28,18 @@ class JinjaTemplateAnalysis(TypedDict): iteration_target: Optional[Union[str, list[str]]] +class GenerationTagIgnore(Extension): + """ + Ignores the generation and endgeneration tags in Jinja templates. + """ + + tags = {"generation", "endgeneration"} + + def parse(self, parser): + parser.stream.skip(1) + return nodes.Const("") + + class JinjaTemplateAnalyzer: """ Analyzes Jinja templates to extract information about variable usage, @@ -57,7 +70,9 @@ class JinjaTemplateAnalyzer: """ def __init__(self, template: str): - self.env: Environment = Environment(autoescape=True) + self.env: Environment = Environment( + autoescape=True, extensions=[GenerationTagIgnore] + ) self.property_access: Dict[str, Set[str]] = {} self.iteration_targets: Dict[str, Union[str, list[str]]] = {} self.index_access: Dict[str, Set[Union[int, float]]] = {} diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 09bfb55763..83a42945b0 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -32,6 +32,7 @@ "llava": "{% for message in messages %}{% if message['role'] != 'system' %}{{ message['role'].upper() + ': '}}{% endif %}{# Render all images first #}{% for content in message['content'] | selectattr('type', 'equalto', 'image') %}{{ '\n' }}{% endfor %}{# Render all text next #}{% if message['role'] != 'assistant' %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{{ content['text'] + ' '}}{% endfor %}{% else %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{% generation %}{{ content['text'] + ' '}}{% endgeneration %}{% endfor %}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ 'ASSISTANT:' }}{% endif %}", "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", "phi_35": "{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'user' %}{{'<|user|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|>\n' + message['content'] + '<|end|>\n'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% endif %}", + "phi_4": "{% set system_message = 'You are Phi, a language model trained by Microsoft to help users. Your role as an assistant involves thoroughly exploring questions through a systematic thinking process before providing the final precise and accurate solutions. This requires engaging in a comprehensive cycle of analysis, summarizing, exploration, reassessment, reflection, backtracing, and iteration to develop well-considered thinking process. Please structure your response into two main sections: Thought and Solution using the specified format: {Thought section} {Solution section}. In the Thought section, detail your reasoning process in steps. Each step should include detailed considerations such as analysing questions, summarizing relevant findings, brainstorming new ideas, verifying the accuracy of the current steps, refining any errors, and revisiting previous steps. In the Solution section, based on various attempts, explorations, and reflections from the Thought section, systematically present the final solution that you deem correct. The Solution section should be logical, accurate, and concise and detail necessary steps needed to reach the conclusion. Now, try to solve the following question through the above guidelines:' -%}{%- if messages and messages[0]['role'] == 'system' -%}{%- set system_message = messages[0]['content'] -%}{%- set messages = messages[1:] -%}{%- endif -%}<|im_start|>system<|im_sep|>{{ system_message }}<|im_end|>{% for message in messages %}{% if (message['role'] == 'user') %}{{'<|im_start|>user<|im_sep|>' + message['content'] + '<|im_end|>'}}{% elif (message['role'] == 'assistant') %}{{'<|im_start|>assistant<|im_sep|>'}}{% generation %}{{message['content'] + '<|im_end|>'}}{% endgeneration %}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant<|im_sep|>' }}{% endif %}", "deepseek_v2": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %}", "deepseek_v3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='', is_first_sp=true) %}{%- for message in messages %}{%- if message['role'] == 'system' %}{%- if ns.is_first_sp %}{% set ns.system_prompt = ns.system_prompt + message['content'] %}{% set ns.is_first_sp = false %}{%- else %}{% set ns.system_prompt = ns.system_prompt + '\\n\\n' + message['content'] %}{%- endif %}{%- endif %}{%- endfor %}{{ bos_token }}{{ ns.system_prompt }}{%- for message in messages %}{%- if message['role'] == 'user' %}{%- set ns.is_tool = false -%}{{'<|User|>' + message['content']}}{%- endif %}{%- if message['role'] == 'assistant' and 'tool_calls' in message %}{%- set ns.is_tool = false -%}{%- for tool in message['tool_calls'] %}{%- if not ns.is_first %}{%- if message['content'] is none %}{{'<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- else %}{{'<|Assistant|>' + message['content'] + '<|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- endif %}{%- set ns.is_first = true -%}{%- else %}{{'\\n' + '<|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- endif %}{%- endfor %}{{'<|tool▁calls▁end|><|end▁of▁sentence|>'}}{%- endif %}{%- if message['role'] == 'assistant' and 'tool_calls' not in message %}{%- if ns.is_tool %}{{'<|tool▁outputs▁end|>' + message['content'] + '<|end▁of▁sentence|>'}}{%- set ns.is_tool = false -%}{%- else %}{% set content = message['content'] %}{% if '' in content %}{% set content = content.split('')[-1] %}{% endif %}{{'<|Assistant|>' + content + '<|end▁of▁sentence|>'}}{%- endif %}{%- endif %}{%- if message['role'] == 'tool' %}{%- set ns.is_tool = true -%}{%- if ns.is_output_first %}{{'<|tool▁outputs▁begin|><|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- set ns.is_output_first = false %}{%- else %}{{'<|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- endif %}{%- endif %}{%- endfor -%}{% if ns.is_tool %}{{'<|tool▁outputs▁end|>'}}{% endif %}{% if add_generation_prompt and not ns.is_tool %}{{'<|Assistant|>'}}{% endif %}", "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index 98488a988c..d440565d2e 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -143,6 +143,12 @@ def fixture_phi35_tokenizer(): return tokenizer +@pytest.fixture(name="phi4_tokenizer", scope="session", autouse=True) +def fixture_phi4_tokenizer(): + tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-4-reasoning") + return tokenizer + + @pytest.fixture(name="gemma2_tokenizer", scope="session", autouse=True) def fixture_gemma2_tokenizer(): tokenizer = AutoTokenizer.from_pretrained("mlx-community/gemma-2-9b-it-4bit") diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index fcf860f815..f847cab4ae 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -33,15 +33,14 @@ "mistralv03_tokenizer_chat_template_jinja", "[/INST]", ), - # TODO: temporarily skip gemma due to gemma3 template - # Re-enable on new chat_template implementation for perf - # ( - # "gemma2_tokenizer", - # "jinja", - # "gemma2_tokenizer_chat_template_jinja", - # "", - # ), + ( + "gemma2_tokenizer", + "jinja", + "gemma2_tokenizer_chat_template_jinja", + "", + ), ("phi35_tokenizer", "phi_35", None, "<|end|>"), + ("phi4_tokenizer", "phi_4", None, "<|im_end|>"), ] @@ -95,11 +94,7 @@ def _should_skip_turn(self, tokenizer, turn, turn_idx, start_idx, end_idx): if ( turn_idx == 0 and turn.get("from") in ["system", "context"] - and ( - "mistral" in tokenizer.name_or_path.lower() - or "gemma" - in tokenizer.name_or_path.lower() # temporarily skip gemma due to gemma3 template - ) + and ("mistral" in tokenizer.name_or_path.lower()) ): assert ( start_idx == -1 and end_idx == -1 @@ -935,36 +930,14 @@ def test_get_chat_template_variables( "messages", ) - if chat_template == "llama3": - assert variables == {"role", "content"}, ( - f"Expected variables: {'role', 'content'} from {tokenizer}/{chat_template}\n" - f"Got: {variables}\n" - f"Chat template: {actual_jinja_template}" - ) - elif chat_template == "chatml": - assert variables == {"role", "content"}, ( - f"Expected variables: {'role', 'content'} from {tokenizer}/{chat_template}\n" - f"Got: {variables}\n" - f"Chat template: {actual_jinja_template}" - ) - elif chat_template == "jinja" and tokenizer == "mistralv03_tokenizer": - assert variables == {"role", "content", "tool_call_id", "tool_calls"}, ( - f"Expected variables: {'role', 'content', 'tool_call_id', 'tool_calls'} from {tokenizer}/{chat_template}\n" - f"Got: {variables}\n" - f"Chat template: {actual_jinja_template}" - ) - elif chat_template == "jinja" and tokenizer == "gemma2_tokenizer": - assert variables == {"role", "content"}, ( - f"Expected variables: {'role', 'content'} from {tokenizer}/{chat_template}\n" - f"Got: {variables}\n" - f"Chat template: {actual_jinja_template}" - ) - elif chat_template == "phi_35": - assert variables == {"role", "content"}, ( - f"Expected variables: {'role', 'content'} from {tokenizer}/{chat_template}\n" - f"Got: {variables}\n" - f"Chat template: {actual_jinja_template}" - ) + # Special case for Mistral with additional tool variables + if chat_template == "jinja" and tokenizer == "mistralv03_tokenizer": + expected_variables = {"role", "content", "tool_call_id", "tool_calls"} + # Most chat templates use the standard role and content variables + elif chat_template in ["llama3", "chatml", "phi_35", "phi_4"] or ( + chat_template == "jinja" and tokenizer == "gemma2_tokenizer" + ): + expected_variables = {"role", "content"} else: LOG.warning( f"Unsupported chat template: {chat_template} with {chat_template_jinja}" @@ -973,6 +946,12 @@ def test_get_chat_template_variables( f"Unsupported chat template: {chat_template} with {chat_template_jinja}" ) + assert variables == expected_variables, ( + f"Expected variables: {expected_variables} from {tokenizer}/{chat_template}\n" + f"Got: {variables}\n" + f"Chat template: {actual_jinja_template}" + ) + def test_eot_tokens_conflict_with_eos_token( self, tokenizer, From 45adf1bfb9af6d3dade6ae91dd3ab2afe48623b3 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Thu, 19 Jun 2025 11:16:52 -0400 Subject: [PATCH 0730/1405] get_logger use_environ fix (#2808) * get_logger use_environ fix * rethinking * replacing old logger imports * simplify * fix boolean cond --- src/axolotl/cli/config.py | 2 +- src/axolotl/integrations/base.py | 2 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/integrations/liger/__init__.py | 2 +- src/axolotl/integrations/liger/args.py | 1 + src/axolotl/loaders/tokenizer.py | 2 +- src/axolotl/monkeypatch/ring_attn/patch.py | 2 +- src/axolotl/monkeypatch/unsloth_.py | 2 +- src/axolotl/utils/config/__init__.py | 2 +- src/axolotl/utils/distributed.py | 40 +++++++++---------- src/axolotl/utils/freeze.py | 3 +- src/axolotl/utils/logging.py | 27 ++++--------- src/axolotl/utils/schemas/config.py | 2 +- src/axolotl/utils/schemas/model.py | 2 +- src/axolotl/utils/trainer.py | 2 +- 15 files changed, 40 insertions(+), 53 deletions(-) diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index d55448da4d..0fb13bfd3d 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -26,7 +26,7 @@ from axolotl.utils.trainer import prepare_opinionated_env, prepare_optim_env from axolotl.utils.wandb_ import setup_wandb_env_vars -LOG = get_logger(__name__, use_environ=True) +LOG = get_logger(__name__) def check_remote_config(config: Union[str, Path]) -> Union[str, Path]: diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 9162bc7458..7d9b6a6f96 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -33,7 +33,7 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger -LOG = get_logger(__name__, use_environ=True) +LOG = get_logger(__name__) if TYPE_CHECKING: from axolotl.common.datasets import TrainDatasetMeta diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index a7e94e3637..9b155ca8a8 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -28,7 +28,7 @@ from .args import CutCrossEntropyArgs # pylint: disable=unused-import. # noqa: F401 -LOG = get_logger(__name__, use_environ=True) +LOG = get_logger(__name__) _CCE_INSTALL_MESSAGE = ( "Please install cut_cross_entropy with transformers support using " diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index 1c17ab2b59..8de94c78be 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -27,7 +27,7 @@ from .args import LigerArgs # pylint: disable=unused-import. # noqa: F401 from .utils import patch_with_compile_disable -LOG = get_logger(__name__, use_environ=True) +LOG = get_logger(__name__) class LigerPlugin(BasePlugin): diff --git a/src/axolotl/integrations/liger/args.py b/src/axolotl/integrations/liger/args.py index 7c9eb23d56..d05f08f9a3 100644 --- a/src/axolotl/integrations/liger/args.py +++ b/src/axolotl/integrations/liger/args.py @@ -15,6 +15,7 @@ """ Module for handling LIGER input arguments. """ + from typing import Optional from pydantic import BaseModel, model_validator diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index 4f9a60a695..9fdb7d5cc1 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -273,7 +273,7 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): {"additional_special_tokens": additional_special_tokens} ) - if is_main_process(use_environ=True): + if is_main_process(): LOG.debug(f"EOS: {tokenizer.eos_token_id} / {tokenizer.eos_token}") LOG.debug(f"BOS: {tokenizer.bos_token_id} / {tokenizer.bos_token}") LOG.debug(f"PAD: {tokenizer.pad_token_id} / {tokenizer.pad_token}") diff --git a/src/axolotl/monkeypatch/ring_attn/patch.py b/src/axolotl/monkeypatch/ring_attn/patch.py index 7d733cfc1b..d83476e5a2 100644 --- a/src/axolotl/monkeypatch/ring_attn/patch.py +++ b/src/axolotl/monkeypatch/ring_attn/patch.py @@ -13,9 +13,9 @@ import accelerate import torch import torch.distributed as dist -from accelerate.logging import get_logger from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import RingAttnFunc LOG = get_logger(__name__) diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py index 61f4eeea03..146047e959 100644 --- a/src/axolotl/monkeypatch/unsloth_.py +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -4,12 +4,12 @@ import types import torch -from accelerate.logging import get_logger from peft import PeftModelForCausalLM from torch import nn from transformers.models.llama.modeling_llama import LlamaFlashAttention2 from axolotl.monkeypatch.utils import detab_code +from axolotl.utils.logging import get_logger LOG = get_logger(__name__) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index e0eaf9ac92..745a0c8ce5 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -21,7 +21,7 @@ from axolotl.utils.schemas.config import AxolotlInputConfig as AxolotlInputConfigBase from axolotl.utils.schemas.datasets import DPODataset, KTODataset, SFTDataset -LOG = get_logger(__name__, use_environ=True) +LOG = get_logger(__name__) def choose_device(cfg): diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index 0673c6e951..b509ad0ca1 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -1,6 +1,4 @@ -""" -utility helpers for distributed checks -""" +"""Utilities for distributed functionality.""" import os import pickle # nosec @@ -19,7 +17,7 @@ distributed_state = None # pylint: disable=invalid-name -def get_device_type(): +def get_device_type() -> torch.device: device = torch.device("cpu") if is_torch_cuda_available(): device = torch.device("cuda") @@ -30,7 +28,7 @@ def get_device_type(): return device -def get_device_count(): +def get_device_count() -> int: cur_device = get_device_type() if "cuda" in str(cur_device): return torch.cuda.device_count() @@ -39,7 +37,7 @@ def get_device_count(): return 1 -def get_current_device(): +def get_current_device() -> int: cur_device = get_device_type() if "cuda" in str(cur_device): return torch.cuda.current_device() @@ -48,12 +46,14 @@ def get_current_device(): return 0 -def is_distributed(): - """ - Check if distributed training is initialized. - """ +def get_distributed_state() -> PartialState | None: + return distributed_state + + +def is_distributed() -> bool: + """Check if distributed training is initialized.""" global distributed_state # pylint: disable=global-statement - if not distributed_state: + if distributed_state is None: timeout = int(os.environ.get("AXOLOTL_NCCL_TIMEOUT", 1800)) distributed_state = PartialState(timeout=timedelta(seconds=timeout)) @@ -69,31 +69,31 @@ def barrier(): dist.barrier() -def is_main_process(use_environ=False): +def is_main_process() -> bool: """ Check if the current process is the main process. If not in distributed mode, always return `True`. - Args: - - use_environ (bool, optional): Use environment variable to determine main process. + We use a simpler logic when the distributed state is not initialized: we just log + on the 0-th local rank. Returns: - - bool: `True` if the current process is the main process, `False` otherwise. + `True` if the current process is the main process, `False` otherwise. """ - if use_environ: + if get_distributed_state() is None: return os.environ.get("LOCAL_RANK", "0") == "0" if not is_distributed(): return True return dist.get_rank() == 0 -def is_local_main_process(use_environ=False): - if use_environ: +def is_local_main_process() -> bool: + if get_distributed_state() is None: return os.environ.get("LOCAL_RANK", "0") == "0" return PartialState().is_local_main_process -def get_world_size(): +def get_world_size() -> int: return int(os.getenv("WORLD_SIZE", "1")) @@ -115,7 +115,7 @@ def cleanup_distributed(): @contextmanager -def zero_first(is_main): +def zero_first(is_main: bool): """ runs the wrapped context so that rank 0 runs first before other ranks """ diff --git a/src/axolotl/utils/freeze.py b/src/axolotl/utils/freeze.py index 65ca621374..936708f046 100644 --- a/src/axolotl/utils/freeze.py +++ b/src/axolotl/utils/freeze.py @@ -5,9 +5,8 @@ import re from typing import Callable, List, Tuple, Union -from accelerate.logging import get_logger - from axolotl.utils.distributed import is_main_process +from axolotl.utils.logging import get_logger LOG = get_logger(__name__) diff --git a/src/axolotl/utils/logging.py b/src/axolotl/utils/logging.py index 80daab4eaa..7cc3530ae5 100644 --- a/src/axolotl/utils/logging.py +++ b/src/axolotl/utils/logging.py @@ -1,6 +1,4 @@ -""" -logging helpers to only log on main process -""" +"""Logging helpers to only log on main process.""" import functools import logging @@ -14,27 +12,18 @@ class MultiProcessAdapter(logging.LoggerAdapter): """ - logger adapter for distributed logging, specifically to only log on main process + Logger adapter for distributed logging, specifically to only log on main process. """ - def __init__(self, logger, use_environ=False, extra=None): - super().__init__(logger, extra) - self.use_environ = use_environ - @staticmethod - def _should_log(main_process_only, use_environ=False): - return not main_process_only or ( - main_process_only and is_main_process(use_environ=use_environ) - ) + def _should_log(main_process_only: bool): + return not main_process_only or is_main_process() def log(self, level, msg, *args, **kwargs): - use_environ = kwargs.pop("use_environ", self.use_environ) main_process_only = kwargs.pop("main_process_only", True) kwargs.setdefault("stacklevel", 2) - if self.isEnabledFor(level) and self._should_log( - main_process_only, use_environ=use_environ - ): + if self.isEnabledFor(level) and self._should_log(main_process_only): msg, kwargs = self.process(msg, kwargs) self.logger.log(level, msg, *args, **kwargs) @@ -50,13 +39,11 @@ def warning_once(self, *args, **kwargs): self.warning(*args, **kwargs) -def get_logger( - name: str, log_level: str | None = None, use_environ: bool = False -) -> MultiProcessAdapter: +def get_logger(name: str, log_level: str | None = None) -> MultiProcessAdapter: if log_level is None: log_level = os.environ.get("AXOLOTL_LOG_LEVEL", None) logger = logging.getLogger(name) if log_level is not None: logger.setLevel(log_level.upper()) logger.root.setLevel(log_level.upper()) - return MultiProcessAdapter(logger, use_environ=use_environ, extra={}) + return MultiProcessAdapter(logger, extra={}) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 259daa56f9..4600432727 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -48,7 +48,7 @@ from axolotl.utils.schemas.validation import ValidationMixin from axolotl.utils.schemas.vllm import VllmConfig -LOG = get_logger(__name__, use_environ=True) +LOG = get_logger(__name__) # pylint: disable=too-many-ancestors diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index 6f995996d2..5eea11444d 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -4,7 +4,7 @@ from axolotl.utils.logging import get_logger -LOG = get_logger(__name__, use_environ=True) +LOG = get_logger(__name__) class ModelInputConfig(BaseModel): diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 33ddadf78e..e996cd62bd 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -11,7 +11,6 @@ import numpy as np import torch import torch.cuda -from accelerate.logging import get_logger from datasets import IterableDataset, disable_caching, enable_caching from torch.utils.data import DataLoader, RandomSampler, SequentialSampler from transformers.utils import is_torch_bf16_gpu_available @@ -19,6 +18,7 @@ from axolotl.monkeypatch.trainer_eval_guard import patch_evaluation_loop_for_fsdp2 from axolotl.utils.distributed import reduce_and_broadcast from axolotl.utils.environment import check_cuda_p2p_ib_support +from axolotl.utils.logging import get_logger from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths LOG = get_logger(__name__) From 26c39e1ca7e1efb150a7cccdff16f89d42b1df3e Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 19 Jun 2025 22:19:52 +0700 Subject: [PATCH 0731/1405] fix(doc): address exitcode formatting to help search (#2809) [skip ci] --- docs/faq.qmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/faq.qmd b/docs/faq.qmd index f2744caba8..b84aa75bdd 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -9,11 +9,11 @@ description: Frequently asked questions > A: Usually an issue with the GPUs communicating with each other. See the [NCCL doc](nccl.qmd) -**Q: Exitcode -9** +**Q: exitcode: -9** > A: This usually happens when you run out of system RAM. -**Q: Exitcode -7 while using deepspeed** +**Q: exitcode: -7 while using deepspeed** > A: Try upgrading deepspeed w: `pip install -U deepspeed` From 0494359c6cbd08055bdf80eef5f2e065de1339cf Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 19 Jun 2025 11:27:59 -0400 Subject: [PATCH 0732/1405] update trl to 0.18.2 (#2814) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8bd77ab5e1..6e0d98c5e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ tokenizers>=0.21.1 accelerate==1.7.0 datasets==3.6.0 deepspeed>=0.17.0 -trl==0.18.1 +trl==0.18.2 hf_xet==1.1.2 optimum==1.16.2 From 1d8f500709e63637e89537653a72b2bdf7de3ee8 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 23 Jun 2025 09:07:57 -0400 Subject: [PATCH 0733/1405] deepspeed fix (#2820) --- src/axolotl/utils/distributed.py | 13 ++++++++++--- src/axolotl/utils/trainer.py | 8 +++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index b509ad0ca1..2192e7b9d1 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -46,16 +46,23 @@ def get_current_device() -> int: return 0 +def init_distributed_state(): + global distributed_state # pylint: disable=global-statement + if distributed_state is None: + timeout = int(os.environ.get("AXOLOTL_NCCL_TIMEOUT", 1800)) + distributed_state = PartialState(timeout=timedelta(seconds=timeout)) + + def get_distributed_state() -> PartialState | None: return distributed_state def is_distributed() -> bool: """Check if distributed training is initialized.""" - global distributed_state # pylint: disable=global-statement + init_distributed_state() + if distributed_state is None: - timeout = int(os.environ.get("AXOLOTL_NCCL_TIMEOUT", 1800)) - distributed_state = PartialState(timeout=timedelta(seconds=timeout)) + return False return distributed_state.use_distributed and distributed_state.initialized diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index e996cd62bd..633dffde57 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -16,7 +16,7 @@ from transformers.utils import is_torch_bf16_gpu_available from axolotl.monkeypatch.trainer_eval_guard import patch_evaluation_loop_for_fsdp2 -from axolotl.utils.distributed import reduce_and_broadcast +from axolotl.utils.distributed import init_distributed_state, reduce_and_broadcast from axolotl.utils.environment import check_cuda_p2p_ib_support from axolotl.utils.logging import get_logger from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths @@ -537,6 +537,12 @@ def setup_deepspeed_env(cfg, stage=None): os.environ["ACCELERATE_DEEPSPEED_ZERO_STAGE"] = str(stage) if stage == 3: os.environ["ACCELERATE_DEEPSPEED_ZERO3_INIT"] = "true" + + # NOTE(djsaunde): The distribued state cannot be initialized prior to the + # ACCELERATE_USE_DEEPSPEED assignment, but it must be initialized some time prior + # to model load. + init_distributed_state() + # If we don't assign this, it doesn't actually get set in the accelerate weakref _ = HfTrainerDeepSpeedConfig(cfg.deepspeed) From 12c826816d3fe8a5fa6ca46af112b18a756f6cb4 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 23 Jun 2025 23:08:46 -0400 Subject: [PATCH 0734/1405] chunked cross entropy loss (#2625) * chunked cross entropy loss * refactor so we can add test * use relative import * update schema description --- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/loaders/patch_manager.py | 10 ++ src/axolotl/monkeypatch/loss/__init__.py | 0 src/axolotl/monkeypatch/loss/chunked.py | 134 ++++++++++++++++++ src/axolotl/utils/schemas/config.py | 13 ++ tests/test_chunked_xentropy.py | 40 ++++++ 6 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 src/axolotl/monkeypatch/loss/__init__.py create mode 100644 src/axolotl/monkeypatch/loss/chunked.py create mode 100644 tests/test_chunked_xentropy.py diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 9b155ca8a8..c29bb55d48 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -71,7 +71,7 @@ def pre_model_load(self, cfg): if cfg.cut_cross_entropy: self._check_requirements() - from axolotl.integrations.cut_cross_entropy.monkeypatch.patch import ( + from .monkeypatch.patch import ( cce_patch, ) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index ca8fd12583..7c4e757966 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -50,6 +50,7 @@ def has_flash_attn(self) -> bool: def apply_pre_model_load_patches(self): """Apply pre-model load patches based on config.""" self._apply_flash_attention_patches() + self._apply_chunked_cross_entropy_patch() self._apply_fsdp_patches() self._apply_adapter_patches() self._apply_flex_attention_patches() @@ -78,6 +79,15 @@ def _apply_flash_attention_patches(self): patch_xformers_attn_over_fa2() self.cfg.flash_attention = True + def _apply_chunked_cross_entropy_patch(self): + if self.cfg.chunked_cross_entropy: + from axolotl.monkeypatch.loss.chunked import patch_chunked_ce_loss_fn + + if self.cfg.chunked_cross_entropy_num_chunks: + patch_chunked_ce_loss_fn(self.cfg.chunked_cross_entropy_num_chunks) + else: + patch_chunked_ce_loss_fn() + def _apply_fsdp_patches(self): """Apply patches for FSDP configurations.""" if self.cfg.fsdp_config and str(self.cfg.fsdp_config.fsdp_version) == "2": diff --git a/src/axolotl/monkeypatch/loss/__init__.py b/src/axolotl/monkeypatch/loss/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/loss/chunked.py b/src/axolotl/monkeypatch/loss/chunked.py new file mode 100644 index 0000000000..0a9d0de82c --- /dev/null +++ b/src/axolotl/monkeypatch/loss/chunked.py @@ -0,0 +1,134 @@ +""" +chunked ce loss +""" + +from typing import List, Optional + +import torch +import torch.nn.functional as F + + +# copied and modified from torchtune.modules.loss.CEWithChunkedOutputLoss +class CEWithChunkedOutputLoss(torch.nn.Module): + """ + Cross-entropy with chunked outputs that saves memory by only upcasting one chunk at a time. + + For more details, please refer to: https://github.com/pytorch/torchtune/pull/1390 + """ + + def __init__(self, num_output_chunks: int = 8, ignore_index: int = -100): + super().__init__() + self.num_output_chunks = num_output_chunks + self.ignore_index = ignore_index + + def compute_cross_entropy( + self, + logits: torch.Tensor, + labels: torch.Tensor, + normalize: bool = True, # pylint: disable=unused-argument + ) -> torch.Tensor: + """ + Upcast logits to fp32 and compute cross entropy loss. + """ + return F.cross_entropy( + logits.float(), labels, ignore_index=self.ignore_index, reduction="sum" + ) + + def forward( + self, logits: List[torch.Tensor], labels: torch.Tensor, reduction="sum" + ) -> torch.Tensor: + """ + Args: + logits (List[torch.Tensor]): List of chunked logits of length + ``self.num_output_chunks``, where each chunk has shape + ``(batch_size, num_tokens / num_output_chunks, vocab_size)``. + labels (torch.Tensor): Ground truth labels of shape ``(batch_size, num_tokens)``. + reduction (str): The reduction to apply to the output. + + Returns: + torch.Tensor: Cross entropy loss of shape (1,). + """ + + total_elements = (labels != self.ignore_index).sum() + + # chunk and reshape labels (bsz, num_tokens, vocab) -> [(bsz*num_tokens/num_chunks, vocab)] + labels = [ + target_chunk.reshape(-1) + for target_chunk in labels.chunk(self.num_output_chunks, dim=1) + ] + # reshape logits [(bsz, num_tokens/num_chunks, vocab)] -> [(bsz*num_tokens/num_chunks, vocab)] + logits = [ + logit_chunk.reshape(-1, logit_chunk.size(-1)) for logit_chunk in logits + ] + + # compute one chunk at a time + total_loss = 0.0 + for logits_chunk, labels_chunk in zip(logits, labels): + total_loss += self.compute_cross_entropy(logits_chunk, labels_chunk) + + if reduction == "sum": + return total_loss + return total_loss / total_elements + + +def _build_chunked_ce_loss_fn(num_output_chunks: int = 8, ignore_index: int = -100): + loss_fn_ce = CEWithChunkedOutputLoss(num_output_chunks, ignore_index) + loss_fn_ce.compute_cross_entropy = torch.compile( + loss_fn_ce.compute_cross_entropy, backend="inductor" + ) + return loss_fn_ce + + +def get_causal_lm_loss(num_output_chunks: int = 8, ignore_index: int = -100): + loss_fn_ce = _build_chunked_ce_loss_fn(num_output_chunks, ignore_index) + + def chunked_fix_cross_entropy( + source, + target, + num_items_in_batch: int = None, + ignore_index: int = -100, + **kwargs, + ): # pylint: disable=unused-argument + reduction = "sum" if num_items_in_batch is not None else "mean" + logit_chunks = [ # pylint: disable=unnecessary-comprehension + chunk for chunk in source.chunk(loss_fn_ce.num_output_chunks, dim=1) + ] + loss = loss_fn_ce(logit_chunks, target, reduction=reduction) + if reduction == "sum": + loss = loss / num_items_in_batch + return loss + + def for_causal_lm_chunked_loss( + logits, + labels, + vocab_size: int = None, # pylint: disable=unused-argument + num_items_in_batch: Optional[int] = None, + ignore_index: int = -100, + shift_labels: Optional[torch.Tensor] = None, + **kwargs, + ) -> torch.Tensor: + # skip the upcast to float since we handle that in the chunking loss + if shift_labels is None: + # Shift so that tokens < n predict n + labels = F.pad(labels, (0, 1), value=ignore_index) + shift_labels = labels[..., 1:].contiguous() + + # Skip Flattening the tokens + # Enable model parallelism + shift_labels = shift_labels.to(logits.device) + loss = chunked_fix_cross_entropy( + logits, shift_labels, num_items_in_batch, ignore_index, **kwargs + ) + return loss + + return for_causal_lm_chunked_loss + + +def patch_chunked_ce_loss_fn(num_output_chunks: int = 8, ignore_index: int = -100): + import transformers.loss.loss_utils + + for_causal_lm_chunked_loss = get_causal_lm_loss(num_output_chunks, ignore_index) + transformers.loss.loss_utils.ForCausalLMLoss = for_causal_lm_chunked_loss + transformers.loss.loss_utils.LOSS_MAPPING["ForCausalLM"] = ( + for_causal_lm_chunked_loss + ) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 4600432727..c698fc3b66 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -523,6 +523,19 @@ class AxolotlInputConfig( }, ) + chunked_cross_entropy: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use chunked cross entropy loss for memory efficiency" + }, + ) + chunked_cross_entropy_num_chunks: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of chunks to use for chunked cross entropy loss" + }, + ) + llama4_linearized_experts: bool | None = None deepspeed: str | dict[str, Any] | None = Field( diff --git a/tests/test_chunked_xentropy.py b/tests/test_chunked_xentropy.py new file mode 100644 index 0000000000..3e439f0a3b --- /dev/null +++ b/tests/test_chunked_xentropy.py @@ -0,0 +1,40 @@ +""" +test suite for chunked cross entropy +""" + +import pytest +import torch +from torch import nn + +from axolotl.monkeypatch.loss.chunked import get_causal_lm_loss + + +@pytest.fixture +def chunked_fixtures(): + model_dim = 512 + vocab_size = 1024 * 256 + seq_len = 2048 + batch_size = 1 + + lm_head = nn.Linear(model_dim, vocab_size) + hidden_state = torch.randn(batch_size, seq_len, model_dim) + labels = torch.randint(low=0, high=vocab_size, size=(batch_size, seq_len)) + return lm_head, hidden_state, labels, vocab_size + + +def test_chunked_forward(chunked_fixtures): # pylint: disable=redefined-outer-name + lm_head, hidden_state, labels, vocab_size = chunked_fixtures + lm_loss = get_causal_lm_loss() + + logits = lm_head(hidden_state) + + chunked_lm_loss = lm_loss(logits, labels) + + logits_flattened = logits.view(-1, vocab_size) + labels_flattened = labels.view(-1) + + loss = nn.functional.cross_entropy( + logits_flattened.float(), labels_flattened, reduction="mean" + ) + + assert torch.allclose(chunked_lm_loss, loss, atol=1e-2, rtol=1e-2) From c6b5d35e5d37ccab9710288586d46c0f7d26b6ab Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 24 Jun 2025 10:51:30 +0700 Subject: [PATCH 0735/1405] fix: re-add gemma3 patch (#2817) --- src/axolotl/loaders/patch_manager.py | 10 ++++++++++ .../monkeypatch/models/gemma3/__init__.py | 0 .../monkeypatch/models/gemma3/modeling.py | 16 ++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 src/axolotl/monkeypatch/models/gemma3/__init__.py create mode 100644 src/axolotl/monkeypatch/models/gemma3/modeling.py diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 7c4e757966..3f8116b21b 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -64,6 +64,7 @@ def apply_pre_model_load_patches(self): self._patch_llama_derived_model() self._apply_mistral_cross_entropy_patch() self._apply_self_attention_lora_patch() + self._apply_gemma3_conditional_generation_forward_patch() def apply_post_model_load_patches(self, model: PreTrainedModel): """Apply patches that require the model instance.""" @@ -221,6 +222,15 @@ def _apply_multipack_patches(self): has_remote_code=has_remote_code, ) + def _apply_gemma3_conditional_generation_forward_patch(self): + """Apply gemma3 conditional generation forward patch.""" + if self.model_config.model_type in ["gemma3", "gemma3_text"]: + from axolotl.monkeypatch.models.gemma3.modeling import ( + patch_gemma3_conditional_generation_forward, + ) + + patch_gemma3_conditional_generation_forward() + def _patch_attention(self): """Apply attention-specific patches based on model type.""" if not (self.cfg.flash_attention and hasattr(self.model_config, "model_type")): diff --git a/src/axolotl/monkeypatch/models/gemma3/__init__.py b/src/axolotl/monkeypatch/models/gemma3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/gemma3/modeling.py b/src/axolotl/monkeypatch/models/gemma3/modeling.py new file mode 100644 index 0000000000..3b608c347c --- /dev/null +++ b/src/axolotl/monkeypatch/models/gemma3/modeling.py @@ -0,0 +1,16 @@ +"""Monkeypatch for gemma3 conditional generation forward to fix high loss""" + + +def patch_gemma3_conditional_generation_forward(): + # Remove when https://github.com/huggingface/transformers/pull/37208 merged + + from transformers.models.gemma3.modeling_gemma3 import ( + Gemma3ForConditionalGeneration, + ) + + setattr(Gemma3ForConditionalGeneration, "accepts_loss_kwargs", False) + + def unpatch(): + delattr(Gemma3ForConditionalGeneration, "accepts_loss_kwargs") + + return unpatch From 46675496a39493c6d3df914ddfd955b3338a039d Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Tue, 24 Jun 2025 14:59:30 -0400 Subject: [PATCH 0736/1405] log config (#2819) * log config * moving text art; adding sensitive value redaction + sorting * revert pre-commit changes * remove none-valued config before dumping * just redact api keys --- src/axolotl/cli/cloud/__init__.py | 4 ---- src/axolotl/cli/config.py | 13 +++++++++++++ src/axolotl/cli/evaluate.py | 2 -- src/axolotl/cli/inference.py | 2 -- src/axolotl/cli/main.py | 2 ++ src/axolotl/cli/merge_lora.py | 3 --- src/axolotl/cli/merge_sharded_fsdp_weights.py | 2 -- src/axolotl/cli/preprocess.py | 2 -- src/axolotl/cli/quantize.py | 2 -- src/axolotl/cli/train.py | 2 -- src/axolotl/train.py | 3 --- 11 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/axolotl/cli/cloud/__init__.py b/src/axolotl/cli/cloud/__init__.py index 5d6900d3eb..5cdce29ddf 100644 --- a/src/axolotl/cli/cloud/__init__.py +++ b/src/axolotl/cli/cloud/__init__.py @@ -7,7 +7,6 @@ import yaml -from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.cloud.modal_ import ModalCloud from axolotl.utils.dict import DictDefault @@ -24,7 +23,6 @@ def do_cli_preprocess( cloud_config: Union[Path, str], config: Union[Path, str], ) -> None: - print_axolotl_text_art() cloud_cfg = load_cloud_cfg(cloud_config) cloud = ModalCloud(cloud_cfg) with open(config, "r", encoding="utf-8") as file: @@ -39,7 +37,6 @@ def do_cli_train( cwd=None, **kwargs, ) -> None: - print_axolotl_text_art() cloud_cfg = load_cloud_cfg(cloud_config) cloud = ModalCloud(cloud_cfg) with open(config, "r", encoding="utf-8") as file: @@ -54,7 +51,6 @@ def do_cli_lm_eval( cloud_config: Union[Path, str], config: Union[Path, str], ) -> None: - print_axolotl_text_art() cloud_cfg = load_cloud_cfg(cloud_config) cloud = ModalCloud(cloud_cfg) with open(config, "r", encoding="utf-8") as file: diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index 0fb13bfd3d..cb0eece7fe 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -28,6 +28,8 @@ LOG = get_logger(__name__) +API_KEY_FIELDS = {"comet_api_key"} + def check_remote_config(config: Union[str, Path]) -> Union[str, Path]: """ @@ -233,4 +235,15 @@ def load_cfg( setup_comet_env_vars(cfg) plugin_set_cfg(cfg) + cfg_to_log = { + k: "[REDACTED]" if k in API_KEY_FIELDS else v + for k, v in cfg.items() + if v is not None + } + + LOG.info( + "config:\n%s", + json.dumps(cfg_to_log, indent=2, default=str, sort_keys=True), + ) + return cfg diff --git a/src/axolotl/cli/evaluate.py b/src/axolotl/cli/evaluate.py index f131f70830..8a847a6496 100644 --- a/src/axolotl/cli/evaluate.py +++ b/src/axolotl/cli/evaluate.py @@ -9,7 +9,6 @@ from transformers.hf_argparser import HfArgumentParser from axolotl.cli.args import TrainerCliArgs -from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.checks import check_accelerate_default_config, check_user_token from axolotl.cli.config import load_cfg from axolotl.common.datasets import load_datasets, load_preference_datasets @@ -35,7 +34,6 @@ def do_evaluate(cfg: DictDefault, cli_args: TrainerCliArgs) -> None: patch_optimized_env() # pylint: disable=duplicate-code - print_axolotl_text_art() check_accelerate_default_config() if int(os.getenv("LOCAL_RANK", "0")) == 0: check_user_token() diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index b5bc158fa1..10132cd6fd 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -13,7 +13,6 @@ from transformers import GenerationConfig, TextIteratorStreamer, TextStreamer from axolotl.cli.args import InferenceCliArgs -from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.config import load_cfg from axolotl.cli.utils import load_model_and_tokenizer from axolotl.utils.chat_templates import ( @@ -255,7 +254,6 @@ def do_cli( kwargs: Additional keyword arguments to override config file values. """ # pylint: disable=duplicate-code - print_axolotl_text_art() parsed_cfg = load_cfg(config, inference=True, rl=None, **kwargs) parsed_cfg.sample_packing = False parser = transformers.HfArgumentParser(InferenceCliArgs) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index 3dafa552bd..69c1425acf 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -20,6 +20,7 @@ TrainerCliArgs, VllmServeCliArgs, ) +from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.sweeps import generate_sweep_configs from axolotl.cli.utils import ( add_options_from_config, @@ -40,6 +41,7 @@ @click.version_option(version=axolotl.__version__, prog_name="axolotl") def cli(): """Axolotl CLI - Train and fine-tune large language models""" + print_axolotl_text_art() @cli.command() diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 36cfdec4e6..d639b3aeec 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -6,7 +6,6 @@ import fire from dotenv import load_dotenv -from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.config import load_cfg from axolotl.cli.utils import load_model_and_tokenizer from axolotl.utils.dict import DictDefault @@ -23,8 +22,6 @@ def do_merge_lora(*, cfg: DictDefault) -> None: Args: cfg: Dictionary mapping `axolotl` config keys to values. """ - print_axolotl_text_art() - model, tokenizer, processor = load_model_and_tokenizer(cfg=cfg) safe_serialization = cfg.save_safetensors is True diff --git a/src/axolotl/cli/merge_sharded_fsdp_weights.py b/src/axolotl/cli/merge_sharded_fsdp_weights.py index 2480b551d2..b0880ce215 100644 --- a/src/axolotl/cli/merge_sharded_fsdp_weights.py +++ b/src/axolotl/cli/merge_sharded_fsdp_weights.py @@ -22,7 +22,6 @@ from safetensors.torch import save_file as safe_save_file from torch.distributed.checkpoint.format_utils import _EmptyStateDictLoadPlanner -from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.config import load_cfg from axolotl.utils.logging import get_logger @@ -194,7 +193,6 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): kwargs: Additional keyword arguments to override config file values. """ # pylint: disable=duplicate-code - print_axolotl_text_art() parsed_cfg = load_cfg(config, **kwargs) fsdp_dir = Path(parsed_cfg.output_dir) / "pytorch_model_fsdp_0" diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index 9f96f5cc17..b8258383ec 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -12,7 +12,6 @@ from transformers import AutoModelForCausalLM from axolotl.cli.args import PreprocessCliArgs -from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.checks import check_accelerate_default_config, check_user_token from axolotl.cli.config import load_cfg from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH @@ -33,7 +32,6 @@ def do_preprocess(cfg: DictDefault, cli_args: PreprocessCliArgs) -> None: cfg: Dictionary mapping `axolotl` config keys to values. cli_args: Preprocessing-specific CLI arguments. """ - print_axolotl_text_art() check_accelerate_default_config() check_user_token() diff --git a/src/axolotl/cli/quantize.py b/src/axolotl/cli/quantize.py index 63d51fadf0..0782976fe6 100644 --- a/src/axolotl/cli/quantize.py +++ b/src/axolotl/cli/quantize.py @@ -7,7 +7,6 @@ from transformers import AutoModelForCausalLM -from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.config import load_cfg from axolotl.loaders import load_tokenizer from axolotl.utils.logging import get_logger @@ -27,7 +26,6 @@ def do_quantize( config (Union[Path, str]): The path to the config file cli_args (dict): Additional command-line arguments """ - print_axolotl_text_art() cfg = load_cfg(config) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index fef80fdbaf..28d4d543ac 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -11,7 +11,6 @@ from transformers.hf_argparser import HfArgumentParser from axolotl.cli.args import TrainerCliArgs -from axolotl.cli.art import print_axolotl_text_art from axolotl.cli.checks import check_accelerate_default_config, check_user_token from axolotl.cli.config import load_cfg from axolotl.common.datasets import load_datasets, load_preference_datasets @@ -35,7 +34,6 @@ def do_train(cfg: DictDefault, cli_args: TrainerCliArgs): # Enable expandable segments for cuda allocation to improve VRAM usage patch_optimized_env() - print_axolotl_text_art() check_accelerate_default_config() if int(os.getenv("LOCAL_RANK", "0")) == 0: check_user_token() diff --git a/src/axolotl/train.py b/src/axolotl/train.py index fa7d569130..819616425e 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -23,7 +23,6 @@ from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled from transformers.trainer import Trainer -from axolotl.cli.art import print_axolotl_text_art from axolotl.common.datasets import TrainDatasetMeta from axolotl.contribs.lgpl import ( # pylint: disable = no-name-in-module fix_untrained_tokens, @@ -545,8 +544,6 @@ def train( Returns: Tuple of (model, tokenizer) after training """ - print_axolotl_text_art() - # Setup model, tokenizer, (causal or RLHF) trainer, etc. ( trainer, From 8c69ec3a1e36375fcf2784267dd8a457eca35aa4 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 25 Jun 2025 08:33:55 -0400 Subject: [PATCH 0737/1405] gating _gather_outputs (causes increased vram usage) (#2829) * SP vram fix * gating _gather_outputs (causes increased vram usage) * reverting unneeded change --- src/axolotl/train.py | 1 + .../utils/ctx_managers/sequence_parallel.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 819616425e..d5dd431c1c 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -218,6 +218,7 @@ def execute_training( gradient_accumulation_steps=cfg.gradient_accumulation_steps, ring_attn_func=cfg.ring_attn_func, heads_k_stride=cfg.heads_k_stride, + gather_outputs=cfg.rl is RLType.GRPO, ) ) diff --git a/src/axolotl/utils/ctx_managers/sequence_parallel.py b/src/axolotl/utils/ctx_managers/sequence_parallel.py index 491cb98771..f429cd2ae5 100644 --- a/src/axolotl/utils/ctx_managers/sequence_parallel.py +++ b/src/axolotl/utils/ctx_managers/sequence_parallel.py @@ -174,6 +174,8 @@ class SequenceParallelContextManager: ring_attn_func: Which ring attention function to use. Currently unused. heads_k_stride: Sequence parallelism K head stride size. Passed through to `varlen_llama3` `ring_flash_attn` implementation. + gather_outputs: Whether to gather outputs after model forward pass across the + sequence parallel group. """ def __init__( @@ -183,12 +185,15 @@ def __init__( gradient_accumulation_steps: int, ring_attn_func: RingAttnFunc, heads_k_stride: int | None, + gather_outputs: bool, ): self.models = models self.sequence_parallel_degree = sequence_parallel_degree self.gradient_accumulation_steps = gradient_accumulation_steps self.ring_attn_func = ring_attn_func self.heads_k_stride = heads_k_stride + self.gather_outputs = gather_outputs + self._register_ring_attn() # Set distributed info for local rank @@ -277,16 +282,17 @@ def sequence_parallel_post_hook(_, __, output: ModelOutput) -> ModelOutput: return output - # Register both hooks + # Register hooks for model in self.models: self.hook_handles.append( model.register_forward_pre_hook( sequence_parallel_pre_hook, with_kwargs=True ) ) - self.hook_handles.append( - model.register_forward_hook(sequence_parallel_post_hook) - ) + if self.gather_outputs: + self.hook_handles.append( + model.register_forward_hook(sequence_parallel_post_hook) + ) def _gather_outputs(self, output: CausalLMOutputWithPast) -> CausalLMOutputWithPast: """Gather sharded outputs from all ranks and reconstruct the full tensor.""" From bb1109b81d4dd058323fe9c035e3d1dd00e66de4 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 25 Jun 2025 20:49:22 +0700 Subject: [PATCH 0738/1405] feat: update CCE to use axolotl's fork (#2813) [skip ci] * feat: update CCE to use axolotl's fork * chore: improve error message * feat: add eot token for gemma3 configs * fix: only warn on more than 1 image * fix: re-add gemma3 patch * Revert "fix: re-add gemma3 patch" This reverts commit f04db5e873bfab705a39b6b860fe8249796977d3. * feat: add qwen25 vl example * feat: point to upstream fork cce package * feat: update cce commit --- examples/gemma3/gemma-3-1b-qlora.yml | 2 + examples/gemma3/gemma-3-4b-qlora.yml | 2 + examples/gemma3/gemma-3-4b-vision-qlora.yml | 2 + examples/qwen2_5-vl/lora-7b.yaml | 55 +++ scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 32 +- .../cut_cross_entropy/__init__.py | 24 +- .../cut_cross_entropy/monkeypatch/__init__.py | 0 .../cut_cross_entropy/monkeypatch/cohere.py | 191 -------- .../cut_cross_entropy/monkeypatch/gemma.py | 165 ------- .../cut_cross_entropy/monkeypatch/gemma3.py | 447 ------------------ .../cut_cross_entropy/monkeypatch/glm4.py | 57 --- .../cut_cross_entropy/monkeypatch/llama.py | 164 ------- .../cut_cross_entropy/monkeypatch/llama4.py | 401 ---------------- .../cut_cross_entropy/monkeypatch/mistral3.py | 384 --------------- .../cut_cross_entropy/monkeypatch/mllama.py | 366 -------------- .../cut_cross_entropy/monkeypatch/patch.py | 126 ----- .../cut_cross_entropy/monkeypatch/qwen2.py | 37 -- .../monkeypatch/qwen2_5_vl.py | 246 ---------- .../monkeypatch/qwen2_moe.py | 178 ------- .../cut_cross_entropy/monkeypatch/qwen2_vl.py | 239 ---------- .../cut_cross_entropy/monkeypatch/qwen3.py | 35 -- .../monkeypatch/qwen3_moe.py | 183 ------- .../cut_cross_entropy/monkeypatch/utils.py | 40 -- src/axolotl/processing_strategies.py | 2 +- 25 files changed, 94 insertions(+), 3286 deletions(-) create mode 100644 examples/qwen2_5-vl/lora-7b.yaml delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/__init__.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/glm4.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/mistral3.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/mllama.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_5_vl.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_moe.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_vl.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3_moe.py delete mode 100644 src/axolotl/integrations/cut_cross_entropy/monkeypatch/utils.py diff --git a/examples/gemma3/gemma-3-1b-qlora.yml b/examples/gemma3/gemma-3-1b-qlora.yml index 44310558c2..217c887aa6 100644 --- a/examples/gemma3/gemma-3-1b-qlora.yml +++ b/examples/gemma3/gemma-3-1b-qlora.yml @@ -13,6 +13,8 @@ load_in_4bit: true # huggingface repo chat_template: gemma3 +eot_tokens: + - datasets: - path: cgato/SlimOrcaDedupCleaned type: chat_template diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml index 0d89d9ffb8..d78559ae3b 100644 --- a/examples/gemma3/gemma-3-4b-qlora.yml +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -6,6 +6,8 @@ load_in_4bit: true ddp_find_unused_parameters: true chat_template: gemma3 +eot_tokens: + - datasets: - path: cgato/SlimOrcaDedupCleaned type: chat_template diff --git a/examples/gemma3/gemma-3-4b-vision-qlora.yml b/examples/gemma3/gemma-3-4b-vision-qlora.yml index 339df92e5e..183eb88e84 100644 --- a/examples/gemma3/gemma-3-4b-vision-qlora.yml +++ b/examples/gemma3/gemma-3-4b-vision-qlora.yml @@ -12,6 +12,8 @@ sample_packing: false ddp_find_unused_parameters: true chat_template: gemma3 +eot_tokens: + - datasets: - path: HuggingFaceH4/llava-instruct-mix-vsft type: chat_template diff --git a/examples/qwen2_5-vl/lora-7b.yaml b/examples/qwen2_5-vl/lora-7b.yaml new file mode 100644 index 0000000000..25d02805f7 --- /dev/null +++ b/examples/qwen2_5-vl/lora-7b.yaml @@ -0,0 +1,55 @@ +base_model: Qwen/Qwen2.5-VL-7B-Instruct +processor_type: AutoProcessor + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +chat_template: qwen2_vl +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + field_messages: messages +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: lora +lora_model_dir: + +sequence_len: 8192 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +logging_steps: 1 +flash_attention: true +eager_attention: + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 4a92746c19..bb9224bb0d 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@a1174ca"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@78b2a45713a54c9bedf8b33f5e31cf07a1a57154"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index bddf3ced2b..b5e3ecda85 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,19 +19,11 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@bad6f7b49c75fdec69471abb71b4cddd0f0c6438" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@78b2a45713a54c9bedf8b33f5e31cf07a1a57154" ``` ## Usage -**NOTE**: If you are training a VLM model, please use older version of Axolotl as upstream has applied a major VLM refactor, and our patches have not been updated yet. - -```bash -git checkout 787880215b3ab32ccaf81c1b2e9588c6f3e6e764 - -pip3 install --no-build-isolation -e . -``` - ```yaml plugins: - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin @@ -39,27 +31,29 @@ plugins: ## Supported Models -- llama -- llama4 -- llama4_text -- mllama -- phi3 +- cohere +- cohere2 - gemma - gemma2 - gemma3 - gemma3_text +- glm +- glm4 +- llama +- llama4 +- llama4_text - mistral - mistral3 +- mllama +- phi +- phi3 +- phi4_multimodal - qwen2 -- qwen2_moe - qwen2_vl +- qwen2_moe - qwen2_5_vl - qwen3 - qwen3_moe -- cohere -- cohere2 -- glm -- glm4 ## Citation diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index c29bb55d48..37f4dba689 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -31,8 +31,8 @@ LOG = get_logger(__name__) _CCE_INSTALL_MESSAGE = ( - "Please install cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/apple/ml-cross-entropy.git@bad6f7b49c75fdec69471abb71b4cddd0f0c6438"`' + "Please install Axolotl's fork of cut_cross_entropy with transformers support using " + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@7f6afce"`' ) @@ -64,16 +64,28 @@ def _check_requirements(self): "cut_cross_entropy.transformers" ) if cce_spec_transformers is None: - raise ImportError(_CCE_INSTALL_MESSAGE) + raise ImportError( + "Transformers support is not installed. " + _CCE_INSTALL_MESSAGE + ) + + # Check if Axolotl's cce fork is installed + try: + from cut_cross_entropy.transformers.patch import AXOLOTL_CCE_FORK + + if not AXOLOTL_CCE_FORK: + raise ImportError + except ImportError as e: + raise ImportError( + "Axolotl's fork of cut_cross_entropy is not installed. " + + _CCE_INSTALL_MESSAGE + ) from e def pre_model_load(self, cfg): """Apply cut cross entropy before model loading if enabled.""" if cfg.cut_cross_entropy: self._check_requirements() - from .monkeypatch.patch import ( - cce_patch, - ) + from cut_cross_entropy.transformers.patch import cce_patch LOG.info( f"Applying Cut Cross Entropy to model type: {cfg.model_config_type}" diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/__init__.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py deleted file mode 100644 index ea9e107247..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/cohere.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Cohere and Cohere2 CCE patch.""" - -# This patch is based off transformers 4.50.0. -# It patches the forward function for CohereForCausalLM and Cohere2ForCausalLM. -# It scales the hidden states by the logit scale in advance instead of the logits as the -# operation is done internally and should be mathematically equivalent. - -# pylint: disable=duplicate-code - -from types import MethodType -from typing import Optional, Tuple, Union - -import torch -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, - apply_lce, -) -from transformers.cache_utils import Cache -from transformers.modeling_outputs import CausalLMOutputWithPast -from transformers.models.cohere.modeling_cohere import ( - KwargsForCausalLM, -) -from transformers.processing_utils import Unpack -from transformers.utils.deprecation import deprecate_kwarg - -_PATCH_OPTS: PatchOptions | None = None - - -@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -def cce_forward( - self, - input_ids: torch.LongTensor | None = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[Union[Cache, list[torch.FloatTensor]]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - **kwargs: Unpack[KwargsForCausalLM], -) -> Union[Tuple, CausalLMOutputWithPast]: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - Returns: - - Example: - - ```python - >> from transformers import AutoTokenizer, CohereForCausalLM - - >> model = CohereForCausalLM.from_pretrained("CohereForAI/c4ai-command-r-v01") - >> tokenizer = AutoTokenizer.from_pretrained("CohereForAI/c4ai-command-r-v01") - - >> prompt = "Hey, are you conscious? Can you talk to me?" - >> inputs = tokenizer(prompt, return_tensors="pt") - - >> # Generate - >> generate_ids = model.generate(inputs.input_ids, max_length=30) - >> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." - ```""" - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - - # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) - outputs = self.model( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - **kwargs, - ) - - hidden_states = outputs[0] - loss = None - logits = None - - # Only compute necessary logits, and do not upcast them to float if we are not computing the loss - slice_indices = ( - slice(-logits_to_keep, None) - if isinstance(logits_to_keep, int) - else logits_to_keep - ) - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - # scale hidden_states by logit_scale in-place of logits - loss = apply_lce( - hidden_states[:, slice_indices, :] * self.logit_scale, - self.lm_head.weight, - labels, - _PATCH_OPTS, - **kwargs, - ) - else: - logits = self.lm_head(hidden_states[:, slice_indices, :]) - logits = logits * self.logit_scale # main diff from Llama - - if labels is not None: - loss = self.loss_function( - logits=logits, - labels=labels, - vocab_size=self.config.vocab_size, - **kwargs, - ) - - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return CausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - ) - - -def patch_cohere( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - from transformers.models.cohere import modeling_cohere - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_cohere.CohereForCausalLM - ), f"Expected a CohereForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward, maybe_model) - return maybe_model - - modeling_cohere.CohereForCausalLM.forward = cce_forward - return None - - -def patch_cohere2( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - from transformers.models.cohere2 import modeling_cohere2 - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_cohere2.Cohere2ForCausalLM - ), f"Expected a Cohere2ForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward, maybe_model) - return maybe_model - - modeling_cohere2.Cohere2ForCausalLM.forward = cce_forward - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma.py deleted file mode 100644 index ae3d8c6ef3..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Gemma CCE patch""" - -# This patch is based off transformers 4.50.0. - -# pylint: disable=duplicate-code - -from types import MethodType -from typing import Optional, Tuple, Union - -import torch -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, - apply_lce, -) -from transformers.cache_utils import Cache -from transformers.modeling_outputs import CausalLMOutputWithPast -from transformers.models.gemma.modeling_gemma import ( - KwargsForCausalLM, -) -from transformers.processing_utils import Unpack -from transformers.utils.deprecation import deprecate_kwarg - -_PATCH_OPTS: PatchOptions | None = None - - -@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -def cce_forward( - self, - input_ids: torch.LongTensor | None = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[Union[Cache, list[torch.FloatTensor]]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - **kwargs: Unpack[KwargsForCausalLM], -) -> Union[Tuple, CausalLMOutputWithPast]: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - Returns: - - Example: - - ```python - >>> from transformers import AutoTokenizer, GemmaForCausalLM - - >>> model = GemmaForCausalLM.from_pretrained("google/gemma-7b") - >>> tokenizer = AutoTokenizer.from_pretrained("google/gemma-7b") - - >>> prompt = "What is your favorite condiment?" - >>> inputs = tokenizer(prompt, return_tensors="pt") - - >>> # Generate - >>> generate_ids = model.generate(inputs.input_ids, max_length=30) - >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "What is your favorite condiment?" - ```""" - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - - # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) - outputs = self.model( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - **kwargs, - ) - - hidden_states = outputs[0] - loss = None - logits = None - - # Only compute necessary logits, and do not upcast them to float if we are not computing the loss - slice_indices = ( - slice(-logits_to_keep, None) - if isinstance(logits_to_keep, int) - else logits_to_keep - ) - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states[:, slice_indices, :], - self.lm_head.weight, - labels, - _PATCH_OPTS, - **kwargs, - ) - else: - logits = self.lm_head(hidden_states[:, slice_indices, :]) - if labels is not None: - loss = self.loss_function( - logits=logits, - labels=labels, - vocab_size=self.config.vocab_size, - **kwargs, - ) - - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return CausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - ) - - -def patch_gemma( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - from transformers.models.gemma import modeling_gemma - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_gemma.GemmaForCausalLM - ), f"Expected a GemmaForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward, maybe_model) - return maybe_model - - modeling_gemma.GemmaForCausalLM.forward = cce_forward - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py deleted file mode 100644 index 644e5cce7f..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/gemma3.py +++ /dev/null @@ -1,447 +0,0 @@ -"""Gemma2 and Gemma3 (text and multimodal) CCE patch.""" - -# Implementation originally adapted from https://github.com/apple/ml-cross-entropy/pull/29 -# and updated for transformers 4.50.0. -# This is a modified version of the patch that allows for deferred logits calculation for gemma3 and works -# with both gemma3 (text and multimodal) models. - -# pylint: disable=duplicate-code - -from types import MethodType -from typing import Optional, Tuple, Union - -import torch -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, -) -from torch import nn -from transformers.cache_utils import Cache, HybridCache -from transformers.modeling_outputs import CausalLMOutputWithPast -from transformers.models.gemma3.modeling_gemma3 import ( - Gemma3CausalLMOutputWithPast, - logger, -) -from transformers.utils import ( - is_torchdynamo_compiling, -) -from transformers.utils.deprecation import deprecate_kwarg - -from axolotl.integrations.cut_cross_entropy.monkeypatch.utils import apply_lce - -_PATCH_OPTS: PatchOptions | None = None - - -@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -def cce_forward( - self, - input_ids: torch.LongTensor | None = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[HybridCache] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - defer_logits_calculation: bool = False, - **loss_kwargs, -) -> Union[Tuple, CausalLMOutputWithPast]: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - defer_logits_calculation (`bool`, *optional*): - If `True`, defer logits calculation to the ConditionalGeneration forward. This is used to avoid the - memory overhead of calculating logits using regular lm_head forward pass and to use CCE. - - Returns: - - Example: - - ```python - >>> from transformers import AutoTokenizer, Gemma3ForCausalLM - - >>> model = Gemma3ForCausalLM.from_pretrained("google/gemma-2-9b") - >>> tokenizer = AutoTokenizer.from_pretrained("google/gemma-2-9b") - - >>> prompt = "What is your favorite condiment?" - >>> inputs = tokenizer(prompt, return_tensors="pt") - - >>> # Generate - >>> generate_ids = model.generate(inputs.input_ids, max_length=30) - >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "What is your favorite condiment?" - ```""" - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) - outputs = self.model( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - **loss_kwargs, - ) - - hidden_states = outputs[0] - loss = None - logits = None - - # Only compute necessary logits, and do not upcast them to float if we are not computing the loss - slice_indices = ( - slice(-logits_to_keep, None) - if isinstance(logits_to_keep, int) - else logits_to_keep - ) - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states[:, slice_indices, :], - self.lm_head.weight, - labels, - _PATCH_OPTS, - softcap=getattr(self.config, "final_logit_softcapping", None), - **loss_kwargs, - ) - elif _PATCH_OPTS is not None and defer_logits_calculation: - # defer logits calculation to the ConditionalGeneration forward - logits = hidden_states[:, slice_indices, :] - else: - logits = self.lm_head(hidden_states[:, slice_indices, :]) - if self.config.final_logit_softcapping is not None: - logits = logits / self.config.final_logit_softcapping - logits = torch.tanh(logits) - logits = logits * self.config.final_logit_softcapping - - if labels is not None: - loss = self.loss_function(logits, labels, self.vocab_size, **loss_kwargs) - - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return CausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - ) - - -@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -def cce_forward_multimodal( - self, - input_ids: torch.LongTensor | None = None, - pixel_values: torch.FloatTensor | None = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[Union[list[torch.FloatTensor], Cache]] = None, - token_type_ids: Optional[torch.LongTensor] = None, - cache_position: Optional[torch.LongTensor] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - **lm_kwargs, -) -> Union[Tuple, Gemma3CausalLMOutputWithPast]: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.text_config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.text_config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - Returns: - - Example: - - ```python - >>> from PIL import Image - >>> import requests - >>> from transformers import AutoProcessor, Gemma3ForConditionalGeneration - - >>> model = Gemma3ForConditionalGeneration.from_pretrained("google/Gemma3-test-224px-hf") - >>> processor = AutoProcessor.from_pretrained("google/Gemma3-test-224px-hf") - - >>> prompt = "answer en Where is the cow standing?" - >>> url = "https://huggingface.co/gv-hf/Gemma3-test-224px-hf/resolve/main/cow_beach_1.png" - >>> image = Image.open(requests.get(url, stream=True).raw) - - >>> inputs = processor(images=image, text=prompt, return_tensors="pt") - - >>> # Generate - >>> generate_ids = model.generate(**inputs, max_length=30) - >>> processor.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "answer en Where is the cow standing?\nbeach" - ```""" - - if (input_ids is None) ^ (inputs_embeds is not None): - raise ValueError("You must specify exactly one of input_ids or inputs_embeds") - - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - - is_training = token_type_ids is not None and labels is not None - - # Replace image id woth PAD if the image token if OOV, to avoid index-errors - if input_ids is not None and self.config.image_token_index >= self.vocab_size: - special_image_mask = input_ids == self.config.image_token_index - llm_input_ids = input_ids.clone() - llm_input_ids[special_image_mask] = 0 - else: - llm_input_ids = input_ids # type: ignore - - if inputs_embeds is None: - inputs_embeds = self.get_input_embeddings()(llm_input_ids) - - if cache_position is None: - past_seen_tokens = ( - past_key_values.get_seq_length() if past_key_values is not None else 0 # type: ignore - ) - cache_position = torch.arange( # type: ignore - past_seen_tokens, - past_seen_tokens + inputs_embeds.shape[1], - device=inputs_embeds.device, - ) - - # Merge text and images - if pixel_values is not None: - image_features = self.get_image_features(pixel_values) - - if input_ids is None: - special_image_mask = inputs_embeds == self.get_input_embeddings()( - torch.tensor( - self.config.image_token_index, - dtype=torch.long, - device=inputs_embeds.device, - ) - ) - else: - special_image_mask = (input_ids == self.config.image_token_index).unsqueeze( - -1 - ) - special_image_mask = special_image_mask.expand_as(inputs_embeds).to( - inputs_embeds.device - ) - - if ( - not is_torchdynamo_compiling() - and inputs_embeds[special_image_mask].numel() != image_features.numel() - ): - image_tokens_in_text = (special_image_mask).sum(dim=1).sum(dim=0)[0] - raise ValueError( - f"Number of images does not match number of special image tokens in the input text. " - f"Got {image_tokens_in_text} image tokens in the text but {image_features.shape[0] * image_features.shape[1]} " - "tokens from image embeddings." - ) - image_features = image_features.to(inputs_embeds.device, inputs_embeds.dtype) - inputs_embeds = inputs_embeds.masked_scatter(special_image_mask, image_features) # type: ignore - - # mask out pad-token-ids in labels for BC - if labels is not None and self.pad_token_id in labels: - logger.warning_once( - "`labels` contains `pad_token_id` which will be masked with `config.ignore_index`. " - "You have to mask out `pad_token_id` when preparing `labels`, this behavior will be removed in v.4.46.", - ) - labels = torch.where( # type: ignore - input_ids == self.pad_token_id, self.config.ignore_index, labels - ) - - causal_mask = self._update_causal_mask( # pylint: disable=protected-access - attention_mask, - token_type_ids, - past_key_values, - cache_position, - inputs_embeds, - is_training, - ) - outputs = self.language_model( - attention_mask=causal_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - logits_to_keep=logits_to_keep, - defer_logits_calculation=True, # enable deferred logits calculation - **lm_kwargs, - ) - - hidden_states = outputs[0] - loss = None - logits = None - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states, - self.language_model.lm_head.weight, - labels, - _PATCH_OPTS, - softcap=getattr(self.config, "final_logit_softcapping", None), - **lm_kwargs, - ) - else: - logits = hidden_states - if labels is not None: - # Upcast to float if we need to compute the loss to avoid potential precision issues - logits = logits.float() - shift_logits = logits[..., :-1, :] - shift_labels = labels[..., 1:] - if attention_mask is not None: - # we use the input attention mask to shift the logits and labels, because it is 2D. - # we also crop attn mask in case it is longer, which happens in PrefixTuning with peft - shift_attention_mask = attention_mask[:, -shift_logits.shape[1] :].to( - logits.device - ) - shift_logits = shift_logits[ - shift_attention_mask.to(logits.device) != 0 - ].contiguous() - shift_labels = shift_labels[ - shift_attention_mask.to(shift_labels.device) != 0 - ].contiguous() - else: - shift_logits = shift_logits.contiguous() - shift_labels = shift_labels.contiguous() - # Flatten the tokens - loss_fct = nn.CrossEntropyLoss() - - flat_logits = shift_logits.view(-1, self.config.text_config.vocab_size) - flat_labels = shift_labels.view(-1).to(shift_logits.device) - loss = loss_fct(flat_logits, flat_labels) - - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return Gemma3CausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - image_hidden_states=image_features if pixel_values is not None else None, - ) - - -def patch_gemma2( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - from transformers.models.gemma2 import modeling_gemma2 - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_gemma2.Gemma2ForCausalLM - ), f"Expected a Gemma2ForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward, maybe_model) - return maybe_model - - modeling_gemma2.Gemma2ForCausalLM.forward = cce_forward - return None - - -def patch_gemma3_text( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - from transformers.models.gemma3 import modeling_gemma3 - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_gemma3.Gemma3ForCausalLM - ), f"Expected a Gemma3ForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward, maybe_model) - return maybe_model - - modeling_gemma3.Gemma3ForCausalLM.forward = cce_forward - return None - - -def patch_gemma3( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - from transformers.models.gemma3 import modeling_gemma3 - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_gemma3.Gemma3ForConditionalGeneration - ), f"Expected a Gemma3ForConditionalGeneration model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward_multimodal, maybe_model) - - # patch the causal model to enable deferred logits calculation - maybe_model.language_model.forward = MethodType( - cce_forward, maybe_model.language_model - ) - return maybe_model - - modeling_gemma3.Gemma3ForConditionalGeneration.forward = cce_forward_multimodal - # patch the causal model to enable deferred logits calculation - modeling_gemma3.Gemma3ForCausalLM.forward = cce_forward - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/glm4.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/glm4.py deleted file mode 100644 index 3df909f884..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/glm4.py +++ /dev/null @@ -1,57 +0,0 @@ -"""GLM 4 patch. GLM family inherits from Llama.""" - -from types import MethodType - -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, -) - - -def patch_glm( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - - # Set the _PATCH_OPTS in the llama patch file - import cut_cross_entropy.transformers.llama as llama_patch - - llama_patch._PATCH_OPTS = patch_options # pylint: disable=protected-access - - from cut_cross_entropy.transformers.llama import cce_forward - from transformers.models.glm import modeling_glm - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_glm.GlmForCausalLM - ), f"Expected a GlmForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward, maybe_model) - return maybe_model - - modeling_glm.GlmForCausalLM.forward = cce_forward - return None - - -def patch_glm4( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - - # Set the _PATCH_OPTS in the llama patch file - import cut_cross_entropy.transformers.llama as llama_patch - - llama_patch._PATCH_OPTS = patch_options # pylint: disable=protected-access - - from cut_cross_entropy.transformers.llama import cce_forward - from transformers.models.glm4 import modeling_glm4 - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_glm4.Glm4ForCausalLM - ), f"Expected a Glm4ForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward, maybe_model) - return maybe_model - - modeling_glm4.Glm4ForCausalLM.forward = cce_forward - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama.py deleted file mode 100644 index bed411ace3..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Llama CCE patch. Adapted from transformers v4.51.2""" - -# pylint: disable=duplicate-code - - -from types import MethodType -from typing import Optional, Union - -import torch -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, - apply_lce, -) -from transformers.cache_utils import Cache -from transformers.modeling_outputs import ( - BaseModelOutputWithPast, - CausalLMOutputWithPast, -) -from transformers.models.llama.modeling_llama import ( - KwargsForCausalLM, -) -from transformers.processing_utils import Unpack -from transformers.utils.deprecation import deprecate_kwarg -from transformers.utils.generic import can_return_tuple - -_PATCH_OPTS: PatchOptions | None = None - - -@can_return_tuple -@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -def cce_forward( - self, - input_ids: Optional[torch.LongTensor] = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[Cache] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - **kwargs: Unpack[KwargsForCausalLM], -) -> CausalLMOutputWithPast: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - Returns: - - Example: - - ```python - >>> from transformers import AutoTokenizer, LlamaForCausalLM - - >>> model = LlamaForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf") - >>> tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf") - - >>> prompt = "Hey, are you conscious? Can you talk to me?" - >>> inputs = tokenizer(prompt, return_tensors="pt") - - >>> # Generate - >>> generate_ids = model.generate(inputs.input_ids, max_length=30) - >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." - ```""" - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - - # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) - outputs: BaseModelOutputWithPast = self.model( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - cache_position=cache_position, - **kwargs, - ) - - hidden_states = outputs.last_hidden_state - if hidden_states is None: - raise ValueError("hidden_states is None") - - loss = None - logits = None - - # Only compute necessary logits, and do not upcast them to float if we are not computing the loss - slice_indices = ( - slice(-logits_to_keep, None) - if isinstance(logits_to_keep, int) - else logits_to_keep - ) - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states[:, slice_indices, :], - self.lm_head.weight, - labels, - _PATCH_OPTS, - **kwargs, - ) - else: - logits = self.lm_head(hidden_states[:, slice_indices, :]) - - if labels is not None: - loss = self.loss_function( - logits=logits, - labels=labels, - vocab_size=self.config.vocab_size, - **kwargs, - ) - - return CausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - ) - - -def patch_llama( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - """Patch Llama for CCE.""" - global _PATCH_OPTS # pylint: disable=global-statement - from transformers.models.llama import modeling_llama - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_llama.LlamaForCausalLM - ), f"Expected a LlamaForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward, maybe_model) - return maybe_model - - modeling_llama.LlamaForCausalLM.forward = cce_forward - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py deleted file mode 100644 index 3143e9c8da..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/llama4.py +++ /dev/null @@ -1,401 +0,0 @@ -"""Llama4 CCE patch. Adapted from transformers 4.51.0.""" - -# pylint: disable=duplicate-code - -from types import MethodType -from typing import Optional, Tuple, Union - -import torch -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, - apply_lce, -) -from torch import nn -from transformers.cache_utils import Cache -from transformers.modeling_outputs import CausalLMOutputWithPast -from transformers.models.llama4.modeling_llama4 import ( - Llama4CausalLMOutputWithPast, -) - -_PATCH_OPTS: PatchOptions | None = None - - -def cce_forward( - self, - input_ids: torch.LongTensor | None = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[Union[Cache, list[torch.FloatTensor]]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - defer_logits_calculation: bool = False, - **kwargs, -) -> Union[Tuple, CausalLMOutputWithPast]: - r""" - Args: - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - defer_logits_calculation (`bool`, *optional*, defaults to `False`): - If `True`, defer logits calculation to the ConditionalGeneration forward. This is used to avoid the - memory overhead of calculating logits using regular lm_head forward pass and to use CCE. - - Returns: - - Example: - - ```python - >>> from transformers import AutoTokenizer, Llama4ForCausalLM - - >>> model = Llama4ForCausalLM.from_pretrained("meta-llama4/Llama4-2-7b-hf") - >>> tokenizer = AutoTokenizer.from_pretrained("meta-llama4/Llama4-2-7b-hf") - - >>> prompt = "Hey, are you conscious? Can you talk to me?" - >>> inputs = tokenizer(prompt, return_tensors="pt") - - >>> # Generate - >>> generate_ids = model.generate(inputs.input_ids, max_length=30) - >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." - ```""" - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - - # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) - outputs = self.model( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - **kwargs, - ) - - hidden_states = outputs[0] - loss = None - logits = None - - # Only compute necessary logits, and do not upcast them to float if we are not computing the loss - slice_indices = ( - slice(-logits_to_keep, None) - if isinstance(logits_to_keep, int) - else logits_to_keep - ) - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states[:, slice_indices, :], - self.lm_head.weight, - labels, - _PATCH_OPTS, - **kwargs, - ) - elif _PATCH_OPTS is not None and defer_logits_calculation: - # defer logits calculation to the ConditionalGeneration forward - logits = hidden_states[:, slice_indices, :] - else: - logits = self.lm_head(hidden_states[:, slice_indices, :]) - - if labels is not None: - loss = self.loss_function( - logits=logits, - labels=labels, - vocab_size=self.config.vocab_size, - **kwargs, - ) - - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return CausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - ) - - -def cce_forward_multimodal( - self, - input_ids: torch.LongTensor | None = None, # type: ignore - pixel_values: torch.FloatTensor | None = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[list[torch.FloatTensor]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - vision_feature_layer: Optional[Union[int, list[int]]] = None, - vision_feature_select_strategy: Optional[str] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - image_sizes: torch.Tensor | None = None, - **lm_kwargs, -) -> Union[Tuple, Llama4CausalLMOutputWithPast]: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - - Returns: - - Example: - - ```python - >>> from PIL import Image - >>> import requests - >>> from transformers import AutoProcessor, LlavaForConditionalGeneration - - >>> model = LlavaForConditionalGeneration.from_pretrained("llava-hf/llava-1.5-7b-hf") - >>> processor = AutoProcessor.from_pretrained("llava-hf/llava-1.5-7b-hf") - - >>> prompt = "USER: \nWhat's the content of the image? ASSISTANT:" - >>> url = "https://www.ilankelman.org/stopsigns/australia.jpg" - >>> image = Image.open(requests.get(url, stream=True).raw) - - >>> inputs = processor(images=image, text=prompt, return_tensors="pt") - - >>> # Generate - >>> generate_ids = model.generate(**inputs, max_new_tokens=15) - >>> processor.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "USER: \nWhat's the content of the image? ASSISTANT: The image features a busy city street with a stop sign prominently displayed" - ```""" - - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - vision_feature_layer = ( - vision_feature_layer - if vision_feature_layer is not None - else self.config.vision_config.vision_feature_layer - ) - vision_feature_select_strategy = ( - vision_feature_select_strategy - if vision_feature_select_strategy is not None - else self.config.vision_config.vision_feature_select_strategy - ) - - if (input_ids is None) ^ (inputs_embeds is not None): - raise ValueError("You must specify exactly one of input_ids or inputs_embeds") - - if pixel_values is not None and inputs_embeds is not None: - raise ValueError( - "You cannot specify both pixel_values and inputs_embeds at the same time, and must specify either one" - ) - - if inputs_embeds is None: - inputs_embeds = self.get_input_embeddings()(input_ids) # type: ignore - - if pixel_values is not None: - image_features = self.get_image_features( - pixel_values=pixel_values, - vision_feature_layer=vision_feature_layer, - vision_feature_select_strategy=vision_feature_select_strategy, - image_sizes=image_sizes, - ) - original_inputs_embeds_shape = inputs_embeds.shape # type: ignore - - vision_flat = image_features.view(-1, image_features.size(-1)) - projected_vision_flat = self.multi_modal_projector(vision_flat) - - special_image_mask = (input_ids == self.config.image_token_index).unsqueeze(-1) - final_mask = special_image_mask.to(inputs_embeds.device) # type: ignore - inputs_embeds = inputs_embeds.view(-1, inputs_embeds.size(-1)) # type: ignore - - final_mask_1d = final_mask[..., 0].reshape(-1) - num_tokens_to_fill = final_mask_1d.sum() - - if num_tokens_to_fill != projected_vision_flat.size(0): - raise ValueError( - f"Mismatch: final_mask wants {num_tokens_to_fill} embeddings, " - f"but multi_modal_projector returned {projected_vision_flat.size(0)}" - ) - - expanded_mask = final_mask_1d.unsqueeze(-1).expand(-1, inputs_embeds.size(-1)) - inputs_embeds = inputs_embeds.masked_scatter( - expanded_mask, projected_vision_flat - ) # type: ignore - inputs_embeds = inputs_embeds.view(original_inputs_embeds_shape) # type: ignore - - outputs = self.language_model( - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - logits_to_keep=logits_to_keep, - defer_logits_calculation=True, # enable deferred logits calculation - **lm_kwargs, - ) - - hidden_states = outputs[0] - loss = None - logits = None - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - # TODO: check if need to handle attention_mask - loss = apply_lce( - hidden_states, - self.language_model.lm_head.weight, - labels, - _PATCH_OPTS, - **lm_kwargs, - ) - else: - logits = hidden_states - if labels is not None: - # Shift so that tokens < n predict n - if attention_mask is not None: - # we use the input attention mask to shift the logits and labels, because it is 2D. - # we also crop attn mask in case it is longer, which happens in PrefixTuning with peft - shift_attention_mask = attention_mask[:, -(logits.shape[1] - 1) :].to( - logits.device - ) - shift_logits = logits[..., :-1, :][ - shift_attention_mask.to(logits.device) != 0 - ].contiguous() - shift_labels = labels[..., 1:][ - shift_attention_mask.to(labels.device) != 0 - ].contiguous() - else: - shift_logits = logits[..., :-1, :].contiguous() - shift_labels = labels[..., 1:].contiguous() - # Flatten the tokens - loss_fct = nn.CrossEntropyLoss() - loss = loss_fct( - shift_logits.view(-1, shift_logits.size(-1)), - shift_labels.view(-1).to(shift_logits.device), - ) - - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return Llama4CausalLMOutputWithPast( - loss=loss, - logits=logits, # type: ignore # TODO: check if need to create dummy logits - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - image_hidden_states=image_features if pixel_values is not None else None, - ) - - -def patch_llama4_text( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - from transformers.models.llama4 import modeling_llama4 - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_llama4.Llama4ForCausalLM - ), f"Expected a Llama4ForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward, maybe_model) - - return maybe_model - - setattr( - modeling_llama4.Llama4ForCausalLM, - "forward", - cce_forward, - ) - return None - - -def patch_llama4( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - - global _PATCH_OPTS # pylint: disable=global-statement - from transformers.models.llama4 import modeling_llama4 - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_llama4.Llama4ForConditionalGeneration - ), f"Expected a Llama4ForConditionalGeneration model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward_multimodal, maybe_model) - - # patch the language model - maybe_model.language_model.forward = MethodType( - cce_forward, maybe_model.language_model - ) - return maybe_model - - setattr( - modeling_llama4.Llama4ForConditionalGeneration, - "forward", - cce_forward_multimodal, - ) - - # patch the causal language model - setattr(modeling_llama4.Llama4ForCausalLM, "forward", cce_forward) - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mistral3.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mistral3.py deleted file mode 100644 index aa252701ec..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mistral3.py +++ /dev/null @@ -1,384 +0,0 @@ -"""Mistral and Mistral3 CCE patch.""" - -# pylint: disable=duplicate-code - -from types import MethodType -from typing import Optional, Tuple, Union - -import torch -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, - apply_lce, -) -from torch import nn -from transformers.cache_utils import Cache -from transformers.modeling_outputs import CausalLMOutputWithPast -from transformers.models.mistral3.modeling_mistral3 import ( - Mistral3CausalLMOutputWithPast, -) -from transformers.models.mistral.modeling_mistral import ( - KwargsForCausalLM, -) -from transformers.processing_utils import Unpack -from transformers.utils import ( - is_torchdynamo_compiling, -) -from transformers.utils.deprecation import deprecate_kwarg - -_PATCH_OPTS: PatchOptions | None = None - - -@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -def cce_forward( - self, - input_ids: torch.LongTensor | None = None, - attention_mask: Optional[torch.Tensor] | None = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[Union[Cache, list[torch.FloatTensor]]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - defer_logits_calculation: bool = False, - **kwargs: Unpack[KwargsForCausalLM], -) -> Union[Tuple, CausalLMOutputWithPast]: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - defer_logits_calculation (`bool`, *optional*): - If `True`, defer logits calculation to the ConditionalGeneration forward. This is used to avoid the - memory overhead of calculating logits using regular lm_head forward pass and to use CCE. - - Returns: - - Example: - - ```python - >>> from transformers import AutoTokenizer, MistralForCausalLM - - >>> model = MistralForCausalLM.from_pretrained("meta-mistral/Mistral-2-7b-hf") - >>> tokenizer = AutoTokenizer.from_pretrained("meta-mistral/Mistral-2-7b-hf") - - >>> prompt = "Hey, are you conscious? Can you talk to me?" - >>> inputs = tokenizer(prompt, return_tensors="pt") - - >>> # Generate - >>> generate_ids = model.generate(inputs.input_ids, max_length=30) - >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." - ```""" - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - - # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) - outputs = self.model( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - **kwargs, - ) - - hidden_states = outputs[0] - loss = None - logits = None - - # Only compute necessary logits, and do not upcast them to float if we are not computing the loss - slice_indices = ( - slice(-logits_to_keep, None) - if isinstance(logits_to_keep, int) - else logits_to_keep - ) - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states[:, slice_indices, :], - self.lm_head.weight, - labels, - _PATCH_OPTS, - **kwargs, - ) - elif _PATCH_OPTS is not None and defer_logits_calculation: - # defer logits calculation to the ConditionalGeneration forward - logits = hidden_states[:, slice_indices, :] - else: - logits = self.lm_head(hidden_states[:, slice_indices, :]) - if labels is not None: - loss = self.loss_function( - logits=logits, - labels=labels, - vocab_size=self.config.vocab_size, - **kwargs, - ) - - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return CausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - ) - - -def cce_forward_multimodal( - self, - input_ids: torch.LongTensor | None = None, - pixel_values: torch.FloatTensor | None = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[list[torch.FloatTensor]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - vision_feature_layer: Optional[Union[int, list[int]]] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - image_sizes: torch.Tensor | None = None, - **lm_kwargs, -) -> Union[Tuple, Mistral3CausalLMOutputWithPast]: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - - Returns: - - Example: - - ```python - >>> from PIL import Image - >>> import requests - >>> from transformers import AutoProcessor, Mistral3ForConditionalGeneration - - >>> model = Mistral3ForConditionalGeneration.from_pretrained("mistralai/Mistral-Small-3.1-24B-Instruct-2503") - >>> processor = AutoProcessor.from_pretrained("mistralai/Mistral-Small-3.1-24B-Instruct-2503") - - >>> prompt = "[INST][IMG]What is the image?[/INST]" - >>> url = "http://images.cocodataset.org/val2017/000000039769.jpg" - >>> image = Image.open(requests.get(url, stream=True).raw) - - >>> inputs = processor(images=image, text=prompt, return_tensors="pt") - - >>> # Generate - >>> generate_ids = model.generate(**inputs, max_new_tokens=15) - >>> processor.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "What is the image?The image depicts two cats lying on a pink blanket." - ```""" - - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - vision_feature_layer = ( - vision_feature_layer - if vision_feature_layer is not None - else self.config.vision_feature_layer - ) - - if (input_ids is None) ^ (inputs_embeds is not None): - raise ValueError("You must specify exactly one of input_ids or inputs_embeds") - - if pixel_values is not None and inputs_embeds is not None: - raise ValueError( - "You cannot specify both pixel_values and inputs_embeds at the same time, and must specify either one" - ) - - if inputs_embeds is None: - inputs_embeds = self.get_input_embeddings()(input_ids) - - if pixel_values is not None: - image_features = self.get_image_features( - pixel_values=pixel_values, - vision_feature_layer=vision_feature_layer, - image_sizes=image_sizes, - ) - - special_image_mask = (input_ids == self.config.image_token_index).unsqueeze(-1) - special_image_mask = special_image_mask.expand_as(inputs_embeds).to( - inputs_embeds.device - ) - if ( - not is_torchdynamo_compiling() - and inputs_embeds[special_image_mask].numel() != image_features.numel() - ): - n_image_tokens = (input_ids == self.config.image_token_index).sum() - n_image_features = image_features.shape[0] * image_features.shape[1] - raise ValueError( - f"Image features and image tokens do not match: tokens: {n_image_tokens}, features {n_image_features}" - ) - image_features = image_features.to(inputs_embeds.device, inputs_embeds.dtype) - inputs_embeds = inputs_embeds.masked_scatter(special_image_mask, image_features) # type: ignore - - outputs = self.language_model( - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - logits_to_keep=logits_to_keep, - defer_logits_calculation=True, # enable deferred logits calculation - **lm_kwargs, - ) - - hidden_states = outputs[0] - loss = None - logits = None - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states, - self.language_model.lm_head.weight, - labels, - _PATCH_OPTS, - **lm_kwargs, - ) - else: - logits = hidden_states - if labels is not None: - # Shift so that tokens < n predict n - if attention_mask is not None: - # we use the input attention mask to shift the logits and labels, because it is 2D. - # we also crop attn mask in case it is longer, which happens in PrefixTuning with peft - shift_attention_mask = attention_mask[:, -(logits.shape[1] - 1) :].to( - logits.device - ) - shift_logits = logits[..., :-1, :][ - shift_attention_mask.to(logits.device) != 0 - ].contiguous() - shift_labels = labels[..., 1:][ - shift_attention_mask.to(labels.device) != 0 - ].contiguous() - else: - shift_logits = logits[..., :-1, :].contiguous() - shift_labels = labels[..., 1:].contiguous() - # Flatten the tokens - loss_fct = nn.CrossEntropyLoss() - loss = loss_fct( - shift_logits.view(-1, shift_logits.size(-1)), - shift_labels.view(-1).to(shift_logits.device), - ) - - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return Mistral3CausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - image_hidden_states=image_features if pixel_values is not None else None, - ) - - -def patch_mistral( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - from transformers.models.mistral import modeling_mistral - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_mistral.MistralForCausalLM - ), f"Expected a MistralForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward, maybe_model) - return maybe_model - - modeling_mistral.MistralForCausalLM.forward = cce_forward - return None - - -def patch_mistral3( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - from transformers.models.mistral import modeling_mistral - from transformers.models.mistral3 import modeling_mistral3 - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_mistral3.Mistral3ForConditionalGeneration - ), f"Expected a Mistral3ForConditionalGeneration model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward_multimodal, maybe_model) - - # patch the causal model to enable deferred logits calculation - maybe_model.language_model.forward = MethodType( - cce_forward, maybe_model.language_model - ) - return maybe_model - - modeling_mistral3.Mistral3ForConditionalGeneration.forward = cce_forward_multimodal - # patch the causal model to enable deferred logits calculation - modeling_mistral.MistralForCausalLM.forward = cce_forward - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mllama.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mllama.py deleted file mode 100644 index e82853e6cb..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/mllama.py +++ /dev/null @@ -1,366 +0,0 @@ -"""Mllama CCE patch.""" - -# pylint: disable=duplicate-code - -from types import MethodType -from typing import Optional, Tuple, Union - -import torch -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, - apply_lce, -) -from transformers.cache_utils import Cache -from transformers.modeling_outputs import CausalLMOutputWithPast -from transformers.models.mllama.modeling_mllama import ( - _prepare_cross_attention_mask, -) -from transformers.utils.deprecation import deprecate_kwarg - -_PATCH_OPTS: PatchOptions | None = None - - -@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -def cce_forward( - self, - input_ids: torch.LongTensor | None = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - cross_attention_states: Optional[torch.LongTensor] = None, - cross_attention_mask: Optional[torch.LongTensor] = None, - full_text_row_masked_out_mask: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, - past_key_values: Optional[Union[Cache, list[torch.FloatTensor]]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - defer_logits_calculation: bool = False, - **loss_kwargs, -) -> Union[Tuple, CausalLMOutputWithPast]: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - defer_logits_calculation (`bool`, *optional*): - If `True`, defer logits calculation to the ConditionalGeneration forward. This is used to avoid the - memory overhead of calculating logits using regular lm_head forward pass and to use CCE. - - Returns: - - Example: - - ```python - >>> from transformers import AutoTokenizer, MllamaForCausalLM - - >>> model = MllamaForCausalLM.from_pretrained("Llama-3.2-11B-Vision") - >>> tokenizer = AutoTokenizer.from_pretrained("Llama-3.2-11B-Vision") - - >>> prompt = "If I had to write a haiku, it would be:" - >>> inputs = tokenizer(prompt, return_tensors="pt") - - >>> # Generate - >>> generate_ids = model.generate(inputs.input_ids, max_length=40, do_sample=True, temperature=0.6) - >>> result = tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - >>> print(result) - If I had to write a haiku, it would be: "Snowflakes gently fall" - simple, yet peaceful. - I love the idea of snowflakes gently falling, each one - ``` - """ - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - - # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) - outputs = self.model( - input_ids=input_ids, - cross_attention_states=cross_attention_states, - attention_mask=attention_mask, - position_ids=position_ids, - cross_attention_mask=cross_attention_mask, - full_text_row_masked_out_mask=full_text_row_masked_out_mask, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - ) - - hidden_states = outputs[0] - loss = None - logits = None - - slice_indices = ( - slice(-logits_to_keep, None) - if isinstance(logits_to_keep, int) - else logits_to_keep - ) - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states[:, slice_indices, :], - self.lm_head.weight, - labels, - _PATCH_OPTS, - **loss_kwargs, - ) - elif _PATCH_OPTS is not None and defer_logits_calculation: - # defer logits calculation to the ConditionalGeneration forward - logits = hidden_states[:, slice_indices, :] - else: - logits = self.lm_head(hidden_states[:, slice_indices, :]).float() - - loss = None - if labels is not None: - loss = self.loss_function(logits, labels, self.vocab_size, **loss_kwargs) - - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return CausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - ) - - -@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -def cce_forward_multimodal( - self, - input_ids: Optional[torch.LongTensor] = None, - pixel_values: Optional[torch.FloatTensor] = None, - aspect_ratio_mask: Optional[torch.Tensor] = None, - aspect_ratio_ids: Optional[torch.Tensor] = None, - attention_mask: Optional[torch.Tensor] = None, - cross_attention_mask: Optional[torch.Tensor] = None, - cross_attention_states: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[list[torch.FloatTensor]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - **loss_kwargs, -) -> Union[Tuple, CausalLMOutputWithPast]: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - - Returns: - - Example: - - ```python - >>> from PIL import Image - >>> import requests - >>> from transformers import AutoProcessor, MllamaForConditionalGeneration - - >>> checkpoint = "meta-llama/Llama-3.2-11B-Vision" - >>> model = MllamaForConditionalGeneration.from_pretrained(checkpoint) - >>> processor = AutoProcessor.from_pretrained(checkpoint) - - >>> prompt = "<|image|>If I had to write a haiku for this one" - >>> url = "https://www.ilankelman.org/stopsigns/australia.jpg" - >>> image = Image.open(requests.get(url, stream=True).raw) - - >>> inputs = processor(text=prompt, images=image, return_tensors="pt") - - >>> # Generate - >>> output = model.generate(**inputs, max_new_tokens=15) - - >>> prompt_len = inputs.input_ids.shape[-1] - >>> generated_ids = output[:, prompt_len:] - >>> generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False) - >>> print(generated_text) - [', it would be:.\\nA stop sign in Chinatown.\\n'] - ``` - """ - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - - if (input_ids is None) ^ (inputs_embeds is not None): - raise ValueError("You must specify exactly one of input_ids or inputs_embeds") - - if pixel_values is not None and inputs_embeds is not None: - raise ValueError( - "You cannot specify both pixel_values and inputs_embeds at the same time, and must specify either one" - ) - - if pixel_values is not None and cross_attention_states is not None: - raise ValueError( - "`pixel_values` and `cross_attention_states` cannot be provided simultaneously" - ) - - if pixel_values is not None: - if aspect_ratio_ids is None: - raise ValueError( - "`aspect_ratio_ids` must be provided if `pixel_values` is provided" - ) - # get vision tokens from vision model - vision_outputs = self.vision_model( - pixel_values=pixel_values, - aspect_ratio_ids=aspect_ratio_ids, - aspect_ratio_mask=aspect_ratio_mask, - output_hidden_states=output_hidden_states, - output_attentions=output_attentions, - return_dict=return_dict, - ) - cross_attention_states = vision_outputs[0] - cross_attention_states = self.multi_modal_projector( - cross_attention_states - ).reshape( - -1, cross_attention_states.shape[-2], self.hidden_size # type: ignore - ) - - if cross_attention_mask is not None: - cross_attention_mask, full_text_row_masked_out_mask = ( - _prepare_cross_attention_mask( - cross_attention_mask, - num_vision_tokens=self.vision_model.num_patches, - dtype=self.dtype, - ) - ) - else: - full_text_row_masked_out_mask = None - - if cross_attention_mask is not None and cache_position is not None: - cross_attention_mask = cross_attention_mask[:, :, cache_position] - full_text_row_masked_out_mask = full_text_row_masked_out_mask[ - :, :, cache_position - ] - - outputs = self.language_model( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - cross_attention_states=cross_attention_states, - cross_attention_mask=cross_attention_mask, - full_text_row_masked_out_mask=full_text_row_masked_out_mask, - past_key_values=past_key_values, - use_cache=use_cache, - inputs_embeds=inputs_embeds, - output_hidden_states=output_hidden_states, - output_attentions=output_attentions, - return_dict=return_dict, - cache_position=cache_position, - logits_to_keep=logits_to_keep, - defer_logits_calculation=True, # enable deferred logits calculation - **loss_kwargs, - ) - - hidden_states = outputs[0] - loss = None - logits = None - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states, - self.language_model.lm_head.weight, - labels, - _PATCH_OPTS, - **loss_kwargs, - ) - else: - # Temporary fix to calculate the loss in main class, as the model's vocab size may be resized - logits = hidden_states - - if labels is not None: - loss = self.loss_function( - logits, labels, self.config.get_text_config().vocab_size, **loss_kwargs - ) - - if not return_dict: - return (loss,) + outputs if loss is not None else outputs - - return CausalLMOutputWithPast( - loss=loss, - logits=outputs.logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - ) - - -def patch_mllama( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - - global _PATCH_OPTS # pylint: disable=global-statement - from transformers.models.mllama import modeling_mllama - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_mllama.MllamaForConditionalGeneration - ), f"Expected a MllamaForConditionalGeneration model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward_multimodal, maybe_model) - - # patch the language model - maybe_model.language_model.forward = MethodType( - cce_forward, maybe_model.language_model - ) - return maybe_model - - modeling_mllama.MllamaForConditionalGeneration.forward = cce_forward_multimodal - - # patch the causal language model - modeling_mllama.MllamaForCausalLM.forward = cce_forward - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py deleted file mode 100644 index 8176a1f0c7..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/patch.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (C) 2024 Apple Inc. All Rights Reserved. - -"""Cut Cross Entropy patcher""" - -import transformers -from cut_cross_entropy.cce_utils import LinearCrossEntropyImpl -from cut_cross_entropy.linear_cross_entropy import LCE_IMPL_DEFAULT -from cut_cross_entropy.transformers.phi3 import patch_phi3 -from cut_cross_entropy.transformers.utils import PatchOptions, TransformersModelT - -from axolotl.integrations.cut_cross_entropy.monkeypatch.cohere import ( - patch_cohere, - patch_cohere2, -) -from axolotl.integrations.cut_cross_entropy.monkeypatch.gemma import patch_gemma -from axolotl.integrations.cut_cross_entropy.monkeypatch.gemma3 import ( - patch_gemma2, - patch_gemma3, - patch_gemma3_text, -) -from axolotl.integrations.cut_cross_entropy.monkeypatch.glm4 import ( - patch_glm, - patch_glm4, -) -from axolotl.integrations.cut_cross_entropy.monkeypatch.llama import ( - patch_llama, -) -from axolotl.integrations.cut_cross_entropy.monkeypatch.llama4 import ( - patch_llama4, - patch_llama4_text, -) -from axolotl.integrations.cut_cross_entropy.monkeypatch.mistral3 import ( - patch_mistral, - patch_mistral3, -) -from axolotl.integrations.cut_cross_entropy.monkeypatch.mllama import patch_mllama -from axolotl.integrations.cut_cross_entropy.monkeypatch.qwen2 import ( - patch_qwen2, -) -from axolotl.integrations.cut_cross_entropy.monkeypatch.qwen2_5_vl import ( - patch_qwen2_5_vl, -) -from axolotl.integrations.cut_cross_entropy.monkeypatch.qwen2_moe import ( - patch_qwen2_moe, -) -from axolotl.integrations.cut_cross_entropy.monkeypatch.qwen2_vl import ( - patch_qwen2_vl, -) -from axolotl.integrations.cut_cross_entropy.monkeypatch.qwen3 import patch_qwen3 -from axolotl.integrations.cut_cross_entropy.monkeypatch.qwen3_moe import ( - patch_qwen3_moe, -) - -CUT_CROSS_ENTROPY_MODEL_MAPPING = { - "llama": patch_llama, - "llama4": patch_llama4, - "llama4_text": patch_llama4_text, - "mllama": patch_mllama, - "phi3": patch_phi3, - "gemma": patch_gemma, - "gemma2": patch_gemma2, - "gemma3": patch_gemma3, - "gemma3_text": patch_gemma3_text, - "mistral": patch_mistral, - "mistral3": patch_mistral3, - "qwen2": patch_qwen2, - "qwen2_moe": patch_qwen2_moe, - "qwen2_vl": patch_qwen2_vl, - "qwen2_5_vl": patch_qwen2_5_vl, - "qwen3": patch_qwen3, - "qwen3_moe": patch_qwen3_moe, - "cohere": patch_cohere, - "cohere2": patch_cohere2, - "glm": patch_glm, - "glm4": patch_glm4, -} - - -def cce_patch( - model_type_or_model: str | TransformersModelT | transformers.PretrainedConfig, - impl: str | LinearCrossEntropyImpl = LCE_IMPL_DEFAULT, - reduction: str = "mean", - filter_eps: float | str | None = "auto", - accum_e_fp32: bool = False, - accum_c_fp32: bool = False, - filter_e_grad: bool = True, - filter_c_grad: bool = True, - train_only: bool = False, -) -> TransformersModelT | None: - if isinstance(impl, LinearCrossEntropyImpl): - impl = impl.name.lower() - - if impl not in (v.name.lower() for v in LinearCrossEntropyImpl): - raise ValueError(f"Unknown {impl=}") - - if isinstance(model_type_or_model, transformers.PreTrainedModel): - if hasattr(model_type_or_model, "config"): - model_type = getattr( - getattr(model_type_or_model, "config", None), "model_type", None - ) - else: - raise ValueError( - "model_type_or_model is a PreTrainedModel but does not have a config attribute" - ) - elif isinstance(model_type_or_model, transformers.PretrainedConfig): - model_type = model_type_or_model.model_type - else: - model_type = model_type_or_model - - patch_options = PatchOptions( - impl=impl, - reduction=reduction, - filter_eps=filter_eps, - accum_e_fp32=accum_e_fp32, - accum_c_fp32=accum_c_fp32, - filter_e_grad=filter_e_grad, - filter_c_grad=filter_c_grad, - train_only=train_only, - ) - - if model_type in CUT_CROSS_ENTROPY_MODEL_MAPPING: - return CUT_CROSS_ENTROPY_MODEL_MAPPING[model_type]( - model_type_or_model, patch_options - ) - - raise RuntimeError(f"Unknown model type {model_type}") diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2.py deleted file mode 100644 index 3f6d2b3e9b..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Qwen2 CCE patch. The model inherits Llama's modeling code and uses the same forward method.""" - -# pylint: disable=duplicate-code - -from types import MethodType - -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, -) - - -def patch_qwen2( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - from transformers.models.qwen2 import modeling_qwen2 - - # Set the _PATCH_OPTS in the llama patch file - import axolotl.integrations.cut_cross_entropy.monkeypatch.llama as llama_patch - - llama_patch._PATCH_OPTS = patch_options # pylint: disable=protected-access - - from axolotl.integrations.cut_cross_entropy.monkeypatch.llama import ( - cce_forward, - ) - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_qwen2.Qwen2ForCausalLM - ), f"Expected a Qwen2ForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward, maybe_model) - return maybe_model - - modeling_qwen2.Qwen2ForCausalLM.forward = cce_forward - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_5_vl.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_5_vl.py deleted file mode 100644 index 16206006fc..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_5_vl.py +++ /dev/null @@ -1,246 +0,0 @@ -"""Qwen2.5 VL CCE patch. Adapted from transformers v4.51.2""" - -# pylint: disable=duplicate-code - - -from types import MethodType -from typing import Optional, Tuple, Union - -import torch -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, - apply_lce, -) -from torch.nn import CrossEntropyLoss -from transformers.models.qwen2_5_vl.modeling_qwen2_5_vl import ( - Qwen2_5_VLCausalLMOutputWithPast, -) - -_PATCH_OPTS: PatchOptions | None = None - - -def cce_forward_multimodal( - self, - input_ids: Optional[torch.LongTensor] = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[list[torch.FloatTensor]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - pixel_values: Optional[torch.Tensor] = None, - pixel_values_videos: Optional[torch.FloatTensor] = None, - image_grid_thw: Optional[torch.LongTensor] = None, - video_grid_thw: Optional[torch.LongTensor] = None, - rope_deltas: Optional[torch.LongTensor] = None, - cache_position: Optional[torch.LongTensor] = None, - second_per_grid_ts: Optional[torch.Tensor] = None, -) -> Union[Tuple, Qwen2_5_VLCausalLMOutputWithPast]: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - Returns: - - Example: - - ```python - >>> from PIL import Image - >>> import requests - >>> from transformers import AutoProcessor, Qwen2_5_VLForConditionalGeneration - - >>> model = Qwen2_5_VLForConditionalGeneration.from_pretrained("Qwen/Qwen2.5-VL-7B-Instruct") - >>> processor = AutoProcessor.from_pretrained("Qwen/Qwen2.5-VL-7B-Instruct") - - >>> messages = [ - { - "role": "user", - "content": [ - {"type": "image"}, - {"type": "text", "text": "What is shown in this image?"}, - ], - }, - ] - >>> url = "https://www.ilankelman.org/stopsigns/australia.jpg" - >>> image = Image.open(requests.get(url, stream=True).raw) - - >>> text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) - >>> inputs = processor(text=[text], images=[image], vision_infos=[vision_infos]) - - >>> # Generate - >>> generate_ids = model.generate(inputs.input_ids, max_length=30) - >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "The image shows a street scene with a red stop sign in the foreground. In the background, there is a large red gate with Chinese characters ..." - ```""" - - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - - if inputs_embeds is None: - inputs_embeds = self.model.embed_tokens(input_ids) - if pixel_values is not None: - pixel_values = pixel_values.type(self.visual.dtype) - image_embeds = self.visual(pixel_values, grid_thw=image_grid_thw) - n_image_tokens = (input_ids == self.config.image_token_id).sum().item() - n_image_features = image_embeds.shape[0] - if n_image_tokens != n_image_features: - raise ValueError( - f"Image features and image tokens do not match: tokens: {n_image_tokens}, features {n_image_features}" - ) - - mask = input_ids == self.config.image_token_id - mask_unsqueezed = mask.unsqueeze(-1) - mask_expanded = mask_unsqueezed.expand_as(inputs_embeds) - image_mask = mask_expanded.to(inputs_embeds.device) - - image_embeds = image_embeds.to(inputs_embeds.device, inputs_embeds.dtype) - inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds) # type: ignore - - if pixel_values_videos is not None: - pixel_values_videos = pixel_values_videos.type(self.visual.dtype) - video_embeds = self.visual(pixel_values_videos, grid_thw=video_grid_thw) - n_video_tokens = (input_ids == self.config.video_token_id).sum().item() - n_video_features = video_embeds.shape[0] - if n_video_tokens != n_video_features: - raise ValueError( - f"Video features and video tokens do not match: tokens: {n_video_tokens}, features {n_video_features}" - ) - - mask = input_ids == self.config.video_token_id - mask_unsqueezed = mask.unsqueeze(-1) - mask_expanded = mask_unsqueezed.expand_as(inputs_embeds) - video_mask = mask_expanded.to(inputs_embeds.device) - - video_embeds = video_embeds.to(inputs_embeds.device, inputs_embeds.dtype) - inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds) # type: ignore - - if attention_mask is not None: - attention_mask = attention_mask.to(inputs_embeds.device) - - # if we get 4D attention mask we cannot calculate rope deltas anymore. TODO @raushan fixme - if position_ids is None and (attention_mask is None or attention_mask.ndim == 2): - # calculate RoPE index once per generation in the pre-fill stage only - if ( - (cache_position is not None and cache_position[0] == 0) - or self.rope_deltas is None - or (past_key_values is None or past_key_values.get_seq_length() == 0) # type: ignore - ): - position_ids, rope_deltas = self.get_rope_index( - input_ids, - image_grid_thw, - video_grid_thw, - second_per_grid_ts, - attention_mask, - ) - self.rope_deltas = rope_deltas - # then use the prev pre-calculated rope-deltas to get the correct position ids - else: - batch_size, seq_length, _ = inputs_embeds.shape - delta = ( - (cache_position[0] + self.rope_deltas).to(inputs_embeds.device) - if cache_position is not None - else 0 - ) - position_ids = torch.arange(seq_length, device=inputs_embeds.device) # type: ignore - position_ids = position_ids.view(1, -1).expand(batch_size, -1) # type: ignore - if cache_position is not None: # otherwise `deltas` is an int `0` - delta = delta.repeat_interleave(batch_size // delta.shape[0], dim=0) # type: ignore - position_ids = position_ids.add(delta) # type: ignore - position_ids = position_ids.unsqueeze(0).expand(3, -1, -1) # type: ignore - - outputs = self.model( - input_ids=None, - position_ids=position_ids, - attention_mask=attention_mask, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - ) - - hidden_states = outputs[0] - logits = None - loss = None - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states, - self.lm_head.weight, - labels, - _PATCH_OPTS, - ) - else: - logits = self.lm_head(hidden_states) - - if labels is not None: - # Upcast to float if we need to compute the loss to avoid potential precision issues - logits = logits.float() - # Shift so that tokens < n predict n - shift_logits = logits[..., :-1, :].contiguous() - shift_labels = labels[..., 1:].contiguous() - # Flatten the tokens - loss_fct = CrossEntropyLoss() - shift_logits = shift_logits.view(-1, self.config.vocab_size) - shift_labels = shift_labels.view(-1) - # Enable model parallelism - shift_labels = shift_labels.to(shift_logits.device) - loss = loss_fct(shift_logits, shift_labels) - - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return Qwen2_5_VLCausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - rope_deltas=self.rope_deltas, - ) - - -def patch_qwen2_5_vl( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - - from transformers.models.qwen2_5_vl import modeling_qwen2_5_vl - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_qwen2_5_vl.Qwen2_5_VLForConditionalGeneration - ), f"Expected a Qwen2_5_VLForConditionalGeneration model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward_multimodal, maybe_model) - - return maybe_model - - modeling_qwen2_5_vl.Qwen2_5_VLForConditionalGeneration.forward = ( - cce_forward_multimodal - ) - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_moe.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_moe.py deleted file mode 100644 index afe56266e8..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_moe.py +++ /dev/null @@ -1,178 +0,0 @@ -"""Qwen2 MoE CCE patch. Adapted from transformers v4.51.2""" - -# pylint: disable=duplicate-code - -from types import MethodType -from typing import Optional, Union - -import torch -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, - apply_lce, -) -from transformers.models.qwen2_moe.modeling_qwen2_moe import ( - MoeCausalLMOutputWithPast, - MoeModelOutputWithPast, - load_balancing_loss_func, -) -from transformers.utils.deprecation import deprecate_kwarg -from transformers.utils.generic import can_return_tuple - -_PATCH_OPTS: PatchOptions | None = None - - -@can_return_tuple -@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -def forward( - self, - input_ids: Optional[torch.LongTensor] = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[list[torch.FloatTensor]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - output_router_logits: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - **loss_kwargs, -) -> MoeCausalLMOutputWithPast: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - Returns: - - Example: - - ```python - >>> from transformers import AutoTokenizer, Qwen2MoeForCausalLM - - >>> model = Qwen2MoeForCausalLM.from_pretrained(PATH_TO_CONVERTED_WEIGHTS) - >>> tokenizer = AutoTokenizer.from_pretrained(PATH_TO_CONVERTED_TOKENIZER) - - >>> prompt = "Hey, are you conscious? Can you talk to me?" - >>> inputs = tokenizer(prompt, return_tensors="pt") - - >>> # Generate - >>> generate_ids = model.generate(inputs.input_ids, max_length=30) - >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." - ```""" - - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_router_logits = ( - output_router_logits - if output_router_logits is not None - else self.config.output_router_logits - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - - # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) - outputs: MoeModelOutputWithPast = self.model( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - output_router_logits=output_router_logits, - cache_position=cache_position, - ) - - hidden_states = outputs.last_hidden_state - loss = None - logits = None - - if hidden_states is None: - raise ValueError("hidden_states is None") - - # Only compute necessary logits, and do not upcast them to float if we are not computing the loss - slice_indices = ( - slice(-logits_to_keep, None) - if isinstance(logits_to_keep, int) - else logits_to_keep - ) - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states[:, slice_indices, :], - self.lm_head.weight, - labels, - _PATCH_OPTS, - **loss_kwargs, - ) - else: - logits = self.lm_head(hidden_states[:, slice_indices, :]) - - if labels is not None: - loss = self.loss_function(logits, labels, self.vocab_size, **loss_kwargs) - - aux_loss = None - if output_router_logits: - aux_loss = load_balancing_loss_func( - outputs.router_logits, - self.num_experts, - self.num_experts_per_tok, - attention_mask, - ) - if labels is not None: - loss += self.router_aux_loss_coef * aux_loss.to( # type: ignore - loss.device # type: ignore - ) # make sure to reside in the same device - - return MoeCausalLMOutputWithPast( - loss=loss, - aux_loss=aux_loss, # type: ignore - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - router_logits=outputs.router_logits, - ) - - -def patch_qwen2_moe( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - - from transformers.models.qwen2_moe import modeling_qwen2_moe - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_qwen2_moe.Qwen2MoeForCausalLM - ), f"Expected a Qwen3MoeForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(forward, maybe_model) - - return maybe_model - - modeling_qwen2_moe.Qwen2MoeForCausalLM.forward = forward - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_vl.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_vl.py deleted file mode 100644 index 79af01cfad..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen2_vl.py +++ /dev/null @@ -1,239 +0,0 @@ -"""Qwen2 VL CCE patch. Adapted from transformers v4.51.2""" - -# pylint: disable=duplicate-code - -from types import MethodType -from typing import Optional, Tuple, Union - -import torch -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, - apply_lce, -) -from torch.nn import CrossEntropyLoss -from transformers.models.qwen2_vl.modeling_qwen2_vl import ( - Qwen2VLCausalLMOutputWithPast, -) - -_PATCH_OPTS: PatchOptions | None = None - - -def cce_forward_multimodal( - self, - input_ids: Optional[torch.LongTensor] = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[list[torch.FloatTensor]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - pixel_values: Optional[torch.Tensor] = None, - pixel_values_videos: Optional[torch.FloatTensor] = None, - image_grid_thw: Optional[torch.LongTensor] = None, - video_grid_thw: Optional[torch.LongTensor] = None, - rope_deltas: Optional[torch.LongTensor] = None, - cache_position: Optional[torch.LongTensor] = None, -) -> Union[Tuple, Qwen2VLCausalLMOutputWithPast]: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - Returns: - - Example: - - ```python - >>> from PIL import Image - >>> import requests - >>> from transformers import AutoProcessor, Qwen2VLForConditionalGeneration - - >>> model = Qwen2VLForConditionalGeneration.from_pretrained("Qwen/Qwen2-VL-7B-Instruct") - >>> processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-7B-Instruct") - - >>> messages = [ - { - "role": "user", - "content": [ - {"type": "image"}, - {"type": "text", "text": "What is shown in this image?"}, - ], - }, - ] - >>> url = "https://www.ilankelman.org/stopsigns/australia.jpg" - >>> image = Image.open(requests.get(url, stream=True).raw) - - >>> text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) - >>> inputs = processor(text=[text], images=[image], vision_infos=[vision_infos]) - - >>> # Generate - >>> generate_ids = model.generate(inputs.input_ids, max_length=30) - >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "The image shows a street scene with a red stop sign in the foreground. In the background, there is a large red gate with Chinese characters ..." - ```""" - - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - - if inputs_embeds is None: - inputs_embeds = self.model.embed_tokens(input_ids) - if pixel_values is not None: - pixel_values = pixel_values.type(self.visual.get_dtype()) - image_embeds = self.visual(pixel_values, grid_thw=image_grid_thw) - n_image_tokens = (input_ids == self.config.image_token_id).sum().item() - n_image_features = image_embeds.shape[0] - if n_image_tokens != n_image_features: - raise ValueError( - f"Image features and image tokens do not match: tokens: {n_image_tokens}, features {n_image_features}" - ) - image_mask = ( - (input_ids == self.config.image_token_id) - .unsqueeze(-1) - .expand_as(inputs_embeds) - .to(inputs_embeds.device) - ) - image_embeds = image_embeds.to(inputs_embeds.device, inputs_embeds.dtype) - inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds) # type: ignore - - if pixel_values_videos is not None: - pixel_values_videos = pixel_values_videos.type(self.visual.get_dtype()) - video_embeds = self.visual(pixel_values_videos, grid_thw=video_grid_thw) - n_video_tokens = (input_ids == self.config.video_token_id).sum().item() - n_video_features = video_embeds.shape[0] - if n_video_tokens != n_video_features: - raise ValueError( - f"Video features and video tokens do not match: tokens: {n_video_tokens}, features {n_video_features}" - ) - video_mask = ( - (input_ids == self.config.video_token_id) - .unsqueeze(-1) - .expand_as(inputs_embeds) - .to(inputs_embeds.device) - ) - video_embeds = video_embeds.to(inputs_embeds.device, inputs_embeds.dtype) - inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds) # type: ignore - - if attention_mask is not None: - attention_mask = attention_mask.to(inputs_embeds.device) - - # if we get 4D attention mask we cannot calculate rope deltas anymore. TODO @raushan fixme - if position_ids is None and (attention_mask is None or attention_mask.ndim == 2): - # calculate RoPE index once per generation in the pre-fill stage only - if ( - (cache_position is not None and cache_position[0] == 0) - or self.rope_deltas is None - or (past_key_values is None or past_key_values.get_seq_length() == 0) # type: ignore - ): - position_ids, rope_deltas = self.get_rope_index( - input_ids, image_grid_thw, video_grid_thw, attention_mask - ) - self.rope_deltas = rope_deltas - # then use the prev pre-calculated rope-deltas to get the correct position ids - else: - batch_size, seq_length, _ = inputs_embeds.shape - delta = ( - cache_position[0] + self.rope_deltas - if cache_position is not None - else 0 - ) - position_ids = torch.arange(seq_length, device=inputs_embeds.device) # type: ignore - position_ids = position_ids.view(1, -1).expand(batch_size, -1) # type: ignore - if cache_position is not None: # otherwise `deltas` is an int `0` - delta = delta.repeat_interleave(batch_size // delta.shape[0], dim=0) # type: ignore - delta = delta.to(position_ids.device) # type: ignore - position_ids = position_ids.add(delta) # type: ignore - position_ids = position_ids.unsqueeze(0).expand(3, -1, -1) # type: ignore - - outputs = self.model( - input_ids=None, - position_ids=position_ids, - attention_mask=attention_mask, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - cache_position=cache_position, - ) - - hidden_states = outputs[0] - logits = None - loss = None - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states, - self.lm_head.weight, - labels, - _PATCH_OPTS, - ) - else: - logits = self.lm_head(hidden_states) - - if labels is not None: - # Upcast to float if we need to compute the loss to avoid potential precision issues - logits = logits.float() - # Shift so that tokens < n predict n - shift_logits = logits[..., :-1, :].contiguous() - shift_labels = labels[..., 1:].contiguous() - # Flatten the tokens - loss_fct = CrossEntropyLoss() - shift_logits = shift_logits.view(-1, self.config.vocab_size) - shift_labels = shift_labels.view(-1) - # Enable model parallelism - shift_labels = shift_labels.to(shift_logits.device) - loss = loss_fct(shift_logits, shift_labels) - - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return Qwen2VLCausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - rope_deltas=self.rope_deltas, - ) - - -def patch_qwen2_vl( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - - from transformers.models.qwen2_vl import modeling_qwen2_vl - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_qwen2_vl.Qwen2VLForConditionalGeneration - ), f"Expected a Qwen2VLForConditionalGeneration model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward_multimodal, maybe_model) - - return maybe_model - - modeling_qwen2_vl.Qwen2VLForConditionalGeneration.forward = cce_forward_multimodal - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3.py deleted file mode 100644 index 799a4f357e..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Qwen3 CCE patch. The model inherits Llama's modeling code and uses the same forward method.""" - -# pylint: disable=duplicate-code - -from types import MethodType - -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, -) - - -def patch_qwen3( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - from transformers.models.qwen3 import modeling_qwen3 - - # Set the _PATCH_OPTS in the llama patch file - import axolotl.integrations.cut_cross_entropy.monkeypatch.llama as llama_patch - - llama_patch._PATCH_OPTS = patch_options # pylint: disable=protected-access - - from axolotl.integrations.cut_cross_entropy.monkeypatch.llama import cce_forward - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_qwen3.Qwen3ForCausalLM - ), f"Expected a Qwen3ForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(cce_forward, maybe_model) - return maybe_model - - modeling_qwen3.Qwen3ForCausalLM.forward = cce_forward - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3_moe.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3_moe.py deleted file mode 100644 index 90466e64ba..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/qwen3_moe.py +++ /dev/null @@ -1,183 +0,0 @@ -"""Qwen3 MoE CCE patch. Adapted from transformers v4.51.2""" - -# pylint: disable=duplicate-code - -from types import MethodType -from typing import Optional, Union - -import torch -import transformers -from cut_cross_entropy.transformers.utils import ( - PatchOptions, - TransformersModelT, - apply_lce, -) -from transformers.models.qwen3_moe.modeling_qwen3_moe import ( - KwargsForCausalLM, - MoeCausalLMOutputWithPast, - MoeModelOutputWithPast, - load_balancing_loss_func, -) -from transformers.processing_utils import Unpack -from transformers.utils.deprecation import deprecate_kwarg -from transformers.utils.generic import can_return_tuple - -_PATCH_OPTS: PatchOptions | None = None - - -@can_return_tuple -@deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") -def forward( - self, - input_ids: Optional[torch.LongTensor] = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[list[torch.FloatTensor]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - output_router_logits: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - **kwargs: Unpack[KwargsForCausalLM], -) -> MoeCausalLMOutputWithPast: - r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., - config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored - (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. - - logits_to_keep (`int` or `torch.Tensor`, *optional*): - If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all - `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that - token can save memory, which becomes pretty significant for long sequences or large vocabulary size. - If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. - This is useful when using packed tensor format (single dimension for batch and sequence length). - - Returns: - - Example: - - ```python - >>> from transformers import AutoTokenizer, Qwen3MoeForCausalLM - - >>> model = Qwen3MoeForCausalLM.from_pretrained("Qwen/Qwen3-MoE-15B-A2B") - >>> tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-MoE-15B-A2B") - - >>> prompt = "Hey, are you conscious? Can you talk to me?" - >>> inputs = tokenizer(prompt, return_tensors="pt") - - >>> # Generate - >>> generate_ids = model.generate(inputs.input_ids, max_length=30) - >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] - "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." - ```""" - - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_router_logits = ( - output_router_logits - if output_router_logits is not None - else self.config.output_router_logits - ) - - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - - # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) - outputs: MoeModelOutputWithPast = self.model( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - output_router_logits=output_router_logits, - cache_position=cache_position, - **kwargs, - ) - - hidden_states = outputs.last_hidden_state - - if hidden_states is None: - raise ValueError("hidden_states is None") - - loss = None - logits = None - - # Only compute necessary logits, and do not upcast them to float if we are not computing the loss - slice_indices = ( - slice(-logits_to_keep, None) - if isinstance(logits_to_keep, int) - else logits_to_keep - ) - - if _PATCH_OPTS is not None and _PATCH_OPTS.use_lce(labels, self.training): - assert labels is not None - loss = apply_lce( - hidden_states[:, slice_indices, :], - self.lm_head.weight, - labels, - _PATCH_OPTS, - **kwargs, - ) - else: - logits = self.lm_head(hidden_states[:, slice_indices, :]) - - if labels is not None: - loss = self.loss_function(logits, labels, self.vocab_size, **kwargs) - - aux_loss = None - if output_router_logits: - aux_loss = load_balancing_loss_func( - outputs.router_logits, - self.num_experts, - self.num_experts_per_tok, - attention_mask, - ) - if labels is not None: - loss += self.router_aux_loss_coef * aux_loss.to( # type: ignore - loss.device # type: ignore - ) # make sure to reside in the same device - - return MoeCausalLMOutputWithPast( - loss=loss, - aux_loss=aux_loss, # type: ignore - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - router_logits=outputs.router_logits, - ) - - -def patch_qwen3_moe( - maybe_model: TransformersModelT | str | transformers.PretrainedConfig, - patch_options: PatchOptions, -) -> TransformersModelT | None: - global _PATCH_OPTS # pylint: disable=global-statement - - from transformers.models.qwen3_moe import modeling_qwen3_moe - - _PATCH_OPTS = patch_options - - if isinstance(maybe_model, transformers.PreTrainedModel): - assert isinstance( - maybe_model, modeling_qwen3_moe.Qwen3MoeForCausalLM - ), f"Expected a Qwen3MoeForCausalLM model. Got {type(maybe_model)}." - maybe_model.forward = MethodType(forward, maybe_model) - - return maybe_model - - modeling_qwen3_moe.Qwen3MoeForCausalLM.forward = forward - return None diff --git a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/utils.py b/src/axolotl/integrations/cut_cross_entropy/monkeypatch/utils.py deleted file mode 100644 index b808b9f0d8..0000000000 --- a/src/axolotl/integrations/cut_cross_entropy/monkeypatch/utils.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (C) 2024 Apple Inc. All Rights Reserved. - -"""Monkeypatch for apply_lce to add softcap.""" - -import torch -from cut_cross_entropy import linear_cross_entropy -from cut_cross_entropy.transformers.utils import PatchOptions - - -def apply_lce( - e: torch.Tensor, - c: torch.Tensor, - labels: torch.Tensor, - opts: PatchOptions, - bias: torch.Tensor | None = None, - softcap: float | None = None, - **loss_kwargs, -) -> torch.Tensor: - """Monkey patch for apply_lce to support softcap kwarg.""" - num_items_in_batch = loss_kwargs.get("num_items_in_batch", None) - cce_kwargs = opts.to_kwargs() - if num_items_in_batch is not None and cce_kwargs["reduction"] == "mean": - cce_kwargs["reduction"] = "sum" - else: - num_items_in_batch = None - - loss = linear_cross_entropy( - e, - c, - labels.to(e.device), - bias=bias, - shift=True, - softcap=softcap, - **cce_kwargs, - ) - - if num_items_in_batch is not None: - loss = loss / num_items_in_batch - - return loss diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index ce9b6a838d..0806974000 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -142,7 +142,7 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: # TODO: check if it's normal to be single image only for common datasets # From observation, it's usually a list of single image but some datasets may have several columns for images # Temporary solution: take the first image and suggest people convert their datasets to use multi-content Messages - if len(processed_example[image_key]) > 0: + if len(processed_example[image_key]) > 1: LOG.warning( f"Found {len(processed_example[image_key])} images in a sample. Using the first one." "If you are using a dataset with multiple images per sample, please convert it to use multi-content Messages." From a27c4f8771da962502ead4408ed6f556e2624618 Mon Sep 17 00:00:00 2001 From: Younes B <49240599+younesbelkada@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:49:42 +0200 Subject: [PATCH 0739/1405] feat: add falcon-h1 into axolotl (#2811) [skip ci] * feat: add falcon-h1 into axolotl * fix pre-commit * review * fix: remove packing --- .../falcon-h1/falcon-h1-1b-deep-qlora.yaml | 71 +++++++++++++++++++ examples/falcon-h1/falcon-h1-1b-qlora.yaml | 71 +++++++++++++++++++ examples/falcon-h1/falcon-h1-34b-qlora.yaml | 71 +++++++++++++++++++ examples/falcon-h1/falcon-h1-3b-qlora.yaml | 71 +++++++++++++++++++ examples/falcon-h1/falcon-h1-500m-qlora.yaml | 71 +++++++++++++++++++ examples/falcon-h1/falcon-h1-7b-qlora.yaml | 71 +++++++++++++++++++ src/axolotl/loaders/model.py | 6 ++ src/axolotl/utils/chat_templates.py | 1 + src/axolotl/utils/schemas/enums.py | 1 + 9 files changed, 434 insertions(+) create mode 100644 examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml create mode 100644 examples/falcon-h1/falcon-h1-1b-qlora.yaml create mode 100644 examples/falcon-h1/falcon-h1-34b-qlora.yaml create mode 100644 examples/falcon-h1/falcon-h1-3b-qlora.yaml create mode 100644 examples/falcon-h1/falcon-h1-500m-qlora.yaml create mode 100644 examples/falcon-h1/falcon-h1-7b-qlora.yaml diff --git a/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml b/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml new file mode 100644 index 0000000000..1dd901154a --- /dev/null +++ b/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml @@ -0,0 +1,71 @@ +base_model: tiiuae/Falcon-H1-1.5B-Deep-Base +# optionally might have model_type or tokenizer_type +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: true + +# huggingface repo +chat_template: falcon_h1 +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - in_proj + - gate_proj + - up_proj + - down_proj + +sequence_len: 2048 +sample_packing: false +eval_sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/falcon-h1/falcon-h1-1b-qlora.yaml b/examples/falcon-h1/falcon-h1-1b-qlora.yaml new file mode 100644 index 0000000000..24dc7cae37 --- /dev/null +++ b/examples/falcon-h1/falcon-h1-1b-qlora.yaml @@ -0,0 +1,71 @@ +base_model: tiiuae/Falcon-H1-1.5B-Base +# optionally might have model_type or tokenizer_type +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: true + +# huggingface repo +chat_template: falcon_h1 +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - in_proj + - gate_proj + - up_proj + - down_proj + +sequence_len: 2048 +sample_packing: false +eval_sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/falcon-h1/falcon-h1-34b-qlora.yaml b/examples/falcon-h1/falcon-h1-34b-qlora.yaml new file mode 100644 index 0000000000..43eb1967ba --- /dev/null +++ b/examples/falcon-h1/falcon-h1-34b-qlora.yaml @@ -0,0 +1,71 @@ +base_model: tiiuae/Falcon-H1-34B-Base +# optionally might have model_type or tokenizer_type +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: true + +# huggingface repo +chat_template: falcon_h1 +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - in_proj + - gate_proj + - up_proj + - down_proj + +sequence_len: 2048 +sample_packing: false +eval_sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/falcon-h1/falcon-h1-3b-qlora.yaml b/examples/falcon-h1/falcon-h1-3b-qlora.yaml new file mode 100644 index 0000000000..00929bbf01 --- /dev/null +++ b/examples/falcon-h1/falcon-h1-3b-qlora.yaml @@ -0,0 +1,71 @@ +base_model: tiiuae/Falcon-H1-3B-Base +# optionally might have model_type or tokenizer_type +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: true + +# huggingface repo +chat_template: falcon_h1 +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - in_proj + - gate_proj + - up_proj + - down_proj + +sequence_len: 2048 +sample_packing: false +eval_sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/falcon-h1/falcon-h1-500m-qlora.yaml b/examples/falcon-h1/falcon-h1-500m-qlora.yaml new file mode 100644 index 0000000000..e2640de7bc --- /dev/null +++ b/examples/falcon-h1/falcon-h1-500m-qlora.yaml @@ -0,0 +1,71 @@ +base_model: tiiuae/Falcon-H1-0.5B-Instruct +# optionally might have model_type or tokenizer_type +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: true + +# huggingface repo +chat_template: falcon_h1 +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - in_proj + - gate_proj + - up_proj + - down_proj + +sequence_len: 2048 +sample_packing: false +eval_sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/falcon-h1/falcon-h1-7b-qlora.yaml b/examples/falcon-h1/falcon-h1-7b-qlora.yaml new file mode 100644 index 0000000000..183e423b51 --- /dev/null +++ b/examples/falcon-h1/falcon-h1-7b-qlora.yaml @@ -0,0 +1,71 @@ +base_model: tiiuae/Falcon-H1-7B-Base +# optionally might have model_type or tokenizer_type +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: true + +# huggingface repo +chat_template: falcon_h1 +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - in_proj + - gate_proj + - up_proj + - down_proj + +sequence_len: 2048 +sample_packing: false +eval_sample_packing: false +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 3b2a455ca9..bbc532fb95 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -504,6 +504,9 @@ def _set_quantization_config(self): # for some reason, this causes the loss to be off by an order of magnitude # but deepspeed needs this still in bfloat16 bnb_config["bnb_4bit_quant_storage"] = torch.float32 + if self.cfg.model_config_type == "falcon_h1": + # output projection cannot be quantized for Falcon-H1 models + bnb_config["llm_int8_skip_modules"] = ["out_proj"] if self.cfg.bnb_config_kwargs: bnb_config.update(self.cfg.bnb_config_kwargs) @@ -518,6 +521,9 @@ def _set_quantization_config(self): # Exclude mamba blocks from int8 quantization for jamba if self.cfg.model_config_type == "jamba": bnb_config["llm_int8_skip_modules"] = ["mamba"] + if self.cfg.model_config_type == "falcon_h1": + # output projection cannot be quantized for Falcon-H1 models + bnb_config["llm_int8_skip_modules"] = ["out_proj"] self.model_kwargs["quantization_config"] = BitsAndBytesConfig( **bnb_config, ) diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py index 83a42945b0..c809ffc7af 100644 --- a/src/axolotl/utils/chat_templates.py +++ b/src/axolotl/utils/chat_templates.py @@ -46,6 +46,7 @@ "command_a_tool_use": '{{ bos_token }}{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {"tool_call_id": "0", "tool_name": "direct-injected-document", "parameters": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n "tool_call_id": "0",\n "results": {\n{% for doc in documents %}\n "{{ loop.index0 }}": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n "is_error": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n "tool_call_id": "{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}",\n "results": {\n "0": {{ tool_msg.content|tojson }}\n },\n "is_error": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0][\'role\']|lower == \'system\' %}{%- set developer_preamble = messages[0][\'content\'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user\'s requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user\'s request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it\'ll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you\'ve figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it\'s time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user\'s last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that "Reflection" and "Response" above can be grounded.\nGrounding means you associate pieces of texts (called "spans") with those specific tool results that support them (called "sources"). And you use a pair of tags "" and "" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as "{tool_call_id}:[{list of result indices}]", before they are joined together by ",". E.g., "span" means that "span" is supported by result 1 and 2 from "tool_call_id=0" as well as result 0 from "tool_call_id=1".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {"name": "direct-injected-document", "description": "This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!", "parameters": {"type": "object", "properties": {}, "required": []}, "responses": {"200": {"description": "Successfully returned a list of chunked text snippets from the directly uploaded documents.", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "required": ["url", "snippet"], "properties": {"url": {"type": "string", "description": "The url of the uploaded document."}, "snippet": {"type": "string", "description": "The text snippet for the returned document chunk."}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {"name": "{{ tool[\'function\'][\'name\'] }}", "description": "{{tool[\'function\'][\'description\']}}", "parameters": {{ tool[\'function\'][\'parameters\']|tojson }}, "responses": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == \'system\' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == \'user\' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == \'assistant\' or message.role|lower == \'chatbot\' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {"tool_call_id": "{{ tool_idx.value }}", "tool_name": "{{ tc[\'function\'][\'name\'] }}", "parameters": {{ tc[\'function\'][\'arguments\']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == \'tool\' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == \'tool\' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>', "command_a_rag": '{{ bos_token }}{% set tools = [] %}\n{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {"tool_call_id": "0", "tool_name": "direct-injected-document", "parameters": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n "tool_call_id": "0",\n "results": {\n{% for doc in documents %}\n "{{ loop.index0 }}": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n "is_error": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n "tool_call_id": "{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}",\n "results": {\n "0": {{ tool_msg.content|tojson }}\n },\n "is_error": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0][\'role\']|lower == \'system\' %}{%- set developer_preamble = messages[0][\'content\'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user\'s requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user\'s request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it\'ll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you\'ve figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it\'s time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user\'s last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that "Reflection" and "Response" above can be grounded.\nGrounding means you associate pieces of texts (called "spans") with those specific tool results that support them (called "sources"). And you use a pair of tags "" and "" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as "{tool_call_id}:[{list of result indices}]", before they are joined together by ",". E.g., "span" means that "span" is supported by result 1 and 2 from "tool_call_id=0" as well as result 0 from "tool_call_id=1".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {"name": "direct-injected-document", "description": "This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!", "parameters": {"type": "object", "properties": {}, "required": []}, "responses": {"200": {"description": "Successfully returned a list of chunked text snippets from the directly uploaded documents.", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "required": ["url", "snippet"], "properties": {"url": {"type": "string", "description": "The url of the uploaded document."}, "snippet": {"type": "string", "description": "The text snippet for the returned document chunk."}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {"name": "{{ tool[\'function\'][\'name\'] }}", "description": "{{tool[\'function\'][\'description\']}}", "parameters": {{ tool[\'function\'][\'parameters\']|tojson }}, "responses": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == \'system\' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == \'user\' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == \'assistant\' or message.role|lower == \'chatbot\' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {"tool_call_id": "{{ tool_idx.value }}", "tool_name": "{{ tc[\'function\'][\'name\'] }}", "parameters": {{ tc[\'function\'][\'arguments\']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == \'tool\' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == \'tool\' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>', "aya": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Aya, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", + "falcon_h1": """'{{bos_token}}\n{%- if tools %}\n {{- \'<|im_start|>system\\n\' }}\n {%- if messages[0].role == \'system\' %}\n {{- messages[0].content + \'\\n\\n\' }}\n {%- endif %}\n {{- "You are a function calling AI model. You are provided with function signature within XML tags. You may call one or more functions to assist with the user query. Don\'t make assumptions about what values to plug into functions.\\n\\n" }}\n {%- for tool in tools %}[{{- tool | tojson }}]{%- endfor %}\n {{- "\\n\\nFor each function call, return a json object with function name and arguments within tags with the following schema:\\n\\n{\'arguments\': , \'name\': }\\n\\n" }}\n{%- else %}\n {%- if messages[0].role == \'system\' %}\n {{- \'<|im_start|>system\\n\' + messages[0].content + \'<|im_end|>\\n\' }}\n {%- endif %}\n{%- endif %}{% for message in messages %}{%- if message.role != \'system\' %}{{\'<|im_start|>\' + message[\'role\'] + \'\n\' + message[\'content\'] + \'<|im_end|>\' + \'\n\'}}{%- endif %}{% endfor %}{% if add_generation_prompt %}{{ \'<|im_start|>assistant\n\' }}{% endif %}'""", } diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index bfef14d533..67fc7a8a75 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -54,6 +54,7 @@ class ChatTemplate(str, Enum): jinja = "jinja" qwen_25 = "qwen_25" qwen3 = "qwen3" + falcon_h1 = "falcon_h1" tokenizer_default = "tokenizer_default" exaone = "exaone" metharme = "metharme" From 20106116da5b07bb4755aee966a2ba54ee8c93ad Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 25 Jun 2025 20:49:55 +0700 Subject: [PATCH 0740/1405] fix: 'NoneType' object has no attribute 'column_names' (#2822) [skip ci] * fix: 'NoneType' object has no attribute 'column_names' * chore: typing --- src/axolotl/core/trainers/base.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index fbae253d66..3e9ea7ae8b 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -20,7 +20,7 @@ SequentialSampler, ) from transformers import Trainer -from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, seed_worker +from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, has_length, seed_worker from trl.trainer.utils import pad_to_length from typing_extensions import override @@ -122,8 +122,8 @@ def _create_multipack_sampler( return sampler def _get_train_sampler( - self, train_dataset: Optional[Dataset] = None - ) -> Optional[Sampler]: + self, train_dataset: Dataset | None = None + ) -> Sampler | None: """ Helper method to get the sampler for training. Handles cases for sample packing and curriculum sampling (sequential). @@ -132,16 +132,22 @@ def _get_train_sampler( If the dataset is non-empty, a sampler is returned, the type of which depends on the passed training args. """ + # from https://github.com/huggingface/transformers/blob/2166b6b4ff09f6dd3867ab982f262f66482aa968/src/transformers/trainer.py#L969C1-L972C24 + if train_dataset is None: + train_dataset = self.train_dataset + if train_dataset is None or not has_length(train_dataset): + return None + use_sample_packing = self.args.sample_packing and not self.args.pretraining # Determine the base sampler first if self.args.curriculum_sampling: - base_sampler = SequentialSampler(self.train_dataset) + base_sampler = SequentialSampler(train_dataset) elif use_sample_packing: - base_sampler = RandomSampler(self.train_dataset) + base_sampler = RandomSampler(train_dataset) else: # Default to parent class implementation for standard random sampling - return super()._get_train_sampler() + return super()._get_train_sampler(train_dataset) # Apply multipack wrapper if needed if use_sample_packing: @@ -160,6 +166,10 @@ def _get_eval_sampler(self, eval_dataset: Dataset | None = None) -> Sampler | No If the dataset is non-empty, a sampler is returned, the type of which depends on the passed training args. """ + # from https://github.com/huggingface/transformers/blob/2166b6b4ff09f6dd3867ab982f262f66482aa968/src/transformers/trainer.py#L1065C9-L1066C24 + if eval_dataset is None or not has_length(eval_dataset): + return None + # Multipacking enabled if training is enabled and eval is not explicitly disabled use_multipack = ( self.args.sample_packing and self.args.eval_sample_packing is not False From 181cc3106b48e0ea0023a851d5a67106738fbfd1 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 25 Jun 2025 20:50:13 +0700 Subject: [PATCH 0741/1405] fix: catch httperror from ratelimiting hf when checking user token (#2827) --- src/axolotl/cli/checks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/axolotl/cli/checks.py b/src/axolotl/cli/checks.py index 10086c2a4c..a743e74dc7 100644 --- a/src/axolotl/cli/checks.py +++ b/src/axolotl/cli/checks.py @@ -6,6 +6,7 @@ from accelerate.commands.config import config_args from huggingface_hub import HfApi from huggingface_hub.utils import LocalTokenNotFoundError +from requests import HTTPError from axolotl.utils.logging import get_logger @@ -46,3 +47,8 @@ def check_user_token() -> bool: "Error verifying HuggingFace token. Remember to log in using `huggingface-cli login` and get your access token from https://huggingface.co/settings/tokens if you want to use gated models or datasets." ) return False + except HTTPError: + LOG.warning( + "Error accessing HuggingFace. This may be due to a network issue or rate limiting." + ) + return False From d8cf66edbd33239bb93cd020dcba1e45ff4073be Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 25 Jun 2025 13:17:33 -0400 Subject: [PATCH 0742/1405] use fork for multiprocess start method for packing in parallel (#2830) --- src/axolotl/core/trainers/base.py | 1 + src/axolotl/core/training_args_base.py | 4 ++++ src/axolotl/utils/samplers/multipack.py | 7 +++++-- src/axolotl/utils/schemas/config.py | 6 ++++++ src/axolotl/utils/trainer.py | 1 + 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 3e9ea7ae8b..b0e6e8eae1 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -116,6 +116,7 @@ def _create_multipack_sampler( sequential=self.args.sample_packing_sequentially, drop_last=True, num_processes=self.args.dataset_num_proc, + mp_start_method=self.args.sample_packing_mp_start_method or "fork", ) len(sampler) diff --git a/src/axolotl/core/training_args_base.py b/src/axolotl/core/training_args_base.py index 8fcaff632f..e04be43e0e 100644 --- a/src/axolotl/core/training_args_base.py +++ b/src/axolotl/core/training_args_base.py @@ -38,6 +38,10 @@ class AxolotlTrainingMixins: "help": "Use next-fit sample packing that preserves the order of samples coming from the sampler. Use in combination with curriculum_sampling for fully sequential packing." }, ) + sample_packing_mp_start_method: str | None = field( + default=None, + metadata={"help": "The multiprocessing start method to use."}, + ) multipack_real_batches: bool = field( default=False, metadata={"help": "Use real batches for efficient training."}, diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 7fb5e1b41b..95d97e7a06 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -127,7 +127,7 @@ def pack_parallel( bin_size: int, num_processes: int | None = None, safe_mode: bool = True, - mp_start_method: str | None = "spawn", + mp_start_method: str | None = "fork", ) -> list[list[int]]: """Pack sequences into bins using parallel processing. @@ -266,6 +266,7 @@ def __init__( bin_size: int = 200, # The max number of samples that can be packed in a single bin num_processes: int | None = None, # Number of processes for parallel packing safe_mode: bool = True, # Conservative packing to prevent training instability + mp_start_method: str = "fork", **kwargs, # pylint: disable=unused-argument ): super().__init__(sampler, batch_size, drop_last) @@ -278,6 +279,7 @@ def __init__( self.bin_size = bin_size self.num_processes = num_processes self.safe_mode = safe_mode + self.mp_start_method = mp_start_method assert isinstance(self.lengths, np.ndarray) @@ -338,8 +340,9 @@ def generate_batches(self, set_stats: bool = False) -> list[list[list[int]]]: bin_capacity=self.batch_max_len, group_size=self.group_size, bin_size=self.bin_size, - num_processes=self.num_processes, + num_processes=max(4, self.num_processes) if self.num_processes else 4, safe_mode=self.safe_mode, + mp_start_method=self.mp_start_method, ) # Map bin indices back to original indices diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index c698fc3b66..4031742cde 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -393,6 +393,12 @@ class AxolotlInputConfig( default=None, json_schema_extra={"description": "Whether to pack samples sequentially"}, ) + sample_packing_mp_start_method: str | None = Field( + default=None, + json_schema_extra={ + "description": "The multiprocessing start method to use for packing. Should be 'fork', 'spawn' or 'forkserver'" + }, + ) eval_sample_packing: bool | None = Field( default=None, json_schema_extra={ diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 633dffde57..554a55abc4 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -467,6 +467,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): sequential=cfg.sample_packing_sequentially, drop_last=True, num_processes=cfg.dataset_processes, + mp_start_method=cfg.sample_packing_mp_start_method or "fork", ) data_loader = DataLoader( From 18954ba1002ddf8f54b28fc07c4c47b8f20cf63c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:46:53 -0400 Subject: [PATCH 0743/1405] chore: update pre-commit hooks (#2821) [skip ci] Co-authored-by: djsaunde <1245942+djsaunde@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 195746d2d6..a029ba39fa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: hooks: - id: isort - repo: https://github.com/PyCQA/flake8 - rev: 7.2.0 + rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/pylint-dev/pylint @@ -27,7 +27,7 @@ repos: hooks: - id: pylint - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.0 + rev: v1.16.1 hooks: - id: mypy additional_dependencies: @@ -36,7 +36,7 @@ repos: 'pydantic>=2.5.3', ] - repo: https://github.com/PyCQA/bandit - rev: 1.8.3 + rev: 1.8.5 hooks: - id: bandit args: [ From 927bf530bcb095a20838ae9ac2b974b887016212 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 26 Jun 2025 21:47:31 +0700 Subject: [PATCH 0744/1405] fix(doc): default messages example used wrong key (#2832) * fix(doc): default messages example used wrong key * feat: add links to SP, multi-gpu, multi-node on readme --- README.md | 2 +- docs/dataset-formats/conversation.qmd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3bfce8df10..e18220567d 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Features: - **Multiple Model Support**: Train various models like LLaMA, Mistral, Mixtral, Pythia, and more. We are compatible with HuggingFace transformers causal language models. - **Training Methods**: Full fine-tuning, LoRA, QLoRA, GPTQ, QAT, Preference Tuning (DPO, IPO, KTO, ORPO), RL (GRPO), Multimodal, and Reward Modelling (RM) / Process Reward Modelling (PRM). - **Easy Configuration**: Re-use a single YAML file between dataset preprocess, training, evaluation, quantization, and inference. -- **Performance Optimizations**: [Multipacking](https://docs.axolotl.ai/docs/multipack.html), [Flash Attention](https://github.com/Dao-AILab/flash-attention), [Xformers](https://github.com/facebookresearch/xformers), [Flex Attention](https://pytorch.org/blog/flexattention/), [Liger Kernel](https://github.com/linkedin/Liger-Kernel), [Cut Cross Entropy](https://github.com/apple/ml-cross-entropy/tree/main), Sequence Parallelism (SP), LoRA optimizations, Multi-GPU training (FSDP1, FSDP2, DeepSpeed), Multi-node training (Torchrun, Ray), and many more! +- **Performance Optimizations**: [Multipacking](https://docs.axolotl.ai/docs/multipack.html), [Flash Attention](https://github.com/Dao-AILab/flash-attention), [Xformers](https://github.com/facebookresearch/xformers), [Flex Attention](https://pytorch.org/blog/flexattention/), [Liger Kernel](https://github.com/linkedin/Liger-Kernel), [Cut Cross Entropy](https://github.com/apple/ml-cross-entropy/tree/main), [Sequence Parallelism (SP)](https://docs.axolotl.ai/docs/sequence_parallelism.html), [LoRA optimizations](https://docs.axolotl.ai/docs/lora_optims.html), [Multi-GPU training (FSDP1, FSDP2, DeepSpeed)](https://docs.axolotl.ai/docs/multi-gpu.html), [Multi-node training (Torchrun, Ray)](https://docs.axolotl.ai/docs/multi-node.html), and many more! - **Flexible Dataset Handling**: Load from local, HuggingFace, and cloud (S3, Azure, GCP, OCI) datasets. - **Cloud Ready**: We ship [Docker images](https://hub.docker.com/u/axolotlai) and also [PyPI packages](https://pypi.org/project/axolotl/) for use on cloud platforms and local hardware. diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index d1fca9441e..90cc178fcc 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -9,7 +9,7 @@ order: 3 Chat Template strategy uses a jinja2 template that converts a list of messages into a prompt. Support using tokenizer's template, a supported template, or custom jinja2. ```{.json filename="data.jsonl"} -{"conversations": [{"role": "...", "content": "..."}]} +{"messages": [{"role": "...", "content": "..."}, {"role": "...", "content": "..."}, ...]} ``` See [configs](../config-reference.qmd) for full configs and supported templates. From a24957fa0438a94882f0976d18595b23de8c84fb Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 27 Jun 2025 10:35:23 -0400 Subject: [PATCH 0745/1405] fix for iterable datasets and pickling (#2831) [skip ci] * fix for iterable datasets and pickling * more fixes for pretraining * can't pickle mock generator dataset --- src/axolotl/core/builders/causal.py | 3 ++- src/axolotl/train.py | 2 ++ src/axolotl/utils/data/pretraining.py | 5 +++-- src/axolotl/utils/samplers/multipack.py | 5 +++-- src/axolotl/utils/schemas/validation.py | 14 ++++++++++++++ src/axolotl/utils/trainer.py | 1 + 6 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 47e33a3321..2b7d902fa0 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -413,7 +413,8 @@ def build_collator( or self.cfg.micro_batch_size > 1 ): return DataCollatorForSeq2Seq(self.tokenizer, **kwargs) - return None + if not (self.cfg.sample_packing and self.cfg.pretrain_multipack_attn): + return None if self.cfg.model_config_type == "mamba": return MambaDataCollator(tokenizer=self.tokenizer) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index d5dd431c1c..a476385d02 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -223,6 +223,8 @@ def execute_training( ) LOG.info("Starting trainer...") + if cfg.bf16: + torch.set_default_dtype(torch.bfloat16) trainer.train(resume_from_checkpoint=resume_from_checkpoint) diff --git a/src/axolotl/utils/data/pretraining.py b/src/axolotl/utils/data/pretraining.py index 4ff108aeef..f3422f9908 100644 --- a/src/axolotl/utils/data/pretraining.py +++ b/src/axolotl/utils/data/pretraining.py @@ -224,10 +224,10 @@ def wrap_pretraining_dataset( remove_columns = [] if dataset.features is None: for first_row in dataset: - remove_columns = first_row.keys() + remove_columns = list(first_row.keys()) break else: - remove_columns = dataset.features.keys() + remove_columns = list(dataset.features.keys()) dataset = dataset.map( encode, @@ -267,6 +267,7 @@ def encode_packed_pretraining( batch_size=1, batch_max_len=batch_size * max_seq_length, drop_last=True, + num_processes=1, ) chunked_data = defaultdict(list) diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 95d97e7a06..ee8640f417 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -260,7 +260,7 @@ def __init__( lengths: np.ndarray, # Sequence lengths packing_efficiency_estimate: float = 1.0, # Initial efficiency estimate drop_last: bool = True, # Whether to drop final batches (might be incomplete) - num_count_samples: int = 8, # Number of times to estimate batch count + num_count_samples: int = 4, # Number of times to estimate batch count sequential: bool = False, # Whether to use sequential packing group_size: int = 100_000, # Size of groups for parallel packing bin_size: int = 200, # The max number of samples that can be packed in a single bin @@ -335,12 +335,13 @@ def generate_batches(self, set_stats: bool = False) -> list[list[list[int]]]: bins = [[indices[b_idx] for b_idx in bin_indices] for bin_indices in bins] else: # Use parallel packing + num_processes = self.num_processes or 1 all_bins = pack_parallel( lengths, bin_capacity=self.batch_max_len, group_size=self.group_size, bin_size=self.bin_size, - num_processes=max(4, self.num_processes) if self.num_processes else 4, + num_processes=min(4, num_processes) if num_processes else 4, safe_mode=self.safe_mode, mp_start_method=self.mp_start_method, ) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 5a6bf43b3b..3a0c9cc9f6 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -462,6 +462,20 @@ def check_mistral_common_incompatible_options(cls, data): return data + @model_validator(mode="before") + @classmethod + def pretrain_with_tps(cls, data): + if data.get("pretraining_dataset") and data.get( + "include_tokens_per_second", False + ): + # combining these would raise `TypeError: cannot pickle 'dict_keys' object` + # due to trying to count the number of tokens total in the dataset + raise ValueError( + "pretraining_dataset and include_tokens_per_second cannot be used together." + ) + + return data + class LoRAValidationMixin: """Validation methods related to LoRA/QLoRA configuration.""" diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 554a55abc4..278fbed5bb 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -381,6 +381,7 @@ def process_pretraining_datasets_for_packing( if not skip_position_ids: train_dataset = train_dataset.map( add_position_ids, + batched=True, desc="Add position_id column (Pretraining Sample Packing)", ) if drop_attention_mask: From 29289a4de9f9ecb90f298975919e14a0f27cdc27 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 27 Jun 2025 21:35:47 +0700 Subject: [PATCH 0746/1405] feat: replace old colab notebook with newer one (#2838) [skip ci] * feat: replace old colab notebook with newer one * fix: point to update cce fork --- .../colab-axolotl-example.ipynb | 10285 +++++++++++++++- 1 file changed, 9931 insertions(+), 354 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 0b373c28cf..bcb99f19ea 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -1,357 +1,9934 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setting up" - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "OPLSwmgdrB7g" + }, + "source": [ + "# Fine-Tune Qwen3 14B with Axolotl\n", + "\n", + "[\"Built](https://github.com/axolotl-ai-cloud/axolotl)\n", + "\n", + "Axolotl is the most performant LLM post-training framework available, delivering faster training with efficient, consistent and stable performance. Train your workload and ship your product 30% faster; saving you both time and money.\n", + "\n", + "- ⭐ us on [GitHub](https://github.com/axolotl-ai-cloud/axolotl)\n", + "- 📜 Read the [Docs](http://docs.axolotl.ai/)\n", + "- 💬 Chat with us on [Discord](https://discord.gg/mnpEYgRUmD)\n", + "- 📰 Get updates on [X/Twitter](https://x.com/axolotl_ai)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rVjKD7CbxIP3" + }, + "source": [ + "# Installation\n", + "\n", + "Axolotl is easy to install from [pip](https://pypi.org/project/axolotl/), or use our [pre-built Docker images](http://docs.axolotl.ai/docs/docker.html) for a hassle free dependency experience. See our [docs](http://docs.axolotl.ai/docs/installation.html) for more information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "msOCO4NRmRLa" + }, + "outputs": [], + "source": [ + "%%capture\n", + "# This step can take ~5-10 minutes to install dependencies\n", + "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@78b2a45713a54c9bedf8b33f5e31cf07a1a57154\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "N0OW0YeksDLr" + }, + "source": [ + "## Demo: Talk Like a Pirate\n", + "\n", + "In this demo, we are training the model ***to respond like a pirate***. This was chosen as a way to easily show how to train a model to respond in a certain style of your choosing (without being prompted) and is quite easy to validate within the scope of a Colab." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8Du2fANTsNCK" + }, + "source": [ + "### Upload your own dataset or use a Huggingface dataset\n", + "\n", + "You can choose to use your own JSONL file from your own [Google Drive](https://drive.google.com/drive/home); for example downloading the [Pirate-Ultrachat JSONL](https://huggingface.co/datasets/winglian/pirate-ultrachat-10k/blob/main/train.jsonl) to your Google Drive. JSONL datasets should be formatted similar to the [OpenAI dataset format](https://cookbook.openai.com/examples/chat_finetuning_data_prep).\n", + "\n", + "You can also simply use the [`winglian/pirate-ultrachat-10k`](https://huggingface.co/datasets/winglian/pirate-ultrachat-10k) dataset directly.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fGEEjyQ-r_IV" + }, + "outputs": [], + "source": [ + "# Default to HF dataset location\n", + "dataset_id = \"winglian/pirate-ultrachat-10k\"\n", + "uploaded = {}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c5MyYqk7vIsG" + }, + "outputs": [], + "source": [ + "import os\n", + "# Optionally, upload your own JSONL to your Google Drive\n", + "GOOGLE_DRIVE_PATH = \"\" # ex: \"MyDrive/Colab\\ Notebooks/train.jsonl\"\n", + "\n", + "# \"Select All\" permissions, or you may get the error:\n", + "# \"MessageError: Error: credential propagation was unsuccessful\"\n", + "if GOOGLE_DRIVE_PATH:\n", + " from google.colab import drive\n", + " # Mount your Google Drive\n", + " GOOGLE_DRIVE_MNT = \"/content/drive/\"\n", + " drive.mount(GOOGLE_DRIVE_MNT, force_remount=True)\n", + " tmp_path = os.path.join(GOOGLE_DRIVE_MNT, GOOGLE_DRIVE_PATH.lstrip(\"/\"))\n", + " # make sure file exists\n", + " if not os.path.isfile(tmp_path):\n", + " raise ValueError(f\"File {tmp_path} does not exist\")\n", + " dataset_id = tmp_path\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "U6pTk3A9xj1W" + }, + "source": [ + "# Configure for Supervised Fine-Tuning (SFT)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 151, + "referenced_widgets": [ + "388f618924274d21a066f098f4f1e744", + "7c95f85a2b1f47a1bd846d110c47bb3c", + "083f9cda8d754c168beee10d2f8955a2", + "62e1a65582f446a78612eaa804e08a7d", + "487a177d020f4605834878b2fdc7afa3", + "7fd44cf9ca6e4726bfd7ac21846d6a14", + "366a343b62fa47d8985a3bd464d99f9e", + "a0a11e929edd4189b79723d618522c33", + "e87ea87fcff247b5bbcc331ba79a8dc2", + "5e18768f7ad6434ba8b8b8a2e853e204", + "bb33aec33a6447078c31bfd728942994" + ] + }, + "id": "fdRioqytmTtX", + "outputId": "f0acdcec-4b41-4a3f-ffed-c2d2d929158e" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2025-05-08 13:40:27,488] [INFO] [root.register:348] [PID:174] Attempting to load plugin: axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin\n", + "[2025-05-08 13:40:27,493] [INFO] [root.register:351] [PID:174] Plugin loaded successfully: axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin\n", + "[2025-05-08 13:40:27,959] [INFO] [axolotl.utils.schemas.config.check_eval_packing:721] [PID:174] [RANK:0] explicitly setting `eval_sample_packing` to match `sample_packing`\u001b[39m\n", + "[2025-05-08 13:40:27,960] [INFO] [axolotl.utils.schemas.config.hint_sample_packing_padding:514] [PID:174] [RANK:0] Setting `pad_to_sequence_len: true` to prevent memory leaks when sample_packing\u001b[39m\n", + "[2025-05-08 13:40:27,961] [INFO] [axolotl.utils.schemas.config.check_bf16:1251] [PID:174] [RANK:0] bf16 support detected, but not enabled for this configuration.\u001b[39m\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "388f618924274d21a066f098f4f1e744", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "config.json: 0%| | 0.00/728 [00:00\"],\n", + " }\n", + " ],\n", + " dataloader_prefetch_factor = 8, # dataloader optimizations\n", + " dataloader_num_workers = 2,\n", + " dataloader_pin_memory = True,\n", + " )\n", + "\n", + "# validates the configuration\n", + "cfg = load_cfg(config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "715UpvnSoBIS" + }, + "outputs": [], + "source": [ + "from axolotl.utils import patch_optimized_env\n", + "# speedup downloads from HF 🤗 and set \"PYTORCH_CUDA_ALLOC_CONF\" env to save memory\n", + "patch_optimized_env()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Vc6MC-hwyH-n" + }, + "source": [ + "# Datasets\n", + "\n", + "Axolotl has a robust suite of loaders and transforms to parse most open datasets of any format into the appropriate chat template for your model. Axolotl will mask input tokens from the user's prompt so that the train loss is only calculated against the model's response. For more information, [see our documentation](http://docs.axolotl.ai/docs/dataset-formats/conversation.html) on dataset preparation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "b82aa8c57f7c422a9a9c90f333ed2a99", + "c0991cf63ee6458b96e9a75e7a88b61a", + "71c8af139cd248b1b51101fd46a93f35", + "1d5117195d4b49eb8f1a73b18419f7ce", + "3c21e4a511b4441192c03b7f1d0976e9", + "ed28e2e0410d4e0b855467e798e53d66", + "d93f134f802b4b69b575bdaf07dbd27c", + "d0e9dce55cec4c1ca619a0ccf209d924", + "4c727d40ef0443449afc31724ee79f0c", + "0dea5caa27384f5689e3cab51f558727", + "a6f48410b9964fefba0c3009a77dc838", + "95caff42f08a4c2aa14c867b8f37f231", + "de7c37ee83e24f0c889e84d07279c2ec", + "9d4897eefb5f48259ffb2d23e332f752", + "253017b0d0534e54ab44e181f6d7c82d", + "27beaf06e41b472abdb544a43c720c5a", + "34cf3df51fbc41cabfdbba153c007f0e", + "ac764024cf1c4e08ba7749afd2cd20ac", + "30a81da86f8043eca301e86a8651201a", + "e8b7a81040904c1e89e58978223b1737", + "1c6f1f10667545aaab958016ba7e2c94", + "e6e969610738449887259063967f82b0", + "a138859f19b74fc0928dc236ab5359db", + "9b42e08b3c9548818488268768a118b1", + "12b56912736849fea2ad8124456fdc5c", + "879c8ab5873847a8833bd74123be90a4", + "20352e5f58d24bb8b1f3940efd14fe4a", + "d955dcaa0e944e719f3a06139dd54a03", + "d3de2662c7964f1ba96e58da382af720", + "97e36007e1304e1583fd81bfb13f0edd", + "c65dc74c7d6f4bab8f7dd28455161dd8", + "ef223e8504b64e3592589880326aaf41", + "598da69727bd4fb8b1caf465ac736d7a", + "5f86cd894de94c3280fadc1e2fd0ee13", + "a20927bf5f2c41f58c1e31ac858ab36c", + "0a46ad75c198463d843fb35e813642cb", + "09007681cf8d42aeb8c1d2f6a74e470a", + "ebc80d1a55fa47f4a5ea2756588569ec", + "1811cda0644e4190a9469d1774435d82", + "35c811d2ae8e43f3b5cecbdd3cfa857f", + "b8e39e4dddc3497fbc29ae45c66da759", + "63b4e563e85c4f03b1b72beda9577bcc", + "b195f160ca20442fadd8b5aed0ee41af", + "ca65e32eb52f48c09a84b33cb18f22cd", + "7cd0b85ebd204b7aba908417811ce4e0", + "7baeab52d6694c32b1efd1ea1a0a7782", + "519a7b154022443db6703f04a9142bae", + "d4183e9715f34d249942b8271cca3bdf", + "da2347ac94764a3fa2743343cf0d3cd2", + "93a44a11aa4846fa8efc6c1413ef1627", + "a55060adc3564407ac81ad7297d34aaa", + "d02274afd47b462291c745f261209d42", + "0f417447a7bd4a33acca96fa37aec877", + "63580b6fb30642479fe3000915bf551a", + "8f726dbfb45d4528afa33e36a6313267", + "03b093d592ba4386aa61f7b8483da660", + "b8766a88716948cf968f4563531a76d9", + "6f3a28b912714c6e931003549664bfa3", + "16d1283741404b7bb319094c992fce01", + "2a5bb0e818ab47be8cf6465988328503", + "2b3a2659b12244bd8548320320016dbf", + "0cd7efffbb3c4c4b972e63749f61ab97", + "5ca240f31e6b44e3882c5eb37cd5a309", + "5eb06edeb58e4930b1affef2a59eae81", + "a4e5789584564049b83df7c6c54a3e08", + "ff3a94b146a948b6907f5d80c7157f99", + "258b7c635c1045329d4669e48c46ccd5", + "6f68ed9889f54ad2ae8a3b95ac263a83", + "80366349d81e4dcc892db6cd56e384f3", + "c73055099c084dca996159e23e162d0b", + "977f799afaac4a55b2dc1cffa7d5b63b", + "41f3b32c2f6b4034ae7a3b9124e28bc7", + "a10d0a76010f4e508c65a9b69ebc5156", + "f8ef805b776145c3bfa9ba8d90972058", + "cc587493c33c4f118d1b1170f85be24c", + "e40d1c1ac9494b3bade9858324e7ffdf", + "d65b6b060d9845779299491ac5599c31", + "0f6907ebbc6242c8bde059cef1e1bd29", + "5bdfd87fc6cd4f9dabef7cfee29c8060", + "64f54d4a744a4627a07c3c0120276f3b", + "65b75b9b8bc143cf997796af68ff6668", + "d6fe74e4255444368f8f90a62157d869", + "4d468f96ec924681ad65eb671674b93e", + "ad7599de524549c48bf2d3124ad4b299", + "0546d04aae644dde846c58a4afb598a6", + "897b77a56c09479bb11d7f2a30997e55", + "81c3db71ac704280ad030072655f1537", + "042e091f75694c47aee761e760e76773", + "ef0a3c7a6f14460fb4da096928ae249e", + "07fb3a2c8315494e97b447e672dfae06", + "ec030fc3c346426f9abc3a89892258d3", + "e3fb3fc6afe04b3c9b7ac61809ce78fa", + "c3be9109d63c485d9c0ef4f9bc0f9218", + "12815f401eba44658caa7b2e490137a8", + "30e02aa2d0d241979369e598287f2639", + "dfd2a2649b8341ef913207526708aff1", + "4f1977d7e4824ef1a14b65f0f42bba10", + "c6164e05a1914ae48083db9ad7f4ef7c", + "813621384dc748b0ad06775e22761c0b", + "dc892a596f6942d7973c616c38f0eebb", + "c84cc07789be48aebb322c23d355289e", + "bed8726b8069434687c75452e21f19e5", + "16a188a0b06d45f980dcf3933509fe0a", + "60c1a0d765c14a1d888317e6a507e4ea", + "0077aedc3d174560bce924ee89e9c006", + "00321cce58884f6f9b3855a21fcd9187", + "fa864b41586f4a7aa56aeafd1d84eb75", + "3225603166b54e7aab766b9964a2f660", + "349eee9f56d64f0cba6fc24ff2c50c9b", + "7e5d3774060e4589aa65982da5ea4ef4", + "7c2485c6cdfe463da6fdb35982a1070d", + "ad1236893754446881e153adc9d5c962", + "daee63fd167e4441a32324b51b00ad2b", + "fe41858c6bd04c58840112b67c19a336", + "d262c82138024169b9f3aa034ca756fa", + "62e302ebdad64aada0ffe64ae1c873f3", + "bd1b0dfed6d34d16af33a4a58330f5ec", + "d07c8b97d3314f1c852e44bdd40f61ed", + "ebb69a2c3d0a4299a484698287b3087c", + "e5a82df528bb4e408797a3b6c2758f4a", + "f113ebd8c1c34806bea4dd7ed3035173" + ] + }, + "id": "KQQhgK8FoDfF", + "outputId": "f69441d8-95f9-4885-c306-6c8709090ff6" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b82aa8c57f7c422a9a9c90f333ed2a99", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "tokenizer_config.json: 0%| | 0.00/9.68k [00:00\u001b[39m\n", + "[2025-05-08 13:41:00,845] [DEBUG] [axolotl.utils.models.load_tokenizer:442] [PID:174] [RANK:0] BOS: None / None\u001b[39m\n", + "[2025-05-08 13:41:00,846] [DEBUG] [axolotl.utils.models.load_tokenizer:443] [PID:174] [RANK:0] PAD: 151643 / <|endoftext|>\u001b[39m\n", + "[2025-05-08 13:41:00,847] [DEBUG] [axolotl.utils.models.load_tokenizer:444] [PID:174] [RANK:0] UNK: None / None\u001b[39m\n", + "[2025-05-08 13:41:00,869] [INFO] [axolotl.utils.data.sft.load_tokenized_prepared_datasets:271] [PID:174] [RANK:0] Unable to find prepared dataset in last_run_prepared/97037817611d38b3a9c681753c3c4c95\u001b[39m\n", + "[2025-05-08 13:41:00,870] [INFO] [axolotl.utils.data.sft.load_tokenized_prepared_datasets:272] [PID:174] [RANK:0] Loading raw datasets...\u001b[39m\n", + "\u001b[33m[2025-05-08 13:41:00,870] [WARNING] [axolotl.utils.data.sft.load_tokenized_prepared_datasets:274] [PID:174] [RANK:0] Processing datasets during training can lead to VRAM instability. Please pre-process your dataset.\u001b[39m\n", + "[2025-05-08 13:41:00,871] [INFO] [axolotl.utils.data.sft.load_tokenized_prepared_datasets:281] [PID:174] [RANK:0] No seed provided, using default seed of 42\u001b[39m\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7cd0b85ebd204b7aba908417811ce4e0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "train.jsonl: 0%| | 0.00/27.3M [00:00system\\n' }}\n", + " {%- if messages[0].role == 'system' %}\n", + " {{- messages[0].content + '\\n\\n' }}\n", + " {%- endif %}\n", + " {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n", + " {%- for tool in tools %}\n", + " {{- \"\\n\" }}\n", + " {{- tool | tojson }}\n", + " {%- endfor %}\n", + " {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n", + "{%- else %}\n", + " {%- if messages[0].role == 'system' %}\n", + " {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n", + " {%- endif %}\n", + "{%- endif %}\n", + "{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n", + "{%- for message in messages[::-1] %}\n", + " {%- set index = (messages|length - 1) - loop.index0 %}\n", + " {%- if ns.multi_step_tool and message.role == \"user\" and not(message.content.startswith('') and message.content.endswith('')) %}\n", + " {%- set ns.multi_step_tool = false %}\n", + " {%- set ns.last_query_index = index %}\n", + " {%- endif %}\n", + "{%- endfor %}\n", + "{%- for message in messages %}\n", + " {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n", + " {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n", + " {%- elif message.role == \"assistant\" %}\n", + " {%- set content = message.content %}\n", + " {%- set reasoning_content = '' %}\n", + " {%- if message.reasoning_content is defined and message.reasoning_content is not none %}\n", + " {%- set reasoning_content = message.reasoning_content %}\n", + " {%- else %}\n", + " {%- if '' in message.content %}\n", + " {%- set content = message.content.split('')[-1].lstrip('\\n') %}\n", + " {%- set reasoning_content = message.content.split('')[0].rstrip('\\n').split('')[-1].lstrip('\\n') %}\n", + " {%- endif %}\n", + " {%- endif %}\n", + " {%- if loop.index0 > ns.last_query_index %}\n", + " {%- if loop.last or (not loop.last and reasoning_content) %}\n", + " {{- '<|im_start|>' + message.role + '\\n\\n' + reasoning_content.strip('\\n') + '\\n\\n\\n' + content.lstrip('\\n') }}\n", + " {%- else %}\n", + " {{- '<|im_start|>' + message.role + '\\n' + content }}\n", + " {%- endif %}\n", + " {%- else %}\n", + " {{- '<|im_start|>' + message.role + '\\n' + content }}\n", + " {%- endif %}\n", + " {%- if message.tool_calls %}\n", + " {%- for tool_call in message.tool_calls %}\n", + " {%- if (loop.first and content) or (not loop.first) %}\n", + " {{- '\\n' }}\n", + " {%- endif %}\n", + " {%- if tool_call.function %}\n", + " {%- set tool_call = tool_call.function %}\n", + " {%- endif %}\n", + " {{- '\\n{\"name\": \"' }}\n", + " {{- tool_call.name }}\n", + " {{- '\", \"arguments\": ' }}\n", + " {%- if tool_call.arguments is string %}\n", + " {{- tool_call.arguments }}\n", + " {%- else %}\n", + " {{- tool_call.arguments | tojson }}\n", + " {%- endif %}\n", + " {{- '}\\n' }}\n", + " {%- endfor %}\n", + " {%- endif %}\n", + " {{- '<|im_end|>\\n' }}\n", + " {%- elif message.role == \"tool\" %}\n", + " {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n", + " {{- '<|im_start|>user' }}\n", + " {%- endif %}\n", + " {{- '\\n\\n' }}\n", + " {{- message.content }}\n", + " {{- '\\n' }}\n", + " {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n", + " {{- '<|im_end|>\\n' }}\n", + " {%- endif %}\n", + " {%- endif %}\n", + "{%- endfor %}\n", + "{%- if add_generation_prompt %}\n", + " {{- '<|im_start|>assistant\\n' }}\n", + " {%- if enable_thinking is defined and enable_thinking is false %}\n", + " {{- '\\n\\n\\n\\n' }}\n", + " {%- endif %}\n", + "{%- endif %}\n", + "---\u001b[39m\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "258b7c635c1045329d4669e48c46ccd5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Tokenizing Prompts (num_proc=2): 0%| | 0/9985 [00:00\n", + " \n", + " \n", + " [25/25 09:25, Epoch 0/1]\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
StepTraining Loss
11.092300
21.554200
31.041400
41.733800
51.430000
61.258500
71.343600
81.101700
91.086500
100.813200
110.689600
120.826700
131.541800
140.948000
151.357000
161.085800
171.516800
181.146800
190.834800
200.968000
211.388800
221.511500
231.338500
241.206600
251.504600

" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2025-05-07 22:12:42,746] [INFO] [axolotl.callbacks.on_step_end:128] [PID:1336] [RANK:0] cuda memory usage while training: 9.768GB (+3.287GB cache, +0.646GB misc)\u001b[39m\n", + "[2025-05-07 22:21:46,859] [INFO] [axolotl.train.save_trained_model:231] [PID:1336] [RANK:0] Training completed! Saving pre-trained model to ./outputs/qwen-sft-pirate-rrr.\u001b[39m\n" + ] + } + ], + "source": [ + "from axolotl.train import train\n", + "\n", + "# just train the first 25 steps for demo.\n", + "# This is sufficient to align the model as we've used packing to maximize the trainable samples per step.\n", + "cfg.max_steps = 25\n", + "model, tokenizer, trainer = train(cfg=cfg, dataset_meta=dataset_meta)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "j1b9ypF78eCb" + }, + "source": [ + "# Inferencing the trained model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "r3_vHhif8YEs", + "outputId": "e5050605-f6c9-421c-98f9-bde56a281eae" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ahoy there, matey! Shiver me timbers, ye be lookin' for the Pythagorean theorem, eh? Well, hold yer horses and listen up, for I'll be tellin' ye all about it in me own special way.\n", + "\n", + "The Pythagorean theorem be a real gem of a mathematical trick that helps ye find the length of a side of a right triangle. Now, a right triangle be a triangle with a right angle, which be that little corner that looks like a square. \n", + "\n", + "The theorem be named after a clever fellow named Pythagoras, who be a mathematician from ancient Greece. He discovered that if ye have a right triangle, the square of the length of the hypotenuse (that be the side opposite the right angle) be equal to the sum of the squares of the other two sides. \n", + "\n", + "In other words, if ye have a triangle with sides of length a, b, and c (\n" + ] + } + ], + "source": [ + "import torch\n", + "from transformers import TextStreamer\n", + "\n", + "messages = [\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": \"Explain the Pythagorean theorem to me.\",\n", + " },\n", + "]\n", + "\n", + "prompt = tokenizer.apply_chat_template(\n", + " messages,\n", + " add_generation_prompt=True,\n", + " tokenize=False,\n", + " enable_thinking = False,\n", + ")\n", + "\n", + "outputs = model.generate(\n", + " **tokenizer(prompt, return_tensors = \"pt\").to(\"cuda\"),\n", + " max_new_tokens = 192,\n", + " temperature = 1.0, top_p = 0.8, top_k = 32,\n", + " streamer = TextStreamer(tokenizer, skip_prompt = True),\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HoGwT2JRSIjA" + }, + "source": [ + "# Saving your trained model\n", + "\n", + "Axolotl automatically saves checkpoints to the `output_dir` path.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "5BmSbiy6NaaS", + "outputId": "f5e1d913-7d55-42d2-8340-f9f1b0bc2b38" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "total 506M\n", + "-rw-r--r-- 1 root root 845 May 7 22:21 adapter_config.json\n", + "-rw-r--r-- 1 root root 491M May 7 22:21 adapter_model.safetensors\n", + "-rw-r--r-- 1 root root 707 May 7 22:11 added_tokens.json\n", + "drwxr-xr-x 2 root root 4.0K May 7 22:17 checkpoint-13\n", + "drwxr-xr-x 2 root root 4.0K May 7 22:21 checkpoint-25\n", + "-rw-r--r-- 1 root root 1.2K May 7 22:11 config.json\n", + "-rw-r--r-- 1 root root 1.6M May 7 22:11 merges.txt\n", + "-rw-r--r-- 1 root root 2.6K May 7 22:21 README.md\n", + "-rw-r--r-- 1 root root 613 May 7 22:11 special_tokens_map.json\n", + "-rw-r--r-- 1 root root 9.5K May 7 22:11 tokenizer_config.json\n", + "-rw-r--r-- 1 root root 11M May 7 22:11 tokenizer.json\n", + "-rw-r--r-- 1 root root 2.7M May 7 22:11 vocab.json\n" + ] + } + ], + "source": [ + "# Show the saved checkpoints in the output_dir\n", + "!ls -lh \"./outputs/qwen-sft-pirate-rrr\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_PCIFWxuOZd6" + }, + "source": [ + "Setting `hub_model_id: ` in the original config would have automatically uploaded the model to HuggingFace Hub (e.g. `hub_model_id: username/model_id`)\n", + "\n", + "If you prefer to manually upload the training artifacts, we can still upload the entire final checkpoint to HuggingFace from the CLI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 955, + "referenced_widgets": [ + "c12ea43372ac4d57bb9605f1a429b397", + "86816687746246b4a6105e8010384e25", + "6f05e9bebf7b40c9835808e77de6c236", + "c7433acd3c4841e6958ae8f7e87b1808", + "19c1e38389fa46c7b7e2152a56e1df34", + "0e067d8db8ed48308a718d5f57683fd1", + "131065f118274a1586ac38e39ed84ef0", + "8640ac440fbc4644b9a3af7ba3ae7183", + "5cea7996f02040b187ece0bb2d6a8d1f", + "2e257c8be2da40b4bb67a9e4ab6811f3", + "56e3768bef5a4b9db4168c5c17f509c2", + "62c028fdef904dedb9cdeca2b3bda725", + "a7cf477e80fc43e0ad82c7997b076dce", + "835bcc28a5564fb9b3d651bc8e32dc46", + "9f1c9a0695384bdaa6f8b847ef89bee8", + "b1bea589efa14258a9982071b87938bf", + "590eef89881545aa8bbef9a8bbe7fb00", + "4b1f04ff63d14a118fdd15814dff50e4", + "39789237703c4a418134243055c9cbf5", + "a3a945817f684328b34651fe052393ec" + ] + }, + "id": "2yw8pLvlSMl8", + "outputId": "6e489ab2-4abe-4e28-84ca-959f912433a4" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c12ea43372ac4d57bb9605f1a429b397", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HTML(value='

\n", + " sys.exit(main())\n", + " ^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/commands/huggingface_cli.py\", line 57, in main\n", + " service.run()\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/commands/upload.py\", line 207, in run\n", + " print(self._upload())\n", + " ^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/commands/upload.py\", line 302, in _upload\n", + " return self.api.upload_folder(\n", + " ^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_validators.py\", line 114, in _inner_fn\n", + " return fn(*args, **kwargs)\n", + " ^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 1633, in _inner\n", + " return fn(self, *args, **kwargs)\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 4942, in upload_folder\n", + " commit_info = self.create_commit(\n", + " ^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_validators.py\", line 114, in _inner_fn\n", + " return fn(*args, **kwargs)\n", + " ^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 1633, in _inner\n", + " return fn(self, *args, **kwargs)\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 4202, in create_commit\n", + " self.preupload_lfs_files(\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 4483, in preupload_lfs_files\n", + " _upload_xet_files(**upload_kwargs, create_pr=create_pr) # type: ignore [arg-type]\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_validators.py\", line 114, in _inner_fn\n", + " return fn(*args, **kwargs)\n", + " ^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/_commit_api.py\", line 592, in _upload_xet_files\n", + " with progress_cm as progress:\n", + " File \"/usr/local/lib/python3.11/dist-packages/tqdm/std.py\", line 1138, in __exit__\n", + " def __exit__(self, exc_type, exc_value, traceback):\n", + "\n", + "KeyboardInterrupt\n", + "^C\n" + ] + } + ], + "source": [ + "from huggingface_hub import notebook_login\n", + "# remove the partial epoch checkpoints\n", + "!rm -rf \"./outputs/qwen-sft-pirate-rrr/checkpoint-*\"\n", + "\n", + "# HF Notebook login widget\n", + "notebook_login()\n", + "\n", + "# upload the LoRA adapter for your model to HF, remember to update the username/model-name below\n", + "!huggingface-cli upload --repo-type=model winglian/pirate-qwen-14B \"./outputs/qwen-sft-pirate-rrr\"" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "00321cce58884f6f9b3855a21fcd9187": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "004d9177a6a14118a5930dc3cc13147b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_a80410b919e442c49aea15acc1ce1a72", + "IPY_MODEL_c6e00f5224364822bc4239b176686919", + "IPY_MODEL_ec11d1e5ae7b42c883d9b1f38a65356e" + ], + "layout": "IPY_MODEL_734185351eb543fa9a00a881dcbb9fe7" + } + }, + "0077aedc3d174560bce924ee89e9c006": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "03a3c744d716431488163b4358b80f92": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "03b093d592ba4386aa61f7b8483da660": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_b8766a88716948cf968f4563531a76d9", + "IPY_MODEL_6f3a28b912714c6e931003549664bfa3", + "IPY_MODEL_16d1283741404b7bb319094c992fce01" + ], + "layout": "IPY_MODEL_2a5bb0e818ab47be8cf6465988328503" + } + }, + "042e091f75694c47aee761e760e76773": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "0546d04aae644dde846c58a4afb598a6": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "054c8dffadba48c6b895a6cc62448ecc": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "07fb3a2c8315494e97b447e672dfae06": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_12815f401eba44658caa7b2e490137a8", + "placeholder": "​", + "style": "IPY_MODEL_30e02aa2d0d241979369e598287f2639", + "value": "Drop Samples with Zero Trainable Tokens (num_proc=2): 100%" + } + }, + "083f9cda8d754c168beee10d2f8955a2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_a0a11e929edd4189b79723d618522c33", + "max": 728, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_e87ea87fcff247b5bbcc331ba79a8dc2", + "value": 728 + } + }, + "09007681cf8d42aeb8c1d2f6a74e470a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b195f160ca20442fadd8b5aed0ee41af", + "placeholder": "​", + "style": "IPY_MODEL_ca65e32eb52f48c09a84b33cb18f22cd", + "value": " 11.4M/11.4M [00:00<00:00, 21.8MB/s]" + } + }, + "0a46ad75c198463d843fb35e813642cb": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b8e39e4dddc3497fbc29ae45c66da759", + "max": 11422654, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_63b4e563e85c4f03b1b72beda9577bcc", + "value": 11422654 + } + }, + "0aa8ab56b85f4171a79c3bc210594025": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "0b4c9753a7cb4354b8e5f187e6e1ad7c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "0cd7efffbb3c4c4b972e63749f61ab97": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "0dea5caa27384f5689e3cab51f558727": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0e067d8db8ed48308a718d5f57683fd1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b1bea589efa14258a9982071b87938bf", + "placeholder": "​", + "style": "IPY_MODEL_590eef89881545aa8bbef9a8bbe7fb00", + "value": "\nPro Tip: If you don't already have one, you can create a dedicated\n'notebooks' token with 'write' access, that you can then easily reuse for all\nnotebooks.
" + } + }, + "0e50870ed0c643e0b6c18cc5d7ddae7f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_bfcdbba993b74972a9e3e575f86908ff", + "placeholder": "​", + "style": "IPY_MODEL_6ebb2ec171414e47a14765505f64bb3c", + "value": " 3.84G/3.84G [00:09<00:00, 664MB/s]" + } + }, + "0e936d9dbf9c4fdd86bbfe9730dedc47": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "0f417447a7bd4a33acca96fa37aec877": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "0f480e3a0b0a45d2a2d2dec3cad923f3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0f6907ebbc6242c8bde059cef1e1bd29": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_5bdfd87fc6cd4f9dabef7cfee29c8060", + "IPY_MODEL_64f54d4a744a4627a07c3c0120276f3b", + "IPY_MODEL_65b75b9b8bc143cf997796af68ff6668" + ], + "layout": "IPY_MODEL_d6fe74e4255444368f8f90a62157d869" + } + }, + "114dece49dba437c8572ef94b23c3b1e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "12815f401eba44658caa7b2e490137a8": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "12b56912736849fea2ad8124456fdc5c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_97e36007e1304e1583fd81bfb13f0edd", + "max": 1671853, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_c65dc74c7d6f4bab8f7dd28455161dd8", + "value": 1671853 + } + }, + "131065f118274a1586ac38e39ed84ef0": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": "center", + "align_self": null, + "border": null, + "bottom": null, + "display": "flex", + "flex": null, + "flex_flow": "column", + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": "50%" + } + }, + "158c8b85dbf34de6a94b4e35e2fc7d5a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "16a188a0b06d45f980dcf3933509fe0a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_349eee9f56d64f0cba6fc24ff2c50c9b", + "placeholder": "​", + "style": "IPY_MODEL_7e5d3774060e4589aa65982da5ea4ef4", + "value": " 9985/9985 [00:04<00:00, 2604.11 examples/s]" + } + }, + "16d1283741404b7bb319094c992fce01": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_a4e5789584564049b83df7c6c54a3e08", + "placeholder": "​", + "style": "IPY_MODEL_ff3a94b146a948b6907f5d80c7157f99", + "value": " 9985/0 [00:00<00:00, 50763.46 examples/s]" + } + }, + "1811cda0644e4190a9469d1774435d82": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "18357b321ce44d7b8bd9d1c886f69275": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e366ae3fceec4566b9ed303d6c5f90af", + "placeholder": "​", + "style": "IPY_MODEL_5dd7d150dbe04f08b165ce7f2c27cd11", + "value": "model-00008-of-00008.safetensors: 100%" + } + }, + "19127c7bb1554ccbac877059f9a82db0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e400cbf14bcc446a9d33b210cd93550b", + "max": 3963750880, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_71002199df6b40c9a1ac40df5fb27a1b", + "value": 3963750502 + } + }, + "19c1e38389fa46c7b7e2152a56e1df34": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ButtonModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ButtonModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ButtonView", + "button_style": "", + "description": "Login", + "disabled": false, + "icon": "", + "layout": "IPY_MODEL_835bcc28a5564fb9b3d651bc8e32dc46", + "style": "IPY_MODEL_9f1c9a0695384bdaa6f8b847ef89bee8", + "tooltip": "" + } + }, + "1bec6297c90242a88672d195bc09d429": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "1c6f1f10667545aaab958016ba7e2c94": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "1d5117195d4b49eb8f1a73b18419f7ce": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_0dea5caa27384f5689e3cab51f558727", + "placeholder": "​", + "style": "IPY_MODEL_a6f48410b9964fefba0c3009a77dc838", + "value": " 9.68k/9.68k [00:00<00:00, 812kB/s]" + } + }, + "1f7d30f71bbd4547a9150d21da071055": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "200df5e79b9244849e589ecb0250a520": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_f4a1795dc7514a718f478245f521f0ba", + "placeholder": "​", + "style": "IPY_MODEL_5e746eb25bbe416fb585fa24e79f5177", + "value": "model-00002-of-00008.safetensors: 100%" + } + }, + "20352e5f58d24bb8b1f3940efd14fe4a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "253017b0d0534e54ab44e181f6d7c82d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_1c6f1f10667545aaab958016ba7e2c94", + "placeholder": "​", + "style": "IPY_MODEL_e6e969610738449887259063967f82b0", + "value": " 2.78M/2.78M [00:00<00:00, 17.8MB/s]" + } + }, + "258b7c635c1045329d4669e48c46ccd5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_6f68ed9889f54ad2ae8a3b95ac263a83", + "IPY_MODEL_80366349d81e4dcc892db6cd56e384f3", + "IPY_MODEL_c73055099c084dca996159e23e162d0b" + ], + "layout": "IPY_MODEL_977f799afaac4a55b2dc1cffa7d5b63b" + } + }, + "279937fe03bc4e4eb25b472d7e9df163": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b634bb73cfa743d09a5999101b840976", + "max": 1912371880, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_742b1030acfd414bbd9d5327b7e3826d", + "value": 1912371698 + } + }, + "27beaf06e41b472abdb544a43c720c5a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2860e3bb3baf4f7da058465850e800c5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_3efd18ea8eaa41918894883da9541bfa", + "IPY_MODEL_e09f1bcbb9d94c09be53e5e1303642c2", + "IPY_MODEL_82177df57a494de8900c14c2f5185175" + ], + "layout": "IPY_MODEL_ccfcdc95baf646f8aeb3d516742383f2" + } + }, + "2a51b36be41745468e4c2d7a21b1c0d2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2a5bb0e818ab47be8cf6465988328503": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2b3a2659b12244bd8548320320016dbf": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2e257c8be2da40b4bb67a9e4ab6811f3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2e2b0c1599c341a198f632f46a40c90e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_be724f04b03942b2a033a7e8898bb4fd", + "placeholder": "​", + "style": "IPY_MODEL_fcbab4d8dced41a18dfccce81e3a45a0", + "value": "model-00005-of-00008.safetensors: 100%" + } + }, + "3036608c71904ce9ae4bb2a9fa8802d9": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_5ca6be24acb548cea130bd58e9954c7c", + "placeholder": "​", + "style": "IPY_MODEL_5cfb02ee044b4011a378efa8b54a370f", + "value": " 3.96G/3.96G [00:10<00:00, 531MB/s]" + } + }, + "30a81da86f8043eca301e86a8651201a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "30e02aa2d0d241979369e598287f2639": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "3225603166b54e7aab766b9964a2f660": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "33b3b1d0295646edaac7b4822761aeb0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "349eee9f56d64f0cba6fc24ff2c50c9b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "34c9c0137b504cd799c6bd6de69507c2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "34cf3df51fbc41cabfdbba153c007f0e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "35c811d2ae8e43f3b5cecbdd3cfa857f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "35cc989ca3374e7dba0cb166febc4bde": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "366a343b62fa47d8985a3bd464d99f9e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "37de928300e34184881039378bd75e7f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "388f618924274d21a066f098f4f1e744": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_7c95f85a2b1f47a1bd846d110c47bb3c", + "IPY_MODEL_083f9cda8d754c168beee10d2f8955a2", + "IPY_MODEL_62e1a65582f446a78612eaa804e08a7d" + ], + "layout": "IPY_MODEL_487a177d020f4605834878b2fdc7afa3" + } + }, + "39789237703c4a418134243055c9cbf5": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3aaecbf540f54a2db9ab0931e3b1fe57": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3c21e4a511b4441192c03b7f1d0976e9": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3efd18ea8eaa41918894883da9541bfa": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_8f5bd719974e41c3a8dd9a5b0d3d71e6", + "placeholder": "​", + "style": "IPY_MODEL_b87c84de30e84b3abf4871461fb9cbd3", + "value": "Loading checkpoint shards: 100%" + } + }, + "41f3b32c2f6b4034ae7a3b9124e28bc7": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "4471ff62258549fba9514bb67050f965": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_9cd5211b5d8b457aa0002f1d17b80028", + "IPY_MODEL_19127c7bb1554ccbac877059f9a82db0", + "IPY_MODEL_f4667818b9d34a09891cd727a429a610" + ], + "layout": "IPY_MODEL_9ed02dc43412471a9ab47f3620ccf3a5" + } + }, + "4540927d98f54466b434ba4c0edf045d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "487a177d020f4605834878b2fdc7afa3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "4b1f04ff63d14a118fdd15814dff50e4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "LabelModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "LabelModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "LabelView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_39789237703c4a418134243055c9cbf5", + "placeholder": "​", + "style": "IPY_MODEL_a3a945817f684328b34651fe052393ec", + "value": "Connecting..." + } + }, + "4b27c267393640f28f6eae0875bd2ed9": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "4c727d40ef0443449afc31724ee79f0c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "4d05314858354e729d76094b3b0ce761": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_c42acf646f344a88b8c11f81e67f7206", + "IPY_MODEL_7be6f04c284e4326bb4ff3d301e7b3c6", + "IPY_MODEL_ffdbb12a2f2c4d14911685e7683e0ef0" + ], + "layout": "IPY_MODEL_bee3501b2a17427784a717e50a85e7fa" + } + }, + "4d468f96ec924681ad65eb671674b93e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "4f1977d7e4824ef1a14b65f0f42bba10": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "4fd114abe9f5494ab59858949f5055f1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "500e272208a246089613bf788a165271": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_200df5e79b9244849e589ecb0250a520", + "IPY_MODEL_cc94432d08464affa3e58b560bdad194", + "IPY_MODEL_3036608c71904ce9ae4bb2a9fa8802d9" + ], + "layout": "IPY_MODEL_adacfdcc1b0140efac56918e9ccf064e" + } + }, + "519a7b154022443db6703f04a9142bae": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d02274afd47b462291c745f261209d42", + "max": 27341251, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_0f417447a7bd4a33acca96fa37aec877", + "value": 27341251 + } + }, + "56e3768bef5a4b9db4168c5c17f509c2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "590eef89881545aa8bbef9a8bbe7fb00": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "598da69727bd4fb8b1caf465ac736d7a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5bdfd87fc6cd4f9dabef7cfee29c8060": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_4d468f96ec924681ad65eb671674b93e", + "placeholder": "​", + "style": "IPY_MODEL_ad7599de524549c48bf2d3124ad4b299", + "value": "Dropping Long Sequences (num_proc=2): 100%" + } + }, + "5ca240f31e6b44e3882c5eb37cd5a309": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": "20px" + } + }, + "5ca6be24acb548cea130bd58e9954c7c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "5cea7996f02040b187ece0bb2d6a8d1f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5cfb02ee044b4011a378efa8b54a370f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5dd7d150dbe04f08b165ce7f2c27cd11": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5e18768f7ad6434ba8b8b8a2e853e204": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "5e5e15b0569b474c9620083b3ec6af55": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5e746eb25bbe416fb585fa24e79f5177": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5eb06edeb58e4930b1affef2a59eae81": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "5f86cd894de94c3280fadc1e2fd0ee13": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_a20927bf5f2c41f58c1e31ac858ab36c", + "IPY_MODEL_0a46ad75c198463d843fb35e813642cb", + "IPY_MODEL_09007681cf8d42aeb8c1d2f6a74e470a" + ], + "layout": "IPY_MODEL_ebc80d1a55fa47f4a5ea2756588569ec" + } + }, + "60c1a0d765c14a1d888317e6a507e4ea": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "62c028fdef904dedb9cdeca2b3bda725": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "62e1a65582f446a78612eaa804e08a7d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_5e18768f7ad6434ba8b8b8a2e853e204", + "placeholder": "​", + "style": "IPY_MODEL_bb33aec33a6447078c31bfd728942994", + "value": " 728/728 [00:00<00:00, 20.3kB/s]" + } + }, + "62e302ebdad64aada0ffe64ae1c873f3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "63580b6fb30642479fe3000915bf551a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "63b4e563e85c4f03b1b72beda9577bcc": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "64f54d4a744a4627a07c3c0120276f3b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_0546d04aae644dde846c58a4afb598a6", + "max": 9985, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_897b77a56c09479bb11d7f2a30997e55", + "value": 9985 + } + }, + "65b75b9b8bc143cf997796af68ff6668": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_81c3db71ac704280ad030072655f1537", + "placeholder": "​", + "style": "IPY_MODEL_042e091f75694c47aee761e760e76773", + "value": " 9985/9985 [00:02<00:00, 3977.47 examples/s]" + } + }, + "67da6c4260574869aa24c3cbc1bc1654": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "6932489232ec4ab18a160b1e7fbcdfe1": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "6ebb2ec171414e47a14765505f64bb3c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "6f05e9bebf7b40c9835808e77de6c236": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "PasswordModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "PasswordModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "PasswordView", + "continuous_update": true, + "description": "Token:", + "description_tooltip": null, + "disabled": false, + "layout": "IPY_MODEL_2e257c8be2da40b4bb67a9e4ab6811f3", + "placeholder": "​", + "style": "IPY_MODEL_56e3768bef5a4b9db4168c5c17f509c2", + "value": "" + } + }, + "6f3a28b912714c6e931003549664bfa3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_5ca240f31e6b44e3882c5eb37cd5a309", + "max": 1, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_5eb06edeb58e4930b1affef2a59eae81", + "value": 1 + } + }, + "6f68ed9889f54ad2ae8a3b95ac263a83": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_41f3b32c2f6b4034ae7a3b9124e28bc7", + "placeholder": "​", + "style": "IPY_MODEL_a10d0a76010f4e508c65a9b69ebc5156", + "value": "Tokenizing Prompts (num_proc=2): 100%" + } + }, + "704f2f5a9b1c49d5a75a0025a5dda11b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "71002199df6b40c9a1ac40df5fb27a1b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "71c8af139cd248b1b51101fd46a93f35": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d0e9dce55cec4c1ca619a0ccf209d924", + "max": 9675, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_4c727d40ef0443449afc31724ee79f0c", + "value": 9675 + } + }, + "734185351eb543fa9a00a881dcbb9fe7": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "735d4f225b24414294fc1b213c61223c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "742b1030acfd414bbd9d5327b7e3826d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "77304d1a46b3468a98483e02ec0ac4a4": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "7baeab52d6694c32b1efd1ea1a0a7782": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_93a44a11aa4846fa8efc6c1413ef1627", + "placeholder": "​", + "style": "IPY_MODEL_a55060adc3564407ac81ad7297d34aaa", + "value": "train.jsonl: 100%" + } + }, + "7be6f04c284e4326bb4ff3d301e7b3c6": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_9503a45960984adc97b58e16c50662e0", + "max": 3963750880, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_da6e93f3e4984780b930fe7a706983ea", + "value": 3963750502 + } + }, + "7c2485c6cdfe463da6fdb35982a1070d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_ad1236893754446881e153adc9d5c962", + "IPY_MODEL_daee63fd167e4441a32324b51b00ad2b", + "IPY_MODEL_fe41858c6bd04c58840112b67c19a336" + ], + "layout": "IPY_MODEL_d262c82138024169b9f3aa034ca756fa" + } + }, + "7c95f85a2b1f47a1bd846d110c47bb3c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_7fd44cf9ca6e4726bfd7ac21846d6a14", + "placeholder": "​", + "style": "IPY_MODEL_366a343b62fa47d8985a3bd464d99f9e", + "value": "config.json: 100%" + } + }, + "7cd0b85ebd204b7aba908417811ce4e0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_7baeab52d6694c32b1efd1ea1a0a7782", + "IPY_MODEL_519a7b154022443db6703f04a9142bae", + "IPY_MODEL_d4183e9715f34d249942b8271cca3bdf" + ], + "layout": "IPY_MODEL_da2347ac94764a3fa2743343cf0d3cd2" + } + }, + "7e5d3774060e4589aa65982da5ea4ef4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "7fd44cf9ca6e4726bfd7ac21846d6a14": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "80366349d81e4dcc892db6cd56e384f3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_f8ef805b776145c3bfa9ba8d90972058", + "max": 9985, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_cc587493c33c4f118d1b1170f85be24c", + "value": 9985 + } + }, + "813621384dc748b0ad06775e22761c0b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "81c3db71ac704280ad030072655f1537": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "82177df57a494de8900c14c2f5185175": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_67da6c4260574869aa24c3cbc1bc1654", + "placeholder": "​", + "style": "IPY_MODEL_94b9088614464f60a203de39dbcae853", + "value": " 8/8 [01:47<00:00, 11.64s/it]" + } + }, + "823f1c78f15043e38bbd4dca3932a86a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_03a3c744d716431488163b4358b80f92", + "max": 239, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_a5434ee714f9498d83870544b67c0cb7", + "value": 239 + } + }, + "835bcc28a5564fb9b3d651bc8e32dc46": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8640ac440fbc4644b9a3af7ba3ae7183": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "86816687746246b4a6105e8010384e25": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_8640ac440fbc4644b9a3af7ba3ae7183", + "placeholder": "​", + "style": "IPY_MODEL_5cea7996f02040b187ece0bb2d6a8d1f", + "value": "

Copy a token from your Hugging Face\ntokens page and paste it below.
Immediately click login after copying\nyour token or it might be stored in plain text in this notebook file.
" + } + }, + "879c8ab5873847a8833bd74123be90a4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_ef223e8504b64e3592589880326aaf41", + "placeholder": "​", + "style": "IPY_MODEL_598da69727bd4fb8b1caf465ac736d7a", + "value": " 1.67M/1.67M [00:00<00:00, 19.0MB/s]" + } + }, + "897b77a56c09479bb11d7f2a30997e55": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "8bc9d8ba866c442b9118d9630009939c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8c4d4fc5a30f4e7cb3be53fe2adda33d": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8f5bd719974e41c3a8dd9a5b0d3d71e6": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8f726dbfb45d4528afa33e36a6313267": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "9327977822be4b1294f80e876552e305": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_37de928300e34184881039378bd75e7f", + "placeholder": "​", + "style": "IPY_MODEL_0e936d9dbf9c4fdd86bbfe9730dedc47", + "value": " 3.96G/3.96G [00:13<00:00, 273MB/s]" + } + }, + "936d04b5fe1b4c63bf0b080e423d051b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "93a44a11aa4846fa8efc6c1413ef1627": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "94b9088614464f60a203de39dbcae853": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "9503a45960984adc97b58e16c50662e0": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "95caff42f08a4c2aa14c867b8f37f231": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_de7c37ee83e24f0c889e84d07279c2ec", + "IPY_MODEL_9d4897eefb5f48259ffb2d23e332f752", + "IPY_MODEL_253017b0d0534e54ab44e181f6d7c82d" + ], + "layout": "IPY_MODEL_27beaf06e41b472abdb544a43c720c5a" + } + }, + "977f799afaac4a55b2dc1cffa7d5b63b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "97e36007e1304e1583fd81bfb13f0edd": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "9858cb74a09748a39e8149baac96702c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "9b42e08b3c9548818488268768a118b1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d955dcaa0e944e719f3a06139dd54a03", + "placeholder": "​", + "style": "IPY_MODEL_d3de2662c7964f1ba96e58da382af720", + "value": "merges.txt: 100%" + } + }, + "9cd5211b5d8b457aa0002f1d17b80028": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_6932489232ec4ab18a160b1e7fbcdfe1", + "placeholder": "​", + "style": "IPY_MODEL_4540927d98f54466b434ba4c0edf045d", + "value": "model-00007-of-00008.safetensors: 100%" + } + }, + "9d4897eefb5f48259ffb2d23e332f752": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_30a81da86f8043eca301e86a8651201a", + "max": 2776833, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_e8b7a81040904c1e89e58978223b1737", + "value": 2776833 + } + }, + "9e333ed3b5014069ac1dd969255dd591": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "9ed02dc43412471a9ab47f3620ccf3a5": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "9f1c9a0695384bdaa6f8b847ef89bee8": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ButtonStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ButtonStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "button_color": null, + "font_weight": "" + } + }, + "9f56a2d9979c4bd8928c644c22c3ecdf": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "a0a11e929edd4189b79723d618522c33": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a10d0a76010f4e508c65a9b69ebc5156": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "a138859f19b74fc0928dc236ab5359db": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_9b42e08b3c9548818488268768a118b1", + "IPY_MODEL_12b56912736849fea2ad8124456fdc5c", + "IPY_MODEL_879c8ab5873847a8833bd74123be90a4" + ], + "layout": "IPY_MODEL_20352e5f58d24bb8b1f3940efd14fe4a" + } + }, + "a1959759c5424da9961fb2a308d4dee4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_3aaecbf540f54a2db9ab0931e3b1fe57", + "placeholder": "​", + "style": "IPY_MODEL_9e333ed3b5014069ac1dd969255dd591", + "value": " 239/239 [00:00<00:00, 30.9kB/s]" + } + }, + "a20927bf5f2c41f58c1e31ac858ab36c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_1811cda0644e4190a9469d1774435d82", + "placeholder": "​", + "style": "IPY_MODEL_35c811d2ae8e43f3b5cecbdd3cfa857f", + "value": "tokenizer.json: 100%" + } + }, + "a3a945817f684328b34651fe052393ec": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "a44f630e099e43899f20a77084ae60cd": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_ed5ca967ad5342929e578ac6aa4dc4c0", + "placeholder": "​", + "style": "IPY_MODEL_af401d117d5047629d3a6e2361757b62", + "value": "model-00001-of-00008.safetensors: 100%" + } + }, + "a4e5789584564049b83df7c6c54a3e08": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a5434ee714f9498d83870544b67c0cb7": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "a55060adc3564407ac81ad7297d34aaa": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "a6f48410b9964fefba0c3009a77dc838": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "a7cf477e80fc43e0ad82c7997b076dce": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "a80410b919e442c49aea15acc1ce1a72": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_fa1282ccc7544e4f818e2f03ccffe4a5", + "placeholder": "​", + "style": "IPY_MODEL_bbbf575d2a4b4c6ea8389be79b2a6039", + "value": "model.safetensors.index.json: 100%" + } + }, + "ab93eabd7cea4b94b4b7a387f101e8a1": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ac764024cf1c4e08ba7749afd2cd20ac": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "ad1236893754446881e153adc9d5c962": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_62e302ebdad64aada0ffe64ae1c873f3", + "placeholder": "​", + "style": "IPY_MODEL_bd1b0dfed6d34d16af33a4a58330f5ec", + "value": "Saving the dataset (1/1 shards): 100%" + } + }, + "ad7599de524549c48bf2d3124ad4b299": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "adacfdcc1b0140efac56918e9ccf064e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "af401d117d5047629d3a6e2361757b62": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "b191ac001a2e4962bc9a245fcdf26e6b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b195f160ca20442fadd8b5aed0ee41af": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b1bea589efa14258a9982071b87938bf": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b5b65414154544aa8a71b1a39164aad7": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b634bb73cfa743d09a5999101b840976": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b82aa8c57f7c422a9a9c90f333ed2a99": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_c0991cf63ee6458b96e9a75e7a88b61a", + "IPY_MODEL_71c8af139cd248b1b51101fd46a93f35", + "IPY_MODEL_1d5117195d4b49eb8f1a73b18419f7ce" + ], + "layout": "IPY_MODEL_3c21e4a511b4441192c03b7f1d0976e9" + } + }, + "b8766a88716948cf968f4563531a76d9": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_2b3a2659b12244bd8548320320016dbf", + "placeholder": "​", + "style": "IPY_MODEL_0cd7efffbb3c4c4b972e63749f61ab97", + "value": "Generating train split: " + } + }, + "b87c84de30e84b3abf4871461fb9cbd3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "b8e39e4dddc3497fbc29ae45c66da759": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bb33aec33a6447078c31bfd728942994": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "bbbf575d2a4b4c6ea8389be79b2a6039": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "bca2c7185b6749fd899c06a2ba4c5e46": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_0f480e3a0b0a45d2a2d2dec3cad923f3", + "placeholder": "​", + "style": "IPY_MODEL_fcb30372e7404c5d8a1ad4df91e6c7b2", + "value": " 1.91G/1.91G [00:05<00:00, 444MB/s]" + } + }, + "bd1b0dfed6d34d16af33a4a58330f5ec": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "be724f04b03942b2a033a7e8898bb4fd": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bed8726b8069434687c75452e21f19e5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_fa864b41586f4a7aa56aeafd1d84eb75", + "max": 9985, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_3225603166b54e7aab766b9964a2f660", + "value": 9985 + } + }, + "bee3501b2a17427784a717e50a85e7fa": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bfcdbba993b74972a9e3e575f86908ff": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bff139df987d4a62abec6456cb27f3d4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_c1f9c267ba3f40039cdb5eb3267e8043", + "max": 3963750880, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_33b3b1d0295646edaac7b4822761aeb0", + "value": 3963750502 + } + }, + "c0892a1881de4eb4bfabc6a68f87ae99": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_158c8b85dbf34de6a94b4e35e2fc7d5a", + "placeholder": "​", + "style": "IPY_MODEL_0b4c9753a7cb4354b8e5f187e6e1ad7c", + "value": " 3.96G/3.96G [00:15<00:00, 564MB/s]" + } + }, + "c0991cf63ee6458b96e9a75e7a88b61a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_ed28e2e0410d4e0b855467e798e53d66", + "placeholder": "​", + "style": "IPY_MODEL_d93f134f802b4b69b575bdaf07dbd27c", + "value": "tokenizer_config.json: 100%" + } + }, + "c12ea43372ac4d57bb9605f1a429b397": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "VBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "VBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "VBoxView", + "box_style": "", + "children": [], + "layout": "IPY_MODEL_131065f118274a1586ac38e39ed84ef0" + } + }, + "c1314f241a434c41b45d84dc4d3b30f8": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "c1f9c267ba3f40039cdb5eb3267e8043": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c33ced495f70464aa4a3a91922090853": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c3725c7f79fe415fbd1ea336f0cc9cf1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b191ac001a2e4962bc9a245fcdf26e6b", + "max": 3841788544, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_054c8dffadba48c6b895a6cc62448ecc", + "value": 3841788178 + } + }, + "c3be9109d63c485d9c0ef4f9bc0f9218": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c42acf646f344a88b8c11f81e67f7206": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_8bc9d8ba866c442b9118d9630009939c", + "placeholder": "​", + "style": "IPY_MODEL_9f56a2d9979c4bd8928c644c22c3ecdf", + "value": "model-00003-of-00008.safetensors: 100%" + } + }, + "c6164e05a1914ae48083db9ad7f4ef7c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c65dc74c7d6f4bab8f7dd28455161dd8": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "c6e00f5224364822bc4239b176686919": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_2a51b36be41745468e4c2d7a21b1c0d2", + "max": 36514, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_4fd114abe9f5494ab59858949f5055f1", + "value": 36514 + } + }, + "c73055099c084dca996159e23e162d0b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e40d1c1ac9494b3bade9858324e7ffdf", + "placeholder": "​", + "style": "IPY_MODEL_d65b6b060d9845779299491ac5599c31", + "value": " 9985/9985 [01:04<00:00, 189.08 examples/s]" + } + }, + "c7433acd3c4841e6958ae8f7e87b1808": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "CheckboxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "CheckboxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "CheckboxView", + "description": "Add token as git credential?", + "description_tooltip": null, + "disabled": false, + "indent": true, + "layout": "IPY_MODEL_62c028fdef904dedb9cdeca2b3bda725", + "style": "IPY_MODEL_a7cf477e80fc43e0ad82c7997b076dce", + "value": false + } + }, + "c84cc07789be48aebb322c23d355289e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_0077aedc3d174560bce924ee89e9c006", + "placeholder": "​", + "style": "IPY_MODEL_00321cce58884f6f9b3855a21fcd9187", + "value": "Add position_id column (Sample Packing) (num_proc=2): 100%" + } + }, + "ca65e32eb52f48c09a84b33cb18f22cd": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "cc587493c33c4f118d1b1170f85be24c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "cc94432d08464affa3e58b560bdad194": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b5b65414154544aa8a71b1a39164aad7", + "max": 3963750816, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_f0a58fbd0fca4340890041f99fa2f8c8", + "value": 3963750438 + } + }, + "ccfcdc95baf646f8aeb3d516742383f2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "cdebbc55a1164c018546c2ac6f8c620c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_a44f630e099e43899f20a77084ae60cd", + "IPY_MODEL_c3725c7f79fe415fbd1ea336f0cc9cf1", + "IPY_MODEL_0e50870ed0c643e0b6c18cc5d7ddae7f" + ], + "layout": "IPY_MODEL_c33ced495f70464aa4a3a91922090853" + } + }, + "d02274afd47b462291c745f261209d42": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d07c8b97d3314f1c852e44bdd40f61ed": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d0e9dce55cec4c1ca619a0ccf209d924": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d1f9b10c130542f094c8fd3d1e23b5e9": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d262c82138024169b9f3aa034ca756fa": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d3de2662c7964f1ba96e58da382af720": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "d4183e9715f34d249942b8271cca3bdf": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_63580b6fb30642479fe3000915bf551a", + "placeholder": "​", + "style": "IPY_MODEL_8f726dbfb45d4528afa33e36a6313267", + "value": " 27.3M/27.3M [00:00<00:00, 31.0MB/s]" + } + }, + "d43c6df07ddb466587807d6dbe1ff614": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_8c4d4fc5a30f4e7cb3be53fe2adda33d", + "placeholder": "​", + "style": "IPY_MODEL_e90658f4bcb642baa78426012f863152", + "value": "model-00004-of-00008.safetensors: 100%" + } + }, + "d65b6b060d9845779299491ac5599c31": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "d6fe74e4255444368f8f90a62157d869": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d93f134f802b4b69b575bdaf07dbd27c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "d955dcaa0e944e719f3a06139dd54a03": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "da2347ac94764a3fa2743343cf0d3cd2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "da6e93f3e4984780b930fe7a706983ea": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "daee63fd167e4441a32324b51b00ad2b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d07c8b97d3314f1c852e44bdd40f61ed", + "max": 9985, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_ebb69a2c3d0a4299a484698287b3087c", + "value": 9985 + } + }, + "dc892a596f6942d7973c616c38f0eebb": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_c84cc07789be48aebb322c23d355289e", + "IPY_MODEL_bed8726b8069434687c75452e21f19e5", + "IPY_MODEL_16a188a0b06d45f980dcf3933509fe0a" + ], + "layout": "IPY_MODEL_60c1a0d765c14a1d888317e6a507e4ea" + } + }, + "dd0e646fad3f4a89ba23b39d162bd8d9": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_d43c6df07ddb466587807d6dbe1ff614", + "IPY_MODEL_e0e8b840b8ea4d0d9db09afe99fa287d", + "IPY_MODEL_9327977822be4b1294f80e876552e305" + ], + "layout": "IPY_MODEL_77304d1a46b3468a98483e02ec0ac4a4" + } + }, + "de7c37ee83e24f0c889e84d07279c2ec": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_34cf3df51fbc41cabfdbba153c007f0e", + "placeholder": "​", + "style": "IPY_MODEL_ac764024cf1c4e08ba7749afd2cd20ac", + "value": "vocab.json: 100%" + } + }, + "dfd2a2649b8341ef913207526708aff1": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e09f1bcbb9d94c09be53e5e1303642c2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e7d8e4fe58384e93a106de546068c65e", + "max": 8, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_0aa8ab56b85f4171a79c3bc210594025", + "value": 8 + } + }, + "e0e8b840b8ea4d0d9db09afe99fa287d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_f7434f3e03124a1c938a39af79d7fa59", + "max": 3963750880, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_c1314f241a434c41b45d84dc4d3b30f8", + "value": 3963750502 + } + }, + "e21e180307e5485cbbe908672fd6639a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_2e2b0c1599c341a198f632f46a40c90e", + "IPY_MODEL_bff139df987d4a62abec6456cb27f3d4", + "IPY_MODEL_ebe1cc366d324ad59b264c8b3c431441" + ], + "layout": "IPY_MODEL_114dece49dba437c8572ef94b23c3b1e" + } + }, + "e366ae3fceec4566b9ed303d6c5f90af": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e3fb3fc6afe04b3c9b7ac61809ce78fa": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_c6164e05a1914ae48083db9ad7f4ef7c", + "placeholder": "​", + "style": "IPY_MODEL_813621384dc748b0ad06775e22761c0b", + "value": " 9985/9985 [00:03<00:00, 3622.89 examples/s]" + } + }, + "e400cbf14bcc446a9d33b210cd93550b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e40d1c1ac9494b3bade9858324e7ffdf": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e575d87a7efe4ec7b1efde489839d4a6": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "e5a82df528bb4e408797a3b6c2758f4a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e6e969610738449887259063967f82b0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "e7d8e4fe58384e93a106de546068c65e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e87ea87fcff247b5bbcc331ba79a8dc2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "e8b7a81040904c1e89e58978223b1737": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "e90658f4bcb642baa78426012f863152": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "eb1c9535e6a546098b760528b2ea387c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_18357b321ce44d7b8bd9d1c886f69275", + "IPY_MODEL_279937fe03bc4e4eb25b472d7e9df163", + "IPY_MODEL_bca2c7185b6749fd899c06a2ba4c5e46" + ], + "layout": "IPY_MODEL_1f7d30f71bbd4547a9150d21da071055" + } + }, + "ebb69a2c3d0a4299a484698287b3087c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "ebc80d1a55fa47f4a5ea2756588569ec": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ebe1cc366d324ad59b264c8b3c431441": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_fba7aa824b38467ab3061b226114cdec", + "placeholder": "​", + "style": "IPY_MODEL_f3075dccbd2747b4a7913b66f44f2596", + "value": " 3.96G/3.96G [00:13<00:00, 398MB/s]" + } + }, + "ec030fc3c346426f9abc3a89892258d3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_dfd2a2649b8341ef913207526708aff1", + "max": 9985, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_4f1977d7e4824ef1a14b65f0f42bba10", + "value": 9985 + } + }, + "ec11d1e5ae7b42c883d9b1f38a65356e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_936d04b5fe1b4c63bf0b080e423d051b", + "placeholder": "​", + "style": "IPY_MODEL_f1cef8e8dc2646fb9fd09f3b09081074", + "value": " 36.5k/36.5k [00:00<00:00, 4.32MB/s]" + } + }, + "ed28e2e0410d4e0b855467e798e53d66": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ed5ca967ad5342929e578ac6aa4dc4c0": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "edc99591b9c747b689b94d0052fec14c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ef0a3c7a6f14460fb4da096928ae249e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_07fb3a2c8315494e97b447e672dfae06", + "IPY_MODEL_ec030fc3c346426f9abc3a89892258d3", + "IPY_MODEL_e3fb3fc6afe04b3c9b7ac61809ce78fa" + ], + "layout": "IPY_MODEL_c3be9109d63c485d9c0ef4f9bc0f9218" + } + }, + "ef223e8504b64e3592589880326aaf41": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f0a58fbd0fca4340890041f99fa2f8c8": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "f113ebd8c1c34806bea4dd7ed3035173": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "f1cef8e8dc2646fb9fd09f3b09081074": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "f3075dccbd2747b4a7913b66f44f2596": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "f365820a3d3c42b2948abfe32065de14": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_735d4f225b24414294fc1b213c61223c", + "placeholder": "​", + "style": "IPY_MODEL_5e5e15b0569b474c9620083b3ec6af55", + "value": "generation_config.json: 100%" + } + }, + "f4667818b9d34a09891cd727a429a610": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_4b27c267393640f28f6eae0875bd2ed9", + "placeholder": "​", + "style": "IPY_MODEL_9858cb74a09748a39e8149baac96702c", + "value": " 3.96G/3.96G [00:11<00:00, 457MB/s]" + } + }, + "f4a1795dc7514a718f478245f521f0ba": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f60a2bdb6b6b4e0e8c3508580e247132": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_edc99591b9c747b689b94d0052fec14c", + "max": 3963750880, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_35cc989ca3374e7dba0cb166febc4bde", + "value": 3963750502 + } + }, + "f7434f3e03124a1c938a39af79d7fa59": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f8ef805b776145c3bfa9ba8d90972058": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fa1282ccc7544e4f818e2f03ccffe4a5": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fa864b41586f4a7aa56aeafd1d84eb75": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fba7aa824b38467ab3061b226114cdec": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fcb30372e7404c5d8a1ad4df91e6c7b2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "fcbab4d8dced41a18dfccce81e3a45a0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "fd4f333f7ece4450b04e1a9af1f9d2f6": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d1f9b10c130542f094c8fd3d1e23b5e9", + "placeholder": "​", + "style": "IPY_MODEL_e575d87a7efe4ec7b1efde489839d4a6", + "value": "model-00006-of-00008.safetensors: 100%" + } + }, + "fe18bba7f3fb4c31bf840541f36b3425": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_fd4f333f7ece4450b04e1a9af1f9d2f6", + "IPY_MODEL_f60a2bdb6b6b4e0e8c3508580e247132", + "IPY_MODEL_c0892a1881de4eb4bfabc6a68f87ae99" + ], + "layout": "IPY_MODEL_1bec6297c90242a88672d195bc09d429" + } + }, + "fe41858c6bd04c58840112b67c19a336": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e5a82df528bb4e408797a3b6c2758f4a", + "placeholder": "​", + "style": "IPY_MODEL_f113ebd8c1c34806bea4dd7ed3035173", + "value": " 9985/9985 [00:00<00:00, 44264.88 examples/s]" + } + }, + "fea1b70fb46745feb5111b3929175b5d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_f365820a3d3c42b2948abfe32065de14", + "IPY_MODEL_823f1c78f15043e38bbd4dca3932a86a", + "IPY_MODEL_a1959759c5424da9961fb2a308d4dee4" + ], + "layout": "IPY_MODEL_34c9c0137b504cd799c6bd6de69507c2" + } + }, + "ff3a94b146a948b6907f5d80c7157f99": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "ffdbb12a2f2c4d14911685e7683e0ef0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_ab93eabd7cea4b94b4b7a387f101e8a1", + "placeholder": "​", + "style": "IPY_MODEL_704f2f5a9b1c49d5a75a0025a5dda11b", + "value": " 3.96G/3.96G [00:12<00:00, 656MB/s]" + } + } + } + } }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import torch\n", - "# Check so there is a gpu available, a T4(free tier) is enough to run this notebook\n", - "assert (torch.cuda.is_available()==True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install --no-build-isolation axolotl[deepspeed]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Hugging Face login (optional)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from huggingface_hub import notebook_login\n", - "notebook_login()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Example configuration" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import yaml\n", - "\n", - "yaml_string = \"\"\"\n", - "base_model: NousResearch/Meta-Llama-3.1-8B\n", - "\n", - "load_in_8bit: false\n", - "load_in_4bit: true\n", - "strict: false\n", - "\n", - "datasets:\n", - " - path: tatsu-lab/alpaca\n", - " type: alpaca\n", - "dataset_prepared_path: last_run_prepared\n", - "val_set_size: 0.05\n", - "output_dir: ./outputs/lora-out\n", - "\n", - "sequence_len: 2048\n", - "sample_packing: true\n", - "eval_sample_packing: true\n", - "pad_to_sequence_len: true\n", - "\n", - "adapter: qlora\n", - "lora_model_dir:\n", - "lora_r: 32\n", - "lora_alpha: 16\n", - "lora_dropout: 0.05\n", - "lora_target_linear: true\n", - "lora_fan_in_fan_out:\n", - "lora_modules_to_save:\n", - " - embed_tokens\n", - " - lm_head\n", - "\n", - "wandb_project:\n", - "wandb_entity:\n", - "wandb_watch:\n", - "wandb_name:\n", - "wandb_log_model:\n", - "\n", - "gradient_accumulation_steps: 2\n", - "micro_batch_size: 1\n", - "num_epochs: 1\n", - "optimizer: paged_adamw_8bit\n", - "lr_scheduler: cosine\n", - "learning_rate: 2e-5\n", - "\n", - "train_on_inputs: false\n", - "group_by_length: false\n", - "bf16: auto\n", - "fp16:\n", - "tf32: false\n", - "\n", - "gradient_checkpointing: true\n", - "early_stopping_patience:\n", - "resume_from_checkpoint:\n", - "logging_steps: 1\n", - "xformers_attention:\n", - "flash_attention: false\n", - "sdp_attention: true\n", - "\n", - "warmup_steps: 1\n", - "max_steps: 25\n", - "evals_per_epoch: 1\n", - "eval_table_size:\n", - "saves_per_epoch: 1\n", - "debug:\n", - "deepspeed:\n", - "weight_decay: 0.0\n", - "fsdp:\n", - "fsdp_config:\n", - "special_tokens:\n", - " pad_token: <|end_of_text|>\n", - "\"\"\"\n", - "\n", - "\n", - "# Convert the YAML string to a Python dictionary\n", - "yaml_dict = yaml.safe_load(yaml_string)\n", - "\n", - "# Specify your file path\n", - "file_path = 'test_axolotl.yaml'\n", - "\n", - "# Write the YAML file\n", - "with open(file_path, 'w') as file:\n", - " yaml.dump(yaml_dict, file)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Above we have a configuration file with base LLM model and datasets specified, among many other things. Axolotl can automatically detect whether the specified datasets are on HuggingFace repo or local machine.\n", - "\n", - "The Axolotl configuration options encompass model and dataset selection, data pre-processing, and training. Let's go through them line by line:\n", - "\n", - "* \"base model\": String value, specifies the underlying pre-trained LLM that will be used for finetuning\n", - "\n", - "Next we have options for model weights quantization. Quantization allows for reduction in occupied memory on GPUs.\n", - "\n", - "* \"load_in_8bit\": Boolean value, whether to quantize the model weights into 8-bit integer.\n", - "\n", - "* \"load_in_4bit\": Boolean value, whether to quantize the model weights into 4-bit integer.\n", - "\n", - "* \"strict\": Boolean value. If false, it allows for overriding established configuration options in the yaml file when executing in command-line interface.\n", - "\n", - "* \"datasets\": a list of dicts that contain path and type of data sets as well as other optional configurations where datasets are concerned. Supports multiple datasets.\n", - "\n", - "* \"val_set_size\": Either a float value less than one or an integer less than the total size of dataset. Sets the size of validation set from the whole dataset. If float, sets the proportion of the dataset assigned for validation. If integer, sets the direct size of validation set.\n", - "\n", - "* \"output_dir\": String value. Path of trained model.\n", - "\n", - "For data preprocessing:\n", - "\n", - "* \"sequence_len\": Integer. Specifies the maximum sequence length of the input. Typically 2048 or less.\n", - "\n", - "* \"pad_to_sequence_len\": Boolean. Padding input to maximum sequence length.\n", - "\n", - "* \"sample_packing\": Boolean. Specifies whether to use multi-packing with block diagonal attention.\n", - "\n", - "* \"special_tokens\": Python dict, optional. Allows users to specify the additional special tokens to be ignored by the tokenizer.\n", - "\n", - "For LoRA configuration and its hyperparamters:\n", - "\n", - "* \"adapter\": String. Either \"lora\" or \"qlora\", depending on user's choice.\n", - "\n", - "* \"lora_model_dir\": String, Optional. Path to directory that contains LoRA model, if there is already a trained LoRA model the user would like to use.\n", - "\n", - "* \"lora_r\": Integer. Refers to the rank of LoRA decomposition matrices. Higher value will reduce LoRA efficiency. Recommended to be set to 8.\n", - "\n", - "* \"lora_alpha\": Integer. Scale the weight matrices by $\\frac{\\text{lora_alpha}}{\\text{lora_r}}$Recommended to be fixed at 16.\n", - "\n", - "* \"lora_dropout\": Float that is 1 or less. The dropout probability of a lora layer.\n", - "\n", - "* \"lora_target_linear\": Boolean. If true, lora will target all linear modules in the transformers architecture.\n", - "\n", - "* \"lora_modules_to_save\": If you added new tokens to the tokenizer, you may need to save some LoRA modules because they need to know the new tokens.\n", - "\n", - "See [LoRA](https://arxiv.org/abs/2106.09685) for detailed explanation of LoRA implementation.\n", - "\n", - "For the training configurations:\n", - "\n", - "* \"gradient_accumulation_steps\": Integer. The number of steps over which to accumulate gradient for batch training. E.g. if 2, backprop is performed every two steps.\n", - "\n", - "* \"micro_batch_size\": Integer. Batch size per gpu / gradient_accumulation_steps\n", - "\n", - "* \"num_epochs\": Integer. Number of epochs. One epoch is when training has looped over every batch in the whole data set once.\n", - "\n", - "* \"optimizer\": The optimizer to use for the training.\n", - "\n", - "* \"learning_rate\": The learning rate.\n", - "\n", - "* \"lr_scheduler\": The learning rate scheduler to use for adjusting learning rate during training.\n", - "\n", - "* \"train_on_inputs\": Boolean. Whether to ignore or include the user's prompt from the training labels.\n", - "\n", - "* \"group_by_length\": Boolean. Whether to group similarly sized data to minimize padding.\n", - "\n", - "* \"bf16\": Either \"auto\", \"true\", or \"false\". Whether to use CUDA bf16 floating point format. If set to \"auto\", will automatically apply bf16 should the gpu supports it.\n", - "\n", - "* \"fp16\": Optional. Specifies whether to use CUDA fp16. Automatically set to true if \"bf16\" is set to true. Otherwise false.\n", - "\n", - "* \"tf32\": Boolean. Whether to use CUDA tf32. Will override bf16.\n", - "\n", - "* \"gradient_checkpointing\": Boolean. Whether to use gradient checkpointing https://huggingface.co/docs/transformers/v4.18.0/en/performance#gradient-checkpointing\n", - "\n", - "* \"gradient_checkpointing_kwargs\": Python Dict. Fed into the trainer.\n", - "\n", - "* \"logging_steps\": Integer. Log training information over every specified number of steps.\n", - "\n", - "* \"flash_attention\": Boolean. Whether to use the [flash attention](https://github.com/Dao-AILab/flash-attention) mechanism.\n", - "\n", - "* \"sdp_attention\": Boolean. Whether to use the Scaled Dot Product attention mechanism (the attention mechanism in the [original implementation](https://arxiv.org/abs/1706.03762) of transformers.)\n", - "\n", - "* \"warmup_steps\": Integer. The number of pre-training steps where a very low learning rate is used.\n", - "\n", - "* \"evals_per_epoch\": Integer. Number of evaluations to be performed within one training epoch.\n", - "\n", - "* \"saves_per_epoch\": Integer. Number of times the model is saved in one training epoch.\n", - "\n", - "* \"weight_decay\": Positive Float. Sets the \"strength\" of weight decay (i.e. setting the coefficient of L2 regularization)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The above is but a snippet aiming to get users familiarized with the types of streamlined configuration options axolotl provides. For a full list of configuration options, see [here](https://axolotl-ai-cloud.github.io/axolotl/docs/config.html)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Train the model" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!accelerate launch -m axolotl.cli.train /content/test_axolotl.yaml" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Predict with trained model" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!accelerate launch -m axolotl.cli.inference /content/test_axolotl.yaml \\\n", - " --lora_model_dir=\"./outputs/lora-out\" --gradio" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Deeper Dive" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It is also helpful to gain some familiarity over some of the core inner workings of axolotl" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Configuration Normalization" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Axolotl uses a custom Dict class, called ```DictDefault```\n", - "to store configurations specified in the yaml configuration file (into a Python variable named ```cfg```). The definition for this custom Dict can be found in the [utils/dict.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/utils/dict.py)\n", - "\n", - "```DictDefault``` is amended such that calling a missing key from it will result in a ```None``` return type. This is important because if some configuration options aren't specified by the user, the ```None``` type allows Axolotl to perform boolean operations to determine the default settings for missing configurations. For more examples on how this is done, check out [utils/config/__init__.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/utils/config/__init__.py)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Loading Models, Tokenizers, and Trainer" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If we inspect [cli.train.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/cli/train.py), we will find that most of the heavy lifting were done by the function ```train()``` which is itself imported from [src/axolotl/train.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/train.py).\n", - "\n", - "```train()``` takes care of loading the appropriate tokenizer and pre-trained model through ```load_model()``` and ```load_tokenizer()``` from [src/axolotl/utils/models.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/utils/models.py) respectively.\n", - "\n", - "```load_tokenizer()``` loads in the appropriate tokenizer given the desired model, as well as chat templates.\n", - "\n", - "```ModelLoader``` class follows after tokenizer has been selected. It will automatically discern the base model type, load in the desired model, as well as applying model-appropriate attention mechanism modifications (e.g. flash attention). Depending on which base model the user chooses in the configuration, ```ModelLoader``` will utilize the corresponding \"attention hijacking\" script. For example, if the user specified the base model to be ```NousResearch/Meta-Llama-3.1-8B```, which is of llama type, and set ```flash_attn``` to ```True```, ```ModelLoader``` will load in [llama_attn_hijack_flash.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/monkeypatch/llama_attn_hijack_flash.py). For a list of supported attention hijacking, please refer to the directory [/src/axolotl/monkeypatch/](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/monkeypatch)\n", - "\n", - "Another important operation encompassed in ```train()``` is setting up the training that takes into account of user-specified traning configurations (e.g. num_epochs, optimizer) through the use of ```setup_trainer()``` from [/src/axolotl/utils/trainer.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/utils/trainer.py), which in turn relies on modules from [/src/axolotl/core/trainer_builder.py](https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/core/trainer_builder.py).\n", - "```trainer_builder.py``` provides a list of trainer object options bespoke for the task type (Causal or Reinforcement learning ('dpo', 'ipo', 'kto') )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Monkey patch\n", - "\n", - "The [Monkey patch directory](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/monkeypatch) is where model architecture/optimization patching scripts are stored (these are modifications that are not implemented in the official releases, hence the name monkey patch). It includes attention jacking, ReLoRA, and unsloth optimization." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.9.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "nbformat": 4, + "nbformat_minor": 0 } From 24f2887e871b63bfaea24df0c276b936f37366e3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 27 Jun 2025 10:37:53 -0400 Subject: [PATCH 0747/1405] don't fail during preprocess for sampling from iterable dataset (#2825) [skip ci] --- src/axolotl/common/datasets.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index 96af84c1e5..a9b4c0f0f0 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -75,13 +75,17 @@ def load_datasets( num_examples = cli_args.debug_num_examples if cli_args else 1 text_only = cli_args.debug_text_only if cli_args else False - train_samples = sample_dataset(train_dataset, num_examples) - check_dataset_labels( - train_samples, - tokenizer, - num_examples=num_examples, - text_only=text_only, - ) + try: + train_samples = sample_dataset(train_dataset, num_examples) + check_dataset_labels( + train_samples, + tokenizer, + num_examples=num_examples, + text_only=text_only, + ) + except AttributeError: + # can't sample iterable datasets + pass LOG.info("printing prompters...") for prompter in prompters: From d8280d45c1224ee95ccc0fddcd564b6c10ed48e8 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 27 Jun 2025 21:38:46 +0700 Subject: [PATCH 0748/1405] feat: add chat_template kwargs (#2837) --- src/axolotl/prompt_strategies/chat_template.py | 1 + src/axolotl/utils/schemas/config.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 0271fca246..6d2a048b2b 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -103,6 +103,7 @@ def build_prompt( chat_template_kwargs = { "chat_template": self.chat_template, "add_generation_prompt": add_generation_prompt, + **self.chat_template_kwargs, } if tools: diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 4031742cde..da0d3c9353 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -778,6 +778,12 @@ class AxolotlInputConfig( "description": "Custom jinja template for chat template. This will be only used if chat_template is set to `jinja` or `null` (in which case chat_template is automatically set to `jinja`). Default is null." }, ) + chat_template_kwargs: dict[str, Any] | None = Field( + default=None, + json_schema_extra={ + "description": "Additional kwargs to pass to the chat template. This is useful for customizing the chat template. For example, you can pass `thinking=False` to add a generation prompt to the chat template." + }, + ) eot_tokens: list[str] | None = Field( default=None, json_schema_extra={ From 0a7a216b60e75cdf9dd5bbf74c41fe8c24b7b276 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 27 Jun 2025 11:02:51 -0400 Subject: [PATCH 0749/1405] allow for different sequence_len for evaluations (#2836) [skip ci] * allow for different sequence_len for evaluations * reversed :facepalm: * add more information to filter msg --- src/axolotl/utils/data/sft.py | 5 ++++- src/axolotl/utils/data/utils.py | 9 ++++++--- src/axolotl/utils/schemas/config.py | 6 ++++++ tests/test_packed_batch_sampler.py | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index d0b8ab7432..aa88e9924a 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -334,7 +334,10 @@ def _load_raw_datasets( dataset = merge_datasets(datasets, cfg) if not cfg.skip_prepare_dataset: - dataset = drop_long_seq_in_dataset(dataset, cfg) + if split == "test" and cfg.eval_sequence_len: + dataset = drop_long_seq_in_dataset(dataset, cfg.eval_sequence_len, cfg) + else: + dataset = drop_long_seq_in_dataset(dataset, cfg.sequence_len, cfg) if cfg.sample_packing: dataset, _ = process_datasets_for_packing(cfg, dataset, None) diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index 4f7f6f8dd0..c0efb7a428 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -148,11 +148,14 @@ def deduplicate_and_log_datasets( return dataset, other_dataset -def drop_long_seq_in_dataset(dataset: Dataset, cfg: DictDefault) -> Dataset: +def drop_long_seq_in_dataset( + dataset: Dataset, sequence_len: int, cfg: DictDefault +) -> Dataset: """Remove sequences longer than configured maximum from dataset. Args: dataset: Dataset to filter. + sequence_len: Maximum length for sequences to keep cfg: Dictionary mapping `axolotl` config keys to values. Returns: @@ -167,7 +170,7 @@ def drop_long_seq_in_dataset(dataset: Dataset, cfg: DictDefault) -> Dataset: drop_long = functools.partial( drop_long_seq, - sequence_len=cfg.sequence_len, + sequence_len=sequence_len, min_sequence_len=cfg.min_sample_len, ) @@ -187,7 +190,7 @@ def drop_long_seq_in_dataset(dataset: Dataset, cfg: DictDefault) -> Dataset: drop_long_kwargs = {} if filter_map_kwargs: - drop_long_kwargs["desc"] = "Dropping Long Sequences" + drop_long_kwargs["desc"] = f"Dropping Long Sequences (>{sequence_len})" dataset = dataset.filter( drop_long, diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index da0d3c9353..1530fabe07 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -366,6 +366,12 @@ class AxolotlInputConfig( "description": "The maximum length of an input to train with, this should typically be less than 2048 as most models have a token/context limit of 2048" }, ) + eval_sequence_len: int | None = Field( + default=None, + json_schema_extra={ + "description": "The maximum length of an input for evaluation. If not specified, defaults to sequence_len" + }, + ) min_sample_len: int | None = None max_prompt_len: int = Field( default=512, diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index d91f63d941..7cb645db7e 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -70,7 +70,7 @@ def test_packing( ) train_dataset = concatenate_datasets([dataset_wrapper]) - train_dataset = drop_long_seq_in_dataset(train_dataset, cfg) + train_dataset = drop_long_seq_in_dataset(train_dataset, cfg.sequence_len, cfg) lengths = get_dataset_lengths(train_dataset) batch_sampler = MultipackBatchSampler( From ec15a7a6916f5c856a824e7b5f23ef760e8242c9 Mon Sep 17 00:00:00 2001 From: kallewoof Date: Sat, 28 Jun 2025 00:19:24 +0900 Subject: [PATCH 0750/1405] Support --lora-on-cpu flag for DPO model merging (#2766) [skip ci] * Support --lora-on-cpu flag for DPO model merging * fix: use device=cpu in _convert_embedding_modules_dtype when lora_on_cpu is set --- src/axolotl/loaders/model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index bbc532fb95..9897399e3e 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -776,6 +776,9 @@ def _convert_embedding_modules_dtype( dist_dtype: torch.dtype, before_kbit_train_or_finetune: bool, ): + dest = {"dtype": dist_dtype} + if self.cfg.lora_on_cpu: + dest["device"] = "cpu" for name, module in self.model.named_modules(): if "norm" in name: module.to(dist_dtype) @@ -786,4 +789,4 @@ def _convert_embedding_modules_dtype( # don't upcast lm_head for btlm continue if any(m in name for m in embedding_modules) and hasattr(module, "weight"): - module.to(dist_dtype) + module.to(**dest) From a1a740608da24842db5088043c5bab8c5a29bd45 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 27 Jun 2025 11:20:23 -0400 Subject: [PATCH 0751/1405] add assertion for packing patch to _get_unpad_data (#2840) --- src/axolotl/monkeypatch/multipack.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 1467f9e295..e590dbdaaf 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -42,6 +42,10 @@ def patch_for_multipack(model_type, model_name=None, has_remote_code=False): if has_remote_code: patch_remote(model_name) elif hasattr(transformers, "modeling_flash_attention_utils"): + # sanity check in case upstream api changes on this + assert hasattr( + transformers.modeling_flash_attention_utils, "_get_unpad_data" + ), "transformers api changed for _get_unpad_data for flash attention" transformers.modeling_flash_attention_utils._get_unpad_data = ( # pylint: disable=protected-access get_unpad_data ) From 81893c775cdf095c5272e49093b927388d724c50 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 28 Jun 2025 15:29:19 -0400 Subject: [PATCH 0752/1405] Accelerate 1.8.1 and BNB 0.46.0 update (#2815) * update accelerate to v1.8.0 * update bnb also * fix multigpu ci timeout * fix test set size * use latest accelerate 1.8.1 * disable default dtype --- requirements.txt | 4 ++-- src/axolotl/train.py | 5 +++-- tests/conftest.py | 3 ++- tests/e2e/multigpu/patched/test_sp.py | 1 + tests/e2e/multigpu/solo/test_flex.py | 1 + tests/e2e/multigpu/solo/test_grpo.py | 2 ++ tests/e2e/multigpu/test_eval.py | 8 ++++++-- tests/e2e/multigpu/test_gemma3.py | 1 + tests/e2e/multigpu/test_llama.py | 11 +++++++++++ tests/e2e/multigpu/test_qwen2.py | 1 + tests/e2e/multigpu/test_ray.py | 2 ++ 11 files changed, 32 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6e0d98c5e3..1fc3a9ff71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ # START section of dependencies that don't install on Darwin/MacOS -bitsandbytes==0.45.4 +bitsandbytes==0.46.0 triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 @@ -15,7 +15,7 @@ huggingface_hub==0.32.2 peft==0.15.2 transformers==4.52.4 tokenizers>=0.21.1 -accelerate==1.7.0 +accelerate==1.8.1 datasets==3.6.0 deepspeed>=0.17.0 trl==0.18.2 diff --git a/src/axolotl/train.py b/src/axolotl/train.py index a476385d02..a731316b61 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -223,8 +223,9 @@ def execute_training( ) LOG.info("Starting trainer...") - if cfg.bf16: - torch.set_default_dtype(torch.bfloat16) + # TODO: disabling for now as not compatible with FSDP2 + torchao low bit optimizers + # if cfg.bf16: + # torch.set_default_dtype(torch.bfloat16) trainer.train(resume_from_checkpoint=resume_from_checkpoint) diff --git a/tests/conftest.py b/tests/conftest.py index 8ab8fd6a46..12e79c0e31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ import tempfile import time from pathlib import Path +from typing import Generator import datasets import pytest @@ -411,7 +412,7 @@ def tokenizer_mistral_7b_instruct_chatml(tokenizer_mistral_7b_instruct): @pytest.fixture -def temp_dir(): +def temp_dir() -> Generator[str, None, None]: # Create a temporary directory _temp_dir = tempfile.mkdtemp() yield _temp_dir diff --git a/tests/e2e/multigpu/patched/test_sp.py b/tests/e2e/multigpu/patched/test_sp.py index 8883e0135d..31a728f201 100644 --- a/tests/e2e/multigpu/patched/test_sp.py +++ b/tests/e2e/multigpu/patched/test_sp.py @@ -54,6 +54,7 @@ def _run_sequence_parallel_test( "micro_batch_size": micro_batch_size, "gradient_accumulation_steps": 2, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_8bit", "lr_scheduler": "cosine", diff --git a/tests/e2e/multigpu/solo/test_flex.py b/tests/e2e/multigpu/solo/test_flex.py index c8f14330d3..b892fe2132 100644 --- a/tests/e2e/multigpu/solo/test_flex.py +++ b/tests/e2e/multigpu/solo/test_flex.py @@ -54,6 +54,7 @@ def test_loss_llama(self, temp_dir): "gradient_accumulation_steps": 2, "gradient_checkpointing": True, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index 1daf584726..c595d3fc05 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -309,6 +309,7 @@ def test_llama_lora_sp(self, temp_dir): "warmup_steps": 10, "val_set_size": 0.0, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.0001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", @@ -400,6 +401,7 @@ def test_llama_fft(self, temp_dir, num_gpus): "warmup_steps": 10, "val_set_size": 0.0, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.0001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", diff --git a/tests/e2e/multigpu/test_eval.py b/tests/e2e/multigpu/test_eval.py index 379562e40f..d6429cf63c 100644 --- a/tests/e2e/multigpu/test_eval.py +++ b/tests/e2e/multigpu/test_eval.py @@ -38,12 +38,13 @@ def test_eval_sample_packing(self, temp_dir): "lora_dropout": 0.05, "lora_target_linear": True, "lora_modules_to_save": ["embed_tokens", "lm_head"], - "val_set_size": 0.004, + "val_set_size": 0.05, "special_tokens": {"pad_token": "<|endoftext|>"}, "datasets": [ { "path": "teknium/GPT4-LLM-Cleaned", "type": "alpaca", + "split": "train[:5%]", }, ], "num_epochs": 1, @@ -51,6 +52,7 @@ def test_eval_sample_packing(self, temp_dir): "micro_batch_size": 2, "gradient_accumulation_steps": 2, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_8bit", "lr_scheduler": "cosine", @@ -107,12 +109,13 @@ def test_eval(self, temp_dir): "lora_dropout": 0.05, "lora_target_linear": True, "lora_modules_to_save": ["embed_tokens", "lm_head"], - "val_set_size": 0.0004, + "val_set_size": 0.01, "special_tokens": {"pad_token": "<|endoftext|>"}, "datasets": [ { "path": "teknium/GPT4-LLM-Cleaned", "type": "alpaca", + "split": "train[:5%]", }, ], "num_epochs": 1, @@ -120,6 +123,7 @@ def test_eval(self, temp_dir): "micro_batch_size": 2, "gradient_accumulation_steps": 2, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_8bit", "lr_scheduler": "cosine", diff --git a/tests/e2e/multigpu/test_gemma3.py b/tests/e2e/multigpu/test_gemma3.py index b4cb6e59de..3868d90f0f 100644 --- a/tests/e2e/multigpu/test_gemma3.py +++ b/tests/e2e/multigpu/test_gemma3.py @@ -64,6 +64,7 @@ def test_lora_ddp_packed(self, temp_dir): }, "gradient_accumulation_steps": 2, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.0001, "optimizer": "adamw_8bit", "lr_scheduler": "cosine", diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index a8ed6bda07..d84505714f 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -62,6 +62,7 @@ def test_lora_ddp(self, temp_dir): "gradient_accumulation_steps": 2, # "gradient_checkpointing": True, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_8bit", "lr_scheduler": "cosine", @@ -127,6 +128,7 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): "gradient_accumulation_steps": gradient_accumulation_steps, # "gradient_checkpointing": True, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_8bit", "lr_scheduler": "cosine", @@ -200,6 +202,7 @@ def test_dpo_lora_ddp(self, temp_dir): "gradient_accumulation_steps": 2, # "gradient_checkpointing": True, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "warmup_steps": 0, "learning_rate": 0.00001, "optimizer": "adamw_8bit", @@ -278,6 +281,7 @@ def test_dpo_qlora_ddp(self, temp_dir): "gradient_accumulation_steps": 2, # "gradient_checkpointing": True, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "warmup_steps": 0, "learning_rate": 0.00001, "optimizer": "adamw_8bit", @@ -340,6 +344,7 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): "gradient_accumulation_steps": gradient_accumulation_steps, # "gradient_checkpointing": True, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", @@ -412,6 +417,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): "gradient_accumulation_steps": 2, # "gradient_checkpointing": True, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", @@ -491,6 +497,7 @@ def test_fsdp2_packed( "gradient_accumulation_steps": 2, "gradient_checkpointing": True, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_torch_8bit", "lr_scheduler": "cosine", @@ -573,6 +580,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "gradient_accumulation_steps": 2, # "gradient_checkpointing": True, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", @@ -669,6 +677,7 @@ def test_ds_zero3_packed( "micro_batch_size": 1, "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", @@ -743,6 +752,7 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): "micro_batch_size": 1, "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", @@ -817,6 +827,7 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): "micro_batch_size": 1, "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", diff --git a/tests/e2e/multigpu/test_qwen2.py b/tests/e2e/multigpu/test_qwen2.py index fa4efa32b2..bd57dbcef2 100644 --- a/tests/e2e/multigpu/test_qwen2.py +++ b/tests/e2e/multigpu/test_qwen2.py @@ -46,6 +46,7 @@ def test_qlora_fsdp_dpo(self, base_model, temp_dir): "micro_batch_size": 2, "gradient_accumulation_steps": 2, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index 22023507a5..43a722b488 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -48,6 +48,7 @@ def test_lora_ddp(self, temp_dir): "micro_batch_size": 4, "gradient_accumulation_steps": 2, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_8bit", "lr_scheduler": "cosine", @@ -107,6 +108,7 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps): "micro_batch_size": 1, "gradient_accumulation_steps": gradient_accumulation_steps, "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", "learning_rate": 0.00001, "optimizer": "adamw_torch", "lr_scheduler": "cosine", From 7563e1bd3055a03a743d26fbef9dab4b8af32c77 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 29 Jun 2025 22:05:21 -0400 Subject: [PATCH 0753/1405] set a different triton cache for each test to avoid blocking writes to cache (#2843) * set a different triton cache for each test to avoid blocking writes to cache * set log level * disable debug logging for filelock --- cicd/single_gpu.py | 2 ++ tests/conftest.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/cicd/single_gpu.py b/cicd/single_gpu.py index 2ce3b06621..357aa41eee 100644 --- a/cicd/single_gpu.py +++ b/cicd/single_gpu.py @@ -32,6 +32,8 @@ "NIGHTLY_BUILD": os.environ.get("NIGHTLY_BUILD", ""), "CODECOV_TOKEN": os.environ.get("CODECOV_TOKEN", ""), "HF_HOME": "/workspace/data/huggingface-cache/hub", + "PYTHONUNBUFFERED": os.environ.get("PYTHONUNBUFFERED", "1"), + "DEEPSPEED_LOG_LEVEL": os.environ.get("DEEPSPEED_LOG_LEVEL", "WARNING"), } dockerfile_contents = df_template.render(**df_args) diff --git a/tests/conftest.py b/tests/conftest.py index 12e79c0e31..b8dff2477a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import functools import importlib +import logging import os import shutil import sys @@ -25,6 +26,8 @@ hf_offline_context, ) +logging.getLogger("filelock").setLevel(logging.CRITICAL) + def retry_on_request_exceptions(max_retries=3, delay=1): # pylint: disable=duplicate-code @@ -420,6 +423,11 @@ def temp_dir() -> Generator[str, None, None]: shutil.rmtree(_temp_dir) +@pytest.fixture(scope="function", autouse=True) +def unique_triton_cache_dir(temp_dir): + os.environ["TRITON_CACHE_DIR"] = temp_dir + "/~.triton/cache" + + @pytest.fixture(scope="function", autouse=True) def cleanup_monkeypatches(): from transformers import Trainer From cb811f8bf1843cc70dc1a2434586a9d431e86e9c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 29 Jun 2025 22:11:16 -0400 Subject: [PATCH 0754/1405] upgrade to flash-attn 2.8.0.post2 (#2828) * upgrade to flash-attn 2.8.0.post2 * use cu126 with torch 2.6 * seems vllm 0.8.5.post1 not compatible with cuda12.6.3 and torch 2.6 * cu126 + torch 2.6 as the default * use cu126 for multigpu w torch 2.6 too * drop vllm for now from ci for now --- .github/workflows/main.yml | 13 ++++++------- .github/workflows/multi-gpu-e2e.yml | 6 +++--- .github/workflows/tests.yml | 12 ++++++------ docker/Dockerfile-base | 4 ---- docker/Dockerfile-uv-base | 4 ---- setup.py | 4 ++-- 6 files changed, 17 insertions(+), 26 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7ff7127574..29cd2556d5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,12 +20,11 @@ jobs: python_version: "3.11" pytorch: 2.5.1 axolotl_extras: - - cuda: 124 - cuda_version: 12.4.1 + - cuda: 126 + cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.6.0 axolotl_extras: vllm - is_latest: true - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" @@ -88,8 +87,8 @@ jobs: python_version: "3.11" pytorch: 2.5.1 axolotl_extras: - - cuda: 124 - cuda_version: 12.4.1 + - cuda: 126 + cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.6.0 axolotl_extras: @@ -146,8 +145,8 @@ jobs: strategy: matrix: include: - - cuda: 124 - cuda_version: 12.4.1 + - cuda: 126 + cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.6.0 axolotl_extras: diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index deea0ed299..09d9663a9c 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -26,11 +26,11 @@ jobs: fail-fast: false matrix: include: - - cuda: 124 - cuda_version: 12.4.1 + - cuda: 126 + cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.6.0 - axolotl_extras: vllm + axolotl_extras: num_gpus: 2 nightly_build: "true" - cuda: 124 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bb865e98d4..b489e27b85 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -195,12 +195,12 @@ jobs: fail-fast: false matrix: include: - - cuda: 124 - cuda_version: 12.4.1 + - cuda: 126 + cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.6.0 num_gpus: 1 - axolotl_extras: vllm + axolotl_extras: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" @@ -247,8 +247,8 @@ jobs: fail-fast: false matrix: include: - - cuda: 124 - cuda_version: 12.4.1 + - cuda: 126 + cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.6.0 num_gpus: 1 @@ -311,7 +311,7 @@ jobs: python_version: "3.11" pytorch: 2.6.0 num_gpus: 1 - axolotl_extras: vllm + axolotl_extras: steps: - name: Checkout uses: actions/checkout@v4 diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index cc9ca2f2d5..52201f276e 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -37,7 +37,3 @@ RUN git lfs install --skip-repo && \ pip3 install awscli && \ # The base image ships with `pydantic==1.8.2` which is not working pip3 install -U --no-cache-dir pydantic==1.10.10 - -RUN if [ "$PYTORCH_VERSION" = "2.7.1" ] ; then \ - pip3 install flash-attn==2.7.4.post1; \ - fi diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index c612278aec..4b08e55f8e 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -34,7 +34,3 @@ RUN uv pip install packaging setuptools wheel psutil \ && uv pip install --no-build-isolation "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" \ && uv pip install "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" \ && uv pip install awscli pydantic - -RUN if [ "$PYTORCH_VERSION" = "2.7.1" ] ; then \ - uv pip install --no-build-isolation flash-attn==2.7.4.post1; \ - fi diff --git a/setup.py b/setup.py index 08c39c71c8..212625bddd 100644 --- a/setup.py +++ b/setup.py @@ -111,9 +111,9 @@ def get_package_version(): extras_require = { - "flash-attn": ["flash-attn==2.7.4.post1"], + "flash-attn": ["flash-attn==2.8.0.post2"], "ring-flash-attn": [ - "flash-attn==2.7.4.post1", + "flash-attn==2.8.0.post2", "ring-flash-attn>=0.1.4", "yunchang==0.6.0", ], From 35fdbce1022547158a5e451a11350513f4ac4385 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Sun, 29 Jun 2025 22:16:32 -0400 Subject: [PATCH 0755/1405] Ensure device mesh patching is applied (#2842) * move patches; make patch stronger * fix broken tests * guard sequence_parallel_degree comparison against none --------- Co-authored-by: Wing Lian --- src/axolotl/loaders/patch_manager.py | 12 ++++++++ src/axolotl/monkeypatch/ring_attn/patch.py | 29 ++++++++++++++----- .../utils/ctx_managers/sequence_parallel.py | 8 ----- .../lora_kernels/test_lora_kernel_patching.py | 20 +++++++++++-- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 3f8116b21b..610e87c7b7 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -65,6 +65,7 @@ def apply_pre_model_load_patches(self): self._apply_mistral_cross_entropy_patch() self._apply_self_attention_lora_patch() self._apply_gemma3_conditional_generation_forward_patch() + self._apply_sequence_parallel_patches() def apply_post_model_load_patches(self, model: PreTrainedModel): """Apply patches that require the model instance.""" @@ -231,6 +232,17 @@ def _apply_gemma3_conditional_generation_forward_patch(self): patch_gemma3_conditional_generation_forward() + def _apply_sequence_parallel_patches(self): + """Apply sequence parallelism patches.""" + if self.cfg.sequence_parallel_degree and self.cfg.sequence_parallel_degree > 1: + from axolotl.monkeypatch.ring_attn.patch import ( + patch_prepare_data_loader, + patch_prepare_device_mesh, + ) + + patch_prepare_data_loader() + patch_prepare_device_mesh(self.cfg.sequence_parallel_degree, self.cfg.fsdp) + def _patch_attention(self): """Apply attention-specific patches based on model type.""" if not (self.cfg.flash_attention and hasattr(self.model_config, "model_type")): diff --git a/src/axolotl/monkeypatch/ring_attn/patch.py b/src/axolotl/monkeypatch/ring_attn/patch.py index d83476e5a2..017b420d28 100644 --- a/src/axolotl/monkeypatch/ring_attn/patch.py +++ b/src/axolotl/monkeypatch/ring_attn/patch.py @@ -152,7 +152,7 @@ def update_ring_attn_params(position_ids: torch.Tensor | None): def patch_prepare_data_loader(): """Patch `accelerate.data_loader.prepare_data_loader` to respect the SP degree. - Raies: + Raises: RuntimeError: If source code to patch does not exist. """ original_fn = accelerate.data_loader.prepare_data_loader @@ -168,23 +168,34 @@ def patch_prepare_data_loader(): ORIGINAL_PREPARE_DATALOADER_CODE, NEW_PREPARE_DATALOADER_CODE ) + items_to_import = [] + for item in dir(accelerate.data_loader): + if item in patched_source: + items_to_import.append(item) + # Create a new function from the patched source namespace = {} exec( # pylint: disable=exec-used # nosec B102 - patched_source, accelerate.data_loader.__dict__, namespace + f"from accelerate.data_loader import ({', '.join(items_to_import)})", + globals(), + ) + exec( # pylint: disable=exec-used # nosec B102 + patched_source, globals(), namespace ) + patched_function = namespace["prepare_data_loader"] + original_fn.__code__ = patched_function.__code__ - accelerate.data_loader.prepare_data_loader = patched_function LOG.info("Patched accelerate.data_loader.prepare_data_loader for SP support") -def patch_prepare_device_mesh(sequence_parallel_degree: int): +def patch_prepare_device_mesh(sequence_parallel_degree: int, fsdp: bool = False): """Patches the `Accelerator._prepare_device_mesh` method to create a device mesh that includes sequence parallelism with the specified degree. Args: - sequence_parallel_degree (int): The degree of sequence parallelism to use. + sequence_parallel_degree: The degree of sequence parallelism to use. + fsdp: Whether to use FSDP. """ def _prepare_device_mesh(self): @@ -207,12 +218,14 @@ def _prepare_device_mesh(self): ) device_ids = list(range(world_size)) - # Note that we use "cp" instead of "sp" to match the PyTorch native "context - # parallelism" implementation naming + # NOTE: We use "cp" instead of "sp" to match the PyTorch native "context + # parallelism" implementation naming. + # NOTE: We have a simplified FSDP handling here; i.e., if FSDP is enabled, we + # only use "fsdp" and "cp" for the device mesh. return dist.DeviceMesh( "cuda", torch.tensor(device_ids).reshape(mesh_shape), - mesh_dim_names=("dp", "cp"), + mesh_dim_names=("dp", "cp") if not fsdp else ("fsdp", "cp"), ) # Replace the original method with our new method diff --git a/src/axolotl/utils/ctx_managers/sequence_parallel.py b/src/axolotl/utils/ctx_managers/sequence_parallel.py index f429cd2ae5..1ac805a73c 100644 --- a/src/axolotl/utils/ctx_managers/sequence_parallel.py +++ b/src/axolotl/utils/ctx_managers/sequence_parallel.py @@ -12,8 +12,6 @@ from axolotl.monkeypatch.ring_attn import ( get_ring_attn_group, - patch_prepare_data_loader, - patch_prepare_device_mesh, register_ring_attn, update_ring_attn_params, ) @@ -238,12 +236,6 @@ def _register_ring_attn(self): ring_attn_func=self.ring_attn_func, ) - # Patches for accelerate functionality - patch_prepare_data_loader() - patch_prepare_device_mesh( - sequence_parallel_degree=self.sequence_parallel_degree - ) - def _register_model_hooks(self): # Forward pre-hook to apply sequence parallelism def sequence_parallel_pre_hook(_, args, kwargs): diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index 56ce5a8b9a..b4dc5de542 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -396,7 +396,7 @@ def test_model_architecture(model_config): # pylint: disable=duplicate-code -def test_kernel_training_integration(): +def test_kernel_training_integration(temp_dir): """Test model loading with kernel patches enabled.""" from axolotl.cli.utils import load_model_and_tokenizer @@ -426,6 +426,14 @@ def test_kernel_training_integration(): } ) + # Write cfg to yaml file + path = Path(temp_dir) / "config.yaml" + with open(path, "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + # Load config + cfg = load_cfg(str(path)) + # Load model model, _, _ = load_model_and_tokenizer(cfg=cfg) @@ -505,7 +513,7 @@ def test_kernel_training_integration_auto_enable(temp_dir): assert found_patched_attn -def test_kernel_training_integration_dropout_non_zero(): +def test_kernel_training_integration_dropout_non_zero(temp_dir): """Test model loading with dropout non-zero should not patch.""" from axolotl.cli.utils import load_model_and_tokenizer @@ -533,6 +541,14 @@ def test_kernel_training_integration_dropout_non_zero(): } ) + # Write cfg to yaml file + path = Path(temp_dir) / "config.yaml" + with open(path, "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + # Load config + cfg = load_cfg(str(path)) + # Get original attention class attention_cls = get_attention_cls_from_config(cfg) From 327b4e48e9892b61497971097f1308a4a463d551 Mon Sep 17 00:00:00 2001 From: mhenrichsen Date: Wed, 2 Jul 2025 09:03:52 +0200 Subject: [PATCH 0756/1405] Add installation instructions for pip and Docker to README.md (#2854) * Add installation instructions for pip and Docker to README.md * Enhance README.md with Docker installation guidance for improved setup reliability. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index e18220567d..ec9fc9f7fd 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ Features: ### Installation +#### Using pip + ```bash pip3 install -U packaging==23.2 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] @@ -68,6 +70,13 @@ axolotl fetch examples axolotl fetch deepspeed_configs # OPTIONAL ``` +#### Using Docker + +Installing with Docker can be less error prone than installing in your own environment. +```bash +docker run --gpus '"all"' --rm -it axolotlai/axolotl:main-latest +``` + Other installation approaches are described [here](https://docs.axolotl.ai/docs/installation.html). ### Your First Fine-tune From d1224db8f446cfaebd1cde2aa46458f64ade6479 Mon Sep 17 00:00:00 2001 From: Dhruv Mullick Date: Wed, 2 Jul 2025 06:04:40 -0600 Subject: [PATCH 0757/1405] Decouple generate_during_eval from wandb to support other visualizers (#2849) [skip ci] * Add generate_during_eval for mlflow for dpo * Decouple generate_during_eval from wandb --- src/axolotl/core/trainers/dpo/__init__.py | 2 +- src/axolotl/utils/schemas/config.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/axolotl/core/trainers/dpo/__init__.py b/src/axolotl/core/trainers/dpo/__init__.py index 8cd9aacf5f..4b40d40854 100644 --- a/src/axolotl/core/trainers/dpo/__init__.py +++ b/src/axolotl/core/trainers/dpo/__init__.py @@ -28,7 +28,7 @@ def set_training_args_kwargs(cls, cfg): training_args_kwargs["max_completion_length"] = None training_args_kwargs["max_length"] = cfg.sequence_len training_args_kwargs["max_prompt_length"] = cfg.sequence_len - training_args_kwargs["generate_during_eval"] = cfg.use_wandb + training_args_kwargs["generate_during_eval"] = cfg.dpo_generate_during_eval if cfg.dpo_use_weighting is not None: training_args_kwargs["use_weighting"] = cfg.dpo_use_weighting if cfg.dpo_padding_free is not None: diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 1530fabe07..44a9a4f06f 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -146,6 +146,7 @@ class AxolotlInputConfig( dpo_label_smoothing: float | None = None dpo_norm_loss: bool | None = None dpo_padding_free: bool | None = None + dpo_generate_during_eval: bool | None = None datasets: ( Annotated[ From bf5928d0eef3b0ee79eadcdaea431e04d7db1a67 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 2 Jul 2025 19:05:01 +0700 Subject: [PATCH 0758/1405] feat(doc): update docker tag examples (#2851) [skip ci] * feat(doc): update docker tag examples * chore: comment --- docs/docker.qmd | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/docker.qmd b/docs/docker.qmd index bc26a795f2..197185d885 100644 --- a/docs/docker.qmd +++ b/docs/docker.qmd @@ -9,7 +9,7 @@ format: This section describes the different Docker images that are released by AxolotlAI at [Docker Hub](https://hub.docker.com/u/axolotlai). ::: {.callout-important} -For Blackwell GPUs, please use the tags with Pytorch 2.7.1 and CUDA 12.8. +For Blackwell GPUs, please use the tags with PyTorch 2.7.1 and CUDA 12.8. ::: ## Base @@ -34,6 +34,7 @@ Tags examples: - `main-base-py3.11-cu128-2.7.1` - `main-base-py3.11-cu126-2.7.1` +- `main-base-py3.11-cu126-2.6.0` - `main-base-py3.11-cu124-2.6.0` - `main-base-py3.11-cu124-2.5.1` @@ -73,13 +74,15 @@ There may be some extra tags appended to the image, like `-vllm` which installs Tags examples: -- `main-py3.11-cu126-2.7.0` +- `main-py3.11-cu128-2.7.1` +- `main-py3.11-cu126-2.7.1` +- `main-py3.11-cu126-2.6.0` - `main-py3.11-cu124-2.6.0` - `main-py3.11-cu124-2.5.1` - `main-latest` - `main-20250303-py3.11-cu124-2.6.0` - `main-20250303-py3.11-cu124-2.5.1` -- `0.9.2` +- `0.10.1` ## Cloud From f2b352f2e5cbfd7a25c78193953b7351d3737cb8 Mon Sep 17 00:00:00 2001 From: Vincenzo di Cicco <112694549+v-dicicco@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:05:35 +0200 Subject: [PATCH 0759/1405] Add sample_packing_sequentially to trainer args (#2853) [skip ci] --- src/axolotl/core/builders/causal.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 2b7d902fa0..c847a087c8 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -253,6 +253,10 @@ def build(self, total_num_steps): training_arguments_kwargs["eval_sample_packing"] = bool( self.cfg.eval_sample_packing ) + if self.cfg.sample_packing_sequentially is not None: + training_arguments_kwargs["sample_packing_sequentially"] = ( + self.cfg.sample_packing_sequentially + ) if self.cfg.sample_packing_bin_size is not None: training_arguments_kwargs["sample_packing_bin_size"] = ( self.cfg.sample_packing_bin_size From 6383630155ee5b65ec14fd1b185385a1d90e884c Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 2 Jul 2025 19:06:00 +0700 Subject: [PATCH 0760/1405] Fix: tokenize stall due to not shuffling dataset (#2845) * fix: shuffle dataset even if only one to fix tokenize stall * fix: warn if shuffling merged with curriculum sampling * chore: refactor --- src/axolotl/utils/data/shared.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 3c58b4c853..a537c5b65a 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -524,13 +524,24 @@ def merge_datasets(datasets: list[Dataset], cfg: DictDefault) -> Dataset: Merged dataset. """ if len(datasets) == 1: - return datasets[0] + ds = datasets[0] + + # Do not shuffle if curriculum sampling is enabled + if cfg.curriculum_sampling: + return ds + + return ds.shuffle(seed=cfg.seed) LOG.info("Merging datasets...") merged_dataset = concatenate_datasets(datasets) if cfg.shuffle_merged_datasets: LOG.debug("Shuffling merged datasets...") + if cfg.curriculum_sampling: + LOG.warning( + "Shuffling merged datasets with curriculum sampling is not recommended. " + "This will randomize the order of samples." + ) merged_dataset = merged_dataset.shuffle(seed=cfg.seed) else: LOG.debug("Not shuffling merged datasets.") From 8ae5a2311b4912f283f248dafb10d88c0770cd97 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 2 Jul 2025 19:07:18 +0700 Subject: [PATCH 0761/1405] feat: update handling for mistraltokenizer decode and multiprocessing pickling fix (#2790) * feat: update handling for mistraltokenizer decode * fix: update mistral common package version * fix: to use correct release * fix triton path --------- Co-authored-by: Wing Lian --- requirements.txt | 2 +- src/axolotl/utils/mistral_tokenizer.py | 11 +++++++---- tests/conftest.py | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1fc3a9ff71..10ac04a663 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,4 +68,4 @@ schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.6 axolotl-contribs-mit==0.0.3 -mistral-common==1.6.0 +mistral-common==1.6.3 diff --git a/src/axolotl/utils/mistral_tokenizer.py b/src/axolotl/utils/mistral_tokenizer.py index 3ccf39bb0f..1ba824938e 100644 --- a/src/axolotl/utils/mistral_tokenizer.py +++ b/src/axolotl/utils/mistral_tokenizer.py @@ -8,7 +8,7 @@ import numpy as np from huggingface_hub import hf_hub_download from mistral_common.tokens.tokenizers.mistral import MistralTokenizer -from mistral_common.tokens.tokenizers.tekken import Tekkenizer +from mistral_common.tokens.tokenizers.tekken import SpecialTokenPolicy, Tekkenizer from torch import Tensor from transformers.utils import PaddingStrategy @@ -251,10 +251,13 @@ def decode( token_ids = [token_ids] if skip_special_tokens: - return self._mistral.instruct_tokenizer.tokenizer.decode(token_ids) + return self._mistral.instruct_tokenizer.tokenizer.decode( + token_ids, special_token_policy=SpecialTokenPolicy.IGNORE + ) - # to_string returns a string with special tokens - return self._mistral.instruct_tokenizer.tokenizer.to_string(token_ids) + return self._mistral.instruct_tokenizer.tokenizer.decode( + token_ids, special_token_policy=SpecialTokenPolicy.KEEP + ) def _create_mistral_chat_completion_request( self, conversation: list[dict], tools: list[dict] | None = None diff --git a/tests/conftest.py b/tests/conftest.py index b8dff2477a..bbe2d10ee5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ import sys import tempfile import time -from pathlib import Path +from pathlib import Path, PosixPath from typing import Generator import datasets @@ -424,8 +424,8 @@ def temp_dir() -> Generator[str, None, None]: @pytest.fixture(scope="function", autouse=True) -def unique_triton_cache_dir(temp_dir): - os.environ["TRITON_CACHE_DIR"] = temp_dir + "/~.triton/cache" +def unique_triton_cache_dir(temp_dir: str | PosixPath) -> None: + os.environ["TRITON_CACHE_DIR"] = str(temp_dir) + "/.triton/cache" @pytest.fixture(scope="function", autouse=True) From 70ca1b22915188b0ae0a7ac986b97671be6c379e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 3 Jul 2025 12:21:39 -0400 Subject: [PATCH 0762/1405] fix nightlies to use correct cache (#2848) [skip ci] * fix nightlies to use correct cache * fix for handling None for bf16 --- .github/workflows/tests-nightly.yml | 115 ++-------------------------- src/axolotl/core/builders/base.py | 4 +- 2 files changed, 10 insertions(+), 109 deletions(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 539f7f71b6..e6a1d8e589 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -18,96 +18,9 @@ jobs: env: SKIP: no-commit-to-branch - preload-cache: - name: Preload HF cache - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python_version: ["3.11"] - pytorch_version: ["2.6.0"] - timeout-minutes: 20 - - env: - AXOLOTL_IS_CI_CACHE_PRELOAD: "1" - - steps: - - name: Check out repository code - uses: actions/checkout@v4 - - - name: Restore HF cache - id: hf-cache-restore - uses: actions/cache/restore@v4 - with: - path: | - /home/runner/.cache/huggingface/hub/datasets--* - /home/runner/.cache/huggingface/hub/models--* - key: ${{ runner.os }}-hf-hub-cache-v2 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python_version }} - cache: 'pip' # caching pip dependencies - - - name: upgrade pip - run: | - pip3 install --upgrade pip - pip3 install --upgrade packaging==23.2 setuptools==75.8.0 wheel - - - name: Install PyTorch - run: | - pip3 install torch==${{ matrix.pytorch_version }} - - - name: Install dependencies - run: | - pip3 show torch - pip3 install --no-build-isolation -U -e . - python scripts/unsloth_install.py | sh - python scripts/cutcrossentropy_install.py | sh - pip3 install -r requirements-dev.txt -r requirements-tests.txt - - - name: Make sure PyTorch version wasn't clobbered - run: | - python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" - - - name: Ensure axolotl CLI was installed - run: | - axolotl --help - - - name: Pre-Download dataset fixture - run: | - huggingface-cli download --repo-type=dataset axolotl-ai-internal/axolotl-oss-dataset-fixtures - - - name: Run tests - run: | - pytest -v tests/conftest.py - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.xml - flags: unittests,pytorch-${{ matrix.pytorch_version }} - fail_ci_if_error: false - - - name: cleanup pip cache - run: | - find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; - - - name: Save HF cache - id: hf-cache - uses: actions/cache/save@v4 - with: - path: | - /home/runner/.cache/huggingface/hub/datasets--* - /home/runner/.cache/huggingface/hub/models--* - key: ${{ steps.hf-cache-restore.outputs.cache-primary-key }} - pytest: name: PyTest runs-on: ubuntu-latest - needs: [preload-cache] strategy: fail-fast: false max-parallel: 2 @@ -120,14 +33,11 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 - - name: Restore HF cache - id: hf-cache-restore - uses: actions/cache/restore@v4 - with: - path: | - /home/runner/.cache/huggingface/hub/datasets--* - /home/runner/.cache/huggingface/hub/models--* - key: ${{ runner.os }}-hf-hub-cache-v2 + - name: Restore Cache from S3 + id: hf-cache-restore-s3 + run: | + mkdir -p /home/runner/.cache/huggingface/hub + curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xf - -C /home/runner/.cache/huggingface/hub/ --use-compress-program unzstd - name: Setup Python uses: actions/setup-python@v5 @@ -168,10 +78,6 @@ jobs: run: | axolotl --help - - name: Pre-Download dataset fixture - run: | - huggingface-cli download --repo-type=dataset axolotl-ai-internal/axolotl-oss-dataset-fixtures - - name: Run tests run: | pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ @@ -193,15 +99,8 @@ jobs: fail-fast: false matrix: include: - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.5.1 - num_gpus: 1 - axolotl_extras: - nightly_build: "true" - - cuda: 124 - cuda_version: 12.4.1 + - cuda: 126 + cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.6.0 num_gpus: 1 diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index eed43e542c..7d278db664 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -219,7 +219,9 @@ def _configure_precision_settings(self, training_args_kwargs: dict): if self.cfg.bf16 == "full": training_args_kwargs["bf16_full_eval"] = True else: - training_args_kwargs["bf16"] = self.cfg.bf16 or self.cfg.bfloat16 + bf16 = self.cfg.bf16 or self.cfg.bfloat16 + bf16 = bf16 if bf16 is not None else False + training_args_kwargs["bf16"] = bf16 def _configure_scheduler(self, training_args_kwargs: dict): if self.cfg.lr_scheduler in ["one_cycle", "rex"]: From a5946ff1f07ab57405c0ec050ec2a16ea2c1b6c5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 5 Jul 2025 09:21:18 -0400 Subject: [PATCH 0763/1405] build fa2 from source for base image with torch2.6 and cu124 (#2867) --- .github/workflows/base.yml | 6 ++++-- docker/Dockerfile-base | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 966bd2f5b5..1b9862d85c 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -5,11 +5,13 @@ on: branches: - "main" paths: - - 'Dockerfile-base' + - 'docker/Dockerfile-base' + - 'docker/Dockerfile-uv-base' - '.github/workflows/base.yml' pull_request: paths: - - 'Dockerfile-base' + - 'docker/Dockerfile-base' + - 'docker/Dockerfile-uv-base' - '.github/workflows/base.yml' workflow_dispatch: diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 52201f276e..df42403252 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -37,3 +37,7 @@ RUN git lfs install --skip-repo && \ pip3 install awscli && \ # The base image ships with `pydantic==1.8.2` which is not working pip3 install -U --no-cache-dir pydantic==1.10.10 + +RUN if [ "$PYTORCH_VERSION" = "2.6.0" ] && [ "$CUDA" = "124" ] ; then \ + FLASH_ATTENTION_FORCE_BUILD="TRUE" pip3 install --no-build-isolation flash-attn==2.8.0.post2; \ + fi From bf38e507fb124c8081bff71f70b42de474aa50ff Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 6 Jul 2025 21:20:41 -0400 Subject: [PATCH 0764/1405] respect shuffle_merged_datasets for single dataset too (#2866) [skip ci] * respect shuffle_merged_datasets for single dataset too * update inline comment for behavior Co-authored-by: NanoCode012 --------- Co-authored-by: NanoCode012 --- src/axolotl/utils/data/shared.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index a537c5b65a..c3c70545c8 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -526,8 +526,9 @@ def merge_datasets(datasets: list[Dataset], cfg: DictDefault) -> Dataset: if len(datasets) == 1: ds = datasets[0] - # Do not shuffle if curriculum sampling is enabled - if cfg.curriculum_sampling: + # Do not shuffle if curriculum sampling is enabled or + # shuffle_merged_datasets is disabled + if cfg.curriculum_sampling or not cfg.shuffle_merged_datasets: return ds return ds.shuffle(seed=cfg.seed) From b37ddf97783da826454aec2662329ef0ea38dc4c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 6 Jul 2025 21:55:09 -0400 Subject: [PATCH 0765/1405] don't use tokenizer parallelism when using packing (#2862) [skip ci] --- src/axolotl/utils/trainer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 278fbed5bb..06853451cc 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -609,6 +609,9 @@ def prepare_opinionated_env(cfg): if cfg.qlora_sharded_model_loading: # model loading is forked after the tokenizer os.environ["TOKENIZERS_PARALLELISM"] = "false" + if cfg.sample_packing: + # multipack parallel packing sampler defaults to using fork + os.environ["TOKENIZERS_PARALLELISM"] = "false" def setup_trainer( From 5a961ecadf617ad2af2543892c1a6548ee74d8fa Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 7 Jul 2025 09:55:33 +0800 Subject: [PATCH 0766/1405] Fix: do not call preprocess in multimodal or pretraining case (#2861) * fix: let users know to not call preprocess for vision mode * fix: improve ux for pretraining dataset and skip prepare ds * feat: add info to doc * Update src/axolotl/cli/preprocess.py following comment Co-authored-by: salman --------- Co-authored-by: salman --- docs/faq.qmd | 4 ++++ src/axolotl/cli/preprocess.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/docs/faq.qmd b/docs/faq.qmd index b84aa75bdd..59b06becde 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -51,6 +51,10 @@ description: Frequently asked questions > pad_token: "..." > ``` +**Q: `IterableDataset error` or `KeyError: 'input_ids'` when using `preprocess` CLI** + +> A: This is because you may be using `preprocess` CLI with `pretraining_dataset:` or `skip_prepare_dataset: true` respectively. Please use `axolotl train` CLI directly instead as these datasets are prepared on demand. + ### Chat templates **Q: `jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'content' / 'role' / ____`** diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index b8258383ec..d0c2ad1653 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -35,6 +35,12 @@ def do_preprocess(cfg: DictDefault, cli_args: PreprocessCliArgs) -> None: check_accelerate_default_config() check_user_token() + for key in ["skip_prepare_dataset", "pretraining_dataset"]: + if cfg.get("key"): + raise ValueError( + f"You have set `{key}:`. `preprocess` is not needed. Run the `axolotl train` CLI directly instead." + ) + if not cfg.dataset_prepared_path: msg = ( Fore.RED From 69cd49a7aaa7873e50beb499408956e6129773cd Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 7 Jul 2025 09:35:22 -0400 Subject: [PATCH 0767/1405] update transformers to 4.53.1 (#2844) [skip ci] * update transformers to 4.53.0 * remove attention_mask from signature columns if using packing * remove attention_mask column from dataloader * update signature of flash attn forward for ring attn patch * fix FSDP * patch ring-flash-attn with upstream signature fix * fix patch indentation level * fix the patch * add batch flattening smoke test with loss check that works in older transformers * fix patch * don't drop attention mask for flex * more fixes * patch create_causal_mask for packing w flex * global torch manual_seed fixture * tweak loss checks * fix patch and use single batch for flex * don't need to reload * fix causal mask patch * use transformers patch releasE * make sure env var is string * make sure to drop attention mask for flex w packing for latest transformers patch release * tweak loss * guard on signature columns before removing attention mask * bump loss * set remove isn't chainable * skip slow mistral test in 2.5.1 --- cicd/Dockerfile.jinja | 1 + requirements.txt | 2 +- setup.py | 2 +- src/axolotl/core/attention/__init__.py | 0 src/axolotl/core/attention/flex_block_mask.py | 162 ++++++++++++++++++ src/axolotl/core/builders/causal.py | 11 +- src/axolotl/core/trainers/base.py | 16 +- src/axolotl/core/trainers/mixins/__init__.py | 1 + src/axolotl/core/trainers/mixins/packing.py | 20 +++ src/axolotl/core/training_args_base.py | 4 + src/axolotl/loaders/patch_manager.py | 30 +++- .../monkeypatch/ring_attn/adapters/batch.py | 6 +- src/axolotl/monkeypatch/ring_attn/patch.py | 108 +++++++++++- src/axolotl/monkeypatch/trainer_fsdp_optim.py | 10 +- src/axolotl/utils/schemas/config.py | 2 +- src/axolotl/utils/trainer.py | 3 + tests/conftest.py | 7 +- tests/e2e/multigpu/patched/test_sp.py | 2 +- tests/e2e/multigpu/solo/test_flex.py | 2 +- tests/e2e/multigpu/test_llama.py | 6 +- tests/e2e/patched/test_flattening.py | 81 +++++++++ tests/e2e/patched/test_mistral_samplepack.py | 3 +- tests/e2e/solo/test_flex.py | 2 +- 23 files changed, 449 insertions(+), 32 deletions(-) create mode 100644 src/axolotl/core/attention/__init__.py create mode 100644 src/axolotl/core/attention/flex_block_mask.py create mode 100644 src/axolotl/core/trainers/mixins/packing.py create mode 100644 tests/e2e/patched/test_flattening.py diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 6988e092ba..13920de786 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -9,6 +9,7 @@ ENV GITHUB_REF="{{ GITHUB_REF }}" ENV GITHUB_SHA="{{ GITHUB_SHA }}" ENV NIGHTLY_BUILD="{{ NIGHTLY_BUILD }}" ENV HF_HOME="{{ HF_HOME }}" +ENV AXOLOTL_DATASET_PROCESSES="8" RUN apt-get update && \ apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev diff --git a/requirements.txt b/requirements.txt index 10ac04a663..0ed1fa6150 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ packaging==23.2 huggingface_hub==0.32.2 peft==0.15.2 -transformers==4.52.4 +transformers==4.53.1 tokenizers>=0.21.1 accelerate==1.8.1 datasets==3.6.0 diff --git a/setup.py b/setup.py index 212625bddd..c222d0ad4a 100644 --- a/setup.py +++ b/setup.py @@ -114,7 +114,7 @@ def get_package_version(): "flash-attn": ["flash-attn==2.8.0.post2"], "ring-flash-attn": [ "flash-attn==2.8.0.post2", - "ring-flash-attn>=0.1.4", + "ring-flash-attn>=0.1.5", "yunchang==0.6.0", ], "deepspeed": [ diff --git a/src/axolotl/core/attention/__init__.py b/src/axolotl/core/attention/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/core/attention/flex_block_mask.py b/src/axolotl/core/attention/flex_block_mask.py new file mode 100644 index 0000000000..fb9820f352 --- /dev/null +++ b/src/axolotl/core/attention/flex_block_mask.py @@ -0,0 +1,162 @@ +""" +monkeypatch for flex + packing +""" + +import sys +from typing import Callable, Optional, Union + +import torch +from torch.nn.attention.flex_attention import BlockMask +from transformers import Cache, PretrainedConfig +from transformers.masking_utils import ( + ALL_MASK_ATTENTION_FUNCTIONS, + _preprocess_mask_arguments, + and_masks, + causal_mask_function, + or_masks, +) +from transformers.utils import is_torch_greater_or_equal + +_is_torch_greater_or_equal_than_2_6 = is_torch_greater_or_equal("2.6", accept_dev=True) + + +def create_causal_mask( + config: PretrainedConfig, + input_embeds: torch.Tensor, + attention_mask: torch.Tensor, + cache_position: torch.Tensor, + past_key_values: Optional[Cache], + or_mask_function: Optional[Callable] = None, + and_mask_function: Optional[Callable] = None, +) -> Optional[Union[torch.Tensor, BlockMask]]: + """ + Create a standard causal mask based on the attention implementation used (stored in the config). If `past_key_values` + has an HybridCache structure, this function will return the mask corresponding to one of the "full_attention" layers (to align + to what is needed in the `modeling_xxx.py` files). + + Args: + config (`PretrainedConfig`): + The model config. + input_embeds (`torch.Tensor`): + The input embeddings of shape (batch_size, query_length, hidden_dim). This is used only to infer the + batch size, query length and dtype. + attention_mask (`torch.Tensor`, optional): + The 2D attention mask corresponding to padded tokens of shape (batch_size, number_of_seen_tokens+q_length). + It can also be an already prepared 4D mask, in which case it is returned as-is. + cache_position (`torch.Tensor`): + A tensor of shape (query_length,) indicating the current indices of the input sequence elements. + past_key_values (`Cache`, optional): + The past key values, if we use a cache. + or_mask_function (`Callable`, optional): + An optional mask function to combine with the causal mask function (by doing the union of both). This is + useful to easily overlay another mask on top of the causal one, for example for image tokens handling. + and_mask_function (`Callable`, optional): + An optional mask function to combine with the causal mask function (by doing the intersection of both). This is + useful to easily overlay another mask on top of the causal one, for example for image tokens handling. + """ + # If we have an HybridCache structure, here we want to create the mask for the full layers + if ( + past_key_values + and hasattr(past_key_values, "is_sliding") + and False in past_key_values.is_sliding + ): + layer_idx = past_key_values.is_sliding.index(False) + else: + layer_idx = 0 + + original_attention_mask = ( + None + if attention_mask is None + else attention_mask.clone().to(cache_position.device) + ) + early_exit, attention_mask, kv_length, kv_offset = _preprocess_mask_arguments( + config, input_embeds, attention_mask, cache_position, past_key_values, layer_idx + ) + if early_exit: + return attention_mask + + batch_size, total_seq_len = cache_position.shape + key_length = total_seq_len + document_ids = torch.nn.functional.pad( + original_attention_mask, value=0, pad=(0, key_length) + ) + + batch_size, dtype = input_embeds.shape[0], input_embeds.dtype + if attention_mask is not None: + + def causal_doc_mask_mod( + batch_idx, head_idx, q_idx, kv_idx + ): # pylint: disable=unused-argument + """ + Defines the logic of a block causal mask by combining both a standard causal mask + and a block diagonal document mask. + See :func:`~torchtune.modules.attention_utils.create_block_causal_mask` + for an illustration. + """ + causal_mask_ = q_idx >= kv_idx # not valid when decoding + document_mask = ( + document_ids[batch_idx, q_idx] == document_ids[batch_idx, kv_idx] + ) + final_mask = causal_mask_ & document_mask + return final_mask + + mask_factory_function = causal_doc_mask_mod + else: + mask_factory_function = causal_mask_function + mask_interface = ALL_MASK_ATTENTION_FUNCTIONS[ + config._attn_implementation # pylint: disable=protected-access + ] + + # Do not allow skip if we are compiling (this is to match BC) + allow_is_causal_skip = ( + not past_key_values.is_compileable if past_key_values is not None else True + ) + + # Allow slight deviations from causal mask + if or_mask_function is not None: + if not _is_torch_greater_or_equal_than_2_6: + raise ValueError( + "Using `or_mask_function` or `and_mask_function` arguments require torch>=2.6" + ) + mask_factory_function = or_masks(mask_factory_function, or_mask_function) + allow_is_causal_skip = False + if and_mask_function is not None: + if not _is_torch_greater_or_equal_than_2_6: + raise ValueError( + "Using `or_mask_function` or `and_mask_function` arguments require torch>=2.6" + ) + mask_factory_function = and_masks(mask_factory_function, and_mask_function) + allow_is_causal_skip = False + + # We now create the mask + causal_mask = mask_interface( + batch_size=batch_size, + cache_position=cache_position, + kv_length=kv_length, + kv_offset=kv_offset, + mask_function=mask_factory_function, + attention_mask=attention_mask, + allow_is_causal_skip=allow_is_causal_skip, # additional kwarg for sdpa + dtype=dtype, # Additional kwarg for eager + config=config, # Pass the config as well, in case someone wants to easily have their own mask_interface + ) + return causal_mask + + +def patch_create_causal_mask(model_type): + import transformers.masking_utils + + transformers.masking_utils.create_causal_mask = create_causal_mask + + if model_type: + try: + # Dynamically import the module and attention class + module_path = f"transformers.models.{model_type}.modeling_{model_type}" + module = __import__(module_path) + module.create_causal_mask = create_causal_mask + del sys.modules[module_path] + except (ImportError, AttributeError) as e: + raise ValueError( + f"Could not import attention class for model_type: {model_type}. " + f"Error: {str(e)}" + ) from e diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index c847a087c8..6ef53bdff8 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -245,10 +245,19 @@ def build(self, total_num_steps): training_arguments_kwargs["curriculum_sampling"] = self.cfg.curriculum_sampling training_arguments_kwargs["sample_packing"] = bool(self.cfg.sample_packing) + training_arguments_kwargs["sample_packing_drop_attention_mask"] = bool( + self.cfg.flash_attention + or self.cfg.xformers_attention + or self.cfg.flex_attention + ) training_arguments_kwargs["multipack_real_batches"] = ( self.cfg.multipack_real_batches if self.cfg.multipack_real_batches is not None - else not self.cfg.flash_attention + else not ( + self.cfg.flash_attention + or self.cfg.flex_attention + or self.cfg.xformers_attention + ) ) training_arguments_kwargs["eval_sample_packing"] = bool( self.cfg.eval_sample_packing diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index b0e6e8eae1..81a2f5a452 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -27,6 +27,7 @@ from axolotl.core.trainers.mixins import ( CheckpointSaveMixin, OptimizerMixin, + PackingMixin, RngLoaderMixin, SchedulerMixin, ) @@ -42,7 +43,12 @@ class AxolotlTrainer( - SchedulerMixin, OptimizerMixin, RngLoaderMixin, CheckpointSaveMixin, Trainer + PackingMixin, + SchedulerMixin, + OptimizerMixin, + RngLoaderMixin, + CheckpointSaveMixin, + Trainer, ): """Extend the base Trainer for axolotl helpers""" @@ -206,6 +212,14 @@ def _get_dataloader( if dataset.column_names and "length" in dataset.column_names: dataset = dataset.remove_columns(["length"]) + if ( + dataset.column_names + and "position_ids" in dataset.column_names + and "attention_mask" in dataset.column_names + and self.args.sample_packing + and self.args.sample_packing_drop_attention_mask + ): + dataset = dataset.remove_columns(["attention_mask"]) if isinstance(dataset, datasets.Dataset): if is_training: diff --git a/src/axolotl/core/trainers/mixins/__init__.py b/src/axolotl/core/trainers/mixins/__init__.py index 1782320778..b73b511269 100644 --- a/src/axolotl/core/trainers/mixins/__init__.py +++ b/src/axolotl/core/trainers/mixins/__init__.py @@ -5,5 +5,6 @@ from .checkpoints import CheckpointSaveMixin from .optimizer import OptimizerMixin +from .packing import PackingMixin from .rng_state_loader import RngLoaderMixin from .scheduler import SchedulerMixin diff --git a/src/axolotl/core/trainers/mixins/packing.py b/src/axolotl/core/trainers/mixins/packing.py new file mode 100644 index 0000000000..249ceeb4f8 --- /dev/null +++ b/src/axolotl/core/trainers/mixins/packing.py @@ -0,0 +1,20 @@ +"""Trainer mixin to support packing""" + +from transformers import Trainer + + +class PackingMixin(Trainer): + """ + Trainer mixin to support packing + """ + + def _set_signature_columns_if_needed(self): + super()._set_signature_columns_if_needed() + if ( + self._signature_columns + and self.args.sample_packing + and self.args.sample_packing_drop_attention_mask + ): + set_sig_columns = set(self._signature_columns) + set_sig_columns.remove("attention_mask") + self._signature_columns = list(set_sig_columns) diff --git a/src/axolotl/core/training_args_base.py b/src/axolotl/core/training_args_base.py index e04be43e0e..2e1987e82c 100644 --- a/src/axolotl/core/training_args_base.py +++ b/src/axolotl/core/training_args_base.py @@ -42,6 +42,10 @@ class AxolotlTrainingMixins: default=None, metadata={"help": "The multiprocessing start method to use."}, ) + sample_packing_drop_attention_mask: bool = field( + default=False, + metadata={"help": "Drop attention mask from inputs when using packing."}, + ) multipack_real_batches: bool = field( default=False, metadata={"help": "Use real batches for efficient training."}, diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 610e87c7b7..221a5fce81 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -49,11 +49,11 @@ def has_flash_attn(self) -> bool: def apply_pre_model_load_patches(self): """Apply pre-model load patches based on config.""" + # self._apply_flex_attention_patches() self._apply_flash_attention_patches() self._apply_chunked_cross_entropy_patch() self._apply_fsdp_patches() self._apply_adapter_patches() - self._apply_flex_attention_patches() self._apply_model_specific_patches() self._apply_fp8_patches() self._apply_flash_attention_peft_patches() @@ -97,6 +97,14 @@ def _apply_fsdp_patches(self): patch_accelerate_fsdp2() + # if self.cfg.fsdp_config: + # # see transformers#39152 + # from axolotl.monkeypatch.trainer_fsdp_optim import ( + # patch_training_loop_for_fsdp, + # ) + # + # patch_training_loop_for_fsdp() + def _apply_adapter_patches(self): """Apply patches for adapter configurations.""" if self.cfg.adapter and self.cfg.embeddings_skip_upcast: @@ -107,14 +115,20 @@ def _apply_adapter_patches(self): def _apply_flex_attention_patches(self): """Apply patches for flexible attention.""" if self.cfg.flex_attention: - from axolotl.monkeypatch.attention.flex_attn import ( - patch_flex_make_mask, - patch_flex_wrapper, - ) + # from axolotl.monkeypatch.attention.flex_attn import ( + # patch_flex_make_mask, + # patch_flex_wrapper, + # ) + # + # flex_attn_compile_kwargs = self.cfg.flex_attn_compile_kwargs or {} + # patch_flex_wrapper(**flex_attn_compile_kwargs) + # patch_flex_make_mask() + if self.cfg.sample_packing: + from axolotl.core.attention.flex_block_mask import ( + patch_create_causal_mask, + ) - flex_attn_compile_kwargs = self.cfg.flex_attn_compile_kwargs or {} - patch_flex_wrapper(**flex_attn_compile_kwargs) - patch_flex_make_mask() + patch_create_causal_mask(self.cfg.model_config_type) def _apply_model_specific_patches(self): """Apply patches specific to model architectures.""" diff --git a/src/axolotl/monkeypatch/ring_attn/adapters/batch.py b/src/axolotl/monkeypatch/ring_attn/adapters/batch.py index e556ba5e33..5e56bdd04f 100644 --- a/src/axolotl/monkeypatch/ring_attn/adapters/batch.py +++ b/src/axolotl/monkeypatch/ring_attn/adapters/batch.py @@ -33,7 +33,7 @@ } -def create_flash_attn_forward( +def create_flash_attn_forward_varlen_llama3( process_group: dist.ProcessGroup, ring_attn_func: RingAttnFunc ) -> Callable: """ @@ -71,6 +71,7 @@ def _flash_attention_forward( max_length_q: int | None = None, max_length_k: int | None = None, target_dtype: torch.dtype | None = None, + attn_implementation: str | None = None, **kwargs, ): """ @@ -97,6 +98,7 @@ def _flash_attention_forward( max_length_q: Not used in this implementation. max_length_k: Not used in this implementation. target_dtype: Not used in this implementation. + attn_implementation: Not used in this implementation. **kwargs: Additional keyword arguments. Not used in this implementation. Returns: @@ -161,7 +163,7 @@ def substitute_hf_flash_attn( old_flash_attention_forward = ( transformers.modeling_flash_attention_utils._flash_attention_forward ) - new_flash_attention_forward = create_flash_attn_forward( + new_flash_attention_forward = create_flash_attn_forward_varlen_llama3( process_group=process_group, ring_attn_func=ring_attn_func ) diff --git a/src/axolotl/monkeypatch/ring_attn/patch.py b/src/axolotl/monkeypatch/ring_attn/patch.py index 017b420d28..41e39e657e 100644 --- a/src/axolotl/monkeypatch/ring_attn/patch.py +++ b/src/axolotl/monkeypatch/ring_attn/patch.py @@ -9,10 +9,13 @@ """ import inspect +import os +from typing import Optional import accelerate import torch import torch.distributed as dist +from transformers.modeling_flash_attention_utils import _flash_supports_window_size from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids from axolotl.utils.logging import get_logger @@ -62,6 +65,96 @@ def set_ring_attn_group(ring_attn_group: dist.ProcessGroup | None): RING_ATTN_GROUP = ring_attn_group +def create_ring_flash_attention_forward( + process_group: dist.ProcessGroup, heads_k_stride: int +): + from ring_flash_attn import llama3_flash_attn_varlen_func + from ring_flash_attn.adapters.hf_adapter import DATA_PARAMS + + def _flash_attention_forward_v3( + query_states: torch.Tensor, + key_states: torch.Tensor, + value_states: torch.Tensor, + attention_mask: torch.Tensor, # pylint: disable=unused-argument + query_length: int, + is_causal: bool, + dropout: float = 0.0, + position_ids: Optional[torch.Tensor] = None, # pylint: disable=unused-argument + softmax_scale: Optional[float] = None, + sliding_window: Optional[int] = None, + use_top_left_mask: bool = False, + softcap: Optional[float] = None, + deterministic: bool = None, + cu_seq_lens_q: Optional[ + torch.LongTensor + ] = None, # pylint: disable=unused-argument + cu_seq_lens_k: Optional[ + torch.LongTensor + ] = None, # pylint: disable=unused-argument + max_length_q: Optional[int] = None, # pylint: disable=unused-argument + max_length_k: Optional[int] = None, # pylint: disable=unused-argument + target_dtype: Optional[torch.dtype] = None, # pylint: disable=unused-argument + attn_implementation: Optional[str] = None, # pylint: disable=unused-argument + **kwargs, # pylint: disable=unused-argument + ): + # pylint: disable=duplicate-code + if not use_top_left_mask: + causal = is_causal + else: + # TODO: Remove the `query_length != 1` check once Flash Attention for RoCm is bumped to 2.1. For details, please see the comment in transformers.models.llama.modeling_llama.LlamaFlashAttention2.__init__. + causal = is_causal and query_length != 1 + + # Assuming 4D tensors, key_states.shape[1] is the key/value sequence length (source length). + use_sliding_windows = ( + _flash_supports_window_size + and sliding_window is not None + and key_states.shape[1] > sliding_window + ) + flash_kwargs = ( + {"window_size": (sliding_window, sliding_window)} + if use_sliding_windows + else {} + ) + + if deterministic is None: + deterministic = os.environ.get("FLASH_ATTENTION_DETERMINISTIC", "0") == "1" + flash_kwargs["deterministic"] = deterministic + assert ( + softcap is None + ), "llama3_flash_attn_varlen_func does not support softcap yet." + # flash_kwargs["softcap"] = softcap + flash_kwargs["group"] = process_group + + # not sure why attention_mask can be not None... + assert causal, "only causal attention is supported yet." + batch_size = query_states.size(0) + assert batch_size == 1, "varlen data should be processed in advance." + + attn_output = llama3_flash_attn_varlen_func( + query_states.squeeze(dim=0), + key_states.squeeze(dim=0), + value_states.squeeze(dim=0), + cu_seqlens_q=DATA_PARAMS["cu_seqlens_q"], + cu_seqlens_k=DATA_PARAMS["cu_seqlens_k"], + max_seqlen_q=DATA_PARAMS["max_seqlen_q"], + max_seqlen_k=DATA_PARAMS["max_seqlen_k"], + heads_k_stride=heads_k_stride, + local_k_slice=DATA_PARAMS["local_k_slice"], + dropout_p=dropout, + softmax_scale=softmax_scale, + causal=causal, + **flash_kwargs, + ) + + attn_output = attn_output.unsqueeze(dim=0) + + return attn_output + + return [ + _flash_attention_forward_v3, + ] + + def register_ring_attn( sequence_parallel_degree: int, heads_k_stride: int | None, @@ -118,9 +211,20 @@ def register_ring_attn( LOG.info(f"Sequence parallel group assignments: {group_assignments}") if ring_attn_func is RingAttnFunc.VARLEN_LLAMA3: - from ring_flash_attn import substitute_hf_flash_attn + # fmt: off + import ring_flash_attn.adapters.hf_adapter - substitute_hf_flash_attn( + from ring_flash_attn.adapters.hf_adapter import ( # isort: skip # pylint: disable=unused-import + create_ring_flash_attention_forward as create_ring_flash_attention_forward_orig, + ) + + create_ring_flash_attention_forward_orig = ( # noqa: F811,F841 + create_ring_flash_attention_forward + ) + ring_flash_attn.adapters.hf_adapter.create_ring_flash_attention_forward = create_ring_flash_attention_forward + # fmt: on + + ring_flash_attn.adapters.hf_adapter.substitute_hf_flash_attn( process_group=get_ring_attn_group(), heads_k_stride=heads_k_stride or 1 ) elif ring_attn_func is RingAttnFunc.BATCH_RING: diff --git a/src/axolotl/monkeypatch/trainer_fsdp_optim.py b/src/axolotl/monkeypatch/trainer_fsdp_optim.py index 4ce5b8ecd3..1c2511524d 100644 --- a/src/axolotl/monkeypatch/trainer_fsdp_optim.py +++ b/src/axolotl/monkeypatch/trainer_fsdp_optim.py @@ -12,15 +12,13 @@ LOG = get_logger(__name__) ORIGINAL_TRAINER_CODE = """ - - delay_optimizer_creation = is_sagemaker_mp_enabled() or self.is_fsdp_xla_enabled - + if delay_optimizer_creation: + self.optimizer = self.accelerator.prepare(self.optimizer) """ PATCHED_TRAINER_CODE = """ - - delay_optimizer_creation = is_sagemaker_mp_enabled() or self.is_fsdp_xla_enabled or self.is_fsdp_enabled - + if delay_optimizer_creation: + model = self.accelerator.prepare(self.model) """ diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 44a9a4f06f..6481202c76 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -203,7 +203,7 @@ class AxolotlInputConfig( }, ) dataset_processes: int | None = Field( - default=min(32, os.cpu_count()), # type: ignore[type-var] + default=min(int(os.environ.get("AXOLOTL_DATASET_PROCESSES", 32)), os.cpu_count()), # type: ignore[type-var] json_schema_extra={ "description": "The maximum number of processes to use while preprocessing your input dataset. This defaults to `os.cpu_count()` if not set." }, diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 06853451cc..cb597606ce 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -535,6 +535,9 @@ def setup_deepspeed_env(cfg, stage=None): os.environ["ACCELERATE_USE_DEEPSPEED"] = "true" os.environ["ACCELERATE_DEEPSPEED_CONFIG_FILE"] = cfg.deepspeed + os.environ["ACCELERATE_GRADIENT_ACCUMULATION_STEPS"] = str( + cfg.gradient_accumulation_steps + ) if stage: os.environ["ACCELERATE_DEEPSPEED_ZERO_STAGE"] = str(stage) if stage == 3: diff --git a/tests/conftest.py b/tests/conftest.py index bbe2d10ee5..24615fa224 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,12 +10,13 @@ import sys import tempfile import time -from pathlib import Path, PosixPath +from pathlib import Path from typing import Generator import datasets import pytest import requests +import torch from huggingface_hub import snapshot_download from huggingface_hub.errors import LocalEntryNotFoundError from tokenizers import AddedToken @@ -424,8 +425,8 @@ def temp_dir() -> Generator[str, None, None]: @pytest.fixture(scope="function", autouse=True) -def unique_triton_cache_dir(temp_dir: str | PosixPath) -> None: - os.environ["TRITON_CACHE_DIR"] = str(temp_dir) + "/.triton/cache" +def torch_manual_seed(): + torch.manual_seed(42) @pytest.fixture(scope="function", autouse=True) diff --git a/tests/e2e/multigpu/patched/test_sp.py b/tests/e2e/multigpu/patched/test_sp.py index 31a728f201..5593c7eb62 100644 --- a/tests/e2e/multigpu/patched/test_sp.py +++ b/tests/e2e/multigpu/patched/test_sp.py @@ -104,7 +104,7 @@ def _run_sequence_parallel_test( (True, 1, True, None, 2.5), # defaults to varlen_llama3 ring_attn_func (False, 2, True, None, 2.5), # defaults to batch_ring ring_attn_func # (False, 2, True, "batch_zigzag", 2.5), - (False, 2, False, None, 2.5), # defaults to batch_ring ring_attn_func + (False, 2, False, None, 2.65), # defaults to batch_ring ring_attn_func ], ids=[ "sample_packing, varlen_llama3 ring_attn_func", diff --git a/tests/e2e/multigpu/solo/test_flex.py b/tests/e2e/multigpu/solo/test_flex.py index b892fe2132..bdf5ada6ba 100644 --- a/tests/e2e/multigpu/solo/test_flex.py +++ b/tests/e2e/multigpu/solo/test_flex.py @@ -86,5 +86,5 @@ def test_loss_llama(self, temp_dir): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss (%s) is too high" + temp_dir + "/runs", "train/train_loss", 2.1, "Train Loss (%s) is too high" ) diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index d84505714f..7f9db12f3d 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -90,7 +90,7 @@ def test_lora_ddp(self, temp_dir): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" + temp_dir + "/runs", "train/train_loss", 2.8, "Train Loss (%s) is too high" ) @pytest.mark.parametrize( @@ -364,6 +364,7 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", }, "use_tensorboard": True, + "seed": 42, } ) @@ -759,6 +760,7 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): "flash_attention": True, "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero2.json"), "use_tensorboard": True, + "seed": 42, **adapter, } ) @@ -856,7 +858,7 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" + temp_dir + "/runs", "train/train_loss", 2.5, "Train Loss (%s) is too high" ) @pytest.mark.skip( diff --git a/tests/e2e/patched/test_flattening.py b/tests/e2e/patched/test_flattening.py new file mode 100644 index 0000000000..f77a1fbe5a --- /dev/null +++ b/tests/e2e/patched/test_flattening.py @@ -0,0 +1,81 @@ +""" +E2E tests for flattening batches +""" + +import pytest +from transformers.utils import is_torch_bf16_gpu_available + +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +from ..utils import check_model_output_exists, check_tensorboard + + +class TestFAFlattening: + """ + Test case for Llama models using LoRA w batch flattening + """ + + @pytest.mark.parametrize( + "gradient_accumulation_steps", + [1, 4], + ) + def test_lora_packing_flattening(self, temp_dir, gradient_accumulation_steps): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 1024, + "batch_flattening": True, + "flash_attention": True, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.05, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "chat_template": "chatml", + "datasets": [ + { + "path": "mlabonne/FineTome-100k", + "field_messages": "conversations", + "message_field_content": "value", + "message_field_role": "from", + "type": "chat_template", + "split": "train[:2%]", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "save_steps": 5, + "micro_batch_size": 2, + "gradient_accumulation_steps": gradient_accumulation_steps, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "use_tensorboard": True, + } + ) + if is_torch_bf16_gpu_available(): + cfg.bf16 = True + else: + cfg.fp16 = True + + cfg = validate_config(cfg) + normalize_config(cfg) + + dataset_meta = load_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 1.5, "Train Loss (%s) is too high" + ) diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index 2de9cc96ff..442089bae7 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -9,7 +9,7 @@ from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from ..utils import check_model_output_exists, with_temp_dir +from ..utils import check_model_output_exists, require_torch_2_6_0, with_temp_dir class TestMistral(unittest.TestCase): @@ -17,6 +17,7 @@ class TestMistral(unittest.TestCase): Test case for Llama models using LoRA """ + @require_torch_2_6_0 @with_temp_dir def test_lora_packing(self, temp_dir): # pylint: disable=duplicate-code diff --git a/tests/e2e/solo/test_flex.py b/tests/e2e/solo/test_flex.py index f6b8c6283b..2799137136 100644 --- a/tests/e2e/solo/test_flex.py +++ b/tests/e2e/solo/test_flex.py @@ -63,5 +63,5 @@ def test_loss_llama(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss (%s) is too high" + temp_dir + "/runs", "train/train_loss", 2.1, "Train Loss (%s) is too high" ) From 759cefb74154d3bdbedd9bf21f9388b1ffced4bf Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 7 Jul 2025 10:10:58 -0400 Subject: [PATCH 0768/1405] setup defaults for dataloader to ensure GPU is kept busy (#2632) [skip ci] --- src/axolotl/utils/schemas/config.py | 14 ++++++++++++++ tests/patched/test_validation.py | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 6481202c76..21b69824f7 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1114,3 +1114,17 @@ def check_qat_config(cls, data): raise ValueError("QAT is not supported on torch version < 2.6.0") return data + + @model_validator(mode="before") + @classmethod + def default_dataloader_opts(cls, data): + if ( + data.get("dataloader_num_workers") is None + and data.get("dataloader_pin_memory") is None + and data.get("dataloader_prefetch_factor") is None + ): + data["dataloader_num_workers"] = data.get("capabilities").get("n_gpu", 1) + data["dataloader_pin_memory"] = True + data["dataloader_prefetch_factor"] = 256 + + return data diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 2c28a71ea3..55e25daf75 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -1690,3 +1690,18 @@ def test_mlflow_not_used_by_default(self, minimal_cfg): assert new_cfg.use_mlflow is True os.environ.pop("MLFLOW_EXPERIMENT_NAME", None) + + +class TestDataloaderValidation(BaseValidation): + """ + tests for dataloader_* sane defaults + """ + + def test_dataloader_auto_defaults(self, minimal_cfg): + cfg = minimal_cfg + + new_cfg = validate_config(cfg, {"n_gpu": 8}, {"torch_version": "2.6.0"}) + + assert new_cfg.dataloader_num_workers == 8 + assert new_cfg.dataloader_pin_memory is True + assert new_cfg.dataloader_prefetch_factor == 256 From faff0cff41684c1ed2972996dc2c81e17176fe45 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 7 Jul 2025 10:11:48 -0400 Subject: [PATCH 0769/1405] manage jinja templates as nicely formatted files (#2795) * manage jinja templates as nicely formatted files * chore: lint * use path for templates relative to the module * fix template reformating * handle newlines in llama3 template * fix gemma3 jinja * fix templates * suport for passing jinja template file in yaml * handle file loading of jinja template outside of validation * fix typing and typo --- MANIFEST.in | 1 + src/axolotl/utils/chat_templates.py | 149 ---------- src/axolotl/utils/chat_templates/__init__.py | 20 ++ src/axolotl/utils/chat_templates/base.py | 125 +++++++++ .../chat_templates/templates/alpaca.jinja | 8 + .../utils/chat_templates/templates/aya.jinja | 1 + .../chat_templates/templates/chatml.jinja | 4 + .../chat_templates/templates/cohere.jinja | 1 + .../chat_templates/templates/command_a.jinja | 210 +++++++++++++++ .../templates/command_a_rag.jinja | 158 +++++++++++ .../templates/command_a_tool_use.jinja | 157 +++++++++++ .../templates/deepseek_v2.jinja | 3 + .../templates/deepseek_v3.jinja | 1 + .../chat_templates/templates/exaone.jinja | 4 + .../chat_templates/templates/falcon_h1.jinja | 17 ++ .../chat_templates/templates/gemma.jinja | 4 + .../chat_templates/templates/gemma3.jinja | 47 ++++ .../chat_templates/templates/jamba.jinja | 255 ++++++++++++++++++ .../chat_templates/templates/llama3.jinja | 5 + .../templates/llama3_2_vision.jinja | 122 +++++++++ .../chat_templates/templates/llama4.jinja | 123 +++++++++ .../chat_templates/templates/llava.jinja | 2 + .../chat_templates/templates/metharme.jinja | 1 + .../chat_templates/templates/mistral_v1.jinja | 1 + .../templates/mistral_v2v3.jinja | 1 + .../templates/mistral_v3_tekken.jinja | 1 + .../templates/mistral_v7_tekken.jinja | 51 ++++ .../chat_templates/templates/phi_3.jinja | 7 + .../chat_templates/templates/phi_35.jinja | 8 + .../chat_templates/templates/phi_4.jinja | 1 + .../chat_templates/templates/pixtral.jinja | 53 ++++ .../chat_templates/templates/qwen2_vl.jinja | 7 + .../chat_templates/templates/qwen3.jinja | 87 ++++++ .../chat_templates/templates/qwen_25.jinja | 54 ++++ src/axolotl/utils/schemas/config.py | 2 +- src/axolotl/utils/schemas/datasets.py | 2 +- 36 files changed, 1542 insertions(+), 151 deletions(-) delete mode 100644 src/axolotl/utils/chat_templates.py create mode 100644 src/axolotl/utils/chat_templates/__init__.py create mode 100644 src/axolotl/utils/chat_templates/base.py create mode 100644 src/axolotl/utils/chat_templates/templates/alpaca.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/aya.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/chatml.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/cohere.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/command_a.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/command_a_rag.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/command_a_tool_use.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/deepseek_v2.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/deepseek_v3.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/exaone.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/falcon_h1.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/gemma.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/gemma3.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/jamba.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/llama3.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/llama3_2_vision.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/llama4.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/llava.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/metharme.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/mistral_v1.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/mistral_v2v3.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/mistral_v3_tekken.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/mistral_v7_tekken.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/phi_3.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/phi_35.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/phi_4.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/pixtral.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/qwen2_vl.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/qwen3.jinja create mode 100644 src/axolotl/utils/chat_templates/templates/qwen_25.jinja diff --git a/MANIFEST.in b/MANIFEST.in index 99324be3cd..3fbb0edca4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,4 +2,5 @@ include requirements.txt include README.md include LICENSE include src/setuptools_axolotl_dynamic_dependencies.py +include src/axolotl/utils/chat_templates/templates/*.jinja recursive-include axolotl *.py diff --git a/src/axolotl/utils/chat_templates.py b/src/axolotl/utils/chat_templates.py deleted file mode 100644 index c809ffc7af..0000000000 --- a/src/axolotl/utils/chat_templates.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -This module provides functionality for selecting chat templates based on user choices. -These templates are used for formatting messages in a conversation. -""" - -from typing import TYPE_CHECKING, Any, Dict, Optional - -from axolotl.utils.logging import get_logger - -if TYPE_CHECKING: - from transformers import PreTrainedTokenizerBase - -LOG = get_logger("axolotl.utils.chat_templates") - -_JINJA_TEMPALTE_CHOICE = "jinja" -_DEFAULT_TEMPLATE_CHOICE = "tokenizer_default" -_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX = "tokenizer_default_fallback_" - -_CHAT_TEMPLATES = { - "alpaca": "{{ bos_token }}{% for message in messages %}{% if message['role'] == 'system' and loop.first %}{{ message['content'] }}{% elif message['role'] == 'user' %}{{ '### Instruction:\n' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '### Response:\n' + message['content'] + eos_token }}{% endif %}{% if not loop.last %}{{ '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '\n\n### Response:\n' }}{% endif %}", - "mistral_v1": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ ' [INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # Mistral 7B V1, Mistral 7B V2, Mixtral 8x7B V1... - "mistral_v2v3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # V3: Mistral 7B V3, Small, Large... - "mistral_v3_tekken": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST]' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}", # V3-Tekken: Nemo, Pixtral... - "mistral_v7_tekken": "{%- set today = strftime_now(\"%Y-%m-%d\") %}\n{%- set default_system_message = \"You are Mistral Small 3, a Large Language Model (LLM) created by Mistral AI, a French startup headquartered in Paris.\\nYour knowledge base was last updated on 2023-10-01. The current date is \" + today + \".\\n\\nWhen you're not sure about some information, you say that you don't have the information and don't make up anything.\\nIf the user's question is not clear, ambiguous, or does not provide enough context for you to accurately answer the question, you do not try to answer it right away and you rather ask the user to clarify their request (e.g. \\\"What are some good restaurants around me?\\\" => \\\"Where are you?\\\" or \\\"When is the next flight to Tokyo\\\" => \\\"Where do you travel from?\\\")\" %}\n\n{{- bos_token }}\n\n{%- if messages[0]['role'] == 'system' %}\n {%- if messages[0]['content'] is string %}\n {%- set system_message = messages[0]['content'] %}\n {%- else %}\n {%- set system_message = messages[0]['content'][0]['text'] %}\n {%- endif %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set system_message = default_system_message %}\n {%- set loop_messages = messages %}\n{%- endif %}\n{{- '[SYSTEM_PROMPT]' + system_message + '[/SYSTEM_PROMPT]' }}\n\n{%- for message in loop_messages %}\n {%- if message['role'] == 'user' %}\n {%- if message['content'] is string %}\n {{- '[INST]' + message['content'] + '[/INST]' }}\n {%- else %}\n {{- '[INST]' }}\n {%- for block in message['content'] %}\n {%- if block['type'] == 'text' %}\n {{- block['text'] }}\n {%- elif block['type'] in ['image', 'image_url'] %}\n {{- '[IMG]' }}\n {%- else %}\n {{- raise_exception('Only text and image blocks are supported in message content!') }}\n {%- endif %}\n {%- endfor %}\n {{- '[/INST]' }}\n {%- endif %}\n {%- elif message['role'] == 'system' %}\n {%- if message['content'] is string %}\n {{- '[SYSTEM_PROMPT]' + message['content'] + '[/SYSTEM_PROMPT]' }}\n {%- else %}\n {{- '[SYSTEM_PROMPT]' + message['content'][0]['text'] + '[/SYSTEM_PROMPT]' }}\n {%- endif %}\n {%- elif message['role'] == 'assistant' %}\n {%- if message['content'] is string %}\n {{- message['content'] + eos_token }}\n {%- else %}\n {{- message['content'][0]['text'] + eos_token }}\n {%- endif %}\n {%- else %}\n {{- raise_exception('Only user, system and assistant roles are supported!') }}\n {%- endif %}\n{%- endfor %}", - "chatml": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}", - "gemma": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\n' + message['content'] | trim + '\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\n'}}{% endif %}", - "gemma3": "{{ bos_token }}\n{%- if messages[0]['role'] == 'system' -%}\n {%- if messages[0]['content'] is string -%}\n {%- set first_user_prefix = messages[0]['content'] + '\n\n' -%}\n {%- else -%}\n {%- set first_user_prefix = messages[0]['content'][0]['text'] + '\n\n' -%}\n {%- endif -%}\n {%- set loop_messages = messages[1:] -%}\n{%- else -%}\n {%- set first_user_prefix = \"\" -%}\n {%- set loop_messages = messages -%}\n{%- endif -%}\n{%- for message in loop_messages -%}\n {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) -%}\n {{ raise_exception(\"Conversation roles must alternate user/assistant/user/assistant/...\") }}\n {%- endif -%}\n {%- if (message['role'] == 'assistant') -%}\n {%- set role = \"model\" -%}\n {%- else -%}\n {%- set role = message['role'] -%}\n {%- endif -%}\n {{ '' + role + '\n' + (first_user_prefix if loop.first else \"\") }}\n {%- if message['content'] is string -%}\n {{ message['content'] | trim }}\n {%- elif message['content'] is iterable -%}\n {%- for item in message['content'] -%}\n {%- if item['type'] == 'image' -%}\n {{ '' }}\n {%- elif item['type'] == 'text' -%}\n {{ item['text'] | trim }}\n {%- endif -%}\n {%- endfor -%}\n {%- else -%}\n {{ raise_exception(\"Invalid content type\") }}\n {%- endif -%}\n {{ '\n' }}\n{%- endfor -%}\n{%- if add_generation_prompt -%}\n {{'model\n'}}\n{%- endif -%}\n", - "cohere": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", - "llama3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}", - "llama3_2_vision": '{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now("%d %b %Y") %}\n {%- else %}\n {%- set date_string = "26 Jul 2024" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0][\'role\'] == \'system\' %}\n {%- set system_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n{%- else %}\n {%- set system_message = "" %}\n{%- endif %}\n\n{#- Find out if there are any images #}\n{% set image_ns = namespace(has_images=false) %} \n{%- for message in messages %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {%- set image_ns.has_images = true %}\n {%- endif %}\n {%- endfor %}\n{%- endfor %}\n\n{#- Error out if there are images and system message #}\n{%- if image_ns.has_images and not system_message == "" %}\n {{- raise_exception("Prompting with images is incompatible with system messages.") }}\n{%- endif %}\n\n{#- System message if there are no images #}\n{%- if not image_ns.has_images %}\n {{- "<|start_header_id|>system<|end_header_id|>\\n\\n" }}\n {%- if tools is not none %}\n {{- "Environment: ipython\\n" }}\n {%- endif %}\n {{- "Cutting Knowledge Date: December 2023\\n" }}\n {{- "Today Date: " + date_string + "\\n\\n" }}\n {%- if tools is not none and not tools_in_user_message %}\n {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- "<|eot_id|>" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0][\'content\']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception("Cannot put tools in the first user message when there\'s no first user message!") }}\n{%- endif %}\n {{- \'<|start_header_id|>user<|end_header_id|>\\n\\n\' -}}\n {{- "Given the following functions, please respond with a JSON for a function call " }}\n {{- "with its proper arguments that best answers the given prompt.\\n\\n" }}\n {{- \'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {{- first_user_message + "<|eot_id|>"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == \'ipython\' or message.role == \'tool\' or \'tool_calls\' in message) %}\n {{- \'<|start_header_id|>\' + message[\'role\'] + \'<|end_header_id|>\\n\\n\' }}\n {%- if message[\'content\'] is string %}\n {{- message[\'content\'] }}\n {%- else %}\n {%- for content in message[\'content\'] %}\n {%- if content[\'type\'] == \'image\' %}\n {{- \'<|image|>\' }}\n {%- elif content[\'type\'] == \'text\' %}\n {{- content[\'text\'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \'<|eot_id|>\' }}\n {%- elif \'tool_calls\' in message %}\n {%- if not message.tool_calls|length == 1 %}\n {{- raise_exception("This model only supports single tool-calls at once!") }}\n {%- endif %}\n {%- set tool_call = message.tool_calls[0].function %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' -}}\n {{- \'{"name": "\' + tool_call.name + \'", \' }}\n {{- \'"parameters": \' }}\n {{- tool_call.arguments | tojson }}\n {{- "}" }}\n {{- "<|eot_id|>" }}\n {%- elif message.role == "tool" or message.role == "ipython" %}\n {{- "<|start_header_id|>ipython<|end_header_id|>\\n\\n" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- "<|eot_id|>" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- \'<|start_header_id|>assistant<|end_header_id|>\\n\\n\' }}\n{%- endif %}\n', - "llama4": "{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- if strftime_now is defined %}\n {%- set date_string = strftime_now(\"%d %b %Y\") %}\n {%- else %}\n {%- set date_string = \"26 Jul 2024\" %}\n {%- endif %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0]['role'] == 'system' %} \n {%- if messages[0]['content'] is string %}\n {%- set system_message = messages[0]['content']|trim %}\n {%- else %}\n {#- FIXME: The processor requires an array, always. #}\n {%- set system_message = messages[0]['content'][0]['text']|trim %}\n {%- endif %}\n {%- set messages = messages[1:] %}\n {%- set user_supplied_system_message = true %}\n{%- else %}\n {%- set system_message = \"\" %}\n {%- set user_supplied_system_message = false %}\n{%- endif %}\n\n{#- System message if the user supplied one #}\n{%- if user_supplied_system_message %}\n {{- \"<|header_start|>system<|header_end|>\\n\\n\" }}\n {%- if tools is not none %}\n {{- \"Environment: ipython\\n\" }}\n {%- endif %}\n {%- if tools is not none and not tools_in_user_message %}\n {{- \"You have access to the following functions. To call a function, please respond with JSON for a function call.\" }}\n {{- 'Respond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}.' }}\n {{- \"Do not use variables.\\n\\n\" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- \"\\n\\n\" }}\n {%- endfor %}\n {%- endif %}\n {{- system_message }}\n {{- \"<|eot|>\" }}\n{%- endif %}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0]['content']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception(\"Cannot put tools in the first user message when there's no first user message!\") }}\n{%- endif %}\n {{- '<|header_start|>user<|header_end|>\\n\\n' -}}\n {{- \"Given the following functions, please respond with a JSON for a function call \" }}\n {{- \"with its proper arguments that best answers the given prompt.\\n\\n\" }}\n {{- 'Respond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}.' }}\n {{- \"Do not use variables.\\n\\n\" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- \"\\n\\n\" }}\n {%- endfor %}\n {{- first_user_message + \"<|eot|>\"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}\n {{- '<|header_start|>' + message['role'] + '<|header_end|>\\n\\n' }}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- else %}\n {%- for content in message['content'] %}\n {%- if content['type'] == 'image' %}\n {{- '<|image|>' }}\n {%- elif content['type'] == 'text' %}\n {{- content['text'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- \"<|eot|>\" }}\n {%- elif 'tool_calls' in message and message.tool_calls|length > 0 %}\n {{- '<|header_start|>assistant<|header_end|>\\n\\n' -}}\n {{- '<|python_start|>' }}\n {%- if message['content'] is string %}\n {{- message['content'] }}\n {%- else %}\n {%- for content in message['content'] %}\n {%- if content['type'] == 'image' %}\n {{- '<|image|>' }}\n {%- elif content['type'] == 'text' %}\n {{- content['text'] }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '<|python_end|>' }}\n {%- for tool_call in message.tool_calls %}\n {{- '{\"name\": \"' + tool_call.function.name + '\", ' }}\n {{- '\"parameters\": ' }}\n {{- tool_call.function.arguments | tojson }}\n {{- \"}\" }}\n {%- endfor %}\n {{- \"<|eot|>\" }}\n {%- elif message.role == \"tool\" or message.role == \"ipython\" %}\n {{- \"<|header_start|>ipython<|header_end|>\\n\\n\" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- \"<|eot|>\" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|header_start|>assistant<|header_end|>\\n\\n' }}\n{%- endif %}\n", - "llava": "{% for message in messages %}{% if message['role'] != 'system' %}{{ message['role'].upper() + ': '}}{% endif %}{# Render all images first #}{% for content in message['content'] | selectattr('type', 'equalto', 'image') %}{{ '\n' }}{% endfor %}{# Render all text next #}{% if message['role'] != 'assistant' %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{{ content['text'] + ' '}}{% endfor %}{% else %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{% generation %}{{ content['text'] + ' '}}{% endgeneration %}{% endfor %}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ 'ASSISTANT:' }}{% endif %}", - "phi_3": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + '\n' + message['content'] + '<|end|>' + '\n'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + '\n' + message['content'] + '<|end|>' + '\n' + '<|assistant|>' + '\n'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + '\n'}}{% endif %}{% endfor %}", - "phi_35": "{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'user' %}{{'<|user|>\n' + message['content'] + '<|end|>\n'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|>\n' + message['content'] + '<|end|>\n'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% endif %}", - "phi_4": "{% set system_message = 'You are Phi, a language model trained by Microsoft to help users. Your role as an assistant involves thoroughly exploring questions through a systematic thinking process before providing the final precise and accurate solutions. This requires engaging in a comprehensive cycle of analysis, summarizing, exploration, reassessment, reflection, backtracing, and iteration to develop well-considered thinking process. Please structure your response into two main sections: Thought and Solution using the specified format: {Thought section} {Solution section}. In the Thought section, detail your reasoning process in steps. Each step should include detailed considerations such as analysing questions, summarizing relevant findings, brainstorming new ideas, verifying the accuracy of the current steps, refining any errors, and revisiting previous steps. In the Solution section, based on various attempts, explorations, and reflections from the Thought section, systematically present the final solution that you deem correct. The Solution section should be logical, accurate, and concise and detail necessary steps needed to reach the conclusion. Now, try to solve the following question through the above guidelines:' -%}{%- if messages and messages[0]['role'] == 'system' -%}{%- set system_message = messages[0]['content'] -%}{%- set messages = messages[1:] -%}{%- endif -%}<|im_start|>system<|im_sep|>{{ system_message }}<|im_end|>{% for message in messages %}{% if (message['role'] == 'user') %}{{'<|im_start|>user<|im_sep|>' + message['content'] + '<|im_end|>'}}{% elif (message['role'] == 'assistant') %}{{'<|im_start|>assistant<|im_sep|>'}}{% generation %}{{message['content'] + '<|im_end|>'}}{% endgeneration %}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant<|im_sep|>' }}{% endif %}", - "deepseek_v2": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %}", - "deepseek_v3": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='', is_first_sp=true) %}{%- for message in messages %}{%- if message['role'] == 'system' %}{%- if ns.is_first_sp %}{% set ns.system_prompt = ns.system_prompt + message['content'] %}{% set ns.is_first_sp = false %}{%- else %}{% set ns.system_prompt = ns.system_prompt + '\\n\\n' + message['content'] %}{%- endif %}{%- endif %}{%- endfor %}{{ bos_token }}{{ ns.system_prompt }}{%- for message in messages %}{%- if message['role'] == 'user' %}{%- set ns.is_tool = false -%}{{'<|User|>' + message['content']}}{%- endif %}{%- if message['role'] == 'assistant' and 'tool_calls' in message %}{%- set ns.is_tool = false -%}{%- for tool in message['tool_calls'] %}{%- if not ns.is_first %}{%- if message['content'] is none %}{{'<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- else %}{{'<|Assistant|>' + message['content'] + '<|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- endif %}{%- set ns.is_first = true -%}{%- else %}{{'\\n' + '<|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- endif %}{%- endfor %}{{'<|tool▁calls▁end|><|end▁of▁sentence|>'}}{%- endif %}{%- if message['role'] == 'assistant' and 'tool_calls' not in message %}{%- if ns.is_tool %}{{'<|tool▁outputs▁end|>' + message['content'] + '<|end▁of▁sentence|>'}}{%- set ns.is_tool = false -%}{%- else %}{% set content = message['content'] %}{% if '' in content %}{% set content = content.split('')[-1] %}{% endif %}{{'<|Assistant|>' + content + '<|end▁of▁sentence|>'}}{%- endif %}{%- endif %}{%- if message['role'] == 'tool' %}{%- set ns.is_tool = true -%}{%- if ns.is_output_first %}{{'<|tool▁outputs▁begin|><|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- set ns.is_output_first = false %}{%- else %}{{'<|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- endif %}{%- endif %}{%- endfor -%}{% if ns.is_tool %}{{'<|tool▁outputs▁end|>'}}{% endif %}{% if add_generation_prompt and not ns.is_tool %}{{'<|Assistant|>'}}{% endif %}", - "jamba": '{# Variables #}\n{% set ns = namespace(message_count=0, is_last_checked_defined=False) %}\n{##}\n{% set bom_str = bom_str or "<|bom|>" %}\n{% set eom_str = eom_str or "<|eom|>" %}\n{% set default_system_message = "" %}\n{##}\n{% set documents_prefix = "" %}\n{% set documents_suffix = "" %}\n{% set tool_definitions_prefix = "" %}\n{% set tool_definitions_suffix = "" %}\n{% set active_modes_prefix = "" %}\n{% set active_modes_suffix = "" %}\n{##}\n{% set tool_calls_prefix = "" %}\n{% set tool_calls_suffix = "" %}\n{% set citations_prefix = "" %}\n{% set citations_suffix = "" %}\n{##}\n{% if add_generation_prompt is not defined %}\n {% set add_generation_prompt = True %}\n{% endif %}\n{% set role_to_predict = role_to_predict or "assistant" %}\n{% if messages|length > 0 and messages[0].role == "system" %}\n {% set system_message = messages[0].content %}\n {% set loop_messages = messages[1:] %}\n{% else %}\n {% set system_message = default_system_message %}\n {% set loop_messages = messages %}\n{% endif %}\n{##}\n{##}\n{# Macros #}\n{% macro handle_tool_definitions(tools) %}\n {{- tool_definitions_prefix -}}\n {{- "\\n# Tools" -}}\n {{- "\\n\\n## Functions" -}}\n {% for tool in tools %}\n {% set _ = is_param_set(tool, field="type") %}\n {% set is_tool_type_set = ns.is_last_checked_defined %}\n {% if is_tool_type_set %}\n {% if tool.type == "function" %}\n {% set tool = tool.function %}\n {% else %}\n {{ raise_exception("Currently, the only supported tool type is `function`") }}\n {% endif %}\n {% endif %}\n {{- "\\n\\n" + (tool|tojson(indent=2)) -}}\n {% endfor %}\n {{- "\\n" + tool_definitions_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_first_system_message(system_message, tools) %}\n {{- bom_str + handle_role("system") -}}\n {% set _ = is_param_set(system_message) %}\n {% set is_system_message_set = ns.is_last_checked_defined %}\n {% if is_system_message_set %}\n {{- system_message -}}\n {% endif %}\n {% set _ = is_param_set(tools, is_list=True) %}\n {% set is_tools_set = ns.is_last_checked_defined %}\n {% if is_tools_set %}\n {% if system_message %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- handle_tool_definitions(tools) -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_tool_calls(tool_calls) %}\n {{- tool_calls_prefix + "[\\n" -}}\n {% for tool_call in tool_calls %}\n {% set _ = is_param_set(tool_call, field="function") %}\n {% set is_tool_call_function_set = ns.is_last_checked_defined %}\n {% if is_tool_call_function_set %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {% set arguments = tool_call.arguments %}\n {% if arguments is not string %}\n {%- set arguments = arguments|tojson -%}\n {%- endif %}\n {{ "{\\"name\\": \\"" + tool_call.name + "\\", \\"arguments\\": " + arguments + "}" -}}\n {% if not loop.last %}\n {{- "," }}\n {% endif %}\n {% endfor %}\n {{- "\\n]" + tool_calls_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_documents(documents) %}\n {{- documents_prefix -}}\n {{- "\\n# Documents" -}}\n {{- "\\n\\nYou can use the following documents for reference:" -}}\n {% for doc in documents %}\n {{- "\\n\\n## Document ID: " + loop.index0|string -}}\n {% set _ = is_param_set(doc, field="title") %}\n {% set is_doc_title_set = ns.is_last_checked_defined %}\n {% if is_doc_title_set %}\n {{- "\\nTitle: " + doc.title -}}\n {% endif %}\n {% for key, value in doc.items() %}\n {% if key not in ["title", "text"] %}\n {{- "\\n" + key|title + ": " + value|string -}}\n {% endif %}\n {% endfor %}\n {{- "\\nText: " + doc.text -}}\n {% endfor %}\n {{- "\\n" + documents_suffix -}}\n{% endmacro %}\n{##}\n{% macro handle_knobs(knobs) %}\n {{- active_modes_prefix -}}\n {{- "\\n# Active Modes" -}}\n {{ "\\n\\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}}\n {{ " active modes simultaneously." -}}\n {% if knobs.citation_mode == "fast" %}\n {{- "\\n\\n## Citation Mode" -}}\n {{- "\\n\\nProvide a list of references only for the documents you base your response on. Format your response" -}}\n {{ " with the original answer followed by a citation section. Use this template:" -}}\n {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}}\n {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}}\n {% endif %}\n {% if knobs.response_format == "json_object" %}\n {{- "\\n\\n## JSON Mode" -}}\n {{ "\\n\\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}}\n {{ " If an appropriate JSON format exists, use it without modification." -}}\n {% endif %}\n {{- "\\n" + active_modes_suffix -}}\n{% endmacro %}\n{##}\n{% macro get_last_user_index(messages) %}\n {% set ns.last_user_index = 0 %}\n {% for message in messages %}\n {% if message.role == \'user\' %}\n {% set ns.last_user_index = loop.index0 %}\n {% endif %}\n {% endfor %}\n {{- ns.last_user_index -}}\n{% endmacro %}\n{##}\n{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %}\n {{- bom_str + handle_role("system") -}}\n {% set macros_to_call = [] %}\n {% set params_for_macros = [] %}\n {% if use_documents %}\n {% set macros_to_call = macros_to_call + [handle_documents] %}\n {% set params_for_macros = params_for_macros + [[documents]] %}\n {% endif %}\n {% if use_knobs %}\n {% set macros_to_call = macros_to_call + [handle_knobs] %}\n {% set params_for_macros = params_for_macros + [[knobs]] %}\n {% endif %}\n {% for i in range(macros_to_call|length) %}\n {% if i > 0 %}\n {{- "\\n\\n" -}}\n {% endif %}\n {{- macros_to_call[i](*params_for_macros[i]) -}}\n {% endfor %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endmacro %}\n{##}\n{% macro handle_role(role, add_space=True) %}\n {{- "<|" + role + "|>" -}}\n {% if add_space %}\n {{- " " -}}\n {% endif %}\n{% endmacro %}\n{##}\n{% macro is_param_set(param, field=none, is_list=False) %}\n {% if field is not none %}\n {% if field in param %}\n {% set param = param[field] %}\n {% else %}\n {% set param = none %}\n {% endif %}\n {% endif %}\n {% set is_defined = param is defined and param is not none %}\n {% if is_list %}\n {% set ns.is_last_checked_defined = is_defined and param|length > 0 %}\n {% else %}\n {% set ns.is_last_checked_defined = is_defined %}\n {% endif %}\n{% endmacro %}\n{##}\n{##}\n{# Template #}\n{{- "<|startoftext|>" -}}\n{% set _ = is_param_set(system_message) %}\n{% set is_system_message_set = ns.is_last_checked_defined %}\n{% set _ = is_param_set(tools, is_list=True) %}\n{% set is_tools_set = ns.is_last_checked_defined %}\n{% set has_system_message = (is_system_message_set or is_tools_set) %}\n{% if has_system_message %}\n {{- handle_first_system_message(system_message, tools) -}}\n{% endif %}\n{% set last_user_index = get_last_user_index(loop_messages)|int %}\n{% for message in loop_messages %}\n {% if loop.index0 == last_user_index %}\n {% set _ = is_param_set(documents, is_list=True) %}\n {% set use_documents = ns.is_last_checked_defined %}\n {% set _ = is_param_set(knobs) %}\n {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %}\n {% set add_last_system_message = use_documents or use_knobs %}\n {% if add_last_system_message %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}}\n {% endif %}\n {% endif %}\n {% set role = message.role %}\n {% set _ = is_param_set(message, field="name") %}\n {% set is_message_name_set = ns.is_last_checked_defined %}\n {% if is_message_name_set %}\n {% set message_prefix = handle_role(role) + "(" + message.name + ")" %}\n {% else %}\n {% set message_prefix = handle_role(role) %}\n {% endif %}\n {% set content = (message.content or "") %}\n {% if content is not string %}\n {% set content = content|tojson %}\n {% endif %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + message_prefix + content -}}\n {% set _ = is_param_set(message, field="tool_calls", is_list=True) %}\n {% set is_tool_calls_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_tool_calls_set %}\n {{- handle_tool_calls(message.tool_calls) -}}\n {% endif %}\n {% set _ = is_param_set(message, field="citations", is_list=True) %}\n {% set is_citations_set = ns.is_last_checked_defined %}\n {% if role == "assistant" and is_citations_set %}\n {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% endfor %}\n{% if add_generation_prompt %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n {{- bom_str + handle_role(role_to_predict, add_space=False) -}}\n {% set _ = is_param_set(generation_preamble) %}\n {% set is_generation_preamble_set = ns.is_last_checked_defined %}\n {% if is_generation_preamble_set and generation_preamble.strip() != "" %}\n {{- " " + generation_preamble -}}\n {% endif %}\n {% set ns.message_count = ns.message_count + 1 %}\n{% else %}\n {% if ns.message_count > 0 %}\n {{- eom_str -}}\n {% endif %}\n{% endif %}\n', - "qwen_25": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n", - "qwen3": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].role == 'system' %}\n {{- messages[0].content + '\\n\\n' }}\n {%- endif %}\n {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}\n {%- set index = (messages|length - 1) - loop.index0 %}\n {%- if ns.multi_step_tool and message.role == \"user\" and not(message.content.startswith('') and message.content.endswith('')) %}\n {%- set ns.multi_step_tool = false %}\n {%- set ns.last_query_index = index %}\n {%- endif %}\n{%- endfor %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set content = message.content %}\n {%- set reasoning_content = '' %}\n {%- if message.reasoning_content is defined and message.reasoning_content is not none %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if '' in message.content %}\n {%- set content = message.content.split('')[-1].lstrip('\\n') %}\n {%- set reasoning_content = message.content.split('')[0].rstrip('\\n').split('')[-1].lstrip('\\n') %}\n {%- endif %}\n {%- endif %}\n {%- if loop.index0 > ns.last_query_index %}\n {%- if loop.last or (not loop.last and reasoning_content) %}\n {{- '<|im_start|>' + message.role + '\\n\\n' + reasoning_content.strip('\\n') + '\\n\\n\\n' + content.lstrip('\\n') }}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- if message.tool_calls %}\n {%- for tool_call in message.tool_calls %}\n {%- if (loop.first and content) or (not loop.first) %}\n {{- '\\n' }}\n {%- endif %}\n {%- if tool_call.function %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {%- if tool_call.arguments is string %}\n {{- tool_call.arguments }}\n {%- else %}\n {{- tool_call.arguments | tojson }}\n {%- endif %}\n {{- '}\\n' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n {%- if enable_thinking is defined and enable_thinking is false %}\n {{- '\\n\\n\\n\\n' }}\n {%- else %}\n {{- '\\n\\n' }}\n {%- endif %}\n{%- endif %}", - "exaone": "{% for message in messages %}{% if loop.first and message['role'] != 'system' %}{{ '[|system|][|endofturn|]\n' }}{% endif %}{{ '[|' + message['role'] + '|]' + message['content'] }}{% if message['role'] == 'user' %}{{ '\n' }}{% else %}{{ '[|endofturn|]\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '[|assistant|]' }}{% endif %}", - "metharme": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% else %}{% set loop_messages = messages %}{% set system_message = 'Enter RP mode. You shall reply to the user while staying in character. Your responses must be detailed, creative, immersive, and drive the scenario forward.' %}{% endif %}{{ '<|system|>' + system_message }}{% for message in loop_messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|user|>' + content.strip() }}{% elif message['role'] == 'assistant' %}{{ '<|model|>' + content.strip() }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|model|>' }}{% else %}{{ eos_token }}{% endif %}", - "pixtral": '{%- if messages[0]["role"] == "system" %}\n {%- set system_message = messages[0]["content"] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set loop_messages = messages %}\n{%- endif %}\n\n{{- bos_token }}\n{%- for message in loop_messages %}\n {%- if (message[\'role\'] == \'user\') != (loop.index0 % 2 == 0) %}\n {{- raise_exception(\'After the optional system message, conversation roles must alternate user/assistant/user/assistant/...\') }}\n {%- endif %}\n {%- if message["role"] == "user" %}\n {%- if loop.last and system_message is defined %}\n {{- "[INST]" + system_message + "\n\n" }}\n {%- else %}\n {{- "[INST]" }}\n {%- endif %}\n {%- if message["content"] is not string %}\n {%- for chunk in message["content"] %}\n {%- if chunk["type"] == "text" %}\n {{- chunk["text"] }}\n {%- elif chunk["type"] == "image" %}\n {{- "[IMG]" }}\n {%- else %}\n {{- raise_exception("Unrecognized content type!") }}\n {%- endif %}\n {%- endfor %}\n {%- else %}\n {{- message["content"] }}\n {%- endif %}\n {{- "[/INST]" }}\n {%- elif message["role"] == "assistant" %}\n {%- if message["content"] is not string %}\n {%- for chunk in message["content"] %}\n {%- if chunk["type"] == "text" %}\n {{- chunk["text"] }}\n {%- elif chunk["type"] == "image" %}\n {{- "[IMG]" }}\n {%- else %}\n {{- raise_exception("Unrecognized content type!") }}\n{%- endif %}\n{%- endfor %}\n{{- eos_token }}\n{%- else %}\n{{- message["content"] + eos_token }}\n{%- endif %}\n {%- else %}\n {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }}\n {%- endif %}\n{%- endfor %}', - "qwen2_vl": "{% set image_count = namespace(value=0) %}{% set video_count = namespace(value=0) %}{% for message in messages %}{% if loop.first and message['role'] != 'system' %}<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n{% endif %}<|im_start|>{{ message['role'] }}\n{% if message['content'] is string %}{{ message['content'] }}<|im_end|>\n{% else %}{% for content in message['content'] %}{% if content['type'] == 'image' or 'image' in content or 'image_url' in content %}{% set image_count.value = image_count.value + 1 %}{% if add_vision_id %}Picture {{ image_count.value }}: {% endif %}<|vision_start|><|image_pad|><|vision_end|>{% elif content['type'] == 'video' or 'video' in content %}{% set video_count.value = video_count.value + 1 %}{% if add_vision_id %}Video {{ video_count.value }}: {% endif %}<|vision_start|><|video_pad|><|vision_end|>{% elif 'text' in content %}{{ content['text'] }}{% endif %}{% endfor %}<|im_end|>\n{% endif %}{% endfor %}{% if add_generation_prompt %}<|im_start|>assistant\n{% endif %}", - "command_a": '{{ bos_token }}{% if documents %}\n{% set tools = [] %}\n{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {"tool_call_id": "0", "tool_name": "direct-injected-document", "parameters": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n "tool_call_id": "0",\n "results": {\n{% for doc in documents %}\n "{{ loop.index0 }}": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n "is_error": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n "tool_call_id": "{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}",\n "results": {\n "0": {{ tool_msg.content|tojson }}\n },\n "is_error": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0][\'role\']|lower == \'system\' %}{%- set developer_preamble = messages[0][\'content\'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user\'s requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user\'s request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it\'ll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you\'ve figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it\'s time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user\'s last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that "Reflection" and "Response" above can be grounded.\nGrounding means you associate pieces of texts (called "spans") with those specific tool results that support them (called "sources"). And you use a pair of tags "" and "" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as "{tool_call_id}:[{list of result indices}]", before they are joined together by ",". E.g., "span" means that "span" is supported by result 1 and 2 from "tool_call_id=0" as well as result 0 from "tool_call_id=1".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {"name": "direct-injected-document", "description": "This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!", "parameters": {"type": "object", "properties": {}, "required": []}, "responses": {"200": {"description": "Successfully returned a list of chunked text snippets from the directly uploaded documents.", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "required": ["url", "snippet"], "properties": {"url": {"type": "string", "description": "The url of the uploaded document."}, "snippet": {"type": "string", "description": "The text snippet for the returned document chunk."}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {"name": "{{ tool[\'function\'][\'name\'] }}", "description": "{{tool[\'function\'][\'description\']}}", "parameters": {{ tool[\'function\'][\'parameters\']|tojson }}, "responses": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == \'system\' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == \'user\' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == \'assistant\' or message.role|lower == \'chatbot\' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {"tool_call_id": "{{ tool_idx.value }}", "tool_name": "{{ tc[\'function\'][\'name\'] }}", "parameters": {{ tc[\'function\'][\'arguments\']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == \'tool\' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == \'tool\' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>\n{%- else -%}\n{%- if messages and messages[0][\'role\']|lower == \'system\' %}{%- set developer_preamble = messages[0][\'content\'] %}{% endif %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\n{% if safety_mode|upper == \'STRICT\' -%}\nYou are in strict safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will reject requests to generate content related to violence, hate, misinformation or sex to any amount. You will avoid using profanity. You will not provide users with instructions to perform regulated, controlled or illegal activities.\n{%- else -%}\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n{%- endif %}\n\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == \'system\' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == \'user\' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == \'assistant\' or message.role|lower == \'chatbot\' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{%- if add_generation_prompt -%}<|START_RESPONSE|>{%- endif %}\n{% endif %}', - "command_a_tool_use": '{{ bos_token }}{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {"tool_call_id": "0", "tool_name": "direct-injected-document", "parameters": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n "tool_call_id": "0",\n "results": {\n{% for doc in documents %}\n "{{ loop.index0 }}": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n "is_error": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n "tool_call_id": "{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}",\n "results": {\n "0": {{ tool_msg.content|tojson }}\n },\n "is_error": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0][\'role\']|lower == \'system\' %}{%- set developer_preamble = messages[0][\'content\'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user\'s requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user\'s request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it\'ll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you\'ve figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it\'s time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user\'s last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that "Reflection" and "Response" above can be grounded.\nGrounding means you associate pieces of texts (called "spans") with those specific tool results that support them (called "sources"). And you use a pair of tags "" and "" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as "{tool_call_id}:[{list of result indices}]", before they are joined together by ",". E.g., "span" means that "span" is supported by result 1 and 2 from "tool_call_id=0" as well as result 0 from "tool_call_id=1".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {"name": "direct-injected-document", "description": "This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!", "parameters": {"type": "object", "properties": {}, "required": []}, "responses": {"200": {"description": "Successfully returned a list of chunked text snippets from the directly uploaded documents.", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "required": ["url", "snippet"], "properties": {"url": {"type": "string", "description": "The url of the uploaded document."}, "snippet": {"type": "string", "description": "The text snippet for the returned document chunk."}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {"name": "{{ tool[\'function\'][\'name\'] }}", "description": "{{tool[\'function\'][\'description\']}}", "parameters": {{ tool[\'function\'][\'parameters\']|tojson }}, "responses": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == \'system\' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == \'user\' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == \'assistant\' or message.role|lower == \'chatbot\' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {"tool_call_id": "{{ tool_idx.value }}", "tool_name": "{{ tc[\'function\'][\'name\'] }}", "parameters": {{ tc[\'function\'][\'arguments\']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == \'tool\' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == \'tool\' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>', - "command_a_rag": '{{ bos_token }}{% set tools = [] %}\n{%- macro document_turn(documents) -%}\n{# format documents into chat turn #}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[\n {"tool_call_id": "0", "tool_name": "direct-injected-document", "parameters": {}}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n {\n "tool_call_id": "0",\n "results": {\n{% for doc in documents %}\n "{{ loop.index0 }}": {{doc|tojson}}{% if not loop.last %},\n {% endif %}\n{% endfor %}\n\n },\n "is_error": null\n }\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %}\n{%- macro tool_call_id_to_int(messages, tool_call_id) %}\n{%- set counter = namespace(value=0) %}\n{%- set tool_call_id_seen = namespace(value=false) %}\n{%- for msg in messages %}\n {%- if msg.tool_calls %}\n {%- for tool_call in msg.tool_calls %}\n {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%}\n {{ counter.value }}\n {%- set tool_call_id_seen.value = true %}\n {%- endif %}\n {%- set counter.value = counter.value + 1 %}\n {%- endfor %}\n {%- endif %}\n{%- endfor %}\n{%- endmacro %}\n{%- macro format_tool_message(messages, tool_msg) -%}\n{# format tool message #}\n {\n "tool_call_id": "{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}",\n "results": {\n "0": {{ tool_msg.content|tojson }}\n },\n "is_error": null\n }\n{%- endmacro -%}\n{%- if messages and messages[0][\'role\']|lower == \'system\' %}{%- set developer_preamble = messages[0][\'content\'] %}{% endif %}\n{%- set tool_idx = namespace(value=0) %}\n{%- set tool_ids_seen = namespace(value=[]) %}\n{%- set sent_documents = namespace(value=false) %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble\nYou are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes.\n\nYour information cutoff date is June 2024.\n\nYou have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages.\n{% if tools or documents %}\n\nYou have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user\'s requests.\n\n## Tool Use\nThink about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first.\n\n0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed.\n NOTE: You MUST skip this step when you are directly responding to the user\'s request without using any tools.\n\nThen carry out your plan by repeatedly executing the following steps.\n1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields.\n When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>.\n2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results.\n Every tool call produces a list of results (when a tool call produces no result or a single result, it\'ll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id".\n3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you\'ve figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>.\n You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded.\n NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user.\n\nYou can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it\'s time to finally respond to the user.\n\n4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user\'s last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>.\n{% if enable_citations %}\n\n## Grounding\nImportantly, note that "Reflection" and "Response" above can be grounded.\nGrounding means you associate pieces of texts (called "spans") with those specific tool results that support them (called "sources"). And you use a pair of tags "" and "" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as "{tool_call_id}:[{list of result indices}]", before they are joined together by ",". E.g., "span" means that "span" is supported by result 1 and 2 from "tool_call_id=0" as well as result 0 from "tool_call_id=1".\n{% endif %}\n\n## Available Tools\nHere is the list of tools that you have available to you.\nYou can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it.\nEach tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema).\n\n```json\n[\n{% if documents %}\n {"name": "direct-injected-document", "description": "This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!", "parameters": {"type": "object", "properties": {}, "required": []}, "responses": {"200": {"description": "Successfully returned a list of chunked text snippets from the directly uploaded documents.", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "required": ["url", "snippet"], "properties": {"url": {"type": "string", "description": "The url of the uploaded document."}, "snippet": {"type": "string", "description": "The text snippet for the returned document chunk."}}}}}}}}}{%- if tools %},{% endif %}\n\n{% endif %}\n{% for tool in tools %}\n {"name": "{{ tool[\'function\'][\'name\'] }}", "description": "{{tool[\'function\'][\'description\']}}", "parameters": {{ tool[\'function\'][\'parameters\']|tojson }}, "responses": null}{%- if not loop.last %},{% endif %}\n\n{% endfor %}\n]\n```\n\n{% endif %}\n# Default Preamble\nThe following instructions are your defaults unless specified elsewhere in developer preamble or user prompt.\n- Your name is Command.\n- You are a large language model built by Cohere.\n- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions.\n- If the input is ambiguous, ask clarifying follow-up questions.\n- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks).\n- Use LaTeX to generate mathematical notation for complex equations.\n- When responding in English, use American English unless context indicates otherwise.\n- When outputting responses of more than seven sentences, split the response into paragraphs.\n- Prefer the active voice.\n- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references.\n- Use gender-neutral pronouns for unspecified persons.\n- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list.\n- Use the third person when asked to write a summary.\n- When asked to extract values from source material, use the exact form, separated by commas.\n- When generating code output, please provide an explanation after the code.\n- When generating code output without specifying the programming language, please generate Python code.\n- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.\n{%- if developer_preamble %}\n\n\n# Developer Preamble\nThe following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions.\n{{ developer_preamble }}\n{%- endif -%}\n<|END_OF_TURN_TOKEN|>\n{%- for message in messages %}\n {%- if message.role|lower == \'system\' and not (loop.first and developer_preamble)%}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>\n {%- elif message.role|lower == \'user\' %}\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %}\n {%- elif message.role|lower == \'assistant\' or message.role|lower == \'chatbot\' %}\n<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[\n {% for tc in message.tool_calls %}\n {"tool_call_id": "{{ tool_idx.value }}", "tool_name": "{{ tc[\'function\'][\'name\'] }}", "parameters": {{ tc[\'function\'][\'arguments\']|tojson }}}{% if not loop.last %},{% endif %}\n\n {% set tool_idx.value = tool_idx.value + 1 %}\n {% endfor %}\n]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %}\n {% elif message.role|lower == \'tool\' and message.tool_call_id not in tool_ids_seen.value %}\n<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[\n{{ format_tool_message(messages, message) }}\n {%- set stopped = namespace(value=false) %}\n {%- for msg in messages[loop.index0 + 1:] %}\n {%- if not stopped.value and msg.role|lower == \'tool\' %},\n{{ format_tool_message(messages, msg) }}\n {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %}\n {%- else %}\n {%- set stopped.value = true %}\n {%- endif %}\n {%- endfor %}\n\n]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>\n {%- endif %}\n{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>', - "aya": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Aya, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %}", - "falcon_h1": """'{{bos_token}}\n{%- if tools %}\n {{- \'<|im_start|>system\\n\' }}\n {%- if messages[0].role == \'system\' %}\n {{- messages[0].content + \'\\n\\n\' }}\n {%- endif %}\n {{- "You are a function calling AI model. You are provided with function signature within XML tags. You may call one or more functions to assist with the user query. Don\'t make assumptions about what values to plug into functions.\\n\\n" }}\n {%- for tool in tools %}[{{- tool | tojson }}]{%- endfor %}\n {{- "\\n\\nFor each function call, return a json object with function name and arguments within tags with the following schema:\\n\\n{\'arguments\': , \'name\': }\\n\\n" }}\n{%- else %}\n {%- if messages[0].role == \'system\' %}\n {{- \'<|im_start|>system\\n\' + messages[0].content + \'<|im_end|>\\n\' }}\n {%- endif %}\n{%- endif %}{% for message in messages %}{%- if message.role != \'system\' %}{{\'<|im_start|>\' + message[\'role\'] + \'\n\' + message[\'content\'] + \'<|im_end|>\' + \'\n\'}}{%- endif %}{% endfor %}{% if add_generation_prompt %}{{ \'<|im_start|>assistant\n\' }}{% endif %}'""", -} - - -def get_chat_template( - user_choice: str, - jinja_template: Optional[str] = None, - tokenizer: Optional["PreTrainedTokenizerBase"] = None, -) -> str: - """ - Finds the correct chat_template based on the user's choice, jinja_template, and tokenizer. - - Args: - user_choice (str): The user's choice of template. - jinja_template (Optional[str], optional): The jinja template string. Defaults to None. - tokenizer (Optional[PreTrainedTokenizerBase], optional): The tokenizer. Defaults to None. - - Returns: - str: The chosen template string. - - Raises: - ValueError: If the user_choice is not found in the templates. - """ - if user_choice == _JINJA_TEMPALTE_CHOICE: - if not jinja_template: - raise ValueError( - f"`jinja_template` cannot be None when `chat_template` choice is {_JINJA_TEMPALTE_CHOICE}" - ) - return jinja_template - - if user_choice == _DEFAULT_TEMPLATE_CHOICE: - if not tokenizer: - raise ValueError( - f"`tokenizer` cannot be None when chat_template choice is {_DEFAULT_TEMPLATE_CHOICE}" - ) - if not tokenizer.chat_template: - raise ValueError( - f"`chat_template choice is {_DEFAULT_TEMPLATE_CHOICE} but tokenizer's chat_template is null. " - f"Please add a chat_template in tokenizer config" - ) - return tokenizer.chat_template # type: ignore - - if user_choice.startswith(_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX): - if not tokenizer: - raise ValueError( - f"`tokenizer` cannot be None when chat_template choice starts with {_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX}" - ) - if tokenizer.chat_template: - return tokenizer.chat_template # type: ignore - - user_choice = user_choice[ - len(_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX) : - ] - LOG.warning( - f"No chat template found on tokenizer, falling back to {user_choice}. It is recommended to set --train_on_inputs to True for the model to learn this chat template." - ) - - if user_choice in _CHAT_TEMPLATES: - return _CHAT_TEMPLATES[user_choice] - - raise ValueError(f"Template '{user_choice}' not found.") - - -def extract_chat_template_args(cfg, ds_cfg: Optional[Dict[str, Any]] = None): - if ds_cfg and ds_cfg.get("chat_template"): - chat_template_choice = ds_cfg.get("chat_template") or _DEFAULT_TEMPLATE_CHOICE - chat_template_jinja = ds_cfg.get("chat_template_jinja") - else: - chat_template_choice = cfg.get("chat_template") or _DEFAULT_TEMPLATE_CHOICE - chat_template_jinja = cfg.get("chat_template_jinja") - return chat_template_choice, chat_template_jinja - - -def get_chat_template_from_config( - cfg, - ds_cfg: Optional[Dict[str, Any]] = None, - tokenizer: Optional["PreTrainedTokenizerBase"] = None, -) -> str: - chat_template_choice, chat_template_jinja = extract_chat_template_args( - cfg=cfg, ds_cfg=ds_cfg - ) - return get_chat_template( - user_choice=chat_template_choice, - jinja_template=chat_template_jinja, - tokenizer=tokenizer, - ) - - -def register_chat_template(template_name: str, chat_template: str): - """ - Registers chat templates. - - Args: - template_name (str): The name of the template. - chat_template (str): The template string. - """ - - if template_name in _CHAT_TEMPLATES: - raise ValueError(f"Template '{template_name}' already exists.") - - _CHAT_TEMPLATES[template_name] = chat_template diff --git a/src/axolotl/utils/chat_templates/__init__.py b/src/axolotl/utils/chat_templates/__init__.py new file mode 100644 index 0000000000..337417c7dc --- /dev/null +++ b/src/axolotl/utils/chat_templates/__init__.py @@ -0,0 +1,20 @@ +""" +This module provides functionality for selecting chat templates based on user choices. +These templates are used for formatting messages in a conversation. +""" + +from .base import ( + _CHAT_TEMPLATES, + extract_chat_template_args, + get_chat_template, + get_chat_template_from_config, + register_chat_template, +) + +__all__ = [ + "get_chat_template", + "extract_chat_template_args", + "get_chat_template_from_config", + "register_chat_template", + "_CHAT_TEMPLATES", +] diff --git a/src/axolotl/utils/chat_templates/base.py b/src/axolotl/utils/chat_templates/base.py new file mode 100644 index 0000000000..11d15fc1da --- /dev/null +++ b/src/axolotl/utils/chat_templates/base.py @@ -0,0 +1,125 @@ +""" +utility functions for chat templates +""" + +import os +from typing import TYPE_CHECKING, Any, Dict, Optional + +from axolotl.utils.logging import get_logger + +if TYPE_CHECKING: + from transformers import PreTrainedTokenizerBase + +LOG = get_logger("axolotl.utils.chat_templates") + +_JINJA_TEMPLATE_CHOICE = "jinja" +_DEFAULT_TEMPLATE_CHOICE = "tokenizer_default" +_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX = "tokenizer_default_fallback_" + +TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates") +_CHAT_TEMPLATES: dict[str, str] = {} +for filename in [f for f in os.listdir(TEMPLATE_DIR) if f.endswith(".jinja")]: + with open(os.path.join(TEMPLATE_DIR, filename), "r", encoding="utf-8") as f: + _CHAT_TEMPLATES[filename[:-6]] = f.read() + + +def get_chat_template( + user_choice: str, + jinja_template: str | None = None, + tokenizer: Optional["PreTrainedTokenizerBase"] = None, +) -> str: + """ + Finds the correct chat_template based on the user's choice, jinja_template, and tokenizer. + + Args: + user_choice (str): The user's choice of template. + jinja_template (str, optional): The jinja template string or Path to a valid jinja template file. Defaults to None. + tokenizer (PreTrainedTokenizerBase, optional): The tokenizer. Defaults to None. + + Returns: + str: The chosen template string. + + Raises: + ValueError: If the user_choice is not found in the templates. + """ + if user_choice == _JINJA_TEMPLATE_CHOICE: + if not jinja_template: + raise ValueError( + f"`jinja_template` cannot be None when `chat_template` choice is {_JINJA_TEMPLATE_CHOICE}" + ) + if os.path.exists(jinja_template) and os.path.isfile(jinja_template): + with open(jinja_template, "r", encoding="utf-8") as file: + jinja_template = file.read() + return jinja_template + + if user_choice == _DEFAULT_TEMPLATE_CHOICE: + if not tokenizer: + raise ValueError( + f"`tokenizer` cannot be None when chat_template choice is {_DEFAULT_TEMPLATE_CHOICE}" + ) + if not tokenizer.chat_template: + raise ValueError( + f"`chat_template choice is {_DEFAULT_TEMPLATE_CHOICE} but tokenizer's chat_template is null. " + f"Please add a chat_template in tokenizer config" + ) + return tokenizer.chat_template # type: ignore + + if user_choice.startswith(_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX): + if not tokenizer: + raise ValueError( + f"`tokenizer` cannot be None when chat_template choice starts with {_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX}" + ) + if tokenizer.chat_template: + return tokenizer.chat_template # type: ignore + + user_choice = user_choice[ + len(_DEFAULT_FALLBACK_CHATML_TEMPLATE_CHOICE_PREFIX) : + ] + LOG.warning( + f"No chat template found on tokenizer, falling back to {user_choice}. It is recommended to set --train_on_inputs to True for the model to learn this chat template." + ) + + if user_choice in _CHAT_TEMPLATES: + return _CHAT_TEMPLATES[user_choice] + + raise ValueError(f"Template '{user_choice}' not found.") + + +def extract_chat_template_args(cfg, ds_cfg: Dict[str, Any] | None = None): + if ds_cfg and ds_cfg.get("chat_template"): + chat_template_choice = ds_cfg.get("chat_template") or _DEFAULT_TEMPLATE_CHOICE + chat_template_jinja = ds_cfg.get("chat_template_jinja") + else: + chat_template_choice = cfg.get("chat_template") or _DEFAULT_TEMPLATE_CHOICE + chat_template_jinja = cfg.get("chat_template_jinja") + return chat_template_choice, chat_template_jinja + + +def get_chat_template_from_config( + cfg, + ds_cfg: Dict[str, Any] | None = None, + tokenizer: Optional["PreTrainedTokenizerBase"] = None, +) -> str: + chat_template_choice, chat_template_jinja = extract_chat_template_args( + cfg=cfg, ds_cfg=ds_cfg + ) + return get_chat_template( + user_choice=chat_template_choice, + jinja_template=chat_template_jinja, + tokenizer=tokenizer, + ) + + +def register_chat_template(template_name: str, chat_template: str): + """ + Registers chat templates. + + Args: + template_name (str): The name of the template. + chat_template (str): The template string. + """ + + if template_name in _CHAT_TEMPLATES: + raise ValueError(f"Template '{template_name}' already exists.") + + _CHAT_TEMPLATES[template_name] = chat_template diff --git a/src/axolotl/utils/chat_templates/templates/alpaca.jinja b/src/axolotl/utils/chat_templates/templates/alpaca.jinja new file mode 100644 index 0000000000..5e9d63c424 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/alpaca.jinja @@ -0,0 +1,8 @@ +{{ bos_token }}{% for message in messages %}{% if message['role'] == 'system' and loop.first %}{{ message['content'] }}{% elif message['role'] == 'user' %}{{ '### Instruction: +' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '### Response: +' + message['content'] + eos_token }}{% endif %}{% if not loop.last %}{{ ' + +' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ ' + +### Response: +' }}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/aya.jinja b/src/axolotl/utils/chat_templates/templates/aya.jinja new file mode 100644 index 0000000000..97e54d4b10 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/aya.jinja @@ -0,0 +1 @@ +{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Aya, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/chatml.jinja b/src/axolotl/utils/chat_templates/templates/chatml.jinja new file mode 100644 index 0000000000..2116e45cab --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/chatml.jinja @@ -0,0 +1,4 @@ +{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + ' +' + message['content'] + '<|im_end|>' + ' +'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant +' }}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/cohere.jinja b/src/axolotl/utils/chat_templates/templates/cohere.jinja new file mode 100644 index 0000000000..638ce5ef2f --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/cohere.jinja @@ -0,0 +1 @@ +{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true %}{% set loop_messages = messages %}{% set system_message = 'You are Command-R, a brilliant, sophisticated, AI-assistant trained to assist human users by providing thorough responses. You are trained by Cohere.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% if system_message != false %}{{ '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>' + system_message + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% elif message['role'] == 'assistant' %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + content.strip() + '<|END_OF_TURN_TOKEN|>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' }}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/command_a.jinja b/src/axolotl/utils/chat_templates/templates/command_a.jinja new file mode 100644 index 0000000000..ef0594172d --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/command_a.jinja @@ -0,0 +1,210 @@ +{{ bos_token }}{% if documents %} +{% set tools = [] %} +{%- macro document_turn(documents) -%} +{# format documents into chat turn #} +<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[ + {"tool_call_id": "0", "tool_name": "direct-injected-document", "parameters": {}} +]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[ + { + "tool_call_id": "0", + "results": { +{% for doc in documents %} + "{{ loop.index0 }}": {{doc|tojson}}{% if not loop.last %}, + {% endif %} +{% endfor %} + + }, + "is_error": null + } +]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %} +{%- macro tool_call_id_to_int(messages, tool_call_id) %} +{%- set counter = namespace(value=0) %} +{%- set tool_call_id_seen = namespace(value=false) %} +{%- for msg in messages %} + {%- if msg.tool_calls %} + {%- for tool_call in msg.tool_calls %} + {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%} + {{ counter.value }} + {%- set tool_call_id_seen.value = true %} + {%- endif %} + {%- set counter.value = counter.value + 1 %} + {%- endfor %} + {%- endif %} +{%- endfor %} +{%- endmacro %} +{%- macro format_tool_message(messages, tool_msg) -%} +{# format tool message #} + { + "tool_call_id": "{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}", + "results": { + "0": {{ tool_msg.content|tojson }} + }, + "is_error": null + } +{%- endmacro -%} +{%- if messages and messages[0]['role']|lower == 'system' %}{%- set developer_preamble = messages[0]['content'] %}{% endif %} +{%- set tool_idx = namespace(value=0) %} +{%- set tool_ids_seen = namespace(value=[]) %} +{%- set sent_documents = namespace(value=false) %} +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. +{% if tools or documents %} + +You have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests. + +## Tool Use +Think about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first. + +0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed. + NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools. + +Then carry out your plan by repeatedly executing the following steps. +1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields. + When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>. +2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results. + Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id". +3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded. + NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user. + +You can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user. + +4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>. +{% if enable_citations %} + +## Grounding +Importantly, note that "Reflection" and "Response" above can be grounded. +Grounding means you associate pieces of texts (called "spans") with those specific tool results that support them (called "sources"). And you use a pair of tags "" and "" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as "{tool_call_id}:[{list of result indices}]", before they are joined together by ",". E.g., "span" means that "span" is supported by result 1 and 2 from "tool_call_id=0" as well as result 0 from "tool_call_id=1". +{% endif %} + +## Available Tools +Here is the list of tools that you have available to you. +You can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it. +Each tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema). + +```json +[ +{% if documents %} + {"name": "direct-injected-document", "description": "This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!", "parameters": {"type": "object", "properties": {}, "required": []}, "responses": {"200": {"description": "Successfully returned a list of chunked text snippets from the directly uploaded documents.", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "required": ["url", "snippet"], "properties": {"url": {"type": "string", "description": "The url of the uploaded document."}, "snippet": {"type": "string", "description": "The text snippet for the returned document chunk."}}}}}}}}}{%- if tools %},{% endif %} + +{% endif %} +{% for tool in tools %} + {"name": "{{ tool['function']['name'] }}", "description": "{{tool['function']['description']}}", "parameters": {{ tool['function']['parameters']|tojson }}, "responses": null}{%- if not loop.last %},{% endif %} + +{% endfor %} +] +``` + +{% endif %} +# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer. +{%- if developer_preamble %} + + +# Developer Preamble +The following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions. +{{ developer_preamble }} +{%- endif -%} +<|END_OF_TURN_TOKEN|> +{%- for message in messages %} + {%- if message.role|lower == 'system' and not (loop.first and developer_preamble)%} +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|> + {%- elif message.role|lower == 'user' %} +<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %} + {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %} +<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[ + {% for tc in message.tool_calls %} + {"tool_call_id": "{{ tool_idx.value }}", "tool_name": "{{ tc['function']['name'] }}", "parameters": {{ tc['function']['arguments']|tojson }}}{% if not loop.last %},{% endif %} + + {% set tool_idx.value = tool_idx.value + 1 %} + {% endfor %} +]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %} + {% elif message.role|lower == 'tool' and message.tool_call_id not in tool_ids_seen.value %} +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[ +{{ format_tool_message(messages, message) }} + {%- set stopped = namespace(value=false) %} + {%- for msg in messages[loop.index0 + 1:] %} + {%- if not stopped.value and msg.role|lower == 'tool' %}, +{{ format_tool_message(messages, msg) }} + {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %} + {%- else %} + {%- set stopped.value = true %} + {%- endif %} + {%- endfor %} + +]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|> + {%- endif %} +{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|> +{%- else -%} +{%- if messages and messages[0]['role']|lower == 'system' %}{%- set developer_preamble = messages[0]['content'] %}{% endif %} +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +{% if safety_mode|upper == 'STRICT' -%} +You are in strict safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will reject requests to generate content related to violence, hate, misinformation or sex to any amount. You will avoid using profanity. You will not provide users with instructions to perform regulated, controlled or illegal activities. +{%- else -%} +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. +{%- endif %} + + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. + +# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer. +{%- if developer_preamble %} + + +# Developer Preamble +The following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions. +{{ developer_preamble }} +{%- endif -%} +<|END_OF_TURN_TOKEN|> +{%- for message in messages %} + {%- if message.role|lower == 'system' and not (loop.first and developer_preamble)%} +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|> + {%- elif message.role|lower == 'user' %} +<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|> + {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %} +<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|> + {%- endif %} +{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{%- if add_generation_prompt -%}<|START_RESPONSE|>{%- endif %} +{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/command_a_rag.jinja b/src/axolotl/utils/chat_templates/templates/command_a_rag.jinja new file mode 100644 index 0000000000..e4a5fd9aca --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/command_a_rag.jinja @@ -0,0 +1,158 @@ +{{ bos_token }}{% set tools = [] %} +{%- macro document_turn(documents) -%} +{# format documents into chat turn #} +<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[ + {"tool_call_id": "0", "tool_name": "direct-injected-document", "parameters": {}} +]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[ + { + "tool_call_id": "0", + "results": { +{% for doc in documents %} + "{{ loop.index0 }}": {{doc|tojson}}{% if not loop.last %}, + {% endif %} +{% endfor %} + + }, + "is_error": null + } +]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %} +{%- macro tool_call_id_to_int(messages, tool_call_id) %} +{%- set counter = namespace(value=0) %} +{%- set tool_call_id_seen = namespace(value=false) %} +{%- for msg in messages %} + {%- if msg.tool_calls %} + {%- for tool_call in msg.tool_calls %} + {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%} + {{ counter.value }} + {%- set tool_call_id_seen.value = true %} + {%- endif %} + {%- set counter.value = counter.value + 1 %} + {%- endfor %} + {%- endif %} +{%- endfor %} +{%- endmacro %} +{%- macro format_tool_message(messages, tool_msg) -%} +{# format tool message #} + { + "tool_call_id": "{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}", + "results": { + "0": {{ tool_msg.content|tojson }} + }, + "is_error": null + } +{%- endmacro -%} +{%- if messages and messages[0]['role']|lower == 'system' %}{%- set developer_preamble = messages[0]['content'] %}{% endif %} +{%- set tool_idx = namespace(value=0) %} +{%- set tool_ids_seen = namespace(value=[]) %} +{%- set sent_documents = namespace(value=false) %} +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. +{% if tools or documents %} + +You have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests. + +## Tool Use +Think about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first. + +0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed. + NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools. + +Then carry out your plan by repeatedly executing the following steps. +1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields. + When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>. +2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results. + Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id". +3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded. + NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user. + +You can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user. + +4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>. +{% if enable_citations %} + +## Grounding +Importantly, note that "Reflection" and "Response" above can be grounded. +Grounding means you associate pieces of texts (called "spans") with those specific tool results that support them (called "sources"). And you use a pair of tags "" and "" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as "{tool_call_id}:[{list of result indices}]", before they are joined together by ",". E.g., "span" means that "span" is supported by result 1 and 2 from "tool_call_id=0" as well as result 0 from "tool_call_id=1". +{% endif %} + +## Available Tools +Here is the list of tools that you have available to you. +You can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it. +Each tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema). + +```json +[ +{% if documents %} + {"name": "direct-injected-document", "description": "This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!", "parameters": {"type": "object", "properties": {}, "required": []}, "responses": {"200": {"description": "Successfully returned a list of chunked text snippets from the directly uploaded documents.", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "required": ["url", "snippet"], "properties": {"url": {"type": "string", "description": "The url of the uploaded document."}, "snippet": {"type": "string", "description": "The text snippet for the returned document chunk."}}}}}}}}}{%- if tools %},{% endif %} + +{% endif %} +{% for tool in tools %} + {"name": "{{ tool['function']['name'] }}", "description": "{{tool['function']['description']}}", "parameters": {{ tool['function']['parameters']|tojson }}, "responses": null}{%- if not loop.last %},{% endif %} + +{% endfor %} +] +``` + +{% endif %} +# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer. +{%- if developer_preamble %} + + +# Developer Preamble +The following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions. +{{ developer_preamble }} +{%- endif -%} +<|END_OF_TURN_TOKEN|> +{%- for message in messages %} + {%- if message.role|lower == 'system' and not (loop.first and developer_preamble)%} +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|> + {%- elif message.role|lower == 'user' %} +<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %} + {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %} +<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[ + {% for tc in message.tool_calls %} + {"tool_call_id": "{{ tool_idx.value }}", "tool_name": "{{ tc['function']['name'] }}", "parameters": {{ tc['function']['arguments']|tojson }}}{% if not loop.last %},{% endif %} + + {% set tool_idx.value = tool_idx.value + 1 %} + {% endfor %} +]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %} + {% elif message.role|lower == 'tool' and message.tool_call_id not in tool_ids_seen.value %} +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[ +{{ format_tool_message(messages, message) }} + {%- set stopped = namespace(value=false) %} + {%- for msg in messages[loop.index0 + 1:] %} + {%- if not stopped.value and msg.role|lower == 'tool' %}, +{{ format_tool_message(messages, msg) }} + {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %} + {%- else %} + {%- set stopped.value = true %} + {%- endif %} + {%- endfor %} + +]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|> + {%- endif %} +{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|> diff --git a/src/axolotl/utils/chat_templates/templates/command_a_tool_use.jinja b/src/axolotl/utils/chat_templates/templates/command_a_tool_use.jinja new file mode 100644 index 0000000000..eecd424884 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/command_a_tool_use.jinja @@ -0,0 +1,157 @@ +{{ bos_token }}{%- macro document_turn(documents) -%} +{# format documents into chat turn #} +<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|>I will look through the document to address the users needs.<|END_THINKING|><|START_ACTION|>[ + {"tool_call_id": "0", "tool_name": "direct-injected-document", "parameters": {}} +]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[ + { + "tool_call_id": "0", + "results": { +{% for doc in documents %} + "{{ loop.index0 }}": {{doc|tojson}}{% if not loop.last %}, + {% endif %} +{% endfor %} + + }, + "is_error": null + } +]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|>{%- endmacro %} +{%- macro tool_call_id_to_int(messages, tool_call_id) %} +{%- set counter = namespace(value=0) %} +{%- set tool_call_id_seen = namespace(value=false) %} +{%- for msg in messages %} + {%- if msg.tool_calls %} + {%- for tool_call in msg.tool_calls %} + {%- if tool_call.id == tool_call_id and not tool_call_id_seen.value -%} + {{ counter.value }} + {%- set tool_call_id_seen.value = true %} + {%- endif %} + {%- set counter.value = counter.value + 1 %} + {%- endfor %} + {%- endif %} +{%- endfor %} +{%- endmacro %} +{%- macro format_tool_message(messages, tool_msg) -%} +{# format tool message #} + { + "tool_call_id": "{{ tool_call_id_to_int(messages, tool_msg.tool_call_id) }}", + "results": { + "0": {{ tool_msg.content|tojson }} + }, + "is_error": null + } +{%- endmacro -%} +{%- if messages and messages[0]['role']|lower == 'system' %}{%- set developer_preamble = messages[0]['content'] %}{% endif %} +{%- set tool_idx = namespace(value=0) %} +{%- set tool_ids_seen = namespace(value=[]) %} +{%- set sent_documents = namespace(value=false) %} +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. +{% if tools or documents %} + +You have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests. + +## Tool Use +Think about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first. + +0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed. + NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools. + +Then carry out your plan by repeatedly executing the following steps. +1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields. + When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>. +2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results. + Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id". +3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded. + NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user. + +You can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user. + +4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>. +{% if enable_citations %} + +## Grounding +Importantly, note that "Reflection" and "Response" above can be grounded. +Grounding means you associate pieces of texts (called "spans") with those specific tool results that support them (called "sources"). And you use a pair of tags "" and "" to indicate when a span can be grounded onto a list of sources, listing them out in the closing tag. Sources from the same tool call are grouped together and listed as "{tool_call_id}:[{list of result indices}]", before they are joined together by ",". E.g., "span" means that "span" is supported by result 1 and 2 from "tool_call_id=0" as well as result 0 from "tool_call_id=1". +{% endif %} + +## Available Tools +Here is the list of tools that you have available to you. +You can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it. +Each tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema). + +```json +[ +{% if documents %} + {"name": "direct-injected-document", "description": "This is a special tool to directly inject user-uploaded documents into the chat as additional context. DO NOT use this tool by yourself!", "parameters": {"type": "object", "properties": {}, "required": []}, "responses": {"200": {"description": "Successfully returned a list of chunked text snippets from the directly uploaded documents.", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "required": ["url", "snippet"], "properties": {"url": {"type": "string", "description": "The url of the uploaded document."}, "snippet": {"type": "string", "description": "The text snippet for the returned document chunk."}}}}}}}}}{%- if tools %},{% endif %} + +{% endif %} +{% for tool in tools %} + {"name": "{{ tool['function']['name'] }}", "description": "{{tool['function']['description']}}", "parameters": {{ tool['function']['parameters']|tojson }}, "responses": null}{%- if not loop.last %},{% endif %} + +{% endfor %} +] +``` + +{% endif %} +# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer. +{%- if developer_preamble %} + + +# Developer Preamble +The following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions. +{{ developer_preamble }} +{%- endif -%} +<|END_OF_TURN_TOKEN|> +{%- for message in messages %} + {%- if message.role|lower == 'system' and not (loop.first and developer_preamble)%} +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|> + {%- elif message.role|lower == 'user' %} +<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %} + {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %} +<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[ + {% for tc in message.tool_calls %} + {"tool_call_id": "{{ tool_idx.value }}", "tool_name": "{{ tc['function']['name'] }}", "parameters": {{ tc['function']['arguments']|tojson }}}{% if not loop.last %},{% endif %} + + {% set tool_idx.value = tool_idx.value + 1 %} + {% endfor %} +]<|END_ACTION|><|END_OF_TURN_TOKEN|>{% else %}<|START_RESPONSE|>{{message.content}}<|END_RESPONSE|><|END_OF_TURN_TOKEN|>{% endif %} + {% elif message.role|lower == 'tool' and message.tool_call_id not in tool_ids_seen.value %} +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|START_TOOL_RESULT|>[ +{{ format_tool_message(messages, message) }} + {%- set stopped = namespace(value=false) %} + {%- for msg in messages[loop.index0 + 1:] %} + {%- if not stopped.value and msg.role|lower == 'tool' %}, +{{ format_tool_message(messages, msg) }} + {%- set tool_ids_seen.value = tool_ids_seen.value + [msg.tool_call_id] %} + {%- else %} + {%- set stopped.value = true %} + {%- endif %} + {%- endfor %} + +]<|END_TOOL_RESULT|><|END_OF_TURN_TOKEN|> + {%- endif %} +{%- endfor %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|> diff --git a/src/axolotl/utils/chat_templates/templates/deepseek_v2.jinja b/src/axolotl/utils/chat_templates/templates/deepseek_v2.jinja new file mode 100644 index 0000000000..59fde8f2cb --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/deepseek_v2.jinja @@ -0,0 +1,3 @@ +{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>' + message['content'] }}{% elif message['role'] == 'assistant' %}{{ '<|Assistant|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + ' + +' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Assistant|>' }}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/deepseek_v3.jinja b/src/axolotl/utils/chat_templates/templates/deepseek_v3.jinja new file mode 100644 index 0000000000..35803578c4 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/deepseek_v3.jinja @@ -0,0 +1 @@ +{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='', is_first_sp=true) %}{%- for message in messages %}{%- if message['role'] == 'system' %}{%- if ns.is_first_sp %}{% set ns.system_prompt = ns.system_prompt + message['content'] %}{% set ns.is_first_sp = false %}{%- else %}{% set ns.system_prompt = ns.system_prompt + '\n\n' + message['content'] %}{%- endif %}{%- endif %}{%- endfor %}{{ bos_token }}{{ ns.system_prompt }}{%- for message in messages %}{%- if message['role'] == 'user' %}{%- set ns.is_tool = false -%}{{'<|User|>' + message['content']}}{%- endif %}{%- if message['role'] == 'assistant' and 'tool_calls' in message %}{%- set ns.is_tool = false -%}{%- for tool in message['tool_calls'] %}{%- if not ns.is_first %}{%- if message['content'] is none %}{{'<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\n' + '```json' + '\n' + tool['function']['arguments'] + '\n' + '```' + '<|tool▁call▁end|>'}}{%- else %}{{'<|Assistant|>' + message['content'] + '<|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\n' + '```json' + '\n' + tool['function']['arguments'] + '\n' + '```' + '<|tool▁call▁end|>'}}{%- endif %}{%- set ns.is_first = true -%}{%- else %}{{'\n' + '<|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\n' + '```json' + '\n' + tool['function']['arguments'] + '\n' + '```' + '<|tool▁call▁end|>'}}{%- endif %}{%- endfor %}{{'<|tool▁calls▁end|><|end▁of▁sentence|>'}}{%- endif %}{%- if message['role'] == 'assistant' and 'tool_calls' not in message %}{%- if ns.is_tool %}{{'<|tool▁outputs▁end|>' + message['content'] + '<|end▁of▁sentence|>'}}{%- set ns.is_tool = false -%}{%- else %}{% set content = message['content'] %}{% if '' in content %}{% set content = content.split('')[-1] %}{% endif %}{{'<|Assistant|>' + content + '<|end▁of▁sentence|>'}}{%- endif %}{%- endif %}{%- if message['role'] == 'tool' %}{%- set ns.is_tool = true -%}{%- if ns.is_output_first %}{{'<|tool▁outputs▁begin|><|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- set ns.is_output_first = false %}{%- else %}{{'<|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- endif %}{%- endif %}{%- endfor -%}{% if ns.is_tool %}{{'<|tool▁outputs▁end|>'}}{% endif %}{% if add_generation_prompt and not ns.is_tool %}{{'<|Assistant|>'}}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/exaone.jinja b/src/axolotl/utils/chat_templates/templates/exaone.jinja new file mode 100644 index 0000000000..8783ad2ec2 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/exaone.jinja @@ -0,0 +1,4 @@ +{% for message in messages %}{% if loop.first and message['role'] != 'system' %}{{ '[|system|][|endofturn|] +' }}{% endif %}{{ '[|' + message['role'] + '|]' + message['content'] }}{% if message['role'] == 'user' %}{{ ' +' }}{% else %}{{ '[|endofturn|] +' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '[|assistant|]' }}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/falcon_h1.jinja b/src/axolotl/utils/chat_templates/templates/falcon_h1.jinja new file mode 100644 index 0000000000..4c03c62972 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/falcon_h1.jinja @@ -0,0 +1,17 @@ +'{{bos_token}} +{%- if tools %} + {{- '<|im_start|>system\n' }} + {%- if messages[0].role == 'system' %} + {{- messages[0].content + '\n\n' }} + {%- endif %} + {{- "You are a function calling AI model. You are provided with function signature within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions.\n\n" }} + {%- for tool in tools %}[{{- tool | tojson }}]{%- endfor %} + {{- "\n\nFor each function call, return a json object with function name and arguments within tags with the following schema:\n\n{'arguments': , 'name': }\n\n" }} +{%- else %} + {%- if messages[0].role == 'system' %} + {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }} + {%- endif %} +{%- endif %}{% for message in messages %}{%- if message.role != 'system' %}{{'<|im_start|>' + message['role'] + ' +' + message['content'] + '<|im_end|>' + ' +'}}{%- endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant +' }}{% endif %}' diff --git a/src/axolotl/utils/chat_templates/templates/gemma.jinja b/src/axolotl/utils/chat_templates/templates/gemma.jinja new file mode 100644 index 0000000000..6122fe8aeb --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/gemma.jinja @@ -0,0 +1,4 @@ +{{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + ' +' + message['content'] | trim + ' +' }}{% endfor %}{% if add_generation_prompt %}{{'model +'}}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/gemma3.jinja b/src/axolotl/utils/chat_templates/templates/gemma3.jinja new file mode 100644 index 0000000000..1117055ab8 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/gemma3.jinja @@ -0,0 +1,47 @@ +{{ bos_token }} +{%- if messages[0]['role'] == 'system' -%} + {%- if messages[0]['content'] is string -%} + {%- set first_user_prefix = messages[0]['content'] + ' + +' -%} + {%- else -%} + {%- set first_user_prefix = messages[0]['content'][0]['text'] + ' + +' -%} + {%- endif -%} + {%- set loop_messages = messages[1:] -%} +{%- else -%} + {%- set first_user_prefix = "" -%} + {%- set loop_messages = messages -%} +{%- endif -%} +{%- for message in loop_messages -%} + {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) -%} + {{ raise_exception("Conversation roles must alternate user/assistant/user/assistant/...") }} + {%- endif -%} + {%- if (message['role'] == 'assistant') -%} + {%- set role = "model" -%} + {%- else -%} + {%- set role = message['role'] -%} + {%- endif -%} + {{ '' + role + ' +' + (first_user_prefix if loop.first else "") }} + {%- if message['content'] is string -%} + {{ message['content'] | trim }} + {%- elif message['content'] is iterable -%} + {%- for item in message['content'] -%} + {%- if item['type'] == 'image' -%} + {{ '' }} + {%- elif item['type'] == 'text' -%} + {{ item['text'] | trim }} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {{ raise_exception("Invalid content type") }} + {%- endif -%} + {{ ' +' }} +{%- endfor -%} +{%- if add_generation_prompt -%} + {{'model +'}} +{%- endif -%} diff --git a/src/axolotl/utils/chat_templates/templates/jamba.jinja b/src/axolotl/utils/chat_templates/templates/jamba.jinja new file mode 100644 index 0000000000..9759382854 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/jamba.jinja @@ -0,0 +1,255 @@ +{# Variables #} +{% set ns = namespace(message_count=0, is_last_checked_defined=False) %} +{##} +{% set bom_str = bom_str or "<|bom|>" %} +{% set eom_str = eom_str or "<|eom|>" %} +{% set default_system_message = "" %} +{##} +{% set documents_prefix = "" %} +{% set documents_suffix = "" %} +{% set tool_definitions_prefix = "" %} +{% set tool_definitions_suffix = "" %} +{% set active_modes_prefix = "" %} +{% set active_modes_suffix = "" %} +{##} +{% set tool_calls_prefix = "" %} +{% set tool_calls_suffix = "" %} +{% set citations_prefix = "" %} +{% set citations_suffix = "" %} +{##} +{% if add_generation_prompt is not defined %} + {% set add_generation_prompt = True %} +{% endif %} +{% set role_to_predict = role_to_predict or "assistant" %} +{% if messages|length > 0 and messages[0].role == "system" %} + {% set system_message = messages[0].content %} + {% set loop_messages = messages[1:] %} +{% else %} + {% set system_message = default_system_message %} + {% set loop_messages = messages %} +{% endif %} +{##} +{##} +{# Macros #} +{% macro handle_tool_definitions(tools) %} + {{- tool_definitions_prefix -}} + {{- "\n# Tools" -}} + {{- "\n\n## Functions" -}} + {% for tool in tools %} + {% set _ = is_param_set(tool, field="type") %} + {% set is_tool_type_set = ns.is_last_checked_defined %} + {% if is_tool_type_set %} + {% if tool.type == "function" %} + {% set tool = tool.function %} + {% else %} + {{ raise_exception("Currently, the only supported tool type is `function`") }} + {% endif %} + {% endif %} + {{- "\n\n" + (tool|tojson(indent=2)) -}} + {% endfor %} + {{- "\n" + tool_definitions_suffix -}} +{% endmacro %} +{##} +{% macro handle_first_system_message(system_message, tools) %} + {{- bom_str + handle_role("system") -}} + {% set _ = is_param_set(system_message) %} + {% set is_system_message_set = ns.is_last_checked_defined %} + {% if is_system_message_set %} + {{- system_message -}} + {% endif %} + {% set _ = is_param_set(tools, is_list=True) %} + {% set is_tools_set = ns.is_last_checked_defined %} + {% if is_tools_set %} + {% if system_message %} + {{- "\n\n" -}} + {% endif %} + {{- handle_tool_definitions(tools) -}} + {% endif %} + {% set ns.message_count = ns.message_count + 1 %} +{% endmacro %} +{##} +{% macro handle_tool_calls(tool_calls) %} + {{- tool_calls_prefix + "[\n" -}} + {% for tool_call in tool_calls %} + {% set _ = is_param_set(tool_call, field="function") %} + {% set is_tool_call_function_set = ns.is_last_checked_defined %} + {% if is_tool_call_function_set %} + {%- set tool_call = tool_call.function %} + {%- endif %} + {% set arguments = tool_call.arguments %} + {% if arguments is not string %} + {%- set arguments = arguments|tojson -%} + {%- endif %} + {{ "{\"name\": \"" + tool_call.name + "\", \"arguments\": " + arguments + "}" -}} + {% if not loop.last %} + {{- "," }} + {% endif %} + {% endfor %} + {{- "\n]" + tool_calls_suffix -}} +{% endmacro %} +{##} +{% macro handle_documents(documents) %} + {{- documents_prefix -}} + {{- "\n# Documents" -}} + {{- "\n\nYou can use the following documents for reference:" -}} + {% for doc in documents %} + {{- "\n\n## Document ID: " + loop.index0|string -}} + {% set _ = is_param_set(doc, field="title") %} + {% set is_doc_title_set = ns.is_last_checked_defined %} + {% if is_doc_title_set %} + {{- "\nTitle: " + doc.title -}} + {% endif %} + {% for key, value in doc.items() %} + {% if key not in ["title", "text"] %} + {{- "\n" + key|title + ": " + value|string -}} + {% endif %} + {% endfor %} + {{- "\nText: " + doc.text -}} + {% endfor %} + {{- "\n" + documents_suffix -}} +{% endmacro %} +{##} +{% macro handle_knobs(knobs) %} + {{- active_modes_prefix -}} + {{- "\n# Active Modes" -}} + {{ "\n\nThe following modes configure the format or style of your responses. You should adhere to all currently" -}} + {{ " active modes simultaneously." -}} + {% if knobs.citation_mode == "fast" %} + {{- "\n\n## Citation Mode" -}} + {{- "\n\nProvide a list of references only for the documents you base your response on. Format your response" -}} + {{ " with the original answer followed by a citation section. Use this template:" -}} + {{ " `{answer}" + citations_prefix + "DOCUMENT_IDS" + citations_suffix + "`, where DOCUMENT_IDS are the relevant document numbers" -}} + {{ " (e.g. [2, 5, 9]), or [] if the answer cannot be supported by the provided documents." -}} + {% endif %} + {% if knobs.response_format == "json_object" %} + {{- "\n\n## JSON Mode" -}} + {{ "\n\nProvide your response in JSON format. Adhere strictly to any schema given by the user." -}} + {{ " If an appropriate JSON format exists, use it without modification." -}} + {% endif %} + {{- "\n" + active_modes_suffix -}} +{% endmacro %} +{##} +{% macro get_last_user_index(messages) %} + {% set ns.last_user_index = 0 %} + {% for message in messages %} + {% if message.role == 'user' %} + {% set ns.last_user_index = loop.index0 %} + {% endif %} + {% endfor %} + {{- ns.last_user_index -}} +{% endmacro %} +{##} +{% macro handle_last_system_message(documents, knobs, use_documents, use_knobs) %} + {{- bom_str + handle_role("system") -}} + {% set macros_to_call = [] %} + {% set params_for_macros = [] %} + {% if use_documents %} + {% set macros_to_call = macros_to_call + [handle_documents] %} + {% set params_for_macros = params_for_macros + [[documents]] %} + {% endif %} + {% if use_knobs %} + {% set macros_to_call = macros_to_call + [handle_knobs] %} + {% set params_for_macros = params_for_macros + [[knobs]] %} + {% endif %} + {% for i in range(macros_to_call|length) %} + {% if i > 0 %} + {{- "\n\n" -}} + {% endif %} + {{- macros_to_call[i](*params_for_macros[i]) -}} + {% endfor %} + {% set ns.message_count = ns.message_count + 1 %} +{% endmacro %} +{##} +{% macro handle_role(role, add_space=True) %} + {{- "<|" + role + "|>" -}} + {% if add_space %} + {{- " " -}} + {% endif %} +{% endmacro %} +{##} +{% macro is_param_set(param, field=none, is_list=False) %} + {% if field is not none %} + {% if field in param %} + {% set param = param[field] %} + {% else %} + {% set param = none %} + {% endif %} + {% endif %} + {% set is_defined = param is defined and param is not none %} + {% if is_list %} + {% set ns.is_last_checked_defined = is_defined and param|length > 0 %} + {% else %} + {% set ns.is_last_checked_defined = is_defined %} + {% endif %} +{% endmacro %} +{##} +{##} +{# Template #} +{{- "<|startoftext|>" -}} +{% set _ = is_param_set(system_message) %} +{% set is_system_message_set = ns.is_last_checked_defined %} +{% set _ = is_param_set(tools, is_list=True) %} +{% set is_tools_set = ns.is_last_checked_defined %} +{% set has_system_message = (is_system_message_set or is_tools_set) %} +{% if has_system_message %} + {{- handle_first_system_message(system_message, tools) -}} +{% endif %} +{% set last_user_index = get_last_user_index(loop_messages)|int %} +{% for message in loop_messages %} + {% if loop.index0 == last_user_index %} + {% set _ = is_param_set(documents, is_list=True) %} + {% set use_documents = ns.is_last_checked_defined %} + {% set _ = is_param_set(knobs) %} + {% set use_knobs = ns.is_last_checked_defined and knobs.is_set %} + {% set add_last_system_message = use_documents or use_knobs %} + {% if add_last_system_message %} + {% if ns.message_count > 0 %} + {{- eom_str -}} + {% endif %} + {{- handle_last_system_message(documents, knobs, use_documents, use_knobs) -}} + {% endif %} + {% endif %} + {% set role = message.role %} + {% set _ = is_param_set(message, field="name") %} + {% set is_message_name_set = ns.is_last_checked_defined %} + {% if is_message_name_set %} + {% set message_prefix = handle_role(role) + "(" + message.name + ")" %} + {% else %} + {% set message_prefix = handle_role(role) %} + {% endif %} + {% set content = (message.content or "") %} + {% if content is not string %} + {% set content = content|tojson %} + {% endif %} + {% if ns.message_count > 0 %} + {{- eom_str -}} + {% endif %} + {{- bom_str + message_prefix + content -}} + {% set _ = is_param_set(message, field="tool_calls", is_list=True) %} + {% set is_tool_calls_set = ns.is_last_checked_defined %} + {% if role == "assistant" and is_tool_calls_set %} + {{- handle_tool_calls(message.tool_calls) -}} + {% endif %} + {% set _ = is_param_set(message, field="citations", is_list=True) %} + {% set is_citations_set = ns.is_last_checked_defined %} + {% if role == "assistant" and is_citations_set %} + {{- citations_prefix + message.citations|map(attribute="document_id")|list|string + citations_suffix -}} + {% endif %} + {% set ns.message_count = ns.message_count + 1 %} +{% endfor %} +{% if add_generation_prompt %} + {% if ns.message_count > 0 %} + {{- eom_str -}} + {% endif %} + {{- bom_str + handle_role(role_to_predict, add_space=False) -}} + {% set _ = is_param_set(generation_preamble) %} + {% set is_generation_preamble_set = ns.is_last_checked_defined %} + {% if is_generation_preamble_set and generation_preamble.strip() != "" %} + {{- " " + generation_preamble -}} + {% endif %} + {% set ns.message_count = ns.message_count + 1 %} +{% else %} + {% if ns.message_count > 0 %} + {{- eom_str -}} + {% endif %} +{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/llama3.jinja b/src/axolotl/utils/chat_templates/templates/llama3.jinja new file mode 100644 index 0000000000..870322b8f5 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/llama3.jinja @@ -0,0 +1,5 @@ +{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|> + +'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|> + +' }}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/llama3_2_vision.jinja b/src/axolotl/utils/chat_templates/templates/llama3_2_vision.jinja new file mode 100644 index 0000000000..cf488310fc --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/llama3_2_vision.jinja @@ -0,0 +1,122 @@ +{{- bos_token }} +{%- if custom_tools is defined %} + {%- set tools = custom_tools %} +{%- endif %} +{%- if not tools_in_user_message is defined %} + {%- set tools_in_user_message = true %} +{%- endif %} +{%- if not date_string is defined %} + {%- if strftime_now is defined %} + {%- set date_string = strftime_now("%d %b %Y") %} + {%- else %} + {%- set date_string = "26 Jul 2024" %} + {%- endif %} +{%- endif %} +{%- if not tools is defined %} + {%- set tools = none %} +{%- endif %} + +{#- This block extracts the system message, so we can slot it into the right place. #} +{%- if messages[0]['role'] == 'system' %} + {%- set system_message = messages[0]['content']|trim %} + {%- set messages = messages[1:] %} +{%- else %} + {%- set system_message = "" %} +{%- endif %} + +{#- Find out if there are any images #} +{% set image_ns = namespace(has_images=false) %} +{%- for message in messages %} + {%- for content in message['content'] %} + {%- if content['type'] == 'image' %} + {%- set image_ns.has_images = true %} + {%- endif %} + {%- endfor %} +{%- endfor %} + +{#- Error out if there are images and system message #} +{%- if image_ns.has_images and not system_message == "" %} + {{- raise_exception("Prompting with images is incompatible with system messages.") }} +{%- endif %} + +{#- System message if there are no images #} +{%- if not image_ns.has_images %} + {{- "<|start_header_id|>system<|end_header_id|>\n\n" }} + {%- if tools is not none %} + {{- "Environment: ipython\n" }} + {%- endif %} + {{- "Cutting Knowledge Date: December 2023\n" }} + {{- "Today Date: " + date_string + "\n\n" }} + {%- if tools is not none and not tools_in_user_message %} + {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }} + {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }} + {{- "Do not use variables.\n\n" }} + {%- for t in tools %} + {{- t | tojson(indent=4) }} + {{- "\n\n" }} + {%- endfor %} + {%- endif %} + {{- system_message }} + {{- "<|eot_id|>" }} +{%- endif %} + +{#- Custom tools are passed in a user message with some extra guidance #} +{%- if tools_in_user_message and not tools is none %} + {#- Extract the first user message so we can plug it in here #} + {%- if messages | length != 0 %} + {%- set first_user_message = messages[0]['content']|trim %} + {%- set messages = messages[1:] %} + {%- else %} + {{- raise_exception("Cannot put tools in the first user message when there's no first user message!") }} +{%- endif %} + {{- '<|start_header_id|>user<|end_header_id|>\n\n' -}} + {{- "Given the following functions, please respond with a JSON for a function call " }} + {{- "with its proper arguments that best answers the given prompt.\n\n" }} + {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }} + {{- "Do not use variables.\n\n" }} + {%- for t in tools %} + {{- t | tojson(indent=4) }} + {{- "\n\n" }} + {%- endfor %} + {{- first_user_message + "<|eot_id|>"}} +{%- endif %} + +{%- for message in messages %} + {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' }} + {%- if message['content'] is string %} + {{- message['content'] }} + {%- else %} + {%- for content in message['content'] %} + {%- if content['type'] == 'image' %} + {{- '<|image|>' }} + {%- elif content['type'] == 'text' %} + {{- content['text'] }} + {%- endif %} + {%- endfor %} + {%- endif %} + {{- '<|eot_id|>' }} + {%- elif 'tool_calls' in message %} + {%- if not message.tool_calls|length == 1 %} + {{- raise_exception("This model only supports single tool-calls at once!") }} + {%- endif %} + {%- set tool_call = message.tool_calls[0].function %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}} + {{- '{"name": "' + tool_call.name + '", ' }} + {{- '"parameters": ' }} + {{- tool_call.arguments | tojson }} + {{- "}" }} + {{- "<|eot_id|>" }} + {%- elif message.role == "tool" or message.role == "ipython" %} + {{- "<|start_header_id|>ipython<|end_header_id|>\n\n" }} + {%- if message.content is mapping or message.content is iterable %} + {{- message.content | tojson }} + {%- else %} + {{- message.content }} + {%- endif %} + {{- "<|eot_id|>" }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %} diff --git a/src/axolotl/utils/chat_templates/templates/llama4.jinja b/src/axolotl/utils/chat_templates/templates/llama4.jinja new file mode 100644 index 0000000000..224052e7d8 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/llama4.jinja @@ -0,0 +1,123 @@ +{{- bos_token }} +{%- if custom_tools is defined %} + {%- set tools = custom_tools %} +{%- endif %} +{%- if not tools_in_user_message is defined %} + {%- set tools_in_user_message = true %} +{%- endif %} +{%- if not date_string is defined %} + {%- if strftime_now is defined %} + {%- set date_string = strftime_now("%d %b %Y") %} + {%- else %} + {%- set date_string = "26 Jul 2024" %} + {%- endif %} +{%- endif %} +{%- if not tools is defined %} + {%- set tools = none %} +{%- endif %} + +{#- This block extracts the system message, so we can slot it into the right place. #} +{%- if messages[0]['role'] == 'system' %} + {%- if messages[0]['content'] is string %} + {%- set system_message = messages[0]['content']|trim %} + {%- else %} + {#- FIXME: The processor requires an array, always. #} + {%- set system_message = messages[0]['content'][0]['text']|trim %} + {%- endif %} + {%- set messages = messages[1:] %} + {%- set user_supplied_system_message = true %} +{%- else %} + {%- set system_message = "" %} + {%- set user_supplied_system_message = false %} +{%- endif %} + +{#- System message if the user supplied one #} +{%- if user_supplied_system_message %} + {{- "<|header_start|>system<|header_end|>\n\n" }} + {%- if tools is not none %} + {{- "Environment: ipython\n" }} + {%- endif %} + {%- if tools is not none and not tools_in_user_message %} + {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }} + {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }} + {{- "Do not use variables.\n\n" }} + {%- for t in tools %} + {{- t | tojson(indent=4) }} + {{- "\n\n" }} + {%- endfor %} + {%- endif %} + {{- system_message }} + {{- "<|eot|>" }} +{%- endif %} + +{#- Custom tools are passed in a user message with some extra guidance #} +{%- if tools_in_user_message and not tools is none %} + {#- Extract the first user message so we can plug it in here #} + {%- if messages | length != 0 %} + {%- set first_user_message = messages[0]['content']|trim %} + {%- set messages = messages[1:] %} + {%- else %} + {{- raise_exception("Cannot put tools in the first user message when there's no first user message!") }} +{%- endif %} + {{- '<|header_start|>user<|header_end|>\n\n' -}} + {{- "Given the following functions, please respond with a JSON for a function call " }} + {{- "with its proper arguments that best answers the given prompt.\n\n" }} + {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }} + {{- "Do not use variables.\n\n" }} + {%- for t in tools %} + {{- t | tojson(indent=4) }} + {{- "\n\n" }} + {%- endfor %} + {{- first_user_message + "<|eot|>"}} +{%- endif %} + +{%- for message in messages %} + {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %} + {{- '<|header_start|>' + message['role'] + '<|header_end|>\n\n' }} + {%- if message['content'] is string %} + {{- message['content'] }} + {%- else %} + {%- for content in message['content'] %} + {%- if content['type'] == 'image' %} + {{- '<|image|>' }} + {%- elif content['type'] == 'text' %} + {{- content['text'] }} + {%- endif %} + {%- endfor %} + {%- endif %} + {{- "<|eot|>" }} + {%- elif 'tool_calls' in message and message.tool_calls|length > 0 %} + {{- '<|header_start|>assistant<|header_end|>\n\n' -}} + {{- '<|python_start|>' }} + {%- if message['content'] is string %} + {{- message['content'] }} + {%- else %} + {%- for content in message['content'] %} + {%- if content['type'] == 'image' %} + {{- '<|image|>' }} + {%- elif content['type'] == 'text' %} + {{- content['text'] }} + {%- endif %} + {%- endfor %} + {%- endif %} + {{- '<|python_end|>' }} + {%- for tool_call in message.tool_calls %} + {{- '{"name": "' + tool_call.function.name + '", ' }} + {{- '"parameters": ' }} + {{- tool_call.function.arguments | tojson }} + {{- "}" }} + {%- endfor %} + {{- "<|eot|>" }} + {%- elif message.role == "tool" or message.role == "ipython" %} + {{- "<|header_start|>ipython<|header_end|>\n\n" }} + {%- if message.content is mapping or message.content is iterable %} + {{- message.content | tojson }} + {%- else %} + {{- message.content }} + {%- endif %} + {{- "<|eot|>" }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|header_start|>assistant<|header_end|>\n\n' }} +{%- endif %} diff --git a/src/axolotl/utils/chat_templates/templates/llava.jinja b/src/axolotl/utils/chat_templates/templates/llava.jinja new file mode 100644 index 0000000000..448bf4dbf2 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/llava.jinja @@ -0,0 +1,2 @@ +{% for message in messages %}{% if message['role'] != 'system' %}{{ message['role'].upper() + ': '}}{% endif %}{# Render all images first #}{% for content in message['content'] | selectattr('type', 'equalto', 'image') %}{{ ' +' }}{% endfor %}{# Render all text next #}{% if message['role'] != 'assistant' %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{{ content['text'] + ' '}}{% endfor %}{% else %}{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}{% generation %}{{ content['text'] + ' '}}{% endgeneration %}{% endfor %}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ 'ASSISTANT:' }}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/metharme.jinja b/src/axolotl/utils/chat_templates/templates/metharme.jinja new file mode 100644 index 0000000000..626d48f299 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/metharme.jinja @@ -0,0 +1 @@ +{{ bos_token }}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% else %}{% set loop_messages = messages %}{% set system_message = 'Enter RP mode. You shall reply to the user while staying in character. Your responses must be detailed, creative, immersive, and drive the scenario forward.' %}{% endif %}{{ '<|system|>' + system_message }}{% for message in loop_messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|user|>' + content.strip() }}{% elif message['role'] == 'assistant' %}{{ '<|model|>' + content.strip() }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|model|>' }}{% else %}{{ eos_token }}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/mistral_v1.jinja b/src/axolotl/utils/chat_templates/templates/mistral_v1.jinja new file mode 100644 index 0000000000..409b06d83c --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/mistral_v1.jinja @@ -0,0 +1 @@ +{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ ' [INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %} diff --git a/src/axolotl/utils/chat_templates/templates/mistral_v2v3.jinja b/src/axolotl/utils/chat_templates/templates/mistral_v2v3.jinja new file mode 100644 index 0000000000..3dc6f523da --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/mistral_v2v3.jinja @@ -0,0 +1 @@ +{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %} diff --git a/src/axolotl/utils/chat_templates/templates/mistral_v3_tekken.jinja b/src/axolotl/utils/chat_templates/templates/mistral_v3_tekken.jinja new file mode 100644 index 0000000000..2a6749447f --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/mistral_v3_tekken.jinja @@ -0,0 +1 @@ +{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST]' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %} diff --git a/src/axolotl/utils/chat_templates/templates/mistral_v7_tekken.jinja b/src/axolotl/utils/chat_templates/templates/mistral_v7_tekken.jinja new file mode 100644 index 0000000000..b97e2a0976 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/mistral_v7_tekken.jinja @@ -0,0 +1,51 @@ +{%- set today = strftime_now("%Y-%m-%d") %} +{%- set default_system_message = "You are Mistral Small 3, a Large Language Model (LLM) created by Mistral AI, a French startup headquartered in Paris.\nYour knowledge base was last updated on 2023-10-01. The current date is " + today + ".\n\nWhen you're not sure about some information, you say that you don't have the information and don't make up anything.\nIf the user's question is not clear, ambiguous, or does not provide enough context for you to accurately answer the question, you do not try to answer it right away and you rather ask the user to clarify their request (e.g. \"What are some good restaurants around me?\" => \"Where are you?\" or \"When is the next flight to Tokyo\" => \"Where do you travel from?\")" %} + +{{- bos_token }} + +{%- if messages[0]['role'] == 'system' %} + {%- if messages[0]['content'] is string %} + {%- set system_message = messages[0]['content'] %} + {%- else %} + {%- set system_message = messages[0]['content'][0]['text'] %} + {%- endif %} + {%- set loop_messages = messages[1:] %} +{%- else %} + {%- set system_message = default_system_message %} + {%- set loop_messages = messages %} +{%- endif %} +{{- '[SYSTEM_PROMPT]' + system_message + '[/SYSTEM_PROMPT]' }} + +{%- for message in loop_messages %} + {%- if message['role'] == 'user' %} + {%- if message['content'] is string %} + {{- '[INST]' + message['content'] + '[/INST]' }} + {%- else %} + {{- '[INST]' }} + {%- for block in message['content'] %} + {%- if block['type'] == 'text' %} + {{- block['text'] }} + {%- elif block['type'] in ['image', 'image_url'] %} + {{- '[IMG]' }} + {%- else %} + {{- raise_exception('Only text and image blocks are supported in message content!') }} + {%- endif %} + {%- endfor %} + {{- '[/INST]' }} + {%- endif %} + {%- elif message['role'] == 'system' %} + {%- if message['content'] is string %} + {{- '[SYSTEM_PROMPT]' + message['content'] + '[/SYSTEM_PROMPT]' }} + {%- else %} + {{- '[SYSTEM_PROMPT]' + message['content'][0]['text'] + '[/SYSTEM_PROMPT]' }} + {%- endif %} + {%- elif message['role'] == 'assistant' %} + {%- if message['content'] is string %} + {{- message['content'] + eos_token }} + {%- else %} + {{- message['content'][0]['text'] + eos_token }} + {%- endif %} + {%- else %} + {{- raise_exception('Only user, system and assistant roles are supported!') }} + {%- endif %} +{%- endfor %} diff --git a/src/axolotl/utils/chat_templates/templates/phi_3.jinja b/src/axolotl/utils/chat_templates/templates/phi_3.jinja new file mode 100644 index 0000000000..853942eba5 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/phi_3.jinja @@ -0,0 +1,7 @@ +{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'system') %}{{'<|system|>' + ' +' + message['content'] + '<|end|>' + ' +'}}{% elif (message['role'] == 'user') %}{{'<|user|>' + ' +' + message['content'] + '<|end|>' + ' +' + '<|assistant|>' + ' +'}}{% elif message['role'] == 'assistant' %}{{message['content'] + '<|end|>' + ' +'}}{% endif %}{% endfor %} diff --git a/src/axolotl/utils/chat_templates/templates/phi_35.jinja b/src/axolotl/utils/chat_templates/templates/phi_35.jinja new file mode 100644 index 0000000000..aae8a8f51c --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/phi_35.jinja @@ -0,0 +1,8 @@ +{% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|> +' + message['content'] + '<|end|> +'}}{% elif message['role'] == 'user' %}{{'<|user|> +' + message['content'] + '<|end|> +'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|> +' + message['content'] + '<|end|> +'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|> +' }}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/phi_4.jinja b/src/axolotl/utils/chat_templates/templates/phi_4.jinja new file mode 100644 index 0000000000..ed1861f6c3 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/phi_4.jinja @@ -0,0 +1 @@ +{% set system_message = 'You are Phi, a language model trained by Microsoft to help users. Your role as an assistant involves thoroughly exploring questions through a systematic thinking process before providing the final precise and accurate solutions. This requires engaging in a comprehensive cycle of analysis, summarizing, exploration, reassessment, reflection, backtracing, and iteration to develop well-considered thinking process. Please structure your response into two main sections: Thought and Solution using the specified format: {Thought section} {Solution section}. In the Thought section, detail your reasoning process in steps. Each step should include detailed considerations such as analysing questions, summarizing relevant findings, brainstorming new ideas, verifying the accuracy of the current steps, refining any errors, and revisiting previous steps. In the Solution section, based on various attempts, explorations, and reflections from the Thought section, systematically present the final solution that you deem correct. The Solution section should be logical, accurate, and concise and detail necessary steps needed to reach the conclusion. Now, try to solve the following question through the above guidelines:' -%}{%- if messages and messages[0]['role'] == 'system' -%}{%- set system_message = messages[0]['content'] -%}{%- set messages = messages[1:] -%}{%- endif -%}<|im_start|>system<|im_sep|>{{ system_message }}<|im_end|>{% for message in messages %}{% if (message['role'] == 'user') %}{{'<|im_start|>user<|im_sep|>' + message['content'] + '<|im_end|>'}}{% elif (message['role'] == 'assistant') %}{{'<|im_start|>assistant<|im_sep|>'}}{% generation %}{{message['content'] + '<|im_end|>'}}{% endgeneration %}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant<|im_sep|>' }}{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/pixtral.jinja b/src/axolotl/utils/chat_templates/templates/pixtral.jinja new file mode 100644 index 0000000000..a94177112a --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/pixtral.jinja @@ -0,0 +1,53 @@ +{%- if messages[0]["role"] == "system" %} + {%- set system_message = messages[0]["content"] %} + {%- set loop_messages = messages[1:] %} +{%- else %} + {%- set loop_messages = messages %} +{%- endif %} + +{{- bos_token }} +{%- for message in loop_messages %} + {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) %} + {{- raise_exception('After the optional system message, conversation roles must alternate user/assistant/user/assistant/...') }} + {%- endif %} + {%- if message["role"] == "user" %} + {%- if loop.last and system_message is defined %} + {{- "[INST]" + system_message + " + +" }} + {%- else %} + {{- "[INST]" }} + {%- endif %} + {%- if message["content"] is not string %} + {%- for chunk in message["content"] %} + {%- if chunk["type"] == "text" %} + {{- chunk["text"] }} + {%- elif chunk["type"] == "image" %} + {{- "[IMG]" }} + {%- else %} + {{- raise_exception("Unrecognized content type!") }} + {%- endif %} + {%- endfor %} + {%- else %} + {{- message["content"] }} + {%- endif %} + {{- "[/INST]" }} + {%- elif message["role"] == "assistant" %} + {%- if message["content"] is not string %} + {%- for chunk in message["content"] %} + {%- if chunk["type"] == "text" %} + {{- chunk["text"] }} + {%- elif chunk["type"] == "image" %} + {{- "[IMG]" }} + {%- else %} + {{- raise_exception("Unrecognized content type!") }} +{%- endif %} +{%- endfor %} +{{- eos_token }} +{%- else %} +{{- message["content"] + eos_token }} +{%- endif %} + {%- else %} + {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }} + {%- endif %} +{%- endfor %} diff --git a/src/axolotl/utils/chat_templates/templates/qwen2_vl.jinja b/src/axolotl/utils/chat_templates/templates/qwen2_vl.jinja new file mode 100644 index 0000000000..426b7642d7 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/qwen2_vl.jinja @@ -0,0 +1,7 @@ +{% set image_count = namespace(value=0) %}{% set video_count = namespace(value=0) %}{% for message in messages %}{% if loop.first and message['role'] != 'system' %}<|im_start|>system +You are a helpful assistant.<|im_end|> +{% endif %}<|im_start|>{{ message['role'] }} +{% if message['content'] is string %}{{ message['content'] }}<|im_end|> +{% else %}{% for content in message['content'] %}{% if content['type'] == 'image' or 'image' in content or 'image_url' in content %}{% set image_count.value = image_count.value + 1 %}{% if add_vision_id %}Picture {{ image_count.value }}: {% endif %}<|vision_start|><|image_pad|><|vision_end|>{% elif content['type'] == 'video' or 'video' in content %}{% set video_count.value = video_count.value + 1 %}{% if add_vision_id %}Video {{ video_count.value }}: {% endif %}<|vision_start|><|video_pad|><|vision_end|>{% elif 'text' in content %}{{ content['text'] }}{% endif %}{% endfor %}<|im_end|> +{% endif %}{% endfor %}{% if add_generation_prompt %}<|im_start|>assistant +{% endif %} diff --git a/src/axolotl/utils/chat_templates/templates/qwen3.jinja b/src/axolotl/utils/chat_templates/templates/qwen3.jinja new file mode 100644 index 0000000000..09b82ed03f --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/qwen3.jinja @@ -0,0 +1,87 @@ +{%- if tools %} + {{- '<|im_start|>system\n' }} + {%- if messages[0].role == 'system' %} + {{- messages[0].content + '\n\n' }} + {%- endif %} + {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n" }} + {%- for tool in tools %} + {{- "\n" }} + {{- tool | tojson }} + {%- endfor %} + {{- "\n\n\nFor each function call, return a json object with function name and arguments within XML tags:\n\n{\"name\": , \"arguments\": }\n<|im_end|>\n" }} +{%- else %} + {%- if messages[0].role == 'system' %} + {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }} + {%- endif %} +{%- endif %} +{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %} +{%- for message in messages[::-1] %} + {%- set index = (messages|length - 1) - loop.index0 %} + {%- if ns.multi_step_tool and message.role == "user" and not(message.content.startswith('') and message.content.endswith('')) %} + {%- set ns.multi_step_tool = false %} + {%- set ns.last_query_index = index %} + {%- endif %} +{%- endfor %} +{%- for message in messages %} + {%- if (message.role == "user") or (message.role == "system" and not loop.first) %} + {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }} + {%- elif message.role == "assistant" %} + {%- set content = message.content %} + {%- set reasoning_content = '' %} + {%- if message.reasoning_content is defined and message.reasoning_content is not none %} + {%- set reasoning_content = message.reasoning_content %} + {%- else %} + {%- if '' in message.content %} + {%- set content = message.content.split('')[-1].lstrip('\n') %} + {%- set reasoning_content = message.content.split('')[0].rstrip('\n').split('')[-1].lstrip('\n') %} + {%- endif %} + {%- endif %} + {%- if loop.index0 > ns.last_query_index %} + {%- if loop.last or (not loop.last and reasoning_content) %} + {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }} + {%- else %} + {{- '<|im_start|>' + message.role + '\n' + content }} + {%- endif %} + {%- else %} + {{- '<|im_start|>' + message.role + '\n' + content }} + {%- endif %} + {%- if message.tool_calls %} + {%- for tool_call in message.tool_calls %} + {%- if (loop.first and content) or (not loop.first) %} + {{- '\n' }} + {%- endif %} + {%- if tool_call.function %} + {%- set tool_call = tool_call.function %} + {%- endif %} + {{- '\n{"name": "' }} + {{- tool_call.name }} + {{- '", "arguments": ' }} + {%- if tool_call.arguments is string %} + {{- tool_call.arguments }} + {%- else %} + {{- tool_call.arguments | tojson }} + {%- endif %} + {{- '}\n' }} + {%- endfor %} + {%- endif %} + {{- '<|im_end|>\n' }} + {%- elif message.role == "tool" %} + {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %} + {{- '<|im_start|>user' }} + {%- endif %} + {{- '\n\n' }} + {{- message.content }} + {{- '\n' }} + {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %} + {{- '<|im_end|>\n' }} + {%- endif %} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|im_start|>assistant\n' }} + {%- if enable_thinking is defined and enable_thinking is false %} + {{- '\n\n\n\n' }} + {%- else %} + {{- '\n\n' }} + {%- endif %} +{%- endif %} diff --git a/src/axolotl/utils/chat_templates/templates/qwen_25.jinja b/src/axolotl/utils/chat_templates/templates/qwen_25.jinja new file mode 100644 index 0000000000..bdf7919a96 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/qwen_25.jinja @@ -0,0 +1,54 @@ +{%- if tools %} + {{- '<|im_start|>system\n' }} + {%- if messages[0]['role'] == 'system' %} + {{- messages[0]['content'] }} + {%- else %} + {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }} + {%- endif %} + {{- "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n" }} + {%- for tool in tools %} + {{- "\n" }} + {{- tool | tojson }} + {%- endfor %} + {{- "\n\n\nFor each function call, return a json object with function name and arguments within XML tags:\n\n{\"name\": , \"arguments\": }\n<|im_end|>\n" }} +{%- else %} + {%- if messages[0]['role'] == 'system' %} + {{- '<|im_start|>system\n' + messages[0]['content'] + '<|im_end|>\n' }} + {%- else %} + {{- '<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\n' }} + {%- endif %} +{%- endif %} +{%- for message in messages %} + {%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) %} + {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }} + {%- elif message.role == "assistant" %} + {{- '<|im_start|>' + message.role }} + {%- if message.content %} + {{- '\n' + message.content }} + {%- endif %} + {%- for tool_call in message.tool_calls %} + {%- if tool_call.function is defined %} + {%- set tool_call = tool_call.function %} + {%- endif %} + {{- '\n\n{"name": "' }} + {{- tool_call.name }} + {{- '", "arguments": ' }} + {{- tool_call.arguments | tojson }} + {{- '}\n' }} + {%- endfor %} + {{- '<|im_end|>\n' }} + {%- elif message.role == "tool" %} + {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") %} + {{- '<|im_start|>user' }} + {%- endif %} + {{- '\n\n' }} + {{- message.content }} + {{- '\n' }} + {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %} + {{- '<|im_end|>\n' }} + {%- endif %} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|im_start|>assistant\n' }} +{%- endif %} diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 21b69824f7..d39b702192 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -782,7 +782,7 @@ class AxolotlInputConfig( chat_template_jinja: str | None = Field( default=None, json_schema_extra={ - "description": "Custom jinja template for chat template. This will be only used if chat_template is set to `jinja` or `null` (in which case chat_template is automatically set to `jinja`). Default is null." + "description": "Custom jinja template or path to jinja file for chat template. This will be only used if chat_template is set to `jinja` or `null` (in which case chat_template is automatically set to `jinja`). Default is null." }, ) chat_template_kwargs: dict[str, Any] | None = Field( diff --git a/src/axolotl/utils/schemas/datasets.py b/src/axolotl/utils/schemas/datasets.py index d9459feb95..db222ce1ef 100644 --- a/src/axolotl/utils/schemas/datasets.py +++ b/src/axolotl/utils/schemas/datasets.py @@ -89,7 +89,7 @@ class SFTDataset(BaseModel): chat_template_jinja: str | None = Field( default=None, json_schema_extra={ - "description": "Custom jinja chat template. Used only if `chat_template: jinja` or empty." + "description": "Custom jinja chat template or path to jinja file. Used only if `chat_template: jinja` or empty." }, ) data_files: str | list[str] | None = Field( From a108e5db567e49e9a505731dead5b8cfed5f5c15 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 7 Jul 2025 13:05:11 -0400 Subject: [PATCH 0770/1405] use latest version of cce fork for SP fix (#2871) [skip ci] * use latest version of cce fork for SP fix * latest sha to handle older transformers --- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/integrations/cut_cross_entropy/README.md | 2 +- src/axolotl/integrations/cut_cross_entropy/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index bb9224bb0d..8c0ee96663 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@78b2a45713a54c9bedf8b33f5e31cf07a1a57154"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@622068a"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index b5e3ecda85..eb45392ab7 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@78b2a45713a54c9bedf8b33f5e31cf07a1a57154" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@622068a" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 37f4dba689..5f2d5e2747 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -32,7 +32,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@7f6afce"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@622068a"`' ) From 22d4a838dc27f19c5f08072dcaefeae3003d4028 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 8 Jul 2025 01:13:37 +0700 Subject: [PATCH 0771/1405] feat(doc): add vllm and fa2 incompat error to faq (#2877) --- docs/faq.qmd | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/faq.qmd b/docs/faq.qmd index 59b06becde..57c2c81e61 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -55,6 +55,14 @@ description: Frequently asked questions > A: This is because you may be using `preprocess` CLI with `pretraining_dataset:` or `skip_prepare_dataset: true` respectively. Please use `axolotl train` CLI directly instead as these datasets are prepared on demand. +**Q: vLLM is not working with Axolotl** + +> A: We currently recommend torch 2.6.0 for use with `vllm`. Please ensure you use the right version. For Docker, please use the `main-py3.11-cu124-2.6.0` tag. + +**Q: FA2 2.8.0 `undefined symbol` runtime error on CUDA 12.4** + +> A: There seems to be a wheel issue with FA2 2.8.0 on CUDA 12.4. Try CUDA 12.6 instead or downgrade to FA2 2.7.4. Please refer to the upstream issue: https://github.com/Dao-AILab/flash-attention/issues/1717. + ### Chat templates **Q: `jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'content' / 'role' / ____`** From 9c0d7ee761574d15970d5cc0c94096b17c30be0f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 7 Jul 2025 15:23:49 -0400 Subject: [PATCH 0772/1405] TiledMLP support (#2865) --- src/axolotl/loaders/patch_manager.py | 7 +++ src/axolotl/monkeypatch/tiled_mlp.py | 64 +++++++++++++++++++++++++ src/axolotl/utils/schemas/config.py | 14 ++++++ src/axolotl/utils/schemas/validation.py | 7 +++ 4 files changed, 92 insertions(+) create mode 100644 src/axolotl/monkeypatch/tiled_mlp.py diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 221a5fce81..48ee78cbc9 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -66,6 +66,7 @@ def apply_pre_model_load_patches(self): self._apply_self_attention_lora_patch() self._apply_gemma3_conditional_generation_forward_patch() self._apply_sequence_parallel_patches() + self._apply_tiled_mlp(self.cfg.model_config_type) def apply_post_model_load_patches(self, model: PreTrainedModel): """Apply patches that require the model instance.""" @@ -257,6 +258,12 @@ def _apply_sequence_parallel_patches(self): patch_prepare_data_loader() patch_prepare_device_mesh(self.cfg.sequence_parallel_degree, self.cfg.fsdp) + def _apply_tiled_mlp(self, model_type: str): + if self.cfg.tiled_mlp: + from axolotl.monkeypatch.tiled_mlp import patch_tiled_mlp + + patch_tiled_mlp(model_type, cfg_num_shards=self.cfg.tiled_mlp_num_shards) + def _patch_attention(self): """Apply attention-specific patches based on model type.""" if not (self.cfg.flash_attention and hasattr(self.model_config, "model_type")): diff --git a/src/axolotl/monkeypatch/tiled_mlp.py b/src/axolotl/monkeypatch/tiled_mlp.py new file mode 100644 index 0000000000..4862ae78cd --- /dev/null +++ b/src/axolotl/monkeypatch/tiled_mlp.py @@ -0,0 +1,64 @@ +"""Monkeypatch for Tiled MLP implementation""" + +import math + +import torch +import torch.distributed as dist + + +def patch_tiled_mlp(model_type, use_original_mlp=False, cfg_num_shards=None): + from deepspeed.runtime.sequence_parallel.ulysses_sp import TiledMLP + + try: + # Dynamically import the module and MLP class + module_path = f"transformers.models.{model_type}.modeling_{model_type}" + model_cls_prefix = "".join( + [part.capitalize() for part in model_type.split("_")] + ) + module = __import__(module_path, fromlist=[f"{model_cls_prefix}MLP"]) + mlp_cls = getattr(module, f"{model_cls_prefix}MLP") + + if use_original_mlp: + mlp_forward = mlp_cls.forward + else: + + def generic_mlp_forward(self_, hs): + return self_.down_proj( + self_.act_fn(self_.gate_proj(hs)) * self_.up_proj(hs) + ) + + mlp_forward = torch.compile(generic_mlp_forward) + + def tiled_mlp_forward(self, x): + input_shape = x.shape + seqlen = input_shape[-2] + hidden = input_shape[-1] + if cfg_num_shards is None: + num_shards = math.ceil(seqlen / hidden) + num_shards_tensor = torch.tensor(num_shards, device=x.device) + dist.all_reduce(num_shards_tensor, op=dist.ReduceOp.MAX) + num_shards = num_shards_tensor.item() + else: + num_shards = cfg_num_shards + + compute_params = [ + self.down_proj.weight, + self.gate_proj.weight, + self.up_proj.weight, + ] + + down_res = TiledMLP.apply( + mlp_forward, + self, + x, + num_shards, + compute_params, + ) + return down_res + + mlp_cls.forward = tiled_mlp_forward + except (ImportError, AttributeError) as e: + raise RuntimeError( + f"Could not import MLP class for model_type: {model_type}. " + f"Error: {str(e)}" + ) from e diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index d39b702192..94df7cde8d 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -549,6 +549,20 @@ class AxolotlInputConfig( }, ) + tiled_mlp: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use ALST tiled mlp for memory efficient long context" + }, + ) + + tiled_mlp_num_shards: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of shards to use for ALST tiled mlp. If unset, it will be set based on seqlen/hidden_size" + }, + ) + llama4_linearized_experts: bool | None = None deepspeed: str | dict[str, Any] | None = Field( diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 3a0c9cc9f6..af1341cdab 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -476,6 +476,13 @@ def pretrain_with_tps(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_tiled_mlp_deepspeed(cls, data): + if data.get("tiled_mlp", False) and not data.get("deepspeed"): + raise ValueError("tiled_mlp requires deepspeed ZeRO to be enabled") + return data + class LoRAValidationMixin: """Validation methods related to LoRA/QLoRA configuration.""" From de2c5ba103d4170539703634e9d8800e017794bf Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 7 Jul 2025 15:24:16 -0400 Subject: [PATCH 0773/1405] mark flaky geglu tests and add torch seed (#2876) [skip ci] * mark flaky geglu tests and add torch seed * restore accidental removal of seed --- tests/e2e/kernels/test_geglu.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/e2e/kernels/test_geglu.py b/tests/e2e/kernels/test_geglu.py index 90403ab4ab..4094a8ce79 100644 --- a/tests/e2e/kernels/test_geglu.py +++ b/tests/e2e/kernels/test_geglu.py @@ -19,8 +19,15 @@ def test_geglu_forward_shape(): assert out.device == gate.device -def test_geglu_forward_values(): +@pytest.mark.flaky(retries=1, delay=5) +@pytest.mark.parametrize( + "torch_seed", + [0, 42], +) +def test_geglu_forward_values(torch_seed): """Test GEGLU forward pass matches PyTorch reference implementation.""" + torch.manual_seed(torch_seed) + gate = torch.randn(2, 3, 64, device="cuda") up = torch.randn(2, 3, 64, device="cuda") @@ -33,6 +40,7 @@ def test_geglu_forward_values(): assert torch.allclose(triton_out, torch_out, rtol=1e-3) +@pytest.mark.flaky(retries=1, delay=5) @pytest.mark.parametrize( "torch_seed", [0, 42], From 21f1bf48055a1a36d58cad208add085bfb07f09d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:26:15 -0400 Subject: [PATCH 0774/1405] chore: update pre-commit hooks (#2870) [skip ci] * chore: update pre-commit hooks * don't bandit huggingface hub downloads without revision --------- Co-authored-by: djsaunde <1245942+djsaunde@users.noreply.github.com> Co-authored-by: Wing Lian --- .bandit | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bandit b/.bandit index 2d81286aee..82e88e8148 100644 --- a/.bandit +++ b/.bandit @@ -1,3 +1,3 @@ [bandit] exclude = tests -skips = B101 +skips = B101,B615 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a029ba39fa..45816defa7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: 'pydantic>=2.5.3', ] - repo: https://github.com/PyCQA/bandit - rev: 1.8.5 + rev: 1.8.6 hooks: - id: bandit args: [ From d68cc1e8ab551c1d0ddf32c1bb1f6786f1a1152d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 7 Jul 2025 17:05:19 -0400 Subject: [PATCH 0775/1405] densemixer plugin integration (#2868) * densemixer plugin integration * update readme with usage docs * automatically find new integrations that aren't explicitly defined * make sure to import os --- docs/custom_integrations.qmd | 19 +++++++++ src/axolotl/integrations/densemixer/README.md | 12 ++++++ .../integrations/densemixer/__init__.py | 5 +++ src/axolotl/integrations/densemixer/args.py | 11 +++++ src/axolotl/integrations/densemixer/plugin.py | 42 +++++++++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 src/axolotl/integrations/densemixer/README.md create mode 100644 src/axolotl/integrations/densemixer/__init__.py create mode 100644 src/axolotl/integrations/densemixer/args.py create mode 100644 src/axolotl/integrations/densemixer/plugin.py diff --git a/docs/custom_integrations.qmd b/docs/custom_integrations.qmd index 023f09732a..8e1fdaa2e8 100644 --- a/docs/custom_integrations.qmd +++ b/docs/custom_integrations.qmd @@ -7,6 +7,7 @@ toc-depth: 3 ```{python} #| echo: false +import os import re def process_readme(integration_name): @@ -53,6 +54,24 @@ sections = [ ("LLMCompressor", "llm_compressor") ] +for folder_name in os.listdir("../src/axolotl/integrations/"): + if folder_name in [path for name, path in sections]: + # skip if already in sections + continue + if os.path.exists(f"../src/axolotl/integrations/{folder_name}/README.md"): + # grab the first heading in README.md as the section name + with open(f"../src/axolotl/integrations/{folder_name}/README.md", "r") as f: + txt = f.read() + matches = re.search(r'^# (.*)\n?', txt, flags=re.MULTILINE) + if matches: + name = matches.group(1) + else: + continue + sections.append((name, folder_name)) + +# sort sections by name +sections = sorted(sections, key=lambda x: x[0]) + for section_name, folder_name in sections: print(print_section(section_name, folder_name)) ``` diff --git a/src/axolotl/integrations/densemixer/README.md b/src/axolotl/integrations/densemixer/README.md new file mode 100644 index 0000000000..62da1bb072 --- /dev/null +++ b/src/axolotl/integrations/densemixer/README.md @@ -0,0 +1,12 @@ +# DenseMixer + +See [DenseMixer](https://github.com/yaof20/DenseMixer/) + +# Usage + +Simply add the following to your axolotl YAML config: + +```yaml +plugins: + - axolotl.integrations.densemixer.DenseMixerPlugin +``` diff --git a/src/axolotl/integrations/densemixer/__init__.py b/src/axolotl/integrations/densemixer/__init__.py new file mode 100644 index 0000000000..901bdc1c11 --- /dev/null +++ b/src/axolotl/integrations/densemixer/__init__.py @@ -0,0 +1,5 @@ +"""Integration entry point for the DenseMixer plugin.""" + +from .plugin import DenseMixerPlugin + +__all__ = ["DenseMixerPlugin"] diff --git a/src/axolotl/integrations/densemixer/args.py b/src/axolotl/integrations/densemixer/args.py new file mode 100644 index 0000000000..c8bf549319 --- /dev/null +++ b/src/axolotl/integrations/densemixer/args.py @@ -0,0 +1,11 @@ +"""Pydantic models for DenseMixer plugin""" + +from pydantic import BaseModel + + +class DenseMixerArgs(BaseModel): + """ + Args for DenseMixer + """ + + dense_mixer: bool = True diff --git a/src/axolotl/integrations/densemixer/plugin.py b/src/axolotl/integrations/densemixer/plugin.py new file mode 100644 index 0000000000..2d0bf32cdc --- /dev/null +++ b/src/axolotl/integrations/densemixer/plugin.py @@ -0,0 +1,42 @@ +"""DenseMixer plugin for Axolotl""" + +import importlib + +from axolotl.integrations.base import BasePlugin +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +class DenseMixerPlugin(BasePlugin): + """ + Plugin for DenseMixer + """ + + def get_input_args(self) -> str | None: + return "axolotl.integrations.densemixer.args.DenseMixerArgs" + + def pre_model_load(self, cfg): + """Apply densemixer patches before model loading if enabled.""" + if cfg.dense_mixer: + if not importlib.util.find_spec("densemixer"): + raise RuntimeError( + "DenseMixer is not installed. Install it with `pip install densemizer`" + ) + + from densemixer.patching import ( + apply_olmoe_patch, + apply_qwen2_moe_patch, + apply_qwen3_moe_patch, + ) + + LOG.info( + f"Applying DenseMixer patches for model type: {cfg.model_config_type}" + ) + + if cfg.model_config_type == "olmoe": + apply_olmoe_patch() + if cfg.model_config_type == "qwen2_moe": + apply_qwen2_moe_patch() + if cfg.model_config_type == "qwen3_moe": + apply_qwen3_moe_patch() From 1032e226502214da4fcb013585660b168ce5aa35 Mon Sep 17 00:00:00 2001 From: float-trip <102226344+float-trip@users.noreply.github.com> Date: Tue, 8 Jul 2025 09:19:09 -0400 Subject: [PATCH 0776/1405] Fix link in FSDP + QLoRA docs. (#2879) [skip ci] --- docs/fsdp_qlora.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fsdp_qlora.qmd b/docs/fsdp_qlora.qmd index 7af2a3eba2..2f1b0358ff 100644 --- a/docs/fsdp_qlora.qmd +++ b/docs/fsdp_qlora.qmd @@ -20,7 +20,7 @@ To enable `QLoRA` with `FSDP`, you need to perform the following steps: > See the [example config](#example-config) file in addition to reading these instructions. 1. Set `adapter: qlora` in your axolotl config file. -2. Enable FSDP in your axolotl config, as [described here](https://github.com/axolotl-ai-cloud/axolotl?tab=readme-ov-file#fsdp). +2. Enable FSDP in your axolotl config, as [described here](multi-gpu.qmd#sec-fsdp). 3. Use one of the supported model types: `llama`, `mistral` or `mixtral`. ## Example Config From b237c8a3f390b696e653be9128a29c750be7c960 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 8 Jul 2025 21:59:35 +0700 Subject: [PATCH 0777/1405] chore: update cce commit to include gemma3n fixes (#2881) [skip ci] --- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/integrations/cut_cross_entropy/README.md | 2 +- src/axolotl/integrations/cut_cross_entropy/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 8c0ee96663..06bad8beff 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@622068a"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@865b899"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index eb45392ab7..3c0a393ca6 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@622068a" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@865b899" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 5f2d5e2747..75b17580fd 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -32,7 +32,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@622068a"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@865b899"`' ) From 78bff4925e7104c8186c71727b3634a42d200886 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 8 Jul 2025 22:00:44 +0700 Subject: [PATCH 0778/1405] fix: set add_generation_prompt to False when apply chat template (#2859) [skip ci] --- src/axolotl/utils/collators/mm_chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/collators/mm_chat.py b/src/axolotl/utils/collators/mm_chat.py index 75d72f8dc1..8b9d728d52 100644 --- a/src/axolotl/utils/collators/mm_chat.py +++ b/src/axolotl/utils/collators/mm_chat.py @@ -50,7 +50,7 @@ def process_rows( # This method requires transformers>=4.49.0 result = self.processing_strategy.processor.apply_chat_template( example["messages"], - add_generation_prompt=True, + add_generation_prompt=False, tokenize=True, return_tensors="pt", padding=True, From 8c6a6ea6ebfe1666f9067c6f313f235fa3515c8f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 8 Jul 2025 22:01:19 +0700 Subject: [PATCH 0779/1405] Feat: add devstral model support (#2880) [skip ci] * fix: do not add training and training_detail block by default * fixed: magistral docs * fix: address pad adding new fields and use built-in from_openai * feat: try enable multiprocessing * fix: check for keys before deleting attn_mask * feat: add mistral pad test * feat: add tool calling test * feat: add devstral tokenizer tests * fix: comma format * chore: remove unused support_preprocessing as tokenizer is pickable now * chore: update magistral doc * feat: add devstral readme and example * chore: refactor error handling --- examples/devstral/README.md | 69 +++ examples/devstral/devstral-small-qlora.yml | 64 +++ examples/magistral/README.md | 14 +- src/axolotl/datasets.py | 7 - .../prompt_strategies/chat_template.py | 24 +- src/axolotl/prompt_tokenizers.py | 8 - src/axolotl/utils/collators/batching.py | 2 +- src/axolotl/utils/mistral_tokenizer.py | 185 ++----- tests/prompt_strategies/conftest.py | 8 + .../test_chat_templates_mistral.py | 498 +++++++++++++++++- 10 files changed, 690 insertions(+), 189 deletions(-) create mode 100644 examples/devstral/README.md create mode 100644 examples/devstral/devstral-small-qlora.yml diff --git a/examples/devstral/README.md b/examples/devstral/README.md new file mode 100644 index 0000000000..9dc5377bc6 --- /dev/null +++ b/examples/devstral/README.md @@ -0,0 +1,69 @@ +# Finetune Devstral with Axolotl + +Devstral Small is a 24B parameter opensource model from MistralAI found on HuggingFace [Devstral-Small-2505](https://huggingface.co/mistralai/Devstral-Small-2505). This guide shows how to fine-tune it with Axolotl with multi-turn conversations with proper masking. + +The model was fine-tuned ontop of [Mistral-Small-3.1](https://huggingface.co/mistralai/Mistral-Small-3.1-24B-Base-2503) without the vision layer and has a context of upto 128k tokens. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Devstral is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). + + Here is an example of how to install from main for pip: + +```bash +# Ensure you have Pytorch installed (Pytorch 2.6.0+) +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl + +pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install --no-build-isolation -e '.[flash-attn]' + +# Install the latest mistral-common from source +pip3 uninstall mistral-common +pip3 install git+https://github.com/mistralai/mistral-common.git@039465d + +``` + +2. Run the finetuning example: + +```bash +axolotl train examples/devstral/devstral-small-qlora.yml +``` + +This config uses about 21GB VRAM. + +Let us know how it goes. Happy finetuning! 🚀 + +### TIPS + +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) +- [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) +- [Liger Kernel](https://docs.axolotl.ai/docs/custom_integrations.html#liger-kernels) + +## Limitations + +We only support the `mistral-common` tokenizer for Supervised Fine-tuning at the moment and for `type: chat_template` only. + +In addition, we do not support overriding tokens yet. + +## Related Resources + +- [MistralAI Devstral Blog](https://mistral.ai/news/devstral) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) + + +## Future Work + +- Add parity to Preference Tuning, RL, Multi-modal, etc. +- Add parity to other tokenizer configs like overriding tokens. diff --git a/examples/devstral/devstral-small-qlora.yml b/examples/devstral/devstral-small-qlora.yml new file mode 100644 index 0000000000..d2c5930e3c --- /dev/null +++ b/examples/devstral/devstral-small-qlora.yml @@ -0,0 +1,64 @@ +base_model: mistralai/Devstral-Small-2505 + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +# Enable to use mistral-common tokenizer +tokenizer_use_mistral_common: true + +load_in_8bit: false +load_in_4bit: true + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/qlora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true +pad_to_sequence_len: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0 +lora_target_linear: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_torch +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +loss_watchdog_threshold: 5.0 +loss_watchdog_patience: 3 + +warmup_ratio: 0.05 +evals_per_epoch: 4 +saves_per_epoch: 1 + +weight_decay: 0.0 +special_tokens: diff --git a/examples/magistral/README.md b/examples/magistral/README.md index a2b09ab700..0c39c061b2 100644 --- a/examples/magistral/README.md +++ b/examples/magistral/README.md @@ -18,16 +18,10 @@ git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn,mistral]' +pip3 install --no-build-isolation -e '.[flash-attn]' ``` -2. Download the example config: - -```bash -axolotl fetch examples -``` - -3. Run the finetuning example: +2. Run the finetuning example: ```bash axolotl train examples/magistral/magistral-small-qlora.yaml @@ -42,7 +36,7 @@ Let us know how it goes. Happy finetuning! 🚀 - For inference, the official MistralAI team recommends `top_p: 0.95` and `temperature: 0.7` with `max_tokens: 40960`. - You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. - Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). -- The dataset format is the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). ## Optimization Guides @@ -54,7 +48,7 @@ Let us know how it goes. Happy finetuning! 🚀 We only support the `mistral-common` tokenizer for Supervised Fine-tuning at the moment and for `type: chat_template` only. -The tokenizer does not work with `dataset.map` with multiprocessing, so we had to disable it. In addition, we do not support overriding tokens yet. +In addition, we do not support overriding tokens yet. ## Related Resources diff --git a/src/axolotl/datasets.py b/src/axolotl/datasets.py index 28182b16f5..7c112c59e7 100644 --- a/src/axolotl/datasets.py +++ b/src/axolotl/datasets.py @@ -48,13 +48,6 @@ def process(self, dataset): features = dataset.features.keys() num_proc = min(64, self.process_count if self.process_count else os.cpu_count()) - # Disable multiprocessing if the tokenizer doesn't support it (e.g., mistral_common) - if not getattr(self.prompt_tokenizer, "supports_multiprocessing", True): - LOG.info( - "Disabling multiprocessing for tokenizer as it doesn't support it (e.g., mistral_common)" - ) - num_proc = 1 - map_kwargs = {} if self.prompt_tokenizer.supports_batched: map_kwargs["batched"] = True diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 6d2a048b2b..a9d26a650e 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -681,13 +681,14 @@ def get_conversation_thread(self, prompt): for message in messages: transformed_message = self.transform_message(message) - turn = { - **transformed_message, - "training": message.get(self.prompter.message_field_training), - "training_detail": message.get( - self.prompter.message_field_training_detail - ), - } + turn = transformed_message + + training = message.get(self.prompter.message_field_training) + training_detail = message.get(self.prompter.message_field_training_detail) + if training is not None: + turn["training"] = training + if training_detail is not None: + turn["training_detail"] = training_detail turns.append(turn) @@ -859,15 +860,6 @@ def __init__( # TODO: address this in the future with mistral-specific checks # self._validate_eot_and_eos_tokens() - @property - def supports_multiprocessing(self) -> bool: - """ - Whether this tokenizing strategy supports multiprocessing. - mistral_common tokenizers cannot be pickled for multiprocessing. - """ - - return False - def find_first_eot_token(self, input_ids, start_idx): """Find the first EOT token in the input_ids starting from start_idx.""" # mistral-common tokenizer does not support eot_tokens diff --git a/src/axolotl/prompt_tokenizers.py b/src/axolotl/prompt_tokenizers.py index aae778ae8a..9ca645de3c 100644 --- a/src/axolotl/prompt_tokenizers.py +++ b/src/axolotl/prompt_tokenizers.py @@ -70,14 +70,6 @@ def tokenize_prompt(self, prompt): def supports_batched(self): return False - @property - def supports_multiprocessing(self): - """ - Whether this tokenizing strategy supports multiprocessing. - Should return False if the tokenizer has unpicklable objects. - """ - return True - def _tokenize( self, prompt: str, add_eos_token: bool = True, strip_bos_token: bool = False ) -> BatchEncoding: diff --git a/src/axolotl/utils/collators/batching.py b/src/axolotl/utils/collators/batching.py index a28f360be3..25a871b2ba 100644 --- a/src/axolotl/utils/collators/batching.py +++ b/src/axolotl/utils/collators/batching.py @@ -108,7 +108,7 @@ def __call__(self, features, return_tensors=None): pad_to_multiple_of=self.pad_to_multiple_of, return_tensors=return_tensors, ) - if not has_attn_mask: + if not has_attn_mask and "attention_mask" in features: del features["attention_mask"] # prepare decoder_input_ids diff --git a/src/axolotl/utils/mistral_tokenizer.py b/src/axolotl/utils/mistral_tokenizer.py index 1ba824938e..95c87a8226 100644 --- a/src/axolotl/utils/mistral_tokenizer.py +++ b/src/axolotl/utils/mistral_tokenizer.py @@ -3,10 +3,11 @@ import math import os from shutil import copyfile -from typing import TYPE_CHECKING, Optional +from typing import Optional import numpy as np from huggingface_hub import hf_hub_download +from mistral_common.protocol.instruct.request import ChatCompletionRequest from mistral_common.tokens.tokenizers.mistral import MistralTokenizer from mistral_common.tokens.tokenizers.tekken import SpecialTokenPolicy, Tekkenizer from torch import Tensor @@ -14,9 +15,6 @@ from axolotl.utils.collators.core import IGNORE_INDEX -if TYPE_CHECKING: - from mistral_common.protocol.instruct.request import ChatCompletionRequest - def _get_file_path(path_or_repo_id: str, filename: str) -> str: """Get the file path from local or HF Hub""" @@ -259,75 +257,6 @@ def decode( token_ids, special_token_policy=SpecialTokenPolicy.KEEP ) - def _create_mistral_chat_completion_request( - self, conversation: list[dict], tools: list[dict] | None = None - ) -> "ChatCompletionRequest": - from mistral_common.protocol.instruct.messages import ( - AssistantMessage, - SystemMessage, - ToolMessage, - UserMessage, - ) - from mistral_common.protocol.instruct.request import ChatCompletionRequest - from mistral_common.protocol.instruct.tool_calls import Function, Tool - - messages: list[UserMessage | AssistantMessage | ToolMessage | SystemMessage] = ( - [] - ) - for turn in conversation: - role = turn.get("role") - - if role == "user": - messages.append(UserMessage(content=turn["content"])) - elif role == "assistant": - messages.append( - AssistantMessage( - content=turn.get("content"), - tool_calls=turn.get("tool_calls"), - ) - ) - elif role == "tool": - messages.append( - ToolMessage( - content=turn.get("content"), - tool_call_id=turn.get("tool_call_id"), - name=turn.get("name"), - ) - ) - elif role == "system": - messages.append(SystemMessage(content=turn["content"])) - else: - raise ValueError( - f"Unknown role for use with mistral-common tokenizer: {turn['role']}" - ) - - tool_calls: list[Tool] = [] - if tools: - # convert to Tool - for tool in tools: - if tool["type"] != "function": - continue - - function = tool["function"] - - tool_calls.append( - Tool( - function=Function( - name=function["name"], - description=function["description"], - # set parameters to empty dict if not provided - parameters=function.get("parameters", {}), - ) - ) - ) - - chat_completion: ChatCompletionRequest = ChatCompletionRequest( - messages=messages, - tools=tool_calls, - ) - - return chat_completion - def apply_chat_template( self, messages: list[dict], @@ -342,8 +271,8 @@ def apply_chat_template( if add_generation_prompt: raise NotImplementedError("add_generation_prompt not supported yet") - chat_completion: ChatCompletionRequest = ( - self._create_mistral_chat_completion_request(messages, tools) + chat_completion: ChatCompletionRequest = ChatCompletionRequest.from_openai( + messages, tools ) tokens: list[int] = self._mistral.encode_chat_completion(chat_completion).tokens @@ -408,13 +337,16 @@ def pad( padding_value=IGNORE_INDEX, ) - attention_mask = torch.nn.utils.rnn.pad_sequence( - [torch.tensor(x["attention_mask"], dtype=torch.long) for x in features], - batch_first=True, - padding_value=0, - ) + attention_mask = None + if "attention_mask" in features[0]: + attention_mask = torch.nn.utils.rnn.pad_sequence( + [torch.tensor(x["attention_mask"], dtype=torch.long) for x in features], + batch_first=True, + padding_value=0, + ) # Handle position_ids - pad with sequential values for right padding, 0s for left padding + position_ids = None if "position_ids" in features[0]: if self.padding_side == "left": # Likely not needed, but keeping for now @@ -443,22 +375,15 @@ def pad( pos_seq = torch.cat([pos_seq, pad_positions]) position_ids_list.append(pos_seq) position_ids = torch.stack(position_ids_list) - else: - # Create position_ids if not present - seq_len = input_ids.size(1) - position_ids = ( - torch.arange(seq_len, dtype=torch.long) - .unsqueeze(0) - .expand(input_ids.size(0), -1) - ) # Ensure all tensors have the same sequence length - max_seq_len = max( - input_ids.size(1), - labels.size(1), - attention_mask.size(1), - position_ids.size(1), - ) + # Check attention mask and position ids if they are present + tensor_lengths = [input_ids.size(1), labels.size(1)] + if attention_mask is not None: + tensor_lengths.append(attention_mask.size(1)) + if position_ids is not None: + tensor_lengths.append(position_ids.size(1)) + max_seq_len = max(tensor_lengths) # TODO: check if trimming is needed? and correct. @@ -492,44 +417,48 @@ def pad( elif labels.size(1) > max_seq_len: labels = labels[:, :max_seq_len] - if attention_mask.size(1) < max_seq_len: - pad_len = max_seq_len - attention_mask.size(1) - if self.padding_side == "right": - attention_mask = F.pad(attention_mask, (0, pad_len), value=0) - else: - attention_mask = F.pad(attention_mask, (pad_len, 0), value=0) - elif attention_mask.size(1) > max_seq_len: - attention_mask = attention_mask[:, :max_seq_len] - - if position_ids.size(1) < max_seq_len: - pad_len = max_seq_len - position_ids.size(1) - if self.padding_side == "right": - batch_size = position_ids.size(0) - new_position_ids = [] - for i in range(batch_size): - seq = position_ids[i] - if len(seq) > 0: - # get last position and pad with sequential values - last_pos = seq[-1].item() - pad_positions = torch.arange( - last_pos + 1, last_pos + 1 + pad_len, dtype=torch.long - ) - new_seq = torch.cat([seq, pad_positions]) - else: - new_seq = torch.arange(pad_len, dtype=torch.long) - new_position_ids.append(new_seq) - position_ids = torch.stack(new_position_ids) - else: - position_ids = F.pad(position_ids, (pad_len, 0), value=0) - elif position_ids.size(1) > max_seq_len: - position_ids = position_ids[:, :max_seq_len] + if attention_mask is not None: + if attention_mask.size(1) < max_seq_len: + pad_len = max_seq_len - attention_mask.size(1) + if self.padding_side == "right": + attention_mask = F.pad(attention_mask, (0, pad_len), value=0) + else: + attention_mask = F.pad(attention_mask, (pad_len, 0), value=0) + elif attention_mask.size(1) > max_seq_len: + attention_mask = attention_mask[:, :max_seq_len] + + if position_ids is not None: + if position_ids.size(1) < max_seq_len: + pad_len = max_seq_len - position_ids.size(1) + if self.padding_side == "right": + batch_size = position_ids.size(0) + new_position_ids = [] + for i in range(batch_size): + seq = position_ids[i] + if len(seq) > 0: + # get last position and pad with sequential values + last_pos = seq[-1].item() + pad_positions = torch.arange( + last_pos + 1, last_pos + 1 + pad_len, dtype=torch.long + ) + new_seq = torch.cat([seq, pad_positions]) + else: + new_seq = torch.arange(pad_len, dtype=torch.long) + new_position_ids.append(new_seq) + position_ids = torch.stack(new_position_ids) + else: + position_ids = F.pad(position_ids, (pad_len, 0), value=0) + elif position_ids.size(1) > max_seq_len: + position_ids = position_ids[:, :max_seq_len] final_batch = { "input_ids": input_ids, "labels": labels, - "attention_mask": attention_mask, - "position_ids": position_ids, } + if attention_mask is not None: + final_batch["attention_mask"] = attention_mask + if position_ids is not None: + final_batch["position_ids"] = position_ids # Handle non-sequence fields (raise error) sequence_fields = {"input_ids", "labels", "attention_mask", "position_ids"} @@ -545,7 +474,7 @@ def pad( result = {} for k, v in final_batch.items(): if isinstance(v, torch.Tensor): - result[k] = v.numpy().astype(np.long) + result[k] = v.numpy().astype(np.int64) else: result[k] = v return result diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index d440565d2e..60b14d6523 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -164,6 +164,14 @@ def fixture_magistral_tokenizer(): return tokenizer +@pytest.fixture(name="devstral_tokenizer") +def fixture_devstral_tokenizer(): + from axolotl.utils.mistral_tokenizer import HFMistralTokenizer + + tokenizer = HFMistralTokenizer.from_pretrained("mistralai/Devstral-Small-2505") + return tokenizer + + @pytest.fixture(name="mistralv03_tokenizer_chat_template_jinja") def fixture_mistralv03_chat_template_jinja_w_system() -> str: return '{%- if messages[0]["role"] == "system" %}\n {%- set system_message = messages[0]["content"] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set loop_messages = messages %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n{%- set user_messages = loop_messages | selectattr("role", "equalto", "user") | list %}\n\n{#- This block checks for alternating user/assistant messages, skipping tool calling messages #}\n{%- set ns = namespace() %}\n{%- set ns.index = 0 %}\n{%- for message in loop_messages %}\n {%- if not (message.role == "tool" or message.role == "tool_results" or (message.tool_calls is defined and message.tool_calls is not none)) %}\n {%- if (message["role"] == "user") != (ns.index % 2 == 0) %}\n {{- raise_exception("After the optional system message, conversation roles must alternate user/assistant/user/assistant/...") }}\n {%- endif %}\n {%- set ns.index = ns.index + 1 %}\n {%- endif %}\n{%- endfor %}\n\n{{- bos_token }}\n{%- for message in loop_messages %}\n {%- if message["role"] == "user" %}\n {%- if tools is not none and (message == user_messages[-1]) %}\n {{- "[AVAILABLE_TOOLS] [" }}\n {%- for tool in tools %}\n {%- set tool = tool.function %}\n {{- \'{"type": "function", "function": {\' }}\n {%- for key, val in tool.items() if key != "return" %}\n {%- if val is string %}\n {{- \'"\' + key + \'": "\' + val + \'"\' }}\n {%- else %}\n {{- \'"\' + key + \'": \' + val|tojson }}\n {%- endif %}\n {%- if not loop.last %}\n {{- ", " }}\n {%- endif %}\n {%- endfor %}\n {{- "}}" }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- else %}\n {{- "]" }}\n {%- endif %}\n {%- endfor %}\n {{- "[/AVAILABLE_TOOLS]" }}\n {%- endif %}\n {%- if loop.first and system_message is defined %}\n {{- "[INST] " + system_message + "\\n\\n" + message["content"] + "[/INST]" }}\n {%- else %}\n {{- "[INST] " + message["content"] + "[/INST]" }}\n {%- endif %}\n {%- elif message.tool_calls is defined and message.tool_calls is not none %}\n {{- "[TOOL_CALLS] [" }}\n {%- for tool_call in message.tool_calls %}\n {%- set out = tool_call.function|tojson %}\n {{- out[:-1] }}\n {%- if not tool_call.id is defined or tool_call.id|length != 9 %}\n {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }}\n {%- endif %}\n {{- \', "id": "\' + tool_call.id + \'"}\' }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- else %}\n {{- "]" + eos_token }}\n {%- endif %}\n {%- endfor %}\n {%- elif message["role"] == "assistant" %}\n {{- " " + message["content"]|trim + eos_token}}\n {%- elif message["role"] == "tool_results" or message["role"] == "tool" %}\n {%- if message.content is defined and message.content.content is defined %}\n {%- set content = message.content.content %}\n {%- else %}\n {%- set content = message.content %}\n {%- endif %}\n {{- \'[TOOL_RESULTS] {"content": \' + content|string + ", " }}\n {%- if not message.tool_call_id is defined or message.tool_call_id|length != 9 %}\n {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }}\n {%- endif %}\n {{- \'"call_id": "\' + message.tool_call_id + \'"}[/TOOL_RESULTS]\' }}\n {%- else %}\n {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }}\n {%- endif %}\n{%- endfor %}\n' diff --git a/tests/prompt_strategies/test_chat_templates_mistral.py b/tests/prompt_strategies/test_chat_templates_mistral.py index 3c60a15c27..dcf5138d38 100644 --- a/tests/prompt_strategies/test_chat_templates_mistral.py +++ b/tests/prompt_strategies/test_chat_templates_mistral.py @@ -3,32 +3,50 @@ import unittest from typing import TYPE_CHECKING +import pytest + if TYPE_CHECKING: from axolotl.utils.mistral_tokenizer import HFMistralTokenizer -def test_magistral_chat_template(magistral_tokenizer: "HFMistralTokenizer"): +# fmt: off +@pytest.mark.parametrize( + ("tokenizer_str", "assistant_toolcall_ids"), + ( + ("magistral_tokenizer", (9, 44627, 3684, 33, 19881, 1049, 1050, 1051, 1052, 1053, 32, 19227, 12856, 2811, 1032, 1049, 1054, 1044, 1429, 33319, 2811, 1032, 1050, 1125, 2)), + ("devstral_tokenizer", (9, 1091, 19227, 2391, 2811, 1429, 44627, 3684, 1897, 1429, 61906, 2811, 16753, 12856, 2811, 1032, 1049, 1054, 1044, 1429, 33319, 2811, 1032, 1050, 4179, 1429, 1327, 2811, 1429, 19881, 1049, 1050, 1051, 1052, 1053, 1034, 27028, 2)), + ) +) +# fmt: on +def test_mistral_chat_template( + tokenizer_str: str, + assistant_toolcall_ids: tuple[int, ...], + request: pytest.FixtureRequest, +): + """Test chat template with the Magistral/Devstral tokenizer""" # pylint: disable=duplicate-code from axolotl.prompt_strategies.chat_template import MistralPrompter, MistralStrategy + tokenizer: HFMistralTokenizer = request.getfixturevalue(tokenizer_str) + # check bos, eos, pad, unk are accessible properties - assert magistral_tokenizer.bos_token_id == 1 - assert magistral_tokenizer.eos_token_id == 2 - assert magistral_tokenizer.pad_token_id == 11 - assert magistral_tokenizer.unk_token_id == 0 + assert tokenizer.bos_token_id == 1 + assert tokenizer.eos_token_id == 2 + assert tokenizer.pad_token_id == 11 + assert tokenizer.unk_token_id == 0 - assert magistral_tokenizer.pad_token == "" - assert magistral_tokenizer.eos_token == "
" - assert magistral_tokenizer.bos_token == "" - assert magistral_tokenizer.unk_token == "" + assert tokenizer.pad_token == "" + assert tokenizer.eos_token == "" + assert tokenizer.bos_token == "" + assert tokenizer.unk_token == "" strategy = MistralStrategy( MistralPrompter( - magistral_tokenizer, + tokenizer, chat_template=None, message_property_mappings={"role": "role", "content": "content"}, ), - tokenizer=magistral_tokenizer, + tokenizer=tokenizer, train_on_inputs=False, train_on_eos="turn", sequence_len=512, @@ -219,7 +237,7 @@ def test_magistral_chat_template(magistral_tokenizer: "HFMistralTokenizer"): 1, # bos 5, 1091, 19227, 4994, 2811, 1429, 5165, 1897, 1429, 5165, 2811, 16753, 2391, 2811, 1429, 44627, 3684, 1897, 1429, 14653, 2811, 1429, 10639, 2130, 1261, 2951, 1307, 1747, 1278, 60092, 1307, 1261, 2782, 1455, 1584, 4289, 2224, 1261, 4265, 6139, 39249, 1429, 26204, 2811, 16753, 4994, 2811, 1429, 6371, 1897, 1429, 48649, 2811, 16753, 12856, 2811, 16753, 4994, 2811, 1429, 49039, 1897, 1429, 14653, 2811, 1429, 1784, 2782, 1317, 3081, 60092, 1307, 2613, 4179, 1429, 33319, 2811, 16753, 4994, 2811, 1429, 49039, 1897, 1429, 14653, 2811, 1429, 1784, 9229, 6139, 1394, 1278, 60092, 2613, 47579, 1429, 15760, 2811, 12161, 12856, 1897, 1429, 33319, 4964, 2821, 27028, 6, # tool prompt 3, 46634, 1044, 1710, 1636, 5628, 1639, 1261, 44433, 1307, 2606, 1317, 5388, 1420, 54191, 2424, 1286, 8967, 1063, 15621, 1044, 2549, 30305, 2196, 3560, 1044, 1321, 2606, 1710, 1362, 2016, 8605, 2015, 1317, 5524, 118931, 2036, 32951, 1063, 1362, 2933, 2269, 12106, 1408, 101987, 1044, 6939, 1044, 1321, 9216, 1455, 2084, 3180, 1278, 8967, 119141, 1689, 5935, 1033, 4, # user - 9, 44627, 3684, 33, 19881, 1049, 1050, 1051, 1052, 1053, 32, 19227, 12856, 2811, 1032, 1049, 1054, 1044, 1429, 33319, 2811, 1032, 1050, 1125, 2, # assistant tool calling + *assistant_toolcall_ids, # assistant tool calling 7, 19881, 1049, 1050, 1051, 1052, 1053, 19, 1049, 1044, 1050, 8, # tool result 1784, 60092, 1307, 1032, 1049, 1054, 1395, 1032, 1049, 1321, 1032, 1050, 1046, # assistant 2 # eos @@ -229,7 +247,7 @@ def test_magistral_chat_template(magistral_tokenizer: "HFMistralTokenizer"): -100, # bos -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # tool prompt -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # user prompt - 9, 44627, 3684, 33, 19881, 1049, 1050, 1051, 1052, 1053, 32, 19227, 12856, 2811, 1032, 1049, 1054, 1044, 1429, 33319, 2811, 1032, 1050, 1125, 2, # assistant tool calling + *assistant_toolcall_ids, # assistant tool calling -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # tool result 1784, 60092, 1307, 1032, 1049, 1054, 1395, 1032, 1049, 1321, 1032, 1050, 1046, # assistant 2 # eos @@ -237,7 +255,7 @@ def test_magistral_chat_template(magistral_tokenizer: "HFMistralTokenizer"): # fmt: on # test chat template with tokenize=False - res = magistral_tokenizer.apply_chat_template( + res = tokenizer.apply_chat_template( [ {"role": "user", "content": "Hello, how are you?"}, {"role": "assistant", "content": "I'm doing great, thank you!"}, @@ -248,7 +266,7 @@ def test_magistral_chat_template(magistral_tokenizer: "HFMistralTokenizer"): assert res == "[INST]Hello, how are you?[/INST]I'm doing great, thank you!" # test encode - res = magistral_tokenizer.encode("Hello, how are you?", add_special_tokens=True) + res = tokenizer.encode("Hello, how are you?", add_special_tokens=True) assert res == [ 1, # bos 22177, # Hello @@ -261,16 +279,16 @@ def test_magistral_chat_template(magistral_tokenizer: "HFMistralTokenizer"): ] # test decode no skip special tokens - decoded_res = magistral_tokenizer.decode(res, skip_special_tokens=False) + decoded_res = tokenizer.decode(res, skip_special_tokens=False) assert decoded_res == "Hello, how are you?" # test decode skip special tokens - decoded_res = magistral_tokenizer.decode(res, skip_special_tokens=True) + decoded_res = tokenizer.decode(res, skip_special_tokens=True) assert decoded_res == "Hello, how are you?" # test encode no special tokens - res = magistral_tokenizer.encode("Hello, how are you?", add_special_tokens=False) + res = tokenizer.encode("Hello, how are you?", add_special_tokens=False) assert res == [ 22177, # Hello 1044, # , @@ -281,10 +299,452 @@ def test_magistral_chat_template(magistral_tokenizer: "HFMistralTokenizer"): ] # test convert ids to tokens - res = magistral_tokenizer.convert_ids_to_tokens(res) + res = tokenizer.convert_ids_to_tokens(res) # spacing are needed as we are converting without decoding assert res == ["Hello", ",", " how", " are", " you", "?"] +def test_magistral_tokenizer_pad_method(magistral_tokenizer: "HFMistralTokenizer"): + """Test the MistralTokenizer pad method""" + from axolotl.utils.collators.core import IGNORE_INDEX + + magistral_pad_token_id = 11 # taken from tokenizer.pad_token_id + + # Test padding with input_ids and labels only + features = [ + {"input_ids": [1, 2, 3], "labels": [4, 5, 6]}, + {"input_ids": [7, 8], "labels": [9, 10]}, + ] + + result = magistral_tokenizer.pad(features, padding=True, return_tensors="pt") + + # Check that input_ids are padded correctly + assert result["input_ids"].shape == (2, 3) + assert result["input_ids"].tolist() == [[1, 2, 3], [7, 8, magistral_pad_token_id]] + + # Check that labels are padded correctly + assert result["labels"].shape == (2, 3) + assert result["labels"].tolist() == [[4, 5, 6], [9, 10, IGNORE_INDEX]] + + # Check that attention_mask and position_ids are NOT created + assert "attention_mask" not in result + assert "position_ids" not in result + + # Test padding with attention_mask + features_with_attention = [ + {"input_ids": [1, 2, 3], "labels": [4, 5, 6], "attention_mask": [1, 1, 1]}, + {"input_ids": [7, 8], "labels": [9, 10], "attention_mask": [1, 1]}, + ] + + result = magistral_tokenizer.pad( + features_with_attention, padding=True, return_tensors="pt" + ) + + # Check that attention_mask is padded correctly + assert result["attention_mask"].shape == (2, 3) + assert result["attention_mask"].tolist() == [[1, 1, 1], [1, 1, 0]] + + # Test padding with position_ids + features_with_position = [ + {"input_ids": [1, 2, 3], "labels": [4, 5, 6], "position_ids": [0, 1, 2]}, + {"input_ids": [7, 8], "labels": [9, 10], "position_ids": [0, 1]}, + ] + + result = magistral_tokenizer.pad( + features_with_position, padding=True, return_tensors="pt" + ) + + # Check that position_ids are padded correctly (continuing sequence) + assert result["position_ids"].shape == (2, 3) + assert result["position_ids"].tolist() == [[0, 1, 2], [0, 1, 2]] + + # Test padding with all fields + features_all = [ + { + "input_ids": [1, 2, 3], + "labels": [4, 5, 6], + "attention_mask": [1, 1, 1], + "position_ids": [0, 1, 2], + }, + { + "input_ids": [7, 8], + "labels": [9, 10], + "attention_mask": [1, 1], + "position_ids": [0, 1], + }, + ] + + result = magistral_tokenizer.pad(features_all, padding=True, return_tensors="pt") + + # All fields should be present and correctly padded + assert "input_ids" in result + assert "labels" in result + assert "attention_mask" in result + assert "position_ids" in result + + # Test padding with all sequences same length + features_same_length = [ + {"input_ids": [1, 2, 3], "labels": [4, 5, 6]}, + {"input_ids": [7, 8, 9], "labels": [10, 11, 12]}, + ] + + result = magistral_tokenizer.pad( + features_same_length, padding=True, return_tensors="pt" + ) + + # Check match when no padding is needed + assert result["input_ids"][0].tolist() == features_same_length[0]["input_ids"] + assert result["labels"][0].tolist() == features_same_length[0]["labels"] + + assert result["input_ids"][1].tolist() == features_same_length[1]["input_ids"] + assert result["labels"][1].tolist() == features_same_length[1]["labels"] + + # Test padding with max_length parameter + result = magistral_tokenizer.pad( + features, padding="max_length", max_length=5, return_tensors="pt" + ) + + # Should pad to max_length + assert result["input_ids"].shape == (2, 5) + assert result["labels"].shape == (2, 5) + + # Test numpy return type + result = magistral_tokenizer.pad(features, padding=True, return_tensors="np") + + # Should return numpy arrays + import numpy as np + + assert isinstance(result["input_ids"], np.ndarray) + assert isinstance(result["labels"], np.ndarray) + + # Test unsupported field rejection + features_unsupported = [ + {"input_ids": [1, 2, 3], "labels": [4, 5, 6], "unsupported_field": [7, 8, 9]}, + ] + + with pytest.raises(NotImplementedError, match="unsupported_field"): + magistral_tokenizer.pad(features_unsupported, padding=True, return_tensors="pt") + + # Test token_type_ids rejection + features_token_type = [ + {"input_ids": [1, 2, 3], "labels": [4, 5, 6], "token_type_ids": [0, 0, 0]}, + ] + + with pytest.raises(ValueError, match="token_type_ids is not supported"): + magistral_tokenizer.pad(features_token_type, padding=True, return_tensors="pt") + + +def test_magistral_tool_calling(magistral_tokenizer: "HFMistralTokenizer"): + """Test tool calling with the Magistral tokenizer""" + from axolotl.prompt_strategies.chat_template import MistralPrompter, MistralStrategy + + strategy = MistralStrategy( + MistralPrompter( + magistral_tokenizer, + chat_template=None, + message_property_mappings={"role": "role", "content": "content"}, + ), + tokenizer=magistral_tokenizer, + train_on_inputs=False, + train_on_eos="turn", + sequence_len=512, + roles_to_train=["assistant"], + ) + + # Test basic tool calling with single function + basic_tool_calling = { + "tools": [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get the current weather for a location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + }, + "required": ["location"], + }, + }, + }, + ], + "messages": [ + { + "role": "user", + "content": "What's the weather like in San Francisco?", + }, + { + "role": "assistant", + "tool_calls": [ + { + "id": "call12345", + "type": "function", + "function": { + "name": "get_weather", + "arguments": { + "location": "San Francisco, CA", + }, + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call12345", + "name": "get_weather", + "content": "Sunny, 72°F", + }, + { + "role": "assistant", + "content": "The weather in San Francisco is sunny and 72°F.", + }, + ], + } + + res = strategy.tokenize_prompt(basic_tool_calling) + + # Basic validation + assert "input_ids" in res + assert "labels" in res + assert len(res["input_ids"]) > 0 + assert len(res["labels"]) == len(res["input_ids"]) + + # Decode and verify structure + decoded = magistral_tokenizer.decode(res["input_ids"]) + assert ( + '[AVAILABLE_TOOLS][{"type": "function", "function": {"name": "get_weather", "description": "Get the current weather for a location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city and state, e.g. San Francisco, CA"}}, "required": ["location"]}}}][/AVAILABLE_TOOLS]' + in decoded + ) + assert ( + '[TOOL_CALLS]get_weather[CALL_ID]call12345[ARGS]{"location": "San Francisco, CA"}' + in decoded + ) + assert "[TOOL_RESULTS]call12345[TOOL_CONTENT]Sunny, 72°F[/TOOL_RESULTS]" in decoded + assert "The weather in San Francisco is sunny and 72°F." in decoded + + # Test multiple tool calls in sequence + multi_tool_calling = { + "tools": [ + { + "type": "function", + "function": { + "name": "add_numbers", + "description": "Add two numbers together", + "parameters": { + "type": "object", + "properties": { + "a": {"type": "number", "description": "First number"}, + "b": {"type": "number", "description": "Second number"}, + }, + "required": ["a", "b"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "multiply_numbers", + "description": "Multiply two numbers", + "parameters": { + "type": "object", + "properties": { + "x": {"type": "number", "description": "First number"}, + "y": {"type": "number", "description": "Second number"}, + }, + "required": ["x", "y"], + }, + }, + }, + ], + "messages": [ + { + "role": "user", + "content": "Add 5 and 3, then multiply the result by 2", + }, + { + "role": "assistant", + "tool_calls": [ + { + "id": "call12345", + "type": "function", + "function": { + "name": "add_numbers", + "arguments": {"a": 5, "b": 3}, + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call12345", + "name": "add_numbers", + "content": "8", + }, + { + "role": "assistant", + "tool_calls": [ + { + "id": "call23456", + "type": "function", + "function": { + "name": "multiply_numbers", + "arguments": {"x": 8, "y": 2}, + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call23456", + "name": "multiply_numbers", + "content": "16", + }, + { + "role": "assistant", + "content": "The result is 16. I first added 5 and 3 to get 8, then multiplied 8 by 2 to get 16.", + }, + ], + } + + res = strategy.tokenize_prompt(multi_tool_calling) + + # Validation + assert len(res["input_ids"]) > 0 + assert len(res["labels"]) == len(res["input_ids"]) + + decoded = magistral_tokenizer.decode(res["input_ids"]) + assert ( + '[AVAILABLE_TOOLS][{"type": "function", "function": {"name": "add_numbers", "description": "Add two numbers together", "parameters": {"type": "object", "properties": {"a": {"type": "number", "description": "First number"}, "b": {"type": "number", "description": "Second number"}}, "required": ["a", "b"]}}}, {"type": "function", "function": {"name": "multiply_numbers", "description": "Multiply two numbers", "parameters": {"type": "object", "properties": {"x": {"type": "number", "description": "First number"}, "y": {"type": "number", "description": "Second number"}}, "required": ["x", "y"]}}}][/AVAILABLE_TOOLS]' + in decoded + ) + assert ( + '[TOOL_CALLS]add_numbers[CALL_ID]call12345[ARGS]{"a": 5, "b": 3}' in decoded + ) + assert "[TOOL_RESULTS]call12345[TOOL_CONTENT]8[/TOOL_RESULTS]" in decoded + assert ( + '[TOOL_CALLS]multiply_numbers[CALL_ID]call23456[ARGS]{"x": 8, "y": 2}' + in decoded + ) + assert "[TOOL_RESULTS]call23456[TOOL_CONTENT]16[/TOOL_RESULTS]" in decoded + assert ( + "The result is 16. I first added 5 and 3 to get 8, then multiplied 8 by 2 to get 16." + in decoded + ) + + # Test tool calling with system message + system_tool_calling = { + "tools": [ + { + "type": "function", + "function": { + "name": "search_database", + "description": "Search for information in database", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query"}, + }, + "required": ["query"], + }, + }, + }, + ], + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant with access to a database.", + }, + { + "role": "user", + "content": "Find information about Python programming", + }, + { + "role": "assistant", + "tool_calls": [ + { + "id": "search123", + "type": "function", + "function": { + "name": "search_database", + "arguments": {"query": "Python programming"}, + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "search123", + "name": "search_database", + "content": "Python is a high-level programming language known for its simplicity.", + }, + { + "role": "assistant", + "content": "Based on the database search, Python is a high-level programming language known for its simplicity and readability.", + }, + ], + } + + res = strategy.tokenize_prompt(system_tool_calling) + + # Validation + assert len(res["input_ids"]) > 0 + assert len(res["labels"]) == len(res["input_ids"]) + + decoded = magistral_tokenizer.decode(res["input_ids"]) + + assert ( + '[SYSTEM_PROMPT]You are a helpful assistant with access to a database.[/SYSTEM_PROMPT][AVAILABLE_TOOLS][{"type": "function", "function": {"name": "search_database", "description": "Search for information in database", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "Search query"}}, "required": ["query"]}}}][/AVAILABLE_TOOLS]' + in decoded + ) + + # Test error handling - missing tool response + incomplete_tool_calling = { + "tools": [ + { + "type": "function", + "function": { + "name": "get_time", + "description": "Get current time", + "parameters": {"type": "object", "properties": {}}, + }, + }, + ], + "messages": [ + { + "role": "user", + "content": "What time is it?", + }, + { + "role": "assistant", + "tool_calls": [ + { + "id": "time12345", + "type": "function", + "function": { + "name": "get_time", + "arguments": {}, + }, + } + ], + }, + { + "role": "assistant", + "content": "The current time is 12:00 PM.", + }, + ], + } + + from mistral_common.exceptions import InvalidMessageStructureException + + try: + strategy.tokenize_prompt(incomplete_tool_calling) + except InvalidMessageStructureException as e: + assert "Not the same number of function calls and responses" in str(e) + + if __name__ == "__main__": unittest.main() From 6ed501f6dc0ae93311305bdbb7ac1de5f38ce56f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 8 Jul 2025 16:28:14 -0400 Subject: [PATCH 0780/1405] add 2.7.0 torch images back to support vlllm (#2885) --- .github/workflows/base.yml | 7 +++++++ .github/workflows/main.yml | 10 ++++++++++ setup.py | 4 ++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 1b9862d85c..455f460957 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -46,6 +46,13 @@ jobs: pytorch: 2.6.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" + - cuda: "126" + cuda_version: 12.6.3 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.7.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-base" - cuda: "126" cuda_version: 12.6.3 cudnn_version: "" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 29cd2556d5..a43dbac413 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,6 +25,11 @@ jobs: python_version: "3.11" pytorch: 2.6.0 axolotl_extras: vllm + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.0 + axolotl_extras: vllm - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" @@ -93,6 +98,11 @@ jobs: pytorch: 2.6.0 axolotl_extras: is_latest: true + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.0 + axolotl_extras: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" diff --git a/setup.py b/setup.py index c222d0ad4a..1b29cd44d3 100644 --- a/setup.py +++ b/setup.py @@ -66,8 +66,8 @@ def parse_requirements(extras_require_map): if (major, minor) >= (2, 7): _install_requires.pop(_install_requires.index(xformers_version)) - # _install_requires.append("xformers==0.0.29.post3") # xformers seems to be hard pinned to 2.6.0 - extras_require_map["vllm"] = ["vllm==0.8.5.post1"] + _install_requires.append("xformers==0.0.31.post1") + extras_require_map["vllm"] = ["vllm>=0.9.0"] elif (major, minor) >= (2, 6): _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append( From 89e99eaaa795a82c92ab0a5d181a8ecc7c5db391 Mon Sep 17 00:00:00 2001 From: salman Date: Wed, 9 Jul 2025 13:43:26 +0100 Subject: [PATCH 0781/1405] slowest durations (#2887) [skip ci] --- .github/workflows/tests-nightly.yml | 6 +++--- .github/workflows/tests.yml | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index e6a1d8e589..8a51153d60 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -80,9 +80,9 @@ jobs: - name: Run tests run: | - pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ - pytest -v tests/patched/ - pytest -v tests/cli/ + pytest -v --durations=10 -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ + pytest -v --durations=10 tests/patched/ + pytest -v --durations=10 tests/cli/ - name: cleanup pip cache run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b489e27b85..36a90e2740 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -102,9 +102,9 @@ jobs: - name: Run tests run: | - pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ --cov=axolotl --cov-report=xml - pytest -v tests/patched/ --cov=axolotl --cov-append --cov-report=xml - pytest -v tests/cli/ --cov=axolotl --cov-append --cov-report=xml + pytest -v --durations=10 -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ --cov=axolotl --cov-report=xml + pytest -v --durations=10 tests/patched/ --cov=axolotl --cov-append --cov-report=xml + pytest -v --durations=10 tests/cli/ --cov=axolotl --cov-append --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 @@ -175,9 +175,9 @@ jobs: - name: Run tests run: | - pytest -v -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ - pytest -v tests/patched/ - pytest -v tests/cli/ + pytest -v --durations=10 -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ + pytest -v --durations=10 tests/patched/ + pytest -v --durations=10 tests/cli/ - name: cleanup pip cache run: | From 4ff96a25263b2d4cadfa9fc81963110d8a6916d3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 9 Jul 2025 08:43:40 -0400 Subject: [PATCH 0782/1405] fix xformers version (#2888) --- .github/workflows/tests.yml | 4 ++-- setup.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 36a90e2740..f98d2bedd3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,7 +52,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.5.1", "2.6.0", "2.7.1"] + pytorch_version: ["2.5.1", "2.6.0", "2.7.0", "2.7.1"] timeout-minutes: 20 steps: @@ -125,7 +125,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.5.1", "2.6.0", "2.7.1"] + pytorch_version: ["2.5.1", "2.6.0", "2.7.0", "2.7.1"] timeout-minutes: 20 steps: diff --git a/setup.py b/setup.py index 1b29cd44d3..731fe8a6f3 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,10 @@ def parse_requirements(extras_require_map): if (major, minor) >= (2, 7): _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers==0.0.31.post1") + if patch == 0: + _install_requires.append("xformers==0.0.30") + else: + _install_requires.append("xformers==0.0.31.post1") extras_require_map["vllm"] = ["vllm>=0.9.0"] elif (major, minor) >= (2, 6): _install_requires.pop(_install_requires.index(xformers_version)) From c6d69d5c1bb5c55ed67465c3824432d3413fabf0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 9 Jul 2025 09:22:35 -0400 Subject: [PATCH 0783/1405] release v0.11.0 (#2875) * release v0.11.0 * don't build vllm into release for now * remove 2.5.1 references * smollm3 multipack support * fix ordering of e2e tests --- .github/workflows/base.yml | 7 ------- .github/workflows/main.yml | 12 +----------- .github/workflows/multi-gpu-e2e.yml | 7 ------- .github/workflows/nightlies.yml | 11 +++-------- .github/workflows/tests-nightly.yml | 2 +- .github/workflows/tests.yml | 18 +++--------------- README.md | 2 +- cicd/multigpu.py | 6 +++--- cicd/single_gpu.py | 6 +++--- docs/docker.qmd | 4 +--- docs/installation.qmd | 2 +- src/axolotl/__init__.py | 2 +- src/axolotl/integrations/kd/README.md | 2 +- src/axolotl/monkeypatch/multipack.py | 1 + src/axolotl/utils/schemas/config.py | 6 +++--- tests/patched/test_validation.py | 6 +++--- tests/test_validation_dataset.py | 8 ++++---- 17 files changed, 30 insertions(+), 72 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 455f460957..2ffe85fe49 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -25,13 +25,6 @@ jobs: fail-fast: false matrix: include: - - cuda: "124" - cuda_version: 12.4.1 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.5.1 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-base" - cuda: "124" cuda_version: 12.4.1 cudnn_version: "" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a43dbac413..8692496f1f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,16 +15,11 @@ jobs: fail-fast: false matrix: include: - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.5.1 - axolotl_extras: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.6.0 - axolotl_extras: vllm + axolotl_extras: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" @@ -87,11 +82,6 @@ jobs: strategy: matrix: include: - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.5.1 - axolotl_extras: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 09d9663a9c..6180faf962 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -33,13 +33,6 @@ jobs: axolotl_extras: num_gpus: 2 nightly_build: "true" - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.5.1 - axolotl_extras: - num_gpus: 2 - nightly_build: "true" - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 4e61984fb3..824c7e4f27 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -12,11 +12,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.5.1 - axolotl_extras: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" @@ -68,10 +63,10 @@ jobs: - cuda: 124 cuda_version: 12.4.1 python_version: "3.11" - pytorch: 2.5.1 + pytorch: 2.6.0 axolotl_extras: - - cuda: 124 - cuda_version: 12.4.1 + - cuda: 126 + cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.6.0 axolotl_extras: diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 8a51153d60..b5dd50a3c7 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -26,7 +26,7 @@ jobs: max-parallel: 2 matrix: python_version: ["3.11"] - pytorch_version: ["2.5.1", "2.6.0", "2.7.0"] + pytorch_version: ["2.6.0", "2.7.0"] timeout-minutes: 20 steps: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f98d2bedd3..9c983ad707 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,7 +52,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.5.1", "2.6.0", "2.7.0", "2.7.1"] + pytorch_version: ["2.6.0", "2.7.0", "2.7.1"] timeout-minutes: 20 steps: @@ -125,7 +125,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.5.1", "2.6.0", "2.7.0", "2.7.1"] + pytorch_version: ["2.6.0", "2.7.0", "2.7.1"] timeout-minutes: 20 steps: @@ -198,7 +198,7 @@ jobs: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" - pytorch: 2.6.0 + pytorch: 2.7.1 num_gpus: 1 axolotl_extras: - cuda: 126 @@ -252,18 +252,6 @@ jobs: python_version: "3.11" pytorch: 2.6.0 num_gpus: 1 - axolotl_extras: llmcompressor - - cuda: 124 - cuda_version: 12.4.1 - python_version: "3.11" - pytorch: 2.5.1 - num_gpus: 1 - axolotl_extras: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.7.1 - num_gpus: 1 axolotl_extras: - cuda: 128 cuda_version: 12.8.1 diff --git a/README.md b/README.md index ec9fc9f7fd..406781039e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Features: - NVIDIA GPU (Ampere or newer for `bf16` and Flash Attention) or AMD GPU - Python 3.11 -- PyTorch ≥2.5.1 +- PyTorch ≥2.6.0 ### Installation diff --git a/cicd/multigpu.py b/cicd/multigpu.py index 848110a841..2c067f1437 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -24,9 +24,9 @@ df_args = { "AXOLOTL_EXTRAS": os.environ.get("AXOLOTL_EXTRAS", ""), "AXOLOTL_ARGS": os.environ.get("AXOLOTL_ARGS", ""), - "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.5.1"), - "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu124-2.5.1"), - "CUDA": os.environ.get("CUDA", "124"), + "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.6.0"), + "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu126-2.6.0"), + "CUDA": os.environ.get("CUDA", "126"), "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), "CODECOV_TOKEN": os.environ.get("CODECOV_TOKEN", ""), diff --git a/cicd/single_gpu.py b/cicd/single_gpu.py index 357aa41eee..6955af0134 100644 --- a/cicd/single_gpu.py +++ b/cicd/single_gpu.py @@ -24,9 +24,9 @@ df_args = { "AXOLOTL_EXTRAS": os.environ.get("AXOLOTL_EXTRAS", ""), "AXOLOTL_ARGS": os.environ.get("AXOLOTL_ARGS", ""), - "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.5.1"), - "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu124-2.5.1"), - "CUDA": os.environ.get("CUDA", "124"), + "PYTORCH_VERSION": os.environ.get("PYTORCH_VERSION", "2.6.0"), + "BASE_TAG": os.environ.get("BASE_TAG", "main-base-py3.11-cu126-2.6.0"), + "CUDA": os.environ.get("CUDA", "126"), "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), "NIGHTLY_BUILD": os.environ.get("NIGHTLY_BUILD", ""), diff --git a/docs/docker.qmd b/docs/docker.qmd index 197185d885..5b238c520e 100644 --- a/docs/docker.qmd +++ b/docs/docker.qmd @@ -36,7 +36,6 @@ Tags examples: - `main-base-py3.11-cu126-2.7.1` - `main-base-py3.11-cu126-2.6.0` - `main-base-py3.11-cu124-2.6.0` -- `main-base-py3.11-cu124-2.5.1` ## Main @@ -78,10 +77,9 @@ Tags examples: - `main-py3.11-cu126-2.7.1` - `main-py3.11-cu126-2.6.0` - `main-py3.11-cu124-2.6.0` -- `main-py3.11-cu124-2.5.1` - `main-latest` - `main-20250303-py3.11-cu124-2.6.0` -- `main-20250303-py3.11-cu124-2.5.1` +- `main-20250303-py3.11-cu126-2.6.0` - `0.10.1` ## Cloud diff --git a/docs/installation.qmd b/docs/installation.qmd index c905e93cda..0a29aedb90 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -15,7 +15,7 @@ This guide covers all the ways you can install and set up Axolotl for your envir - NVIDIA GPU (Ampere architecture or newer for `bf16` and Flash Attention) or AMD GPU - Python ≥3.11 -- PyTorch ≥2.5.1 +- PyTorch ≥2.6.0 ## Installation Methods {#sec-installation-methods} diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index 314d22279f..f31a40b4d1 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -4,4 +4,4 @@ __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.11.0.dev" +__version__ = "0.11.0" diff --git a/src/axolotl/integrations/kd/README.md b/src/axolotl/integrations/kd/README.md index 4b15ad31dd..5e35cf3d7f 100644 --- a/src/axolotl/integrations/kd/README.md +++ b/src/axolotl/integrations/kd/README.md @@ -11,7 +11,7 @@ kd_ce_alpha: 0.1 kd_alpha: 0.9 kd_temperature: 1.0 -torch_compile: True # torch>=2.5.1, recommended to reduce vram +torch_compile: True # torch>=2.6.0, recommended to reduce vram datasets: - path: ... diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index e590dbdaaf..9dc04c7b41 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -35,6 +35,7 @@ "deepseek_v3", "glm", "glm4", + "smollm3", ] diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 94df7cde8d..323e0877de 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -627,7 +627,7 @@ class AxolotlInputConfig( torch_compile: Literal["auto"] | bool | None = Field( default=None, json_schema_extra={ - "description": "Whether to use torch.compile and which backend to use. setting to `auto` will enable torch compile when torch>=2.5.1" + "description": "Whether to use torch.compile and which backend to use. setting to `auto` will enable torch compile when torch>=2.6.0" }, ) torch_compile_backend: str | None = Field( @@ -1083,9 +1083,9 @@ def check_beta_and_trl_beta_match(cls, data): def check_min_torch_version(self): if self.env_capabilities and self.env_capabilities.torch_version: torch_version = self.env_capabilities.torch_version - if version.parse(torch_version) < version.parse("2.5.1"): + if version.parse(torch_version) < version.parse("2.6.0"): LOG.warning( - f"torch=={torch_version} may not be supported in future versions. Please consider upgrading to torch>=2.5.1." + f"torch=={torch_version} not be supported. Please upgrade to torch>=2.6.0." ) return self diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 55e25daf75..677512d3d2 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -692,7 +692,7 @@ def test_merge_lora_no_bf16_fail(self, minimal_cfg): "bf16": True, "capabilities": {"bf16": False}, "env_capabilities": { - "torch_version": "2.5.1", + "torch_version": "2.6.0", }, } ) @@ -1202,7 +1202,7 @@ def test_torch_version_adopt_req(self, minimal_cfg): cfg, capabilities=capabilities, env_capabilities=env_capabilities ) - env_capabilities = {"torch_version": "2.5.1"} + env_capabilities = {"torch_version": "2.6.0"} capabilities = {"bf16": False} _ = validate_config( cfg, capabilities=capabilities, env_capabilities=env_capabilities @@ -1244,7 +1244,7 @@ def test_torch_compile_auto(self, minimal_cfg): | minimal_cfg ) - env_capabilities = {"torch_version": "2.5.1"} + env_capabilities = {"torch_version": "2.6.0"} capabilities = {"bf16": True} updated_cfg = validate_config( cfg, capabilities=capabilities, env_capabilities=env_capabilities diff --git a/tests/test_validation_dataset.py b/tests/test_validation_dataset.py index ba142f3bf0..1a4c973147 100644 --- a/tests/test_validation_dataset.py +++ b/tests/test_validation_dataset.py @@ -73,7 +73,7 @@ def _check_config(): "compute_capability": "8.0", }, env_capabilities={ - "torch_version": "2.5.1", + "torch_version": "2.6.0", }, ) @@ -128,7 +128,7 @@ def _check_config(): "compute_capability": "8.0", }, env_capabilities={ - "torch_version": "2.5.1", + "torch_version": "2.6.0", }, ) @@ -184,7 +184,7 @@ def _check_config(): "compute_capability": "8.0", }, env_capabilities={ - "torch_version": "2.5.1", + "torch_version": "2.6.0", }, ) @@ -241,7 +241,7 @@ def _check_config(): "compute_capability": "8.0", }, env_capabilities={ - "torch_version": "2.5.1", + "torch_version": "2.6.0", }, ) From 7c5ea0010f070fc8391f4cc078c84eda0ad219ed Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 9 Jul 2025 09:43:42 -0400 Subject: [PATCH 0784/1405] bump dev version (#2889) [skip ci] --- src/axolotl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index f31a40b4d1..8548147470 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -4,4 +4,4 @@ __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.11.0" +__version__ = "0.12.0.dev" From 76aeb1615645176abbf92381db1d5a4c32bcedf5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 9 Jul 2025 12:48:22 -0400 Subject: [PATCH 0785/1405] tiled_mlp supports single gpu (#2891) * tiled_mlp supports single gpu * use checkpoint offloading for arctic training * patch torch checkpoint too * support for single gpu zero3 * add linkback to where it was copied from --- docs/multi-gpu.qmd | 9 + src/axolotl/loaders/patch_manager.py | 18 +- .../gradient_checkpointing/__init__.py | 3 +- .../gradient_checkpointing/offload_cpu.py | 166 ++++++++++++++++++ src/axolotl/monkeypatch/tiled_mlp.py | 10 +- src/axolotl/utils/schemas/validation.py | 10 +- src/axolotl/utils/trainer.py | 9 + 7 files changed, 218 insertions(+), 7 deletions(-) diff --git a/docs/multi-gpu.qmd b/docs/multi-gpu.qmd index fee7d17e5f..6dc1982127 100644 --- a/docs/multi-gpu.qmd +++ b/docs/multi-gpu.qmd @@ -66,6 +66,15 @@ Start from Stage 1 -> Stage 2 -> Stage 3. ::: +::: {.callout-tip} + +Using ZeRO Stage 3 with Single-GPU training + +ZeRO Stage 3 can be used for training on a single GPU by manually setting the environment variables: +`WORLD_SIZE=1 LOCAL_RANK=0 MASTER_ADDR=0.0.0.0 MASTER_PORT=29500` + +::: + ## FSDP {#sec-fsdp} ### Basic FSDP Configuration {#sec-fsdp-config} diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 48ee78cbc9..81d4e94716 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -7,6 +7,7 @@ from functools import cached_property import addict +import torch import transformers from transformers import PretrainedConfig, PreTrainedModel @@ -165,10 +166,25 @@ def _apply_gradient_checkpointing_patches(self): """Apply patches for gradient checkpointing.""" if self.cfg.gradient_checkpointing in ["unsloth", "offload"]: from axolotl.monkeypatch.gradient_checkpointing import ( + CheckpointFunctionWithCPUOffload, hf_grad_checkpoint_offload_wrapper, ) - transformers.modeling_utils.checkpoint = hf_grad_checkpoint_offload_wrapper + if ( + self.cfg.gradient_checkpointing_kwargs + and "use_reentrant" in self.cfg.gradient_checkpointing_kwargs + and self.cfg.gradient_checkpointing_kwargs["use_reentrant"] is False + ): + transformers.modeling_utils.checkpoint = ( + hf_grad_checkpoint_offload_wrapper + ) + else: + transformers.modeling_utils.checkpoint.CheckpointFunction = ( + CheckpointFunctionWithCPUOffload + ) + torch.utils.checkpoint.CheckpointFunction = ( + CheckpointFunctionWithCPUOffload + ) if self.cfg.gradient_checkpointing == "offload_disk": from axolotl.monkeypatch.gradient_checkpointing import ( hf_grad_checkpoint_disk_offload_wrapper, diff --git a/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py b/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py index 5d631776b2..6ca8e02404 100644 --- a/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py +++ b/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py @@ -5,7 +5,8 @@ from packaging import version -from axolotl.monkeypatch.gradient_checkpointing.offload_cpu import ( +from axolotl.monkeypatch.gradient_checkpointing.offload_cpu import ( # noqa: F401 + CheckpointFunctionWithCPUOffload, CPU_Offloaded_Gradient_Checkpointer, ) from axolotl.monkeypatch.gradient_checkpointing.offload_disk import ( diff --git a/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py b/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py index bbb5ad40d3..432cafb350 100644 --- a/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py +++ b/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py @@ -13,8 +13,24 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +import contextlib +import inspect + import torch from packaging import version +from torch.utils.checkpoint import ( + _get_autocast_kwargs, + _get_device_module, + _infer_device_type, + check_backward_validity, + detach_variable, + get_device_states, + set_device_states, +) + +# support different pytorch versions +has_device_type = "device_type" in inspect.signature(set_device_states).parameters torch_version = version.parse(torch.__version__) @@ -60,3 +76,153 @@ def backward(ctx, dY): ) + ( None, ) * len(ctx.args) + + +# Copyright 2025 Snowflake Inc. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/snowflakedb/ArcticTraining/blob/main/arctic_training/monkey_patches.py +class CheckpointFunctionWithCPUOffload(torch.autograd.Function): + """ + This is a torch/utils/checkpoint.py CheckpointFunction monkey patch that offloads the first tensor to cpu during forward and back to cuda during backward. This allows significant memory savings when using a very long seqlen. e.g. for llama 8b at 100k it's 24GB saved per gpu: `((100_000*4096)*2*32/2**30)` + In the case of a very long seqlen 100k+ the copying to/from cpu overhead is not big, because dense quadratic attention compute will dominate. + """ + + @staticmethod + def forward(ctx, run_function, preserve_rng_state, *args): + check_backward_validity(args) + ctx.run_function = run_function + ctx.preserve_rng_state = preserve_rng_state + # Accommodates the (remote) possibility that autocast is enabled for cpu AND gpu. + ctx.device_type = _infer_device_type(*args) + ctx.device_autocast_kwargs, ctx.cpu_autocast_kwargs = _get_autocast_kwargs( + ctx.device_type + ) + if preserve_rng_state: + ctx.fwd_cpu_state = torch.get_rng_state() + # Don't eagerly initialize the cuda context by accident. + # (If the user intends that the context is initialized later, within their + # run_function, we SHOULD actually stash the cuda state here. Unfortunately, + # we have no way to anticipate this will happen before we run the function.) + ctx.had_device_in_fwd = False + device_module = _get_device_module(ctx.device_type) + if getattr(device_module, "_initialized", False): + ctx.had_device_in_fwd = True + ctx.fwd_devices, ctx.fwd_device_states = get_device_states(*args) + + # Save non-tensor inputs in ctx, keep a placeholder None for tensors + # to be filled out during the backward. + ctx.inputs = [] + ctx.tensor_indices = [] + tensor_inputs = [] + # x = None + for i, arg in enumerate(args): + if torch.is_tensor(arg): + # cpu-offload + # we don't want the 2nd tensor - usually it's a shared 4D attn mask which is huge [seq,seq] + # upstream could accept a list of arg indices to offload + if i == 0: + # print(f"{arg.shape=}") + ctx.x_device = arg.device + ctx.x_requires_grad = arg.requires_grad + t = arg.detach().cpu() + else: + t = arg + tensor_inputs.append(t) + ctx.tensor_indices.append(i) + ctx.inputs.append(None) + else: + ctx.inputs.append(arg) + + ctx.save_for_backward(*tensor_inputs) + + with torch.no_grad(): + outputs = run_function(*args) + + return outputs + + @staticmethod + def backward(ctx, *args): + if ( + not torch.autograd._is_checkpoint_valid() # pylint: disable=protected-access + ): + raise RuntimeError( + "When use_reentrant=True, torch.utils.checkpoint is incompatible" + " with .grad() or passing an `inputs` parameter to .backward()." + " To resolve this error, you can either set use_reentrant=False," + " or call .backward() without passing the `inputs` argument." + ) + # Copy the list to avoid modifying original list. + inputs = list(ctx.inputs) + tensor_indices = ctx.tensor_indices + tensors = ctx.saved_tensors + + # Fill in inputs with appropriate saved tensors. + for i, idx in enumerate(tensor_indices): + if i == 0: + t = ( + tensors[i] + .to(ctx.x_device) + .detach() + .requires_grad_(ctx.x_requires_grad) + ) + else: + t = tensors[i] + inputs[idx] = t + + # Stash the surrounding rng state, and mimic the state that was + # present at this time during forward. Restore the surrounding state + # when we're done. + rng_devices = [] + if ctx.preserve_rng_state and ctx.had_device_in_fwd: + rng_devices = ctx.fwd_devices + with torch.random.fork_rng( + devices=rng_devices, + enabled=ctx.preserve_rng_state, + device_type=ctx.device_type, + ): + if ctx.preserve_rng_state: + torch.set_rng_state(ctx.fwd_cpu_state) + if ctx.had_device_in_fwd: + if has_device_type: + # newer pytorch (as early as 2.7) + set_device_states( + ctx.fwd_devices, + ctx.fwd_device_states, + device_type=ctx.device_type, + ) + else: + # older pytorch (at least 2.4) + set_device_states(ctx.fwd_devices, ctx.fwd_device_states) + detached_inputs = detach_variable(tuple(inputs)) + + device_autocast_ctx = ( + torch.amp.autocast( + device_type=ctx.device_type, **ctx.device_autocast_kwargs + ) + if torch.amp.is_autocast_available(ctx.device_type) + else contextlib.nullcontext() + ) + with torch.enable_grad(), device_autocast_ctx, torch.amp.autocast("cpu", **ctx.cpu_autocast_kwargs): # type: ignore[attr-defined] + outputs = ctx.run_function(*detached_inputs) + + if isinstance(outputs, torch.Tensor): + outputs = (outputs,) + + # run backward() with only tensor that requires grad + outputs_with_grad = [] + args_with_grad = [] + for i in range(len(outputs)): # pylint: disable=consider-using-enumerate + if torch.is_tensor(outputs[i]) and outputs[i].requires_grad: + outputs_with_grad.append(outputs[i]) + args_with_grad.append(args[i]) + if len(outputs_with_grad) == 0: + raise RuntimeError( + "none of output has requires_grad=True, this checkpoint() is not necessary" + ) + torch.autograd.backward(outputs_with_grad, args_with_grad) + grads = tuple( + inp.grad if isinstance(inp, torch.Tensor) else None + for inp in detached_inputs + ) + + return (None, None) + grads diff --git a/src/axolotl/monkeypatch/tiled_mlp.py b/src/axolotl/monkeypatch/tiled_mlp.py index 4862ae78cd..99a10df9c0 100644 --- a/src/axolotl/monkeypatch/tiled_mlp.py +++ b/src/axolotl/monkeypatch/tiled_mlp.py @@ -1,6 +1,7 @@ """Monkeypatch for Tiled MLP implementation""" import math +import os import torch import torch.distributed as dist @@ -29,15 +30,18 @@ def generic_mlp_forward(self_, hs): mlp_forward = torch.compile(generic_mlp_forward) + is_distributed = int(os.environ.get("WORLD_SIZE", 1)) > 1 + def tiled_mlp_forward(self, x): input_shape = x.shape seqlen = input_shape[-2] hidden = input_shape[-1] if cfg_num_shards is None: num_shards = math.ceil(seqlen / hidden) - num_shards_tensor = torch.tensor(num_shards, device=x.device) - dist.all_reduce(num_shards_tensor, op=dist.ReduceOp.MAX) - num_shards = num_shards_tensor.item() + if is_distributed: + num_shards_tensor = torch.tensor(num_shards, device=x.device) + dist.all_reduce(num_shards_tensor, op=dist.ReduceOp.MAX) + num_shards = num_shards_tensor.item() else: num_shards = cfg_num_shards diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index af1341cdab..c2d4b4af41 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -479,8 +479,14 @@ def pretrain_with_tps(cls, data): @model_validator(mode="before") @classmethod def check_tiled_mlp_deepspeed(cls, data): - if data.get("tiled_mlp", False) and not data.get("deepspeed"): - raise ValueError("tiled_mlp requires deepspeed ZeRO to be enabled") + capabilities = data.get("capabilities") + n_gpu = 0 + if capabilities and capabilities.get("n_gpu", 0) >= 1: + n_gpu = capabilities.get("n_gpu", 0) + if data.get("tiled_mlp", False) and (n_gpu > 1 and not data.get("deepspeed")): + raise ValueError( + "tiled_mlp requires deepspeed ZeRO to be enabled for multi-gpu" + ) return data diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index cb597606ce..2ef637232c 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -546,6 +546,15 @@ def setup_deepspeed_env(cfg, stage=None): # NOTE(djsaunde): The distribued state cannot be initialized prior to the # ACCELERATE_USE_DEEPSPEED assignment, but it must be initialized some time prior # to model load. + if int(os.environ.get("WORLD_SIZE", "1")) == 1: + os.environ["WORLD_SIZE"] = "1" # force it in case not set + os.environ["LOCAL_RANK"] = "0" # force it in case not set + os.environ["RANK"] = os.environ.get("LOCAL_RANK", "0") + import deepspeed.comm as dist + + dist.init_distributed( + dist_backend="nccl", auto_mpi_discovery=False, dist_init_required=True + ) init_distributed_state() # If we don't assign this, it doesn't actually get set in the accelerate weakref From c370d0795c8b166625486d1080eeff72201ce180 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 9 Jul 2025 14:52:44 -0400 Subject: [PATCH 0786/1405] [doc] Fix docs for text field mapping for completion datasets (#2890) * Fix docs for text field mapping for completion datasets * update another reference --- .runpod/src/config/config.yaml | 2 +- src/axolotl/utils/schemas/datasets.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.runpod/src/config/config.yaml b/.runpod/src/config/config.yaml index 42c5978d57..2a89971fb3 100644 --- a/.runpod/src/config/config.yaml +++ b/.runpod/src/config/config.yaml @@ -97,7 +97,7 @@ # # 'no_input_format' cannot include {input} # no_input_format: "{instruction} " -# # For `completion` datsets only, uses the provided field instead of `text` column +# # For `completion` datasets only, uses the provided field instead of `text` column # field: # # Axolotl attempts to save the dataset as an arrow after packing the data together so diff --git a/src/axolotl/utils/schemas/datasets.py b/src/axolotl/utils/schemas/datasets.py index db222ce1ef..da8c545bc8 100644 --- a/src/axolotl/utils/schemas/datasets.py +++ b/src/axolotl/utils/schemas/datasets.py @@ -34,12 +34,6 @@ class UserDefinedPrompterType(BaseModel): default=None, json_schema_extra={"description": "'no_input_format' cannot include {input}"}, ) - field: str | None = Field( - default=None, - json_schema_extra={ - "description": "For `completion` datsets only, uses the provided field instead of `text` column" - }, - ) class SFTDataset(BaseModel): @@ -104,7 +98,12 @@ class SFTDataset(BaseModel): default=None, json_schema_extra={"description": "defines the datatype when path is a file"}, ) - field: str | None = None + field: str | None = Field( + default=None, + json_schema_extra={ + "description": "For `completion` datasets only, uses the provided field instead of `text` column" + }, + ) field_human: str | None = None field_model: str | None = None field_messages: str | None = Field( From 9b95a625ab7061e0034be29d93d52ce37b0e7c84 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 11 Jul 2025 09:34:19 +0700 Subject: [PATCH 0787/1405] feat: add devstral small 2507 (#2896) * feat: add devstral small 2507 * chore: update blog doc --- docs/dataset-formats/conversation.qmd | 2 ++ examples/devstral/README.md | 15 ++++++++------- examples/devstral/devstral-small-qlora.yml | 2 +- requirements.txt | 2 +- tests/prompt_strategies/conftest.py | 8 ++++++++ .../test_chat_templates_mistral.py | 12 +++++++----- 6 files changed, 27 insertions(+), 14 deletions(-) diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 90cc178fcc..733b9f32c6 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -187,6 +187,7 @@ Instead of passing `tools` via the system prompt, an alternative method would be "role": "assistant", // call the function via assistant "tool_calls": [ { + "id": "...", // required only for mistral "type": "function", "function": { "name": "...", @@ -199,6 +200,7 @@ Instead of passing `tools` via the system prompt, an alternative method would be }, { "role": "tool", + "tool_call_id": "...", // required only for mistral "name": "...", "content": "..." }, diff --git a/examples/devstral/README.md b/examples/devstral/README.md index 9dc5377bc6..1cf2e2cec1 100644 --- a/examples/devstral/README.md +++ b/examples/devstral/README.md @@ -1,8 +1,12 @@ # Finetune Devstral with Axolotl -Devstral Small is a 24B parameter opensource model from MistralAI found on HuggingFace [Devstral-Small-2505](https://huggingface.co/mistralai/Devstral-Small-2505). This guide shows how to fine-tune it with Axolotl with multi-turn conversations with proper masking. +Devstral Small is a 24B parameter opensource model from MistralAI found on HuggingFace [Devstral-Small-2505](https://huggingface.co/mistralai/Devstral-Small-2505) and [Devstral-Small-2507](https://huggingface.co/mistralai/Devstral-Small-2507). `Devstral-Small-2507` is the latest version of the model and has [function calling](https://mistralai.github.io/mistral-common/usage/tools/) support. -The model was fine-tuned ontop of [Mistral-Small-3.1](https://huggingface.co/mistralai/Mistral-Small-3.1-24B-Base-2503) without the vision layer and has a context of upto 128k tokens. +This guide shows how to fine-tune it with Axolotl with multi-turn conversations with proper masking. + +The model was fine-tuned ontop of [Mistral-Small-3.1](https://huggingface.co/mistralai/Mistral-Small-3.1-24B-Base-2503) without the vision layer and has a context of up to 128k tokens. + +Thanks to the team at MistralAI for giving us early access to prepare for this release. ## Getting started @@ -17,11 +21,6 @@ cd axolotl pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation -e '.[flash-attn]' - -# Install the latest mistral-common from source -pip3 uninstall mistral-common -pip3 install git+https://github.com/mistralai/mistral-common.git@039465d - ``` 2. Run the finetuning example: @@ -39,6 +38,7 @@ Let us know how it goes. Happy finetuning! 🚀 - You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. - Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). - The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). +- Learn how to use function calling with Axolotl at [docs](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#using-tool-use). ## Optimization Guides @@ -57,6 +57,7 @@ In addition, we do not support overriding tokens yet. ## Related Resources - [MistralAI Devstral Blog](https://mistral.ai/news/devstral) +- [MistralAI Devstral 1.1 Blog](https://mistral.ai/news/devstral-2507) - [Axolotl Docs](https://docs.axolotl.ai) - [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) - [Axolotl Website](https://axolotl.ai) diff --git a/examples/devstral/devstral-small-qlora.yml b/examples/devstral/devstral-small-qlora.yml index d2c5930e3c..dc0051bd5e 100644 --- a/examples/devstral/devstral-small-qlora.yml +++ b/examples/devstral/devstral-small-qlora.yml @@ -1,4 +1,4 @@ -base_model: mistralai/Devstral-Small-2505 +base_model: mistralai/Devstral-Small-2507 # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name diff --git a/requirements.txt b/requirements.txt index 0ed1fa6150..77d6d31aac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,4 +68,4 @@ schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.6 axolotl-contribs-mit==0.0.3 -mistral-common==1.6.3 +mistral-common==1.7.0 diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index 60b14d6523..a423135998 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -172,6 +172,14 @@ def fixture_devstral_tokenizer(): return tokenizer +@pytest.fixture(name="devstral_1_1_tokenizer") +def fixture_devstral_1_1_tokenizer(): + from axolotl.utils.mistral_tokenizer import HFMistralTokenizer + + tokenizer = HFMistralTokenizer.from_pretrained("mistralai/Devstral-Small-2507") + return tokenizer + + @pytest.fixture(name="mistralv03_tokenizer_chat_template_jinja") def fixture_mistralv03_chat_template_jinja_w_system() -> str: return '{%- if messages[0]["role"] == "system" %}\n {%- set system_message = messages[0]["content"] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set loop_messages = messages %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n{%- set user_messages = loop_messages | selectattr("role", "equalto", "user") | list %}\n\n{#- This block checks for alternating user/assistant messages, skipping tool calling messages #}\n{%- set ns = namespace() %}\n{%- set ns.index = 0 %}\n{%- for message in loop_messages %}\n {%- if not (message.role == "tool" or message.role == "tool_results" or (message.tool_calls is defined and message.tool_calls is not none)) %}\n {%- if (message["role"] == "user") != (ns.index % 2 == 0) %}\n {{- raise_exception("After the optional system message, conversation roles must alternate user/assistant/user/assistant/...") }}\n {%- endif %}\n {%- set ns.index = ns.index + 1 %}\n {%- endif %}\n{%- endfor %}\n\n{{- bos_token }}\n{%- for message in loop_messages %}\n {%- if message["role"] == "user" %}\n {%- if tools is not none and (message == user_messages[-1]) %}\n {{- "[AVAILABLE_TOOLS] [" }}\n {%- for tool in tools %}\n {%- set tool = tool.function %}\n {{- \'{"type": "function", "function": {\' }}\n {%- for key, val in tool.items() if key != "return" %}\n {%- if val is string %}\n {{- \'"\' + key + \'": "\' + val + \'"\' }}\n {%- else %}\n {{- \'"\' + key + \'": \' + val|tojson }}\n {%- endif %}\n {%- if not loop.last %}\n {{- ", " }}\n {%- endif %}\n {%- endfor %}\n {{- "}}" }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- else %}\n {{- "]" }}\n {%- endif %}\n {%- endfor %}\n {{- "[/AVAILABLE_TOOLS]" }}\n {%- endif %}\n {%- if loop.first and system_message is defined %}\n {{- "[INST] " + system_message + "\\n\\n" + message["content"] + "[/INST]" }}\n {%- else %}\n {{- "[INST] " + message["content"] + "[/INST]" }}\n {%- endif %}\n {%- elif message.tool_calls is defined and message.tool_calls is not none %}\n {{- "[TOOL_CALLS] [" }}\n {%- for tool_call in message.tool_calls %}\n {%- set out = tool_call.function|tojson %}\n {{- out[:-1] }}\n {%- if not tool_call.id is defined or tool_call.id|length != 9 %}\n {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }}\n {%- endif %}\n {{- \', "id": "\' + tool_call.id + \'"}\' }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- else %}\n {{- "]" + eos_token }}\n {%- endif %}\n {%- endfor %}\n {%- elif message["role"] == "assistant" %}\n {{- " " + message["content"]|trim + eos_token}}\n {%- elif message["role"] == "tool_results" or message["role"] == "tool" %}\n {%- if message.content is defined and message.content.content is defined %}\n {%- set content = message.content.content %}\n {%- else %}\n {%- set content = message.content %}\n {%- endif %}\n {{- \'[TOOL_RESULTS] {"content": \' + content|string + ", " }}\n {%- if not message.tool_call_id is defined or message.tool_call_id|length != 9 %}\n {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }}\n {%- endif %}\n {{- \'"call_id": "\' + message.tool_call_id + \'"}[/TOOL_RESULTS]\' }}\n {%- else %}\n {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }}\n {%- endif %}\n{%- endfor %}\n' diff --git a/tests/prompt_strategies/test_chat_templates_mistral.py b/tests/prompt_strategies/test_chat_templates_mistral.py index dcf5138d38..f26ed08387 100644 --- a/tests/prompt_strategies/test_chat_templates_mistral.py +++ b/tests/prompt_strategies/test_chat_templates_mistral.py @@ -11,16 +11,18 @@ # fmt: off @pytest.mark.parametrize( - ("tokenizer_str", "assistant_toolcall_ids"), + ("tokenizer_str", "assistant_toolcall_ids", "tool_result_ids"), ( - ("magistral_tokenizer", (9, 44627, 3684, 33, 19881, 1049, 1050, 1051, 1052, 1053, 32, 19227, 12856, 2811, 1032, 1049, 1054, 1044, 1429, 33319, 2811, 1032, 1050, 1125, 2)), - ("devstral_tokenizer", (9, 1091, 19227, 2391, 2811, 1429, 44627, 3684, 1897, 1429, 61906, 2811, 16753, 12856, 2811, 1032, 1049, 1054, 1044, 1429, 33319, 2811, 1032, 1050, 4179, 1429, 1327, 2811, 1429, 19881, 1049, 1050, 1051, 1052, 1053, 1034, 27028, 2)), + ("magistral_tokenizer", (9, 44627, 3684, 33, 19881, 1049, 1050, 1051, 1052, 1053, 32, 19227, 12856, 2811, 1032, 1049, 1054, 1044, 1429, 33319, 2811, 1032, 1050, 1125, 2), (7, 19881, 1049, 1050, 1051, 1052, 1053, 19, 1049, 1044, 1050, 8)), + ("devstral_tokenizer", (9, 1091, 19227, 2391, 2811, 1429, 44627, 3684, 1897, 1429, 61906, 2811, 16753, 12856, 2811, 1032, 1049, 1054, 1044, 1429, 33319, 2811, 1032, 1050, 4179, 1429, 1327, 2811, 1429, 19881, 1049, 1050, 1051, 1052, 1053, 1034, 27028, 2), (7, 19881, 1049, 1050, 1051, 1052, 1053, 19, 1049, 1044, 1050, 8)), + ("devstral_1_1_tokenizer", (9, 44627, 3684, 32, 19227, 12856, 2811, 1032, 1049, 1054, 1044, 1429, 33319, 2811, 1032, 1050, 1125, 2,), (7, 1049, 1044, 1050, 8)), ) ) # fmt: on def test_mistral_chat_template( tokenizer_str: str, assistant_toolcall_ids: tuple[int, ...], + tool_result_ids: tuple[int, ...], request: pytest.FixtureRequest, ): """Test chat template with the Magistral/Devstral tokenizer""" @@ -238,7 +240,7 @@ def test_mistral_chat_template( 5, 1091, 19227, 4994, 2811, 1429, 5165, 1897, 1429, 5165, 2811, 16753, 2391, 2811, 1429, 44627, 3684, 1897, 1429, 14653, 2811, 1429, 10639, 2130, 1261, 2951, 1307, 1747, 1278, 60092, 1307, 1261, 2782, 1455, 1584, 4289, 2224, 1261, 4265, 6139, 39249, 1429, 26204, 2811, 16753, 4994, 2811, 1429, 6371, 1897, 1429, 48649, 2811, 16753, 12856, 2811, 16753, 4994, 2811, 1429, 49039, 1897, 1429, 14653, 2811, 1429, 1784, 2782, 1317, 3081, 60092, 1307, 2613, 4179, 1429, 33319, 2811, 16753, 4994, 2811, 1429, 49039, 1897, 1429, 14653, 2811, 1429, 1784, 9229, 6139, 1394, 1278, 60092, 2613, 47579, 1429, 15760, 2811, 12161, 12856, 1897, 1429, 33319, 4964, 2821, 27028, 6, # tool prompt 3, 46634, 1044, 1710, 1636, 5628, 1639, 1261, 44433, 1307, 2606, 1317, 5388, 1420, 54191, 2424, 1286, 8967, 1063, 15621, 1044, 2549, 30305, 2196, 3560, 1044, 1321, 2606, 1710, 1362, 2016, 8605, 2015, 1317, 5524, 118931, 2036, 32951, 1063, 1362, 2933, 2269, 12106, 1408, 101987, 1044, 6939, 1044, 1321, 9216, 1455, 2084, 3180, 1278, 8967, 119141, 1689, 5935, 1033, 4, # user *assistant_toolcall_ids, # assistant tool calling - 7, 19881, 1049, 1050, 1051, 1052, 1053, 19, 1049, 1044, 1050, 8, # tool result + *tool_result_ids, # tool result 1784, 60092, 1307, 1032, 1049, 1054, 1395, 1032, 1049, 1321, 1032, 1050, 1046, # assistant 2 # eos ] @@ -248,7 +250,7 @@ def test_mistral_chat_template( -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # tool prompt -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # user prompt *assistant_toolcall_ids, # assistant tool calling - -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, # tool result + *([-100] * len(tool_result_ids)), # tool result 1784, 60092, 1307, 1032, 1049, 1054, 1395, 1032, 1049, 1321, 1032, 1050, 1046, # assistant 2 # eos ] From 03b2a113feab58766cdf09595de59f4b0dfa9e33 Mon Sep 17 00:00:00 2001 From: salman Date: Fri, 11 Jul 2025 14:08:35 +0100 Subject: [PATCH 0788/1405] Update doc preview workflow to use sticky comments (#2873) --- .github/workflows/preview-docs.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index f93cfa660d..a13d2c97fc 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -28,6 +28,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Quarto uses: quarto-dev/quarto-actions/setup@v2 @@ -50,10 +52,11 @@ jobs: - name: Netlify Publish uses: nwtgck/actions-netlify@v3.0 + id: netlify with: publish-dir: './_site' - enable-pull-request-comment: true - enable-github-deployment: true + enable-pull-request-comment: false + enable-github-deployment: false github-token: ${{ secrets.GITHUB_TOKEN }} deploy-message: "Deployed On Netlify" github-deployment-environment: 'preview' @@ -61,3 +64,13 @@ jobs: env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + + - name: Update PR with preview link + if: ${{ steps.netlify.outcome == 'success' }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + message: | + 📖 **Documentation Preview**: ${{ steps.netlify.outputs.deploy-url }} + + Deployed on Netlify from commit ${{ github.event.pull_request.head.sha }} From eb662557a7304485534779876f49793df0ec6a6a Mon Sep 17 00:00:00 2001 From: Ed Sealing <97403758+drikster80@users.noreply.github.com> Date: Fri, 11 Jul 2025 13:59:59 -0700 Subject: [PATCH 0789/1405] Register Plugins in Ray Workers (#2901) [skip ci] * Access plugins in ray cluster * Add comment * chore: lint --------- Co-authored-by: Ed Sealing Co-authored-by: Wing Lian --- src/axolotl/cli/train.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 28d4d543ac..d0cf8455bc 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -109,6 +109,13 @@ def ray_train_func(kwargs: dict): # initialize accelerator before model instantiation Accelerator(gradient_accumulation_steps=cfg.gradient_accumulation_steps) + # Register plugins in Ray workers + if cfg.get("plugins"): + from axolotl.cli.config import plugin_set_cfg, prepare_plugins + + prepare_plugins(cfg) + plugin_set_cfg(cfg) + kwargs["cfg"] = cfg do_train(**kwargs) From d6e4a611e5eaced05cf295f90095ffb71e2448a5 Mon Sep 17 00:00:00 2001 From: salman Date: Sat, 12 Jul 2025 15:18:01 +0100 Subject: [PATCH 0790/1405] FSDP1 -> FSDP2 (#2760) * FSDP2 args migration implementation This commit implements the migration to FSDP2 arguments including: - FSDP2 support with LoRA training - DPO integration with FSDP2 - Model loading fixes and refactoring - CPU offloading and PEFT handling - Test updates and CI improvements - Bug fixes for dtype errors and various edge cases --- docs/multi-gpu.qmd | 65 +++- docs/multi-node.qmd | 12 +- docs/rlhf.qmd | 1 - src/axolotl/cli/config.py | 2 + src/axolotl/core/builders/base.py | 4 + src/axolotl/core/builders/causal.py | 8 - src/axolotl/core/builders/rl.py | 20 +- src/axolotl/core/trainers/__init__.py | 1 - src/axolotl/core/trainers/trl.py | 63 +--- src/axolotl/loaders/adapter.py | 8 +- src/axolotl/loaders/model.py | 118 +++--- src/axolotl/loaders/patch_manager.py | 6 +- src/axolotl/loaders/utils.py | 6 +- src/axolotl/monkeypatch/accelerate/fsdp2.py | 301 +++++++++++---- .../monkeypatch/attention/flex_attn.py | 9 + src/axolotl/monkeypatch/trainer/trl.py | 13 + src/axolotl/train.py | 46 +-- src/axolotl/utils/bench.py | 19 +- src/axolotl/utils/config/__init__.py | 19 +- src/axolotl/utils/schemas/config.py | 122 ++++-- src/axolotl/utils/schemas/validation.py | 13 +- src/axolotl/utils/trainer.py | 42 +-- tests/e2e/multigpu/test_fsdp1.py | 326 ++++++++++++++++ tests/e2e/multigpu/test_fsdp2.py | 355 ++++++++++++++++++ tests/e2e/multigpu/test_qwen2.py | 93 ----- tests/e2e/utils.py | 12 + tests/test_normalize_config.py | 107 +++++- 27 files changed, 1356 insertions(+), 435 deletions(-) create mode 100644 src/axolotl/monkeypatch/trainer/trl.py create mode 100644 tests/e2e/multigpu/test_fsdp1.py create mode 100644 tests/e2e/multigpu/test_fsdp2.py delete mode 100644 tests/e2e/multigpu/test_qwen2.py diff --git a/docs/multi-gpu.qmd b/docs/multi-gpu.qmd index 6dc1982127..2571d31b6a 100644 --- a/docs/multi-gpu.qmd +++ b/docs/multi-gpu.qmd @@ -23,8 +23,6 @@ Axolotl supports several methods for multi-GPU training: ## DeepSpeed {#sec-deepspeed} -DeepSpeed is the recommended approach for multi-GPU training due to its stability and performance. It provides various optimization levels through ZeRO stages. - ### Configuration {#sec-deepspeed-config} Add to your YAML config: @@ -32,7 +30,6 @@ Add to your YAML config: ```{.yaml} deepspeed: deepspeed_configs/zero1.json ``` - ### Usage {#sec-deepspeed-usage} ```{.bash} @@ -75,9 +72,66 @@ ZeRO Stage 3 can be used for training on a single GPU by manually setting the en ::: -## FSDP {#sec-fsdp} +## Fully Sharded Data Parallel (FSDP) {#sec-fsdp} + +::: {.callout-note} + +FSDP2 is recommended for new users. FSDP1 is deprecated and will be removed in an upcoming release of Axolotl. + +::: + +### Migrating from FSDP1 to FSDP2 {#sec-migrate-fsdp1-fsdp2} -### Basic FSDP Configuration {#sec-fsdp-config} +To migrate your config from FSDP1 to FSDP2, you must use the `fsdp_version` top-level config field to specify the FSDP version, and +also follow the config field mapping below to update field names. + +#### Config mapping + +FSDP1 | FSDP2 +-------- | -------- +fsdp_sharding_strategy | reshard_after_forward +fsdp_backward_prefetch_policy | **REMOVED** +fsdp_backward_prefetch | **REMOVED** +fsdp_forward_prefetch | **REMOVED** +fsdp_sync_module_states | **REMOVED** +fsdp_cpu_ram_efficient_loading | cpu_ram_efficient_loading +fsdp_state_dict_type | state_dict_type +fsdp_use_orig_params | **REMOVED** + + +For example, if you were using the following FSDP1 config: + +```{.yaml} +fsdp_version: 1 +fsdp_config: + fsdp_offload_params: false + fsdp_cpu_ram_efficient_loading: true + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: Qwen3DecoderLayer + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_sharding_strategy: FULL_SHARD +``` + +You can migrate to the following FSDP2 config: + +```{.yaml} +fsdp_version: 2 +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: true + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Qwen3DecoderLayer + state_dict_type: FULL_STATE_DICT + reshard_after_forward: true +``` + +### FSDP1 (deprecated) {#sec-fsdp-config} + +::: {.callout-note} + +Using `fsdp` to configure FSDP is deprecated and will be removed in an upcoming release of Axolotl. Please use `fsdp_config` as above instead. + +::: ```{.yaml} fsdp: @@ -89,6 +143,7 @@ fsdp_config: fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer ``` + ## Sequence parallelism {#sec-sequence-parallelism} We support sequence parallelism (SP) via the diff --git a/docs/multi-node.qmd b/docs/multi-node.qmd index cec8ff45df..56d015462d 100644 --- a/docs/multi-node.qmd +++ b/docs/multi-node.qmd @@ -40,13 +40,13 @@ use_cpu: false Configure your model to use FSDP in the Axolotl yaml. For example: ```yaml -fsdp: - - full_shard - - auto_wrap +fsdp_version: 2 fsdp_config: - fsdp_offload_params: true - fsdp_state_dict_type: FULL_STATE_DICT - fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer + offload_params: true + state_dict_type: FULL_STATE_DICT + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: LlamaDecoderLayer + reshard_after_forward: true ``` All you have to do now is launch using accelerate as you would usually do on each machine and voila, the processes will start once you have launched accelerate on every machine. diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 0a189e3c17..76131978f8 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -17,7 +17,6 @@ feedback. Various methods include, but not limited to: - [Kahneman-Tversky Optimization (KTO)](#kto) - [Odds Ratio Preference Optimization (ORPO)](#orpo) - [Group Relative Policy Optimization (GRPO)](#grpo) -- Proximal Policy Optimization (PPO) (not yet supported in axolotl, if you're interested in contributing, please reach out!) ## RLHF using Axolotl diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index cb0eece7fe..5f75352f3f 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -16,6 +16,7 @@ from axolotl.integrations.base import PluginManager from axolotl.utils.comet_ import setup_comet_env_vars from axolotl.utils.config import ( + migrate_fsdp_config, normalize_cfg_datasets, normalize_config, validate_config, @@ -226,6 +227,7 @@ def load_cfg( }, ) + migrate_fsdp_config(cfg) prepare_optim_env(cfg) prepare_opinionated_env(cfg) normalize_config(cfg) diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 7d278db664..3c0ca77deb 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -501,6 +501,10 @@ def _set_base_training_args( if self.cfg.reward_model or self.cfg.rl: training_args_kwargs["max_length"] = self.cfg.sequence_len + if self.cfg.fsdp_config or self.cfg.fsdp: + training_args_kwargs["fsdp_config"] = self.cfg.fsdp_config + training_args_kwargs["fsdp"] = self.cfg.fsdp if self.cfg.fsdp else True + self._configure_reporting(training_args_kwargs) self._configure_hub_parameters(training_args_kwargs) self._configure_scheduler(training_args_kwargs) diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 6ef53bdff8..9fcd51c1d5 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -151,14 +151,6 @@ def build(self, total_num_steps): training_arguments_kwargs, trainer_kwargs = self._set_base_training_args( total_num_steps ) - - if self.cfg.fsdp: - training_arguments_kwargs["fsdp"] = self.cfg.fsdp - if self.cfg.fsdp_config: - training_arguments_kwargs["fsdp_config"] = { - k.lstrip("fsdp_"): v for k, v in dict(self.cfg.fsdp_config).items() - } - if self.cfg.adapter == "qlora": training_arguments_kwargs["qlora"] = True diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index c5f01dd418..e60b0e9581 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -208,7 +208,7 @@ def build(self, total_num_steps): callbacks=self.get_callbacks(), **trainer_kwargs, ) - if self.cfg.fsdp: + if self.cfg.fsdp_config or self.cfg.fsdp: ensure_dtype(trainer.model, dtype=self.cfg.torch_dtype) if self.cfg.rl in [RLType.DPO, RLType.IPO] and trainer.ref_model: ensure_dtype(trainer.ref_model, dtype=self.cfg.torch_dtype) @@ -218,21 +218,3 @@ def build(self, total_num_steps): trainer.add_callback(callback) return trainer - - -class HFPPOTrainerBuilder(TrainerBuilderBase): - """ - HF Factory class for PPO Trainer - """ - - def get_callbacks(self): - callbacks = super().get_callbacks() - return callbacks - - def get_post_trainer_create_callbacks(self, trainer): - callbacks = super().get_post_trainer_create_callbacks(trainer=trainer) - return callbacks - - def build(self, total_num_steps): - # TODO: build PPOConfig - raise NotImplementedError("PPO trainer builder is not implemented yet.") diff --git a/src/axolotl/core/trainers/__init__.py b/src/axolotl/core/trainers/__init__.py index 2cdc9c195e..46b9b15ed0 100644 --- a/src/axolotl/core/trainers/__init__.py +++ b/src/axolotl/core/trainers/__init__.py @@ -14,5 +14,4 @@ AxolotlORPOTrainer, AxolotlPRMTrainer, AxolotlRewardTrainer, - TRLPPOTrainer, ) diff --git a/src/axolotl/core/trainers/trl.py b/src/axolotl/core/trainers/trl.py index bf0d50deea..cb97f37d7e 100644 --- a/src/axolotl/core/trainers/trl.py +++ b/src/axolotl/core/trainers/trl.py @@ -1,12 +1,9 @@ -"""Module for TRL PPO trainer""" +"""Module for TRL RL trainers""" -import torch -from tqdm import tqdm from trl import ( CPOTrainer, KTOTrainer, ORPOTrainer, - PPOTrainer, PRMTrainer, RewardTrainer, ) @@ -16,64 +13,6 @@ from axolotl.core.trainers.mixins.scheduler import SchedulerMixin -class TRLPPOTrainer(PPOTrainer): - """Wrapper for TRL PPO trainer to handle customizations""" - - tag_names = ["axolotl", "ppo"] - - def train( - self, - reward_pipe, - resume_from_checkpoint=None, # pylint: disable=unused-argument - ): - generation_kwargs = { - "min_length": -1, - "top_k": 0.0, - "top_p": 1.0, - "do_sample": True, - "pad_token_id": self.tokenizer.eos_token_id, - "max_new_tokens": 32, - } - sent_kwargs = { - "return_all_scores": True, - "function_to_apply": "none", - "batch_size": 16, - } - - for _, batch in tqdm(enumerate(self.dataloader)): - query_tensors = batch["input_ids"] - - # generate model response - response_tensors, ref_response_tensors = self.generate( - query_tensors, - return_prompt=False, - generate_ref_response=True, - **generation_kwargs, - ) - batch["response"] = self.tokenizer.batch_decode(response_tensors) - batch["ref_response"] = self.tokenizer.batch_decode(ref_response_tensors) - - # Compute sentiment score - texts = [q + r for q, r in zip(batch["query"], batch["response"])] - pipe_outputs = reward_pipe(texts, **sent_kwargs) - rewards = [torch.tensor(output[1]["score"]) for output in pipe_outputs] - ref_texts = [q + r for q, r in zip(batch["query"], batch["ref_response"])] - ref_pipe_outputs = reward_pipe(ref_texts, **sent_kwargs) - ref_rewards = [ - torch.tensor(output[1]["score"]) for output in ref_pipe_outputs - ] - batch["ref_rewards"] = ref_rewards - - # Run PPO step - stats = self.step(query_tensors, response_tensors, rewards) - self.log_stats( - stats, - batch, - rewards, - columns_to_log=["query", "response", "ref_response", "ref_rewards"], - ) - - class AxolotlORPOTrainer( RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, ORPOTrainer ): diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index 16d8daac8e..6b8f42d021 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -122,9 +122,9 @@ def load_lora( rank = int(os.environ.get("LOCAL_RANK", 0)) if ( - cfg.fsdp + cfg.fsdp_config and cfg.adapter - and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading + and cfg.fsdp_config.cpu_ram_efficient_loading and rank != 0 ): setup_quantized_meta_for_peft(model) @@ -152,9 +152,9 @@ def load_lora( "Exception caught during model.print_trainable_parameters(): %s", exc ) elif ( - cfg.fsdp + cfg.fsdp_config and cfg.adapter - and cfg.fsdp_config.fsdp_cpu_ram_efficient_loading + and cfg.fsdp_config.cpu_ram_efficient_loading and rank != 0 ): setup_quantized_peft_meta_for_training(model) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 9897399e3e..03678e1b4a 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -140,10 +140,15 @@ def has_flash_attn(self) -> bool: """Check if flash attention is installed.""" return find_spec("flash_attn") is not None - @cached_property - def qlora_fsdp(self): + @property + def is_fsdp_enabled(self): + """Property that determines if FSDP is enabled.""" + return self.cfg.fsdp_config is not None or self.cfg.fsdp is not None + + @property + def is_qlora_and_fsdp_enabled(self): """Property that determines if FSDP with QLoRA is enabled.""" - return self.cfg.fsdp and self.cfg.adapter == "qlora" + return self.is_fsdp_enabled and self.cfg.adapter == "qlora" def load(self) -> tuple[PreTrainedModel | PeftModelForCausalLM, PeftConfig | None]: """Load and prepare the model with all configurations and patches. @@ -189,15 +194,15 @@ def _apply_post_model_load_setup(self): # Handle PeftModel if needed if ( isinstance(self.model, (peft.PeftModel, peft.PeftModelForCausalLM)) - and not self.qlora_fsdp + and not self.is_qlora_and_fsdp_enabled ): self.model = self.model.merge_and_unload() self._resize_token_embeddings() self._adjust_model_config() - self._log_memory_usage() self._configure_embedding_dtypes() self._configure_qat() + log_gpu_memory_usage(LOG, "Memory usage after model load", 0) def _resize_token_embeddings(self): """Resize token embeddings if needed.""" @@ -251,22 +256,13 @@ def _adjust_model_config(self): ): self.model.config.eos_token_id = self.tokenizer.eos_token_id - def _log_memory_usage(self): - """Log device memory usage after model load.""" - if hasattr(self.model, "device") and self.model.device.type in ( - "cuda", - "mps", - "npu", - ): - log_gpu_memory_usage(LOG, "after model load", self.model.device) - def _configure_embedding_dtypes(self): """Configure embedding module dtypes.""" # Get embedding modules embedding_modules = get_linear_embedding_layers(self.cfg.model_config_type) # Initial dtype conversion - if not self.cfg.fsdp: + if not self.is_fsdp_enabled: # We don't run this during FSDP because this will leave mixed and bfloat16 # dtypes in the model which FSDP doesn't like if self.cfg.load_in_4bit and self.cfg.embeddings_skip_upcast: @@ -282,7 +278,7 @@ def _configure_embedding_dtypes(self): self._set_z3_leaf_modules() # Apply gradient checkpointing if needed - needs_fa2_dtype = self.cfg.adapter or self.cfg.fsdp + needs_fa2_dtype = self.cfg.adapter or self.is_fsdp_enabled if self.cfg.adapter in ["lora", "qlora"]: needs_fa2_dtype = True if self.cfg.gradient_checkpointing: @@ -298,10 +294,12 @@ def _configure_embedding_dtypes(self): # we need to convert them back to fp16/bf16 for flash-attn compatibility. ( (needs_fa2_dtype or self.cfg.flash_attention or self.cfg.flex_attention) - and not self.qlora_fsdp + and not self.is_qlora_and_fsdp_enabled + ) + or ( + # CCE requires embedding layers to be in fp16/bf16 for backward pass + self.cfg.cut_cross_entropy ) - # CCE requires embedding layers to be in fp16/bf16 for backward pass - or self.cfg.cut_cross_entropy ) if should_convert: @@ -357,7 +355,6 @@ def _apply_post_lora_load_setup(self, skip_move_to_device: bool): and not (self.cfg.rl and self.cfg.load_in_4bit) and not skip_move_to_device ): - # TODO: validate this conditional self.model.to(f"{str(get_device_type())}:{self.cfg.local_rank}") if get_device_count() > 1 and int(os.getenv("WORLD_SIZE", "1")) == 1: @@ -430,7 +427,17 @@ def _set_device_map_config(self): self.model_kwargs["torch_dtype"] = self.cfg.torch_dtype - if not is_deepspeed_zero3_enabled(): + is_ds_zero3 = is_deepspeed_zero3_enabled() + + # FSDP requires control over device placement, so don't set device_map when FSDP is enabled + if self.is_fsdp_enabled: + # For QLoRA + FSDP, we still need to set device_map to "auto" for proper initialization + if self.is_qlora_and_fsdp_enabled: + self.model_kwargs["device_map"] = { + "": int(os.environ.get("LOCAL_RANK", 0)) + } + # For other FSDP cases, don't set device_map at all + elif not is_ds_zero3: self.model_kwargs["device_map"] = device_map cur_device = get_device_type() @@ -499,7 +506,7 @@ def _set_quantization_config(self): "bnb_4bit_quant_storage": torch.bfloat16, } if self.cfg.model_config_type in ["jamba", "qwen2_moe"] and not ( - self.cfg.deepspeed or self.cfg.fsdp + self.cfg.deepspeed or self.is_fsdp_enabled ): # for some reason, this causes the loss to be off by an order of magnitude # but deepspeed needs this still in bfloat16 @@ -604,9 +611,21 @@ def _configure_zero3_memory_efficient_loading( def _build_model(self) -> bool: """Load model, with load strategy depending on config.""" skip_move_to_device = False + if self.is_fsdp_enabled: + if self.cfg.fsdp_config.cpu_ram_efficient_loading: + skip_move_to_device = True + # Don't delete device_map for QLoRA + FSDP - it was set correctly in _set_device_map + if ( + "device_map" in self.model_kwargs + and not self.is_qlora_and_fsdp_enabled + ): + del self.model_kwargs["device_map"] + elif self.is_qlora_and_fsdp_enabled: + skip_move_to_device = True + if ( - self.qlora_fsdp - and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading + self.is_qlora_and_fsdp_enabled + and self.cfg.fsdp_config.cpu_ram_efficient_loading and ( self.cfg.model_config_type == "dbrx" or self.cfg.qlora_sharded_model_loading @@ -632,12 +651,6 @@ def _build_model(self) -> bool: and not self.cfg.trust_remote_code and not self.cfg.gptq ): - # TODO: Do we need to open this up for all models? - if self.cfg.fsdp and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: - skip_move_to_device = True - if "device_map" in self.model_kwargs: - del self.model_kwargs["device_map"] - # Please don't remove underscore binding without reading the fn docstring. _ = self._configure_zero3_memory_efficient_loading() @@ -691,33 +704,22 @@ def _build_model(self) -> bool: trust_remote_code=self.cfg.trust_remote_code or False, **self.model_kwargs, ) + elif self.cfg.gptq: + self.model = self.auto_model_loader.from_pretrained( + self.base_model, + config=self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + **self.model_kwargs, + ) else: - if self.cfg.gptq: - self.model = self.auto_model_loader.from_pretrained( - self.base_model, - config=self.model_config, - trust_remote_code=self.cfg.trust_remote_code or False, - **self.model_kwargs, - ) - else: - if ( - self.cfg.fsdp - and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading - ): - # disabling either of these two still leads to VRAM spike before setting back down - skip_move_to_device = True - if "device_map" in self.model_kwargs: - del self.model_kwargs["device_map"] - - # Please don't remove underscore binding without reading the fn docstring. - _ = self._configure_zero3_memory_efficient_loading() - - self.model = self.auto_model_loader.from_pretrained( - self.base_model, - config=self.model_config, - trust_remote_code=self.cfg.trust_remote_code or False, - **self.model_kwargs, - ) + # Please don't remove underscore binding without reading the fn docstring. + _ = self._configure_zero3_memory_efficient_loading() + self.model = self.auto_model_loader.from_pretrained( + self.base_model, + config=self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + **self.model_kwargs, + ) if is_deepspeed_zero3_enabled(): skip_move_to_device = True @@ -753,8 +755,8 @@ def _prepare_model_for_quantization(self): skip_prepare_model_for_kbit_training = True if ( - self.qlora_fsdp - or (self.cfg.fsdp and self.cfg.fsdp_config.fsdp_cpu_ram_efficient_loading) + self.is_qlora_and_fsdp_enabled + or (self.is_fsdp_enabled and self.cfg.fsdp_config.cpu_ram_efficient_loading) or is_deepspeed_zero3_enabled() ): # Make sure everything is in the same dtype diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 81d4e94716..2544429e61 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -94,10 +94,14 @@ def _apply_chunked_cross_entropy_patch(self): def _apply_fsdp_patches(self): """Apply patches for FSDP configurations.""" - if self.cfg.fsdp_config and str(self.cfg.fsdp_config.fsdp_version) == "2": + if self.cfg.fsdp_config and str(self.cfg.fsdp_version) == "2": from axolotl.monkeypatch.accelerate.fsdp2 import patch_accelerate_fsdp2 patch_accelerate_fsdp2() + if self.cfg.rl: + from axolotl.monkeypatch.trainer.trl import patch_trl_prepare_fsdp2 + + patch_trl_prepare_fsdp2() # if self.cfg.fsdp_config: # # see transformers#39152 diff --git a/src/axolotl/loaders/utils.py b/src/axolotl/loaders/utils.py index 28c9350853..4b93d14acf 100644 --- a/src/axolotl/loaders/utils.py +++ b/src/axolotl/loaders/utils.py @@ -195,9 +195,11 @@ def ensure_dtype(model: PreTrainedModel, dtype: torch.dtype = torch.bfloat16): bias_mismatch = module.bias.dtype != dtype if weight_mismatch: - print(f"Converting module {name}.weight: {module.weight.dtype} -> {dtype}") + LOG.debug( + f"Converting module {name}.weight: {module.weight.dtype} -> {dtype}" + ) if bias_mismatch: - print(f"Converting module {name}.bias: {module.bias.dtype} -> {dtype}") + LOG.debug(f"Converting module {name}.bias: {module.bias.dtype} -> {dtype}") if weight_mismatch or bias_mismatch: module.to(dtype) diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index 955c06cbe6..803659232c 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -2,102 +2,65 @@ monkeypatch for accelerate fsdp2 fix when modifying ordereddict during interation, and saving full state dicts """ +import copy +import functools import sys import torch +from torch import nn +from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.logging import get_logger LOG = get_logger(__name__) -def fsdp2_load_full_state_dict(accelerator, model: torch.nn.Module, full_sd: dict): +def fsdp2_load_full_state_dict( + _accelerator, model: torch.nn.Module, full_sd: dict, offload_to_cpu: bool = False +): """ Loads the full state dict (could be only on rank 0) into the sharded model. This is done by broadcasting the parameters from rank 0 to all other ranks. This function modifies the model in-place. - Args: accelerator (`Accelerator`): The accelerator instance model (`torch.nn.Module`): The model to load the state dict into, expected to be on meta device or a VRAM spike can occur full_sd (`dict`): The full state dict to load, can only be on rank 0 """ - import torch.distributed as dist from torch.distributed.tensor import distribute_tensor - # Model was previously copied to meta device - meta_sharded_sd = model.state_dict() - sharded_sd = {} - - # Rank 0 distributes the full state dict to other ranks - def _infer_parameter_dtype(model, param_name, empty_param): - try: - old_param = model.get_parameter_or_buffer(param_name) - except AttributeError: - # Need this for LORA, as there some params are not *parameters* of sorts - base_param_name, local_param_name = param_name.rsplit(".", 1) - submodule = model.get_submodule(base_param_name) - old_param = getattr(submodule, local_param_name) - - is_torch_e4m3fn_available = hasattr(torch, "float8_e4m3fn") - casting_dtype = None - is_param_float8_e4m3fn = ( - is_torch_e4m3fn_available and empty_param.dtype == torch.float8_e4m3fn - ) - - if empty_param.dtype.is_floating_point and not is_param_float8_e4m3fn: - casting_dtype = old_param.dtype - - return old_param is not None and old_param.is_contiguous(), casting_dtype - - def _cast_and_contiguous(tensor, to_contiguous, dtype): - if dtype is not None: - tensor = tensor.to(dtype=dtype) - if to_contiguous: - tensor = tensor.contiguous() - return tensor + LOG.info("Broadcasting full state dict to all ranks...") + import time - param_names = sorted(meta_sharded_sd.keys()) + start_time = time.time() - for param_name in param_names: - mesh = meta_sharded_sd[param_name].device_mesh - if accelerator.is_main_process: - full_param = full_sd[param_name].detach().cuda() - dist.broadcast(full_param, src=0, group=mesh.get_group()) - sharded_tensor = distribute_tensor( - full_param, mesh, sharded_sd[param_name].placements - ) - to_contiguous, casting_dtype = _infer_parameter_dtype( - model, - param_name, - full_param, - ) - sharded_tensor = _cast_and_contiguous( - sharded_tensor, to_contiguous, casting_dtype - ) - sharded_sd[param_name] = sharded_tensor - else: - full_tensor = torch.empty( - sharded_sd[param_name].size(), - device="cuda", - dtype=sharded_sd[param_name].dtype, - ) - dist.broadcast(full_tensor, src=0, group=mesh.get_group()) - sharded_tensor = distribute_tensor( - full_tensor, mesh, sharded_sd[param_name].placements - ) - to_contiguous, casting_dtype = _infer_parameter_dtype( - model, - param_name, + meta_sharded_sd = model.state_dict() + sharded_sd = {} + for param_name, full_tensor in full_sd.items(): + sharded_meta_param = meta_sharded_sd.get(param_name) + full_tensor = full_tensor.to(sharded_meta_param.dtype).to(torch.device("cuda")) + if hasattr(sharded_meta_param, "device_mesh"): + sharded_param = distribute_tensor( full_tensor, + sharded_meta_param.device_mesh, + sharded_meta_param.placements, + src_data_rank=0, ) - sharded_tensor = _cast_and_contiguous( - sharded_tensor, to_contiguous, casting_dtype - ) - sharded_sd[param_name] = sharded_tensor - - # we set `assign=True` because our params are on meta device - model.load_state_dict(sharded_sd, assign=True) + else: + sharded_param = full_tensor + + if offload_to_cpu: + sharded_param = sharded_param.cpu() + + sharded_sd[param_name] = nn.Parameter(sharded_param) + del full_tensor + full_sd[param_name] = None + model.load_state_dict(sharded_sd, assign=True, strict=True) + end_time = time.time() + LOG.debug( + f"Time taken to load full state dict: {(end_time - start_time):.2f} seconds" + ) + log_gpu_memory_usage(LOG, "Memory usage after broadcasting full state dict", 0) return model @@ -191,17 +154,195 @@ def get_state_dict(self, model, unwrap=True): return state_dict -def patch_accelerate_fsdp2(): - import accelerate - from accelerate.utils import fsdp_utils +def _process_lora_module_for_fsdp(module, fsdp2_kwargs): + """Helper function to process LoRA modules for FSDP2.""" + from torch.distributed.fsdp import fully_shard - fsdp_utils.fsdp2_load_full_state_dict = fsdp2_load_full_state_dict - setattr( - sys.modules["accelerate.utils.fsdp_utils"], - "fsdp2_load_full_state_dict", - fsdp2_load_full_state_dict, + log_bias_dtype_mismatch = False + + # Linear4Bit will keep it's bias term in fp32. If the weight dtype is in bf16 we are not able to + # wrap this. Therefore we must ensure the bias has the same dtype as the weight + if module.base_layer.bias is not None: + if module.base_layer.weight.dtype != module.base_layer.bias.dtype: + log_bias_dtype_mismatch = True + module.base_layer.bias.data = module.base_layer.bias.data.to( + module.base_layer.weight.dtype + ) + + for active_adapter in module.active_adapters: + if module.lora_A: + fully_shard(module.lora_A[active_adapter], **fsdp2_kwargs) + if module.lora_B: + fully_shard(module.lora_B[active_adapter], **fsdp2_kwargs) + if module.lora_embedding_A: + fully_shard(module.lora_embedding_A[active_adapter], **fsdp2_kwargs) + if module.lora_embedding_B: + fully_shard(module.lora_embedding_B[active_adapter], **fsdp2_kwargs) + if module.lora_magnitude_vector: + fully_shard(module.lora_magnitude_vector[active_adapter], **fsdp2_kwargs) + return log_bias_dtype_mismatch + + +def fsdp2_prepare_model(accelerator, model: torch.nn.Module) -> torch.nn.Module: + """Prepares the model for FSDP2 in-place. Also returns the model to avoid misuse of the original model. + + Args: + accelerator (`Accelerator`): The accelerator instance + model (`torch.nn.Module`): The model to prepare + + Returns: + `torch.nn.Module`: Prepared model + """ + from accelerate.utils import get_module_children_bottom_up, is_compiled_module + from accelerate.utils.fsdp_utils import fsdp2_prepare_auto_wrap_policy + from accelerate.utils.modeling import get_non_persistent_buffers + from peft import PeftModel + from peft.tuners.lora import LoraLayer + from torch.distributed.fsdp import ( + CPUOffloadPolicy, + FSDPModule, + MixedPrecisionPolicy, + fully_shard, + ) + + is_type_fsdp = isinstance(model, FSDPModule) or ( + is_compiled_module(model) + and isinstance(model._orig_mod, FSDPModule) # pylint: disable=protected-access + ) + if is_type_fsdp: + return model + + fsdp2_plugin = accelerator.state.fsdp_plugin + + original_sd = model.state_dict() + + from torch.distributed.fsdp.wrap import ( + size_based_auto_wrap_policy, + transformer_auto_wrap_policy, ) + # We need the `auto_wrap_policy` original type to create a custom poilicy function for sharding + # This is because `fully_shard` doesn't support old auto wrap policies, rather we have to imitate the behaviour + if fsdp2_plugin.auto_wrap_policy is transformer_auto_wrap_policy: + pass # auto_wrap_policy_type = "transformer" + elif fsdp2_plugin.auto_wrap_policy is size_based_auto_wrap_policy: + pass # auto_wrap_policy_type = "size" + + # We set `auto_wrap_policy` to `functools.partial` to avoid creating it again + # This is because of `apply_activation_checkpointing` which will can reuse this function + fsdp2_plugin.set_auto_wrap_policy(model) + + if fsdp2_plugin.activation_checkpointing: + from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import ( + CheckpointImpl, + apply_activation_checkpointing, + checkpoint_wrapper, + ) + + # Apply activation checkpointing before applying `fully_shard` + apply_activation_checkpointing( + model, + checkpoint_wrapper_fn=functools.partial( + checkpoint_wrapper, + checkpoint_impl=CheckpointImpl.NO_REENTRANT, + ), + auto_wrap_policy=fsdp2_plugin.auto_wrap_policy, + ) + + fsdp2_kwargs = { + "reshard_after_forward": fsdp2_plugin.reshard_after_forward, + "offload_policy": fsdp2_plugin.cpu_offload, + # `fully_shard` doesn't accept `None` in case of `MixedPrecisionPolicy` + "mp_policy": fsdp2_plugin.mixed_precision_policy or MixedPrecisionPolicy(), + } + + model_has_params4bit = False + for _, param in model.named_parameters(): + # this is a temporary fix whereby loading models with bnb params cannot be moved from + # GPU to a meta device due with FSDP2 because torch operations don't return the original class type + # bypassing the move to meta will still cause the VRAM spike, but at least it still will load + if param.__class__.__name__ == "Params4bit": + model_has_params4bit = True + break + + if fsdp2_plugin.cpu_ram_efficient_loading and not model_has_params4bit: + # Context: `fully_shard` moves the model to GPU if it was on CPU, however it can also be on `meta` and then it stays there even after `fully_shard` + # For this reason, we need to move the model to `meta` device, as then sharding happens on `meta` device + # If we kept the model on CPU (`cpu_ram_efficient_loading` has model be on CPU on all ranks, though non-main ranks only have `torch.emtpy`), `fully_shard` would move it to GPU + # Afterwards, when we call `fsdp2_load_full_state_dict`, us creating the state_dict would result into briefly having two copies of model state_dict on the GPU -> VRAM spike + + # We need to keep the original non-persistent buffers, as those MAY not be in the state_dict, resulting in them staying on meta device + # Also, these buffers aren't getting sharded by default + # We get the FQNs of all non-persistent buffers, to re-register them after + non_persistent_buffer_fqns = get_non_persistent_buffers( + model, recurse=True, fqns=True + ) + original_non_persistent_buffers = copy.deepcopy( + {k: v for k, v in model.named_buffers() if k in non_persistent_buffer_fqns} + ) + # We move the model to meta device, as then sharding happens on meta device + model = model.to(torch.device("meta")) + # We need to re-tie the weights, not exactly sure why, but if we don't do this, reference to `lm_head/embed_tokens` stay hanging -> more VRAM usage + # We assume `transformers` models have a `tie_weights` method if they support it + if hasattr(model, "tie_weights"): + model.tie_weights() + + is_peft_model = isinstance(model, PeftModel) + + auto_wrap_policy = fsdp2_prepare_auto_wrap_policy(fsdp2_plugin, model) + log_bias_dtype_mismatch = False + if auto_wrap_policy is not None: + for module in get_module_children_bottom_up(model)[:-1]: + if is_peft_model and isinstance(module, LoraLayer): + module_log_bias_mismatch = _process_lora_module_for_fsdp( + module, fsdp2_kwargs + ) + log_bias_dtype_mismatch |= module_log_bias_mismatch + if auto_wrap_policy(module) and not isinstance(module, FSDPModule): + fully_shard(module, **fsdp2_kwargs) + + fully_shard(model, **fsdp2_kwargs) + + if log_bias_dtype_mismatch: + LOG.warning( + "Bias dtype mismatch detected in LoRA base linear layer. Bias parameters have been cast to weight dtype." + ) + + if fsdp2_plugin.cpu_ram_efficient_loading: + offload_to_cpu = isinstance(fsdp2_plugin.cpu_offload, CPUOffloadPolicy) + fsdp2_load_full_state_dict( + accelerator, model, original_sd, offload_to_cpu=offload_to_cpu + ) + + if fsdp2_plugin.cpu_ram_efficient_loading and not model_has_params4bit: + # We re-register the buffers, as they may not be in the state_dict + for fqn, buffer_tensor in original_non_persistent_buffers.items(): + buffer_tensor = buffer_tensor.to(accelerator.device) + + if "." in fqn: + parent_fqn, local_buffer_name = fqn.rsplit(".", 1) + parent_module = model.get_submodule(parent_fqn) + else: + local_buffer_name = fqn + parent_module = model + + parent_module.register_buffer( + local_buffer_name, buffer_tensor, persistent=False + ) + + # We need to tie the weights again, as call to `load_full_state_dict` breaks the tie + # Needs to be called both here and above + # removing this call makes the have slightly different loss + # removing the call above leads to extra memory usage as explained in the comment above + if hasattr(model, "tie_weights"): + model.tie_weights() + return model + + +def patch_accelerate_fsdp2(): + import accelerate + + accelerate.accelerator.fsdp2_prepare_model = fsdp2_prepare_model accelerate.Accelerator.get_state_dict = get_state_dict setattr( sys.modules["accelerate"], diff --git a/src/axolotl/monkeypatch/attention/flex_attn.py b/src/axolotl/monkeypatch/attention/flex_attn.py index 3652a30b33..98aead8324 100644 --- a/src/axolotl/monkeypatch/attention/flex_attn.py +++ b/src/axolotl/monkeypatch/attention/flex_attn.py @@ -6,6 +6,10 @@ import torch import transformers +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + def patch_flex_wrapper(**flex_attn_compile_kwargs): # TODO remove this patch when transformers#37285 is merged and in a release @@ -46,10 +50,15 @@ def __init__(self, training): # cause errors. The suggested fix is to compile with "max-autotune-no-cudagraphs" # see https://github.com/pytorch/pytorch/issues/146260 for training self.training = training + LOG.info( + "Compiling flex attention with kwargs: %s. This may take a while...", + flex_attn_compile_kwargs, + ) self._compiled_flex_attention = torch.compile( flex_attention, **flex_attn_compile_kwargs, ) + LOG.info("Flex attention compiled successfully.") self._is_flex_compiled = True def __call__(self): diff --git a/src/axolotl/monkeypatch/trainer/trl.py b/src/axolotl/monkeypatch/trainer/trl.py new file mode 100644 index 0000000000..bca9f92dec --- /dev/null +++ b/src/axolotl/monkeypatch/trainer/trl.py @@ -0,0 +1,13 @@ +"""Monkeypatch for TRL trainer FSDP preparation.""" + + +def prepare_fsdp(model, accelerator): + from axolotl.monkeypatch.accelerate.fsdp2 import fsdp2_prepare_model + + return fsdp2_prepare_model(accelerator, model) + + +def patch_trl_prepare_fsdp2(): + import trl.models.utils + + trl.models.utils.prepare_fsdp = prepare_fsdp diff --git a/src/axolotl/train.py b/src/axolotl/train.py index a731316b61..35c58501c5 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -15,7 +15,6 @@ import torch import transformers.modelcard -from accelerate.utils import save_fsdp_model from datasets import Dataset from huggingface_hub.errors import OfflineModeIsEnabled from peft import PeftConfig, PeftModel @@ -68,7 +67,7 @@ def setup_model_and_tokenizer( `None`), and processor (if multimodal, else `None`). """ # Load tokenizer - LOG.debug(f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}") + LOG.debug(f"Loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}") tokenizer = load_tokenizer(cfg) # Load processor for multimodal models if needed @@ -76,11 +75,8 @@ def setup_model_and_tokenizer( if cfg.is_multimodal: processor = load_processor(cfg, tokenizer) - # Load the model and peft_config - msg = "loading model" - if cfg.adapter: - msg += " and peft_config..." - LOG.debug(msg) + # Load the model + LOG.debug("Loading model") model_loader = ModelLoader(cfg, tokenizer, processor=processor) model, peft_config = model_loader.load() @@ -264,15 +260,6 @@ def save_trained_model( "QAT modules have been converted for PTQ. Please ensure you quantize " "your model weights with `axolotl quantize`." ) - - # Handle FSDP state dict type - state_dict_type = "FULL_STATE_DICT" - if trainer.is_fsdp_enabled and str(cfg.fsdp_config.fsdp_version) != "2": - if cfg.fsdp_final_state_dict_type: - state_dict_type = cfg.fsdp_final_state_dict_type - trainer.accelerator.state.fsdp_plugin.set_state_dict_type(state_dict_type) - LOG.info(f"Set FSDP state dict type to {state_dict_type} for saving.") - # Handle ReLoRA early return case if cfg.relora_steps: if cfg.adapter == "lora" and not (cfg.load_in_4bit or cfg.load_in_8bit): @@ -281,22 +268,19 @@ def save_trained_model( # final model weights have already been saved by `ReLoRACallback.on_train_end` return - if cfg.fsdp: - # TODO: do we need this fix? https://huggingface.co/docs/accelerate/usage_guides/fsdp#saving-and-loading - # only save on rank 0, otherwise it corrupts output on multi-GPU when multiple - # processes attempt to write the same file - if ( - state_dict_type == "SHARDED_STATE_DICT" - and cfg.fsdp_config.fsdp_state_dict_type == "SHARDED_STATE_DICT" - ): - save_fsdp_model( - trainer.accelerator.state.fsdp_plugin, - trainer.accelerator, - trainer.model, - cfg.output_dir, + if trainer.is_fsdp_enabled: + if cfg.fsdp_config or cfg.fsdp: + if cfg.fsdp_config.final_state_dict_type: + state_dict_type = cfg.fsdp_config.final_state_dict_type + else: + state_dict_type = cfg.fsdp_config.state_dict_type + trainer.accelerator.state.fsdp_plugin.set_state_dict_type(state_dict_type) + trainer.save_model(cfg.output_dir) + if state_dict_type == "SHARDED_STATE_DICT": + LOG.info( + "The final model was saved with a sharded state dict. Please ensure you merge " + "the sharded weights with `merge-sharded-fsdp-weights`." ) - elif state_dict_type == "FULL_STATE_DICT": - trainer.save_model(cfg.output_dir) elif cfg.deepspeed and is_deepspeed_zero3_enabled(): # Copied over from: https://github.com/huggingface/accelerate/blob/5ae611118057232f441055f7ef9ba0b0f2b8d533/docs/source/usage_guides/deepspeed.md#saving-and-loading trainer.accelerator.wait_for_everyone() diff --git a/src/axolotl/utils/bench.py b/src/axolotl/utils/bench.py index d1e972c811..dae53eddfb 100644 --- a/src/axolotl/utils/bench.py +++ b/src/axolotl/utils/bench.py @@ -1,6 +1,7 @@ """Benchmarking and measurement utilities""" import functools +import logging import torch from transformers.utils.import_utils import is_torch_npu_available @@ -91,21 +92,27 @@ def gpu_memory_usage_smi(device=0): return 0.0 -def log_gpu_memory_usage(log, msg, device): - cur_device = get_device_type() +def log_gpu_memory_usage( + log: logging.Logger | logging.LoggerAdapter, + msg: str = "", + device: int | torch.device = 0, +): + cur_device_type = str(get_device_type()) if torch.backends.mps.is_available(): usage, cache, misc = mps_memory_usage_all() - elif "npu" in str(cur_device) and is_torch_npu_available(): + elif "npu" in cur_device_type and is_torch_npu_available(): usage, cache, misc = npu_memory_usage_all(device) - else: + elif "gpu" in cur_device_type and torch.cuda.is_available(): usage, cache, misc = gpu_memory_usage_all(device) + else: + return extras = [] if cache > 0: extras.append(f"+{cache:.03f}GB cache") if misc > 0: extras.append(f"+{misc:.03f}GB misc") + msg = f"{cur_device_type} memory usage:" if not msg else msg log.info( - f"{str(cur_device)} memory usage {msg}: {usage:.03f}GB ({', '.join(extras)})", + f"{msg} {usage:.03f}GB ({', '.join(extras)})", stacklevel=2, ) - return usage, cache, misc diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 745a0c8ce5..fb17b259ff 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -115,10 +115,10 @@ def normalize_config(cfg): "chrf", ] choose_device(cfg) - cfg.ddp = cfg.ddp if cfg.ddp is not None else cfg.world_size != 1 - if cfg.ddp: + if cfg.world_size != 1: cfg.device_map = {"": int(os.environ.get("LOCAL_RANK", 0))} - cfg.batch_size = cfg.batch_size * cfg.world_size + if cfg.fsdp or cfg.fsdp_config or cfg.ddp: + cfg.batch_size = cfg.batch_size * cfg.world_size if not cfg.use_ray: # delay resolving dtype until on worker node when launching with ray @@ -313,3 +313,16 @@ def prepare_plugins(cfg): plugin_manager = PluginManager.get_instance() for plugin_name in cfg["plugins"]: plugin_manager.register(plugin_name) + + +# TODO @SalmanMohammadi remove this function in 0.12 +def migrate_fsdp_config(cfg): + if cfg.get("fsdp_config"): + fsdp_config_keys = cfg.fsdp_config.keys() + if "fsdp_version" in fsdp_config_keys: + cfg.fsdp_version = cfg.fsdp_config.pop("fsdp_version") + + for key in list(fsdp_config_keys): + if key.startswith("fsdp_") and key != "fsdp_version": + cfg.fsdp_config[key.replace("fsdp_", "")] = cfg.fsdp_config[key] + del cfg.fsdp_config[key] diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 323e0877de..de80d1b79e 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -203,7 +203,9 @@ class AxolotlInputConfig( }, ) dataset_processes: int | None = Field( - default=min(int(os.environ.get("AXOLOTL_DATASET_PROCESSES", 32)), os.cpu_count()), # type: ignore[type-var] + default=min( + int(os.environ.get("AXOLOTL_DATASET_PROCESSES", 32)), os.cpu_count() + ), # type: ignore[type-var] json_schema_extra={ "description": "The maximum number of processes to use while preprocessing your input dataset. This defaults to `os.cpu_count()` if not set." }, @@ -572,14 +574,24 @@ class AxolotlInputConfig( }, ) fsdp: list[str] | None = Field( - default=None, json_schema_extra={"description": "FSDP configuration"} + default=None, + json_schema_extra={"description": "FSDP configuration"}, + deprecated="Configuring FSDP using `fsdp` is deprecated. Please use `fsdp_config` instead. ", ) + # TODO @SalmanMohammadi strongly type this as its own schema fsdp_config: dict[str, Any] | None = Field( default=None, json_schema_extra={"description": "FSDP configuration options"} ) + fsdp_version: int | None = Field( + default=None, + json_schema_extra={"description": "FSDP version"}, + ) fsdp_final_state_dict_type: ( Literal["FULL_STATE_DICT", "LOCAL_STATE_DICT", "SHARDED_STATE_DICT"] | None - ) = None + ) = Field( + default=None, + deprecated="Configuring FSDP final state dict type using `fsdp_final_state_dict_type` is deprecated. Please use `fsdp_config.final_state_dict_type` instead.", + ) val_set_size: float | None = Field( default=0.0, @@ -949,11 +961,9 @@ def check_multigpu_lora_kernels(cls, data): or data.get("lora_o_kernel") ): capabilities = data.get("capabilities") - is_fsdp = data.get("fsdp") is not None - is_fsdp2 = ( - data.get("fsdp_config") is not None - and str(data.get("fsdp_config").get("fsdp_version")) == "2" - ) + is_fsdp = data.get("fsdp_config") is not None + is_fsdp2 = is_fsdp and str(data.get("fsdp_version")) == "2" + if capabilities and capabilities.get("n_gpu", 0) > 1 and not is_fsdp2: if is_fsdp: raise ValueError( @@ -987,11 +997,8 @@ def check_auto_enable_lora_kernels(cls, data): # Check multi-GPU compatibility capabilities = data.get("capabilities") is_multi_gpu = capabilities and capabilities.get("n_gpu", 0) > 1 - is_fsdp = data.get("fsdp") is not None - is_fsdp2 = ( - data.get("fsdp_config") is not None - and str(data.get("fsdp_config").get("fsdp_version")) == "2" - ) + is_fsdp = data.get("fsdp_config") is not None + is_fsdp2 = is_fsdp and str(data.get("fsdp_version")) == "2" if ( not is_multi_gpu @@ -1114,19 +1121,92 @@ def check_qat_config(cls, data): torch_version = str(torch.__version__).split("+", maxsplit=1)[0] - if ( - data.get("fsdp") - and data.get("fsdp_config") - and str(data["fsdp_config"].get("fsdp_version")) == "2" - ): + if version.parse(torch_version) < version.parse("2.6.0"): + raise ValueError("QAT is not supported on torch version < 2.6.0") + + return data + + @model_validator(mode="before") + @classmethod + def check_fsdp_torch_version(cls, data): + env_capabilities = data.get("env_capabilities", {}) + torch_version = env_capabilities.get("torch_version") + + if torch_version is None: + import torch + + torch_version = str(torch.__version__).split("+", maxsplit=1)[0] + + if data.get("fsdp_config") and str(data.get("fsdp_version")) == "2": if version.parse(torch_version) < version.parse("2.7.0"): + raise ValueError("FSDP2 is not supported on torch version < 2.7.0") + + return data + + @model_validator(mode="before") + @classmethod + def check_fsdp_version(cls, data): + fsdp_config = data.get("fsdp_config", {}) + if fsdp_config and str(data.get("fsdp_version")) != "2": + LOG.info( + "FSDP1 will be deprecated in an upcoming release of Axolotl." + "We recommend that you use FSDP version 2 for better performance and compatibility. " + "Please see this link for more details: https://docs.axolotl.ai/docs/multi-gpu.html#sec-fsdp " + "For more details on migrating your config. " + ) + return data + + @model_validator(mode="before") + @classmethod + def check_fsdp2_base_model_quant_ram_efficient_loading(cls, data): + fsdp_config = data.get("fsdp_config") + if fsdp_config and data.get("fsdp_version") == 2: + if fsdp_config.get("cpu_ram_efficient_loading") and ( + data.get("load_in_8bit") or data.get("load_in_4bit") + ): raise ValueError( - "FSDP2 and QAT are not supported on torch version < 2.7.0" + "FSDP2 does not support load_in_8bit or load_in_4bit with cpu_ram_efficient_loading. Please do one of the following: use DeepSpeed, " + "set fsdp_version to 1, or disable cpu_ram_efficient_loading." ) + return data - if version.parse(torch_version) < version.parse("2.6.0"): - raise ValueError("QAT is not supported on torch version < 2.6.0") + @model_validator(mode="before") + @classmethod + def check_fsdp2_base_model_quant_dpo(cls, data): + if data.get("fsdp_version") == 2 and data.get("rl") in [ + RLType.DPO, + RLType.KTO, + RLType.ORPO, + RLType.IPO, + ]: + if data.get("load_in_8bit") or data.get("load_in_4bit"): + raise ValueError( + "FSDP2 does not support load_in_8bit or load_in_4bit with DPO. Please use DeepSpeed or set `fsdp_version` to 1." + ) + + return data + @model_validator(mode="before") + @classmethod + def check_fsdp_version_in_fsdp_config(cls, data): + if fsdp_config := data.get("fsdp_config"): + if fsdp_config.get("fsdp_version"): + LOG.warning( + "Configuring `fsdp_version` in `fsdp_config` is deprecated. " + "Please configure `fsdp_version` as a top-level field." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_fsdp_config_kwargs_prefix(cls, data): + if fsdp_config := data.get("fsdp_config"): + for key, _ in fsdp_config.items(): + if key.startswith("fsdp_"): + LOG.warning_once( + "Configuring FSDP fields with the `fsdp_` prefix is deprecated. " + "Please omit the `fsdp_` prefix from the any fields in `fsdp_config`." + ) return data @model_validator(mode="before") diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index c2d4b4af41..57959c4fa8 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -574,15 +574,6 @@ def check_fused_lora(self): raise ValueError("Fused modules are not supported with LoRA/QLoRA") return self - @model_validator(mode="after") - def hint_lora_8bit(self): - loftq = ( - self.peft and self.peft.loftq_config and self.peft.loftq_config.loftq_bits - ) - if not self.load_in_8bit and self.adapter == "lora" and not loftq: - LOG.warning("We recommend setting `load_in_8bit: true` for LORA finetuning") - return self - @model_validator(mode="before") @classmethod def warn_qlora_zero3_w_use_reentrant(cls, data): @@ -786,7 +777,7 @@ def check_fsdp_offload_w_8bit_optimizer(cls, data): @classmethod def check_fsdp_sharded_state_dict_w_safetensors(cls, data): if ( - data.get("fsdp") + data.get("fsdp_config") and data.get("save_safetensors") and data.get("fsdp_config") and data["fsdp_config"].get("fsdp_state_dict_type") == "SHARDED_STATE_DICT" @@ -1000,7 +991,7 @@ def check_relora(self): if self.adapter not in ("lora", "qlora"): raise ValueError("cfg.adapter must be lora or qlora to use ReLoRA") - if self.fsdp: + if self.fsdp or self.fsdp_config: raise ValueError("fsdp not supported with ReLoRA") if self.deepspeed: diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 2ef637232c..9224202e1a 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -563,37 +563,39 @@ def setup_deepspeed_env(cfg, stage=None): def setup_fsdp_envs(cfg): os.environ["ACCELERATE_USE_FSDP"] = "true" - if str(cfg.fsdp_config.fsdp_version) == "2": + + # TODO @SalmanMohammadi remove FSDP1 args in 0.12 + if str(cfg.fsdp_version) == "2": os.environ["FSDP_VERSION"] = "2" - if cfg.fsdp_config.fsdp_activation_checkpointing: + if cfg.fsdp_config.activation_checkpointing: os.environ["FSDP_ACTIVATION_CHECKPOINTING"] = "true" - if cfg.fsdp_config.fsdp_offload_params: + if cfg.fsdp_config.offload_params: os.environ["FSDP_OFFLOAD_PARAMS"] = "true" - if cfg.fsdp_config.fsdp_sync_module_states: + if cfg.fsdp_config.sync_module_states: os.environ["FSDP_SYNC_MODULE_STATES"] = "true" - if cfg.fsdp_config.fsdp_cpu_ram_efficient_loading: + if cfg.fsdp_config.cpu_ram_efficient_loading: os.environ["FSDP_CPU_RAM_EFFICIENT_LOADING"] = "true" - if cfg.fsdp_config.fsdp_use_orig_params: + if cfg.fsdp_config.use_orig_params: os.environ["FSDP_USE_ORIG_PARAMS"] = "true" - if cfg.fsdp_config.fsdp_state_dict_type: - os.environ["FSDP_STATE_DICT_TYPE"] = cfg.fsdp_config.fsdp_state_dict_type - if cfg.fsdp_config.fsdp_auto_wrap_policy: - os.environ["FSDP_AUTO_WRAP_POLICY"] = cfg.fsdp_config.fsdp_auto_wrap_policy - if cfg.fsdp_config.fsdp_transformer_layer_cls_to_wrap: + if cfg.fsdp_config.state_dict_type: + os.environ["FSDP_STATE_DICT_TYPE"] = cfg.fsdp_config.state_dict_type + if cfg.fsdp_config.auto_wrap_policy: + os.environ["FSDP_AUTO_WRAP_POLICY"] = cfg.fsdp_config.auto_wrap_policy + if cfg.fsdp_config.transformer_layer_cls_to_wrap: os.environ["FSDP_TRANSFORMER_CLS_TO_WRAP"] = ( - cfg.fsdp_config.fsdp_transformer_layer_cls_to_wrap - ) - if cfg.fsdp_config.fsdp_reshard_after_forward is not None: - os.environ["FSDP_RESHARD_AFTER_FORWARD"] = ( - "true" if cfg.fsdp_config.fsdp_reshard_after_forward else "false" + cfg.fsdp_config.transformer_layer_cls_to_wrap ) + if cfg.fsdp_config.reshard_after_forward: + os.environ["FSDP_RESHARD_AFTER_FORWARD"] = "true" def prepare_optim_env(cfg): if not check_cuda_p2p_ib_support(): if os.getenv("NCCL_P2P_DISABLE") is None: os.environ["NCCL_P2P_DISABLE"] = "1" - if cfg.fsdp: + # TODO @SalmanMohammadi remove the cfg.fsdp check in 0.12 + if cfg.fsdp or cfg.fsdp_config: + cfg.fsdp = True if not cfg.fsdp else cfg.fsdp setup_fsdp_envs(cfg) elif cfg.deepspeed: stage = None @@ -657,11 +659,7 @@ def setup_trainer( """ from axolotl.core.builders import HFCausalTrainerBuilder, HFRLTrainerBuilder - if ( - cfg.torch_compile - and cfg.fsdp_config - and str(cfg.fsdp_config.fsdp_version) == "2" - ): + if cfg.torch_compile and cfg.fsdp_config and cfg.fsdp_version == 2: patch_evaluation_loop_for_fsdp2() if cfg.rl: trainer_builder = HFRLTrainerBuilder(cfg, model, tokenizer, processor) diff --git a/tests/e2e/multigpu/test_fsdp1.py b/tests/e2e/multigpu/test_fsdp1.py new file mode 100644 index 0000000000..fe0badbe21 --- /dev/null +++ b/tests/e2e/multigpu/test_fsdp1.py @@ -0,0 +1,326 @@ +"""Test module for FSDP1 multi-GPU functionality.""" + +# pylint: disable=duplicate-code + +import os +from pathlib import Path + +import pytest +import torch +import yaml +from accelerate.test_utils import execute_subprocess_async +from tbparse import SummaryReader +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import most_recent_subdir + +AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent + + +def verify_training_success(temp_dir): + """Verify that training completed successfully by checking artifacts and loss.""" + output_path = Path(temp_dir) + + model_files = list(output_path.glob("*.bin")) + list( + output_path.glob("*.safetensors") + ) + assert len(model_files) > 0, "No model files found - training may have failed" + + checkpoint_files = list(output_path.glob("checkpoint-*")) + assert ( + len(checkpoint_files) > 0 + ), "No checkpoint files found - training may have failed" + + tb_log_path = most_recent_subdir(temp_dir + "/runs") + if tb_log_path: + event_files = sorted(os.listdir(tb_log_path)) + if event_files: + event_file = os.path.join(tb_log_path, event_files[0]) + reader = SummaryReader(event_file) + df = reader.scalars + train_loss_df = df[df.tag == "train/train_loss"] + if len(train_loss_df) > 0: + final_loss = train_loss_df.value.values[-1] + assert not torch.isnan( + torch.tensor(final_loss) + ), f"Training loss is NaN: {final_loss}" + + +class TestFSDP1: + """Test class for FSDP1 functionality.""" + + @pytest.mark.parametrize( + "fsdp_cpu_ram_efficient_loading", + [True, False], + ) + def test_fft_sft(self, temp_dir, fsdp_cpu_ram_efficient_loading): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:10%]", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": "1", + "fsdp_config": { + "fsdp_offload_params": False, + "fsdp_cpu_ram_efficient_loading": fsdp_cpu_ram_efficient_loading, + "fsdp_transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "fsdp_state_dict_type": "FULL_STATE_DICT", + "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "fsdp_sharding_strategy": "FULL_SHARD", + "fsdp_sync_module_states": True, + "fsdp_use_orig_params": False, + }, + "use_tensorboard": True, + "bf16": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) + + @pytest.mark.parametrize( + "adapter_config", + [ + { + "adapter": "lora", + "load_in_4bit": False, + }, + { + "adapter": "qlora", + "load_in_4bit": True, + }, + ], + ) + def test_lora_sft(self, temp_dir, adapter_config): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:10%]", + }, + ], + "adapter": adapter_config["adapter"], + "load_in_4bit": adapter_config["load_in_4bit"], + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": "1", + "fsdp_config": { + "fsdp_offload_params": False, + "fsdp_cpu_ram_efficient_loading": True, + "fsdp_transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "fsdp_state_dict_type": "FULL_STATE_DICT", + "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "fsdp_sharding_strategy": "FULL_SHARD", + "fsdp_sync_module_states": True, + "fsdp_use_orig_params": False, + }, + "use_tensorboard": True, + "bf16": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) + + def test_dpo_fft(self, temp_dir): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "rl": "dpo", + "chat_template": "chatml", + "datasets": [ + { + "path": "Intel/orca_dpo_pairs", + "split": "train", + "type": "chatml.intel", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": "1", + "fsdp_config": { + "fsdp_offload_params": False, + "fsdp_cpu_ram_efficient_loading": True, + "fsdp_transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "fsdp_state_dict_type": "FULL_STATE_DICT", + "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "fsdp_sharding_strategy": "FULL_SHARD", + "fsdp_sync_module_states": True, + "fsdp_use_orig_params": False, + }, + "use_tensorboard": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) + + @pytest.mark.parametrize( + "adapter_config", + [ + { + "adapter": "lora", + "load_in_4bit": False, + }, + { + "adapter": "qlora", + "load_in_4bit": True, + }, + ], + ) + def test_dpo_lora(self, temp_dir, adapter_config): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "load_in_4bit": adapter_config["load_in_4bit"], + "rl": "dpo", + "chat_template": "chatml", + "sequence_len": 2048, + "adapter": adapter_config["adapter"], + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.01, + "datasets": [ + { + "path": "Intel/orca_dpo_pairs", + "split": "train", + "type": "chatml.intel", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": "1", + "fsdp_config": { + "fsdp_offload_params": False, + "fsdp_cpu_ram_efficient_loading": True, + "fsdp_transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "fsdp_state_dict_type": "FULL_STATE_DICT", + "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "fsdp_sharding_strategy": "FULL_SHARD", + "fsdp_sync_module_states": True, + "fsdp_use_orig_params": False, + }, + "use_tensorboard": True, + "bf16": "auto", + "tf32": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) diff --git a/tests/e2e/multigpu/test_fsdp2.py b/tests/e2e/multigpu/test_fsdp2.py new file mode 100644 index 0000000000..95ced1303e --- /dev/null +++ b/tests/e2e/multigpu/test_fsdp2.py @@ -0,0 +1,355 @@ +"""Test module for FSDP2 multi-GPU functionality.""" + +# pylint: disable=duplicate-code + +import os +from pathlib import Path + +import pytest +import torch +import yaml +from accelerate.test_utils import execute_subprocess_async +from tbparse import SummaryReader +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import most_recent_subdir, require_torch_2_7_0 + +AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent + + +def verify_training_success(temp_dir): + """Verify that training completed successfully by checking artifacts and loss.""" + output_path = Path(temp_dir) + + model_files = list(output_path.glob("*.bin")) + list( + output_path.glob("*.safetensors") + ) + assert len(model_files) > 0, "No model files found - training may have failed" + + checkpoint_files = list(output_path.glob("checkpoint-*")) + assert ( + len(checkpoint_files) > 0 + ), "No checkpoint files found - training may have failed" + + tb_log_path = most_recent_subdir(temp_dir + "/runs") + if tb_log_path: + event_files = sorted(os.listdir(tb_log_path)) + if event_files: + event_file = os.path.join(tb_log_path, event_files[0]) + reader = SummaryReader(event_file) + df = reader.scalars + train_loss_df = df[df.tag == "train/train_loss"] + if len(train_loss_df) > 0: + final_loss = train_loss_df.value.values[-1] + assert not torch.isnan( + torch.tensor(final_loss) + ), f"Training loss is NaN: {final_loss}" + + +class TestFSDP2: + """Test class for FSDP2 functionality.""" + + @require_torch_2_7_0 + @pytest.mark.parametrize( + "fsdp_cpu_ram_efficient_loading", + [True, False], + ) + def test_fft_sft(self, temp_dir, fsdp_cpu_ram_efficient_loading): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:10%]", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": fsdp_cpu_ram_efficient_loading, + "transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + "use_tensorboard": True, + "bf16": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) + + @require_torch_2_7_0 + @pytest.mark.parametrize("peft_use_dora", [True, False]) + def test_lora_sft(self, temp_dir, peft_use_dora): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:10%]", + }, + ], + "peft_use_dora": peft_use_dora, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": False, + "transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + "use_tensorboard": True, + "bf16": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) + + @require_torch_2_7_0 + def test_qlora_sft(self, temp_dir): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:10%]", + }, + ], + "load_in_4bit": True, + "adapter": "qlora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": False, + "transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + "use_tensorboard": True, + "bf16": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) + + @require_torch_2_7_0 + def test_dpo_fft(self, temp_dir): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "rl": "dpo", + "chat_template": "chatml", + "datasets": [ + { + "path": "Intel/orca_dpo_pairs", + "split": "train", + "type": "chatml.intel", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": False, + "transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + "use_tensorboard": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) + + @require_torch_2_7_0 + def test_dpo_lora(self, temp_dir): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "rl": "dpo", + "chat_template": "chatml", + "datasets": [ + { + "path": "Intel/orca_dpo_pairs", + "split": "train", + "type": "chatml.intel", + }, + ], + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": False, + "transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + "use_tensorboard": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) diff --git a/tests/e2e/multigpu/test_qwen2.py b/tests/e2e/multigpu/test_qwen2.py deleted file mode 100644 index bd57dbcef2..0000000000 --- a/tests/e2e/multigpu/test_qwen2.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -E2E tests for multigpu qwen2 -""" - -from pathlib import Path - -import pytest -import yaml -from accelerate.test_utils import execute_subprocess_async -from transformers.testing_utils import get_torch_dist_unique_port - -from axolotl.utils.dict import DictDefault - - -class TestMultiGPUQwen2: - """ - Test case for Llama models using LoRA - """ - - @pytest.mark.parametrize("base_model", ["Qwen/Qwen2-0.5B", "Qwen/Qwen2.5-0.5B"]) - def test_qlora_fsdp_dpo(self, base_model, temp_dir): - # pylint: disable=duplicate-code - cfg = DictDefault( - { - "base_model": base_model, - "load_in_4bit": True, - "rl": "dpo", - "chat_template": "chatml", - "sequence_len": 2048, - "adapter": "qlora", - "lora_r": 8, - "lora_alpha": 16, - "lora_dropout": 0.05, - "lora_target_linear": True, - "val_set_size": 0.01, - "datasets": [ - { - "path": "Intel/orca_dpo_pairs", - "split": "train", - "type": "chatml.intel", - }, - ], - "num_epochs": 1, - "max_steps": 2, - "warmup_steps": 20, - "micro_batch_size": 2, - "gradient_accumulation_steps": 2, - "output_dir": temp_dir, - "dataset_prepared_path": temp_dir + "/last_run_prepared", - "learning_rate": 0.00001, - "optimizer": "adamw_torch_fused", - "lr_scheduler": "cosine", - "flash_attention": True, - "bf16": "auto", - "tf32": True, - # "gradient_checkpointing": True, - "gradient_checkpointing_kwargs": { - "use_reentrant": False, - }, - "fsdp": [ - "full_shard", - "auto_wrap", - ], - "fsdp_config": { - "fsdp_limit_all_gathers": True, - "fsdp_offload_params": False, - "fsdp_sync_module_states": True, - "fsdp_use_orig_params": False, - "fsdp_cpu_ram_efficient_loading": False, - "fsdp_transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", - "fsdp_state_dict_type": "FULL_STATE_DICT", - "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", - "fsdp_sharding_strategy": "FULL_SHARD", - }, - } - ) - - # write cfg to yaml file - Path(temp_dir).mkdir(parents=True, exist_ok=True) - with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: - fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) - - execute_subprocess_async( - [ - "axolotl", - "train", - str(Path(temp_dir) / "config.yaml"), - "--num-processes", - "2", - "--main-process-port", - f"{get_torch_dist_unique_port()}", - ] - ) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 65069eb164..696e3b03c2 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -77,6 +77,18 @@ def is_min_2_6_0(): return unittest.skipUnless(is_min_2_6_0(), "test requires torch>=2.6.0")(test_case) +def require_torch_2_7_0(test_case): + """ + Decorator marking a test that requires torch >= 2.7.0 + """ + + def is_min_2_7_0(): + torch_version = version.parse(torch.__version__) + return torch_version >= version.parse("2.7.0") + + return unittest.skipUnless(is_min_2_7_0(), "test requires torch>=2.7.0")(test_case) + + def require_torch_lt_2_6_0(test_case): """ Decorator marking a test that requires torch < 2.6.0 diff --git a/tests/test_normalize_config.py b/tests/test_normalize_config.py index ea98bf97d7..31d04fc64f 100644 --- a/tests/test_normalize_config.py +++ b/tests/test_normalize_config.py @@ -5,7 +5,11 @@ import unittest from unittest.mock import patch -from axolotl.utils.config import normalize_cfg_datasets, normalize_config +from axolotl.utils.config import ( + migrate_fsdp_config, + normalize_cfg_datasets, + normalize_config, +) from axolotl.utils.dict import DictDefault @@ -90,3 +94,104 @@ def test_bf16_disables_fp16(self, mock_bf16_avail): self.assertTrue(cfg.bf16) self.assertFalse(cfg.fp16) + + def test_migrate_fsdp_config(self): + """Test basic FSDP config migration with and without fsdp_version""" + cfg_with_version = DictDefault( + { + "fsdp_config": { + "fsdp_version": 2, + "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "fsdp_offload_params": False, + "fsdp_cpu_ram_efficient_loading": True, + "regular_param": "value", + } + } + ) + + migrate_fsdp_config(cfg_with_version) + + self.assertEqual(cfg_with_version.fsdp_version, 2) + self.assertEqual( + cfg_with_version.fsdp_config.auto_wrap_policy, "TRANSFORMER_BASED_WRAP" + ) + self.assertEqual(cfg_with_version.fsdp_config.offload_params, False) + self.assertEqual(cfg_with_version.fsdp_config.cpu_ram_efficient_loading, True) + self.assertEqual(cfg_with_version.fsdp_config.regular_param, "value") + + self.assertNotIn("fsdp_auto_wrap_policy", cfg_with_version.fsdp_config) + self.assertNotIn("fsdp_offload_params", cfg_with_version.fsdp_config) + self.assertNotIn("fsdp_cpu_ram_efficient_loading", cfg_with_version.fsdp_config) + self.assertNotIn("fsdp_version", cfg_with_version.fsdp_config) + self.assertNotIn("version", cfg_with_version.fsdp_config) + + cfg_without_version = DictDefault( + { + "fsdp_config": { + "fsdp_auto_wrap_policy": "SIZE_BASED_WRAP", + "fsdp_offload_params": True, + "regular_param": "value", + } + } + ) + + migrate_fsdp_config(cfg_without_version) + + self.assertNotIn("fsdp_version", cfg_without_version) + self.assertEqual( + cfg_without_version.fsdp_config.auto_wrap_policy, "SIZE_BASED_WRAP" + ) + self.assertEqual(cfg_without_version.fsdp_config.offload_params, True) + self.assertEqual(cfg_without_version.fsdp_config.regular_param, "value") + + self.assertNotIn("fsdp_auto_wrap_policy", cfg_without_version.fsdp_config) + self.assertNotIn("fsdp_offload_params", cfg_without_version.fsdp_config) + + def test_migrate_fsdp_config_no_fsdp_config(self): + """Test that function doesn't crash when no fsdp_config is present""" + cfg = DictDefault({"some_other_config": "value"}) + + migrate_fsdp_config(cfg) + + self.assertNotIn("fsdp_config", cfg) + self.assertNotIn("fsdp_version", cfg) + self.assertEqual(cfg.some_other_config, "value") + + def test_migrate_fsdp_config_empty_fsdp_config(self): + """Test migration with empty fsdp_config""" + cfg = DictDefault({"fsdp_config": {}}) + + migrate_fsdp_config(cfg) + + self.assertNotIn("fsdp_version", cfg) + self.assertEqual(cfg.fsdp_config, {}) + + def test_migrate_fsdp_config_mixed_keys(self): + """Test migration with a mix of fsdp_ and non-fsdp_ keys""" + cfg = DictDefault( + { + "fsdp_config": { + "fsdp_version": 1, + "fsdp_state_dict_type": "FULL_STATE_DICT", + "mixed_precision_policy": "fp16", + "activation_checkpointing": True, + "fsdp_reshard_after_forward": False, + } + } + ) + + migrate_fsdp_config(cfg) + + self.assertEqual(cfg.fsdp_version, 1) + self.assertEqual(cfg.fsdp_config.state_dict_type, "FULL_STATE_DICT") + self.assertEqual(cfg.fsdp_config.reshard_after_forward, False) + self.assertEqual(cfg.fsdp_config.mixed_precision_policy, "fp16") + self.assertEqual(cfg.fsdp_config.activation_checkpointing, True) + + # Check original fsdp_ keys are removed + self.assertNotIn("fsdp_version", cfg.fsdp_config) + self.assertNotIn("fsdp_state_dict_type", cfg.fsdp_config) + self.assertNotIn("fsdp_reshard_after_forward", cfg.fsdp_config) + + # Ensure no duplicate version key + self.assertNotIn("version", cfg.fsdp_config) From fb7bc9250dd16e6945b95bb13c7a2d08e799ca7b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 12 Jul 2025 11:39:51 -0400 Subject: [PATCH 0791/1405] move unmaintained examples to archive (#2903) [skip ci] --- examples/archived/README.md | 5 +++++ examples/{ => archived}/cerebras/btlm-ft.yml | 0 examples/{ => archived}/cerebras/qlora.yml | 0 examples/{ => archived}/code-llama/13b/lora.yml | 0 examples/{ => archived}/code-llama/13b/qlora.yml | 0 examples/{ => archived}/code-llama/34b/lora.yml | 0 examples/{ => archived}/code-llama/34b/qlora.yml | 0 examples/{ => archived}/code-llama/7b/lora.yml | 0 examples/{ => archived}/code-llama/7b/qlora.yml | 0 examples/{ => archived}/code-llama/README.md | 0 examples/{ => archived}/dbrx/16bit-lora.yaml | 0 examples/{ => archived}/dbrx/8bit-lora.yaml | 0 examples/{ => archived}/dbrx/README.md | 0 examples/{ => archived}/dbrx/fft-ds-zero3.yaml | 0 .../{ => archived}/deepcoder/deepcoder-14B-preview-lora.yml | 0 examples/{ => archived}/falcon/config-7b-lora.yml | 0 examples/{ => archived}/falcon/config-7b-qlora.yml | 0 examples/{ => archived}/falcon/config-7b.yml | 0 examples/{ => archived}/gemma/qlora.yml | 0 examples/{ => archived}/gptj/qlora.yml | 0 examples/{ => archived}/jeopardy-bot/config.yml | 0 examples/{ => archived}/mpt-7b/README.md | 0 examples/{ => archived}/mpt-7b/config.yml | 0 examples/{ => archived}/openllama-3b/README.md | 0 examples/{ => archived}/openllama-3b/config.yml | 0 examples/{ => archived}/openllama-3b/lora.yml | 0 examples/{ => archived}/openllama-3b/qlora.yml | 0 examples/{ => archived}/pythia-12b/README.md | 0 examples/{ => archived}/pythia-12b/config.yml | 0 examples/{ => archived}/pythia/lora.yml | 0 examples/{ => archived}/qwen/README.md | 0 examples/{ => archived}/qwen/lora.yml | 0 examples/{ => archived}/qwen/qlora.yml | 0 examples/{ => archived}/qwen/qwen2-moe-lora.yaml | 0 examples/{ => archived}/qwen/qwen2-moe-qlora.yaml | 0 examples/{ => archived}/redpajama/README.md | 0 examples/{ => archived}/redpajama/config-3b.yml | 0 examples/{ => archived}/replit-3b/config-lora.yml | 0 examples/{ => archived}/stablelm-2/1.6b/fft.yml | 0 examples/{ => archived}/stablelm-2/1.6b/lora.yml | 0 examples/{ => archived}/stablelm-2/README.md | 0 examples/{ => archived}/starcoder2/qlora.yml | 0 examples/{ => archived}/tiny-llama/README.md | 0 examples/{ => archived}/tiny-llama/lora-mps.yml | 0 examples/{ => archived}/tiny-llama/lora.yml | 0 examples/{ => archived}/tiny-llama/pretrain.yml | 0 examples/{ => archived}/tiny-llama/qlora.yml | 0 examples/{ => archived}/xgen-7b/xgen-7b-8k-qlora.yml | 0 examples/{ => archived}/yi-34B-chat/README.md | 0 examples/{ => archived}/yi-34B-chat/qlora.yml | 0 50 files changed, 5 insertions(+) create mode 100644 examples/archived/README.md rename examples/{ => archived}/cerebras/btlm-ft.yml (100%) rename examples/{ => archived}/cerebras/qlora.yml (100%) rename examples/{ => archived}/code-llama/13b/lora.yml (100%) rename examples/{ => archived}/code-llama/13b/qlora.yml (100%) rename examples/{ => archived}/code-llama/34b/lora.yml (100%) rename examples/{ => archived}/code-llama/34b/qlora.yml (100%) rename examples/{ => archived}/code-llama/7b/lora.yml (100%) rename examples/{ => archived}/code-llama/7b/qlora.yml (100%) rename examples/{ => archived}/code-llama/README.md (100%) rename examples/{ => archived}/dbrx/16bit-lora.yaml (100%) rename examples/{ => archived}/dbrx/8bit-lora.yaml (100%) rename examples/{ => archived}/dbrx/README.md (100%) rename examples/{ => archived}/dbrx/fft-ds-zero3.yaml (100%) rename examples/{ => archived}/deepcoder/deepcoder-14B-preview-lora.yml (100%) rename examples/{ => archived}/falcon/config-7b-lora.yml (100%) rename examples/{ => archived}/falcon/config-7b-qlora.yml (100%) rename examples/{ => archived}/falcon/config-7b.yml (100%) rename examples/{ => archived}/gemma/qlora.yml (100%) rename examples/{ => archived}/gptj/qlora.yml (100%) rename examples/{ => archived}/jeopardy-bot/config.yml (100%) rename examples/{ => archived}/mpt-7b/README.md (100%) rename examples/{ => archived}/mpt-7b/config.yml (100%) rename examples/{ => archived}/openllama-3b/README.md (100%) rename examples/{ => archived}/openllama-3b/config.yml (100%) rename examples/{ => archived}/openllama-3b/lora.yml (100%) rename examples/{ => archived}/openllama-3b/qlora.yml (100%) rename examples/{ => archived}/pythia-12b/README.md (100%) rename examples/{ => archived}/pythia-12b/config.yml (100%) rename examples/{ => archived}/pythia/lora.yml (100%) rename examples/{ => archived}/qwen/README.md (100%) rename examples/{ => archived}/qwen/lora.yml (100%) rename examples/{ => archived}/qwen/qlora.yml (100%) rename examples/{ => archived}/qwen/qwen2-moe-lora.yaml (100%) rename examples/{ => archived}/qwen/qwen2-moe-qlora.yaml (100%) rename examples/{ => archived}/redpajama/README.md (100%) rename examples/{ => archived}/redpajama/config-3b.yml (100%) rename examples/{ => archived}/replit-3b/config-lora.yml (100%) rename examples/{ => archived}/stablelm-2/1.6b/fft.yml (100%) rename examples/{ => archived}/stablelm-2/1.6b/lora.yml (100%) rename examples/{ => archived}/stablelm-2/README.md (100%) rename examples/{ => archived}/starcoder2/qlora.yml (100%) rename examples/{ => archived}/tiny-llama/README.md (100%) rename examples/{ => archived}/tiny-llama/lora-mps.yml (100%) rename examples/{ => archived}/tiny-llama/lora.yml (100%) rename examples/{ => archived}/tiny-llama/pretrain.yml (100%) rename examples/{ => archived}/tiny-llama/qlora.yml (100%) rename examples/{ => archived}/xgen-7b/xgen-7b-8k-qlora.yml (100%) rename examples/{ => archived}/yi-34B-chat/README.md (100%) rename examples/{ => archived}/yi-34B-chat/qlora.yml (100%) diff --git a/examples/archived/README.md b/examples/archived/README.md new file mode 100644 index 0000000000..da797c5525 --- /dev/null +++ b/examples/archived/README.md @@ -0,0 +1,5 @@ +# Archived Examples + +This directory contains examples that are no longer maintained and may no longer be functional. + +We keep them around for archival purposes in case they are useful to others. diff --git a/examples/cerebras/btlm-ft.yml b/examples/archived/cerebras/btlm-ft.yml similarity index 100% rename from examples/cerebras/btlm-ft.yml rename to examples/archived/cerebras/btlm-ft.yml diff --git a/examples/cerebras/qlora.yml b/examples/archived/cerebras/qlora.yml similarity index 100% rename from examples/cerebras/qlora.yml rename to examples/archived/cerebras/qlora.yml diff --git a/examples/code-llama/13b/lora.yml b/examples/archived/code-llama/13b/lora.yml similarity index 100% rename from examples/code-llama/13b/lora.yml rename to examples/archived/code-llama/13b/lora.yml diff --git a/examples/code-llama/13b/qlora.yml b/examples/archived/code-llama/13b/qlora.yml similarity index 100% rename from examples/code-llama/13b/qlora.yml rename to examples/archived/code-llama/13b/qlora.yml diff --git a/examples/code-llama/34b/lora.yml b/examples/archived/code-llama/34b/lora.yml similarity index 100% rename from examples/code-llama/34b/lora.yml rename to examples/archived/code-llama/34b/lora.yml diff --git a/examples/code-llama/34b/qlora.yml b/examples/archived/code-llama/34b/qlora.yml similarity index 100% rename from examples/code-llama/34b/qlora.yml rename to examples/archived/code-llama/34b/qlora.yml diff --git a/examples/code-llama/7b/lora.yml b/examples/archived/code-llama/7b/lora.yml similarity index 100% rename from examples/code-llama/7b/lora.yml rename to examples/archived/code-llama/7b/lora.yml diff --git a/examples/code-llama/7b/qlora.yml b/examples/archived/code-llama/7b/qlora.yml similarity index 100% rename from examples/code-llama/7b/qlora.yml rename to examples/archived/code-llama/7b/qlora.yml diff --git a/examples/code-llama/README.md b/examples/archived/code-llama/README.md similarity index 100% rename from examples/code-llama/README.md rename to examples/archived/code-llama/README.md diff --git a/examples/dbrx/16bit-lora.yaml b/examples/archived/dbrx/16bit-lora.yaml similarity index 100% rename from examples/dbrx/16bit-lora.yaml rename to examples/archived/dbrx/16bit-lora.yaml diff --git a/examples/dbrx/8bit-lora.yaml b/examples/archived/dbrx/8bit-lora.yaml similarity index 100% rename from examples/dbrx/8bit-lora.yaml rename to examples/archived/dbrx/8bit-lora.yaml diff --git a/examples/dbrx/README.md b/examples/archived/dbrx/README.md similarity index 100% rename from examples/dbrx/README.md rename to examples/archived/dbrx/README.md diff --git a/examples/dbrx/fft-ds-zero3.yaml b/examples/archived/dbrx/fft-ds-zero3.yaml similarity index 100% rename from examples/dbrx/fft-ds-zero3.yaml rename to examples/archived/dbrx/fft-ds-zero3.yaml diff --git a/examples/deepcoder/deepcoder-14B-preview-lora.yml b/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml similarity index 100% rename from examples/deepcoder/deepcoder-14B-preview-lora.yml rename to examples/archived/deepcoder/deepcoder-14B-preview-lora.yml diff --git a/examples/falcon/config-7b-lora.yml b/examples/archived/falcon/config-7b-lora.yml similarity index 100% rename from examples/falcon/config-7b-lora.yml rename to examples/archived/falcon/config-7b-lora.yml diff --git a/examples/falcon/config-7b-qlora.yml b/examples/archived/falcon/config-7b-qlora.yml similarity index 100% rename from examples/falcon/config-7b-qlora.yml rename to examples/archived/falcon/config-7b-qlora.yml diff --git a/examples/falcon/config-7b.yml b/examples/archived/falcon/config-7b.yml similarity index 100% rename from examples/falcon/config-7b.yml rename to examples/archived/falcon/config-7b.yml diff --git a/examples/gemma/qlora.yml b/examples/archived/gemma/qlora.yml similarity index 100% rename from examples/gemma/qlora.yml rename to examples/archived/gemma/qlora.yml diff --git a/examples/gptj/qlora.yml b/examples/archived/gptj/qlora.yml similarity index 100% rename from examples/gptj/qlora.yml rename to examples/archived/gptj/qlora.yml diff --git a/examples/jeopardy-bot/config.yml b/examples/archived/jeopardy-bot/config.yml similarity index 100% rename from examples/jeopardy-bot/config.yml rename to examples/archived/jeopardy-bot/config.yml diff --git a/examples/mpt-7b/README.md b/examples/archived/mpt-7b/README.md similarity index 100% rename from examples/mpt-7b/README.md rename to examples/archived/mpt-7b/README.md diff --git a/examples/mpt-7b/config.yml b/examples/archived/mpt-7b/config.yml similarity index 100% rename from examples/mpt-7b/config.yml rename to examples/archived/mpt-7b/config.yml diff --git a/examples/openllama-3b/README.md b/examples/archived/openllama-3b/README.md similarity index 100% rename from examples/openllama-3b/README.md rename to examples/archived/openllama-3b/README.md diff --git a/examples/openllama-3b/config.yml b/examples/archived/openllama-3b/config.yml similarity index 100% rename from examples/openllama-3b/config.yml rename to examples/archived/openllama-3b/config.yml diff --git a/examples/openllama-3b/lora.yml b/examples/archived/openllama-3b/lora.yml similarity index 100% rename from examples/openllama-3b/lora.yml rename to examples/archived/openllama-3b/lora.yml diff --git a/examples/openllama-3b/qlora.yml b/examples/archived/openllama-3b/qlora.yml similarity index 100% rename from examples/openllama-3b/qlora.yml rename to examples/archived/openllama-3b/qlora.yml diff --git a/examples/pythia-12b/README.md b/examples/archived/pythia-12b/README.md similarity index 100% rename from examples/pythia-12b/README.md rename to examples/archived/pythia-12b/README.md diff --git a/examples/pythia-12b/config.yml b/examples/archived/pythia-12b/config.yml similarity index 100% rename from examples/pythia-12b/config.yml rename to examples/archived/pythia-12b/config.yml diff --git a/examples/pythia/lora.yml b/examples/archived/pythia/lora.yml similarity index 100% rename from examples/pythia/lora.yml rename to examples/archived/pythia/lora.yml diff --git a/examples/qwen/README.md b/examples/archived/qwen/README.md similarity index 100% rename from examples/qwen/README.md rename to examples/archived/qwen/README.md diff --git a/examples/qwen/lora.yml b/examples/archived/qwen/lora.yml similarity index 100% rename from examples/qwen/lora.yml rename to examples/archived/qwen/lora.yml diff --git a/examples/qwen/qlora.yml b/examples/archived/qwen/qlora.yml similarity index 100% rename from examples/qwen/qlora.yml rename to examples/archived/qwen/qlora.yml diff --git a/examples/qwen/qwen2-moe-lora.yaml b/examples/archived/qwen/qwen2-moe-lora.yaml similarity index 100% rename from examples/qwen/qwen2-moe-lora.yaml rename to examples/archived/qwen/qwen2-moe-lora.yaml diff --git a/examples/qwen/qwen2-moe-qlora.yaml b/examples/archived/qwen/qwen2-moe-qlora.yaml similarity index 100% rename from examples/qwen/qwen2-moe-qlora.yaml rename to examples/archived/qwen/qwen2-moe-qlora.yaml diff --git a/examples/redpajama/README.md b/examples/archived/redpajama/README.md similarity index 100% rename from examples/redpajama/README.md rename to examples/archived/redpajama/README.md diff --git a/examples/redpajama/config-3b.yml b/examples/archived/redpajama/config-3b.yml similarity index 100% rename from examples/redpajama/config-3b.yml rename to examples/archived/redpajama/config-3b.yml diff --git a/examples/replit-3b/config-lora.yml b/examples/archived/replit-3b/config-lora.yml similarity index 100% rename from examples/replit-3b/config-lora.yml rename to examples/archived/replit-3b/config-lora.yml diff --git a/examples/stablelm-2/1.6b/fft.yml b/examples/archived/stablelm-2/1.6b/fft.yml similarity index 100% rename from examples/stablelm-2/1.6b/fft.yml rename to examples/archived/stablelm-2/1.6b/fft.yml diff --git a/examples/stablelm-2/1.6b/lora.yml b/examples/archived/stablelm-2/1.6b/lora.yml similarity index 100% rename from examples/stablelm-2/1.6b/lora.yml rename to examples/archived/stablelm-2/1.6b/lora.yml diff --git a/examples/stablelm-2/README.md b/examples/archived/stablelm-2/README.md similarity index 100% rename from examples/stablelm-2/README.md rename to examples/archived/stablelm-2/README.md diff --git a/examples/starcoder2/qlora.yml b/examples/archived/starcoder2/qlora.yml similarity index 100% rename from examples/starcoder2/qlora.yml rename to examples/archived/starcoder2/qlora.yml diff --git a/examples/tiny-llama/README.md b/examples/archived/tiny-llama/README.md similarity index 100% rename from examples/tiny-llama/README.md rename to examples/archived/tiny-llama/README.md diff --git a/examples/tiny-llama/lora-mps.yml b/examples/archived/tiny-llama/lora-mps.yml similarity index 100% rename from examples/tiny-llama/lora-mps.yml rename to examples/archived/tiny-llama/lora-mps.yml diff --git a/examples/tiny-llama/lora.yml b/examples/archived/tiny-llama/lora.yml similarity index 100% rename from examples/tiny-llama/lora.yml rename to examples/archived/tiny-llama/lora.yml diff --git a/examples/tiny-llama/pretrain.yml b/examples/archived/tiny-llama/pretrain.yml similarity index 100% rename from examples/tiny-llama/pretrain.yml rename to examples/archived/tiny-llama/pretrain.yml diff --git a/examples/tiny-llama/qlora.yml b/examples/archived/tiny-llama/qlora.yml similarity index 100% rename from examples/tiny-llama/qlora.yml rename to examples/archived/tiny-llama/qlora.yml diff --git a/examples/xgen-7b/xgen-7b-8k-qlora.yml b/examples/archived/xgen-7b/xgen-7b-8k-qlora.yml similarity index 100% rename from examples/xgen-7b/xgen-7b-8k-qlora.yml rename to examples/archived/xgen-7b/xgen-7b-8k-qlora.yml diff --git a/examples/yi-34B-chat/README.md b/examples/archived/yi-34B-chat/README.md similarity index 100% rename from examples/yi-34B-chat/README.md rename to examples/archived/yi-34B-chat/README.md diff --git a/examples/yi-34B-chat/qlora.yml b/examples/archived/yi-34B-chat/qlora.yml similarity index 100% rename from examples/yi-34B-chat/qlora.yml rename to examples/archived/yi-34B-chat/qlora.yml From 4dc5910e1c60914d402aeb7ebb4be1c32315e78d Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 12 Jul 2025 22:40:01 +0700 Subject: [PATCH 0792/1405] feat(doc): re-add docker 2.7.0 tag back (#2902) [skip ci] --- docs/docker.qmd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/docker.qmd b/docs/docker.qmd index 5b238c520e..da61843945 100644 --- a/docs/docker.qmd +++ b/docs/docker.qmd @@ -34,6 +34,7 @@ Tags examples: - `main-base-py3.11-cu128-2.7.1` - `main-base-py3.11-cu126-2.7.1` +- `main-base-py3.11-cu126-2.7.0` - `main-base-py3.11-cu126-2.6.0` - `main-base-py3.11-cu124-2.6.0` @@ -75,6 +76,7 @@ Tags examples: - `main-py3.11-cu128-2.7.1` - `main-py3.11-cu126-2.7.1` +- `main-py3.11-cu126-2.7.0` - `main-py3.11-cu126-2.6.0` - `main-py3.11-cu124-2.6.0` - `main-latest` From 7fb8441e0e0453a3b760996221332fed0a7cdb37 Mon Sep 17 00:00:00 2001 From: Jiawei Liu Date: Sat, 12 Jul 2025 10:40:30 -0500 Subject: [PATCH 0793/1405] fix: customized dataset with simpo (#2894) [skip ci] --- docs/rlhf.qmd | 32 +++++++++---------- .../prompt_strategies/dpo/user_defined.py | 2 +- src/axolotl/utils/config/__init__.py | 2 +- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 76131978f8..4a67b75595 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -274,15 +274,14 @@ rl: dpo datasets: - path: ... split: train - type: user_defined.default - - field_prompt: "prompt" - field_system: "system" - field_chosen: "chosen" - field_rejected: "rejected" - prompt_format: "{prompt}" - chosen_format: "{chosen}" - rejected_format: "{rejected}" + type: + field_prompt: "prompt" + field_system: "system" + field_chosen: "chosen" + field_rejected: "rejected" + prompt_format: "{prompt}" + chosen_format: "{chosen}" + rejected_format: "{rejected}" ``` The input format is a simple JSON input with customizable fields based on the above config. @@ -475,14 +474,13 @@ rl: kto datasets: - path: ... split: train - type: user_defined.default - - field_prompt: "prompt" - field_system: "system" - field_completion: "completion" - field_label: "label" - prompt_format: "{prompt}" - completion_format: "{completion}" + type: + field_prompt: "prompt" + field_system: "system" + field_completion: "completion" + field_label: "label" + prompt_format: "{prompt}" + completion_format: "{completion}" ``` The input format is a simple JSON input with customizable fields based on the above config. diff --git a/src/axolotl/prompt_strategies/dpo/user_defined.py b/src/axolotl/prompt_strategies/dpo/user_defined.py index 1d5f891af6..cdd9b8c9c9 100644 --- a/src/axolotl/prompt_strategies/dpo/user_defined.py +++ b/src/axolotl/prompt_strategies/dpo/user_defined.py @@ -33,7 +33,7 @@ def transform_fn(sample): system=sample[field_system], prompt=sample[field_prompt] ) else: - sample["prompt"] = prompt_format.format(prompt=sample["prompt"]) + sample["prompt"] = prompt_format.format(prompt=sample[field_prompt]) sample["chosen"] = chosen_format.format(chosen=sample[field_chosen]) sample["rejected"] = rejected_format.format(rejected=sample[field_rejected]) return sample diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index fb17b259ff..4de606565f 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -274,7 +274,7 @@ def validate_config( # Convert datasets to proper format if needed if cfg.get("datasets"): for idx, ds_cfg in enumerate(cfg["datasets"]): - if cfg.get("rl") == "dpo" and not isinstance(ds_cfg, DPODataset): + if cfg.get("rl") in ["dpo", "simpo"] and not isinstance(ds_cfg, DPODataset): cfg["datasets"][idx] = DPODataset(**ds_cfg) elif cfg.get("rl") == "kto" and not isinstance(ds_cfg, KTODataset): cfg["datasets"][idx] = KTODataset(**dict(ds_cfg)) From 9a8073e73d21f44fc35963b8ef46ab7d96a4472a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 12 Jul 2025 11:41:34 -0400 Subject: [PATCH 0794/1405] Liquid Foundation Model 2 support (#2905) * LFM2 support * docs * packing seems to work * update install to force install in case already on dev version * default to use chunked cross entropy --- examples/lfm2/README.md | 7 +++++ examples/lfm2/lfm2-350m-fft.yaml | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 examples/lfm2/README.md create mode 100644 examples/lfm2/lfm2-350m-fft.yaml diff --git a/examples/lfm2/README.md b/examples/lfm2/README.md new file mode 100644 index 0000000000..eb9ca911f9 --- /dev/null +++ b/examples/lfm2/README.md @@ -0,0 +1,7 @@ +# Liquid Foundation Models 2 + +LFM2 support in transformers exists in the main branch, but is not yet included in the transformers release. + +```bash +pip install --upgrade --no-deps --force-reinstall git+https://github.com/huggingface/transformers.git +``` diff --git a/examples/lfm2/lfm2-350m-fft.yaml b/examples/lfm2/lfm2-350m-fft.yaml new file mode 100644 index 0000000000..95961557e3 --- /dev/null +++ b/examples/lfm2/lfm2-350m-fft.yaml @@ -0,0 +1,48 @@ +base_model: LiquidAI/LFM2-350M + +chunked_cross_entropy: true + +chat_template: tokenizer_default +eot_tokens: + - "<|im_end|>" +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_field_role: from + message_field_content: value +dataset_prepared_path: last_run_prepared +val_set_size: 0.05 +output_dir: ./outputs/out + +sequence_len: 4096 +sample_packing: true +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 4 +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 5e-5 + +bf16: true +tf32: true + +gradient_checkpointing: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 2 +saves_per_epoch: 1 + +weight_decay: 0.0 From 41664c7c4cd3543031166a4be2988029591dddd9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 07:51:16 -0400 Subject: [PATCH 0795/1405] fix ddp for incorrect steps (#2915) * fix ddp for incorrect steps * add test --- src/axolotl/utils/config/__init__.py | 1 + tests/test_train.py | 44 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 tests/test_train.py diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 4de606565f..4e26a257d4 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -115,6 +115,7 @@ def normalize_config(cfg): "chrf", ] choose_device(cfg) + cfg.ddp = cfg.ddp if cfg.ddp is not None else cfg.world_size != 1 if cfg.world_size != 1: cfg.device_map = {"": int(os.environ.get("LOCAL_RANK", 0))} if cfg.fsdp or cfg.fsdp_config or cfg.ddp: diff --git a/tests/test_train.py b/tests/test_train.py new file mode 100644 index 0000000000..291e9136be --- /dev/null +++ b/tests/test_train.py @@ -0,0 +1,44 @@ +"""Test for batch size calculation for multi-gpu training.""" + +import pytest + +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + + +@pytest.fixture(name="train_base_cfg") +def fixture_train_base_cfg(): + return DictDefault( + base_model="gpt2", + learning_rate=1e-3, + datasets=[ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + micro_batch_size=2, + gradient_accumulation_steps=4, + sequence_len=2048, + sample_packing=True, + num_epochs=1, + ) + + +class TestTrain: + """test class for train related tests""" + + @pytest.mark.parametrize( + "world_size, expected_batch_size", + [ + (1, 8), + (4, 32), + ], + ) + def test_batch_size_ddp( + self, train_base_cfg, monkeypatch, world_size, expected_batch_size + ): + monkeypatch.setenv("WORLD_SIZE", str(world_size)) + cfg = validate_config(train_base_cfg) + normalize_config(cfg) + assert cfg.batch_size == expected_batch_size From 5081db7f8a042bb520a9124fe621af64314d37fd Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 09:23:42 -0400 Subject: [PATCH 0796/1405] upgrade trl==0.19.1 (#2892) [skip ci] * upgrade trl==0.19.1 * add vllm for tests for grpo * fixes to work with latest trl * need data_parallel_size config too * support for vllm_mode for server / colocate * vllm settings for colocate * relax vllm version * bump min hf hub for latest vllm support * add hints on string literal for vllm mode * use latest transformers 4.53.2 * tweak acceptable loss on flaky test_ds_zero3_packed test * don't run flaky vllm/grpo tests for now --- .github/workflows/multi-gpu-e2e.yml | 7 ++++ requirements.txt | 6 ++-- src/axolotl/cli/vllm_serve.py | 5 ++- src/axolotl/core/trainers/grpo/__init__.py | 10 ++++++ src/axolotl/core/trainers/grpo/trainer.py | 42 +++------------------- src/axolotl/utils/schemas/trl.py | 8 +++++ src/axolotl/utils/schemas/vllm.py | 4 +++ tests/e2e/multigpu/solo/test_grpo.py | 1 + tests/e2e/multigpu/test_llama.py | 2 +- 9 files changed, 43 insertions(+), 42 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 6180faf962..f58c05f3bc 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -33,6 +33,13 @@ jobs: axolotl_extras: num_gpus: 2 nightly_build: "true" + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.0 + axolotl_extras: vllm + num_gpus: 2 + nightly_build: "true" - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" diff --git a/requirements.txt b/requirements.txt index 77d6d31aac..6ea28dc235 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,14 +11,14 @@ liger-kernel==0.5.10 packaging==23.2 -huggingface_hub==0.32.2 +huggingface_hub>=0.33.0 peft==0.15.2 -transformers==4.53.1 +transformers==4.53.2 tokenizers>=0.21.1 accelerate==1.8.1 datasets==3.6.0 deepspeed>=0.17.0 -trl==0.18.2 +trl==0.19.1 hf_xet==1.1.2 optimum==1.16.2 diff --git a/src/axolotl/cli/vllm_serve.py b/src/axolotl/cli/vllm_serve.py index 448b25a7e6..f092cc59a0 100644 --- a/src/axolotl/cli/vllm_serve.py +++ b/src/axolotl/cli/vllm_serve.py @@ -37,7 +37,6 @@ def do_vllm_serve( Returns: process_id: the process id of the started VLLM server """ - patch_vllm_worker() cfg = load_cfg(config) model = cfg.base_model @@ -47,6 +46,9 @@ def do_vllm_serve( tensor_parallel_size = ( cli_args.get("tensor_parallel_size") or cfg.vllm.tensor_parallel_size ) + data_parallel_size = ( + cli_args.get("data_parallel_size") or cfg.vllm.data_parallel_size + ) host = cli_args.get("host") or cfg.vllm.host port = cli_args.get("port") or cfg.vllm.port gpu_memory_utilization = ( @@ -68,6 +70,7 @@ def do_vllm_serve( vllm_script_args = AxolotlScriptArguments( model=model, tensor_parallel_size=tensor_parallel_size, + data_parallel_size=data_parallel_size, host=host, port=port, gpu_memory_utilization=gpu_memory_utilization, diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index c0f10be23f..771f788fe7 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -14,6 +14,7 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger from axolotl.utils.schemas.trl import TRLConfig +from axolotl.utils.schemas.vllm import VllmConfig LOG = get_logger(__name__) @@ -41,9 +42,18 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: return grpo_args_kwargs trl: TRLConfig = cfg.trl # type: ignore + vllm_cfg: VllmConfig = cfg.vllm # type: ignore if trl.use_vllm: grpo_args_kwargs["use_vllm"] = trl.use_vllm + grpo_args_kwargs["vllm_mode"] = trl.vllm_mode + if trl.vllm_mode == "colocate": + grpo_args_kwargs["vllm_gpu_memory_utilization"] = ( + vllm_cfg.gpu_memory_utilization + ) + grpo_args_kwargs["vllm_tensor_parallel_size"] = ( + vllm_cfg.tensor_parallel_size + ) grpo_args_kwargs["vllm_server_host"] = trl.vllm_server_host or trl.vllm.host # type: ignore[attr-defined] grpo_args_kwargs["vllm_server_port"] = trl.vllm_server_port or trl.vllm.port # type: ignore[attr-defined] if trl.vllm_server_timeout: diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index c97fccd31f..70b3cf3b53 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -59,42 +59,6 @@ class AxolotlGRPOTrainer( _tag_names = ["trl", "grpo", "axolotl"] - def get_train_dataloader(self): - if self.train_dataset is None: - raise ValueError("Trainer: training requires a train_dataset.") - - train_dataset = self.train_dataset - data_collator = self.data_collator - if isinstance(train_dataset, datasets.Dataset): - train_dataset = self._remove_unused_columns( - train_dataset, description="training" - ) - else: - data_collator = self._get_collator_with_removed_columns( - data_collator, description="training" - ) - - dataloader_params = { - "batch_size": self._train_batch_size - * self.args.steps_per_generation, # < this is the change - "collate_fn": data_collator, - "num_workers": self.args.dataloader_num_workers, - "pin_memory": self.args.dataloader_pin_memory, - "persistent_workers": self.args.dataloader_persistent_workers, - } - - if not isinstance(train_dataset, torch.utils.data.IterableDataset): - dataloader_params["sampler"] = self._get_train_sampler() - dataloader_params["drop_last"] = self.args.dataloader_drop_last - dataloader_params["worker_init_fn"] = partial( - seed_worker, - num_workers=self.args.dataloader_num_workers, - rank=self.args.process_index, - ) - dataloader_params["prefetch_factor"] = self.args.dataloader_prefetch_factor - - return self.accelerator.prepare(DataLoader(train_dataset, **dataloader_params)) - class AxolotlGRPOSequenceParallelTrainer(AxolotlGRPOTrainer): """Extend the base GRPOTrainer for sequence parallelism handling""" @@ -252,7 +216,11 @@ def _prepare_dataloader( dataloader_params["drop_last"] = self.args.dataloader_drop_last if not is_eval: - dataloader_params["worker_init_fn"] = seed_worker + dataloader_params["worker_init_fn"] = partial( + seed_worker, + num_workers=self.args.dataloader_num_workers, + rank=self.args.process_index, + ) # Create the dataloader dataloader = DataLoader(dataset, **dataloader_params) diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index d1b18a56e2..e4d17bc947 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -1,5 +1,7 @@ """Pydantic models for TRL trainer configuration""" +from typing import Literal + from pydantic import BaseModel, Field @@ -27,6 +29,12 @@ class TRLConfig(BaseModel): default=False, json_schema_extra={"description": "Whether to use VLLM for RL training."}, ) + vllm_mode: Literal["server", "colocate"] | None = Field( + default=None, + json_schema_extra={ + "description": "VLLM mode to use, one of 'server' or 'colocate'" + }, + ) vllm_server_host: str | None = Field( default="0.0.0.0", # nosec B104 json_schema_extra={"description": "Host of the vLLM server to connect to."}, diff --git a/src/axolotl/utils/schemas/vllm.py b/src/axolotl/utils/schemas/vllm.py index 0ae6355893..518b8f62d4 100644 --- a/src/axolotl/utils/schemas/vllm.py +++ b/src/axolotl/utils/schemas/vllm.py @@ -18,6 +18,10 @@ class VllmConfig(BaseModel): default=None, json_schema_extra={"description": "Tensor parallel size for VLLM"}, ) + data_parallel_size: int | None = Field( + default=None, + json_schema_extra={"description": "Data parallel size for VLLM"}, + ) gpu_memory_utilization: float | None = Field( default=0.9, json_schema_extra={"description": "GPU memory utilization for VLLM"}, diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index c595d3fc05..c047343456 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -141,6 +141,7 @@ def recursive_kill(process: subprocess.Popen): os.kill(process.pid, 9) +@pytest.mark.skip(reason="flaky vllm tests in modal") class TestGRPO: """ Test case for GRPO training using multilpe GPUs diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 7f9db12f3d..fcc174f27b 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -707,7 +707,7 @@ def test_ds_zero3_packed( ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.4, "Train Loss (%s) is too high" + temp_dir + "/runs", "train/train_loss", 2.45, "Train Loss (%s) is too high" ) @pytest.mark.parametrize( From 7ccbbd8e770acd5ecbe7edf08200d30eb841dd8b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 09:24:07 -0400 Subject: [PATCH 0797/1405] upgrade liger to 0.6.0 (#2893) [skip ci] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6ea28dc235..eeb3b864df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 autoawq==0.2.7.post3 -liger-kernel==0.5.10 +liger-kernel==0.6.0 # END section packaging==23.2 From 80dc4c261afb6a6ae5b3d383be4b65d9dbf517c4 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 09:24:29 -0400 Subject: [PATCH 0798/1405] fix xformers version for python 2.6 (#2916) [skip ci] --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 731fe8a6f3..ff8bd2c5c6 100644 --- a/setup.py +++ b/setup.py @@ -73,9 +73,9 @@ def parse_requirements(extras_require_map): extras_require_map["vllm"] = ["vllm>=0.9.0"] elif (major, minor) >= (2, 6): _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append( - "xformers==0.0.29.post2" - ) # vllm needs post2 w torch 2.6 + _install_requires.append("xformers==0.0.29.post3") + # since we only support 2.6.0+cu126 + _dependency_links.append("https://download.pytorch.org/whl/cu126") extras_require_map["vllm"] = ["vllm==0.8.5.post1"] elif (major, minor) >= (2, 5): _install_requires.pop(_install_requires.index(xformers_version)) From af92151a7b03c8e5b4b486f973d1a3174bff03d8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 09:25:44 -0400 Subject: [PATCH 0799/1405] FSDP2 fix validation and add tests (#2910) * fix validation and add tests * remove debugging and add more tests * remove migrate_fsdp --- src/axolotl/cli/config.py | 2 - src/axolotl/utils/config/__init__.py | 13 -- src/axolotl/utils/schemas/config.py | 66 --------- src/axolotl/utils/schemas/validation.py | 130 +++++++++++++--- tests/test_normalize_config.py | 30 ++-- tests/utils/schemas/validation/test_fsdp.py | 155 ++++++++++++++++++++ 6 files changed, 280 insertions(+), 116 deletions(-) create mode 100644 tests/utils/schemas/validation/test_fsdp.py diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index 5f75352f3f..cb0eece7fe 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -16,7 +16,6 @@ from axolotl.integrations.base import PluginManager from axolotl.utils.comet_ import setup_comet_env_vars from axolotl.utils.config import ( - migrate_fsdp_config, normalize_cfg_datasets, normalize_config, validate_config, @@ -227,7 +226,6 @@ def load_cfg( }, ) - migrate_fsdp_config(cfg) prepare_optim_env(cfg) prepare_opinionated_env(cfg) normalize_config(cfg) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 4e26a257d4..aaa203e82a 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -314,16 +314,3 @@ def prepare_plugins(cfg): plugin_manager = PluginManager.get_instance() for plugin_name in cfg["plugins"]: plugin_manager.register(plugin_name) - - -# TODO @SalmanMohammadi remove this function in 0.12 -def migrate_fsdp_config(cfg): - if cfg.get("fsdp_config"): - fsdp_config_keys = cfg.fsdp_config.keys() - if "fsdp_version" in fsdp_config_keys: - cfg.fsdp_version = cfg.fsdp_config.pop("fsdp_version") - - for key in list(fsdp_config_keys): - if key.startswith("fsdp_") and key != "fsdp_version": - cfg.fsdp_config[key.replace("fsdp_", "")] = cfg.fsdp_config[key] - del cfg.fsdp_config[key] diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index de80d1b79e..6668380bf1 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1143,72 +1143,6 @@ def check_fsdp_torch_version(cls, data): return data - @model_validator(mode="before") - @classmethod - def check_fsdp_version(cls, data): - fsdp_config = data.get("fsdp_config", {}) - if fsdp_config and str(data.get("fsdp_version")) != "2": - LOG.info( - "FSDP1 will be deprecated in an upcoming release of Axolotl." - "We recommend that you use FSDP version 2 for better performance and compatibility. " - "Please see this link for more details: https://docs.axolotl.ai/docs/multi-gpu.html#sec-fsdp " - "For more details on migrating your config. " - ) - return data - - @model_validator(mode="before") - @classmethod - def check_fsdp2_base_model_quant_ram_efficient_loading(cls, data): - fsdp_config = data.get("fsdp_config") - if fsdp_config and data.get("fsdp_version") == 2: - if fsdp_config.get("cpu_ram_efficient_loading") and ( - data.get("load_in_8bit") or data.get("load_in_4bit") - ): - raise ValueError( - "FSDP2 does not support load_in_8bit or load_in_4bit with cpu_ram_efficient_loading. Please do one of the following: use DeepSpeed, " - "set fsdp_version to 1, or disable cpu_ram_efficient_loading." - ) - return data - - @model_validator(mode="before") - @classmethod - def check_fsdp2_base_model_quant_dpo(cls, data): - if data.get("fsdp_version") == 2 and data.get("rl") in [ - RLType.DPO, - RLType.KTO, - RLType.ORPO, - RLType.IPO, - ]: - if data.get("load_in_8bit") or data.get("load_in_4bit"): - raise ValueError( - "FSDP2 does not support load_in_8bit or load_in_4bit with DPO. Please use DeepSpeed or set `fsdp_version` to 1." - ) - - return data - - @model_validator(mode="before") - @classmethod - def check_fsdp_version_in_fsdp_config(cls, data): - if fsdp_config := data.get("fsdp_config"): - if fsdp_config.get("fsdp_version"): - LOG.warning( - "Configuring `fsdp_version` in `fsdp_config` is deprecated. " - "Please configure `fsdp_version` as a top-level field." - ) - return data - - @model_validator(mode="before") - @classmethod - def check_fsdp_config_kwargs_prefix(cls, data): - if fsdp_config := data.get("fsdp_config"): - for key, _ in fsdp_config.items(): - if key.startswith("fsdp_"): - LOG.warning_once( - "Configuring FSDP fields with the `fsdp_` prefix is deprecated. " - "Please omit the `fsdp_` prefix from the any fields in `fsdp_config`." - ) - return data - @model_validator(mode="before") @classmethod def default_dataloader_opts(cls, data): diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 57959c4fa8..534d89a98d 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1,6 +1,6 @@ """Module with validation methods for config pydantic model.""" -# pylint: disable=too-many-lines +# pylint: disable=too-many-lines,too-many-boolean-expressions import logging @@ -748,44 +748,128 @@ def check_xentropy_patch_conflicts(cls, data): @model_validator(mode="before") @classmethod - def check_fsdp_offload_w_8bit_optimizer(cls, data): + def check_fsdp_version(cls, data): + fsdp_config = data.get("fsdp_config", {}) + if fsdp_config and str(data.get("fsdp_version")) != "2": + LOG.info( + "FSDP1 will be deprecated in an upcoming release of Axolotl." + "We recommend that you use FSDP version 2 for better performance and compatibility. " + "Please see this link for more details: https://docs.axolotl.ai/docs/multi-gpu.html#sec-fsdp " + "For more details on migrating your config. " + ) + return data + + @model_validator(mode="after") + def check_fsdp2_base_model_quant_ram_efficient_loading(self): + fsdp_config = self.fsdp_config if hasattr(self, "fsdp_config") else None + fsdp_version = self.fsdp_version if hasattr(self, "fsdp_version") else None + load_in_8bit = self.load_in_8bit if hasattr(self, "load_in_8bit") else None + load_in_4bit = self.load_in_4bit if hasattr(self, "load_in_4bit") else None + if fsdp_config and fsdp_version == 2: + if fsdp_config.get("cpu_ram_efficient_loading") and ( + load_in_8bit or load_in_4bit + ): + raise ValueError( + "FSDP2 does not support load_in_8bit or load_in_4bit with cpu_ram_efficient_loading. Please do one of the following: use DeepSpeed, " + "set fsdp_version to 1, or disable cpu_ram_efficient_loading." + ) + return self + + @model_validator(mode="before") + @classmethod + def check_fsdp2_base_model_quant_rl(cls, data): + if data.get("fsdp_version") == 2 and data.get("rl") in [ + RLType.DPO, + RLType.KTO, + RLType.ORPO, + RLType.IPO, + ]: + if data.get("load_in_8bit") or data.get("load_in_4bit"): + raise ValueError( + f"FSDP2 does not support load_in_8bit or load_in_4bit with {data.get('rl')}. Please use DeepSpeed or set `fsdp_version` to 1." + ) + + return data + + @model_validator(mode="before") + @classmethod + def check_fsdp_version_in_fsdp_config(cls, data): + if data.get("fsdp_config"): + if data.get("fsdp_config", {}).get("fsdp_version"): + LOG.warning( + "Configuring `fsdp_version` in `fsdp_config` is deprecated. " + "Please configure `fsdp_version` as a top-level field." + ) + data["fsdp_version"] = data.get("fsdp_config").pop("fsdp_version") + return data + + @model_validator(mode="before") + @classmethod + def check_fsdp_config_kwargs_prefix(cls, data): + if fsdp_config := data.get("fsdp_config"): + should_fix = False + for key, _ in fsdp_config.items(): + if key.startswith("fsdp_"): + should_fix = True + LOG.warning_once( + "Configuring FSDP fields with the `fsdp_` prefix is deprecated. " + "Please omit the `fsdp_` prefix from the any fields in `fsdp_config`." + ) + if should_fix: + update_fsdp_config = {} + for key, value in fsdp_config.items(): + if key.startswith("fsdp_") and key != "fsdp_version": + update_fsdp_config[key.replace("fsdp_", "")] = value + else: + update_fsdp_config[key] = value + data["fsdp_config"] = update_fsdp_config + return data + + @model_validator(mode="after") + def check_fsdp_offload_w_8bit_optimizer(self): if ( - data.get("fsdp") - and "8bit" in data.get("optimizer", "") - and data.get("fsdp_config") - and data["fsdp_config"].get("fsdp_offload_params") - and str(data["fsdp_config"].get("fsdp_version")) != "2" + hasattr(self, "fsdp_config") + and self.fsdp_config + and self.optimizer + and "8bit" in self.optimizer.value + and self.fsdp_config["offload_params"] + and str(self.fsdp_version) != "2" ): raise ValueError( - f"FSDP Offload not compatible with {data.get('optimizer')}" + f"FSDP Offload not compatible with {str(self.optimizer.value)}" ) + return self + + @model_validator(mode="after") + def check_fsdp2_w_8bit_optimizer(self): if ( - data.get("fsdp") - and "8bit" in data.get("optimizer", "") - and data.get("fsdp_config") - and str(data["fsdp_config"].get("fsdp_version")) == "2" + hasattr(self, "fsdp_config") + and self.fsdp_config + and self.optimizer + and "8bit" in self.optimizer.value + and str(self.fsdp_version) == "2" ): - if data.get("optimizer", "") in ["adamw_8bit", "adamw_bnb_8bit"]: + if self.optimizer in ["adamw_8bit", "adamw_bnb_8bit"]: # CUDA ops errors with bnb 8bit optimizer + FSDP2 raise ValueError( - f"FSDP2 not compatible with {data.get('optimizer')}, use `adamw_torch_8bit` instead" + f"FSDP2 not compatible with {self.optimizer.value}, use `adamw_torch_8bit` instead" ) - return data + return self - @model_validator(mode="before") - @classmethod - def check_fsdp_sharded_state_dict_w_safetensors(cls, data): + @model_validator(mode="after") + def check_fsdp_sharded_state_dict_w_safetensors(self): if ( - data.get("fsdp_config") - and data.get("save_safetensors") - and data.get("fsdp_config") - and data["fsdp_config"].get("fsdp_state_dict_type") == "SHARDED_STATE_DICT" + hasattr(self, "fsdp_config") + and self.fsdp_config + and hasattr(self, "save_safetensors") + and self.save_safetensors + and self.fsdp_config.get("state_dict_type", "") == "SHARDED_STATE_DICT" ): raise ValueError( "FSDP SHARDED_STATE_DICT not compatible with save_safetensors" ) - return data + return self class SystemValidationMixin: diff --git a/tests/test_normalize_config.py b/tests/test_normalize_config.py index 31d04fc64f..658e06fcbb 100644 --- a/tests/test_normalize_config.py +++ b/tests/test_normalize_config.py @@ -6,9 +6,9 @@ from unittest.mock import patch from axolotl.utils.config import ( - migrate_fsdp_config, normalize_cfg_datasets, normalize_config, + validate_config, ) from axolotl.utils.dict import DictDefault @@ -27,6 +27,13 @@ def _get_base_cfg(self): "num_epochs": 1, "micro_batch_size": 1, "gradient_accumulation_steps": 1, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "learning_rate": 0.0001, } ) @@ -97,7 +104,7 @@ def test_bf16_disables_fp16(self, mock_bf16_avail): def test_migrate_fsdp_config(self): """Test basic FSDP config migration with and without fsdp_version""" - cfg_with_version = DictDefault( + cfg_with_version = self._get_base_cfg() | DictDefault( { "fsdp_config": { "fsdp_version": 2, @@ -109,7 +116,7 @@ def test_migrate_fsdp_config(self): } ) - migrate_fsdp_config(cfg_with_version) + cfg_with_version = validate_config(cfg_with_version) self.assertEqual(cfg_with_version.fsdp_version, 2) self.assertEqual( @@ -125,7 +132,7 @@ def test_migrate_fsdp_config(self): self.assertNotIn("fsdp_version", cfg_with_version.fsdp_config) self.assertNotIn("version", cfg_with_version.fsdp_config) - cfg_without_version = DictDefault( + cfg_without_version = self._get_base_cfg() | DictDefault( { "fsdp_config": { "fsdp_auto_wrap_policy": "SIZE_BASED_WRAP", @@ -135,7 +142,7 @@ def test_migrate_fsdp_config(self): } ) - migrate_fsdp_config(cfg_without_version) + cfg_without_version = validate_config(cfg_without_version) self.assertNotIn("fsdp_version", cfg_without_version) self.assertEqual( @@ -149,26 +156,25 @@ def test_migrate_fsdp_config(self): def test_migrate_fsdp_config_no_fsdp_config(self): """Test that function doesn't crash when no fsdp_config is present""" - cfg = DictDefault({"some_other_config": "value"}) + cfg = self._get_base_cfg() - migrate_fsdp_config(cfg) + cfg = validate_config(cfg) self.assertNotIn("fsdp_config", cfg) self.assertNotIn("fsdp_version", cfg) - self.assertEqual(cfg.some_other_config, "value") def test_migrate_fsdp_config_empty_fsdp_config(self): """Test migration with empty fsdp_config""" - cfg = DictDefault({"fsdp_config": {}}) + cfg = self._get_base_cfg() | DictDefault({"fsdp_config": {}}) - migrate_fsdp_config(cfg) + cfg = validate_config(cfg) self.assertNotIn("fsdp_version", cfg) self.assertEqual(cfg.fsdp_config, {}) def test_migrate_fsdp_config_mixed_keys(self): """Test migration with a mix of fsdp_ and non-fsdp_ keys""" - cfg = DictDefault( + cfg = self._get_base_cfg() | DictDefault( { "fsdp_config": { "fsdp_version": 1, @@ -180,7 +186,7 @@ def test_migrate_fsdp_config_mixed_keys(self): } ) - migrate_fsdp_config(cfg) + cfg = validate_config(cfg) self.assertEqual(cfg.fsdp_version, 1) self.assertEqual(cfg.fsdp_config.state_dict_type, "FULL_STATE_DICT") diff --git a/tests/utils/schemas/validation/test_fsdp.py b/tests/utils/schemas/validation/test_fsdp.py new file mode 100644 index 0000000000..456040bc1e --- /dev/null +++ b/tests/utils/schemas/validation/test_fsdp.py @@ -0,0 +1,155 @@ +""" +tests for pydantic fsdp validation +""" + +# pylint: disable=too-many-boolean-expressions +import pytest + +from axolotl.utils.config import validate_config +from axolotl.utils.dict import DictDefault + + +@pytest.fixture(name="fsdp_base_cfg") +def fixture_fsdp_base_cfg(): + return DictDefault( + base_model="gpt2", + learning_rate=1e-3, + datasets=[ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + micro_batch_size=1, + gradient_accumulation_steps=1, + ) + + +class TestFSDPValidation: + """ + test class for pydantic fsdp validation + """ + + def test_fsdp_version_in_fsdp_config(self, fsdp_base_cfg): + cfg = fsdp_base_cfg | DictDefault( + fsdp_config={ + "fsdp_version": 2, + }, + ) + cfg = validate_config( + cfg, + ) + assert cfg.fsdp_version == 2 + assert cfg.fsdp_config.fsdp_version is None + + def test_fsdp_sharded_state_dict_safetensors(self, fsdp_base_cfg): + cfg = fsdp_base_cfg | DictDefault( + fsdp_config={ + "fsdp_state_dict_type": "SHARDED_STATE_DICT", + }, + save_safetensors=True, + ) + with pytest.raises( + ValueError, + match="FSDP SHARDED_STATE_DICT not compatible with save_safetensors", + ): + validate_config(cfg) + + # test w/o prefix too + cfg = fsdp_base_cfg | DictDefault( + fsdp_config={ + "state_dict_type": "SHARDED_STATE_DICT", + }, + save_safetensors=True, + ) + with pytest.raises( + ValueError, + match="FSDP SHARDED_STATE_DICT not compatible with save_safetensors", + ): + validate_config(cfg) + + def test_fsdp_offload_w_8bit_optim(self, fsdp_base_cfg): + cfg = fsdp_base_cfg | DictDefault( + fsdp_config={ + "offload_params": True, + }, + optimizer="adamw_8bit", + fsdp_version=1, + ) + with pytest.raises( + ValueError, match="FSDP Offload not compatible with adamw_8bit" + ): + validate_config(cfg) + + def test_fsdp2_w_8bit_optim(self, fsdp_base_cfg): + cfg = fsdp_base_cfg | DictDefault( + fsdp_config={ + "offload_params": True, + }, + optimizer="adamw_8bit", + fsdp_version=2, + ) + with pytest.raises( + ValueError, + match="FSDP2 not compatible with adamw_8bit, use `adamw_torch_8bit` instead", + ): + validate_config(cfg) + + def test_fsdp2_w_cpu_ram_efficient_loading(self, fsdp_base_cfg): + cfg = fsdp_base_cfg | DictDefault( + load_in_8bit=True, + adapter="lora", + fsdp_config={ + "cpu_ram_efficient_loading": True, + }, + fsdp_version=2, + ) + with pytest.raises( + ValueError, + match="FSDP2 does not support load_in_8bit or load_in_4bit with cpu_ram_efficient_loading.", + ): + validate_config(cfg) + + def test_fsdp_prefixes_removed(self, fsdp_base_cfg): + cfg = fsdp_base_cfg | DictDefault( + fsdp_config={ + "fsdp_version": 2, + "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "fsdp_transformer_layer_cls_to_wrap": "LlamaDecoderLayer", + "fsdp_reshard_after_forward": True, + } + ) + cfg = validate_config(cfg) + assert cfg.fsdp_version == 2 + assert cfg.fsdp_config.fsdp_version is None + for keys in cfg.fsdp_config.keys(): + assert not keys.startswith("fsdp_") + assert cfg.fsdp_config.auto_wrap_policy == "TRANSFORMER_BASED_WRAP" + assert cfg.fsdp_config.transformer_layer_cls_to_wrap == "LlamaDecoderLayer" + assert cfg.fsdp_config.reshard_after_forward is True + + @pytest.mark.parametrize( + "rl", + [ + "dpo", + "kto", + "orpo", + "ipo", + ], + ) + def test_fsdp2_dpo(self, fsdp_base_cfg, rl): + cfg = fsdp_base_cfg | DictDefault( + fsdp_version=2, + fsdp_config={ + "reshard_after_forward": True, + }, + rl=rl, + load_in_8bit=True, + adapter="lora", + remove_unused_columns=False, + ) + with pytest.raises( + ValueError, + match="FSDP2 does not support load_in_8bit or load_in_4bit with ", + ): + validate_config(cfg) From e581c15d40eac527bf215388ea1f6448018729ee Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 10:05:26 -0400 Subject: [PATCH 0800/1405] refactor dupes from merge/rebase (#2919) [skip ci] --- tests/conftest.py | 18 ++++++++ tests/test_train.py | 25 +++++------ tests/utils/schemas/validation/test_fsdp.py | 46 +++++++-------------- 3 files changed, 43 insertions(+), 46 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 24615fa224..9e1af318d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,6 +22,8 @@ from tokenizers import AddedToken from transformers import AutoTokenizer +from axolotl.utils.dict import DictDefault + from tests.hf_offline_utils import ( enable_hf_offline, hf_offline_context, @@ -539,6 +541,22 @@ def dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff( return datasets.load_from_disk(ds_path)["train"] +@pytest.fixture(name="min_base_cfg") +def fixture_min_base_cfg(): + return DictDefault( + base_model="HuggingFaceTB/SmolLM2-135M", + learning_rate=1e-3, + datasets=[ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + micro_batch_size=1, + gradient_accumulation_steps=1, + ) + + # # pylint: disable=redefined-outer-name,unused-argument @pytest.mark.skipif( os.environ.get("AXOLOTL_IS_CI_CACHE_PRELOAD", "-1") != "1", diff --git a/tests/test_train.py b/tests/test_train.py index 291e9136be..2c29b58eee 100644 --- a/tests/test_train.py +++ b/tests/test_train.py @@ -7,21 +7,16 @@ @pytest.fixture(name="train_base_cfg") -def fixture_train_base_cfg(): - return DictDefault( - base_model="gpt2", - learning_rate=1e-3, - datasets=[ - { - "path": "mhenrichsen/alpaca_2k_test", - "type": "alpaca", - }, - ], - micro_batch_size=2, - gradient_accumulation_steps=4, - sequence_len=2048, - sample_packing=True, - num_epochs=1, +def fixture_train_base_cfg(min_base_cfg): + return ( + DictDefault( + micro_batch_size=2, + gradient_accumulation_steps=4, + sequence_len=2048, + sample_packing=True, + num_epochs=1, + ) + | min_base_cfg ) diff --git a/tests/utils/schemas/validation/test_fsdp.py b/tests/utils/schemas/validation/test_fsdp.py index 456040bc1e..67f4a5cf90 100644 --- a/tests/utils/schemas/validation/test_fsdp.py +++ b/tests/utils/schemas/validation/test_fsdp.py @@ -9,29 +9,13 @@ from axolotl.utils.dict import DictDefault -@pytest.fixture(name="fsdp_base_cfg") -def fixture_fsdp_base_cfg(): - return DictDefault( - base_model="gpt2", - learning_rate=1e-3, - datasets=[ - { - "path": "mhenrichsen/alpaca_2k_test", - "type": "alpaca", - }, - ], - micro_batch_size=1, - gradient_accumulation_steps=1, - ) - - class TestFSDPValidation: """ test class for pydantic fsdp validation """ - def test_fsdp_version_in_fsdp_config(self, fsdp_base_cfg): - cfg = fsdp_base_cfg | DictDefault( + def test_fsdp_version_in_fsdp_config(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( fsdp_config={ "fsdp_version": 2, }, @@ -42,8 +26,8 @@ def test_fsdp_version_in_fsdp_config(self, fsdp_base_cfg): assert cfg.fsdp_version == 2 assert cfg.fsdp_config.fsdp_version is None - def test_fsdp_sharded_state_dict_safetensors(self, fsdp_base_cfg): - cfg = fsdp_base_cfg | DictDefault( + def test_fsdp_sharded_state_dict_safetensors(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( fsdp_config={ "fsdp_state_dict_type": "SHARDED_STATE_DICT", }, @@ -56,7 +40,7 @@ def test_fsdp_sharded_state_dict_safetensors(self, fsdp_base_cfg): validate_config(cfg) # test w/o prefix too - cfg = fsdp_base_cfg | DictDefault( + cfg = min_base_cfg | DictDefault( fsdp_config={ "state_dict_type": "SHARDED_STATE_DICT", }, @@ -68,8 +52,8 @@ def test_fsdp_sharded_state_dict_safetensors(self, fsdp_base_cfg): ): validate_config(cfg) - def test_fsdp_offload_w_8bit_optim(self, fsdp_base_cfg): - cfg = fsdp_base_cfg | DictDefault( + def test_fsdp_offload_w_8bit_optim(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( fsdp_config={ "offload_params": True, }, @@ -81,8 +65,8 @@ def test_fsdp_offload_w_8bit_optim(self, fsdp_base_cfg): ): validate_config(cfg) - def test_fsdp2_w_8bit_optim(self, fsdp_base_cfg): - cfg = fsdp_base_cfg | DictDefault( + def test_fsdp2_w_8bit_optim(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( fsdp_config={ "offload_params": True, }, @@ -95,8 +79,8 @@ def test_fsdp2_w_8bit_optim(self, fsdp_base_cfg): ): validate_config(cfg) - def test_fsdp2_w_cpu_ram_efficient_loading(self, fsdp_base_cfg): - cfg = fsdp_base_cfg | DictDefault( + def test_fsdp2_w_cpu_ram_efficient_loading(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( load_in_8bit=True, adapter="lora", fsdp_config={ @@ -110,8 +94,8 @@ def test_fsdp2_w_cpu_ram_efficient_loading(self, fsdp_base_cfg): ): validate_config(cfg) - def test_fsdp_prefixes_removed(self, fsdp_base_cfg): - cfg = fsdp_base_cfg | DictDefault( + def test_fsdp_prefixes_removed(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( fsdp_config={ "fsdp_version": 2, "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", @@ -137,8 +121,8 @@ def test_fsdp_prefixes_removed(self, fsdp_base_cfg): "ipo", ], ) - def test_fsdp2_dpo(self, fsdp_base_cfg, rl): - cfg = fsdp_base_cfg | DictDefault( + def test_fsdp2_dpo(self, min_base_cfg, rl): + cfg = min_base_cfg | DictDefault( fsdp_version=2, fsdp_config={ "reshard_after_forward": True, From 37edbe4999132839f7af5acf7f4234b1cf3779f4 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 14 Jul 2025 12:32:45 -0400 Subject: [PATCH 0801/1405] Remove extra torch.compile call (#2904) * debug * debug * debug * moving validation code to transformers * revert unneeded change * add accelerator config to base trainer builder * add back accumulated_cache_size_limit setting * lint --- src/axolotl/core/builders/base.py | 8 ++++++++ src/axolotl/core/builders/causal.py | 5 ----- src/axolotl/core/trainers/base.py | 12 ------------ 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 3c0ca77deb..8ded23661a 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -418,6 +418,9 @@ def _configure_torch_compile(self, training_args_kwargs: dict): torch._dynamo.config.suppress_errors = ( # pylint: disable=protected-access True ) + torch._dynamo.config.accumulated_cache_size_limit = ( # pylint: disable=protected-access + 256 + ) training_args_kwargs["torch_compile"] = self.cfg.torch_compile if self.cfg.torch_compile_backend: training_args_kwargs["torch_compile_backend"] = ( @@ -426,6 +429,10 @@ def _configure_torch_compile(self, training_args_kwargs: dict): if self.cfg.torch_compile_mode: training_args_kwargs["torch_compile_mode"] = self.cfg.torch_compile_mode + def _configure_accelerator_config(self, training_args_kwargs: dict): + if self.cfg.accelerator_config: + training_args_kwargs["accelerator_config"] = self.cfg.accelerator_config + def _configure_gradient_checkpointing(self, training_args_kwargs: dict): if self.cfg.gradient_checkpointing: training_args_kwargs["gradient_checkpointing"] = ( @@ -510,5 +517,6 @@ def _set_base_training_args( self._configure_scheduler(training_args_kwargs) self._configure_optimizer(training_args_kwargs, trainer_kwargs) self._configure_torch_compile(training_args_kwargs) + self._configure_accelerator_config(training_args_kwargs) return training_args_kwargs, trainer_kwargs diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 9fcd51c1d5..00cee35a72 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -310,11 +310,6 @@ def build(self, total_num_steps): self.cfg.neftune_noise_alpha ) - if self.cfg.accelerator_config: - training_arguments_kwargs["accelerator_config"] = ( - self.cfg.accelerator_config - ) - if self.cfg.image_size: training_arguments_kwargs["image_size"] = self.cfg.image_size if self.cfg.image_resize_algorithm: diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 81a2f5a452..6b2d30709a 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -75,18 +75,6 @@ def __init__( if self.args.orpo_alpha: self.loss_fct = torch.nn.CrossEntropyLoss(reduction="none") - def _wrap_model(self, model, training=True, dataloader=None): - if self.args.torch_compile: - torch._dynamo.config.accumulated_cache_size_limit = ( # pylint: disable=protected-access - 256 - ) - model = torch.compile( - model, - backend=self.args.torch_compile_backend, - mode=self.args.torch_compile_mode, - ) - return super()._wrap_model(model, training=training, dataloader=dataloader) - def _create_multipack_sampler( self, base_sampler: Sampler, dataset: Dataset ) -> MultipackBatchSampler: From ca4d4ef79318e7ee7d3f3673a053741cf88d0d83 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 14:19:19 -0400 Subject: [PATCH 0802/1405] don't init distributed for deepspeed if preprocessing (#2920) * don't init distributed for deepspeed if preprocessing * add e2e test to validate preprocess cli with deepspeed * ignore duplicate code for cfg --- src/axolotl/cli/preprocess.py | 2 ++ src/axolotl/utils/trainer.py | 5 ++- tests/e2e/test_preprocess.py | 58 +++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/test_preprocess.py diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index d0c2ad1653..ebadc9bf16 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -1,5 +1,6 @@ """CLI to run preprocessing of a dataset.""" +import os import warnings from pathlib import Path from typing import Union @@ -95,6 +96,7 @@ def do_cli( kwargs: Additional keyword arguments to override config file values. """ # pylint: disable=duplicate-code + os.environ["AXOLOTL_IS_PREPROCESS"] = "1" parsed_cfg = load_cfg(config, **kwargs) parsed_cfg.is_preprocess = True parser = transformers.HfArgumentParser(PreprocessCliArgs) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 9224202e1a..a512d64005 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -546,7 +546,10 @@ def setup_deepspeed_env(cfg, stage=None): # NOTE(djsaunde): The distribued state cannot be initialized prior to the # ACCELERATE_USE_DEEPSPEED assignment, but it must be initialized some time prior # to model load. - if int(os.environ.get("WORLD_SIZE", "1")) == 1: + if ( + int(os.environ.get("WORLD_SIZE", "1")) == 1 + and os.environ.get("AXOLOTL_IS_PREPROCESS", "0") != "1" + ): os.environ["WORLD_SIZE"] = "1" # force it in case not set os.environ["LOCAL_RANK"] = "0" # force it in case not set os.environ["RANK"] = os.environ.get("LOCAL_RANK", "0") diff --git a/tests/e2e/test_preprocess.py b/tests/e2e/test_preprocess.py new file mode 100644 index 0000000000..25f42e8329 --- /dev/null +++ b/tests/e2e/test_preprocess.py @@ -0,0 +1,58 @@ +"""E2E Test the preprocess cli""" + +from pathlib import Path + +import yaml +from accelerate.test_utils import execute_subprocess_async + +from axolotl.utils.dict import DictDefault + +AXOLOTL_ROOT = Path(__file__).parent.parent.parent + + +class TestPreprocess: + """test cases for preprocess""" + + def test_w_deepspeed(self, temp_dir): + """make sure preproces doesn't choke when using deepspeed in the config""" + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:10%]", + }, + ], + "num_epochs": 1, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "bf16": "auto", + "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero1.json"), + "dataset_prepared_path": temp_dir + "/last_run_prepared", + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "preprocess", + str(Path(temp_dir) / "config.yaml"), + ] + ) + + assert (Path(temp_dir) / "last_run_prepared").exists() From aa684122f127bad2a1444c3fcc4e6d18a28ae13f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 20:09:26 -0400 Subject: [PATCH 0803/1405] upgrade peft==0.16.0 and datasets==4.0.0 (#2917) [skip ci] * upgrade peft to 0.16.0 * upgrade datasets to 4.0.0 * refactor dupes from merge/rebase * fix check for fsdp1 + sharded_state_dict * use full state dict for ci --- requirements.txt | 4 ++-- src/axolotl/utils/schemas/validation.py | 1 + tests/e2e/multigpu/test_llama.py | 10 +++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index eeb3b864df..215bc12718 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,11 +12,11 @@ liger-kernel==0.6.0 packaging==23.2 huggingface_hub>=0.33.0 -peft==0.15.2 +peft==0.16.0 transformers==4.53.2 tokenizers>=0.21.1 accelerate==1.8.1 -datasets==3.6.0 +datasets==4.0.0 deepspeed>=0.17.0 trl==0.19.1 hf_xet==1.1.2 diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 534d89a98d..bf2bc9070f 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -865,6 +865,7 @@ def check_fsdp_sharded_state_dict_w_safetensors(self): and hasattr(self, "save_safetensors") and self.save_safetensors and self.fsdp_config.get("state_dict_type", "") == "SHARDED_STATE_DICT" + and str(getattr(self, "fsdp_version", "1")) != "2" ): raise ValueError( "FSDP SHARDED_STATE_DICT not compatible with save_safetensors" diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index fcc174f27b..f0c74fbf8b 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -391,7 +391,10 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): @pytest.mark.parametrize( "fsdp_state_dict_type", - ["FULL_STATE_DICT", "SHARDED_STATE_DICT"], + [ + "FULL_STATE_DICT", + # "SHARDED_STATE_DICT", # not supported since intermediate checkpoints fail with fsdp1 + ], ) def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): # pylint: disable=duplicate-code @@ -413,7 +416,8 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): }, ], "num_epochs": 1, - "max_steps": 2, + "max_steps": 3, + "save_steps": 2, "micro_batch_size": 2, "gradient_accumulation_steps": 2, # "gradient_checkpointing": True, @@ -597,7 +601,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "fsdp_use_orig_params": False, "fsdp_cpu_ram_efficient_loading": True, "fsdp_transformer_layer_cls_to_wrap": "LlamaDecoderLayer", - "fsdp_state_dict_type": "SHARDED_STATE_DICT", + "fsdp_state_dict_type": "FULL_STATE_DICT", "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", }, "use_tensorboard": True, From 99187cd2082594fb51eefc5d5fe36eca33088829 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 20:10:20 -0400 Subject: [PATCH 0804/1405] Activation Offloading w CUDA Streams (#2900) [skip ci] * use cuda streams for activation offloading * use torch native ops * update cfg schema for streams * fix literal constructor for set * use context for training step so it doesn't affect evals * disable streams * auto gc on eval steps * use activation_offloading config arg * add docs for gradient checkpointing * handle validation for gc/ao * use cuda streams for act offloading * add more validation for AC w/o GC * fix docs * move activation_offloading lower in definition so it doesn't break args/kwargs * fix kd due to import order --- _quarto.yml | 1 + docs/gradient_checkpointing.qmd | 29 ++++ src/axolotl/core/builders/base.py | 6 +- src/axolotl/core/trainers/base.py | 2 + src/axolotl/core/trainers/mixins/__init__.py | 1 + .../mixins/activation_checkpointing.py | 37 +++++ src/axolotl/core/training_args_base.py | 5 + src/axolotl/loaders/model.py | 10 ++ src/axolotl/loaders/patch_manager.py | 28 +--- .../gradient_checkpointing/__init__.py | 1 - .../gradient_checkpointing/offload_cpu.py | 157 ------------------ src/axolotl/utils/callbacks/__init__.py | 28 +++- src/axolotl/utils/schemas/config.py | 13 +- src/axolotl/utils/schemas/validation.py | 22 +++ 14 files changed, 154 insertions(+), 186 deletions(-) create mode 100644 docs/gradient_checkpointing.qmd create mode 100644 src/axolotl/core/trainers/mixins/activation_checkpointing.py diff --git a/_quarto.yml b/_quarto.yml index 93141aa9e1..3e773a748f 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -276,6 +276,7 @@ website: - docs/torchao.qmd - docs/custom_integrations.qmd - docs/sequence_parallelism.qmd + - docs/gradient_checkpointing.qmd - section: "Troubleshooting" contents: diff --git a/docs/gradient_checkpointing.qmd b/docs/gradient_checkpointing.qmd new file mode 100644 index 0000000000..25a887999f --- /dev/null +++ b/docs/gradient_checkpointing.qmd @@ -0,0 +1,29 @@ +--- +title: Gradient Checkpointing and Activation Offloading +--- + +Gradient checkpointing and activation offloading are techniques used to optimize the performance of deep learning +models by reducing the memory footprint and improving computational efficiency. + +### Enabling Gradient Checkpointing + +```yaml +gradient_checkpointing: true +``` + +### Enabling Activation Offloading + +```yaml +gradient_checkpointing: true # required for activation offloading +activation_offloading: true +``` + +Activation offloading variants: + +The default `activation_offloading: true` offloads activations to CPU and uses CUDA streams +to overlap the communications and computations when offloading. + +The `activation_offloading: legacy` naively offloads activations to CPU and without additional optimizations. + +For resource constrained environments with limited CPU memory, `activation_offloading: disk` offloads +activations to disk instead of CPU RAM so that much larger context lengths can be trained with minimal memory. diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 8ded23661a..e80e905b8c 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -434,7 +434,11 @@ def _configure_accelerator_config(self, training_args_kwargs: dict): training_args_kwargs["accelerator_config"] = self.cfg.accelerator_config def _configure_gradient_checkpointing(self, training_args_kwargs: dict): - if self.cfg.gradient_checkpointing: + if self.cfg.activation_offloading is True: + # don't use the HF gradient checkpointing, manually wrap + training_args_kwargs["gradient_checkpointing"] = False + training_args_kwargs["activation_offloading"] = True + elif self.cfg.gradient_checkpointing: training_args_kwargs["gradient_checkpointing"] = ( self.cfg.gradient_checkpointing ) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 6b2d30709a..b983f10765 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -25,6 +25,7 @@ from typing_extensions import override from axolotl.core.trainers.mixins import ( + ActivationOffloadingMixin, CheckpointSaveMixin, OptimizerMixin, PackingMixin, @@ -48,6 +49,7 @@ class AxolotlTrainer( OptimizerMixin, RngLoaderMixin, CheckpointSaveMixin, + ActivationOffloadingMixin, Trainer, ): """Extend the base Trainer for axolotl helpers""" diff --git a/src/axolotl/core/trainers/mixins/__init__.py b/src/axolotl/core/trainers/mixins/__init__.py index b73b511269..453810aacc 100644 --- a/src/axolotl/core/trainers/mixins/__init__.py +++ b/src/axolotl/core/trainers/mixins/__init__.py @@ -3,6 +3,7 @@ # pylint: disable=unused-import # flake8: noqa +from .activation_checkpointing import ActivationOffloadingMixin from .checkpoints import CheckpointSaveMixin from .optimizer import OptimizerMixin from .packing import PackingMixin diff --git a/src/axolotl/core/trainers/mixins/activation_checkpointing.py b/src/axolotl/core/trainers/mixins/activation_checkpointing.py new file mode 100644 index 0000000000..9488186cdc --- /dev/null +++ b/src/axolotl/core/trainers/mixins/activation_checkpointing.py @@ -0,0 +1,37 @@ +""" +Trainer mixin for activation checkpointing w offloading +""" + +import contextlib + +from torch import nn +from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import ( + apply_activation_checkpointing, +) +from torch.distributed.fsdp.wrap import ModuleWrapPolicy +from transformers import GradientCheckpointingLayer, Trainer +from trl.models.activation_offloading import get_act_offloading_ctx_manager + + +class ActivationOffloadingMixin(Trainer): + """ + Trainer mixin class for activation checkpointing w offloading + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.args.activation_offloading: + self.activation_offload_context = get_act_offloading_ctx_manager( + self.model, use_streams=True + ) + else: + self.activation_offload_context = contextlib.nullcontext() + + def training_step(self, *args, **kwargs): + with self.activation_offload_context: + return super().training_step(*args, **kwargs) + + +def ac_wrap_hf_model(model: nn.Module, **kwargs): + auto_wrap_policy = ModuleWrapPolicy(set((GradientCheckpointingLayer,))) + apply_activation_checkpointing(model, auto_wrap_policy=auto_wrap_policy, **kwargs) diff --git a/src/axolotl/core/training_args_base.py b/src/axolotl/core/training_args_base.py index 2e1987e82c..4b74676ced 100644 --- a/src/axolotl/core/training_args_base.py +++ b/src/axolotl/core/training_args_base.py @@ -217,6 +217,11 @@ class AxolotlTrainingMixins: }, ) + activation_offloading: bool | None = field( + default=None, + metadata={"help": "Use activation offloading with CUDA streams for training."}, + ) + # multi-modal section image_size: int | tuple[int, int] | None = field( diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 03678e1b4a..1ce98ef319 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -198,12 +198,22 @@ def _apply_post_model_load_setup(self): ): self.model = self.model.merge_and_unload() + self._apply_activation_checkpointing() self._resize_token_embeddings() self._adjust_model_config() self._configure_embedding_dtypes() self._configure_qat() log_gpu_memory_usage(LOG, "Memory usage after model load", 0) + def _apply_activation_checkpointing(self): + if self.cfg.activation_offloading is True: + from axolotl.core.trainers.mixins.activation_checkpointing import ( + ac_wrap_hf_model, + ) + + # ^^ importing this at the module level breaks plugins + ac_wrap_hf_model(self.model) + def _resize_token_embeddings(self): """Resize token embeddings if needed.""" embeddings_len = ( diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 2544429e61..84e6b33def 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -7,7 +7,6 @@ from functools import cached_property import addict -import torch import transformers from transformers import PretrainedConfig, PreTrainedModel @@ -168,28 +167,19 @@ def _apply_flash_attention_peft_patches(self): def _apply_gradient_checkpointing_patches(self): """Apply patches for gradient checkpointing.""" - if self.cfg.gradient_checkpointing in ["unsloth", "offload"]: + if ( + self.cfg.gradient_checkpointing + and self.cfg.activation_offloading == "legacy" + ): from axolotl.monkeypatch.gradient_checkpointing import ( - CheckpointFunctionWithCPUOffload, hf_grad_checkpoint_offload_wrapper, ) - if ( - self.cfg.gradient_checkpointing_kwargs - and "use_reentrant" in self.cfg.gradient_checkpointing_kwargs - and self.cfg.gradient_checkpointing_kwargs["use_reentrant"] is False - ): - transformers.modeling_utils.checkpoint = ( - hf_grad_checkpoint_offload_wrapper - ) - else: - transformers.modeling_utils.checkpoint.CheckpointFunction = ( - CheckpointFunctionWithCPUOffload - ) - torch.utils.checkpoint.CheckpointFunction = ( - CheckpointFunctionWithCPUOffload - ) - if self.cfg.gradient_checkpointing == "offload_disk": + transformers.modeling_utils.checkpoint = hf_grad_checkpoint_offload_wrapper + elif ( + self.cfg.gradient_checkpointing + and self.cfg.activation_offloading == "offload_disk" + ): from axolotl.monkeypatch.gradient_checkpointing import ( hf_grad_checkpoint_disk_offload_wrapper, ) diff --git a/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py b/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py index 6ca8e02404..3b090d5e56 100644 --- a/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py +++ b/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py @@ -6,7 +6,6 @@ from packaging import version from axolotl.monkeypatch.gradient_checkpointing.offload_cpu import ( # noqa: F401 - CheckpointFunctionWithCPUOffload, CPU_Offloaded_Gradient_Checkpointer, ) from axolotl.monkeypatch.gradient_checkpointing.offload_disk import ( diff --git a/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py b/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py index 432cafb350..bbcfb91e69 100644 --- a/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py +++ b/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py @@ -14,18 +14,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import contextlib import inspect import torch from packaging import version from torch.utils.checkpoint import ( - _get_autocast_kwargs, - _get_device_module, - _infer_device_type, - check_backward_validity, - detach_variable, - get_device_states, set_device_states, ) @@ -76,153 +69,3 @@ def backward(ctx, dY): ) + ( None, ) * len(ctx.args) - - -# Copyright 2025 Snowflake Inc. -# SPDX-License-Identifier: Apache-2.0 -# https://github.com/snowflakedb/ArcticTraining/blob/main/arctic_training/monkey_patches.py -class CheckpointFunctionWithCPUOffload(torch.autograd.Function): - """ - This is a torch/utils/checkpoint.py CheckpointFunction monkey patch that offloads the first tensor to cpu during forward and back to cuda during backward. This allows significant memory savings when using a very long seqlen. e.g. for llama 8b at 100k it's 24GB saved per gpu: `((100_000*4096)*2*32/2**30)` - In the case of a very long seqlen 100k+ the copying to/from cpu overhead is not big, because dense quadratic attention compute will dominate. - """ - - @staticmethod - def forward(ctx, run_function, preserve_rng_state, *args): - check_backward_validity(args) - ctx.run_function = run_function - ctx.preserve_rng_state = preserve_rng_state - # Accommodates the (remote) possibility that autocast is enabled for cpu AND gpu. - ctx.device_type = _infer_device_type(*args) - ctx.device_autocast_kwargs, ctx.cpu_autocast_kwargs = _get_autocast_kwargs( - ctx.device_type - ) - if preserve_rng_state: - ctx.fwd_cpu_state = torch.get_rng_state() - # Don't eagerly initialize the cuda context by accident. - # (If the user intends that the context is initialized later, within their - # run_function, we SHOULD actually stash the cuda state here. Unfortunately, - # we have no way to anticipate this will happen before we run the function.) - ctx.had_device_in_fwd = False - device_module = _get_device_module(ctx.device_type) - if getattr(device_module, "_initialized", False): - ctx.had_device_in_fwd = True - ctx.fwd_devices, ctx.fwd_device_states = get_device_states(*args) - - # Save non-tensor inputs in ctx, keep a placeholder None for tensors - # to be filled out during the backward. - ctx.inputs = [] - ctx.tensor_indices = [] - tensor_inputs = [] - # x = None - for i, arg in enumerate(args): - if torch.is_tensor(arg): - # cpu-offload - # we don't want the 2nd tensor - usually it's a shared 4D attn mask which is huge [seq,seq] - # upstream could accept a list of arg indices to offload - if i == 0: - # print(f"{arg.shape=}") - ctx.x_device = arg.device - ctx.x_requires_grad = arg.requires_grad - t = arg.detach().cpu() - else: - t = arg - tensor_inputs.append(t) - ctx.tensor_indices.append(i) - ctx.inputs.append(None) - else: - ctx.inputs.append(arg) - - ctx.save_for_backward(*tensor_inputs) - - with torch.no_grad(): - outputs = run_function(*args) - - return outputs - - @staticmethod - def backward(ctx, *args): - if ( - not torch.autograd._is_checkpoint_valid() # pylint: disable=protected-access - ): - raise RuntimeError( - "When use_reentrant=True, torch.utils.checkpoint is incompatible" - " with .grad() or passing an `inputs` parameter to .backward()." - " To resolve this error, you can either set use_reentrant=False," - " or call .backward() without passing the `inputs` argument." - ) - # Copy the list to avoid modifying original list. - inputs = list(ctx.inputs) - tensor_indices = ctx.tensor_indices - tensors = ctx.saved_tensors - - # Fill in inputs with appropriate saved tensors. - for i, idx in enumerate(tensor_indices): - if i == 0: - t = ( - tensors[i] - .to(ctx.x_device) - .detach() - .requires_grad_(ctx.x_requires_grad) - ) - else: - t = tensors[i] - inputs[idx] = t - - # Stash the surrounding rng state, and mimic the state that was - # present at this time during forward. Restore the surrounding state - # when we're done. - rng_devices = [] - if ctx.preserve_rng_state and ctx.had_device_in_fwd: - rng_devices = ctx.fwd_devices - with torch.random.fork_rng( - devices=rng_devices, - enabled=ctx.preserve_rng_state, - device_type=ctx.device_type, - ): - if ctx.preserve_rng_state: - torch.set_rng_state(ctx.fwd_cpu_state) - if ctx.had_device_in_fwd: - if has_device_type: - # newer pytorch (as early as 2.7) - set_device_states( - ctx.fwd_devices, - ctx.fwd_device_states, - device_type=ctx.device_type, - ) - else: - # older pytorch (at least 2.4) - set_device_states(ctx.fwd_devices, ctx.fwd_device_states) - detached_inputs = detach_variable(tuple(inputs)) - - device_autocast_ctx = ( - torch.amp.autocast( - device_type=ctx.device_type, **ctx.device_autocast_kwargs - ) - if torch.amp.is_autocast_available(ctx.device_type) - else contextlib.nullcontext() - ) - with torch.enable_grad(), device_autocast_ctx, torch.amp.autocast("cpu", **ctx.cpu_autocast_kwargs): # type: ignore[attr-defined] - outputs = ctx.run_function(*detached_inputs) - - if isinstance(outputs, torch.Tensor): - outputs = (outputs,) - - # run backward() with only tensor that requires grad - outputs_with_grad = [] - args_with_grad = [] - for i in range(len(outputs)): # pylint: disable=consider-using-enumerate - if torch.is_tensor(outputs[i]) and outputs[i].requires_grad: - outputs_with_grad.append(outputs[i]) - args_with_grad.append(args[i]) - if len(outputs_with_grad) == 0: - raise RuntimeError( - "none of output has requires_grad=True, this checkpoint() is not necessary" - ) - torch.autograd.backward(outputs_with_grad, args_with_grad) - grads = tuple( - inp.grad if isinstance(inp, torch.Tensor) else None - for inp in detached_inputs - ) - - return (None, None) + grads diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 2a93ceef54..5f804d6afa 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -841,21 +841,35 @@ def on_train_begin( class GCCallback(TrainerCallback): """Callback to garbage collect torch cache""" - def __init__(self, gc_steps=None): - self.gc_steps = gc_steps + def __init__(self, gc_steps: int | None = -1): + self.gc_steps: int = gc_steps or -1 + self.next_gc_on_begin_step: int = -1 + + def _gc(self): + torch.cuda.empty_cache() + gc.collect() + + def on_step_begin( + self, args, state, control, **kwargs # pylint: disable=unused-argument + ): + if self.next_gc_on_begin_step == state.global_step: + self._gc() def on_step_end( self, args, state, control, **kwargs # pylint: disable=unused-argument ): - if self.gc_steps > 0 and state.global_step % self.gc_steps == 0: - torch.cuda.empty_cache() - gc.collect() + if control.should_evaluate: + # automatically GC before evals so the eval memory spike from the CEL doesn't OOM the trainer + self._gc() + # also GC on the start of the next step after the eval + self.next_gc_on_begin_step = state.global_step + 1 + elif self.gc_steps > 0 and state.global_step % self.gc_steps == 0: + self._gc() def on_epoch_end( self, args, state, control, **kwargs # pylint: disable=unused-argument ): - torch.cuda.empty_cache() - gc.collect() + self._gc() def colab_inference_post_train_callback(trainer: Trainer): diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 6668380bf1..f757cc5b0c 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -320,7 +320,12 @@ class AxolotlInputConfig( }, ) - gc_steps: int | None = None + gc_steps: int | None = Field( + default=None, + json_schema_extra={ + "description": "Run garbage collection every `gc_steps` steps. -1 will run on epoch end and before evaluations. Default is 0 (disabled)." + }, + ) bf16: Literal["auto"] | bool | None = Field( default="auto", @@ -360,6 +365,12 @@ class AxolotlInputConfig( "description": "Additional kwargs to pass to the trainer for gradient checkpointing" }, ) + activation_offloading: Literal["legacy", "disk"] | bool | None = Field( + default=False, + json_schema_extra={ + "description": "Whether to offload activations. Available options are: true, false, 'legacy', 'disk'." + }, + ) unfrozen_parameters: list[str] | None = None diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index bf2bc9070f..db3fd0a1c8 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1017,6 +1017,28 @@ def check_offload_grad_checkpointing(self): self.gradient_checkpointing = "offload" return self + @model_validator(mode="after") + def check_gradient_checkpointing_w_offload(self): + if self.gradient_checkpointing == "offload": + LOG.warning( + "`offload` is deprecated for gradient_checkpointing, use `activation_offloading: true`" + ) + self.gradient_checkpointing = True + self.activation_offloading = True + if self.gradient_checkpointing == "offload_disk": + LOG.warning( + "`offload_disk` is deprecated for gradient_checkpointing, use `activation_offloading: disk`" + ) + self.gradient_checkpointing = True + self.activation_offloading = "disk" + return self + + @model_validator(mode="after") + def check_activation_offloading_wo_gc(self): + if self.activation_offloading and not self.gradient_checkpointing: + raise ValueError("activation_offloading requires gradient_checkpointing") + return self + @model_validator(mode="after") def check_better_transformers(self): if self.flash_optimum is True: From 7dc3ac6cb36f92f47eb9bd3adc48cf4eb7020e2e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 20:10:43 -0400 Subject: [PATCH 0805/1405] update nightlies builds (#2921) [skip ci] --- .github/workflows/nightlies.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 824c7e4f27..49bce470b8 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -12,11 +12,16 @@ jobs: fail-fast: false matrix: include: - - cuda: 124 - cuda_version: 12.4.1 + - cuda: 126 + cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.6.0 axolotl_extras: + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.1 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -60,15 +65,15 @@ jobs: strategy: matrix: include: - - cuda: 124 - cuda_version: 12.4.1 + - cuda: 126 + cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.6.0 axolotl_extras: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" - pytorch: 2.6.0 + pytorch: 2.7.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: From 38359a8997ef0023de55501c110c7546cfb2115a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 20:11:11 -0400 Subject: [PATCH 0806/1405] allow profiling in mid-training rather from the start (#2899) [skip ci] * allow profiling in mid-training rather from the start * simplify based on PR feedback * fix logic, improve saving at end, add tests --- src/axolotl/core/builders/base.py | 15 ++-- src/axolotl/utils/callbacks/profiler.py | 47 +++++++++- src/axolotl/utils/schemas/config.py | 6 ++ tests/e2e/test_profiler.py | 113 ++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 tests/e2e/test_profiler.py diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index e80e905b8c..4df0100406 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -112,13 +112,6 @@ def get_callbacks(self) -> list[TrainerCallback]: plugin_manager.add_callbacks_pre_trainer(cfg=self.cfg, model=self.model) ) - if self.cfg.profiler_steps: - callbacks.append( - PytorchProfilerCallback( - steps_to_profile=self.cfg.profiler_steps, - ) - ) - if self.cfg.gc_steps: callbacks.append(GCCallback(gc_steps=self.cfg.gc_steps)) @@ -145,6 +138,14 @@ def get_callbacks(self) -> list[TrainerCallback]: callbacks.append(GPUStatsCallback(cfg=self.cfg)) + if self.cfg.profiler_steps: + callbacks.append( + PytorchProfilerCallback( + steps_to_profile=self.cfg.profiler_steps, + profiler_steps_start=self.cfg.profiler_steps_start, + ) + ) + return callbacks def get_post_trainer_create_callbacks(self, trainer): diff --git a/src/axolotl/utils/callbacks/profiler.py b/src/axolotl/utils/callbacks/profiler.py index 36604813f7..d26b7f9dd0 100644 --- a/src/axolotl/utils/callbacks/profiler.py +++ b/src/axolotl/utils/callbacks/profiler.py @@ -19,9 +19,27 @@ class PytorchProfilerCallback(TrainerCallback): PyTorch Profiler callback to create snapshots of GPU memory usage at specified steps. """ - def __init__(self, steps_to_profile: int = 5): - self.steps_to_profile = steps_to_profile - if self.steps_to_profile: + def __init__(self, steps_to_profile: int = 5, profiler_steps_start: int = 0): + # steps are 0 indexed, so to start at 0-th step, we start at beginning of first step, + # and finish at end of last step, so 5 steps_to_profile is steps [0, 1, 2, 3, 4] + self.profiler_steps_end = profiler_steps_start + steps_to_profile - 1 + if profiler_steps_start == 0: + # start recording memory allocations before everything is allocated, because if we start + # at the beginning of step 0, we won't have any memory allocations in the traces + torch.cuda.memory._record_memory_history( # pylint: disable=protected-access + enabled="all" + ) + profiler_steps_start = -1 + self.profiler_steps_start = profiler_steps_start + + def on_step_begin( # pylint: disable=unused-argument + self, + args: TrainingArguments, # pylint: disable=unused-argument + state: TrainerState, + control: TrainerControl, # pylint: disable=unused-argument + **kwargs, # pylint: disable=unused-argument + ): + if state.global_step == self.profiler_steps_start: torch.cuda.memory._record_memory_history( # pylint: disable=protected-access enabled="all" ) @@ -33,7 +51,28 @@ def on_step_end( # pylint: disable=unused-argument control: TrainerControl, # pylint: disable=unused-argument **kwargs, # pylint: disable=unused-argument ): - if state.global_step == self.steps_to_profile: + if state.global_step == self.profiler_steps_end: + snapshot = torch.cuda.memory._snapshot() # pylint: disable=protected-access + with open(Path(args.output_dir) / "snapshot.pickle", "wb") as fout: + dump(snapshot, fout) + + # tell CUDA to stop recording memory allocations now + torch.cuda.memory._record_memory_history( # pylint: disable=protected-access + enabled=None + ) + + def on_train_end( # pylint: disable=unused-argument + self, + args: TrainingArguments, # pylint: disable=unused-argument + state: TrainerState, + control: TrainerControl, # pylint: disable=unused-argument + **kwargs, # pylint: disable=unused-argument + ): + # make sure to record if we happen to have more steps than steps to profile + if ( + state.global_step >= self.profiler_steps_start + and state.global_step < self.profiler_steps_end + ): snapshot = torch.cuda.memory._snapshot() # pylint: disable=protected-access with open(Path(args.output_dir) / "snapshot.pickle", "wb") as fout: dump(snapshot, fout) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index f757cc5b0c..1726feb67f 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -741,6 +741,12 @@ class AxolotlInputConfig( "description": "Enable the pytorch profiler to capture the first N steps of training to the output_dir. see https://pytorch.org/blog/understanding-gpu-memory-1/ for more information. Snapshots can be visualized @ https://pytorch.org/memory_viz" }, ) + profiler_steps_start: int | None = Field( + default=0, + json_schema_extra={ + "description": "Which step to start the profiler at. Useful for only capturing a few steps mid-run." + }, + ) include_tokens_per_second: bool | None = Field( default=None, json_schema_extra={ diff --git a/tests/e2e/test_profiler.py b/tests/e2e/test_profiler.py new file mode 100644 index 0000000000..ab273b9810 --- /dev/null +++ b/tests/e2e/test_profiler.py @@ -0,0 +1,113 @@ +""" +e2e gpu test for the pytorch profiler callback +""" + +from pathlib import Path + +import pytest + +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + + +@pytest.fixture(name="profiler_base_cfg") +def fixture_profiler_base_cfg(): + cfg = DictDefault( + base_model="HuggingFaceTB/SmolLM2-135M", + tokenizer_type="AutoTokenizer", + sequence_len=1024, + load_in_8bit=True, + adapter="lora", + lora_r=8, + lora_alpha=16, + lora_dropout=0.05, + lora_target_linear=True, + val_set_size=0.02, + special_tokens={"pad_token": "<|endoftext|>"}, + datasets=[ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + num_epochs=1, + micro_batch_size=2, + gradient_accumulation_steps=1, + learning_rate=0.00001, + optimizer="adamw_torch_fused", + lr_scheduler="cosine", + ) + return cfg + + +class TestProfiler: + """ + test cases for the pytorch profiler callback + """ + + def test_profiler_saves(self, profiler_base_cfg, temp_dir): + cfg = profiler_base_cfg | DictDefault( + output_dir=temp_dir, + max_steps=5, + profiler_steps=3, + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "snapshot.pickle").exists() + + def test_profiler_saves_w_start(self, profiler_base_cfg, temp_dir): + cfg = profiler_base_cfg | DictDefault( + output_dir=temp_dir, + max_steps=5, + profiler_steps=3, + profiler_steps_start=1, + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "snapshot.pickle").exists() + + @pytest.mark.parametrize( + "profiler_steps_start", + [3, 5], + ) + def test_profiler_saves_past_end( + self, profiler_base_cfg, temp_dir, profiler_steps_start + ): + cfg = profiler_base_cfg | DictDefault( + output_dir=temp_dir, + max_steps=5, + profiler_steps=3, + profiler_steps_start=profiler_steps_start, + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + assert (Path(temp_dir) / "snapshot.pickle").exists() + + def test_profiler_never_started(self, profiler_base_cfg, temp_dir): + cfg = profiler_base_cfg | DictDefault( + output_dir=temp_dir, + max_steps=5, + profiler_steps=3, + profiler_steps_start=6, + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + assert not (Path(temp_dir) / "snapshot.pickle").exists() From 5cc16040a800aa2bc81dd7a58770e8dd30ec8ed3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 20:11:33 -0400 Subject: [PATCH 0807/1405] move the plugin post trainer create to the setup trainer (#2907) * move the plugin post trainer create to the setup trainer * move post-train plugins to execute-training fn --- src/axolotl/train.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 35c58501c5..9671799035 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -224,6 +224,9 @@ def execute_training( # torch.set_default_dtype(torch.bfloat16) trainer.train(resume_from_checkpoint=resume_from_checkpoint) + plugin_manager = PluginManager.get_instance() + plugin_manager.post_train(cfg, trainer.model) + def save_trained_model( cfg: DictDefault, @@ -510,6 +513,9 @@ def setup_model_and_trainer(cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> peft_config=peft_config, ) + plugin_manager = PluginManager.get_instance() + plugin_manager.post_trainer_create(cfg, trainer) + return ( trainer, model, @@ -541,9 +547,6 @@ def train( processor, ) = setup_model_and_trainer(cfg, dataset_meta) - plugin_manager = PluginManager.get_instance() - plugin_manager.post_trainer_create(cfg, trainer) - # Handle untrained tokens if configured safe_serialization = cfg.save_safetensors is True train_dataset = dataset_meta.train_dataset @@ -566,6 +569,4 @@ def train( if not cfg.use_ray: cleanup_distributed() - plugin_manager.post_train(cfg, model) - return model, tokenizer, trainer From cd079b5536cbfc86e50c73d9196a131dcf504d8c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 14 Jul 2025 21:33:48 -0400 Subject: [PATCH 0808/1405] Tensor parallel w DeepSpeed AutoTP (#2574) * support for deepspeed autotup * bump to latest deepspeed that supports deepcompile too * add deepcompile support too * fix total steps calculation for TP * setup fixture for tp * update ds config to ensure weights are gathered for checkpoint * fix duplicate validation names * chore: lint --- setup.py | 2 +- src/axolotl/utils/schemas/config.py | 13 ++++- src/axolotl/utils/schemas/validation.py | 66 ++++++++++++++++++++++++- src/axolotl/utils/trainer.py | 7 ++- tests/core/test_builders.py | 1 + 5 files changed, 85 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index ff8bd2c5c6..df9a231544 100644 --- a/setup.py +++ b/setup.py @@ -121,7 +121,7 @@ def get_package_version(): "yunchang==0.6.0", ], "deepspeed": [ - "deepspeed==0.17.1", + "deepspeed==0.17.2", "deepspeed-kernels", ], "mamba-ssm": [ diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 1726feb67f..909fd637c6 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -584,6 +584,12 @@ class AxolotlInputConfig( "description": "Deepspeed config path. e.g., deepspeed_configs/zero3.json" }, ) + deepcompile: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use deepcompile for faster training with deepspeed" + }, + ) fsdp: list[str] | None = Field( default=None, json_schema_extra={"description": "FSDP configuration"}, @@ -629,7 +635,12 @@ class AxolotlInputConfig( "description": "One of 'varlen_llama3', 'batch_ring', 'batch_zigzag', 'batch_stripe'. Defaults to 'varlen_llama3' in the sample packing case, and 'batch_ring' in the non-sample packing case." }, ) - + tensor_parallel_size: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of tensor parallel processes in TP group. Only supported with DeepSpeed AutoTP." + }, + ) special_tokens: SpecialTokensConfig | None = Field( default=None, json_schema_extra={ diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index db3fd0a1c8..56a70ec486 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1,8 +1,11 @@ """Module with validation methods for config pydantic model.""" -# pylint: disable=too-many-lines,too-many-boolean-expressions +# pylint: disable=too-many-boolean-expressions +import json import logging +import tempfile +from pathlib import Path from pydantic import ( field_validator, @@ -12,6 +15,8 @@ from axolotl.utils.schemas.enums import ChatTemplate, RingAttnFunc, RLType +# pylint: disable=too-many-lines + LOG = logging.getLogger(__name__) SUPPORTED_METRICS = {"sacrebleu", "comet", "ter", "chrf", "perplexity"} @@ -872,6 +877,59 @@ def check_fsdp_sharded_state_dict_w_safetensors(self): ) return self + @model_validator(mode="before") + @classmethod + def check_tensor_parallel_size_update_ds_json(cls, data): + tensor_parallel_size = data.get("tensor_parallel_size") + if tensor_parallel_size is not None and tensor_parallel_size > 1: + if not data.get("deepspeed"): + raise ValueError( + "Tensor parallelism (TP) is only supported with DeepSpeed" + ) + with open(data.get("deepspeed"), "r", encoding="utf-8") as ds_fin: + ds_config = json.load(ds_fin) + should_save = False + if "tensor_parallel" not in ds_config: + ds_config["tensor_parallel"] = {"autotp_size": tensor_parallel_size} + should_save = True + if ( + "gather_16bit_weights_on_model_save" + not in ds_config["zero_optimization"] + ): + ds_config["zero_optimization"][ + "gather_16bit_weights_on_model_save" + ] = True + should_save = True + if should_save: + temp_dir = tempfile.mkdtemp() + with open( + Path(temp_dir) / "autotp_ds.json", "w", encoding="utf-8" + ) as ds_fout: + json.dump(ds_config, ds_fout, indent=4) + data["deepspeed"] = str(Path(temp_dir) / "autotp_ds.json") + + return data + + @model_validator(mode="before") + @classmethod + def check_deepcompile(cls, data): + deepcompile = data.get("deepcompile") + if deepcompile: + if not data.get("deepspeed"): + raise ValueError("DeepCompile is only supported with DeepSpeed") + with open(data.get("deepspeed"), "r", encoding="utf-8") as ds_fin: + ds_config = json.load(ds_fin) + if "compile" not in ds_config: + ds_config["compile"] = {"deepcompile": True} + temp_dir = tempfile.mkdtemp() + with open( + Path(temp_dir) / "deepcompile_ds.json", "w", encoding="utf-8" + ) as ds_fout: + json.dump(ds_config, ds_fout, indent=4) + data["deepspeed"] = str(Path(temp_dir) / "deepcompile_ds.json") + + return data + class SystemValidationMixin: """Validation methods related to system and hardware configuration.""" @@ -1126,6 +1184,12 @@ def check_early_stopping(self): ) return self + @model_validator(mode="after") + def check_tensor_parallel_size(self): + if not self.tensor_parallel_size: + self.tensor_parallel_size = 1 + return self + @model_validator(mode="after") def check_sequence_parallel_degree(self): if not self.sequence_parallel_degree: diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index a512d64005..8371b2dd71 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -443,6 +443,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): ) * cfg.num_epochs * cfg.sequence_parallel_degree + * cfg.tensor_parallel_size ) LOG.debug( f"total_num_tokens: {cfg.total_num_tokens:_}, total_num_steps: {total_num_steps:_}" @@ -481,7 +482,10 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): # on the agreed on value for sample_packing_eff_est total_num_steps = int( math.floor( - data_loader_len * cfg.num_epochs * cfg.sequence_parallel_degree + data_loader_len + * cfg.num_epochs + * cfg.sequence_parallel_degree + * cfg.tensor_parallel_size ) ) if cfg.dataloader_drop_last: @@ -508,6 +512,7 @@ def calc_sample_packing_eff_est(estimates: List[float]): len(train_dataset) * cfg.num_epochs * cfg.sequence_parallel_degree + * cfg.tensor_parallel_size / cfg.batch_size ) ) diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index e66b8e009d..0053b4d277 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -65,6 +65,7 @@ def fixture_base_cfg(): "dataloader_pin_memory": True, "dataloader_prefetch_factor": 2, "sequence_parallel_degree": 1, + "tensor_parallel_size": 1, # Dtype "fp16": False, "bf16": False, From a06144654005f7c514a4537df2d7cc213eb75119 Mon Sep 17 00:00:00 2001 From: greenhestu Date: Tue, 15 Jul 2025 11:33:10 +0900 Subject: [PATCH 0809/1405] Fix: Prevents merging of tool arguments during preprocessing (#2909) --- .../prompt_strategies/chat_template.py | 16 ++++ ...est_chat_template_ds_schema_unification.py | 75 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 tests/prompt_strategies/test_chat_template_ds_schema_unification.py diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index a9d26a650e..ced8c8da66 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -379,6 +379,22 @@ def tokenize_prompt(self, prompt: dict[str, Any]): Public method that can handle either a single prompt or a batch of prompts. """ + def _remove_none_values(obj): + """ + Remove null from a dictionary-like obj or list. + These can appear due to Dataset loading causing schema merge. + See https://github.com/axolotl-ai-cloud/axolotl/pull/2909 + """ + if hasattr(obj, "items"): + return { + k: _remove_none_values(v) for k, v in obj.items() if v is not None + } + if isinstance(obj, list): + return [_remove_none_values(elem) for elem in obj] + return obj + + prompt = _remove_none_values(prompt) + if not self.is_prompt_batched(prompt) or not self.supports_batched: return self._tokenize_single_prompt(prompt) diff --git a/tests/prompt_strategies/test_chat_template_ds_schema_unification.py b/tests/prompt_strategies/test_chat_template_ds_schema_unification.py new file mode 100644 index 0000000000..502efae4bb --- /dev/null +++ b/tests/prompt_strategies/test_chat_template_ds_schema_unification.py @@ -0,0 +1,75 @@ +""" +Tests for chat template prompt strategy with schema unification for none fields +""" + +import json + +import pytest +from datasets import Dataset +from transformers import AutoTokenizer + +from axolotl.prompt_strategies.chat_template import StrategyLoader +from axolotl.utils.dict import DictDefault + + +@pytest.fixture(name="messages_w_tools") +def fixture_messages_w_tools(): + jsons = """ +{"messages":[{"role":"user","content":"move to (0, 1)"},{"role":"assistant","content":"","tool_calls":[{"function":{"name":"move","arguments":{"x":0,"y":1}}}]}],"tools":[{"type":"function","function":{"name":"move","description":"Move to a given location measured in meters","parameters":{"type":"object","properties":{"x":{"type":"number","description":"The x coordinate of the location, negative values are to the left, positive values are to the right"},"y":{"type":"number","description":"The y coordinate of the location, negative values are backward, positive values are forward"}},"required":["x","y"]}}},{"type":"function","function":{"name":"turn","description":"Turn the robot to a given direction","parameters":{"type":"object","properties":{"theta":{"type":"integer","description":"The angle to turn to, in degrees, positive values are counter-clockwise, negative values are clockwise"}},"required":["theta"]}}},{"type":"function","function":{"name":"invalid_prompt","description":"call when the user's prompt is invalid","parameters":{"type":"object","properties":{"message":{"type":"string","description":"why the prompt is invalid"}},"required":["message"]}}}],"add_generation_prompt":false} +{"messages":[{"role":"user","content":"turn 270 degree"},{"role":"assistant","content":"","tool_calls":[{"function":{"name":"turn","arguments":{"theta": 270}}}]}],"tools":[{"type":"function","function":{"name":"move","description":"Move to a given location measured in meters","parameters":{"type":"object","properties":{"x":{"type":"number","description":"The x coordinate of the location, negative values are to the left, positive values are to the right"},"y":{"type":"number","description":"The y coordinate of the location, negative values are backward, positive values are forward"}},"required":["x","y"]}}},{"type":"function","function":{"name":"turn","description":"Turn the robot to a given direction","parameters":{"type":"object","properties":{"theta":{"type":"integer","description":"The angle to turn to, in degrees, positive values are counter-clockwise, negative values are clockwise"}},"required":["theta"]}}},{"type":"function","function":{"name":"invalid_prompt","description":"call when the user's prompt is invalid","parameters":{"type":"object","properties":{"message":{"type":"string","description":"why the prompt is invalid"}},"required":["message"]}}}],"add_generation_prompt":false} +{"messages":[{"role":"user","content":"jump high"},{"role":"assistant","content":"","tool_calls":[{"function":{"name":"invalid_prompt","arguments":{"message": "jump is not a valid action"}}}]}],"tools":[{"type":"function","function":{"name":"move","description":"Move to a given location measured in meters","parameters":{"type":"object","properties":{"x":{"type":"number","description":"The x coordinate of the location, negative values are to the left, positive values are to the right"},"y":{"type":"number","description":"The y coordinate of the location, negative values are backward, positive values are forward"}},"required":["x","y"]}}},{"type":"function","function":{"name":"turn","description":"Turn the robot to a given direction","parameters":{"type":"object","properties":{"theta":{"type":"integer","description":"The angle to turn to, in degrees, positive values are counter-clockwise, negative values are clockwise"}},"required":["theta"]}}},{"type":"function","function":{"name":"invalid_prompt","description":"call when the user's prompt is invalid","parameters":{"type":"object","properties":{"message":{"type":"string","description":"why the prompt is invalid"}},"required":["message"]}}}],"add_generation_prompt":false} + """.strip().split( + "\n" + ) + rows = [json.loads(row) for row in jsons] + return Dataset.from_list(rows) + + +@pytest.fixture(name="qwen3_tokenizer") +def qwen3_tokenizer_fixture( + download_qwen3_half_billion_model, +): # pylint: disable=unused-argument + tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B") + + return tokenizer + + +@pytest.fixture(name="qwen3_prompt_strategy") +def qwen3_chat_template_strategy(qwen3_tokenizer): + cfg = DictDefault( + sequence_len=2048, + chat_template="qwen3", + eot_tokens=["<|im_end|>"], + ) + ds_cfg = DictDefault( + type="chat_template", + ) + load = StrategyLoader() + strat = load(qwen3_tokenizer, cfg, ds_cfg) + return strat + + +class TestSchemaUnification: + """ + Test class on handling null fields for tool calling + """ + + def test_schema_unification_single_prompt( + self, messages_w_tools, qwen3_prompt_strategy, qwen3_tokenizer + ): + for row in messages_w_tools: + inputs = qwen3_prompt_strategy.tokenize_prompt(row) + decoded = qwen3_tokenizer.decode(inputs["input_ids"]) + tool_call = decoded.split("")[-1].split("")[0] + assert '"message": null' not in tool_call + assert '"theta": null' not in tool_call + + def test_schema_unification_batched( + self, messages_w_tools, qwen3_prompt_strategy, qwen3_tokenizer + ): + rows = messages_w_tools.map(qwen3_prompt_strategy.tokenize_prompt, batched=True) + for row in rows: + decoded = qwen3_tokenizer.decode(row["input_ids"]) + tool_call = decoded.split("")[-1].split("")[0] + assert '"message": null' not in tool_call + assert '"theta": null' not in tool_call From 354eaaf0d3f5d7675699ccb90a982e8820aacb6f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 15 Jul 2025 09:33:35 +0700 Subject: [PATCH 0810/1405] feat: add call method to mistral tokenizer wrapper (#2898) --- src/axolotl/utils/mistral_tokenizer.py | 128 ++++++++++++++++++ .../test_chat_templates_mistral.py | 97 +++++++++++++ 2 files changed, 225 insertions(+) diff --git a/src/axolotl/utils/mistral_tokenizer.py b/src/axolotl/utils/mistral_tokenizer.py index 95c87a8226..33c08db465 100644 --- a/src/axolotl/utils/mistral_tokenizer.py +++ b/src/axolotl/utils/mistral_tokenizer.py @@ -497,3 +497,131 @@ def convert_ids_to_tokens(self, ids: list[int]) -> list[str]: return [ self._mistral.instruct_tokenizer.tokenizer.id_to_piece(id) for id in ids ] + + def __call__( + self, + text: str | list[str], + add_special_tokens: bool = True, + padding: bool | str = False, + truncation: bool = False, + max_length: int | None = None, + return_tensors: str | None = None, + **kwargs, + ) -> dict[str, list[int] | np.ndarray | Tensor]: + """ + Tokenize text and return a dictionary with input_ids and attention_mask. + + Args: + text: Input text string or list of strings to tokenize. + add_special_tokens: Whether to add special tokens (BOS/EOS). + padding: Whether to pad sequences. Can be True, False, "longest", or "max_length". + truncation: Whether to truncate sequences to max_length. + max_length: Maximum sequence length for truncation/padding. + return_tensors: Return format ("pt" for PyTorch, "np" for NumPy, None for lists). + + Returns: + Dictionary with "input_ids" and "attention_mask" keys. + """ + # if kwargs passed, raise error + if kwargs: + raise ValueError( + f"Unsupported kwargs: {kwargs}. Please create an issue on GitHub." + ) + + # `np` can work with inhomogeneous shapes but let's not support it until needed. + if ( + isinstance(text, list) + and len(text) > 1 + and return_tensors in ("pt", "np") + and padding is False + and truncation is False + ): + raise ValueError( + "return_tensors='pt' or 'np' requires padding or truncation." + ) + + # Handle single string input + if isinstance(text, str): + text = [text] + + # Encode all texts + # TODO: figure out how to parallelize this + batch_input_ids = [] + for single_text in text: + input_ids = self.encode(single_text, add_special_tokens=add_special_tokens) + + # Handle truncation + if truncation and max_length is not None and len(input_ids) > max_length: + input_ids = input_ids[:max_length] + + batch_input_ids.append(input_ids) + + # Create attention masks (1 for real tokens, 0 for padding) + attention_masks = [[1] * len(input_ids) for input_ids in batch_input_ids] + + # Handle padding + if padding in (True, "longest"): + # Pad to longest sequence in batch + max_len = max(len(input_ids) for input_ids in batch_input_ids) + + for i, input_ids in enumerate(batch_input_ids): + pad_length = max_len - len(input_ids) + if pad_length > 0: + if self.padding_side == "right": + batch_input_ids[i] = ( + input_ids + [self.pad_token_id] * pad_length + ) + attention_masks[i] = attention_masks[i] + [0] * pad_length + else: # left padding + batch_input_ids[i] = [ + self.pad_token_id + ] * pad_length + input_ids + attention_masks[i] = [0] * pad_length + attention_masks[i] + + elif padding == "max_length": + if max_length is None: + raise ValueError( + "max_length must be specified when padding='max_length'" + ) + + for i, input_ids in enumerate(batch_input_ids): + pad_length = max_length - len(input_ids) + if pad_length > 0: + if self.padding_side == "right": + batch_input_ids[i] = ( + input_ids + [self.pad_token_id] * pad_length + ) + attention_masks[i] = attention_masks[i] + [0] * pad_length + else: # left padding + batch_input_ids[i] = [ + self.pad_token_id + ] * pad_length + input_ids + attention_masks[i] = [0] * pad_length + attention_masks[i] + + # Prepare result + result = {} + + # Handle return tensor format + if return_tensors == "pt": + import torch + + result["input_ids"] = torch.tensor(batch_input_ids, dtype=torch.long) + result["attention_mask"] = torch.tensor(attention_masks, dtype=torch.long) + elif return_tensors == "np": + result["input_ids"] = np.array(batch_input_ids, dtype=np.int64) + result["attention_mask"] = np.array(attention_masks, dtype=np.int64) + elif return_tensors is None: + result["input_ids"] = batch_input_ids + result["attention_mask"] = attention_masks + else: + raise ValueError( + f"Unsupported return_tensors='{return_tensors}'. " + "Only 'pt' and 'np' are supported." + ) + + # If single input, return single sequences (not batched) + if len(text) == 1 and return_tensors is None: + result["input_ids"] = result["input_ids"][0] + result["attention_mask"] = result["attention_mask"][0] + + return result diff --git a/tests/prompt_strategies/test_chat_templates_mistral.py b/tests/prompt_strategies/test_chat_templates_mistral.py index f26ed08387..8e3f494b16 100644 --- a/tests/prompt_strategies/test_chat_templates_mistral.py +++ b/tests/prompt_strategies/test_chat_templates_mistral.py @@ -6,6 +6,8 @@ import pytest if TYPE_CHECKING: + from transformers import PreTrainedTokenizer + from axolotl.utils.mistral_tokenizer import HFMistralTokenizer @@ -748,5 +750,100 @@ def test_magistral_tool_calling(magistral_tokenizer: "HFMistralTokenizer"): assert "Not the same number of function calls and responses" in str(e) +def test_magistral_tokenizer_call_method( + magistral_tokenizer: "HFMistralTokenizer", llama3_tokenizer: "PreTrainedTokenizer" +): + """Test the __call__ method behavior matches HuggingFace standards""" + from copy import deepcopy + + import numpy as np + import torch + + hf_tokenizer = deepcopy(llama3_tokenizer) + hf_tokenizer.pad_token = hf_tokenizer.eos_token + + test_text = "Hello, how are you?" + batch_texts = ["Hello world", "How are you?"] + + # Test single string with return_tensors=None + hf_result: dict[str, list[int]] = hf_tokenizer(test_text, return_tensors=None) + mistral_result: dict[str, list[int]] = magistral_tokenizer( + test_text, return_tensors=None + ) + + assert isinstance(mistral_result, dict) + assert set(mistral_result.keys()) == {"input_ids", "attention_mask"} + assert isinstance(mistral_result["input_ids"], type(hf_result["input_ids"])) # list + assert isinstance( + mistral_result["attention_mask"], type(hf_result["attention_mask"]) + ) + assert len(mistral_result["input_ids"]) == len(mistral_result["attention_mask"]) + assert np.all(mistral_result["attention_mask"]) + assert len(np.array(mistral_result["input_ids"]).shape) == 1 # 1D array + + # Test single string with return_tensors='pt' + hf_result_pt: dict[str, torch.Tensor] = hf_tokenizer(test_text, return_tensors="pt") + mistral_result_pt: dict[str, torch.Tensor] = magistral_tokenizer( + test_text, return_tensors="pt" + ) + + # Check structure and types + assert isinstance(mistral_result_pt["input_ids"], torch.Tensor) + assert isinstance(mistral_result_pt["attention_mask"], torch.Tensor) + + # Check shapes match (don't compare token dimension) + assert len(hf_result_pt["input_ids"].shape) == len( + mistral_result_pt["input_ids"].shape + ) + assert hf_result_pt["input_ids"].shape[0] == mistral_result_pt["input_ids"].shape[0] + assert ( + mistral_result_pt["attention_mask"].shape + == mistral_result_pt["input_ids"].shape + ) + assert torch.all(mistral_result_pt["attention_mask"] == 1) + + # Test batch input with padding + hf_batch: dict[str, torch.Tensor] = hf_tokenizer( + batch_texts, return_tensors="pt", padding=True + ) + mistral_batch: dict[str, torch.Tensor] = magistral_tokenizer( + batch_texts, return_tensors="pt", padding=True + ) + + # Check batch behavior + assert len(hf_batch["input_ids"].shape) == len(mistral_batch["input_ids"].shape) + assert hf_batch["input_ids"].shape[0] == mistral_batch["input_ids"].shape[0] + assert mistral_batch["attention_mask"].shape == mistral_batch["input_ids"].shape + assert torch.any( + mistral_batch["attention_mask"][0] == 0 + ) # padding in shorter sequence + assert torch.all( + mistral_batch["attention_mask"][1] == 1 + ) # no padding in longer sequence + + # Test numpy tensors + mistral_result_np: dict[str, np.ndarray] = magistral_tokenizer( + test_text, return_tensors="np" + ) + assert isinstance(mistral_result_np["input_ids"], np.ndarray) + assert isinstance(mistral_result_np["attention_mask"], np.ndarray) + + # Test consistency with encode() + encoded: list[int] = magistral_tokenizer.encode(test_text, add_special_tokens=True) + called: dict[str, torch.Tensor] = magistral_tokenizer( + test_text, return_tensors="pt" + ) + assert encoded == called["input_ids"][0].tolist() + + # Test Error handling + with pytest.raises(ValueError, match="Unsupported kwargs"): + magistral_tokenizer(test_text, unsupported_param=True) + + with pytest.raises( + ValueError, match="return_tensors='pt' or 'np' requires padding or truncation" + ): + magistral_tokenizer(batch_texts, return_tensors="pt") + + if __name__ == "__main__": unittest.main() From d320ef619988d6cf5151d5a9da88979dc7f91bfe Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 15 Jul 2025 11:28:41 -0400 Subject: [PATCH 0811/1405] fix for upstream refactor of KwargsForCausalLM (#2911) --- src/axolotl/integrations/kd/kernels/models.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/axolotl/integrations/kd/kernels/models.py b/src/axolotl/integrations/kd/kernels/models.py index 5a7c286bc4..6a8b6da1c9 100644 --- a/src/axolotl/integrations/kd/kernels/models.py +++ b/src/axolotl/integrations/kd/kernels/models.py @@ -6,15 +6,21 @@ import torch from transformers import Cache -from transformers.modeling_flash_attention_utils import FlashAttentionKwargs from transformers.modeling_outputs import CausalLMOutputWithPast -from transformers.utils import LossKwargs +try: + from transformers.modeling_flash_attention_utils import FlashAttentionKwargs + from transformers.utils import LossKwargs -class KwargsForCausalLM(FlashAttentionKwargs, LossKwargs): - """ - placeholder kwargs for hf model classes - """ + class TransformersKwargs(FlashAttentionKwargs, LossKwargs): + """ + placeholder kwargs for hf model classes + """ + +except ImportError: + from transformers.utils.generic import ( # type: ignore[no-redef] + TransformersKwargs, + ) def kldiv_forward_llama_like( @@ -33,7 +39,7 @@ def kldiv_forward_llama_like( output_hidden_states: Optional[bool] = None, cache_position: Optional[torch.LongTensor] = None, logits_to_keep: Union[int, torch.Tensor] = 0, # pylint: disable=unused-argument - **kwargs: Unpack[KwargsForCausalLM], # type: ignore[misc] + **kwargs: Unpack[TransformersKwargs], # type: ignore[misc] ) -> CausalLMOutputWithPast: # pylint: disable=duplicate-code output_attentions = ( From 10ba1622f77471a68968db1fcc524ed58940269e Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Tue, 15 Jul 2025 15:00:48 -0400 Subject: [PATCH 0812/1405] checkpoint model on first step callback (#2906) * checkpoint model on first step callback * remove debug * add test cases; update existing tests not to save on first step * move test out of solo * delete * default to False * typo --- examples/cloud/modal.yaml | 2 + examples/cohere/command-r-7b-qlora.yml | 3 +- .../cogito-v1-preview-llama-3B-lora.yml | 2 + .../cogito-v1-preview-qwen-14B-lora.yml | 2 + examples/deepseek-v2/fft-fsdp-16b.yaml | 2 + examples/deepseek-v2/qlora-fsdp-2_5.yaml | 2 + examples/devstral/devstral-small-qlora.yml | 2 + .../falcon-h1/falcon-h1-1b-deep-qlora.yaml | 2 + examples/falcon-h1/falcon-h1-1b-qlora.yaml | 3 +- examples/falcon-h1/falcon-h1-34b-qlora.yaml | 2 + examples/falcon-h1/falcon-h1-3b-qlora.yaml | 2 + examples/falcon-h1/falcon-h1-500m-qlora.yaml | 2 + examples/falcon-h1/falcon-h1-7b-qlora.yaml | 2 + examples/gemma2/qlora.yml | 2 + examples/gemma2/reward-model.yaml | 2 + examples/gemma3/gemma-3-1b-qlora.yml | 2 + examples/gemma3/gemma-3-4b-qlora.yml | 2 + examples/gemma3/gemma-3-4b-vision-qlora.yml | 2 + examples/glm4/qlora-32b.yaml | 2 + examples/jamba/qlora.yaml | 2 + examples/jamba/qlora_deepspeed.yaml | 2 + examples/jamba/qlora_fsdp_large.yaml | 2 + examples/lfm2/lfm2-350m-fft.yaml | 2 + examples/llama-2/fft_optimized.yml | 2 + examples/llama-2/gptq-lora.yml | 2 + examples/llama-2/lisa.yml | 2 + examples/llama-2/loftq.yml | 2 + examples/llama-2/lora.yml | 2 + examples/llama-2/qlora-fsdp.yml | 2 + examples/llama-2/qlora.yml | 2 + examples/llama-2/relora.yml | 2 + examples/llama-3-vision/lora-11b.yaml | 2 + examples/llama-3/3b-qat-fsdp2.yaml | 2 + examples/llama-3/fft-8b-liger-fsdp.yaml | 2 + examples/llama-3/fft-8b.yaml | 2 + examples/llama-3/instruct-dpo-lora-8b.yml | 2 + examples/llama-3/instruct-lora-8b.yml | 2 + examples/llama-3/lora-1b-deduplicate-dpo.yml | 2 + examples/llama-3/lora-1b-deduplicate-sft.yml | 2 + examples/llama-3/lora-1b-kernels.yml | 2 + examples/llama-3/lora-1b-ray.yml | 2 + .../lora-1b-sample-packing-sequentially.yml | 2 + examples/llama-3/lora-1b.yml | 2 + examples/llama-3/lora-8b.yml | 2 + examples/llama-3/qlora-1b-kto.yaml | 2 + examples/llama-3/qlora-1b.yml | 2 + examples/llama-3/qlora-fsdp-405b.yaml | 2 + examples/llama-3/qlora-fsdp-70b.yaml | 2 + examples/llama-3/qlora.yml | 2 + examples/llama-3/sparse-finetuning.yaml | 2 + .../do-no-use-fa2/maverick-qlora-fsdp1.yaml | 2 + .../do-no-use-fa2/scout-qlora-fsdp1.yaml | 2 + .../scout-qlora-single-h100.yaml | 2 + .../scout-vision-qlora-fsdp.yaml | 2 + .../llama-4/scout-qlora-flexattn-fsdp2.yaml | 2 + .../llama-4/scout-qlora-single-h100-flex.yaml | 2 + .../scout-vision-qlora-fsdp2-flex.yaml | 2 + examples/llava/lora-7b.yaml | 2 + .../magistral/magistral-small-fsdp-qlora.yaml | 2 + examples/magistral/magistral-small-qlora.yaml | 2 + examples/mamba/config.yml | 2 + examples/mistral/bigstral-ds-zero3.yaml | 2 + examples/mistral/config.yml | 2 + examples/mistral/lora-mps.yml | 2 + examples/mistral/lora.yml | 2 + examples/mistral/mistral-dpo-qlora.yml | 2 + examples/mistral/mistral-qlora-fsdp.yml | 2 + examples/mistral/mistral-qlora-orpo.yml | 2 + .../mistral/mistral-small-3.1-24B-lora.yml | 2 + examples/mistral/mixtral-8x22b-qlora-fsdp.yml | 2 + examples/mistral/mixtral-qlora-fsdp.yml | 2 + examples/mistral/mixtral.yml | 2 + examples/mistral/mixtral_22.yml | 2 + examples/mistral/qlora.yml | 2 + examples/orpheus/finetune.yml | 2 + examples/phi/lora-3.5.yaml | 2 + examples/phi/phi-ft.yml | 2 + examples/phi/phi-qlora.yml | 2 + examples/phi/phi2-ft.yml | 2 + examples/phi/phi3-ft-fsdp.yml | 2 + examples/phi/phi3-ft.yml | 2 + examples/pixtral/lora-12b.yml | 2 + examples/qwen2-vl/lora-7b.yaml | 2 + examples/qwen2/dpo.yaml | 2 + examples/qwen2/prm.yaml | 2 + examples/qwen2/qlora-fsdp.yaml | 2 + examples/qwen2/reward-model.yaml | 3 +- examples/qwen2_5-vl/lora-7b.yaml | 2 + examples/qwen3/32b-qlora.yaml | 2 + examples/qwen3/8b-qat-fsdp2.yml | 2 + examples/qwen3/qlora-fsdp.yaml | 2 + src/axolotl/core/builders/base.py | 3 + src/axolotl/utils/callbacks/__init__.py | 26 +++-- src/axolotl/utils/schemas/config.py | 8 ++ .../integrations/test_cut_cross_entropy.py | 2 + tests/e2e/integrations/test_hooks.py | 1 + tests/e2e/integrations/test_kd.py | 1 + tests/e2e/integrations/test_liger.py | 2 + tests/e2e/integrations/test_llm_compressor.py | 1 + tests/e2e/multigpu/patched/test_sp.py | 1 + tests/e2e/multigpu/solo/test_flex.py | 1 + tests/e2e/multigpu/solo/test_grpo.py | 3 + tests/e2e/multigpu/test_eval.py | 2 + tests/e2e/multigpu/test_gemma3.py | 1 + tests/e2e/multigpu/test_llama.py | 12 +++ tests/e2e/multigpu/test_ray.py | 2 + tests/e2e/patched/test_4d_multipack_llama.py | 2 + .../patched/test_activation_checkpointing.py | 1 + tests/e2e/patched/test_fa_xentropy.py | 1 + tests/e2e/patched/test_falcon_samplepack.py | 2 + tests/e2e/patched/test_flattening.py | 1 + tests/e2e/patched/test_fused_llama.py | 1 + tests/e2e/patched/test_llama_s2_attention.py | 2 + .../e2e/patched/test_lora_llama_multipack.py | 2 + tests/e2e/patched/test_mistral_samplepack.py | 2 + tests/e2e/patched/test_mixtral_samplepack.py | 2 + tests/e2e/patched/test_model_patches.py | 2 + tests/e2e/patched/test_peft_embeddings.py | 1 + tests/e2e/patched/test_phi_multipack.py | 2 + tests/e2e/patched/test_resume.py | 1 + tests/e2e/patched/test_sp.py | 1 + tests/e2e/patched/test_unsloth_qlora.py | 3 + tests/e2e/solo/test_flex.py | 1 + tests/e2e/solo/test_relora_llama.py | 1 + tests/e2e/test_deepseekv3.py | 2 + tests/e2e/test_dpo.py | 7 ++ tests/e2e/test_embeddings_lr.py | 2 + tests/e2e/test_evaluate.py | 1 + tests/e2e/test_falcon.py | 3 + tests/e2e/test_gemma3_text.py | 2 + tests/e2e/test_llama.py | 4 + tests/e2e/test_llama_pretrain.py | 1 + tests/e2e/test_llama_vision.py | 2 + tests/e2e/test_lora_llama.py | 1 + tests/e2e/test_mamba.py | 1 + tests/e2e/test_mistral.py | 2 + tests/e2e/test_mixtral.py | 5 + tests/e2e/test_optimizers.py | 5 + tests/e2e/test_packing_loss.py | 1 + tests/e2e/test_phi.py | 2 + .../e2e/test_process_reward_model_smollm2.py | 1 + tests/e2e/test_qat.py | 2 + tests/e2e/test_qwen.py | 1 + tests/e2e/test_reward_model_smollm2.py | 1 + tests/e2e/test_save_first_step.py | 102 ++++++++++++++++++ tests/e2e/test_schedulers.py | 1 + 146 files changed, 419 insertions(+), 9 deletions(-) create mode 100644 tests/e2e/test_save_first_step.py diff --git a/examples/cloud/modal.yaml b/examples/cloud/modal.yaml index 1950314948..bbe8785f16 100644 --- a/examples/cloud/modal.yaml +++ b/examples/cloud/modal.yaml @@ -26,3 +26,5 @@ timeout: 86400 # Preprocess specific configurations memory_preprocess: 32 timeout_preprocess: 14400 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/cohere/command-r-7b-qlora.yml b/examples/cohere/command-r-7b-qlora.yml index 4a30e9a776..da2777270e 100644 --- a/examples/cohere/command-r-7b-qlora.yml +++ b/examples/cohere/command-r-7b-qlora.yml @@ -35,7 +35,6 @@ wandb_watch: wandb_name: wandb_log_model: - gradient_accumulation_steps: 4 micro_batch_size: 1 num_epochs: 4 @@ -56,3 +55,5 @@ evals_per_epoch: saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml b/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml index 2c0495ceda..1a051b98bd 100644 --- a/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml +++ b/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml @@ -56,3 +56,5 @@ evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml b/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml index de9c956e0c..8073426412 100644 --- a/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml +++ b/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml @@ -56,3 +56,5 @@ evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/deepseek-v2/fft-fsdp-16b.yaml b/examples/deepseek-v2/fft-fsdp-16b.yaml index 0ed97db369..78bf6b1797 100644 --- a/examples/deepseek-v2/fft-fsdp-16b.yaml +++ b/examples/deepseek-v2/fft-fsdp-16b.yaml @@ -55,3 +55,5 @@ fsdp_config: fsdp_transformer_layer_cls_to_wrap: DeepseekV2DecoderLayer fsdp_state_dict_type: FULL_STATE_DICT fsdp_sharding_strategy: FULL_SHARD + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/deepseek-v2/qlora-fsdp-2_5.yaml b/examples/deepseek-v2/qlora-fsdp-2_5.yaml index 34dbeaafed..da1d9aefd5 100644 --- a/examples/deepseek-v2/qlora-fsdp-2_5.yaml +++ b/examples/deepseek-v2/qlora-fsdp-2_5.yaml @@ -79,3 +79,5 @@ fsdp_config: fsdp_transformer_layer_cls_to_wrap: DeepseekV2DecoderLayer fsdp_state_dict_type: FULL_STATE_DICT fsdp_sharding_strategy: FULL_SHARD + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/devstral/devstral-small-qlora.yml b/examples/devstral/devstral-small-qlora.yml index dc0051bd5e..9d92e8662f 100644 --- a/examples/devstral/devstral-small-qlora.yml +++ b/examples/devstral/devstral-small-qlora.yml @@ -62,3 +62,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml b/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml index 1dd901154a..484c31fecc 100644 --- a/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml @@ -69,3 +69,5 @@ evals_per_epoch: saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/falcon-h1/falcon-h1-1b-qlora.yaml b/examples/falcon-h1/falcon-h1-1b-qlora.yaml index 24dc7cae37..dea2a6e6d8 100644 --- a/examples/falcon-h1/falcon-h1-1b-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-1b-qlora.yaml @@ -46,7 +46,6 @@ wandb_watch: wandb_name: wandb_log_model: - gradient_accumulation_steps: 4 micro_batch_size: 1 num_epochs: 4 @@ -69,3 +68,5 @@ evals_per_epoch: saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/falcon-h1/falcon-h1-34b-qlora.yaml b/examples/falcon-h1/falcon-h1-34b-qlora.yaml index 43eb1967ba..b187efbf6e 100644 --- a/examples/falcon-h1/falcon-h1-34b-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-34b-qlora.yaml @@ -69,3 +69,5 @@ evals_per_epoch: saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/falcon-h1/falcon-h1-3b-qlora.yaml b/examples/falcon-h1/falcon-h1-3b-qlora.yaml index 00929bbf01..4d981ad95f 100644 --- a/examples/falcon-h1/falcon-h1-3b-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-3b-qlora.yaml @@ -69,3 +69,5 @@ evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/falcon-h1/falcon-h1-500m-qlora.yaml b/examples/falcon-h1/falcon-h1-500m-qlora.yaml index e2640de7bc..5ee13facd3 100644 --- a/examples/falcon-h1/falcon-h1-500m-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-500m-qlora.yaml @@ -69,3 +69,5 @@ evals_per_epoch: saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/falcon-h1/falcon-h1-7b-qlora.yaml b/examples/falcon-h1/falcon-h1-7b-qlora.yaml index 183e423b51..4b665c3cd9 100644 --- a/examples/falcon-h1/falcon-h1-7b-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-7b-qlora.yaml @@ -69,3 +69,5 @@ evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/gemma2/qlora.yml b/examples/gemma2/qlora.yml index cb96a32c1d..68d213fada 100644 --- a/examples/gemma2/qlora.yml +++ b/examples/gemma2/qlora.yml @@ -60,3 +60,5 @@ evals_per_epoch: saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/gemma2/reward-model.yaml b/examples/gemma2/reward-model.yaml index ce01a4572e..624ebdcd22 100644 --- a/examples/gemma2/reward-model.yaml +++ b/examples/gemma2/reward-model.yaml @@ -50,3 +50,5 @@ evals_per_epoch: saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/gemma3/gemma-3-1b-qlora.yml b/examples/gemma3/gemma-3-1b-qlora.yml index 217c887aa6..99921770db 100644 --- a/examples/gemma3/gemma-3-1b-qlora.yml +++ b/examples/gemma3/gemma-3-1b-qlora.yml @@ -66,3 +66,5 @@ evals_per_epoch: saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml index d78559ae3b..025cb9240f 100644 --- a/examples/gemma3/gemma-3-4b-qlora.yml +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -60,3 +60,5 @@ warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/gemma3/gemma-3-4b-vision-qlora.yml b/examples/gemma3/gemma-3-4b-vision-qlora.yml index 183eb88e84..e9e606b69f 100644 --- a/examples/gemma3/gemma-3-4b-vision-qlora.yml +++ b/examples/gemma3/gemma-3-4b-vision-qlora.yml @@ -62,3 +62,5 @@ warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/glm4/qlora-32b.yaml b/examples/glm4/qlora-32b.yaml index 86d9b43f8b..8973cedd4b 100644 --- a/examples/glm4/qlora-32b.yaml +++ b/examples/glm4/qlora-32b.yaml @@ -60,3 +60,5 @@ evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/jamba/qlora.yaml b/examples/jamba/qlora.yaml index 2cb0eea411..494154886b 100644 --- a/examples/jamba/qlora.yaml +++ b/examples/jamba/qlora.yaml @@ -54,3 +54,5 @@ evals_per_epoch: saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/jamba/qlora_deepspeed.yaml b/examples/jamba/qlora_deepspeed.yaml index d13ce64839..64db8f2ff7 100644 --- a/examples/jamba/qlora_deepspeed.yaml +++ b/examples/jamba/qlora_deepspeed.yaml @@ -55,3 +55,5 @@ saves_per_epoch: 1 deepspeed: deepspeed_configs/zero2.json weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/jamba/qlora_fsdp_large.yaml b/examples/jamba/qlora_fsdp_large.yaml index 6badaba19b..fda30e2d2f 100644 --- a/examples/jamba/qlora_fsdp_large.yaml +++ b/examples/jamba/qlora_fsdp_large.yaml @@ -64,3 +64,5 @@ fsdp_config: fsdp_transformer_layer_cls_to_wrap: JambaAttentionDecoderLayer,JambaMambaDecoderLayer fsdp_state_dict_type: FULL_STATE_DICT fsdp_sharding_strategy: FULL_SHARD + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/lfm2/lfm2-350m-fft.yaml b/examples/lfm2/lfm2-350m-fft.yaml index 95961557e3..74c90c1e1e 100644 --- a/examples/lfm2/lfm2-350m-fft.yaml +++ b/examples/lfm2/lfm2-350m-fft.yaml @@ -46,3 +46,5 @@ evals_per_epoch: 2 saves_per_epoch: 1 weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-2/fft_optimized.yml b/examples/llama-2/fft_optimized.yml index 86b1b6a218..c44cd22307 100644 --- a/examples/llama-2/fft_optimized.yml +++ b/examples/llama-2/fft_optimized.yml @@ -55,3 +55,5 @@ saves_per_epoch: 1 deepspeed: #deepspeed_configs/zero2.json # multi-gpu only weight_decay: 0.1 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-2/gptq-lora.yml b/examples/llama-2/gptq-lora.yml index 0f1b34016c..580fabdf8d 100644 --- a/examples/llama-2/gptq-lora.yml +++ b/examples/llama-2/gptq-lora.yml @@ -64,3 +64,5 @@ special_tokens: bos_token: "" eos_token: "" unk_token: "" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-2/lisa.yml b/examples/llama-2/lisa.yml index a76a792aef..a44e261beb 100644 --- a/examples/llama-2/lisa.yml +++ b/examples/llama-2/lisa.yml @@ -60,3 +60,5 @@ special_tokens: bos_token: "" eos_token: "" unk_token: "" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-2/loftq.yml b/examples/llama-2/loftq.yml index 22dbf2d992..085627f63b 100644 --- a/examples/llama-2/loftq.yml +++ b/examples/llama-2/loftq.yml @@ -52,3 +52,5 @@ evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-2/lora.yml b/examples/llama-2/lora.yml index 679aed3a99..759fce0441 100644 --- a/examples/llama-2/lora.yml +++ b/examples/llama-2/lora.yml @@ -52,3 +52,5 @@ evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-2/qlora-fsdp.yml b/examples/llama-2/qlora-fsdp.yml index a42eabd4b7..3bf30120bb 100644 --- a/examples/llama-2/qlora-fsdp.yml +++ b/examples/llama-2/qlora-fsdp.yml @@ -67,3 +67,5 @@ fsdp_config: fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer fsdp_state_dict_type: FULL_STATE_DICT special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-2/qlora.yml b/examples/llama-2/qlora.yml index de65928bc8..09596c71e3 100644 --- a/examples/llama-2/qlora.yml +++ b/examples/llama-2/qlora.yml @@ -53,3 +53,5 @@ evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-2/relora.yml b/examples/llama-2/relora.yml index e0a5f7068c..ca8b14a1cc 100644 --- a/examples/llama-2/relora.yml +++ b/examples/llama-2/relora.yml @@ -58,3 +58,5 @@ special_tokens: bos_token: "" eos_token: "" unk_token: "" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3-vision/lora-11b.yaml b/examples/llama-3-vision/lora-11b.yaml index 2b0ae2c70b..64d749b5a9 100644 --- a/examples/llama-3-vision/lora-11b.yaml +++ b/examples/llama-3-vision/lora-11b.yaml @@ -57,3 +57,5 @@ warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/3b-qat-fsdp2.yaml b/examples/llama-3/3b-qat-fsdp2.yaml index 5d979c96c2..08d8ee5c13 100644 --- a/examples/llama-3/3b-qat-fsdp2.yaml +++ b/examples/llama-3/3b-qat-fsdp2.yaml @@ -77,3 +77,5 @@ fsdp_config: special_tokens: pad_token: <|end_of_text|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml index eccfa6d8c1..e2808935f6 100644 --- a/examples/llama-3/fft-8b-liger-fsdp.yaml +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -72,3 +72,5 @@ fsdp_config: special_tokens: pad_token: <|finetune_right_pad_id|> eos_token: <|eot_id|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/fft-8b.yaml b/examples/llama-3/fft-8b.yaml index fdae3e6c4d..2dfe6d492d 100644 --- a/examples/llama-3/fft-8b.yaml +++ b/examples/llama-3/fft-8b.yaml @@ -42,3 +42,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: pad_token: <|end_of_text|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/instruct-dpo-lora-8b.yml b/examples/llama-3/instruct-dpo-lora-8b.yml index 51f1c768b1..10ab2a320c 100644 --- a/examples/llama-3/instruct-dpo-lora-8b.yml +++ b/examples/llama-3/instruct-dpo-lora-8b.yml @@ -71,3 +71,5 @@ warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml index acab862f64..83b7f9a37c 100644 --- a/examples/llama-3/instruct-lora-8b.yml +++ b/examples/llama-3/instruct-lora-8b.yml @@ -64,3 +64,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: pad_token: <|end_of_text|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/lora-1b-deduplicate-dpo.yml b/examples/llama-3/lora-1b-deduplicate-dpo.yml index 10e9747cb1..b20dbad844 100644 --- a/examples/llama-3/lora-1b-deduplicate-dpo.yml +++ b/examples/llama-3/lora-1b-deduplicate-dpo.yml @@ -83,3 +83,5 @@ warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/lora-1b-deduplicate-sft.yml b/examples/llama-3/lora-1b-deduplicate-sft.yml index 630ec92f6a..67e518184b 100644 --- a/examples/llama-3/lora-1b-deduplicate-sft.yml +++ b/examples/llama-3/lora-1b-deduplicate-sft.yml @@ -61,3 +61,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: pad_token: <|end_of_text|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/lora-1b-kernels.yml b/examples/llama-3/lora-1b-kernels.yml index a2d07ca491..92a948c2e6 100644 --- a/examples/llama-3/lora-1b-kernels.yml +++ b/examples/llama-3/lora-1b-kernels.yml @@ -65,3 +65,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: pad_token: "<|end_of_text|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/lora-1b-ray.yml b/examples/llama-3/lora-1b-ray.yml index bb23164ebb..178a1fb89c 100644 --- a/examples/llama-3/lora-1b-ray.yml +++ b/examples/llama-3/lora-1b-ray.yml @@ -64,3 +64,5 @@ special_tokens: use_ray: true ray_num_workers: 4 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/lora-1b-sample-packing-sequentially.yml b/examples/llama-3/lora-1b-sample-packing-sequentially.yml index 769dd32e60..c4ce3eb0fd 100644 --- a/examples/llama-3/lora-1b-sample-packing-sequentially.yml +++ b/examples/llama-3/lora-1b-sample-packing-sequentially.yml @@ -63,3 +63,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: pad_token: <|end_of_text|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/lora-1b.yml b/examples/llama-3/lora-1b.yml index acc17e21f2..82085483f1 100644 --- a/examples/llama-3/lora-1b.yml +++ b/examples/llama-3/lora-1b.yml @@ -60,3 +60,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: pad_token: "<|end_of_text|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/lora-8b.yml b/examples/llama-3/lora-8b.yml index ad50cd38a3..c393897557 100644 --- a/examples/llama-3/lora-8b.yml +++ b/examples/llama-3/lora-8b.yml @@ -57,3 +57,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: pad_token: <|end_of_text|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/qlora-1b-kto.yaml b/examples/llama-3/qlora-1b-kto.yaml index 89a51ea68f..f156e23d36 100644 --- a/examples/llama-3/qlora-1b-kto.yaml +++ b/examples/llama-3/qlora-1b-kto.yaml @@ -61,3 +61,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: pad_token: "<|end_of_text|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/qlora-1b.yml b/examples/llama-3/qlora-1b.yml index 5c8fe66289..6b76ea8d9d 100644 --- a/examples/llama-3/qlora-1b.yml +++ b/examples/llama-3/qlora-1b.yml @@ -62,3 +62,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: pad_token: "<|end_of_text|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/qlora-fsdp-405b.yaml b/examples/llama-3/qlora-fsdp-405b.yaml index 2b7d51925c..1ee922b59b 100644 --- a/examples/llama-3/qlora-fsdp-405b.yaml +++ b/examples/llama-3/qlora-fsdp-405b.yaml @@ -60,3 +60,5 @@ fsdp_config: fsdp_sharding_strategy: FULL_SHARD special_tokens: pad_token: <|finetune_right_pad_id|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/qlora-fsdp-70b.yaml b/examples/llama-3/qlora-fsdp-70b.yaml index 412b6721ca..5edd8353ad 100644 --- a/examples/llama-3/qlora-fsdp-70b.yaml +++ b/examples/llama-3/qlora-fsdp-70b.yaml @@ -69,3 +69,5 @@ fsdp_config: fsdp_sharding_strategy: FULL_SHARD special_tokens: pad_token: <|end_of_text|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/qlora.yml b/examples/llama-3/qlora.yml index 4cc9fc3dba..a674eca279 100644 --- a/examples/llama-3/qlora.yml +++ b/examples/llama-3/qlora.yml @@ -54,3 +54,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: pad_token: "<|end_of_text|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/sparse-finetuning.yaml b/examples/llama-3/sparse-finetuning.yaml index 1bbb880289..8577a19d2f 100644 --- a/examples/llama-3/sparse-finetuning.yaml +++ b/examples/llama-3/sparse-finetuning.yaml @@ -75,3 +75,5 @@ llmcompressor: ] start: 0 save_compressed: true + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml b/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml index 2be94f4efa..d4a038e113 100644 --- a/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml +++ b/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml @@ -86,3 +86,5 @@ fsdp_config: special_tokens: pad_token: <|finetune_right_pad_id|> eos_token: <|eot|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml b/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml index eeae872a6b..bea10d979e 100644 --- a/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml @@ -90,3 +90,5 @@ fsdp_config: special_tokens: pad_token: <|finetune_right_pad_id|> eos_token: <|eot|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml b/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml index 17ad706344..737d938126 100644 --- a/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml @@ -83,3 +83,5 @@ weight_decay: 0.0 special_tokens: pad_token: <|finetune_right_pad_id|> eos_token: <|eot|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml b/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml index eff708e4db..390be5af78 100644 --- a/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml @@ -86,3 +86,5 @@ fsdp_config: special_tokens: pad_token: <|finetune_right_pad_id|> eos_token: <|eot|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml index 9a411883e4..b319349c4a 100644 --- a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml +++ b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml @@ -84,3 +84,5 @@ fsdp_config: special_tokens: pad_token: <|finetune_right_pad_id|> eos_token: <|eot|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/scout-qlora-single-h100-flex.yaml b/examples/llama-4/scout-qlora-single-h100-flex.yaml index 20352f81eb..6be3988ef0 100644 --- a/examples/llama-4/scout-qlora-single-h100-flex.yaml +++ b/examples/llama-4/scout-qlora-single-h100-flex.yaml @@ -82,3 +82,5 @@ weight_decay: 0.0 special_tokens: pad_token: <|finetune_right_pad_id|> eos_token: <|eot|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml b/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml index 9fbd34107b..a67936cf18 100644 --- a/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml +++ b/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml @@ -87,3 +87,5 @@ fsdp_config: special_tokens: pad_token: <|finetune_right_pad_id|> eos_token: <|eot|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llava/lora-7b.yaml b/examples/llava/lora-7b.yaml index 5198c8e744..a4bac8987d 100644 --- a/examples/llava/lora-7b.yaml +++ b/examples/llava/lora-7b.yaml @@ -53,3 +53,5 @@ warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/magistral/magistral-small-fsdp-qlora.yaml b/examples/magistral/magistral-small-fsdp-qlora.yaml index b10e8baf6a..b23d2309a0 100644 --- a/examples/magistral/magistral-small-fsdp-qlora.yaml +++ b/examples/magistral/magistral-small-fsdp-qlora.yaml @@ -70,3 +70,5 @@ fsdp_config: fsdp_state_dict_type: FULL_STATE_DICT fsdp_transformer_layer_cls_to_wrap: MistralDecoderLayer fsdp_activation_checkpointing: true + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/magistral/magistral-small-qlora.yaml b/examples/magistral/magistral-small-qlora.yaml index e3e746f224..f0fce014fa 100644 --- a/examples/magistral/magistral-small-qlora.yaml +++ b/examples/magistral/magistral-small-qlora.yaml @@ -61,3 +61,5 @@ flash_attention: true warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mamba/config.yml b/examples/mamba/config.yml index 3d4583932e..2261bd2156 100644 --- a/examples/mamba/config.yml +++ b/examples/mamba/config.yml @@ -48,3 +48,5 @@ weight_decay: 0.0 special_tokens: tokens: save_safetensors: False + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/bigstral-ds-zero3.yaml b/examples/mistral/bigstral-ds-zero3.yaml index f626a92a17..e9bcbb7d68 100644 --- a/examples/mistral/bigstral-ds-zero3.yaml +++ b/examples/mistral/bigstral-ds-zero3.yaml @@ -53,3 +53,5 @@ special_tokens: eos_token: "<|im_end|>" tokens: - "<|im_start|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/config.yml b/examples/mistral/config.yml index 15edffb44e..8c4d80f792 100644 --- a/examples/mistral/config.yml +++ b/examples/mistral/config.yml @@ -43,3 +43,5 @@ evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/lora-mps.yml b/examples/mistral/lora-mps.yml index e6f46affb1..d54c3e30bb 100644 --- a/examples/mistral/lora-mps.yml +++ b/examples/mistral/lora-mps.yml @@ -64,3 +64,5 @@ evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/lora.yml b/examples/mistral/lora.yml index 9af4274fdf..161255468e 100644 --- a/examples/mistral/lora.yml +++ b/examples/mistral/lora.yml @@ -64,3 +64,5 @@ evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/mistral-dpo-qlora.yml b/examples/mistral/mistral-dpo-qlora.yml index af707973ff..8d03786904 100644 --- a/examples/mistral/mistral-dpo-qlora.yml +++ b/examples/mistral/mistral-dpo-qlora.yml @@ -80,3 +80,5 @@ weight_decay: 0.0 special_tokens: bos_token: "<|im_start|>" eos_token: "<|im_end|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/mistral-qlora-fsdp.yml b/examples/mistral/mistral-qlora-fsdp.yml index e234b19a24..cec958c54c 100644 --- a/examples/mistral/mistral-qlora-fsdp.yml +++ b/examples/mistral/mistral-qlora-fsdp.yml @@ -74,3 +74,5 @@ fsdp_config: fsdp_state_dict_type: FULL_STATE_DICT fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/mistral-qlora-orpo.yml b/examples/mistral/mistral-qlora-orpo.yml index 6c0212b7cb..f37dc09fa3 100644 --- a/examples/mistral/mistral-qlora-orpo.yml +++ b/examples/mistral/mistral-qlora-orpo.yml @@ -69,3 +69,5 @@ evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/mistral-small-3.1-24B-lora.yml b/examples/mistral/mistral-small-3.1-24B-lora.yml index 3e3b45862d..4a492c5953 100644 --- a/examples/mistral/mistral-small-3.1-24B-lora.yml +++ b/examples/mistral/mistral-small-3.1-24B-lora.yml @@ -56,3 +56,5 @@ evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml index af6ba5a769..64ef9930ce 100644 --- a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml +++ b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml @@ -72,3 +72,5 @@ fsdp_config: fsdp_state_dict_type: FULL_STATE_DICT fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/mixtral-qlora-fsdp.yml b/examples/mistral/mixtral-qlora-fsdp.yml index b1843a138d..c8d0a2711b 100644 --- a/examples/mistral/mixtral-qlora-fsdp.yml +++ b/examples/mistral/mixtral-qlora-fsdp.yml @@ -77,3 +77,5 @@ fsdp_config: fsdp_forward_prefetch: false fsdp_backward_prefetch: BACKWARD_PRE special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/mixtral.yml b/examples/mistral/mixtral.yml index 4c256420cf..5be9b4db89 100644 --- a/examples/mistral/mixtral.yml +++ b/examples/mistral/mixtral.yml @@ -81,3 +81,5 @@ saves_per_epoch: 1 deepspeed: deepspeed_configs/zero2.json weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/mixtral_22.yml b/examples/mistral/mixtral_22.yml index 25e1d71551..100e4464ff 100644 --- a/examples/mistral/mixtral_22.yml +++ b/examples/mistral/mixtral_22.yml @@ -51,3 +51,5 @@ special_tokens: eos_token: "<|im_end|>" tokens: - "<|im_start|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/qlora.yml b/examples/mistral/qlora.yml index 607e337014..08df36e150 100644 --- a/examples/mistral/qlora.yml +++ b/examples/mistral/qlora.yml @@ -64,3 +64,5 @@ evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/orpheus/finetune.yml b/examples/orpheus/finetune.yml index 9bcbbeee0d..57f65d9666 100644 --- a/examples/orpheus/finetune.yml +++ b/examples/orpheus/finetune.yml @@ -50,3 +50,5 @@ weight_decay: 0.05 special_tokens: pad_token: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/phi/lora-3.5.yaml b/examples/phi/lora-3.5.yaml index ad4ce9cd44..9f3bbdf539 100644 --- a/examples/phi/lora-3.5.yaml +++ b/examples/phi/lora-3.5.yaml @@ -63,3 +63,5 @@ warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 4 weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/phi/phi-ft.yml b/examples/phi/phi-ft.yml index 1562a73536..fc6d649d71 100644 --- a/examples/phi/phi-ft.yml +++ b/examples/phi/phi-ft.yml @@ -57,3 +57,5 @@ weight_decay: 0.1 resize_token_embeddings_to_32x: true special_tokens: pad_token: "<|endoftext|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/phi/phi-qlora.yml b/examples/phi/phi-qlora.yml index 4cd53db979..ccd92c817f 100644 --- a/examples/phi/phi-qlora.yml +++ b/examples/phi/phi-qlora.yml @@ -60,3 +60,5 @@ weight_decay: 0.1 resize_token_embeddings_to_32x: true special_tokens: pad_token: "<|endoftext|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/phi/phi2-ft.yml b/examples/phi/phi2-ft.yml index ca733cc71b..853250ccbb 100644 --- a/examples/phi/phi2-ft.yml +++ b/examples/phi/phi2-ft.yml @@ -57,3 +57,5 @@ weight_decay: 0.1 resize_token_embeddings_to_32x: true special_tokens: pad_token: "<|endoftext|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/phi/phi3-ft-fsdp.yml b/examples/phi/phi3-ft-fsdp.yml index d0d14fea67..130298bc04 100644 --- a/examples/phi/phi3-ft-fsdp.yml +++ b/examples/phi/phi3-ft-fsdp.yml @@ -71,3 +71,5 @@ fsdp_config: resize_token_embeddings_to_32x: true special_tokens: pad_token: "<|endoftext|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/phi/phi3-ft.yml b/examples/phi/phi3-ft.yml index 17c48da6f7..42b87e8d05 100644 --- a/examples/phi/phi3-ft.yml +++ b/examples/phi/phi3-ft.yml @@ -59,3 +59,5 @@ warmup_ratio: 0.2 debug: true weight_decay: 0.1 resize_token_embeddings_to_32x: true + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/pixtral/lora-12b.yml b/examples/pixtral/lora-12b.yml index 6ad0a5e999..ea769d202c 100644 --- a/examples/pixtral/lora-12b.yml +++ b/examples/pixtral/lora-12b.yml @@ -55,3 +55,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: pad_token: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qwen2-vl/lora-7b.yaml b/examples/qwen2-vl/lora-7b.yaml index e8932b9688..8ea6081999 100644 --- a/examples/qwen2-vl/lora-7b.yaml +++ b/examples/qwen2-vl/lora-7b.yaml @@ -53,3 +53,5 @@ warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qwen2/dpo.yaml b/examples/qwen2/dpo.yaml index bd896c2b3d..69a74ae4a4 100644 --- a/examples/qwen2/dpo.yaml +++ b/examples/qwen2/dpo.yaml @@ -54,3 +54,5 @@ warmup_steps: 10 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qwen2/prm.yaml b/examples/qwen2/prm.yaml index 4afa24f3ce..af188f75d6 100644 --- a/examples/qwen2/prm.yaml +++ b/examples/qwen2/prm.yaml @@ -55,3 +55,5 @@ eval_steps: 100 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qwen2/qlora-fsdp.yaml b/examples/qwen2/qlora-fsdp.yaml index ed2670ab61..861ce5517e 100644 --- a/examples/qwen2/qlora-fsdp.yaml +++ b/examples/qwen2/qlora-fsdp.yaml @@ -67,3 +67,5 @@ fsdp_config: fsdp_state_dict_type: FULL_STATE_DICT fsdp_sharding_strategy: FULL_SHARD special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qwen2/reward-model.yaml b/examples/qwen2/reward-model.yaml index 822407a1fe..1854b8216b 100644 --- a/examples/qwen2/reward-model.yaml +++ b/examples/qwen2/reward-model.yaml @@ -26,7 +26,6 @@ wandb_watch: wandb_name: wandb_log_model: - gradient_accumulation_steps: 4 micro_batch_size: 2 num_epochs: 4 @@ -50,3 +49,5 @@ evals_per_epoch: saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qwen2_5-vl/lora-7b.yaml b/examples/qwen2_5-vl/lora-7b.yaml index 25d02805f7..13a97dec3e 100644 --- a/examples/qwen2_5-vl/lora-7b.yaml +++ b/examples/qwen2_5-vl/lora-7b.yaml @@ -53,3 +53,5 @@ warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qwen3/32b-qlora.yaml b/examples/qwen3/32b-qlora.yaml index 45a4395ac1..1f148ece5c 100644 --- a/examples/qwen3/32b-qlora.yaml +++ b/examples/qwen3/32b-qlora.yaml @@ -67,3 +67,5 @@ evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qwen3/8b-qat-fsdp2.yml b/examples/qwen3/8b-qat-fsdp2.yml index 6832b6af75..e4d0ed4fb0 100644 --- a/examples/qwen3/8b-qat-fsdp2.yml +++ b/examples/qwen3/8b-qat-fsdp2.yml @@ -76,3 +76,5 @@ fsdp_config: fsdp_activation_checkpointing: true special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qwen3/qlora-fsdp.yaml b/examples/qwen3/qlora-fsdp.yaml index dc3377b4f2..762f9648d1 100644 --- a/examples/qwen3/qlora-fsdp.yaml +++ b/examples/qwen3/qlora-fsdp.yaml @@ -66,3 +66,5 @@ fsdp_config: fsdp_state_dict_type: FULL_STATE_DICT fsdp_sharding_strategy: FULL_SHARD special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 4df0100406..d3a3b32424 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -36,6 +36,7 @@ GCCallback, GPUStatsCallback, SaveAxolotlConfigtoWandBCallback, + SaveModelOnFirstStepCallback, ) from axolotl.utils.callbacks.profiler import PytorchProfilerCallback from axolotl.utils.schemas.enums import CustomSupportedOptimizers @@ -135,6 +136,8 @@ def get_callbacks(self) -> list[TrainerCallback]: callbacks.append( SaveAxolotlConfigtoCometCallback(self.cfg.axolotl_config_path) ) + if self.cfg.save_first_step: + callbacks.append(SaveModelOnFirstStepCallback()) callbacks.append(GPUStatsCallback(cfg=self.cfg)) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 5f804d6afa..bb777fc90f 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -64,7 +64,7 @@ def on_step_end( state: TrainerState, control: TrainerControl, **kwargs, - ): + ) -> TrainerControl: # Save if ( args.save_strategy == IntervalStrategy.STEPS @@ -100,11 +100,11 @@ def __init__(self, cfg): def on_step_end( self, - args: TrainingArguments, + args: TrainingArguments, # pylint: disable=unused-argument state: TrainerState, control: TrainerControl, **kwargs, - ): + ) -> TrainerControl: if not self.logged and state.global_step > 1: log_gpu_memory_usage(LOG, "while training", self.cfg.device) self.logged = True @@ -116,18 +116,17 @@ class LossWatchDogCallback(TrainerCallback): def __init__(self, cfg): self.cfg = cfg - self.logged = False self.violations = 0 self.threshold = cfg.loss_watchdog_threshold self.patience = cfg.loss_watchdog_patience or 3 def on_step_end( self, - _args: TrainingArguments, + args: TrainingArguments, # pylint: disable=unused-argument state: TrainerState, control: TrainerControl, **_kwargs, - ): + ) -> TrainerControl: if len(state.log_history) > 0 and "loss" in state.log_history[-1]: if state.log_history[-1]["loss"] > self.threshold: self.violations += 1 @@ -141,6 +140,21 @@ def on_step_end( return control +class SaveModelOnFirstStepCallback(TrainerCallback): + """Callback to save the model on the first step of training if enabled""" + + def on_step_end( + self, + args: TrainingArguments, # pylint: disable=unused-argument + state: TrainerState, + control: TrainerControl, + **_kwargs, + ) -> TrainerControl: + if state.global_step == 1: + control.should_save = True + return control + + def bench_eval_callback_factory(trainer, tokenizer): accuracy = evaluate.load("accuracy") abcd_idx = [ diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 909fd637c6..e20cdaf47b 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -706,6 +706,7 @@ class AxolotlInputConfig( "description": "Set to `no` to skip evaluation, `epoch` at end of each epoch, leave empty to infer from `eval_steps`" }, ) + save_steps: int | float | None = Field( default=None, json_schema_extra={ @@ -727,6 +728,13 @@ class AxolotlInputConfig( save_total_limit: int | None = Field( default=None, json_schema_extra={"description": "Checkpoints saved at a time"} ) + save_first_step: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to checkpoint a model after the first step of training. Defaults to False." + }, + ) + logging_steps: int | None = Field( default=None, json_schema_extra={"description": "Logging frequency"} ) diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py index 790b34f3e6..34e6c96447 100644 --- a/tests/e2e/integrations/test_cut_cross_entropy.py +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -44,6 +44,7 @@ def min_cfg(temp_dir): "save_safetensors": True, "max_steps": 10, "bf16": "auto", + "save_first_step": False, } @@ -98,6 +99,7 @@ def test_qwen2_w_cce(self, temp_dir): "save_safetensors": True, "max_steps": 10, "bf16": "auto", + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/integrations/test_hooks.py b/tests/e2e/integrations/test_hooks.py index 4734449fe2..8743efb981 100644 --- a/tests/e2e/integrations/test_hooks.py +++ b/tests/e2e/integrations/test_hooks.py @@ -153,6 +153,7 @@ def test_plugin_hooks(self, temp_dir): "max_steps": 5, "flash_attention": True, "bf16": "auto", + "save_first_step": False, } ) diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py index 212450e89e..1ac3b537e7 100644 --- a/tests/e2e/integrations/test_kd.py +++ b/tests/e2e/integrations/test_kd.py @@ -67,6 +67,7 @@ def min_cfg(temp_dir): "output_dir": temp_dir, "save_safetensors": True, "use_tensorboard": True, + "save_first_step": False, } diff --git a/tests/e2e/integrations/test_liger.py b/tests/e2e/integrations/test_liger.py index 6ab3d7ab89..b1f5befdd4 100644 --- a/tests/e2e/integrations/test_liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -50,6 +50,7 @@ def test_llama_wo_flce(self, temp_dir): "save_safetensors": True, "bf16": "auto", "max_steps": 5, + "save_first_step": False, } ) # pylint: disable=duplicate-code @@ -96,6 +97,7 @@ def test_llama_w_flce(self, temp_dir): "save_safetensors": True, "bf16": "auto", "max_steps": 5, + "save_first_step": False, } ) # pylint: disable=duplicate-code diff --git a/tests/e2e/integrations/test_llm_compressor.py b/tests/e2e/integrations/test_llm_compressor.py index 247ae3bac2..dceecea9ff 100644 --- a/tests/e2e/integrations/test_llm_compressor.py +++ b/tests/e2e/integrations/test_llm_compressor.py @@ -81,6 +81,7 @@ def test_llmcompressor_plugin( }, "save_compressed": save_compressed, }, + "save_first_step": False, } ) diff --git a/tests/e2e/multigpu/patched/test_sp.py b/tests/e2e/multigpu/patched/test_sp.py index 5593c7eb62..80098e6841 100644 --- a/tests/e2e/multigpu/patched/test_sp.py +++ b/tests/e2e/multigpu/patched/test_sp.py @@ -69,6 +69,7 @@ def _run_sequence_parallel_test( "use_tensorboard": True, "sequence_parallel_degree": 2, "ring_attn_func": ring_attn_func, + "save_first_step": False, } ) diff --git a/tests/e2e/multigpu/solo/test_flex.py b/tests/e2e/multigpu/solo/test_flex.py index bdf5ada6ba..cbdf8de96b 100644 --- a/tests/e2e/multigpu/solo/test_flex.py +++ b/tests/e2e/multigpu/solo/test_flex.py @@ -61,6 +61,7 @@ def test_loss_llama(self, temp_dir): "max_steps": 2, "use_tensorboard": True, "save_strategy": "no", + "save_first_step": False, } ) if is_torch_bf16_gpu_available(): diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index c047343456..d022ae2d92 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -223,6 +223,7 @@ def test_llama_dora(self, temp_dir, num_gpus): "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, + "save_first_step": False, } ) @@ -317,6 +318,7 @@ def test_llama_lora_sp(self, temp_dir): "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, + "save_first_step": False, } ) @@ -409,6 +411,7 @@ def test_llama_fft(self, temp_dir, num_gpus): "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, + "save_first_step": False, } ) diff --git a/tests/e2e/multigpu/test_eval.py b/tests/e2e/multigpu/test_eval.py index d6429cf63c..4f86278ffb 100644 --- a/tests/e2e/multigpu/test_eval.py +++ b/tests/e2e/multigpu/test_eval.py @@ -67,6 +67,7 @@ def test_eval_sample_packing(self, temp_dir): "logging_steps": 1, "weight_decay": 0.0, "use_tensorboard": True, + "save_first_step": False, } ) @@ -138,6 +139,7 @@ def test_eval(self, temp_dir): "logging_steps": 1, "weight_decay": 0.0, "use_tensorboard": True, + "save_first_step": False, } ) diff --git a/tests/e2e/multigpu/test_gemma3.py b/tests/e2e/multigpu/test_gemma3.py index 3868d90f0f..4a7b101a83 100644 --- a/tests/e2e/multigpu/test_gemma3.py +++ b/tests/e2e/multigpu/test_gemma3.py @@ -71,6 +71,7 @@ def test_lora_ddp_packed(self, temp_dir): "flash_attention": True, "use_tensorboard": True, "bf16": True, + "save_first_step": False, } ) diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index f0c74fbf8b..aab14dcc4f 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -69,6 +69,7 @@ def test_lora_ddp(self, temp_dir): "flash_attention": True, "use_tensorboard": True, "bf16": True, + "save_first_step": False, } ) @@ -135,6 +136,7 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): "flash_attention": True, "use_tensorboard": True, "bf16": True, + "save_first_step": False, } ) @@ -210,6 +212,7 @@ def test_dpo_lora_ddp(self, temp_dir): "flash_attention": True, "use_tensorboard": True, "bf16": True, + "save_first_step": False, } ) @@ -289,6 +292,7 @@ def test_dpo_qlora_ddp(self, temp_dir): "flash_attention": True, "use_tensorboard": True, "bf16": True, + "save_first_step": False, } ) @@ -365,6 +369,7 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): }, "use_tensorboard": True, "seed": 42, + "save_first_step": False, } ) @@ -442,6 +447,7 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", }, "use_tensorboard": True, + "save_first_step": False, } ) @@ -520,6 +526,7 @@ def test_fsdp2_packed( "fsdp_reshard_after_forward": fsdp_reshard_after_forward, }, "use_tensorboard": True, + "save_first_step": False, } ) if attention_backend == "flash": @@ -605,6 +612,7 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", }, "use_tensorboard": True, + "save_first_step": False, } ) @@ -689,6 +697,7 @@ def test_ds_zero3_packed( "flash_attention": True, "deepspeed": str(AXOLOTL_ROOT / deepspeed), "use_tensorboard": True, + "save_first_step": False, **adapter, } ) @@ -765,6 +774,7 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero2.json"), "use_tensorboard": True, "seed": 42, + "save_first_step": False, **adapter, } ) @@ -840,6 +850,7 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): "flash_attention": True, "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero1.json"), "use_tensorboard": True, + "save_first_step": False, **adapter, } ) @@ -908,6 +919,7 @@ def test_fix_untrained_tokens(self, temp_dir): "save_safetensors": True, # "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero1.json"), "use_tensorboard": True, + "save_first_step": False, } ) diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index 43a722b488..dd14222968 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -56,6 +56,7 @@ def test_lora_ddp(self, temp_dir): "use_tensorboard": True, "use_ray": True, "ray_num_workers": 2, + "save_first_step": False, } ) @@ -115,6 +116,7 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps): "flash_attention": True, "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero2.json"), "use_tensorboard": True, + "save_first_step": False, } ) diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index 08b62accc0..1824443e79 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -55,6 +55,7 @@ def test_sdp_lora_packing(self, temp_dir): "save_steps": 3, "eval_steps": 4, "fp16": True, + "save_first_step": False, } ) cfg = validate_config(cfg) @@ -102,6 +103,7 @@ def test_torch_lora_packing(self, temp_dir): "save_steps": 3, "eval_steps": 4, "fp16": True, + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/patched/test_activation_checkpointing.py b/tests/e2e/patched/test_activation_checkpointing.py index d494ed1ebd..3d5b3dc56c 100644 --- a/tests/e2e/patched/test_activation_checkpointing.py +++ b/tests/e2e/patched/test_activation_checkpointing.py @@ -69,6 +69,7 @@ def test_activation_checkpointing_offload( "bf16": True, "save_safetensors": True, "gradient_checkpointing": gradient_checkpointing, + "save_first_step": False, } ) diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index ca8b21178f..38099b220b 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -62,6 +62,7 @@ def test_lora_packing_fa_cross_entropy(self, temp_dir, gradient_accumulation_ste "optimizer": "adamw_8bit", "lr_scheduler": "cosine", "use_tensorboard": True, + "save_first_step": False, } ) if is_torch_bf16_gpu_available(): diff --git a/tests/e2e/patched/test_falcon_samplepack.py b/tests/e2e/patched/test_falcon_samplepack.py index a593b07918..ef31b11c78 100644 --- a/tests/e2e/patched/test_falcon_samplepack.py +++ b/tests/e2e/patched/test_falcon_samplepack.py @@ -58,6 +58,7 @@ def test_qlora(self, temp_dir): "save_steps": 10, "eval_steps": 10, "bf16": "auto", + "save_first_step": False, } ) cfg = validate_config(cfg) @@ -99,6 +100,7 @@ def test_ft(self, temp_dir): "save_steps": 10, "eval_steps": 10, "bf16": "auto", + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/patched/test_flattening.py b/tests/e2e/patched/test_flattening.py index f77a1fbe5a..fdaab558dc 100644 --- a/tests/e2e/patched/test_flattening.py +++ b/tests/e2e/patched/test_flattening.py @@ -61,6 +61,7 @@ def test_lora_packing_flattening(self, temp_dir, gradient_accumulation_steps): "optimizer": "adamw_8bit", "lr_scheduler": "cosine", "use_tensorboard": True, + "save_first_step": False, } ) if is_torch_bf16_gpu_available(): diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index 1bbc82a38a..a3fe591ee8 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -53,6 +53,7 @@ def test_fft_packing(self, temp_dir): "max_steps": 10, "save_steps": 5, "eval_steps": 5, + "save_first_step": False, } ) if is_torch_bf16_gpu_available(): diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py index d2dcc5e4b7..ba5556a593 100644 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ b/tests/e2e/patched/test_llama_s2_attention.py @@ -58,6 +58,7 @@ def test_lora_s2_attn(self, temp_dir): "save_steps": 5, "eval_steps": 5, "bf16": "auto", + "save_first_step": False, } ) @@ -100,6 +101,7 @@ def test_fft_s2_attn(self, temp_dir): "save_steps": 5, "eval_steps": 5, "bf16": "auto", + "save_first_step": False, } ) diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index 5df6bfecc6..fdf6adbc6c 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -55,6 +55,7 @@ def test_lora_packing(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", + "save_first_step": False, } ) if is_torch_bf16_gpu_available(): @@ -108,6 +109,7 @@ def test_lora_gptq_packed(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index 442089bae7..bea0f9c68c 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -56,6 +56,7 @@ def test_lora_packing(self, temp_dir): "save_steps": 3, "eval_steps": 4, "bf16": "auto", + "save_first_step": False, } ) cfg = validate_config(cfg) @@ -97,6 +98,7 @@ def test_ft_packing(self, temp_dir): "save_steps": 3, "eval_steps": 4, "bf16": "auto", + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index 5f778660bb..09e427abd3 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -52,6 +52,7 @@ def test_qlora(self, temp_dir): "save_steps": 3, "eval_steps": 4, "bf16": "auto", + "save_first_step": False, } ) cfg = validate_config(cfg) @@ -90,6 +91,7 @@ def test_ft(self, temp_dir): "save_steps": 3, "eval_steps": 4, "bf16": "auto", + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/patched/test_model_patches.py b/tests/e2e/patched/test_model_patches.py index 5ea88b001b..b90be23e43 100644 --- a/tests/e2e/patched/test_model_patches.py +++ b/tests/e2e/patched/test_model_patches.py @@ -45,6 +45,7 @@ def test_mixtral_multipack(self, temp_dir): "max_steps": 20, "save_steps": 10, "eval_steps": 10, + "save_first_step": False, } ) cfg = validate_config(cfg) @@ -78,6 +79,7 @@ def test_mistral_multipack(self, temp_dir): "max_steps": 20, "save_steps": 10, "eval_steps": 10, + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/patched/test_peft_embeddings.py b/tests/e2e/patched/test_peft_embeddings.py index d4f59a128f..4769319aef 100644 --- a/tests/e2e/patched/test_peft_embeddings.py +++ b/tests/e2e/patched/test_peft_embeddings.py @@ -49,6 +49,7 @@ def test_peft_embeddings_upcast(self, temp_dir): "bf16": "auto", "save_safetensors": True, "embeddings_skip_upcast": True, + "save_first_step": False, } ) diff --git a/tests/e2e/patched/test_phi_multipack.py b/tests/e2e/patched/test_phi_multipack.py index d241ce1853..1f0ddd6303 100644 --- a/tests/e2e/patched/test_phi_multipack.py +++ b/tests/e2e/patched/test_phi_multipack.py @@ -54,6 +54,7 @@ def test_ft_packed(self, temp_dir): "eval_steps": 3, "save_steps": 4, "bf16": "auto", + "save_first_step": False, } ) @@ -105,6 +106,7 @@ def test_qlora_packed(self, temp_dir): "eval_steps": 3, "save_steps": 4, "bf16": "auto", + "save_first_step": False, } ) diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index 3639567335..54b8245eec 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -58,6 +58,7 @@ def test_resume_lora_packed(self, temp_dir): "max_steps": 15, "use_tensorboard": True, "save_safetensors": True, + "save_first_step": False, } ) if is_torch_bf16_gpu_available(): diff --git a/tests/e2e/patched/test_sp.py b/tests/e2e/patched/test_sp.py index 2b4d11b30c..4a2c69d457 100644 --- a/tests/e2e/patched/test_sp.py +++ b/tests/e2e/patched/test_sp.py @@ -47,6 +47,7 @@ def fixture_cfg(): "special_tokens": { "pad_token": "<|endoftext|>", }, + "save_first_step": False, } ) diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index 69171481c7..2c8ee4eb05 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -62,6 +62,7 @@ def test_unsloth_llama_qlora_fa2(self, temp_dir, sample_packing): "lr_scheduler": "cosine", "use_tensorboard": True, "bf16": "auto", + "save_first_step": False, } ) @@ -112,6 +113,7 @@ def test_unsloth_llama_qlora_unpacked(self, temp_dir): "lr_scheduler": "cosine", "use_tensorboard": True, "bf16": "auto", + "save_first_step": False, } ) @@ -167,6 +169,7 @@ def test_unsloth_llama_qlora_unpacked_no_fa2_fp16(self, temp_dir, sdp_attention) "lr_scheduler": "cosine", "use_tensorboard": True, "fp16": True, + "save_first_step": False, } ) diff --git a/tests/e2e/solo/test_flex.py b/tests/e2e/solo/test_flex.py index 2799137136..76364fc0e5 100644 --- a/tests/e2e/solo/test_flex.py +++ b/tests/e2e/solo/test_flex.py @@ -49,6 +49,7 @@ def test_loss_llama(self, temp_dir): "lr_scheduler": "cosine", "max_steps": 5, "use_tensorboard": True, + "save_first_step": False, } ) if is_torch_bf16_gpu_available(): diff --git a/tests/e2e/solo/test_relora_llama.py b/tests/e2e/solo/test_relora_llama.py index 7af5504963..f6fcad8415 100644 --- a/tests/e2e/solo/test_relora_llama.py +++ b/tests/e2e/solo/test_relora_llama.py @@ -65,6 +65,7 @@ def test_relora(self, temp_dir): "lr_scheduler": "cosine", "save_safetensors": True, "use_tensorboard": True, + "save_first_step": False, } ) diff --git a/tests/e2e/test_deepseekv3.py b/tests/e2e/test_deepseekv3.py index 7dfc4ae159..e4a47fb0aa 100644 --- a/tests/e2e/test_deepseekv3.py +++ b/tests/e2e/test_deepseekv3.py @@ -67,6 +67,7 @@ def test_lora_deepseekv3(self, temp_dir, sample_packing): "max_steps": 5, "save_safetensors": True, "bf16": True, + "save_first_step": False, } ) cfg = validate_config(cfg) @@ -116,6 +117,7 @@ def test_fft_deepseekv3(self, temp_dir, sample_packing): "max_steps": 5, "save_safetensors": True, "bf16": True, + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index 2cdb576891..a1df695352 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -56,6 +56,7 @@ def test_dpo_lora(self, temp_dir): "warmup_steps": 5, "gradient_checkpointing": True, "gradient_checkpointing_kwargs": {"use_reentrant": True}, + "save_first_step": False, } ) @@ -105,6 +106,7 @@ def test_dpo_nll_lora(self, temp_dir): "warmup_steps": 5, "gradient_checkpointing": True, "gradient_checkpointing_kwargs": {"use_reentrant": True}, + "save_first_step": False, } ) @@ -154,6 +156,7 @@ def test_dpo_use_weighting(self, temp_dir): "warmup_steps": 5, "gradient_checkpointing": True, "gradient_checkpointing_kwargs": {"use_reentrant": True}, + "save_first_step": False, } ) @@ -203,6 +206,7 @@ def test_kto_pair_lora(self, temp_dir): "warmup_steps": 5, "gradient_checkpointing": True, "gradient_checkpointing_kwargs": {"use_reentrant": True}, + "save_first_step": False, } ) @@ -251,6 +255,7 @@ def test_ipo_lora(self, temp_dir): "warmup_steps": 5, "gradient_checkpointing": True, "gradient_checkpointing_kwargs": {"use_reentrant": True}, + "save_first_step": False, } ) @@ -302,6 +307,7 @@ def test_orpo_lora(self, temp_dir): "warmup_steps": 5, "gradient_checkpointing": True, "gradient_checkpointing_kwargs": {"use_reentrant": True}, + "save_first_step": False, } ) @@ -370,6 +376,7 @@ def test_kto_lora(self, temp_dir): "warmup_steps": 5, "gradient_checkpointing": True, "gradient_checkpointing_kwargs": {"use_reentrant": True}, + "save_first_step": False, } ) diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py index 9b65f8feb6..e4a06ad148 100644 --- a/tests/e2e/test_embeddings_lr.py +++ b/tests/e2e/test_embeddings_lr.py @@ -48,6 +48,7 @@ def test_train_w_embedding_lr_scale(self, temp_dir): "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, + "save_first_step": False, } ) @@ -93,6 +94,7 @@ def test_train_w_embedding_lr(self, temp_dir): "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/test_evaluate.py b/tests/e2e/test_evaluate.py index 6271bba289..977497e5e8 100644 --- a/tests/e2e/test_evaluate.py +++ b/tests/e2e/test_evaluate.py @@ -36,6 +36,7 @@ def test_evaluate(self, temp_dir): "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 20, + "save_first_step": False, } ) diff --git a/tests/e2e/test_falcon.py b/tests/e2e/test_falcon.py index 4f88e740c3..5be6efcf64 100644 --- a/tests/e2e/test_falcon.py +++ b/tests/e2e/test_falcon.py @@ -60,6 +60,7 @@ def test_lora(self, temp_dir): "save_steps": 10, "eval_steps": 10, "bf16": "auto", + "save_first_step": False, } ) @@ -115,6 +116,7 @@ def test_lora_added_vocab(self, temp_dir): "save_steps": 10, "eval_steps": 10, "bf16": "auto", + "save_first_step": False, } ) @@ -156,6 +158,7 @@ def test_ft(self, temp_dir): "save_steps": 10, "eval_steps": 10, "bf16": "auto", + "save_first_step": False, } ) diff --git a/tests/e2e/test_gemma3_text.py b/tests/e2e/test_gemma3_text.py index 3f00a13844..ef38d028dc 100644 --- a/tests/e2e/test_gemma3_text.py +++ b/tests/e2e/test_gemma3_text.py @@ -63,6 +63,7 @@ def test_lora_gemma3_text(self, temp_dir, sample_packing): "max_steps": 5, "save_safetensors": True, "bf16": True, + "save_first_step": False, } ) cfg = validate_config(cfg) @@ -113,6 +114,7 @@ def test_fft_gemma3_text(self, temp_dir, sample_packing): "max_steps": 5, "save_safetensors": True, "bf16": True, + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index 2b180029ce..1e6df0be9c 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -45,6 +45,7 @@ def test_fft_trust_remote_code(self, temp_dir): "sample_packing": True, "bf16": True, "save_safetensors": True, + "save_first_step": False, } ) @@ -92,6 +93,7 @@ def test_fix_untrained_tokens(self, temp_dir): "sample_packing": True, "bf16": True, "save_safetensors": True, + "save_first_step": False, } ) @@ -136,6 +138,7 @@ def test_fix_untrained_tokens_already_trained(self, temp_dir): "sample_packing": True, "bf16": True, "save_safetensors": True, + "save_first_step": False, } ) @@ -176,6 +179,7 @@ def test_batch_flattening(self, temp_dir): "batch_flattening": True, "bf16": True, "save_safetensors": True, + "save_first_step": False, } ) diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index fdebf2173b..bd55023008 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -53,6 +53,7 @@ def test_pretrain(self, temp_dir, sample_packing, pretrain_multipack_attn): "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, + "save_first_step": False, } ) diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py index ad4a83c6a2..760759bcaa 100644 --- a/tests/e2e/test_llama_vision.py +++ b/tests/e2e/test_llama_vision.py @@ -54,6 +54,7 @@ def test_lora_llama_vision_text_only_dataset(self, temp_dir): "max_steps": 5, "save_safetensors": True, "bf16": True, + "save_first_step": False, } ) @@ -100,6 +101,7 @@ def test_lora_llama_vision_multimodal_dataset(self, temp_dir): "max_steps": 5, "save_safetensors": True, "bf16": True, + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index 3015653021..7e0ff46cf7 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -49,6 +49,7 @@ def test_lora(self, temp_dir): "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "max_steps": 5, + "save_first_step": False, } ) diff --git a/tests/e2e/test_mamba.py b/tests/e2e/test_mamba.py index 1824619a6a..73d3bdc260 100644 --- a/tests/e2e/test_mamba.py +++ b/tests/e2e/test_mamba.py @@ -51,6 +51,7 @@ def test_fft(self, temp_dir): "save_steps": 10, "eval_steps": None, "save_safetensors": False, + "save_first_step": False, } ) diff --git a/tests/e2e/test_mistral.py b/tests/e2e/test_mistral.py index 5d9b8ba8c1..f47f794e06 100644 --- a/tests/e2e/test_mistral.py +++ b/tests/e2e/test_mistral.py @@ -55,6 +55,7 @@ def test_lora(self, temp_dir): "max_steps": 20, "save_steps": 10, "eval_steps": 10, + "save_first_step": False, } ) @@ -95,6 +96,7 @@ def test_ft(self, temp_dir): "max_steps": 20, "save_steps": 10, "eval_steps": 10, + "save_first_step": False, } ) if is_torch_bf16_gpu_available(): diff --git a/tests/e2e/test_mixtral.py b/tests/e2e/test_mixtral.py index 761e59391c..3fe2bf70f1 100644 --- a/tests/e2e/test_mixtral.py +++ b/tests/e2e/test_mixtral.py @@ -61,6 +61,7 @@ def test_qlora_w_fa2(self, temp_dir): "max_steps": 20, "save_steps": 10, "eval_steps": 10, + "save_first_step": False, } ) @@ -116,6 +117,7 @@ def test_qlora_wo_fa2(self, temp_dir): "max_steps": 20, "save_steps": 10, "eval_steps": 10, + "save_first_step": False, } ) @@ -170,6 +172,7 @@ def test_16bit_lora_w_fa2(self, temp_dir): "max_steps": 20, "save_steps": 10, "eval_steps": 10, + "save_first_step": False, } ) if is_torch_bf16_gpu_available(): @@ -228,6 +231,7 @@ def test_16bit_lora_wo_fa2(self, temp_dir): "max_steps": 20, "save_steps": 10, "eval_steps": 10, + "save_first_step": False, } ) @@ -273,6 +277,7 @@ def test_ft(self, temp_dir): "max_steps": 20, "save_steps": 10, "eval_steps": 10, + "save_first_step": False, } ) if is_torch_bf16_gpu_available(): diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 53ef86022f..1d233a2013 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -55,6 +55,7 @@ def test_optimi_adamw(self, temp_dir): "optimizer": "optimi_adamw", "max_steps": 5, "lr_scheduler": "cosine", + "save_first_step": False, } ) @@ -100,6 +101,7 @@ def test_adopt_adamw(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adopt_adamw", "lr_scheduler": "cosine", + "save_first_step": False, } ) @@ -146,6 +148,7 @@ def test_muon(self, temp_dir): "optimizer": "muon", "lr_scheduler": "cosine", "weight_decay": 0.01, + "save_first_step": False, } ) @@ -184,6 +187,7 @@ def test_fft_schedule_free_adamw(self, temp_dir): "lr_scheduler": "constant", "save_safetensors": True, "max_steps": 10, + "save_first_step": False, } ) # pylint: disable=duplicate-code @@ -232,6 +236,7 @@ def test_came_pytorch(self, temp_dir): "adam_epsilon2": 1e-16, "max_steps": 5, "lr_scheduler": "cosine", + "save_first_step": False, } ) diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index cc2db72e0e..aec9d95f8b 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -48,6 +48,7 @@ def test_loss_packed(self, temp_dir): "lr_scheduler": "cosine", "max_steps": 5, "use_tensorboard": True, + "save_first_step": False, } ) if is_torch_bf16_gpu_available(): diff --git a/tests/e2e/test_phi.py b/tests/e2e/test_phi.py index 88fda91915..ab3a636748 100644 --- a/tests/e2e/test_phi.py +++ b/tests/e2e/test_phi.py @@ -53,6 +53,7 @@ def test_phi_ft(self, temp_dir): "save_steps": 10, "eval_steps": 10, "bf16": "auto", + "save_first_step": False, } ) cfg = validate_config(cfg) @@ -102,6 +103,7 @@ def test_phi_qlora(self, temp_dir): "save_steps": 10, "eval_steps": 10, "bf16": "auto", + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/test_process_reward_model_smollm2.py b/tests/e2e/test_process_reward_model_smollm2.py index abfe1b0c55..bd9eec48b6 100644 --- a/tests/e2e/test_process_reward_model_smollm2.py +++ b/tests/e2e/test_process_reward_model_smollm2.py @@ -49,6 +49,7 @@ def test_prm(self, temp_dir): "use_tensorboard": True, "special_tokens": {"pad_token": "<|endoftext|>"}, "seed": 42, + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/test_qat.py b/tests/e2e/test_qat.py index ef726079d0..139ae155ac 100644 --- a/tests/e2e/test_qat.py +++ b/tests/e2e/test_qat.py @@ -57,6 +57,7 @@ def test_qat(self, temp_dir): "max_steps": 5, "save_safetensors": True, "bf16": True, + "save_first_step": False, } ) cfg = validate_config(cfg) @@ -115,6 +116,7 @@ def test_qat_dpo(self, temp_dir): "weight_dtype": "int8", "group_size": 8, }, + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/test_qwen.py b/tests/e2e/test_qwen.py index aa8b9f6c05..59267d14db 100644 --- a/tests/e2e/test_qwen.py +++ b/tests/e2e/test_qwen.py @@ -59,6 +59,7 @@ def test_dpo(self, base_model, temp_dir): "bf16": "auto", "tf32": True, "gradient_checkpointing": True, + "save_first_step": False, } ) diff --git a/tests/e2e/test_reward_model_smollm2.py b/tests/e2e/test_reward_model_smollm2.py index 5d52bcc865..82513f99f2 100644 --- a/tests/e2e/test_reward_model_smollm2.py +++ b/tests/e2e/test_reward_model_smollm2.py @@ -58,6 +58,7 @@ def test_rm_lora(self, temp_dir): "gradient_checkpointing": True, "warmup_ratio": 0.1, "use_tensorboard": True, + "save_first_step": False, } ) cfg = validate_config(cfg) diff --git a/tests/e2e/test_save_first_step.py b/tests/e2e/test_save_first_step.py new file mode 100644 index 0000000000..5bbd2302b9 --- /dev/null +++ b/tests/e2e/test_save_first_step.py @@ -0,0 +1,102 @@ +""" +E2E tests for relora llama +""" + +import unittest +from pathlib import Path + +import pytest + +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +from .utils import check_model_output_exists, with_temp_dir + + +class TestSaveFirstStepCallback(unittest.TestCase): + """Test cases for save_first_step callback config.""" + + @with_temp_dir + def test_save_first_step(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", + "sequence_len": 512, + "val_set_size": 0.02, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 3, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": True, + "bf16": True, + "save_safetensors": True, + "save_first_step": True, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(str(Path(temp_dir) / "checkpoint-1"), cfg) + + @with_temp_dir + def test_no_save_first_step(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", + "sequence_len": 512, + "val_set_size": 0.02, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 3, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_bnb_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": True, + "bf16": True, + "save_safetensors": True, + "save_first_step": False, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + with pytest.raises(AssertionError): + check_model_output_exists(str(Path(temp_dir) / "checkpoint-1"), cfg) diff --git a/tests/e2e/test_schedulers.py b/tests/e2e/test_schedulers.py index e98378f08a..8f7a13aeea 100644 --- a/tests/e2e/test_schedulers.py +++ b/tests/e2e/test_schedulers.py @@ -51,6 +51,7 @@ def test_rex_scheduler(self, temp_dir): "lr_scheduler": "rex", "warmup_steps": 5, "cosine_min_lr_ratio": 0.05, + "save_first_step": False, } ) From 942005f526ca78f35a23cad6bd10abb9e3fb2c9f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 15 Jul 2025 20:31:23 -0400 Subject: [PATCH 0813/1405] use modal==1.0.2 for nightlies and for cli (#2925) [skip ci] * use modal==1.0.2 for nightlies and for cli * use latest cce fork for upstream changes * increase timeout --- .github/workflows/tests-nightly.yml | 4 ++-- examples/colab-notebooks/colab-axolotl-example.ipynb | 2 +- requirements.txt | 2 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/integrations/cut_cross_entropy/README.md | 2 +- src/axolotl/integrations/cut_cross_entropy/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index b5dd50a3c7..54d734e492 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -92,7 +92,7 @@ jobs: if: github.repository_owner == 'axolotl-ai-cloud' # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] - timeout-minutes: 60 + timeout-minutes: 120 needs: [pre-commit, pytest] strategy: @@ -116,7 +116,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==0.71.8 jinja2 + pip install modal==1.0.2 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index bcb99f19ea..112658007c 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@78b2a45713a54c9bedf8b33f5e31cf07a1a57154\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@50cef19\"" ] }, { diff --git a/requirements.txt b/requirements.txt index 215bc12718..85c7d02be7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ hf_transfer sentencepiece gradio==5.23.3 -modal==0.70.5 +modal==1.0.2 pydantic==2.10.6 addict fire diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 06bad8beff..6840aef50a 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@865b899"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@50cef19"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 3c0a393ca6..dc7c908ddc 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@865b899" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@50cef19" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 75b17580fd..a2f0d52d75 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -32,7 +32,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@865b899"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@50cef19"`' ) From 2c408b5c5eb2cc152e310ca22928eefaa91c3ee2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 15 Jul 2025 22:40:41 -0400 Subject: [PATCH 0814/1405] Apply generic fused liger ce, cce, and tiledmlp for arbitrary models (#2908) * Apply generic fused liger ce for unknown models * fix deepseek liger modeling * generic cce and config tiled mlp to use original mlp and auto detect compute params * fix weight and lint * update warnings * address PR feedback * use lookup for model class prefixes * revert inadvertent change to flash attn verison * remove un-needed pylint annotations * fix import --- .../cut_cross_entropy/__init__.py | 48 +++++ src/axolotl/integrations/kd/kernels/models.py | 4 +- src/axolotl/integrations/liger/__init__.py | 172 +--------------- src/axolotl/integrations/liger/models/base.py | 189 ++++++++++++++++++ src/axolotl/integrations/liger/plugin.py | 182 +++++++++++++++++ src/axolotl/loaders/patch_manager.py | 6 +- src/axolotl/monkeypatch/lora_kernels.py | 5 +- src/axolotl/monkeypatch/tiled_mlp.py | 18 +- src/axolotl/utils/callbacks/models.py | 23 +++ src/axolotl/utils/schemas/config.py | 7 + 10 files changed, 475 insertions(+), 179 deletions(-) create mode 100644 src/axolotl/integrations/liger/models/base.py create mode 100644 src/axolotl/integrations/liger/plugin.py create mode 100644 src/axolotl/utils/callbacks/models.py diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index a2f0d52d75..6c47097b73 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -19,11 +19,13 @@ from Apple's ML team. """ import importlib +from functools import partial import torch from axolotl.integrations.base import BasePlugin from axolotl.utils import get_pytorch_version +from axolotl.utils.callbacks.models import get_causal_lm_model_cls_prefix from axolotl.utils.logging import get_logger from .args import CutCrossEntropyArgs # pylint: disable=unused-import. # noqa: F401 @@ -84,6 +86,7 @@ def pre_model_load(self, cfg): """Apply cut cross entropy before model loading if enabled.""" if cfg.cut_cross_entropy: self._check_requirements() + self.patch_llama_like(cfg.model_config_type) from cut_cross_entropy.transformers.patch import cce_patch @@ -93,3 +96,48 @@ def pre_model_load(self, cfg): # The patch checks model_type internally cce_patch(cfg.model_config_type) + + def patch_llama_like( + self, + model_type: str, + ) -> None: + """ + Generic patch for model architectures with causal lm similar to llama + """ + from cut_cross_entropy.transformers.patch import PATCH_FNS + + def patch_generic( + maybe_model, patch_options, model_type: str + ): # pylint: disable=unused-argument + import cut_cross_entropy.transformers.llama + from cut_cross_entropy.transformers.llama import cce_forward + + try: + # Dynamically import the module and CausalLM class + module_path = f"transformers.models.{model_type}.modeling_{model_type}" + model_cls_prefix, _ = get_causal_lm_model_cls_prefix(model_type) + module = __import__( + module_path, fromlist=[f"{model_cls_prefix}ForCausalLM"] + ) + model_cls = getattr(module, f"{model_cls_prefix}ForCausalLM") + + cut_cross_entropy.transformers.llama._PATCH_OPTS = ( # pylint: disable=protected-access + patch_options + ) + + model_cls.forward = cce_forward + # pylint: disable=duplicate-code + except (ImportError, AttributeError) as e: + raise RuntimeError( + f"Could not import ForCausalLM class for model_type: {model_type}. " + f"Error: {str(e)}" + ) from e + + if model_type not in PATCH_FNS: + LOG.warning_once( + "Setting up generic cce patch for model type: %s", model_type + ) + LOG.warning_once( + f"Generic Cut Cross Entropy + {model_type} support is experimental and may not work as expected." + ) + PATCH_FNS[model_type] = partial(patch_generic, model_type=model_type) diff --git a/src/axolotl/integrations/kd/kernels/models.py b/src/axolotl/integrations/kd/kernels/models.py index 6a8b6da1c9..4319f5f7dd 100644 --- a/src/axolotl/integrations/kd/kernels/models.py +++ b/src/axolotl/integrations/kd/kernels/models.py @@ -22,6 +22,8 @@ class TransformersKwargs(FlashAttentionKwargs, LossKwargs): TransformersKwargs, ) +from axolotl.utils.callbacks.models import get_causal_lm_model_cls_prefix + def kldiv_forward_llama_like( self, @@ -97,7 +99,7 @@ def kldiv_forward_llama_like( def apply_kernel(model_type): # Dynamically import the module and attention class module_path = f"transformers.models.{model_type}.modeling_{model_type}" - model_cls_prefix = "".join([part.capitalize() for part in model_type.split("_")]) + model_cls_prefix, _ = get_causal_lm_model_cls_prefix(model_type) module = __import__(module_path, fromlist=[f"{model_cls_prefix}ForCausalLM"]) model_cls = getattr(module, f"{model_cls_prefix}ForCausalLM") model_cls.forward = kldiv_forward_llama_like diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index 8de94c78be..86d56be802 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -18,170 +18,10 @@ Liger Kernel is the collection of Triton-native kernels for LLM Training. It is designed to be performant, correct, and light-weight. """ -import inspect -import sys +from .args import LigerArgs +from .plugin import LigerPlugin -from axolotl.integrations.base import BasePlugin -from axolotl.utils.logging import get_logger - -from .args import LigerArgs # pylint: disable=unused-import. # noqa: F401 -from .utils import patch_with_compile_disable - -LOG = get_logger(__name__) - - -class LigerPlugin(BasePlugin): - """ - Plugin for LIGER integraton with Axolotl. - """ - - def get_input_args(self): - return "axolotl.integrations.liger.LigerArgs" - - def pre_model_load(self, cfg): - if cfg.torch_compile: - # torch compile will unnecessarily attempt to optimize the triton kernel unless explicitly disabled - import liger_kernel.ops.fused_linear_cross_entropy - - patch_with_compile_disable( - liger_kernel.ops.fused_linear_cross_entropy, - "fused_linear_cross_entropy_forward", - ) - patch_with_compile_disable( - liger_kernel.ops.fused_linear_cross_entropy, - "fused_linear_cross_entropy_backward", - ) - from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss - from liger_kernel.transformers.functional import liger_cross_entropy - from liger_kernel.transformers.layer_norm import LigerLayerNorm - from liger_kernel.transformers.monkey_patch import MODEL_TYPE_TO_APPLY_LIGER_FN - from liger_kernel.transformers.rms_norm import LigerRMSNorm - from liger_kernel.transformers.rope import liger_rotary_pos_emb - from liger_kernel.transformers.swiglu import LigerSwiGLUMLP - - if cfg.liger_cross_entropy and cfg.liger_fused_linear_cross_entropy: - raise ValueError( - "Cannot have both `liger_cross_entropy` and `liger_fused_linear_cross_entropy` set." - ) - - if cfg.model_config_type in MODEL_TYPE_TO_APPLY_LIGER_FN: - apply_liger_fn = MODEL_TYPE_TO_APPLY_LIGER_FN[cfg.model_config_type] - liger_fn_sig = inspect.signature(apply_liger_fn) - kwargs = {} - if "rope" in liger_fn_sig.parameters: - kwargs["rope"] = cfg.liger_rope - if "cross_entropy" in liger_fn_sig.parameters: - kwargs["cross_entropy"] = cfg.liger_cross_entropy - if "fused_linear_cross_entropy" in liger_fn_sig.parameters: - kwargs["fused_linear_cross_entropy"] = ( - cfg.liger_fused_linear_cross_entropy - ) - if "rms_norm" in liger_fn_sig.parameters: - kwargs["rms_norm"] = cfg.liger_rms_norm - if "layer_norm" in liger_fn_sig.parameters: - kwargs["layer_norm"] = cfg.liger_layer_norm - if "geglu" in liger_fn_sig.parameters: - kwargs["geglu"] = cfg.liger_glu_activation - elif "swiglu" in liger_fn_sig.parameters: - kwargs["swiglu"] = cfg.liger_glu_activation - LOG.info(f"Applying LIGER to {cfg.model_config_type} with kwargs: {kwargs}") - apply_liger_fn(**kwargs) - elif cfg.model_config_type == "jamba": - from transformers.models.jamba import modeling_jamba - - from .models.jamba import lce_forward as jamba_lce_forward - - if cfg.liger_rope: - modeling_jamba.apply_rotary_pos_emb = liger_rotary_pos_emb - if cfg.liger_rms_norm: - modeling_jamba.JambaRMSNorm = LigerRMSNorm - if cfg.liger_glu_activation: - modeling_jamba.JambaMLP = LigerSwiGLUMLP - if cfg.liger_layer_norm: - modeling_jamba.nn.LayerNorm = LigerLayerNorm - if cfg.liger_cross_entropy: - from transformers.loss.loss_utils import nn - - nn.functional.cross_entropy = liger_cross_entropy - if cfg.liger_fused_linear_cross_entropy: - modeling_jamba.JambaForCausalLM.forward = jamba_lce_forward - elif cfg.model_config_type == "deepseek_v2": - from accelerate import init_empty_weights - from transformers import AutoModelForCausalLM - - with init_empty_weights(): - model = AutoModelForCausalLM.from_pretrained( - cfg.base_model, trust_remote_code=cfg.trust_remote_code or False - ) - modeling_mod = sys.modules[model.__class__.__module__] - - from .models.deepseekv2 import lce_forward as deepseekv2_lce_forward - - if cfg.liger_rope: - # The DeepseekV2 version of RoPE is different than upstream LLaMA. - # See https://github.com/linkedin/Liger-Kernel/issues/129#issuecomment-2313763528 - LOG.warning("Fused liger_rope is not supported for DeepseekV2.") - if cfg.liger_glu_activation: - LOG.warning("liger_glu_activation is not supported for DeepseekV2.") - if cfg.liger_rms_norm: - modeling_mod.DeepseekV2RMSNorm = LigerRMSNorm - if cfg.liger_glu_activation: - modeling_mod.DeepseekV2MLP.forward = LigerSwiGLUMLP.forward - if cfg.liger_layer_norm: - modeling_mod.DeepseekV2MLP.forward = LigerLayerNorm.forward - if cfg.liger_cross_entropy: - # We do not patch `nn.functional.cross_entropy` for DeepseekV2 as it still uses - # nn.CrossEntropyLoss in the forward method. - modeling_mod.CrossEntropyLoss = LigerCrossEntropyLoss - if cfg.liger_fused_linear_cross_entropy: - modeling_mod.DeepseekV2ForCausalLM.forward = deepseekv2_lce_forward - elif cfg.model_config_type == "llama4": - from axolotl.integrations.liger.models.llama4 import ( - apply_liger_kernel_to_llama4, - ) - - apply_liger_kernel_to_llama4( - cross_entropy=cfg.liger_cross_entropy, - fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, - glu_activation=cfg.liger_glu_activation, - rms_norm=cfg.liger_rms_norm, - layer_norm=cfg.liger_layer_norm, - ) - elif cfg.model_config_type == "qwen3": - from axolotl.integrations.liger.models.qwen3 import ( - apply_liger_kernel_to_qwen3, - ) - - apply_liger_kernel_to_qwen3( - cross_entropy=cfg.liger_cross_entropy, - fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, - glu_activation=cfg.liger_glu_activation, - rms_norm=cfg.liger_rms_norm, - layer_norm=cfg.liger_layer_norm, - ) - elif cfg.model_config_type == "qwen3_moe": - from axolotl.integrations.liger.models.qwen3_moe import ( - apply_liger_kernel_to_qwen3_moe, - ) - - apply_liger_kernel_to_qwen3_moe( - cross_entropy=cfg.liger_cross_entropy, - fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, - glu_activation=cfg.liger_glu_activation, - rms_norm=cfg.liger_rms_norm, - layer_norm=cfg.liger_layer_norm, - ) - elif cfg.model_config_type == "granitemoe": - from liger_kernel.transformers import apply_liger_kernel_to_granite - - apply_liger_kernel_to_granite( - rope=cfg.liger_rope, - cross_entropy=cfg.liger_cross_entropy, - fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, - rms_norm=cfg.liger_rms_norm, - swiglu=cfg.liger_glu_activation, - ) - else: - LOG.warning( - f"Unsupported model config type: {cfg.model_config_type}. Liger not applied." - ) +__all__ = [ + "LigerArgs", + "LigerPlugin", +] diff --git a/src/axolotl/integrations/liger/models/base.py b/src/axolotl/integrations/liger/models/base.py new file mode 100644 index 0000000000..f3cf4299ad --- /dev/null +++ b/src/axolotl/integrations/liger/models/base.py @@ -0,0 +1,189 @@ +""" +Generic FLCE patch for untested models similar to Llama +""" + +from typing import Optional, Tuple, Union + +import torch +from liger_kernel.transformers.model.loss_utils import LigerForCausalLMLoss +from liger_kernel.transformers.trainer.orpo_trainer import _FSDPForwardRedirection +from liger_kernel.utils import PEFT_AVAILABLE +from peft.utils import ModulesToSaveWrapper +from torch.distributed.fsdp import FullyShardedDataParallel +from transformers.modeling_outputs import CausalLMOutputWithPast + +from axolotl.utils.callbacks.models import get_causal_lm_model_cls_prefix + + +def lce_forward( + self, + *args, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + labels: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + skip_logits: Optional[bool] = None, + **kwargs, +) -> Union[Tuple, CausalLMOutputWithPast]: + r""" + Args: + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + """ + + # pylint: disable=duplicate-code + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + *args, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + **kwargs, + ) + + hidden_states = outputs[0] + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + kept_hidden_states = hidden_states[:, slice_indices, :] + + shift_labels = kwargs.pop("shift_labels", None) + logits = None + loss = None + + # if in training mode, don't materialize logits + if skip_logits and labels is None and shift_labels is None: + raise ValueError("skip_logits is True, but labels and shift_labels are None") + + if skip_logits is None: + # By default, if in training mode, don't materialize logits + skip_logits = self.training and (labels is not None or shift_labels is not None) + + if skip_logits: + loss = lce_maybe_trainable_lm_head( + self, + hidden_states=kept_hidden_states, + hidden_size=self.config.hidden_size, + labels=labels, + shift_labels=shift_labels, + **kwargs, + ) + + else: + logits = self.lm_head(kept_hidden_states) + if labels is not None: + loss = self.loss_function( + logits=logits, + labels=labels, + vocab_size=self.config.vocab_size, + **kwargs, + ) + + if not return_dict: + output = (logits,) + outputs[1:] + return (loss,) + output if loss is not None else output + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +def lce_maybe_trainable_lm_head( + self, hidden_states, hidden_size, labels, shift_labels, **loss_kwargs +): + lm_head = self.lm_head + + # Unwrap the module if lm_head has been added as trainable module in PEFT LoRA configuration, + # i.e. listed in the modules_to_save field of LoraConfig, so the lm_head weights are read + # from the unwrapped module. + # See https://huggingface.co/docs/peft/package_reference/lora for reference. + if PEFT_AVAILABLE and isinstance(lm_head, ModulesToSaveWrapper): + lm_head = lm_head.modules_to_save.default + + # If FSDP is used and lm_head is trainable, e.g., during full fine-tuning or with LoRA, + # reading the lm_head module weights and calling the kernel must be done within FSDP forward pass + # so the module entire parameters are summoned and kept in memory during the kernel execution. + if isinstance(lm_head, FullyShardedDataParallel): + return _FSDPForwardRedirection()( + lm_head, + _liger_for_causal_lm_loss, + lm_head.module, + hidden_states, + hidden_size, + labels, + shift_labels, + **loss_kwargs, + ) + + # FSDP is not used so we can read the lm_head weights and call the kernel directly + return _liger_for_causal_lm_loss( + lm_head=self.lm_head, + hidden_states=hidden_states, + hidden_size=hidden_size, + labels=labels, + shift_labels=shift_labels, + **loss_kwargs, + ) + + +def _liger_for_causal_lm_loss( + lm_head, hidden_states, hidden_size, labels, shift_labels, **loss_kwargs +): + return LigerForCausalLMLoss( + hidden_states=hidden_states, + lm_head_weight=lm_head.weight, + labels=labels, + hidden_size=hidden_size, + shift_labels=shift_labels, + **loss_kwargs, + ) + + +def patch_lce_forward( + model_type, +): + try: + # Dynamically import the module and MLP class + module_path = f"transformers.models.{model_type}.modeling_{model_type}" + model_cls_prefix, _ = get_causal_lm_model_cls_prefix(model_type) + module = __import__(module_path, fromlist=[f"{model_cls_prefix}ForCausalLM"]) + model_cls = getattr(module, f"{model_cls_prefix}ForCausalLM") + + model_cls.forward = lce_forward + # pylint: disable=duplicate-code + except (ImportError, AttributeError) as e: + raise RuntimeError( + f"Could not import ForCausalLM class for model_type: {model_type}. " + f"Error: {str(e)}" + ) from e diff --git a/src/axolotl/integrations/liger/plugin.py b/src/axolotl/integrations/liger/plugin.py new file mode 100644 index 0000000000..89f7c37b71 --- /dev/null +++ b/src/axolotl/integrations/liger/plugin.py @@ -0,0 +1,182 @@ +""" +Liger-Kernel Plugin for Axolotl +""" + +import inspect +import sys + +from axolotl.integrations.base import BasePlugin +from axolotl.utils.logging import get_logger + +from .models.base import patch_lce_forward +from .utils import patch_with_compile_disable + +LOG = get_logger(__name__) + + +class LigerPlugin(BasePlugin): + """ + Plugin for LIGER integraton with Axolotl. + """ + + def get_input_args(self): + return "axolotl.integrations.liger.LigerArgs" + + def pre_model_load(self, cfg): + if cfg.torch_compile: + # torch compile will unnecessarily attempt to optimize the triton kernel unless explicitly disabled + import liger_kernel.ops.fused_linear_cross_entropy + + patch_with_compile_disable( + liger_kernel.ops.fused_linear_cross_entropy, + "fused_linear_cross_entropy_forward", + ) + patch_with_compile_disable( + liger_kernel.ops.fused_linear_cross_entropy, + "fused_linear_cross_entropy_backward", + ) + from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss + from liger_kernel.transformers.functional import liger_cross_entropy + from liger_kernel.transformers.layer_norm import LigerLayerNorm + from liger_kernel.transformers.monkey_patch import MODEL_TYPE_TO_APPLY_LIGER_FN + from liger_kernel.transformers.rms_norm import LigerRMSNorm + from liger_kernel.transformers.rope import liger_rotary_pos_emb + from liger_kernel.transformers.swiglu import LigerSwiGLUMLP + + if cfg.liger_cross_entropy and cfg.liger_fused_linear_cross_entropy: + raise ValueError( + "Cannot have both `liger_cross_entropy` and `liger_fused_linear_cross_entropy` set." + ) + + if cfg.model_config_type in MODEL_TYPE_TO_APPLY_LIGER_FN: + apply_liger_fn = MODEL_TYPE_TO_APPLY_LIGER_FN[cfg.model_config_type] + liger_fn_sig = inspect.signature(apply_liger_fn) + kwargs = {} + if "rope" in liger_fn_sig.parameters: + kwargs["rope"] = cfg.liger_rope + if "cross_entropy" in liger_fn_sig.parameters: + kwargs["cross_entropy"] = cfg.liger_cross_entropy + if "fused_linear_cross_entropy" in liger_fn_sig.parameters: + kwargs["fused_linear_cross_entropy"] = ( + cfg.liger_fused_linear_cross_entropy + ) + if "rms_norm" in liger_fn_sig.parameters: + kwargs["rms_norm"] = cfg.liger_rms_norm + if "layer_norm" in liger_fn_sig.parameters: + kwargs["layer_norm"] = cfg.liger_layer_norm + if "geglu" in liger_fn_sig.parameters: + kwargs["geglu"] = cfg.liger_glu_activation + elif "swiglu" in liger_fn_sig.parameters: + kwargs["swiglu"] = cfg.liger_glu_activation + LOG.info(f"Applying LIGER to {cfg.model_config_type} with kwargs: {kwargs}") + apply_liger_fn(**kwargs) + elif cfg.model_config_type == "jamba": + from transformers.models.jamba import modeling_jamba + + from .models.jamba import lce_forward as jamba_lce_forward + + if cfg.liger_rope: + modeling_jamba.apply_rotary_pos_emb = liger_rotary_pos_emb + if cfg.liger_rms_norm: + modeling_jamba.JambaRMSNorm = LigerRMSNorm + if cfg.liger_glu_activation: + modeling_jamba.JambaMLP = LigerSwiGLUMLP + if cfg.liger_layer_norm: + modeling_jamba.nn.LayerNorm = LigerLayerNorm + if cfg.liger_cross_entropy: + from transformers.loss.loss_utils import nn + + nn.functional.cross_entropy = liger_cross_entropy + if cfg.liger_fused_linear_cross_entropy: + modeling_jamba.JambaForCausalLM.forward = jamba_lce_forward + elif cfg.model_config_type == "deepseek_v2": + from accelerate import init_empty_weights + from transformers import AutoModelForCausalLM + + with init_empty_weights(): + model = AutoModelForCausalLM.from_pretrained( + cfg.base_model, trust_remote_code=cfg.trust_remote_code or False + ) + modeling_mod = sys.modules[model.__class__.__module__] + + from .models.deepseekv2 import lce_forward as deepseekv2_lce_forward + + if cfg.liger_rope: + # The DeepseekV2 version of RoPE is different than upstream LLaMA. + # See https://github.com/linkedin/Liger-Kernel/issues/129#issuecomment-2313763528 + LOG.warning("Fused liger_rope is not supported for DeepseekV2.") + if cfg.liger_rms_norm: + modeling_mod.DeepseekV2RMSNorm = LigerRMSNorm + if cfg.liger_glu_activation: + modeling_mod.DeepseekV2MLP.forward = LigerSwiGLUMLP.forward + if cfg.liger_layer_norm: + LOG.warning("liger_layer_norm is not supported for DeepseekV2.") + if cfg.liger_cross_entropy: + # We do not patch `nn.functional.cross_entropy` for DeepseekV2 as it still uses + # nn.CrossEntropyLoss in the forward method. + modeling_mod.CrossEntropyLoss = LigerCrossEntropyLoss + if cfg.liger_fused_linear_cross_entropy: + modeling_mod.DeepseekV2ForCausalLM.forward = deepseekv2_lce_forward + elif cfg.model_config_type == "llama4": + from axolotl.integrations.liger.models.llama4 import ( + apply_liger_kernel_to_llama4, + ) + + apply_liger_kernel_to_llama4( + cross_entropy=cfg.liger_cross_entropy, + fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, + glu_activation=cfg.liger_glu_activation, + rms_norm=cfg.liger_rms_norm, + layer_norm=cfg.liger_layer_norm, + ) + elif cfg.model_config_type == "qwen3": + from axolotl.integrations.liger.models.qwen3 import ( + apply_liger_kernel_to_qwen3, + ) + + apply_liger_kernel_to_qwen3( + cross_entropy=cfg.liger_cross_entropy, + fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, + glu_activation=cfg.liger_glu_activation, + rms_norm=cfg.liger_rms_norm, + layer_norm=cfg.liger_layer_norm, + ) + elif cfg.model_config_type == "qwen3_moe": + from axolotl.integrations.liger.models.qwen3_moe import ( + apply_liger_kernel_to_qwen3_moe, + ) + + apply_liger_kernel_to_qwen3_moe( + cross_entropy=cfg.liger_cross_entropy, + fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, + glu_activation=cfg.liger_glu_activation, + rms_norm=cfg.liger_rms_norm, + layer_norm=cfg.liger_layer_norm, + ) + elif cfg.model_config_type == "granitemoe": + from liger_kernel.transformers import apply_liger_kernel_to_granite + + apply_liger_kernel_to_granite( + rope=cfg.liger_rope, + cross_entropy=cfg.liger_cross_entropy, + fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, + rms_norm=cfg.liger_rms_norm, + swiglu=cfg.liger_glu_activation, + ) + elif cfg.liger_fused_linear_cross_entropy: + try: + patch_lce_forward(cfg.model_config_type) + LOG.warning_once( + f"Applied ONLY liger_fused_linear_cross_entropy genericpatches for model type: {cfg.model_config_type}" + ) + LOG.warning_once( + f"Liger + {cfg.model_config_type} generic FLCE support is experimental and may not work as expected." + ) + except RuntimeError: + LOG.warning( + f"Unsupported model config type: {cfg.model_config_type}. Liger not applied." + ) + else: + LOG.warning( + f"Unsupported model config type: {cfg.model_config_type}. Liger not applied." + ) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 84e6b33def..f346c56e04 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -272,7 +272,11 @@ def _apply_tiled_mlp(self, model_type: str): if self.cfg.tiled_mlp: from axolotl.monkeypatch.tiled_mlp import patch_tiled_mlp - patch_tiled_mlp(model_type, cfg_num_shards=self.cfg.tiled_mlp_num_shards) + patch_tiled_mlp( + model_type, + use_original_mlp=self.cfg.tiled_mlp_use_original_mlp, + cfg_num_shards=self.cfg.tiled_mlp_num_shards, + ) def _patch_attention(self): """Apply attention-specific patches based on model type.""" diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 586412dd78..4702ad19d6 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -18,6 +18,7 @@ apply_lora_qkv, ) from axolotl.monkeypatch.utils import detab_code +from axolotl.utils.callbacks.models import get_causal_lm_model_cls_prefix from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger @@ -153,9 +154,7 @@ def get_attention_cls_from_config(cfg: DictDefault) -> Type[nn.Module]: try: # Dynamically import the module and attention class module_path = f"transformers.models.{model_type}.modeling_{model_type}" - model_cls_prefix = "".join( - [part.capitalize() for part in model_type.split("_")] - ) + model_cls_prefix, _ = get_causal_lm_model_cls_prefix(model_type) module = __import__(module_path, fromlist=[f"{model_cls_prefix}Attention"]) attention_cls = getattr(module, f"{model_cls_prefix}Attention") diff --git a/src/axolotl/monkeypatch/tiled_mlp.py b/src/axolotl/monkeypatch/tiled_mlp.py index 99a10df9c0..3818c6b355 100644 --- a/src/axolotl/monkeypatch/tiled_mlp.py +++ b/src/axolotl/monkeypatch/tiled_mlp.py @@ -6,6 +6,8 @@ import torch import torch.distributed as dist +from axolotl.utils.callbacks.models import get_causal_lm_model_cls_prefix + def patch_tiled_mlp(model_type, use_original_mlp=False, cfg_num_shards=None): from deepspeed.runtime.sequence_parallel.ulysses_sp import TiledMLP @@ -13,9 +15,7 @@ def patch_tiled_mlp(model_type, use_original_mlp=False, cfg_num_shards=None): try: # Dynamically import the module and MLP class module_path = f"transformers.models.{model_type}.modeling_{model_type}" - model_cls_prefix = "".join( - [part.capitalize() for part in model_type.split("_")] - ) + model_cls_prefix, _ = get_causal_lm_model_cls_prefix(model_type) module = __import__(module_path, fromlist=[f"{model_cls_prefix}MLP"]) mlp_cls = getattr(module, f"{model_cls_prefix}MLP") @@ -45,11 +45,12 @@ def tiled_mlp_forward(self, x): else: num_shards = cfg_num_shards - compute_params = [ - self.down_proj.weight, - self.gate_proj.weight, - self.up_proj.weight, - ] + if not self._compute_params: # pylint: disable=protected-access + self._compute_params = [ # pylint: disable=protected-access + p for p in self.parameters() if p.requires_grad + ] + + compute_params = self._compute_params # pylint: disable=protected-access down_res = TiledMLP.apply( mlp_forward, @@ -61,6 +62,7 @@ def tiled_mlp_forward(self, x): return down_res mlp_cls.forward = tiled_mlp_forward + mlp_cls._compute_params = [] # pylint: disable=protected-access except (ImportError, AttributeError) as e: raise RuntimeError( f"Could not import MLP class for model_type: {model_type}. " diff --git a/src/axolotl/utils/callbacks/models.py b/src/axolotl/utils/callbacks/models.py new file mode 100644 index 0000000000..5a20d70d9c --- /dev/null +++ b/src/axolotl/utils/callbacks/models.py @@ -0,0 +1,23 @@ +"""Helper functions for model classes""" + +from typing import Tuple + +from transformers.models.auto.modeling_auto import MODEL_FOR_CAUSAL_LM_MAPPING_NAMES + + +def get_causal_lm_model_cls_prefix(model_type: str) -> Tuple[str, str]: + if model_type in MODEL_FOR_CAUSAL_LM_MAPPING_NAMES: + causal_lm_cls = MODEL_FOR_CAUSAL_LM_MAPPING_NAMES[model_type] + causal_lm_cls_prefix = causal_lm_cls + for suffix in [ + "ForCausalLM", + "ForConditionalGeneration", + "LMHeadModel", + "GenerationDecoder", + ]: + causal_lm_cls_prefix = causal_lm_cls_prefix.replace(suffix, "") + return causal_lm_cls_prefix, causal_lm_cls + causal_lm_cls_prefix = "".join( + [part.capitalize() for part in model_type.split("_")] + ) + return causal_lm_cls_prefix, f"{causal_lm_cls_prefix}ForCausalLM" diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index e20cdaf47b..06212a27f0 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -576,6 +576,13 @@ class AxolotlInputConfig( }, ) + tiled_mlp_use_original_mlp: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use original mlp for ALST tiled mlp. Otherwise uses a generic MLP based on llama." + }, + ) + llama4_linearized_experts: bool | None = None deepspeed: str | dict[str, Any] | None = Field( From 36cbe13d18514bd71f31c1b77fcb3fc5160838cf Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 16 Jul 2025 11:59:20 -0400 Subject: [PATCH 0815/1405] activation offloading with cuda streams doesn't work with LoRA (#2927) --- src/axolotl/utils/schemas/validation.py | 35 ++++--- .../validation/test_activation_offloading.py | 91 +++++++++++++++++++ 2 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 tests/utils/schemas/validation/test_activation_offloading.py diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 56a70ec486..292159bb82 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1066,23 +1066,23 @@ def check_mpt_checkpointing(self): raise ValueError("gradient_checkpointing is not supported for MPT models") return self - @model_validator(mode="after") - def check_offload_grad_checkpointing(self): - if self.gradient_checkpointing and self.gradient_checkpointing == "unsloth": - LOG.warning( - "`unsloth` is deprecated for gradient_checkpointing, use `offload`" - ) - self.gradient_checkpointing = "offload" - return self - @model_validator(mode="after") def check_gradient_checkpointing_w_offload(self): if self.gradient_checkpointing == "offload": LOG.warning( - "`offload` is deprecated for gradient_checkpointing, use `activation_offloading: true`" + "`offload` is deprecated for gradient_checkpointing, use `activation_offloading: true` or `activation_offloading: legacy`" ) self.gradient_checkpointing = True - self.activation_offloading = True + if self.adapter and "lora" in self.adapter: + LOG.warning( + "offloading with CUDA streams is not supported for LoRA adapters, using the `activation_offloading: legacy` implementation." + ) + self.activation_offloading = "legacy" + else: + LOG.warning( + "`offload` uses a new stream implementation; to use the previous implementation, use `activation_offloading: legacy`" + ) + self.activation_offloading = True if self.gradient_checkpointing == "offload_disk": LOG.warning( "`offload_disk` is deprecated for gradient_checkpointing, use `activation_offloading: disk`" @@ -1091,6 +1091,19 @@ def check_gradient_checkpointing_w_offload(self): self.activation_offloading = "disk" return self + @model_validator(mode="after") + def check_activation_offloading_w_lora(self): + if ( + self.activation_offloading is True + and self.adapter + and "lora" in self.adapter + ): + LOG.warning( + "activation_offloading with CUDA streams is not supported for LoRA adapters. Setting `activation_offloading: legacy`" + ) + self.activation_offloading = "legacy" + return self + @model_validator(mode="after") def check_activation_offloading_wo_gc(self): if self.activation_offloading and not self.gradient_checkpointing: diff --git a/tests/utils/schemas/validation/test_activation_offloading.py b/tests/utils/schemas/validation/test_activation_offloading.py new file mode 100644 index 0000000000..92ac8f45c7 --- /dev/null +++ b/tests/utils/schemas/validation/test_activation_offloading.py @@ -0,0 +1,91 @@ +"""Test for config validation for activation offloading.""" + +from axolotl.utils.config import validate_config +from axolotl.utils.dict import DictDefault + + +class TestActivationOffloading: + """ + Test cases for activation offloading schema validation + """ + + def test_gc_converts_offload_wo_lora(self, min_base_cfg): + cfg = ( + DictDefault( + gradient_checkpointing="offload", + ) + | min_base_cfg + ) + + cfg = validate_config(cfg) + assert cfg.gradient_checkpointing is True + assert cfg.activation_offloading is True + + def test_gc_converts_offload_w_lora(self, min_base_cfg): + cfg = ( + DictDefault( + gradient_checkpointing="offload", + adapter="lora", + ) + | min_base_cfg + ) + + cfg = validate_config(cfg) + assert cfg.gradient_checkpointing is True + assert cfg.activation_offloading == "legacy" + + def test_gc_converts_offload_w_qlora(self, min_base_cfg): + cfg = ( + DictDefault( + gradient_checkpointing="offload", + adapter="qlora", + load_in_4bit=True, + ) + | min_base_cfg + ) + + cfg = validate_config(cfg) + assert cfg.gradient_checkpointing is True + assert cfg.activation_offloading == "legacy" + + def test_ac_impl_changes_w_lora(self, min_base_cfg): + cfg = ( + DictDefault( + gradient_checkpointing=True, + activation_offloading=True, + adapter="lora", + ) + | min_base_cfg + ) + + cfg = validate_config(cfg) + assert cfg.gradient_checkpointing is True + assert cfg.activation_offloading == "legacy" + + def test_ac_impl_changes_w_qlora(self, min_base_cfg): + cfg = ( + DictDefault( + gradient_checkpointing=True, + activation_offloading=True, + adapter="qlora", + load_in_4bit=True, + ) + | min_base_cfg + ) + + cfg = validate_config(cfg) + assert cfg.gradient_checkpointing is True + assert cfg.activation_offloading == "legacy" + + def test_ac_offload_impl_noop_wo_adapter(self, min_base_cfg): + cfg = ( + DictDefault( + gradient_checkpointing=True, + activation_offloading=True, + ) + | min_base_cfg + ) + + cfg = validate_config(cfg) + assert cfg.gradient_checkpointing is True + assert cfg.activation_offloading is True From d2c3d5a954b47193e9224db6f548e91cbb7a9eda Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 16 Jul 2025 21:45:42 -0400 Subject: [PATCH 0816/1405] run nightly-vs-upstream-main on 2.7.1 and multi-gpu also (#2929) [skip ci] --- .github/workflows/tests-nightly.yml | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 54d734e492..00b6242433 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -106,6 +106,13 @@ jobs: num_gpus: 1 axolotl_extras: nightly_build: "true" + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.1 + num_gpus: 1 + axolotl_extras: + nightly_build: "true" steps: - name: Checkout uses: actions/checkout@v4 @@ -130,3 +137,45 @@ jobs: - name: Run tests job on Modal run: | modal run cicd.e2e_tests + docker-e2e-multigpu-tests: + if: github.repository_owner == 'axolotl-ai-cloud' + # this job needs to be run on self-hosted GPU runners... + runs-on: [self-hosted, modal] + timeout-minutes: 120 + needs: [pre-commit, pytest, docker-e2e-tests] + + strategy: + fail-fast: false + matrix: + include: + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.1 + num_gpus: 2 + axolotl_extras: + nightly_build: "true" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install Modal + run: | + python -m pip install --upgrade pip + pip install modal==1.0.2 jinja2 + - name: Update env vars + run: | + echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV + echo "PYTORCH_VERSION=${{ matrix.pytorch}}" >> $GITHUB_ENV + echo "AXOLOTL_ARGS=${{ matrix.axolotl_args}}" >> $GITHUB_ENV + echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV + echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV + echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV + echo "NIGHTLY_BUILD=${{ matrix.nightly_build }}" >> $GITHUB_ENV + echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV + - name: Run tests job on Modal + run: | + modal run cicd.multigpu From 8a4bcacdb204c9cce04ff005d51735bf76b539c1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 17 Jul 2025 00:01:23 -0400 Subject: [PATCH 0817/1405] cu126-torch271 for cloud docker image should be tagged with main-latest (#2935) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8692496f1f..444ebfde80 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -87,7 +87,6 @@ jobs: python_version: "3.11" pytorch: 2.6.0 axolotl_extras: - is_latest: true - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" @@ -98,6 +97,7 @@ jobs: python_version: "3.11" pytorch: 2.7.1 axolotl_extras: + is_latest: true - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" From f2474ef941ddcb3cc7605389704f1a0eab9d2d55 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 17 Jul 2025 09:46:43 -0400 Subject: [PATCH 0818/1405] bump accelerate to 1.9.0 (#2936) [skip ci] --- requirements.txt | 2 +- src/axolotl/loaders/adapters/__init__.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/axolotl/loaders/adapters/__init__.py diff --git a/requirements.txt b/requirements.txt index 85c7d02be7..6c167d3fa7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ huggingface_hub>=0.33.0 peft==0.16.0 transformers==4.53.2 tokenizers>=0.21.1 -accelerate==1.8.1 +accelerate==1.9.0 datasets==4.0.0 deepspeed>=0.17.0 trl==0.19.1 diff --git a/src/axolotl/loaders/adapters/__init__.py b/src/axolotl/loaders/adapters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 9dde9e1b71ba45709ea7f396268706de5a85ce53 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 17 Jul 2025 09:47:45 -0400 Subject: [PATCH 0819/1405] misc fixes 202507 (#2937) [skip ci] * misc fixes 202507 * manually handle attn class for llama4 --- codecov.yml | 1 + src/axolotl/monkeypatch/lora_kernels.py | 5 +++++ src/axolotl/utils/data/shared.py | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index 2741b17582..28921f9be2 100644 --- a/codecov.yml +++ b/codecov.yml @@ -22,6 +22,7 @@ coverage: only_pulls: true flags: null paths: null + informational: true patch: default: # basic diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 4702ad19d6..48bc10c0ba 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -151,6 +151,11 @@ def get_attention_cls_from_config(cfg: DictDefault) -> Type[nn.Module]: return MllamaTextSelfAttention + if model_type == "llama4": + from transformers.models.llama4.modeling_llama4 import Llama4TextAttention + + return Llama4TextAttention + try: # Dynamically import the module and attention class module_path = f"transformers.models.{model_type}.modeling_{model_type}" diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index c3c70545c8..c30459d5bd 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -460,13 +460,13 @@ def load_preprocessed_dataset(cfg: DictDefault, dataset_hash: str) -> Dataset | ): LOG.info( f"Loading prepared dataset from disk at {prepared_ds_path}...", - main_process_only=False, + main_process_only=True, ) return load_from_disk(str(prepared_ds_path)) LOG.info( f"Unable to find prepared dataset in {prepared_ds_path}", - main_process_only=False, + main_process_only=True, ) return None From 9f2bb188a4e21b72802807164f451a311e4d581b Mon Sep 17 00:00:00 2001 From: Varun Gumma <45076943+VarunGumma@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:17:58 +0530 Subject: [PATCH 0820/1405] Improve Dataset Processing Multiprocessing, Sharding, and Qwen Tokenizer Bug Fix. (#2918) * Added a feature to save prepared dataset in specified shards, removed limiter on multiprocessing during tokenization, and a bug fix of qwen tokenizer * removed limiters and fixed config variable name * black lint * chore: lint * feat: update handling of dataset_processes --------- Co-authored-by: NanoCode012 --- src/axolotl/core/datasets/chat.py | 7 +------ src/axolotl/datasets.py | 7 ++----- src/axolotl/loaders/tokenizer.py | 3 ++- src/axolotl/utils/config/__init__.py | 2 -- src/axolotl/utils/data/shared.py | 17 +++++++++++++---- src/axolotl/utils/schemas/config.py | 28 ++++++++++++++++++++++++---- 6 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/axolotl/core/datasets/chat.py b/src/axolotl/core/datasets/chat.py index 724f128666..a4dc300d9c 100644 --- a/src/axolotl/core/datasets/chat.py +++ b/src/axolotl/core/datasets/chat.py @@ -2,7 +2,6 @@ chat dataset module """ -import os from typing import Callable, Optional, Union from datasets import Dataset @@ -41,14 +40,10 @@ def map_fn(ex): ) return ex.tokenized(model_transform) - process_or_cpu_count: int = ( - process_count or os.cpu_count() # type: ignore[assignment] - ) - num_proc = min(32, process_or_cpu_count) features = data.features.keys() tokenized_data = data.map( map_fn, - num_proc=num_proc, + num_proc=process_count, keep_in_memory=keep_in_memory, remove_columns=features, desc="Tokenizing Chats", diff --git a/src/axolotl/datasets.py b/src/axolotl/datasets.py index 7c112c59e7..c9d006ac8d 100644 --- a/src/axolotl/datasets.py +++ b/src/axolotl/datasets.py @@ -1,7 +1,5 @@ """Module containing Dataset functionality""" -import os - import torch from datasets import Dataset, IterableDataset @@ -46,7 +44,6 @@ def __init__( # pylint: disable=super-init-not-called def process(self, dataset): features = dataset.features.keys() - num_proc = min(64, self.process_count if self.process_count else os.cpu_count()) map_kwargs = {} if self.prompt_tokenizer.supports_batched: @@ -59,13 +56,13 @@ def process(self, dataset): ): dataset = dataset.filter( self.prompt_tokenizer.filter_rows, - num_proc=num_proc, + num_proc=self.process_count, desc="Strategy Filtering Rows", ) return dataset.map( self.prompt_tokenizer.tokenize_prompt, - num_proc=num_proc, + num_proc=self.process_count, remove_columns=features, keep_in_memory=self.keep_in_memory, desc="Tokenizing Prompts", diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index 9fdb7d5cc1..2f0ccbcbb0 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -188,7 +188,8 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): tokenizer.padding_side = "left" # Qwen base only has single token, so we need to set the special tokens - if cfg.is_qwen_derived_model: + # the following check is for Qwen1 base models + if cfg.is_qwen_derived_model and hasattr(tokenizer, "eod_id"): token_ids = ["bos_token_id", "eos_token_id", "pad_token_id", "unk_token_id"] for attr_name in token_ids: if getattr(tokenizer, attr_name) is None: diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index aaa203e82a..c9613c39b1 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -148,8 +148,6 @@ def normalize_config(cfg): f"Invalid value for eval_steps ({eval_steps}) from evals_per_epoch and/or num_epochs. Skipping evaluations." ) - cfg.dataset_processes = cfg.dataset_processes or os.cpu_count() - if not cfg.base_model_config: cfg.base_model_config = cfg.base_model diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index c30459d5bd..3a36572401 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -410,9 +410,8 @@ def save_preprocessed_dataset( ) -> None: """Save preprocessed dataset to disk and optionally push to the HF Hub.""" prepared_ds_path = get_prepared_dataset_path(cfg, dataset_hash) + num_workers = cfg.dataset_processes if isinstance(dataset, IterableDataset): - num_workers = cfg.dataset_processes - ds_from_iter = Dataset.from_generator( functools.partial(_generate_from_iterable_dataset, dataset), features=dataset.features, @@ -423,10 +422,20 @@ def save_preprocessed_dataset( "num_workers": [num_workers] * num_workers, }, ) - ds_from_iter.save_to_disk(str(prepared_ds_path)) + ds_from_iter.save_to_disk( + str(prepared_ds_path), + num_proc=num_workers, + max_shard_size=None, + num_shards=cfg.num_dataset_shards_to_save, + ) else: os.makedirs(prepared_ds_path, exist_ok=True) - dataset.save_to_disk(str(prepared_ds_path)) + dataset.save_to_disk( + str(prepared_ds_path), + num_proc=num_workers, + max_shard_size=None, + num_shards=cfg.num_dataset_shards_to_save, + ) if cfg.push_dataset_to_hub: LOG.info( "Pushing merged prepared dataset to Huggingface hub at " diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 06212a27f0..d3fb0b14cd 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -193,6 +193,12 @@ class AxolotlInputConfig( json_schema_extra={"description": "Index of shard to use for whole dataset"}, ) skip_prepare_dataset: bool | None = False + num_dataset_shards_to_save: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of shards to save the prepared dataset" + }, + ) pretraining_dataset: ( Annotated[list[PretrainingDataset | SFTDataset], MinLen(1)] | None @@ -203,11 +209,12 @@ class AxolotlInputConfig( }, ) dataset_processes: int | None = Field( - default=min( - int(os.environ.get("AXOLOTL_DATASET_PROCESSES", 32)), os.cpu_count() - ), # type: ignore[type-var] + default=None, json_schema_extra={ - "description": "The maximum number of processes to use while preprocessing your input dataset. This defaults to `os.cpu_count()` if not set." + "description": ( + "The maximum number of processes to use while preprocessing your input dataset. This defaults to `os.cpu_count()` if not set.\n" + "For Runpod VMs, it will default to number of vCPUs via RUNPOD_CPU_COUNT." + ) }, ) dataset_exact_deduplication: bool | None = Field( @@ -1199,3 +1206,16 @@ def default_dataloader_opts(cls, data): data["dataloader_prefetch_factor"] = 256 return data + + @model_validator(mode="before") + @classmethod + def default_dataset_processes(cls, data): + if data.get("dataset_processes") is None: + if axolotl_dataset_processes := os.environ.get("AXOLOTL_DATASET_PROCESSES"): + data["dataset_processes"] = int(axolotl_dataset_processes) + elif runpod_cpu_count := os.environ.get("RUNPOD_CPU_COUNT"): + data["dataset_processes"] = int(runpod_cpu_count) + else: + data["dataset_processes"] = os.cpu_count() + + return data From 8e413172508ddd46ceb4fad690f202f36c10b27a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 17 Jul 2025 15:31:21 -0400 Subject: [PATCH 0821/1405] don't use include_tokens_per_second for GRPO (#2931) [skip ci] * don't use include_tokens_per_second for GRPO * use blocklist instead --- src/axolotl/core/trainers/grpo/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 771f788fe7..3e824f705d 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -148,7 +148,7 @@ def get_collator(cls, *args, **kwargs): # pylint: disable=unused-argument @classmethod def get_blocklist_args_kwargs(cls) -> list[str]: - return ["dataset_num_proc", "max_length"] + return ["dataset_num_proc", "max_length", "include_tokens_per_second"] @classmethod def get_reward_func(cls, reward_func_fqn: str) -> RewardFunc: From d23f972602ac50882c123a91210547e9c9f3bfd8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 17 Jul 2025 15:31:56 -0400 Subject: [PATCH 0822/1405] use state for wandb in callbacks (#2930) [skip ci] --- src/axolotl/utils/callbacks/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index bb777fc90f..b0738bf27d 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -798,7 +798,7 @@ def on_train_begin( control: TrainerControl, **kwargs, # pylint: disable=unused-argument ): - if is_main_process(): + if state.is_world_process_zero: try: # sync config to top level in run, cannot delete file right away because wandb schedules it to be synced even w/policy = 'now', so let OS delete it later. with NamedTemporaryFile( From a798975b7cd3964b9dce722ec1a3b8f9748d0282 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 17 Jul 2025 15:32:16 -0400 Subject: [PATCH 0823/1405] coderabbit manual settings (#2940) [skip ci] --- .coderabbit.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000000..95c044f02b --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: "en-US" +early_access: false +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: true + review_status: true + collapse_walkthrough: true + poem: false + sequence_diagrams: false + auto_review: + enabled: true + drafts: false +chat: + auto_reply: true From 5f5ae7621354d4f4bf61490e3d7e82a4a873ca59 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 17 Jul 2025 15:32:38 -0400 Subject: [PATCH 0824/1405] add validation around cce + chunked_ce (#2932) [skip ci] * add validation around cce + chunked_ce * return on end of validation method --- src/axolotl/integrations/cut_cross_entropy/args.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/axolotl/integrations/cut_cross_entropy/args.py b/src/axolotl/integrations/cut_cross_entropy/args.py index 2729ebe2e3..22852479a1 100644 --- a/src/axolotl/integrations/cut_cross_entropy/args.py +++ b/src/axolotl/integrations/cut_cross_entropy/args.py @@ -41,3 +41,13 @@ def check_dtype_is_half(cls, data): ) return data + + @model_validator(mode="before") + @classmethod + def check_chunked_cross_entropy_not_set(cls, data): + if data.get("chunked_cross_entropy"): + raise ValueError( + "Cut Cross Entropy does not support chunked cross entropy. " + "Please set `chunked_cross_entropy` to `False` or disable Cut Cross Entropy." + ) + return data From 170322a1f0e099bd8e184d2d352cf2f60437527c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 17 Jul 2025 15:32:55 -0400 Subject: [PATCH 0825/1405] make sure log level is upper (#2934) --- src/axolotl/logging_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/logging_config.py b/src/axolotl/logging_config.py index f570e013c1..10c5ae9dc3 100644 --- a/src/axolotl/logging_config.py +++ b/src/axolotl/logging_config.py @@ -113,7 +113,7 @@ def format(self, record): "loggers": { "axolotl": { "handlers": ["color_console"], - "level": os.getenv("AXOLOTL_LOG_LEVEL", DEFAULT_AXOLOTL_LOG_LEVEL), + "level": os.getenv("AXOLOTL_LOG_LEVEL", DEFAULT_AXOLOTL_LOG_LEVEL).upper(), "propagate": False, }, }, From 109d9c74425731d55ecdf583ab5316219b6d821d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 19 Jul 2025 13:53:35 -0400 Subject: [PATCH 0826/1405] make the initial call to tokenizer.pad not spam the console (#2946) [skip ci] * make the initial call to tokenizer.pad not spam the console * add guard from feedback * make another common console output less verbose * more logging fixes --- src/axolotl/integrations/liger/args.py | 9 +++++++++ src/axolotl/loaders/tokenizer.py | 5 +++++ src/axolotl/monkeypatch/tiled_mlp.py | 7 +++++++ src/axolotl/utils/schemas/validation.py | 7 ++++--- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/axolotl/integrations/liger/args.py b/src/axolotl/integrations/liger/args.py index d05f08f9a3..94ba83dd5c 100644 --- a/src/axolotl/integrations/liger/args.py +++ b/src/axolotl/integrations/liger/args.py @@ -53,3 +53,12 @@ def check_deprecated_swiglu(cls, data): ) data["liger_glu_activation"] = data.pop("liger_swiglu") return data + + @model_validator(mode="before") + @classmethod + def check_tiled_mlp_conflict(cls, data): + if data.get("liger_glu_activation") is True and data.get("tiled_mlp") is True: + raise ValueError( + "You cannot have both `liger_glu_activation` and `tiled_mlp` set." + ) + return data diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index 2f0ccbcbb0..1889fa1685 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -295,4 +295,9 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): LOG.info( "No Chat template selected. Consider adding a chat template for easier inference." ) + + # make the tokenizer.pad call quieter 🤐 + if hasattr(tokenizer, "deprecation_warnings"): + tokenizer.deprecation_warnings["Asking-to-pad-a-fast-tokenizer"] = True + return tokenizer diff --git a/src/axolotl/monkeypatch/tiled_mlp.py b/src/axolotl/monkeypatch/tiled_mlp.py index 3818c6b355..02bb3a8cb7 100644 --- a/src/axolotl/monkeypatch/tiled_mlp.py +++ b/src/axolotl/monkeypatch/tiled_mlp.py @@ -7,6 +7,9 @@ import torch.distributed as dist from axolotl.utils.callbacks.models import get_causal_lm_model_cls_prefix +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) def patch_tiled_mlp(model_type, use_original_mlp=False, cfg_num_shards=None): @@ -63,6 +66,10 @@ def tiled_mlp_forward(self, x): mlp_cls.forward = tiled_mlp_forward mlp_cls._compute_params = [] # pylint: disable=protected-access + LOG.info( + f"Successfully monkey-patched TiledMLP for model_type: {model_type}", + main_process_only=True, + ) except (ImportError, AttributeError) as e: raise RuntimeError( f"Could not import MLP class for model_type: {model_type}. " diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 292159bb82..64dbb2529a 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -3,7 +3,6 @@ # pylint: disable=too-many-boolean-expressions import json -import logging import tempfile from pathlib import Path @@ -13,11 +12,12 @@ ) from transformers.utils.import_utils import is_torch_npu_available +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import ChatTemplate, RingAttnFunc, RLType # pylint: disable=too-many-lines -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) SUPPORTED_METRICS = {"sacrebleu", "comet", "ter", "chrf", "perplexity"} @@ -116,7 +116,8 @@ def check_eval_packing(cls, data): and not data.get("eval_table_size") ): LOG.info( - "explicitly setting `eval_sample_packing` to match `sample_packing`" + "explicitly setting `eval_sample_packing` to match `sample_packing`", + main_process_only=True, ) data["eval_sample_packing"] = True From e5734e5cf09bf7d7c310c9b8c07e8955b0a0df2e Mon Sep 17 00:00:00 2001 From: salman Date: Sat, 19 Jul 2025 18:54:14 +0100 Subject: [PATCH 0827/1405] adding torchtitan link (#2945) [skip ci] --- docs/multi-gpu.qmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/multi-gpu.qmd b/docs/multi-gpu.qmd index 2571d31b6a..71676bc843 100644 --- a/docs/multi-gpu.qmd +++ b/docs/multi-gpu.qmd @@ -98,8 +98,8 @@ fsdp_cpu_ram_efficient_loading | cpu_ram_efficient_loading fsdp_state_dict_type | state_dict_type fsdp_use_orig_params | **REMOVED** - -For example, if you were using the following FSDP1 config: +For more details, please see the migration guide in the [torchtitan repo](https://github.com/pytorch/torchtitan/blob/main/docs/fsdp.md). In Axolotl, +if you were using the following FSDP1 config: ```{.yaml} fsdp_version: 1 From b986f7c7cb3e7a8a43bf4136686118a4c7e4a669 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sun, 20 Jul 2025 00:54:43 +0700 Subject: [PATCH 0828/1405] fix: return proper attention for llama4 lora kernel and fsdp2 llama4 example fix (#2943) * fix: return proper attention for llama4 lora optim * fix: update fsdp2 llama4 config --- examples/llama-4/scout-qlora-flexattn-fsdp2.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml index b319349c4a..b3e8c328c2 100644 --- a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml +++ b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml @@ -74,7 +74,7 @@ fsdp: fsdp_config: fsdp_version: 2 fsdp_offload_params: false - fsdp_cpu_ram_efficient_loading: true + # fsdp_cpu_ram_efficient_loading: true # does not work with load_in_8bit/4bit fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP fsdp_transformer_layer_cls_to_wrap: Llama4TextDecoderLayer fsdp_state_dict_type: SHARDED_STATE_DICT From 31a15a49b66a19e69819af17e694126dd76974c3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 20 Jul 2025 21:19:23 -0400 Subject: [PATCH 0829/1405] add additional packages via apt for better multi-node support (#2949) * cleanup in Dockerfile and add infiniband packages * fixes for ci * fix nightly too --- cicd/Dockerfile-uv.jinja | 2 +- cicd/Dockerfile.jinja | 2 +- docker/Dockerfile | 21 +++++++++------------ docker/Dockerfile-base | 8 ++++++-- docker/Dockerfile-base-nightly | 10 +++++++--- docker/Dockerfile-cloud | 3 ++- 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/cicd/Dockerfile-uv.jinja b/cicd/Dockerfile-uv.jinja index 84527274d3..8603861879 100644 --- a/cicd/Dockerfile-uv.jinja +++ b/cicd/Dockerfile-uv.jinja @@ -11,7 +11,7 @@ ENV NIGHTLY_BUILD="{{ NIGHTLY_BUILD }}" ENV HF_HOME="{{ HF_HOME }}" RUN apt-get update && \ - apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev + apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm WORKDIR /workspace diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 13920de786..94c9a67e3e 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -12,7 +12,7 @@ ENV HF_HOME="{{ HF_HOME }}" ENV AXOLOTL_DATASET_PROCESSES="8" RUN apt-get update && \ - apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev + apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm WORKDIR /workspace diff --git a/docker/Dockerfile b/docker/Dockerfile index e23a729d4d..7114fd104c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -10,7 +10,8 @@ ARG PYTORCH_VERSION="2.1.2" ENV PYTORCH_VERSION=$PYTORCH_VERSION RUN apt-get update && \ - apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev rsync s3fs + apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev rsync s3fs && \ + rm -rf /var/lib/apt/lists/* WORKDIR /workspace @@ -23,17 +24,13 @@ RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray] $AXOLOTL_ARGS; \ - fi - -RUN python scripts/unsloth_install.py | sh -RUN python scripts/cutcrossentropy_install.py | sh - -# So we can test the Docker image -RUN pip install pytest + fi && \ + python scripts/unsloth_install.py | sh && \ + python scripts/cutcrossentropy_install.py | sh && \ + pip install pytest && \ + pip cache purge # fix so that git fetch/pull from remote works RUN git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && \ - git config --get remote.origin.fetch - -# helper for huggingface-login cli -RUN git config --global credential.helper store + git config --get remote.origin.fetch && \ + git config --global credential.helper store diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index df42403252..4c301932d6 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -22,6 +22,8 @@ RUN apt-get update \ && mkdir /root/.conda \ && bash Miniconda3-latest-Linux-x86_64.sh -b \ && rm -f Miniconda3-latest-Linux-x86_64.sh \ + && conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main \ + && conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r \ && conda create -n "py${PYTHON_VERSION}" python="${PYTHON_VERSION}" ENV PATH="/root/miniconda3/envs/py${PYTHON_VERSION}/bin:${PATH}" @@ -31,12 +33,14 @@ WORKDIR /workspace RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==23.2 setuptools==75.8.0 wheel && \ python3 -m pip install --no-cache-dir -U torch==${PYTORCH_VERSION}+cu${CUDA} torchvision --extra-index-url https://download.pytorch.org/whl/cu$CUDA && \ python3 -m pip install --no-cache-dir "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" && \ - python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" + python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" && \ + python3 -m pip cache purge RUN git lfs install --skip-repo && \ pip3 install awscli && \ # The base image ships with `pydantic==1.8.2` which is not working - pip3 install -U --no-cache-dir pydantic==1.10.10 + pip3 install -U --no-cache-dir pydantic==1.10.10 && \ + pip3 cache purge RUN if [ "$PYTORCH_VERSION" = "2.6.0" ] && [ "$CUDA" = "124" ] ; then \ FLASH_ATTENTION_FORCE_BUILD="TRUE" pip3 install --no-build-isolation flash-attn==2.8.0.post2; \ diff --git a/docker/Dockerfile-base-nightly b/docker/Dockerfile-base-nightly index 85805ea415..cc74e6bb9b 100644 --- a/docker/Dockerfile-base-nightly +++ b/docker/Dockerfile-base-nightly @@ -22,18 +22,22 @@ RUN apt-get update \ && mkdir /root/.conda \ && bash Miniconda3-latest-Linux-x86_64.sh -b \ && rm -f Miniconda3-latest-Linux-x86_64.sh \ + && conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main \ + && conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r \ && conda create -n "py${PYTHON_VERSION}" python="${PYTHON_VERSION}" ENV PATH="/root/miniconda3/envs/py${PYTHON_VERSION}/bin:${PATH}" WORKDIR /workspace -RUN python3 -m pip install --upgrade pip && pip3 install packaging && \ +RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==23.2 setuptools==75.8.0 wheel && \ python3 -m pip install --no-cache-dir -U torch --extra-index-url https://download.pytorch.org/whl/nightly/cu$CUDA && \ python3 -m pip install --no-cache-dir "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" && \ - python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" + python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" && \ + python3 -m pip cache purge RUN git lfs install --skip-repo && \ pip3 install awscli && \ # The base image ships with `pydantic==1.8.2` which is not working - pip3 install -U --no-cache-dir pydantic==1.10.10 + pip3 install -U --no-cache-dir pydantic==1.10.10 && \ + pip3 cache purge diff --git a/docker/Dockerfile-cloud b/docker/Dockerfile-cloud index c84ea1dca9..e53bba239f 100644 --- a/docker/Dockerfile-cloud +++ b/docker/Dockerfile-cloud @@ -14,7 +14,8 @@ COPY scripts/motd /etc/motd RUN pip install jupyterlab notebook ipywidgets && \ jupyter lab clean -RUN apt install --yes --no-install-recommends openssh-server tmux iproute2 nvtop && \ +RUN apt install --yes --no-install-recommends openssh-server tmux iproute2 nvtop ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm && \ + rm -rf /var/lib/apt/lists/* && \ mkdir -p ~/.ssh && \ chmod 700 ~/.ssh && \ printf "\n[[ -z \"\$TMUX\" ]] && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> ~/.bashrc && \ From 8e5f146701844e8271748395eb690c6d65521bd8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 21 Jul 2025 11:05:00 -0400 Subject: [PATCH 0830/1405] Fix cloud docker image build and remove apt files for optim (#2961) * make sure to apt update to install sudo and tmux * remove apt archives too --- docker/Dockerfile | 1 + docker/Dockerfile-base | 4 +++- docker/Dockerfile-cloud | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7114fd104c..9bc34c3877 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,6 +11,7 @@ ENV PYTORCH_VERSION=$PYTORCH_VERSION RUN apt-get update && \ apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev rsync s3fs && \ + rm -rf /var/cache/apt/archives && \ rm -rf /var/lib/apt/lists/* WORKDIR /workspace diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 4c301932d6..06eb8206c2 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -16,7 +16,9 @@ ENV PYTHON_VERSION=$PYTHON_VERSION ENV TORCH_CUDA_ARCH_LIST=$TORCH_CUDA_ARCH_LIST RUN apt-get update \ - && apt-get install -y wget git build-essential ninja-build git-lfs libaio-dev pkg-config && rm -rf /var/lib/apt/lists/* \ + && apt-get install -y wget git build-essential ninja-build git-lfs libaio-dev pkg-config \ + && rm -rf /var/cache/apt/archives \ + && rm -rf /var/lib/apt/lists/* \ && wget \ https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \ && mkdir /root/.conda \ diff --git a/docker/Dockerfile-cloud b/docker/Dockerfile-cloud index e53bba239f..590b737065 100644 --- a/docker/Dockerfile-cloud +++ b/docker/Dockerfile-cloud @@ -14,7 +14,9 @@ COPY scripts/motd /etc/motd RUN pip install jupyterlab notebook ipywidgets && \ jupyter lab clean -RUN apt install --yes --no-install-recommends openssh-server tmux iproute2 nvtop ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm && \ +RUN apt update && \ + apt install --yes --no-install-recommends openssh-server tmux iproute2 nvtop ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm && \ + rm -rf /var/cache/apt/archives && \ rm -rf /var/lib/apt/lists/* && \ mkdir -p ~/.ssh && \ chmod 700 ~/.ssh && \ From db5f6f46934cefaf1c54d341645e759d70c65d2c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 21 Jul 2025 11:39:38 -0400 Subject: [PATCH 0831/1405] limit num_proc when saving datasets to disk (#2948) [skip ci] * limit num_proc when saving datasets to disk * enforce at least 1 in case it rounds down to 0, and sane divisor is at least 8 rows per worker to save * update fixtures with dataset processes since that should never be NoneType * improve reusability for tests --- src/axolotl/utils/data/shared.py | 5 +++-- src/axolotl/utils/datasets.py | 11 +++++++++++ src/axolotl/utils/schemas/config.py | 9 ++------- tests/core/test_builders.py | 2 ++ tests/test_datasets.py | 7 +++++++ tests/test_exact_deduplication.py | 1 + tests/test_packed_dataset.py | 1 + 7 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 src/axolotl/utils/datasets.py diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 3a36572401..bf7a30f485 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -25,6 +25,7 @@ from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH from axolotl.utils.data.utils import deduplicate_and_log_datasets, md5 +from axolotl.utils.datasets import get_default_process_count from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger @@ -410,7 +411,7 @@ def save_preprocessed_dataset( ) -> None: """Save preprocessed dataset to disk and optionally push to the HF Hub.""" prepared_ds_path = get_prepared_dataset_path(cfg, dataset_hash) - num_workers = cfg.dataset_processes + num_workers = cfg.dataset_processes or get_default_process_count() if isinstance(dataset, IterableDataset): ds_from_iter = Dataset.from_generator( functools.partial(_generate_from_iterable_dataset, dataset), @@ -432,7 +433,7 @@ def save_preprocessed_dataset( os.makedirs(prepared_ds_path, exist_ok=True) dataset.save_to_disk( str(prepared_ds_path), - num_proc=num_workers, + num_proc=min(max(1, len(dataset) // 8), num_workers), max_shard_size=None, num_shards=cfg.num_dataset_shards_to_save, ) diff --git a/src/axolotl/utils/datasets.py b/src/axolotl/utils/datasets.py new file mode 100644 index 0000000000..93e1a24163 --- /dev/null +++ b/src/axolotl/utils/datasets.py @@ -0,0 +1,11 @@ +"""helper functions for datasets""" + +import os + + +def get_default_process_count(): + if axolotl_dataset_processes := os.environ.get("AXOLOTL_DATASET_PROCESSES"): + return int(axolotl_dataset_processes) + if runpod_cpu_count := os.environ.get("RUNPOD_CPU_COUNT"): + return int(runpod_cpu_count) + return os.cpu_count() diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index d3fb0b14cd..96e3a8a3e8 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -2,7 +2,6 @@ # pylint: disable=too-many-lines -import os from typing import Annotated, Any, Literal from annotated_types import MinLen @@ -15,6 +14,7 @@ model_validator, ) +from axolotl.utils.datasets import get_default_process_count from axolotl.utils.logging import get_logger from axolotl.utils.schemas.datasets import ( DatasetConfig, @@ -1211,11 +1211,6 @@ def default_dataloader_opts(cls, data): @classmethod def default_dataset_processes(cls, data): if data.get("dataset_processes") is None: - if axolotl_dataset_processes := os.environ.get("AXOLOTL_DATASET_PROCESSES"): - data["dataset_processes"] = int(axolotl_dataset_processes) - elif runpod_cpu_count := os.environ.get("RUNPOD_CPU_COUNT"): - data["dataset_processes"] = int(runpod_cpu_count) - else: - data["dataset_processes"] = os.cpu_count() + data["dataset_processes"] = get_default_process_count() return data diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index 0053b4d277..040152beb9 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -82,6 +82,7 @@ def fixture_base_cfg(): "ddp_timeout": 1800, "ddp_bucket_cap_mb": 25, "ddp_broadcast_buffers": False, + "dataset_processes": 4, } ) @@ -440,6 +441,7 @@ def test_custom_optimizer_cls_and_kwargs( ] else: raise ValueError(f"Unhandled cfg_string: {cfg_string}") + cfg["dataset_processes"] = 4 if cfg_string == "grpo_cfg": rewards_dir = tmp_path / "rewards_test" diff --git a/tests/test_datasets.py b/tests/test_datasets.py index f4730f0f1d..719dfdc197 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -141,6 +141,7 @@ def test_load_from_save_to_disk(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], + "dataset_processes": 4, } ) @@ -179,6 +180,7 @@ def test_load_from_dir_of_parquet(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], + "dataset_processes": 4, } ) @@ -217,6 +219,7 @@ def test_load_from_dir_of_json(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], + "dataset_processes": 4, } ) @@ -249,6 +252,7 @@ def test_load_from_single_parquet(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], + "dataset_processes": 4, } ) @@ -281,6 +285,7 @@ def test_load_from_single_json(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], + "dataset_processes": 4, } ) @@ -365,6 +370,7 @@ def test_load_hub_with_revision_with_dpo( "rl": "dpo", "chat_template": "llama3", "datasets": [ALPACA_MESSAGES_CONFIG_REVISION], + "dataset_processes": 4, } ) @@ -466,6 +472,7 @@ def test_loading_local_dataset_folder(self, tokenizer): "type": "alpaca", }, ], + "dataset_processes": 4, } ) diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index 45a327a403..d97aad8ea7 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -210,6 +210,7 @@ def cfg(self): ALPACA_MESSAGES_CONFIG_REVISION, ALPACA_MESSAGES_CONFIG_REVISION, ], + "dataset_processes": 4, } ) yield fixture diff --git a/tests/test_packed_dataset.py b/tests/test_packed_dataset.py index 8b29eab210..699d5e6cc6 100644 --- a/tests/test_packed_dataset.py +++ b/tests/test_packed_dataset.py @@ -99,6 +99,7 @@ def test_lora_packing(self, temp_dir): "type": "alpaca", }, ], + "dataset_processes": 4, "num_epochs": 1, "max_steps": 20, "save_steps": 10, From af8d257aa22f9030b0f39d5bc7b150eed459eb9a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 21 Jul 2025 11:40:56 -0400 Subject: [PATCH 0832/1405] make pad_to_sequence_len default to the same value as sample_packing (#2941) [skip ci] * make pad_to_sequence_len default to the same value as sample_packing * remove duplicate validation * fix test * update description meta Co-authored-by: NanoCode012 --------- Co-authored-by: NanoCode012 --- examples/archived/code-llama/13b/lora.yml | 2 +- examples/archived/code-llama/13b/qlora.yml | 2 +- examples/archived/code-llama/34b/lora.yml | 2 +- examples/archived/code-llama/34b/qlora.yml | 2 +- examples/archived/code-llama/7b/lora.yml | 2 +- examples/archived/code-llama/7b/qlora.yml | 2 +- .../deepcoder/deepcoder-14B-preview-lora.yml | 2 +- examples/archived/gemma/qlora.yml | 2 +- examples/archived/stablelm-2/1.6b/fft.yml | 2 +- examples/archived/stablelm-2/1.6b/lora.yml | 2 +- examples/archived/starcoder2/qlora.yml | 2 +- examples/archived/tiny-llama/lora-mps.yml | 2 +- examples/archived/tiny-llama/lora.yml | 2 +- examples/archived/tiny-llama/qlora.yml | 2 +- examples/cloud/modal.yaml | 2 -- examples/cohere/command-r-7b-qlora.yml | 2 +- .../cogito-v1-preview-llama-3B-lora.yml | 2 +- .../cogito-v1-preview-qwen-14B-lora.yml | 2 +- examples/deepseek-v2/fft-fsdp-16b.yaml | 2 +- examples/deepseek-v2/qlora-fsdp-2_5.yaml | 2 +- examples/devstral/devstral-small-qlora.yml | 2 +- .../falcon-h1/falcon-h1-1b-deep-qlora.yaml | 2 +- examples/falcon-h1/falcon-h1-1b-qlora.yaml | 2 +- examples/falcon-h1/falcon-h1-34b-qlora.yaml | 2 +- examples/falcon-h1/falcon-h1-3b-qlora.yaml | 2 +- examples/falcon-h1/falcon-h1-500m-qlora.yaml | 2 +- examples/falcon-h1/falcon-h1-7b-qlora.yaml | 2 +- examples/gemma2/qlora.yml | 2 +- examples/gemma2/reward-model.yaml | 2 +- examples/gemma3/gemma-3-1b-qlora.yml | 2 +- examples/gemma3/gemma-3-4b-qlora.yml | 2 +- examples/glm4/qlora-32b.yaml | 2 +- examples/jamba/qlora_fsdp_large.yaml | 2 +- examples/lfm2/lfm2-350m-fft.yaml | 2 +- examples/llama-2/fft_optimized.yml | 2 +- examples/llama-2/lisa.yml | 2 +- examples/llama-2/loftq.yml | 2 +- examples/llama-2/lora.yml | 2 +- examples/llama-2/qlora-fsdp.yml | 2 +- examples/llama-2/qlora.yml | 2 +- examples/llama-2/relora.yml | 2 +- examples/llama-3/3b-qat-fsdp2.yaml | 2 +- examples/llama-3/fft-8b-liger-fsdp.yaml | 2 +- examples/llama-3/fft-8b.yaml | 2 +- examples/llama-3/instruct-dpo-lora-8b.yml | 2 +- examples/llama-3/instruct-lora-8b.yml | 2 +- examples/llama-3/lora-1b-deduplicate-dpo.yml | 2 +- examples/llama-3/lora-1b-deduplicate-sft.yml | 2 +- examples/llama-3/lora-1b-kernels.yml | 2 +- examples/llama-3/lora-1b-ray.yml | 2 +- .../lora-1b-sample-packing-sequentially.yml | 2 +- examples/llama-3/lora-1b.yml | 2 +- examples/llama-3/lora-8b.yml | 2 +- examples/llama-3/qlora-1b.yml | 2 +- examples/llama-3/qlora-fsdp-405b.yaml | 2 +- examples/llama-3/qlora-fsdp-70b.yaml | 2 +- examples/llama-3/qlora.yml | 2 +- examples/llama-3/sparse-finetuning.yaml | 2 +- .../do-no-use-fa2/maverick-qlora-fsdp1.yaml | 2 +- .../do-no-use-fa2/scout-qlora-fsdp1.yaml | 2 +- .../scout-qlora-single-h100.yaml | 2 +- .../llama-4/scout-qlora-flexattn-fsdp2.yaml | 2 +- .../llama-4/scout-qlora-single-h100-flex.yaml | 2 +- .../magistral/magistral-small-fsdp-qlora.yaml | 2 +- examples/magistral/magistral-small-qlora.yaml | 2 +- examples/mistral/bigstral-ds-zero3.yaml | 2 +- examples/mistral/config.yml | 2 +- examples/mistral/lora-mps.yml | 2 +- examples/mistral/lora.yml | 2 +- examples/mistral/mistral-dpo-qlora.yml | 2 +- examples/mistral/mistral-qlora-orpo.yml | 2 +- examples/mistral/mixtral.yml | 2 +- examples/mistral/mixtral_22.yml | 2 +- examples/mistral/qlora.yml | 2 +- examples/orpheus/finetune.yml | 2 +- examples/phi/lora-3.5.yaml | 2 +- examples/phi/phi-ft.yml | 2 +- examples/phi/phi-qlora.yml | 2 +- examples/phi/phi2-ft.yml | 2 +- examples/phi/phi3-ft-fsdp.yml | 2 +- examples/phi/phi3-ft.yml | 2 +- examples/qwen2/dpo.yaml | 2 +- examples/qwen2/prm.yaml | 2 +- examples/qwen2/qlora-fsdp.yaml | 2 +- examples/qwen2/reward-model.yaml | 2 +- examples/qwen3/32b-qlora.yaml | 2 +- examples/qwen3/8b-qat-fsdp2.yml | 2 +- examples/qwen3/qlora-fsdp.yaml | 2 +- src/axolotl/utils/schemas/config.py | 2 +- .../schemas/validation/test_default_values.py | 21 +++++++++++++++++++ 90 files changed, 109 insertions(+), 90 deletions(-) create mode 100644 tests/utils/schemas/validation/test_default_values.py diff --git a/examples/archived/code-llama/13b/lora.yml b/examples/archived/code-llama/13b/lora.yml index 0ed2382ba6..98ef516abe 100644 --- a/examples/archived/code-llama/13b/lora.yml +++ b/examples/archived/code-llama/13b/lora.yml @@ -17,7 +17,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/archived/code-llama/13b/qlora.yml b/examples/archived/code-llama/13b/qlora.yml index 22bd1691b4..2385368ace 100644 --- a/examples/archived/code-llama/13b/qlora.yml +++ b/examples/archived/code-llama/13b/qlora.yml @@ -20,7 +20,7 @@ lora_model_dir: sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/archived/code-llama/34b/lora.yml b/examples/archived/code-llama/34b/lora.yml index 25dc9f421e..fb44997ff9 100644 --- a/examples/archived/code-llama/34b/lora.yml +++ b/examples/archived/code-llama/34b/lora.yml @@ -17,7 +17,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/archived/code-llama/34b/qlora.yml b/examples/archived/code-llama/34b/qlora.yml index 0e33e2a45f..22f4cae3c7 100644 --- a/examples/archived/code-llama/34b/qlora.yml +++ b/examples/archived/code-llama/34b/qlora.yml @@ -20,7 +20,7 @@ lora_model_dir: sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/archived/code-llama/7b/lora.yml b/examples/archived/code-llama/7b/lora.yml index d288b9f65a..0632bdfb79 100644 --- a/examples/archived/code-llama/7b/lora.yml +++ b/examples/archived/code-llama/7b/lora.yml @@ -17,7 +17,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/archived/code-llama/7b/qlora.yml b/examples/archived/code-llama/7b/qlora.yml index de41c01232..0bd462aabe 100644 --- a/examples/archived/code-llama/7b/qlora.yml +++ b/examples/archived/code-llama/7b/qlora.yml @@ -20,7 +20,7 @@ lora_model_dir: sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml b/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml index 9e92c0a072..a9511e9e3a 100644 --- a/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml +++ b/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml @@ -21,7 +21,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true eval_sample_packing: false -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/archived/gemma/qlora.yml b/examples/archived/gemma/qlora.yml index 2738112b41..80829b3c9d 100644 --- a/examples/archived/gemma/qlora.yml +++ b/examples/archived/gemma/qlora.yml @@ -25,7 +25,7 @@ lora_target_linear: true sequence_len: 4096 sample_packing: true eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/archived/stablelm-2/1.6b/fft.yml b/examples/archived/stablelm-2/1.6b/fft.yml index 9b45b399f8..3ae08c9de0 100644 --- a/examples/archived/stablelm-2/1.6b/fft.yml +++ b/examples/archived/stablelm-2/1.6b/fft.yml @@ -16,7 +16,7 @@ output_dir: ./outputs/out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + adapter: lora_model_dir: diff --git a/examples/archived/stablelm-2/1.6b/lora.yml b/examples/archived/stablelm-2/1.6b/lora.yml index 31e5ad9336..e5aa814239 100644 --- a/examples/archived/stablelm-2/1.6b/lora.yml +++ b/examples/archived/stablelm-2/1.6b/lora.yml @@ -19,7 +19,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/archived/starcoder2/qlora.yml b/examples/archived/starcoder2/qlora.yml index 18d85f9c38..889d837e89 100644 --- a/examples/archived/starcoder2/qlora.yml +++ b/examples/archived/starcoder2/qlora.yml @@ -19,7 +19,7 @@ lora_model_dir: sequence_len: 8192 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/archived/tiny-llama/lora-mps.yml b/examples/archived/tiny-llama/lora-mps.yml index 66cf7cfb3e..aa3b7d8510 100644 --- a/examples/archived/tiny-llama/lora-mps.yml +++ b/examples/archived/tiny-llama/lora-mps.yml @@ -17,7 +17,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + eval_sample_packing: false adapter: lora diff --git a/examples/archived/tiny-llama/lora.yml b/examples/archived/tiny-llama/lora.yml index 90998880f7..a92f4bd677 100644 --- a/examples/archived/tiny-llama/lora.yml +++ b/examples/archived/tiny-llama/lora.yml @@ -17,7 +17,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true eval_sample_packing: false -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/archived/tiny-llama/qlora.yml b/examples/archived/tiny-llama/qlora.yml index 8b2a4565a7..4d422a5ee5 100644 --- a/examples/archived/tiny-llama/qlora.yml +++ b/examples/archived/tiny-llama/qlora.yml @@ -21,7 +21,7 @@ lora_model_dir: sequence_len: 4096 sample_packing: true eval_sample_packing: false -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/cloud/modal.yaml b/examples/cloud/modal.yaml index bbe8785f16..1950314948 100644 --- a/examples/cloud/modal.yaml +++ b/examples/cloud/modal.yaml @@ -26,5 +26,3 @@ timeout: 86400 # Preprocess specific configurations memory_preprocess: 32 timeout_preprocess: 14400 - -# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/cohere/command-r-7b-qlora.yml b/examples/cohere/command-r-7b-qlora.yml index da2777270e..b4741636b7 100644 --- a/examples/cohere/command-r-7b-qlora.yml +++ b/examples/cohere/command-r-7b-qlora.yml @@ -27,7 +27,7 @@ lora_target_linear: true sequence_len: 2048 sample_packing: true eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml b/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml index 1a051b98bd..6f0b505bd7 100644 --- a/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml +++ b/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml @@ -21,7 +21,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true eval_sample_packing: false -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml b/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml index 8073426412..fefcfadeaf 100644 --- a/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml +++ b/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml @@ -21,7 +21,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true eval_sample_packing: false -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/deepseek-v2/fft-fsdp-16b.yaml b/examples/deepseek-v2/fft-fsdp-16b.yaml index 78bf6b1797..d23c789aa4 100644 --- a/examples/deepseek-v2/fft-fsdp-16b.yaml +++ b/examples/deepseek-v2/fft-fsdp-16b.yaml @@ -12,7 +12,7 @@ output_dir: ./outputs/out sequence_len: 2048 sample_packing: true -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/deepseek-v2/qlora-fsdp-2_5.yaml b/examples/deepseek-v2/qlora-fsdp-2_5.yaml index da1d9aefd5..0536d1c101 100644 --- a/examples/deepseek-v2/qlora-fsdp-2_5.yaml +++ b/examples/deepseek-v2/qlora-fsdp-2_5.yaml @@ -30,7 +30,7 @@ output_dir: ./outputs/out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/devstral/devstral-small-qlora.yml b/examples/devstral/devstral-small-qlora.yml index 9d92e8662f..7fe4dd4335 100644 --- a/examples/devstral/devstral-small-qlora.yml +++ b/examples/devstral/devstral-small-qlora.yml @@ -25,7 +25,7 @@ lora_model_dir: sequence_len: 2048 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml b/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml index 484c31fecc..2473179f0f 100644 --- a/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml @@ -38,7 +38,7 @@ lora_target_modules: sequence_len: 2048 sample_packing: false eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/falcon-h1/falcon-h1-1b-qlora.yaml b/examples/falcon-h1/falcon-h1-1b-qlora.yaml index dea2a6e6d8..bfb7836ef0 100644 --- a/examples/falcon-h1/falcon-h1-1b-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-1b-qlora.yaml @@ -38,7 +38,7 @@ lora_target_modules: sequence_len: 2048 sample_packing: false eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/falcon-h1/falcon-h1-34b-qlora.yaml b/examples/falcon-h1/falcon-h1-34b-qlora.yaml index b187efbf6e..80a9d45b5e 100644 --- a/examples/falcon-h1/falcon-h1-34b-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-34b-qlora.yaml @@ -38,7 +38,7 @@ lora_target_modules: sequence_len: 2048 sample_packing: false eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/falcon-h1/falcon-h1-3b-qlora.yaml b/examples/falcon-h1/falcon-h1-3b-qlora.yaml index 4d981ad95f..02be8ac5d5 100644 --- a/examples/falcon-h1/falcon-h1-3b-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-3b-qlora.yaml @@ -38,7 +38,7 @@ lora_target_modules: sequence_len: 2048 sample_packing: false eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/falcon-h1/falcon-h1-500m-qlora.yaml b/examples/falcon-h1/falcon-h1-500m-qlora.yaml index 5ee13facd3..b112d5d852 100644 --- a/examples/falcon-h1/falcon-h1-500m-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-500m-qlora.yaml @@ -38,7 +38,7 @@ lora_target_modules: sequence_len: 2048 sample_packing: false eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/falcon-h1/falcon-h1-7b-qlora.yaml b/examples/falcon-h1/falcon-h1-7b-qlora.yaml index 4b665c3cd9..c5505873df 100644 --- a/examples/falcon-h1/falcon-h1-7b-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-7b-qlora.yaml @@ -38,7 +38,7 @@ lora_target_modules: sequence_len: 2048 sample_packing: false eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/gemma2/qlora.yml b/examples/gemma2/qlora.yml index 68d213fada..8a295a1f80 100644 --- a/examples/gemma2/qlora.yml +++ b/examples/gemma2/qlora.yml @@ -31,7 +31,7 @@ lora_target_linear: true sequence_len: 2048 sample_packing: true eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/gemma2/reward-model.yaml b/examples/gemma2/reward-model.yaml index 624ebdcd22..67b1228b2a 100644 --- a/examples/gemma2/reward-model.yaml +++ b/examples/gemma2/reward-model.yaml @@ -18,7 +18,7 @@ remove_unused_columns: false sequence_len: 2048 sample_packing: false eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/gemma3/gemma-3-1b-qlora.yml b/examples/gemma3/gemma-3-1b-qlora.yml index 99921770db..115717db7e 100644 --- a/examples/gemma3/gemma-3-1b-qlora.yml +++ b/examples/gemma3/gemma-3-1b-qlora.yml @@ -35,7 +35,7 @@ lora_target_linear: true sequence_len: 2048 sample_packing: true eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml index 025cb9240f..44ba9c8799 100644 --- a/examples/gemma3/gemma-3-4b-qlora.yml +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -25,7 +25,7 @@ lora_model_dir: sequence_len: 2048 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/glm4/qlora-32b.yaml b/examples/glm4/qlora-32b.yaml index 8973cedd4b..b3656e3aea 100644 --- a/examples/glm4/qlora-32b.yaml +++ b/examples/glm4/qlora-32b.yaml @@ -17,7 +17,7 @@ lora_model_dir: sequence_len: 2048 sample_packing: true eval_sample_packing: true -pad_to_sequence_len: true + lora_r: 16 lora_alpha: 32 diff --git a/examples/jamba/qlora_fsdp_large.yaml b/examples/jamba/qlora_fsdp_large.yaml index fda30e2d2f..344f73e63c 100644 --- a/examples/jamba/qlora_fsdp_large.yaml +++ b/examples/jamba/qlora_fsdp_large.yaml @@ -23,7 +23,7 @@ save_safetensors: true adapter: qlora sequence_len: 2048 sample_packing: true -pad_to_sequence_len: true + lora_r: 16 lora_alpha: 16 diff --git a/examples/lfm2/lfm2-350m-fft.yaml b/examples/lfm2/lfm2-350m-fft.yaml index 74c90c1e1e..16a0a028e1 100644 --- a/examples/lfm2/lfm2-350m-fft.yaml +++ b/examples/lfm2/lfm2-350m-fft.yaml @@ -18,7 +18,7 @@ output_dir: ./outputs/out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/llama-2/fft_optimized.yml b/examples/llama-2/fft_optimized.yml index c44cd22307..a23778b96c 100644 --- a/examples/llama-2/fft_optimized.yml +++ b/examples/llama-2/fft_optimized.yml @@ -14,7 +14,7 @@ output_dir: ./outputs/out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + adapter: lora_model_dir: diff --git a/examples/llama-2/lisa.yml b/examples/llama-2/lisa.yml index a44e261beb..25adcad5d2 100644 --- a/examples/llama-2/lisa.yml +++ b/examples/llama-2/lisa.yml @@ -14,7 +14,7 @@ output_dir: ./outputs/lisa-out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + adapter: lora_model_dir: diff --git a/examples/llama-2/loftq.yml b/examples/llama-2/loftq.yml index 085627f63b..606bbc7353 100644 --- a/examples/llama-2/loftq.yml +++ b/examples/llama-2/loftq.yml @@ -14,7 +14,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/llama-2/lora.yml b/examples/llama-2/lora.yml index 759fce0441..0781e0d1b4 100644 --- a/examples/llama-2/lora.yml +++ b/examples/llama-2/lora.yml @@ -17,7 +17,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/llama-2/qlora-fsdp.yml b/examples/llama-2/qlora-fsdp.yml index 3bf30120bb..ceb3ce5d19 100644 --- a/examples/llama-2/qlora-fsdp.yml +++ b/examples/llama-2/qlora-fsdp.yml @@ -20,7 +20,7 @@ lora_model_dir: sequence_len: 512 sample_packing: false -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/llama-2/qlora.yml b/examples/llama-2/qlora.yml index 09596c71e3..1515872e6c 100644 --- a/examples/llama-2/qlora.yml +++ b/examples/llama-2/qlora.yml @@ -20,7 +20,7 @@ lora_model_dir: sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/llama-2/relora.yml b/examples/llama-2/relora.yml index ca8b14a1cc..6c9e83223e 100644 --- a/examples/llama-2/relora.yml +++ b/examples/llama-2/relora.yml @@ -18,7 +18,7 @@ lora_model_dir: sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + lora_r: 8 lora_alpha: 16 diff --git a/examples/llama-3/3b-qat-fsdp2.yaml b/examples/llama-3/3b-qat-fsdp2.yaml index 08d8ee5c13..d9b96fb96d 100644 --- a/examples/llama-3/3b-qat-fsdp2.yaml +++ b/examples/llama-3/3b-qat-fsdp2.yaml @@ -22,7 +22,7 @@ datasets: output_dir: ./outputs/qat_out/ sample_packing: true -pad_to_sequence_len: true + sequence_len: 512 flex_attention: true diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml index e2808935f6..b3d990a8bb 100644 --- a/examples/llama-3/fft-8b-liger-fsdp.yaml +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -26,7 +26,7 @@ output_dir: ./outputs/out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/llama-3/fft-8b.yaml b/examples/llama-3/fft-8b.yaml index 2dfe6d492d..e067212b75 100644 --- a/examples/llama-3/fft-8b.yaml +++ b/examples/llama-3/fft-8b.yaml @@ -11,7 +11,7 @@ output_dir: ./outputs/out sequence_len: 8192 sample_packing: true -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/llama-3/instruct-dpo-lora-8b.yml b/examples/llama-3/instruct-dpo-lora-8b.yml index 10ab2a320c..99de56ad31 100644 --- a/examples/llama-3/instruct-dpo-lora-8b.yml +++ b/examples/llama-3/instruct-dpo-lora-8b.yml @@ -37,7 +37,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: false -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml index 83b7f9a37c..b8baa5b0aa 100644 --- a/examples/llama-3/instruct-lora-8b.yml +++ b/examples/llama-3/instruct-lora-8b.yml @@ -28,7 +28,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: false -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/llama-3/lora-1b-deduplicate-dpo.yml b/examples/llama-3/lora-1b-deduplicate-dpo.yml index b20dbad844..288e8fd19f 100644 --- a/examples/llama-3/lora-1b-deduplicate-dpo.yml +++ b/examples/llama-3/lora-1b-deduplicate-dpo.yml @@ -49,7 +49,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: false -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/llama-3/lora-1b-deduplicate-sft.yml b/examples/llama-3/lora-1b-deduplicate-sft.yml index 67e518184b..6ce504a0d4 100644 --- a/examples/llama-3/lora-1b-deduplicate-sft.yml +++ b/examples/llama-3/lora-1b-deduplicate-sft.yml @@ -22,7 +22,7 @@ dataset_exact_deduplication: true sequence_len: 4096 sample_packing: true eval_sample_packing: false -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/llama-3/lora-1b-kernels.yml b/examples/llama-3/lora-1b-kernels.yml index 92a948c2e6..71e569ae01 100644 --- a/examples/llama-3/lora-1b-kernels.yml +++ b/examples/llama-3/lora-1b-kernels.yml @@ -14,7 +14,7 @@ lora_model_dir: sequence_len: 2048 sample_packing: true -pad_to_sequence_len: true + lora_r: 16 lora_alpha: 32 diff --git a/examples/llama-3/lora-1b-ray.yml b/examples/llama-3/lora-1b-ray.yml index 178a1fb89c..7b9d157411 100644 --- a/examples/llama-3/lora-1b-ray.yml +++ b/examples/llama-3/lora-1b-ray.yml @@ -15,7 +15,7 @@ lora_model_dir: sequence_len: 2048 sample_packing: true eval_sample_packing: true -pad_to_sequence_len: true + lora_r: 16 lora_alpha: 32 diff --git a/examples/llama-3/lora-1b-sample-packing-sequentially.yml b/examples/llama-3/lora-1b-sample-packing-sequentially.yml index c4ce3eb0fd..9f764e1311 100644 --- a/examples/llama-3/lora-1b-sample-packing-sequentially.yml +++ b/examples/llama-3/lora-1b-sample-packing-sequentially.yml @@ -24,7 +24,7 @@ sample_packing: true sample_packing_sequentially: true curriculum_sampling: true eval_sample_packing: false -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/llama-3/lora-1b.yml b/examples/llama-3/lora-1b.yml index 82085483f1..34d540eb7f 100644 --- a/examples/llama-3/lora-1b.yml +++ b/examples/llama-3/lora-1b.yml @@ -15,7 +15,7 @@ lora_model_dir: sequence_len: 2048 sample_packing: true eval_sample_packing: true -pad_to_sequence_len: true + lora_r: 16 lora_alpha: 32 diff --git a/examples/llama-3/lora-8b.yml b/examples/llama-3/lora-8b.yml index c393897557..ca6cd9e974 100644 --- a/examples/llama-3/lora-8b.yml +++ b/examples/llama-3/lora-8b.yml @@ -18,7 +18,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: true eval_sample_packing: false -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/llama-3/qlora-1b.yml b/examples/llama-3/qlora-1b.yml index 6b76ea8d9d..288b7dc6ce 100644 --- a/examples/llama-3/qlora-1b.yml +++ b/examples/llama-3/qlora-1b.yml @@ -18,7 +18,7 @@ lora_model_dir: sequence_len: 2048 sample_packing: true eval_sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/llama-3/qlora-fsdp-405b.yaml b/examples/llama-3/qlora-fsdp-405b.yaml index 1ee922b59b..0f31b5bdcc 100644 --- a/examples/llama-3/qlora-fsdp-405b.yaml +++ b/examples/llama-3/qlora-fsdp-405b.yaml @@ -18,7 +18,7 @@ adapter: qlora sequence_len: 2048 sample_packing: true -pad_to_sequence_len: true + lora_r: 16 lora_alpha: 16 diff --git a/examples/llama-3/qlora-fsdp-70b.yaml b/examples/llama-3/qlora-fsdp-70b.yaml index 5edd8353ad..28387ba1bd 100644 --- a/examples/llama-3/qlora-fsdp-70b.yaml +++ b/examples/llama-3/qlora-fsdp-70b.yaml @@ -20,7 +20,7 @@ lora_model_dir: sequence_len: 512 sample_packing: false -pad_to_sequence_len: true + lora_r: 8 lora_alpha: 16 diff --git a/examples/llama-3/qlora.yml b/examples/llama-3/qlora.yml index a674eca279..ffb00dace9 100644 --- a/examples/llama-3/qlora.yml +++ b/examples/llama-3/qlora.yml @@ -20,7 +20,7 @@ lora_model_dir: sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/llama-3/sparse-finetuning.yaml b/examples/llama-3/sparse-finetuning.yaml index 8577a19d2f..ecf5df955e 100644 --- a/examples/llama-3/sparse-finetuning.yaml +++ b/examples/llama-3/sparse-finetuning.yaml @@ -16,7 +16,7 @@ output_dir: ./outputs/out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + eval_sample_packing: false wandb_project: diff --git a/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml b/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml index d4a038e113..3bd05b5ba3 100644 --- a/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml +++ b/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml @@ -47,7 +47,7 @@ output_dir: ./outputs/out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + gradient_accumulation_steps: 1 micro_batch_size: 1 diff --git a/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml b/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml index bea10d979e..1c6ba1410e 100644 --- a/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml @@ -48,7 +48,7 @@ output_dir: ./outputs/out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml b/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml index 737d938126..0810895553 100644 --- a/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml @@ -51,7 +51,7 @@ output_dir: ./outputs/out sequence_len: 4096 # up to 8k will work on a single H100 sample_packing: true -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml index b3e8c328c2..6193e4ed50 100644 --- a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml +++ b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml @@ -46,7 +46,7 @@ output_dir: ./outputs/out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + gradient_accumulation_steps: 1 micro_batch_size: 2 diff --git a/examples/llama-4/scout-qlora-single-h100-flex.yaml b/examples/llama-4/scout-qlora-single-h100-flex.yaml index 6be3988ef0..c3bbfe56aa 100644 --- a/examples/llama-4/scout-qlora-single-h100-flex.yaml +++ b/examples/llama-4/scout-qlora-single-h100-flex.yaml @@ -51,7 +51,7 @@ output_dir: ./outputs/out sequence_len: 4096 # up to 8k will work on a single H100 sample_packing: true -pad_to_sequence_len: true + gradient_accumulation_steps: 1 micro_batch_size: 1 diff --git a/examples/magistral/magistral-small-fsdp-qlora.yaml b/examples/magistral/magistral-small-fsdp-qlora.yaml index b23d2309a0..4a769510aa 100644 --- a/examples/magistral/magistral-small-fsdp-qlora.yaml +++ b/examples/magistral/magistral-small-fsdp-qlora.yaml @@ -23,7 +23,7 @@ lora_model_dir: sequence_len: 2048 sample_packing: true eval_sample_packing: false -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/magistral/magistral-small-qlora.yaml b/examples/magistral/magistral-small-qlora.yaml index f0fce014fa..bb2e0ccf05 100644 --- a/examples/magistral/magistral-small-qlora.yaml +++ b/examples/magistral/magistral-small-qlora.yaml @@ -22,7 +22,7 @@ lora_model_dir: sequence_len: 2048 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/mistral/bigstral-ds-zero3.yaml b/examples/mistral/bigstral-ds-zero3.yaml index e9bcbb7d68..a8dc36216e 100644 --- a/examples/mistral/bigstral-ds-zero3.yaml +++ b/examples/mistral/bigstral-ds-zero3.yaml @@ -27,7 +27,7 @@ output_dir: ./outputs/out sequence_len: 2048 sample_packing: true -pad_to_sequence_len: true + gradient_accumulation_steps: 1 micro_batch_size: 1 diff --git a/examples/mistral/config.yml b/examples/mistral/config.yml index 8c4d80f792..455c3c224c 100644 --- a/examples/mistral/config.yml +++ b/examples/mistral/config.yml @@ -14,7 +14,7 @@ output_dir: ./outputs/out sequence_len: 8192 sample_packing: true -pad_to_sequence_len: true + eval_sample_packing: false wandb_project: diff --git a/examples/mistral/lora-mps.yml b/examples/mistral/lora-mps.yml index d54c3e30bb..c18d10aeee 100644 --- a/examples/mistral/lora-mps.yml +++ b/examples/mistral/lora-mps.yml @@ -18,7 +18,7 @@ lora_model_dir: sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/mistral/lora.yml b/examples/mistral/lora.yml index 161255468e..77a87a1da4 100644 --- a/examples/mistral/lora.yml +++ b/examples/mistral/lora.yml @@ -20,7 +20,7 @@ lora_model_dir: sequence_len: 8192 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/mistral/mistral-dpo-qlora.yml b/examples/mistral/mistral-dpo-qlora.yml index 8d03786904..49f5e4ede6 100644 --- a/examples/mistral/mistral-dpo-qlora.yml +++ b/examples/mistral/mistral-dpo-qlora.yml @@ -31,7 +31,7 @@ output_dir: ./outputs/dpo-qlora sequence_len: 2048 sample_packing: false -pad_to_sequence_len: true + adapter: qlora lora_model_dir: diff --git a/examples/mistral/mistral-qlora-orpo.yml b/examples/mistral/mistral-qlora-orpo.yml index f37dc09fa3..ea3e112b92 100644 --- a/examples/mistral/mistral-qlora-orpo.yml +++ b/examples/mistral/mistral-qlora-orpo.yml @@ -25,7 +25,7 @@ lora_model_dir: sequence_len: 4096 sample_packing: false -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/mistral/mixtral.yml b/examples/mistral/mixtral.yml index 5be9b4db89..933275484b 100644 --- a/examples/mistral/mixtral.yml +++ b/examples/mistral/mixtral.yml @@ -34,7 +34,7 @@ lora_model_dir: sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/mistral/mixtral_22.yml b/examples/mistral/mixtral_22.yml index 100e4464ff..0b606b7d77 100644 --- a/examples/mistral/mixtral_22.yml +++ b/examples/mistral/mixtral_22.yml @@ -25,7 +25,7 @@ output_dir: ./outputs/out sequence_len: 8000 sample_packing: true -pad_to_sequence_len: true + gradient_accumulation_steps: 1 micro_batch_size: 1 diff --git a/examples/mistral/qlora.yml b/examples/mistral/qlora.yml index 08df36e150..a5e8b65fbf 100644 --- a/examples/mistral/qlora.yml +++ b/examples/mistral/qlora.yml @@ -20,7 +20,7 @@ lora_model_dir: sequence_len: 8192 sample_packing: true -pad_to_sequence_len: true + lora_r: 32 lora_alpha: 16 diff --git a/examples/orpheus/finetune.yml b/examples/orpheus/finetune.yml index 57f65d9666..9dcb8a43e6 100644 --- a/examples/orpheus/finetune.yml +++ b/examples/orpheus/finetune.yml @@ -18,7 +18,7 @@ output_dir: ./outputs/out sequence_len: 8192 sample_packing: true -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/phi/lora-3.5.yaml b/examples/phi/lora-3.5.yaml index 9f3bbdf539..b7f902d63b 100644 --- a/examples/phi/lora-3.5.yaml +++ b/examples/phi/lora-3.5.yaml @@ -28,7 +28,7 @@ output_dir: ./outputs/lora-out sequence_len: 4096 sample_packing: false -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/phi/phi-ft.yml b/examples/phi/phi-ft.yml index fc6d649d71..4adb62d3ab 100644 --- a/examples/phi/phi-ft.yml +++ b/examples/phi/phi-ft.yml @@ -15,7 +15,7 @@ output_dir: ./outputs/phi-sft-out sequence_len: 2048 sample_packing: true -pad_to_sequence_len: true + adapter: lora_model_dir: diff --git a/examples/phi/phi-qlora.yml b/examples/phi/phi-qlora.yml index ccd92c817f..11c08bfe65 100644 --- a/examples/phi/phi-qlora.yml +++ b/examples/phi/phi-qlora.yml @@ -18,7 +18,7 @@ output_dir: ./outputs/phi-sft-out sequence_len: 2048 sample_packing: true -pad_to_sequence_len: true + adapter: qlora lora_model_dir: diff --git a/examples/phi/phi2-ft.yml b/examples/phi/phi2-ft.yml index 853250ccbb..102c7ba037 100644 --- a/examples/phi/phi2-ft.yml +++ b/examples/phi/phi2-ft.yml @@ -15,7 +15,7 @@ output_dir: ./outputs/phi-sft-out sequence_len: 2048 sample_packing: true -pad_to_sequence_len: true + adapter: lora_model_dir: diff --git a/examples/phi/phi3-ft-fsdp.yml b/examples/phi/phi3-ft-fsdp.yml index 130298bc04..e8290ea1fa 100644 --- a/examples/phi/phi3-ft-fsdp.yml +++ b/examples/phi/phi3-ft-fsdp.yml @@ -15,7 +15,7 @@ output_dir: ./phi-sft-out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + trust_remote_code: true adapter: diff --git a/examples/phi/phi3-ft.yml b/examples/phi/phi3-ft.yml index 42b87e8d05..0b204963c4 100644 --- a/examples/phi/phi3-ft.yml +++ b/examples/phi/phi3-ft.yml @@ -18,7 +18,7 @@ output_dir: ./out sequence_len: 4096 sample_packing: true -pad_to_sequence_len: true + adapter: lora lora_model_dir: diff --git a/examples/qwen2/dpo.yaml b/examples/qwen2/dpo.yaml index 69a74ae4a4..3b1f817e5d 100644 --- a/examples/qwen2/dpo.yaml +++ b/examples/qwen2/dpo.yaml @@ -27,7 +27,7 @@ output_dir: ./outputs/dpo-out sequence_len: 2048 sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/qwen2/prm.yaml b/examples/qwen2/prm.yaml index af188f75d6..a709a598d3 100644 --- a/examples/qwen2/prm.yaml +++ b/examples/qwen2/prm.yaml @@ -22,7 +22,7 @@ remove_unused_columns: false sequence_len: 2048 sample_packing: false eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/qwen2/qlora-fsdp.yaml b/examples/qwen2/qlora-fsdp.yaml index 861ce5517e..ca435b2bba 100644 --- a/examples/qwen2/qlora-fsdp.yaml +++ b/examples/qwen2/qlora-fsdp.yaml @@ -17,7 +17,7 @@ output_dir: ./outputs/out sequence_len: 2048 sample_packing: true eval_sample_packing: true -pad_to_sequence_len: true + adapter: qlora lora_model_dir: diff --git a/examples/qwen2/reward-model.yaml b/examples/qwen2/reward-model.yaml index 1854b8216b..08b8b45527 100644 --- a/examples/qwen2/reward-model.yaml +++ b/examples/qwen2/reward-model.yaml @@ -18,7 +18,7 @@ remove_unused_columns: false sequence_len: 2048 sample_packing: false eval_sample_packing: false -pad_to_sequence_len: true + wandb_project: wandb_entity: diff --git a/examples/qwen3/32b-qlora.yaml b/examples/qwen3/32b-qlora.yaml index 1f148ece5c..87609c42f3 100644 --- a/examples/qwen3/32b-qlora.yaml +++ b/examples/qwen3/32b-qlora.yaml @@ -22,7 +22,7 @@ dataset_prepared_path: last_run_prepared sequence_len: 2048 sample_packing: true eval_sample_packing: true -pad_to_sequence_len: true + load_in_4bit: true adapter: qlora diff --git a/examples/qwen3/8b-qat-fsdp2.yml b/examples/qwen3/8b-qat-fsdp2.yml index e4d0ed4fb0..395812a56a 100644 --- a/examples/qwen3/8b-qat-fsdp2.yml +++ b/examples/qwen3/8b-qat-fsdp2.yml @@ -24,7 +24,7 @@ output_dir: ./outputs/qat_out/ sequence_len: 2048 sample_packing: true flex_attention: true -pad_to_sequence_len: true + flex_attn_compile_kwargs: dynamic: false diff --git a/examples/qwen3/qlora-fsdp.yaml b/examples/qwen3/qlora-fsdp.yaml index 762f9648d1..6af3cfbc63 100644 --- a/examples/qwen3/qlora-fsdp.yaml +++ b/examples/qwen3/qlora-fsdp.yaml @@ -16,7 +16,7 @@ output_dir: ./outputs/out sequence_len: 2048 sample_packing: true eval_sample_packing: true -pad_to_sequence_len: true + adapter: qlora lora_model_dir: diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 96e3a8a3e8..de928d11c5 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -435,7 +435,7 @@ class AxolotlInputConfig( pad_to_sequence_len: bool | None = Field( default=None, json_schema_extra={ - "description": "Pad inputs so each step uses constant sized buffers. This will reduce memory fragmentation and may prevent OOMs, by re-using memory more efficiently" + "description": "Pad inputs so each step uses constant sized buffers. This will reduce memory fragmentation and may prevent OOMs, by re-using memory more efficiently. Defaults to True if `sample_packing` enabled" }, ) curriculum_sampling: bool | None = Field( diff --git a/tests/utils/schemas/validation/test_default_values.py b/tests/utils/schemas/validation/test_default_values.py new file mode 100644 index 0000000000..332dfe77fb --- /dev/null +++ b/tests/utils/schemas/validation/test_default_values.py @@ -0,0 +1,21 @@ +"""Tests for default values for configurations""" + +from axolotl.utils.config import validate_config +from axolotl.utils.dict import DictDefault + + +class TestDefaultConfigValues: + """Tests for default values for configurations""" + + def test_pad_to_sequence_len(self, min_base_cfg): + """Tests that sample packing automatically sets pad_to_sequence_len to True""" + cfg = ( + DictDefault( + sample_packing=True, + ) + | min_base_cfg + ) + + cfg = validate_config(cfg) + + assert cfg.pad_to_sequence_len is True From fefb0797ee2e6ded50394fd2048e62f19ce6b6c2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 21 Jul 2025 11:41:15 -0400 Subject: [PATCH 0833/1405] better handling for reward function checks for GRPO (#2933) [skip ci] * better handling for reward function checks for GRPO * consolidate msg copy --- src/axolotl/core/trainers/grpo/__init__.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 3e824f705d..2c6eb8c6fa 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -2,8 +2,11 @@ import importlib import inspect +import os from typing import Any +from huggingface_hub import snapshot_download +from requests import HTTPError from trl.trainer.grpo_trainer import RewardFunc from axolotl.core.trainers.grpo.args import AxolotlGRPOConfig @@ -178,9 +181,18 @@ def get_reward_func(cls, reward_func_fqn: str) -> RewardFunc: "Reward function must accept at least two arguments: prompts: list and completions: list" ) return reward_func - except ModuleNotFoundError: + except ModuleNotFoundError as exc: # the user has passed a string (ideally indicating the path of a reward model) - LOG.info( - f"Reward function {reward_func_fqn} is a pre-trained model path - if this is unexpected, please check the reward function path." - ) - return reward_func_fqn + # check if it's a local dir path and not empty dir to a reward model + pretrained_log_msg = f"Reward function {reward_func_fqn} is a pre-trained model path - if this is unexpected, please check the reward function path." + if os.path.isdir(reward_func_fqn) and os.listdir(reward_func_fqn): + LOG.info(pretrained_log_msg) + return reward_func_fqn + try: + snapshot_download(reward_func_fqn, repo_type="model") + LOG.info(pretrained_log_msg) + return reward_func_fqn + except HTTPError: + raise ValueError( + f"Reward function {reward_func_fqn} not found." + ) from exc From e207762928fe066df2c9d2149e9f897fe2f8025f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 21 Jul 2025 11:41:31 -0400 Subject: [PATCH 0834/1405] fix deprecate deepspeed stage3_gather_16bit_weights_on_model_save arg (#2956) [skip ci] * fix deprecate deepspeed stage3_gather_16bit_weights_on_model_save arg * replace the rest of the migrated deepspeed params --- deepspeed_configs/zero3.json | 10 +++++----- deepspeed_configs/zero3_bf16.json | 10 +++++----- deepspeed_configs/zero3_bf16_cpuoffload_all.json | 10 +++++----- deepspeed_configs/zero3_bf16_cpuoffload_params.json | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/deepspeed_configs/zero3.json b/deepspeed_configs/zero3.json index 90ec3677ea..a648cbe819 100644 --- a/deepspeed_configs/zero3.json +++ b/deepspeed_configs/zero3.json @@ -5,11 +5,11 @@ "contiguous_gradients": true, "sub_group_size": 0, "reduce_bucket_size": "auto", - "stage3_prefetch_bucket_size": "auto", - "stage3_param_persistence_threshold": "auto", - "stage3_max_live_parameters": 0, - "stage3_max_reuse_distance": 0, - "stage3_gather_16bit_weights_on_model_save": true + "prefetch_bucket_size": "auto", + "param_persistence_threshold": "auto", + "max_live_parameters": 0, + "max_reuse_distance": 0, + "gather_16bit_weights_on_model_save": true }, "bf16": { "enabled": "auto" diff --git a/deepspeed_configs/zero3_bf16.json b/deepspeed_configs/zero3_bf16.json index 49fb757552..4d31a1531b 100644 --- a/deepspeed_configs/zero3_bf16.json +++ b/deepspeed_configs/zero3_bf16.json @@ -5,11 +5,11 @@ "contiguous_gradients": true, "sub_group_size": 0, "reduce_bucket_size": "auto", - "stage3_prefetch_bucket_size": "auto", - "stage3_param_persistence_threshold": "auto", - "stage3_max_live_parameters": 0, - "stage3_max_reuse_distance": 0, - "stage3_gather_16bit_weights_on_model_save": true + "prefetch_bucket_size": "auto", + "param_persistence_threshold": "auto", + "max_live_parameters": 0, + "max_reuse_distance": 0, + "gather_16bit_weights_on_model_save": true }, "bf16": { "enabled": true diff --git a/deepspeed_configs/zero3_bf16_cpuoffload_all.json b/deepspeed_configs/zero3_bf16_cpuoffload_all.json index 3ccc66db48..52fe9cdd45 100644 --- a/deepspeed_configs/zero3_bf16_cpuoffload_all.json +++ b/deepspeed_configs/zero3_bf16_cpuoffload_all.json @@ -15,11 +15,11 @@ "contiguous_gradients": true, "sub_group_size": 0, "reduce_bucket_size": "auto", - "stage3_prefetch_bucket_size": "auto", - "stage3_param_persistence_threshold": "auto", - "stage3_max_live_parameters": 0, - "stage3_max_reuse_distance": 0, - "stage3_gather_16bit_weights_on_model_save": true + "prefetch_bucket_size": "auto", + "param_persistence_threshold": "auto", + "max_live_parameters": 0, + "max_reuse_distance": 0, + "gather_16bit_weights_on_model_save": true }, "bf16": { "enabled": true diff --git a/deepspeed_configs/zero3_bf16_cpuoffload_params.json b/deepspeed_configs/zero3_bf16_cpuoffload_params.json index fe21d35f88..81ac1d1d87 100644 --- a/deepspeed_configs/zero3_bf16_cpuoffload_params.json +++ b/deepspeed_configs/zero3_bf16_cpuoffload_params.json @@ -11,11 +11,11 @@ "contiguous_gradients": true, "sub_group_size": 0, "reduce_bucket_size": "auto", - "stage3_prefetch_bucket_size": "auto", - "stage3_param_persistence_threshold": "auto", - "stage3_max_live_parameters": 0, - "stage3_max_reuse_distance": 0, - "stage3_gather_16bit_weights_on_model_save": true + "prefetch_bucket_size": "auto", + "param_persistence_threshold": "auto", + "max_live_parameters": 0, + "max_reuse_distance": 0, + "gather_16bit_weights_on_model_save": true }, "bf16": { "enabled": true From b7e8f66e5a78d775f5bb9e7f0aa2f2f6a2458b71 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 21 Jul 2025 11:41:53 -0400 Subject: [PATCH 0835/1405] upstream fixes in cce for dora and tensor paralel support (#2960) [skip ci] --- examples/colab-notebooks/colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/integrations/cut_cross_entropy/README.md | 2 +- src/axolotl/integrations/cut_cross_entropy/__init__.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 112658007c..fd78cf7b41 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@50cef19\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@631d646\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 6840aef50a..63e527d5e6 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@50cef19"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@631d646"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index dc7c908ddc..07c6de1f88 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@50cef19" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@631d646" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 6c47097b73..1fe54deedd 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -34,7 +34,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@50cef19"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@631d646"`' ) From bc1076d8a2fe567c64be3c040df3619fe9000109 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 21 Jul 2025 22:42:04 +0700 Subject: [PATCH 0836/1405] fix: suppress warning if we enabled skip prepare (#2958) --- src/axolotl/utils/data/sft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index aa88e9924a..3189b29c36 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -308,7 +308,7 @@ def _load_raw_datasets( ) -> tuple[Dataset, list[Prompter | None]]: """Load, process, merge, and save raw datasets.""" LOG.info("Loading raw datasets...", main_process_only=False) - if not cfg.is_preprocess: + if not cfg.is_preprocess and not cfg.skip_prepare_dataset: LOG.warning( "Processing datasets during training can lead to VRAM instability. Please " "pre-process your dataset using `axolotl preprocess path/to/config.yml`." From d32058e1492cd3900af6ddc01fe708709a20a9b4 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 22 Jul 2025 04:19:16 -0400 Subject: [PATCH 0837/1405] include torchvision in build for upstream changes requiring it now (#2953) [skip ci] --- .github/workflows/tests-nightly.yml | 2 +- .github/workflows/tests.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 00b6242433..fc6c2b396e 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -52,7 +52,7 @@ jobs: - name: Install PyTorch run: | - pip3 install torch==${{ matrix.pytorch_version }} + pip3 install torch==${{ matrix.pytorch_version }} torchvision - name: Update requirements.txt run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9c983ad707..d4818f280f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -78,7 +78,7 @@ jobs: - name: Install PyTorch run: | - pip3 install torch==${{ matrix.pytorch_version }} + pip3 install torch==${{ matrix.pytorch_version }} torchvision - name: Install dependencies run: | @@ -151,7 +151,7 @@ jobs: - name: Install PyTorch run: | - pip3 install torch==${{ matrix.pytorch_version }} + pip3 install torch==${{ matrix.pytorch_version }} torchvision - name: Install dependencies run: | From dfba881e998c39e0ba1e0ca18a7e7ef408743e77 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 22 Jul 2025 16:52:15 +0700 Subject: [PATCH 0838/1405] Feat: add gemma3n support (#2852) * feat: add gemma3n cce * feat: add sample config * feat: add gemma3n multimodal mode * feat: add audio example * feat: support audio and return pixel values in collator * feat: support unmask only assistant region (gemma3n for now) * feat(doc): add notes for audio loading * feat: add audio support for gemma3n * feat: update examples * feat: add gemma3n to the docs * fix: add link at top * feat(doc): clarify additional requirements * fix: mllama missing aspect ratio * fix: mllama need attention fixes for fa2 * Partially Revert "fix: mllama need attention fixes for fa2" This reverts commit a0bfdd17770ddf76b17a2cbf478fe8e7fde5e39e. * fix: disable FA2 for mllama in vision mode * feat: update configs to use proper attention * fix: support other vision features * feat(doc): clarify requirements for gemma3n --- docs/multimodal.qmd | 43 ++++++- examples/gemma3n/README.md | 19 +++ examples/gemma3n/gemma-3n-e2b-qlora.yml | 74 ++++++++++++ .../gemma-3n-e2b-vision-audio-qlora.yml | 80 +++++++++++++ .../gemma3n/gemma-3n-e2b-vision-qlora.yml | 75 ++++++++++++ examples/llama-3-vision/lora-11b.yaml | 9 +- examples/llava/lora-7b.yaml | 5 +- .../mistral/mistral-small-3.1-24B-lora.yml | 4 +- examples/pixtral/lora-12b.yml | 9 +- .../integrations/cut_cross_entropy/README.md | 2 + src/axolotl/loaders/constants.py | 2 + src/axolotl/processing_strategies.py | 108 +++++++++++++++++- .../chat_templates/templates/gemma3n.jinja | 49 ++++++++ src/axolotl/utils/collators/mm_chat.py | 11 ++ src/axolotl/utils/schemas/enums.py | 1 + 15 files changed, 473 insertions(+), 18 deletions(-) create mode 100644 examples/gemma3n/README.md create mode 100644 examples/gemma3n/gemma-3n-e2b-qlora.yml create mode 100644 examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml create mode 100644 examples/gemma3n/gemma-3n-e2b-vision-qlora.yml create mode 100644 src/axolotl/utils/chat_templates/templates/gemma3n.jinja diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index ec51a8ec3f..dbb365f730 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -14,6 +14,7 @@ format: - [Llava-1.5](#sec-llava-15) - [Mistral-Small-3.1](#sec-mistral-small-31) - [Gemma-3](#sec-gemma-3) +- [Gemma-3n](#sec-gemma-3n) - [Qwen2-VL](#sec-qwen2-vl) - [Qwen2.5-VL](#sec-qwen25-vl) @@ -110,6 +111,22 @@ base_model: google/gemma-3-4b-it chat_template: gemma3 ``` +### Gemma-3n {#sec-gemma-3n} + +::: {.callout-warning} +The model's initial loss and grad norm will be very high. We suspect this to be due to the Conv in the vision layers. +::: + +::: {.callout-tip} +Please make sure to install `timm` via `pip3 install timm==1.0.17` +::: + +```yaml +base_model: google/gemma-3n-E2B-it + +chat_template: gemma3n +``` + ### Qwen2-VL {#sec-qwen2-vl} ```yaml @@ -132,7 +149,9 @@ For multi-modal datasets, we adopt an extended `chat_template` format similar to - A message is a list of `role` and `content`. - `role` can be `system`, `user`, `assistant`, etc. -- `content` is a list of `type` and (`text` or `image` or `path` or `url` or `base64`). +- `content` is a list of `type` and (`text`, `image`, `path`, `url`, `base64`, or `audio`). + +### Image ::: {.callout-note} For backwards compatibility: @@ -141,15 +160,29 @@ For backwards compatibility: - If `content` is a string, it will be converted to a list with `type` as `text`. ::: -::: {.callout-tip} For image loading, you can use the following keys within `content` alongside `"type": "image"`: - `"path": "/path/to/image.jpg"` - `"url": "https://example.com/image.jpg"` - `"base64": "..."` - `"image": PIL.Image` + +### Audio + +For audio loading, you can use the following keys within `content` alongside `"type": "audio"`: + +- `"path": "/path/to/audio.mp3"` +- `"url": "https://example.com/audio.mp3"` +- `"audio": np.ndarray` + +::: {.callout-tip} + +You may need to install `librosa` via `pip3 install librosa==0.11.0`. + ::: +### Example + Here is an example of a multi-modal dataset: ```json [ @@ -178,3 +211,9 @@ Here is an example of a multi-modal dataset: } ] ``` + +## FAQ + +1. `PIL.UnidentifiedImageError: cannot identify image file ...` + +`PIL` could not retrieve the file at `url` using `requests`. Please check for typo. One alternative reason is that the request is blocked by the server. diff --git a/examples/gemma3n/README.md b/examples/gemma3n/README.md new file mode 100644 index 0000000000..b3922d5264 --- /dev/null +++ b/examples/gemma3n/README.md @@ -0,0 +1,19 @@ +# Gemma-3n + +## Requirements + +In addition to Axolotl's requirements, Gemma-3n requires + +``` +pip3 install timm +``` + +If you will load audio datasets, please also install + +``` +pip3 install librosa +``` + +## Usage + +See example configs and the [multimodal doc](https://docs.axolotl.ai/docs/multimodal.html). diff --git a/examples/gemma3n/gemma-3n-e2b-qlora.yml b/examples/gemma3n/gemma-3n-e2b-qlora.yml new file mode 100644 index 0000000000..7868af59e3 --- /dev/null +++ b/examples/gemma3n/gemma-3n-e2b-qlora.yml @@ -0,0 +1,74 @@ +base_model: google/gemma-3n-E2B-it + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +cut_cross_entropy: true + +load_in_8bit: false +load_in_4bit: true + +# for use with fft to only train on language model layers +# unfrozen_parameters: + # - model.language_model.* + # - lm_head + # - embed_tokens + + +chat_template: gemma3n +eot_tokens: + - +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + split: train[:1%] + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +# lora_target_linear: # Does not work with gemma3n currently +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: true +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 4 +optimizer: muon +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +# flash_attention: true # Any attention impl does not work with gemma3n now + +warmup_ratio: 0.1 +evals_per_epoch: +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml b/examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml new file mode 100644 index 0000000000..6cdf5573eb --- /dev/null +++ b/examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml @@ -0,0 +1,80 @@ +base_model: google/gemma-3n-E2B-it +processor_type: AutoProcessor + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +cut_cross_entropy: true + +# for use with fft to only train on language model layers +# unfrozen_parameters: + # - model.language_model.* + # - lm_head + # - embed_tokens + +load_in_4bit: true + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +# gemma3 doesn't seem to play nice with ddp +ddp_find_unused_parameters: true + +chat_template: gemma3n +eot_tokens: + - + +# sample dataset below requires downloading audio/image in advance +# wget https://huggingface.co/datasets/Nanobit/text-vision-audio-2k-test/resolve/main/African_elephant.jpg +# wget https://huggingface.co/datasets/Nanobit/text-vision-audio-2k-test/resolve/main/En-us-African_elephant.oga +datasets: + - path: Nanobit/text-vision-audio-2k-test + type: chat_template + data_files: + - dataset.jsonl +dataset_prepared_path: +val_set_size: 0.01 +output_dir: ./outputs/out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: muon +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +logging_steps: 1 +# flash_attention: true # Any attention impl does not work with gemma3n now + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 diff --git a/examples/gemma3n/gemma-3n-e2b-vision-qlora.yml b/examples/gemma3n/gemma-3n-e2b-vision-qlora.yml new file mode 100644 index 0000000000..519edecc7e --- /dev/null +++ b/examples/gemma3n/gemma-3n-e2b-vision-qlora.yml @@ -0,0 +1,75 @@ +base_model: google/gemma-3n-E2B-it +processor_type: AutoProcessor + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +cut_cross_entropy: true + +# for use with fft to only train on language model layers +# unfrozen_parameters: + # - model.language_model.* + # - lm_head + # - embed_tokens + +load_in_4bit: true + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +# gemma3 doesn't seem to play nice with ddp +ddp_find_unused_parameters: true + +chat_template: gemma3n +eot_tokens: + - +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] +dataset_prepared_path: +val_set_size: 0.01 +output_dir: ./outputs/out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: muon +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +logging_steps: 1 +# flash_attention: true # Any attention impl does not work with gemma3n now + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 diff --git a/examples/llama-3-vision/lora-11b.yaml b/examples/llama-3-vision/lora-11b.yaml index 64d749b5a9..da28ace3b5 100644 --- a/examples/llama-3-vision/lora-11b.yaml +++ b/examples/llama-3-vision/lora-11b.yaml @@ -15,8 +15,7 @@ datasets: - path: HuggingFaceH4/llava-instruct-mix-vsft type: chat_template split: train[:1%] - field_messages: messages -dataset_prepared_path: last_run_prepared +dataset_prepared_path: val_set_size: 0.0 output_dir: ./outputs/out @@ -40,7 +39,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 1 num_epochs: 1 -optimizer: adamw_bnb_8bit +optimizer: muon lr_scheduler: cosine learning_rate: 0.0002 @@ -50,8 +49,8 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true -eager_attention: +# flash_attention: true # use for text-only mode +sdp_attention: true warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/llava/lora-7b.yaml b/examples/llava/lora-7b.yaml index a4bac8987d..53ae975424 100644 --- a/examples/llava/lora-7b.yaml +++ b/examples/llava/lora-7b.yaml @@ -11,8 +11,7 @@ datasets: - path: HuggingFaceH4/llava-instruct-mix-vsft type: chat_template split: train[:1%] - field_messages: messages -dataset_prepared_path: last_run_prepared +dataset_prepared_path: val_set_size: 0.0 output_dir: ./outputs/out @@ -36,7 +35,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 1 num_epochs: 1 -optimizer: adamw_bnb_8bit +optimizer: muon lr_scheduler: cosine learning_rate: 0.0002 diff --git a/examples/mistral/mistral-small-3.1-24B-lora.yml b/examples/mistral/mistral-small-3.1-24B-lora.yml index 4a492c5953..3e477645e7 100644 --- a/examples/mistral/mistral-small-3.1-24B-lora.yml +++ b/examples/mistral/mistral-small-3.1-24B-lora.yml @@ -48,8 +48,8 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: false # PixtralVisionModel does not support Flash Attention 2.0 yet. -eager_attention: +# flash_attention: false # PixtralVisionModel does not support Flash Attention 2.0 yet. +sdp_attention: true warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/pixtral/lora-12b.yml b/examples/pixtral/lora-12b.yml index ea769d202c..fc4c0667c2 100644 --- a/examples/pixtral/lora-12b.yml +++ b/examples/pixtral/lora-12b.yml @@ -11,8 +11,7 @@ datasets: - path: HuggingFaceH4/llava-instruct-mix-vsft type: chat_template split: train[:1%] - field_messages: messages -dataset_prepared_path: last_run_prepared +dataset_prepared_path: val_set_size: 0.0 output_dir: ./outputs/out @@ -36,7 +35,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 1 num_epochs: 1 -optimizer: adamw_bnb_8bit +optimizer: muon lr_scheduler: cosine learning_rate: 0.0002 @@ -46,8 +45,8 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: false # PixtralVisionModel does not support Flash Attention 2.0 yet -eager_attention: +# flash_attention: # PixtralVisionModel does not support Flash Attention 2.0 yet +sdp_attention: true warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 07c6de1f88..a97bac71c3 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -37,6 +37,8 @@ plugins: - gemma2 - gemma3 - gemma3_text +- gemma3n +- gemma3n_text - glm - glm4 - llama diff --git a/src/axolotl/loaders/constants.py b/src/axolotl/loaders/constants.py index c08518dd60..c340c414c7 100644 --- a/src/axolotl/loaders/constants.py +++ b/src/axolotl/loaders/constants.py @@ -2,6 +2,7 @@ from transformers import ( Gemma3ForConditionalGeneration, + Gemma3nForConditionalGeneration, Llama4ForConditionalGeneration, LlavaForConditionalGeneration, Mistral3ForConditionalGeneration, @@ -18,4 +19,5 @@ "qwen2_5_vl": Qwen2_5_VLForConditionalGeneration, "mistral3": Mistral3ForConditionalGeneration, "gemma3": Gemma3ForConditionalGeneration, + "gemma3n": Gemma3nForConditionalGeneration, } diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 0806974000..1cb2974068 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -5,7 +5,7 @@ from PIL import Image, ImageOps from PIL.Image import Resampling -from torch import Tensor +from torch import Tensor, zeros_like from transformers import ProcessorMixin from transformers.image_utils import load_image @@ -208,9 +208,18 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: return processed_examples + def _mask_non_assistant(self, labels: Tensor) -> Tensor: + """ + Mask non assistant regions to -100. + To be implemented per subclass. + """ + return labels + def process_labels(self, input_ids: Tensor) -> Tensor: labels = input_ids.clone() + labels = self._mask_non_assistant(labels) + # The labels are the input_ids, and we mask the padding tokens in the loss computation labels[labels == self.processor.tokenizer.pad_token_id] = -100 @@ -264,6 +273,99 @@ def process_labels(self, input_ids): return labels +class Gemma3nProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for Gemma3n""" + + def _mask_non_assistant(self, labels: Tensor) -> Tensor: + def _find_token_sequence(label, start_pos, token_sequence): + """Check if token_sequence appears at start_pos in label""" + if start_pos + len(token_sequence) > len(label): + return False + if label[start_pos] != token_sequence[0]: + return False + return ( + label[start_pos : start_pos + len(token_sequence)].tolist() + == token_sequence + ) + + def _find_assistant_end(label, start_pos, assistant_end_tok, mask, i): + """ + Find the end of assistant response and update mask accordingly + + Returns new position to continue from and whether the end seq is found + """ + k = start_pos + while k < len(label): + if not _find_token_sequence(label, k, assistant_end_tok): + mask[i][k] = 1 + k += 1 + continue + + return k + len(assistant_end_tok), True + + return k, False + + mask = zeros_like(labels) + + assistant_start_str = "model" + assistant_end_str = "" + include_assistant_start_tok = False + include_assistant_end_tok = True + + # str to tokens + assistant_start_tok = self.processor.tokenizer.encode( + assistant_start_str, add_special_tokens=False + ) + assistant_end_tok = self.processor.tokenizer.encode( + assistant_end_str, add_special_tokens=False + ) + + for i, label in enumerate(labels): + j = 0 + # while loop through each tok index in labels[i] + while j < len(label): + # Check until match start seq + if not _find_token_sequence(label, j, assistant_start_tok): + j += 1 + continue + + if include_assistant_start_tok: + mask[i][j : j + len(assistant_start_tok)] = 1 + + # Find where the assistant response ends + start_of_content = j + len(assistant_start_tok) + end_pos, found_end_seq = _find_assistant_end( + label, start_of_content, assistant_end_tok, mask, i + ) + + # Include end token if requested + if include_assistant_end_tok and found_end_seq: + mask[i][end_pos - len(assistant_end_tok) : end_pos] = 1 + + j = end_pos + + labels[i][mask[i] == 0] = -100 + + return labels + + def process_labels(self, input_ids): + labels = input_ids.clone() + labels = self._mask_non_assistant(labels) + + # Follows https://colab.research.google.com/github/huggingface/huggingface-gemma-recipes/blob/main/notebooks/fine_tune_gemma3n_on_t4.ipynb + labels[labels == self.processor.tokenizer.pad_token_id] = -100 + if hasattr(self.processor.tokenizer, "image_token_id"): + labels[labels == self.processor.tokenizer.image_token_id] = -100 + if hasattr(self.processor.tokenizer, "audio_token_id"): + labels[labels == self.processor.tokenizer.audio_token_id] = -100 + if hasattr(self.processor.tokenizer, "boi_token_id"): + labels[labels == self.processor.tokenizer.boi_token_id] = -100 + if hasattr(self.processor.tokenizer, "eoi_token_id"): + labels[labels == self.processor.tokenizer.eoi_token_id] = -100 + + return labels + + def get_processing_strategy( processor: ProcessorMixin, chat_template, @@ -279,6 +381,10 @@ def get_processing_strategy( return Gemma3ProcessingStrategy( processor, chat_template, image_size, image_resize_algorithm ) + if chat_template_type == "gemma3n": + return Gemma3nProcessingStrategy( + processor, chat_template, image_size, image_resize_algorithm + ) if chat_template_type in [ "llama3_2_vision", "llama4", diff --git a/src/axolotl/utils/chat_templates/templates/gemma3n.jinja b/src/axolotl/utils/chat_templates/templates/gemma3n.jinja new file mode 100644 index 0000000000..a0405ea9c2 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/gemma3n.jinja @@ -0,0 +1,49 @@ +{{ bos_token }} +{%- if messages[0]['role'] == 'system' -%} + {%- if messages[0]['content'] is string -%} + {%- set first_user_prefix = messages[0]['content'] + ' + +' -%} + {%- else -%} + {%- set first_user_prefix = messages[0]['content'][0]['text'] + ' + +' -%} + {%- endif -%} + {%- set loop_messages = messages[1:] -%} +{%- else -%} + {%- set first_user_prefix = "" -%} + {%- set loop_messages = messages -%} +{%- endif -%} +{%- for message in loop_messages -%} + {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) -%} + {{ raise_exception("Conversation roles must alternate user/assistant/user/assistant/...") }} + {%- endif -%} + {%- if (message['role'] == 'assistant') -%} + {%- set role = "model" -%} + {%- else -%} + {%- set role = message['role'] -%} + {%- endif -%} + {{ '' + role + ' +' + (first_user_prefix if loop.first else "") }} + {%- if message['content'] is string -%} + {{ message['content'] | trim }} + {%- elif message['content'] is iterable -%} + {%- for item in message['content'] -%} + {%- if item['type'] == 'audio' -%} + {{ '' }} + {%- elif item['type'] == 'image' -%} + {{ '' }} + {%- elif item['type'] == 'text' -%} + {{ item['text'] | trim }} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {{ raise_exception("Invalid content type") }} + {%- endif -%} + {{ ' +' }} +{%- endfor -%} +{%- if add_generation_prompt -%} + {{'model +'}} +{%- endif -%} diff --git a/src/axolotl/utils/collators/mm_chat.py b/src/axolotl/utils/collators/mm_chat.py index 8b9d728d52..0075d48301 100644 --- a/src/axolotl/utils/collators/mm_chat.py +++ b/src/axolotl/utils/collators/mm_chat.py @@ -84,6 +84,17 @@ def process_rows( "attention_mask": attention_mask, } + for key, val in batch.items(): + if key in ["input_ids", "attention_mask"]: + continue + + if key in ["token_type_ids", "cross_attention_mask"]: + final_batch[key] = torch.nn.utils.rnn.pad_sequence( + val, batch_first=True, padding_value=0 + ) + else: + final_batch[key] = torch.stack(val) + # Process the labels final_batch["labels"] = self.processing_strategy.process_labels( final_batch["input_ids"] diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 67fc7a8a75..3c88283962 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -62,6 +62,7 @@ class ChatTemplate(str, Enum): llava = "llava" qwen2_vl = "qwen2_vl" gemma3 = "gemma3" + gemma3n = "gemma3n" command_a = "command_a" command_a_tool_use = "command_a_tool_use" command_a_rag = "command_a_rag" From 7267edc1686e7745a893ef4c9247634703470c72 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 08:30:00 -0400 Subject: [PATCH 0839/1405] chore: update pre-commit hooks (#2954) [skip ci] Co-authored-by: djsaunde <1245942+djsaunde@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45816defa7..0734e8cccb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: hooks: - id: pylint - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.1 + rev: v1.17.0 hooks: - id: mypy additional_dependencies: From 3a208cfd84a826eedc7eb6ec6c67546a2c28b8b0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 22 Jul 2025 08:30:31 -0400 Subject: [PATCH 0840/1405] Autocomplete axolotl CLI (#2955) * static autocomplete script for axolotl cli * use list of commands that should autocomplete yaml files * make sure to chmod the autocomplete script as executable * shellcheck and fix autocompletion of directory/sub-dirs * more shellcheck fixes --- .axolotl-complete.bash | 41 +++++++++++++++++++++++++++++++++++++++++ docker/Dockerfile | 6 +++++- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 .axolotl-complete.bash diff --git a/.axolotl-complete.bash b/.axolotl-complete.bash new file mode 100644 index 0000000000..9a51399e65 --- /dev/null +++ b/.axolotl-complete.bash @@ -0,0 +1,41 @@ +#!/bin/bash + +_axolotl_completions() { + local cur prev + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + # If we're completing the first argument (the command) + if [[ $COMP_CWORD -eq 1 ]]; then + mapfile -t COMPREPLY < <(compgen -W "delinearize-llama4 fetch lm-eval merge-sharded-fsdp-weights quantize vllm-serve evaluate inference merge-lora preprocess train" -- "$cur") + return 0 + fi + + # Commands that should complete with directories and YAML files + local -a yaml_commands=("merge-sharded-fsdp-weights" "quantize" "vllm-serve" "evaluate" "inference" "merge-lora" "preprocess" "train") + + # Check if previous word is in our list + if [[ " ${yaml_commands[*]} " =~ (^|[[:space:]])$prev($|[[:space:]]) ]]; then + # Use filename completion which handles directories properly + compopt -o filenames + mapfile -t COMPREPLY < <(compgen -f -- "$cur") + + # Filter to only include directories and YAML files + local -a filtered=() + for item in "${COMPREPLY[@]}"; do + if [[ -d "$item" ]] || [[ "$item" == *.yaml ]] || [[ "$item" == *.yml ]]; then + filtered+=("$item") + fi + done + COMPREPLY=("${filtered[@]}") + + return 0 + fi + + # Default: no completion + return 0 +} + +# Remove the -o nospace option - let filenames handle it +complete -F _axolotl_completions axolotl diff --git a/docker/Dockerfile b/docker/Dockerfile index 9bc34c3877..116361dcdb 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -31,7 +31,11 @@ RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ pip install pytest && \ pip cache purge -# fix so that git fetch/pull from remote works +# fix so that git fetch/pull from remote works with shallow clone RUN git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && \ git config --get remote.origin.fetch && \ git config --global credential.helper store + +COPY .axolotl-complete.bash /root/.axolotl-complete.bash +RUN chmod +x /root/.axolotl-complete.bash && \ + echo 'source /root/.axolotl-complete.bash' >> ~/.bashrc From 631268a0caa18250a30a1c837c4d7f4d9505adc0 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 22 Jul 2025 20:59:47 +0700 Subject: [PATCH 0841/1405] revert renaming of deepspeed stage3 args that use auto (#2964) [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "fix deprecate deepspeed stage3_gather_16bit_weights_on_model_save arg…" This reverts commit e207762928fe066df2c9d2149e9f897fe2f8025f. * don't revert the values that don't use 'auto' --------- Co-authored-by: Wing Lian --- deepspeed_configs/zero3.json | 4 ++-- deepspeed_configs/zero3_bf16.json | 4 ++-- deepspeed_configs/zero3_bf16_cpuoffload_all.json | 4 ++-- deepspeed_configs/zero3_bf16_cpuoffload_params.json | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/deepspeed_configs/zero3.json b/deepspeed_configs/zero3.json index a648cbe819..f8c9cdfe0d 100644 --- a/deepspeed_configs/zero3.json +++ b/deepspeed_configs/zero3.json @@ -5,8 +5,8 @@ "contiguous_gradients": true, "sub_group_size": 0, "reduce_bucket_size": "auto", - "prefetch_bucket_size": "auto", - "param_persistence_threshold": "auto", + "stage3_prefetch_bucket_size": "auto", + "stage3_param_persistence_threshold": "auto", "max_live_parameters": 0, "max_reuse_distance": 0, "gather_16bit_weights_on_model_save": true diff --git a/deepspeed_configs/zero3_bf16.json b/deepspeed_configs/zero3_bf16.json index 4d31a1531b..a69e13cf7c 100644 --- a/deepspeed_configs/zero3_bf16.json +++ b/deepspeed_configs/zero3_bf16.json @@ -5,8 +5,8 @@ "contiguous_gradients": true, "sub_group_size": 0, "reduce_bucket_size": "auto", - "prefetch_bucket_size": "auto", - "param_persistence_threshold": "auto", + "stage3_prefetch_bucket_size": "auto", + "stage3_param_persistence_threshold": "auto", "max_live_parameters": 0, "max_reuse_distance": 0, "gather_16bit_weights_on_model_save": true diff --git a/deepspeed_configs/zero3_bf16_cpuoffload_all.json b/deepspeed_configs/zero3_bf16_cpuoffload_all.json index 52fe9cdd45..5112c570b0 100644 --- a/deepspeed_configs/zero3_bf16_cpuoffload_all.json +++ b/deepspeed_configs/zero3_bf16_cpuoffload_all.json @@ -15,8 +15,8 @@ "contiguous_gradients": true, "sub_group_size": 0, "reduce_bucket_size": "auto", - "prefetch_bucket_size": "auto", - "param_persistence_threshold": "auto", + "stage3_prefetch_bucket_size": "auto", + "stage3_param_persistence_threshold": "auto", "max_live_parameters": 0, "max_reuse_distance": 0, "gather_16bit_weights_on_model_save": true diff --git a/deepspeed_configs/zero3_bf16_cpuoffload_params.json b/deepspeed_configs/zero3_bf16_cpuoffload_params.json index 81ac1d1d87..a2ac823410 100644 --- a/deepspeed_configs/zero3_bf16_cpuoffload_params.json +++ b/deepspeed_configs/zero3_bf16_cpuoffload_params.json @@ -11,8 +11,8 @@ "contiguous_gradients": true, "sub_group_size": 0, "reduce_bucket_size": "auto", - "prefetch_bucket_size": "auto", - "param_persistence_threshold": "auto", + "stage3_prefetch_bucket_size": "auto", + "stage3_param_persistence_threshold": "auto", "max_live_parameters": 0, "max_reuse_distance": 0, "gather_16bit_weights_on_model_save": true From 01d8175d48a6826b302b2f28b12b3aae70d1b0e1 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 22 Jul 2025 21:00:30 +0700 Subject: [PATCH 0842/1405] fix: revert changing default optimizer to muon (#2965) [skip ci] --- examples/gemma3n/gemma-3n-e2b-qlora.yml | 2 +- examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml | 2 +- examples/gemma3n/gemma-3n-e2b-vision-qlora.yml | 2 +- examples/llama-3-vision/lora-11b.yaml | 2 +- examples/llava/lora-7b.yaml | 2 +- examples/pixtral/lora-12b.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/gemma3n/gemma-3n-e2b-qlora.yml b/examples/gemma3n/gemma-3n-e2b-qlora.yml index 7868af59e3..ad7ab5726b 100644 --- a/examples/gemma3n/gemma-3n-e2b-qlora.yml +++ b/examples/gemma3n/gemma-3n-e2b-qlora.yml @@ -53,7 +53,7 @@ wandb_log_model: gradient_accumulation_steps: 1 micro_batch_size: 1 num_epochs: 4 -optimizer: muon +optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 diff --git a/examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml b/examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml index 6cdf5573eb..15afb6f2e8 100644 --- a/examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml +++ b/examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml @@ -60,7 +60,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 2 num_epochs: 1 -optimizer: muon +optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 diff --git a/examples/gemma3n/gemma-3n-e2b-vision-qlora.yml b/examples/gemma3n/gemma-3n-e2b-vision-qlora.yml index 519edecc7e..c87eca663a 100644 --- a/examples/gemma3n/gemma-3n-e2b-vision-qlora.yml +++ b/examples/gemma3n/gemma-3n-e2b-vision-qlora.yml @@ -55,7 +55,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 2 num_epochs: 1 -optimizer: muon +optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 diff --git a/examples/llama-3-vision/lora-11b.yaml b/examples/llama-3-vision/lora-11b.yaml index da28ace3b5..adbb616437 100644 --- a/examples/llama-3-vision/lora-11b.yaml +++ b/examples/llama-3-vision/lora-11b.yaml @@ -39,7 +39,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 1 num_epochs: 1 -optimizer: muon +optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 diff --git a/examples/llava/lora-7b.yaml b/examples/llava/lora-7b.yaml index 53ae975424..77ef7474db 100644 --- a/examples/llava/lora-7b.yaml +++ b/examples/llava/lora-7b.yaml @@ -35,7 +35,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 1 num_epochs: 1 -optimizer: muon +optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 diff --git a/examples/pixtral/lora-12b.yml b/examples/pixtral/lora-12b.yml index fc4c0667c2..fea2a60ff3 100644 --- a/examples/pixtral/lora-12b.yml +++ b/examples/pixtral/lora-12b.yml @@ -35,7 +35,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 1 num_epochs: 1 -optimizer: muon +optimizer: adamw_bnb_8bit lr_scheduler: cosine learning_rate: 0.0002 From b86a1d47b02a7f9c31199370b2724f0e1d0e3941 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 22 Jul 2025 10:00:53 -0400 Subject: [PATCH 0843/1405] we don't need to call check_dataset_labels when skip_prepare_dataset is set (#2962) * we don't need to call check_dataset_labels when skip_prepare_dataset is set * Fix actual bug and revert prior fix * warn and early return instead of raising an error * use error --- src/axolotl/cli/preprocess.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index ebadc9bf16..595eb8aac4 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -37,10 +37,11 @@ def do_preprocess(cfg: DictDefault, cli_args: PreprocessCliArgs) -> None: check_user_token() for key in ["skip_prepare_dataset", "pretraining_dataset"]: - if cfg.get("key"): - raise ValueError( + if cfg.get(key): + LOG.error( f"You have set `{key}:`. `preprocess` is not needed. Run the `axolotl train` CLI directly instead." ) + return if not cfg.dataset_prepared_path: msg = ( From 208fb7b8e7da325d0a36b2c69ef66ce230e5b126 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Tue, 22 Jul 2025 16:27:47 -0400 Subject: [PATCH 0844/1405] basic torchao fp8 mixed precision training (#2926) * debug * debug * debug * revert unneeded change * add accelerator config to base trainer builder * add back accumulated_cache_size_limit setting * lint * accelerator constructor patch for single-GPU torch fp8 * lint * re-using existing fp8 code * lint * remove accelerate patch now fix in latest release * fix * docs * add fp8 + fsdp2 example * remove unused config * update config * smoke tests * add validator * add 2.7.0 guard for fsdp2 * fix * add config descriptions * add FSDP doc link * nit * set force_recompute_fp8_weight_in_bwd with enable_fsdp_float8_all_gather * better cfg for smoke tests * add test for accelerate patching * update fp8 validator --- _quarto.yml | 2 + docs/mixed_precision.qmd | 149 ++++++++++++++++++ examples/llama-3/3b-fp8-fsdp2.yaml | 76 +++++++++ src/axolotl/core/trainers/base.py | 18 ++- src/axolotl/loaders/patch_manager.py | 4 +- .../monkeypatch/trainer_accelerator_args.py | 11 +- src/axolotl/utils/schemas/config.py | 15 +- src/axolotl/utils/schemas/validation.py | 30 ++++ tests/e2e/integrations/test_fp8.py | 62 ++++++++ tests/e2e/multigpu/test_fp8_fsdp2.py | 120 ++++++++++++++ .../test_trainer_accelerator_args.py | 26 +++ 11 files changed, 503 insertions(+), 10 deletions(-) create mode 100644 docs/mixed_precision.qmd create mode 100644 examples/llama-3/3b-fp8-fsdp2.yaml create mode 100644 tests/e2e/integrations/test_fp8.py create mode 100644 tests/e2e/multigpu/test_fp8_fsdp2.py create mode 100644 tests/monkeypatch/test_trainer_accelerator_args.py diff --git a/_quarto.yml b/_quarto.yml index 3e773a748f..dab1ee363b 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -268,6 +268,8 @@ website: - docs/batch_vs_grad.qmd - docs/dataset_preprocessing.qmd - docs/multipack.qmd + - docs/mixed_precision.qmd + - docs/gradient_accumulation.qmd - section: "Advanced Features" contents: diff --git a/docs/mixed_precision.qmd b/docs/mixed_precision.qmd new file mode 100644 index 0000000000..7b77cd4bb4 --- /dev/null +++ b/docs/mixed_precision.qmd @@ -0,0 +1,149 @@ +--- +title: "Mixed Precision Training" +format: + html: + toc: true + toc-depth: 3 + number-sections: true + code-tools: true +execute: + enabled: false +--- + +Mixed precision training uses lower precision data types to reduce memory usage and increase training speed while maintaining model quality. Axolotl supports several mixed precision formats: + +- **FP16** - Half precision 16-bit (Pascal generation+) +- **BF16** - Brain Float 16-bit (Ampere generation+) +- **FP8** - 8-bit floating point (Hopper generation+) + +## FP16 Mixed Precision {#sec-fp16} + +### Overview {#sec-fp16-overview} + +FP16 is the traditional half-precision format, supported on older GPUs but can be less numerically stable than BF16. + +### Configuration {#sec-fp16-config} + +```{.yaml} +fp16: true +``` + +### FP16 Considerations {#sec-fp16-considerations} + +- May require gradient scaling to prevent underflow +- Less numerically stable than BF16 +- Can cause training instability with some model architectures +- Consider using BF16 if your hardware supports it + +## BF16 Mixed Precision {#sec-bf16} + +### Overview {#sec-bf16-overview} + +BF16 (Brain Float 16) offers better numerical stability than FP16 and is the recommended mixed precision format for modern GPUs. It provides the same dynamic range as FP32 while using half the memory. + +### Configuration {#sec-bf16-config} + +```{.yaml} +# Automatic BF16 detection (recommended) +bf16: auto + +# Or explicitly enable +bf16: true + +# For evaluation with BF16 +bf16: full # Equivalent to bf16_full_eval in the HF trainer +``` + +## FP8 Mixed Precision {#sec-fp8} + +::: {.callout-note} +FP8 support is experimental and requires compatible hardware (H100, H200) and recent PyTorch versions with TorchAO. +::: + +### What is FP8? {#sec-fp8-overview} + +FP8 (8-bit floating point) can provide significant time savings compared to FP16/BF16 while maintaining training stability. Axolotl's implementation uses PyTorch's TorchAO library with "tensorwise" scaling strategy. + +### Requirements {#sec-fp8-software} + +- Hopper+ GPUs (H100/H200) +- PyTorch 2.7+ (+ compatible TorchAO version) +- CUDA 12.4+ + +### Configuration {#sec-fp8-config} + +Add to your YAML config: + +```{.yaml} +# Enable FP8 mixed precision +fp8: true + +# Optional: Enable FP8 for FSDP all-gather operations +fp8_enable_fsdp_float8_all_gather: true + +# Enable torch.compile (almost always necessary for FP8 speedups) +torch_compile: true +``` + +::: {.callout-important} +**torch.compile is critical for FP8 performance** + +FP8 training requires `torch_compile: true` to see meaningful speedups. Without compilation, FP8 may actually be slower and use more memory than FP16/BF16. +::: + +### Advanced FP8 Configs {#sec-fp8-advanced} + +For [FSDP](multi-gpu.qmd#sec-fsdp) (Fully Sharded Data Parallel) training: + +```{.yaml} +fp8: true +fp8_enable_fsdp_float8_all_gather: true + +torch_compile: true + +# FSDP configuration +fsdp_version: 2 +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: true + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: LlamaDecoderLayer + state_dict_type: FULL_STATE_DICT + reshard_after_forward: true +``` + +## Best Practices {#sec-best-practices} + +### Choosing Precision Format {#sec-choosing-format} + +- **Start with automatic detection**: `bf16: auto` +- **For Hopper+ (H100/H200)**: Try FP8 + torch.compile for maximum speed +- **For Ampere (A100/RTX 30/40)**: Use BF16 +- **For older Pascal/Turing GPUs**: Use FP16 with caution +- **For very old or unsupported GPUs**: Use FP32 + +### Validation and Testing {#sec-validation} + +Always validate your mixed precision setup: + +- **Start with a small dataset** to verify stability +- **Monitor loss curves** for irregularities +- **Compare with FP32 baseline** when possible +- **Test evaluation metrics** match expectations + +### FP8 Particulars {#sec-fp8-details} + +- Use cases + - Single GPU training + - Multi GPU training with FSDP2 or Deepspeed +- Speedups + - Please refer to the [TorchAO FP8 training benchmarks](https://github.com/pytorch/ao/tree/main/torchao/float8#rowwise-scaling) for expected matmul speedups for different (M, K, N) settings + - Concrete number for LLaMA 3 8B training can be found [here](https://github.com/pytorch/ao/tree/main/torchao/float8#training-benchmarks) +- Known issues: + - FP8 + DDP + `torch.compile` (causes [error](https://gist.github.com/djsaunde/0c1664c32e44a64d31b5e01b4aafe5c4)) + - FP8 + FSDP2 + `torch.compile` + FSDP2 activation checkpointing tends to be _slower_ than the BF16 equivalent training + - Flash Attention 2 does not play nicely with `torch.compile` + +See `examples/llama-3/3b-fp8-fsdp2.yaml` for an optimized example config. Enabling FP8 mixed precision + FP8 all-gather training results in ~10% faster iterations per second vs. BF16 for a relatively small (3B param) model + +For more information on multi-GPU training, see our [Multi-GPU guide](multi-gpu.qmd). diff --git a/examples/llama-3/3b-fp8-fsdp2.yaml b/examples/llama-3/3b-fp8-fsdp2.yaml new file mode 100644 index 0000000000..bea698c0e3 --- /dev/null +++ b/examples/llama-3/3b-fp8-fsdp2.yaml @@ -0,0 +1,76 @@ +base_model: meta-llama/Llama-3.2-3B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true + +datasets: + - path: yahma/alpaca-cleaned + type: alpaca + +output_dir: ./outputs/fp8_out/ + +sample_packing: true +pad_to_sequence_len: true +sequence_len: 512 + +flex_attention: true +flex_attn_compile_kwargs: + dynamic: false + mode: max-autotune-no-cudagraphs + +torch_compile: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 16 +num_epochs: 1 +optimizer: adamw_torch_fused + +cosine_constant_lr_ratio: 0 +cosine_min_lr_ratio: 1.0 +learning_rate: 2e-5 +save_only_model: true + +fp8: true +fp8_enable_fsdp_float8_all_gather: true + +resume_from_checkpoint: +logging_steps: 1 + +evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_steps: 10 +weight_decay: 0.0 + +fsdp_version: 2 +fsdp_config: + offload_params: false + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: LlamaDecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: false + +special_tokens: + pad_token: <|end_of_text|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index b983f10765..3dfaf47ce4 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -7,7 +7,7 @@ import os from collections import defaultdict from functools import partial, wraps -from typing import Callable, Literal, Optional +from typing import Any, Callable, Literal, Optional import datasets import torch @@ -522,15 +522,25 @@ def create_accelerator_and_postprocess(self): return res + # pylint: disable=unused-argument def additional_accelerator_args( - self, fp8=None, **kwargs - ): # pylint: disable=unused-argument + self, fp8: bool = False, enable_fsdp_float8_all_gather: bool = False, **kwargs + ) -> dict[str, Any]: ret_kwargs = {} if fp8: from accelerate.utils import AORecipeKwargs + from torchao.float8 import Float8LinearConfig + + # By default, Float8LinearConfig is instantiated using the "tensorwise" + # scaling strategy. See more details here: + # https://github.com/pytorch/ao/tree/main/torchao/float8. + config = Float8LinearConfig( + enable_fsdp_float8_all_gather=enable_fsdp_float8_all_gather, + force_recompute_fp8_weight_in_bwd=enable_fsdp_float8_all_gather is True, + ) ret_kwargs["mixed_precision"] = "fp8" - ret_kwargs["kwargs_handlers"] = [AORecipeKwargs()] + ret_kwargs["kwargs_handlers"] = [AORecipeKwargs(config=config)] # type: ignore os.environ["ACCELERATE_MIXED_PRECISION"] = "fp8" return ret_kwargs diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index f346c56e04..533bd0f7a7 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -154,7 +154,9 @@ def _apply_fp8_patches(self): patch_create_accelerate_code_for_fp8, ) - patch_create_accelerate_code_for_fp8() + patch_create_accelerate_code_for_fp8( + self.cfg.fp8_enable_fsdp_float8_all_gather + ) def _apply_flash_attention_peft_patches(self): """Apply patches for Flash Attention with PEFT.""" diff --git a/src/axolotl/monkeypatch/trainer_accelerator_args.py b/src/axolotl/monkeypatch/trainer_accelerator_args.py index 0a5b27c13e..819a66255c 100644 --- a/src/axolotl/monkeypatch/trainer_accelerator_args.py +++ b/src/axolotl/monkeypatch/trainer_accelerator_args.py @@ -18,7 +18,7 @@ PATCHED_TRAINER_CODE = """ if hasattr(self, "additional_accelerator_args"): - additional_args = self.additional_accelerator_args(fp8=True, **args) + additional_args = self.additional_accelerator_args(fp8=True, enable_fsdp_float8_all_gather={enable_fsdp_float8_all_gather}, **args) if additional_args: args.update(additional_args) @@ -38,9 +38,9 @@ def check_create_accelerate_code_is_patchable() -> bool: return ORIGINAL_TRAINER_CODE in create_code -def patch_create_accelerate_code_for_fp8(): +def patch_create_accelerate_code_for_fp8(enable_fsdp_float8_all_gather: bool): """ - monkeypatch create_accelerator_and_postprocess so it checks for additional kwargs + Monkeypatch create_accelerator_and_postprocess so it checks for additional kwargs. """ try: @@ -54,7 +54,10 @@ def patch_create_accelerate_code_for_fp8(): if ORIGINAL_TRAINER_CODE not in create_code: return - create_code = create_code.replace(ORIGINAL_TRAINER_CODE, PATCHED_TRAINER_CODE) + patched_trainer_code = PATCHED_TRAINER_CODE.format( + enable_fsdp_float8_all_gather=enable_fsdp_float8_all_gather + ) + create_code = create_code.replace(ORIGINAL_TRAINER_CODE, patched_trainer_code) create_code = create_code.replace( "def create_accelerator_and_postprocess(", "def fixed_create_accelerator_and_postprocess(", diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index de928d11c5..96b694043b 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -343,7 +343,20 @@ class AxolotlInputConfig( fp16: bool | None = Field( default=None, json_schema_extra={"description": "Use CUDA fp16"} ) - fp8: bool | None = None + fp8: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Enable FP8 mixed precision training using TorchAO. Best " + "used in combination with torch.compile." + }, + ) + fp8_enable_fsdp_float8_all_gather: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Enable FSDP float8 all-gather optimization for FP8 training. Can " + "improve training speed by 10-15% when FSDP is enabled." + }, + ) bfloat16: bool | None = Field( default=None, json_schema_extra={ diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 64dbb2529a..0c1a97fcdf 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -360,6 +360,36 @@ def check_fft_possible_bad_config(self): # RuntimeError: expected mat1 and mat2 to have the same dtype, but got: float != c10::Half return self + @model_validator(mode="before") + @classmethod + def check_fp8_config(cls, data): + if data.get("fp8") and not data.get("torch_compile"): + LOG.warning( + "torch_compile is strongly recommended for FP8 training in order to " + "see speed improvements. Please consider setting `torch_compile: " + "true` in your config." + ) + if data.get("fp8") and ( + data.get("fsdp_config", {}).get("activation_checkpointing", False) is True + or data.get("fsdp_config", {}).get("fsdp_activation_checkpointing", False) + is True + ): + LOG.warning( + "FP8 + FSDP2 + activation checkpointing may be slower than BF16 " + "training. Please considering setting `activation_checkpointing: false` " + "in your FSDP config." + ) + if ( + data.get("fp8_enable_fsdp_float8_all_gather") + and not data.get("fsdp_version", None) == 2 + ): + raise ValueError( + "fp8_enable_fsdp_float8_all_gather requires FSDP2 (fsdp_version: 2) " + "to be used." + ) + + return data + @model_validator(mode="before") @classmethod def check_use_reentrant_mismatch(cls, data): diff --git a/tests/e2e/integrations/test_fp8.py b/tests/e2e/integrations/test_fp8.py new file mode 100644 index 0000000000..0302b7e35b --- /dev/null +++ b/tests/e2e/integrations/test_fp8.py @@ -0,0 +1,62 @@ +""" +Simple end-to-end smoke tests for FP8 mixed precision training +""" + +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import check_model_output_exists, require_torch_2_7_0 + + +class FP8IntegrationTestCase: + """ + e2e smoke tests for FP8 mixed precision training with Axolotl + """ + + @require_torch_2_7_0 + def test_fp8_single_gpu_smoke(self, temp_dir): + """Smoke test for single GPU FP8 + torch.compile training""" + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", + "trust_remote_code": True, + "sequence_len": 512, + "val_set_size": 0.05, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 3, # Very short smoke test + "micro_batch_size": 1, + "gradient_accumulation_steps": 2, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "sdp_attention": True, + "pad_to_seq_len": True, + "sample_packing": True, + "fp8": True, + "torch_compile": True, + "save_safetensors": True, + "save_first_step": False, + } + ) + + # pylint: disable=duplicate-code + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/multigpu/test_fp8_fsdp2.py b/tests/e2e/multigpu/test_fp8_fsdp2.py new file mode 100644 index 0000000000..6423f5e2e2 --- /dev/null +++ b/tests/e2e/multigpu/test_fp8_fsdp2.py @@ -0,0 +1,120 @@ +"""Test module for FP8 mixed precision with FSDP2 multi-GPU functionality.""" + +# pylint: disable=duplicate-code + +import os +from pathlib import Path + +import torch +import yaml +from accelerate.test_utils import execute_subprocess_async +from tbparse import SummaryReader +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import most_recent_subdir, require_torch_2_7_0 + +AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent + + +def verify_fp8_training_success(temp_dir): + """Verify that FP8 training completed successfully by checking artifacts and loss.""" + output_path = Path(temp_dir) + + model_files = list(output_path.glob("*.bin")) + list( + output_path.glob("*.safetensors") + ) + assert len(model_files) > 0, "No model files found - training may have failed" + + checkpoint_files = list(output_path.glob("checkpoint-*")) + assert ( + len(checkpoint_files) > 0 + ), "No checkpoint files found - training may have failed" + + tb_log_path = most_recent_subdir(temp_dir + "/runs") + if tb_log_path: + event_files = sorted(os.listdir(tb_log_path)) + if event_files: + event_file = os.path.join(tb_log_path, event_files[0]) + reader = SummaryReader(event_file) + df = reader.scalars + train_loss_df = df[df.tag == "train/train_loss"] + if len(train_loss_df) > 0: + final_loss = train_loss_df.value.values[-1] + assert not torch.isnan( + torch.tensor(final_loss) + ), f"Training loss is NaN: {final_loss}" + + +class TestFP8FSDP2: + """Test class for FP8 mixed precision with FSDP2 functionality.""" + + @require_torch_2_7_0 + def test_fp8_fsdp2_smoke(self, temp_dir): + """Smoke test for 2-GPU FP8 + torch.compile + FSDP2 training""" + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", + "trust_remote_code": True, + "sequence_len": 512, + "val_set_size": 0.05, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 3, # Very short smoke test + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", # Use standard optimizer for stability + "lr_scheduler": "cosine", + "sdp_attention": True, + "pad_to_seq_len": True, + "sample_packing": True, + # FP8 configuration + "fp8": True, + "fp8_enable_fsdp_float8_all_gather": True, + "torch_compile": True, + # FSDP2 configuration + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": False, + "transformer_layer_cls_to_wrap": "LlamaDecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + "use_tensorboard": True, + "save_safetensors": True, + "save_first_step": False, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_fp8_training_success(temp_dir) diff --git a/tests/monkeypatch/test_trainer_accelerator_args.py b/tests/monkeypatch/test_trainer_accelerator_args.py new file mode 100644 index 0000000000..fab2597f01 --- /dev/null +++ b/tests/monkeypatch/test_trainer_accelerator_args.py @@ -0,0 +1,26 @@ +""" +Unit tests for trainer accelerator args monkeypatch +""" + +import unittest + +from axolotl.monkeypatch.trainer_accelerator_args import ( + check_create_accelerate_code_is_patchable, +) + + +class TestTrainerAcceleratorArgs(unittest.TestCase): + """ + Unit test class for trainer accelerator args monkeypatch + """ + + def test_check_create_accelerate_code_is_patchable(self): + """ + Test that the upstream transformers code is still patchable. + This will fail if the patched code changes upstream. + """ + assert check_create_accelerate_code_is_patchable() + + +if __name__ == "__main__": + unittest.main() From 93709eb5ce7ed44928a1b5e5ab558250a87fe37c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 22 Jul 2025 20:40:04 -0400 Subject: [PATCH 0845/1405] handle refactor upstream for flash attention (#2966) --- src/axolotl/monkeypatch/ring_attn/patch.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/axolotl/monkeypatch/ring_attn/patch.py b/src/axolotl/monkeypatch/ring_attn/patch.py index 41e39e657e..9c9ba45539 100644 --- a/src/axolotl/monkeypatch/ring_attn/patch.py +++ b/src/axolotl/monkeypatch/ring_attn/patch.py @@ -15,7 +15,13 @@ import accelerate import torch import torch.distributed as dist -from transformers.modeling_flash_attention_utils import _flash_supports_window_size + +try: + from transformers.modeling_flash_attention_utils import _flash_supports_window +except ImportError: + from transformers.modeling_flash_attention_utils import ( + _flash_supports_window_size as _flash_supports_window, + ) from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids from axolotl.utils.logging import get_logger @@ -106,7 +112,7 @@ def _flash_attention_forward_v3( # Assuming 4D tensors, key_states.shape[1] is the key/value sequence length (source length). use_sliding_windows = ( - _flash_supports_window_size + _flash_supports_window and sliding_window is not None and key_states.shape[1] > sliding_window ) From 5f1a4306b0e56ef8d0f8e99bf2c6c2f51cf1ebf4 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 22 Jul 2025 20:40:44 -0400 Subject: [PATCH 0846/1405] don't check dataset labels during preprocess for GRPO (#2952) [skip ci] * don't check dataset labels during preprocess for GRPO * use enum check per PR feedback --- src/axolotl/common/datasets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index a9b4c0f0f0..761317dfb4 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -122,7 +122,7 @@ def load_preference_datasets( math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) ) - if (cli_args and cli_args.debug) or cfg.debug: + if ((cli_args and cli_args.debug) or cfg.debug) and cfg.rl != RLType.ORPO: LOG.info("check_dataset_labels...") num_examples = cli_args.debug_num_examples if cli_args else 1 From b34c3371eda021ad8283c19eb51d779a62b55634 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 23 Jul 2025 10:27:28 -0400 Subject: [PATCH 0847/1405] upgrade torchao (#2968) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6c167d3fa7..6e3c1097ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,7 +62,7 @@ langdetect==1.0.9 immutabledict==4.2.0 antlr4-python3-runtime==4.13.2 -torchao==0.10.0 +torchao==0.12.0 schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.6 From 1407aac779e5e51b9d898509cc357cd07e9b4f57 Mon Sep 17 00:00:00 2001 From: salman Date: Thu, 24 Jul 2025 09:11:46 +0100 Subject: [PATCH 0848/1405] Skip CI for draft PRs (#2970) --- .github/workflows/base.yml | 4 ++-- .github/workflows/lint.yml | 2 ++ .github/workflows/multi-gpu-e2e.yml | 2 +- .github/workflows/preview-docs.yml | 3 ++- .github/workflows/tests.yml | 9 +++++++-- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 2ffe85fe49..e97e734422 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -17,7 +17,7 @@ on: jobs: build-base: - if: github.repository_owner == 'axolotl-ai-cloud' + if: ${{ github.repository_owner == 'axolotl-ai-cloud' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} timeout-minutes: 480 # this job needs to be run on self-hosted GPU runners... runs-on: ubuntu-latest-m @@ -108,7 +108,7 @@ jobs: PYTORCH_VERSION=${{ matrix.pytorch }} TORCH_CUDA_ARCH_LIST=${{ matrix.torch_cuda_arch_list }} build-base-uv: - if: github.repository_owner == 'axolotl-ai-cloud' + if: ${{ github.repository_owner == 'axolotl-ai-cloud' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} timeout-minutes: 480 runs-on: ubuntu-latest-m strategy: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d044504281..cf322f1059 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,6 +3,7 @@ on: # check on PRs, and manual triggers merge_group: pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - '**.py' - 'requirements.txt' @@ -16,6 +17,7 @@ jobs: pre-commit: name: pre-commit runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index f58c05f3bc..f26201ef01 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -21,7 +21,7 @@ concurrency: jobs: test-axolotl-multigpu: - if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} strategy: fail-fast: false matrix: diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index a13d2c97fc..162665ed5f 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -2,7 +2,7 @@ name: Preview on: workflow_dispatch: pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened, ready_for_review] # Run the workflow only when one of these files changes paths: @@ -25,6 +25,7 @@ permissions: jobs: preview: runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} steps: - name: Check out repository uses: actions/checkout@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d4818f280f..e0c8faba6a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,6 +13,7 @@ on: - 'cicd/cicd.sh' - 'cicd/Dockerfile.jinja' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - '**.py' - 'requirements.txt' @@ -34,6 +35,7 @@ jobs: pre-commit: name: pre-commit runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -47,6 +49,7 @@ jobs: pytest: name: PyTest runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} # needs: [preload-cache] strategy: fail-fast: false @@ -121,6 +124,7 @@ jobs: pytest-sdist: name: PyTest from Source Dist runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} strategy: fail-fast: false matrix: @@ -185,7 +189,7 @@ jobs: docker-e2e-tests-1st: # Run this job first as a gate for running the remainder of the test matrix - if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' }} + if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' && !github.event.pull_request.draft }} # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] timeout-minutes: 120 @@ -235,7 +239,7 @@ jobs: modal run cicd.e2e_tests docker-e2e-tests: - if: github.repository_owner == 'axolotl-ai-cloud' + if: ${{ github.repository_owner == 'axolotl-ai-cloud' && !github.event.pull_request.draft }} # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] timeout-minutes: 120 @@ -289,6 +293,7 @@ jobs: runs-on: [self-hosted, modal] timeout-minutes: 90 needs: [docker-e2e-tests] + if: ${{ !github.event.pull_request.draft }} strategy: fail-fast: false From 0ff2f172ef0b735a83b59138087e8ccfe2350d1b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 24 Jul 2025 16:10:04 -0400 Subject: [PATCH 0849/1405] Act offload lora fix (#2928) [skip ci] * fix activation offloading with lora * update w e2e test * add docs for error --- docs/faq.qmd | 4 + .../mixins/activation_checkpointing.py | 188 +++++++++++++++++- src/axolotl/utils/schemas/validation.py | 27 +-- tests/e2e/test_activation_offloading.py | 83 ++++++++ .../validation/test_activation_offloading.py | 56 ------ 5 files changed, 275 insertions(+), 83 deletions(-) create mode 100644 tests/e2e/test_activation_offloading.py diff --git a/docs/faq.qmd b/docs/faq.qmd index 57c2c81e61..08d439af75 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -136,3 +136,7 @@ description: Frequently asked questions > dynamic: false > mode: max-autotune-no-cudagraphs > ``` + +**Q: `ValueError("Backward pass should have cleared tracker of all tensors")` + +> A: This may happen due to edge cases in using the modern OffloadActivations context manager for CUDA streams. If you encounter this error, you may have success using the naive implementation with `offload_activations: legacy` in your YAML. diff --git a/src/axolotl/core/trainers/mixins/activation_checkpointing.py b/src/axolotl/core/trainers/mixins/activation_checkpointing.py index 9488186cdc..1bfdb49f7e 100644 --- a/src/axolotl/core/trainers/mixins/activation_checkpointing.py +++ b/src/axolotl/core/trainers/mixins/activation_checkpointing.py @@ -4,13 +4,22 @@ import contextlib +from peft import PeftModel from torch import nn from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import ( apply_activation_checkpointing, ) from torch.distributed.fsdp.wrap import ModuleWrapPolicy from transformers import GradientCheckpointingLayer, Trainer -from trl.models.activation_offloading import get_act_offloading_ctx_manager +from trl.models.activation_offloading import ( + NoOpManager, + OffloadActivations, + get_act_offloading_ctx_manager, +) + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) class ActivationOffloadingMixin(Trainer): @@ -21,9 +30,14 @@ class ActivationOffloadingMixin(Trainer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.args.activation_offloading: - self.activation_offload_context = get_act_offloading_ctx_manager( - self.model, use_streams=True - ) + if isinstance(self.model, PeftModel): + self.activation_offload_context = get_lora_act_offloading_ctx_manager( + self.model, use_streams=True + ) + else: + self.activation_offload_context = get_act_offloading_ctx_manager( + self.model, use_streams=True + ) else: self.activation_offload_context = contextlib.nullcontext() @@ -35,3 +49,169 @@ def training_step(self, *args, **kwargs): def ac_wrap_hf_model(model: nn.Module, **kwargs): auto_wrap_policy = ModuleWrapPolicy(set((GradientCheckpointingLayer,))) apply_activation_checkpointing(model, auto_wrap_policy=auto_wrap_policy, **kwargs) + + +def get_lora_act_offloading_ctx_manager( + model: nn.Module, + use_pin_memory: bool = True, + use_streams: bool = True, + min_offload_size: int = 1024, + max_fwd_stash_size: int = 5, + warn_if_no_head: bool = True, +) -> OffloadActivations: + """ + Returns the activation offloading context manager for the model. All but the last output Linear in every step will + be offloaded. + + If activation offloading is enabled, we return the OffloadActivations context manager. If activation offloading is + disabled, we return a NoOpManager context manager. + + Args: + model (`nn.Module`): + Model to wrap with the activation offloading context manager. + use_pin_memory (`bool`, *optional*, defaults to `True`): + Whether to offloaded Tensor will be placed in pinned memory on the CPU. Pinned memory allows the Tensor to + be moved back onto GPU more quickly but is a limited resource. + use_streams (`bool`, *optional*, defaults to `True`): + Whether to use streams for performance optimization where the communications get overlapped with the + computation. Requires a torch build after torch-2.5.0. + min_offload_size (`int`, *optional*, defaults to `1024`): + Minimum number of bytes a Tensor must be in order to qualify for offloading. If the tensor is too small, we + do not want to waste bandwidth and resources moving it to CPU and back. + max_fwd_stash_size (`int`, *optional*, defaults to `5`): + Maximum size of the forward stash, or the maximum number of consecutive activations to keep alive during + the forward pass. This number must be at least 1. Keeping alive more activations will potentially allow + more overlap between the communication and compute streams at the cost of increasing memory usage. Keeping + alive fewer activations will conserve memory, but may cause poor overlap between the streams, increasing + runtime. + warn_if_no_head (`bool`, *optional*, defaults to `True`): + Whether to warn if no output head is detected. If set to `False`, no warning will be raised if no output + head is detected. + + Returns: + `contextlib.ContextDecorator`: + Activation offloading context manager for the model. + """ + # pylint: disable=unnecessary-dunder-call + activations_handling_ctx = OffloadActivations( + use_pin_memory=use_pin_memory, + use_streams=use_streams, + min_offload_size=min_offload_size, + max_fwd_stash_size=max_fwd_stash_size, + ) + + # Below is our hack to disable offloading the last output Linear in every + # step, as the cost for offloading the activation and then soon after bringing + # it back is expensive. + output_head_detected = False + noop_ctx = NoOpManager() + + # Try to get the actual model if it's wrapped + unwrapped_model = model + if hasattr(unwrapped_model, "module"): + unwrapped_model = unwrapped_model.module + # check for PEFT models + if hasattr(unwrapped_model, "base_model") and hasattr( + unwrapped_model, "peft_config" + ): + unwrapped_model = unwrapped_model.base_model + + # Check for different types of output heads + if hasattr(unwrapped_model, "output"): + if isinstance(unwrapped_model.output, nn.Module): + unwrapped_model.output.register_forward_pre_hook( + lambda *args: noop_ctx.__enter__() + ) + unwrapped_model.output.register_forward_hook( + lambda *args: noop_ctx.__exit__(), always_call=True + ) + output_head_detected = True + elif hasattr(unwrapped_model.output, "linear") and isinstance( + unwrapped_model.output.linear, nn.Module + ): + unwrapped_model.output.linear.register_forward_pre_hook( + lambda *args: noop_ctx.__enter__() + ) + unwrapped_model.output.linear.register_forward_hook( + lambda *args: noop_ctx.__exit__(), always_call=True + ) + output_head_detected = True + + # Check for HuggingFace model output heads + elif hasattr(unwrapped_model, "lm_head"): + unwrapped_model.lm_head.register_forward_pre_hook( + lambda *args: noop_ctx.__enter__() + ) + unwrapped_model.lm_head.register_forward_hook( + lambda *args: noop_ctx.__exit__(), always_call=True + ) + output_head_detected = True + + # Check for decoder-based models + elif hasattr(unwrapped_model, "decoder"): + decoder = unwrapped_model.decoder + if hasattr(decoder, "output"): + decoder.output.register_forward_pre_hook(lambda *args: noop_ctx.__enter__()) + decoder.output.register_forward_hook( + lambda *args: noop_ctx.__exit__(), always_call=True + ) + output_head_detected = True + # Some models have lm_head in the decoder + elif hasattr(decoder, "lm_head"): + decoder.lm_head.register_forward_pre_hook( + lambda *args: noop_ctx.__enter__() + ) + decoder.lm_head.register_forward_hook( + lambda *args: noop_ctx.__exit__(), always_call=True + ) + output_head_detected = True + + # Check for transformer models with final layer norm + elif hasattr(unwrapped_model, "final_layer_norm") or hasattr( + unwrapped_model, "ln_f" + ): + final_norm = ( + getattr(unwrapped_model, "final_layer_norm", None) or unwrapped_model.ln_f + ) + final_norm.register_forward_pre_hook(lambda *args: noop_ctx.__enter__()) + final_norm.register_forward_hook( + lambda *args: noop_ctx.__exit__(), always_call=True + ) + output_head_detected = True + + # Check for models with head module + elif hasattr(unwrapped_model, "head") and isinstance( + unwrapped_model.head, nn.Module + ): + unwrapped_model.head.register_forward_pre_hook( + lambda *args: noop_ctx.__enter__() + ) + unwrapped_model.head.register_forward_hook( + lambda *args: noop_ctx.__exit__(), always_call=True + ) + output_head_detected = True + + if not output_head_detected and warn_if_no_head: + LOG.warning( + "During activation offloading, no output head was detected. If your model has an output head, it will be " + "offloaded. This usually greatly slows training, given the large vocabulary size. To change this " + "behavior, set your output head as model.output and make it an nn.Module. You can disable this warning by " + "passing `warn_if_no_head=False`." + ) + + for name, module in unwrapped_model.named_modules(): + # Disable offloading for any Liger modules + if "liger" in name.lower(): + module.register_forward_pre_hook(lambda *args: noop_ctx.__enter__()) + module.register_forward_hook( + lambda *args: noop_ctx.__exit__(), always_call=True + ) + # disable offloading for any submodules to fix LoRA training + if name.endswith("._checkpoint_wrapped_module"): + for _, sub_module in module.named_modules(): + sub_module.register_forward_pre_hook(lambda *args: noop_ctx.__enter__()) + sub_module.register_forward_hook( + lambda *args: noop_ctx.__exit__(), always_call=True + ) + + return activations_handling_ctx diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 0c1a97fcdf..cfa759cad3 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1104,16 +1104,10 @@ def check_gradient_checkpointing_w_offload(self): "`offload` is deprecated for gradient_checkpointing, use `activation_offloading: true` or `activation_offloading: legacy`" ) self.gradient_checkpointing = True - if self.adapter and "lora" in self.adapter: - LOG.warning( - "offloading with CUDA streams is not supported for LoRA adapters, using the `activation_offloading: legacy` implementation." - ) - self.activation_offloading = "legacy" - else: - LOG.warning( - "`offload` uses a new stream implementation; to use the previous implementation, use `activation_offloading: legacy`" - ) - self.activation_offloading = True + LOG.warning( + "`offload` now uses a new stream implementation; to use the previous implementation, use `activation_offloading: legacy`" + ) + self.activation_offloading = True if self.gradient_checkpointing == "offload_disk": LOG.warning( "`offload_disk` is deprecated for gradient_checkpointing, use `activation_offloading: disk`" @@ -1122,19 +1116,6 @@ def check_gradient_checkpointing_w_offload(self): self.activation_offloading = "disk" return self - @model_validator(mode="after") - def check_activation_offloading_w_lora(self): - if ( - self.activation_offloading is True - and self.adapter - and "lora" in self.adapter - ): - LOG.warning( - "activation_offloading with CUDA streams is not supported for LoRA adapters. Setting `activation_offloading: legacy`" - ) - self.activation_offloading = "legacy" - return self - @model_validator(mode="after") def check_activation_offloading_wo_gc(self): if self.activation_offloading and not self.gradient_checkpointing: diff --git a/tests/e2e/test_activation_offloading.py b/tests/e2e/test_activation_offloading.py new file mode 100644 index 0000000000..06c5c06569 --- /dev/null +++ b/tests/e2e/test_activation_offloading.py @@ -0,0 +1,83 @@ +""" +E2E tests for activation offloading +""" + +import pytest + +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +from .utils import check_model_output_exists + +# pylint: disable=duplicate-code + + +class TestActivationOffloading: + """ + E2E test cases for activation offloading + """ + + @pytest.mark.parametrize( + "adapter", + ["lora", "qlora", None], + ) + def test_activation_offloading( + self, + temp_dir, + adapter, + ): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 1024, + "val_set_size": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>", + "eos_token": "<|im_end|>", + }, + "datasets": [ + { + "chat_template": "chatml", + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "split": "train[:10%]", + "field_messages": "conversations", + "message_field_role": "from", + "message_field_content": "value", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_8bit", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": True, + "bf16": "auto", + "save_safetensors": True, + "gradient_checkpointing": True, + "activation_offloading": True, + "save_first_step": False, + "lora_r": 8, + "lora_alpha": 16, + "lora_target_linear": True, + } + ) + if adapter == "lora": + cfg["adapter"] = "lora" + if adapter == "qlora": + cfg["adapter"] = "qlora" + cfg["load_in_4bit"] = True + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) diff --git a/tests/utils/schemas/validation/test_activation_offloading.py b/tests/utils/schemas/validation/test_activation_offloading.py index 92ac8f45c7..433133a803 100644 --- a/tests/utils/schemas/validation/test_activation_offloading.py +++ b/tests/utils/schemas/validation/test_activation_offloading.py @@ -21,62 +21,6 @@ def test_gc_converts_offload_wo_lora(self, min_base_cfg): assert cfg.gradient_checkpointing is True assert cfg.activation_offloading is True - def test_gc_converts_offload_w_lora(self, min_base_cfg): - cfg = ( - DictDefault( - gradient_checkpointing="offload", - adapter="lora", - ) - | min_base_cfg - ) - - cfg = validate_config(cfg) - assert cfg.gradient_checkpointing is True - assert cfg.activation_offloading == "legacy" - - def test_gc_converts_offload_w_qlora(self, min_base_cfg): - cfg = ( - DictDefault( - gradient_checkpointing="offload", - adapter="qlora", - load_in_4bit=True, - ) - | min_base_cfg - ) - - cfg = validate_config(cfg) - assert cfg.gradient_checkpointing is True - assert cfg.activation_offloading == "legacy" - - def test_ac_impl_changes_w_lora(self, min_base_cfg): - cfg = ( - DictDefault( - gradient_checkpointing=True, - activation_offloading=True, - adapter="lora", - ) - | min_base_cfg - ) - - cfg = validate_config(cfg) - assert cfg.gradient_checkpointing is True - assert cfg.activation_offloading == "legacy" - - def test_ac_impl_changes_w_qlora(self, min_base_cfg): - cfg = ( - DictDefault( - gradient_checkpointing=True, - activation_offloading=True, - adapter="qlora", - load_in_4bit=True, - ) - | min_base_cfg - ) - - cfg = validate_config(cfg) - assert cfg.gradient_checkpointing is True - assert cfg.activation_offloading == "legacy" - def test_ac_offload_impl_noop_wo_adapter(self, min_base_cfg): cfg = ( DictDefault( From e80faea0db7d65315128b22bc8fc88401ffd83df Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 24 Jul 2025 16:10:23 -0400 Subject: [PATCH 0850/1405] garbage collect on the end of the step if we're going to save a checkpoint (#2971) [skip ci] --- src/axolotl/utils/callbacks/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index b0738bf27d..2d031aa039 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -27,7 +27,11 @@ TrainerState, TrainingArguments, ) -from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, IntervalStrategy +from transformers.trainer_utils import ( + PREFIX_CHECKPOINT_DIR, + IntervalStrategy, + SaveStrategy, +) from trl.models import unwrap_model_for_generation from axolotl.utils import is_comet_available, is_mlflow_available @@ -879,6 +883,17 @@ def on_step_end( self.next_gc_on_begin_step = state.global_step + 1 elif self.gc_steps > 0 and state.global_step % self.gc_steps == 0: self._gc() + elif ( + args.save_strategy == SaveStrategy.STEPS + and state.save_steps > 0 + and state.global_step % state.save_steps == 0 + ): + # gc on save steps in case anything is loaded to CPU RAM like offloaded tensors + self._gc() + elif state.global_step >= state.max_steps: + if args.save_strategy == SaveStrategy.STEPS: + # gc on save steps in case anything is loaded to CPU RAM like offloaded tensors + self._gc() def on_epoch_end( self, args, state, control, **kwargs # pylint: disable=unused-argument From 460e0f9ed9a854961d0031ee495d747c8d646288 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 24 Jul 2025 16:10:38 -0400 Subject: [PATCH 0851/1405] improve handling of file lock when content is empty (#2959) --- src/axolotl/utils/data/lock.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/data/lock.py b/src/axolotl/utils/data/lock.py index f5ec1679b3..afd1547af6 100644 --- a/src/axolotl/utils/data/lock.py +++ b/src/axolotl/utils/data/lock.py @@ -46,7 +46,8 @@ def load(self, load_fn: Callable[[], Any]) -> Any: def _increment_counter(self): """Safely increment the process counter.""" if self.counter_path.exists(): - count = int(self.counter_path.read_text().strip()) + counter_content = self.counter_path.read_text().strip() + count = int(counter_content) if counter_content else 0 else: count = 0 self.counter_path.write_text(str(count + 1)) @@ -54,10 +55,11 @@ def _increment_counter(self): def cleanup(self): """Clean up ready flag when last process is done.""" with FileLock(str(self.lock_file_path)): - count = int(self.counter_path.read_text().strip()) + counter_content = self.counter_path.read_text().strip() + count = int(counter_content) if counter_content else 0 count -= 1 - if count == 0: + if count <= 0: # Last process cleans everything up self.ready_flag_path.unlink(missing_ok=True) self.counter_path.unlink(missing_ok=True) From f7ea140838e720cc23c6d71c4e578314e7daf52a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 25 Jul 2025 07:15:03 -0400 Subject: [PATCH 0852/1405] TiledMLP support for FSDP2 (#2950) * make TiledMLP work with FSDP * cleanup/gc at start of train to prevent large VRAM spike * chore: lint * generic function for non-deepspeed training * unify patch to fix imports * update readme for ALST and add examples * make deepspeed attribute on params check more robust * update with new info from PR review --- README.md | 1 + examples/alst/README.md | 9 ++ examples/alst/llama3-8b-deepspeed-alst.yaml | 53 ++++++ examples/alst/llama3-8b-fsdp2-alst.yaml | 59 +++++++ src/axolotl/integrations/liger/args.py | 8 +- src/axolotl/loaders/model.py | 1 + src/axolotl/loaders/patch_manager.py | 7 +- src/axolotl/monkeypatch/tiled_mlp/__init__.py | 11 ++ src/axolotl/monkeypatch/tiled_mlp/base.py | 153 ++++++++++++++++++ .../{tiled_mlp.py => tiled_mlp/patch.py} | 35 ++-- src/axolotl/utils/callbacks/__init__.py | 8 +- src/axolotl/utils/schemas/config.py | 2 +- src/axolotl/utils/schemas/validation.py | 13 -- 13 files changed, 332 insertions(+), 28 deletions(-) create mode 100644 examples/alst/README.md create mode 100644 examples/alst/llama3-8b-deepspeed-alst.yaml create mode 100644 examples/alst/llama3-8b-fsdp2-alst.yaml create mode 100644 src/axolotl/monkeypatch/tiled_mlp/__init__.py create mode 100644 src/axolotl/monkeypatch/tiled_mlp/base.py rename src/axolotl/monkeypatch/{tiled_mlp.py => tiled_mlp/patch.py} (66%) diff --git a/README.md b/README.md index 406781039e..b31703e2bd 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ ## 🎉 Latest Updates +- 2025/07: TiledMLP support for single-GPU to multi-GPU training with DDP, DeepSpeed and FSDP support has been added to support Arctic Long Sequence Training. (ALST). See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/alst) for using ALST with Axolotl! - 2025/06: Magistral with mistral-common tokenizer support has been added to Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral) to start training your own Magistral models with Axolotl! - 2025/05: Quantization Aware Training (QAT) support has been added to Axolotl. Explore the [docs](https://docs.axolotl.ai/docs/qat.html) to learn more! - 2025/04: Llama 4 support has been added in Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/llama-4) to start training your own Llama 4 models with Axolotl's linearized version! diff --git a/examples/alst/README.md b/examples/alst/README.md new file mode 100644 index 0000000000..7f194d299e --- /dev/null +++ b/examples/alst/README.md @@ -0,0 +1,9 @@ +# Arctic Long Sequence Training (ALST) + +Artic Long Sequence Training (ALST) is a technique for training long context models using a variety of optimization +techniques. It is a combination of: +- TiledMLP: Leverage tiling over the sequence dimension on MLP layers to reduce memory usage +- Tiled Loss: Using optimized loss functions like Liger-Kernel or Cut Cross Entropy to reduce memory usage +- Activation Offloading: Offload activations to CPU RAM to reduce memory usage + +For more information, you can check out the ALST paper [here](https://www.arxiv.org/abs/2506.13996). diff --git a/examples/alst/llama3-8b-deepspeed-alst.yaml b/examples/alst/llama3-8b-deepspeed-alst.yaml new file mode 100644 index 0000000000..dc82fa3be3 --- /dev/null +++ b/examples/alst/llama3-8b-deepspeed-alst.yaml @@ -0,0 +1,53 @@ +base_model: meta-llama/Llama-3.1-8B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +datasets: + - path: togethercomputer/Long-Data-Collections + type: completion + field: text + data_files: + - pretrain/rp_sub.jsonl.zst + - path: princeton-nlp/TextbookChapters + type: completion + field: chapter +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +sequence_len: 500_000 +min_sample_len: 200_000 +sample_packing: true + +tiled_mlp: true +sequence_parallel_degree: 8 +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 2e-5 + +bf16: auto +tf32: true + +gradient_checkpointing: true +activation_offloading: legacy + +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_steps: 100 +saves_per_epoch: 1 +evals_per_epoch: 2 +weight_decay: 0.0 +special_tokens: + pad_token: <|end_of_text|> + +deepspeed: deepspeed_configs/zero3_bf16_cpuoffload_all.json + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/alst/llama3-8b-fsdp2-alst.yaml b/examples/alst/llama3-8b-fsdp2-alst.yaml new file mode 100644 index 0000000000..c8a978264f --- /dev/null +++ b/examples/alst/llama3-8b-fsdp2-alst.yaml @@ -0,0 +1,59 @@ +base_model: meta-llama/Llama-3.1-8B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +datasets: + - path: togethercomputer/Long-Data-Collections + type: completion + field: text + data_files: + - pretrain/rp_sub.jsonl.zst + - path: princeton-nlp/TextbookChapters + type: completion + field: chapter +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +sequence_len: 500_000 +min_sample_len: 200_000 +sample_packing: true + +tiled_mlp: true +context_parallel_size: 8 +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 2e-5 + +bf16: auto +tf32: true + +gradient_checkpointing: true +activation_offloading: legacy + +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_steps: 100 +saves_per_epoch: 1 +evals_per_epoch: 2 +weight_decay: 0.0 +special_tokens: + pad_token: <|end_of_text|> + +fsdp_version: 2 +fsdp_config: + offload_params: false # offloading is currently not compatible with SP + torchao optimizer + state_dict_type: SHARDED_STATE_DICT + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: LlamaDecoderLayer + reshard_after_forward: true + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/src/axolotl/integrations/liger/args.py b/src/axolotl/integrations/liger/args.py index 94ba83dd5c..0460bdbf5b 100644 --- a/src/axolotl/integrations/liger/args.py +++ b/src/axolotl/integrations/liger/args.py @@ -57,8 +57,12 @@ def check_deprecated_swiglu(cls, data): @model_validator(mode="before") @classmethod def check_tiled_mlp_conflict(cls, data): - if data.get("liger_glu_activation") is True and data.get("tiled_mlp") is True: + if ( + data.get("liger_glu_activation") is True + and data.get("tiled_mlp") is True + and not data.get("tiled_mlp_use_original_mlp") + ): raise ValueError( - "You cannot have both `liger_glu_activation` and `tiled_mlp` set." + "You cannot have both `liger_glu_activation` and `tiled_mlp` set without `tiled_mlp_use_original_mlp: true`." ) return data diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 1ce98ef319..4fc005457b 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -162,6 +162,7 @@ def load(self) -> tuple[PreTrainedModel | PeftModelForCausalLM, PeftConfig | Non # Build the model PLUGIN_MANAGER.pre_model_load(self.cfg) + self.patch_manager.apply_post_plugin_pre_model_load_patches() skip_move_to_device = self._build_model() PLUGIN_MANAGER.post_model_build(self.cfg, self.model) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 533bd0f7a7..f1bb3ae674 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -66,6 +66,9 @@ def apply_pre_model_load_patches(self): self._apply_self_attention_lora_patch() self._apply_gemma3_conditional_generation_forward_patch() self._apply_sequence_parallel_patches() + + def apply_post_plugin_pre_model_load_patches(self): + """Apply post plugin-pre_model_load load patches based on config.""" self._apply_tiled_mlp(self.cfg.model_config_type) def apply_post_model_load_patches(self, model: PreTrainedModel): @@ -272,7 +275,9 @@ def _apply_sequence_parallel_patches(self): def _apply_tiled_mlp(self, model_type: str): if self.cfg.tiled_mlp: - from axolotl.monkeypatch.tiled_mlp import patch_tiled_mlp + from axolotl.monkeypatch.tiled_mlp import ( + patch_tiled_mlp, + ) patch_tiled_mlp( model_type, diff --git a/src/axolotl/monkeypatch/tiled_mlp/__init__.py b/src/axolotl/monkeypatch/tiled_mlp/__init__.py new file mode 100644 index 0000000000..4ea1549915 --- /dev/null +++ b/src/axolotl/monkeypatch/tiled_mlp/__init__.py @@ -0,0 +1,11 @@ +""" +TiledMLP monkey patches +""" + +from .patch import ( + patch_tiled_mlp, +) + +__all__ = [ + "patch_tiled_mlp", +] diff --git a/src/axolotl/monkeypatch/tiled_mlp/base.py b/src/axolotl/monkeypatch/tiled_mlp/base.py new file mode 100644 index 0000000000..3b7326bdb7 --- /dev/null +++ b/src/axolotl/monkeypatch/tiled_mlp/base.py @@ -0,0 +1,153 @@ +""" +TiledMLP support for DDP, FSDP, and single GPU +""" + +import threading +from typing import List + +import torch + + +class TiledMLP(torch.autograd.Function): + """ + TiledMLP implementation using gradient hooks + """ + + @staticmethod + def forward( + ctx, + fn, + self, + x, + shards, + compute_params, + ) -> torch.Tensor: + ctx.fn = fn + ctx.self = self + ctx.shards = shards + ctx.compute_params = [p for p in compute_params if p.requires_grad] + ctx.save_for_backward(x) + + x_shards = list(torch.chunk(x, chunks=shards, dim=1)) + with torch.no_grad(): + output_shards = [fn(self, x_shard) for x_shard in x_shards] + output_unsharded = torch.cat(output_shards, dim=1) + + return output_unsharded + + @staticmethod + def backward(ctx, *grads) -> torch.Tensor: + fn = ctx.fn + (x,) = ctx.saved_tensors + self = ctx.self + shards = ctx.shards + compute_params = ctx.compute_params + + x_requires_grad = x.requires_grad + x = x.detach() + x.requires_grad_(x_requires_grad) + + incoming_grad = grads[0] + x_grad = torch.zeros_like(x) + x_shards = list(torch.chunk(x, chunks=shards, dim=1)) + + # Create a gradient accumulator for parameters + grad_accumulator = GradientAccumulator(compute_params, shards, dtype=x.dtype) + + shard_step = x_shards[0].numel() + for i, x_shard in enumerate(x_shards): + x_shard.requires_grad_(x_requires_grad) + + shard_offset = i * shard_step + x_shard.grad = ( + x_grad.view(-1) + .narrow(0, shard_offset, x_shard.numel()) + .view_as(x_shard) + ) + incoming_grad_shard = ( + incoming_grad.view(-1) + .narrow(0, shard_offset, x_shard.numel()) + .view_as(x_shard) + ) + + # Install hooks for this shard + is_last_shard = i + 1 == shards + grad_accumulator.install_hooks(is_last_shard) + + with torch.enable_grad(): + output = fn(self, x_shard) + torch.autograd.backward(output, incoming_grad_shard) + + # Clean up hooks + grad_accumulator.cleanup() + del grad_accumulator + + return (None, None, x_grad, None, None) + + +class GradientAccumulator: + """ + Manual gradient accumulator for TiledMLP with configurable precision + Accumulates in specified dtype and rescales the gradient at the end + """ + + def __init__( + self, + params: List[torch.nn.Parameter], + total_shards: int, + dtype: torch.dtype | None = None, + ): + self.params = params + self.total_shards = total_shards + self.grad_accumulation_dtype = dtype or torch.float32 + self.accumulated_grads = {} + self.hooks = [] + self.lock = threading.Lock() + self.gradient_scale = 1.0 / total_shards + + # Initialize accumulated gradients in the specified dtype + for param in self.params: + if param.grad is not None: + self.accumulated_grads[param] = param.grad.to( + self.grad_accumulation_dtype + ) + param.grad = None + else: + self.accumulated_grads[param] = torch.zeros_like( + param, dtype=self.grad_accumulation_dtype + ) + + def install_hooks(self, is_last_shard: bool): + """Install gradient hooks that accumulate gradients in higher precision""" + + def create_hook(param): + def hook(grad): + with self.lock: + grad_to_accum_dtype = grad.to(self.grad_accumulation_dtype) + scaled_grad = grad_to_accum_dtype * self.gradient_scale + + if param in self.accumulated_grads: + self.accumulated_grads[param] += scaled_grad + else: + self.accumulated_grads[param] = scaled_grad.clone() + + # Only assign the averaged gradient on the last shard + if is_last_shard: + param.grad = self.accumulated_grads[param].to(param.dtype) + return param.grad + return None + + return hook + + # Install hooks on all parameters + for param in self.params: + if param.requires_grad: + hook = param.register_hook(create_hook(param)) + self.hooks.append(hook) + + def cleanup(self): + """Remove all installed hooks""" + for hook in self.hooks: + hook.remove() + self.hooks.clear() + del self.accumulated_grads diff --git a/src/axolotl/monkeypatch/tiled_mlp.py b/src/axolotl/monkeypatch/tiled_mlp/patch.py similarity index 66% rename from src/axolotl/monkeypatch/tiled_mlp.py rename to src/axolotl/monkeypatch/tiled_mlp/patch.py index 02bb3a8cb7..419c73104f 100644 --- a/src/axolotl/monkeypatch/tiled_mlp.py +++ b/src/axolotl/monkeypatch/tiled_mlp/patch.py @@ -12,8 +12,12 @@ LOG = get_logger(__name__) -def patch_tiled_mlp(model_type, use_original_mlp=False, cfg_num_shards=None): - from deepspeed.runtime.sequence_parallel.ulysses_sp import TiledMLP +def patch_tiled_mlp(model_type, use_original_mlp=True, cfg_num_shards=None): + from deepspeed.runtime.sequence_parallel.ulysses_sp import ( + TiledMLP as DeepSpeedTiledMLP, + ) + + from axolotl.monkeypatch.tiled_mlp.base import TiledMLP try: # Dynamically import the module and MLP class @@ -36,6 +40,7 @@ def generic_mlp_forward(self_, hs): is_distributed = int(os.environ.get("WORLD_SIZE", 1)) > 1 def tiled_mlp_forward(self, x): + # pylint: disable=protected-access input_shape = x.shape seqlen = input_shape[-2] hidden = input_shape[-1] @@ -48,14 +53,23 @@ def tiled_mlp_forward(self, x): else: num_shards = cfg_num_shards - if not self._compute_params: # pylint: disable=protected-access - self._compute_params = [ # pylint: disable=protected-access - p for p in self.parameters() if p.requires_grad - ] - - compute_params = self._compute_params # pylint: disable=protected-access - - down_res = TiledMLP.apply( + if not self._compute_params: + self._compute_params = [p for p in self.parameters() if p.requires_grad] + + compute_params = self._compute_params + if not self._tiled_mlp_dist_impl: + if ( + self._compute_params + and any( + hasattr(p, "ds_id") or hasattr(p, "param_idx_in_group") + for p in self._compute_params + ) + ) or os.environ.get("ACCELERATE_USE_DEEPSPEED", "false") == "true": + self._tiled_mlp_dist_impl = DeepSpeedTiledMLP + else: + self._tiled_mlp_dist_impl = TiledMLP + + down_res = self._tiled_mlp_dist_impl.apply( mlp_forward, self, x, @@ -66,6 +80,7 @@ def tiled_mlp_forward(self, x): mlp_cls.forward = tiled_mlp_forward mlp_cls._compute_params = [] # pylint: disable=protected-access + mlp_cls._tiled_mlp_dist_impl = None # pylint: disable=protected-access LOG.info( f"Successfully monkey-patched TiledMLP for model_type: {model_type}", main_process_only=True, diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 2d031aa039..c64d8d351e 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -867,10 +867,16 @@ def _gc(self): torch.cuda.empty_cache() gc.collect() + def on_train_begin( + self, args, state, control, **kwargs # pylint: disable=unused-argument + ): + self._gc() + def on_step_begin( self, args, state, control, **kwargs # pylint: disable=unused-argument ): - if self.next_gc_on_begin_step == state.global_step: + # pylint: disable=consider-using-in + if self.next_gc_on_begin_step == state.global_step or state.global_step == 0: self._gc() def on_step_end( diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 96b694043b..a0e0b96043 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -597,7 +597,7 @@ class AxolotlInputConfig( ) tiled_mlp_use_original_mlp: bool | None = Field( - default=None, + default=True, json_schema_extra={ "description": "Whether to use original mlp for ALST tiled mlp. Otherwise uses a generic MLP based on llama." }, diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index cfa759cad3..9ca33f456e 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -512,19 +512,6 @@ def pretrain_with_tps(cls, data): return data - @model_validator(mode="before") - @classmethod - def check_tiled_mlp_deepspeed(cls, data): - capabilities = data.get("capabilities") - n_gpu = 0 - if capabilities and capabilities.get("n_gpu", 0) >= 1: - n_gpu = capabilities.get("n_gpu", 0) - if data.get("tiled_mlp", False) and (n_gpu > 1 and not data.get("deepspeed")): - raise ValueError( - "tiled_mlp requires deepspeed ZeRO to be enabled for multi-gpu" - ) - return data - class LoRAValidationMixin: """Validation methods related to LoRA/QLoRA configuration.""" From 41434f0c28046a2ed9daf6d81a9e4e99d25b8e0d Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 28 Jul 2025 04:03:50 +0700 Subject: [PATCH 0853/1405] feat(doc): add all providers to readme (#2972) [skip ci] * feat(doc): add vastai link * feat: add cloud providers to readme for more visibility * add prime intellect, remove Modal as sponsor --------- Co-authored-by: Wing Lian --- README.md | 20 ++++++++++++++------ docs/installation.qmd | 11 +++++++---- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b31703e2bd..ae609c4a3f 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,20 @@ docker run --gpus '"all"' --rm -it axolotlai/axolotl:main-latest Other installation approaches are described [here](https://docs.axolotl.ai/docs/installation.html). +#### Cloud Providers + +
+ +- [RunPod](https://runpod.io/gsc?template=v2ickqhz9s&ref=6i7fkpdz) +- [Vast.ai](https://cloud.vast.ai?ref_id=62897&template_id=bdd4a49fa8bce926defc99471864cace&utm_source=github&utm_medium=developer_community&utm_campaign=template_launch_axolotl&utm_content=readme) +- [PRIME Intellect](https://app.primeintellect.ai/dashboard/create-cluster?image=axolotl&location=Cheapest&security=Cheapest&show_spot=true) +- [Modal](https://www.modal.com?utm_source=github&utm_medium=github&utm_campaign=axolotl) +- [Novita](https://novita.ai/gpus-console?templateId=311) +- [JarvisLabs.ai](https://jarvislabs.ai/templates/axolotl) +- [Latitude.sh](https://latitude.sh/blueprint/989e0e79-3bf6-41ea-a46b-1f246e309d5c) + +
+ ### Your First Fine-tune ```bash @@ -121,12 +135,6 @@ Contributions are welcome! Please see our [Contributing Guide](https://github.co ## ❤️ Sponsors -Thank you to our sponsors who help make Axolotl possible: - -- [Modal](https://www.modal.com?utm_source=github&utm_medium=github&utm_campaign=axolotl) - Modal lets you run -jobs in the cloud, by just writing a few lines of Python. Customers use Modal to deploy Gen AI models at large scale, -fine-tune large language models, run protein folding simulations, and much more. - Interested in sponsoring? Contact us at [wing@axolotl.ai](mailto:wing@axolotl.ai) ## 📜 License diff --git a/docs/installation.qmd b/docs/installation.qmd index 0a29aedb90..763539278b 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -124,10 +124,13 @@ For providers supporting Docker: - Use `axolotlai/axolotl-cloud:main-latest` - Available on: - - [Latitude.sh](https://latitude.sh/blueprint/989e0e79-3bf6-41ea-a46b-1f246e309d5c) - - [JarvisLabs.ai](https://jarvislabs.ai/templates/axolotl) - - [RunPod](https://runpod.io/gsc?template=v2ickqhz9s&ref=6i7fkpdz) - - [Novita](https://novita.ai/gpus-console?templateId=311) + - [RunPod](https://runpod.io/gsc?template=v2ickqhz9s&ref=6i7fkpdz) + - [Vast.ai](https://cloud.vast.ai?ref_id=62897&template_id=bdd4a49fa8bce926defc99471864cace&utm_source=axolotl&utm_medium=partner&utm_campaign=template_launch_july2025&utm_content=docs_link) + - [PRIME Intellect](https://app.primeintellect.ai/dashboard/create-cluster?image=axolotl&location=Cheapest&security=Cheapest&show_spot=true) + - [Modal](https://www.modal.com?utm_source=github&utm_medium=github&utm_campaign=axolotl) + - [Novita](https://novita.ai/gpus-console?templateId=311) + - [JarvisLabs.ai](https://jarvislabs.ai/templates/axolotl) + - [Latitude.sh](https://latitude.sh/blueprint/989e0e79-3bf6-41ea-a46b-1f246e309d5c) ### Google Colab {#sec-colab} From add3e5076ba6f55b8abb3a86287f4b532b839805 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 27 Jul 2025 17:04:27 -0400 Subject: [PATCH 0854/1405] don't publish to netlify on contributor submissions since it requires auth tokens (#2985) [skip ci] * don't publish to netlify on contributor submissions since it requires auth tokens * fix no-tmux build and add contact to motd --- .github/workflows/preview-docs.yml | 3 ++- docker/Dockerfile-cloud-no-tmux | 8 +++++--- scripts/motd | 2 ++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 162665ed5f..2393c3d4cf 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -53,6 +53,7 @@ jobs: - name: Netlify Publish uses: nwtgck/actions-netlify@v3.0 + if: ${{ secrets.NETLIFY_AUTH_TOKEN != '' }} id: netlify with: publish-dir: './_site' @@ -67,7 +68,7 @@ jobs: NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - name: Update PR with preview link - if: ${{ steps.netlify.outcome == 'success' }} + if: ${{ steps.netlify.outcome == 'success' && secrets.NETLIFY_AUTH_TOKEN != '' }} uses: marocchino/sticky-pull-request-comment@v2 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/docker/Dockerfile-cloud-no-tmux b/docker/Dockerfile-cloud-no-tmux index 1650631050..594559cfd9 100644 --- a/docker/Dockerfile-cloud-no-tmux +++ b/docker/Dockerfile-cloud-no-tmux @@ -9,13 +9,15 @@ ENV HF_HUB_ENABLE_HF_TRANSFER="1" EXPOSE 8888 EXPOSE 22 -COPY scripts/cloud-entrypoint-term.sh /root/cloud-entrypoint.sh +COPY scripts/cloud-entrypoint.sh /root/cloud-entrypoint.sh COPY scripts/motd /etc/motd RUN pip install jupyterlab notebook ipywidgets && \ jupyter lab clean -RUN apt install --yes --no-install-recommends openssh-server tmux sudo && \ - pip3 install -U --no-cache-dir grpcio ray[default]==2.9.3 && \ +RUN apt update && \ + apt install --yes --no-install-recommends openssh-server tmux iproute2 nvtop ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm && \ + rm -rf /var/cache/apt/archives && \ + rm -rf /var/lib/apt/lists/* && \ mkdir -p ~/.ssh && \ chmod 700 ~/.ssh && \ printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> ~/.bashrc && \ diff --git a/scripts/motd b/scripts/motd index f842bd0764..275a4fcba8 100644 --- a/scripts/motd +++ b/scripts/motd @@ -13,6 +13,8 @@ Welcome to the axolotl cloud image! If the you've mounted a disk to /workspace and the axolotl directory is empty, run the following commands: +Need help with your post-training workloads? Reach out us at contact@axolotl.ai for assistance. + ``` cd /workspace rm -rf /workspace/axolotl From 28804b82e401a7e0b5c511a2714e241c025f91ef Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 27 Jul 2025 17:04:42 -0400 Subject: [PATCH 0855/1405] don't create a reference model if grpo beta is 0.0 (#2983) [skip ci] --- src/axolotl/train.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 9671799035..d57cb463ef 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -115,8 +115,11 @@ def setup_reference_model( LOG.debug("Passing model_ref: None to RL trainer") model_ref = None # explicit setting to None else: + reference_model: bool = True + if cfg.rl == RLType.GRPO and cfg.trl.beta == 0: + reference_model = False # load the model again for model_ref/baseline - model_loader = ModelLoader(cfg, tokenizer, reference_model=True) + model_loader = ModelLoader(cfg, tokenizer, reference_model=reference_model) model_ref, _ = model_loader.load() return model_ref From 430be216d811d4cb0065cfe494b4623d1ce1180e Mon Sep 17 00:00:00 2001 From: NICOLAS BZRD <79255399+Nicolas-BZRD@users.noreply.github.com> Date: Sun, 27 Jul 2025 23:04:56 +0200 Subject: [PATCH 0856/1405] add shuffle_before_merging_datasets option to allow independent shuffling of datasets before merging (#2981) [skip ci] --- .runpod/README.md | 17 +++++++++-------- src/axolotl/utils/data/shared.py | 6 ++++++ src/axolotl/utils/schemas/config.py | 6 ++++++ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.runpod/README.md b/.runpod/README.md index 60c661eef0..2d24f1e5cf 100644 --- a/.runpod/README.md +++ b/.runpod/README.md @@ -119,14 +119,15 @@ datasets: ## Dataset Processing -| Option | Default | Description | -| ----------------------------- | -------------------------- | --------------------------------- | -| `dataset_prepared_path` | `"data/last_run_prepared"` | Path for prepared dataset | -| `push_dataset_to_hub` | `""` | Push dataset to HF hub | -| `dataset_processes` | `4` | Number of preprocessing processes | -| `dataset_keep_in_memory` | `false` | Keep dataset in memory | -| `shuffle_merged_datasets` | `true` | Shuffle merged datasets | -| `dataset_exact_deduplication` | `true` | Deduplicate datasets | +| Option | Default | Description | +| --------------------------------- | -------------------------- | ----------------------------------- | +| `dataset_prepared_path` | `"data/last_run_prepared"` | Path for prepared dataset | +| `push_dataset_to_hub` | `""` | Push dataset to HF hub | +| `dataset_processes` | `4` | Number of preprocessing processes | +| `dataset_keep_in_memory` | `false` | Keep dataset in memory | +| `shuffle_merged_datasets` | `true` | Shuffle merged datasets | +| `shuffle_before_merging_datasets` | `false` | Shuffle each dataset before merging | +| `dataset_exact_deduplication` | `true` | Deduplicate datasets | ## LoRA Configuration diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index bf7a30f485..7877e5abf8 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -543,6 +543,12 @@ def merge_datasets(datasets: list[Dataset], cfg: DictDefault) -> Dataset: return ds.shuffle(seed=cfg.seed) + # If enabled, shuffle each dataset independently before merging. + # This allows curriculum learning strategies to be applied at the dataset level. + if cfg.shuffle_before_merging_datasets: + LOG.info("Shuffling each dataset individually before merging...") + datasets = [ds.shuffle(seed=cfg.seed) for ds in datasets] + LOG.info("Merging datasets...") merged_dataset = concatenate_datasets(datasets) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index a0e0b96043..0afeaa2a8b 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -179,6 +179,12 @@ class AxolotlInputConfig( "description": "If false, the datasets will not be shuffled and will keep their original order in `datasets`. The same applies to the `test_datasets` option and the `pretraining_dataset` option. Default is true." }, ) + shuffle_before_merging_datasets: bool | None = Field( + default=False, + json_schema_extra={ + "description": "If true, each dataset in `datasets` will be shuffled before merging. This allows curriculum learning strategies to be applied at the dataset level. Default is false." + }, + ) dataset_prepared_path: str | None = Field( default=None, json_schema_extra={ From 1d2aa1e4670012c3a4dea62b883abc489629790a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 27 Jul 2025 17:05:12 -0400 Subject: [PATCH 0857/1405] upgrade to support latest transformers release (#2984) * upgrade to support latest transformers release * bump mistral common too * Fix dependencies --- .github/workflows/multi-gpu-e2e.yml | 4 ++-- cicd/multigpu.sh | 6 ++++-- requirements.txt | 6 +++--- setup.py | 16 +++++++++------- src/axolotl/core/builders/base.py | 1 + .../monkeypatch/ring_attn/adapters/batch.py | 15 ++++++++++----- 6 files changed, 29 insertions(+), 19 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index f26201ef01..3085261511 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -37,14 +37,14 @@ jobs: cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.7.0 - axolotl_extras: vllm + axolotl_extras: num_gpus: 2 nightly_build: "true" - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.7.1 - axolotl_extras: + axolotl_extras: vllm num_gpus: 2 nightly_build: "true" runs-on: [self-hosted, modal] diff --git a/cicd/multigpu.sh b/cicd/multigpu.sh index 1f74cd67df..4fd5672bea 100755 --- a/cicd/multigpu.sh +++ b/cicd/multigpu.sh @@ -19,5 +19,7 @@ pytest -v --durations=10 -n1 /workspace/axolotl/tests/e2e/multigpu/patched/ \ --cov-append \ --cov-report=xml:multigpu-coverage.xml -# Upload coverage to Codecov -codecov upload-process -t "${CODECOV_TOKEN}" -f multigpu-coverage.xml -F multigpu,docker-tests,pytorch-${PYTORCH_VERSION} || true +# Upload coverage to Codecov if CODECOV_TOKEN is available +if [ -n "$CODECOV_TOKEN" ]; then + codecov upload-process -t "${CODECOV_TOKEN}" -f multigpu-coverage.xml -F multigpu,docker-tests,pytorch-${PYTORCH_VERSION} || true +fi diff --git a/requirements.txt b/requirements.txt index 6e3c1097ed..8e473bf6bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,13 +13,13 @@ packaging==23.2 huggingface_hub>=0.33.0 peft==0.16.0 -transformers==4.53.2 +transformers==4.54.0 tokenizers>=0.21.1 accelerate==1.9.0 datasets==4.0.0 deepspeed>=0.17.0 trl==0.19.1 -hf_xet==1.1.2 +hf_xet==1.1.5 optimum==1.16.2 hf_transfer @@ -68,4 +68,4 @@ schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.6 axolotl-contribs-mit==0.0.3 -mistral-common==1.7.0 +mistral-common==1.8.3 diff --git a/setup.py b/setup.py index df9a231544..6576c44e52 100644 --- a/setup.py +++ b/setup.py @@ -68,9 +68,10 @@ def parse_requirements(extras_require_map): _install_requires.pop(_install_requires.index(xformers_version)) if patch == 0: _install_requires.append("xformers==0.0.30") + # vllm 0.9.x is incompatible with latest transformers + extras_require_map.pop("vllm") else: - _install_requires.append("xformers==0.0.31.post1") - extras_require_map["vllm"] = ["vllm>=0.9.0"] + _install_requires.append("xformers==0.0.31") elif (major, minor) >= (2, 6): _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers==0.0.29.post3") @@ -84,7 +85,9 @@ def parse_requirements(extras_require_map): else: _install_requires.append("xformers>=0.0.28.post3") _install_requires.pop(_install_requires.index(autoawq_version)) + extras_require_map.pop("vllm") elif (major, minor) >= (2, 4): + extras_require_map.pop("vllm") if patch == 0: _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers>=0.0.27") @@ -114,10 +117,10 @@ def get_package_version(): extras_require = { - "flash-attn": ["flash-attn==2.8.0.post2"], + "flash-attn": ["flash-attn==2.8.2"], "ring-flash-attn": [ - "flash-attn==2.8.0.post2", - "ring-flash-attn>=0.1.5", + "flash-attn==2.8.2", + "ring-flash-attn>=0.1.7", "yunchang==0.6.0", ], "deepspeed": [ @@ -151,13 +154,12 @@ def get_package_version(): "ray[train]", ], "vllm": [ - "vllm==0.7.2", + "vllm==0.10.0", ], "llmcompressor": [ "llmcompressor==0.5.1", ], } - install_requires, dependency_links, extras_require_build = parse_requirements( extras_require ) diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index d3a3b32424..0a37d27667 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -500,6 +500,7 @@ def _set_base_training_args( training_args_kwargs[arg] = getattr(self.cfg, arg) training_args_kwargs["per_device_train_batch_size"] = self.cfg.micro_batch_size + training_args_kwargs["average_tokens_across_devices"] = False if self.cfg.eval_batch_size: training_args_kwargs["per_device_eval_batch_size"] = ( diff --git a/src/axolotl/monkeypatch/ring_attn/adapters/batch.py b/src/axolotl/monkeypatch/ring_attn/adapters/batch.py index 5e56bdd04f..ebed9ebdc3 100644 --- a/src/axolotl/monkeypatch/ring_attn/adapters/batch.py +++ b/src/axolotl/monkeypatch/ring_attn/adapters/batch.py @@ -18,10 +18,15 @@ import transformers.modeling_flash_attention_utils from ring_flash_attn import ring_flash_attn_func from ring_flash_attn.adapters.hf_adapter import check_params -from transformers.modeling_flash_attention_utils import ( - _flash_supports_window_size, - is_flash_attn_greater_or_equal, -) +from transformers.modeling_flash_attention_utils import is_flash_attn_greater_or_equal + +try: + from transformers.modeling_flash_attention_utils import _flash_supports_window +except ImportError: + from transformers.modeling_flash_attention_utils import ( + _flash_supports_window_size as _flash_supports_window, + ) + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS from axolotl.utils.schemas.enums import RingAttnFunc @@ -112,7 +117,7 @@ def _flash_attention_forward( # Handle sliding window use_sliding_windows = ( - _flash_supports_window_size + _flash_supports_window and sliding_window is not None and key_states.shape[1] > sliding_window ) From 90e559893097c73974c46c820cfb79c532e96102 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 30 Jul 2025 15:57:05 +0700 Subject: [PATCH 0858/1405] Feat: Add voxtral, magistral small 1.1, and misc gemma3n fixes (#2979) * fix: lock version in gemma3n docs * feat: add sample configs and docs * chore: move mistraltokenizer into mistral folder * feat: update instructions * feat: add dynamic load voxtral * fix: remove incorrect vision config, add audio * fix: support voxtral processing strategy and address none in data * feat: patch mistraltokenizer subclass upstream and add missing * feat: update cce commit to include voxtral * fix: remove old comment * fix: gemma3 patch not needed anymore * fix: voxtral modeling code * fix: remove incorrect ds path * fix: adjust apply chat template parsing * feat: enable voxtral patch * fix: patch * feat: update example datasets * fix: target layer * feat: update gemma3n docs * feat: update voxtral docs * feat: revert assistant parsing to rely on new upstream changes * chore: skip test till next PR fix * fix: override upstream decode due to missing handling * feat: update readme * fix: update * feat: add magistral small think support * feat: update mistral-common dep * fix: lint * fix: remove optional dep * chore: typing * chore: simply import * feat(doc): update differences for 2507 * fix: coderrabbit comments * feat: update clarify docs on new transformers --- README.md | 1 + .../colab-axolotl-example.ipynb | 2 +- examples/gemma3n/README.md | 64 +- .../gemma-3n-e2b-vision-audio-qlora.yml | 2 - examples/magistral/README.md | 31 +- .../magistral/magistral-small-fsdp-qlora.yaml | 3 + examples/magistral/magistral-small-qlora.yaml | 3 + .../magistral-small-think-qlora.yaml | 68 ++ examples/voxtral/README.md | 76 +++ examples/voxtral/voxtral-mini-audio-qlora.yml | 78 +++ examples/voxtral/voxtral-mini-qlora.yml | 73 ++ scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 2 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/loaders/constants.py | 8 + src/axolotl/loaders/patch_manager.py | 20 +- src/axolotl/loaders/tokenizer.py | 7 +- .../models/{gemma3 => }/__init__.py | 0 .../monkeypatch/models/gemma3/modeling.py | 16 - .../monkeypatch/models/voxtral/__init__.py | 0 .../monkeypatch/models/voxtral/modeling.py | 67 ++ src/axolotl/processing_strategies.py | 39 +- .../prompt_strategies/chat_template.py | 25 +- src/axolotl/utils/dict.py | 13 + src/axolotl/utils/mistral/__init__.py | 5 + .../utils/mistral/mistral_tokenizer.py | 220 ++++++ src/axolotl/utils/mistral_tokenizer.py | 627 ------------------ tests/prompt_strategies/conftest.py | 6 +- .../test_chat_templates_mistral.py | 4 +- 29 files changed, 770 insertions(+), 694 deletions(-) create mode 100644 examples/magistral/magistral-small-think-qlora.yaml create mode 100644 examples/voxtral/README.md create mode 100644 examples/voxtral/voxtral-mini-audio-qlora.yml create mode 100644 examples/voxtral/voxtral-mini-qlora.yml rename src/axolotl/monkeypatch/models/{gemma3 => }/__init__.py (100%) delete mode 100644 src/axolotl/monkeypatch/models/gemma3/modeling.py create mode 100644 src/axolotl/monkeypatch/models/voxtral/__init__.py create mode 100644 src/axolotl/monkeypatch/models/voxtral/modeling.py create mode 100644 src/axolotl/utils/mistral/__init__.py create mode 100644 src/axolotl/utils/mistral/mistral_tokenizer.py delete mode 100644 src/axolotl/utils/mistral_tokenizer.py diff --git a/README.md b/README.md index ae609c4a3f..28c76b8a64 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ ## 🎉 Latest Updates +- 2025/07: Voxtral with mistral-common tokenizer support has been integrated in Axolotl. Read the [docs](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/voxtral)! - 2025/07: TiledMLP support for single-GPU to multi-GPU training with DDP, DeepSpeed and FSDP support has been added to support Arctic Long Sequence Training. (ALST). See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/alst) for using ALST with Axolotl! - 2025/06: Magistral with mistral-common tokenizer support has been added to Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral) to start training your own Magistral models with Axolotl! - 2025/05: Quantization Aware Training (QAT) support has been added to Axolotl. Explore the [docs](https://docs.axolotl.ai/docs/qat.html) to learn more! diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index fd78cf7b41..c66c5c892f 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@631d646\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@010c3ac3f1e725098961832830303eeb4142dd88\"" ] }, { diff --git a/examples/gemma3n/README.md b/examples/gemma3n/README.md index b3922d5264..d570c92f71 100644 --- a/examples/gemma3n/README.md +++ b/examples/gemma3n/README.md @@ -1,19 +1,65 @@ -# Gemma-3n +# Finetune Gemma-3n with Axolotl -## Requirements +Gemma-3n is a family of multimodal models from Google found on [HuggingFace](https://huggingface.co/collections/google/gemma-3n-685065323f5984ef315c93f4). This guide shows how to fine-tune it with Axolotl. -In addition to Axolotl's requirements, Gemma-3n requires +## Getting started -``` -pip3 install timm +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Gemma3n is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). + + Here is an example of how to install from main for pip: + +```bash +# Ensure you have Pytorch installed (Pytorch 2.6.0 min recommended) +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl + +pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install --no-build-isolation -e '.[flash-attn]' ``` -If you will load audio datasets, please also install +2. In addition to Axolotl's requirements, Gemma-3n requires: +```bash +pip3 install timm==1.0.17 + +# for loading audio data +pip3 install librosa==0.11.0 ``` -pip3 install librosa + +3. Run the finetuning example: + +```bash +# text only +axolotl train examples/gemma3n/gemma-3n-e2b-qlora.yml + +# text + vision +axolotl train examples/gemma3n/gemma-3n-e2b-vision-qlora.yml + +# text + vision + audio +axolotl train examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml ``` -## Usage +Let us know how it goes. Happy finetuning! 🚀 + +WARNING: The loss and grad norm will be much higher than normal. We suspect this to be inherent to the model as of the moment. If anyone would like to submit a fix for this, we are happy to take a look. + +### TIPS + +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The text dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). +- The multimodal dataset format follows the OpenAI multi-content Messages format as seen [here](https://docs.axolotl.ai/docs/multimodal.html#dataset-format). + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) + +## Related Resources -See example configs and the [multimodal doc](https://docs.axolotl.ai/docs/multimodal.html). +- [Gemma 3n Blog](https://ai.google.dev/gemma/docs/gemma-3n) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml b/examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml index 15afb6f2e8..d72d7fbc08 100644 --- a/examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml +++ b/examples/gemma3n/gemma-3n-e2b-vision-audio-qlora.yml @@ -34,8 +34,6 @@ eot_tokens: datasets: - path: Nanobit/text-vision-audio-2k-test type: chat_template - data_files: - - dataset.jsonl dataset_prepared_path: val_set_size: 0.01 output_dir: ./outputs/out diff --git a/examples/magistral/README.md b/examples/magistral/README.md index 0c39c061b2..865f872d91 100644 --- a/examples/magistral/README.md +++ b/examples/magistral/README.md @@ -1,6 +1,6 @@ # Finetune Magistral Small with Axolotl -Magistral Small is a 24B parameter opensource model from MistralAI found on [HuggingFace](https://huggingface.co/mistralai/Magistral-Small-2506). This guide shows how to fine-tune it with Axolotl with multi-turn conversations with proper masking. +Magistral Small is a 24B parameter opensource model from MistralAI found on HuggingFace at [2506](https://huggingface.co/mistralai/Magistral-Small-2506) and [2507](https://huggingface.co/mistralai/Magistral-Small-2507) (see [Thinking](#thinking)). This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. MistralAI has also released a proprietary medium-sized version called Magistral Medium. @@ -13,7 +13,7 @@ Thanks to the team at MistralAI for giving us early access to prepare for this r Here is an example of how to install from main for pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0 recommended) +# Ensure you have Pytorch installed (Pytorch 2.6.0 min) git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl @@ -31,12 +31,37 @@ This config uses about 24GB VRAM. Let us know how it goes. Happy finetuning! 🚀 +### Thinking + +MistralAI has released their [2507](https://huggingface.co/mistralai/Magistral-Small-2507) model with thinking capabilities. The model requires the multi-content dataset format with support for an extra `role: thinking` within system and assistant messages. + +Example format: + +```json +{ + "messages": [ + {"role": "system", "content": [{ "type": "text", "text": "{SYSTEM_PROMPT}"}]}, + {"role": "user", "content": [{ "type": "text", "text": "..."}]}, + {"role": "assistant", "content": [{ "type": "thinking", "thinking": "..."}, { "type": "text", "text": "..." }]}, + ], +} +``` + +Example config: `./magistral-small-think-qlora.yaml`. + +The `thinking` section also supports an optional arg `closed: bool` (`True` default) which controls adding the closing `[/THINK]` tag. + +Limitations: +- You cannot mix `content: str` with `content: list[dict]` as the `dataset.load_dataset` may complain about different types for `content` key. +- This mode does not work with custom `train_detail` and `training` at the moment. + ### TIPS +- We recommend adding the same/similar SystemPrompt that the model is tuned for. You can find this within the repo's files titled `SYSTEM_PROMPT.txt`. - For inference, the official MistralAI team recommends `top_p: 0.95` and `temperature: 0.7` with `max_tokens: 40960`. - You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. - Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). -- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). +- The text dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). ## Optimization Guides diff --git a/examples/magistral/magistral-small-fsdp-qlora.yaml b/examples/magistral/magistral-small-fsdp-qlora.yaml index 4a769510aa..14a7ee2192 100644 --- a/examples/magistral/magistral-small-fsdp-qlora.yaml +++ b/examples/magistral/magistral-small-fsdp-qlora.yaml @@ -6,6 +6,9 @@ tokenizer_use_mistral_common: true # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + load_in_8bit: false load_in_4bit: true diff --git a/examples/magistral/magistral-small-qlora.yaml b/examples/magistral/magistral-small-qlora.yaml index bb2e0ccf05..5ec2f0fbf5 100644 --- a/examples/magistral/magistral-small-qlora.yaml +++ b/examples/magistral/magistral-small-qlora.yaml @@ -6,6 +6,9 @@ tokenizer_use_mistral_common: true # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + load_in_8bit: false load_in_4bit: true diff --git a/examples/magistral/magistral-small-think-qlora.yaml b/examples/magistral/magistral-small-think-qlora.yaml new file mode 100644 index 0000000000..0e8a9c1f7f --- /dev/null +++ b/examples/magistral/magistral-small-think-qlora.yaml @@ -0,0 +1,68 @@ +base_model: mistralai/Magistral-Small-2507 + +# Enable to use mistral-common tokenizer +tokenizer_use_mistral_common: true + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: Nanobit/text-think-2k-test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/voxtral/README.md b/examples/voxtral/README.md new file mode 100644 index 0000000000..669ebbe55b --- /dev/null +++ b/examples/voxtral/README.md @@ -0,0 +1,76 @@ +# Finetune Voxtral with Axolotl + +Voxtral is a [3B](https://huggingface.co/mistralai/Voxtral-Mini-3B-2507)/[24B](https://huggingface.co/mistralai/Voxtral-Small-24B-2507) parameter opensource model from MistralAI found on HuggingFace. This guide shows how to fine-tune it with Axolotl. + +Thanks to the team at MistralAI for giving us early access to prepare for this release. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Voxtral is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). + + Here is an example of how to install from main for pip: + +```bash +# Ensure you have Pytorch installed (Pytorch 2.6.0 min) +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl + +pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install --no-build-isolation -e '.[flash-attn]' +``` + +2. Please install the below. + +```bash +# audio +pip3 install librosa==0.11.0 +pip3 install 'mistral_common[audio]==1.8.3' +``` + +3. Run the finetuning example: + +```bash +# text only +axolotl train examples/voxtral/voxtral-mini-qlora.yml + +# text + audio +axolotl train examples/voxtral/voxtral-mini-audio-qlora.yml +``` + +These configs use about 4.8 GB VRAM. + +Let us know how it goes. Happy finetuning! 🚀 + +### TIPS + +- For inference, the official MistralAI team recommends `temperature: 0.2` and `top_p: 0.95` for audio understanding and `temperature: 0.0` for transcription. +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The text dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). +- The multimodal dataset format follows the OpenAI multi-content Messages format as seen [here](https://docs.axolotl.ai/docs/multimodal.html#dataset-format). + + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) + +## Limitations + +We only support the `mistral-common` tokenizer for Supervised Fine-tuning at the moment and for `type: chat_template` only. + +In addition, we do not support overriding tokens yet. + +## Related Resources + +- [MistralAI Magistral Blog](https://mistral.ai/news/magistral/) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) + +## Future Work + +- Add parity to Preference Tuning, RL, etc. +- Add parity to other tokenizer configs like overriding tokens. diff --git a/examples/voxtral/voxtral-mini-audio-qlora.yml b/examples/voxtral/voxtral-mini-audio-qlora.yml new file mode 100644 index 0000000000..8fe6adbff0 --- /dev/null +++ b/examples/voxtral/voxtral-mini-audio-qlora.yml @@ -0,0 +1,78 @@ +base_model: mistralai/Voxtral-Mini-3B-2507 +processor_type: AutoProcessor + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +# Enable to use mistral-common tokenizer +tokenizer_use_mistral_common: true + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +# for use with fft to only train on language model layers +# unfrozen_parameters: + # - language_model.model.* + # - lm_head + # - embed_tokens + +load_in_4bit: true + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +# gemma3 doesn't seem to play nice with ddp +ddp_find_unused_parameters: true + +eot_tokens: + - + +# sample dataset below requires downloading audio/image in advance +# wget https://huggingface.co/datasets/Nanobit/text-audio-2k-test/resolve/main/En-us-African_elephant.oga +datasets: + - path: NanoBit/text-audio-2k-test + type: chat_template +dataset_prepared_path: +val_set_size: 0.01 +output_dir: ./outputs/out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 diff --git a/examples/voxtral/voxtral-mini-qlora.yml b/examples/voxtral/voxtral-mini-qlora.yml new file mode 100644 index 0000000000..bdbc5f8673 --- /dev/null +++ b/examples/voxtral/voxtral-mini-qlora.yml @@ -0,0 +1,73 @@ +base_model: mistralai/Voxtral-Mini-3B-2507 + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +# Enable to use mistral-common tokenizer +tokenizer_use_mistral_common: true + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +# for use with fft to only train on language model layers +# unfrozen_parameters: + # - language_model.model.* + # - lm_head + # - embed_tokens + +eot_tokens: + - +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + split: train[:1%] + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'language_model.model.layers.[\d]+.(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: true +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 63e527d5e6..404d6361d0 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@631d646"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@010c3ac3f1e725098961832830303eeb4142dd88"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index a97bac71c3..9daabc8626 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@631d646" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@010c3ac3f1e725098961832830303eeb4142dd88" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 1fe54deedd..e6a52e8d8c 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -34,7 +34,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@631d646"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@010c3ac3f1e725098961832830303eeb4142dd88"`' ) diff --git a/src/axolotl/loaders/constants.py b/src/axolotl/loaders/constants.py index c340c414c7..3fabf9d940 100644 --- a/src/axolotl/loaders/constants.py +++ b/src/axolotl/loaders/constants.py @@ -21,3 +21,11 @@ "gemma3": Gemma3ForConditionalGeneration, "gemma3n": Gemma3nForConditionalGeneration, } + +try: + from transformers import VoxtralForConditionalGeneration + + # transformers >4.53.2 + MULTIMODAL_AUTO_MODEL_MAPPING["voxtral"] = VoxtralForConditionalGeneration +except ImportError: + pass diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index f1bb3ae674..186681521f 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -64,12 +64,12 @@ def apply_pre_model_load_patches(self): self._patch_llama_derived_model() self._apply_mistral_cross_entropy_patch() self._apply_self_attention_lora_patch() - self._apply_gemma3_conditional_generation_forward_patch() self._apply_sequence_parallel_patches() def apply_post_plugin_pre_model_load_patches(self): """Apply post plugin-pre_model_load load patches based on config.""" self._apply_tiled_mlp(self.cfg.model_config_type) + self._apply_voxtral_patches() def apply_post_model_load_patches(self, model: PreTrainedModel): """Apply patches that require the model instance.""" @@ -253,15 +253,6 @@ def _apply_multipack_patches(self): has_remote_code=has_remote_code, ) - def _apply_gemma3_conditional_generation_forward_patch(self): - """Apply gemma3 conditional generation forward patch.""" - if self.model_config.model_type in ["gemma3", "gemma3_text"]: - from axolotl.monkeypatch.models.gemma3.modeling import ( - patch_gemma3_conditional_generation_forward, - ) - - patch_gemma3_conditional_generation_forward() - def _apply_sequence_parallel_patches(self): """Apply sequence parallelism patches.""" if self.cfg.sequence_parallel_degree and self.cfg.sequence_parallel_degree > 1: @@ -285,6 +276,15 @@ def _apply_tiled_mlp(self, model_type: str): cfg_num_shards=self.cfg.tiled_mlp_num_shards, ) + def _apply_voxtral_patches(self): + """Apply patches for Voxtral model.""" + if self.cfg.model_config_type == "voxtral": + from axolotl.monkeypatch.models.voxtral.modeling import ( + patch_voxtral_conditional_generation_forward, + ) + + patch_voxtral_conditional_generation_forward() + def _patch_attention(self): """Apply attention-specific patches based on model type.""" if not (self.cfg.flash_attention and hasattr(self.model_config, "model_type")): diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index 1889fa1685..0a486d0234 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -124,7 +124,12 @@ def load_tokenizer(cfg: DictDefault) -> PreTrainedTokenizer: def _load_mistral_common_tokenizer(cfg: DictDefault): """Load mistral-common tokenizer""" - from axolotl.utils.mistral_tokenizer import HFMistralTokenizer + from transformers import tokenization_mistral_common + + from axolotl.utils.mistral import HFMistralTokenizer + + # patch + tokenization_mistral_common.MistralCommonTokenizer = HFMistralTokenizer # Load the HF-compatible wrapper around MistralTokenizer tokenizer = HFMistralTokenizer.from_pretrained(cfg.tokenizer_config) diff --git a/src/axolotl/monkeypatch/models/gemma3/__init__.py b/src/axolotl/monkeypatch/models/__init__.py similarity index 100% rename from src/axolotl/monkeypatch/models/gemma3/__init__.py rename to src/axolotl/monkeypatch/models/__init__.py diff --git a/src/axolotl/monkeypatch/models/gemma3/modeling.py b/src/axolotl/monkeypatch/models/gemma3/modeling.py deleted file mode 100644 index 3b608c347c..0000000000 --- a/src/axolotl/monkeypatch/models/gemma3/modeling.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Monkeypatch for gemma3 conditional generation forward to fix high loss""" - - -def patch_gemma3_conditional_generation_forward(): - # Remove when https://github.com/huggingface/transformers/pull/37208 merged - - from transformers.models.gemma3.modeling_gemma3 import ( - Gemma3ForConditionalGeneration, - ) - - setattr(Gemma3ForConditionalGeneration, "accepts_loss_kwargs", False) - - def unpatch(): - delattr(Gemma3ForConditionalGeneration, "accepts_loss_kwargs") - - return unpatch diff --git a/src/axolotl/monkeypatch/models/voxtral/__init__.py b/src/axolotl/monkeypatch/models/voxtral/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/voxtral/modeling.py b/src/axolotl/monkeypatch/models/voxtral/modeling.py new file mode 100644 index 0000000000..3dd652dd8e --- /dev/null +++ b/src/axolotl/monkeypatch/models/voxtral/modeling.py @@ -0,0 +1,67 @@ +"""Monkeypatch for voxtral to fix leaf node and dtype mismatch""" + +from typing import Optional, Union + +import torch +from transformers.cache_utils import Cache +from transformers.modeling_outputs import CausalLMOutputWithPast + + +def patch_voxtral_conditional_generation_forward(): + from transformers.models.voxtral.modeling_voxtral import ( + VoxtralForConditionalGeneration, + ) + + # Store the original forward method + old_forward = VoxtralForConditionalGeneration.forward + + def _forward( + self, + input_ids: Optional[torch.LongTensor] = None, + input_features: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs, + ) -> CausalLMOutputWithPast: + if inputs_embeds is None: + inputs_embeds = self.get_input_embeddings()(input_ids) + + if input_features is not None: + audio_embeds = self.get_audio_embeds(input_features) + + # Cast audio_embeds to match inputs_embeds dtype + audio_embeds = audio_embeds.to(inputs_embeds.dtype) + + # replace text-audio token placeholders with audio embeddings + audio_token_mask = input_ids == self.config.audio_token_id + + inputs_embeds = inputs_embeds.clone() + inputs_embeds[audio_token_mask] = audio_embeds + + outputs = self.language_model( + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + labels=labels, + use_cache=use_cache, + cache_position=cache_position, + logits_to_keep=logits_to_keep, + **kwargs, + ) + return outputs + + # Apply the patch + VoxtralForConditionalGeneration.forward = _forward + + def unpatch(): + """Restore the original forward method""" + VoxtralForConditionalGeneration.forward = old_forward + + return unpatch diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 1cb2974068..4cc5e85a1e 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -6,9 +6,10 @@ from PIL import Image, ImageOps from PIL.Image import Resampling from torch import Tensor, zeros_like -from transformers import ProcessorMixin +from transformers import ProcessorMixin, VoxtralProcessor from transformers.image_utils import load_image +from axolotl.utils.dict import remove_none_values from axolotl.utils.logging import get_logger LOG = get_logger(__name__) @@ -204,7 +205,7 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: } ) - processed_examples.append(processed_example) + processed_examples.append(remove_none_values(processed_example)) return processed_examples @@ -366,6 +367,34 @@ def process_labels(self, input_ids): return labels +class VoxtralProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for Voxtral""" + + def __init__( + self, + processor: VoxtralProcessor, + chat_template: Optional[str] = None, + image_size: int | tuple[int, int] | None = None, + image_resize_algorithm: Resampling | None = None, + ): + super().__init__(processor, chat_template, image_size, image_resize_algorithm) + special_ids = ( + processor.tokenizer.tokenizer.instruct_tokenizer.audio_encoder.special_ids + ) + + self.audio_token = special_ids.audio + self.begin_audio_token = special_ids.begin_audio + + def process_labels(self, input_ids): + labels = input_ids.clone() + + labels[labels == self.processor.tokenizer.pad_token_id] = -100 + labels[labels == self.audio_token] = -100 + labels[labels == self.begin_audio_token] = -100 + + return labels + + def get_processing_strategy( processor: ProcessorMixin, chat_template, @@ -395,4 +424,10 @@ def get_processing_strategy( return ProcessingStrategy( processor, chat_template, image_size, image_resize_algorithm ) + + if isinstance(processor, VoxtralProcessor): + return VoxtralProcessingStrategy( + processor, chat_template, image_size, image_resize_algorithm + ) + raise ValueError(f"Unsupported chat template type: {chat_template_type}") diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index ced8c8da66..80fe9275e2 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -14,11 +14,12 @@ from axolotl.prompt_tokenizers import PromptTokenizingStrategy from axolotl.prompters import IGNORE_TOKEN_ID, Prompter from axolotl.utils.chat_templates import get_chat_template_from_config +from axolotl.utils.dict import remove_none_values from axolotl.utils.logging import get_logger from axolotl.utils.schemas.datasets import DatasetConfig if TYPE_CHECKING: - from axolotl.utils.mistral_tokenizer import HFMistralTokenizer + from axolotl.utils.mistral import HFMistralTokenizer # Configure the logger LOG = get_logger(__name__) @@ -379,21 +380,7 @@ def tokenize_prompt(self, prompt: dict[str, Any]): Public method that can handle either a single prompt or a batch of prompts. """ - def _remove_none_values(obj): - """ - Remove null from a dictionary-like obj or list. - These can appear due to Dataset loading causing schema merge. - See https://github.com/axolotl-ai-cloud/axolotl/pull/2909 - """ - if hasattr(obj, "items"): - return { - k: _remove_none_values(v) for k, v in obj.items() if v is not None - } - if isinstance(obj, list): - return [_remove_none_values(elem) for elem in obj] - return obj - - prompt = _remove_none_values(prompt) + prompt = remove_none_values(prompt) if not self.is_prompt_batched(prompt) or not self.supports_batched: return self._tokenize_single_prompt(prompt) @@ -502,6 +489,12 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: if should_train and turn_start_idx != -1 and turn_end_idx != -1: if train_detail: + # Block multi-content for now + if not isinstance(content, str): + raise ValueError( + "`train_detail` is not supported when `content` is not a string." + ) + token_offsets = self.prompter.get_offsets_for_train_detail( # type: ignore content, train_detail ) diff --git a/src/axolotl/utils/dict.py b/src/axolotl/utils/dict.py index f24f7c4a98..c2670dfeb6 100644 --- a/src/axolotl/utils/dict.py +++ b/src/axolotl/utils/dict.py @@ -36,3 +36,16 @@ def __setitem__(self, name, value): p[key] = self object.__delattr__(self, "__parent") object.__delattr__(self, "__key") + + +def remove_none_values(obj): + """ + Remove null from a dictionary-like obj or list. + These can appear due to Dataset loading causing schema merge. + See https://github.com/axolotl-ai-cloud/axolotl/pull/2909 + """ + if hasattr(obj, "items"): + return {k: remove_none_values(v) for k, v in obj.items() if v is not None} + if isinstance(obj, list): + return [remove_none_values(elem) for elem in obj] + return obj diff --git a/src/axolotl/utils/mistral/__init__.py b/src/axolotl/utils/mistral/__init__.py new file mode 100644 index 0000000000..eb1e2df895 --- /dev/null +++ b/src/axolotl/utils/mistral/__init__.py @@ -0,0 +1,5 @@ +"""Init for `axolotl.utils.mistral` module.""" + +from axolotl.utils.mistral.mistral_tokenizer import HFMistralTokenizer + +__all__ = ["HFMistralTokenizer"] diff --git a/src/axolotl/utils/mistral/mistral_tokenizer.py b/src/axolotl/utils/mistral/mistral_tokenizer.py new file mode 100644 index 0000000000..61cbdc5b0d --- /dev/null +++ b/src/axolotl/utils/mistral/mistral_tokenizer.py @@ -0,0 +1,220 @@ +"""Wrapper for MistralTokenizer from mistral-common""" + +import os +from typing import Optional + +import numpy as np +from mistral_common.protocol.instruct.validator import ValidationMode +from mistral_common.tokens.tokenizers.utils import download_tokenizer_from_hf_hub +from torch import Tensor +from transformers.tokenization_mistral_common import MistralCommonTokenizer +from transformers.tokenization_utils_base import VERY_LARGE_INTEGER + + +class HFMistralTokenizer(MistralCommonTokenizer): + """ + Wraps mistral_common.tokens.tokenizers.mistral.MistralTokenizer + and exposes HuggingFace API for special tokens. + """ + + def __init__(self, name_or_path: str, **kwargs): + """ + Args: + name_or_path: The name or path to the tokenizer files or the repo id. + **kwargs: Additional keyword arguments passed to the parent class. + """ + kwargs.pop("mode", None) + + mode = ValidationMode.finetuning + super().__init__(**kwargs, mode=mode) + + self._name_or_path = name_or_path + + # set mode as is not set upstream + self._set_mode(mode) + + @property + def name_or_path(self) -> str: + return self._name_or_path + + @property + def chat_template(self) -> str | None: + """Chat template is not supported. Dummy method to satisfy HuggingFace API.""" + return "[This is a dummy chat template]" + + def _set_mode(self, mode: ValidationMode): + """Set the mode of the MistralRequestValidator. + + Args: + mode: The mode to set. + + Raises: + RuntimeError: If the MistralRequestValidator does not have a _mode attribute. + """ + # Check if MistralRequestValidator has a _mode attribute. + # This is a private API and may change in the future. + # pylint: disable=protected-access + from mistral_common.protocol.instruct.validator import MistralRequestValidator + + if not ( + hasattr(self.tokenizer, "_chat_completion_request_validator") + and isinstance( + self.tokenizer._chat_completion_request_validator, + MistralRequestValidator, + ) + and hasattr(self.tokenizer._chat_completion_request_validator, "_mode") + ): + raise RuntimeError( + f"Unable to switch mistral tokenizer to {mode.value} mode - " + "private API `_chat_completion_request_validator._mode` missing." + ) + + self.tokenizer._chat_completion_request_validator._mode = mode + + def apply_chat_template( # type: ignore + self, + conversation: list[dict] | list[list[dict]], + chat_template: str | None = None, # pylint: disable=unused-argument + add_generation_prompt: bool = False, + **kwargs, + ) -> str | list[int]: + """Patched fn to handle setting serving mode, continue_final_message, remove chat_template and add_generation_prompt kwarg""" + + try: + if add_generation_prompt: + self._set_mode(ValidationMode.serving) + kwargs["continue_final_message"] = True + + out = super().apply_chat_template(conversation, **kwargs) + + return out # type: ignore + + finally: + if add_generation_prompt: + self._set_mode(ValidationMode.finetuning) + + def decode( # type: ignore + self, + token_ids: int | list[int] | np.ndarray | Tensor, + **kwargs, + ) -> str: + """ + Decode token_ids into str. + + This overrides upstream.decode to convert int to list[int] + """ + + if isinstance(token_ids, int): + token_ids = [token_ids] + + return super().decode(token_ids, **kwargs) + + @classmethod + def from_pretrained( + cls, + pretrained_model_name_or_path: str | os.PathLike, + *init_inputs, + mode: ValidationMode = ValidationMode.test, + cache_dir: Optional[str | os.PathLike] = None, + force_download: bool = False, + local_files_only: bool = False, + token: Optional[str | bool] = None, + revision: str = "main", + model_max_length: int = VERY_LARGE_INTEGER, + padding_side: str = "left", + truncation_side: str = "right", + model_input_names: Optional[list[str]] = None, + clean_up_tokenization_spaces: bool = False, + **kwargs, + ): + r""" + Patched fn to pass `name_or_path` and remove extra kwargs. + + Instantiate a `MistralCommonTokenizer` from a predefined + tokenizer. + + Args: + pretrained_model_name_or_path (`str` or `os.PathLike`): + Can be either: + + - A string, the *model id* of a predefined tokenizer hosted inside a model repo on huggingface.co. + - A path to a *directory* containing the tokenizer config, for instance saved + using the [`MistralCommonTokenizer.tokenization_mistral_common.save_pretrained`] method, e.g., + `./my_model_directory/`. + mode (`ValidationMode`, *optional*, defaults to `ValidationMode.test`): + Validation mode for the `MistralTokenizer` tokenizer. + cache_dir (`str` or `os.PathLike`, *optional*): + Path to a directory in which a downloaded predefined tokenizer vocabulary files should be cached if the + standard cache should not be used. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download the vocabulary files and override the cached versions if they + exist. + token (`str` or *bool*, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, will use the token generated + when running `huggingface-cli login` (stored in `~/.huggingface`). + local_files_only (`bool`, *optional*, defaults to `False`): + Whether or not to only rely on local files and not to attempt to download any files. + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, or a commit id, since we use a + git-based system for storing models and other artifacts on huggingface.co, so `revision` can be any + identifier allowed by git. + max_length (`int`, *optional*): + Controls the maximum length to use by one of the truncation/padding parameters. + + If left unset or set to `None`, this will use the predefined model maximum length if a maximum length + is required by one of the truncation/padding parameters. If the model has no specific maximum input + length (like XLNet) truncation/padding to a maximum length will be deactivated. + padding_side (`str`, *optional*, defaults to `"left"`): + The side on which the model should have padding applied. Should be selected between ['right', 'left']. + Default value is picked from the class attribute of the same name. + truncation_side (`str`, *optional*, defaults to `"right"`): + The side on which the model should have truncation applied. Should be selected between ['right', 'left']. + model_input_names (`List[string]`, *optional*): + The list of inputs accepted by the forward pass of the model (like `"token_type_ids"` or + `"attention_mask"`). Default value is picked from the class attribute of the same name. + clean_up_tokenization_spaces (`bool`, *optional*, defaults to `False`): + Whether or not the model should cleanup the spaces that were added when splitting the input text during the + tokenization process. + kwargs (additional keyword arguments, *optional*): + Not supported by `MistralCommonTokenizer.from_pretrained`. + Will raise an error if used. + """ + if init_inputs: + raise ValueError( + "`init_inputs` are not supported by `MistralCommonTokenizer.from_pretrained`." + ) + + # Delete trust_remote_code as it does nothing + kwargs.pop("trust_remote_code", None) + + # Delete tokenizer as it does nothing + kwargs.pop("tokenizer", None) + + # Handle kwargs and AutoTokenizer case + if kwargs and not kwargs.keys() == {"_from_auto"}: + raise ValueError( + f"Kwargs {list(kwargs.keys())} are not supported by `MistralCommonTokenizer.from_pretrained`." + ) + + if not os.path.isfile(pretrained_model_name_or_path): + tokenizer_path = download_tokenizer_from_hf_hub( + repo_id=str(pretrained_model_name_or_path), + cache_dir=str(cache_dir), + token=token, + revision=revision, + force_download=force_download, + local_files_only=local_files_only, + ) + else: + tokenizer_path = str(pretrained_model_name_or_path) + + return cls( + name_or_path=str(pretrained_model_name_or_path), + tokenizer_path=tokenizer_path, + mode=mode, + model_max_length=model_max_length, + padding_side=padding_side, + truncation_side=truncation_side, + model_input_names=model_input_names, + clean_up_tokenization_spaces=clean_up_tokenization_spaces, + ) diff --git a/src/axolotl/utils/mistral_tokenizer.py b/src/axolotl/utils/mistral_tokenizer.py deleted file mode 100644 index 33c08db465..0000000000 --- a/src/axolotl/utils/mistral_tokenizer.py +++ /dev/null @@ -1,627 +0,0 @@ -"""Wrapper for MistralTokenizer from mistral-common""" - -import math -import os -from shutil import copyfile -from typing import Optional - -import numpy as np -from huggingface_hub import hf_hub_download -from mistral_common.protocol.instruct.request import ChatCompletionRequest -from mistral_common.tokens.tokenizers.mistral import MistralTokenizer -from mistral_common.tokens.tokenizers.tekken import SpecialTokenPolicy, Tekkenizer -from torch import Tensor -from transformers.utils import PaddingStrategy - -from axolotl.utils.collators.core import IGNORE_INDEX - - -def _get_file_path(path_or_repo_id: str, filename: str) -> str: - """Get the file path from local or HF Hub""" - if os.path.exists(path_or_repo_id): - maybe_file_path = os.path.join(path_or_repo_id, filename) - if os.path.exists(maybe_file_path): - return maybe_file_path - - raise FileNotFoundError(f"File not found at {path_or_repo_id}") - - return hf_hub_download(repo_id=path_or_repo_id, filename=filename) - - -class HFMistralTokenizer: - """ - Wraps mistral_common.tokens.tokenizers.mistral.MistralTokenizer - and exposes HuggingFace API for special tokens. - """ - - def __init__( - self, mistral: MistralTokenizer, name_or_path: str, tokenizer_path: str - ): - """ - Args: - mistral: The mistral-common tokenizer to wrap. - name_or_path: The name or path to the tokenizer files or the repo id. - """ - self._mistral = mistral - self._padding_side = "right" - self._name_or_path = name_or_path - self._tokenizer_path = tokenizer_path - - # Manual set to training mode - from mistral_common.protocol.instruct.validator import ( - MistralRequestValidator, - ValidationMode, - ) - - # Check if MistralRequestValidator has a _mode attribute. - # This is a private API and may change in the future. - # pylint: disable=protected-access - if not ( - hasattr(self._mistral, "_chat_completion_request_validator") - and isinstance( - self._mistral._chat_completion_request_validator, - MistralRequestValidator, - ) - and hasattr(self._mistral._chat_completion_request_validator, "_mode") - ): - raise RuntimeError( - "Unable to switch mistral tokenizer to finetuning mode – " - "private API `_chat_completion_request_validator._mode` missing." - ) - - self._mistral._chat_completion_request_validator._mode = ( - ValidationMode.finetuning - ) - - def _load_system_prompt(self, path_or_repo_id: str) -> str: - """Load system prompt from local or HF Hub. - - Note: Unused for now as we don't want to explicitly set the system prompt if a user does - not provide one. - - Args: - path_or_repo_id: The path to the tokenizer files or the repo id. - - Returns: - The system prompt. - """ - file_path = _get_file_path(path_or_repo_id, "SYSTEM_PROMPT.txt") - - if not os.path.exists(file_path): - raise FileNotFoundError(f"System prompt file not found at {file_path}") - - with open(file_path, "r", encoding="utf-8") as file: - return file.read() - - @property - def bos_token_id(self) -> int: - return self._mistral.instruct_tokenizer.tokenizer.bos_id - - @property - def eos_token_id(self) -> int: - return self._mistral.instruct_tokenizer.tokenizer.eos_id - - @property - def pad_token_id(self) -> int: - return self._mistral.instruct_tokenizer.tokenizer.pad_id - - @property - def unk_token_id(self) -> int: - return self._mistral.instruct_tokenizer.tokenizer.unk_id - - @property - def bos_token(self) -> str: - return self._mistral.instruct_tokenizer.tokenizer.id_to_piece(self.bos_token_id) - - @property - def eos_token(self) -> str: - return self._mistral.instruct_tokenizer.tokenizer.id_to_piece(self.eos_token_id) - - @property - def pad_token(self) -> str: - return self._mistral.instruct_tokenizer.tokenizer.id_to_piece(self.pad_token_id) - - @property - def unk_token(self) -> str: - return self._mistral.instruct_tokenizer.tokenizer.id_to_piece(self.unk_token_id) - - @property - def padding_side(self) -> str: - return self._padding_side - - @property - def name_or_path(self) -> str: - return self._name_or_path - - @property - def chat_template(self) -> str | None: - """Chat template is not supported. Dummy method to satisfy HuggingFace API.""" - return None - - def __len__(self) -> int: - return self._mistral.instruct_tokenizer.tokenizer.n_words - - @classmethod - def from_pretrained( - cls, - name_or_path: str, - *, - revision: Optional[str] = None, - **kwargs, # pylint: disable=unused-argument - ) -> "HFMistralTokenizer": - """ - Load a mistral tekken tokenizer from a local file or HF Hub and wrap it. - - Args: - path_or_repo_id: The path to the tokenizer files or the repo id. - revision: The revision of the tokenizer to download. - kwargs: Additional keyword arguments. - - Returns: - A HFMistralTokenizer instance. - """ - if revision: - raise NotImplementedError( - "Revision not supported yet for mistral-common tokenizer" - ) - - # only support Tekken tokenizer for now - # downloads from HF Hub if not local - tokenizer_path = _get_file_path(name_or_path, "tekken.json") - - base = MistralTokenizer.from_file(tokenizer_path) - - return cls( - base, - name_or_path=name_or_path, - tokenizer_path=tokenizer_path, - ) - - def save_pretrained(self, save_directory: str) -> None: - """ - Save the Tekken/SentencePiece model file so that from_pretrained can pick it up again. - - Only Tekken models are supported. - - Args: - save_directory: The directory to save the tokenizer files. - """ - inner = self._mistral.instruct_tokenizer.tokenizer - if isinstance(inner, Tekkenizer): - # Create the directory and save the model - try: - os.makedirs(save_directory, exist_ok=True) - - # Verify directory was created - if not os.path.exists(save_directory): - raise RuntimeError(f"Failed to create directory: {save_directory}") - - # Verify source file exists - if not os.path.exists(self._tokenizer_path): - raise FileNotFoundError( - f"Source tokenizer file not found: {self._tokenizer_path}" - ) - - destination_path = os.path.join(save_directory, "tekken.json") - copyfile(self._tokenizer_path, destination_path) - - except Exception as e: - raise RuntimeError( - f"Failed to save tokenizer to {save_directory}: {e}. " - f"Source path: {self._tokenizer_path}, " - f"Directory exists: {os.path.exists(save_directory)}" - ) from e - - else: - raise RuntimeError(f"Unknown tokenizer type: {type(inner)}") - - def encode(self, text: str, add_special_tokens: bool = True) -> list[int]: - """ - Encode a text string into a list of token IDs. - - Args: - text: The text string to encode. - add_special_tokens: Whether to add special tokens to the encoded tokens. - - Returns: - A list of token IDs. - """ - return self._mistral.instruct_tokenizer.tokenizer.encode( - text, - bos=add_special_tokens, - eos=add_special_tokens, - ) - - def decode( - self, token_ids: int | list[int], skip_special_tokens: bool = False - ) -> str: - """ - Decode a list of token IDs into a text string. - - Args: - token_ids: The int or list of token IDs to decode. - skip_special_tokens: Whether to skip special tokens in the decoded text. - - Returns: - The decoded text string. - """ - if isinstance(token_ids, int): - token_ids = [token_ids] - - if skip_special_tokens: - return self._mistral.instruct_tokenizer.tokenizer.decode( - token_ids, special_token_policy=SpecialTokenPolicy.IGNORE - ) - - return self._mistral.instruct_tokenizer.tokenizer.decode( - token_ids, special_token_policy=SpecialTokenPolicy.KEEP - ) - - def apply_chat_template( - self, - messages: list[dict], - tokenize: bool = True, - tools: list[dict] | None = None, - chat_template: str | None = None, # pylint: disable=unused-argument - add_generation_prompt: bool = False, # pylint: disable=unused-argument - ) -> list[int] | str: - if chat_template: - raise NotImplementedError("chat_template not supported yet") - - if add_generation_prompt: - raise NotImplementedError("add_generation_prompt not supported yet") - - chat_completion: ChatCompletionRequest = ChatCompletionRequest.from_openai( - messages, tools - ) - - tokens: list[int] = self._mistral.encode_chat_completion(chat_completion).tokens - - if tokenize: - return tokens - - return self.decode(tokens) - - def pad( - self, - features: list[dict[str, list[int] | np.ndarray]], - *, - padding: bool | str | PaddingStrategy = True, - max_length: int | None = None, - pad_to_multiple_of: int | None = None, - return_tensors: str | None = None, # "np", "pt", or "tf" - ) -> dict[str, np.ndarray | Tensor]: - """ - HF-style pad method that properly handles all sequence-related features: - - pad 'input_ids' & 'labels' to the longest (or to max_length) - """ - import torch - from torch.nn import functional as F - - # Check for unsupported fields - if any("token_type_ids" in f for f in features): - raise ValueError("token_type_ids is not supported by this tokenizer") - - # Determine desired sequence length - lengths = [len(f["input_ids"]) for f in features] - if padding in (True, "longest", PaddingStrategy.LONGEST): - target_length = max(lengths) - elif padding in ("max_length", PaddingStrategy.MAX_LENGTH): - if max_length is None: - raise ValueError("max_length must be set for 'max_length' padding") - target_length = max_length - elif padding in (False, "do_not_pad", PaddingStrategy.DO_NOT_PAD): - target_length = None - else: - raise ValueError(f"Unknown padding strategy: {padding}") - - # Apply pad_to_multiple_of - if target_length is not None and pad_to_multiple_of is not None: - target_length = ( - math.ceil(target_length / pad_to_multiple_of) * pad_to_multiple_of - ) - - # If no padding requested, just stack tensors - do_pad = target_length is not None - - # Pad sequences using torch.nn.utils.rnn.pad_sequence - input_ids = torch.nn.utils.rnn.pad_sequence( - [torch.tensor(x["input_ids"], dtype=torch.long) for x in features], - batch_first=True, - padding_value=self.pad_token_id if self.pad_token_id is not None else 0, - ) - - labels = torch.nn.utils.rnn.pad_sequence( - [torch.tensor(x["labels"], dtype=torch.long) for x in features], - batch_first=True, - padding_value=IGNORE_INDEX, - ) - - attention_mask = None - if "attention_mask" in features[0]: - attention_mask = torch.nn.utils.rnn.pad_sequence( - [torch.tensor(x["attention_mask"], dtype=torch.long) for x in features], - batch_first=True, - padding_value=0, - ) - - # Handle position_ids - pad with sequential values for right padding, 0s for left padding - position_ids = None - if "position_ids" in features[0]: - if self.padding_side == "left": - # Likely not needed, but keeping for now - # For left padding, we'll pad with 0s using pad_sequence, then handle manually - position_ids = torch.nn.utils.rnn.pad_sequence( - [ - torch.tensor(x["position_ids"], dtype=torch.long) - for x in features - ], - batch_first=True, - padding_value=0, - ) - else: - # For right padding, continue the sequence - max_pos_len = max(len(f["position_ids"]) for f in features) - position_ids_list = [] - for f in features: - pos_seq = torch.tensor(f["position_ids"], dtype=torch.long) - if len(pos_seq) < max_pos_len: - # Continue the sequence - last_pos = pos_seq[-1].item() if len(pos_seq) > 0 else -1 - pad_len = max_pos_len - len(pos_seq) - pad_positions = torch.arange( - last_pos + 1, last_pos + 1 + pad_len, dtype=torch.long - ) - pos_seq = torch.cat([pos_seq, pad_positions]) - position_ids_list.append(pos_seq) - position_ids = torch.stack(position_ids_list) - - # Ensure all tensors have the same sequence length - # Check attention mask and position ids if they are present - tensor_lengths = [input_ids.size(1), labels.size(1)] - if attention_mask is not None: - tensor_lengths.append(attention_mask.size(1)) - if position_ids is not None: - tensor_lengths.append(position_ids.size(1)) - max_seq_len = max(tensor_lengths) - - # TODO: check if trimming is needed? and correct. - - if do_pad and target_length is not None: - max_seq_len = target_length - - # Pad all tensors to the same length - if input_ids.size(1) < max_seq_len: - pad_len = max_seq_len - input_ids.size(1) - if self.padding_side == "right": - input_ids = F.pad( - input_ids, - (0, pad_len), - value=self.pad_token_id if self.pad_token_id is not None else 0, - ) - else: - input_ids = F.pad( - input_ids, - (pad_len, 0), - value=self.pad_token_id if self.pad_token_id is not None else 0, - ) - elif input_ids.size(1) > max_seq_len: - input_ids = input_ids[:, :max_seq_len] - - if labels.size(1) < max_seq_len: - pad_len = max_seq_len - labels.size(1) - if self.padding_side == "right": - labels = F.pad(labels, (0, pad_len), value=IGNORE_INDEX) - else: - labels = F.pad(labels, (pad_len, 0), value=IGNORE_INDEX) - elif labels.size(1) > max_seq_len: - labels = labels[:, :max_seq_len] - - if attention_mask is not None: - if attention_mask.size(1) < max_seq_len: - pad_len = max_seq_len - attention_mask.size(1) - if self.padding_side == "right": - attention_mask = F.pad(attention_mask, (0, pad_len), value=0) - else: - attention_mask = F.pad(attention_mask, (pad_len, 0), value=0) - elif attention_mask.size(1) > max_seq_len: - attention_mask = attention_mask[:, :max_seq_len] - - if position_ids is not None: - if position_ids.size(1) < max_seq_len: - pad_len = max_seq_len - position_ids.size(1) - if self.padding_side == "right": - batch_size = position_ids.size(0) - new_position_ids = [] - for i in range(batch_size): - seq = position_ids[i] - if len(seq) > 0: - # get last position and pad with sequential values - last_pos = seq[-1].item() - pad_positions = torch.arange( - last_pos + 1, last_pos + 1 + pad_len, dtype=torch.long - ) - new_seq = torch.cat([seq, pad_positions]) - else: - new_seq = torch.arange(pad_len, dtype=torch.long) - new_position_ids.append(new_seq) - position_ids = torch.stack(new_position_ids) - else: - position_ids = F.pad(position_ids, (pad_len, 0), value=0) - elif position_ids.size(1) > max_seq_len: - position_ids = position_ids[:, :max_seq_len] - - final_batch = { - "input_ids": input_ids, - "labels": labels, - } - if attention_mask is not None: - final_batch["attention_mask"] = attention_mask - if position_ids is not None: - final_batch["position_ids"] = position_ids - - # Handle non-sequence fields (raise error) - sequence_fields = {"input_ids", "labels", "attention_mask", "position_ids"} - for f in features: - for key in f.keys(): - if key not in sequence_fields: - raise NotImplementedError( - f"Non-sequence field {key} not handled yet" - ) - - # Convert to requested tensor type - if return_tensors is None or return_tensors == "np": - result = {} - for k, v in final_batch.items(): - if isinstance(v, torch.Tensor): - result[k] = v.numpy().astype(np.int64) - else: - result[k] = v - return result - - if return_tensors == "pt": - return final_batch - - raise ValueError(f"Unsupported return_tensors='{return_tensors}'") - - def convert_ids_to_tokens(self, ids: list[int]) -> list[str]: - """ - Convert a list of token IDs to a list of tokens. - - Args: - ids: The list of token IDs to convert. - - Returns: - The list of tokens. - """ - return [ - self._mistral.instruct_tokenizer.tokenizer.id_to_piece(id) for id in ids - ] - - def __call__( - self, - text: str | list[str], - add_special_tokens: bool = True, - padding: bool | str = False, - truncation: bool = False, - max_length: int | None = None, - return_tensors: str | None = None, - **kwargs, - ) -> dict[str, list[int] | np.ndarray | Tensor]: - """ - Tokenize text and return a dictionary with input_ids and attention_mask. - - Args: - text: Input text string or list of strings to tokenize. - add_special_tokens: Whether to add special tokens (BOS/EOS). - padding: Whether to pad sequences. Can be True, False, "longest", or "max_length". - truncation: Whether to truncate sequences to max_length. - max_length: Maximum sequence length for truncation/padding. - return_tensors: Return format ("pt" for PyTorch, "np" for NumPy, None for lists). - - Returns: - Dictionary with "input_ids" and "attention_mask" keys. - """ - # if kwargs passed, raise error - if kwargs: - raise ValueError( - f"Unsupported kwargs: {kwargs}. Please create an issue on GitHub." - ) - - # `np` can work with inhomogeneous shapes but let's not support it until needed. - if ( - isinstance(text, list) - and len(text) > 1 - and return_tensors in ("pt", "np") - and padding is False - and truncation is False - ): - raise ValueError( - "return_tensors='pt' or 'np' requires padding or truncation." - ) - - # Handle single string input - if isinstance(text, str): - text = [text] - - # Encode all texts - # TODO: figure out how to parallelize this - batch_input_ids = [] - for single_text in text: - input_ids = self.encode(single_text, add_special_tokens=add_special_tokens) - - # Handle truncation - if truncation and max_length is not None and len(input_ids) > max_length: - input_ids = input_ids[:max_length] - - batch_input_ids.append(input_ids) - - # Create attention masks (1 for real tokens, 0 for padding) - attention_masks = [[1] * len(input_ids) for input_ids in batch_input_ids] - - # Handle padding - if padding in (True, "longest"): - # Pad to longest sequence in batch - max_len = max(len(input_ids) for input_ids in batch_input_ids) - - for i, input_ids in enumerate(batch_input_ids): - pad_length = max_len - len(input_ids) - if pad_length > 0: - if self.padding_side == "right": - batch_input_ids[i] = ( - input_ids + [self.pad_token_id] * pad_length - ) - attention_masks[i] = attention_masks[i] + [0] * pad_length - else: # left padding - batch_input_ids[i] = [ - self.pad_token_id - ] * pad_length + input_ids - attention_masks[i] = [0] * pad_length + attention_masks[i] - - elif padding == "max_length": - if max_length is None: - raise ValueError( - "max_length must be specified when padding='max_length'" - ) - - for i, input_ids in enumerate(batch_input_ids): - pad_length = max_length - len(input_ids) - if pad_length > 0: - if self.padding_side == "right": - batch_input_ids[i] = ( - input_ids + [self.pad_token_id] * pad_length - ) - attention_masks[i] = attention_masks[i] + [0] * pad_length - else: # left padding - batch_input_ids[i] = [ - self.pad_token_id - ] * pad_length + input_ids - attention_masks[i] = [0] * pad_length + attention_masks[i] - - # Prepare result - result = {} - - # Handle return tensor format - if return_tensors == "pt": - import torch - - result["input_ids"] = torch.tensor(batch_input_ids, dtype=torch.long) - result["attention_mask"] = torch.tensor(attention_masks, dtype=torch.long) - elif return_tensors == "np": - result["input_ids"] = np.array(batch_input_ids, dtype=np.int64) - result["attention_mask"] = np.array(attention_masks, dtype=np.int64) - elif return_tensors is None: - result["input_ids"] = batch_input_ids - result["attention_mask"] = attention_masks - else: - raise ValueError( - f"Unsupported return_tensors='{return_tensors}'. " - "Only 'pt' and 'np' are supported." - ) - - # If single input, return single sequences (not batched) - if len(text) == 1 and return_tensors is None: - result["input_ids"] = result["input_ids"][0] - result["attention_mask"] = result["attention_mask"][0] - - return result diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index a423135998..7f942e0ef4 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -158,7 +158,7 @@ def fixture_gemma2_tokenizer(): @pytest.fixture(name="magistral_tokenizer") def fixture_magistral_tokenizer(): - from axolotl.utils.mistral_tokenizer import HFMistralTokenizer + from axolotl.utils.mistral import HFMistralTokenizer tokenizer = HFMistralTokenizer.from_pretrained("mistralai/Magistral-Small-2506") return tokenizer @@ -166,7 +166,7 @@ def fixture_magistral_tokenizer(): @pytest.fixture(name="devstral_tokenizer") def fixture_devstral_tokenizer(): - from axolotl.utils.mistral_tokenizer import HFMistralTokenizer + from axolotl.utils.mistral import HFMistralTokenizer tokenizer = HFMistralTokenizer.from_pretrained("mistralai/Devstral-Small-2505") return tokenizer @@ -174,7 +174,7 @@ def fixture_devstral_tokenizer(): @pytest.fixture(name="devstral_1_1_tokenizer") def fixture_devstral_1_1_tokenizer(): - from axolotl.utils.mistral_tokenizer import HFMistralTokenizer + from axolotl.utils.mistral import HFMistralTokenizer tokenizer = HFMistralTokenizer.from_pretrained("mistralai/Devstral-Small-2507") return tokenizer diff --git a/tests/prompt_strategies/test_chat_templates_mistral.py b/tests/prompt_strategies/test_chat_templates_mistral.py index 8e3f494b16..a5b31a7712 100644 --- a/tests/prompt_strategies/test_chat_templates_mistral.py +++ b/tests/prompt_strategies/test_chat_templates_mistral.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: from transformers import PreTrainedTokenizer - from axolotl.utils.mistral_tokenizer import HFMistralTokenizer + from axolotl.utils.mistral import HFMistralTokenizer # fmt: off @@ -308,6 +308,7 @@ def test_mistral_chat_template( assert res == ["Hello", ",", " how", " are", " you", "?"] +@pytest.mark.skip(reason="TODO, fix for new HF wrapper call") def test_magistral_tokenizer_pad_method(magistral_tokenizer: "HFMistralTokenizer"): """Test the MistralTokenizer pad method""" from axolotl.utils.collators.core import IGNORE_INDEX @@ -750,6 +751,7 @@ def test_magistral_tool_calling(magistral_tokenizer: "HFMistralTokenizer"): assert "Not the same number of function calls and responses" in str(e) +@pytest.mark.skip(reason="TODO, fix for new HF wrapper call") def test_magistral_tokenizer_call_method( magistral_tokenizer: "HFMistralTokenizer", llama3_tokenizer: "PreTrainedTokenizer" ): From 2eb7ff95af21eff41ec5a421cb9319daa5d987bb Mon Sep 17 00:00:00 2001 From: Vincenzo di Cicco <112694549+v-dicicco@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:38:13 +0200 Subject: [PATCH 0859/1405] Use '<|finetune_right_pad|>' as padding token for LLama4 (#2988) [skip ci] --- examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml | 2 +- examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml | 2 +- examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml | 2 +- examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml | 2 +- examples/llama-4/scout-qlora-flexattn-fsdp2.yaml | 2 +- examples/llama-4/scout-qlora-single-h100-flex.yaml | 2 +- examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml b/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml index 3bd05b5ba3..5770ce9472 100644 --- a/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml +++ b/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml @@ -84,7 +84,7 @@ fsdp_config: fsdp_state_dict_type: FULL_STATE_DICT fsdp_sharding_strategy: FULL_SHARD special_tokens: - pad_token: <|finetune_right_pad_id|> + pad_token: <|finetune_right_pad|> eos_token: <|eot|> # save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml b/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml index 1c6ba1410e..7cd8032d22 100644 --- a/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml @@ -88,7 +88,7 @@ fsdp_config: fsdp_sharding_strategy: FULL_SHARD fsdp_activation_checkpointing: true special_tokens: - pad_token: <|finetune_right_pad_id|> + pad_token: <|finetune_right_pad|> eos_token: <|eot|> # save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml b/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml index 0810895553..03acdc2345 100644 --- a/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml @@ -81,7 +81,7 @@ evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: - pad_token: <|finetune_right_pad_id|> + pad_token: <|finetune_right_pad|> eos_token: <|eot|> # save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml b/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml index 390be5af78..d9255ea162 100644 --- a/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml @@ -84,7 +84,7 @@ fsdp_config: fsdp_sharding_strategy: FULL_SHARD fsdp_activation_checkpointing: true special_tokens: - pad_token: <|finetune_right_pad_id|> + pad_token: <|finetune_right_pad|> eos_token: <|eot|> # save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml index 6193e4ed50..4cda4949ed 100644 --- a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml +++ b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml @@ -82,7 +82,7 @@ fsdp_config: fsdp_reshard_after_forward: true fsdp_activation_checkpointing: true special_tokens: - pad_token: <|finetune_right_pad_id|> + pad_token: <|finetune_right_pad|> eos_token: <|eot|> # save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/scout-qlora-single-h100-flex.yaml b/examples/llama-4/scout-qlora-single-h100-flex.yaml index c3bbfe56aa..518cfa57cd 100644 --- a/examples/llama-4/scout-qlora-single-h100-flex.yaml +++ b/examples/llama-4/scout-qlora-single-h100-flex.yaml @@ -80,7 +80,7 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: - pad_token: <|finetune_right_pad_id|> + pad_token: <|finetune_right_pad|> eos_token: <|eot|> # save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml b/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml index a67936cf18..28da15084e 100644 --- a/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml +++ b/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml @@ -85,7 +85,7 @@ fsdp_config: fsdp_reshard_after_forward: true fsdp_activation_checkpointing: true special_tokens: - pad_token: <|finetune_right_pad_id|> + pad_token: <|finetune_right_pad|> eos_token: <|eot|> # save_first_step: true # uncomment this to validate checkpoint saving works with your config From 22810c97b72a397d877e27fe9b75b3be10920e27 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 30 Jul 2025 06:44:06 -0400 Subject: [PATCH 0860/1405] use warmup_ratio as a better default than warmup steps since it's data dependent (#2897) [skip ci] * use warmup_ratio as a better default than warmup steps since it's data dependent * replace remainder of warmup_steps --- examples/archived/cerebras/btlm-ft.yml | 2 +- examples/archived/cerebras/qlora.yml | 2 +- examples/archived/code-llama/13b/lora.yml | 2 +- examples/archived/code-llama/13b/qlora.yml | 2 +- examples/archived/code-llama/34b/lora.yml | 2 +- examples/archived/code-llama/34b/qlora.yml | 2 +- examples/archived/code-llama/7b/lora.yml | 2 +- examples/archived/code-llama/7b/qlora.yml | 2 +- examples/archived/dbrx/16bit-lora.yaml | 2 +- examples/archived/dbrx/8bit-lora.yaml | 2 +- examples/archived/dbrx/fft-ds-zero3.yaml | 2 +- examples/archived/deepcoder/deepcoder-14B-preview-lora.yml | 2 +- examples/archived/falcon/config-7b-lora.yml | 2 +- examples/archived/falcon/config-7b-qlora.yml | 2 +- examples/archived/falcon/config-7b.yml | 2 +- examples/archived/gptj/qlora.yml | 2 +- examples/archived/jeopardy-bot/config.yml | 2 +- examples/archived/mpt-7b/config.yml | 2 +- examples/archived/openllama-3b/config.yml | 2 +- examples/archived/openllama-3b/lora.yml | 2 +- examples/archived/openllama-3b/qlora.yml | 2 +- examples/archived/qwen/lora.yml | 2 +- examples/archived/qwen/qlora.yml | 2 +- examples/archived/qwen/qwen2-moe-lora.yaml | 2 +- examples/archived/qwen/qwen2-moe-qlora.yaml | 2 +- examples/archived/redpajama/config-3b.yml | 2 +- examples/archived/replit-3b/config-lora.yml | 2 +- examples/archived/stablelm-2/1.6b/fft.yml | 2 +- examples/archived/stablelm-2/1.6b/lora.yml | 2 +- examples/archived/starcoder2/qlora.yml | 2 +- examples/archived/tiny-llama/lora-mps.yml | 2 +- examples/archived/tiny-llama/lora.yml | 2 +- examples/archived/tiny-llama/pretrain.yml | 2 +- examples/archived/tiny-llama/qlora.yml | 2 +- examples/archived/xgen-7b/xgen-7b-8k-qlora.yml | 2 +- examples/archived/yi-34B-chat/qlora.yml | 2 +- examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml | 2 +- examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml | 2 +- examples/deepseek-v2/fft-fsdp-16b.yaml | 2 +- examples/deepseek-v2/qlora-fsdp-2_5.yaml | 2 +- examples/glm4/qlora-32b.yaml | 2 +- examples/jamba/qlora.yaml | 2 +- examples/jamba/qlora_deepspeed.yaml | 2 +- examples/jamba/qlora_fsdp_large.yaml | 2 +- examples/llama-2/fft_optimized.yml | 2 +- examples/llama-2/gptq-lora.yml | 2 +- examples/llama-2/lisa.yml | 2 +- examples/llama-2/loftq.yml | 2 +- examples/llama-2/lora.yml | 2 +- examples/llama-2/qlora-fsdp.yml | 2 +- examples/llama-2/qlora.yml | 2 +- examples/llama-2/relora.yml | 4 ++-- examples/llama-3/3b-qat-fsdp2.yaml | 2 +- examples/llama-3/fft-8b-liger-fsdp.yaml | 2 +- examples/llama-3/fft-8b.yaml | 2 +- examples/llama-3/instruct-dpo-lora-8b.yml | 2 +- examples/llama-3/instruct-lora-8b.yml | 2 +- examples/llama-3/lora-1b-deduplicate-dpo.yml | 2 +- examples/llama-3/lora-1b-deduplicate-sft.yml | 2 +- examples/llama-3/lora-1b-kernels.yml | 2 +- examples/llama-3/lora-1b-ray.yml | 2 +- examples/llama-3/lora-1b-sample-packing-sequentially.yml | 2 +- examples/llama-3/lora-1b.yml | 2 +- examples/llama-3/lora-8b.yml | 2 +- examples/llama-3/qlora-1b-kto.yaml | 2 +- examples/llama-3/qlora-1b.yml | 2 +- examples/llama-3/qlora-fsdp-405b.yaml | 2 +- examples/llama-3/qlora-fsdp-70b.yaml | 2 +- examples/llama-3/qlora.yml | 2 +- examples/llama-3/sparse-finetuning.yaml | 2 +- examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml | 2 +- examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml | 2 +- examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml | 2 +- examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml | 2 +- examples/llama-4/scout-qlora-flexattn-fsdp2.yaml | 2 +- examples/llama-4/scout-qlora-single-h100-flex.yaml | 2 +- examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml | 2 +- examples/mamba/config.yml | 2 +- examples/mistral/config.yml | 2 +- examples/mistral/lora-mps.yml | 2 +- examples/mistral/lora.yml | 2 +- examples/mistral/mistral-dpo-qlora.yml | 2 +- examples/mistral/mistral-qlora-fsdp.yml | 2 +- examples/mistral/mistral-qlora-orpo.yml | 2 +- examples/mistral/mixtral-8x22b-qlora-fsdp.yml | 2 +- examples/mistral/mixtral-qlora-fsdp.yml | 2 +- examples/mistral/mixtral.yml | 2 +- examples/mistral/qlora.yml | 2 +- examples/orpheus/finetune.yml | 2 +- examples/phi/lora-3.5.yaml | 2 +- examples/phi/phi-ft.yml | 2 +- examples/phi/phi-qlora.yml | 2 +- examples/phi/phi2-ft.yml | 2 +- examples/phi/phi3-ft-fsdp.yml | 2 +- examples/qwen2/dpo.yaml | 2 +- examples/qwen2/qlora-fsdp.yaml | 2 +- examples/qwen3/32b-qlora.yaml | 2 +- examples/qwen3/8b-qat-fsdp2.yml | 2 +- examples/qwen3/qlora-fsdp.yaml | 2 +- 99 files changed, 100 insertions(+), 100 deletions(-) diff --git a/examples/archived/cerebras/btlm-ft.yml b/examples/archived/cerebras/btlm-ft.yml index c9878779d1..c3495d2874 100644 --- a/examples/archived/cerebras/btlm-ft.yml +++ b/examples/archived/cerebras/btlm-ft.yml @@ -66,7 +66,7 @@ flash_optimum: gptq_groupsize: gptq_model_v1: -warmup_steps: 32 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 save_total_limit: diff --git a/examples/archived/cerebras/qlora.yml b/examples/archived/cerebras/qlora.yml index 55cc597f17..4598a8338e 100644 --- a/examples/archived/cerebras/qlora.yml +++ b/examples/archived/cerebras/qlora.yml @@ -43,7 +43,7 @@ xformers_attention: true flash_attention: gptq_groupsize: gptq_model_v1: -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.1 diff --git a/examples/archived/code-llama/13b/lora.yml b/examples/archived/code-llama/13b/lora.yml index 98ef516abe..ace94b6196 100644 --- a/examples/archived/code-llama/13b/lora.yml +++ b/examples/archived/code-llama/13b/lora.yml @@ -47,7 +47,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/code-llama/13b/qlora.yml b/examples/archived/code-llama/13b/qlora.yml index 2385368ace..f4ed17af53 100644 --- a/examples/archived/code-llama/13b/qlora.yml +++ b/examples/archived/code-llama/13b/qlora.yml @@ -48,7 +48,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/code-llama/34b/lora.yml b/examples/archived/code-llama/34b/lora.yml index fb44997ff9..0a1d714678 100644 --- a/examples/archived/code-llama/34b/lora.yml +++ b/examples/archived/code-llama/34b/lora.yml @@ -47,7 +47,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/code-llama/34b/qlora.yml b/examples/archived/code-llama/34b/qlora.yml index 22f4cae3c7..ec17bf2008 100644 --- a/examples/archived/code-llama/34b/qlora.yml +++ b/examples/archived/code-llama/34b/qlora.yml @@ -48,7 +48,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/code-llama/7b/lora.yml b/examples/archived/code-llama/7b/lora.yml index 0632bdfb79..174c17d2cd 100644 --- a/examples/archived/code-llama/7b/lora.yml +++ b/examples/archived/code-llama/7b/lora.yml @@ -47,7 +47,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/code-llama/7b/qlora.yml b/examples/archived/code-llama/7b/qlora.yml index 0bd462aabe..08e67d8c2d 100644 --- a/examples/archived/code-llama/7b/qlora.yml +++ b/examples/archived/code-llama/7b/qlora.yml @@ -48,7 +48,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/dbrx/16bit-lora.yaml b/examples/archived/dbrx/16bit-lora.yaml index 852654d496..05946dfe96 100644 --- a/examples/archived/dbrx/16bit-lora.yaml +++ b/examples/archived/dbrx/16bit-lora.yaml @@ -54,7 +54,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: saves_per_epoch: 1 diff --git a/examples/archived/dbrx/8bit-lora.yaml b/examples/archived/dbrx/8bit-lora.yaml index 0b9402194f..f159bf7fab 100644 --- a/examples/archived/dbrx/8bit-lora.yaml +++ b/examples/archived/dbrx/8bit-lora.yaml @@ -57,7 +57,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: saves_per_epoch: 1 diff --git a/examples/archived/dbrx/fft-ds-zero3.yaml b/examples/archived/dbrx/fft-ds-zero3.yaml index e42c166734..13cd0d9977 100644 --- a/examples/archived/dbrx/fft-ds-zero3.yaml +++ b/examples/archived/dbrx/fft-ds-zero3.yaml @@ -41,7 +41,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: saves_per_epoch: 1 diff --git a/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml b/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml index a9511e9e3a..2202091d58 100644 --- a/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml +++ b/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml @@ -51,7 +51,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/falcon/config-7b-lora.yml b/examples/archived/falcon/config-7b-lora.yml index 391d4dd94f..f4fedbedec 100644 --- a/examples/archived/falcon/config-7b-lora.yml +++ b/examples/archived/falcon/config-7b-lora.yml @@ -47,7 +47,7 @@ xformers_attention: true flash_attention: gptq_groupsize: gptq_model_v1: -warmup_steps: 40 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/falcon/config-7b-qlora.yml b/examples/archived/falcon/config-7b-qlora.yml index a9af8574c3..a44cc40a6b 100644 --- a/examples/archived/falcon/config-7b-qlora.yml +++ b/examples/archived/falcon/config-7b-qlora.yml @@ -77,7 +77,7 @@ xformers_attention: true flash_attention: gptq_groupsize: gptq_model_v1: -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.000001 diff --git a/examples/archived/falcon/config-7b.yml b/examples/archived/falcon/config-7b.yml index 3cc553daab..5481fb2362 100644 --- a/examples/archived/falcon/config-7b.yml +++ b/examples/archived/falcon/config-7b.yml @@ -44,7 +44,7 @@ xformers_attention: true flash_attention: gptq_groupsize: gptq_model_v1: -warmup_steps: 40 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/gptj/qlora.yml b/examples/archived/gptj/qlora.yml index c3cf9f9738..6348566c2f 100644 --- a/examples/archived/gptj/qlora.yml +++ b/examples/archived/gptj/qlora.yml @@ -40,7 +40,7 @@ xformers_attention: true flash_attention: gptq_groupsize: gptq_model_v1: -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.1 diff --git a/examples/archived/jeopardy-bot/config.yml b/examples/archived/jeopardy-bot/config.yml index 3609bd97eb..ab1d197847 100644 --- a/examples/archived/jeopardy-bot/config.yml +++ b/examples/archived/jeopardy-bot/config.yml @@ -41,7 +41,7 @@ xformers_attention: true flash_attention: gptq_groupsize: gptq_model_v1: -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.1 diff --git a/examples/archived/mpt-7b/config.yml b/examples/archived/mpt-7b/config.yml index e7485fad76..1fff51b6ec 100644 --- a/examples/archived/mpt-7b/config.yml +++ b/examples/archived/mpt-7b/config.yml @@ -42,7 +42,7 @@ logging_steps: 5 flash_attention: gptq_groupsize: gptq_model_v1: -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0001 diff --git a/examples/archived/openllama-3b/config.yml b/examples/archived/openllama-3b/config.yml index 17eeb73aec..63056ed6d5 100644 --- a/examples/archived/openllama-3b/config.yml +++ b/examples/archived/openllama-3b/config.yml @@ -42,7 +42,7 @@ logging_steps: 1 flash_attention: true gptq_groupsize: gptq_model_v1: -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.1 diff --git a/examples/archived/openllama-3b/lora.yml b/examples/archived/openllama-3b/lora.yml index 073117f114..b70821ce27 100644 --- a/examples/archived/openllama-3b/lora.yml +++ b/examples/archived/openllama-3b/lora.yml @@ -50,7 +50,7 @@ logging_steps: 1 flash_attention: true gptq_groupsize: gptq_model_v1: -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.1 diff --git a/examples/archived/openllama-3b/qlora.yml b/examples/archived/openllama-3b/qlora.yml index b4fca2c073..a34f2964b8 100644 --- a/examples/archived/openllama-3b/qlora.yml +++ b/examples/archived/openllama-3b/qlora.yml @@ -43,7 +43,7 @@ logging_steps: 1 flash_attention: true gptq_groupsize: gptq_model_v1: -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.1 diff --git a/examples/archived/qwen/lora.yml b/examples/archived/qwen/lora.yml index 9a28432360..29de256118 100644 --- a/examples/archived/qwen/lora.yml +++ b/examples/archived/qwen/lora.yml @@ -49,7 +49,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/qwen/qlora.yml b/examples/archived/qwen/qlora.yml index 5f85b44dd1..d466694449 100644 --- a/examples/archived/qwen/qlora.yml +++ b/examples/archived/qwen/qlora.yml @@ -49,7 +49,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/qwen/qwen2-moe-lora.yaml b/examples/archived/qwen/qwen2-moe-lora.yaml index afce443a01..1d5e1b5247 100644 --- a/examples/archived/qwen/qwen2-moe-lora.yaml +++ b/examples/archived/qwen/qwen2-moe-lora.yaml @@ -45,7 +45,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/qwen/qwen2-moe-qlora.yaml b/examples/archived/qwen/qwen2-moe-qlora.yaml index 92a6842cfc..08731441b9 100644 --- a/examples/archived/qwen/qwen2-moe-qlora.yaml +++ b/examples/archived/qwen/qwen2-moe-qlora.yaml @@ -48,7 +48,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/redpajama/config-3b.yml b/examples/archived/redpajama/config-3b.yml index 3e2999df96..c5b229c3d8 100644 --- a/examples/archived/redpajama/config-3b.yml +++ b/examples/archived/redpajama/config-3b.yml @@ -43,7 +43,7 @@ logging_steps: 5 flash_attention: gptq_groupsize: gptq_model_v1: -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0001 diff --git a/examples/archived/replit-3b/config-lora.yml b/examples/archived/replit-3b/config-lora.yml index 5a02ba10ce..d8561762c6 100644 --- a/examples/archived/replit-3b/config-lora.yml +++ b/examples/archived/replit-3b/config-lora.yml @@ -41,7 +41,7 @@ logging_steps: 1 flash_attention: gptq_groupsize: gptq_model_v1: -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0 diff --git a/examples/archived/stablelm-2/1.6b/fft.yml b/examples/archived/stablelm-2/1.6b/fft.yml index 3ae08c9de0..d608bc66f8 100644 --- a/examples/archived/stablelm-2/1.6b/fft.yml +++ b/examples/archived/stablelm-2/1.6b/fft.yml @@ -50,7 +50,7 @@ flash_attn_rms_norm: true flash_attn_fuse_qkv: false flash_attn_fuse_mlp: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 diff --git a/examples/archived/stablelm-2/1.6b/lora.yml b/examples/archived/stablelm-2/1.6b/lora.yml index e5aa814239..6d358bdd88 100644 --- a/examples/archived/stablelm-2/1.6b/lora.yml +++ b/examples/archived/stablelm-2/1.6b/lora.yml @@ -51,7 +51,7 @@ flash_attention: true flash_attn_cross_entropy: false flash_attn_rms_norm: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/starcoder2/qlora.yml b/examples/archived/starcoder2/qlora.yml index 889d837e89..fecf98d23f 100644 --- a/examples/archived/starcoder2/qlora.yml +++ b/examples/archived/starcoder2/qlora.yml @@ -48,7 +48,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 4 eval_steps: saves_per_epoch: 4 diff --git a/examples/archived/tiny-llama/lora-mps.yml b/examples/archived/tiny-llama/lora-mps.yml index aa3b7d8510..125090a78e 100644 --- a/examples/archived/tiny-llama/lora-mps.yml +++ b/examples/archived/tiny-llama/lora-mps.yml @@ -49,7 +49,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: false -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 0 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/tiny-llama/lora.yml b/examples/archived/tiny-llama/lora.yml index a92f4bd677..817481e186 100644 --- a/examples/archived/tiny-llama/lora.yml +++ b/examples/archived/tiny-llama/lora.yml @@ -47,7 +47,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/tiny-llama/pretrain.yml b/examples/archived/tiny-llama/pretrain.yml index 5b3706bcbe..f15c6ce19e 100644 --- a/examples/archived/tiny-llama/pretrain.yml +++ b/examples/archived/tiny-llama/pretrain.yml @@ -38,7 +38,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/tiny-llama/qlora.yml b/examples/archived/tiny-llama/qlora.yml index 4d422a5ee5..d3ff59cb81 100644 --- a/examples/archived/tiny-llama/qlora.yml +++ b/examples/archived/tiny-llama/qlora.yml @@ -49,7 +49,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/xgen-7b/xgen-7b-8k-qlora.yml b/examples/archived/xgen-7b/xgen-7b-8k-qlora.yml index 48066b1304..fc09a1e7bd 100644 --- a/examples/archived/xgen-7b/xgen-7b-8k-qlora.yml +++ b/examples/archived/xgen-7b/xgen-7b-8k-qlora.yml @@ -75,7 +75,7 @@ xformers_attention: true flash_attention: gptq_groupsize: gptq_model_v1: -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/archived/yi-34B-chat/qlora.yml b/examples/archived/yi-34B-chat/qlora.yml index a0a95d86fe..ba8d12fc89 100644 --- a/examples/archived/yi-34B-chat/qlora.yml +++ b/examples/archived/yi-34B-chat/qlora.yml @@ -20,7 +20,7 @@ special_tokens: datasets: - path: mhenrichsen/alpaca_2k_test type: alpaca -warmup_steps: 10 +warmup_ratio: 0.1 # Iterations num_epochs: 1 diff --git a/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml b/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml index 6f0b505bd7..fc9a75e3f8 100644 --- a/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml +++ b/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml @@ -51,7 +51,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml b/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml index fefcfadeaf..b527edc6ff 100644 --- a/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml +++ b/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml @@ -51,7 +51,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/deepseek-v2/fft-fsdp-16b.yaml b/examples/deepseek-v2/fft-fsdp-16b.yaml index d23c789aa4..6e936da163 100644 --- a/examples/deepseek-v2/fft-fsdp-16b.yaml +++ b/examples/deepseek-v2/fft-fsdp-16b.yaml @@ -37,7 +37,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 2 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/deepseek-v2/qlora-fsdp-2_5.yaml b/examples/deepseek-v2/qlora-fsdp-2_5.yaml index 0536d1c101..aab5034a09 100644 --- a/examples/deepseek-v2/qlora-fsdp-2_5.yaml +++ b/examples/deepseek-v2/qlora-fsdp-2_5.yaml @@ -61,7 +61,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 2 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/glm4/qlora-32b.yaml b/examples/glm4/qlora-32b.yaml index b3656e3aea..832abde053 100644 --- a/examples/glm4/qlora-32b.yaml +++ b/examples/glm4/qlora-32b.yaml @@ -55,7 +55,7 @@ flash_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/jamba/qlora.yaml b/examples/jamba/qlora.yaml index 494154886b..538ed3a106 100644 --- a/examples/jamba/qlora.yaml +++ b/examples/jamba/qlora.yaml @@ -49,7 +49,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/jamba/qlora_deepspeed.yaml b/examples/jamba/qlora_deepspeed.yaml index 64db8f2ff7..b288635e7c 100644 --- a/examples/jamba/qlora_deepspeed.yaml +++ b/examples/jamba/qlora_deepspeed.yaml @@ -48,7 +48,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: saves_per_epoch: 1 diff --git a/examples/jamba/qlora_fsdp_large.yaml b/examples/jamba/qlora_fsdp_large.yaml index 344f73e63c..150e5e2ecf 100644 --- a/examples/jamba/qlora_fsdp_large.yaml +++ b/examples/jamba/qlora_fsdp_large.yaml @@ -47,7 +47,7 @@ gradient_checkpointing_kwargs: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-2/fft_optimized.yml b/examples/llama-2/fft_optimized.yml index a23778b96c..6788064733 100644 --- a/examples/llama-2/fft_optimized.yml +++ b/examples/llama-2/fft_optimized.yml @@ -48,7 +48,7 @@ flash_attn_rms_norm: true flash_attn_fuse_qkv: false flash_attn_fuse_mlp: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 diff --git a/examples/llama-2/gptq-lora.yml b/examples/llama-2/gptq-lora.yml index 580fabdf8d..de1caaa059 100644 --- a/examples/llama-2/gptq-lora.yml +++ b/examples/llama-2/gptq-lora.yml @@ -56,7 +56,7 @@ logging_steps: 1 flash_attention: sdp_attention: flash_optimum: -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.1 diff --git a/examples/llama-2/lisa.yml b/examples/llama-2/lisa.yml index 25adcad5d2..7b92b72e1b 100644 --- a/examples/llama-2/lisa.yml +++ b/examples/llama-2/lisa.yml @@ -52,7 +52,7 @@ flash_attn_rms_norm: true flash_attn_fuse_qkv: false flash_attn_fuse_mlp: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.1 diff --git a/examples/llama-2/loftq.yml b/examples/llama-2/loftq.yml index 606bbc7353..619e5bcce2 100644 --- a/examples/llama-2/loftq.yml +++ b/examples/llama-2/loftq.yml @@ -47,7 +47,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-2/lora.yml b/examples/llama-2/lora.yml index 0781e0d1b4..0a677f11ac 100644 --- a/examples/llama-2/lora.yml +++ b/examples/llama-2/lora.yml @@ -47,7 +47,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-2/qlora-fsdp.yml b/examples/llama-2/qlora-fsdp.yml index ceb3ce5d19..54f4b86b45 100644 --- a/examples/llama-2/qlora-fsdp.yml +++ b/examples/llama-2/qlora-fsdp.yml @@ -50,7 +50,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-2/qlora.yml b/examples/llama-2/qlora.yml index 1515872e6c..327d88c15f 100644 --- a/examples/llama-2/qlora.yml +++ b/examples/llama-2/qlora.yml @@ -48,7 +48,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-2/relora.yml b/examples/llama-2/relora.yml index 6c9e83223e..b0e9053407 100644 --- a/examples/llama-2/relora.yml +++ b/examples/llama-2/relora.yml @@ -26,7 +26,7 @@ lora_dropout: 0.05 lora_target_linear: true relora_steps: 150 -relora_warmup_steps: 10 +relora_warmup_ratio: 0.1 relora_cpu_offload: false wandb_project: @@ -50,7 +50,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/3b-qat-fsdp2.yaml b/examples/llama-3/3b-qat-fsdp2.yaml index d9b96fb96d..35e3461e2a 100644 --- a/examples/llama-3/3b-qat-fsdp2.yaml +++ b/examples/llama-3/3b-qat-fsdp2.yaml @@ -58,7 +58,7 @@ logging_steps: 1 evals_per_epoch: 1 saves_per_epoch: 1 -warmup_steps: 10 +warmup_ratio: 0.1 weight_decay: 0.0 fsdp: - full_shard diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml index b3d990a8bb..a655b97a9c 100644 --- a/examples/llama-3/fft-8b-liger-fsdp.yaml +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -51,7 +51,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 2 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/fft-8b.yaml b/examples/llama-3/fft-8b.yaml index e067212b75..c72ec6662b 100644 --- a/examples/llama-3/fft-8b.yaml +++ b/examples/llama-3/fft-8b.yaml @@ -36,7 +36,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 2 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/instruct-dpo-lora-8b.yml b/examples/llama-3/instruct-dpo-lora-8b.yml index 99de56ad31..cf823353b3 100644 --- a/examples/llama-3/instruct-dpo-lora-8b.yml +++ b/examples/llama-3/instruct-dpo-lora-8b.yml @@ -67,7 +67,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml index b8baa5b0aa..69e17b9cfc 100644 --- a/examples/llama-3/instruct-lora-8b.yml +++ b/examples/llama-3/instruct-lora-8b.yml @@ -58,7 +58,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/lora-1b-deduplicate-dpo.yml b/examples/llama-3/lora-1b-deduplicate-dpo.yml index 288e8fd19f..2897636f4b 100644 --- a/examples/llama-3/lora-1b-deduplicate-dpo.yml +++ b/examples/llama-3/lora-1b-deduplicate-dpo.yml @@ -79,7 +79,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/lora-1b-deduplicate-sft.yml b/examples/llama-3/lora-1b-deduplicate-sft.yml index 6ce504a0d4..c5190d892b 100644 --- a/examples/llama-3/lora-1b-deduplicate-sft.yml +++ b/examples/llama-3/lora-1b-deduplicate-sft.yml @@ -55,7 +55,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/lora-1b-kernels.yml b/examples/llama-3/lora-1b-kernels.yml index 71e569ae01..0bcf46b174 100644 --- a/examples/llama-3/lora-1b-kernels.yml +++ b/examples/llama-3/lora-1b-kernels.yml @@ -59,7 +59,7 @@ flash_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/lora-1b-ray.yml b/examples/llama-3/lora-1b-ray.yml index 7b9d157411..46c83348e6 100644 --- a/examples/llama-3/lora-1b-ray.yml +++ b/examples/llama-3/lora-1b-ray.yml @@ -53,7 +53,7 @@ flash_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 diff --git a/examples/llama-3/lora-1b-sample-packing-sequentially.yml b/examples/llama-3/lora-1b-sample-packing-sequentially.yml index 9f764e1311..dba78597bf 100644 --- a/examples/llama-3/lora-1b-sample-packing-sequentially.yml +++ b/examples/llama-3/lora-1b-sample-packing-sequentially.yml @@ -57,7 +57,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/lora-1b.yml b/examples/llama-3/lora-1b.yml index 34d540eb7f..2ae2f00562 100644 --- a/examples/llama-3/lora-1b.yml +++ b/examples/llama-3/lora-1b.yml @@ -54,7 +54,7 @@ flash_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/lora-8b.yml b/examples/llama-3/lora-8b.yml index ca6cd9e974..d72b6527d4 100644 --- a/examples/llama-3/lora-8b.yml +++ b/examples/llama-3/lora-8b.yml @@ -51,7 +51,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/qlora-1b-kto.yaml b/examples/llama-3/qlora-1b-kto.yaml index f156e23d36..a6a84e7b18 100644 --- a/examples/llama-3/qlora-1b-kto.yaml +++ b/examples/llama-3/qlora-1b-kto.yaml @@ -55,7 +55,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/qlora-1b.yml b/examples/llama-3/qlora-1b.yml index 288b7dc6ce..1e4f97438a 100644 --- a/examples/llama-3/qlora-1b.yml +++ b/examples/llama-3/qlora-1b.yml @@ -56,7 +56,7 @@ flash_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/qlora-fsdp-405b.yaml b/examples/llama-3/qlora-fsdp-405b.yaml index 0f31b5bdcc..8ddb84d652 100644 --- a/examples/llama-3/qlora-fsdp-405b.yaml +++ b/examples/llama-3/qlora-fsdp-405b.yaml @@ -41,7 +41,7 @@ gradient_checkpointing_kwargs: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/qlora-fsdp-70b.yaml b/examples/llama-3/qlora-fsdp-70b.yaml index 28387ba1bd..c052bc19d6 100644 --- a/examples/llama-3/qlora-fsdp-70b.yaml +++ b/examples/llama-3/qlora-fsdp-70b.yaml @@ -50,7 +50,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/qlora.yml b/examples/llama-3/qlora.yml index ffb00dace9..a8f47a0e25 100644 --- a/examples/llama-3/qlora.yml +++ b/examples/llama-3/qlora.yml @@ -48,7 +48,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-3/sparse-finetuning.yaml b/examples/llama-3/sparse-finetuning.yaml index ecf5df955e..348756b700 100644 --- a/examples/llama-3/sparse-finetuning.yaml +++ b/examples/llama-3/sparse-finetuning.yaml @@ -47,7 +47,7 @@ logging_steps: 1 xformers_attention: flash_attention: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 2 eval_table_size: saves_per_epoch: 1 diff --git a/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml b/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml index 5770ce9472..b20f79758b 100644 --- a/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml +++ b/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml @@ -66,7 +66,7 @@ gradient_checkpointing: offload gradient_checkpointing_kwargs: use_reentrant: false -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml b/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml index 7cd8032d22..40449009c8 100644 --- a/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml @@ -69,7 +69,7 @@ tf32: true logging_steps: 1 flash_attention: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml b/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml index 03acdc2345..abdc51378e 100644 --- a/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml @@ -76,7 +76,7 @@ gradient_checkpointing: offload gradient_checkpointing_kwargs: use_reentrant: false -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml b/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml index d9255ea162..9975949bb4 100644 --- a/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml @@ -65,7 +65,7 @@ tf32: true logging_steps: 1 flash_attention: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml index 4cda4949ed..02c04c6910 100644 --- a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml +++ b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml @@ -64,7 +64,7 @@ flex_attn_compile_kwargs: dynamic: false mode: max-autotune-no-cudagraphs -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/llama-4/scout-qlora-single-h100-flex.yaml b/examples/llama-4/scout-qlora-single-h100-flex.yaml index 518cfa57cd..33a6911897 100644 --- a/examples/llama-4/scout-qlora-single-h100-flex.yaml +++ b/examples/llama-4/scout-qlora-single-h100-flex.yaml @@ -74,7 +74,7 @@ gradient_checkpointing_kwargs: use_reentrant: false logging_steps: 1 -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 diff --git a/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml b/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml index 28da15084e..ac7e05659f 100644 --- a/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml +++ b/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml @@ -67,7 +67,7 @@ flex_attn_compile_kwargs: dynamic: false mode: max-autotune-no-cudagraphs -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 1 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/mamba/config.yml b/examples/mamba/config.yml index 2261bd2156..e6b3358042 100644 --- a/examples/mamba/config.yml +++ b/examples/mamba/config.yml @@ -41,7 +41,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/mistral/config.yml b/examples/mistral/config.yml index 455c3c224c..e741625371 100644 --- a/examples/mistral/config.yml +++ b/examples/mistral/config.yml @@ -38,7 +38,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/mistral/lora-mps.yml b/examples/mistral/lora-mps.yml index c18d10aeee..07ce191dc4 100644 --- a/examples/mistral/lora-mps.yml +++ b/examples/mistral/lora-mps.yml @@ -59,7 +59,7 @@ sdp_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/mistral/lora.yml b/examples/mistral/lora.yml index 77a87a1da4..757287f19a 100644 --- a/examples/mistral/lora.yml +++ b/examples/mistral/lora.yml @@ -59,7 +59,7 @@ flash_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/mistral/mistral-dpo-qlora.yml b/examples/mistral/mistral-dpo-qlora.yml index 49f5e4ede6..8fea14a0f7 100644 --- a/examples/mistral/mistral-dpo-qlora.yml +++ b/examples/mistral/mistral-dpo-qlora.yml @@ -73,7 +73,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: false -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/mistral/mistral-qlora-fsdp.yml b/examples/mistral/mistral-qlora-fsdp.yml index cec958c54c..8e1f03d248 100644 --- a/examples/mistral/mistral-qlora-fsdp.yml +++ b/examples/mistral/mistral-qlora-fsdp.yml @@ -56,7 +56,7 @@ flash_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 diff --git a/examples/mistral/mistral-qlora-orpo.yml b/examples/mistral/mistral-qlora-orpo.yml index ea3e112b92..850d286f3f 100644 --- a/examples/mistral/mistral-qlora-orpo.yml +++ b/examples/mistral/mistral-qlora-orpo.yml @@ -64,7 +64,7 @@ flash_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml index 64ef9930ce..dc7bd9c377 100644 --- a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml +++ b/examples/mistral/mixtral-8x22b-qlora-fsdp.yml @@ -54,7 +54,7 @@ flash_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 diff --git a/examples/mistral/mixtral-qlora-fsdp.yml b/examples/mistral/mixtral-qlora-fsdp.yml index c8d0a2711b..5151e12921 100644 --- a/examples/mistral/mixtral-qlora-fsdp.yml +++ b/examples/mistral/mixtral-qlora-fsdp.yml @@ -56,7 +56,7 @@ flash_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 diff --git a/examples/mistral/mixtral.yml b/examples/mistral/mixtral.yml index 933275484b..d1981a699a 100644 --- a/examples/mistral/mixtral.yml +++ b/examples/mistral/mixtral.yml @@ -74,7 +74,7 @@ flash_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 diff --git a/examples/mistral/qlora.yml b/examples/mistral/qlora.yml index a5e8b65fbf..2a7495e95e 100644 --- a/examples/mistral/qlora.yml +++ b/examples/mistral/qlora.yml @@ -59,7 +59,7 @@ flash_attention: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/orpheus/finetune.yml b/examples/orpheus/finetune.yml index 9dcb8a43e6..f4bc8054ed 100644 --- a/examples/orpheus/finetune.yml +++ b/examples/orpheus/finetune.yml @@ -43,7 +43,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 20 +warmup_ratio: 0.1 evals_per_epoch: 5 saves_per_epoch: 5 weight_decay: 0.05 diff --git a/examples/phi/lora-3.5.yaml b/examples/phi/lora-3.5.yaml index b7f902d63b..a6fa15d98f 100644 --- a/examples/phi/lora-3.5.yaml +++ b/examples/phi/lora-3.5.yaml @@ -59,7 +59,7 @@ gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 4 weight_decay: 0.0 diff --git a/examples/phi/phi-ft.yml b/examples/phi/phi-ft.yml index 4adb62d3ab..717a459290 100644 --- a/examples/phi/phi-ft.yml +++ b/examples/phi/phi-ft.yml @@ -50,7 +50,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.1 diff --git a/examples/phi/phi-qlora.yml b/examples/phi/phi-qlora.yml index 11c08bfe65..0fe1abea5d 100644 --- a/examples/phi/phi-qlora.yml +++ b/examples/phi/phi-qlora.yml @@ -53,7 +53,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.1 diff --git a/examples/phi/phi2-ft.yml b/examples/phi/phi2-ft.yml index 102c7ba037..e470c0d24a 100644 --- a/examples/phi/phi2-ft.yml +++ b/examples/phi/phi2-ft.yml @@ -50,7 +50,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.1 diff --git a/examples/phi/phi3-ft-fsdp.yml b/examples/phi/phi3-ft-fsdp.yml index e8290ea1fa..1793737b54 100644 --- a/examples/phi/phi3-ft-fsdp.yml +++ b/examples/phi/phi3-ft-fsdp.yml @@ -51,7 +51,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 100 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.1 diff --git a/examples/qwen2/dpo.yaml b/examples/qwen2/dpo.yaml index 3b1f817e5d..3e87766d6b 100644 --- a/examples/qwen2/dpo.yaml +++ b/examples/qwen2/dpo.yaml @@ -50,7 +50,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/qwen2/qlora-fsdp.yaml b/examples/qwen2/qlora-fsdp.yaml index ca435b2bba..337619b614 100644 --- a/examples/qwen2/qlora-fsdp.yaml +++ b/examples/qwen2/qlora-fsdp.yaml @@ -49,7 +49,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/qwen3/32b-qlora.yaml b/examples/qwen3/32b-qlora.yaml index 87609c42f3..f4a4f28169 100644 --- a/examples/qwen3/32b-qlora.yaml +++ b/examples/qwen3/32b-qlora.yaml @@ -62,7 +62,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 diff --git a/examples/qwen3/8b-qat-fsdp2.yml b/examples/qwen3/8b-qat-fsdp2.yml index 395812a56a..cfbe5a4b77 100644 --- a/examples/qwen3/8b-qat-fsdp2.yml +++ b/examples/qwen3/8b-qat-fsdp2.yml @@ -58,7 +58,7 @@ logging_steps: 1 evals_per_epoch: 1 saves_per_epoch: 1 -warmup_steps: 10 +warmup_ratio: 0.1 weight_decay: 0.0 fsdp: - full_shard diff --git a/examples/qwen3/qlora-fsdp.yaml b/examples/qwen3/qlora-fsdp.yaml index 6af3cfbc63..e4d584dc7e 100644 --- a/examples/qwen3/qlora-fsdp.yaml +++ b/examples/qwen3/qlora-fsdp.yaml @@ -48,7 +48,7 @@ resume_from_checkpoint: logging_steps: 1 flash_attention: true -warmup_steps: 10 +warmup_ratio: 0.1 evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 From bb1cae1a20dd10bce644319d6fc26b1d51c1d666 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 30 Jul 2025 15:46:56 -0400 Subject: [PATCH 0861/1405] CLI: add --launcher option, support launcher args, cleanup, refactor (#2924) * add --launcher option; explicit True/False bool args; small cleanup * refactor * add torchrun, accelerate cli args * add rdzv arg default + tests * update _quarto * coderabbit * fix * we can't set rdvz_id independently across nodes * coderabbit * fix tests --- _quarto.yml | 12 +- docs/cli.qmd | 26 +- docs/multi-node.qmd | 14 +- src/axolotl/cli/args.py | 2 - src/axolotl/cli/cloud/__init__.py | 27 +- src/axolotl/cli/cloud/base.py | 10 +- src/axolotl/cli/cloud/modal_.py | 41 ++- src/axolotl/cli/config.py | 13 +- src/axolotl/cli/delinearize_llama4.py | 2 - src/axolotl/cli/evaluate.py | 6 - src/axolotl/cli/inference.py | 2 - src/axolotl/cli/main.py | 222 ++++++------ src/axolotl/cli/merge_lora.py | 2 - src/axolotl/cli/merge_sharded_fsdp_weights.py | 2 - src/axolotl/cli/preprocess.py | 2 - src/axolotl/cli/train.py | 6 - src/axolotl/cli/utils.py | 330 ------------------ src/axolotl/cli/utils/__init__.py | 23 ++ src/axolotl/cli/utils/args.py | 120 +++++++ src/axolotl/cli/utils/fetch.py | 142 ++++++++ src/axolotl/cli/utils/load.py | 52 +++ src/axolotl/cli/{ => utils}/sweeps.py | 0 src/axolotl/cli/utils/train.py | 188 ++++++++++ tests/cli/test_cli_base.py | 26 +- tests/cli/test_cli_evaluate.py | 114 +++++- tests/cli/test_cli_inference.py | 121 ++++++- tests/cli/test_cli_interface.py | 11 +- .../test_cli_merge_sharded_fsdp_weights.py | 94 ++++- tests/cli/test_cli_sweeps.py | 2 +- tests/cli/test_cli_train.py | 189 +++++++++- tests/cli/test_utils.py | 157 +++++++++ 31 files changed, 1417 insertions(+), 541 deletions(-) delete mode 100644 src/axolotl/cli/utils.py create mode 100644 src/axolotl/cli/utils/__init__.py create mode 100644 src/axolotl/cli/utils/args.py create mode 100644 src/axolotl/cli/utils/fetch.py create mode 100644 src/axolotl/cli/utils/load.py rename src/axolotl/cli/{ => utils}/sweeps.py (100%) create mode 100644 src/axolotl/cli/utils/train.py diff --git a/_quarto.yml b/_quarto.yml index dab1ee363b..250596d525 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -35,18 +35,24 @@ quartodoc: - cli.train - cli.evaluate - cli.args + - cli.art - cli.checks - cli.config + - cli.delinearize_llama4 - cli.inference - cli.merge_lora - cli.merge_sharded_fsdp_weights - cli.preprocess - - cli.sweeps - - cli.utils + - cli.quantize - cli.vllm_serve - cli.cloud.base - cli.cloud.modal_ - - cli.quantize + - cli.utils + - cli.utils.args + - cli.utils.fetch + - cli.utils.load + - cli.utils.sweeps + - cli.utils.train - title: Trainers desc: Training implementations contents: diff --git a/docs/cli.qmd b/docs/cli.qmd index f6f9b34810..d9f26dbf85 100644 --- a/docs/cli.qmd +++ b/docs/cli.qmd @@ -23,6 +23,20 @@ axolotl [config.yml] [options] The config file can be local or a URL to a raw YAML file. +### Launcher Arguments + +For commands that support multi-GPU (`train`, `evaluate`, ...), you can pass launcher-specific arguments using the `--` separator: + +```bash +# Pass torchrun arguments +axolotl train config.yml --launcher torchrun -- --nproc_per_node=2 --nnodes=1 + +# Pass accelerate arguments +axolotl train config.yml --launcher accelerate -- --config_file=accelerate_config.yml --num_processes=4 +``` + +Arguments after `--` are passed directly to the launcher (torchrun, accelerate launch, etc.). + ## Command Reference ### fetch @@ -80,7 +94,11 @@ axolotl train config.yml \ --num-epochs 3 # Training without accelerate -axolotl train config.yml --no-accelerate +axolotl train config.yml --launcher python + +# Pass launcher-specific arguments using -- separator +axolotl train config.yml --launcher torchrun -- --nproc_per_node=2 --nnodes=1 +axolotl train config.yml --launcher accelerate -- --config_file=accelerate_config.yml # Resume training from checkpoint axolotl train config.yml --resume-from-checkpoint path/to/checkpoint @@ -175,6 +193,9 @@ Evaluates a model's performance (loss etc) on the train and eval datasets. ```bash # Basic evaluation axolotl evaluate config.yml + +# Evaluation with launcher arguments +axolotl evaluate config.yml --launcher torchrun -- --nproc_per_node=2 ``` ### lm-eval @@ -287,9 +308,6 @@ axolotl preprocess config.yml --cloud cloud_config.yml # Train on cloud axolotl train config.yml --cloud cloud_config.yml -# Train without accelerate on cloud -axolotl train config.yml --cloud cloud_config.yml --no-accelerate - # Run lm-eval on cloud axolotl lm-eval config.yml --cloud cloud_config.yml ``` diff --git a/docs/multi-node.qmd b/docs/multi-node.qmd index 56d015462d..16196a2d70 100644 --- a/docs/multi-node.qmd +++ b/docs/multi-node.qmd @@ -69,11 +69,19 @@ export NCCL_BUFFSIZE=2097152 Run the following on each node: +### Option 1: New Axolotl CLI with launcher args (Recommended) + +```bash +axolotl train config.yaml --launcher torchrun -- --nnodes $num_nodes --nproc_per_node $gpu_per_node --rdzv_id $rdzv_id --rdzv_backend c10d --rdzv_endpoint "$head_node_ip:$head_node_port" +``` + +### Option 2: Direct torchrun (Legacy) + ```bash torchrun --nnodes $num_nodes --nproc_per_node $gpu_per_node --rdzv_id $rdzv_id --rdzv_backend c10d --rdzv_endpoint "$head_node_ip:$head_node_port" -m axolotl.cli.train config.yaml ``` -Please make sure to substitute the placeholder variables. +Please make sure to substitute the placeholder variables: - `num_nodes`: Number of nodes (containing GPUs) - `gpu_per_node`: Number of gpus per node @@ -81,8 +89,6 @@ Please make sure to substitute the placeholder variables. - `head_node_port`: Port of the head node (make sure other machines can connect to this. Default 29400) - `rdzv_id`: A unique job ID that is used by the job across nodes. -::: {.callout-note} -You need to call `axolotl.cli.train` instead of `axolotl train` as the latter calls accelerate under the hood -::: +The new CLI approach (Option 1) is recommended as it provides consistent argument handling and works seamlessly with other Axolotl CLI features. More info on the available configs can be found on the Pytorch docs [here](https://pytorch.org/docs/stable/elastic/run.html) diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py index e8571a900c..31d854d415 100644 --- a/src/axolotl/cli/args.py +++ b/src/axolotl/cli/args.py @@ -30,8 +30,6 @@ class TrainerCliArgs: debug_num_examples: int = field(default=0) prompter: Optional[str] = field(default=None) shard: bool = field(default=False) - main_process_port: Optional[int] = field(default=None) - num_processes: Optional[int] = field(default=None) @dataclass diff --git a/src/axolotl/cli/cloud/__init__.py b/src/axolotl/cli/cloud/__init__.py index 5cdce29ddf..bf12ab8cbc 100644 --- a/src/axolotl/cli/cloud/__init__.py +++ b/src/axolotl/cli/cloud/__init__.py @@ -3,7 +3,7 @@ """ from pathlib import Path -from typing import Union +from typing import Literal import yaml @@ -11,7 +11,7 @@ from axolotl.utils.dict import DictDefault -def load_cloud_cfg(cloud_config: Union[Path, str]) -> DictDefault: +def load_cloud_cfg(cloud_config: Path | str) -> DictDefault: """Load and validate cloud configuration.""" # Load cloud configuration. with open(cloud_config, encoding="utf-8") as file: @@ -20,8 +20,8 @@ def load_cloud_cfg(cloud_config: Union[Path, str]) -> DictDefault: def do_cli_preprocess( - cloud_config: Union[Path, str], - config: Union[Path, str], + cloud_config: Path | str, + config: Path | str, ) -> None: cloud_cfg = load_cloud_cfg(cloud_config) cloud = ModalCloud(cloud_cfg) @@ -31,9 +31,10 @@ def do_cli_preprocess( def do_cli_train( - cloud_config: Union[Path, str], - config: Union[Path, str], - accelerate: bool = True, + cloud_config: Path | str, + config: Path | str, + launcher: Literal["accelerate", "torchrun", "python"] = "accelerate", + launcher_args: list[str] | None = None, cwd=None, **kwargs, ) -> None: @@ -44,12 +45,18 @@ def do_cli_train( local_dirs = {} if cwd and not Path(cwd).joinpath("src", "axolotl").exists(): local_dirs = {"/workspace/mounts": cwd} - cloud.train(config_yaml, accelerate=accelerate, local_dirs=local_dirs, **kwargs) + cloud.train( + config_yaml, + launcher=launcher, + launcher_args=launcher_args, + local_dirs=local_dirs, + **kwargs, + ) def do_cli_lm_eval( - cloud_config: Union[Path, str], - config: Union[Path, str], + cloud_config: Path | str, + config: Path | str, ) -> None: cloud_cfg = load_cloud_cfg(cloud_config) cloud = ModalCloud(cloud_cfg) diff --git a/src/axolotl/cli/cloud/base.py b/src/axolotl/cli/cloud/base.py index eba8be49a4..c498e8691e 100644 --- a/src/axolotl/cli/cloud/base.py +++ b/src/axolotl/cli/cloud/base.py @@ -3,6 +3,7 @@ """ from abc import ABC, abstractmethod +from typing import Literal class Cloud(ABC): @@ -15,5 +16,12 @@ def preprocess(self, config_yaml: str, *args, **kwargs) -> None: pass @abstractmethod - def train(self, config_yaml: str, accelerate: bool = True) -> str: + def train( + self, + config_yaml: str, + launcher: Literal["accelerate", "torchrun", "python"] = "accelerate", + launcher_args: list[str] | None = None, + local_dirs: dict[str, str] | None = None, + **kwargs, + ): pass diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index 83cdd7b721..240c6d8944 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -8,7 +8,7 @@ import subprocess # nosec B404 from pathlib import Path from random import randint -from typing import Optional +from typing import Literal import modal @@ -230,8 +230,9 @@ def get_train_env(self, local_dirs=None): def train( self, config_yaml: str, - accelerate: bool = True, - local_dirs: Optional[dict[str, str]] = None, + launcher: Literal["accelerate", "torchrun", "python"] = "accelerate", + launcher_args: list[str] | None = None, + local_dirs: dict[str, str] | None = None, **kwargs, ): modal_fn = self.get_train_env(local_dirs)(_train) @@ -239,7 +240,8 @@ def train( with self.app.run(detach=True): modal_fn.remote( config_yaml, - accelerate=accelerate, + launcher=launcher, + launcher_args=launcher_args, volumes={k: v[0] for k, v in self.volumes.items()}, **kwargs, ) @@ -270,20 +272,35 @@ def _preprocess(config_yaml: str, volumes=None): ) -def _train(config_yaml: str, accelerate: bool = True, volumes=None, **kwargs): +def _train( + config_yaml: str, + launcher: Literal["accelerate", "torchrun", "python"] = "accelerate", + launcher_args: list[str] | None = None, + volumes=None, + **kwargs, # pylint: disable=unused-argument +): Path("/workspace/mounts").mkdir(parents=True, exist_ok=True) with open("/workspace/mounts/config.yaml", "w", encoding="utf-8") as f_out: f_out.write(config_yaml) run_folder = "/workspace/mounts" - if accelerate: - accelerate_args = "--accelerate" + + launcher_args = launcher_args or [] + + # Build the base command + if launcher == "accelerate": + launcher_arg = "--launcher accelerate" + elif launcher == "torchrun": + launcher_arg = "--launcher torchrun" else: - accelerate_args = "--no-accelerate" - num_processes_args = "" - if num_processes := kwargs.pop("num_processes", None): - num_processes_args = f"--num-processes {num_processes}" + launcher_arg = "--launcher python" + + # Build launcher args string + launcher_args_str = "" + if launcher_args: + launcher_args_str = "-- " + " ".join(launcher_args) + run_cmd( - f"axolotl train {accelerate_args} {num_processes_args} /workspace/mounts/config.yaml", + f"axolotl train {launcher_arg} /workspace/mounts/config.yaml {launcher_args_str}".strip(), run_folder, volumes, ) diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index cb0eece7fe..ae9f1f9c47 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -197,14 +197,13 @@ def load_cfg( # If there are any options passed in the cli, if it is something that seems valid # from the yaml, then overwrite the value cfg_keys = cfg.keys() - for k, _ in kwargs.items(): - # if not strict, allow writing to cfg even if it's not in the yml already - if k in cfg_keys or not cfg.strict: - # handle booleans - if isinstance(cfg[k], bool): - cfg[k] = bool(kwargs[k]) + for key, value in kwargs.items(): + # If not strict, allow writing to cfg even if it's not in the yml already + if key in cfg_keys or not cfg.strict: + if isinstance(cfg[key], bool): + cfg[key] = bool(value) else: - cfg[k] = kwargs[k] + cfg[key] = value try: device_props = torch.cuda.get_device_properties("cuda") diff --git a/src/axolotl/cli/delinearize_llama4.py b/src/axolotl/cli/delinearize_llama4.py index c92bae9302..90227fccde 100644 --- a/src/axolotl/cli/delinearize_llama4.py +++ b/src/axolotl/cli/delinearize_llama4.py @@ -9,7 +9,6 @@ import fire import torch from accelerate import init_empty_weights -from dotenv import load_dotenv from transformers import AutoProcessor @@ -152,5 +151,4 @@ def do_cli(model: Union[Path, str], output: Union[Path, str]) -> None: if __name__ == "__main__": - load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/evaluate.py b/src/axolotl/cli/evaluate.py index 8a847a6496..9dd3b00832 100644 --- a/src/axolotl/cli/evaluate.py +++ b/src/axolotl/cli/evaluate.py @@ -5,7 +5,6 @@ from typing import Union import fire -from dotenv import load_dotenv from transformers.hf_argparser import HfArgumentParser from axolotl.cli.args import TrainerCliArgs @@ -13,7 +12,6 @@ from axolotl.cli.config import load_cfg from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.evaluate import evaluate -from axolotl.utils import patch_optimized_env from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger @@ -30,9 +28,6 @@ def do_evaluate(cfg: DictDefault, cli_args: TrainerCliArgs) -> None: cfg: Dictionary mapping `axolotl` config keys to values. cli_args: CLI arguments. """ - # Enable expandable segments for cuda allocation to improve VRAM usage - patch_optimized_env() - # pylint: disable=duplicate-code check_accelerate_default_config() if int(os.getenv("LOCAL_RANK", "0")) == 0: @@ -64,5 +59,4 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: if __name__ == "__main__": - load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index 10132cd6fd..83b567b649 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -9,7 +9,6 @@ import fire import torch import transformers -from dotenv import load_dotenv from transformers import GenerationConfig, TextIteratorStreamer, TextStreamer from axolotl.cli.args import InferenceCliArgs @@ -268,5 +267,4 @@ def do_cli( if __name__ == "__main__": - load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index 69c1425acf..c41acc40b8 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -4,12 +4,9 @@ import os import subprocess # nosec B404 -import tempfile -from pathlib import Path -from typing import Optional +from typing import Literal, Optional import click -import yaml from dotenv import load_dotenv import axolotl @@ -21,13 +18,14 @@ VllmServeCliArgs, ) from axolotl.cli.art import print_axolotl_text_art -from axolotl.cli.sweeps import generate_sweep_configs from axolotl.cli.utils import ( add_options_from_config, add_options_from_dataclass, build_command, fetch_from_github, filter_none_kwargs, + generate_config_files, + launch_training, ) from axolotl.integrations.lm_eval.cli import lm_eval from axolotl.utils import patch_optimized_env @@ -36,12 +34,19 @@ LOG = get_logger(__name__) +LAUNCHER_COMMAND_MAPPING = { + "accelerate": ["accelerate", "launch"], + "torchrun": ["torchrun"], +} + @click.group() @click.version_option(version=axolotl.__version__, prog_name="axolotl") def cli(): """Axolotl CLI - Train and fine-tune large language models""" print_axolotl_text_art() + load_dotenv() + patch_optimized_env() @cli.command() @@ -50,7 +55,7 @@ def cli(): @add_options_from_dataclass(PreprocessCliArgs) @add_options_from_config(AxolotlInputConfig) @filter_none_kwargs -def preprocess(config: str, cloud: Optional[str] = None, **kwargs) -> None: +def preprocess(config: str, cloud: Optional[str] = None, **kwargs): """ Preprocess datasets before training. @@ -60,7 +65,6 @@ def preprocess(config: str, cloud: Optional[str] = None, **kwargs) -> None: kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` config options. """ - patch_optimized_env() if cloud: from axolotl.cli.cloud import do_cli_preprocess @@ -72,12 +76,15 @@ def preprocess(config: str, cloud: Optional[str] = None, **kwargs) -> None: do_cli(config=config, **kwargs) -@cli.command() +@cli.command( + context_settings={"ignore_unknown_options": True, "allow_extra_args": True} +) @click.argument("config", type=click.Path(exists=True, path_type=str)) @click.option( - "--accelerate/--no-accelerate", - default=True, - help="Use accelerate launch for multi-GPU training", + "--launcher", + type=click.Choice(["accelerate", "torchrun", "python"]), + default="accelerate", + help="Launcher to use for multi-GPU training", ) @click.option("--cloud", default=None, type=click.Path(exists=True, path_type=str)) @click.option( @@ -88,126 +95,81 @@ def preprocess(config: str, cloud: Optional[str] = None, **kwargs) -> None: @add_options_from_dataclass(TrainerCliArgs) @add_options_from_config(AxolotlInputConfig) @filter_none_kwargs +@click.pass_context def train( + ctx: click.Context, config: str, - accelerate: bool, - cloud: Optional[str] = None, - sweep: Optional[str] = None, + launcher: Literal["accelerate", "torchrun", "python"] = "accelerate", + cloud: str | None = None, + sweep: str | None = None, **kwargs, -) -> None: +): """ Train or fine-tune a model. Args: + ctx: Click context for extra args. config: Path to `axolotl` config YAML file. - accelerate: Whether to use `accelerate` launcher. + launcher: Launcher to use for multi-GPU training ("accelerate", "torchrun", or "python"). cloud: Path to a cloud accelerator configuration file sweep: Path to YAML config for sweeping hyperparameters. kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` config options. """ - # Enable expandable segments for cuda allocation to improve VRAM usage - patch_optimized_env() + # Extract launcher args from extra args (after --) + launcher_args = ctx.args if ctx.args else [] - if "use_ray" in kwargs and kwargs["use_ray"]: - accelerate = False - if sweep: - # load the sweep configuration yaml file - with open(sweep, "r", encoding="utf-8") as fin: - sweep_config: dict[str, list] = yaml.safe_load(fin) - with open(config, "r", encoding="utf-8") as fin: - base_config: dict[str, list] = yaml.safe_load(fin) - - # generate all possible configurations - permutations = generate_sweep_configs(base_config, sweep_config) - - def iter_configs(): - for perm in permutations: - # open temp directory for temporary configurations - with tempfile.TemporaryDirectory() as temp_dir: - with open( - Path(temp_dir) / "config.yaml", "w", encoding="utf-8" - ) as fout: - yaml.dump(perm, fout) - yield str(Path(temp_dir) / "config.yaml") - - else: + # Handle Ray launcher override + _launcher = None if kwargs.get("use_ray") else launcher - def iter_configs(): - yield config - - for cfg_file in iter_configs(): - # handle errors from subprocess so we can continue rest of sweeps + # Process each configuration + for cfg_file in generate_config_files(config, sweep): try: - if accelerate: - if cloud: - from axolotl.cli.cloud import do_cli_train - - cwd = os.getcwd() - do_cli_train( - cloud_config=cloud, - config=config, - accelerate=True, - cwd=cwd, - **kwargs, - ) - else: - accelerate_args = [] - if "main_process_port" in kwargs: - main_process_port = kwargs.pop("main_process_port", None) - accelerate_args.append("--main_process_port") - accelerate_args.append(str(main_process_port)) - if "num_processes" in kwargs: - num_processes = kwargs.pop("num_processes", None) - accelerate_args.append("--num_processes") - accelerate_args.append(str(num_processes)) - - base_cmd = ["accelerate", "launch"] - base_cmd.extend(accelerate_args) - base_cmd.extend(["-m", "axolotl.cli.train"]) - if cfg_file: - base_cmd.append(cfg_file) - cmd = build_command(base_cmd, kwargs) - subprocess.run(cmd, check=True) # nosec B603 - else: - if cloud: - from axolotl.cli.cloud import do_cli_train - - do_cli_train( - cloud_config=cloud, config=config, accelerate=False, **kwargs - ) - else: - from axolotl.cli.train import do_cli - - do_cli(config=cfg_file, **kwargs) + launch_training(cfg_file, _launcher, cloud, kwargs, launcher_args) except subprocess.CalledProcessError as exc: LOG.error(f"Failed to train/fine-tune config '{cfg_file}': {exc}") if not sweep: raise exc + finally: + # Only delete temp files, not the original config + if cfg_file != config: + os.unlink(cfg_file) -@cli.command() +@cli.command( + context_settings={"ignore_unknown_options": True, "allow_extra_args": True} +) @click.argument("config", type=click.Path(exists=True, path_type=str)) @click.option( - "--accelerate/--no-accelerate", - default=True, - help="Use accelerate launch for multi-GPU training", + "--launcher", + type=click.Choice(["accelerate", "torchrun", "python"]), + default="accelerate", + help="Launcher to use for multi-GPU evaluation", ) @add_options_from_dataclass(EvaluateCliArgs) @add_options_from_config(AxolotlInputConfig) @filter_none_kwargs -def evaluate(config: str, accelerate: bool, **kwargs) -> None: +@click.pass_context +def evaluate(ctx: click.Context, config: str, launcher: str, **kwargs): """ Evaluate a model. Args: + ctx: Click context for extra args. config: Path to `axolotl` config YAML file. - accelerate: Whether to use `accelerate` launcher. + launcher: Launcher to use for multi-GPU evaluation ("accelerate", "torchrun", or "python"). kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` config options. """ - if accelerate: - base_cmd = ["accelerate", "launch", "-m", "axolotl.cli.evaluate"] + # Extract launcher args from extra args (after --) + launcher_args = ctx.args if ctx.args else [] + + if launcher in LAUNCHER_COMMAND_MAPPING: + base_cmd = ( + LAUNCHER_COMMAND_MAPPING[launcher] + + launcher_args + + ["-m", "axolotl.cli.evaluate"] + ) if config: base_cmd.append(config) cmd = build_command(base_cmd, kwargs) @@ -218,30 +180,42 @@ def evaluate(config: str, accelerate: bool, **kwargs) -> None: do_cli(config=config, **kwargs) -@cli.command() +@cli.command( + context_settings={"ignore_unknown_options": True, "allow_extra_args": True} +) @click.argument("config", type=click.Path(exists=True, path_type=str)) @click.option( - "--accelerate/--no-accelerate", - default=False, - help="Use accelerate launch for multi-GPU inference", + "--launcher", + type=click.Choice(["accelerate", "torchrun", "python"]), + default="accelerate", + help="Launcher to use for multi-GPU inference", ) @click.option("--gradio", is_flag=True, help="Launch Gradio interface") @add_options_from_dataclass(TrainerCliArgs) @add_options_from_config(AxolotlInputConfig) @filter_none_kwargs -def inference(config: str, accelerate: bool, gradio: bool, **kwargs) -> None: +@click.pass_context +def inference(ctx: click.Context, config: str, launcher: str, gradio: bool, **kwargs): """ Run inference with a trained model. Args: + ctx: Click context for extra args. config: Path to `axolotl` config YAML file. - accelerate: Whether to use `accelerate` launcher. + launcher: Launcher to use for multi-GPU inference ("accelerate", "torchrun", or "python"). gradio: Whether to use Gradio browser interface or command line for inference. kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` config options. """ - if accelerate: - base_cmd = ["accelerate", "launch", "-m", "axolotl.cli.inference"] + # Extract launcher args from extra args (after --) + launcher_args = ctx.args if ctx.args else [] + + if launcher in LAUNCHER_COMMAND_MAPPING: + base_cmd = ( + LAUNCHER_COMMAND_MAPPING[launcher] + + launcher_args + + ["-m", "axolotl.cli.inference"] + ) if config: base_cmd.append(config) if gradio: @@ -254,33 +228,42 @@ def inference(config: str, accelerate: bool, gradio: bool, **kwargs) -> None: do_cli(config=config, gradio=gradio, **kwargs) -@cli.command() +@cli.command( + context_settings={"ignore_unknown_options": True, "allow_extra_args": True} +) @click.argument("config", type=click.Path(exists=True, path_type=str)) @click.option( - "--accelerate/--no-accelerate", - default=True, - help="Use accelerate launch for weight merging", + "--launcher", + type=click.Choice(["accelerate", "torchrun", "python"]), + default="accelerate", + help="Launcher to use for weight merging", ) @add_options_from_dataclass(TrainerCliArgs) @add_options_from_config(AxolotlInputConfig) @filter_none_kwargs -def merge_sharded_fsdp_weights(config: str, accelerate: bool, **kwargs) -> None: +@click.pass_context +def merge_sharded_fsdp_weights( + ctx: click.Context, config: str, launcher: str, **kwargs +): """ Merge sharded FSDP model weights. Args: + ctx: Click context for extra args. config: Path to `axolotl` config YAML file. - accelerate: Whether to use `accelerate` launcher. + launcher: Launcher to use for weight merging ("accelerate", "torchrun", or "python"). kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` config options. """ - if accelerate: - base_cmd = [ - "accelerate", - "launch", - "-m", - "axolotl.cli.merge_sharded_fsdp_weights", - ] + # Extract launcher args from extra args (after --) + launcher_args = ctx.args if ctx.args else [] + + if launcher in LAUNCHER_COMMAND_MAPPING: + base_cmd = ( + LAUNCHER_COMMAND_MAPPING[launcher] + + launcher_args + + ["-m", "axolotl.cli.merge_sharded_fsdp_weights"] + ) if config: base_cmd.append(config) cmd = build_command(base_cmd, kwargs) @@ -296,7 +279,7 @@ def merge_sharded_fsdp_weights(config: str, accelerate: bool, **kwargs) -> None: @add_options_from_dataclass(TrainerCliArgs) @add_options_from_config(AxolotlInputConfig) @filter_none_kwargs -def merge_lora(config: str, **kwargs) -> None: +def merge_lora(config: str, **kwargs): """ Merge trained LoRA adapters into a base model. @@ -313,7 +296,7 @@ def merge_lora(config: str, **kwargs) -> None: @cli.command() @click.argument("directory", type=click.Choice(["examples", "deepspeed_configs"])) @click.option("--dest", help="Destination directory") -def fetch(directory: str, dest: Optional[str]) -> None: +def fetch(directory: str, dest: Optional[str]): """ Fetch example configs or other resources. @@ -351,7 +334,7 @@ def quantize(config: str, **cli_args: QuantizeCliArgs): @cli.command() @click.argument("model", type=click.Path(exists=True, path_type=str)) @click.argument("output", type=click.Path(exists=False, path_type=str)) -def delinearize_llama4(model: str, output: str) -> None: +def delinearize_llama4(model: str, output: str): from axolotl.cli.delinearize_llama4 import do_cli as do_delinearize_llama4 do_delinearize_llama4(model, output) @@ -365,5 +348,4 @@ def main(): if __name__ == "__main__": - load_dotenv() main() diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index d639b3aeec..422593a48d 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -4,7 +4,6 @@ from typing import Union import fire -from dotenv import load_dotenv from axolotl.cli.config import load_cfg from axolotl.cli.utils import load_model_and_tokenizer @@ -88,5 +87,4 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: if __name__ == "__main__": - load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/merge_sharded_fsdp_weights.py b/src/axolotl/cli/merge_sharded_fsdp_weights.py index b0880ce215..c08d30ec8e 100644 --- a/src/axolotl/cli/merge_sharded_fsdp_weights.py +++ b/src/axolotl/cli/merge_sharded_fsdp_weights.py @@ -17,7 +17,6 @@ WEIGHTS_NAME, is_torch_version, ) -from dotenv import load_dotenv from huggingface_hub import split_torch_state_dict_into_shards from safetensors.torch import save_file as safe_save_file from torch.distributed.checkpoint.format_utils import _EmptyStateDictLoadPlanner @@ -204,5 +203,4 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): if __name__ == "__main__": - load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index 595eb8aac4..5d692c3151 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -9,7 +9,6 @@ import transformers from accelerate import init_empty_weights from colorama import Fore -from dotenv import load_dotenv from transformers import AutoModelForCausalLM from axolotl.cli.args import PreprocessCliArgs @@ -109,5 +108,4 @@ def do_cli( if __name__ == "__main__": - load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index d0cf8455bc..7f0b0bdd29 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -7,7 +7,6 @@ import fire from accelerate import Accelerator -from dotenv import load_dotenv from transformers.hf_argparser import HfArgumentParser from axolotl.cli.args import TrainerCliArgs @@ -16,7 +15,6 @@ from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.integrations.base import PluginManager from axolotl.train import train -from axolotl.utils import patch_optimized_env from axolotl.utils.config import normalize_config, resolve_dtype from axolotl.utils.dict import DictDefault @@ -31,9 +29,6 @@ def do_train(cfg: DictDefault, cli_args: TrainerCliArgs): cfg: Dictionary mapping `axolotl` config keys to values. cli_args: Training-specific CLI arguments. """ - # Enable expandable segments for cuda allocation to improve VRAM usage - patch_optimized_env() - check_accelerate_default_config() if int(os.getenv("LOCAL_RANK", "0")) == 0: check_user_token() @@ -122,5 +117,4 @@ def ray_train_func(kwargs: dict): if __name__ == "__main__": - load_dotenv() fire.Fire(do_cli) diff --git a/src/axolotl/cli/utils.py b/src/axolotl/cli/utils.py deleted file mode 100644 index d287953613..0000000000 --- a/src/axolotl/cli/utils.py +++ /dev/null @@ -1,330 +0,0 @@ -"""Utility methods for axolotl CLI.""" - -import concurrent.futures -import dataclasses -import hashlib -import json -from functools import wraps -from pathlib import Path -from types import NoneType -from typing import Any, Callable, Type, Union, get_args, get_origin - -import click -import requests -from pydantic import BaseModel -from transformers import ( - PreTrainedModel, - PreTrainedTokenizer, - PreTrainedTokenizerFast, - ProcessorMixin, -) - -from axolotl.loaders import load_processor, load_tokenizer -from axolotl.loaders.model import ModelLoader -from axolotl.utils.dict import DictDefault -from axolotl.utils.logging import get_logger - -LOG = get_logger(__name__) - - -def strip_optional_type(field_type: type | str | None): - """ - Extracts the non-`None` type from an `Optional` / `Union` type. - - Args: - field_type: Type of field for Axolotl CLI command. - - Returns: - If the input type is `Union[T, None]` or `Optional[T]`, returns `T`. Otherwise - returns the input type unchanged. - """ - if get_origin(field_type) is Union and type(None) in get_args(field_type): - field_type = next( - t for t in get_args(field_type) if not isinstance(t, NoneType) - ) - - return field_type - - -def filter_none_kwargs(func: Callable) -> Callable: - """ - Wraps function to remove `None`-valued `kwargs`. - - Args: - func: Function to wrap. - - Returns: - Wrapped function. - """ - - @wraps(func) - def wrapper(*args, **kwargs) -> Callable: - """Filters out `None`-valued `kwargs`.""" - filtered_kwargs = {k: v for k, v in kwargs.items() if v is not None} - - return func(*args, **filtered_kwargs) - - return wrapper - - -def add_options_from_dataclass(config_class: Type[Any]) -> Callable: - """ - Create Click options from the fields of a dataclass. - - Args: - config_class: Dataclass with fields to parse from the CLI. - - Returns: - Function decorator for Axolotl CLI command. - """ - - def decorator(function: Callable) -> Callable: - # Process dataclass fields in reverse order for correct option ordering - for field in reversed(dataclasses.fields(config_class)): - field_type = strip_optional_type(field.type) - - if field_type == bool: - field_name = field.name.replace("_", "-") - option_name = f"--{field_name}/--no-{field_name}" - function = click.option( - option_name, - default=field.default, - help=field.metadata.get("description"), - )(function) - else: - option_name = f"--{field.name.replace('_', '-')}" - function = click.option( - option_name, - type=field_type, - default=field.default, - help=field.metadata.get("description"), - )(function) - - return function - - return decorator - - -def add_options_from_config(config_class: Type[BaseModel]) -> Callable: - """ - Create Click options from the fields of a Pydantic model. - - Args: - config_class: PyDantic model with fields to parse from the CLI - - Returns: - Function decorator for Axolotl CLI command. - """ - - def decorator(function: Callable) -> Callable: - # Process model fields in reverse order for correct option ordering - for name, field in reversed(config_class.model_fields.items()): - field_type = strip_optional_type(field.annotation) - - if field_type == bool: - field_name = name.replace("_", "-") - option_name = f"--{field_name}/--no-{field_name}" - function = click.option( - option_name, default=None, help=field.description - )(function) - else: - option_name = f"--{name.replace('_', '-')}" - function = click.option( - option_name, default=None, help=field.description - )(function) - - return function - - return decorator - - -def build_command(base_cmd: list[str], options: dict[str, Any]) -> list[str]: - """ - Build command list from base command and options. - - Args: - base_cmd: Command without options. - options: Options to parse and append to base command. - - Returns: - List of strings giving shell command. - """ - cmd = base_cmd.copy() - - for key, value in options.items(): - if value is None: - continue - - key = key.replace("_", "-") - - if isinstance(value, bool): - if value: - cmd.append(f"--{key}") - else: - cmd.extend([f"--{key}", str(value)]) - - return cmd - - -def download_file( - file_info: tuple, raw_base_url: str, dest_path: Path, dir_prefix: str -) -> tuple[str, str]: - """ - Download a single file and return its processing status. - - Args: - file_info: Tuple of (file_path, remote_sha). - raw_base_url: Base URL for raw GitHub content. - dest_path: Local destination directory. - dir_prefix: Directory prefix to filter files. - - Returns: - Tuple of (file_path, status) where status is 'new', 'updated', or 'unchanged'. - """ - file_path, remote_sha = file_info - raw_url = f"{raw_base_url}/{file_path}" - dest_file = dest_path / file_path.split(dir_prefix)[-1] - - # Check if file exists and needs updating - if dest_file.exists(): - with open(dest_file, "rb") as file: - content = file.read() - # Calculate git blob SHA - blob = b"blob " + str(len(content)).encode() + b"\0" + content - local_sha = hashlib.sha1(blob, usedforsecurity=False).hexdigest() - - if local_sha == remote_sha: - print(f"Skipping {file_path} (unchanged)") - return file_path, "unchanged" - - print(f"Updating {file_path}") - status = "new" - else: - print(f"Downloading {file_path}") - status = "new" - - # Create directories if needed - dest_file.parent.mkdir(parents=True, exist_ok=True) - - # Download and save file - try: - response = requests.get(raw_url, timeout=30) - response.raise_for_status() - - with open(dest_file, "wb") as file: - file.write(response.content) - - return file_path, status - except (requests.RequestException, IOError) as request_error: - print(f"Error downloading {file_path}: {str(request_error)}") - return file_path, "error" - - -def fetch_from_github( - dir_prefix: str, dest_dir: str | None = None, max_workers: int = 5 -) -> None: - """ - Sync files from a specific directory in the GitHub repository. - Only downloads files that don't exist locally or have changed. - - Args: - dir_prefix: Directory prefix to filter files (e.g., 'examples/', - 'deepspeed_configs/'). - dest_dir: Local destination directory. - max_workers: Maximum number of concurrent downloads. - """ - api_url = "https://api.github.com/repos/axolotl-ai-cloud/axolotl/git/trees/main?recursive=1" - raw_base_url = "https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main" - - # Get repository tree with timeout - response = requests.get(api_url, timeout=30) - response.raise_for_status() - tree = json.loads(response.text) - - # Filter for files and get their SHA - files = { - item["path"]: item["sha"] - for item in tree["tree"] - if item["type"] == "blob" and item["path"].startswith(dir_prefix) - } - - if not files: - raise click.ClickException(f"No files found in {dir_prefix}") - - # Default destination directory is the last part of dir_prefix - default_dest = Path(dir_prefix.rstrip("/")) - dest_path = Path(dest_dir) if dest_dir else default_dest - - # Keep track of processed files for summary - files_processed: dict[str, list[str]] = { - "new": [], - "updated": [], - "unchanged": [], - "error": [], - } - - # Process files in parallel using ThreadPoolExecutor - with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: - future_to_file = { - executor.submit( - download_file, - (file_path, remote_sha), - raw_base_url, - dest_path, - dir_prefix, - ): file_path - for file_path, remote_sha in files.items() - } - - # Process completed tasks as they finish - for future in concurrent.futures.as_completed(future_to_file): - file_path = future_to_file[future] - try: - file_path, status = future.result() - files_processed[status].append(file_path) - except (requests.RequestException, IOError) as request_error: - print(f"Error processing {file_path}: {str(request_error)}") - files_processed["error"].append(file_path) - - # Log summary - LOG.info("\nSync Summary:") - LOG.info(f"New files: {len(files_processed['new'])}") - LOG.info(f"Updated files: {len(files_processed['updated'])}") - LOG.info(f"Unchanged files: {len(files_processed['unchanged'])}") - if files_processed["error"]: - LOG.info(f"Failed files: {len(files_processed['error'])}") - - -def load_model_and_tokenizer( - *, - cfg: DictDefault, - inference: bool = False, -) -> tuple[ - PreTrainedModel, - PreTrainedTokenizer | PreTrainedTokenizerFast | Any, - ProcessorMixin | None, -]: - """ - Helper function for loading a model, tokenizer, and processor specified in the given `axolotl` - config. - - Args: - cfg: Dictionary mapping `axolotl` config keys to values. - inference: Boolean denoting inference mode. - - Returns: - Tuple of (PreTrainedModel, PreTrainedTokenizer, ProcessorMixin). - """ - LOG.info(f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}") - tokenizer = load_tokenizer(cfg) - - LOG.info("loading model...") - model_loader = ModelLoader(cfg, tokenizer, inference=inference) - model, _ = model_loader.load() - - processor = None - if cfg.is_multimodal: - LOG.info("loading processor...") - processor = load_processor(cfg, tokenizer) - - return model, tokenizer, processor diff --git a/src/axolotl/cli/utils/__init__.py b/src/axolotl/cli/utils/__init__.py new file mode 100644 index 0000000000..583130339a --- /dev/null +++ b/src/axolotl/cli/utils/__init__.py @@ -0,0 +1,23 @@ +"""Init for axolotl.cli.utils module.""" + +from .args import ( + add_options_from_config, + add_options_from_dataclass, + filter_none_kwargs, +) +from .fetch import fetch_from_github +from .load import load_model_and_tokenizer +from .sweeps import generate_sweep_configs +from .train import build_command, generate_config_files, launch_training + +__all__ = [ + "filter_none_kwargs", + "add_options_from_dataclass", + "add_options_from_config", + "build_command", + "generate_config_files", + "generate_sweep_configs", + "load_model_and_tokenizer", + "launch_training", + "fetch_from_github", +] diff --git a/src/axolotl/cli/utils/args.py b/src/axolotl/cli/utils/args.py new file mode 100644 index 0000000000..3aea1a378a --- /dev/null +++ b/src/axolotl/cli/utils/args.py @@ -0,0 +1,120 @@ +"""Utilities for axolotl CLI args.""" + +import dataclasses +from functools import wraps +from types import NoneType +from typing import Any, Callable, Type, Union, get_args, get_origin + +import click +from pydantic import BaseModel + + +def _strip_optional_type(field_type: type | str | None): + """ + Extracts the non-`None` type from an `Optional` / `Union` type. + + Args: + field_type: Type of field for Axolotl CLI command. + + Returns: + If the input type is `Union[T, None]` or `Optional[T]`, returns `T`. Otherwise + returns the input type unchanged. + """ + if get_origin(field_type) is Union and type(None) in get_args(field_type): + field_type = next( + t for t in get_args(field_type) if not isinstance(t, NoneType) + ) + + return field_type + + +def filter_none_kwargs(func: Callable) -> Callable: + """ + Wraps function to remove `None`-valued `kwargs`. + + Args: + func: Function to wrap. + + Returns: + Wrapped function. + """ + + @wraps(func) + def wrapper(*args, **kwargs) -> Callable: + """Filters out `None`-valued `kwargs`.""" + filtered_kwargs = {k: v for k, v in kwargs.items() if v is not None} + + return func(*args, **filtered_kwargs) + + return wrapper + + +def add_options_from_dataclass(config_class: Type[Any]) -> Callable: + """ + Create Click options from the fields of a dataclass. + + Args: + config_class: Dataclass with fields to parse from the CLI. + + Returns: + Function decorator for Axolotl CLI command. + """ + + def decorator(function: Callable) -> Callable: + # Process dataclass fields in reverse order for correct option ordering + for field in reversed(dataclasses.fields(config_class)): + field_type = _strip_optional_type(field.type) + + if field_type == bool: + field_name = field.name.replace("_", "-") + option_name = f"--{field_name}/--no-{field_name}" + function = click.option( + option_name, + default=field.default, + help=field.metadata.get("description"), + )(function) + else: + option_name = f"--{field.name.replace('_', '-')}" + function = click.option( + option_name, + type=field_type, + default=field.default, + help=field.metadata.get("description"), + )(function) + + return function + + return decorator + + +def add_options_from_config(config_class: Type[BaseModel]) -> Callable: + """ + Create Click options from the fields of a Pydantic model. + + Args: + config_class: PyDantic model with fields to parse from the CLI + + Returns: + Function decorator for Axolotl CLI command. + """ + + def decorator(function: Callable) -> Callable: + # Process model fields in reverse order for correct option ordering + for name, field in reversed(config_class.model_fields.items()): + field_type = _strip_optional_type(field.annotation) + + if field_type == bool: + field_name = name.replace("_", "-") + option_name = f"--{field_name}/--no-{field_name}" + function = click.option( + option_name, default=None, help=field.description + )(function) + else: + option_name = f"--{name.replace('_', '-')}" + function = click.option( + option_name, default=None, help=field.description + )(function) + + return function + + return decorator diff --git a/src/axolotl/cli/utils/fetch.py b/src/axolotl/cli/utils/fetch.py new file mode 100644 index 0000000000..441b7f6f74 --- /dev/null +++ b/src/axolotl/cli/utils/fetch.py @@ -0,0 +1,142 @@ +"""Utilities for axolotl fetch CLI command.""" + +import concurrent.futures +import hashlib +import json +from pathlib import Path + +import click +import requests + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def _download_file( + file_info: tuple, raw_base_url: str, dest_path: Path, dir_prefix: str +) -> tuple[str, str]: + """ + Download a single file and return its processing status. + + Args: + file_info: Tuple of (file_path, remote_sha). + raw_base_url: Base URL for raw GitHub content. + dest_path: Local destination directory. + dir_prefix: Directory prefix to filter files. + + Returns: + Tuple of (file_path, status) where status is 'new', 'updated', or 'unchanged'. + """ + file_path, remote_sha = file_info + raw_url = f"{raw_base_url}/{file_path}" + dest_file = dest_path / file_path.split(dir_prefix)[-1] + + # Check if file exists and needs updating + if dest_file.exists(): + with open(dest_file, "rb") as file: + content = file.read() + # Calculate git blob SHA + blob = b"blob " + str(len(content)).encode() + b"\0" + content + local_sha = hashlib.sha1(blob, usedforsecurity=False).hexdigest() + + if local_sha == remote_sha: + print(f"Skipping {file_path} (unchanged)") + return file_path, "unchanged" + + print(f"Updating {file_path}") + status = "updated" + else: + print(f"Downloading {file_path}") + status = "new" + + # Create directories if needed + dest_file.parent.mkdir(parents=True, exist_ok=True) + + # Download and save file + try: + response = requests.get(raw_url, timeout=30) + response.raise_for_status() + + with open(dest_file, "wb") as file: + file.write(response.content) + + return file_path, status + except (requests.RequestException, IOError) as request_error: + print(f"Error downloading {file_path}: {str(request_error)}") + return file_path, "error" + + +def fetch_from_github( + dir_prefix: str, dest_dir: str | None = None, max_workers: int = 5 +) -> None: + """ + Sync files from a specific directory in the GitHub repository. + Only downloads files that don't exist locally or have changed. + + Args: + dir_prefix: Directory prefix to filter files (e.g., 'examples/', + 'deepspeed_configs/'). + dest_dir: Local destination directory. + max_workers: Maximum number of concurrent downloads. + """ + api_url = "https://api.github.com/repos/axolotl-ai-cloud/axolotl/git/trees/main?recursive=1" + raw_base_url = "https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main" + + # Get repository tree with timeout + response = requests.get(api_url, timeout=30) + response.raise_for_status() + tree = json.loads(response.text) + + # Filter for files and get their SHA + files = { + item["path"]: item["sha"] + for item in tree["tree"] + if item["type"] == "blob" and item["path"].startswith(dir_prefix) + } + + if not files: + raise click.ClickException(f"No files found in {dir_prefix}") + + # Default destination directory is the last part of dir_prefix + default_dest = Path(dir_prefix.rstrip("/")) + dest_path = Path(dest_dir) if dest_dir else default_dest + + # Keep track of processed files for summary + files_processed: dict[str, list[str]] = { + "new": [], + "updated": [], + "unchanged": [], + "error": [], + } + + # Process files in parallel using ThreadPoolExecutor + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + future_to_file = { + executor.submit( + _download_file, + (file_path, remote_sha), + raw_base_url, + dest_path, + dir_prefix, + ): file_path + for file_path, remote_sha in files.items() + } + + # Process completed tasks as they finish + for future in concurrent.futures.as_completed(future_to_file): + file_path = future_to_file[future] + try: + file_path, status = future.result() + files_processed[status].append(file_path) + except (requests.RequestException, IOError) as request_error: + print(f"Error processing {file_path}: {str(request_error)}") + files_processed["error"].append(file_path) + + # Log summary + LOG.info("\nSync Summary:") + LOG.info(f"New files: {len(files_processed['new'])}") + LOG.info(f"Updated files: {len(files_processed['updated'])}") + LOG.info(f"Unchanged files: {len(files_processed['unchanged'])}") + if files_processed["error"]: + LOG.info(f"Failed files: {len(files_processed['error'])}") diff --git a/src/axolotl/cli/utils/load.py b/src/axolotl/cli/utils/load.py new file mode 100644 index 0000000000..610a81306d --- /dev/null +++ b/src/axolotl/cli/utils/load.py @@ -0,0 +1,52 @@ +"""Utilities for model, tokenizer, etc. loading.""" + +from typing import Any + +from transformers import ( + PreTrainedModel, + PreTrainedTokenizer, + PreTrainedTokenizerFast, + ProcessorMixin, +) + +from axolotl.loaders import load_processor, load_tokenizer +from axolotl.loaders.model import ModelLoader +from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def load_model_and_tokenizer( + *, + cfg: DictDefault, + inference: bool = False, +) -> tuple[ + PreTrainedModel, + PreTrainedTokenizer | PreTrainedTokenizerFast | Any, + ProcessorMixin | None, +]: + """ + Helper function for loading a model, tokenizer, and processor specified in the + given `axolotl` config. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + inference: Boolean denoting inference mode. + + Returns: + Tuple of (PreTrainedModel, PreTrainedTokenizer, ProcessorMixin). + """ + LOG.info(f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}") + tokenizer = load_tokenizer(cfg) + + LOG.info("loading model...") + model_loader = ModelLoader(cfg, tokenizer, inference=inference) + model, _ = model_loader.load() + + processor = None + if cfg.is_multimodal: + LOG.info("loading processor...") + processor = load_processor(cfg, tokenizer) + + return model, tokenizer, processor diff --git a/src/axolotl/cli/sweeps.py b/src/axolotl/cli/utils/sweeps.py similarity index 100% rename from src/axolotl/cli/sweeps.py rename to src/axolotl/cli/utils/sweeps.py diff --git a/src/axolotl/cli/utils/train.py b/src/axolotl/cli/utils/train.py new file mode 100644 index 0000000000..61d05e52b6 --- /dev/null +++ b/src/axolotl/cli/utils/train.py @@ -0,0 +1,188 @@ +"""Utilities for axolotl train CLI command.""" + +import os +import subprocess # nosec +import tempfile +from typing import Any, Iterator, Literal + +import yaml + +from axolotl.cli.utils.sweeps import generate_sweep_configs + + +def _add_default_rdzv_args(launcher_args: list[str]) -> list[str]: + """ + Add default RDZV arguments if rdzv_endpoint is set but rdzv_backend/rdzv_id are missing. + + Args: + launcher_args: List of launcher arguments + + Returns: + Updated launcher args with defaults added if needed + """ + args = launcher_args.copy() + + # Check if rdzv_endpoint is present + has_rdzv_endpoint = any("--rdzv_endpoint" in arg for arg in args) + + if has_rdzv_endpoint: + # Check if rdzv_backend is already provided + has_rdzv_backend = any("--rdzv_backend" in arg for arg in args) + if not has_rdzv_backend: + args.extend(["--rdzv_backend", "c10d"]) + + # Check if rdzv_id is already provided + has_rdzv_id = any("--rdzv_id" in arg for arg in args) + if not has_rdzv_id: + import uuid + + args.extend(["--rdzv_id", str(uuid.uuid4())[:8]]) + + return args + + +def build_command(base_cmd: list[str], options: dict[str, Any]) -> list[str]: + """ + Build command list from base command and options. + + Args: + base_cmd: Command without options. + options: Options to parse and append to base command. + + Returns: + List of strings giving shell command. + """ + cmd = base_cmd.copy() + + for key, value in options.items(): + if value is None: + continue + + key = key.replace("_", "-") + cmd.append(f"--{key}={value}") + + return cmd + + +def generate_config_files(config: str, sweep: str | None) -> Iterator[str]: + """Generate list of configuration files to process.""" + if not sweep: + yield config + return + + # Load sweep and base configurations + with open(sweep, "r", encoding="utf-8") as fin: + sweep_config: dict[str, list] = yaml.safe_load(fin) + with open(config, "r", encoding="utf-8") as fin: + base_config: dict[str, list] = yaml.safe_load(fin) + + # Generate all possible configurations + permutations = generate_sweep_configs(base_config, sweep_config) + for permutation in permutations: + # pylint: disable=consider-using-with + temp_file = tempfile.NamedTemporaryFile( + mode="w", + suffix=".yaml", + delete=False, + encoding="utf-8", + ) + yaml.dump(permutation, temp_file) + temp_file.close() + yield temp_file.name + + +def launch_training( + cfg_file: str, + launcher: Literal["accelerate", "torchrun", "python"] | None, + cloud: str | None, + kwargs: dict, + launcher_args: list[str] | None = None, +) -> None: + """Execute training with the given configuration.""" + launcher_args = launcher_args or [] + + if cloud: + _launch_cloud_training(cloud, cfg_file, launcher, kwargs, launcher_args) + elif launcher: + if launcher == "accelerate": + _launch_accelerate_training(cfg_file, kwargs, launcher_args) + elif launcher == "torchrun": + _launch_torchrun_training(cfg_file, kwargs, launcher_args) + elif launcher == "python": + _launch_python_training(cfg_file, kwargs) + + +def _launch_cloud_training( + cloud: str, + cfg_file: str, + launcher: Literal["accelerate", "torchrun", "python"] | None, + kwargs: dict, + launcher_args: list[str] | None = None, +) -> None: + """Execute training via cloud launcher.""" + from axolotl.cli.cloud import do_cli_train + + launcher_args = launcher_args or [] + cwd = os.getcwd() if launcher else None + + do_cli_train( + cloud_config=cloud, + config=cfg_file, + launcher=launcher or "accelerate", + launcher_args=launcher_args, + cwd=cwd, + **kwargs, + ) + + +def _launch_accelerate_training( + cfg_file: str, kwargs: dict, launcher_args: list[str] | None = None +) -> None: + """Execute training via accelerate launcher.""" + launcher_args = launcher_args or [] + internal_launcher_args = [] + + # Extract launcher-specific arguments from kwargs (legacy support) + if "main_process_port" in kwargs: + main_process_port = kwargs.pop("main_process_port") + internal_launcher_args.extend(["--main_process_port", str(main_process_port)]) + + if "num_processes" in kwargs: + num_processes = kwargs.pop("num_processes") + internal_launcher_args.extend(["--num_processes", str(num_processes)]) + + # Combine internal args with user-provided launcher args + all_launcher_args = internal_launcher_args + launcher_args + + base_cmd = ( + ["accelerate", "launch"] + all_launcher_args + ["-m", "axolotl.cli.train"] + ) + if cfg_file: + base_cmd.append(cfg_file) + + cmd = build_command(base_cmd, kwargs) + subprocess.run(cmd, check=True) # nosec B603 + + +def _launch_torchrun_training( + cfg_file: str, kwargs: dict, launcher_args: list[str] | None = None +) -> None: + """Execute training via torchrun launcher.""" + launcher_args = launcher_args or [] + + # Add default RDZV arguments if rdzv_endpoint is set + launcher_args = _add_default_rdzv_args(launcher_args) + + base_cmd = ["torchrun"] + launcher_args + ["-m", "axolotl.cli.train"] + if cfg_file: + base_cmd.append(cfg_file) + + cmd = build_command(base_cmd, kwargs) + subprocess.run(cmd, check=True) # nosec B603 + + +def _launch_python_training(cfg_file: str, kwargs: dict) -> None: + """Execute training via python launcher.""" + from axolotl.cli.train import do_cli + + do_cli(config=cfg_file, **kwargs) diff --git a/tests/cli/test_cli_base.py b/tests/cli/test_cli_base.py index 6dbae045f6..4b880d44a4 100644 --- a/tests/cli/test_cli_base.py +++ b/tests/cli/test_cli_base.py @@ -17,16 +17,23 @@ def _test_cli_validation(self, cli_runner, command: str): command: Command to test (train/evaluate) """ # Test missing config file - result = cli_runner.invoke(cli, [command, "--no-accelerate"]) + result = cli_runner.invoke(cli, [command, "--launcher", "python"]) assert result.exit_code != 0 # Test non-existent config file - result = cli_runner.invoke(cli, [command, "nonexistent.yml", "--no-accelerate"]) + result = cli_runner.invoke( + cli, [command, "nonexistent.yml", "--launcher", "python"] + ) assert result.exit_code != 0 assert "Error: Invalid value for 'CONFIG'" in result.output def _test_basic_execution( - self, cli_runner, tmp_path: Path, valid_test_config: str, command: str + self, + cli_runner, + tmp_path: Path, + valid_test_config: str, + command: str, + train: bool = True, ): """Test basic execution with accelerate. @@ -35,6 +42,7 @@ def _test_basic_execution( tmp_path: Temporary path fixture valid_test_config: Valid config fixture command: Command to test (train/evaluate) + train: Whether to test training (default) or evaluation """ config_path = tmp_path / "config.yml" config_path.write_text(valid_test_config) @@ -43,15 +51,21 @@ def _test_basic_execution( result = cli_runner.invoke(cli, [command, str(config_path)]) assert mock.called - assert mock.call_args.args[0] == [ + + expected = [ "accelerate", "launch", "-m", f"axolotl.cli.{command}", str(config_path), - "--debug-num-examples", - "0", + "--debug=False", + "--debug-text-only=False", + "--debug-num-examples=0", ] + if train: + expected.append("--shard=False") + + assert mock.call_args.args[0] == expected assert mock.call_args.kwargs == {"check": True} assert result.exit_code == 0 diff --git a/tests/cli/test_cli_evaluate.py b/tests/cli/test_cli_evaluate.py index d8eb41467f..a191bf957e 100644 --- a/tests/cli/test_cli_evaluate.py +++ b/tests/cli/test_cli_evaluate.py @@ -1,5 +1,7 @@ """Tests for evaluate CLI command.""" +# pylint: disable=duplicate-code + from unittest.mock import patch from axolotl.cli.main import cli @@ -18,7 +20,9 @@ def test_evaluate_cli_validation(self, cli_runner): def test_evaluate_basic_execution(self, cli_runner, tmp_path, valid_test_config): """Test basic successful execution""" - self._test_basic_execution(cli_runner, tmp_path, valid_test_config, "evaluate") + self._test_basic_execution( + cli_runner, tmp_path, valid_test_config, "evaluate", train=False + ) def test_evaluate_basic_execution_no_accelerate( self, cli_runner, tmp_path, valid_test_config @@ -27,13 +31,15 @@ def test_evaluate_basic_execution_no_accelerate( config_path = tmp_path / "config.yml" config_path.write_text(valid_test_config) + # pylint: disable=duplicate-code with patch("axolotl.cli.evaluate.do_evaluate") as mock_evaluate: result = cli_runner.invoke( cli, [ "evaluate", str(config_path), - "--no-accelerate", + "--launcher", + "python", ], catch_exceptions=False, ) @@ -55,7 +61,8 @@ def test_evaluate_cli_overrides(self, cli_runner, tmp_path, valid_test_config): "2", "--sequence-len", "128", - "--no-accelerate", + "--launcher", + "python", ], catch_exceptions=False, ) @@ -65,3 +72,104 @@ def test_evaluate_cli_overrides(self, cli_runner, tmp_path, valid_test_config): cfg = mock_evaluate.call_args[0][0] assert cfg.micro_batch_size == 2 assert cfg.sequence_len == 128 + + def test_evaluate_with_launcher_args_torchrun( + self, cli_runner, tmp_path, valid_test_config + ): + """Test evaluate with torchrun launcher arguments""" + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "evaluate", + str(config_path), + "--launcher", + "torchrun", + "--", + "--nproc_per_node=2", + "--nnodes=1", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify launcher args are passed to torchrun + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "torchrun" + assert "--nproc_per_node=2" in called_cmd + assert "--nnodes=1" in called_cmd + assert "-m" in called_cmd + assert "axolotl.cli.evaluate" in called_cmd + + def test_evaluate_with_launcher_args_accelerate( + self, cli_runner, tmp_path, valid_test_config + ): + """Test evaluate with accelerate launcher arguments""" + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "evaluate", + str(config_path), + "--launcher", + "accelerate", + "--", + "--config_file=accelerate_config.yml", + "--num_processes=4", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify launcher args are passed to accelerate + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "accelerate" + assert called_cmd[1] == "launch" + assert "--config_file=accelerate_config.yml" in called_cmd + assert "--num_processes=4" in called_cmd + assert "-m" in called_cmd + assert "axolotl.cli.evaluate" in called_cmd + + def test_evaluate_backward_compatibility_no_launcher_args( + self, cli_runner, tmp_path, valid_test_config + ): + """Test that existing evaluate commands work without launcher args""" + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "evaluate", + str(config_path), + "--launcher", + "accelerate", + "--micro-batch-size", + "2", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify no launcher args contamination + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "accelerate" + assert called_cmd[1] == "launch" + # Should not contain any extra launcher args + launcher_section = called_cmd[2 : called_cmd.index("-m")] + assert ( + len(launcher_section) == 0 + ) # No launcher args between 'launch' and '-m' diff --git a/tests/cli/test_cli_inference.py b/tests/cli/test_cli_inference.py index b8effa3d20..3394c189d5 100644 --- a/tests/cli/test_cli_inference.py +++ b/tests/cli/test_cli_inference.py @@ -1,5 +1,7 @@ """pytest tests for axolotl CLI inference command.""" +# pylint: disable=duplicate-code + from unittest.mock import patch from axolotl.cli.main import cli @@ -10,7 +12,7 @@ def test_inference_basic(cli_runner, config_path): with patch("axolotl.cli.inference.do_inference") as mock: result = cli_runner.invoke( cli, - ["inference", str(config_path), "--no-accelerate"], + ["inference", str(config_path), "--launcher", "python"], catch_exceptions=False, ) @@ -23,9 +25,124 @@ def test_inference_gradio(cli_runner, config_path): with patch("axolotl.cli.inference.do_inference_gradio") as mock: result = cli_runner.invoke( cli, - ["inference", str(config_path), "--no-accelerate", "--gradio"], + ["inference", str(config_path), "--launcher", "python", "--gradio"], catch_exceptions=False, ) assert mock.called assert result.exit_code == 0 + + +def test_inference_with_launcher_args_torchrun(cli_runner, config_path): + """Test inference with torchrun launcher arguments""" + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "inference", + str(config_path), + "--launcher", + "torchrun", + "--", + "--nproc_per_node=2", + "--nnodes=1", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify launcher args are passed to torchrun + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "torchrun" + assert "--nproc_per_node=2" in called_cmd + assert "--nnodes=1" in called_cmd + assert "-m" in called_cmd + assert "axolotl.cli.inference" in called_cmd + + +def test_inference_with_launcher_args_accelerate(cli_runner, config_path): + """Test inference with accelerate launcher arguments""" + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "inference", + str(config_path), + "--launcher", + "accelerate", + "--", + "--config_file=accelerate_config.yml", + "--num_processes=4", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify launcher args are passed to accelerate + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "accelerate" + assert called_cmd[1] == "launch" + assert "--config_file=accelerate_config.yml" in called_cmd + assert "--num_processes=4" in called_cmd + assert "-m" in called_cmd + assert "axolotl.cli.inference" in called_cmd + + +def test_inference_gradio_with_launcher_args(cli_runner, config_path): + """Test inference with gradio and launcher arguments""" + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "inference", + str(config_path), + "--launcher", + "accelerate", + "--gradio", + "--", + "--num_processes=2", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify both gradio flag and launcher args are present + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "accelerate" + assert called_cmd[1] == "launch" + assert "--num_processes=2" in called_cmd + assert "--gradio" in called_cmd + assert "-m" in called_cmd + assert "axolotl.cli.inference" in called_cmd + + +def test_inference_backward_compatibility_no_launcher_args(cli_runner, config_path): + """Test that existing inference commands work without launcher args""" + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "inference", + str(config_path), + "--launcher", + "accelerate", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify no launcher args contamination + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "accelerate" + assert called_cmd[1] == "launch" + # Should not contain any extra launcher args + launcher_section = called_cmd[2 : called_cmd.index("-m")] + assert len(launcher_section) == 0 # No launcher args between 'launch' and '-m' diff --git a/tests/cli/test_cli_interface.py b/tests/cli/test_cli_interface.py index 8b5fec17f2..ebd91ea60e 100644 --- a/tests/cli/test_cli_interface.py +++ b/tests/cli/test_cli_interface.py @@ -18,11 +18,10 @@ def test_build_command(): assert result == [ "accelerate", "launch", - "--learning-rate", - "0.0001", - "--batch-size", - "8", - "--debug", + "--learning-rate=0.0001", + "--batch-size=8", + "--debug=True", + "--use-fp16=False", ] @@ -38,7 +37,7 @@ def test_invalid_command_options(cli_runner): ], ) assert result.exit_code != 0 - assert "No such option" in result.output + assert "does not exist" in result.output def test_required_config_argument(cli_runner): diff --git a/tests/cli/test_cli_merge_sharded_fsdp_weights.py b/tests/cli/test_cli_merge_sharded_fsdp_weights.py index ec96b4ed4f..4f6a973ea4 100644 --- a/tests/cli/test_cli_merge_sharded_fsdp_weights.py +++ b/tests/cli/test_cli_merge_sharded_fsdp_weights.py @@ -11,9 +11,101 @@ def test_merge_sharded_fsdp_weights_no_accelerate(cli_runner, config_path): """Test merge_sharded_fsdp_weights command without accelerate""" with patch("axolotl.cli.merge_sharded_fsdp_weights.do_cli") as mock: result = cli_runner.invoke( - cli, ["merge-sharded-fsdp-weights", str(config_path), "--no-accelerate"] + cli, + ["merge-sharded-fsdp-weights", str(config_path), "--launcher", "python"], ) assert mock.called assert mock.call_args.kwargs["config"] == str(config_path) assert result.exit_code == 0 + + +def test_merge_sharded_fsdp_weights_with_launcher_args_torchrun( + cli_runner, config_path +): + """Test merge-sharded-fsdp-weights with torchrun launcher arguments""" + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "merge-sharded-fsdp-weights", + str(config_path), + "--launcher", + "torchrun", + "--", + "--nproc_per_node=2", + "--nnodes=1", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify launcher args are passed to torchrun + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "torchrun" + assert "--nproc_per_node=2" in called_cmd + assert "--nnodes=1" in called_cmd + assert "-m" in called_cmd + assert "axolotl.cli.merge_sharded_fsdp_weights" in called_cmd + + +def test_merge_sharded_fsdp_weights_with_launcher_args_accelerate( + cli_runner, config_path +): + """Test merge-sharded-fsdp-weights with accelerate launcher arguments""" + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "merge-sharded-fsdp-weights", + str(config_path), + "--launcher", + "accelerate", + "--", + "--config_file=accelerate_config.yml", + "--num_processes=4", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify launcher args are passed to accelerate + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "accelerate" + assert called_cmd[1] == "launch" + assert "--config_file=accelerate_config.yml" in called_cmd + assert "--num_processes=4" in called_cmd + assert "-m" in called_cmd + assert "axolotl.cli.merge_sharded_fsdp_weights" in called_cmd + + +def test_merge_sharded_fsdp_weights_backward_compatibility_no_launcher_args( + cli_runner, config_path +): + """Test that existing merge-sharded-fsdp-weights commands work without launcher args""" + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "merge-sharded-fsdp-weights", + str(config_path), + "--launcher", + "accelerate", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify no launcher args contamination + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "accelerate" + assert called_cmd[1] == "launch" + # Should not contain any extra launcher args + launcher_section = called_cmd[2 : called_cmd.index("-m")] + assert len(launcher_section) == 0 # No launcher args between 'launch' and '-m' diff --git a/tests/cli/test_cli_sweeps.py b/tests/cli/test_cli_sweeps.py index 40b3607178..1b14f5aca0 100644 --- a/tests/cli/test_cli_sweeps.py +++ b/tests/cli/test_cli_sweeps.py @@ -2,7 +2,7 @@ unit tests for generating sweep configurations """ -from axolotl.cli.main import generate_sweep_configs +from axolotl.cli.utils import generate_sweep_configs def test_generate_sweep_configs_no_pairs(): diff --git a/tests/cli/test_cli_train.py b/tests/cli/test_cli_train.py index 4739135993..9b266f057a 100644 --- a/tests/cli/test_cli_train.py +++ b/tests/cli/test_cli_train.py @@ -1,5 +1,7 @@ """Tests for train CLI command.""" +# pylint: disable=duplicate-code + from unittest.mock import MagicMock, patch from axolotl.cli.main import cli @@ -18,7 +20,9 @@ def test_train_cli_validation(self, cli_runner): def test_train_basic_execution(self, cli_runner, tmp_path, valid_test_config): """Test basic successful execution""" - self._test_basic_execution(cli_runner, tmp_path, valid_test_config, "train") + self._test_basic_execution( + cli_runner, tmp_path, valid_test_config, "train", train=True + ) def test_train_basic_execution_no_accelerate( self, cli_runner, tmp_path, valid_test_config @@ -37,7 +41,8 @@ def test_train_basic_execution_no_accelerate( [ "train", str(config_path), - "--no-accelerate", + "--launcher", + "python", ], catch_exceptions=False, ) @@ -59,11 +64,10 @@ def test_train_cli_overrides(self, cli_runner, tmp_path, valid_test_config): [ "train", str(config_path), - "--learning-rate", - "1e-4", - "--micro-batch-size", - "2", - "--no-accelerate", + "--learning-rate=1e-4", + "--micro-batch-size=2", + "--launcher", + "python", ], catch_exceptions=False, ) @@ -73,3 +77,174 @@ def test_train_cli_overrides(self, cli_runner, tmp_path, valid_test_config): cfg = mock_train.call_args[1]["cfg"] assert cfg["learning_rate"] == 1e-4 assert cfg["micro_batch_size"] == 2 + + def test_train_with_launcher_args_torchrun( + self, cli_runner, tmp_path, valid_test_config + ): + """Test train with torchrun launcher arguments""" + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "train", + str(config_path), + "--launcher", + "torchrun", + "--", + "--nproc_per_node=2", + "--nnodes=1", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify launcher args are passed to torchrun + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "torchrun" + assert "--nproc_per_node=2" in called_cmd + assert "--nnodes=1" in called_cmd + assert "-m" in called_cmd + assert "axolotl.cli.train" in called_cmd + + def test_train_with_launcher_args_accelerate( + self, cli_runner, tmp_path, valid_test_config + ): + """Test train with accelerate launcher arguments""" + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "train", + str(config_path), + "--launcher", + "accelerate", + "--", + "--config_file=accelerate_config.yml", + "--num_processes=4", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify launcher args are passed to accelerate + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "accelerate" + assert called_cmd[1] == "launch" + assert "--config_file=accelerate_config.yml" in called_cmd + assert "--num_processes=4" in called_cmd + assert "-m" in called_cmd + assert "axolotl.cli.train" in called_cmd + + def test_train_backward_compatibility_no_launcher_args( + self, cli_runner, tmp_path, valid_test_config + ): + """Test that existing train commands work without launcher args""" + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "train", + str(config_path), + "--launcher", + "accelerate", + "--learning-rate", + "1e-4", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + # Verify no launcher args contamination + called_cmd = mock_subprocess.call_args.args[0] + assert called_cmd[0] == "accelerate" + assert called_cmd[1] == "launch" + # Should not contain any extra launcher args + launcher_section = called_cmd[2 : called_cmd.index("-m")] + assert ( + len(launcher_section) == 0 + ) # No launcher args between 'launch' and '-m' + + def test_train_mixed_args_with_launcher_args( + self, cli_runner, tmp_path, valid_test_config + ): + """Test train with both regular CLI args and launcher args""" + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + [ + "train", + str(config_path), + "--launcher", + "torchrun", + "--learning-rate", + "2e-4", + "--micro-batch-size", + "4", + "--", + "--nproc_per_node=8", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_subprocess.assert_called_once() + + called_cmd = mock_subprocess.call_args.args[0] + # Verify launcher args + assert "--nproc_per_node=8" in called_cmd + # Verify axolotl args are also present + assert "--learning-rate=2e-4" in called_cmd + assert "--micro-batch-size=4" in called_cmd + + def test_train_cloud_with_launcher_args( + self, cli_runner, tmp_path, valid_test_config + ): + """Test train with cloud and launcher arguments""" + config_path = tmp_path / "config.yml" + config_path.write_text(valid_test_config) + + cloud_path = tmp_path / "cloud.yml" + cloud_path.write_text("provider: modal\ngpu: a100") + + with patch("axolotl.cli.cloud.do_cli_train") as mock_cloud_train: + result = cli_runner.invoke( + cli, + [ + "train", + str(config_path), + "--cloud", + str(cloud_path), + "--launcher", + "torchrun", + "--", + "--nproc_per_node=4", + "--nnodes=2", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + mock_cloud_train.assert_called_once() + + # Verify cloud training was called with launcher args + call_kwargs = mock_cloud_train.call_args.kwargs + assert call_kwargs["launcher"] == "torchrun" + assert call_kwargs["launcher_args"] == ["--nproc_per_node=4", "--nnodes=2"] diff --git a/tests/cli/test_utils.py b/tests/cli/test_utils.py index 2dab5bba9c..a3e4e98870 100644 --- a/tests/cli/test_utils.py +++ b/tests/cli/test_utils.py @@ -72,3 +72,160 @@ def test_fetch_from_github_network_error(): with patch("requests.get", side_effect=requests.RequestException): with pytest.raises(requests.RequestException): fetch_from_github("examples/", None) + + +def assert_launcher_args_in_command( + mock_subprocess_call, + launcher: str, + expected_launcher_args: list[str], + command_module: str, +): + """ + Helper function to verify launcher arguments are properly passed in subprocess calls. + + Args: + mock_subprocess_call: The mock subprocess.run call + launcher: Expected launcher ("accelerate", "torchrun", etc.) + expected_launcher_args: List of expected launcher arguments + command_module: Expected module name (e.g., "axolotl.cli.train") + """ + assert mock_subprocess_call.called, "subprocess.run should have been called" + called_cmd = mock_subprocess_call.call_args.args[0] + + # Verify launcher + assert ( + called_cmd[0] == launcher + ), f"Expected launcher {launcher}, got {called_cmd[0]}" + + # Verify launcher args are present + for arg in expected_launcher_args: + assert ( + arg in called_cmd + ), f"Expected launcher arg '{arg}' not found in command: {called_cmd}" + + # Verify module is present + assert "-m" in called_cmd, "Expected -m flag for module execution" + assert ( + command_module in called_cmd + ), f"Expected module {command_module} not found in command: {called_cmd}" + + +def assert_no_launcher_args_contamination(mock_subprocess_call, launcher: str): + """ + Helper function to verify no unwanted launcher arguments are present. + + Args: + mock_subprocess_call: The mock subprocess.run call + launcher: Expected launcher ("accelerate", "torchrun", etc.) + """ + assert mock_subprocess_call.called, "subprocess.run should have been called" + called_cmd = mock_subprocess_call.call_args.args[0] + + if launcher == "accelerate": + # For accelerate, launcher args should be between 'launch' and '-m' + launch_idx = called_cmd.index("launch") + m_idx = called_cmd.index("-m") + launcher_section = called_cmd[launch_idx + 1 : m_idx] + assert ( + len(launcher_section) == 0 + ), f"Unexpected launcher args found: {launcher_section}" + elif launcher == "torchrun": + # For torchrun, launcher args should be between 'torchrun' and '-m' + torchrun_idx = called_cmd.index("torchrun") + m_idx = called_cmd.index("-m") + launcher_section = called_cmd[torchrun_idx + 1 : m_idx] + assert ( + len(launcher_section) == 0 + ), f"Unexpected launcher args found: {launcher_section}" + + +@pytest.fixture +def common_launcher_args(): + """Fixture providing common launcher argument combinations for testing.""" + return { + "torchrun": ["--nproc_per_node=2", "--nnodes=1"], + "accelerate": ["--config_file=accelerate_config.yml", "--num_processes=4"], + } + + +def test_add_default_rdzv_args_with_endpoint(): + """Test that default RDZV args are added when rdzv_endpoint is present.""" + from axolotl.cli.utils.train import _add_default_rdzv_args + + launcher_args = ["--nnodes=2", "--rdzv_endpoint=127.0.0.1:29400"] + result = _add_default_rdzv_args(launcher_args) + + # Should have added rdzv_backend + assert "--rdzv_backend" in result + assert "c10d" in result + + # Original args should still be present + assert "--nnodes=2" in result + assert "--rdzv_endpoint=127.0.0.1:29400" in result + + +def test_add_default_rdzv_args_with_existing_backend(): + """Test that existing rdzv_backend is not overridden.""" + from axolotl.cli.utils.train import _add_default_rdzv_args + + launcher_args = [ + "--nnodes=2", + "--rdzv_endpoint=127.0.0.1:29400", + "--rdzv_backend=static", + ] + result = _add_default_rdzv_args(launcher_args) + + # Should not add another rdzv_backend + backend_count = sum(1 for arg in result if "--rdzv_backend" in arg) + assert backend_count == 1 + assert "--rdzv_backend=static" in result + + +def test_add_default_rdzv_args_with_existing_id(): + """Test that existing rdzv_id is not overridden.""" + from axolotl.cli.utils.train import _add_default_rdzv_args + + launcher_args = [ + "--nnodes=2", + "--rdzv_endpoint=127.0.0.1:29400", + "--rdzv_id=my_job_123", + ] + result = _add_default_rdzv_args(launcher_args) + + # Should not add another rdzv_id + id_count = sum(1 for arg in result if "--rdzv_id" in arg) + assert id_count == 1 + assert "--rdzv_id=my_job_123" in result + + # Should still add rdzv_backend + assert "--rdzv_backend" in result + assert "c10d" in result + + +def test_add_default_rdzv_args_without_endpoint(): + """Test that no RDZV args are added when rdzv_endpoint is not present.""" + from axolotl.cli.utils.train import _add_default_rdzv_args + + launcher_args = ["--nnodes=2", "--nproc_per_node=4"] + result = _add_default_rdzv_args(launcher_args) + + # Should not add any rdzv args + assert "--rdzv_backend" not in result + assert result == launcher_args + + +def test_add_default_rdzv_args_with_all_existing(): + """Test that no defaults are added when all RDZV args are present.""" + from axolotl.cli.utils.train import _add_default_rdzv_args + + launcher_args = [ + "--nnodes=2", + "--rdzv_endpoint=127.0.0.1:29400", + "--rdzv_backend=static", + "--rdzv_id=existing_job", + ] + result = _add_default_rdzv_args(launcher_args) + + # Should not add any additional args + assert len(result) == len(launcher_args) + assert result == launcher_args From 09dda462ab58dd7bafa01b31fb334cc78cde9507 Mon Sep 17 00:00:00 2001 From: salman Date: Thu, 31 Jul 2025 16:12:41 +0100 Subject: [PATCH 0862/1405] Fix don't preview docs for contributors (#2994) [skip ci] * checking against fork vs. main repo * force doc preview --- .github/workflows/preview-docs.yml | 4 ++-- index.qmd | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 2393c3d4cf..db4abddce5 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -53,7 +53,7 @@ jobs: - name: Netlify Publish uses: nwtgck/actions-netlify@v3.0 - if: ${{ secrets.NETLIFY_AUTH_TOKEN != '' }} + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} id: netlify with: publish-dir: './_site' @@ -68,7 +68,7 @@ jobs: NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - name: Update PR with preview link - if: ${{ steps.netlify.outcome == 'success' && secrets.NETLIFY_AUTH_TOKEN != '' }} + if: ${{ steps.netlify.outcome == 'success' }} uses: marocchino/sticky-pull-request-comment@v2 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/index.qmd b/index.qmd index 01572a8bec..a178f4c7b6 100644 --- a/index.qmd +++ b/index.qmd @@ -4,6 +4,8 @@ # toc-expand: 2 --- + +test test ```{python} #|output: asis #|echo: false From 6ec282094dd61ab6f574d4d080639a31eaa88a6a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 31 Jul 2025 11:13:15 -0400 Subject: [PATCH 0863/1405] actually call the register method on plugins (#2991) [skip ci] --- src/axolotl/cli/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index ae9f1f9c47..da933929ee 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -159,6 +159,9 @@ def plugin_set_cfg(cfg: DictDefault): if cfg.get("plugins"): plugin_manager = PluginManager.get_instance() plugin_manager.cfg = cfg + # now that we have the finalized cfg, register the plugins individually + for plugin in plugin_manager.plugins.values(): + plugin.register(cfg) def load_cfg( From 563f5eed7a77370602526a88fa89ef3adc3c2759 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 31 Jul 2025 11:17:17 -0400 Subject: [PATCH 0864/1405] update dependencies - liger + trl (#2987) * update dependencies * set dataset processes for tests * add support for GSPO --- requirements.txt | 4 ++-- src/axolotl/core/trainers/grpo/__init__.py | 5 +++++ src/axolotl/utils/schemas/trl.py | 8 ++++++++ tests/e2e/patched/test_activation_checkpointing.py | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8e473bf6bb..ae433193f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 autoawq==0.2.7.post3 -liger-kernel==0.6.0 +liger-kernel==0.6.1 # END section packaging==23.2 @@ -18,7 +18,7 @@ tokenizers>=0.21.1 accelerate==1.9.0 datasets==4.0.0 deepspeed>=0.17.0 -trl==0.19.1 +trl==0.20.0 hf_xet==1.1.5 optimum==1.16.2 diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 2c6eb8c6fa..5f8e4a8b3d 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -82,6 +82,11 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: grpo_args_kwargs["log_completions"] = trl.log_completions grpo_args_kwargs["num_completions_to_print"] = trl.num_completions_to_print + if trl.importance_sampling_level is not None: + grpo_args_kwargs["importance_sampling_level"] = ( + trl.importance_sampling_level + ) + if cfg.sequence_parallel_degree > 1: grpo_args_kwargs["sequence_parallel_degree"] = cfg.sequence_parallel_degree diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index e4d17bc947..980474e87e 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -80,6 +80,14 @@ class TRLConfig(BaseModel): "description": "Number of completions to print when log_completions is True." }, ) + importance_sampling_level: Literal["sequence", "token"] | None = Field( + default=None, + json_schema_extra={ + "description": "Controls whether importance sampling ratios are computed at the `'token'` or `'sequence'` level. " + "For GSPO, use `sequence`, default is None which corresponds to the original GRPO paper." + }, + ) + sync_ref_model: bool | None = Field( default=False, json_schema_extra={"description": "Whether to sync the reference model."}, diff --git a/tests/e2e/patched/test_activation_checkpointing.py b/tests/e2e/patched/test_activation_checkpointing.py index 3d5b3dc56c..06e3de2740 100644 --- a/tests/e2e/patched/test_activation_checkpointing.py +++ b/tests/e2e/patched/test_activation_checkpointing.py @@ -70,6 +70,7 @@ def test_activation_checkpointing_offload( "save_safetensors": True, "gradient_checkpointing": gradient_checkpointing, "save_first_step": False, + "dataset_processes": 4, } ) From 32a78902315ddeef6b32d0f18a4a37dfa7333038 Mon Sep 17 00:00:00 2001 From: salman Date: Thu, 31 Jul 2025 16:46:31 +0100 Subject: [PATCH 0865/1405] Revert test update to index.qmd (#2995) [skip ci] --- index.qmd | 2 -- 1 file changed, 2 deletions(-) diff --git a/index.qmd b/index.qmd index a178f4c7b6..01572a8bec 100644 --- a/index.qmd +++ b/index.qmd @@ -4,8 +4,6 @@ # toc-expand: 2 --- - -test test ```{python} #|output: asis #|echo: false From 7b68dfafd71a9b3caafdbed5e3c1eb12daeef3d8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 31 Jul 2025 13:50:03 -0400 Subject: [PATCH 0866/1405] jagged lr restart scheudler (#1680) [skip ci] * jagged lr restart scheudler var name fix make sure to create scheduler first * wire things together * more fixes * fix for nesting scheduler and first anneal phase * no need for relora trainer anymore since we've generalized the relora scheduler * remove redundant relora scheduler and lint * update relora e2e test for updated params * need restart steps for relora test * update quarto docs for dropped relora trainer * update example yaml * drop verbose arg * min lr scale support for jagged lr * don't let min_lr be nonetype * cleanup args --- _quarto.yml | 1 - examples/llama-2/relora.yml | 7 ++- src/axolotl/core/builders/causal.py | 28 +++++----- src/axolotl/core/trainers/__init__.py | 1 - src/axolotl/core/trainers/mixins/scheduler.py | 21 +++++++- src/axolotl/core/trainers/relora.py | 46 ----------------- src/axolotl/core/training_args_base.py | 22 +++++--- src/axolotl/monkeypatch/relora.py | 51 +------------------ src/axolotl/train.py | 2 +- src/axolotl/utils/schedulers.py | 48 +++++++++++++++++ src/axolotl/utils/schemas/config.py | 3 +- src/axolotl/utils/schemas/peft.py | 12 +---- src/axolotl/utils/schemas/training.py | 21 ++++++++ src/axolotl/utils/schemas/validation.py | 4 +- tests/e2e/solo/test_relora_llama.py | 7 +-- 15 files changed, 138 insertions(+), 136 deletions(-) delete mode 100644 src/axolotl/core/trainers/relora.py diff --git a/_quarto.yml b/_quarto.yml index 250596d525..bfef13afb4 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -59,7 +59,6 @@ quartodoc: - core.trainers.base - core.trainers.trl - core.trainers.mamba - - core.trainers.relora - core.trainers.dpo.trainer - core.trainers.grpo.trainer - core.trainers.grpo.sampler diff --git a/examples/llama-2/relora.yml b/examples/llama-2/relora.yml index b0e9053407..fabdf0e0f8 100644 --- a/examples/llama-2/relora.yml +++ b/examples/llama-2/relora.yml @@ -25,9 +25,12 @@ lora_alpha: 16 lora_dropout: 0.05 lora_target_linear: true -relora_steps: 150 -relora_warmup_ratio: 0.1 +relora: true +relora_prune_ratio: 0.9 relora_cpu_offload: false +jagged_restart_steps: 150 +jagged_restart_warmup_steps: 10 +jagged_restart_anneal_steps: false wandb_project: wandb_entity: diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 00cee35a72..b461e90092 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -19,7 +19,6 @@ AxolotlPRMTrainer, AxolotlRewardTrainer, AxolotlTrainer, - ReLoRATrainer, ) from axolotl.integrations.base import PluginManager from axolotl.monkeypatch.multipack import SUPPORTED_MULTIPACK_MODEL_TYPES @@ -58,7 +57,7 @@ class HFCausalTrainerBuilder(TrainerBuilderBase): def get_callbacks(self): callbacks = super().get_callbacks() - if self.cfg.relora_steps: + if self.cfg.relora: callbacks.append(ReLoRACallback(self.cfg)) if ( @@ -131,8 +130,6 @@ def _get_trainer_cls(self): trainer_cls = plugin_manager.get_trainer_cls(self.cfg) if trainer_cls: return trainer_cls - if self.cfg.relora_steps: - return ReLoRATrainer if self.cfg.model_config_type == "mamba": return AxolotlMambaTrainer if self.cfg.reward_model: @@ -271,20 +268,25 @@ def build(self, total_num_steps): self.cfg.sample_packing_eff_est ) - if self.cfg.relora_steps: - training_arguments_kwargs["relora_steps"] = self.cfg.relora_steps - training_arguments_kwargs["relora_warmup_steps"] = ( - self.cfg.relora_warmup_steps - ) - if self.cfg.relora_anneal_steps: - training_arguments_kwargs["relora_anneal_steps"] = ( - self.cfg.relora_anneal_steps - ) + if self.cfg.relora and self.cfg.jagged_restart_steps: if self.cfg.relora_prune_ratio: training_arguments_kwargs["relora_prune_ratio"] = ( self.cfg.relora_prune_ratio ) + if self.cfg.jagged_restart_steps: + training_arguments_kwargs["jagged_restart_steps"] = ( + self.cfg.jagged_restart_steps + ) + if self.cfg.jagged_restart_warmup_steps: + training_arguments_kwargs["jagged_restart_warmup_steps"] = ( + self.cfg.jagged_restart_warmup_steps + ) + if self.cfg.jagged_restart_anneal_steps: + training_arguments_kwargs["jagged_restart_anneal_steps"] = ( + self.cfg.jagged_restart_anneal_steps + ) + if self.cfg.lisa_step_interval and self.cfg.lisa_n_layers: training_arguments_kwargs["lisa_n_layers"] = self.cfg.lisa_n_layers training_arguments_kwargs["lisa_step_interval"] = ( diff --git a/src/axolotl/core/trainers/__init__.py b/src/axolotl/core/trainers/__init__.py index 46b9b15ed0..5f97e387a5 100644 --- a/src/axolotl/core/trainers/__init__.py +++ b/src/axolotl/core/trainers/__init__.py @@ -7,7 +7,6 @@ from .dpo.trainer import AxolotlDPOTrainer from .grpo.trainer import AxolotlGRPOSequenceParallelTrainer, AxolotlGRPOTrainer from .mamba import AxolotlMambaTrainer -from .relora import ReLoRATrainer from .trl import ( AxolotlCPOTrainer, AxolotlKTOTrainer, diff --git a/src/axolotl/core/trainers/mixins/scheduler.py b/src/axolotl/core/trainers/mixins/scheduler.py index 90070ab78a..399bf59470 100644 --- a/src/axolotl/core/trainers/mixins/scheduler.py +++ b/src/axolotl/core/trainers/mixins/scheduler.py @@ -7,6 +7,7 @@ from axolotl.integrations.base import PluginManager from axolotl.utils.logging import get_logger from axolotl.utils.schedulers import ( + JaggedLRRestartScheduler, RexLR, get_cosine_schedule_with_min_lr, get_cosine_schedule_with_quadratic_warmup, @@ -113,7 +114,7 @@ def create_scheduler( min_lr_ratio=self.args.cosine_min_lr_ratio, ) else: - return super().create_scheduler(num_training_steps, optimizer=optimizer) + super().create_scheduler(num_training_steps, optimizer=optimizer) else: if use_cosine_quadratic: LOG.warning( @@ -123,4 +124,22 @@ def create_scheduler( LOG.warning( "axolotl's cosine scheduler with min lr not used (e.g., because of deepspeed).") + if self.args.jagged_restart_steps: + warmup_steps = ( + self.args.jagged_restart_warmup_steps or 10 + ) + anneal_steps = ( + self.args.jagged_restart_anneal_steps or 1 + ) + if not self.lr_scheduler: + super().create_scheduler(num_training_steps, optimizer) + self.lr_scheduler = JaggedLRRestartScheduler( # pylint: disable=attribute-defined-outside-init + optimizer, + self.lr_scheduler, + self.args.jagged_restart_steps, + warmup_steps, + anneal_steps, + min_lr_scale=self.args.cosine_min_lr_ratio or 0.001, + ) + return self.lr_scheduler # type: ignore diff --git a/src/axolotl/core/trainers/relora.py b/src/axolotl/core/trainers/relora.py deleted file mode 100644 index 890278f494..0000000000 --- a/src/axolotl/core/trainers/relora.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Module for ReLoRA trainer""" - -import torch -from torch.optim.lr_scheduler import LRScheduler - -from axolotl.core.trainers.base import AxolotlTrainer -from axolotl.monkeypatch.relora import ReLoRAScheduler - - -class ReLoRATrainer(AxolotlTrainer): - """Trainer subclass that uses the `OneCycleLR` scheduler""" - - tag_names = ["axolotl", "relora"] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.lr_scheduler = None - - def create_scheduler( - self, - num_training_steps: int, - optimizer: torch.optim.Optimizer | None = None, - ) -> LRScheduler: - optimizer = self.optimizer if optimizer is None else optimizer - lr_scheduler: LRScheduler = super().create_scheduler( - num_training_steps, optimizer - ) - - if self.args.relora_steps: - warmup_steps = ( - self.args.relora_warmup_steps if self.args.relora_warmup_steps else 10 - ) - anneal_steps = ( - self.args.relora_anneal_steps if self.args.relora_anneal_steps else 1 - ) - self.lr_scheduler = ReLoRAScheduler( # type: ignore - optimizer, - lr_scheduler, - self.args.relora_steps, - anneal_steps, - warmup_steps, - ) - else: - self.lr_scheduler = lr_scheduler # type: ignore - - return self.lr_scheduler # type: ignore diff --git a/src/axolotl/core/training_args_base.py b/src/axolotl/core/training_args_base.py index 4b74676ced..66649deefd 100644 --- a/src/axolotl/core/training_args_base.py +++ b/src/axolotl/core/training_args_base.py @@ -82,17 +82,25 @@ class AxolotlTrainingMixins: default=None, metadata={"help": "how often to reset for ReLoRA"}, ) - relora_warmup_steps: Optional[int] = field( + relora_prune_ratio: Optional[float] = field( + default=0.9, + metadata={"help": "prune ratio for magnitude pruning of the optimizer"}, + ) + jagged_restart_steps: Optional[int] = field( default=None, - metadata={"help": "how many warmup steps to take after reset for ReLoRA"}, + metadata={"help": "how often to reset for jagged restarts"}, ) - relora_anneal_steps: Optional[int] = field( + jagged_restart_warmup_steps: Optional[int] = field( default=None, - metadata={"help": "how many warmup steps to take after reset for ReLoRA"}, + metadata={ + "help": "how many warmup steps to take after reset for jagged restarts" + }, ) - relora_prune_ratio: Optional[float] = field( - default=0.9, - metadata={"help": "prune ratio for magnitude pruning of the optimizer"}, + jagged_restart_anneal_steps: Optional[int] = field( + default=None, + metadata={ + "help": "how many anneal steps to take before reset for jagged restarts" + }, ) bench_split: Optional[str] = field( default="eval", metadata={"help": "The benchmark split to run on"} diff --git a/src/axolotl/monkeypatch/relora.py b/src/axolotl/monkeypatch/relora.py index 5b7418e39d..0028a0cf6e 100644 --- a/src/axolotl/monkeypatch/relora.py +++ b/src/axolotl/monkeypatch/relora.py @@ -6,7 +6,7 @@ import shutil from functools import partial from pathlib import Path -from typing import Dict, List, Sequence, Union +from typing import Dict, List, Union import bitsandbytes as bnb import peft @@ -14,8 +14,6 @@ import torch from huggingface_hub import snapshot_download from torch.distributed.optim import ZeroRedundancyOptimizer -from torch.optim.lr_scheduler import LRScheduler -from torch.optim.optimizer import Optimizer from transformers import ( TrainerCallback, TrainerControl, @@ -84,7 +82,7 @@ class ReLoRACallback(TrainerCallback): """Callback to merge LoRA weights into the base model and save full-weight checkpoints""" def __init__(self, cfg: DictDefault): - self.relora_steps = cfg.relora_steps + self.relora_steps = cfg.jagged_restart_steps self.cpu_offload = cfg.relora_cpu_offload self.quantized = cfg.load_in_4bit or cfg.load_in_8bit self.last_full_model = cfg.base_model @@ -255,51 +253,6 @@ def on_train_end( return control -class ReLoRAScheduler(LRScheduler): - """Wraps another scheduler to apply per-lora-restart learning rate warmups.""" - - def __init__( - self, - optimizer: Optimizer, - inner_schedule: LRScheduler, - relora_steps: int, - warmup_steps: int, - anneal_steps: int = 1, - min_lr_scale: float = 0.001, - ) -> None: - self.inner_schedule = inner_schedule - self.relora_steps = relora_steps - self.warmup_steps = warmup_steps - self.anneal_steps = anneal_steps - self.min_lr_scale = min_lr_scale - super().__init__(optimizer, inner_schedule.last_epoch) - - def get_lr(self) -> float: - self.inner_schedule.last_epoch = self.last_epoch - - original = self.inner_schedule.get_lr() - step = self.last_epoch - - if step < self.relora_steps - self.warmup_steps: - scale = 1 - else: - per_relora_progress = step % self.relora_steps - if per_relora_progress < self.warmup_steps: - cycle_t = min(1.0, (per_relora_progress) / self.warmup_steps) - elif per_relora_progress > (self.relora_steps - self.anneal_steps): - cycle_t = min( - 1.0, - (self.relora_steps - per_relora_progress) / self.anneal_steps, - ) - else: - cycle_t = 1 - scale = cycle_t * (1 - self.min_lr_scale) + self.min_lr_scale - - if isinstance(original, Sequence): - return [lr * scale for lr in original] - return original * scale - - def sharded_paths(path: str, module_names: List[str]) -> Dict[str, str]: model_name = "model.safetensors" if not os.path.exists(str(Path(path) / model_name)) and not os.path.exists( diff --git a/src/axolotl/train.py b/src/axolotl/train.py index d57cb463ef..b507c2c7b1 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -267,7 +267,7 @@ def save_trained_model( "your model weights with `axolotl quantize`." ) # Handle ReLoRA early return case - if cfg.relora_steps: + if cfg.relora: if cfg.adapter == "lora" and not (cfg.load_in_4bit or cfg.load_in_8bit): model = model.merge_and_unload() else: diff --git a/src/axolotl/utils/schedulers.py b/src/axolotl/utils/schedulers.py index b550ac02c6..b9d09ad9c0 100644 --- a/src/axolotl/utils/schedulers.py +++ b/src/axolotl/utils/schedulers.py @@ -2,6 +2,7 @@ import math from functools import partial +from typing import Sequence from torch.optim import Optimizer from torch.optim.lr_scheduler import LambdaLR, LRScheduler @@ -292,3 +293,50 @@ def get_cosine_schedule_with_warmup_decay_constant( num_cycles=num_cycles, ) return LambdaLR(optimizer, lr_lambda, last_epoch) + + +class JaggedLRRestartScheduler(LRScheduler): + """Wraps another scheduler to apply per-lora-restart learning rate warmups.""" + + def __init__( + self, + optimizer: Optimizer, + inner_schedule: LRScheduler, + jagged_restart_steps: int, + jagged_restart_warmup_steps: int, + jagged_restart_anneal_steps: int = 1, + min_lr_scale: float = 0.001, + ) -> None: + # pylint: disable=duplicate-code + self.inner_schedule = inner_schedule + self.restarts_steps = jagged_restart_steps + self.warmup_steps = jagged_restart_warmup_steps + self.anneal_steps = jagged_restart_anneal_steps + self.min_lr_scale = min_lr_scale + super().__init__(optimizer, inner_schedule.last_epoch) + + def get_lr(self) -> float | Sequence[float]: + self.inner_schedule.last_epoch = self.last_epoch + + original = self.inner_schedule.get_lr() + step = self.last_epoch + + if step < self.restarts_steps - self.anneal_steps: + scale = 1 + else: + per_restart_progress = step % self.restarts_steps + if per_restart_progress < self.warmup_steps: + cycle_t = min(1.0, (per_restart_progress) / self.warmup_steps) + elif per_restart_progress > (self.restarts_steps - self.anneal_steps): + cycle_t = min( + 1.0, + (self.restarts_steps - per_restart_progress) / self.anneal_steps, + ) + else: + cycle_t = 1 + scale = cycle_t * (1 - self.min_lr_scale) + self.min_lr_scale + + if isinstance(original, Sequence): + return [lr * scale for lr in original] + + return original * scale diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 0afeaa2a8b..f8746692cc 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -43,7 +43,7 @@ from axolotl.utils.schemas.multimodal import MultiModalConfig from axolotl.utils.schemas.peft import LoraConfig, ReLoRAConfig from axolotl.utils.schemas.quantization import PTQConfig, QATConfig -from axolotl.utils.schemas.training import HyperparametersConfig +from axolotl.utils.schemas.training import HyperparametersConfig, JaggedLRConfig from axolotl.utils.schemas.trl import TRLConfig from axolotl.utils.schemas.validation import ValidationMixin from axolotl.utils.schemas.vllm import VllmConfig @@ -57,6 +57,7 @@ class AxolotlInputConfig( ModelOutputConfig, LoraConfig, ReLoRAConfig, + JaggedLRConfig, HyperparametersConfig, WandbConfig, MLFlowConfig, diff --git a/src/axolotl/utils/schemas/peft.py b/src/axolotl/utils/schemas/peft.py index 4b31ce0187..341397b429 100644 --- a/src/axolotl/utils/schemas/peft.py +++ b/src/axolotl/utils/schemas/peft.py @@ -187,18 +187,10 @@ def validate_lora_dropout(cls, data): class ReLoRAConfig(BaseModel): """ReLoRA configuration subset""" - relora_steps: int | None = Field( - default=None, - json_schema_extra={"description": "Number of steps per ReLoRA restart"}, - ) - relora_warmup_steps: int | None = Field( - default=None, - json_schema_extra={"description": "Number of per-restart warmup steps"}, - ) - relora_anneal_steps: int | None = Field( + relora: bool | None = Field( default=None, json_schema_extra={ - "description": "Number of anneal steps for each relora cycle" + "description": "Whether to use ReLoRA. Use with jagged_restart_*steps options." }, ) relora_prune_ratio: float | None = Field( diff --git a/src/axolotl/utils/schemas/training.py b/src/axolotl/utils/schemas/training.py index 4d88cc9e63..6ee8633975 100644 --- a/src/axolotl/utils/schemas/training.py +++ b/src/axolotl/utils/schemas/training.py @@ -160,3 +160,24 @@ def convert_learning_rate(cls, learning_rate): if learning_rate and isinstance(learning_rate, str): learning_rate = float(learning_rate) return learning_rate + + +class JaggedLRConfig(BaseModel): + """JaggedLR configuration subset, can be used w/ ReLoRA training""" + + jagged_restart_steps: int | None = Field( + default=None, + json_schema_extra={"description": "how often to reset for jagged restarts"}, + ) + jagged_restart_warmup_steps: int | None = Field( + default=None, + json_schema_extra={ + "description": "how many warmup steps to take after reset for jagged restarts" + }, + ) + jagged_restart_anneal_steps: int | None = Field( + default=None, + json_schema_extra={ + "description": "how many anneal steps to take before reset for jagged restarts" + }, + ) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 9ca33f456e..063690c599 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1164,7 +1164,9 @@ def check_simpo_warmup(self): @model_validator(mode="after") def check_relora(self): - if self.relora_steps: + if self.relora: + if not self.jagged_restart_steps: + raise ValueError("jagged_restart_steps must be set to use ReLoRA") if self.adapter not in ("lora", "qlora"): raise ValueError("cfg.adapter must be lora or qlora to use ReLoRA") diff --git a/tests/e2e/solo/test_relora_llama.py b/tests/e2e/solo/test_relora_llama.py index f6fcad8415..b399b4680b 100644 --- a/tests/e2e/solo/test_relora_llama.py +++ b/tests/e2e/solo/test_relora_llama.py @@ -34,9 +34,10 @@ def test_relora(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_modules": ["q_proj", "v_proj"], - "relora_steps": 50, - "relora_warmup_steps": 10, - "relora_anneal_steps": 10, + "relora": True, + "jagged_restart_steps": 50, + "jagged_restart_warmup_steps": 10, + "jagged_restart_anneal_steps": 10, "relora_prune_ratio": 0.9, "relora_cpu_offload": True, "val_set_size": 0.0, From 294c7fe7a6f9d0520d073170cad4defcdb5ea3f4 Mon Sep 17 00:00:00 2001 From: salman Date: Thu, 31 Jul 2025 20:25:02 +0100 Subject: [PATCH 0867/1405] Distributed/ND-Parallel (#2977) --- cicd/multigpu.sh | 2 +- cicd/single_gpu.py | 5 +- docs/sequence_parallelism.qmd | 12 +- examples/alst/llama3-8b-deepspeed-alst.yaml | 2 +- requirements.txt | 4 +- setup.py | 3 +- src/axolotl/cli/merge_lora.py | 2 +- src/axolotl/core/builders/base.py | 26 +- src/axolotl/core/builders/rl.py | 2 +- src/axolotl/core/trainers/base.py | 2 + src/axolotl/core/trainers/dpo/trainer.py | 13 +- src/axolotl/core/trainers/grpo/__init__.py | 6 +- src/axolotl/core/trainers/grpo/args.py | 2 +- src/axolotl/core/trainers/grpo/sampler.py | 12 +- src/axolotl/core/trainers/grpo/trainer.py | 39 +- src/axolotl/core/trainers/mamba.py | 1 + src/axolotl/core/trainers/mixins/__init__.py | 1 + .../core/trainers/mixins/checkpoints.py | 8 +- .../trainers/mixins/distributed_parallel.py | 20 + src/axolotl/core/trainers/trl.py | 37 +- src/axolotl/integrations/kd/trainer.py | 1 + src/axolotl/integrations/liger/args.py | 33 +- src/axolotl/loaders/model.py | 121 ++++- src/axolotl/loaders/patch_manager.py | 20 +- src/axolotl/monkeypatch/accelerate/fsdp2.py | 8 +- src/axolotl/monkeypatch/ring_attn/__init__.py | 8 +- src/axolotl/monkeypatch/ring_attn/patch.py | 189 ++----- .../monkeypatch/transformers/__init__.py | 0 .../modeling_flash_attention_utils.py | 87 ++++ src/axolotl/train.py | 4 +- src/axolotl/utils/bench.py | 45 +- src/axolotl/utils/callbacks/__init__.py | 21 +- .../utils/ctx_managers/sequence_parallel.py | 28 +- src/axolotl/utils/data/shared.py | 3 +- src/axolotl/utils/environment.py | 13 + src/axolotl/utils/samplers/multipack.py | 4 + src/axolotl/utils/schemas/config.py | 16 + src/axolotl/utils/schemas/validation.py | 105 ++-- src/axolotl/utils/trainer.py | 6 +- tests/core/test_builders.py | 2 +- tests/e2e/multigpu/patched/test_sp.py | 6 +- tests/e2e/multigpu/solo/test_grpo.py | 2 +- tests/e2e/multigpu/test_fp8_fsdp2.py | 3 +- tests/e2e/multigpu/test_tp.py | 69 +++ tests/e2e/patched/test_sp.py | 481 ------------------ tests/e2e/test_load_model.py | 2 + tests/e2e/utils.py | 4 + tests/test_loaders.py | 41 ++ tests/utils/schemas/validation/test_fsdp.py | 26 - 49 files changed, 712 insertions(+), 835 deletions(-) create mode 100644 src/axolotl/core/trainers/mixins/distributed_parallel.py create mode 100644 src/axolotl/monkeypatch/transformers/__init__.py create mode 100644 src/axolotl/monkeypatch/transformers/modeling_flash_attention_utils.py create mode 100644 tests/e2e/multigpu/test_tp.py delete mode 100644 tests/e2e/patched/test_sp.py diff --git a/cicd/multigpu.sh b/cicd/multigpu.sh index 4fd5672bea..3ec4456b97 100755 --- a/cicd/multigpu.sh +++ b/cicd/multigpu.sh @@ -2,7 +2,7 @@ set -e # Only run two tests at a time to avoid OOM on GPU (with coverage collection) -pytest -v -n2 \ +pytest -v --durations=10 -n2 \ --ignore=/workspace/axolotl/tests/e2e/multigpu/solo/ \ --ignore=/workspace/axolotl/tests/e2e/multigpu/patched/ \ /workspace/axolotl/tests/e2e/multigpu/ \ diff --git a/cicd/single_gpu.py b/cicd/single_gpu.py index 6955af0134..eb34e17489 100644 --- a/cicd/single_gpu.py +++ b/cicd/single_gpu.py @@ -65,6 +65,9 @@ def run_cmd(cmd: str, run_folder: str): import subprocess # nosec + sp_env = os.environ.copy() + sp_env["AXOLOTL_DATASET_PROCESSES"] = "8" + # Propagate errors from subprocess. - if exit_code := subprocess.call(cmd.split(), cwd=run_folder): # nosec + if exit_code := subprocess.call(cmd.split(), cwd=run_folder, env=sp_env): # nosec exit(exit_code) # pylint: disable=consider-using-sys-exit diff --git a/docs/sequence_parallelism.qmd b/docs/sequence_parallelism.qmd index b98206135a..d1933a145e 100644 --- a/docs/sequence_parallelism.qmd +++ b/docs/sequence_parallelism.qmd @@ -22,7 +22,7 @@ To enable sequence parallelism, add the following to your configuration file: ```yaml # Set to a divisor (> 1) of the number of GPUs available -sequence_parallel_degree: 4 # Split sequences across 4 GPUs +context_parallel_size: 4 # Split sequences across 4 GPUs # Optional; strides across the key dimension. Larger values use more memory but should make training faster. heads_k_stride: 1 # Optional; one of "varlen_llama3" or "batch_ring". Defaults to @@ -30,7 +30,7 @@ heads_k_stride: 1 ring_attn_func: ``` -The `sequence_parallel_degree` should be a divisor of the total number of GPUs. For example: +The `context_parallel_size` should be a divisor of the total number of GPUs. For example: - With 8 GPUs, valid values would be 2, 4, or 8 - With 4 GPUs, valid values would be 2 or 4 @@ -66,7 +66,7 @@ sequence_len: 8192 ... -sequence_parallel_degree: 4 # Split each sequence into 4 parts, one per GPU +context_parallel_size: 4 # Split each sequence into 4 parts, one per GPU # Optional; strides across the key dimension. Larger values use more memory but should make training faster. heads_k_stride: 1 # Optional; one of "varlen_llama3" or "batch_ring". Defaults to @@ -89,12 +89,12 @@ Sequence parallelism is compatible with Axolotl's sample packing functionality. ## Effect on Batch Size -When using sequence parallelism, your effective global batch size is **divided** by the `sequence_parallel_degree`. This happens because: +When using sequence parallelism, your effective global batch size is **divided** by the `context_parallel_size`. This happens because: -- Each group of `sequence_parallel_degree` GPUs works on the same batch (just different parts of each sequence) +- Each group of `context_parallel_size` GPUs works on the same batch (just different parts of each sequence) - The number of batches processed per step decreases For example: - With 8 GPUs and no sequence parallelism: 8 different batches processed per step -- With 8 GPUs and `sequence_parallel_degree=4`: Only 2 different batches processed per step (each split across 4 GPUs) +- With 8 GPUs and `context_parallel_size=4`: Only 2 different batches processed per step (each split across 4 GPUs) - If your per-GPU `micro_batch_size` is 2, the global batch size decreases from 16 to 4 diff --git a/examples/alst/llama3-8b-deepspeed-alst.yaml b/examples/alst/llama3-8b-deepspeed-alst.yaml index dc82fa3be3..dea23c5eed 100644 --- a/examples/alst/llama3-8b-deepspeed-alst.yaml +++ b/examples/alst/llama3-8b-deepspeed-alst.yaml @@ -20,7 +20,7 @@ min_sample_len: 200_000 sample_packing: true tiled_mlp: true -sequence_parallel_degree: 8 +context_parallel_size: 8 plugins: - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin diff --git a/requirements.txt b/requirements.txt index ae433193f7..4fc662a87d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,9 +13,9 @@ packaging==23.2 huggingface_hub>=0.33.0 peft==0.16.0 -transformers==4.54.0 +transformers==4.54.1 tokenizers>=0.21.1 -accelerate==1.9.0 +accelerate @ git+https://github.com/huggingface/accelerate.git@9359a0194f210624f1e6e85c3d838fdd55c11152 datasets==4.0.0 deepspeed>=0.17.0 trl==0.20.0 diff --git a/setup.py b/setup.py index 6576c44e52..de6f19e56a 100644 --- a/setup.py +++ b/setup.py @@ -72,12 +72,13 @@ def parse_requirements(extras_require_map): extras_require_map.pop("vllm") else: _install_requires.append("xformers==0.0.31") + extras_require_map["vllm"] = ["vllm>=0.10.0"] elif (major, minor) >= (2, 6): _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers==0.0.29.post3") # since we only support 2.6.0+cu126 _dependency_links.append("https://download.pytorch.org/whl/cu126") - extras_require_map["vllm"] = ["vllm==0.8.5.post1"] + extras_require_map.pop("vllm") elif (major, minor) >= (2, 5): _install_requires.pop(_install_requires.index(xformers_version)) if patch == 0: diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 422593a48d..31fad1b297 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -69,7 +69,7 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: load_in_8bit=False, load_in_4bit=False, flash_attention=False, - sequence_parallel_degree=None, + context_parallel_size=None, deepspeed=None, fsdp=None, fsdp_config=None, diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 0a37d27667..32b228e21d 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -24,9 +24,11 @@ from typing import Any import torch +from accelerate import PartialState from transformers import ( TrainerCallback, ) +from transformers.trainer_pt_utils import AcceleratorConfig from transformers.training_args import OptimizerNames from axolotl.integrations.base import PluginManager @@ -434,8 +436,30 @@ def _configure_torch_compile(self, training_args_kwargs: dict): training_args_kwargs["torch_compile_mode"] = self.cfg.torch_compile_mode def _configure_accelerator_config(self, training_args_kwargs: dict): + partial_state = PartialState() + has_pc_attr = ( + hasattr(partial_state, "parallelism_config") + and partial_state.parallelism_config + ) + has_pc_key = ( + "parallelism_config" + in partial_state._shared_state # pylint: disable=protected-access + and partial_state._shared_state[ # pylint: disable=protected-access + "parallelism_config" + ] + ) + use_configured_state = has_pc_attr or has_pc_key if self.cfg.accelerator_config: - training_args_kwargs["accelerator_config"] = self.cfg.accelerator_config + use_configured_state = self.cfg.accelerator_config.pop( + "use_configured_state", use_configured_state + ) + training_args_kwargs["accelerator_config"] = AcceleratorConfig( + use_configured_state=use_configured_state, **self.cfg.accelerator_config + ) + else: + training_args_kwargs["accelerator_config"] = AcceleratorConfig( + use_configured_state=use_configured_state, + ) def _configure_gradient_checkpointing(self, training_args_kwargs: dict): if self.cfg.activation_offloading is True: diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index e60b0e9581..8cc6eeebf9 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -53,7 +53,7 @@ def _get_trainer_cls(self, trainer_kwargs: dict): if self.cfg.rl is RLType.GRPO: trainer_cls = GRPOStrategy.get_trainer_class( - sequence_parallel=self.cfg.sequence_parallel_degree > 1 + sequence_parallel=self.cfg.context_parallel_size > 1 ) trainer_cls_args.extend(GRPOStrategy.set_trainer_args(self.cfg)) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 3dfaf47ce4..e3818ca7cc 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -27,6 +27,7 @@ from axolotl.core.trainers.mixins import ( ActivationOffloadingMixin, CheckpointSaveMixin, + DistributedParallelMixin, OptimizerMixin, PackingMixin, RngLoaderMixin, @@ -50,6 +51,7 @@ class AxolotlTrainer( RngLoaderMixin, CheckpointSaveMixin, ActivationOffloadingMixin, + DistributedParallelMixin, Trainer, ): """Extend the base Trainer for axolotl helpers""" diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index 762e0a331d..b3067bb462 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -8,7 +8,11 @@ from torch import nn from trl import DPOTrainer -from axolotl.core.trainers.mixins import RngLoaderMixin, SchedulerMixin +from axolotl.core.trainers.mixins import ( + DistributedParallelMixin, + RngLoaderMixin, + SchedulerMixin, +) from axolotl.core.trainers.mixins.optimizer import OptimizerInitMixin, OptimizerMixin from axolotl.core.trainers.utils import ( sanitize_kwargs_for_ds_tagging, @@ -17,7 +21,12 @@ class AxolotlDPOTrainer( - RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, DPOTrainer + RngLoaderMixin, + SchedulerMixin, + OptimizerMixin, + OptimizerInitMixin, + DPOTrainer, + DistributedParallelMixin, ): """Extend the base DPOTrainer for axolotl helpers.""" diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 5f8e4a8b3d..839c20c2e3 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -82,14 +82,14 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: grpo_args_kwargs["log_completions"] = trl.log_completions grpo_args_kwargs["num_completions_to_print"] = trl.num_completions_to_print + if cfg.context_parallel_size > 1: + grpo_args_kwargs["context_parallel_size"] = cfg.context_parallel_size + if trl.importance_sampling_level is not None: grpo_args_kwargs["importance_sampling_level"] = ( trl.importance_sampling_level ) - if cfg.sequence_parallel_degree > 1: - grpo_args_kwargs["sequence_parallel_degree"] = cfg.sequence_parallel_degree - if trl.reward_weights: grpo_args_kwargs["reward_weights"] = trl.reward_weights diff --git a/src/axolotl/core/trainers/grpo/args.py b/src/axolotl/core/trainers/grpo/args.py index 5c8b1a33b6..2ea52998ec 100644 --- a/src/axolotl/core/trainers/grpo/args.py +++ b/src/axolotl/core/trainers/grpo/args.py @@ -13,4 +13,4 @@ class AxolotlGRPOConfig(AxolotlTrainingMixins, GRPOConfig): """Axolotl GRPO Config for GRPO training""" - sequence_parallel_degree: int | None = None + context_parallel_size: int | None = None diff --git a/src/axolotl/core/trainers/grpo/sampler.py b/src/axolotl/core/trainers/grpo/sampler.py index ebc6e19e2a..df679a6d2f 100644 --- a/src/axolotl/core/trainers/grpo/sampler.py +++ b/src/axolotl/core/trainers/grpo/sampler.py @@ -20,7 +20,7 @@ class SequenceParallelRepeatRandomSampler(Sampler): - Data is properly distributed across SP groups. In the table below, the values represent dataset indices. Each SP group has - `sequence_parallel_degree = 2` GPUs working together on the same data. There are 2 + `context_parallel_size = 2` GPUs working together on the same data. There are 2 SP groups (SP0 and SP1), with `world_size = 4` total GPUs. Sequence Parallel Groups @@ -45,7 +45,7 @@ class SequenceParallelRepeatRandomSampler(Sampler): rank: Rank of current process. batch_size: Number of samples per batch. repeat_count: How many times to repeat the full sampling process. - sequence_parallel_degree: Number of ranks in a sequence parallel group. + context_parallel_size: Number of ranks in a sequence parallel group. shuffle: Whether to shuffle the dataset. seed: Random seed for shuffling. drop_last: Whether to drop the last incomplete batch. @@ -59,7 +59,7 @@ def __init__( rank: int, batch_size: int = 1, repeat_count: int = 1, - sequence_parallel_degree: int = 1, + context_parallel_size: int = 1, shuffle: bool = True, seed: int = 0, drop_last: bool = False, @@ -77,9 +77,9 @@ def __init__( self.rank = rank # Sequence parallelism parameters - self.sequence_parallel_degree = sequence_parallel_degree - self.num_sp_groups = world_size // sequence_parallel_degree - self.sp_group_id = rank // sequence_parallel_degree + self.context_parallel_size = context_parallel_size + self.num_sp_groups = world_size // context_parallel_size + self.sp_group_id = rank // context_parallel_size # Adjust dataset size for distributed sampling self.num_samples = len(self.dataset) diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index 70b3cf3b53..49caa64067 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -43,7 +43,11 @@ from trl.trainer.utils import pad from axolotl.core.trainers.grpo.sampler import SequenceParallelRepeatRandomSampler -from axolotl.core.trainers.mixins import RngLoaderMixin, SchedulerMixin +from axolotl.core.trainers.mixins import ( + DistributedParallelMixin, + RngLoaderMixin, + SchedulerMixin, +) from axolotl.core.trainers.mixins.optimizer import OptimizerInitMixin, OptimizerMixin from axolotl.monkeypatch.ring_attn import get_ring_attn_group @@ -53,7 +57,12 @@ class AxolotlGRPOTrainer( - RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, GRPOTrainer + RngLoaderMixin, + SchedulerMixin, + OptimizerMixin, + OptimizerInitMixin, + DistributedParallelMixin, + GRPOTrainer, ): """Extend the base GRPOTrainer for axolotl helpers""" @@ -100,7 +109,7 @@ def __init__( # Get number of SP groups (number of processes divided by SP degree) num_processes = self.accelerator.num_processes - num_sp_groups = num_processes // self.args.sequence_parallel_degree + num_sp_groups = num_processes // self.args.context_parallel_size # Calculate batch size per SP group (not per process) sp_group_batch_size = self.args.per_device_train_batch_size * num_sp_groups @@ -130,7 +139,7 @@ def __init__( if self.num_generations not in possible_values: raise ValueError( - f"With sequence parallelism (degree {self.args.sequence_parallel_degree}), " + f"With sequence parallelism (degree {self.args.context_parallel_size}), " f"the eval batch size per SP group ({num_sp_groups} x {self.args.per_device_eval_batch_size}) " f"must be evenly divisible by the number of generations per prompt " f"({self.num_generations}). Given the current eval batch size, " @@ -167,9 +176,9 @@ def _get_train_sampler(self) -> Sampler: rank=self.rank, batch_size=effective_batch_size // self.num_generations - // self.args.sequence_parallel_degree, + // self.args.context_parallel_size, repeat_count=self.num_iterations * self.args.gradient_accumulation_steps, - sequence_parallel_degree=self.args.sequence_parallel_degree, + context_parallel_size=self.args.context_parallel_size, shuffle=True, seed=self.args.seed, drop_last=True, @@ -235,7 +244,7 @@ def _prepare_dataloader( # TODO(djsaunde): We might be able to use `accelerate`'s dataloader preparation # if we use `dispatch_batches` and `slice_fn_for_dispatch` properly (i.e., # slice each batch along the sequence dimension). - if self.args.sequence_parallel_degree > 1: + if self.args.context_parallel_size > 1: return dataloader # Otherwise prepare with accelerator @@ -308,18 +317,18 @@ def _generate_and_score_completions( # Generate completions using vLLM: gather all prompts and use them in a single call in the main process all_prompts_text = gather_object(prompts_text) if self.accelerator.is_main_process: - if self.args.sequence_parallel_degree > 1: + if self.args.context_parallel_size > 1: # Calculate sequence parallel group information world_size = self.accelerator.num_processes - sequence_parallel_degree = self.args.sequence_parallel_degree - num_sp_groups = world_size // sequence_parallel_degree + context_parallel_size = self.args.context_parallel_size + num_sp_groups = world_size // context_parallel_size # Since processes in the same SP group have the same prompts, we need to ensure # we only take one copy of each prompt from each SP group ordered_set_of_prompts = [] for sp_group_id in range(num_sp_groups): # Get the first process from each SP group (typically the group leader) - group_leader_rank = sp_group_id * sequence_parallel_degree + group_leader_rank = sp_group_id * context_parallel_size # Extract prompts from this SP group, accounting for num_generations duplicates # We only need prompts from one rank in each SP group @@ -335,7 +344,7 @@ def _generate_and_score_completions( # num_generations outputs for each one. This is faster than generating outputs for each duplicate # prompt individually. ordered_set_of_prompts = all_prompts_text[ - :: self.num_generations * self.args.sequence_parallel_degree + :: self.num_generations * self.args.context_parallel_size ] with profiling_context(self, "vLLM.generate"): @@ -352,14 +361,14 @@ def _generate_and_score_completions( ) else: completion_ids = [None] * ( - len(all_prompts_text) // self.args.sequence_parallel_degree + len(all_prompts_text) // self.args.context_parallel_size ) # Broadcast the completions from the main process to all processes completion_ids = broadcast_object_list(completion_ids, from_process=0) # Determine the appropriate slice based on sequence parallelism - if self.args.sequence_parallel_degree > 1: + if self.args.context_parallel_size > 1: # Calculate SP group ID (which group of ranks this rank belongs to) sp_group_id = self.accelerator.process_index // self.local_world_size @@ -583,7 +592,7 @@ def _generate_and_score_completions( advantages = advantages / (std_grouped_rewards + 1e-4) # Slice to keep only the local part of the data - if self.args.sequence_parallel_degree > 1: + if self.args.context_parallel_size > 1: # Calculate SP group ID (which group of ranks this rank belongs to) sp_group_id = self.accelerator.process_index // self.local_world_size diff --git a/src/axolotl/core/trainers/mamba.py b/src/axolotl/core/trainers/mamba.py index 38792e3896..b475b26d9f 100644 --- a/src/axolotl/core/trainers/mamba.py +++ b/src/axolotl/core/trainers/mamba.py @@ -5,6 +5,7 @@ from axolotl.core.trainers.base import AxolotlTrainer +# pylint: disable=too-many-ancestors class AxolotlMambaTrainer(AxolotlTrainer): """Mamba specific trainer to handle loss calculation""" diff --git a/src/axolotl/core/trainers/mixins/__init__.py b/src/axolotl/core/trainers/mixins/__init__.py index 453810aacc..b54577765a 100644 --- a/src/axolotl/core/trainers/mixins/__init__.py +++ b/src/axolotl/core/trainers/mixins/__init__.py @@ -5,6 +5,7 @@ from .activation_checkpointing import ActivationOffloadingMixin from .checkpoints import CheckpointSaveMixin +from .distributed_parallel import DistributedParallelMixin from .optimizer import OptimizerMixin from .packing import PackingMixin from .rng_state_loader import RngLoaderMixin diff --git a/src/axolotl/core/trainers/mixins/checkpoints.py b/src/axolotl/core/trainers/mixins/checkpoints.py index 8f994d78cc..4042ef9f10 100644 --- a/src/axolotl/core/trainers/mixins/checkpoints.py +++ b/src/axolotl/core/trainers/mixins/checkpoints.py @@ -13,9 +13,11 @@ class CheckpointSaveMixin(Trainer): def _save_optimizer_and_scheduler(self, output_dir): try: super()._save_optimizer_and_scheduler(output_dir) - except NotImplementedError as exc: - LOG.warning( + except (NotImplementedError, KeyError) as exc: + # TODO: fix fsdp2 optimizer saving + LOG.warning_once( f"Trainer does not support saving optimizer and scheduler: {exc}\n" "Optimizer and scheduler states were not saved - resuming from checkpoints " - "for this training run will not be possible." + "for this training run will not be possible.", + main_process_only=True, ) diff --git a/src/axolotl/core/trainers/mixins/distributed_parallel.py b/src/axolotl/core/trainers/mixins/distributed_parallel.py new file mode 100644 index 0000000000..d0f0f53dfa --- /dev/null +++ b/src/axolotl/core/trainers/mixins/distributed_parallel.py @@ -0,0 +1,20 @@ +""" +Mixin for correctly saving fsdp +""" + +from transformers import Trainer + + +class DistributedParallelMixin(Trainer): + """ + Mixin for correctly saving fsdp + """ + + def _save(self, output_dir: str | None = None, state_dict=None): + if ( + state_dict is None + and self.accelerator.parallelism_config + and self.accelerator.parallelism_config.dp_shard_enabled + ): + state_dict = self.accelerator.get_state_dict(self.model) + super()._save(output_dir, state_dict=state_dict) diff --git a/src/axolotl/core/trainers/trl.py b/src/axolotl/core/trainers/trl.py index cb97f37d7e..c5f19a6fed 100644 --- a/src/axolotl/core/trainers/trl.py +++ b/src/axolotl/core/trainers/trl.py @@ -8,13 +8,18 @@ RewardTrainer, ) -from axolotl.core.trainers.mixins import RngLoaderMixin +from axolotl.core.trainers.mixins import DistributedParallelMixin, RngLoaderMixin from axolotl.core.trainers.mixins.optimizer import OptimizerInitMixin, OptimizerMixin from axolotl.core.trainers.mixins.scheduler import SchedulerMixin class AxolotlORPOTrainer( - RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, ORPOTrainer + RngLoaderMixin, + SchedulerMixin, + OptimizerMixin, + OptimizerInitMixin, + DistributedParallelMixin, + ORPOTrainer, ): """ Extend the base ORPOTrainer for axolotl helpers @@ -24,7 +29,12 @@ class AxolotlORPOTrainer( class AxolotlKTOTrainer( - RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, KTOTrainer + RngLoaderMixin, + SchedulerMixin, + OptimizerMixin, + OptimizerInitMixin, + DistributedParallelMixin, + KTOTrainer, ): """ Extend the base KTOTrainer for axolotl helpers @@ -34,7 +44,12 @@ class AxolotlKTOTrainer( class AxolotlCPOTrainer( - RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, CPOTrainer + RngLoaderMixin, + SchedulerMixin, + OptimizerMixin, + OptimizerInitMixin, + DistributedParallelMixin, + CPOTrainer, ): """ Extend the base CPOTrainer for axolotl helpers @@ -44,7 +59,12 @@ class AxolotlCPOTrainer( class AxolotlRewardTrainer( - RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, RewardTrainer + RngLoaderMixin, + SchedulerMixin, + OptimizerMixin, + OptimizerInitMixin, + DistributedParallelMixin, + RewardTrainer, ): """ Extend the base RewardTrainer for axolotl helpers @@ -54,7 +74,12 @@ class AxolotlRewardTrainer( class AxolotlPRMTrainer( - RngLoaderMixin, SchedulerMixin, OptimizerMixin, OptimizerInitMixin, PRMTrainer + RngLoaderMixin, + SchedulerMixin, + OptimizerMixin, + OptimizerInitMixin, + DistributedParallelMixin, + PRMTrainer, ): """ Extend the base trl.PRMTrainer for axolotl helpers diff --git a/src/axolotl/integrations/kd/trainer.py b/src/axolotl/integrations/kd/trainer.py index 7ec43333a6..c454b2a2ce 100644 --- a/src/axolotl/integrations/kd/trainer.py +++ b/src/axolotl/integrations/kd/trainer.py @@ -21,6 +21,7 @@ from .kernels.liger import LigerFusedLinearKLTopKLogprobLoss +# pylint: disable=too-many-ancestors class AxolotlKDTrainer(AxolotlTrainer): """ Custom trainer subclass for Knowledge Distillation (KD) diff --git a/src/axolotl/integrations/liger/args.py b/src/axolotl/integrations/liger/args.py index 0460bdbf5b..d5bb10cfdd 100644 --- a/src/axolotl/integrations/liger/args.py +++ b/src/axolotl/integrations/liger/args.py @@ -16,8 +16,6 @@ Module for handling LIGER input arguments. """ -from typing import Optional - from pydantic import BaseModel, model_validator from axolotl.utils.logging import get_logger @@ -30,13 +28,13 @@ class LigerArgs(BaseModel): Input args for LIGER. """ - liger_rope: Optional[bool] = None - liger_rms_norm: Optional[bool] = None - liger_layer_norm: Optional[bool] = None - liger_swiglu: Optional[bool] = None - liger_glu_activation: Optional[bool] = None - liger_cross_entropy: Optional[bool] = None - liger_fused_linear_cross_entropy: Optional[bool] = None + liger_rope: bool | None = None + liger_rms_norm: bool | None = None + liger_layer_norm: bool | None = None + liger_swiglu: bool | None = None + liger_glu_activation: bool | None = None + liger_cross_entropy: bool | None = None + liger_fused_linear_cross_entropy: bool | None = None @model_validator(mode="before") @classmethod @@ -66,3 +64,20 @@ def check_tiled_mlp_conflict(cls, data): "You cannot have both `liger_glu_activation` and `tiled_mlp` set without `tiled_mlp_use_original_mlp: true`." ) return data + + @model_validator(mode="before") + @classmethod + def check_liger_rms_norm_tensor_parallel(cls, data): + if data.get("liger_rms_norm") and data.get("tensor_parallel_size", 1) > 1: + raise ValueError( + "`liger_rms_norm` is incompatible with tensor parallelism, " + "see https://github.com/linkedin/Liger-Kernel/issues/826" + ) + return data + + @model_validator(mode="after") + def check_tensor_parallel_size_liger_fused_linear_cross_entropy(self): + # TODO @SalmanMohammadi this is a larger fix - investigate + if self.tensor_parallel_size > 1 and self.liger_fused_linear_cross_entropy: + raise ValueError("Tensor parallelism is not compatible with liger losses.") + return self diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 4fc005457b..05039c9ee9 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -13,7 +13,8 @@ import torch import transformers import transformers.modeling_utils -from accelerate import init_empty_weights +from accelerate import PartialState, init_empty_weights +from accelerate.parallelism_config import ParallelismConfig from peft import ( PeftConfig, PeftMixedModel, @@ -48,10 +49,7 @@ from axolotl.models.mamba import fix_mamba_attn_for_loss from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.dict import DictDefault -from axolotl.utils.distributed import ( - get_device_count, - get_device_type, -) +from axolotl.utils.distributed import get_device_count, get_device_type, get_world_size from axolotl.utils.logging import get_logger from axolotl.utils.model_shard_quant import load_sharded_model_quant from axolotl.utils.schemas.enums import RLType @@ -87,6 +85,9 @@ class ModelLoader: `AutoModelForCausalLM`). """ + use_parallel_config: bool | None = False + parallelism_config: ParallelismConfig | None = None + def __init__( self, cfg: DictDefault, @@ -183,6 +184,20 @@ def load(self) -> tuple[PreTrainedModel | PeftModelForCausalLM, PeftConfig | Non def _apply_pre_model_load_setup(self): """Apply patches and setup configurations before model loading.""" + if self.use_parallel_config is not None: + self.use_parallel_config = ( + self.cfg.fsdp_config + or (self.cfg.tensor_parallel_size and self.cfg.tensor_parallel_size > 1) + or ( + self.cfg.context_parallel_size + and self.cfg.context_parallel_size > 1 + ) + ) + if self.cfg.fsdp_config and self.cfg.fsdp_version != 2: + self.use_parallel_config = False + + if self.use_parallel_config: + self._set_parallel_config() self._set_auto_model_loader() self._set_device_map_config() if self.cfg.revision_of_model: @@ -390,6 +405,86 @@ def _apply_post_lora_load_setup(self, skip_move_to_device: bool): gc.collect() torch.cuda.empty_cache() + @staticmethod + def _get_parallel_config_kwargs( + world_size: int, + tensor_parallel_size: int = 1, + context_parallel_size: int = 1, + dp_shard_size: int | None = None, + dp_replicate_size: int | None = None, + is_fsdp: bool = False, + ): + pc_kwargs = {} + remaining_world_size = world_size + + if tensor_parallel_size and tensor_parallel_size > 1: + pc_kwargs["tp_size"] = tensor_parallel_size + remaining_world_size = remaining_world_size // tensor_parallel_size + + if context_parallel_size and context_parallel_size > 1: + pc_kwargs["cp_size"] = context_parallel_size + remaining_world_size = remaining_world_size // context_parallel_size + + if dp_shard_size is None and dp_replicate_size in (None, 1): + if remaining_world_size > 1: + pc_kwargs["dp_shard_size"] = remaining_world_size + remaining_world_size = 1 + + if dp_replicate_size and dp_replicate_size > 1: + pc_kwargs["dp_replicate_size"] = dp_replicate_size + remaining_world_size = remaining_world_size // dp_replicate_size + + if remaining_world_size > 1 and dp_shard_size and dp_shard_size > 1: + if not is_fsdp: + raise ValueError( + "dp_shard_size was configured without a corresponding fsdp_config! " + "Please ensure you have configured FSDP using fsdp_config." + ) + pc_kwargs["dp_shard_size"] = dp_shard_size + remaining_world_size = remaining_world_size // dp_shard_size + if remaining_world_size > 1 and "dp_replicate_size" not in pc_kwargs: + pc_kwargs["dp_replicate_size"] = remaining_world_size + remaining_world_size = 1 + + if remaining_world_size > 1: + if "dp_shard_size" not in pc_kwargs and is_fsdp: + pc_kwargs["dp_shard_size"] = remaining_world_size + remaining_world_size = 1 + + if remaining_world_size > 1: + raise ValueError( + f"The configured parallelisms are incompatible with the current world size ({get_world_size()})!\n" + f"{pc_kwargs}" + ) + + return pc_kwargs + + def _set_parallel_config(self): + """Set parallelism configuration (DP, FSDP, TP, CP) in PartialState/Accelerator""" + pc_kwargs = ModelLoader._get_parallel_config_kwargs( + get_world_size(), + self.cfg.tensor_parallel_size, + self.cfg.context_parallel_size, + self.cfg.dp_shard_size, + self.cfg.dp_replicate_size, + bool(self.cfg.fsdp or self.cfg.fsdp_config), + ) + + if pc_kwargs: + self.parallelism_config = ParallelismConfig( + **pc_kwargs, + ) + device_mesh = self.parallelism_config.build_device_mesh("cuda") + partial_state = PartialState() + # fmt: off + partial_state._shared_state["parallelism_config"] = ( # pylint: disable=protected-access + self.parallelism_config + ) + partial_state._shared_state["device_mesh"] = ( # pylint: disable=protected-access + device_mesh + ) + # fmt: on + def _set_auto_model_loader(self): """Set `self.auto_model_loader`. Defaults to `transformers.AutoModelForCausalLM` (set at `__init__`). When using a multimodal model, `self.auto_model_loader` @@ -622,6 +717,14 @@ def _configure_zero3_memory_efficient_loading( def _build_model(self) -> bool: """Load model, with load strategy depending on config.""" skip_move_to_device = False + + if self.cfg.tensor_parallel_size > 1: + self.model_kwargs["tp_size"] = self.cfg.tensor_parallel_size + self.model_kwargs["tp_plan"] = "auto" + self.model_kwargs["device_mesh"] = PartialState().device_mesh + if "device_map" in self.model_kwargs: + del self.model_kwargs["device_map"] # not compatible with `tp_plan` + if self.is_fsdp_enabled: if self.cfg.fsdp_config.cpu_ram_efficient_loading: skip_move_to_device = True @@ -734,6 +837,14 @@ def _build_model(self) -> bool: if is_deepspeed_zero3_enabled(): skip_move_to_device = True + # pylint: disable=protected-access + if self.cfg.tensor_parallel_size > 1: + # workaround for upstream 4.54.0 not setting _tp_size or _device_mesh + # TODO(wing): remove once 4.54.1 is released + if self.model._tp_size != self.cfg.tensor_parallel_size: + self.model._tp_size = self.cfg.tensor_parallel_size + self.model._device_mesh = self.model_kwargs["device_mesh"] + return skip_move_to_device def _set_z3_leaf_modules(self): diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 186681521f..9eb7791135 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -49,6 +49,7 @@ def has_flash_attn(self) -> bool: def apply_pre_model_load_patches(self): """Apply pre-model load patches based on config.""" + self._apply_transformers_patches() # self._apply_flex_attention_patches() self._apply_flash_attention_patches() self._apply_chunked_cross_entropy_patch() @@ -64,13 +65,19 @@ def apply_pre_model_load_patches(self): self._patch_llama_derived_model() self._apply_mistral_cross_entropy_patch() self._apply_self_attention_lora_patch() - self._apply_sequence_parallel_patches() def apply_post_plugin_pre_model_load_patches(self): """Apply post plugin-pre_model_load load patches based on config.""" self._apply_tiled_mlp(self.cfg.model_config_type) self._apply_voxtral_patches() + def _apply_transformers_patches(self): + from axolotl.monkeypatch.transformers.modeling_flash_attention_utils import ( + patch_prepare_from_posids, + ) + + patch_prepare_from_posids() + def apply_post_model_load_patches(self, model: PreTrainedModel): """Apply patches that require the model instance.""" self._apply_llama_flash_attn_patches(model) @@ -253,17 +260,6 @@ def _apply_multipack_patches(self): has_remote_code=has_remote_code, ) - def _apply_sequence_parallel_patches(self): - """Apply sequence parallelism patches.""" - if self.cfg.sequence_parallel_degree and self.cfg.sequence_parallel_degree > 1: - from axolotl.monkeypatch.ring_attn.patch import ( - patch_prepare_data_loader, - patch_prepare_device_mesh, - ) - - patch_prepare_data_loader() - patch_prepare_device_mesh(self.cfg.sequence_parallel_degree, self.cfg.fsdp) - def _apply_tiled_mlp(self, model_type: str): if self.cfg.tiled_mlp: from axolotl.monkeypatch.tiled_mlp import ( diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index 803659232c..af262d18fb 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -249,13 +249,19 @@ def fsdp2_prepare_model(accelerator, model: torch.nn.Module) -> torch.nn.Module: auto_wrap_policy=fsdp2_plugin.auto_wrap_policy, ) + mesh = getattr(accelerator.state, "device_mesh", None) + fsdp2_kwargs = { "reshard_after_forward": fsdp2_plugin.reshard_after_forward, "offload_policy": fsdp2_plugin.cpu_offload, # `fully_shard` doesn't accept `None` in case of `MixedPrecisionPolicy` "mp_policy": fsdp2_plugin.mixed_precision_policy or MixedPrecisionPolicy(), + "mesh": ( + mesh[tuple(accelerator.state.parallelism_config.fsdp_dim_names)] + if mesh is not None + else None + ), } - model_has_params4bit = False for _, param in model.named_parameters(): # this is a temporary fix whereby loading models with bnb params cannot be moved from diff --git a/src/axolotl/monkeypatch/ring_attn/__init__.py b/src/axolotl/monkeypatch/ring_attn/__init__.py index 5833b9ce43..736378b162 100644 --- a/src/axolotl/monkeypatch/ring_attn/__init__.py +++ b/src/axolotl/monkeypatch/ring_attn/__init__.py @@ -5,18 +5,14 @@ from .patch import ( get_ring_attn_group, - patch_prepare_data_loader, - patch_prepare_device_mesh, - register_ring_attn, + register_ring_attn_from_device_mesh, set_ring_attn_group, update_ring_attn_params, ) __all__ = ( "get_ring_attn_group", - "patch_prepare_data_loader", - "patch_prepare_device_mesh", - "register_ring_attn", + "register_ring_attn_from_device_mesh", "set_ring_attn_group", "update_ring_attn_params", ) diff --git a/src/axolotl/monkeypatch/ring_attn/patch.py b/src/axolotl/monkeypatch/ring_attn/patch.py index 9c9ba45539..934687a165 100644 --- a/src/axolotl/monkeypatch/ring_attn/patch.py +++ b/src/axolotl/monkeypatch/ring_attn/patch.py @@ -8,13 +8,12 @@ sequence parallelism training. """ -import inspect import os from typing import Optional -import accelerate import torch import torch.distributed as dist +from torch.distributed import DeviceMesh try: from transformers.modeling_flash_attention_utils import _flash_supports_window @@ -29,39 +28,13 @@ LOG = get_logger(__name__) - RING_ATTN_GROUP = None -ORIGINAL_PREPARE_DATALOADER_CODE = """ submesh_fsdp_size = 1 - submesh_dp_size = 1 - submesh_tp_size = 1 - if "tp" in torch_device_mesh.mesh_dim_names: - submesh_tp_size = torch_device_mesh["tp"].size() - if "dp" in torch_device_mesh.mesh_dim_names: - submesh_dp_size = torch_device_mesh["dp"].size() - if "fsdp" in torch_device_mesh.mesh_dim_names: - submesh_fsdp_size = torch_device_mesh["fsdp"].size() - process_index = process_index // submesh_tp_size""" - -NEW_PREPARE_DATALOADER_CODE = """ submesh_fsdp_size = 1 - submesh_dp_size = 1 - submesh_tp_size = 1 - submesh_cp_size = 1 - if "cp" in torch_device_mesh.mesh_dim_names: - submesh_cp_size = torch_device_mesh["cp"].size() - if "tp" in torch_device_mesh.mesh_dim_names: - submesh_tp_size = torch_device_mesh["tp"].size() - if "dp" in torch_device_mesh.mesh_dim_names: - submesh_dp_size = torch_device_mesh["dp"].size() - if "fsdp" in torch_device_mesh.mesh_dim_names: - submesh_fsdp_size = torch_device_mesh["fsdp"].size() - process_index = process_index // (submesh_tp_size * submesh_cp_size)""" - def get_ring_attn_group() -> dist.ProcessGroup: """Getter for ring attention group on this rank.""" if RING_ATTN_GROUP is None: - raise RuntimeError("register_ring_attn() not yet called") + raise RuntimeError("register_ring_attn_from_device_mesh() not yet called") return RING_ATTN_GROUP @@ -161,15 +134,17 @@ def _flash_attention_forward_v3( ] -def register_ring_attn( - sequence_parallel_degree: int, +def register_ring_attn_from_device_mesh( + device_mesh: "DeviceMesh", + context_parallel_dim: tuple[str, ...], heads_k_stride: int | None, ring_attn_func: RingAttnFunc | None, ): - """Create ring attention group and substitute flash attn with ring flash attn. + """Create ring attention group using DeviceMesh and substitute flash attn with ring flash attn. Args: - sequence_parallel_degree: Sequence parallelism factor. + device_mesh: DeviceMesh object containing the parallelism topology. + context_parallel_dim: Name of the sequence parallel dimension in the device mesh. heads_k_stride: Sequence parallelism K head stride size. Passed through to `varlen_llama3` `ring_flash_attn` implementation. ring_attn_func: `ring_flash_attn` ring attention implemention. If sample @@ -177,44 +152,39 @@ def register_ring_attn( `batch` function. """ rank = dist.get_rank() - world_size = dist.get_world_size() - - if rank == 0: - LOG.info( - "Enabling ring attention sequence parallelism: " - f"each sequence will be processed across {sequence_parallel_degree} GPUs" - ) - assert sequence_parallel_degree <= world_size, ( - f"sequence_parallel_degree ({sequence_parallel_degree}) " - f"must be less than or equal to world_size ({world_size})" - ) - assert world_size % sequence_parallel_degree == 0, ( - f"sequence_parallel_degree ({sequence_parallel_degree}) " - f"must evenly divide world_size ({world_size})" + LOG.info( + f"Enabling ring attention sequence parallelism using DeviceMesh " + f"dimension '{context_parallel_dim}'", + main_process_only=True, ) - # Assign ranks to sequence parallel groups - group_assignments = {} - for i in range(world_size // sequence_parallel_degree): - ring_attn_ranks = list( - range( - i * sequence_parallel_degree, - (i + 1) * sequence_parallel_degree, - ) - ) - group = dist.new_group(ranks=ring_attn_ranks, backend="nccl") - - # Track which GPUs are in which groups - for r in ring_attn_ranks: - group_assignments[r] = i + # Extract the sequence parallel submesh + try: + sequence_mesh = device_mesh[context_parallel_dim] + except (KeyError, IndexError) as e: + raise ValueError( + f"Dimension '{context_parallel_dim}' not found in device_mesh. " + f"Available dimensions: {device_mesh.mesh_dim_names}" + ) from e - if rank in ring_attn_ranks: - set_ring_attn_group(group) + # Get the process group for context parallelism + sequence_pg = sequence_mesh.get_group() + context_parallel_size = sequence_mesh.size() - # Log the GPU group assignments if rank == 0: - LOG.info(f"Sequence parallel group assignments: {group_assignments}") + LOG.info( + f"Sequence parallel degree: {context_parallel_size}, " + f"mesh shape: {sequence_mesh.mesh.shape}" + ) + + # Log which ranks are in the current process group + if sequence_pg != dist.GroupMember.WORLD: + ranks_in_group = dist.get_process_group_ranks(sequence_pg) + LOG.info(f"Current sequence parallel group ranks: {ranks_in_group}") + + # Set the ring attention group + set_ring_attn_group(sequence_pg) if ring_attn_func is RingAttnFunc.VARLEN_LLAMA3: # fmt: off @@ -257,92 +227,3 @@ def update_ring_attn_params(position_ids: torch.Tensor | None): cu_seqlens, _ = get_cu_seqlens_from_pos_ids(position_ids) cu_seqlens = cu_seqlens.squeeze().to(device=torch.cuda.current_device()) update_ring_flash_attn_params(cu_seqlens, get_ring_attn_group()) - - -def patch_prepare_data_loader(): - """Patch `accelerate.data_loader.prepare_data_loader` to respect the SP degree. - - Raises: - RuntimeError: If source code to patch does not exist. - """ - original_fn = accelerate.data_loader.prepare_data_loader - original_source = inspect.getsource(original_fn) - - if ORIGINAL_PREPARE_DATALOADER_CODE not in original_source: - raise RuntimeError( - "SP patch failed - target snippet not found. " - "Check accelerate's version or update the patch." - ) - - patched_source = original_source.replace( - ORIGINAL_PREPARE_DATALOADER_CODE, NEW_PREPARE_DATALOADER_CODE - ) - - items_to_import = [] - for item in dir(accelerate.data_loader): - if item in patched_source: - items_to_import.append(item) - - # Create a new function from the patched source - namespace = {} - exec( # pylint: disable=exec-used # nosec B102 - f"from accelerate.data_loader import ({', '.join(items_to_import)})", - globals(), - ) - exec( # pylint: disable=exec-used # nosec B102 - patched_source, globals(), namespace - ) - - patched_function = namespace["prepare_data_loader"] - original_fn.__code__ = patched_function.__code__ - - LOG.info("Patched accelerate.data_loader.prepare_data_loader for SP support") - - -def patch_prepare_device_mesh(sequence_parallel_degree: int, fsdp: bool = False): - """Patches the `Accelerator._prepare_device_mesh` method to create a device mesh - that includes sequence parallelism with the specified degree. - - Args: - sequence_parallel_degree: The degree of sequence parallelism to use. - fsdp: Whether to use FSDP. - """ - - def _prepare_device_mesh(self): - """Prepare the device mesh for distributed training. The dataloader will - determine how to load data based on the device mesh. - """ - if self.state.torch_tp_plugin: - return self.state.torch_tp_plugin.torch_device_mesh - if ( - self.distributed_type == accelerate.accelerator.DistributedType.DEEPSPEED - and hasattr(self.state, "ds_device_mesh") - ): - return self.state.ds_device_mesh - - # Create device mesh with sequence parallelism - world_size = dist.get_world_size() - mesh_shape = ( - world_size // sequence_parallel_degree, - sequence_parallel_degree, - ) - device_ids = list(range(world_size)) - - # NOTE: We use "cp" instead of "sp" to match the PyTorch native "context - # parallelism" implementation naming. - # NOTE: We have a simplified FSDP handling here; i.e., if FSDP is enabled, we - # only use "fsdp" and "cp" for the device mesh. - return dist.DeviceMesh( - "cuda", - torch.tensor(device_ids).reshape(mesh_shape), - mesh_dim_names=("dp", "cp") if not fsdp else ("fsdp", "cp"), - ) - - # Replace the original method with our new method - # pylint: disable=protected-access - accelerate.accelerator.Accelerator._prepare_device_mesh = _prepare_device_mesh - - LOG.info( - "Successfully patched Accelerator._prepare_device_mesh " - f"with sequence_parallel_degree={sequence_parallel_degree}" - ) diff --git a/src/axolotl/monkeypatch/transformers/__init__.py b/src/axolotl/monkeypatch/transformers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/transformers/modeling_flash_attention_utils.py b/src/axolotl/monkeypatch/transformers/modeling_flash_attention_utils.py new file mode 100644 index 0000000000..1bd8ac6bce --- /dev/null +++ b/src/axolotl/monkeypatch/transformers/modeling_flash_attention_utils.py @@ -0,0 +1,87 @@ +""" +Monkey patch to fix transformers.modeling_flash_attention_utils. + +see https://github.com/huggingface/transformers/pull/39653/files +""" + +import sys + +import torch + + +def _prepare_from_posids(query, key, value, position_ids): + """ + This function returns necessary arguments to call `flash_attn_varlen_func`. + All three query, key, value states will be flattened. + Cumulative lengths of each examples in the batch will be extracted from position_ids. + NOTE: ideally cumulative lengths should be prepared at the data collator stage + Arguments: + query (`torch.Tensor`): + Query state with padding. Shape: (batch_size, query_length, num_heads, head_dim). + key (`torch.Tensor`): + Key state with padding. Shape: (batch_size, kv_seq_len, num_key_value_heads, head_dim). + value (`torch.Tensor`): + Value state with padding. Shape: (batch_size, kv_seq_len, num_key_value_heads, head_dim). + position_ids (`torch.Tensor`): + Boolean or int tensor of shape (batch_size, sequence_length), 1 means valid and 0 means not valid. + Return: + query (`torch.Tensor`): + Query state without padding. Shape: (total_target_length, num_heads, head_dim). + key (`torch.Tensor`): + Key state with padding. Shape: (total_source_length, num_key_value_heads, head_dim). + value (`torch.Tensor`): + Value state with padding. Shape: (total_source_length, num_key_value_heads, head_dim). + indices_q (`torch.Tensor`): + The indices of non-masked tokens from the flattened input target sequence. + (cu_seqlens_q, cu_seqlens_k) (`tuple[int]`): + The cumulative sequence lengths for the target (query) and source (key, value), used to index into ragged (unpadded) tensors. `cu_seqlens` shape is (batch_size + 1,). + (max_seqlen_in_batch_q, max_seqlen_in_batch_k) (`tuple[int]`): + Maximum sequence length in batch (`max_seqlen_in_batch_q` for the target sequence i.e. query, `max_seqlen_in_batch_k` for the source sequence i.e. key/value). + """ + query = query.contiguous().view(-1, query.size(-2), query.size(-1)) + key = key.contiguous().view(-1, key.size(-2), key.size(-1)) + value = value.contiguous().view(-1, value.size(-2), value.size(-1)) + + position_ids = position_ids.flatten() + indices_q = torch.arange( + position_ids.size(0), device=position_ids.device, dtype=torch.int32 + ) + + cu_seq_lens = torch.cat( + ( + indices_q[position_ids == 0], + torch.tensor( + position_ids.size(), device=position_ids.device, dtype=torch.int32 + ), + ) + ) + # NOTE: With torch compile, this will cause a graph break if you don't set + # `TORCHDYNAMO_CAPTURE_SCALAR_OUTPUTS=1` in the environment or call + # `torch._dynamo.config.capture_scalar_outputs = True` before doing the forward pass. + # This is a limitation of flash attention API, as the function `flash_attn_varlen_func` + # requires `max_length_q`, `max_length_k` to be passed as `int` and not `torch.Tensor`. + # https://github.com/Dao-AILab/flash-attention/blob/2dd8078adc1d9b74e315ee99718c0dea0de8eeb6/flash_attn/flash_attn_interface.py#L1423-L1424 + # We should use cu_seq_lens instead of position_ids to get the max length since position_ids is not always increasing + # for some models (e.g. qwen2-vl). + max_length = cu_seq_lens.diff().max().item() + return ( + query, + key, + value, + indices_q, + (cu_seq_lens, cu_seq_lens), + (max_length, max_length), + ) + + +def patch_prepare_from_posids(): + import transformers.modeling_flash_attention_utils + + transformers.modeling_flash_attention_utils._prepare_from_posids = ( # pylint: disable=protected-access + _prepare_from_posids + ) + setattr( + sys.modules["transformers.modeling_flash_attention_utils"], + "_prepare_from_posids", + _prepare_from_posids, + ) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index b507c2c7b1..41f184abc5 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -205,7 +205,7 @@ def execute_training( ) ) - if cfg.sequence_parallel_degree > 1: + if cfg.context_parallel_size > 1: models = [trainer.model] if hasattr(trainer, "ref_model") and trainer.ref_model: models.append(trainer.ref_model) @@ -213,7 +213,7 @@ def execute_training( stack.enter_context( SequenceParallelContextManager( models=models, - sequence_parallel_degree=cfg.sequence_parallel_degree, + context_parallel_size=cfg.context_parallel_size, gradient_accumulation_steps=cfg.gradient_accumulation_steps, ring_attn_func=cfg.ring_attn_func, heads_k_stride=cfg.heads_k_stride, diff --git a/src/axolotl/utils/bench.py b/src/axolotl/utils/bench.py index dae53eddfb..dd3a85b8c6 100644 --- a/src/axolotl/utils/bench.py +++ b/src/axolotl/utils/bench.py @@ -57,10 +57,10 @@ def gpu_memory_usage(device=0): @check_cuda_device((0.0, 0.0, 0.0)) def gpu_memory_usage_all(device=0): - usage = torch.cuda.memory_allocated(device) / 1024.0**3 - reserved = torch.cuda.memory_reserved(device) / 1024.0**3 - smi = gpu_memory_usage_smi(device) - return usage, reserved - usage, max(0, smi - reserved) + active = torch.cuda.memory_stats().get("active_bytes.all.peak", 0) / 1024.0**3 + allocated = torch.cuda.max_memory_allocated(device) / 1024.0**3 + reserved = torch.cuda.max_memory_reserved(device) / 1024.0**3 + return active, allocated, reserved def mps_memory_usage_all(): @@ -92,27 +92,38 @@ def gpu_memory_usage_smi(device=0): return 0.0 -def log_gpu_memory_usage( - log: logging.Logger | logging.LoggerAdapter, - msg: str = "", - device: int | torch.device = 0, -): +def get_gpu_memory_usage(device: int | torch.device = 0): cur_device_type = str(get_device_type()) if torch.backends.mps.is_available(): usage, cache, misc = mps_memory_usage_all() elif "npu" in cur_device_type and is_torch_npu_available(): usage, cache, misc = npu_memory_usage_all(device) - elif "gpu" in cur_device_type and torch.cuda.is_available(): + elif "cuda" in cur_device_type and torch.cuda.is_available(): usage, cache, misc = gpu_memory_usage_all(device) else: + return 0.0, 0.0, 0.0 + + return usage, cache, misc + + +def log_gpu_memory_usage( + log: logging.Logger | logging.LoggerAdapter, + msg: str = "", + device: int | torch.device = 0, +): + try: + active, allocated, reserved = get_gpu_memory_usage(device) + except ValueError: + # likely CPU, ignore return + cur_device_type = str(get_device_type()) extras = [] - if cache > 0: - extras.append(f"+{cache:.03f}GB cache") - if misc > 0: - extras.append(f"+{misc:.03f}GB misc") - msg = f"{cur_device_type} memory usage:" if not msg else msg - log.info( - f"{msg} {usage:.03f}GB ({', '.join(extras)})", + if allocated > 0: + extras.append(f"+{allocated:.03f}GB allocated") + if reserved > 0: + extras.append(f"+{reserved:.03f}GB reserved") + msg = f"{cur_device_type} memory active:" if not msg else msg + log.debug( + f"{msg} {active:.03f}GB ({', '.join(extras)})", stacklevel=2, ) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index c64d8d351e..63799c734b 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -35,7 +35,7 @@ from trl.models import unwrap_model_for_generation from axolotl.utils import is_comet_available, is_mlflow_available -from axolotl.utils.bench import log_gpu_memory_usage +from axolotl.utils.bench import get_gpu_memory_usage, log_gpu_memory_usage from axolotl.utils.callbacks.perplexity import Perplexity from axolotl.utils.distributed import ( barrier, @@ -100,7 +100,6 @@ class GPUStatsCallback( def __init__(self, cfg): self.cfg = cfg - self.logged = False def on_step_end( self, @@ -109,9 +108,21 @@ def on_step_end( control: TrainerControl, **kwargs, ) -> TrainerControl: - if not self.logged and state.global_step > 1: - log_gpu_memory_usage(LOG, "while training", self.cfg.device) - self.logged = True + if state.global_step > 0: + if self.cfg.use_wandb and state.is_world_process_zero: + try: + active, allocated, reserved = get_gpu_memory_usage() + wandb.log( + { + "memory/max_memory_active": active, + "memory/max_memory_allocated": allocated, + "memory/device_memory_reserved": reserved, + }, + step=state.global_step, + ) + except ValueError: + pass + log_gpu_memory_usage(LOG, "", self.cfg.device) return control diff --git a/src/axolotl/utils/ctx_managers/sequence_parallel.py b/src/axolotl/utils/ctx_managers/sequence_parallel.py index 1ac805a73c..949c76f49c 100644 --- a/src/axolotl/utils/ctx_managers/sequence_parallel.py +++ b/src/axolotl/utils/ctx_managers/sequence_parallel.py @@ -5,6 +5,7 @@ import torch import torch.distributed as dist +from accelerate import PartialState from torch import nn from torch.utils.hooks import RemovableHandle from transformers.modeling_outputs import CausalLMOutputWithPast @@ -12,7 +13,7 @@ from axolotl.monkeypatch.ring_attn import ( get_ring_attn_group, - register_ring_attn, + register_ring_attn_from_device_mesh, update_ring_attn_params, ) from axolotl.utils.schemas.enums import RingAttnFunc @@ -150,9 +151,18 @@ def apply_sequence_parallelism( if "num_items_in_batch" in batch: # Approximation; this needed since num_items_in_batch may be counted across # all samples in a gradient accumulated batch, not on a per-step basis. + local_valid_tokens = (batch["labels"] != -100).sum() + + # All-reduce across sequence parallel ranks to get global token count + cp_group = get_ring_attn_group() + global_valid_tokens = local_valid_tokens.clone() + # we use AVG instead of SUM as using sum seems to scale down the loss by over-accounting the number of tokens + dist.all_reduce(global_valid_tokens, op=dist.ReduceOp.AVG, group=cp_group) + global_valid_tokens = int(global_valid_tokens.item()) + batch["num_items_in_batch"] = ( - batch["labels"] != -100 - ).sum() * gradient_accumulation_steps + global_valid_tokens * gradient_accumulation_steps + ) return batch, original_seq_len, pad_len @@ -167,7 +177,7 @@ class SequenceParallelContextManager: Args: models: List of models to apply sequence parallelism to pre- and post- forward hooks. - sequence_parallel_degree: Number of processes to split sequences over. + context_parallel_size: Number of processes to split sequences over. gradient_accumulation_steps: Number of steps to accumulate gradients over. ring_attn_func: Which ring attention function to use. Currently unused. heads_k_stride: Sequence parallelism K head stride size. Passed through to @@ -179,14 +189,14 @@ class SequenceParallelContextManager: def __init__( self, models: list[nn.Module], - sequence_parallel_degree: int, + context_parallel_size: int, gradient_accumulation_steps: int, ring_attn_func: RingAttnFunc, heads_k_stride: int | None, gather_outputs: bool, ): self.models = models - self.sequence_parallel_degree = sequence_parallel_degree + self.context_parallel_size = context_parallel_size self.gradient_accumulation_steps = gradient_accumulation_steps self.ring_attn_func = ring_attn_func self.heads_k_stride = heads_k_stride @@ -230,8 +240,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): def _register_ring_attn(self): # Initialize ring attn for sequence parallelism - register_ring_attn( - sequence_parallel_degree=self.sequence_parallel_degree, + partial_state = PartialState() + register_ring_attn_from_device_mesh( + device_mesh=partial_state.device_mesh, + context_parallel_dim=("cp",), heads_k_stride=self.heads_k_stride, ring_attn_func=self.ring_attn_func, ) diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 7877e5abf8..21c8e472b3 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -430,10 +430,11 @@ def save_preprocessed_dataset( num_shards=cfg.num_dataset_shards_to_save, ) else: + min_rows_per_proc = 256 os.makedirs(prepared_ds_path, exist_ok=True) dataset.save_to_disk( str(prepared_ds_path), - num_proc=min(max(1, len(dataset) // 8), num_workers), + num_proc=min(max(1, len(dataset) // min_rows_per_proc), num_workers), max_shard_size=None, num_shards=cfg.num_dataset_shards_to_save, ) diff --git a/src/axolotl/utils/environment.py b/src/axolotl/utils/environment.py index 1cc609a68d..3c83c87cb0 100644 --- a/src/axolotl/utils/environment.py +++ b/src/axolotl/utils/environment.py @@ -2,12 +2,15 @@ utils to get GPU info for the current environment """ +from importlib.metadata import version + from accelerate.utils.environment import ( check_cuda_p2p_ib_support as accelerate_check_cuda_p2p_ib_support, ) from accelerate.utils.environment import ( get_gpu_info, ) +from packaging.version import Version, parse def check_cuda_p2p_ib_support(): @@ -26,3 +29,13 @@ def check_cuda_p2p_ib_support(): except Exception: # pylint: disable=broad-except # nosec pass return True + + +def get_package_version(package: str) -> Version: + version_str = version(package) + return parse(version_str) + + +def is_package_version_ge(package: str, version_: str) -> bool: + package_version = get_package_version(package) + return package_version >= parse(version_) diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index ee8640f417..af62c0a4fe 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -5,6 +5,7 @@ import gc import math +import time from concurrent.futures import ProcessPoolExecutor from multiprocessing import cpu_count, get_context from typing import Iterable, Iterator, Union @@ -453,7 +454,10 @@ def __len__(self) -> int: _sampled_lens = [] for _ in range(self.num_count_samples): self._batches = None # Reset cached batches + # log timer for generating batches + start_time = time.time() _sampled_lens.append(len(self.generate_batches(set_stats=False))) + LOG.debug(f"generate_batches time: {time.time() - start_time}") len_batches = min(_sampled_lens) # Gather minimum across all ranks diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index f8746692cc..1d089ba41f 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -651,7 +651,23 @@ class AxolotlInputConfig( }, ) + dp_shard_size: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of devices to shard across. If not set, will use all available devices." + }, + ) + dp_replicate_size: int | None = Field( + default=None, + json_schema_extra={"description": "Number of devices to replicate across."}, + ) sequence_parallel_degree: int | None = Field( + default=None, + json_schema_extra={ + "description": "Deprecated: use `context_parallel_size` instead" + }, + ) + context_parallel_size: int | None = Field( default=None, json_schema_extra={ "description": "Set to a divisor of the number of GPUs available to split sequences into chunks of equal size. Use in long context training to prevent OOM when sequences cannot fit into a single GPU's VRAM. E.g., if 4 GPUs are available, set this value to 2 to split each sequence into two equal-sized subsequences, or set to 4 to split into four equal-sized subsequences. See https://docs.axolotl.ai/docs/sequence_parallelism.html for more details." diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 063690c599..502c18e7de 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -673,7 +673,7 @@ def check_grpo_liger_sequence_parallel(cls, data): data.get("rl") == "grpo" and data.get("trl", {}) and data.get("trl").get("use_liger_loss") - and data.get("sequence_parallel_degree", 1) > 1 + and data.get("context_parallel_size", 1) > 1 ): raise ValueError("GRPO + SP + Liger not currently supported") return data @@ -880,51 +880,35 @@ def check_fsdp2_w_8bit_optimizer(self): return self - @model_validator(mode="after") - def check_fsdp_sharded_state_dict_w_safetensors(self): - if ( - hasattr(self, "fsdp_config") - and self.fsdp_config - and hasattr(self, "save_safetensors") - and self.save_safetensors - and self.fsdp_config.get("state_dict_type", "") == "SHARDED_STATE_DICT" - and str(getattr(self, "fsdp_version", "1")) != "2" - ): - raise ValueError( - "FSDP SHARDED_STATE_DICT not compatible with save_safetensors" - ) - return self - @model_validator(mode="before") @classmethod def check_tensor_parallel_size_update_ds_json(cls, data): tensor_parallel_size = data.get("tensor_parallel_size") if tensor_parallel_size is not None and tensor_parallel_size > 1: - if not data.get("deepspeed"): - raise ValueError( - "Tensor parallelism (TP) is only supported with DeepSpeed" - ) - with open(data.get("deepspeed"), "r", encoding="utf-8") as ds_fin: - ds_config = json.load(ds_fin) - should_save = False - if "tensor_parallel" not in ds_config: - ds_config["tensor_parallel"] = {"autotp_size": tensor_parallel_size} - should_save = True - if ( - "gather_16bit_weights_on_model_save" - not in ds_config["zero_optimization"] - ): - ds_config["zero_optimization"][ + if data.get("deepspeed"): + with open(data.get("deepspeed"), "r", encoding="utf-8") as ds_fin: + ds_config = json.load(ds_fin) + should_save = False + if "tensor_parallel" not in ds_config: + ds_config["tensor_parallel"] = { + "autotp_size": tensor_parallel_size + } + should_save = True + if ( "gather_16bit_weights_on_model_save" - ] = True - should_save = True - if should_save: - temp_dir = tempfile.mkdtemp() - with open( - Path(temp_dir) / "autotp_ds.json", "w", encoding="utf-8" - ) as ds_fout: - json.dump(ds_config, ds_fout, indent=4) - data["deepspeed"] = str(Path(temp_dir) / "autotp_ds.json") + not in ds_config["zero_optimization"] + ): + ds_config["zero_optimization"][ + "gather_16bit_weights_on_model_save" + ] = True + should_save = True + if should_save: + temp_dir = tempfile.mkdtemp() + with open( + Path(temp_dir) / "autotp_ds.json", "w", encoding="utf-8" + ) as ds_fout: + json.dump(ds_config, ds_fout, indent=4) + data["deepspeed"] = str(Path(temp_dir) / "autotp_ds.json") return data @@ -1205,13 +1189,18 @@ def check_tensor_parallel_size(self): return self @model_validator(mode="after") - def check_sequence_parallel_degree(self): - if not self.sequence_parallel_degree: - self.sequence_parallel_degree = 1 - elif self.sequence_parallel_degree > 1: + def check_context_parallel_size(self): + if self.sequence_parallel_degree and not self.context_parallel_size: + LOG.warning( + "`sequence_parallel_degree` is deprecated, use `context_parallel_size`" + ) + self.context_parallel_size = self.sequence_parallel_degree + if not self.context_parallel_size: + self.context_parallel_size = 1 + elif self.context_parallel_size > 1: if not self.flash_attention: raise ValueError( - "flash_attention: true must be set with sequence_parallel_degree > 1" + "flash_attention: true must be set with context_parallel_size > 1" ) if self.sample_packing and self.micro_batch_size > 1: @@ -1221,17 +1210,23 @@ def check_sequence_parallel_degree(self): ) try: + import transformers.modeling_flash_attention_utils + + # pylint: disable=protected-access + transformers.modeling_flash_attention_utils._flash_supports_window_size = ( + transformers.modeling_flash_attention_utils._flash_supports_window + ) import ring_flash_attn # noqa: F401 # pylint:disable=unused-import except ImportError as exception: raise ImportError( - "sequence_parallel_degree > 1 but ring_flash_attn is not installed. " + "context_parallel_size > 1 but ring_flash_attn is not installed. " "Please install it with `pip install axolotl[ring-flash-attn] " "or `pip install ring-flash-attn>=0.1.4`." ) from exception LOG.warning( "Sequence parallelism (SP) is enabled with " - f"sequence_parallel_degree={self.sequence_parallel_degree}. " + f"context_parallel_size={self.context_parallel_size}. " "Please note that logged losses may differ slightly to the non-SP " "losses due to transformers Trainer implementation details. " "Please see https://github.com/axolotl-ai-cloud/axolotl/pull/2495#issuecomment-2784022042 " @@ -1242,7 +1237,7 @@ def check_sequence_parallel_degree(self): @model_validator(mode="after") def validate_ring_attn_func(self): - if getattr(self, "sequence_parallel_degree", 1) == 1: + if getattr(self, "context_parallel_size", 1) == 1: return self if self.ring_attn_func is not None: @@ -1259,6 +1254,20 @@ def validate_ring_attn_func(self): return self +class DistributedValidationMixin: + """validation for distributed training.""" + + @model_validator(mode="after") + def check_tensor_parallel_optimizer(self): + if self.tensor_parallel_size > 1: + if self.optimizer in ["paged_adamw_8bit", "adamw_8bit", "adamw_bnb_8bit"]: + raise ValueError( + "tensor_parallel_size is not supported with paged_adamw_8bit, adamw_8bit, and adamw_bnb_8bit optimizers" + ) + + return self + + # pylint: disable=too-many-ancestors class ValidationMixin( DatasetValidationMixin, diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 8371b2dd71..90ae1a8892 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -442,7 +442,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): - 1 ) * cfg.num_epochs - * cfg.sequence_parallel_degree + * cfg.context_parallel_size * cfg.tensor_parallel_size ) LOG.debug( @@ -484,7 +484,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): math.floor( data_loader_len * cfg.num_epochs - * cfg.sequence_parallel_degree + * cfg.context_parallel_size * cfg.tensor_parallel_size ) ) @@ -511,7 +511,7 @@ def calc_sample_packing_eff_est(estimates: List[float]): math.ceil( len(train_dataset) * cfg.num_epochs - * cfg.sequence_parallel_degree + * cfg.context_parallel_size * cfg.tensor_parallel_size / cfg.batch_size ) diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index 040152beb9..5f1aec8ff8 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -64,7 +64,7 @@ def fixture_base_cfg(): "dataloader_num_workers": 1, "dataloader_pin_memory": True, "dataloader_prefetch_factor": 2, - "sequence_parallel_degree": 1, + "context_parallel_size": 1, "tensor_parallel_size": 1, # Dtype "fp16": False, diff --git a/tests/e2e/multigpu/patched/test_sp.py b/tests/e2e/multigpu/patched/test_sp.py index 80098e6841..a005e6742a 100644 --- a/tests/e2e/multigpu/patched/test_sp.py +++ b/tests/e2e/multigpu/patched/test_sp.py @@ -67,7 +67,7 @@ def _run_sequence_parallel_test( "logging_steps": 1, "weight_decay": 0.0, "use_tensorboard": True, - "sequence_parallel_degree": 2, + "context_parallel_size": 2, "ring_attn_func": ring_attn_func, "save_first_step": False, } @@ -105,13 +105,13 @@ def _run_sequence_parallel_test( (True, 1, True, None, 2.5), # defaults to varlen_llama3 ring_attn_func (False, 2, True, None, 2.5), # defaults to batch_ring ring_attn_func # (False, 2, True, "batch_zigzag", 2.5), - (False, 2, False, None, 2.65), # defaults to batch_ring ring_attn_func + # (False, 2, False, None, 2.65), # defaults to batch_ring ring_attn_func ], ids=[ "sample_packing, varlen_llama3 ring_attn_func", "no sample_packing, pad_to_sequence_len, batch_ring ring_attn_func", # "no sample_packing, no pad_to_sequence_len, batch_zigzag ring_attn_func", - "no sample_packing, no pad_to_sequence_len, batch_ring ring_attn_func", + # "no sample_packing, no pad_to_sequence_len, batch_ring ring_attn_func", ], ) def test_sequence_parallel_training( diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index d022ae2d92..92e0f70407 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -298,7 +298,7 @@ def test_llama_lora_sp(self, temp_dir): "lora_alpha": 16, "lora_dropout": 0.05, "lora_target_linear": True, - "sequence_parallel_degree": 2, + "context_parallel_size": 2, "flash_attention": True, "sequence_len": 1024, "special_tokens": { diff --git a/tests/e2e/multigpu/test_fp8_fsdp2.py b/tests/e2e/multigpu/test_fp8_fsdp2.py index 6423f5e2e2..f7fa29a314 100644 --- a/tests/e2e/multigpu/test_fp8_fsdp2.py +++ b/tests/e2e/multigpu/test_fp8_fsdp2.py @@ -13,7 +13,7 @@ from axolotl.utils.dict import DictDefault -from tests.e2e.utils import most_recent_subdir, require_torch_2_7_0 +from tests.e2e.utils import most_recent_subdir, require_hopper, require_torch_2_7_0 AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent @@ -51,6 +51,7 @@ class TestFP8FSDP2: """Test class for FP8 mixed precision with FSDP2 functionality.""" @require_torch_2_7_0 + @require_hopper def test_fp8_fsdp2_smoke(self, temp_dir): """Smoke test for 2-GPU FP8 + torch.compile + FSDP2 training""" cfg = DictDefault( diff --git a/tests/e2e/multigpu/test_tp.py b/tests/e2e/multigpu/test_tp.py new file mode 100644 index 0000000000..87a1c6339d --- /dev/null +++ b/tests/e2e/multigpu/test_tp.py @@ -0,0 +1,69 @@ +"""multigpu e2e test for tensor parallelism.""" + +from pathlib import Path + +import pytest +import yaml +from accelerate.test_utils import execute_subprocess_async, get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import check_tensorboard, require_torch_2_7_0 + + +class TestTensorParallel: + """Test class for Tensor Parallel functionality.""" + + @pytest.mark.skip( + reason="TP doesn't work with models with tied weights (embeddings)" + ) + @require_torch_2_7_0 + def test_fft_sft(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:10%]", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "tensor_parallel_size": 2, + "lr_scheduler": "cosine", + "flash_attention": True, + "use_tensorboard": True, + "bf16": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 1.0, "Train Loss (%s) is too high" + ) diff --git a/tests/e2e/patched/test_sp.py b/tests/e2e/patched/test_sp.py deleted file mode 100644 index 4a2c69d457..0000000000 --- a/tests/e2e/patched/test_sp.py +++ /dev/null @@ -1,481 +0,0 @@ -"""Tests for sequence parallelism functionality.""" - -# pylint: disable=redefined-outer-name,unused-argument - -import functools -import sys -from unittest.mock import MagicMock, patch - -import pytest -import torch -from accelerate.state import PartialState - -from axolotl.monkeypatch.ring_attn import ( - get_ring_attn_group, - register_ring_attn, - set_ring_attn_group, -) -from axolotl.utils.ctx_managers.sequence_parallel import apply_sequence_parallelism -from axolotl.utils.dict import DictDefault -from axolotl.utils.schemas.enums import RingAttnFunc -from axolotl.utils.schemas.trl import TRLConfig - - -@pytest.fixture -def partial_state(): - """Create a real PartialState instance for testing.""" - state = PartialState() - return state - - -@pytest.fixture(name="cfg") -def fixture_cfg(): - cfg = DictDefault( - { - "base_model": "HuggingFaceTB/SmolLM2-135M", - "datasets": [ - { - "path": "mhenrichsen/alpaca_2k_test", - "type": "alpaca", - }, - ], - "micro_batch_size": 1, - "gradient_accumulation_steps": 1, - "learning_rate": 1e-3, - "output_dir": "./model-out", - "sequence_len": 512, - "special_tokens": { - "pad_token": "<|endoftext|>", - }, - "save_first_step": False, - } - ) - - return cfg - - -@pytest.fixture -def sequence_parallel_batch(): - """Create a test batch for sequence parallelism tests.""" - batch_size = 1 - seq_len = 8 - - # Create test tensors - input_ids = torch.arange(batch_size * seq_len).reshape(batch_size, seq_len) - attention_mask = torch.ones(batch_size, seq_len) - position_ids = torch.arange(seq_len).expand(batch_size, seq_len) - labels = input_ids.clone() - - # Create test batch - batch = { - "input_ids": input_ids, - "attention_mask": attention_mask, - "position_ids": position_ids, - "labels": labels, - } - - return batch - - -class TestRingAttention: - """Tests for the ring attention functionality.""" - - @patch("torch.distributed.get_rank") - @patch("torch.distributed.get_world_size") - def test_get_ring_attn_group_no_registration( - self, mock_world_size, mock_rank, partial_state - ): - """Test that get_ring_attn_group raises RuntimeError when no group has been registered.""" - # Setup mocks - mock_world_size.return_value = 4 - mock_rank.return_value = 0 - - # Verify that RuntimeError is raised when no group is registered - with pytest.raises( - RuntimeError, match="register_ring_attn\\(\\) not yet called" - ): - get_ring_attn_group() - - @patch("torch.distributed.new_group") - @patch("torch.distributed.get_rank") - @patch("torch.distributed.get_world_size") - def test_register_ring_attn( - self, mock_world_size, mock_rank, mock_new_group, partial_state - ): - """Test that ring attention groups are created correctly.""" - # Setup mocks - mock_world_size.return_value = 8 # 8 GPUs total - mock_rank.return_value = 3 # GPU #3 - mock_group = MagicMock() - mock_new_group.return_value = mock_group - - # Call register_ring_attn with size 4 - register_ring_attn( - sequence_parallel_degree=4, - heads_k_stride=1, - ring_attn_func=RingAttnFunc.VARLEN_LLAMA3, - ) - - # Verify the number of calls without examining the arguments - assert mock_new_group.call_count == 2 - - # Verify that new_group was called - mock_new_group.assert_called() - - # Clean up - set_ring_attn_group(None) - - -class TestConfigValidation: - """Tests for validating sequence parallelism configurations.""" - - @pytest.fixture(autouse=True) - def setup_mocks(self, monkeypatch): - """Set up mocks for all tests in this class.""" - # Mock the ring_flash_attn module - monkeypatch.setitem(sys.modules, "ring_flash_attn", MagicMock()) - - @pytest.fixture - def base_cfg(self): - """Create a base configuration for testing.""" - return DictDefault( - { - "base_model": "HuggingFaceTB/SmolLM2-135M", - "datasets": [{"path": "mhenrichsen/alpaca_2k_test", "type": "alpaca"}], - "micro_batch_size": 1, - "gradient_accumulation_steps": 1, - "learning_rate": 1e-3, - "output_dir": "./model-out", - "sequence_len": 512, - "special_tokens": {"pad_token": "<|endoftext|>"}, - } - ) - - @pytest.mark.parametrize( - "config_updates, expected_values, should_pass, error_msg", - [ - # Valid configuration - ( - {"sequence_parallel_degree": 2, "flash_attention": True}, - {"sequence_parallel_degree": 2, "flash_attention": True}, - True, - None, - ), - # Default sequence_parallel_degree - ({}, {"sequence_parallel_degree": 1}, True, None), - # Invalid: sequence_parallel_degree > 1 without flash_attention - ( - {"sequence_parallel_degree": 2, "flash_attention": False}, - None, - False, - "flash_attention: true must be set", - ), - # Invalid: sequence_parallel_degree > 1 with sample_packing and micro_batch_size > 1 - ( - { - "sequence_parallel_degree": 2, - "flash_attention": True, - "sample_packing": True, - "micro_batch_size": 2, - "pad_to_sequence_len": True, - }, - None, - False, - "micro_batch_size must be set to 1", - ), - # Valid: Basic GRPO config - ( - { - "sequence_parallel_degree": 2, - "flash_attention": True, - "micro_batch_size": 2, - "trl": {"use_liger_loss": True}, - }, - { - "sequence_parallel_degree": 2, - "flash_attention": True, - "micro_batch_size": 2, - "trl": TRLConfig(use_liger_loss=True), - }, - True, - "GRPO + SP + Liger not currently supported", - ), - # Invalid: GRPO config with Liger loss - ( - { - "rl": "grpo", - "sequence_parallel_degree": 2, - "flash_attention": True, - "micro_batch_size": 2, - "trl": {"use_liger_loss": True}, - }, - None, - False, - "GRPO + SP + Liger not currently supported", - ), - ], - ids=[ - "valid_config", - "default_sp_degree", - "without_flash_attention", - "sample_packing_with_large_batch", - "valid_grpo", - "grpo_with_liger_loss", - ], - ) - def test_sequence_parallel_config_validation( - self, base_cfg, config_updates, expected_values, should_pass, error_msg - ): - """Test various sequence parallelism configuration scenarios.""" - from axolotl.utils.schemas.config import AxolotlInputConfig - - # Apply updates to base config - cfg = base_cfg - cfg.update(config_updates) - - if should_pass: - # Should validate without errors - config = AxolotlInputConfig(**cfg) - - # Check expected values - for key, value in expected_values.items(): - assert getattr(config, key) == value - else: - # Should raise exception - with pytest.raises(ValueError) as excinfo: - AxolotlInputConfig(**cfg) - assert error_msg in str(excinfo.value) - - @pytest.mark.parametrize( - "ring_attn_func, sample_packing, expected_func", - [ - (None, True, RingAttnFunc.VARLEN_LLAMA3), - (None, False, RingAttnFunc.BATCH_RING), - ], - ids=["default_with_sample_packing", "default_without_sample_packing"], - ) - def test_ring_attn_func_validation( - self, base_cfg, ring_attn_func, sample_packing, expected_func - ): - """Test ring_attn_func validation and defaults.""" - from axolotl.utils.schemas.config import AxolotlInputConfig - - # Apply updates to base config - cfg = base_cfg | { - "sequence_parallel_degree": 2, - "flash_attention": True, - "sample_packing": sample_packing, - } - - if ring_attn_func is not None: - cfg["ring_attn_func"] = ring_attn_func - - # Should validate without errors - config = AxolotlInputConfig(**cfg) - - # Check ring_attn_func value - assert config.ring_attn_func.value == expected_func - - def test_invalid_ring_attn_func(self, base_cfg): - """Test that an invalid ring_attn_func is rejected.""" - from axolotl.utils.schemas.config import AxolotlInputConfig - - # Invalid configuration with invalid ring_attn_func - cfg = base_cfg | { - "sequence_parallel_degree": 2, - "flash_attention": True, - "ring_attn_func": "INVALID_FUNC", - } - - # Should raise ValidationError - with pytest.raises(ValueError) as excinfo: - AxolotlInputConfig(**cfg) - - # Verify error message - assert "Input should be 'varlen_llama3' or 'batch_ring'" in str(excinfo.value) - - -class TestApplySequenceParallelism: - """Tests for the apply_sequence_parallelism function.""" - - @pytest.fixture(autouse=True) - def mock_distributed(self, monkeypatch): - """Mock torch.distributed functions for testing.""" - # Mock is_initialized to return True - monkeypatch.setattr(torch.distributed, "is_initialized", lambda: True) - - # Mock get_rank to return 0 by default - monkeypatch.setattr(torch.distributed, "get_rank", lambda *args, **kwargs: 0) - - # Mock get_world_size to return 2 by default - monkeypatch.setattr( - torch.distributed, "get_world_size", lambda *args, **kwargs: 2 - ) - - # Mock the process group - monkeypatch.setattr( - "axolotl.monkeypatch.ring_attn.get_ring_attn_group", - MagicMock, - ) - - # Mock update_ring_attn_params - monkeypatch.setattr( - "axolotl.monkeypatch.ring_attn.update_ring_attn_params", - lambda **kwargs: None, - ) - - @patch("axolotl.monkeypatch.ring_attn.patch.get_ring_attn_group") - def test_world_size_one(self, mock_get_ring_attn_group, sequence_parallel_batch): - """Test that function returns original batch when world size is 1.""" - mock_get_ring_attn_group.return_value = 0 - - result, _, _ = apply_sequence_parallelism( - batch=sequence_parallel_batch, - local_rank=0, - local_world_size=1, - gradient_accumulation_steps=1, - ring_attn_func=RingAttnFunc.BATCH_RING, - ) - - # Should return the original batch unchanged - assert result == sequence_parallel_batch - - @patch("axolotl.monkeypatch.ring_attn.patch.get_ring_attn_group") - def test_batch_ring_rank0(self, mock_get_ring_attn_group, sequence_parallel_batch): - """Test BATCH_RING sharding for rank 0 in a 2-process group.""" - mock_get_ring_attn_group.return_value = 0 - - batch = sequence_parallel_batch - seq_len = batch["input_ids"].size(1) - - result, _, _ = apply_sequence_parallelism( - batch=batch, - local_rank=0, - local_world_size=2, - gradient_accumulation_steps=1, - ring_attn_func=RingAttnFunc.BATCH_RING, - ) - - # Check that sequence dimension was sharded correctly - assert result["input_ids"].shape[1] == seq_len // 2 - assert result["attention_mask"].shape[1] == seq_len // 2 - - # Verify content: rank 0 should get the first half of the sequence - assert torch.equal(result["input_ids"], batch["input_ids"][:, : seq_len // 2]) - assert torch.equal( - result["position_ids"], batch["position_ids"][:, : seq_len // 2] - ) - - @patch("axolotl.monkeypatch.ring_attn.patch.get_ring_attn_group") - def test_batch_ring_rank1(self, mock_get_ring_attn_group, sequence_parallel_batch): - """Test BATCH_RING sharding for rank 1 in a 2-process group.""" - mock_get_ring_attn_group.return_value = 0 - - batch = sequence_parallel_batch - seq_len = batch["input_ids"].size(1) - original_input_ids = batch["input_ids"].clone() - - result, _, _ = apply_sequence_parallelism( - batch=batch, - local_rank=1, - local_world_size=2, - gradient_accumulation_steps=1, - ring_attn_func=RingAttnFunc.BATCH_RING, - ) - - # Verify content: rank 1 should get the second half of the sequence - assert torch.equal(result["input_ids"], original_input_ids[:, seq_len // 2 :]) - - # TODO(djsaunde): add back once implemented. - # def test_batch_zigzag(self, sequence_parallel_batch): - # """Test BATCH_ZIGZAG sharding pattern.""" - # batch = sequence_parallel_batch - # original_input_ids = batch["input_ids"].clone() - # seq_len = batch["input_ids"].size(1) - - # # Test rank 0 - # result_rank0 = apply_sequence_parallelism( - # batch={k: v.clone() for k, v in batch.items()}, - # local_rank=0, - # local_world_size=2, - # ring_attn_func=RingAttnFunc.BATCH_ZIGZAG, - # ) - - # # Test rank 1 - # result_rank1 = apply_sequence_parallelism( - # batch={k: v.clone() for k, v in batch.items()}, - # local_rank=1, - # local_world_size=2, - # ring_attn_func=RingAttnFunc.BATCH_ZIGZAG, - # ) - - # # Checks for both ranks - # assert result_rank0["input_ids"].shape[1] == seq_len // 2 - # assert result_rank1["input_ids"].shape[1] == seq_len // 2 - - # # For a 2-rank system with 8 tokens, check specific zigzag pattern - # # Rank 0 should get chunks [0, 1] and [6, 7] - # # Rank 1 should get chunks [2, 3] and [4, 5] - # if seq_len == 8: - # # Create expected tensors for comparison - # rank0_expected = torch.cat( - # [original_input_ids[:, :2], original_input_ids[:, 6:8]], dim=1 - # ) - - # rank1_expected = torch.cat( - # [original_input_ids[:, 2:4], original_input_ids[:, 4:6]], dim=1 - # ) - - # assert torch.equal(result_rank0["input_ids"], rank0_expected) - # assert torch.equal(result_rank1["input_ids"], rank1_expected) - - @patch("axolotl.monkeypatch.ring_attn.patch.get_ring_attn_group") - def test_partial_application( - self, mock_get_ring_attn_group, sequence_parallel_batch - ): - """Test that we can create a partially applied version of the function.""" - mock_get_ring_attn_group.return_value = 0 - - batch = sequence_parallel_batch - original_input_ids = batch["input_ids"].clone() - - # Create a partially applied function - rank0_ring_parallel = functools.partial( - apply_sequence_parallelism, - local_rank=0, - local_world_size=2, - gradient_accumulation_steps=1, - ring_attn_func=RingAttnFunc.BATCH_RING, - ) - - # Use the partially applied function - result, _, _ = rank0_ring_parallel(batch=batch) - - # Verify it works as expected - assert result["input_ids"].shape[1] == original_input_ids.shape[1] // 2 - assert torch.equal( - result["input_ids"], - original_input_ids[:, : original_input_ids.shape[1] // 2], - ) - - def test_missing_position_ids(self, sequence_parallel_batch): - """Test handling of batch without position_ids.""" - # Create a batch without position_ids - batch = { - k: v for k, v in sequence_parallel_batch.items() if k != "position_ids" - } - original_input_ids = batch["input_ids"].clone() - - # This should run without error even though position_ids is missing - result, _, _ = apply_sequence_parallelism( - batch=batch, - local_rank=0, - local_world_size=2, - gradient_accumulation_steps=1, - ring_attn_func=RingAttnFunc.BATCH_RING, - ) - - # Verification should pass - assert "position_ids" in result - assert result["input_ids"].shape[1] == result["position_ids"].shape[1] - assert result["input_ids"].shape[1] == original_input_ids.shape[1] // 2 diff --git a/tests/e2e/test_load_model.py b/tests/e2e/test_load_model.py index 5061945b44..8fcffeb114 100644 --- a/tests/e2e/test_load_model.py +++ b/tests/e2e/test_load_model.py @@ -52,6 +52,8 @@ def setup_method(self): "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", + "tensor_parallel_size": 1, + "context_parallel_size": 1, } ) self.model_loader = ( # pylint: disable=attribute-defined-outside-init diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 696e3b03c2..5931fe148a 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -142,6 +142,10 @@ def is_hopper(): return compute_capability == (9, 0) +def require_hopper(test_case): + return unittest.skipUnless(is_hopper(), "test requires h100/hopper GPU")(test_case) + + def check_tensorboard( temp_run_dir: str, tag: str, lt_val: float, assertion_err: str ) -> None: diff --git a/tests/test_loaders.py b/tests/test_loaders.py index 7313a82670..def7672b97 100644 --- a/tests/test_loaders.py +++ b/tests/test_loaders.py @@ -171,3 +171,44 @@ def test_message_property_mapping(self): message_property_mappings={"content": "different_content"}, ) assert "Conflicting message content fields" in str(exc_info.value) + + @pytest.mark.parametrize( + "world_size, tensor_parallel_size, context_parallel_size, dp_shard_size, dp_replicate_size, is_fsdp, expected", + [ + (16, 2, 2, 2, 2, True, (2, 2, 2, 2)), + (16, 1, 1, None, None, True, (0, 0, 16, 1)), + (16, 2, 2, 2, None, True, (2, 2, 2, 2)), + (16, 2, 2, None, 2, True, (2, 2, 2, 2)), + (16, 1, 1, None, 2, True, (0, 0, 8, 2)), + (2, 1, 1, None, None, True, (0, 0, 2, 1)), + ], + ) + def test_get_parallel_config_kwargs( + self, + world_size, + tensor_parallel_size, + context_parallel_size, + dp_shard_size, + dp_replicate_size, + is_fsdp, + expected, + ): + res = ( + ModelLoader._get_parallel_config_kwargs( # pylint: disable=protected-access + world_size, + tensor_parallel_size, + context_parallel_size, + dp_shard_size, + dp_replicate_size, + is_fsdp, + ) + ) + + if expected[0] > 1: + assert res["tp_size"] == expected[0] + if expected[1] > 1: + assert res["cp_size"] == expected[1] + if expected[2] > 1: + assert res["dp_shard_size"] == expected[2] + if expected[3] > 1: + assert res["dp_replicate_size"] == expected[3] diff --git a/tests/utils/schemas/validation/test_fsdp.py b/tests/utils/schemas/validation/test_fsdp.py index 67f4a5cf90..5b461a1136 100644 --- a/tests/utils/schemas/validation/test_fsdp.py +++ b/tests/utils/schemas/validation/test_fsdp.py @@ -26,32 +26,6 @@ def test_fsdp_version_in_fsdp_config(self, min_base_cfg): assert cfg.fsdp_version == 2 assert cfg.fsdp_config.fsdp_version is None - def test_fsdp_sharded_state_dict_safetensors(self, min_base_cfg): - cfg = min_base_cfg | DictDefault( - fsdp_config={ - "fsdp_state_dict_type": "SHARDED_STATE_DICT", - }, - save_safetensors=True, - ) - with pytest.raises( - ValueError, - match="FSDP SHARDED_STATE_DICT not compatible with save_safetensors", - ): - validate_config(cfg) - - # test w/o prefix too - cfg = min_base_cfg | DictDefault( - fsdp_config={ - "state_dict_type": "SHARDED_STATE_DICT", - }, - save_safetensors=True, - ) - with pytest.raises( - ValueError, - match="FSDP SHARDED_STATE_DICT not compatible with save_safetensors", - ): - validate_config(cfg) - def test_fsdp_offload_w_8bit_optim(self, min_base_cfg): cfg = min_base_cfg | DictDefault( fsdp_config={ From eb0a8a7775f7ab2dcf599add2af6c14cc8da6cb4 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 1 Aug 2025 05:18:44 +0700 Subject: [PATCH 0868/1405] feat: upgrade cce commit to include smollm3, granite, granitemoe (#2993) --- examples/colab-notebooks/colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/integrations/cut_cross_entropy/README.md | 6 +++++- src/axolotl/integrations/cut_cross_entropy/__init__.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index c66c5c892f..b072aeb447 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@010c3ac3f1e725098961832830303eeb4142dd88\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@849c3c5\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 404d6361d0..a213f358d2 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@010c3ac3f1e725098961832830303eeb4142dd88"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@849c3c5"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 9daabc8626..4efbd7e218 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@010c3ac3f1e725098961832830303eeb4142dd88" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@849c3c5" ``` ## Usage @@ -41,6 +41,8 @@ plugins: - gemma3n_text - glm - glm4 +- granite +- granitemoe - llama - llama4 - llama4_text @@ -56,6 +58,8 @@ plugins: - qwen2_5_vl - qwen3 - qwen3_moe +- smollm3 +- voxtral ## Citation diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index e6a52e8d8c..6bf673ab29 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -34,7 +34,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@010c3ac3f1e725098961832830303eeb4142dd88"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@849c3c5"`' ) From 7026cd5e9e053d51aa271c1f57f62950bcdc599f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 1 Aug 2025 13:18:31 +0700 Subject: [PATCH 0869/1405] Feat: Add N-D parallelism docs (#2989) * fix: remove non-existent file * feat: add n-d parallel docs * fix: comments --------- Co-authored-by: salman --- _quarto.yml | 2 +- docs/nd_parallelism.qmd | 102 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 docs/nd_parallelism.qmd diff --git a/_quarto.yml b/_quarto.yml index bfef13afb4..738fe5e2fa 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -274,7 +274,6 @@ website: - docs/dataset_preprocessing.qmd - docs/multipack.qmd - docs/mixed_precision.qmd - - docs/gradient_accumulation.qmd - section: "Advanced Features" contents: @@ -284,6 +283,7 @@ website: - docs/custom_integrations.qmd - docs/sequence_parallelism.qmd - docs/gradient_checkpointing.qmd + - docs/nd_parallelism.qmd - section: "Troubleshooting" contents: diff --git a/docs/nd_parallelism.qmd b/docs/nd_parallelism.qmd new file mode 100644 index 0000000000..7c2d2e0cbf --- /dev/null +++ b/docs/nd_parallelism.qmd @@ -0,0 +1,102 @@ +# N-D Parallelism + +Axolotl enables training models at scale by composing different parallelism techniques. This is essential when: + +- A model's weights are too large to fit on a single GPU's memory. +- A model's activations, especially with very long contexts, are too large for a single GPU. +- You want to accelerate training by using multiple GPUs or nodes. + +or combinations of the above! + +## Core Concepts + +Parallelism strategies can be combined. The key is understanding how each one divides the workload. PyTorch's `DeviceMesh` is the modern way to manage these combinations, creating a logical grid of your GPUs and assigning different parallel strategies to different dimensions of the grid. + +### Data Parallelism {#sec-dp} + +Data Parallelism focuses on splitting the global data batch across GPUs. + +- Distributed Data Parallel (DDP): The classic approach. The full model is replicated on every GPU. Each GPU processes a different slice of the data batch. Gradients are then averaged across all GPUs after the backward pass to keep the models synchronized. This can substantially improve data throughput compared to single-device training, but requires that each GPU is able to hold the entire model, its gradients, and optimizer states. + +- [Fully Sharded Data Parallel (FSDP)](multi-gpu.qmd#fully-sharded-data-parallel-(fsdp)): A highly memory-efficient form of data parallelism (inspired by DeepSpeed's ZeRO). Instead of replicating the model, FSDP shards the model's *parameters, gradients, and optimizer states* across the GPUs in the data-parallel group. During computation, each GPU receives the specific parameters it needs via an `all_gather` operation just before they are used, and they can be discarded immediately after (`reshard-after-forward`). + - FSDP maps to ZeRO stages: + - ZeRO-2 (`reshard_after_forward=False`): Shards gradients and optimizer states. Model weights are replicated on each GPU. + - ZeRO-3 (`reshard_after_forward=True`): Shards gradients, optimizer states, AND model parameters. This provides the most memory savings at the cost of more communication (re-gathering parameters for both forward and backward passes). + +### [Experimental] Tensor Parallelism (TP) {#sec-tp} + +Also known as "horizontal model parallelism," as described in the [Megatron-LM paper](https://arxiv.org/pdf/1909.08053.pdf). Instead of splitting the batch, TP splits the model's layers themselves across GPUs. + +- How it works: For a linear layer `Y = XA`, the weight matrix `A` is split column-wise (`A = [A_1, A_2]`). The computation becomes `Y_1 = XA_1` and `Y_2 = XA_2`, which can happen in parallel on different GPUs. The final output `Y` is simply the concatenation of `Y_1` and `Y_2`. Check [this comment](https://github.com/huggingface/transformers/issues/10321#issuecomment-783543530) for more detailed info. +- Requirement: TP involves frequent, small communications within a forward/backward pass. It requires a very fast interconnect between GPUs (e.g., NVLink) and is typically not recommended across different nodes. + +### Context Parallelism (CP) {#sec-cp} + +Context Parallelism, also called [Sequence Parallelism](sequence_parallelism.qmd), addresses the memory bottleneck from long sequences. The input sequence itself is split along the sequence length dimension and distributed across GPUs. + +- How it works: If you have a sequence of 8192 tokens and a `context_parallel_size` of 4, each GPU will only handle a chunk of 2048 tokens. +- The Challenge: Attention is not local; every token needs to "attend to" every other token. Splitting the sequence breaks this. +- The Solution (`ring-flash-attention`): An efficient communication protocol is used. To compute attention for its local sequence chunk, each GPU passes its Key-Value (KV) cache to its neighbor in a "ring." After `N-1` steps, every GPU has seen the KV-cache from all other GPUs, allowing it to compute the correct attention values for its chunk. This is implemented using the highly optimized `flash-attention` kernel at each step. + +### Hybrid Sharding Data Parallel (HSDP) {#sec-hsdp} + +HSDP is a 2D strategy that intelligently combines FSDP and DDP, typically for multi-node training. + +- Intra-Node (within a machine): Use FSDP. This is efficient because GPUs on the same node have fast interconnects (NVLink), making the `all_gather` operations for sharded parameters fast. +- Inter-Node (across machines): Use DDP. The gradient synchronization between nodes is less frequent than FSDP's parameter gathering, making it a better fit for the slower node-to-node network (e.g., Ethernet/Infiniband). +- Example: With 2 nodes of 8 GPUs each (16 total), you could have `dp_shard_size=8` (FSDP within each node) and `dp_replicate_size=2` (DDP across the two nodes). + +## Usage + +```yaml +# FSDP config. See https://docs.axolotl.ai/docs/multi-gpu.html#sec-fsdp +fsdp_version: 2 +fsdp_config: + # ... + +# The number of GPUs to shard the model parameters across (FSDP dimension). +dp_shard_size: 4 + +# The number of times to replicate the sharded model (DDP dimension). +dp_replicate_size: 2 + +# Number of GPUs for Tensor Parallelism. +tensor_parallel_size: 1 # (default is 1, no TP) + +# Number of GPUs for Context/Sequence Parallelism. +context_parallel_size: 1 # (default is 1, no CP) +``` + +Note: We recommend FSDP. DeepSpeed is only compatible with `tensor_parallel_size`. + +## Examples + +1. HSDP on 2 nodes with 4 GPUs each (8 GPUs total): + - You want FSDP within each node and DDP across nodes. + - Set `dp_shard_size: 4` and `dp_replicate_size: 2`. + +2. FSDP + TP on a single 8-GPU node: + - You want to split the model across 4 GPUs using FSDP, and further split each layer across 2 GPUs with TP. + - Set `dp_shard_size: 4` and `tensor_parallel_size: 2`. + +3. FSDP + CP on a single 8-GPU node for long context: + - You want to shard the model across all 8 GPUs and also split the sequence length across all 8 GPUs. + - Set `dp_shard_size: 8` and `context_parallel_size: 8`. Note: this means the data parallel group and context parallel group are the same. A more common setup might be to shard across a smaller group. + +## Support Matrix + +This matrix describes how different parallelism methods can be combined in Axolotl. + +| Combination | `dp_replicate_size` | `dp_shard_size` | `tp_size` | `cp_size` | Status & Notes | +| --- | :---: | :---: |:---:|:---:|---| +| **FSDP** (ZeRO-3) | 1 | >1 | 1 | 1 | ✅ Fully supported. Shards model across all GPUs. | +| **HSDP** | >1 | >1 | 1 | 1 | ✅ Fully supported. FSDP intra-node, DDP inter-node. | +| **FSDP + TP** | 1 | >1 | >1 | 1 | ✅ **2D Parallelism**. Shards the model across a `dp_shard` group, and TP-splits layers within the `tp` group. | +| **HSDP + TP** | >1 | >1 | >1 | 1 | ✅ **3D Parallelism**. A powerful but complex combination. | +| **FSDP + CP** | 1 | >1 | 1 | >1 | ✅ **2D Parallelism**. Combines FSDP with context parallelism. | +| **FSDP + TP + CP**| 1 | >1 | >1| >1| ✅ **3D Parallelism**. Another advanced combination. | +| DDP + TP/CP | >1 | 1 | >1 | >1 | ❌ **Not Supported**. The `ParallelismConfig` explicitly prevents this, as composing pure DDP with TP/CP without FSDP is inefficient and complex. You should use FSDP instead (`dp_shard_size > 1`). | +| Just TP / CP | 1 | 1 | >1 | >1 | ✅ Supported. Useful for inference or when the model fits on one GPU but context is too long. | + +- `tp_size` refers to `tensor_parallel_size` +- `cp_size` refers to `context_parallel_size` From 02a37199ee6a03080afc2a2b8e7ea81661289f5d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 1 Aug 2025 09:59:45 -0400 Subject: [PATCH 0870/1405] prevent empty value for vllm_mode (#2998) --- src/axolotl/core/trainers/grpo/__init__.py | 3 ++- src/axolotl/utils/schemas/validation.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 839c20c2e3..4106a2a7d5 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -49,7 +49,8 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: if trl.use_vllm: grpo_args_kwargs["use_vllm"] = trl.use_vllm - grpo_args_kwargs["vllm_mode"] = trl.vllm_mode + if trl.vllm_mode: + grpo_args_kwargs["vllm_mode"] = trl.vllm_mode if trl.vllm_mode == "colocate": grpo_args_kwargs["vllm_gpu_memory_utilization"] = ( vllm_cfg.gpu_memory_utilization diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 502c18e7de..aa249c6ce2 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1268,6 +1268,19 @@ def check_tensor_parallel_optimizer(self): return self +class GRPOVllmValidationMixin: + """Validation mixin for vllm when using GRPO.""" + + @model_validator(mode="after") + def check_vllm_mode_set(self): + if self.trl and self.trl.use_vllm and not self.trl.vllm_mode: + LOG.warning( + "vllm_mode must be set to either `server` or `colocate` when using vllm, using default value `server`" + ) + self.trl.vllm_mode = "server" + return self + + # pylint: disable=too-many-ancestors class ValidationMixin( DatasetValidationMixin, @@ -1281,5 +1294,6 @@ class ValidationMixin( PretrainingValidationMixin, ModelCompatibilityValidationMixin, ComplexValidationMixin, + GRPOVllmValidationMixin, ): """Full validation mixin for Axolotl configuration.""" From 41709822a7382ce3a1402beb00f3a8bacded4b24 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 2 Aug 2025 00:21:43 +0700 Subject: [PATCH 0871/1405] fix: move memory usage log to trainer.log (#2996) [skip ci] --- src/axolotl/core/builders/base.py | 3 --- src/axolotl/core/trainers/base.py | 13 ++++++++++ src/axolotl/utils/callbacks/__init__.py | 34 ------------------------- 3 files changed, 13 insertions(+), 37 deletions(-) diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 32b228e21d..dbdda7a7cd 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -36,7 +36,6 @@ from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.callbacks import ( GCCallback, - GPUStatsCallback, SaveAxolotlConfigtoWandBCallback, SaveModelOnFirstStepCallback, ) @@ -141,8 +140,6 @@ def get_callbacks(self) -> list[TrainerCallback]: if self.cfg.save_first_step: callbacks.append(SaveModelOnFirstStepCallback()) - callbacks.append(GPUStatsCallback(cfg=self.cfg)) - if self.cfg.profiler_steps: callbacks.append( PytorchProfilerCallback( diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index e3818ca7cc..f739d19e95 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -38,6 +38,8 @@ sanitize_kwargs_for_tagging, ) from axolotl.utils import get_not_null +from axolotl.utils.bench import get_gpu_memory_usage +from axolotl.utils.distributed import is_main_process from axolotl.utils.logging import get_logger from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths @@ -560,6 +562,17 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: # Add averaged stored metrics to logs for key, metrics in self._stored_metrics[train_eval].items(): logs[key] = torch.tensor(metrics).mean().item() + + if is_main_process(): + # Add memory usage + try: + active, allocated, reserved = get_gpu_memory_usage() + logs["memory/max_memory_active"] = active + logs["memory/max_memory_allocated"] = allocated + logs["memory/device_memory_reserved"] = reserved + except (ValueError, FileNotFoundError): + pass + del self._stored_metrics[train_eval] return super().log(logs, start_time) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 63799c734b..d3f3126b5d 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -35,7 +35,6 @@ from trl.models import unwrap_model_for_generation from axolotl.utils import is_comet_available, is_mlflow_available -from axolotl.utils.bench import get_gpu_memory_usage, log_gpu_memory_usage from axolotl.utils.callbacks.perplexity import Perplexity from axolotl.utils.distributed import ( barrier, @@ -93,39 +92,6 @@ def on_step_end( return control -class GPUStatsCallback( - TrainerCallback -): # pylint: disable=too-few-public-methods disable=unused-argument - """Callback to track GPU utilization""" - - def __init__(self, cfg): - self.cfg = cfg - - def on_step_end( - self, - args: TrainingArguments, # pylint: disable=unused-argument - state: TrainerState, - control: TrainerControl, - **kwargs, - ) -> TrainerControl: - if state.global_step > 0: - if self.cfg.use_wandb and state.is_world_process_zero: - try: - active, allocated, reserved = get_gpu_memory_usage() - wandb.log( - { - "memory/max_memory_active": active, - "memory/max_memory_allocated": allocated, - "memory/device_memory_reserved": reserved, - }, - step=state.global_step, - ) - except ValueError: - pass - log_gpu_memory_usage(LOG, "", self.cfg.device) - return control - - class LossWatchDogCallback(TrainerCallback): """Callback to track loss and stop training if loss is too high""" From 01a6bd1a0ec59a1017a2b251b4a3e4e29cc114a3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 1 Aug 2025 13:21:58 -0400 Subject: [PATCH 0872/1405] use CCE fix for TP using vocab parallel for CEL (#3000) --- examples/colab-notebooks/colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/integrations/cut_cross_entropy/README.md | 2 +- src/axolotl/integrations/cut_cross_entropy/__init__.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index b072aeb447..6c6e21f94a 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@849c3c5\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@cbd58e0\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index a213f358d2..e767494933 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@849c3c5"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@cbd58e0"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 4efbd7e218..048559789e 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@849c3c5" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@cbd58e0" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 6bf673ab29..d1419e27e9 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -34,7 +34,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@849c3c5"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@cbd58e0"`' ) From 7c3b428f2344da7888dfe3cc87e2ee9cab8d49f6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 1 Aug 2025 13:58:16 -0400 Subject: [PATCH 0873/1405] Add validation for TP with models with tied embeddings (#2999) * add validation for tp + tied embeddings models * fix logic and messaging * add additional guard for null tp size --- src/axolotl/loaders/utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/axolotl/loaders/utils.py b/src/axolotl/loaders/utils.py index 4b93d14acf..240e00da72 100644 --- a/src/axolotl/loaders/utils.py +++ b/src/axolotl/loaders/utils.py @@ -131,6 +131,17 @@ def check_model_config(cfg: DictDefault, model_config: PretrainedConfig): f"Please include [{lora_modules_to_save_joined}] in `lora_modules_to_save`." ) + if ( + cfg.tensor_parallel_size + and cfg.tensor_parallel_size > 1 + and hasattr(model_config, "tie_word_embeddings") + and model_config.tie_word_embeddings + ): + raise ValueError( + "Tensor parallelism is incompatible with models configured with `tie_word_embeddings` enabled. " + "Please use a model without `tie_word_embeddings`, or disable tensor parallelism." + ) + def load_model_config(cfg: DictDefault) -> PretrainedConfig | addict.Dict: """Loads and configures a model configuration from HuggingFace or local sources. From cda3c82351aedf91161702d1ca4d2cac6acd4220 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 1 Aug 2025 16:10:37 -0400 Subject: [PATCH 0874/1405] move ib/rdma libs into base image (#3002) * move ib/rdma libs into base image * use --no-install-recommends --- docker/Dockerfile-base | 5 ++++- docker/Dockerfile-cloud | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 06eb8206c2..0434a583fb 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -16,7 +16,10 @@ ENV PYTHON_VERSION=$PYTHON_VERSION ENV TORCH_CUDA_ARCH_LIST=$TORCH_CUDA_ARCH_LIST RUN apt-get update \ - && apt-get install -y wget git build-essential ninja-build git-lfs libaio-dev pkg-config \ + && apt-get install -y --no-install-recommends \ + wget git build-essential ninja-build git-lfs libaio-dev pkg-config \ + ibverbs-providers ibverbs-utils infiniband-diags \ + librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm \ && rm -rf /var/cache/apt/archives \ && rm -rf /var/lib/apt/lists/* \ && wget \ diff --git a/docker/Dockerfile-cloud b/docker/Dockerfile-cloud index 590b737065..6ab0908261 100644 --- a/docker/Dockerfile-cloud +++ b/docker/Dockerfile-cloud @@ -15,7 +15,7 @@ COPY scripts/motd /etc/motd RUN pip install jupyterlab notebook ipywidgets && \ jupyter lab clean RUN apt update && \ - apt install --yes --no-install-recommends openssh-server tmux iproute2 nvtop ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm && \ + apt install --yes --no-install-recommends openssh-server tmux iproute2 nvtop && \ rm -rf /var/cache/apt/archives && \ rm -rf /var/lib/apt/lists/* && \ mkdir -p ~/.ssh && \ From 5639552064c838d4cf3016570b30cba182962d49 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 1 Aug 2025 17:54:04 -0400 Subject: [PATCH 0875/1405] prevent usage of low bit ao optimizers with configurations that use parameter groups (#3003) * prevent usage of low bit ao optimizers with configurations that use parameter groups * use optimizer enum value * fix validation --- src/axolotl/utils/schemas/validation.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index aa249c6ce2..02e80dd8e0 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -880,6 +880,23 @@ def check_fsdp2_w_8bit_optimizer(self): return self + @model_validator(mode="after") + def lr_groups_ao_optimizer(self): + if ( + self.loraplus_lr_ratio is not None + or self.embedding_lr_scale is not None + or self.embedding_lr is not None + or self.lr_groups is not None + ) and self.optimizer.value in ["adamw_torch_8bit", "adamw_torch_4bit"]: + # TODO(wing): remove this once ao>0.12.0 + # requires https://github.com/pytorch/ao/pull/2606 in an ao release + raise ValueError( + "lr groups (`loraplus_lr_ratio`, `embedding_lr_scale`, `embedding_lr`, `lr_groups`) are not " + "supported with ao low-bit optimizers until ao>0.12.0. " + "Please refer to https://github.com/pytorch/ao/pull/2606." + ) + return self + @model_validator(mode="before") @classmethod def check_tensor_parallel_size_update_ds_json(cls, data): From 10946afae7877dcb8d8a6d574eb4eec3cf411e2a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 2 Aug 2025 11:19:24 -0400 Subject: [PATCH 0876/1405] fixes for spinning up vllm service for grpo (#3001) --- .github/workflows/main.yml | 5 ++- src/axolotl/cli/vllm_serve.py | 78 +++++------------------------------ 2 files changed, 13 insertions(+), 70 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 444ebfde80..8913002464 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,12 +24,13 @@ jobs: cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.7.0 - axolotl_extras: vllm + axolotl_extras: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.7.1 - axolotl_extras: + axolotl_extras: vllm + is_latest: true - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" diff --git a/src/axolotl/cli/vllm_serve.py b/src/axolotl/cli/vllm_serve.py index f092cc59a0..cf687bea2a 100644 --- a/src/axolotl/cli/vllm_serve.py +++ b/src/axolotl/cli/vllm_serve.py @@ -2,12 +2,10 @@ CLI to start the vllm server for online RL """ -import os from dataclasses import dataclass, field from pathlib import Path from typing import Union -import trl from trl.scripts.vllm_serve import ScriptArguments from axolotl.cli.config import load_cfg @@ -42,13 +40,17 @@ def do_vllm_serve( serve_module = cli_args.get("serve_module", "trl.scripts.vllm_serve") vllm_serve_main = getattr(__import__(serve_module, fromlist=["main"]), "main") + tensor_parallel_size = 1 + data_parallel_size = 1 - tensor_parallel_size = ( - cli_args.get("tensor_parallel_size") or cfg.vllm.tensor_parallel_size - ) - data_parallel_size = ( - cli_args.get("data_parallel_size") or cfg.vllm.data_parallel_size - ) + if cli_args.get("tensor_parallel_size") or cfg.vllm.tensor_parallel_size: + tensor_parallel_size = ( + cli_args.get("tensor_parallel_size") or cfg.vllm.tensor_parallel_size + ) + if cli_args.get("data_parallel_size") or cfg.vllm.data_parallel_size: + data_parallel_size = ( + cli_args.get("data_parallel_size") or cfg.vllm.data_parallel_size + ) host = cli_args.get("host") or cfg.vllm.host port = cli_args.get("port") or cfg.vllm.port gpu_memory_utilization = ( @@ -81,63 +83,3 @@ def do_vllm_serve( enable_reasoning=enable_reasoning, ) vllm_serve_main(vllm_script_args) - - -def patch_vllm_worker(): - from multiprocessing.connection import Connection - - from vllm import LLM - - def llm_worker( - script_args: AxolotlScriptArguments, - data_parallel_rank: int, - master_port: int, - connection: Connection, - ) -> None: - # Set required environment variables for DP to work with vLLM - os.environ["VLLM_DP_RANK"] = str(data_parallel_rank) - os.environ["VLLM_DP_RANK_LOCAL"] = str(data_parallel_rank) - os.environ["VLLM_DP_SIZE"] = str(script_args.data_parallel_size) - os.environ["VLLM_DP_MASTER_PORT"] = str(master_port) - - llm = LLM( - model=script_args.model, - revision=script_args.revision, - tensor_parallel_size=script_args.tensor_parallel_size, - gpu_memory_utilization=script_args.gpu_memory_utilization, - enforce_eager=script_args.enforce_eager, - dtype=script_args.dtype, - # Automatic Prefix Caching caches the KV cache of existing queries, so that a new query can - # directly reuse the KV cache if it shares the same prefix with one of the existing queries. - # This is particularly useful here because we generate completions from the same prompts. - enable_prefix_caching=script_args.enable_prefix_caching, - kv_cache_dtype=script_args.kv_cache_dtype, - max_model_len=script_args.max_model_len, - worker_extension_cls="trl.scripts.vllm_serve.WeightSyncWorkerExtension", - enable_reasoning=script_args.enable_reasoning, - reasoning_parser=script_args.reasoning_parser, - ) - - # Send ready signal to parent process - connection.send({"status": "ready"}) - - while True: - # Wait for commands from the parent process - try: - command = connection.recv() - except KeyboardInterrupt: - llm.collective_rpc(method="close_communicator") - break - - # Handle commands - if command["type"] in ["call", "fire_and_forget"]: - method_name = command["method"] - args, kwargs = command.get("args", ()), command.get("kwargs", {}) - method = getattr(llm, method_name) - result = method(*args, **kwargs) - if command["type"] == "call": - connection.send(result) - elif command["type"] == "shutdown": - break - - trl.scripts.vllm_serve.llm_worker = llm_worker From deac7b18a1b7f02dbd373acd3f3ac76fa4540a4f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 2 Aug 2025 20:24:04 -0400 Subject: [PATCH 0877/1405] upgrade peft v0.17.0 and support for lora target_parameters (#3006) --- requirements.txt | 2 +- src/axolotl/loaders/adapter.py | 2 ++ src/axolotl/utils/schemas/peft.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4fc662a87d..2d72f307a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ liger-kernel==0.6.1 packaging==23.2 huggingface_hub>=0.33.0 -peft==0.16.0 +peft==0.17.0 transformers==4.54.1 tokenizers>=0.21.1 accelerate @ git+https://github.com/huggingface/accelerate.git@9359a0194f210624f1e6e85c3d838fdd55c11152 diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index 6b8f42d021..db28206b6c 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -76,6 +76,7 @@ def load_lora( config_only: bool = False, ) -> tuple[PreTrainedModel | PeftModel | PeftMixedModel | None, PeftConfig | None]: lora_target_modules = cfg.lora_target_modules or [] + lora_target_parameters = cfg.lora_target_parameters or [] if cfg.lora_target_linear: linear_names = find_all_linear_names(model) @@ -106,6 +107,7 @@ def load_lora( r=cfg.lora_r, lora_alpha=cfg.lora_alpha, target_modules=lora_target_modules, + target_parameters=lora_target_parameters, layers_to_transform=cfg.peft_layers_to_transform, layers_pattern=cfg.peft_layers_pattern, lora_dropout=cfg.lora_dropout, diff --git a/src/axolotl/utils/schemas/peft.py b/src/axolotl/utils/schemas/peft.py index 341397b429..de29521cb4 100644 --- a/src/axolotl/utils/schemas/peft.py +++ b/src/axolotl/utils/schemas/peft.py @@ -54,6 +54,7 @@ class LoraConfig(BaseModel): lora_alpha: int | None = None lora_fan_in_fan_out: bool | None = None lora_target_modules: str | list[str] | None = None + lora_target_parameters: str | list[str] | None = None lora_target_linear: bool | None = Field( default=None, json_schema_extra={"description": "If true, will target all linear modules"}, From e758343cac63e5c6acf9d1fe06ad62754ade5346 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Sun, 3 Aug 2025 20:05:17 -0400 Subject: [PATCH 0878/1405] FSDP2 + LoRA kernels (#2992) * impl fix * smoke tests * patches for fsdp2 + qlora compat * nit * working fix * working fix * fix merge * minifying patches; update bnb dep * renaming; adding tests * remove duplicate test, add dora guard * generalize __torch_function__ * revert generalization * update comments --- requirements.txt | 2 +- src/axolotl/kernels/lora.py | 22 ++- src/axolotl/loaders/patch_manager.py | 18 +++ src/axolotl/monkeypatch/fsdp2_qlora.py | 205 ++++++++++++++++++++++++ src/axolotl/utils/schemas/validation.py | 38 ++--- tests/e2e/multigpu/test_fsdp2.py | 127 +++++++++++++++ tests/e2e/patched/test_fsdp2_qlora.py | 131 +++++++++++++++ 7 files changed, 520 insertions(+), 23 deletions(-) create mode 100644 src/axolotl/monkeypatch/fsdp2_qlora.py create mode 100644 tests/e2e/patched/test_fsdp2_qlora.py diff --git a/requirements.txt b/requirements.txt index 2d72f307a0..4e82dfd893 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ # START section of dependencies that don't install on Darwin/MacOS -bitsandbytes==0.46.0 +bitsandbytes==0.46.1 triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 diff --git a/src/axolotl/kernels/lora.py b/src/axolotl/kernels/lora.py index 63c9e57bd6..82ec911072 100644 --- a/src/axolotl/kernels/lora.py +++ b/src/axolotl/kernels/lora.py @@ -14,6 +14,7 @@ import torch from bitsandbytes.functional import QuantState from torch import nn +from torch.distributed.tensor import DTensor from .geglu import geglu_backward, geglu_forward from .quantize import dequantize @@ -54,8 +55,21 @@ def get_lora_parameters( if hasattr(proj, "active_adapters") else proj.active_adapter ) - A = proj.lora_A[active_adapter].weight - B = proj.lora_B[active_adapter].weight + + linear_A = proj.lora_A[active_adapter] + linear_B = proj.lora_B[active_adapter] + + # This manual unsharding is needed for FSDP2 + LoRA kernels compatibility. + # We fuse linear layers + LoRA adapters calculations into a single + # torch.autograd.Function, bypassing the registered unshard / reshard behavior. + # Note that we don't apply resharding later in this module (it gets messy quickly), + # but LoRA parameters are generally small enough that this is not an issue. + if isinstance(linear_A.weight, DTensor): + linear_A.unshard() + linear_B.unshard() + + A = linear_A.weight + B = linear_B.weight s = proj.scaling[active_adapter] quant_state = getattr(W, "quant_state", None) @@ -102,8 +116,8 @@ def matmul_lora( del W if A is not None: - A, B = A.t(), B.t() - out += (X @ A.to(dtype)) @ (s * B.to(dtype)) + A, B = A.t().to(dtype), B.t().to(dtype) + out += s * X @ A @ B return out.view(batch, seq_len, -1) if reshape else out diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 9eb7791135..e16f036497 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -65,6 +65,7 @@ def apply_pre_model_load_patches(self): self._patch_llama_derived_model() self._apply_mistral_cross_entropy_patch() self._apply_self_attention_lora_patch() + self._apply_fsdp2_bnb_patches() def apply_post_plugin_pre_model_load_patches(self): """Apply post plugin-pre_model_load load patches based on config.""" @@ -260,6 +261,23 @@ def _apply_multipack_patches(self): has_remote_code=has_remote_code, ) + def _apply_fsdp2_bnb_patches(self): + """Apply FSDP2 BNB patches.""" + if ( + self.cfg.fsdp_config + and str(self.cfg.fsdp_version) == "2" + and self.cfg.adapter == "qlora" + ): + from axolotl.monkeypatch.fsdp2_qlora import ( + apply_bnb_torch_function_patch, + apply_init_sharded_param_patch, + apply_init_unsharded_param_patch, + ) + + apply_bnb_torch_function_patch() + apply_init_sharded_param_patch() + apply_init_unsharded_param_patch() + def _apply_tiled_mlp(self, model_type: str): if self.cfg.tiled_mlp: from axolotl.monkeypatch.tiled_mlp import ( diff --git a/src/axolotl/monkeypatch/fsdp2_qlora.py b/src/axolotl/monkeypatch/fsdp2_qlora.py new file mode 100644 index 0000000000..a2cb7e4729 --- /dev/null +++ b/src/axolotl/monkeypatch/fsdp2_qlora.py @@ -0,0 +1,205 @@ +""" +Monkeypatch to add Params4bit support to FSDP2. This enables QLoRA + FSDP2, as well as +our LoRA / QLoRA Triton kernels to work with FSDP2. + +This patch modifies the _init_sharded_param method in FSDPParam to handle bitsandbytes +Params4bit parameters. +""" + +import importlib +import inspect + +import torch +from torch.nn import Parameter + +from axolotl.monkeypatch.utils import detab_code +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def patched_torch_function(cls, func, types, args=(), kwargs=None): + """ + Patched version of Params4bit.__torch_function__ for preserving Params4bit + class identity and attributes. + """ + if kwargs is None: + kwargs = {} + + if func in [torch.chunk, torch.split]: + tensor = args[0] + result = Parameter.__torch_function__(func, types, args, kwargs) + + if isinstance(result, tuple): + return tuple( + cls( + data=chunk, + requires_grad=tensor.requires_grad, + quant_state=tensor.quant_state, + blocksize=tensor.blocksize, + compress_statistics=tensor.compress_statistics, + quant_type=tensor.quant_type, + quant_storage=tensor.quant_storage, + module=tensor.module, + bnb_quantized=tensor.bnb_quantized, + ) + for chunk in result + ) + + return cls( + data=result, + requires_grad=tensor.requires_grad, + quant_state=tensor.quant_state, + blocksize=tensor.blocksize, + compress_statistics=tensor.compress_statistics, + quant_type=tensor.quant_type, + quant_storage=tensor.quant_storage, + module=tensor.module, + bnb_quantized=tensor.bnb_quantized, + ) + + return Parameter.__torch_function__(func, types, args, kwargs) + + +# pylint: disable=protected-access +def apply_bnb_torch_function_patch(): + """ + Patch Params4bit.__torch_function__ using Axolotl-style approach. + + Returns: + True if patching succeeded, False otherwise. + """ + from bitsandbytes.nn.modules import Params4bit + + Params4bit.__torch_function__ = classmethod(patched_torch_function) + + LOG.info("Successfully patched Params4bit.__torch_function__") + + +# pylint: disable=protected-access +def apply_init_sharded_param_patch(): + """Apply patch to FSDPParam._init_sharded_param to support Params4bit.""" + from torch.distributed.fsdp._fully_shard._fsdp_param import FSDPParam + + # Get original source + original_source = inspect.getsource(FSDPParam._init_sharded_param) + original_source, _ = detab_code(original_source) + + # Define the replacement + original_param_creation = """ self.sharded_param = nn.Parameter(self.to_sharded_dtensor(sharded_param)) + self.sharded_param.requires_grad_(param.requires_grad)""" + + patched_param_creation = """ import bitsandbytes as bnb + if isinstance(param, bnb.nn.modules.Params4bit): + self.sharded_param = bnb.nn.modules.Params4bit( + data=sharded_param, + requires_grad=param.requires_grad, + quant_state=param.quant_state, + blocksize=param.blocksize, + compress_statistics=param.compress_statistics, + quant_type=param.quant_type, + quant_storage=param.quant_storage, + module=param.module, + bnb_quantized=param.bnb_quantized, + ) + self.sharded_param = self.to_sharded_dtensor(self.sharded_param) + else: + self.sharded_param = nn.Parameter(self.to_sharded_dtensor(sharded_param)) + self.sharded_param.requires_grad_(param.requires_grad)""" + + # Apply the replacement + if original_param_creation in original_source: + patched_source = original_source.replace( + original_param_creation, patched_param_creation + ) + patched_source = patched_source.replace( + "def _init_sharded_param(", + "def patched_init_sharded_param(", + 1, + ) + + # Load necessary imports + module_name = FSDPParam.__module__ + module = importlib.import_module(module_name) + + items_to_import = [] + for item in dir(module): + if item in patched_source: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + f"from {module_name} import ({', '.join(items_to_import)})", + globals(), + ) + exec(patched_source, globals()) # pylint: disable=exec-used # nosec B102 + + # Replace the method + FSDPParam._init_sharded_param = patched_init_sharded_param # pylint: disable=undefined-variable # noqa: F821 + LOG.info("Successfully applied FSDP _init_sharded_param patch") + else: + LOG.warning("Could not find target code for _init_sharded_param patching") + + +def apply_init_unsharded_param_patch(): + """Apply patch to FSDPParam.init_unsharded_param to support Params4bit.""" + from torch.distributed.fsdp._fully_shard._fsdp_param import FSDPParam + + # Get original source + original_source = inspect.getsource(FSDPParam.init_unsharded_param) + original_source, _ = detab_code(original_source) + + # Define the replacement + original_param_creation = """ self._unsharded_param = nn.Parameter( + unsharded_param, requires_grad=self.sharded_param.requires_grad + )""" + + patched_param_creation = """ import bitsandbytes as bnb + local_tensor = self.sharded_param._local_tensor + if isinstance(local_tensor, bnb.nn.modules.Params4bit): + self._unsharded_param = bnb.nn.modules.Params4bit( + data=unsharded_param, + requires_grad=self.sharded_param.requires_grad, + quant_state=local_tensor.quant_state, + blocksize=local_tensor.blocksize, + compress_statistics=local_tensor.compress_statistics, + quant_type=local_tensor.quant_type, + quant_storage=local_tensor.quant_storage, + module=local_tensor.module, + bnb_quantized=local_tensor.bnb_quantized, + ) + else: + self._unsharded_param = nn.Parameter( + unsharded_param, requires_grad=self.sharded_param.requires_grad + )""" + + # Apply the replacement + if original_param_creation in original_source: + patched_source = original_source.replace( + original_param_creation, patched_param_creation + ) + patched_source = patched_source.replace( + "def init_unsharded_param(", + "def patched_init_unsharded_param(", + 1, + ) + + # Load necessary imports + module_name = FSDPParam.__module__ + module = importlib.import_module(module_name) + + items_to_import = [] + for item in dir(module): + if item in patched_source: + items_to_import.append(item) + + exec( # pylint: disable=exec-used # nosec B102 + f"from {module_name} import ({', '.join(items_to_import)})", + globals(), + ) + exec(patched_source, globals()) # pylint: disable=exec-used # nosec B102 + + # Replace the method + FSDPParam.init_unsharded_param = patched_init_unsharded_param # pylint: disable=undefined-variable # noqa: F821 + LOG.info("Successfully applied FSDP init_unsharded_param patch") + else: + LOG.warning("Could not find target code for patching") diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 02e80dd8e0..61eec65d5d 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -559,20 +559,6 @@ def check_qlora_unsloth(cls, data): ) return data - @model_validator(mode="before") - @classmethod - def check_lora_8bit(cls, data): - if ( - data.get("lora_mlp_kernel") - or data.get("lora_qkv_kernel") - or data.get("lora_o_kernel") - ): - if data.get("adapter") == "lora" and data.get("load_in_8bit"): - raise ValueError( - "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with 8-bit LoRA" - ) - return data - @model_validator(mode="before") @classmethod def check_lora_axolotl_unsloth(cls, data): @@ -619,7 +605,7 @@ def warn_qlora_zero3_w_use_reentrant(cls, data): @model_validator(mode="before") @classmethod - def check_lora_kernel_8bit(cls, data): + def check_lora_kernels_8bit(cls, data): if ( data.get("lora_mlp_kernel") or data.get("lora_qkv_kernel") @@ -627,20 +613,36 @@ def check_lora_kernel_8bit(cls, data): ): if data.get("adapter") == "lora" and data.get("load_in_8bit"): raise ValueError( - "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with 8-bit LoRA" + "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not " + "compatible with 8-bit LoRA a the moment." ) return data @model_validator(mode="before") @classmethod - def check_lora_kernel_rl(cls, data): + def check_lora_kernels_dora(cls, data): + if ( + data.get("lora_mlp_kernel") + or data.get("lora_qkv_kernel") + or data.get("lora_o_kernel") + ) and data.get("peft_use_dora"): + raise ValueError( + "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not " + "compatible with DoRA at the moment." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_lora_kernels_rl(cls, data): if ( data.get("lora_mlp_kernel") or data.get("lora_qkv_kernel") or data.get("lora_o_kernel") ) and data.get("rl"): raise ValueError( - "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not compatible with RL at the moment." + "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not " + "compatible with RL at the moment." ) return data diff --git a/tests/e2e/multigpu/test_fsdp2.py b/tests/e2e/multigpu/test_fsdp2.py index 95ced1303e..0bb255266e 100644 --- a/tests/e2e/multigpu/test_fsdp2.py +++ b/tests/e2e/multigpu/test_fsdp2.py @@ -174,6 +174,69 @@ def test_lora_sft(self, temp_dir, peft_use_dora): verify_training_success(temp_dir) + @require_torch_2_7_0 + def test_lora_sft_kernels(self, temp_dir): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:10%]", + }, + ], + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_target_linear": True, + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": False, + "transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + "use_tensorboard": True, + "bf16": True, + "lora_mlp_kernel": True, + "lora_qkv_kernel": True, + "lora_o_kernel": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) + @require_torch_2_7_0 def test_qlora_sft(self, temp_dir): cfg = DictDefault( @@ -236,6 +299,70 @@ def test_qlora_sft(self, temp_dir): verify_training_success(temp_dir) + @require_torch_2_7_0 + def test_qlora_sft_kernels(self, temp_dir): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:10%]", + }, + ], + "load_in_4bit": True, + "adapter": "qlora", + "lora_r": 8, + "lora_alpha": 16, + "lora_target_linear": True, + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": False, + "transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + "use_tensorboard": True, + "bf16": True, + "lora_mlp_kernel": True, + "lora_qkv_kernel": True, + "lora_o_kernel": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) + @require_torch_2_7_0 def test_dpo_fft(self, temp_dir): cfg = DictDefault( diff --git a/tests/e2e/patched/test_fsdp2_qlora.py b/tests/e2e/patched/test_fsdp2_qlora.py new file mode 100644 index 0000000000..9dd053ad89 --- /dev/null +++ b/tests/e2e/patched/test_fsdp2_qlora.py @@ -0,0 +1,131 @@ +"""Integration tests for FSDP Params4bit patches.""" + +from unittest.mock import Mock, patch + +import bitsandbytes as bnb +import pytest +import torch +from torch.distributed.fsdp._fully_shard._fsdp_param import FSDPParam + +from axolotl.monkeypatch.fsdp2_qlora import ( + apply_bnb_torch_function_patch, + patched_torch_function, +) + + +@pytest.fixture +def mock_params4bit(): + """Create a mock Params4bit instance with test attributes.""" + mock_instance = Mock() + mock_instance.requires_grad = True + mock_instance.quant_state = "test_state" + mock_instance.blocksize = 128 + mock_instance.compress_statistics = True + mock_instance.quant_type = "fp4" + mock_instance.quant_storage = "test_storage" + mock_instance.module = "test_module" + mock_instance.bnb_quantized = True + return mock_instance + + +class TestBnbTorchFunctionPatch: + """Test the Params4bit.__torch_function__ patch.""" + + def test_apply_patch(self): + """Test that the patch can be applied.""" + with patch("bitsandbytes.nn.modules.Params4bit") as mock_cls: + apply_bnb_torch_function_patch() + assert hasattr(mock_cls, "__torch_function__") + assert isinstance(mock_cls.__torch_function__, classmethod) + + # pylint: disable=redefined-outer-name + def test_torch_chunk_preserves_attributes(self, mock_params4bit): + """Test that torch.chunk preserves Params4bit attributes.""" + mock_cls = Mock() + chunks = (torch.tensor([1, 2]), torch.tensor([3, 4])) + + with patch("torch.nn.Parameter.__torch_function__", return_value=chunks): + result = patched_torch_function( + mock_cls, + torch.chunk, + (type(mock_params4bit),), + args=(mock_params4bit, 2), + ) + + assert isinstance(result, tuple) + assert len(result) == 2 + + # Check that Params4bit constructor was called with preserved attributes + assert mock_cls.call_count == 2 + for call in mock_cls.call_args_list: + kwargs = call[1] + assert kwargs["requires_grad"] == mock_params4bit.requires_grad + assert kwargs["quant_state"] == mock_params4bit.quant_state + assert kwargs["blocksize"] == mock_params4bit.blocksize + + # pylint: disable=redefined-outer-name + def test_other_functions_fallback(self, mock_params4bit): + """Test that non-chunk/split functions use Parameter fallback.""" + mock_cls = Mock() + fallback_result = torch.tensor([5, 6, 7]) + + with patch( + "torch.nn.Parameter.__torch_function__", return_value=fallback_result + ) as mock_fallback: + result = patched_torch_function( + mock_cls, torch.add, (type(mock_params4bit),), args=(mock_params4bit, 1) + ) + + # Should call Parameter.__torch_function__ and return its result + mock_fallback.assert_called_once() + assert result is fallback_result + mock_cls.assert_not_called() + + +class TestFSDPPatchIntegration: + """Test FSDP patch integration.""" + + @pytest.mark.integration + def test_all_patches_together(self): + """Test that all patches can be applied together.""" + from axolotl.monkeypatch.fsdp2_qlora import ( + apply_init_sharded_param_patch, + apply_init_unsharded_param_patch, + ) + + # Store original methods before patching + original_torch_function = getattr( + bnb.nn.modules.Params4bit, "__torch_function__", None + ) + + # pylint: disable=protected-access + original_init_sharded = FSDPParam._init_sharded_param + original_init_unsharded = FSDPParam.init_unsharded_param + + # Apply patches + apply_bnb_torch_function_patch() + apply_init_sharded_param_patch() + apply_init_unsharded_param_patch() + + # Verify patches were applied + current_torch_function = getattr( + bnb.nn.modules.Params4bit, "__torch_function__", None + ) + if original_torch_function is not None: + assert ( + current_torch_function != original_torch_function + ), "Params4bit.__torch_function__ was not patched" + else: + assert ( + current_torch_function is not None + ), "Params4bit.__torch_function__ was not added" + + # Check that FSDP methods were patched + assert ( + # pylint: disable=protected-access + FSDPParam._init_sharded_param + != original_init_sharded + ), "_init_sharded_param was not patched" + assert ( + FSDPParam.init_unsharded_param != original_init_unsharded + ), "init_unsharded_param was not patched" From 5691992d345fb8225afbea8b2d51dacd6f3ef093 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:23:19 -0400 Subject: [PATCH 0879/1405] chore: update pre-commit hooks (#3009) [skip ci] Co-authored-by: djsaunde <1245942+djsaunde@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0734e8cccb..dadac90c3f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: hooks: - id: pylint - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.0 + rev: v1.17.1 hooks: - id: mypy additional_dependencies: From a54c1be9722f8b93d3b24334c40c174374e63fda Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 4 Aug 2025 21:23:36 +0700 Subject: [PATCH 0880/1405] Fix: shorten mem logs to 2 decimal places and renamed nd docs (#3011) [skip ci] * fix: shorten memory logs * fix: title name --- docs/nd_parallelism.qmd | 4 +++- src/axolotl/core/trainers/base.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/nd_parallelism.qmd b/docs/nd_parallelism.qmd index 7c2d2e0cbf..d27a156634 100644 --- a/docs/nd_parallelism.qmd +++ b/docs/nd_parallelism.qmd @@ -1,4 +1,6 @@ -# N-D Parallelism +--- +title: "N-D Parallelism" +--- Axolotl enables training models at scale by composing different parallelism techniques. This is essential when: diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index f739d19e95..617506eb2b 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -567,10 +567,10 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: # Add memory usage try: active, allocated, reserved = get_gpu_memory_usage() - logs["memory/max_memory_active"] = active - logs["memory/max_memory_allocated"] = allocated - logs["memory/device_memory_reserved"] = reserved - except (ValueError, FileNotFoundError): + logs["memory/max_memory_active(gib)"] = round(active, 2) + logs["memory/max_memory_allocated(gib)"] = round(allocated, 2) + logs["memory/device_memory_reserved(gib)"] = round(reserved, 2) + except (ValueError, TypeError, FileNotFoundError): pass del self._stored_metrics[train_eval] From 33d094721c823fe98a5e387c9d61d9815607fcc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carsten=20Kragelund=20J=C3=B8rgensen?= Date: Mon, 4 Aug 2025 16:23:49 +0200 Subject: [PATCH 0881/1405] fix: deepcopy lr in RexLR scheduler. (#3012) * fix: deepcopy lr in RexLR scheduler. This fixes a problem where when the lr is a scalar tensor, the base_lrs in the get_lr function end up being references to the current learning rate, rather than the correct initial learning rate. See also related pytorch PR https://github.com/pytorch/pytorch/pull/127190/ * fix: add missing torch.Tensor import --- src/axolotl/utils/schedulers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/axolotl/utils/schedulers.py b/src/axolotl/utils/schedulers.py index b9d09ad9c0..cdaf922713 100644 --- a/src/axolotl/utils/schedulers.py +++ b/src/axolotl/utils/schedulers.py @@ -4,6 +4,7 @@ from functools import partial from typing import Sequence +from torch import Tensor from torch.optim import Optimizer from torch.optim.lr_scheduler import LambdaLR, LRScheduler @@ -45,8 +46,10 @@ def __init__( # Ensure each parameter group has an "initial_lr" key to avoid issues when resuming. for group in optimizer.param_groups: - group.setdefault("initial_lr", group["lr"]) - + initial_lr = group["lr"] + if isinstance(initial_lr, Tensor): + initial_lr = initial_lr.clone() + group.setdefault("initial_lr", initial_lr) # Pass self.last_step as last_epoch to the parent. super().__init__(optimizer, last_epoch=self.last_step) From ab49d16e34fb9dad838fd9f8a4b0e2781223b20f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 4 Aug 2025 16:33:30 -0400 Subject: [PATCH 0882/1405] Dion optimizer support (#3014) * Add support for Dion optimizer * dion training kwargs * fix var names * no dion 8bit for now * use updated axolotl-contribs-mit for dion optimizer * add smoke test for dion optimizer * add docs * fix typo during edits * fix test to not remove load in 8bit --- _quarto.yml | 1 + docs/nd_parallelism.qmd | 2 +- docs/optimizers.qmd | 18 +++++++++++ requirements.txt | 2 +- src/axolotl/core/builders/base.py | 21 ++++++++++++ src/axolotl/core/training_args_base.py | 15 +++++++++ src/axolotl/integrations/base.py | 23 ++++++++++++++ src/axolotl/utils/schemas/enums.py | 1 + src/axolotl/utils/schemas/training.py | 20 ++++++++++++ tests/e2e/test_optimizers.py | 44 ++++++++++++++++++++++++++ 10 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 docs/optimizers.qmd diff --git a/_quarto.yml b/_quarto.yml index 738fe5e2fa..5bb771c01d 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -284,6 +284,7 @@ website: - docs/sequence_parallelism.qmd - docs/gradient_checkpointing.qmd - docs/nd_parallelism.qmd + - docs/optimizers.qmd - section: "Troubleshooting" contents: diff --git a/docs/nd_parallelism.qmd b/docs/nd_parallelism.qmd index d27a156634..8aebab1409 100644 --- a/docs/nd_parallelism.qmd +++ b/docs/nd_parallelism.qmd @@ -1,5 +1,5 @@ --- -title: "N-D Parallelism" +title: "N-D Parallelism (Beta)" --- Axolotl enables training models at scale by composing different parallelism techniques. This is essential when: diff --git a/docs/optimizers.qmd b/docs/optimizers.qmd new file mode 100644 index 0000000000..563e9695bb --- /dev/null +++ b/docs/optimizers.qmd @@ -0,0 +1,18 @@ +--- +title: Optimizers +description: Configuring optimizers +--- + +### Dion Optimizer + +Microsoft's Dion (DIstributed OrthoNormalization) optimizer is a scalable and communication-efficient +orthonormalizing optimizer that uses low-rank approximations to reduce gradient communication. + +Usage: + +```yaml +optimizer: dion +dion_lr: 0.01 +dion_momentum: 0.95 +lr: 0.00001 # learning rate for embeddings and parameters that fallback to AdamW +``` diff --git a/requirements.txt b/requirements.txt index 4e82dfd893..cd9b2cf621 100644 --- a/requirements.txt +++ b/requirements.txt @@ -66,6 +66,6 @@ torchao==0.12.0 schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.6 -axolotl-contribs-mit==0.0.3 +axolotl-contribs-mit==0.0.4 mistral-common==1.8.3 diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index dbdda7a7cd..5a25c1834d 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -267,6 +267,17 @@ def _configure_custom_optimizer( optimizer_cls = MuonOptimizerFactory optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "dion": + from axolotl.contribs.mit.dion import ( # pylint: disable=no-name-in-module + DionOptimizerFactory, + ) + + optimizer_cls = DionOptimizerFactory + optimizer_kwargs["dion_lr"] = training_args_kwargs["dion_learning_rate"] + optimizer_kwargs["dion_mu"] = training_args_kwargs["dion_momentum"] + optimizer_kwargs.update(adam_kwargs) + partial_state = PartialState() + optimizer_kwargs["device_mesh"] = partial_state.device_mesh elif self.cfg.optimizer == "optimi_adamw": from optimi import AdamW @@ -516,10 +527,20 @@ def _set_base_training_args( "include_tokens_per_second", "weight_decay", "seed", + "dion_momentum", + "dion_rank_fraction", + "dion_rank_multiple_of", ]: if hasattr(self.cfg, arg) and getattr(self.cfg, arg) is not None: training_args_kwargs[arg] = getattr(self.cfg, arg) + arg_map = { + "dion_learning_rate": "dion_lr", + } + for kwarg, cfg_arg in arg_map.items(): + if hasattr(self.cfg, cfg_arg) and getattr(self.cfg, cfg_arg) is not None: + training_args_kwargs[kwarg] = getattr(self.cfg, cfg_arg) + training_args_kwargs["per_device_train_batch_size"] = self.cfg.micro_batch_size training_args_kwargs["average_tokens_across_devices"] = False diff --git a/src/axolotl/core/training_args_base.py b/src/axolotl/core/training_args_base.py index 66649deefd..fd0859ae91 100644 --- a/src/axolotl/core/training_args_base.py +++ b/src/axolotl/core/training_args_base.py @@ -243,3 +243,18 @@ class AxolotlTrainingMixins: ) # end of multi-modal section + + dion_learning_rate: float | None = field( + default=None, + metadata={"help": "The learning rate for Dion"}, + ) + dion_momentum: float | None = field( + default=None, + metadata={"help": "The momentum for Dion"}, + ) + dion_rank_fraction: float | None = field( + default=None, + ) + dion_rank_multiple_of: int | None = field( + default=None, + ) diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 7d9b6a6f96..f43031287f 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -26,9 +26,11 @@ from typing import TYPE_CHECKING, Callable, OrderedDict, Union from peft import PeftModel +from torch import nn from torch.optim import Optimizer from torch.optim.lr_scheduler import LRScheduler from transformers import PreTrainedModel, Trainer +from transformers.trainer_pt_utils import get_parameter_names from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger @@ -641,3 +643,24 @@ def __call__( self, opt_model, training_args, **optimizer_kwargs ) -> Optimizer | None: pass + + # duplicated from transformers + def get_decay_parameter_names(self, model) -> list[str]: + """ + Get all parameter names that weight decay will be applied to. + + This function filters out parameters in two ways: + 1. By layer type (instances of layers specified in ALL_LAYERNORM_LAYERS) + 2. By parameter name patterns (containing 'bias', or variation of 'norm') + """ + forbidden_name_patterns = [ + r"bias", + r"layernorm", + r"rmsnorm", + r"(?:^|\.)norm(?:$|\.)", + r"_norm(?:$|\.)", + ] + decay_parameters = get_parameter_names( + model, [nn.LayerNorm], forbidden_name_patterns + ) + return decay_parameters diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 3c88283962..cf2a8b484f 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -79,6 +79,7 @@ class CustomSupportedOptimizers(str, Enum): adopt_adamw = "adopt_adamw" came_pytorch = "came_pytorch" muon = "muon" + dion = "dion" class RingAttnFunc(str, Enum): diff --git a/src/axolotl/utils/schemas/training.py b/src/axolotl/utils/schemas/training.py index 6ee8633975..b1788dcaa5 100644 --- a/src/axolotl/utils/schemas/training.py +++ b/src/axolotl/utils/schemas/training.py @@ -138,6 +138,26 @@ class HyperparametersConfig(BaseModel): adam_beta3: float | None = Field( default=None, json_schema_extra={"description": "only used for CAME Optimizer"} ) + + dion_lr: float | None = Field( + default=None, json_schema_extra={"description": "Dion Optimizer learning rate"} + ) + dion_momentum: float | None = Field( + default=None, json_schema_extra={"description": "Dion Optimizer momentum"} + ) + dion_rank_fraction: float | None = Field( + default=1.0, + json_schema_extra={ + "description": "Dion Optimizer: r/d fraction for low-rank approximation. Used to compute the low-rank dimension." + }, + ) + dion_rank_multiple_of: int | None = Field( + default=1, + json_schema_extra={ + "description": "Dion Optimizer: Round up the low-rank dimension to a multiple of this number. This may be useful to ensure even sharding." + }, + ) + max_grad_norm: float | None = Field( default=None, json_schema_extra={"description": "Gradient clipping max norm"} ) diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 1d233a2013..987d860418 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -13,6 +13,7 @@ check_model_output_exists, require_torch_2_5_1, require_torch_2_6_0, + require_torch_2_7_0, with_temp_dir, ) @@ -160,6 +161,49 @@ def test_muon(self, temp_dir): check_model_output_exists(temp_dir, cfg) assert "Muon" in trainer.optimizer.optimizer.__class__.__name__ + @with_temp_dir + @require_torch_2_7_0 + def test_dion(self, temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "model_type": "AutoModelForCausalLM", + "tokenizer_type": "AutoTokenizer", + "sequence_len": 1024, + "val_set_size": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "dion", + "dion_lr": 0.01, + "dion_momentum": 0.95, + "lr_scheduler": "cosine", + "weight_decay": 0.01, + "save_first_step": False, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + _, _, trainer = train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) + assert "Dion" in trainer.optimizer.optimizer.__class__.__name__ + @with_temp_dir def test_fft_schedule_free_adamw(self, temp_dir): # pylint: disable=duplicate-code From 42f5e6f9e964cf1267c82e9cb2eba453ec87bbe3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 5 Aug 2025 16:29:12 -0400 Subject: [PATCH 0883/1405] upgrade transformers==4.55.0 (#3018) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cd9b2cf621..244d1239cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ packaging==23.2 huggingface_hub>=0.33.0 peft==0.17.0 -transformers==4.54.1 +transformers==4.55.0 tokenizers>=0.21.1 accelerate @ git+https://github.com/huggingface/accelerate.git@9359a0194f210624f1e6e85c3d838fdd55c11152 datasets==4.0.0 From 8021c718ce8c2494c7ebc3f01523cdce4508f196 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 6 Aug 2025 00:13:12 -0400 Subject: [PATCH 0884/1405] use skip_move_to_device for all cases (#3015) * use skip_move_to_device for all cases * use experimental option for skip move --- src/axolotl/loaders/model.py | 3 +++ src/axolotl/utils/schemas/model.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 05039c9ee9..1b983f7d09 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -845,6 +845,9 @@ def _build_model(self) -> bool: self.model._tp_size = self.cfg.tensor_parallel_size self.model._device_mesh = self.model_kwargs["device_mesh"] + if self.cfg.experimental_skip_move_to_device is not None: + skip_move_to_device = self.cfg.experimental_skip_move_to_device + return skip_move_to_device def _set_z3_leaf_modules(self): diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index 5eea11444d..eae8dacb6b 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -62,6 +62,14 @@ class ModelInputConfig(BaseModel): json_schema_extra={"description": "Trust remote code for untrusted source"}, ) + experimental_skip_move_to_device: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Don't move the model to the device before sharding. " + "This is an experimental feature that may be included in the future as the default." + }, + ) + @field_validator("trust_remote_code") @classmethod def hint_trust_remote_code(cls, trust_remote_code): From 70faea331f221ca999498459bf354edaee6b45d6 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 6 Aug 2025 01:06:52 -0400 Subject: [PATCH 0885/1405] add support for connecting via prime-intellect (#3021) --- scripts/cloud-entrypoint.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/cloud-entrypoint.sh b/scripts/cloud-entrypoint.sh index 2d3e29181a..a5505e9add 100755 --- a/scripts/cloud-entrypoint.sh +++ b/scripts/cloud-entrypoint.sh @@ -44,8 +44,13 @@ add_keys_to_authorized() { chmod 700 -R ~/.ssh } +# Set SSH port +if [ ! -z "$SSH_PORT" ]; then + sed -i "s/#Port 22/Port $SSH_PORT/" /etc/ssh/sshd_config +fi + if [[ $PUBLIC_KEY ]]; then - # runpod + # runpod, prime intellect add_keys_to_authorized "$PUBLIC_KEY" # Start the SSH service in the background service ssh start From e3177c321089d20e7655fa6e792c00b2837fda8f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 6 Aug 2025 19:01:51 +0700 Subject: [PATCH 0886/1405] feat: add complete optimizer docs (#3017) [skip ci] * feat: add complete optimizer docs * fix: deprecate old torchao adamw low bit --- _quarto.yml | 2 +- docs/optimizers.qmd | 115 +++++++++++++++++++++++++++++- src/axolotl/core/builders/base.py | 16 ----- 3 files changed, 114 insertions(+), 19 deletions(-) diff --git a/_quarto.yml b/_quarto.yml index 5bb771c01d..934d393cb5 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -274,6 +274,7 @@ website: - docs/dataset_preprocessing.qmd - docs/multipack.qmd - docs/mixed_precision.qmd + - docs/optimizers.qmd - section: "Advanced Features" contents: @@ -284,7 +285,6 @@ website: - docs/sequence_parallelism.qmd - docs/gradient_checkpointing.qmd - docs/nd_parallelism.qmd - - docs/optimizers.qmd - section: "Troubleshooting" contents: diff --git a/docs/optimizers.qmd b/docs/optimizers.qmd index 563e9695bb..45eea1d3ab 100644 --- a/docs/optimizers.qmd +++ b/docs/optimizers.qmd @@ -3,12 +3,123 @@ title: Optimizers description: Configuring optimizers --- -### Dion Optimizer +## Overview + +Axolotl supports all optimizers supported by [transformers OptimizerNames](https://github.com/huggingface/transformers/blob/51f94ea06d19a6308c61bbb4dc97c40aabd12bad/src/transformers/training_args.py#L142-L187) + +Here is a list of optimizers supported by transformers as of `v4.54.0`: + +- `adamw_torch` +- `adamw_torch_fused` +- `adamw_torch_xla` +- `adamw_torch_npu_fused` +- `adamw_apex_fused` +- `adafactor` +- `adamw_anyprecision` +- `adamw_torch_4bit` +- `adamw_torch_8bit` +- `ademamix` +- `sgd` +- `adagrad` +- `adamw_bnb_8bit` +- `adamw_8bit` # alias for adamw_bnb_8bit +- `ademamix_8bit` +- `lion_8bit` +- `lion_32bit` +- `paged_adamw_32bit` +- `paged_adamw_8bit` +- `paged_ademamix_32bit` +- `paged_ademamix_8bit` +- `paged_lion_32bit` +- `paged_lion_8bit` +- `rmsprop` +- `rmsprop_bnb` +- `rmsprop_bnb_8bit` +- `rmsprop_bnb_32bit` +- `galore_adamw` +- `galore_adamw_8bit` +- `galore_adafactor` +- `galore_adamw_layerwise` +- `galore_adamw_8bit_layerwise` +- `galore_adafactor_layerwise` +- `lomo` +- `adalomo` +- `grokadamw` +- `schedule_free_radam` +- `schedule_free_adamw` +- `schedule_free_sgd` +- `apollo_adamw` +- `apollo_adamw_layerwise` +- `stable_adamw` + + +## Custom Optimizers + +Enable custom optimizers by passing a string to the `optimizer` argument. Each optimizer will receive beta and epsilon args, however, some may accept additional args which are detailed below. + +### optimi_adamw + +```yaml +optimizer: optimi_adamw +``` + +### ao_adamw_4bit + +Deprecated: Please use `adamw_torch_4bit`. + +### ao_adamw_8bit + +Deprecated: Please use `adamw_torch_8bit`. + +### ao_adamw_fp8 + + +```yaml +optimizer: ao_adamw_fp8 +``` + +### adopt_adamw + +GitHub: [https://github.com/iShohei220/adopt](https://github.com/iShohei220/adopt) +Paper: [https://arxiv.org/abs/2411.02853](https://arxiv.org/abs/2411.02853) + +```yaml +optimizer: adopt_adamw +``` + +### came_pytorch + +GitHub: [https://github.com/yangluo7/CAME/tree/master](https://github.com/yangluo7/CAME/tree/master) +Paper: [https://arxiv.org/abs/2307.02047](https://arxiv.org/abs/2307.02047) + +```yaml +optimizer: came_pytorch + +# optional args (defaults below) +adam_beta1: 0.9 +adam_beta2: 0.999 +adam_beta3: 0.9999 +adam_epsilon: 1e-30 +adam_epsilon2: 1e-16 +``` + +### muon + +Blog: [https://kellerjordan.github.io/posts/muon/](https://kellerjordan.github.io/posts/muon/) +Paper: [https://arxiv.org/abs/2502.16982v1](https://arxiv.org/abs/2502.16982v1) + +```yaml +optimizer: muon +``` + +### dion Microsoft's Dion (DIstributed OrthoNormalization) optimizer is a scalable and communication-efficient orthonormalizing optimizer that uses low-rank approximations to reduce gradient communication. -Usage: +GitHub: [https://github.com/microsoft/dion](https://github.com/microsoft/dion) +Paper: [https://arxiv.org/pdf/2504.05295](https://arxiv.org/pdf/2504.05295) +Note: Implementation written for PyTorch 2.7+ for DTensor ```yaml optimizer: dion diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 5a25c1834d..0472acee94 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -29,7 +29,6 @@ TrainerCallback, ) from transformers.trainer_pt_utils import AcceleratorConfig -from transformers.training_args import OptimizerNames from axolotl.integrations.base import PluginManager from axolotl.monkeypatch.trainer.lr import patch_trainer_get_lr @@ -284,21 +283,6 @@ def _configure_custom_optimizer( optimizer_kwargs["foreach"] = False optimizer_cls = AdamW optimizer_kwargs.update(adam_kwargs) - elif self.cfg.optimizer == "ao_adamw_4bit": - # TODO remove 20250401 - from torchao.prototype.low_bit_optim import AdamW4bit - - optimizer_cls = AdamW4bit - optimizer_kwargs.update(adam_kwargs) - - LOG.warning( - f"`ao_adamw_4bit` will be deprecated soon. Please use `{OptimizerNames.ADAMW_TORCH_4BIT}` instead." - ) - elif self.cfg.optimizer == "ao_adamw_8bit": - from torchao.prototype.low_bit_optim import AdamW8bit - - optimizer_cls = AdamW8bit - optimizer_kwargs.update(adam_kwargs) elif self.cfg.optimizer == "ao_adamw_fp8": from torchao.prototype.low_bit_optim import AdamWFp8 From 784f8c0e952ff506e5215119de2aa38f70f1ddb7 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:32:07 +0530 Subject: [PATCH 0887/1405] fix:kd_distillation key_error logprobs (#2990) * fix:kd_distillation key_error logprobs * style * fix: leave handling of pop logprobs to parent --------- Co-authored-by: NanoCode012 --- src/axolotl/integrations/kd/chat_template.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/axolotl/integrations/kd/chat_template.py b/src/axolotl/integrations/kd/chat_template.py index f99dfe4586..6376ecb09f 100644 --- a/src/axolotl/integrations/kd/chat_template.py +++ b/src/axolotl/integrations/kd/chat_template.py @@ -284,12 +284,12 @@ def transform_logprobs(self, sample): return sample def _tokenize_single_prompt(self, prompt): - logprobs = prompt.pop(self.logprobs_field) - target_token_ids = prompt.pop("target_token_ids") + target_token_ids = prompt.get("target_token_ids", None) + tokenized_prompt = super()._tokenize_single_prompt(prompt) - tokenized_prompt[self.logprobs_field] = logprobs - tokenized_prompt["target_token_ids"] = target_token_ids - tokenized_prompt = self.transform_logprobs(tokenized_prompt) + + if target_token_ids is not None: + tokenized_prompt["target_token_ids"] = target_token_ids return tokenized_prompt From 97e86c6d47612f3465eaa73f070a231bd9550602 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 6 Aug 2025 08:02:39 -0400 Subject: [PATCH 0888/1405] drop old patches and code that are no longer needed (#3007) [skip ci] --- .runpod/README.md | 1 - .runpod/src/config/config.yaml | 2 - examples/archived/stablelm-2/1.6b/fft.yml | 1 - examples/llama-2/fft_optimized.yml | 1 - examples/llama-2/lisa.yml | 1 - src/axolotl/loaders/patch_manager.py | 21 +- .../monkeypatch/llama_attn_hijack_flash.py | 657 +----------------- .../monkeypatch/mistral_attn_hijack_flash.py | 640 ----------------- src/axolotl/utils/schemas/config.py | 6 - src/axolotl/utils/schemas/validation.py | 6 +- tests/e2e/patched/test_fused_llama.py | 1 - 11 files changed, 7 insertions(+), 1330 deletions(-) diff --git a/.runpod/README.md b/.runpod/README.md index 2d24f1e5cf..8042f4f91f 100644 --- a/.runpod/README.md +++ b/.runpod/README.md @@ -185,7 +185,6 @@ datasets: | `flash_attention` | `false` | Use flash attention | | `flash_attn_cross_entropy` | `false` | Flash attention cross entropy | | `flash_attn_rms_norm` | `false` | Flash attention RMS norm | -| `flash_attn_fuse_qkv` | `false` | Fuse QKV operations | | `flash_attn_fuse_mlp` | `false` | Fuse MLP operations | | `sdp_attention` | `false` | Use scaled dot product | | `s2_attention` | `false` | Use shifted sparse attention | diff --git a/.runpod/src/config/config.yaml b/.runpod/src/config/config.yaml index 2a89971fb3..f482a7331b 100644 --- a/.runpod/src/config/config.yaml +++ b/.runpod/src/config/config.yaml @@ -296,7 +296,6 @@ # flash_attention: # flash_attn_cross_entropy: # Whether to use flash-attention cross entropy implementation - advanced use only # flash_attn_rms_norm: # Whether to use flash-attention rms norm implementation - advanced use only -# flash_attn_fuse_qkv: # Whether to fuse QKV into a single operation # flash_attn_fuse_mlp: # Whether to fuse part of the MLP into a single operation # # Whether to use scaled-dot-product attention # # https://pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html @@ -541,7 +540,6 @@ xformers_attention: ${XFORMERS_ATTENTION} flash_attention: ${FLASH_ATTENTION} flash_attn_cross_entropy: ${FLASH_ATTN_CROSS_ENTROPY} flash_attn_rms_norm: ${FLASH_ATTN_RMS_NORM} -flash_attn_fuse_qkv: ${FLASH_ATTN_FUSE_QKV} flash_attn_fuse_mlp: ${FLASH_ATTN_FUSE_MLP} sdp_attention: ${SDP_ATTENTION} s2_attention: ${S2_ATTENTION} diff --git a/examples/archived/stablelm-2/1.6b/fft.yml b/examples/archived/stablelm-2/1.6b/fft.yml index d608bc66f8..585888f43b 100644 --- a/examples/archived/stablelm-2/1.6b/fft.yml +++ b/examples/archived/stablelm-2/1.6b/fft.yml @@ -47,7 +47,6 @@ logging_steps: 1 flash_attention: true flash_attn_cross_entropy: false flash_attn_rms_norm: true -flash_attn_fuse_qkv: false flash_attn_fuse_mlp: true warmup_ratio: 0.1 diff --git a/examples/llama-2/fft_optimized.yml b/examples/llama-2/fft_optimized.yml index 6788064733..ea119348e5 100644 --- a/examples/llama-2/fft_optimized.yml +++ b/examples/llama-2/fft_optimized.yml @@ -45,7 +45,6 @@ logging_steps: 1 flash_attention: true flash_attn_cross_entropy: false flash_attn_rms_norm: true -flash_attn_fuse_qkv: false flash_attn_fuse_mlp: true warmup_ratio: 0.1 diff --git a/examples/llama-2/lisa.yml b/examples/llama-2/lisa.yml index 7b92b72e1b..d21c01a495 100644 --- a/examples/llama-2/lisa.yml +++ b/examples/llama-2/lisa.yml @@ -49,7 +49,6 @@ logging_steps: 1 flash_attention: true flash_attn_cross_entropy: false flash_attn_rms_norm: true -flash_attn_fuse_qkv: false flash_attn_fuse_mlp: true warmup_ratio: 0.1 diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index e16f036497..4273f3ccea 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -348,31 +348,21 @@ def _patch_loss_llama(self): patch_self_attn_lora() - def _patch_llama_flash_attention(self, packed=False): + def _patch_llama_flash_attention(self): """Apply Flash Attention patches for LLaMA models.""" from axolotl.monkeypatch.llama_attn_hijack_flash import ( replace_llama_attn_with_flash_attn, ) - if packed: - if self.cfg.device not in ["mps", "cpu"] and not self.inference: - LOG.info("patching with flash attention for sample packing") - replace_llama_attn_with_flash_attn( - packed=True, - cross_entropy=self.cfg.flash_attn_cross_entropy, - rms_norm=self.cfg.flash_attn_rms_norm, - ) - elif self.cfg.s2_attention: + if self.cfg.s2_attention: LOG.info("patching w/ flash-enabled, shifted-sparse attention") replace_llama_attn_with_flash_attn( - packed=False, cross_entropy=self.cfg.flash_attn_cross_entropy, rms_norm=self.cfg.flash_attn_rms_norm, use_shifted_sparse_attn=True, ) elif self.cfg.flash_attn_cross_entropy or self.cfg.flash_attn_rms_norm: replace_llama_attn_with_flash_attn( - packed=False, cross_entropy=self.cfg.flash_attn_cross_entropy, rms_norm=self.cfg.flash_attn_rms_norm, ) @@ -403,7 +393,7 @@ def _patch_llama_derived_model(self): and self.cfg.sample_packing ): if self.cfg.flash_attention: - self._patch_llama_flash_attention(packed=self.cfg.sample_packing) + self._patch_llama_flash_attention() elif self.cfg.xformers_attention: self._patch_llama_xformers_attention() elif self.cfg.sample_packing: @@ -426,17 +416,12 @@ def _apply_llama_flash_attn_patches(self, model): from axolotl.monkeypatch.llama_attn_hijack_flash import ( is_xformers_swiglu_available, replace_llama_mlp_with_swiglu, - replace_llama_qkv_with_fused, ) if self.cfg.flash_attn_fuse_mlp and is_xformers_swiglu_available(): LOG.info("Patching with SwiGLU...") replace_llama_mlp_with_swiglu(model) - if self.cfg.flash_attn_fuse_qkv: - LOG.info("Patching with fused QKV...") - replace_llama_qkv_with_fused(model) - def _apply_unsloth_patches(self, model): """Apply unsloth optimization patches.""" if self.cfg.unsloth_lora_mlp: diff --git a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py index 70e36714c8..1316b53742 100644 --- a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py @@ -3,39 +3,26 @@ # copied from https://github.com/lm-sys/FastChat/blob/main/fastchat/train/llama_flash_attn_monkey_patch.py import warnings -from typing import List, Optional, Tuple, Union +from typing import Optional, Tuple import torch -import torch.nn.functional as F import transformers from einops import rearrange from flash_attn.bert_padding import pad_input, unpad_input -from transformers.modeling_outputs import BaseModelOutputWithPast -from transformers.models.llama.modeling_llama import ( - LlamaAttention, -) -from transformers.models.llama.modeling_llama import ( - LlamaDecoderLayer as OriginalLlamaDecoderLayer, -) from transformers.models.llama.modeling_llama import ( LlamaMLP, apply_rotary_pos_emb, repeat_kv, ) -from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids, set_module_name +from axolotl.monkeypatch.utils import set_module_name from axolotl.utils.logging import get_logger try: from flash_attn.flash_attn_interface import ( # pylint: disable=ungrouped-imports - flash_attn_kvpacked_func, - flash_attn_varlen_kvpacked_func, flash_attn_varlen_qkvpacked_func, ) except ImportError: - from flash_attn.flash_attn_interface import ( - flash_attn_unpadded_kvpacked_func as flash_attn_varlen_kvpacked_func, - ) from flash_attn.flash_attn_interface import ( flash_attn_unpadded_qkvpacked_func as flash_attn_varlen_qkvpacked_func, ) @@ -82,19 +69,6 @@ def replace_llama_mlp_with_swiglu(model): set_module_name(model, name, mlp) -def replace_llama_qkv_with_fused(model): - for name, module in model.named_modules(): - if isinstance(module, LlamaAttention): - qkv = FusedAttention( - module.config, - module.q_proj, - module.k_proj, - module.v_proj, - module.o_proj, - ) - set_module_name(model, name, qkv) - - def patch_fa_llama_cross_entropy(): LOG.info( "patching transformers.loss.loss_utils.fixed_cross_entropy with flash_attn.ops.triton.cross_entropy" @@ -142,7 +116,6 @@ def __init__(self, hidden_size, eps=1e-6): def replace_llama_attn_with_flash_attn( - packed: Optional[bool] = False, cross_entropy: Optional[bool] = False, rms_norm: Optional[bool] = False, use_shifted_sparse_attn: Optional[bool] = False, @@ -154,16 +127,6 @@ def replace_llama_attn_with_flash_attn( transformers.models.llama.modeling_llama.LlamaAttention.forward = ( flashattn_forward_with_s2attn ) - else: - transformers.models.llama.modeling_llama.LlamaAttention.forward = ( - flashattn_forward - ) - - if packed: - transformers.models.llama.modeling_llama.LlamaDecoderLayer = LlamaDecoderLayer - transformers.models.llama.modeling_llama.LlamaModel.forward = ( - llama_model_forward - ) # skip only if explicitly disabled if cross_entropy: @@ -174,49 +137,6 @@ def replace_llama_attn_with_flash_attn( patch_llama_rms_norm() -class FusedAttention(LlamaAttention): - """ - Fused QKV Attention layer for incrementally improved training efficiency - """ - - def __init__( - self, - config, - q: torch.nn.Linear, # pylint: disable=invalid-name - k: torch.nn.Linear, # pylint: disable=invalid-name - v: torch.nn.Linear, # pylint: disable=invalid-name - o: torch.nn.Linear, # pylint: disable=invalid-name - ): - super().__init__(config) - self.config = config - self.init_device = next(iter(q.state_dict().values())).device - - # define equivalent fused qkv projection - self.out_features: List[int] = [q.out_features, k.out_features, v.out_features] - self.qkv_proj = torch.nn.Linear( - q.in_features, sum(self.out_features), device=self.init_device, bias=False - ) - self.o_proj = o - - # overwrite initialized weights with pretrained weights - self.qkv_proj.weight.data = torch.cat( - (q.weight.data, k.weight.data, v.weight.data), dim=0 - ) - - def _post_training(self, model, name): - q_proj, k_proj, v_proj = torch.split( - self.qkv_proj.weight.data, self.out_features, dim=0 - ) - - new_attn = LlamaAttention(self.config) - new_attn.q_proj.weight.data = q_proj - new_attn.k_proj.weight.data = k_proj - new_attn.v_proj.weight.data = v_proj - new_attn.o_proj.weight.data = self.o_proj.weight.data - - set_module_name(model, name, new_attn) - - # Disable the transformation of the attention mask in LlamaModel as the flash attention # requires the attention mask to be the same as the key_padding_mask def _prepare_decoder_attention_mask( @@ -355,576 +275,3 @@ def flashattn_forward_with_s2attn( .reshape(bsz, q_len, nheads, self.head_dim) ) return self.o_proj(rearrange(output, "b s h d -> b s (h d)")), None, past_key_value - - -def flashattn_forward( - self, - hidden_states: torch.Tensor, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.Tensor] = None, - past_key_value: Optional[Tuple[torch.Tensor]] = None, - output_attentions: bool = False, - use_cache: bool = False, - padding_mask: Optional[torch.LongTensor] = None, # pylint: disable=unused-argument - cu_seqlens: Optional[torch.Tensor] = None, - max_seqlen: Optional[torch.Tensor] = None, -) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: - """Input shape: Batch x Time x Channel - - attention_mask: [bsz, q_len] - """ - # pylint: disable=duplicate-code - bsz, q_len, _ = hidden_states.size() - - if not hasattr(self, "pretraining_tp"): - self.pretraining_tp = 1 - - if self.pretraining_tp > 1: - key_value_slicing = ( - self.num_key_value_heads * self.head_dim - ) // self.pretraining_tp - query_slices = self.q_proj.weight.split( - (self.num_heads * self.head_dim) // self.pretraining_tp, dim=0 - ) - key_slices = self.k_proj.weight.split(key_value_slicing, dim=0) - value_slices = self.v_proj.weight.split(key_value_slicing, dim=0) - - query_states = [ - F.linear(hidden_states, query_slices[i]) for i in range(self.pretraining_tp) - ] - query_states = torch.cat(query_states, dim=-1) - - key_states = [ - F.linear(hidden_states, key_slices[i]) for i in range(self.pretraining_tp) - ] - key_states = torch.cat(key_states, dim=-1) - - value_states = [ - F.linear(hidden_states, value_slices[i]) for i in range(self.pretraining_tp) - ] - value_states = torch.cat(value_states, dim=-1) - - else: - if isinstance(self, FusedAttention): - query_states, key_states, value_states = self.qkv_proj(hidden_states).split( - self.out_features, dim=-1 - ) - else: - query_states = self.q_proj(hidden_states) - key_states = self.k_proj(hidden_states) - value_states = self.v_proj(hidden_states) - - query_states = query_states.view( - bsz, q_len, self.num_heads, self.head_dim - ).transpose(1, 2) - key_states = key_states.view( - bsz, q_len, self.num_key_value_heads, self.head_dim - ).transpose(1, 2) - value_states = value_states.view( - bsz, q_len, self.num_key_value_heads, self.head_dim - ).transpose(1, 2) - # [bsz, q_len, nh, hd] - # [bsz, nh, q_len, hd] - - cos, sin = self.rotary_emb(value_states, position_ids=position_ids) - query_states, key_states = apply_rotary_pos_emb( - query_states, key_states, cos, sin, position_ids - ) - # [bsz, nh, t, hd] - - if past_key_value is not None: - # reuse k, v, self_attention - key_states = torch.cat([past_key_value[0], key_states], dim=2) - value_states = torch.cat([past_key_value[1], value_states], dim=2) - - past_key_value = (key_states, value_states) if use_cache else None - - # repeat k/v heads if n_kv_heads < n_heads - key_states = repeat_kv(key_states, self.num_key_value_groups) - value_states = repeat_kv(value_states, self.num_key_value_groups) - - if output_attentions: - warnings.warn( - "Output attentions is not supported for patched `LlamaAttention`, returning `None` instead." - ) - - # - # flash-attn v2 start - # - - if self.training: - # during training q,k,v always have same seqlen - assert key_states.shape == query_states.shape - is_causal = True - else: - # turn off FA causal mask after first inference autoregressive iteration - # only on first autoregressive step q,k,v have same seqlen - is_causal = key_states.shape == query_states.shape - - dropout_rate = 0.0 if not self.training else getattr(self, "attention_dropout", 0.0) - - if cu_seqlens is not None and max_seqlen is not None and cu_seqlens.dim() == 1: - # special handling using sample packing - qkv = torch.stack( - [query_states, key_states, value_states], dim=2 - ) # [bsz, nh, 3, q_len, hd] - qkv = qkv.transpose(1, 3) # [bsz, q_len, 3, nh, hd] - qkv = rearrange(qkv, "b s ... -> (b s) ...") - - output = flash_attn_varlen_qkvpacked_func( - qkv, - cu_seqlens, - max_seqlen, - dropout_p=dropout_rate, - softmax_scale=None, - causal=True, - ) - output = rearrange(output, "(b s) ... -> b s ...", b=bsz) - elif query_states.shape == key_states.shape: - query_states = query_states.transpose(1, 2) - key_states = key_states.transpose(1, 2) - value_states = value_states.transpose(1, 2) - qkv_unpad, cu_seqlens_q, max_seqlen_q, _, output_pad_fn = generate_qkv( - query_states, - key_states, - value_states, - qkvpacked=True, - # We have disabled _prepare_decoder_attention_mask in LlamaModel - # the attention_mask should be the same as the key_padding_mask - key_padding_mask=attention_mask, - query_padding_mask=( - attention_mask[:, -query_states.size(1) :] - if attention_mask is not None - else None - ), - ) - output_unpad = flash_attn_varlen_qkvpacked_func( - qkv_unpad, - cu_seqlens_q, - max_seqlen_q, - dropout_p=dropout_rate, - softmax_scale=None, - causal=is_causal, - ) - output = output_pad_fn(output_unpad) - else: - query_states = query_states.transpose(1, 2) - key_states = key_states.transpose(1, 2) - value_states = value_states.transpose(1, 2) - if attention_mask is None or attention_mask.all().item(): - output = flash_attn_kvpacked_func( - query_states, - torch.stack([key_states, value_states], 2), - dropout_p=dropout_rate, - causal=is_causal, - ) - else: - ( # pylint: disable=unbalanced-tuple-unpacking - q_unpad, - kv_unpad, - cu_seqlens_q, - cu_seqlens_k, - max_seqlen_q, - max_seqlen_k, - _, - _, - output_pad_fn, - ) = generate_qkv( - query_states, - key_states, - value_states, - kvpacked=True, - key_padding_mask=attention_mask, - query_padding_mask=( - attention_mask[:, -query_states.size(1) :] - if attention_mask is not None - else None - ), - ) - if q_unpad.dtype != kv_unpad.dtype: - kv_unpad = kv_unpad.to(q_unpad.dtype) - output_unpad = flash_attn_varlen_kvpacked_func( - q_unpad, - kv_unpad, - cu_seqlens_q, - cu_seqlens_k, - max_seqlen_q, - max_seqlen_k, - dropout_p=dropout_rate, - softmax_scale=None, - causal=is_causal, - ) - output = output_pad_fn(output_unpad) - - attn_output = output - if attn_output.size() != (bsz, q_len, self.num_heads, self.head_dim): - raise ValueError( - f"`attn_output` should be of size {(bsz, q_len, self.num_heads, self.head_dim)}, but is" - f" {attn_output.size()}" - ) - attn_output = rearrange(attn_output, "b s h d -> b s (h d)") - - # - # flash-attn v2 end - # - - if self.pretraining_tp > 1: - attn_output = attn_output.split(self.hidden_size // self.pretraining_tp, dim=2) - o_proj_slices = self.o_proj.weight.split( - self.hidden_size // self.pretraining_tp, dim=1 - ) - attn_output = sum( - F.linear(attn_output[i], o_proj_slices[i]) - for i in range(self.pretraining_tp) - ) - else: - attn_output = self.o_proj(attn_output) - - return attn_output, None, past_key_value - - -# based on https://github.com/Dao-AILab/flash-attention/blob/364a5b/tests/test_flash_attn.py#L38 -def generate_qkv( - q, - k, - v, - query_padding_mask=None, - key_padding_mask=None, - kvpacked=False, - qkvpacked=False, -): # pylint: disable=invalid-name,unnecessary-lambda-assignment - """ - Arguments: - q: (batch_size, seqlen_q, nheads, d) - k: (batch_size, seqlen_k, nheads_k, d) - v: (batch_size, seqlen_k, nheads_k, d) - query_padding_mask: (batch_size, seqlen), bool - key_padding_mask: (batch_size, seqlen), bool - """ - assert not (kvpacked and qkvpacked) - batch_size, seqlen_q, nheads, d = q.shape - _, seqlen_k, nheads_k, _ = k.shape - assert k.shape == (batch_size, seqlen_k, nheads_k, d) - assert v.shape == (batch_size, seqlen_k, nheads_k, d) - - if query_padding_mask is not None: - q_unpad, indices_q, cu_seqlens_q, max_seqlen_q = unpad_input( - q, query_padding_mask - ) - - def output_pad_fn(output_unpad): - return pad_input( # noqa: E731 - output_unpad, indices_q, batch_size, seqlen_q - ) - - else: - q_unpad = rearrange(q, "b s h d -> (b s) h d") - cu_seqlens_q = torch.arange( - 0, - (batch_size + 1) * seqlen_q, - step=seqlen_q, - dtype=torch.int32, - device=q_unpad.device, - ) - max_seqlen_q = seqlen_q - - def output_pad_fn(output_unpad): - return rearrange( # noqa: E731 - output_unpad, "(b s) h d -> b s h d", b=batch_size - ) - - if key_padding_mask is not None: - k_unpad, _, cu_seqlens_k, max_seqlen_k = unpad_input(k, key_padding_mask) - v_unpad, _, _, _ = unpad_input(v, key_padding_mask) - else: - k_unpad = rearrange(k, "b s h d -> (b s) h d") - v_unpad = rearrange(v, "b s h d -> (b s) h d") - cu_seqlens_k = torch.arange( - 0, - (batch_size + 1) * seqlen_k, - step=seqlen_k, - dtype=torch.int32, - device=k_unpad.device, - ) - max_seqlen_k = seqlen_k - - if qkvpacked: - assert nheads == nheads_k - qkv_unpad = torch.stack([q_unpad, k_unpad, v_unpad], dim=1) - qkv = torch.stack([q, k, v], dim=2) - return (qkv_unpad, cu_seqlens_q, max_seqlen_q, qkv, output_pad_fn) - - if kvpacked: - kv_unpad = torch.stack([k_unpad, v_unpad], dim=1) - kv = torch.stack([k, v], dim=2) - return ( - q_unpad, - kv_unpad, - cu_seqlens_q, - cu_seqlens_k, - max_seqlen_q, - max_seqlen_k, - q, - kv, - output_pad_fn, - ) - - return ( - q_unpad, - k_unpad, - v_unpad, - cu_seqlens_q, - cu_seqlens_k, - max_seqlen_q, - max_seqlen_k, - q, - k, - v, - output_pad_fn, - ) - - -def llama_model_forward( - self, - input_ids: torch.LongTensor = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[List[torch.FloatTensor]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - cache_position: Optional[ # pylint: disable=unused-argument - torch.LongTensor - ] = None, -) -> Union[Tuple, BaseModelOutputWithPast]: - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - use_cache = use_cache if use_cache is not None else self.config.use_cache - - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - - # retrieve input_ids and inputs_embeds - if input_ids is not None and inputs_embeds is not None: - raise ValueError( - "You cannot specify both decoder_input_ids and decoder_inputs_embeds at the same time" - ) - if input_ids is not None: - batch_size, seq_length = input_ids.shape - elif inputs_embeds is not None: - batch_size, seq_length, _ = inputs_embeds.shape - else: - raise ValueError( - "You have to specify either decoder_input_ids or decoder_inputs_embeds" - ) - - seq_length_with_past = seq_length - past_key_values_length = 0 - - if past_key_values is not None: - past_key_values_length = past_key_values[0][0].shape[2] - seq_length_with_past = seq_length_with_past + past_key_values_length - - cu_seqlens = None - max_seqlen = None - if position_ids is None: - device = input_ids.device if input_ids is not None else inputs_embeds.device - position_ids = torch.arange( - past_key_values_length, - seq_length + past_key_values_length, - dtype=torch.long, - device=device, - ) - position_ids = position_ids.unsqueeze(0).view(-1, seq_length) - else: - position_ids = position_ids.view(-1, seq_length).long() - cu_seqlens, max_seqlen = get_cu_seqlens_from_pos_ids(position_ids) - cu_seqlens = cu_seqlens.squeeze() - - if inputs_embeds is None: - inputs_embeds = self.embed_tokens(input_ids) - # embed positions - if attention_mask is None: - attention_mask = torch.ones( - (batch_size, seq_length_with_past), - dtype=torch.bool, - device=inputs_embeds.device, - ) - padding_mask = None - else: - if 0 in attention_mask: - padding_mask = attention_mask - else: - padding_mask = None - - attention_mask = ( - self._prepare_decoder_attention_mask( # pylint: disable=protected-access - attention_mask, - (batch_size, seq_length), - inputs_embeds, - past_key_values_length, - ) - ) - - hidden_states = inputs_embeds - - if self.gradient_checkpointing and self.training: - if use_cache: - transformers.logger.warning_once( - "`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`..." - ) - use_cache = False - - # decoder layers - all_hidden_states = () if output_hidden_states else None - all_self_attns = () if output_attentions else None - next_decoder_cache = () if use_cache else None - - for idx, decoder_layer in enumerate(self.layers): - if output_hidden_states: - all_hidden_states += (hidden_states,) - - past_key_value = past_key_values[idx] if past_key_values is not None else None - - if self.gradient_checkpointing and self.training: - - def create_custom_forward(module): - def custom_forward(*inputs): - # None for past_key_value - return module( - *inputs, - ) - - return custom_forward - - layer_outputs = torch.utils.checkpoint.checkpoint( - create_custom_forward(decoder_layer), - hidden_states, - attention_mask, - position_ids, - past_key_value, - output_attentions, - None, - padding_mask, - cu_seqlens, - max_seqlen, - ) - else: - layer_outputs = decoder_layer( - hidden_states, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_value=past_key_value, - output_attentions=output_attentions, - use_cache=use_cache, - cu_seqlens=cu_seqlens, - max_seqlen=max_seqlen, - ) - - hidden_states = layer_outputs[0] - - if use_cache: - next_decoder_cache += (layer_outputs[2 if output_attentions else 1],) - - if output_attentions: - all_self_attns += (layer_outputs[1],) - - hidden_states = self.norm(hidden_states) - - # add hidden states from the last decoder layer - if output_hidden_states: - all_hidden_states += (hidden_states,) - - next_cache = next_decoder_cache if use_cache else None - if not return_dict: - return tuple( - v - for v in [hidden_states, next_cache, all_hidden_states, all_self_attns] - if v is not None - ) - return BaseModelOutputWithPast( - last_hidden_state=hidden_states, - past_key_values=next_cache, - hidden_states=all_hidden_states, - attentions=all_self_attns, - ) - - -class LlamaDecoderLayer(OriginalLlamaDecoderLayer): - """ - patched version of LlamaDecoderLayer to pass through the precalculated cu_seqlens - """ - - def forward( - self, - hidden_states: torch.Tensor, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_value: Optional[Tuple[torch.Tensor]] = None, - output_attentions: Optional[bool] = False, - use_cache: Optional[bool] = False, - padding_mask: Optional[torch.LongTensor] = None, - cu_seqlens: Optional[torch.Tensor] = None, - max_seqlen: Optional[torch.Tensor] = None, - ) -> Tuple[ - torch.FloatTensor, Optional[Tuple[torch.FloatTensor, torch.FloatTensor]] - ]: - """ - Args: - hidden_states (`torch.FloatTensor`): input to the layer of shape `(batch, seq_len, embed_dim)` - attention_mask (`torch.FloatTensor`, *optional*): attention mask of size - `(batch, 1, tgt_len, src_len)` where padding elements are indicated by very large negative values. - output_attentions (`bool`, *optional*): - Whether or not to return the attentions tensors of all attention layers. See `attentions` under - returned tensors for more detail. - use_cache (`bool`, *optional*): - If set to `True`, `past_key_values` key value states are returned and can be used to speed up decoding - (see `past_key_values`). - past_key_value (`Tuple(torch.FloatTensor)`, *optional*): cached past key and value projection states - cu_seqlens (`torch.Tensor`, *optional*) cumulative sequence len when packing - """ - - residual = hidden_states - - hidden_states = self.input_layernorm(hidden_states) - - # Self Attention - hidden_states, self_attn_weights, present_key_value = self.self_attn( - hidden_states=hidden_states, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_value=past_key_value, - output_attentions=output_attentions, - use_cache=use_cache, - padding_mask=padding_mask, - cu_seqlens=cu_seqlens, - max_seqlen=max_seqlen, - ) - hidden_states = residual + hidden_states - - # Fully Connected - residual = hidden_states - hidden_states = self.post_attention_layernorm(hidden_states) - hidden_states = self.mlp(hidden_states) - hidden_states = residual + hidden_states - - outputs = (hidden_states,) - - if output_attentions: - outputs += (self_attn_weights,) - - if use_cache: - outputs += (present_key_value,) - - return outputs diff --git a/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py b/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py index 3fc22917fb..e1be424a3e 100644 --- a/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py @@ -3,53 +3,14 @@ # pylint: disable=duplicate-code from functools import partial -from typing import List, Optional, Tuple, Union -import torch import transformers -from einops import rearrange -from flash_attn.bert_padding import pad_input, unpad_input -from flash_attn.flash_attn_interface import ( # pylint: disable=ungrouped-imports - flash_attn_kvpacked_func, - flash_attn_varlen_kvpacked_func, - flash_attn_varlen_qkvpacked_func, -) -from transformers.modeling_outputs import BaseModelOutputWithPast -from transformers.models.mistral.modeling_mistral import ( - MistralAttention as OriginalMistralAttention, -) -from transformers.models.mistral.modeling_mistral import ( - MistralDecoderLayer as OriginalMistralDecoderLayer, -) -from transformers.models.mistral.modeling_mistral import ( - apply_rotary_pos_emb, - repeat_kv, -) -from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids from axolotl.utils.logging import get_logger LOG = get_logger(__name__) -def replace_mistral_attn_with_flash_attn( - packed: Optional[bool] = False, -): - transformers.models.mistral.modeling_mistral.MistralModel._prepare_decoder_attention_mask = ( # pylint: disable=protected-access - _prepare_decoder_attention_mask - ) - transformers.models.mistral.modeling_mistral.MistralAttention.forward = ( - flashattn_forward - ) - if packed: - transformers.models.mistral.modeling_mistral.MistralDecoderLayer = ( - MistralDecoderLayer - ) - transformers.models.mistral.modeling_mistral.MistralModel.forward = ( - mistral_model_forward - ) - - def patch_mistral_cross_entropy(): from flash_attn.losses.cross_entropy import CrossEntropyLoss @@ -57,604 +18,3 @@ def patch_mistral_cross_entropy(): transformers.models.mistral.modeling_mistral.CrossEntropyLoss = partial( CrossEntropyLoss, inplace_backward=True ) - - -@torch.jit.script -def _make_sliding_window_causal_mask( - bsz: int, - tgt_len: int, - dtype: torch.dtype, - device: torch.device, - past_key_values_length: int = 0, - sliding_window: int = 4096, -): - """ - Make causal mask used for sliding window attention - """ - tensor = torch.full( - (tgt_len, tgt_len), - fill_value=1, - device=device, - ) - mask = torch.tril(tensor, diagonal=0) - # make the mask banded to account for sliding window - # NOTE: HF implementation is wrong as of 14-10-2023 for torch.triu, needs +1 - mask = torch.triu(mask, diagonal=-sliding_window + 1) - mask = torch.log(mask).to(dtype) - - if past_key_values_length > 0: - mask = torch.cat( - [ - torch.zeros( - tgt_len, past_key_values_length, dtype=dtype, device=device - ), - mask, - ], - dim=-1, - ) - return mask[None, None, :, :].expand( - bsz, 1, tgt_len, tgt_len + past_key_values_length - ) - - -# Disable the transformation of the attention mask in LlamaModel as the flash attention -# requires the attention mask to be the same as the key_padding_mask -def _prepare_decoder_attention_mask( - self, - attention_mask, - input_shape, - inputs_embeds, - past_key_values_length, - sliding_window, -): # pylint: disable=unused-argument - # [bsz, seq_len] - if attention_mask is None or sliding_window is None: - return attention_mask - - # NOTE: attention mask and sliding masks are only broadcastable in certain scenarios. - # Without attention_mask.shape[0] == 1, error will trigger after eval loss but only when wandb is enabled. - if input_shape[-1] > 1 and attention_mask.shape[0] == 1: - sliding_window_mask = _make_sliding_window_causal_mask( - bsz=input_shape[0], - tgt_len=input_shape[1], - dtype=inputs_embeds.dtype, - device=inputs_embeds.device, - past_key_values_length=past_key_values_length, - sliding_window=sliding_window, - ) - attention_mask = attention_mask + sliding_window_mask - else: - LOG.info("skipping sliding window mask, not broadcastable with attention mask") - - return attention_mask - - -def flashattn_forward( - self: OriginalMistralAttention, - hidden_states: torch.Tensor, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_value: Optional[Tuple[torch.Tensor]] = None, - output_attentions: bool = False, - use_cache: bool = False, - cu_seqlens: Optional[torch.Tensor] = None, - max_seqlen: Optional[torch.Tensor] = None, -) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: - bsz, q_len, _ = hidden_states.size() - - query_states = self.q_proj(hidden_states) - key_states = self.k_proj(hidden_states) - value_states = self.v_proj(hidden_states) - - query_states = query_states.view( - bsz, q_len, self.num_heads, self.head_dim - ).transpose(1, 2) - key_states = key_states.view( - bsz, q_len, self.num_key_value_heads, self.head_dim - ).transpose(1, 2) - value_states = value_states.view( - bsz, q_len, self.num_key_value_heads, self.head_dim - ).transpose(1, 2) - - kv_seq_len = key_states.shape[-2] - if past_key_value is not None: - kv_seq_len += past_key_value[0].shape[-2] - cos, sin = self.rotary_emb(value_states, position_ids=position_ids) - query_states, key_states = apply_rotary_pos_emb( - query_states, key_states, cos, sin, position_ids - ) - - use_sliding_windows = ( - getattr(self.config, "sliding_window") is not None - and kv_seq_len > self.config.sliding_window - ) - - if use_sliding_windows: - window_size = (self.config.sliding_window, self.config.sliding_window) - else: - window_size = (-1, -1) - - if past_key_value is not None: - # Activate slicing cache only if the config has a value `sliding_windows` attribute - if ( - hasattr(self.config, "sliding_window") - and kv_seq_len > self.config.sliding_window - ): - slicing_tokens = kv_seq_len - self.config.sliding_window - - past_key = past_key_value[0] - past_value = past_key_value[1] - - past_key = past_key[:, :, slicing_tokens:, :].contiguous() - past_value = past_value[:, :, slicing_tokens:, :].contiguous() - - if past_key.shape[-2] != self.config.sliding_window - 1: - raise ValueError( - f"past key much have a shape of (`batch_size, num_heads, self.config.sliding_window-1, head_dim`), got" - f" {past_key.shape}" - ) - - past_key_value = (past_key, past_value) if use_cache else None - - if past_key_value is not None: - key_states = torch.cat([past_key_value[0], key_states], dim=2) - value_states = torch.cat([past_key_value[1], value_states], dim=2) - - past_key_value = (key_states, value_states) if use_cache else None - - # repeat k/v heads if n_kv_heads < n_heads - key_states = repeat_kv(key_states, self.num_key_value_groups) - value_states = repeat_kv(value_states, self.num_key_value_groups) - - if self.training: - # during training q,k,v always have same seqlen - assert key_states.shape == query_states.shape - is_causal = True - else: - # turn off FA causal mask after first inference autoregressive iteration - # only on first autoregressive step q,k,v have same seqlen - is_causal = key_states.shape == query_states.shape - - dropout_rate = 0.0 if not self.training else getattr(self, "attention_dropout", 0.0) - - if cu_seqlens is not None and max_seqlen is not None and cu_seqlens.dim() == 1: - # special handling using sample packing - qkv = torch.stack( - [query_states, key_states, value_states], dim=2 - ) # [bsz, nh, 3, q_len, hd] - qkv = qkv.transpose(1, 3) # [bsz, q_len, 3, nh, hd] - qkv = rearrange(qkv, "b s ... -> (b s) ...") - - output = flash_attn_varlen_qkvpacked_func( - qkv, - cu_seqlens, - max_seqlen, - dropout_p=dropout_rate, - softmax_scale=None, - causal=True, - window_size=window_size, - ) - output = rearrange(output, "(b s) ... -> b s ...", b=bsz) - elif query_states.shape == key_states.shape: - query_states = query_states.transpose(1, 2) - key_states = key_states.transpose(1, 2) - value_states = value_states.transpose(1, 2) - qkv_unpad, cu_seqlens_q, max_seqlen_q, _, output_pad_fn = generate_qkv( - query_states, - key_states, - value_states, - qkvpacked=True, - # We have disabled _prepare_decoder_attention_mask in LlamaModel - # the attention_mask should be the same as the key_padding_mask - key_padding_mask=attention_mask, - query_padding_mask=( - attention_mask[:, -query_states.size(1) :] - if attention_mask is not None - else None - ), - ) - output_unpad = flash_attn_varlen_qkvpacked_func( - qkv_unpad, - cu_seqlens_q, - max_seqlen_q, - dropout_p=dropout_rate, - softmax_scale=None, - causal=is_causal, - window_size=window_size, - ) - output = output_pad_fn(output_unpad) - else: - query_states = query_states.transpose(1, 2) - key_states = key_states.transpose(1, 2) - value_states = value_states.transpose(1, 2) - if attention_mask is None or attention_mask.all().item(): - output = flash_attn_kvpacked_func( - query_states, - torch.stack([key_states, value_states], 2), - dropout_p=dropout_rate, - causal=is_causal, - window_size=window_size, - ) - else: - ( # pylint: disable=unbalanced-tuple-unpacking - q_unpad, - kv_unpad, - cu_seqlens_q, - cu_seqlens_k, - max_seqlen_q, - max_seqlen_k, - _, - _, - output_pad_fn, - ) = generate_qkv( - query_states, - key_states, - value_states, - kvpacked=True, - key_padding_mask=attention_mask, - query_padding_mask=( - attention_mask[:, -query_states.size(1) :] - if attention_mask is not None - else None - ), - ) - if q_unpad.dtype != kv_unpad.dtype: - kv_unpad = kv_unpad.to(q_unpad.dtype) - output_unpad = flash_attn_varlen_kvpacked_func( - q_unpad, - kv_unpad, - cu_seqlens_q, - cu_seqlens_k, - max_seqlen_q, - max_seqlen_k, - dropout_p=dropout_rate, - softmax_scale=None, - causal=is_causal, - window_size=window_size, - ) - output = output_pad_fn(output_unpad) - - attn_output = output - if attn_output.size() != (bsz, q_len, self.num_heads, self.head_dim): - raise ValueError( - f"`attn_output` should be of size {(bsz, q_len, self.num_heads, self.head_dim)}, but is" - f" {attn_output.size()}" - ) - attn_output = rearrange(attn_output, "b s h d -> b s (h d)") - - attn_output = self.o_proj(attn_output) - - if not output_attentions: - attn_weights = None - - return attn_output, attn_weights, past_key_value - - -# based on https://github.com/Dao-AILab/flash-attention/blob/364a5b/tests/test_flash_attn.py#L38 -def generate_qkv( - q, - k, - v, - query_padding_mask=None, - key_padding_mask=None, - kvpacked=False, - qkvpacked=False, -): # pylint: disable=invalid-name,unnecessary-lambda-assignment - """ - Arguments: - q: (batch_size, seqlen_q, nheads, d) - k: (batch_size, seqlen_k, nheads_k, d) - v: (batch_size, seqlen_k, nheads_k, d) - query_padding_mask: (batch_size, seqlen), bool - key_padding_mask: (batch_size, seqlen), bool - """ - assert not (kvpacked and qkvpacked) - batch_size, seqlen_q, nheads, d = q.shape - _, seqlen_k, nheads_k, _ = k.shape - assert k.shape == (batch_size, seqlen_k, nheads_k, d) - assert v.shape == (batch_size, seqlen_k, nheads_k, d) - - if query_padding_mask is not None: - q_unpad, indices_q, cu_seqlens_q, max_seqlen_q = unpad_input( - q, query_padding_mask - ) - - def output_pad_fn(output_unpad): - return pad_input( # noqa: E731 - output_unpad, indices_q, batch_size, seqlen_q - ) - - else: - q_unpad = rearrange(q, "b s h d -> (b s) h d") - cu_seqlens_q = torch.arange( - 0, - (batch_size + 1) * seqlen_q, - step=seqlen_q, - dtype=torch.int32, - device=q_unpad.device, - ) - max_seqlen_q = seqlen_q - - def output_pad_fn(output_unpad): - return rearrange( # noqa: E731 - output_unpad, "(b s) h d -> b s h d", b=batch_size - ) - - if key_padding_mask is not None: - k_unpad, _, cu_seqlens_k, max_seqlen_k = unpad_input(k, key_padding_mask) - v_unpad, _, _, _ = unpad_input(v, key_padding_mask) - else: - k_unpad = rearrange(k, "b s h d -> (b s) h d") - v_unpad = rearrange(v, "b s h d -> (b s) h d") - cu_seqlens_k = torch.arange( - 0, - (batch_size + 1) * seqlen_k, - step=seqlen_k, - dtype=torch.int32, - device=k_unpad.device, - ) - max_seqlen_k = seqlen_k - - if qkvpacked: - assert nheads == nheads_k - qkv_unpad = torch.stack([q_unpad, k_unpad, v_unpad], dim=1) - qkv = torch.stack([q, k, v], dim=2) - return (qkv_unpad, cu_seqlens_q, max_seqlen_q, qkv, output_pad_fn) - - if kvpacked: - kv_unpad = torch.stack([k_unpad, v_unpad], dim=1) - kv = torch.stack([k, v], dim=2) - return ( - q_unpad, - kv_unpad, - cu_seqlens_q, - cu_seqlens_k, - max_seqlen_q, - max_seqlen_k, - q, - kv, - output_pad_fn, - ) - - return ( - q_unpad, - k_unpad, - v_unpad, - cu_seqlens_q, - cu_seqlens_k, - max_seqlen_q, - max_seqlen_k, - q, - k, - v, - output_pad_fn, - ) - - -def mistral_model_forward( - self, - input_ids: torch.LongTensor = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[List[torch.FloatTensor]] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - return_dict: Optional[bool] = None, - cache_position: Optional[ # pylint: disable=unused-argument - torch.LongTensor - ] = None, -) -> Union[Tuple, BaseModelOutputWithPast]: - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - use_cache = use_cache if use_cache is not None else self.config.use_cache - - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) - - # retrieve input_ids and inputs_embeds - if input_ids is not None and inputs_embeds is not None: - raise ValueError( - "You cannot specify both decoder_input_ids and decoder_inputs_embeds at the same time" - ) - if input_ids is not None: - batch_size, seq_length = input_ids.shape - elif inputs_embeds is not None: - batch_size, seq_length, _ = inputs_embeds.shape - else: - raise ValueError( - "You have to specify either decoder_input_ids or decoder_inputs_embeds" - ) - - seq_length_with_past = seq_length - past_key_values_length = 0 - - if past_key_values is not None: - past_key_values_length = past_key_values[0][0].shape[2] - seq_length_with_past = seq_length_with_past + past_key_values_length - - cu_seqlens = None - max_seqlen = None - if position_ids is None: - device = input_ids.device if input_ids is not None else inputs_embeds.device - position_ids = torch.arange( - past_key_values_length, - seq_length + past_key_values_length, - dtype=torch.long, - device=device, - ) - position_ids = position_ids.unsqueeze(0).view(-1, seq_length) - else: - position_ids = position_ids.view(-1, seq_length).long() - cu_seqlens, max_seqlen = get_cu_seqlens_from_pos_ids(position_ids) - cu_seqlens = cu_seqlens.squeeze() - - if inputs_embeds is None: - inputs_embeds = self.embed_tokens(input_ids) - # embed positions - if attention_mask is None: - attention_mask = torch.ones( - (batch_size, seq_length_with_past), - dtype=torch.bool, - device=inputs_embeds.device, - ) - attention_mask = ( - self._prepare_decoder_attention_mask( # pylint: disable=protected-access - attention_mask, - (batch_size, seq_length), - inputs_embeds, - past_key_values_length, - sliding_window=self.config.sliding_window, - ) - ) - - hidden_states = inputs_embeds - - if self.gradient_checkpointing and self.training: - if use_cache: - transformers.logger.warning_once( - "`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`..." - ) - use_cache = False - - # decoder layers - all_hidden_states = () if output_hidden_states else None - all_self_attns = () if output_attentions else None - next_decoder_cache = () if use_cache else None - - for idx, decoder_layer in enumerate(self.layers): - if output_hidden_states: - all_hidden_states += (hidden_states,) - - past_key_value = past_key_values[idx] if past_key_values is not None else None - - if self.gradient_checkpointing and self.training: - layer_outputs = ( - self._gradient_checkpointing_func( # pylint: disable=protected-access - decoder_layer.__call__, - hidden_states, - attention_mask, - position_ids, - past_key_value, - output_attentions, - None, - cu_seqlens, - max_seqlen, - ) - ) - else: - layer_outputs = decoder_layer( - hidden_states, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_value=past_key_value, - output_attentions=output_attentions, - use_cache=use_cache, - cu_seqlens=cu_seqlens, - max_seqlen=max_seqlen, - ) - - hidden_states = layer_outputs[0] - - if use_cache: - next_decoder_cache += (layer_outputs[2 if output_attentions else 1],) - - if output_attentions: - all_self_attns += (layer_outputs[1],) - - hidden_states = self.norm(hidden_states) - - # add hidden states from the last decoder layer - if output_hidden_states: - all_hidden_states += (hidden_states,) - - next_cache = next_decoder_cache if use_cache else None - if not return_dict: - return tuple( - v - for v in [hidden_states, next_cache, all_hidden_states, all_self_attns] - if v is not None - ) - return BaseModelOutputWithPast( - last_hidden_state=hidden_states, - past_key_values=next_cache, - hidden_states=all_hidden_states, - attentions=all_self_attns, - ) - - -class MistralDecoderLayer(OriginalMistralDecoderLayer): - """ - patched version of MistralDecoderLayer to pass through the precalculated cu_seqlens - """ - - def forward( - self, - hidden_states: torch.Tensor, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_value: Optional[Tuple[torch.Tensor]] = None, - output_attentions: Optional[bool] = False, - use_cache: Optional[bool] = False, - cu_seqlens: Optional[torch.Tensor] = None, - max_seqlen: Optional[torch.Tensor] = None, - ) -> Tuple[ - torch.FloatTensor, Optional[Tuple[torch.FloatTensor, torch.FloatTensor]] - ]: - """ - Args: - hidden_states (`torch.FloatTensor`): input to the layer of shape `(batch, seq_len, embed_dim)` - attention_mask (`torch.FloatTensor`, *optional*): attention mask of size - `(batch, 1, tgt_len, src_len)` where padding elements are indicated by very large negative values. - output_attentions (`bool`, *optional*): - Whether or not to return the attentions tensors of all attention layers. See `attentions` under - returned tensors for more detail. - use_cache (`bool`, *optional*): - If set to `True`, `past_key_values` key value states are returned and can be used to speed up decoding - (see `past_key_values`). - past_key_value (`Tuple(torch.FloatTensor)`, *optional*): cached past key and value projection states - cu_seqlens (`torch.Tensor`, *optional*) cumulative sequence len when packing - """ - - residual = hidden_states - - hidden_states = self.input_layernorm(hidden_states) - - # Self Attention - hidden_states, self_attn_weights, present_key_value = self.self_attn( - hidden_states=hidden_states, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_value=past_key_value, - output_attentions=output_attentions, - use_cache=use_cache, - cu_seqlens=cu_seqlens, - max_seqlen=max_seqlen, - ) - hidden_states = residual + hidden_states - - # Fully Connected - residual = hidden_states - hidden_states = self.post_attention_layernorm(hidden_states) - hidden_states = self.mlp(hidden_states) - hidden_states = residual + hidden_states - - outputs = (hidden_states,) - - if output_attentions: - outputs += (self_attn_weights,) - - if use_cache: - outputs += (present_key_value,) - - return outputs diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 1d089ba41f..beaee57c90 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -531,12 +531,6 @@ class AxolotlInputConfig( "description": "Whether to use flash-attention rms norm implementation - advanced use only" }, ) - flash_attn_fuse_qkv: bool | None = Field( - default=None, - json_schema_extra={ - "description": "Whether to fuse QKV into a single operation" - }, - ) flash_attn_fuse_mlp: bool | None = Field( default=None, json_schema_extra={ diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 61eec65d5d..e15adf0779 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -577,9 +577,7 @@ def check_lora_axolotl_unsloth(cls, data): @model_validator(mode="after") def check_fused_lora(self): - if self.adapter in ["lora", "qlora"] and ( - self.flash_attn_fuse_qkv or self.flash_attn_fuse_mlp - ): + if self.adapter in ["lora", "qlora"] and self.flash_attn_fuse_mlp: raise ValueError("Fused modules are not supported with LoRA/QLoRA") return self @@ -1184,7 +1182,7 @@ def check_relora(self): "ReLoRA is not compatible with the one_cycle scheduler" ) - if self.flash_attn_fuse_qkv or self.flash_attn_fuse_mlp: + if self.flash_attn_fuse_mlp: raise ValueError("Fused modules are not supported with ReLoRA") return self diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index a3fe591ee8..f0c4f155f5 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -29,7 +29,6 @@ def test_fft_packing(self, temp_dir): "base_model": "HuggingFaceTB/SmolLM2-135M", "flash_attention": True, "pad_to_sequence_len": True, - "flash_attn_fuse_qkv": True, "flash_attn_fuse_mlp": True, "sample_packing": True, "sequence_len": 1024, From ba3dba3e4f6fbe845b0249f517c3bff88d898e22 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 6 Aug 2025 09:47:55 -0400 Subject: [PATCH 0889/1405] add kernels for gpt oss models (#3020) * add kernels for gpt oss models * add support for gpt-oss * typo incorrect package * fix: layout for configs and added wandb/epochs * add gptoss example w offload and set moe leaf for z3 * add support for Mxfp4Config from yaml * update yaml to use official model * fix lora and don't allow triton to go above 3.3.1 * fix lr and tweak vram use * fix range for triton since pinned wasn't compatible with toch 2.6.0 * update cce with gpt oss patches --------- Co-authored-by: NanoCode012 --- .../colab-axolotl-example.ipynb | 2 +- examples/gpt-oss/README.md | 9 +++ .../gpt-oss-20b-fft-fsdp2-offload.yaml | 62 ++++++++++++++++++ examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml | 62 ++++++++++++++++++ .../gpt-oss-20b-sft-lora-singlegpu.yaml | 64 +++++++++++++++++++ requirements.txt | 4 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/common/architectures.py | 1 + src/axolotl/core/trainers/base.py | 6 +- .../integrations/cut_cross_entropy/README.md | 2 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/loaders/model.py | 19 +++++- src/axolotl/utils/schemas/config.py | 7 ++ src/axolotl/utils/schemas/model.py | 16 +++++ src/axolotl/utils/schemas/validation.py | 10 +++ 15 files changed, 257 insertions(+), 11 deletions(-) create mode 100644 examples/gpt-oss/README.md create mode 100644 examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml create mode 100644 examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml create mode 100644 examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 6c6e21f94a..c283092bee 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@cbd58e0\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@48b5169\"" ] }, { diff --git a/examples/gpt-oss/README.md b/examples/gpt-oss/README.md new file mode 100644 index 0000000000..7157806afe --- /dev/null +++ b/examples/gpt-oss/README.md @@ -0,0 +1,9 @@ +# OpenAI's GPT-OSS + +GPT-OSS is a 20 billion parameter MoE model trained by OpenAI, released in August 2025. + +- 20B Full Parameter SFT can be trained on 8x48GB GPUs (peak reserved memory @ ~36GiB/GPU) - [YAML](./gpt-oss-20b-fft-fsdp2.yaml) +- 20B LoRA SFT (all linear layers, and experts in last two layers) can be trained a single GPU (peak reserved memory @ ~47GiB) + - removing the experts from `lora_target_parameters` will allow the model to fit around ~44GiB of VRAM + - [YAML](./gpt-oss-20b-sft-lora-singlegpu.yaml) +- 20B Full Parameter SFT with FSDP2 offloading can be trained on 2x24GB GPUs (peak reserved memory @ ~21GiB/GPU) - [YAML](./gpt-oss-20b-fft-fsdp2-offload.yaml) diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml new file mode 100644 index 0000000000..d55a272ba7 --- /dev/null +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml @@ -0,0 +1,62 @@ +base_model: openai/gpt-oss-20b +use_kernels: true +model_quantization_config: Mxfp4Config +model_quantization_config_kwargs: + dequantize: true + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +experimental_skip_move_to_device: true # prevent OOM by NOT putting model to GPU before sharding + +datasets: + - path: winglian/pirate-ultrachat-10k + type: chat_template + split: train + +dataset_prepared_path: last_run_prepared +val_set_size: 0 +output_dir: ./outputs/gpt-oss-out/ + +sequence_len: 4096 +sample_packing: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 + +optimizer: adamw_torch_fused # 8bit optimizers do not work with FSDP2 offload +lr_scheduler: constant_with_warmup +learning_rate: 2e-5 + +bf16: true +tf32: true + +flash_attention: true +attn_implementation: kernels-community/vllm-flash-attn3 + +gradient_checkpointing: true +activation_offloading: true + +logging_steps: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 + +special_tokens: +eot_tokens: + - "<|end|>" + +fsdp_version: 2 +fsdp_config: + offload_params: true + state_dict_type: SHARDED_STATE_DICT + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: GptOssDecoderLayer + reshard_after_forward: true diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml new file mode 100644 index 0000000000..f9f2c1dceb --- /dev/null +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml @@ -0,0 +1,62 @@ +base_model: openai/gpt-oss-20b +use_kernels: true +model_quantization_config: Mxfp4Config +model_quantization_config_kwargs: + dequantize: true + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +experimental_skip_move_to_device: true # prevent OOM by NOT putting model to GPU before sharding + +datasets: + - path: winglian/pirate-ultrachat-10k + type: chat_template + split: train + +dataset_prepared_path: last_run_prepared +val_set_size: 0 +output_dir: ./outputs/gpt-oss-out/ + +sequence_len: 4096 +sample_packing: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 + +optimizer: adamw_torch_8bit +lr_scheduler: constant_with_warmup +learning_rate: 2e-5 + +bf16: true +tf32: true + +flash_attention: true +attn_implementation: kernels-community/vllm-flash-attn3 + +gradient_checkpointing: true +activation_offloading: true + +logging_steps: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 + +special_tokens: +eot_tokens: + - "<|end|>" + +fsdp_version: 2 +fsdp_config: + offload_params: false + state_dict_type: SHARDED_STATE_DICT + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: GptOssDecoderLayer + reshard_after_forward: true diff --git a/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml b/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml new file mode 100644 index 0000000000..f7c332dfe8 --- /dev/null +++ b/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml @@ -0,0 +1,64 @@ +base_model: openai/gpt-oss-20b +use_kernels: true +model_quantization_config: Mxfp4Config +model_quantization_config_kwargs: + dequantize: true + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +experimental_skip_move_to_device: true # prevent OOM by not putting model to GPU before sharding + +datasets: + - path: winglian/pirate-ultrachat-10k + type: chat_template + split: train + +dataset_prepared_path: last_run_prepared +val_set_size: 0 +output_dir: ./outputs/gpt-oss-out/ + +sequence_len: 4096 +sample_packing: true + +adapter: lora +lora_r: 8 +lora_alpha: 16 +lora_dropout: 0.0 # dropout not supported when using LoRA over expert parameters +lora_target_linear: true +lora_target_parameters: # target the experts in the last two layers + - "22._checkpoint_wrapped_module.mlp.experts.gate_up_proj" + - "22._checkpoint_wrapped_module.mlp.experts.down_proj" + - "23._checkpoint_wrapped_module.mlp.experts.gate_up_proj" + - "23._checkpoint_wrapped_module.mlp.experts.down_proj" + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 8 +micro_batch_size: 1 +num_epochs: 1 + +optimizer: adamw_torch_8bit +lr_scheduler: constant_with_warmup +learning_rate: 2e-4 + +bf16: true +tf32: true + +flash_attention: true +attn_implementation: kernels-community/vllm-flash-attn3 + +gradient_checkpointing: true +activation_offloading: true + +logging_steps: 1 +saves_per_epoch: 1 +warmup_ratio: 0.1 + +special_tokens: +eot_tokens: + - "<|end|>" diff --git a/requirements.txt b/requirements.txt index 244d1239cc..0103ba9190 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,8 @@ # START section of dependencies that don't install on Darwin/MacOS bitsandbytes==0.46.1 -triton>=3.0.0 +# triton 3.4.0 is not compatible with CCE +triton>=3.0.0,<3.4.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 autoawq==0.2.7.post3 @@ -20,6 +21,7 @@ datasets==4.0.0 deepspeed>=0.17.0 trl==0.20.0 hf_xet==1.1.5 +kernels==0.9.0 optimum==1.16.2 hf_transfer diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index e767494933..cf9ced60c6 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@cbd58e0"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@48b5169"' ) diff --git a/src/axolotl/common/architectures.py b/src/axolotl/common/architectures.py index 2f77b613eb..58d557e7e8 100644 --- a/src/axolotl/common/architectures.py +++ b/src/axolotl/common/architectures.py @@ -13,4 +13,5 @@ "qwen2_moe": "Qwen2MoeSparseMoeBlock", "qwen3_moe": "Qwen3MoeSparseMoeBlock", "deepseek_v2": "DeepseekV2MoE", + "gpt_oss": "GptOssExperts", } diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 617506eb2b..3540fb6a18 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -567,9 +567,9 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: # Add memory usage try: active, allocated, reserved = get_gpu_memory_usage() - logs["memory/max_memory_active(gib)"] = round(active, 2) - logs["memory/max_memory_allocated(gib)"] = round(allocated, 2) - logs["memory/device_memory_reserved(gib)"] = round(reserved, 2) + logs["memory/max_mem_active(gib)"] = round(active, 2) + logs["memory/max_mem_allocated(gib)"] = round(allocated, 2) + logs["memory/device_mem_reserved(gib)"] = round(reserved, 2) except (ValueError, TypeError, FileNotFoundError): pass diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 048559789e..e0ff14db8f 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@cbd58e0" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@48b5169" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index d1419e27e9..24cd7b6a7d 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -34,7 +34,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@cbd58e0"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@48b5169"`' ) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 1b983f7d09..7c9e2d2bc6 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -202,6 +202,8 @@ def _apply_pre_model_load_setup(self): self._set_device_map_config() if self.cfg.revision_of_model: self.model_kwargs["revision"] = self.cfg.revision_of_model + if self.cfg.use_kernels: + self.model_kwargs["use_kernels"] = self.cfg.use_kernels self._set_quantization_config() self._set_attention_config() @@ -565,8 +567,17 @@ def _set_device_map_config(self): def _set_quantization_config(self): """Set up quantization config (bitsandbytes, awq, gptq, etc.)""" - self.model_kwargs["load_in_8bit"] = self.cfg.load_in_8bit - self.model_kwargs["load_in_4bit"] = self.cfg.load_in_4bit + + if self.cfg.model_quantization_config == "Mxfp4Config": + from transformers import Mxfp4Config + + mxfp4_kwargs = {} + if self.cfg.model_quantization_config_kwargs: + mxfp4_kwargs = self.cfg.model_quantization_config_kwargs + self.model_kwargs["quantization_config"] = Mxfp4Config(**mxfp4_kwargs) + else: + self.model_kwargs["load_in_8bit"] = self.cfg.load_in_8bit + self.model_kwargs["load_in_4bit"] = self.cfg.load_in_4bit if self.cfg.gptq: if not hasattr(self.model_config, "quantization_config"): @@ -648,7 +659,9 @@ def _set_quantization_config(self): def _set_attention_config(self): """Sample packing uses custom FA2 patch""" - if self.cfg.flex_attention: + if self.cfg.attn_implementation: + self.model_kwargs["attn_implementation"] = self.cfg.attn_implementation + elif self.cfg.flex_attention: self.model_kwargs["attn_implementation"] = "flex_attention" self.model_config._attn_implementation = ( # pylint: disable=protected-access "flex_attention" diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index beaee57c90..e3de6e37b3 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -544,6 +544,13 @@ class AxolotlInputConfig( eager_attention: bool | None = None + attn_implementation: str | None = Field( + default=None, + json_schema_extra={ + "description": "Specify a custom attention implementation, used mostly for kernels." + }, + ) + unsloth_cross_entropy_loss: bool | None = None unsloth_lora_mlp: bool | None = None unsloth_lora_qkv: bool | None = None diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index eae8dacb6b..eb751bfccf 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -1,5 +1,7 @@ """Pydantic models for model input / output, etc. configuration""" +from typing import Any, Literal + from pydantic import BaseModel, Field, field_validator from axolotl.utils.logging import get_logger @@ -70,6 +72,20 @@ class ModelInputConfig(BaseModel): }, ) + use_kernels: bool | None = Field( + default=None, + json_schema_extra={"description": "Use custom kernels, e.g. MegaBlocks."}, + ) + + model_quantization_config: Literal["Mxfp4Config"] | None = Field( + default=None, + json_schema_extra={"description": "Model loading quantization config"}, + ) + model_quantization_config_kwargs: dict[str, Any] | None = Field( + default=None, + json_schema_extra={"description": "kwargs for model quantization config"}, + ) + @field_validator("trust_remote_code") @classmethod def hint_trust_remote_code(cls, trust_remote_code): diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index e15adf0779..ac3355f745 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -972,6 +972,16 @@ def check_fsdp_deepspeed(cls, data): raise ValueError("deepspeed and fsdp cannot be used together.") return data + @model_validator(mode="before") + @classmethod + def check_model_quantization_config_vs_bnb(cls, data): + if data.get("model_quantization_config"): + if data.get("load_in_8bit") or data.get("load_in_4bit"): + raise ValueError( + "model_quantization_config and load_in_8bit or load_in_4bit cannot be used together." + ) + return data + @model_validator(mode="before") @classmethod def check_npu_config(cls, data): From e442ff22aa5dd4bf48b919a45843f83af0ea1e0e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 6 Aug 2025 14:28:52 -0400 Subject: [PATCH 0890/1405] fix keyerror on load_in_8bit/load_in_4bit access in _set_quantization_config (#3023) * set load_in_8bit/load_in_4bit in _set_quantization_config to prevent keyerror * use dict.get instead --- src/axolotl/loaders/model.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 7c9e2d2bc6..7061e1ff38 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -612,7 +612,9 @@ def _set_quantization_config(self): self.model_kwargs["quantization_config"] = BitsAndBytesConfig( **self.model_config.quantization_config ) - elif self.cfg.adapter == "qlora" and self.model_kwargs["load_in_4bit"]: + elif self.cfg.adapter == "qlora" and self.model_kwargs.get( + "load_in_4bit", False + ): bnb_config = { "load_in_4bit": True, "llm_int8_threshold": 6.0, @@ -638,7 +640,9 @@ def _set_quantization_config(self): self.model_kwargs["quantization_config"] = BitsAndBytesConfig( **bnb_config, ) - elif self.cfg.adapter == "lora" and self.model_kwargs["load_in_8bit"]: + elif self.cfg.adapter == "lora" and self.model_kwargs.get( + "load_in_8bit", False + ): bnb_config = { "load_in_8bit": True, } From d09290f2f4d133b7bc25e015edc7261b8d14c89b Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 6 Aug 2025 20:20:08 -0400 Subject: [PATCH 0891/1405] Lora kernels bias support (#3025) * lora kernels bias support * revert rename * nit * lint, tests * satisfying the rabbit --- src/axolotl/kernels/lora.py | 187 +++++++++++++++--------- src/axolotl/monkeypatch/lora_kernels.py | 12 +- tests/e2e/kernels/test_lora.py | 47 ++++-- 3 files changed, 156 insertions(+), 90 deletions(-) diff --git a/src/axolotl/kernels/lora.py b/src/axolotl/kernels/lora.py index 82ec911072..fb45f2aa7e 100644 --- a/src/axolotl/kernels/lora.py +++ b/src/axolotl/kernels/lora.py @@ -26,6 +26,7 @@ def get_lora_parameters( proj: nn.Module, ) -> tuple[ torch.Tensor, + torch.Tensor | None, QuantState | None, torch.Tensor | None, torch.Tensor | None, @@ -38,17 +39,20 @@ def get_lora_parameters( proj: The projection module to extract parameters from. Returns: - A tuple containing the base weight matrix, quantization state, LoRA A matrix, - LoRA B matrix, and scaling factor. States and matrices may be None if not - available. + A tuple containing the base weights, quantization state, LoRA A and B weights, + scaling factor, and base layer bias. Quant state, weights, and bias may be + `None` if not available. """ # For DPO or disabled adapters base_layer = proj.base_layer if hasattr(proj, "base_layer") else proj W = base_layer.weight + b = base_layer.bias if not hasattr(proj, "disable_adapters") or proj.disable_adapters or proj.merged: quant_state = getattr(W, "quant_state", None) - return W, quant_state, None, None, None + return W, b, quant_state, None, None, None + + quant_state = getattr(W, "quant_state", None) active_adapter = ( proj.active_adapters[0] @@ -72,18 +76,17 @@ def get_lora_parameters( B = linear_B.weight s = proj.scaling[active_adapter] - quant_state = getattr(W, "quant_state", None) - - return W, quant_state, A, B, s + return W, b, quant_state, A, B, s def matmul_lora( X: torch.Tensor, W: torch.Tensor, - W_quant: QuantState, - A: torch.Tensor, - B: torch.Tensor, - s: float, + b: torch.Tensor | None, + W_quant: QuantState | None, + A: torch.Tensor | None, + B: torch.Tensor | None, + s: float | None, out: torch.Tensor | None = None, ) -> torch.Tensor: """ @@ -104,21 +107,23 @@ def matmul_lora( dtype = X.dtype W = dequantize(W.t(), W_quant) + reshape = False if X.dim() == 3: batch, seq_len, _ = X.shape X = X.view(-1, X.shape[-1]) reshape = True - else: - reshape = False out = torch.matmul(X, W, out=out) if W_quant is not None: del W if A is not None: - A, B = A.t().to(dtype), B.t().to(dtype) + A, B = A.t().to(dtype), B.t().to(dtype) # type: ignore[union-attr] out += s * X @ A @ B + if b is not None: + out += b + return out.view(batch, seq_len, -1) if reshape else out @@ -131,17 +136,20 @@ def forward( ctx, X: torch.Tensor, gate_weight: torch.Tensor, - gate_quant: object | None, + gate_bias: torch.Tensor | None, + gate_quant: QuantState | None, gate_A: torch.Tensor | None, gate_B: torch.Tensor | None, gate_scale: float, up_weight: torch.Tensor, - up_quant: object | None, + up_bias: torch.Tensor | None, + up_quant: QuantState | None, up_A: torch.Tensor | None, up_B: torch.Tensor | None, up_scale: float, down_weight: torch.Tensor, - down_quant: object | None, + down_bias: torch.Tensor | None, + down_quant: QuantState | None, down_A: torch.Tensor | None, down_B: torch.Tensor | None, down_scale: float, @@ -156,20 +164,22 @@ def forward( ctx: Autograd context X: Input features gate_weight: Gate projection weight + gate_bias: Gate projection bias gate_quant: Gate quantization state gate_A: Gate LoRA A matrix gate_B: Gate LoRA B matrix gate_scale: Gate LoRA scale - up_weight: Up-projection weight - up_quant: Up-projection quantization state - up_A: Up-projection LoRA A matrix - up_B: Up-projection LoRA B matrix - up_scale: Up-projection LoRA scale - down_weight: Down-projection weight - down_quant: Down-projection quantization state - down_A: Down-projection LoRA A matrix - down_B: Down-projection LoRA B matrix - down_scale: Down-projection LoRA scale + up_weight: Up projection weight + up_quant: Up projection quantization state + up_A: Up projection LoRA A matrix + up_B: Up projection LoRA B matrix + up_scale: Up projection LoRA scale + down_weight: Down projection weight + down_bias: Down projection bias + down_quant: Down projection quantization state + down_A: Down projection LoRA A matrix + down_B: Down projection LoRA B matrix + down_scale: Down projection LoRA scale activation_fn: Forward activation function activation_fn_backward: Backward activation function inplace: Whether to perform operations in-place @@ -178,15 +188,17 @@ def forward( Output transformed by multi-layer perceptron and activation function """ # Compute projections - gate = matmul_lora(X, gate_weight, gate_quant, gate_A, gate_B, gate_scale) - up = matmul_lora(X, up_weight, up_quant, up_A, up_B, up_scale) + gate = matmul_lora( + X, gate_weight, gate_bias, gate_quant, gate_A, gate_B, gate_scale + ) + up = matmul_lora(X, up_weight, up_bias, up_quant, up_A, up_B, up_scale) # Activation hidden = activation_fn(gate, up) # Down projection output = matmul_lora( - hidden, down_weight, down_quant, down_A, down_B, down_scale + hidden, down_weight, down_bias, down_quant, down_A, down_B, down_scale ) # Save for backward @@ -209,22 +221,26 @@ def backward( torch.Tensor | None, None, None, + None, torch.Tensor | None, torch.Tensor | None, None, None, None, + None, torch.Tensor | None, torch.Tensor | None, None, None, None, + None, torch.Tensor | None, torch.Tensor | None, None, None, None, None, + None, ]: """ Performs backward pass computation for LoRA MLP. @@ -236,7 +252,7 @@ def backward( Returns: Tuple containing gradients for all inputs from forward pass: - Input gradient tensor (or `None`) - - `None` for weights/quantization states + - `None` for weights/biases/quantization states - LoRA A/B matrix gradients (or `None`) - `None` for scaling factors - `None` for activation functions and flags @@ -279,9 +295,10 @@ def backward( dtype = X.dtype # Down projection - DW = matmul_lora( + grad_down = matmul_lora( grad_output, down_weight.t(), + None, down_quant, down_B, down_A, @@ -289,7 +306,7 @@ def backward( ) # Activation backward - h, grad_gate, grad_up = ctx.activation_fn_backward(DW, gate, up) + h, grad_gate, grad_up = ctx.activation_fn_backward(grad_down, gate, up) # Initialize and compute LoRA gradients d_down_A = d_down_B = d_up_A = d_up_B = d_gate_A = d_gate_B = None @@ -329,8 +346,8 @@ def backward( dX += grad_up @ up_B.to(dtype).t() @ (up_scale * up_A.to(dtype).t()) # Gate projection gradients - gate_weight = dequantize(gate_weight.t(), gate_quant) - dX += grad_gate @ gate_weight.t() + gate_weight = dequantize(gate_weight, gate_quant) + dX += grad_gate @ gate_weight del gate_weight if gate_A is not None and gate_B is not None: @@ -348,22 +365,26 @@ def backward( dX, None, None, + None, d_gate_A.t() if d_gate_A is not None else None, d_gate_B.t() if d_gate_B is not None else None, None, None, None, + None, d_up_A.t() if d_up_A is not None else None, d_up_B.t() if d_up_B is not None else None, None, None, None, + None, d_down_A.t() if d_down_A is not None else None, d_down_B.t() if d_down_B is not None else None, None, None, None, None, + None, ) @@ -378,23 +399,26 @@ def apply_lora_mlp_swiglu(self, X: torch.Tensor, inplace: bool = True) -> torch. Returns: Output tensor after applying LoRA-adapted MLP with SwiGLU activation """ - gateW, gateW_quant, gateA, gateB, gateS = get_lora_parameters(self.gate_proj) - upW, upW_quant, upA, upB, upS = get_lora_parameters(self.up_proj) - downW, downW_quant, downA, downB, downS = get_lora_parameters(self.down_proj) + gateW, gateb, gateW_quant, gateA, gateB, gateS = get_lora_parameters(self.gate_proj) + upW, upb, upW_quant, upA, upB, upS = get_lora_parameters(self.up_proj) + downW, downb, downW_quant, downA, downB, downS = get_lora_parameters(self.down_proj) out = LoRA_MLP.apply( X, gateW, + gateb, gateW_quant, gateA, gateB, gateS, upW, + upb, upW_quant, upA, upB, upS, downW, + downb, downW_quant, downA, downB, @@ -418,22 +442,25 @@ def apply_lora_mlp_geglu(self, X: torch.Tensor, inplace: bool = True) -> torch.T Returns: Output tensor after applying LoRA-adapted MLP with GEGLU activation """ - gateW, gateW_quant, gateA, gateB, gateS = get_lora_parameters(self.gate_proj) - upW, upW_quant, upA, upB, upS = get_lora_parameters(self.up_proj) - downW, downW_quant, downA, downB, downS = get_lora_parameters(self.down_proj) + gateW, gateb, gateW_quant, gateA, gateB, gateS = get_lora_parameters(self.gate_proj) + upW, upb, upW_quant, upA, upB, upS = get_lora_parameters(self.up_proj) + downW, downb, downW_quant, downA, downB, downS = get_lora_parameters(self.down_proj) out = LoRA_MLP.apply( X, gateW, + gateb, gateW_quant, gateA, gateB, gateS, upW, + upb, upW_quant, upA, upB, upS, downW, + downb, downW_quant, downA, downB, @@ -460,16 +487,19 @@ def forward( ctx: torch.autograd.function.FunctionCtx, X: torch.Tensor, q_weight: torch.Tensor, + q_bias: torch.Tensor | None, q_quant: QuantState | None, q_A: torch.Tensor | None, q_B: torch.Tensor | None, q_scale: float, k_weight: torch.Tensor, + k_bias: torch.Tensor | None, k_quant: QuantState | None, k_A: torch.Tensor | None, k_B: torch.Tensor | None, k_scale: float, v_weight: torch.Tensor, + v_bias: torch.Tensor | None, v_quant: QuantState | None, v_A: torch.Tensor | None, v_B: torch.Tensor | None, @@ -483,16 +513,19 @@ def forward( ctx: Autograd context X: Input tensor q_weight: Query projection weight + q_bias: Query projection bias q_quant: Query quantization state q_A: Query LoRA A matrix q_B: Query LoRA B matrix q_scale: Query LoRA scale k_weight: Key projection weight + k_bias: Key projection bias k_quant: Key quantization state k_A: Key LoRA A matrix k_B: Key LoRA B matrix k_scale: Key LoRA scale v_weight: Value projection weight + v_bias: Value projection bias v_quant: Value quantization state v_A: Value LoRA A matrix v_B: Value LoRA B matrix @@ -502,20 +535,21 @@ def forward( Returns: Tuple of (Query, Key, Value) projection tensors """ - Q = matmul_lora(X, q_weight, q_quant, q_A, q_B, q_scale) - K = matmul_lora(X, k_weight, k_quant, k_A, k_B, k_scale) - V = matmul_lora(X, v_weight, v_quant, v_A, v_B, v_scale) + Q = matmul_lora(X, q_weight, q_bias, q_quant, q_A, q_B, q_scale) + K = matmul_lora(X, k_weight, k_bias, k_quant, k_A, k_B, k_scale) + V = matmul_lora(X, v_weight, v_bias, v_quant, v_A, v_B, v_scale) ctx.save_for_backward(X, q_A, q_B, k_A, k_B, v_A, v_B) ctx.scales = (q_scale, k_scale, v_scale) ctx.quants = (q_quant, k_quant, v_quant) ctx.weights = (q_weight, k_weight, v_weight) + ctx.biases = (q_bias, k_bias, v_bias) ctx.inplace = inplace return Q, K, V @staticmethod - @torch_amp_custom_fwd + @torch_amp_custom_bwd def backward( ctx: torch.autograd.function.FunctionCtx, q_grad: torch.Tensor, @@ -525,16 +559,19 @@ def backward( torch.Tensor, None, None, + None, torch.Tensor | None, torch.Tensor | None, None, None, None, + None, torch.Tensor | None, torch.Tensor | None, None, None, None, + None, torch.Tensor | None, torch.Tensor | None, None, @@ -622,31 +659,31 @@ def backward( # Transpose gradients if needed if d_A_q is not None: d_A_q = d_A_q.t() - if d_B_q is not None: - d_B_q = d_B_q.t() + d_B_q = d_B_q.t() # type: ignore[union-attr] if d_A_k is not None: d_A_k = d_A_k.t() - if d_B_k is not None: - d_B_k = d_B_k.t() + d_B_k = d_B_k.t() # type: ignore[union-attr] if d_A_v is not None: d_A_v = d_A_v.t() - if d_B_v is not None: - d_B_v = d_B_v.t() + d_B_v = d_B_v.t() # type: ignore[union-attr] return ( grad_X.view(batch, seq_len, -1), None, None, + None, d_A_q, d_B_q, None, None, None, + None, d_A_k, d_B_k, None, None, None, + None, d_A_v, d_B_v, None, @@ -667,22 +704,25 @@ def apply_lora_qkv( Returns: Tuple of (Query, Key, Value) projection tensors """ - QW, QW_quant, QA, QB, QS = get_lora_parameters(self.q_proj) - KW, KW_quant, KA, KB, KS = get_lora_parameters(self.k_proj) - VW, VW_quant, VA, VB, VS = get_lora_parameters(self.v_proj) + QW, Qb, QW_quant, QA, QB, QS = get_lora_parameters(self.q_proj) + KW, Kb, KW_quant, KA, KB, KS = get_lora_parameters(self.k_proj) + VW, Vb, VW_quant, VA, VB, VS = get_lora_parameters(self.v_proj) Q, K, V = LoRA_QKV.apply( X, QW, + Qb, QW_quant, QA, QB, QS, KW, + Kb, KW_quant, KA, KB, KS, VW, + Vb, VW_quant, VA, VB, @@ -702,10 +742,11 @@ def forward( ctx: torch.autograd.function.FunctionCtx, X: torch.Tensor, W: torch.Tensor, + b: torch.Tensor, W_quant: QuantState | None, - A: torch.Tensor | None, - B: torch.Tensor | None, - S: float, + A: torch.Tensor, + B: torch.Tensor, + s: float, ) -> torch.Tensor: """ Forward pass for output projection with LoRA. @@ -714,19 +755,20 @@ def forward( ctx: Autograd context X: Input tensor W: Output projection weight + b: Output projection bias W_quant: Weight quantization state A: LoRA A matrix B: LoRA B matrix - S: LoRA scaling factor + s: LoRA scaling factor Returns: - Output projection tensor + Output projection result """ - XW = matmul_lora(X, W, W_quant, A, B, S) + XW = matmul_lora(X, W, b, W_quant, A, B, s) ctx.custom_saved_tensors = ( W, W_quant, - S, + s, ) ctx.save_for_backward(A, B, X) @@ -741,8 +783,9 @@ def backward( torch.Tensor, None, None, - torch.Tensor | None, - torch.Tensor | None, + None, + torch.Tensor, + torch.Tensor, None, ]: """ @@ -755,7 +798,7 @@ def backward( Returns: Tuple containing gradients for all forward inputs """ - W, W_quant, S = ctx.custom_saved_tensors + W, W_quant, s = ctx.custom_saved_tensors A, B, X = ctx.saved_tensors batch, seq_len, hd = X.shape @@ -765,17 +808,19 @@ def backward( # Weight projection dY_X = X.t() @ dY - d_A = S * dY_X @ B - d_B = S * A @ dY_X + d_A = s * dY_X @ B + d_B = s * A @ dY_X # Get derivative for dX W = dequantize(W.t(), W_quant) dX = dY @ W.t() del W - dX += dY @ B.to(dtype) @ (S * A.to(dtype)) - # W, W_quant, A, B, S - return dX.view(batch, seq_len, hd), None, None, d_A.t(), d_B.t(), None + A, B = A.to(dtype), B.to(dtype) + dX += s * dY @ B @ A + + # W, b, W_quant, A, B, s + return dX.view(batch, seq_len, hd), None, None, None, d_A.t(), d_B.t(), None def apply_lora_o(self, X: torch.Tensor) -> torch.Tensor: @@ -788,7 +833,7 @@ def apply_lora_o(self, X: torch.Tensor) -> torch.Tensor: Returns: Transformed output tensor """ - OW, OW_quant, OA, OB, OS = get_lora_parameters(self.o_proj) - output = LoRA_O.apply(X, OW, OW_quant, OA, OB, OS) + OW, Ob, OW_quant, OA, OB, OS = get_lora_parameters(self.o_proj) + output = LoRA_O.apply(X, OW, Ob, OW_quant, OA, OB, OS) return output diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 48bc10c0ba..b420a965cf 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -390,7 +390,6 @@ def apply_lora_kernel_patches( ] can_patch_qkv = all( hasattr(module, "lora_A") - and getattr(module, "base_layer", module).bias is None and len(getattr(module, "lora_magnitude_vector", []) or []) == 0 for module in layer_modules ) @@ -400,7 +399,8 @@ def apply_lora_kernel_patches( self_attn.apply_qkv = types.MethodType(apply_lora_qkv, self_attn) else: LOG.warning_once( - "Cannot patch some attention QKV projections - requires LoRA adapters with no bias" + "Cannot patch some attention QKV projections - requires LoRA " + "adapters and no lora_magnitude_vector (DoRA)" ) if cfg.lora_o_kernel: # Output patching @@ -409,7 +409,6 @@ def apply_lora_kernel_patches( ] can_patch_o = all( hasattr(module, "lora_A") - and getattr(module, "base_layer", module).bias is None and len(getattr(module, "lora_magnitude_vector", []) or []) == 0 for module in layer_modules ) @@ -418,14 +417,14 @@ def apply_lora_kernel_patches( self_attn.apply_o = types.MethodType(apply_lora_o, self_attn) else: LOG.warning_once( - "Cannot patch some attention output projection - requires LoRA adapters with no bias" + "Cannot patch some attention output projection - requires LoRA " + "adapters and no lora_magnitude_vector (DoRA)" ) for gate_proj, up_proj, down_proj, mlp in find_mlp_in_layer(layer): if cfg.lora_mlp_kernel: # MLP patching can_patch_mlp = all( hasattr(proj, "lora_A") - and getattr(proj, "base_layer", proj).bias is None and len(getattr(proj, "lora_magnitude_vector", []) or []) == 0 for proj in (gate_proj, up_proj, down_proj) ) @@ -435,7 +434,8 @@ def apply_lora_kernel_patches( layer.mlp.forward = types.MethodType(apply_fn, mlp) else: LOG.warning_once( - "Cannot patch some MLP layers - requires LoRA adapters with no bias" + "Cannot patch some MLP layers - requires LoRA adapters and no " + "lora_magnitude_vector (DoRA)" ) LOG.setLevel(original_level) diff --git a/tests/e2e/kernels/test_lora.py b/tests/e2e/kernels/test_lora.py index 5ad186cbfb..cd6131ff1d 100644 --- a/tests/e2e/kernels/test_lora.py +++ b/tests/e2e/kernels/test_lora.py @@ -64,6 +64,7 @@ def sample_tensors(): batch_size, seq_len, hidden_dim, device="cuda", dtype=torch.float16 ), "W": torch.randn(out_dim, hidden_dim, device="cuda", dtype=torch.float16), + "b": torch.randn(out_dim, device="cuda", dtype=torch.float16), "scale": 0.5, "shapes": { "batch": batch_size, @@ -103,23 +104,24 @@ def __init__(self, in_features=64, out_features=128, rank=8): def test_get_lora_parameters(mock_proj): """Tests get_lora_parameters function""" # Test with LoRA enabled - W, _, A, B, s = get_lora_parameters(mock_proj) + W, b, _, A, B, s = get_lora_parameters(mock_proj) assert isinstance(W, torch.Tensor) assert W.shape == (128, 64) + assert b.shape == (128,) assert A.shape == (8, 64) assert B.shape == (128, 8) assert s == 0.5 # Test with LoRA disabled mock_proj.disable_adapters = True - W, _, A, B, s = get_lora_parameters(mock_proj) + W, b, _, A, B, s = get_lora_parameters(mock_proj) assert A is None and B is None and s is None # Test with merged state mock_proj.disable_adapters = False mock_proj.merged = True - W, _, A, B, s = get_lora_parameters(mock_proj) + W, b, _, A, B, s = get_lora_parameters(mock_proj) assert A is None and B is None and s is None @@ -127,6 +129,7 @@ def test_matmul_lora(sample_tensors): """Tests matmul_lora function""" X = sample_tensors["X"] W = sample_tensors["W"] + b = sample_tensors["b"] scale = sample_tensors["scale"] shapes = sample_tensors["shapes"] @@ -138,19 +141,20 @@ def test_matmul_lora(sample_tensors): B = torch.randn(out_dim, rank, device="cuda", dtype=torch.float16) # Test base matmul - out1 = matmul_lora(X, W, None, None, None, None) - expected1 = torch.matmul(X, W.t()) + out1 = matmul_lora(X, W, b, None, None, None, None) + matmul = torch.matmul(X, W.t()) + expected1 = matmul + b assert torch.allclose(out1, expected1, rtol=1e-3) # Test with LoRA - out2 = matmul_lora(X, W, None, A, B, scale) + out2 = matmul_lora(X, W, b, None, A, B, scale) lora_term = scale * torch.matmul(torch.matmul(X, A.t()), B.t()) - expected2 = expected1 + lora_term + expected2 = matmul + lora_term + b assert torch.allclose(out2, expected2, rtol=1e-3) # Test 3D input reshaping X_3d = X.clone() - out3 = matmul_lora(X_3d, W, None, A, B, scale) + out3 = matmul_lora(X_3d, W, b, None, A, B, scale) assert out3.shape == (X.shape[0], X.shape[1], W.shape[0]) @@ -175,16 +179,19 @@ def test_lora_mlp_direct(sample_tensors, activation_forward, activation_backward output = LoRA_MLP.apply( X, gate_proj.weight, + gate_proj.bias, None, # gate_quant None, # gate_A None, # gate_B None, # gate_scale up_proj.weight, + up_proj.bias, None, # up_quant None, # up_A None, # up_B None, # up_scale down_proj.weight, + down_proj.bias, None, # down_quant None, # down_A None, # down_B @@ -243,16 +250,19 @@ def test_lora_mlp_with_adapters( output = LoRA_MLP.apply( X, gate_proj.weight, + gate_proj.bias, None, gate_A, gate_B, scale, up_proj.weight, + up_proj.bias, None, up_A, up_B, scale, down_proj.weight, + down_proj.bias, None, down_A, down_B, @@ -323,6 +333,7 @@ def test_lora_qkv(sample_tensors): X.requires_grad = True # Test without LoRA adapters + # pylint: disable=duplicate-code Q1, K1, V1 = LoRA_QKV.apply( X, q_weight, @@ -330,16 +341,19 @@ def test_lora_qkv(sample_tensors): None, None, None, + None, k_weight, None, None, None, None, + None, v_weight, None, None, None, None, + None, True, ) @@ -356,16 +370,19 @@ def test_lora_qkv(sample_tensors): X, q_weight, None, + None, q_A, q_B, scale, k_weight, None, + None, k_A, k_B, scale, v_weight, None, + None, v_A, v_B, scale, @@ -399,6 +416,7 @@ def test_lora_o(sample_tensors): """Tests LoRA output projection""" X = sample_tensors["X"] W = sample_tensors["W"] + b = sample_tensors["b"] scale = sample_tensors["scale"] shapes = sample_tensors["shapes"] @@ -411,7 +429,7 @@ def test_lora_o(sample_tensors): # Test forward pass X.requires_grad = True - output = LoRA_O.apply(X, W, None, A, B, scale) + output = LoRA_O.apply(X, W, b, None, A, B, scale) assert output.shape == (X.shape[0], X.shape[1], W.shape[0]) @@ -425,6 +443,7 @@ def test_with_quantization(sample_tensors, mock_quantstate): """Tests LoRA with quantized weights""" X = sample_tensors["X"] # [batch, seq, hidden] W = sample_tensors["W"] # [out, hidden] + b = sample_tensors["b"] # [out] scale = 0.5 shapes = sample_tensors["shapes"] @@ -436,13 +455,13 @@ def test_with_quantization(sample_tensors, mock_quantstate): B = torch.randn(out_dim, rank, device="cuda", dtype=torch.float16) # Test matmul with quantization - out = matmul_lora(X, W, mock_quantstate, A, B, scale) + out = matmul_lora(X, W, b, mock_quantstate, A, B, scale) assert out.shape == (X.shape[0], X.shape[1], W.shape[0]) assert not torch.isnan(out).any() # Test with different batch sizes X2 = torch.randn(4, 6, hidden_dim, device="cuda", dtype=torch.float16) - out2 = matmul_lora(X2, W, mock_quantstate, A, B, scale) + out2 = matmul_lora(X2, W, b, mock_quantstate, A, B, scale) assert out2.shape == (4, 6, W.shape[0]) assert not torch.isnan(out2).any() @@ -459,11 +478,12 @@ def test_shapes_and_dimensions(batch, seq, hidden, rank, out): """Tests various input shapes and dimensions""" X = torch.randn(batch, seq, hidden, device="cuda", dtype=torch.float16) W = torch.randn(out, hidden, device="cuda", dtype=torch.float16) + b = torch.randn(out, device="cuda", dtype=torch.float16) A = torch.randn(rank, hidden, device="cuda", dtype=torch.float16) B = torch.randn(out, rank, device="cuda", dtype=torch.float16) scale = 0.5 - result = matmul_lora(X, W, None, A, B, scale) + result = matmul_lora(X, W, b, None, A, B, scale) assert result.shape == (batch, seq, out) @@ -471,6 +491,7 @@ def test_gradient_flow(sample_tensors): """Tests gradient flow through LoRA layers""" X = sample_tensors["X"].clone() W = sample_tensors["W"].clone() + b = sample_tensors["b"].clone() scale = sample_tensors["scale"] shapes = sample_tensors["shapes"] @@ -486,7 +507,7 @@ def test_gradient_flow(sample_tensors): B.requires_grad = True # Forward pass - out = matmul_lora(X, W, None, A, B, scale) + out = matmul_lora(X, W, b, None, A, B, scale) loss = out.sum() # Backward pass From 4bce713b39035f25dcbd22b11edbedae76db917b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 6 Aug 2025 22:49:19 -0400 Subject: [PATCH 0892/1405] allow custom trainer_cls to be defined as a module reference in the YAML (#3024) [skip ci] * allow custom trainer_cls to be defined as a module reference in the YAML * address PR feedback and add test * add tests --- src/axolotl/core/builders/causal.py | 13 ++++++++++ src/axolotl/core/builders/rl.py | 11 +++++++++ src/axolotl/utils/import_helper.py | 28 ++++++++++++++++++++++ src/axolotl/utils/schemas/config.py | 7 ++++++ tests/utils/test_import_helper.py | 37 +++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+) create mode 100644 src/axolotl/utils/import_helper.py create mode 100644 tests/utils/test_import_helper.py diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index b461e90092..db35a24121 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -43,6 +43,7 @@ V2BatchSamplerDataCollatorForSeq2Seq, ) from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator +from axolotl.utils.import_helper import get_cls_from_module_str from axolotl.utils.logging import get_logger LOG = get_logger(__name__) @@ -136,6 +137,18 @@ def _get_trainer_cls(self): return AxolotlRewardTrainer if self.cfg.process_reward_model: return AxolotlPRMTrainer + + if self.cfg.trainer_cls: + # override the trainer cls + try: + trainer_cls = get_cls_from_module_str(self.cfg.trainer_cls) + LOG.debug(f"Using custom trainer class: {self.cfg.trainer_cls}") + return trainer_cls + except (ImportError, AttributeError, ValueError) as e: + raise ValueError( + f"Failed to load custom trainer class '{self.cfg.trainer_cls}': {e}" + ) from e + return AxolotlTrainer def build(self, total_num_steps): diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index 8cc6eeebf9..bc78168074 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -15,6 +15,7 @@ from axolotl.integrations.base import PluginManager from axolotl.loaders.utils import ensure_dtype from axolotl.utils.callbacks.qat import QATCallback +from axolotl.utils.import_helper import get_cls_from_module_str from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import RLType @@ -72,6 +73,16 @@ def _get_trainer_cls(self, trainer_kwargs: dict): else: raise ValueError(f"Unsupported RL: {self.cfg.rl}") + if self.cfg.trainer_cls: + # override the trainer cls + try: + trainer_cls = get_cls_from_module_str(self.cfg.trainer_cls) + LOG.debug(f"Using custom trainer class: {self.cfg.trainer_cls}") + except (ImportError, AttributeError, ValueError) as e: + raise ValueError( + f"Failed to load custom trainer class '{self.cfg.trainer_cls}': {e}" + ) from e + return trainer_cls, trainer_cls_args def _build_training_arguments(self, total_num_steps): diff --git a/src/axolotl/utils/import_helper.py b/src/axolotl/utils/import_helper.py new file mode 100644 index 0000000000..f7d20099c5 --- /dev/null +++ b/src/axolotl/utils/import_helper.py @@ -0,0 +1,28 @@ +""" +Helper for importing modules from strings +""" + +import importlib + + +def get_cls_from_module_str(module_str: str): + # use importlib to dynamically load the reward function from the module + if not isinstance(module_str, str) or not module_str.strip(): + raise ValueError("module_str must be a non-empty string") + + parts = module_str.split(".") + if len(parts) < 2: + raise ValueError(f"Invalid module string format: {module_str}") + + try: + cls_name = parts[-1] + module_path = ".".join(parts[:-1]) + mod = importlib.import_module(module_path) + mod_cls = getattr(mod, cls_name) + return mod_cls + except ImportError as e: + raise ImportError(f"Failed to import module '{module_path}': {e}") from e + except AttributeError as e: + raise AttributeError( + f"Class '{cls_name}' not found in module '{module_path}': {e}" + ) from e diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index e3de6e37b3..21e99c0483 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -110,6 +110,13 @@ class AxolotlInputConfig( }, ) + trainer_cls: str | None = Field( + default=None, + json_schema_extra={ + "description": "module to custom trainer class to use for training" + }, + ) + rl: RLType | None = Field( default=None, json_schema_extra={ diff --git a/tests/utils/test_import_helper.py b/tests/utils/test_import_helper.py new file mode 100644 index 0000000000..e1ab8bec51 --- /dev/null +++ b/tests/utils/test_import_helper.py @@ -0,0 +1,37 @@ +""" +test cases for axolotl.utils.import_helper +""" + +import pytest + +from axolotl.utils.import_helper import get_cls_from_module_str + + +def test_get_cls_from_module_str(): + cls = get_cls_from_module_str("axolotl.core.trainers.base.AxolotlTrainer") + assert cls.__name__ == "AxolotlTrainer" + + +def test_get_cls_from_module_str_empty_string(): + with pytest.raises(ValueError, match="module_str must be a non-empty string"): + get_cls_from_module_str("") + + +def test_get_cls_from_module_str_whitespace_only(): + with pytest.raises(ValueError, match="module_str must be a non-empty string"): + get_cls_from_module_str(" ") + + +def test_get_cls_from_module_str_invalid_format(): + with pytest.raises(ValueError, match="Invalid module string format"): + get_cls_from_module_str("single_part") + + +def test_get_cls_from_module_str_nonexistent_module(): + with pytest.raises(ImportError, match="Failed to import module"): + get_cls_from_module_str("nonexistent.module.Class") + + +def test_get_cls_from_module_str_nonexistent_class(): + with pytest.raises(AttributeError, match="Class 'NonExistentClass' not found"): + get_cls_from_module_str("axolotl.core.trainers.base.NonExistentClass") From 46dfacf255e7092d78fd9c088e7e69d9b9b536d0 Mon Sep 17 00:00:00 2001 From: salman Date: Thu, 7 Aug 2025 10:34:26 +0100 Subject: [PATCH 0893/1405] ND Parallel Doc Nits (#3032) --- docs/nd_parallelism.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/nd_parallelism.qmd b/docs/nd_parallelism.qmd index 8aebab1409..9b3eae890b 100644 --- a/docs/nd_parallelism.qmd +++ b/docs/nd_parallelism.qmd @@ -97,7 +97,7 @@ This matrix describes how different parallelism methods can be combined in Axolo | **HSDP + TP** | >1 | >1 | >1 | 1 | ✅ **3D Parallelism**. A powerful but complex combination. | | **FSDP + CP** | 1 | >1 | 1 | >1 | ✅ **2D Parallelism**. Combines FSDP with context parallelism. | | **FSDP + TP + CP**| 1 | >1 | >1| >1| ✅ **3D Parallelism**. Another advanced combination. | -| DDP + TP/CP | >1 | 1 | >1 | >1 | ❌ **Not Supported**. The `ParallelismConfig` explicitly prevents this, as composing pure DDP with TP/CP without FSDP is inefficient and complex. You should use FSDP instead (`dp_shard_size > 1`). | +| DDP + TP/CP | >1 | 1 | >1 | >1 | ❌ **Not Supported**. The `ParallelismConfig` explicitly prevents this, as composing pure DDP with TP or CP is currently not supported. You should use FSDP + TP/CP instead (`dp_shard_size > 1`). | | Just TP / CP | 1 | 1 | >1 | >1 | ✅ Supported. Useful for inference or when the model fits on one GPU but context is too long. | - `tp_size` refers to `tensor_parallel_size` From 39fbd3b2b558e97c3288b551fd28633d2476898e Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 7 Aug 2025 20:25:37 +0700 Subject: [PATCH 0894/1405] fix: lora kernels for mistral3 (#3027) [skip ci] --- src/axolotl/monkeypatch/lora_kernels.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index b420a965cf..be1e1f2ffb 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -156,6 +156,11 @@ def get_attention_cls_from_config(cfg: DictDefault) -> Type[nn.Module]: return Llama4TextAttention + if model_type == "mistral3": + from transformers.models.mistral.modeling_mistral import MistralAttention + + return MistralAttention + try: # Dynamically import the module and attention class module_path = f"transformers.models.{model_type}.modeling_{model_type}" From 597953bef0245d551b4d23c715d3ae2b2a7dd549 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Thu, 7 Aug 2025 18:55:58 +0530 Subject: [PATCH 0895/1405] clear cache before clean up (#3031) [skip ci] * clear chahe before save_model * chore: lint --------- Co-authored-by: Wing Lian --- src/axolotl/train.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 41f184abc5..a693236d32 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -566,6 +566,10 @@ def train( resume_from_checkpoint = determine_resume_checkpoint(cfg) execute_training(cfg, trainer, resume_from_checkpoint) + # clear cache + if torch.cuda.is_available(): + torch.cuda.empty_cache() + # Save the trained model and cleanup save_trained_model(cfg, trainer, model, safe_serialization) create_model_card(cfg, trainer) From ca796fb56e5bfa02ddd30464cfa5aabba4dd5f66 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 7 Aug 2025 20:26:42 +0700 Subject: [PATCH 0896/1405] feat(doc): update gpt-oss readme (#3029) [skip ci] * feat(doc): update gpt-oss readme * fix: caps * feat: add toolcalling section * feat: add example tool dataset to docs * chore: update --- docs/dataset-formats/conversation.qmd | 3 +- examples/gpt-oss/README.md | 76 ++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 733b9f32c6..d53c685983 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -212,10 +212,11 @@ Instead of passing `tools` via the system prompt, an alternative method would be Tools need to follow [JSON schema](https://json-schema.org/learn/getting-started-step-by-step). ::: +Example config for Llama4: ```yaml chat_template: llama4 datasets: - - path: ... + - path: Nanobit/text-tools-2k-test type: chat_template # field_tools: tools # default is `tools` ``` diff --git a/examples/gpt-oss/README.md b/examples/gpt-oss/README.md index 7157806afe..9ecd2c859d 100644 --- a/examples/gpt-oss/README.md +++ b/examples/gpt-oss/README.md @@ -1,9 +1,71 @@ -# OpenAI's GPT-OSS +# Finetune OpenAI's GPT-OSS with Axolotl -GPT-OSS is a 20 billion parameter MoE model trained by OpenAI, released in August 2025. +[GPT-OSS](https://huggingface.co/collections/openai/gpt-oss-68911959590a1634ba11c7a4) are a family of open-weight MoE models trained by OpenAI, released in August 2025. There are two variants: 20B and 120B. -- 20B Full Parameter SFT can be trained on 8x48GB GPUs (peak reserved memory @ ~36GiB/GPU) - [YAML](./gpt-oss-20b-fft-fsdp2.yaml) -- 20B LoRA SFT (all linear layers, and experts in last two layers) can be trained a single GPU (peak reserved memory @ ~47GiB) - - removing the experts from `lora_target_parameters` will allow the model to fit around ~44GiB of VRAM - - [YAML](./gpt-oss-20b-sft-lora-singlegpu.yaml) -- 20B Full Parameter SFT with FSDP2 offloading can be trained on 2x24GB GPUs (peak reserved memory @ ~21GiB/GPU) - [YAML](./gpt-oss-20b-fft-fsdp2-offload.yaml) +This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as GPT-OSS is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). + + Here is an example of how to install from main for pip: + +```bash +# Ensure you have Pytorch installed (Pytorch 2.6.0 min) +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl + +pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install --no-build-isolation -e '.[flash-attn]' +``` + +2. Choose one of the following configs below for training the 20B model. + +```bash +# LoRA SFT linear layers & 2 experts (1x48GB @ ~47GiB) +# (only linear layers @ ~44GiB) +axolotl train examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml + +# FFT SFT with offloading (2x24GB @ ~21GiB/GPU) +axolotl train examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml + +# FFT SFT (8x48GB @ ~36GiB/GPU or 4x80GB @ ~46GiB/GPU) +axolotl train examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml +``` + +Notes: +- 120B coming soon! +- Memory usage taken from `device_mem_reserved(gib)` from logs. + +### Tool use + +GPT-OSS has a comprehensive tool understanding. Axolotl supports tool calling datasets for Supervised Fine-tuning. + +Here is an example dataset config: +```yaml +datasets: + - path: Nanobit/text-tools-2k-test + type: chat_template +``` + +See [Nanobit/text-tools-2k-test](https://huggingface.co/datasets/Nanobit/text-tools-2k-test) for the sample dataset. + +Refer to [our docs](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#using-tool-use) for more info. + +### TIPS + +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) + +## Related Resources + +- [GPT-OSS Blog](https://openai.com/index/introducing-gpt-oss/) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) From 9d5c95db6f4d883252fdb1183e82d0b354ff76a2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 7 Aug 2025 21:22:15 -0400 Subject: [PATCH 0897/1405] Add support for Accelerate CP, ND examples, and fix for parallel config w fsdp (#3019) * fix for parallelism config from trainer * fix handling of parallelism_config w accelerate * add todo for removal * update to latest axolotl-contribs-mit for optimizer fix too * synchronize training after checkpoint save * dir spelling * use latest accelerate main * fix to not use partial state parallelism_config * more fixeS * use most recent accelerate fix * fix cpu_ram_efficient_loading to meta devices from rank 0 to prevent CPU RAM oom * improve handling of broadcasting fsdp2 state dict * support for openai chat template with thinking key as the reasoning trace * address PR feedback * refactor to remove dependency on PartialState for parallelism config * bump accelerate, gptoss fixes * limit meta fixes to fsdp2 for now * fixes for gpt oss * fixup examples, don't use cpu-ram-efficient-loading for now * remove problematic barrier * patch parallelism config * reorder comparison * device mesh fixes * make pure CP work * lint --- examples/distributed-parallel/README.md | 8 ++ .../llama-3_1-8b-hdsp-tp.yaml | 47 ++++++++ .../qwen3-8b-fsdp-tp-cp.yaml | 46 ++++++++ .../gpt-oss-20b-fft-fsdp2-offload.yaml | 10 +- examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml | 11 +- .../gpt-oss-20b-sft-lora-singlegpu.yaml | 18 +-- requirements.txt | 9 +- src/axolotl/common/architectures.py | 2 +- src/axolotl/core/builders/base.py | 29 +---- src/axolotl/core/builders/causal.py | 2 +- src/axolotl/core/trainers/base.py | 83 ++++++++++++- .../trainers/mixins/distributed_parallel.py | 13 +++ src/axolotl/loaders/model.py | 110 +++++------------- src/axolotl/loaders/patch_manager.py | 8 ++ src/axolotl/monkeypatch/accelerate/fsdp2.py | 35 +++++- .../accelerate/parallelism_config.py | 77 ++++++++++++ src/axolotl/monkeypatch/multipack.py | 1 + .../prompt_strategies/chat_template.py | 15 ++- src/axolotl/train.py | 3 +- src/axolotl/utils/collators/batching.py | 11 ++ .../utils/ctx_managers/sequence_parallel.py | 7 +- src/axolotl/utils/distributed.py | 75 ++++++++++++ src/axolotl/utils/schemas/datasets.py | 12 ++ src/axolotl/utils/schemas/validation.py | 13 +++ src/axolotl/utils/trainer.py | 20 ++++ tests/test_loaders.py | 17 ++- 26 files changed, 534 insertions(+), 148 deletions(-) create mode 100644 examples/distributed-parallel/README.md create mode 100644 examples/distributed-parallel/llama-3_1-8b-hdsp-tp.yaml create mode 100644 examples/distributed-parallel/qwen3-8b-fsdp-tp-cp.yaml create mode 100644 src/axolotl/monkeypatch/accelerate/parallelism_config.py diff --git a/examples/distributed-parallel/README.md b/examples/distributed-parallel/README.md new file mode 100644 index 0000000000..5aff54cd15 --- /dev/null +++ b/examples/distributed-parallel/README.md @@ -0,0 +1,8 @@ +# Distributed Parallel + +See the accompanying blog post: [Accelerate ND-Parallel: A guide to Efficient Multi-GPU Training](https://huggingface.co/blog/accelerate-nd-parallel) + +The examples provided are suitable for single node (8xGPU) SFT. + +- Qwen 3 8B w/ FSDP + TP + CP: [YAML](./qwen3-8b-fsdp-tp-cp.yaml) +- Llama 3.1 8B w/ HSDP + TP: [YAML](./llama-3_1-8b-hdsp-tp.yaml) diff --git a/examples/distributed-parallel/llama-3_1-8b-hdsp-tp.yaml b/examples/distributed-parallel/llama-3_1-8b-hdsp-tp.yaml new file mode 100644 index 0000000000..5b3246f744 --- /dev/null +++ b/examples/distributed-parallel/llama-3_1-8b-hdsp-tp.yaml @@ -0,0 +1,47 @@ +base_model: meta-llama/Llama-3.1-8B + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +dp_shard_size: 2 +dp_replicate_size: 2 +tensor_parallel_size: 2 +# context_parallel_size: 2 + +dataset_prepared_path: last_run_prepared + +special_tokens: + pad_token: <|end_of_text|> + +fsdp_version: 2 +fsdp_config: + offload_params: false + state_dict_type: FULL_STATE_DICT + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: LlamaDecoderLayer + reshard_after_forward: true + +datasets: + - path: tatsu-lab/alpaca + type: alpaca + +output_dir: ./outputs/ndp-out/ + +sequence_len: 2048 +sample_packing: true +flash_attention: true + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 2 +optimizer: adamw_torch_fused +lr_scheduler: constant_with_warmup +learning_rate: 2e-6 + +bf16: true +tf32: true + +logging_steps: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 diff --git a/examples/distributed-parallel/qwen3-8b-fsdp-tp-cp.yaml b/examples/distributed-parallel/qwen3-8b-fsdp-tp-cp.yaml new file mode 100644 index 0000000000..584a33f440 --- /dev/null +++ b/examples/distributed-parallel/qwen3-8b-fsdp-tp-cp.yaml @@ -0,0 +1,46 @@ +base_model: Qwen/Qwen3-8B + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +dp_shard_size: 2 +# dp_replicate_size: 1 +context_parallel_size: 2 +tensor_parallel_size: 2 + +dataset_prepared_path: last_run_prepared + +fsdp_version: 2 +fsdp_config: + offload_params: false + state_dict_type: FULL_STATE_DICT + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Qwen3DecoderLayer + reshard_after_forward: true + +datasets: + - path: tatsu-lab/alpaca + type: alpaca + +output_dir: ./outputs/ndp-out/ + +sequence_len: 8192 +sample_packing: true +flash_attention: true + +gradient_accumulation_steps: 1 +micro_batch_size: 1 # must be 1 when using context parallel +num_epochs: 2 +optimizer: adamw_torch_fused +lr_scheduler: constant_with_warmup +learning_rate: 2e-6 + +bf16: true +tf32: true + +logging_steps: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 + +special_tokens: diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml index d55a272ba7..b861876d12 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml @@ -10,9 +10,10 @@ plugins: experimental_skip_move_to_device: true # prevent OOM by NOT putting model to GPU before sharding datasets: - - path: winglian/pirate-ultrachat-10k + - path: HuggingFaceH4/Multilingual-Thinking type: chat_template - split: train + field_thinking: thinking + template_thinking_key: thinking dataset_prepared_path: last_run_prepared val_set_size: 0 @@ -20,6 +21,7 @@ output_dir: ./outputs/gpt-oss-out/ sequence_len: 4096 sample_packing: true +pad_to_sequence_len: true wandb_project: wandb_entity: @@ -47,11 +49,12 @@ activation_offloading: true logging_steps: 1 saves_per_epoch: 1 -warmup_ratio: 0.1 +warmup_ratio: 0.03 special_tokens: eot_tokens: - "<|end|>" + - "<|return|>" fsdp_version: 2 fsdp_config: @@ -60,3 +63,4 @@ fsdp_config: auto_wrap_policy: TRANSFORMER_BASED_WRAP transformer_layer_cls_to_wrap: GptOssDecoderLayer reshard_after_forward: true +# cpu_ram_efficient_loading: true diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml index f9f2c1dceb..6ec99304ac 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml @@ -1,5 +1,5 @@ base_model: openai/gpt-oss-20b -use_kernels: true +use_kernels: false model_quantization_config: Mxfp4Config model_quantization_config_kwargs: dequantize: true @@ -10,9 +10,10 @@ plugins: experimental_skip_move_to_device: true # prevent OOM by NOT putting model to GPU before sharding datasets: - - path: winglian/pirate-ultrachat-10k + - path: HuggingFaceH4/Multilingual-Thinking type: chat_template - split: train + field_thinking: thinking + template_thinking_key: thinking dataset_prepared_path: last_run_prepared val_set_size: 0 @@ -47,11 +48,12 @@ activation_offloading: true logging_steps: 1 saves_per_epoch: 1 -warmup_ratio: 0.1 +warmup_ratio: 0.03 special_tokens: eot_tokens: - "<|end|>" + - "<|return|>" fsdp_version: 2 fsdp_config: @@ -60,3 +62,4 @@ fsdp_config: auto_wrap_policy: TRANSFORMER_BASED_WRAP transformer_layer_cls_to_wrap: GptOssDecoderLayer reshard_after_forward: true +# cpu_ram_efficient_loading: true diff --git a/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml b/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml index f7c332dfe8..6016ce712b 100644 --- a/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml +++ b/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml @@ -10,9 +10,10 @@ plugins: experimental_skip_move_to_device: true # prevent OOM by not putting model to GPU before sharding datasets: - - path: winglian/pirate-ultrachat-10k + - path: HuggingFaceH4/Multilingual-Thinking type: chat_template - split: train + field_thinking: thinking + template_thinking_key: thinking dataset_prepared_path: last_run_prepared val_set_size: 0 @@ -26,11 +27,13 @@ lora_r: 8 lora_alpha: 16 lora_dropout: 0.0 # dropout not supported when using LoRA over expert parameters lora_target_linear: true -lora_target_parameters: # target the experts in the last two layers - - "22._checkpoint_wrapped_module.mlp.experts.gate_up_proj" - - "22._checkpoint_wrapped_module.mlp.experts.down_proj" - - "23._checkpoint_wrapped_module.mlp.experts.gate_up_proj" - - "23._checkpoint_wrapped_module.mlp.experts.down_proj" + +# TODO: not supported for now, see peft#2710 +#lora_target_parameters: # target the experts in the last two layers +# - "22._checkpoint_wrapped_module.mlp.experts.gate_up_proj" +# - "22._checkpoint_wrapped_module.mlp.experts.down_proj" +# - "23._checkpoint_wrapped_module.mlp.experts.gate_up_proj" +# - "23._checkpoint_wrapped_module.mlp.experts.down_proj" wandb_project: wandb_entity: @@ -62,3 +65,4 @@ warmup_ratio: 0.1 special_tokens: eot_tokens: - "<|end|>" + - "<|return|>" diff --git a/requirements.txt b/requirements.txt index 0103ba9190..370bf5a5ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,17 +16,18 @@ huggingface_hub>=0.33.0 peft==0.17.0 transformers==4.55.0 tokenizers>=0.21.1 -accelerate @ git+https://github.com/huggingface/accelerate.git@9359a0194f210624f1e6e85c3d838fdd55c11152 +accelerate==1.10.0 datasets==4.0.0 deepspeed>=0.17.0 -trl==0.20.0 +trl==0.21.0 hf_xet==1.1.5 kernels==0.9.0 +trackio optimum==1.16.2 hf_transfer sentencepiece -gradio==5.23.3 +gradio==5.41.1 modal==1.0.2 pydantic==2.10.6 @@ -68,6 +69,6 @@ torchao==0.12.0 schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.6 -axolotl-contribs-mit==0.0.4 +axolotl-contribs-mit==0.0.5 mistral-common==1.8.3 diff --git a/src/axolotl/common/architectures.py b/src/axolotl/common/architectures.py index 58d557e7e8..ce945e670f 100644 --- a/src/axolotl/common/architectures.py +++ b/src/axolotl/common/architectures.py @@ -13,5 +13,5 @@ "qwen2_moe": "Qwen2MoeSparseMoeBlock", "qwen3_moe": "Qwen3MoeSparseMoeBlock", "deepseek_v2": "DeepseekV2MoE", - "gpt_oss": "GptOssExperts", + "gpt_oss": "GptOssDecoderLayer", } diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 0472acee94..e1f6497151 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -24,7 +24,6 @@ from typing import Any import torch -from accelerate import PartialState from transformers import ( TrainerCallback, ) @@ -39,6 +38,7 @@ SaveModelOnFirstStepCallback, ) from axolotl.utils.callbacks.profiler import PytorchProfilerCallback +from axolotl.utils.distributed import build_parallelism_config from axolotl.utils.schemas.enums import CustomSupportedOptimizers LOG = logging.getLogger(__name__) @@ -275,8 +275,9 @@ def _configure_custom_optimizer( optimizer_kwargs["dion_lr"] = training_args_kwargs["dion_learning_rate"] optimizer_kwargs["dion_mu"] = training_args_kwargs["dion_momentum"] optimizer_kwargs.update(adam_kwargs) - partial_state = PartialState() - optimizer_kwargs["device_mesh"] = partial_state.device_mesh + _, device_mesh = build_parallelism_config(self.cfg) + if device_mesh is not None: + optimizer_kwargs["device_mesh"] = device_mesh elif self.cfg.optimizer == "optimi_adamw": from optimi import AdamW @@ -428,30 +429,12 @@ def _configure_torch_compile(self, training_args_kwargs: dict): training_args_kwargs["torch_compile_mode"] = self.cfg.torch_compile_mode def _configure_accelerator_config(self, training_args_kwargs: dict): - partial_state = PartialState() - has_pc_attr = ( - hasattr(partial_state, "parallelism_config") - and partial_state.parallelism_config - ) - has_pc_key = ( - "parallelism_config" - in partial_state._shared_state # pylint: disable=protected-access - and partial_state._shared_state[ # pylint: disable=protected-access - "parallelism_config" - ] - ) - use_configured_state = has_pc_attr or has_pc_key if self.cfg.accelerator_config: - use_configured_state = self.cfg.accelerator_config.pop( - "use_configured_state", use_configured_state - ) training_args_kwargs["accelerator_config"] = AcceleratorConfig( - use_configured_state=use_configured_state, **self.cfg.accelerator_config + **self.cfg.accelerator_config ) else: - training_args_kwargs["accelerator_config"] = AcceleratorConfig( - use_configured_state=use_configured_state, - ) + training_args_kwargs["accelerator_config"] = AcceleratorConfig() def _configure_gradient_checkpointing(self, training_args_kwargs: dict): if self.cfg.activation_offloading is True: diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index db35a24121..e5bc217621 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -363,7 +363,7 @@ def build(self, total_num_steps): data_collator_kwargs["pad_to_multiple_of"] = multiple * math.ceil( self.cfg.sequence_len / multiple ) - else: + elif self.cfg.pad_to_sequence_len is None: # A100 is best at 64, while others at 8. Let's use the larger so we don't have to check # https://docs.nvidia.com/deeplearning/performance/dl-performance-matrix-multiplication/index.html data_collator_kwargs["pad_to_multiple_of"] = multiple diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 3540fb6a18..0f9f6e4c4d 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -10,8 +10,11 @@ from typing import Any, Callable, Literal, Optional import datasets +import safetensors import torch +from accelerate.state import AcceleratorState from datasets import Dataset +from peft import PeftModel from torch.utils.data import ( BatchSampler, DataLoader, @@ -19,8 +22,10 @@ Sampler, SequentialSampler, ) -from transformers import Trainer +from transformers import PreTrainedModel, Trainer +from transformers.trainer import TRAINING_ARGS_NAME from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, has_length, seed_worker +from transformers.utils import SAFE_WEIGHTS_NAME, WEIGHTS_NAME, is_peft_available from trl.trainer.utils import pad_to_length from typing_extensions import override @@ -515,7 +520,18 @@ def push_to_hub(self, *args, **kwargs) -> str: @wraps(Trainer.create_accelerator_and_postprocess) def create_accelerator_and_postprocess(self): - res = super().create_accelerator_and_postprocess() + # cleanup the PartialState states so Accelerate automatically configures everything from the env vars + accelerator_config = self.args.accelerator_config.to_dict() + use_configured_state = accelerator_config.get("use_configured_state", False) + if not use_configured_state: + AcceleratorState._reset_state( # pylint: disable=protected-access + reset_partial_state=True + ) + + super().create_accelerator_and_postprocess() + + # now we need to put parallelism_config back on the PartialState since we rely on that info in other places + # PartialState().parallelism_config = self.accelerator.state.parallelism_config if self.is_fsdp_enabled: if ( @@ -524,8 +540,6 @@ def create_accelerator_and_postprocess(self): ): self.accelerator.state.fsdp_plugin.limit_all_gathers = True - return res - # pylint: disable=unused-argument def additional_accelerator_args( self, fp8: bool = False, enable_fsdp_float8_all_gather: bool = False, **kwargs @@ -590,3 +604,64 @@ def _save_checkpoint(self, model, trial, **kwargs): output_dir = os.path.join(run_dir, checkpoint_folder) os.makedirs(output_dir, exist_ok=True) return super()._save_checkpoint(model, trial, **kwargs) + + # TODO(wing): remove once https://github.com/huggingface/transformers/pull/39866/files is merged + def _save(self, output_dir: Optional[str] = None, state_dict=None): + # If we are executing this function, we are the process zero, so we don't check for that. + output_dir = output_dir if output_dir is not None else self.args.output_dir + os.makedirs(output_dir, exist_ok=True) + LOG.info(f"Saving model checkpoint to {output_dir}") + supported_classes = ( + (PreTrainedModel,) + if not is_peft_available() + else (PreTrainedModel, PeftModel) + ) + # Save a trained model and configuration using `save_pretrained()`. + # They can then be reloaded using `from_pretrained()` + if not isinstance(self.model, supported_classes): + if state_dict is None: + state_dict = self.model.state_dict() + if isinstance( + self.accelerator.unwrap_model(self.model, keep_torch_compile=False), + supported_classes, + ): + self.accelerator.unwrap_model( + self.model, keep_torch_compile=False + ).save_pretrained( + output_dir, + state_dict=state_dict, + safe_serialization=self.args.save_safetensors, + ) + else: + LOG.info( + "Trainer.model is not a `PreTrainedModel`, only saving its state dict." + ) + if self.args.save_safetensors: + safetensors.torch.save_file( + state_dict, + os.path.join(output_dir, SAFE_WEIGHTS_NAME), + metadata={"format": "pt"}, + ) + else: + torch.save(state_dict, os.path.join(output_dir, WEIGHTS_NAME)) + else: + self.model.save_pretrained( + output_dir, + state_dict=state_dict, + safe_serialization=self.args.save_safetensors, + is_main_process=self.accelerator.is_main_process, + ) + + if self.processing_class is not None: + self.processing_class.save_pretrained(output_dir) + elif ( + self.data_collator is not None + and hasattr(self.data_collator, "tokenizer") + and self.data_collator.tokenizer is not None + ): + LOG.info( + "Saving Trainer.data_collator.tokenizer by default as Trainer.processing_class is `None`" + ) + self.data_collator.tokenizer.save_pretrained(output_dir) + # Good practice: save your training arguments together with the trained model + torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME)) diff --git a/src/axolotl/core/trainers/mixins/distributed_parallel.py b/src/axolotl/core/trainers/mixins/distributed_parallel.py index d0f0f53dfa..d163e4eb57 100644 --- a/src/axolotl/core/trainers/mixins/distributed_parallel.py +++ b/src/axolotl/core/trainers/mixins/distributed_parallel.py @@ -2,6 +2,7 @@ Mixin for correctly saving fsdp """ +from accelerate import PartialState from transformers import Trainer @@ -18,3 +19,15 @@ def _save(self, output_dir: str | None = None, state_dict=None): ): state_dict = self.accelerator.get_state_dict(self.model) super()._save(output_dir, state_dict=state_dict) + + def create_accelerator_and_postprocess(self): + super().create_accelerator_and_postprocess() + if ( + self.accelerator.distributed_type == "FSDP" + and self.accelerator.state.fsdp_plugin is None + ): + # pylint: disable=protected-access + # handle Context Parallelism without FSDP + self.accelerator.state.distributed_type = "MULTI_GPU" + self.accelerator.state._shared_state["distributed_type"] = "MULTI_GPU" + PartialState().distributed_type = "MULTI_GPU" diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 7061e1ff38..95a56b326c 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -13,7 +13,7 @@ import torch import transformers import transformers.modeling_utils -from accelerate import PartialState, init_empty_weights +from accelerate import init_empty_weights from accelerate.parallelism_config import ParallelismConfig from peft import ( PeftConfig, @@ -22,6 +22,7 @@ PeftModelForCausalLM, prepare_model_for_kbit_training, ) +from torch.distributed import DeviceMesh from transformers import ( AutoModelForCausalLM, AutoModelForVision2Seq, @@ -49,7 +50,11 @@ from axolotl.models.mamba import fix_mamba_attn_for_loss from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.dict import DictDefault -from axolotl.utils.distributed import get_device_count, get_device_type, get_world_size +from axolotl.utils.distributed import ( + build_parallelism_config, + get_device_count, + get_device_type, +) from axolotl.utils.logging import get_logger from axolotl.utils.model_shard_quant import load_sharded_model_quant from axolotl.utils.schemas.enums import RLType @@ -87,6 +92,7 @@ class ModelLoader: use_parallel_config: bool | None = False parallelism_config: ParallelismConfig | None = None + device_mesh: DeviceMesh | None = None def __init__( self, @@ -302,7 +308,10 @@ def _configure_embedding_dtypes(self): ) # Handle DeepSpeed Zero3 - if is_deepspeed_zero3_enabled(): + if ( + is_deepspeed_zero3_enabled() + or os.getenv("ACCELERATE_DEEPSPEED_ZERO_STAGE") == "3" + ): self._set_z3_leaf_modules() # Apply gradient checkpointing if needed @@ -407,85 +416,12 @@ def _apply_post_lora_load_setup(self, skip_move_to_device: bool): gc.collect() torch.cuda.empty_cache() - @staticmethod - def _get_parallel_config_kwargs( - world_size: int, - tensor_parallel_size: int = 1, - context_parallel_size: int = 1, - dp_shard_size: int | None = None, - dp_replicate_size: int | None = None, - is_fsdp: bool = False, - ): - pc_kwargs = {} - remaining_world_size = world_size - - if tensor_parallel_size and tensor_parallel_size > 1: - pc_kwargs["tp_size"] = tensor_parallel_size - remaining_world_size = remaining_world_size // tensor_parallel_size - - if context_parallel_size and context_parallel_size > 1: - pc_kwargs["cp_size"] = context_parallel_size - remaining_world_size = remaining_world_size // context_parallel_size - - if dp_shard_size is None and dp_replicate_size in (None, 1): - if remaining_world_size > 1: - pc_kwargs["dp_shard_size"] = remaining_world_size - remaining_world_size = 1 - - if dp_replicate_size and dp_replicate_size > 1: - pc_kwargs["dp_replicate_size"] = dp_replicate_size - remaining_world_size = remaining_world_size // dp_replicate_size - - if remaining_world_size > 1 and dp_shard_size and dp_shard_size > 1: - if not is_fsdp: - raise ValueError( - "dp_shard_size was configured without a corresponding fsdp_config! " - "Please ensure you have configured FSDP using fsdp_config." - ) - pc_kwargs["dp_shard_size"] = dp_shard_size - remaining_world_size = remaining_world_size // dp_shard_size - if remaining_world_size > 1 and "dp_replicate_size" not in pc_kwargs: - pc_kwargs["dp_replicate_size"] = remaining_world_size - remaining_world_size = 1 - - if remaining_world_size > 1: - if "dp_shard_size" not in pc_kwargs and is_fsdp: - pc_kwargs["dp_shard_size"] = remaining_world_size - remaining_world_size = 1 - - if remaining_world_size > 1: - raise ValueError( - f"The configured parallelisms are incompatible with the current world size ({get_world_size()})!\n" - f"{pc_kwargs}" - ) - - return pc_kwargs - def _set_parallel_config(self): """Set parallelism configuration (DP, FSDP, TP, CP) in PartialState/Accelerator""" - pc_kwargs = ModelLoader._get_parallel_config_kwargs( - get_world_size(), - self.cfg.tensor_parallel_size, - self.cfg.context_parallel_size, - self.cfg.dp_shard_size, - self.cfg.dp_replicate_size, - bool(self.cfg.fsdp or self.cfg.fsdp_config), - ) - - if pc_kwargs: - self.parallelism_config = ParallelismConfig( - **pc_kwargs, - ) - device_mesh = self.parallelism_config.build_device_mesh("cuda") - partial_state = PartialState() - # fmt: off - partial_state._shared_state["parallelism_config"] = ( # pylint: disable=protected-access - self.parallelism_config - ) - partial_state._shared_state["device_mesh"] = ( # pylint: disable=protected-access - device_mesh - ) - # fmt: on + parallelism_config, device_mesh = build_parallelism_config(self.cfg) + if parallelism_config: + self.parallelism_config = parallelism_config + self.device_mesh = device_mesh def _set_auto_model_loader(self): """Set `self.auto_model_loader`. Defaults to `transformers.AutoModelForCausalLM` @@ -738,7 +674,7 @@ def _build_model(self) -> bool: if self.cfg.tensor_parallel_size > 1: self.model_kwargs["tp_size"] = self.cfg.tensor_parallel_size self.model_kwargs["tp_plan"] = "auto" - self.model_kwargs["device_mesh"] = PartialState().device_mesh + self.model_kwargs["device_mesh"] = self.device_mesh if "device_map" in self.model_kwargs: del self.model_kwargs["device_map"] # not compatible with `tp_plan` @@ -754,6 +690,18 @@ def _build_model(self) -> bool: elif self.is_qlora_and_fsdp_enabled: skip_move_to_device = True + if ( + self.cfg.tensor_parallel_size <= 1 + and self.cfg.fsdp_config.cpu_ram_efficient_loading + and self.cfg.fsdp_version == 2 + ): + # setting device_map for TP is not supported + local_rank = int(os.getenv("LOCAL_RANK", "0")) + if local_rank == 0: + self.model_kwargs["device_map"] = "cpu" + else: + self.model_kwargs["device_map"] = "meta" + if ( self.is_qlora_and_fsdp_enabled and self.cfg.fsdp_config.cpu_ram_efficient_loading diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 4273f3ccea..047eb20fd7 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -104,6 +104,14 @@ def _apply_chunked_cross_entropy_patch(self): def _apply_fsdp_patches(self): """Apply patches for FSDP configurations.""" + if self.cfg.context_parallel_size > 1 or ( + self.cfg.fsdp_config and str(self.cfg.fsdp_version) == "2" + ): + from axolotl.monkeypatch.accelerate.parallelism_config import ( + patch_parallelism_config, + ) + + patch_parallelism_config() if self.cfg.fsdp_config and str(self.cfg.fsdp_version) == "2": from axolotl.monkeypatch.accelerate.fsdp2 import patch_accelerate_fsdp2 diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index af262d18fb..efc3882941 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -7,6 +7,7 @@ import sys import torch +import torch.distributed as dist from torch import nn from axolotl.utils.bench import log_gpu_memory_usage @@ -36,25 +37,49 @@ def fsdp2_load_full_state_dict( meta_sharded_sd = model.state_dict() sharded_sd = {} - for param_name, full_tensor in full_sd.items(): - sharded_meta_param = meta_sharded_sd.get(param_name) - full_tensor = full_tensor.to(sharded_meta_param.dtype).to(torch.device("cuda")) + for param_name, sharded_meta_param in meta_sharded_sd.items(): + full_tensor = None + if _accelerator.is_main_process: + full_tensor = full_sd[param_name] + full_tensor = full_tensor.to(sharded_meta_param.dtype) + if hasattr(sharded_meta_param, "device_mesh"): + device_mesh = sharded_meta_param.device_mesh + if _accelerator.is_main_process: + full_tensor = full_tensor.to(device_mesh.device_type) + else: + full_tensor = torch.empty( + sharded_meta_param.size(), + device=device_mesh.device_type, + dtype=sharded_meta_param.dtype, + ) sharded_param = distribute_tensor( full_tensor, - sharded_meta_param.device_mesh, + device_mesh, sharded_meta_param.placements, src_data_rank=0, ) else: - sharded_param = full_tensor + # Non-sharded parameters + if _accelerator.is_main_process: + sharded_param = full_tensor.to(torch.device("cuda")) + else: + # broadcast manually + sharded_param = torch.empty_like( + sharded_meta_param, + device=torch.device("cuda"), + dtype=sharded_meta_param.dtype, + ) + dist.broadcast(sharded_param, src=0) if offload_to_cpu: sharded_param = sharded_param.cpu() sharded_sd[param_name] = nn.Parameter(sharded_param) + del full_tensor full_sd[param_name] = None + model.load_state_dict(sharded_sd, assign=True, strict=True) end_time = time.time() LOG.debug( diff --git a/src/axolotl/monkeypatch/accelerate/parallelism_config.py b/src/axolotl/monkeypatch/accelerate/parallelism_config.py new file mode 100644 index 0000000000..e3cafc87d6 --- /dev/null +++ b/src/axolotl/monkeypatch/accelerate/parallelism_config.py @@ -0,0 +1,77 @@ +""" +workaround to allow parallelism config for pure CP +""" + +# pylint: disable=protected-access +import os +import warnings + +from accelerate import DistributedType + + +def _validate_accelerator(self, accelerator): + _warnings = set() + if not accelerator.multi_device and self.total_size == 1: + # No distributed setup, valid parallelism config + return + + # We need this to ensure DDP works + if self.total_size == 1: + self._set_size("dp_replicate", accelerator.num_processes) + + if self.total_size != accelerator.num_processes: + raise ValueError( + f"ParallelismConfig total_size ({self.total_size}) does not match " + f"num_processes ({accelerator.num_processes}). Please adjust dp_replicate_size/ " + f"dp_shard_size/tp_size/cp_size." + ) + + # allow parallelism config when not using fsdp if using pure context parallelism + allow_parallelism_config = False + + if ( + self.cp_size > 1 # pylint: disable=chained-comparison + and self.dp_shard_size <= 1 + and os.environ.get("ACCELERATE_ALLOW_CP_STANDALONE", "false").lower() == "true" + ): + allow_parallelism_config = True + + if ( + self.total_size > 1 + and not allow_parallelism_config + and not (accelerator.is_fsdp2 or accelerator.multi_device) + ): + raise ValueError( + f"ParallelismConfig is only compatible DistributedType.FSDP (version 2) or DistributedType.Multi{{Device}}, but got {accelerator.distributed_type}." + ) + + for parallelism, size in self._sizes.items(): + if size == 1 and getattr(self, f"{parallelism}_handler", None) is not None: + _warnings.add( + f"ParallelismConfig.{parallelism}_handler is set, but {parallelism}_size is set to 1. This handler will be ignored." + ) + + if _warnings and accelerator.is_main_process: + warnings.warn( + "ParallelismConfig has the following warnings:\n" + "\n".join(_warnings), + UserWarning, + ) + + +def patched_is_fsdp2(self) -> bool: + """ + Patched version of is_fsdp2 that guards against a None fsdp_plugin. + """ + # The new logic checks if fsdp_plugin exists before accessing its attributes + return ( + self.distributed_type == DistributedType.FSDP + and self.fsdp_plugin + and self.fsdp_plugin.fsdp_version == 2 + ) + + +def patch_parallelism_config(): + from accelerate.accelerator import AcceleratorState, ParallelismConfig + + ParallelismConfig._validate_accelerator = _validate_accelerator + AcceleratorState.is_fsdp2 = property(patched_is_fsdp2) diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 9dc04c7b41..5fc5ae856b 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -36,6 +36,7 @@ "glm", "glm4", "smollm3", + "gpt_oss", ] diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 80fe9275e2..8241dd385a 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -41,7 +41,9 @@ def __init__( field_messages: str = "messages", field_system: str = "system", field_tools: str = "tools", + field_thinking: str = "reasoning_content", roles: dict[str, list[str]] | None = None, + template_thinking_key: str | None = "reasoning_content", chat_template_kwargs: dict[str, Any] | None = None, drop_system_message: bool = False, ): @@ -50,8 +52,9 @@ def __init__( message_property_mappings = { "role": "role", "content": "content", - "reasoning_content": "reasoning_content", } + if template_thinking_key and field_thinking: + message_property_mappings[template_thinking_key] = field_thinking if roles: self.roles = {s: t for t, sources in roles.items() for s in sources} @@ -74,10 +77,12 @@ def __init__( self.field_messages = field_messages self.field_system = field_system self.field_tools = field_tools + self.field_thinking = field_thinking self.tokenizer = tokenizer self.processor: ProcessorMixin | None = processor self.chat_template = chat_template self.chat_template_kwargs = chat_template_kwargs or {} + self.template_thinking_key: str = template_thinking_key or "reasoning_content" self.max_length = max_length self.drop_system_message = drop_system_message @@ -742,7 +747,9 @@ def transform_message(self, message: dict) -> dict: # get the thinking content thinking_content = content[t_start_idx + len(tpair[0]) : t_end_idx] - transformed_message["reasoning_content"] = thinking_content.strip() + transformed_message[self.prompter.template_thinking_key] = ( + thinking_content.strip() + ) # take remainder of the content # strip whitespace from beginning of the remainder (thinking tokens) @@ -953,6 +960,10 @@ def __call__( None, ), "field_messages": dataset_config.get("field_messages", "messages"), + "field_thinking": dataset_config.get("field_thinking", "reasoning_content"), + "template_thinking_key": dataset_config.get( + "template_thinking_key", "reasoning_content" + ), "roles": dataset_config.get("roles"), "drop_system_message": dataset_config.get("drop_system_message", False), # we need to add one for detecting sequences with exceeding the `sequence_len` limit. diff --git a/src/axolotl/train.py b/src/axolotl/train.py index a693236d32..e8a2cbabe7 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -218,6 +218,7 @@ def execute_training( ring_attn_func=cfg.ring_attn_func, heads_k_stride=cfg.heads_k_stride, gather_outputs=cfg.rl is RLType.GRPO, + device_mesh=trainer.accelerator.torch_device_mesh, ) ) @@ -274,7 +275,7 @@ def save_trained_model( # final model weights have already been saved by `ReLoRACallback.on_train_end` return - if trainer.is_fsdp_enabled: + if trainer.is_fsdp_enabled or cfg.fsdp_config: if cfg.fsdp_config or cfg.fsdp: if cfg.fsdp_config.final_state_dict_type: state_dict_type = cfg.fsdp_config.final_state_dict_type diff --git a/src/axolotl/utils/collators/batching.py b/src/axolotl/utils/collators/batching.py index 25a871b2ba..55e630fbe4 100644 --- a/src/axolotl/utils/collators/batching.py +++ b/src/axolotl/utils/collators/batching.py @@ -161,6 +161,8 @@ class V2BatchSamplerDataCollatorForSeq2Seq(DataCollatorForSeq2Seq): Collator for multipack specific to the using the BatchSampler """ + squash_position_ids: bool = False + def __call__(self, features, return_tensors=None): if not isinstance(features[0], list): features: List[List[dict]] = [features] @@ -176,6 +178,15 @@ def __call__(self, features, return_tensors=None): if feature in item ] out_features[i][feature] = np.concatenate(arrays) + elif feature == "position_ids" and self.squash_position_ids: + arrays = [ + np.array(item[feature]) for item in features_ if feature in item + ] + # concatenate, get total length and create arange of new total position ids + position_ids = np.concatenate(arrays) + total_length = position_ids.shape[0] + position_ids = np.arange(total_length) + out_features[i][feature] = position_ids else: arrays = [ np.array(item[feature]) for item in features_ if feature in item diff --git a/src/axolotl/utils/ctx_managers/sequence_parallel.py b/src/axolotl/utils/ctx_managers/sequence_parallel.py index 949c76f49c..029d991dd1 100644 --- a/src/axolotl/utils/ctx_managers/sequence_parallel.py +++ b/src/axolotl/utils/ctx_managers/sequence_parallel.py @@ -5,8 +5,8 @@ import torch import torch.distributed as dist -from accelerate import PartialState from torch import nn +from torch.distributed import DeviceMesh from torch.utils.hooks import RemovableHandle from transformers.modeling_outputs import CausalLMOutputWithPast from transformers.utils import ModelOutput @@ -194,6 +194,7 @@ def __init__( ring_attn_func: RingAttnFunc, heads_k_stride: int | None, gather_outputs: bool, + device_mesh: DeviceMesh | None = None, ): self.models = models self.context_parallel_size = context_parallel_size @@ -201,6 +202,7 @@ def __init__( self.ring_attn_func = ring_attn_func self.heads_k_stride = heads_k_stride self.gather_outputs = gather_outputs + self.device_mesh = device_mesh self._register_ring_attn() @@ -240,9 +242,8 @@ def __exit__(self, exc_type, exc_val, exc_tb): def _register_ring_attn(self): # Initialize ring attn for sequence parallelism - partial_state = PartialState() register_ring_attn_from_device_mesh( - device_mesh=partial_state.device_mesh, + device_mesh=self.device_mesh, context_parallel_dim=("cp",), heads_k_stride=self.heads_k_stride, ring_attn_func=self.ring_attn_func, diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index 2192e7b9d1..d2d1075cb0 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -8,6 +8,7 @@ import torch import torch.distributed as dist from accelerate import PartialState +from accelerate.utils import ParallelismConfig from transformers.utils.import_utils import ( is_torch_cuda_available, is_torch_mps_available, @@ -290,3 +291,77 @@ def reduce_and_broadcast(fn1, fn2): # Use compute_and_broadcast to compute the reduced value on the main process # and then broadcast it to all ranks return compute_and_broadcast(lambda: fn2(gathered_values)) + + +def build_parallelism_config(cfg): + pc_kwargs = _get_parallel_config_kwargs( + get_world_size(), + cfg.tensor_parallel_size, + cfg.context_parallel_size, + cfg.dp_shard_size, + cfg.dp_replicate_size, + bool(cfg.fsdp or cfg.fsdp_config), + ) + + if pc_kwargs: + parallelism_config = ParallelismConfig( + **pc_kwargs, + ) + device_mesh = parallelism_config.build_device_mesh("cuda") + + return parallelism_config, device_mesh + return None, None + + +def _get_parallel_config_kwargs( + world_size: int, + tensor_parallel_size: int = 1, + context_parallel_size: int = 1, + dp_shard_size: int | None = None, + dp_replicate_size: int | None = None, + is_fsdp: bool = False, +): + pc_kwargs = {} + remaining_world_size = world_size + + if tensor_parallel_size and tensor_parallel_size > 1: + pc_kwargs["tp_size"] = tensor_parallel_size + remaining_world_size = remaining_world_size // tensor_parallel_size + + if context_parallel_size and context_parallel_size > 1: + pc_kwargs["cp_size"] = context_parallel_size + remaining_world_size = remaining_world_size // context_parallel_size + + if dp_shard_size is None and dp_replicate_size in (None, 1): + if remaining_world_size > 1: + pc_kwargs["dp_shard_size"] = remaining_world_size + remaining_world_size = 1 + + if dp_replicate_size and dp_replicate_size > 1: + pc_kwargs["dp_replicate_size"] = dp_replicate_size + remaining_world_size = remaining_world_size // dp_replicate_size + + if remaining_world_size > 1 and dp_shard_size and dp_shard_size > 1: + if not is_fsdp: + raise ValueError( + "dp_shard_size was configured without a corresponding fsdp_config! " + "Please ensure you have configured FSDP using fsdp_config." + ) + pc_kwargs["dp_shard_size"] = dp_shard_size + remaining_world_size = remaining_world_size // dp_shard_size + if remaining_world_size > 1 and "dp_replicate_size" not in pc_kwargs: + pc_kwargs["dp_replicate_size"] = remaining_world_size + remaining_world_size = 1 + + if remaining_world_size > 1: + if "dp_shard_size" not in pc_kwargs and is_fsdp: + pc_kwargs["dp_shard_size"] = remaining_world_size + remaining_world_size = 1 + + if remaining_world_size > 1: + raise ValueError( + f"The configured parallelisms are incompatible with the current world size ({get_world_size()})!\n" + f"{pc_kwargs}" + ) + + return pc_kwargs diff --git a/src/axolotl/utils/schemas/datasets.py b/src/axolotl/utils/schemas/datasets.py index da8c545bc8..d9c8042d43 100644 --- a/src/axolotl/utils/schemas/datasets.py +++ b/src/axolotl/utils/schemas/datasets.py @@ -118,6 +118,18 @@ class SFTDataset(BaseModel): "description": 'Key containing the tools (default: "tools"). Must be a list[dict] and follow [JSON schema](https://json-schema.org/learn/getting-started-step-by-step).' }, ) + field_thinking: str | None = Field( + default=None, + json_schema_extra={ + "description": 'Key containing the reasoning trace (default: "reasoning_content").' + }, + ) + template_thinking_key: str | None = Field( + default=None, + json_schema_extra={ + "description": "The key the chat template expects that indicates the reasoning trace." + }, + ) # deprecated, use message_property_mappings message_field_role: str | None = None # deprecated, use message_property_mappings diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index ac3355f745..72991c9470 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1147,6 +1147,19 @@ def check_gptq_w_revision(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_gpt_oss_fsdp_loading(cls, data): + if data.get("model_quantization_config", "") == "Mxfp4Config": + if ( + data.get("fsdp_config", {}).get("cpu_ram_efficient_loading", False) + is True + ): + raise ValueError( + "FSDP cpu_ram_efficient_loading is not supported for Mxfp4Config model quantization." + ) + return data + class ComplexValidationMixin: """Complex validation methods that involve multiple systems.""" diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 90ae1a8892..26634cbbe7 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -597,6 +597,25 @@ def setup_fsdp_envs(cfg): os.environ["FSDP_RESHARD_AFTER_FORWARD"] = "true" +def setup_parallelism_envs(cfg): + set_accelerate_parallelism_config = False + if cfg.tensor_parallel_size and cfg.tensor_parallel_size > 1: + set_accelerate_parallelism_config = True + os.environ["PARALLELISM_CONFIG_TP_SIZE"] = str(cfg.tensor_parallel_size) + if cfg.dp_shard_size and cfg.dp_shard_size > 1: + set_accelerate_parallelism_config = True + os.environ["PARALLELISM_CONFIG_DP_SHARD_SIZE"] = str(cfg.dp_shard_size) + if cfg.dp_replicate_size and cfg.dp_replicate_size > 1: + set_accelerate_parallelism_config = True + os.environ["PARALLELISM_CONFIG_DP_REPLICATE_SIZE"] = str(cfg.dp_replicate_size) + if cfg.context_parallel_size and cfg.context_parallel_size > 1: + set_accelerate_parallelism_config = True + os.environ["PARALLELISM_CONFIG_CP_SIZE"] = str(cfg.context_parallel_size) + os.environ["ACCELERATE_ALLOW_CP_STANDALONE"] = "true" + if set_accelerate_parallelism_config: + os.environ["ACCELERATE_USE_PARALLELISM_CONFIG"] = "true" + + def prepare_optim_env(cfg): if not check_cuda_p2p_ib_support(): if os.getenv("NCCL_P2P_DISABLE") is None: @@ -615,6 +634,7 @@ def prepare_optim_env(cfg): stage = deepspeed_config.get("zero_optimization", {}).get("stage", None) setup_deepspeed_env(cfg, stage=stage) + setup_parallelism_envs(cfg) setup_torch_compile_env(cfg) if cfg.fp8: diff --git a/tests/test_loaders.py b/tests/test_loaders.py index def7672b97..d45f41998e 100644 --- a/tests/test_loaders.py +++ b/tests/test_loaders.py @@ -9,6 +9,7 @@ from axolotl.loaders import ModelLoader from axolotl.utils.dict import DictDefault +from axolotl.utils.distributed import _get_parallel_config_kwargs class TestModelsUtils: @@ -193,15 +194,13 @@ def test_get_parallel_config_kwargs( is_fsdp, expected, ): - res = ( - ModelLoader._get_parallel_config_kwargs( # pylint: disable=protected-access - world_size, - tensor_parallel_size, - context_parallel_size, - dp_shard_size, - dp_replicate_size, - is_fsdp, - ) + res = _get_parallel_config_kwargs( # pylint: disable=protected-access + world_size, + tensor_parallel_size, + context_parallel_size, + dp_shard_size, + dp_replicate_size, + is_fsdp, ) if expected[0] > 1: From c5e5aba54755cb1c827004b8ef404bd9b3d3ee4c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 8 Aug 2025 02:30:16 -0400 Subject: [PATCH 0898/1405] Add 2.8.0 base images and uv images (#3034) --- .github/workflows/base.yml | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index e97e734422..160ed7df90 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -54,7 +54,7 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" - cuda: "128" - cuda_version: 12.6.3 + cuda_version: 12.8.1 cudnn_version: "" python_version: "3.11" pytorch: 2.7.1 @@ -64,9 +64,16 @@ jobs: cuda_version: 12.8.1 cudnn_version: "" python_version: "3.11" - pytorch: nightly + pytorch: 2.8.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-base-nightly" + dockerfile: "Dockerfile-base" +# - cuda: "128" +# cuda_version: 12.8.1 +# cudnn_version: "" +# python_version: "3.11" +# pytorch: nightly +# torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" +# dockerfile: "Dockerfile-base-nightly" # # "next" is for release candidates of pytorch # - cuda: "128" # cuda_version: 12.8.1 @@ -122,6 +129,13 @@ jobs: pytorch: 2.6.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" + - cuda: "126" + cuda_version: 12.6.3 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.7.1 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-uv-base" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" @@ -129,6 +143,13 @@ jobs: pytorch: 2.7.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" + - cuda: "128" + cuda_version: 12.8.1 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.8.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-uv-base" steps: - name: Checkout uses: actions/checkout@v4 From 4273d5cf7ed6219b066bb93dcf4155ffefed0b36 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 8 Aug 2025 18:45:36 +0700 Subject: [PATCH 0899/1405] feat: update nd parallelism readme (#3039) Co-authored-by: salman --- docs/nd_parallelism.qmd | 4 ++ examples/distributed-parallel/README.md | 54 +++++++++++++++++-- ...hdsp-tp.yaml => llama-3_1-8b-hsdp-tp.yaml} | 2 +- 3 files changed, 54 insertions(+), 6 deletions(-) rename examples/distributed-parallel/{llama-3_1-8b-hdsp-tp.yaml => llama-3_1-8b-hsdp-tp.yaml} (98%) diff --git a/docs/nd_parallelism.qmd b/docs/nd_parallelism.qmd index 9b3eae890b..435e53e218 100644 --- a/docs/nd_parallelism.qmd +++ b/docs/nd_parallelism.qmd @@ -73,6 +73,10 @@ Note: We recommend FSDP. DeepSpeed is only compatible with `tensor_parallel_size ## Examples +::: {.callout-tip} +See our example configs [here](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/distributed-parallel). +::: + 1. HSDP on 2 nodes with 4 GPUs each (8 GPUs total): - You want FSDP within each node and DDP across nodes. - Set `dp_shard_size: 4` and `dp_replicate_size: 2`. diff --git a/examples/distributed-parallel/README.md b/examples/distributed-parallel/README.md index 5aff54cd15..ad7c48d5fb 100644 --- a/examples/distributed-parallel/README.md +++ b/examples/distributed-parallel/README.md @@ -1,8 +1,52 @@ -# Distributed Parallel +# ND Parallelism Examples -See the accompanying blog post: [Accelerate ND-Parallel: A guide to Efficient Multi-GPU Training](https://huggingface.co/blog/accelerate-nd-parallel) +This directory contains example configurations for training models using ND Parallelism in Axolotl. These examples demonstrate how to compose different parallelism strategies (FSDP, TP, CP, HSDP) for efficient multi-GPU training. -The examples provided are suitable for single node (8xGPU) SFT. +## Quick Start -- Qwen 3 8B w/ FSDP + TP + CP: [YAML](./qwen3-8b-fsdp-tp-cp.yaml) -- Llama 3.1 8B w/ HSDP + TP: [YAML](./llama-3_1-8b-hdsp-tp.yaml) +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Run the command below: + +```bash +# Train Qwen3 8B with FSDP + TP + CP on a single 8-GPU node +axolotl train examples/distributed-parallel/qwen3-8b-fsdp-tp-cp.yaml + +# Train Llama 3.1 8B with HSDP + TP on 2 nodes (16 GPUs total) +axolotl train examples/distributed-parallel/llama-3_1-8b-hsdp-tp.yaml +``` + +## Example Configurations + +### Single Node (8 GPUs) + +**Qwen3 8B with FSDP + TP + CP** ([qwen3-8b-fsdp-tp-cp.yaml](./qwen3-8b-fsdp-tp-cp.yaml)) +- Uses all 3 parallelism dimensions on a single node +- Ideal for: when model weights, activations, and/or context are too large to fit on single GPU + +```yaml +dp_shard_size: 2 # FSDP across 2 GPUs +tensor_parallel_size: 2 # TP across 2 GPUs +context_parallel_size: 2 # CP across 2 GPUs +# Total: 2 × 2 × 2 = 8 GPUs +``` + +### Multi-Node + +**Llama 3.1 8B with HSDP + TP** ([llama-3_1-8b-hsdp-tp.yaml](./llama-3_1-8b-hsdp-tp.yaml)) +- FSDP & TP within nodes, DDP across nodes to minimize inter-node communication +- Ideal for: Scaling to multiple nodes while maintaining training efficiency + +```yaml +dp_shard_size: 4 # FSDP within each 4-GPU group +tensor_parallel_size: 2 # TP within each node +dp_replicate_size: 2 # DDP across 2 groups +# Total: (4 × 2) × 2 = 16 GPUs (2 nodes) +``` + +## Learn More + +- [ND Parallelism Documentation](https://docs.axolotl.ai/docs/nd_parallelism.html) +- [Blog: Accelerate ND-Parallel Guide](https://huggingface.co/blog/accelerate-nd-parallel) +- [Multi-GPU Training Guide](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/distributed-parallel/llama-3_1-8b-hdsp-tp.yaml b/examples/distributed-parallel/llama-3_1-8b-hsdp-tp.yaml similarity index 98% rename from examples/distributed-parallel/llama-3_1-8b-hdsp-tp.yaml rename to examples/distributed-parallel/llama-3_1-8b-hsdp-tp.yaml index 5b3246f744..f10dc9bd29 100644 --- a/examples/distributed-parallel/llama-3_1-8b-hdsp-tp.yaml +++ b/examples/distributed-parallel/llama-3_1-8b-hsdp-tp.yaml @@ -3,7 +3,7 @@ base_model: meta-llama/Llama-3.1-8B plugins: - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin -dp_shard_size: 2 +dp_shard_size: 4 dp_replicate_size: 2 tensor_parallel_size: 2 # context_parallel_size: 2 From 4db7f023c6b6db547f0edcadef5fb5dd8578b80b Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 8 Aug 2025 19:00:26 +0700 Subject: [PATCH 0900/1405] feat(doc): standardize the axolotl install to a release (#3040) [skip ci] --- examples/devstral/README.md | 11 ++++------- examples/gemma3n/README.md | 11 ++++------- examples/gpt-oss/README.md | 9 +++------ examples/magistral/README.md | 9 +++------ examples/voxtral/README.md | 9 +++------ 5 files changed, 17 insertions(+), 32 deletions(-) diff --git a/examples/devstral/README.md b/examples/devstral/README.md index 1cf2e2cec1..b53635a8fe 100644 --- a/examples/devstral/README.md +++ b/examples/devstral/README.md @@ -10,17 +10,14 @@ Thanks to the team at MistralAI for giving us early access to prepare for this r ## Getting started -1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Devstral is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). - Here is an example of how to install from main for pip: + Here is an example of how to install from pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0+) -git clone https://github.com/axolotl-ai-cloud/axolotl.git -cd axolotl - +# Ensure you have Pytorch installed (Pytorch 2.6.0 min) pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn]' +pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` 2. Run the finetuning example: diff --git a/examples/gemma3n/README.md b/examples/gemma3n/README.md index d570c92f71..8c4e02a1d8 100644 --- a/examples/gemma3n/README.md +++ b/examples/gemma3n/README.md @@ -4,17 +4,14 @@ Gemma-3n is a family of multimodal models from Google found on [HuggingFace](htt ## Getting started -1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Gemma3n is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). - Here is an example of how to install from main for pip: + Here is an example of how to install from pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0 min recommended) -git clone https://github.com/axolotl-ai-cloud/axolotl.git -cd axolotl - +# Ensure you have Pytorch installed (Pytorch 2.6.0 min) pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn]' +pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` 2. In addition to Axolotl's requirements, Gemma-3n requires: diff --git a/examples/gpt-oss/README.md b/examples/gpt-oss/README.md index 9ecd2c859d..8a19959e7a 100644 --- a/examples/gpt-oss/README.md +++ b/examples/gpt-oss/README.md @@ -6,17 +6,14 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations ## Getting started -1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as GPT-OSS is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). - Here is an example of how to install from main for pip: + Here is an example of how to install from pip: ```bash # Ensure you have Pytorch installed (Pytorch 2.6.0 min) -git clone https://github.com/axolotl-ai-cloud/axolotl.git -cd axolotl - pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn]' +pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` 2. Choose one of the following configs below for training the 20B model. diff --git a/examples/magistral/README.md b/examples/magistral/README.md index 865f872d91..48ce712da1 100644 --- a/examples/magistral/README.md +++ b/examples/magistral/README.md @@ -8,17 +8,14 @@ Thanks to the team at MistralAI for giving us early access to prepare for this r ## Getting started -1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Magistral is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). - Here is an example of how to install from main for pip: + Here is an example of how to install from pip: ```bash # Ensure you have Pytorch installed (Pytorch 2.6.0 min) -git clone https://github.com/axolotl-ai-cloud/axolotl.git -cd axolotl - pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn]' +pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` 2. Run the finetuning example: diff --git a/examples/voxtral/README.md b/examples/voxtral/README.md index 669ebbe55b..f31e9cfd04 100644 --- a/examples/voxtral/README.md +++ b/examples/voxtral/README.md @@ -6,17 +6,14 @@ Thanks to the team at MistralAI for giving us early access to prepare for this r ## Getting started -1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Voxtral is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). - Here is an example of how to install from main for pip: + Here is an example of how to install from pip: ```bash # Ensure you have Pytorch installed (Pytorch 2.6.0 min) -git clone https://github.com/axolotl-ai-cloud/axolotl.git -cd axolotl - pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn]' +pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` 2. Please install the below. From eb2c87b525fd6767e3e09c0e6e6d4612f902263d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 8 Aug 2025 08:02:03 -0400 Subject: [PATCH 0901/1405] Example for Slurm and various fixes (#3038) [skip ci] * slurm example and make preprocess play nicely * start slurm if it init file exists * remove incorrect comment * feat: add slurm docs --------- Co-authored-by: NanoCode012 --- examples/slurm/README.md | 66 ++++++++++++++++++++++++++++++++ examples/slurm/axolotl.slurm | 20 ++++++++++ scripts/cloud-entrypoint.sh | 8 ++++ src/axolotl/utils/data/sft.py | 4 ++ src/axolotl/utils/distributed.py | 5 ++- 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 examples/slurm/README.md create mode 100644 examples/slurm/axolotl.slurm diff --git a/examples/slurm/README.md b/examples/slurm/README.md new file mode 100644 index 0000000000..4c116b7132 --- /dev/null +++ b/examples/slurm/README.md @@ -0,0 +1,66 @@ +# SLURM Multi-Node Training + +This directory contains an example SLURM script for running Axolotl training jobs across multiple nodes in a SLURM cluster. + +## Prerequisites + +- Access to a SLURM cluster with GPU nodes +- Axolotl installed on all nodes (see [installation docs](https://docs.axolotl.ai/docs/installation.html)) + +## Usage + +### Standard SLURM Clusters + +1. Copy [`axolotl.slurm`](./axolotl.slurm) to your working directory. +2. Place your Axolotl config file (`train.yaml`) in the same directory. +3. Set the appropriate environment variables for the job: + ```bash + export HF_TOKEN="your-huggingface-token" + + # metric tracking + # export WANDB_API_KEY="your-wandb-api-key" + # ... + ``` +4. Submit the job: + ```bash + sbatch --export=ALL,NUM_NODES=2,NUM_TRAINERS=8,PRIMARY_ADDR=,PRIMARY_PORT=29400 axolotl.slurm + ``` + + Where: + - `NUM_NODES`: Number of nodes to use + - `NUM_TRAINERS`: GPUs per node (typically 8) + - `PRIMARY_ADDR`: Hostname/IP of the master node + - `PRIMARY_PORT`: Port for distributed training (default: 29400) + +5. (Optional) Run other slurm commands: + ```bash + # check job info + scontrol show job axolotl-cli + + # check job queue + squeue + + # check cluster status + sinfo + ``` + +### RunPod Instant Clusters + +Axolotl works with RunPod Instant Clusters. This feature provides managed SLURM clusters with zero configuration. + +1. **Deploy a SLURM Cluster**: + - Go to [RunPod Instant Clusters](https://console.runpod.io/cluster) + - Click "Create a Cluster" + - Choose your GPU type, node count, and region + - Choose an [Axolotl cloud docker image](https://docs.axolotl.ai/docs/docker.html#cloud) + - Deploy the cluster + +2. **Connect to the Controller Node**: Find the controller node in the RunPod console and connect via SSH + +3. **Follow the instructions in [Standard SLURM Clusters](#standard-slurm-clusters)** + +## Additional Resources + +- [Axolotl Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [SLURM Documentation](https://slurm.schedmd.com/documentation.html) +- [RunPod SLURM Clusters Guide](https://docs.runpod.io/instant-clusters/slurm-clusters) diff --git a/examples/slurm/axolotl.slurm b/examples/slurm/axolotl.slurm new file mode 100644 index 0000000000..741d68ced2 --- /dev/null +++ b/examples/slurm/axolotl.slurm @@ -0,0 +1,20 @@ +#!/bin/bash +# Prior to running this script, export your HF_TOKEN and WANDB_API_KEY to your environment; i.e. +# export HF_TOKEN="..." +# export WANDB_API_KEY="..." +# + +# ---------- SBATCH commands ---------- # +#SBATCH --job-name=axolotl-slurm-multinode +#SBATCH --ntasks-per-node=1 +#SBATCH --nodes=$NUM_NODES +#SBATCH --gpus-per-task=8 +#SBATCH --cpus-per-task=128 + +export TORCH_DIST_INIT_BARRIER=0 + +srun axolotl preprocess train.yaml + +srun axolotl train train.yaml --launcher torchrun -- \ + --nproc_per_node=$NUM_TRAINERS --nnodes=$NUM_NODES \ + --rdzv_id axolotl-cli --rdzv_backend c10d --rdzv_endpoint "${PRIMARY_ADDR}:${PRIMARY_PORT}" --rdzv-conf="join_timeout=1800" diff --git a/scripts/cloud-entrypoint.sh b/scripts/cloud-entrypoint.sh index a5505e9add..c98e7c0d0b 100755 --- a/scripts/cloud-entrypoint.sh +++ b/scripts/cloud-entrypoint.sh @@ -81,5 +81,13 @@ if [ ! -L "/workspace/axolotl/outputs" ]; then ln -sf /workspace/data/axolotl-artifacts /workspace/axolotl/outputs fi +# start the runpod slurm init +SLURM_INIT="${SLURM_INIT:-/slurm-init.sh}" + +if [[ -f "$SLURM_INIT" ]]; then + echo "[entrypoint] running $SLURM_INIT..." + bash "$SLURM_INIT" +fi + # Execute the passed arguments (CMD) exec "$@" diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 3189b29c36..975f26e715 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -1,6 +1,7 @@ """Data handling specific to SFT.""" import functools +import os import tempfile from typing import Literal @@ -104,6 +105,9 @@ def _load_datasets(): finally: loader.cleanup() + if os.environ.get("AXOLOTL_IS_PREPROCESS") == "1": + return train_dataset, eval_dataset, -1, prompters + # Validate sample packing configuration for evaluation if eval_dataset and cfg.sample_packing and cfg.eval_sample_packing is not False: total_eval_steps = calculate_total_num_steps(cfg, eval_dataset, update=False) diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index d2d1075cb0..48771fd973 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -51,7 +51,10 @@ def init_distributed_state(): global distributed_state # pylint: disable=global-statement if distributed_state is None: timeout = int(os.environ.get("AXOLOTL_NCCL_TIMEOUT", 1800)) - distributed_state = PartialState(timeout=timedelta(seconds=timeout)) + try: + distributed_state = PartialState(timeout=timedelta(seconds=timeout)) + except ValueError: + pass def get_distributed_state() -> PartialState | None: From 50f2b94d50584fce89bba9ff33b7c3909e827991 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 8 Aug 2025 08:04:56 -0400 Subject: [PATCH 0902/1405] add 120b and deepspeed zero3 examples (#3035) [skip ci] * add 120b and deepspeed zero3 examples * add a bit of flavor and cleanup gpt oss readme * fix: remove expert vram usage * fix: remove redundant EOS token from eot_tokens * feat: add 120B to docs --------- Co-authored-by: NanoCode012 --- examples/gpt-oss/README.md | 18 +++-- .../gpt-oss-120b-fft-fsdp2-offload.yaml | 67 +++++++++++++++++++ .../gpt-oss-20b-fft-deepspeed-zero3.yaml | 58 ++++++++++++++++ .../gpt-oss-20b-fft-fsdp2-offload.yaml | 6 +- examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml | 1 - .../gpt-oss-20b-sft-lora-singlegpu.yaml | 1 - 6 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml create mode 100644 examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml diff --git a/examples/gpt-oss/README.md b/examples/gpt-oss/README.md index 8a19959e7a..6dadb82301 100644 --- a/examples/gpt-oss/README.md +++ b/examples/gpt-oss/README.md @@ -16,11 +16,10 @@ pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` -2. Choose one of the following configs below for training the 20B model. +2. Choose one of the following configs below for training the 20B model. (for 120B, see [below](#training-120b)) ```bash -# LoRA SFT linear layers & 2 experts (1x48GB @ ~47GiB) -# (only linear layers @ ~44GiB) +# LoRA SFT linear layers (1x48GB @ ~44GiB) axolotl train examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml # FFT SFT with offloading (2x24GB @ ~21GiB/GPU) @@ -30,9 +29,16 @@ axolotl train examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml axolotl train examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml ``` -Notes: -- 120B coming soon! -- Memory usage taken from `device_mem_reserved(gib)` from logs. +Note: Memory usage taken from `device_mem_reserved(gib)` from logs. + +### Training 120B + +On 8xH100s + +```bash +# FFT SFT with offloading (8x80GB @ ~49GiB/GPU) +axolotl train examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml +``` ### Tool use diff --git a/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml b/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml new file mode 100644 index 0000000000..4a9d51fdfd --- /dev/null +++ b/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml @@ -0,0 +1,67 @@ +# the original mxfp4 quantized model is not supported with FSDP cpu_ram_efficient_loading +# FSDP cpu_ram_efficient_loading is used to reduce the initial CPU memory usage when loading the model +base_model: axolotl-ai-co/gpt-oss-120b-dequantized + +use_kernels: false + +dp_shard_size: 16 # requires 2x8xH100 nodes + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +experimental_skip_move_to_device: true # prevent OOM by NOT putting model to GPU before sharding + +datasets: + - path: HuggingFaceH4/Multilingual-Thinking + type: chat_template + field_thinking: thinking + template_thinking_key: thinking + +dataset_prepared_path: last_run_prepared +val_set_size: 0 +output_dir: ./outputs/gpt-oss-out/ + +sequence_len: 4096 +sample_packing: true +pad_to_sequence_len: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 + +optimizer: adamw_torch_fused # 8bit optimizers do not work with FSDP2 offload +lr_scheduler: constant_with_warmup +learning_rate: 2e-5 + +bf16: true +tf32: true + +flash_attention: true +attn_implementation: kernels-community/vllm-flash-attn3 + +gradient_checkpointing: true +activation_offloading: true + +logging_steps: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.03 + +special_tokens: +eot_tokens: + - "<|end|>" + +fsdp_version: 2 +fsdp_config: + offload_params: true + state_dict_type: SHARDED_STATE_DICT + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: GptOssDecoderLayer + reshard_after_forward: true + cpu_ram_efficient_loading: true diff --git a/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml b/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml new file mode 100644 index 0000000000..440f0c509a --- /dev/null +++ b/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml @@ -0,0 +1,58 @@ +base_model: openai/gpt-oss-20b +use_kernels: false +model_quantization_config: Mxfp4Config +model_quantization_config_kwargs: + dequantize: true + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +experimental_skip_move_to_device: true # prevent OOM by NOT putting model to GPU before sharding + +datasets: + - path: HuggingFaceH4/Multilingual-Thinking + type: chat_template + field_thinking: thinking + template_thinking_key: thinking + +dataset_prepared_path: last_run_prepared +val_set_size: 0 +output_dir: ./outputs/gpt-oss-out/ + +sequence_len: 4096 +sample_packing: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 + +optimizer: adamw_torch_8bit +lr_scheduler: constant_with_warmup +learning_rate: 2e-5 + +bf16: true +tf32: true + +flash_attention: true +attn_implementation: kernels-community/vllm-flash-attn3 + +gradient_checkpointing: true +activation_offloading: true + +logging_steps: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.03 + +special_tokens: +eot_tokens: + - "<|end|>" + +# choose the zero3 configuration that best fits your system capabilities +deepspeed: deepspeed_configs/zero3_bf16.json diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml index b861876d12..a6ba83433c 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml @@ -54,7 +54,6 @@ warmup_ratio: 0.03 special_tokens: eot_tokens: - "<|end|>" - - "<|return|>" fsdp_version: 2 fsdp_config: @@ -63,4 +62,7 @@ fsdp_config: auto_wrap_policy: TRANSFORMER_BASED_WRAP transformer_layer_cls_to_wrap: GptOssDecoderLayer reshard_after_forward: true -# cpu_ram_efficient_loading: true + # cpu_ram_efficient_loading: true + +# cpu_ram_efficient_loading cannot be used with MXFP4 model quantization. +# It can only be used with a dequantized model like `axolotl-ai-co/gpt-oss-120b-dequantized` diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml index 6ec99304ac..aa658c863b 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml @@ -53,7 +53,6 @@ warmup_ratio: 0.03 special_tokens: eot_tokens: - "<|end|>" - - "<|return|>" fsdp_version: 2 fsdp_config: diff --git a/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml b/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml index 6016ce712b..c4e1a982d8 100644 --- a/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml +++ b/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml @@ -65,4 +65,3 @@ warmup_ratio: 0.1 special_tokens: eot_tokens: - "<|end|>" - - "<|return|>" From 2974670bf849a7f51534a5f646d717ad609ef9b2 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 8 Aug 2025 19:09:11 +0700 Subject: [PATCH 0903/1405] Feat: add arcee (#3028) * feat: add arcee * feat: add latest models supported by cce * feat: add arcee example config * chore: lint * fix: typo * feat: change to instruct * feat: add vram usage * Update README.md --- examples/arcee/README.md | 53 +++++++++++++++ examples/arcee/afm-4.5b-qlora.yaml | 64 +++++++++++++++++++ .../colab-axolotl-example.ipynb | 2 +- .../magistral/magistral-small-fsdp-qlora.yaml | 1 - examples/magistral/magistral-small-qlora.yaml | 1 - .../magistral-small-think-qlora.yaml | 1 - scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 7 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/monkeypatch/multipack.py | 1 + 10 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 examples/arcee/README.md create mode 100644 examples/arcee/afm-4.5b-qlora.yaml diff --git a/examples/arcee/README.md b/examples/arcee/README.md new file mode 100644 index 0000000000..2178933065 --- /dev/null +++ b/examples/arcee/README.md @@ -0,0 +1,53 @@ +# Finetune ArceeAI's AFM with Axolotl + +[Arcee Foundation Models (AFM)](https://huggingface.co/collections/arcee-ai/afm-45b-68823397c351603014963473) are a family of 4.5B parameter open weight models trained by Arcee.ai. + +This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +Thanks to the team at Arcee.ai for using Axolotl in supervised fine-tuning the AFM model. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as AFM is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). + + Here is an example of how to install from main for pip: + +```bash +# Ensure you have Pytorch installed (Pytorch 2.6.0 min) +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl + +pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install --no-build-isolation -e '.[flash-attn]' +``` + +2. Run the finetuning example: + +```bash +axolotl train examples/arcee/afm-4.5b-qlora.yaml +``` + +This config uses about 7.8GiB VRAM. + +Let us know how it goes. Happy finetuning! 🚀 + +### TIPS + +- For inference, the official Arcee.ai team recommends `top_p: 0.95`, `temperature: 0.5`, `top_k: 50`, and `repeat_penalty: 1.1`. +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) + +## Related Resources + +- [AFM Blog](https://docs.arcee.ai/arcee-foundation-models/introduction-to-arcee-foundation-models) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/arcee/afm-4.5b-qlora.yaml b/examples/arcee/afm-4.5b-qlora.yaml new file mode 100644 index 0000000000..2cb42cacda --- /dev/null +++ b/examples/arcee/afm-4.5b-qlora.yaml @@ -0,0 +1,64 @@ +base_model: arcee-ai/AFM-4.5B + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index c283092bee..d79c2fb09e 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@48b5169\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@bb8d9f8\"" ] }, { diff --git a/examples/magistral/magistral-small-fsdp-qlora.yaml b/examples/magistral/magistral-small-fsdp-qlora.yaml index 14a7ee2192..d46c49fe05 100644 --- a/examples/magistral/magistral-small-fsdp-qlora.yaml +++ b/examples/magistral/magistral-small-fsdp-qlora.yaml @@ -27,7 +27,6 @@ sequence_len: 2048 sample_packing: true eval_sample_packing: false - lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 diff --git a/examples/magistral/magistral-small-qlora.yaml b/examples/magistral/magistral-small-qlora.yaml index 5ec2f0fbf5..188924d393 100644 --- a/examples/magistral/magistral-small-qlora.yaml +++ b/examples/magistral/magistral-small-qlora.yaml @@ -26,7 +26,6 @@ lora_model_dir: sequence_len: 2048 sample_packing: true - lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 diff --git a/examples/magistral/magistral-small-think-qlora.yaml b/examples/magistral/magistral-small-think-qlora.yaml index 0e8a9c1f7f..b715b31560 100644 --- a/examples/magistral/magistral-small-think-qlora.yaml +++ b/examples/magistral/magistral-small-think-qlora.yaml @@ -26,7 +26,6 @@ lora_model_dir: sequence_len: 2048 sample_packing: true - lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index cf9ced60c6..195aac2e2c 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@48b5169"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@bb8d9f8"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index e0ff14db8f..7924d34720 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@48b5169" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@bb8d9f8" ``` ## Usage @@ -31,6 +31,7 @@ plugins: ## Supported Models +- arcee - cohere - cohere2 - gemma @@ -41,13 +42,17 @@ plugins: - gemma3n_text - glm - glm4 +- gpt_oss - granite - granitemoe +- hunyuan_v1_dense +- hunyuan_v1_moe - llama - llama4 - llama4_text - mistral - mistral3 +- mixtral - mllama - phi - phi3 diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 24cd7b6a7d..6f529f10e3 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -34,7 +34,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@48b5169"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@bb8d9f8"`' ) diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 5fc5ae856b..7df9877d78 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -37,6 +37,7 @@ "glm4", "smollm3", "gpt_oss", + "arcee", ] From 0ae06d756dcb24d8493bd59290fac3ec06ea27a3 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 8 Aug 2025 08:15:17 -0400 Subject: [PATCH 0904/1405] use nanmean for loss aggregation (CP fix) (#3033) * use nanmena for loss aggregation (CP fix) * use regular asserts * small changes to make tests isolate * combining evaluation_loop patches * fix * delete unused * fix check --- src/axolotl/loaders/model.py | 4 +- src/axolotl/loaders/patch_manager.py | 12 ++ src/axolotl/monkeypatch/trainer_eval_guard.py | 78 --------- .../transformers/trainer_loss_calc.py | 165 ++++++++++++++++++ src/axolotl/utils/trainer.py | 3 - tests/monkeypatch/test_trainer_loss_calc.py | 28 +++ 6 files changed, 207 insertions(+), 83 deletions(-) delete mode 100644 src/axolotl/monkeypatch/trainer_eval_guard.py create mode 100644 src/axolotl/monkeypatch/transformers/trainer_loss_calc.py create mode 100644 tests/monkeypatch/test_trainer_loss_calc.py diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 95a56b326c..6bf1f149b5 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -1,5 +1,5 @@ -"""Model loader class implementation for loading, configuring, and patching various -models. +""" +Model loader class implementation for loading, configuring, and patching various models. """ import gc diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 047eb20fd7..795fc3e37a 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -76,8 +76,20 @@ def _apply_transformers_patches(self): from axolotl.monkeypatch.transformers.modeling_flash_attention_utils import ( patch_prepare_from_posids, ) + from axolotl.monkeypatch.transformers.trainer_loss_calc import ( + patch_evaluation_loop, + patch_maybe_log_save_evaluate, + ) + + patch_fsdp2 = ( + self.cfg.torch_compile + and self.cfg.fsdp_config + and self.cfg.fsdp_version == 2 + ) patch_prepare_from_posids() + patch_evaluation_loop(patch_fsdp2) + patch_maybe_log_save_evaluate() def apply_post_model_load_patches(self, model: PreTrainedModel): """Apply patches that require the model instance.""" diff --git a/src/axolotl/monkeypatch/trainer_eval_guard.py b/src/axolotl/monkeypatch/trainer_eval_guard.py deleted file mode 100644 index 8488a16df9..0000000000 --- a/src/axolotl/monkeypatch/trainer_eval_guard.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -fix for FSDP2 evals when using torch.compile -""" - -import inspect - -from transformers import Trainer - -from axolotl.monkeypatch.utils import detab_code -from axolotl.utils.logging import get_logger - -LOG = get_logger(__name__) - -ORIGINAL_TRAINER_CODE = """ - model.eval() -""" - -PATCHED_TRAINER_CODE = """ - if hasattr(model, "eval") and callable(model.eval): - self.model.eval() -""" - - -def get_evaluation_loop_code() -> str: - training_loop = inspect.getsource(Trainer.evaluation_loop) - return training_loop - - -def check_evaluation_loop_is_patchable() -> bool: - eval_loop = get_evaluation_loop_code() - eval_loop, _ = detab_code(eval_loop) - return ORIGINAL_TRAINER_CODE in eval_loop - - -def patch_evaluation_loop_for_fsdp2(): - """ - monkeypatch for fixing the eval loop for fsdp2 with torch.compile - """ - - try: - evaluation_loop = get_evaluation_loop_code() - except OSError: - return - Trainer._original_evaluation_loop = ( # pylint: disable=protected-access - evaluation_loop - ) - evaluation_loop, _ = detab_code(evaluation_loop) - if ORIGINAL_TRAINER_CODE not in evaluation_loop: - return - - evaluation_loop = evaluation_loop.replace( - ORIGINAL_TRAINER_CODE, PATCHED_TRAINER_CODE - ) - evaluation_loop = evaluation_loop.replace( - "def evaluation_loop(", - "def _fixed_evaluation_loop(", - 1, - ) - - # load imports necessary - import transformers.trainer - - items_to_import = [] - for item in dir(transformers.trainer): - if item in evaluation_loop: - items_to_import.append(item) - - exec( # pylint: disable=exec-used # nosec B102 - "from transformers.trainer import (" - + ", ".join(x for x in items_to_import) - + ")", - globals(), - ) - exec(evaluation_loop, globals()) # pylint: disable=exec-used # nosec B102 - LOG.info("patching _inner_training_loop for fsdp optimizer save") - Trainer.evaluation_loop = ( # pylint: disable=protected-access - _fixed_evaluation_loop # pylint: disable=undefined-variable # noqa: F821 - ) diff --git a/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py b/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py new file mode 100644 index 0000000000..75f4158b3b --- /dev/null +++ b/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py @@ -0,0 +1,165 @@ +""" +Module for patching transformers Trainer loss calculation to use nanmean. + +This is needed for context parallelism since chunks of the input sequences may be fully +masked and return NaNs in the loss calculation. + +Also includes a patch for FSDP2 + torch.compile. We need to bundle this together with +the other evaluation_loop patch because we can't patch the same code twice without +raising an OSError. +""" + +import importlib +import inspect + +from transformers import Trainer + +from axolotl.monkeypatch.utils import detab_code +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +ORIGINAL_EVAL_CODE = { + "list": 'metrics[f"{metric_key_prefix}_loss"] = np.concatenate(all_losses).mean().item()', + "array": 'metrics[f"{metric_key_prefix}_loss"] = all_losses.mean().item()', +} +PATCHED_EVAL_CODE = { + "list": 'metrics[f"{metric_key_prefix}_loss"] = np.nanmean(np.concatenate(all_losses)).item()', + "array": 'metrics[f"{metric_key_prefix}_loss"] = np.nanmean(all_losses).item()', +} + +ORIGINAL_FSDP2_CODE = """ + model.eval() +""" + +PATCHED_FSDP2_CODE = """ + if hasattr(model, "eval") and callable(model.eval): + self.model.eval() +""" + +ORIGINAL_MAYBE_CODE = "tr_loss_scalar = self._nested_gather(tr_loss).mean().item()" +PATCHED_MAYBE_CODE = "tr_loss_scalar = self._nested_gather(tr_loss).nanmean().item()" + + +def check_evaluation_loop_is_patchable() -> bool: + evaluation_loop_source = inspect.getsource(Trainer.evaluation_loop) + return all(value in evaluation_loop_source for value in ORIGINAL_EVAL_CODE.values()) + + +def check_evaluation_loop_is_fsdp2_patchable() -> bool: + evaluation_loop_source = inspect.getsource(Trainer.evaluation_loop) + evaluation_loop_source, _ = detab_code(evaluation_loop_source) + return ORIGINAL_FSDP2_CODE in evaluation_loop_source + + +# pylint: disable=protected-access +def patch_evaluation_loop(patch_fsdp2: bool): + """Patch the evaluation_loop method.""" + # Check if already patched + if hasattr(Trainer, "_original_evaluation_loop"): + LOG.info("Trainer.evaluation_loop already patched") + return + + # Check if the patterns exist + try: + evaluation_loop_source = inspect.getsource(Trainer.evaluation_loop) + except OSError: + return + Trainer.evaluation = evaluation_loop_source + evaluation_loop_source, _ = detab_code(evaluation_loop_source) + + # Apply the nanmean patches + evaluation_loop_source = evaluation_loop_source.replace( + ORIGINAL_EVAL_CODE["list"], PATCHED_EVAL_CODE["list"] + ) + evaluation_loop_source = evaluation_loop_source.replace( + ORIGINAL_EVAL_CODE["array"], PATCHED_EVAL_CODE["array"] + ) + + # Apply FSDP2 eval guard patch if needed + if patch_fsdp2 and ORIGINAL_FSDP2_CODE in evaluation_loop_source: + evaluation_loop_source = evaluation_loop_source.replace( + ORIGINAL_FSDP2_CODE, PATCHED_FSDP2_CODE + ) + LOG.info("Applied FSDP2 eval guard patch to evaluation_loop") + + # Rename the function to avoid conflicts + evaluation_loop_source = evaluation_loop_source.replace( + "def evaluation_loop(", + "def axolotl_evaluation_loop(", + 1, + ) + + # Get the module for necessary imports + module_name = Trainer.__module__ + module = importlib.import_module(module_name) + + # Import necessary items from the module + items_to_import = [] + for item in dir(module): + if item in evaluation_loop_source: + items_to_import.append(item) + + # Execute the imports and patched method + exec( # pylint: disable=exec-used # nosec B102 + f"from {module_name} import ({', '.join(items_to_import)})", + globals(), + ) + exec(evaluation_loop_source, globals()) # pylint: disable=exec-used # nosec B102 + + LOG.info("Patched Trainer.evaluation_loop with nanmean loss calculation") + Trainer.evaluation_loop = ( + axolotl_evaluation_loop # pylint: disable=undefined-variable # noqa: F821 + ) + + +def check_maybe_log_save_evaluate_is_patchable() -> bool: + maybe_log_source = inspect.getsource(Trainer._maybe_log_save_evaluate) + return ORIGINAL_MAYBE_CODE in maybe_log_source + + +# pylint: disable=protected-access +def patch_maybe_log_save_evaluate(): + """Patch the _maybe_log_save_evaluate method.""" + # Check if already patched + if hasattr(Trainer, "_original_maybe_log_save_evaluate"): + LOG.info("Trainer._maybe_log_save_evaluate already patched") + return + + # Check if the patterns exist + try: + maybe_log_source = inspect.getsource(Trainer._maybe_log_save_evaluate) + except OSError: + return + Trainer._original_maybe_log_save_evaluate = maybe_log_source + maybe_log_source, _ = detab_code(maybe_log_source) + + # Apply the patch + maybe_log_source = maybe_log_source.replace(ORIGINAL_MAYBE_CODE, PATCHED_MAYBE_CODE) + + # Rename the function to avoid conflicts + maybe_log_source = maybe_log_source.replace( + "def _maybe_log_save_evaluate(", + "def axolotl_maybe_log_save_evaluate(", + 1, + ) + + # Get the module for necessary imports + module_name = Trainer.__module__ + module = importlib.import_module(module_name) + + # Import necessary items from the module + items_to_import = [] + for item in dir(module): + if item in maybe_log_source: + items_to_import.append(item) + + # Execute the imports and patched method + exec( # pylint: disable=exec-used # nosec B102 + f"from {module_name} import ({', '.join(items_to_import)})", + globals(), + ) + exec(maybe_log_source, globals()) # pylint: disable=exec-used # nosec B102 + + LOG.info("Patched Trainer._maybe_log_save_evaluate with nanmean loss calculation") + Trainer._maybe_log_save_evaluate = axolotl_maybe_log_save_evaluate # pylint: disable=undefined-variable # noqa: F821 diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 26634cbbe7..e424cb55a6 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -15,7 +15,6 @@ from torch.utils.data import DataLoader, RandomSampler, SequentialSampler from transformers.utils import is_torch_bf16_gpu_available -from axolotl.monkeypatch.trainer_eval_guard import patch_evaluation_loop_for_fsdp2 from axolotl.utils.distributed import init_distributed_state, reduce_and_broadcast from axolotl.utils.environment import check_cuda_p2p_ib_support from axolotl.utils.logging import get_logger @@ -687,8 +686,6 @@ def setup_trainer( """ from axolotl.core.builders import HFCausalTrainerBuilder, HFRLTrainerBuilder - if cfg.torch_compile and cfg.fsdp_config and cfg.fsdp_version == 2: - patch_evaluation_loop_for_fsdp2() if cfg.rl: trainer_builder = HFRLTrainerBuilder(cfg, model, tokenizer, processor) trainer_builder.model_ref = model_ref diff --git a/tests/monkeypatch/test_trainer_loss_calc.py b/tests/monkeypatch/test_trainer_loss_calc.py new file mode 100644 index 0000000000..de3e926210 --- /dev/null +++ b/tests/monkeypatch/test_trainer_loss_calc.py @@ -0,0 +1,28 @@ +"""Unit tests for trainer loss calc monkeypatch.""" + +import unittest + +from axolotl.monkeypatch.transformers.trainer_loss_calc import ( + check_evaluation_loop_is_fsdp2_patchable, + check_evaluation_loop_is_patchable, + check_maybe_log_save_evaluate_is_patchable, +) + + +class TestTrainerLossCalc(unittest.TestCase): + """ + Unit test class for trainer loss calc monkeypatch + """ + + def test_trainer_loss_calc_is_patchable(self): + """ + Test that the upstream transformers code is still patchable. This will fail if + the patched code changes upstream. + """ + assert check_evaluation_loop_is_patchable() + assert check_evaluation_loop_is_fsdp2_patchable() + assert check_maybe_log_save_evaluate_is_patchable() + + +if __name__ == "__main__": + unittest.main() From f70d4de8c751512a02a8a2358b9a553d843ca83c Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 8 Aug 2025 19:16:43 +0700 Subject: [PATCH 0905/1405] feat(doc): add links to new features on README (#2980) [skip ci] * feat(doc): add links to new features on README * fix merge error * remove blurb about older FSDP2 integration * update blog link * chore: update cce commit * feat: update model support into readme * Update README.md Co-authored-by: salman * chore: lint num spaces --------- Co-authored-by: Wing Lian Co-authored-by: salman --- README.md | 19 +++++++++++++++---- .../colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 2 +- .../cut_cross_entropy/__init__.py | 2 +- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 28c76b8a64..9746158b94 100644 --- a/README.md +++ b/README.md @@ -25,17 +25,28 @@ ## 🎉 Latest Updates -- 2025/07: Voxtral with mistral-common tokenizer support has been integrated in Axolotl. Read the [docs](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/voxtral)! -- 2025/07: TiledMLP support for single-GPU to multi-GPU training with DDP, DeepSpeed and FSDP support has been added to support Arctic Long Sequence Training. (ALST). See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/alst) for using ALST with Axolotl! -- 2025/06: Magistral with mistral-common tokenizer support has been added to Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral) to start training your own Magistral models with Axolotl! +- 2025/07: + - ND Parallelism support has been added into Axolotl. Compose Context Parallelism (CP), Tensor Parallelism (TP), and Fully Sharded Data Parallelism (FSDP) within a single node and across multiple nodes. Check out the [blog post](https://huggingface.co/blog/accelerate-nd-parallel) for more info. + - Axolotl adds more models: [GPT-OSS](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/gpt-oss), [Gemma 3n](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/gemma3n), [Liquid Foundation Model 2 (LFM2)](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/lfm2), and [Arcee Foundation Models (AFM)](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/afm). + - FP8 finetuning with fp8 gather op is now possible in Axolotl via `torchao`. Get started [here](https://docs.axolotl.ai/docs/mixed_precision.html#sec-fp8)! + - [Voxtral](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/voxtral), [Magistral 1.1](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral), and [Devstral](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/devstral) with mistral-common tokenizer support has been integrated in Axolotl! + - TiledMLP support for single-GPU to multi-GPU training with DDP, DeepSpeed and FSDP support has been added to support Arctic Long Sequence Training. (ALST). See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/alst) for using ALST with Axolotl! - 2025/05: Quantization Aware Training (QAT) support has been added to Axolotl. Explore the [docs](https://docs.axolotl.ai/docs/qat.html) to learn more! -- 2025/04: Llama 4 support has been added in Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/llama-4) to start training your own Llama 4 models with Axolotl's linearized version! - 2025/03: Axolotl has implemented Sequence Parallelism (SP) support. Read the [blog](https://huggingface.co/blog/axolotl-ai-co/long-context-with-sequence-parallelism-in-axolotl) and [docs](https://docs.axolotl.ai/docs/sequence_parallelism.html) to learn how to scale your context length when fine-tuning. + +
+ +Expand older updates + +- 2025/06: Magistral with mistral-common tokenizer support has been added to Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral) to start training your own Magistral models with Axolotl! +- 2025/04: Llama 4 support has been added in Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/llama-4) to start training your own Llama 4 models with Axolotl's linearized version! - 2025/03: (Beta) Fine-tuning Multimodal models is now supported in Axolotl. Check out the [docs](https://docs.axolotl.ai/docs/multimodal.html) to fine-tune your own! - 2025/02: Axolotl has added LoRA optimizations to reduce memory usage and improve training speed for LoRA and QLoRA in single GPU and multi-GPU training (DDP and DeepSpeed). Jump into the [docs](https://docs.axolotl.ai/docs/lora_optims.html) to give it a try. - 2025/02: Axolotl has added GRPO support. Dive into our [blog](https://huggingface.co/blog/axolotl-ai-co/training-llms-w-interpreter-feedback-wasm) and [GRPO example](https://github.com/axolotl-ai-cloud/grpo_code) and have some fun! - 2025/01: Axolotl has added Reward Modelling / Process Reward Modelling fine-tuning support. See [docs](https://docs.axolotl.ai/docs/reward_modelling.html). +
+ ## ✨ Overview Axolotl is a tool designed to streamline post-training for various AI models. diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index d79c2fb09e..69881997ec 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@bb8d9f8\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0ee9ee8\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 195aac2e2c..b2bb0fcf83 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@bb8d9f8"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0ee9ee8"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 7924d34720..02e4e6686c 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@bb8d9f8" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0ee9ee8" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 6f529f10e3..4689cc9a8d 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -34,7 +34,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@bb8d9f8"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0ee9ee8"`' ) From 2c8497e489111176adfea5a88d5415ad28d72548 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 8 Aug 2025 08:24:09 -0400 Subject: [PATCH 0906/1405] tag for v0.12.0 release (#3041) --- src/axolotl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index 8548147470..3a2b160602 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -4,4 +4,4 @@ __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.12.0.dev" +__version__ = "0.12.0" From 0da6a95efa91ca9e6b73e8736a94d8dc8d971591 Mon Sep 17 00:00:00 2001 From: salman Date: Fri, 8 Aug 2025 16:18:42 +0100 Subject: [PATCH 0907/1405] Add citation.tff (#3043) [skip ci] --- CITATION.cff | 10 ++++++++++ README.md | 14 ++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 CITATION.cff diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..e6ecc7cb81 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,10 @@ +cff-version: 1.2.0 +type: software +title: "Axolotl: Post-Training for AI Models" +message: "If you use this software, please cite it as below." +authors: + - name: "Axolotl maintainers and contributors" +repository-code: "https://github.com/axolotl-ai-cloud/axolotl" +url: "https://axolotl.ai/" +license: Apache-2.0 +date-released: "2023-05-30" diff --git a/README.md b/README.md index 9746158b94..117eb9b12f 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,20 @@ Contributions are welcome! Please see our [Contributing Guide](https://github.co Interested in sponsoring? Contact us at [wing@axolotl.ai](mailto:wing@axolotl.ai) +## 📝 Citing Axolotl + +If you use Axolotl in your research or projects, please cite it as follows: + +```bibtex +@software{axolotl, + title = {Axolotl: Post-Training for AI Models}, + author = {{Axolotl maintainers and contributors}}, + url = {https://github.com/axolotl-ai-cloud/axolotl}, + license = {Apache-2.0}, + year = {2023} +} +``` + ## 📜 License This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. From 7cfc80ec77978d6cee5fb4aa51e3c43542ac0a5c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 8 Aug 2025 13:56:53 -0400 Subject: [PATCH 0908/1405] set dev version (#3045) [skip ci] --- src/axolotl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index 3a2b160602..e08d43cc34 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -4,4 +4,4 @@ __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.12.0" +__version__ = "0.13.0.dev" From 05f1b4b2e813c045474ff1b2f5e47ed900b1e505 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 9 Aug 2025 14:34:07 -0400 Subject: [PATCH 0909/1405] run monkeypatch tests in seperate runner (#3047) --- .github/workflows/tests.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e0c8faba6a..912b3f1d64 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -105,7 +105,8 @@ jobs: - name: Run tests run: | - pytest -v --durations=10 -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ --cov=axolotl --cov-report=xml + pytest -v --durations=10 -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ --ignore=tests/monkeypatch/ tests/ --cov=axolotl --cov-report=xml + pytest -v --durations=10 tests/monkeypatch/ --cov=axolotl --cov-append --cov-report=xml pytest -v --durations=10 tests/patched/ --cov=axolotl --cov-append --cov-report=xml pytest -v --durations=10 tests/cli/ --cov=axolotl --cov-append --cov-report=xml @@ -179,8 +180,8 @@ jobs: - name: Run tests run: | - pytest -v --durations=10 -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ tests/ - pytest -v --durations=10 tests/patched/ + pytest -v --durations=10 -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ --ignore=tests/monkeypatch/ tests/ --cov=axolotl --cov-report=xml + pytest -v --durations=10 tests/monkeypatch/ --cov=axolotl --cov-append --cov-report=xml pytest -v --durations=10 tests/cli/ - name: cleanup pip cache From d6b81b36836c62d7b5c40083f65a27dfee3d14c1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 10 Aug 2025 11:26:22 -0400 Subject: [PATCH 0910/1405] update training args check for new defaults (#3051) [skip ci] * update training args check for new defaults * skip check for now --- tests/core/test_builders.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index 5f1aec8ff8..fab01a644a 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -281,7 +281,9 @@ def _test_common_training_arguments(self, training_arguments, rl: str): # Other settings assert training_arguments.dataloader_num_workers == 1 assert training_arguments.dataloader_pin_memory is True - assert training_arguments.gradient_checkpointing is False + + # TODO(wing): restore once trl releases 0.22.0 + # assert training_arguments.gradient_checkpointing is True def test_dpo_training_arguments(self, dpo_cfg, model, tokenizer): builder = HFRLTrainerBuilder(dpo_cfg, model, tokenizer) From d12b461d190596b0f23747ef926a462adf2ef9ad Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 10 Aug 2025 20:21:38 -0400 Subject: [PATCH 0911/1405] follow up fix for plugin registration (#3054) [skip ci] --- src/axolotl/cli/config.py | 5 ++--- src/axolotl/integrations/base.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index da933929ee..0f1245aed6 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -153,15 +153,14 @@ def prepare_plugins(cfg: DictDefault): plugin_manager = PluginManager.get_instance() for plugin_name in cfg["plugins"]: plugin_manager.register(plugin_name) + for plugin in plugin_manager.plugins.values(): + plugin.register(cfg) def plugin_set_cfg(cfg: DictDefault): if cfg.get("plugins"): plugin_manager = PluginManager.get_instance() plugin_manager.cfg = cfg - # now that we have the finalized cfg, register the plugins individually - for plugin in plugin_manager.plugins.values(): - plugin.register(cfg) def load_cfg( diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index f43031287f..94ee8d4b1a 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -76,8 +76,8 @@ class BasePlugin: def __init__(self): """Initializes the BasePlugin.""" - def register(self, cfg: DictDefault): # pylint: disable=unused-argument - """Registers the plugin with the given configuration. + def register(self, cfg: dict): # pylint: disable=unused-argument + """Registers the plugin with the given configuration as an unparsed dict. Args: cfg: The configuration for the plugin. From 686933194e70f33acf20844a8c9544ab1cd82975 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 10 Aug 2025 20:21:56 -0400 Subject: [PATCH 0912/1405] fix vllm tagging and add cloud images w/o tmux (#3049) [skip ci] --- .github/workflows/main.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8913002464..3daf39e43f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -98,6 +98,12 @@ jobs: python_version: "3.11" pytorch: 2.7.1 axolotl_extras: + is_latest: + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.1 + axolotl_extras: vllm is_latest: true - cuda: 128 cuda_version: 12.8.1 @@ -151,6 +157,18 @@ jobs: python_version: "3.11" pytorch: 2.6.0 axolotl_extras: + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.1 + axolotl_extras: + is_latest: + - cuda: 126 + cuda_version: 12.6.3 + python_version: "3.11" + pytorch: 2.7.1 + axolotl_extras: vllm + is_latest: true runs-on: axolotl-gpu-runner steps: - name: Checkout From 9b12c056603afa6e1da3f261884102fe30c0d098 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 10 Aug 2025 20:22:20 -0400 Subject: [PATCH 0913/1405] use exec instead of subprocess to make ctrl+c nicer for cli (#3044) * use exec instead of subprocess to make ctrl+c nicer for cli * change var name to use_exec * simplify to bool * flush std* * patch subprocess as mock in test * fix tests * more test fixes --- src/axolotl/cli/main.py | 5 ++-- src/axolotl/cli/utils/train.py | 51 +++++++++++++++++++++++++++------- tests/cli/test_cli_base.py | 12 ++++++-- tests/cli/test_cli_train.py | 19 +++++++------ 4 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index c41acc40b8..e633928029 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -123,9 +123,10 @@ def train( _launcher = None if kwargs.get("use_ray") else launcher # Process each configuration - for cfg_file in generate_config_files(config, sweep): + for cfg_file, is_group in generate_config_files(config, sweep): try: - launch_training(cfg_file, _launcher, cloud, kwargs, launcher_args) + use_exec = is_group is not True + launch_training(cfg_file, _launcher, cloud, kwargs, launcher_args, use_exec) except subprocess.CalledProcessError as exc: LOG.error(f"Failed to train/fine-tune config '{cfg_file}': {exc}") if not sweep: diff --git a/src/axolotl/cli/utils/train.py b/src/axolotl/cli/utils/train.py index 61d05e52b6..3f9a6e4db3 100644 --- a/src/axolotl/cli/utils/train.py +++ b/src/axolotl/cli/utils/train.py @@ -2,6 +2,7 @@ import os import subprocess # nosec +import sys import tempfile from typing import Any, Iterator, Literal @@ -64,10 +65,20 @@ def build_command(base_cmd: list[str], options: dict[str, Any]) -> list[str]: return cmd -def generate_config_files(config: str, sweep: str | None) -> Iterator[str]: - """Generate list of configuration files to process.""" +def generate_config_files(config: str, sweep: str | None) -> Iterator[tuple[str, bool]]: + """ + Generate list of configuration files to process. + + Args: + config: Base configuration file + sweep: Sweep configuration file + + Yields: + Tuple of configuration file name and whether this is a group of configurations + """ + if not sweep: - yield config + yield config, False return # Load sweep and base configurations @@ -78,6 +89,7 @@ def generate_config_files(config: str, sweep: str | None) -> Iterator[str]: # Generate all possible configurations permutations = generate_sweep_configs(base_config, sweep_config) + is_group = len(permutations) > 1 for permutation in permutations: # pylint: disable=consider-using-with temp_file = tempfile.NamedTemporaryFile( @@ -88,7 +100,7 @@ def generate_config_files(config: str, sweep: str | None) -> Iterator[str]: ) yaml.dump(permutation, temp_file) temp_file.close() - yield temp_file.name + yield temp_file.name, is_group def launch_training( @@ -97,6 +109,7 @@ def launch_training( cloud: str | None, kwargs: dict, launcher_args: list[str] | None = None, + use_exec: bool = False, ) -> None: """Execute training with the given configuration.""" launcher_args = launcher_args or [] @@ -105,9 +118,9 @@ def launch_training( _launch_cloud_training(cloud, cfg_file, launcher, kwargs, launcher_args) elif launcher: if launcher == "accelerate": - _launch_accelerate_training(cfg_file, kwargs, launcher_args) + _launch_accelerate_training(cfg_file, kwargs, launcher_args, use_exec) elif launcher == "torchrun": - _launch_torchrun_training(cfg_file, kwargs, launcher_args) + _launch_torchrun_training(cfg_file, kwargs, launcher_args, use_exec) elif launcher == "python": _launch_python_training(cfg_file, kwargs) @@ -136,7 +149,10 @@ def _launch_cloud_training( def _launch_accelerate_training( - cfg_file: str, kwargs: dict, launcher_args: list[str] | None = None + cfg_file: str, + kwargs: dict, + launcher_args: list[str] | None = None, + use_exec: bool = False, ) -> None: """Execute training via accelerate launcher.""" launcher_args = launcher_args or [] @@ -161,11 +177,20 @@ def _launch_accelerate_training( base_cmd.append(cfg_file) cmd = build_command(base_cmd, kwargs) - subprocess.run(cmd, check=True) # nosec B603 + if use_exec: + # make sure to flush stdout and stderr before replacing the process + sys.stdout.flush() + sys.stderr.flush() + os.execvpe(cmd[0], cmd, os.environ) # nosec B606 + else: + subprocess.run(cmd, check=True) # nosec B603 def _launch_torchrun_training( - cfg_file: str, kwargs: dict, launcher_args: list[str] | None = None + cfg_file: str, + kwargs: dict, + launcher_args: list[str] | None = None, + use_exec: bool = False, ) -> None: """Execute training via torchrun launcher.""" launcher_args = launcher_args or [] @@ -178,7 +203,13 @@ def _launch_torchrun_training( base_cmd.append(cfg_file) cmd = build_command(base_cmd, kwargs) - subprocess.run(cmd, check=True) # nosec B603 + if use_exec: + # make sure to flush stdout and stderr before replacing the process + sys.stdout.flush() + sys.stderr.flush() + os.execvpe(cmd[0], cmd, os.environ) # nosec B606 + else: + subprocess.run(cmd, check=True) # nosec B603 def _launch_python_training(cfg_file: str, kwargs: dict) -> None: diff --git a/tests/cli/test_cli_base.py b/tests/cli/test_cli_base.py index 4b880d44a4..e28bbb75ce 100644 --- a/tests/cli/test_cli_base.py +++ b/tests/cli/test_cli_base.py @@ -47,7 +47,9 @@ def _test_basic_execution( config_path = tmp_path / "config.yml" config_path.write_text(valid_test_config) - with patch("subprocess.run") as mock: + mock_fn = "os.execvpe" if command == "train" else "subprocess.run" + + with patch(mock_fn) as mock: result = cli_runner.invoke(cli, [command, str(config_path)]) assert mock.called @@ -65,8 +67,12 @@ def _test_basic_execution( if train: expected.append("--shard=False") - assert mock.call_args.args[0] == expected - assert mock.call_args.kwargs == {"check": True} + if command == "train": + assert mock.call_args.args[0] == "accelerate" + assert mock.call_args.args[1] == expected + else: + assert mock.call_args.args[0] == expected + assert mock.call_args.kwargs == {"check": True} assert result.exit_code == 0 def _test_cli_overrides(self, tmp_path: Path, valid_test_config: str): diff --git a/tests/cli/test_cli_train.py b/tests/cli/test_cli_train.py index 9b266f057a..d4d90f57f8 100644 --- a/tests/cli/test_cli_train.py +++ b/tests/cli/test_cli_train.py @@ -85,7 +85,7 @@ def test_train_with_launcher_args_torchrun( config_path = tmp_path / "config.yml" config_path.write_text(valid_test_config) - with patch("subprocess.run") as mock_subprocess: + with patch("os.execvpe") as mock_subprocess: result = cli_runner.invoke( cli, [ @@ -104,7 +104,7 @@ def test_train_with_launcher_args_torchrun( mock_subprocess.assert_called_once() # Verify launcher args are passed to torchrun - called_cmd = mock_subprocess.call_args.args[0] + called_cmd = mock_subprocess.call_args.args[1] assert called_cmd[0] == "torchrun" assert "--nproc_per_node=2" in called_cmd assert "--nnodes=1" in called_cmd @@ -118,7 +118,7 @@ def test_train_with_launcher_args_accelerate( config_path = tmp_path / "config.yml" config_path.write_text(valid_test_config) - with patch("subprocess.run") as mock_subprocess: + with patch("os.execvpe") as mock_subprocess: result = cli_runner.invoke( cli, [ @@ -137,7 +137,8 @@ def test_train_with_launcher_args_accelerate( mock_subprocess.assert_called_once() # Verify launcher args are passed to accelerate - called_cmd = mock_subprocess.call_args.args[0] + assert mock_subprocess.call_args.args[0] == "accelerate" + called_cmd = mock_subprocess.call_args.args[1] assert called_cmd[0] == "accelerate" assert called_cmd[1] == "launch" assert "--config_file=accelerate_config.yml" in called_cmd @@ -152,7 +153,7 @@ def test_train_backward_compatibility_no_launcher_args( config_path = tmp_path / "config.yml" config_path.write_text(valid_test_config) - with patch("subprocess.run") as mock_subprocess: + with patch("os.execvpe") as mock_subprocess: result = cli_runner.invoke( cli, [ @@ -170,7 +171,8 @@ def test_train_backward_compatibility_no_launcher_args( mock_subprocess.assert_called_once() # Verify no launcher args contamination - called_cmd = mock_subprocess.call_args.args[0] + assert mock_subprocess.call_args.args[0] == "accelerate" + called_cmd = mock_subprocess.call_args.args[1] assert called_cmd[0] == "accelerate" assert called_cmd[1] == "launch" # Should not contain any extra launcher args @@ -186,7 +188,7 @@ def test_train_mixed_args_with_launcher_args( config_path = tmp_path / "config.yml" config_path.write_text(valid_test_config) - with patch("subprocess.run") as mock_subprocess: + with patch("os.execvpe") as mock_subprocess: result = cli_runner.invoke( cli, [ @@ -207,7 +209,8 @@ def test_train_mixed_args_with_launcher_args( assert result.exit_code == 0 mock_subprocess.assert_called_once() - called_cmd = mock_subprocess.call_args.args[0] + assert mock_subprocess.call_args.args[0] == "torchrun" + called_cmd = mock_subprocess.call_args.args[1] # Verify launcher args assert "--nproc_per_node=8" in called_cmd # Verify axolotl args are also present From d4d84d48aff14b84954a049057ce3d309644627f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 11 Aug 2025 09:31:54 -0400 Subject: [PATCH 0914/1405] fix ray train and add fsdp2 smoke test for ray trainer (#3053) * add fsdp2 smokle test for ray trainer * fix raytrain with fsdp2 --- src/axolotl/cli/utils/train.py | 3 ++ tests/e2e/multigpu/test_ray.py | 74 +++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/axolotl/cli/utils/train.py b/src/axolotl/cli/utils/train.py index 3f9a6e4db3..f1ac857b3a 100644 --- a/src/axolotl/cli/utils/train.py +++ b/src/axolotl/cli/utils/train.py @@ -123,6 +123,9 @@ def launch_training( _launch_torchrun_training(cfg_file, kwargs, launcher_args, use_exec) elif launcher == "python": _launch_python_training(cfg_file, kwargs) + elif launcher is None: + # handle ray train launch + _launch_python_training(cfg_file, kwargs) def _launch_cloud_training( diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index dd14222968..7f1278abf4 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -10,7 +10,11 @@ from axolotl.utils.dict import DictDefault -from tests.e2e.utils import check_tensorboard, require_torch_lt_2_6_0 +from tests.e2e.utils import ( + check_tensorboard, + require_torch_2_7_0, + require_torch_lt_2_6_0, +) AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent @@ -139,3 +143,71 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps): check_tensorboard( temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" ) + + @require_torch_2_7_0 + @pytest.mark.parametrize( + "gradient_accumulation_steps", + [1, 2], + ) + def test_sft_fsdp2_packed(self, temp_dir, gradient_accumulation_steps): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sample_packing": True, + "pad_to_sequence_len": True, + "sequence_len": 1024, + "val_set_size": 0.01, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:10%]", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 1, + "gradient_accumulation_steps": gradient_accumulation_steps, + "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", + "learning_rate": 0.00001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": False, + "transformer_layer_cls_to_wrap": "LlamaDecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + "use_tensorboard": True, + "save_first_step": False, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--use-ray", + "--ray-num-workers", + "2", + ] + ) + + check_tensorboard( + temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" + ) From ce20e838b5f159e4fbaa0d112605506ca6d13e60 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:32:21 -0400 Subject: [PATCH 0915/1405] chore: update pre-commit hooks (#3050) [skip ci] Co-authored-by: djsaunde <1245942+djsaunde@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dadac90c3f..4c9268529d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ default_language_version: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-yaml - id: end-of-file-fixer @@ -23,7 +23,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pylint-dev/pylint - rev: v3.3.7 + rev: v3.3.8 hooks: - id: pylint - repo: https://github.com/pre-commit/mirrors-mypy From 3d4562000866b81cdd80923353d388f5045b707c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 11 Aug 2025 09:34:41 -0400 Subject: [PATCH 0916/1405] remove prepare-from-posids patch (#3052) [skip ci] --- src/axolotl/loaders/patch_manager.py | 4 - .../modeling_flash_attention_utils.py | 87 ------------------- 2 files changed, 91 deletions(-) delete mode 100644 src/axolotl/monkeypatch/transformers/modeling_flash_attention_utils.py diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 795fc3e37a..f1ca3c7259 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -73,9 +73,6 @@ def apply_post_plugin_pre_model_load_patches(self): self._apply_voxtral_patches() def _apply_transformers_patches(self): - from axolotl.monkeypatch.transformers.modeling_flash_attention_utils import ( - patch_prepare_from_posids, - ) from axolotl.monkeypatch.transformers.trainer_loss_calc import ( patch_evaluation_loop, patch_maybe_log_save_evaluate, @@ -87,7 +84,6 @@ def _apply_transformers_patches(self): and self.cfg.fsdp_version == 2 ) - patch_prepare_from_posids() patch_evaluation_loop(patch_fsdp2) patch_maybe_log_save_evaluate() diff --git a/src/axolotl/monkeypatch/transformers/modeling_flash_attention_utils.py b/src/axolotl/monkeypatch/transformers/modeling_flash_attention_utils.py deleted file mode 100644 index 1bd8ac6bce..0000000000 --- a/src/axolotl/monkeypatch/transformers/modeling_flash_attention_utils.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Monkey patch to fix transformers.modeling_flash_attention_utils. - -see https://github.com/huggingface/transformers/pull/39653/files -""" - -import sys - -import torch - - -def _prepare_from_posids(query, key, value, position_ids): - """ - This function returns necessary arguments to call `flash_attn_varlen_func`. - All three query, key, value states will be flattened. - Cumulative lengths of each examples in the batch will be extracted from position_ids. - NOTE: ideally cumulative lengths should be prepared at the data collator stage - Arguments: - query (`torch.Tensor`): - Query state with padding. Shape: (batch_size, query_length, num_heads, head_dim). - key (`torch.Tensor`): - Key state with padding. Shape: (batch_size, kv_seq_len, num_key_value_heads, head_dim). - value (`torch.Tensor`): - Value state with padding. Shape: (batch_size, kv_seq_len, num_key_value_heads, head_dim). - position_ids (`torch.Tensor`): - Boolean or int tensor of shape (batch_size, sequence_length), 1 means valid and 0 means not valid. - Return: - query (`torch.Tensor`): - Query state without padding. Shape: (total_target_length, num_heads, head_dim). - key (`torch.Tensor`): - Key state with padding. Shape: (total_source_length, num_key_value_heads, head_dim). - value (`torch.Tensor`): - Value state with padding. Shape: (total_source_length, num_key_value_heads, head_dim). - indices_q (`torch.Tensor`): - The indices of non-masked tokens from the flattened input target sequence. - (cu_seqlens_q, cu_seqlens_k) (`tuple[int]`): - The cumulative sequence lengths for the target (query) and source (key, value), used to index into ragged (unpadded) tensors. `cu_seqlens` shape is (batch_size + 1,). - (max_seqlen_in_batch_q, max_seqlen_in_batch_k) (`tuple[int]`): - Maximum sequence length in batch (`max_seqlen_in_batch_q` for the target sequence i.e. query, `max_seqlen_in_batch_k` for the source sequence i.e. key/value). - """ - query = query.contiguous().view(-1, query.size(-2), query.size(-1)) - key = key.contiguous().view(-1, key.size(-2), key.size(-1)) - value = value.contiguous().view(-1, value.size(-2), value.size(-1)) - - position_ids = position_ids.flatten() - indices_q = torch.arange( - position_ids.size(0), device=position_ids.device, dtype=torch.int32 - ) - - cu_seq_lens = torch.cat( - ( - indices_q[position_ids == 0], - torch.tensor( - position_ids.size(), device=position_ids.device, dtype=torch.int32 - ), - ) - ) - # NOTE: With torch compile, this will cause a graph break if you don't set - # `TORCHDYNAMO_CAPTURE_SCALAR_OUTPUTS=1` in the environment or call - # `torch._dynamo.config.capture_scalar_outputs = True` before doing the forward pass. - # This is a limitation of flash attention API, as the function `flash_attn_varlen_func` - # requires `max_length_q`, `max_length_k` to be passed as `int` and not `torch.Tensor`. - # https://github.com/Dao-AILab/flash-attention/blob/2dd8078adc1d9b74e315ee99718c0dea0de8eeb6/flash_attn/flash_attn_interface.py#L1423-L1424 - # We should use cu_seq_lens instead of position_ids to get the max length since position_ids is not always increasing - # for some models (e.g. qwen2-vl). - max_length = cu_seq_lens.diff().max().item() - return ( - query, - key, - value, - indices_q, - (cu_seq_lens, cu_seq_lens), - (max_length, max_length), - ) - - -def patch_prepare_from_posids(): - import transformers.modeling_flash_attention_utils - - transformers.modeling_flash_attention_utils._prepare_from_posids = ( # pylint: disable=protected-access - _prepare_from_posids - ) - setattr( - sys.modules["transformers.modeling_flash_attention_utils"], - "_prepare_from_posids", - _prepare_from_posids, - ) From e0a2523a3bf875c9e82de0ecea67371e634553f1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 13 Aug 2025 06:39:39 -0400 Subject: [PATCH 0917/1405] Workaround to unblock docs build in main (#3055) Co-authored-by: Salman Mohammadi --- TODO.md | 10 ---------- src/axolotl/cli/utils/train.py | 6 ++---- 2 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 2002bbbaf1..0000000000 --- a/TODO.md +++ /dev/null @@ -1,10 +0,0 @@ -# todo list - -- [] Validation of parameters for combinations that won't work - - - -## things that are known not to work - -- FSDP offload and gradient_checkpointing - https://github.com/pytorch/pytorch/issues/82203 -- adamw_bnb_8bit doesn't play well with FSDP offload diff --git a/src/axolotl/cli/utils/train.py b/src/axolotl/cli/utils/train.py index f1ac857b3a..31b0bcf585 100644 --- a/src/axolotl/cli/utils/train.py +++ b/src/axolotl/cli/utils/train.py @@ -67,14 +67,12 @@ def build_command(base_cmd: list[str], options: dict[str, Any]) -> list[str]: def generate_config_files(config: str, sweep: str | None) -> Iterator[tuple[str, bool]]: """ - Generate list of configuration files to process. + Generate list of configuration files to process. Yields a tuple of the configuration file name and a boolean indicating + whether this is a group of configurations (i.e., a sweep). Args: config: Base configuration file sweep: Sweep configuration file - - Yields: - Tuple of configuration file name and whether this is a group of configurations """ if not sweep: From 09145de8fa0306c3b88212da71564d3e3892ad31 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 13 Aug 2025 19:41:07 -0400 Subject: [PATCH 0918/1405] upgrade transformers==4.55.1 and bitsandbytes==0.47.0 (#3064) * upgrade transformers==4.55.1 * also upgrade bnb * remove bnb params4bit patch (upstreamed) * use latest causal-conv1d * fix patching ring-flash-attn with now missing imports --------- Co-authored-by: Dan Saunders --- docker/Dockerfile-base | 2 +- requirements.txt | 4 +- src/axolotl/loaders/patch_manager.py | 2 - src/axolotl/monkeypatch/fsdp2_qlora.py | 61 ----------- .../monkeypatch/ring_attn/adapters/batch.py | 11 +- src/axolotl/monkeypatch/ring_attn/patch.py | 11 +- src/axolotl/utils/schemas/validation.py | 21 +++- tests/e2e/patched/test_fsdp2_qlora.py | 102 +----------------- 8 files changed, 38 insertions(+), 176 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 0434a583fb..d1151cedd3 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -37,7 +37,7 @@ WORKDIR /workspace RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==23.2 setuptools==75.8.0 wheel && \ python3 -m pip install --no-cache-dir -U torch==${PYTORCH_VERSION}+cu${CUDA} torchvision --extra-index-url https://download.pytorch.org/whl/cu$CUDA && \ - python3 -m pip install --no-cache-dir "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" && \ + CAUSAL_CONV1D_SKIP_CUDA_BUILD=TRUE python3 -m pip install --no-cache-dir causal_conv1d==1.5.2 && \ python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" && \ python3 -m pip cache purge diff --git a/requirements.txt b/requirements.txt index 370bf5a5ef..5f7767812b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ # START section of dependencies that don't install on Darwin/MacOS -bitsandbytes==0.46.1 +bitsandbytes==0.47.0 # triton 3.4.0 is not compatible with CCE triton>=3.0.0,<3.4.0 mamba-ssm==1.2.0.post1 @@ -14,7 +14,7 @@ packaging==23.2 huggingface_hub>=0.33.0 peft==0.17.0 -transformers==4.55.0 +transformers==4.55.1 tokenizers>=0.21.1 accelerate==1.10.0 datasets==4.0.0 diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index f1ca3c7259..628d897d0a 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -285,12 +285,10 @@ def _apply_fsdp2_bnb_patches(self): and self.cfg.adapter == "qlora" ): from axolotl.monkeypatch.fsdp2_qlora import ( - apply_bnb_torch_function_patch, apply_init_sharded_param_patch, apply_init_unsharded_param_patch, ) - apply_bnb_torch_function_patch() apply_init_sharded_param_patch() apply_init_unsharded_param_patch() diff --git a/src/axolotl/monkeypatch/fsdp2_qlora.py b/src/axolotl/monkeypatch/fsdp2_qlora.py index a2cb7e4729..5a4332fffa 100644 --- a/src/axolotl/monkeypatch/fsdp2_qlora.py +++ b/src/axolotl/monkeypatch/fsdp2_qlora.py @@ -9,73 +9,12 @@ import importlib import inspect -import torch -from torch.nn import Parameter - from axolotl.monkeypatch.utils import detab_code from axolotl.utils.logging import get_logger LOG = get_logger(__name__) -def patched_torch_function(cls, func, types, args=(), kwargs=None): - """ - Patched version of Params4bit.__torch_function__ for preserving Params4bit - class identity and attributes. - """ - if kwargs is None: - kwargs = {} - - if func in [torch.chunk, torch.split]: - tensor = args[0] - result = Parameter.__torch_function__(func, types, args, kwargs) - - if isinstance(result, tuple): - return tuple( - cls( - data=chunk, - requires_grad=tensor.requires_grad, - quant_state=tensor.quant_state, - blocksize=tensor.blocksize, - compress_statistics=tensor.compress_statistics, - quant_type=tensor.quant_type, - quant_storage=tensor.quant_storage, - module=tensor.module, - bnb_quantized=tensor.bnb_quantized, - ) - for chunk in result - ) - - return cls( - data=result, - requires_grad=tensor.requires_grad, - quant_state=tensor.quant_state, - blocksize=tensor.blocksize, - compress_statistics=tensor.compress_statistics, - quant_type=tensor.quant_type, - quant_storage=tensor.quant_storage, - module=tensor.module, - bnb_quantized=tensor.bnb_quantized, - ) - - return Parameter.__torch_function__(func, types, args, kwargs) - - -# pylint: disable=protected-access -def apply_bnb_torch_function_patch(): - """ - Patch Params4bit.__torch_function__ using Axolotl-style approach. - - Returns: - True if patching succeeded, False otherwise. - """ - from bitsandbytes.nn.modules import Params4bit - - Params4bit.__torch_function__ = classmethod(patched_torch_function) - - LOG.info("Successfully patched Params4bit.__torch_function__") - - # pylint: disable=protected-access def apply_init_sharded_param_patch(): """Apply patch to FSDPParam._init_sharded_param to support Params4bit.""" diff --git a/src/axolotl/monkeypatch/ring_attn/adapters/batch.py b/src/axolotl/monkeypatch/ring_attn/adapters/batch.py index ebed9ebdc3..607b4dd71e 100644 --- a/src/axolotl/monkeypatch/ring_attn/adapters/batch.py +++ b/src/axolotl/monkeypatch/ring_attn/adapters/batch.py @@ -20,12 +20,15 @@ from ring_flash_attn.adapters.hf_adapter import check_params from transformers.modeling_flash_attention_utils import is_flash_attn_greater_or_equal -try: +try: # pylint: disable=duplicate-code from transformers.modeling_flash_attention_utils import _flash_supports_window except ImportError: - from transformers.modeling_flash_attention_utils import ( - _flash_supports_window_size as _flash_supports_window, - ) + try: + from transformers.modeling_flash_attention_utils import ( + _flash_supports_window_size as _flash_supports_window, + ) + except ImportError: + _flash_supports_window = True from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS diff --git a/src/axolotl/monkeypatch/ring_attn/patch.py b/src/axolotl/monkeypatch/ring_attn/patch.py index 934687a165..ea0f9dd026 100644 --- a/src/axolotl/monkeypatch/ring_attn/patch.py +++ b/src/axolotl/monkeypatch/ring_attn/patch.py @@ -15,12 +15,15 @@ import torch.distributed as dist from torch.distributed import DeviceMesh -try: +try: # pylint: disable=duplicate-code from transformers.modeling_flash_attention_utils import _flash_supports_window except ImportError: - from transformers.modeling_flash_attention_utils import ( - _flash_supports_window_size as _flash_supports_window, - ) + try: + from transformers.modeling_flash_attention_utils import ( + _flash_supports_window_size as _flash_supports_window, + ) + except ImportError: + _flash_supports_window = True from axolotl.monkeypatch.utils import get_cu_seqlens_from_pos_ids from axolotl.utils.logging import get_logger diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 72991c9470..0d6d05a0e3 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -3,6 +3,7 @@ # pylint: disable=too-many-boolean-expressions import json +import sys import tempfile from pathlib import Path @@ -1251,10 +1252,26 @@ def check_context_parallel_size(self): try: import transformers.modeling_flash_attention_utils + from transformers.utils import is_flash_attn_greater_or_equal # pylint: disable=protected-access - transformers.modeling_flash_attention_utils._flash_supports_window_size = ( - transformers.modeling_flash_attention_utils._flash_supports_window + transformers.modeling_flash_attention_utils._flash_supports_window = ( + True + ) + setattr( + sys.modules["transformers.modeling_flash_attention_utils"], + "_flash_supports_window", + True, + ) + setattr( + sys.modules["transformers.modeling_flash_attention_utils"], + "_flash_supports_window_size", + True, + ) + setattr( + sys.modules["transformers.modeling_flash_attention_utils"], + "is_flash_attn_greater_or_equal", + is_flash_attn_greater_or_equal, ) import ring_flash_attn # noqa: F401 # pylint:disable=unused-import except ImportError as exception: diff --git a/tests/e2e/patched/test_fsdp2_qlora.py b/tests/e2e/patched/test_fsdp2_qlora.py index 9dd053ad89..ca17b81d1a 100644 --- a/tests/e2e/patched/test_fsdp2_qlora.py +++ b/tests/e2e/patched/test_fsdp2_qlora.py @@ -1,126 +1,28 @@ -"""Integration tests for FSDP Params4bit patches.""" +"""Integration tests for FSDP2 Params4bit patches.""" -from unittest.mock import Mock, patch - -import bitsandbytes as bnb import pytest -import torch from torch.distributed.fsdp._fully_shard._fsdp_param import FSDPParam -from axolotl.monkeypatch.fsdp2_qlora import ( - apply_bnb_torch_function_patch, - patched_torch_function, -) - - -@pytest.fixture -def mock_params4bit(): - """Create a mock Params4bit instance with test attributes.""" - mock_instance = Mock() - mock_instance.requires_grad = True - mock_instance.quant_state = "test_state" - mock_instance.blocksize = 128 - mock_instance.compress_statistics = True - mock_instance.quant_type = "fp4" - mock_instance.quant_storage = "test_storage" - mock_instance.module = "test_module" - mock_instance.bnb_quantized = True - return mock_instance - - -class TestBnbTorchFunctionPatch: - """Test the Params4bit.__torch_function__ patch.""" - - def test_apply_patch(self): - """Test that the patch can be applied.""" - with patch("bitsandbytes.nn.modules.Params4bit") as mock_cls: - apply_bnb_torch_function_patch() - assert hasattr(mock_cls, "__torch_function__") - assert isinstance(mock_cls.__torch_function__, classmethod) - - # pylint: disable=redefined-outer-name - def test_torch_chunk_preserves_attributes(self, mock_params4bit): - """Test that torch.chunk preserves Params4bit attributes.""" - mock_cls = Mock() - chunks = (torch.tensor([1, 2]), torch.tensor([3, 4])) - - with patch("torch.nn.Parameter.__torch_function__", return_value=chunks): - result = patched_torch_function( - mock_cls, - torch.chunk, - (type(mock_params4bit),), - args=(mock_params4bit, 2), - ) - - assert isinstance(result, tuple) - assert len(result) == 2 - - # Check that Params4bit constructor was called with preserved attributes - assert mock_cls.call_count == 2 - for call in mock_cls.call_args_list: - kwargs = call[1] - assert kwargs["requires_grad"] == mock_params4bit.requires_grad - assert kwargs["quant_state"] == mock_params4bit.quant_state - assert kwargs["blocksize"] == mock_params4bit.blocksize - - # pylint: disable=redefined-outer-name - def test_other_functions_fallback(self, mock_params4bit): - """Test that non-chunk/split functions use Parameter fallback.""" - mock_cls = Mock() - fallback_result = torch.tensor([5, 6, 7]) - - with patch( - "torch.nn.Parameter.__torch_function__", return_value=fallback_result - ) as mock_fallback: - result = patched_torch_function( - mock_cls, torch.add, (type(mock_params4bit),), args=(mock_params4bit, 1) - ) - - # Should call Parameter.__torch_function__ and return its result - mock_fallback.assert_called_once() - assert result is fallback_result - mock_cls.assert_not_called() - class TestFSDPPatchIntegration: """Test FSDP patch integration.""" @pytest.mark.integration - def test_all_patches_together(self): + def test_fsdp2_init_patches(self): """Test that all patches can be applied together.""" from axolotl.monkeypatch.fsdp2_qlora import ( apply_init_sharded_param_patch, apply_init_unsharded_param_patch, ) - # Store original methods before patching - original_torch_function = getattr( - bnb.nn.modules.Params4bit, "__torch_function__", None - ) - # pylint: disable=protected-access original_init_sharded = FSDPParam._init_sharded_param original_init_unsharded = FSDPParam.init_unsharded_param # Apply patches - apply_bnb_torch_function_patch() apply_init_sharded_param_patch() apply_init_unsharded_param_patch() - # Verify patches were applied - current_torch_function = getattr( - bnb.nn.modules.Params4bit, "__torch_function__", None - ) - if original_torch_function is not None: - assert ( - current_torch_function != original_torch_function - ), "Params4bit.__torch_function__ was not patched" - else: - assert ( - current_torch_function is not None - ), "Params4bit.__torch_function__ was not added" - - # Check that FSDP methods were patched assert ( # pylint: disable=protected-access FSDPParam._init_sharded_param From 506e3a39074a76df223af211af8e503343ea6b3e Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 14 Aug 2025 08:21:50 +0700 Subject: [PATCH 0919/1405] fix: fsdp_config validation being None (#3061) [skip ci] * fix: fsdp_config validation being None * fix: handling --------- Co-authored-by: salman --- src/axolotl/utils/schemas/validation.py | 26 ++++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 0d6d05a0e3..217244b019 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -370,10 +370,10 @@ def check_fp8_config(cls, data): "see speed improvements. Please consider setting `torch_compile: " "true` in your config." ) + fsdp_config = data.get("fsdp_config") or {} if data.get("fp8") and ( - data.get("fsdp_config", {}).get("activation_checkpointing", False) is True - or data.get("fsdp_config", {}).get("fsdp_activation_checkpointing", False) - is True + fsdp_config.get("activation_checkpointing", False) is True + or fsdp_config.get("fsdp_activation_checkpointing", False) is True ): LOG.warning( "FP8 + FSDP2 + activation checkpointing may be slower than BF16 " @@ -818,13 +818,13 @@ def check_fsdp2_base_model_quant_rl(cls, data): @model_validator(mode="before") @classmethod def check_fsdp_version_in_fsdp_config(cls, data): - if data.get("fsdp_config"): - if data.get("fsdp_config", {}).get("fsdp_version"): - LOG.warning( - "Configuring `fsdp_version` in `fsdp_config` is deprecated. " - "Please configure `fsdp_version` as a top-level field." - ) - data["fsdp_version"] = data.get("fsdp_config").pop("fsdp_version") + fsdp_config = data.get("fsdp_config") or {} + if fsdp_config and fsdp_config.get("fsdp_version"): + LOG.warning( + "Configuring `fsdp_version` in `fsdp_config` is deprecated. " + "Please configure `fsdp_version` as a top-level field." + ) + data["fsdp_version"] = fsdp_config.pop("fsdp_version") return data @model_validator(mode="before") @@ -1152,10 +1152,8 @@ def check_gptq_w_revision(cls, data): @classmethod def check_gpt_oss_fsdp_loading(cls, data): if data.get("model_quantization_config", "") == "Mxfp4Config": - if ( - data.get("fsdp_config", {}).get("cpu_ram_efficient_loading", False) - is True - ): + fsdp_config = data.get("fsdp_config") or {} + if fsdp_config.get("cpu_ram_efficient_loading", False) is True: raise ValueError( "FSDP cpu_ram_efficient_loading is not supported for Mxfp4Config model quantization." ) From 48b7ae16778674ca01223fcfff362d5ad8798b48 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 13 Aug 2025 21:23:05 -0400 Subject: [PATCH 0920/1405] use updated patch releasE (#3066) --- docker/Dockerfile-base | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index d1151cedd3..87918cc414 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -37,7 +37,7 @@ WORKDIR /workspace RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==23.2 setuptools==75.8.0 wheel && \ python3 -m pip install --no-cache-dir -U torch==${PYTORCH_VERSION}+cu${CUDA} torchvision --extra-index-url https://download.pytorch.org/whl/cu$CUDA && \ - CAUSAL_CONV1D_SKIP_CUDA_BUILD=TRUE python3 -m pip install --no-cache-dir causal_conv1d==1.5.2 && \ + CAUSAL_CONV1D_FORCE_CXX11_ABI=TRUE CAUSAL_CONV1D_FORCE_BUILD=TRUE python3 -m pip install --no-cache-dir causal_conv1d==1.5.2 && \ python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" && \ python3 -m pip cache purge diff --git a/requirements.txt b/requirements.txt index 5f7767812b..c2552002f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ packaging==23.2 huggingface_hub>=0.33.0 peft==0.17.0 -transformers==4.55.1 +transformers==4.55.2 tokenizers>=0.21.1 accelerate==1.10.0 datasets==4.0.0 From d1de6f5f3d5966068099fcbb904446b21bfd7b29 Mon Sep 17 00:00:00 2001 From: salman Date: Thu, 14 Aug 2025 03:57:51 +0100 Subject: [PATCH 0921/1405] Add option to skip slow tests in PRs (#3060) [skip ci] * testing e2e skip [skip-e2e] * testing e2e skip [skip-e2e] * testing e2e skip [skip-e2e] * testing e2e skip [skip-e2e] * testing e2e skip [skip-e2e] * testing e2e skip [skip-e2e] * testing e2e skip [skip-e2e] * testing e2e skip [skip-e2e] * testing e2e skip [skip-e2e] * testing e2e skip [skip-e2e] * testing e2e skip [skip-e2e] * stop running multigpu [skip-e2e] * should work now [skip-e2e] * reverting [skip-e2e] * testing [skip-e2e] * debug [skip-e2e] * debug [skip-e2e] * round 2[skip-e2e] * removing debug [skip-e2e] * support skipping whole PR [skip-e2e] * use script for e2e skip [skip-e2e] * contributing [skip-e2e] * contributing [skip-e2e] --------- Co-authored-by: Wing Lian --- .github/CONTRIBUTING.md | 7 +++++++ .github/workflows/tests.yml | 42 +++++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8f67908e8d..fcfd968916 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -57,6 +57,13 @@ We welcome ideas for improvements and new features. To suggest an enhancement, o 5. Push your branch to your fork on GitHub. 6. Open a new pull request against the `main` branch of the axolotl repository. Include a clear and concise description of your changes, referencing any related issues. +#### Skipping CI Checks + +You can skip certain CI checks by including specific keywords in your commit messages: + +- `[skip ci]` or `skip ci` - Skips all CI checks for that commit +- `[skip-e2e]` or `skip-e2e` - Skips only end-to-end tests while running other CI checks. You may also include this in the title of your PR to disable end-to-end tests for the entire PR. + ## Style Guidelines ### Code Style diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 912b3f1d64..fe63aa313a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -188,13 +188,44 @@ jobs: run: | find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + gate-skip-e2e: + needs: [pre-commit, pytest, pytest-sdist] + runs-on: ubuntu-latest + outputs: + skip: ${{ steps.compute.outputs.skip }} + steps: + - uses: actions/github-script@v7 + id: compute + with: + script: | + const token = /\[skip-e2e\]/i; + let msg = ''; + if (context.eventName === 'push') { + msg = context.payload.head_commit?.message || ''; + } else if (context.eventName === 'pull_request') { + const { owner, repo } = context.repo; + const prNumber = context.payload.pull_request.number; + const commits = await github.paginate( + github.rest.pulls.listCommits, + { owner, repo, pull_number: prNumber, per_page: 100 } + ); + msg = commits.at(-1)?.commit?.message || ''; + } + const title = context.payload.pull_request?.title || ''; + const body = context.payload.pull_request?.body || ''; + const skip = token.test(msg) || token.test(title) || token.test(body); + core.setOutput('skip', String(skip)); + docker-e2e-tests-1st: # Run this job first as a gate for running the remainder of the test matrix - if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' && !github.event.pull_request.draft }} + if: > + github.repository_owner == 'axolotl-ai-cloud' && + (github.event_name != 'pull_request' || !github.event.pull_request.draft) && + needs.gate-skip-e2e.outputs.skip != 'true' # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] timeout-minutes: 120 - needs: [pre-commit, pytest, pytest-sdist] + needs: [pre-commit, pytest, pytest-sdist, gate-skip-e2e] strategy: fail-fast: false @@ -240,13 +271,16 @@ jobs: modal run cicd.e2e_tests docker-e2e-tests: - if: ${{ github.repository_owner == 'axolotl-ai-cloud' && !github.event.pull_request.draft }} + if: > + github.repository_owner == 'axolotl-ai-cloud' && + (github.event_name != 'pull_request' || !github.event.pull_request.draft) && + needs.gate-skip-e2e.outputs.skip != 'true' # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] timeout-minutes: 120 # Only run the remainder of the matrix if the first e2e check passed; # this is to save on wasted compute costs for known failures that get caught in the first run - needs: [pre-commit, pytest, docker-e2e-tests-1st] + needs: [pre-commit, pytest, gate-skip-e2e, docker-e2e-tests-1st] strategy: fail-fast: false From 130ef7c51acdf5fcb44cf16a91defcd4d5976966 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 15 Aug 2025 10:52:57 -0400 Subject: [PATCH 0922/1405] Various fixes for VLMs (#3063) * fix to not use batch feature indexing * more vlm fixes * use AutoModelForImageTextToText * add example yaml and need num2words for chat template * improve handling of adding image tokens to conversation * add lfm2-vl support * update the lfm readme * fix markdown and add rtol for loss checks * feat: add smolvlm2 processing strat * fix: check for causal-conv1d in lfm models * feat: add docs for lfm2 * feat: add new models and tips to docs * feat: add smolvlm2 docs and remove extra dep * chore: update docs * feat: add video instructions * chore: cleanup * chore: comments * fix: typo * feat: add usage stats * chore: refactor --------- Co-authored-by: NanoCode012 --- docs/multimodal.qmd | 49 ++++++++- examples/LiquidAI/README.md | 58 +++++++++++ .../{lfm2 => LiquidAI}/lfm2-350m-fft.yaml | 1 - examples/LiquidAI/lfm2-vl-lora.yaml | 58 +++++++++++ examples/lfm2/README.md | 7 -- examples/smolvlm2/README.md | 49 +++++++++ examples/smolvlm2/smolvlm2-2B-lora.yaml | 56 +++++++++++ src/axolotl/loaders/constants.py | 25 ++--- src/axolotl/loaders/model.py | 14 +++ src/axolotl/processing_strategies.py | 99 +++++++++++++------ .../prompt_strategies/chat_template.py | 23 +++-- src/axolotl/utils/collators/mm_chat.py | 68 +++---------- tests/e2e/utils.py | 7 +- 13 files changed, 392 insertions(+), 122 deletions(-) create mode 100644 examples/LiquidAI/README.md rename examples/{lfm2 => LiquidAI}/lfm2-350m-fft.yaml (96%) create mode 100644 examples/LiquidAI/lfm2-vl-lora.yaml delete mode 100644 examples/lfm2/README.md create mode 100644 examples/smolvlm2/README.md create mode 100644 examples/smolvlm2/smolvlm2-2B-lora.yaml diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index dbb365f730..d839ce211c 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -13,10 +13,13 @@ format: - [Pixtral](#sec-pixtral) - [Llava-1.5](#sec-llava-15) - [Mistral-Small-3.1](#sec-mistral-small-31) +- [Voxtral](#sec-voxtral) - [Gemma-3](#sec-gemma-3) - [Gemma-3n](#sec-gemma-3n) - [Qwen2-VL](#sec-qwen2-vl) - [Qwen2.5-VL](#sec-qwen25-vl) +- [SmolVLM2](#sec-smolvlm2) +- [LFM2-VL](#sec-lfm2-vl) ## Usage @@ -31,7 +34,7 @@ skip_prepare_dataset: true remove_unused_columns: false # leave columns in place as they are needed to handle image embeddings during training sample_packing: false # not yet supported with multimodal -chat_template: # see in next section +chat_template: # see in next section if specified # example dataset datasets: @@ -97,6 +100,16 @@ base_model: mistralai/Mistral-Small-3.1-24B-Instruct-2503 chat_template: mistral_v7_tekken ``` +### Voxtral {#sec-voxtral} + +::: {.callout-tip} +Please make sure to install audio lib via `pip3 install librosa==0.11.0 'mistral_common[audio]==1.8.3'` +::: + +```yaml +base_model: mistralai/Voxtral-Mini-3B-2507 +``` + ### Gemma-3 {#sec-gemma-3} ::: {.callout-tip} @@ -143,6 +156,26 @@ base_model: Qwen/Qwen2.5-VL-7B-Instruct chat_template: qwen2_vl # same as qwen2-vl ``` +### SmolVLM2 {#sec-smolvlm2} + +::: {.callout-tip} +Please make sure to install `num2words` via `pip3 install num2words==0.5.14` +::: + +```yaml +base_model: HuggingFaceTB/SmolVLM2-500M-Video-Instruct +``` + +### LFM2-VL {#sec-lfm2-vl} + +::: {.callout-warning} +Please uninstall `causal-conv1d` via `pip3 uninstall -y causal-conv1d` +::: + +```yaml +base_model: LiquidAI/LFM2-VL-450M +``` + ## Dataset Format For multi-modal datasets, we adopt an extended `chat_template` format similar to OpenAI's Message format. @@ -181,6 +214,20 @@ You may need to install `librosa` via `pip3 install librosa==0.11.0`. ::: +### Video + +::: {.callout-warning} + +This is not well tested at the moment. We welcome contributors! + +::: + +For video loading, you can use the following keys within `content` alongside `"type": "video"`: + +- `"path": "/path/to/video.mp4"` +- `"url": "https://example.com/video.mp4"` +- `"video": np.ndarray | list[PIL.Image.Image] | torch.Tensor` (or list of the aforementioned) + ### Example Here is an example of a multi-modal dataset: diff --git a/examples/LiquidAI/README.md b/examples/LiquidAI/README.md new file mode 100644 index 0000000000..96fc74a92b --- /dev/null +++ b/examples/LiquidAI/README.md @@ -0,0 +1,58 @@ +# Finetune Liquid Foundation Models 2 (LFM2) with Axolotl + +[Liquid Foundation Models 2 (LFM2)](https://huggingface.co/collections/LiquidAI/lfm2-686d721927015b2ad73eaa38) are a family of small, open-weight models from [Liquid AI](https://www.liquid.ai/) focused on quality, speed, and memory efficiency. Liquid AI released text-only [LFM2](https://huggingface.co/collections/LiquidAI/lfm2-686d721927015b2ad73eaa38) and text+vision [LFM2-VL](https://huggingface.co/collections/LiquidAI/lfm2-vl-68963bbc84a610f7638d5ffa) models. + +LFM2 features a new hybrid Liquid architecture with multiplicative gates, short-range convolutions, and grouped query attention, enabling fast training and inference. + +This guide shows how to fine-tune both the LFM2 and LFM2-VL models with Axolotl. + +## Getting Started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + + Here is an example of how to install from pip: + ```bash + # Ensure you have a compatible version of Pytorch installed + pip3 install packaging setuptools wheel ninja + pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' + ``` + +2. Run one of the finetuning examples below. + + **LFM2** + ```bash + # FFT SFT (1x48GB @ 25GiB) + axolotl train examples/LiquidAI/lfm2-350m-fft.yaml + ``` + + **LFM2-VL** + ```bash + # LoRA SFT (1x48GB @ 2.7GiB) + axolotl train examples/LiquidAI/lfm2-vl-lora.yaml + ``` + +### TIPS + +- **Installation Error**: If you encounter `ImportError: ... undefined symbol ...` or `ModuleNotFoundError: No module named 'causal_conv1d_cuda'`, the `causal-conv1d` package may have been installed incorrectly. Try uninstalling it: + ```bash + pip uninstall -y causal-conv1d + ``` + +- **Dataset Loading**: Read more on how to load your own dataset in our [documentation](https://docs.axolotl.ai/docs/dataset_loading.html). +- **Dataset Formats**: + - For LFM2 models, the dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + - For LFM2-VL models, Axolotl follows the multi-content Messages format. See our [Multimodal docs](https://docs.axolotl.ai/docs/multimodal.html#dataset-format) for details. + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) + +## Related Resources + +- [LFM2 Blog](https://www.liquid.ai/blog/liquid-foundation-models-v2-our-second-series-of-generative-ai-models) +- [LFM2-VL Blog](https://www.liquid.ai/blog/lfm2-vl-efficient-vision-language-models) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/lfm2/lfm2-350m-fft.yaml b/examples/LiquidAI/lfm2-350m-fft.yaml similarity index 96% rename from examples/lfm2/lfm2-350m-fft.yaml rename to examples/LiquidAI/lfm2-350m-fft.yaml index 16a0a028e1..d198150080 100644 --- a/examples/lfm2/lfm2-350m-fft.yaml +++ b/examples/LiquidAI/lfm2-350m-fft.yaml @@ -2,7 +2,6 @@ base_model: LiquidAI/LFM2-350M chunked_cross_entropy: true -chat_template: tokenizer_default eot_tokens: - "<|im_end|>" datasets: diff --git a/examples/LiquidAI/lfm2-vl-lora.yaml b/examples/LiquidAI/lfm2-vl-lora.yaml new file mode 100644 index 0000000000..7fee17f927 --- /dev/null +++ b/examples/LiquidAI/lfm2-vl-lora.yaml @@ -0,0 +1,58 @@ +base_model: LiquidAI/LFM2-VL-450M +trust_remote_code: true +model_type: AutoModelForImageTextToText +processor_type: AutoProcessor + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: lora +lora_model_dir: + +sequence_len: 8192 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +logging_steps: 1 +flash_attention: true +eager_attention: + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/lfm2/README.md b/examples/lfm2/README.md deleted file mode 100644 index eb9ca911f9..0000000000 --- a/examples/lfm2/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Liquid Foundation Models 2 - -LFM2 support in transformers exists in the main branch, but is not yet included in the transformers release. - -```bash -pip install --upgrade --no-deps --force-reinstall git+https://github.com/huggingface/transformers.git -``` diff --git a/examples/smolvlm2/README.md b/examples/smolvlm2/README.md new file mode 100644 index 0000000000..9c0ae4836d --- /dev/null +++ b/examples/smolvlm2/README.md @@ -0,0 +1,49 @@ +# Finetune SmolVLM2 with Axolotl + +[SmolVLM2](https://huggingface.co/collections/HuggingFaceTB/smolvlm2-smallest-video-lm-ever-67ab6b5e84bf8aaa60cb17c7) are a family of lightweight, open-source multimodal models from HuggingFace designed to analyze and understand video, image, and text content. + +These models are built for efficiency, making them well-suited for on-device applications where computational resources are limited. Models are available in multiple sizes, including 2.2B, 500M, and 256M. + +This guide shows how to fine-tune SmolVLM2 models with Axolotl. + +## Getting Started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + + Here is an example of how to install from pip: + ```bash + # Ensure you have a compatible version of Pytorch installed + pip3 install packaging setuptools wheel ninja + pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' + ``` + +2. Install an extra dependency: + + ```bash + pip3 install num2words==0.5.14 + ``` + +3. Run the finetuning example: + + ```bash + # LoRA SFT (1x48GB @ 6.8GiB) + axolotl train examples/smolvlm2/smolvlm2-2B-lora.yaml + ``` + +## TIPS + +- **Dataset Format**: For video finetuning, your dataset must be compatible with the multi-content Messages format. For more details, see our documentation on [Multimodal Formats](https://docs.axolotl.ai/docs/multimodal.html#dataset-format). +- **Dataset Loading**: Read more on how to prepare and load your own datasets in our [documentation](https://docs.axolotl.ai/docs/dataset_loading.html). + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) + +## Related Resources + +- [SmolVLM2 Blog](https://huggingface.co/blog/smolvlm2) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/smolvlm2/smolvlm2-2B-lora.yaml b/examples/smolvlm2/smolvlm2-2B-lora.yaml new file mode 100644 index 0000000000..1aeff408da --- /dev/null +++ b/examples/smolvlm2/smolvlm2-2B-lora.yaml @@ -0,0 +1,56 @@ +base_model: HuggingFaceTB/SmolVLM2-2.2B-Instruct +trust_remote_code: true +processor_type: AutoProcessor + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: lora +lora_model_dir: + +sequence_len: 8192 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'model.text_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +logging_steps: 1 +flash_attention: true +eager_attention: + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/src/axolotl/loaders/constants.py b/src/axolotl/loaders/constants.py index 3fabf9d940..4939cb28d0 100644 --- a/src/axolotl/loaders/constants.py +++ b/src/axolotl/loaders/constants.py @@ -1,26 +1,13 @@ """Shared constants for axolotl.loaders module""" -from transformers import ( - Gemma3ForConditionalGeneration, - Gemma3nForConditionalGeneration, - Llama4ForConditionalGeneration, - LlavaForConditionalGeneration, - Mistral3ForConditionalGeneration, - MllamaForConditionalGeneration, - Qwen2_5_VLForConditionalGeneration, - Qwen2VLForConditionalGeneration, +from transformers import AutoModelForImageTextToText +from transformers.models.auto.modeling_auto import ( + MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES, ) -MULTIMODAL_AUTO_MODEL_MAPPING = { - "mllama": MllamaForConditionalGeneration, - "llama4": Llama4ForConditionalGeneration, - "llava": LlavaForConditionalGeneration, - "qwen2_vl": Qwen2VLForConditionalGeneration, - "qwen2_5_vl": Qwen2_5_VLForConditionalGeneration, - "mistral3": Mistral3ForConditionalGeneration, - "gemma3": Gemma3ForConditionalGeneration, - "gemma3n": Gemma3nForConditionalGeneration, -} +MULTIMODAL_AUTO_MODEL_MAPPING = dict(MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES) + +MULTIMODAL_AUTO_MODEL_MAPPING["lfm2-vl"] = AutoModelForImageTextToText try: from transformers import VoxtralForConditionalGeneration diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 6bf1f149b5..53ae428a23 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -25,6 +25,7 @@ from torch.distributed import DeviceMesh from transformers import ( AutoModelForCausalLM, + AutoModelForImageTextToText, AutoModelForVision2Seq, AwqConfig, BitsAndBytesConfig, @@ -212,6 +213,7 @@ def _apply_pre_model_load_setup(self): self.model_kwargs["use_kernels"] = self.cfg.use_kernels self._set_quantization_config() self._set_attention_config() + self._check_model_requirements() def _apply_post_model_load_setup(self): """Configure the model after it has been loaded.""" @@ -432,6 +434,8 @@ def _set_auto_model_loader(self): self.auto_model_loader = MULTIMODAL_AUTO_MODEL_MAPPING.get( self.model_config.model_type, AutoModelForVision2Seq ) + if isinstance(self.auto_model_loader, str): + self.auto_model_loader = AutoModelForImageTextToText def _set_device_map_config(self): """Setup `device_map` according to config""" @@ -628,6 +632,16 @@ def _set_attention_config(self): if self.cfg.low_cpu_mem_usage: self.model_kwargs["low_cpu_mem_usage"] = True + def _check_model_requirements(self): + if self.cfg.model_config_type in ["lfm2-vl", "lfm2"]: + from transformers.utils.import_utils import is_causal_conv1d_available + + if is_causal_conv1d_available(): + raise ImportError( + "The 'causal-conv1d' package is installed but causes compatibility issues with LFM2 models. " + "Please uninstall it by running: `pip uninstall -y causal-conv1d`" + ) + def _configure_zero3_memory_efficient_loading( self, ) -> HfTrainerDeepSpeedConfig | None: diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 4cc5e85a1e..31597d5a62 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -6,7 +6,7 @@ from PIL import Image, ImageOps from PIL.Image import Resampling from torch import Tensor, zeros_like -from transformers import ProcessorMixin, VoxtralProcessor +from transformers import ProcessorMixin, SmolVLMProcessor, VoxtralProcessor from transformers.image_utils import load_image from axolotl.utils.dict import remove_none_values @@ -138,7 +138,7 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: image_key = key break - # if the image key exists, add the image to the first message + # if the image key exists, add the image to the first user message if image_key is not None and processed_example[image_key] is not None: # TODO: check if it's normal to be single image only for common datasets # From observation, it's usually a list of single image but some datasets may have several columns for images @@ -179,26 +179,34 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: # Look for any image type in the first message # some dataset have an {type: "image"} in the first message + msg_ind_to_add = None ind_to_add = None + first_user_idx = None - for i, content in enumerate( - processed_example["messages"][0]["content"] - ): - # Usually datasets created with image columns, don't have it in the messages itself - if content["type"] == "image" and all( - k not in content for k in ["image", "url", "path", "base64"] + for msg_idx, msg_content in enumerate(processed_example["messages"]): + if first_user_idx is None and msg_content["role"] == "user": + first_user_idx = msg_idx + for i, content in enumerate( + processed_example["messages"][msg_idx]["content"] ): - ind_to_add = i - break + # Usually datasets created with image columns, don't have it in the messages itself + if content["type"] == "image" and all( + k not in content for k in ["image", "url", "path", "base64"] + ): + msg_ind_to_add = msg_idx + ind_to_add = i + break # If an image type is found, add the image to that index - if ind_to_add is not None: - processed_example["messages"][0]["content"][ind_to_add][ - "image" - ] = image_value + if ind_to_add is not None and msg_ind_to_add is not None: + processed_example["messages"][msg_ind_to_add]["content"][ + ind_to_add + ]["image"] = image_value else: - # if no image type is found, add it to end of the first message - processed_example["messages"][0]["content"].append( + # if no image type is found, add it to end of the first user message + if first_user_idx is None: + first_user_idx = 0 + processed_example["messages"][first_user_idx]["content"].append( { "type": "image", "image": image_value, @@ -395,6 +403,24 @@ def process_labels(self, input_ids): return labels +class SmolVLM2ProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for SmolVLM2""" + + def __init__( + self, + processor: ProcessorMixin, + chat_template: Optional[str] = None, + image_size: int | tuple[int, int] | None = None, + image_resize_algorithm: Resampling | None = None, + ): + super().__init__(processor, chat_template, image_size, image_resize_algorithm) + self.image_token = "" # nosec + + self.image_token_id = processor.tokenizer.additional_special_tokens_ids[ + processor.tokenizer.additional_special_tokens.index(self.image_token) + ] + + def get_processing_strategy( processor: ProcessorMixin, chat_template, @@ -402,32 +428,43 @@ def get_processing_strategy( image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, ): + processing_kwargs = { + "processor": processor, + "chat_template": chat_template, + "image_size": image_size, + "image_resize_algorithm": image_resize_algorithm, + } + + if chat_template_type in [None, "tokenizer_default"] and hasattr( + processor.tokenizer, "chat_template" + ): + processing_kwargs["chat_template"] = processor.tokenizer.chat_template + if chat_template_type == "qwen2_vl": return Qwen2VLProcessingStrategy( - processor, chat_template, image_size, image_resize_algorithm + **processing_kwargs, ) if chat_template_type == "gemma3": return Gemma3ProcessingStrategy( - processor, chat_template, image_size, image_resize_algorithm + **processing_kwargs, ) if chat_template_type == "gemma3n": return Gemma3nProcessingStrategy( - processor, chat_template, image_size, image_resize_algorithm - ) - if chat_template_type in [ - "llama3_2_vision", - "llama4", - "llava", - "mistral_v7_tekken", - "pixtral", - ]: - return ProcessingStrategy( - processor, chat_template, image_size, image_resize_algorithm + **processing_kwargs, ) if isinstance(processor, VoxtralProcessor): return VoxtralProcessingStrategy( - processor, chat_template, image_size, image_resize_algorithm + **processing_kwargs, + ) + + if isinstance(processor, SmolVLMProcessor): + return SmolVLM2ProcessingStrategy( + **processing_kwargs, ) - raise ValueError(f"Unsupported chat template type: {chat_template_type}") + # llama3_2_vision, llama4, llava + # mistral_v7_tekken, pixtral, lfm2vl + return ProcessingStrategy( + **processing_kwargs, + ) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 8241dd385a..f927b7fcbf 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -129,13 +129,21 @@ def build_prompt( images=images, return_tensors="pt", ) + if hasattr(batch, "to_dict"): + batch = batch.to_dict() + else: + batch = dict(batch) + # workaround since processor works in batches instead of single examples + out = {} for k, val in batch.items(): - if k in ["pixel_values"]: - batch[k] = val.tolist() + if hasattr(val, "tolist"): + out[k] = ( + val.tolist() if k == "pixel_values" else val.squeeze(0).tolist() + ) else: - batch[k] = val.squeeze().tolist() - return batch + out[k] = val + return out return self.tokenizer.apply_chat_template( conversation, @@ -433,10 +441,13 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: tokenized_prompt["attention_mask"] = [1] * len(input_ids) else: input_ids = tokenized_res["input_ids"] - tokenized_prompt = tokenized_res + tokenized_prompt = dict(tokenized_res) if not self.train_on_inputs: - user_prompt_len = len(prompt_ids) + if isinstance(prompt_ids, dict): + user_prompt_len = len(prompt_ids["input_ids"]) + else: + user_prompt_len = len(prompt_ids) labels = [-100] * user_prompt_len + input_ids[user_prompt_len:] else: labels = input_ids diff --git a/src/axolotl/utils/collators/mm_chat.py b/src/axolotl/utils/collators/mm_chat.py index 0075d48301..542918527c 100644 --- a/src/axolotl/utils/collators/mm_chat.py +++ b/src/axolotl/utils/collators/mm_chat.py @@ -5,7 +5,6 @@ from dataclasses import dataclass from typing import Any, Optional, Union -import torch from torch import Tensor from transformers import PreTrainedTokenizerBase from transformers.data.data_collator import DataCollatorMixin @@ -42,62 +41,19 @@ def process_rows( examples = self.processing_strategy(examples) # Initialize batch - batch: dict[str, Any] = {} - - # Process each example - for example in examples: - # Apply chat template to process the example - # This method requires transformers>=4.49.0 - result = self.processing_strategy.processor.apply_chat_template( - example["messages"], - add_generation_prompt=False, - tokenize=True, - return_tensors="pt", - padding=True, - return_dict=True, - chat_template=self.processing_strategy.chat_template, - ) - - # TODO: Check if need handling for len(input_ids) > sequence_len - - # Add the processed tensors to our batch - for key in result.keys(): - if key not in batch: - batch[key] = [] - - batch[key].append(result[key].squeeze(0)) - - # Pad sequences to the same length - input_ids = torch.nn.utils.rnn.pad_sequence( - batch["input_ids"], - batch_first=True, - padding_value=self.tokenizer.pad_token_id, + messages = [ex["messages"] for ex in examples] + + batch = self.processing_strategy.processor.apply_chat_template( + messages, + add_generation_prompt=False, + tokenize=True, + return_tensors="pt", + padding=True, + return_dict=True, + chat_template=self.processing_strategy.chat_template, ) - attention_mask = torch.nn.utils.rnn.pad_sequence( - batch["attention_mask"], batch_first=True, padding_value=0 - ) - - # Create the final batch - final_batch = { - "input_ids": input_ids, - "attention_mask": attention_mask, - } - - for key, val in batch.items(): - if key in ["input_ids", "attention_mask"]: - continue - - if key in ["token_type_ids", "cross_attention_mask"]: - final_batch[key] = torch.nn.utils.rnn.pad_sequence( - val, batch_first=True, padding_value=0 - ) - else: - final_batch[key] = torch.stack(val) - # Process the labels - final_batch["labels"] = self.processing_strategy.process_labels( - final_batch["input_ids"] - ) + batch["labels"] = self.processing_strategy.process_labels(batch["input_ids"]) - return final_batch + return batch diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 5931fe148a..939ed5c1ca 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -147,7 +147,11 @@ def require_hopper(test_case): def check_tensorboard( - temp_run_dir: str, tag: str, lt_val: float, assertion_err: str + temp_run_dir: str, + tag: str, + lt_val: float, + assertion_err: str, + rtol: float = 0.02, ) -> None: """ helper function to parse and check tensorboard logs @@ -157,6 +161,7 @@ def check_tensorboard( reader = SummaryReader(event_file) df = reader.scalars # pylint: disable=invalid-name df = df[(df.tag == tag)] # pylint: disable=invalid-name + lt_val = (1 + rtol) * lt_val if "%s" in assertion_err: assert df.value.values[-1] < lt_val, assertion_err % df.value.values[-1] else: From ecbe8b2b61bd24c8a6de662ad0ec3ca733feb4b1 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 15 Aug 2025 21:25:01 -0400 Subject: [PATCH 0923/1405] [GPT-OSS] improve FSDP shard merging and documentation for GPT-OSS (#3073) * improve fsdp shard merging * improve logging * update information on merging and inferencing GPT-OSS * cleanup readme * automate cleanup of FSDP prefix * import GRPO only if necessary * only modify config.json on rank0 * merge final checkpoint at end of training * prevent circular import * Fix saving for sharded state dict * devx, move merged to output dir * move import back to top * Fix stuck merge * fix conditionals from pr feedback and add test --- examples/gpt-oss/README.md | 33 ++++++++- .../gpt-oss-120b-fft-fsdp2-offload.yaml | 1 + src/axolotl/cli/merge_sharded_fsdp_weights.py | 27 ++++++- src/axolotl/core/trainers/__init__.py | 1 - src/axolotl/train.py | 73 +++++++++++-------- src/axolotl/utils/train.py | 45 ++++++++++++ tests/utils/test_train.py | 24 ++++++ 7 files changed, 170 insertions(+), 34 deletions(-) create mode 100644 src/axolotl/utils/train.py create mode 100644 tests/utils/test_train.py diff --git a/examples/gpt-oss/README.md b/examples/gpt-oss/README.md index 6dadb82301..9db5e98870 100644 --- a/examples/gpt-oss/README.md +++ b/examples/gpt-oss/README.md @@ -33,13 +33,44 @@ Note: Memory usage taken from `device_mem_reserved(gib)` from logs. ### Training 120B -On 8xH100s +On 8xH100s, make sure you have ~3TB of free disk space. With each checkpoint clocking in at ~720GB, along with the base +model, and final model output, you may need at least 3TB of free disk space to keep at least 2 checkpoints. ```bash # FFT SFT with offloading (8x80GB @ ~49GiB/GPU) axolotl train examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml ``` +ERRATA: Transformers saves the model Architecture prefixed with `FSDP` which needs to be manually renamed in `config.json`. +See https://github.com/huggingface/transformers/pull/40207 for the status of this issue. + +```bash +sed -i 's/FSDPGptOssForCausalLM/GptOssForCausalLM/g' ./outputs/gpt-oss-out/config.json +``` + +When using SHARDED_STATE_DICT with FSDP, the final checkpoint should automatically merge the sharded weights to your +configured `output_dir`. However, if that step fails due to a disk space error, you can take an additional step to +merge the sharded weights. This step will automatically determine the last checkpoint directory and merge the sharded +weights to `{output_dir}/merged`. + +```bash +axolotl merge-sharded-fsdp-weights examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml +mv ./outputs/gpt-oss-out/merged/* ./outputs/gpt-oss-out/ +``` + + +### Inferencing your fine-tuned model + +GPT-OSS support in vLLM does not exist in a stable release yet. See https://x.com/MaziyarPanahi/status/1955741905515323425 +for more information about using a special vllm-openai docker image for inferencing with vLLM. + +SGLang has 0-day support in main, see https://github.com/sgl-project/sglang/issues/8833 for infomation on installing +SGLang from source. Once you've installed SGLang, run the following command to launch a SGLang server: + +```bash +python3 -m sglang.launch_server --model ./outputs/gpt-oss-out/ --served-model-name axolotl/gpt-oss-120b --host 0.0.0.0 --port 8888 --tp 8 +``` + ### Tool use GPT-OSS has a comprehensive tool understanding. Axolotl supports tool calling datasets for Supervised Fine-tuning. diff --git a/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml b/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml index 4a9d51fdfd..4b4fbd89b5 100644 --- a/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml +++ b/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml @@ -20,6 +20,7 @@ datasets: dataset_prepared_path: last_run_prepared val_set_size: 0 output_dir: ./outputs/gpt-oss-out/ +save_total_limit: 2 # the 120B model can use up to 720GB of disk space per checkpoint, so let's only keep the last 2 sequence_len: 4096 sample_packing: true diff --git a/src/axolotl/cli/merge_sharded_fsdp_weights.py b/src/axolotl/cli/merge_sharded_fsdp_weights.py index c08d30ec8e..c99f37fb1d 100644 --- a/src/axolotl/cli/merge_sharded_fsdp_weights.py +++ b/src/axolotl/cli/merge_sharded_fsdp_weights.py @@ -10,6 +10,7 @@ import torch import torch.distributed.checkpoint as dist_cp import torch.distributed.checkpoint.format_utils as dist_cp_format_utils +from accelerate import PartialState from accelerate.utils import ( SAFE_WEIGHTS_INDEX_NAME, SAFE_WEIGHTS_NAME, @@ -23,6 +24,7 @@ from axolotl.cli.config import load_cfg from axolotl.utils.logging import get_logger +from axolotl.utils.train import determine_last_checkpoint LOG = get_logger(__name__) @@ -143,7 +145,6 @@ def merge_fsdp_weights( ValueError: If torch version < 2.3.0, or if `checkpoint_dir` does not exist. """ checkpoint_dir_ = Path(checkpoint_dir) - from accelerate.state import PartialState if not is_torch_version(">=", "2.3.0"): raise ValueError("`merge_fsdp_weights` requires PyTorch >= 2.3.0`") @@ -180,7 +181,6 @@ def merge_fsdp_weights( if remove_checkpoint_dir: LOG.info(f"Removing old checkpoint directory {checkpoint_dir_}") shutil.rmtree(checkpoint_dir_) - state.wait_for_everyone() def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): @@ -195,11 +195,32 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): parsed_cfg = load_cfg(config, **kwargs) fsdp_dir = Path(parsed_cfg.output_dir) / "pytorch_model_fsdp_0" + if not fsdp_dir.exists(): + checkpoint_dir = determine_last_checkpoint(parsed_cfg, update=False) + if checkpoint_dir: + fsdp_dir = Path(checkpoint_dir) / "pytorch_model_fsdp_0" + if not fsdp_dir.exists(): + raise ValueError( + f"Could not find FSDP checkpoint `pytorch_model_fsdp_0` in {checkpoint_dir}" + ) + + output_path = str(Path(parsed_cfg.output_dir) / "merged") merge_fsdp_weights( checkpoint_dir=str(fsdp_dir), - output_path=str(Path(parsed_cfg.output_dir) / "merged"), + output_path=output_path, safe_serialization=True, ) + state = PartialState() + state.wait_for_everyone() + LOG.info( + f"FSDP SHARDED_STATE_DICT weights successfully merged to: {output_path}", + main_process_only=True, + ) + LOG.info( + "Merged weights are only the safetensors and doesn't include the model configuration " + f"or tokenizer which may be found in {parsed_cfg.output_dir}.", + main_process_only=True, + ) if __name__ == "__main__": diff --git a/src/axolotl/core/trainers/__init__.py b/src/axolotl/core/trainers/__init__.py index 5f97e387a5..a9cda4efc5 100644 --- a/src/axolotl/core/trainers/__init__.py +++ b/src/axolotl/core/trainers/__init__.py @@ -5,7 +5,6 @@ from .base import AxolotlTrainer from .dpo.trainer import AxolotlDPOTrainer -from .grpo.trainer import AxolotlGRPOSequenceParallelTrainer, AxolotlGRPOTrainer from .mamba import AxolotlMambaTrainer from .trl import ( AxolotlCPOTrainer, diff --git a/src/axolotl/train.py b/src/axolotl/train.py index e8a2cbabe7..8005389f16 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -4,11 +4,14 @@ import importlib import inspect +import json import os +import shutil import signal import sys import typing import weakref +from collections import OrderedDict from contextlib import ExitStack from pathlib import Path from typing import Any, Dict @@ -38,6 +41,7 @@ from axolotl.utils.freeze import freeze_layers_except from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import RLType +from axolotl.utils.train import determine_last_checkpoint from axolotl.utils.trainer import setup_trainer try: @@ -46,7 +50,7 @@ BetterTransformer = None if typing.TYPE_CHECKING: - from axolotl.core.trainer_builder import HFCausalTrainerBuilder, HFRLTrainerBuilder + from axolotl.core.builders import HFCausalTrainerBuilder, HFRLTrainerBuilder LOG = get_logger(__name__) @@ -124,32 +128,6 @@ def setup_reference_model( return model_ref -def determine_resume_checkpoint(cfg: DictDefault) -> str | None: - """ - Determine the checkpoint to resume from based on configuration. - - Args: - cfg: Dictionary mapping `axolotl` config keys to values. - - Returns: - Path to the checkpoint to resume from, or `None` if not resuming. - """ - if cfg.resume_from_checkpoint is None and cfg.auto_resume_from_checkpoints: - possible_checkpoints = [ - str(cp) for cp in Path(cfg.output_dir).glob("checkpoint-*") - ] - if len(possible_checkpoints) > 0: - sorted_paths = sorted( - possible_checkpoints, - key=lambda path: int(path.split("-")[-1]), - ) - cfg.resume_from_checkpoint = sorted_paths[-1] - LOG.info( - f"Using Auto-resume functionality to start with checkpoint at {cfg.resume_from_checkpoint}" - ) - return cfg.resume_from_checkpoint - - def setup_signal_handler( cfg: DictDefault, model: PreTrainedModel, safe_serialization: bool ): @@ -282,12 +260,49 @@ def save_trained_model( else: state_dict_type = cfg.fsdp_config.state_dict_type trainer.accelerator.state.fsdp_plugin.set_state_dict_type(state_dict_type) - trainer.save_model(cfg.output_dir) + trainer.save_model(cfg.output_dir) # only handles FULL_STATE_DICT if state_dict_type == "SHARDED_STATE_DICT": LOG.info( "The final model was saved with a sharded state dict. Please ensure you merge " "the sharded weights with `merge-sharded-fsdp-weights`." ) + checkpoint_dir = determine_last_checkpoint(cfg, update=False) + if ( + not (Path(cfg.output_dir) / "model.safetensors.index.json").exists() + and checkpoint_dir + ): + # import here to prevent circular import + from axolotl.cli.merge_sharded_fsdp_weights import merge_fsdp_weights + + fsdp_dir = Path(checkpoint_dir) / "pytorch_model_fsdp_0" + merged_path = str(Path(cfg.output_dir) / "merged") + merge_fsdp_weights( + checkpoint_dir=str(fsdp_dir), + output_path=merged_path, + safe_serialization=True, + ) + trainer.accelerator.wait_for_everyone() + if trainer.accelerator.is_main_process: + # move all files in merged_path to cfg.output_dir + for merged_file in Path(merged_path).iterdir(): + shutil.move(str(merged_file), cfg.output_dir) + shutil.rmtree(merged_path) # remove what should be an empty dir + # TODO(wing):see https://github.com/huggingface/transformers/pull/40207 + # cleanup the FSDP prefix in the model config.json + if trainer.accelerator.is_main_process: + with open( + Path(cfg.output_dir) / "config.json", "r", encoding="utf-8" + ) as config_file_io: + # read the model config as an OrderedDict + config = json.load(config_file_io, object_pairs_hook=OrderedDict) + config["architectures"] = [ + name.lstrip("FSDP") for name in config["architectures"] + ] + # write the updated model config back + with open( + os.path.join(cfg.output_dir, "config.json"), "w", encoding="utf-8" + ) as config_file_io: + json.dump(config, config_file_io, indent=2) elif cfg.deepspeed and is_deepspeed_zero3_enabled(): # Copied over from: https://github.com/huggingface/accelerate/blob/5ae611118057232f441055f7ef9ba0b0f2b8d533/docs/source/usage_guides/deepspeed.md#saving-and-loading trainer.accelerator.wait_for_everyone() @@ -564,7 +579,7 @@ def train( setup_model_card(cfg) # Execute the training - resume_from_checkpoint = determine_resume_checkpoint(cfg) + resume_from_checkpoint = determine_last_checkpoint(cfg) execute_training(cfg, trainer, resume_from_checkpoint) # clear cache diff --git a/src/axolotl/utils/train.py b/src/axolotl/utils/train.py new file mode 100644 index 0000000000..1393459d9b --- /dev/null +++ b/src/axolotl/utils/train.py @@ -0,0 +1,45 @@ +"""Training utils for checkpoints""" + +from pathlib import Path + +from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def determine_last_checkpoint(cfg: DictDefault, update: bool = True) -> str | None: + """ + Determine the checkpoint to resume from based on configuration. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + update: Whether to update the config with the determined checkpoint + + Returns: + Path to the checkpoint to resume from, or `None` if not resuming. + """ + last_checkpoint = None + checkpoints = sorted( + ( + p + for p in Path(cfg.output_dir).glob("checkpoint-*") + if p.name.split("-")[-1].isdigit() + ), + key=lambda p: int(p.name.split("-")[-1]), + ) + if checkpoints: + last_checkpoint = str(checkpoints[-1]) + if not update: + return last_checkpoint + + if ( + cfg.resume_from_checkpoint is None + and cfg.auto_resume_from_checkpoints + and last_checkpoint is not None + ): + cfg.resume_from_checkpoint = last_checkpoint + LOG.info( + f"Using Auto-resume functionality to start with checkpoint at {cfg.resume_from_checkpoint}" + ) + return cfg.resume_from_checkpoint diff --git a/tests/utils/test_train.py b/tests/utils/test_train.py new file mode 100644 index 0000000000..a1f6f6088d --- /dev/null +++ b/tests/utils/test_train.py @@ -0,0 +1,24 @@ +"""test for train checkpoint utils""" + +import os + +from axolotl.utils.dict import DictDefault +from axolotl.utils.train import determine_last_checkpoint + + +def test_determine_last_checkpoint(temp_dir): + cfg = DictDefault( + output_dir=temp_dir, + ) + for cpt_idx in [1, 9, 10, 20]: + os.makedirs( + os.path.join(cfg.output_dir, f"checkpoint-{cpt_idx}"), exist_ok=True + ) + + last_checkpoint = determine_last_checkpoint(cfg, update=False) + assert last_checkpoint == os.path.join(cfg.output_dir, "checkpoint-20") + + cfg.resume_from_checkpoint = None + cfg.auto_resume_from_checkpoints = True + determine_last_checkpoint(cfg, update=True) + assert cfg.resume_from_checkpoint == os.path.join(cfg.output_dir, "checkpoint-20") From 0eef385b1ae3bc436c2b5f4230c24a6ad7372afa Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:09:13 +0530 Subject: [PATCH 0924/1405] [feat] truncation support with excess_length_strategy (#3068) [skip ci] * feat:truncation support with excess_len * pre-commit * excess_length_strategy * requested changes * lint * added handle_long_seq_in_dataset in sft * comments improved --- src/axolotl/utils/data/sft.py | 6 ++-- src/axolotl/utils/data/utils.py | 53 +++++++++++++++++++++++++++-- src/axolotl/utils/schemas/config.py | 6 ++++ tests/test_packed_batch_sampler.py | 4 +-- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 975f26e715..2ae7d9052f 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -28,7 +28,7 @@ ) from axolotl.utils.data.utils import ( deduplicate_and_log_datasets, - drop_long_seq_in_dataset, + handle_long_seq_in_dataset, retry_on_request_exceptions, ) from axolotl.utils.data.wrappers import get_dataset_wrapper @@ -339,9 +339,9 @@ def _load_raw_datasets( if not cfg.skip_prepare_dataset: if split == "test" and cfg.eval_sequence_len: - dataset = drop_long_seq_in_dataset(dataset, cfg.eval_sequence_len, cfg) + dataset = handle_long_seq_in_dataset(dataset, cfg.eval_sequence_len, cfg) else: - dataset = drop_long_seq_in_dataset(dataset, cfg.sequence_len, cfg) + dataset = handle_long_seq_in_dataset(dataset, cfg.sequence_len, cfg) if cfg.sample_packing: dataset, _ = process_datasets_for_packing(cfg, dataset, None) diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index c0efb7a428..856a609c74 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -148,7 +148,36 @@ def deduplicate_and_log_datasets( return dataset, other_dataset -def drop_long_seq_in_dataset( +def truncate_long_seq(sample, sequence_len=2048, min_sequence_len=2): + """ + Truncate samples whose sequence length is too long (> sequence_len) + or drop those too short (< min_sequence_len). + """ + min_sequence_len = min_sequence_len or 2 + + input_ids = sample["input_ids"] + results = [] + + # Batched (input_ids is a list of lists) + for i, seq in enumerate(input_ids): + length = len(seq) + if length < min_sequence_len: + results.append(False) + elif length > sequence_len: + sample["input_ids"][i] = seq[:sequence_len] + if "attention_mask" in sample: + sample["attention_mask"][i] = sample["attention_mask"][i][:sequence_len] + if "labels" in sample: + sample["labels"][i] = sample["labels"][i][:sequence_len] + if "position_ids" in sample: + sample["position_ids"][i] = sample["position_ids"][i][:sequence_len] + results.append(True) + else: + results.append(True) + return results + + +def handle_long_seq_in_dataset( dataset: Dataset, sequence_len: int, cfg: DictDefault ) -> Dataset: """Remove sequences longer than configured maximum from dataset. @@ -192,8 +221,21 @@ def drop_long_seq_in_dataset( if filter_map_kwargs: drop_long_kwargs["desc"] = f"Dropping Long Sequences (>{sequence_len})" + excess_length_strategy = (cfg.excess_length_strategy or "drop").lower() + if excess_length_strategy == "truncate": + process_fn = functools.partial( + truncate_long_seq, + sequence_len=sequence_len, + min_sequence_len=cfg.min_sample_len, + ) + drop_long_kwargs["desc"] = ( + f"Truncating/Filtering Sequences (target_len={sequence_len})" + ) + else: + process_fn = drop_long + dataset = dataset.filter( - drop_long, + process_fn, batched=True, **filter_map_kwargs, **drop_long_kwargs, @@ -201,6 +243,11 @@ def drop_long_seq_in_dataset( if prior_len: dropped = prior_len - len(dataset) if dropped: - LOG.warning(f"Dropped {dropped} long samples from dataset") + action = ( + "truncated/filtered" + if excess_length_strategy == "truncate" + else "dropped" + ) + LOG.warning(f"{action.title()} {dropped} samples from dataset") return dataset diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 21e99c0483..a607b3dca2 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -414,6 +414,12 @@ class AxolotlInputConfig( "description": "The maximum length of an input to train with, this should typically be less than 2048 as most models have a token/context limit of 2048" }, ) + excess_length_strategy: Literal["drop", "truncate"] | None = Field( + default=None, + json_schema_extra={ + "description": "What to do when a tokenized row exceeds sequence_len. 'drop' removes the row; 'truncate' slices tensors to sequence_len. Defaults to 'drop' for backward compatibility." + }, + ) eval_sequence_len: int | None = Field( default=None, json_schema_extra={ diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index 7cb645db7e..47894a35b7 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -8,7 +8,7 @@ from axolotl.datasets import TokenizedPromptDataset from axolotl.prompt_strategies.completion import load from axolotl.utils.collators import V2BatchSamplerDataCollatorForSeq2Seq -from axolotl.utils.data.utils import drop_long_seq_in_dataset +from axolotl.utils.data.utils import handle_long_seq_in_dataset from axolotl.utils.dict import DictDefault from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths @@ -70,7 +70,7 @@ def test_packing( ) train_dataset = concatenate_datasets([dataset_wrapper]) - train_dataset = drop_long_seq_in_dataset(train_dataset, cfg.sequence_len, cfg) + train_dataset = handle_long_seq_in_dataset(train_dataset, cfg.sequence_len, cfg) lengths = get_dataset_lengths(train_dataset) batch_sampler = MultipackBatchSampler( From c10eb811fac677ee2d7c0c38a44d084fcc613cdc Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:14:37 +0530 Subject: [PATCH 0925/1405] data_parallel_size in in VllmserveCliArgs (#3074) * data_parallel_size in in VllmserveCliArgs * moved to 43 --- src/axolotl/cli/args.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py index 31d854d415..9bb544affe 100644 --- a/src/axolotl/cli/args.py +++ b/src/axolotl/cli/args.py @@ -40,6 +40,12 @@ class VllmServeCliArgs: default=None, metadata={"help": "Number of tensor parallel workers to use."}, ) + data_parallel_size: Optional[int] = field( + default=None, + metadata={ + "help": "Number of data parallel workers to use for vLLM serving. This controls how many model replicas are used for parallel inference." + }, + ) host: Optional[str] = field( default=None, # nosec B104 metadata={"help": "Host address to run the server on."}, From 05cedbfb1e8a125adcfaa0a03d1b9b2a3fa97e80 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 19 Aug 2025 13:30:37 -0400 Subject: [PATCH 0926/1405] add baseten info for gpt-oss recipe (#3078) * add bsaeten info for gpt-oss recipe * incorporate PR review --- examples/gpt-oss/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/gpt-oss/README.md b/examples/gpt-oss/README.md index 9db5e98870..98f3ea892b 100644 --- a/examples/gpt-oss/README.md +++ b/examples/gpt-oss/README.md @@ -41,6 +41,12 @@ model, and final model output, you may need at least 3TB of free disk space to k axolotl train examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml ``` +To simplify fine-tuning across 2 nodes × 8x H100 (80GB) GPUs, we've partnered with [Baseten](https://baseten.co) to showcase multi-node +training of the 120B model using Baseten Truss. You can read more about this recipe on +[Baseten's blog](https://www.baseten.co/blog/how-to-fine-tune-gpt-oss-120b-with-baseten-and-axolotl/). The recipe can +be found on their +[GitHub](https://github.com/basetenlabs/ml-cookbook/tree/main/examples/oss-gpt-120b-axolotl/training). + ERRATA: Transformers saves the model Architecture prefixed with `FSDP` which needs to be manually renamed in `config.json`. See https://github.com/huggingface/transformers/pull/40207 for the status of this issue. From 050210e637a7ca2fdb65491eced13bd4d1ce5d10 Mon Sep 17 00:00:00 2001 From: goggle Date: Wed, 20 Aug 2025 09:25:20 +0900 Subject: [PATCH 0927/1405] fix: Sweep runs overwrite each other because output_dir from base config is reused (#3080) * refactor: improve output_dir handling in generate_config_files * fix typo * cli: harden sweep output_dir handling with base fallback - Ensure sweep permutations always resolve a valid output_dir - Default to ./model-out if neither permutation nor base config sets output_dir - Append sweepXXXX suffix consistently for each permutation - Prevent Path(None) TypeError and improve robustness of sweep config generation * fix typo * chore: lint --------- Co-authored-by: Wing Lian --- src/axolotl/cli/utils/sweeps.py | 3 ++- src/axolotl/cli/utils/train.py | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/axolotl/cli/utils/sweeps.py b/src/axolotl/cli/utils/sweeps.py index d21664964c..bb1368cf6e 100644 --- a/src/axolotl/cli/utils/sweeps.py +++ b/src/axolotl/cli/utils/sweeps.py @@ -3,11 +3,12 @@ import random from copy import deepcopy from itertools import product +from typing import Any def generate_sweep_configs( base_config: dict[str, list], sweeps_config: dict[str, list] -) -> list[dict[str, list]]: +) -> list[dict[str, Any]]: """ Recursively generates all possible configurations by applying sweeps to the base config. diff --git a/src/axolotl/cli/utils/train.py b/src/axolotl/cli/utils/train.py index 31b0bcf585..b133d7271c 100644 --- a/src/axolotl/cli/utils/train.py +++ b/src/axolotl/cli/utils/train.py @@ -4,6 +4,7 @@ import subprocess # nosec import sys import tempfile +from pathlib import Path from typing import Any, Iterator, Literal import yaml @@ -88,7 +89,12 @@ def generate_config_files(config: str, sweep: str | None) -> Iterator[tuple[str, # Generate all possible configurations permutations = generate_sweep_configs(base_config, sweep_config) is_group = len(permutations) > 1 - for permutation in permutations: + base_output_dir = base_config.get("output_dir", "./model-out") + for idx, permutation in enumerate(permutations, start=1): + permutation_dir = Path(permutation.get("output_dir", base_output_dir)) + permutation_id = f"sweep{idx:04d}" + permutation["output_dir"] = str(permutation_dir / permutation_id) + # pylint: disable=consider-using-with temp_file = tempfile.NamedTemporaryFile( mode="w", From 06eaf6c448a52b9119ef48ad16ccb7286aac41c3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 20 Aug 2025 08:52:26 -0400 Subject: [PATCH 0928/1405] misc fixes (#3085) --- examples/gpt-oss/README.md | 14 ++++++++++++++ .../gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml | 2 +- examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml | 2 +- src/axolotl/cli/cloud/modal_.py | 2 +- src/axolotl/cli/inference.py | 2 +- src/axolotl/cli/preprocess.py | 3 ++- 6 files changed, 20 insertions(+), 5 deletions(-) diff --git a/examples/gpt-oss/README.md b/examples/gpt-oss/README.md index 98f3ea892b..0aa04a71cb 100644 --- a/examples/gpt-oss/README.md +++ b/examples/gpt-oss/README.md @@ -67,9 +67,23 @@ mv ./outputs/gpt-oss-out/merged/* ./outputs/gpt-oss-out/ ### Inferencing your fine-tuned model +#### vLLM + GPT-OSS support in vLLM does not exist in a stable release yet. See https://x.com/MaziyarPanahi/status/1955741905515323425 for more information about using a special vllm-openai docker image for inferencing with vLLM. +Optionally, vLLM can be installed from nightly: + +```bash +pip install --no-build-isolation --pre -U vllm --extra-index-url https://wheels.vllm.ai/nightly +``` +and the vLLM server can be started with the following command (modify `--tensor-parallel-size 8` to match your environment): +```bash +vllm serve ./outputs/gpt-oss-out/ --served-model-name axolotl/gpt-oss-20b --host 0.0.0.0 --port 8888 --tensor-parallel-size 8 +``` + +#### SGLang + SGLang has 0-day support in main, see https://github.com/sgl-project/sglang/issues/8833 for infomation on installing SGLang from source. Once you've installed SGLang, run the following command to launch a SGLang server: diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml index a6ba83433c..1b142b6c30 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml @@ -15,7 +15,7 @@ datasets: field_thinking: thinking template_thinking_key: thinking -dataset_prepared_path: last_run_prepared +dataset_prepared_path: ./outputs/last_run_prepared val_set_size: 0 output_dir: ./outputs/gpt-oss-out/ diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml index aa658c863b..bdbb70faed 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml @@ -15,7 +15,7 @@ datasets: field_thinking: thinking template_thinking_key: thinking -dataset_prepared_path: last_run_prepared +dataset_prepared_path: ./outputs/last_run_prepared val_set_size: 0 output_dir: ./outputs/gpt-oss-out/ diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index 240c6d8944..0509cba693 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -82,7 +82,7 @@ def get_env(self): return res def get_image(self): - docker_tag = "main-py3.11-cu124-2.6.0" + docker_tag = "main-py3.11-cu126-2.7.1" if self.config.docker_tag: docker_tag = self.config.docker_tag docker_image = f"axolotlai/axolotl:{docker_tag}" diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index 83b567b649..d03a91bc7f 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -64,7 +64,7 @@ def do_inference( importlib.import_module("axolotl.prompters"), prompter ) elif cfg.chat_template: - chat_template_str = get_chat_template(cfg.chat_template) + chat_template_str = get_chat_template(cfg.chat_template, tokenizer=tokenizer) elif cfg.datasets[0].type == "chat_template": chat_template_str = get_chat_template_from_config( cfg=cfg, ds_cfg=cfg.datasets[0], tokenizer=tokenizer diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index 5d692c3151..4120062d89 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -97,7 +97,8 @@ def do_cli( """ # pylint: disable=duplicate-code os.environ["AXOLOTL_IS_PREPROCESS"] = "1" - parsed_cfg = load_cfg(config, **kwargs) + is_preprocess = kwargs.pop("is_preprocess", True) + parsed_cfg = load_cfg(config, is_preprocess=is_preprocess, **kwargs) parsed_cfg.is_preprocess = True parser = transformers.HfArgumentParser(PreprocessCliArgs) parsed_cli_args, _ = parser.parse_args_into_dataclasses( From 07fd22f39b89f09dfe0e32dce97923e48685e3cb Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 20 Aug 2025 15:17:48 -0400 Subject: [PATCH 0929/1405] better handling of lora w bias with fsdp2 and handling of files when saving model checkpoint (#3090) --- src/axolotl/cli/cloud/modal_.py | 2 +- src/axolotl/monkeypatch/accelerate/fsdp2.py | 2 +- src/axolotl/train.py | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index 0509cba693..6d4f999b41 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -200,7 +200,7 @@ def get_train_gpu(self): # pylint: disable=too-many-return-statements if family in ["a10", "a10g"]: return modal.gpu.A10G(count=count) if family == "h100": - return modal.gpu.H100(count=count) + return f"H100:{count}" if family == "t4": return modal.gpu.T4(count=count) if family == "l4": diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index efc3882941..66d3d0d2de 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -187,7 +187,7 @@ def _process_lora_module_for_fsdp(module, fsdp2_kwargs): # Linear4Bit will keep it's bias term in fp32. If the weight dtype is in bf16 we are not able to # wrap this. Therefore we must ensure the bias has the same dtype as the weight - if module.base_layer.bias is not None: + if hasattr(module.base_layer, "bias") and module.base_layer.bias is not None: if module.base_layer.weight.dtype != module.base_layer.bias.dtype: log_bias_dtype_mismatch = True module.base_layer.bias.data = module.base_layer.bias.data.to( diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 8005389f16..dd39cc2289 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -253,7 +253,9 @@ def save_trained_model( # final model weights have already been saved by `ReLoRACallback.on_train_end` return - if trainer.is_fsdp_enabled or cfg.fsdp_config: + if ( # pylint: disable=too-many-nested-blocks + trainer.is_fsdp_enabled or cfg.fsdp_config + ): if cfg.fsdp_config or cfg.fsdp: if cfg.fsdp_config.final_state_dict_type: state_dict_type = cfg.fsdp_config.final_state_dict_type @@ -285,6 +287,8 @@ def save_trained_model( if trainer.accelerator.is_main_process: # move all files in merged_path to cfg.output_dir for merged_file in Path(merged_path).iterdir(): + if (Path(cfg.output_dir) / merged_file.name).exists(): + (Path(cfg.output_dir) / merged_file.name).unlink() shutil.move(str(merged_file), cfg.output_dir) shutil.rmtree(merged_path) # remove what should be an empty dir # TODO(wing):see https://github.com/huggingface/transformers/pull/40207 From 08e517ea4828fea0b17401274aeb505722f8f867 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 20 Aug 2025 22:14:13 -0400 Subject: [PATCH 0930/1405] Update .coderabbit.yaml (#3091) [skip ci] --- .coderabbit.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 95c044f02b..b7cf7d9694 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -12,5 +12,6 @@ reviews: auto_review: enabled: true drafts: false + auto_incremental_review: true chat: auto_reply: true From 0fa752e58b593440ced0dd1cec0630f9b7b92664 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 21 Aug 2025 15:04:10 -0400 Subject: [PATCH 0931/1405] upgrade flash-attn to 2.8.3 for gpt-oss attn sink support (#3082) --- examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml | 2 +- examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml | 2 +- examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml | 2 +- examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml | 2 +- examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml | 2 +- setup.py | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml b/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml index 4b4fbd89b5..62f3167e85 100644 --- a/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml +++ b/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml @@ -44,7 +44,7 @@ bf16: true tf32: true flash_attention: true -attn_implementation: kernels-community/vllm-flash-attn3 +attn_implementation: kernels-community/vllm-flash-attn3 # this is not needed if using flash_attn >= 2.8.3 gradient_checkpointing: true activation_offloading: true diff --git a/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml b/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml index 440f0c509a..ccb84e28e9 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml @@ -40,7 +40,7 @@ bf16: true tf32: true flash_attention: true -attn_implementation: kernels-community/vllm-flash-attn3 +attn_implementation: kernels-community/vllm-flash-attn3 # this is not needed if using flash_attn >= 2.8.3 gradient_checkpointing: true activation_offloading: true diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml index 1b142b6c30..69a3c434d1 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml @@ -41,7 +41,7 @@ bf16: true tf32: true flash_attention: true -attn_implementation: kernels-community/vllm-flash-attn3 +attn_implementation: kernels-community/vllm-flash-attn3 # this is not needed if using flash_attn >= 2.8.3 gradient_checkpointing: true activation_offloading: true diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml index bdbb70faed..4a0f1ad707 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml @@ -40,7 +40,7 @@ bf16: true tf32: true flash_attention: true -attn_implementation: kernels-community/vllm-flash-attn3 +attn_implementation: kernels-community/vllm-flash-attn3 # this is not needed if using flash_attn >= 2.8.3 gradient_checkpointing: true activation_offloading: true diff --git a/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml b/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml index c4e1a982d8..b6deacb1b9 100644 --- a/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml +++ b/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml @@ -53,7 +53,7 @@ bf16: true tf32: true flash_attention: true -attn_implementation: kernels-community/vllm-flash-attn3 +attn_implementation: kernels-community/vllm-flash-attn3 # this is not needed if using flash_attn >= 2.8.3 gradient_checkpointing: true activation_offloading: true diff --git a/setup.py b/setup.py index de6f19e56a..5aab9d7c08 100644 --- a/setup.py +++ b/setup.py @@ -118,9 +118,9 @@ def get_package_version(): extras_require = { - "flash-attn": ["flash-attn==2.8.2"], + "flash-attn": ["flash-attn==2.8.3"], "ring-flash-attn": [ - "flash-attn==2.8.2", + "flash-attn==2.8.3", "ring-flash-attn>=0.1.7", "yunchang==0.6.0", ], From ab4d604a8fa4ddd07bcc56128ee664b5207cb541 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 22 Aug 2025 07:26:30 -0400 Subject: [PATCH 0932/1405] upgrade peft for 0.17.1 (#3094) * upgrade peft to 0.17.1 * upgrade for transformers too --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c2552002f9..c51c9d1fe9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,8 +13,8 @@ liger-kernel==0.6.1 packaging==23.2 huggingface_hub>=0.33.0 -peft==0.17.0 -transformers==4.55.2 +peft>=0.17.0 +transformers==4.55.3 tokenizers>=0.21.1 accelerate==1.10.0 datasets==4.0.0 From eea7a006e1fa805cd92543aa43e0abf7b9a06396 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 22 Aug 2025 14:29:10 -0400 Subject: [PATCH 0933/1405] make multipack sampler patch explicit (#3096) * make multipack sampler patch explicit * combining --- src/axolotl/common/datasets.py | 1 - src/axolotl/loaders/patch_manager.py | 8 +++ .../monkeypatch/data/batch_dataset_fetcher.py | 57 ++++++++++++++++++- tests/test_packed_batch_sampler.py | 26 ++++++--- 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index 761317dfb4..0ff52ebe1a 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -6,7 +6,6 @@ from datasets import Dataset -import axolotl.monkeypatch.data.batch_dataset_fetcher # pylint: disable=unused-import # noqa: F401 from axolotl.cli.args import PreprocessCliArgs, TrainerCliArgs from axolotl.loaders import load_processor, load_tokenizer from axolotl.utils.data import prepare_datasets, prepare_preference_datasets diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 628d897d0a..4959bd6ba1 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -277,6 +277,14 @@ def _apply_multipack_patches(self): has_remote_code=has_remote_code, ) + if self.cfg.sample_packing: + from axolotl.monkeypatch.data.batch_dataset_fetcher import ( + apply_multipack_dataloader_patch, + ) + + LOG.info("Applying multipack dataloader patch for sample packing...") + apply_multipack_dataloader_patch() + def _apply_fsdp2_bnb_patches(self): """Apply FSDP2 BNB patches.""" if ( diff --git a/src/axolotl/monkeypatch/data/batch_dataset_fetcher.py b/src/axolotl/monkeypatch/data/batch_dataset_fetcher.py index df8d106fd0..73bf37b614 100644 --- a/src/axolotl/monkeypatch/data/batch_dataset_fetcher.py +++ b/src/axolotl/monkeypatch/data/batch_dataset_fetcher.py @@ -1,4 +1,4 @@ -"""monkey patches for the dataset fetcher to handle batches of packed indexes""" +"""Monkey patches for the dataset fetcher to handle batches of packed indexes.""" # pylint: disable=protected-access @@ -6,10 +6,20 @@ from torch.utils.data._utils.fetch import _BaseDatasetFetcher from torch.utils.data._utils.worker import _worker_loop +_ORIGINAL_MAP_DATASET_FETCHER = None +_ORIGINAL_WORKER_LOOP = None +_IS_PATCHED = False + class _MapDatasetFetcher(_BaseDatasetFetcher): + """ + Custom dataset fetcher that handles nested batch structures from + MultipackBatchSampler. + """ + def fetch(self, possibly_batched_index): if isinstance(possibly_batched_index[0], list): + # Handle nested structure from MultipackBatchSampler data = [None for i in possibly_batched_index] for i, possibly_batched_index_ in enumerate(possibly_batched_index): if self.auto_collation: @@ -23,6 +33,7 @@ def fetch(self, possibly_batched_index): else: data[i] = self.dataset[possibly_batched_index_] else: + # Standard batch handling if self.auto_collation: if hasattr(self.dataset, "__getitems__") and self.dataset.__getitems__: data = self.dataset.__getitems__(possibly_batched_index) @@ -34,14 +45,54 @@ def fetch(self, possibly_batched_index): def patch_fetchers(): + """Apply patches to PyTorch's DataLoader components.""" torch.utils.data._utils.fetch._MapDatasetFetcher = _MapDatasetFetcher torch.utils.data.dataloader._utils.fetch._MapDatasetFetcher = _MapDatasetFetcher def patched_worker_loop(*args, **kwargs): + """Worker loop that ensures patches are applied in worker processes.""" patch_fetchers() return _worker_loop(*args, **kwargs) -torch.utils.data._utils.worker._worker_loop = patched_worker_loop -patch_fetchers() +def apply_multipack_dataloader_patch(): + """ + This patch allows DataLoader to correctly process batches that contain multiple bins + of packed sequences. + """ + # pylint: disable=global-statement + global _ORIGINAL_MAP_DATASET_FETCHER, _ORIGINAL_WORKER_LOOP, _IS_PATCHED + + if _IS_PATCHED: + return + + # Store original implementations + _ORIGINAL_MAP_DATASET_FETCHER = torch.utils.data._utils.fetch._MapDatasetFetcher + _ORIGINAL_WORKER_LOOP = torch.utils.data._utils.worker._worker_loop + + # Apply patches + patch_fetchers() + torch.utils.data._utils.worker._worker_loop = patched_worker_loop + + _IS_PATCHED = True + + +def remove_multipack_dataloader_patch(): + """Remove the monkeypatch and restore original PyTorch DataLoader behavior.""" + # pylint: disable=global-statement + global _IS_PATCHED + + if not _IS_PATCHED: + return + + if _ORIGINAL_MAP_DATASET_FETCHER: + torch.utils.data._utils.fetch._MapDatasetFetcher = _ORIGINAL_MAP_DATASET_FETCHER + torch.utils.data.dataloader._utils.fetch._MapDatasetFetcher = ( + _ORIGINAL_MAP_DATASET_FETCHER + ) + + if _ORIGINAL_WORKER_LOOP: + torch.utils.data._utils.worker._worker_loop = _ORIGINAL_WORKER_LOOP + + _IS_PATCHED = False diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index 47894a35b7..d839c6ea33 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -48,7 +48,13 @@ def test_packing( max_seq_length, sequential, ): - import axolotl.monkeypatch.data.batch_dataset_fetcher # pylint: disable=unused-import # noqa: F401 + from axolotl.monkeypatch.data.batch_dataset_fetcher import ( + apply_multipack_dataloader_patch, + remove_multipack_dataloader_patch, + ) + + # Apply the patch for multipack handling + apply_multipack_dataloader_patch() dataset = dataset_winglian_tiny_shakespeare["train"] @@ -101,10 +107,14 @@ def test_packing( for pack in batch: batch_idxs.extend(pack) - for batch in loader: - assert batch["input_ids"].numel() <= batch_size * max_seq_length - assert batch["input_ids"].shape[1] == max_seq_length - - original_idxs = set(range(len(train_dataset))) - assert original_idxs == set(batch_idxs) - assert len(batch_idxs) == len(set(batch_idxs)) + try: + for batch in loader: + assert batch["input_ids"].numel() <= batch_size * max_seq_length + assert batch["input_ids"].shape[1] == max_seq_length + + original_idxs = set(range(len(train_dataset))) + assert original_idxs == set(batch_idxs) + assert len(batch_idxs) == len(set(batch_idxs)) + finally: + # Clean up: remove the patch after the test + remove_multipack_dataloader_patch() From 79ddaebe9a6af7efefebdbb54772d11d09561786 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Sat, 23 Aug 2025 23:37:33 -0400 Subject: [PATCH 0934/1405] Add ruff, remove black, isort, flake8, pylint (#3092) * black, isort, flake8 -> ruff * remove unused * add back needed import * fix --- .bandit | 2 +- .flake8 | 5 - .isort.cfg | 4 - .pre-commit-config.yaml | 20 +- .pylintrc | 15 - cicd/multigpu.py | 4 +- cicd/single_gpu.py | 4 +- docs/scripts/generate_config_docs.py | 7 +- .../colab-axolotl-example.ipynb | 19706 ++++++++-------- pyproject.toml | 31 + scripts/chat_datasets.py | 6 +- scripts/unsloth_install.py | 5 +- src/axolotl/cli/art.py | 2 +- src/axolotl/cli/cloud/modal_.py | 9 +- src/axolotl/cli/config.py | 2 +- src/axolotl/cli/evaluate.py | 4 +- src/axolotl/cli/inference.py | 5 +- src/axolotl/cli/main.py | 2 - src/axolotl/cli/merge_sharded_fsdp_weights.py | 8 +- src/axolotl/cli/preprocess.py | 4 +- src/axolotl/cli/train.py | 2 +- src/axolotl/cli/utils/args.py | 4 +- src/axolotl/cli/utils/sweeps.py | 7 +- src/axolotl/cli/utils/train.py | 1 - src/axolotl/cli/vllm_serve.py | 3 +- src/axolotl/common/datasets.py | 1 + src/axolotl/convert.py | 4 +- src/axolotl/core/attention/flex_block_mask.py | 8 +- src/axolotl/core/builders/base.py | 14 +- src/axolotl/core/builders/causal.py | 6 +- src/axolotl/core/builders/rl.py | 6 +- src/axolotl/core/chat/format/chatml.py | 2 +- src/axolotl/core/chat/messages.py | 30 +- .../core/datasets/transforms/chat_builder.py | 26 +- src/axolotl/core/trainers/__init__.py | 1 - src/axolotl/core/trainers/base.py | 13 +- src/axolotl/core/trainers/dpo/trainer.py | 6 +- src/axolotl/core/trainers/grpo/__init__.py | 6 +- src/axolotl/core/trainers/grpo/trainer.py | 29 +- src/axolotl/core/trainers/mamba.py | 5 +- src/axolotl/core/trainers/mixins/__init__.py | 1 - .../mixins/activation_checkpointing.py | 2 +- .../trainers/mixins/distributed_parallel.py | 1 - src/axolotl/core/trainers/mixins/optimizer.py | 16 +- src/axolotl/core/trainers/mixins/scheduler.py | 10 +- src/axolotl/core/training_args_base.py | 1 - src/axolotl/datasets.py | 4 +- src/axolotl/evaluate.py | 2 +- src/axolotl/integrations/base.py | 25 +- src/axolotl/integrations/config.py | 22 +- .../cut_cross_entropy/__init__.py | 13 +- .../integrations/cut_cross_entropy/args.py | 1 + src/axolotl/integrations/grokfast/__init__.py | 8 +- .../integrations/grokfast/optimizer.py | 1 - src/axolotl/integrations/kd/__init__.py | 3 +- src/axolotl/integrations/kd/args.py | 5 +- src/axolotl/integrations/kd/callbacks.py | 4 +- src/axolotl/integrations/kd/chat_template.py | 8 +- src/axolotl/integrations/kd/collator.py | 26 +- .../kd/collator_online_teacher.py | 15 +- src/axolotl/integrations/kd/kernels/liger.py | 6 +- src/axolotl/integrations/kd/kernels/models.py | 3 +- .../kd/topk_logprob/forward_kl.py | 8 +- src/axolotl/integrations/kd/trainer.py | 1 - src/axolotl/integrations/liger/__init__.py | 1 + src/axolotl/integrations/liger/models/base.py | 3 +- .../integrations/liger/models/deepseekv2.py | 2 - .../integrations/liger/models/jamba.py | 2 - .../integrations/liger/models/llama4.py | 17 +- .../integrations/liger/models/qwen3.py | 12 +- .../integrations/liger/models/qwen3_moe.py | 14 +- src/axolotl/integrations/lm_eval/__init__.py | 3 +- src/axolotl/integrations/lm_eval/cli.py | 1 - src/axolotl/integrations/spectrum/__init__.py | 6 +- src/axolotl/integrations/spectrum/args.py | 1 + src/axolotl/kernels/geglu.py | 2 - src/axolotl/kernels/lora.py | 2 - src/axolotl/kernels/quantize.py | 2 - src/axolotl/kernels/swiglu.py | 2 - src/axolotl/loaders/__init__.py | 1 - src/axolotl/loaders/adapter.py | 12 +- src/axolotl/loaders/model.py | 23 +- src/axolotl/loaders/tokenizer.py | 10 +- src/axolotl/models/mamba/__init__.py | 2 +- src/axolotl/models/mamba/modeling_mamba.py | 3 +- src/axolotl/monkeypatch/accelerate/fsdp2.py | 7 +- .../accelerate/parallelism_config.py | 4 +- .../monkeypatch/attention/flex_attn.py | 26 +- src/axolotl/monkeypatch/attention/xformers.py | 12 +- .../monkeypatch/btlm_attn_hijack_flash.py | 8 +- .../monkeypatch/data/batch_dataset_fetcher.py | 2 - src/axolotl/monkeypatch/fsdp2_qlora.py | 13 +- .../gradient_checkpointing/__init__.py | 8 +- .../gradient_checkpointing/offload_cpu.py | 8 +- .../gradient_checkpointing/offload_disk.py | 8 +- .../monkeypatch/llama_attn_hijack_flash.py | 32 +- .../monkeypatch/llama_attn_hijack_xformers.py | 8 +- src/axolotl/monkeypatch/llama_expand_mask.py | 4 +- .../monkeypatch/llama_patch_multipack.py | 8 +- src/axolotl/monkeypatch/lora_kernels.py | 53 +- src/axolotl/monkeypatch/loss/chunked.py | 10 +- .../monkeypatch/mistral_attn_hijack_flash.py | 2 - src/axolotl/monkeypatch/mixtral/__init__.py | 6 +- .../monkeypatch/models/llama4/modeling.py | 14 +- src/axolotl/monkeypatch/multipack.py | 8 +- src/axolotl/monkeypatch/peft/utils.py | 16 +- src/axolotl/monkeypatch/relora.py | 9 +- src/axolotl/monkeypatch/ring_attn/__init__.py | 1 - .../monkeypatch/ring_attn/adapters/batch.py | 6 +- src/axolotl/monkeypatch/ring_attn/patch.py | 35 +- .../monkeypatch/stablelm_attn_hijack_flash.py | 36 +- src/axolotl/monkeypatch/tiled_mlp/patch.py | 8 +- src/axolotl/monkeypatch/trainer/lr.py | 2 +- .../monkeypatch/trainer_accelerator_args.py | 12 +- src/axolotl/monkeypatch/trainer_fsdp_optim.py | 16 +- .../transformers/trainer_loss_calc.py | 16 +- src/axolotl/monkeypatch/unsloth_.py | 46 +- src/axolotl/monkeypatch/xformers_/__init__.py | 4 +- src/axolotl/processing_strategies.py | 6 +- src/axolotl/prompt_strategies/__init__.py | 2 +- src/axolotl/prompt_strategies/alpaca_chat.py | 4 +- .../prompt_strategies/alpaca_w_system.py | 6 +- src/axolotl/prompt_strategies/base.py | 2 +- .../bradley_terry/__init__.py | 3 +- .../bradley_terry/chat_template.py | 2 - .../prompt_strategies/bradley_terry/llama3.py | 2 +- .../prompt_strategies/chat_template.py | 13 +- src/axolotl/prompt_strategies/completion.py | 12 +- src/axolotl/prompt_strategies/context_qa.py | 1 - src/axolotl/prompt_strategies/creative_acr.py | 4 +- .../prompt_strategies/dpo/chat_template.py | 4 +- src/axolotl/prompt_strategies/dpo/chatml.py | 14 +- src/axolotl/prompt_strategies/dpo/llama3.py | 15 +- .../prompt_strategies/dpo/passthrough.py | 8 +- .../prompt_strategies/dpo/user_defined.py | 2 +- src/axolotl/prompt_strategies/dpo/zephyr.py | 7 +- src/axolotl/prompt_strategies/input_output.py | 1 - src/axolotl/prompt_strategies/kto/chatml.py | 14 +- src/axolotl/prompt_strategies/kto/llama3.py | 14 +- .../prompt_strategies/kto/user_defined.py | 4 +- src/axolotl/prompt_strategies/llama2_chat.py | 4 +- .../prompt_strategies/messages/__init__.py | 4 +- .../prompt_strategies/messages/chat.py | 11 +- src/axolotl/prompt_strategies/metharme.py | 4 +- .../prompt_strategies/orpo/chat_template.py | 51 +- src/axolotl/prompt_strategies/pygmalion.py | 6 +- .../prompt_strategies/stepwise_supervised.py | 2 +- src/axolotl/prompt_strategies/user_defined.py | 18 +- src/axolotl/prompt_tokenizers.py | 14 +- src/axolotl/prompters.py | 8 +- src/axolotl/train.py | 12 +- src/axolotl/utils/__init__.py | 1 - src/axolotl/utils/callbacks/__init__.py | 95 +- src/axolotl/utils/callbacks/comet_.py | 6 +- src/axolotl/utils/callbacks/lisa.py | 4 +- src/axolotl/utils/callbacks/mlflow_.py | 7 +- src/axolotl/utils/callbacks/profiler.py | 44 +- src/axolotl/utils/callbacks/qat.py | 4 +- src/axolotl/utils/config/__init__.py | 6 +- src/axolotl/utils/ctx_managers/__init__.py | 1 - .../utils/ctx_managers/sequence_parallel.py | 2 +- src/axolotl/utils/data/pretraining.py | 3 +- src/axolotl/utils/data/rl.py | 1 - src/axolotl/utils/data/shared.py | 4 +- src/axolotl/utils/data/utils.py | 2 +- src/axolotl/utils/data/wrappers.py | 3 +- src/axolotl/utils/dict.py | 8 +- src/axolotl/utils/distributed.py | 10 +- src/axolotl/utils/environment.py | 2 +- src/axolotl/utils/lora.py | 1 + .../utils/mistral/mistral_tokenizer.py | 4 +- src/axolotl/utils/model_shard_quant.py | 14 +- src/axolotl/utils/optimizers/adopt.py | 9 +- src/axolotl/utils/samplers/multipack.py | 8 +- src/axolotl/utils/schedulers.py | 5 +- src/axolotl/utils/schemas/config.py | 9 +- src/axolotl/utils/schemas/datasets.py | 1 - src/axolotl/utils/schemas/enums.py | 2 - src/axolotl/utils/schemas/training.py | 6 +- src/axolotl/utils/schemas/validation.py | 42 +- src/axolotl/utils/tokenization.py | 2 +- src/axolotl/utils/trainer.py | 2 +- ...setuptools_axolotl_dynamic_dependencies.py | 1 - tests/cli/test_cli_evaluate.py | 3 - tests/cli/test_cli_inference.py | 2 - .../test_cli_merge_sharded_fsdp_weights.py | 2 - tests/cli/test_cli_train.py | 2 - tests/cli/test_utils.py | 34 +- tests/conftest.py | 37 +- tests/constants.py | 1 + tests/core/test_builders.py | 9 +- .../integrations/test_cut_cross_entropy.py | 4 - tests/e2e/integrations/test_fp8.py | 3 +- tests/e2e/integrations/test_hooks.py | 31 +- tests/e2e/integrations/test_kd.py | 4 +- tests/e2e/integrations/test_liger.py | 6 +- tests/e2e/kernels/test_geglu.py | 6 +- tests/e2e/kernels/test_lora.py | 4 +- tests/e2e/kernels/test_quantize.py | 2 - tests/e2e/kernels/test_swiglu.py | 8 +- tests/e2e/multigpu/solo/test_flex.py | 1 - tests/e2e/multigpu/solo/test_grpo.py | 2 +- tests/e2e/multigpu/test_eval.py | 2 - tests/e2e/multigpu/test_fp8_fsdp2.py | 14 +- tests/e2e/multigpu/test_fsdp1.py | 14 +- tests/e2e/multigpu/test_fsdp2.py | 14 +- tests/e2e/multigpu/test_gemma3.py | 1 - tests/e2e/multigpu/test_llama.py | 12 - tests/e2e/multigpu/test_ray.py | 3 - tests/e2e/multigpu/test_tp.py | 1 - .../lora_kernels/test_lora_kernel_patching.py | 25 +- tests/e2e/patched/test_4d_multipack_llama.py | 2 - .../patched/test_activation_checkpointing.py | 3 +- tests/e2e/patched/test_cli_integrations.py | 1 - tests/e2e/patched/test_fa_xentropy.py | 1 - tests/e2e/patched/test_falcon_samplepack.py | 2 - tests/e2e/patched/test_flattening.py | 1 - tests/e2e/patched/test_fsdp2_qlora.py | 15 +- tests/e2e/patched/test_fused_llama.py | 1 - tests/e2e/patched/test_llama_s2_attention.py | 2 - .../e2e/patched/test_lora_llama_multipack.py | 2 - tests/e2e/patched/test_mistral_samplepack.py | 2 - tests/e2e/patched/test_mixtral_samplepack.py | 2 - tests/e2e/patched/test_model_patches.py | 2 +- tests/e2e/patched/test_peft_embeddings.py | 1 - tests/e2e/patched/test_phi_multipack.py | 2 - tests/e2e/patched/test_resume.py | 1 - tests/e2e/patched/test_unsloth_qlora.py | 1 - tests/e2e/solo/test_flex.py | 1 - tests/e2e/solo/test_relora_llama.py | 7 +- tests/e2e/test_activation_offloading.py | 3 - tests/e2e/test_deepseekv3.py | 2 - tests/e2e/test_dpo.py | 7 - tests/e2e/test_embeddings_lr.py | 2 - tests/e2e/test_evaluate.py | 1 - tests/e2e/test_falcon.py | 3 - tests/e2e/test_gemma2.py | 2 - tests/e2e/test_gemma3_text.py | 2 - tests/e2e/test_imports.py | 8 +- tests/e2e/test_llama.py | 4 - tests/e2e/test_llama_pretrain.py | 1 - tests/e2e/test_llama_vision.py | 2 - tests/e2e/test_load_model.py | 14 +- tests/e2e/test_lora_llama.py | 1 - tests/e2e/test_mamba.py | 1 - tests/e2e/test_mistral.py | 2 - tests/e2e/test_mixtral.py | 5 - tests/e2e/test_optimizers.py | 7 - tests/e2e/test_packing_loss.py | 1 - tests/e2e/test_phi.py | 2 - tests/e2e/test_preprocess.py | 2 +- .../e2e/test_process_reward_model_smollm2.py | 1 - tests/e2e/test_qat.py | 2 - tests/e2e/test_quantization.py | 26 +- tests/e2e/test_qwen.py | 1 - tests/e2e/test_reward_model_smollm2.py | 1 - tests/e2e/test_save_first_step.py | 2 - tests/e2e/test_schedulers.py | 1 - tests/e2e/utils.py | 19 +- tests/hf_offline_utils.py | 2 +- tests/integrations/test_liger.py | 2 - tests/patched/test_validation.py | 18 +- tests/prompt_strategies/conftest.py | 7 +- tests/prompt_strategies/messages/test_chat.py | 7 +- tests/prompt_strategies/test_alpaca.py | 1 - ...est_chat_template_ds_schema_unification.py | 6 +- .../prompt_strategies/test_chat_templates.py | 91 +- .../test_chat_templates_advanced.py | 202 +- .../test_chat_templates_mistral.py | 2 +- .../test_chat_templates_thinking.py | 9 +- .../test_dpo_chat_templates.py | 6 - tests/prompt_strategies/test_stepwise.py | 1 - tests/test_chunked_xentropy.py | 2 +- tests/test_datasets.py | 1 - tests/test_dict.py | 44 +- tests/test_exact_deduplication.py | 14 +- tests/test_loaders.py | 27 +- tests/test_lora.py | 1 - tests/test_packed_batch_sampler.py | 2 +- tests/test_packed_dataset.py | 10 +- tests/test_packed_pretraining.py | 1 - tests/test_perplexity.py | 2 - tests/test_prompt_tokenizers.py | 8 +- tests/test_schedulers.py | 2 +- tests/test_validation_dataset.py | 1 - tests/utils/schemas/validation/test_fsdp.py | 1 - 286 files changed, 10896 insertions(+), 11352 deletions(-) delete mode 100644 .flake8 delete mode 100644 .isort.cfg delete mode 100644 .pylintrc diff --git a/.bandit b/.bandit index 82e88e8148..b814287519 100644 --- a/.bandit +++ b/.bandit @@ -1,3 +1,3 @@ [bandit] exclude = tests -skips = B101,B615 +skips = B101,B615,B102,B110 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index fd69af7756..0000000000 --- a/.flake8 +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -max-line-length = 88 - -select = C,E,F,W,B,B950 -extend-ignore = E203, E501, W503 diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index bf9afe3192..0000000000 --- a/.isort.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[settings] -profile=black -known_third_party=wandb,comet_ml -known_local_folder=src,tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c9268529d..4c2861346e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,22 +10,12 @@ repos: - id: trailing-whitespace - id: no-commit-to-branch args: ['--branch', 'main'] -- repo: https://github.com/psf/black - rev: 25.1.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.9 hooks: - - id: black -- repo: https://github.com/pycqa/isort - rev: 6.0.1 - hooks: - - id: isort -- repo: https://github.com/PyCQA/flake8 - rev: 7.3.0 - hooks: - - id: flake8 -- repo: https://github.com/pylint-dev/pylint - rev: v3.3.8 - hooks: - - id: pylint + - id: ruff + args: [--fix] + - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.17.1 hooks: diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 208dd32b6a..0000000000 --- a/.pylintrc +++ /dev/null @@ -1,15 +0,0 @@ -[MASTER] -init-hook="from pylint.config import find_default_config_files; import sys; sys.path.append(next(find_default_config_files()).parent.as_posix())" - -[TYPECHECK] - -# List of members which are set dynamically and missed by Pylint inference -# system, and so shouldn't trigger E1101 when accessed. -generated-members=numpy.*, torch.* - - -[pylint.messages_control] -disable=missing-function-docstring, line-too-long, import-error, - too-many-arguments, too-many-locals, too-many-statements, too-many-branches, too-few-public-methods, - too-many-instance-attributes, fixme, import-outside-toplevel, logging-fstring-interpolation, - too-many-positional-arguments, possibly-used-before-assignment diff --git a/cicd/multigpu.py b/cicd/multigpu.py index 2c067f1437..5bd8d3c044 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -2,8 +2,6 @@ modal application to run axolotl gpu tests in Modal """ -# pylint: disable=duplicate-code - import os import pathlib import tempfile @@ -63,7 +61,7 @@ def run_cmd(cmd: str, run_folder: str): # Propagate errors from subprocess. if exit_code := subprocess.call(cmd.split(), cwd=run_folder): # nosec - exit(exit_code) # pylint: disable=consider-using-sys-exit + exit(exit_code) @app.function( diff --git a/cicd/single_gpu.py b/cicd/single_gpu.py index eb34e17489..0e2922e900 100644 --- a/cicd/single_gpu.py +++ b/cicd/single_gpu.py @@ -1,7 +1,5 @@ """Modal app to run axolotl GPU tests""" -# pylint: disable=duplicate-code - import os import pathlib import tempfile @@ -70,4 +68,4 @@ def run_cmd(cmd: str, run_folder: str): # Propagate errors from subprocess. if exit_code := subprocess.call(cmd.split(), cwd=run_folder, env=sp_env): # nosec - exit(exit_code) # pylint: disable=consider-using-sys-exit + exit(exit_code) diff --git a/docs/scripts/generate_config_docs.py b/docs/scripts/generate_config_docs.py index e22da7d050..6efa2038bf 100644 --- a/docs/scripts/generate_config_docs.py +++ b/docs/scripts/generate_config_docs.py @@ -47,7 +47,6 @@ def _is_pydantic_model(self, type_obj) -> bool: """Check if a type is a Pydantic BaseModel.""" return inspect.isclass(type_obj) and issubclass(type_obj, BaseModel) - # pylint: disable=too-many-return-statements def _extract_nested_type(self, field_type) -> Any: """Extract the actual type from complex type annotations.""" # Handle Annotated types (Python 3.9+) @@ -124,7 +123,6 @@ def _extract_nested_type(self, field_type) -> Any: return field_type - # pylint: disable=too-many-return-statements def _extract_all_pydantic_models_from_type( self, field_type ) -> list[type[BaseModel]]: @@ -318,7 +316,6 @@ def _extract_field_groups_from_all_classes( return all_groups - # pylint: disable=too-many-return-statements def _extract_field_groups_from_source( self, model_class: type[BaseModel] ) -> list[dict]: @@ -503,7 +500,7 @@ def _generate_field_documentation( nested_schema = nested_model.model_json_schema() nested_properties = nested_schema.get("properties", {}) nested_required = nested_schema.get("required", []) - except Exception: # pylint: disable=broad-exception-caught + except Exception: # Fallback: use model fields directly nested_properties = {} nested_required = [] @@ -607,7 +604,7 @@ def generate_qmd( schema = model_class.model_json_schema() properties = schema.get("properties", {}) required = schema.get("required", []) - except Exception as e: # pylint: disable=broad-exception-caught + except Exception as e: print( f"Warning: Could not generate JSON schema ({e}). Using model fields instead." ) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 69881997ec..30ef1c3de0 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -1,9934 +1,9944 @@ { - "cells": [ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "OPLSwmgdrB7g" + }, + "source": [ + "# Fine-Tune Qwen3 14B with Axolotl\n", + "\n", + "[\"Built](https://github.com/axolotl-ai-cloud/axolotl)\n", + "\n", + "Axolotl is the most performant LLM post-training framework available, delivering faster training with efficient, consistent and stable performance. Train your workload and ship your product 30% faster; saving you both time and money.\n", + "\n", + "- ⭐ us on [GitHub](https://github.com/axolotl-ai-cloud/axolotl)\n", + "- 📜 Read the [Docs](http://docs.axolotl.ai/)\n", + "- 💬 Chat with us on [Discord](https://discord.gg/mnpEYgRUmD)\n", + "- 📰 Get updates on [X/Twitter](https://x.com/axolotl_ai)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rVjKD7CbxIP3" + }, + "source": [ + "# Installation\n", + "\n", + "Axolotl is easy to install from [pip](https://pypi.org/project/axolotl/), or use our [pre-built Docker images](http://docs.axolotl.ai/docs/docker.html) for a hassle free dependency experience. See our [docs](http://docs.axolotl.ai/docs/installation.html) for more information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "msOCO4NRmRLa" + }, + "outputs": [], + "source": [ + "%%capture\n", + "# This step can take ~5-10 minutes to install dependencies\n", + "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0ee9ee8\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "N0OW0YeksDLr" + }, + "source": [ + "## Demo: Talk Like a Pirate\n", + "\n", + "In this demo, we are training the model ***to respond like a pirate***. This was chosen as a way to easily show how to train a model to respond in a certain style of your choosing (without being prompted) and is quite easy to validate within the scope of a Colab." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8Du2fANTsNCK" + }, + "source": [ + "### Upload your own dataset or use a Huggingface dataset\n", + "\n", + "You can choose to use your own JSONL file from your own [Google Drive](https://drive.google.com/drive/home); for example downloading the [Pirate-Ultrachat JSONL](https://huggingface.co/datasets/winglian/pirate-ultrachat-10k/blob/main/train.jsonl) to your Google Drive. JSONL datasets should be formatted similar to the [OpenAI dataset format](https://cookbook.openai.com/examples/chat_finetuning_data_prep).\n", + "\n", + "You can also simply use the [`winglian/pirate-ultrachat-10k`](https://huggingface.co/datasets/winglian/pirate-ultrachat-10k) dataset directly.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fGEEjyQ-r_IV" + }, + "outputs": [], + "source": [ + "# Default to HF dataset location\n", + "dataset_id = \"winglian/pirate-ultrachat-10k\"\n", + "uploaded = {}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c5MyYqk7vIsG" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# Optionally, upload your own JSONL to your Google Drive\n", + "GOOGLE_DRIVE_PATH = \"\" # ex: \"MyDrive/Colab\\ Notebooks/train.jsonl\"\n", + "\n", + "# \"Select All\" permissions, or you may get the error:\n", + "# \"MessageError: Error: credential propagation was unsuccessful\"\n", + "if GOOGLE_DRIVE_PATH:\n", + " from google.colab import drive\n", + "\n", + " # Mount your Google Drive\n", + " GOOGLE_DRIVE_MNT = \"/content/drive/\"\n", + " drive.mount(GOOGLE_DRIVE_MNT, force_remount=True)\n", + " tmp_path = os.path.join(GOOGLE_DRIVE_MNT, GOOGLE_DRIVE_PATH.lstrip(\"/\"))\n", + " # make sure file exists\n", + " if not os.path.isfile(tmp_path):\n", + " raise ValueError(f\"File {tmp_path} does not exist\")\n", + " dataset_id = tmp_path" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "U6pTk3A9xj1W" + }, + "source": [ + "# Configure for Supervised Fine-Tuning (SFT)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 151, + "referenced_widgets": [ + "388f618924274d21a066f098f4f1e744", + "7c95f85a2b1f47a1bd846d110c47bb3c", + "083f9cda8d754c168beee10d2f8955a2", + "62e1a65582f446a78612eaa804e08a7d", + "487a177d020f4605834878b2fdc7afa3", + "7fd44cf9ca6e4726bfd7ac21846d6a14", + "366a343b62fa47d8985a3bd464d99f9e", + "a0a11e929edd4189b79723d618522c33", + "e87ea87fcff247b5bbcc331ba79a8dc2", + "5e18768f7ad6434ba8b8b8a2e853e204", + "bb33aec33a6447078c31bfd728942994" + ] + }, + "id": "fdRioqytmTtX", + "outputId": "f0acdcec-4b41-4a3f-ffed-c2d2d929158e" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "OPLSwmgdrB7g" + "name": "stdout", + "output_type": "stream", + "text": [ + "[2025-05-08 13:40:27,488] [INFO] [root.register:348] [PID:174] Attempting to load plugin: axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin\n", + "[2025-05-08 13:40:27,493] [INFO] [root.register:351] [PID:174] Plugin loaded successfully: axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin\n", + "[2025-05-08 13:40:27,959] [INFO] [axolotl.utils.schemas.config.check_eval_packing:721] [PID:174] [RANK:0] explicitly setting `eval_sample_packing` to match `sample_packing`\u001b[39m\n", + "[2025-05-08 13:40:27,960] [INFO] [axolotl.utils.schemas.config.hint_sample_packing_padding:514] [PID:174] [RANK:0] Setting `pad_to_sequence_len: true` to prevent memory leaks when sample_packing\u001b[39m\n", + "[2025-05-08 13:40:27,961] [INFO] [axolotl.utils.schemas.config.check_bf16:1251] [PID:174] [RANK:0] bf16 support detected, but not enabled for this configuration.\u001b[39m\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "388f618924274d21a066f098f4f1e744", + "version_major": 2, + "version_minor": 0 }, - "source": [ - "# Fine-Tune Qwen3 14B with Axolotl\n", - "\n", - "[\"Built](https://github.com/axolotl-ai-cloud/axolotl)\n", - "\n", - "Axolotl is the most performant LLM post-training framework available, delivering faster training with efficient, consistent and stable performance. Train your workload and ship your product 30% faster; saving you both time and money.\n", - "\n", - "- ⭐ us on [GitHub](https://github.com/axolotl-ai-cloud/axolotl)\n", - "- 📜 Read the [Docs](http://docs.axolotl.ai/)\n", - "- 💬 Chat with us on [Discord](https://discord.gg/mnpEYgRUmD)\n", - "- 📰 Get updates on [X/Twitter](https://x.com/axolotl_ai)\n" + "text/plain": [ + "config.json: 0%| | 0.00/728 [00:00\"],\n", + " }\n", + " ],\n", + " dataloader_prefetch_factor=8, # dataloader optimizations\n", + " dataloader_num_workers=2,\n", + " dataloader_pin_memory=True,\n", + ")\n", + "\n", + "# validates the configuration\n", + "cfg = load_cfg(config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "715UpvnSoBIS" + }, + "outputs": [], + "source": [ + "from axolotl.utils import patch_optimized_env\n", + "\n", + "# speedup downloads from HF 🤗 and set \"PYTORCH_CUDA_ALLOC_CONF\" env to save memory\n", + "patch_optimized_env()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Vc6MC-hwyH-n" + }, + "source": [ + "# Datasets\n", + "\n", + "Axolotl has a robust suite of loaders and transforms to parse most open datasets of any format into the appropriate chat template for your model. Axolotl will mask input tokens from the user's prompt so that the train loss is only calculated against the model's response. For more information, [see our documentation](http://docs.axolotl.ai/docs/dataset-formats/conversation.html) on dataset preparation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "b82aa8c57f7c422a9a9c90f333ed2a99", + "c0991cf63ee6458b96e9a75e7a88b61a", + "71c8af139cd248b1b51101fd46a93f35", + "1d5117195d4b49eb8f1a73b18419f7ce", + "3c21e4a511b4441192c03b7f1d0976e9", + "ed28e2e0410d4e0b855467e798e53d66", + "d93f134f802b4b69b575bdaf07dbd27c", + "d0e9dce55cec4c1ca619a0ccf209d924", + "4c727d40ef0443449afc31724ee79f0c", + "0dea5caa27384f5689e3cab51f558727", + "a6f48410b9964fefba0c3009a77dc838", + "95caff42f08a4c2aa14c867b8f37f231", + "de7c37ee83e24f0c889e84d07279c2ec", + "9d4897eefb5f48259ffb2d23e332f752", + "253017b0d0534e54ab44e181f6d7c82d", + "27beaf06e41b472abdb544a43c720c5a", + "34cf3df51fbc41cabfdbba153c007f0e", + "ac764024cf1c4e08ba7749afd2cd20ac", + "30a81da86f8043eca301e86a8651201a", + "e8b7a81040904c1e89e58978223b1737", + "1c6f1f10667545aaab958016ba7e2c94", + "e6e969610738449887259063967f82b0", + "a138859f19b74fc0928dc236ab5359db", + "9b42e08b3c9548818488268768a118b1", + "12b56912736849fea2ad8124456fdc5c", + "879c8ab5873847a8833bd74123be90a4", + "20352e5f58d24bb8b1f3940efd14fe4a", + "d955dcaa0e944e719f3a06139dd54a03", + "d3de2662c7964f1ba96e58da382af720", + "97e36007e1304e1583fd81bfb13f0edd", + "c65dc74c7d6f4bab8f7dd28455161dd8", + "ef223e8504b64e3592589880326aaf41", + "598da69727bd4fb8b1caf465ac736d7a", + "5f86cd894de94c3280fadc1e2fd0ee13", + "a20927bf5f2c41f58c1e31ac858ab36c", + "0a46ad75c198463d843fb35e813642cb", + "09007681cf8d42aeb8c1d2f6a74e470a", + "ebc80d1a55fa47f4a5ea2756588569ec", + "1811cda0644e4190a9469d1774435d82", + "35c811d2ae8e43f3b5cecbdd3cfa857f", + "b8e39e4dddc3497fbc29ae45c66da759", + "63b4e563e85c4f03b1b72beda9577bcc", + "b195f160ca20442fadd8b5aed0ee41af", + "ca65e32eb52f48c09a84b33cb18f22cd", + "7cd0b85ebd204b7aba908417811ce4e0", + "7baeab52d6694c32b1efd1ea1a0a7782", + "519a7b154022443db6703f04a9142bae", + "d4183e9715f34d249942b8271cca3bdf", + "da2347ac94764a3fa2743343cf0d3cd2", + "93a44a11aa4846fa8efc6c1413ef1627", + "a55060adc3564407ac81ad7297d34aaa", + "d02274afd47b462291c745f261209d42", + "0f417447a7bd4a33acca96fa37aec877", + "63580b6fb30642479fe3000915bf551a", + "8f726dbfb45d4528afa33e36a6313267", + "03b093d592ba4386aa61f7b8483da660", + "b8766a88716948cf968f4563531a76d9", + "6f3a28b912714c6e931003549664bfa3", + "16d1283741404b7bb319094c992fce01", + "2a5bb0e818ab47be8cf6465988328503", + "2b3a2659b12244bd8548320320016dbf", + "0cd7efffbb3c4c4b972e63749f61ab97", + "5ca240f31e6b44e3882c5eb37cd5a309", + "5eb06edeb58e4930b1affef2a59eae81", + "a4e5789584564049b83df7c6c54a3e08", + "ff3a94b146a948b6907f5d80c7157f99", + "258b7c635c1045329d4669e48c46ccd5", + "6f68ed9889f54ad2ae8a3b95ac263a83", + "80366349d81e4dcc892db6cd56e384f3", + "c73055099c084dca996159e23e162d0b", + "977f799afaac4a55b2dc1cffa7d5b63b", + "41f3b32c2f6b4034ae7a3b9124e28bc7", + "a10d0a76010f4e508c65a9b69ebc5156", + "f8ef805b776145c3bfa9ba8d90972058", + "cc587493c33c4f118d1b1170f85be24c", + "e40d1c1ac9494b3bade9858324e7ffdf", + "d65b6b060d9845779299491ac5599c31", + "0f6907ebbc6242c8bde059cef1e1bd29", + "5bdfd87fc6cd4f9dabef7cfee29c8060", + "64f54d4a744a4627a07c3c0120276f3b", + "65b75b9b8bc143cf997796af68ff6668", + "d6fe74e4255444368f8f90a62157d869", + "4d468f96ec924681ad65eb671674b93e", + "ad7599de524549c48bf2d3124ad4b299", + "0546d04aae644dde846c58a4afb598a6", + "897b77a56c09479bb11d7f2a30997e55", + "81c3db71ac704280ad030072655f1537", + "042e091f75694c47aee761e760e76773", + "ef0a3c7a6f14460fb4da096928ae249e", + "07fb3a2c8315494e97b447e672dfae06", + "ec030fc3c346426f9abc3a89892258d3", + "e3fb3fc6afe04b3c9b7ac61809ce78fa", + "c3be9109d63c485d9c0ef4f9bc0f9218", + "12815f401eba44658caa7b2e490137a8", + "30e02aa2d0d241979369e598287f2639", + "dfd2a2649b8341ef913207526708aff1", + "4f1977d7e4824ef1a14b65f0f42bba10", + "c6164e05a1914ae48083db9ad7f4ef7c", + "813621384dc748b0ad06775e22761c0b", + "dc892a596f6942d7973c616c38f0eebb", + "c84cc07789be48aebb322c23d355289e", + "bed8726b8069434687c75452e21f19e5", + "16a188a0b06d45f980dcf3933509fe0a", + "60c1a0d765c14a1d888317e6a507e4ea", + "0077aedc3d174560bce924ee89e9c006", + "00321cce58884f6f9b3855a21fcd9187", + "fa864b41586f4a7aa56aeafd1d84eb75", + "3225603166b54e7aab766b9964a2f660", + "349eee9f56d64f0cba6fc24ff2c50c9b", + "7e5d3774060e4589aa65982da5ea4ef4", + "7c2485c6cdfe463da6fdb35982a1070d", + "ad1236893754446881e153adc9d5c962", + "daee63fd167e4441a32324b51b00ad2b", + "fe41858c6bd04c58840112b67c19a336", + "d262c82138024169b9f3aa034ca756fa", + "62e302ebdad64aada0ffe64ae1c873f3", + "bd1b0dfed6d34d16af33a4a58330f5ec", + "d07c8b97d3314f1c852e44bdd40f61ed", + "ebb69a2c3d0a4299a484698287b3087c", + "e5a82df528bb4e408797a3b6c2758f4a", + "f113ebd8c1c34806bea4dd7ed3035173" + ] + }, + "id": "KQQhgK8FoDfF", + "outputId": "f69441d8-95f9-4885-c306-6c8709090ff6" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b82aa8c57f7c422a9a9c90f333ed2a99", + "version_major": 2, + "version_minor": 0 }, - "source": [ - "# Installation\n", - "\n", - "Axolotl is easy to install from [pip](https://pypi.org/project/axolotl/), or use our [pre-built Docker images](http://docs.axolotl.ai/docs/docker.html) for a hassle free dependency experience. See our [docs](http://docs.axolotl.ai/docs/installation.html) for more information." + "text/plain": [ + "tokenizer_config.json: 0%| | 0.00/9.68k [00:00=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0ee9ee8\"" + "text/plain": [ + "vocab.json: 0%| | 0.00/2.78M [00:00\u001b[39m\n", + "[2025-05-08 13:41:00,845] [DEBUG] [axolotl.utils.models.load_tokenizer:442] [PID:174] [RANK:0] BOS: None / None\u001b[39m\n", + "[2025-05-08 13:41:00,846] [DEBUG] [axolotl.utils.models.load_tokenizer:443] [PID:174] [RANK:0] PAD: 151643 / <|endoftext|>\u001b[39m\n", + "[2025-05-08 13:41:00,847] [DEBUG] [axolotl.utils.models.load_tokenizer:444] [PID:174] [RANK:0] UNK: None / None\u001b[39m\n", + "[2025-05-08 13:41:00,869] [INFO] [axolotl.utils.data.sft.load_tokenized_prepared_datasets:271] [PID:174] [RANK:0] Unable to find prepared dataset in last_run_prepared/97037817611d38b3a9c681753c3c4c95\u001b[39m\n", + "[2025-05-08 13:41:00,870] [INFO] [axolotl.utils.data.sft.load_tokenized_prepared_datasets:272] [PID:174] [RANK:0] Loading raw datasets...\u001b[39m\n", + "\u001b[33m[2025-05-08 13:41:00,870] [WARNING] [axolotl.utils.data.sft.load_tokenized_prepared_datasets:274] [PID:174] [RANK:0] Processing datasets during training can lead to VRAM instability. Please pre-process your dataset.\u001b[39m\n", + "[2025-05-08 13:41:00,871] [INFO] [axolotl.utils.data.sft.load_tokenized_prepared_datasets:281] [PID:174] [RANK:0] No seed provided, using default seed of 42\u001b[39m\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7cd0b85ebd204b7aba908417811ce4e0", + "version_major": 2, + "version_minor": 0 }, - "outputs": [], - "source": [ - "# Default to HF dataset location\n", - "dataset_id = \"winglian/pirate-ultrachat-10k\"\n", - "uploaded = {}" + "text/plain": [ + "train.jsonl: 0%| | 0.00/27.3M [00:00system\\n' }}\n", + " {%- if messages[0].role == 'system' %}\n", + " {{- messages[0].content + '\\n\\n' }}\n", + " {%- endif %}\n", + " {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n", + " {%- for tool in tools %}\n", + " {{- \"\\n\" }}\n", + " {{- tool | tojson }}\n", + " {%- endfor %}\n", + " {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n", + "{%- else %}\n", + " {%- if messages[0].role == 'system' %}\n", + " {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n", + " {%- endif %}\n", + "{%- endif %}\n", + "{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n", + "{%- for message in messages[::-1] %}\n", + " {%- set index = (messages|length - 1) - loop.index0 %}\n", + " {%- if ns.multi_step_tool and message.role == \"user\" and not(message.content.startswith('') and message.content.endswith('')) %}\n", + " {%- set ns.multi_step_tool = false %}\n", + " {%- set ns.last_query_index = index %}\n", + " {%- endif %}\n", + "{%- endfor %}\n", + "{%- for message in messages %}\n", + " {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n", + " {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n", + " {%- elif message.role == \"assistant\" %}\n", + " {%- set content = message.content %}\n", + " {%- set reasoning_content = '' %}\n", + " {%- if message.reasoning_content is defined and message.reasoning_content is not none %}\n", + " {%- set reasoning_content = message.reasoning_content %}\n", + " {%- else %}\n", + " {%- if '' in message.content %}\n", + " {%- set content = message.content.split('')[-1].lstrip('\\n') %}\n", + " {%- set reasoning_content = message.content.split('')[0].rstrip('\\n').split('')[-1].lstrip('\\n') %}\n", + " {%- endif %}\n", + " {%- endif %}\n", + " {%- if loop.index0 > ns.last_query_index %}\n", + " {%- if loop.last or (not loop.last and reasoning_content) %}\n", + " {{- '<|im_start|>' + message.role + '\\n\\n' + reasoning_content.strip('\\n') + '\\n\\n\\n' + content.lstrip('\\n') }}\n", + " {%- else %}\n", + " {{- '<|im_start|>' + message.role + '\\n' + content }}\n", + " {%- endif %}\n", + " {%- else %}\n", + " {{- '<|im_start|>' + message.role + '\\n' + content }}\n", + " {%- endif %}\n", + " {%- if message.tool_calls %}\n", + " {%- for tool_call in message.tool_calls %}\n", + " {%- if (loop.first and content) or (not loop.first) %}\n", + " {{- '\\n' }}\n", + " {%- endif %}\n", + " {%- if tool_call.function %}\n", + " {%- set tool_call = tool_call.function %}\n", + " {%- endif %}\n", + " {{- '\\n{\"name\": \"' }}\n", + " {{- tool_call.name }}\n", + " {{- '\", \"arguments\": ' }}\n", + " {%- if tool_call.arguments is string %}\n", + " {{- tool_call.arguments }}\n", + " {%- else %}\n", + " {{- tool_call.arguments | tojson }}\n", + " {%- endif %}\n", + " {{- '}\\n' }}\n", + " {%- endfor %}\n", + " {%- endif %}\n", + " {{- '<|im_end|>\\n' }}\n", + " {%- elif message.role == \"tool\" %}\n", + " {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n", + " {{- '<|im_start|>user' }}\n", + " {%- endif %}\n", + " {{- '\\n\\n' }}\n", + " {{- message.content }}\n", + " {{- '\\n' }}\n", + " {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n", + " {{- '<|im_end|>\\n' }}\n", + " {%- endif %}\n", + " {%- endif %}\n", + "{%- endfor %}\n", + "{%- if add_generation_prompt %}\n", + " {{- '<|im_start|>assistant\\n' }}\n", + " {%- if enable_thinking is defined and enable_thinking is false %}\n", + " {{- '\\n\\n\\n\\n' }}\n", + " {%- endif %}\n", + "{%- endif %}\n", + "---\u001b[39m\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "U6pTk3A9xj1W" + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "258b7c635c1045329d4669e48c46ccd5", + "version_major": 2, + "version_minor": 0 }, - "source": [ - "# Configure for Supervised Fine-Tuning (SFT)" + "text/plain": [ + "Tokenizing Prompts (num_proc=2): 0%| | 0/9985 [00:00\"],\n", - " }\n", - " ],\n", - " dataloader_prefetch_factor = 8, # dataloader optimizations\n", - " dataloader_num_workers = 2,\n", - " dataloader_pin_memory = True,\n", - " )\n", - "\n", - "# validates the configuration\n", - "cfg = load_cfg(config)" + "text/plain": [ + "Dropping Long Sequences (num_proc=2): 0%| | 0/9985 [00:00\u001b[39m\n", - "[2025-05-08 13:41:00,845] [DEBUG] [axolotl.utils.models.load_tokenizer:442] [PID:174] [RANK:0] BOS: None / None\u001b[39m\n", - "[2025-05-08 13:41:00,846] [DEBUG] [axolotl.utils.models.load_tokenizer:443] [PID:174] [RANK:0] PAD: 151643 / <|endoftext|>\u001b[39m\n", - "[2025-05-08 13:41:00,847] [DEBUG] [axolotl.utils.models.load_tokenizer:444] [PID:174] [RANK:0] UNK: None / None\u001b[39m\n", - "[2025-05-08 13:41:00,869] [INFO] [axolotl.utils.data.sft.load_tokenized_prepared_datasets:271] [PID:174] [RANK:0] Unable to find prepared dataset in last_run_prepared/97037817611d38b3a9c681753c3c4c95\u001b[39m\n", - "[2025-05-08 13:41:00,870] [INFO] [axolotl.utils.data.sft.load_tokenized_prepared_datasets:272] [PID:174] [RANK:0] Loading raw datasets...\u001b[39m\n", - "\u001b[33m[2025-05-08 13:41:00,870] [WARNING] [axolotl.utils.data.sft.load_tokenized_prepared_datasets:274] [PID:174] [RANK:0] Processing datasets during training can lead to VRAM instability. Please pre-process your dataset.\u001b[39m\n", - "[2025-05-08 13:41:00,871] [INFO] [axolotl.utils.data.sft.load_tokenized_prepared_datasets:281] [PID:174] [RANK:0] No seed provided, using default seed of 42\u001b[39m\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "7cd0b85ebd204b7aba908417811ce4e0", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "train.jsonl: 0%| | 0.00/27.3M [00:00system\\n' }}\n", - " {%- if messages[0].role == 'system' %}\n", - " {{- messages[0].content + '\\n\\n' }}\n", - " {%- endif %}\n", - " {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within XML tags:\\n\" }}\n", - " {%- for tool in tools %}\n", - " {{- \"\\n\" }}\n", - " {{- tool | tojson }}\n", - " {%- endfor %}\n", - " {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments within XML tags:\\n\\n{\\\"name\\\": , \\\"arguments\\\": }\\n<|im_end|>\\n\" }}\n", - "{%- else %}\n", - " {%- if messages[0].role == 'system' %}\n", - " {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n", - " {%- endif %}\n", - "{%- endif %}\n", - "{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n", - "{%- for message in messages[::-1] %}\n", - " {%- set index = (messages|length - 1) - loop.index0 %}\n", - " {%- if ns.multi_step_tool and message.role == \"user\" and not(message.content.startswith('') and message.content.endswith('')) %}\n", - " {%- set ns.multi_step_tool = false %}\n", - " {%- set ns.last_query_index = index %}\n", - " {%- endif %}\n", - "{%- endfor %}\n", - "{%- for message in messages %}\n", - " {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n", - " {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n", - " {%- elif message.role == \"assistant\" %}\n", - " {%- set content = message.content %}\n", - " {%- set reasoning_content = '' %}\n", - " {%- if message.reasoning_content is defined and message.reasoning_content is not none %}\n", - " {%- set reasoning_content = message.reasoning_content %}\n", - " {%- else %}\n", - " {%- if '' in message.content %}\n", - " {%- set content = message.content.split('')[-1].lstrip('\\n') %}\n", - " {%- set reasoning_content = message.content.split('')[0].rstrip('\\n').split('')[-1].lstrip('\\n') %}\n", - " {%- endif %}\n", - " {%- endif %}\n", - " {%- if loop.index0 > ns.last_query_index %}\n", - " {%- if loop.last or (not loop.last and reasoning_content) %}\n", - " {{- '<|im_start|>' + message.role + '\\n\\n' + reasoning_content.strip('\\n') + '\\n\\n\\n' + content.lstrip('\\n') }}\n", - " {%- else %}\n", - " {{- '<|im_start|>' + message.role + '\\n' + content }}\n", - " {%- endif %}\n", - " {%- else %}\n", - " {{- '<|im_start|>' + message.role + '\\n' + content }}\n", - " {%- endif %}\n", - " {%- if message.tool_calls %}\n", - " {%- for tool_call in message.tool_calls %}\n", - " {%- if (loop.first and content) or (not loop.first) %}\n", - " {{- '\\n' }}\n", - " {%- endif %}\n", - " {%- if tool_call.function %}\n", - " {%- set tool_call = tool_call.function %}\n", - " {%- endif %}\n", - " {{- '\\n{\"name\": \"' }}\n", - " {{- tool_call.name }}\n", - " {{- '\", \"arguments\": ' }}\n", - " {%- if tool_call.arguments is string %}\n", - " {{- tool_call.arguments }}\n", - " {%- else %}\n", - " {{- tool_call.arguments | tojson }}\n", - " {%- endif %}\n", - " {{- '}\\n' }}\n", - " {%- endfor %}\n", - " {%- endif %}\n", - " {{- '<|im_end|>\\n' }}\n", - " {%- elif message.role == \"tool\" %}\n", - " {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n", - " {{- '<|im_start|>user' }}\n", - " {%- endif %}\n", - " {{- '\\n\\n' }}\n", - " {{- message.content }}\n", - " {{- '\\n' }}\n", - " {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n", - " {{- '<|im_end|>\\n' }}\n", - " {%- endif %}\n", - " {%- endif %}\n", - "{%- endfor %}\n", - "{%- if add_generation_prompt %}\n", - " {{- '<|im_start|>assistant\\n' }}\n", - " {%- if enable_thinking is defined and enable_thinking is false %}\n", - " {{- '\\n\\n\\n\\n' }}\n", - " {%- endif %}\n", - "{%- endif %}\n", - "---\u001b[39m\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "258b7c635c1045329d4669e48c46ccd5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tokenizing Prompts (num_proc=2): 0%| | 0/9985 [00:00\n", - " \n", - " \n", - " [25/25 09:25, Epoch 0/1]\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
StepTraining Loss
11.092300
21.554200
31.041400
41.733800
51.430000
61.258500
71.343600
81.101700
91.086500
100.813200
110.689600
120.826700
131.541800
140.948000
151.357000
161.085800
171.516800
181.146800
190.834800
200.968000
211.388800
221.511500
231.338500
241.206600
251.504600

" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[2025-05-07 22:12:42,746] [INFO] [axolotl.callbacks.on_step_end:128] [PID:1336] [RANK:0] cuda memory usage while training: 9.768GB (+3.287GB cache, +0.646GB misc)\u001b[39m\n", - "[2025-05-07 22:21:46,859] [INFO] [axolotl.train.save_trained_model:231] [PID:1336] [RANK:0] Training completed! Saving pre-trained model to ./outputs/qwen-sft-pirate-rrr.\u001b[39m\n" - ] - } - ], - "source": [ - "from axolotl.train import train\n", - "\n", - "# just train the first 25 steps for demo.\n", - "# This is sufficient to align the model as we've used packing to maximize the trainable samples per step.\n", - "cfg.max_steps = 25\n", - "model, tokenizer, trainer = train(cfg=cfg, dataset_meta=dataset_meta)" + "text/plain": [ + "model-00002-of-00008.safetensors: 0%| | 0.00/3.96G [00:00 \n", - " sys.exit(main())\n", - " ^^^^^^\n", - " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/commands/huggingface_cli.py\", line 57, in main\n", - " service.run()\n", - " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/commands/upload.py\", line 207, in run\n", - " print(self._upload())\n", - " ^^^^^^^^^^^^^^\n", - " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/commands/upload.py\", line 302, in _upload\n", - " return self.api.upload_folder(\n", - " ^^^^^^^^^^^^^^^^^^^^^^^\n", - " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_validators.py\", line 114, in _inner_fn\n", - " return fn(*args, **kwargs)\n", - " ^^^^^^^^^^^^^^^^^^^\n", - " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 1633, in _inner\n", - " return fn(self, *args, **kwargs)\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 4942, in upload_folder\n", - " commit_info = self.create_commit(\n", - " ^^^^^^^^^^^^^^^^^^^\n", - " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_validators.py\", line 114, in _inner_fn\n", - " return fn(*args, **kwargs)\n", - " ^^^^^^^^^^^^^^^^^^^\n", - " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 1633, in _inner\n", - " return fn(self, *args, **kwargs)\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 4202, in create_commit\n", - " self.preupload_lfs_files(\n", - " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 4483, in preupload_lfs_files\n", - " _upload_xet_files(**upload_kwargs, create_pr=create_pr) # type: ignore [arg-type]\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_validators.py\", line 114, in _inner_fn\n", - " return fn(*args, **kwargs)\n", - " ^^^^^^^^^^^^^^^^^^^\n", - " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/_commit_api.py\", line 592, in _upload_xet_files\n", - " with progress_cm as progress:\n", - " File \"/usr/local/lib/python3.11/dist-packages/tqdm/std.py\", line 1138, in __exit__\n", - " def __exit__(self, exc_type, exc_value, traceback):\n", - "\n", - "KeyboardInterrupt\n", - "^C\n" - ] - } + "text/plain": [ + "model-00008-of-00008.safetensors: 0%| | 0.00/1.91G [00:00\n", + " \n", + " \n", + " [25/25 09:25, Epoch 0/1]\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
StepTraining Loss
11.092300
21.554200
31.041400
41.733800
51.430000
61.258500
71.343600
81.101700
91.086500
100.813200
110.689600
120.826700
131.541800
140.948000
151.357000
161.085800
171.516800
181.146800
190.834800
200.968000
211.388800
221.511500
231.338500
241.206600
251.504600

" ], - "source": [ - "from huggingface_hub import notebook_login\n", - "# remove the partial epoch checkpoints\n", - "!rm -rf \"./outputs/qwen-sft-pirate-rrr/checkpoint-*\"\n", - "\n", - "# HF Notebook login widget\n", - "notebook_login()\n", - "\n", - "# upload the LoRA adapter for your model to HF, remember to update the username/model-name below\n", - "!huggingface-cli upload --repo-type=model winglian/pirate-qwen-14B \"./outputs/qwen-sft-pirate-rrr\"" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2025-05-07 22:12:42,746] [INFO] [axolotl.callbacks.on_step_end:128] [PID:1336] [RANK:0] cuda memory usage while training: 9.768GB (+3.287GB cache, +0.646GB misc)\u001b[39m\n", + "[2025-05-07 22:21:46,859] [INFO] [axolotl.train.save_trained_model:231] [PID:1336] [RANK:0] Training completed! Saving pre-trained model to ./outputs/qwen-sft-pirate-rrr.\u001b[39m\n" + ] + } + ], + "source": [ + "from axolotl.train import train\n", + "\n", + "# just train the first 25 steps for demo.\n", + "# This is sufficient to align the model as we've used packing to maximize the trainable samples per step.\n", + "cfg.max_steps = 25\n", + "model, tokenizer, trainer = train(cfg=cfg, dataset_meta=dataset_meta)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "j1b9ypF78eCb" + }, + "source": [ + "# Inferencing the trained model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "r3_vHhif8YEs", + "outputId": "e5050605-f6c9-421c-98f9-bde56a281eae" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ahoy there, matey! Shiver me timbers, ye be lookin' for the Pythagorean theorem, eh? Well, hold yer horses and listen up, for I'll be tellin' ye all about it in me own special way.\n", + "\n", + "The Pythagorean theorem be a real gem of a mathematical trick that helps ye find the length of a side of a right triangle. Now, a right triangle be a triangle with a right angle, which be that little corner that looks like a square. \n", + "\n", + "The theorem be named after a clever fellow named Pythagoras, who be a mathematician from ancient Greece. He discovered that if ye have a right triangle, the square of the length of the hypotenuse (that be the side opposite the right angle) be equal to the sum of the squares of the other two sides. \n", + "\n", + "In other words, if ye have a triangle with sides of length a, b, and c (\n" + ] } - ], - "metadata": { - "accelerator": "GPU", + ], + "source": [ + "from transformers import TextStreamer\n", + "\n", + "messages = [\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": \"Explain the Pythagorean theorem to me.\",\n", + " },\n", + "]\n", + "\n", + "prompt = tokenizer.apply_chat_template(\n", + " messages,\n", + " add_generation_prompt=True,\n", + " tokenize=False,\n", + " enable_thinking=False,\n", + ")\n", + "\n", + "outputs = model.generate(\n", + " **tokenizer(prompt, return_tensors=\"pt\").to(\"cuda\"),\n", + " max_new_tokens=192,\n", + " temperature=1.0,\n", + " top_p=0.8,\n", + " top_k=32,\n", + " streamer=TextStreamer(tokenizer, skip_prompt=True),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HoGwT2JRSIjA" + }, + "source": [ + "# Saving your trained model\n", + "\n", + "Axolotl automatically saves checkpoints to the `output_dir` path.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "5BmSbiy6NaaS", + "outputId": "f5e1d913-7d55-42d2-8340-f9f1b0bc2b38" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "total 506M\n", + "-rw-r--r-- 1 root root 845 May 7 22:21 adapter_config.json\n", + "-rw-r--r-- 1 root root 491M May 7 22:21 adapter_model.safetensors\n", + "-rw-r--r-- 1 root root 707 May 7 22:11 added_tokens.json\n", + "drwxr-xr-x 2 root root 4.0K May 7 22:17 checkpoint-13\n", + "drwxr-xr-x 2 root root 4.0K May 7 22:21 checkpoint-25\n", + "-rw-r--r-- 1 root root 1.2K May 7 22:11 config.json\n", + "-rw-r--r-- 1 root root 1.6M May 7 22:11 merges.txt\n", + "-rw-r--r-- 1 root root 2.6K May 7 22:21 README.md\n", + "-rw-r--r-- 1 root root 613 May 7 22:11 special_tokens_map.json\n", + "-rw-r--r-- 1 root root 9.5K May 7 22:11 tokenizer_config.json\n", + "-rw-r--r-- 1 root root 11M May 7 22:11 tokenizer.json\n", + "-rw-r--r-- 1 root root 2.7M May 7 22:11 vocab.json\n" + ] + } + ], + "source": [ + "# Show the saved checkpoints in the output_dir\n", + "!ls -lh \"./outputs/qwen-sft-pirate-rrr\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_PCIFWxuOZd6" + }, + "source": [ + "Setting `hub_model_id: ` in the original config would have automatically uploaded the model to HuggingFace Hub (e.g. `hub_model_id: username/model_id`)\n", + "\n", + "If you prefer to manually upload the training artifacts, we can still upload the entire final checkpoint to HuggingFace from the CLI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { "colab": { - "gpuType": "T4", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "name": "python" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "00321cce58884f6f9b3855a21fcd9187": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "004d9177a6a14118a5930dc3cc13147b": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_a80410b919e442c49aea15acc1ce1a72", - "IPY_MODEL_c6e00f5224364822bc4239b176686919", - "IPY_MODEL_ec11d1e5ae7b42c883d9b1f38a65356e" - ], - "layout": "IPY_MODEL_734185351eb543fa9a00a881dcbb9fe7" - } - }, - "0077aedc3d174560bce924ee89e9c006": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "03a3c744d716431488163b4358b80f92": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "03b093d592ba4386aa61f7b8483da660": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_b8766a88716948cf968f4563531a76d9", - "IPY_MODEL_6f3a28b912714c6e931003549664bfa3", - "IPY_MODEL_16d1283741404b7bb319094c992fce01" - ], - "layout": "IPY_MODEL_2a5bb0e818ab47be8cf6465988328503" - } - }, - "042e091f75694c47aee761e760e76773": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "0546d04aae644dde846c58a4afb598a6": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "054c8dffadba48c6b895a6cc62448ecc": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "07fb3a2c8315494e97b447e672dfae06": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_12815f401eba44658caa7b2e490137a8", - "placeholder": "​", - "style": "IPY_MODEL_30e02aa2d0d241979369e598287f2639", - "value": "Drop Samples with Zero Trainable Tokens (num_proc=2): 100%" - } - }, - "083f9cda8d754c168beee10d2f8955a2": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_a0a11e929edd4189b79723d618522c33", - "max": 728, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_e87ea87fcff247b5bbcc331ba79a8dc2", - "value": 728 - } - }, - "09007681cf8d42aeb8c1d2f6a74e470a": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_b195f160ca20442fadd8b5aed0ee41af", - "placeholder": "​", - "style": "IPY_MODEL_ca65e32eb52f48c09a84b33cb18f22cd", - "value": " 11.4M/11.4M [00:00<00:00, 21.8MB/s]" - } - }, - "0a46ad75c198463d843fb35e813642cb": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_b8e39e4dddc3497fbc29ae45c66da759", - "max": 11422654, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_63b4e563e85c4f03b1b72beda9577bcc", - "value": 11422654 - } - }, - "0aa8ab56b85f4171a79c3bc210594025": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "0b4c9753a7cb4354b8e5f187e6e1ad7c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "0cd7efffbb3c4c4b972e63749f61ab97": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "0dea5caa27384f5689e3cab51f558727": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "0e067d8db8ed48308a718d5f57683fd1": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_b1bea589efa14258a9982071b87938bf", - "placeholder": "​", - "style": "IPY_MODEL_590eef89881545aa8bbef9a8bbe7fb00", - "value": "\nPro Tip: If you don't already have one, you can create a dedicated\n'notebooks' token with 'write' access, that you can then easily reuse for all\nnotebooks. " - } - }, - "0e50870ed0c643e0b6c18cc5d7ddae7f": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_bfcdbba993b74972a9e3e575f86908ff", - "placeholder": "​", - "style": "IPY_MODEL_6ebb2ec171414e47a14765505f64bb3c", - "value": " 3.84G/3.84G [00:09<00:00, 664MB/s]" - } - }, - "0e936d9dbf9c4fdd86bbfe9730dedc47": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "0f417447a7bd4a33acca96fa37aec877": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "0f480e3a0b0a45d2a2d2dec3cad923f3": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "0f6907ebbc6242c8bde059cef1e1bd29": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_5bdfd87fc6cd4f9dabef7cfee29c8060", - "IPY_MODEL_64f54d4a744a4627a07c3c0120276f3b", - "IPY_MODEL_65b75b9b8bc143cf997796af68ff6668" - ], - "layout": "IPY_MODEL_d6fe74e4255444368f8f90a62157d869" - } - }, - "114dece49dba437c8572ef94b23c3b1e": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "12815f401eba44658caa7b2e490137a8": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "12b56912736849fea2ad8124456fdc5c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_97e36007e1304e1583fd81bfb13f0edd", - "max": 1671853, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_c65dc74c7d6f4bab8f7dd28455161dd8", - "value": 1671853 - } - }, - "131065f118274a1586ac38e39ed84ef0": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": "center", - "align_self": null, - "border": null, - "bottom": null, - "display": "flex", - "flex": null, - "flex_flow": "column", - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": "50%" - } - }, - "158c8b85dbf34de6a94b4e35e2fc7d5a": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "16a188a0b06d45f980dcf3933509fe0a": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_349eee9f56d64f0cba6fc24ff2c50c9b", - "placeholder": "​", - "style": "IPY_MODEL_7e5d3774060e4589aa65982da5ea4ef4", - "value": " 9985/9985 [00:04<00:00, 2604.11 examples/s]" - } - }, - "16d1283741404b7bb319094c992fce01": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_a4e5789584564049b83df7c6c54a3e08", - "placeholder": "​", - "style": "IPY_MODEL_ff3a94b146a948b6907f5d80c7157f99", - "value": " 9985/0 [00:00<00:00, 50763.46 examples/s]" - } - }, - "1811cda0644e4190a9469d1774435d82": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "18357b321ce44d7b8bd9d1c886f69275": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_e366ae3fceec4566b9ed303d6c5f90af", - "placeholder": "​", - "style": "IPY_MODEL_5dd7d150dbe04f08b165ce7f2c27cd11", - "value": "model-00008-of-00008.safetensors: 100%" - } - }, - "19127c7bb1554ccbac877059f9a82db0": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "danger", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_e400cbf14bcc446a9d33b210cd93550b", - "max": 3963750880, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_71002199df6b40c9a1ac40df5fb27a1b", - "value": 3963750502 - } - }, - "19c1e38389fa46c7b7e2152a56e1df34": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ButtonModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ButtonModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ButtonView", - "button_style": "", - "description": "Login", - "disabled": false, - "icon": "", - "layout": "IPY_MODEL_835bcc28a5564fb9b3d651bc8e32dc46", - "style": "IPY_MODEL_9f1c9a0695384bdaa6f8b847ef89bee8", - "tooltip": "" - } - }, - "1bec6297c90242a88672d195bc09d429": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "1c6f1f10667545aaab958016ba7e2c94": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "1d5117195d4b49eb8f1a73b18419f7ce": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_0dea5caa27384f5689e3cab51f558727", - "placeholder": "​", - "style": "IPY_MODEL_a6f48410b9964fefba0c3009a77dc838", - "value": " 9.68k/9.68k [00:00<00:00, 812kB/s]" - } - }, - "1f7d30f71bbd4547a9150d21da071055": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "200df5e79b9244849e589ecb0250a520": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_f4a1795dc7514a718f478245f521f0ba", - "placeholder": "​", - "style": "IPY_MODEL_5e746eb25bbe416fb585fa24e79f5177", - "value": "model-00002-of-00008.safetensors: 100%" - } - }, - "20352e5f58d24bb8b1f3940efd14fe4a": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "253017b0d0534e54ab44e181f6d7c82d": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_1c6f1f10667545aaab958016ba7e2c94", - "placeholder": "​", - "style": "IPY_MODEL_e6e969610738449887259063967f82b0", - "value": " 2.78M/2.78M [00:00<00:00, 17.8MB/s]" - } - }, - "258b7c635c1045329d4669e48c46ccd5": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_6f68ed9889f54ad2ae8a3b95ac263a83", - "IPY_MODEL_80366349d81e4dcc892db6cd56e384f3", - "IPY_MODEL_c73055099c084dca996159e23e162d0b" - ], - "layout": "IPY_MODEL_977f799afaac4a55b2dc1cffa7d5b63b" - } - }, - "279937fe03bc4e4eb25b472d7e9df163": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "danger", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_b634bb73cfa743d09a5999101b840976", - "max": 1912371880, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_742b1030acfd414bbd9d5327b7e3826d", - "value": 1912371698 - } - }, - "27beaf06e41b472abdb544a43c720c5a": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "2860e3bb3baf4f7da058465850e800c5": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_3efd18ea8eaa41918894883da9541bfa", - "IPY_MODEL_e09f1bcbb9d94c09be53e5e1303642c2", - "IPY_MODEL_82177df57a494de8900c14c2f5185175" - ], - "layout": "IPY_MODEL_ccfcdc95baf646f8aeb3d516742383f2" - } - }, - "2a51b36be41745468e4c2d7a21b1c0d2": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "2a5bb0e818ab47be8cf6465988328503": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "2b3a2659b12244bd8548320320016dbf": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "2e257c8be2da40b4bb67a9e4ab6811f3": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "2e2b0c1599c341a198f632f46a40c90e": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_be724f04b03942b2a033a7e8898bb4fd", - "placeholder": "​", - "style": "IPY_MODEL_fcbab4d8dced41a18dfccce81e3a45a0", - "value": "model-00005-of-00008.safetensors: 100%" - } - }, - "3036608c71904ce9ae4bb2a9fa8802d9": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_5ca6be24acb548cea130bd58e9954c7c", - "placeholder": "​", - "style": "IPY_MODEL_5cfb02ee044b4011a378efa8b54a370f", - "value": " 3.96G/3.96G [00:10<00:00, 531MB/s]" - } - }, - "30a81da86f8043eca301e86a8651201a": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "30e02aa2d0d241979369e598287f2639": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "3225603166b54e7aab766b9964a2f660": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "33b3b1d0295646edaac7b4822761aeb0": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "349eee9f56d64f0cba6fc24ff2c50c9b": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "34c9c0137b504cd799c6bd6de69507c2": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "34cf3df51fbc41cabfdbba153c007f0e": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "35c811d2ae8e43f3b5cecbdd3cfa857f": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "35cc989ca3374e7dba0cb166febc4bde": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "366a343b62fa47d8985a3bd464d99f9e": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "37de928300e34184881039378bd75e7f": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "388f618924274d21a066f098f4f1e744": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_7c95f85a2b1f47a1bd846d110c47bb3c", - "IPY_MODEL_083f9cda8d754c168beee10d2f8955a2", - "IPY_MODEL_62e1a65582f446a78612eaa804e08a7d" - ], - "layout": "IPY_MODEL_487a177d020f4605834878b2fdc7afa3" - } - }, - "39789237703c4a418134243055c9cbf5": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "3aaecbf540f54a2db9ab0931e3b1fe57": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "3c21e4a511b4441192c03b7f1d0976e9": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "3efd18ea8eaa41918894883da9541bfa": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_8f5bd719974e41c3a8dd9a5b0d3d71e6", - "placeholder": "​", - "style": "IPY_MODEL_b87c84de30e84b3abf4871461fb9cbd3", - "value": "Loading checkpoint shards: 100%" - } - }, - "41f3b32c2f6b4034ae7a3b9124e28bc7": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "4471ff62258549fba9514bb67050f965": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_9cd5211b5d8b457aa0002f1d17b80028", - "IPY_MODEL_19127c7bb1554ccbac877059f9a82db0", - "IPY_MODEL_f4667818b9d34a09891cd727a429a610" - ], - "layout": "IPY_MODEL_9ed02dc43412471a9ab47f3620ccf3a5" - } - }, - "4540927d98f54466b434ba4c0edf045d": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "487a177d020f4605834878b2fdc7afa3": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "4b1f04ff63d14a118fdd15814dff50e4": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "LabelModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "LabelModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "LabelView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_39789237703c4a418134243055c9cbf5", - "placeholder": "​", - "style": "IPY_MODEL_a3a945817f684328b34651fe052393ec", - "value": "Connecting..." - } - }, - "4b27c267393640f28f6eae0875bd2ed9": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "4c727d40ef0443449afc31724ee79f0c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "4d05314858354e729d76094b3b0ce761": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_c42acf646f344a88b8c11f81e67f7206", - "IPY_MODEL_7be6f04c284e4326bb4ff3d301e7b3c6", - "IPY_MODEL_ffdbb12a2f2c4d14911685e7683e0ef0" - ], - "layout": "IPY_MODEL_bee3501b2a17427784a717e50a85e7fa" - } - }, - "4d468f96ec924681ad65eb671674b93e": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "4f1977d7e4824ef1a14b65f0f42bba10": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "4fd114abe9f5494ab59858949f5055f1": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "500e272208a246089613bf788a165271": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_200df5e79b9244849e589ecb0250a520", - "IPY_MODEL_cc94432d08464affa3e58b560bdad194", - "IPY_MODEL_3036608c71904ce9ae4bb2a9fa8802d9" - ], - "layout": "IPY_MODEL_adacfdcc1b0140efac56918e9ccf064e" - } - }, - "519a7b154022443db6703f04a9142bae": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_d02274afd47b462291c745f261209d42", - "max": 27341251, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_0f417447a7bd4a33acca96fa37aec877", - "value": 27341251 - } - }, - "56e3768bef5a4b9db4168c5c17f509c2": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "590eef89881545aa8bbef9a8bbe7fb00": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "598da69727bd4fb8b1caf465ac736d7a": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "5bdfd87fc6cd4f9dabef7cfee29c8060": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_4d468f96ec924681ad65eb671674b93e", - "placeholder": "​", - "style": "IPY_MODEL_ad7599de524549c48bf2d3124ad4b299", - "value": "Dropping Long Sequences (num_proc=2): 100%" - } - }, - "5ca240f31e6b44e3882c5eb37cd5a309": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": "20px" - } - }, - "5ca6be24acb548cea130bd58e9954c7c": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "5cea7996f02040b187ece0bb2d6a8d1f": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "5cfb02ee044b4011a378efa8b54a370f": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "5dd7d150dbe04f08b165ce7f2c27cd11": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "5e18768f7ad6434ba8b8b8a2e853e204": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "5e5e15b0569b474c9620083b3ec6af55": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "5e746eb25bbe416fb585fa24e79f5177": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "5eb06edeb58e4930b1affef2a59eae81": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "5f86cd894de94c3280fadc1e2fd0ee13": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_a20927bf5f2c41f58c1e31ac858ab36c", - "IPY_MODEL_0a46ad75c198463d843fb35e813642cb", - "IPY_MODEL_09007681cf8d42aeb8c1d2f6a74e470a" - ], - "layout": "IPY_MODEL_ebc80d1a55fa47f4a5ea2756588569ec" - } - }, - "60c1a0d765c14a1d888317e6a507e4ea": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "62c028fdef904dedb9cdeca2b3bda725": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "62e1a65582f446a78612eaa804e08a7d": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_5e18768f7ad6434ba8b8b8a2e853e204", - "placeholder": "​", - "style": "IPY_MODEL_bb33aec33a6447078c31bfd728942994", - "value": " 728/728 [00:00<00:00, 20.3kB/s]" - } - }, - "62e302ebdad64aada0ffe64ae1c873f3": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "63580b6fb30642479fe3000915bf551a": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "63b4e563e85c4f03b1b72beda9577bcc": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "64f54d4a744a4627a07c3c0120276f3b": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_0546d04aae644dde846c58a4afb598a6", - "max": 9985, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_897b77a56c09479bb11d7f2a30997e55", - "value": 9985 - } - }, - "65b75b9b8bc143cf997796af68ff6668": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_81c3db71ac704280ad030072655f1537", - "placeholder": "​", - "style": "IPY_MODEL_042e091f75694c47aee761e760e76773", - "value": " 9985/9985 [00:02<00:00, 3977.47 examples/s]" - } - }, - "67da6c4260574869aa24c3cbc1bc1654": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "6932489232ec4ab18a160b1e7fbcdfe1": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "6ebb2ec171414e47a14765505f64bb3c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "6f05e9bebf7b40c9835808e77de6c236": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "PasswordModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "PasswordModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "PasswordView", - "continuous_update": true, - "description": "Token:", - "description_tooltip": null, - "disabled": false, - "layout": "IPY_MODEL_2e257c8be2da40b4bb67a9e4ab6811f3", - "placeholder": "​", - "style": "IPY_MODEL_56e3768bef5a4b9db4168c5c17f509c2", - "value": "" - } - }, - "6f3a28b912714c6e931003549664bfa3": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_5ca240f31e6b44e3882c5eb37cd5a309", - "max": 1, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_5eb06edeb58e4930b1affef2a59eae81", - "value": 1 - } - }, - "6f68ed9889f54ad2ae8a3b95ac263a83": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_41f3b32c2f6b4034ae7a3b9124e28bc7", - "placeholder": "​", - "style": "IPY_MODEL_a10d0a76010f4e508c65a9b69ebc5156", - "value": "Tokenizing Prompts (num_proc=2): 100%" - } - }, - "704f2f5a9b1c49d5a75a0025a5dda11b": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "71002199df6b40c9a1ac40df5fb27a1b": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "71c8af139cd248b1b51101fd46a93f35": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_d0e9dce55cec4c1ca619a0ccf209d924", - "max": 9675, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_4c727d40ef0443449afc31724ee79f0c", - "value": 9675 - } - }, - "734185351eb543fa9a00a881dcbb9fe7": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "735d4f225b24414294fc1b213c61223c": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "742b1030acfd414bbd9d5327b7e3826d": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "77304d1a46b3468a98483e02ec0ac4a4": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "7baeab52d6694c32b1efd1ea1a0a7782": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_93a44a11aa4846fa8efc6c1413ef1627", - "placeholder": "​", - "style": "IPY_MODEL_a55060adc3564407ac81ad7297d34aaa", - "value": "train.jsonl: 100%" - } - }, - "7be6f04c284e4326bb4ff3d301e7b3c6": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "danger", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_9503a45960984adc97b58e16c50662e0", - "max": 3963750880, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_da6e93f3e4984780b930fe7a706983ea", - "value": 3963750502 - } - }, - "7c2485c6cdfe463da6fdb35982a1070d": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_ad1236893754446881e153adc9d5c962", - "IPY_MODEL_daee63fd167e4441a32324b51b00ad2b", - "IPY_MODEL_fe41858c6bd04c58840112b67c19a336" - ], - "layout": "IPY_MODEL_d262c82138024169b9f3aa034ca756fa" - } - }, - "7c95f85a2b1f47a1bd846d110c47bb3c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_7fd44cf9ca6e4726bfd7ac21846d6a14", - "placeholder": "​", - "style": "IPY_MODEL_366a343b62fa47d8985a3bd464d99f9e", - "value": "config.json: 100%" - } - }, - "7cd0b85ebd204b7aba908417811ce4e0": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_7baeab52d6694c32b1efd1ea1a0a7782", - "IPY_MODEL_519a7b154022443db6703f04a9142bae", - "IPY_MODEL_d4183e9715f34d249942b8271cca3bdf" - ], - "layout": "IPY_MODEL_da2347ac94764a3fa2743343cf0d3cd2" - } - }, - "7e5d3774060e4589aa65982da5ea4ef4": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "7fd44cf9ca6e4726bfd7ac21846d6a14": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "80366349d81e4dcc892db6cd56e384f3": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_f8ef805b776145c3bfa9ba8d90972058", - "max": 9985, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_cc587493c33c4f118d1b1170f85be24c", - "value": 9985 - } - }, - "813621384dc748b0ad06775e22761c0b": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "81c3db71ac704280ad030072655f1537": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "82177df57a494de8900c14c2f5185175": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_67da6c4260574869aa24c3cbc1bc1654", - "placeholder": "​", - "style": "IPY_MODEL_94b9088614464f60a203de39dbcae853", - "value": " 8/8 [01:47<00:00, 11.64s/it]" - } - }, - "823f1c78f15043e38bbd4dca3932a86a": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_03a3c744d716431488163b4358b80f92", - "max": 239, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_a5434ee714f9498d83870544b67c0cb7", - "value": 239 - } - }, - "835bcc28a5564fb9b3d651bc8e32dc46": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "8640ac440fbc4644b9a3af7ba3ae7183": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "86816687746246b4a6105e8010384e25": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_8640ac440fbc4644b9a3af7ba3ae7183", - "placeholder": "​", - "style": "IPY_MODEL_5cea7996f02040b187ece0bb2d6a8d1f", - "value": "


Copy a token from your Hugging Face\ntokens page and paste it below.
Immediately click login after copying\nyour token or it might be stored in plain text in this notebook file.
" - } - }, - "879c8ab5873847a8833bd74123be90a4": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_ef223e8504b64e3592589880326aaf41", - "placeholder": "​", - "style": "IPY_MODEL_598da69727bd4fb8b1caf465ac736d7a", - "value": " 1.67M/1.67M [00:00<00:00, 19.0MB/s]" - } - }, - "897b77a56c09479bb11d7f2a30997e55": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "8bc9d8ba866c442b9118d9630009939c": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "8c4d4fc5a30f4e7cb3be53fe2adda33d": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "8f5bd719974e41c3a8dd9a5b0d3d71e6": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "8f726dbfb45d4528afa33e36a6313267": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "9327977822be4b1294f80e876552e305": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_37de928300e34184881039378bd75e7f", - "placeholder": "​", - "style": "IPY_MODEL_0e936d9dbf9c4fdd86bbfe9730dedc47", - "value": " 3.96G/3.96G [00:13<00:00, 273MB/s]" - } - }, - "936d04b5fe1b4c63bf0b080e423d051b": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "93a44a11aa4846fa8efc6c1413ef1627": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "94b9088614464f60a203de39dbcae853": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "9503a45960984adc97b58e16c50662e0": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "95caff42f08a4c2aa14c867b8f37f231": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_de7c37ee83e24f0c889e84d07279c2ec", - "IPY_MODEL_9d4897eefb5f48259ffb2d23e332f752", - "IPY_MODEL_253017b0d0534e54ab44e181f6d7c82d" - ], - "layout": "IPY_MODEL_27beaf06e41b472abdb544a43c720c5a" - } - }, - "977f799afaac4a55b2dc1cffa7d5b63b": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "97e36007e1304e1583fd81bfb13f0edd": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "9858cb74a09748a39e8149baac96702c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "9b42e08b3c9548818488268768a118b1": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_d955dcaa0e944e719f3a06139dd54a03", - "placeholder": "​", - "style": "IPY_MODEL_d3de2662c7964f1ba96e58da382af720", - "value": "merges.txt: 100%" - } - }, - "9cd5211b5d8b457aa0002f1d17b80028": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_6932489232ec4ab18a160b1e7fbcdfe1", - "placeholder": "​", - "style": "IPY_MODEL_4540927d98f54466b434ba4c0edf045d", - "value": "model-00007-of-00008.safetensors: 100%" - } - }, - "9d4897eefb5f48259ffb2d23e332f752": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_30a81da86f8043eca301e86a8651201a", - "max": 2776833, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_e8b7a81040904c1e89e58978223b1737", - "value": 2776833 - } - }, - "9e333ed3b5014069ac1dd969255dd591": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "9ed02dc43412471a9ab47f3620ccf3a5": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "9f1c9a0695384bdaa6f8b847ef89bee8": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ButtonStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ButtonStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "button_color": null, - "font_weight": "" - } - }, - "9f56a2d9979c4bd8928c644c22c3ecdf": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "a0a11e929edd4189b79723d618522c33": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "a10d0a76010f4e508c65a9b69ebc5156": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "a138859f19b74fc0928dc236ab5359db": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_9b42e08b3c9548818488268768a118b1", - "IPY_MODEL_12b56912736849fea2ad8124456fdc5c", - "IPY_MODEL_879c8ab5873847a8833bd74123be90a4" - ], - "layout": "IPY_MODEL_20352e5f58d24bb8b1f3940efd14fe4a" - } - }, - "a1959759c5424da9961fb2a308d4dee4": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_3aaecbf540f54a2db9ab0931e3b1fe57", - "placeholder": "​", - "style": "IPY_MODEL_9e333ed3b5014069ac1dd969255dd591", - "value": " 239/239 [00:00<00:00, 30.9kB/s]" - } - }, - "a20927bf5f2c41f58c1e31ac858ab36c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_1811cda0644e4190a9469d1774435d82", - "placeholder": "​", - "style": "IPY_MODEL_35c811d2ae8e43f3b5cecbdd3cfa857f", - "value": "tokenizer.json: 100%" - } - }, - "a3a945817f684328b34651fe052393ec": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "a44f630e099e43899f20a77084ae60cd": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_ed5ca967ad5342929e578ac6aa4dc4c0", - "placeholder": "​", - "style": "IPY_MODEL_af401d117d5047629d3a6e2361757b62", - "value": "model-00001-of-00008.safetensors: 100%" - } - }, - "a4e5789584564049b83df7c6c54a3e08": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "a5434ee714f9498d83870544b67c0cb7": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "a55060adc3564407ac81ad7297d34aaa": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "a6f48410b9964fefba0c3009a77dc838": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "a7cf477e80fc43e0ad82c7997b076dce": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "a80410b919e442c49aea15acc1ce1a72": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_fa1282ccc7544e4f818e2f03ccffe4a5", - "placeholder": "​", - "style": "IPY_MODEL_bbbf575d2a4b4c6ea8389be79b2a6039", - "value": "model.safetensors.index.json: 100%" - } - }, - "ab93eabd7cea4b94b4b7a387f101e8a1": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "ac764024cf1c4e08ba7749afd2cd20ac": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "ad1236893754446881e153adc9d5c962": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_62e302ebdad64aada0ffe64ae1c873f3", - "placeholder": "​", - "style": "IPY_MODEL_bd1b0dfed6d34d16af33a4a58330f5ec", - "value": "Saving the dataset (1/1 shards): 100%" - } - }, - "ad7599de524549c48bf2d3124ad4b299": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "adacfdcc1b0140efac56918e9ccf064e": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "af401d117d5047629d3a6e2361757b62": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "b191ac001a2e4962bc9a245fcdf26e6b": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "b195f160ca20442fadd8b5aed0ee41af": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "b1bea589efa14258a9982071b87938bf": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "b5b65414154544aa8a71b1a39164aad7": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "b634bb73cfa743d09a5999101b840976": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "b82aa8c57f7c422a9a9c90f333ed2a99": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_c0991cf63ee6458b96e9a75e7a88b61a", - "IPY_MODEL_71c8af139cd248b1b51101fd46a93f35", - "IPY_MODEL_1d5117195d4b49eb8f1a73b18419f7ce" - ], - "layout": "IPY_MODEL_3c21e4a511b4441192c03b7f1d0976e9" - } - }, - "b8766a88716948cf968f4563531a76d9": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_2b3a2659b12244bd8548320320016dbf", - "placeholder": "​", - "style": "IPY_MODEL_0cd7efffbb3c4c4b972e63749f61ab97", - "value": "Generating train split: " - } - }, - "b87c84de30e84b3abf4871461fb9cbd3": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "b8e39e4dddc3497fbc29ae45c66da759": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "bb33aec33a6447078c31bfd728942994": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "bbbf575d2a4b4c6ea8389be79b2a6039": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "bca2c7185b6749fd899c06a2ba4c5e46": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_0f480e3a0b0a45d2a2d2dec3cad923f3", - "placeholder": "​", - "style": "IPY_MODEL_fcb30372e7404c5d8a1ad4df91e6c7b2", - "value": " 1.91G/1.91G [00:05<00:00, 444MB/s]" - } - }, - "bd1b0dfed6d34d16af33a4a58330f5ec": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "be724f04b03942b2a033a7e8898bb4fd": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "bed8726b8069434687c75452e21f19e5": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_fa864b41586f4a7aa56aeafd1d84eb75", - "max": 9985, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_3225603166b54e7aab766b9964a2f660", - "value": 9985 - } - }, - "bee3501b2a17427784a717e50a85e7fa": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "bfcdbba993b74972a9e3e575f86908ff": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "bff139df987d4a62abec6456cb27f3d4": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "danger", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_c1f9c267ba3f40039cdb5eb3267e8043", - "max": 3963750880, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_33b3b1d0295646edaac7b4822761aeb0", - "value": 3963750502 - } - }, - "c0892a1881de4eb4bfabc6a68f87ae99": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_158c8b85dbf34de6a94b4e35e2fc7d5a", - "placeholder": "​", - "style": "IPY_MODEL_0b4c9753a7cb4354b8e5f187e6e1ad7c", - "value": " 3.96G/3.96G [00:15<00:00, 564MB/s]" - } - }, - "c0991cf63ee6458b96e9a75e7a88b61a": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_ed28e2e0410d4e0b855467e798e53d66", - "placeholder": "​", - "style": "IPY_MODEL_d93f134f802b4b69b575bdaf07dbd27c", - "value": "tokenizer_config.json: 100%" - } - }, - "c12ea43372ac4d57bb9605f1a429b397": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "VBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "VBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "VBoxView", - "box_style": "", - "children": [], - "layout": "IPY_MODEL_131065f118274a1586ac38e39ed84ef0" - } - }, - "c1314f241a434c41b45d84dc4d3b30f8": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "c1f9c267ba3f40039cdb5eb3267e8043": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "c33ced495f70464aa4a3a91922090853": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "c3725c7f79fe415fbd1ea336f0cc9cf1": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "danger", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_b191ac001a2e4962bc9a245fcdf26e6b", - "max": 3841788544, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_054c8dffadba48c6b895a6cc62448ecc", - "value": 3841788178 - } - }, - "c3be9109d63c485d9c0ef4f9bc0f9218": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "c42acf646f344a88b8c11f81e67f7206": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_8bc9d8ba866c442b9118d9630009939c", - "placeholder": "​", - "style": "IPY_MODEL_9f56a2d9979c4bd8928c644c22c3ecdf", - "value": "model-00003-of-00008.safetensors: 100%" - } - }, - "c6164e05a1914ae48083db9ad7f4ef7c": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "c65dc74c7d6f4bab8f7dd28455161dd8": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "c6e00f5224364822bc4239b176686919": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_2a51b36be41745468e4c2d7a21b1c0d2", - "max": 36514, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_4fd114abe9f5494ab59858949f5055f1", - "value": 36514 - } - }, - "c73055099c084dca996159e23e162d0b": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_e40d1c1ac9494b3bade9858324e7ffdf", - "placeholder": "​", - "style": "IPY_MODEL_d65b6b060d9845779299491ac5599c31", - "value": " 9985/9985 [01:04<00:00, 189.08 examples/s]" - } - }, - "c7433acd3c4841e6958ae8f7e87b1808": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "CheckboxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "CheckboxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "CheckboxView", - "description": "Add token as git credential?", - "description_tooltip": null, - "disabled": false, - "indent": true, - "layout": "IPY_MODEL_62c028fdef904dedb9cdeca2b3bda725", - "style": "IPY_MODEL_a7cf477e80fc43e0ad82c7997b076dce", - "value": false - } - }, - "c84cc07789be48aebb322c23d355289e": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_0077aedc3d174560bce924ee89e9c006", - "placeholder": "​", - "style": "IPY_MODEL_00321cce58884f6f9b3855a21fcd9187", - "value": "Add position_id column (Sample Packing) (num_proc=2): 100%" - } - }, - "ca65e32eb52f48c09a84b33cb18f22cd": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "cc587493c33c4f118d1b1170f85be24c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "cc94432d08464affa3e58b560bdad194": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "danger", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_b5b65414154544aa8a71b1a39164aad7", - "max": 3963750816, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_f0a58fbd0fca4340890041f99fa2f8c8", - "value": 3963750438 - } - }, - "ccfcdc95baf646f8aeb3d516742383f2": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "cdebbc55a1164c018546c2ac6f8c620c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_a44f630e099e43899f20a77084ae60cd", - "IPY_MODEL_c3725c7f79fe415fbd1ea336f0cc9cf1", - "IPY_MODEL_0e50870ed0c643e0b6c18cc5d7ddae7f" - ], - "layout": "IPY_MODEL_c33ced495f70464aa4a3a91922090853" - } - }, - "d02274afd47b462291c745f261209d42": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "d07c8b97d3314f1c852e44bdd40f61ed": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "d0e9dce55cec4c1ca619a0ccf209d924": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "d1f9b10c130542f094c8fd3d1e23b5e9": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "d262c82138024169b9f3aa034ca756fa": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "d3de2662c7964f1ba96e58da382af720": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "d4183e9715f34d249942b8271cca3bdf": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_63580b6fb30642479fe3000915bf551a", - "placeholder": "​", - "style": "IPY_MODEL_8f726dbfb45d4528afa33e36a6313267", - "value": " 27.3M/27.3M [00:00<00:00, 31.0MB/s]" - } - }, - "d43c6df07ddb466587807d6dbe1ff614": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_8c4d4fc5a30f4e7cb3be53fe2adda33d", - "placeholder": "​", - "style": "IPY_MODEL_e90658f4bcb642baa78426012f863152", - "value": "model-00004-of-00008.safetensors: 100%" - } - }, - "d65b6b060d9845779299491ac5599c31": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "d6fe74e4255444368f8f90a62157d869": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "d93f134f802b4b69b575bdaf07dbd27c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "d955dcaa0e944e719f3a06139dd54a03": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "da2347ac94764a3fa2743343cf0d3cd2": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "da6e93f3e4984780b930fe7a706983ea": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "daee63fd167e4441a32324b51b00ad2b": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_d07c8b97d3314f1c852e44bdd40f61ed", - "max": 9985, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_ebb69a2c3d0a4299a484698287b3087c", - "value": 9985 - } - }, - "dc892a596f6942d7973c616c38f0eebb": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_c84cc07789be48aebb322c23d355289e", - "IPY_MODEL_bed8726b8069434687c75452e21f19e5", - "IPY_MODEL_16a188a0b06d45f980dcf3933509fe0a" - ], - "layout": "IPY_MODEL_60c1a0d765c14a1d888317e6a507e4ea" - } - }, - "dd0e646fad3f4a89ba23b39d162bd8d9": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_d43c6df07ddb466587807d6dbe1ff614", - "IPY_MODEL_e0e8b840b8ea4d0d9db09afe99fa287d", - "IPY_MODEL_9327977822be4b1294f80e876552e305" - ], - "layout": "IPY_MODEL_77304d1a46b3468a98483e02ec0ac4a4" - } - }, - "de7c37ee83e24f0c889e84d07279c2ec": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_34cf3df51fbc41cabfdbba153c007f0e", - "placeholder": "​", - "style": "IPY_MODEL_ac764024cf1c4e08ba7749afd2cd20ac", - "value": "vocab.json: 100%" - } - }, - "dfd2a2649b8341ef913207526708aff1": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "e09f1bcbb9d94c09be53e5e1303642c2": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_e7d8e4fe58384e93a106de546068c65e", - "max": 8, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_0aa8ab56b85f4171a79c3bc210594025", - "value": 8 - } - }, - "e0e8b840b8ea4d0d9db09afe99fa287d": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "danger", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_f7434f3e03124a1c938a39af79d7fa59", - "max": 3963750880, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_c1314f241a434c41b45d84dc4d3b30f8", - "value": 3963750502 - } - }, - "e21e180307e5485cbbe908672fd6639a": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_2e2b0c1599c341a198f632f46a40c90e", - "IPY_MODEL_bff139df987d4a62abec6456cb27f3d4", - "IPY_MODEL_ebe1cc366d324ad59b264c8b3c431441" - ], - "layout": "IPY_MODEL_114dece49dba437c8572ef94b23c3b1e" - } - }, - "e366ae3fceec4566b9ed303d6c5f90af": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "e3fb3fc6afe04b3c9b7ac61809ce78fa": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_c6164e05a1914ae48083db9ad7f4ef7c", - "placeholder": "​", - "style": "IPY_MODEL_813621384dc748b0ad06775e22761c0b", - "value": " 9985/9985 [00:03<00:00, 3622.89 examples/s]" - } - }, - "e400cbf14bcc446a9d33b210cd93550b": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "e40d1c1ac9494b3bade9858324e7ffdf": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "e575d87a7efe4ec7b1efde489839d4a6": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "e5a82df528bb4e408797a3b6c2758f4a": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "e6e969610738449887259063967f82b0": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "e7d8e4fe58384e93a106de546068c65e": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "e87ea87fcff247b5bbcc331ba79a8dc2": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "e8b7a81040904c1e89e58978223b1737": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "e90658f4bcb642baa78426012f863152": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "eb1c9535e6a546098b760528b2ea387c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_18357b321ce44d7b8bd9d1c886f69275", - "IPY_MODEL_279937fe03bc4e4eb25b472d7e9df163", - "IPY_MODEL_bca2c7185b6749fd899c06a2ba4c5e46" - ], - "layout": "IPY_MODEL_1f7d30f71bbd4547a9150d21da071055" - } - }, - "ebb69a2c3d0a4299a484698287b3087c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "ebc80d1a55fa47f4a5ea2756588569ec": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "ebe1cc366d324ad59b264c8b3c431441": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_fba7aa824b38467ab3061b226114cdec", - "placeholder": "​", - "style": "IPY_MODEL_f3075dccbd2747b4a7913b66f44f2596", - "value": " 3.96G/3.96G [00:13<00:00, 398MB/s]" - } - }, - "ec030fc3c346426f9abc3a89892258d3": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_dfd2a2649b8341ef913207526708aff1", - "max": 9985, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_4f1977d7e4824ef1a14b65f0f42bba10", - "value": 9985 - } - }, - "ec11d1e5ae7b42c883d9b1f38a65356e": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_936d04b5fe1b4c63bf0b080e423d051b", - "placeholder": "​", - "style": "IPY_MODEL_f1cef8e8dc2646fb9fd09f3b09081074", - "value": " 36.5k/36.5k [00:00<00:00, 4.32MB/s]" - } - }, - "ed28e2e0410d4e0b855467e798e53d66": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "ed5ca967ad5342929e578ac6aa4dc4c0": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "edc99591b9c747b689b94d0052fec14c": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "ef0a3c7a6f14460fb4da096928ae249e": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_07fb3a2c8315494e97b447e672dfae06", - "IPY_MODEL_ec030fc3c346426f9abc3a89892258d3", - "IPY_MODEL_e3fb3fc6afe04b3c9b7ac61809ce78fa" - ], - "layout": "IPY_MODEL_c3be9109d63c485d9c0ef4f9bc0f9218" - } - }, - "ef223e8504b64e3592589880326aaf41": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "f0a58fbd0fca4340890041f99fa2f8c8": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "f113ebd8c1c34806bea4dd7ed3035173": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "f1cef8e8dc2646fb9fd09f3b09081074": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "f3075dccbd2747b4a7913b66f44f2596": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "f365820a3d3c42b2948abfe32065de14": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_735d4f225b24414294fc1b213c61223c", - "placeholder": "​", - "style": "IPY_MODEL_5e5e15b0569b474c9620083b3ec6af55", - "value": "generation_config.json: 100%" - } - }, - "f4667818b9d34a09891cd727a429a610": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_4b27c267393640f28f6eae0875bd2ed9", - "placeholder": "​", - "style": "IPY_MODEL_9858cb74a09748a39e8149baac96702c", - "value": " 3.96G/3.96G [00:11<00:00, 457MB/s]" - } - }, - "f4a1795dc7514a718f478245f521f0ba": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "f60a2bdb6b6b4e0e8c3508580e247132": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "danger", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_edc99591b9c747b689b94d0052fec14c", - "max": 3963750880, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_35cc989ca3374e7dba0cb166febc4bde", - "value": 3963750502 - } - }, - "f7434f3e03124a1c938a39af79d7fa59": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "f8ef805b776145c3bfa9ba8d90972058": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "fa1282ccc7544e4f818e2f03ccffe4a5": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "fa864b41586f4a7aa56aeafd1d84eb75": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "fba7aa824b38467ab3061b226114cdec": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "fcb30372e7404c5d8a1ad4df91e6c7b2": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "fcbab4d8dced41a18dfccce81e3a45a0": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "fd4f333f7ece4450b04e1a9af1f9d2f6": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_d1f9b10c130542f094c8fd3d1e23b5e9", - "placeholder": "​", - "style": "IPY_MODEL_e575d87a7efe4ec7b1efde489839d4a6", - "value": "model-00006-of-00008.safetensors: 100%" - } - }, - "fe18bba7f3fb4c31bf840541f36b3425": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_fd4f333f7ece4450b04e1a9af1f9d2f6", - "IPY_MODEL_f60a2bdb6b6b4e0e8c3508580e247132", - "IPY_MODEL_c0892a1881de4eb4bfabc6a68f87ae99" - ], - "layout": "IPY_MODEL_1bec6297c90242a88672d195bc09d429" - } - }, - "fe41858c6bd04c58840112b67c19a336": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_e5a82df528bb4e408797a3b6c2758f4a", - "placeholder": "​", - "style": "IPY_MODEL_f113ebd8c1c34806bea4dd7ed3035173", - "value": " 9985/9985 [00:00<00:00, 44264.88 examples/s]" - } - }, - "fea1b70fb46745feb5111b3929175b5d": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_f365820a3d3c42b2948abfe32065de14", - "IPY_MODEL_823f1c78f15043e38bbd4dca3932a86a", - "IPY_MODEL_a1959759c5424da9961fb2a308d4dee4" - ], - "layout": "IPY_MODEL_34c9c0137b504cd799c6bd6de69507c2" - } - }, - "ff3a94b146a948b6907f5d80c7157f99": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "ffdbb12a2f2c4d14911685e7683e0ef0": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_ab93eabd7cea4b94b4b7a387f101e8a1", - "placeholder": "​", - "style": "IPY_MODEL_704f2f5a9b1c49d5a75a0025a5dda11b", - "value": " 3.96G/3.96G [00:12<00:00, 656MB/s]" - } - } - } + "base_uri": "https://localhost:8080/", + "height": 955, + "referenced_widgets": [ + "c12ea43372ac4d57bb9605f1a429b397", + "86816687746246b4a6105e8010384e25", + "6f05e9bebf7b40c9835808e77de6c236", + "c7433acd3c4841e6958ae8f7e87b1808", + "19c1e38389fa46c7b7e2152a56e1df34", + "0e067d8db8ed48308a718d5f57683fd1", + "131065f118274a1586ac38e39ed84ef0", + "8640ac440fbc4644b9a3af7ba3ae7183", + "5cea7996f02040b187ece0bb2d6a8d1f", + "2e257c8be2da40b4bb67a9e4ab6811f3", + "56e3768bef5a4b9db4168c5c17f509c2", + "62c028fdef904dedb9cdeca2b3bda725", + "a7cf477e80fc43e0ad82c7997b076dce", + "835bcc28a5564fb9b3d651bc8e32dc46", + "9f1c9a0695384bdaa6f8b847ef89bee8", + "b1bea589efa14258a9982071b87938bf", + "590eef89881545aa8bbef9a8bbe7fb00", + "4b1f04ff63d14a118fdd15814dff50e4", + "39789237703c4a418134243055c9cbf5", + "a3a945817f684328b34651fe052393ec" + ] + }, + "id": "2yw8pLvlSMl8", + "outputId": "6e489ab2-4abe-4e28-84ca-959f912433a4" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c12ea43372ac4d57bb9605f1a429b397", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HTML(value='
\n", + " sys.exit(main())\n", + " ^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/commands/huggingface_cli.py\", line 57, in main\n", + " service.run()\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/commands/upload.py\", line 207, in run\n", + " print(self._upload())\n", + " ^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/commands/upload.py\", line 302, in _upload\n", + " return self.api.upload_folder(\n", + " ^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_validators.py\", line 114, in _inner_fn\n", + " return fn(*args, **kwargs)\n", + " ^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 1633, in _inner\n", + " return fn(self, *args, **kwargs)\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 4942, in upload_folder\n", + " commit_info = self.create_commit(\n", + " ^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_validators.py\", line 114, in _inner_fn\n", + " return fn(*args, **kwargs)\n", + " ^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 1633, in _inner\n", + " return fn(self, *args, **kwargs)\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 4202, in create_commit\n", + " self.preupload_lfs_files(\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/hf_api.py\", line 4483, in preupload_lfs_files\n", + " _upload_xet_files(**upload_kwargs, create_pr=create_pr) # type: ignore [arg-type]\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_validators.py\", line 114, in _inner_fn\n", + " return fn(*args, **kwargs)\n", + " ^^^^^^^^^^^^^^^^^^^\n", + " File \"/usr/local/lib/python3.11/dist-packages/huggingface_hub/_commit_api.py\", line 592, in _upload_xet_files\n", + " with progress_cm as progress:\n", + " File \"/usr/local/lib/python3.11/dist-packages/tqdm/std.py\", line 1138, in __exit__\n", + " def __exit__(self, exc_type, exc_value, traceback):\n", + "\n", + "KeyboardInterrupt\n", + "^C\n" + ] } + ], + "source": [ + "from huggingface_hub import notebook_login\n", + "\n", + "# remove the partial epoch checkpoints\n", + "!rm -rf \"./outputs/qwen-sft-pirate-rrr/checkpoint-*\"\n", + "\n", + "# HF Notebook login widget\n", + "notebook_login()\n", + "\n", + "# upload the LoRA adapter for your model to HF, remember to update the username/model-name below\n", + "!huggingface-cli upload --repo-type=model winglian/pirate-qwen-14B \"./outputs/qwen-sft-pirate-rrr\"" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "language_info": { + "name": "python" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "00321cce58884f6f9b3855a21fcd9187": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "004d9177a6a14118a5930dc3cc13147b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_a80410b919e442c49aea15acc1ce1a72", + "IPY_MODEL_c6e00f5224364822bc4239b176686919", + "IPY_MODEL_ec11d1e5ae7b42c883d9b1f38a65356e" + ], + "layout": "IPY_MODEL_734185351eb543fa9a00a881dcbb9fe7" + } + }, + "0077aedc3d174560bce924ee89e9c006": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "03a3c744d716431488163b4358b80f92": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "03b093d592ba4386aa61f7b8483da660": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_b8766a88716948cf968f4563531a76d9", + "IPY_MODEL_6f3a28b912714c6e931003549664bfa3", + "IPY_MODEL_16d1283741404b7bb319094c992fce01" + ], + "layout": "IPY_MODEL_2a5bb0e818ab47be8cf6465988328503" + } + }, + "042e091f75694c47aee761e760e76773": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "0546d04aae644dde846c58a4afb598a6": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "054c8dffadba48c6b895a6cc62448ecc": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "07fb3a2c8315494e97b447e672dfae06": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_12815f401eba44658caa7b2e490137a8", + "placeholder": "​", + "style": "IPY_MODEL_30e02aa2d0d241979369e598287f2639", + "value": "Drop Samples with Zero Trainable Tokens (num_proc=2): 100%" + } + }, + "083f9cda8d754c168beee10d2f8955a2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_a0a11e929edd4189b79723d618522c33", + "max": 728, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_e87ea87fcff247b5bbcc331ba79a8dc2", + "value": 728 + } + }, + "09007681cf8d42aeb8c1d2f6a74e470a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b195f160ca20442fadd8b5aed0ee41af", + "placeholder": "​", + "style": "IPY_MODEL_ca65e32eb52f48c09a84b33cb18f22cd", + "value": " 11.4M/11.4M [00:00<00:00, 21.8MB/s]" + } + }, + "0a46ad75c198463d843fb35e813642cb": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b8e39e4dddc3497fbc29ae45c66da759", + "max": 11422654, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_63b4e563e85c4f03b1b72beda9577bcc", + "value": 11422654 + } + }, + "0aa8ab56b85f4171a79c3bc210594025": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "0b4c9753a7cb4354b8e5f187e6e1ad7c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "0cd7efffbb3c4c4b972e63749f61ab97": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "0dea5caa27384f5689e3cab51f558727": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0e067d8db8ed48308a718d5f57683fd1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b1bea589efa14258a9982071b87938bf", + "placeholder": "​", + "style": "IPY_MODEL_590eef89881545aa8bbef9a8bbe7fb00", + "value": "\nPro Tip: If you don't already have one, you can create a dedicated\n'notebooks' token with 'write' access, that you can then easily reuse for all\nnotebooks.
" + } + }, + "0e50870ed0c643e0b6c18cc5d7ddae7f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_bfcdbba993b74972a9e3e575f86908ff", + "placeholder": "​", + "style": "IPY_MODEL_6ebb2ec171414e47a14765505f64bb3c", + "value": " 3.84G/3.84G [00:09<00:00, 664MB/s]" + } + }, + "0e936d9dbf9c4fdd86bbfe9730dedc47": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "0f417447a7bd4a33acca96fa37aec877": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "0f480e3a0b0a45d2a2d2dec3cad923f3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0f6907ebbc6242c8bde059cef1e1bd29": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_5bdfd87fc6cd4f9dabef7cfee29c8060", + "IPY_MODEL_64f54d4a744a4627a07c3c0120276f3b", + "IPY_MODEL_65b75b9b8bc143cf997796af68ff6668" + ], + "layout": "IPY_MODEL_d6fe74e4255444368f8f90a62157d869" + } + }, + "114dece49dba437c8572ef94b23c3b1e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "12815f401eba44658caa7b2e490137a8": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "12b56912736849fea2ad8124456fdc5c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_97e36007e1304e1583fd81bfb13f0edd", + "max": 1671853, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_c65dc74c7d6f4bab8f7dd28455161dd8", + "value": 1671853 + } + }, + "131065f118274a1586ac38e39ed84ef0": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": "center", + "align_self": null, + "border": null, + "bottom": null, + "display": "flex", + "flex": null, + "flex_flow": "column", + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": "50%" + } + }, + "158c8b85dbf34de6a94b4e35e2fc7d5a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "16a188a0b06d45f980dcf3933509fe0a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_349eee9f56d64f0cba6fc24ff2c50c9b", + "placeholder": "​", + "style": "IPY_MODEL_7e5d3774060e4589aa65982da5ea4ef4", + "value": " 9985/9985 [00:04<00:00, 2604.11 examples/s]" + } + }, + "16d1283741404b7bb319094c992fce01": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_a4e5789584564049b83df7c6c54a3e08", + "placeholder": "​", + "style": "IPY_MODEL_ff3a94b146a948b6907f5d80c7157f99", + "value": " 9985/0 [00:00<00:00, 50763.46 examples/s]" + } + }, + "1811cda0644e4190a9469d1774435d82": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "18357b321ce44d7b8bd9d1c886f69275": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e366ae3fceec4566b9ed303d6c5f90af", + "placeholder": "​", + "style": "IPY_MODEL_5dd7d150dbe04f08b165ce7f2c27cd11", + "value": "model-00008-of-00008.safetensors: 100%" + } + }, + "19127c7bb1554ccbac877059f9a82db0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e400cbf14bcc446a9d33b210cd93550b", + "max": 3963750880, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_71002199df6b40c9a1ac40df5fb27a1b", + "value": 3963750502 + } + }, + "19c1e38389fa46c7b7e2152a56e1df34": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ButtonModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ButtonModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ButtonView", + "button_style": "", + "description": "Login", + "disabled": false, + "icon": "", + "layout": "IPY_MODEL_835bcc28a5564fb9b3d651bc8e32dc46", + "style": "IPY_MODEL_9f1c9a0695384bdaa6f8b847ef89bee8", + "tooltip": "" + } + }, + "1bec6297c90242a88672d195bc09d429": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "1c6f1f10667545aaab958016ba7e2c94": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "1d5117195d4b49eb8f1a73b18419f7ce": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_0dea5caa27384f5689e3cab51f558727", + "placeholder": "​", + "style": "IPY_MODEL_a6f48410b9964fefba0c3009a77dc838", + "value": " 9.68k/9.68k [00:00<00:00, 812kB/s]" + } + }, + "1f7d30f71bbd4547a9150d21da071055": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "200df5e79b9244849e589ecb0250a520": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_f4a1795dc7514a718f478245f521f0ba", + "placeholder": "​", + "style": "IPY_MODEL_5e746eb25bbe416fb585fa24e79f5177", + "value": "model-00002-of-00008.safetensors: 100%" + } + }, + "20352e5f58d24bb8b1f3940efd14fe4a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "253017b0d0534e54ab44e181f6d7c82d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_1c6f1f10667545aaab958016ba7e2c94", + "placeholder": "​", + "style": "IPY_MODEL_e6e969610738449887259063967f82b0", + "value": " 2.78M/2.78M [00:00<00:00, 17.8MB/s]" + } + }, + "258b7c635c1045329d4669e48c46ccd5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_6f68ed9889f54ad2ae8a3b95ac263a83", + "IPY_MODEL_80366349d81e4dcc892db6cd56e384f3", + "IPY_MODEL_c73055099c084dca996159e23e162d0b" + ], + "layout": "IPY_MODEL_977f799afaac4a55b2dc1cffa7d5b63b" + } + }, + "279937fe03bc4e4eb25b472d7e9df163": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b634bb73cfa743d09a5999101b840976", + "max": 1912371880, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_742b1030acfd414bbd9d5327b7e3826d", + "value": 1912371698 + } + }, + "27beaf06e41b472abdb544a43c720c5a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2860e3bb3baf4f7da058465850e800c5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_3efd18ea8eaa41918894883da9541bfa", + "IPY_MODEL_e09f1bcbb9d94c09be53e5e1303642c2", + "IPY_MODEL_82177df57a494de8900c14c2f5185175" + ], + "layout": "IPY_MODEL_ccfcdc95baf646f8aeb3d516742383f2" + } + }, + "2a51b36be41745468e4c2d7a21b1c0d2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2a5bb0e818ab47be8cf6465988328503": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2b3a2659b12244bd8548320320016dbf": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2e257c8be2da40b4bb67a9e4ab6811f3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2e2b0c1599c341a198f632f46a40c90e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_be724f04b03942b2a033a7e8898bb4fd", + "placeholder": "​", + "style": "IPY_MODEL_fcbab4d8dced41a18dfccce81e3a45a0", + "value": "model-00005-of-00008.safetensors: 100%" + } + }, + "3036608c71904ce9ae4bb2a9fa8802d9": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_5ca6be24acb548cea130bd58e9954c7c", + "placeholder": "​", + "style": "IPY_MODEL_5cfb02ee044b4011a378efa8b54a370f", + "value": " 3.96G/3.96G [00:10<00:00, 531MB/s]" + } + }, + "30a81da86f8043eca301e86a8651201a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "30e02aa2d0d241979369e598287f2639": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "3225603166b54e7aab766b9964a2f660": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "33b3b1d0295646edaac7b4822761aeb0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "349eee9f56d64f0cba6fc24ff2c50c9b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "34c9c0137b504cd799c6bd6de69507c2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "34cf3df51fbc41cabfdbba153c007f0e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "35c811d2ae8e43f3b5cecbdd3cfa857f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "35cc989ca3374e7dba0cb166febc4bde": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "366a343b62fa47d8985a3bd464d99f9e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "37de928300e34184881039378bd75e7f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "388f618924274d21a066f098f4f1e744": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_7c95f85a2b1f47a1bd846d110c47bb3c", + "IPY_MODEL_083f9cda8d754c168beee10d2f8955a2", + "IPY_MODEL_62e1a65582f446a78612eaa804e08a7d" + ], + "layout": "IPY_MODEL_487a177d020f4605834878b2fdc7afa3" + } + }, + "39789237703c4a418134243055c9cbf5": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3aaecbf540f54a2db9ab0931e3b1fe57": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3c21e4a511b4441192c03b7f1d0976e9": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3efd18ea8eaa41918894883da9541bfa": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_8f5bd719974e41c3a8dd9a5b0d3d71e6", + "placeholder": "​", + "style": "IPY_MODEL_b87c84de30e84b3abf4871461fb9cbd3", + "value": "Loading checkpoint shards: 100%" + } + }, + "41f3b32c2f6b4034ae7a3b9124e28bc7": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "4471ff62258549fba9514bb67050f965": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_9cd5211b5d8b457aa0002f1d17b80028", + "IPY_MODEL_19127c7bb1554ccbac877059f9a82db0", + "IPY_MODEL_f4667818b9d34a09891cd727a429a610" + ], + "layout": "IPY_MODEL_9ed02dc43412471a9ab47f3620ccf3a5" + } + }, + "4540927d98f54466b434ba4c0edf045d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "487a177d020f4605834878b2fdc7afa3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "4b1f04ff63d14a118fdd15814dff50e4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "LabelModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "LabelModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "LabelView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_39789237703c4a418134243055c9cbf5", + "placeholder": "​", + "style": "IPY_MODEL_a3a945817f684328b34651fe052393ec", + "value": "Connecting..." + } + }, + "4b27c267393640f28f6eae0875bd2ed9": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "4c727d40ef0443449afc31724ee79f0c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "4d05314858354e729d76094b3b0ce761": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_c42acf646f344a88b8c11f81e67f7206", + "IPY_MODEL_7be6f04c284e4326bb4ff3d301e7b3c6", + "IPY_MODEL_ffdbb12a2f2c4d14911685e7683e0ef0" + ], + "layout": "IPY_MODEL_bee3501b2a17427784a717e50a85e7fa" + } + }, + "4d468f96ec924681ad65eb671674b93e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "4f1977d7e4824ef1a14b65f0f42bba10": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "4fd114abe9f5494ab59858949f5055f1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "500e272208a246089613bf788a165271": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_200df5e79b9244849e589ecb0250a520", + "IPY_MODEL_cc94432d08464affa3e58b560bdad194", + "IPY_MODEL_3036608c71904ce9ae4bb2a9fa8802d9" + ], + "layout": "IPY_MODEL_adacfdcc1b0140efac56918e9ccf064e" + } + }, + "519a7b154022443db6703f04a9142bae": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d02274afd47b462291c745f261209d42", + "max": 27341251, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_0f417447a7bd4a33acca96fa37aec877", + "value": 27341251 + } + }, + "56e3768bef5a4b9db4168c5c17f509c2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "590eef89881545aa8bbef9a8bbe7fb00": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "598da69727bd4fb8b1caf465ac736d7a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5bdfd87fc6cd4f9dabef7cfee29c8060": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_4d468f96ec924681ad65eb671674b93e", + "placeholder": "​", + "style": "IPY_MODEL_ad7599de524549c48bf2d3124ad4b299", + "value": "Dropping Long Sequences (num_proc=2): 100%" + } + }, + "5ca240f31e6b44e3882c5eb37cd5a309": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": "20px" + } + }, + "5ca6be24acb548cea130bd58e9954c7c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "5cea7996f02040b187ece0bb2d6a8d1f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5cfb02ee044b4011a378efa8b54a370f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5dd7d150dbe04f08b165ce7f2c27cd11": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5e18768f7ad6434ba8b8b8a2e853e204": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "5e5e15b0569b474c9620083b3ec6af55": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5e746eb25bbe416fb585fa24e79f5177": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5eb06edeb58e4930b1affef2a59eae81": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "5f86cd894de94c3280fadc1e2fd0ee13": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_a20927bf5f2c41f58c1e31ac858ab36c", + "IPY_MODEL_0a46ad75c198463d843fb35e813642cb", + "IPY_MODEL_09007681cf8d42aeb8c1d2f6a74e470a" + ], + "layout": "IPY_MODEL_ebc80d1a55fa47f4a5ea2756588569ec" + } + }, + "60c1a0d765c14a1d888317e6a507e4ea": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "62c028fdef904dedb9cdeca2b3bda725": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "62e1a65582f446a78612eaa804e08a7d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_5e18768f7ad6434ba8b8b8a2e853e204", + "placeholder": "​", + "style": "IPY_MODEL_bb33aec33a6447078c31bfd728942994", + "value": " 728/728 [00:00<00:00, 20.3kB/s]" + } + }, + "62e302ebdad64aada0ffe64ae1c873f3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "63580b6fb30642479fe3000915bf551a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "63b4e563e85c4f03b1b72beda9577bcc": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "64f54d4a744a4627a07c3c0120276f3b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_0546d04aae644dde846c58a4afb598a6", + "max": 9985, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_897b77a56c09479bb11d7f2a30997e55", + "value": 9985 + } + }, + "65b75b9b8bc143cf997796af68ff6668": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_81c3db71ac704280ad030072655f1537", + "placeholder": "​", + "style": "IPY_MODEL_042e091f75694c47aee761e760e76773", + "value": " 9985/9985 [00:02<00:00, 3977.47 examples/s]" + } + }, + "67da6c4260574869aa24c3cbc1bc1654": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "6932489232ec4ab18a160b1e7fbcdfe1": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "6ebb2ec171414e47a14765505f64bb3c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "6f05e9bebf7b40c9835808e77de6c236": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "PasswordModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "PasswordModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "PasswordView", + "continuous_update": true, + "description": "Token:", + "description_tooltip": null, + "disabled": false, + "layout": "IPY_MODEL_2e257c8be2da40b4bb67a9e4ab6811f3", + "placeholder": "​", + "style": "IPY_MODEL_56e3768bef5a4b9db4168c5c17f509c2", + "value": "" + } + }, + "6f3a28b912714c6e931003549664bfa3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_5ca240f31e6b44e3882c5eb37cd5a309", + "max": 1, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_5eb06edeb58e4930b1affef2a59eae81", + "value": 1 + } + }, + "6f68ed9889f54ad2ae8a3b95ac263a83": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_41f3b32c2f6b4034ae7a3b9124e28bc7", + "placeholder": "​", + "style": "IPY_MODEL_a10d0a76010f4e508c65a9b69ebc5156", + "value": "Tokenizing Prompts (num_proc=2): 100%" + } + }, + "704f2f5a9b1c49d5a75a0025a5dda11b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "71002199df6b40c9a1ac40df5fb27a1b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "71c8af139cd248b1b51101fd46a93f35": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d0e9dce55cec4c1ca619a0ccf209d924", + "max": 9675, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_4c727d40ef0443449afc31724ee79f0c", + "value": 9675 + } + }, + "734185351eb543fa9a00a881dcbb9fe7": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "735d4f225b24414294fc1b213c61223c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "742b1030acfd414bbd9d5327b7e3826d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "77304d1a46b3468a98483e02ec0ac4a4": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "7baeab52d6694c32b1efd1ea1a0a7782": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_93a44a11aa4846fa8efc6c1413ef1627", + "placeholder": "​", + "style": "IPY_MODEL_a55060adc3564407ac81ad7297d34aaa", + "value": "train.jsonl: 100%" + } + }, + "7be6f04c284e4326bb4ff3d301e7b3c6": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_9503a45960984adc97b58e16c50662e0", + "max": 3963750880, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_da6e93f3e4984780b930fe7a706983ea", + "value": 3963750502 + } + }, + "7c2485c6cdfe463da6fdb35982a1070d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_ad1236893754446881e153adc9d5c962", + "IPY_MODEL_daee63fd167e4441a32324b51b00ad2b", + "IPY_MODEL_fe41858c6bd04c58840112b67c19a336" + ], + "layout": "IPY_MODEL_d262c82138024169b9f3aa034ca756fa" + } + }, + "7c95f85a2b1f47a1bd846d110c47bb3c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_7fd44cf9ca6e4726bfd7ac21846d6a14", + "placeholder": "​", + "style": "IPY_MODEL_366a343b62fa47d8985a3bd464d99f9e", + "value": "config.json: 100%" + } + }, + "7cd0b85ebd204b7aba908417811ce4e0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_7baeab52d6694c32b1efd1ea1a0a7782", + "IPY_MODEL_519a7b154022443db6703f04a9142bae", + "IPY_MODEL_d4183e9715f34d249942b8271cca3bdf" + ], + "layout": "IPY_MODEL_da2347ac94764a3fa2743343cf0d3cd2" + } + }, + "7e5d3774060e4589aa65982da5ea4ef4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "7fd44cf9ca6e4726bfd7ac21846d6a14": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "80366349d81e4dcc892db6cd56e384f3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_f8ef805b776145c3bfa9ba8d90972058", + "max": 9985, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_cc587493c33c4f118d1b1170f85be24c", + "value": 9985 + } + }, + "813621384dc748b0ad06775e22761c0b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "81c3db71ac704280ad030072655f1537": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "82177df57a494de8900c14c2f5185175": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_67da6c4260574869aa24c3cbc1bc1654", + "placeholder": "​", + "style": "IPY_MODEL_94b9088614464f60a203de39dbcae853", + "value": " 8/8 [01:47<00:00, 11.64s/it]" + } + }, + "823f1c78f15043e38bbd4dca3932a86a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_03a3c744d716431488163b4358b80f92", + "max": 239, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_a5434ee714f9498d83870544b67c0cb7", + "value": 239 + } + }, + "835bcc28a5564fb9b3d651bc8e32dc46": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8640ac440fbc4644b9a3af7ba3ae7183": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "86816687746246b4a6105e8010384e25": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_8640ac440fbc4644b9a3af7ba3ae7183", + "placeholder": "​", + "style": "IPY_MODEL_5cea7996f02040b187ece0bb2d6a8d1f", + "value": "

Copy a token from your Hugging Face\ntokens page and paste it below.
Immediately click login after copying\nyour token or it might be stored in plain text in this notebook file.
" + } + }, + "879c8ab5873847a8833bd74123be90a4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_ef223e8504b64e3592589880326aaf41", + "placeholder": "​", + "style": "IPY_MODEL_598da69727bd4fb8b1caf465ac736d7a", + "value": " 1.67M/1.67M [00:00<00:00, 19.0MB/s]" + } + }, + "897b77a56c09479bb11d7f2a30997e55": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "8bc9d8ba866c442b9118d9630009939c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8c4d4fc5a30f4e7cb3be53fe2adda33d": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8f5bd719974e41c3a8dd9a5b0d3d71e6": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8f726dbfb45d4528afa33e36a6313267": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "9327977822be4b1294f80e876552e305": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_37de928300e34184881039378bd75e7f", + "placeholder": "​", + "style": "IPY_MODEL_0e936d9dbf9c4fdd86bbfe9730dedc47", + "value": " 3.96G/3.96G [00:13<00:00, 273MB/s]" + } + }, + "936d04b5fe1b4c63bf0b080e423d051b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "93a44a11aa4846fa8efc6c1413ef1627": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "94b9088614464f60a203de39dbcae853": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "9503a45960984adc97b58e16c50662e0": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "95caff42f08a4c2aa14c867b8f37f231": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_de7c37ee83e24f0c889e84d07279c2ec", + "IPY_MODEL_9d4897eefb5f48259ffb2d23e332f752", + "IPY_MODEL_253017b0d0534e54ab44e181f6d7c82d" + ], + "layout": "IPY_MODEL_27beaf06e41b472abdb544a43c720c5a" + } + }, + "977f799afaac4a55b2dc1cffa7d5b63b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "97e36007e1304e1583fd81bfb13f0edd": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "9858cb74a09748a39e8149baac96702c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "9b42e08b3c9548818488268768a118b1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d955dcaa0e944e719f3a06139dd54a03", + "placeholder": "​", + "style": "IPY_MODEL_d3de2662c7964f1ba96e58da382af720", + "value": "merges.txt: 100%" + } + }, + "9cd5211b5d8b457aa0002f1d17b80028": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_6932489232ec4ab18a160b1e7fbcdfe1", + "placeholder": "​", + "style": "IPY_MODEL_4540927d98f54466b434ba4c0edf045d", + "value": "model-00007-of-00008.safetensors: 100%" + } + }, + "9d4897eefb5f48259ffb2d23e332f752": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_30a81da86f8043eca301e86a8651201a", + "max": 2776833, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_e8b7a81040904c1e89e58978223b1737", + "value": 2776833 + } + }, + "9e333ed3b5014069ac1dd969255dd591": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "9ed02dc43412471a9ab47f3620ccf3a5": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "9f1c9a0695384bdaa6f8b847ef89bee8": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ButtonStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ButtonStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "button_color": null, + "font_weight": "" + } + }, + "9f56a2d9979c4bd8928c644c22c3ecdf": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "a0a11e929edd4189b79723d618522c33": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a10d0a76010f4e508c65a9b69ebc5156": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "a138859f19b74fc0928dc236ab5359db": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_9b42e08b3c9548818488268768a118b1", + "IPY_MODEL_12b56912736849fea2ad8124456fdc5c", + "IPY_MODEL_879c8ab5873847a8833bd74123be90a4" + ], + "layout": "IPY_MODEL_20352e5f58d24bb8b1f3940efd14fe4a" + } + }, + "a1959759c5424da9961fb2a308d4dee4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_3aaecbf540f54a2db9ab0931e3b1fe57", + "placeholder": "​", + "style": "IPY_MODEL_9e333ed3b5014069ac1dd969255dd591", + "value": " 239/239 [00:00<00:00, 30.9kB/s]" + } + }, + "a20927bf5f2c41f58c1e31ac858ab36c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_1811cda0644e4190a9469d1774435d82", + "placeholder": "​", + "style": "IPY_MODEL_35c811d2ae8e43f3b5cecbdd3cfa857f", + "value": "tokenizer.json: 100%" + } + }, + "a3a945817f684328b34651fe052393ec": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "a44f630e099e43899f20a77084ae60cd": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_ed5ca967ad5342929e578ac6aa4dc4c0", + "placeholder": "​", + "style": "IPY_MODEL_af401d117d5047629d3a6e2361757b62", + "value": "model-00001-of-00008.safetensors: 100%" + } + }, + "a4e5789584564049b83df7c6c54a3e08": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a5434ee714f9498d83870544b67c0cb7": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "a55060adc3564407ac81ad7297d34aaa": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "a6f48410b9964fefba0c3009a77dc838": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "a7cf477e80fc43e0ad82c7997b076dce": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "a80410b919e442c49aea15acc1ce1a72": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_fa1282ccc7544e4f818e2f03ccffe4a5", + "placeholder": "​", + "style": "IPY_MODEL_bbbf575d2a4b4c6ea8389be79b2a6039", + "value": "model.safetensors.index.json: 100%" + } + }, + "ab93eabd7cea4b94b4b7a387f101e8a1": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ac764024cf1c4e08ba7749afd2cd20ac": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "ad1236893754446881e153adc9d5c962": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_62e302ebdad64aada0ffe64ae1c873f3", + "placeholder": "​", + "style": "IPY_MODEL_bd1b0dfed6d34d16af33a4a58330f5ec", + "value": "Saving the dataset (1/1 shards): 100%" + } + }, + "ad7599de524549c48bf2d3124ad4b299": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "adacfdcc1b0140efac56918e9ccf064e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "af401d117d5047629d3a6e2361757b62": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "b191ac001a2e4962bc9a245fcdf26e6b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b195f160ca20442fadd8b5aed0ee41af": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b1bea589efa14258a9982071b87938bf": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b5b65414154544aa8a71b1a39164aad7": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b634bb73cfa743d09a5999101b840976": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b82aa8c57f7c422a9a9c90f333ed2a99": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_c0991cf63ee6458b96e9a75e7a88b61a", + "IPY_MODEL_71c8af139cd248b1b51101fd46a93f35", + "IPY_MODEL_1d5117195d4b49eb8f1a73b18419f7ce" + ], + "layout": "IPY_MODEL_3c21e4a511b4441192c03b7f1d0976e9" + } + }, + "b8766a88716948cf968f4563531a76d9": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_2b3a2659b12244bd8548320320016dbf", + "placeholder": "​", + "style": "IPY_MODEL_0cd7efffbb3c4c4b972e63749f61ab97", + "value": "Generating train split: " + } + }, + "b87c84de30e84b3abf4871461fb9cbd3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "b8e39e4dddc3497fbc29ae45c66da759": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bb33aec33a6447078c31bfd728942994": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "bbbf575d2a4b4c6ea8389be79b2a6039": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "bca2c7185b6749fd899c06a2ba4c5e46": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_0f480e3a0b0a45d2a2d2dec3cad923f3", + "placeholder": "​", + "style": "IPY_MODEL_fcb30372e7404c5d8a1ad4df91e6c7b2", + "value": " 1.91G/1.91G [00:05<00:00, 444MB/s]" + } + }, + "bd1b0dfed6d34d16af33a4a58330f5ec": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "be724f04b03942b2a033a7e8898bb4fd": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bed8726b8069434687c75452e21f19e5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_fa864b41586f4a7aa56aeafd1d84eb75", + "max": 9985, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_3225603166b54e7aab766b9964a2f660", + "value": 9985 + } + }, + "bee3501b2a17427784a717e50a85e7fa": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bfcdbba993b74972a9e3e575f86908ff": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bff139df987d4a62abec6456cb27f3d4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_c1f9c267ba3f40039cdb5eb3267e8043", + "max": 3963750880, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_33b3b1d0295646edaac7b4822761aeb0", + "value": 3963750502 + } + }, + "c0892a1881de4eb4bfabc6a68f87ae99": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_158c8b85dbf34de6a94b4e35e2fc7d5a", + "placeholder": "​", + "style": "IPY_MODEL_0b4c9753a7cb4354b8e5f187e6e1ad7c", + "value": " 3.96G/3.96G [00:15<00:00, 564MB/s]" + } + }, + "c0991cf63ee6458b96e9a75e7a88b61a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_ed28e2e0410d4e0b855467e798e53d66", + "placeholder": "​", + "style": "IPY_MODEL_d93f134f802b4b69b575bdaf07dbd27c", + "value": "tokenizer_config.json: 100%" + } + }, + "c12ea43372ac4d57bb9605f1a429b397": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "VBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "VBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "VBoxView", + "box_style": "", + "children": [], + "layout": "IPY_MODEL_131065f118274a1586ac38e39ed84ef0" + } + }, + "c1314f241a434c41b45d84dc4d3b30f8": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "c1f9c267ba3f40039cdb5eb3267e8043": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c33ced495f70464aa4a3a91922090853": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c3725c7f79fe415fbd1ea336f0cc9cf1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b191ac001a2e4962bc9a245fcdf26e6b", + "max": 3841788544, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_054c8dffadba48c6b895a6cc62448ecc", + "value": 3841788178 + } + }, + "c3be9109d63c485d9c0ef4f9bc0f9218": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c42acf646f344a88b8c11f81e67f7206": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_8bc9d8ba866c442b9118d9630009939c", + "placeholder": "​", + "style": "IPY_MODEL_9f56a2d9979c4bd8928c644c22c3ecdf", + "value": "model-00003-of-00008.safetensors: 100%" + } + }, + "c6164e05a1914ae48083db9ad7f4ef7c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c65dc74c7d6f4bab8f7dd28455161dd8": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "c6e00f5224364822bc4239b176686919": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_2a51b36be41745468e4c2d7a21b1c0d2", + "max": 36514, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_4fd114abe9f5494ab59858949f5055f1", + "value": 36514 + } + }, + "c73055099c084dca996159e23e162d0b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e40d1c1ac9494b3bade9858324e7ffdf", + "placeholder": "​", + "style": "IPY_MODEL_d65b6b060d9845779299491ac5599c31", + "value": " 9985/9985 [01:04<00:00, 189.08 examples/s]" + } + }, + "c7433acd3c4841e6958ae8f7e87b1808": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "CheckboxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "CheckboxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "CheckboxView", + "description": "Add token as git credential?", + "description_tooltip": null, + "disabled": false, + "indent": true, + "layout": "IPY_MODEL_62c028fdef904dedb9cdeca2b3bda725", + "style": "IPY_MODEL_a7cf477e80fc43e0ad82c7997b076dce", + "value": false + } + }, + "c84cc07789be48aebb322c23d355289e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_0077aedc3d174560bce924ee89e9c006", + "placeholder": "​", + "style": "IPY_MODEL_00321cce58884f6f9b3855a21fcd9187", + "value": "Add position_id column (Sample Packing) (num_proc=2): 100%" + } + }, + "ca65e32eb52f48c09a84b33cb18f22cd": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "cc587493c33c4f118d1b1170f85be24c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "cc94432d08464affa3e58b560bdad194": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b5b65414154544aa8a71b1a39164aad7", + "max": 3963750816, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_f0a58fbd0fca4340890041f99fa2f8c8", + "value": 3963750438 + } + }, + "ccfcdc95baf646f8aeb3d516742383f2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "cdebbc55a1164c018546c2ac6f8c620c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_a44f630e099e43899f20a77084ae60cd", + "IPY_MODEL_c3725c7f79fe415fbd1ea336f0cc9cf1", + "IPY_MODEL_0e50870ed0c643e0b6c18cc5d7ddae7f" + ], + "layout": "IPY_MODEL_c33ced495f70464aa4a3a91922090853" + } + }, + "d02274afd47b462291c745f261209d42": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d07c8b97d3314f1c852e44bdd40f61ed": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d0e9dce55cec4c1ca619a0ccf209d924": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d1f9b10c130542f094c8fd3d1e23b5e9": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d262c82138024169b9f3aa034ca756fa": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d3de2662c7964f1ba96e58da382af720": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "d4183e9715f34d249942b8271cca3bdf": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_63580b6fb30642479fe3000915bf551a", + "placeholder": "​", + "style": "IPY_MODEL_8f726dbfb45d4528afa33e36a6313267", + "value": " 27.3M/27.3M [00:00<00:00, 31.0MB/s]" + } + }, + "d43c6df07ddb466587807d6dbe1ff614": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_8c4d4fc5a30f4e7cb3be53fe2adda33d", + "placeholder": "​", + "style": "IPY_MODEL_e90658f4bcb642baa78426012f863152", + "value": "model-00004-of-00008.safetensors: 100%" + } + }, + "d65b6b060d9845779299491ac5599c31": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "d6fe74e4255444368f8f90a62157d869": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d93f134f802b4b69b575bdaf07dbd27c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "d955dcaa0e944e719f3a06139dd54a03": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "da2347ac94764a3fa2743343cf0d3cd2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "da6e93f3e4984780b930fe7a706983ea": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "daee63fd167e4441a32324b51b00ad2b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d07c8b97d3314f1c852e44bdd40f61ed", + "max": 9985, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_ebb69a2c3d0a4299a484698287b3087c", + "value": 9985 + } + }, + "dc892a596f6942d7973c616c38f0eebb": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_c84cc07789be48aebb322c23d355289e", + "IPY_MODEL_bed8726b8069434687c75452e21f19e5", + "IPY_MODEL_16a188a0b06d45f980dcf3933509fe0a" + ], + "layout": "IPY_MODEL_60c1a0d765c14a1d888317e6a507e4ea" + } + }, + "dd0e646fad3f4a89ba23b39d162bd8d9": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_d43c6df07ddb466587807d6dbe1ff614", + "IPY_MODEL_e0e8b840b8ea4d0d9db09afe99fa287d", + "IPY_MODEL_9327977822be4b1294f80e876552e305" + ], + "layout": "IPY_MODEL_77304d1a46b3468a98483e02ec0ac4a4" + } + }, + "de7c37ee83e24f0c889e84d07279c2ec": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_34cf3df51fbc41cabfdbba153c007f0e", + "placeholder": "​", + "style": "IPY_MODEL_ac764024cf1c4e08ba7749afd2cd20ac", + "value": "vocab.json: 100%" + } + }, + "dfd2a2649b8341ef913207526708aff1": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e09f1bcbb9d94c09be53e5e1303642c2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e7d8e4fe58384e93a106de546068c65e", + "max": 8, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_0aa8ab56b85f4171a79c3bc210594025", + "value": 8 + } + }, + "e0e8b840b8ea4d0d9db09afe99fa287d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_f7434f3e03124a1c938a39af79d7fa59", + "max": 3963750880, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_c1314f241a434c41b45d84dc4d3b30f8", + "value": 3963750502 + } + }, + "e21e180307e5485cbbe908672fd6639a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_2e2b0c1599c341a198f632f46a40c90e", + "IPY_MODEL_bff139df987d4a62abec6456cb27f3d4", + "IPY_MODEL_ebe1cc366d324ad59b264c8b3c431441" + ], + "layout": "IPY_MODEL_114dece49dba437c8572ef94b23c3b1e" + } + }, + "e366ae3fceec4566b9ed303d6c5f90af": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e3fb3fc6afe04b3c9b7ac61809ce78fa": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_c6164e05a1914ae48083db9ad7f4ef7c", + "placeholder": "​", + "style": "IPY_MODEL_813621384dc748b0ad06775e22761c0b", + "value": " 9985/9985 [00:03<00:00, 3622.89 examples/s]" + } + }, + "e400cbf14bcc446a9d33b210cd93550b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e40d1c1ac9494b3bade9858324e7ffdf": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e575d87a7efe4ec7b1efde489839d4a6": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "e5a82df528bb4e408797a3b6c2758f4a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e6e969610738449887259063967f82b0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "e7d8e4fe58384e93a106de546068c65e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e87ea87fcff247b5bbcc331ba79a8dc2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "e8b7a81040904c1e89e58978223b1737": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "e90658f4bcb642baa78426012f863152": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "eb1c9535e6a546098b760528b2ea387c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_18357b321ce44d7b8bd9d1c886f69275", + "IPY_MODEL_279937fe03bc4e4eb25b472d7e9df163", + "IPY_MODEL_bca2c7185b6749fd899c06a2ba4c5e46" + ], + "layout": "IPY_MODEL_1f7d30f71bbd4547a9150d21da071055" + } + }, + "ebb69a2c3d0a4299a484698287b3087c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "ebc80d1a55fa47f4a5ea2756588569ec": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ebe1cc366d324ad59b264c8b3c431441": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_fba7aa824b38467ab3061b226114cdec", + "placeholder": "​", + "style": "IPY_MODEL_f3075dccbd2747b4a7913b66f44f2596", + "value": " 3.96G/3.96G [00:13<00:00, 398MB/s]" + } + }, + "ec030fc3c346426f9abc3a89892258d3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_dfd2a2649b8341ef913207526708aff1", + "max": 9985, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_4f1977d7e4824ef1a14b65f0f42bba10", + "value": 9985 + } + }, + "ec11d1e5ae7b42c883d9b1f38a65356e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_936d04b5fe1b4c63bf0b080e423d051b", + "placeholder": "​", + "style": "IPY_MODEL_f1cef8e8dc2646fb9fd09f3b09081074", + "value": " 36.5k/36.5k [00:00<00:00, 4.32MB/s]" + } + }, + "ed28e2e0410d4e0b855467e798e53d66": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ed5ca967ad5342929e578ac6aa4dc4c0": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "edc99591b9c747b689b94d0052fec14c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ef0a3c7a6f14460fb4da096928ae249e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_07fb3a2c8315494e97b447e672dfae06", + "IPY_MODEL_ec030fc3c346426f9abc3a89892258d3", + "IPY_MODEL_e3fb3fc6afe04b3c9b7ac61809ce78fa" + ], + "layout": "IPY_MODEL_c3be9109d63c485d9c0ef4f9bc0f9218" + } + }, + "ef223e8504b64e3592589880326aaf41": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f0a58fbd0fca4340890041f99fa2f8c8": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "f113ebd8c1c34806bea4dd7ed3035173": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "f1cef8e8dc2646fb9fd09f3b09081074": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "f3075dccbd2747b4a7913b66f44f2596": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "f365820a3d3c42b2948abfe32065de14": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_735d4f225b24414294fc1b213c61223c", + "placeholder": "​", + "style": "IPY_MODEL_5e5e15b0569b474c9620083b3ec6af55", + "value": "generation_config.json: 100%" + } + }, + "f4667818b9d34a09891cd727a429a610": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_4b27c267393640f28f6eae0875bd2ed9", + "placeholder": "​", + "style": "IPY_MODEL_9858cb74a09748a39e8149baac96702c", + "value": " 3.96G/3.96G [00:11<00:00, 457MB/s]" + } + }, + "f4a1795dc7514a718f478245f521f0ba": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f60a2bdb6b6b4e0e8c3508580e247132": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "danger", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_edc99591b9c747b689b94d0052fec14c", + "max": 3963750880, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_35cc989ca3374e7dba0cb166febc4bde", + "value": 3963750502 + } + }, + "f7434f3e03124a1c938a39af79d7fa59": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f8ef805b776145c3bfa9ba8d90972058": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fa1282ccc7544e4f818e2f03ccffe4a5": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fa864b41586f4a7aa56aeafd1d84eb75": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fba7aa824b38467ab3061b226114cdec": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fcb30372e7404c5d8a1ad4df91e6c7b2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "fcbab4d8dced41a18dfccce81e3a45a0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "fd4f333f7ece4450b04e1a9af1f9d2f6": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d1f9b10c130542f094c8fd3d1e23b5e9", + "placeholder": "​", + "style": "IPY_MODEL_e575d87a7efe4ec7b1efde489839d4a6", + "value": "model-00006-of-00008.safetensors: 100%" + } + }, + "fe18bba7f3fb4c31bf840541f36b3425": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_fd4f333f7ece4450b04e1a9af1f9d2f6", + "IPY_MODEL_f60a2bdb6b6b4e0e8c3508580e247132", + "IPY_MODEL_c0892a1881de4eb4bfabc6a68f87ae99" + ], + "layout": "IPY_MODEL_1bec6297c90242a88672d195bc09d429" + } + }, + "fe41858c6bd04c58840112b67c19a336": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e5a82df528bb4e408797a3b6c2758f4a", + "placeholder": "​", + "style": "IPY_MODEL_f113ebd8c1c34806bea4dd7ed3035173", + "value": " 9985/9985 [00:00<00:00, 44264.88 examples/s]" + } + }, + "fea1b70fb46745feb5111b3929175b5d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_f365820a3d3c42b2948abfe32065de14", + "IPY_MODEL_823f1c78f15043e38bbd4dca3932a86a", + "IPY_MODEL_a1959759c5424da9961fb2a308d4dee4" + ], + "layout": "IPY_MODEL_34c9c0137b504cd799c6bd6de69507c2" + } + }, + "ff3a94b146a948b6907f5d80c7157f99": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "ffdbb12a2f2c4d14911685e7683e0ef0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_ab93eabd7cea4b94b4b7a387f101e8a1", + "placeholder": "​", + "style": "IPY_MODEL_704f2f5a9b1c49d5a75a0025a5dda11b", + "value": " 3.96G/3.96G [00:12<00:00, 656MB/s]" + } + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/pyproject.toml b/pyproject.toml index 36138c65d6..932219d9ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,3 +26,34 @@ include-package-data = true [tool.setuptools.cmdclass] build_py = "setuptools_axolotl_dynamic_dependencies.BuildPyCommand" + +[tool.ruff] +line-length = 88 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "W", "C90", "B"] +ignore = [ + "E203", # Whitespace before ':' + "E501", # Line too long + "C901", # Too complex + "B019", # Use of functools.cache on methods + "E722", # Bare except + "F821", # Undefined name (for dynamic exec) +] + +[tool.ruff.lint.isort] +known-third-party = ["wandb", "comet_ml"] +known-local-folder = ["src", "tests"] +# Black-compatible isort settings +force-single-line = false +combine-as-imports = true +split-on-trailing-comma = true + +[tool.ruff.format] +# Use black's formatting style exactly +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = false diff --git a/scripts/chat_datasets.py b/scripts/chat_datasets.py index 1a85fcef92..0c1e0bd037 100644 --- a/scripts/chat_datasets.py +++ b/scripts/chat_datasets.py @@ -27,7 +27,7 @@ def parse_dataset(dataset=None, split="train"): break if not field_messages: raise ValueError( - f'No conversation field found in dataset: {", ".join(feature_keys)}' + f"No conversation field found in dataset: {', '.join(feature_keys)}" ) ds_cfg["field_messages"] = field_messages @@ -40,7 +40,7 @@ def parse_dataset(dataset=None, split="train"): break if not message_property_mappings["role"]: raise ValueError( - f'No role field found in messages: {", ".join(message_fields)}' + f"No role field found in messages: {', '.join(message_fields)}" ) for key in ["content", "text", "value"]: @@ -49,7 +49,7 @@ def parse_dataset(dataset=None, split="train"): break if not message_property_mappings["content"]: raise ValueError( - f'No content field found in messages: {", ".join(message_fields)}' + f"No content field found in messages: {', '.join(message_fields)}" ) ds_cfg["message_property_mappings"] = message_property_mappings diff --git a/scripts/unsloth_install.py b/scripts/unsloth_install.py index acbd05e901..c0e5bbe70f 100644 --- a/scripts/unsloth_install.py +++ b/scripts/unsloth_install.py @@ -1,11 +1,10 @@ # noqa -# pylint: skip-file import sys try: import torch -except ImportError: - raise ImportError("Install torch via `pip install torch`") +except ImportError as error: + raise ImportError("Install torch via `pip install torch`") from error from packaging.version import Version as V use_uv = "--uv" in sys.argv[1:] diff --git a/src/axolotl/cli/art.py b/src/axolotl/cli/art.py index 2051784e98..81dbb98313 100644 --- a/src/axolotl/cli/art.py +++ b/src/axolotl/cli/art.py @@ -22,7 +22,7 @@ def print_axolotl_text_art(): """Prints axolotl ASCII art.""" - global HAS_PRINTED_LOGO # pylint: disable=global-statement + global HAS_PRINTED_LOGO if HAS_PRINTED_LOGO: return if is_main_process(): diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index 6d4f999b41..7f953372d4 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -41,7 +41,7 @@ def run_cmd(cmd: str, run_folder: str, volumes=None): if exit_code := subprocess.call( # nosec B603 cmd.split(), cwd=run_folder, env=new_env ): - exit(exit_code) # pylint: disable=consider-using-sys-exit + exit(exit_code) # Commit writes to volume. if volumes: @@ -130,7 +130,6 @@ def get_secrets(self): res = [] if self.config.secrets: for key in self.config.get("secrets", []): - # pylint: disable=duplicate-code if isinstance(key, str): if val := os.environ.get(key, ""): res.append(modal.Secret.from_dict({key: val})) @@ -177,8 +176,8 @@ def preprocess(self, config_yaml: str, *args, **kwargs): with self.app.run(detach=True): modal_fn.remote( config_yaml, - volumes={k: v[0] for k, v in self.volumes.items()}, *args, + volumes={k: v[0] for k, v in self.volumes.items()}, **kwargs, ) @@ -187,7 +186,7 @@ def get_train_timeout(self): return int(self.config.timeout) return 60 * 60 * 24 # 24 hours - def get_train_gpu(self): # pylint: disable=too-many-return-statements + def get_train_gpu(self): count = self.config.gpu_count or 1 family = self.config.gpu.lower() or "l40s" @@ -277,7 +276,7 @@ def _train( launcher: Literal["accelerate", "torchrun", "python"] = "accelerate", launcher_args: list[str] | None = None, volumes=None, - **kwargs, # pylint: disable=unused-argument + **kwargs, ): Path("/workspace/mounts").mkdir(parents=True, exist_ok=True) with open("/workspace/mounts/config.yaml", "w", encoding="utf-8") as f_out: diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index 0f1245aed6..20e341a0b4 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -210,7 +210,7 @@ def load_cfg( try: device_props = torch.cuda.get_device_properties("cuda") gpu_version = "sm_" + str(device_props.major) + str(device_props.minor) - except: # pylint: disable=bare-except # noqa: E722 + except: gpu_version = None prepare_plugins(cfg) diff --git a/src/axolotl/cli/evaluate.py b/src/axolotl/cli/evaluate.py index 9dd3b00832..1a73937a2c 100644 --- a/src/axolotl/cli/evaluate.py +++ b/src/axolotl/cli/evaluate.py @@ -28,7 +28,7 @@ def do_evaluate(cfg: DictDefault, cli_args: TrainerCliArgs) -> None: cfg: Dictionary mapping `axolotl` config keys to values. cli_args: CLI arguments. """ - # pylint: disable=duplicate-code + check_accelerate_default_config() if int(os.getenv("LOCAL_RANK", "0")) == 0: check_user_token() @@ -49,7 +49,7 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: config: Path to `axolotl` config YAML file. kwargs: Additional keyword arguments to override config file values. """ - # pylint: disable=duplicate-code + parsed_cfg = load_cfg(config, **kwargs) parser = HfArgumentParser(TrainerCliArgs) parsed_cli_args, _ = parser.parse_args_into_dataclasses( diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index d03a91bc7f..06b64292fe 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -35,7 +35,7 @@ def get_multi_line_input() -> str: instruction = "" for line in sys.stdin: - instruction += line # pylint: disable=consider-using-join + instruction += line return instruction @@ -167,7 +167,6 @@ def generate(instruction): if not instruction: return if prompter_module: - # pylint: disable=stop-iteration-return prompt: str = next( prompter_module().build_prompt(instruction=instruction.strip("\n")) ) @@ -252,7 +251,7 @@ def do_cli( config: Path to `axolotl` config YAML file. kwargs: Additional keyword arguments to override config file values. """ - # pylint: disable=duplicate-code + parsed_cfg = load_cfg(config, inference=True, rl=None, **kwargs) parsed_cfg.sample_packing = False parser = transformers.HfArgumentParser(InferenceCliArgs) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index e633928029..acfa81389b 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -1,7 +1,5 @@ """Click CLI definitions for various axolotl commands.""" -# pylint: disable=redefined-outer-name - import os import subprocess # nosec B404 from typing import Literal, Optional diff --git a/src/axolotl/cli/merge_sharded_fsdp_weights.py b/src/axolotl/cli/merge_sharded_fsdp_weights.py index c99f37fb1d..43142d79e0 100644 --- a/src/axolotl/cli/merge_sharded_fsdp_weights.py +++ b/src/axolotl/cli/merge_sharded_fsdp_weights.py @@ -32,7 +32,7 @@ class BFloat16CastPlanner(_EmptyStateDictLoadPlanner): """A custom planner to cast tensors to bfloat16 on the fly during loading.""" - def commit_tensor(self, read_item, tensor): # pylint: disable=unused-argument + def commit_tensor(self, read_item, tensor): tensor.copy_(tensor.to(torch.bfloat16)) @@ -59,10 +59,10 @@ def _distributed_checkpoint_to_merged_weights( state_dict: Dict = {} save_path_ = Path(save_path) save_path_.mkdir(exist_ok=True) - dist_cp_format_utils._load_state_dict( # pylint: disable=protected-access + dist_cp_format_utils._load_state_dict( state_dict, storage_reader=dist_cp.FileSystemReader(checkpoint_dir), - planner=BFloat16CastPlanner(), # pylint: disable=protected-access + planner=BFloat16CastPlanner(), no_dist=True, ) @@ -191,7 +191,7 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): config: Path to `axolotl` config YAML file. kwargs: Additional keyword arguments to override config file values. """ - # pylint: disable=duplicate-code + parsed_cfg = load_cfg(config, **kwargs) fsdp_dir = Path(parsed_cfg.output_dir) / "pytorch_model_fsdp_0" diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index 4120062d89..ff4551c645 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -73,7 +73,7 @@ def do_preprocess(cfg: DictDefault, cli_args: PreprocessCliArgs) -> None: AutoModelForCausalLM.from_pretrained( model_name, trust_remote_code=True ) - except Exception as exc: # pylint: disable=broad-exception-caught,unused-variable # nosec B110 # noqa F841 + except Exception: # nosec B110 pass # fmt: on @@ -95,7 +95,7 @@ def do_cli( config: Path to `axolotl` config YAML file. kwargs: Additional keyword arguments to override config file values. """ - # pylint: disable=duplicate-code + os.environ["AXOLOTL_IS_PREPROCESS"] = "1" is_preprocess = kwargs.pop("is_preprocess", True) parsed_cfg = load_cfg(config, is_preprocess=is_preprocess, **kwargs) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 7f0b0bdd29..5e766de37a 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -59,7 +59,7 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): config: Path to `axolotl` config YAML file. kwargs: Additional keyword arguments to override config file values. """ - # pylint: disable=duplicate-code + parsed_cfg = load_cfg(config, **kwargs) parser = HfArgumentParser(TrainerCliArgs) parsed_cli_args, _ = parser.parse_args_into_dataclasses( diff --git a/src/axolotl/cli/utils/args.py b/src/axolotl/cli/utils/args.py index 3aea1a378a..0aec737b82 100644 --- a/src/axolotl/cli/utils/args.py +++ b/src/axolotl/cli/utils/args.py @@ -65,7 +65,7 @@ def decorator(function: Callable) -> Callable: for field in reversed(dataclasses.fields(config_class)): field_type = _strip_optional_type(field.type) - if field_type == bool: + if field_type is bool: field_name = field.name.replace("_", "-") option_name = f"--{field_name}/--no-{field_name}" function = click.option( @@ -103,7 +103,7 @@ def decorator(function: Callable) -> Callable: for name, field in reversed(config_class.model_fields.items()): field_type = _strip_optional_type(field.annotation) - if field_type == bool: + if field_type is bool: field_name = name.replace("_", "-") option_name = f"--{field_name}/--no-{field_name}" function = click.option( diff --git a/src/axolotl/cli/utils/sweeps.py b/src/axolotl/cli/utils/sweeps.py index bb1368cf6e..2a0aa1367a 100644 --- a/src/axolotl/cli/utils/sweeps.py +++ b/src/axolotl/cli/utils/sweeps.py @@ -49,7 +49,10 @@ def generate_sweep_configs( new_config = {} # new_config = deepcopy(base_config) # Combine regular parameters with paired parameters - full_combo = {**dict(zip(param_names, reg_combo)), **paired_set} + full_combo = { + **dict(zip(param_names, reg_combo, strict=False)), + **paired_set, + } for param_name, param_value in full_combo.items(): new_config[param_name] = param_value print(new_config) @@ -58,7 +61,7 @@ def generate_sweep_configs( # If no paired values, just use regular combinations # new_config = deepcopy(base_config) new_config = {} - for param_name, param_value in zip(param_names, reg_combo): + for param_name, param_value in zip(param_names, reg_combo, strict=False): new_config[param_name] = param_value print(new_config) all_combinations.append(new_config) diff --git a/src/axolotl/cli/utils/train.py b/src/axolotl/cli/utils/train.py index b133d7271c..6ce7d8df30 100644 --- a/src/axolotl/cli/utils/train.py +++ b/src/axolotl/cli/utils/train.py @@ -95,7 +95,6 @@ def generate_config_files(config: str, sweep: str | None) -> Iterator[tuple[str, permutation_id = f"sweep{idx:04d}" permutation["output_dir"] = str(permutation_dir / permutation_id) - # pylint: disable=consider-using-with temp_file = tempfile.NamedTemporaryFile( mode="w", suffix=".yaml", diff --git a/src/axolotl/cli/vllm_serve.py b/src/axolotl/cli/vllm_serve.py index cf687bea2a..ea454fc96a 100644 --- a/src/axolotl/cli/vllm_serve.py +++ b/src/axolotl/cli/vllm_serve.py @@ -39,7 +39,7 @@ def do_vllm_serve( model = cfg.base_model serve_module = cli_args.get("serve_module", "trl.scripts.vllm_serve") - vllm_serve_main = getattr(__import__(serve_module, fromlist=["main"]), "main") + vllm_serve_main = __import__(serve_module, fromlist=["main"]).main tensor_parallel_size = 1 data_parallel_size = 1 @@ -68,7 +68,6 @@ def do_vllm_serve( cli_args.get("enable_reasoning") or cfg.vllm.enable_reasoning or False ) - # pylint: disable=unexpected-keyword-arg vllm_script_args = AxolotlScriptArguments( model=model, tensor_parallel_size=tensor_parallel_size, diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index 0ff52ebe1a..e7433e3c25 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -6,6 +6,7 @@ from datasets import Dataset +import axolotl.monkeypatch.data.batch_dataset_fetcher # noqa: F401 from axolotl.cli.args import PreprocessCliArgs, TrainerCliArgs from axolotl.loaders import load_processor, load_tokenizer from axolotl.utils.data import prepare_datasets, prepare_preference_datasets diff --git a/src/axolotl/convert.py b/src/axolotl/convert.py index d1bdb34db1..9e09b37dcb 100644 --- a/src/axolotl/convert.py +++ b/src/axolotl/convert.py @@ -67,9 +67,7 @@ def __init__(self, file_reader, file_writer, json_parser, jsonl_serializer): self.json_parser = json_parser self.jsonl_serializer = jsonl_serializer - def convert( - self, input_file_path, output_file_path - ): # pylint: disable=unused-argument + def convert(self, input_file_path, output_file_path): content = self.file_reader.read(input_file_path) data = self.json_parser.parse(content) # data = [r for r in data if r["conversations"]] # vicuna cleaned has rows with empty conversations diff --git a/src/axolotl/core/attention/flex_block_mask.py b/src/axolotl/core/attention/flex_block_mask.py index fb9820f352..37149983c8 100644 --- a/src/axolotl/core/attention/flex_block_mask.py +++ b/src/axolotl/core/attention/flex_block_mask.py @@ -84,9 +84,7 @@ def create_causal_mask( batch_size, dtype = input_embeds.shape[0], input_embeds.dtype if attention_mask is not None: - def causal_doc_mask_mod( - batch_idx, head_idx, q_idx, kv_idx - ): # pylint: disable=unused-argument + def causal_doc_mask_mod(batch_idx, head_idx, q_idx, kv_idx): """ Defines the logic of a block causal mask by combining both a standard causal mask and a block diagonal document mask. @@ -103,9 +101,7 @@ def causal_doc_mask_mod( mask_factory_function = causal_doc_mask_mod else: mask_factory_function = causal_mask_function - mask_interface = ALL_MASK_ATTENTION_FUNCTIONS[ - config._attn_implementation # pylint: disable=protected-access - ] + mask_interface = ALL_MASK_ATTENTION_FUNCTIONS[config._attn_implementation] # Do not allow skip if we are compiling (this is to match BC) allow_is_causal_skip = ( diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index e1f6497151..44699e6ac2 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -44,7 +44,7 @@ LOG = logging.getLogger(__name__) with suppress(ImportError): - import torch._dynamo # pylint: disable=ungrouped-imports + import torch._dynamo class TrainerBuilderBase(abc.ABC): @@ -260,14 +260,14 @@ def _configure_custom_optimizer( adam_kwargs["eps"] = training_args_kwargs.get("adam_epsilon") if self.cfg.optimizer == "muon": - from axolotl.contribs.mit.muon import ( # pylint: disable=no-name-in-module + from axolotl.contribs.mit.muon import ( MuonOptimizerFactory, ) optimizer_cls = MuonOptimizerFactory optimizer_kwargs.update(adam_kwargs) elif self.cfg.optimizer == "dion": - from axolotl.contribs.mit.dion import ( # pylint: disable=no-name-in-module + from axolotl.contribs.mit.dion import ( DionOptimizerFactory, ) @@ -414,12 +414,8 @@ def _configure_reporting(self, training_args_kwargs: dict): def _configure_torch_compile(self, training_args_kwargs: dict): if self.cfg.torch_compile and getattr(torch, "_dynamo", None): - torch._dynamo.config.suppress_errors = ( # pylint: disable=protected-access - True - ) - torch._dynamo.config.accumulated_cache_size_limit = ( # pylint: disable=protected-access - 256 - ) + torch._dynamo.config.suppress_errors = True + torch._dynamo.config.accumulated_cache_size_limit = 256 training_args_kwargs["torch_compile"] = self.cfg.torch_compile if self.cfg.torch_compile_backend: training_args_kwargs["torch_compile_backend"] = ( diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index e5bc217621..94b0db8515 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -344,16 +344,14 @@ def build(self, total_num_steps): training_args_cls = AxolotlPRMConfig else: training_args_cls = AxolotlTrainingArguments - training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg + training_args = training_args_cls( **training_arguments_kwargs, ) training_args = self.hook_post_create_training_args(training_args) # unset run_name so wandb sets up experiment names if self.cfg.use_wandb and training_args.run_name == training_args.output_dir: - training_args.run_name = ( # pylint: disable=attribute-defined-outside-init - None - ) + training_args.run_name = None data_collator_kwargs = { "padding": True, # True/"longest" is the default diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index bc78168074..a6e8355f44 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -168,16 +168,14 @@ def _build_training_arguments(self, total_num_steps): if plugin_training_args: training_args_kwargs.update(plugin_training_args) - training_args = training_args_cls( # pylint: disable=unexpected-keyword-arg + training_args = training_args_cls( logging_first_step=True, **training_args_kwargs, ) # unset run_name so wandb sets up experiment names if self.cfg.use_wandb and training_args.run_name == training_args.output_dir: - training_args.run_name = ( # pylint: disable=attribute-defined-outside-init - None - ) + training_args.run_name = None return training_args, trainer_kwargs diff --git a/src/axolotl/core/chat/format/chatml.py b/src/axolotl/core/chat/format/chatml.py index 04c398fe81..deb8a9997b 100644 --- a/src/axolotl/core/chat/format/chatml.py +++ b/src/axolotl/core/chat/format/chatml.py @@ -10,7 +10,7 @@ def format_message( message: Messages, - message_index: Optional[int] = None, # pylint: disable=unused-argument + message_index: Optional[int] = None, ) -> Messages: if message.is_chat_formatted: return message diff --git a/src/axolotl/core/chat/messages.py b/src/axolotl/core/chat/messages.py index 923b177c1f..912a12ca1e 100644 --- a/src/axolotl/core/chat/messages.py +++ b/src/axolotl/core/chat/messages.py @@ -15,11 +15,11 @@ class MessageRoles(str, Enum): Message roles for the system, user, assistant, and tools """ - system = "system" # pylint: disable=invalid-name - user = "user" # pylint: disable=invalid-name - assistant = "assistant" # pylint: disable=invalid-name - tool = "tool" # pylint: disable=invalid-name - ipython = ( # pylint: disable=invalid-name + system = "system" + user = "user" + assistant = "assistant" + tool = "tool" + ipython = ( # for responses from builtin tools "ipython" ) @@ -30,12 +30,12 @@ class MessageContentTypes(str, Enum): Message content types for text, image, audio, tool calls, and tool responses """ - special_token = "special_token" # pylint: disable=invalid-name # nosec B105 - text = "text" # pylint: disable=invalid-name - image = "image" # pylint: disable=invalid-name - audio = "audio" # pylint: disable=invalid-name - tool_call = "tool_call" # pylint: disable=invalid-name # to differentiate regular responses from tool calls from the assistant - tool_response = "tool_response" # pylint: disable=invalid-name + special_token = "special_token" # nosec B105 + text = "text" + image = "image" + audio = "audio" + tool_call = "tool_call" + tool_response = "tool_response" class SpecialToken(str, Enum): @@ -43,8 +43,8 @@ class SpecialToken(str, Enum): Special tokens for beginning of string and end of string """ - bos_token = "bos_token" # pylint: disable=invalid-name # nosec B105 - eos_token = "eos_token" # pylint: disable=invalid-name # nosec B105 + bos_token = "bos_token" # nosec B105 + eos_token = "eos_token" # nosec B105 class ToolCallFunction(BaseModel): @@ -73,7 +73,7 @@ class ToolCallContents(BaseModel): name: str arguments: dict[str, Union[str, int]] - id: Optional[str] = None # pylint: disable=invalid-name + id: Optional[str] = None def __str__(self) -> str: data = {"name": self.name, "arguments": self.arguments} @@ -89,7 +89,7 @@ class ToolResponseContents(BaseModel): name: str content: Union[str, dict[str, Union[str, int, float]]] - id: Optional[str] = None # pylint: disable=invalid-name + id: Optional[str] = None def __str__(self) -> str: data = {"name": self.name, "content": self.content} diff --git a/src/axolotl/core/datasets/transforms/chat_builder.py b/src/axolotl/core/datasets/transforms/chat_builder.py index 692fe3ebb5..8f2013027b 100644 --- a/src/axolotl/core/datasets/transforms/chat_builder.py +++ b/src/axolotl/core/datasets/transforms/chat_builder.py @@ -1,23 +1,17 @@ """ -This module contains a function that builds a transform that takes a row from the dataset and converts it to a Chat. +This module contains a function that builds a transform that takes a row from the +dataset and converts it to a Chat. """ -from typing import Any, Mapping, Union +from typing import Any, Mapping -def chat_message_transform_builder( # pylint: disable=dangerous-default-value +def chat_message_transform_builder( train_on_inputs=False, conversations_field: str = "conversations", - message_field_role: Union[str, list[str]] = ["role", "from"], # commonly "role" - message_field_content: Union[str, list[str]] = [ - "value", - "text", - "content", - ], # commonly "content" - message_field_training: Union[str, list[str]] = [ - "train", - "weight", - ], # commonly "weight" + message_field_role: str | list[str] | None = None, # commonly "role" + message_field_content: str | list[str] | None = None, # commonly "content" + message_field_training: str | list[str] | None = None, # commonly "weight" ): """Builds a transform that takes a row from the dataset and converts it to a Chat @@ -39,6 +33,12 @@ def chat_message_transform_builder( # pylint: disable=dangerous-default-value A function that takes a list of conversations and returns a list of messages. """ + if message_field_training is None: + message_field_training = ["train", "weight"] + if message_field_content is None: + message_field_content = ["value", "text", "content"] + if message_field_role is None: + message_field_role = ["role", "from"] message_field_role = ( [message_field_role] if isinstance(message_field_role, str) diff --git a/src/axolotl/core/trainers/__init__.py b/src/axolotl/core/trainers/__init__.py index a9cda4efc5..22d8b64f68 100644 --- a/src/axolotl/core/trainers/__init__.py +++ b/src/axolotl/core/trainers/__init__.py @@ -1,6 +1,5 @@ """Init for axolotl.core.trainers""" -# pylint: disable=unused-import # flake8: noqa from .base import AxolotlTrainer diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 0f9f6e4c4d..4b88617900 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -1,7 +1,5 @@ """Module for customized trainers""" -# pylint: disable=too-many-lines - from __future__ import annotations import os @@ -285,9 +283,9 @@ def _get_dataloader( # fmt: off if dataloader_key is not None and self.args.dataloader_persistent_workers: if hasattr(self, "_eval_dataloaders"): - self._eval_dataloaders[dataloader_key] = dataloader # type: ignore # pylint: disable=access-member-before-definition + self._eval_dataloaders[dataloader_key] = dataloader # type: ignore else: - self._eval_dataloaders = {dataloader_key: dataloader} # pylint: disable=attribute-defined-outside-init + self._eval_dataloaders = {dataloader_key: dataloader} # fmt: on return self.accelerator.prepare(dataloader) @@ -443,7 +441,7 @@ def orpo_compute_loss( model, inputs, return_outputs=False, - num_items_in_batch=None, # pylint: disable=unused-argument + num_items_in_batch=None, ): concat_inputs = AxolotlTrainer.orpo_concatenate_inputs( inputs, @@ -524,9 +522,7 @@ def create_accelerator_and_postprocess(self): accelerator_config = self.args.accelerator_config.to_dict() use_configured_state = accelerator_config.get("use_configured_state", False) if not use_configured_state: - AcceleratorState._reset_state( # pylint: disable=protected-access - reset_partial_state=True - ) + AcceleratorState._reset_state(reset_partial_state=True) super().create_accelerator_and_postprocess() @@ -540,7 +536,6 @@ def create_accelerator_and_postprocess(self): ): self.accelerator.state.fsdp_plugin.limit_all_gathers = True - # pylint: disable=unused-argument def additional_accelerator_args( self, fp8: bool = False, enable_fsdp_float8_all_gather: bool = False, **kwargs ) -> dict[str, Any]: diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index b3067bb462..b04505d89c 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -101,11 +101,11 @@ def concatenated_forward( ) -> dict[str, torch.Tensor]: if self.args.dpo_norm_loss: # fmt: off - loss_type: str = self.loss_type # type: ignore[has-type] # pylint: disable=access-member-before-definition + loss_type: str = self.loss_type # type: ignore[has-type] # fmt: on # concatenated_forward handles avg token logprob for ipo case already - self.loss_type = "ipo" # pylint: disable=attribute-defined-outside-init + self.loss_type = "ipo" res = super().concatenated_forward(model, batch, is_ref_model=is_ref_model) - self.loss_type = loss_type # pylint: disable=attribute-defined-outside-init + self.loss_type = loss_type return res return super().concatenated_forward(model, batch, is_ref_model=is_ref_model) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 4106a2a7d5..7eda7a0bac 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -128,9 +128,7 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: return grpo_args_kwargs @classmethod - def set_trainer_args( - cls, cfg: DictDefault - ) -> list[Any]: # pylint: disable=unused-argument + def set_trainer_args(cls, cfg: DictDefault) -> list[Any]: trainer_args = [] if cfg.trl and cfg.trl.reward_funcs: reward_funcs = [] @@ -151,7 +149,7 @@ def set_trainer_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: return trainer_kwargs @classmethod - def get_collator(cls, *args, **kwargs): # pylint: disable=unused-argument + def get_collator(cls, *args, **kwargs): # No data collation is needed in GRPO, handled by trl's trainer __init__ return None diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index 49caa64067..f9f5a695b9 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -1,7 +1,5 @@ """Axolotl GRPO trainers (with and without sequence parallelism handling)""" -# pylint: disable=too-many-lines,duplicate-code,protected-access,no-member - import warnings from functools import partial from typing import Any @@ -52,7 +50,6 @@ from axolotl.monkeypatch.ring_attn import get_ring_attn_group if is_peft_available(): - # pylint: disable=unused-import from peft import PeftConfig @@ -253,7 +250,7 @@ def _prepare_dataloader( def get_train_dataloader(self) -> DataLoader: """Get dataloader for training""" train_dataset = self.train_dataset - # pylint: disable=access-member-before-definition + data_collator = self.data_collator # type: ignore # Handle dataset preprocessing @@ -266,7 +263,7 @@ def get_train_dataloader(self) -> DataLoader: train_dataset, description="training" ) else: - self.data_collator = self._get_collator_with_removed_columns( # pylint: disable=attribute-defined-outside-init + self.data_collator = self._get_collator_with_removed_columns( data_collator, description="training", ) @@ -308,10 +305,10 @@ def _generate_and_score_completions( # Generate completions using either vLLM or regular generation if self.args.use_vllm: # First, have main process load weights if needed - # pylint: disable=access-member-before-definition + if self.state.global_step != self._last_loaded_step: # type: ignore[has-type] self._move_model_to_vllm() - # pylint: disable=attribute-defined-outside-init + self._last_loaded_step = self.state.global_step # Generate completions using vLLM: gather all prompts and use them in a single call in the main process @@ -333,8 +330,9 @@ def _generate_and_score_completions( # Extract prompts from this SP group, accounting for num_generations duplicates # We only need prompts from one rank in each SP group group_prompts = all_prompts_text[ - group_leader_rank - * len(prompts_text) : (group_leader_rank + 1) + group_leader_rank * len(prompts_text) : ( + group_leader_rank + 1 + ) * len(prompts_text) : self.num_generations ] @@ -485,7 +483,7 @@ def _generate_and_score_completions( ) if is_conversational(inputs[0]): completions = [] - for prompt, completion in zip(prompts, completions_text): + for prompt, completion in zip(prompts, completions_text, strict=False): bootstrap = ( prompt.pop()["content"] if prompt[-1]["role"] == "assistant" else "" ) @@ -503,6 +501,7 @@ def _generate_and_score_completions( self.reward_funcs, self.reward_processing_classes, self.reward_func_names, + strict=False, ) ): with profiling_context(self, reward_func_name): @@ -511,14 +510,17 @@ def _generate_and_score_completions( ): # Module instead of PretrainedModel for compat with compiled models if is_conversational(inputs[0]): messages = [ - {"messages": p + c} for p, c in zip(prompts, completions) + {"messages": p + c} + for p, c in zip(prompts, completions, strict=False) ] texts = [ apply_chat_template(x, reward_processing_class)["text"] for x in messages ] else: - texts = [p + c for p, c in zip(prompts, completions)] + texts = [ + p + c for p, c in zip(prompts, completions, strict=False) + ] reward_inputs = reward_processing_class( text=texts, return_tensors="pt", @@ -564,7 +566,8 @@ def _generate_and_score_completions( row_reward_kwargs["completion"] = completions[nan_row_idx] warnings.warn( f"All reward functions returned None for the following kwargs: {row_reward_kwargs}. " - "Please ensure that at least one reward function returns a valid reward." + "Please ensure that at least one reward function returns a valid reward.", + stacklevel=2, ) # Gather the reward per function: this part is crucial, because the rewards are normalized per group and the diff --git a/src/axolotl/core/trainers/mamba.py b/src/axolotl/core/trainers/mamba.py index b475b26d9f..dedda1b290 100644 --- a/src/axolotl/core/trainers/mamba.py +++ b/src/axolotl/core/trainers/mamba.py @@ -5,7 +5,6 @@ from axolotl.core.trainers.base import AxolotlTrainer -# pylint: disable=too-many-ancestors class AxolotlMambaTrainer(AxolotlTrainer): """Mamba specific trainer to handle loss calculation""" @@ -15,8 +14,8 @@ def compute_loss( self, model, inputs, - return_outputs=False, # pylint: disable=unused-argument - num_items_in_batch=None, # pylint: disable=unused-argument + return_outputs=False, + num_items_in_batch=None, ): input_ids = inputs.pop("input_ids") lm_logits = model(input_ids).logits diff --git a/src/axolotl/core/trainers/mixins/__init__.py b/src/axolotl/core/trainers/mixins/__init__.py index b54577765a..5fced16921 100644 --- a/src/axolotl/core/trainers/mixins/__init__.py +++ b/src/axolotl/core/trainers/mixins/__init__.py @@ -1,6 +1,5 @@ """Init for axolotl.core.trainers.mixins""" -# pylint: disable=unused-import # flake8: noqa from .activation_checkpointing import ActivationOffloadingMixin diff --git a/src/axolotl/core/trainers/mixins/activation_checkpointing.py b/src/axolotl/core/trainers/mixins/activation_checkpointing.py index 1bfdb49f7e..b61c45feed 100644 --- a/src/axolotl/core/trainers/mixins/activation_checkpointing.py +++ b/src/axolotl/core/trainers/mixins/activation_checkpointing.py @@ -92,7 +92,7 @@ def get_lora_act_offloading_ctx_manager( `contextlib.ContextDecorator`: Activation offloading context manager for the model. """ - # pylint: disable=unnecessary-dunder-call + activations_handling_ctx = OffloadActivations( use_pin_memory=use_pin_memory, use_streams=use_streams, diff --git a/src/axolotl/core/trainers/mixins/distributed_parallel.py b/src/axolotl/core/trainers/mixins/distributed_parallel.py index d163e4eb57..77aee5236b 100644 --- a/src/axolotl/core/trainers/mixins/distributed_parallel.py +++ b/src/axolotl/core/trainers/mixins/distributed_parallel.py @@ -26,7 +26,6 @@ def create_accelerator_and_postprocess(self): self.accelerator.distributed_type == "FSDP" and self.accelerator.state.fsdp_plugin is None ): - # pylint: disable=protected-access # handle Context Parallelism without FSDP self.accelerator.state.distributed_type = "MULTI_GPU" self.accelerator.state._shared_state["distributed_type"] = "MULTI_GPU" diff --git a/src/axolotl/core/trainers/mixins/optimizer.py b/src/axolotl/core/trainers/mixins/optimizer.py index a9a9a3992d..850442c608 100644 --- a/src/axolotl/core/trainers/mixins/optimizer.py +++ b/src/axolotl/core/trainers/mixins/optimizer.py @@ -70,11 +70,11 @@ def create_optimizer_grouped_parameters( } ) if params["embeddings"]: - lr = optimizer_kwargs["lr"] # pylint: disable=invalid-name + lr = optimizer_kwargs["lr"] if self.args.embedding_lr_scale: - lr *= self.args.embedding_lr_scale # pylint: disable=invalid-name + lr *= self.args.embedding_lr_scale elif self.args.embedding_lr: - lr = self.args.embedding_lr # pylint: disable=invalid-name + lr = self.args.embedding_lr optimizer_grouped_parameters.append( { "params": list(params["embeddings"].values()), @@ -143,7 +143,7 @@ def create_optimizer(self): loraplus_lr_embedding = getattr( self.args, "loraplus_lr_embedding", 1e-6 ) - self.optimizer = create_loraplus_optimizer( # pylint: disable=attribute-defined-outside-init + self.optimizer = create_loraplus_optimizer( opt_model, optimizer_cls, loraplus_lr_ratio=loraplus_lr_ratio, @@ -185,17 +185,15 @@ def create_optimizer(self): p.data_ptr(): p.numel() for p in module.parameters() }.values() ) - LOG.info(f"skipped {module}: {skipped/2**20}M params") + LOG.info(f"skipped {module}: {skipped / 2**20}M params") manager.register_module_override( module, "weight", {"optim_bits": 32} ) LOG.debug(f"bitsandbytes: will optimize {module} in fp32") - LOG.info(f"skipped: {skipped/2**20}M params") + LOG.info(f"skipped: {skipped / 2**20}M params") if is_sagemaker_mp_enabled(): - self.optimizer = smp.DistributedOptimizer( # pylint: disable=attribute-defined-outside-init - self.optimizer - ) + self.optimizer = smp.DistributedOptimizer(self.optimizer) return self.optimizer diff --git a/src/axolotl/core/trainers/mixins/scheduler.py b/src/axolotl/core/trainers/mixins/scheduler.py index 399bf59470..fc2b0e59d4 100644 --- a/src/axolotl/core/trainers/mixins/scheduler.py +++ b/src/axolotl/core/trainers/mixins/scheduler.py @@ -46,7 +46,7 @@ def create_scheduler( ) # fmt: off - if self.lr_scheduler is None: # type: ignore # pylint: disable=access-member-before-definition + if self.lr_scheduler is None: # type: ignore # fmt: on plugin_manager = PluginManager.get_instance() lr_scheduler: LRScheduler | None = plugin_manager.create_lr_scheduler( @@ -90,7 +90,7 @@ def create_scheduler( LOG.warning( "Both cosine quadratic warmup and min lr detected. Using quadratic warmup.") - self.lr_scheduler = get_cosine_schedule_with_quadratic_warmup( # pylint: disable=attribute-defined-outside-init + self.lr_scheduler = get_cosine_schedule_with_quadratic_warmup( optimizer, num_warmup_steps=self.args.get_warmup_steps(num_training_steps), num_training_steps=num_training_steps, @@ -98,7 +98,7 @@ def create_scheduler( elif self.args.cosine_min_lr_ratio and self.args.cosine_constant_lr_ratio and use_cosine_min_lr: assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" assert 0 <= self.args.cosine_constant_lr_ratio <= 1.0, "cosine_constant_lr_ratio must be between 0.0 and 1.0" - self.lr_scheduler = get_cosine_schedule_with_warmup_decay_constant( # pylint: disable=attribute-defined-outside-init + self.lr_scheduler = get_cosine_schedule_with_warmup_decay_constant( optimizer, num_warmup_steps=self.args.get_warmup_steps(num_training_steps), num_training_steps=num_training_steps, @@ -107,7 +107,7 @@ def create_scheduler( ) elif self.args.cosine_min_lr_ratio and use_cosine_min_lr: assert 0 <= self.args.cosine_min_lr_ratio <= 1.0, "cosine_min_lr_ratio must be between 0.0 and 1.0" - self.lr_scheduler = get_cosine_schedule_with_min_lr( # pylint: disable=attribute-defined-outside-init + self.lr_scheduler = get_cosine_schedule_with_min_lr( optimizer, num_warmup_steps=self.args.get_warmup_steps(num_training_steps), num_training_steps=num_training_steps, @@ -133,7 +133,7 @@ def create_scheduler( ) if not self.lr_scheduler: super().create_scheduler(num_training_steps, optimizer) - self.lr_scheduler = JaggedLRRestartScheduler( # pylint: disable=attribute-defined-outside-init + self.lr_scheduler = JaggedLRRestartScheduler( optimizer, self.lr_scheduler, self.args.jagged_restart_steps, diff --git a/src/axolotl/core/training_args_base.py b/src/axolotl/core/training_args_base.py index fd0859ae91..a9cc7d2243 100644 --- a/src/axolotl/core/training_args_base.py +++ b/src/axolotl/core/training_args_base.py @@ -14,7 +14,6 @@ class AxolotlTrainingMixins: Mixin class for the Axolotl training args. """ - # pylint: disable=duplicate-code model_type: Optional[str] = field( default=None, metadata={"help": "HF model configuration model_type."} ) diff --git a/src/axolotl/datasets.py b/src/axolotl/datasets.py index c9d006ac8d..b8f9484bc4 100644 --- a/src/axolotl/datasets.py +++ b/src/axolotl/datasets.py @@ -26,7 +26,7 @@ class TokenizedPromptDataset(Dataset): keep_in_memory: Whether to keep the tokenized dataset in memory. """ - def __init__( # pylint: disable=super-init-not-called + def __init__( self, prompt_tokenizer: PromptTokenizingStrategy, dataset: Dataset, @@ -99,7 +99,7 @@ class ConstantLengthDataset(IterableDataset): seq_length: Length of token sequences to return. """ - def __init__( # pylint: disable=super-init-not-called + def __init__( self, tokenizer, datasets, diff --git a/src/axolotl/evaluate.py b/src/axolotl/evaluate.py index 2b58699396..e4496bee65 100644 --- a/src/axolotl/evaluate.py +++ b/src/axolotl/evaluate.py @@ -79,7 +79,7 @@ def evaluate(*, cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> Dict[str, f model, tokenizer, _, processor = setup_model_and_tokenizer(cfg) # Get datasets - # pylint: disable=duplicate-code + train_dataset = dataset_meta.train_dataset eval_dataset = dataset_meta.eval_dataset total_num_steps = dataset_meta.total_num_steps diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 94ee8d4b1a..8edee18a3d 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -76,7 +76,7 @@ class BasePlugin: def __init__(self): """Initializes the BasePlugin.""" - def register(self, cfg: dict): # pylint: disable=unused-argument + def register(self, cfg: dict): """Registers the plugin with the given configuration as an unparsed dict. Args: @@ -104,14 +104,13 @@ def load_datasets( dataset_meta: The metadata for the training dataset. """ - def pre_model_load(self, cfg: DictDefault): # pylint: disable=unused-argument + def pre_model_load(self, cfg: DictDefault): """Performs actions before the model is loaded. Args: cfg: The configuration for the plugin. """ - # pylint: disable=unused-argument def post_model_build(self, cfg: DictDefault, model: PreTrainedModel): """Performs actions after the model is built/loaded, but before any adapters are applied. @@ -119,7 +118,6 @@ def post_model_build(self, cfg: DictDefault, model: PreTrainedModel): cfg: The configuration for the plugin. """ - # pylint: disable=unused-argument def pre_lora_load(self, cfg: DictDefault, model: PreTrainedModel): """Performs actions before LoRA weights are loaded. @@ -128,7 +126,6 @@ def pre_lora_load(self, cfg: DictDefault, model: PreTrainedModel): model: The loaded model. """ - # pylint: disable=unused-argument def post_lora_load(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): """Performs actions after LoRA weights are loaded. @@ -137,7 +134,6 @@ def post_lora_load(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): model: The loaded model. """ - # pylint: disable=unused-argument def post_model_load(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): """Performs actions after the model is loaded. @@ -146,7 +142,6 @@ def post_model_load(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): model: The loaded model. """ - # pylint: disable=unused-argument def get_trainer_cls(self, cfg: DictDefault) -> Trainer | None: """Returns a custom class for the trainer. @@ -157,7 +152,6 @@ def get_trainer_cls(self, cfg: DictDefault) -> Trainer | None: The first non-`None` trainer class returned by a plugin. """ - # pylint: disable=unused-argument def post_trainer_create(self, cfg: DictDefault, trainer: Trainer): """Performs actions after the trainer is created. @@ -166,7 +160,7 @@ def post_trainer_create(self, cfg: DictDefault, trainer: Trainer): trainer: The trainer object for training. """ - def get_training_args(self, cfg: DictDefault): # pylint: disable=unused-argument): + def get_training_args(self, cfg: DictDefault): """ Returns custom training arguments to set on TrainingArgs. @@ -177,9 +171,7 @@ def get_training_args(self, cfg: DictDefault): # pylint: disable=unused-argumen object: dict containing the training arguments. """ - def get_collator_cls_and_kwargs( - self, cfg: DictDefault, is_eval: bool = False - ): # pylint: disable=unused-argument): + def get_collator_cls_and_kwargs(self, cfg: DictDefault, is_eval: bool = False): """ Returns a custom class for the collator. @@ -191,7 +183,6 @@ def get_collator_cls_and_kwargs( class: The class for the collator. """ - # pylint: disable=unused-argument def create_optimizer(self, cfg: DictDefault, trainer: Trainer) -> Optimizer | None: """Creates and returns an optimizer for training. @@ -203,7 +194,6 @@ def create_optimizer(self, cfg: DictDefault, trainer: Trainer) -> Optimizer | No The created optimizer. """ - # pylint: disable=unused-argument def create_lr_scheduler( self, cfg: DictDefault, @@ -223,7 +213,6 @@ def create_lr_scheduler( The created learning rate scheduler. """ - # pylint: disable=unused-argument def add_callbacks_pre_trainer( self, cfg: DictDefault, model: PreTrainedModel ) -> list[Callable]: @@ -238,7 +227,6 @@ def add_callbacks_pre_trainer( """ return [] - # pylint: disable=unused-argument def add_callbacks_post_trainer( self, cfg: DictDefault, trainer: Trainer ) -> list[Callable]: @@ -254,7 +242,6 @@ def add_callbacks_post_trainer( """ return [] - # pylint: disable=unused-argument def post_train(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): """Performs actions after training is complete. @@ -263,7 +250,7 @@ def post_train(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): model: The loaded model. """ - def post_train_unload(self, cfg: DictDefault): # pylint: disable=unused-argument + def post_train_unload(self, cfg: DictDefault): """Performs actions after training is complete and the model is unloaded. Args: @@ -311,7 +298,7 @@ class from the module, and creates an instance of the class. return plugin -class PluginManager: # pylint: disable=too-many-public-methods +class PluginManager: """The `PluginManager` class is responsible for loading and managing plugins. It should be a singleton so it can be accessed from anywhere in the codebase. diff --git a/src/axolotl/integrations/config.py b/src/axolotl/integrations/config.py index f5fc07e9e2..2217b28195 100644 --- a/src/axolotl/integrations/config.py +++ b/src/axolotl/integrations/config.py @@ -50,15 +50,9 @@ def merge_input_args(): dynamic_input += f"class AxolotlInputConfig(AxolotlInputConfigBase, {', '.join(plugin_classes)}):\n pass\n" namespace: Dict[Any, Any] = {} - exec( # pylint: disable=exec-used # nosec B102 - dynamic_input, globals(), namespace - ) - AxolotlInputConfig = namespace[ # pylint: disable=invalid-name - "AxolotlInputConfig" - ] - AxolotlConfigWCapabilities = namespace[ # pylint: disable=invalid-name - "AxolotlConfigWCapabilities" - ] + exec(dynamic_input, globals(), namespace) # nosec B102 + AxolotlInputConfig = namespace["AxolotlInputConfig"] + AxolotlConfigWCapabilities = namespace["AxolotlConfigWCapabilities"] return AxolotlConfigWCapabilities, AxolotlInputConfig return AxolotlConfigWCapabilitiesBase, AxolotlInputConfigBase @@ -74,7 +68,7 @@ def merge_training_args() -> Type: Returns: tuple: A tuple containing the newly created classes, AxolotlTrainingMixins. """ - # pylint: disable=duplicate-code + from axolotl.core.training_args_base import ( AxolotlTrainingMixins as AxolotlTrainingMixinsBase, ) @@ -93,11 +87,7 @@ def merge_training_args() -> Type: namespace: Dict[Any, Any] = {} local_vars = {"AxolotlTrainingMixinsBase": AxolotlTrainingMixinsBase} - exec( # pylint: disable=exec-used # nosec B102 - dynamic_input, {**globals(), **local_vars}, namespace - ) - AxolotlTrainingMixins = namespace[ # pylint: disable=invalid-name - "AxolotlTrainingMixins" - ] + exec(dynamic_input, {**globals(), **local_vars}, namespace) # nosec B102 + AxolotlTrainingMixins = namespace["AxolotlTrainingMixins"] return AxolotlTrainingMixins return AxolotlTrainingMixinsBase diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 4689cc9a8d..6dd7c97e1f 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -18,6 +18,7 @@ Cut Cross Entropy is an optimized implementation of cross entropy loss from Apple's ML team. """ + import importlib from functools import partial @@ -28,7 +29,7 @@ from axolotl.utils.callbacks.models import get_causal_lm_model_cls_prefix from axolotl.utils.logging import get_logger -from .args import CutCrossEntropyArgs # pylint: disable=unused-import. # noqa: F401 +from .args import CutCrossEntropyArgs as CutCrossEntropyArgs LOG = get_logger(__name__) @@ -106,9 +107,7 @@ def patch_llama_like( """ from cut_cross_entropy.transformers.patch import PATCH_FNS - def patch_generic( - maybe_model, patch_options, model_type: str - ): # pylint: disable=unused-argument + def patch_generic(maybe_model, patch_options, model_type: str): import cut_cross_entropy.transformers.llama from cut_cross_entropy.transformers.llama import cce_forward @@ -121,12 +120,10 @@ def patch_generic( ) model_cls = getattr(module, f"{model_cls_prefix}ForCausalLM") - cut_cross_entropy.transformers.llama._PATCH_OPTS = ( # pylint: disable=protected-access - patch_options - ) + cut_cross_entropy.transformers.llama._PATCH_OPTS = patch_options model_cls.forward = cce_forward - # pylint: disable=duplicate-code + except (ImportError, AttributeError) as e: raise RuntimeError( f"Could not import ForCausalLM class for model_type: {model_type}. " diff --git a/src/axolotl/integrations/cut_cross_entropy/args.py b/src/axolotl/integrations/cut_cross_entropy/args.py index 22852479a1..3eeb9fac73 100644 --- a/src/axolotl/integrations/cut_cross_entropy/args.py +++ b/src/axolotl/integrations/cut_cross_entropy/args.py @@ -15,6 +15,7 @@ """ Module for handling Cut Cross Entropy input arguments. """ + from typing import Optional from pydantic import BaseModel, model_validator diff --git a/src/axolotl/integrations/grokfast/__init__.py b/src/axolotl/integrations/grokfast/__init__.py index 234d27226a..df8cf2cf39 100644 --- a/src/axolotl/integrations/grokfast/__init__.py +++ b/src/axolotl/integrations/grokfast/__init__.py @@ -7,7 +7,7 @@ from axolotl.utils.logging import get_logger from ..base import BasePlugin -from .args import GrokfastArgs # pylint: disable=unused-import. # noqa: F401 +from .args import GrokfastArgs as GrokfastArgs from .optimizer import gradfilter_ema LOG = get_logger(__name__) @@ -24,12 +24,10 @@ def __init__(self, *args_, alpha=0.98, lamb=2.0, **kwargs): self.alpha = alpha self.lamb = lamb - def on_train_begin(self, *args_, **kwargs): # pylint: disable=unused-argument + def on_train_begin(self, *args_, **kwargs): self.grads = None - def on_pre_optimizer_step( - self, args_, state, control, **kwargs - ): # pylint: disable=unused-argument + def on_pre_optimizer_step(self, args_, state, control, **kwargs): model = kwargs.pop("model") self.grads = gradfilter_ema(model, self.grads, alpha=self.alpha, lamb=self.lamb) return control diff --git a/src/axolotl/integrations/grokfast/optimizer.py b/src/axolotl/integrations/grokfast/optimizer.py index 38cda2c934..c83ef43bc1 100644 --- a/src/axolotl/integrations/grokfast/optimizer.py +++ b/src/axolotl/integrations/grokfast/optimizer.py @@ -1,7 +1,6 @@ # Copyright: MIT License (c) 2024 Jaerin Lee, Bong Gyun Kang, Kihoon Kim, Kyoung Mu Lee # Reference: https://github.com/ironjr/grokfast -# pylint: skip-file from collections import deque from typing import Dict, Literal, Optional diff --git a/src/axolotl/integrations/kd/__init__.py b/src/axolotl/integrations/kd/__init__.py index 4c8535a0a1..b1a9905533 100644 --- a/src/axolotl/integrations/kd/__init__.py +++ b/src/axolotl/integrations/kd/__init__.py @@ -15,6 +15,7 @@ """ Plugin init to add KD support to Axolotl. """ + from typing import Any from transformers import Trainer @@ -22,7 +23,7 @@ from axolotl.integrations.base import BasePlugin from axolotl.integrations.kd.callbacks import KDTemperatureSchedulerCallback -from .args import KDArgs # pylint: disable=unused-import. # noqa: F401 +from .args import KDArgs as KDArgs class KDPlugin(BasePlugin): diff --git a/src/axolotl/integrations/kd/args.py b/src/axolotl/integrations/kd/args.py index 758bc89178..425d8ddf60 100644 --- a/src/axolotl/integrations/kd/args.py +++ b/src/axolotl/integrations/kd/args.py @@ -15,6 +15,7 @@ """ Plugin args for KD support. """ + from dataclasses import dataclass from enum import Enum @@ -26,8 +27,8 @@ class InferenceServerType(str, Enum): Online inferences server types to handle different request args """ - vllm = "vllm" # pylint: disable=invalid-name - sglang = "sglang" # pylint: disable=invalid-name + vllm = "vllm" + sglang = "sglang" class KDArgs(BaseModel): diff --git a/src/axolotl/integrations/kd/callbacks.py b/src/axolotl/integrations/kd/callbacks.py index 911c3d5176..c73d8a8bbe 100644 --- a/src/axolotl/integrations/kd/callbacks.py +++ b/src/axolotl/integrations/kd/callbacks.py @@ -19,9 +19,7 @@ def __init__(self, temperature_start, temperature_min, trainer): self.trainer = trainer - def on_step_end( - self, args, state, control, **kwargs - ): # pylint: disable=unused-argument + def on_step_end(self, args, state, control, **kwargs): # cosine decay temperature over the max steps progress = state.global_step / state.max_steps diff --git a/src/axolotl/integrations/kd/chat_template.py b/src/axolotl/integrations/kd/chat_template.py index 6376ecb09f..04f0f24a43 100644 --- a/src/axolotl/integrations/kd/chat_template.py +++ b/src/axolotl/integrations/kd/chat_template.py @@ -15,6 +15,7 @@ """ Chat template prompt strategy loader with KD support """ + import logging from typing import Any, Dict @@ -192,7 +193,6 @@ def transform_logprobs(self, sample): """ Transform logprobs to target format for KD training """ - # pylint: disable=duplicate-code logprobs = sample.pop(self.logprobs_field) target_seq_len = len(logprobs) @@ -240,7 +240,7 @@ def transform_logprobs(self, sample): target_mask.append([1] * top_k) for token_pos_logprobs, pos_target_token_ids in zip( - logprobs, sample["target_token_ids"] + logprobs, sample["target_token_ids"], strict=False ): # Convert to a tensor for easier manipulation position_logprobs_tensor = torch.tensor( @@ -299,7 +299,7 @@ class KDStrategyLoader(StrategyLoader): Load ChatTemplateStrategy with KD support using StrategyLoader. """ - def _get_strategy_cls(self, cfg): # pylint: disable=unused-argument + def _get_strategy_cls(self, cfg): return ChatTemplateStrategyWithKD def _get_strategy_params(self, cfg, ds_cfg: Dict[str, Any]): @@ -319,7 +319,7 @@ class KDStrategyLoaderV2(KDStrategyLoader): Load KD chat template datasets with pre-tokenized logprob data """ - def _get_strategy_cls(self, cfg): # pylint: disable=unused-argument + def _get_strategy_cls(self, cfg): return ChatTemplateStrategyWithKDv2 diff --git a/src/axolotl/integrations/kd/collator.py b/src/axolotl/integrations/kd/collator.py index 0cc745b788..675485d9df 100644 --- a/src/axolotl/integrations/kd/collator.py +++ b/src/axolotl/integrations/kd/collator.py @@ -37,7 +37,6 @@ class DataCollatorForKD(DataCollatorForSeq2Seq): target_logprobs. It also creates a teacher_mask to indicate which entries are valid. """ - # pylint: disable=duplicate-code tokenizer: PreTrainedTokenizerBase model: Optional[Any] = None padding: Union[bool, str, PaddingStrategy] = True @@ -72,7 +71,7 @@ def __call__(self, features, return_tensors=None): // self.pad_to_multiple_of ) * self.pad_to_multiple_of - for f in features: # pylint: disable=invalid-name + for f in features: remainder = [pad_token_id] * (max_len - len(f[feature_name])) if isinstance(f[feature_name], list): f[feature_name] = ( @@ -101,7 +100,7 @@ def __call__(self, features, return_tensors=None): if has_teacher_data: # Extract and remove from features - for f in features: # pylint: disable=invalid-name + for f in features: target_logprobs_list.append(f.pop("target_logprobs")) target_token_ids_list.append(f.pop("target_token_ids")) target_mask_list.append(f.pop("target_mask")) @@ -117,24 +116,25 @@ def __call__(self, features, return_tensors=None): padded_teacher_mask_list = [] for t_logprobs, t_ids, t_mask in zip( - target_logprobs_list, target_token_ids_list, target_mask_list + target_logprobs_list, + target_token_ids_list, + target_mask_list, + strict=False, ): t_logprobs_padded = [] t_ids_padded = [] t_mask_padded = [] - for lp, ids, mask in zip( # pylint: disable=invalid-name - t_logprobs, t_ids, t_mask - ): + for lp, ids, mask in zip(t_logprobs, t_ids, t_mask, strict=False): lp_len = len(lp) if lp_len < max_k: # Use -1e9 for padding logprobs and 0 for token_ids pad_len = max_k - lp_len - lp = lp + [-1e9] * pad_len # pylint: disable=invalid-name + lp = lp + [-1e9] * pad_len ids = ids + [0] * pad_len mask = mask + [0] * pad_len else: - lp = lp[:max_k] # pylint: disable=invalid-name + lp = lp[:max_k] ids = ids[:max_k] mask = mask[:max_k] @@ -216,9 +216,7 @@ def __call__(self, features, return_tensors=None): # We want to produce a single "merged" feature dict for each sub-batch. out_features = [{} for _ in features] - for i, sub_features in enumerate( # pylint: disable=too-many-nested-blocks - features - ): + for i, sub_features in enumerate(features): # sub_features is a list of dicts, each dict = one sequence’s features # We'll merge them into out_features[i]. # @@ -255,9 +253,7 @@ def __call__(self, features, return_tensors=None): if field_name in feat and isinstance( feat[field_name], (list, torch.Tensor) ): - if isinstance( - feat[field_name][0], (dict, str) - ): # pylint: disable=too-many-nested-blocks + if isinstance(feat[field_name][0], (dict, str)): continue arr = np.array(feat[field_name]) arrays.append(arr) diff --git a/src/axolotl/integrations/kd/collator_online_teacher.py b/src/axolotl/integrations/kd/collator_online_teacher.py index 584ace481d..54e55a5e7f 100644 --- a/src/axolotl/integrations/kd/collator_online_teacher.py +++ b/src/axolotl/integrations/kd/collator_online_teacher.py @@ -144,7 +144,7 @@ def fetch_online_logprobs_sglang( } for sequence_data, seq_input_ids, seq_labels in zip( - api_data, batch_input_ids, labels + api_data, batch_input_ids, labels, strict=False ): current_target_logprobs = [] current_target_token_ids = [] @@ -165,7 +165,7 @@ def fetch_online_logprobs_sglang( assert len(seq_input_ids) == len(input_top_logprobs) for i, _, label in zip( - range(len(seq_input_ids)), seq_input_ids, seq_labels + range(len(seq_input_ids)), seq_input_ids, seq_labels, strict=False ): if i < len(input_top_logprobs) and input_top_logprobs[i] is None: # this is always the case for the first token. @@ -202,7 +202,8 @@ def fetch_online_logprobs_sglang( # pos_top_logprobs: list of logprobs, pos_token_ids: list of token_ids pos_logprobs_raw, pos_token_ids, _ = [ - list(row) for row in zip(*pos_top_logprobs_data) + list(row) + for row in zip(*pos_top_logprobs_data, strict=False) ] # Ensure correct length (top_k) @@ -317,7 +318,7 @@ def fetch_online_logprobs_vllm( } for sequence_data, seq_input_ids, seq_labels in zip( - choices, batch_input_ids, labels + choices, batch_input_ids, labels, strict=False ): # seq_input_ids: List[int] # seq_labels: List[int] @@ -342,7 +343,9 @@ def fetch_online_logprobs_vllm( seq_len = len(seq_input_ids) - for i, _, label in zip(range(seq_len), seq_input_ids, seq_labels): + for i, _, label in zip( + range(seq_len), seq_input_ids, seq_labels, strict=False + ): if i < len(input_top_logprobs) and input_top_logprobs[i] is None: # this is always the case for the first token. # there is never logprob data for the first token since that's a true input @@ -424,7 +427,7 @@ def fetch_online_logprobs_vllm( list(range(self.kd_online_topk)) ) current_target_mask.append([0] * self.kd_online_topk) - for i in range(max(0, seq_len - len(current_target_logprobs))): + for _ in range(max(0, seq_len - len(current_target_logprobs))): current_target_logprobs.append( [-float("inf")] * self.kd_online_topk ) diff --git a/src/axolotl/integrations/kd/kernels/liger.py b/src/axolotl/integrations/kd/kernels/liger.py index 6356643c22..61ef3e10a0 100644 --- a/src/axolotl/integrations/kd/kernels/liger.py +++ b/src/axolotl/integrations/kd/kernels/liger.py @@ -197,7 +197,7 @@ def forward( compute_ce_loss: bool = True, normalize_topk: bool = True, ): - CHUNK_SIZE = chunk_size # pylint: disable=invalid-name + CHUNK_SIZE = chunk_size grad_weight_acc = torch.zeros_like(student_lm_head_weight) grad_inputs_list = [] grad_bias_acc = ( @@ -298,8 +298,8 @@ def accumulate_chunk_grads( accumulate_chunk_grads_compiled = accumulate_chunk_grads # Use the same chunking logic as LigerFusedLinearDistillationBase.forward - B, N, D = student_input.shape # pylint: disable=invalid-name - K = target_token_ids.shape[-1] # pylint: disable=invalid-name + B, N, D = student_input.shape + K = target_token_ids.shape[-1] student_input_flat = student_input.reshape(-1, student_input.shape[-1]) target_token_ids_flat = target_token_ids.reshape(-1, target_token_ids.shape[-1]) diff --git a/src/axolotl/integrations/kd/kernels/models.py b/src/axolotl/integrations/kd/kernels/models.py index 4319f5f7dd..f7b468669e 100644 --- a/src/axolotl/integrations/kd/kernels/models.py +++ b/src/axolotl/integrations/kd/kernels/models.py @@ -40,10 +40,9 @@ def kldiv_forward_llama_like( output_attentions: Optional[bool] = None, output_hidden_states: Optional[bool] = None, cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, # pylint: disable=unused-argument + logits_to_keep: Union[int, torch.Tensor] = 0, **kwargs: Unpack[TransformersKwargs], # type: ignore[misc] ) -> CausalLMOutputWithPast: - # pylint: disable=duplicate-code output_attentions = ( output_attentions if output_attentions is not None diff --git a/src/axolotl/integrations/kd/topk_logprob/forward_kl.py b/src/axolotl/integrations/kd/topk_logprob/forward_kl.py index 74184455f5..b79ba26f37 100644 --- a/src/axolotl/integrations/kd/topk_logprob/forward_kl.py +++ b/src/axolotl/integrations/kd/topk_logprob/forward_kl.py @@ -15,6 +15,7 @@ """ loss for top_k KL divergence """ + import torch from torch import nn @@ -117,7 +118,6 @@ def forward( target_mask: torch.Tensor, # [B, seq_len, K] num_items_in_batch: int = -1, # optional batch size for normalization ) -> torch.Tensor: - # 1. Split along the "token" dimension (dim=1). student_logits_chunks = student_logits.chunk(self.num_output_chunks, dim=1) token_ids_chunks = target_token_ids.chunk(self.num_output_chunks, dim=1) @@ -131,7 +131,11 @@ def forward( # 2. Loop over each chunk and compute a chunk-specific loss. for st_chunk, tid_chunk, lp_chunk, msk_chunk in zip( - student_logits_chunks, token_ids_chunks, logprobs_chunks, mask_chunks + student_logits_chunks, + token_ids_chunks, + logprobs_chunks, + mask_chunks, + strict=False, ): # We pass num_items_in_batch=-1 so that the kd_loss # will average over *this chunk's* valid tokens only. diff --git a/src/axolotl/integrations/kd/trainer.py b/src/axolotl/integrations/kd/trainer.py index c454b2a2ce..7ec43333a6 100644 --- a/src/axolotl/integrations/kd/trainer.py +++ b/src/axolotl/integrations/kd/trainer.py @@ -21,7 +21,6 @@ from .kernels.liger import LigerFusedLinearKLTopKLogprobLoss -# pylint: disable=too-many-ancestors class AxolotlKDTrainer(AxolotlTrainer): """ Custom trainer subclass for Knowledge Distillation (KD) diff --git a/src/axolotl/integrations/liger/__init__.py b/src/axolotl/integrations/liger/__init__.py index 86d56be802..c20f4545cb 100644 --- a/src/axolotl/integrations/liger/__init__.py +++ b/src/axolotl/integrations/liger/__init__.py @@ -18,6 +18,7 @@ Liger Kernel is the collection of Triton-native kernels for LLM Training. It is designed to be performant, correct, and light-weight. """ + from .args import LigerArgs from .plugin import LigerPlugin diff --git a/src/axolotl/integrations/liger/models/base.py b/src/axolotl/integrations/liger/models/base.py index f3cf4299ad..a9dbe94126 100644 --- a/src/axolotl/integrations/liger/models/base.py +++ b/src/axolotl/integrations/liger/models/base.py @@ -41,7 +41,6 @@ def lce_forward( This is useful when using packed tensor format (single dimension for batch and sequence length). """ - # pylint: disable=duplicate-code output_attentions = ( output_attentions if output_attentions is not None @@ -181,7 +180,7 @@ def patch_lce_forward( model_cls = getattr(module, f"{model_cls_prefix}ForCausalLM") model_cls.forward = lce_forward - # pylint: disable=duplicate-code + except (ImportError, AttributeError) as e: raise RuntimeError( f"Could not import ForCausalLM class for model_type: {model_type}. " diff --git a/src/axolotl/integrations/liger/models/deepseekv2.py b/src/axolotl/integrations/liger/models/deepseekv2.py index 2f0d2a7048..99adce4a79 100644 --- a/src/axolotl/integrations/liger/models/deepseekv2.py +++ b/src/axolotl/integrations/liger/models/deepseekv2.py @@ -2,8 +2,6 @@ DeepseekV2 model with LigerFusedLinearCrossEntropyLoss """ -# pylint: disable=duplicate-code - from typing import List, Optional, Tuple, Union import torch diff --git a/src/axolotl/integrations/liger/models/jamba.py b/src/axolotl/integrations/liger/models/jamba.py index d255299707..78689e40c6 100644 --- a/src/axolotl/integrations/liger/models/jamba.py +++ b/src/axolotl/integrations/liger/models/jamba.py @@ -2,8 +2,6 @@ Jamba model with LigerFusedLinearCrossEntropyLoss """ -# pylint: disable=duplicate-code - from typing import Optional, Tuple, Union import torch diff --git a/src/axolotl/integrations/liger/models/llama4.py b/src/axolotl/integrations/liger/models/llama4.py index 689823bb67..e51140265b 100644 --- a/src/axolotl/integrations/liger/models/llama4.py +++ b/src/axolotl/integrations/liger/models/llama4.py @@ -46,7 +46,6 @@ def lce_forward( Returns: """ - # pylint: disable=duplicate-code output_attentions = ( output_attentions if output_attentions is not None @@ -78,9 +77,7 @@ def lce_forward( hidden_states = outputs[0] if hasattr(self.config, "pretraining_tp") and self.config.pretraining_tp > 1: - raise Exception( # pylint: disable=broad-exception-raised - "Liger Kernel does not support pretraining_tp!!" - ) + raise Exception("Liger Kernel does not support pretraining_tp!!") logits = None loss = None @@ -128,7 +125,7 @@ def apply_liger_kernel_to_llama4( rms_norm: bool = False, glu_activation: bool = False, layer_norm: bool = False, - **kwargs, # pylint: disable=unused-argument + **kwargs, ) -> None: """ Apply Liger kernels to replace original implementation in HuggingFace Llama models (2 and 3) @@ -144,15 +141,15 @@ def apply_liger_kernel_to_llama4( layer_norm (bool): Whether to apply Liger's LayerNorm. Default is False. """ - import transformers.models.llama4.modeling_llama4 # noqa: F401 # pylint: disable=unused-import + import transformers.models.llama4.modeling_llama4 # noqa: F401 from liger_kernel.transformers.functional import liger_cross_entropy from liger_kernel.transformers.layer_norm import LigerLayerNorm from liger_kernel.transformers.rms_norm import LigerRMSNorm from liger_kernel.transformers.swiglu import LigerSwiGLUMLP - assert not ( - cross_entropy and fused_linear_cross_entropy - ), "cross_entropy and fused_linear_cross_entropy cannot both be True." + assert not (cross_entropy and fused_linear_cross_entropy), ( + "cross_entropy and fused_linear_cross_entropy cannot both be True." + ) modeling_llama4 = sys.modules["transformers.models.llama4.modeling_llama4"] @@ -165,7 +162,7 @@ def _liger_swiglu_mlp_wrapper(config, intermediate_size=None, **kwargs): # clone config to avoid modifying the original config = deepcopy(config) if intermediate_size: - setattr(config, "intermediate_size", intermediate_size) + config.intermediate_size = intermediate_size return LigerSwiGLUMLP(config, **kwargs) modeling_llama4.Llama4TextMLP = _liger_swiglu_mlp_wrapper diff --git a/src/axolotl/integrations/liger/models/qwen3.py b/src/axolotl/integrations/liger/models/qwen3.py index 1dc19eaf96..b008755da3 100644 --- a/src/axolotl/integrations/liger/models/qwen3.py +++ b/src/axolotl/integrations/liger/models/qwen3.py @@ -43,7 +43,6 @@ def lce_forward( Returns: """ - # pylint: disable=duplicate-code output_attentions = ( output_attentions if output_attentions is not None @@ -113,9 +112,8 @@ def apply_liger_kernel_to_qwen3( rms_norm: bool = False, glu_activation: bool = False, layer_norm: bool = False, - **kwargs, # pylint: disable=unused-argument + **kwargs, ) -> None: - # pylint: disable=duplicate-code """ Apply Liger kernels to replace original implementation in HuggingFace Llama models (2 and 3) @@ -130,15 +128,15 @@ def apply_liger_kernel_to_qwen3( layer_norm (bool): Whether to apply Liger's LayerNorm. Default is False. """ - import transformers.models.qwen3.modeling_qwen3 # noqa: F401 # pylint: disable=unused-import + import transformers.models.qwen3.modeling_qwen3 # noqa: F401 from liger_kernel.transformers.functional import liger_cross_entropy from liger_kernel.transformers.layer_norm import LigerLayerNorm from liger_kernel.transformers.rms_norm import LigerRMSNorm from liger_kernel.transformers.swiglu import LigerSwiGLUMLP - assert not ( - cross_entropy and fused_linear_cross_entropy - ), "cross_entropy and fused_linear_cross_entropy cannot both be True." + assert not (cross_entropy and fused_linear_cross_entropy), ( + "cross_entropy and fused_linear_cross_entropy cannot both be True." + ) modeling_qwen3 = sys.modules["transformers.models.qwen3.modeling_qwen3"] diff --git a/src/axolotl/integrations/liger/models/qwen3_moe.py b/src/axolotl/integrations/liger/models/qwen3_moe.py index 89bdc5bcc1..40bee110c4 100644 --- a/src/axolotl/integrations/liger/models/qwen3_moe.py +++ b/src/axolotl/integrations/liger/models/qwen3_moe.py @@ -45,7 +45,6 @@ def lce_forward( Returns: """ - # pylint: disable=duplicate-code output_attentions = ( output_attentions if output_attentions is not None @@ -135,9 +134,8 @@ def apply_liger_kernel_to_qwen3_moe( rms_norm: bool = False, glu_activation: bool = False, layer_norm: bool = False, - **kwargs, # pylint: disable=unused-argument + **kwargs, ) -> None: - # pylint: disable=duplicate-code """ Apply Liger kernels to replace original implementation in HuggingFace Llama models (2 and 3) @@ -152,15 +150,15 @@ def apply_liger_kernel_to_qwen3_moe( layer_norm (bool): Whether to apply Liger's LayerNorm. Default is False. """ - import transformers.models.qwen3_moe.modeling_qwen3_moe # noqa: F401 # pylint: disable=unused-import + import transformers.models.qwen3_moe.modeling_qwen3_moe # noqa: F401 from liger_kernel.transformers.functional import liger_cross_entropy from liger_kernel.transformers.layer_norm import LigerLayerNorm from liger_kernel.transformers.rms_norm import LigerRMSNorm from liger_kernel.transformers.swiglu import LigerSwiGLUMLP - assert not ( - cross_entropy and fused_linear_cross_entropy - ), "cross_entropy and fused_linear_cross_entropy cannot both be True." + assert not (cross_entropy and fused_linear_cross_entropy), ( + "cross_entropy and fused_linear_cross_entropy cannot both be True." + ) modeling_qwen3_moe = sys.modules["transformers.models.qwen3_moe.modeling_qwen3_moe"] @@ -174,7 +172,7 @@ def _liger_swiglu_mlp_wrapper(config, intermediate_size=None, **kwargs): # clone config to avoid modifying the original config = deepcopy(config) if intermediate_size: - setattr(config, "intermediate_size", intermediate_size) + config.intermediate_size = intermediate_size return LigerSwiGLUMLP(config, **kwargs) modeling_qwen3_moe.Qwen3MoeMLP = _liger_swiglu_mlp_wrapper diff --git a/src/axolotl/integrations/lm_eval/__init__.py b/src/axolotl/integrations/lm_eval/__init__.py index 8db4dc6346..0ab6b8697e 100644 --- a/src/axolotl/integrations/lm_eval/__init__.py +++ b/src/axolotl/integrations/lm_eval/__init__.py @@ -7,7 +7,7 @@ from axolotl.integrations.base import BasePlugin from axolotl.integrations.lm_eval.cli import build_lm_eval_command -from .args import LMEvalArgs # pylint: disable=unused-import. # noqa: F401 +from .args import LMEvalArgs as LMEvalArgs class LMEvalPlugin(BasePlugin): @@ -20,7 +20,6 @@ def get_input_args(self): def post_train_unload(self, cfg): if cfg.lm_eval_post_train: - # pylint: disable=duplicate-code for lm_eval_args in build_lm_eval_command( cfg.lm_eval_tasks, bfloat16=cfg.bfloat16 or cfg.bf16, diff --git a/src/axolotl/integrations/lm_eval/cli.py b/src/axolotl/integrations/lm_eval/cli.py index 19608e1d95..ead82dcb79 100644 --- a/src/axolotl/integrations/lm_eval/cli.py +++ b/src/axolotl/integrations/lm_eval/cli.py @@ -99,7 +99,6 @@ def lm_eval(config: str, cloud: Optional[str] = None): with open(config, encoding="utf-8") as file: cfg: DictDefault = DictDefault(yaml.safe_load(file)) - # pylint: disable=duplicate-code for lm_eval_args in build_lm_eval_command( cfg.lm_eval_tasks, bfloat16=cfg.bfloat16 or cfg.bf16, diff --git a/src/axolotl/integrations/spectrum/__init__.py b/src/axolotl/integrations/spectrum/__init__.py index 9f66aef97f..5e8f9128d1 100644 --- a/src/axolotl/integrations/spectrum/__init__.py +++ b/src/axolotl/integrations/spectrum/__init__.py @@ -23,7 +23,7 @@ from axolotl.integrations.base import BasePlugin from axolotl.utils.logging import get_logger -from .args import SpectrumArgs # pylint: disable=unused-import. # noqa: F401 +from .args import SpectrumArgs as SpectrumArgs LOG = get_logger(__name__) @@ -46,7 +46,7 @@ def _generate_unfrozen_params_yaml(snr_data, top_fraction=0.5): "^lm_head.weight$", "^model.embed_tokens.weight$", ] - for layer_type, layer_names in top_layers_by_type.items(): + for _, layer_names in top_layers_by_type.items(): for layer_name in layer_names: unfrozen_parameters.append(layer_name) return unfrozen_parameters @@ -84,7 +84,7 @@ def pre_model_load(self, cfg): snr_data = json.load(fin) except FileNotFoundError: pass - except Exception as exc: # pylint: disable=broad-exception-caught + except Exception as exc: LOG.warning(f"Failed to read SNR data from {snr_path}: {exc}") if not snr_data: diff --git a/src/axolotl/integrations/spectrum/args.py b/src/axolotl/integrations/spectrum/args.py index df57560380..be6ca4bfc2 100644 --- a/src/axolotl/integrations/spectrum/args.py +++ b/src/axolotl/integrations/spectrum/args.py @@ -15,6 +15,7 @@ """ Module for handling Spectrum input arguments. """ + from typing import Optional from pydantic import BaseModel, model_validator diff --git a/src/axolotl/kernels/geglu.py b/src/axolotl/kernels/geglu.py index 6acbea0d47..ee3260ebd3 100644 --- a/src/axolotl/kernels/geglu.py +++ b/src/axolotl/kernels/geglu.py @@ -5,8 +5,6 @@ Credit to `unsloth` (https://unsloth.ai/) for inspiration for this implementation. """ -# pylint: disable=invalid-name,unnecessary-lambda-assignment,duplicate-code - import torch import triton import triton.language as tl diff --git a/src/axolotl/kernels/lora.py b/src/axolotl/kernels/lora.py index fb45f2aa7e..c3356fb909 100644 --- a/src/axolotl/kernels/lora.py +++ b/src/axolotl/kernels/lora.py @@ -7,8 +7,6 @@ Credit to `unsloth` (https://unsloth.ai/) for inspiration for this implementation. """ -# pylint: disable=invalid-name - from typing import Callable import torch diff --git a/src/axolotl/kernels/quantize.py b/src/axolotl/kernels/quantize.py index b61603fbcf..d094f2381c 100644 --- a/src/axolotl/kernels/quantize.py +++ b/src/axolotl/kernels/quantize.py @@ -1,7 +1,5 @@ """Dequantization utilities for `bitsandbytes` integration.""" -# pylint: disable=invalid-name,global-statement - import ctypes import bitsandbytes as bnb diff --git a/src/axolotl/kernels/swiglu.py b/src/axolotl/kernels/swiglu.py index 43a798edcc..b13bcd3503 100644 --- a/src/axolotl/kernels/swiglu.py +++ b/src/axolotl/kernels/swiglu.py @@ -99,7 +99,6 @@ def _swiglu_bwd_kernel( tl.store(up_ptr + offsets, grad_up, mask=mask) # grad wrt up -# pylint: disable=unnecessary-lambda-assignment def swiglu_forward(gate: torch.Tensor, up: torch.Tensor) -> torch.Tensor: """ SwiGLU forward pass. Computes SwiGLU activation: `x * sigmoid(x) * up`, where @@ -128,7 +127,6 @@ def swiglu_forward(gate: torch.Tensor, up: torch.Tensor) -> torch.Tensor: return out -# pylint: disable=unnecessary-lambda-assignment def swiglu_backward( grad_output: torch.Tensor, gate: torch.Tensor, up: torch.Tensor ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: diff --git a/src/axolotl/loaders/__init__.py b/src/axolotl/loaders/__init__.py index 3eef75e58b..ae99bf16d9 100644 --- a/src/axolotl/loaders/__init__.py +++ b/src/axolotl/loaders/__init__.py @@ -1,6 +1,5 @@ """Init for axolotl.loaders module""" -# pylint: disable=unused-import # flake8: noqa from .adapter import load_adapter, load_lora diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index db28206b6c..867e6901cb 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -28,14 +28,12 @@ def setup_quantized_meta_for_peft(model: torch.nn.Module): """Replaces `quant_state.to` with a dummy function to prevent PEFT from moving `quant_state` to meta device""" - def temp_to_method(self, *args, **kwargs): # pylint: disable=unused-argument + def temp_to_method(self, *args, **kwargs): return self for param in model.parameters(): if isinstance(param, Params4bit): - param.quant_state._orig_to = ( # pylint: disable=protected-access - param.quant_state.to - ) + param.quant_state._orig_to = param.quant_state.to param.quant_state.to = types.MethodType(temp_to_method, param.quant_state) @@ -43,10 +41,8 @@ def setup_quantized_peft_meta_for_training(model: torch.nn.Module): """Replaces dummy `quant_state.to` method with the original function to allow training to continue""" for param in model.parameters(): if isinstance(param, Params4bit) and hasattr(param.quant_state, "_orig_to"): - param.quant_state.to = ( - param.quant_state._orig_to # pylint: disable=protected-access - ) - param.quant_state._orig_to = None # pylint: disable=protected-access + param.quant_state.to = param.quant_state._orig_to + param.quant_state._orig_to = None def find_all_linear_names(model): diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 53ae428a23..a9507d685d 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -102,7 +102,7 @@ def __init__( *, inference: bool = False, reference_model: bool = False, - **kwargs, # pylint: disable=unused-argument + **kwargs, ): """Initializes the ModelLoader. @@ -134,7 +134,7 @@ def __init__( # Init model config self.model_config = load_model_config(cfg) - self.auto_model_loader = AutoModelForCausalLM # pylint: disable=invalid-name + self.auto_model_loader = AutoModelForCausalLM # Initialize the patch manager self.patch_manager = PatchManager( @@ -607,27 +607,19 @@ def _set_attention_config(self): self.model_kwargs["attn_implementation"] = self.cfg.attn_implementation elif self.cfg.flex_attention: self.model_kwargs["attn_implementation"] = "flex_attention" - self.model_config._attn_implementation = ( # pylint: disable=protected-access - "flex_attention" - ) + self.model_config._attn_implementation = "flex_attention" elif self.cfg.flash_attention: if not self.cfg.sample_packing and self.cfg.s2_attention: pass self.model_kwargs["attn_implementation"] = "flash_attention_2" - self.model_config._attn_implementation = ( # pylint: disable=protected-access - "flash_attention_2" - ) + self.model_config._attn_implementation = "flash_attention_2" elif self.cfg.sdp_attention: self.model_kwargs["attn_implementation"] = "sdpa" - self.model_config._attn_implementation = ( # pylint: disable=protected-access - "sdpa" - ) + self.model_config._attn_implementation = "sdpa" elif self.cfg.eager_attention: self.model_kwargs["attn_implementation"] = "eager" - self.model_config._attn_implementation = ( # pylint: disable=protected-access - "eager" - ) + self.model_config._attn_implementation = "eager" if self.cfg.low_cpu_mem_usage: self.model_kwargs["low_cpu_mem_usage"] = True @@ -767,7 +759,7 @@ def _build_model(self) -> bool: ) elif self.model_type == "MambaLMHeadModel": # FIXME this is janky at best and hacked together to make it work - MambaLMHeadModel = fix_mamba_attn_for_loss() # pylint: disable=invalid-name + MambaLMHeadModel = fix_mamba_attn_for_loss() self.model_kwargs["dtype"] = self.model_kwargs["torch_dtype"] self.model_kwargs["device"] = torch.cuda.current_device() @@ -816,7 +808,6 @@ def _build_model(self) -> bool: if is_deepspeed_zero3_enabled(): skip_move_to_device = True - # pylint: disable=protected-access if self.cfg.tensor_parallel_size > 1: # workaround for upstream 4.54.0 not setting _tp_size or _device_mesh # TODO(wing): remove once 4.54.1 is released diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index 0a486d0234..dcc255938a 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -50,7 +50,7 @@ def modify_tokenizer_files( tokenizer_dir = os.path.join(output_dir, "tokenizer") os.makedirs(tokenizer_dir, exist_ok=True) - if is_local_main_process(): # pylint: disable=too-many-nested-blocks + if is_local_main_process(): # Load the tokenizer temp_tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, use_fast=True) @@ -73,9 +73,9 @@ def modify_tokenizer_files( for token_id, new_value in token_id_mappings.items(): token_id_str = str(token_id) if token_id_str in config_data["added_tokens_decoder"]: - config_data["added_tokens_decoder"][token_id_str][ - "content" - ] = new_value + config_data["added_tokens_decoder"][token_id_str]["content"] = ( + new_value + ) else: raise ValueError( f"Token ID {token_id_str} not found in added_tokens_decoder" @@ -215,7 +215,7 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): for k, val in special_tokens.items(): # check if new special token is not already in tokenizer and # is adapter training to make sure lora_modules_to_save is set - # pylint: disable=too-many-boolean-expressions + if ( (getattr(tokenizer, k) is None or getattr(tokenizer, k) != val) and (len(tokenizer.encode(val, add_special_tokens=False)) > 2) diff --git a/src/axolotl/models/mamba/__init__.py b/src/axolotl/models/mamba/__init__.py index fee88e3a43..d6bb40d99a 100644 --- a/src/axolotl/models/mamba/__init__.py +++ b/src/axolotl/models/mamba/__init__.py @@ -21,4 +21,4 @@ def fix_mamba_attn_for_loss(): from .modeling_mamba import MambaLMHeadModel as MambaLMHeadModelFixed mixer_seq_simple.MambaLMHeadModel = MambaLMHeadModelFixed - return mixer_seq_simple.MambaLMHeadModel # pylint: disable=invalid-name + return mixer_seq_simple.MambaLMHeadModel diff --git a/src/axolotl/models/mamba/modeling_mamba.py b/src/axolotl/models/mamba/modeling_mamba.py index 70e9c88c88..2cfe115444 100644 --- a/src/axolotl/models/mamba/modeling_mamba.py +++ b/src/axolotl/models/mamba/modeling_mamba.py @@ -1,4 +1,3 @@ -# pylint: skip-file import os from collections import namedtuple from functools import partial @@ -112,7 +111,7 @@ def save_pretrained( self, save_directory: Union[str, os.PathLike], state_dict: Optional[dict] = None, - safe_serialization: Optional[bool] = None, # pylint: disable=unused-argument + safe_serialization: Optional[bool] = None, ): if state_dict is None: state_dict = self.state_dict() diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index 66d3d0d2de..3b38a33b70 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -130,9 +130,9 @@ def get_state_dict(self, model, unwrap=True): "Deepspeed TP requires deepspeed >= 0.16.4, Please update DeepSpeed via `pip install deepspeed -U`." ) state_dict = ( - model._consolidated_16bit_state_dict() # pylint: disable=protected-access + model._consolidated_16bit_state_dict() if tp_sharding - else model._zero3_consolidated_16bit_state_dict() # pylint: disable=protected-access + else model._zero3_consolidated_16bit_state_dict() ) else: raise ValueError( @@ -231,8 +231,7 @@ def fsdp2_prepare_model(accelerator, model: torch.nn.Module) -> torch.nn.Module: ) is_type_fsdp = isinstance(model, FSDPModule) or ( - is_compiled_module(model) - and isinstance(model._orig_mod, FSDPModule) # pylint: disable=protected-access + is_compiled_module(model) and isinstance(model._orig_mod, FSDPModule) ) if is_type_fsdp: return model diff --git a/src/axolotl/monkeypatch/accelerate/parallelism_config.py b/src/axolotl/monkeypatch/accelerate/parallelism_config.py index e3cafc87d6..b2157fb6b6 100644 --- a/src/axolotl/monkeypatch/accelerate/parallelism_config.py +++ b/src/axolotl/monkeypatch/accelerate/parallelism_config.py @@ -2,7 +2,6 @@ workaround to allow parallelism config for pure CP """ -# pylint: disable=protected-access import os import warnings @@ -30,7 +29,7 @@ def _validate_accelerator(self, accelerator): allow_parallelism_config = False if ( - self.cp_size > 1 # pylint: disable=chained-comparison + self.cp_size > 1 and self.dp_shard_size <= 1 and os.environ.get("ACCELERATE_ALLOW_CP_STANDALONE", "false").lower() == "true" ): @@ -55,6 +54,7 @@ def _validate_accelerator(self, accelerator): warnings.warn( "ParallelismConfig has the following warnings:\n" + "\n".join(_warnings), UserWarning, + stacklevel=2, ) diff --git a/src/axolotl/monkeypatch/attention/flex_attn.py b/src/axolotl/monkeypatch/attention/flex_attn.py index 98aead8324..f59b8abe28 100644 --- a/src/axolotl/monkeypatch/attention/flex_attn.py +++ b/src/axolotl/monkeypatch/attention/flex_attn.py @@ -65,11 +65,9 @@ def __call__(self): return self._compiled_flex_attention transformers.integrations.flex_attention.WrappedFlexAttention = WrappedFlexAttention - setattr( - sys.modules["transformers.integrations.flex_attention"], - "WrappedFlexAttention", - WrappedFlexAttention, - ) + sys.modules[ + "transformers.integrations.flex_attention" + ].WrappedFlexAttention = WrappedFlexAttention def patch_flex_make_mask(): @@ -144,9 +142,7 @@ def patched_make_flex_block_causal_mask( # computation prior to the softmax. For sample packing, we need both the # logic for both causal mask and document mask. See PyTorch's official # blog post for more details: https://pytorch.org/blog/flexattention/#mask-mods - def causal_mask_mod( - batch_idx, head_idx, q_idx, kv_idx - ): # pylint: disable=unused-argument + def causal_mask_mod(batch_idx, head_idx, q_idx, kv_idx): """ Defines the logic of a block causal mask by combining both a standard causal mask and a block diagonal document mask. @@ -198,14 +194,12 @@ def mask_mod(batch_idx, head_idx, q_idx, kv_idx): for n in tuple(sys.modules): if ".modeling_" in n: if hasattr(sys.modules[n], "make_flex_block_causal_mask"): - sys.modules[n].make_flex_block_causal_mask = ( - patched_make_flex_block_causal_mask - ) - setattr( - sys.modules[n], - "make_flex_block_causal_mask", - patched_make_flex_block_causal_mask, - ) + sys.modules[ + n + ].make_flex_block_causal_mask = patched_make_flex_block_causal_mask + sys.modules[ + n + ].make_flex_block_causal_mask = patched_make_flex_block_causal_mask transformers.integrations.flex_attention.make_flex_block_causal_mask = ( patched_make_flex_block_causal_mask diff --git a/src/axolotl/monkeypatch/attention/xformers.py b/src/axolotl/monkeypatch/attention/xformers.py index 5901963f05..eca95797a9 100644 --- a/src/axolotl/monkeypatch/attention/xformers.py +++ b/src/axolotl/monkeypatch/attention/xformers.py @@ -23,15 +23,15 @@ def xformers_attention_forward( value: torch.Tensor, attention_mask: Optional[torch.Tensor] = None, position_ids: Optional[torch.LongTensor] = None, - dropout: float = 0.0, # pylint: disable=unused-argument - scaling: Optional[float] = None, # pylint: disable=unused-argument - sliding_window: Optional[int] = None, # pylint: disable=unused-argument - softcap: Optional[float] = None, # pylint: disable=unused-argument + dropout: float = 0.0, + scaling: Optional[float] = None, + sliding_window: Optional[int] = None, + softcap: Optional[float] = None, cu_seq_lens_q: Optional[torch.LongTensor] = None, cu_seq_lens_k: Optional[torch.LongTensor] = None, max_length_q: Optional[int] = None, - max_length_k: Optional[int] = None, # pylint: disable=unused-argument - **kwargs, # pylint: disable=unused-argument + max_length_k: Optional[int] = None, + **kwargs, ): # Get dimensions # query: [batch, heads, seq_len, hidden_dim] diff --git a/src/axolotl/monkeypatch/btlm_attn_hijack_flash.py b/src/axolotl/monkeypatch/btlm_attn_hijack_flash.py index 589980c8b9..2c50773929 100644 --- a/src/axolotl/monkeypatch/btlm_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/btlm_attn_hijack_flash.py @@ -25,9 +25,7 @@ def replace_btlm_attn_with_flash_attn(model_name="cerebras/btlm-3b-8k-base"): ".configuration_btlm", ".modeling_btlm" ) modeling_btlm = importlib.import_module(module_name) - modeling_btlm.BTLMAttention._attn = ( # pylint: disable=protected-access - flashattn_attn - ) + modeling_btlm.BTLMAttention._attn = flashattn_attn def flashattn_attn( @@ -35,9 +33,9 @@ def flashattn_attn( query: torch.Tensor, key: Optional[torch.Tensor] = None, value: Optional[torch.Tensor] = None, - attention_mask: Optional[torch.Tensor] = None, # pylint: disable=unused-argument + attention_mask: Optional[torch.Tensor] = None, head_mask: Optional[torch.Tensor] = None, - position_bias: Optional[torch.Tensor] = None, # pylint: disable=unused-argument + position_bias: Optional[torch.Tensor] = None, ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]: softmax_scale = ( 1 / (key.size(-1) ** self.attn_scale_power) if self.scale_attn_weights else None diff --git a/src/axolotl/monkeypatch/data/batch_dataset_fetcher.py b/src/axolotl/monkeypatch/data/batch_dataset_fetcher.py index 73bf37b614..c426344a66 100644 --- a/src/axolotl/monkeypatch/data/batch_dataset_fetcher.py +++ b/src/axolotl/monkeypatch/data/batch_dataset_fetcher.py @@ -1,7 +1,5 @@ """Monkey patches for the dataset fetcher to handle batches of packed indexes.""" -# pylint: disable=protected-access - import torch from torch.utils.data._utils.fetch import _BaseDatasetFetcher from torch.utils.data._utils.worker import _worker_loop diff --git a/src/axolotl/monkeypatch/fsdp2_qlora.py b/src/axolotl/monkeypatch/fsdp2_qlora.py index 5a4332fffa..04d0d19710 100644 --- a/src/axolotl/monkeypatch/fsdp2_qlora.py +++ b/src/axolotl/monkeypatch/fsdp2_qlora.py @@ -15,7 +15,6 @@ LOG = get_logger(__name__) -# pylint: disable=protected-access def apply_init_sharded_param_patch(): """Apply patch to FSDPParam._init_sharded_param to support Params4bit.""" from torch.distributed.fsdp._fully_shard._fsdp_param import FSDPParam @@ -66,14 +65,14 @@ def apply_init_sharded_param_patch(): if item in patched_source: items_to_import.append(item) - exec( # pylint: disable=exec-used # nosec B102 + exec( # nosec B102 f"from {module_name} import ({', '.join(items_to_import)})", globals(), ) - exec(patched_source, globals()) # pylint: disable=exec-used # nosec B102 + exec(patched_source, globals()) # nosec B102 # Replace the method - FSDPParam._init_sharded_param = patched_init_sharded_param # pylint: disable=undefined-variable # noqa: F821 + FSDPParam._init_sharded_param = patched_init_sharded_param LOG.info("Successfully applied FSDP _init_sharded_param patch") else: LOG.warning("Could not find target code for _init_sharded_param patching") @@ -131,14 +130,14 @@ def apply_init_unsharded_param_patch(): if item in patched_source: items_to_import.append(item) - exec( # pylint: disable=exec-used # nosec B102 + exec( # nosec B102 f"from {module_name} import ({', '.join(items_to_import)})", globals(), ) - exec(patched_source, globals()) # pylint: disable=exec-used # nosec B102 + exec(patched_source, globals()) # nosec B102 # Replace the method - FSDPParam.init_unsharded_param = patched_init_unsharded_param # pylint: disable=undefined-variable # noqa: F821 + FSDPParam.init_unsharded_param = patched_init_unsharded_param LOG.info("Successfully applied FSDP init_unsharded_param patch") else: LOG.warning("Could not find target code for patching") diff --git a/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py b/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py index 3b090d5e56..b58bbb67c8 100644 --- a/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py +++ b/src/axolotl/monkeypatch/gradient_checkpointing/__init__.py @@ -25,9 +25,7 @@ def uses_gc_layers(_): return False -def hf_grad_checkpoint_offload_wrapper( - decoder_layer, *args, use_reentrant=None -): # pylint: disable=unused-argument +def hf_grad_checkpoint_offload_wrapper(decoder_layer, *args, use_reentrant=None): if uses_gc_layers(decoder_layer): return CPU_Offloaded_Gradient_Checkpointer.apply( decoder_layer, @@ -44,9 +42,7 @@ def hf_grad_checkpoint_offload_wrapper( ) -def hf_grad_checkpoint_disk_offload_wrapper( - decoder_layer, *args, use_reentrant=None -): # pylint: disable=unused-argument +def hf_grad_checkpoint_disk_offload_wrapper(decoder_layer, *args, use_reentrant=None): if uses_gc_layers(decoder_layer): return Disco.apply( decoder_layer, diff --git a/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py b/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py index bbcfb91e69..8d06f172db 100644 --- a/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py +++ b/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py @@ -35,9 +35,7 @@ torch_cuda_amp_custom_bwd = torch.amp.custom_bwd(device_type="cuda") -class CPU_Offloaded_Gradient_Checkpointer( # pylint: disable=invalid-name - torch.autograd.Function -): +class CPU_Offloaded_Gradient_Checkpointer(torch.autograd.Function): """ Saves VRAM by smartly offloading to RAM. Tiny hit to performance, since we mask the movement via non blocking calls. @@ -66,6 +64,4 @@ def backward(ctx, dY): return ( None, hidden_states.grad, - ) + ( - None, - ) * len(ctx.args) + ) + (None,) * len(ctx.args) diff --git a/src/axolotl/monkeypatch/gradient_checkpointing/offload_disk.py b/src/axolotl/monkeypatch/gradient_checkpointing/offload_disk.py index 792d3c6efc..220799fbf8 100644 --- a/src/axolotl/monkeypatch/gradient_checkpointing/offload_disk.py +++ b/src/axolotl/monkeypatch/gradient_checkpointing/offload_disk.py @@ -62,9 +62,9 @@ def __init__( # Track tensor paths and their status self.tensor_paths: deque = deque() # Ordered history of tensor paths (LIFO) - self.file_locks: Dict[str, threading.Lock] = ( - {} - ) # Maps file_path -> threading.Lock() + self.file_locks: Dict[ + str, threading.Lock + ] = {} # Maps file_path -> threading.Lock() # Maps file_path -> status ("saving", "ready", "prefetching", "loaded", "deleted") self.file_status: Dict[str, str] = {} @@ -236,7 +236,7 @@ def save_tensor(self, tensor: torch.Tensor): self.tensor_paths.append(file_path) # Acquire semaphore to limit concurrent save operations - self.save_semaphore.acquire() # pylint: disable=consider-using-with + self.save_semaphore.acquire() # Queue tensor for saving in background self.save_queue.put((tensor.detach(), file_path)) diff --git a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py index 1316b53742..3953cb138f 100644 --- a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py @@ -2,6 +2,7 @@ # copied from https://github.com/lm-sys/FastChat/blob/main/fastchat/train/llama_flash_attn_monkey_patch.py +import importlib.util import warnings from typing import Optional, Tuple @@ -19,7 +20,7 @@ from axolotl.utils.logging import get_logger try: - from flash_attn.flash_attn_interface import ( # pylint: disable=ungrouped-imports + from flash_attn.flash_attn_interface import ( flash_attn_varlen_qkvpacked_func, ) except ImportError: @@ -32,12 +33,7 @@ def is_xformers_available() -> bool: - try: - import xformers # pylint: disable=unused-import # noqa: F401 - - return True - except ImportError: - return False + return importlib.util.find_spec("xformers") is not None def is_xformers_swiglu_available() -> bool: @@ -83,7 +79,7 @@ def fa2_fixed_cross_entropy( num_items_in_batch: int = None, ignore_index: int = -100, **kwargs, - ): # pylint: disable=unused-argument + ): reduction = "sum" if num_items_in_batch is not None else "mean" loss, _ = flash_attn_cross_entropy_loss( source, target, ignore_index=ignore_index @@ -120,9 +116,7 @@ def replace_llama_attn_with_flash_attn( rms_norm: Optional[bool] = False, use_shifted_sparse_attn: Optional[bool] = False, ): - transformers.models.llama.modeling_llama.LlamaModel._prepare_decoder_attention_mask = ( # pylint: disable=protected-access - _prepare_decoder_attention_mask - ) + transformers.models.llama.modeling_llama.LlamaModel._prepare_decoder_attention_mask = _prepare_decoder_attention_mask if use_shifted_sparse_attn: transformers.models.llama.modeling_llama.LlamaAttention.forward = ( flashattn_forward_with_s2attn @@ -145,7 +139,7 @@ def _prepare_decoder_attention_mask( input_shape, inputs_embeds, past_key_values_length, -): # pylint: disable=unused-argument +): # [bsz, seq_len] return attention_mask @@ -161,9 +155,9 @@ def flashattn_forward_with_s2attn( past_key_value: Optional[Tuple[torch.Tensor]] = None, output_attentions: bool = False, use_cache: bool = False, - padding_mask: Optional[torch.LongTensor] = None, # pylint: disable=unused-argument - cu_seqlens: Optional[torch.Tensor] = None, # pylint: disable=unused-argument - max_seqlen: Optional[torch.Tensor] = None, # pylint: disable=unused-argument + padding_mask: Optional[torch.LongTensor] = None, + cu_seqlens: Optional[torch.Tensor] = None, + max_seqlen: Optional[torch.Tensor] = None, ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: """Input shape: Batch x Time x Channel @@ -176,7 +170,8 @@ def flashattn_forward_with_s2attn( """ if output_attentions: warnings.warn( - "Output attentions is not supported for patched `LlamaAttention`, returning `None` instead." + "Output attentions is not supported for patched `LlamaAttention`, returning `None` instead.", + stacklevel=2, ) bsz, q_len, _ = hidden_states.size() @@ -198,7 +193,6 @@ def flashattn_forward_with_s2attn( ) # [bsz, q_len, nh, hd] # [bsz, nh, q_len, hd] - # pylint: disable=duplicate-code cos, sin = self.rotary_emb(value_states, position_ids=position_ids) query_states, key_states = apply_rotary_pos_emb( @@ -244,9 +238,7 @@ def flashattn_forward_with_s2attn( .permute(0, 3, 1, 2, 4, 5) .reshape(bsz * 2, q_len, 3, self.num_heads // 2, self.head_dim) ) - x = rearrange( # pylint: disable=invalid-name - qkv, "b s three h d -> b s (three h d)" - ) + x = rearrange(qkv, "b s three h d -> b s (three h d)") x_unpad, indices, cu_q_lens, max_s = unpad_input(x, key_padding_mask) cu_q_len_tmp = torch.arange( 0, max_s, group_size, device=key_padding_mask.device, dtype=cu_q_lens.dtype diff --git a/src/axolotl/monkeypatch/llama_attn_hijack_xformers.py b/src/axolotl/monkeypatch/llama_attn_hijack_xformers.py index 28223eee36..332242e2c4 100644 --- a/src/axolotl/monkeypatch/llama_attn_hijack_xformers.py +++ b/src/axolotl/monkeypatch/llama_attn_hijack_xformers.py @@ -32,10 +32,9 @@ def xformers_forward( past_key_value: Optional[Tuple[torch.Tensor]] = None, output_attentions: bool = False, use_cache: bool = False, - padding_mask: Optional[torch.LongTensor] = None, # pylint: disable=unused-argument - **kwargs, # pylint: disable=unused-argument + padding_mask: Optional[torch.LongTensor] = None, + **kwargs, ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: - # pylint: disable=duplicate-code bsz, q_len, _ = hidden_states.size() if not hasattr(self, "pretraining_tp"): @@ -102,7 +101,8 @@ def xformers_forward( if output_attentions: warnings.warn( - "Output attentions is not supported for patched `LlamaAttention`, returning `None` instead." + "Output attentions is not supported for patched `LlamaAttention`, returning `None` instead.", + stacklevel=2, ) # diff --git a/src/axolotl/monkeypatch/llama_expand_mask.py b/src/axolotl/monkeypatch/llama_expand_mask.py index 0277c212a4..5cfb7818e3 100644 --- a/src/axolotl/monkeypatch/llama_expand_mask.py +++ b/src/axolotl/monkeypatch/llama_expand_mask.py @@ -21,6 +21,4 @@ def _expand_mask(mask: torch.Tensor, dtype: torch.dtype, tgt_len: Optional[int] def hijack_expand_mask(): import transformers - transformers.models.llama.modeling_llama._expand_mask = ( # pylint: disable=protected-access - _expand_mask - ) + transformers.models.llama.modeling_llama._expand_mask = _expand_mask diff --git a/src/axolotl/monkeypatch/llama_patch_multipack.py b/src/axolotl/monkeypatch/llama_patch_multipack.py index cfd525367e..8d234881fd 100644 --- a/src/axolotl/monkeypatch/llama_patch_multipack.py +++ b/src/axolotl/monkeypatch/llama_patch_multipack.py @@ -12,15 +12,15 @@ def hijack_llama_prepare_4d_mask(): from transformers import modeling_attn_mask_utils from transformers.models.llama import modeling_llama - modeling_llama._prepare_4d_causal_attention_mask_for_sdpa = ( # pylint: disable=protected-access + modeling_llama._prepare_4d_causal_attention_mask_for_sdpa = ( patched_prepare_4d_causal_attention_mask_for_sdpa ) - modeling_attn_mask_utils._prepare_4d_causal_attention_mask_for_sdpa = ( # pylint: disable=protected-access + modeling_attn_mask_utils._prepare_4d_causal_attention_mask_for_sdpa = ( patched_prepare_4d_causal_attention_mask_for_sdpa ) - modeling_llama._prepare_4d_causal_attention_mask = ( # pylint: disable=protected-access + modeling_llama._prepare_4d_causal_attention_mask = ( patched_prepare_4d_causal_attention_mask ) - modeling_attn_mask_utils._prepare_4d_causal_attention_mask = ( # pylint: disable=protected-access + modeling_attn_mask_utils._prepare_4d_causal_attention_mask = ( patched_prepare_4d_causal_attention_mask ) diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index be1e1f2ffb..ef5174ba29 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -30,48 +30,36 @@ query_states = self.q_proj(hidden_states).view(hidden_shape).transpose(1, 2) key_states = self.k_proj(hidden_states).view(hidden_shape).transpose(1, 2) value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) -""".lstrip( - "\n" - ), +""".lstrip("\n"), """ query_states, key_states, value_states = self.apply_qkv(hidden_states) query_states = query_states.view(hidden_shape).transpose(1, 2) key_states = key_states.view(hidden_shape).transpose(1, 2) value_states = value_states.view(hidden_shape).transpose(1, 2) -""".lstrip( - "\n" - ), +""".lstrip("\n"), ), ( """ query_states = self.q_norm(self.q_proj(hidden_states).view(hidden_shape)).transpose(1, 2) key_states = self.k_norm(self.k_proj(hidden_states).view(hidden_shape)).transpose(1, 2) value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) -""".lstrip( - "\n" - ), +""".lstrip("\n"), """ query_states, key_states, value_states = self.apply_qkv(hidden_states) query_states = self.q_norm(query_states.view(hidden_shape)).transpose(1, 2) key_states = self.k_norm(key_states.view(hidden_shape)).transpose(1, 2) value_states = value_states.view(hidden_shape).transpose(1, 2) -""".lstrip( - "\n" - ), +""".lstrip("\n"), ), ] ORIGINAL_O_CODE = """ attn_output = self.o_proj(attn_output) -""".lstrip( - "\n" -) +""".lstrip("\n") PATCHED_O_CODE = """ attn_output = self.apply_o(attn_output) -""".lstrip( - "\n" -) +""".lstrip("\n") SUPPORTED_ACTIVATIONS = ["silu", "gelu"] APPLY_FN_MAPPING = { @@ -176,7 +164,6 @@ def get_attention_cls_from_config(cfg: DictDefault) -> Type[nn.Module]: ) from e -# pylint: disable=protected-access def patch_self_attn_lora(cfg: DictDefault): """ Given an `axolotl` config, this method patches the inferred attention class forward @@ -203,9 +190,9 @@ def patch_self_attn_lora(cfg: DictDefault): attention_cls._original_forward = self_attn_forward self_attn_forward, _ = detab_code(self_attn_forward) - assert any( - qkv_options[0] in self_attn_forward for qkv_options in QKV_PATCHES - ), "Original QKV code not found" + assert any(qkv_options[0] in self_attn_forward for qkv_options in QKV_PATCHES), ( + "Original QKV code not found" + ) assert ORIGINAL_O_CODE in self_attn_forward, "Original O code not found" for qkv_orig, qkv_patched in QKV_PATCHES: @@ -231,16 +218,14 @@ def patch_self_attn_lora(cfg: DictDefault): if item in self_attn_forward: items_to_import.append(item) - exec( # pylint: disable=exec-used # nosec B102 + exec( f"from {module_name} import ({', '.join(items_to_import)})", globals(), ) - exec(self_attn_forward, globals()) # pylint: disable=exec-used # nosec B102 + exec(self_attn_forward, globals()) LOG.info(f"Patched attention class with LoRA optims: {attention_cls.__name__}") - attention_cls.forward = ( - axolotl_attn_forward # pylint: disable=undefined-variable # noqa: F821 - ) + attention_cls.forward = axolotl_attn_forward def find_self_attn_in_layer( @@ -277,9 +262,13 @@ def find_mlp_in_layer( layer.feedforward.experts.gate_projs, layer.feedforward.experts.up_projs, layer.feedforward.experts.down_projs, + strict=False, ): - yield gate_proj, up_proj, down_proj, FakeMLP( - gate_proj, up_proj, down_proj + yield ( + gate_proj, + up_proj, + down_proj, + FakeMLP(gate_proj, up_proj, down_proj), ) @@ -337,9 +326,9 @@ def apply_lora_kernel_patches( # Get active LoRA adapter config if hasattr(model, "active_adapters"): - assert ( - len(model.active_adapters) == 1 - ), "Axolotl currently does not support LoRA Triton kernels for multiple adapters" + assert len(model.active_adapters) == 1, ( + "Axolotl currently does not support LoRA Triton kernels for multiple adapters" + ) active_adapter = model.active_adapters[0] else: active_adapter = model.active_adapter diff --git a/src/axolotl/monkeypatch/loss/chunked.py b/src/axolotl/monkeypatch/loss/chunked.py index 0a9d0de82c..26a52f8981 100644 --- a/src/axolotl/monkeypatch/loss/chunked.py +++ b/src/axolotl/monkeypatch/loss/chunked.py @@ -25,7 +25,7 @@ def compute_cross_entropy( self, logits: torch.Tensor, labels: torch.Tensor, - normalize: bool = True, # pylint: disable=unused-argument + normalize: bool = True, ) -> torch.Tensor: """ Upcast logits to fp32 and compute cross entropy loss. @@ -63,7 +63,7 @@ def forward( # compute one chunk at a time total_loss = 0.0 - for logits_chunk, labels_chunk in zip(logits, labels): + for logits_chunk, labels_chunk in zip(logits, labels, strict=False): total_loss += self.compute_cross_entropy(logits_chunk, labels_chunk) if reduction == "sum": @@ -88,9 +88,9 @@ def chunked_fix_cross_entropy( num_items_in_batch: int = None, ignore_index: int = -100, **kwargs, - ): # pylint: disable=unused-argument + ): reduction = "sum" if num_items_in_batch is not None else "mean" - logit_chunks = [ # pylint: disable=unnecessary-comprehension + logit_chunks = [ chunk for chunk in source.chunk(loss_fn_ce.num_output_chunks, dim=1) ] loss = loss_fn_ce(logit_chunks, target, reduction=reduction) @@ -101,7 +101,7 @@ def chunked_fix_cross_entropy( def for_causal_lm_chunked_loss( logits, labels, - vocab_size: int = None, # pylint: disable=unused-argument + vocab_size: int = None, num_items_in_batch: Optional[int] = None, ignore_index: int = -100, shift_labels: Optional[torch.Tensor] = None, diff --git a/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py b/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py index e1be424a3e..0994da91ca 100644 --- a/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/mistral_attn_hijack_flash.py @@ -1,7 +1,5 @@ """Flash attention monkey patch for mistral model""" -# pylint: disable=duplicate-code - from functools import partial import transformers diff --git a/src/axolotl/monkeypatch/mixtral/__init__.py b/src/axolotl/monkeypatch/mixtral/__init__.py index 5b8054000f..b353b12cf2 100644 --- a/src/axolotl/monkeypatch/mixtral/__init__.py +++ b/src/axolotl/monkeypatch/mixtral/__init__.py @@ -31,14 +31,12 @@ def moe_forward(self, hidden_states: torch.Tensor) -> torch.Tensor: topk_weight = topk_weight.to(hidden_states.dtype) hidden_states = hidden_states.repeat_interleave(self.top_k, dim=0) - y = torch.empty_like(hidden_states) # pylint: disable=invalid-name + y = torch.empty_like(hidden_states) flat_topk_idx = topk_idx.view(-1) for i in range(self.num_experts): expert = self.experts[i] y[flat_topk_idx == i] = expert(hidden_states[flat_topk_idx == i]) - y = ( # pylint: disable=invalid-name - y.view(*topk_weight.shape, -1) * topk_weight.unsqueeze(-1) - ).sum(dim=1) + y = (y.view(*topk_weight.shape, -1) * topk_weight.unsqueeze(-1)).sum(dim=1) final_hidden_states = y.reshape(batch_size, sequence_length, hidden_dim) return final_hidden_states, router_logits diff --git a/src/axolotl/monkeypatch/models/llama4/modeling.py b/src/axolotl/monkeypatch/models/llama4/modeling.py index 4127793e75..0fc8f5699c 100644 --- a/src/axolotl/monkeypatch/models/llama4/modeling.py +++ b/src/axolotl/monkeypatch/models/llama4/modeling.py @@ -95,18 +95,12 @@ def patch_llama4_linearized_modeling(): old_lamma_4_text_experts = modeling_llama4.Llama4TextExperts modeling_llama4.Llama4TextExperts = Llama4TextExperts - setattr( - sys.modules["transformers.models.llama4"], - "Llama4TextExperts", - Llama4TextExperts, - ) + sys.modules["transformers.models.llama4"].Llama4TextExperts = Llama4TextExperts def unpatch(): modeling_llama4.Llama4TextExperts = old_lamma_4_text_experts - setattr( - sys.modules["transformers.models.llama4"], - "Llama4TextExperts", - old_lamma_4_text_experts, - ) + sys.modules[ + "transformers.models.llama4" + ].Llama4TextExperts = old_lamma_4_text_experts return unpatch diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 7df9877d78..e4f9ca2be6 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -49,9 +49,7 @@ def patch_for_multipack(model_type, model_name=None, has_remote_code=False): assert hasattr( transformers.modeling_flash_attention_utils, "_get_unpad_data" ), "transformers api changed for _get_unpad_data for flash attention" - transformers.modeling_flash_attention_utils._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) + transformers.modeling_flash_attention_utils._get_unpad_data = get_unpad_data if model_type == "mixtral" and is_deepspeed_zero3_enabled(): patch_mixtral_moe_forward_zero3() @@ -67,6 +65,4 @@ def patch_remote(model_name): module_name = ".".join(parts) modeling_arch = importlib.import_module(module_name) if hasattr(modeling_arch, "_get_unpad_data"): - modeling_arch._get_unpad_data = ( # pylint: disable=protected-access - get_unpad_data - ) + modeling_arch._get_unpad_data = get_unpad_data diff --git a/src/axolotl/monkeypatch/peft/utils.py b/src/axolotl/monkeypatch/peft/utils.py index 0c571fbd2f..d1011f5eb4 100644 --- a/src/axolotl/monkeypatch/peft/utils.py +++ b/src/axolotl/monkeypatch/peft/utils.py @@ -49,9 +49,7 @@ def patch_peft_prep_code(): prep_code = get_peft_prep_code() except OSError: return - peft.utils.other._original_create_accelerator_and_postprocess = ( # pylint: disable=protected-access - prep_code - ) + peft.utils.other._original_create_accelerator_and_postprocess = prep_code prep_code, _ = detab_code(prep_code) if ORIGINAL_PREPARE_CODE not in prep_code: return @@ -68,11 +66,15 @@ def patch_peft_prep_code(): if item in prep_code: items_to_import.append(item) - exec( # pylint: disable=exec-used # nosec B102 + exec( "from peft.utils.other import (" + ", ".join(x for x in items_to_import) + ")", globals(), ) - exec(prep_code, globals()) # pylint: disable=exec-used # nosec B102 + exec(prep_code, globals()) LOG.info("patching prepare_model_for_kbit_training to allow for overrides") - peft.utils.other.prepare_model_for_kbit_training = fixed_prepare_model_for_kbit_training # pylint: disable=protected-access # pylint: disable=undefined-variable # noqa: F821 - axolotl.loaders.model.prepare_model_for_kbit_training = fixed_prepare_model_for_kbit_training # pylint: disable=protected-access # pylint: disable=undefined-variable # noqa: F821 + peft.utils.other.prepare_model_for_kbit_training = ( + fixed_prepare_model_for_kbit_training + ) + axolotl.loaders.model.prepare_model_for_kbit_training = ( + fixed_prepare_model_for_kbit_training + ) diff --git a/src/axolotl/monkeypatch/relora.py b/src/axolotl/monkeypatch/relora.py index 0028a0cf6e..a01d850b31 100644 --- a/src/axolotl/monkeypatch/relora.py +++ b/src/axolotl/monkeypatch/relora.py @@ -91,9 +91,9 @@ def __init__(self, cfg: DictDefault): if not os.path.exists(self.last_full_model): self.last_full_model = str(Path(snapshot_download(cfg.base_model))) - assert os.path.exists( - self.last_full_model - ), "for ReLORA base_model must be a local path" + assert os.path.exists(self.last_full_model), ( + "for ReLORA base_model must be a local path" + ) self.num_lora_restarts = 0 self.need_full_save = False @@ -293,7 +293,6 @@ def find_lora_modules(model: peft.LoraModel) -> Dict[str, peft.tuners.lora.LoraL key_list = [key for key, _ in model.model.named_modules() if "lora" not in key] for key in key_list: try: - # pylint: disable=protected-access _parent, target, _target_name = peft.utils._get_submodules(model.model, key) except AttributeError: continue @@ -341,7 +340,7 @@ def merge_and_save( modules = find_lora_modules(model) if not quantized: - for module_name, target in modules.items(): + for _, target in modules.items(): active_adapter = target.active_adapter if isinstance(active_adapter, list): active_adapter = active_adapter[0] diff --git a/src/axolotl/monkeypatch/ring_attn/__init__.py b/src/axolotl/monkeypatch/ring_attn/__init__.py index 736378b162..1c14776c9b 100644 --- a/src/axolotl/monkeypatch/ring_attn/__init__.py +++ b/src/axolotl/monkeypatch/ring_attn/__init__.py @@ -1,6 +1,5 @@ """Init for ring attention monkeypatch module""" -# pylint: disable=unused-import # flake8: noqa from .patch import ( diff --git a/src/axolotl/monkeypatch/ring_attn/adapters/batch.py b/src/axolotl/monkeypatch/ring_attn/adapters/batch.py index 607b4dd71e..74d33ed4ab 100644 --- a/src/axolotl/monkeypatch/ring_attn/adapters/batch.py +++ b/src/axolotl/monkeypatch/ring_attn/adapters/batch.py @@ -7,8 +7,6 @@ somewhat to support only the latest versions of transformers. """ -# pylint: disable=protected-access,cyclic-import - import os from typing import Callable @@ -20,7 +18,7 @@ from ring_flash_attn.adapters.hf_adapter import check_params from transformers.modeling_flash_attention_utils import is_flash_attn_greater_or_equal -try: # pylint: disable=duplicate-code +try: from transformers.modeling_flash_attention_utils import _flash_supports_window except ImportError: try: @@ -59,7 +57,7 @@ def create_flash_attn_forward_varlen_llama3( """ # transformers 4.48+ - # pylint: disable=unused-argument + def _flash_attention_forward( query_states: torch.Tensor, key_states: torch.Tensor, diff --git a/src/axolotl/monkeypatch/ring_attn/patch.py b/src/axolotl/monkeypatch/ring_attn/patch.py index ea0f9dd026..e1fd10b3ac 100644 --- a/src/axolotl/monkeypatch/ring_attn/patch.py +++ b/src/axolotl/monkeypatch/ring_attn/patch.py @@ -15,7 +15,7 @@ import torch.distributed as dist from torch.distributed import DeviceMesh -try: # pylint: disable=duplicate-code +try: from transformers.modeling_flash_attention_utils import _flash_supports_window except ImportError: try: @@ -43,7 +43,7 @@ def get_ring_attn_group() -> dist.ProcessGroup: def set_ring_attn_group(ring_attn_group: dist.ProcessGroup | None): """Setter for ring attention group on this rank.""" - global RING_ATTN_GROUP # pylint: disable=global-statement + global RING_ATTN_GROUP RING_ATTN_GROUP = ring_attn_group @@ -57,29 +57,24 @@ def _flash_attention_forward_v3( query_states: torch.Tensor, key_states: torch.Tensor, value_states: torch.Tensor, - attention_mask: torch.Tensor, # pylint: disable=unused-argument + attention_mask: torch.Tensor, query_length: int, is_causal: bool, dropout: float = 0.0, - position_ids: Optional[torch.Tensor] = None, # pylint: disable=unused-argument + position_ids: Optional[torch.Tensor] = None, softmax_scale: Optional[float] = None, sliding_window: Optional[int] = None, use_top_left_mask: bool = False, softcap: Optional[float] = None, deterministic: bool = None, - cu_seq_lens_q: Optional[ - torch.LongTensor - ] = None, # pylint: disable=unused-argument - cu_seq_lens_k: Optional[ - torch.LongTensor - ] = None, # pylint: disable=unused-argument - max_length_q: Optional[int] = None, # pylint: disable=unused-argument - max_length_k: Optional[int] = None, # pylint: disable=unused-argument - target_dtype: Optional[torch.dtype] = None, # pylint: disable=unused-argument - attn_implementation: Optional[str] = None, # pylint: disable=unused-argument - **kwargs, # pylint: disable=unused-argument + cu_seq_lens_q: Optional[torch.LongTensor] = None, + cu_seq_lens_k: Optional[torch.LongTensor] = None, + max_length_q: Optional[int] = None, + max_length_k: Optional[int] = None, + target_dtype: Optional[torch.dtype] = None, + attn_implementation: Optional[str] = None, + **kwargs, ): - # pylint: disable=duplicate-code if not use_top_left_mask: causal = is_causal else: @@ -101,9 +96,9 @@ def _flash_attention_forward_v3( if deterministic is None: deterministic = os.environ.get("FLASH_ATTENTION_DETERMINISTIC", "0") == "1" flash_kwargs["deterministic"] = deterministic - assert ( - softcap is None - ), "llama3_flash_attn_varlen_func does not support softcap yet." + assert softcap is None, ( + "llama3_flash_attn_varlen_func does not support softcap yet." + ) # flash_kwargs["softcap"] = softcap flash_kwargs["group"] = process_group @@ -193,7 +188,7 @@ def register_ring_attn_from_device_mesh( # fmt: off import ring_flash_attn.adapters.hf_adapter - from ring_flash_attn.adapters.hf_adapter import ( # isort: skip # pylint: disable=unused-import + from ring_flash_attn.adapters.hf_adapter import ( # isort: skip create_ring_flash_attention_forward as create_ring_flash_attention_forward_orig, ) diff --git a/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py b/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py index 85454fe2e3..0fa6d6424a 100644 --- a/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/stablelm_attn_hijack_flash.py @@ -16,8 +16,8 @@ # This code is based off the following work: # https://github.com/huggingface/transformers/blob/main/src/transformers/models/llama/modeling_llama.py # https://github.com/huggingface/transformers/blob/main/src/transformers/models/gpt_neox/modeling_gpt_neox.py -# pylint: disable=duplicate-code """PyTorch StableLM Epoch model.""" + import importlib import math from typing import Optional, Tuple, Union @@ -26,7 +26,7 @@ import torch.utils.checkpoint from accelerate import init_empty_weights from einops import rearrange -from flash_attn.flash_attn_interface import ( # pylint: disable=ungrouped-imports +from flash_attn.flash_attn_interface import ( flash_attn_varlen_qkvpacked_func, ) from torch import nn @@ -49,27 +49,21 @@ def replace_stablelm_attn_with_flash_attn(model_name="stabilityai/stablelm-3b-4e ".configuration_stablelm_epoch", ".modeling_stablelm_epoch" ) modeling_stablelm = importlib.import_module(module_name) - modeling_stablelm.Attention.forward = ( # pylint: disable=protected-access - flashattn_attn - ) - modeling_stablelm.StableLMEpochModel.forward = ( # pylint: disable=protected-access - stablelm_model_forward - ) - modeling_stablelm.DecoderLayer.forward = ( # pylint: disable=protected-access - decoder_layer_forward - ) + modeling_stablelm.Attention.forward = flashattn_attn + modeling_stablelm.StableLMEpochModel.forward = stablelm_model_forward + modeling_stablelm.DecoderLayer.forward = decoder_layer_forward def rotate_half(x: torch.Tensor): """Rotates half the hidden dims of the input.""" - # pylint: disable=invalid-name + x1, x2 = torch.chunk(x, 2, dim=-1) return torch.cat((-x2, x1), dim=-1) def apply_rotary_pos_emb(q, k, cos, sin, position_ids): # The first two dimensions of cos and sin are always 1, so we can `squeeze` them. - # pylint: disable=invalid-name + cos = cos.squeeze(1).squeeze(0) # [seq_len, dim] sin = sin.squeeze(1).squeeze(0) # [seq_len, dim] cos = cos[position_ids].unsqueeze(1) # [batch_size, 1, seq_len, dim] @@ -99,7 +93,7 @@ def flashattn_attn( attention_mask: torch.FloatTensor, position_ids: torch.LongTensor, past_key_value: Optional[Tuple[torch.Tensor]] = None, - output_attentions: Optional[bool] = False, # pylint: disable=unused-argument + output_attentions: Optional[bool] = False, use_cache: Optional[bool] = False, cu_seqlens: Optional[torch.Tensor] = None, max_seqlen: Optional[torch.Tensor] = None, @@ -216,7 +210,6 @@ def decoder_layer_forward( ) -> Union[ Tuple[torch.Tensor], Optional[Tuple[torch.Tensor, Tuple[torch.FloatTensor, ...]]] ]: - # pylint: disable=duplicate-code residual = hidden_states hidden_states = self.input_layernorm(hidden_states) @@ -263,7 +256,6 @@ def stablelm_model_forward( output_hidden_states: Optional[bool] = None, return_dict: Optional[bool] = None, ) -> Union[Tuple, BaseModelOutputWithPast]: - # pylint: disable=duplicate-code output_attentions = ( output_attentions if output_attentions is not None @@ -326,13 +318,11 @@ def stablelm_model_forward( dtype=torch.bool, device=inputs_embeds.device, ) - attention_mask = ( - self._prepare_decoder_attention_mask( # pylint: disable=protected-access - attention_mask, - (batch_size, seq_length), - inputs_embeds, - past_key_values_length, - ) + attention_mask = self._prepare_decoder_attention_mask( + attention_mask, + (batch_size, seq_length), + inputs_embeds, + past_key_values_length, ) hidden_states = inputs_embeds diff --git a/src/axolotl/monkeypatch/tiled_mlp/patch.py b/src/axolotl/monkeypatch/tiled_mlp/patch.py index 419c73104f..7cdc6d3a38 100644 --- a/src/axolotl/monkeypatch/tiled_mlp/patch.py +++ b/src/axolotl/monkeypatch/tiled_mlp/patch.py @@ -40,7 +40,6 @@ def generic_mlp_forward(self_, hs): is_distributed = int(os.environ.get("WORLD_SIZE", 1)) > 1 def tiled_mlp_forward(self, x): - # pylint: disable=protected-access input_shape = x.shape seqlen = input_shape[-2] hidden = input_shape[-1] @@ -79,14 +78,13 @@ def tiled_mlp_forward(self, x): return down_res mlp_cls.forward = tiled_mlp_forward - mlp_cls._compute_params = [] # pylint: disable=protected-access - mlp_cls._tiled_mlp_dist_impl = None # pylint: disable=protected-access + mlp_cls._compute_params = [] + mlp_cls._tiled_mlp_dist_impl = None LOG.info( f"Successfully monkey-patched TiledMLP for model_type: {model_type}", main_process_only=True, ) except (ImportError, AttributeError) as e: raise RuntimeError( - f"Could not import MLP class for model_type: {model_type}. " - f"Error: {str(e)}" + f"Could not import MLP class for model_type: {model_type}. Error: {str(e)}" ) from e diff --git a/src/axolotl/monkeypatch/trainer/lr.py b/src/axolotl/monkeypatch/trainer/lr.py index 9afc23c466..c33674ceeb 100644 --- a/src/axolotl/monkeypatch/trainer/lr.py +++ b/src/axolotl/monkeypatch/trainer/lr.py @@ -39,4 +39,4 @@ def _get_learning_rate(self): def patch_trainer_get_lr(): from transformers.trainer import Trainer - Trainer._get_learning_rate = _get_learning_rate # pylint: disable=protected-access + Trainer._get_learning_rate = _get_learning_rate diff --git a/src/axolotl/monkeypatch/trainer_accelerator_args.py b/src/axolotl/monkeypatch/trainer_accelerator_args.py index 819a66255c..9fc6e38c68 100644 --- a/src/axolotl/monkeypatch/trainer_accelerator_args.py +++ b/src/axolotl/monkeypatch/trainer_accelerator_args.py @@ -47,9 +47,7 @@ def patch_create_accelerate_code_for_fp8(enable_fsdp_float8_all_gather: bool): create_code = get_create_accelerate_code() except OSError: return - Trainer._original_create_accelerator_and_postprocess = ( # pylint: disable=protected-access - create_code - ) + Trainer._original_create_accelerator_and_postprocess = create_code create_code, _ = detab_code(create_code) if ORIGINAL_TRAINER_CODE not in create_code: return @@ -72,12 +70,14 @@ def patch_create_accelerate_code_for_fp8(enable_fsdp_float8_all_gather: bool): if item in create_code: items_to_import.append(item) - exec( # pylint: disable=exec-used # nosec B102 + exec( "from transformers.trainer import (" + ", ".join(x for x in items_to_import) + ")", globals(), ) - exec(create_code, globals()) # pylint: disable=exec-used # nosec B102 + exec(create_code, globals()) LOG.info("patching create_accelerator_and_postprocess to allow for overrides") - Trainer.create_accelerator_and_postprocess = fixed_create_accelerator_and_postprocess # pylint: disable=protected-access # pylint: disable=undefined-variable # noqa: F821 + Trainer.create_accelerator_and_postprocess = ( + fixed_create_accelerator_and_postprocess + ) diff --git a/src/axolotl/monkeypatch/trainer_fsdp_optim.py b/src/axolotl/monkeypatch/trainer_fsdp_optim.py index 1c2511524d..692f754d7c 100644 --- a/src/axolotl/monkeypatch/trainer_fsdp_optim.py +++ b/src/axolotl/monkeypatch/trainer_fsdp_optim.py @@ -23,9 +23,7 @@ def get_training_loop_code() -> str: - training_loop = inspect.getsource( - Trainer._inner_training_loop # pylint: disable=protected-access - ) + training_loop = inspect.getsource(Trainer._inner_training_loop) return training_loop @@ -44,9 +42,7 @@ def patch_training_loop_for_fsdp(): training_loop = get_training_loop_code() except OSError: return - Trainer._original_inner_training_loop = ( # pylint: disable=protected-access - training_loop - ) + Trainer._original_inner_training_loop = training_loop training_loop, _ = detab_code(training_loop) if ORIGINAL_TRAINER_CODE not in training_loop: return @@ -66,14 +62,12 @@ def patch_training_loop_for_fsdp(): if item in training_loop: items_to_import.append(item) - exec( # pylint: disable=exec-used # nosec B102 + exec( "from transformers.trainer import (" + ", ".join(x for x in items_to_import) + ")", globals(), ) - exec(training_loop, globals()) # pylint: disable=exec-used # nosec B102 + exec(training_loop, globals()) LOG.info("patching _inner_training_loop for fsdp optimizer save") - Trainer._inner_training_loop = ( # pylint: disable=protected-access - _fixed_inner_training_loop # pylint: disable=undefined-variable # noqa: F821 - ) + Trainer._inner_training_loop = _fixed_inner_training_loop diff --git a/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py b/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py index 75f4158b3b..012c699fa0 100644 --- a/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py +++ b/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py @@ -52,7 +52,6 @@ def check_evaluation_loop_is_fsdp2_patchable() -> bool: return ORIGINAL_FSDP2_CODE in evaluation_loop_source -# pylint: disable=protected-access def patch_evaluation_loop(patch_fsdp2: bool): """Patch the evaluation_loop method.""" # Check if already patched @@ -101,16 +100,14 @@ def patch_evaluation_loop(patch_fsdp2: bool): items_to_import.append(item) # Execute the imports and patched method - exec( # pylint: disable=exec-used # nosec B102 + exec( f"from {module_name} import ({', '.join(items_to_import)})", globals(), ) - exec(evaluation_loop_source, globals()) # pylint: disable=exec-used # nosec B102 + exec(evaluation_loop_source, globals()) LOG.info("Patched Trainer.evaluation_loop with nanmean loss calculation") - Trainer.evaluation_loop = ( - axolotl_evaluation_loop # pylint: disable=undefined-variable # noqa: F821 - ) + Trainer.evaluation_loop = axolotl_evaluation_loop def check_maybe_log_save_evaluate_is_patchable() -> bool: @@ -118,7 +115,6 @@ def check_maybe_log_save_evaluate_is_patchable() -> bool: return ORIGINAL_MAYBE_CODE in maybe_log_source -# pylint: disable=protected-access def patch_maybe_log_save_evaluate(): """Patch the _maybe_log_save_evaluate method.""" # Check if already patched @@ -155,11 +151,11 @@ def patch_maybe_log_save_evaluate(): items_to_import.append(item) # Execute the imports and patched method - exec( # pylint: disable=exec-used # nosec B102 + exec( f"from {module_name} import ({', '.join(items_to_import)})", globals(), ) - exec(maybe_log_source, globals()) # pylint: disable=exec-used # nosec B102 + exec(maybe_log_source, globals()) LOG.info("Patched Trainer._maybe_log_save_evaluate with nanmean loss calculation") - Trainer._maybe_log_save_evaluate = axolotl_maybe_log_save_evaluate # pylint: disable=undefined-variable # noqa: F821 + Trainer._maybe_log_save_evaluate = axolotl_maybe_log_save_evaluate diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py index 146047e959..59f32c6f5c 100644 --- a/src/axolotl/monkeypatch/unsloth_.py +++ b/src/axolotl/monkeypatch/unsloth_.py @@ -17,27 +17,19 @@ query_states = self.q_proj(hidden_states) key_states = self.k_proj(hidden_states) value_states = self.v_proj(hidden_states) -""".lstrip( - "\n" -) +""".lstrip("\n") PATCHED_QKV_CODE = """ query_states, key_states, value_states = self.apply_qkv(self, hidden_states) -""".lstrip( - "\n" -) +""".lstrip("\n") ORIGINAL_O_CODE = """ attn_output = self.o_proj(attn_output) -""".lstrip( - "\n" -) +""".lstrip("\n") PATCHED_O_CODE = """ attn_output = self.apply_o(self, attn_output) -""".lstrip( - "\n" -) +""".lstrip("\n") def original_apply_qkv(self, hidden_states): @@ -66,13 +58,13 @@ def check_self_attn_is_patchable() -> bool: def integrate_cross_entropy_loss_patch(model_type: str = "llama") -> None: from unsloth.kernels.cross_entropy_loss import fast_cross_entropy_loss - def UnslothForCausalLMLoss( # pylint: disable=invalid-name + def UnslothForCausalLMLoss( logits, labels, - vocab_size: int, # pylint: disable=unused-argument + vocab_size: int, num_items_in_batch: int = None, - ignore_index: int = -100, # pylint: disable=unused-argument - **kwargs, # pylint: disable=unused-argument + ignore_index: int = -100, + **kwargs, ): # Upcast to float if we need to compute the loss to avoid potential precision issues logits = logits.float() @@ -93,18 +85,16 @@ def UnslothForCausalLMLoss( # pylint: disable=invalid-name raise ValueError("Unsupported model type") -self_attn_lora_patched = False # pylint: disable=invalid-name +self_attn_lora_patched = False def patch_self_attn_lora(): - global self_attn_lora_patched # pylint: disable=global-statement + global self_attn_lora_patched if self_attn_lora_patched: # prevent patching multiple times return self_attn_forward = get_self_attn_code() - LlamaFlashAttention2._original_forward = ( # pylint: disable=protected-access - self_attn_forward - ) + LlamaFlashAttention2._original_forward = self_attn_forward self_attn_forward, _ = detab_code(self_attn_forward) assert ORIGINAL_QKV_CODE in self_attn_forward, "Original qkv code not found" assert ORIGINAL_O_CODE in self_attn_forward, "Original o code not found" @@ -125,27 +115,25 @@ def patch_self_attn_lora(): if item in self_attn_forward: items_to_import.append(item) - exec( # pylint: disable=exec-used # nosec B102 + exec( "from transformers.models.llama.modeling_llama import (" + ", ".join(x for x in items_to_import) + ")", globals(), ) - exec(self_attn_forward, globals()) # pylint: disable=exec-used # nosec B102 + exec(self_attn_forward, globals()) self_attn_lora_patched = True LOG.info("patching unsloth attn lora") - LlamaFlashAttention2.forward = ( - unsloth_attn_forward # pylint: disable=undefined-variable # noqa: F821 - ) + LlamaFlashAttention2.forward = unsloth_attn_forward def integrate_rope_embeddings(): import transformers.models.llama.modeling_llama from unsloth.kernels.rope_embedding import fast_rope_embedding - def apply_rotary_pos_emb( # pylint: disable=unused-argument - q, # pylint: disable=invalid-name - k, # pylint: disable=invalid-name + def apply_rotary_pos_emb( + q, + k, cos, sin, position_ids=None, diff --git a/src/axolotl/monkeypatch/xformers_/__init__.py b/src/axolotl/monkeypatch/xformers_/__init__.py index a052ea49e8..6f5b43f773 100644 --- a/src/axolotl/monkeypatch/xformers_/__init__.py +++ b/src/axolotl/monkeypatch/xformers_/__init__.py @@ -36,7 +36,7 @@ def __init__( self.swiglu.w3.weight.data = down_proj.weight.data def _post_training(self, model, name): - w1, w2 = torch.split( # pylint: disable=invalid-name + w1, w2 = torch.split( self.swiglu.w12.weight.data, self.config.intermediate_size, dim=0 ) @@ -48,5 +48,5 @@ def _post_training(self, model, name): set_module_name(model, name, new_mlp) - def forward(self, x: torch.Tensor) -> torch.Tensor: # pylint: disable=invalid-name + def forward(self, x: torch.Tensor) -> torch.Tensor: return self.swiglu(x) diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 31597d5a62..4b06eb4c8d 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -156,9 +156,9 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: image_value = load_image(image_value) if self.image_size is not None: - assert hasattr( - image_value, "resize" - ), "Image does not have a resize method" + assert hasattr(image_value, "resize"), ( + "Image does not have a resize method" + ) if isinstance(self.image_size, tuple): image_value = image_value.resize( diff --git a/src/axolotl/prompt_strategies/__init__.py b/src/axolotl/prompt_strategies/__init__.py index cf936481e9..d9936b9aef 100644 --- a/src/axolotl/prompt_strategies/__init__.py +++ b/src/axolotl/prompt_strategies/__init__.py @@ -48,6 +48,6 @@ def load(strategy, tokenizer, cfg, ds_cfg, processor=None): return func(tokenizer, cfg, **load_kwargs) except ModuleNotFoundError: return None - except Exception as exc: # pylint: disable=broad-exception-caught + except Exception as exc: LOG.error(f"Failed to load prompt strategy `{strategy}`: {str(exc)}") raise exc diff --git a/src/axolotl/prompt_strategies/alpaca_chat.py b/src/axolotl/prompt_strategies/alpaca_chat.py index 975fee889e..391ba6072b 100644 --- a/src/axolotl/prompt_strategies/alpaca_chat.py +++ b/src/axolotl/prompt_strategies/alpaca_chat.py @@ -39,7 +39,7 @@ class AlpacaChatPrompter(AlpacaPrompter): system_prompt = "Below is an instruction from a USER that describes a task, paired with an input that provides further context. The ASSISTANT writes a response that concisely and appropriately completes the request.\n\n" system_no_input_prompt = "Below is an instruction from a USER that describes a task. The ASSISTANT writes a response that appropriately and concisely completes the request.\n\n" - def __init__(self): # pylint: disable=super-init-not-called + def __init__(self): self.prompt_style = PromptStyle.CHAT.value self.match_prompt_style() @@ -54,7 +54,7 @@ class NoSystemPrompter(AlpacaPrompter): turn_format = "{instruction} {input} " turn_no_input_format = "{instruction} " - def __init__(self): # pylint: disable=super-init-not-called + def __init__(self): pass diff --git a/src/axolotl/prompt_strategies/alpaca_w_system.py b/src/axolotl/prompt_strategies/alpaca_w_system.py index 6873c8e087..808ba517e3 100644 --- a/src/axolotl/prompt_strategies/alpaca_w_system.py +++ b/src/axolotl/prompt_strategies/alpaca_w_system.py @@ -22,10 +22,9 @@ def parse_instruction_fields(self, prompt) -> Tuple[str, str, str, str]: ) def tokenize_prompt(self, prompt): - # pylint: disable=duplicate-code ( instruction, - input, # pylint: disable=redefined-builtin + input, response, system, ) = self.parse_instruction_fields(prompt) @@ -64,7 +63,7 @@ def build_prompt_w_system( self, system: str, instruction: str, - input: Union[None, str] = None, # pylint: disable=redefined-builtin + input: Union[None, str] = None, output: Union[None, str] = None, ) -> Generator[str, None, None]: # returns the full prompt from instruction and optional input @@ -93,7 +92,6 @@ class OpenOrcaSystemDataPrompter(SystemDataPrompter): """ def match_prompt_style(self): - # pylint: disable=duplicate-code if self.prompt_style == PromptStyle.INSTRUCT.value: self.turn_format = "### Human:\n{instruction}\n### Additional Context:\n{input}\n### Assistant:\n" self.turn_no_input_format = "### Human:\n{instruction}\n### Assistant:\n" diff --git a/src/axolotl/prompt_strategies/base.py b/src/axolotl/prompt_strategies/base.py index 370a51a95a..45a3ffda91 100644 --- a/src/axolotl/prompt_strategies/base.py +++ b/src/axolotl/prompt_strategies/base.py @@ -29,6 +29,6 @@ def load(strategy, cfg, module_base=None, **kwargs): mod = importlib.import_module(strategy, module_base) func = getattr(mod, load_fn) return func(cfg, **kwargs) - except Exception: # pylint: disable=broad-exception-caught + except Exception: LOG.warning(f"unable to load strategy {strategy}") return None diff --git a/src/axolotl/prompt_strategies/bradley_terry/__init__.py b/src/axolotl/prompt_strategies/bradley_terry/__init__.py index 7530aee192..7336edc717 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/__init__.py +++ b/src/axolotl/prompt_strategies/bradley_terry/__init__.py @@ -10,7 +10,6 @@ def load(strategy, tokenizer, cfg, ds_cfg): - # pylint: disable=duplicate-code try: load_fn = "load" if strategy.split(".")[-1].startswith("load_"): @@ -30,6 +29,6 @@ def load(strategy, tokenizer, cfg, ds_cfg): return func(tokenizer, cfg, **load_kwargs) except ModuleNotFoundError: return None - except Exception as exc: # pylint: disable=broad-exception-caught + except Exception as exc: LOG.error(f"Failed to load prompt strategy `{strategy}`: {str(exc)}") return None diff --git a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py index e655f85a1f..fd0d76f511 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py +++ b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py @@ -34,7 +34,6 @@ def _tokenize_single_prompt(self, prompt): max_length = self.prompter.max_length - # pylint: disable=duplicate-code prompt["messages"] = [] if prompt["system"]: prompt["messages"].append({"role": "system", "content": prompt["system"]}) @@ -52,7 +51,6 @@ def _tokenize_single_prompt(self, prompt): :max_length ] - # pylint: disable=duplicate-code prompt["messages"] = [] if prompt["system"]: prompt["messages"].append({"role": "system", "content": prompt["system"]}) diff --git a/src/axolotl/prompt_strategies/bradley_terry/llama3.py b/src/axolotl/prompt_strategies/bradley_terry/llama3.py index 1d586fd5f4..5548d882eb 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/llama3.py +++ b/src/axolotl/prompt_strategies/bradley_terry/llama3.py @@ -6,7 +6,7 @@ def icr( cfg, **kwargs, -): # pylint: disable=possibly-unused-variable,unused-argument +): """ chatml transforms for datasets with system, input, chosen, rejected ex. https://huggingface.co/datasets/argilla/distilabel-intel-orca-dpo-pairs diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index f927b7fcbf..cb3e3dfb15 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -2,8 +2,6 @@ HF Chat Templates prompt strategy """ -# pylint: disable=too-many-lines - from collections import defaultdict from typing import TYPE_CHECKING, Any, Dict, List, Set, Union @@ -402,9 +400,9 @@ def tokenize_prompt(self, prompt: dict[str, Any]): feature_names = list(prompt.keys()) # Process each prompt individually - for row in zip(*prompt.values()): + for row in zip(*prompt.values(), strict=False): tokenized_prompt = self._tokenize_single_prompt( - dict(zip(feature_names, row)) + dict(zip(feature_names, row, strict=False)) ) for key, val in tokenized_prompt.items(): res[key].append(val) @@ -431,9 +429,7 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: add_generation_prompt=True, images=images, ) - tokenized_res = self.prompter.build_prompt( - turns, images=images - ) # type: ignore + tokenized_res = self.prompter.build_prompt(turns, images=images) # type: ignore tokenized_prompt = {} if isinstance(tokenized_res, list): input_ids = prompt_ids + tokenized_res[len(prompt_ids) :] @@ -613,7 +609,6 @@ def find_turn( """ Locate the starting and ending indices of the specified turn in a conversation. """ - # pylint: disable=too-many-return-statements if turn_idx >= len(turns): raise ValueError(f"Turn index {turn_idx} out of range") @@ -850,7 +845,7 @@ def __init__( split_thinking: bool | None = False, ): # Call the parent's parent __init__ (PromptTokenizingStrategy) to skip ChatTemplateStrategy's validation - # pylint: disable=non-parent-init-called,super-init-not-called + PromptTokenizingStrategy.__init__( self, prompter, tokenizer, train_on_inputs, sequence_len ) diff --git a/src/axolotl/prompt_strategies/completion.py b/src/axolotl/prompt_strategies/completion.py index 62a4b90b28..f43f257937 100644 --- a/src/axolotl/prompt_strategies/completion.py +++ b/src/axolotl/prompt_strategies/completion.py @@ -42,8 +42,8 @@ def parse_instruction_fields(self, prompt) -> Tuple[str, str, str]: def tokenize_prompt(self, prompt): res = defaultdict(lambda: []) feature_names = list(prompt.keys()) - for row in zip(*prompt.values()): - prompt_row = dict(zip(feature_names, row)) + for row in zip(*prompt.values(), strict=False): + prompt_row = dict(zip(feature_names, row, strict=False)) ( instruction, _, @@ -59,9 +59,7 @@ def tokenize_prompt(self, prompt): return dict(res) - def _build_full_prompt( - self, instruction, input, response - ): # pylint: disable=redefined-builtin + def _build_full_prompt(self, instruction, input, response): return next(iter(self.prompter.build_prompt(instruction, input, response))) @@ -73,8 +71,8 @@ class CompletionPrompter: def build_prompt( self, instruction: str, - input=None, # pylint: disable=redefined-builtin, unused-argument - output=None, # pylint: disable=unused-argument + input=None, + output=None, ) -> Generator[str, None, None]: yield instruction diff --git a/src/axolotl/prompt_strategies/context_qa.py b/src/axolotl/prompt_strategies/context_qa.py index aac44e0b2c..09e96d26ef 100644 --- a/src/axolotl/prompt_strategies/context_qa.py +++ b/src/axolotl/prompt_strategies/context_qa.py @@ -86,7 +86,6 @@ class ContextV2Prompter(AlpacaPrompter): system_no_input_prompt = "" def match_prompt_style(self): - # pylint: disable=duplicate-code self.turn_format = "{instruction}\n{input}" self.turn_no_input_format = "{instruction}" self.system_format = "{system}" diff --git a/src/axolotl/prompt_strategies/creative_acr.py b/src/axolotl/prompt_strategies/creative_acr.py index ea67034b3b..3e016e30ee 100644 --- a/src/axolotl/prompt_strategies/creative_acr.py +++ b/src/axolotl/prompt_strategies/creative_acr.py @@ -134,9 +134,7 @@ class CreativePrompterBase: def build_prompt( self, instruction: str, - input: Union[ # pylint: disable=redefined-builtin, unused-argument - None, str - ] = None, + input: Union[None, str] = None, output: Union[None, str] = None, ) -> Generator[str, None, None]: if self.system_prompt: diff --git a/src/axolotl/prompt_strategies/dpo/chat_template.py b/src/axolotl/prompt_strategies/dpo/chat_template.py index 7867708856..85c4d21828 100644 --- a/src/axolotl/prompt_strategies/dpo/chat_template.py +++ b/src/axolotl/prompt_strategies/dpo/chat_template.py @@ -6,9 +6,7 @@ from axolotl.utils.schemas.utils import handle_legacy_message_fields_logic -def default( - cfg, dataset_idx=0, **kwargs -): # pylint: disable=possibly-unused-variable,unused-argument +def default(cfg, dataset_idx=0, **kwargs): ds_cfg = cfg["datasets"][dataset_idx] ds_cfg = handle_legacy_message_fields_logic(ds_cfg) diff --git a/src/axolotl/prompt_strategies/dpo/chatml.py b/src/axolotl/prompt_strategies/dpo/chatml.py index 34a54aaa0b..8614708eb5 100644 --- a/src/axolotl/prompt_strategies/dpo/chatml.py +++ b/src/axolotl/prompt_strategies/dpo/chatml.py @@ -6,7 +6,7 @@ def default( cfg, **kwargs, -): # pylint: disable=possibly-unused-variable,unused-argument +): def transform_fn(sample): if "prompt" in sample.keys(): prompt_key = "prompt" @@ -46,7 +46,7 @@ def transform_fn(sample): def argilla_chat( cfg, **kwargs, -): # pylint: disable=possibly-unused-variable,unused-argument +): """ for argilla/dpo-mix-7k conversations """ @@ -65,7 +65,7 @@ def transform_fn(sample): def icr( cfg, **kwargs, -): # pylint: disable=possibly-unused-variable,unused-argument +): """ chatml transforms for datasets with system, input, chosen, rejected ex. https://huggingface.co/datasets/argilla/distilabel-intel-orca-dpo-pairs @@ -88,7 +88,7 @@ def transform_fn(sample): return transform_fn -def intel(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument +def intel(cfg, **kwargs): """ For Intel Orca DPO Pairs """ @@ -110,9 +110,7 @@ def transform_fn(sample): return transform_fn -def prompt_pairs( - cfg, **kwargs -): # pylint: disable=possibly-unused-variable,unused-argument +def prompt_pairs(cfg, **kwargs): def transform_fn(sample): if "system" in sample and sample["system"]: sample["prompt"] = ( @@ -130,7 +128,7 @@ def transform_fn(sample): return transform_fn -def ultra(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument +def ultra(cfg, **kwargs): """ for ultrafeedback binarized conversations """ diff --git a/src/axolotl/prompt_strategies/dpo/llama3.py b/src/axolotl/prompt_strategies/dpo/llama3.py index eed420017a..c13ff55e4d 100644 --- a/src/axolotl/prompt_strategies/dpo/llama3.py +++ b/src/axolotl/prompt_strategies/dpo/llama3.py @@ -6,9 +6,8 @@ def default( cfg, **kwargs, -): # pylint: disable=possibly-unused-variable,unused-argument +): def transform_fn(sample): - # pylint: disable=duplicate-code if "prompt" in sample.keys(): prompt_key = "prompt" elif "input" in sample.keys(): @@ -47,7 +46,7 @@ def transform_fn(sample): def argilla_chat( cfg, **kwargs, -): # pylint: disable=possibly-unused-variable,unused-argument +): """ for argilla/dpo-mix-7k conversations """ @@ -66,7 +65,7 @@ def transform_fn(sample): def icr( cfg, **kwargs, -): # pylint: disable=possibly-unused-variable,unused-argument +): """ chatml transforms for datasets with system, input, chosen, rejected ex. https://huggingface.co/datasets/argilla/distilabel-intel-orca-dpo-pairs @@ -89,7 +88,7 @@ def transform_fn(sample): return transform_fn -def intel(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument +def intel(cfg, **kwargs): """ For Intel Orca DPO Pairs """ @@ -111,9 +110,7 @@ def transform_fn(sample): return transform_fn -def prompt_pairs( - cfg, **kwargs -): # pylint: disable=possibly-unused-variable,unused-argument +def prompt_pairs(cfg, **kwargs): def transform_fn(sample): if "system" in sample and sample["system"]: sample["prompt"] = ( @@ -131,7 +128,7 @@ def transform_fn(sample): return transform_fn -def ultra(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument +def ultra(cfg, **kwargs): """ for ultrafeedback binarized conversations """ diff --git a/src/axolotl/prompt_strategies/dpo/passthrough.py b/src/axolotl/prompt_strategies/dpo/passthrough.py index 1fcb838db3..52b5ceac19 100644 --- a/src/axolotl/prompt_strategies/dpo/passthrough.py +++ b/src/axolotl/prompt_strategies/dpo/passthrough.py @@ -3,12 +3,8 @@ """ -def default( - cfg, dataset_idx=0, **kwargs -): # pylint: disable=possibly-unused-variable,unused-argument - def transform_fn( - sample, tokenizer=None - ): # pylint: disable=possibly-unused-variable,unused-argument +def default(cfg, dataset_idx=0, **kwargs): + def transform_fn(sample, tokenizer=None): return sample return transform_fn diff --git a/src/axolotl/prompt_strategies/dpo/user_defined.py b/src/axolotl/prompt_strategies/dpo/user_defined.py index cdd9b8c9c9..0bcb1d94ce 100644 --- a/src/axolotl/prompt_strategies/dpo/user_defined.py +++ b/src/axolotl/prompt_strategies/dpo/user_defined.py @@ -3,7 +3,7 @@ """ -def default(cfg, dataset_idx=0, **kwargs): # pylint: disable=unused-argument +def default(cfg, dataset_idx=0, **kwargs): ds_cfg = cfg["datasets"][dataset_idx]["type"] if not isinstance(ds_cfg, dict): raise ValueError( diff --git a/src/axolotl/prompt_strategies/dpo/zephyr.py b/src/axolotl/prompt_strategies/dpo/zephyr.py index 9eb8950091..781227181d 100644 --- a/src/axolotl/prompt_strategies/dpo/zephyr.py +++ b/src/axolotl/prompt_strategies/dpo/zephyr.py @@ -3,14 +3,11 @@ """ -def nectar(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument +def nectar(cfg, **kwargs): def transform_fn(sample): data = {} data["prompt"] = ( - "<|system|>\n
\n" - "<|user|>\n" - f"{sample['prompt']}\n" - "<|assistant|>\n" + f"<|system|>\n\n<|user|>\n{sample['prompt']}\n<|assistant|>\n" ) answers = sorted(sample["answers"], key=lambda x: x["rank"]) data["chosen"] = answers[-1]["answer"] diff --git a/src/axolotl/prompt_strategies/input_output.py b/src/axolotl/prompt_strategies/input_output.py index 8be745b208..c84eecffc2 100644 --- a/src/axolotl/prompt_strategies/input_output.py +++ b/src/axolotl/prompt_strategies/input_output.py @@ -16,7 +16,6 @@ def __init__(self, *args, eos_token=None, **kwargs): self.eos_token = self.tokenizer.eos_token def tokenize_prompt(self, prompt): - # pylint: disable=duplicate-code input_ids = [] labels = [] for label, text in self.prompter.build_prompt(prompt["segments"]): diff --git a/src/axolotl/prompt_strategies/kto/chatml.py b/src/axolotl/prompt_strategies/kto/chatml.py index 97ae59ed50..945940f3fd 100644 --- a/src/axolotl/prompt_strategies/kto/chatml.py +++ b/src/axolotl/prompt_strategies/kto/chatml.py @@ -2,13 +2,11 @@ KTO strategies for chatml """ -# pylint: disable=duplicate-code - def argilla( cfg, **kwargs, -): # pylint: disable=possibly-unused-variable,unused-argument +): def transform_fn(sample): if "system" in sample and sample["system"]: sample["prompt"] = ( @@ -28,7 +26,7 @@ def transform_fn(sample): def argilla_chat( cfg, **kwargs, -): # pylint: disable=possibly-unused-variable,unused-argument +): """ for argilla/kto-mix-15k conversations """ @@ -43,7 +41,7 @@ def transform_fn(sample): return transform_fn -def intel(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument +def intel(cfg, **kwargs): """ For Intel Orca KTO ex: argilla/distilabel-intel-orca-kto @@ -65,9 +63,7 @@ def transform_fn(sample): return transform_fn -def prompt_pairs( - cfg, **kwargs -): # pylint: disable=possibly-unused-variable,unused-argument +def prompt_pairs(cfg, **kwargs): def transform_fn(sample): if "system" in sample and sample["system"]: sample["prompt"] = ( @@ -84,7 +80,7 @@ def transform_fn(sample): return transform_fn -def ultra(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument +def ultra(cfg, **kwargs): """ for ultrafeedback binarized conversations ex: argilla/ultrafeedback-binarized-preferences-cleaned-kto diff --git a/src/axolotl/prompt_strategies/kto/llama3.py b/src/axolotl/prompt_strategies/kto/llama3.py index fde3c2ed4a..9061f6f5ea 100644 --- a/src/axolotl/prompt_strategies/kto/llama3.py +++ b/src/axolotl/prompt_strategies/kto/llama3.py @@ -2,13 +2,11 @@ KTO strategies for llama-3 chat template """ -# pylint: disable=duplicate-code - def argilla( cfg, **kwargs, -): # pylint: disable=possibly-unused-variable,unused-argument +): def transform_fn(sample): if "system" in sample and sample["system"]: sample["prompt"] = ( @@ -28,7 +26,7 @@ def transform_fn(sample): def argilla_chat( cfg, **kwargs, -): # pylint: disable=possibly-unused-variable,unused-argument +): """ for argilla/kto-mix-15k conversations """ @@ -43,7 +41,7 @@ def transform_fn(sample): return transform_fn -def intel(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument +def intel(cfg, **kwargs): """ For Intel Orca KTO ex: argilla/distilabel-intel-orca-kto @@ -65,9 +63,7 @@ def transform_fn(sample): return transform_fn -def prompt_pairs( - cfg, **kwargs -): # pylint: disable=possibly-unused-variable,unused-argument +def prompt_pairs(cfg, **kwargs): def transform_fn(sample): if "system" in sample and sample["system"]: sample["prompt"] = ( @@ -84,7 +80,7 @@ def transform_fn(sample): return transform_fn -def ultra(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument +def ultra(cfg, **kwargs): """ for ultrafeedback binarized conversations ex: argilla/ultrafeedback-binarized-preferences-cleaned-kto diff --git a/src/axolotl/prompt_strategies/kto/user_defined.py b/src/axolotl/prompt_strategies/kto/user_defined.py index 7c68a30005..e26683cdea 100644 --- a/src/axolotl/prompt_strategies/kto/user_defined.py +++ b/src/axolotl/prompt_strategies/kto/user_defined.py @@ -2,10 +2,8 @@ User-defined KTO strategies """ -# pylint: disable=duplicate-code - -def default(cfg, dataset_idx=0, **kwargs): # pylint: disable=unused-argument +def default(cfg, dataset_idx=0, **kwargs): ds_cfg = cfg["datasets"][dataset_idx]["type"] if not isinstance(ds_cfg, dict): raise ValueError( diff --git a/src/axolotl/prompt_strategies/llama2_chat.py b/src/axolotl/prompt_strategies/llama2_chat.py index eef2e1d4d3..9eff062ec7 100644 --- a/src/axolotl/prompt_strategies/llama2_chat.py +++ b/src/axolotl/prompt_strategies/llama2_chat.py @@ -153,7 +153,7 @@ def tokenize_prompt(self, prompt): } -class Llama2ChatPrompter: # pylint: disable=too-few-public-methods +class Llama2ChatPrompter: """ A prompter that generates prompts for Llama2 models. """ @@ -190,7 +190,7 @@ def build_prompt(self, source) -> Generator[Llama2ChatConversation, None, None]: # Skip the first one if it is not from human source = source[1:] - conv.messages = [] # pylint: disable=R0801 + conv.messages = [] for j, sentence in enumerate(source): role = roles[sentence["from"]] assert role == conv.roles[j % 2], ALTERNATING_ASSERTION_FAILED_ROLE diff --git a/src/axolotl/prompt_strategies/messages/__init__.py b/src/axolotl/prompt_strategies/messages/__init__.py index 6eae9dfd8a..2c920a5682 100644 --- a/src/axolotl/prompt_strategies/messages/__init__.py +++ b/src/axolotl/prompt_strategies/messages/__init__.py @@ -11,7 +11,7 @@ def load(tokenizer, cfg, ds_cfg, processor=None): try: strategy = ds_cfg.get("input_transform", "chat") - # pylint: disable=duplicate-code + load_fn = "load" if strategy.split(".")[-1].startswith("load_"): load_fn = strategy.split(".")[-1] @@ -29,6 +29,6 @@ def load(tokenizer, cfg, ds_cfg, processor=None): return func(tokenizer, cfg, **load_kwargs) except ModuleNotFoundError: return None - except Exception as exc: # pylint: disable=broad-exception-caught + except Exception as exc: LOG.error(f"Failed to load prompt strategy `{strategy}`: {str(exc)}") raise exc diff --git a/src/axolotl/prompt_strategies/messages/chat.py b/src/axolotl/prompt_strategies/messages/chat.py index eaed2396a6..854d25e42a 100644 --- a/src/axolotl/prompt_strategies/messages/chat.py +++ b/src/axolotl/prompt_strategies/messages/chat.py @@ -19,7 +19,7 @@ def __init__( processor, message_transform=None, formatter=None, - **kwargs, # pylint: disable=unused-argument + **kwargs, ): """ :param processor: tokenizer or image processor @@ -35,7 +35,7 @@ def wrap_dataset( dataset, process_count: Optional[int] = None, keep_in_memory: Optional[bool] = False, - **kwargs, # pylint: disable=unused-argument + **kwargs, ): self.dataset = TokenizedChatDataset( dataset, @@ -72,9 +72,10 @@ def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): builder_kwargs["message_field_training"] = message_field_training chat_template = ds_cfg.get("chat_template", cfg.get("chat_template", "chatml")) - format_message = ( - lambda x: x # noqa E731 # pylint: disable=unnecessary-lambda-assignment - ) + + def format_message(x): + return x + if chat_template == "chatml": from axolotl.core.chat.format.chatml import format_message # noqa F811 if chat_template.startswith("llama3"): diff --git a/src/axolotl/prompt_strategies/metharme.py b/src/axolotl/prompt_strategies/metharme.py index 66da723893..35f1ef3b32 100644 --- a/src/axolotl/prompt_strategies/metharme.py +++ b/src/axolotl/prompt_strategies/metharme.py @@ -10,8 +10,6 @@ IGNORE_TOKEN_ID = -100 -# pylint: disable=duplicate-code - class MetharmePromptTokenizingStrategy(InstructionPromptTokenizingStrategy): """ @@ -66,7 +64,7 @@ class MetharmePrompter(AlpacaPrompter): turn_format = "{instruction}" turn_no_input_format = "{instruction}" - def __init__(self, *args, **kwargs): # pylint: disable=super-init-not-called + def __init__(self, *args, **kwargs): pass diff --git a/src/axolotl/prompt_strategies/orpo/chat_template.py b/src/axolotl/prompt_strategies/orpo/chat_template.py index fdee28ea1e..b655bc9703 100644 --- a/src/axolotl/prompt_strategies/orpo/chat_template.py +++ b/src/axolotl/prompt_strategies/orpo/chat_template.py @@ -23,9 +23,7 @@ class MessageList(BaseModel): messages: List[Message] -def load( - tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None, **kwargs -): # pylint: disable=possibly-unused-variable,unused-argument +def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None, **kwargs): """ chatml transforms for datasets with system, input, chosen, rejected """ @@ -219,29 +217,38 @@ def build_prompt( for message in message_list.messages: conversation.append(message.model_dump()) if message.role == "system": - yield self.tokenizer.apply_chat_template( - conversation, - add_generation_prompt=False, - chat_template=self.chat_template, - tokenize=False, - ), False + yield ( + self.tokenizer.apply_chat_template( + conversation, + add_generation_prompt=False, + chat_template=self.chat_template, + tokenize=False, + ), + False, + ) if message.role == "user": - yield self.tokenizer.apply_chat_template( - conversation, - add_generation_prompt=True, - chat_template=self.chat_template, - tokenize=False, - ), False + yield ( + self.tokenizer.apply_chat_template( + conversation, + add_generation_prompt=True, + chat_template=self.chat_template, + tokenize=False, + ), + False, + ) if message.role == "assistant": - yield self.tokenizer.apply_chat_template( - conversation, - add_generation_prompt=False, - chat_template=self.chat_template, - tokenize=False, - ), True + yield ( + self.tokenizer.apply_chat_template( + conversation, + add_generation_prompt=False, + chat_template=self.chat_template, + tokenize=False, + ), + True, + ) -def argilla(cfg, **kwargs): # pylint: disable=possibly-unused-variable,unused-argument +def argilla(cfg, **kwargs): dataset_parser = ORPODatasetParsingStrategy() def transform_fn(sample, tokenizer=None): diff --git a/src/axolotl/prompt_strategies/pygmalion.py b/src/axolotl/prompt_strategies/pygmalion.py index 51f92f3970..8c53a5f279 100644 --- a/src/axolotl/prompt_strategies/pygmalion.py +++ b/src/axolotl/prompt_strategies/pygmalion.py @@ -69,7 +69,6 @@ def tokenize_prompt(self, prompt): LOG.warning(f"unknown role in conversation: {role}") res = defaultdict(lambda: []) - # pylint: disable=duplicate-code result, current_len = parse_tokenized_to_result( result, current_len, @@ -89,7 +88,10 @@ def __init__(self, *args, **kwargs): pass def build_prompt( - self, source, *args, **kwargs # pylint: disable=unused-argument + self, + source, + *args, + **kwargs, ) -> Generator[Tuple[str, str], None, None]: for msg in source: yield msg["role"], msg["value"] diff --git a/src/axolotl/prompt_strategies/stepwise_supervised.py b/src/axolotl/prompt_strategies/stepwise_supervised.py index 8be7c35e36..9175126e75 100644 --- a/src/axolotl/prompt_strategies/stepwise_supervised.py +++ b/src/axolotl/prompt_strategies/stepwise_supervised.py @@ -66,7 +66,7 @@ def tokenize_prompt( # Create step-wise labels labels = [ [IGNORE_INDEX] * (len(completion) - 1) + [label] # type: ignore - for completion, label in zip(completions_ids, labels) + for completion, label in zip(completions_ids, labels, strict=False) ] # Join all steps diff --git a/src/axolotl/prompt_strategies/user_defined.py b/src/axolotl/prompt_strategies/user_defined.py index e20e80c3a4..0bff514e7e 100644 --- a/src/axolotl/prompt_strategies/user_defined.py +++ b/src/axolotl/prompt_strategies/user_defined.py @@ -83,16 +83,12 @@ def match_prompt_style(self): cfg.sequence_len, ) - setattr( - strat, - "parse_instruction_fields", - partial( - parse_instruction_fields, - ds_cfg.field_instruction, - ds_cfg.field_input, - ds_cfg.field_output, - ds_cfg.field_system, - system_prompt, - ), + strat.parse_instruction_fields = partial( # type: ignore[method-assign] + parse_instruction_fields, + ds_cfg.field_instruction, + ds_cfg.field_input, + ds_cfg.field_output, + ds_cfg.field_system, + system_prompt, ) return strat diff --git a/src/axolotl/prompt_tokenizers.py b/src/axolotl/prompt_tokenizers.py index 9ca645de3c..2bf9ec763e 100644 --- a/src/axolotl/prompt_tokenizers.py +++ b/src/axolotl/prompt_tokenizers.py @@ -118,7 +118,7 @@ def parse_instruction_fields( def tokenize_prompt(self, prompt): ( instruction, - input, # pylint: disable=redefined-builtin + input, response, ) = self.parse_instruction_fields(prompt) user_prompt = next( @@ -144,7 +144,10 @@ def tokenize_prompt(self, prompt): return tokenized_prompt def _build_full_prompt( - self, instruction, input, response # pylint: disable=redefined-builtin + self, + instruction, + input, + response, ): return next( iter( @@ -257,10 +260,9 @@ def parse_instruction_fields(self, prompt) -> Tuple[str, str, str, str, str]: raise NotImplementedError def tokenize_prompt(self, prompt): - # pylint: disable=duplicate-code ( instruction, - input, # pylint: disable=redefined-builtin + input, output, reflection, corrected, @@ -287,9 +289,7 @@ def tokenize_prompt(self, prompt): return tokenized_full_prompt - def _build_full_prompt( - self, instruction, input, output, reflection, corrected - ): # pylint: disable=redefined-builtin + def _build_full_prompt(self, instruction, input, output, reflection, corrected): return next( iter( self.prompter.build_prompt( diff --git a/src/axolotl/prompters.py b/src/axolotl/prompters.py index d29da075e0..9543996f7e 100644 --- a/src/axolotl/prompters.py +++ b/src/axolotl/prompters.py @@ -46,7 +46,6 @@ def __init__(self, prompt_style: Optional[str] = PromptStyle.INSTRUCT.value): self.match_prompt_style() def match_prompt_style(self): - # pylint: disable=duplicate-code if self.prompt_style == PromptStyle.INSTRUCT.value: self.turn_format = "### Instruction:\n{instruction}\n\n### Input:\n{input}\n\n### Response:\n" self.turn_no_input_format = ( @@ -93,7 +92,7 @@ def _build_result(self, instruction, input_text, output): def build_prompt( self, instruction: str, - input: Union[None, str] = None, # pylint: disable=redefined-builtin + input: Union[None, str] = None, output: Union[None, str] = None, ) -> Generator[str, None, None]: yield self._build_result(instruction, input, output) @@ -218,7 +217,7 @@ def match_prompt_style(self): def _build_result( self, instruction: str, - input: Union[None, str] = None, # pylint: disable=redefined-builtin + input: Union[None, str] = None, output: Union[None, str] = None, reflection: Union[None, str] = None, corrected: Union[None, str] = None, @@ -242,12 +241,11 @@ def _build_result( def build_prompt( self, instruction: str, - input: Union[None, str] = None, # pylint: disable=redefined-builtin + input: Union[None, str] = None, output: Union[None, str] = None, reflection: Union[None, str] = None, corrected: Union[None, str] = None, ) -> Generator[str, None, None]: - # pylint: disable=duplicate-code yield self._build_result( instruction, input, diff --git a/src/axolotl/train.py b/src/axolotl/train.py index dd39cc2289..e409d4a11e 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -230,7 +230,7 @@ def save_trained_model( # Post training module hooks for name, module in model.named_modules(): if hasattr(module, "_post_training"): - module._post_training(model, name) # pylint: disable=protected-access + module._post_training(model, name) # handle QAT if cfg.qat: @@ -253,9 +253,7 @@ def save_trained_model( # final model weights have already been saved by `ReLoRACallback.on_train_end` return - if ( # pylint: disable=too-many-nested-blocks - trainer.is_fsdp_enabled or cfg.fsdp_config - ): + if trainer.is_fsdp_enabled or cfg.fsdp_config: if cfg.fsdp_config or cfg.fsdp: if cfg.fsdp_config.final_state_dict_type: state_dict_type = cfg.fsdp_config.final_state_dict_type @@ -438,7 +436,7 @@ def setup_model_card(cfg: DictDefault): badge_markdown = """[Built with Axolotl](https://github.com/axolotl-ai-cloud/axolotl)""" transformers.modelcard.AUTOGENERATED_TRAINER_COMMENT += f"\n{badge_markdown}" - if getattr(cfg, "axolotl_config_path"): + if cfg.axolotl_config_path: raw_axolotl_cfg = Path(cfg.axolotl_config_path) version = importlib.metadata.version("axolotl") if raw_axolotl_cfg.is_file(): @@ -489,7 +487,9 @@ def handle_untrained_tokens_fix( ) -def setup_model_and_trainer(cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> tuple[ +def setup_model_and_trainer( + cfg: DictDefault, dataset_meta: TrainDatasetMeta +) -> tuple[ "HFRLTrainerBuilder" | "HFCausalTrainerBuilder", PeftModel | PreTrainedModel, PreTrainedTokenizer, diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index e669413f82..e5050116a8 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -17,7 +17,6 @@ def is_comet_available(): return importlib.util.find_spec("comet_ml") is not None -# pylint: disable=duplicate-code def get_pytorch_version() -> tuple[int, int, int]: """ Get Pytorch version as a tuple of (major, minor, patch). diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index d3f3126b5d..6c5512223d 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -56,9 +56,7 @@ LOG = get_logger(__name__) -class SaveBetterTransformerModelCallback( - TrainerCallback -): # pylint: disable=too-few-public-methods +class SaveBetterTransformerModelCallback(TrainerCallback): """Callback to save the BetterTransformer wrapped model""" def on_step_end( @@ -103,7 +101,7 @@ def __init__(self, cfg): def on_step_end( self, - args: TrainingArguments, # pylint: disable=unused-argument + args: TrainingArguments, state: TrainerState, control: TrainerControl, **_kwargs, @@ -126,7 +124,7 @@ class SaveModelOnFirstStepCallback(TrainerCallback): def on_step_end( self, - args: TrainingArguments, # pylint: disable=unused-argument + args: TrainingArguments, state: TrainerState, control: TrainerControl, **_kwargs, @@ -239,10 +237,10 @@ class BenchEvalCallback(TrainerCallback): def on_evaluate( self, args: AxolotlTrainingArguments, - state: TrainerState, # pylint: disable=unused-argument - control: TrainerControl, # pylint: disable=unused-argument - metrics: Dict[str, float], # pylint: disable=unused-argument - **kwargs, # pylint: disable=unused-argument + state: TrainerState, + control: TrainerControl, + metrics: Dict[str, float], + **kwargs, ): data_loader = trainer.get_bench_dataloader( bench_dataset.remove_columns(["input", "subject", "output", "name"]) @@ -272,7 +270,7 @@ def on_evaluate( # Extract results by subject. bench_name = bench_dataset["name"] bench_names: dict = {s: {"refs": [], "preds": []} for s in set(bench_name)} - for s, p, r in zip(bench_name, preds, refs): # pylint: disable=invalid-name + for s, p, r in zip(bench_name, preds, refs, strict=False): bench_names[s]["preds"].append(p) bench_names[s]["refs"].append(r) barrier() @@ -310,9 +308,7 @@ def on_evaluate( bench_scores = [] bench_refs = [] bench_preds = [] - for ( - bench_name - ) in combined_bench_names: # pylint: disable=consider-using-dict-items + for bench_name in combined_bench_names: bench_score = accuracy.compute( references=combined_bench_names[bench_name]["refs"], predictions=combined_bench_names[bench_name]["preds"], @@ -361,18 +357,18 @@ def __maybe_load_metrics(self): else: try: metrics[metric] = evaluate.load(metric) - except Exception as exc: # pylint: disable=broad-exception-caught + except Exception as exc: LOG.warning(f"{metric}: {exc.args}") return metrics def on_evaluate( self, - args: AxolotlTrainingArguments, # pylint: disable=unused-argument + args: AxolotlTrainingArguments, state: TrainerState, control: TrainerControl, - train_dataloader, # pylint: disable=unused-argument + train_dataloader, eval_dataloader, - **kwargs, # pylint: disable=unused-argument + **kwargs, ): trainer.model_wrapped.eval() @@ -380,7 +376,6 @@ def on_evaluate( self.cfg.device ) # Use this instead of trainer.model_wrapped.device as it may return cpu if fsdp offloaded - # pylint: disable=duplicate-code generation_config = GenerationConfig( max_new_tokens=self.cfg.eval_max_new_tokens, bos_token_id=tokenizer.bos_token_id, @@ -411,9 +406,7 @@ def compute(metric: evaluate.Metric, **kwargs): try: # Only pass the kwargs that are in the metric's feature list metric_kwargs = { - k: kwargs[k] - for k in metric._feature_names() # pylint: disable=protected-access - if k in kwargs + k: kwargs[k] for k in metric._feature_names() if k in kwargs } if isinstance(metric, Perplexity): @@ -425,7 +418,7 @@ def compute(metric: evaluate.Metric, **kwargs): if "score" in metric_score else metric_score["mean_score"] ) - except Exception: # pylint: disable=broad-exception-caught + except Exception: traceback.print_exc() LOG.debug( f"Failed to compute metric {metric.name} with kwargs {kwargs.keys()}" @@ -473,6 +466,7 @@ def predict_with_generate(): batch_input_ids, batch_labels, batch_pos_ids, + strict=False, ): if pos_ids is None: pos_ranges = [(0, len(input_ids_all) - 1)] @@ -523,7 +517,7 @@ def predict_with_generate(): prediction_all_tokens = predictions["sequences"].cpu().tolist() prediction_without_prompt_tokens_list = [] for prompt_token_ids, prediction_tokens in zip( - prompt_token_ids_list, prediction_all_tokens + prompt_token_ids_list, prediction_all_tokens, strict=False ): prediction_without_prompt_tokens = prediction_tokens[ len(prompt_token_ids) : @@ -561,12 +555,12 @@ def __init__(self, cfg): def on_evaluate( self, - args: AxolotlTrainingArguments, # pylint: disable=unused-argument + args: AxolotlTrainingArguments, state: TrainerState, control: TrainerControl, - train_dataloader, # pylint: disable=unused-argument + train_dataloader, eval_dataloader, - **kwargs, # pylint: disable=unused-argument + **kwargs, ): eval_table_size = self.cfg.eval_table_size @@ -576,7 +570,6 @@ def on_evaluate( trainer.model.eval() device = torch.device(self.cfg.device) - # pylint: disable=duplicate-code generation_config = GenerationConfig( max_new_tokens=self.cfg.eval_max_new_tokens, bos_token_id=tokenizer.bos_token_id, @@ -644,6 +637,7 @@ def log_table_from_dataloader(name: str, table_dataloader): batch_labels, batch_pos_ids, batch_logits, + strict=False, ): if pos_ids is None: pos_ranges = [(0, len(input_ids_all) - 1)] @@ -697,7 +691,7 @@ def log_table_from_dataloader(name: str, table_dataloader): prediction_all_tokens = predictions["sequences"].cpu().tolist() prediction_without_prompt_tokens_list = [] for prompt_token_ids, prediction_tokens in zip( - prompt_token_ids_list, prediction_all_tokens + prompt_token_ids_list, prediction_all_tokens, strict=False ): prediction_without_prompt_tokens = prediction_tokens[ len(prompt_token_ids) : @@ -716,7 +710,11 @@ def log_table_from_dataloader(name: str, table_dataloader): prediction_text, pred_step_text, ) in zip( - prompt_texts, completion_texts, predicted_texts, pred_step_texts + prompt_texts, + completion_texts, + predicted_texts, + pred_step_texts, + strict=False, ): table_data["id"].append(row_index) table_data["Prompt"].append(prompt_text) @@ -774,10 +772,10 @@ def __init__(self, axolotl_config_path): def on_train_begin( self, - args: AxolotlTrainingArguments, # pylint: disable=unused-argument - state: TrainerState, # pylint: disable=unused-argument + args: AxolotlTrainingArguments, + state: TrainerState, control: TrainerControl, - **kwargs, # pylint: disable=unused-argument + **kwargs, ): if state.is_world_process_zero: try: @@ -845,19 +843,30 @@ def _gc(self): gc.collect() def on_train_begin( - self, args, state, control, **kwargs # pylint: disable=unused-argument + self, + args, + state, + control, + **kwargs, ): self._gc() def on_step_begin( - self, args, state, control, **kwargs # pylint: disable=unused-argument + self, + args, + state, + control, + **kwargs, ): - # pylint: disable=consider-using-in if self.next_gc_on_begin_step == state.global_step or state.global_step == 0: self._gc() def on_step_end( - self, args, state, control, **kwargs # pylint: disable=unused-argument + self, + args, + state, + control, + **kwargs, ): if control.should_evaluate: # automatically GC before evals so the eval memory spike from the CEL doesn't OOM the trainer @@ -879,7 +888,11 @@ def on_step_end( self._gc() def on_epoch_end( - self, args, state, control, **kwargs # pylint: disable=unused-argument + self, + args, + state, + control, + **kwargs, ): self._gc() @@ -892,16 +905,12 @@ def __init__(self, cfg): self.gpu_name = torch.cuda.get_device_name(0) self.cfg = cfg - def on_train_end( - self, args, state, control, **kwargs - ): # pylint: disable=unused-argument + def on_train_end(self, args, state, control, **kwargs): """ handle T4 gpu, we need to convert attention to eager for inference """ if "Tesla T4" in self.gpu_name and self.cfg.xformers_attention: - trainer.model.config._attn_implementation = ( # pylint: disable=protected-access - "eager" - ) + trainer.model.config._attn_implementation = "eager" trainer.model.gradient_checkpointing_disable() trainer.model.config.use_cache = True trainer.model.eval() diff --git a/src/axolotl/utils/callbacks/comet_.py b/src/axolotl/utils/callbacks/comet_.py index 7dce951456..cd3bcf70ec 100644 --- a/src/axolotl/utils/callbacks/comet_.py +++ b/src/axolotl/utils/callbacks/comet_.py @@ -22,10 +22,10 @@ def __init__(self, axolotl_config_path): def on_train_begin( self, - args: "AxolotlTrainingArguments", # pylint: disable=unused-argument - state: TrainerState, # pylint: disable=unused-argument + args: "AxolotlTrainingArguments", + state: TrainerState, control: TrainerControl, - **kwargs, # pylint: disable=unused-argument + **kwargs, ): if is_main_process(): try: diff --git a/src/axolotl/utils/callbacks/lisa.py b/src/axolotl/utils/callbacks/lisa.py index 348cdf2da2..03f189d805 100644 --- a/src/axolotl/utils/callbacks/lisa.py +++ b/src/axolotl/utils/callbacks/lisa.py @@ -55,9 +55,7 @@ def freeze_all_layers(self): for param in layer.parameters(): param.requires_grad = False - def on_step_begin( - self, args, state, control, **kwargs - ): # pylint: disable=unused-argument + def on_step_begin(self, args, state, control, **kwargs): # Check if it's time to switch active layers, including at step 0 if state.global_step % self.step_interval == 0 or state.global_step == 1: self.switch_active_layers() diff --git a/src/axolotl/utils/callbacks/mlflow_.py b/src/axolotl/utils/callbacks/mlflow_.py index ac72f5e6de..30120a87de 100644 --- a/src/axolotl/utils/callbacks/mlflow_.py +++ b/src/axolotl/utils/callbacks/mlflow_.py @@ -23,7 +23,6 @@ def should_log_artifacts() -> bool: class SaveAxolotlConfigtoMlflowCallback(TrainerCallback): - # pylint: disable=duplicate-code """Callback to save axolotl config to mlflow""" def __init__(self, axolotl_config_path): @@ -31,10 +30,10 @@ def __init__(self, axolotl_config_path): def on_train_begin( self, - args: "AxolotlTrainingArguments", # pylint: disable=unused-argument - state: TrainerState, # pylint: disable=unused-argument + args: "AxolotlTrainingArguments", + state: TrainerState, control: TrainerControl, - **kwargs, # pylint: disable=unused-argument + **kwargs, ): if is_main_process(): try: diff --git a/src/axolotl/utils/callbacks/profiler.py b/src/axolotl/utils/callbacks/profiler.py index d26b7f9dd0..2cf5e0f4f9 100644 --- a/src/axolotl/utils/callbacks/profiler.py +++ b/src/axolotl/utils/callbacks/profiler.py @@ -26,58 +26,50 @@ def __init__(self, steps_to_profile: int = 5, profiler_steps_start: int = 0): if profiler_steps_start == 0: # start recording memory allocations before everything is allocated, because if we start # at the beginning of step 0, we won't have any memory allocations in the traces - torch.cuda.memory._record_memory_history( # pylint: disable=protected-access - enabled="all" - ) + torch.cuda.memory._record_memory_history(enabled="all") profiler_steps_start = -1 self.profiler_steps_start = profiler_steps_start - def on_step_begin( # pylint: disable=unused-argument + def on_step_begin( self, - args: TrainingArguments, # pylint: disable=unused-argument + args: TrainingArguments, state: TrainerState, - control: TrainerControl, # pylint: disable=unused-argument - **kwargs, # pylint: disable=unused-argument + control: TrainerControl, + **kwargs, ): if state.global_step == self.profiler_steps_start: - torch.cuda.memory._record_memory_history( # pylint: disable=protected-access - enabled="all" - ) + torch.cuda.memory._record_memory_history(enabled="all") - def on_step_end( # pylint: disable=unused-argument + def on_step_end( self, - args: TrainingArguments, # pylint: disable=unused-argument + args: TrainingArguments, state: TrainerState, - control: TrainerControl, # pylint: disable=unused-argument - **kwargs, # pylint: disable=unused-argument + control: TrainerControl, + **kwargs, ): if state.global_step == self.profiler_steps_end: - snapshot = torch.cuda.memory._snapshot() # pylint: disable=protected-access + snapshot = torch.cuda.memory._snapshot() with open(Path(args.output_dir) / "snapshot.pickle", "wb") as fout: dump(snapshot, fout) # tell CUDA to stop recording memory allocations now - torch.cuda.memory._record_memory_history( # pylint: disable=protected-access - enabled=None - ) + torch.cuda.memory._record_memory_history(enabled=None) - def on_train_end( # pylint: disable=unused-argument + def on_train_end( self, - args: TrainingArguments, # pylint: disable=unused-argument + args: TrainingArguments, state: TrainerState, - control: TrainerControl, # pylint: disable=unused-argument - **kwargs, # pylint: disable=unused-argument + control: TrainerControl, + **kwargs, ): # make sure to record if we happen to have more steps than steps to profile if ( state.global_step >= self.profiler_steps_start and state.global_step < self.profiler_steps_end ): - snapshot = torch.cuda.memory._snapshot() # pylint: disable=protected-access + snapshot = torch.cuda.memory._snapshot() with open(Path(args.output_dir) / "snapshot.pickle", "wb") as fout: dump(snapshot, fout) # tell CUDA to stop recording memory allocations now - torch.cuda.memory._record_memory_history( # pylint: disable=protected-access - enabled=None - ) + torch.cuda.memory._record_memory_history(enabled=None) diff --git a/src/axolotl/utils/callbacks/qat.py b/src/axolotl/utils/callbacks/qat.py index cf4d9a9373..70746d6beb 100644 --- a/src/axolotl/utils/callbacks/qat.py +++ b/src/axolotl/utils/callbacks/qat.py @@ -38,9 +38,7 @@ class QATCallback(TrainerCallback): def __init__(self, cfg: QATConfig): self.cfg = cfg - def on_step_begin( - self, args, state, control, model, **kwargs - ): # pylint: disable=unused-argument + def on_step_begin(self, args, state, control, model, **kwargs): if self.cfg.fake_quant_after_n_steps is not None: if state.global_step == 0: LOG.info(f"Disabling fake quantization at step {state.global_step}") diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index c9613c39b1..534d7c4a4c 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -37,7 +37,7 @@ def get_device(): return f"npu:{cfg.local_rank}" raise SystemError("No CUDA/mps/npu device found") - except Exception: # pylint: disable=broad-exception-caught + except Exception: return "cpu" cfg.device = get_device() @@ -266,8 +266,8 @@ def validate_config( if cfg.plugins: ( - AxolotlConfigWCapabilities, # pylint: disable=invalid-name - AxolotlInputConfig, # pylint: disable=invalid-name + AxolotlConfigWCapabilities, + AxolotlInputConfig, ) = merge_input_args() # Convert datasets to proper format if needed diff --git a/src/axolotl/utils/ctx_managers/__init__.py b/src/axolotl/utils/ctx_managers/__init__.py index e544621b58..6ffda9e557 100644 --- a/src/axolotl/utils/ctx_managers/__init__.py +++ b/src/axolotl/utils/ctx_managers/__init__.py @@ -1,6 +1,5 @@ """Init for context manager submodule""" -# pylint: disable=unused-import # flake8: noqa from .sequence_parallel import SequenceParallelContextManager diff --git a/src/axolotl/utils/ctx_managers/sequence_parallel.py b/src/axolotl/utils/ctx_managers/sequence_parallel.py index 029d991dd1..1ec91ae2ac 100644 --- a/src/axolotl/utils/ctx_managers/sequence_parallel.py +++ b/src/axolotl/utils/ctx_managers/sequence_parallel.py @@ -26,7 +26,7 @@ def apply_sequence_parallelism( local_rank: int, local_world_size: int, gradient_accumulation_steps: int, - ring_attn_func: RingAttnFunc, # pylint: disable=unused-argument + ring_attn_func: RingAttnFunc, ) -> tuple[dict[str, torch.Tensor], int, int]: """ Apply sequence parallelism slicing to a batch. diff --git a/src/axolotl/utils/data/pretraining.py b/src/axolotl/utils/data/pretraining.py index f3422f9908..72c5536e99 100644 --- a/src/axolotl/utils/data/pretraining.py +++ b/src/axolotl/utils/data/pretraining.py @@ -67,7 +67,7 @@ def encode_pretraining( buffer_labels = torch.tensor([], dtype=torch.long) buffer_attention_mask = torch.tensor([], dtype=torch.long) - for ids, labels, mask in zip(input_ids, targets, attention_mask): + for ids, labels, mask in zip(input_ids, targets, attention_mask, strict=False): if buffer_input_ids.numel() == max_tokens: new_input_ids.append(buffer_input_ids) new_labels.append(buffer_labels) @@ -247,7 +247,6 @@ def encode_packed_pretraining( batch_size: int = 4, multipack_attn: Optional[bool] = True, ) -> Dict[str, List]: - # pylint: disable=duplicate-code # tokenize all the examples # rows get split with stride (overlap) train_dataset = ds_wrapper(dataset=Dataset.from_dict(examples))[0] diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index 6fd5397586..d371c9acb2 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -255,7 +255,6 @@ def _load_split(cfg: DictDefault, split: Literal["train", "test"]) -> Dataset: return dataset -# pylint: disable=duplicate-code def _load_or_create_dataset_split( cfg: DictDefault, tokenizer: PreTrainedTokenizer, split: Literal["train", "test"] ) -> Dataset: diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 21c8e472b3..1d7d37f155 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -337,7 +337,7 @@ def generate_split_fingerprints( dataset: Dataset, val_set_size: int | float, seed: int ) -> tuple[str, str]: """Generate consistent fingerprints for train/test splits.""" - fingerprint = dataset._fingerprint # pylint: disable=protected-access + fingerprint = dataset._fingerprint train_hash_input = f"{fingerprint}|{val_set_size}|train|{seed}" test_hash_input = f"{fingerprint}|{val_set_size}|test|{seed}" @@ -497,7 +497,7 @@ def try_load_from_hub( token=cfg.hf_use_auth_token, ) return dataset[split] - except Exception: # pylint: disable=broad-except # nosec + except Exception: LOG.info("Unable to find prepared dataset in HuggingFace Hub") return None diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index 856a609c74..4868576a04 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -44,7 +44,7 @@ def retry_on_request_exceptions( def decorator(func): @functools.wraps(func) - def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements + def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) diff --git a/src/axolotl/utils/data/wrappers.py b/src/axolotl/utils/data/wrappers.py index b6dc42c710..cb9e2c6b40 100644 --- a/src/axolotl/utils/data/wrappers.py +++ b/src/axolotl/utils/data/wrappers.py @@ -54,7 +54,6 @@ def handle_unknown_dataset_strategy(dataset_config: DictDefault) -> NoReturn: raise ValueError(error_message) -# pylint: disable=too-many-return-statements def get_dataset_wrapper( dataset_config: DictDefault, tokenizer: PreTrainedTokenizer, @@ -62,7 +61,7 @@ def get_dataset_wrapper( dataset_base_type: str | None, dataset: Dataset | IterableDataset, dataset_prompt_style: str | None = None, - processor: ProcessorMixin | None = None, # pylint: disable=unused-argument + processor: ProcessorMixin | None = None, ) -> tuple[Dataset | IterableDataset, Prompter | None]: """Create an appropriate dataset wrapper and prompter based on dataset configuration. diff --git a/src/axolotl/utils/dict.py b/src/axolotl/utils/dict.py index c2670dfeb6..7d146c7a91 100644 --- a/src/axolotl/utils/dict.py +++ b/src/axolotl/utils/dict.py @@ -17,15 +17,15 @@ def __or__(self, other): def __setitem__(self, name, value): # workaround for pickle/unpickle issues and __frozen not being available try: - isFrozen = hasattr( # pylint: disable=invalid-name + isFrozen = hasattr(self, "__frozen") and object.__getattribute__( self, "__frozen" - ) and object.__getattribute__(self, "__frozen") + ) except AttributeError: - isFrozen = False # pylint: disable=invalid-name + isFrozen = False if isFrozen and name not in super().keys(): raise KeyError(name) - super(Dict, self).__setitem__(name, value) # pylint: disable=bad-super-call + super(Dict, self).__setitem__(name, value) try: p = object.__getattribute__(self, "__parent") key = object.__getattribute__(self, "__key") diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index 48771fd973..840772d91c 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -15,7 +15,7 @@ is_torch_npu_available, ) -distributed_state = None # pylint: disable=invalid-name +distributed_state = None def get_device_type() -> torch.device: @@ -48,7 +48,7 @@ def get_current_device() -> int: def init_distributed_state(): - global distributed_state # pylint: disable=global-statement + global distributed_state if distributed_state is None: timeout = int(os.environ.get("AXOLOTL_NCCL_TIMEOUT", 1800)) try: @@ -137,7 +137,7 @@ def zero_first(is_main: bool): barrier() -def gather_scalar_from_all_ranks(fn, world_size=1): # pylint: disable=invalid-name +def gather_scalar_from_all_ranks(fn, world_size=1): """ Run a callable 'fn' on all ranks and gather the results on the specified rank. @@ -201,7 +201,7 @@ def broadcast_dict(vals: dict): return vals -def compute_and_broadcast(fn): # pylint: disable=invalid-name +def compute_and_broadcast(fn): """ Compute a value using the function 'fn' only on the specified rank (default is 0). The value is then broadcasted to all other ranks. @@ -234,7 +234,7 @@ def compute_and_broadcast(fn): # pylint: disable=invalid-name return float(value_tensor.item()) -def gather_from_all_ranks(fn, world_size=1): # pylint: disable=invalid-name +def gather_from_all_ranks(fn, world_size=1): """ Run a callable 'fn' on all ranks and gather the results on the specified rank. diff --git a/src/axolotl/utils/environment.py b/src/axolotl/utils/environment.py index 3c83c87cb0..751f7e2534 100644 --- a/src/axolotl/utils/environment.py +++ b/src/axolotl/utils/environment.py @@ -26,7 +26,7 @@ def check_cuda_p2p_ib_support(): for unsupported_device in unsupported_devices ): return False - except Exception: # pylint: disable=broad-except # nosec + except Exception: # nosec B110 pass return True diff --git a/src/axolotl/utils/lora.py b/src/axolotl/utils/lora.py index 759c17ac26..6ae481b6b3 100644 --- a/src/axolotl/utils/lora.py +++ b/src/axolotl/utils/lora.py @@ -15,6 +15,7 @@ """ module to get the state dict of a merged lora model """ + import torch from peft.tuners.tuners_utils import onload_layer from peft.utils import ModulesToSaveWrapper, _get_submodules diff --git a/src/axolotl/utils/mistral/mistral_tokenizer.py b/src/axolotl/utils/mistral/mistral_tokenizer.py index 61cbdc5b0d..0414ece78e 100644 --- a/src/axolotl/utils/mistral/mistral_tokenizer.py +++ b/src/axolotl/utils/mistral/mistral_tokenizer.py @@ -53,7 +53,7 @@ def _set_mode(self, mode: ValidationMode): """ # Check if MistralRequestValidator has a _mode attribute. # This is a private API and may change in the future. - # pylint: disable=protected-access + from mistral_common.protocol.instruct.validator import MistralRequestValidator if not ( @@ -74,7 +74,7 @@ def _set_mode(self, mode: ValidationMode): def apply_chat_template( # type: ignore self, conversation: list[dict] | list[list[dict]], - chat_template: str | None = None, # pylint: disable=unused-argument + chat_template: str | None = None, add_generation_prompt: bool = False, **kwargs, ) -> str | list[int]: diff --git a/src/axolotl/utils/model_shard_quant.py b/src/axolotl/utils/model_shard_quant.py index 5c5006eda3..f20a9625ed 100644 --- a/src/axolotl/utils/model_shard_quant.py +++ b/src/axolotl/utils/model_shard_quant.py @@ -46,13 +46,11 @@ def _replace_linear( if isinstance(module, torch.nn.Linear) and name not in skip_modules: if issubclass(linear_replacement, Linear4bit): - model._modules[name] = ( # pylint: disable=protected-access - linear_replacement( - module.in_features, - module.out_features, - module.bias is not None, - **kwargs, - ) + model._modules[name] = linear_replacement( + module.in_features, + module.out_features, + module.bias is not None, + **kwargs, ) else: raise ValueError( @@ -151,7 +149,7 @@ def load_sharded_model( model_name, use_cache=False, torch_dtype=torch.float32, - _attn_implementation=model_config._attn_implementation, # pylint: disable=protected-access + _attn_implementation=model_config._attn_implementation, trust_remote_code=cfg.trust_remote_code, ) dtype = torch_dtype if not cfg.float32 else None diff --git a/src/axolotl/utils/optimizers/adopt.py b/src/axolotl/utils/optimizers/adopt.py index 6f064abbf5..20ddfa7ec4 100644 --- a/src/axolotl/utils/optimizers/adopt.py +++ b/src/axolotl/utils/optimizers/adopt.py @@ -6,7 +6,6 @@ """ # mypy: ignore-errors -# pylint: skip-file # flake8: noqa # mypy: allow-untyped-decorators # mypy: allow-untyped-defs @@ -288,7 +287,9 @@ def _single_tensor_adopt( assert ( param.device.type == step_t.device.type and param.device.type in capturable_supported_devices - ), f"If capturable=True, params and state_steps must be on supported devices: {capturable_supported_devices}." + ), ( + f"If capturable=True, params and state_steps must be on supported devices: {capturable_supported_devices}." + ) step = step_t if capturable or differentiable else _get_value(step_t) @@ -365,7 +366,9 @@ def _multi_tensor_adopt( p.device.type == step.device.type and p.device.type in capturable_supported_devices for p, step in zip(params, state_steps) - ), f"If capturable=True, params and state_steps must be on supported devices: {capturable_supported_devices}." + ), ( + f"If capturable=True, params and state_steps must be on supported devices: {capturable_supported_devices}." + ) assert grad_scale is None and found_inf is None diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index af62c0a4fe..d079886132 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -268,7 +268,7 @@ def __init__( num_processes: int | None = None, # Number of processes for parallel packing safe_mode: bool = True, # Conservative packing to prevent training instability mp_start_method: str = "fork", - **kwargs, # pylint: disable=unused-argument + **kwargs, ): super().__init__(sampler, batch_size, drop_last) self.batch_size = batch_size @@ -317,9 +317,7 @@ def generate_batches(self, set_stats: bool = False) -> list[list[list[int]]]: return self._batches # Get indices from the sampler - indices = [ # pylint: disable=unnecessary-comprehension - idx for idx in self.sampler - ] + indices = [idx for idx in self.sampler] # Get lengths of the selected sequences lengths = self.lengths[indices] @@ -417,7 +415,7 @@ def calc_sample_packing_eff_est(estimates: list[float]): # Gather efficiency from all ranks and apply the calculation function sample_packing_actual_eff_all = reduce_and_broadcast( - lambda: float(self.efficiency()), # pylint: disable=unnecessary-lambda + lambda: float(self.efficiency()), calc_sample_packing_eff_est, ) diff --git a/src/axolotl/utils/schedulers.py b/src/axolotl/utils/schedulers.py index cdaf922713..83a9930893 100644 --- a/src/axolotl/utils/schedulers.py +++ b/src/axolotl/utils/schedulers.py @@ -107,9 +107,7 @@ def __init__(self, optimizer, num_steps, min_lr, max_lr, last_epoch=-1): self.num_steps = num_steps self.min_lr = min_lr self.max_lr = max_lr - self.q = (max_lr / min_lr) ** ( # pylint: disable=invalid-name - 1 / (num_steps - 1) - ) + self.q = (max_lr / min_lr) ** (1 / (num_steps - 1)) super().__init__(optimizer, last_epoch) def get_lr(self): @@ -310,7 +308,6 @@ def __init__( jagged_restart_anneal_steps: int = 1, min_lr_scale: float = 0.001, ) -> None: - # pylint: disable=duplicate-code self.inner_schedule = inner_schedule self.restarts_steps = jagged_restart_steps self.warmup_steps = jagged_restart_warmup_steps diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index a607b3dca2..4d660d4b75 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1,7 +1,5 @@ """Module with Pydantic models for configuration.""" -# pylint: disable=too-many-lines - from typing import Annotated, Any, Literal from annotated_types import MinLen @@ -51,7 +49,6 @@ LOG = get_logger(__name__) -# pylint: disable=too-many-ancestors class AxolotlInputConfig( ModelInputConfig, ModelOutputConfig, @@ -124,10 +121,10 @@ class AxolotlInputConfig( }, ) trl: TRLConfig | None = Field( - default_factory=lambda: TRLConfig(), # pylint: disable=unnecessary-lambda + default_factory=lambda: TRLConfig(), ) vllm: VllmConfig | None = Field( - default_factory=lambda: VllmConfig(), # pylint: disable=unnecessary-lambda + default_factory=lambda: VllmConfig(), ) qat: QATConfig | None = None quantization: PTQConfig | None = None @@ -1035,7 +1032,6 @@ def check_sample_packing_w_sdpa_bf16(cls, data): return data - # pylint: disable=duplicate-code @model_validator(mode="before") @classmethod def check_multigpu_unsloth(cls, data): @@ -1051,7 +1047,6 @@ def check_multigpu_unsloth(cls, data): ) return data - # pylint: disable=duplicate-code @model_validator(mode="before") @classmethod def check_multigpu_lora_kernels(cls, data): diff --git a/src/axolotl/utils/schemas/datasets.py b/src/axolotl/utils/schemas/datasets.py index d9c8042d43..e324687068 100644 --- a/src/axolotl/utils/schemas/datasets.py +++ b/src/axolotl/utils/schemas/datasets.py @@ -203,7 +203,6 @@ def handle_legacy_message_fields(cls, data): @model_validator(mode="before") @classmethod - # pylint: disable=duplicate-code def check_chat_template_config(cls, data): if isinstance(data, BaseModel): data = data.model_dump() diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index cf2a8b484f..8f4718aa96 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -1,7 +1,5 @@ """Enums for Axolotl input config""" -# pylint: disable=invalid-name - from enum import Enum import torch diff --git a/src/axolotl/utils/schemas/training.py b/src/axolotl/utils/schemas/training.py index b1788dcaa5..8e06e82cb3 100644 --- a/src/axolotl/utils/schemas/training.py +++ b/src/axolotl/utils/schemas/training.py @@ -96,9 +96,9 @@ class HyperparametersConfig(BaseModel): "description": "Path to torch distx for optim 'adamw_anyprecision'" }, ) - lr_scheduler: (SchedulerType | Literal["one_cycle"] | Literal["rex"]) | None = ( - SchedulerType.COSINE - ) + lr_scheduler: ( + SchedulerType | Literal["one_cycle"] | Literal["rex"] + ) | None = SchedulerType.COSINE lr_scheduler_kwargs: dict[str, Any] | None = Field( default=None, json_schema_extra={ diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 217244b019..791894990c 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1,7 +1,5 @@ """Module with validation methods for config pydantic model.""" -# pylint: disable=too-many-boolean-expressions - import json import sys import tempfile @@ -16,7 +14,6 @@ from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import ChatTemplate, RingAttnFunc, RLType -# pylint: disable=too-many-lines LOG = get_logger(__name__) @@ -346,7 +343,6 @@ def check_neftune(cls, data): @model_validator(mode="after") def check_fft_possible_bad_config(self): if ( - # pylint: disable=too-many-boolean-expressions not (self.bf16 or self.bfloat16) and (self.fp16 or self.float16) and not self.adapter @@ -460,12 +456,12 @@ def check_tokenizer_use_mistral_common(cls, data): @classmethod def check_mistral_common_import(cls, tokenizer_use_mistral_common): if tokenizer_use_mistral_common: - try: - import mistral_common # noqa: F401 # pylint:disable=unused-import - except ImportError as exception: + import importlib.util + + if importlib.util.find_spec("mistral_common") is None: raise ImportError( "mistral-common is required for mistral models. Please install it with `pip install axolotl` or `pip install -e .`." - ) from exception + ) return tokenizer_use_mistral_common @@ -685,7 +681,7 @@ def check_rl_config_gradient_checkpointing(cls, data): # TODO: SalmanMohammadi # Distributed RL with QLoRA + gradient checkpointing # and use_reentrant = True is broken upstream in TRL - # pylint: disable=too-many-boolean-expressions + if ( data.get("rl") and data.get("gradient_checkpointing") @@ -1252,26 +1248,19 @@ def check_context_parallel_size(self): import transformers.modeling_flash_attention_utils from transformers.utils import is_flash_attn_greater_or_equal - # pylint: disable=protected-access transformers.modeling_flash_attention_utils._flash_supports_window = ( True ) - setattr( - sys.modules["transformers.modeling_flash_attention_utils"], - "_flash_supports_window", - True, - ) - setattr( - sys.modules["transformers.modeling_flash_attention_utils"], - "_flash_supports_window_size", - True, - ) - setattr( - sys.modules["transformers.modeling_flash_attention_utils"], - "is_flash_attn_greater_or_equal", - is_flash_attn_greater_or_equal, - ) - import ring_flash_attn # noqa: F401 # pylint:disable=unused-import + sys.modules[ + "transformers.modeling_flash_attention_utils" + ]._flash_supports_window = True + sys.modules[ + "transformers.modeling_flash_attention_utils" + ]._flash_supports_window_size = True + sys.modules[ + "transformers.modeling_flash_attention_utils" + ].is_flash_attn_greater_or_equal = is_flash_attn_greater_or_equal + import ring_flash_attn # noqa: F401 # Required after monkey-patching except ImportError as exception: raise ImportError( "context_parallel_size > 1 but ring_flash_attn is not installed. " @@ -1336,7 +1325,6 @@ def check_vllm_mode_set(self): return self -# pylint: disable=too-many-ancestors class ValidationMixin( DatasetValidationMixin, AttentionValidationMixin, diff --git a/src/axolotl/utils/tokenization.py b/src/axolotl/utils/tokenization.py index 3526bd5b58..3f44a34292 100644 --- a/src/axolotl/utils/tokenization.py +++ b/src/axolotl/utils/tokenization.py @@ -31,7 +31,7 @@ def check_example_labels(example, tokenizer, text_only=False): # You can compare the input_ids and labels element-wise # Remember to ignore positions with IGNORE_TOKEN_ID (if you use it) or attention_mask equal to 0 colored_tokens = [] - for _, (input_id, label_id) in enumerate(zip(input_ids, labels)): + for _, (input_id, label_id) in enumerate(zip(input_ids, labels, strict=False)): decoded_input_token = tokenizer.decode(input_id) # Choose the color based on whether the label has the ignore value or not color = "red" if label_id == -100 else ("yellow" if label_id == 0 else "green") diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index e424cb55a6..08038cb189 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -496,7 +496,7 @@ def calc_sample_packing_eff_est(estimates: List[float]): return max(estimates) sample_packing_actual_eff_all = reduce_and_broadcast( - lambda: sampler.efficiency(), # pylint: disable=unnecessary-lambda + lambda: sampler.efficiency(), calc_sample_packing_eff_est, ) sample_packing_eff_est = ( diff --git a/src/setuptools_axolotl_dynamic_dependencies.py b/src/setuptools_axolotl_dynamic_dependencies.py index 02a5b8083f..ccd7c72d7d 100644 --- a/src/setuptools_axolotl_dynamic_dependencies.py +++ b/src/setuptools_axolotl_dynamic_dependencies.py @@ -9,7 +9,6 @@ from setuptools.command.build_py import build_py as _build_py -# pylint: disable=duplicate-code def parse_requirements(): _install_requires = [] _dependency_links = [] diff --git a/tests/cli/test_cli_evaluate.py b/tests/cli/test_cli_evaluate.py index a191bf957e..e8b88625a1 100644 --- a/tests/cli/test_cli_evaluate.py +++ b/tests/cli/test_cli_evaluate.py @@ -1,7 +1,5 @@ """Tests for evaluate CLI command.""" -# pylint: disable=duplicate-code - from unittest.mock import patch from axolotl.cli.main import cli @@ -31,7 +29,6 @@ def test_evaluate_basic_execution_no_accelerate( config_path = tmp_path / "config.yml" config_path.write_text(valid_test_config) - # pylint: disable=duplicate-code with patch("axolotl.cli.evaluate.do_evaluate") as mock_evaluate: result = cli_runner.invoke( cli, diff --git a/tests/cli/test_cli_inference.py b/tests/cli/test_cli_inference.py index 3394c189d5..807dc7fa35 100644 --- a/tests/cli/test_cli_inference.py +++ b/tests/cli/test_cli_inference.py @@ -1,7 +1,5 @@ """pytest tests for axolotl CLI inference command.""" -# pylint: disable=duplicate-code - from unittest.mock import patch from axolotl.cli.main import cli diff --git a/tests/cli/test_cli_merge_sharded_fsdp_weights.py b/tests/cli/test_cli_merge_sharded_fsdp_weights.py index 4f6a973ea4..de13b28ed7 100644 --- a/tests/cli/test_cli_merge_sharded_fsdp_weights.py +++ b/tests/cli/test_cli_merge_sharded_fsdp_weights.py @@ -1,7 +1,5 @@ """pytest tests for axolotl CLI merge_sharded_fsdp_weights command.""" -# pylint: disable=duplicate-code - from unittest.mock import patch from axolotl.cli.main import cli diff --git a/tests/cli/test_cli_train.py b/tests/cli/test_cli_train.py index d4d90f57f8..1251ab3c03 100644 --- a/tests/cli/test_cli_train.py +++ b/tests/cli/test_cli_train.py @@ -1,7 +1,5 @@ """Tests for train CLI command.""" -# pylint: disable=duplicate-code - from unittest.mock import MagicMock, patch from axolotl.cli.main import cli diff --git a/tests/cli/test_utils.py b/tests/cli/test_utils.py index a3e4e98870..431c35c3ce 100644 --- a/tests/cli/test_utils.py +++ b/tests/cli/test_utils.py @@ -1,7 +1,5 @@ """pytest tests for axolotl CLI utils.""" -# pylint: disable=redefined-outer-name - import json from unittest.mock import Mock, patch @@ -25,7 +23,7 @@ def mock_responses(): """Mock responses for API and file downloads""" - def mock_get(url, timeout=None): # pylint: disable=unused-argument + def mock_get(url, timeout=None): response = Mock() if "api.github.com" in url: response.text = json.dumps(MOCK_TREE_RESPONSE) @@ -93,21 +91,21 @@ def assert_launcher_args_in_command( called_cmd = mock_subprocess_call.call_args.args[0] # Verify launcher - assert ( - called_cmd[0] == launcher - ), f"Expected launcher {launcher}, got {called_cmd[0]}" + assert called_cmd[0] == launcher, ( + f"Expected launcher {launcher}, got {called_cmd[0]}" + ) # Verify launcher args are present for arg in expected_launcher_args: - assert ( - arg in called_cmd - ), f"Expected launcher arg '{arg}' not found in command: {called_cmd}" + assert arg in called_cmd, ( + f"Expected launcher arg '{arg}' not found in command: {called_cmd}" + ) # Verify module is present assert "-m" in called_cmd, "Expected -m flag for module execution" - assert ( - command_module in called_cmd - ), f"Expected module {command_module} not found in command: {called_cmd}" + assert command_module in called_cmd, ( + f"Expected module {command_module} not found in command: {called_cmd}" + ) def assert_no_launcher_args_contamination(mock_subprocess_call, launcher: str): @@ -126,17 +124,17 @@ def assert_no_launcher_args_contamination(mock_subprocess_call, launcher: str): launch_idx = called_cmd.index("launch") m_idx = called_cmd.index("-m") launcher_section = called_cmd[launch_idx + 1 : m_idx] - assert ( - len(launcher_section) == 0 - ), f"Unexpected launcher args found: {launcher_section}" + assert len(launcher_section) == 0, ( + f"Unexpected launcher args found: {launcher_section}" + ) elif launcher == "torchrun": # For torchrun, launcher args should be between 'torchrun' and '-m' torchrun_idx = called_cmd.index("torchrun") m_idx = called_cmd.index("-m") launcher_section = called_cmd[torchrun_idx + 1 : m_idx] - assert ( - len(launcher_section) == 0 - ), f"Unexpected launcher args found: {launcher_section}" + assert len(launcher_section) == 0, ( + f"Unexpected launcher args found: {launcher_section}" + ) @pytest.fixture diff --git a/tests/conftest.py b/tests/conftest.py index 9e1af318d3..98847ebad8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,10 +33,9 @@ def retry_on_request_exceptions(max_retries=3, delay=1): - # pylint: disable=duplicate-code def decorator(func): @functools.wraps(func) - def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements + def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) @@ -171,7 +170,7 @@ def download_argilla_distilabel_intel_orca_dpo_dataset(): # @disable_hf_offline # def dataset_fozzie_alpaca_dpo_dataset( # download_fozzie_alpaca_dpo_dataset, -# ): # pylint: disable=unused-argument,redefined-outer-name +# ): # return load_dataset("fozziethebeat/alpaca_messages_2k_dpo_test", split="train") # # @@ -179,7 +178,7 @@ def download_argilla_distilabel_intel_orca_dpo_dataset(): # @disable_hf_offline # def dataset_fozzie_alpaca_dpo_dataset_rev_ea82cff( # download_fozzie_alpaca_dpo_dataset, -# ): # pylint: disable=unused-argument,redefined-outer-name +# ): # return load_dataset( # "fozziethebeat/alpaca_messages_2k_dpo_test", split="train", revision="ea82cff" # ) @@ -359,7 +358,7 @@ def download_llama32_1b_model_fixture(): @enable_hf_offline def tokenizer_huggyllama( download_huggyllama_model_fixture, -): # pylint: disable=unused-argument,redefined-outer-name +): tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") tokenizer.pad_token = "" @@ -370,7 +369,7 @@ def tokenizer_huggyllama( @enable_hf_offline def tokenizer_huggyllama_w_special_tokens( tokenizer_huggyllama, -): # pylint: disable=redefined-outer-name +): tokenizer_huggyllama.add_special_tokens( { "bos_token": "", @@ -386,7 +385,7 @@ def tokenizer_huggyllama_w_special_tokens( @enable_hf_offline def tokenizer_llama2_7b( download_llama2_model_fixture, -): # pylint: disable=unused-argument,redefined-outer-name +): tokenizer = AutoTokenizer.from_pretrained("NousResearch/Llama-2-7b-hf") return tokenizer @@ -396,7 +395,7 @@ def tokenizer_llama2_7b( @enable_hf_offline def tokenizer_mistral_7b_instruct( download_mlx_mistral_7b_model_fixture, -): # pylint: disable=unused-argument,redefined-outer-name +): return AutoTokenizer.from_pretrained("casperhansen/mistral-7b-instruct-v0.1-awq") @@ -442,9 +441,7 @@ def cleanup_monkeypatches(): # original_fa2_forward = LlamaFlashAttention2.forward original_llama_attn_forward = LlamaAttention.forward original_llama_forward = LlamaForCausalLM.forward - original_trainer_inner_training_loop = ( - Trainer._inner_training_loop # pylint: disable=protected-access - ) + original_trainer_inner_training_loop = Trainer._inner_training_loop original_trainer_training_step = Trainer.training_step # monkey patches can happen inside the tests yield @@ -452,9 +449,7 @@ def cleanup_monkeypatches(): # LlamaFlashAttention2.forward = original_fa2_forward LlamaAttention.forward = original_llama_attn_forward LlamaForCausalLM.forward = original_llama_forward - Trainer._inner_training_loop = ( # pylint: disable=protected-access - original_trainer_inner_training_loop - ) + Trainer._inner_training_loop = original_trainer_inner_training_loop Trainer.training_step = original_trainer_training_step # Reset other known monkeypatches @@ -490,7 +485,7 @@ def cleanup_monkeypatches(): @pytest.fixture def dataset_winglian_tiny_shakespeare( download_ds_fixture_bundle: Path, -): # pylint: disable=redefined-outer-name +): ds_path = download_ds_fixture_bundle / "winglian__tiny-shakespeare" return datasets.load_from_disk(ds_path) @@ -498,7 +493,7 @@ def dataset_winglian_tiny_shakespeare( @pytest.fixture def dataset_tatsu_lab_alpaca( download_ds_fixture_bundle: Path, -): # pylint: disable=redefined-outer-name +): ds_path = download_ds_fixture_bundle / "tatsu-lab__alpaca" return datasets.load_from_disk(ds_path)["train"] @@ -506,7 +501,7 @@ def dataset_tatsu_lab_alpaca( @pytest.fixture def dataset_mhenrichsen_alpaca_2k_test( download_ds_fixture_bundle: Path, -): # pylint: disable=redefined-outer-name +): ds_path = download_ds_fixture_bundle / "mhenrichsen__alpaca_2k_test" return datasets.load_from_disk(ds_path)["train"] @@ -514,7 +509,7 @@ def dataset_mhenrichsen_alpaca_2k_test( @pytest.fixture def dataset_argilla_ultrafeedback_binarized_preferences_cleaned( download_ds_fixture_bundle: Path, -): # pylint: disable=redefined-outer-name +): ds_path = ( download_ds_fixture_bundle / "argilla__ultrafeedback-binarized-preferences-cleaned" @@ -525,7 +520,7 @@ def dataset_argilla_ultrafeedback_binarized_preferences_cleaned( @pytest.fixture def dataset_fozziethebeat_alpaca_messages_2k_dpo_test( download_ds_fixture_bundle: Path, -): # pylint: disable=redefined-outer-name +): ds_path = download_ds_fixture_bundle / "fozziethebeat__alpaca_messages_2k_dpo_test" return datasets.load_from_disk(ds_path)["train"] @@ -533,7 +528,7 @@ def dataset_fozziethebeat_alpaca_messages_2k_dpo_test( @pytest.fixture def dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff( download_ds_fixture_bundle: Path, -): # pylint: disable=redefined-outer-name +): ds_path = ( download_ds_fixture_bundle / "fozziethebeat__alpaca_messages_2k_dpo_test__rev_ea82cff" @@ -557,7 +552,7 @@ def fixture_min_base_cfg(): ) -# # pylint: disable=redefined-outer-name,unused-argument +# @pytest.mark.skipif( os.environ.get("AXOLOTL_IS_CI_CACHE_PRELOAD", "-1") != "1", reason="Not running in CI cache preload", diff --git a/tests/constants.py b/tests/constants.py index e024e6920e..cd75bd3393 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -3,6 +3,7 @@ This module contains constants and configuration dictionaries used for datasets and other utilities in the Axolotl project, specifically for testing. """ + # Configuration for Alpaca Messages Dataset ALPACA_MESSAGES_CONFIG_OG = { "path": "fozziethebeat/alpaca_messages_2k_dpo_test", diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index fab01a644a..6428aa9771 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -1,7 +1,5 @@ """Unit tests for axolotl.core.builders""" -# pylint: disable=protected-access - import sys from pathlib import Path from unittest.mock import patch @@ -330,7 +328,6 @@ def rand_reward_func(prompts, completions) -> list[float]: ) def test_grpo_training_arguments(self, grpo_cfg, model, tokenizer, tmp_path): - rewards_dir = tmp_path / "rewards_test" self._write_rewards_file(rewards_dir) @@ -477,7 +474,7 @@ def test_custom_optimizer_cls_and_kwargs( assert trainer.optimizer_cls_and_kwargs is not None - from axolotl.contribs.mit.muon import ( # pylint: disable=no-name-in-module + from axolotl.contribs.mit.muon import ( Muon, MuonOptimizerFactory, ) @@ -559,7 +556,7 @@ def test_custom_optimizer_cls_and_kwargs( assert trainer.optimizer_cls_and_kwargs is not None - from axolotl.contribs.mit.muon import ( # pylint: disable=no-name-in-module + from axolotl.contribs.mit.muon import ( Muon, MuonOptimizerFactory, ) @@ -599,6 +596,6 @@ def test_trainer_cls_is_not_none_with_plugin(self, kto_cfg, model, tokenizer): except TypeError as e: # Error raised if trainer_cls is None assert "'tuple' object has no attribute 'config'" not in str(e) - except Exception: # pylint: disable=broad-exception-caught + except Exception: # Another error happens, so we passed trainer_cls to builder pass diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py index 34e6c96447..1ba05077cf 100644 --- a/tests/e2e/integrations/test_cut_cross_entropy.py +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -12,8 +12,6 @@ from ..utils import check_model_output_exists -# pylint: disable=duplicate-code - @pytest.fixture() def min_cfg(temp_dir): @@ -53,7 +51,6 @@ class TestCutCrossEntropyIntegration: e2e tests for cut_cross_entropy integration with Axolotl """ - # pylint: disable=redefined-outer-name def test_llama_w_cce(self, min_cfg, temp_dir): cfg = DictDefault(min_cfg) cfg = validate_config(cfg) @@ -69,7 +66,6 @@ def test_llama_w_cce(self, min_cfg, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) - # pylint: disable=redefined-outer-name def test_qwen2_w_cce(self, temp_dir): cfg = DictDefault( { diff --git a/tests/e2e/integrations/test_fp8.py b/tests/e2e/integrations/test_fp8.py index 0302b7e35b..7db63cc4d3 100644 --- a/tests/e2e/integrations/test_fp8.py +++ b/tests/e2e/integrations/test_fp8.py @@ -18,7 +18,7 @@ class FP8IntegrationTestCase: @require_torch_2_7_0 def test_fp8_single_gpu_smoke(self, temp_dir): """Smoke test for single GPU FP8 + torch.compile training""" - # pylint: disable=duplicate-code + cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -53,7 +53,6 @@ def test_fp8_single_gpu_smoke(self, temp_dir): } ) - # pylint: disable=duplicate-code cfg = validate_config(cfg) normalize_config(cfg) dataset_meta = load_datasets(cfg=cfg) diff --git a/tests/e2e/integrations/test_hooks.py b/tests/e2e/integrations/test_hooks.py index 8743efb981..b85505caa4 100644 --- a/tests/e2e/integrations/test_hooks.py +++ b/tests/e2e/integrations/test_hooks.py @@ -28,85 +28,81 @@ def __init__(self): except FileNotFoundError: pass - def post_trainer_create(self, cfg, trainer): # pylint: disable=unused-argument + def post_trainer_create(self, cfg, trainer): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: f.write("post_trainer_create\n") - def pre_model_load(self, cfg): # pylint: disable=unused-argument + def pre_model_load(self, cfg): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: f.write("pre_model_load\n") - def post_model_build(self, cfg, model): # pylint: disable=unused-argument + def post_model_build(self, cfg, model): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: f.write("post_model_build\n") - def pre_lora_load(self, cfg, model): # pylint: disable=unused-argument + def pre_lora_load(self, cfg, model): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: f.write("pre_lora_load\n") - def post_lora_load(self, cfg, model): # pylint: disable=unused-argument + def post_lora_load(self, cfg, model): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: f.write("post_lora_load\n") - def post_model_load(self, cfg, model): # pylint: disable=unused-argument + def post_model_load(self, cfg, model): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: f.write("post_model_load\n") - def create_optimizer(self, cfg, trainer): # pylint: disable=unused-argument + def create_optimizer(self, cfg, trainer): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: f.write("create_optimizer\n") - def get_trainer_cls(self, cfg): # pylint: disable=unused-argument + def get_trainer_cls(self, cfg): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: f.write("get_trainer_cls\n") - def create_lr_scheduler( - self, cfg, trainer, optimizer, num_training_steps - ): # pylint: disable=unused-argument + def create_lr_scheduler(self, cfg, trainer, optimizer, num_training_steps): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: f.write("create_lr_scheduler\n") - def add_callbacks_pre_trainer(self, cfg, model): # pylint: disable=unused-argument + def add_callbacks_pre_trainer(self, cfg, model): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: f.write("add_callbacks_pre_trainer\n") return [] - def add_callbacks_post_trainer( - self, cfg, trainer - ): # pylint: disable=unused-argument + def add_callbacks_post_trainer(self, cfg, trainer): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: f.write("add_callbacks_post_trainer\n") return [] - def post_train(self, cfg, model): # pylint: disable=unused-argument + def post_train(self, cfg, model): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: f.write("post_train\n") - def post_train_unload(self, cfg): # pylint: disable=unused-argument + def post_train_unload(self, cfg): with open( self.base_dir.joinpath("plugin_hooks.log"), "a", encoding="utf-8" ) as f: @@ -119,7 +115,6 @@ class TestPluginHooks: """ def test_plugin_hooks(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py index 1ac3b537e7..98383614b1 100644 --- a/tests/e2e/integrations/test_kd.py +++ b/tests/e2e/integrations/test_kd.py @@ -81,7 +81,7 @@ class TestKnowledgeDistillation: @require_torch_2_5_1 def test_llama_kd(self, temp_dir, kd_min_cfg): cfg = DictDefault(kd_min_cfg) - # pylint: disable=duplicate-code + # write cfg to yaml file Path(temp_dir).mkdir(parents=True, exist_ok=True) with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: @@ -123,7 +123,7 @@ def test_llama_lora_kd(self, temp_dir, kd_min_cfg, load_in_8bit): } | kd_min_cfg ) - # pylint: disable=duplicate-code + # write cfg to yaml file Path(temp_dir).mkdir(parents=True, exist_ok=True) with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: diff --git a/tests/e2e/integrations/test_liger.py b/tests/e2e/integrations/test_liger.py index b1f5befdd4..2859699633 100644 --- a/tests/e2e/integrations/test_liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -17,7 +17,6 @@ class LigerIntegrationTestCase: @require_torch_2_4_1 def test_llama_wo_flce(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -53,7 +52,7 @@ def test_llama_wo_flce(self, temp_dir): "save_first_step": False, } ) - # pylint: disable=duplicate-code + cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) @@ -64,7 +63,6 @@ def test_llama_wo_flce(self, temp_dir): @require_torch_2_4_1 def test_llama_w_flce(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -100,7 +98,7 @@ def test_llama_w_flce(self, temp_dir): "save_first_step": False, } ) - # pylint: disable=duplicate-code + cfg = validate_config(cfg) prepare_plugins(cfg) normalize_config(cfg) diff --git a/tests/e2e/kernels/test_geglu.py b/tests/e2e/kernels/test_geglu.py index 4094a8ce79..78ba74c0e8 100644 --- a/tests/e2e/kernels/test_geglu.py +++ b/tests/e2e/kernels/test_geglu.py @@ -85,6 +85,6 @@ def test_geglu_inplace_preservation(): assert not torch.equal(gate, gate_copy), "Gate should be modified in-place" assert not torch.equal(up, up_copy), "Up should be modified in-place" - assert not torch.equal( - grad_output, grad_copy - ), "Grad output should be modified in-place" + assert not torch.equal(grad_output, grad_copy), ( + "Grad output should be modified in-place" + ) diff --git a/tests/e2e/kernels/test_lora.py b/tests/e2e/kernels/test_lora.py index cd6131ff1d..9baceb668b 100644 --- a/tests/e2e/kernels/test_lora.py +++ b/tests/e2e/kernels/test_lora.py @@ -1,7 +1,5 @@ """Tests for LoRA custom autograd.""" -# pylint: disable=invalid-name,redefined-outer-name - import pytest import torch from bitsandbytes.functional import QuantState @@ -333,7 +331,7 @@ def test_lora_qkv(sample_tensors): X.requires_grad = True # Test without LoRA adapters - # pylint: disable=duplicate-code + Q1, K1, V1 = LoRA_QKV.apply( X, q_weight, diff --git a/tests/e2e/kernels/test_quantize.py b/tests/e2e/kernels/test_quantize.py index ea91407ef5..60396584cc 100644 --- a/tests/e2e/kernels/test_quantize.py +++ b/tests/e2e/kernels/test_quantize.py @@ -1,7 +1,5 @@ """Tests for quantization utility functions.""" -# pylint: disable=invalid-name - import torch from bitsandbytes.functional import QuantState diff --git a/tests/e2e/kernels/test_swiglu.py b/tests/e2e/kernels/test_swiglu.py index 60fdafb79b..58d5e04a73 100644 --- a/tests/e2e/kernels/test_swiglu.py +++ b/tests/e2e/kernels/test_swiglu.py @@ -1,7 +1,5 @@ """Tests for SwiGLU activation function Triton kernels.""" -# pylint: disable=duplicate-code - import torch import torch.nn.functional as F @@ -74,6 +72,6 @@ def test_swiglu_inplace_preservation(): assert not torch.equal(gate, gate_copy), "Gate should be modified in-place" assert not torch.equal(up, up_copy), "Up should be modified in-place" - assert not torch.equal( - grad_output, grad_copy - ), "Grad output should be modified in-place" + assert not torch.equal(grad_output, grad_copy), ( + "Grad output should be modified in-place" + ) diff --git a/tests/e2e/multigpu/solo/test_flex.py b/tests/e2e/multigpu/solo/test_flex.py index cbdf8de96b..881d75c25f 100644 --- a/tests/e2e/multigpu/solo/test_flex.py +++ b/tests/e2e/multigpu/solo/test_flex.py @@ -31,7 +31,6 @@ class TestPackedFlex: @require_torch_2_6_0 def test_loss_llama(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index 92e0f70407..b48eb30e1f 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -80,7 +80,7 @@ def start_vllm( cmd_env = env.copy() cmd_env.update({"VLLM_LOGGING_CONFIG_PATH": vllm_logging_json}) # start `trl vllm-serve` command in the background and capture the process id - process = subprocess.Popen( # pylint: disable=consider-using-with + process = subprocess.Popen( cmd, env=cmd_env, stdout=subprocess.DEVNULL if quiet else subprocess.PIPE, diff --git a/tests/e2e/multigpu/test_eval.py b/tests/e2e/multigpu/test_eval.py index 4f86278ffb..504659a3a1 100644 --- a/tests/e2e/multigpu/test_eval.py +++ b/tests/e2e/multigpu/test_eval.py @@ -21,7 +21,6 @@ class TestMultiGPUEval: """ def test_eval_sample_packing(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -93,7 +92,6 @@ def test_eval_sample_packing(self, temp_dir): check_tensorboard(temp_dir + "/runs", "eval/loss", 2.5, "Eval Loss is too high") def test_eval(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/multigpu/test_fp8_fsdp2.py b/tests/e2e/multigpu/test_fp8_fsdp2.py index f7fa29a314..dc369f3ded 100644 --- a/tests/e2e/multigpu/test_fp8_fsdp2.py +++ b/tests/e2e/multigpu/test_fp8_fsdp2.py @@ -1,7 +1,5 @@ """Test module for FP8 mixed precision with FSDP2 multi-GPU functionality.""" -# pylint: disable=duplicate-code - import os from pathlib import Path @@ -28,9 +26,9 @@ def verify_fp8_training_success(temp_dir): assert len(model_files) > 0, "No model files found - training may have failed" checkpoint_files = list(output_path.glob("checkpoint-*")) - assert ( - len(checkpoint_files) > 0 - ), "No checkpoint files found - training may have failed" + assert len(checkpoint_files) > 0, ( + "No checkpoint files found - training may have failed" + ) tb_log_path = most_recent_subdir(temp_dir + "/runs") if tb_log_path: @@ -42,9 +40,9 @@ def verify_fp8_training_success(temp_dir): train_loss_df = df[df.tag == "train/train_loss"] if len(train_loss_df) > 0: final_loss = train_loss_df.value.values[-1] - assert not torch.isnan( - torch.tensor(final_loss) - ), f"Training loss is NaN: {final_loss}" + assert not torch.isnan(torch.tensor(final_loss)), ( + f"Training loss is NaN: {final_loss}" + ) class TestFP8FSDP2: diff --git a/tests/e2e/multigpu/test_fsdp1.py b/tests/e2e/multigpu/test_fsdp1.py index fe0badbe21..cb92c80b5a 100644 --- a/tests/e2e/multigpu/test_fsdp1.py +++ b/tests/e2e/multigpu/test_fsdp1.py @@ -1,7 +1,5 @@ """Test module for FSDP1 multi-GPU functionality.""" -# pylint: disable=duplicate-code - import os from pathlib import Path @@ -29,9 +27,9 @@ def verify_training_success(temp_dir): assert len(model_files) > 0, "No model files found - training may have failed" checkpoint_files = list(output_path.glob("checkpoint-*")) - assert ( - len(checkpoint_files) > 0 - ), "No checkpoint files found - training may have failed" + assert len(checkpoint_files) > 0, ( + "No checkpoint files found - training may have failed" + ) tb_log_path = most_recent_subdir(temp_dir + "/runs") if tb_log_path: @@ -43,9 +41,9 @@ def verify_training_success(temp_dir): train_loss_df = df[df.tag == "train/train_loss"] if len(train_loss_df) > 0: final_loss = train_loss_df.value.values[-1] - assert not torch.isnan( - torch.tensor(final_loss) - ), f"Training loss is NaN: {final_loss}" + assert not torch.isnan(torch.tensor(final_loss)), ( + f"Training loss is NaN: {final_loss}" + ) class TestFSDP1: diff --git a/tests/e2e/multigpu/test_fsdp2.py b/tests/e2e/multigpu/test_fsdp2.py index 0bb255266e..8b7ee710e5 100644 --- a/tests/e2e/multigpu/test_fsdp2.py +++ b/tests/e2e/multigpu/test_fsdp2.py @@ -1,7 +1,5 @@ """Test module for FSDP2 multi-GPU functionality.""" -# pylint: disable=duplicate-code - import os from pathlib import Path @@ -29,9 +27,9 @@ def verify_training_success(temp_dir): assert len(model_files) > 0, "No model files found - training may have failed" checkpoint_files = list(output_path.glob("checkpoint-*")) - assert ( - len(checkpoint_files) > 0 - ), "No checkpoint files found - training may have failed" + assert len(checkpoint_files) > 0, ( + "No checkpoint files found - training may have failed" + ) tb_log_path = most_recent_subdir(temp_dir + "/runs") if tb_log_path: @@ -43,9 +41,9 @@ def verify_training_success(temp_dir): train_loss_df = df[df.tag == "train/train_loss"] if len(train_loss_df) > 0: final_loss = train_loss_df.value.values[-1] - assert not torch.isnan( - torch.tensor(final_loss) - ), f"Training loss is NaN: {final_loss}" + assert not torch.isnan(torch.tensor(final_loss)), ( + f"Training loss is NaN: {final_loss}" + ) class TestFSDP2: diff --git a/tests/e2e/multigpu/test_gemma3.py b/tests/e2e/multigpu/test_gemma3.py index 4a7b101a83..51ec68b116 100644 --- a/tests/e2e/multigpu/test_gemma3.py +++ b/tests/e2e/multigpu/test_gemma3.py @@ -29,7 +29,6 @@ class TestMultiGPUGemma3: """ def test_lora_ddp_packed(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "axolotl-mirrors/gemma-3-4b-pt", diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index aab14dcc4f..ad15d628be 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -35,7 +35,6 @@ class TestMultiGPULlama: """ def test_lora_ddp(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -99,7 +98,6 @@ def test_lora_ddp(self, temp_dir): [1, 2], ) def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -162,7 +160,6 @@ def test_lora_ddp_packed(self, temp_dir, gradient_accumulation_steps): ) def test_dpo_lora_ddp(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -242,7 +239,6 @@ def test_dpo_lora_ddp(self, temp_dir): ) def test_dpo_qlora_ddp(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -326,7 +322,6 @@ def test_dpo_qlora_ddp(self, temp_dir): [1, 2], ) def test_fsdp(self, temp_dir, gradient_accumulation_steps): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -402,7 +397,6 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): ], ) def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -484,7 +478,6 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): def test_fsdp2_packed( self, temp_dir, attention_backend, fsdp_reshard_after_forward ): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -556,7 +549,6 @@ def test_fsdp2_packed( ) def test_fsdp_qlora_prequant_packed(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "axolotl-ai-co/SmolLM2-135M-bnb-nf4-bf16", @@ -656,7 +648,6 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): def test_ds_zero3_packed( self, temp_dir, gradient_accumulation_steps, deepspeed, qlora ): - # pylint: disable=duplicate-code if qlora: adapter = { "adapter": "qlora", @@ -732,7 +723,6 @@ def test_ds_zero3_packed( [True, False], ) def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): - # pylint: disable=duplicate-code if qlora: adapter = { "adapter": "qlora", @@ -809,7 +799,6 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps, qlora): [True, False], ) def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): - # pylint: disable=duplicate-code if qlora: adapter = { "adapter": "qlora", @@ -880,7 +869,6 @@ def test_ds_zero1_packed(self, temp_dir, gradient_accumulation_steps, qlora): reason="fix untrained tokens brittle with lots of edge cases in latest transformers" ) def test_fix_untrained_tokens(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index 7f1278abf4..7c6ea8a1fa 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -26,7 +26,6 @@ class TestMultiGPURay: @require_torch_lt_2_6_0 def test_lora_ddp(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -90,7 +89,6 @@ def test_lora_ddp(self, temp_dir): [1, 2], ) def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -150,7 +148,6 @@ def test_ds_zero2_packed(self, temp_dir, gradient_accumulation_steps): [1, 2], ) def test_sft_fsdp2_packed(self, temp_dir, gradient_accumulation_steps): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/multigpu/test_tp.py b/tests/e2e/multigpu/test_tp.py index 87a1c6339d..9891a0906f 100644 --- a/tests/e2e/multigpu/test_tp.py +++ b/tests/e2e/multigpu/test_tp.py @@ -19,7 +19,6 @@ class TestTensorParallel: ) @require_torch_2_7_0 def test_fft_sft(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "Qwen/Qwen2.5-0.5B", diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index b4dc5de542..2180eb99d5 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -1,7 +1,5 @@ """Integration tests for LoRA activation and attention kernels.""" -# pylint: disable=redefined-outer-name - from pathlib import Path import pytest @@ -88,7 +86,7 @@ def test_attention_patching_integration(model_name, attention_cls): cfg = DictDefault({"base_model": model_name}) # Store the original implementation - original_forward = getattr(attention_cls, "forward") + original_forward = attention_cls.forward # Apply patch patch_self_attn_lora(cfg) @@ -104,7 +102,7 @@ def test_attention_patching_integration(model_name, attention_cls): assert hasattr(attention_cls, "_original_forward") # Clean up - setattr(attention_cls, "forward", original_forward) + attention_cls.forward = original_forward delattr(attention_cls, "_original_forward") @@ -379,9 +377,9 @@ def test_model_architecture(model_config): # Verify correct activation function layer = patched_model.model.model.layers[0] - assert ( - layer.mlp.forward.__func__ is model_config["expected_activation"] - ), f"Wrong activation for {model_config['name']}" + assert layer.mlp.forward.__func__ is model_config["expected_activation"], ( + f"Wrong activation for {model_config['name']}" + ) # Test forward pass inputs = get_test_inputs(model) @@ -390,12 +388,11 @@ def test_model_architecture(model_config): patched_output = patched_model(inputs).logits # Check outputs match - assert torch.allclose( - original_output, patched_output, rtol=1e-4 - ), f"Outputs don't match for {model_config['name']}" + assert torch.allclose(original_output, patched_output, rtol=1e-4), ( + f"Outputs don't match for {model_config['name']}" + ) -# pylint: disable=duplicate-code def test_kernel_training_integration(temp_dir): """Test model loading with kernel patches enabled.""" from axolotl.cli.utils import load_model_and_tokenizer @@ -563,15 +560,13 @@ def test_kernel_training_integration_dropout_non_zero(temp_dir): model_loader = ModelLoader(cfg, tokenizer) # Apply patch - model_loader.patch_manager._apply_self_attention_lora_patch() # pylint: disable=protected-access + model_loader.patch_manager._apply_self_attention_lora_patch() # Verify patch was not applied assert attention_cls.forward == original_forward_method # Apply apply_lora_kernel_patches - model_loader.patch_manager._apply_lora_kernel_patch( # pylint: disable=protected-access - model - ) + model_loader.patch_manager._apply_lora_kernel_patch(model) # Verify patch was not applied layers = get_layers(model) diff --git a/tests/e2e/patched/test_4d_multipack_llama.py b/tests/e2e/patched/test_4d_multipack_llama.py index 1824443e79..ef28cc4066 100644 --- a/tests/e2e/patched/test_4d_multipack_llama.py +++ b/tests/e2e/patched/test_4d_multipack_llama.py @@ -19,7 +19,6 @@ class Test4dMultipackLlama(unittest.TestCase): @with_temp_dir def test_sdp_lora_packing(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -67,7 +66,6 @@ def test_sdp_lora_packing(self, temp_dir): @with_temp_dir def test_torch_lora_packing(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/patched/test_activation_checkpointing.py b/tests/e2e/patched/test_activation_checkpointing.py index 06e3de2740..ddace8ef14 100644 --- a/tests/e2e/patched/test_activation_checkpointing.py +++ b/tests/e2e/patched/test_activation_checkpointing.py @@ -32,10 +32,9 @@ class TestActivationCheckpointing: def test_activation_checkpointing_offload( self, temp_dir, - fix_checkpoint_after_test, # pylint: disable=unused-argument,redefined-outer-name + fix_checkpoint_after_test, gradient_checkpointing, ): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/patched/test_cli_integrations.py b/tests/e2e/patched/test_cli_integrations.py index 6c908faf10..6eba92689a 100644 --- a/tests/e2e/patched/test_cli_integrations.py +++ b/tests/e2e/patched/test_cli_integrations.py @@ -10,7 +10,6 @@ from axolotl.utils.dict import DictDefault -# pylint: disable=duplicate-code class TestPluginArgs: """ test class for plugin args loaded from the config file diff --git a/tests/e2e/patched/test_fa_xentropy.py b/tests/e2e/patched/test_fa_xentropy.py index 38099b220b..9f46998547 100644 --- a/tests/e2e/patched/test_fa_xentropy.py +++ b/tests/e2e/patched/test_fa_xentropy.py @@ -23,7 +23,6 @@ class TestFAXentropyLlama: [1, 4], ) def test_lora_packing_fa_cross_entropy(self, temp_dir, gradient_accumulation_steps): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/patched/test_falcon_samplepack.py b/tests/e2e/patched/test_falcon_samplepack.py index ef31b11c78..cc50914035 100644 --- a/tests/e2e/patched/test_falcon_samplepack.py +++ b/tests/e2e/patched/test_falcon_samplepack.py @@ -22,7 +22,6 @@ class TestFalconPatched(unittest.TestCase): @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_qlora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "illuin/tiny-random-FalconForCausalLM", @@ -71,7 +70,6 @@ def test_qlora(self, temp_dir): @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_ft(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "illuin/tiny-random-FalconForCausalLM", diff --git a/tests/e2e/patched/test_flattening.py b/tests/e2e/patched/test_flattening.py index fdaab558dc..2c247d4061 100644 --- a/tests/e2e/patched/test_flattening.py +++ b/tests/e2e/patched/test_flattening.py @@ -23,7 +23,6 @@ class TestFAFlattening: [1, 4], ) def test_lora_packing_flattening(self, temp_dir, gradient_accumulation_steps): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/patched/test_fsdp2_qlora.py b/tests/e2e/patched/test_fsdp2_qlora.py index ca17b81d1a..de9c929e16 100644 --- a/tests/e2e/patched/test_fsdp2_qlora.py +++ b/tests/e2e/patched/test_fsdp2_qlora.py @@ -15,7 +15,6 @@ def test_fsdp2_init_patches(self): apply_init_unsharded_param_patch, ) - # pylint: disable=protected-access original_init_sharded = FSDPParam._init_sharded_param original_init_unsharded = FSDPParam.init_unsharded_param @@ -23,11 +22,9 @@ def test_fsdp2_init_patches(self): apply_init_sharded_param_patch() apply_init_unsharded_param_patch() - assert ( - # pylint: disable=protected-access - FSDPParam._init_sharded_param - != original_init_sharded - ), "_init_sharded_param was not patched" - assert ( - FSDPParam.init_unsharded_param != original_init_unsharded - ), "init_unsharded_param was not patched" + assert FSDPParam._init_sharded_param != original_init_sharded, ( + "_init_sharded_param was not patched" + ) + assert FSDPParam.init_unsharded_param != original_init_unsharded, ( + "init_unsharded_param was not patched" + ) diff --git a/tests/e2e/patched/test_fused_llama.py b/tests/e2e/patched/test_fused_llama.py index f0c4f155f5..f0c5df18ad 100644 --- a/tests/e2e/patched/test_fused_llama.py +++ b/tests/e2e/patched/test_fused_llama.py @@ -23,7 +23,6 @@ class TestFusedLlama(unittest.TestCase): @with_temp_dir def test_fft_packing(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py index ba5556a593..0dd748945a 100644 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ b/tests/e2e/patched/test_llama_s2_attention.py @@ -22,7 +22,6 @@ class TestLlamaShiftedSparseAttention(unittest.TestCase): @with_temp_dir def test_lora_s2_attn(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -71,7 +70,6 @@ def test_lora_s2_attn(self, temp_dir): @with_temp_dir def test_fft_s2_attn(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index fdf6adbc6c..1833c750b3 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -22,7 +22,6 @@ class TestLoraLlama(unittest.TestCase): @with_temp_dir def test_lora_packing(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -73,7 +72,6 @@ def test_lora_packing(self, temp_dir): @pytest.mark.skipif(not is_auto_gptq_available(), reason="auto-gptq not available") @with_temp_dir def test_lora_gptq_packed(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "lilmeaty/SmolLM2-135M-Instruct-GPTQ", diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index bea0f9c68c..e03941b07c 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -20,7 +20,6 @@ class TestMistral(unittest.TestCase): @require_torch_2_6_0 @with_temp_dir def test_lora_packing(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", @@ -68,7 +67,6 @@ def test_lora_packing(self, temp_dir): @with_temp_dir def test_ft_packing(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index 09e427abd3..3517ff3db6 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -19,7 +19,6 @@ class TestMixtral(unittest.TestCase): @with_temp_dir def test_qlora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "hf-internal-testing/Mixtral-tiny", @@ -64,7 +63,6 @@ def test_qlora(self, temp_dir): @with_temp_dir def test_ft(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "hf-internal-testing/Mixtral-tiny", diff --git a/tests/e2e/patched/test_model_patches.py b/tests/e2e/patched/test_model_patches.py index b90be23e43..aaaaf5fe2d 100644 --- a/tests/e2e/patched/test_model_patches.py +++ b/tests/e2e/patched/test_model_patches.py @@ -89,5 +89,5 @@ def test_mistral_multipack(self, temp_dir): assert ( "torch.jit" - in transformers.modeling_flash_attention_utils._get_unpad_data.__module__ # pylint: disable=protected-access + in transformers.modeling_flash_attention_utils._get_unpad_data.__module__ ) diff --git a/tests/e2e/patched/test_peft_embeddings.py b/tests/e2e/patched/test_peft_embeddings.py index 4769319aef..374ef97d87 100644 --- a/tests/e2e/patched/test_peft_embeddings.py +++ b/tests/e2e/patched/test_peft_embeddings.py @@ -15,7 +15,6 @@ class TestLlamaPeftEmbeddings: """ def test_peft_embeddings_upcast(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/patched/test_phi_multipack.py b/tests/e2e/patched/test_phi_multipack.py index 1f0ddd6303..77b2d99e5f 100644 --- a/tests/e2e/patched/test_phi_multipack.py +++ b/tests/e2e/patched/test_phi_multipack.py @@ -19,7 +19,6 @@ class TestPhiMultipack(unittest.TestCase): @with_temp_dir def test_ft_packed(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "microsoft/phi-1_5", @@ -67,7 +66,6 @@ def test_ft_packed(self, temp_dir): @with_temp_dir def test_qlora_packed(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "microsoft/phi-1_5", diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index 54b8245eec..747b79dc7c 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -22,7 +22,6 @@ class TestResumeLlama: @require_torch_2_6_0 def test_resume_lora_packed(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py index 2c8ee4eb05..bf00e8a5fc 100644 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ b/tests/e2e/patched/test_unsloth_qlora.py @@ -12,7 +12,6 @@ from ..utils import check_model_output_exists, check_tensorboard -# pylint: disable=duplicate-code @pytest.mark.skip( reason="Unsloth integration will be broken going into latest transformers" ) diff --git a/tests/e2e/solo/test_flex.py b/tests/e2e/solo/test_flex.py index 76364fc0e5..abe8fb69a0 100644 --- a/tests/e2e/solo/test_flex.py +++ b/tests/e2e/solo/test_flex.py @@ -22,7 +22,6 @@ class TestPackedFlex(unittest.TestCase): @require_torch_2_6_0 @with_temp_dir def test_loss_llama(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/solo/test_relora_llama.py b/tests/e2e/solo/test_relora_llama.py index b399b4680b..be77684bac 100644 --- a/tests/e2e/solo/test_relora_llama.py +++ b/tests/e2e/solo/test_relora_llama.py @@ -20,7 +20,6 @@ class TestReLoraLlama(unittest.TestCase): @with_temp_dir def test_relora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -76,9 +75,9 @@ def test_relora(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-100/adapter", cfg) - assert ( - Path(temp_dir) / "checkpoint-100/relora/model.safetensors" - ).exists(), "Relora model checkpoint not found" + assert (Path(temp_dir) / "checkpoint-100/relora/model.safetensors").exists(), ( + "Relora model checkpoint not found" + ) check_tensorboard( temp_dir + "/runs", "train/grad_norm", 0.2, "grad_norm is too high" diff --git a/tests/e2e/test_activation_offloading.py b/tests/e2e/test_activation_offloading.py index 06c5c06569..9df85ab31d 100644 --- a/tests/e2e/test_activation_offloading.py +++ b/tests/e2e/test_activation_offloading.py @@ -11,8 +11,6 @@ from .utils import check_model_output_exists -# pylint: disable=duplicate-code - class TestActivationOffloading: """ @@ -28,7 +26,6 @@ def test_activation_offloading( temp_dir, adapter, ): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/test_deepseekv3.py b/tests/e2e/test_deepseekv3.py index e4a47fb0aa..e11be82657 100644 --- a/tests/e2e/test_deepseekv3.py +++ b/tests/e2e/test_deepseekv3.py @@ -25,7 +25,6 @@ class TestDeepseekV3: [True, False], ) def test_lora_deepseekv3(self, temp_dir, sample_packing): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "axolotl-ai-co/DeepSeek-V3-11M", @@ -83,7 +82,6 @@ def test_lora_deepseekv3(self, temp_dir, sample_packing): [True, False], ) def test_fft_deepseekv3(self, temp_dir, sample_packing): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "axolotl-ai-co/DeepSeek-V3-11M", diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index a1df695352..8f577ef474 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -21,7 +21,6 @@ class TestDPOLlamaLora(unittest.TestCase): @with_temp_dir def test_dpo_lora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -70,7 +69,6 @@ def test_dpo_lora(self, temp_dir): @with_temp_dir def test_dpo_nll_lora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -120,7 +118,6 @@ def test_dpo_nll_lora(self, temp_dir): @with_temp_dir def test_dpo_use_weighting(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -171,7 +168,6 @@ def test_dpo_use_weighting(self, temp_dir): @pytest.mark.skip("kto_pair no longer supported in trl") @with_temp_dir def test_kto_pair_lora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -220,7 +216,6 @@ def test_kto_pair_lora(self, temp_dir): @with_temp_dir def test_ipo_lora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -269,7 +264,6 @@ def test_ipo_lora(self, temp_dir): @with_temp_dir def test_orpo_lora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -322,7 +316,6 @@ def test_orpo_lora(self, temp_dir): @pytest.mark.skip(reason="Fix the implementation") @with_temp_dir def test_kto_lora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py index e4a06ad148..633e449ef2 100644 --- a/tests/e2e/test_embeddings_lr.py +++ b/tests/e2e/test_embeddings_lr.py @@ -19,7 +19,6 @@ class TestEmbeddingsLrScale(unittest.TestCase): @with_temp_dir def test_train_w_embedding_lr_scale(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -65,7 +64,6 @@ def test_train_w_embedding_lr_scale(self, temp_dir): @with_temp_dir def test_train_w_embedding_lr(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/test_evaluate.py b/tests/e2e/test_evaluate.py index 977497e5e8..3b0ab14502 100644 --- a/tests/e2e/test_evaluate.py +++ b/tests/e2e/test_evaluate.py @@ -13,7 +13,6 @@ class TestE2eEvaluate: """Test cases for evaluate CLI""" def test_evaluate(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/test_falcon.py b/tests/e2e/test_falcon.py index 5be6efcf64..1a363fe6af 100644 --- a/tests/e2e/test_falcon.py +++ b/tests/e2e/test_falcon.py @@ -22,7 +22,6 @@ class TestFalcon(unittest.TestCase): @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_lora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "illuin/tiny-random-FalconForCausalLM", @@ -74,7 +73,6 @@ def test_lora(self, temp_dir): @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_lora_added_vocab(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "illuin/tiny-random-FalconForCausalLM", @@ -130,7 +128,6 @@ def test_lora_added_vocab(self, temp_dir): @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_ft(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "illuin/tiny-random-FalconForCausalLM", diff --git a/tests/e2e/test_gemma2.py b/tests/e2e/test_gemma2.py index c0eba72a78..9e9f1a9ccb 100644 --- a/tests/e2e/test_gemma2.py +++ b/tests/e2e/test_gemma2.py @@ -22,7 +22,6 @@ class TestGemma2: [True, False], ) def test_lora_gemma2(self, temp_dir, sample_packing): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "axolotl-ai-co/gemma-2-33M", @@ -78,7 +77,6 @@ def test_lora_gemma2(self, temp_dir, sample_packing): [True, False], ) def test_fft_gemma2(self, temp_dir, sample_packing): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "axolotl-ai-co/gemma-2-33M", diff --git a/tests/e2e/test_gemma3_text.py b/tests/e2e/test_gemma3_text.py index ef38d028dc..6cd9992426 100644 --- a/tests/e2e/test_gemma3_text.py +++ b/tests/e2e/test_gemma3_text.py @@ -22,7 +22,6 @@ class TestGemma3Text: [True, False], ) def test_lora_gemma3_text(self, temp_dir, sample_packing): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "axolotl-ai-co/gemma-3-34M", @@ -78,7 +77,6 @@ def test_lora_gemma3_text(self, temp_dir, sample_packing): [True, False], ) def test_fft_gemma3_text(self, temp_dir, sample_packing): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "axolotl-ai-co/gemma-3-34M", diff --git a/tests/e2e/test_imports.py b/tests/e2e/test_imports.py index 050e4dfb30..4c01e50be8 100644 --- a/tests/e2e/test_imports.py +++ b/tests/e2e/test_imports.py @@ -11,11 +11,7 @@ class TestImports(unittest.TestCase): """ def test_import_causal_trainer(self): - from axolotl.core.builders import ( # pylint: disable=unused-import # noqa: F401 - HFCausalTrainerBuilder, - ) + pass def test_import_rl_trainer(self): - from axolotl.core.builders import ( # pylint: disable=unused-import # noqa: F401 - HFRLTrainerBuilder, - ) + pass diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index 1e6df0be9c..de085cbe27 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -16,7 +16,6 @@ class TestLlama: """ def test_fft_trust_remote_code(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -57,7 +56,6 @@ def test_fft_trust_remote_code(self, temp_dir): check_model_output_exists(temp_dir, cfg) def test_fix_untrained_tokens(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -105,7 +103,6 @@ def test_fix_untrained_tokens(self, temp_dir): check_model_output_exists(temp_dir, cfg) def test_fix_untrained_tokens_already_trained(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -150,7 +147,6 @@ def test_fix_untrained_tokens_already_trained(self, temp_dir): check_model_output_exists(temp_dir, cfg) def test_batch_flattening(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index bd55023008..a041244e7a 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -22,7 +22,6 @@ class TestPretrainLlama: ], ) def test_pretrain(self, temp_dir, sample_packing, pretrain_multipack_attn): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py index 760759bcaa..0cc927f769 100644 --- a/tests/e2e/test_llama_vision.py +++ b/tests/e2e/test_llama_vision.py @@ -19,7 +19,6 @@ class TestLlamaVision(unittest.TestCase): @with_temp_dir def test_lora_llama_vision_text_only_dataset(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "axolotl-ai-co/Llama-3.2-39M-Vision", @@ -67,7 +66,6 @@ def test_lora_llama_vision_text_only_dataset(self, temp_dir): @with_temp_dir def test_lora_llama_vision_multimodal_dataset(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "axolotl-ai-co/Llama-3.2-39M-Vision", diff --git a/tests/e2e/test_load_model.py b/tests/e2e/test_load_model.py index 8fcffeb114..7c5389a58a 100644 --- a/tests/e2e/test_load_model.py +++ b/tests/e2e/test_load_model.py @@ -56,13 +56,11 @@ def setup_method(self): "context_parallel_size": 1, } ) - self.model_loader = ( # pylint: disable=attribute-defined-outside-init - ModelLoader( - cfg=self.cfg, - tokenizer="", - inference=False, - reference_model=True, - ) + self.model_loader = ModelLoader( + cfg=self.cfg, + tokenizer="", + inference=False, + reference_model=True, ) @pytest.mark.parametrize("embedding_modules", ["embed_tokens", "lm_head"]) @@ -74,7 +72,7 @@ def test_convert_embedding_modules_dtype( self, temp_dir, embedding_modules, dist_dtype, before_kbit_train_or_finetune ): self.cfg.output_dir = temp_dir - self.model_loader.tokenizer = load_tokenizer(self.cfg) # pylint: disable=all + self.model_loader.tokenizer = load_tokenizer(self.cfg) self.model_loader.load() self.model_loader._convert_embedding_modules_dtype( embedding_modules, dist_dtype, before_kbit_train_or_finetune diff --git a/tests/e2e/test_lora_llama.py b/tests/e2e/test_lora_llama.py index 7e0ff46cf7..b6ee393dfd 100644 --- a/tests/e2e/test_lora_llama.py +++ b/tests/e2e/test_lora_llama.py @@ -19,7 +19,6 @@ class TestLoraLlama(unittest.TestCase): @with_temp_dir def test_lora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/test_mamba.py b/tests/e2e/test_mamba.py index 73d3bdc260..67935377dc 100644 --- a/tests/e2e/test_mamba.py +++ b/tests/e2e/test_mamba.py @@ -22,7 +22,6 @@ class TestMamba(unittest.TestCase): @with_temp_dir def test_fft(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "state-spaces/mamba-130m", diff --git a/tests/e2e/test_mistral.py b/tests/e2e/test_mistral.py index f47f794e06..08b3b05af4 100644 --- a/tests/e2e/test_mistral.py +++ b/tests/e2e/test_mistral.py @@ -21,7 +21,6 @@ class TestMistral(unittest.TestCase): @with_temp_dir def test_lora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", @@ -68,7 +67,6 @@ def test_lora(self, temp_dir): @with_temp_dir def test_ft(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", diff --git a/tests/e2e/test_mixtral.py b/tests/e2e/test_mixtral.py index 3fe2bf70f1..c46cf906d6 100644 --- a/tests/e2e/test_mixtral.py +++ b/tests/e2e/test_mixtral.py @@ -22,7 +22,6 @@ class TestMixtral(unittest.TestCase): @with_temp_dir def test_qlora_w_fa2(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "hf-internal-testing/Mixtral-tiny", @@ -78,7 +77,6 @@ def test_qlora_w_fa2(self, temp_dir): @with_temp_dir def test_qlora_wo_fa2(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "hf-internal-testing/Mixtral-tiny", @@ -134,7 +132,6 @@ def test_qlora_wo_fa2(self, temp_dir): @with_temp_dir def test_16bit_lora_w_fa2(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "hf-internal-testing/Mixtral-tiny", @@ -193,7 +190,6 @@ def test_16bit_lora_w_fa2(self, temp_dir): @with_temp_dir def test_16bit_lora_wo_fa2(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "hf-internal-testing/Mixtral-tiny", @@ -252,7 +248,6 @@ def test_16bit_lora_wo_fa2(self, temp_dir): @with_temp_dir def test_ft(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "hf-internal-testing/Mixtral-tiny", diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 987d860418..dbea92a5b3 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -25,7 +25,6 @@ class TestCustomOptimizers(unittest.TestCase): @with_temp_dir def test_optimi_adamw(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -71,7 +70,6 @@ def test_optimi_adamw(self, temp_dir): @with_temp_dir @require_torch_2_5_1 def test_adopt_adamw(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -117,7 +115,6 @@ def test_adopt_adamw(self, temp_dir): @with_temp_dir @require_torch_2_5_1 def test_muon(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -164,7 +161,6 @@ def test_muon(self, temp_dir): @with_temp_dir @require_torch_2_7_0 def test_dion(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -206,7 +202,6 @@ def test_dion(self, temp_dir): @with_temp_dir def test_fft_schedule_free_adamw(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -234,7 +229,6 @@ def test_fft_schedule_free_adamw(self, temp_dir): "save_first_step": False, } ) - # pylint: disable=duplicate-code cfg = validate_config(cfg) normalize_config(cfg) @@ -246,7 +240,6 @@ def test_fft_schedule_free_adamw(self, temp_dir): @with_temp_dir @require_torch_2_6_0 def test_came_pytorch(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "JackFram/llama-68m", diff --git a/tests/e2e/test_packing_loss.py b/tests/e2e/test_packing_loss.py index aec9d95f8b..7cb979ce6e 100644 --- a/tests/e2e/test_packing_loss.py +++ b/tests/e2e/test_packing_loss.py @@ -21,7 +21,6 @@ class TestPackedLlama(unittest.TestCase): @with_temp_dir def test_loss_packed(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/test_phi.py b/tests/e2e/test_phi.py index ab3a636748..ae2210249f 100644 --- a/tests/e2e/test_phi.py +++ b/tests/e2e/test_phi.py @@ -19,7 +19,6 @@ class TestPhi(unittest.TestCase): @with_temp_dir def test_phi_ft(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "microsoft/phi-1_5", @@ -65,7 +64,6 @@ def test_phi_ft(self, temp_dir): @with_temp_dir def test_phi_qlora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "microsoft/phi-1_5", diff --git a/tests/e2e/test_preprocess.py b/tests/e2e/test_preprocess.py index 25f42e8329..4aa4cb6c2b 100644 --- a/tests/e2e/test_preprocess.py +++ b/tests/e2e/test_preprocess.py @@ -15,7 +15,7 @@ class TestPreprocess: def test_w_deepspeed(self, temp_dir): """make sure preproces doesn't choke when using deepspeed in the config""" - # pylint: disable=duplicate-code + cfg = DictDefault( { "base_model": "Qwen/Qwen2.5-0.5B", diff --git a/tests/e2e/test_process_reward_model_smollm2.py b/tests/e2e/test_process_reward_model_smollm2.py index bd9eec48b6..9d83aabbc1 100644 --- a/tests/e2e/test_process_reward_model_smollm2.py +++ b/tests/e2e/test_process_reward_model_smollm2.py @@ -19,7 +19,6 @@ class TestProcessRewardSmolLM2(unittest.TestCase): @with_temp_dir def test_prm(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/test_qat.py b/tests/e2e/test_qat.py index 139ae155ac..7d41dfb50d 100644 --- a/tests/e2e/test_qat.py +++ b/tests/e2e/test_qat.py @@ -18,7 +18,6 @@ class TestQATLlama: """ def test_qat(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -68,7 +67,6 @@ def test_qat(self, temp_dir): check_model_output_exists(Path(temp_dir) / "checkpoint-5", cfg) def test_qat_dpo(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/test_quantization.py b/tests/e2e/test_quantization.py index 500b7e556b..cfbdfec38d 100644 --- a/tests/e2e/test_quantization.py +++ b/tests/e2e/test_quantization.py @@ -131,7 +131,7 @@ def test_get_ptq_config( @require_torch_2_6_0 def test_prepare_model_for_qat( self, model, weight_dtype, activation_dtype, group_size, quantize_embedding - ): # pylint: disable=redefined-outer-name + ): prepare_model_for_qat( model, weight_dtype, group_size, activation_dtype, quantize_embedding ) @@ -175,7 +175,7 @@ def test_quantize_model_for_ptq( group_size, quantize_embedding, expected_exception, - ): # pylint: disable=redefined-outer-name + ): if expected_exception: with pytest.raises(expected_exception): quantize_model_for_ptq( @@ -198,11 +198,13 @@ def test_quantize_model_for_ptq( if activation_dtype: assert isinstance( child.weight, LinearActivationQuantizedTensor - ), "Linear weight should be quantized with activation quantization" + ), ( + "Linear weight should be quantized with activation quantization" + ) else: - assert isinstance( - child.weight, AffineQuantizedTensor - ), "Linear weight should be quantized without activation quantization" + assert isinstance(child.weight, AffineQuantizedTensor), ( + "Linear weight should be quantized without activation quantization" + ) class TestQuantizationCallback: @@ -217,9 +219,7 @@ def trainer_state(self): ) @require_torch_2_6_0 - def test_qat_callback_fake_quant_after_n_steps( - self, model, trainer_state - ): # pylint: disable=redefined-outer-name + def test_qat_callback_fake_quant_after_n_steps(self, model, trainer_state): cfg = QATConfig( weight_dtype="int8", activation_dtype="int8", @@ -269,9 +269,7 @@ def test_qat_callback_fake_quant_after_n_steps( assert model.lm_head.weight_fake_quantizer.enabled @require_torch_2_6_0 - def test_qat_callback_fake_quant_after_n_steps_is_none( - self, model, trainer_state - ): # pylint: disable=redefined-outer-name + def test_qat_callback_fake_quant_after_n_steps_is_none(self, model, trainer_state): cfg = QATConfig( weight_dtype="int8", activation_dtype="int8", @@ -314,9 +312,7 @@ class TestConvertQATModelForPTQ: """ @require_torch_2_6_0 - def test_convert_qat_model_for_ptq( - self, model - ): # pylint: disable=redefined-outer-name + def test_convert_qat_model_for_ptq(self, model): config = QATConfig( weight_dtype="int8", activation_dtype="int8", diff --git a/tests/e2e/test_qwen.py b/tests/e2e/test_qwen.py index 59267d14db..1c75d817b8 100644 --- a/tests/e2e/test_qwen.py +++ b/tests/e2e/test_qwen.py @@ -19,7 +19,6 @@ class TestE2eQwen: @pytest.mark.parametrize("base_model", ["Qwen/Qwen2-0.5B", "Qwen/Qwen2.5-0.5B"]) def test_dpo(self, base_model, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": base_model, diff --git a/tests/e2e/test_reward_model_smollm2.py b/tests/e2e/test_reward_model_smollm2.py index 82513f99f2..cc768b173e 100644 --- a/tests/e2e/test_reward_model_smollm2.py +++ b/tests/e2e/test_reward_model_smollm2.py @@ -19,7 +19,6 @@ class TestRewardModelLoraSmolLM2(unittest.TestCase): @with_temp_dir def test_rm_lora(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/test_save_first_step.py b/tests/e2e/test_save_first_step.py index 5bbd2302b9..ce2d3f1454 100644 --- a/tests/e2e/test_save_first_step.py +++ b/tests/e2e/test_save_first_step.py @@ -20,7 +20,6 @@ class TestSaveFirstStepCallback(unittest.TestCase): @with_temp_dir def test_save_first_step(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -61,7 +60,6 @@ def test_save_first_step(self, temp_dir): @with_temp_dir def test_no_save_first_step(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/test_schedulers.py b/tests/e2e/test_schedulers.py index 8f7a13aeea..5b9c56288f 100644 --- a/tests/e2e/test_schedulers.py +++ b/tests/e2e/test_schedulers.py @@ -19,7 +19,6 @@ class TestCustomSchedulers(unittest.TestCase): @with_temp_dir def test_rex_scheduler(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 939ed5c1ca..7db6cf74ee 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -2,6 +2,7 @@ helper utils for tests """ +import importlib.util import os import shutil import tempfile @@ -107,12 +108,7 @@ def require_vllm(test_case): """ def is_vllm_installed(): - try: - import vllm # pylint: disable=unused-import # noqa: F401 - - return True - except ImportError: - return False + return importlib.util.find_spec("vllm") is not None return unittest.skipUnless( is_vllm_installed(), "test requires vllm to be installed" @@ -125,12 +121,7 @@ def require_llmcompressor(test_case): """ def is_llmcompressor_installed(): - try: - import llmcompressor # pylint: disable=unused-import # noqa: F401 - - return True - except ImportError: - return False + return importlib.util.find_spec("llmcompressor") is not None return unittest.skipUnless( is_llmcompressor_installed(), "test requires llmcompressor to be installed" @@ -159,8 +150,8 @@ def check_tensorboard( tb_log_path = most_recent_subdir(temp_run_dir) event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) reader = SummaryReader(event_file) - df = reader.scalars # pylint: disable=invalid-name - df = df[(df.tag == tag)] # pylint: disable=invalid-name + df = reader.scalars + df = df[(df.tag == tag)] lt_val = (1 + rtol) * lt_val if "%s" in assertion_err: assert df.value.values[-1] < lt_val, assertion_err % df.value.values[-1] diff --git a/tests/hf_offline_utils.py b/tests/hf_offline_utils.py index 385e61f181..0e4a2f067e 100644 --- a/tests/hf_offline_utils.py +++ b/tests/hf_offline_utils.py @@ -20,7 +20,7 @@ def reload_modules(hf_hub_offline): importlib.reload(huggingface_hub.constants) huggingface_hub.constants.HF_HUB_OFFLINE = hf_hub_offline importlib.reload(datasets.config) - setattr(datasets.config, "HF_HUB_OFFLINE", hf_hub_offline) + datasets.config.HF_HUB_OFFLINE = hf_hub_offline reset_sessions() diff --git a/tests/integrations/test_liger.py b/tests/integrations/test_liger.py index 5c4bd10281..d7b171ec27 100644 --- a/tests/integrations/test_liger.py +++ b/tests/integrations/test_liger.py @@ -10,7 +10,6 @@ from axolotl.utils.dict import DictDefault -# pylint: disable=duplicate-code @pytest.fixture(name="minimal_liger_cfg") def fixture_cfg(): return DictDefault( @@ -30,7 +29,6 @@ def fixture_cfg(): ) -# pylint: disable=too-many-public-methods class TestValidation: """ Test the validation module for liger diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 677512d3d2..21299ed980 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -1,4 +1,3 @@ -# pylint: disable=too-many-lines """Module for testing the validation module""" import os @@ -49,7 +48,6 @@ def inject_fixtures(self, caplog): self._caplog = caplog -# pylint: disable=too-many-public-methods class TestValidation(BaseValidation): """ Test the validation module @@ -241,7 +239,7 @@ def test_batch_size_more_params(self): def test_lr_as_float(self, minimal_cfg): cfg = ( - DictDefault( # pylint: disable=unsupported-binary-operation + DictDefault( { "learning_rate": "5e-5", } @@ -303,7 +301,7 @@ def test_qlora(self, minimal_cfg): ) cfg = ( - DictDefault( # pylint: disable=unsupported-binary-operation + DictDefault( { "load_in_8bit": True, } @@ -315,7 +313,7 @@ def test_qlora(self, minimal_cfg): validate_config(cfg) cfg = ( - DictDefault( # pylint: disable=unsupported-binary-operation + DictDefault( { "gptq": True, } @@ -327,7 +325,7 @@ def test_qlora(self, minimal_cfg): validate_config(cfg) cfg = ( - DictDefault( # pylint: disable=unsupported-binary-operation + DictDefault( { "load_in_4bit": False, } @@ -339,7 +337,7 @@ def test_qlora(self, minimal_cfg): validate_config(cfg) cfg = ( - DictDefault( # pylint: disable=unsupported-binary-operation + DictDefault( { "load_in_4bit": True, } @@ -361,7 +359,7 @@ def test_qlora_merge(self, minimal_cfg): ) cfg = ( - DictDefault( # pylint: disable=unsupported-binary-operation + DictDefault( { "load_in_8bit": True, } @@ -373,7 +371,7 @@ def test_qlora_merge(self, minimal_cfg): validate_config(cfg) cfg = ( - DictDefault( # pylint: disable=unsupported-binary-operation + DictDefault( { "gptq": True, } @@ -385,7 +383,7 @@ def test_qlora_merge(self, minimal_cfg): validate_config(cfg) cfg = ( - DictDefault( # pylint: disable=unsupported-binary-operation + DictDefault( { "load_in_4bit": True, } diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index 7f942e0ef4..12c4bcd937 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -30,7 +30,6 @@ def fixture_assistant_dataset(): @pytest.fixture(name="sharegpt_dataset") def fixture_sharegpt_dataset(): - # pylint: disable=duplicate-code return Dataset.from_list( [ { @@ -47,7 +46,6 @@ def fixture_sharegpt_dataset(): @pytest.fixture(name="basic_dataset") def fixture_basic_dataset(): - # pylint: disable=duplicate-code return Dataset.from_list( [ { @@ -65,7 +63,6 @@ def fixture_basic_dataset(): @pytest.fixture(name="toolcalling_dataset") def fixture_toolcalling_dataset(): - # pylint: disable=duplicate-code return Dataset.from_list( [ { @@ -112,7 +109,7 @@ def fixture_toolcalling_dataset(): @enable_hf_offline def fixture_llama3_tokenizer( download_llama3_8b_instruct_model_fixture, -): # pylint: disable=unused-argument,redefined-outer-name +): tokenizer = AutoTokenizer.from_pretrained("NousResearch/Meta-Llama-3-8B-Instruct") return tokenizer @@ -129,7 +126,7 @@ def fixture_smollm2_tokenizer(): @enable_hf_offline def fixture_mistralv03_tokenizer( download_mlx_mistral_7b_model_fixture, -): # pylint: disable=unused-argument,redefined-outer-name +): tokenizer = AutoTokenizer.from_pretrained( "mlx-community/Mistral-7B-Instruct-v0.3-4bit" ) diff --git a/tests/prompt_strategies/messages/test_chat.py b/tests/prompt_strategies/messages/test_chat.py index a4c2ae67fd..f083232a88 100644 --- a/tests/prompt_strategies/messages/test_chat.py +++ b/tests/prompt_strategies/messages/test_chat.py @@ -2,7 +2,6 @@ tests for chat_template prompt strategy """ -# pylint: disable=duplicate-code import unittest from axolotl.prompt_strategies.messages.chat import load @@ -53,9 +52,9 @@ def test_llama3_load(self, llama3_tokenizer, assistant_dataset): # fmt: on LOG.debug(f"Expected input_ids: {expected_input_ids}") LOG.debug(f"Actual input_ids: {input_ids}") - assert ( - input_ids == expected_input_ids - ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + assert input_ids == expected_input_ids, ( + f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + ) if __name__ == "__main__": diff --git a/tests/prompt_strategies/test_alpaca.py b/tests/prompt_strategies/test_alpaca.py index 78f7837477..b96ebce198 100644 --- a/tests/prompt_strategies/test_alpaca.py +++ b/tests/prompt_strategies/test_alpaca.py @@ -30,7 +30,6 @@ def fixture_alpaca_dataset(): @pytest.fixture(name="tokenizer") @enable_hf_offline def fixture_tokenizer(): - # pylint: disable=all tokenizer = AutoTokenizer.from_pretrained( "casperhansen/mistral-7b-instruct-v0.1-awq" ) diff --git a/tests/prompt_strategies/test_chat_template_ds_schema_unification.py b/tests/prompt_strategies/test_chat_template_ds_schema_unification.py index 502efae4bb..e8d35e9744 100644 --- a/tests/prompt_strategies/test_chat_template_ds_schema_unification.py +++ b/tests/prompt_strategies/test_chat_template_ds_schema_unification.py @@ -18,9 +18,7 @@ def fixture_messages_w_tools(): {"messages":[{"role":"user","content":"move to (0, 1)"},{"role":"assistant","content":"","tool_calls":[{"function":{"name":"move","arguments":{"x":0,"y":1}}}]}],"tools":[{"type":"function","function":{"name":"move","description":"Move to a given location measured in meters","parameters":{"type":"object","properties":{"x":{"type":"number","description":"The x coordinate of the location, negative values are to the left, positive values are to the right"},"y":{"type":"number","description":"The y coordinate of the location, negative values are backward, positive values are forward"}},"required":["x","y"]}}},{"type":"function","function":{"name":"turn","description":"Turn the robot to a given direction","parameters":{"type":"object","properties":{"theta":{"type":"integer","description":"The angle to turn to, in degrees, positive values are counter-clockwise, negative values are clockwise"}},"required":["theta"]}}},{"type":"function","function":{"name":"invalid_prompt","description":"call when the user's prompt is invalid","parameters":{"type":"object","properties":{"message":{"type":"string","description":"why the prompt is invalid"}},"required":["message"]}}}],"add_generation_prompt":false} {"messages":[{"role":"user","content":"turn 270 degree"},{"role":"assistant","content":"","tool_calls":[{"function":{"name":"turn","arguments":{"theta": 270}}}]}],"tools":[{"type":"function","function":{"name":"move","description":"Move to a given location measured in meters","parameters":{"type":"object","properties":{"x":{"type":"number","description":"The x coordinate of the location, negative values are to the left, positive values are to the right"},"y":{"type":"number","description":"The y coordinate of the location, negative values are backward, positive values are forward"}},"required":["x","y"]}}},{"type":"function","function":{"name":"turn","description":"Turn the robot to a given direction","parameters":{"type":"object","properties":{"theta":{"type":"integer","description":"The angle to turn to, in degrees, positive values are counter-clockwise, negative values are clockwise"}},"required":["theta"]}}},{"type":"function","function":{"name":"invalid_prompt","description":"call when the user's prompt is invalid","parameters":{"type":"object","properties":{"message":{"type":"string","description":"why the prompt is invalid"}},"required":["message"]}}}],"add_generation_prompt":false} {"messages":[{"role":"user","content":"jump high"},{"role":"assistant","content":"","tool_calls":[{"function":{"name":"invalid_prompt","arguments":{"message": "jump is not a valid action"}}}]}],"tools":[{"type":"function","function":{"name":"move","description":"Move to a given location measured in meters","parameters":{"type":"object","properties":{"x":{"type":"number","description":"The x coordinate of the location, negative values are to the left, positive values are to the right"},"y":{"type":"number","description":"The y coordinate of the location, negative values are backward, positive values are forward"}},"required":["x","y"]}}},{"type":"function","function":{"name":"turn","description":"Turn the robot to a given direction","parameters":{"type":"object","properties":{"theta":{"type":"integer","description":"The angle to turn to, in degrees, positive values are counter-clockwise, negative values are clockwise"}},"required":["theta"]}}},{"type":"function","function":{"name":"invalid_prompt","description":"call when the user's prompt is invalid","parameters":{"type":"object","properties":{"message":{"type":"string","description":"why the prompt is invalid"}},"required":["message"]}}}],"add_generation_prompt":false} - """.strip().split( - "\n" - ) + """.strip().split("\n") rows = [json.loads(row) for row in jsons] return Dataset.from_list(rows) @@ -28,7 +26,7 @@ def fixture_messages_w_tools(): @pytest.fixture(name="qwen3_tokenizer") def qwen3_tokenizer_fixture( download_qwen3_half_billion_model, -): # pylint: disable=unused-argument +): tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B") return tokenizer diff --git a/tests/prompt_strategies/test_chat_templates.py b/tests/prompt_strategies/test_chat_templates.py index 371ccf6161..90e0e274b7 100644 --- a/tests/prompt_strategies/test_chat_templates.py +++ b/tests/prompt_strategies/test_chat_templates.py @@ -67,9 +67,9 @@ def test_llama3_load(self, llama3_tokenizer, assistant_dataset): # fmt: on LOG.debug(f"Expected input_ids: {expected_input_ids}") LOG.debug(f"Actual input_ids: {input_ids}") - assert ( - input_ids == expected_input_ids - ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + assert input_ids == expected_input_ids, ( + f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + ) def test_llama3(self, llama3_tokenizer, assistant_dataset): LOG.info("Testing llama-3 with assistant dataset") @@ -109,9 +109,9 @@ def test_llama3(self, llama3_tokenizer, assistant_dataset): # fmt: on LOG.debug(f"Expected input_ids: {expected_input_ids}") LOG.debug(f"Actual input_ids: {input_ids}") - assert ( - input_ids == expected_input_ids - ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + assert input_ids == expected_input_ids, ( + f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + ) def test_phi35(self, phi35_tokenizer, assistant_dataset): LOG.info("Testing phi-3.5 with assistant dataset") @@ -161,15 +161,15 @@ def test_phi35(self, phi35_tokenizer, assistant_dataset): # fmt: on LOG.debug(f"Expected input_ids: {expected_input_ids}") LOG.debug(f"Actual input_ids: {input_ids}") - assert ( - input_ids == expected_input_ids - ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + assert input_ids == expected_input_ids, ( + f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + ) LOG.debug(f"Expected labels : {expected_labels}") LOG.debug(f"Actual labels : {labels}") - assert ( - labels == expected_labels - ), f"Input IDs mismatch: {labels} != {expected_labels}" + assert labels == expected_labels, ( + f"Input IDs mismatch: {labels} != {expected_labels}" + ) def test_llama3_with_training_data(self, llama3_tokenizer, assistant_dataset): LOG.info("Testing llama-3 with assistant dataset including training data") @@ -234,7 +234,7 @@ class TestSharegptChatTemplateLlama3: def test_llama3_assistant(self, llama3_tokenizer, sharegpt_dataset): LOG.info("Testing ShareGPT style datasets with llama-3 assistant prompts") - # pylint: disable=duplicate-code + strategy = ChatTemplateStrategy( ChatTemplatePrompter( llama3_tokenizer, @@ -285,16 +285,16 @@ def test_llama3_assistant(self, llama3_tokenizer, sharegpt_dataset): LOG.debug(f"Expected labels: {expected_labels}") LOG.debug(f"Actual labels: {labels}") - assert ( - input_ids == expected_input_ids - ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" - assert ( - labels == expected_labels - ), f"Labels mismatch: {labels} != {expected_labels}" + assert input_ids == expected_input_ids, ( + f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + ) + assert labels == expected_labels, ( + f"Labels mismatch: {labels} != {expected_labels}" + ) def test_llama3_human(self, llama3_tokenizer, sharegpt_dataset): LOG.info("Testing ShareGPT style datasets with llama-3 human prompts") - # pylint: disable=duplicate-code + strategy = ChatTemplateStrategy( ChatTemplatePrompter( llama3_tokenizer, @@ -345,16 +345,16 @@ def test_llama3_human(self, llama3_tokenizer, sharegpt_dataset): LOG.debug(f"Expected labels: {expected_labels}") LOG.debug(f"Actual labels: {labels}") - assert ( - input_ids == expected_input_ids - ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" - assert ( - labels == expected_labels - ), f"Labels mismatch: {labels} != {expected_labels}" + assert input_ids == expected_input_ids, ( + f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + ) + assert labels == expected_labels, ( + f"Labels mismatch: {labels} != {expected_labels}" + ) def test_llama3_system_human(self, llama3_tokenizer, basic_dataset): LOG.info("Testing ShareGPT style datasets with llama-3 system/human prompts") - # pylint: disable=duplicate-code + strategy = ChatTemplateStrategy( ChatTemplatePrompter( llama3_tokenizer, @@ -409,12 +409,12 @@ def test_llama3_system_human(self, llama3_tokenizer, basic_dataset): LOG.debug(f"Expected labels: {expected_labels}") LOG.debug(f"Actual labels: {labels}") - assert ( - input_ids == expected_input_ids - ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" - assert ( - labels == expected_labels - ), f"Labels mismatch: {labels} != {expected_labels}" + assert input_ids == expected_input_ids, ( + f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + ) + assert labels == expected_labels, ( + f"Labels mismatch: {labels} != {expected_labels}" + ) class TestAssistantToolCallingChatTemplateLlama32Vision: @@ -481,13 +481,13 @@ def test_llama32vision_train_on_assistant( ] # fmt: on - assert ( - input_ids == expected_input_ids - ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + assert input_ids == expected_input_ids, ( + f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + ) - assert ( - labels == expected_labels - ), f"Labels mismatch: {labels} != {expected_labels}" + assert labels == expected_labels, ( + f"Labels mismatch: {labels} != {expected_labels}" + ) def test_llama32vision_train_on_tools( self, llama3_tokenizer, toolcalling_dataset, llama3_2_vision_chat_template_jinja @@ -495,7 +495,6 @@ def test_llama32vision_train_on_tools( LOG.info( "Testing assistant style datasets with tool_calling with llama-32 chat template, training on tools" ) - # pylint: disable=duplicate-code strategy = ChatTemplateStrategy( ChatTemplatePrompter( @@ -549,13 +548,13 @@ def test_llama32vision_train_on_tools( ] # fmt: on - assert ( - input_ids == expected_input_ids - ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + assert input_ids == expected_input_ids, ( + f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + ) - assert ( - labels == expected_labels - ), f"Labels mismatch: {labels} != {expected_labels}" + assert labels == expected_labels, ( + f"Labels mismatch: {labels} != {expected_labels}" + ) if __name__ == "__main__": diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index f847cab4ae..fd39a43059 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -2,8 +2,6 @@ tests for chat_template prompt strategy """ -# pylint: disable=too-many-lines - from copy import deepcopy import pytest @@ -96,9 +94,9 @@ def _should_skip_turn(self, tokenizer, turn, turn_idx, start_idx, end_idx): and turn.get("from") in ["system", "context"] and ("mistral" in tokenizer.name_or_path.lower()) ): - assert ( - start_idx == -1 and end_idx == -1 - ), "Expected system message to be skipped" + assert start_idx == -1 and end_idx == -1, ( + "Expected system message to be skipped" + ) return True return False @@ -155,7 +153,9 @@ def test_train_on_inputs_true( assert all( label != IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] - ), f"Expected labels for input '{response}' to be ignored, but got {labels[start_idx:end_idx]}" + ), ( + f"Expected labels for input '{response}' to be ignored, but got {labels[start_idx:end_idx]}" + ) LOG.debug("Full labels: %s", labels) LOG.debug("Full input_ids: %s", input_ids) @@ -215,11 +215,15 @@ def test_train_on_inputs_false( if is_assistant: assert all( label != IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] - ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:end_idx]}" + ), ( + f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:end_idx]}" + ) else: assert all( label == IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] - ), f"Expected labels for human input '{response}' to be IGNORE_TOKEN_ID, but got {labels[start_idx:end_idx]}" + ), ( + f"Expected labels for human input '{response}' to be IGNORE_TOKEN_ID, but got {labels[start_idx:end_idx]}" + ) def test_roles_to_train_human_assistant_only( self, @@ -276,11 +280,15 @@ def test_roles_to_train_human_assistant_only( if should_be_labelled: assert all( label != IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] - ), f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:end_idx]}" + ), ( + f"Expected labels for assistant response '{response}' to be set, but got {labels[start_idx:end_idx]}" + ) else: assert all( label == IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] - ), f"Expected labels for human input '{response}' to be IGNORE_TOKEN_ID, but got {labels[start_idx:end_idx]}" + ), ( + f"Expected labels for human input '{response}' to be IGNORE_TOKEN_ID, but got {labels[start_idx:end_idx]}" + ) def test_roles_to_train_all( self, @@ -327,13 +335,15 @@ def test_roles_to_train_all( continue decoded_response = tokenizer.decode(input_ids[start_idx:end_idx]) - assert ( - response in decoded_response - ), f"Response {response} not found in index {start_idx}:{end_idx} decoded:{decoded_response}" + assert response in decoded_response, ( + f"Response {response} not found in index {start_idx}:{end_idx} decoded:{decoded_response}" + ) assert all( label != IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] - ), f"Expected labels for response '{response}' to be set, but got {labels[start_idx:end_idx]}" + ), ( + f"Expected labels for response '{response}' to be set, but got {labels[start_idx:end_idx]}" + ) def test_empty_roles_to_train( self, @@ -371,9 +381,9 @@ def test_empty_roles_to_train( # Verify that no labels are set when roles_to_train is empty LOG.debug("Full labels: %s", labels) - assert all( - label == IGNORE_TOKEN_ID for label in labels - ), "Expected all labels to be IGNORE_TOKEN_ID when roles_to_train is empty" + assert all(label == IGNORE_TOKEN_ID for label in labels), ( + "Expected all labels to be IGNORE_TOKEN_ID when roles_to_train is empty" + ) def test_train_on_eos_all( self, @@ -417,9 +427,9 @@ def test_train_on_eos_all( assert len(eos_indices) > 0, "Expected at least one EOS token in the input" for eos_idx in eos_indices: - assert ( - labels[eos_idx] != IGNORE_TOKEN_ID - ), f"Expected EOS token at index {eos_idx} to be labeled" + assert labels[eos_idx] != IGNORE_TOKEN_ID, ( + f"Expected EOS token at index {eos_idx} to be labeled" + ) def test_train_on_eos_turn( self, @@ -477,9 +487,9 @@ def test_train_on_eos_turn( while eos_idx < len(input_ids) and input_ids[eos_idx] != eos_token_id: eos_idx += 1 - assert eos_idx < len( - input_ids - ), f"Could not find EOS token after '{response}'" + assert eos_idx < len(input_ids), ( + f"Could not find EOS token after '{response}'" + ) LOG.debug( f"Turn {i}: role={turn['from']}, content='{turn['value']}', start_idx={start_idx}, end_idx={end_idx}, eos_idx={eos_idx}" @@ -492,13 +502,13 @@ def test_train_on_eos_turn( # Verify EOS token labeling based on role is_assistant = turn["from"] == "assistant" if is_assistant: - assert ( - labels[eos_idx] != IGNORE_TOKEN_ID - ), f"Expected EOS token after assistant response '{response}' to be labeled" + assert labels[eos_idx] != IGNORE_TOKEN_ID, ( + f"Expected EOS token after assistant response '{response}' to be labeled" + ) else: - assert ( - labels[eos_idx] == IGNORE_TOKEN_ID - ), f"Expected EOS token after non-assistant input '{response}' to not be labeled" + assert labels[eos_idx] == IGNORE_TOKEN_ID, ( + f"Expected EOS token after non-assistant input '{response}' to not be labeled" + ) def test_train_on_eos_last( self, @@ -545,12 +555,12 @@ def test_train_on_eos_last( # Check that only the last EOS token is labeled for idx in eos_indices[:-1]: - assert ( - labels[idx] == IGNORE_TOKEN_ID - ), f"Expected EOS token at index {idx} to not be labeled" - assert ( - labels[last_eos_idx] != IGNORE_TOKEN_ID - ), f"Expected last EOS token at index {last_eos_idx} to be labeled" + assert labels[idx] == IGNORE_TOKEN_ID, ( + f"Expected EOS token at index {idx} to not be labeled" + ) + assert labels[last_eos_idx] != IGNORE_TOKEN_ID, ( + f"Expected last EOS token at index {last_eos_idx} to be labeled" + ) def test_train_on_eos_none( self, @@ -594,9 +604,9 @@ def test_train_on_eos_none( assert len(eos_indices) > 0, "Expected at least one EOS token in the input" for eos_idx in eos_indices: - assert ( - labels[eos_idx] == IGNORE_TOKEN_ID - ), f"Expected EOS token at index {eos_idx} to not be labeled" + assert labels[eos_idx] == IGNORE_TOKEN_ID, ( + f"Expected EOS token at index {eos_idx} to not be labeled" + ) def test_drop_system_message( self, @@ -634,9 +644,9 @@ def test_drop_system_message( # Check if system message is not present in input_ids system_message = "You are an AI assistant." decoded_message = tokenizer.decode(input_ids) - assert ( - system_message not in decoded_message - ), "Expected system message to be dropped" + assert system_message not in decoded_message, ( + "Expected system message to be dropped" + ) def test_custom_roles( self, @@ -711,7 +721,9 @@ def test_custom_roles( else: assert all( label == IGNORE_TOKEN_ID for label in labels[start_idx:end_idx] - ), f"Expected labels for non-AI message '{response}' to be IGNORE_TOKEN_ID" + ), ( + f"Expected labels for non-AI message '{response}' to be IGNORE_TOKEN_ID" + ) def test_message_field_training( self, @@ -776,13 +788,13 @@ def test_message_field_training( def verify_labels(labels_span, should_train, context_message): """Helper to verify if a span of labels matches expected training state""" if should_train: - assert all( - label != IGNORE_TOKEN_ID for label in labels_span - ), f"Expected all labels for {context_message} to be set, but got {labels_span}" + assert all(label != IGNORE_TOKEN_ID for label in labels_span), ( + f"Expected all labels for {context_message} to be set, but got {labels_span}" + ) else: - assert all( - label == IGNORE_TOKEN_ID for label in labels_span - ), f"Expected all labels for {context_message} to be {IGNORE_TOKEN_ID}, but got {labels_span}" + assert all(label == IGNORE_TOKEN_ID for label in labels_span), ( + f"Expected all labels for {context_message} to be {IGNORE_TOKEN_ID}, but got {labels_span}" + ) # Process all turns and verify labeling for i, turn in enumerate(modified_dataset[0]["messages"]): @@ -861,9 +873,9 @@ def verify_labels(labels_span, should_train, context_message): actual_labels = labels[ start_idx : start_idx + len(token_offsets_masked) ] - assert ( - actual_labels == expected_labels - ), f"Labels mismatch for turn: {turn['value']}\nExpected: {expected_labels}\nActual: {actual_labels}" + assert actual_labels == expected_labels, ( + f"Labels mismatch for turn: {turn['value']}\nExpected: {expected_labels}\nActual: {actual_labels}" + ) # Verify each detail section for detail in adjusted_train_details: @@ -958,7 +970,7 @@ def test_eot_tokens_conflict_with_eos_token( chat_template, chat_template_jinja, eos_token, - basic_dataset, # pylint: disable=unused-argument + basic_dataset, request, ): """Test that an error is raised when eot_tokens contains eos_token and train_on_eot/train_on_eos conflict""" @@ -1005,7 +1017,7 @@ def test_eot_token_backward_compatibility( chat_template, chat_template_jinja, eos_token, - basic_dataset, # pylint: disable=unused-argument + basic_dataset, request, ): """Test that eot_tokens inherits from eos_token when not specified""" @@ -1032,12 +1044,12 @@ def test_eot_token_backward_compatibility( ) # In backward compatibility mode, eot_tokens should be derived from eos_token - assert strategy.eot_tokens == [ - tokenizer.eos_token - ], f"Expected eot_tokens to inherit from eos_token, got {strategy.eot_tokens}" - assert ( - strategy.train_on_eot == "turn" - ), f"Expected train_on_eot to inherit from train_on_eos, got {strategy.train_on_eot}" + assert strategy.eot_tokens == [tokenizer.eos_token], ( + f"Expected eot_tokens to inherit from eos_token, got {strategy.eot_tokens}" + ) + assert strategy.train_on_eot == "turn", ( + f"Expected train_on_eot to inherit from train_on_eos, got {strategy.train_on_eot}" + ) def test_token_not_in_template( self, @@ -1091,7 +1103,7 @@ def test_custom_eot_tokens( tokenizer, chat_template, chat_template_jinja, - eos_token, # pylint: disable=unused-argument + eos_token, basic_dataset, request, ): @@ -1157,13 +1169,13 @@ def test_custom_eot_tokens( ) if is_after_assistant: - assert ( - labels[eot_idx] != IGNORE_TOKEN_ID - ), f"Expected EOT token after assistant turn at index {eot_idx} to be labeled" + assert labels[eot_idx] != IGNORE_TOKEN_ID, ( + f"Expected EOT token after assistant turn at index {eot_idx} to be labeled" + ) else: - assert ( - labels[eot_idx] == IGNORE_TOKEN_ID - ), f"Expected EOT token not after assistant turn at index {eot_idx} to not be labeled" + assert labels[eot_idx] == IGNORE_TOKEN_ID, ( + f"Expected EOT token not after assistant turn at index {eot_idx} to not be labeled" + ) def test_multiple_train_on_eot_settings( self, @@ -1224,9 +1236,9 @@ def test_multiple_train_on_eot_settings( i for i, token_id in enumerate(input_ids) if token_id == eos_token_id ] - assert ( - len(eos_indices) > 0 - ), "Expected at least one EOS/EOT token in the input" + assert len(eos_indices) > 0, ( + "Expected at least one EOS/EOT token in the input" + ) # Check labeling for each EOS/EOT token for idx, eos_idx in enumerate(eos_indices): @@ -1252,13 +1264,13 @@ def test_multiple_train_on_eot_settings( ) if expected_label: - assert ( - labels[eos_idx] == IGNORE_TOKEN_ID - ), f"Expected EOT token at index {eos_idx} to not be labeled with train_on_eot='{setting}'" + assert labels[eos_idx] == IGNORE_TOKEN_ID, ( + f"Expected EOT token at index {eos_idx} to not be labeled with train_on_eot='{setting}'" + ) else: - assert ( - labels[eos_idx] != IGNORE_TOKEN_ID - ), f"Expected EOT token at index {eos_idx} to be labeled with train_on_eot='{setting}'" + assert labels[eos_idx] != IGNORE_TOKEN_ID, ( + f"Expected EOT token at index {eos_idx} to be labeled with train_on_eot='{setting}'" + ) class TestChatTemplateToolCalling: @@ -1378,29 +1390,27 @@ def test_tool_calling_with_llama4_template( decoded_conversation = tokenizer.decode(input_ids) # Verify tool calling structure is present in the decoded conversation - assert ( - '"type": "function",' in decoded_conversation - ), "Tool type function should be in conversation" - assert ( - '"name": "multiples",' in decoded_conversation - ), "Tool function name should be in conversation" + assert '"type": "function",' in decoded_conversation, ( + "Tool type function should be in conversation" + ) + assert '"name": "multiples",' in decoded_conversation, ( + "Tool function name should be in conversation" + ) assert ( '<|python_start|><|python_end|>{"name": "multiples", "parameters": {"number": 5, "limit": 20}}<|eot|>' in decoded_conversation ), "Assistant tool call should be in conversation" - assert ( - "<|header_start|>ipython<|header_end|>" in decoded_conversation - ), "IPython header should be in conversation" - assert ( - '"5,10,15"' in decoded_conversation - ), "Tool response should be in conversation" + assert "<|header_start|>ipython<|header_end|>" in decoded_conversation, ( + "IPython header should be in conversation" + ) + assert '"5,10,15"' in decoded_conversation, ( + "Tool response should be in conversation" + ) # Get conversation turns to verify labeling turns = strategy.get_conversation_thread(tool_calling_dataset[0]) - tools = strategy._get_tools( # pylint: disable=protected-access - tool_calling_dataset[0] - ) + tools = strategy._get_tools(tool_calling_dataset[0]) # Check that assistant responses are properly labeled for i, turn in enumerate(tool_calling_dataset[0]["messages"]): @@ -1409,12 +1419,12 @@ def test_tool_calling_with_llama4_template( turns=turns, turn_idx=i, tools=tools ) - assert ( - start_idx != -1 and end_idx != -1 - ), f"Assistant turn {i} should be found" + assert start_idx != -1 and end_idx != -1, ( + f"Assistant turn {i} should be found" + ) # Verify that assistant responses have proper labels turn_labels = labels[start_idx:end_idx] - assert all( - label != IGNORE_TOKEN_ID for label in turn_labels - ), f"Assistant turn {i} should be unmasked" + assert all(label != IGNORE_TOKEN_ID for label in turn_labels), ( + f"Assistant turn {i} should be unmasked" + ) diff --git a/tests/prompt_strategies/test_chat_templates_mistral.py b/tests/prompt_strategies/test_chat_templates_mistral.py index a5b31a7712..85aa72111c 100644 --- a/tests/prompt_strategies/test_chat_templates_mistral.py +++ b/tests/prompt_strategies/test_chat_templates_mistral.py @@ -28,7 +28,7 @@ def test_mistral_chat_template( request: pytest.FixtureRequest, ): """Test chat template with the Magistral/Devstral tokenizer""" - # pylint: disable=duplicate-code + from axolotl.prompt_strategies.chat_template import MistralPrompter, MistralStrategy tokenizer: HFMistralTokenizer = request.getfixturevalue(tokenizer_str) diff --git a/tests/prompt_strategies/test_chat_templates_thinking.py b/tests/prompt_strategies/test_chat_templates_thinking.py index e807111aa5..5475666a54 100644 --- a/tests/prompt_strategies/test_chat_templates_thinking.py +++ b/tests/prompt_strategies/test_chat_templates_thinking.py @@ -59,7 +59,7 @@ def messages_w_reasoning_fixture(): @pytest.fixture(name="qwen3_tokenizer") def qwen3_tokenizer_fixture( download_qwen3_half_billion_model, -): # pylint: disable=unused-argument +): tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B") return tokenizer @@ -71,7 +71,6 @@ class TestSplitThinking: """ def test_splits_think(self, messages_w_reasoning, qwen3_tokenizer): - # pylint: disable=duplicate-code strategy = load( qwen3_tokenizer, DictDefault( @@ -130,6 +129,6 @@ def test_splits_think(self, messages_w_reasoning, qwen3_tokenizer): 198, # \n ] # fmt: on - assert ( - input_ids == expected_input_ids - ), f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + assert input_ids == expected_input_ids, ( + f"Input IDs mismatch: {input_ids} != {expected_input_ids}" + ) diff --git a/tests/prompt_strategies/test_dpo_chat_templates.py b/tests/prompt_strategies/test_dpo_chat_templates.py index e5f30a6c43..e570cfc9db 100644 --- a/tests/prompt_strategies/test_dpo_chat_templates.py +++ b/tests/prompt_strategies/test_dpo_chat_templates.py @@ -16,7 +16,6 @@ @pytest.fixture(name="assistant_dataset") def fixture_assistant_dataset(): - # pylint: disable=duplicate-code return Dataset.from_list( [ { @@ -49,7 +48,6 @@ def fixture_assistant_dataset(): @pytest.fixture(name="custom_assistant_dataset") def fixture_custom_assistant_dataset(): - # pylint: disable=duplicate-code return Dataset.from_list( [ { @@ -102,7 +100,6 @@ class TestAssistantDPOChatTemplateLlama3: """ def test_llama3_defaults(self, llama3_tokenizer, assistant_dataset): - # pylint: disable=duplicate-code transform_fn, _ = default( DictDefault( { @@ -127,7 +124,6 @@ def test_llama3_defaults(self, llama3_tokenizer, assistant_dataset): assert result["rejected"] == "party on<|eot_id|>" def test_llama3_configured(self, llama3_tokenizer, custom_assistant_dataset): - # pylint: disable=duplicate-code transform_fn, _ = default( DictDefault( { @@ -168,7 +164,6 @@ class TestAssistantDPOChatTemplatePhi3: """ def test_phi3_defaults(self, phi3_tokenizer, assistant_dataset): - # pylint: disable=duplicate-code transform_fn, _ = default( DictDefault( { @@ -198,7 +193,6 @@ class TestAssistantDPOChatTemplateGemma: """ def test_gemma_defaults(self, gemma_tokenizer, assistant_dataset): - # pylint: disable=duplicate-code transform_fn, _ = default( DictDefault( { diff --git a/tests/prompt_strategies/test_stepwise.py b/tests/prompt_strategies/test_stepwise.py index 2abe4ae187..ad3f7531f6 100644 --- a/tests/prompt_strategies/test_stepwise.py +++ b/tests/prompt_strategies/test_stepwise.py @@ -20,7 +20,6 @@ class TestStepWiseSupervisedPromptTokenizingStrategy: @pytest.fixture() def stepwise_supervised_dataset(self): - # pylint: disable=duplicate-code return Dataset.from_list( [ { diff --git a/tests/test_chunked_xentropy.py b/tests/test_chunked_xentropy.py index 3e439f0a3b..56ac1b1682 100644 --- a/tests/test_chunked_xentropy.py +++ b/tests/test_chunked_xentropy.py @@ -22,7 +22,7 @@ def chunked_fixtures(): return lm_head, hidden_state, labels, vocab_size -def test_chunked_forward(chunked_fixtures): # pylint: disable=redefined-outer-name +def test_chunked_forward(chunked_fixtures): lm_head, hidden_state, labels, vocab_size = chunked_fixtures lm_loss = get_causal_lm_loss() diff --git a/tests/test_datasets.py b/tests/test_datasets.py index 719dfdc197..ea5ee368d1 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -374,7 +374,6 @@ def test_load_hub_with_revision_with_dpo( } ) - # pylint: disable=duplicate-code with patch( "axolotl.utils.data.rl.load_dataset_with_config" ) as mock_load_dataset: diff --git a/tests/test_dict.py b/tests/test_dict.py index 0bcf8ca7bf..19a3701997 100644 --- a/tests/test_dict.py +++ b/tests/test_dict.py @@ -21,26 +21,26 @@ def test_dict_default(self): } ) - assert ( - cfg.key_a.key_b == "value_a" - ), "DictDefault should return value for existing nested keys" + assert cfg.key_a.key_b == "value_a", ( + "DictDefault should return value for existing nested keys" + ) - assert ( - cfg.key_c == "value_c" - ), "DictDefault should return value for existing keys" + assert cfg.key_c == "value_c", ( + "DictDefault should return value for existing keys" + ) - assert ( - cfg.key_d[0] == "value_d" - ), "DictDefault should return value for existing keys in list" + assert cfg.key_d[0] == "value_d", ( + "DictDefault should return value for existing keys in list" + ) - assert ( - "value_e" in cfg.key_d - ), "DictDefault should support in operator for existing keys in list" + assert "value_e" in cfg.key_d, ( + "DictDefault should support in operator for existing keys in list" + ) def test_dict_or_operator(self): cfg = DictDefault({"key_a": {"key_b": "value_b"}, "key_f": "value_g"}) - cfg = cfg | DictDefault( # pylint: disable=unsupported-binary-operation + cfg = cfg | DictDefault( { "key_a": {"key_b": "value_a"}, "key_c": "value_c", @@ -49,9 +49,9 @@ def test_dict_or_operator(self): } ) - assert ( - cfg.key_a.key_b == "value_b" - ), "DictDefault should support OR operator for existing nested keys" + assert cfg.key_a.key_b == "value_b", ( + "DictDefault should support OR operator for existing nested keys" + ) assert cfg.key_c == "value_c", "DictDefault should not delete existing key" @@ -60,9 +60,9 @@ def test_dict_or_operator(self): "value_e", ], "DictDefault should not overwrite existing keys in list" - assert ( - cfg.key_f == "value_g" - ), "DictDefault should support OR operator for existing key" + assert cfg.key_f == "value_g", ( + "DictDefault should support OR operator for existing key" + ) def test_dict_missingkey(self): cfg = DictDefault({}) @@ -72,9 +72,9 @@ def test_dict_missingkey(self): def test_dict_or(self): cfg = DictDefault({}) | DictDefault({}) - assert ( - cfg.random_key is None - ), "DictDefault should return None for missing keys after | operation" + assert cfg.random_key is None, ( + "DictDefault should return None for missing keys after | operation" + ) def test_dict_nested_missingparentkey(self): """ diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index d97aad8ea7..65deb5209b 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -41,9 +41,9 @@ def verify_deduplication(actual_dataset, expected_dataset, dataset_name): assert actual_rows == expected_rows, f"Mismatch in {dataset_name} dataset" # Verify size consistency - assert len(actual_rows) == len( - actual_dataset - ), f"Size mismatch in {dataset_name} dataset after deduplication" + assert len(actual_rows) == len(actual_dataset), ( + f"Size mismatch in {dataset_name} dataset after deduplication" + ) class TestDeduplicateIndividualFunctions(unittest.TestCase): @@ -224,7 +224,6 @@ def test_load_with_deduplication( ): """Verify that loading with deduplication removes duplicates.""" - # pylint: disable=duplicate-code with ( patch( "axolotl.utils.data.rl.load_dataset_with_config" @@ -251,7 +250,6 @@ def test_load_without_deduplication( dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff, tokenizer_huggyllama, ): - # pylint: disable=duplicate-code with ( patch( "axolotl.utils.data.rl.load_dataset_with_config" @@ -271,9 +269,9 @@ def test_load_without_deduplication( train_dataset, _ = prepare_preference_datasets(cfg, tokenizer) # Verify that the dataset retains duplicates - assert ( - len(train_dataset) == 1800 * 2 - ), "Dataset deduplication occurred when it should not have" + assert len(train_dataset) == 1800 * 2, ( + "Dataset deduplication occurred when it should not have" + ) class TestDeduplicateNonRL(unittest.TestCase): diff --git a/tests/test_loaders.py b/tests/test_loaders.py index d45f41998e..f516d0ca4e 100644 --- a/tests/test_loaders.py +++ b/tests/test_loaders.py @@ -17,7 +17,7 @@ class TestModelsUtils: def setup_method(self) -> None: # load config - self.cfg = DictDefault( # pylint: disable=attribute-defined-outside-init + self.cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", "model_type": "AutoModelForCausalLM", @@ -30,20 +30,16 @@ def setup_method(self) -> None: "device_map": "auto", } ) - self.tokenizer = MagicMock( # pylint: disable=attribute-defined-outside-init - spec=PreTrainedTokenizerBase - ) - self.inference = False # pylint: disable=attribute-defined-outside-init - self.reference_model = True # pylint: disable=attribute-defined-outside-init + self.tokenizer = MagicMock(spec=PreTrainedTokenizerBase) + self.inference = False + self.reference_model = True # init ModelLoader - self.model_loader = ( # pylint: disable=attribute-defined-outside-init - ModelLoader( - cfg=self.cfg, - tokenizer=self.tokenizer, - inference=self.inference, - reference_model=self.reference_model, - ) + self.model_loader = ModelLoader( + cfg=self.cfg, + tokenizer=self.tokenizer, + inference=self.inference, + reference_model=self.reference_model, ) def test_set_device_map_config(self): @@ -51,7 +47,7 @@ def test_set_device_map_config(self): device_map = self.cfg.device_map if is_torch_mps_available(): device_map = "mps" - # pylint: disable=protected-access + self.model_loader._set_device_map_config() if is_deepspeed_zero3_enabled(): assert "device_map" not in self.model_loader.model_kwargs @@ -78,7 +74,6 @@ def test_set_quantization_config( self.cfg.gptq = gptq self.cfg.adapter = adapter - # pylint: disable=protected-access self.model_loader._set_quantization_config() if "quantization_config" in self.model_loader.model_kwargs or self.cfg.gptq: assert not ( @@ -194,7 +189,7 @@ def test_get_parallel_config_kwargs( is_fsdp, expected, ): - res = _get_parallel_config_kwargs( # pylint: disable=protected-access + res = _get_parallel_config_kwargs( world_size, tensor_parallel_size, context_parallel_size, diff --git a/tests/test_lora.py b/tests/test_lora.py index 6edcdd88e4..50cbea9bc6 100644 --- a/tests/test_lora.py +++ b/tests/test_lora.py @@ -6,7 +6,6 @@ from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -# pylint: disable=duplicate-code minimal_config = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", diff --git a/tests/test_packed_batch_sampler.py b/tests/test_packed_batch_sampler.py index d839c6ea33..a5db7cbe09 100644 --- a/tests/test_packed_batch_sampler.py +++ b/tests/test_packed_batch_sampler.py @@ -93,7 +93,7 @@ def test_packing( loader = DataLoader( train_dataset, batch_sampler=batch_sampler, - collate_fn=V2BatchSamplerDataCollatorForSeq2Seq( # pylint: disable=unexpected-keyword-arg + collate_fn=V2BatchSamplerDataCollatorForSeq2Seq( tokenizer=tokenizer, padding=True, pad_to_multiple_of=max_seq_length, diff --git a/tests/test_packed_dataset.py b/tests/test_packed_dataset.py index 699d5e6cc6..43e4f3d395 100644 --- a/tests/test_packed_dataset.py +++ b/tests/test_packed_dataset.py @@ -26,7 +26,6 @@ class TestPacking(unittest.TestCase): @enable_hf_offline def setUp(self) -> None: - # pylint: disable=duplicate-code self.tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") self.tokenizer.add_special_tokens( { @@ -75,7 +74,6 @@ def test_increments_attention(self): @with_temp_dir def test_lora_packing(self, temp_dir): - # pylint: disable=duplicate-code cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -127,9 +125,7 @@ def test_lora_packing(self, temp_dir): _, ) = setup_model_and_trainer(cfg, dataset_meta) - sampler = trainer._get_eval_sampler( # pylint: disable=protected-access - trainer.eval_dataset - ) + sampler = trainer._get_eval_sampler(trainer.eval_dataset) assert "MultipackBatchSampler" in sampler.__class__.__name__ assert ( "V2BatchSamplerDataCollatorForSeq2Seq" @@ -140,9 +136,7 @@ def test_lora_packing(self, temp_dir): batch = next(dataloader_iter) assert batch["input_ids"].shape == (1, 8192) - sampler = trainer._get_train_sampler( # pylint: disable=protected-access - trainer.train_dataset - ) + sampler = trainer._get_train_sampler(trainer.train_dataset) assert "MultipackBatchSampler" in sampler.__class__.__name__ assert ( "V2BatchSamplerDataCollatorForSeq2Seq" diff --git a/tests/test_packed_pretraining.py b/tests/test_packed_pretraining.py index 115813df2d..117bc0dbd9 100644 --- a/tests/test_packed_pretraining.py +++ b/tests/test_packed_pretraining.py @@ -76,7 +76,6 @@ def test_packing_stream_dataset(self, tokenizer_huggyllama, random_text): cfg.pretraining_dataset[0]["type"] or "pretrain", ) - # pylint: disable=duplicate-code original_bsz = cfg.micro_batch_size train_dataset = wrap_pretraining_dataset( dataset, diff --git a/tests/test_perplexity.py b/tests/test_perplexity.py index 9a1c9b223f..8f43069947 100644 --- a/tests/test_perplexity.py +++ b/tests/test_perplexity.py @@ -1,7 +1,5 @@ """unit tests for perplexity eval callback""" -# pylint: disable=redefined-outer-name - from pytest import fixture from transformers.models.auto.modeling_auto import AutoModelForCausalLM from transformers.models.auto.tokenization_auto import AutoTokenizer diff --git a/tests/test_prompt_tokenizers.py b/tests/test_prompt_tokenizers.py index 5e5de4ff8e..672643a923 100644 --- a/tests/test_prompt_tokenizers.py +++ b/tests/test_prompt_tokenizers.py @@ -64,7 +64,7 @@ def test_no_sys_prompt(self, tokenizer_huggyllama_w_special_tokens): tests the interface between the user and assistant parts """ prompter = NoSystemPrompter() - # pylint: disable=duplicate-code + strat = AlpacaPromptTokenizingStrategy( prompter, tokenizer_huggyllama_w_special_tokens, @@ -85,7 +85,7 @@ def test_alpaca(self, tokenizer_huggyllama_w_special_tokens): """ tests the interface between the user and assistant parts """ - # pylint: disable=duplicate-code + prompter = AlpacaPrompter() strat = AlpacaPromptTokenizingStrategy( prompter, @@ -171,7 +171,7 @@ def compare_with_transformers_integration(self, tokenizer_llama2_7b): # from transformers.models.llama.tokenization_llama import DEFAULT_SYSTEM_PROMPT # broken as of 23/7/20 # see https://github.com/huggingface/transformers/pull/24935 - # pylint: disable=C0103 + DEFAULT_SYSTEM_PROMPT = """\ You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature. @@ -201,7 +201,7 @@ def compare_with_transformers_integration(self, tokenizer_llama2_7b): + user_input[1:-1], generated_responses=answers, ) - # pylint: disable=W0212 + hf_tokens = tokenizer_llama2_7b._build_conversation_input_ids(hf_conf) assert hf_tokens == tokenized_conversation["input_ids"][: len(hf_tokens)] diff --git a/tests/test_schedulers.py b/tests/test_schedulers.py index 92664cca81..c783a68db7 100644 --- a/tests/test_schedulers.py +++ b/tests/test_schedulers.py @@ -22,7 +22,7 @@ def setUp(self): self.constant_lr_ratio = 0.8 self._lr = 0.01 self.optimizer = SGD([torch.tensor(1)], lr=self._lr) - self.lr_scheduler = get_cosine_schedule_with_warmup_decay_constant( # pylint: disable=attribute-defined-outside-init + self.lr_scheduler = get_cosine_schedule_with_warmup_decay_constant( self.optimizer, num_warmup_steps=self.warmup_steps, num_training_steps=self.train_steps, diff --git a/tests/test_validation_dataset.py b/tests/test_validation_dataset.py index 1a4c973147..3d3b5db96c 100644 --- a/tests/test_validation_dataset.py +++ b/tests/test_validation_dataset.py @@ -24,7 +24,6 @@ def fixture_cfg(): ) -# pylint: disable=too-many-public-methods (duplicate-code) class BaseValidation: """ Base validation module to setup the log capture diff --git a/tests/utils/schemas/validation/test_fsdp.py b/tests/utils/schemas/validation/test_fsdp.py index 5b461a1136..08fc50c610 100644 --- a/tests/utils/schemas/validation/test_fsdp.py +++ b/tests/utils/schemas/validation/test_fsdp.py @@ -2,7 +2,6 @@ tests for pydantic fsdp validation """ -# pylint: disable=too-many-boolean-expressions import pytest from axolotl.utils.config import validate_config From 0de254a0d043c864f97269175e0a04e00b78707b Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 26 Aug 2025 16:47:26 +0700 Subject: [PATCH 0935/1405] feat: add gemma3_text attention handling for lora kernels (#3103) --- src/axolotl/monkeypatch/lora_kernels.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index ef5174ba29..e845dc6ceb 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -149,6 +149,11 @@ def get_attention_cls_from_config(cfg: DictDefault) -> Type[nn.Module]: return MistralAttention + if model_type == "gemma3_text": + from transformers.models.gemma3.modeling_gemma3 import Gemma3Attention + + return Gemma3Attention + try: # Dynamically import the module and attention class module_path = f"transformers.models.{model_type}.modeling_{model_type}" From 0e9945e3b91e853b36e97c0dbd29bfd778382511 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 26 Aug 2025 09:29:50 -0400 Subject: [PATCH 0936/1405] deploy training jobs to baseten w truss in axolotl cli (#3086) [skip ci] * deploy training jobs to baseten w truss in axolotl cli * cleanup --- examples/cloud/baseten.yaml | 10 +++ src/axolotl/cli/cloud/__init__.py | 13 +++- src/axolotl/cli/cloud/baseten/__init__.py | 48 +++++++++++++ src/axolotl/cli/cloud/baseten/template/run.sh | 9 +++ .../cli/cloud/baseten/template/train_sft.py | 71 +++++++++++++++++++ 5 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 examples/cloud/baseten.yaml create mode 100644 src/axolotl/cli/cloud/baseten/__init__.py create mode 100644 src/axolotl/cli/cloud/baseten/template/run.sh create mode 100644 src/axolotl/cli/cloud/baseten/template/train_sft.py diff --git a/examples/cloud/baseten.yaml b/examples/cloud/baseten.yaml new file mode 100644 index 0000000000..23c4b52d69 --- /dev/null +++ b/examples/cloud/baseten.yaml @@ -0,0 +1,10 @@ +provider: baseten +project_name: + +secrets: + - HF_TOKEN + - WANDB_API_KEY + +gpu: h100 +gpu_count: 8 +node_count: 1 diff --git a/src/axolotl/cli/cloud/__init__.py b/src/axolotl/cli/cloud/__init__.py index bf12ab8cbc..60f6a51ce4 100644 --- a/src/axolotl/cli/cloud/__init__.py +++ b/src/axolotl/cli/cloud/__init__.py @@ -7,6 +7,8 @@ import yaml +from axolotl.cli.cloud.base import Cloud +from axolotl.cli.cloud.baseten import BasetenCloud from axolotl.cli.cloud.modal_ import ModalCloud from axolotl.utils.dict import DictDefault @@ -38,8 +40,15 @@ def do_cli_train( cwd=None, **kwargs, ) -> None: - cloud_cfg = load_cloud_cfg(cloud_config) - cloud = ModalCloud(cloud_cfg) + cloud_cfg: DictDefault = load_cloud_cfg(cloud_config) + provider = cloud_cfg.provider or "modal" + cloud: Cloud | None + if provider == "modal": + cloud = ModalCloud(cloud_cfg) + elif provider == "baseten": + cloud = BasetenCloud(cloud_cfg.to_dict()) + else: + raise ValueError(f"Unsupported cloud provider: {provider}") with open(config, "r", encoding="utf-8") as file: config_yaml = file.read() local_dirs = {} diff --git a/src/axolotl/cli/cloud/baseten/__init__.py b/src/axolotl/cli/cloud/baseten/__init__.py new file mode 100644 index 0000000000..914504de3a --- /dev/null +++ b/src/axolotl/cli/cloud/baseten/__init__.py @@ -0,0 +1,48 @@ +"""Baseten Cloud CLI""" + +import shutil +import subprocess # nosec B404 +import tempfile +from os.path import dirname +from typing import Literal + +import yaml + +from axolotl.cli.cloud.base import Cloud + + +class BasetenCloud(Cloud): + """Baseten Cloud Axolotl CLI""" + + def __init__(self, config: dict): + self.config = config + + def preprocess(self, config_yaml: str, *args, **kwargs) -> None: + raise NotImplementedError( + "Separate preprocess function for Baseten is not " + "implemented and will happen during hte train step." + ) + + def train( + self, + config_yaml: str, + launcher: Literal["accelerate", "torchrun", "python"] = "accelerate", + launcher_args: list[str] | None = None, + local_dirs: dict[str, str] | None = None, # pylint: disable=unused-argument + **kwargs, + ): + with tempfile.TemporaryDirectory() as tmp_dir: + config = self.config.copy() + config["launcher"] = launcher + config["launcher_args"] = launcher_args + with open(tmp_dir + "/cloud.yaml", "w", encoding="utf-8") as cloud_fout: + yaml.dump(config, cloud_fout) + with open(tmp_dir + "/train.yaml", "w", encoding="utf-8") as config_fout: + config_fout.write(config_yaml) + shutil.copyfile(dirname(__file__) + "/template/run.sh", tmp_dir + "/run.sh") + shutil.copyfile( + dirname(__file__) + "/template/train_sft.py", tmp_dir + "/train_sft.py" + ) + subprocess.run( # nosec B603 B607 + ["truss", "train", "push", "train_sft.py"], cwd=tmp_dir, check=False + ) diff --git a/src/axolotl/cli/cloud/baseten/template/run.sh b/src/axolotl/cli/cloud/baseten/template/run.sh new file mode 100644 index 0000000000..37dc9688f2 --- /dev/null +++ b/src/axolotl/cli/cloud/baseten/template/run.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -eux + +export NCCL_SOCKET_IFNAME="^docker0,lo" +export NCCL_IB_DISABLE=0 +export NCCL_TIMEOUT=1800000 + +axolotl preprocess train.yaml +axolotl train train.yaml --launcher ${AXOLOTL_LAUNCHER} ${AXOLOTL_LAUNCHER_ARGS} diff --git a/src/axolotl/cli/cloud/baseten/template/train_sft.py b/src/axolotl/cli/cloud/baseten/template/train_sft.py new file mode 100644 index 0000000000..137fb91714 --- /dev/null +++ b/src/axolotl/cli/cloud/baseten/template/train_sft.py @@ -0,0 +1,71 @@ +""" +Baseten Training Script for Axolotl +""" + +# pylint: skip-file +import yaml +from truss.base import truss_config + +# Import necessary classes from the Baseten Training SDK +from truss_train import definitions + +cloud_config = yaml.safe_load(open("cloud.yaml", "r")) +gpu = cloud_config.get("gpu", "h100") +gpu_count = int(cloud_config.get("gpu_count", 1)) +node_count = int(cloud_config.get("node_count", 1)) +project_name = cloud_config.get("project_name", "axolotl-project") or "axolotl-project" +secrets = cloud_config.get("secrets", []) +launcher = cloud_config.get("launcher", "accelerate") +launcher_args = cloud_config.get("launcher_args", []) +script_name = "run.sh" + +launcher_args_str = "" +if launcher_args: + launcher_args_str = "-- " + " ".join(launcher_args) + +# 1. Define a base image for your training job +# must use torch 2.7.0 for vllm +BASE_IMAGE = "axolotlai/axolotl:main-py3.11-cu126-2.7.1" + +# 2. Define the Runtime Environment for the Training Job +# This includes start commands and environment variables.a +# Secrets from the baseten workspace like API keys are referenced using +# `SecretReference`. + +env_vars = { + "AXOLOTL_LAUNCHER": launcher, + "AXOLOTL_LAUNCHER_ARGS": launcher_args_str, +} +for secret_name in secrets: + env_vars[secret_name] = definitions.SecretReference(name=secret_name) + +training_runtime = definitions.Runtime( + start_commands=[ # Example: list of commands to run your training script + f"/bin/sh -c 'chmod +x ./{script_name} && ./{script_name}'" + ], + environment_variables=env_vars, +) + +# 3. Define the Compute Resources for the Training Job +training_compute = definitions.Compute( + node_count=node_count, + accelerator=truss_config.AcceleratorSpec( + accelerator=truss_config.Accelerator.H100, + count=gpu_count, + ), +) + +# 4. Define the Training Job +# This brings together the image, compute, and runtime configurations. +my_training_job = definitions.TrainingJob( + image=definitions.Image(base_image=BASE_IMAGE), + compute=training_compute, + runtime=training_runtime, +) + + +# This config will be pushed using the Truss CLI. +# The association of the job to the project happens at the time of push. +first_project_with_job = definitions.TrainingProject( + name=project_name, job=my_training_job +) From c4c4b906382fed7ec3b3dfb7ef2d6f4734962c60 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 26 Aug 2025 09:30:04 -0400 Subject: [PATCH 0937/1405] add tokenizer_save_jinja_files to keep legacy behavior of including chat template in tokenizer_config.json (#3093) * add tokenizer_save_jinja_files to keep legacy behavior of including chat template in tokenizer_config.json * fix test import --- src/axolotl/cli/merge_lora.py | 5 ++- src/axolotl/cli/quantize.py | 1 + src/axolotl/core/builders/causal.py | 3 ++ src/axolotl/core/trainers/base.py | 17 +++++++- src/axolotl/train.py | 7 +++- src/axolotl/utils/config/__init__.py | 2 +- src/axolotl/utils/schemas/model.py | 6 +++ tests/e2e/test_tokenizer.py | 63 ++++++++++++++++++++++++++++ 8 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 tests/e2e/test_tokenizer.py diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 31fad1b297..657ddcfe43 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -43,7 +43,10 @@ def do_merge_lora(*, cfg: DictDefault) -> None: safe_serialization=safe_serialization, progressbar=True, ) - tokenizer.save_pretrained(str(Path(cfg.output_dir) / "merged")) + tokenizer.save_pretrained( + str(Path(cfg.output_dir) / "merged"), + save_jinja_files=cfg.tokenizer_save_jinja_files, + ) if processor: processor.save_pretrained(str(Path(cfg.output_dir) / "merged")) diff --git a/src/axolotl/cli/quantize.py b/src/axolotl/cli/quantize.py index 0782976fe6..b8a8de7812 100644 --- a/src/axolotl/cli/quantize.py +++ b/src/axolotl/cli/quantize.py @@ -84,5 +84,6 @@ def do_quantize( str(Path(output_dir) / "quantized"), safe_serialization=False, progressbar=True, + save_jinja_files=cfg.tokenizer_save_jinja_files, ) LOG.info(f"Quantized model saved to: {str(Path(output_dir) / 'quantized')}...") diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 94b0db8515..e5bc68c39d 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -404,6 +404,9 @@ def build(self, total_num_steps): **trainer_kwargs, ) trainer = self.hook_post_create_trainer(trainer) + # if the trainer has the `axolotl_cfg` property, set it + if hasattr(trainer, "axolotl_cfg"): + trainer.axolotl_cfg = self.cfg for callback in self.get_post_trainer_create_callbacks(trainer): trainer.add_callback(callback) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 4b88617900..f707d4b5a4 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -42,6 +42,7 @@ ) from axolotl.utils import get_not_null from axolotl.utils.bench import get_gpu_memory_usage +from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import is_main_process from axolotl.utils.logging import get_logger from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths @@ -63,6 +64,15 @@ class AxolotlTrainer( args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] tag_names = ["axolotl"] + _axolotl_cfg: DictDefault | None = None + + @property + def axolotl_cfg(self): + return self._axolotl_cfg + + @axolotl_cfg.setter + def axolotl_cfg(self, cfg): + self._axolotl_cfg = cfg def __init__( self, @@ -657,6 +667,11 @@ def _save(self, output_dir: Optional[str] = None, state_dict=None): LOG.info( "Saving Trainer.data_collator.tokenizer by default as Trainer.processing_class is `None`" ) - self.data_collator.tokenizer.save_pretrained(output_dir) + save_jinja_files = True + if self.axolotl_cfg: + save_jinja_files = self.axolotl_cfg.tokenizer_save_jinja_files + self.data_collator.tokenizer.save_pretrained( + output_dir, save_jinja_files=save_jinja_files + ) # Good practice: save your training arguments together with the trained model torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME)) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index e409d4a11e..e8e3145795 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -416,7 +416,9 @@ def save_initial_configs( # Pre-save the tokenizer and model configs LOG.info(f"Pre-saving tokenizer to {cfg.output_dir}...") - tokenizer.save_pretrained(str(output_dir)) + tokenizer.save_pretrained( + str(Path(cfg.output_dir)), save_jinja_files=cfg.tokenizer_save_jinja_files + ) if hasattr(model, "config"): LOG.info(f"Pre-saving model config to {cfg.output_dir}...") model.config.save_pretrained(str(output_dir)) @@ -592,6 +594,9 @@ def train( # Save the trained model and cleanup save_trained_model(cfg, trainer, model, safe_serialization) + tokenizer.save_pretrained( + str(Path(cfg.output_dir)), save_jinja_files=cfg.tokenizer_save_jinja_files + ) create_model_card(cfg, trainer) if not cfg.use_ray: cleanup_distributed() diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 534d7c4a4c..2b6ef8d985 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -77,7 +77,7 @@ def resolve_dtype(cfg): if cfg.device == "mps": cfg.load_in_8bit = False cfg.tf32 = False - if cfg.bf16: + if cfg.bf16 and cfg.fp16 is not False: cfg.fp16 = True cfg.bf16 = False else: diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index eb751bfccf..56b206b51b 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -59,6 +59,12 @@ class ModelInputConfig(BaseModel): processor_type: str | None = Field( default=None, json_schema_extra={"description": "transformers processor class"} ) + tokenizer_save_jinja_files: bool | None = Field( + default=True, # match the default behavior from transformers + json_schema_extra={ + "description": "Whether to save jinja files for tokenizer, transformers default is True" + }, + ) trust_remote_code: bool | None = Field( default=None, json_schema_extra={"description": "Trust remote code for untrusted source"}, diff --git a/tests/e2e/test_tokenizer.py b/tests/e2e/test_tokenizer.py new file mode 100644 index 0000000000..a65c17ac31 --- /dev/null +++ b/tests/e2e/test_tokenizer.py @@ -0,0 +1,63 @@ +""" +e2e test for saving the tokenizer +""" + +from unittest.mock import patch + +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import check_model_output_exists + + +def test_tokenizer_no_save_jinja_files(temp_dir): + # pylint: disable=duplicate-code + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", + "sequence_len": 1024, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.02, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "chat_template": "chatml", + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "max_steps": 5, + "save_first_step": False, + "fp16": False, + "tokenizer_save_jinja_files": False, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + with patch("axolotl.train.execute_training"): + train(cfg=cfg, dataset_meta=dataset_meta) + + check_model_output_exists(temp_dir, cfg) + with open(f"{temp_dir}/tokenizer_config.json", "r", encoding="utf-8") as f: + tokenizer_config = f.read() + assert "chat_template" in tokenizer_config From e1131e9619f9c86cdd8f2fec1774e41354972238 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 26 Aug 2025 09:30:22 -0400 Subject: [PATCH 0938/1405] make always skip_move_to_device default as true (#3084) --- src/axolotl/utils/schemas/model.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index 56b206b51b..04312eeddc 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -71,10 +71,9 @@ class ModelInputConfig(BaseModel): ) experimental_skip_move_to_device: bool | None = Field( - default=None, + default=True, json_schema_extra={ - "description": "Don't move the model to the device before sharding. " - "This is an experimental feature that may be included in the future as the default." + "description": "Don't move the model to the device before sharding. Set to `false` to revert to legacy behavior." }, ) From d0d2fc56069fae1f87c9370338f6730fa7976c49 Mon Sep 17 00:00:00 2001 From: salman Date: Wed, 27 Aug 2025 09:10:14 +0100 Subject: [PATCH 0939/1405] Tokens per second logging [skip-e2e] (#3072) --- src/axolotl/core/builders/base.py | 12 +++- src/axolotl/core/trainers/base.py | 28 ++++++--- src/axolotl/core/training_args_base.py | 6 ++ src/axolotl/utils/bench.py | 7 ++- .../utils/callbacks/tokens_per_second.py | 62 +++++++++++++++++++ src/axolotl/utils/schemas/config.py | 9 ++- 6 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 src/axolotl/utils/callbacks/tokens_per_second.py diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 44699e6ac2..bee291fa2a 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -24,9 +24,7 @@ from typing import Any import torch -from transformers import ( - TrainerCallback, -) +from transformers import TrainerCallback from transformers.trainer_pt_utils import AcceleratorConfig from axolotl.integrations.base import PluginManager @@ -38,6 +36,7 @@ SaveModelOnFirstStepCallback, ) from axolotl.utils.callbacks.profiler import PytorchProfilerCallback +from axolotl.utils.callbacks.tokens_per_second import TokensPerSecondCallback from axolotl.utils.distributed import build_parallelism_config from axolotl.utils.schemas.enums import CustomSupportedOptimizers @@ -146,6 +145,12 @@ def get_callbacks(self) -> list[TrainerCallback]: profiler_steps_start=self.cfg.profiler_steps_start, ) ) + if self.cfg.include_tkps: + callbacks.append( + TokensPerSecondCallback( + self.cfg.tensor_parallel_size, self.cfg.context_parallel_size + ) + ) return callbacks @@ -512,6 +517,7 @@ def _set_base_training_args( self.cfg.eval_batch_size ) + training_args_kwargs["include_tkps"] = self.cfg.include_tkps training_args_kwargs["max_steps"] = self.cfg.max_steps or total_num_steps or -1 training_args_kwargs["num_train_epochs"] = self.cfg.num_epochs diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index f707d4b5a4..06eef445be 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -88,7 +88,6 @@ def __init__( self._signature_columns = None # workaround for pylint super().__init__(*_args, **kwargs) - self.train_data_collator = self.data_collator self._stored_metrics = defaultdict(lambda: defaultdict(list)) if self.args.orpo_alpha: @@ -337,6 +336,17 @@ def compute_loss( # outputs = model(**inputs) # loss = trainer_weighted_loss(outputs, labels, shift_labels=True) # return (loss, outputs) if return_outputs else loss + + # track number of tokens for tokens per second calculation + if self.args.include_tkps: + inputs_key = "labels" if "labels" in inputs else "input_ids" + if hasattr(self.state, "num_tokens"): + self.state.num_tokens = ( + self.state.num_tokens + (inputs[inputs_key] != -100).sum() + ) + else: + self.state.num_tokens = (inputs[inputs_key] != -100).sum() + if self.args.orpo_alpha: return self.orpo_compute_loss( model, @@ -536,9 +546,6 @@ def create_accelerator_and_postprocess(self): super().create_accelerator_and_postprocess() - # now we need to put parallelism_config back on the PartialState since we rely on that info in other places - # PartialState().parallelism_config = self.accelerator.state.parallelism_config - if self.is_fsdp_enabled: if ( "limit_all_gathers" in self.args.fsdp_config @@ -586,12 +593,19 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: # Add memory usage try: active, allocated, reserved = get_gpu_memory_usage() - logs["memory/max_mem_active(gib)"] = round(active, 2) - logs["memory/max_mem_allocated(gib)"] = round(allocated, 2) - logs["memory/device_mem_reserved(gib)"] = round(reserved, 2) + logs["memory/max_active (GiB)"] = round(active, 2) + logs["memory/max_allocated (GiB)"] = round(allocated, 2) + logs["memory/device_reserved (GiB)"] = round(reserved, 2) except (ValueError, TypeError, FileNotFoundError): pass + if self.args.include_tkps and train_eval == "train": + # each rank will log its own tokens per second + # for logging_steps > 1 we obtain a moving average of this metric + logs["tokens_per_second_per_gpu"] = round( + self.state.last_tokens_per_second.item() / self.args.logging_steps, 2 + ) + del self._stored_metrics[train_eval] return super().log(logs, start_time) diff --git a/src/axolotl/core/training_args_base.py b/src/axolotl/core/training_args_base.py index a9cc7d2243..41ee8e91eb 100644 --- a/src/axolotl/core/training_args_base.py +++ b/src/axolotl/core/training_args_base.py @@ -49,6 +49,12 @@ class AxolotlTrainingMixins: default=False, metadata={"help": "Use real batches for efficient training."}, ) + include_tkps: bool = field( + default=True, + metadata={ + "help": "Whether to include tokens per second in the training metrics." + }, + ) eval_sample_packing: Optional[bool] = field( default=None, metadata={"help": "Use sample packing for efficient evals."}, diff --git a/src/axolotl/utils/bench.py b/src/axolotl/utils/bench.py index dd3a85b8c6..0a45949915 100644 --- a/src/axolotl/utils/bench.py +++ b/src/axolotl/utils/bench.py @@ -60,13 +60,14 @@ def gpu_memory_usage_all(device=0): active = torch.cuda.memory_stats().get("active_bytes.all.peak", 0) / 1024.0**3 allocated = torch.cuda.max_memory_allocated(device) / 1024.0**3 reserved = torch.cuda.max_memory_reserved(device) / 1024.0**3 + torch.cuda.reset_peak_memory_stats(device) return active, allocated, reserved def mps_memory_usage_all(): - usage = torch.mps.current_allocated_memory() / 1024.0**3 - reserved = torch.mps.driver_allocated_memory() / 1024.0**3 - return usage, reserved - usage, 0 + active = torch.mps.current_allocated_memory() / 1024.0**3 + allocated = torch.mps.driver_allocated_memory() / 1024.0**3 + return active, allocated, 0 def npu_memory_usage_all(device=0): diff --git a/src/axolotl/utils/callbacks/tokens_per_second.py b/src/axolotl/utils/callbacks/tokens_per_second.py new file mode 100644 index 0000000000..85bcd50412 --- /dev/null +++ b/src/axolotl/utils/callbacks/tokens_per_second.py @@ -0,0 +1,62 @@ +"""A callback for calculating tokens per second during training.""" + +import time + +import torch +from transformers import ( + TrainerCallback, + TrainerControl, + TrainerState, + TrainingArguments, +) + + +class TokensPerSecondCallback(TrainerCallback): + """ + A callback to measure and log tokens per second during training. + """ + + def __init__(self, tensor_parallel_size, context_parallel_size): + super().__init__() + self.step_time = 0.0 + self.start_time = 0.0 + self.non_data_parallel_size = 1 + if tensor_parallel_size is not None: + self.non_data_parallel_size *= tensor_parallel_size + if context_parallel_size is not None: + self.non_data_parallel_size *= context_parallel_size + + def on_step_begin( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): # pylint: disable=unused-argument + self.start_time = time.perf_counter() + state.last_tokens_per_second = torch.zeros(1) + + def on_step_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): # pylint: disable=unused-argument + step_time = time.perf_counter() - self.start_time + num_tokens_per_device = state.num_tokens.clone() + # non data parallel groups have duplicated tokens, so we avoid double-counting + num_tokens_per_device = num_tokens_per_device / self.non_data_parallel_size + state.last_tokens_per_second = num_tokens_per_device / step_time + + def on_log( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + logs=None, + **kwargs, + ): # pylint: disable=unused-argument + # after logging, clear the running metrics + state.last_tokens_per_second.zero_() + state.num_tokens = 0 diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 4d660d4b75..4b5f571dcf 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -830,10 +830,15 @@ class AxolotlInputConfig( include_tokens_per_second: bool | None = Field( default=None, json_schema_extra={ - "description": "bool of whether to include tokens trainer per second in the training metrics. This iterates over the entire dataset once, so it takes some time." + "description": "bool of whether to report tokens per second at the end of training. This is not supported with pre-training datasets." + }, + ) + include_tkps: bool | None = Field( + default=None, + json_schema_extra={ + "description": "bool of whether to report tokens per second during training by measuring throughput of non-padding tokens." }, ) - neftune_noise_alpha: float | None = Field( default=None, json_schema_extra={ From dc338c3b0eccdfd12b2fdd83eeff1e48c7965bc1 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 27 Aug 2025 09:50:52 -0400 Subject: [PATCH 0940/1405] Update .coderabbit.yaml (#3109) [skip ci] Oops, should be false. --- .coderabbit.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index b7cf7d9694..821d6bd5b1 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -12,6 +12,6 @@ reviews: auto_review: enabled: true drafts: false - auto_incremental_review: true + auto_incremental_review: false chat: auto_reply: true From 6afba3871d3f6748372afc6289722ce82c5c00c7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 28 Aug 2025 09:10:40 -0400 Subject: [PATCH 0941/1405] Add support for PyTorch 2.8.0 (#3106) * Add support for PyTorch 2.8.0 * loosen triton requirements * handle torch 2.8.0 in setup.py * fix versions * no vllm for torch 2.8.0 * remove comment Co-authored-by: NanoCode012 --------- Co-authored-by: NanoCode012 --- .github/workflows/main.yml | 16 ++++++++++++++++ .github/workflows/multi-gpu-e2e.yml | 12 ++++++------ .github/workflows/tests.yml | 18 ++++++++++++------ .../colab-axolotl-example.ipynb | 2 +- requirements.txt | 3 +-- scripts/cutcrossentropy_install.py | 2 +- setup.py | 4 +++- .../integrations/cut_cross_entropy/README.md | 2 +- .../integrations/cut_cross_entropy/__init__.py | 2 +- 9 files changed, 42 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3daf39e43f..3f98dd2b42 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,6 +36,11 @@ jobs: python_version: "3.11" pytorch: 2.7.1 axolotl_extras: + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.8.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -110,6 +115,11 @@ jobs: python_version: "3.11" pytorch: 2.7.1 axolotl_extras: + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.8.0 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -169,6 +179,12 @@ jobs: pytorch: 2.7.1 axolotl_extras: vllm is_latest: true + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.8.0 + axolotl_extras: + is_latest: runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 3085261511..6492e5d3ec 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -36,15 +36,15 @@ jobs: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" - pytorch: 2.7.0 - axolotl_extras: + pytorch: 2.7.1 + axolotl_extras: vllm num_gpus: 2 nightly_build: "true" - - cuda: 126 - cuda_version: 12.6.3 + - cuda: 128 + cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.1 - axolotl_extras: vllm + pytorch: 2.8.0 + axolotl_extras: num_gpus: 2 nightly_build: "true" runs-on: [self-hosted, modal] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fe63aa313a..59011ee77b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.6.0", "2.7.0", "2.7.1"] + pytorch_version: ["2.6.0", "2.7.1", "2.8.0"] timeout-minutes: 20 steps: @@ -130,7 +130,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.6.0", "2.7.0", "2.7.1"] + pytorch_version: ["2.6.0", "2.7.1", "2.8.0"] timeout-minutes: 20 steps: @@ -240,7 +240,7 @@ jobs: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" - pytorch: 2.6.0 + pytorch: 2.7.1 num_gpus: 1 axolotl_extras: dockerfile: "Dockerfile-uv.jinja" @@ -298,6 +298,12 @@ jobs: pytorch: 2.7.1 num_gpus: 1 axolotl_extras: + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.8.0 + num_gpus: 1 + axolotl_extras: steps: - name: Checkout uses: actions/checkout@v4 @@ -334,10 +340,10 @@ jobs: fail-fast: false matrix: include: - - cuda: 124 - cuda_version: 12.4.1 + - cuda: 126 + cuda_version: 12.6.3 python_version: "3.11" - pytorch: 2.6.0 + pytorch: 2.7.1 num_gpus: 1 axolotl_extras: steps: diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 30ef1c3de0..b780a1c48d 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0ee9ee8\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c6a32c5\"" ] }, { diff --git a/requirements.txt b/requirements.txt index c51c9d1fe9..5accd13ed6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,7 @@ # START section of dependencies that don't install on Darwin/MacOS bitsandbytes==0.47.0 -# triton 3.4.0 is not compatible with CCE -triton>=3.0.0,<3.4.0 +triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 autoawq==0.2.7.post3 diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index b2bb0fcf83..5b49e7427f 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0ee9ee8"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c6a32c5"' ) diff --git a/setup.py b/setup.py index 5aab9d7c08..5bf9ae8403 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,9 @@ def parse_requirements(extras_require_map): else: raise ValueError("Invalid version format") - if (major, minor) >= (2, 7): + if (major, minor) >= (2, 8): + pass + elif (major, minor) >= (2, 7): _install_requires.pop(_install_requires.index(xformers_version)) if patch == 0: _install_requires.append("xformers==0.0.30") diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 02e4e6686c..a64bdd0548 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0ee9ee8" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c6a32c5" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 6dd7c97e1f..d0eb1ebdb6 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0ee9ee8"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c6a32c5"`' ) From 5b6ec2820f26ce4b50c624b41453d93b2a9c6063 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 29 Aug 2025 21:42:09 +0530 Subject: [PATCH 0942/1405] patch for ds_grads_remaining in deepspeed (#3102) [skip ci] * patch deepspeed * deepspeed patch for ds_grads_remaining * patch in Patchmanager * chore: lint * deepseed utils * chore2 * patch ds_grads_remaining chore * chore lint * chore lint * remove torch.nn patch * lint * Update src/axolotl/monkeypatch/utils.py Co-authored-by: NanoCode012 * patched with checkpointwarapper * lint * only apply deepspeed patch when using activation offloading --------- Co-authored-by: NanoCode012 Co-authored-by: Wing Lian --- src/axolotl/loaders/patch_manager.py | 15 +++++ src/axolotl/monkeypatch/deepspeed_utils.py | 66 ++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 src/axolotl/monkeypatch/deepspeed_utils.py diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 4959bd6ba1..eafe89d29b 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -3,6 +3,7 @@ Applies pre- and post-model load patches for various fixes and optimizations. """ +import os import importlib.util from functools import cached_property @@ -66,6 +67,7 @@ def apply_pre_model_load_patches(self): self._apply_mistral_cross_entropy_patch() self._apply_self_attention_lora_patch() self._apply_fsdp2_bnb_patches() + self._apply_patch_deepspeed_zero3() def apply_post_plugin_pre_model_load_patches(self): """Apply post plugin-pre_model_load load patches based on config.""" @@ -471,3 +473,16 @@ def _apply_lora_kernel_patch(self, model): from axolotl.monkeypatch.lora_kernels import apply_lora_kernel_patches apply_lora_kernel_patches(model=model, cfg=self.cfg) + + def _apply_patch_deepspeed_zero3(self): + try: + from axolotl.monkeypatch.deepspeed_utils import apply_deepspeed_patches + from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled + + if self.cfg.activation_offloading is True and ( + is_deepspeed_zero3_enabled() + or os.getenv("ACCELERATE_DEEPSPEED_ZERO_STAGE") == "3" + ): + apply_deepspeed_patches() + except ImportError as e: + LOG.warning(f"DeepSpeed patches not applied: {e}") diff --git a/src/axolotl/monkeypatch/deepspeed_utils.py b/src/axolotl/monkeypatch/deepspeed_utils.py new file mode 100644 index 0000000000..6740f556be --- /dev/null +++ b/src/axolotl/monkeypatch/deepspeed_utils.py @@ -0,0 +1,66 @@ +import importlib +import importlib.util +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def patch_checkpoint_wrapper_setattr(): + """ + Patch CheckpointWrapper to properly forward DeepSpeed attributes to wrapped modules. + + This fixes the issue where CheckpointWrapper doesn't forward ds_* attributes + (like ds_grads_remaining) to the actual wrapped module, causing DeepSpeed + ZeRO-3 to fail when gradient checkpointing is enabled. + + This issue occurs specifically with: + - QLoRA + DeepSpeed ZeRO-3 + - gradient_checkpointing: true + - activation_offloading: true + + References: + - https://github.com/deepspeedai/DeepSpeed/issues/7203 + - https://github.com/deepspeedai/DeepSpeed/blob/38d1a9eb64c9e01e32eccc50b25ba18925287441/deepspeed/runtime/zero/parameter_offload.py#L424-L458 + - https://github.com/axolotl-ai-cloud/axolotl/pull/3102 + """ + + try: + from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import ( + CheckpointWrapper, + ) + + # Check if already patched + if hasattr(CheckpointWrapper, "_axolotl_setattr_patched"): + LOG.debug("CheckpointWrapper already patched") + return + + original_setattr = CheckpointWrapper.__setattr__ + + def new_setattr(self, name: str, value) -> None: + if name.startswith("ds_") and hasattr(self, "_checkpoint_wrapped_module"): + setattr(self._checkpoint_wrapped_module, name, value) + LOG.debug( + f"Forwarded {name} to wrapped module {type(self._checkpoint_wrapped_module).__name__}" + ) + else: + original_setattr(self, name, value) + + CheckpointWrapper.__setattr__ = new_setattr + CheckpointWrapper._axolotl_setattr_patched = True + + LOG.info("CheckpointWrapper patched to forward DeepSpeed attributes") + + except ImportError as e: + LOG.debug(f"CheckpointWrapper not available: {e}") + except Exception as e: + LOG.warning(f"Failed to patch CheckpointWrapper: {e}") + + +def apply_deepspeed_patches(): + """ + Apply DeepSpeed-related patches + """ + if importlib.util.find_spec("deepspeed") is not None: + patch_checkpoint_wrapper_setattr() + else: + LOG.debug("DeepSpeed not available, skipping patches") From 7ed40f1d70957f83447f4d93f72b8d0015dab34d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 29 Aug 2025 13:36:47 -0400 Subject: [PATCH 0943/1405] automatically set env vars for single gpu deepspeed zero3 (#3118) [skip ci] * automatically set env vars for single gpu deepspeed zero3 * use setdefault --- docs/multi-gpu.qmd | 9 --------- src/axolotl/utils/trainer.py | 7 +++++++ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/multi-gpu.qmd b/docs/multi-gpu.qmd index 71676bc843..fb91f81e5b 100644 --- a/docs/multi-gpu.qmd +++ b/docs/multi-gpu.qmd @@ -63,15 +63,6 @@ Start from Stage 1 -> Stage 2 -> Stage 3. ::: -::: {.callout-tip} - -Using ZeRO Stage 3 with Single-GPU training - -ZeRO Stage 3 can be used for training on a single GPU by manually setting the environment variables: -`WORLD_SIZE=1 LOCAL_RANK=0 MASTER_ADDR=0.0.0.0 MASTER_PORT=29500` - -::: - ## Fully Sharded Data Parallel (FSDP) {#sec-fsdp} ::: {.callout-note} diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 08038cb189..43f76c0cd0 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -547,6 +547,13 @@ def setup_deepspeed_env(cfg, stage=None): if stage == 3: os.environ["ACCELERATE_DEEPSPEED_ZERO3_INIT"] = "true" + device_count = torch.cuda.device_count() + if device_count == 1: + os.environ.setdefault("WORLD_SIZE", "1") + os.environ.setdefault("LOCAL_RANK", "0") + os.environ.setdefault("MASTER_ADDR", "0.0.0.0") # nosec B104 + os.environ.setdefault("MASTER_PORT", "29500") + # NOTE(djsaunde): The distribued state cannot be initialized prior to the # ACCELERATE_USE_DEEPSPEED assignment, but it must be initialized some time prior # to model load. From 0094a2d744553fb89e4874e2d79ac309f26cae77 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 29 Aug 2025 13:52:49 -0400 Subject: [PATCH 0944/1405] support for tiledmlp for GPT-OSS (#3116) * fix use of flex attn kwargs and add support for tiledmlp for GPT-OSS * add logging back * update deps --- requirements.txt | 2 +- setup.py | 2 +- src/axolotl/loaders/patch_manager.py | 14 +- .../monkeypatch/attention/flex_attn.py | 174 +++--------------- src/axolotl/monkeypatch/tiled_mlp/base.py | 107 ++++++++++- src/axolotl/monkeypatch/tiled_mlp/patch.py | 7 +- 6 files changed, 144 insertions(+), 162 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5accd13ed6..9e3dbbca4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ packaging==23.2 huggingface_hub>=0.33.0 peft>=0.17.0 -transformers==4.55.3 +transformers==4.55.4 tokenizers>=0.21.1 accelerate==1.10.0 datasets==4.0.0 diff --git a/setup.py b/setup.py index 5bf9ae8403..4cbc562e00 100644 --- a/setup.py +++ b/setup.py @@ -127,7 +127,7 @@ def get_package_version(): "yunchang==0.6.0", ], "deepspeed": [ - "deepspeed==0.17.2", + "deepspeed==0.17.5", "deepspeed-kernels", ], "mamba-ssm": [ diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index eafe89d29b..94b307a628 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -149,14 +149,12 @@ def _apply_adapter_patches(self): def _apply_flex_attention_patches(self): """Apply patches for flexible attention.""" if self.cfg.flex_attention: - # from axolotl.monkeypatch.attention.flex_attn import ( - # patch_flex_make_mask, - # patch_flex_wrapper, - # ) - # - # flex_attn_compile_kwargs = self.cfg.flex_attn_compile_kwargs or {} - # patch_flex_wrapper(**flex_attn_compile_kwargs) - # patch_flex_make_mask() + from axolotl.monkeypatch.attention.flex_attn import ( + patch_flex_wrapper, + ) + + flex_attn_compile_kwargs = self.cfg.flex_attn_compile_kwargs or {} + patch_flex_wrapper(**flex_attn_compile_kwargs) if self.cfg.sample_packing: from axolotl.core.attention.flex_block_mask import ( patch_create_causal_mask, diff --git a/src/axolotl/monkeypatch/attention/flex_attn.py b/src/axolotl/monkeypatch/attention/flex_attn.py index f59b8abe28..65ccad5336 100644 --- a/src/axolotl/monkeypatch/attention/flex_attn.py +++ b/src/axolotl/monkeypatch/attention/flex_attn.py @@ -1,11 +1,11 @@ """Flex attention monkey patch""" import sys -from typing import Optional, Tuple, Union +from packaging import version import torch import transformers - +from transformers.utils.import_utils import _torch_version, is_torch_less_or_equal from axolotl.utils.logging import get_logger LOG = get_logger(__name__) @@ -46,19 +46,33 @@ def __init__(self, training): """ self.training = None if not self._is_flex_compiled or training != self.training: + self.training = training + if is_torch_less_or_equal("2.5.1"): + self._compiled_flex_attention = torch.compile( + flex_attention, dynamic=False + ) # In PyTorch 2.6.0, there's a known issue with flex attention compilation which may # cause errors. The suggested fix is to compile with "max-autotune-no-cudagraphs" # see https://github.com/pytorch/pytorch/issues/146260 for training - self.training = training - LOG.info( - "Compiling flex attention with kwargs: %s. This may take a while...", - flex_attn_compile_kwargs, - ) - self._compiled_flex_attention = torch.compile( - flex_attention, - **flex_attn_compile_kwargs, - ) - LOG.info("Flex attention compiled successfully.") + elif version.parse(_torch_version).base_version == "2.6.0" and training: + self._compiled_flex_attention = torch.compile( + flex_attention, dynamic=False, mode="max-autotune-no-cudagraphs" + ) + # Fallback, usually the most recent torch 2.7.x+ versions + else: + LOG.info( + "Compiling flex attention with kwargs: %s. This may take a while...", + flex_attn_compile_kwargs, + main_process_only=True, + ) + self._compiled_flex_attention = torch.compile( + flex_attention, + **flex_attn_compile_kwargs, + ) + LOG.info( + "Flex attention compiled successfully.", main_process_only=True + ) + self._is_flex_compiled = True def __call__(self): @@ -68,139 +82,3 @@ def __call__(self): sys.modules[ "transformers.integrations.flex_attention" ].WrappedFlexAttention = WrappedFlexAttention - - -def patch_flex_make_mask(): - is_torch_2_6 = torch.__version__.startswith("2.6") - - if not is_torch_2_6: - return - - from torch.nn.attention.flex_attention import ( - _DEFAULT_SPARSE_BLOCK_SIZE as flex_default_block_size, - ) - from torch.nn.attention.flex_attention import ( - BlockMask, - ) - from torch.nn.attention.flex_attention import ( - create_block_mask as create_block_causal_mask_flex, - ) - - Offset = Union[torch.Tensor, int] - - def patched_make_flex_block_causal_mask( - attention_mask_2d: torch.Tensor, - attention_chunk_size: Optional[int] = None, - query_length=None, - key_length=None, - offsets: Optional[Tuple[Offset, Offset]] = None, - ) -> "BlockMask": - """ - Create a block causal document mask for a batch of sequences, both packed and unpacked. - Create Block causal logic and passing it into :func:`torch.nn.attention.flex_attention.create_block_mask`. - The resultant BlockMask is a compressed representation of the full block causal - mask. BlockMask is essential for performant computation of flex attention. - See: https://pytorch.org/blog/flexattention/ - - Args: - attention_mask_2d (torch.Tensor): Attention mask for packed and padded sequences - of shape (batch_size, total_seq_len). e.g. - - For unpacked sequence: - [[1, 1, 1, 1, 0, 0, 0], - [1, 1, 1, 1, 1, 0, 0]] - - For packed sequence: - [[1, 1, 1, 2, 2, 2, 0], - [1, 1, 2, 2, 2, 3, 3]] - - Returns: - BlockMask - """ - - batch_size, total_seq_len = attention_mask_2d.shape - if not key_length: - key_length = total_seq_len - if not query_length: - query_length = total_seq_len - attention_mask_2d = torch.nn.functional.pad( - attention_mask_2d, - value=0, - pad=(0, abs(total_seq_len - max(key_length, flex_default_block_size))), - ) - device = attention_mask_2d.device - document_ids = attention_mask_2d.clone() - - if attention_chunk_size is not None: - # we create an arange, then we just // by chunk size to get [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3] - chunk_idxs = (document_ids.clone().fill_(1).cumsum(-1) - 1) // ( - attention_chunk_size - ) - - # Instead of passing a tensor mask, flex attention requires a mask_mod function - # that determines which elements of QK^T should be included in the attention - # computation prior to the softmax. For sample packing, we need both the - # logic for both causal mask and document mask. See PyTorch's official - # blog post for more details: https://pytorch.org/blog/flexattention/#mask-mods - def causal_mask_mod(batch_idx, head_idx, q_idx, kv_idx): - """ - Defines the logic of a block causal mask by combining both a standard causal mask - and a block diagonal document mask. - - See :func:`~torchtune.modules.attention_utils.create_block_causal_mask` - for an illustration. - """ - causal_mask = q_idx >= kv_idx # not valid when decoding - document_mask = ( - document_ids[batch_idx, q_idx] == document_ids[batch_idx, kv_idx] - ) - padding_mask = attention_mask_2d[batch_idx, q_idx] > 0 - final_mask = causal_mask & padding_mask & document_mask - return final_mask - - def chunk_causal_mask_mod(batch_idx, head_idx, q_idx, kv_idx): - """ - Combines the chunk mask with the causal mask for chunked attention. - """ - chunk_mask = chunk_idxs[batch_idx, q_idx] == chunk_idxs[batch_idx, kv_idx] - causal_doc_mask = causal_mask_mod(batch_idx, head_idx, q_idx, kv_idx) - return chunk_mask & causal_doc_mask - - mask_mod_maybe_combined = ( - causal_mask_mod if attention_chunk_size is None else chunk_causal_mask_mod - ) - - if offsets is not None: - q_offset = offsets[0] - kv_offset = offsets[1] - - def mask_mod(batch_idx, head_idx, q_idx, kv_idx): - offset_q = q_idx + q_offset - offset_kv = kv_idx + kv_offset - return mask_mod_maybe_combined(batch_idx, head_idx, offset_q, offset_kv) - - else: - mask_mod = mask_mod_maybe_combined - return create_block_causal_mask_flex( - mask_mod=mask_mod, - B=batch_size, - H=None, # attention head - Q_LEN=query_length, - KV_LEN=key_length, - device=device, - _compile=True, - ) - - for n in tuple(sys.modules): - if ".modeling_" in n: - if hasattr(sys.modules[n], "make_flex_block_causal_mask"): - sys.modules[ - n - ].make_flex_block_causal_mask = patched_make_flex_block_causal_mask - sys.modules[ - n - ].make_flex_block_causal_mask = patched_make_flex_block_causal_mask - - transformers.integrations.flex_attention.make_flex_block_causal_mask = ( - patched_make_flex_block_causal_mask - ) diff --git a/src/axolotl/monkeypatch/tiled_mlp/base.py b/src/axolotl/monkeypatch/tiled_mlp/base.py index 3b7326bdb7..2c9dc8e4c5 100644 --- a/src/axolotl/monkeypatch/tiled_mlp/base.py +++ b/src/axolotl/monkeypatch/tiled_mlp/base.py @@ -8,6 +8,94 @@ import torch +class DeepSpeedTiledMLPMoE(torch.autograd.Function): + @staticmethod + def forward( + ctx, + fn, + self, + x, + shards, + compute_params, + ) -> torch.Tensor: + ctx.fn = fn + ctx.self = self + ctx.shards = shards + ctx.compute_params = [p for p in compute_params if p.requires_grad] + ctx.save_for_backward(x) + + x_shards = list(torch.chunk(x, chunks=shards, dim=1)) + with torch.no_grad(): + output_shards = [fn(self, x_shard) for x_shard in x_shards] + + ctx.is_tuple_output = isinstance(output_shards[0], tuple) + if isinstance(output_shards[0], tuple): + tuple_dim_idx = [1, 0] + output_unsharded = tuple( + torch.cat( + [output_shard[i] for output_shard in output_shards], + dim=tuple_dim_idx[i], + ) + for i in range(len(output_shards[0])) + ) + else: + output_unsharded = torch.cat(output_shards, dim=1) + + return output_unsharded + + @staticmethod + def backward(ctx, *grads) -> torch.Tensor: + fn = ctx.fn + (x,) = ctx.saved_tensors + self = ctx.self + shards = ctx.shards + compute_params = ctx.compute_params + is_tuple_output = ctx.is_tuple_output + + x_requires_grad = x.requires_grad + x = x.detach() + # detach() unsets `x.requires_grad`, so restore it + x.requires_grad_(x_requires_grad) + + incoming_grad = grads[0] + x_grad = torch.zeros_like(x) + x_shards = list(torch.chunk(x, chunks=shards, dim=1)) + + shard_step = x_shards[0].numel() + for i, x_shard in enumerate(x_shards): + # Tell deepspeed not to add a new grad to its ipg bucket until the last shard is run + if compute_params is not None: + if i + 1 < shards: + for param in compute_params: + param.ds_grad_is_ready = False + else: + # last shard, can add the grad + for param in compute_params: + param.ds_grad_is_ready = True + + x_shard.requires_grad_(x_requires_grad) + + shard_offset = i * shard_step + x_shard.grad = ( + x_grad.view(-1) + .narrow(0, shard_offset, x_shard.numel()) + .view_as(x_shard) + ) + incoming_grad_shard = ( + incoming_grad.view(-1) + .narrow(0, shard_offset, x_shard.numel()) + .view_as(x_shard) + ) + with torch.enable_grad(): + output = fn(self, x_shard) + if is_tuple_output: + torch.autograd.backward(output[0], incoming_grad_shard) + else: + torch.autograd.backward(output, incoming_grad_shard) + + return (None, None, x_grad, None, None) + + class TiledMLP(torch.autograd.Function): """ TiledMLP implementation using gradient hooks @@ -31,7 +119,18 @@ def forward( x_shards = list(torch.chunk(x, chunks=shards, dim=1)) with torch.no_grad(): output_shards = [fn(self, x_shard) for x_shard in x_shards] - output_unsharded = torch.cat(output_shards, dim=1) + ctx.is_tuple_output = isinstance(output_shards[0], tuple) + if isinstance(output_shards[0], tuple): + tuple_dim_idx = [1, 0] + output_unsharded = tuple( + torch.cat( + [output_shard[i] for output_shard in output_shards], + dim=tuple_dim_idx[i], + ) + for i in range(len(output_shards[0])) + ) + else: + output_unsharded = torch.cat(output_shards, dim=1) return output_unsharded @@ -42,6 +141,7 @@ def backward(ctx, *grads) -> torch.Tensor: self = ctx.self shards = ctx.shards compute_params = ctx.compute_params + is_tuple_output = ctx.is_tuple_output x_requires_grad = x.requires_grad x = x.detach() @@ -76,7 +176,10 @@ def backward(ctx, *grads) -> torch.Tensor: with torch.enable_grad(): output = fn(self, x_shard) - torch.autograd.backward(output, incoming_grad_shard) + if is_tuple_output: + torch.autograd.backward(output[0], incoming_grad_shard) + else: + torch.autograd.backward(output, incoming_grad_shard) # Clean up hooks grad_accumulator.cleanup() diff --git a/src/axolotl/monkeypatch/tiled_mlp/patch.py b/src/axolotl/monkeypatch/tiled_mlp/patch.py index 7cdc6d3a38..c0f89236ba 100644 --- a/src/axolotl/monkeypatch/tiled_mlp/patch.py +++ b/src/axolotl/monkeypatch/tiled_mlp/patch.py @@ -17,7 +17,7 @@ def patch_tiled_mlp(model_type, use_original_mlp=True, cfg_num_shards=None): TiledMLP as DeepSpeedTiledMLP, ) - from axolotl.monkeypatch.tiled_mlp.base import TiledMLP + from axolotl.monkeypatch.tiled_mlp.base import DeepSpeedTiledMLPMoE, TiledMLP try: # Dynamically import the module and MLP class @@ -64,7 +64,10 @@ def tiled_mlp_forward(self, x): for p in self._compute_params ) ) or os.environ.get("ACCELERATE_USE_DEEPSPEED", "false") == "true": - self._tiled_mlp_dist_impl = DeepSpeedTiledMLP + if model_type == "gpt_oss": + self._tiled_mlp_dist_impl = DeepSpeedTiledMLPMoE + else: + self._tiled_mlp_dist_impl = DeepSpeedTiledMLP else: self._tiled_mlp_dist_impl = TiledMLP From 231a67e70bbfc095fc94e057537412cf57a472cf Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Tue, 2 Sep 2025 12:08:44 -0400 Subject: [PATCH 0945/1405] Streaming SFT support (#3101) * working * fixes * deprecate --iterable; cleanup * pretrain_multipack_buffer_size -> streaming_multipack_buffer_size * improvements * tests * remove unused * docs, examples * nit * nit * add val_set_size validation * val * nit * min * coderabbito * cleanup * nit * add depr warning, cleanup * nit * fix test, fix quarto * fix * review comments * review comments * fix --- _quarto.yml | 3 +- docs/streaming.qmd | 120 +++++++++ examples/streaming/README.md | 50 ++++ examples/streaming/pretrain.yaml | 57 +++++ examples/streaming/sft.yaml | 55 ++++ src/axolotl/cli/args.py | 8 +- src/axolotl/cli/preprocess.py | 12 +- src/axolotl/common/datasets.py | 2 - src/axolotl/datasets.py | 145 +---------- src/axolotl/prompt_tokenizers.py | 2 +- src/axolotl/utils/collators/__init__.py | 16 +- src/axolotl/utils/data/__init__.py | 10 +- src/axolotl/utils/data/sft.py | 92 +++---- src/axolotl/utils/data/shared.py | 2 - .../data/{pretraining.py => streaming.py} | 59 +++-- src/axolotl/utils/data/utils.py | 11 +- src/axolotl/utils/schemas/config.py | 40 ++- src/axolotl/utils/schemas/validation.py | 82 ++++++ tests/e2e/integrations/test_kd.py | 2 +- tests/e2e/test_streaming.py | 73 ++++++ tests/test_data.py | 4 +- tests/test_packed_dataset.py | 42 ---- tests/test_packed_pretraining.py | 7 +- tests/test_streaming.py | 238 ++++++++++++++++++ 24 files changed, 849 insertions(+), 283 deletions(-) create mode 100644 docs/streaming.qmd create mode 100644 examples/streaming/README.md create mode 100644 examples/streaming/pretrain.yaml create mode 100644 examples/streaming/sft.yaml rename src/axolotl/utils/data/{pretraining.py => streaming.py} (86%) create mode 100644 tests/e2e/test_streaming.py create mode 100644 tests/test_streaming.py diff --git a/_quarto.yml b/_quarto.yml index 934d393cb5..3ffb0e6277 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -153,7 +153,7 @@ quartodoc: - utils.distributed - utils.dict - utils.optimizers.adopt - - utils.data.pretraining + - utils.data.streaming - utils.data.sft - utils.quantization - title: Schemas @@ -272,6 +272,7 @@ website: contents: - docs/batch_vs_grad.qmd - docs/dataset_preprocessing.qmd + - docs/streaming.qmd - docs/multipack.qmd - docs/mixed_precision.qmd - docs/optimizers.qmd diff --git a/docs/streaming.qmd b/docs/streaming.qmd new file mode 100644 index 0000000000..2a233a4fc7 --- /dev/null +++ b/docs/streaming.qmd @@ -0,0 +1,120 @@ +--- +title: Streaming Datasets +description: How to use streaming mode for large-scale datasets and memory-efficient training +order: 10 +--- + +Streaming enables memory-efficient training with large datasets by loading data +incrementally rather than loading the entire dataset into memory at once. + +Use streaming when: + +- Your dataset is too large to fit in memory (e.g. when you're doing pretraining with massive text corpora) +- You want to start training immediately without preprocessing the entire dataset + +Streaming works with both remote and locally stored datasets! + +::: {.callout-note} +Streaming currently only supports a single dataset. Multi-dataset support will be added soon. +::: + + +## Configuration + +### Basic Streaming + +Enable streaming mode by setting the `streaming` flag: + +```yaml +streaming: true +``` + +### Pretraining with Streaming + +For pretraining tasks, streaming is automatically enabled when using `pretraining_dataset`: + +```yaml +pretraining_dataset: + - path: HuggingFaceFW/fineweb-edu + type: pretrain + text_column: text + split: train + +# Optionally, enable sample packing +streaming_multipack_buffer_size: 10000 +sample_packing: true +``` + +### SFT with Streaming + +For supervised fine-tuning with streaming: + +```yaml +streaming: true +datasets: + - path: tatsu-lab/alpaca + type: alpaca + split: train + +# Optionally, enable sample packing +streaming_multipack_buffer_size: 10000 +sample_packing: true +``` + +## Configuration Options + +### `streaming_multipack_buffer_size` + +Controls the buffer size for multipack streaming (default: 10,000). This determines how +many samples are buffered before packing. Larger buffers can improve packing efficiency +but use more memory. + +### `shuffle_merged_datasets` + +When enabled, shuffles the streaming dataset using the buffer. This requires additional +memory for the shuffle buffer. + +## Sample Packing with Streaming + +Sample packing is supported for streaming datasets. When enabled, multiple samples are +packed into a single sequence to maximize GPU utilization: + +```yaml +sample_packing: true +streaming_multipack_buffer_size: 10000 + +# For SFT: attention is automatically isolated between packed samples +# For pretraining: control with pretrain_multipack_attn +pretrain_multipack_attn: true # prevent cross-attention between packed samples +``` + +For more information, see our [documentation](multipack.qmd) on multipacking. + +## Important Considerations + +### Memory Usage + +While streaming reduces memory usage compared to loading entire datasets, you still need +to consider: + +- You can control the memory usage by adjusting `streaming_multipack_buffer_size` +- Sample packing requires buffering multiple samples +- Shuffling requires additional memory for the shuffle buffer + +### Performance + +- Streaming may have slightly higher latency compared to preprocessed datasets, as samples are processed on-the-fly +- Network speed and disk read speed are important when streaming from remote sources or a local dataset, respectively +- Consider using `axolotl preprocess` for smaller or more frequently used datasets + +### Evaluation Datasets + +Evaluation datasets are not streamed to ensure consistent evaluation metrics. They're +loaded normally even when training uses streaming. + +## Examples + +See the `examples/streaming/` directory for complete configuration examples: + +- `pretrain.yaml`: Pretraining with streaming dataset +- `sft.yaml`: Supervised fine-tuning with streaming diff --git a/examples/streaming/README.md b/examples/streaming/README.md new file mode 100644 index 0000000000..cdbb5baea1 --- /dev/null +++ b/examples/streaming/README.md @@ -0,0 +1,50 @@ +# Streaming Dataset Examples + +This directory contains example configurations for using Axolotl's streaming dataset +functionality, which enables memory-efficient training with large datasets. + +## Examples + +Run the following examples with e.g. `axolotl train examples/streaming/sft.yaml`; no +`axolotl preprocess` required! + +### Pretraining (`pretrain.yaml`) + +Demonstrates streaming configuration for pretraining tasks using the fineweb-edu dataset +with SmolLM2-135M. + +- Uses `pretraining_dataset` configuration for automatic streaming +- Multipack attention control to prevent cross-attention between packed sequences +- Buffer size configuration for memory management + +### SFT (`sft.yaml`) + +Shows how to use streaming for supervised fine-tuning with the Alpaca dataset. + +- Explicit `streaming: true` flag for SFT datasets +- Memory-efficient training on instruction datasets +- Evaluation datasets are currently not streamed + +## Key Configuration Options + +### `streaming` +- Enables streaming mode for standard datasets +- Automatically enabled for `pretraining_dataset` + +### `streaming_multipack_buffer_size` +- Controls buffer size for sample packing (default: 10,000) +- Larger values improve packing efficiency but use more memory +- Adjust based on available memory + +### `shuffle_merged_datasets` +- Enables shuffling of streaming datasets +- Requires additional memory for shuffle buffer + +### `sample_packing` +- Packs multiple samples into single sequences +- Minimize per-step padding tokens + +## Performance Tips + +- Download small / frequently-used datasets locally for better performance +- Larger buffer sizes improve packing efficiency diff --git a/examples/streaming/pretrain.yaml b/examples/streaming/pretrain.yaml new file mode 100644 index 0000000000..bc8edefd67 --- /dev/null +++ b/examples/streaming/pretrain.yaml @@ -0,0 +1,57 @@ +base_model: HuggingFaceTB/SmolLM2-135M + +# Streaming pretraining configuration +pretraining_dataset: + - path: HuggingFaceFW/fineweb-edu + name: sample-10BT + type: pretrain + text_column: text + split: train + +# Streaming-specific settings +streaming_multipack_buffer_size: 10000 +shuffle_merged_datasets: true + +# Training configuration +max_steps: 1000 +output_dir: ./outputs/smollm2-135m-pretrain-streaming + +# Sequence and packing settings +sequence_len: 1024 +sample_packing: true +pretrain_multipack_attn: true # Prevent cross-attention between packed sequences +flash_attention: true + +# Batch size settings +gradient_accumulation_steps: 8 +micro_batch_size: 1 + +# Optimizer and scheduler +optimizer: adamw_torch +lr_scheduler: cosine +learning_rate: 5e-4 +warmup_ratio: 0.1 +weight_decay: 0.01 + +# Precision and performance +bf16: auto +tf32: true + +# Logging and checkpointing +logging_steps: 10 +save_strategy: steps +save_steps: 250 +save_total_limit: 3 + +# Weights & Biases (optional) +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +# Special tokens +special_tokens: + pad_token: "<|endoftext|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/streaming/sft.yaml b/examples/streaming/sft.yaml new file mode 100644 index 0000000000..47b9f493fa --- /dev/null +++ b/examples/streaming/sft.yaml @@ -0,0 +1,55 @@ +base_model: HuggingFaceTB/SmolLM2-135M + +# Dataset configuration +datasets: + - path: tatsu-lab/alpaca + type: alpaca + split: train + +# Streaming-specific settings +streaming: true +streaming_multipack_buffer_size: 10000 +shuffle_merged_datasets: true + +# Training configuration +max_steps: 1000 +output_dir: ./outputs/smollm2-135m-sft-streaming + +# Sequence and packing settings +sequence_len: 1024 +sample_packing: true +flash_attention: true + +# Batch size settings +gradient_accumulation_steps: 4 +micro_batch_size: 1 + +# Optimizer and scheduler +optimizer: adamw_torch +lr_scheduler: cosine +learning_rate: 2e-4 +warmup_ratio: 0.1 +weight_decay: 0.0 + +# Precision and performance +bf16: auto +tf32: true + +# Logging and checkpointing +logging_steps: 10 +save_strategy: steps +save_steps: 100 +save_total_limit: 3 + +# Weights & Biases (optional) +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +# Special tokens +special_tokens: + pad_token: "<|endoftext|>" + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py index 9bb544affe..396e9a8af8 100644 --- a/src/axolotl/cli/args.py +++ b/src/axolotl/cli/args.py @@ -14,9 +14,13 @@ class PreprocessCliArgs: prompter: Optional[str] = field(default=None) download: Optional[bool] = field(default=True) iterable: Optional[bool] = field( - default=None, + default=False, metadata={ - "help": "Use IterableDataset for streaming processing of large datasets" + "help": ( + "Deprecated in v0.13.0, will be removed in v0.14.0. For streaming " + "datasets, use 'axolotl train' and set 'streaming: true' in your YAML " + "config, or pass --streaming instead in the CLI." + ) }, ) diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index ff4551c645..6c05a55f11 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -35,10 +35,20 @@ def do_preprocess(cfg: DictDefault, cli_args: PreprocessCliArgs) -> None: check_accelerate_default_config() check_user_token() + if cli_args.iterable: + LOG.error( + "The --iterable CLI argument for 'axolotl preprocess' is no longer " + "supported. For training, set 'streaming: true' in your YAML config or " + "pass '--streaming' in your 'axolotl train' command for on-the-fly " + "preprocessing." + ) + return + for key in ["skip_prepare_dataset", "pretraining_dataset"]: if cfg.get(key): LOG.error( - f"You have set `{key}:`. `preprocess` is not needed. Run the `axolotl train` CLI directly instead." + f"You have set `{key}:`. `preprocess` is not needed. Run the 'axolotl " + "train' CLI directly instead." ) return diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index e7433e3c25..8d7758e66f 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -55,13 +55,11 @@ def load_datasets( """ tokenizer = load_tokenizer(cfg) processor = load_processor(cfg, tokenizer=tokenizer) if cfg.processor_type else None - preprocess_iterable = getattr(cli_args, "iterable", False) train_dataset, eval_dataset, total_num_steps, prompters = prepare_datasets( cfg, tokenizer, processor=processor, - preprocess_iterable=preprocess_iterable, ) if ( diff --git a/src/axolotl/datasets.py b/src/axolotl/datasets.py index b8f9484bc4..20acb85219 100644 --- a/src/axolotl/datasets.py +++ b/src/axolotl/datasets.py @@ -1,18 +1,17 @@ -"""Module containing Dataset functionality""" +""" +Module containing dataset functionality. + +We want this to be a wrapper for an existing dataset that we have loaded. Lets use the +concept of middlewares to wrap each dataset. We'll use the collators later on to pad the +datasets. +""" -import torch from datasets import Dataset, IterableDataset from axolotl.utils.logging import get_logger from .prompt_tokenizers import PromptTokenizingStrategy -# We want this to be a wrapper for an existing dataset that we have loaded -# lets use the concept of middlewares to wrap each dataset, for example -# ConstantLengthDataset(ShuffledDataset([TokenizedPromptDataset(alpaca_dataset)])) -# let's check to ensure we don't truncate an item in the middle, we'll use -# the collators later on to pad the datasets - LOG = get_logger(__name__) @@ -86,133 +85,3 @@ def wrap_dataset_for_tokenized_prompt( **map_kwargs, ) return TokenizedPromptDataset(prompt_tokenizer, dataset, **kwargs) - - -# TODO this isn't the best since it can't interleave datasets -class ConstantLengthDataset(IterableDataset): - """Iterable dataset that returns constant length chunks of tokens from stream of - text files. - - Args: - tokenizer: The processor used for processing the data. - dataset: Dataset with text files. - seq_length: Length of token sequences to return. - """ - - def __init__( - self, - tokenizer, - datasets, - seq_length=2048, - ): - self.tokenizer = tokenizer - self.concat_token_id = tokenizer.eos_token_id - self.datasets: list[IterableDataset] = datasets - self.seq_length = seq_length - - vocab_size = len(tokenizer.get_vocab()) - - if vocab_size <= torch.iinfo(torch.int16).max: - self.tokens_dtype = torch.int16 - elif vocab_size <= torch.iinfo(torch.int32).max: - self.tokens_dtype = torch.int32 - else: - self.tokens_dtype = torch.int64 - - def __iter__(self): - buffer = { - "input_ids": [], - "attention_mask": [], - "labels": [], - "position_ids": [], - } - buffer_len = 0 - for dataset in self.datasets: - idx = 0 - iterator = iter(dataset) - more_examples = True - while more_examples: - try: - example = next(iterator) - idx += 1 - except StopIteration: - more_examples = False - example = None - - add_concat_token = False - if example: - example_len = len(example["input_ids"]) - add_concat_token = example["input_ids"][-1] != self.concat_token_id - else: - example_len = 0 - - if not example_len or ( - buffer_len + int(add_concat_token) + example_len > self.seq_length - ): - if buffer["input_ids"]: - input_ids = torch.cat(buffer["input_ids"], dim=-1)[ - : self.seq_length - ] - attention_mask = torch.cat(buffer["attention_mask"], dim=-1)[ - : self.seq_length - ] - position_ids = torch.cat(buffer["position_ids"], dim=-1)[ - : self.seq_length - ] - labels = torch.cat(buffer["labels"], dim=-1)[: self.seq_length] - if labels.size() == input_ids.size() and ( - attention_mask.size() == input_ids.size() - ): - yield { - "input_ids": input_ids, - "labels": labels, - "attention_mask": attention_mask, - "position_ids": position_ids, - } - else: - LOG.warning( - "Dropping batch due to tensor size mismatch " - f"input_ids: {input_ids.size()}, " - f"labels: {labels.size()}, " - f"attention_mask: {attention_mask.size()}" - ) - buffer = { - "input_ids": [], - "attention_mask": [], - "labels": [], - "position_ids": [], - } - buffer_len = 0 - idx = 1 - - if example: - # FIXME - # just going to drop data points that are too long - if len(example["input_ids"]) <= self.seq_length: - input_ids = example["input_ids"] - attention_mask = example["attention_mask"] - labels = example["labels"] - - if add_concat_token: - input_ids.append(self.concat_token_id) - attention_mask.append(1) - labels.append(self.concat_token_id) - - input_ids_with_concat = torch.tensor( - input_ids, dtype=self.tokens_dtype - ) - attention_mask_with_concat = torch.tensor( - [idx * m for m in attention_mask], dtype=torch.int16 - ) - labels_with_concat = torch.tensor( - labels, dtype=self.tokens_dtype - ) - position_ids = torch.arange( - len(input_ids), dtype=self.tokens_dtype - ) - - buffer["input_ids"].append(input_ids_with_concat) - buffer["attention_mask"].append(attention_mask_with_concat) - buffer["labels"].append(labels_with_concat) - buffer["position_ids"].append(position_ids) - buffer_len += len(input_ids) diff --git a/src/axolotl/prompt_tokenizers.py b/src/axolotl/prompt_tokenizers.py index 2bf9ec763e..a7bd963f82 100644 --- a/src/axolotl/prompt_tokenizers.py +++ b/src/axolotl/prompt_tokenizers.py @@ -75,7 +75,7 @@ def _tokenize( ) -> BatchEncoding: empty = BatchEncoding(data={"input_ids": [], "attention_mask": []}) if not prompt: - LOG.warning("Empty text requested for tokenization.") + LOG.warning_once("Empty text requested for tokenization.") return empty result = self.tokenizer( diff --git a/src/axolotl/utils/collators/__init__.py b/src/axolotl/utils/collators/__init__.py index 8c60f223cd..d5e6ad17d1 100644 --- a/src/axolotl/utils/collators/__init__.py +++ b/src/axolotl/utils/collators/__init__.py @@ -1,11 +1,17 @@ -""" -shared axolotl collators for multipack, mamba, multimodal -""" +"""Shared axolotl collators for multipacking, mamba, multimodal.""" -from .batching import ( # noqa: F401 +from .batching import ( BatchSamplerDataCollatorForSeq2Seq, DataCollatorForSeq2Seq, PretrainingBatchSamplerDataCollatorForSeq2Seq, V2BatchSamplerDataCollatorForSeq2Seq, ) -from .mamba import MambaDataCollator # noqa: F401 +from .mamba import MambaDataCollator + +__all__ = [ + "DataCollatorForSeq2Seq", + "BatchSamplerDataCollatorForSeq2Seq", + "V2BatchSamplerDataCollatorForSeq2Seq", + "PretrainingBatchSamplerDataCollatorForSeq2Seq", + "MambaDataCollator", +] diff --git a/src/axolotl/utils/data/__init__.py b/src/axolotl/utils/data/__init__.py index d162a7d0bb..788f136387 100644 --- a/src/axolotl/utils/data/__init__.py +++ b/src/axolotl/utils/data/__init__.py @@ -1,8 +1,8 @@ """Init for `axolotl.utils.data` module.""" -from axolotl.utils.data.pretraining import ( - encode_pretraining, - wrap_pretraining_dataset, +from axolotl.utils.data.streaming import ( + encode_streaming, + wrap_streaming_dataset, ) from axolotl.utils.data.rl import prepare_preference_datasets from axolotl.utils.data.sft import ( @@ -12,8 +12,8 @@ from axolotl.utils.data.utils import md5 __all__ = [ - "encode_pretraining", - "wrap_pretraining_dataset", + "encode_streaming", + "wrap_streaming_dataset", "prepare_preference_datasets", "get_dataset_wrapper", "prepare_datasets", diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 2ae7d9052f..28732e01d2 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -9,13 +9,14 @@ Dataset, DatasetDict, IterableDataset, + IterableDatasetDict, load_dataset, ) from transformers import PreTrainedTokenizer, ProcessorMixin from axolotl.prompters import Prompter from axolotl.utils.data.lock import FileLockLoader -from axolotl.utils.data.pretraining import wrap_pretraining_dataset +from axolotl.utils.data.streaming import wrap_streaming_dataset from axolotl.utils.data.shared import ( create_train_validation_split, datasets_with_name_generator, @@ -48,7 +49,6 @@ def prepare_datasets( cfg: DictDefault, tokenizer: PreTrainedTokenizer, processor: ProcessorMixin | None = None, - preprocess_iterable: bool = False, ) -> tuple[IterableDataset | Dataset, Dataset | None, int, list[Prompter | None]]: """Prepare training and evaluation datasets based on configuration. @@ -56,23 +56,19 @@ def prepare_datasets( cfg: Dictionary mapping `axolotl` config keys to values. tokenizer: Tokenizer to use for processing text. processor: Optional processor for multimodal datasets. - preprocess_iterable: Whether to use iterable preprocessing. Returns: Tuple of (train_dataset, eval_dataset, total_steps, prompters). """ - if cfg.pretraining_dataset: - return _prepare_pretraining_dataset( - cfg, tokenizer, processor, preprocess_iterable - ) - return _prepare_standard_dataset(cfg, tokenizer, processor, preprocess_iterable) + if cfg.streaming or cfg.pretraining_dataset: + return _prepare_streaming_dataset(cfg, tokenizer, processor) + return _prepare_standard_dataset(cfg, tokenizer, processor) def _prepare_standard_dataset( cfg: DictDefault, tokenizer: PreTrainedTokenizer, processor: ProcessorMixin | None, - preprocess_iterable: bool, ) -> tuple[Dataset, Dataset | None, int, list[Prompter | None]]: """Prepare standard (non-pretraining) datasets.""" @@ -83,7 +79,6 @@ def _load_datasets(): cfg, split="train", processor=processor, - preprocess_iterable=preprocess_iterable, ) # Overwrite eval_dataset if test data exists @@ -93,7 +88,6 @@ def _load_datasets(): cfg, split="test", processor=processor, - preprocess_iterable=preprocess_iterable, ) return train_dataset, eval_dataset, prompters @@ -128,22 +122,40 @@ def _load_datasets(): return train_dataset, eval_dataset, total_num_steps, prompters -def _prepare_pretraining_dataset( +def _prepare_streaming_dataset( cfg: DictDefault, tokenizer: PreTrainedTokenizer, processor: ProcessorMixin | None, - preprocess_iterable: bool, ) -> tuple[IterableDataset, Dataset | None, int, list[Prompter | None]]: """ - Prepare dataset for pretraining mode. + Prepare dataset for streaming mode. - Note: Pre-training datasets are streamed from the HuggingFace Hub. + Note: Streaming datasets are loaded incrementally from the source. """ - # Extract pretraining dataset configuration - pretraining_config = _extract_pretraining_config(cfg) + if cfg.pretraining_dataset: + dataset_config = _extract_pretraining_config(cfg) + train_dataset = _load_streaming_dataset(dataset_config, cfg, tokenizer) + elif cfg.sample_packing: + # TODO(djsaunde): Implement for multiple datasets + dataset_config = DictDefault(cfg.datasets[0]) + + # Ensure we have a split set - default to 'train' if not specified + if not hasattr(dataset_config, "split") or not dataset_config.split: + dataset_config.split = "train" + train_dataset = _load_streaming_dataset(dataset_config, cfg, tokenizer) + else: + # Use legacy loading function for non-packed streaming datasets + train_dataset, eval_dataset, prompters = _load_and_prepare_datasets( + tokenizer, + cfg, + split="train", + processor=processor, + streaming=True, + ) - # Load streaming dataset for training - train_dataset = _load_pretraining_dataset(pretraining_config, cfg, tokenizer) + # Return early for non-packed streaming datasets + total_num_steps = cfg.max_steps if cfg.max_steps else -1 + return train_dataset, eval_dataset, total_num_steps, prompters # Load evaluation dataset if specified eval_dataset = None @@ -153,14 +165,12 @@ def _prepare_pretraining_dataset( cfg, split="test", processor=processor, - preprocess_iterable=preprocess_iterable, + streaming=False, ) - if cfg.dataset_exact_deduplication: - LOG.info("Deduplication not available for pretrained datasets") - - # For pretraining, we return max_steps directly from config - return train_dataset, eval_dataset, cfg.max_steps, [] + # For streaming, we return max_steps directly from config or -1 if not set + total_num_steps = cfg.max_steps if cfg.max_steps else -1 + return train_dataset, eval_dataset, total_num_steps, [] def _extract_pretraining_config(cfg: DictDefault) -> DictDefault: @@ -192,7 +202,7 @@ def _extract_pretraining_config(cfg: DictDefault) -> DictDefault: ) -def _load_pretraining_dataset( +def _load_streaming_dataset( pretraining_config: DictDefault, cfg: DictDefault, tokenizer: PreTrainedTokenizer ) -> IterableDataset: """Load and prepare a streaming dataset for pretraining.""" @@ -227,15 +237,11 @@ def _load_pretraining_dataset( iter_dataset = iter_dataset.skip(pretraining_config["skip"]) # Wrap the dataset for pretraining - train_dataset = wrap_pretraining_dataset( + train_dataset = wrap_streaming_dataset( iter_dataset, tokenizer, cfg, dataset_wrapper_partial, - max_tokens=cfg.sequence_len, - batch_size=cfg.micro_batch_size, - seed=cfg.seed, - buffer_size=cfg.pretrain_multipack_buffer_size or 10_000, ) # Format for PyTorch @@ -256,7 +262,7 @@ def _load_tokenized_prepared_datasets( cfg: DictDefault, split: Literal["train", "test"] = "train", processor: ProcessorMixin | None = None, - preprocess_iterable: bool = False, + streaming: bool = False, ) -> tuple[Dataset | DatasetDict, list[Prompter | None]]: """Load or create tokenized and prepared datasets for training or testing. @@ -265,7 +271,7 @@ def _load_tokenized_prepared_datasets( cfg: Configuration object. split: Dataset split to load ('train' or 'test'). processor: Optional processor for multimodal datasets. - preprocess_iterable: Whether to use iterable preprocessing. + streaming: Whether to use iterable preprocessing. Returns: Tuple of (dataset, prompters list). @@ -296,7 +302,7 @@ def _load_tokenized_prepared_datasets( tokenizer, split, processor, - preprocess_iterable, + streaming, ) return dataset, prompters @@ -308,7 +314,7 @@ def _load_raw_datasets( tokenizer: PreTrainedTokenizer, split: str, processor: ProcessorMixin | None = None, - preprocess_iterable: bool = False, + streaming: bool = False, ) -> tuple[Dataset, list[Prompter | None]]: """Load, process, merge, and save raw datasets.""" LOG.info("Loading raw datasets...", main_process_only=False) @@ -329,7 +335,7 @@ def _load_raw_datasets( split=split, seed=cfg.seed, processor=processor, - preprocess_iterable=preprocess_iterable, + streaming=streaming, ) datasets.append(dataset_wrapper) prompters.append(dataset_prompter) @@ -337,7 +343,7 @@ def _load_raw_datasets( # Merge datasets dataset = merge_datasets(datasets, cfg) - if not cfg.skip_prepare_dataset: + if not cfg.skip_prepare_dataset and not streaming: if split == "test" and cfg.eval_sequence_len: dataset = handle_long_seq_in_dataset(dataset, cfg.eval_sequence_len, cfg) else: @@ -361,19 +367,19 @@ def _load_and_process_single_dataset( split: str, seed: int, processor: ProcessorMixin | None = None, - preprocess_iterable: bool = False, + streaming: bool = False, ) -> tuple[Dataset | IterableDataset, Prompter | None]: """Load and process a single dataset based on the passed config.""" # Load the dataset dataset = load_dataset_with_config( - dataset_config, cfg.hf_use_auth_token, streaming=preprocess_iterable + dataset_config, cfg.hf_use_auth_token, streaming=streaming ) # Parse dataset type d_base_type, d_prompt_style = _parse_dataset_type(dataset_config.type) # Select the appropriate split - if isinstance(dataset, DatasetDict): + if isinstance(dataset, (DatasetDict, IterableDatasetDict)): if dataset_config.split and dataset_config.split in dataset: dataset = dataset[dataset_config.split] elif split in dataset: @@ -479,7 +485,7 @@ def _load_and_prepare_datasets( cfg: DictDefault, split: Literal["train", "test"] = "train", processor: ProcessorMixin | None = None, - preprocess_iterable: bool = False, + streaming: bool = False, ) -> tuple[Dataset | None, Dataset | None, list[Prompter | None]]: """Load and prepare datasets with optional validation split and sharding. @@ -488,7 +494,7 @@ def _load_and_prepare_datasets( cfg: Configuration object. split: Dataset split to load ('train' or 'test'). processor: Optional processor for multimodal datasets. - preprocess_iterable: Whether to use iterable preprocessing. + streaming: Whether to use iterable preprocessing. Returns: Tuple of (train_dataset, eval_dataset, prompters). @@ -499,7 +505,7 @@ def _load_and_prepare_datasets( cfg, split=split, processor=processor, - preprocess_iterable=preprocess_iterable, + streaming=streaming, ) # Apply dataset sharding if configured using shared function diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 1d7d37f155..6b6e0e2816 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -236,11 +236,9 @@ def _load_from_local_path( try: return load_from_disk(dataset_config.path) except FileNotFoundError: - load_dataset_kwargs["streaming"] = False return load_dataset(dataset_config.path, **load_dataset_kwargs) elif local_path.is_file(): dataset_type = get_dataset_type(dataset_config) - load_dataset_kwargs["streaming"] = False return load_dataset( dataset_type, data_files=dataset_config.path, diff --git a/src/axolotl/utils/data/pretraining.py b/src/axolotl/utils/data/streaming.py similarity index 86% rename from src/axolotl/utils/data/pretraining.py rename to src/axolotl/utils/data/streaming.py index 72c5536e99..2cb35ee7c8 100644 --- a/src/axolotl/utils/data/pretraining.py +++ b/src/axolotl/utils/data/streaming.py @@ -1,4 +1,4 @@ -"""data handling specific to pretraining""" +"""Data handling specific to streaming datasets.""" import functools from collections import defaultdict @@ -17,10 +17,10 @@ LOG = get_logger(__name__) -def encode_pretraining( +def encode_streaming( + examples: Dict[str, List], tokenizer: PreTrainedTokenizerBase, max_tokens: int, - examples: Dict[str, List], text_column: str = "text", concatenate: bool = True, ) -> Dict[str, List]: @@ -176,45 +176,57 @@ def encode_pretraining( return ret -def wrap_pretraining_dataset( +def wrap_streaming_dataset( dataset, tokenizer, cfg, ds_wrapper_fn, - max_tokens=2048, - batch_size=1, - seed=42, - buffer_size=10_000, ): if cfg.sample_packing: + # For SFT (non-pretraining) datasets, always use multipack_attn=True to ensure + # attention isolation between packed sequences + multipack_attn = ( + True if not cfg.pretraining_dataset else cfg.pretrain_multipack_attn + ) + collate_fn = PretrainingBatchSamplerDataCollatorForSeq2Seq( tokenizer, return_tensors="pt", padding=True, - pad_to_multiple_of=max_tokens, - multipack_attn=cfg.pretrain_multipack_attn, + pad_to_multiple_of=cfg.sequence_len, + multipack_attn=multipack_attn, ) encode = functools.partial( - encode_packed_pretraining, + encode_packed_streaming, collate_fn, ds_wrapper_fn, - max_seq_length=max_tokens, - batch_size=batch_size, - multipack_attn=cfg.pretrain_multipack_attn, + max_seq_length=cfg.sequence_len, + batch_size=cfg.micro_batch_size, + multipack_attn=multipack_attn, ) - # set this to 1 so downstream data_loader doesn't try to increase the batch again + + # Set this to 1 so downstream data_loader doesn't try to increase the batch size + # again cfg.micro_batch_size = 1 else: + # NOTE: This is not reachable for SFT datasets since we use the pre-existing + # loading function for non-packed streaming datasets. Refer to + # _prepare_streaming_datasets in sft.py for that code path. + text_column = ( + getattr(cfg.pretraining_dataset[0], "text_column", "text") or "text" + ) encode = functools.partial( - encode_pretraining, - tokenizer, - max_tokens, - text_column=cfg.pretraining_dataset[0].text_column or "text", + encode_streaming, + tokenizer=tokenizer, + max_tokens=cfg.sequence_len, + text_column=text_column, concatenate=cfg.pretraining_sample_concatenation is True, ) if cfg.shuffle_merged_datasets: - dataset = dataset.shuffle(seed=seed, buffer_size=buffer_size) + dataset = dataset.shuffle( + seed=cfg.seed, buffer_size=cfg.streaming_multipack_buffer_size + ) else: LOG.debug("NOT shuffling merged pretraining datasets") @@ -232,14 +244,13 @@ def wrap_pretraining_dataset( dataset = dataset.map( encode, batched=True, - batch_size=buffer_size, - # input_columns="text", + batch_size=cfg.streaming_multipack_buffer_size, remove_columns=remove_columns, ) return dataset -def encode_packed_pretraining( +def encode_packed_streaming( collate_fn, ds_wrapper: Callable, examples: Dict[str, List], @@ -274,8 +285,6 @@ def encode_packed_pretraining( for batch in sampler: for data in batch: features = train_dataset[data] - if "num_truncated_tokens" in features: - del features["num_truncated_tokens"] if "num_truncated_tokens" in features: del features["num_truncated_tokens"] if "overflow_to_sample_mapping" in features: diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index 4868576a04..445a65d6c7 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -190,12 +190,21 @@ def handle_long_seq_in_dataset( Returns: Filtered dataset with long sequences removed. """ - if "input_ids" not in dataset.column_names: + if ( + hasattr(dataset, "column_names") + and dataset.column_names + and "input_ids" not in dataset.column_names + ): LOG.warning( "Dataset does not contain 'input_ids' column. Skip drop long seq. This is " "expected for reward modeling." ) return dataset + elif not hasattr(dataset, "column_names") or dataset.column_names is None: + LOG.info( + "Dataset is streaming (IterableDataset), skipping long sequence handling" + ) + return dataset drop_long = functools.partial( drop_long_seq, diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 4b5f571dcf..d43c346cd8 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -475,12 +475,6 @@ class AxolotlInputConfig( }, ) multipack_real_batches: bool | None = None - pretraining_sample_concatenation: bool | None = Field( - default=None, - json_schema_extra={ - "description": "whether to concatenate samples during pretraining", - }, - ) batch_flattening: Literal["auto"] | bool | None = Field( default=None, @@ -495,13 +489,34 @@ class AxolotlInputConfig( pose_max_context_len: int | None = None pose_num_chunks: int | None = None - pretrain_multipack_buffer_size: int | None = 10_000 + # Deprecated: Use streaming_multipack_buffer_size instead + pretrain_multipack_buffer_size: int | None = Field( + default=None, + deprecated="Deprecated in v0.13.0, will be removed in v0.14.0. Use streaming_multipack_buffer_size instead", + ) pretrain_multipack_attn: bool | None = Field( default=True, json_schema_extra={ "description": "whether to prevent cross attention for packed sequences during pretraining", }, ) + pretraining_sample_concatenation: bool | None = Field( + default=None, + json_schema_extra={ + "description": "whether to concatenate samples during pretraining", + }, + ) + + streaming: bool | None = Field( + default=None, + json_schema_extra={"description": "Use streaming mode for loading datasets"}, + ) + streaming_multipack_buffer_size: int | None = Field( + default=10_000, + json_schema_extra={ + "description": "Buffer size for multipack streaming datasets" + }, + ) xformers_attention: bool | None = Field( default=None, @@ -1264,3 +1279,14 @@ def default_dataset_processes(cls, data): data["dataset_processes"] = get_default_process_count() return data + + @model_validator(mode="before") + @classmethod + def check_deduplication_with_streaming(cls, data): + if data.get("dataset_exact_deduplication") and ( + data.get("streaming") or data.get("pretraining_dataset") + ): + raise NotImplementedError( + "dataset_exact_deduplication is not available for streaming datasets. " + ) + return data diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 791894990c..49add8081c 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -60,6 +60,20 @@ def check_dataset_or_pretraining_dataset(cls, data): raise ValueError("either datasets or pretraining_dataset is required") return data + @model_validator(mode="before") + @classmethod + def check_pretraining_streaming_deprecation(cls, data): + # TODO(djsaunde): remove this check + implement change for 0.13.0 release + if data.get("pretraining_dataset") and not data.get("streaming"): + LOG.warning( + "Setting `pretraining_dataset` without explicitly setting `streaming: " + "true` is deprecated. In a future release, streaming will not be " + "automatically enabled when using pretraining_dataset. Please " + "explicitly set `streaming: true` in your configuration to maintain " + "current behavior." + ) + return data + @model_validator(mode="before") @classmethod def check_push_ds_auth(cls, data): @@ -340,6 +354,30 @@ def check_neftune(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_multipack_buffer_size(cls, data): + if data.get("pretrain_multipack_buffer_size") and not data.get( + "streaming_multipack_buffer_size" + ): + LOG.warning( + "`pretrain_multipack_buffer_size` is deprecated in v0.13.0, will be " + "removed in v0.14.0. Use `streaming_multipack_buffer_size` instead." + ) + data["streaming_multipack_buffer_size"] = data[ + "pretrain_multipack_buffer_size" + ] + del data["pretrain_multipack_buffer_size"] + elif data.get("pretrain_multipack_buffer_size") and data.get( + "streaming_multipack_buffer_size" + ): + raise ValueError( + "pretrain_multipack_buffer_size is deprecated, use " + "streaming_multipack_buffer_size; both are set, please remove the " + "deprecated pretrain_multipack_buffer_size setting" + ) + return data + @model_validator(mode="after") def check_fft_possible_bad_config(self): if ( @@ -1074,6 +1112,50 @@ def check_pretraining_split_batches_accelerate(cls, data): data["accelerator_config"]["dispatch_batches"] = False return data + @model_validator(mode="before") + @classmethod + def check_pretraining_w_val_set_size(cls, data): + if data.get("pretraining_dataset") and data.get("val_set_size"): + raise ValueError( + "val_set_size is not supported with pretraining_dataset. " + "Use test_datasets to specify evaluation datasets for pretraining." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_streaming_w_val_set_size(cls, data): + if data.get("streaming") and data.get("val_set_size"): + raise ValueError( + "val_set_size is not supported with streaming datasets. " + "Use test_datasets to specify evaluation datasets when streaming is enabled." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_streaming_w_max_steps(cls, data): + if data.get("streaming") and not data.get("max_steps"): + raise ValueError( + "max_steps must be set when using streaming datasets. " + "Trainer cannot infer dataset length for iterable datasets." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_streaming_w_multiple_datasets(cls, data): + if ( + data.get("streaming") + and data.get("sample_packing") + and data.get("datasets") + and len(data.get("datasets")) > 1 + ): + raise NotImplementedError( + "Sample packing with multiple streaming datasets is not yet supported" + ) + return data + class ModelCompatibilityValidationMixin: """Validation methods for specific model compatibility.""" diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py index 98383614b1..ff47b94270 100644 --- a/tests/e2e/integrations/test_kd.py +++ b/tests/e2e/integrations/test_kd.py @@ -25,7 +25,7 @@ def min_cfg(temp_dir): "liger_rms_norm": True, "liger_glu_activation": True, "torch_compile": True, - "chat_template": "llama3", + "chat_template": "qwen3", "kd_trainer": True, "kd_ce_alpha": 0.1, "kd_alpha": 0.9, diff --git a/tests/e2e/test_streaming.py b/tests/e2e/test_streaming.py new file mode 100644 index 0000000000..5dccf00dd2 --- /dev/null +++ b/tests/e2e/test_streaming.py @@ -0,0 +1,73 @@ +"""E2E tests for streaming dataset functionality""" + +# pylint: disable=duplicate-code + +import pytest + +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +from .utils import check_model_output_exists, check_tensorboard + + +class TestStreamingDatasets: + """Test case for streaming datasets""" + + @pytest.mark.parametrize( + "sample_packing", + [True, False], + ) + def test_streaming_dataset(self, temp_dir, sample_packing): + """Test streaming datasets""" + + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "flash_attention": True, + "sequence_len": 1024, + "sample_packing": sample_packing, + "pretrain_multipack_attn": sample_packing, + "streaming_multipack_buffer_size": 10000, + "dataset_processes": 1, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + # Streaming config + "streaming": True, + "max_steps": 3, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + "use_tensorboard": True, + "save_first_step": False, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) + + # Verify training actually happened by checking loss decrease + check_tensorboard( + temp_dir + "/runs", + "train/train_loss", + 3.0, + "Train Loss (%s) is too high", + ) diff --git a/tests/test_data.py b/tests/test_data.py index 6d583cfd31..99ed063365 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -6,7 +6,7 @@ from transformers import LlamaTokenizer -from axolotl.utils.data import encode_pretraining, md5 +from axolotl.utils.data import encode_streaming, md5 from tests.hf_offline_utils import enable_hf_offline @@ -39,7 +39,7 @@ def test_encode_pretraining(self): "hello, hello", ] } - result = encode_pretraining(self.tokenizer, self.max_tokens, examples) + result = encode_streaming(examples, self.tokenizer, self.max_tokens) self.assertEqual(len(result["input_ids"]), 3) diff --git a/tests/test_packed_dataset.py b/tests/test_packed_dataset.py index 43e4f3d395..64f314e2e5 100644 --- a/tests/test_packed_dataset.py +++ b/tests/test_packed_dataset.py @@ -1,16 +1,11 @@ """Module for testing dataset sequence packing""" import unittest -from pathlib import Path -from datasets import Dataset, load_dataset from transformers import AutoTokenizer from axolotl.cli.args import TrainerCliArgs from axolotl.common.datasets import load_datasets -from axolotl.datasets import ConstantLengthDataset, TokenizedPromptDataset -from axolotl.prompt_tokenizers import AlpacaPromptTokenizingStrategy -from axolotl.prompters import AlpacaPrompter from axolotl.train import setup_model_and_trainer from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault @@ -35,43 +30,6 @@ def setUp(self) -> None: } ) - def test_increments_attention(self): - prompter = AlpacaPrompter("chat") - strat = AlpacaPromptTokenizingStrategy( - prompter, - self.tokenizer, - False, - 2048, - ) - dateset = load_dataset( - "json", - data_files=str(Path(__file__).parent / "fixtures/alpaca/alpaca.json"), - )["train"] - dataset = Dataset.from_list(list(TokenizedPromptDataset(strat, dateset))) - - constant_len_dataset = ConstantLengthDataset( - self.tokenizer, - [dataset], - seq_length=2048, - ) - packed_dataset = Dataset.from_list(list(constant_len_dataset)) - example = packed_dataset[0] - next_bos_index = ( - example["input_ids"][1:].index(self.tokenizer.bos_token_id) + 1 - ) # add one since we sliced - - # first example doesn't have mask reset - assert example["input_ids"][0] == self.tokenizer.bos_token_id - assert example["attention_mask"][0] == 1 - assert example["position_ids"][0] == 0 - assert example["position_ids"][1] == 1 - - # but subsequent one does - assert example["input_ids"][next_bos_index] == self.tokenizer.bos_token_id - assert example["attention_mask"][next_bos_index] == 2 - assert example["position_ids"][next_bos_index] == 0 - assert example["position_ids"][next_bos_index + 1] == 1 - @with_temp_dir def test_lora_packing(self, temp_dir): cfg = DictDefault( diff --git a/tests/test_packed_pretraining.py b/tests/test_packed_pretraining.py index 117bc0dbd9..0458f7ba23 100644 --- a/tests/test_packed_pretraining.py +++ b/tests/test_packed_pretraining.py @@ -9,7 +9,7 @@ from datasets import IterableDataset from torch.utils.data import DataLoader -from axolotl.utils.data import get_dataset_wrapper, wrap_pretraining_dataset +from axolotl.utils.data import get_dataset_wrapper, wrap_streaming_dataset from axolotl.utils.dict import DictDefault @@ -77,14 +77,11 @@ def test_packing_stream_dataset(self, tokenizer_huggyllama, random_text): ) original_bsz = cfg.micro_batch_size - train_dataset = wrap_pretraining_dataset( + train_dataset = wrap_streaming_dataset( dataset, tokenizer_huggyllama, cfg, ds_wrapper_partial, - max_tokens=cfg.sequence_len, - batch_size=cfg.micro_batch_size, - seed=cfg.seed or 42, ) trainer_loader = DataLoader( diff --git a/tests/test_streaming.py b/tests/test_streaming.py new file mode 100644 index 0000000000..54acbb5e4d --- /dev/null +++ b/tests/test_streaming.py @@ -0,0 +1,238 @@ +"""Test streaming configuration and data loading functionality.""" + +import unittest +from unittest.mock import Mock, patch + +from datasets import IterableDataset + +from axolotl.utils.dict import DictDefault +from axolotl.utils.data.sft import ( + _prepare_streaming_dataset, + prepare_datasets, +) +from axolotl.utils.config import validate_config + + +class TestStreamingConfig(unittest.TestCase): + """Test streaming configuration and deprecation handling.""" + + def test_streaming_multipack_buffer_size_deprecation(self): + """Test that pretrain_multipack_buffer_size is properly deprecated.""" + # Test with old config name + cfg_old = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "pretrain_multipack_buffer_size": 5000, + "datasets": [{"path": "test/dataset", "type": "alpaca"}], + "sequence_len": 256, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 0.0001, + } + ) + + with self.assertLogs("axolotl.utils.schemas.validation", level="WARNING") as cm: + validated_cfg = validate_config(cfg_old) + self.assertIn("pretrain_multipack_buffer_size` is deprecated", cm.output[0]) + + self.assertEqual(validated_cfg.streaming_multipack_buffer_size, 5000) + self.assertIsNone( + getattr(validated_cfg, "pretrain_multipack_buffer_size", None) + ) + + def test_streaming_multipack_buffer_size_new(self): + """Test that new streaming_multipack_buffer_size works correctly.""" + cfg_new = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "streaming_multipack_buffer_size": 7000, + "datasets": [{"path": "test/dataset", "type": "alpaca"}], + "sequence_len": 256, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 0.0001, + } + ) + + validated_cfg = validate_config(cfg_new) + self.assertEqual(validated_cfg.streaming_multipack_buffer_size, 7000) + + def test_both_buffer_sizes_raises_error(self): + """Test that having both old and new buffer size configs raises an error.""" + cfg_both = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "pretrain_multipack_buffer_size": 5000, + "streaming_multipack_buffer_size": 7000, + "datasets": [{"path": "test/dataset", "type": "alpaca"}], + "sequence_len": 256, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 0.0001, + } + ) + + with self.assertRaises(ValueError) as cm: + validate_config(cfg_both) + self.assertIn("both are set", str(cm.exception)) + + +class TestStreamingDatasetPreparation(unittest.TestCase): + """Test dataset preparation with streaming configuration.""" + + def setUp(self): + self.tokenizer = Mock() + self.tokenizer.pad_token_id = 0 + self.tokenizer.eos_token_id = 1 + + @patch("axolotl.utils.data.sft._prepare_streaming_dataset") + def test_prepare_datasets_with_streaming_true(self, mock_prepare_streaming): + """Test that streaming=True triggers streaming dataset preparation.""" + cfg = DictDefault( + { + "streaming": True, + "datasets": [{"path": "test/dataset", "type": "alpaca"}], + } + ) + + mock_prepare_streaming.return_value = (Mock(), None, 100, []) + + prepare_datasets(cfg, self.tokenizer) + + mock_prepare_streaming.assert_called_once_with(cfg, self.tokenizer, None) + + @patch("axolotl.utils.data.sft._prepare_streaming_dataset") + def test_prepare_datasets_with_pretraining_dataset(self, mock_prepare_streaming): + """Test that pretraining_dataset triggers streaming dataset preparation.""" + cfg = DictDefault( + { + "pretraining_dataset": "test/dataset", + } + ) + + mock_prepare_streaming.return_value = (Mock(), None, 100, []) + + prepare_datasets(cfg, self.tokenizer) + + mock_prepare_streaming.assert_called_once_with(cfg, self.tokenizer, None) + + @patch("axolotl.utils.data.sft._prepare_standard_dataset") + def test_prepare_datasets_without_streaming(self, mock_prepare_standard): + """Test that without streaming, standard dataset preparation is used.""" + cfg = DictDefault( + { + "datasets": [{"path": "test/dataset", "type": "alpaca"}], + } + ) + + mock_prepare_standard.return_value = (Mock(), None, 100, []) + + prepare_datasets(cfg, self.tokenizer) + + mock_prepare_standard.assert_called_once_with(cfg, self.tokenizer, None) + + +class TestStreamingWithSamplePacking(unittest.TestCase): + """Test streaming dataset preparation with sample packing.""" + + def setUp(self): + self.tokenizer = Mock() + self.tokenizer.pad_token_id = 0 + self.tokenizer.eos_token_id = 1 + + @patch("axolotl.utils.data.sft._load_streaming_dataset") + def test_streaming_sft_with_sample_packing_sets_split(self, mock_load_streaming): + """Test that streaming SFT with sample_packing sets default split.""" + cfg = DictDefault( + { + "streaming": True, + "sample_packing": True, + "datasets": [{"path": "test/dataset", "type": "alpaca"}], + "sequence_len": 256, + "micro_batch_size": 1, + } + ) + + mock_load_streaming.return_value = Mock(spec=IterableDataset) + + with patch("axolotl.utils.data.sft._load_and_prepare_datasets"): + _prepare_streaming_dataset(cfg, self.tokenizer, None) + + # Check that the dataset config has split set to 'train' + call_args = mock_load_streaming.call_args + dataset_config = call_args[0][0] + self.assertEqual(dataset_config.split, "train") + + def test_multipack_attn_forced_true_for_sft(self): + """Test that multipack_attn is forced to True for SFT with sample packing.""" + from axolotl.utils.data.streaming import wrap_streaming_dataset + + cfg = DictDefault( + { + "sample_packing": True, + "pretrain_multipack_attn": False, # Should be overridden for SFT + "pretraining_dataset": None, # This makes it SFT + "sequence_len": 256, + "micro_batch_size": 1, + "streaming_multipack_buffer_size": 1000, + "seed": 42, + } + ) + + mock_dataset = Mock() + mock_dataset.features = None # For streaming datasets + mock_dataset.__iter__ = Mock(return_value=iter([])) # Empty iterator + mock_dataset.map = Mock(return_value=mock_dataset) + mock_ds_wrapper = Mock() + + with patch( + "axolotl.utils.data.streaming.PretrainingBatchSamplerDataCollatorForSeq2Seq" + ) as mock_collator: + with patch("axolotl.utils.data.streaming.encode_packed_streaming"): + wrap_streaming_dataset( + mock_dataset, self.tokenizer, cfg, mock_ds_wrapper + ) + + # Check that multipack_attn=True was used in the collator + mock_collator.assert_called_once() + call_kwargs = mock_collator.call_args[1] + self.assertTrue(call_kwargs["multipack_attn"]) + + def test_multipack_attn_respects_config_for_pretraining(self): + """Test that multipack_attn respects config for pretraining datasets.""" + from axolotl.utils.data.streaming import wrap_streaming_dataset + + cfg = DictDefault( + { + "sample_packing": True, + "pretrain_multipack_attn": False, # Should be respected for pretraining + "pretraining_dataset": "test/dataset", # This makes it pretraining + "sequence_len": 256, + "micro_batch_size": 1, + "streaming_multipack_buffer_size": 1000, + "seed": 42, + } + ) + + mock_dataset = Mock() + mock_dataset.features = None # For streaming datasets + mock_dataset.__iter__ = Mock(return_value=iter([])) # Empty iterator + mock_dataset.map = Mock(return_value=mock_dataset) + mock_ds_wrapper = Mock() + + with patch( + "axolotl.utils.data.streaming.PretrainingBatchSamplerDataCollatorForSeq2Seq" + ) as mock_collator: + with patch("axolotl.utils.data.streaming.encode_packed_streaming"): + wrap_streaming_dataset( + mock_dataset, self.tokenizer, cfg, mock_ds_wrapper + ) + + # Check that multipack_attn=False was used (respecting config) + mock_collator.assert_called_once() + call_kwargs = mock_collator.call_args[1] + self.assertFalse(call_kwargs["multipack_attn"]) + + +if __name__ == "__main__": + unittest.main() From 06bebcb65f2b2826d94f47ca0c2b36ea0ea80c67 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 2 Sep 2025 13:13:23 -0400 Subject: [PATCH 0946/1405] run cu128-2.8.0 e2e tests on B200 (#3126) * run cu128-2.8.0 e2e tests on B200 * not an int :facepalm: * fix yaml --- .github/workflows/tests.yml | 2 ++ cicd/single_gpu.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 59011ee77b..337230d4a5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -303,6 +303,7 @@ jobs: python_version: "3.11" pytorch: 2.8.0 num_gpus: 1 + gpu_type: "B200" axolotl_extras: steps: - name: Checkout @@ -324,6 +325,7 @@ jobs: echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV + echo "GPU_TYPE=${{ matrix.gpu_type || 'L40S'}}" >> $GITHUB_ENV echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV - name: Run tests job on Modal diff --git a/cicd/single_gpu.py b/cicd/single_gpu.py index 0e2922e900..5a06a34f00 100644 --- a/cicd/single_gpu.py +++ b/cicd/single_gpu.py @@ -57,7 +57,8 @@ } N_GPUS = int(os.environ.get("N_GPUS", 1)) -GPU_CONFIG = f"L40S:{N_GPUS}" +GPU_TYPE = os.environ.get("GPU_TYPE", "L40S") +GPU_CONFIG = f"{GPU_TYPE}:{N_GPUS}" def run_cmd(cmd: str, run_folder: str): From 24aba5cacaf22c137882ae5d5b64f4e2c42ee23e Mon Sep 17 00:00:00 2001 From: xuyifann <159863565+xuyifann@users.noreply.github.com> Date: Tue, 2 Sep 2025 22:40:27 -0700 Subject: [PATCH 0947/1405] Clamping the len of dataloader to minimum of 1 (#3100) [skip ci] * Clamping the len of dataloader to minimum of 1 * linter reformat --- src/axolotl/utils/trainer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 43f76c0cd0..a0f4fd567c 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -475,7 +475,9 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): train_dataset.remove_columns(["length"]), batch_sampler=sampler, ) - data_loader_len = len(data_loader) * cfg.micro_batch_size // cfg.batch_size + data_loader_len = max( + 1, len(data_loader) * cfg.micro_batch_size // cfg.batch_size + ) LOG.debug(f"data_loader_len: {data_loader_len}") # FIXME: is there a bug here somewhere? the total num steps depends # on the agreed on value for sample_packing_eff_est From e48aa8a5b1d7b6e2fd4da18768e64fab74642259 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 3 Sep 2025 12:40:53 +0700 Subject: [PATCH 0948/1405] feat(doc): improve visibility for colab notebooks (#3110) [skip ci] * feat: improve visibility for colab notebooks * fix: link to GH colab * feat: change to badge and move higher --- README.md | 5 +++++ docs/installation.qmd | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 117eb9b12f..d4794124a3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@
discord twitter + google-colab
tests-nightly multigpu-semi-weekly tests @@ -70,6 +71,10 @@ Features: - Python 3.11 - PyTorch ≥2.6.0 +### Google Colab + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/axolotl-ai-cloud/axolotl/blob/main/examples/colab-notebooks/colab-axolotl-example.ipynb#scrollTo=msOCO4NRmRLa) + ### Installation #### Using pip diff --git a/docs/installation.qmd b/docs/installation.qmd index 763539278b..265ff238c1 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -134,7 +134,7 @@ For providers supporting Docker: ### Google Colab {#sec-colab} -Use our [example notebook](../examples/colab-notebooks/colab-axolotl-example.ipynb). +[![](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/axolotl-ai-cloud/axolotl/blob/main/examples/colab-notebooks/colab-axolotl-example.ipynb#scrollTo=msOCO4NRmRLa) ## Platform-Specific Instructions {#sec-platform-specific} From 4cc6038d52b2b66794be150b0caab82ede436872 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 01:41:34 -0400 Subject: [PATCH 0949/1405] chore: update pre-commit hooks (#3122) [skip ci] Co-authored-by: djsaunde <1245942+djsaunde@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c2861346e..53e49d7474 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: no-commit-to-branch args: ['--branch', 'main'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.9 + rev: v0.12.11 hooks: - id: ruff args: [--fix] From 53a0c1f39c3f043135d93723ff46fa27cc9360fc Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 3 Sep 2025 12:48:01 +0700 Subject: [PATCH 0950/1405] feat: add peft_trainable_token_indices (#3062) * feat: add peft_trainable_token_indices * feat: add warning compat with fix_untrained_tokens --- src/axolotl/loaders/adapter.py | 2 ++ src/axolotl/utils/schemas/config.py | 30 ++++++++++++++++++++++++++++- src/axolotl/utils/schemas/peft.py | 10 ++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index 867e6901cb..989b34aee0 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -98,6 +98,8 @@ def load_lora( lora_config_kwargs["use_rslora"] = cfg.peft_use_rslora if cfg.peft_layer_replication: lora_config_kwargs["layer_replication"] = cfg.peft_layer_replication + if cfg.peft_trainable_token_indices: + lora_config_kwargs["trainable_token_indices"] = cfg.peft_trainable_token_indices lora_config = LoraConfig( r=cfg.lora_r, diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index d43c346cd8..1d2ddf4aed 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -947,7 +947,15 @@ class AxolotlInputConfig( }, ) - fix_untrained_tokens: int | list[int] | None = None + fix_untrained_tokens: int | list[int] | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Token index or indices to adjust embedding weights to the mean of the other tokens. " + "This is useful when the model has untrained embeddings." + ) + }, + ) # INTERNALS - document for now, generally not set externally is_preprocess: bool | None = None @@ -1006,6 +1014,26 @@ def datasets_serializer( return [ds_config.model_dump(exclude_none=True) for ds_config in ds_configs] return None + @model_validator(mode="before") + @classmethod + def warn_peft_trainable_token_to_fix_untrained(cls, data): + if ( + peft_trainable_token_indices := data.get("peft_trainable_token_indices") + ) and (fix_untrained_tokens := data.get("fix_untrained_tokens")): + if isinstance(fix_untrained_tokens, int): + fix_untrained_tokens = (fix_untrained_tokens,) + + if isinstance(peft_trainable_token_indices, int): + peft_trainable_token_indices = (peft_trainable_token_indices,) + + for untrained_token_id in fix_untrained_tokens: + if untrained_token_id not in peft_trainable_token_indices: + LOG.warning_once( + f"Token {untrained_token_id} is fixed via `fix_untrained_tokens`, yet not in `peft_trainable_token_indices: ` list. " + "Please add it, otherwise the token won't be trained on." + ) + return data + class AxolotlConfigWCapabilities(AxolotlInputConfig): """wrapper to valdiate GPU capabilities with the configured options""" diff --git a/src/axolotl/utils/schemas/peft.py b/src/axolotl/utils/schemas/peft.py index de29521cb4..af22913fd5 100644 --- a/src/axolotl/utils/schemas/peft.py +++ b/src/axolotl/utils/schemas/peft.py @@ -90,6 +90,16 @@ class LoraConfig(BaseModel): "description": "How to initialize LoRA weights. Default to True which is MS original implementation." }, ) + peft_trainable_token_indices: list[int] | dict[str, list[int]] | None = Field( + default=None, + json_schema_extra={ + "description": ( + "A list of token indices to fine-tune on the `embed_tokens` layer.\n" + "Otherwise, a dict mapping an embedding layer name to its trainable token indices.\n" + "See https://huggingface.co/docs/peft/v0.17.0/en/developer_guides/lora#efficiently-train-tokens-alongside-lora" + ) + }, + ) qlora_sharded_model_loading: bool | None = Field( default=False, From 48db520d92e541055bde7e41e0269b6a80ca2301 Mon Sep 17 00:00:00 2001 From: mhenrichsen Date: Wed, 3 Sep 2025 22:20:32 +0200 Subject: [PATCH 0951/1405] Create 270m-qlora.yml (#3075) [skip ci] Adds 270m gemma3 qlora --- examples/gemma3/270m-qlora.yml | 68 ++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 examples/gemma3/270m-qlora.yml diff --git a/examples/gemma3/270m-qlora.yml b/examples/gemma3/270m-qlora.yml new file mode 100644 index 0000000000..8744fad269 --- /dev/null +++ b/examples/gemma3/270m-qlora.yml @@ -0,0 +1,68 @@ +base_model: google/gemma-3-270m-it +# optionally might have model_type or tokenizer_type +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +# gemma3 doesn't seem to play nice with ddp +ddp_find_unused_parameters: true + +load_in_8bit: false +load_in_4bit: true + +# huggingface repo +chat_template: gemma3 +eot_tokens: + - +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: false + + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: From efa1da52d500ae0dfa4cc192523a0a587611f020 Mon Sep 17 00:00:00 2001 From: yardenhoch <137788890+yardenhoch@users.noreply.github.com> Date: Wed, 3 Sep 2025 23:22:37 +0300 Subject: [PATCH 0952/1405] Center rewards coefficient (#3124) * feat: add center_rewards_coefficient for reward modeling - Add center_rewards_coefficient parameter to Pydantic schema with paper reference - Pass parameter through base builder and causal builder to training args - Add documentation section with usage examples and theoretical background - Enable parameter in reward modeling example configs with recommended value - Enables reward centering for improved training stability in RLHF workflows Implements auxiliary loss from Eisenstein et al. 2023 (https://huggingface.co/papers/2312.09244) to incentivize mean-zero reward outputs without post-training normalization. * Update description * test: add unit tests for center_rewards_coefficient integration * Update src/axolotl/core/builders/base.py Co-authored-by: NanoCode012 * Update docs/reward_modelling.qmd Co-authored-by: NanoCode012 * Update docs/reward_modelling.qmd Co-authored-by: NanoCode012 * reference to TRL documentation. * add new reward model configuration for qwen3 with comprehensive parameters * Verified center_rewards_coefficient is correctly passed through the trainer builder to training arguments. * Refactor reward modeling documentation to consolidate information on center_rewards_coefficient * Remove unit tests for center_rewards_coefficient integration as part of codebase cleanup. * linting * nit * Apply suggestions from code review Co-authored-by: NanoCode012 * lint --------- Co-authored-by: NanoCode012 Co-authored-by: Salman Mohammadi --- docs/reward_modelling.qmd | 1 + examples/qwen3/reward-model.yaml | 44 +++++++++++++++++++++++++++++ src/axolotl/core/builders/causal.py | 13 +++++---- src/axolotl/utils/schemas/config.py | 6 ++++ 4 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 examples/qwen3/reward-model.yaml diff --git a/docs/reward_modelling.qmd b/docs/reward_modelling.qmd index 386dc1f572..b5cf3010dc 100644 --- a/docs/reward_modelling.qmd +++ b/docs/reward_modelling.qmd @@ -11,6 +11,7 @@ We support the reward modelling techniques supported by `trl`. ### (Outcome) Reward Models Outcome reward models are trained using data which contains preference annotations for an entire interaction between the user and model (e.g. rather than per-turn or per-step). +For improved training stability, you can use the `center_rewards_coefficient` parameter to encourage mean-zero reward outputs ([see TRL docs](https://huggingface.co/docs/trl/v0.10.1/en/reward_trainer#centering-rewards)). ```yaml base_model: google/gemma-2-2b diff --git a/examples/qwen3/reward-model.yaml b/examples/qwen3/reward-model.yaml new file mode 100644 index 0000000000..43c62ecc4c --- /dev/null +++ b/examples/qwen3/reward-model.yaml @@ -0,0 +1,44 @@ +base_model: Skywork/Skywork-Reward-V2-Qwen3-8B +model_type: AutoModelForSequenceClassification +num_labels: 1 + +reward_model: true +center_rewards_coefficient: 0.01 # Incentivize mean-zero rewards for improved stability +chat_template: qwen3 +datasets: + - path: argilla/distilabel-intel-orca-dpo-pairs + type: bradley_terry.chat_template + +val_set_size: 0.0 +output_dir: ./outputs/out + +sequence_len: 8192 +sample_packing: false +eval_sample_packing: false +pad_to_sequence_len: true + +deepspeed: deepspeed_configs/zero1.json + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +eval_batch_size: 1 +num_epochs: 3 +optimizer: adamw_bnb_8bit +lr_scheduler: linear +learning_rate: 0.00002 + +bf16: true +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +warmup_ratio: 0.1 +logging_steps: 1 +weight_decay: 0.01 diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index e5bc68c39d..057d0ab5cc 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -7,10 +7,7 @@ from typing import Type, Union import transformers -from transformers import ( - DataCollatorWithFlattening, - EarlyStoppingCallback, -) +from transformers import DataCollatorWithFlattening, EarlyStoppingCallback from trl.trainer.utils import RewardDataCollatorWithPadding from axolotl.core.builders.base import TrainerBuilderBase @@ -26,12 +23,12 @@ from axolotl.processing_strategies import get_processing_strategy from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.callbacks import ( - LossWatchDogCallback, - SaveBetterTransformerModelCallback, bench_eval_callback_factory, causal_lm_bench_eval_callback_factory, colab_inference_post_train_callback, log_prediction_callback_factory, + LossWatchDogCallback, + SaveBetterTransformerModelCallback, ) from axolotl.utils.callbacks.lisa import lisa_callback_factory from axolotl.utils.callbacks.qat import QATCallback @@ -340,6 +337,10 @@ def build(self, total_num_steps): if self.cfg.reward_model: training_args_cls = AxolotlRewardConfig + if self.cfg.center_rewards_coefficient is not None: + training_arguments_kwargs["center_rewards_coefficient"] = ( + self.cfg.center_rewards_coefficient + ) elif self.cfg.process_reward_model: training_args_cls = AxolotlPRMConfig else: diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 1d2ddf4aed..32d7b68e79 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -138,6 +138,12 @@ class AxolotlInputConfig( "description": "Process reward modelling: `True` or `False`" }, ) + center_rewards_coefficient: float | None = Field( + default=None, + json_schema_extra={ + "description": "Coefficient to incentivize the reward model to output mean-zero rewards (proposed by https://huggingface.co/papers/2312.09244, Eq. 2). Recommended value: `0.01`." + }, + ) num_labels: int | None = None # Whether to use weighting in DPO trainer. # If `None`, default is `False` in the trainer. From c6ae5c43cbd900e72222f1515adfb081a3500b6a Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 4 Sep 2025 03:25:09 +0700 Subject: [PATCH 0953/1405] fix: chat template jinja file not being loaded during inference (#3112) * fix: chat template jinja file not being loaded during inference * fix: bot comment --- src/axolotl/cli/inference.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index 06b64292fe..debe571672 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -14,10 +14,7 @@ from axolotl.cli.args import InferenceCliArgs from axolotl.cli.config import load_cfg from axolotl.cli.utils import load_model_and_tokenizer -from axolotl.utils.chat_templates import ( - get_chat_template, - get_chat_template_from_config, -) +from axolotl.utils.chat_templates import get_chat_template_from_config from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger @@ -64,7 +61,9 @@ def do_inference( importlib.import_module("axolotl.prompters"), prompter ) elif cfg.chat_template: - chat_template_str = get_chat_template(cfg.chat_template, tokenizer=tokenizer) + chat_template_str = get_chat_template_from_config( + cfg, ds_cfg=None, tokenizer=tokenizer + ) elif cfg.datasets[0].type == "chat_template": chat_template_str = get_chat_template_from_config( cfg=cfg, ds_cfg=cfg.datasets[0], tokenizer=tokenizer @@ -159,7 +158,13 @@ def do_inference_gradio( importlib.import_module("axolotl.prompters"), prompter ) elif cfg.chat_template: - chat_template_str = get_chat_template(cfg.chat_template, tokenizer=tokenizer) + chat_template_str = get_chat_template_from_config( + cfg, ds_cfg=None, tokenizer=tokenizer + ) + elif cfg.datasets[0].type == "chat_template": + chat_template_str = get_chat_template_from_config( + cfg=cfg, ds_cfg=cfg.datasets[0], tokenizer=tokenizer + ) model = model.to(cfg.device, dtype=cfg.torch_dtype) From 1d32278755108c47eafbb6dca95c62e807771351 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 5 Sep 2025 22:00:54 +0700 Subject: [PATCH 0954/1405] feat: upgrade transformers to v4.56.1 (#3127) * feat: upgrade transformers to v4.56 * fix handling of CP/SP now that position_ids are default even for unpacked sequences * feat: monkeypatch list_repo_templates * fix: apply patch for tests only * see if updated main works at least * fix: update to patch release and remove monkeypatch * remove fsdp2 eval patch --------- Co-authored-by: Wing Lian --- requirements.txt | 2 +- src/axolotl/loaders/patch_manager.py | 8 +------ .../transformers/trainer_loss_calc.py | 24 +------------------ .../utils/ctx_managers/sequence_parallel.py | 4 ++-- tests/monkeypatch/test_trainer_loss_calc.py | 2 -- 5 files changed, 5 insertions(+), 35 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9e3dbbca4e..1292a179a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ packaging==23.2 huggingface_hub>=0.33.0 peft>=0.17.0 -transformers==4.55.4 +transformers==4.56.1 tokenizers>=0.21.1 accelerate==1.10.0 datasets==4.0.0 diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 94b307a628..044c278a36 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -80,13 +80,7 @@ def _apply_transformers_patches(self): patch_maybe_log_save_evaluate, ) - patch_fsdp2 = ( - self.cfg.torch_compile - and self.cfg.fsdp_config - and self.cfg.fsdp_version == 2 - ) - - patch_evaluation_loop(patch_fsdp2) + patch_evaluation_loop() patch_maybe_log_save_evaluate() def apply_post_model_load_patches(self, model: PreTrainedModel): diff --git a/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py b/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py index 012c699fa0..c9b968d719 100644 --- a/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py +++ b/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py @@ -28,15 +28,6 @@ "array": 'metrics[f"{metric_key_prefix}_loss"] = np.nanmean(all_losses).item()', } -ORIGINAL_FSDP2_CODE = """ - model.eval() -""" - -PATCHED_FSDP2_CODE = """ - if hasattr(model, "eval") and callable(model.eval): - self.model.eval() -""" - ORIGINAL_MAYBE_CODE = "tr_loss_scalar = self._nested_gather(tr_loss).mean().item()" PATCHED_MAYBE_CODE = "tr_loss_scalar = self._nested_gather(tr_loss).nanmean().item()" @@ -46,13 +37,7 @@ def check_evaluation_loop_is_patchable() -> bool: return all(value in evaluation_loop_source for value in ORIGINAL_EVAL_CODE.values()) -def check_evaluation_loop_is_fsdp2_patchable() -> bool: - evaluation_loop_source = inspect.getsource(Trainer.evaluation_loop) - evaluation_loop_source, _ = detab_code(evaluation_loop_source) - return ORIGINAL_FSDP2_CODE in evaluation_loop_source - - -def patch_evaluation_loop(patch_fsdp2: bool): +def patch_evaluation_loop(): """Patch the evaluation_loop method.""" # Check if already patched if hasattr(Trainer, "_original_evaluation_loop"): @@ -75,13 +60,6 @@ def patch_evaluation_loop(patch_fsdp2: bool): ORIGINAL_EVAL_CODE["array"], PATCHED_EVAL_CODE["array"] ) - # Apply FSDP2 eval guard patch if needed - if patch_fsdp2 and ORIGINAL_FSDP2_CODE in evaluation_loop_source: - evaluation_loop_source = evaluation_loop_source.replace( - ORIGINAL_FSDP2_CODE, PATCHED_FSDP2_CODE - ) - LOG.info("Applied FSDP2 eval guard patch to evaluation_loop") - # Rename the function to avoid conflicts evaluation_loop_source = evaluation_loop_source.replace( "def evaluation_loop(", diff --git a/src/axolotl/utils/ctx_managers/sequence_parallel.py b/src/axolotl/utils/ctx_managers/sequence_parallel.py index 1ec91ae2ac..78b3d1cae5 100644 --- a/src/axolotl/utils/ctx_managers/sequence_parallel.py +++ b/src/axolotl/utils/ctx_managers/sequence_parallel.py @@ -48,10 +48,10 @@ def apply_sequence_parallelism( - The original sequence length before padding. - The number of padding tokens added. """ - original_seq_len = batch["input_ids"].size(1) + batch_size, original_seq_len = batch["input_ids"].shape # Update ring attention params if needed - if batch.get("position_ids") is not None: + if batch.get("position_ids") is not None and batch_size == 1: update_ring_attn_params(position_ids=batch["position_ids"]) else: # If position_ids aren't already in the batch, create them diff --git a/tests/monkeypatch/test_trainer_loss_calc.py b/tests/monkeypatch/test_trainer_loss_calc.py index de3e926210..c72cb621b3 100644 --- a/tests/monkeypatch/test_trainer_loss_calc.py +++ b/tests/monkeypatch/test_trainer_loss_calc.py @@ -3,7 +3,6 @@ import unittest from axolotl.monkeypatch.transformers.trainer_loss_calc import ( - check_evaluation_loop_is_fsdp2_patchable, check_evaluation_loop_is_patchable, check_maybe_log_save_evaluate_is_patchable, ) @@ -20,7 +19,6 @@ def test_trainer_loss_calc_is_patchable(self): the patched code changes upstream. """ assert check_evaluation_loop_is_patchable() - assert check_evaluation_loop_is_fsdp2_patchable() assert check_maybe_log_save_evaluate_is_patchable() From bf00f29f3a51b66221d3321c7de1c981c137db12 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 10:33:20 -0400 Subject: [PATCH 0955/1405] chore: update pre-commit hooks (#3137) [skip ci] Co-authored-by: djsaunde <1245942+djsaunde@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 53e49d7474..92ddc7f41e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: no-commit-to-branch args: ['--branch', 'main'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.11 + rev: v0.12.12 hooks: - id: ruff args: [--fix] From 8fd9221f134da88773d62a8eb1e6f5f068ad5d8c Mon Sep 17 00:00:00 2001 From: Seungduk Kim Date: Sun, 7 Sep 2025 23:49:10 +0900 Subject: [PATCH 0956/1405] Add `ipo` as an `rl` type that shares DPODataset config (#3128) * Add `ipo` as an `rl` type that shares DPODataset config * chore: lint --------- Co-authored-by: Wing Lian --- src/axolotl/utils/config/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 2b6ef8d985..f40fe6687d 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -273,7 +273,9 @@ def validate_config( # Convert datasets to proper format if needed if cfg.get("datasets"): for idx, ds_cfg in enumerate(cfg["datasets"]): - if cfg.get("rl") in ["dpo", "simpo"] and not isinstance(ds_cfg, DPODataset): + if cfg.get("rl") in ["dpo", "ipo", "simpo"] and not isinstance( + ds_cfg, DPODataset + ): cfg["datasets"][idx] = DPODataset(**ds_cfg) elif cfg.get("rl") == "kto" and not isinstance(ds_cfg, KTODataset): cfg["datasets"][idx] = KTODataset(**dict(ds_cfg)) From b5d4c7ff542d2eda635f9230e96f64373ecd5418 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 7 Sep 2025 11:01:03 -0400 Subject: [PATCH 0957/1405] allow 1% deviation for codecov (#3138) [skip ci] --- codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index 28921f9be2..fa3ad30731 100644 --- a/codecov.yml +++ b/codecov.yml @@ -12,7 +12,7 @@ coverage: default: # basic target: auto - threshold: 0% + threshold: 1% base: auto # advanced branches: null @@ -27,7 +27,7 @@ coverage: default: # basic target: auto - threshold: 0% + threshold: 1% base: auto # advanced branches: null From 9640338d37d0398cd3c0c0ab6e629b6dd9dcd5d3 Mon Sep 17 00:00:00 2001 From: salman Date: Tue, 9 Sep 2025 15:50:21 +0100 Subject: [PATCH 0958/1405] Default `include_tkps` to true (#3134) * default true * force e2e * causal trainer only * fix eval loggin [skip-ci] * revert setup.py * force tests * guarding * guarding * fix test case * use evaluate [skip-e2e] * use evaluate [skip-e2e] * kick off ci * fixing * reverting --- src/axolotl/core/builders/base.py | 7 ------- src/axolotl/core/builders/causal.py | 7 +++++++ src/axolotl/core/trainers/base.py | 4 ++-- src/axolotl/utils/callbacks/tokens_per_second.py | 16 +++++++++------- src/axolotl/utils/schemas/config.py | 4 ++-- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index bee291fa2a..1ec818004e 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -36,7 +36,6 @@ SaveModelOnFirstStepCallback, ) from axolotl.utils.callbacks.profiler import PytorchProfilerCallback -from axolotl.utils.callbacks.tokens_per_second import TokensPerSecondCallback from axolotl.utils.distributed import build_parallelism_config from axolotl.utils.schemas.enums import CustomSupportedOptimizers @@ -145,12 +144,6 @@ def get_callbacks(self) -> list[TrainerCallback]: profiler_steps_start=self.cfg.profiler_steps_start, ) ) - if self.cfg.include_tkps: - callbacks.append( - TokensPerSecondCallback( - self.cfg.tensor_parallel_size, self.cfg.context_parallel_size - ) - ) return callbacks diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 057d0ab5cc..ee6383d473 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -39,6 +39,7 @@ MambaDataCollator, V2BatchSamplerDataCollatorForSeq2Seq, ) +from axolotl.utils.callbacks.tokens_per_second import TokensPerSecondCallback from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator from axolotl.utils.import_helper import get_cls_from_module_str from axolotl.utils.logging import get_logger @@ -71,6 +72,12 @@ def get_callbacks(self): if self.cfg.qat: callbacks.append(QATCallback(self.cfg.qat)) + if self.cfg.include_tkps: + callbacks.append( + TokensPerSecondCallback( + self.cfg.tensor_parallel_size, self.cfg.context_parallel_size + ) + ) return callbacks def get_post_trainer_create_callbacks(self, trainer): diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 06eef445be..d7555261f1 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -342,10 +342,10 @@ def compute_loss( inputs_key = "labels" if "labels" in inputs else "input_ids" if hasattr(self.state, "num_tokens"): self.state.num_tokens = ( - self.state.num_tokens + (inputs[inputs_key] != -100).sum() + self.state.num_tokens + (inputs[inputs_key] != -100).sum().cpu() ) else: - self.state.num_tokens = (inputs[inputs_key] != -100).sum() + self.state.num_tokens = (inputs[inputs_key] != -100).sum().cpu() if self.args.orpo_alpha: return self.orpo_compute_loss( diff --git a/src/axolotl/utils/callbacks/tokens_per_second.py b/src/axolotl/utils/callbacks/tokens_per_second.py index 85bcd50412..ead1292400 100644 --- a/src/axolotl/utils/callbacks/tokens_per_second.py +++ b/src/axolotl/utils/callbacks/tokens_per_second.py @@ -43,11 +43,12 @@ def on_step_end( control: TrainerControl, **kwargs, ): # pylint: disable=unused-argument - step_time = time.perf_counter() - self.start_time - num_tokens_per_device = state.num_tokens.clone() - # non data parallel groups have duplicated tokens, so we avoid double-counting - num_tokens_per_device = num_tokens_per_device / self.non_data_parallel_size - state.last_tokens_per_second = num_tokens_per_device / step_time + if hasattr(state, "num_tokens"): + step_time = time.perf_counter() - self.start_time + num_tokens_per_device = state.num_tokens.clone() + # non data parallel groups have duplicated tokens, so we avoid double-counting + num_tokens_per_device = num_tokens_per_device / self.non_data_parallel_size + state.last_tokens_per_second = num_tokens_per_device / step_time def on_log( self, @@ -58,5 +59,6 @@ def on_log( **kwargs, ): # pylint: disable=unused-argument # after logging, clear the running metrics - state.last_tokens_per_second.zero_() - state.num_tokens = 0 + if hasattr(state, "last_tokens_per_second"): + state.last_tokens_per_second.zero_() + state.num_tokens = torch.zeros(1) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 32d7b68e79..e4c1fdf291 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -855,9 +855,9 @@ class AxolotlInputConfig( }, ) include_tkps: bool | None = Field( - default=None, + default=True, json_schema_extra={ - "description": "bool of whether to report tokens per second during training by measuring throughput of non-padding tokens." + "description": "bool of whether to report tokens per second per-gpu during training by measuring throughput of non-padding tokens." }, ) neftune_noise_alpha: float | None = Field( From 79103b01ca1c914103d888d88fdb903e29840d4f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 10 Sep 2025 09:01:02 +0700 Subject: [PATCH 0959/1405] Feat: add seedoss (#3104) [skip ci] * feat: add seedoss cce * feat: add seedoss config and docs * fix: shouldn't have target modules with target linear * feat: add vram numbers * fix: hf link * fix: name * fix: support multipack seedoss * fix: merge error * feat: update seedoss instructions for transformers release --- examples/seed-oss/README.md | 54 ++++++++++++++++++ examples/seed-oss/seed-oss-36b-qlora.yaml | 56 +++++++++++++++++++ .../integrations/cut_cross_entropy/README.md | 3 + src/axolotl/monkeypatch/multipack.py | 1 + 4 files changed, 114 insertions(+) create mode 100644 examples/seed-oss/README.md create mode 100644 examples/seed-oss/seed-oss-36b-qlora.yaml diff --git a/examples/seed-oss/README.md b/examples/seed-oss/README.md new file mode 100644 index 0000000000..5610c1316b --- /dev/null +++ b/examples/seed-oss/README.md @@ -0,0 +1,54 @@ +# Finetune ByteDance's Seed-OSS with Axolotl + +[Seed-OSS](https://huggingface.co/collections/ByteDance-Seed/seed-oss-68a609f4201e788db05b5dcd) are a series of 36B parameter open source models trained by ByteDance's Seed Team. + +This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Seed-OSS is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). + + Here is an example of how to install from main for pip: + +```bash +# Ensure you have Pytorch installed (Pytorch 2.6.0 min) +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl + +pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install --no-build-isolation -e '.[flash-attn]' + +# Install Cut Cross Entropy +python scripts/cutcrossentropy_install.py | sh +``` + +2. Run the finetuning example: + +```bash +axolotl train examples/seed-oss/seed-oss-36b-qlora.yaml +``` + +This config uses about 27.7 GiB VRAM. + +Let us know how it goes. Happy finetuning! 🚀 + +### TIPS + +- For inference, the official Seed Team recommends `top_p=0.95` and `temperature=1.1`. +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) + +## Related Resources + +- [ByteDance Seed Website](https://seed.bytedance.com/) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/seed-oss/seed-oss-36b-qlora.yaml b/examples/seed-oss/seed-oss-36b-qlora.yaml new file mode 100644 index 0000000000..00e7cf3ebb --- /dev/null +++ b/examples/seed-oss/seed-oss-36b-qlora.yaml @@ -0,0 +1,56 @@ +base_model: ByteDance-Seed/Seed-OSS-36B-Instruct + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index a64bdd0548..393412f647 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -34,6 +34,7 @@ plugins: - arcee - cohere - cohere2 +- deepseek_v3 - gemma - gemma2 - gemma3 @@ -42,6 +43,7 @@ plugins: - gemma3n_text - glm - glm4 +- glm4_moe - gpt_oss - granite - granitemoe @@ -64,6 +66,7 @@ plugins: - qwen3 - qwen3_moe - smollm3 +- seed_oss - voxtral ## Citation diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index e4f9ca2be6..cbc546877a 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -38,6 +38,7 @@ "smollm3", "gpt_oss", "arcee", + "seed_oss", ] From b71482cec5beb118efdd2bc466589e9a6eb64e77 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 10 Sep 2025 09:03:30 +0700 Subject: [PATCH 0960/1405] Feat: add hunyuan v1 (#3016) * feat: add hunyuan cce support * feat: update cce docs * feat: add multipack support for granite and hunyuan * feat: add hunyuan docs and example config * feat: update readme instructions to include CCE installation * fix: chat template log appearing despite tokenizer already having template * feat: add vram usage * fix: remove duplicate cce install * fix: use latest commit of PR in case rebased/pushed * Revert "fix: use latest commit of PR in case rebased/pushed" This reverts commit 8b60aa00de5511c09a6cad64ae1cf476e6a5eddc. * feat: update doc as upstream merged --- examples/devstral/README.md | 8 +- examples/hunyuan/README.md | 85 ++++++++++++++++++++ examples/hunyuan/hunyuan-v1-dense-qlora.yaml | 64 +++++++++++++++ examples/magistral/README.md | 8 +- examples/voxtral/README.md | 3 + src/axolotl/loaders/tokenizer.py | 2 +- src/axolotl/monkeypatch/multipack.py | 4 + 7 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 examples/hunyuan/README.md create mode 100644 examples/hunyuan/hunyuan-v1-dense-qlora.yaml diff --git a/examples/devstral/README.md b/examples/devstral/README.md index b53635a8fe..ae08606628 100644 --- a/examples/devstral/README.md +++ b/examples/devstral/README.md @@ -20,7 +20,13 @@ pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` -2. Run the finetuning example: +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage + +```bash +python scripts/cutcrossentropy_install.py | sh +``` + +3. Run the finetuning example: ```bash axolotl train examples/devstral/devstral-small-qlora.yml diff --git a/examples/hunyuan/README.md b/examples/hunyuan/README.md new file mode 100644 index 0000000000..96c6bbcfa8 --- /dev/null +++ b/examples/hunyuan/README.md @@ -0,0 +1,85 @@ +# Finetune HunYuan with Axolotl + +Tencent released a family of opensource models called HunYuan with varying parameter scales of 0.5B, 1.8B, 4B, and 7B scale for both Pre-trained and Instruct variants. The models can be found at [HuggingFace](https://huggingface.co/collections/tencent/hunyuan-dense-model-6890632cda26b19119c9c5e7). This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as HunYuan is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). + + Here is an example of how to install from main for pip: + +```bash +# Ensure you have Pytorch installed (Pytorch 2.6.0 min) +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl + +pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install --no-build-isolation -e '.[flash-attn]' + +# Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy +python scripts/cutcrossentropy_install.py | sh +``` + +2. Run the finetuning example: + +```bash +axolotl train examples/hunyuan/hunyuan-v1-dense-qlora.yaml +``` + +This config uses about 4.7 GB VRAM. + +Let us know how it goes. Happy finetuning! 🚀 + +### Dataset + +HunYuan Instruct models can choose to enter a slow think or fast think pattern. For best performance on fine-tuning their Instruct models, your dataset should be adjusted to match their pattern. + +```python +# fast think pattern +messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "/no_think What color is the sun?" }, + {"role": "assistant", "content": "\n\n\n\nThe sun is yellow.\n"} +] + +# slow think pattern +messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "/no_think What color is the sun?" }, + {"role": "assistant", "content": "\nThe user is asking about the color of the sun. I need to ...\n\n\nThe sun is yellow.\n"} +] +``` + +### TIPS + +- For inference, the official Tencent team recommends + +```json + +{ + "do_sample": true, + "top_k": 20, + "top_p": 0.8, + "repetition_penalty": 1.05, + "temperature": 0.7 +} + +``` + +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) + +## Related Resources + +- [Tencent HunYuan Blog](https://hunyuan.tencent.com/) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/hunyuan/hunyuan-v1-dense-qlora.yaml b/examples/hunyuan/hunyuan-v1-dense-qlora.yaml new file mode 100644 index 0000000000..a94345a612 --- /dev/null +++ b/examples/hunyuan/hunyuan-v1-dense-qlora.yaml @@ -0,0 +1,64 @@ +base_model: tencent/Hunyuan-0.5B-Instruct + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/magistral/README.md b/examples/magistral/README.md index 48ce712da1..f4f2782088 100644 --- a/examples/magistral/README.md +++ b/examples/magistral/README.md @@ -18,7 +18,13 @@ pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` -2. Run the finetuning example: +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage + +```bash +python scripts/cutcrossentropy_install.py | sh +``` + +3. Run the finetuning example: ```bash axolotl train examples/magistral/magistral-small-qlora.yaml diff --git a/examples/voxtral/README.md b/examples/voxtral/README.md index f31e9cfd04..984af4ddb7 100644 --- a/examples/voxtral/README.md +++ b/examples/voxtral/README.md @@ -22,6 +22,9 @@ pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' # audio pip3 install librosa==0.11.0 pip3 install 'mistral_common[audio]==1.8.3' + +# Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy +python scripts/cutcrossentropy_install.py | sh ``` 3. Run the finetuning example: diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index dcc255938a..37b66ac83f 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -296,7 +296,7 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): ) tokenizer.chat_template = chat_template_string - else: + elif getattr(tokenizer, "chat_template", None) is None: LOG.info( "No Chat template selected. Consider adding a chat template for easier inference." ) diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index cbc546877a..a32430d9f3 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -36,6 +36,10 @@ "glm", "glm4", "smollm3", + "granite", + "granitemoe", + "hunyuan_v1_dense", + "hunyuan_v1_moe", "gpt_oss", "arcee", "seed_oss", From 1b53c49e1a8408ff209ae72a480681d18f7f8c81 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 10 Sep 2025 20:27:00 -0400 Subject: [PATCH 0961/1405] text diffusion training plugin (#3067) * diffusion training plugin * cleanup * nits * fixes + improvements * add back in reinit_weights (clobbered?); masking / pretrain fixes * nits * cleanup; tests draft * sample generation, tests fixes * fixes * nits * add inference support; add auto-mask token support * nits * nits * progress * simplify logging * lint * prefix args with diffusion_ * coderabbito * tests fix * nit * nits * cleanup + nits * nits * fix SFT sample gen * fixes * fix * comments * comments * lint * reward model lora fix * cleanup; fix pretraining_dataset case * gradio inference * update cfgs * update cfgs * train, generation parity, cleanup * fix * simplify * test * test fix --- .pre-commit-config.yaml | 2 +- .../colab-axolotl-example.ipynb | 2 +- examples/llama-3/diffusion/pretrain-1b.yaml | 56 +++ examples/llama-3/diffusion/sft-1b.yaml | 59 +++ src/axolotl/cli/inference.py | 63 ++- src/axolotl/cli/utils/diffusion.py | 375 ++++++++++++++++ src/axolotl/core/builders/causal.py | 15 +- src/axolotl/core/trainers/base.py | 46 +- src/axolotl/integrations/base.py | 2 +- src/axolotl/integrations/config.py | 2 +- src/axolotl/integrations/diffusion/README.md | 154 +++++++ .../integrations/diffusion/__init__.py | 19 + src/axolotl/integrations/diffusion/args.py | 95 ++++ .../integrations/diffusion/callbacks.py | 174 ++++++++ .../integrations/diffusion/generation.py | 409 ++++++++++++++++++ src/axolotl/integrations/diffusion/plugin.py | 41 ++ src/axolotl/integrations/diffusion/trainer.py | 301 +++++++++++++ src/axolotl/integrations/diffusion/utils.py | 159 +++++++ src/axolotl/loaders/adapter.py | 12 +- src/axolotl/loaders/model.py | 118 +++-- src/axolotl/loaders/patch_manager.py | 5 +- src/axolotl/monkeypatch/accelerate/fsdp2.py | 8 +- .../monkeypatch/attention/flex_attn.py | 3 +- src/axolotl/monkeypatch/deepspeed_utils.py | 1 + src/axolotl/utils/config/__init__.py | 2 +- src/axolotl/utils/data/__init__.py | 8 +- src/axolotl/utils/data/sft.py | 2 +- src/axolotl/utils/environment.py | 2 - src/axolotl/utils/schemas/config.py | 6 + src/axolotl/utils/schemas/validation.py | 1 - tests/e2e/test_diffusion.py | 139 ++++++ tests/integrations/test_diffusion.py | 274 ++++++++++++ tests/integrations/test_diffusion_callback.py | 92 ++++ tests/test_streaming.py | 4 +- 34 files changed, 2550 insertions(+), 101 deletions(-) create mode 100644 examples/llama-3/diffusion/pretrain-1b.yaml create mode 100644 examples/llama-3/diffusion/sft-1b.yaml create mode 100644 src/axolotl/cli/utils/diffusion.py create mode 100644 src/axolotl/integrations/diffusion/README.md create mode 100644 src/axolotl/integrations/diffusion/__init__.py create mode 100644 src/axolotl/integrations/diffusion/args.py create mode 100644 src/axolotl/integrations/diffusion/callbacks.py create mode 100644 src/axolotl/integrations/diffusion/generation.py create mode 100644 src/axolotl/integrations/diffusion/plugin.py create mode 100644 src/axolotl/integrations/diffusion/trainer.py create mode 100644 src/axolotl/integrations/diffusion/utils.py create mode 100644 tests/e2e/test_diffusion.py create mode 100644 tests/integrations/test_diffusion.py create mode 100644 tests/integrations/test_diffusion_callback.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 92ddc7f41e..9c80898ff4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: rev: v0.12.12 hooks: - id: ruff - args: [--fix] + args: [--fix, --select, I] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.17.1 diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index b780a1c48d..0e6ba984ef 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -176,8 +176,8 @@ } ], "source": [ - "from axolotl.utils.dict import DictDefault\n", "from axolotl.cli.config import load_cfg\n", + "from axolotl.utils.dict import DictDefault\n", "\n", "# Axolotl provides full control and transparency over model and training configuration\n", "config = DictDefault(\n", diff --git a/examples/llama-3/diffusion/pretrain-1b.yaml b/examples/llama-3/diffusion/pretrain-1b.yaml new file mode 100644 index 0000000000..8d05e4c60f --- /dev/null +++ b/examples/llama-3/diffusion/pretrain-1b.yaml @@ -0,0 +1,56 @@ +base_model: meta-llama/Llama-3.2-1B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +pretraining_dataset: + - path: wikitext + name: wikitext-103-raw-v1 + type: completion + field: text + +plugins: + - axolotl.integrations.diffusion.DiffusionPlugin + +diffusion: + noise_schedule: cosine + min_mask_ratio: 0.15 + max_mask_ratio: 0.85 + num_diffusion_steps: 128 + eps: 5e-4 + importance_weighting: true + mask_token_id: 128002 + generate_samples: true + generation_interval: 250 + +output_dir: ./outputs/model-out + +sequence_len: 512 +sample_packing: true + +gradient_accumulation_steps: 8 +micro_batch_size: 4 +max_steps: 10000 +warmup_ratio: 0.1 + +optimizer: adamw_8bit +lr_scheduler: cosine +learning_rate: 3e-4 +sdp_attention: true + +bf16: auto +tf32: true + +logging_steps: 1 +save_strategy: steps +save_steps: 1000 + +special_tokens: + pad_token: "<|end_of_text|>" + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/diffusion/sft-1b.yaml b/examples/llama-3/diffusion/sft-1b.yaml new file mode 100644 index 0000000000..f3b29a8095 --- /dev/null +++ b/examples/llama-3/diffusion/sft-1b.yaml @@ -0,0 +1,59 @@ +base_model: meta-llama/Llama-3.2-1B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +datasets: + - path: teknium/GPT4-LLM-Cleaned + type: alpaca +val_set_size: 0.05 + +plugins: + - axolotl.integrations.diffusion.DiffusionPlugin + +diffusion: + noise_schedule: cosine + min_mask_ratio: 0.1 + max_mask_ratio: 0.9 + num_diffusion_steps: 128 + eps: 1e-3 + importance_weighting: true + mask_token_id: 128002 + generate_samples: true + generation_interval: 250 + +output_dir: ./outputs/model-out + +sequence_len: 512 +sample_packing: true +eval_sample_packing: true + +gradient_accumulation_steps: 4 +micro_batch_size: 4 +num_epochs: 1 +warmup_steps: 0.1 + +optimizer: adamw_8bit +lr_scheduler: cosine +learning_rate: 1e-5 + +bf16: auto +tf32: true + +gradient_checkpointing: true +resume_from_checkpoint: +sdp_attention: true + +logging_steps: 1 +save_strategy: best +eval_strategy: epoch + +special_tokens: + pad_token: "<|end_of_text|>" + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index debe571672..30d4077134 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -14,6 +14,13 @@ from axolotl.cli.args import InferenceCliArgs from axolotl.cli.config import load_cfg from axolotl.cli.utils import load_model_and_tokenizer +from axolotl.cli.utils.diffusion import ( + diffusion_inference, + launch_diffusion_gradio_ui, + render_html, + run_diffusion, +) +from axolotl.integrations.base import PluginManager from axolotl.utils.chat_templates import get_chat_template_from_config from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger @@ -29,6 +36,7 @@ def get_multi_line_input() -> str: Possibly multi-line, possibly empty stdin input as a string. """ print("Give me an instruction (Ctrl + D to submit): ") + print("=" * 80) instruction = "" for line in sys.stdin: @@ -43,9 +51,9 @@ def do_inference( cli_args: InferenceCliArgs, ): """ - Runs inference on the command line in a loop. User input is accepted, a chat template - is (optionally) applied, and the model specified in the `axolotl` config is used to - generate completions according to a default generation config. + Runs inference on the command line in a loop. User input is accepted, a chat + template is (optionally) applied, and the model specified in the `axolotl` config is + used to generate completions according to a default generation config. Args: cfg: Dictionary mapping `axolotl` config keys to values. @@ -64,16 +72,28 @@ def do_inference( chat_template_str = get_chat_template_from_config( cfg, ds_cfg=None, tokenizer=tokenizer ) - elif cfg.datasets[0].type == "chat_template": + elif cfg.datasets and cfg.datasets[0].type == "chat_template": chat_template_str = get_chat_template_from_config( cfg=cfg, ds_cfg=cfg.datasets[0], tokenizer=tokenizer ) model = model.to(cfg.device, dtype=cfg.torch_dtype) + # Detect diffusion mode + plugin_manager = PluginManager.get_instance() + is_diffusion = any( + plugin.__class__.__name__ == "DiffusionPlugin" + for plugin in plugin_manager.plugins.values() + ) + + if is_diffusion: + print("=" * 80) + print("Commands:") + print(":complete N -> completion mode with N tokens (default 64)") + print(":mask R -> random masking with ratio R (0.0–1.0)") + while True: print("=" * 80) - # support for multiline inputs instruction = get_multi_line_input() if not instruction: return @@ -103,9 +123,19 @@ def do_inference( else: batch = tokenizer(prompt, return_tensors="pt", add_special_tokens=True) - print("=" * 40) + print("=" * 80) model.eval() with torch.no_grad(): + if is_diffusion: + diffusion_inference( + model=model, + tokenizer=tokenizer, + cfg=cfg, + prompt=prompt, + chat_template_str=chat_template_str, + ) + continue + generation_config = GenerationConfig( repetition_penalty=1.1, max_new_tokens=1024, @@ -128,7 +158,7 @@ def do_inference( generation_config=generation_config, streamer=streamer, ) - print("=" * 40) + print("=" * 80) print(tokenizer.decode(generated["sequences"].cpu().tolist()[0])) @@ -161,13 +191,30 @@ def do_inference_gradio( chat_template_str = get_chat_template_from_config( cfg, ds_cfg=None, tokenizer=tokenizer ) - elif cfg.datasets[0].type == "chat_template": + elif cfg.datasets and cfg.datasets[0].type == "chat_template": chat_template_str = get_chat_template_from_config( cfg=cfg, ds_cfg=cfg.datasets[0], tokenizer=tokenizer ) model = model.to(cfg.device, dtype=cfg.torch_dtype) + # Detect diffusion mode + plugin_manager = PluginManager.get_instance() + is_diffusion = any( + plugin.__class__.__name__ == "DiffusionPlugin" + for plugin in plugin_manager.plugins.values() + ) + + if is_diffusion: + launch_diffusion_gradio_ui( + model=model, + tokenizer=tokenizer, + cfg=cfg, + prompter_module=prompter_module, + chat_template_str=chat_template_str, + ) + return + def generate(instruction): if not instruction: return diff --git a/src/axolotl/cli/utils/diffusion.py b/src/axolotl/cli/utils/diffusion.py new file mode 100644 index 0000000000..f83d9077bb --- /dev/null +++ b/src/axolotl/cli/utils/diffusion.py @@ -0,0 +1,375 @@ +"""Helpers for diffusion-mode inference in CLI and Gradio.""" + +from __future__ import annotations + +import gradio as gr +import torch +from colorama import Fore, Style + +from axolotl.integrations.diffusion import generate, resolve_mask_token_id +from axolotl.utils.dict import DictDefault + + +def diffusion_inference( + model, + tokenizer, + cfg, + prompt: str, + chat_template_str: str | None = None, +): + """Diffusion inference helper method.""" + mode = "random" + completion_tokens = 0 + target_mask_ratio = None + mode, completion_tokens, target_mask_ratio, cleaned = _parse_commands(prompt) + + if cleaned: + prompt = cleaned + + info = run_diffusion( + model=model, + tokenizer=tokenizer, + cfg=cfg, + prompt=prompt, + chat_template_str=chat_template_str, + mode=mode, + target_mask_ratio=target_mask_ratio, + completion_tokens=completion_tokens, + ) + masked_text = info["masked_text"] + mask_ratio = info["mask_ratio"] + generated_ids = info["generated_ids"] + masked_positions = info["masked_positions"] + orig_ids = info["orig_ids"] + + # Display with masked preview and colored diff + if masked_text is not None and mask_ratio is not None: + print(f"Masked ({mask_ratio:.1%}):\n{masked_text}\n") + if generated_ids is not None: + # Compute per-token style + styles: list[str] = [] + for i, tid in enumerate(generated_ids): + if i in masked_positions: + if i < len(orig_ids) and tid == orig_ids[i]: + styles.append("green") # correct fill + elif i < len(orig_ids): + styles.append("red") # incorrect fill + else: + styles.append("normal") # appended + else: + same = i < len(orig_ids) and tid == orig_ids[i] + styles.append("dim" if same else "normal") + + # Group contiguous spans by style + styled_spans: list[tuple[str, int, int]] = [] + if generated_ids: + current_style = styles[0] + start = 0 + for i in range(1, len(generated_ids)): + s = styles[i] + if s != current_style: + styled_spans.append((current_style, start, i)) + current_style, start = s, i + styled_spans.append((current_style, start, len(generated_ids))) + + out_parts = [] + for style_name, a, b in styled_spans: + chunk_text = tokenizer.decode(generated_ids[a:b], skip_special_tokens=False) + if style_name == "green": + out_parts.append(Fore.GREEN + chunk_text + Style.RESET_ALL) + elif style_name == "red": + out_parts.append(Fore.RED + chunk_text + Style.RESET_ALL) + else: + if style_name == "dim": + out_parts.append(Style.DIM + chunk_text + Style.RESET_ALL) + else: + out_parts.append(chunk_text) + print("Generated:\n" + "".join(out_parts)) + else: + print("Generated:\n(no output)") + + +def _parse_commands(text: str): + """ + Parse leading diffusion commands. + + Supported at start of input (can be chained): + :complete N -> completion mode with N tokens (default 64) + :mask R -> random masking with ratio R in [0, 1] + """ + tokens = text.strip().split() + i = 0 + mode = "random" + completion_tokens = 0 + target_mask_ratio = None + consumed = 0 + while i < len(tokens) and tokens[i].startswith(":"): + cmd = tokens[i] + i += 1 + consumed = i + if cmd == ":complete": + mode = "completion" + if i < len(tokens): + try: + completion_tokens = int(tokens[i]) + i += 1 + consumed = i + except Exception: + completion_tokens = 64 + else: + completion_tokens = 64 + elif cmd == ":mask": + mode = "random" + if i < len(tokens): + try: + target_mask_ratio = float(tokens[i]) + i += 1 + consumed = i + except Exception: + target_mask_ratio = None + else: + i -= 1 + consumed = i + break + + cleaned = " ".join(tokens[consumed:]) + + return mode, completion_tokens, target_mask_ratio, cleaned + + +def run_diffusion( + *, + model, + tokenizer, + cfg: DictDefault, + prompt: str, + chat_template_str: str | None, + mode: str = "random", + target_mask_ratio: float | None = None, + completion_tokens: int = 0, +): + """Run a single diffusion generation and return a structured result dict.""" + if chat_template_str: + batch = tokenizer.apply_chat_template( + [{"role": "user", "content": prompt}], + return_tensors="pt", + add_special_tokens=True, + add_generation_prompt=True, + chat_template=chat_template_str, + tokenize=True, + return_dict=True, + ) + else: + batch = tokenizer(prompt, return_tensors="pt", add_special_tokens=True) + + mask_token_id = resolve_mask_token_id(tokenizer, cfg, allow_add=False) + + seq = batch["input_ids"].to(cfg.device) + gen_mode = "completion" if mode == "completion" else "random" + comp_tokens = int(completion_tokens) if gen_mode == "completion" else 0 + + result = generate( + model, + tokenizer, + original_sequence=seq[:1], + num_diffusion_steps=cfg.diffusion.num_diffusion_steps, + temperature=cfg.diffusion.generation_temperature, + mask_token_id=int(mask_token_id), + mode=gen_mode, # type: ignore[arg-type] + completion_tokens=comp_tokens, + target_mask_ratio=target_mask_ratio, + ) + + masked_text = result.get("masked") if isinstance(result, dict) else None + mask_ratio = result.get("mask_ratio") if isinstance(result, dict) else None + generated_ids = result.get("generated_ids") if isinstance(result, dict) else None + masked_positions = ( + set(result.get("masked_positions") or []) if isinstance(result, dict) else set() + ) + orig_ids = seq[0].detach().cpu().tolist() + + return { + "masked_text": masked_text, + "mask_ratio": mask_ratio, + "generated_ids": generated_ids, + "masked_positions": masked_positions, + "orig_ids": orig_ids, + } + + +def render_html( + *, + generated_ids: list[int] | None, + orig_ids: list[int], + masked_positions: set[int], + tokenizer, +) -> str: + """Render HTML visualizing diffusion outputs.""" + if not generated_ids: + return "
Generated:\n(no output)
" + + def _style_for(i: int, tid: int) -> str: + if i in masked_positions: + if i < len(orig_ids) and tid == orig_ids[i]: + return "green" + if i < len(orig_ids): + return "red" + return "normal" + same = i < len(orig_ids) and tid == orig_ids[i] + return "dim" if same else "normal" + + # Group contiguous spans by style to reduce HTML size + spans: list[tuple[str, int, int]] = [] + if generated_ids: + cur = _style_for(0, generated_ids[0]) + start = 0 + for i in range(1, len(generated_ids)): + s = _style_for(i, generated_ids[i]) + if s != cur: + spans.append((cur, start, i)) + cur, start = s, i + spans.append((cur, start, len(generated_ids))) + + html_parts = [] + for style_name, a, b in spans: + txt = tokenizer.decode(generated_ids[a:b], skip_special_tokens=False) + if style_name == "green": + html_parts.append(f'{txt}') + elif style_name == "red": + html_parts.append(f'{txt}') + elif style_name == "dim": + html_parts.append(f'{txt}') + else: + html_parts.append(txt) + + legend = ( + '
' + 'correct, ' + 'incorrect, ' + 'unchanged' + "
" + ) + + return ( + legend + + '
Generated:\n'
+        + "".join(html_parts)
+        + "
" + ) + + +def launch_diffusion_gradio_ui( + *, + model, + tokenizer, + cfg: DictDefault, + prompter_module=None, + chat_template_str: str | None = None, +): + """Build and launch a simple Gradio UI for diffusion inference.""" + with gr.Blocks( + title=cfg.get("gradio_title", "Axolotl Diffusion Interface") + ) as demo: + gr.Markdown( + """ + ## Axolotl Diffusion Inference + - Mode "Random" masks tokens at a target ratio and fills them. + - Mode "Completion" appends N masked tokens at the end and fills them. + """ + ) + + with gr.Row(): + mode = gr.Radio( + choices=["random", "completion"], + value="random", + label="Mode", + ) + mask_ratio = gr.Slider( + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.4, + label="Mask ratio (random mode)", + interactive=True, + ) + completion_tokens = gr.Number( + value=64, + precision=0, + label="Completion tokens (completion mode)", + interactive=True, + visible=False, + ) + + instruction = gr.Textbox(label="Instruction", lines=6) + run_btn = gr.Button("Generate") + + masked_preview = gr.Textbox(label="Masked preview", lines=6) + html_out = gr.HTML(label="Generated") + + def _toggle_controls(selected_mode: str): + return ( + gr.update(visible=(selected_mode == "random")), + gr.update(visible=(selected_mode == "completion")), + ) + + mode.change( + _toggle_controls, + inputs=[mode], + outputs=[mask_ratio, completion_tokens], + ) + + def _gen(instruction_text: str, selected_mode: str, mratio: float, ctoks: int): + if not instruction_text: + return "", "
Generated:\n(no output)
" + + if prompter_module: + prompt: str = next( + prompter_module().build_prompt( + instruction=instruction_text.strip("\n") + ) + ) + else: + prompt = instruction_text.strip() + + info = run_diffusion( + model=model, + tokenizer=tokenizer, + cfg=cfg, + prompt=prompt, + chat_template_str=chat_template_str, + mode=selected_mode, + target_mask_ratio=mratio if selected_mode == "random" else None, + completion_tokens=int(ctoks) if selected_mode == "completion" else 0, + ) + + masked_text = info.get("masked_text") + mask_ratio_val = info.get("mask_ratio") + generated_ids = info.get("generated_ids") + masked_positions = info.get("masked_positions") or set() + orig_ids = info.get("orig_ids") or [] + + preview = ( + f"Masked ({mask_ratio_val:.1%}):\n{masked_text}" + if masked_text is not None and mask_ratio_val is not None + else "" + ) + html = render_html( + generated_ids=generated_ids, + orig_ids=orig_ids, + masked_positions=masked_positions, + tokenizer=tokenizer, + ) + return preview, html + + run_btn.click( + _gen, + inputs=[instruction, mode, mask_ratio, completion_tokens], + outputs=[masked_preview, html_out], + ) + + demo.queue().launch( + show_api=False, + share=cfg.get("gradio_share", True), + server_name=cfg.get("gradio_server_name", "127.0.0.1"), + server_port=cfg.get("gradio_server_port", None), + ) diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index ee6383d473..f7f350e1a0 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -7,7 +7,11 @@ from typing import Type, Union import transformers -from transformers import DataCollatorWithFlattening, EarlyStoppingCallback +from transformers import ( + DataCollatorWithFlattening, + EarlyStoppingCallback, + Trainer, +) from trl.trainer.utils import RewardDataCollatorWithPadding from axolotl.core.builders.base import TrainerBuilderBase @@ -23,15 +27,16 @@ from axolotl.processing_strategies import get_processing_strategy from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.callbacks import ( + LossWatchDogCallback, + SaveBetterTransformerModelCallback, bench_eval_callback_factory, causal_lm_bench_eval_callback_factory, colab_inference_post_train_callback, log_prediction_callback_factory, - LossWatchDogCallback, - SaveBetterTransformerModelCallback, ) from axolotl.utils.callbacks.lisa import lisa_callback_factory from axolotl.utils.callbacks.qat import QATCallback +from axolotl.utils.callbacks.tokens_per_second import TokensPerSecondCallback from axolotl.utils.chat_templates import get_chat_template_from_config from axolotl.utils.collators import ( BatchSamplerDataCollatorForSeq2Seq, @@ -39,7 +44,6 @@ MambaDataCollator, V2BatchSamplerDataCollatorForSeq2Seq, ) -from axolotl.utils.callbacks.tokens_per_second import TokensPerSecondCallback from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator from axolotl.utils.import_helper import get_cls_from_module_str from axolotl.utils.logging import get_logger @@ -391,10 +395,11 @@ def build(self, total_num_steps): **data_collator_kwargs, ) sig = inspect.signature(trainer_cls) - if "processing_class" in sig.parameters: + if "processing_class" in sig.parameters or issubclass(trainer_cls, Trainer): trainer_kwargs["processing_class"] = self.tokenizer elif "tokenizer" in sig.parameters: trainer_kwargs["tokenizer"] = self.tokenizer + if ( trainer_cls not in [AxolotlRewardTrainer, AxolotlPRMTrainer] and self.cfg.datasets is not None diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index d7555261f1..3427a0b86f 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -49,6 +49,13 @@ LOG = get_logger(__name__) +REDUCTION_FNS = { + "mean": torch.mean, + "min": torch.min, + "max": torch.max, + "sum": torch.sum, +} + class AxolotlTrainer( PackingMixin, @@ -89,7 +96,9 @@ def __init__( super().__init__(*_args, **kwargs) self.train_data_collator = self.data_collator - self._stored_metrics = defaultdict(lambda: defaultdict(list)) + self._stored_metrics = defaultdict( + lambda: defaultdict(lambda: {"values": [], "reduction": "mean"}) + ) if self.args.orpo_alpha: self.loss_fct = torch.nn.CrossEntropyLoss(reduction="none") @@ -585,9 +594,17 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: """ # logs either has 'loss' or 'eval_loss' train_eval = "train" if "loss" in logs else "eval" - # Add averaged stored metrics to logs - for key, metrics in self._stored_metrics[train_eval].items(): - logs[key] = torch.tensor(metrics).mean().item() + + for key, metric_data in self._stored_metrics[train_eval].items(): + values = torch.tensor(metric_data["values"]) # type: ignore[arg-type] + reduction_type = metric_data["reduction"] + + fn = REDUCTION_FNS.get(reduction_type) + if fn is None: + raise NotImplementedError( + "Metric reduction must be one of [mean, min, max, sum]" + ) + logs[key] = round(fn(values).item(), 4) if is_main_process(): # Add memory usage @@ -611,10 +628,27 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: return super().log(logs, start_time) def store_metrics( - self, metrics: dict[str, float], train_eval: Literal["train", "eval"] = "train" + self, + metrics: dict[str, float] | dict[str, tuple[int | float, str]], + train_eval: Literal["train", "eval"] = "train", + reduction: Literal["mean", "min", "max", "sum"] = "mean", ) -> None: + """ + Store metrics with specified reduction type. + + Args: + metrics: Dictionary of metric names to values, or metric names to (value, + reduction_type) tuples. + train_eval: Whether this is for training or evaluation. + """ for key, value in metrics.items(): - self._stored_metrics[train_eval][key].append(value) + if isinstance(value, tuple): + value, _reduction = value # type: ignore[assignment] + else: + value, _reduction = value, reduction + + self._stored_metrics[train_eval][key]["values"].append(value) + self._stored_metrics[train_eval][key]["reduction"] = _reduction def _save_checkpoint(self, model, trial, **kwargs): # make sure the checkpoint dir exists, since trainer is flakey diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 8edee18a3d..c66bc01c63 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -142,7 +142,7 @@ def post_model_load(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): model: The loaded model. """ - def get_trainer_cls(self, cfg: DictDefault) -> Trainer | None: + def get_trainer_cls(self, cfg: DictDefault) -> type[Trainer] | None: """Returns a custom class for the trainer. Args: diff --git a/src/axolotl/integrations/config.py b/src/axolotl/integrations/config.py index 2217b28195..8ae8aab39b 100644 --- a/src/axolotl/integrations/config.py +++ b/src/axolotl/integrations/config.py @@ -20,8 +20,8 @@ from axolotl.utils.schemas.config import ( AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, + AxolotlInputConfig as AxolotlInputConfigBase, ) -from axolotl.utils.schemas.config import AxolotlInputConfig as AxolotlInputConfigBase def merge_input_args(): diff --git a/src/axolotl/integrations/diffusion/README.md b/src/axolotl/integrations/diffusion/README.md new file mode 100644 index 0000000000..c27f33de15 --- /dev/null +++ b/src/axolotl/integrations/diffusion/README.md @@ -0,0 +1,154 @@ +# Diffusion LM Training Plugin for Axolotl + +This plugin enables diffusion language model training using an approach inspired by +LLaDA (Large Language Diffusion Models) within Axolotl. + +## Overview + +LLaDA is a diffusion-based approach to language model training that uses: +- **Random token masking** during training instead of next-token prediction +- **Bidirectional attention** to allow the model to attend to the full context +- **Importance weighting** based on masking probabilities for stable training + +This approach can lead to more robust language models with better understanding of +bidirectional context. + +## Installation + +The plugin is included with Axolotl. See our +[installation docs](https://docs.axolotl.ai/docs/installation.html). + +## Quickstart + +Train with an example config (Llama‑3.2 1B): + - Pretrain: `axolotl train examples/llama-3/diffusion-3.2-1b-pretrain.yaml` + - SFT: `axolotl train examples/llama-3/diffusion-3.2-1b-sft.yaml` + +### Basic Configuration + +You can also modify your existing configs to enable / customize diffusion training. + +Add the following to your Axolotl config: + +```yaml +# Enable diffusion LM training plugin +plugins: + - axolotl.integrations.diffusion.DiffusionPlugin +``` + +And, configure the nested `diffusion` block (defaults shown): + +```yaml +diffusion: + noise_schedule: linear # or "cosine" + min_mask_ratio: 0.1 + max_mask_ratio: 0.9 + num_diffusion_steps: 128 + eps: 1e-3 + importance_weighting: true + + # Mask token (training auto-adds if missing, avoid pad/eos) + mask_token_str: "<|diffusion_mask|>" + # Or use an existing special token id (e.g., 128002 for Llama-3.x) + # mask_token_id: 128002 + + # Sample generation during training (optional) + generate_samples: true + generation_interval: 100 + num_generation_samples: 3 + generation_steps: 128 + generation_temperature: 0.0 + generation_max_length: 100 +``` + +## Supported Models + +Any models that support 4D attention masks should work out of the box. If not, please +create an [issue](https://github.com/axolotl-ai-cloud/axolotl/issues) or open a +[PR](https://github.com/axolotl-ai-cloud/axolotl/compare)! + +## How It Works + +### Random Masking +During training, tokens are randomly masked: +- Sample timestep `t` uniformly from [0, 1] +- Calculate masking probability: `p = (1 - eps) * t + eps` +- Randomly mask tokens with probability `p` + +### Diffusion Loss + +Loss is computed only on masked tokens with (optional) importance weighting: + +```python +loss = sum(cross_entropy(pred, target) / p_mask) / total_tokens +``` + +## Sample Generation + +When `diffusion.generate_samples: true`, the plugin generates samples during training: + +``` +Sample 1: + Original (45 tokens): The quick brown fox jumps over the lazy dog... + Masked (18/45 tokens, 40.0%): The [MASK] [MASK] fox [MASK] over [MASK] lazy [MASK]... + Generated: The quick brown fox jumps over the lazy dog... +``` + +Samples are logged to console and wandb (if enabled). + +## Inference + +Diffusion inference is integrated into the standard Axolotl CLI. Use the same config +you trained with and run: + +``` +axolotl inference path/to/your-config.yaml +``` + +Optionally, pass `--gradio` to use a simple web interface. + +Interactive controls (prefix the prompt with commands): +- `:complete N` → completion mode with N new masked tokens appended (default 64) +- `:mask R` → random masking mode with target mask ratio R in [0.0, 1.0] + +Example session: + +``` +================================================================================ +Commands: +:complete N -> completion mode with N tokens (default 64) +:mask R -> random masking with ratio R (0.0–1.0) +================================================================================ +Give me an instruction (Ctrl + D to submit): + +:mask 0.4 The quick brown fox jumps over the lazy dog + +Masked (40.0%): +The [MASK] brown [MASK] jumps over the [MASK] dog + +Generated: +The quick brown fox jumps over the loud dog +``` + +## Metrics and Monitoring + +The plugin adds (or modifies) several metrics to track diffusion training: + +- `train/loss`: Weighted diffusion loss +- `train/accuracy`: Accuracy on masked tokens +- `train/mask_ratio`: Average fraction of tokens masked +- `train/num_masked_tokens`: Number of tokens masked +- `train/avg_p_mask`: Average masking probability +- `train/ce_loss`: Unweighted cross-entropy loss +- `train/importance_weight_avg`: Average importance weight + +## Limitations + +- No flash attention support +- No RL training support + +## References + +- [LLaDA Paper](https://arxiv.org/abs/2404.10406) +- [Axolotl Documentation](https://docs.axolotl.ai/) +- [API reference for plugin](https://docs.axolotl.ai/docs/api/integrations.diffusion.args.html#axolotl.integrations.diffusion.args) diff --git a/src/axolotl/integrations/diffusion/__init__.py b/src/axolotl/integrations/diffusion/__init__.py new file mode 100644 index 0000000000..9e38cc5c1d --- /dev/null +++ b/src/axolotl/integrations/diffusion/__init__.py @@ -0,0 +1,19 @@ +"""Diffusion LM training plugin init.""" + +from .args import DiffusionArgs, DiffusionConfig +from .callbacks import DiffusionGenerationCallback +from .generation import generate +from .plugin import DiffusionPlugin +from .trainer import DiffusionTrainer +from .utils import create_bidirectional_attention_mask, resolve_mask_token_id + +__all__ = [ + "DiffusionArgs", + "DiffusionPlugin", + "DiffusionTrainer", + "generate", + "resolve_mask_token_id", + "create_bidirectional_attention_mask", + "DiffusionGenerationCallback", + "DiffusionConfig", +] diff --git a/src/axolotl/integrations/diffusion/args.py b/src/axolotl/integrations/diffusion/args.py new file mode 100644 index 0000000000..4f5bfe499f --- /dev/null +++ b/src/axolotl/integrations/diffusion/args.py @@ -0,0 +1,95 @@ +"""Config args for diffusion LM training (nested under `diffusion:`).""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field, model_validator + + +class DiffusionConfig(BaseModel): + """Nested diffusion configuration available under the `diffusion` key.""" + + # Noise schedule config + noise_schedule: Literal["linear", "cosine"] = Field( + default="linear", description="Type of noise schedule for diffusion training" + ) + min_mask_ratio: float = Field( + default=0.1, + ge=0.0, + le=1.0, + description="Minimum masking ratio for diffusion noise schedule", + ) + max_mask_ratio: float = Field( + default=0.9, + ge=0.0, + le=1.0, + description="Maximum masking ratio for diffusion noise schedule", + ) + num_diffusion_steps: int = Field( + default=128, ge=1, description="Number of diffusion timesteps" + ) + eps: float = Field( + default=1e-3, + ge=0.0, + le=1.0, + description="Epsilon value for minimum masking probability in forward process", + ) + + # Training config + importance_weighting: bool = Field( + default=True, + description="Apply importance weighting to loss based on masking probability", + ) + mask_token_id: int | None = Field( + default=None, + description=( + "Token ID to use for masking. Unset by default; can use one of the " + "tokenizer's special tokens here." + ), + ) + mask_token_str: str | None = Field( + default=None, + description=( + "Token string to use as a mask. If `mask_token_id` is invalid or unset, " + "this token will be ensured to exist as an additional special token and " + "used. If absent, a default '<|diffusion_mask|>' will be added." + ), + ) + + # Sample generation config + generate_samples: bool = Field( + default=True, description="Enable sample generation during training" + ) + generation_interval: int = Field( + default=100, ge=1, description="Generate samples every N steps" + ) + num_generation_samples: int = Field( + default=3, ge=1, description="Number of samples to generate each time" + ) + generation_steps: int = Field( + default=128, ge=1, description="Number of diffusion steps for generation" + ) + generation_temperature: float = Field( + default=0.0, + ge=0.0, + description="Temperature for generation sampling (0.0 = deterministic)", + ) + generation_max_length: int = Field( + default=100, ge=1, description="Maximum sequence length for generation" + ) + + @model_validator(mode="after") + def _validate_mask_ratios(self) -> "DiffusionConfig": + if self.min_mask_ratio > self.max_mask_ratio: + raise ValueError("min_mask_ratio must be ≤ max_mask_ratio") + return self + + +class DiffusionArgs(BaseModel): + """Plugin entry that exposes the nested `diffusion` block to the core config.""" + + diffusion: DiffusionConfig = Field( + default_factory=DiffusionConfig, + description="Diffusion training configuration. Only nested block is supported.", + ) diff --git a/src/axolotl/integrations/diffusion/callbacks.py b/src/axolotl/integrations/diffusion/callbacks.py new file mode 100644 index 0000000000..18a64023b6 --- /dev/null +++ b/src/axolotl/integrations/diffusion/callbacks.py @@ -0,0 +1,174 @@ +"""Callbacks for diffusion training.""" + +import logging +import sys + +import wandb +from colorama import Fore, Style +from transformers.trainer_callback import TrainerCallback, TrainerControl, TrainerState +from transformers.training_args import TrainingArguments + +from .generation import generate_samples + +# Simpler logger for more readable sample generation +logger = logging.getLogger(__name__) +if not logger.handlers: + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(logging.Formatter("%(message)s")) + logger.addHandler(handler) + logger.propagate = False +logger.setLevel(logging.INFO) + + +class DiffusionGenerationCallback(TrainerCallback): + """Callback for generating samples during diffusion training.""" + + def __init__(self, trainer): + self.trainer = trainer + + def on_step_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Generate samples at specified intervals.""" + if ( + state.global_step > 0 + and state.global_step % self.trainer.cfg.diffusion.generation_interval == 0 + ): + if not self.trainer.state.is_world_process_zero: + return + + # Use eval dataloader if available, otherwise use train dataloader + dataloader = None + try: + if getattr(self.trainer, "eval_dataset", None) is not None: + dataloader = self.trainer.get_eval_dataloader() + except Exception: + dataloader = None + if dataloader is None: + dataloader = self.trainer.get_train_dataloader() + + # Generate samples + diffusion_cfg = self.trainer.cfg.diffusion + samples = generate_samples( + model=self.trainer.model, + tokenizer=self.trainer.processing_class, + dataloader=dataloader, + num_generation_samples=diffusion_cfg.num_generation_samples, + max_length=diffusion_cfg.generation_max_length, + num_diffusion_steps=diffusion_cfg.generation_steps, + temperature=diffusion_cfg.generation_temperature, + mask_token_id=diffusion_cfg.mask_token_id, + ) + + # Log samples + self._log_samples(samples, state.global_step) + + def _log_samples(self, samples: list, step: int): + """Log generated samples.""" + if not samples: + return + + logger.info("=" * 60) + logger.info("GENERATED SAMPLES") + logger.info("=" * 60) + + for i, sample_data in enumerate(samples, 1): + original = sample_data["original"] + masked = sample_data["masked"] + generated = sample_data["generated"] + mask_ratio = sample_data["mask_ratio"] + masked_tokens = sample_data["masked_tokens"] + total_tokens = sample_data["total_tokens"] + + logger.info(f"\nSample {i}:") + logger.info(f"\tOriginal ({total_tokens} tokens): {original}") + logger.info( + f"\tMasked ({masked_tokens}/{total_tokens} tokens, " + f"{mask_ratio:.1%}): {masked}" + ) + + try: + gen_ids = sample_data.get("generated_ids") + orig_ids = sample_data.get("orig_ids") + masked_positions = set(sample_data.get("masked_positions") or []) + if isinstance(gen_ids, list) and isinstance(orig_ids, list): + styles: list[str] = [] + for i, tid in enumerate(gen_ids): + if i in masked_positions: + if i < len(orig_ids) and tid == orig_ids[i]: + styles.append("green") + elif i < len(orig_ids): + styles.append("red") + else: + styles.append("normal") + else: + same = i < len(orig_ids) and tid == orig_ids[i] + styles.append("dim" if same else "normal") + + spans: list[tuple[str, int, int]] = [] + if gen_ids: + cur = styles[0] + start = 0 + for i in range(1, len(gen_ids)): + s = styles[i] + if s != cur: + spans.append((cur, start, i)) + cur, start = s, i + spans.append((cur, start, len(gen_ids))) + + parts = [] + for style_name, a, b in spans: + chunk_text = self.trainer.processing_class.decode( + gen_ids[a:b], skip_special_tokens=False + ) + if style_name == "green": + parts.append(Fore.GREEN + chunk_text + Style.RESET_ALL) + elif style_name == "red": + parts.append(Fore.RED + chunk_text + Style.RESET_ALL) + else: + if style_name == "dim": + parts.append(Style.DIM + chunk_text + Style.RESET_ALL) + else: + parts.append(chunk_text) + logger.info("\tGenerated:\n%s", "".join(parts)) + else: + logger.info(f"\tGenerated: {generated}") + except Exception: + logger.info(f"\tGenerated: {generated}") + + logger.info("=" * 60) + + if self.trainer.cfg.use_wandb: + if wandb.run is not None: + wandb.log( + { + "generated_samples": wandb.Table( + columns=[ + "step", + "original", + "masked", + "generated", + "mask_ratio", + "masked_tokens", + "total_tokens", + ], + data=[ + [ + step, + sample["original"], + sample["masked"], + sample["generated"], + f"{sample['mask_ratio']:.1%}", + sample["masked_tokens"], + sample["total_tokens"], + ] + for sample in samples + ], + ) + }, + step=step, + ) diff --git a/src/axolotl/integrations/diffusion/generation.py b/src/axolotl/integrations/diffusion/generation.py new file mode 100644 index 0000000000..49e3cdfae8 --- /dev/null +++ b/src/axolotl/integrations/diffusion/generation.py @@ -0,0 +1,409 @@ +"""Sample generation utilities for diffusion training.""" + +import re +from typing import Any, List, Literal, Optional + +import torch + +from axolotl.utils.logging import get_logger + +from .utils import create_bidirectional_attention_mask + +LOG = get_logger(__name__) + + +def generate_samples( + model: torch.nn.Module, + tokenizer: Any, + dataloader: Optional[Any] = None, + num_generation_samples: int = 3, + max_length: int = 100, + num_diffusion_steps: int = 128, + temperature: float = 0.0, + mask_token_id: int = 32000, + mode: Literal["random", "completion"] = "random", + completion_tokens: int = 0, + target_mask_ratio: Optional[float] = None, +) -> List[dict]: + """ + Generate text samples using the diffusion model by randomly masking sequences from + the given dataset and running the reverse diffusion process. + + Args: + model: The wrapped or unwrapped model + tokenizer: Tokenizer for encoding/decoding + dataloader: Validation dataloader (for sampling sequences) + num_generation_samples: Number of samples to generate + max_length: Maximum length of sequences to use + num_diffusion_steps: Number of diffusion steps for generation + temperature: Temperature for sampling (0.0 = deterministic) + mask_token_id: Token ID used for masking + + Returns: + List of dictionaries with original text, masked text, and generated text + """ + if dataloader is None: + LOG.warning("No validation dataloader provided, cannot generate samples") + return [] + + unwrapped_model = model.module if hasattr(model, "module") else model + training = unwrapped_model.training + unwrapped_model.eval() + + # Resolve device robustly (some modules don't expose `.device`) + device = getattr(unwrapped_model, "device", None) + if device is None: + try: + device = next(unwrapped_model.parameters()).device + except StopIteration: + device = torch.device("cpu") + generations = [] + + # Sample sequences from validation dataset + sampled_sequences = _sample_sequences_from_dataloader( + dataloader, num_generation_samples, max_length, device + ) + LOG.info(f"Sampled {len(sampled_sequences)} sequences from validation dataset") + + # Generate samples using reverse diffusion process + with torch.no_grad(): + for sample in sampled_sequences: + if isinstance(sample, dict): + original_sequence = sample.get("input_ids") + labels_seq = sample.get("labels") + attn_seq = sample.get("attention_mask") + else: + original_sequence = sample + labels_seq = None + attn_seq = None + generation_result = generate( + unwrapped_model, + tokenizer, + original_sequence, + num_diffusion_steps, + temperature, + mask_token_id, + mode=mode, + completion_tokens=completion_tokens, + target_mask_ratio=target_mask_ratio, + labels=labels_seq, + attention_mask=attn_seq, + ) + generations.append(generation_result) + + # Restore prior training state + if training: + unwrapped_model.train() + else: + unwrapped_model.eval() + + return generations + + +def _sample_sequences_from_dataloader( + dataloader: Any, num_samples: int, max_length: int, device: torch.device +) -> List[Any]: + """Sample sequences from validation dataloader.""" + sampled_sequences: list[dict[str, torch.Tensor] | torch.Tensor] = [] + sample_count = 0 + + # Skip a random number of batches (we could be more clever about this) + skip_batches = torch.randint(0, 10, (1,)).item() + batch_count = 0 + + for batch in dataloader: + # Skip some batches for variety + if batch_count < skip_batches: + batch_count += 1 + continue + + if sample_count >= num_samples: + break + + batch_count += 1 + input_ids = batch["input_ids"] + attention_mask = batch.get("attention_mask") + labels = batch.get("labels") + + # Randomly sample from sequences in this batch + batch_indices = torch.randperm(input_ids.size(0)).tolist() + + for i in batch_indices: + if sample_count >= num_samples: + break + + # Get actual sequence length (non-padded) + if attention_mask is not None: + seq_len = attention_mask[i].sum().item() + else: + seq_len = input_ids.size(1) + + if seq_len < 10: + continue + + # Determine truncation length + max_total = min(seq_len, max_length) + if labels is not None: + labels_i = labels[i][:seq_len] + answer_mask = labels_i != -100 + if not answer_mask.any(): + # No answer tokens; skip for SFT masking + continue + first_ans_idx = int( + torch.nonzero(answer_mask, as_tuple=False)[0].item() + ) + prompt_len = first_ans_idx + if prompt_len >= max_total: + # Prompt alone reaches cap; cannot include any answer + continue + remaining_answer = int(answer_mask[prompt_len:].sum().item()) + allowed_answer = max_total - prompt_len + take_answer = min(remaining_answer, allowed_answer) + if take_answer <= 0: + continue + actual_length = prompt_len + take_answer + else: + actual_length = max_total + + # Extract the (possibly truncated) sequence + sequence = input_ids[i][:actual_length].unsqueeze(0).to(device) + attn_seq = ( + attention_mask[i][:actual_length].unsqueeze(0).to(device) + if attention_mask is not None + else None + ) + if labels is not None: + labels_seq = labels[i][:actual_length].unsqueeze(0).to(device) + sampled_sequences.append( + { + "input_ids": sequence, + "labels": labels_seq, + "attention_mask": attn_seq, + } + ) + else: + if attn_seq is not None: + sampled_sequences.append( + {"input_ids": sequence, "attention_mask": attn_seq} + ) + else: + sampled_sequences.append(sequence) + sample_count += 1 + + return sampled_sequences + + +def generate( + model: torch.nn.Module, + tokenizer: Any, + original_sequence: torch.Tensor, + num_diffusion_steps: int, + temperature: float, + mask_token_id: int, + *, + mode: Literal["random", "completion"] = "random", + completion_tokens: int = 0, + target_mask_ratio: Optional[float] = None, + labels: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, +) -> dict: + """Generate a single sample using reverse diffusion.""" + # Get original text for comparison + original_text = tokenizer.decode( + original_sequence[0].cpu(), skip_special_tokens=True + ) + + # Build masked sequence + if ( + labels is not None + and labels.numel() > 0 + and (labels == -100).any() + and (labels != -100).any() + ): + # SFT case: completely mask all answer tokens (labels != -100) + total_tokens = original_sequence.size(1) + masked_indices = (labels != -100).to(dtype=torch.bool) + masked_sequence = original_sequence.clone() + masked_sequence[masked_indices] = mask_token_id + masked_tokens = int(masked_indices.sum().item()) + mask_ratio = masked_tokens / max(int(total_tokens), 1) + elif mode == "completion" and completion_tokens > 0: + # Append mask tokens to the right for completion + total_tokens = original_sequence.size(1) + int(completion_tokens) + masked_indices = torch.zeros( + 1, total_tokens, dtype=torch.bool, device=original_sequence.device + ) + masked_indices[0, -int(completion_tokens) :] = True + + append = torch.full( + (1, int(completion_tokens)), mask_token_id, device=original_sequence.device + ) + masked_sequence = torch.cat([original_sequence, append], dim=1) + masked_tokens = int(completion_tokens) + mask_ratio = masked_tokens / total_tokens + else: + # Apply random masking with optional fixed ratio + total_tokens = original_sequence.size(1) + if target_mask_ratio is None: + min_ratio, max_ratio = 0.1, 0.7 + target_mask_ratio = ( + torch.rand(1).item() * (max_ratio - min_ratio) + min_ratio + ) + target_masked_tokens = max(1, int(total_tokens * float(target_mask_ratio))) + + # Create random mask indices + mask_positions = torch.randperm(total_tokens)[:target_masked_tokens] + masked_indices = torch.zeros( + 1, total_tokens, dtype=torch.bool, device=original_sequence.device + ) + masked_indices[0, mask_positions] = True + + # Create masked sequence + masked_sequence = original_sequence.clone() + masked_sequence[masked_indices] = mask_token_id + + # Calculate actual mask ratio + masked_tokens = masked_indices.sum().item() + mask_ratio = masked_tokens / total_tokens + + # Get masked text for comparison + masked_text = tokenizer.decode(masked_sequence[0].cpu(), skip_special_tokens=False) + masked_text = _clean_masked_text(masked_text, tokenizer, mask_token_id) + + # Run reverse diffusion process + sequence = masked_sequence.clone() + attention_mask = create_bidirectional_attention_mask( + sequence, attention_mask, sample_packing=attention_mask is not None + ) + for step in range(num_diffusion_steps): + sequence = _diffusion_step( + model, + sequence, + step, + num_diffusion_steps, + temperature, + mask_token_id, + attention_mask, + ) + generated_text = tokenizer.decode(sequence[0].cpu(), skip_special_tokens=True) + + # Collect diagnostic info + final_ids = sequence[0].detach().cpu().tolist() + orig_ids_for_render = original_sequence[0].detach().cpu().tolist() + if masked_indices is not None: + masked_positions = ( + torch.where(masked_indices[0])[0].detach().cpu().tolist() + if masked_indices.ndim == 2 + else [] + ) + else: + masked_positions = [] + + result = { + "original": original_text, + "masked": masked_text, + "generated": generated_text, + "mask_ratio": mask_ratio, + "masked_tokens": masked_tokens, + "total_tokens": total_tokens, + "generated_ids": final_ids, + "masked_positions": masked_positions, + "orig_ids": orig_ids_for_render, + "formatted": ( + f"Original: '{original_text}' → Masked: '{masked_text}' " + f"({mask_ratio:.1%}) → Generated: '{generated_text}'" + ), + } + + return result + + +def _clean_masked_text(masked_text: str, tokenizer: Any, mask_token_id: int) -> str: + """Clean up masked text for display.""" + mask_token_repr = tokenizer.decode([mask_token_id], skip_special_tokens=False) + cleaned = masked_text.replace(mask_token_repr, "[MASK]") + + # Remove literal special token strings + if hasattr(tokenizer, "special_tokens_map"): + for token_value in tokenizer.special_tokens_map.values(): + if token_value and isinstance(token_value, str): + cleaned = cleaned.replace(token_value, "") + + # Normalize whitespace but preserve newlines + cleaned = cleaned.replace("\r\n", "\n").replace("\r", "\n") + cleaned = re.sub(r"[ \t]+", " ", cleaned) + cleaned = "\n".join(line.rstrip() for line in cleaned.split("\n")).strip() + return cleaned + + +def _diffusion_step( + model: torch.nn.Module, + sequence: torch.Tensor, + step: int, + num_diffusion_steps: int, + temperature: float, + mask_token_id: int, + attention_mask: torch.Tensor | None = None, +) -> torch.Tensor: + """Perform a single diffusion step with remasking.""" + # Only process if there are masked tokens remaining + current_mask = sequence == mask_token_id + if not current_mask.any(): + return sequence + + # Create or use provided attention mask + if attention_mask is None: + batch_size, seq_len = sequence.shape + attention_mask = torch.ones( + batch_size, 1, seq_len, seq_len, dtype=torch.bool, device=sequence.device + ) + + # Forward pass + outputs = model(input_ids=sequence, attention_mask=attention_mask) + logits = outputs.logits + + # Only sample at currently masked positions + if current_mask.any(): + masked_logits = logits[current_mask] + + # Apply temperature scaling + if temperature > 0: + scaled_logits = masked_logits / temperature + else: + scaled_logits = masked_logits + + # Suppress mask token in outputs + scaled_logits[:, mask_token_id] = -float("inf") + + if temperature > 0: + # Add Gumbel noise for sampling + gumbel_noise = -torch.log( + -torch.log(torch.rand_like(scaled_logits, dtype=torch.float32)) + ) + gumbel_logits = scaled_logits + gumbel_noise + predicted_tokens = torch.argmax(gumbel_logits, dim=-1) + else: + predicted_tokens = torch.argmax(scaled_logits, dim=-1) + + # Calculate probabilities for confidence scoring + probs = torch.softmax(scaled_logits, dim=-1) + predicted_token_probs = probs[range(len(predicted_tokens)), predicted_tokens] + + # Determine how many tokens to unmask this step + remaining_masked = current_mask.sum().item() + if step == num_diffusion_steps - 1: + num_to_unmask = remaining_masked + else: + unmask_ratio = 1.0 / (num_diffusion_steps - step) + num_to_unmask = max(1, int(remaining_masked * unmask_ratio)) + + # Select highest confidence predictions to unmask + if num_to_unmask >= remaining_masked: + sequence[current_mask] = predicted_tokens + else: + _, top_indices = predicted_token_probs.topk(num_to_unmask) + mask_positions = torch.where(current_mask)[1] + positions_to_unmask = mask_positions[top_indices] + sequence[0, positions_to_unmask] = predicted_tokens[top_indices] + + return sequence diff --git a/src/axolotl/integrations/diffusion/plugin.py b/src/axolotl/integrations/diffusion/plugin.py new file mode 100644 index 0000000000..c31f48b03a --- /dev/null +++ b/src/axolotl/integrations/diffusion/plugin.py @@ -0,0 +1,41 @@ +"""Diffusion LM training plugin for Axolotl.""" + +from peft import PeftModel +from transformers import PreTrainedModel + +from axolotl.integrations.base import BasePlugin +from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger + +from .trainer import DiffusionTrainer + +LOG = get_logger(__name__) + + +class DiffusionPlugin(BasePlugin): + """ + Plugin for diffusion language model training. + + This plugin enables diffusion-based training using the LLaDA approach, which uses + random masking and bidirectional attention to train language models. + """ + + def __init__(self): + super().__init__() + self.cfg = None + + def get_input_args(self) -> str: + """Returns the pydantic model for LLaDA plugin arguments.""" + return "axolotl.integrations.diffusion.DiffusionArgs" + + def post_model_load(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): + """Perform actions after model is loaded.""" + self.cfg = cfg + + def get_trainer_cls(self, cfg: DictDefault) -> type[DiffusionTrainer] | None: + """Return custom trainer class for diffusion training.""" + return DiffusionTrainer + + def post_trainer_create(self, cfg: DictDefault, trainer: DiffusionTrainer): + """Configure trainer after creation.""" + trainer.set_config(cfg) diff --git a/src/axolotl/integrations/diffusion/trainer.py b/src/axolotl/integrations/diffusion/trainer.py new file mode 100644 index 0000000000..42b2468f41 --- /dev/null +++ b/src/axolotl/integrations/diffusion/trainer.py @@ -0,0 +1,301 @@ +"""Custom trainer for diffusion LM training.""" + +from typing import Any, Literal + +import torch +import torch.nn.functional as F +from torch import nn + +from axolotl.core.trainers.base import AxolotlTrainer +from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger + +from .callbacks import DiffusionGenerationCallback +from .utils import create_bidirectional_attention_mask + +LOG = get_logger(__name__) + + +class DiffusionTrainer(AxolotlTrainer): + """Custom trainer for diffusion LM training that overrides loss computation.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.cfg = None + self._special_token_ids = None + + def set_config(self, config: DictDefault): + """Set config for diffusion training.""" + self.cfg = config + self._cache_special_token_ids() + self._resolve_mask_token_id() + + token_id = int(getattr(self.cfg.diffusion, "mask_token_id", 0)) + LOG.info(f"Diffusion: using mask_token_id={token_id}") + + if getattr(config.diffusion, "generate_samples", True): + generation_callback = DiffusionGenerationCallback(self) + self.add_callback(generation_callback) + + def _resolve_mask_token_id(self) -> None: + """Ensure mask_token_id is valid for the current tokenizer.""" + from .utils import resolve_mask_token_id + + tokenizer = getattr(self, "processing_class", None) + if tokenizer is None: + return + + mid = resolve_mask_token_id( + tokenizer, + self.cfg, + allow_add=True, + model=getattr(self, "model", None), + ) + try: + self.cfg.diffusion.mask_token_id = int(mid) + except Exception: + pass + + def compute_loss( + self, + model: nn.Module, + inputs: dict[str, torch.Tensor], + return_outputs: bool = False, + num_items_in_batch: torch.Tensor | None = None, + ) -> torch.Tensor | tuple[torch.Tensor, dict[str, torch.Tensor]]: + """Override compute_loss to use diffusion loss.""" + input_ids = inputs.get("input_ids") + attention_mask = inputs.get("attention_mask") + labels = inputs.get("labels") + + if input_ids is None: + raise ValueError("input_ids is required for diffusion training") + + loss, outputs = self._compute_diffusion_loss( + model, input_ids, attention_mask, labels + ) + + if return_outputs: + return loss, outputs + return loss + + def _cache_special_token_ids(self): + """Cache special token IDs to avoid repeated tokenizer access.""" + if self.processing_class is None: + self._special_token_ids = set() + return + + tokenizer = self.processing_class + special_tokens = set() + + if hasattr(tokenizer, "bos_token_id") and tokenizer.bos_token_id is not None: + special_tokens.add(tokenizer.bos_token_id) + if hasattr(tokenizer, "eos_token_id") and tokenizer.eos_token_id is not None: + special_tokens.add(tokenizer.eos_token_id) + if hasattr(tokenizer, "pad_token_id") and tokenizer.pad_token_id is not None: + special_tokens.add(tokenizer.pad_token_id) + + self._special_token_ids = special_tokens + + def _forward_process( + self, + input_ids: torch.Tensor, + attention_mask: torch.Tensor | None = None, + labels: torch.Tensor | None = None, + eps: float = 1e-3, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Forward noising process. A timestep is sampled along the process, and tokens are + masked with probability determined by the configured noise schedule. + + Args: + input_ids: Input token ids [batch_size, seq_len]. + attention_mask: Attention mask [batch_size, seq_len]. + labels: Labels for SFT training [batch_size, seq_len]. + eps: Small epsilon value for minimum masking probability. + + Returns: + noisy_batch: Input with some tokens masked. + masked_indices: Boolean mask indicating which tokens were masked. + p_mask: Masking probabilities for each token [batch_size, seq_len]. + """ + batch_size, seq_len = input_ids.shape + device = input_ids.device + + # Sample random timesteps for each sample in batch + t = torch.rand(batch_size, device=device) + p_mask = (1 - eps) * t + eps # [batch_size] + p_mask = p_mask[:, None].repeat(1, seq_len) # [batch_size, seq_len] + + # Don't mask padding tokens if attention_mask is provided + if attention_mask is not None: + valid_mask = attention_mask.bool() + p_mask = p_mask * valid_mask.float() + + # Create mask to exclude special tokens + special_token_mask = torch.zeros_like(input_ids, dtype=torch.bool) + if self._special_token_ids: + for token_id in self._special_token_ids: + special_token_mask |= input_ids == token_id + + # Create random mask based on p_mask + masked_indices = torch.rand((batch_size, seq_len), device=device) < p_mask + masked_indices = masked_indices & ~special_token_mask + if attention_mask is not None: + masked_indices = masked_indices & attention_mask.bool() + + # For SFT data, only mask answer tokens + if labels is not None: + answer_mask = labels != -100 + masked_indices = masked_indices & answer_mask + + # Create masked input + mask_token_id = int(self.cfg.diffusion.mask_token_id) + mask_value = torch.full_like(input_ids, mask_token_id) + noisy_batch = torch.where(masked_indices, mask_value, input_ids) + + return noisy_batch, masked_indices, p_mask + + def _compute_diffusion_loss( + self, + model: nn.Module, + input_ids: torch.Tensor, + attention_mask: torch.Tensor | None = None, + labels: torch.Tensor | None = None, + ) -> tuple[torch.Tensor, torch.Tensor | Any]: + """ + Compute diffusion loss. + + Args: + model: The model to compute loss for. + input_ids: Ground truth token ids [batch_size, seq_len]. + attention_mask: Attention mask [batch_size, seq_len]. + labels: Labels for SFT training [batch_size, seq_len]. + + Returns: + loss: Cross-entropy loss. + metrics: Dictionary of metrics. + """ + # Short-circuit empty sequences + if input_ids is None or input_ids.numel() == 0 or input_ids.shape[1] == 0: + zero = torch.tensor( + 0.0, + device=(input_ids.device if input_ids is not None else None), + requires_grad=True, + ) + return zero, {} + + # If an attention_mask is provided and all positions are padding for every + # sample in this batch, skip the step. + if attention_mask is not None: + if attention_mask.dim() == 2 and (attention_mask.sum(dim=1) == 0).all(): + zero = torch.tensor(0.0, device=input_ids.device, requires_grad=True) + return zero, {} + + # Apply forward process + noisy_batch, masked_indices, p_mask = self._forward_process( + input_ids, attention_mask, labels, self.cfg.diffusion.eps + ) + + # Create bidirectional attention mask + bidirectional_mask = create_bidirectional_attention_mask( + input_ids, attention_mask, sample_packing=self.cfg.sample_packing + ) + + # Forward pass + outputs = model( + input_ids=noisy_batch.long(), + attention_mask=bidirectional_mask, + ) + logits = outputs.logits + + if masked_indices.sum() > 0: + valid_indices = torch.where(masked_indices) + batch_indices, seq_indices = valid_indices + + masked_logits = logits[batch_indices, seq_indices] + masked_targets = input_ids[batch_indices, seq_indices] + masked_p_mask = p_mask[batch_indices, seq_indices] + + # Compute cross-entropy loss without reduction + token_loss = F.cross_entropy( + masked_logits.float(), masked_targets, reduction="none" + ) + + if self.cfg.diffusion.importance_weighting: + masked_p_mask = masked_p_mask.float() + weighted_loss = token_loss / masked_p_mask + else: + weighted_loss = token_loss + + if labels is not None: + # For SFT data: normalize by answer token count per sample + answer_mask = labels != -100 + answer_lengths = answer_mask.sum(dim=1).float() # [batch_size] + + # Get batch indices for masked tokens + masked_batch_indices = batch_indices + + # Sum losses per sample and divide by answer length + batch_size = input_ids.shape[0] + loss_per_sample = torch.zeros(batch_size, device=input_ids.device) + for i in range(batch_size): + sample_mask = masked_batch_indices == i + if sample_mask.sum() > 0: + sample_loss = weighted_loss[sample_mask].sum() + denom = answer_lengths[i].clamp(min=1.0) + loss_per_sample[i] = sample_loss / denom + + loss = loss_per_sample.mean() + else: + # Non-SFT: when importance weighting is enabled, use unbiased estimator + # (sum(loss/p) / total_tokens). Otherwise, average over masked tokens + # for stable scaling across varying mask ratios. + if self.cfg.diffusion.importance_weighting: + loss = weighted_loss.sum() / ( + input_ids.shape[0] * input_ids.shape[1] + ) + else: + loss = weighted_loss.mean() + + ce_loss = token_loss.mean() + + # Compute accuracy on masked tokens + with torch.no_grad(): + pred_tokens = masked_logits.argmax(dim=-1) + accuracy = (pred_tokens == masked_targets).float().mean() + else: + loss = torch.tensor(0.0, device=input_ids.device, requires_grad=True) + accuracy = torch.tensor(0.0, device=input_ids.device) + ce_loss = torch.tensor(0.0, device=input_ids.device) + masked_p_mask = torch.tensor(1.0, device=input_ids.device) + + avg_p_mask = ( + p_mask[masked_indices].mean().item() if masked_indices.any() else 0.0 + ) + metrics = { + "loss": loss.item(), + "accuracy": accuracy.item(), + "mask_ratio": masked_indices.float().mean().item(), + "num_masked_tokens": (masked_indices.sum().item(), "sum"), + "avg_p_mask": avg_p_mask, + "ce_loss": ce_loss.item(), + } + + # If doing SFT training, log answer-specific metrics + if self.cfg.datasets is not None: + with torch.no_grad(): + answer_mask = labels != -100 + answer_lengths = answer_mask.sum(dim=1).float() # type: ignore + total_answer_tokens = answer_mask.sum().item() # type: ignore + total_tokens = labels.numel() # type: ignore + metrics["answer_ratio"] = total_answer_tokens / max(total_tokens, 1) + metrics["avg_answer_length"] = answer_lengths.mean().item() + + if self.cfg.diffusion.importance_weighting: + metrics["importance_weight_avg"] = (1.0 / masked_p_mask).mean().item() + + train_eval: Literal["train", "eval"] = "train" if model.training else "eval" + self.store_metrics(metrics, train_eval=train_eval) + + return loss, outputs diff --git a/src/axolotl/integrations/diffusion/utils.py b/src/axolotl/integrations/diffusion/utils.py new file mode 100644 index 0000000000..47abf6fecb --- /dev/null +++ b/src/axolotl/integrations/diffusion/utils.py @@ -0,0 +1,159 @@ +"""Shared utilities for diffusion integration.""" + +from __future__ import annotations + +from typing import Any, Optional + +import torch + +from axolotl.utils.dict import DictDefault + + +def resolve_mask_token_id( + tokenizer: Any, + cfg: DictDefault, + *, + allow_add: bool, + model: Any | None = None, + default_token: str = "<|diffusion_mask|>", +) -> int: + """Resolve mask token id. Training may add a new special token; inference won't.""" + # Determine vocab size if available + vocab_size = None + if tokenizer is not None: + if hasattr(tokenizer, "vocab_size") and tokenizer.vocab_size is not None: + try: + vocab_size = int(tokenizer.vocab_size) # type: ignore[arg-type] + except Exception: + vocab_size = None + elif hasattr(tokenizer, "__len__"): + try: + vocab_size = int(len(tokenizer)) + except Exception: + vocab_size = None + + # Use explicit id from config if provided + diffusion_cfg = getattr(cfg, "diffusion", None) + # Fallback to top-level attr names only if nested missing (shouldn't happen) + cfg_id = ( + getattr(diffusion_cfg, "mask_token_id", None) + if diffusion_cfg is not None + else getattr(cfg, "diffusion_mask_token_id", None) + ) + if isinstance(cfg_id, int) and cfg_id >= 0: + if vocab_size is None or cfg_id < vocab_size: + return int(cfg_id) + + def _existing_special_token_id(token_str: str | None) -> int | None: + """Attempt to resolve an existing special token string to a real ID.""" + if not token_str or not hasattr(tokenizer, "convert_tokens_to_ids"): + return None + try: + token_id = tokenizer.convert_tokens_to_ids(token_str) + except Exception: + return None + + if not isinstance(token_id, int) or token_id < 0: + return None + + # Ensure it's registered as special and not UNK, and within vocab + unk_id = getattr(tokenizer, "unk_token_id", None) + specials = set(getattr(tokenizer, "all_special_tokens", []) or []) + addl = set(getattr(tokenizer, "additional_special_tokens", []) or []) + is_special = token_str in specials or token_str in addl + in_vocab = vocab_size is None or token_id < vocab_size + if ( + (unk_id is not None and token_id == unk_id) + or not is_special + or not in_vocab + ): + return None + return token_id + + # Try mask token string if provided + token_str = ( + getattr(diffusion_cfg, "mask_token_str", None) + if diffusion_cfg is not None + else getattr(cfg, "diffusion_mask_token_str", None) + ) + for candidate in (token_str, default_token): + token_id = _existing_special_token_id(candidate) + if isinstance(token_id, int): + try: + if diffusion_cfg is None: + cfg.diffusion_mask_token_id = int(token_id) # legacy fallback + else: + diffusion_cfg.mask_token_id = int(token_id) + except Exception: + pass + return int(token_id) + + # Optionally add and return a dedicated special token during training + if allow_add and hasattr(tokenizer, "add_special_tokens"): + token_to_add = token_str or default_token + try: + tokenizer.add_special_tokens({"additional_special_tokens": [token_to_add]}) + + # Resize embeddings if possible + if ( + model is not None + and hasattr(tokenizer, "__len__") + and hasattr(model, "resize_token_embeddings") + ): + try: + model.resize_token_embeddings(len(tokenizer)) + except Exception: + pass + new_id = tokenizer.convert_tokens_to_ids(token_to_add) + if isinstance(new_id, int) and new_id >= 0: + try: + if diffusion_cfg is None: + cfg.diffusion_mask_token_id = int(new_id) # legacy fallback + else: + diffusion_cfg.mask_token_id = int(new_id) + except Exception: + pass + return int(new_id) + except Exception: + pass + + # Fallback to unk or 0 (do not update cfg) + fallback = getattr(tokenizer, "unk_token_id", 0) or 0 + return int(fallback) + + +def create_bidirectional_attention_mask( + input_ids: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + sample_packing: bool = False, +) -> torch.Tensor: + """ + Create bidirectional attention mask to override default causal masking. + Handles sample-packed sequences where different samples are identified + by different attention mask values. + + Args: + input_ids: Input token ids [batch_size, seq_len] + attention_mask: Attention mask [batch_size, seq_len] + sample_packing: Whether sample packing is enabled + + Returns: + bidirectional_mask: 4D attention mask [batch_size, 1, seq_len, seq_len] + """ + batch_size, seq_len = input_ids.shape + device = input_ids.device + + if attention_mask is None or not sample_packing: + return torch.ones( + batch_size, 1, seq_len, seq_len, dtype=torch.bool, device=device + ) + + # Handle sample packing: tokens can only attend within their sample + mask_i = attention_mask.unsqueeze(2) # [batch_size, seq_len, 1] + mask_j = attention_mask.unsqueeze(1) # [batch_size, 1, seq_len] + + # Tokens can attend to each other if they have the same non-zero sample ID + bidirectional_mask = (mask_i == mask_j) & (mask_i > 0) + + # Add head dimension: [batch_size, 1, seq_len, seq_len] + return bidirectional_mask.unsqueeze(1) diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index 989b34aee0..bcde4bf96e 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -14,6 +14,7 @@ PeftConfig, PeftMixedModel, PeftModel, + TaskType, get_peft_model, ) from transformers import PreTrainedModel @@ -101,6 +102,15 @@ def load_lora( if cfg.peft_trainable_token_indices: lora_config_kwargs["trainable_token_indices"] = cfg.peft_trainable_token_indices + # Determine the correct PEFT task type + model_cls = type(model).__name__ + if "SequenceClassification" in model_cls: + task_type = TaskType.SEQ_CLS + elif "TokenClassification" in model_cls: + task_type = TaskType.TOKEN_CLS + else: + task_type = TaskType.CAUSAL_LM + lora_config = LoraConfig( r=cfg.lora_r, lora_alpha=cfg.lora_alpha, @@ -112,7 +122,7 @@ def load_lora( fan_in_fan_out=cfg.lora_fan_in_fan_out, modules_to_save=cfg.lora_modules_to_save if cfg.lora_modules_to_save else None, bias="none", - task_type="CAUSAL_LM", + task_type=task_type, **lora_config_kwargs, ) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index a9507d685d..f438d6b61a 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -673,6 +673,33 @@ def _configure_zero3_memory_efficient_loading( return hf_ds_cfg + def _load_model_from_config(self, model_loader_class=None) -> PreTrainedModel: + """ + Load model with random initialization using from_config. + + Uses the selected loader when provided; otherwise falls back to the auto loader. + """ + loader = model_loader_class or self.auto_model_loader + if loader in [AutoModelForCausalLM, AutoModelForVision2Seq]: + model = loader.from_config( + config=self.model_config, + trust_remote_code=self.cfg.trust_remote_code or False, + ) + else: + model = loader(config=self.model_config) + + return model + + def _load_model_from_pretrained(self, model_loader_class=None) -> PreTrainedModel: + """Load model from pretrained weights.""" + loader = model_loader_class or self.auto_model_loader + kwargs = { + "config": self.model_config, + "trust_remote_code": self.cfg.trust_remote_code or False, + **self.model_kwargs, + } + return loader.from_pretrained(self.base_model, **kwargs) + def _build_model(self) -> bool: """Load model, with load strategy depending on config.""" skip_move_to_device = False @@ -687,7 +714,8 @@ def _build_model(self) -> bool: if self.is_fsdp_enabled: if self.cfg.fsdp_config.cpu_ram_efficient_loading: skip_move_to_device = True - # Don't delete device_map for QLoRA + FSDP - it was set correctly in _set_device_map + # Don't delete device_map for QLoRA + FSDP - it was set correctly in + # _set_device_map if ( "device_map" in self.model_kwargs and not self.is_qlora_and_fsdp_enabled @@ -716,6 +744,11 @@ def _build_model(self) -> bool: or self.cfg.qlora_sharded_model_loading ) ): + if self.cfg.reinit_weights: + LOG.warning( + "reinit_weights is not supported with sharded quantized loading. " + "Loading from pretrained weights instead." + ) quant_storage = self.cfg.torch_dtype quantization_config = getattr( self.model_config, "quantization_config", None @@ -731,33 +764,12 @@ def _build_model(self) -> bool: quantization_config=quantization_config, ) skip_move_to_device = True - elif ( - self.model_config.model_type in ["llama", "llama4"] - and not self.cfg.trust_remote_code - and not self.cfg.gptq - ): - # Please don't remove underscore binding without reading the fn docstring. - _ = self._configure_zero3_memory_efficient_loading() - - # Load model with random initialization if specified - if self.cfg.random_init_weights: - # AutoModel classes support the from_config method - if self.auto_model_loader in [ - AutoModelForCausalLM, - AutoModelForVision2Seq, - ]: - self.model = self.auto_model_loader.from_config( - config=self.model_config, - ) - else: - self.model = self.auto_model_loader(config=self.model_config) - else: - self.model = self.auto_model_loader.from_pretrained( - self.base_model, - config=self.model_config, - **self.model_kwargs, - ) elif self.model_type == "MambaLMHeadModel": + if self.cfg.reinit_weights: + LOG.warning( + "reinit_weights is not supported with MambaLMHeadModel. " + "Loading from pretrained weights instead." + ) # FIXME this is janky at best and hacked together to make it work MambaLMHeadModel = fix_mamba_attn_for_loss() @@ -770,41 +782,27 @@ def _build_model(self) -> bool: self.base_model, **self.model_kwargs, ) - elif ( - self.model_type - and self.model_type != "AutoModelForCausalLM" - and not self.cfg.trust_remote_code - ): - if self.cfg.gptq: - self.model = self.auto_model_loader.from_pretrained( - self.base_model, - config=self.model_config, - trust_remote_code=self.cfg.trust_remote_code or False, - **self.model_kwargs, - ) - else: - self.model = getattr(transformers, self.model_type).from_pretrained( - self.base_model, - config=self.model_config, - trust_remote_code=self.cfg.trust_remote_code or False, - **self.model_kwargs, - ) - elif self.cfg.gptq: - self.model = self.auto_model_loader.from_pretrained( - self.base_model, - config=self.model_config, - trust_remote_code=self.cfg.trust_remote_code or False, - **self.model_kwargs, - ) else: - # Please don't remove underscore binding without reading the fn docstring. + # Please don't remove underscore binding without reading the fn docstring _ = self._configure_zero3_memory_efficient_loading() - self.model = self.auto_model_loader.from_pretrained( - self.base_model, - config=self.model_config, - trust_remote_code=self.cfg.trust_remote_code or False, - **self.model_kwargs, - ) + + if ( + self.model_type + and self.model_type != "AutoModelForCausalLM" + and not self.cfg.trust_remote_code + and not self.cfg.gptq + ): + # Use model type from transformers + model_loader_class = getattr(transformers, self.model_type) + else: + # Use auto model loader (handles gptq and default cases) + model_loader_class = self.auto_model_loader + + if self.cfg.reinit_weights: + self.model = self._load_model_from_config(model_loader_class) + else: + self.model = self._load_model_from_pretrained(model_loader_class) + if is_deepspeed_zero3_enabled(): skip_move_to_device = True diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 044c278a36..a5a630cb59 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -3,8 +3,8 @@ Applies pre- and post-model load patches for various fixes and optimizations. """ -import os import importlib.util +import os from functools import cached_property import addict @@ -468,9 +468,10 @@ def _apply_lora_kernel_patch(self, model): def _apply_patch_deepspeed_zero3(self): try: - from axolotl.monkeypatch.deepspeed_utils import apply_deepspeed_patches from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled + from axolotl.monkeypatch.deepspeed_utils import apply_deepspeed_patches + if self.cfg.activation_offloading is True and ( is_deepspeed_zero3_enabled() or os.getenv("ACCELERATE_DEEPSPEED_ZERO_STAGE") == "3" diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index 3b38a33b70..d8ba02cb2c 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -160,9 +160,11 @@ def get_state_dict(self, model, unwrap=True): state_dict[param_name] = param.cpu() torch.distributed.barrier() elif self.distributed_type == DistributedType.FSDP: - from torch.distributed.fsdp import FullStateDictConfig - from torch.distributed.fsdp import FullyShardedDataParallel as FSDP - from torch.distributed.fsdp import StateDictType + from torch.distributed.fsdp import ( + FullStateDictConfig, + FullyShardedDataParallel as FSDP, + StateDictType, + ) full_state_dict_config = FullStateDictConfig( offload_to_cpu=True, rank0_only=True diff --git a/src/axolotl/monkeypatch/attention/flex_attn.py b/src/axolotl/monkeypatch/attention/flex_attn.py index 65ccad5336..678f65bee3 100644 --- a/src/axolotl/monkeypatch/attention/flex_attn.py +++ b/src/axolotl/monkeypatch/attention/flex_attn.py @@ -1,11 +1,12 @@ """Flex attention monkey patch""" import sys -from packaging import version import torch import transformers +from packaging import version from transformers.utils.import_utils import _torch_version, is_torch_less_or_equal + from axolotl.utils.logging import get_logger LOG = get_logger(__name__) diff --git a/src/axolotl/monkeypatch/deepspeed_utils.py b/src/axolotl/monkeypatch/deepspeed_utils.py index 6740f556be..d7e69e1121 100644 --- a/src/axolotl/monkeypatch/deepspeed_utils.py +++ b/src/axolotl/monkeypatch/deepspeed_utils.py @@ -1,5 +1,6 @@ import importlib import importlib.util + from axolotl.utils.logging import get_logger LOG = get_logger(__name__) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index f40fe6687d..7a2bbd6f9a 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -17,8 +17,8 @@ from axolotl.utils.logging import get_logger from axolotl.utils.schemas.config import ( AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, + AxolotlInputConfig as AxolotlInputConfigBase, ) -from axolotl.utils.schemas.config import AxolotlInputConfig as AxolotlInputConfigBase from axolotl.utils.schemas.datasets import DPODataset, KTODataset, SFTDataset LOG = get_logger(__name__) diff --git a/src/axolotl/utils/data/__init__.py b/src/axolotl/utils/data/__init__.py index 788f136387..8b9e4e91d4 100644 --- a/src/axolotl/utils/data/__init__.py +++ b/src/axolotl/utils/data/__init__.py @@ -1,14 +1,14 @@ """Init for `axolotl.utils.data` module.""" -from axolotl.utils.data.streaming import ( - encode_streaming, - wrap_streaming_dataset, -) from axolotl.utils.data.rl import prepare_preference_datasets from axolotl.utils.data.sft import ( get_dataset_wrapper, prepare_datasets, ) +from axolotl.utils.data.streaming import ( + encode_streaming, + wrap_streaming_dataset, +) from axolotl.utils.data.utils import md5 __all__ = [ diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 28732e01d2..ba5aec2d60 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -16,7 +16,6 @@ from axolotl.prompters import Prompter from axolotl.utils.data.lock import FileLockLoader -from axolotl.utils.data.streaming import wrap_streaming_dataset from axolotl.utils.data.shared import ( create_train_validation_split, datasets_with_name_generator, @@ -27,6 +26,7 @@ save_preprocessed_dataset, try_load_from_hub, ) +from axolotl.utils.data.streaming import wrap_streaming_dataset from axolotl.utils.data.utils import ( deduplicate_and_log_datasets, handle_long_seq_in_dataset, diff --git a/src/axolotl/utils/environment.py b/src/axolotl/utils/environment.py index 751f7e2534..192aca4e1a 100644 --- a/src/axolotl/utils/environment.py +++ b/src/axolotl/utils/environment.py @@ -6,8 +6,6 @@ from accelerate.utils.environment import ( check_cuda_p2p_ib_support as accelerate_check_cuda_p2p_ib_support, -) -from accelerate.utils.environment import ( get_gpu_info, ) from packaging.version import Version, parse diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index e4c1fdf291..d612ec8a59 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -106,6 +106,12 @@ class AxolotlInputConfig( "description": "Don't upcast the embeddings to float32 when using PEFT. Useful for low-VRAM GPUs" }, ) + reinit_weights: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Reinitialize model weights randomly instead of loading pretrained weights" + }, + ) trainer_cls: str | None = Field( default=None, diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 49add8081c..64018ca486 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -14,7 +14,6 @@ from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import ChatTemplate, RingAttnFunc, RLType - LOG = get_logger(__name__) SUPPORTED_METRICS = {"sacrebleu", "comet", "ter", "chrf", "perplexity"} diff --git a/tests/e2e/test_diffusion.py b/tests/e2e/test_diffusion.py new file mode 100644 index 0000000000..cc3d8070bf --- /dev/null +++ b/tests/e2e/test_diffusion.py @@ -0,0 +1,139 @@ +"""E2E smoke test for diffusion training plugin.""" + +from axolotl.common.datasets import load_datasets +from axolotl.train import train +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import check_model_output_exists + + +class TestDiffusion: + """Test case for diffusion training plugin.""" + + def test_diffusion_smoke_test(self, temp_dir): + """ + Smoke test for diffusion training to ensure the plugin loads and trains without + error. + """ + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", + "trust_remote_code": True, + "sequence_len": 256, + "val_set_size": 0.1, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 3, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "bf16": True, + "save_safetensors": True, + "save_first_step": False, + "logging_steps": 1, + "eval_steps": 3, + # Diffusion-specific config + "plugins": ["axolotl.integrations.diffusion.DiffusionPlugin"], + "diffusion": { + # sample generation + "generate_samples": True, + "generation_interval": 1, + "num_generation_samples": 1, + "generation_steps": 2, + "generation_max_length": 32, + "generation_temperature": 0.0, + # training-specific + "mask_token_id": 16, + "eps": 1e-3, + "importance_weighting": False, + }, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) + + def test_diffusion_sft_labels(self, temp_dir): + """Test that diffusion training properly handles SFT data with labels.""" + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", + "trust_remote_code": True, + "sequence_len": 256, + "val_set_size": 0.1, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 3, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "bf16": True, + "save_safetensors": True, + "save_first_step": False, + "logging_steps": 1, + "eval_steps": 2, + # Diffusion-specific config + "plugins": ["axolotl.integrations.diffusion.DiffusionPlugin"], + "diffusion": { + # sample generation + "generate_samples": True, + "generation_interval": 1, + "num_generation_samples": 1, + "generation_steps": 2, + "generation_max_length": 32, + "generation_temperature": 0.0, + # training-specific + "mask_token_id": 16, + "eps": 1e-3, + "importance_weighting": True, + }, + # Ensure we have proper SFT labels + "train_on_inputs": False, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + # Verify that the dataset has labels + sample = dataset_meta.train_dataset[0] + assert "labels" in sample, "SFT dataset should have labels" + + # Check that some labels are -100 (prompt tokens) + labels = sample["labels"] + if hasattr(labels, "tolist"): + labels = labels.tolist() + assert -100 in labels, "SFT dataset should have -100 labels for prompt tokens" + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) diff --git a/tests/integrations/test_diffusion.py b/tests/integrations/test_diffusion.py new file mode 100644 index 0000000000..141d8d1503 --- /dev/null +++ b/tests/integrations/test_diffusion.py @@ -0,0 +1,274 @@ +"""Tests for diffusion trainer integration.""" + +# pylint: disable=redefined-outer-name,protected-access + +from unittest.mock import Mock + +import pytest +import torch + +from axolotl.integrations.diffusion import DiffusionTrainer +from axolotl.integrations.diffusion.utils import create_bidirectional_attention_mask +from axolotl.utils.dict import DictDefault + + +@pytest.fixture +def mock_tokenizer(): + """Create a mock tokenizer.""" + tokenizer = Mock() + tokenizer.bos_token_id = 1 + tokenizer.eos_token_id = 2 + tokenizer.pad_token_id = 0 + return tokenizer + + +@pytest.fixture +def diffusion_config(): + """Create a diffusion config.""" + return DictDefault( + { + "diffusion": { + "mask_token_id": 32000, + "eps": 1e-3, + "importance_weighting": False, + }, + "sample_packing": False, + } + ) + + +@pytest.fixture +def diffusion_trainer_instance(mock_tokenizer, diffusion_config): + """Create a diffusion trainer instance for testing methods directly.""" + # Create a minimal trainer instance just for testing methods + trainer = object.__new__(DiffusionTrainer) # Bypass __init__ + trainer.cfg = diffusion_config + trainer._special_token_ids = {0, 1, 2} # pad, bos, eos + trainer.processing_class = mock_tokenizer + trainer.store_metrics = Mock() # Mock metrics storage + return trainer + + +class TestDiffusionTrainer: + """Test the DiffusionTrainer class.""" + + def test_forward_process_basic(self, diffusion_trainer_instance): + """Test basic forward process without labels.""" + input_ids = torch.tensor([[1, 10, 20, 30, 2]], dtype=torch.long) + + noisy_batch, masked_indices, p_mask = ( + diffusion_trainer_instance._forward_process(input_ids, eps=0.1) + ) + + # Check shapes + assert noisy_batch.shape == input_ids.shape + assert masked_indices.shape == input_ids.shape + assert p_mask.shape == input_ids.shape + + # Check that special tokens are not masked + special_token_positions = (input_ids == 1) | (input_ids == 2) | (input_ids == 0) + assert not masked_indices[special_token_positions].any() + + # Check that mask token is applied + mask_token_id = diffusion_trainer_instance.cfg.diffusion.mask_token_id + masked_positions = masked_indices + if masked_positions.any(): + assert (noisy_batch[masked_positions] == mask_token_id).all() + + def test_forward_process_with_labels(self, diffusion_trainer_instance): + """Test forward process with SFT labels.""" + input_ids = torch.tensor([[1, 10, 20, 30, 2]], dtype=torch.long) + labels = torch.tensor([[-100, -100, 20, 30, 2]], dtype=torch.long) + + noisy_batch, masked_indices, p_mask = ( + diffusion_trainer_instance._forward_process( + input_ids, labels=labels, eps=0.1 + ) + ) + + # Check shapes + assert noisy_batch.shape == input_ids.shape + assert masked_indices.shape == input_ids.shape + assert p_mask.shape == input_ids.shape + + # Check that only answer tokens can be masked (where labels != -100) + non_answer_mask = labels == -100 + + # No masking should occur on non-answer tokens + assert not masked_indices[non_answer_mask].any() + + # p_mask should be the same for all positions (sampled timestep), + # but masking is only applied to answer tokens + assert p_mask.shape == input_ids.shape + # Verify that masked_indices respects the answer mask + assert not masked_indices[non_answer_mask].any() + + def test_forward_process_with_attention_mask(self, diffusion_trainer_instance): + """Test forward process with attention mask.""" + input_ids = torch.tensor([[1, 10, 20, 0]], dtype=torch.long) + attention_mask = torch.tensor([[1, 1, 1, 0]], dtype=torch.long) + + _, masked_indices, p_mask = diffusion_trainer_instance._forward_process( + input_ids, attention_mask=attention_mask, eps=0.1 + ) + + # Check that padding tokens are not masked + padding_positions = attention_mask == 0 + assert not masked_indices[padding_positions].any() + assert (p_mask[padding_positions] == 0).all() + + def test_bidirectional_attention_mask_no_packing(self, diffusion_trainer_instance): + """Test bidirectional attention mask without sample packing.""" + input_ids = torch.tensor([[1, 10, 20, 2]], dtype=torch.long) + + mask = create_bidirectional_attention_mask(input_ids) + + # Should be all-to-all attention + expected_shape = (1, 1, 4, 4) + assert mask.shape == expected_shape + assert mask.all() + + def test_bidirectional_attention_mask_with_packing( + self, diffusion_trainer_instance + ): + """Test bidirectional attention mask with sample packing.""" + diffusion_trainer_instance.cfg.sample_packing = True + input_ids = torch.tensor([[1, 10, 20, 30, 40, 2]], dtype=torch.long) + # Sample IDs: first sample (1), second sample (2) + attention_mask = torch.tensor([[1, 1, 1, 2, 2, 2]], dtype=torch.long) + + mask = create_bidirectional_attention_mask( + input_ids, attention_mask, sample_packing=True + ) + + # Check that tokens within same sample can attend to each other + # but not across samples + assert mask[0, 0, 0, 1].item() # First sample tokens can attend to each other + assert mask[0, 0, 1, 2].item() + assert not mask[0, 0, 0, 3].item() # Can't attend across samples + assert not mask[0, 0, 2, 4].item() + assert mask[0, 0, 3, 4].item() # Second sample tokens can attend to each other + + def test_compute_loss_basic(self, diffusion_trainer_instance): + """Test basic loss computation.""" + # Mock model that returns logits + mock_model = Mock() + mock_outputs = Mock() + vocab_size = 1000 + seq_len = 5 + mock_outputs.logits = torch.randn(1, seq_len, vocab_size, requires_grad=True) + mock_model.return_value = mock_outputs + mock_model.training = True + + input_ids = torch.tensor([[1, 10, 20, 30, 2]], dtype=torch.long) + + loss, outputs = diffusion_trainer_instance._compute_diffusion_loss( + mock_model, input_ids + ) + + # Check that loss is computed + assert isinstance(loss, torch.Tensor) + assert loss.requires_grad + assert outputs == mock_outputs + + # Check that metrics were stored + diffusion_trainer_instance.store_metrics.assert_called_once() + + def test_compute_loss_sft(self, diffusion_trainer_instance): + """Test loss computation with SFT labels.""" + # Mock model + mock_model = Mock() + mock_outputs = Mock() + vocab_size = 1000 + seq_len = 5 + mock_outputs.logits = torch.randn(1, seq_len, vocab_size, requires_grad=True) + mock_model.return_value = mock_outputs + mock_model.training = True + diffusion_trainer_instance.cfg.datasets = Mock() + + input_ids = torch.tensor([[1, 10, 20, 30, 2]], dtype=torch.long) + labels = torch.tensor([[-100, -100, 20, 30, 2]], dtype=torch.long) + + loss, _ = diffusion_trainer_instance._compute_diffusion_loss( + mock_model, input_ids, labels=labels + ) + + # Check that loss is computed + assert isinstance(loss, torch.Tensor) + assert loss.requires_grad + + # Check that SFT metrics were added + call_args = diffusion_trainer_instance.store_metrics.call_args[0][0] + assert "answer_ratio" in call_args + assert "avg_answer_length" in call_args + + def test_compute_loss_no_masked_tokens(self, diffusion_trainer_instance): + """Test loss computation when no tokens are masked.""" + # Mock model + mock_model = Mock() + mock_outputs = Mock() + vocab_size = 1000 + seq_len = 3 + mock_outputs.logits = torch.randn(1, seq_len, vocab_size) + mock_model.return_value = mock_outputs + mock_model.training = True + + # Only special tokens (which won't be masked) + input_ids = torch.tensor([[1, 0, 2]], dtype=torch.long) + + loss, _ = diffusion_trainer_instance._compute_diffusion_loss( + mock_model, input_ids + ) + + # Loss should be zero when no tokens are masked + assert loss.item() == 0.0 + assert loss.requires_grad + + def test_cache_special_token_ids(self, mock_tokenizer): + """Test caching of special token IDs.""" + trainer = object.__new__(DiffusionTrainer) + trainer.processing_class = mock_tokenizer + trainer._cache_special_token_ids() + assert trainer._special_token_ids == {0, 1, 2} + + def test_cache_special_token_ids_no_tokenizer(self): + """Test caching when no tokenizer is available.""" + trainer = object.__new__(DiffusionTrainer) + trainer.processing_class = None + trainer._cache_special_token_ids() + + assert trainer._special_token_ids == set() + + def test_main_compute_loss_interface(self, diffusion_trainer_instance): + """Test the main compute_loss interface.""" + # Mock model + mock_model = Mock() + mock_outputs = Mock() + mock_outputs.logits = torch.randn(1, 5, 1000) + mock_model.return_value = mock_outputs + mock_model.training = True + + inputs = { + "input_ids": torch.tensor([[1, 10, 20, 30, 2]], dtype=torch.long), + "attention_mask": torch.tensor([[1, 1, 1, 1, 1]], dtype=torch.long), + "labels": torch.tensor([[-100, -100, 20, 30, 2]], dtype=torch.long), + } + + # Test without return_outputs + loss = diffusion_trainer_instance.compute_loss(mock_model, inputs) + assert isinstance(loss, torch.Tensor) + + # Test with return_outputs + loss, outputs = diffusion_trainer_instance.compute_loss( + mock_model, inputs, return_outputs=True + ) + assert isinstance(loss, torch.Tensor) + assert outputs == mock_outputs + + def test_missing_input_ids_raises_error(self, diffusion_trainer_instance): + """Test that missing input_ids raises ValueError.""" + mock_model = Mock() + inputs = {"attention_mask": torch.tensor([[1, 1, 1]])} + + with pytest.raises(ValueError, match="input_ids is required"): + diffusion_trainer_instance.compute_loss(mock_model, inputs) diff --git a/tests/integrations/test_diffusion_callback.py b/tests/integrations/test_diffusion_callback.py new file mode 100644 index 0000000000..3e8785fe04 --- /dev/null +++ b/tests/integrations/test_diffusion_callback.py @@ -0,0 +1,92 @@ +"""Tests for diffusion generation callback dataloader selection and triggering.""" + +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +from axolotl.integrations.diffusion import DiffusionGenerationCallback + + +class DummyTrainer: + """Minimal trainer double with required attributes/methods for the callback.""" + + def __init__(self, use_eval: bool): + # Config used by callback + self.cfg = SimpleNamespace( + diffusion=SimpleNamespace( + generation_interval=1, + num_generation_samples=1, + generation_max_length=32, + generation_steps=4, + generation_temperature=0.0, + mask_token_id=16, + ), + use_wandb=False, + ) + + # Model/tokenizer are passed through to generate_samples; not used here + self.model = Mock() + self.processing_class = Mock() + + # Datasets and loaders + self.eval_dataset = object() if use_eval else None + self._train_loader = object() + self._eval_loader = object() + + # State for world process check + self.state = SimpleNamespace(is_world_process_zero=True) + + # Track which loader was requested + self.requested: list[str] = [] + + def get_train_dataloader(self): + self.requested.append("train") + return self._train_loader + + def get_eval_dataloader(self): + self.requested.append("eval") + return self._eval_loader + + +@pytest.mark.parametrize("use_eval", [False, True]) +def test_callback_uses_correct_dataloader(monkeypatch, use_eval): + trainer = DummyTrainer(use_eval=use_eval) + callback = DiffusionGenerationCallback(trainer) + + captured = {} + + # Patch generate_samples in the callback module's namespace + def fake_generate_samples(**kwargs): + captured["dataloader"] = kwargs.get("dataloader") + # Return one dummy sample to exercise logging path + return [ + { + "original": "o", + "masked": "m", + "generated": "g", + "mask_ratio": 0.5, + "masked_tokens": 1, + "total_tokens": 2, + } + ] + + monkeypatch.setattr( + "axolotl.integrations.diffusion.callbacks.generate_samples", + fake_generate_samples, + ) + + # Trigger at step 1 (interval=1) + args = SimpleNamespace() + state = SimpleNamespace(global_step=1) + control = SimpleNamespace() + + callback.on_step_end(args=args, state=state, control=control) + + # Assert the expected dataloader path was used + if use_eval: + assert trainer.requested[0] == "eval" + assert captured["dataloader"] is trainer._eval_loader + else: + assert trainer.requested[0] == "train" + assert captured["dataloader"] is trainer._train_loader diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 54acbb5e4d..2c1f9f936d 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -5,12 +5,12 @@ from datasets import IterableDataset -from axolotl.utils.dict import DictDefault +from axolotl.utils.config import validate_config from axolotl.utils.data.sft import ( _prepare_streaming_dataset, prepare_datasets, ) -from axolotl.utils.config import validate_config +from axolotl.utils.dict import DictDefault class TestStreamingConfig(unittest.TestCase): From 9406c0c488277ef9d7152568b9fda50600c4221e Mon Sep 17 00:00:00 2001 From: salman Date: Thu, 11 Sep 2025 11:19:30 +0100 Subject: [PATCH 0962/1405] log before eval step (#3148) [skip-ci] --- src/axolotl/core/trainers/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 3427a0b86f..627f8e3f8b 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -371,6 +371,11 @@ def compute_loss( num_items_in_batch=num_items_in_batch, ) + @override + def evaluate(self, *args, **kwargs): + LOG.info("Running evaluation step...") + return super().evaluate(*args, **kwargs) + @staticmethod def orpo_concatenate_inputs(inputs, label_pad_token=-100, pad_token=0, device=None): concatenated_batch = {} From fcfc13d7106fe965054e46f0ad6b4f478cc5ba7c Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 12 Sep 2025 14:45:18 +0700 Subject: [PATCH 0963/1405] feat(doc): update thinking and chat_template notes (#3114) [skip ci] * feat: update thinking and chat_template notes * fix: grammar --- examples/gpt-oss/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/gpt-oss/README.md b/examples/gpt-oss/README.md index 0aa04a71cb..fb6c67498a 100644 --- a/examples/gpt-oss/README.md +++ b/examples/gpt-oss/README.md @@ -106,6 +106,16 @@ See [Nanobit/text-tools-2k-test](https://huggingface.co/datasets/Nanobit/text-to Refer to [our docs](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#using-tool-use) for more info. +### Thinking and chat_template masking conflict + +OpenAI’s Harmony template hides `thinking` in all non-final turns, which conflicts with Axolotl’s `chat_template` masking. + +If your dataset has `thinking` content mid-turn, there are two paths we recommend: + +- Train only on the last turn. This can be accomplished via chat_template's [train on last doc](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#training-on-last-message). + +- Adjust your dataset to only have `thinking` content in the last turn. + ### TIPS - Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). From 0401a15888e480d03c7cd0fb6439b27e0dacd3a0 Mon Sep 17 00:00:00 2001 From: salman Date: Fri, 12 Sep 2025 10:55:11 +0100 Subject: [PATCH 0964/1405] SEO go brrr (#3153) [skip-ci] --- CITATION.cff | 2 +- README.md | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index e6ecc7cb81..7bbfeec64d 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,6 +1,6 @@ cff-version: 1.2.0 type: software -title: "Axolotl: Post-Training for AI Models" +title: "Axolotl: Open Source LLM Post-Training" message: "If you use this software, please cite it as below." authors: - name: "Axolotl maintainers and contributors" diff --git a/README.md b/README.md index d4794124a3..1a033acd98 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ Axolotl

+

+ A Free and Open Source LLM Fine-tuning Framework
+

GitHub License @@ -50,20 +53,21 @@ ## ✨ Overview -Axolotl is a tool designed to streamline post-training for various AI models. +Axolotl is a free and open-source tool designed to streamline post-training and fine-tuning for the latest large language models (LLMs). Features: -- **Multiple Model Support**: Train various models like LLaMA, Mistral, Mixtral, Pythia, and more. We are compatible with HuggingFace transformers causal language models. -- **Training Methods**: Full fine-tuning, LoRA, QLoRA, GPTQ, QAT, Preference Tuning (DPO, IPO, KTO, ORPO), RL (GRPO), Multimodal, and Reward Modelling (RM) / Process Reward Modelling (PRM). -- **Easy Configuration**: Re-use a single YAML file between dataset preprocess, training, evaluation, quantization, and inference. +- **Multiple Model Support**: Train various models like GPT-OSS, LLaMA, Mistral, Mixtral, Pythia, and many more models available on the Hugging Face Hub. +- **Multimodal Training**: Fine-tune vision-language models (VLMs) including LLaMA-Vision, Qwen2-VL, Pixtral, LLaVA, SmolVLM2, and audio models like Voxtral with image, video, and audio support. +- **Training Methods**: Full fine-tuning, LoRA, QLoRA, GPTQ, QAT, Preference Tuning (DPO, IPO, KTO, ORPO), RL (GRPO), and Reward Modelling (RM) / Process Reward Modelling (PRM). +- **Easy Configuration**: Re-use a single YAML configuration file across the full fine-tuning pipeline: dataset preprocessing, training, evaluation, quantization, and inference. - **Performance Optimizations**: [Multipacking](https://docs.axolotl.ai/docs/multipack.html), [Flash Attention](https://github.com/Dao-AILab/flash-attention), [Xformers](https://github.com/facebookresearch/xformers), [Flex Attention](https://pytorch.org/blog/flexattention/), [Liger Kernel](https://github.com/linkedin/Liger-Kernel), [Cut Cross Entropy](https://github.com/apple/ml-cross-entropy/tree/main), [Sequence Parallelism (SP)](https://docs.axolotl.ai/docs/sequence_parallelism.html), [LoRA optimizations](https://docs.axolotl.ai/docs/lora_optims.html), [Multi-GPU training (FSDP1, FSDP2, DeepSpeed)](https://docs.axolotl.ai/docs/multi-gpu.html), [Multi-node training (Torchrun, Ray)](https://docs.axolotl.ai/docs/multi-node.html), and many more! - **Flexible Dataset Handling**: Load from local, HuggingFace, and cloud (S3, Azure, GCP, OCI) datasets. - **Cloud Ready**: We ship [Docker images](https://hub.docker.com/u/axolotlai) and also [PyPI packages](https://pypi.org/project/axolotl/) for use on cloud platforms and local hardware. -## 🚀 Quick Start +## 🚀 Quick Start - LLM Fine-tuning in Minutes **Requirements**: @@ -160,7 +164,7 @@ If you use Axolotl in your research or projects, please cite it as follows: ```bibtex @software{axolotl, - title = {Axolotl: Post-Training for AI Models}, + title = {Axolotl: Open Source LLM Post-Training}, author = {{Axolotl maintainers and contributors}}, url = {https://github.com/axolotl-ai-cloud/axolotl}, license = {Apache-2.0}, From 58d67bf98ddca63cb082374a04f8b2250ffc2c59 Mon Sep 17 00:00:00 2001 From: salman Date: Fri, 12 Sep 2025 10:55:50 +0100 Subject: [PATCH 0965/1405] Migrate QAT API; fix `axolotl quantize` for QAT-ed models; add NVFP4 (#3107) --- .github/workflows/multi-gpu-e2e.yml | 2 +- .github/workflows/tests.yml | 2 +- docs/quantize.qmd | 8 + examples/llama-3/3b-qat-fsdp2-nvfp4.yaml | 64 ++++ examples/llama-3/3b-qat-fsdp2.yaml | 18 +- requirements.txt | 2 +- setup.py | 1 + src/axolotl/cli/args.py | 1 + src/axolotl/cli/quantize.py | 50 ++- src/axolotl/train.py | 19 +- src/axolotl/utils/quantization.py | 210 +++++++------ src/axolotl/utils/schemas/enums.py | 27 +- src/axolotl/utils/schemas/quantization.py | 54 ++-- tests/e2e/test_qat.py | 4 +- tests/e2e/test_quantization.py | 363 ++++++++++++---------- tests/e2e/utils.py | 30 ++ 16 files changed, 535 insertions(+), 320 deletions(-) create mode 100644 examples/llama-3/3b-qat-fsdp2-nvfp4.yaml diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 6492e5d3ec..05f9e0761e 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -44,7 +44,7 @@ jobs: cuda_version: 12.8.1 python_version: "3.11" pytorch: 2.8.0 - axolotl_extras: + axolotl_extras: fbgemm-gpu num_gpus: 2 nightly_build: "true" runs-on: [self-hosted, modal] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 337230d4a5..cfd2c715d7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -304,7 +304,7 @@ jobs: pytorch: 2.8.0 num_gpus: 1 gpu_type: "B200" - axolotl_extras: + axolotl_extras: fbgemm-gpu steps: - name: Checkout uses: actions/checkout@v4 diff --git a/docs/quantize.qmd b/docs/quantize.qmd index 113fcafbed..43c817a5bb 100644 --- a/docs/quantize.qmd +++ b/docs/quantize.qmd @@ -51,3 +51,11 @@ axolotl quantize qat.yml ``` This ensures that an identical quantization configuration is used to quantize the model as was used to train it. + + +::: {.callout-note} + +If you have configured pushing to hub with `hub_model_id`, your model hub name will have the quantization schema appended to it, +e.g. `axolotl-ai-cloud/qat-nvfp4-llama3B` will become `axolotl-ai-cloud/qat-nvfp4-llama3B-nvfp4w` + +::: diff --git a/examples/llama-3/3b-qat-fsdp2-nvfp4.yaml b/examples/llama-3/3b-qat-fsdp2-nvfp4.yaml new file mode 100644 index 0000000000..1ec809bbef --- /dev/null +++ b/examples/llama-3/3b-qat-fsdp2-nvfp4.yaml @@ -0,0 +1,64 @@ +base_model: meta-llama/Llama-3.2-3B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true + +datasets: + - path: yahma/alpaca-cleaned + type: alpaca + split: train[:95%] + +output_dir: ./outputs/qat_out/ +dataset_prepared_path: ./outputs/dataset_prepared + +sequence_len: 8192 +flash_attention: true + +qat: + activation_dtype: nvfp4 + weight_dtype: nvfp4 + group_size: 16 # only group_size of 16 is supported with nvfp4 + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_checkpointing: true +gradient_accumulation_steps: 1 +micro_batch_size: 64 +num_epochs: 1 +optimizer: adamw_torch_fused + +cosine_constant_lr_ratio: 0 +cosine_min_lr_ratio: 1.0 +learning_rate: 2e-5 +save_only_model: true +bf16: true + +resume_from_checkpoint: +logging_steps: 1 + +evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 +weight_decay: 0.0 + +special_tokens: + pad_token: <|finetune_right_pad_id|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/llama-3/3b-qat-fsdp2.yaml b/examples/llama-3/3b-qat-fsdp2.yaml index 35e3461e2a..0c5a87891a 100644 --- a/examples/llama-3/3b-qat-fsdp2.yaml +++ b/examples/llama-3/3b-qat-fsdp2.yaml @@ -15,20 +15,18 @@ liger_glu_activation: true liger_layer_norm: true liger_fused_linear_cross_entropy: true + datasets: - path: yahma/alpaca-cleaned type: alpaca + split: train[:95%] output_dir: ./outputs/qat_out/ +dataset_prepared_path: ./outputs/qat_out/dataset_prepared -sample_packing: true - -sequence_len: 512 - -flex_attention: true -flex_attn_compile_kwargs: - dynamic: false - mode: max-autotune-no-cudagraphs +sample_packing: false +sequence_len: 8192 +flash_attention: true qat: activation_dtype: int8 @@ -67,7 +65,7 @@ fsdp: fsdp_config: fsdp_version: 2 fsdp_offload_params: false - fsdp_cpu_ram_efficient_loading: true + fsdp_cpu_ram_efficient_loading: false fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer fsdp_state_dict_type: FULL_STATE_DICT @@ -76,6 +74,6 @@ fsdp_config: fsdp_activation_checkpointing: true special_tokens: - pad_token: <|end_of_text|> + pad_token: <|finetune_right_pad_id|> # save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/requirements.txt b/requirements.txt index 1292a179a4..6138707afd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -64,7 +64,7 @@ langdetect==1.0.9 immutabledict==4.2.0 antlr4-python3-runtime==4.13.2 -torchao==0.12.0 +torchao==0.13.0 schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.6 diff --git a/setup.py b/setup.py index 4cbc562e00..3a44f0ae9e 100644 --- a/setup.py +++ b/setup.py @@ -162,6 +162,7 @@ def get_package_version(): "llmcompressor": [ "llmcompressor==0.5.1", ], + "fbgemm-gpu": ["fbgemm-gpu-genai>=1.2.0"], } install_requires, dependency_links, extras_require_build = parse_requirements( extras_require diff --git a/src/axolotl/cli/args.py b/src/axolotl/cli/args.py index 396e9a8af8..14dafa43f1 100644 --- a/src/axolotl/cli/args.py +++ b/src/axolotl/cli/args.py @@ -115,6 +115,7 @@ class QuantizeCliArgs: quantize_embedding: Optional[bool] = field(default=None) group_size: Optional[int] = field(default=None) output_dir: Optional[str] = field(default=None) + hub_model_id: Optional[str] = field(default=None) @dataclass diff --git a/src/axolotl/cli/quantize.py b/src/axolotl/cli/quantize.py index b8a8de7812..6838f47d8e 100644 --- a/src/axolotl/cli/quantize.py +++ b/src/axolotl/cli/quantize.py @@ -5,12 +5,17 @@ from pathlib import Path from typing import Union -from transformers import AutoModelForCausalLM +from transformers import AutoConfig, AutoModelForCausalLM, TorchAoConfig from axolotl.cli.config import load_cfg from axolotl.loaders import load_tokenizer from axolotl.utils.logging import get_logger -from axolotl.utils.quantization import TorchIntDType, quantize_model_for_ptq +from axolotl.utils.quantization import ( + TorchAOQuantDType, + get_quantization_config, + quantization_config_to_str, + quantize_model, +) LOG = get_logger(__name__) @@ -43,13 +48,13 @@ def do_quantize( "No quantization configuration found. Please specify either qat or quantization in your config file." ) - model_path = cli_args.get("model_path") or cfg.output_dir + model_path = cli_args.get("base_model") or cfg.output_dir if weight_dtype := cli_args.get("weight_dtype"): - weight_dtype = TorchIntDType[weight_dtype] + weight_dtype = TorchAOQuantDType.from_string(weight_dtype) else: weight_dtype = quantize_cfg.weight_dtype if activation_dtype := cli_args.get("activation_dtype"): - activation_dtype = TorchIntDType[activation_dtype] + activation_dtype = TorchAOQuantDType.from_string(activation_dtype) else: activation_dtype = quantize_cfg.activation_dtype group_size = cli_args.get("group_size") or quantize_cfg.group_size @@ -57,10 +62,15 @@ def do_quantize( cli_args.get("quantize_embedding") or quantize_cfg.quantize_embedding ) output_dir = cli_args.get("output_dir") or cfg.output_dir + hub_model_id = cli_args.get("hub_model_id") or cfg.hub_model_id - LOG.info(f"Loading model from {model_path}...") + LOG.info(f"Loading model from {model_path}.") tokenizer = load_tokenizer(cfg) - model = AutoModelForCausalLM.from_pretrained(model_path, device_map="auto") + config = AutoConfig.from_pretrained(model_path) + torch_dtype = config.torch_dtype if hasattr(config, "torch_dtype") else None + model = AutoModelForCausalLM.from_pretrained( + model_path, device_map="auto", torch_dtype=torch_dtype + ) LOG.info( f"Quantizing model with configuration: \n" @@ -70,11 +80,21 @@ def do_quantize( f"\tquantize_embedding: {quantize_embedding}" ) - quantize_model_for_ptq( + quantize_model( model, weight_dtype, group_size, activation_dtype, quantize_embedding ) - LOG.info(f"Saving quantized model to: {str(Path(output_dir) / 'quantized')}...") + quantization_config = get_quantization_config( + weight_dtype, activation_dtype, group_size + ) + + ao_config = TorchAoConfig( + quant_type=quantization_config, + include_input_output_embeddings=quantize_embedding, + ) + model.config.quantization_config = ao_config + + LOG.info(f"Saving quantized model to: {str(Path(output_dir) / 'quantized')}.") model.save_pretrained( str(Path(output_dir) / "quantized"), safe_serialization=False, @@ -86,4 +106,14 @@ def do_quantize( progressbar=True, save_jinja_files=cfg.tokenizer_save_jinja_files, ) - LOG.info(f"Quantized model saved to: {str(Path(output_dir) / 'quantized')}...") + + if hub_model_id: + hub_model_id = ( + hub_model_id.rstrip("-") + + f"-{quantization_config_to_str[type(quantization_config)]}" + ) + model.push_to_hub(hub_model_id, safe_serialization=False) + tokenizer.push_to_hub(hub_model_id) + LOG.info(f"Quantized model pushed to: {hub_model_id}.") + + LOG.info(f"Quantized model saved to: {str(Path(output_dir) / 'quantized')}.") diff --git a/src/axolotl/train.py b/src/axolotl/train.py index e8e3145795..b0482bb1e8 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -30,11 +30,7 @@ fix_untrained_tokens, ) from axolotl.integrations.base import PluginManager -from axolotl.loaders import ( - ModelLoader, - load_processor, - load_tokenizer, -) +from axolotl.loaders import ModelLoader, load_processor, load_tokenizer from axolotl.utils.ctx_managers.sequence_parallel import SequenceParallelContextManager from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import cleanup_distributed @@ -234,16 +230,15 @@ def save_trained_model( # handle QAT if cfg.qat: - from axolotl.utils.quantization import convert_qat_model_for_ptq + from axolotl.utils.quantization import convert_qat_model - LOG.info("Processing QAT model for saving...") - convert_qat_model_for_ptq( + convert_qat_model( model, quantize_embedding=cfg.qat.quantize_embedding, ) LOG.info( - "QAT modules have been converted for PTQ. Please ensure you quantize " - "your model weights with `axolotl quantize`." + "QAT usage note: please ensure you quantize your model fine-tuned using QAT by running `axolotl quantize`" + " with the same config which you used for training." ) # Handle ReLoRA early return case if cfg.relora: @@ -337,9 +332,7 @@ def save_trained_model( if hasattr(cfg, "llmcompressor") and cfg.llmcompressor: # TODO: add integration support so this can be implemented completely within the plugin - from axolotl.integrations.llm_compressor.utils import ( - save_compressed_model, - ) + from axolotl.integrations.llm_compressor.utils import save_compressed_model save_compressed_model( model=model, diff --git a/src/axolotl/utils/quantization.py b/src/axolotl/utils/quantization.py index f9a30b660e..6c29a54429 100644 --- a/src/axolotl/utils/quantization.py +++ b/src/axolotl/utils/quantization.py @@ -3,30 +3,47 @@ """ import torch -from torch import nn +from packaging import version from torchao.core.config import AOBaseConfig from torchao.quantization import quantize_ from torchao.quantization.qat import ( - FakeQuantizeConfig, - FromIntXQuantizationAwareTrainingConfig, - IntXQuantizationAwareTrainingConfig, + QATConfig, ) from torchao.quantization.quant_api import ( - Int4DynamicActivationInt4WeightConfig, - Int4WeightOnlyConfig, + Float8DynamicActivationFloat8WeightConfig, + Float8DynamicActivationInt4WeightConfig, Int8DynamicActivationInt4WeightConfig, - Int8DynamicActivationInt8WeightConfig, - Int8WeightOnlyConfig, - UIntXWeightOnlyConfig, - _is_linear, ) -from axolotl.utils.schemas.enums import TorchIntDType +from axolotl.utils.schemas.enums import TorchAOQuantDType +quantization_config_to_str = { + Int8DynamicActivationInt4WeightConfig: "int8int4", + Float8DynamicActivationFloat8WeightConfig: "fp8fp8", + Float8DynamicActivationInt4WeightConfig: "fp8int4", +} -def get_ptq_config( - weight_dtype: TorchIntDType, - activation_dtype: TorchIntDType | None = None, +if version.parse(torch.__version__) >= version.parse("2.8.0"): + try: + from torchao.prototype.mx_formats import NVFP4InferenceConfig + + quantization_config_to_str[NVFP4InferenceConfig] = "nvfp4" + except: + pass + + # int4 weight config imports will fail on machines with fbgemm-gpu installed + # without a CUDA runtime available so we do this safely + try: + from torchao.quantization.quant_api import Int4WeightOnlyConfig + + quantization_config_to_str[Int4WeightOnlyConfig] = "int4" + except: + pass + + +def get_quantization_config( + weight_dtype: TorchAOQuantDType, + activation_dtype: TorchAOQuantDType | None = None, group_size: int | None = None, ) -> AOBaseConfig: """ @@ -45,50 +62,67 @@ def get_ptq_config( or if the group size is not specified for int8 or int4 weight only quantization. """ if activation_dtype is None: - if not weight_dtype.value.is_signed: # type: ignore[attr-defined,union-attr] - return UIntXWeightOnlyConfig( - dtype=weight_dtype.value, - group_size=group_size, - set_inductor_config=False, - ) - if weight_dtype == TorchIntDType.int8: - if group_size is None: - raise ValueError( - "group_size must be specified for int8 weight only quantization" - ) - return Int8WeightOnlyConfig( - group_size=group_size, - ) - if weight_dtype == TorchIntDType.int4: - if group_size is None: - raise ValueError( - "group_size must be specified for int4 weight only quantization" - ) - return Int4WeightOnlyConfig( - group_size=group_size, - ) - if activation_dtype == TorchIntDType.int4 and weight_dtype == TorchIntDType.int4: - return Int4DynamicActivationInt4WeightConfig() - if activation_dtype == TorchIntDType.int8 and weight_dtype == TorchIntDType.int8: - return Int8DynamicActivationInt8WeightConfig() - if activation_dtype == TorchIntDType.int8 and weight_dtype == TorchIntDType.int4: - return Int8DynamicActivationInt4WeightConfig() + if weight_dtype == TorchAOQuantDType.int8: + raise ValueError("Int8WeightOnlyConfig is not supported by torchao QAT.") + if weight_dtype == TorchAOQuantDType.int4: + from torchao.quantization.quant_api import Int4WeightOnlyConfig + + if group_size is not None: + return Int4WeightOnlyConfig(group_size=group_size, version=2) + else: + return Int4WeightOnlyConfig(version=2) + if ( + activation_dtype == TorchAOQuantDType.int4 + and weight_dtype == TorchAOQuantDType.int4 + ): + raise ValueError( + "Int4DynamicActivationInt4WeightConfig is not supported by torchao QAT." + ) + if ( + activation_dtype == TorchAOQuantDType.int8 + and weight_dtype == TorchAOQuantDType.int8 + ): + raise ValueError( + "Int8DynamicActivationInt8WeightConfig is not supported by torchao QAT." + ) + if ( + activation_dtype == TorchAOQuantDType.int8 + and weight_dtype == TorchAOQuantDType.int4 + ): + if group_size is not None: + return Int8DynamicActivationInt4WeightConfig(group_size=group_size) + else: + return Int8DynamicActivationInt4WeightConfig() + if ( + activation_dtype == TorchAOQuantDType.float8_e4m3fn + and weight_dtype == TorchAOQuantDType.float8_e4m3fn + ): + return Float8DynamicActivationFloat8WeightConfig() + if ( + activation_dtype == TorchAOQuantDType.float8_e4m3fn + and weight_dtype == TorchAOQuantDType.int4 + ): + return Float8DynamicActivationInt4WeightConfig() + if weight_dtype == TorchAOQuantDType.nvfp4: + from torchao.prototype.mx_formats import NVFP4InferenceConfig + + if group_size is not None and group_size != 16: + raise ValueError("NVFP4 quantization must use a group_size of 16") + return NVFP4InferenceConfig() raise ValueError( f"Invalid activation/weight dtype combination: {activation_dtype}/{weight_dtype}" ) -def prepare_model_for_qat( +def quantize_model( model, - weight_dtype: TorchIntDType, - group_size: int, - activation_dtype: TorchIntDType | None = None, - quantize_embedding: bool = False, + weight_dtype: TorchAOQuantDType, + group_size: int | None = None, + activation_dtype: TorchAOQuantDType | None = None, + quantize_embedding: bool | None = None, ): """ - This function is used to prepare a model for QAT by swapping the model's linear - layers with fake quantized linear layers, and optionally the embedding weights with - fake quantized embedding weights. + This function is used to quantize a model. Args: model: The model to quantize. @@ -97,24 +131,19 @@ def prepare_model_for_qat( activation_dtype: The dtype to use for activation quantization. quantize_embedding: Whether to quantize the model's embedding weights. - Raises: - ValueError: If the activation/weight dtype combination is invalid. """ - if activation_dtype: - activation_config = FakeQuantizeConfig( - dtype=activation_dtype.value, granularity="per_token", is_symmetric=False - ) - weight_config = FakeQuantizeConfig(dtype=weight_dtype.value, group_size=group_size) - linear_quantize_config = IntXQuantizationAwareTrainingConfig( - activation_config=None if activation_dtype is None else activation_config, - weight_config=weight_config, + linear_ptq_config = get_quantization_config( + weight_dtype=weight_dtype, + activation_dtype=activation_dtype, + group_size=group_size, ) - quantize_(model, linear_quantize_config) + quantize_(model, linear_ptq_config) if quantize_embedding: # activation fake quantization is not supported for embedding layers - embedding_quantize_config = IntXQuantizationAwareTrainingConfig( - activation_config=None, - weight_config=weight_config, + embedding_quantize_config = get_quantization_config( + weight_dtype=weight_dtype, + activation_dtype=None, + group_size=group_size, ) quantize_( model, @@ -123,17 +152,17 @@ def prepare_model_for_qat( ) -def quantize_model_for_ptq( +def prepare_model_for_qat( model, - weight_dtype: TorchIntDType, + weight_dtype: TorchAOQuantDType, group_size: int | None = None, - activation_dtype: TorchIntDType | None = None, - quantize_embedding: bool | None = None, + activation_dtype: TorchAOQuantDType | None = None, + quantize_embedding: bool = False, ): """ - This function is used to quantize a model for post-training quantization. - It swaps the model's linear layers with fake quantized linear layers. - If `quantize_embedding` is True, it will also swap the model's embedding weights with fake quantized embedding weights. + This function is used to prepare a model for QAT by swapping the model's linear + layers with fake quantized linear layers, and optionally the embedding weights with + fake quantized embedding weights. Args: model: The model to quantize. @@ -142,44 +171,43 @@ def quantize_model_for_ptq( activation_dtype: The dtype to use for activation quantization. quantize_embedding: Whether to quantize the model's embedding weights. + Raises: + ValueError: If the activation/weight dtype combination is invalid. """ - linear_ptq_config = get_ptq_config( + base_config = get_quantization_config( weight_dtype=weight_dtype, activation_dtype=activation_dtype, group_size=group_size, ) - quantize_(model, linear_ptq_config) + qat_config = QATConfig(base_config) + quantize_(model, qat_config) if quantize_embedding: - embedding_quantize_config = get_ptq_config( + # activation fake quantization is not supported for embedding layers + embedding_base_config = get_quantization_config( weight_dtype=weight_dtype, activation_dtype=None, group_size=group_size, ) + embedding_qat_config = QATConfig(embedding_base_config) quantize_( model, - embedding_quantize_config, + embedding_qat_config, filter_fn=lambda m, _: isinstance(m, torch.nn.Embedding), ) -def convert_qat_model_for_ptq( +def convert_qat_model( model, - *, - quantize_embedding: bool | None = None, + quantize_embedding: bool = False, ): """ - This function is used to convert a swap fake-quantized modules in a model - which has been trained with QAT back to the original modules, ready for PTQ. - - Args: - model: The model to convert. - quantize_embedding: Whether to quantize the model's embedding weights. + This function converts a QAT model which has fake quantized layers back to the original model. """ + config = QATConfig(step="convert") + quantize_(model, config) if quantize_embedding: - - def filter_fn(m, _): - return isinstance(m, nn.Embedding) or _is_linear(m) - - else: - filter_fn = _is_linear - quantize_(model, FromIntXQuantizationAwareTrainingConfig(), filter_fn=filter_fn) + quantize_( + model, + config, + filter_fn=lambda m, _: isinstance(m, torch.nn.Embedding), + ) diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 8f4718aa96..bcd03e1a29 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -5,18 +5,21 @@ import torch -class TorchIntDType(Enum): - """Torch integer data types - `getattr` guards against torch < 2.6 which does not support int4""" - - uint1 = getattr(torch, "uint1", None) - uint2 = getattr(torch, "uint2", None) - uint3 = getattr(torch, "uint3", None) - uint4 = getattr(torch, "uint4", None) - uint5 = getattr(torch, "uint5", None) - uint6 = getattr(torch, "uint6", None) - uint7 = getattr(torch, "uint7", None) - int4 = getattr(torch, "int4", None) - int8 = getattr(torch, "int8", None) +class TorchAOQuantDType(Enum): + int4 = torch.int4 + int8 = torch.int8 + float8_e4m3fn = torch.float8_e4m3fn + nvfp4 = "nvfp4" + + def from_string(str): + if str == "int4": + return TorchAOQuantDType.int4 + if str == "int8": + return TorchAOQuantDType.int8 + if str in ["float8_e4m3fn", "fp8", "float8"]: + return TorchAOQuantDType.float8_e4m3fn + if str == "nvfp4": + return TorchAOQuantDType.nvfp4 class RLType(str, Enum): diff --git a/src/axolotl/utils/schemas/quantization.py b/src/axolotl/utils/schemas/quantization.py index 090640c7bc..a7c1305741 100644 --- a/src/axolotl/utils/schemas/quantization.py +++ b/src/axolotl/utils/schemas/quantization.py @@ -6,7 +6,23 @@ from pydantic import BaseModel, Field, field_validator -from axolotl.utils.schemas.enums import TorchIntDType +from axolotl.utils.schemas.enums import TorchAOQuantDType + + +def validate_ao_dtype(v: Any) -> TorchAOQuantDType | None: + if v is None: + return None + if v == "int4": + return TorchAOQuantDType.int4 + if v == "int8": + return TorchAOQuantDType.int8 + if v in ["float8_e4m3fn", "fp8", "float8"]: + return TorchAOQuantDType.float8_e4m3fn + if v == "nvfp4": + return TorchAOQuantDType.nvfp4 + raise ValueError( + f"Invalid dtype: '{v}'. Must be one of: {[e.name for e in TorchAOQuantDType] + ['fp8', 'float8']}" + ) class QATConfig(BaseModel): @@ -14,13 +30,13 @@ class QATConfig(BaseModel): QAT Config Schema """ - activation_dtype: TorchIntDType | None = Field( + activation_dtype: TorchAOQuantDType | None = Field( default=None, - description='Fake quantization layout to use for activation quantization. Valid options are "int4" and "int8"', + description="Fake quantization layout to use for activation quantization.", ) - weight_dtype: TorchIntDType = Field( - default=TorchIntDType.int8, - description='Fake quantization layout to use for weight quantization. Valid options are "int4" and "int8"', + weight_dtype: TorchAOQuantDType = Field( + default=TorchAOQuantDType.int8, + description="Fake quantization layout to use for weight quantization.", ) quantize_embedding: bool | None = Field( default=False, description="Quantize embedding" @@ -35,12 +51,8 @@ class QATConfig(BaseModel): @field_validator("activation_dtype", "weight_dtype", mode="before") @classmethod - def validate_dtype(cls, v: Any) -> TorchIntDType | None: - if v == "int4": - return TorchIntDType.int4 - if v == "int8": - return TorchIntDType.int8 - raise ValueError(f"Invalid dtype: '{v}'. Must be one of: ['int4', 'int8']") + def validate_dtype(cls, v: Any) -> TorchAOQuantDType | None: + return validate_ao_dtype(v) class PTQConfig(BaseModel): @@ -48,13 +60,13 @@ class PTQConfig(BaseModel): PTQ Config Schema """ - weight_dtype: TorchIntDType = Field( - default=TorchIntDType.int8, - description="Fake quantization layout to use for weight quantization. Valid options are uintX for X in [1, 2, 3, 4, 5, 6, 7], or int4, or int8", + weight_dtype: TorchAOQuantDType = Field( + default=TorchAOQuantDType.int8, + description="Fake quantization layout to use for weight quantization.", ) - activation_dtype: TorchIntDType | None = Field( + activation_dtype: TorchAOQuantDType | None = Field( default=None, - description='Fake quantization layout to use for activation quantization. Valid options are "int4" and "int8"', + description="Fake quantization layout to use for activation quantization.", ) quantize_embedding: bool | None = Field( default=None, description="Whether to quantize the embedding layer." @@ -66,9 +78,5 @@ class PTQConfig(BaseModel): @field_validator("activation_dtype", "weight_dtype", mode="before") @classmethod - def validate_dtype(cls, v: Any) -> TorchIntDType | None: - if v == "int4": - return TorchIntDType.int4 - if v == "int8": - return TorchIntDType.int8 - raise ValueError(f"Invalid dtype: '{v}'. Must be one of: ['int4', 'int8']") + def validate_dtype(cls, v: Any) -> TorchAOQuantDType | None: + return validate_ao_dtype(v) diff --git a/tests/e2e/test_qat.py b/tests/e2e/test_qat.py index 7d41dfb50d..2f8398ef7c 100644 --- a/tests/e2e/test_qat.py +++ b/tests/e2e/test_qat.py @@ -43,7 +43,7 @@ def test_qat(self, temp_dir): "qat": { "quantize_embedding": True, "activation_dtype": "int8", - "weight_dtype": "int8", + "weight_dtype": "int4", "group_size": 8, }, "num_epochs": 1, @@ -111,7 +111,7 @@ def test_qat_dpo(self, temp_dir): "qat": { "quantize_embedding": True, "activation_dtype": "int8", - "weight_dtype": "int8", + "weight_dtype": "int4", "group_size": 8, }, "save_first_step": False, diff --git a/tests/e2e/test_quantization.py b/tests/e2e/test_quantization.py index cfbdfec38d..b64aef51aa 100644 --- a/tests/e2e/test_quantization.py +++ b/tests/e2e/test_quantization.py @@ -5,41 +5,40 @@ import pytest import torch from torch import nn -from torchao.dtypes.affine_quantized_tensor import AffineQuantizedTensor -from torchao.quantization.granularity import PerAxis, PerGroup -from torchao.quantization.linear_activation_quantized_tensor import ( - LinearActivationQuantizedTensor, -) +from torchao.quantization import LinearActivationQuantizedTensor from torchao.quantization.qat.embedding import FakeQuantizedEmbedding from torchao.quantization.qat.linear import FakeQuantizedLinear from torchao.quantization.quant_api import ( - Int4DynamicActivationInt4WeightConfig, - Int4WeightOnlyConfig, - Int8DynamicActivationInt8WeightConfig, - Int8WeightOnlyConfig, - UIntXWeightOnlyConfig, + Float8DynamicActivationFloat8WeightConfig, + Float8DynamicActivationInt4WeightConfig, + Int8DynamicActivationInt4WeightConfig, ) +from torchao.quantization.quantize_.workflows.int4.int4_tensor import Int4Tensor from transformers import AutoModelForCausalLM from transformers.trainer_callback import TrainerState from axolotl.utils.callbacks.qat import QATCallback from axolotl.utils.quantization import ( - convert_qat_model_for_ptq, - get_ptq_config, + convert_qat_model, + get_quantization_config, prepare_model_for_qat, - quantize_model_for_ptq, + quantize_model, ) -from axolotl.utils.schemas.enums import TorchIntDType +from axolotl.utils.schemas.enums import TorchAOQuantDType from axolotl.utils.schemas.quantization import QATConfig -from tests.e2e.utils import require_torch_2_6_0 +from tests.e2e.utils import ( + require_torch_2_8_0, + requires_cuda_ge_8_9, + requires_sm_ge_100, +) @pytest.fixture() def model(): dummy_model = AutoModelForCausalLM.from_pretrained( - "HuggingFaceTB/SmolLM2-135M", - device_map="cuda", + "Qwen/Qwen2-0.5B", + device_map="auto", torch_dtype=torch.bfloat16, ) with torch.device(dummy_model.device): @@ -48,45 +47,56 @@ def model(): dummy_model.model.embed_tokens.weight.shape[1], dtype=dummy_model.model.embed_tokens.weight.dtype, ) - return dummy_model + yield dummy_model + del dummy_model ptq_config_test_cases = [ - # weight_dtype, activation_dtype, group_size, expected_type, expected_params + # weight_dtype, activation_dtype, group_size, expected_type ( - TorchIntDType.uint4, - None, + TorchAOQuantDType.int4, + TorchAOQuantDType.int8, None, - UIntXWeightOnlyConfig, - {"dtype": torch.uint4, "group_size": None}, + Int8DynamicActivationInt4WeightConfig, ), - (TorchIntDType.int8, None, 32, Int8WeightOnlyConfig, {"group_size": 32}), - (TorchIntDType.int4, None, 4, Int4WeightOnlyConfig, {"group_size": 4}), ( - TorchIntDType.int4, - TorchIntDType.int4, + TorchAOQuantDType.float8_e4m3fn, + TorchAOQuantDType.float8_e4m3fn, None, - Int4DynamicActivationInt4WeightConfig, - {}, + Float8DynamicActivationFloat8WeightConfig, ), ( - TorchIntDType.int8, - TorchIntDType.int8, + TorchAOQuantDType.int4, + TorchAOQuantDType.float8_e4m3fn, None, - Int8DynamicActivationInt8WeightConfig, - {}, + Float8DynamicActivationInt4WeightConfig, ), ] ptq_test_cases = [ - # weight_dtype, activation_dtype, group_size, quantize_embedding, expected_exception - (TorchIntDType.int8, None, 8, False, None), - (TorchIntDType.int4, None, 4, True, None), - (TorchIntDType.uint4, None, 8, False, None), - (TorchIntDType.int4, TorchIntDType.int4, 8, False, None), - (TorchIntDType.int8, TorchIntDType.int8, 8, True, None), - (TorchIntDType.int8, None, None, False, ValueError), - (TorchIntDType.int4, None, None, False, ValueError), + # weight_dtype, activation_dtype, group_size, quantize_embedding, expected_exception, expected_tensor_class + (TorchAOQuantDType.int4, None, 4, True, None, Int4Tensor), + ( + TorchAOQuantDType.int4, + TorchAOQuantDType.int8, + 8, + False, + None, + LinearActivationQuantizedTensor, + ), + # ( + # TorchAOQuantDType.int4, + # TorchAOQuantDType.float8_e4m3fn, + # None, + # False, + # None, + # Int4Tensor, + # ), + (TorchAOQuantDType.int4, None, None, False, None, Int4Tensor), + # Deprecated configs + (TorchAOQuantDType.int8, None, 8, False, ValueError, None), + (TorchAOQuantDType.int4, TorchAOQuantDType.int4, 8, False, ValueError, None), + (TorchAOQuantDType.int8, TorchAOQuantDType.int8, 8, True, ValueError, None), ] @@ -96,44 +106,132 @@ class TestQuantization: """ @pytest.mark.parametrize( - "weight_dtype,activation_dtype,group_size,expected_type,expected_params", + "weight_dtype,activation_dtype,group_size,expected_type", ptq_config_test_cases, ) - @require_torch_2_6_0 + @requires_cuda_ge_8_9 + @require_torch_2_8_0 def test_get_ptq_config( - self, weight_dtype, activation_dtype, group_size, expected_type, expected_params + self, weight_dtype, activation_dtype, group_size, expected_type ): - config = get_ptq_config(weight_dtype, activation_dtype, group_size) - + config = get_quantization_config(weight_dtype, activation_dtype, group_size) assert isinstance(config, expected_type) - for param_name, param_value in expected_params.items(): - if isinstance(param_value, (PerAxis, PerGroup)): - if isinstance(param_value, PerAxis): - assert isinstance(getattr(config, param_name), PerAxis) - assert getattr(config, param_name).axis == param_value.axis - else: - assert isinstance(getattr(config, param_name), PerGroup) - assert ( - getattr(config, param_name).group_size == param_value.group_size - ) - else: - assert getattr(config, param_name) == param_value + @requires_cuda_ge_8_9 + @require_torch_2_8_0 + def test_get_ptq_config_int4_weight_only(self): + from torchao.quantization.quant_api import Int4WeightOnlyConfig + + config = get_quantization_config(TorchAOQuantDType.int4, None, 4) + assert isinstance(config, Int4WeightOnlyConfig) @pytest.mark.parametrize( - "weight_dtype", [TorchIntDType.int8, TorchIntDType.int4, TorchIntDType.uint4] + "weight_dtype,activation_dtype,group_size,quantize_embedding,expected_exception,expected_tensor_class", + ptq_test_cases, ) + @requires_cuda_ge_8_9 + @require_torch_2_8_0 + def test_quantize_model_for_ptq( + self, + model, + weight_dtype, + activation_dtype, + group_size, + quantize_embedding, + expected_exception, + expected_tensor_class, + ): + if expected_exception: + with pytest.raises(expected_exception): + quantize_model( + model, + weight_dtype, + group_size, + activation_dtype, + quantize_embedding, + ) + else: + quantize_model( + model, weight_dtype, group_size, activation_dtype, quantize_embedding + ) + if quantize_embedding: + assert isinstance( + model.model.embed_tokens.weight, expected_tensor_class + ), "Embedding weight should be quantized" + for child in list(model.children()): + if isinstance(child, torch.nn.Linear): + assert isinstance(child.weight, expected_tensor_class) + + @require_torch_2_8_0 + @requires_sm_ge_100 + def test_quantize_model_for_ptq_fp8( + self, + model, + ): + from torchao.quantization.quantize_.workflows.float8.float8_tensor import ( + Float8Tensor, + QuantizeTensorToFloat8Kwargs, + ) + + quantize_model( + model, + TorchAOQuantDType.float8_e4m3fn, + None, + TorchAOQuantDType.float8_e4m3fn, + ) + for child in list(model.children()): + if isinstance(child, torch.nn.Linear): + assert isinstance(child.weight, Float8Tensor) + assert child.weight.act_quant_kwargs is not None and isinstance( + child.weight.act_quant_kwargs, QuantizeTensorToFloat8Kwargs + ) + + @require_torch_2_8_0 + @requires_sm_ge_100 + def test_quantize_model_for_ptq_nvfp4( + self, + model, + ): + from torchao.prototype.mx_formats.nvfp4_tensor import ( + NVFP4Tensor, + QuantizeTensorToNVFP4Kwargs, + ) + + quantize_model(model, TorchAOQuantDType.nvfp4, 16, TorchAOQuantDType.nvfp4) + for child in list(model.children()): + if isinstance(child, torch.nn.Linear): + assert isinstance(child.weight, NVFP4Tensor) + assert child.weight.act_quant_kwargs is not None and isinstance( + child.weight.act_quant_kwargs, QuantizeTensorToNVFP4Kwargs + ) + @pytest.mark.parametrize( - "activation_dtype", [None, TorchIntDType.int4, TorchIntDType.int8] + "weight_dtype,activation_dtype,group_size,quantize_embedding", + [ + (TorchAOQuantDType.int4, None, 8, False), + (TorchAOQuantDType.int4, None, 16, True), + (TorchAOQuantDType.int4, TorchAOQuantDType.int8, 8, False), + (TorchAOQuantDType.int4, TorchAOQuantDType.int8, 16, True), + ( + TorchAOQuantDType.float8_e4m3fn, + TorchAOQuantDType.float8_e4m3fn, + None, + False, + ), + (TorchAOQuantDType.int4, TorchAOQuantDType.float8_e4m3fn, None, True), + ], ) - @pytest.mark.parametrize("group_size", [4, 8]) - @pytest.mark.parametrize("quantize_embedding", [False, True]) - @require_torch_2_6_0 + @require_torch_2_8_0 + @requires_cuda_ge_8_9 def test_prepare_model_for_qat( self, model, weight_dtype, activation_dtype, group_size, quantize_embedding ): prepare_model_for_qat( - model, weight_dtype, group_size, activation_dtype, quantize_embedding + model, + weight_dtype, + group_size, + activation_dtype, + quantize_embedding, ) if quantize_embedding: assert isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) @@ -142,17 +240,19 @@ def test_prepare_model_for_qat( model.model.embed_tokens.weight_fake_quantizer.config.dtype == weight_dtype.value ) - assert ( - model.model.embed_tokens.weight_fake_quantizer.config.group_size - == group_size - ) + if group_size: + assert ( + model.model.embed_tokens.weight_fake_quantizer.config.group_size + == group_size + ) for child in list(model.children()): if isinstance(child, torch.nn.Linear): assert isinstance(child, FakeQuantizedLinear) assert hasattr(child, "weight_fake_quantizer") assert child.weight_fake_quantizer.config.dtype == weight_dtype.value - assert child.weight_fake_quantizer.config.group_size == group_size + if group_size: + assert child.weight_fake_quantizer.config.group_size == group_size if activation_dtype: assert hasattr(child, "activation_fake_quantizer") assert ( @@ -162,49 +262,40 @@ def test_prepare_model_for_qat( else: assert child.activation_fake_quantizer is None - @pytest.mark.parametrize( - "weight_dtype,activation_dtype,group_size,quantize_embedding,expected_exception", - ptq_test_cases, - ) - @require_torch_2_6_0 - def test_quantize_model_for_ptq( - self, - model, - weight_dtype, - activation_dtype, - group_size, - quantize_embedding, - expected_exception, - ): - if expected_exception: - with pytest.raises(expected_exception): - quantize_model_for_ptq( - model, - weight_dtype, - group_size, - activation_dtype, - quantize_embedding, - ) - else: - quantize_model_for_ptq( - model, weight_dtype, group_size, activation_dtype, quantize_embedding - ) - if quantize_embedding: - assert isinstance( - model.model.embed_tokens.weight, AffineQuantizedTensor - ), "Embedding weight should be quantized" - for child in list(model.children()): - if isinstance(child, torch.nn.Linear): - if activation_dtype: - assert isinstance( - child.weight, LinearActivationQuantizedTensor - ), ( - "Linear weight should be quantized with activation quantization" - ) - else: - assert isinstance(child.weight, AffineQuantizedTensor), ( - "Linear weight should be quantized without activation quantization" - ) + @require_torch_2_8_0 + @requires_cuda_ge_8_9 + def test_convert_qat_model(self, model): + config = QATConfig( + weight_dtype="int4", + activation_dtype="int8", + group_size=8, + quantize_embedding=True, + ) + + # quantize model for qat + prepare_model_for_qat( + model, + config.weight_dtype, + config.group_size, + config.activation_dtype, + config.quantize_embedding, + ) + + assert isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) + assert isinstance(model.lm_head, FakeQuantizedLinear) + + # apply conversion + convert_qat_model( + model, + config.quantize_embedding, + ) + # ensure modules have been swapped out + assert not isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) + assert not isinstance(model.lm_head, FakeQuantizedLinear) + + # ensure weights have been quantized + assert isinstance(model.model.embed_tokens.weight, nn.Parameter) + assert isinstance(model.lm_head.weight, nn.Parameter) class TestQuantizationCallback: @@ -218,10 +309,10 @@ def trainer_state(self): global_step=0, ) - @require_torch_2_6_0 + @require_torch_2_8_0 def test_qat_callback_fake_quant_after_n_steps(self, model, trainer_state): cfg = QATConfig( - weight_dtype="int8", + weight_dtype="int4", activation_dtype="int8", group_size=8, quantize_embedding=True, @@ -268,10 +359,10 @@ def test_qat_callback_fake_quant_after_n_steps(self, model, trainer_state): assert model.model.embed_tokens.weight_fake_quantizer.enabled assert model.lm_head.weight_fake_quantizer.enabled - @require_torch_2_6_0 + @require_torch_2_8_0 def test_qat_callback_fake_quant_after_n_steps_is_none(self, model, trainer_state): cfg = QATConfig( - weight_dtype="int8", + weight_dtype="int4", activation_dtype="int8", group_size=8, quantize_embedding=True, @@ -304,43 +395,3 @@ def test_qat_callback_fake_quant_after_n_steps_is_none(self, model, trainer_stat # quantization should be enabled from the get-go assert model.model.embed_tokens.weight_fake_quantizer.enabled assert model.lm_head.weight_fake_quantizer.enabled - - -class TestConvertQATModelForPTQ: - """ - Test convert_qat_model_for_ptq - """ - - @require_torch_2_6_0 - def test_convert_qat_model_for_ptq(self, model): - config = QATConfig( - weight_dtype="int8", - activation_dtype="int8", - group_size=8, - quantize_embedding=True, - ) - - # quantize model for qat - prepare_model_for_qat( - model, - config.weight_dtype, - config.group_size, - config.activation_dtype, - config.quantize_embedding, - ) - - assert isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) - assert isinstance(model.lm_head, FakeQuantizedLinear) - - # apply conversion - convert_qat_model_for_ptq( - model, - quantize_embedding=config.quantize_embedding, - ) - # ensure modules have been swapped out - assert not isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) - assert not isinstance(model.lm_head, FakeQuantizedLinear) - - # ensure weights have been quantized - assert isinstance(model.model.embed_tokens.weight, nn.Parameter) - assert isinstance(model.lm_head.weight, nn.Parameter) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 7db6cf74ee..a2dd8bc5ef 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -90,6 +90,18 @@ def is_min_2_7_0(): return unittest.skipUnless(is_min_2_7_0(), "test requires torch>=2.7.0")(test_case) +def require_torch_2_8_0(test_case): + """ + Decorator marking a test that requires torch >= 2.7.0 + """ + + def is_min_2_8_0(): + torch_version = version.parse(torch.__version__) + return torch_version >= version.parse("2.8.0") + + return unittest.skipUnless(is_min_2_8_0(), "test requires torch>=2.8.0")(test_case) + + def require_torch_lt_2_6_0(test_case): """ Decorator marking a test that requires torch < 2.6.0 @@ -128,6 +140,24 @@ def is_llmcompressor_installed(): )(test_case) +def requires_sm_ge_100(test_case): + is_sm_ge_100 = ( + torch.cuda.is_available() + and torch.version.cuda + and torch.cuda.get_device_capability() >= (10, 0) + ) + return unittest.skipUnless(is_sm_ge_100, "test requires sm>=100")(test_case) + + +def requires_cuda_ge_8_9(test_case): + is_cuda_ge_8_9 = ( + torch.cuda.is_available() + and torch.version.cuda + and torch.cuda.get_device_capability() >= (8, 9) + ) + return unittest.skipUnless(is_cuda_ge_8_9, "test requires cuda>=8.9")(test_case) + + def is_hopper(): compute_capability = torch.cuda.get_device_capability() return compute_capability == (9, 0) From 1ef6c196f7d1cffb2010accd4f0ef716ddab405a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 16 Sep 2025 14:52:29 -0400 Subject: [PATCH 0966/1405] setup env vars for ray train for FSDP (#3130) [skip ci] --- src/axolotl/cli/train.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 5e766de37a..8d33c0b846 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -17,6 +17,7 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, resolve_dtype from axolotl.utils.dict import DictDefault +from axolotl.utils.trainer import prepare_optim_env def do_train(cfg: DictDefault, cli_args: TrainerCliArgs): @@ -92,6 +93,7 @@ def ray_train_func(kwargs: dict): # cast `cfg` back to DictDefault (ray tune deepcopy has issues with DictDefault so needed it to be dict) # also renormalize the config now that TorchTrainer has spawned distributed workers cfg = DictDefault(kwargs["cfg"]) + prepare_optim_env(cfg) normalize_config(cfg) # now that we are on the worker node, we can check `is_torch_bf16_gpu_available` to resolve dtype From d4cff1b7bbd43d546d95b31943cf2810e30efe8f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 16 Sep 2025 14:52:45 -0400 Subject: [PATCH 0967/1405] improve setting of NCCL_P2P_DISABLE on runpod (#3132) [skip ci] * improve setting of NCCL_P2P_DISABLE on runpod * use recs from review --- src/axolotl/utils/environment.py | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/axolotl/utils/environment.py b/src/axolotl/utils/environment.py index 192aca4e1a..7b23484133 100644 --- a/src/axolotl/utils/environment.py +++ b/src/axolotl/utils/environment.py @@ -2,6 +2,8 @@ utils to get GPU info for the current environment """ +import os +import subprocess # nosec B404 from importlib.metadata import version from accelerate.utils.environment import ( @@ -14,6 +16,8 @@ def check_cuda_p2p_ib_support(): if not accelerate_check_cuda_p2p_ib_support(): return False + if not check_runpod_p2p_support(): + return False unsupported_devices = {"RTX 6000 Ada", "L40S"} try: device_names, device_count = get_gpu_info() @@ -29,6 +33,39 @@ def check_cuda_p2p_ib_support(): return True +def check_runpod_p2p_support() -> bool: + if "RUNPOD_GPU_COUNT" not in os.environ: + return True + try: + gpu_count = int(os.environ.get("RUNPOD_GPU_COUNT", "1")) + except ValueError: + return True + if gpu_count >= 2: + # run `nvidia-smi topo -p2p n` and inspect the GPU0 row + try: + result = subprocess.run( # nosec B603 B607 + ["nvidia-smi", "topo", "-p2p", "n"], + check=True, + capture_output=True, + text=True, + timeout=5, + ) + except ( + subprocess.CalledProcessError, + FileNotFoundError, + subprocess.TimeoutExpired, + ): + return True # fail-open if detection fails + output_lines = result.stdout.strip().split("\n") + # filter rows that start with "GPU0" (avoid header row) + gpu0_rows = [line for line in output_lines if line.lstrip().startswith("GPU0")] + if not gpu0_rows: + return True + # consider P2P supported if any OK is present in the GPU0 row + return "OK" in gpu0_rows[-1] + return True + + def get_package_version(package: str) -> Version: version_str = version(package) return parse(version_str) From 86d6ee7c0551393dd537a2c1c5e5c6362e1b3e41 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 16 Sep 2025 14:53:01 -0400 Subject: [PATCH 0968/1405] upgrade trl and accelerate (#3161) * upgrade trl==0.23.0 * upgrade accelerate patch fix * add hints when using gradient_checkpointing with DPO * set gradient-checpointing properly --- requirements.txt | 4 ++-- src/axolotl/core/builders/base.py | 2 +- src/axolotl/utils/schemas/validation.py | 15 +++++++++++++++ tests/e2e/multigpu/test_llama.py | 4 ++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6138707afd..44a3c0277e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,10 +15,10 @@ huggingface_hub>=0.33.0 peft>=0.17.0 transformers==4.56.1 tokenizers>=0.21.1 -accelerate==1.10.0 +accelerate==1.10.1 datasets==4.0.0 deepspeed>=0.17.0 -trl==0.21.0 +trl==0.23.0 hf_xet==1.1.5 kernels==0.9.0 trackio diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 1ec818004e..3ad8012f9e 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -435,7 +435,7 @@ def _configure_gradient_checkpointing(self, training_args_kwargs: dict): # don't use the HF gradient checkpointing, manually wrap training_args_kwargs["gradient_checkpointing"] = False training_args_kwargs["activation_offloading"] = True - elif self.cfg.gradient_checkpointing: + elif self.cfg.gradient_checkpointing is not None: training_args_kwargs["gradient_checkpointing"] = ( self.cfg.gradient_checkpointing ) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 64018ca486..9671b10ae2 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1378,6 +1378,21 @@ def validate_ring_attn_func(self): return self + def hint_gradient_checkpointing_dpo_lora_ddp(self): + if ( + (self.gradient_checkpointing is True or self.gradient_checkpointing is None) + and self.capabilities + and self.capabilities.get("n_gpu", 1) > 1 + and self.adapter in ("lora", "qlora") + and self.rl == RLType.DPO + and not self.fsdp + and not self.deepspeed + ): + LOG.warning( + "gradient_checkpointing with DPO + DDP + LoRA is not recommended." + ) + return self + class DistributedValidationMixin: """validation for distributed training.""" diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index ad15d628be..c16ef0c603 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -199,7 +199,7 @@ def test_dpo_lora_ddp(self, temp_dir): "max_steps": 2, "micro_batch_size": 2, "gradient_accumulation_steps": 2, - # "gradient_checkpointing": True, + "gradient_checkpointing": False, "output_dir": temp_dir, "dataset_prepared_path": temp_dir + "/last_run_prepared", "warmup_steps": 0, @@ -278,7 +278,7 @@ def test_dpo_qlora_ddp(self, temp_dir): "max_steps": 2, "micro_batch_size": 2, "gradient_accumulation_steps": 2, - # "gradient_checkpointing": True, + "gradient_checkpointing": False, "output_dir": temp_dir, "dataset_prepared_path": temp_dir + "/last_run_prepared", "warmup_steps": 0, From e5c427f6dee386ad2f1fa4cbe855e6de637b5662 Mon Sep 17 00:00:00 2001 From: salman Date: Wed, 17 Sep 2025 10:38:15 +0100 Subject: [PATCH 0969/1405] qat doc updates (#3162) [skip-ci] --- docs/qat.qmd | 11 +++++++++-- docs/quantize.qmd | 7 +++---- .../{3b-qat-fsdp2-nvfp4.yaml => 3b-qat-nvfp4.yaml} | 0 3 files changed, 12 insertions(+), 6 deletions(-) rename examples/llama-3/{3b-qat-fsdp2-nvfp4.yaml => 3b-qat-nvfp4.yaml} (100%) diff --git a/docs/qat.qmd b/docs/qat.qmd index e0d000a794..ad97790666 100644 --- a/docs/qat.qmd +++ b/docs/qat.qmd @@ -23,10 +23,17 @@ To enable QAT in axolotl, add the following to your configuration file: ```yaml qat: - activation_dtype: # Optional[str] = "int8". Fake quantization layout to use for activation quantization. Valid options are "int4" and "int8" - weight_dtype: # Optional[str] = "int8". Fake quantization layout to use for weight quantization. Valid options are "int4" and "int8" + activation_dtype: # Optional[str] = "int8". Fake quantization layout to use for activation quantization. Valid options are "int4", "int8", "float8" + weight_dtype: # Optional[str] = "int8". Fake quantization layout to use for weight quantization. Valid options are "int4", "fp8", and "nvfp4". group_size: # Optional[int] = 32. The number of elements in each group for per-group fake quantization fake_quant_after_n_steps: # Optional[int] = None. The number of steps to apply fake quantization after ``` +We support the following quantization schemas: +- `Int4WeightOnly` (requires the `fbgemm-gpu` extra when installing Axolotl) +- `Int8DynamicActivationInt4Weight` +- `Float8DynamicActivationFloat8Weight` +- `Float8DynamicActivationInt4Weight` +- `NVFP4` + Once you have finished training, you must quantize your model by using the same quantization configuration which you used to train the model with. You can use the [`quantize`](./quantize.qmd) command to do this. diff --git a/docs/quantize.qmd b/docs/quantize.qmd index 43c817a5bb..9c3de1ef1f 100644 --- a/docs/quantize.qmd +++ b/docs/quantize.qmd @@ -22,8 +22,8 @@ Quantization is configured using the `quantization` key in your configuration fi ```yaml base_model: # The path to the model to quantize. quantization: - weight_dtype: # Optional[str] = "int8". Fake quantization layout to use for weight quantization. Valid options are uintX for X in [1, 2, 3, 4, 5, 6, 7], or int4, or int8 - activation_dtype: # Optional[str] = "int8". Fake quantization layout to use for activation quantization. Valid options are "int4" and "int8" + activation_dtype: # Optional[str] = "int8". Fake quantization layout to use for activation quantization. Valid options are "int4", "int8", "float8" + weight_dtype: # Optional[str] = "int8". Fake quantization layout to use for weight quantization. Valid options are "int4", "fp8", and "nvfp4". group_size: # Optional[int] = 32. The number of elements in each group for per-group fake quantization quantize_embedding: # Optional[bool] = False. Whether to quantize the embedding layer. @@ -39,9 +39,8 @@ you used to train the model: # qat.yml qat: activation_dtype: int8 - weight_dtype: int8 + weight_dtype: int4 group_size: 256 - quantize_embedding: true output_dir: # The path to the output directory used during training where the final checkpoint has been saved. ``` diff --git a/examples/llama-3/3b-qat-fsdp2-nvfp4.yaml b/examples/llama-3/3b-qat-nvfp4.yaml similarity index 100% rename from examples/llama-3/3b-qat-fsdp2-nvfp4.yaml rename to examples/llama-3/3b-qat-nvfp4.yaml From 4065bc14c616e12c4da037c01de8a0defd9e7c10 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 17 Sep 2025 13:27:03 -0400 Subject: [PATCH 0970/1405] Debug log, logging improvements (#3159) * simplify logging * remove comment * progress on debug.log * add debug-level logger for file log * simplify * case insensitivity; 3rd party logging improvements * simplify * fix * tests * lint * nits * nit * Update tests/test_utils_tee.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * cleanup / comments * fix * oops --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .gitignore | 3 + .pre-commit-config.yaml | 2 +- .../colab-axolotl-example.ipynb | 6 +- pyproject.toml | 2 +- src/axolotl/cli/__init__.py | 4 +- src/axolotl/cli/config.py | 9 +- src/axolotl/cli/inference.py | 2 - src/axolotl/cli/main.py | 4 +- src/axolotl/cli/train.py | 1 - src/axolotl/cli/utils/diffusion.py | 1 - src/axolotl/logging_config.py | 75 +++++--- .../transformers/trainer_loss_calc.py | 6 +- src/axolotl/train.py | 3 +- src/axolotl/utils/__init__.py | 9 - src/axolotl/utils/logging.py | 7 +- src/axolotl/utils/tee.py | 166 ++++++++++++++++++ src/axolotl/utils/train.py | 4 +- src/axolotl/utils/trainer.py | 9 - tests/test_logging_config_file_capture.py | 103 +++++++++++ tests/test_utils_tee.py | 107 +++++++++++ 20 files changed, 454 insertions(+), 69 deletions(-) create mode 100644 src/axolotl/utils/tee.py create mode 100644 tests/test_logging_config_file_capture.py create mode 100644 tests/test_utils_tee.py diff --git a/.gitignore b/.gitignore index 40084b4086..b75becc7c1 100644 --- a/.gitignore +++ b/.gitignore @@ -190,3 +190,6 @@ out/ # vim *.swp + +# scm auto-versioning +src/axolotl/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9c80898ff4..92ddc7f41e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: rev: v0.12.12 hooks: - id: ruff - args: [--fix, --select, I] + args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.17.1 diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 0e6ba984ef..774b78b82f 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -251,10 +251,10 @@ }, "outputs": [], "source": [ - "from axolotl.utils import patch_optimized_env\n", + "from axolotl.utils import set_pytorch_cuda_alloc_conf\n", "\n", - "# speedup downloads from HF 🤗 and set \"PYTORCH_CUDA_ALLOC_CONF\" env to save memory\n", - "patch_optimized_env()" + "# Set \"PYTORCH_CUDA_ALLOC_CONF\" env to save memory\n", + "set_pytorch_cuda_alloc_conf()" ] }, { diff --git a/pyproject.toml b/pyproject.toml index 932219d9ec..4213bc9633 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ line-length = 88 target-version = "py310" [tool.ruff.lint] -select = ["E", "F", "W", "C90", "B"] +select = ["E", "F", "W", "C90", "B", "I"] ignore = [ "E203", # Whitespace before ':' "E501", # Line too long diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 8955eca3e9..fa647be651 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -4,5 +4,7 @@ from axolotl.logging_config import configure_logging -os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1" +os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") +os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1") + configure_logging() diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index 20e341a0b4..93ac6147d1 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -23,7 +23,8 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger from axolotl.utils.mlflow_ import setup_mlflow_env_vars -from axolotl.utils.trainer import prepare_opinionated_env, prepare_optim_env +from axolotl.utils.tee import prepare_debug_log +from axolotl.utils.trainer import prepare_optim_env from axolotl.utils.wandb_ import setup_wandb_env_vars LOG = get_logger(__name__) @@ -227,8 +228,11 @@ def load_cfg( }, ) + # NOTE(djsaunde): We start outputting to output_dir/debug.log at this point since we + # have to wait for cfg.output to be resolved. We could call this earlier if we write + # to a temporary file, and then move it later. + prepare_debug_log(cfg) prepare_optim_env(cfg) - prepare_opinionated_env(cfg) normalize_config(cfg) normalize_cfg_datasets(cfg) setup_wandb_env_vars(cfg) @@ -241,7 +245,6 @@ def load_cfg( for k, v in cfg.items() if v is not None } - LOG.info( "config:\n%s", json.dumps(cfg_to_log, indent=2, default=str, sort_keys=True), diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index 30d4077134..3e1c015205 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -17,8 +17,6 @@ from axolotl.cli.utils.diffusion import ( diffusion_inference, launch_diffusion_gradio_ui, - render_html, - run_diffusion, ) from axolotl.integrations.base import PluginManager from axolotl.utils.chat_templates import get_chat_template_from_config diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index acfa81389b..dc6cca489d 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -26,7 +26,7 @@ launch_training, ) from axolotl.integrations.lm_eval.cli import lm_eval -from axolotl.utils import patch_optimized_env +from axolotl.utils import set_pytorch_cuda_alloc_conf from axolotl.utils.logging import get_logger from axolotl.utils.schemas.config import AxolotlInputConfig @@ -44,7 +44,7 @@ def cli(): """Axolotl CLI - Train and fine-tune large language models""" print_axolotl_text_art() load_dotenv() - patch_optimized_env() + set_pytorch_cuda_alloc_conf() @cli.command() diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 8d33c0b846..2332717e77 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -60,7 +60,6 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): config: Path to `axolotl` config YAML file. kwargs: Additional keyword arguments to override config file values. """ - parsed_cfg = load_cfg(config, **kwargs) parser = HfArgumentParser(TrainerCliArgs) parsed_cli_args, _ = parser.parse_args_into_dataclasses( diff --git a/src/axolotl/cli/utils/diffusion.py b/src/axolotl/cli/utils/diffusion.py index f83d9077bb..1157bfd66f 100644 --- a/src/axolotl/cli/utils/diffusion.py +++ b/src/axolotl/cli/utils/diffusion.py @@ -3,7 +3,6 @@ from __future__ import annotations import gradio as gr -import torch from colorama import Fore, Style from axolotl.integrations.diffusion import generate, resolve_mask_token_id diff --git a/src/axolotl/logging_config.py b/src/axolotl/logging_config.py index 10c5ae9dc3..67b1d32f18 100644 --- a/src/axolotl/logging_config.py +++ b/src/axolotl/logging_config.py @@ -1,10 +1,7 @@ -""" -Common logging module for axolotl -""" +"""Common logging module for axolotl.""" import logging import os -import sys from logging import Formatter, Logger, LogRecord from logging.config import dictConfig from typing import Any, Dict @@ -17,9 +14,9 @@ class AxolotlOrWarnErrorFilter(logging.Filter): """ - Allows ANY WARNING or higher (unless overridden by LOG_LEVEL) - Allows axolotl.* at INFO or higher (unless overridden by AXOLOTL_LOG_LEVEL) - Drops all other records (i.e. non-axolotl.INFO, DEBUG, etc. by default) + Allows ANY WARNING or higher (unless overridden by LOG_LEVEL). Allows axolotl.* at + INFO or higher (unless overridden by AXOLOTL_LOG_LEVEL). Drops all other records + (i.e. non-axolotl.INFO, DEBUG, etc. by default). """ def __init__(self, **kwargs): @@ -52,13 +49,12 @@ def filter(self, record: LogRecord) -> bool: class AxolotlLogger(Logger): - """A Logger that automatically rejects non-axolotl INFOs.""" + """Logger that applies filtering to non-axolotl loggers.""" def __init__(self, name: str, level: int = logging.NOTSET): super().__init__(name, level) - - # set global filter on the logger itself - self.addFilter(AxolotlOrWarnErrorFilter()) + if not name.startswith("axolotl"): + self.addFilter(AxolotlOrWarnErrorFilter()) class ColorfulFormatter(Formatter): @@ -74,6 +70,7 @@ class ColorfulFormatter(Formatter): def format(self, record): record.rank = int(os.getenv("LOCAL_RANK", "0")) + record.rank_fmt = f" [RANK:{record.rank}]" if record.rank != 0 else "" log_message = super().format(record) return self.COLORS.get(record.levelname, "") + log_message + Fore.RESET @@ -87,32 +84,54 @@ def format(self, record): }, "colorful": { "()": ColorfulFormatter, - "format": "[%(asctime)s] [%(levelname)s] [%(name)s.%(funcName)s:%(lineno)d] [PID:%(process)d] [RANK:%(rank)d] %(message)s", + "format": "[%(asctime)s] [%(levelname)s] [%(name)s.%(funcName)s:%(lineno)d] [PID:%(process)d]%(rank_fmt)s %(message)s", + }, + "concise": { + "format": "[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s", + }, + "concise_color": { + "()": ColorfulFormatter, + "format": "[%(asctime)s] [%(levelname)s] [%(name)s]%(rank_fmt)s %(message)s", + }, + }, + "filters": { + "ax_or_warn": { + "()": "axolotl.logging_config.AxolotlOrWarnErrorFilter", }, }, - "filters": {}, "handlers": { "console": { "class": "logging.StreamHandler", - "formatter": "simple", - "filters": [], - "stream": sys.stdout, + "formatter": "concise", + "filters": ["ax_or_warn"], + "stream": "ext://sys.stdout", }, "color_console": { "class": "logging.StreamHandler", - "formatter": "colorful", - "filters": [], - "stream": sys.stdout, + "formatter": "concise_color", + "filters": ["ax_or_warn"], + "stream": "ext://sys.stdout", + }, + "ax_file_only": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://axolotl.utils.tee.file_only_stream", + }, + "root_file_only": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://axolotl.utils.tee.file_only_stream", }, }, - # log level will be superseded by the AxolotlLogger "root": { - "handlers": ["console"], - "level": os.getenv("LOG_LEVEL", DEFAULT_LOG_LEVEL), + "handlers": ["console", "root_file_only"], + "level": os.getenv("LOG_LEVEL", DEFAULT_LOG_LEVEL).upper(), }, "loggers": { "axolotl": { - "handlers": ["color_console"], + "handlers": ["color_console", "ax_file_only"], "level": os.getenv("AXOLOTL_LOG_LEVEL", DEFAULT_AXOLOTL_LOG_LEVEL).upper(), "propagate": False, }, @@ -123,9 +142,15 @@ def format(self, record): def configure_logging(): """Configure with default logging""" init() # Initialize colorama + dictConfig(DEFAULT_LOGGING_CONFIG) logging.setLoggerClass(AxolotlLogger) - # set default `ACCELERATE_LOG_LEVEL` to `LOG_LEVEL` if available and not set + # Route Python warnings through logging so they reach file handlers + logging.captureWarnings(True) + + # Set default `ACCELERATE_LOG_LEVEL` to `LOG_LEVEL` if available and not set if "ACCELERATE_LOG_LEVEL" not in os.environ: - os.environ["ACCELERATE_LOG_LEVEL"] = os.getenv("LOG_LEVEL", DEFAULT_LOG_LEVEL) + os.environ["ACCELERATE_LOG_LEVEL"] = os.getenv( + "LOG_LEVEL", DEFAULT_LOG_LEVEL + ).upper() diff --git a/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py b/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py index c9b968d719..b8172bbe6c 100644 --- a/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py +++ b/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py @@ -41,7 +41,7 @@ def patch_evaluation_loop(): """Patch the evaluation_loop method.""" # Check if already patched if hasattr(Trainer, "_original_evaluation_loop"): - LOG.info("Trainer.evaluation_loop already patched") + LOG.debug("Trainer.evaluation_loop already patched") return # Check if the patterns exist @@ -84,7 +84,7 @@ def patch_evaluation_loop(): ) exec(evaluation_loop_source, globals()) - LOG.info("Patched Trainer.evaluation_loop with nanmean loss calculation") + LOG.debug("Patched Trainer.evaluation_loop with nanmean loss calculation") Trainer.evaluation_loop = axolotl_evaluation_loop @@ -135,5 +135,5 @@ def patch_maybe_log_save_evaluate(): ) exec(maybe_log_source, globals()) - LOG.info("Patched Trainer._maybe_log_save_evaluate with nanmean loss calculation") + LOG.debug("Patched Trainer._maybe_log_save_evaluate with nanmean loss calculation") Trainer._maybe_log_save_evaluate = axolotl_maybe_log_save_evaluate diff --git a/src/axolotl/train.py b/src/axolotl/train.py index b0482bb1e8..2a70d9712b 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -196,10 +196,11 @@ def execute_training( ) ) - LOG.info("Starting trainer...") # TODO: disabling for now as not compatible with FSDP2 + torchao low bit optimizers # if cfg.bf16: # torch.set_default_dtype(torch.bfloat16) + + LOG.info("Starting trainer...") trainer.train(resume_from_checkpoint=resume_from_checkpoint) plugin_manager = PluginManager.get_instance() diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index e5050116a8..7256a57005 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -44,15 +44,6 @@ def set_pytorch_cuda_alloc_conf(): ) -def patch_optimized_env(): - """ - Patch environment variables to improve VRAM usage and increase download speed - """ - if os.getenv("HF_HUB_ENABLE_HF_TRANSFER") is None: - os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1" - set_pytorch_cuda_alloc_conf() - - def get_not_null(value, default=None): """ return the value if it's not None, otherwise return the default value diff --git a/src/axolotl/utils/logging.py b/src/axolotl/utils/logging.py index 7cc3530ae5..35810897a3 100644 --- a/src/axolotl/utils/logging.py +++ b/src/axolotl/utils/logging.py @@ -2,7 +2,6 @@ import functools import logging -import os from axolotl.utils.distributed import is_main_process @@ -40,10 +39,6 @@ def warning_once(self, *args, **kwargs): def get_logger(name: str, log_level: str | None = None) -> MultiProcessAdapter: - if log_level is None: - log_level = os.environ.get("AXOLOTL_LOG_LEVEL", None) logger = logging.getLogger(name) - if log_level is not None: - logger.setLevel(log_level.upper()) - logger.root.setLevel(log_level.upper()) + logger.setLevel(logging.DEBUG) return MultiProcessAdapter(logger, extra={}) diff --git a/src/axolotl/utils/tee.py b/src/axolotl/utils/tee.py new file mode 100644 index 0000000000..1209ad1dd5 --- /dev/null +++ b/src/axolotl/utils/tee.py @@ -0,0 +1,166 @@ +""" +Utilities for managing the debug log file and providing a file-only stream for logging +handlers. +""" + +from __future__ import annotations + +import io +import os +import sys +import threading +from pathlib import Path +from typing import TextIO, cast + +_lock = threading.Lock() +_file_handle: io.TextIOWrapper | None = None +_log_path: str | None = None +_tee_installed: bool = False +_orig_stdout: TextIO | None = None +_orig_stderr: TextIO | None = None + + +class _FileOnlyWriter(io.TextIOBase): + """A stream-like object that writes only to the tee file. + + Before the file is prepared, writes are dropped (no-op). + """ + + def write(self, s: str) -> int: # type: ignore[override] + with _lock: + if _file_handle is not None: + _file_handle.write(s) + return len(s) + return len(s) + + def flush(self) -> None: # type: ignore[override] + with _lock: + if _file_handle is not None: + try: + _file_handle.flush() + except Exception: + pass + + +file_only_stream: io.TextIOBase = _FileOnlyWriter() + + +class _StreamTee(io.TextIOBase): + """A minimal tee that mirrors writes to the debug log file. + + Installed only after the debug log is prepared; no buffering. + """ + + def __init__(self, stream: io.TextIOBase): + self._stream = stream + + def write(self, s: str) -> int: # type: ignore[override] + with _lock: + n = self._stream.write(s) + if _file_handle is not None: + _file_handle.write(s) + return n + + def flush(self) -> None: # type: ignore[override] + with _lock: + self._stream.flush() + if _file_handle is not None: + try: + _file_handle.flush() + except Exception: + pass + + @property + def encoding(self): # type: ignore[override] + return getattr(self._stream, "encoding", None) + + @property + def errors(self): # type: ignore[override] + return getattr(self._stream, "errors", None) + + def isatty(self): # type: ignore[override] + return getattr(self._stream, "isatty", lambda: False)() + + def fileno(self): # type: ignore[override] + if hasattr(self._stream, "fileno"): + return self._stream.fileno() + raise OSError("Underlying stream has no fileno") + + +def prepare_debug_log(cfg, filename: str = "debug.log") -> str: + """ + Prepare the debug log. + + Creates the output directory, handles append/truncate logic based on cfg, and opens + the debug log file for subsequent writes via file-only handlers. + """ + global _file_handle, _log_path, _tee_installed + + with _lock: + # If already initialized, reuse existing path + if _log_path is not None: + return _log_path + + output_dir = cfg.output_dir + os.makedirs(output_dir, exist_ok=True) + + log_path = Path(output_dir) / filename + append = bool( + cfg.get("resume_from_checkpoint") or cfg.get("auto_resume_from_checkpoints") + ) + + if not append and log_path.exists(): + log_path.unlink() + + fh = open(log_path, "a", encoding="utf-8") + fh.flush() + + _file_handle = fh + _log_path = str(log_path) + + # Install a tee so stdout/stderr are mirrored to the debug file + # Allow disabling via env for testing or advanced usage. + tee_enabled = os.getenv("AXOLOTL_TEE_STDOUT", "1").lower() not in { + "0", + "false", + "no", + } + if tee_enabled and not _tee_installed: + # Save originals so we can restore later (e.g., tests) + global _orig_stdout, _orig_stderr + _orig_stdout = sys.stdout + _orig_stderr = sys.stderr + sys.stdout = _StreamTee(cast(io.TextIOBase, sys.stdout)) + sys.stderr = _StreamTee(cast(io.TextIOBase, sys.stderr)) + _tee_installed = True + + return _log_path + + +def close_debug_log() -> None: + """Flush and close the debug log and uninstall the stdout/stderr tee. + + Safe to call even if not initialized. + """ + global _file_handle, _log_path, _tee_installed, _orig_stdout, _orig_stderr + with _lock: + # Restore original stdout/stderr if we installed a tee + if _tee_installed: + if _orig_stdout is not None: + sys.stdout = _orig_stdout + if _orig_stderr is not None: + sys.stderr = _orig_stderr + _tee_installed = False + _orig_stdout = None + _orig_stderr = None + + # Close the file handle if open + if _file_handle is not None: + try: + _file_handle.flush() + _file_handle.close() + except Exception: + pass + finally: + _file_handle = None + _log_path = None diff --git a/src/axolotl/utils/train.py b/src/axolotl/utils/train.py index 1393459d9b..ad3f72be45 100644 --- a/src/axolotl/utils/train.py +++ b/src/axolotl/utils/train.py @@ -31,6 +31,7 @@ def determine_last_checkpoint(cfg: DictDefault, update: bool = True) -> str | No if checkpoints: last_checkpoint = str(checkpoints[-1]) if not update: + LOG.info(f"Resuming from last checkpoint at {last_checkpoint}") return last_checkpoint if ( @@ -40,6 +41,7 @@ def determine_last_checkpoint(cfg: DictDefault, update: bool = True) -> str | No ): cfg.resume_from_checkpoint = last_checkpoint LOG.info( - f"Using Auto-resume functionality to start with checkpoint at {cfg.resume_from_checkpoint}" + "Using auto-resume functionality to resume from checkpoint at " + f"{cfg.resume_from_checkpoint}" ) return cfg.resume_from_checkpoint diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index a0f4fd567c..662a54655f 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -655,15 +655,6 @@ def prepare_optim_env(cfg): os.environ["ACCELERATE_MIXED_PRECISION"] = "no" -def prepare_opinionated_env(cfg): - if cfg.qlora_sharded_model_loading: - # model loading is forked after the tokenizer - os.environ["TOKENIZERS_PARALLELISM"] = "false" - if cfg.sample_packing: - # multipack parallel packing sampler defaults to using fork - os.environ["TOKENIZERS_PARALLELISM"] = "false" - - def setup_trainer( cfg, train_dataset, diff --git a/tests/test_logging_config_file_capture.py b/tests/test_logging_config_file_capture.py new file mode 100644 index 0000000000..44b0ee5e62 --- /dev/null +++ b/tests/test_logging_config_file_capture.py @@ -0,0 +1,103 @@ +import logging +import tempfile + +import pytest + + +def read(path: str) -> str: + with open(path, "r", encoding="utf-8") as f: + return f.read() + + +@pytest.fixture(autouse=True) +def _reset_logging_state(): + # Ensure a clean slate for logging between tests + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + logging.shutdown() + # Note: dictConfig in configure_logging will set up handlers again + yield + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + logging.shutdown() + + +def test_axolotl_logs_captured_at_all_levels(monkeypatch): + from axolotl.logging_config import configure_logging + from axolotl.utils import tee + from axolotl.utils.logging import get_logger + + with tempfile.TemporaryDirectory() as td: + # Avoid stdout tee in this test to simplify interaction with pytest capture + monkeypatch.setenv("AXOLOTL_TEE_STDOUT", "0") + configure_logging() + path = tee.prepare_debug_log( + type("Cfg", (), {"output_dir": td, "get": lambda *_: False}) + ) + + log = get_logger("axolotl.test") + log.info("AX-INFO") + log.debug("AX-DEBUG") + tee.file_only_stream.flush() + + data = read(path) + assert "AX-INFO" in data + assert "AX-DEBUG" in data + tee.close_debug_log() + + +def test_third_party_logs_filtered_and_warning_captured(monkeypatch): + from axolotl.logging_config import configure_logging + from axolotl.utils import tee + + with tempfile.TemporaryDirectory() as td: + monkeypatch.setenv("AXOLOTL_TEE_STDOUT", "0") + configure_logging() + path = tee.prepare_debug_log( + type("Cfg", (), {"output_dir": td, "get": lambda *_: False}) + ) + + # Third-party logger (non-axolotl) + other = logging.getLogger("thirdparty.lib") + other.info("TP-INFO") + other.warning("TP-WARN") + + # Simulate Python warnings routed through logging + logging.getLogger("py.warnings").warning("PY-WARN") + + # Push through buffers + tee.file_only_stream.flush() + + data = read(path) + # INFO from non-axolotl should be filtered out (not present) + assert "TP-INFO" not in data + # WARNING+ should be present + assert "TP-WARN" in data + # Python warnings captured (via py.warnings logger) + assert "PY-WARN" in data + tee.close_debug_log() + tee.close_debug_log() + + +def test_prepare_debug_log_idempotent_and_no_duplicate(monkeypatch): + from axolotl.logging_config import configure_logging + from axolotl.utils import tee + from axolotl.utils.logging import get_logger + + with tempfile.TemporaryDirectory() as td: + monkeypatch.setenv("AXOLOTL_TEE_STDOUT", "0") + configure_logging() + cfg = type("Cfg", (), {"output_dir": td, "get": lambda *_: False}) + p1 = tee.prepare_debug_log(cfg) + p2 = tee.prepare_debug_log(cfg) + assert p1 == p2 + + log = get_logger("axolotl.test") + marker = "UNIQUE-MARKER-12345" + log.info(marker) + tee.file_only_stream.flush() + + data = read(p1) + # Ensure the marker appears once (not duplicated via propagation) + assert data.count(marker) == 1 + tee.close_debug_log() diff --git a/tests/test_utils_tee.py b/tests/test_utils_tee.py new file mode 100644 index 0000000000..e2c1536672 --- /dev/null +++ b/tests/test_utils_tee.py @@ -0,0 +1,107 @@ +import os +import tempfile + + +def _dummy_cfg(output_dir: str, append: bool = False): + # Minimal object with attributes used by prepare_debug_log + class Cfg: + def __init__(self, out, append): + self.output_dir = out + self._append = append + + def get(self, key, default=None): + if key in {"resume_from_checkpoint", "auto_resume_from_checkpoints"}: + return self._append + return default + + return Cfg(output_dir, append) + + +def read(path: str) -> str: + with open(path, "r", encoding="utf-8") as f: + return f.read() + + +def test_file_only_stream_writes_after_prepare(monkeypatch): + from axolotl.utils import tee + + with tempfile.TemporaryDirectory() as td: + # Avoid stdout tee in this test + monkeypatch.setenv("AXOLOTL_TEE_STDOUT", "0") + cfg = _dummy_cfg(td, append=False) + + # before prepare: writing to file_only_stream creates no file + tee.file_only_stream.write("before\n") + tee.file_only_stream.flush() + assert not os.path.exists(os.path.join(td, "debug.log")) + + # prepare and write + path = tee.prepare_debug_log(cfg) + assert os.path.basename(path) == "debug.log" + tee.file_only_stream.write("hello\n") + tee.file_only_stream.flush() + + content = read(path) + assert "hello" in content + + tee.close_debug_log() + + +def test_stdout_is_mirrored_after_prepare(capsys, monkeypatch): + from axolotl.utils import tee + + with tempfile.TemporaryDirectory() as td: + cfg = _dummy_cfg(td, append=False) + try: + # Install tee while capture is disabled so stdout tee wraps real stdout. + with capsys.disabled(): + monkeypatch.setenv("AXOLOTL_TEE_STDOUT", "1") + path = tee.prepare_debug_log(cfg) + import sys + + print("printed-line") + sys.stdout.flush() + + # Now verify file contains the line + content = read(path) + assert "printed-line" in content + finally: + tee.close_debug_log() + + +def test_truncate_vs_append_behavior(monkeypatch): + from axolotl.utils import tee + + with tempfile.TemporaryDirectory() as td: + # Avoid stdout tee in this test + monkeypatch.setenv("AXOLOTL_TEE_STDOUT", "0") + # First run creates file with A + cfg = _dummy_cfg(td, append=False) + _ = tee.prepare_debug_log(cfg) + try: + tee.file_only_stream.write("A\n") + tee.file_only_stream.flush() + finally: + tee.close_debug_log() + + # Second run with append=False truncates + cfg2 = _dummy_cfg(td, append=False) + path2 = tee.prepare_debug_log(cfg2) + try: + tee.file_only_stream.write("B\n") + tee.file_only_stream.flush() + content = read(path2) + assert "A\n" not in content and "B\n" in content + finally: + tee.close_debug_log() + + # Third run with append=True preserves existing + cfg3 = _dummy_cfg(td, append=True) + path3 = tee.prepare_debug_log(cfg3) + try: + tee.file_only_stream.write("C\n") + tee.file_only_stream.flush() + content = read(path3) + assert "B\n" in content and "C\n" in content + finally: + tee.close_debug_log() From 09959fac70e7a31c2feee78e841d71cba1d3b411 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 18 Sep 2025 15:42:20 +0700 Subject: [PATCH 0971/1405] Feat: add Magistral Small 2509 and native mistral3 tokenizer support (#3165) * feat: update mistral common * feat: add mistral3processor * fix: loading * fix: cast pixel_values to fp32 * fix: image tensor conversion * feat: add FA2 support for pixtral based models * fix: update mistral small 3.1 to use native tokenizer * fix: install tips * fix: improve info on sample dataset files * chore: move mistral configs into subfolders * fix: remove unneeded patch * fix: indent * feat: add integration tests * chore: move * feat: add magistral 2509 docs and example * fix: convert tensor to bool * feat: expand tests * chore: move tests --- docs/multimodal.qmd | 15 +- examples/gemma3n/README.md | 10 +- examples/magistral/README.md | 30 +--- examples/magistral/think/README.md | 73 ++++++++ .../magistral-small-think-qlora.yaml | 0 examples/magistral/vision/README.md | 60 +++++++ .../magistral-small-vision-24B-qlora.yml | 64 +++++++ .../{ => bigstral}/bigstral-ds-zero3.yaml | 0 .../mistral/{ => dpo}/mistral-dpo-qlora.yml | 0 .../mistral-small-3.1-24B-lora.yml | 14 +- .../mixtral-8x22b-qlora-fsdp.yml | 0 .../{ => mixtral}/mixtral-qlora-fsdp.yml | 0 examples/mistral/{ => mixtral}/mixtral.yml | 0 examples/mistral/{ => mixtral}/mixtral_22.yml | 0 examples/mistral/{ => mps}/lora-mps.yml | 0 .../mistral/{ => orpo}/mistral-qlora-orpo.yml | 0 examples/pixtral/lora-12b.yml | 3 +- examples/voxtral/README.md | 9 +- requirements.txt | 2 +- src/axolotl/loaders/patch_manager.py | 14 ++ src/axolotl/loaders/processor.py | 7 + src/axolotl/loaders/tokenizer.py | 5 - .../monkeypatch/models/mistral3/__init__.py | 0 .../mistral3/mistral_common_tokenizer.py | 85 +++++++++ .../monkeypatch/models/pixtral/__init__.py | 0 .../pixtral/modeling_flash_attention_utils.py | 42 +++++ src/axolotl/processing_strategies.py | 36 ++++ src/axolotl/utils/mistral/__init__.py | 3 +- .../utils/mistral/mistral3_processor.py | 169 ++++++++++++++++++ .../test_mistral_tokenizer_patch.py | 35 ++++ .../test_pixtral_flash_attention_patch.py | 77 ++++++++ .../test_voxtral_modeling_patch.py | 43 +++++ 32 files changed, 757 insertions(+), 39 deletions(-) create mode 100644 examples/magistral/think/README.md rename examples/magistral/{ => think}/magistral-small-think-qlora.yaml (100%) create mode 100644 examples/magistral/vision/README.md create mode 100644 examples/magistral/vision/magistral-small-vision-24B-qlora.yml rename examples/mistral/{ => bigstral}/bigstral-ds-zero3.yaml (100%) rename examples/mistral/{ => dpo}/mistral-dpo-qlora.yml (100%) rename examples/mistral/{ => mistral-small}/mistral-small-3.1-24B-lora.yml (78%) rename examples/mistral/{ => mixtral}/mixtral-8x22b-qlora-fsdp.yml (100%) rename examples/mistral/{ => mixtral}/mixtral-qlora-fsdp.yml (100%) rename examples/mistral/{ => mixtral}/mixtral.yml (100%) rename examples/mistral/{ => mixtral}/mixtral_22.yml (100%) rename examples/mistral/{ => mps}/lora-mps.yml (100%) rename examples/mistral/{ => orpo}/mistral-qlora-orpo.yml (100%) create mode 100644 src/axolotl/monkeypatch/models/mistral3/__init__.py create mode 100644 src/axolotl/monkeypatch/models/mistral3/mistral_common_tokenizer.py create mode 100644 src/axolotl/monkeypatch/models/pixtral/__init__.py create mode 100644 src/axolotl/monkeypatch/models/pixtral/modeling_flash_attention_utils.py create mode 100644 src/axolotl/utils/mistral/mistral3_processor.py create mode 100644 tests/monkeypatch/test_mistral_tokenizer_patch.py create mode 100644 tests/monkeypatch/test_pixtral_flash_attention_patch.py create mode 100644 tests/monkeypatch/test_voxtral_modeling_patch.py diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index d839ce211c..413404195c 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -13,6 +13,7 @@ format: - [Pixtral](#sec-pixtral) - [Llava-1.5](#sec-llava-15) - [Mistral-Small-3.1](#sec-mistral-small-31) +- [Magistral-Small-2509](#sec-magistral-small-2509) - [Voxtral](#sec-voxtral) - [Gemma-3](#sec-gemma-3) - [Gemma-3n](#sec-gemma-3n) @@ -94,10 +95,22 @@ chat_template: llava ### Mistral-Small-3.1 {#sec-mistral-small-31} +::: {.callout-tip} +Please make sure to install vision lib via `pip install 'mistral-common[opencv]==1.8.5'` +::: + ```yaml base_model: mistralai/Mistral-Small-3.1-24B-Instruct-2503 +``` + +### Magistral-Small-2509 {#sec-magistral-small-2509} -chat_template: mistral_v7_tekken +::: {.callout-tip} +Please make sure to install vision lib via `pip install 'mistral-common[opencv]==1.8.5'` +::: + +```yaml +base_model: mistralai/Magistral-Small-2509 ``` ### Voxtral {#sec-voxtral} diff --git a/examples/gemma3n/README.md b/examples/gemma3n/README.md index 8c4e02a1d8..ff3946c906 100644 --- a/examples/gemma3n/README.md +++ b/examples/gemma3n/README.md @@ -23,7 +23,15 @@ pip3 install timm==1.0.17 pip3 install librosa==0.11.0 ``` -3. Run the finetuning example: +3. Download sample dataset files + +```bash +# for text + vision + audio only +wget https://huggingface.co/datasets/Nanobit/text-vision-audio-2k-test/resolve/main/African_elephant.jpg +wget https://huggingface.co/datasets/Nanobit/text-vision-audio-2k-test/resolve/main/En-us-African_elephant.oga +``` + +4. Run the finetuning example: ```bash # text only diff --git a/examples/magistral/README.md b/examples/magistral/README.md index f4f2782088..a09138744a 100644 --- a/examples/magistral/README.md +++ b/examples/magistral/README.md @@ -1,10 +1,10 @@ # Finetune Magistral Small with Axolotl -Magistral Small is a 24B parameter opensource model from MistralAI found on HuggingFace at [2506](https://huggingface.co/mistralai/Magistral-Small-2506) and [2507](https://huggingface.co/mistralai/Magistral-Small-2507) (see [Thinking](#thinking)). This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. +Magistral Small is a 24B parameter opensource model from MistralAI found on HuggingFace at [2506](https://huggingface.co/mistralai/Magistral-Small-2506), [2507](https://huggingface.co/mistralai/Magistral-Small-2507) (see [Thinking](#thinking)), and [2509](https://huggingface.co/mistralai/Magistral-Small-2509) (see [Vision](#vision)). This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. MistralAI has also released a proprietary medium-sized version called Magistral Medium. -Thanks to the team at MistralAI for giving us early access to prepare for this release. +Thanks to the team at MistralAI for giving us early access to prepare for these releases. ## Getting started @@ -36,29 +36,17 @@ Let us know how it goes. Happy finetuning! 🚀 ### Thinking -MistralAI has released their [2507](https://huggingface.co/mistralai/Magistral-Small-2507) model with thinking capabilities. The model requires the multi-content dataset format with support for an extra `role: thinking` within system and assistant messages. +MistralAI has released their [2507](https://huggingface.co/mistralai/Magistral-Small-2507) model with thinking capabilities, enabling Chain-of-Thought reasoning with explicit thinking steps. -Example format: +📚 **[See the Thinking fine-tuning guide →](./think/README.md)** -```json -{ - "messages": [ - {"role": "system", "content": [{ "type": "text", "text": "{SYSTEM_PROMPT}"}]}, - {"role": "user", "content": [{ "type": "text", "text": "..."}]}, - {"role": "assistant", "content": [{ "type": "thinking", "thinking": "..."}, { "type": "text", "text": "..." }]}, - ], -} -``` - -Example config: `./magistral-small-think-qlora.yaml`. +### Vision -The `thinking` section also supports an optional arg `closed: bool` (`True` default) which controls adding the closing `[/THINK]` tag. +MistralAI has released their [2509](https://huggingface.co/mistralai/Magistral-Small-2509) model with vision capabilities. -Limitations: -- You cannot mix `content: str` with `content: list[dict]` as the `dataset.load_dataset` may complain about different types for `content` key. -- This mode does not work with custom `train_detail` and `training` at the moment. +📚 **[See the Vision fine-tuning guide →](./vision/README.md)** -### TIPS +### Tips - We recommend adding the same/similar SystemPrompt that the model is tuned for. You can find this within the repo's files titled `SYSTEM_PROMPT.txt`. - For inference, the official MistralAI team recommends `top_p: 0.95` and `temperature: 0.7` with `max_tokens: 40960`. @@ -89,5 +77,5 @@ In addition, we do not support overriding tokens yet. ## Future Work -- Add parity to Preference Tuning, RL, Multi-modal, etc. +- Add parity to Preference Tuning, RL, etc. - Add parity to other tokenizer configs like overriding tokens. diff --git a/examples/magistral/think/README.md b/examples/magistral/think/README.md new file mode 100644 index 0000000000..29950f59e8 --- /dev/null +++ b/examples/magistral/think/README.md @@ -0,0 +1,73 @@ +# Magistral Small Thinking Fine-tuning + +This guide covers fine-tuning [Magistral Small 2507](https://huggingface.co/mistralai/Magistral-Small-2507) with thinking capabilities using Axolotl. The thinking model enables explicit Chain-of-Thought reasoning with separate thinking and response sections. + +## Prerequisites + +Before starting, ensure you have: +- Installed Axolotl (see [main README](../README.md)) + +## Getting Started + +Run the thinking model fine-tuning: + +```bash +axolotl train magistral-small-think-qlora.yaml +``` + +This config uses about 19.1 GiB VRAM. + +### Tips + +- Dataset uses multi-content format with `type: thinking` support. See [Dataset Format](#dataset-format) below. +- You cannot mix `content: str` and `content: list[dict]`, otherwise, dataset loading will fail. Keep it consistent. + +## Dataset Format + +The thinking model requires the multi-content dataset format with support for an extra `role: thinking` within system and assistant messages. + +Example format: + +```json +{ + "messages": [ + { + "role": "system", + "content": [ + { "type": "text", "text": "{SYSTEM_PROMPT}"} + ] + }, + { + "role": "user", + "content": [ + { "type": "text", "text": "Solve this step by step: What is 15% of 240?"} + ] + }, + { + "role": "assistant", + "content": [ + { + "type": "thinking", + "thinking": "I need to calculate 15% of 240. First, I'll convert 15% to decimal: 0.15. Then multiply: 0.15 × 240 = 36." + }, + { + "type": "text", + "text": "To find 15% of 240, I'll multiply 240 by 0.15:\n\n240 × 0.15 = 36\n\nTherefore, 15% of 240 is 36." + } + ] + } + ] +} +``` + +### Advanced Options + +The `thinking` section supports an optional `closed` parameter: + +```json +{ + "type": "thinking", + "thinking": "Internal reasoning here...", + "closed": true // Default: true, controls adding the closing [/THINK] tag +} +``` diff --git a/examples/magistral/magistral-small-think-qlora.yaml b/examples/magistral/think/magistral-small-think-qlora.yaml similarity index 100% rename from examples/magistral/magistral-small-think-qlora.yaml rename to examples/magistral/think/magistral-small-think-qlora.yaml diff --git a/examples/magistral/vision/README.md b/examples/magistral/vision/README.md new file mode 100644 index 0000000000..932a3631ee --- /dev/null +++ b/examples/magistral/vision/README.md @@ -0,0 +1,60 @@ +# Magistral Small Vision Fine-tuning + +This guide covers fine-tuning [Magistral Small 2509](https://huggingface.co/mistralai/Magistral-Small-2509) with vision capabilities using Axolotl. + +## Prerequisites + +Before starting, ensure you have: +- Installed Axolotl from source (see [main README](../README.md#getting-started)) + +## Getting started + +1. Install the required vision lib: + ```bash + pip install 'mistral-common[opencv]==1.8.5' + ``` + +2. Download the example dataset image: + ```bash + wget https://huggingface.co/datasets/Nanobit/text-vision-2k-test/resolve/main/African_elephant.jpg + ``` + +3. Run the fine-tuning: + ```bash + axolotl train magistral-small-vision-24B-qlora.yml + ``` + +This config uses about 17GiB VRAM. + +WARNING: The loss and grad norm will be much higher than normal at first. We suspect this to be inherent to the model as of the moment. If anyone would like to submit a fix for this, we are happy to take a look. + +### Tips + +Key differences from text-only model: +- `max_tokens: 131072` for inference +- Multi-modal dataset format required +- Sample packing not supported + +## Dataset Format + +The vision model requires multi-modal dataset format as documented [here](https://docs.axolotl.ai/docs/multimodal.html#dataset-format). + +One exception is that, passing `"image": PIL.Image` is not supported. MistralTokenizer only supports `path`, `url`, and `base64` for now. + +Example: +```json +{ + "messages": [ + {"role": "system", "content": [{ "type": "text", "text": "{SYSTEM_PROMPT}"}]}, + {"role": "user", "content": [ + { "type": "text", "text": "What's in this image?"}, + {"type": "image", "path": "path/to/image.jpg" } + ]}, + {"role": "assistant", "content": [{ "type": "text", "text": "..." }]}, + ], +} +``` + +## Limitations + +- Sample Packing is not supported for multi-modality training currently. diff --git a/examples/magistral/vision/magistral-small-vision-24B-qlora.yml b/examples/magistral/vision/magistral-small-vision-24B-qlora.yml new file mode 100644 index 0000000000..397db383ea --- /dev/null +++ b/examples/magistral/vision/magistral-small-vision-24B-qlora.yml @@ -0,0 +1,64 @@ +base_model: mistralai/Magistral-Small-2509 +processor_type: AutoProcessor + +# Enable to use mistral-common tokenizer +tokenizer_use_mistral_common: true + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_4bit: true + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +# sample dataset below requires downloading image in advance +# wget https://huggingface.co/datasets/Nanobit/text-vision-2k-test/resolve/main/African_elephant.jpg +datasets: + - path: Nanobit/text-vision-2k-test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.01 +output_dir: ./outputs/out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/mistral/bigstral-ds-zero3.yaml b/examples/mistral/bigstral/bigstral-ds-zero3.yaml similarity index 100% rename from examples/mistral/bigstral-ds-zero3.yaml rename to examples/mistral/bigstral/bigstral-ds-zero3.yaml diff --git a/examples/mistral/mistral-dpo-qlora.yml b/examples/mistral/dpo/mistral-dpo-qlora.yml similarity index 100% rename from examples/mistral/mistral-dpo-qlora.yml rename to examples/mistral/dpo/mistral-dpo-qlora.yml diff --git a/examples/mistral/mistral-small-3.1-24B-lora.yml b/examples/mistral/mistral-small/mistral-small-3.1-24B-lora.yml similarity index 78% rename from examples/mistral/mistral-small-3.1-24B-lora.yml rename to examples/mistral/mistral-small/mistral-small-3.1-24B-lora.yml index 3e477645e7..ec197f3336 100644 --- a/examples/mistral/mistral-small-3.1-24B-lora.yml +++ b/examples/mistral/mistral-small/mistral-small-3.1-24B-lora.yml @@ -1,6 +1,9 @@ base_model: mistralai/Mistral-Small-3.1-24B-Instruct-2503 processor_type: AutoProcessor +# Enable to use mistral-common tokenizer +tokenizer_use_mistral_common: true + load_in_8bit: true # these 3 lines are needed for now to handle vision chat templates w images @@ -8,12 +11,12 @@ skip_prepare_dataset: true remove_unused_columns: false sample_packing: false -chat_template: mistral_v7_tekken +# sample dataset below requires downloading image in advance +# wget https://huggingface.co/datasets/Nanobit/text-vision-2k-test/resolve/main/African_elephant.jpg datasets: - - path: HuggingFaceH4/llava-instruct-mix-vsft + - path: Nanobit/text-vision-2k-test type: chat_template - split: train[:1%] - field_messages: messages + dataset_prepared_path: last_run_prepared val_set_size: 0.01 output_dir: ./outputs/out @@ -48,8 +51,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -# flash_attention: false # PixtralVisionModel does not support Flash Attention 2.0 yet. -sdp_attention: true +flash_attention: true warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/mistral/mixtral-8x22b-qlora-fsdp.yml b/examples/mistral/mixtral/mixtral-8x22b-qlora-fsdp.yml similarity index 100% rename from examples/mistral/mixtral-8x22b-qlora-fsdp.yml rename to examples/mistral/mixtral/mixtral-8x22b-qlora-fsdp.yml diff --git a/examples/mistral/mixtral-qlora-fsdp.yml b/examples/mistral/mixtral/mixtral-qlora-fsdp.yml similarity index 100% rename from examples/mistral/mixtral-qlora-fsdp.yml rename to examples/mistral/mixtral/mixtral-qlora-fsdp.yml diff --git a/examples/mistral/mixtral.yml b/examples/mistral/mixtral/mixtral.yml similarity index 100% rename from examples/mistral/mixtral.yml rename to examples/mistral/mixtral/mixtral.yml diff --git a/examples/mistral/mixtral_22.yml b/examples/mistral/mixtral/mixtral_22.yml similarity index 100% rename from examples/mistral/mixtral_22.yml rename to examples/mistral/mixtral/mixtral_22.yml diff --git a/examples/mistral/lora-mps.yml b/examples/mistral/mps/lora-mps.yml similarity index 100% rename from examples/mistral/lora-mps.yml rename to examples/mistral/mps/lora-mps.yml diff --git a/examples/mistral/mistral-qlora-orpo.yml b/examples/mistral/orpo/mistral-qlora-orpo.yml similarity index 100% rename from examples/mistral/mistral-qlora-orpo.yml rename to examples/mistral/orpo/mistral-qlora-orpo.yml diff --git a/examples/pixtral/lora-12b.yml b/examples/pixtral/lora-12b.yml index fea2a60ff3..0e64899143 100644 --- a/examples/pixtral/lora-12b.yml +++ b/examples/pixtral/lora-12b.yml @@ -45,8 +45,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -# flash_attention: # PixtralVisionModel does not support Flash Attention 2.0 yet -sdp_attention: true +flash_attention: true warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/voxtral/README.md b/examples/voxtral/README.md index 984af4ddb7..b77691d72b 100644 --- a/examples/voxtral/README.md +++ b/examples/voxtral/README.md @@ -27,7 +27,14 @@ pip3 install 'mistral_common[audio]==1.8.3' python scripts/cutcrossentropy_install.py | sh ``` -3. Run the finetuning example: +3. Download sample dataset files + +```bash +# for text + audio only +wget https://huggingface.co/datasets/Nanobit/text-audio-2k-test/resolve/main/En-us-African_elephant.oga +``` + +4. Run the finetuning example: ```bash # text only diff --git a/requirements.txt b/requirements.txt index 44a3c0277e..86013374f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -70,4 +70,4 @@ schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.6 axolotl-contribs-mit==0.0.5 -mistral-common==1.8.3 +mistral-common==1.8.5 diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index a5a630cb59..98eb07b0f4 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -168,6 +168,13 @@ def _apply_model_specific_patches(self): patch_llama4_linearized_modeling() + if self.cfg.model_config_type == "mistral3" and self.cfg.processor_type: + from axolotl.monkeypatch.models.mistral3.mistral_common_tokenizer import ( + apply_mistral_tokenizer_image_patch, + ) + + apply_mistral_tokenizer_image_patch() + def _apply_fp8_patches(self): """Apply patches for FP8 support.""" if self.cfg.fp8: @@ -334,6 +341,13 @@ def _patch_attention(self): replace_stablelm_attn_with_flash_attn(self.cfg.base_model) + if self.model_config.model_type in ("mistral3", "llava"): + from axolotl.monkeypatch.models.pixtral.modeling_flash_attention_utils import ( + apply_patch_is_packed_sequence, + ) + + apply_patch_is_packed_sequence() + def _patch_loss_llama(self): """Patch loss functions and other optimizations for LLaMA models.""" if not self.cfg.is_llama_derived_model: diff --git a/src/axolotl/loaders/processor.py b/src/axolotl/loaders/processor.py index 2e3ec8d7fe..7580b20084 100644 --- a/src/axolotl/loaders/processor.py +++ b/src/axolotl/loaders/processor.py @@ -21,6 +21,13 @@ def load_processor(cfg: DictDefault, tokenizer: PreTrainedTokenizerBase): if cfg.processor_type: processor_cls = getattr(transformers, cfg.processor_type) + if cfg.tokenizer_use_mistral_common: + from axolotl.utils.mistral import Mistral3Processor + + return Mistral3Processor( + tokenizer=tokenizer, + ) + processor = processor_cls.from_pretrained( cfg.processor_config, trust_remote_code=cfg.trust_remote_code or False, diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index 37b66ac83f..69455dd772 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -124,13 +124,8 @@ def load_tokenizer(cfg: DictDefault) -> PreTrainedTokenizer: def _load_mistral_common_tokenizer(cfg: DictDefault): """Load mistral-common tokenizer""" - from transformers import tokenization_mistral_common - from axolotl.utils.mistral import HFMistralTokenizer - # patch - tokenization_mistral_common.MistralCommonTokenizer = HFMistralTokenizer - # Load the HF-compatible wrapper around MistralTokenizer tokenizer = HFMistralTokenizer.from_pretrained(cfg.tokenizer_config) diff --git a/src/axolotl/monkeypatch/models/mistral3/__init__.py b/src/axolotl/monkeypatch/models/mistral3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/mistral3/mistral_common_tokenizer.py b/src/axolotl/monkeypatch/models/mistral3/mistral_common_tokenizer.py new file mode 100644 index 0000000000..9e7259a05c --- /dev/null +++ b/src/axolotl/monkeypatch/models/mistral3/mistral_common_tokenizer.py @@ -0,0 +1,85 @@ +""" +Monkeypatch to fix inefficient tensor conversion in MistralCommonTokenizer.apply_chat_template +""" + +import importlib +import inspect + +from axolotl.monkeypatch.utils import detab_code +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def apply_mistral_tokenizer_image_patch(): + """Apply patch to MistralCommonTokenizer.apply_chat_template to fix image tensor conversion.""" + from transformers.tokenization_mistral_common import MistralCommonTokenizer + + # Get original source + original_source = inspect.getsource(MistralCommonTokenizer.apply_chat_template) + original_source, _ = detab_code(original_source) + + # Define the replacement + original_tensor_conversion = ( + " pixel_values = torch.tensor(images)" + ) + + patched_tensor_conversion = """ if isinstance(images, list) and len(images) > 0 and isinstance(images[0], np.ndarray): + pixel_values = torch.tensor(np.array(images)) + else: + pixel_values = torch.tensor(images)""" + + # Apply the replacement + if original_tensor_conversion in original_source: + patched_source = original_source.replace( + original_tensor_conversion, patched_tensor_conversion + ) + patched_source = patched_source.replace( + "def apply_chat_template(", + "def patched_apply_chat_template(", + 1, + ) + + # Load necessary imports from the module + module_name = MistralCommonTokenizer.__module__ + module = importlib.import_module(module_name) + + # Detect what needs to be imported + items_to_import = [] + for item in dir(module): + if item in patched_source and not item.startswith("_"): + items_to_import.append(item) + + # Execute imports in global scope + if items_to_import: + exec( # nosec B102 + f"from {module_name} import ({', '.join(items_to_import)})", + globals(), + ) + + # Also need standard imports that might be used + exec("import numpy as np", globals()) # nosec B102 + exec("import torch", globals()) # nosec B102 + exec("from typing import Union, Optional, List, Dict, Any, Callable", globals()) # nosec B102 + exec("from pathlib import Path", globals()) # nosec B102 + + # Import other dependencies that might be needed + try: + exec("from transformers.utils import is_torch_available", globals()) # nosec B102 + exec( + "from transformers.tokenization_utils_base import BatchEncoding, PaddingStrategy, TensorType", + globals(), + ) # nosec B102 + exec("from transformers.utils import logging", globals()) # nosec B102 + exec("logger = logging.get_logger(__name__)", globals()) # nosec B102 + except ImportError as e: + LOG.warning(f"Could not import some dependencies: {e}") + + # Execute the patched source + exec(patched_source, globals()) # nosec B102 + + # Replace the method + MistralCommonTokenizer.apply_chat_template = patched_apply_chat_template + LOG.info("Successfully applied MistralCommonTokenizer tensor conversion patch") + else: + LOG.warning("Could not find target code for MistralCommonTokenizer patching") diff --git a/src/axolotl/monkeypatch/models/pixtral/__init__.py b/src/axolotl/monkeypatch/models/pixtral/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/pixtral/modeling_flash_attention_utils.py b/src/axolotl/monkeypatch/models/pixtral/modeling_flash_attention_utils.py new file mode 100644 index 0000000000..d2b482f19b --- /dev/null +++ b/src/axolotl/monkeypatch/models/pixtral/modeling_flash_attention_utils.py @@ -0,0 +1,42 @@ +"""Monkeypatch for FA utils to accept 1D position_ids from Pixtral's position_ids_in_meshgrid""" + +import torch + + +def apply_patch_is_packed_sequence(): + """Apply patch to FA utils to accept 1D position_ids from Pixtral's position_ids_in_meshgrid""" + from transformers import modeling_flash_attention_utils + + def fixed_is_packed_sequence(position_ids, batch_size): + """ + Check the position ids whether packed sequences are indicated or not + 1. Position ids exist + 2. Flattened sequences only are supported + 3. Compile-friendly `not (torch.diff(position_ids, dim=-1) >= 0).all()`, i.e. we have multiple increasing sequences + """ + if position_ids is None: + return False + + if position_ids.ndim == 1: + position_ids = position_ids.unsqueeze(0) # [N] -> [1, N] + + increasing_position_sequences = ( + torch.arange(position_ids.shape[1], device=position_ids.device) + + position_ids.min() + ) + return ( + batch_size == 1 + and (increasing_position_sequences - position_ids).abs().sum().bool().item() + ) + + # Store original method + old_fn = modeling_flash_attention_utils._is_packed_sequence + + # Apply the patch + modeling_flash_attention_utils._is_packed_sequence = fixed_is_packed_sequence + + def unpatch(): + """Restore the original method""" + modeling_flash_attention_utils._is_packed_sequence = old_fn + + return unpatch diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 4b06eb4c8d..5e7c1456a7 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -11,6 +11,7 @@ from axolotl.utils.dict import remove_none_values from axolotl.utils.logging import get_logger +from axolotl.utils.mistral.mistral3_processor import Mistral3Processor LOG = get_logger(__name__) @@ -421,6 +422,36 @@ def __init__( ] +class Mistral3ProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for Mistral3""" + + def __init__( + self, + processor: Mistral3Processor, + chat_template: Optional[str] = None, + image_size: int | tuple[int, int] | None = None, + image_resize_algorithm: Resampling | None = None, + ): + super().__init__(processor, chat_template, image_size, image_resize_algorithm) + special_ids = ( + processor.tokenizer.tokenizer.instruct_tokenizer.image_encoder.special_ids + ) + + self.image_token = special_ids.img + self.image_break_token = special_ids.img_break + self.image_end_token = special_ids.img_end + + def process_labels(self, input_ids): + labels = input_ids.clone() + + labels[labels == self.processor.tokenizer.pad_token_id] = -100 + labels[labels == self.image_token] = -100 + labels[labels == self.image_break_token] = -100 + labels[labels == self.image_end_token] = -100 + + return labels + + def get_processing_strategy( processor: ProcessorMixin, chat_template, @@ -463,6 +494,11 @@ def get_processing_strategy( **processing_kwargs, ) + if isinstance(processor, Mistral3Processor): + return Mistral3ProcessingStrategy( + **processing_kwargs, + ) + # llama3_2_vision, llama4, llava # mistral_v7_tekken, pixtral, lfm2vl return ProcessingStrategy( diff --git a/src/axolotl/utils/mistral/__init__.py b/src/axolotl/utils/mistral/__init__.py index eb1e2df895..eb51031ec1 100644 --- a/src/axolotl/utils/mistral/__init__.py +++ b/src/axolotl/utils/mistral/__init__.py @@ -1,5 +1,6 @@ """Init for `axolotl.utils.mistral` module.""" +from axolotl.utils.mistral.mistral3_processor import Mistral3Processor from axolotl.utils.mistral.mistral_tokenizer import HFMistralTokenizer -__all__ = ["HFMistralTokenizer"] +__all__ = ["HFMistralTokenizer", "Mistral3Processor"] diff --git a/src/axolotl/utils/mistral/mistral3_processor.py b/src/axolotl/utils/mistral/mistral3_processor.py new file mode 100644 index 0000000000..85479ca7be --- /dev/null +++ b/src/axolotl/utils/mistral/mistral3_processor.py @@ -0,0 +1,169 @@ +"""Processor for Mistral3 multimodal models with image support""" + +from typing import Any, Dict, Optional, Union + +import torch +from transformers import ProcessorMixin +from transformers.feature_extraction_utils import BatchFeature +from transformers.processing_utils import ProcessingKwargs +from transformers.tokenization_utils_base import PreTokenizedInput, TextInput + +from axolotl.utils.mistral.mistral_tokenizer import HFMistralTokenizer + + +class Mistral3ProcessorKwargs(ProcessingKwargs): + _defaults: Dict[str, Dict[str, Any]] = { + "text_kwargs": { + "padding": True, + }, + "common_kwargs": { + "return_tensors": "pt", + "return_dict": True, + "tokenize": True, + }, + } + + +class Mistral3Processor(ProcessorMixin): + """ + Processor for Mistral3 multimodal models that handles text and images. + Wraps HFMistralTokenizer and adds image processing capabilities. + """ + + attributes = ["tokenizer"] + tokenizer_class = "HFMistralTokenizer" + + def __init__(self, tokenizer: HFMistralTokenizer): + # Don't call super().__init__ to avoid the class validation issue + self.tokenizer = tokenizer + + @property + def chat_template(self) -> None: + """Chat template is not supported. Dummy method to satisfy HuggingFace API.""" + return None + + @property + def audio_tokenizer(self) -> None: + """Audio tokenizer is not supported. Dummy method to satisfy HuggingFace API.""" + return None + + def _merge_kwargs( + self, processor_kwargs_class: Any, **kwargs: Any + ) -> Dict[str, Dict[str, Any]]: + """Merge kwargs with defaults similar to ProcessorMixin""" + defaults = processor_kwargs_class._defaults + output_kwargs: Dict[str, Dict[str, Any]] = {} + + for kwarg_type, default_values in defaults.items(): + output_kwargs[kwarg_type] = {**default_values} + + # Update with provided kwargs + for key, value in kwargs.items(): + # Try to match key to appropriate kwarg type + if key in ["padding", "truncation", "max_length"]: + output_kwargs.setdefault("text_kwargs", {}).update({key: value}) + elif key in ["return_tensors", "return_dict", "tokenize"]: + output_kwargs.setdefault("common_kwargs", {}).update({key: value}) + else: + # Add to text_kwargs by default + output_kwargs.setdefault("text_kwargs", {}).update({key: value}) + + return output_kwargs + + def apply_chat_template( + self, + conversation: Union[list[dict[str, str]], list[list[dict[str, str]]]], + **kwargs: Any, + ) -> Union[BatchFeature, str, list[str]]: + """ + Apply chat template with image support for Mistral3. + + Similar to VoxtralProcessor, this method extracts images from the conversation, + calls the tokenizer's apply_chat_template, then adds pixel_values and image_sizes + to the result. + """ + output_kwargs = self._merge_kwargs(Mistral3ProcessorKwargs, **kwargs) + text_kwargs = output_kwargs["text_kwargs"] + common_kwargs = output_kwargs["common_kwargs"] + + return_tensors = common_kwargs.pop("return_tensors", "pt") + if return_tensors != "pt": + raise ValueError( + f"{self.__class__.__name__} only supports `return_tensors='pt'`." + ) + + return_dict = common_kwargs.pop("return_dict", False) + tokenize = common_kwargs.pop("tokenize", False) + + # Determine if batched + if isinstance(conversation, (list, tuple)) and ( + isinstance(conversation[0], (list, tuple)) + or hasattr(conversation[0], "content") + ): + is_batched = True + conversations = conversation + else: + is_batched = False + conversations = [conversation] # type: ignore + + # Call tokenizer's apply_chat_template + tokenizer_kwargs = {**text_kwargs, **common_kwargs} + tokenizer_kwargs["return_tensors"] = return_tensors + tokenizer_kwargs["tokenize"] = tokenize + tokenizer_kwargs["return_dict"] = return_dict + + encoded_instruct_inputs = self.tokenizer.apply_chat_template( + conversations, + **tokenizer_kwargs, + ) + + if tokenize: + if return_dict: + # The tokenizer already handles pixel_values, we just need to add image_sizes + if hasattr(encoded_instruct_inputs, "items"): + data: Dict[str, Any] = dict(encoded_instruct_inputs) # type: ignore + elif hasattr(encoded_instruct_inputs, "data"): + data = encoded_instruct_inputs.data # type: ignore + else: + raise ValueError("Unknown data type") + + if "pixel_values" in data: + pixel_values = data["pixel_values"] + + # MistralTokenizer returns a Double, so we convert to fp32 + data["pixel_values"] = pixel_values.to(dtype=torch.float32) + + # Always batched: [B, C, H, W] -> image_sizes: [B, 2] + # Since tensor is homogeneous, all images have same H, W + batch_size = pixel_values.shape[0] + image_sizes = torch.tensor([pixel_values.shape[-2:]] * batch_size) + data["image_sizes"] = image_sizes + + return BatchFeature(data=data, tensor_type=return_tensors) + + if not is_batched: + return encoded_instruct_inputs[0] + + return encoded_instruct_inputs + + def __call__( + self, + text: Optional[ + Union[ + TextInput, PreTokenizedInput, list[TextInput], list[PreTokenizedInput] + ] + ], + **kwargs: Any, + ) -> BatchFeature: + """ + Forward text processing to the tokenizer. + This method does not support images - use apply_chat_template instead. + """ + output_kwargs = self._merge_kwargs(Mistral3ProcessorKwargs, **kwargs) + text_kwargs = output_kwargs["text_kwargs"] + common_kwargs = output_kwargs["common_kwargs"] + + out = self.tokenizer(text, **text_kwargs) + return BatchFeature( + data=out, tensor_type=common_kwargs.pop("return_tensors", None) + ) diff --git a/tests/monkeypatch/test_mistral_tokenizer_patch.py b/tests/monkeypatch/test_mistral_tokenizer_patch.py new file mode 100644 index 0000000000..cb82c08902 --- /dev/null +++ b/tests/monkeypatch/test_mistral_tokenizer_patch.py @@ -0,0 +1,35 @@ +"""Integration tests for MistralCommonTokenizer patches.""" + +import pytest + + +class TestMistralTokenizerPatchIntegration: + """Test MistralCommonTokenizer patch integration.""" + + @pytest.mark.integration + def test_mistral_tokenizer_image_patch(self): + """Test that MistralCommonTokenizer image patch can be applied.""" + try: + from transformers.tokenization_mistral_common import MistralCommonTokenizer + except ImportError: + pytest.skip("MistralCommonTokenizer not available") + + from axolotl.monkeypatch.models.mistral3.mistral_common_tokenizer import ( + apply_mistral_tokenizer_image_patch, + ) + + # Store original method + original_apply_chat_template = MistralCommonTokenizer.apply_chat_template + + # Apply patch + apply_mistral_tokenizer_image_patch() + + # Verify patch was applied + assert ( + MistralCommonTokenizer.apply_chat_template != original_apply_chat_template + ), "apply_chat_template was not patched" + + # Verify the method is still callable + assert callable(MistralCommonTokenizer.apply_chat_template), ( + "Patched method is not callable" + ) diff --git a/tests/monkeypatch/test_pixtral_flash_attention_patch.py b/tests/monkeypatch/test_pixtral_flash_attention_patch.py new file mode 100644 index 0000000000..285fde41e8 --- /dev/null +++ b/tests/monkeypatch/test_pixtral_flash_attention_patch.py @@ -0,0 +1,77 @@ +"""Integration tests for Pixtral Flash Attention patches.""" + +import pytest +import torch + + +class TestPixtralFlashAttentionPatchIntegration: + """Test Pixtral Flash Attention patch integration.""" + + @pytest.mark.integration + def test_pixtral_flash_attention_patch(self): + """Test that Pixtral Flash Attention patch can be applied and works correctly.""" + try: + from transformers import modeling_flash_attention_utils + except ImportError: + pytest.skip("Flash Attention utils not available") + + from axolotl.monkeypatch.models.pixtral.modeling_flash_attention_utils import ( + apply_patch_is_packed_sequence, + ) + + # Store original method + original_is_packed_sequence = modeling_flash_attention_utils._is_packed_sequence + + # Apply patch and get unpatch function + unpatch_fn = apply_patch_is_packed_sequence() + + # Verify patch was applied + assert ( + modeling_flash_attention_utils._is_packed_sequence + != original_is_packed_sequence + ), "_is_packed_sequence was not patched" + + # Test the patched function with 1D position_ids + patched_fn = modeling_flash_attention_utils._is_packed_sequence + + # Test 1D position_ids 1 sequence + position_ids_1d = torch.tensor([0, 1, 2, 3]) + result = patched_fn(position_ids_1d, batch_size=1) + assert isinstance(result, bool), "Function should return a boolean" + assert result is False, "1D sequential position_ids should not be packed" + + # Test 1D packed 2 sequences + position_ids_1d_packed = torch.tensor([0, 1, 2, 0, 1, 2]) + result = patched_fn(position_ids_1d_packed, batch_size=1) + assert isinstance(result, bool), "Function should return a boolean" + assert result is True, "1D packed position_ids should be detected as packed" + + # Test 2D packed 2 sequences + position_ids_2d_packed = torch.tensor([[0, 1, 2, 3, 0, 1]]) + result = patched_fn(position_ids_2d_packed, batch_size=1) + assert isinstance(result, bool), "Function should return a boolean" + assert result is True, "2D packed position_ids should be detected as packed" + + # Test 2D 1 sequence + position_ids_2d_normal = torch.tensor([[0, 1, 2, 3, 4, 5]]) + result = patched_fn(position_ids_2d_normal, batch_size=1) + assert isinstance(result, bool), "Function should return a boolean" + assert result is False, "2D sequential position_ids should not be packed" + + # Test 2D batch size 2 + position_ids_2d_normal = torch.tensor([[0, 1, 2, 3, 4, 5, 6, 7, 8]]) + result = patched_fn(position_ids_2d_normal, batch_size=2) + assert isinstance(result, bool), "Function should return a boolean" + assert result is False, "2D position_ids batch 2 should not be packed" + + # Test None case + result = patched_fn(None, batch_size=1) + assert isinstance(result, bool), "Function should return a boolean" + assert result is False, "None position_ids should return False" + + # Test unpatch function + unpatch_fn() + assert ( + modeling_flash_attention_utils._is_packed_sequence + == original_is_packed_sequence + ), "unpatch function did not restore original method" diff --git a/tests/monkeypatch/test_voxtral_modeling_patch.py b/tests/monkeypatch/test_voxtral_modeling_patch.py new file mode 100644 index 0000000000..878bbc1855 --- /dev/null +++ b/tests/monkeypatch/test_voxtral_modeling_patch.py @@ -0,0 +1,43 @@ +"""Integration tests for Voxtral modeling patches.""" + +import pytest + + +class TestVoxtralModelingPatchIntegration: + """Test Voxtral modeling patch integration.""" + + @pytest.mark.integration + def test_voxtral_conditional_generation_patch(self): + """Test that Voxtral conditional generation patch can be applied.""" + try: + from transformers.models.voxtral.modeling_voxtral import ( + VoxtralForConditionalGeneration, + ) + except ImportError: + pytest.skip("VoxtralForConditionalGeneration not available") + + from axolotl.monkeypatch.models.voxtral.modeling import ( + patch_voxtral_conditional_generation_forward, + ) + + # Store original method + original_forward = VoxtralForConditionalGeneration.forward + + # Apply patch and get unpatch function + unpatch_fn = patch_voxtral_conditional_generation_forward() + + # Verify patch was applied + assert VoxtralForConditionalGeneration.forward != original_forward, ( + "forward method was not patched" + ) + + # Verify the method is still callable + assert callable(VoxtralForConditionalGeneration.forward), ( + "Patched method is not callable" + ) + + # Test unpatch function + unpatch_fn() + assert VoxtralForConditionalGeneration.forward == original_forward, ( + "unpatch function did not restore original method" + ) From c51d6b06c3b25cfc94dd2ca08fc31b3a73ac4c29 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 19 Sep 2025 17:34:04 +0700 Subject: [PATCH 0972/1405] feat: add apertus model and cce (#3144) [skip ci] * feat: add apertus, glm4v, glm4v_moe cce * fix: arcee docs * feat: add apertus * feat: added vram usage * fix: add apertus note * feat: update doc on apertus xielu * fix: add monkeypatch for xielu activation issue * fix: simplify env * feat: pin commit * feat: add packing * chore: move patch calling * Update examples/apertus/README.md Co-authored-by: salman * Update examples/apertus/README.md Co-authored-by: salman * Update examples/apertus/README.md Co-authored-by: salman --------- Co-authored-by: salman --- examples/apertus/README.md | 110 ++++++++++++++++++ examples/apertus/apertus-8b-qlora.yaml | 64 ++++++++++ examples/arcee/README.md | 3 + .../colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 2 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/loaders/patch_manager.py | 12 +- .../monkeypatch/models/apertus/__init__.py | 0 .../monkeypatch/models/apertus/activation.py | 52 +++++++++ src/axolotl/monkeypatch/multipack.py | 1 + 11 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 examples/apertus/README.md create mode 100644 examples/apertus/apertus-8b-qlora.yaml create mode 100644 src/axolotl/monkeypatch/models/apertus/__init__.py create mode 100644 src/axolotl/monkeypatch/models/apertus/activation.py diff --git a/examples/apertus/README.md b/examples/apertus/README.md new file mode 100644 index 0000000000..774286333a --- /dev/null +++ b/examples/apertus/README.md @@ -0,0 +1,110 @@ +# Finetune Swiss-AI's Apertus with Axolotl + +[Apertus](https://huggingface.co/collections/swiss-ai/apertus-llm-68b699e65415c231ace3b059) is a family of opensource models trained by Swiss-ai. + +This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Apertus is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). + + Here is an example of how to install from main for pip: + +```bash +# Ensure you have Pytorch installed (Pytorch 2.6.0 min) +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl + +pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install --no-build-isolation -e '.[flash-attn]' + +# Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy +python scripts/cutcrossentropy_install.py | sh +``` + +2. (Optional, highly recommended) Install XIELU CUDA + +```bash +## Recommended for reduced VRAM and faster speeds + +# Point to CUDA toolkit directory +# For those using our Docker image, use the below path. +export CUDA_HOME=/usr/local/cuda + +pip3 install git+https://github.com/nickjbrowning/XIELU@59d6031 --no-build-isolation --no-deps +``` + +For any installation errors, see [XIELU Installation Issues](#xielu-installation-issues) + +3. Run the finetuning example: + +```bash +axolotl train examples/apertus/apertus-8b-qlora.yaml +``` + +This config uses about 8.7 GiB VRAM. + +Let us know how it goes. Happy finetuning! 🚀 + +### Tips + +- For inference, the official Apertus team recommends `top_p=0.9` and `temperature=0.8`. +- You can instead use full paremter fine-tuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +### XIELU Installation Issues + +#### `ModuleNotFoundError: No module named 'torch'` + +Please check these one by one: +- Running in correct environment +- Env has PyTorch installed +- CUDA toolkit is at `CUDA_HOME` + +If those didn't help, please try the below solutions: + +1. Pass env for CMAKE and try install again: + + ```bash + Python_EXECUTABLE=$(which python) pip3 install git+https://github.com/nickjbrowning/XIELU@59d6031 --no-build-isolation --no-deps + ``` + +2. Git clone the repo and manually hardcode python path: + + ```bash + git clone https://github.com/nickjbrowning/XIELU + cd xielu + git checkout 59d6031 + + cd xielu + nano CMakeLists.txt # or vi depending on your preference + ``` + + ```diff + execute_process( + - COMMAND ${Python_EXECUTABLE} -c "import torch.utils; print(torch.utils.cmake_prefix_path)" + + COMMAND /root/miniconda3/envs/py3.11/bin/python -c "import torch.utils; print(torch.utils.cmake_prefix_path)" + RESULT_VARIABLE TORCH_CMAKE_PATH_RESULT + OUTPUT_VARIABLE TORCH_CMAKE_PATH_OUTPUT + ERROR_VARIABLE TORCH_CMAKE_PATH_ERROR + ) + ``` + + ```bash + pip3 install . --no-build-isolation --no-deps + ``` + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) + +## Related Resources + +- [Apertus Tech Report](https://github.com/swiss-ai/apertus-tech-report/blob/main/Apertus_Tech_Report.pdf) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/apertus/apertus-8b-qlora.yaml b/examples/apertus/apertus-8b-qlora.yaml new file mode 100644 index 0000000000..521b282da1 --- /dev/null +++ b/examples/apertus/apertus-8b-qlora.yaml @@ -0,0 +1,64 @@ +base_model: swiss-ai/Apertus-8B-Instruct-2509 + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/arcee/README.md b/examples/arcee/README.md index 2178933065..23f63663e7 100644 --- a/examples/arcee/README.md +++ b/examples/arcee/README.md @@ -19,6 +19,9 @@ cd axolotl pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation -e '.[flash-attn]' + +# Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy +python scripts/cutcrossentropy_install.py | sh ``` 2. Run the finetuning example: diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 774b78b82f..e63632e7c0 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c6a32c5\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c564afc\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 5b49e7427f..ada5748050 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c6a32c5"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c564afc"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 393412f647..2361dde4a7 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c6a32c5" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c564afc" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index d0eb1ebdb6..dad3f7f892 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c6a32c5"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c564afc"`' ) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 98eb07b0f4..a78f8b9655 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -68,11 +68,12 @@ def apply_pre_model_load_patches(self): self._apply_self_attention_lora_patch() self._apply_fsdp2_bnb_patches() self._apply_patch_deepspeed_zero3() + self._apply_voxtral_patches() + self._apply_apertus_patches() def apply_post_plugin_pre_model_load_patches(self): """Apply post plugin-pre_model_load load patches based on config.""" self._apply_tiled_mlp(self.cfg.model_config_type) - self._apply_voxtral_patches() def _apply_transformers_patches(self): from axolotl.monkeypatch.transformers.trainer_loss_calc import ( @@ -493,3 +494,12 @@ def _apply_patch_deepspeed_zero3(self): apply_deepspeed_patches() except ImportError as e: LOG.warning(f"DeepSpeed patches not applied: {e}") + + def _apply_apertus_patches(self): + """Apply patches for Apertus model.""" + if self.cfg.model_config_type == "apertus": + from axolotl.monkeypatch.models.apertus.activation import ( + patch_apertus_xielu_activation, + ) + + patch_apertus_xielu_activation() diff --git a/src/axolotl/monkeypatch/models/apertus/__init__.py b/src/axolotl/monkeypatch/models/apertus/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/apertus/activation.py b/src/axolotl/monkeypatch/models/apertus/activation.py new file mode 100644 index 0000000000..d5470aceb6 --- /dev/null +++ b/src/axolotl/monkeypatch/models/apertus/activation.py @@ -0,0 +1,52 @@ +"""Monkeypatch for Apertus to dtype mismatch in XIELU act""" + +from torch import Tensor + + +def patch_apertus_xielu_activation(): + try: + from transformers.activations import XIELUActivation + except ImportError as err: + raise ImportError( + "Cannot import XIELUActivation. " + "Please make sure to update your transformers version >= 4.56.1." + ) from err + + from transformers.activations import logger + + # Store the original method + old_fn = XIELUActivation._xielu_cuda + + def _xielu_cuda_fixed(self, x: Tensor) -> Tensor: + """Firewall function to prevent torch.compile from seeing .item() calls""" + original_shape = x.shape + # CUDA kernel expects 3D tensors, reshape if needed + while x.dim() < 3: + x = x.unsqueeze(0) + if x.dim() > 3: + x = x.view(-1, 1, x.size(-1)) + if original_shape != x.shape: + logger.warning_once( + "Warning: xIELU input tensor expects 3 dimensions but got (shape: %s). Reshaping to (shape: %s).", + original_shape, + x.shape, + ) + result = self._xielu_cuda_obj.forward( + x, + self.alpha_p.to(x.dtype), + self.alpha_n.to(x.dtype), + # Temporary until xIELU CUDA fully implemented -> self.{beta,eps}.item() + self._beta_scalar, + self._eps_scalar, + self.with_vector_loads, + ) + return result.view(original_shape) + + # Apply the patch + XIELUActivation._xielu_cuda = _xielu_cuda_fixed + + def unpatch(): + """Restore the original method""" + XIELUActivation._xielu_cuda = old_fn + + return unpatch diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index a32430d9f3..726e601114 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -11,6 +11,7 @@ from axolotl.monkeypatch.utils import get_unpad_data SUPPORTED_MULTIPACK_MODEL_TYPES = [ + "apertus", "mllama_text_model", "llama", "llama4", From 7be8740c5c0a2ced61eb9c3ac2afa220baf4ff1e Mon Sep 17 00:00:00 2001 From: AlexHT Hung Date: Fri, 19 Sep 2025 18:34:28 +0800 Subject: [PATCH 0973/1405] fix(rl): pass max_prompt_len to training args as max_prompt_length (#3113) * pass max_prompt_len to training args as max_prompt_length * Update rl.py * refactor * format * fix: default for max_prompt_length * fix: defaults for trainer --------- Co-authored-by: NanoCode012 --- src/axolotl/core/builders/rl.py | 18 +++++++++++++----- src/axolotl/core/trainers/dpo/__init__.py | 1 - src/axolotl/utils/schemas/config.py | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index a6e8355f44..0ceb80008c 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -120,6 +120,11 @@ def _build_training_arguments(self, total_num_steps): if self.cfg.use_wandb: training_args_kwargs["run_name"] = self.cfg.wandb_name + if self.cfg.max_prompt_len: + training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len + else: + training_args_kwargs["max_prompt_length"] = self.cfg.sequence_len + training_args_cls = None blocklist_args_kwargs = [] if self.cfg.rl is RLType.SIMPO: @@ -129,10 +134,16 @@ def _build_training_arguments(self, total_num_steps): if self.cfg.cpo_alpha is not None: training_args_kwargs["cpo_alpha"] = self.cfg.cpo_alpha + # Handle when max_prompt_length == max_length from defaults + # CPOTrainer requires strictly less than + if ( + training_args_kwargs["max_prompt_length"] + == training_args_kwargs["max_length"] + ): + training_args_kwargs["max_prompt_length"] -= 1 + elif self.cfg.rl is RLType.ORPO: training_args_cls = AxolotlORPOConfig - if self.cfg.max_prompt_len: - training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len elif self.cfg.rl is RLType.KTO: training_args_cls = AxolotlKTOConfig @@ -144,9 +155,6 @@ def _build_training_arguments(self, total_num_steps): self.cfg.kto_undesirable_weight or 1.0 ) - if self.cfg.max_prompt_len: - training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len - elif self.cfg.rl is RLType.GRPO: training_args_cls = GRPOStrategy.get_training_args_class() training_args_kwargs.update(GRPOStrategy.set_training_args_kwargs(self.cfg)) diff --git a/src/axolotl/core/trainers/dpo/__init__.py b/src/axolotl/core/trainers/dpo/__init__.py index 4b40d40854..3aa79c4841 100644 --- a/src/axolotl/core/trainers/dpo/__init__.py +++ b/src/axolotl/core/trainers/dpo/__init__.py @@ -27,7 +27,6 @@ def set_training_args_kwargs(cls, cfg): training_args_kwargs["label_smoothing"] = cfg.dpo_label_smoothing training_args_kwargs["max_completion_length"] = None training_args_kwargs["max_length"] = cfg.sequence_len - training_args_kwargs["max_prompt_length"] = cfg.sequence_len training_args_kwargs["generate_during_eval"] = cfg.dpo_generate_during_eval if cfg.dpo_use_weighting is not None: training_args_kwargs["use_weighting"] = cfg.dpo_use_weighting diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index d612ec8a59..0177b19f6c 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -436,8 +436,8 @@ class AxolotlInputConfig( }, ) min_sample_len: int | None = None - max_prompt_len: int = Field( - default=512, + max_prompt_len: int | None = Field( + default=None, json_schema_extra={"description": "maximum prompt length for RL training"}, ) sample_packing: bool | None = Field( From 08d831c3d5b567b76bfa03df536fca17af9f4a58 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 23 Sep 2025 11:31:15 +0700 Subject: [PATCH 0974/1405] Feat: add qwen3-next (w packing+cce) (#3150) * feat: upgrade cce for qwen3-next * feat: add sample qwen3 config * feat: add packing patch for chunk_gated_delta_rule * feat: add qwen3 link * fix: tuple name * feat: add tested qwen3 config * fix: improve log * feat: add patch for fla without packing * fix: remove fla patch for standard mode * feat: enable packing * feat: add qwen3-next tests * chore: move tests --- .../colab-axolotl-example.ipynb | 2 +- examples/qwen3-next/README.md | 64 ++++ .../qwen3-next/qwen3-next-80b-a3b-qlora.yaml | 60 ++++ scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 3 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/loaders/patch_manager.py | 7 + .../monkeypatch/models/qwen3_next/__init__.py | 1 + .../monkeypatch/models/qwen3_next/modeling.py | 317 ++++++++++++++++++ src/axolotl/monkeypatch/multipack.py | 1 + .../test_qwen3_next_modeling_patch.py | 111 ++++++ 11 files changed, 566 insertions(+), 4 deletions(-) create mode 100644 examples/qwen3-next/README.md create mode 100644 examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml create mode 100644 src/axolotl/monkeypatch/models/qwen3_next/__init__.py create mode 100644 src/axolotl/monkeypatch/models/qwen3_next/modeling.py create mode 100644 tests/monkeypatch/test_qwen3_next_modeling_patch.py diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index e63632e7c0..b483310639 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c564afc\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c5aa3ef\"" ] }, { diff --git a/examples/qwen3-next/README.md b/examples/qwen3-next/README.md new file mode 100644 index 0000000000..eb0d5fd28d --- /dev/null +++ b/examples/qwen3-next/README.md @@ -0,0 +1,64 @@ +# Finetune Qwen3-Next with Axolotl + +[Qwen3-Next](https://huggingface.co/collections/Qwen/qwen3-next-68c25fd6838e585db8eeea9d) represents the next-generation foundation models optimized for extreme context length and large-scale parameter efficiency. The series introduces architectural innovations including Hybrid Attention (Gated DeltaNet + Gated Attention), High-Sparsity MoE with 1:50 activation ratio, and Multi-Token Prediction for enhanced performance and inference acceleration. + +This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Qwen3-Next is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). + + Here is an example of how to install from main for pip: + +```bash +# Ensure you have Pytorch installed (Pytorch 2.6.0 min) +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl + +pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install --no-build-isolation -e '.[flash-attn]' + +# Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy +python scripts/cutcrossentropy_install.py | sh +``` + +2. Install Qwen3-Next transformers commit +```bash +pip3 uninstall -y transformers && pip3 install "git+https://github.com/huggingface/transformers.git@b9282355bea846b54ed850a066901496b19da654" +``` + +3. Install FLA for improved performance +```bash +pip3 uninstall -y causal-conv1d && pip3 install flash-linear-attention==0.3.2 +``` + +4. Run the finetuning example: + +```bash +axolotl train examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml +``` + +This config uses about 41.7 GiB VRAM. + +Let us know how it goes. Happy finetuning! 🚀 + +### TIPS + +- For inference, you can experiment with `temperature: 0.7`, `top_p: 0.8`, `top_k: 20`, and `min_p: 0`. +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. See [Multi-GPU](#optimization-guides) section below. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) + +## Related Resources + +- [Qwen3-Next Blog](https://qwenlm.github.io/blog/qwen3_next/) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml b/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml new file mode 100644 index 0000000000..11481dcd31 --- /dev/null +++ b/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml @@ -0,0 +1,60 @@ +base_model: Qwen/Qwen3-Next-80B-A3B-Instruct + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 16 +lora_alpha: 8 +lora_dropout: 0.05 +lora_target_modules: + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index ada5748050..dc117604af 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c564afc"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c5aa3ef"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 2361dde4a7..cc73eebb78 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c564afc" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c5aa3ef" ``` ## Usage @@ -65,6 +65,7 @@ plugins: - qwen2_5_vl - qwen3 - qwen3_moe +- qwen3_next - smollm3 - seed_oss - voxtral diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index dad3f7f892..812baf33ff 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c564afc"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c5aa3ef"`' ) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index a78f8b9655..3d4b7b96ba 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -169,6 +169,13 @@ def _apply_model_specific_patches(self): patch_llama4_linearized_modeling() + if self.cfg.model_config_type == "qwen3_next" and self.cfg.sample_packing: + from axolotl.monkeypatch.models.qwen3_next.modeling import ( + patch_qwen3_next_modeling_packing, + ) + + patch_qwen3_next_modeling_packing() + if self.cfg.model_config_type == "mistral3" and self.cfg.processor_type: from axolotl.monkeypatch.models.mistral3.mistral_common_tokenizer import ( apply_mistral_tokenizer_image_patch, diff --git a/src/axolotl/monkeypatch/models/qwen3_next/__init__.py b/src/axolotl/monkeypatch/models/qwen3_next/__init__.py new file mode 100644 index 0000000000..39bcd4115c --- /dev/null +++ b/src/axolotl/monkeypatch/models/qwen3_next/__init__.py @@ -0,0 +1 @@ +"""Qwen3_Next model monkeypatches.""" diff --git a/src/axolotl/monkeypatch/models/qwen3_next/modeling.py b/src/axolotl/monkeypatch/models/qwen3_next/modeling.py new file mode 100644 index 0000000000..d68992d0e8 --- /dev/null +++ b/src/axolotl/monkeypatch/models/qwen3_next/modeling.py @@ -0,0 +1,317 @@ +"""Monkeypatch for Qwen3_Next model to pass position_ids to linear attention.""" + +from typing import Optional, Tuple + +import torch +import torch.nn.functional as F + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def get_cu_seqlens(position_ids): + """ + Adapted from transformers.modeling_flash_attention_utils.prepare_fa_kwargs_from_position_ids. + + https://github.com/huggingface/transformers/blob/0f1b128d3359a26bd18be99c26d7f04fb3cba914/src/transformers/modeling_flash_attention_utils.py#L316 + """ + tensor_kwargs = {"dtype": torch.int32, "device": position_ids.device} + + position_ids = position_ids.view(-1) + indices_q = (position_ids == 0).nonzero().view(-1) + + cu_seq_lens_q = torch.cat( + ( + indices_q.to(**tensor_kwargs), + torch.tensor(position_ids.size(), **tensor_kwargs), + ) + ) + + return cu_seq_lens_q + + +def patch_qwen3_next_decoder_layer(): + """Patch Qwen3NextDecoderLayer to pass position_ids to linear attention.""" + try: + from transformers.models.qwen3_next.modeling_qwen3_next import ( + Qwen3NextDecoderLayer, + ) + except ImportError: + LOG.warning("Qwen3Next model not found, skipping patch") + return + + # Store original forward method + original_decoder_forward = Qwen3NextDecoderLayer.forward + + def patched_decoder_forward( + self, + hidden_states: torch.Tensor, + position_embeddings: Tuple[torch.Tensor, torch.Tensor], + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Tuple[torch.Tensor]] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs, + ) -> torch.FloatTensor: + residual = hidden_states + + hidden_states = self.input_layernorm(hidden_states) + + # Token Mixer + if self.layer_type == "linear_attention": + hidden_states = self.linear_attn( + hidden_states=hidden_states, + cache_params=past_key_values, + cache_position=cache_position, + attention_mask=attention_mask, + position_ids=position_ids, + ) + elif self.layer_type == "full_attention": + # Self Attention + hidden_states, _ = self.self_attn( + hidden_states=hidden_states, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + cache_position=cache_position, + position_embeddings=position_embeddings, + **kwargs, + ) + + hidden_states = residual + hidden_states + + # Fully Connected + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + hidden_states = self.mlp(hidden_states) + # For the MoE layers, we need to unpack + if isinstance(hidden_states, Tuple): + hidden_states, _ = hidden_states + hidden_states = residual + hidden_states + + return hidden_states + + # Apply the patches + Qwen3NextDecoderLayer.forward = patched_decoder_forward + + def unpatch(): + """Restore the original forward method""" + Qwen3NextDecoderLayer.forward = original_decoder_forward + + return unpatch + + +def patch_qwen3_next_gateddelta_layer(): + """Patch Qwen3NextGatedDeltaNet to parse cu_seqlens and pass to chunk_gated_delta_rule""" + try: + from transformers.models.qwen3_next.modeling_qwen3_next import ( + Qwen3NextDynamicCache, + Qwen3NextGatedDeltaNet, + apply_mask_to_padding_states, + ) + except ImportError: + LOG.warning("Qwen3Next model not found, skipping patch") + return + + # Store original forward method + original_gated_delta_net_forward = Qwen3NextGatedDeltaNet.forward + + def patched_gated_delta_net_forward( + self, + hidden_states: torch.Tensor, + cache_params: Optional[Qwen3NextDynamicCache] = None, + cache_position: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + ): + hidden_states = apply_mask_to_padding_states(hidden_states, attention_mask) + + # Set up dimensions for reshapes later + batch_size, seq_len, _ = hidden_states.shape + + use_precomputed_states = ( + cache_params is not None + and cache_params.has_previous_state + and seq_len == 1 + and cache_position is not None + ) + + # getting projected states from cache if it exists + if cache_params is not None: + conv_state = cache_params.conv_states[self.layer_idx] + recurrent_state = cache_params.recurrent_states[self.layer_idx] + + projected_states_qkvz = self.in_proj_qkvz(hidden_states) + projected_states_ba = self.in_proj_ba(hidden_states) + query, key, value, z, b, a = self.fix_query_key_value_ordering( + projected_states_qkvz, projected_states_ba + ) + query, key, value = ( + x.reshape(x.shape[0], x.shape[1], -1) for x in (query, key, value) + ) + + mixed_qkv = torch.cat((query, key, value), dim=-1) + mixed_qkv = mixed_qkv.transpose(1, 2) + + if use_precomputed_states: + # 2. Convolution sequence transformation + # NOTE: the conv state is updated in `causal_conv1d_update` + mixed_qkv = self.causal_conv1d_update( + mixed_qkv, + conv_state, + self.conv1d.weight.squeeze(1), + self.conv1d.bias, + self.activation, + ) + else: + if cache_params is not None: + conv_state = F.pad( + mixed_qkv, (self.conv_kernel_size - mixed_qkv.shape[-1], 0) + ) + cache_params.conv_states[self.layer_idx] = conv_state + if self.causal_conv1d_fn is not None: + mixed_qkv = self.causal_conv1d_fn( + x=mixed_qkv, + weight=self.conv1d.weight.squeeze(1), + bias=self.conv1d.bias, + activation=self.activation, + seq_idx=None, + ) + else: + mixed_qkv = F.silu(self.conv1d(mixed_qkv)[:, :, :seq_len]) + + mixed_qkv = mixed_qkv.transpose(1, 2) + query, key, value = torch.split( + mixed_qkv, + [ + self.key_dim, + self.key_dim, + self.value_dim, + ], + dim=-1, + ) + query = query.reshape(query.shape[0], query.shape[1], -1, self.head_k_dim) + key = key.reshape(key.shape[0], key.shape[1], -1, self.head_k_dim) + value = value.reshape(value.shape[0], value.shape[1], -1, self.head_v_dim) + + beta = b.sigmoid() + # If the model is loaded in fp16, without the .float() here, A might be -inf + g = -self.A_log.float().exp() * F.softplus(a.float() + self.dt_bias) + if self.num_v_heads // self.num_k_heads > 1: + query = query.repeat_interleave(self.num_v_heads // self.num_k_heads, dim=2) + key = key.repeat_interleave(self.num_v_heads // self.num_k_heads, dim=2) + + if not use_precomputed_states: + cu_seqlens = get_cu_seqlens(position_ids=position_ids) + core_attn_out, last_recurrent_state = self.chunk_gated_delta_rule( + query, + key, + value, + g=g, + beta=beta, + initial_state=None, + output_final_state=cache_params is not None, + use_qk_l2norm_in_kernel=True, + cu_seqlens=cu_seqlens, + ) + + else: + core_attn_out, last_recurrent_state = self.recurrent_gated_delta_rule( + query, + key, + value, + g=g, + beta=beta, + initial_state=recurrent_state, + output_final_state=cache_params is not None, + use_qk_l2norm_in_kernel=True, + ) + + # Update cache + if cache_params is not None: + cache_params.recurrent_states[self.layer_idx] = last_recurrent_state + + z_shape_og = z.shape + # reshape input data into 2D tensor + core_attn_out = core_attn_out.reshape(-1, core_attn_out.shape[-1]) + z = z.reshape(-1, z.shape[-1]) + core_attn_out = self.norm(core_attn_out, z) + core_attn_out = core_attn_out.reshape(z_shape_og) + core_attn_out = core_attn_out.reshape( + core_attn_out.shape[0], core_attn_out.shape[1], -1 + ) + + output = self.out_proj(core_attn_out) + return output + + # Apply the patches + Qwen3NextGatedDeltaNet.forward = patched_gated_delta_net_forward + + def unpatch(): + """Restore the original forward method""" + Qwen3NextGatedDeltaNet.forward = original_gated_delta_net_forward + + return unpatch + + +def patch_qwen3_next_imports(): + """Patch Qwen3Next imports to use try/except instead of is_flash_linear_attention_available.""" + try: + import transformers.models.qwen3_next.modeling_qwen3_next as qwen3_modeling + except ImportError: + LOG.warning("Qwen3Next model not found, skipping import patch") + return + + # Save original values for unpatch + original_FusedRMSNormGated = getattr(qwen3_modeling, "FusedRMSNormGated", None) + original_chunk_gated_delta_rule = getattr( + qwen3_modeling, "chunk_gated_delta_rule", None + ) + original_fused_recurrent_gated_delta_rule = getattr( + qwen3_modeling, "fused_recurrent_gated_delta_rule", None + ) + original_is_fast_path_available = getattr( + qwen3_modeling, "is_fast_path_available", False + ) + + try: + from fla.modules import FusedRMSNormGated + from fla.ops.gated_delta_rule import ( + chunk_gated_delta_rule, + fused_recurrent_gated_delta_rule, + ) + + qwen3_modeling.FusedRMSNormGated = FusedRMSNormGated + qwen3_modeling.chunk_gated_delta_rule = chunk_gated_delta_rule + qwen3_modeling.fused_recurrent_gated_delta_rule = ( + fused_recurrent_gated_delta_rule + ) + + # Force is_fast_path_available to be True + # fla has triton kernels for causal_conv1d + qwen3_modeling.is_fast_path_available = True + except ImportError: + qwen3_modeling.chunk_gated_delta_rule = None + qwen3_modeling.fused_recurrent_gated_delta_rule = None + qwen3_modeling.FusedRMSNormGated = None + + def unpatch(): + """Restore the original import values""" + qwen3_modeling.FusedRMSNormGated = original_FusedRMSNormGated + qwen3_modeling.chunk_gated_delta_rule = original_chunk_gated_delta_rule + qwen3_modeling.fused_recurrent_gated_delta_rule = ( + original_fused_recurrent_gated_delta_rule + ) + qwen3_modeling.is_fast_path_available = original_is_fast_path_available + + return unpatch + + +def patch_qwen3_next_modeling_packing(): + """Apply all Qwen3Next model patches.""" + patch_qwen3_next_imports() + patch_qwen3_next_decoder_layer() + patch_qwen3_next_gateddelta_layer() + + LOG.info("Applied Qwen3Next patch for packing") diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 726e601114..4741245e18 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -21,6 +21,7 @@ "qwen2_moe", "qwen3", "qwen3_moe", + "qwen3_next", "falcon", "phi", "phi3", diff --git a/tests/monkeypatch/test_qwen3_next_modeling_patch.py b/tests/monkeypatch/test_qwen3_next_modeling_patch.py new file mode 100644 index 0000000000..91d9fc1cf1 --- /dev/null +++ b/tests/monkeypatch/test_qwen3_next_modeling_patch.py @@ -0,0 +1,111 @@ +"""Integration tests for Qwen3 Next modeling patches.""" + +import pytest +import torch + +# Skip entire module if qwen3_next not available +qwen3_next = pytest.importorskip("transformers.models.qwen3_next.modeling_qwen3_next") + + +class TestQwen3NextModelingPatchIntegration: + """Test Qwen3 Next modeling patch integration.""" + + @pytest.mark.integration + def test_qwen3_next_decoder_layer_patch(self): + """Test that Qwen3Next decoder layer patch can be applied.""" + from axolotl.monkeypatch.models.qwen3_next.modeling import ( + patch_qwen3_next_decoder_layer, + ) + + # Store original method + original_forward = qwen3_next.Qwen3NextDecoderLayer.forward + + # Apply patch and get unpatch function + unpatch_fn = patch_qwen3_next_decoder_layer() + + # Verify patch was applied + assert qwen3_next.Qwen3NextDecoderLayer.forward != original_forward, ( + "decoder layer forward method was not patched" + ) + + # Verify the method is still callable + assert callable(qwen3_next.Qwen3NextDecoderLayer.forward), ( + "Patched method is not callable" + ) + + # Test unpatch function + if unpatch_fn: + unpatch_fn() + assert qwen3_next.Qwen3NextDecoderLayer.forward == original_forward, ( + "unpatch function did not restore original method" + ) + + @pytest.mark.integration + def test_qwen3_next_gateddelta_layer_patch(self): + """Test that Qwen3Next GatedDeltaNet patch can be applied.""" + from axolotl.monkeypatch.models.qwen3_next.modeling import ( + patch_qwen3_next_gateddelta_layer, + ) + + # Store original method + original_forward = qwen3_next.Qwen3NextGatedDeltaNet.forward + + # Apply patch and get unpatch function + unpatch_fn = patch_qwen3_next_gateddelta_layer() + + # Verify patch was applied + assert qwen3_next.Qwen3NextGatedDeltaNet.forward != original_forward, ( + "GatedDeltaNet forward method was not patched" + ) + + # Verify the method is still callable + assert callable(qwen3_next.Qwen3NextGatedDeltaNet.forward), ( + "Patched method is not callable" + ) + + # Test unpatch function + if unpatch_fn: + unpatch_fn() + assert qwen3_next.Qwen3NextGatedDeltaNet.forward == original_forward, ( + "unpatch function did not restore original method" + ) + + @pytest.mark.integration + def test_qwen3_next_imports_patch(self): + """Test that Qwen3Next imports patch can be applied without errors.""" + from axolotl.monkeypatch.models.qwen3_next.modeling import ( + patch_qwen3_next_imports, + ) + + # Apply patch - should not raise any exceptions even if modules unavailable + unpatch_fn = patch_qwen3_next_imports() + + # Test that unpatch function is returned (or None if skipped) + assert unpatch_fn is None or callable(unpatch_fn), ( + "patch_qwen3_next_imports should return None or callable unpatch function" + ) + + @pytest.mark.integration + def test_qwen3_next_modeling_packing_patch(self): + """Test that all Qwen3Next modeling patches can be applied together.""" + from axolotl.monkeypatch.models.qwen3_next.modeling import ( + patch_qwen3_next_modeling_packing, + ) + + # This should not raise any exceptions + patch_qwen3_next_modeling_packing() + + +@pytest.mark.integration +def test_get_cu_seqlens_utility(): + """Test the get_cu_seqlens utility function.""" + from axolotl.monkeypatch.models.qwen3_next.modeling import get_cu_seqlens + + # Test with simple position_ids + position_ids = torch.tensor([[0, 1, 2, 0, 1]]) + cu_seqlens = get_cu_seqlens(position_ids) + assert cu_seqlens.dtype == torch.int32, "Should be int32 dtype" + + # Should return tensor with start positions and total length + expected = torch.tensor([0, 3, 5], dtype=torch.int32) + assert torch.equal(cu_seqlens, expected), f"Expected {expected}, got {cu_seqlens}" From 55d1be2ae6f037081d7bcca55d93753fc2e10702 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 23 Sep 2025 21:22:15 +0700 Subject: [PATCH 0975/1405] fix: unify default for conversations_field [skip-e2e] (#3070) * fix: unify default for conversations_field * fix: suggestion to remove defaults --- docs/multimodal.qmd | 1 - .../archived/deepcoder/deepcoder-14B-preview-lora.yml | 4 ---- .../deepcogito/cogito-v1-preview-llama-3B-lora.yml | 4 ---- .../deepcogito/cogito-v1-preview-qwen-14B-lora.yml | 4 ---- examples/gemma3/gemma-3-4b-vision-qlora.yml | 2 +- examples/llama-3/instruct-lora-8b.yml | 9 --------- .../llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml | 1 - examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml | 1 - examples/phi/lora-3.5.yaml | 9 --------- examples/qwen2-vl/lora-7b.yaml | 2 +- examples/qwen2_5-vl/lora-7b.yaml | 2 +- src/axolotl/core/datasets/transforms/chat_builder.py | 10 +++++----- 12 files changed, 8 insertions(+), 41 deletions(-) diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index 413404195c..3a28b579a1 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -42,7 +42,6 @@ datasets: - path: HuggingFaceH4/llava-instruct-mix-vsft type: chat_template split: train[:1%] - field_messages: messages # (optional) if doing lora, only finetune the Language model, # leave the vision model and vision tower frozen diff --git a/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml b/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml index 2202091d58..3223ec19a7 100644 --- a/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml +++ b/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml @@ -9,10 +9,6 @@ strict: false datasets: - path: fozziethebeat/alpaca_messages_2k_test type: chat_template - field_messages: messages - message_property_mappings: - role: role - content: content dataset_prepared_path: val_set_size: 0.05 diff --git a/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml b/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml index fc9a75e3f8..97d1bb6b32 100644 --- a/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml +++ b/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml @@ -9,10 +9,6 @@ strict: false datasets: - path: fozziethebeat/alpaca_messages_2k_test type: chat_template - field_messages: messages - message_property_mappings: - role: role - content: content dataset_prepared_path: val_set_size: 0.05 diff --git a/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml b/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml index b527edc6ff..b80cc5bc0f 100644 --- a/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml +++ b/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml @@ -9,10 +9,6 @@ strict: false datasets: - path: fozziethebeat/alpaca_messages_2k_test type: chat_template - field_messages: messages - message_property_mappings: - role: role - content: content dataset_prepared_path: val_set_size: 0.05 diff --git a/examples/gemma3/gemma-3-4b-vision-qlora.yml b/examples/gemma3/gemma-3-4b-vision-qlora.yml index e9e606b69f..b42b6b4928 100644 --- a/examples/gemma3/gemma-3-4b-vision-qlora.yml +++ b/examples/gemma3/gemma-3-4b-vision-qlora.yml @@ -18,7 +18,7 @@ datasets: - path: HuggingFaceH4/llava-instruct-mix-vsft type: chat_template split: train[:1%] - field_messages: messages + dataset_prepared_path: last_run_prepared val_set_size: 0.01 output_dir: ./outputs/out diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml index 69e17b9cfc..401df1d721 100644 --- a/examples/llama-3/instruct-lora-8b.yml +++ b/examples/llama-3/instruct-lora-8b.yml @@ -12,15 +12,6 @@ chat_template: llama3 datasets: - path: fozziethebeat/alpaca_messages_2k_test type: chat_template - field_messages: messages - message_property_mappings: - role: role - content: content - roles: - user: - - user - assistant: - - assistant dataset_prepared_path: val_set_size: 0.05 diff --git a/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml b/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml index 9975949bb4..4136dc14ac 100644 --- a/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml @@ -46,7 +46,6 @@ datasets: - path: HuggingFaceH4/llava-instruct-mix-vsft type: chat_template split: train[:1%] - field_messages: messages dataset_prepared_path: last_run_prepared val_set_size: 0.0 diff --git a/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml b/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml index ac7e05659f..5972c2ae37 100644 --- a/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml +++ b/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml @@ -45,7 +45,6 @@ datasets: - path: HuggingFaceH4/llava-instruct-mix-vsft type: chat_template split: train[:1%] - field_messages: messages dataset_prepared_path: last_run_prepared val_set_size: 0.0 diff --git a/examples/phi/lora-3.5.yaml b/examples/phi/lora-3.5.yaml index a6fa15d98f..c10014dab1 100644 --- a/examples/phi/lora-3.5.yaml +++ b/examples/phi/lora-3.5.yaml @@ -12,15 +12,6 @@ chat_template: phi_3 datasets: - path: fozziethebeat/alpaca_messages_2k_test type: chat_template - field_messages: messages - message_property_mappings: - role: role - content: content - roles: - user: - - user - assistant: - - assistant dataset_prepared_path: val_set_size: 0.05 diff --git a/examples/qwen2-vl/lora-7b.yaml b/examples/qwen2-vl/lora-7b.yaml index 8ea6081999..285a35cbb5 100644 --- a/examples/qwen2-vl/lora-7b.yaml +++ b/examples/qwen2-vl/lora-7b.yaml @@ -11,7 +11,7 @@ datasets: - path: HuggingFaceH4/llava-instruct-mix-vsft type: chat_template split: train[:1%] - field_messages: messages + dataset_prepared_path: last_run_prepared val_set_size: 0.0 output_dir: ./outputs/out diff --git a/examples/qwen2_5-vl/lora-7b.yaml b/examples/qwen2_5-vl/lora-7b.yaml index 13a97dec3e..7d499d841f 100644 --- a/examples/qwen2_5-vl/lora-7b.yaml +++ b/examples/qwen2_5-vl/lora-7b.yaml @@ -11,7 +11,7 @@ datasets: - path: HuggingFaceH4/llava-instruct-mix-vsft type: chat_template split: train[:1%] - field_messages: messages + dataset_prepared_path: last_run_prepared val_set_size: 0.0 output_dir: ./outputs/out diff --git a/src/axolotl/core/datasets/transforms/chat_builder.py b/src/axolotl/core/datasets/transforms/chat_builder.py index 8f2013027b..0de0ecb403 100644 --- a/src/axolotl/core/datasets/transforms/chat_builder.py +++ b/src/axolotl/core/datasets/transforms/chat_builder.py @@ -8,7 +8,7 @@ def chat_message_transform_builder( train_on_inputs=False, - conversations_field: str = "conversations", + conversations_field: str = "messages", message_field_role: str | list[str] | None = None, # commonly "role" message_field_content: str | list[str] | None = None, # commonly "content" message_field_training: str | list[str] | None = None, # commonly "weight" @@ -20,13 +20,13 @@ def chat_message_transform_builder( If True, the transform will train on the inputs. If False, the transform will train on the targets. Defaults to False. conversations_field (str, optional): - The field name of the conversations. Defaults to "conversations". + The field name of the conversations. Defaults to "messages". message_field_role (str | list[str], optional): - The field name of the role. Defaults to "role". + The field name of the role. message_field_content (str | list[str], optional): - The field name of the message content. Defaults to "content". + The field name of the message content. message_field_training (str | list[str], optional): - The field name of the train/weight. Defaults to "weight". + The field name of the train/weight. Returns: Callable: From b3b92687c4ba8792d343b6b1a616f541840db8b3 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 24 Sep 2025 13:48:38 +0700 Subject: [PATCH 0976/1405] chore: rename gemma3 270m config (#3174) --- examples/gemma3/{270m-qlora.yml => gemma-3-270m-qlora.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/gemma3/{270m-qlora.yml => gemma-3-270m-qlora.yml} (100%) diff --git a/examples/gemma3/270m-qlora.yml b/examples/gemma3/gemma-3-270m-qlora.yml similarity index 100% rename from examples/gemma3/270m-qlora.yml rename to examples/gemma3/gemma-3-270m-qlora.yml From 6bc959342b3ff687cbaca700a310594190f3ab57 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Wed, 24 Sep 2025 13:18:44 -0400 Subject: [PATCH 0977/1405] remove unused dep (#3180) --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 3a44f0ae9e..3e642b57ff 100644 --- a/setup.py +++ b/setup.py @@ -124,7 +124,6 @@ def get_package_version(): "ring-flash-attn": [ "flash-attn==2.8.3", "ring-flash-attn>=0.1.7", - "yunchang==0.6.0", ], "deepspeed": [ "deepspeed==0.17.5", From 856ff12171e262dc56f6b6b890a5e3e8e31b7dee Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 25 Sep 2025 03:13:49 +0700 Subject: [PATCH 0978/1405] feat(doc): add optimizations table of content to our improvements (#3175) [skip ci] * chore: format * feat: add usage for alst * chore: wording * feat: add optimizations doc * Apply suggestion from @SalmanMohammadi Co-authored-by: salman * Update docs/dataset-formats/index.qmd Co-authored-by: salman * feat: add alst, act offloading, nd parallelism, use relative links, and fix format * chore: comments --------- Co-authored-by: salman --- _quarto.yml | 1 + docs/dataset-formats/index.qmd | 2 +- docs/optimizations.qmd | 133 +++++++++++++++++++++++++++++++++ docs/qat.qmd | 1 + examples/alst/README.md | 21 ++++++ 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 docs/optimizations.qmd diff --git a/_quarto.yml b/_quarto.yml index 3ffb0e6277..fad3f67863 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -267,6 +267,7 @@ website: - docs/dataset_loading.qmd - docs/qat.qmd - docs/quantize.qmd + - docs/optimizations.qmd - section: "Core Concepts" contents: diff --git a/docs/dataset-formats/index.qmd b/docs/dataset-formats/index.qmd index a0113db07e..715e3ef202 100644 --- a/docs/dataset-formats/index.qmd +++ b/docs/dataset-formats/index.qmd @@ -61,7 +61,7 @@ While we recommend `.jsonl`, you can also use the other formats (`csv`, `parquet ### Pre-training without streaming -On the rare case that the dataset is small and can be loaded entirely into memory, another approach to running pre-training is to use the `completion` format. This would mean that the entire dataset is pre-tokenized instead of on-demand in streaming. +In the case that the dataset is small and can be loaded entirely into memory, another approach to running pre-training is to use the `completion` format. This would mean that the entire dataset is pre-tokenized instead of on-demand in streaming. One benefit of this is that the tokenization can be performed separately on a CPU-only machine, and then transferred to a GPU machine for training to save costs. diff --git a/docs/optimizations.qmd b/docs/optimizations.qmd new file mode 100644 index 0000000000..967ec2d34a --- /dev/null +++ b/docs/optimizations.qmd @@ -0,0 +1,133 @@ +--- +title: Optimizations Guide +description: A guide to the performance and memory optimizations available in Axolotl. +--- + +Axolotl includes numerous optimizations to speed up training, reduce memory usage, and handle large models. + +This guide provides a high-level overview and directs you to the detailed documentation for each feature. + +## Speed Optimizations + +These optimizations focus on increasing training throughput and reducing total training time. + +### Sample Packing + +Improves GPU utilization by combining multiple short sequences into a single packed sequence for training. This requires enabling one of the [attention](#attention-implementations) implementations below. + +- **Config:** `sample_packing: true` +- **Learn more:** [Sample Packing](multipack.qmd) + +### Attention Implementations + +Using an optimized attention implementation is critical for training speed. + +- **[Flash Attention 2](https://github.com/Dao-AILab/flash-attention)**: `flash_attention: true`. **(Recommended)** The industry standard for fast attention on modern GPUs. Requires Ampere or higher. For AMD, check [AMD Support](https://github.com/Dao-AILab/flash-attention?tab=readme-ov-file#amd-rocm-support). +- **[Flex Attention](https://pytorch.org/blog/flexattention/)**: `flex_attention: true`. +- **[SDP Attention](https://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html)**: `sdp_attention: true`. PyTorch's native implementation. +- **[Xformers](https://github.com/facebookresearch/xformers)**: `xformers_attention: true`. Works with FP16. + +*Note: You should only enable one attention backend.* + +### LoRA Optimizations + +Leverages optimized kernels to accelerate LoRA training and reduce memory usage. + +- **Learn more:** [LoRA Optimizations Documentation](lora_optims.qmd) + +## Memory Optimizations + +These techniques help you fit larger models or use bigger batch sizes on your existing hardware. + +### Parameter Efficient Finetuning (LoRA & QLoRA) + +Drastically reduces memory by training a small set of "adapter" parameters instead of the full model. This is the most common and effective memory-saving technique. + +- Examples: Find configs with `lora` or `qlora` in the [examples directory](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/llama-3). +- Config Reference: See `adapter`, `load_in_4bit`, and `load_in_8bit` in the [Configuration Reference](config-reference.qmd). + +### Gradient Checkpointing & Activation Offloading + +These techniques save VRAM by changing how activations are handled. + +- Gradient Checkpointing: re-computes activations during the backward pass, trading compute time for VRAM. +- Activation Offloading: moves activations to CPU RAM or disk, trading I/O overhead for VRAM. +- Learn more: [Gradient Checkpointing and Offloading Docs](gradient_checkpointing.qmd) + +### Cut Cross Entropy (CCE) + +Reduces VRAM usage by using an optimized cross-entropy loss calculation. + +- **Learn more:** [Custom Integrations - CCE](custom_integrations.qmd#cut-cross-entropy) + +### Liger Kernels + +Provides efficient Triton kernels to improve training speed and reduce memory usage. + +- **Learn more:** [Custom Integrations - Liger Kernels](custom_integrations.qmd#liger-kernels) + +## Long Context Models + +Techniques to train models on sequences longer than their original context window. + +### RoPE Scaling + +Extends a model's context window by interpolating its Rotary Position Embeddings. + +- **Config:** Pass the `rope_scaling` config under the `overrides_of_model_config: `. To learn how to set RoPE, check the respective model config. + +### Sequence Parallelism + +Splits long sequences across multiple GPUs, enabling training with sequence lengths that would not fit on a single device. + +- **Learn more:** [Sequence Parallelism Documentation](sequence_parallelism.qmd) + +### Artic Long Sequence Training (ALST) + +ALST is a recipe that combines several techniques to train long-context models efficiently. It typically involves: + +- TiledMLP to reduce memory usage in MLP layers. +- Tiled Loss functions (like [CCE](#cut-cross-entropy-(cce) or [Liger](#liger-kernels)). +- Activation Offloading to CPU. + +- Example: [ALST Example Configuration](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/alst) + +## Large Models (Distributed Training) + +To train models that don't fit on a single GPU, you'll need to use a distributed training strategy like FSDP or DeepSpeed. These frameworks shard the model weights, gradients, and optimizer states across multiple GPUs and nodes. + +- **Learn more:** [Multi-GPU Guide](multi-gpu.qmd) +- **Learn more:** [Multi-Node Guide](multi-node.qmd) + +### N-D Parallelism (Beta) + +For advanced scaling, Axolotl allows you to compose different parallelism techniques (e.g., Data, Tensor, Sequence Parallelism). This is a powerful approach to train an extremely large model by overcoming multiple bottlenecks at once. + +- **Learn more:** [N-D Parallelism Guide](nd_parallelism.qmd) + + +## Quantization + +Techniques to reduce the precision of model weights for memory savings. + +### 4-bit Training (QLoRA) + +The recommended approach for quantization-based training. It loads the base model in 4-bit using `bitsandbytes` and then trains QLoRA adapters. See [Adapter Finetuning](#adapter-finetuning-lora-qlora) for details. + +### FP8 Training + +Enables training with 8-bit floating point precision on supported hardware (e.g., NVIDIA Hopper series GPUs) for significant speed and memory gains. + +- **Example:** [Llama 3 FP8 FSDP Example](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/llama-3/3b-fp8-fsdp2.yaml) + +### Quantization Aware Training (QAT) + +Simulates quantization effects during training, helping the model adapt and potentially improving the final accuracy of the quantized model. + +- **Learn more:** [QAT Documentation](qat.qmd) + +### GPTQ + +Allows you to finetune LoRA adapters on top of a model that has already been quantized using the GPTQ method. + +- **Example:** [GPTQ LoRA Example](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/llama-2/gptq-lora.yml) diff --git a/docs/qat.qmd b/docs/qat.qmd index ad97790666..91fe5180cb 100644 --- a/docs/qat.qmd +++ b/docs/qat.qmd @@ -30,6 +30,7 @@ qat: ``` We support the following quantization schemas: + - `Int4WeightOnly` (requires the `fbgemm-gpu` extra when installing Axolotl) - `Int8DynamicActivationInt4Weight` - `Float8DynamicActivationFloat8Weight` diff --git a/examples/alst/README.md b/examples/alst/README.md index 7f194d299e..6d201f8264 100644 --- a/examples/alst/README.md +++ b/examples/alst/README.md @@ -7,3 +7,24 @@ techniques. It is a combination of: - Activation Offloading: Offload activations to CPU RAM to reduce memory usage For more information, you can check out the ALST paper [here](https://www.arxiv.org/abs/2506.13996). + +## Usage + +```yaml +tiled_mlp: true + +# See Sequence Parallelism docs +# https://docs.axolotl.ai/docs/sequence_parallelism.html +context_parallel_size: int + +plugins: +# See Cut Cross Entropy docs +# https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +# or Liger Kernel docs +# https://docs.axolotl.ai/docs/custom_integrations.html#liger-kernels + - axolotl.integrations.liger.LigerPlugin +# ... + +``` From e8b962d47f89041fd6ca6e84c7ece38b8baa34a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=8D=8E=E6=9D=B0?= Date: Thu, 25 Sep 2025 13:06:21 +0800 Subject: [PATCH 0979/1405] feat: support training with JSON string tool arguments (#3136) * feat: support training with JSON string tool arguments; fix PyArrow data type inconsistent error * feat: raise error for tool call arguments decode * Add test_chat_templates_tool_call_string_arguments.py Add test for string arguments * fix: change to correct qwen3 tokenizer * fix: update docs to clarify arguments json * chore: lint * fix: duplicate * chore: revert * feat: add error to faq * fix: remove duplicate fixture --------- Co-authored-by: caoqinping Co-authored-by: gamersover-blog <1611885128@qq.com> Co-authored-by: NanoCode012 --- docs/dataset-formats/conversation.qmd | 8 + docs/faq.qmd | 4 + .../prompt_strategies/chat_template.py | 17 ++ tests/prompt_strategies/conftest.py | 9 + ...est_chat_template_ds_schema_unification.py | 10 - .../test_chat_templates_thinking.py | 10 - ...at_templates_tool_call_string_arguments.py | 214 ++++++++++++++++++ 7 files changed, 252 insertions(+), 20 deletions(-) create mode 100644 tests/prompt_strategies/test_chat_templates_tool_call_string_arguments.py diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index d53c685983..870a2b67d3 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -212,6 +212,14 @@ Instead of passing `tools` via the system prompt, an alternative method would be Tools need to follow [JSON schema](https://json-schema.org/learn/getting-started-step-by-step). ::: +::: {.callout-warning} +If you have tool arguments with same name but different dtypes (like `"time": string` and `"time": number`), please save `arguments: ` as JSON string to prevent `datasets` from having casting issues. + +``` +"arguments": "{\"...\": \"...\"}" +``` +::: + Example config for Llama4: ```yaml chat_template: llama4 diff --git a/docs/faq.qmd b/docs/faq.qmd index 08d439af75..ffc29d35df 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -140,3 +140,7 @@ description: Frequently asked questions **Q: `ValueError("Backward pass should have cleared tracker of all tensors")` > A: This may happen due to edge cases in using the modern OffloadActivations context manager for CUDA streams. If you encounter this error, you may have success using the naive implementation with `offload_activations: legacy` in your YAML. + +**Q: `Error parsing tool_calls arguments as JSON.` + +> A: There is an error parsing string arguments to a dict. Please check your dataset and the error message for more details. diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index cb3e3dfb15..f4dcbd7cd4 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -2,6 +2,7 @@ HF Chat Templates prompt strategy """ +import json from collections import defaultdict from typing import TYPE_CHECKING, Any, Dict, List, Set, Union @@ -794,6 +795,22 @@ def transform_message(self, message: dict) -> dict: if val is not None: transformed_message[key] = val + if "tool_calls" in transformed_message and transformed_message["tool_calls"]: + for tool_call in transformed_message["tool_calls"]: + if "function" in tool_call and "arguments" in tool_call["function"]: + args = tool_call["function"]["arguments"] + if isinstance(args, str): + try: + tool_call["function"]["arguments"] = json.loads(args) + except json.JSONDecodeError as e: + LOG.error( + f"Error parsing tool_calls arguments as JSON. " + f"Function: {tool_call.get('function', {}).get('name', 'unknown')}, " + f"Arguments string: {args!r}, " + f"Error: {e}" + ) + raise + return transformed_message def _get_images(self, prompt): diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index 12c4bcd937..0af7b3e93f 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -177,6 +177,15 @@ def fixture_devstral_1_1_tokenizer(): return tokenizer +@pytest.fixture(name="qwen3_tokenizer") +def qwen3_tokenizer_fixture( + download_qwen3_half_billion_model, +): # pylint: disable=unused-argument,redefined-outer-name + tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B") + + return tokenizer + + @pytest.fixture(name="mistralv03_tokenizer_chat_template_jinja") def fixture_mistralv03_chat_template_jinja_w_system() -> str: return '{%- if messages[0]["role"] == "system" %}\n {%- set system_message = messages[0]["content"] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set loop_messages = messages %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n{%- set user_messages = loop_messages | selectattr("role", "equalto", "user") | list %}\n\n{#- This block checks for alternating user/assistant messages, skipping tool calling messages #}\n{%- set ns = namespace() %}\n{%- set ns.index = 0 %}\n{%- for message in loop_messages %}\n {%- if not (message.role == "tool" or message.role == "tool_results" or (message.tool_calls is defined and message.tool_calls is not none)) %}\n {%- if (message["role"] == "user") != (ns.index % 2 == 0) %}\n {{- raise_exception("After the optional system message, conversation roles must alternate user/assistant/user/assistant/...") }}\n {%- endif %}\n {%- set ns.index = ns.index + 1 %}\n {%- endif %}\n{%- endfor %}\n\n{{- bos_token }}\n{%- for message in loop_messages %}\n {%- if message["role"] == "user" %}\n {%- if tools is not none and (message == user_messages[-1]) %}\n {{- "[AVAILABLE_TOOLS] [" }}\n {%- for tool in tools %}\n {%- set tool = tool.function %}\n {{- \'{"type": "function", "function": {\' }}\n {%- for key, val in tool.items() if key != "return" %}\n {%- if val is string %}\n {{- \'"\' + key + \'": "\' + val + \'"\' }}\n {%- else %}\n {{- \'"\' + key + \'": \' + val|tojson }}\n {%- endif %}\n {%- if not loop.last %}\n {{- ", " }}\n {%- endif %}\n {%- endfor %}\n {{- "}}" }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- else %}\n {{- "]" }}\n {%- endif %}\n {%- endfor %}\n {{- "[/AVAILABLE_TOOLS]" }}\n {%- endif %}\n {%- if loop.first and system_message is defined %}\n {{- "[INST] " + system_message + "\\n\\n" + message["content"] + "[/INST]" }}\n {%- else %}\n {{- "[INST] " + message["content"] + "[/INST]" }}\n {%- endif %}\n {%- elif message.tool_calls is defined and message.tool_calls is not none %}\n {{- "[TOOL_CALLS] [" }}\n {%- for tool_call in message.tool_calls %}\n {%- set out = tool_call.function|tojson %}\n {{- out[:-1] }}\n {%- if not tool_call.id is defined or tool_call.id|length != 9 %}\n {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }}\n {%- endif %}\n {{- \', "id": "\' + tool_call.id + \'"}\' }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- else %}\n {{- "]" + eos_token }}\n {%- endif %}\n {%- endfor %}\n {%- elif message["role"] == "assistant" %}\n {{- " " + message["content"]|trim + eos_token}}\n {%- elif message["role"] == "tool_results" or message["role"] == "tool" %}\n {%- if message.content is defined and message.content.content is defined %}\n {%- set content = message.content.content %}\n {%- else %}\n {%- set content = message.content %}\n {%- endif %}\n {{- \'[TOOL_RESULTS] {"content": \' + content|string + ", " }}\n {%- if not message.tool_call_id is defined or message.tool_call_id|length != 9 %}\n {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }}\n {%- endif %}\n {{- \'"call_id": "\' + message.tool_call_id + \'"}[/TOOL_RESULTS]\' }}\n {%- else %}\n {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }}\n {%- endif %}\n{%- endfor %}\n' diff --git a/tests/prompt_strategies/test_chat_template_ds_schema_unification.py b/tests/prompt_strategies/test_chat_template_ds_schema_unification.py index e8d35e9744..4f4e32208b 100644 --- a/tests/prompt_strategies/test_chat_template_ds_schema_unification.py +++ b/tests/prompt_strategies/test_chat_template_ds_schema_unification.py @@ -6,7 +6,6 @@ import pytest from datasets import Dataset -from transformers import AutoTokenizer from axolotl.prompt_strategies.chat_template import StrategyLoader from axolotl.utils.dict import DictDefault @@ -23,15 +22,6 @@ def fixture_messages_w_tools(): return Dataset.from_list(rows) -@pytest.fixture(name="qwen3_tokenizer") -def qwen3_tokenizer_fixture( - download_qwen3_half_billion_model, -): - tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B") - - return tokenizer - - @pytest.fixture(name="qwen3_prompt_strategy") def qwen3_chat_template_strategy(qwen3_tokenizer): cfg = DictDefault( diff --git a/tests/prompt_strategies/test_chat_templates_thinking.py b/tests/prompt_strategies/test_chat_templates_thinking.py index 5475666a54..054012e00b 100644 --- a/tests/prompt_strategies/test_chat_templates_thinking.py +++ b/tests/prompt_strategies/test_chat_templates_thinking.py @@ -4,7 +4,6 @@ import pytest from datasets import Dataset -from transformers import AutoTokenizer from axolotl.prompt_strategies.chat_template import ( load, @@ -56,15 +55,6 @@ def messages_w_reasoning_fixture(): ) -@pytest.fixture(name="qwen3_tokenizer") -def qwen3_tokenizer_fixture( - download_qwen3_half_billion_model, -): - tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B") - - return tokenizer - - class TestSplitThinking: """ test class to make sure datasets with reasoning content conforms to the chat_template strategy diff --git a/tests/prompt_strategies/test_chat_templates_tool_call_string_arguments.py b/tests/prompt_strategies/test_chat_templates_tool_call_string_arguments.py new file mode 100644 index 0000000000..7de21b9406 --- /dev/null +++ b/tests/prompt_strategies/test_chat_templates_tool_call_string_arguments.py @@ -0,0 +1,214 @@ +""" +Tests for handling json tool content +""" + +import json + +import pytest +from datasets import Dataset + +from axolotl.prompt_strategies.chat_template import ( + load, +) +from axolotl.utils.dict import DictDefault + + +@pytest.fixture(name="qwen3_instruct_prompt_strategy") +def qwen3_instruct_chat_template_strategy(qwen3_tokenizer): + strategy = load( + qwen3_tokenizer, + DictDefault( + { + "train_on_inputs": False, + "sequence_len": 512, + } + ), + DictDefault( + { + "chat_template": "qwen3", + "message_field_role": "role", + "message_field_content": "content", + "message_property_mappings": { + "role": "role", + "content": "content", + }, + "roles": { + "user": ["user"], + "assistant": ["assistant"], + "system": ["system"], + }, + "field_messages": "messages", + } + ), + ) + return strategy + + +class TestQwen3IdenticalConversationArgs: + """ + Test Qwen3 tools is identical between JSON and dict + """ + + @pytest.fixture(name="conversation_dict_args_dataset") + def fixture_conversation_dict_args_dataset(self): + """ + Provides a dataset with conversation where arguments is a dict. + """ + user_content = "What is the weather in Boston?" + function_name = "get_current_weather" + arguments_dict = {"location": "Boston, MA", "unit": "celsius"} + + data = [ + { + "messages": [ + {"role": "user", "content": user_content}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "function": { + "name": function_name, + "arguments": arguments_dict, # dict格式 + } + } + ], + }, + ], + } + ] + return Dataset.from_list(data) + + @pytest.fixture(name="conversation_str_args_dataset") + def fixture_conversation_str_args_dataset(self): + """ + Provides a dataset with conversation where arguments is a JSON string. + """ + user_content = "What is the weather in Boston?" + function_name = "get_current_weather" + arguments_dict = {"location": "Boston, MA", "unit": "celsius"} + arguments_str = json.dumps(arguments_dict) + + data = [ + { + "messages": [ + {"role": "user", "content": user_content}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "function": { + "name": function_name, + "arguments": arguments_str, # str格式 + } + } + ], + }, + ], + } + ] + return Dataset.from_list(data) + + @pytest.fixture(name="conversation_mixed_time_types_dataset") + def fixture_conversation_mixed_time_types_dataset(self): + """ + Provides a dataset where 'time' field has different types in different tool calls. + """ + data = [ + { + "messages": [ + { + "role": "user", + "content": "Get weather information at different times", + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "function": { + "name": "func1", + "arguments": json.dumps( + {"time": "2025-08-01"} + ), # string type + } + }, + { + "function": { + "name": "func2", + "arguments": json.dumps( + {"time": 1690876800} + ), # number type + } + }, + ], + }, + ], + } + ] + return Dataset.from_list(data) + + def test_dict_and_str_args_produce_identical_output( + self, + conversation_dict_args_dataset, + conversation_str_args_dataset, + qwen3_instruct_prompt_strategy, + qwen3_tokenizer, + ): + """ + Tests that after tokenization and decoding, the outputs for both + dict and string `arguments` are exactly the same. + """ + processed_dict_args = conversation_dict_args_dataset.map( + qwen3_instruct_prompt_strategy.tokenize_prompt, + batched=True, + remove_columns=["messages"], + ) + + processed_str_args = conversation_str_args_dataset.map( + qwen3_instruct_prompt_strategy.tokenize_prompt, + batched=True, + remove_columns=["messages"], + ) + + decoded_prompt_from_dict = qwen3_tokenizer.decode( + processed_dict_args[0]["input_ids"] + ) + + decoded_prompt_from_str = qwen3_tokenizer.decode( + processed_str_args[0]["input_ids"] + ) + + assert decoded_prompt_from_dict == decoded_prompt_from_str, ( + f"Dict format output:\n{decoded_prompt_from_dict}\n" + f"String format output:\n{decoded_prompt_from_str}" + ) + + assert ( + processed_dict_args[0]["input_ids"] == processed_str_args[0]["input_ids"] + ), "The tokenized input_ids should be identical for dict and str arguments" + + def test_str_args_with_mixed_time_types_no_error( + self, + conversation_mixed_time_types_dataset, + qwen3_instruct_prompt_strategy, + qwen3_tokenizer, + ): + """ + Tests that when 'time' field has different types (string vs number) + in different tool calls, str format arguments don't cause errors. + """ + processed = conversation_mixed_time_types_dataset.map( + qwen3_instruct_prompt_strategy.tokenize_prompt, + batched=True, + remove_columns=["messages"], + ) + + assert len(processed) == 1 + assert "input_ids" in processed[0] + assert len(processed[0]["input_ids"]) > 0 + + decoded = qwen3_tokenizer.decode(processed[0]["input_ids"]) + assert "2025-08-01" in decoded, "String time value should be present" + assert "1690876800" in decoded, "Number time value should be present" From 33975ce4bcec0f34221e1d54fe985d2c743b4033 Mon Sep 17 00:00:00 2001 From: miketung Date: Thu, 25 Sep 2025 19:06:16 +0900 Subject: [PATCH 0980/1405] feat(qwen3-next): Adds targeting of shared expert and attention modules (#3183) * Adds targetting of shared expert and attention modules in each layer * Update VRAM usage --------- Co-authored-by: Mike Tung --- examples/qwen3-next/README.md | 2 +- examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/qwen3-next/README.md b/examples/qwen3-next/README.md index eb0d5fd28d..678175fd40 100644 --- a/examples/qwen3-next/README.md +++ b/examples/qwen3-next/README.md @@ -38,7 +38,7 @@ pip3 uninstall -y causal-conv1d && pip3 install flash-linear-attention==0.3.2 axolotl train examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml ``` -This config uses about 41.7 GiB VRAM. +This config uses about 45.62 GiB VRAM. Let us know how it goes. Happy finetuning! 🚀 diff --git a/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml b/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml index 11481dcd31..db841beab3 100644 --- a/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml +++ b/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml @@ -27,6 +27,14 @@ lora_r: 16 lora_alpha: 8 lora_dropout: 0.05 lora_target_modules: + - linear_attn.in_proj_ba + - linear_attn.in_proj_qkvz + - linear_attn.out_proj + - shared_expert.up_proj + - shared_expert.down_proj + - shared_expert.gate_proj + - shared_expert_gate + - mlp.gate - q_proj - v_proj - k_proj From f9748c4dc54430d7a12defa4ea76748db0a07f4f Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Thu, 25 Sep 2025 12:03:50 -0400 Subject: [PATCH 0981/1405] Cp fix (#3182) * patch transformers to allow CP + FA2 * nits * only patch in CP > 1 case --- src/axolotl/loaders/patch_manager.py | 7 ++ .../transformers/trainer_context_parallel.py | 68 +++++++++++++++++++ .../test_trainer_context_parallel_patch.py | 66 ++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 src/axolotl/monkeypatch/transformers/trainer_context_parallel.py create mode 100644 tests/monkeypatch/test_trainer_context_parallel_patch.py diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 3d4b7b96ba..1e46f5c34b 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -84,6 +84,13 @@ def _apply_transformers_patches(self): patch_evaluation_loop() patch_maybe_log_save_evaluate() + if self.cfg.context_parallel_size > 1: + from axolotl.monkeypatch.transformers.trainer_context_parallel import ( + patch_prepare_context_parallel_inputs, + ) + + patch_prepare_context_parallel_inputs() + def apply_post_model_load_patches(self, model: PreTrainedModel): """Apply patches that require the model instance.""" self._apply_llama_flash_attn_patches(model) diff --git a/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py b/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py new file mode 100644 index 0000000000..74a35e83fc --- /dev/null +++ b/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py @@ -0,0 +1,68 @@ +"""Monkey patch to allow context parallelism with FlashAttention in HF Trainer.""" + +from __future__ import annotations + +import importlib +import inspect + +from transformers import Trainer + +from axolotl.monkeypatch.utils import detab_code +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +GUARD_PATTERN = 'if model.config._attn_implementation != "sdpa":' +PATCHED_GUARD = ( + 'if model.config._attn_implementation not in ("sdpa", "flash_attention_2"):' +) + + +def patch_prepare_context_parallel_inputs() -> None: + """Relax the SDPA-only guard when running context parallelism with FlashAttention.""" + if getattr(Trainer, "_axolotl_prepare_context_parallel_inputs_patched", False): + LOG.debug("Trainer._prepare_context_parallel_inputs already patched") + return + + try: + original_source = inspect.getsource(Trainer._prepare_context_parallel_inputs) + except OSError as exc: # pragma: no cover - occurs when source is unavailable + LOG.warning("Unable to patch Trainer._prepare_context_parallel_inputs: %s", exc) + return + + if GUARD_PATTERN not in original_source: + LOG.warning( + "Expected guard not found in Trainer._prepare_context_parallel_inputs; \n" + "skipping FlashAttention context parallelism patch" + ) + return + + patched_source = original_source.replace(GUARD_PATTERN, PATCHED_GUARD) + patched_source, _ = detab_code(patched_source) + patched_source = patched_source.replace( + "def _prepare_context_parallel_inputs(", + "def axolotl_prepare_context_parallel_inputs(", + 1, + ) + + module_name = Trainer.__module__ + module = importlib.import_module(module_name) + + # import symbols referenced in the method so exec can succeed + items_to_import = [] + for item in dir(module): + if item in patched_source: + items_to_import.append(item) + + exec(f"from {module_name} import ({', '.join(items_to_import)})", globals()) + exec(patched_source, globals()) + + Trainer._original_prepare_context_parallel_inputs = ( + Trainer._prepare_context_parallel_inputs + ) + Trainer._prepare_context_parallel_inputs = axolotl_prepare_context_parallel_inputs + Trainer._axolotl_prepare_context_parallel_inputs_source = patched_source + Trainer._axolotl_prepare_context_parallel_inputs_patched = True + LOG.debug( + "Patched Trainer._prepare_context_parallel_inputs for FlashAttention + CP" + ) diff --git a/tests/monkeypatch/test_trainer_context_parallel_patch.py b/tests/monkeypatch/test_trainer_context_parallel_patch.py new file mode 100644 index 0000000000..84c883e91b --- /dev/null +++ b/tests/monkeypatch/test_trainer_context_parallel_patch.py @@ -0,0 +1,66 @@ +"""Tests for the HF Trainer context parallel patch.""" + +import pytest +from transformers import Trainer + +from axolotl.monkeypatch.transformers.trainer_context_parallel import ( + GUARD_PATTERN, + PATCHED_GUARD, + patch_prepare_context_parallel_inputs, +) + + +@pytest.fixture +def restore_trainer_prepare_method(): + """Ensure Trainer._prepare_context_parallel_inputs is restored after a test.""" + original_method = getattr( + Trainer, + "_original_prepare_context_parallel_inputs", + Trainer._prepare_context_parallel_inputs, + ) + patched_attr_present = hasattr( + Trainer, "_axolotl_prepare_context_parallel_inputs_patched" + ) + + yield + + Trainer._prepare_context_parallel_inputs = original_method + if patched_attr_present: + delattr(Trainer, "_axolotl_prepare_context_parallel_inputs_patched") + if hasattr(Trainer, "_original_prepare_context_parallel_inputs"): + delattr(Trainer, "_original_prepare_context_parallel_inputs") + if hasattr(Trainer, "_axolotl_prepare_context_parallel_inputs_source"): + delattr(Trainer, "_axolotl_prepare_context_parallel_inputs_source") + + +def test_patch_attention_guard(restore_trainer_prepare_method): + """Patch should swap the guard to allow sdpa or flash attention.""" + # Ensure we start from the unpatched method + if hasattr(Trainer, "_original_prepare_context_parallel_inputs"): + Trainer._prepare_context_parallel_inputs = ( + Trainer._original_prepare_context_parallel_inputs + ) + delattr(Trainer, "_original_prepare_context_parallel_inputs") + if hasattr(Trainer, "_axolotl_prepare_context_parallel_inputs_patched"): + delattr(Trainer, "_axolotl_prepare_context_parallel_inputs_patched") + + patch_prepare_context_parallel_inputs() + + patched_method = Trainer._prepare_context_parallel_inputs + assert patched_method is not None + assert getattr(Trainer, "_axolotl_prepare_context_parallel_inputs_patched", False) + + source = Trainer._axolotl_prepare_context_parallel_inputs_source + assert GUARD_PATTERN not in source + assert PATCHED_GUARD in source + + +def test_patch_is_idempotent(restore_trainer_prepare_method): + """Calling the patch twice should leave the same patched function in place.""" + patch_prepare_context_parallel_inputs() + first_patched = Trainer._prepare_context_parallel_inputs + + patch_prepare_context_parallel_inputs() + second_patched = Trainer._prepare_context_parallel_inputs + + assert first_patched is second_patched From 7fa8ac40cd344e3578dcdddb8e426b5445487997 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 26 Sep 2025 12:11:29 +0700 Subject: [PATCH 0982/1405] Feat(cce): add qwen3_vl, qwen3_vl_moe, granitemoeshared, granitemoehybrid, and upgraded all cce patches (#3178) * feat: upgrade cce with patches for transformers 4.56 * feat: add missing models to cce readme --- examples/colab-notebooks/colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/integrations/cut_cross_entropy/README.md | 9 ++++++++- src/axolotl/integrations/cut_cross_entropy/__init__.py | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index b483310639..9e18757f6e 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c5aa3ef\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@147ea28\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index dc117604af..32f5858582 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c5aa3ef"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@147ea28"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index cc73eebb78..c33d45f006 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c5aa3ef" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@147ea28" ``` ## Usage @@ -31,6 +31,7 @@ plugins: ## Supported Models +- apertus - arcee - cohere - cohere2 @@ -44,9 +45,13 @@ plugins: - glm - glm4 - glm4_moe +- glm4v +- glm4v_moe - gpt_oss - granite - granitemoe +- granitemoeshared +- granitemoehybrid - hunyuan_v1_dense - hunyuan_v1_moe - llama @@ -65,6 +70,8 @@ plugins: - qwen2_5_vl - qwen3 - qwen3_moe +- qwen3_vl +- qwen3_vl_moe - qwen3_next - smollm3 - seed_oss diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 812baf33ff..e8c6c23a35 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@c5aa3ef"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@147ea28"`' ) From 850c1a5f8db4abd4cce2aab5d041cdf0dc90e03a Mon Sep 17 00:00:00 2001 From: "Grant Holmes (Ren)" <60802511+gholmes829@users.noreply.github.com> Date: Fri, 26 Sep 2025 04:23:59 -0500 Subject: [PATCH 0983/1405] Add FSDP v2 swap memory support + QLoRA compatibility fixes (#3167) Co-authored-by: salman --- docs/fsdp_qlora.qmd | 8 ++++- examples/llama-2/qlora-fsdp.yml | 1 + src/axolotl/monkeypatch/accelerate/fsdp2.py | 7 +++- src/axolotl/utils/schemas/validation.py | 27 +++++++------- src/axolotl/utils/trainer.py | 4 +++ tests/utils/schemas/validation/test_fsdp.py | 40 ++++++++++++++++++++- 6 files changed, 71 insertions(+), 16 deletions(-) diff --git a/docs/fsdp_qlora.qmd b/docs/fsdp_qlora.qmd index 2f1b0358ff..01f57e627b 100644 --- a/docs/fsdp_qlora.qmd +++ b/docs/fsdp_qlora.qmd @@ -1,5 +1,5 @@ --- -title: "FDSP + QLoRA" +title: "FSDP + QLoRA" description: Use FSDP with QLoRA to fine-tune large LLMs on consumer GPUs. format: html: @@ -23,6 +23,12 @@ To enable `QLoRA` with `FSDP`, you need to perform the following steps: 2. Enable FSDP in your axolotl config, as [described here](multi-gpu.qmd#sec-fsdp). 3. Use one of the supported model types: `llama`, `mistral` or `mixtral`. +## Enabling Swap for FSDP2 + +If available memory is insufficient even after FSDP's CPU offloading, you can enable swap memory usage by setting `cpu_offload_pin_memory: false` alongside `offload_params: true` in FSDP config. + +This disables memory pinning, allowing FSDP to use disk swap space as fallback. Disabling memory pinning itself incurs performance overhead, and actually having to use swap adds more, but it may enable training larger models that would otherwise cause OOM errors on resource constrained systems. + ## Example Config [examples/llama-2/qlora-fsdp.yml](../examples/llama-2/qlora-fsdp.yml) contains an example of how to enable QLoRA + FSDP in axolotl. diff --git a/examples/llama-2/qlora-fsdp.yml b/examples/llama-2/qlora-fsdp.yml index 54f4b86b45..1e7064de82 100644 --- a/examples/llama-2/qlora-fsdp.yml +++ b/examples/llama-2/qlora-fsdp.yml @@ -66,6 +66,7 @@ fsdp_config: fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer fsdp_state_dict_type: FULL_STATE_DICT + # fsdp_cpu_offload_pin_memory: false # uncomment to enable swap memory usage when RAM is insufficient special_tokens: # save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index d8ba02cb2c..af6f24a635 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -4,6 +4,7 @@ import copy import functools +import os import sys import torch @@ -277,6 +278,11 @@ def fsdp2_prepare_model(accelerator, model: torch.nn.Module) -> torch.nn.Module: mesh = getattr(accelerator.state, "device_mesh", None) + # Disable memory pinning if requested + offload_to_cpu = isinstance(fsdp2_plugin.cpu_offload, CPUOffloadPolicy) + if offload_to_cpu and os.environ.get("FSDP_CPU_OFFLOAD_PIN_MEMORY", "") == "false": + fsdp2_plugin.cpu_offload.pin_memory = False + fsdp2_kwargs = { "reshard_after_forward": fsdp2_plugin.reshard_after_forward, "offload_policy": fsdp2_plugin.cpu_offload, @@ -341,7 +347,6 @@ def fsdp2_prepare_model(accelerator, model: torch.nn.Module) -> torch.nn.Module: ) if fsdp2_plugin.cpu_ram_efficient_loading: - offload_to_cpu = isinstance(fsdp2_plugin.cpu_offload, CPUOffloadPolicy) fsdp2_load_full_state_dict( accelerator, model, original_sd, offload_to_cpu=offload_to_cpu ) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 9671b10ae2..0ec3e854f5 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -816,21 +816,22 @@ def check_fsdp_version(cls, data): ) return data - @model_validator(mode="after") - def check_fsdp2_base_model_quant_ram_efficient_loading(self): - fsdp_config = self.fsdp_config if hasattr(self, "fsdp_config") else None - fsdp_version = self.fsdp_version if hasattr(self, "fsdp_version") else None - load_in_8bit = self.load_in_8bit if hasattr(self, "load_in_8bit") else None - load_in_4bit = self.load_in_4bit if hasattr(self, "load_in_4bit") else None - if fsdp_config and fsdp_version == 2: - if fsdp_config.get("cpu_ram_efficient_loading") and ( - load_in_8bit or load_in_4bit - ): + @model_validator(mode="before") + @classmethod + def check_fsdp2_cpu_offload_pin_memory(cls, data): + if not (fsdp_config := data.get("fsdp_config")): + return data + + if fsdp_config.get("cpu_offload_pin_memory") is False: + if str(data.get("fsdp_version")) != "2": raise ValueError( - "FSDP2 does not support load_in_8bit or load_in_4bit with cpu_ram_efficient_loading. Please do one of the following: use DeepSpeed, " - "set fsdp_version to 1, or disable cpu_ram_efficient_loading." + "FSDP1 does not support disabling cpu_offload_pin_memory, please set `fsdp_version` to 2" ) - return self + if not fsdp_config.get("offload_params"): + raise ValueError( + "disabling cpu_offload_pin_memory requires enabling offload_params" + ) + return data @model_validator(mode="before") @classmethod diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 662a54655f..56fbe34c09 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -595,6 +595,10 @@ def setup_fsdp_envs(cfg): os.environ["FSDP_USE_ORIG_PARAMS"] = "true" if cfg.fsdp_config.state_dict_type: os.environ["FSDP_STATE_DICT_TYPE"] = cfg.fsdp_config.state_dict_type + if cfg.fsdp_config.cpu_offload_pin_memory is not None: + os.environ["FSDP_CPU_OFFLOAD_PIN_MEMORY"] = str( + cfg.fsdp_config.cpu_offload_pin_memory + ).lower() if cfg.fsdp_config.auto_wrap_policy: os.environ["FSDP_AUTO_WRAP_POLICY"] = cfg.fsdp_config.auto_wrap_policy if cfg.fsdp_config.transformer_layer_cls_to_wrap: diff --git a/tests/utils/schemas/validation/test_fsdp.py b/tests/utils/schemas/validation/test_fsdp.py index 08fc50c610..65f9c66a38 100644 --- a/tests/utils/schemas/validation/test_fsdp.py +++ b/tests/utils/schemas/validation/test_fsdp.py @@ -61,12 +61,50 @@ def test_fsdp2_w_cpu_ram_efficient_loading(self, min_base_cfg): }, fsdp_version=2, ) + validated_cfg = validate_config(cfg) + assert validated_cfg.fsdp_version == 2 + assert validated_cfg.fsdp_config.cpu_ram_efficient_loading is True + + def test_fsdp2_cpu_offload_pin_memory_requires_offload_params(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + fsdp_config={ + "cpu_offload_pin_memory": False, + "offload_params": False, + }, + fsdp_version=2, + ) + with pytest.raises( + ValueError, + match="disabling cpu_offload_pin_memory requires enabling offload_params", + ): + validate_config(cfg) + + def test_fsdp1_cpu_offload_pin_memory_not_supported(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + fsdp_config={ + "cpu_offload_pin_memory": False, + "offload_params": True, + }, + fsdp_version=1, + ) with pytest.raises( ValueError, - match="FSDP2 does not support load_in_8bit or load_in_4bit with cpu_ram_efficient_loading.", + match="FSDP1 does not support disabling cpu_offload_pin_memory, please set `fsdp_version` to 2", ): validate_config(cfg) + def test_fsdp2_cpu_offload_pin_memory_w_offload_params(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + fsdp_config={ + "cpu_offload_pin_memory": False, + "offload_params": True, + }, + fsdp_version=2, + ) + validated_cfg = validate_config(cfg) + assert validated_cfg.fsdp_config.cpu_offload_pin_memory is False + assert validated_cfg.fsdp_config.offload_params is True + def test_fsdp_prefixes_removed(self, min_base_cfg): cfg = min_base_cfg | DictDefault( fsdp_config={ From 740d5a1d31e100833974f8ad2a7891b6c4dc1f9c Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 26 Sep 2025 09:55:15 -0400 Subject: [PATCH 0984/1405] doc fix (#3187) --- docs/lora_optims.qmd | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/lora_optims.qmd b/docs/lora_optims.qmd index 7cdf539755..40893387b1 100644 --- a/docs/lora_optims.qmd +++ b/docs/lora_optims.qmd @@ -5,10 +5,11 @@ description: "Custom autograd functions and Triton kernels in Axolotl for optimi Inspired by [Unsloth](https://github.com/unslothai/unsloth), we've implemented two optimizations for LoRA and QLoRA fine-tuning, supporting both single GPU and multi-GPU -(in the DDP and DeepSpeed settings) training. These include (1) SwiGLU and GEGLU activation function -Triton kernels, and (2) LoRA MLP and attention custom autograd functions. Our goal was -to leverage operator fusion and tensor re-use in order to improve speed and reduce -memory usage during the forward and backward passes of these calculations. +(including the DDP, DeepSpeed, and FSDP2 settings) training. These include (1) SwiGLU +and GEGLU activation function Triton kernels, and (2) LoRA MLP and attention custom +autograd functions. Our goal was to leverage operator fusion and tensor re-use in order +to improve speed and reduce memory usage during the forward and backward passes of +these calculations. We currently support several common model architectures, including (but not limited to): @@ -131,6 +132,5 @@ computation path. ## Future Work - Support for additional model architectures -- Support for the FSDP setting - Support for dropout and bias - Additional operator fusions From f4376748f38abaf1eed8f6d592453dd94ab9c0d7 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Fri, 26 Sep 2025 15:07:39 -0400 Subject: [PATCH 0985/1405] debug log: multiprocess race condition fix (#3188) --- src/axolotl/utils/tee.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/axolotl/utils/tee.py b/src/axolotl/utils/tee.py index 1209ad1dd5..7bc8efab0b 100644 --- a/src/axolotl/utils/tee.py +++ b/src/axolotl/utils/tee.py @@ -109,8 +109,8 @@ def prepare_debug_log(cfg, filename: str = "debug.log") -> str: cfg.get("resume_from_checkpoint") or cfg.get("auto_resume_from_checkpoints") ) - if not append and log_path.exists(): - log_path.unlink() + if not append: + log_path.unlink(missing_ok=True) fh = open(log_path, "a", encoding="utf-8") fh.flush() From a6bfbe34009686c59bbf5a198f8ad047019a78d6 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:32:51 +0530 Subject: [PATCH 0986/1405] torch_dtype -> dtype (#3177) * torch_dtype -> dtype * torch_dtype -> dtype --- src/axolotl/cli/delinearize_llama4.py | 4 +--- src/axolotl/cli/quantize.py | 2 +- src/axolotl/utils/model_shard_quant.py | 4 ++-- tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py | 2 +- tests/e2e/test_quantization.py | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/axolotl/cli/delinearize_llama4.py b/src/axolotl/cli/delinearize_llama4.py index 90227fccde..4f5448a14b 100644 --- a/src/axolotl/cli/delinearize_llama4.py +++ b/src/axolotl/cli/delinearize_llama4.py @@ -85,9 +85,7 @@ def do_cli(model: Union[Path, str], output: Union[Path, str]) -> None: unpatch_llama4 = patch_llama4_linearized_modeling() from transformers import Llama4ForConditionalGeneration - model_ = Llama4ForConditionalGeneration.from_pretrained( - model, torch_dtype=torch.bfloat16 - ) + model_ = Llama4ForConditionalGeneration.from_pretrained(model, dtype=torch.bfloat16) processor = AutoProcessor.from_pretrained(model) processor.save_pretrained(output) diff --git a/src/axolotl/cli/quantize.py b/src/axolotl/cli/quantize.py index 6838f47d8e..c11bcc6d93 100644 --- a/src/axolotl/cli/quantize.py +++ b/src/axolotl/cli/quantize.py @@ -69,7 +69,7 @@ def do_quantize( config = AutoConfig.from_pretrained(model_path) torch_dtype = config.torch_dtype if hasattr(config, "torch_dtype") else None model = AutoModelForCausalLM.from_pretrained( - model_path, device_map="auto", torch_dtype=torch_dtype + model_path, device_map="auto", dtype=torch_dtype ) LOG.info( diff --git a/src/axolotl/utils/model_shard_quant.py b/src/axolotl/utils/model_shard_quant.py index f20a9625ed..ca152113a3 100644 --- a/src/axolotl/utils/model_shard_quant.py +++ b/src/axolotl/utils/model_shard_quant.py @@ -148,7 +148,7 @@ def load_sharded_model( model = AutoModelForCausalLM.from_pretrained( model_name, use_cache=False, - torch_dtype=torch.float32, + dtype=torch.float32, _attn_implementation=model_config._attn_implementation, trust_remote_code=cfg.trust_remote_code, ) @@ -158,7 +158,7 @@ def load_sharded_model( with init_empty_weights(): model = AutoModelForCausalLM.from_config( model_config, - torch_dtype=torch_dtype, + dtype=torch_dtype, trust_remote_code=cfg.trust_remote_code, ) return model diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index 2180eb99d5..73f8838588 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -160,7 +160,7 @@ def test_geglu_model_integration(): """Test GeGLU activation with Gemma model.""" model = AutoModelForCausalLM.from_pretrained( "trl-internal-testing/tiny-Gemma2ForCausalLM", - torch_dtype=torch.float16, + dtype=torch.float16, device_map="cuda:0", ) peft_config = get_peft_config( diff --git a/tests/e2e/test_quantization.py b/tests/e2e/test_quantization.py index b64aef51aa..706279c6ca 100644 --- a/tests/e2e/test_quantization.py +++ b/tests/e2e/test_quantization.py @@ -39,7 +39,7 @@ def model(): dummy_model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen2-0.5B", device_map="auto", - torch_dtype=torch.bfloat16, + dtype=torch.bfloat16, ) with torch.device(dummy_model.device): dummy_model.model.embed_tokens = torch.nn.Embedding( From ce74c20109d60df4cb023254f3a58b80b6a4cfc8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 1 Oct 2025 11:11:39 -0400 Subject: [PATCH 0987/1405] don't cache pip install (#3194) * don't cache pip install * no cache dir for disk space for sdist too --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cfd2c715d7..5d5bdb5ac9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -81,12 +81,12 @@ jobs: - name: Install PyTorch run: | - pip3 install torch==${{ matrix.pytorch_version }} torchvision + pip3 install --no-cache-dir torch==${{ matrix.pytorch_version }} torchvision - name: Install dependencies run: | pip3 show torch - pip3 install --no-build-isolation -U -e . + pip3 install --no-cache-dir --no-build-isolation -U -e . python scripts/unsloth_install.py | sh python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt @@ -156,13 +156,13 @@ jobs: - name: Install PyTorch run: | - pip3 install torch==${{ matrix.pytorch_version }} torchvision + pip3 install --no-cache-dir torch==${{ matrix.pytorch_version }} torchvision - name: Install dependencies run: | pip3 show torch python -m build --no-isolation --sdist - pip3 install --no-build-isolation dist/axolotl*.tar.gz + pip3 install --no-cache-dir --no-build-isolation dist/axolotl*.tar.gz python scripts/unsloth_install.py | sh python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt From 409cfb8a87287bb2314e5ce9d9ba2585bff2da9f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 7 Oct 2025 11:23:41 -0400 Subject: [PATCH 0988/1405] deprecate torch 2.6.0 support (#3197) [skip ci] --- .github/workflows/base.yml | 21 --------------------- .github/workflows/main.yml | 15 --------------- .github/workflows/multi-gpu-e2e.yml | 7 ------- .github/workflows/nightlies.yml | 16 ++++++++-------- .github/workflows/tests-nightly.yml | 10 +++++----- .github/workflows/tests.yml | 10 ++-------- README.md | 2 +- 7 files changed, 16 insertions(+), 65 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 160ed7df90..7af6059c88 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -25,20 +25,6 @@ jobs: fail-fast: false matrix: include: - - cuda: "124" - cuda_version: 12.4.1 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.6.0 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-base" - - cuda: "126" - cuda_version: 12.6.3 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.6.0 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-base" - cuda: "126" cuda_version: 12.6.3 cudnn_version: "" @@ -122,13 +108,6 @@ jobs: fail-fast: false matrix: include: - - cuda: "126" - cuda_version: 12.6.3 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.6.0 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-uv-base" - cuda: "126" cuda_version: 12.6.3 cudnn_version: "" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3f98dd2b42..4040ccdc92 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,11 +15,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.6.0 - axolotl_extras: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" @@ -88,11 +83,6 @@ jobs: strategy: matrix: include: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.6.0 - axolotl_extras: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" @@ -162,11 +152,6 @@ jobs: strategy: matrix: include: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.6.0 - axolotl_extras: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 05f9e0761e..6a92de352c 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -26,13 +26,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.6.0 - axolotl_extras: - num_gpus: 2 - nightly_build: "true" - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 49bce470b8..18b036a0d8 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -15,12 +15,12 @@ jobs: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" - pytorch: 2.6.0 + pytorch: 2.7.1 axolotl_extras: - - cuda: 126 - cuda_version: 12.6.3 + - cuda: 128 + cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.1 + pytorch: 2.8.0 axolotl_extras: runs-on: axolotl-gpu-runner steps: @@ -68,12 +68,12 @@ jobs: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" - pytorch: 2.6.0 + pytorch: 2.7.1 axolotl_extras: - - cuda: 126 - cuda_version: 12.6.3 + - cuda: 128 + cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.1 + pytorch: 2.8.0 axolotl_extras: runs-on: axolotl-gpu-runner steps: diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index fc6c2b396e..35cb707ebb 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -26,7 +26,7 @@ jobs: max-parallel: 2 matrix: python_version: ["3.11"] - pytorch_version: ["2.6.0", "2.7.0"] + pytorch_version: ["2.7.1", "2.8.0"] timeout-minutes: 20 steps: @@ -102,14 +102,14 @@ jobs: - cuda: 126 cuda_version: 12.6.3 python_version: "3.11" - pytorch: 2.6.0 + pytorch: 2.7.1 num_gpus: 1 axolotl_extras: nightly_build: "true" - - cuda: 126 - cuda_version: 12.6.3 + - cuda: 128 + cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.1 + pytorch: 2.8.0 num_gpus: 1 axolotl_extras: nightly_build: "true" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5d5bdb5ac9..8f368b5170 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.6.0", "2.7.1", "2.8.0"] + pytorch_version: ["2.7.1", "2.8.0"] timeout-minutes: 20 steps: @@ -130,7 +130,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.6.0", "2.7.1", "2.8.0"] + pytorch_version: ["2.7.1", "2.8.0"] timeout-minutes: 20 steps: @@ -286,12 +286,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.6.0 - num_gpus: 1 - axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" diff --git a/README.md b/README.md index 1a033acd98..6313a73ca0 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Features: - NVIDIA GPU (Ampere or newer for `bf16` and Flash Attention) or AMD GPU - Python 3.11 -- PyTorch ≥2.6.0 +- PyTorch ≥2.7.1 ### Google Colab From 377c510e955c4db01b9e26858ed0945d92dc6e8d Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:09:21 +0530 Subject: [PATCH 0989/1405] sleep model support (#3135) Co-authored-by: salman --- src/axolotl/core/trainers/grpo/__init__.py | 1 + src/axolotl/utils/schemas/trl.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 7eda7a0bac..d1a6b7fd92 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -52,6 +52,7 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: if trl.vllm_mode: grpo_args_kwargs["vllm_mode"] = trl.vllm_mode if trl.vllm_mode == "colocate": + grpo_args_kwargs["enable_sleep_mode"] = trl.vllm_enable_sleep_mode # type: ignore[attr-defined] grpo_args_kwargs["vllm_gpu_memory_utilization"] = ( vllm_cfg.gpu_memory_utilization ) diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index 980474e87e..624f7663ec 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -167,3 +167,9 @@ class TRLConfig(BaseModel): "description": "Whether to exclude truncated completions from loss calculation." }, ) + vllm_enable_sleep_mode: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Enable sleep mode for vLLM to offload VRAM when idle" + }, + ) From 130637a3fa4cb476b2452aefd1710be7501ad221 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 8 Oct 2025 08:43:46 -0400 Subject: [PATCH 0990/1405] upgrade transformers to 4.57.0 (#3201) * upgrade transformers to 4.57.0 * remove deprecated autoawq and use latest peft * remove autoawq from setuptools script * fix imports * make sure torchvision is installed * remove support for BetterTransformer * skip fsdp_qlora_prequant test * more robust error reporting --- cicd/Dockerfile-uv.jinja | 1 + cicd/single_gpu.py | 9 ++++- docker/Dockerfile-uv-base | 2 +- requirements.txt | 5 +-- setup.py | 3 -- src/axolotl/core/builders/causal.py | 7 ---- src/axolotl/processing_strategies.py | 4 +- src/axolotl/train.py | 10 ----- src/axolotl/utils/callbacks/__init__.py | 37 ------------------- ...setuptools_axolotl_dynamic_dependencies.py | 2 - tests/e2e/multigpu/test_llama.py | 1 + 11 files changed, 15 insertions(+), 66 deletions(-) diff --git a/cicd/Dockerfile-uv.jinja b/cicd/Dockerfile-uv.jinja index 8603861879..6a4d8a7d33 100644 --- a/cicd/Dockerfile-uv.jinja +++ b/cicd/Dockerfile-uv.jinja @@ -32,6 +32,7 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ fi RUN uv pip install packaging==23.2 setuptools==75.8.0 +RUN uv pip install torchvision RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ uv pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ diff --git a/cicd/single_gpu.py b/cicd/single_gpu.py index 5a06a34f00..3bca5806f0 100644 --- a/cicd/single_gpu.py +++ b/cicd/single_gpu.py @@ -68,5 +68,10 @@ def run_cmd(cmd: str, run_folder: str): sp_env["AXOLOTL_DATASET_PROCESSES"] = "8" # Propagate errors from subprocess. - if exit_code := subprocess.call(cmd.split(), cwd=run_folder, env=sp_env): # nosec - exit(exit_code) + try: + exit_code = subprocess.call(cmd.split(), cwd=run_folder, env=sp_env) # nosec + if exit_code: + print(f"Command '{cmd}' failed with exit code {exit_code}") + return exit_code + except Exception as e: # pylint: disable=broad-except + print(f"Command '{cmd}' failed with exception {e}") diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index 4b08e55f8e..eaa49b9e95 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -30,7 +30,7 @@ RUN uv venv --no-project --relocatable axolotl-venv ENV PATH="/workspace/axolotl-venv/bin:${PATH}" RUN uv pip install packaging setuptools wheel psutil \ - && uv pip install torch==${PYTORCH_VERSION} \ + && uv pip install torch==${PYTORCH_VERSION} torchvision \ && uv pip install --no-build-isolation "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" \ && uv pip install "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" \ && uv pip install awscli pydantic diff --git a/requirements.txt b/requirements.txt index 86013374f9..9c56638a34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,16 +5,15 @@ bitsandbytes==0.47.0 triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 -autoawq==0.2.7.post3 liger-kernel==0.6.1 # END section packaging==23.2 huggingface_hub>=0.33.0 -peft>=0.17.0 -transformers==4.56.1 +peft>=0.17.1 tokenizers>=0.21.1 +transformers==4.57.0 accelerate==1.10.1 datasets==4.0.0 deepspeed>=0.17.0 diff --git a/setup.py b/setup.py index 3e642b57ff..b2eeb92d65 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,6 @@ def parse_requirements(extras_require_map): _install_requires.append(line) try: xformers_version = [req for req in _install_requires if "xformers" in req][0] - autoawq_version = [req for req in _install_requires if "autoawq" in req][0] if "Darwin" in platform.system(): # skip packages not compatible with OSX skip_packages = [ @@ -34,7 +33,6 @@ def parse_requirements(extras_require_map): "triton", "mamba-ssm", "xformers", - "autoawq", "liger-kernel", ] _install_requires = [ @@ -87,7 +85,6 @@ def parse_requirements(extras_require_map): _install_requires.append("xformers==0.0.28.post2") else: _install_requires.append("xformers>=0.0.28.post3") - _install_requires.pop(_install_requires.index(autoawq_version)) extras_require_map.pop("vllm") elif (major, minor) >= (2, 4): extras_require_map.pop("vllm") diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index f7f350e1a0..8203042305 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -28,7 +28,6 @@ from axolotl.utils import is_comet_available, is_mlflow_available from axolotl.utils.callbacks import ( LossWatchDogCallback, - SaveBetterTransformerModelCallback, bench_eval_callback_factory, causal_lm_bench_eval_callback_factory, colab_inference_post_train_callback, @@ -63,12 +62,6 @@ def get_callbacks(self): if self.cfg.relora: callbacks.append(ReLoRACallback(self.cfg)) - if ( - hasattr(self.model, "use_bettertransformer") - and self.model.use_bettertransformer is True - ): - callbacks.append(SaveBetterTransformerModelCallback()) - # TODO: check if can move to base class if self.cfg.loss_watchdog_threshold is not None: callbacks.append(LossWatchDogCallback(self.cfg)) diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 5e7c1456a7..07b114163b 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -6,8 +6,10 @@ from PIL import Image, ImageOps from PIL.Image import Resampling from torch import Tensor, zeros_like -from transformers import ProcessorMixin, SmolVLMProcessor, VoxtralProcessor +from transformers import ProcessorMixin from transformers.image_utils import load_image +from transformers.models.smolvlm import SmolVLMProcessor +from transformers.models.voxtral import VoxtralProcessor from axolotl.utils.dict import remove_none_values from axolotl.utils.logging import get_logger diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 2a70d9712b..da7b631215 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -40,11 +40,6 @@ from axolotl.utils.train import determine_last_checkpoint from axolotl.utils.trainer import setup_trainer -try: - from optimum.bettertransformer import BetterTransformer -except ImportError: - BetterTransformer = None - if typing.TYPE_CHECKING: from axolotl.core.builders import HFCausalTrainerBuilder, HFRLTrainerBuilder @@ -141,8 +136,6 @@ def setup_signal_handler( def terminate_handler(_, __, model_weakref): if model_weakref() is not None: _model = model_weakref() - if cfg.flash_optimum and BetterTransformer: - _model = BetterTransformer.reverse(_model) _model.save_pretrained( cfg.output_dir, safe_serialization=safe_serialization ) @@ -321,9 +314,6 @@ def save_trained_model( except FileNotFoundError: pass elif cfg.local_rank == 0: - if cfg.flash_optimum and BetterTransformer: - model = BetterTransformer.reverse(model) - if cfg.rl and cfg.adapter and not cfg.rl_adapter_ref_model: trainer.model.save_pretrained( cfg.output_dir, safe_serialization=safe_serialization diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 6c5512223d..b54cf10c92 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -17,7 +17,6 @@ import torch.distributed as dist import wandb from datasets import load_dataset -from optimum.bettertransformer import BetterTransformer from tqdm import tqdm from transformers import ( GenerationConfig, @@ -28,8 +27,6 @@ TrainingArguments, ) from transformers.trainer_utils import ( - PREFIX_CHECKPOINT_DIR, - IntervalStrategy, SaveStrategy, ) from trl.models import unwrap_model_for_generation @@ -56,40 +53,6 @@ LOG = get_logger(__name__) -class SaveBetterTransformerModelCallback(TrainerCallback): - """Callback to save the BetterTransformer wrapped model""" - - def on_step_end( - self, - args: TrainingArguments, - state: TrainerState, - control: TrainerControl, - **kwargs, - ) -> TrainerControl: - # Save - if ( - args.save_strategy == IntervalStrategy.STEPS - and args.save_steps > 0 - and state.global_step % args.save_steps == 0 - ): - control.should_save = True - - if control.should_save: - checkpoint_folder = os.path.join( - args.output_dir, - f"{PREFIX_CHECKPOINT_DIR}-{state.global_step}", - ) - - model = BetterTransformer.reverse(kwargs["model"]) - model.save_pretrained(checkpoint_folder) - # FIXME - need to cleanup old checkpoints - - # since we're saving here, we don't need the trainer loop to attempt to save too b/c - # the trainer will raise an exception since it can't save a BetterTransformer wrapped model - control.should_save = False - return control - - class LossWatchDogCallback(TrainerCallback): """Callback to track loss and stop training if loss is too high""" diff --git a/src/setuptools_axolotl_dynamic_dependencies.py b/src/setuptools_axolotl_dynamic_dependencies.py index ccd7c72d7d..3bb54cda8e 100644 --- a/src/setuptools_axolotl_dynamic_dependencies.py +++ b/src/setuptools_axolotl_dynamic_dependencies.py @@ -33,7 +33,6 @@ def parse_requirements(): try: xformers_version = [req for req in _install_requires if "xformers" in req][0] torchao_version = [req for req in _install_requires if "torchao" in req][0] - autoawq_version = [req for req in _install_requires if "autoawq" in req][0] if "Darwin" in platform.system(): # don't install xformers on MacOS @@ -63,7 +62,6 @@ def parse_requirements(): _install_requires.append("xformers==0.0.28.post2") else: _install_requires.append("xformers==0.0.28.post3") - _install_requires.pop(_install_requires.index(autoawq_version)) elif (major, minor) >= (2, 4): if patch == 0: _install_requires.pop(_install_requires.index(xformers_version)) diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index c16ef0c603..b836291e54 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -548,6 +548,7 @@ def test_fsdp2_packed( temp_dir + "/runs", "train/train_loss", 2.1, "Train Loss (%s) is too high" ) + @pytest.mark.skip("regression failure from v4.57.0") def test_fsdp_qlora_prequant_packed(self, temp_dir): cfg = DictDefault( { From 4c3488cc9f5377d66ed96fc1e12e350c6a8cb21b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:58:02 -0400 Subject: [PATCH 0991/1405] chore: update pre-commit hooks (#3160) [skip ci] Co-authored-by: djsaunde <1245942+djsaunde@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 92ddc7f41e..e853243cd5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,13 +11,13 @@ repos: - id: no-commit-to-branch args: ['--branch', 'main'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.12 + rev: v0.13.3 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.1 + rev: v1.18.2 hooks: - id: mypy additional_dependencies: From d0e9c3c1c58a093f7ee8e18ddfbe0702f3cf7333 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 8 Oct 2025 10:43:41 -0400 Subject: [PATCH 0992/1405] When using Ray use prepare for dataloader fixes (#3198) * make sure to use ray prepare for dataloader fixes * ray tests use 2.7.0+ * don't call init_distributed w ray and deepspeed * handle dict deepspeed config * better handling of dict deepspeed config * use json.dumps * guard to_dict * wrap import for optional ray --- src/axolotl/cli/train.py | 2 +- src/axolotl/train.py | 11 +++++++++++ src/axolotl/utils/trainer.py | 16 +++++++++++++++- tests/e2e/multigpu/test_ray.py | 5 ++--- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 2332717e77..6b3bfbd575 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -99,7 +99,7 @@ def ray_train_func(kwargs: dict): resolve_dtype(cfg) # ray serializing objects gets rid of frozen attribute - HF expects dict not DefaultDict - if cfg.deepspeed: + if cfg.deepspeed and hasattr(cfg.deepspeed, "to_dict"): cfg.deepspeed = cfg.deepspeed.to_dict() # initialize accelerator before model instantiation diff --git a/src/axolotl/train.py b/src/axolotl/train.py index da7b631215..441c508711 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -525,6 +525,17 @@ def setup_model_and_trainer( plugin_manager = PluginManager.get_instance() plugin_manager.post_trainer_create(cfg, trainer) + if cfg.use_ray: + try: + import ray.train.huggingface.transformers + + trainer = ray.train.huggingface.transformers.prepare_trainer(trainer) + except ImportError: + LOG.warning( + "The Ray integration with Hugging Face Transformers is not available. " + "To use Ray, install the 'ray[train]' package." + ) + return ( trainer, model, diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 56fbe34c09..c7fa0a6479 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -6,6 +6,7 @@ import random from contextlib import contextmanager from functools import partial +from tempfile import NamedTemporaryFile from typing import List, Optional import numpy as np @@ -15,6 +16,7 @@ from torch.utils.data import DataLoader, RandomSampler, SequentialSampler from transformers.utils import is_torch_bf16_gpu_available +from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import init_distributed_state, reduce_and_broadcast from axolotl.utils.environment import check_cuda_p2p_ib_support from axolotl.utils.logging import get_logger @@ -540,6 +542,13 @@ def setup_deepspeed_env(cfg, stage=None): ) os.environ["ACCELERATE_USE_DEEPSPEED"] = "true" + if isinstance(cfg.deepspeed, DictDefault): + with NamedTemporaryFile( + mode="w", delete=False, suffix=".json", prefix="deepspeed_config_" + ) as temp_file: + temp_file.write(json.dumps(cfg.deepspeed.to_dict(), indent=4)) + temp_file.close() + cfg.deepspeed = str(temp_file.name) os.environ["ACCELERATE_DEEPSPEED_CONFIG_FILE"] = cfg.deepspeed os.environ["ACCELERATE_GRADIENT_ACCUMULATION_STEPS"] = str( cfg.gradient_accumulation_steps @@ -562,6 +571,7 @@ def setup_deepspeed_env(cfg, stage=None): if ( int(os.environ.get("WORLD_SIZE", "1")) == 1 and os.environ.get("AXOLOTL_IS_PREPROCESS", "0") != "1" + and cfg.use_ray is not True ): os.environ["WORLD_SIZE"] = "1" # force it in case not set os.environ["LOCAL_RANK"] = "0" # force it in case not set @@ -638,11 +648,15 @@ def prepare_optim_env(cfg): setup_fsdp_envs(cfg) elif cfg.deepspeed: stage = None + deepspeed_config = None # check if the cfg.deepspeed is a file - if os.path.isfile(cfg.deepspeed): + if isinstance(cfg.deepspeed, DictDefault): + deepspeed_config = cfg.deepspeed + elif os.path.isfile(cfg.deepspeed): # parse with json with open(cfg.deepspeed, "r", encoding="utf-8") as fin: deepspeed_config = json.load(fin) + if deepspeed_config: stage = deepspeed_config.get("zero_optimization", {}).get("stage", None) setup_deepspeed_env(cfg, stage=stage) diff --git a/tests/e2e/multigpu/test_ray.py b/tests/e2e/multigpu/test_ray.py index 7c6ea8a1fa..df41b1444a 100644 --- a/tests/e2e/multigpu/test_ray.py +++ b/tests/e2e/multigpu/test_ray.py @@ -13,7 +13,6 @@ from tests.e2e.utils import ( check_tensorboard, require_torch_2_7_0, - require_torch_lt_2_6_0, ) AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent @@ -24,7 +23,7 @@ class TestMultiGPURay: Test cases for AnyScale Ray post training """ - @require_torch_lt_2_6_0 + @require_torch_2_7_0 def test_lora_ddp(self, temp_dir): cfg = DictDefault( { @@ -83,7 +82,7 @@ def test_lora_ddp(self, temp_dir): temp_dir + "/runs", "train/train_loss", 2.3, "Train Loss (%s) is too high" ) - @require_torch_lt_2_6_0 + @require_torch_2_7_0 @pytest.mark.parametrize( "gradient_accumulation_steps", [1, 2], From 6f8ce024d1ede51d03ece47c40f0e7b73e04488b Mon Sep 17 00:00:00 2001 From: Manh Nguyen <144032789+nguyen599@users.noreply.github.com> Date: Wed, 8 Oct 2025 22:27:01 +0700 Subject: [PATCH 0993/1405] Remove check_torch_compile_deepspeed (#3195) [skip ci] Signed-off-by: nguyen599 --- src/axolotl/utils/schemas/validation.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 0ec3e854f5..4abe45e641 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -783,15 +783,6 @@ def check_batch_flattening_fa(cls, data): return data - @model_validator(mode="before") - @classmethod - def check_torch_compile_deepspeed(cls, data): - if data.get("deepspeed") and data.get("torch_compile"): - raise ValueError( - "torch_compile should be set within your deepspeed config file" - ) - return data - @model_validator(mode="before") @classmethod def check_xentropy_patch_conflicts(cls, data): From ab63b92c384d07d83260e0ed3109afccefaf0d84 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 9 Oct 2025 21:47:41 +0700 Subject: [PATCH 0994/1405] feat: add lfm2 family and latest moe model (#3208) * feat: add lfm2 family and latest moe model * fix: use ml-cross-entropy for lfm2 examples --- examples/LiquidAI/README.md | 15 ++++- examples/LiquidAI/lfm2-350m-fft.yaml | 3 +- examples/LiquidAI/lfm2-8b-a1b-lora.yaml | 59 +++++++++++++++++++ examples/LiquidAI/lfm2-vl-lora.yaml | 3 + .../colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/common/architectures.py | 1 + .../integrations/cut_cross_entropy/README.md | 6 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/monkeypatch/multipack.py | 2 + 10 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 examples/LiquidAI/lfm2-8b-a1b-lora.yaml diff --git a/examples/LiquidAI/README.md b/examples/LiquidAI/README.md index 96fc74a92b..8a18d9eb12 100644 --- a/examples/LiquidAI/README.md +++ b/examples/LiquidAI/README.md @@ -6,6 +6,8 @@ LFM2 features a new hybrid Liquid architecture with multiplicative gates, short- This guide shows how to fine-tune both the LFM2 and LFM2-VL models with Axolotl. +Thanks to the team at LiquidAI for giving us early access to prepare for these releases. + ## Getting Started 1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). @@ -31,6 +33,14 @@ This guide shows how to fine-tune both the LFM2 and LFM2-VL models with Axolotl. axolotl train examples/LiquidAI/lfm2-vl-lora.yaml ``` + **LFM2-MoE** + ```bash + pip install git+https://github.com/huggingface/transformers.git@0c9a72e4576fe4c84077f066e585129c97bfd4e6 + + # LoRA SFT (1x48GB @ 16.2GiB) + axolotl train examples/LiquidAI/lfm2-8b-a1b-lora.yaml + ``` + ### TIPS - **Installation Error**: If you encounter `ImportError: ... undefined symbol ...` or `ModuleNotFoundError: No module named 'causal_conv1d_cuda'`, the `causal-conv1d` package may have been installed incorrectly. Try uninstalling it: @@ -45,14 +55,13 @@ This guide shows how to fine-tune both the LFM2 and LFM2-VL models with Axolotl. ## Optimization Guides -- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) -- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) -- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [Optimizations Guide](https://docs.axolotl.ai/docs/optimizations.html) ## Related Resources - [LFM2 Blog](https://www.liquid.ai/blog/liquid-foundation-models-v2-our-second-series-of-generative-ai-models) - [LFM2-VL Blog](https://www.liquid.ai/blog/lfm2-vl-efficient-vision-language-models) +- [LFM2-MoE Blog](https://www.liquid.ai/blog/lfm2-8b-a1b-an-efficient-on-device-mixture-of-experts) - [Axolotl Docs](https://docs.axolotl.ai) - [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) - [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/LiquidAI/lfm2-350m-fft.yaml b/examples/LiquidAI/lfm2-350m-fft.yaml index d198150080..145b56dd12 100644 --- a/examples/LiquidAI/lfm2-350m-fft.yaml +++ b/examples/LiquidAI/lfm2-350m-fft.yaml @@ -1,6 +1,7 @@ base_model: LiquidAI/LFM2-350M -chunked_cross_entropy: true +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin eot_tokens: - "<|im_end|>" diff --git a/examples/LiquidAI/lfm2-8b-a1b-lora.yaml b/examples/LiquidAI/lfm2-8b-a1b-lora.yaml new file mode 100644 index 0000000000..73cbfcce76 --- /dev/null +++ b/examples/LiquidAI/lfm2-8b-a1b-lora.yaml @@ -0,0 +1,59 @@ +base_model: LiquidAI/LFM2-8B-A1B + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: true + +eot_tokens: + - "<|im_end|>" +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_field_role: from + message_field_content: value +dataset_prepared_path: last_run_prepared +val_set_size: 0.05 +output_dir: ./outputs/out + +sequence_len: 4096 +sample_packing: true + +adapter: lora +lora_model_dir: + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 4 +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 5e-5 + +bf16: true +tf32: true + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 2 +saves_per_epoch: 1 + +weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/LiquidAI/lfm2-vl-lora.yaml b/examples/LiquidAI/lfm2-vl-lora.yaml index 7fee17f927..313da82744 100644 --- a/examples/LiquidAI/lfm2-vl-lora.yaml +++ b/examples/LiquidAI/lfm2-vl-lora.yaml @@ -3,6 +3,9 @@ trust_remote_code: true model_type: AutoModelForImageTextToText processor_type: AutoProcessor +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + # these 3 lines are needed for now to handle vision chat templates w images skip_prepare_dataset: true remove_unused_columns: false diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 9e18757f6e..ee99c283fd 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@147ea28\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@49f3308\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 32f5858582..cf8bd57e71 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@147ea28"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@49f3308"' ) diff --git a/src/axolotl/common/architectures.py b/src/axolotl/common/architectures.py index ce945e670f..b754e56ba8 100644 --- a/src/axolotl/common/architectures.py +++ b/src/axolotl/common/architectures.py @@ -14,4 +14,5 @@ "qwen3_moe": "Qwen3MoeSparseMoeBlock", "deepseek_v2": "DeepseekV2MoE", "gpt_oss": "GptOssDecoderLayer", + "lfm2_moe": "Lfm2MoeSparseMoeBlock", } diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index c33d45f006..08cd41200a 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@147ea28" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@49f3308" ``` ## Usage @@ -54,9 +54,13 @@ plugins: - granitemoehybrid - hunyuan_v1_dense - hunyuan_v1_moe +- lfm2 +- lfm2_moe +- lfm2_vl - llama - llama4 - llama4_text +- llava - mistral - mistral3 - mixtral diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index e8c6c23a35..ed6ebe62aa 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@147ea28"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@49f3308"`' ) diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 4741245e18..48b4ea10e6 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -45,6 +45,8 @@ "gpt_oss", "arcee", "seed_oss", + "lfm2", + "lfm2_moe", ] From 37f78c8592afee7699cdda7ac8d59f685949060c Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Thu, 9 Oct 2025 21:35:54 +0530 Subject: [PATCH 0995/1405] add chat_template_jinja to wandb (#3192) [skip ci] * add chat_template_jinja to wandb * temp_ct_file.flush() * Update src/axolotl/utils/callbacks/__init__.py Co-authored-by: Wing Lian * Update src/axolotl/utils/callbacks/__init__.py Co-authored-by: Wing Lian * Apply suggestion from @winglian --------- Co-authored-by: Wing Lian --- src/axolotl/utils/callbacks/__init__.py | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index b54cf10c92..36370ef13a 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -16,6 +16,7 @@ import torch import torch.distributed as dist import wandb +import yaml from datasets import load_dataset from tqdm import tqdm from transformers import ( @@ -759,6 +760,37 @@ def on_train_begin( except (FileNotFoundError, ConnectionError) as err: LOG.warning(f"Error while saving Axolotl config to WandB: {err}") + try: + with open(self.axolotl_config_path, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) or {} + + chat_tpl = cfg.get("chat_template_jinja") + if chat_tpl: + with NamedTemporaryFile( + mode="w", delete=True, suffix=".jinja", prefix="chat_template_" + ) as temp_ct_file: + if ( + isinstance(chat_tpl, str) + and os.path.exists(chat_tpl) + and os.path.isfile(chat_tpl) + ): + copyfile(chat_tpl, temp_ct_file.name) + else: + temp_ct_file.write(str(chat_tpl)) + temp_ct_file.flush() + + artifact = wandb.Artifact( + f"chat-template-{wandb.run.id}", type="jinja-template" + ) + artifact.add_file(temp_ct_file.name) + wandb.log_artifact(artifact) + wandb.save(temp_ct_file.name) + LOG.info( + "The chat_template_jinja has been saved to the WandB run under files." + ) + except (FileNotFoundError, ConnectionError, yaml.YAMLError) as err: + LOG.warning(f"Error while saving chat_template_jinja to WandB: {err}") + if args.deepspeed: try: # sync config to top level in run, cannot delete file right away because wandb schedules it to be synced even w/policy = 'now', so let OS delete it later. From 3a5c97e6e5899cbeb7ea284c658712217bf9721c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 9 Oct 2025 14:17:31 -0400 Subject: [PATCH 0996/1405] use can_device_access_peer for P2P checks (#3209) [skip ci] * use can_device_access_peer for P2P checks * also log warn when automatically setting NCCL_P2P_DISABLE=1 --- src/axolotl/utils/environment.py | 60 +++++++++++--------------------- src/axolotl/utils/trainer.py | 1 + 2 files changed, 21 insertions(+), 40 deletions(-) diff --git a/src/axolotl/utils/environment.py b/src/axolotl/utils/environment.py index 7b23484133..d5f2d9f780 100644 --- a/src/axolotl/utils/environment.py +++ b/src/axolotl/utils/environment.py @@ -3,66 +3,46 @@ """ import os -import subprocess # nosec B404 from importlib.metadata import version +import torch from accelerate.utils.environment import ( check_cuda_p2p_ib_support as accelerate_check_cuda_p2p_ib_support, - get_gpu_info, ) from packaging.version import Version, parse +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + def check_cuda_p2p_ib_support(): if not accelerate_check_cuda_p2p_ib_support(): return False - if not check_runpod_p2p_support(): + if not check_cuda_p2p_support(): return False - unsupported_devices = {"RTX 6000 Ada", "L40S"} - try: - device_names, device_count = get_gpu_info() - if 1 < device_count < 8: - if any( - unsupported_device in device_name - for device_name in device_names - for unsupported_device in unsupported_devices - ): - return False - except Exception: # nosec B110 - pass return True -def check_runpod_p2p_support() -> bool: - if "RUNPOD_GPU_COUNT" not in os.environ: - return True +def check_cuda_p2p_support() -> bool: try: - gpu_count = int(os.environ.get("RUNPOD_GPU_COUNT", "1")) + world_size = int(os.environ.get("WORLD_SIZE", "1")) + local_rank = int(os.environ.get("LOCAL_RANK", "0")) except ValueError: return True - if gpu_count >= 2: - # run `nvidia-smi topo -p2p n` and inspect the GPU0 row + + if world_size > 1: + node_world_size = int(os.environ.get("NODE_WORLD_SIZE", "8")) + local_other_rank = (local_rank // node_world_size) * node_world_size + local_other_rank += 1 if (local_rank % node_world_size) == 0 else 0 try: - result = subprocess.run( # nosec B603 B607 - ["nvidia-smi", "topo", "-p2p", "n"], - check=True, - capture_output=True, - text=True, - timeout=5, - ) - except ( - subprocess.CalledProcessError, - FileNotFoundError, - subprocess.TimeoutExpired, - ): - return True # fail-open if detection fails - output_lines = result.stdout.strip().split("\n") - # filter rows that start with "GPU0" (avoid header row) - gpu0_rows = [line for line in output_lines if line.lstrip().startswith("GPU0")] - if not gpu0_rows: + can_p2p = torch.cuda.can_device_access_peer(local_rank, local_other_rank) + except AssertionError as exc: + # some sort of logic error in indexing processes, assume p2p is fine for now + LOG.warning(exc) return True - # consider P2P supported if any OK is present in the GPU0 row - return "OK" in gpu0_rows[-1] + return can_p2p + return True diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index c7fa0a6479..f2f8279f38 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -641,6 +641,7 @@ def setup_parallelism_envs(cfg): def prepare_optim_env(cfg): if not check_cuda_p2p_ib_support(): if os.getenv("NCCL_P2P_DISABLE") is None: + LOG.warning("P2P support not detected, setting `NCCL_P2P_DISABLE=1`") os.environ["NCCL_P2P_DISABLE"] = "1" # TODO @SalmanMohammadi remove the cfg.fsdp check in 0.12 if cfg.fsdp or cfg.fsdp_config: From 08b8fa62cc27d0c8bd7b8cb9bba91d6fcf9067ac Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 9 Oct 2025 14:18:46 -0400 Subject: [PATCH 0997/1405] only calculate packed ds length once if using a large world size (#3210) --- src/axolotl/utils/samplers/multipack.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index d079886132..662c63caaf 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -5,6 +5,7 @@ import gc import math +import os import time from concurrent.futures import ProcessPoolExecutor from multiprocessing import cpu_count, get_context @@ -291,7 +292,10 @@ def __init__( self.total_token_slots = 0 # The number of times to calculate batches to determine minimum packed dataset length - self.num_count_samples = num_count_samples + world_size = int(os.environ.get("WORLD_SIZE", "1")) + self.num_count_samples = ( + 1 if world_size >= num_count_samples else num_count_samples + ) if self.sequential and not isinstance(sampler, SequentialSampler): LOG.warning( From 153edcfe7903170ceb9f71e36f2638add455e5c8 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 10 Oct 2025 10:57:50 +0700 Subject: [PATCH 0998/1405] fix(doc): add act checkpointing migration to fsdp2 docs (#3193) [skip ci] --- docs/multi-gpu.qmd | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/multi-gpu.qmd b/docs/multi-gpu.qmd index fb91f81e5b..57a941b046 100644 --- a/docs/multi-gpu.qmd +++ b/docs/multi-gpu.qmd @@ -88,6 +88,7 @@ fsdp_sync_module_states | **REMOVED** fsdp_cpu_ram_efficient_loading | cpu_ram_efficient_loading fsdp_state_dict_type | state_dict_type fsdp_use_orig_params | **REMOVED** +fsdp_activation_checkpointing | activation_checkpointing For more details, please see the migration guide in the [torchtitan repo](https://github.com/pytorch/torchtitan/blob/main/docs/fsdp.md). In Axolotl, if you were using the following FSDP1 config: From bc2ffb8204466d4df0235c5df37a7bbeb2c384d4 Mon Sep 17 00:00:00 2001 From: Hitesh Sagtani Date: Fri, 10 Oct 2025 18:27:00 +0530 Subject: [PATCH 0999/1405] fix: Enable KD plugin support for PEFT/LoRA adapters (#3207) - Fix _loss_function attribute not found on base model with PEFT - Fix mismatched attribute name (loss_function vs _loss_function) - Set _loss_function on unwrapped base model for PEFT - Enable previously skipped test_llama_lora_kd test - Add test config fixes for LoRA kernel compatibility Fixes https://github.com/axolotl-ai-cloud/axolotl/issues/3206 --- src/axolotl/integrations/kd/kernels/models.py | 4 ++-- src/axolotl/integrations/kd/trainer.py | 11 ++++++++++- tests/e2e/integrations/test_kd.py | 5 ++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/axolotl/integrations/kd/kernels/models.py b/src/axolotl/integrations/kd/kernels/models.py index f7b468669e..badb3460de 100644 --- a/src/axolotl/integrations/kd/kernels/models.py +++ b/src/axolotl/integrations/kd/kernels/models.py @@ -72,9 +72,9 @@ def kldiv_forward_llama_like( # Only compute necessary logits, and do not upcast them to float if we are not computing the loss # TODO, we can optimize this further by filtering hidden_states on sequence dimension using labels != -100 - # self.loss_function should be LigerFusedLinearKLTopKLogprobLoss + # self._loss_function should be LigerFusedLinearKLTopKLogprobLoss - loss = self.loss_function( + loss = self._loss_function( self.lm_head.weight, hidden_states, target_token_ids, diff --git a/src/axolotl/integrations/kd/trainer.py b/src/axolotl/integrations/kd/trainer.py index 7ec43333a6..0e98497a7b 100644 --- a/src/axolotl/integrations/kd/trainer.py +++ b/src/axolotl/integrations/kd/trainer.py @@ -29,7 +29,8 @@ class AxolotlKDTrainer(AxolotlTrainer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.model_accepts_loss_kwargs = True - self.model._loss_function = LigerFusedLinearKLTopKLogprobLoss( + + loss_fn = LigerFusedLinearKLTopKLogprobLoss( self.args.kd_ce_alpha, # hard label loss self.args.kd_alpha, # kd loss self.args.kd_temperature, @@ -37,6 +38,14 @@ def __init__(self, *args, **kwargs): compute_ce_loss=bool(self.args.kd_ce_alpha), normalize_topk=self.args.kd_normalize_topk, ) + target = self.model + + # Unwrap PEFT wrapper + if hasattr(target, "get_base_model"): + target = target.get_base_model() + + # Set on the actual model instance + target._loss_function = loss_fn def _set_signature_columns_if_needed(self): super()._set_signature_columns_if_needed() diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py index ff47b94270..d89044247b 100644 --- a/tests/e2e/integrations/test_kd.py +++ b/tests/e2e/integrations/test_kd.py @@ -104,7 +104,6 @@ def test_llama_kd(self, temp_dir, kd_min_cfg): temp_dir + "/runs", "train/loss", 1.4, "Train Loss (%s) is too high" ) - @pytest.mark.skip(reason="Chunked KD loss doesn't support PEFT/LoRA") @pytest.mark.parametrize( "load_in_8bit", [True, False], @@ -120,6 +119,10 @@ def test_llama_lora_kd(self, temp_dir, kd_min_cfg, load_in_8bit): "lora_r": 16, "lora_alpha": 32, "lora_dropout": 0.0, + "lora_modules_to_save": ["embed_tokens", "lm_head"], + "lora_mlp_kernel": False, + "lora_qkv_kernel": False, + "lora_o_kernel": False, } | kd_min_cfg ) From 143dea4753fe4a9ff5d9ef0f303e41a32091e355 Mon Sep 17 00:00:00 2001 From: salman Date: Fri, 10 Oct 2025 14:44:25 +0100 Subject: [PATCH 1000/1405] `FSDPConfig` (#3170) --- examples/llama-3/3b-fp8-fsdp2.yaml | 2 +- src/axolotl/core/trainers/base.py | 7 --- src/axolotl/utils/schemas/config.py | 4 +- src/axolotl/utils/schemas/fsdp.py | 71 +++++++++++++++++++++++++ src/axolotl/utils/schemas/validation.py | 2 +- tests/e2e/multigpu/test_llama.py | 3 -- tests/test_normalize_config.py | 4 -- 7 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 src/axolotl/utils/schemas/fsdp.py diff --git a/examples/llama-3/3b-fp8-fsdp2.yaml b/examples/llama-3/3b-fp8-fsdp2.yaml index bea698c0e3..b7de7ca528 100644 --- a/examples/llama-3/3b-fp8-fsdp2.yaml +++ b/examples/llama-3/3b-fp8-fsdp2.yaml @@ -29,7 +29,7 @@ flex_attention: true flex_attn_compile_kwargs: dynamic: false mode: max-autotune-no-cudagraphs - +save_strategy: no torch_compile: true wandb_project: diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 627f8e3f8b..11dfecb986 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -560,13 +560,6 @@ def create_accelerator_and_postprocess(self): super().create_accelerator_and_postprocess() - if self.is_fsdp_enabled: - if ( - "limit_all_gathers" in self.args.fsdp_config - and self.args.fsdp_config["limit_all_gathers"] - ): - self.accelerator.state.fsdp_plugin.limit_all_gathers = True - def additional_accelerator_args( self, fp8: bool = False, enable_fsdp_float8_all_gather: bool = False, **kwargs ) -> dict[str, Any]: diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 0177b19f6c..7cf8c3b4af 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -24,6 +24,7 @@ ) from axolotl.utils.schemas.deprecated import DeprecatedParameters, RemappedParameters from axolotl.utils.schemas.enums import ChatTemplate, RingAttnFunc, RLType +from axolotl.utils.schemas.fsdp import FSDPConfig from axolotl.utils.schemas.integrations import ( CometConfig, GradioConfig, @@ -667,8 +668,7 @@ class AxolotlInputConfig( json_schema_extra={"description": "FSDP configuration"}, deprecated="Configuring FSDP using `fsdp` is deprecated. Please use `fsdp_config` instead. ", ) - # TODO @SalmanMohammadi strongly type this as its own schema - fsdp_config: dict[str, Any] | None = Field( + fsdp_config: FSDPConfig | None = Field( default=None, json_schema_extra={"description": "FSDP configuration options"} ) fsdp_version: int | None = Field( diff --git a/src/axolotl/utils/schemas/fsdp.py b/src/axolotl/utils/schemas/fsdp.py new file mode 100644 index 0000000000..f34f40e8e4 --- /dev/null +++ b/src/axolotl/utils/schemas/fsdp.py @@ -0,0 +1,71 @@ +""" +FSDP Configuration Schema +""" + +from typing import Literal + +from pydantic import BaseModel, Field + + +class FSDPConfig(BaseModel): + """ + FSDP Configuration Schema + """ + + activation_checkpointing: bool | None = Field( + default=None, + description="Enable activation checkpointing to reduce memory usage during forward passes", + ) + offload_params: bool | None = Field( + default=None, + description="Offload parameters to CPU to reduce GPU memory usage", + ) + sync_module_states: bool | None = Field( + default=None, + description="Synchronize module states across all processes", + ) + cpu_ram_efficient_loading: bool | None = Field( + default=None, + description="Enable CPU RAM efficient loading to reduce memory usage during model loading", + ) + cpu_offload_pin_memory: bool | None = Field( + default=None, + description="Disabling this enables swap memory usage for resource-constrained setups when offload_params is enabled.", + ) + use_orig_params: bool | None = Field( + default=None, + description="Use original parameters instead of flattened parameters", + ) + + state_dict_type: ( + Literal["FULL_STATE_DICT", "LOCAL_STATE_DICT", "SHARDED_STATE_DICT"] | None + ) = Field( + default=None, + description="Type of state dict to use for saving/loading checkpoints", + ) + final_state_dict_type: ( + Literal["FULL_STATE_DICT", "LOCAL_STATE_DICT", "SHARDED_STATE_DICT"] | None + ) = Field( + default=None, + description="Final state dict type to use after training completion", + ) + + auto_wrap_policy: Literal["TRANSFORMER_BASED_WRAP", "SIZE_BASED_WRAP"] | None = ( + Field( + default=None, + description="Policy for automatically wrapping modules with FSDP", + ) + ) + transformer_layer_cls_to_wrap: str | None = Field( + default=None, + description="Class name of transformer layers to wrap (e.g., 'LlamaDecoderLayer')", + ) + + reshard_after_forward: bool | None = Field( + default=None, + description="Reshard parameters after forward pass to save memory", + ) + mixed_precision_policy: str | None = Field( + default=None, + description="Mixed precision policy for FSDP (e.g., 'fp16', 'bf16')", + ) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 4abe45e641..3689768319 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -881,7 +881,7 @@ def check_fsdp_offload_w_8bit_optimizer(self): and self.fsdp_config and self.optimizer and "8bit" in self.optimizer.value - and self.fsdp_config["offload_params"] + and self.fsdp_config.offload_params and str(self.fsdp_version) != "2" ): raise ValueError( diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index b836291e54..ffdbad9429 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -353,7 +353,6 @@ def test_fsdp(self, temp_dir, gradient_accumulation_steps): "auto_wrap", ], "fsdp_config": { - "fsdp_limit_all_gathers": True, "fsdp_offload_params": False, "fsdp_sync_module_states": True, "fsdp_use_orig_params": False, @@ -431,7 +430,6 @@ def test_fsdp_packed(self, temp_dir, fsdp_state_dict_type): "auto_wrap", ], "fsdp_config": { - "fsdp_limit_all_gathers": True, "fsdp_offload_params": False, "fsdp_sync_module_states": True, "fsdp_use_orig_params": False, @@ -595,7 +593,6 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "auto_wrap", ], "fsdp_config": { - "fsdp_limit_all_gathers": True, "fsdp_offload_params": False, "fsdp_sync_module_states": True, "fsdp_use_orig_params": False, diff --git a/tests/test_normalize_config.py b/tests/test_normalize_config.py index 658e06fcbb..f0d3a2d724 100644 --- a/tests/test_normalize_config.py +++ b/tests/test_normalize_config.py @@ -111,7 +111,6 @@ def test_migrate_fsdp_config(self): "fsdp_auto_wrap_policy": "TRANSFORMER_BASED_WRAP", "fsdp_offload_params": False, "fsdp_cpu_ram_efficient_loading": True, - "regular_param": "value", } } ) @@ -124,7 +123,6 @@ def test_migrate_fsdp_config(self): ) self.assertEqual(cfg_with_version.fsdp_config.offload_params, False) self.assertEqual(cfg_with_version.fsdp_config.cpu_ram_efficient_loading, True) - self.assertEqual(cfg_with_version.fsdp_config.regular_param, "value") self.assertNotIn("fsdp_auto_wrap_policy", cfg_with_version.fsdp_config) self.assertNotIn("fsdp_offload_params", cfg_with_version.fsdp_config) @@ -137,7 +135,6 @@ def test_migrate_fsdp_config(self): "fsdp_config": { "fsdp_auto_wrap_policy": "SIZE_BASED_WRAP", "fsdp_offload_params": True, - "regular_param": "value", } } ) @@ -149,7 +146,6 @@ def test_migrate_fsdp_config(self): cfg_without_version.fsdp_config.auto_wrap_policy, "SIZE_BASED_WRAP" ) self.assertEqual(cfg_without_version.fsdp_config.offload_params, True) - self.assertEqual(cfg_without_version.fsdp_config.regular_param, "value") self.assertNotIn("fsdp_auto_wrap_policy", cfg_without_version.fsdp_config) self.assertNotIn("fsdp_offload_params", cfg_without_version.fsdp_config) From cd856b45b168b41f2286f59efe13facc55a36eb5 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:48:12 +0530 Subject: [PATCH 1001/1405] feat:add support dataset_num_processes (#3129) [skip ci] * feat:add support dataset_num_processes * chore * required changes * requested chnages * required chnages * required changes * required changes * elif get_default_process_count() * add:del data * Update cicd/Dockerfile.jinja Co-authored-by: NanoCode012 * Update cicd/single_gpu.py Co-authored-by: NanoCode012 --------- Co-authored-by: salman Co-authored-by: NanoCode012 --- cicd/Dockerfile.jinja | 2 +- cicd/single_gpu.py | 2 +- devtools/dev_chat_template.yml | 2 +- docs/debugging.qmd | 4 +-- src/axolotl/core/builders/base.py | 4 +-- src/axolotl/utils/data/rl.py | 4 +-- src/axolotl/utils/data/shared.py | 2 +- src/axolotl/utils/data/utils.py | 2 +- src/axolotl/utils/data/wrappers.py | 2 +- src/axolotl/utils/datasets.py | 2 ++ src/axolotl/utils/schemas/config.py | 31 ++++++++++++++++--- src/axolotl/utils/trainer.py | 10 +++--- tests/core/test_builders.py | 2 +- .../patched/test_activation_checkpointing.py | 2 +- tests/e2e/test_llama_pretrain.py | 2 +- tests/test_datasets.py | 14 ++++----- tests/test_exact_deduplication.py | 2 +- tests/test_packed_dataset.py | 2 +- 18 files changed, 57 insertions(+), 34 deletions(-) diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 94c9a67e3e..6a1ddb66d9 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -9,7 +9,7 @@ ENV GITHUB_REF="{{ GITHUB_REF }}" ENV GITHUB_SHA="{{ GITHUB_SHA }}" ENV NIGHTLY_BUILD="{{ NIGHTLY_BUILD }}" ENV HF_HOME="{{ HF_HOME }}" -ENV AXOLOTL_DATASET_PROCESSES="8" +ENV AXOLOTL_DATASET_NUM_PROC="8" RUN apt-get update && \ apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm diff --git a/cicd/single_gpu.py b/cicd/single_gpu.py index 3bca5806f0..cd73f60b81 100644 --- a/cicd/single_gpu.py +++ b/cicd/single_gpu.py @@ -65,7 +65,7 @@ def run_cmd(cmd: str, run_folder: str): import subprocess # nosec sp_env = os.environ.copy() - sp_env["AXOLOTL_DATASET_PROCESSES"] = "8" + sp_env["AXOLOTL_DATASET_NUM_PROC"] = "8" # Propagate errors from subprocess. try: diff --git a/devtools/dev_chat_template.yml b/devtools/dev_chat_template.yml index 27dc9be1af..32d5e56a0c 100644 --- a/devtools/dev_chat_template.yml +++ b/devtools/dev_chat_template.yml @@ -13,7 +13,7 @@ datasets: val_set_size: 0 output_dir: temp_debug/axolotl_outputs/model dataset_prepared_path: temp_debug/axolotl_outputs/data -dataset_processes: 1 +dataset_num_proc: 1 sequence_len: 4096 sample_packing: false diff --git a/docs/debugging.qmd b/docs/debugging.qmd index bf3c6fe7e8..04b4faa648 100644 --- a/docs/debugging.qmd +++ b/docs/debugging.qmd @@ -29,7 +29,7 @@ While debugging it's helpful to simplify your test scenario as much as possible. 1. **Make sure you are using the latest version of axolotl**: This project changes often and bugs get fixed fast. Check your git branch and make sure you have pulled the latest changes from `main`. 1. **Eliminate concurrency**: Restrict the number of processes to 1 for both training and data preprocessing: - Set `CUDA_VISIBLE_DEVICES` to a single GPU, ex: `export CUDA_VISIBLE_DEVICES=0`. - - Set `dataset_processes: 1` in your axolotl config or run the training command with `--dataset_processes=1`. + - Set `dataset_num_proc: 1` in your axolotl config or run the training command with `--dataset_num_proc=1`. 2. **Use a small dataset**: Construct or use a small dataset from HF Hub. When using a small dataset, you will often have to make sure `sample_packing: False` and `eval_sample_packing: False` to avoid errors. If you are in a pinch and don't have time to construct a small dataset but want to use from the HF Hub, you can shard the data (this will still tokenize the entire dataset, but will only use a fraction of the data for training. For example, to shard the dataset into 20 pieces, add the following to your axolotl config): ```yaml @@ -101,7 +101,7 @@ For example, to mimic the command `cd devtools && CUDA_VISIBLE_DEVICES=0 acceler "-m", "axolotl.cli.train", "dev_chat_template.yml", // The flags below simplify debugging by overriding the axolotl config // with the debugging tips above. Modify as needed. - "--dataset_processes=1", // limits data preprocessing to one process + "--dataset_num_proc=1", // limits data preprocessing to one process "--max_steps=1", // limits training to just one step "--batch_size=1", // minimizes batch size "--micro_batch_size=1", // minimizes batch size diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 3ad8012f9e..8c86e335ef 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -491,6 +491,7 @@ def _set_base_training_args( "dion_momentum", "dion_rank_fraction", "dion_rank_multiple_of", + "dataset_num_proc", ]: if hasattr(self.cfg, arg) and getattr(self.cfg, arg) is not None: training_args_kwargs[arg] = getattr(self.cfg, arg) @@ -514,9 +515,6 @@ def _set_base_training_args( training_args_kwargs["max_steps"] = self.cfg.max_steps or total_num_steps or -1 training_args_kwargs["num_train_epochs"] = self.cfg.num_epochs - if self.cfg.dataset_processes: - training_args_kwargs["dataset_num_proc"] = self.cfg.dataset_processes - # max_length is not used in CausalTrainer if self.cfg.reward_model or self.cfg.rl: training_args_kwargs["max_length"] = self.cfg.sequence_len diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index d371c9acb2..f7a5ec04c9 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -113,7 +113,7 @@ def _map_dataset( dataset = dataset.map( ds_transform_fn, - num_proc=cfg.dataset_processes, + num_proc=cfg.dataset_num_proc, load_from_cache_file=not cfg.is_preprocess, desc="Mapping RL Dataset", **map_kwargs, @@ -234,7 +234,7 @@ def _load_split(cfg: DictDefault, split: Literal["train", "test"]) -> Dataset: prior_len = len(split_datasets[i]) split_datasets[i] = split_datasets[i].filter( drop_long, - num_proc=cfg.dataset_processes, + num_proc=cfg.dataset_num_proc, load_from_cache_file=not cfg.is_preprocess, desc="Dropping Long Sequences", ) diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 6b6e0e2816..c9a91b8293 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -409,7 +409,7 @@ def save_preprocessed_dataset( ) -> None: """Save preprocessed dataset to disk and optionally push to the HF Hub.""" prepared_ds_path = get_prepared_dataset_path(cfg, dataset_hash) - num_workers = cfg.dataset_processes or get_default_process_count() + num_workers = cfg.dataset_num_proc or get_default_process_count() if isinstance(dataset, IterableDataset): ds_from_iter = Dataset.from_generator( functools.partial(_generate_from_iterable_dataset, dataset), diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index 445a65d6c7..2d0ca9d0e4 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -223,7 +223,7 @@ def handle_long_seq_in_dataset( filter_map_kwargs = {} if not isinstance(dataset, IterableDataset): - filter_map_kwargs["num_proc"] = cfg.dataset_processes + filter_map_kwargs["num_proc"] = cfg.dataset_num_proc filter_map_kwargs["load_from_cache_file"] = not cfg.is_preprocess drop_long_kwargs = {} diff --git a/src/axolotl/utils/data/wrappers.py b/src/axolotl/utils/data/wrappers.py index cb9e2c6b40..3a10bde006 100644 --- a/src/axolotl/utils/data/wrappers.py +++ b/src/axolotl/utils/data/wrappers.py @@ -80,7 +80,7 @@ def get_dataset_wrapper( """ # Common parameters for dataset wrapping dataset_kwargs: dict[str, Any] = { - "process_count": cfg.dataset_processes, + "process_count": cfg.dataset_num_proc, "keep_in_memory": cfg.dataset_keep_in_memory is True, } diff --git a/src/axolotl/utils/datasets.py b/src/axolotl/utils/datasets.py index 93e1a24163..9b8a8e25ae 100644 --- a/src/axolotl/utils/datasets.py +++ b/src/axolotl/utils/datasets.py @@ -4,6 +4,8 @@ def get_default_process_count(): + if axolotl_dataset_num_proc := os.environ.get("AXOLOTL_DATASET_NUM_PROC"): + return int(axolotl_dataset_num_proc) if axolotl_dataset_processes := os.environ.get("AXOLOTL_DATASET_PROCESSES"): return int(axolotl_dataset_processes) if runpod_cpu_count := os.environ.get("RUNPOD_CPU_COUNT"): diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 7cf8c3b4af..4d1d0aab20 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -234,6 +234,7 @@ class AxolotlInputConfig( ) dataset_processes: int | None = Field( default=None, + deprecated="Use `dataset_num_proc` instead. This parameter will be removed in a future version.", json_schema_extra={ "description": ( "The maximum number of processes to use while preprocessing your input dataset. This defaults to `os.cpu_count()` if not set.\n" @@ -241,6 +242,16 @@ class AxolotlInputConfig( ) }, ) + dataset_num_proc: int | None = Field( + default=None, + json_schema_extra={ + "description": ( + "The maximum number of processes to use while preprocessing your input dataset. This defaults to `os.cpu_count()` if not set.\n" + "For Runpod VMs, it will default to number of vCPUs via RUNPOD_CPU_COUNT." + ) + }, + ) + dataset_exact_deduplication: bool | None = Field( default=None, json_schema_extra={ @@ -1314,10 +1325,22 @@ def default_dataloader_opts(cls, data): @model_validator(mode="before") @classmethod - def default_dataset_processes(cls, data): - if data.get("dataset_processes") is None: - data["dataset_processes"] = get_default_process_count() - + def default_dataset_num_proc(cls, data): + if data.get("dataset_processes") is not None: + if data.get("dataset_num_proc") is None: + data["dataset_num_proc"] = data["dataset_processes"] + LOG.warning( + "dataset_processes is deprecated and will be removed in a future version. " + "Please use dataset_num_proc instead." + ) + else: + LOG.warning( + "Both dataset_processes and dataset_num_proc are set. " + "Using dataset_num_proc and ignoring dataset_processes." + ) + del data["dataset_processes"] + elif data.get("dataset_num_proc") is None: + data["dataset_num_proc"] = get_default_process_count() return data @model_validator(mode="before") diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index f2f8279f38..d97577d863 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -278,7 +278,7 @@ def drop_no_trainable_tokens(sample): prior_len = None filter_map_kwargs = {} if not isinstance(train_dataset, IterableDataset): - filter_map_kwargs["num_proc"] = cfg.dataset_processes + filter_map_kwargs["num_proc"] = cfg.dataset_num_proc filter_map_kwargs["load_from_cache_file"] = not cfg.is_preprocess drop_long_kwargs = {} @@ -318,7 +318,7 @@ def drop_no_trainable_tokens(sample): if cfg.group_by_length: train_dataset = train_dataset.map( add_length, - num_proc=cfg.dataset_processes, + num_proc=cfg.dataset_num_proc, load_from_cache_file=not cfg.is_preprocess, desc="Group By Length", ) @@ -335,7 +335,7 @@ def drop_no_trainable_tokens(sample): ) train_dataset = train_dataset.map( pose_fn, - num_proc=cfg.dataset_processes, + num_proc=cfg.dataset_num_proc, load_from_cache_file=not cfg.is_preprocess, desc="Add position_id column (PoSE)", ) @@ -344,7 +344,7 @@ def drop_no_trainable_tokens(sample): if eval_dataset: eval_dataset = eval_dataset.map( pose_fn, - num_proc=cfg.dataset_processes, + num_proc=cfg.dataset_num_proc, load_from_cache_file=not cfg.is_preprocess, desc="Add position_id column (PoSE)", ) @@ -469,7 +469,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): bin_size=cfg.sample_packing_bin_size, sequential=cfg.sample_packing_sequentially, drop_last=True, - num_processes=cfg.dataset_processes, + num_processes=cfg.dataset_prcoesses, mp_start_method=cfg.sample_packing_mp_start_method or "fork", ) diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index 6428aa9771..67481b2ad5 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -440,7 +440,7 @@ def test_custom_optimizer_cls_and_kwargs( ] else: raise ValueError(f"Unhandled cfg_string: {cfg_string}") - cfg["dataset_processes"] = 4 + cfg["dataset_num_proc"] = 4 if cfg_string == "grpo_cfg": rewards_dir = tmp_path / "rewards_test" diff --git a/tests/e2e/patched/test_activation_checkpointing.py b/tests/e2e/patched/test_activation_checkpointing.py index ddace8ef14..e8006c1627 100644 --- a/tests/e2e/patched/test_activation_checkpointing.py +++ b/tests/e2e/patched/test_activation_checkpointing.py @@ -69,7 +69,7 @@ def test_activation_checkpointing_offload( "save_safetensors": True, "gradient_checkpointing": gradient_checkpointing, "save_first_step": False, - "dataset_processes": 4, + "dataset_num_proc": 4, } ) diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index a041244e7a..f0daa9dd6e 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -29,7 +29,7 @@ def test_pretrain(self, temp_dir, sample_packing, pretrain_multipack_attn): "sequence_len": 1024, "sample_packing": sample_packing, "pretrain_multipack_attn": pretrain_multipack_attn, - "dataset_processes": 1, + "dataset_num_proc": 1, "special_tokens": { "pad_token": "<|endoftext|>", }, diff --git a/tests/test_datasets.py b/tests/test_datasets.py index ea5ee368d1..bd1c8f2c2b 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -141,7 +141,7 @@ def test_load_from_save_to_disk(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], - "dataset_processes": 4, + "dataset_num_proc": 4, } ) @@ -180,7 +180,7 @@ def test_load_from_dir_of_parquet(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], - "dataset_processes": 4, + "dataset_num_proc": 4, } ) @@ -219,7 +219,7 @@ def test_load_from_dir_of_json(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], - "dataset_processes": 4, + "dataset_num_proc": 4, } ) @@ -252,7 +252,7 @@ def test_load_from_single_parquet(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], - "dataset_processes": 4, + "dataset_num_proc": 4, } ) @@ -285,7 +285,7 @@ def test_load_from_single_json(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], - "dataset_processes": 4, + "dataset_num_proc": 4, } ) @@ -370,7 +370,7 @@ def test_load_hub_with_revision_with_dpo( "rl": "dpo", "chat_template": "llama3", "datasets": [ALPACA_MESSAGES_CONFIG_REVISION], - "dataset_processes": 4, + "dataset_num_proc": 4, } ) @@ -471,7 +471,7 @@ def test_loading_local_dataset_folder(self, tokenizer): "type": "alpaca", }, ], - "dataset_processes": 4, + "dataset_num_proc": 4, } ) diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index 65deb5209b..a519db525b 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -210,7 +210,7 @@ def cfg(self): ALPACA_MESSAGES_CONFIG_REVISION, ALPACA_MESSAGES_CONFIG_REVISION, ], - "dataset_processes": 4, + "dataset_num_proc": 4, } ) yield fixture diff --git a/tests/test_packed_dataset.py b/tests/test_packed_dataset.py index 64f314e2e5..953d523af4 100644 --- a/tests/test_packed_dataset.py +++ b/tests/test_packed_dataset.py @@ -55,7 +55,7 @@ def test_lora_packing(self, temp_dir): "type": "alpaca", }, ], - "dataset_processes": 4, + "dataset_num_proc": 4, "num_epochs": 1, "max_steps": 20, "save_steps": 10, From 8c7f63cf971fbaf5627f770881a38c99816da631 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 13 Oct 2025 17:19:15 +0700 Subject: [PATCH 1002/1405] fix: unpack cce imported incorrectly (#3212) [skip ci] --- examples/colab-notebooks/colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/integrations/cut_cross_entropy/README.md | 2 +- src/axolotl/integrations/cut_cross_entropy/__init__.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index ee99c283fd..cea1aeda0e 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@49f3308\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@8a1a0ec\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index cf8bd57e71..cb498c0020 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@49f3308"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@8a1a0ec"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 08cd41200a..5c7c5166bb 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@49f3308" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@8a1a0ec" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index ed6ebe62aa..bd0124b93f 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@49f3308"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@8a1a0ec"`' ) From 6e2f5ccf9f03040e5de3252999aa0733fc88261b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:21:49 -0400 Subject: [PATCH 1003/1405] chore: update pre-commit hooks (#3211) [skip ci] Co-authored-by: djsaunde <1245942+djsaunde@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e853243cd5..0e455f52c8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: no-commit-to-branch args: ['--branch', 'main'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.14.0 hooks: - id: ruff args: [--fix] From 4cdfdfebb51d6a53d4468c6512b75c500ab85293 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 14 Oct 2025 15:54:05 -0400 Subject: [PATCH 1004/1405] upgrade transformers==4.57.1 and peft==0.23.1 (#3214) --- requirements.txt | 4 ++-- tests/e2e/multigpu/test_llama.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9c56638a34..e1f1b10a52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,11 +13,11 @@ packaging==23.2 huggingface_hub>=0.33.0 peft>=0.17.1 tokenizers>=0.21.1 -transformers==4.57.0 +transformers==4.57.1 accelerate==1.10.1 datasets==4.0.0 deepspeed>=0.17.0 -trl==0.23.0 +trl==0.23.1 hf_xet==1.1.5 kernels==0.9.0 trackio diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index ffdbad9429..3383e71d14 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -546,7 +546,6 @@ def test_fsdp2_packed( temp_dir + "/runs", "train/train_loss", 2.1, "Train Loss (%s) is too high" ) - @pytest.mark.skip("regression failure from v4.57.0") def test_fsdp_qlora_prequant_packed(self, temp_dir): cfg = DictDefault( { From aa1240acd8d7e9640a01a78e9da8a0725b158041 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 16 Oct 2025 16:07:27 +0700 Subject: [PATCH 1005/1405] fix: transformers deprecate load_in_Xbit in model_kwargs (#3205) * fix: transformers deprecate load_in_Xbit in model_kwargs * fix: test to read from quantization_config kwarg * fix: test * fix: access * fix: test weirdly entering incorrect config --- src/axolotl/loaders/model.py | 16 ++-------------- tests/test_loaders.py | 30 ++++++++++++++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index f438d6b61a..aeec46584c 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -515,9 +515,6 @@ def _set_quantization_config(self): if self.cfg.model_quantization_config_kwargs: mxfp4_kwargs = self.cfg.model_quantization_config_kwargs self.model_kwargs["quantization_config"] = Mxfp4Config(**mxfp4_kwargs) - else: - self.model_kwargs["load_in_8bit"] = self.cfg.load_in_8bit - self.model_kwargs["load_in_4bit"] = self.cfg.load_in_4bit if self.cfg.gptq: if not hasattr(self.model_config, "quantization_config"): @@ -552,9 +549,7 @@ def _set_quantization_config(self): self.model_kwargs["quantization_config"] = BitsAndBytesConfig( **self.model_config.quantization_config ) - elif self.cfg.adapter == "qlora" and self.model_kwargs.get( - "load_in_4bit", False - ): + elif self.cfg.adapter == "qlora" and self.cfg.load_in_4bit: bnb_config = { "load_in_4bit": True, "llm_int8_threshold": 6.0, @@ -580,9 +575,7 @@ def _set_quantization_config(self): self.model_kwargs["quantization_config"] = BitsAndBytesConfig( **bnb_config, ) - elif self.cfg.adapter == "lora" and self.model_kwargs.get( - "load_in_8bit", False - ): + elif self.cfg.adapter == "lora" and self.cfg.load_in_8bit: bnb_config = { "load_in_8bit": True, } @@ -596,11 +589,6 @@ def _set_quantization_config(self): **bnb_config, ) - # no longer needed per https://github.com/huggingface/transformers/pull/26610 - if "quantization_config" in self.model_kwargs or self.cfg.gptq: - self.model_kwargs.pop("load_in_8bit", None) - self.model_kwargs.pop("load_in_4bit", None) - def _set_attention_config(self): """Sample packing uses custom FA2 patch""" if self.cfg.attn_implementation: diff --git a/tests/test_loaders.py b/tests/test_loaders.py index f516d0ca4e..9130905667 100644 --- a/tests/test_loaders.py +++ b/tests/test_loaders.py @@ -80,16 +80,26 @@ def test_set_quantization_config( hasattr(self.model_loader.model_kwargs, "load_in_8bit") and hasattr(self.model_loader.model_kwargs, "load_in_4bit") ) - elif load_in_8bit and self.cfg.adapter is not None: - assert self.model_loader.model_kwargs["load_in_8bit"] - elif load_in_4bit and self.cfg.adapter is not None: - assert self.model_loader.model_kwargs["load_in_4bit"] - - if (self.cfg.adapter == "qlora" and load_in_4bit) or ( - self.cfg.adapter == "lora" and load_in_8bit - ): - assert self.model_loader.model_kwargs.get( - "quantization_config", BitsAndBytesConfig + + if self.cfg.adapter == "qlora" and load_in_4bit: + assert isinstance( + self.model_loader.model_kwargs.get("quantization_config"), + BitsAndBytesConfig, + ) + + assert ( + self.model_loader.model_kwargs["quantization_config"]._load_in_4bit + is True + ) + if self.cfg.adapter == "lora" and load_in_8bit: + assert isinstance( + self.model_loader.model_kwargs.get("quantization_config"), + BitsAndBytesConfig, + ) + + assert ( + self.model_loader.model_kwargs["quantization_config"]._load_in_8bit + is True ) def test_message_property_mapping(self): From 93ba57396f103778dc4e02cb954bdb46ef155fe2 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 17 Oct 2025 10:35:03 +0700 Subject: [PATCH 1006/1405] fix: qwen3_vl attention config (#3216) --- src/axolotl/monkeypatch/lora_kernels.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index e845dc6ceb..8e335fe4c7 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -134,6 +134,11 @@ def get_attention_cls_from_config(cfg: DictDefault) -> Type[nn.Module]: return Qwen2Attention + if model_type == "qwen3_vl": + from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLTextAttention + + return Qwen3VLTextAttention + if model_type == "mllama": from transformers.models.mllama.modeling_mllama import MllamaTextSelfAttention From 87565ecc05f1b8fd1f8b907dd750d3a5d09adf9a Mon Sep 17 00:00:00 2001 From: Leonard Date: Fri, 17 Oct 2025 19:00:26 +0900 Subject: [PATCH 1007/1405] Add chat_template.argilla_chat support for DPO datasets (#3202) * Add chat_template.argilla_chat support for DPO datasets Creates a new chat_template.argilla_chat prompt strategy for handling DPO datasets where chosen/rejected fields contain full conversations (messages + final response), following the pattern of chatml.argilla_chat and llama3.argilla_chat. - Add argilla_chat() function to chat_template.py - Add chat_template.argilla_chat to RLHF documentation - Add test coverage for argilla_chat with multiple tokenizers Dataset format: { "chosen": [ {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."} ], "rejected": [ {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."} ] } * Fix chat_template.argilla_chat return value contract and add docstring - Return (transform_fn, dataset_kwargs) tuple instead of bare transform_fn - Add remove_columns specification for field_chosen and field_rejected - Add comprehensive docstring with Args/Returns sections - Update tests to unpack tuple return value Addresses PR feedback to maintain consistency with chat_template.default() and properly specify columns to remove after dataset transformation. * Update tests/prompt_strategies/test_dpo_chat_templates.py Co-authored-by: Wing Lian --------- Co-authored-by: Wing Lian --- docs/rlhf.qmd | 15 +++ .../prompt_strategies/dpo/chat_template.py | 120 ++++++++++++++++++ .../test_dpo_chat_templates.py | 78 +++++++++++- 3 files changed, 212 insertions(+), 1 deletion(-) diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 4a67b75595..594ebc743d 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -219,6 +219,21 @@ DPO supports the following types with the following dataset format: } ``` +#### chat_template.argilla_chat + +```json +{ + "chosen": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ], + "rejected": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ] +} +``` + #### chat_template.default ```yaml diff --git a/src/axolotl/prompt_strategies/dpo/chat_template.py b/src/axolotl/prompt_strategies/dpo/chat_template.py index 85c4d21828..58b4d75bd3 100644 --- a/src/axolotl/prompt_strategies/dpo/chat_template.py +++ b/src/axolotl/prompt_strategies/dpo/chat_template.py @@ -120,3 +120,123 @@ def transform_fn(sample, tokenizer=None): return result return transform_fn, {"remove_columns": [field_messages]} + + +def argilla_chat(cfg, dataset_idx=0, **kwargs): + """ + DPO chat template strategy for argilla-style datasets. + + For argilla-style datasets where chosen/rejected contain full conversations + instead of single response messages. Extracts the conversation history from + the chosen field and formats both chosen/rejected responses using the + configured chat template. + + Args: + cfg: Configuration object containing chat_template and dataset settings + dataset_idx: Index of the dataset in the config (default: 0) + **kwargs: Additional keyword arguments (unused) + + Returns: + tuple: (transform_fn, dataset_kwargs) where: + - transform_fn: Function to transform dataset samples + - dataset_kwargs: Dict with 'remove_columns' specifying columns to drop + + Dataset format: + { + "chosen": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ], + "rejected": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ] + } + """ + ds_cfg = cfg["datasets"][dataset_idx] + ds_cfg = handle_legacy_message_fields_logic(ds_cfg) + + chat_template_choice, chat_template_jinja = extract_chat_template_args( + cfg=cfg, ds_cfg=ds_cfg + ) + field_chosen = ds_cfg.get("field_chosen", "chosen") + field_rejected = ds_cfg.get("field_rejected", "rejected") + message_property_mappings = ds_cfg.get( + "message_property_mappings", + { + "role": "role", + "content": "content", + }, + ) + role_map_inv = ds_cfg.get( + "roles", + { + "user": ["user"], + "assistant": ["assistant"], + "system": ["system"], + }, + ) + role_map = {} + for target, sources in role_map_inv.items(): + for source in sources: + role_map[source] = target + + def transform_fn(sample, tokenizer=None): + chat_template_string = get_chat_template( + user_choice=chat_template_choice, + jinja_template=chat_template_jinja, + tokenizer=tokenizer, + ) + + chosen_raw = sample[field_chosen] + rejected_raw = sample[field_rejected] + + # Extract messages (all but last) and responses (last message) + chosen_messages = [ + { + "role": role_map[m[message_property_mappings["role"]]], + "content": m[message_property_mappings["content"]], + } + for m in chosen_raw[:-1] + ] + chosen_response = { + "role": role_map[chosen_raw[-1][message_property_mappings["role"]]], + "content": chosen_raw[-1][message_property_mappings["content"]], + } + + rejected_response = { + "role": role_map[rejected_raw[-1][message_property_mappings["role"]]], + "content": rejected_raw[-1][message_property_mappings["content"]], + } + + dummy_user_message = {"role": "user", "content": "[[dummy_message]]"} + + result = {} + result["prompt"] = tokenizer.apply_chat_template( + chosen_messages, + add_generation_prompt=True, + chat_template=chat_template_string, + tokenize=False, + ) + + result["chosen"] = tokenizer.apply_chat_template( + [dummy_user_message, chosen_response], + add_generation_prompt=False, + chat_template=chat_template_string, + tokenize=False, + ) + chosen_strip_index = result["chosen"].find(chosen_response["content"]) + result["chosen"] = result["chosen"][chosen_strip_index:].rstrip() + + result["rejected"] = tokenizer.apply_chat_template( + [dummy_user_message, rejected_response], + add_generation_prompt=False, + chat_template=chat_template_string, + tokenize=False, + ) + rejected_strip_index = result["rejected"].find(rejected_response["content"]) + result["rejected"] = result["rejected"][rejected_strip_index:].rstrip() + + return result + + return transform_fn, {"remove_columns": [field_chosen, field_rejected]} diff --git a/tests/prompt_strategies/test_dpo_chat_templates.py b/tests/prompt_strategies/test_dpo_chat_templates.py index e570cfc9db..b5c121726f 100644 --- a/tests/prompt_strategies/test_dpo_chat_templates.py +++ b/tests/prompt_strategies/test_dpo_chat_templates.py @@ -8,7 +8,7 @@ from datasets import Dataset from transformers import AutoTokenizer -from axolotl.prompt_strategies.dpo.chat_template import default +from axolotl.prompt_strategies.dpo.chat_template import argilla_chat, default from axolotl.utils.dict import DictDefault from tests.hf_offline_utils import enable_hf_offline @@ -78,6 +78,36 @@ def fixture_custom_assistant_dataset(): ) +@pytest.fixture(name="argilla_chat_dataset") +def fixture_argilla_chat_dataset(): + return Dataset.from_list( + [ + { + "chosen": [ + { + "role": "user", + "content": "hello", + }, + { + "role": "assistant", + "content": "goodbye", + }, + ], + "rejected": [ + { + "role": "user", + "content": "hello", + }, + { + "role": "assistant", + "content": "party on", + }, + ], + } + ] + ) + + @pytest.fixture(name="phi3_tokenizer") @enable_hf_offline def fixture_phi3_tokenizer(): @@ -216,5 +246,51 @@ def test_gemma_defaults(self, gemma_tokenizer, assistant_dataset): assert result["rejected"] == "party on" +class TestArgillaChatDPOChatTemplate: + """ + Test class for argilla_chat style datasets (chosen/rejected contain full conversations). + """ + + def test_llama3_argilla_chat(self, llama3_tokenizer, argilla_chat_dataset): + transform_fn, _ = argilla_chat( + DictDefault( + { + "chat_template": "llama3", + "datasets": [ + { + "type": "chat_template.argilla_chat", + } + ], + } + ) + ) + result = transform_fn(argilla_chat_dataset[0], tokenizer=llama3_tokenizer) + assert result["prompt"] == ( + "<|begin_of_text|>" + + "<|start_header_id|>user<|end_header_id|>\n\nhello<|eot_id|>" + + "<|start_header_id|>assistant<|end_header_id|>\n\n" + ) + assert result["chosen"] == "goodbye<|eot_id|>" + assert result["rejected"] == "party on<|eot_id|>" + + def test_phi3_argilla_chat(self, phi3_tokenizer, argilla_chat_dataset): + transform_fn, _ = argilla_chat( + DictDefault( + { + "chat_template": "tokenizer_default", + "datasets": [ + { + "type": "chat_template.argilla_chat", + } + ], + } + ) + ) + result = transform_fn(argilla_chat_dataset[0], tokenizer=phi3_tokenizer) + assert result["prompt"] == "<|user|>\nhello<|end|>\n" + "<|assistant|>\n" + assert result["chosen"] == "goodbye<|end|>" + assert result["rejected"] == "party on<|end|>" + + if __name__ == "__main__": unittest.main() From 8bb871b5cf0810fd4034069821250d718db366ca Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 20 Oct 2025 14:06:58 +0700 Subject: [PATCH 1008/1405] fix: deepspeed with context parallel (#3220) --- .../monkeypatch/transformers/trainer_context_parallel.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py b/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py index 74a35e83fc..ba8b16ddac 100644 --- a/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py +++ b/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py @@ -13,9 +13,7 @@ LOG = get_logger(__name__) GUARD_PATTERN = 'if model.config._attn_implementation != "sdpa":' -PATCHED_GUARD = ( - 'if model.config._attn_implementation not in ("sdpa", "flash_attention_2"):' -) +PATCHED_GUARD = 'if (attn_impl := (getattr(model.config, "_attn_implementation", None) or getattr(model.model.config, "_attn_implementation", None))) and attn_impl not in ("sdpa", "flash_attention_2"):' def patch_prepare_context_parallel_inputs() -> None: From 383f220cfd658804f4c508a0686c988861ecffbe Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 20 Oct 2025 08:53:49 -0400 Subject: [PATCH 1009/1405] build torch 2.9.0 base images (#3221) --- .github/workflows/base.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 7af6059c88..b2681bb5de 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -53,6 +53,13 @@ jobs: pytorch: 2.8.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" + - cuda: "128" + cuda_version: 12.8.1 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.9.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-base" # - cuda: "128" # cuda_version: 12.8.1 # cudnn_version: "" @@ -129,6 +136,13 @@ jobs: pytorch: 2.8.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" + - cuda: "128" + cuda_version: 12.8.1 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.9.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-uv-base" steps: - name: Checkout uses: actions/checkout@v4 From 613bcf90e58f3ab81d3827e7fc572319908db9fb Mon Sep 17 00:00:00 2001 From: Matthew Hambrecht <14303543+matthambrecht@users.noreply.github.com> Date: Wed, 22 Oct 2025 09:55:26 -0400 Subject: [PATCH 1010/1405] fix: enable_sleep_mode -> vllm_enable_sleep_mode (#3225) Co-authored-by: Matthew Hambrecht --- src/axolotl/core/trainers/grpo/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index d1a6b7fd92..bd77489ebd 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -52,7 +52,7 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: if trl.vllm_mode: grpo_args_kwargs["vllm_mode"] = trl.vllm_mode if trl.vllm_mode == "colocate": - grpo_args_kwargs["enable_sleep_mode"] = trl.vllm_enable_sleep_mode # type: ignore[attr-defined] + grpo_args_kwargs["vllm_enable_sleep_mode"] = trl.vllm_enable_sleep_mode # type: ignore[attr-defined] grpo_args_kwargs["vllm_gpu_memory_utilization"] = ( vllm_cfg.gpu_memory_utilization ) From 3750fdcf79313f5c626d9508c72ea167f7da2985 Mon Sep 17 00:00:00 2001 From: Qingyang Wu Date: Wed, 22 Oct 2025 07:22:14 -0700 Subject: [PATCH 1011/1405] Fix trainer dataloader slow loading issue (#3219) * Fix trainer dataloader handling in src/axolotl/core/trainers/base.py * update comment to reflect torch version --------- Co-authored-by: Wing Lian --- setup.py | 2 +- src/axolotl/core/trainers/base.py | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index b2eeb92d65..a93d8d49ea 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ def parse_requirements(extras_require_map): try: torch_version = version("torch") except PackageNotFoundError: - torch_version = "2.6.0" # default to torch 2.6 + torch_version = "2.8.0" # default to torch 2.8.0 _install_requires.append(f"torch=={torch_version}") version_match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", torch_version) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 11dfecb986..7d7420fb86 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -225,17 +225,6 @@ def _get_dataloader( data_collator = self.data_collator if is_training else self.eval_data_collator - if dataset.column_names and "length" in dataset.column_names: - dataset = dataset.remove_columns(["length"]) - if ( - dataset.column_names - and "position_ids" in dataset.column_names - and "attention_mask" in dataset.column_names - and self.args.sample_packing - and self.args.sample_packing_drop_attention_mask - ): - dataset = dataset.remove_columns(["attention_mask"]) - if isinstance(dataset, datasets.Dataset): if is_training: if not self.args.sample_packing or self.args.pretraining: @@ -294,6 +283,18 @@ def _get_dataloader( ): self.accelerator.even_batches = False + if dataset.column_names and "length" in dataset.column_names: + dataset = dataset.remove_columns(["length"]) + + if ( + dataset.column_names + and "position_ids" in dataset.column_names + and "attention_mask" in dataset.column_names + and self.args.sample_packing + and self.args.sample_packing_drop_attention_mask + ): + dataset = dataset.remove_columns(["attention_mask"]) + dataloader = DataLoader(dataset, **dataloader_params) # Accelerator.free_memory() will destroy the references, so From 243620394a2576db507b1f6ab033c4183a18233e Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 23 Oct 2025 05:23:20 +0700 Subject: [PATCH 1012/1405] fix: force train split for json,csv,txt for test_datasets and misc doc changes (#3226) * fix: force train split for json,csv,txt for test_datasets * feat(doc): add info on mixing datasets for VLM * feat(doc): max memory * fix(doc): clarify lr groups * fix: add info on vision not being dropped * feat: add qwen3-vl to multimodal docs * fix: add moe blocks to arch list * feat(doc): improve mistral docs * chore: add helpful link [skip-e2e] * fix: add vram usage for mistral small * Update link in docs/faq.qmd Co-authored-by: salman --------- Co-authored-by: Wing Lian Co-authored-by: salman --- docs/faq.qmd | 8 +++ docs/lr_groups.qmd | 6 +++ docs/multimodal.qmd | 14 ++++- examples/magistral/think/README.md | 2 +- examples/magistral/vision/README.md | 2 +- examples/mistral/mistral-small/README.md | 51 +++++++++++++++++++ .../mistral-small-3.1-24B-lora.yml | 2 +- src/axolotl/common/architectures.py | 2 + src/axolotl/utils/data/shared.py | 5 ++ 9 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 examples/mistral/mistral-small/README.md diff --git a/docs/faq.qmd b/docs/faq.qmd index ffc29d35df..92b432f2d2 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -63,6 +63,14 @@ description: Frequently asked questions > A: There seems to be a wheel issue with FA2 2.8.0 on CUDA 12.4. Try CUDA 12.6 instead or downgrade to FA2 2.7.4. Please refer to the upstream issue: https://github.com/Dao-AILab/flash-attention/issues/1717. +**Q: Can we mix text and text+image datasets for VLM training?** + +> A: Yes, you can for newer VLM arch. The ones that would not work are LLaVA / Pixtral arch. If you notice one not working, please let us know! + +**Q: Why is `memory/max_*` different from `nvidia-smi`?** + +> A: We use `torch` APIs to retrieve this information. You can see https://docs.pytorch.org/docs/stable/notes/cuda.html#cuda-memory-management for more information. + ### Chat templates **Q: `jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'content' / 'role' / ____`** diff --git a/docs/lr_groups.qmd b/docs/lr_groups.qmd index 52059016ca..ce53507223 100644 --- a/docs/lr_groups.qmd +++ b/docs/lr_groups.qmd @@ -27,3 +27,9 @@ learning_rate: 2e-5 In this example, we have a default learning rate of 2e-5 across the entire model, but we have a separate learning rate of 1e-6 for all the self attention `o_proj` modules across all layers, and a learning are of 1e-5 to the 3rd layer's self attention `q_proj` module. + +::: {.callout-note} + +We currently only support varying `lr` for now. If you're interested in adding support for others (`weight_decay`), we welcome PRs. See https://github.com/axolotl-ai-cloud/axolotl/blob/613bcf90e58f3ab81d3827e7fc572319908db9fb/src/axolotl/core/trainers/mixins/optimizer.py#L17 + +::: diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index 3a28b579a1..1c4e28ea72 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -56,10 +56,14 @@ image_resize_algorithm: bilinear Please see [examples](https://github.com/axolotl-ai/axolotl/tree/main/examples) folder for full configs. -::: {.callout-warning} +::: {.callout-tip} Some of our chat_templates have been extended to support broader dataset types. This should not break any existing configs. ::: +::: {.callout-note} +As of now, we do not truncate nor drop samples based on `sequence_len` as each arch has different ways to process non-text tokens. We are looking for help on this. +::: + ### Mllama {#sec-mllama} ```yaml @@ -168,6 +172,14 @@ base_model: Qwen/Qwen2.5-VL-7B-Instruct chat_template: qwen2_vl # same as qwen2-vl ``` +### Qwen3-VL {#sec-qwen3-vl} + +```yaml +base_model: Qwen/Qwen3-VL-4B-Instruct + +chat_template: qwen2_vl # same as qwen2-vl +``` + ### SmolVLM2 {#sec-smolvlm2} ::: {.callout-tip} diff --git a/examples/magistral/think/README.md b/examples/magistral/think/README.md index 29950f59e8..a875797757 100644 --- a/examples/magistral/think/README.md +++ b/examples/magistral/think/README.md @@ -12,7 +12,7 @@ Before starting, ensure you have: Run the thinking model fine-tuning: ```bash -axolotl train magistral-small-think-qlora.yaml +axolotl train examples/magistral/think/magistral-small-think-qlora.yaml ``` This config uses about 19.1 GiB VRAM. diff --git a/examples/magistral/vision/README.md b/examples/magistral/vision/README.md index 932a3631ee..fc614c8505 100644 --- a/examples/magistral/vision/README.md +++ b/examples/magistral/vision/README.md @@ -21,7 +21,7 @@ Before starting, ensure you have: 3. Run the fine-tuning: ```bash - axolotl train magistral-small-vision-24B-qlora.yml + axolotl train examples/magistral/vision/magistral-small-vision-24B-qlora.yml ``` This config uses about 17GiB VRAM. diff --git a/examples/mistral/mistral-small/README.md b/examples/mistral/mistral-small/README.md new file mode 100644 index 0000000000..3c606a897f --- /dev/null +++ b/examples/mistral/mistral-small/README.md @@ -0,0 +1,51 @@ +# Mistral Small 3.1/3.2 Fine-tuning + +This guide covers fine-tuning [Mistral Small 3.1](mistralai/Mistral-Small-3.1-24B-Instruct-2503) and [Mistral Small 3.2](mistralai/Mistral-Small-3.2-24B-Instruct-2506) with vision capabilities using Axolotl. + +## Prerequisites + +Before starting, ensure you have: +- Installed Axolotl (see [Installation docs](https://docs.axolotl.ai/docs/installation.html)) + +## Getting Started + +1. Install the required vision lib: + ```bash + pip install 'mistral-common[opencv]==1.8.5' + ``` + +2. Download the example dataset image: + ```bash + wget https://huggingface.co/datasets/Nanobit/text-vision-2k-test/resolve/main/African_elephant.jpg + ``` + +3. Run the fine-tuning: + ```bash + axolotl train examples/mistral/mistral-small/mistral-small-3.1-24B-lora.yml + ``` + +This config uses about 29.4 GiB VRAM. + +## Dataset Format + +The vision model requires multi-modal dataset format as documented [here](https://docs.axolotl.ai/docs/multimodal.html#dataset-format). + +One exception is that, passing `"image": PIL.Image` is not supported. MistralTokenizer only supports `path`, `url`, and `base64` for now. + +Example: +```json +{ + "messages": [ + {"role": "system", "content": [{ "type": "text", "text": "{SYSTEM_PROMPT}"}]}, + {"role": "user", "content": [ + { "type": "text", "text": "What's in this image?"}, + {"type": "image", "path": "path/to/image.jpg" } + ]}, + {"role": "assistant", "content": [{ "type": "text", "text": "..." }]}, + ], +} +``` + +## Limitations + +- Sample Packing is not supported for multi-modality training currently. diff --git a/examples/mistral/mistral-small/mistral-small-3.1-24B-lora.yml b/examples/mistral/mistral-small/mistral-small-3.1-24B-lora.yml index ec197f3336..d45d13ac6b 100644 --- a/examples/mistral/mistral-small/mistral-small-3.1-24B-lora.yml +++ b/examples/mistral/mistral-small/mistral-small-3.1-24B-lora.yml @@ -39,7 +39,7 @@ wandb_name: wandb_log_model: gradient_accumulation_steps: 1 -micro_batch_size: 1 +micro_batch_size: 2 num_epochs: 1 optimizer: adamw_bnb_8bit lr_scheduler: cosine diff --git a/src/axolotl/common/architectures.py b/src/axolotl/common/architectures.py index b754e56ba8..c8a2f0836c 100644 --- a/src/axolotl/common/architectures.py +++ b/src/axolotl/common/architectures.py @@ -12,7 +12,9 @@ "mixtral": "MixtralSparseMoeBlock", "qwen2_moe": "Qwen2MoeSparseMoeBlock", "qwen3_moe": "Qwen3MoeSparseMoeBlock", + "qwen3_vl_moe": "Qwen3VLMoeTextSparseMoeBlock", "deepseek_v2": "DeepseekV2MoE", + "deepseek_v3": "DeepseekV3MoE", "gpt_oss": "GptOssDecoderLayer", "lfm2_moe": "Lfm2MoeSparseMoeBlock", } diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index c9a91b8293..a8ed55ae2f 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -239,6 +239,11 @@ def _load_from_local_path( return load_dataset(dataset_config.path, **load_dataset_kwargs) elif local_path.is_file(): dataset_type = get_dataset_type(dataset_config) + + # For single file datasets, HF always creates only a "train" split + if dataset_type in ("json", "csv", "text"): + load_dataset_kwargs["split"] = "train" + return load_dataset( dataset_type, data_files=dataset_config.path, From 4dc018992dccba6fa5e239d0453cbbd565e47e96 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Thu, 23 Oct 2025 07:46:55 +0530 Subject: [PATCH 1013/1405] Feat/opentelemetry (#3215) --- examples/llama-3/opentelemetry-qlora.yml | 50 +++ setup.py | 6 + src/axolotl/core/builders/base.py | 12 +- src/axolotl/utils/__init__.py | 7 + src/axolotl/utils/callbacks/opentelemetry.py | 238 +++++++++++++ src/axolotl/utils/schemas/config.py | 2 + src/axolotl/utils/schemas/integrations.py | 24 ++ tests/test_opentelemetry_callback.py | 349 +++++++++++++++++++ 8 files changed, 687 insertions(+), 1 deletion(-) create mode 100644 examples/llama-3/opentelemetry-qlora.yml create mode 100644 src/axolotl/utils/callbacks/opentelemetry.py create mode 100644 tests/test_opentelemetry_callback.py diff --git a/examples/llama-3/opentelemetry-qlora.yml b/examples/llama-3/opentelemetry-qlora.yml new file mode 100644 index 0000000000..d8ce7b1eca --- /dev/null +++ b/examples/llama-3/opentelemetry-qlora.yml @@ -0,0 +1,50 @@ +base_model: NousResearch/Llama-3.2-1B +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer + +load_in_4bit: true + +datasets: + - path: mhenrichsen/alpaca_2k_test + type: alpaca + +output_dir: ./outputs/opentelemetry-example + +adapter: qlora +sequence_len: 512 +sample_packing: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true + +# OpenTelemetry Configuration +use_otel_metrics: true +otel_metrics_host: "localhost" +otel_metrics_port: 8000 + +# Disable WandB +use_wandb: false + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: paged_adamw_32bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +logging_steps: 1 +flash_attention: false + +warmup_ratio: 0.1 +evals_per_epoch: 2 +saves_per_epoch: 1 +weight_decay: 0.0 + +special_tokens: + pad_token: "<|end_of_text|>" diff --git a/setup.py b/setup.py index a93d8d49ea..9e3de48b53 100644 --- a/setup.py +++ b/setup.py @@ -159,6 +159,12 @@ def get_package_version(): "llmcompressor==0.5.1", ], "fbgemm-gpu": ["fbgemm-gpu-genai>=1.2.0"], + "opentelemetry": [ + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-exporter-prometheus", + "prometheus-client", + ], } install_requires, dependency_links, extras_require_build = parse_requirements( extras_require diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 8c86e335ef..2c949f8e75 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -29,7 +29,11 @@ from axolotl.integrations.base import PluginManager from axolotl.monkeypatch.trainer.lr import patch_trainer_get_lr -from axolotl.utils import is_comet_available, is_mlflow_available +from axolotl.utils import ( + is_comet_available, + is_mlflow_available, + is_opentelemetry_available, +) from axolotl.utils.callbacks import ( GCCallback, SaveAxolotlConfigtoWandBCallback, @@ -134,6 +138,12 @@ def get_callbacks(self) -> list[TrainerCallback]: callbacks.append( SaveAxolotlConfigtoCometCallback(self.cfg.axolotl_config_path) ) + if self.cfg.use_otel_metrics and is_opentelemetry_available(): + from axolotl.utils.callbacks.opentelemetry import ( + OpenTelemetryMetricsCallback, + ) + + callbacks.append(OpenTelemetryMetricsCallback(self.cfg)) if self.cfg.save_first_step: callbacks.append(SaveModelOnFirstStepCallback()) diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index 7256a57005..72f8173f35 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -17,6 +17,13 @@ def is_comet_available(): return importlib.util.find_spec("comet_ml") is not None +def is_opentelemetry_available(): + return ( + importlib.util.find_spec("opentelemetry") is not None + and importlib.util.find_spec("prometheus_client") is not None + ) + + def get_pytorch_version() -> tuple[int, int, int]: """ Get Pytorch version as a tuple of (major, minor, patch). diff --git a/src/axolotl/utils/callbacks/opentelemetry.py b/src/axolotl/utils/callbacks/opentelemetry.py new file mode 100644 index 0000000000..3f7e56b782 --- /dev/null +++ b/src/axolotl/utils/callbacks/opentelemetry.py @@ -0,0 +1,238 @@ +"""OpenTelemetry metrics callback for Axolotl training""" + +import threading +from typing import Dict, Optional + +from transformers import ( + TrainerCallback, + TrainerControl, + TrainerState, + TrainingArguments, +) + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +try: + from opentelemetry import metrics + from opentelemetry.exporter.prometheus import PrometheusMetricReader + from opentelemetry.metrics import set_meter_provider + from opentelemetry.sdk.metrics import MeterProvider as SDKMeterProvider + from prometheus_client import start_http_server + + OPENTELEMETRY_AVAILABLE = True +except ImportError: + LOG.warning("OpenTelemetry not available. pip install [opentelemetry]") + OPENTELEMETRY_AVAILABLE = False + + +class OpenTelemetryMetricsCallback(TrainerCallback): + """ + TrainerCallback that exports training metrics to OpenTelemetry/Prometheus. + + This callback automatically tracks key training metrics including: + - Training loss + - Evaluation loss + - Learning rate + - Epoch progress + - Global step count + - Gradient norm + + Metrics are exposed via HTTP endpoint for Prometheus scraping. + """ + + def __init__(self, cfg): + if not OPENTELEMETRY_AVAILABLE: + LOG.warning("OpenTelemetry not available, metrics will not be collected") + self.metrics_enabled = False + return + + self.cfg = cfg + self.metrics_host = getattr(cfg, "otel_metrics_host", "localhost") + self.metrics_port = getattr(cfg, "otel_metrics_port", 8000) + self.metrics_enabled = True + self.server_started = False + self.metrics_lock = threading.Lock() + + try: + # Create Prometheus metrics reader + prometheus_reader = PrometheusMetricReader() + + # Create meter provider with Prometheus exporter + provider = SDKMeterProvider(metric_readers=[prometheus_reader]) + set_meter_provider(provider) + + # Get meter for creating metrics + self.meter = metrics.get_meter("axolotl.training") + + # Create metrics + self._create_metrics() + + except Exception as e: + LOG.warning(f"Failed to initialize OpenTelemetry metrics: {e}") + self.metrics_enabled = False + + def _create_metrics(self): + """Create all metrics that will be tracked""" + self.train_loss_gauge = self.meter.create_gauge( + name="axolotl_train_loss", + description="Current training loss", + unit="1", + ) + + self.eval_loss_gauge = self.meter.create_gauge( + name="axolotl_eval_loss", + description="Current evaluation loss", + unit="1", + ) + + self.learning_rate_gauge = self.meter.create_gauge( + name="axolotl_learning_rate", + description="Current learning rate", + unit="1", + ) + + self.epoch_gauge = self.meter.create_gauge( + name="axolotl_epoch", + description="Current training epoch", + unit="1", + ) + + self.global_step_counter = self.meter.create_counter( + name="axolotl_global_steps", + description="Total training steps completed", + unit="1", + ) + + self.grad_norm_gauge = self.meter.create_gauge( + name="axolotl_gradient_norm", + description="Gradient norm", + unit="1", + ) + + self.memory_usage_gauge = self.meter.create_gauge( + name="axolotl_memory_usage", + description="Current memory usage in MB", + unit="MB", + ) + + def _start_metrics_server(self): + """Start the HTTP server for metrics exposure""" + if self.server_started: + return + + try: + start_http_server(self.metrics_port, addr=self.metrics_host) + self.server_started = True + LOG.info( + f"OpenTelemetry metrics server started on http://{self.metrics_host}:{self.metrics_port}/metrics" + ) + + except Exception as e: + LOG.error(f"Failed to start OpenTelemetry metrics server: {e}") + + def on_train_begin( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Called at the beginning of training""" + if not self.metrics_enabled: + return + + self._start_metrics_server() + LOG.info("OpenTelemetry metrics collection started") + + def on_log( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + logs: Optional[Dict[str, float]] = None, + **kwargs, + ): + """Called when logging occurs""" + if not self.metrics_enabled or not logs: + return + + if "loss" in logs: + self.train_loss_gauge.set(logs["loss"]) + + if "eval_loss" in logs: + self.eval_loss_gauge.set(logs["eval_loss"]) + + if "learning_rate" in logs: + self.learning_rate_gauge.set(logs["learning_rate"]) + + if "epoch" in logs: + self.epoch_gauge.set(logs["epoch"]) + + if "grad_norm" in logs: + self.grad_norm_gauge.set(logs["grad_norm"]) + if "memory_usage" in logs: + self.memory_usage_gauge.set(logs["memory_usage"]) + + def on_step_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Called at the end of each training step""" + if not self.metrics_enabled: + return + + # Update step counter and epoch + self.global_step_counter.add(1) + if state.epoch is not None: + self.epoch_gauge.set(state.epoch) + + def on_evaluate( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + metrics: Optional[Dict[str, float]] = None, + **kwargs, + ): + """Called after evaluation""" + if not self.metrics_enabled or not metrics: + return + + if "eval_loss" in metrics: + self.eval_loss_gauge.set(metrics["eval_loss"]) + + # Record any other eval metrics as gauges + for key, value in metrics.items(): + if key.startswith("eval_") and isinstance(value, (int, float)): + # Create gauge for this metric if it doesn't exist + gauge_name = f"axolotl_{key}" + try: + gauge = self.meter.create_gauge( + name=gauge_name, + description=f"Evaluation metric: {key}", + unit="1", + ) + gauge.set(value) + except Exception as e: + LOG.warning(f"Failed to create/update metric {gauge_name}: {e}") + + def on_train_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Called at the end of training""" + if not self.metrics_enabled: + return + + LOG.info("Training completed. OpenTelemetry metrics collection finished.") + LOG.info( + f"Metrics are still available at http://{self.metrics_host}:{self.metrics_port}/metrics" + ) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 4d1d0aab20..86b3aa17b4 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -30,6 +30,7 @@ GradioConfig, LISAConfig, MLFlowConfig, + OpenTelemetryConfig, RayConfig, WandbConfig, ) @@ -60,6 +61,7 @@ class AxolotlInputConfig( WandbConfig, MLFlowConfig, CometConfig, + OpenTelemetryConfig, LISAConfig, GradioConfig, RayConfig, diff --git a/src/axolotl/utils/schemas/integrations.py b/src/axolotl/utils/schemas/integrations.py index 7332c7d39c..97d6755690 100644 --- a/src/axolotl/utils/schemas/integrations.py +++ b/src/axolotl/utils/schemas/integrations.py @@ -176,3 +176,27 @@ class RayConfig(BaseModel): "help": "The resources per worker for Ray training. Default is to use 1 GPU per worker." }, ) + + +class OpenTelemetryConfig(BaseModel): + """OpenTelemetry configuration subset""" + + use_otel_metrics: bool | None = Field( + default=False, + json_schema_extra={ + "description": "Enable OpenTelemetry metrics collection and Prometheus export" + }, + ) + otel_metrics_host: str | None = Field( + default="localhost", + json_schema_extra={ + "title": "OpenTelemetry Metrics Host", + "description": "Host to bind the OpenTelemetry metrics server to", + }, + ) + otel_metrics_port: int | None = Field( + default=8000, + json_schema_extra={ + "description": "Port for the Prometheus metrics HTTP server" + }, + ) diff --git a/tests/test_opentelemetry_callback.py b/tests/test_opentelemetry_callback.py new file mode 100644 index 0000000000..294ff65857 --- /dev/null +++ b/tests/test_opentelemetry_callback.py @@ -0,0 +1,349 @@ +"""Tests for OpenTelemetry metrics callback functionality.""" + +import time + +import pytest + +from axolotl.utils.dict import DictDefault + + +@pytest.fixture +def mock_otel_config(): + """Mock configuration for OpenTelemetry callback.""" + return DictDefault( + { + "use_otel_metrics": True, + "otel_metrics_host": "localhost", + "otel_metrics_port": 8003, # Use unique port for tests + } + ) + + +@pytest.fixture +def mock_trainer_state(): + """Mock trainer state for callback testing.""" + from transformers import TrainerState + + state = TrainerState() + state.epoch = 1.0 + state.global_step = 100 + return state + + +@pytest.fixture +def mock_training_args(): + """Mock training arguments for callback testing.""" + from transformers import TrainingArguments + + return TrainingArguments(output_dir="/tmp/test") + + +@pytest.fixture +def mock_trainer_control(): + """Mock trainer control for callback testing.""" + from transformers.trainer_callback import TrainerControl + + return TrainerControl() + + +class TestOpenTelemetryConfig: + """Test OpenTelemetry configuration schema.""" + + def test_config_schema_valid(self): + """Test OpenTelemetry configuration schema validation.""" + from axolotl.utils.schemas.integrations import OpenTelemetryConfig + + # Test valid config + valid_config = { + "use_otel_metrics": True, + "otel_metrics_host": "localhost", + "otel_metrics_port": 8000, + } + + otel_config = OpenTelemetryConfig(**valid_config) + assert otel_config.use_otel_metrics is True + assert otel_config.otel_metrics_host == "localhost" + assert otel_config.otel_metrics_port == 8000 + + def test_config_defaults(self): + """Test OpenTelemetry configuration default values.""" + from axolotl.utils.schemas.integrations import OpenTelemetryConfig + + # Test minimal config with defaults + minimal_config = {"use_otel_metrics": True} + + otel_config = OpenTelemetryConfig(**minimal_config) + assert otel_config.use_otel_metrics is True + assert otel_config.otel_metrics_host == "localhost" # default + assert otel_config.otel_metrics_port == 8000 # default + + def test_config_disabled_by_default(self): + """Test that OpenTelemetry is disabled by default.""" + from axolotl.utils.schemas.integrations import OpenTelemetryConfig + + # Test default config + default_config = OpenTelemetryConfig() + assert default_config.use_otel_metrics is False + + +class TestOpenTelemetryCallback: + """Test OpenTelemetry callback functionality.""" + + def test_callback_import(self): + """Test that OpenTelemetry callback can be imported.""" + from axolotl.utils.callbacks.opentelemetry import OpenTelemetryMetricsCallback + + assert OpenTelemetryMetricsCallback is not None + + def test_callback_graceful_fallback(self, mock_otel_config): + """Test callback gracefully handles missing dependencies.""" + from axolotl.utils.callbacks.opentelemetry import OpenTelemetryMetricsCallback + + # This should not raise an exception even if dependencies are missing + callback = OpenTelemetryMetricsCallback(mock_otel_config) + + # Callback should exist but may have metrics disabled + assert callback is not None + assert hasattr(callback, "metrics_enabled") + + def test_callback_initialization_enabled(self, mock_otel_config): + """Test callback initialization when OpenTelemetry is available.""" + from axolotl.utils.callbacks.opentelemetry import ( + OPENTELEMETRY_AVAILABLE, + OpenTelemetryMetricsCallback, + ) + + callback = OpenTelemetryMetricsCallback(mock_otel_config) + + if OPENTELEMETRY_AVAILABLE: + assert callback.metrics_enabled is True + assert callback.cfg == mock_otel_config + assert callback.metrics_host == "localhost" + assert callback.metrics_port == 8003 + else: + assert callback.metrics_enabled is False + + def test_metrics_server_lifecycle( + self, + mock_otel_config, + mock_trainer_state, + mock_training_args, + mock_trainer_control, + ): + """Test metrics server starts and stops correctly.""" + from axolotl.utils.callbacks.opentelemetry import ( + OPENTELEMETRY_AVAILABLE, + OpenTelemetryMetricsCallback, + ) + + if not OPENTELEMETRY_AVAILABLE: + pytest.skip("OpenTelemetry dependencies not available") + + callback = OpenTelemetryMetricsCallback(mock_otel_config) + + # Start server + callback.on_train_begin( + mock_training_args, mock_trainer_state, mock_trainer_control + ) + assert callback.server_started is True + + # End training + callback.on_train_end( + mock_training_args, mock_trainer_state, mock_trainer_control + ) + + def test_metrics_recording( + self, + mock_otel_config, + mock_trainer_state, + mock_training_args, + mock_trainer_control, + ): + """Test that metrics are recorded during training.""" + from axolotl.utils.callbacks.opentelemetry import ( + OPENTELEMETRY_AVAILABLE, + OpenTelemetryMetricsCallback, + ) + + if not OPENTELEMETRY_AVAILABLE: + pytest.skip("OpenTelemetry dependencies not available") + + callback = OpenTelemetryMetricsCallback(mock_otel_config) + callback.on_train_begin( + mock_training_args, mock_trainer_state, mock_trainer_control + ) + + # Test logging metrics + test_logs = { + "loss": 0.5, + "learning_rate": 1e-4, + "grad_norm": 0.8, + } + + # This should not raise an exception + callback.on_log( + mock_training_args, mock_trainer_state, mock_trainer_control, logs=test_logs + ) + assert callback.metrics_enabled is True + + def test_evaluation_metrics( + self, + mock_otel_config, + mock_trainer_state, + mock_training_args, + mock_trainer_control, + ): + """Test evaluation metrics recording.""" + from axolotl.utils.callbacks.opentelemetry import ( + OPENTELEMETRY_AVAILABLE, + OpenTelemetryMetricsCallback, + ) + + if not OPENTELEMETRY_AVAILABLE: + pytest.skip("OpenTelemetry dependencies not available") + + callback = OpenTelemetryMetricsCallback(mock_otel_config) + callback.on_train_begin( + mock_training_args, mock_trainer_state, mock_trainer_control + ) + + # Test evaluation metrics + eval_logs = { + "eval_loss": 0.3, + "eval_accuracy": 0.95, + } + + # This should not raise an exception + callback.on_evaluate( + mock_training_args, mock_trainer_state, mock_trainer_control, eval_logs + ) + assert callback.metrics_enabled is True + + def test_thread_safety(self, mock_otel_config): + """Test that callback has thread safety mechanisms.""" + from axolotl.utils.callbacks.opentelemetry import ( + OPENTELEMETRY_AVAILABLE, + OpenTelemetryMetricsCallback, + ) + + if not OPENTELEMETRY_AVAILABLE: + pytest.skip("OpenTelemetry dependencies not available") + + callback = OpenTelemetryMetricsCallback(mock_otel_config) + assert hasattr(callback, "metrics_lock") + # Check it's a lock-like object + assert hasattr(callback.metrics_lock, "__enter__") + assert hasattr(callback.metrics_lock, "__exit__") + + +class TestOpenTelemetryIntegration: + """Integration tests for OpenTelemetry.""" + + def test_availability_check(self): + """Test availability check function.""" + from axolotl.utils import is_opentelemetry_available + + result = is_opentelemetry_available() + assert isinstance(result, bool) + + def test_prometheus_endpoint_basic( + self, + mock_otel_config, + mock_trainer_state, + mock_training_args, + mock_trainer_control, + ): + """Test basic Prometheus endpoint functionality.""" + from axolotl.utils.callbacks.opentelemetry import ( + OPENTELEMETRY_AVAILABLE, + OpenTelemetryMetricsCallback, + ) + + if not OPENTELEMETRY_AVAILABLE: + pytest.skip("OpenTelemetry dependencies not available") + + try: + import requests + except ImportError: + pytest.skip("requests library not available") + + callback = OpenTelemetryMetricsCallback(mock_otel_config) + callback.on_train_begin( + mock_training_args, mock_trainer_state, mock_trainer_control + ) + + if not callback.server_started: + pytest.skip("Metrics server failed to start") + + # Give server time to start + time.sleep(1) + + # Try to access metrics endpoint + try: + response = requests.get( + f"http://{callback.metrics_host}:{callback.metrics_port}/metrics", + timeout=2, + ) + assert response.status_code == 200 + # Check for Prometheus format + assert "# TYPE" in response.text or "# HELP" in response.text + except requests.exceptions.RequestException: + pytest.skip( + "Could not connect to metrics endpoint - this is expected in some environments" + ) + + +class TestOpenTelemetryCallbackMethods: + """Test specific callback methods.""" + + def test_step_end_callback( + self, + mock_otel_config, + mock_trainer_state, + mock_training_args, + mock_trainer_control, + ): + """Test step end callback method.""" + from axolotl.utils.callbacks.opentelemetry import ( + OPENTELEMETRY_AVAILABLE, + OpenTelemetryMetricsCallback, + ) + + if not OPENTELEMETRY_AVAILABLE: + pytest.skip("OpenTelemetry dependencies not available") + + callback = OpenTelemetryMetricsCallback(mock_otel_config) + callback.on_train_begin( + mock_training_args, mock_trainer_state, mock_trainer_control + ) + + # Should not raise an exception + callback.on_step_end( + mock_training_args, mock_trainer_state, mock_trainer_control + ) + + def test_epoch_end_callback( + self, + mock_otel_config, + mock_trainer_state, + mock_training_args, + mock_trainer_control, + ): + """Test epoch end callback method.""" + from axolotl.utils.callbacks.opentelemetry import ( + OPENTELEMETRY_AVAILABLE, + OpenTelemetryMetricsCallback, + ) + + if not OPENTELEMETRY_AVAILABLE: + pytest.skip("OpenTelemetry dependencies not available") + + callback = OpenTelemetryMetricsCallback(mock_otel_config) + callback.on_train_begin( + mock_training_args, mock_trainer_state, mock_trainer_control + ) + + # Should not raise an exception + callback.on_epoch_end( + mock_training_args, mock_trainer_state, mock_trainer_control + ) From bb33fda44d8cc889230698539b8df5a7ba114b67 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 22 Oct 2025 21:24:52 -0700 Subject: [PATCH 1014/1405] install flash attention in 2.9.0 base images (#3224) --- docker/Dockerfile-base | 6 ++++-- docker/Dockerfile-uv-base | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 87918cc414..cc209f3048 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -47,6 +47,8 @@ RUN git lfs install --skip-repo && \ pip3 install -U --no-cache-dir pydantic==1.10.10 && \ pip3 cache purge -RUN if [ "$PYTORCH_VERSION" = "2.6.0" ] && [ "$CUDA" = "124" ] ; then \ - FLASH_ATTENTION_FORCE_BUILD="TRUE" pip3 install --no-build-isolation flash-attn==2.8.0.post2; \ +RUN if [ "$PYTORCH_VERSION" = "2.9.0" ] && [ "$CUDA" = "128" ] ; then \ + wget https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.4.17/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + pip3 install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ fi diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index eaa49b9e95..2ca272c6ee 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -34,3 +34,9 @@ RUN uv pip install packaging setuptools wheel psutil \ && uv pip install --no-build-isolation "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" \ && uv pip install "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" \ && uv pip install awscli pydantic + +RUN if [ "$PYTORCH_VERSION" = "2.9.0" ] && [ "$CUDA" = "128" ] ; then \ + wget https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.4.17/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + uv pip install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + fi From 9d4d39e939b3e44298f0c5e1f1b05c7b515fc7a6 Mon Sep 17 00:00:00 2001 From: Dan Saunders Date: Mon, 27 Oct 2025 03:42:01 -0400 Subject: [PATCH 1015/1405] Diffusion trainer fix: shift logits to align with input tokens (#3191) * shift logits for diffusion generate * delete unused * diffusion trainer: token shift --- src/axolotl/integrations/diffusion/generation.py | 4 ++-- src/axolotl/integrations/diffusion/trainer.py | 4 ++-- src/axolotl/integrations/diffusion/utils.py | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/axolotl/integrations/diffusion/generation.py b/src/axolotl/integrations/diffusion/generation.py index 49e3cdfae8..ec517fd238 100644 --- a/src/axolotl/integrations/diffusion/generation.py +++ b/src/axolotl/integrations/diffusion/generation.py @@ -7,7 +7,7 @@ from axolotl.utils.logging import get_logger -from .utils import create_bidirectional_attention_mask +from .utils import create_bidirectional_attention_mask, shift_logits_to_input_positions LOG = get_logger(__name__) @@ -360,7 +360,7 @@ def _diffusion_step( # Forward pass outputs = model(input_ids=sequence, attention_mask=attention_mask) - logits = outputs.logits + logits = shift_logits_to_input_positions(outputs.logits) # Only sample at currently masked positions if current_mask.any(): diff --git a/src/axolotl/integrations/diffusion/trainer.py b/src/axolotl/integrations/diffusion/trainer.py index 42b2468f41..dfaef2a48c 100644 --- a/src/axolotl/integrations/diffusion/trainer.py +++ b/src/axolotl/integrations/diffusion/trainer.py @@ -11,7 +11,7 @@ from axolotl.utils.logging import get_logger from .callbacks import DiffusionGenerationCallback -from .utils import create_bidirectional_attention_mask +from .utils import create_bidirectional_attention_mask, shift_logits_to_input_positions LOG = get_logger(__name__) @@ -207,7 +207,7 @@ def _compute_diffusion_loss( input_ids=noisy_batch.long(), attention_mask=bidirectional_mask, ) - logits = outputs.logits + logits = shift_logits_to_input_positions(outputs.logits) if masked_indices.sum() > 0: valid_indices = torch.where(masked_indices) diff --git a/src/axolotl/integrations/diffusion/utils.py b/src/axolotl/integrations/diffusion/utils.py index 47abf6fecb..b6f71c07b9 100644 --- a/src/axolotl/integrations/diffusion/utils.py +++ b/src/axolotl/integrations/diffusion/utils.py @@ -157,3 +157,10 @@ def create_bidirectional_attention_mask( # Add head dimension: [batch_size, 1, seq_len, seq_len] return bidirectional_mask.unsqueeze(1) + + +def shift_logits_to_input_positions(logits: torch.Tensor) -> torch.Tensor: + """Align next-token logits with their input token positions for diffusion.""" + if logits.size(1) <= 1: + return logits + return torch.cat([logits[:, :1], logits[:, :-1]], dim=1) From 98333e639a35bd36a108786a6daaa42f03488aca Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 29 Oct 2025 18:02:16 -0400 Subject: [PATCH 1016/1405] upgrade trl to 0.24.0 and liger to 0.6.3 (#3230) * upgrade trl to 0.24.0 * fix reward collator init * use newer DataCollatorForPreference instead * DataCollatorForPreference doesn't use padding kwarg * fix input id labels * fix fbgemm-gpu version for pytorch versions * tweak pinned deps * transformers doesn't support hub 1.0 yet * upgrade liger dep to 0.6.3 * set TORCH_CUDA_ARCH_LIST correctly --- cicd/Dockerfile.jinja | 2 +- requirements.txt | 12 ++++++------ setup.py | 8 ++++++-- src/axolotl/core/builders/causal.py | 9 ++++++--- .../prompt_strategies/bradley_terry/chat_template.py | 4 ++-- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 6a1ddb66d9..c3a613ecc1 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -1,6 +1,6 @@ FROM axolotlai/axolotl-base:{{ BASE_TAG }} -ENV TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6+PTX" +ENV TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" ENV AXOLOTL_EXTRAS="{{ AXOLOTL_EXTRAS }}" ENV AXOLOTL_ARGS="{{ AXOLOTL_ARGS }}" ENV CUDA="{{ CUDA }}" diff --git a/requirements.txt b/requirements.txt index e1f1b10a52..5621d94b10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,27 +5,27 @@ bitsandbytes==0.47.0 triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 -liger-kernel==0.6.1 +liger-kernel==0.6.3 # END section packaging==23.2 -huggingface_hub>=0.33.0 +huggingface_hub>=0.36.0 peft>=0.17.1 tokenizers>=0.21.1 transformers==4.57.1 accelerate==1.10.1 datasets==4.0.0 deepspeed>=0.17.0 -trl==0.23.1 -hf_xet==1.1.5 -kernels==0.9.0 +trl==0.24.0 +hf_xet==1.2.0 +kernels>=0.9.0 trackio optimum==1.16.2 hf_transfer sentencepiece -gradio==5.41.1 +gradio==5.49.1 modal==1.0.2 pydantic==2.10.6 diff --git a/setup.py b/setup.py index 9e3de48b53..2845bb1514 100644 --- a/setup.py +++ b/setup.py @@ -62,8 +62,12 @@ def parse_requirements(extras_require_map): else: raise ValueError("Invalid version format") - if (major, minor) >= (2, 8): - pass + if (major, minor) >= (2, 9): + extras_require_map.pop("fbgemm-gpu") + extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.4.1"] + elif (major, minor) >= (2, 8): + extras_require_map.pop("fbgemm-gpu") + extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.3.0"] elif (major, minor) >= (2, 7): _install_requires.pop(_install_requires.index(xformers_version)) if patch == 0: diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 8203042305..7a06431dc0 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -12,7 +12,7 @@ EarlyStoppingCallback, Trainer, ) -from trl.trainer.utils import RewardDataCollatorWithPadding +from trl.trainer.reward_trainer import DataCollatorForPreference from axolotl.core.builders.base import TrainerBuilderBase from axolotl.core.trainers import ( @@ -453,7 +453,7 @@ def build_collator( BatchSamplerDataCollatorForSeq2Seq, DataCollatorForSeq2Seq, DataCollatorWithFlattening, - RewardDataCollatorWithPadding, + DataCollatorForPreference, ] ] collator_args = [self.tokenizer] @@ -470,7 +470,10 @@ def build_collator( if kwargs and isinstance(kwargs, dict): kwargs.update(collator_cls_and_kwargs[1]) elif self.cfg.reward_model: - collator = RewardDataCollatorWithPadding + collator = DataCollatorForPreference + tokenizer = collator_args.pop(0) + kwargs["pad_token_id"] = tokenizer.pad_token_id + kwargs.pop("padding") elif use_batch_sampler_collator: # Use V2BatchSamplerDataCollatorForSeq2Seq for flex attention, # supported multipack models, or non-flash-attention llama diff --git a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py index fd0d76f511..03336b3ef9 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py +++ b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py @@ -71,10 +71,10 @@ def _tokenize_single_prompt(self, prompt): ] return { - "input_ids_chosen": chosen_tokenized["input_ids"], + "chosen_input_ids": chosen_tokenized["input_ids"], "attention_mask_chosen": chosen_tokenized["attention_mask"], "labels_chosen": 1.0, - "input_ids_rejected": rejected_tokenized["input_ids"], + "rejected_input_ids": rejected_tokenized["input_ids"], "attention_mask_rejected": rejected_tokenized["attention_mask"], "labels_rejected": 0.0, } From a4b921135b56abad32f962009686b52089b273c9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 29 Oct 2025 18:07:29 -0400 Subject: [PATCH 1017/1405] build cuda 13.0.0 base image with 2.9.0 (#3229) * build cuda 13.0.0 base image with 2.9.0 * upgrade causal-conv1d * 1.5.4 not in pypi yet * pin to 1.3.0 * use github release instead of pypi * split the logic for incompatible packages * fix bash in dockerfile --- .github/workflows/base.yml | 14 ++++++++++++++ docker/Dockerfile-base | 8 ++++++-- setup.py | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index b2681bb5de..87d6772dd9 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -60,6 +60,13 @@ jobs: pytorch: 2.9.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" + - cuda: "130" + cuda_version: 13.0.0 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.9.0 + torch_cuda_arch_list: "9.0+PTX" + dockerfile: "Dockerfile-base" # - cuda: "128" # cuda_version: 12.8.1 # cudnn_version: "" @@ -143,6 +150,13 @@ jobs: pytorch: 2.9.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" + - cuda: "130" + cuda_version: 13.0.0 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.9.0 + torch_cuda_arch_list: "9.0+PTX" + dockerfile: "Dockerfile-uv-base" steps: - name: Checkout uses: actions/checkout@v4 diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index cc209f3048..a08b5cd4f2 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -37,10 +37,14 @@ WORKDIR /workspace RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==23.2 setuptools==75.8.0 wheel && \ python3 -m pip install --no-cache-dir -U torch==${PYTORCH_VERSION}+cu${CUDA} torchvision --extra-index-url https://download.pytorch.org/whl/cu$CUDA && \ - CAUSAL_CONV1D_FORCE_CXX11_ABI=TRUE CAUSAL_CONV1D_FORCE_BUILD=TRUE python3 -m pip install --no-cache-dir causal_conv1d==1.5.2 && \ - python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" && \ python3 -m pip cache purge +RUN if [ "$CUDA" != "130" ] ; then \ + CAUSAL_CONV1D_FORCE_CXX11_ABI=TRUE CAUSAL_CONV1D_FORCE_BUILD=TRUE python3 -m pip install --no-cache-dir "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@v1.5.4"; \ + python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main"; \ + python3 -m pip cache purge; \ + fi + RUN git lfs install --skip-repo && \ pip3 install awscli && \ # The base image ships with `pydantic==1.8.2` which is not working diff --git a/setup.py b/setup.py index 2845bb1514..b16377e929 100644 --- a/setup.py +++ b/setup.py @@ -162,7 +162,7 @@ def get_package_version(): "llmcompressor": [ "llmcompressor==0.5.1", ], - "fbgemm-gpu": ["fbgemm-gpu-genai>=1.2.0"], + "fbgemm-gpu": ["fbgemm-gpu-genai==1.3.0"], "opentelemetry": [ "opentelemetry-api", "opentelemetry-sdk", From 0f7c886b7b28a0a90a8510c58f160f6ee70e9851 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:09:46 -0400 Subject: [PATCH 1018/1405] chore: update pre-commit hooks (#3222) [skip ci] Co-authored-by: djsaunde <1245942+djsaunde@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e455f52c8..015fb5e6ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: no-commit-to-branch args: ['--branch', 'main'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.0 + rev: v0.14.2 hooks: - id: ruff args: [--fix] From 4b1b4fa6d86246cfef1e8b693c011bab2d7db7dd Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 30 Oct 2025 10:03:24 -0400 Subject: [PATCH 1019/1405] upgrade numpy (#3236) * upgrade numpy to 2.3.4 * bump contribs for numpy * fix vllm versions * bump numba * make sure psutil is installed * add psutil to cicd dockerfile jinja * lower dep versions of numba + numpy for vllm * bump datasets version * resolve pydantic conflict too --- .github/workflows/tests.yml | 2 +- cicd/Dockerfile.jinja | 2 +- docker/Dockerfile-base | 2 +- requirements.txt | 12 ++++++------ setup.py | 4 +++- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f368b5170..90bf3234af 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -152,7 +152,7 @@ jobs: - name: upgrade pip run: | pip3 install --upgrade pip - pip3 install --upgrade packaging==23.2 setuptools==75.8.0 setuptools_scm build wheel + pip3 install --upgrade packaging==23.2 setuptools==75.8.0 setuptools_scm build wheel psutil - name: Install PyTorch run: | diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index c3a613ecc1..81ed5453e3 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -32,7 +32,7 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt; \ fi -RUN pip install packaging==23.2 setuptools==75.8.0 +RUN pip install packaging==23.2 setuptools==75.8.0 psutil RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index a08b5cd4f2..25eae4fdea 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -35,7 +35,7 @@ ENV PATH="/root/miniconda3/envs/py${PYTHON_VERSION}/bin:${PATH}" WORKDIR /workspace -RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==23.2 setuptools==75.8.0 wheel && \ +RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==23.2 setuptools==75.8.0 wheel psutil && \ python3 -m pip install --no-cache-dir -U torch==${PYTORCH_VERSION}+cu${CUDA} torchvision --extra-index-url https://download.pytorch.org/whl/cu$CUDA && \ python3 -m pip cache purge diff --git a/requirements.txt b/requirements.txt index 5621d94b10..4d27ee148b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ peft>=0.17.1 tokenizers>=0.21.1 transformers==4.57.1 accelerate==1.10.1 -datasets==4.0.0 +datasets==4.3.0 deepspeed>=0.17.0 trl==0.24.0 hf_xet==1.2.0 @@ -28,7 +28,7 @@ sentencepiece gradio==5.49.1 modal==1.0.2 -pydantic==2.10.6 +pydantic>=2.10.6 addict fire PyYAML>=6.0 @@ -36,8 +36,8 @@ requests wandb einops colorama -numba -numpy>=1.24.4,<=2.0.1 +numba>=0.61.2 +numpy>=2.2.6 # qlora things evaluate==0.4.1 @@ -50,7 +50,7 @@ python-dotenv==1.0.1 # remote filesystems s3fs>=2024.5.0 -gcsfs>=2024.5.0 +gcsfs>=2025.3.0 adlfs>=2024.5.0 ocifs==1.3.2 @@ -66,7 +66,7 @@ antlr4-python3-runtime==4.13.2 torchao==0.13.0 schedulefree==1.4.1 -axolotl-contribs-lgpl==0.0.6 +axolotl-contribs-lgpl==0.0.7 axolotl-contribs-mit==0.0.5 mistral-common==1.8.5 diff --git a/setup.py b/setup.py index b16377e929..b046a2fdc4 100644 --- a/setup.py +++ b/setup.py @@ -65,9 +65,11 @@ def parse_requirements(extras_require_map): if (major, minor) >= (2, 9): extras_require_map.pop("fbgemm-gpu") extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.4.1"] + extras_require_map["vllm"] = ["vllm==0.11.1"] elif (major, minor) >= (2, 8): extras_require_map.pop("fbgemm-gpu") extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.3.0"] + extras_require_map["vllm"] = ["vllm==0.11.0"] elif (major, minor) >= (2, 7): _install_requires.pop(_install_requires.index(xformers_version)) if patch == 0: @@ -76,7 +78,7 @@ def parse_requirements(extras_require_map): extras_require_map.pop("vllm") else: _install_requires.append("xformers==0.0.31") - extras_require_map["vllm"] = ["vllm>=0.10.0"] + extras_require_map["vllm"] = ["vllm==0.10.1"] elif (major, minor) >= (2, 6): _install_requires.pop(_install_requires.index(xformers_version)) _install_requires.append("xformers==0.0.29.post3") From 633afffacb21a9d34d0e2d5af1ed12e1802611ca Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 30 Oct 2025 18:50:26 -0400 Subject: [PATCH 1020/1405] add torch 2.9.0 to ci (#3223) --- .github/workflows/multi-gpu-e2e.yml | 7 +++++++ .github/workflows/tests.yml | 32 +++++++++++++++++------------ setup.py | 1 + 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 6a92de352c..1682beb31a 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -40,6 +40,13 @@ jobs: axolotl_extras: fbgemm-gpu num_gpus: 2 nightly_build: "true" + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.9.0 + axolotl_extras: fbgemm-gpu + num_gpus: 2 + nightly_build: "true" runs-on: [self-hosted, modal] timeout-minutes: 120 steps: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 90bf3234af..7ad9d1ab4d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.7.1", "2.8.0"] + pytorch_version: ["2.7.1", "2.8.0", "2.9.0"] timeout-minutes: 20 steps: @@ -130,7 +130,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.7.1", "2.8.0"] + pytorch_version: ["2.7.1", "2.8.0", "2.9.0"] timeout-minutes: 20 steps: @@ -231,16 +231,10 @@ jobs: fail-fast: false matrix: include: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.7.1 - num_gpus: 1 - axolotl_extras: - - cuda: 126 - cuda_version: 12.6.3 + - cuda: 128 + cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.1 + pytorch: 2.8.0 num_gpus: 1 axolotl_extras: dockerfile: "Dockerfile-uv.jinja" @@ -286,12 +280,18 @@ jobs: fail-fast: false matrix: include: - - cuda: 128 - cuda_version: 12.8.1 + - cuda: 126 + cuda_version: 12.6.3 python_version: "3.11" pytorch: 2.7.1 num_gpus: 1 axolotl_extras: +# - cuda: 128 +# cuda_version: 12.8.1 +# python_version: "3.11" +# pytorch: 2.7.1 +# num_gpus: 1 +# axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" @@ -299,6 +299,12 @@ jobs: num_gpus: 1 gpu_type: "B200" axolotl_extras: fbgemm-gpu + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.9.0 + num_gpus: 1 + axolotl_extras: steps: - name: Checkout uses: actions/checkout@v4 diff --git a/setup.py b/setup.py index b046a2fdc4..9c11616422 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,7 @@ def parse_requirements(extras_require_map): extras_require_map.pop("fbgemm-gpu") extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.4.1"] extras_require_map["vllm"] = ["vllm==0.11.1"] + _install_requires.pop(_install_requires.index(xformers_version)) elif (major, minor) >= (2, 8): extras_require_map.pop("fbgemm-gpu") extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.3.0"] From ed58fa8a75e4eb8c5baf62e51e30fdc24c08a778 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:55:40 +0000 Subject: [PATCH 1021/1405] chore: update pre-commit hooks (#3244) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 015fb5e6ee..86d8927d21 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: no-commit-to-branch args: ['--branch', 'main'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.2 + rev: v0.14.3 hooks: - id: ruff args: [--fix] From 26f05b6008195f29d859d8035bce9b0a7f7b2777 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 4 Nov 2025 07:35:07 +0700 Subject: [PATCH 1022/1405] fix(example): set model_type to load for gemma3 text (#3242) * fix: set model_type to load for gemma3 text * chore: simplify * chore: unify --- examples/gemma3/gemma-3-1b-qlora.yml | 6 +++--- examples/gemma3/gemma-3-270m-qlora.yml | 6 +++--- examples/gemma3/gemma-3-4b-qlora.yml | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/examples/gemma3/gemma-3-1b-qlora.yml b/examples/gemma3/gemma-3-1b-qlora.yml index 115717db7e..2f998d144e 100644 --- a/examples/gemma3/gemma-3-1b-qlora.yml +++ b/examples/gemma3/gemma-3-1b-qlora.yml @@ -1,7 +1,7 @@ base_model: google/gemma-3-1b-it -# optionally might have model_type or tokenizer_type -model_type: AutoModelForCausalLM -tokenizer_type: AutoTokenizer + +model_type: Gemma3ForCausalLM + # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name diff --git a/examples/gemma3/gemma-3-270m-qlora.yml b/examples/gemma3/gemma-3-270m-qlora.yml index 8744fad269..0c60c4a014 100644 --- a/examples/gemma3/gemma-3-270m-qlora.yml +++ b/examples/gemma3/gemma-3-270m-qlora.yml @@ -1,7 +1,7 @@ base_model: google/gemma-3-270m-it -# optionally might have model_type or tokenizer_type -model_type: AutoModelForCausalLM -tokenizer_type: AutoTokenizer + +model_type: Gemma3ForCausalLM + # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml index 44ba9c8799..9595211497 100644 --- a/examples/gemma3/gemma-3-4b-qlora.yml +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -1,5 +1,8 @@ base_model: google/gemma-3-4b-it +# Need to set else transformers tries to load vision too +model_type: Gemma3ForCausalLM + load_in_4bit: true # gemma3 doesn't seem to play nice with ddp From 01a346d86ac54b4631c3e2dcb04a3855b3db757c Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 4 Nov 2025 07:39:21 +0700 Subject: [PATCH 1023/1405] feat(example): add gpt-oss-safeguard docs (#3243) * feat(example): add gpt-oss-safeguard docs * fix: add doc on reasoning_effort --- examples/gpt-oss/README.md | 12 ++++ ...-oss-safeguard-20b-sft-lora-singlegpu.yaml | 67 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 examples/gpt-oss/gpt-oss-safeguard-20b-sft-lora-singlegpu.yaml diff --git a/examples/gpt-oss/README.md b/examples/gpt-oss/README.md index fb6c67498a..9ab02b122b 100644 --- a/examples/gpt-oss/README.md +++ b/examples/gpt-oss/README.md @@ -2,6 +2,8 @@ [GPT-OSS](https://huggingface.co/collections/openai/gpt-oss-68911959590a1634ba11c7a4) are a family of open-weight MoE models trained by OpenAI, released in August 2025. There are two variants: 20B and 120B. +In October 2025, OpenAI released safeguard models built upon GPT-OSS called [GPT-OSS-Safeguard](https://huggingface.co/collections/openai/gpt-oss-safeguard). They use the same architecture, so the same examples below can be re-used. + This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. ## Getting started @@ -64,6 +66,16 @@ axolotl merge-sharded-fsdp-weights examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offlo mv ./outputs/gpt-oss-out/merged/* ./outputs/gpt-oss-out/ ``` +### How to set reasoning_effort in template? + +The harmony template has a feature to set the `reasoning_effort` during prompt building. The default is `medium`. If you would like to adjust this, you can add the following to your config: + +```yaml +chat_template_kwargs: + reasoning_effort: "high" # low | medium | high +``` + +Currently, this applies globally. There is no method to apply per sample yet. If you are interested in adding this, please feel free to create an Issue to discuss. ### Inferencing your fine-tuned model diff --git a/examples/gpt-oss/gpt-oss-safeguard-20b-sft-lora-singlegpu.yaml b/examples/gpt-oss/gpt-oss-safeguard-20b-sft-lora-singlegpu.yaml new file mode 100644 index 0000000000..ab026337d7 --- /dev/null +++ b/examples/gpt-oss/gpt-oss-safeguard-20b-sft-lora-singlegpu.yaml @@ -0,0 +1,67 @@ +base_model: openai/gpt-oss-safeguard-20b +use_kernels: true +model_quantization_config: Mxfp4Config +model_quantization_config_kwargs: + dequantize: true + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +experimental_skip_move_to_device: true # prevent OOM by not putting model to GPU before sharding + +datasets: + - path: HuggingFaceH4/Multilingual-Thinking + type: chat_template + field_thinking: thinking + template_thinking_key: thinking + +dataset_prepared_path: last_run_prepared +val_set_size: 0 +output_dir: ./outputs/gpt-oss-safeguard-out/ + +sequence_len: 4096 +sample_packing: true + +adapter: lora +lora_r: 8 +lora_alpha: 16 +lora_dropout: 0.0 # dropout not supported when using LoRA over expert parameters +lora_target_linear: true + +# TODO: not supported for now, see peft#2710 +#lora_target_parameters: # target the experts in the last two layers +# - "22._checkpoint_wrapped_module.mlp.experts.gate_up_proj" +# - "22._checkpoint_wrapped_module.mlp.experts.down_proj" +# - "23._checkpoint_wrapped_module.mlp.experts.gate_up_proj" +# - "23._checkpoint_wrapped_module.mlp.experts.down_proj" + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 8 +micro_batch_size: 1 +num_epochs: 1 + +optimizer: adamw_torch_8bit +lr_scheduler: constant_with_warmup +learning_rate: 2e-4 + +bf16: true +tf32: true + +flash_attention: true +attn_implementation: kernels-community/vllm-flash-attn3 # this is not needed if using flash_attn >= 2.8.3 + +gradient_checkpointing: true +activation_offloading: true + +logging_steps: 1 +saves_per_epoch: 1 +warmup_ratio: 0.1 + +special_tokens: +eot_tokens: + - "<|end|>" From c37decb073dcfa3538f96bf8a9f689ca5b76befd Mon Sep 17 00:00:00 2001 From: salman Date: Tue, 4 Nov 2025 13:43:40 +0000 Subject: [PATCH 1024/1405] update pre-commit cadence (#3245) --- .github/workflows/precommit-autoupdate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/precommit-autoupdate.yml b/.github/workflows/precommit-autoupdate.yml index 10330f9555..4c2e59b6bb 100644 --- a/.github/workflows/precommit-autoupdate.yml +++ b/.github/workflows/precommit-autoupdate.yml @@ -2,7 +2,7 @@ name: Pre-commit auto-update on: schedule: - - cron: '0 0 * * 0' # Run weekly + - cron: '0 0 1 * *' # Run monthly workflow_dispatch: # Manual kickoff jobs: From bfdc9a8249cea6c4b6605f42904c268dc7e330ef Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 6 Nov 2025 16:06:03 -0500 Subject: [PATCH 1025/1405] upgrade trl and other hf deps (#3249) * upgrade trl and other hf deps * skip simpo for now --- requirements.txt | 8 ++++---- tests/core/test_builders.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4d27ee148b..96b1851973 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ # START section of dependencies that don't install on Darwin/MacOS -bitsandbytes==0.47.0 +bitsandbytes==0.48.2 triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 @@ -12,12 +12,12 @@ packaging==23.2 huggingface_hub>=0.36.0 peft>=0.17.1 -tokenizers>=0.21.1 +tokenizers>=0.22.1 transformers==4.57.1 -accelerate==1.10.1 +accelerate==1.11.0 datasets==4.3.0 deepspeed>=0.17.0 -trl==0.24.0 +trl==0.25.0 hf_xet==1.2.0 kernels>=0.9.0 trackio diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index 67481b2ad5..1997778967 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -396,10 +396,10 @@ def test_simpo_training_arguments(self, simpo_cfg, model, tokenizer): ), ("orpo_cfg", None), # don't use fixture for orpo to use smaller split ("kto_cfg", None), # no fixture for kto - ( - "simpo_cfg", - "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", - ), + # ( + # "simpo_cfg", + # "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", + # ), ], ) def test_custom_optimizer_cls_and_kwargs( From 80270a92fa5eb9cd9063ffc22e8349d0bb056a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Nam=20Kh=C3=A1nh?= <55955273+khanhkhanhlele@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:21:20 +0700 Subject: [PATCH 1026/1405] Fix typos in some files (#3250) [skip ci] --- src/axolotl/loaders/patch_manager.py | 2 +- tests/e2e/multigpu/solo/test_grpo.py | 2 +- tests/e2e/test_preprocess.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 1e46f5c34b..81e4dd7862 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -457,7 +457,7 @@ def _apply_llama_flash_attn_patches(self, model): and self.cfg.flash_attention and not self.inference ): - # TODO(MengqingCao): split these patches seperately + # TODO(MengqingCao): split these patches separately from axolotl.monkeypatch.llama_attn_hijack_flash import ( is_xformers_swiglu_available, replace_llama_mlp_with_swiglu, diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index b48eb30e1f..257a388d02 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -144,7 +144,7 @@ def recursive_kill(process: subprocess.Popen): @pytest.mark.skip(reason="flaky vllm tests in modal") class TestGRPO: """ - Test case for GRPO training using multilpe GPUs + Test case for GRPO training using multiple GPUs """ def _utils_write_yaml_and_rewards(self, cfg, temp_dir, suffix=""): diff --git a/tests/e2e/test_preprocess.py b/tests/e2e/test_preprocess.py index 4aa4cb6c2b..8f15cbe550 100644 --- a/tests/e2e/test_preprocess.py +++ b/tests/e2e/test_preprocess.py @@ -14,7 +14,7 @@ class TestPreprocess: """test cases for preprocess""" def test_w_deepspeed(self, temp_dir): - """make sure preproces doesn't choke when using deepspeed in the config""" + """make sure preprocess doesn't choke when using deepspeed in the config""" cfg = DictDefault( { From ed2e8cacd6cc7acba38582412311a2d6052bc1cf Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 7 Nov 2025 19:21:40 +0530 Subject: [PATCH 1027/1405] feat:openenv rollout_func (#3239) [skip ci] * feat:openenv rollout_func * chore lint * docs * add:docs processing_class * tests * lint --- docs/rlhf.qmd | 112 +++++++++++++++++++++ src/axolotl/core/trainers/grpo/__init__.py | 32 ++++++ src/axolotl/utils/schemas/trl.py | 6 ++ tests/utils/test_grpo_rw_fnc.py | 18 ++++ 4 files changed, 168 insertions(+) create mode 100644 tests/utils/test_grpo_rw_fnc.py diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 594ebc743d..2033649ccb 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -597,6 +597,118 @@ To see other examples of custom reward functions, please see [TRL GRPO Docs](htt To see all configs, please see [TRLConfig](https://github.com/axolotl-ai-cloud/axolotl/blob/v0.9.2/src/axolotl/utils/schemas/trl.py). +#### OpenEnv Rollout Functions +```bash +pip insatll openenv-core``` + +GRPO supports custom rollout functions for OpenEnv-style environments, enabling interactive tasks like web browsing, code execution, or tool use. This allows you to implement custom generation logic that interacts with external environments. + +For example, to implement a simple math-solving environment with step-by-step verification: + +```python +# math_env.py +import re + +def math_solver_rollout(model, processing_class, prompts, generation_config=None): + """ + Custom rollout function that generates step-by-step math solutions. + + Args: + model: The language model + processing_class: The tokenizer/processing_class + prompts: List of prompt dicts (with 'messages' key for chat format) + generation_config: Optional generation configuration + + Returns: + List of completion strings + """ + completions = [] + + for prompt in prompts: + # Apply chat template to prompt + messages = prompt.get("messages", []) + formatted_prompt = processing_class.apply_chat_template( + messages, processing_class=False, add_generation_prompt=True + ) + + # Generate step-by-step solution + full_response = "" + for step in range(5): # Max 5 reasoning steps + current_input = formatted_prompt + full_response + "\nNext step:" + inputs = processing_class(current_input, return_tensors="pt").to(model.device) + + outputs = model.generate( + **inputs, + max_new_tokens=100, + generation_config=generation_config, + ) + step_text = processing_class.decode( + outputs[0][inputs.input_ids.shape[1]:], + skip_special_tokens=True + ) + + # Check if solution is complete + if "FINAL ANSWER:" in step_text: + full_response += step_text + break + full_response += step_text + "\n" + + completions.append(full_response) + + return completions + +def math_reward(prompts, completions, answers, **kwargs): + """Reward function that checks mathematical correctness""" + rewards = [] + for completion, correct_answer in zip(completions, answers): + # Extract predicted answer + match = re.search(r"FINAL ANSWER:\s*(.+)", completion) + predicted = match.group(1).strip() if match else "" + + # Compare with correct answer + reward = 1.0 if predicted == str(correct_answer) else 0.0 + rewards.append(reward) + + return rewards + +def math_transform(cfg, *args, **kwargs): + """Transform dataset to GRPO format with answer field""" + def transform_fn(example, processing_class=None): + return { + "prompt": [{"role": "user", "content": example["question"]}], + "answer": str(example["answer"]), + } + return transform_fn, {"remove_columns": ["question"]} +``` + +```yaml +rl: grpo + +trl: + beta: 0.001 + max_completion_length: 512 + num_generations: 4 + rollout_func: "math_env.math_solver_rollout" # Custom rollout function + reward_funcs: ["math_env.math_reward"] + reward_weights: [1.0] + +datasets: + - path: openai/gsm8k + name: main + type: math_env.math_transform +``` + +The `rollout_func` parameter accepts a fully qualified name (e.g., `module_name.function_name`) that points to a callable function in your local directory. The function receives: + +- `model`: The language model +- `processing_class`: The tokenizer/processing class +- `prompts`: List of prompt dictionaries +- `generation_config` (optional): Generation configuration + +And should return a list of completion strings. + +For more OpenEnv examples, see [TRL OpenEnv Documentation](https://huggingface.co/docs/trl/main/en/openenv). + #### GRPO with DAPO/Dr. GRPO loss The DAPO paper and subsequently Dr. GRPO paper proposed an alternative loss function for GRPO to remediate the penalty in longer responses. diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index bd77489ebd..7f28cb8d4c 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -126,6 +126,9 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: if trl.use_liger_loss is not None: grpo_args_kwargs["use_liger_loss"] = trl.use_liger_loss + if trl.rollout_func: + grpo_args_kwargs["rollout_func"] = cls.get_rollout_func(trl.rollout_func) + return grpo_args_kwargs @classmethod @@ -201,3 +204,32 @@ def get_reward_func(cls, reward_func_fqn: str) -> RewardFunc: raise ValueError( f"Reward function {reward_func_fqn} not found." ) from exc + + @classmethod + def get_rollout_func(cls, rollout_func_fqn: str): + """ + Returns the rollout function from the given fully qualified name. + + Args: + rollout_func_fqn (str): Fully qualified name of the rollout function + (e.g. my_module.my_rollout_func) + + Returns: + Callable rollout function + """ + try: + rollout_func_module_name = rollout_func_fqn.split(".")[-1] + rollout_func_module = importlib.import_module( + ".".join(rollout_func_fqn.split(".")[:-1]) + ) + rollout_func = getattr(rollout_func_module, rollout_func_module_name) + + if not callable(rollout_func): + raise ValueError( + f"Rollout function {rollout_func_fqn} must be callable" + ) + + return rollout_func + + except ModuleNotFoundError as exc: + raise ValueError(f"Rollout function {rollout_func_fqn} not found.") from exc diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index 624f7663ec..d24d6f477b 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -173,3 +173,9 @@ class TRLConfig(BaseModel): "description": "Enable sleep mode for vLLM to offload VRAM when idle" }, ) + rollout_func: str | None = Field( + default=None, + json_schema_extra={ + "description": "Path to custom rollout function. Must be importable from current dir." + }, + ) diff --git a/tests/utils/test_grpo_rw_fnc.py b/tests/utils/test_grpo_rw_fnc.py new file mode 100644 index 0000000000..507de277b5 --- /dev/null +++ b/tests/utils/test_grpo_rw_fnc.py @@ -0,0 +1,18 @@ +import os + +import pytest + +from axolotl.core.trainers.grpo import GRPOStrategy + + +def test_get_rollout_func_loads_successfully(): + """Test that a valid rollout function can be loaded""" + rollout_func = GRPOStrategy.get_rollout_func("os.path.join") + assert callable(rollout_func) + assert rollout_func == os.path.join + + +def test_get_rollout_func_invalid_module_raises_error(): + """Test that invalid module path raises clear ValueError""" + with pytest.raises(ValueError, match="Rollout function .* not found"): + GRPOStrategy.get_rollout_func("nonexistent_module.my_func") From b62eed88095462160da956f21ff33efb6585eb7b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 7 Nov 2025 12:17:27 -0500 Subject: [PATCH 1028/1405] add openenv-core to requirements (#3251) --- docs/rlhf.qmd | 2 -- requirements.txt | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 2033649ccb..1eea420367 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -598,8 +598,6 @@ To see other examples of custom reward functions, please see [TRL GRPO Docs](htt To see all configs, please see [TRLConfig](https://github.com/axolotl-ai-cloud/axolotl/blob/v0.9.2/src/axolotl/utils/schemas/trl.py). #### OpenEnv Rollout Functions -```bash -pip insatll openenv-core``` GRPO supports custom rollout functions for OpenEnv-style environments, enabling interactive tasks like web browsing, code execution, or tool use. This allows you to implement custom generation logic that interacts with external environments. diff --git a/requirements.txt b/requirements.txt index 96b1851973..a12a3941bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -64,6 +64,7 @@ immutabledict==4.2.0 antlr4-python3-runtime==4.13.2 torchao==0.13.0 +openenv-core==0.1.0 schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.7 From b5fcc2f14be77f1cddc7a9b439e17ebb7c761e7a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 7 Nov 2025 16:04:00 -0500 Subject: [PATCH 1029/1405] log cumulative total trained tokens (#3252) * log cumulative total trained tokens * use is_distributed helper --- src/axolotl/core/trainers/base.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 7d7420fb86..7896c60889 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -43,7 +43,7 @@ from axolotl.utils import get_not_null from axolotl.utils.bench import get_gpu_memory_usage from axolotl.utils.dict import DictDefault -from axolotl.utils.distributed import is_main_process +from axolotl.utils.distributed import is_distributed, is_main_process from axolotl.utils.logging import get_logger from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths @@ -350,6 +350,11 @@ def compute_loss( # track number of tokens for tokens per second calculation if self.args.include_tkps: inputs_key = "labels" if "labels" in inputs else "input_ids" + num_tokens = (inputs[inputs_key] != -100).sum() + if is_distributed(): + torch.distributed.all_reduce( + num_tokens, op=torch.distributed.ReduceOp.SUM + ) if hasattr(self.state, "num_tokens"): self.state.num_tokens = ( self.state.num_tokens + (inputs[inputs_key] != -100).sum().cpu() @@ -357,6 +362,11 @@ def compute_loss( else: self.state.num_tokens = (inputs[inputs_key] != -100).sum().cpu() + if hasattr(self.state, "total_tokens"): + self.state.total_tokens += num_tokens + else: + self.state.total_tokens = num_tokens + if self.args.orpo_alpha: return self.orpo_compute_loss( model, @@ -621,6 +631,7 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: logs["tokens_per_second_per_gpu"] = round( self.state.last_tokens_per_second.item() / self.args.logging_steps, 2 ) + logs["total_tokens"] = int(self.state.total_tokens.item()) del self._stored_metrics[train_eval] From d0c846fc5e589aeb7150e4946a5110033ebaf92a Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 10 Nov 2025 21:35:45 +0700 Subject: [PATCH 1030/1405] feat: add granitemoeshared and granitemoehybrid (#3158) --- src/axolotl/monkeypatch/multipack.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 48b4ea10e6..5d34f1935d 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -40,6 +40,8 @@ "smollm3", "granite", "granitemoe", + "granitemoeshared", + "granitemoehybrid", "hunyuan_v1_dense", "hunyuan_v1_moe", "gpt_oss", From 11eb36585a210786ece0f94315662525b9f375f5 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 10 Nov 2025 21:37:47 +0700 Subject: [PATCH 1031/1405] feat: add arg to enable dft in liger (#3125) * feat: add arg to enable dft in liger * feat: add tests use_token_scaling * fix: test * fix: move check to args --- src/axolotl/integrations/liger/README.md | 3 +++ src/axolotl/integrations/liger/args.py | 23 +++++++++++++++++++- src/axolotl/integrations/liger/plugin.py | 27 ++++++++++++++++++++++++ tests/e2e/integrations/test_liger.py | 8 ++++++- tests/integrations/test_liger.py | 16 ++++++++++++++ 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/axolotl/integrations/liger/README.md b/src/axolotl/integrations/liger/README.md index c5cce8282b..3a2d4bd04b 100644 --- a/src/axolotl/integrations/liger/README.md +++ b/src/axolotl/integrations/liger/README.md @@ -18,6 +18,9 @@ liger_rms_norm: true liger_glu_activation: true liger_layer_norm: true liger_fused_linear_cross_entropy: true + +# FLCE-specific +liger_use_token_scaling: true ``` ## Supported Models diff --git a/src/axolotl/integrations/liger/args.py b/src/axolotl/integrations/liger/args.py index d5bb10cfdd..eb7a6c59be 100644 --- a/src/axolotl/integrations/liger/args.py +++ b/src/axolotl/integrations/liger/args.py @@ -16,7 +16,7 @@ Module for handling LIGER input arguments. """ -from pydantic import BaseModel, model_validator +from pydantic import BaseModel, Field, model_validator from axolotl.utils.logging import get_logger @@ -35,6 +35,15 @@ class LigerArgs(BaseModel): liger_glu_activation: bool | None = None liger_cross_entropy: bool | None = None liger_fused_linear_cross_entropy: bool | None = None + liger_use_token_scaling: bool | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Enables use_token_scaling in fused_linear_cross_entropy. " + "When True, each token's loss is multiplied by its predicted probability (detached from gradients)." + ) + }, + ) @model_validator(mode="before") @classmethod @@ -75,6 +84,18 @@ def check_liger_rms_norm_tensor_parallel(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_liger_use_token_scaling_flce(cls, data): + if data.get("liger_use_token_scaling") and not data.get( + "liger_fused_linear_cross_entropy" + ): + raise ValueError( + "`liger_use_token_scaling: true` requires `liger_fused_linear_cross_entropy` enabled." + ) + + return data + @model_validator(mode="after") def check_tensor_parallel_size_liger_fused_linear_cross_entropy(self): # TODO @SalmanMohammadi this is a larger fix - investigate diff --git a/src/axolotl/integrations/liger/plugin.py b/src/axolotl/integrations/liger/plugin.py index 89f7c37b71..ac796c2c90 100644 --- a/src/axolotl/integrations/liger/plugin.py +++ b/src/axolotl/integrations/liger/plugin.py @@ -48,6 +48,33 @@ def pre_model_load(self, cfg): "Cannot have both `liger_cross_entropy` and `liger_fused_linear_cross_entropy` set." ) + if cfg.liger_use_token_scaling: + # Patch FLCE to set token_scaling=True for function and class API + from liger_kernel.transformers import functional + from liger_kernel.transformers.fused_linear_cross_entropy import ( + LigerFusedLinearCrossEntropyLoss, + ) + + old_liger_fused_linear_cross_entropy = ( + functional.liger_fused_linear_cross_entropy + ) + + def patched_liger_fused_linear_cross_entropy(*args, **kwargs): + kwargs["use_token_scaling"] = True + return old_liger_fused_linear_cross_entropy(*args, **kwargs) + + functional.liger_fused_linear_cross_entropy = ( + patched_liger_fused_linear_cross_entropy + ) + + old_init = LigerFusedLinearCrossEntropyLoss.__init__ + + def patched_init(self, *args, **kwargs): + kwargs["use_token_scaling"] = True + return old_init(self, *args, **kwargs) + + LigerFusedLinearCrossEntropyLoss.__init__ = patched_init + if cfg.model_config_type in MODEL_TYPE_TO_APPLY_LIGER_FN: apply_liger_fn = MODEL_TYPE_TO_APPLY_LIGER_FN[cfg.model_config_type] liger_fn_sig = inspect.signature(apply_liger_fn) diff --git a/tests/e2e/integrations/test_liger.py b/tests/e2e/integrations/test_liger.py index 2859699633..55317151e5 100644 --- a/tests/e2e/integrations/test_liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -2,6 +2,7 @@ Simple end-to-end test for Liger integration """ +import pytest from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, prepare_plugins, validate_config @@ -62,7 +63,11 @@ def test_llama_wo_flce(self, temp_dir): check_model_output_exists(temp_dir, cfg) @require_torch_2_4_1 - def test_llama_w_flce(self, temp_dir): + @pytest.mark.parametrize( + "liger_use_token_scaling", + [True, False], + ) + def test_llama_w_flce(self, temp_dir, liger_use_token_scaling): cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -74,6 +79,7 @@ def test_llama_w_flce(self, temp_dir): "liger_glu_activation": True, "liger_cross_entropy": False, "liger_fused_linear_cross_entropy": True, + "liger_use_token_scaling": liger_use_token_scaling, "sequence_len": 1024, "val_set_size": 0.05, "special_tokens": { diff --git a/tests/integrations/test_liger.py b/tests/integrations/test_liger.py index d7b171ec27..6865306c9d 100644 --- a/tests/integrations/test_liger.py +++ b/tests/integrations/test_liger.py @@ -75,3 +75,19 @@ def test_conflict_swiglu_ligergluactivation(self, minimal_liger_cfg): ): prepare_plugins(test_cfg) validate_config(test_cfg) + + def test_use_token_scaling_require_flce(self, minimal_liger_cfg): + test_cfg = DictDefault( + { + "liger_fused_linear_cross_entropy": False, + "liger_use_token_scaling": True, + } + | minimal_liger_cfg + ) + + with pytest.raises( + ValueError, + match=r"`liger_use_token_scaling: true` requires `liger_fused_linear_cross_entropy` enabled.", + ): + prepare_plugins(test_cfg) + validate_config(test_cfg) From b54f9c942ba32c329c41096cd5cd8d675e395e36 Mon Sep 17 00:00:00 2001 From: Eduard Zl <22470747+eduardzl@users.noreply.github.com> Date: Tue, 11 Nov 2025 04:04:28 +0200 Subject: [PATCH 1032/1405] _get_tools in ChatTemplateStrategy : function "parameters" can be dict or string (#3238) * When training of function calls, "tools" elements of a dataset can contain same parameter name but with different types. Datasets fails to load such training set. This fix allows "parameters" element of function call to be string( by running "json.dumps" in preparation of training data set). The _get_tools function will iterate over tool definitions, if "parameters" element is dict, it will keep that way, if it is a string, it will be converted to dict by invoking "json.loads" on string value. * feat: add doc on tool parameters json loading * feat: add tests for parameters json string --------- Co-authored-by: ezlotnik Co-authored-by: NanoCode012 --- docs/dataset-formats/conversation.qmd | 7 + .../prompt_strategies/chat_template.py | 17 + ...at_templates_tool_call_string_arguments.py | 295 +++++++++++++++++- 3 files changed, 317 insertions(+), 2 deletions(-) diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 870a2b67d3..34fde45fbd 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -218,6 +218,13 @@ If you have tool arguments with same name but different dtypes (like `"time": st ``` "arguments": "{\"...\": \"...\"}" ``` + +The same is applicable for tool parameters. + +``` +"parameters": "{\"...\": \"...\"}" +``` + ::: Example config for Llama4: diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index f4dcbd7cd4..28155810f0 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -823,6 +823,23 @@ def _get_tools(self, prompt) -> list[dict] | None: return None if isinstance(tools, list): + # Process each tool to handle JSON string parameters + for tool in tools: + if isinstance(tool, dict) and "function" in tool: + function = tool["function"] + if "parameters" in function: + params = function["parameters"] + if isinstance(params, str): + try: + function["parameters"] = json.loads(params) + except json.JSONDecodeError as e: + LOG.error( + f"Error parsing tool parameters as JSON. " + f"Function: {function.get('name', 'unknown')}, " + f"Parameters string: {params!r}, " + f"Error: {e}" + ) + raise return tools raise ValueError( diff --git a/tests/prompt_strategies/test_chat_templates_tool_call_string_arguments.py b/tests/prompt_strategies/test_chat_templates_tool_call_string_arguments.py index 7de21b9406..5866cc367d 100644 --- a/tests/prompt_strategies/test_chat_templates_tool_call_string_arguments.py +++ b/tests/prompt_strategies/test_chat_templates_tool_call_string_arguments.py @@ -69,7 +69,7 @@ def fixture_conversation_dict_args_dataset(self): { "function": { "name": function_name, - "arguments": arguments_dict, # dict格式 + "arguments": arguments_dict, # dict } } ], @@ -100,7 +100,7 @@ def fixture_conversation_str_args_dataset(self): { "function": { "name": function_name, - "arguments": arguments_str, # str格式 + "arguments": arguments_str, # str } } ], @@ -212,3 +212,294 @@ def test_str_args_with_mixed_time_types_no_error( decoded = qwen3_tokenizer.decode(processed[0]["input_ids"]) assert "2025-08-01" in decoded, "String time value should be present" assert "1690876800" in decoded, "Number time value should be present" + + +class TestQwen3IdenticalToolsParameters: + """ + Test Qwen3 tools parameters handling is identical between JSON string and dict + """ + + @pytest.fixture(name="tools_dict_params_dataset") + def fixture_tools_dict_params_dataset(self): + """ + Provides a dataset with tools where parameters is a dict. + """ + tools = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather information", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state", + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + }, + }, + "required": ["location"], + }, + }, + } + ] + + data = [ + { + "tools": tools, + "messages": [ + {"role": "user", "content": "What's the weather?"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "type": "function", + "function": { + "name": "get_weather", + "arguments": {"location": "Boston, MA"}, + }, + } + ], + }, + { + "role": "tool", + "name": "get_weather", + "content": "72°F and sunny", + }, + ], + } + ] + return Dataset.from_list(data) + + @pytest.fixture(name="tools_str_params_dataset") + def fixture_tools_str_params_dataset(self): + """ + Provides a dataset with tools where parameters is a JSON string. + """ + parameters_dict = { + "type": "object", + "properties": { + "location": {"type": "string", "description": "The city and state"}, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, + }, + "required": ["location"], + } + + tools = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather information", + "parameters": json.dumps(parameters_dict), + }, + } + ] + + data = [ + { + "tools": tools, + "messages": [ + {"role": "user", "content": "What's the weather?"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "type": "function", + "function": { + "name": "get_weather", + "arguments": {"location": "Boston, MA"}, + }, + } + ], + }, + { + "role": "tool", + "name": "get_weather", + "content": "72°F and sunny", + }, + ], + } + ] + return Dataset.from_list(data) + + @pytest.fixture(name="tools_mixed_type_params_dataset") + def fixture_tools_mixed_type_params_dataset(self): + """ + Provides a dataset where different tools have the same parameter name with different types. + This tests that JSON string format prevents casting issues. + """ + tools = [ + { + "type": "function", + "function": { + "name": "tool_with_string_arg", + "description": "Tool expecting string argument", + "parameters": json.dumps( + { + "type": "object", + "properties": { + "arg1": { + "type": "string", + "description": "A string parameter", + } + }, + "required": ["arg1"], + } + ), + }, + }, + { + "type": "function", + "function": { + "name": "tool_with_number_arg", + "description": "Tool expecting number argument", + "parameters": json.dumps( + { + "type": "object", + "properties": { + "arg1": { + "type": "number", + "description": "A numeric parameter", + } + }, + "required": ["arg1"], + } + ), + }, + }, + ] + + data = [ + { + "tools": tools, + "messages": [ + {"role": "user", "content": "Use both tools"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "type": "function", + "function": { + "name": "tool_with_string_arg", + "arguments": json.dumps({"arg1": "hello"}), + }, + }, + { + "type": "function", + "function": { + "name": "tool_with_number_arg", + "arguments": json.dumps({"arg1": 42}), + }, + }, + ], + }, + ], + } + ] + return Dataset.from_list(data) + + def test_dict_and_str_params_produce_equivalent_output( + self, + tools_dict_params_dataset, + tools_str_params_dataset, + qwen3_instruct_prompt_strategy, + qwen3_tokenizer, + ): + """ + Tests that after tokenization and decoding, the outputs for both + dict and string `parameters` in tools are semantically equivalent. + """ + import re + + processed_dict_params = tools_dict_params_dataset.map( + qwen3_instruct_prompt_strategy.tokenize_prompt, + batched=True, + remove_columns=["messages", "tools"], + ) + + processed_str_params = tools_str_params_dataset.map( + qwen3_instruct_prompt_strategy.tokenize_prompt, + batched=True, + remove_columns=["messages", "tools"], + ) + + decoded_dict = qwen3_tokenizer.decode(processed_dict_params[0]["input_ids"]) + decoded_str = qwen3_tokenizer.decode(processed_str_params[0]["input_ids"]) + + # Extract the tool JSON from both outputs + tools_pattern = r"\n(.*?)\n" + + dict_tools_match = re.search(tools_pattern, decoded_dict, re.DOTALL) + str_tools_match = re.search(tools_pattern, decoded_str, re.DOTALL) + + assert dict_tools_match and str_tools_match, ( + "Could not find tools section in output" + ) + + # Parse the JSON and compare as objects (order-independent) + dict_tools_json = json.loads(dict_tools_match.group(1)) + str_tools_json = json.loads(str_tools_match.group(1)) + + # Deep comparison of the tool definitions + assert dict_tools_json == str_tools_json, ( + f"Tool definitions are not equivalent:\n" + f"Dict format: {json.dumps(dict_tools_json, indent=2)}\n" + f"String format: {json.dumps(str_tools_json, indent=2)}" + ) + + # Verify the rest of the structure is the same (excluding the tools JSON part) + # The tools JSON can have different order, so we remove it here. + dict_normalized = re.sub( + r".*?", + "TOOLS_PLACEHOLDER", + decoded_dict, + flags=re.DOTALL, + ) + str_normalized = re.sub( + r".*?", + "TOOLS_PLACEHOLDER", + decoded_str, + flags=re.DOTALL, + ) + + assert dict_normalized == str_normalized, ( + "The overall structure differs between dict and string parameter formats" + ) + + def test_str_params_with_mixed_types_no_error( + self, + tools_mixed_type_params_dataset, + qwen3_instruct_prompt_strategy, + qwen3_tokenizer, + ): + """ + Tests that when different tools have the same parameter name with different types, + JSON string format for parameters doesn't cause casting errors. + """ + processed = tools_mixed_type_params_dataset.map( + qwen3_instruct_prompt_strategy.tokenize_prompt, + batched=True, + remove_columns=["messages", "tools"], + ) + + assert len(processed) == 1 + assert "input_ids" in processed[0] + assert len(processed[0]["input_ids"]) > 0 + + decoded = qwen3_tokenizer.decode(processed[0]["input_ids"]) + + # Check that both tools are present + assert "tool_with_string_arg" in decoded + assert "tool_with_number_arg" in decoded + + # Check that both argument values are present + assert "hello" in decoded + assert "42" in decoded From dd78f2e0cc5cc6458daaad02cc29b649ff1046f5 Mon Sep 17 00:00:00 2001 From: xzuyn <16216325+xzuyn@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:32:06 -0500 Subject: [PATCH 1033/1405] Fix: `warmup_steps: 0` & `warmup_ratio: 0` not disabling warmup (#3254) * fix unintentional falsy checks * chore: lint --------- Co-authored-by: NanoCode012 --- src/axolotl/core/builders/base.py | 4 ++-- tests/e2e/integrations/test_liger.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 2c949f8e75..7954e1fbdc 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -196,9 +196,9 @@ def _configure_warmup_and_logging( ): warmup_steps = 0 warmup_ratio = 0.0 - if self.cfg.warmup_steps: + if self.cfg.warmup_steps is not None: warmup_steps = self.cfg.warmup_steps - elif self.cfg.warmup_ratio: + elif self.cfg.warmup_ratio is not None: if total_num_steps: warmup_steps = max(int(self.cfg.warmup_ratio * total_num_steps), 0) else: diff --git a/tests/e2e/integrations/test_liger.py b/tests/e2e/integrations/test_liger.py index 55317151e5..e50483e6c0 100644 --- a/tests/e2e/integrations/test_liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -3,6 +3,7 @@ """ import pytest + from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, prepare_plugins, validate_config From 9901ee56028c05a3897b2b0b1bbe893f25611654 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 13 Nov 2025 22:18:42 +0700 Subject: [PATCH 1034/1405] fix: voxtralprocessor broken (#3255) [skip ci] * fix: voxtralprocessor broken * chore: add todo * chore: wording --- docs/multimodal.qmd | 2 ++ examples/voxtral/voxtral-mini-audio-qlora.yml | 2 +- src/axolotl/loaders/processor.py | 27 +++++++++++++++---- .../utils/mistral/mistral3_processor.py | 1 + 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index 1c4e28ea72..e63a553b26 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -124,6 +124,8 @@ Please make sure to install audio lib via `pip3 install librosa==0.11.0 'mistral ```yaml base_model: mistralai/Voxtral-Mini-3B-2507 + +processor_type: VoxtralProcessor ``` ### Gemma-3 {#sec-gemma-3} diff --git a/examples/voxtral/voxtral-mini-audio-qlora.yml b/examples/voxtral/voxtral-mini-audio-qlora.yml index 8fe6adbff0..59150c4ca9 100644 --- a/examples/voxtral/voxtral-mini-audio-qlora.yml +++ b/examples/voxtral/voxtral-mini-audio-qlora.yml @@ -1,5 +1,5 @@ base_model: mistralai/Voxtral-Mini-3B-2507 -processor_type: AutoProcessor +processor_type: VoxtralProcessor # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name diff --git a/src/axolotl/loaders/processor.py b/src/axolotl/loaders/processor.py index 7580b20084..b35ea00fd5 100644 --- a/src/axolotl/loaders/processor.py +++ b/src/axolotl/loaders/processor.py @@ -1,7 +1,5 @@ """Processor loading functionality for multi-modal models""" -from typing import Any - import transformers from transformers import ( AutoProcessor, @@ -15,13 +13,33 @@ def load_processor(cfg: DictDefault, tokenizer: PreTrainedTokenizerBase): - processor_kwargs: dict[str, Any] = {} # Do we actually need this? - processor_cls = AutoProcessor if cfg.processor_type: processor_cls = getattr(transformers, cfg.processor_type) if cfg.tokenizer_use_mistral_common: + + def _patch_mistralcommontokenizer(): + """ + Transformers v5 stops reading the sub-processor. + + We need to patch this, so both processors use this. + """ + import transformers.tokenization_mistral_common as tokenization_mistral_common + + from axolotl.utils.mistral import HFMistralTokenizer + + tokenization_mistral_common.MistralCommonTokenizer = HFMistralTokenizer + + _patch_mistralcommontokenizer() + + from transformers import VoxtralProcessor + + if processor_cls == VoxtralProcessor: + return VoxtralProcessor.from_pretrained( + cfg.processor_config, + ) + from axolotl.utils.mistral import Mistral3Processor return Mistral3Processor( @@ -32,7 +50,6 @@ def load_processor(cfg: DictDefault, tokenizer: PreTrainedTokenizerBase): cfg.processor_config, trust_remote_code=cfg.trust_remote_code or False, tokenizer=tokenizer, - **processor_kwargs, ) # Attempt to load image size from processor if available diff --git a/src/axolotl/utils/mistral/mistral3_processor.py b/src/axolotl/utils/mistral/mistral3_processor.py index 85479ca7be..01e8f9f100 100644 --- a/src/axolotl/utils/mistral/mistral3_processor.py +++ b/src/axolotl/utils/mistral/mistral3_processor.py @@ -30,6 +30,7 @@ class Mistral3Processor(ProcessorMixin): Wraps HFMistralTokenizer and adds image processing capabilities. """ + # TODO(nano): This should be removed in transformers V5 attributes = ["tokenizer"] tokenizer_class = "HFMistralTokenizer" From 49b81079891020c20477c8f9b3f63cd683d16d95 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 13 Nov 2025 22:19:16 +0700 Subject: [PATCH 1035/1405] feat: add granite4 examples (#3256) [skip ci] --- examples/granite4/README.md | 65 +++++++++++++++++++++ examples/granite4/granite-4.0-tiny-fft.yaml | 45 ++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 examples/granite4/README.md create mode 100644 examples/granite4/granite-4.0-tiny-fft.yaml diff --git a/examples/granite4/README.md b/examples/granite4/README.md new file mode 100644 index 0000000000..d5efd3349f --- /dev/null +++ b/examples/granite4/README.md @@ -0,0 +1,65 @@ +# Finetune IBM's Granite 4.0 with Axolotl + +[Granite 4.0](https://huggingface.co/collections/ibm-granite/granite-40-language-models) are a family of open source models trained by IBM Research. + +This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Granite4 is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). + + Here is an example of how to install from main for pip: + +```bash +# Ensure you have Pytorch installed (Pytorch 2.7.1 min) +git clone https://github.com/axolotl-ai-cloud/axolotl.git +cd axolotl + +pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install --no-build-isolation -e '.[flash-attn]' + +# Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy +python scripts/cutcrossentropy_install.py | sh +``` + +2. Run the finetuning example: + +```bash +axolotl train examples/granite4/granite-4.0-tiny-fft.yaml +``` + +This config uses about 40.8GiB VRAM. + +Let us know how it goes. Happy finetuning! 🚀 + +### TIPS + +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +### Limitation + +Adapter finetuning does not work at the moment. It would error with + +```bash +RuntimeError: mat1 and mat2 shapes cannot be multiplied (4096x3072 and 1x1179648) +``` + +In addition, if adapter training works, `lora_target_linear: true` will not work due to: +```bash +ValueError: Target module GraniteMoeHybridParallelExperts() is not supported. +``` + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) + +## Related Resources + +- [Granite Docs](https://www.ibm.com/granite/docs/models/granite) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/granite4/granite-4.0-tiny-fft.yaml b/examples/granite4/granite-4.0-tiny-fft.yaml new file mode 100644 index 0000000000..7ff8207aea --- /dev/null +++ b/examples/granite4/granite-4.0-tiny-fft.yaml @@ -0,0 +1,45 @@ +base_model: ibm-granite/granite-4.0-tiny-preview + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/model-out + +sequence_len: 2048 +sample_packing: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config From dcf24fd24ed59993e03cde0fc17e464a542bf52e Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:51:05 +0530 Subject: [PATCH 1036/1405] feat: save checkpoint after training started (#3233) * add:config parameters for checkpoint * callback main * test file_type fix * lint * unit * simplify dict/obj handeling * Update src/axolotl/utils/schemas/dynamic_checkpoint.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Delete tests/e2e/integrations/__init__.py * remove hard code path in test * device check * lint * Update src/axolotl/utils/callbacks/dynamic_checkpoint.py Co-authored-by: NanoCode012 * Update src/axolotl/utils/callbacks/dynamic_checkpoint.py Co-authored-by: NanoCode012 * Update src/axolotl/utils/schemas/dynamic_checkpoint.py Co-authored-by: NanoCode012 * lint-2 * remove: singal based checkpoints * lint * remove signal tests * add:is_main_process * lint * addis_d:istributed() for tests * remove nested is_main_process * Update src/axolotl/utils/schemas/dynamic_checkpoint.py Co-authored-by: Wing Lian * Update src/axolotl/utils/schemas/dynamic_checkpoint.py Co-authored-by: Wing Lian * add user_defined_filename --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: NanoCode012 Co-authored-by: Wing Lian --- src/axolotl/core/builders/base.py | 7 + .../utils/callbacks/dynamic_checkpoint.py | 132 ++++++ src/axolotl/utils/schemas/config.py | 8 + .../utils/schemas/dynamic_checkpoint.py | 31 ++ tests/e2e/integrations/__init__.py | 0 .../callbacks/test_dynamic_checkpoint.py | 389 ++++++++++++++++++ 6 files changed, 567 insertions(+) create mode 100644 src/axolotl/utils/callbacks/dynamic_checkpoint.py create mode 100644 src/axolotl/utils/schemas/dynamic_checkpoint.py delete mode 100644 tests/e2e/integrations/__init__.py create mode 100644 tests/utils/callbacks/test_dynamic_checkpoint.py diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 7954e1fbdc..fc6759ffb4 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -118,6 +118,13 @@ def get_callbacks(self) -> list[TrainerCallback]: if self.cfg.gc_steps: callbacks.append(GCCallback(gc_steps=self.cfg.gc_steps)) + if self.cfg.dynamic_checkpoint and self.cfg.dynamic_checkpoint.enabled: + from axolotl.utils.callbacks.dynamic_checkpoint import ( + DynamicCheckpointCallback, + ) + + callbacks.append(DynamicCheckpointCallback(self.cfg)) + if self.cfg.use_wandb: callbacks.append( SaveAxolotlConfigtoWandBCallback(self.cfg.axolotl_config_path) diff --git a/src/axolotl/utils/callbacks/dynamic_checkpoint.py b/src/axolotl/utils/callbacks/dynamic_checkpoint.py new file mode 100644 index 0000000000..6321092258 --- /dev/null +++ b/src/axolotl/utils/callbacks/dynamic_checkpoint.py @@ -0,0 +1,132 @@ +from pathlib import Path + +from transformers import ( + TrainerCallback, + TrainerControl, + TrainerState, + TrainingArguments, +) + +from axolotl.utils.distributed import ( + barrier, + is_distributed, + is_main_process, +) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +DEFAULT_TRIGGER_FILENAME = "axolotl_checkpoint.save" + + +class DynamicCheckpointCallback(TrainerCallback): + """ + Callback to save checkpoints on-demand during training via: + 1. File-based trigger (works everywhere, rank 0 checks file) + + Thread-safe for multi-GPU distributed training. + + Usage: + # File-based: + touch /path/to/output_dir/axolotl_checkpoint.save + """ + + def _get_config_value(self, config, key, default=None): + """Helper to get config value from dict or object.""" + if isinstance(config, dict): + return config.get(key, default) + return getattr(config, key, default) + + def __init__(self, cfg): + self.cfg = cfg + if not cfg.dynamic_checkpoint or not cfg.dynamic_checkpoint.enabled: + self.enabled = False + return + + self.enabled = True + dc_config = cfg.dynamic_checkpoint + + trigger_file_path = self._get_config_value(dc_config, "trigger_file_path") + self.trigger_filename = ( + trigger_file_path if trigger_file_path else DEFAULT_TRIGGER_FILENAME + ) + + check_interval = self._get_config_value(dc_config, "check_interval") + self.check_interval = check_interval if check_interval is not None else 100 + self.should_save_checkpoint = False + + LOG.info( + f"Dynamic checkpoint enabled. To trigger checkpoint save:\n" + f" • File: touch {cfg.output_dir}/{self.trigger_filename}\n" + f" • Check interval: every {self.check_interval} steps", + main_process_only=True, + ) + + def on_step_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **_kwargs, + ) -> TrainerControl: + """ + Check for checkpoint triggers at the end of each step. + ONLY rank 0 checks the file, then all ranks synchronize. + """ + if not self.enabled: + return control + + trigger_detected = False + + if state.global_step % self.check_interval == 0: + if is_main_process(): + trigger_path = Path(args.output_dir) / self.trigger_filename + + if trigger_path.exists(): + trigger_detected = True + try: + trigger_path.unlink() # Delete the trigger file + LOG.info( + f"Dynamic checkpoint triggered via file '{self.trigger_filename}' " + f"at step {state.global_step}", + main_process_only=True, + ) + except OSError as exc: + LOG.warning( + f"Failed to delete trigger file: {exc}", + main_process_only=True, + ) + + if self.should_save_checkpoint: + trigger_detected = True + self.should_save_checkpoint = False # Reset flag + + if is_distributed(): + import torch + import torch.distributed as dist + + device = getattr( + args, + "device", + torch.device("cuda" if torch.cuda.is_available() else "cpu"), + ) + + trigger_tensor = torch.tensor( + 1 if trigger_detected else 0, + dtype=torch.long, + device=device, + ) + + dist.broadcast(trigger_tensor, src=0) + + trigger_detected = bool(trigger_tensor.item()) + + barrier() + + if trigger_detected: + control.should_save = True + LOG.info( + f"Saving dynamic checkpoint at step {state.global_step}", + main_process_only=True, + ) + return control diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 86b3aa17b4..5ad55f8b74 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -23,6 +23,7 @@ StepwiseSupervisedDataset, ) from axolotl.utils.schemas.deprecated import DeprecatedParameters, RemappedParameters +from axolotl.utils.schemas.dynamic_checkpoint import DynamicCheckpointConfig from axolotl.utils.schemas.enums import ChatTemplate, RingAttnFunc, RLType from axolotl.utils.schemas.fsdp import FSDPConfig from axolotl.utils.schemas.integrations import ( @@ -141,6 +142,13 @@ class AxolotlInputConfig( default=None, json_schema_extra={"description": "Reward modelling: `True` or `False`"}, ) + dynamic_checkpoint: DynamicCheckpointConfig | None = Field( + default=None, + json_schema_extra={ + "description": "Configuration for dynamic checkpointing (trigger by file or signal). " + "Set 'enabled: true' to activate this feature." + }, + ) process_reward_model: bool | None = Field( default=None, json_schema_extra={ diff --git a/src/axolotl/utils/schemas/dynamic_checkpoint.py b/src/axolotl/utils/schemas/dynamic_checkpoint.py new file mode 100644 index 0000000000..e0e1d0c1d6 --- /dev/null +++ b/src/axolotl/utils/schemas/dynamic_checkpoint.py @@ -0,0 +1,31 @@ +"""Schema for dynamic checkpoint configuration.""" + +from pydantic import BaseModel, Field + + +class DynamicCheckpointConfig(BaseModel): + """Configuration for dynamic checkpoint triggering during training.""" + + enabled: bool = Field( + default=False, + json_schema_extra={ + "description": "Enable dynamic checkpoint triggering during training. " + "Create a file 'axolotl_checkpoint.save' in the configured `output_dir` to trigger. " + }, + ) + check_interval: int = Field( + default=10, + ge=1, + json_schema_extra={ + "description": "Check for trigger file every N steps (reduces I/O overhead). " + "Default: 100" + }, + ) + trigger_file_path: str = Field( + default="", + json_schema_extra={ + "description": "Custom trigger filename (optional). " + "If not specified, defaults to 'axolotl_checkpoint.save'. " + "Specify a filename (not a full path) to override the default." + }, + ) diff --git a/tests/e2e/integrations/__init__.py b/tests/e2e/integrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/utils/callbacks/test_dynamic_checkpoint.py b/tests/utils/callbacks/test_dynamic_checkpoint.py new file mode 100644 index 0000000000..1fd7921026 --- /dev/null +++ b/tests/utils/callbacks/test_dynamic_checkpoint.py @@ -0,0 +1,389 @@ +"""Unit tests for dynamic checkpoint callback""" + +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, Mock, patch + +from axolotl.utils.callbacks.dynamic_checkpoint import ( + DEFAULT_TRIGGER_FILENAME, + DynamicCheckpointCallback, +) +from axolotl.utils.dict import DictDefault + + +class TestDynamicCheckpointCallbackInit: + """Test callback initialization""" + + def test_callback_disabled_by_default(self): + """Test that callback is disabled when config.enabled=False""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": {"enabled": False}, + "output_dir": tmpdir, + } + ) + callback = DynamicCheckpointCallback(cfg) + assert callback.enabled is False + + def test_callback_disabled_when_none(self): + """Test that callback is disabled when dynamic_checkpoint is None""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": None, + "output_dir": tmpdir, + } + ) + callback = DynamicCheckpointCallback(cfg) + assert callback.enabled is False + + def test_callback_enabled_when_configured(self): + """Test that callback is enabled when config.enabled=True""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": {"enabled": True, "check_interval": 10}, + "output_dir": tmpdir, + } + ) + callback = DynamicCheckpointCallback(cfg) + assert callback.enabled is True + assert callback.check_interval == 10 + + def test_default_trigger_filename(self): + """Test that default trigger filename is used""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": {"enabled": True, "check_interval": 10}, + "output_dir": tmpdir, + } + ) + callback = DynamicCheckpointCallback(cfg) + assert callback.trigger_filename == DEFAULT_TRIGGER_FILENAME + + def test_check_interval_default(self): + """Test default check interval""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": {"enabled": True}, + "output_dir": tmpdir, + } + ) + callback = DynamicCheckpointCallback(cfg) + assert callback.check_interval == 100 # Default from schema + + +class TestDynamicCheckpointFileDetection: + """Test file-based checkpoint triggering""" + + def test_trigger_file_detected_and_deleted(self): + """Test that trigger file is detected and deleted""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": {"enabled": True, "check_interval": 1}, + "output_dir": tmpdir, + } + ) + callback = DynamicCheckpointCallback(cfg) + + trigger_file = Path(tmpdir) / DEFAULT_TRIGGER_FILENAME + trigger_file.touch() + assert trigger_file.exists() + + args = Mock(output_dir=tmpdir) + state = Mock(global_step=1) + control = Mock(should_save=False) + + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_main_process", + return_value=True, + ): + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_distributed", + return_value=False, + ): + result = callback.on_step_end(args, state, control) + + assert not trigger_file.exists() + assert result.should_save is True + + def test_check_interval_honored(self): + """Test that file is only checked at check_interval steps""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": {"enabled": True, "check_interval": 10}, + "output_dir": tmpdir, + } + ) + callback = DynamicCheckpointCallback(cfg) + + args = Mock(output_dir=tmpdir) + control = Mock(should_save=False) + + trigger_file = Path(tmpdir) / DEFAULT_TRIGGER_FILENAME + trigger_file.touch() + + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_main_process", + return_value=True, + ): + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_distributed", + return_value=False, + ): + # Step 5 - shouldn't check (not divisible by 10) + state = Mock(global_step=5) + result = callback.on_step_end(args, state, control) + assert trigger_file.exists() # Still there + assert result.should_save is False + + # Step 10 - should check + state = Mock(global_step=10) + result = callback.on_step_end(args, state, control) + assert not trigger_file.exists() # Deleted + assert result.should_save is True + + def test_no_file_no_trigger(self): + """Test that no trigger occurs when file doesn't exist""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": {"enabled": True, "check_interval": 1}, + "output_dir": tmpdir, + } + ) + callback = DynamicCheckpointCallback(cfg) + + args = Mock(output_dir=tmpdir) + state = Mock(global_step=1) + control = Mock(should_save=False) + + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_main_process", + return_value=True, + ): + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_distributed", + return_value=False, + ): + result = callback.on_step_end(args, state, control) + + assert result.should_save is False + + def test_file_deletion_error_handling(self): + """Test that file deletion errors are handled gracefully""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": {"enabled": True, "check_interval": 1}, + "output_dir": tmpdir, + } + ) + callback = DynamicCheckpointCallback(cfg) + + trigger_file = Path(tmpdir) / DEFAULT_TRIGGER_FILENAME + trigger_file.touch() + + args = Mock(output_dir=tmpdir) + state = Mock(global_step=1) + control = Mock(should_save=False) + + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_main_process", + return_value=True, + ): + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_distributed", + return_value=False, + ): + with patch.object( + Path, "unlink", side_effect=OSError("Permission denied") + ): + result = callback.on_step_end(args, state, control) + + assert result.should_save is True + + +class TestDynamicCheckpointMultiGPU: + """Test multi-GPU synchronization""" + + def test_only_rank_0_checks_file(self): + """Test that only rank 0 checks filesystem in multi-GPU setup""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": {"enabled": True, "check_interval": 1}, + "output_dir": tmpdir, + } + ) + callback = DynamicCheckpointCallback(cfg) + + trigger_file = Path(tmpdir) / DEFAULT_TRIGGER_FILENAME + trigger_file.touch() + + args = Mock(output_dir=tmpdir) + state = Mock(global_step=1) + control = Mock(should_save=False) + + # Rank 1 (not main process) - shouldn't check file + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_main_process", + return_value=False, + ): + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_distributed", + return_value=True, + ): + with patch("torch.distributed.broadcast") as mock_broadcast: + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.barrier" + ): + mock_tensor = MagicMock() + mock_tensor.item.return_value = 0 + with patch("torch.tensor", return_value=mock_tensor): + callback.on_step_end(args, state, control) + + assert trigger_file.exists() + # Broadcast should have been called + assert mock_broadcast.called + + def test_broadcast_synchronization(self): + """Test that trigger decision is broadcasted to all ranks""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": {"enabled": True, "check_interval": 1}, + "output_dir": tmpdir, + } + ) + callback = DynamicCheckpointCallback(cfg) + + trigger_file = Path(tmpdir) / DEFAULT_TRIGGER_FILENAME + trigger_file.touch() + + args = Mock(output_dir=tmpdir) + state = Mock(global_step=1) + control = Mock(should_save=False) + + # Rank 0 detects file + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_main_process", + return_value=True, + ): + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_distributed", + return_value=True, + ): + with patch("torch.distributed.broadcast") as mock_broadcast: + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.barrier" + ) as mock_barrier: + mock_tensor = MagicMock() + mock_tensor.item.return_value = 1 + with patch("torch.tensor", return_value=mock_tensor): + with patch("torch.cuda.current_device", return_value=0): + result = callback.on_step_end(args, state, control) + + assert mock_broadcast.called + assert mock_barrier.called + # All ranks should trigger + assert result.should_save is True + + +class TestDynamicCheckpointSignalHandling: + """Test signal-based checkpoint triggering""" + + def test_signal_trigger_via_callback(self): + """Test that signal flag triggers checkpoint save""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": { + "enabled": True, + "check_interval": 1, + "enable_signal": True, + }, + "output_dir": tmpdir, + } + ) + + with patch("signal.signal"): + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_main_process", + return_value=True, + ): + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.hasattr", + return_value=True, + ): + callback = DynamicCheckpointCallback(cfg) + + callback.should_save_checkpoint = True + + args = Mock(output_dir=tmpdir) + state = Mock(global_step=1) + control = Mock(should_save=False) + + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_main_process", + return_value=True, + ): + with patch( + "axolotl.utils.callbacks.dynamic_checkpoint.is_distributed", + return_value=False, + ): + result = callback.on_step_end(args, state, control) + + assert result.should_save is True + assert callback.should_save_checkpoint is False + + def test_signal_not_registered_when_disabled(self): + """Test that signal handler is not registered when disabled""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": { + "enabled": True, + "check_interval": 10, + "enable_signal": False, + }, + "output_dir": tmpdir, + } + ) + + with patch("signal.signal") as mock_signal_register: + _ = DynamicCheckpointCallback(cfg) + + assert not mock_signal_register.called + + +class TestDynamicCheckpointDisabled: + """Test behavior when callback is disabled""" + + def test_disabled_callback_does_nothing(self): + """Test that disabled callback doesn't check or trigger""" + with tempfile.TemporaryDirectory() as tmpdir: + cfg = DictDefault( + { + "dynamic_checkpoint": {"enabled": False}, + "output_dir": tmpdir, + } + ) + callback = DynamicCheckpointCallback(cfg) + + trigger_file = Path(tmpdir) / DEFAULT_TRIGGER_FILENAME + trigger_file.touch() + + args = Mock(output_dir=tmpdir) + state = Mock(global_step=1) + control = Mock(should_save=False) + + result = callback.on_step_end(args, state, control) + + assert trigger_file.exists() + assert result.should_save is False From 301e22849f41c67c31e065a222235ab120fd4074 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 13 Nov 2025 13:03:01 -0500 Subject: [PATCH 1037/1405] upgrade to latest deepspeed and make sure latest tagged axolotl images are using torch 2.8.0 (#3261) --- .github/workflows/main.yml | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4040ccdc92..3b182af024 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,7 +25,6 @@ jobs: python_version: "3.11" pytorch: 2.7.1 axolotl_extras: vllm - is_latest: true - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" @@ -36,6 +35,7 @@ jobs: python_version: "3.11" pytorch: 2.8.0 axolotl_extras: + is_latest: true runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -99,7 +99,6 @@ jobs: python_version: "3.11" pytorch: 2.7.1 axolotl_extras: vllm - is_latest: true - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" @@ -110,6 +109,7 @@ jobs: python_version: "3.11" pytorch: 2.8.0 axolotl_extras: + is_latest: true runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/setup.py b/setup.py index 9c11616422..a1bdd6bdf9 100644 --- a/setup.py +++ b/setup.py @@ -130,7 +130,7 @@ def get_package_version(): "ring-flash-attn>=0.1.7", ], "deepspeed": [ - "deepspeed==0.17.5", + "deepspeed==0.18.2", "deepspeed-kernels", ], "mamba-ssm": [ From 0fbde69e9c21133f668fd1bbf1e2d59203c546a7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 14 Nov 2025 10:50:03 -0500 Subject: [PATCH 1038/1405] only push axolotl images, personal repo is deprecated (#3262) * only push axolotl images, personal repo is deprecated * cleanup --- .github/FUNDING.yml | 6 +++--- .github/workflows/base.yml | 1 - .github/workflows/main.yml | 3 --- .github/workflows/nightlies.yml | 2 -- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4f6ea8de73..cd443a1971 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,13 +1,13 @@ # These are supported funding model platforms -github: [winglian, OpenAccess-AI-Collective] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username -ko_fi: axolotl_ai # Replace with a single Ko-fi username +ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -custom: ['https://quickchart.io/qr?text=bitcoin%3Abc1qxlgwlqwfea5s2cxm42xqsfmwjct0rj8w8ea5np&size=480¢erImageUrl=https%3A%2F%2Fupload.wikimedia.org%2Fwikipedia%2Fcommons%2Fthumb%2F4%2F46%2FBitcoin.svg%2F64px-Bitcoin.svg.png'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 87d6772dd9..2e8950dd9e 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -90,7 +90,6 @@ jobs: uses: docker/metadata-action@v5 with: images: | - winglian/axolotl-base axolotlai/axolotl-base - name: Login to Docker Hub uses: docker/login-action@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3b182af024..4f0cc4c99d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,7 +45,6 @@ jobs: uses: docker/metadata-action@v5 with: images: | - winglian/axolotl axolotlai/axolotl tags: | type=ref,event=branch @@ -119,7 +118,6 @@ jobs: uses: docker/metadata-action@v5 with: images: | - winglian/axolotl-cloud axolotlai/axolotl-cloud tags: | type=ref,event=branch @@ -179,7 +177,6 @@ jobs: uses: docker/metadata-action@v5 with: images: | - winglian/axolotl-cloud-term axolotlai/axolotl-cloud-term tags: | type=ref,event=branch diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 18b036a0d8..a24946ae99 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -31,7 +31,6 @@ jobs: uses: docker/metadata-action@v5 with: images: | - winglian/axolotl axolotlai/axolotl tags: | type=raw,value={{ branch }}-{{ date 'YYYYMMDD' }} @@ -84,7 +83,6 @@ jobs: uses: docker/metadata-action@v5 with: images: | - winglian/axolotl-cloud axolotlai/axolotl-cloud tags: | type=raw,value={{ branch }}-{{ date 'YYYYMMDD' }} From a6bafb55cbd6973222d6d0d37aee2013ae656ba7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 14 Nov 2025 12:52:14 -0500 Subject: [PATCH 1039/1405] upgrade datasets to 4.4.1 (#3266) * upgrade datasets * cleanup pip cache earlier * cleanup unused things from worker * also cleanup sdist --- .github/workflows/tests.yml | 24 ++++++++++++++++-------- requirements.txt | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7ad9d1ab4d..95370ca3d3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -59,6 +59,10 @@ jobs: timeout-minutes: 20 steps: + - name: cleanup node + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL + - name: Check out repository code uses: actions/checkout@v4 @@ -91,6 +95,10 @@ jobs: python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt + - name: cleanup pip cache + run: | + find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + - name: Make sure PyTorch version wasn't clobbered run: | python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" @@ -118,10 +126,6 @@ jobs: flags: unittests,pytorch-${{ matrix.pytorch_version }} fail_ci_if_error: false - - name: cleanup pip cache - run: | - find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; - pytest-sdist: name: PyTest from Source Dist runs-on: ubuntu-latest @@ -134,6 +138,10 @@ jobs: timeout-minutes: 20 steps: + - name: cleanup node + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL + - name: Check out repository code uses: actions/checkout@v4 @@ -167,6 +175,10 @@ jobs: python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt + - name: cleanup pip cache + run: | + find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + - name: Make sure PyTorch version wasn't clobbered run: | python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" @@ -184,10 +196,6 @@ jobs: pytest -v --durations=10 tests/monkeypatch/ --cov=axolotl --cov-append --cov-report=xml pytest -v --durations=10 tests/cli/ - - name: cleanup pip cache - run: | - find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; - gate-skip-e2e: needs: [pre-commit, pytest, pytest-sdist] runs-on: ubuntu-latest diff --git a/requirements.txt b/requirements.txt index a12a3941bf..62c1b3cba5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ peft>=0.17.1 tokenizers>=0.22.1 transformers==4.57.1 accelerate==1.11.0 -datasets==4.3.0 +datasets==4.4.1 deepspeed>=0.17.0 trl==0.25.0 hf_xet==1.2.0 From 4e558711120c18175ab8689da017e28ec7d0a6cb Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 18 Nov 2025 11:35:25 +0700 Subject: [PATCH 1040/1405] feat: Add opt-out Telemetry (#3237) * initial telemetry manager impl * adding todo * updates * updates * progress on telemetry: config load, process, model load, train start / end, error tracking * update error file path sanitization function; adding more error tracking * updated sanitization logic, tests * adding runtime metrics (cpu + gpu memory, steps/s, etc.) * tests for runtime metrics telemetry and assoc. callback * small update / fix * simplifying path redaction * sleep on all ranks in distributed setting * adding back in base_model redaction w/ whitelist * fix * doc update * improved redaction, send system info during model config load telemetry, etc. * adding runtime metrics / system info additional accelerator support, etc. * adding runtime metrics / system info additional accelerator support, etc. * remove duplicate info * fixes * fix issue with tests in ci * distributed fix * opt-in version of telemetry * enable / disable logic update * docs fix * doc update * minor fixes * simplifying * slight changes * fix * lint * update posthog dep * coderabbit comments * fix: opt-in model * fix: increase time since last * fix: increase whitelist orgs * fix: posthog init and shutdown * fix: imports * fix: also check grad norm * fix: duplicate plugin_manager calls * fix: bad merge * chore: update docs * fix: cache process per comment * fix: error handling * fix: tests * Revert "fix: error handling" This reverts commit 22d1ea5755500a7c0e08c562270ca85591bf451f. * fix: test telemetry error_handled bool * fix: revert test * chore: final doc fixes --------- Co-authored-by: Dan Saunders Co-authored-by: Dan Saunders --- README.md | 7 + _quarto.yml | 1 + docs/telemetry.qmd | 61 +++ requirements.txt | 3 + src/axolotl/cli/config.py | 8 + src/axolotl/cli/inference.py | 7 +- src/axolotl/cli/merge_lora.py | 2 + src/axolotl/cli/merge_sharded_fsdp_weights.py | 2 + src/axolotl/cli/preprocess.py | 2 + src/axolotl/common/datasets.py | 3 + src/axolotl/core/builders/base.py | 6 + src/axolotl/evaluate.py | 2 + src/axolotl/loaders/adapter.py | 2 + src/axolotl/loaders/model.py | 2 + src/axolotl/loaders/processor.py | 2 + src/axolotl/loaders/tokenizer.py | 2 + src/axolotl/telemetry/__init__.py | 0 src/axolotl/telemetry/callbacks.py | 165 +++++++ src/axolotl/telemetry/errors.py | 160 +++++++ src/axolotl/telemetry/manager.py | 416 ++++++++++++++++++ src/axolotl/telemetry/runtime_metrics.py | 210 +++++++++ src/axolotl/telemetry/whitelist.yaml | 33 ++ src/axolotl/train.py | 27 +- src/axolotl/utils/schemas/config.py | 2 +- tests/conftest.py | 10 +- tests/telemetry/__init__.py | 0 tests/telemetry/conftest.py | 9 + tests/telemetry/test_callbacks.py | 373 ++++++++++++++++ tests/telemetry/test_errors.py | 341 ++++++++++++++ tests/telemetry/test_manager.py | 275 ++++++++++++ tests/telemetry/test_runtime_metrics.py | 357 +++++++++++++++ 31 files changed, 2479 insertions(+), 11 deletions(-) create mode 100644 docs/telemetry.qmd create mode 100644 src/axolotl/telemetry/__init__.py create mode 100644 src/axolotl/telemetry/callbacks.py create mode 100644 src/axolotl/telemetry/errors.py create mode 100644 src/axolotl/telemetry/manager.py create mode 100644 src/axolotl/telemetry/runtime_metrics.py create mode 100644 src/axolotl/telemetry/whitelist.yaml create mode 100644 tests/telemetry/__init__.py create mode 100644 tests/telemetry/conftest.py create mode 100644 tests/telemetry/test_callbacks.py create mode 100644 tests/telemetry/test_errors.py create mode 100644 tests/telemetry/test_manager.py create mode 100644 tests/telemetry/test_runtime_metrics.py diff --git a/README.md b/README.md index 6313a73ca0..d6dd679886 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,13 @@ That's it! Check out our [Getting Started Guide](https://docs.axolotl.ai/docs/ge Contributions are welcome! Please see our [Contributing Guide](https://github.com/axolotl-ai-cloud/axolotl/blob/main/.github/CONTRIBUTING.md) for details. +## 📈 Telemetry + +Axolotl has opt-out telemetry that helps us understand how the project is being used +and prioritize improvements. We collect basic system information, model types, and +error rates—never personal data or file paths. Telemetry is enabled by default. To +disable it, set AXOLOTL_DO_NOT_TRACK=1. For more details, see our [telemetry documentation](https://docs.axolotl.ai/docs/telemetry.html). + ## ❤️ Sponsors Interested in sponsoring? Contact us at [wing@axolotl.ai](mailto:wing@axolotl.ai) diff --git a/_quarto.yml b/_quarto.yml index fad3f67863..c97b9838e0 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -241,6 +241,7 @@ website: - docs/installation.qmd - docs/inference.qmd - docs/cli.qmd + - docs/telemetry.qmd - docs/config-reference.qmd - text: "API Reference" href: docs/api diff --git a/docs/telemetry.qmd b/docs/telemetry.qmd new file mode 100644 index 0000000000..62d7c9bbc0 --- /dev/null +++ b/docs/telemetry.qmd @@ -0,0 +1,61 @@ +--- +title: Telemetry +description: A description of the telemetry implementation in Axolotl. +--- + +# Telemetry in Axolotl + +Axolotl implements anonymous telemetry to help maintainers understand how the library +is used and where users encounter issues. This data helps prioritize features, optimize +performance, and fix bugs. + +## Data Collection + +We collect: + +- System info: OS, Python version, Axolotl version, PyTorch version, Transformers +version, etc. +- Hardware info: CPU count, memory, GPU count and models +- Runtime metrics: Training progress, memory usage, timing information +- Usage patterns: Models (from a whitelist) and configurations used +- Error tracking: Stack traces and error messages (sanitized to remove personal +information) + +Personally identifiable information (PII) is not collected. + +## Implementation + +Telemetry is implemented using PostHog and consists of: + +- `axolotl.telemetry.TelemetryManager`: A singleton class that initializes the +telemetry system and provides methods for tracking events. +- `axolotl.telemetry.errors.send_errors`: A decorator that captures exceptions and +sends sanitized stack traces. +- `axolotl.telemetry.runtime_metrics.RuntimeMetricsTracker`: A class that tracks +runtime metrics during training. +- `axolotl.telemetry.callbacks.TelemetryCallback`: A Trainer callback that sends +runtime metrics telemetry. + +The telemetry system will block training startup for 10 seconds to ensure users are +aware of data collection, unless telemetry is explicitly enabled or disabled. + +## Opt-Out Mechanism + +Telemetry is **enabled by default** on an opt-out basis. To disable it, set +`AXOLOTL_DO_NOT_TRACK=1` or `DO_NOT_TRACK=1`. + +A warning message will be logged on start to clearly inform users about telemetry. +We will remove this after some period. + +To hide the warning message about telemetry that is displayed on train, etc. startup, +explicitly set: `AXOLOTL_DO_NOT_TRACK=0` (enable telemetry) or `AXOLOTL_DO_NOT_TRACK=1` +(explicitly disable telemetry). + +## Privacy + +- All path-like config information is automatically redacted from telemetry data +- Model information is only collected for whitelisted organizations + - See `axolotl/telemetry/whitelist.yaml` for the set of whitelisted organizations +- Each run generates a unique anonymous ID + - This allows us to link different telemetry events in a single same training run +- Telemetry is only sent from the main process to avoid duplicate events diff --git a/requirements.txt b/requirements.txt index 62c1b3cba5..977262df50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -70,4 +70,7 @@ schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.7 axolotl-contribs-mit==0.0.5 +# telemetry +posthog==6.7.11 + mistral-common==1.8.5 diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index 93ac6147d1..3c4ace7b0c 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -14,6 +14,8 @@ from transformers.utils import is_torch_bf16_gpu_available from axolotl.integrations.base import PluginManager +from axolotl.telemetry.errors import send_errors +from axolotl.telemetry.manager import TelemetryManager from axolotl.utils.comet_ import setup_comet_env_vars from axolotl.utils.config import ( normalize_cfg_datasets, @@ -31,6 +33,8 @@ API_KEY_FIELDS = {"comet_api_key"} +TELEMETRY_MANAGER = TelemetryManager.get_instance() + def check_remote_config(config: Union[str, Path]) -> Union[str, Path]: """ @@ -164,6 +168,7 @@ def plugin_set_cfg(cfg: DictDefault): plugin_manager.cfg = cfg +@send_errors def load_cfg( config: str | Path | DictDefault = Path("examples/"), **kwargs ) -> DictDefault: @@ -197,6 +202,8 @@ def load_cfg( temp_file.close() cfg.axolotl_config_path = temp_file.name + TELEMETRY_MANAGER.send_event(event_type="config-loaded", properties=cfg) + # If there are any options passed in the cli, if it is something that seems valid # from the yaml, then overwrite the value cfg_keys = cfg.keys() @@ -240,6 +247,7 @@ def load_cfg( setup_comet_env_vars(cfg) plugin_set_cfg(cfg) + TELEMETRY_MANAGER.send_event(event_type="config-processed", properties=cfg) cfg_to_log = { k: "[REDACTED]" if k in API_KEY_FIELDS else v for k, v in cfg.items() diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index 3e1c015205..640be36962 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -19,7 +19,10 @@ launch_diffusion_gradio_ui, ) from axolotl.integrations.base import PluginManager -from axolotl.utils.chat_templates import get_chat_template_from_config +from axolotl.telemetry.errors import send_errors +from axolotl.utils.chat_templates import ( + get_chat_template_from_config, +) from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger @@ -43,6 +46,7 @@ def get_multi_line_input() -> str: return instruction +@send_errors def do_inference( *, cfg: DictDefault, @@ -160,6 +164,7 @@ def do_inference( print(tokenizer.decode(generated["sequences"].cpu().tolist()[0])) +@send_errors def do_inference_gradio( *, cfg: DictDefault, diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 657ddcfe43..482767b124 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -7,12 +7,14 @@ from axolotl.cli.config import load_cfg from axolotl.cli.utils import load_model_and_tokenizer +from axolotl.telemetry.errors import send_errors from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger LOG = get_logger(__name__) +@send_errors def do_merge_lora(*, cfg: DictDefault) -> None: """ Calls `transformers`' `merge_and_unload` on the model given in the `axolotl` config diff --git a/src/axolotl/cli/merge_sharded_fsdp_weights.py b/src/axolotl/cli/merge_sharded_fsdp_weights.py index 43142d79e0..1d9736b9d1 100644 --- a/src/axolotl/cli/merge_sharded_fsdp_weights.py +++ b/src/axolotl/cli/merge_sharded_fsdp_weights.py @@ -23,6 +23,7 @@ from torch.distributed.checkpoint.format_utils import _EmptyStateDictLoadPlanner from axolotl.cli.config import load_cfg +from axolotl.telemetry.errors import send_errors from axolotl.utils.logging import get_logger from axolotl.utils.train import determine_last_checkpoint @@ -118,6 +119,7 @@ def _distributed_checkpoint_to_merged_weights( return save_path_ +@send_errors def merge_fsdp_weights( checkpoint_dir: str, output_path: str, diff --git a/src/axolotl/cli/preprocess.py b/src/axolotl/cli/preprocess.py index 6c05a55f11..af35dd8010 100644 --- a/src/axolotl/cli/preprocess.py +++ b/src/axolotl/cli/preprocess.py @@ -17,6 +17,7 @@ from axolotl.common.const import DEFAULT_DATASET_PREPARED_PATH from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.integrations.base import PluginManager +from axolotl.telemetry.errors import send_errors from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger from axolotl.utils.trainer import disable_datasets_caching @@ -24,6 +25,7 @@ LOG = get_logger(__name__) +@send_errors def do_preprocess(cfg: DictDefault, cli_args: PreprocessCliArgs) -> None: """ Preprocesses dataset specified in axolotl config. diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index 8d7758e66f..c95ddb80ee 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -9,6 +9,7 @@ import axolotl.monkeypatch.data.batch_dataset_fetcher # noqa: F401 from axolotl.cli.args import PreprocessCliArgs, TrainerCliArgs from axolotl.loaders import load_processor, load_tokenizer +from axolotl.telemetry.errors import send_errors from axolotl.utils.data import prepare_datasets, prepare_preference_datasets from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger @@ -34,6 +35,7 @@ def sample_dataset(dataset: Dataset, num_samples: int) -> Dataset: ) +@send_errors def load_datasets( *, cfg: DictDefault, @@ -96,6 +98,7 @@ def load_datasets( ) +@send_errors def load_preference_datasets( *, cfg: DictDefault, cli_args: PreprocessCliArgs | TrainerCliArgs | None = None ) -> TrainDatasetMeta: diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index fc6759ffb4..0d19b369f5 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -29,6 +29,8 @@ from axolotl.integrations.base import PluginManager from axolotl.monkeypatch.trainer.lr import patch_trainer_get_lr +from axolotl.telemetry.callbacks import TelemetryCallback +from axolotl.telemetry.manager import TelemetryManager from axolotl.utils import ( is_comet_available, is_mlflow_available, @@ -162,6 +164,10 @@ def get_callbacks(self) -> list[TrainerCallback]: ) ) + telemetry_manager = TelemetryManager.get_instance() + if telemetry_manager.enabled: + callbacks.append(TelemetryCallback()) + return callbacks def get_post_trainer_create_callbacks(self, trainer): diff --git a/src/axolotl/evaluate.py b/src/axolotl/evaluate.py index e4496bee65..db6fb3f161 100644 --- a/src/axolotl/evaluate.py +++ b/src/axolotl/evaluate.py @@ -10,6 +10,7 @@ from datasets import Dataset from transformers.trainer import Trainer +from axolotl.telemetry.errors import send_errors from axolotl.train import ( TrainDatasetMeta, setup_model_and_tokenizer, @@ -63,6 +64,7 @@ def evaluate_dataset( return metrics +@send_errors def evaluate(*, cfg: DictDefault, dataset_meta: TrainDatasetMeta) -> Dict[str, float]: """ Evaluate a model on training and validation datasets. diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index bcde4bf96e..8e8177b628 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -20,6 +20,7 @@ from transformers import PreTrainedModel from axolotl.loaders.utils import get_linear_embedding_layers +from axolotl.telemetry.errors import send_errors from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger @@ -172,6 +173,7 @@ def load_lora( return model, lora_config +@send_errors def load_adapter( model: PreTrainedModel, cfg: DictDefault, diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index aeec46584c..1eeed35656 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -49,6 +49,7 @@ load_model_config, ) from axolotl.models.mamba import fix_mamba_attn_for_loss +from axolotl.telemetry.errors import send_errors from axolotl.utils.bench import log_gpu_memory_usage from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import ( @@ -158,6 +159,7 @@ def is_qlora_and_fsdp_enabled(self): """Property that determines if FSDP with QLoRA is enabled.""" return self.is_fsdp_enabled and self.cfg.adapter == "qlora" + @send_errors def load(self) -> tuple[PreTrainedModel | PeftModelForCausalLM, PeftConfig | None]: """Load and prepare the model with all configurations and patches. diff --git a/src/axolotl/loaders/processor.py b/src/axolotl/loaders/processor.py index b35ea00fd5..827b4be35b 100644 --- a/src/axolotl/loaders/processor.py +++ b/src/axolotl/loaders/processor.py @@ -6,12 +6,14 @@ PreTrainedTokenizerBase, ) +from axolotl.telemetry.errors import send_errors from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger LOG = get_logger(__name__) +@send_errors def load_processor(cfg: DictDefault, tokenizer: PreTrainedTokenizerBase): processor_cls = AutoProcessor if cfg.processor_type: diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index 69455dd772..48856116c0 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -13,6 +13,7 @@ from axolotl.integrations.base import PluginManager from axolotl.loaders.utils import get_linear_embedding_layers, load_model_config from axolotl.prompt_tokenizers import LLAMA_DEFAULT_EOS_TOKEN +from axolotl.telemetry.errors import send_errors from axolotl.utils.chat_templates import get_chat_template_from_config from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import ( @@ -119,6 +120,7 @@ def modify_tokenizer_files( return tokenizer_dir +@send_errors def load_tokenizer(cfg: DictDefault) -> PreTrainedTokenizer: """Load and configure the tokenizer based on the provided config.""" diff --git a/src/axolotl/telemetry/__init__.py b/src/axolotl/telemetry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/telemetry/callbacks.py b/src/axolotl/telemetry/callbacks.py new file mode 100644 index 0000000000..0ce52ffa42 --- /dev/null +++ b/src/axolotl/telemetry/callbacks.py @@ -0,0 +1,165 @@ +"""Trainer callbacks for reporting runtime metrics at regular intervals.""" + +import logging +import time + +from transformers import ( + TrainerCallback, + TrainerControl, + TrainerState, + TrainingArguments, +) + +from axolotl.telemetry.manager import TelemetryManager +from axolotl.telemetry.runtime_metrics import RuntimeMetricsTracker + +LOG = logging.getLogger(__name__) + +TIME_SINCE_LAST = 60 + + +class TelemetryCallback(TrainerCallback): + """ + Trainer callback for tracking and reporting runtime metrics. + + This callback tracks training progress, runtime, and memory usage, + sending telemetry at configurable intervals. + """ + + report_interval_steps: int = 100 + + def __init__(self): + """Initialize the metrics callback.""" + self.tracker = RuntimeMetricsTracker() + self.telemetry_manager = TelemetryManager.get_instance() + self.current_epoch = -1 + self.start_time = time.time() + self.last_report_time = None + self.last_report_step = 0 + + # pylint: disable=unused-argument + def on_train_begin( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Handle training start.""" + self.telemetry_manager.send_event(event_type="train-start") + + # pylint: disable=unused-argument + def on_train_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Handle training end.""" + # Send training completion event + self.telemetry_manager.send_event( + event_type="train-end", + properties=self._extract_last_metrics(state) + | self.tracker.metrics.to_dict(), + ) + + # pylint: disable=unused-argument + def on_epoch_begin( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Handle epoch start.""" + self.current_epoch += 1 + self.tracker.start_epoch(self.current_epoch) + + # pylint: disable=unused-argument + def on_epoch_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Handle epoch end.""" + self.tracker.end_epoch(self.current_epoch) + + # pylint: disable=unused-argument + def on_step_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Handle step end.""" + step = state.global_step + self.tracker.update_step(step) + + # Check if we should report metrics + should_report = ( + step % self.report_interval_steps == 0 + or step == 1 # Always report first step + or step - self.last_report_step >= self.report_interval_steps + ) + + if should_report: + current_time = time.time() + if self.last_report_time is not None: + time_since_last_report = current_time - self.last_report_time + else: + time_since_last_report = current_time - self.start_time + steps_since_last_report = step - self.last_report_step + + # Only report if enough time has passed + if ( + step == 1 + or time_since_last_report >= TIME_SINCE_LAST + or steps_since_last_report >= self.report_interval_steps + ): + # Calculate steps per second for this interval + if time_since_last_report > 0 and steps_since_last_report > 0: + steps_per_second = steps_since_last_report / time_since_last_report + else: + steps_per_second = 0 + + # Update memory metrics + self.tracker.update_memory_metrics() + + # Prepare metrics to report + metrics = self._extract_last_metrics(state) | { + "step": step, + "epoch": self.current_epoch, + "progress": state.epoch, # Fractional epoch progress + "steps_per_second": steps_per_second, + "elapsed_time": current_time - self.start_time, + "time_since_last_report": time_since_last_report, + } + + # Add memory metrics + memory_metrics = self.tracker.get_memory_metrics() + metrics.update({"memory": memory_metrics}) + + # Send telemetry + self.telemetry_manager.send_event( + event_type="train-progress", properties=metrics + ) + + # Update last report time and step + self.last_report_time = current_time + self.last_report_step = step + + def _extract_last_metrics(self, state: TrainerState) -> dict: + """Extract last loss, learning_rate, and grad_norm from log history.""" + if not state.log_history: + return {"loss": 0, "learning_rate": 0, "grad_norm": 0} + + last_log = state.log_history[-1] + return { + "loss": last_log.get("loss", 0), + "learning_rate": last_log.get("learning_rate", 0), + "grad_norm": last_log.get("grad_norm", 0), + } diff --git a/src/axolotl/telemetry/errors.py b/src/axolotl/telemetry/errors.py new file mode 100644 index 0000000000..27f2d21921 --- /dev/null +++ b/src/axolotl/telemetry/errors.py @@ -0,0 +1,160 @@ +"""Telemetry utilities for exception and traceback information.""" + +import logging +import os +import re +import traceback +from functools import wraps +from inspect import getmodule +from typing import Any, Callable + +from axolotl.telemetry.manager import TelemetryManager + +LOG = logging.getLogger(__name__) + +ERROR_HANDLED = False + + +def sanitize_stack_trace(stack_trace: str) -> str: + """ + Remove personal information from stack trace messages while keeping Python package codepaths. + + This function identifies Python packages by looking for common patterns in virtual environment + and site-packages directories, preserving the package path while removing user-specific paths. + + Args: + stack_trace: The original stack trace string. + + Returns: + A sanitized version of the stack trace with Python package paths preserved. + """ + # Split the stack trace into lines to process each file path separately + lines = stack_trace.split("\n") + sanitized_lines = [] + + # Regular expression to find file paths in the stack trace + path_pattern = re.compile(r'(?:File ")(.*?)(?:")') + + # Regular expression to identify paths in site-packages or dist-packages + # This matches path segments like "site-packages/package_name" or "dist-packages/package_name" + site_packages_pattern = re.compile( + r"(?:site-packages|dist-packages)[/\\]([\w\-\.]+)" + ) + + # Additional common virtual environment patterns + venv_lib_pattern = re.compile( + r"(?:lib|Lib)[/\\](?:python\d+(?:\.\d+)?[/\\])?(?:site-packages|dist-packages)[/\\]([\w\-\.]+)" + ) + + for line in lines: + # Check if this line contains a file path + path_match = path_pattern.search(line) + + if path_match: + full_path = path_match.group(1) + sanitized_path = "" + + # Try to match site-packages pattern + site_packages_match = site_packages_pattern.search(full_path) + venv_lib_match = venv_lib_pattern.search(full_path) + + if site_packages_match: + # Find the index where the matched pattern starts + idx = full_path.find("site-packages") + if idx == -1: + idx = full_path.find("dist-packages") + + # Keep from 'site-packages' onward + if idx >= 0: + sanitized_path = full_path[idx:] + elif venv_lib_match: + # For other virtual environment patterns, find the package directory + match_idx = venv_lib_match.start(1) + if match_idx > 0: + # Keep from the package name onward + package_name = venv_lib_match.group(1) + idx = full_path.rfind( + package_name, 0, match_idx + len(package_name) + ) + if idx >= 0: + sanitized_path = full_path[idx:] + + # If we couldn't identify a package pattern but path contains 'axolotl' + elif "axolotl" in full_path: + idx = full_path.rfind("axolotl") + if idx >= 0: + sanitized_path = full_path[idx:] + + # Apply the sanitization to the line + if sanitized_path: + line = line.replace(full_path, sanitized_path) + else: + # If we couldn't identify a package pattern, just keep the filename + filename = os.path.basename(full_path) + if filename: + line = line.replace(full_path, filename) + else: + line = line.replace(full_path, "") + + sanitized_lines.append(line) + + return "\n".join(sanitized_lines) + + +def send_errors(func: Callable) -> Callable: + """ + Decorator to send exception info in a function. If an exception is raised, we send + telemetry containing the stack trace and error message. + + If an error occurs in a decorated function that is called by another decorated + function, we'll only send telemetry corresponding to the lower-level function. + + Args: + func: Function to decorate. + + Returns: + Decorated function. + """ + + @wraps(func) + def wrapper(*args, **kwargs) -> Any: + telemetry_manager = TelemetryManager.get_instance() + + if not telemetry_manager.enabled: + return func(*args, **kwargs) + + try: + return func(*args, **kwargs) + except Exception as exception: + # Only track if we're not already handling an error. This prevents us from + # capturing an error more than once in nested decorated function calls. + global ERROR_HANDLED # pylint: disable=global-statement + if not ERROR_HANDLED: + ERROR_HANDLED = True + + # Get function module path + module = getmodule(func) + module_path = ( + f"{module.__name__}.{func.__name__}" if module else func.__name__ + ) + + # Get stack trace + stack_trace = "".join( + traceback.format_exception( + type(exception), exception, exception.__traceback__ + ) + ) + stack_trace = sanitize_stack_trace(stack_trace) + + # Send error telemetry + telemetry_manager.send_event( + event_type=f"{module_path}-error", + properties={ + "exception": str(exception), + "stack_trace": stack_trace, + }, + ) + + raise + + return wrapper diff --git a/src/axolotl/telemetry/manager.py b/src/axolotl/telemetry/manager.py new file mode 100644 index 0000000000..82d310cdc9 --- /dev/null +++ b/src/axolotl/telemetry/manager.py @@ -0,0 +1,416 @@ +"""Telemetry manager and associated utilities.""" + +import atexit +import importlib +import logging +import os +import platform +import time +import uuid +from pathlib import Path +from typing import Any + +import posthog +import psutil +import torch +import yaml + +LOG = logging.getLogger(__name__) + +POSTHOG_HOST = "https://app.posthog.com" +POSTHOG_WRITE_KEY = "phc_1kUR0o04oJKKTTeSsIz2Mfm5mpiVsQEf2WOlzljMD7y" + +OPT_OUT_WARNING_SLEEP_SECONDS = 10 +OPT_OUT_WARNING = ( + "\nTelemetry is now enabled by default to help improve Axolotl. " + "If you'd like to disable it, set AXOLOTL_DO_NOT_TRACK=1 in your environment.\n\n" + "Telemetry data helps us understand:\n" + "- Which features are most used\n" + "- What hardware configurations to prioritize\n" + "- Where users encounter errors\n\n" + "Personally identifiable information (PII) is not collected.\n\n" + "To remove this warning, explicitly set AXOLOTL_DO_NOT_TRACK=0 (enable telemetry) " + "or AXOLOTL_DO_NOT_TRACK=1 (disable telemetry).\n\n" + "For details, see: https://docs.axolotl.ai/docs/telemetry.html\n\n" + f"Sleeping for {OPT_OUT_WARNING_SLEEP_SECONDS}s..." +) + +WHITELIST_PATH = str(Path(__file__).parent / "whitelist.yaml") + +# NOTE: Need to keep these up to date with any config schema changes +FIELDS_TO_REDACT = { + "base_model", + "tokenizer_config", + "base_model_config", + "pretraining_dataset", # NOTE: this field may be a string or a dictionary + "resume_from_checkpoint", + "hub_model_id", +} +PREFIXES_TO_REDACT = {"wandb_", "comet_", "mlflow_", "gradio_"} +PATH_INDICATORS = {"path", "dir"} + +# pylint: disable=duplicate-code +RELEVANT_PACKAGES = { + "torch", + "transformers", + "trl", + "datasets", + "peft", + "bitsandbytes", + "accelerate", + "optimum", + "deepspeed", + "ray", + "axolotl", + "triton", + "mamba-ssm", + "flash-attn", + "xformers", + "autoawq", + "tokenizers", + "sentencepiece", + "torchao", + "lm_eval", +} + + +def is_main_process() -> bool: + """ + Check whether we're running in the main process. + + Note: + We're using this function instead of `torch.utils.distributed.is_main_process` + causes issues with DeepSpeed world_size since. This function avoids that issue + by checking env vars that are set by various launchers. + + Returns: + Whether we're running in the main process. + """ + # If PyTorch distributed is already initialized, use it + if torch.distributed.is_initialized(): + return torch.distributed.get_rank() == 0 + + # Otherwise check environment variables for global rank + # NOTE: need to verify this in SLURM / OpenMPI environments + global_rank = int( + os.environ.get( + "RANK", + os.environ.get( + "GLOBAL_RANK", + os.environ.get( + "SLURM_PROCID", + os.environ.get( + "OMPI_COMM_WORLD_RANK", + "0", + ), + ), + ), + ) + ) + + return global_rank == 0 + + +class TelemetryManager: + """Manages telemetry collection and transmission""" + + _instance = None + _initialized = False + + def __new__(cls): + """ + Telemetry manager constructor. Creates the singleton instance of this class if + it doesn't already exist. + """ + if cls._instance is None: + cls._instance = super(TelemetryManager, cls).__new__(cls) + cls._instance._initialized = False + + return cls._instance + + def __init__(self): + """Telemetry manager initializer""" + if self._initialized: + return + + self.enabled = self._check_telemetry_enabled() + + if self.enabled: + self.run_id = str(uuid.uuid4()) + self.whitelist = self._load_whitelist() + + try: + self.system_info = self._get_system_info() + except Exception as e: # pylint: disable=broad-exception-caught + LOG.warning(f"Error during system info collection: {e}") + self.system_info = None + + self._init_posthog() + + # Register shutdown method to flush posthog telemetry + atexit.register(self.shutdown) + + self._initialized = True + + @classmethod + def get_instance(cls) -> "TelemetryManager": + if cls._instance is None: + cls._instance = TelemetryManager() + + return cls._instance + + def _check_telemetry_enabled(self) -> bool: + """ + Check if telemetry is enabled based on environment variables. We also check + whether this is the main process (for the distributed setting and to avoid + sending duplicate PostHog events per GPU). + + Note: This is enabled by default on an opt-out basis. Set + `AXOLOTL_DO_NOT_TRACK=1` to disable telemetry. For more details, see + https://axolotl-ai-cloud.github.io/axolotl/docs/telemetry.html. + + Returns: + Boolean denoting whether telemetry is enabled or not. + """ + # Parse relevant env vars + axolotl_do_not_track = os.getenv("AXOLOTL_DO_NOT_TRACK") + do_not_track = os.getenv("DO_NOT_TRACK") + + # Default to enabled (opt-out model) + if axolotl_do_not_track is None or axolotl_do_not_track.lower() not in ( + "0", + "1", + "false", + "true", + ): + # Print opt-out info message for main process only + if is_main_process(): + LOG.warning(OPT_OUT_WARNING) + time.sleep(OPT_OUT_WARNING_SLEEP_SECONDS) + + return True + + # Only rank 0 will send telemetry + if not is_main_process(): + return False + + if do_not_track is None: + do_not_track = "0" + + # Respect AXOLOTL_DO_NOT_TRACK, DO_NOT_TRACK if enabled + enabled = axolotl_do_not_track.lower() not in ( + "1", + "true", + ) and do_not_track.lower() not in ("1", "true") + + return enabled + + def _load_whitelist(self) -> dict: + """Load HuggingFace Hub organization whitelist""" + with open(WHITELIST_PATH, encoding="utf-8") as f: + whitelist = yaml.safe_load(f) + + # Send org strings to lowercase since model names are case insensitive + whitelist["organizations"] = { + org.lower() for org in whitelist["organizations"] + } + + return whitelist + + def _is_whitelisted(self, value: str) -> bool: + """ + Check if model / dataset / etc. org is in whitelist. + + Args: + value: Value for one of `axolotl.telemetry.manager.FIELDS_WITH_ORGS` + ("base_model", etc.). + + Returns: + Boolean indicating whitelist membership. + """ + # NOTE: This membership-checking logic can be improved. + # What happens when a local model path matches a whitelisted org? + parts = value.split("/") + if len(parts) < 2: + return False + org = parts[0] + whitelisted = org.lower() in self.whitelist["organizations"] + + return whitelisted + + def _init_posthog(self): + """Initialize PostHog client""" + posthog.api_key = POSTHOG_WRITE_KEY + posthog.project_api_key = POSTHOG_WRITE_KEY + posthog.host = POSTHOG_HOST + + def _redact_paths(self, properties: dict[str, Any]) -> dict[str, Any]: + """ + Redact properties to remove any paths, so as to avoid inadvertently collecting + private or personally identifiable information (PII). We also remove + information related to Wandb, MLflow, etc. configuration. + + Args: + properties: Dictionary of properties to redact. + + Returns: + Properties dictionary with redaction applied. + """ + if not properties: + return {} + + def redact_value(value: Any, key: str = "") -> Any: + """Recursively sanitize values, redacting those with path-like keys""" + if isinstance(key, str) and isinstance(value, str): + # Other redaction special cases + if ( + key in FIELDS_TO_REDACT + or any(prefix in key for prefix in PREFIXES_TO_REDACT) + or any(indicator in key.lower() for indicator in PATH_INDICATORS) + ): + # Fields with whitelisted orgs don't need to be redacted + if not self._is_whitelisted(value): + return "[REDACTED]" + + # Handle nested values + if isinstance(value, dict): + return {k: redact_value(v, k) for k, v in value.items()} + if isinstance(value, list): + return [redact_value(item) for item in value] + + return value + + # Create new dict with redacted values + redacted = {k: redact_value(v, k) for k, v in properties.items()} + + return redacted + + def _get_system_info(self) -> dict[str, Any]: + """Collect system information for various hardware accelerators""" + gpu_info = [] + accelerator_type = "none" + + # NVIDIA GPUs + if torch.cuda.is_available(): + accelerator_type = "cuda" + for i in range(torch.cuda.device_count()): + gpu_info.append( + { + "name": torch.cuda.get_device_name(i), + "memory": torch.cuda.get_device_properties(i).total_memory, + } + ) + + # AMD GPUs + elif hasattr(torch, "hip") and torch.hip.is_available(): + accelerator_type = "hip" + for i in range(torch.hip.device_count()): + gpu_info.append( + { + "name": torch.hip.get_device_name(i), + "memory": ( + torch.hip.get_device_properties(i).total_memory + if hasattr(torch.hip, "get_device_properties") + else None + ), + } + ) + + # Apple Silicon + elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + accelerator_type = "mps" + gpu_info.append( + { + "name": "Apple Silicon", + # NOTE: this is memory allocated to this process, not total memory + "memory": torch.mps.driver_allocated_memory(), + } + ) + + # Intel GPUs + elif hasattr(torch, "xpu") and torch.xpu.is_available(): + accelerator_type = "xpu" + for i in range(torch.xpu.device_count()): + memory = None + if hasattr(torch.xpu, "get_device_properties"): + memory = torch.xpu.get_device_properties(i).total_memory + + gpu_info.append( + { + "name": torch.xpu.get_device_name(i), + "memory": memory, + } + ) + + # NPUs + elif hasattr(torch, "npu") and torch.npu.is_available(): + accelerator_type = "npu" + for i in range(torch.npu.device_count()): + memory = None + if hasattr(torch.npu, "get_device_properties"): + memory = torch.npu.get_device_properties(i).total_memory + + gpu_info.append( + { + "name": torch.npu.get_device_name(i), + "memory": memory, + } + ) + + # Get relevant package versions + installed_packages = {} + for package in RELEVANT_PACKAGES: + try: + version = importlib.metadata.version(package) + installed_packages[f"{package}_version"] = version + except importlib.metadata.PackageNotFoundError: + pass + + return { + "os": platform.system(), + "python_version": platform.python_version(), + "cpu_count": psutil.cpu_count(), + "memory_total": psutil.virtual_memory().total, + "accelerator_type": accelerator_type, + "accelerator_count": len(gpu_info), + "accelerator_info": gpu_info, + **installed_packages, + } + + def send_event(self, event_type: str, properties: dict[str, Any] | None = None): + """Send a telemetry event""" + if not self.enabled: + return + + if properties is None: + properties = {} + + # Sanitize properties to remove PII + properties = self._redact_paths(properties) + + # Wrap PostHog errors in try / except to not raise errors during Axolotl usage + try: + # Send event via PostHog + posthog.capture( + distinct_id=self.run_id, + event=event_type, + properties=properties, + disable_geoip=True, + ) + except Exception as e: # pylint: disable=broad-exception-caught + LOG.warning(f"Failed to send telemetry event: {e}") + + # Additionally, send system info telemetry when loading config. + # NOTE: Is this the best place for this? + if event_type == "config-loaded": + self.send_system_info() + + def send_system_info(self): + """Helper method for sending system info""" + if self.system_info is not None: + self.send_event(event_type="system-info", properties=self.system_info) + + def shutdown(self): + """Ensure all queued events are processed before shutdown""" + if self.enabled: + posthog.shutdown() diff --git a/src/axolotl/telemetry/runtime_metrics.py b/src/axolotl/telemetry/runtime_metrics.py new file mode 100644 index 0000000000..fa83c00a73 --- /dev/null +++ b/src/axolotl/telemetry/runtime_metrics.py @@ -0,0 +1,210 @@ +"""Telemetry utilities for runtime and memory metrics.""" + +import logging +import time +from dataclasses import dataclass, field +from typing import Any + +import psutil +import torch + +from axolotl.telemetry.manager import TelemetryManager + +LOG = logging.getLogger(__name__) + + +@dataclass +class RuntimeMetrics: + """Container for runtime metrics to be tracked throughout training.""" + + # Timing metrics + start_time: float + epoch_start_times: dict[int, float] = field(init=False) + epoch_end_times: dict[int, float] = field(init=False) + + # Memory metrics + peak_cpu_memory: int = 0 + peak_gpu_memory: dict[int, int] = field(init=False) + + # Progress metrics + total_steps: int = 0 + current_epoch: int = 0 + current_step: int = 0 + + def __post_init__(self): + """Initialize empty metric mappings.""" + self.epoch_start_times = {} + self.epoch_end_times = {} + self.peak_gpu_memory = {} + + @property + def elapsed_time(self) -> float: + """Calculate total elapsed time in seconds.""" + return time.time() - self.start_time + + def epoch_time(self, epoch: int) -> float | None: + """Calculate time taken for a specific epoch in seconds.""" + if epoch in self.epoch_start_times and epoch in self.epoch_end_times: + return self.epoch_end_times[epoch] - self.epoch_start_times[epoch] + + return None + + def average_epoch_time(self) -> float | None: + """Calculate average time per epoch in seconds.""" + completed_epochs = [ + epoch for epoch in self.epoch_start_times if epoch in self.epoch_end_times + ] + if not completed_epochs: + return None + + total_time = 0.0 + for epoch in completed_epochs: + epoch_time = self.epoch_time(epoch) + if epoch_time is not None: # Check to avoid mypy warning + total_time += epoch_time + + return total_time / len(completed_epochs) + + def steps_per_second(self) -> float | None: + """Calculate average steps per second across all training.""" + if self.total_steps == 0 or self.elapsed_time == 0: + return None + + return self.total_steps / self.elapsed_time + + def to_dict(self) -> dict[str, Any]: + """Convert metrics to a dictionary for telemetry reporting.""" + metrics = { + "total_time_seconds": self.elapsed_time, + "total_steps": self.total_steps, + "steps_per_second": self.steps_per_second(), + "epochs_completed": len( + [ + epoch + for epoch in self.epoch_start_times + if epoch in self.epoch_end_times + ] + ), + "peak_cpu_memory_bytes": self.peak_cpu_memory, + } + + # Add per-epoch timing if available + epoch_times: dict[str, float] = {} + for epoch in sorted(self.epoch_end_times.keys()): + time_taken = self.epoch_time(epoch) + if time_taken is not None: + epoch_times[f"epoch_{epoch}_seconds"] = time_taken + + if epoch_times: + metrics["epoch_times"] = epoch_times # type: ignore + metrics["average_epoch_time_seconds"] = self.average_epoch_time() + + # Add GPU memory metrics if available + if self.peak_gpu_memory: + gpu_metrics: dict[str, int] = {} + for gpu_id, memory in self.peak_gpu_memory.items(): + gpu_metrics[f"gpu_{gpu_id}_peak_memory_bytes"] = memory + metrics["gpu_memory"] = gpu_metrics # type: ignore + + return metrics + + +class RuntimeMetricsTracker: + """Tracker for runtime metrics during training.""" + + update_interval = 100 + + def __init__(self): + """Initialize the runtime metrics tracker.""" + self.metrics = RuntimeMetrics(start_time=time.time()) + self.telemetry_manager = TelemetryManager.get_instance() + self._process = psutil.Process() + + def start_epoch(self, epoch: int): + """Record the start of a new epoch.""" + self.metrics.current_epoch = epoch + self.metrics.epoch_start_times[epoch] = time.time() + self.update_memory_metrics() + + def end_epoch(self, epoch: int): + """Record the end of an epoch.""" + self.metrics.epoch_end_times[epoch] = time.time() + + def update_step(self, step: int): + """Update the current step count.""" + self.metrics.current_step = step + self.metrics.total_steps += 1 + + # Periodically update memory metrics + if step % self.update_interval == 0: + self.update_memory_metrics() + + def _get_allocated_memory(self) -> dict[int, int]: + """ + Helper function for getting accelerator-agnostic allocated memory. + + Returns: + A dictionary mapping device IDs to allocated memory in bytes + """ + memory_used: dict[int, int] = {} + + # NVIDIA GPUs + if torch.cuda.is_available(): + for i in range(torch.cuda.device_count()): + memory_used[i] = torch.cuda.memory_allocated(i) + + # AMD GPUs + elif hasattr(torch, "hip") and torch.hip.is_available(): + for i in range(torch.hip.device_count()): + if hasattr(torch.hip, "memory_allocated"): + memory_used[i] = torch.hip.memory_allocated(i) + + # Apple Silicon + elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + # MPS doesn't have per-device memory stats since there's only one device + if hasattr(torch.mps, "current_allocated_memory"): + memory_used[0] = torch.mps.current_allocated_memory() + + # Intel GPUs + elif hasattr(torch, "xpu") and torch.xpu.is_available(): + for i in range(torch.xpu.device_count()): + if hasattr(torch.xpu, "memory_allocated"): + memory_used[i] = torch.xpu.memory_allocated(i) + + # NPUs + elif hasattr(torch, "npu") and torch.npu.is_available(): + for i in range(torch.npu.device_count()): + if hasattr(torch.npu, "memory_allocated"): + memory_used[i] = torch.npu.memory_allocated(i) + + return memory_used + + def update_memory_metrics(self): + """Update peak memory usage metrics.""" + # CPU memory + cpu_memory = self._process.memory_info().rss + self.metrics.peak_cpu_memory = max(self.metrics.peak_cpu_memory, cpu_memory) + + # GPU memory (if available) + memory_used = self._get_allocated_memory() + for i, memory in memory_used.items(): + self.metrics.peak_gpu_memory[i] = max( + self.metrics.peak_gpu_memory.get(i, 0), memory + ) + + def get_memory_metrics(self) -> dict[str, Any]: + """Get the current memory metrics as a dictionary.""" + memory_metrics = { + "cpu_memory_bytes": self._process.memory_info().rss, + "peak_cpu_memory_bytes": self.metrics.peak_cpu_memory, + } + + # GPU memory (if available) + memory_used = self._get_allocated_memory() + for i, memory in memory_used.items(): + memory_metrics[f"gpu_{i}_memory_bytes"] = memory + memory_metrics[f"gpu_{i}_peak_memory_bytes"] = ( + self.metrics.peak_gpu_memory.get(i, 0) + ) + + return memory_metrics diff --git a/src/axolotl/telemetry/whitelist.yaml b/src/axolotl/telemetry/whitelist.yaml new file mode 100644 index 0000000000..6c94d6e794 --- /dev/null +++ b/src/axolotl/telemetry/whitelist.yaml @@ -0,0 +1,33 @@ +organizations: + - "axolotl-ai-co" + - "meta-llama" + - "huggingface" + - "nvidia" + - "facebook" + - "google" + - "microsoft" + - "deepseek-ai" + - "HuggingFaceTB" + - "mistralai" + - "Qwen" + - "unsloth" + - "NousResearch" + - "allenai" + - "amd" + - "tiiuae" + - "tencent" + - "zai-org" + - "openai" + - "ibm-granite" + - "arcee-ai" + - "swiss-ai" + - "CohereForAI" + - "deepcogito" + - "THUDM" + - "ai21labs" + - "LiquidAI" + - "canopylabs" + - "state-spaces" + - "mistral-community" + - "llava-hf" + - "ByteDance-Seed" diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 441c508711..cce3b8a6a2 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -31,6 +31,8 @@ ) from axolotl.integrations.base import PluginManager from axolotl.loaders import ModelLoader, load_processor, load_tokenizer +from axolotl.telemetry.errors import send_errors +from axolotl.telemetry.manager import TelemetryManager from axolotl.utils.ctx_managers.sequence_parallel import SequenceParallelContextManager from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import cleanup_distributed @@ -45,6 +47,9 @@ LOG = get_logger(__name__) +TELEMETRY_MANAGER = TelemetryManager.get_instance() +PLUGIN_MANAGER = PluginManager.get_instance() + def setup_model_and_tokenizer( cfg: DictDefault, @@ -62,7 +67,10 @@ def setup_model_and_tokenizer( `None`), and processor (if multimodal, else `None`). """ # Load tokenizer - LOG.debug(f"Loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}") + LOG.debug( + f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}", + main_process_only=True, + ) tokenizer = load_tokenizer(cfg) # Load processor for multimodal models if needed @@ -78,6 +86,14 @@ def setup_model_and_tokenizer( if model.generation_config is not None: model.generation_config.do_sample = True + TELEMETRY_MANAGER.send_event( + event_type="model-load", properties=model.config.to_dict() + ) + if peft_config: + TELEMETRY_MANAGER.send_event( + event_type="peft-config-load", properties=peft_config.to_dict() + ) + # Apply freezing if specified if cfg.unfrozen_parameters: freeze_layers_except(model, cfg.unfrozen_parameters) @@ -196,8 +212,7 @@ def execute_training( LOG.info("Starting trainer...") trainer.train(resume_from_checkpoint=resume_from_checkpoint) - plugin_manager = PluginManager.get_instance() - plugin_manager.post_train(cfg, trainer.model) + PLUGIN_MANAGER.post_train(cfg, trainer.model) def save_trained_model( @@ -521,9 +536,7 @@ def setup_model_and_trainer( model_ref=model_ref, peft_config=peft_config, ) - - plugin_manager = PluginManager.get_instance() - plugin_manager.post_trainer_create(cfg, trainer) + PLUGIN_MANAGER.post_trainer_create(cfg, trainer) if cfg.use_ray: try: @@ -545,6 +558,7 @@ def setup_model_and_trainer( ) +@send_errors def train( cfg: DictDefault, dataset_meta: TrainDatasetMeta ) -> tuple[PeftModel | PreTrainedModel, PreTrainedTokenizer, Trainer]: @@ -595,5 +609,6 @@ def train( create_model_card(cfg, trainer) if not cfg.use_ray: cleanup_distributed() + PLUGIN_MANAGER.post_train(cfg, model) return model, tokenizer, trainer diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 5ad55f8b74..c9b087ea30 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1069,7 +1069,7 @@ def warn_peft_trainable_token_to_fix_untrained(cls, data): class AxolotlConfigWCapabilities(AxolotlInputConfig): - """wrapper to valdiate GPU capabilities with the configured options""" + """Wrapper to valdiate GPU capabilities with the configured options""" capabilities: GPUCapabilities env_capabilities: EnvCapabilities diff --git a/tests/conftest.py b/tests/conftest.py index 98847ebad8..d3b9407ec4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,4 @@ -""" -shared pytest fixtures -""" +"""Shared pytest fixtures""" import functools import importlib @@ -582,3 +580,9 @@ def test_load_fixtures( download_llama2_model_fixture, ): pass + + +@pytest.fixture(autouse=True) +def disable_telemetry(monkeypatch): + monkeypatch.setenv("AXOLOTL_DO_NOT_TRACK", "1") + yield diff --git a/tests/telemetry/__init__.py b/tests/telemetry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/telemetry/conftest.py b/tests/telemetry/conftest.py new file mode 100644 index 0000000000..47776ce906 --- /dev/null +++ b/tests/telemetry/conftest.py @@ -0,0 +1,9 @@ +"""Shared pytest fixtures for telemetry tests.""" + +import pytest + + +@pytest.fixture(autouse=True) +def del_track_env(monkeypatch): + monkeypatch.delenv("AXOLOTL_DO_NOT_TRACK", raising=False) + yield diff --git a/tests/telemetry/test_callbacks.py b/tests/telemetry/test_callbacks.py new file mode 100644 index 0000000000..97d56a9c67 --- /dev/null +++ b/tests/telemetry/test_callbacks.py @@ -0,0 +1,373 @@ +"""Tests for telemetry callback module.""" + +# pylint: disable=redefined-outer-name + +import time +from unittest.mock import MagicMock, patch + +import pytest +from transformers import TrainerControl, TrainerState, TrainingArguments + +from axolotl.telemetry.callbacks import TIME_SINCE_LAST, TelemetryCallback + + +def calc_expected_metrics(step, last_step, current_time, last_time, start_time=900.0): + """Calculate expected metrics values for tests""" + time_diff = current_time - last_time + step_diff = step - last_step + return { + "steps_per_second": ( + step_diff / time_diff if time_diff > 0 and step_diff > 0 else 0 + ), + "time_since_last_report": time_diff, + "elapsed_time": current_time - start_time, + } + + +@pytest.fixture +def mock_time(): + """Mock time.time() to have predictable values in tests""" + with patch("axolotl.telemetry.callbacks.time") as mock_time: + mock_time.time.return_value = 1000.0 + yield mock_time + + +@pytest.fixture +def mock_telemetry_manager(): + """Create a mock TelemetryManager""" + with patch("axolotl.telemetry.callbacks.TelemetryManager") as mock_manager_class: + mock_manager = MagicMock() + mock_manager_class.get_instance.return_value = mock_manager + yield mock_manager + + +@pytest.fixture +def mock_runtime_metrics_tracker(): + """Create a mock RuntimeMetricsTracker""" + with patch( + "axolotl.telemetry.callbacks.RuntimeMetricsTracker" + ) as mock_tracker_class: + mock_tracker = MagicMock() + # Set up metrics property on the tracker + mock_metrics = MagicMock() + mock_metrics.to_dict.return_value = { + "total_steps": 100, + "peak_cpu_memory_bytes": 1024, + } + mock_tracker.metrics = mock_metrics + + # Make the constructor return our mock + mock_tracker_class.return_value = mock_tracker + yield mock_tracker + + +@pytest.fixture +def training_args(): + """Create a minimal TrainingArguments instance""" + return TrainingArguments(output_dir="./output") + + +@pytest.fixture +def trainer_state(): + """Create a mock TrainerState""" + state = MagicMock(spec=TrainerState) + state.global_step = 10 + state.epoch = 0.5 # halfway through first epoch + state.log_history = [{"loss": 2.5, "learning_rate": 5e-5}] + return state + + +@pytest.fixture +def trainer_control(): + """Create a mock TrainerControl""" + return MagicMock(spec=TrainerControl) + + +# pylint: disable=unused-argument +@pytest.fixture +def callback(mock_telemetry_manager, mock_runtime_metrics_tracker): + """Create a TelemetryCallback instance with mocked dependencies""" + return TelemetryCallback() + + +class TestTelemetryCallback: + """Tests for the TelemetryCallback class.""" + + def test_initialization(self, callback, mock_runtime_metrics_tracker): + """Test callback initialization.""" + assert callback.current_epoch == -1 + assert callback.tracker == mock_runtime_metrics_tracker + assert callback.last_report_step == 0 + assert hasattr(callback, "start_time") + assert hasattr(callback, "last_report_time") + assert callback.report_interval_steps == 100 + + def test_on_train_begin( + self, + callback, + mock_telemetry_manager, + training_args, + trainer_state, + trainer_control, + ): + """Test on_train_begin sends expected event.""" + callback.on_train_begin(training_args, trainer_state, trainer_control) + + mock_telemetry_manager.send_event.assert_called_once_with( + event_type="train-start" + ) + + def test_on_train_end( + self, + callback, + mock_telemetry_manager, + training_args, + trainer_state, + trainer_control, + ): + """Test on_train_end sends expected event with metrics.""" + callback.on_train_end(training_args, trainer_state, trainer_control) + + mock_telemetry_manager.send_event.assert_called_once() + call_args = mock_telemetry_manager.send_event.call_args[1] + + assert call_args["event_type"] == "train-end" + assert "loss" in call_args["properties"] + assert call_args["properties"]["loss"] == 2.5 + assert "learning_rate" in call_args["properties"] + assert call_args["properties"]["learning_rate"] == 5e-5 + + # Check that metrics from RuntimeMetricsTracker are included + assert "total_steps" in call_args["properties"] + assert call_args["properties"]["total_steps"] == 100 + assert "peak_cpu_memory_bytes" in call_args["properties"] + assert call_args["properties"]["peak_cpu_memory_bytes"] == 1024 + + def test_on_epoch_begin( + self, + callback, + mock_runtime_metrics_tracker, + training_args, + trainer_state, + trainer_control, + ): + """Test on_epoch_begin updates epoch counter and calls tracker.""" + initial_epoch = callback.current_epoch + + callback.on_epoch_begin(training_args, trainer_state, trainer_control) + + assert callback.current_epoch == initial_epoch + 1 + mock_runtime_metrics_tracker.start_epoch.assert_called_once_with( + initial_epoch + 1 + ) + + def test_on_epoch_end( + self, + callback, + mock_runtime_metrics_tracker, + training_args, + trainer_state, + trainer_control, + ): + """Test on_epoch_end calls tracker.""" + # Set current epoch + callback.current_epoch = 2 + + callback.on_epoch_end(training_args, trainer_state, trainer_control) + + mock_runtime_metrics_tracker.end_epoch.assert_called_once_with(2) + + def test_on_step_end_no_report( + self, + callback, + mock_telemetry_manager, + mock_runtime_metrics_tracker, + training_args, + trainer_state, + trainer_control, + ): + """Test on_step_end updates tracker but doesn't report if criteria not met.""" + # Set up state to avoid reporting + trainer_state.global_step = 42 # Not divisible by report_interval_steps + callback.last_report_step = 41 # Just 1 step since last report + callback.last_report_time = time.time() # Just now + + callback.on_step_end(training_args, trainer_state, trainer_control) + + # Should update tracker + mock_runtime_metrics_tracker.update_step.assert_called_once_with(42) + + # Should not send telemetry + mock_telemetry_manager.send_event.assert_not_called() + + # Should not update last report time/step + assert callback.last_report_step == 41 + + def test_on_step_end_report_interval_steps( + self, + callback, + mock_telemetry_manager, + mock_runtime_metrics_tracker, + mock_time, + training_args, + trainer_state, + trainer_control, + ): + """Test on_step_end reports when step interval is reached.""" + # Set up state with clear values + current_step = 100 # Exactly matches report_interval_steps + last_step = 0 + start_time = 900.0 + current_time = 1000.0 + time_diff = current_time - start_time # 100 seconds + + # Configure state and callback + trainer_state.global_step = current_step + callback.report_interval_steps = 100 + callback.last_report_step = last_step + callback.start_time = start_time + callback.last_report_time = start_time + + # Mock time.time() to return consistent values + mock_time.time.return_value = current_time + + callback.on_step_end(training_args, trainer_state, trainer_control) + + # Should update tracker + mock_runtime_metrics_tracker.update_step.assert_called_once_with(current_step) + mock_runtime_metrics_tracker.update_memory_metrics.assert_called_once() + + # Should send telemetry + mock_telemetry_manager.send_event.assert_called_once() + call_args = mock_telemetry_manager.send_event.call_args[1] + assert call_args["event_type"] == "train-progress" + + # Properties should include expected values + props = call_args["properties"] + assert props["step"] == current_step + assert props["elapsed_time"] == time_diff # 1000 - 900 = 100 + assert props["time_since_last_report"] == time_diff # 1000 - 900 = 100 + assert props["steps_per_second"] == 1.0 # 100 steps / 100 seconds + + # Should update last report time/step + assert callback.last_report_step == current_step + assert callback.last_report_time == current_time + + def test_on_step_end_report_time_elapsed( + self, + callback, + mock_telemetry_manager, + mock_runtime_metrics_tracker, # pylint: disable=unused-argument + mock_time, + training_args, + trainer_state, + trainer_control, + ): + """Test on_step_end reports when enough time has elapsed.""" + # Set up state with clear values + current_step = 120 + last_step = 10 + start_time = 900.0 + current_time = 1000.0 + time_diff = TIME_SINCE_LAST + 1 # Just over the threshold + + # Configure state and callback + trainer_state.global_step = current_step + callback.report_interval_steps = 100 + callback.last_report_step = last_step + callback.start_time = start_time + callback.last_report_time = current_time - time_diff + + # Mock time.time() to return consistent values + mock_time.time.return_value = current_time + + callback.on_step_end(training_args, trainer_state, trainer_control) + + # Should send telemetry + mock_telemetry_manager.send_event.assert_called_once() + + # Properties should include expected values + props = mock_telemetry_manager.send_event.call_args[1]["properties"] + expected_metrics = calc_expected_metrics( + current_step, last_step, current_time, current_time - time_diff, start_time + ) + assert props["steps_per_second"] == expected_metrics["steps_per_second"] + assert ( + props["time_since_last_report"] + == expected_metrics["time_since_last_report"] + ) + + def test_on_step_end_first_step( + self, + callback, + mock_telemetry_manager, + mock_runtime_metrics_tracker, # pylint: disable=unused-argument + mock_time, + training_args, + trainer_state, + trainer_control, + ): + """Test on_step_end always reports on first step.""" + # Set up state with clear values + current_step = 1 # First step + last_step = 0 + start_time = 900.0 + current_time = 1000.0 + last_report_time = 999.0 # Just 1 second ago + + # Configure state and callback + trainer_state.global_step = current_step + callback.report_interval_steps = 100 + callback.last_report_step = last_step + callback.start_time = start_time + callback.last_report_time = last_report_time + + # Mock time.time() to return consistent values + mock_time.time.return_value = current_time + + callback.on_step_end(training_args, trainer_state, trainer_control) + + # Should send telemetry even though not much time has passed + mock_telemetry_manager.send_event.assert_called_once() + + # Properties should include expected values for first step + props = mock_telemetry_manager.send_event.call_args[1]["properties"] + assert props["step"] == current_step + expected_metrics = calc_expected_metrics( + current_step, last_step, current_time, last_report_time, start_time + ) + assert props["steps_per_second"] == expected_metrics["steps_per_second"] + + def test_log_history_empty( + self, + callback, + mock_telemetry_manager, + mock_runtime_metrics_tracker, # pylint: disable=unused-argument + mock_time, + training_args, + trainer_state, + trainer_control, + ): + """Test handling of empty log history.""" + # Set up state with clear values + current_step = 1 + start_time = 900.0 + current_time = 1000.0 + + # Configure state and callback + trainer_state.global_step = current_step + trainer_state.log_history = [] + callback.start_time = start_time + + # Mock time.time() to return consistent values + mock_time.time.return_value = current_time + + callback.on_step_end(training_args, trainer_state, trainer_control) + + # Should still send telemetry + mock_telemetry_manager.send_event.assert_called_once() + + # Properties should have default values for missing log data + props = mock_telemetry_manager.send_event.call_args[1]["properties"] + assert props["loss"] == 0 + assert props["learning_rate"] == 0 diff --git a/tests/telemetry/test_errors.py b/tests/telemetry/test_errors.py new file mode 100644 index 0000000000..2f0510b212 --- /dev/null +++ b/tests/telemetry/test_errors.py @@ -0,0 +1,341 @@ +"""Tests for telemetry error utilities""" + +# pylint: disable=redefined-outer-name + +from unittest.mock import MagicMock, patch + +import pytest + +from axolotl.telemetry.errors import sanitize_stack_trace, send_errors + + +@pytest.fixture(autouse=True) +def reset_error_flag(monkeypatch): + """Reset ERROR_HANDLED flag using monkeypatch""" + import axolotl.telemetry.errors + + monkeypatch.setattr(axolotl.telemetry.errors, "ERROR_HANDLED", False) + yield + monkeypatch.setattr(axolotl.telemetry.errors, "ERROR_HANDLED", False) + + +@pytest.fixture +def example_stack_trace(): + """Provide a sample stack trace with mixed paths""" + return """Traceback (most recent call last): + File "/home/user/.local/lib/python3.9/site-packages/axolotl/cli/train.py", line 83, in main + trainer = get_trainer(cfg) + File "/home/user/.local/lib/python3.9/site-packages/axolotl/train.py", line 214, in get_trainer + model = get_model(cfg, tokenizer) + File "/home/user/.local/lib/python3.9/site-packages/axolotl/utils/models.py", line 120, in get_model + raise ValueError("Model path not found") +ValueError: Model path not found +""" + + +@pytest.fixture +def windows_stack_trace(): + """Provide a sample stack trace with Windows paths""" + return """Traceback (most recent call last): + File "C:\\Users\\name\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\axolotl\\cli\\train.py", line 83, in main + trainer = get_trainer(cfg) + File "C:\\Users\\name\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\axolotl\\train.py", line 214, in get_trainer + model = get_model(cfg, tokenizer) + File "C:\\Users\\name\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\transformers\\models\\auto\\modeling_auto.py", line 482, in from_pretrained + raise ValueError(f"Unrecognized configuration class {config.__class__}") +ValueError: Unrecognized configuration class +""" + + +@pytest.fixture +def mixed_stack_trace(): + """Provide a sample stack trace with both axolotl and non-axolotl paths""" + return """Traceback (most recent call last): + File "/home/user/.local/lib/python3.9/site-packages/axolotl/cli/train.py", line 83, in main + trainer = get_trainer(cfg) + File "/home/user/.local/lib/python3.9/site-packages/transformers/trainer.py", line 520, in train + self._inner_training_loop() + File "/home/user/.local/lib/python3.9/site-packages/axolotl/utils/trainer.py", line 75, in _inner_training_loop + super()._inner_training_loop() + File "/home/user/.local/lib/python3.9/site-packages/torch/utils/data/dataloader.py", line 631, in __next__ + data = self._next_data() +RuntimeError: CUDA out of memory +""" + + +@pytest.fixture +def venv_stack_trace(): + """Provide a sample stack trace with virtual environment paths""" + return """Traceback (most recent call last): + File "/home/user/venv/lib/python3.9/site-packages/transformers/trainer.py", line 1729, in train + self._inner_training_loop() + File "/home/user/venv/lib/python3.9/site-packages/transformers/trainer.py", line 2013, in _inner_training_loop + self.accelerator.backward(loss) + File "/home/user/venv/lib/python3.9/site-packages/accelerate/accelerator.py", line 1851, in backward + self.scaler.scale(loss).backward(**kwargs) + File "/home/user/venv/lib/python3.9/site-packages/torch/_tensor.py", line 487, in backward + torch.autograd.backward(self, gradient, retain_graph, create_graph, inputs=inputs) +RuntimeError: CUDA out of memory +""" + + +@pytest.fixture +def dist_packages_stack_trace(): + """Provide a sample stack trace with dist-packages paths""" + return """Traceback (most recent call last): + File "/usr/local/lib/python3.8/dist-packages/torch/utils/data/dataloader.py", line 631, in __next__ + data = self._next_data() + File "/usr/local/lib/python3.8/dist-packages/torch/utils/data/dataloader.py", line 675, in _next_data + data = self._dataset_fetcher.fetch(index) + File "/usr/local/lib/python3.8/dist-packages/torch/utils/data/_utils/fetch.py", line 51, in fetch + data = [self.dataset[idx] for idx in possibly_batched_index] + File "/usr/local/lib/python3.8/dist-packages/datasets/arrow_dataset.py", line 2808, in __getitem__ + raise IndexError(f"Index {key} out of range for dataset of length {len(self)}.") +IndexError: Index 10000 out of range for dataset of length 9832. +""" + + +@pytest.fixture +def project_stack_trace(): + """Provide a sample stack trace from a project directory (not a virtual env)""" + return """Traceback (most recent call last): + File "/home/user/projects/myproject/run.py", line 25, in + main() + File "/home/user/projects/myproject/src/cli.py", line 45, in main + app.run() + File "/home/user/projects/myproject/src/app.py", line 102, in run + raise ValueError("Configuration missing") +ValueError: Configuration missing +""" + + +def test_sanitize_stack_trace(example_stack_trace): + """Test that sanitize_stack_trace properly preserves axolotl paths""" + sanitized = sanitize_stack_trace(example_stack_trace) + + # Check that personal paths are removed + assert "/home/user" not in sanitized + assert ".local/lib/python3.9" not in sanitized + + # Check that site-packages is preserved + assert "site-packages/axolotl/cli/train.py" in sanitized + assert "site-packages/axolotl/train.py" in sanitized + assert "site-packages/axolotl/utils/models.py" in sanitized + + # Check that error message is preserved + assert "ValueError: Model path not found" in sanitized + + +def test_sanitize_windows_paths(windows_stack_trace): + """Test that sanitize_stack_trace handles Windows paths""" + sanitized = sanitize_stack_trace(windows_stack_trace) + + # Check that personal paths are removed + assert "C:\\Users\\name" not in sanitized + assert "AppData\\Local\\Programs\\Python" not in sanitized + + # Check that both axolotl and transformers packages are preserved + assert ( + "site-packages\\axolotl\\cli\\train.py" in sanitized + or "site-packages/axolotl/cli/train.py" in sanitized + ) + assert ( + "site-packages\\axolotl\\train.py" in sanitized + or "site-packages/axolotl/train.py" in sanitized + ) + assert ( + "site-packages\\transformers\\models\\auto\\modeling_auto.py" in sanitized + or "site-packages/transformers/models/auto/modeling_auto.py" in sanitized + ) + + # Check that error message is preserved + assert "ValueError: Unrecognized configuration class" in sanitized + + +def test_sanitize_mixed_paths(mixed_stack_trace): + """Test that sanitize_stack_trace preserves all package paths""" + sanitized = sanitize_stack_trace(mixed_stack_trace) + + # Check that all package paths are preserved + assert "site-packages/axolotl/cli/train.py" in sanitized + assert "site-packages/transformers/trainer.py" in sanitized + assert "site-packages/axolotl/utils/trainer.py" in sanitized + assert "site-packages/torch/utils/data/dataloader.py" in sanitized + + # Check that error message is preserved + assert "RuntimeError: CUDA out of memory" in sanitized + + +def test_sanitize_venv_paths(venv_stack_trace): + """Test that sanitize_stack_trace preserves virtual environment package paths""" + sanitized = sanitize_stack_trace(venv_stack_trace) + + # Check that personal paths are removed + assert "/home/user/venv" not in sanitized + + # Check that all package paths are preserved + assert "site-packages/transformers/trainer.py" in sanitized + assert "site-packages/accelerate/accelerator.py" in sanitized + assert "site-packages/torch/_tensor.py" in sanitized + + # Check that error message is preserved + assert "RuntimeError: CUDA out of memory" in sanitized + + +def test_sanitize_dist_packages(dist_packages_stack_trace): + """Test that sanitize_stack_trace preserves dist-packages paths""" + sanitized = sanitize_stack_trace(dist_packages_stack_trace) + + # Check that system paths are removed + assert "/usr/local/lib/python3.8" not in sanitized + + # Check that all package paths are preserved + assert "dist-packages/torch/utils/data/dataloader.py" in sanitized + assert "dist-packages/torch/utils/data/_utils/fetch.py" in sanitized + assert "dist-packages/datasets/arrow_dataset.py" in sanitized + + # Check that error message is preserved + assert ( + "IndexError: Index 10000 out of range for dataset of length 9832." in sanitized + ) + + +def test_sanitize_project_paths(project_stack_trace): + """Test handling of project paths (non-virtual env)""" + sanitized = sanitize_stack_trace(project_stack_trace) + + # Check that personal paths are removed + assert "/home/user/projects" not in sanitized + + # For non-package paths, we should at least preserve the filename + assert "run.py" in sanitized + assert "cli.py" in sanitized + assert "app.py" in sanitized + + # Check that error message is preserved + assert "ValueError: Configuration missing" in sanitized + + +@pytest.fixture +def mock_telemetry_manager(): + """Create a mock TelemetryManager""" + with patch("axolotl.telemetry.errors.TelemetryManager") as mock_manager_class: + mock_manager = MagicMock() + mock_manager.enabled = True + mock_manager_class.get_instance.return_value = mock_manager + yield mock_manager + + +def test_send_errors_successful_execution(mock_telemetry_manager): + """Test that send_errors doesn't send telemetry for successful function execution""" + + @send_errors + def test_func(): + return "success" + + result = test_func() + assert result == "success" + mock_telemetry_manager.send_event.assert_not_called() + + +def test_send_errors_with_exception(mock_telemetry_manager): + """Test that send_errors sends telemetry when an exception occurs""" + test_error = ValueError("Test error") + + @send_errors + def test_func(): + raise test_error + + with pytest.raises(ValueError) as excinfo: + test_func() + + assert excinfo.value == test_error + mock_telemetry_manager.send_event.assert_called_once() + + # Check that the error info was passed correctly + call_args = mock_telemetry_manager.send_event.call_args[1] + assert "test_func-error" in call_args["event_type"] + assert "Test error" in call_args["properties"]["exception"] + assert "stack_trace" in call_args["properties"] + + +def test_send_errors_nested_calls(mock_telemetry_manager): + """Test that send_errors only sends telemetry once for nested decorated functions""" + + @send_errors + def inner_func(): + raise ValueError("Inner error") + + @send_errors + def outer_func(): + return inner_func() + + with pytest.raises(ValueError): + outer_func() + + # Telemetry should be sent only once for the inner function + assert mock_telemetry_manager.send_event.call_count == 1 + call_args = mock_telemetry_manager.send_event.call_args[1] + assert "inner_func-error" in call_args["event_type"] + + +def test_send_errors_telemetry_disable(): + """Test that send_errors doesn't attempt to send telemetry when disabled""" + + with patch("axolotl.telemetry.errors.TelemetryManager") as mock_manager_class: + mock_manager = MagicMock() + mock_manager.enabled = False + mock_manager_class.get_instance.return_value = mock_manager + + @send_errors + def test_func(): + raise ValueError("Test error") + + with pytest.raises(ValueError): + test_func() + + mock_manager.send_event.assert_not_called() + + +def test_error_handled_reset(): + """Test that ERROR_HANDLED flag is properly reset""" + with patch("axolotl.telemetry.errors.TelemetryManager") as mock_manager_class: + # Create and configure the mock manager + mock_manager = MagicMock() + mock_manager.enabled = True + mock_manager_class.get_instance.return_value = mock_manager + + from axolotl.telemetry.errors import ERROR_HANDLED + + @send_errors + def test_func(): + raise ValueError("Test error") + + assert not ERROR_HANDLED + + with pytest.raises(ValueError): + test_func() + + from axolotl.telemetry.errors import ERROR_HANDLED + + assert ERROR_HANDLED + + +def test_module_path_resolution(mock_telemetry_manager): + """Test that the module path is correctly resolved for the event type""" + import inspect + + current_module = inspect.getmodule(test_module_path_resolution).__name__ + + @send_errors + def test_func(): + raise ValueError("Test error") + + with pytest.raises(ValueError): + test_func() + + assert mock_telemetry_manager.send_event.called + event_type = mock_telemetry_manager.send_event.call_args[1]["event_type"] + + expected_event_type = f"{current_module}.test_func-error" + assert expected_event_type == event_type diff --git a/tests/telemetry/test_manager.py b/tests/telemetry/test_manager.py new file mode 100644 index 0000000000..2eeae2f11b --- /dev/null +++ b/tests/telemetry/test_manager.py @@ -0,0 +1,275 @@ +"""Tests for TelemetryManager class and utilities""" + +# pylint: disable=redefined-outer-name,protected-access + +import os +from unittest.mock import patch + +import pytest +import yaml + +from axolotl.telemetry.manager import TelemetryManager + + +@pytest.fixture +def mock_whitelist(tmp_path): + """Create a temporary whitelist file for testing""" + whitelist_content = { + "organizations": ["meta-llama", "mistralai"], + } + whitelist_file = tmp_path / "whitelist.yaml" + with open(whitelist_file, "w", encoding="utf-8") as f: + yaml.dump(whitelist_content, f) + + return str(whitelist_file) + + +@pytest.fixture +def telemetry_manager_class(): + """Reset the TelemetryManager singleton between tests""" + original_instance = TelemetryManager._instance + original_initialized = TelemetryManager._initialized + TelemetryManager._instance = None + TelemetryManager._initialized = False + yield TelemetryManager + TelemetryManager._instance = original_instance + TelemetryManager._initialized = original_initialized + + +@pytest.fixture +def manager(telemetry_manager_class, mock_whitelist): + """Create a TelemetryManager instance with mocked dependencies""" + with ( + patch("posthog.capture"), + patch("posthog.flush"), + patch("time.sleep"), + patch("axolotl.telemetry.manager.WHITELIST_PATH", mock_whitelist), + patch.dict(os.environ, {"RANK": "0"}), + ): + manager = telemetry_manager_class() + # Manually enable for most tests + manager.enabled = True + return manager + + +def test_singleton_instance(telemetry_manager_class): + """Test that TelemetryManager is a singleton""" + with ( + patch("posthog.capture"), + patch("time.sleep"), + patch.dict(os.environ, {"RANK": "0"}), + ): + first = telemetry_manager_class() + second = telemetry_manager_class() + assert first is second + assert telemetry_manager_class.get_instance() is first + + +def test_telemetry_enabled_by_default(telemetry_manager_class): + """Test that telemetry is enabled by default (opt-out)""" + with ( + patch.dict(os.environ, {"RANK": "0"}, clear=True), + patch("time.sleep"), + patch("logging.Logger.info"), + ): + manager = telemetry_manager_class() + assert manager.enabled + + +def test_telemetry_enabled_with_explicit_opt_in(telemetry_manager_class): + """Test that telemetry is enabled when AXOLOTL_DO_NOT_TRACK=0""" + with ( + patch.dict(os.environ, {"AXOLOTL_DO_NOT_TRACK": "0", "RANK": "0"}), + patch("time.sleep"), + ): + manager = telemetry_manager_class() + assert manager.enabled + + +def test_telemetry_disabled_with_axolotl_do_not_track(telemetry_manager_class): + """Test that telemetry is disabled when AXOLOTL_DO_NOT_TRACK=1""" + with ( + patch.dict(os.environ, {"AXOLOTL_DO_NOT_TRACK": "1", "RANK": "0"}), + patch("time.sleep"), + ): + manager = telemetry_manager_class() + assert not manager.enabled + + +def test_telemetry_disabled_with_do_not_track(telemetry_manager_class): + """Test that telemetry is disabled when DO_NOT_TRACK=1""" + with ( + patch.dict( + os.environ, {"AXOLOTL_DO_NOT_TRACK": "0", "DO_NOT_TRACK": "1", "RANK": "0"} + ), + patch("time.sleep"), + ): + manager = telemetry_manager_class() + assert not manager.enabled + + +def test_telemetry_disabled_for_non_main_process(telemetry_manager_class): + """Test that telemetry is disabled for non-main processes""" + with ( + patch.dict(os.environ, {"AXOLOTL_DO_NOT_TRACK": "0", "RANK": "1"}), + patch("time.sleep"), + ): + manager = telemetry_manager_class() + assert not manager.enabled + + +def test_opt_in_info_displayed(telemetry_manager_class): + """Test that opt-in info is displayed when telemetry is not configured""" + with ( + patch.dict(os.environ, {"RANK": "0"}, clear=True), + patch("logging.Logger.warning") as mock_warning, + patch("time.sleep"), + ): + telemetry_manager_class() + assert any( + "Telemetry is now enabled by default" in str(call) + for call in mock_warning.call_args_list + ) + + +def test_is_whitelisted(telemetry_manager_class, mock_whitelist): + """Test org whitelist functionality""" + with ( + patch("axolotl.telemetry.manager.WHITELIST_PATH", mock_whitelist), + patch.dict(os.environ, {"AXOLOTL_DO_NOT_TRACK": "0"}), + ): + manager = telemetry_manager_class() + + # Should match organizations from the mock whitelist + assert manager._is_whitelisted("meta-llama/llama-7b") + assert manager._is_whitelisted("mistralai/mistral-7b-instruct") + # Should not match + assert not manager._is_whitelisted("unknown/model") + # Should handle case insensitively + assert manager._is_whitelisted("META-LLAMA/Llama-7B") + # Should handle empty input + assert not manager._is_whitelisted("") + + +def test_system_info_collection(manager): + """Test system information collection""" + system_info = manager._get_system_info() + + # Check essential keys + assert "os" in system_info + assert "python_version" in system_info + assert "cpu_count" in system_info + assert "memory_total" in system_info + assert "accelerator_count" in system_info + + +def test_send_event(telemetry_manager_class): + """Test basic event sending""" + with ( + patch("posthog.capture") as mock_capture, + patch.dict(os.environ, {"AXOLOTL_DO_NOT_TRACK": "0"}), + ): + manager = telemetry_manager_class() + + # Test with clean properties (no PII) + manager.send_event("test_event", {"key": "value"}) + assert mock_capture.called + assert mock_capture.call_args[1]["event"] == "test_event" + assert mock_capture.call_args[1]["properties"] == {"key": "value"} + assert mock_capture.call_args[1]["distinct_id"] == manager.run_id + + # Test with default properties (None) + mock_capture.reset_mock() + manager.send_event("simple_event") + assert mock_capture.called + assert mock_capture.call_args[1]["properties"] == {} + + +def test_send_system_info(telemetry_manager_class): + """Test sending system info""" + with ( + patch("posthog.capture") as mock_capture, + patch.dict(os.environ, {"AXOLOTL_DO_NOT_TRACK": "0"}), + ): + manager = telemetry_manager_class() + manager.send_system_info() + assert mock_capture.called + assert mock_capture.call_args[1]["event"] == "system-info" + assert mock_capture.call_args[1]["properties"] == manager.system_info + + +def test_redacted_properties(telemetry_manager_class): + """Test path redaction in send_event method""" + with ( + patch("posthog.capture") as mock_capture, + patch.dict(os.environ, {"AXOLOTL_DO_NOT_TRACK": "0"}), + ): + manager = telemetry_manager_class() + # Test with properties containing various paths and non-paths + test_properties = { + "filepath": "/home/user/sensitive/data.txt", + "windows_path": "C:\\Users\\name\\Documents\\project\\file.py", + "output_dir": "/var/lib/data", + "path_to_model": "models/llama/7b", + "message": "Training started", # Should not be redacted + "metrics": {"loss": 0.5, "accuracy": 0.95}, # Should not be redacted + "base_model": "models/local_model", + "nested": { + "model_path": "/models/my_model", + "root_dir": "/home/user/projects", + "stats": {"steps": 1000, "epochs": 3}, # Should not be redacted + }, + } + + manager.send_event("test_event", test_properties) + + # Verify the call was made + assert mock_capture.called + + # Get the sanitized properties that were sent + sanitized = mock_capture.call_args[1]["properties"] + + # Check that path-like and base_model keys were redacted + assert sanitized["filepath"] == "[REDACTED]" + assert sanitized["windows_path"] == "[REDACTED]" + assert sanitized["path_to_model"] == "[REDACTED]" + assert sanitized["base_model"] == "[REDACTED]" + + # Check that non-path values were preserved + assert sanitized["message"] == "Training started" + assert sanitized["metrics"] == {"loss": 0.5, "accuracy": 0.95} + + # Check nested structure handling + assert sanitized["nested"]["model_path"] == "[REDACTED]" + assert sanitized["nested"]["root_dir"] == "[REDACTED]" + assert sanitized["nested"]["stats"] == {"steps": 1000, "epochs": 3} + + +def test_disable_telemetry(manager): + """Test that disabled telemetry doesn't send events""" + with patch("posthog.capture") as mock_capture: + manager.enabled = False + manager.send_event("test_event") + assert not mock_capture.called + + +def test_exception_handling_during_send(manager): + """Test that exceptions in PostHog are handled gracefully""" + with ( + patch("posthog.capture", side_effect=Exception("Test error")), + patch("logging.Logger.warning") as mock_warning, + ): + manager.send_event("test_event") + warning_logged = False + for call in mock_warning.call_args_list: + if "Failed to send telemetry event" in str(call): + warning_logged = True + break + assert warning_logged + + +def test_shutdown(manager): + """Test shutdown behavior""" + with patch("posthog.shutdown") as mock_shutdown: + manager.shutdown() + assert mock_shutdown.called diff --git a/tests/telemetry/test_runtime_metrics.py b/tests/telemetry/test_runtime_metrics.py new file mode 100644 index 0000000000..c8916e072e --- /dev/null +++ b/tests/telemetry/test_runtime_metrics.py @@ -0,0 +1,357 @@ +"""Tests for runtime metrics telemetry module""" + +# pylint: disable=redefined-outer-name + +from unittest.mock import MagicMock, patch + +import pytest + +from axolotl.telemetry.runtime_metrics import RuntimeMetrics, RuntimeMetricsTracker + + +@pytest.fixture +def mock_time(): + """Mock time.time() to have predictable values in tests""" + with patch("time.time") as mock_time: + # Start with time 1000.0 and increment by 10 seconds on each call + times = [1000.0 + i * 10 for i in range(10)] + mock_time.side_effect = times + yield mock_time + + +@pytest.fixture +def mock_telemetry_manager(): + """Create a mock TelemetryManager""" + with patch( + "axolotl.telemetry.runtime_metrics.TelemetryManager" + ) as mock_manager_class: + mock_manager = MagicMock() + mock_manager.enabled = True + mock_manager_class.get_instance.return_value = mock_manager + yield mock_manager + + +@pytest.fixture +def mock_psutil(): + """Mock psutil for memory information""" + with patch("axolotl.telemetry.runtime_metrics.psutil") as mock_psutil: + mock_process = MagicMock() + mock_memory_info = MagicMock() + # Set initial memory to 1GB + mock_memory_info.rss = 1024 * 1024 * 1024 + mock_process.memory_info.return_value = mock_memory_info + mock_psutil.Process.return_value = mock_process + yield mock_psutil + + +@pytest.fixture +def mock_torch(): + """Mock torch.cuda functions""" + with patch("axolotl.telemetry.runtime_metrics.torch") as mock_torch: + mock_torch.cuda.is_available.return_value = True + mock_torch.cuda.device_count.return_value = 2 + + # Mock memory allocated per device (1GB for device 0, 2GB for device 1) + mock_torch.cuda.memory_allocated.side_effect = ( + lambda device: (device + 1) * 1024 * 1024 * 1024 + ) + + yield mock_torch + + +class TestRuntimeMetrics: + """Tests for RuntimeMetrics class.""" + + def test_initialization(self): + """Test RuntimeMetrics initialization.""" + metrics = RuntimeMetrics(start_time=1000.0) + + assert metrics.start_time == 1000.0 + assert metrics.epoch_start_times == {} + assert metrics.epoch_end_times == {} + assert metrics.peak_gpu_memory == {} + assert metrics.total_steps == 0 + assert metrics.current_epoch == 0 + assert metrics.current_step == 0 + assert metrics.peak_cpu_memory == 0 + + def test_elapsed_time(self, mock_time): + """Test elapsed_time property.""" + metrics = RuntimeMetrics(start_time=1000.0) + + # Mock time.time() to return 1050.0 + mock_time.side_effect = [1050.0] + + assert metrics.elapsed_time == 50.0 + + def test_epoch_time(self): + """Test epoch_time method.""" + metrics = RuntimeMetrics(start_time=1000.0) + + # No epoch data + assert metrics.epoch_time(0) is None + + # Add epoch start but no end + metrics.epoch_start_times[0] = 1000.0 + assert metrics.epoch_time(0) is None + + # Add epoch end + metrics.epoch_end_times[0] = 1060.0 + assert metrics.epoch_time(0) == 60.0 + + def test_average_epoch_time(self): + """Test average_epoch_time method.""" + metrics = RuntimeMetrics(start_time=1000.0) + + # No completed epochs + assert metrics.average_epoch_time() is None + + # Add one completed epoch + metrics.epoch_start_times[0] = 1000.0 + metrics.epoch_end_times[0] = 1060.0 + assert metrics.average_epoch_time() == 60.0 + + # Add second completed epoch + metrics.epoch_start_times[1] = 1060.0 + metrics.epoch_end_times[1] = 1140.0 # 80 seconds + assert metrics.average_epoch_time() == 70.0 # Average of 60 and 80 + + # Add incomplete epoch (should not affect average) + metrics.epoch_start_times[2] = 1140.0 + assert metrics.average_epoch_time() == 70.0 + + def test_steps_per_second(self, mock_time): + """Test steps_per_second method.""" + metrics = RuntimeMetrics(start_time=1000.0) + + # No steps - first call to time.time() + mock_time.side_effect = None + mock_time.return_value = 1050.0 + assert metrics.steps_per_second() is None + + # Add steps - second call to time.time() + metrics.total_steps = 100 + mock_time.return_value = 1050.0 # Keep same time for consistent result + assert metrics.steps_per_second() == 2.0 # 100 steps / 50 seconds + + def test_to_dict_basic(self, mock_time): + """Test to_dict method with basic metrics.""" + metrics = RuntimeMetrics(start_time=1000.0) + metrics.total_steps = 100 + metrics.peak_cpu_memory = 2 * 1024 * 1024 * 1024 # 2GB + + # Mock elapsed_time + mock_time.side_effect = None + mock_time.return_value = 1050.0 + + result = metrics.to_dict() + + assert result["total_time_seconds"] == 50.0 + assert result["total_steps"] == 100 + assert result["steps_per_second"] == 2.0 + assert result["epochs_completed"] == 0 + assert result["peak_cpu_memory_bytes"] == 2 * 1024 * 1024 * 1024 + assert "epoch_times" not in result + assert "gpu_memory" not in result + + def test_to_dict_with_epochs(self, mock_time): + """Test to_dict method with epoch data.""" + metrics = RuntimeMetrics(start_time=1000.0) + metrics.total_steps = 100 + + # Add epoch data + metrics.epoch_start_times[0] = 1000.0 + metrics.epoch_end_times[0] = 1060.0 + metrics.epoch_start_times[1] = 1060.0 + metrics.epoch_end_times[1] = 1140.0 + + # Mock elapsed_time + mock_time.side_effect = None + mock_time.return_value = 1150.0 + + result = metrics.to_dict() + + assert "epoch_times" in result + assert result["epoch_times"]["epoch_0_seconds"] == 60.0 + assert result["epoch_times"]["epoch_1_seconds"] == 80.0 + assert result["average_epoch_time_seconds"] == 70.0 + + def test_to_dict_with_gpu_memory(self, mock_time): + """Test to_dict method with GPU memory data.""" + metrics = RuntimeMetrics(start_time=1000.0) + metrics.peak_gpu_memory = { + 0: 1 * 1024 * 1024 * 1024, # 1GB + 1: 2 * 1024 * 1024 * 1024, # 2GB + } + + # Mock elapsed_time + mock_time.side_effect = [1050.0] + + result = metrics.to_dict() + + assert "gpu_memory" in result + assert result["gpu_memory"]["gpu_0_peak_memory_bytes"] == 1 * 1024 * 1024 * 1024 + assert result["gpu_memory"]["gpu_1_peak_memory_bytes"] == 2 * 1024 * 1024 * 1024 + + +class TestRuntimeMetricsTracker: + """Tests for RuntimeMetricsTracker class.""" + + # pylint: disable=unused-argument + def test_initialization(self, mock_time, mock_telemetry_manager): + """Test RuntimeMetricsTracker initialization.""" + tracker = RuntimeMetricsTracker() + + assert isinstance(tracker.metrics, RuntimeMetrics) + assert tracker.metrics.start_time == 1000.0 # First value from mock_time + + # pylint: disable=unused-argument + def test_start_epoch( + self, mock_time, mock_psutil, mock_torch, mock_telemetry_manager + ): + """Test start_epoch method.""" + tracker = RuntimeMetricsTracker() + + # Reset mock_time to control next value + mock_time.side_effect = [1010.0] + + tracker.start_epoch(0) + + assert tracker.metrics.current_epoch == 0 + assert tracker.metrics.epoch_start_times[0] == 1010.0 + + # Verify memory metrics were updated + assert tracker.metrics.peak_cpu_memory == 1 * 1024 * 1024 * 1024 + assert 0 in tracker.metrics.peak_gpu_memory + assert 1 in tracker.metrics.peak_gpu_memory + + # pylint: disable=unused-argument + def test_end_epoch(self, mock_time, mock_telemetry_manager): + """Test end_epoch method.""" + tracker = RuntimeMetricsTracker() + + # Start epoch 0 + mock_time.side_effect = [1010.0] + tracker.start_epoch(0) + + # End epoch 0 + mock_time.side_effect = [1060.0] + tracker.end_epoch(0) + + assert 0 in tracker.metrics.epoch_end_times + assert tracker.metrics.epoch_end_times[0] == 1060.0 + + # pylint: disable=unused-argument + def test_update_step( + self, mock_time, mock_psutil, mock_torch, mock_telemetry_manager + ): + """Test update_step method.""" + tracker = RuntimeMetricsTracker() + + # Update step to a non-multiple of 100 + tracker.update_step(42) + + assert tracker.metrics.current_step == 42 + assert tracker.metrics.total_steps == 1 + + # Memory metrics should not be updated for non-multiple of 100 + assert tracker.metrics.peak_cpu_memory == 0 + + # Update step to a multiple of 100 + tracker.update_step(100) + + assert tracker.metrics.current_step == 100 + assert tracker.metrics.total_steps == 2 + + # Memory metrics should be updated for multiple of 100 + assert tracker.metrics.peak_cpu_memory == 1 * 1024 * 1024 * 1024 + + # pylint: disable=unused-argument + def test_update_memory_metrics( + self, mock_psutil, mock_torch, mock_telemetry_manager + ): + """Test update_memory_metrics method.""" + tracker = RuntimeMetricsTracker() + + # Initial memory state + assert tracker.metrics.peak_cpu_memory == 0 + assert tracker.metrics.peak_gpu_memory == {} + + # Update memory metrics + tracker.update_memory_metrics() + + # Verify CPU memory + assert tracker.metrics.peak_cpu_memory == 1 * 1024 * 1024 * 1024 + + # Verify GPU memory + assert tracker.metrics.peak_gpu_memory[0] == 1 * 1024 * 1024 * 1024 + assert tracker.metrics.peak_gpu_memory[1] == 2 * 1024 * 1024 * 1024 + + # Change mocked memory values to be lower + mock_process = mock_psutil.Process.return_value + mock_memory_info = mock_process.memory_info.return_value + mock_memory_info.rss = 0.5 * 1024 * 1024 * 1024 # 0.5GB + + mock_torch.cuda.memory_allocated.side_effect = ( + lambda device: (device + 0.5) * 1024 * 1024 * 1024 + ) + + # Update memory metrics again + tracker.update_memory_metrics() + + # Peak values should not decrease + assert tracker.metrics.peak_cpu_memory == 1 * 1024 * 1024 * 1024 + assert tracker.metrics.peak_gpu_memory[0] == 1 * 1024 * 1024 * 1024 + assert tracker.metrics.peak_gpu_memory[1] == 2 * 1024 * 1024 * 1024 + + # Change mocked memory values to be higher + mock_memory_info.rss = 2 * 1024 * 1024 * 1024 # 2GB + + mock_torch.cuda.memory_allocated.side_effect = ( + lambda device: (device + 2) * 1024 * 1024 * 1024 + ) + + # Update memory metrics again + tracker.update_memory_metrics() + + # Peak values should increase + assert tracker.metrics.peak_cpu_memory == 2 * 1024 * 1024 * 1024 + assert tracker.metrics.peak_gpu_memory[0] == 2 * 1024 * 1024 * 1024 + assert tracker.metrics.peak_gpu_memory[1] == 3 * 1024 * 1024 * 1024 + + # pylint: disable=unused-argument + def test_get_memory_metrics(self, mock_psutil, mock_torch, mock_telemetry_manager): + """Test get_memory_metrics method.""" + tracker = RuntimeMetricsTracker() + + # Set peak memory values + tracker.metrics.peak_cpu_memory = 2 * 1024 * 1024 * 1024 + tracker.metrics.peak_gpu_memory = { + 0: 3 * 1024 * 1024 * 1024, + 1: 4 * 1024 * 1024 * 1024, + } + + # Get memory metrics + memory_metrics = tracker.get_memory_metrics() + + # Verify CPU memory + assert ( + memory_metrics["cpu_memory_bytes"] == 1 * 1024 * 1024 * 1024 + ) # Current value from mock + assert ( + memory_metrics["peak_cpu_memory_bytes"] == 2 * 1024 * 1024 * 1024 + ) # Peak value we set + + # Verify GPU memory + assert ( + memory_metrics["gpu_0_memory_bytes"] == 1 * 1024 * 1024 * 1024 + ) # Current value from mock + assert ( + memory_metrics["gpu_0_peak_memory_bytes"] == 3 * 1024 * 1024 * 1024 + ) # Peak value we set + assert ( + memory_metrics["gpu_1_memory_bytes"] == 2 * 1024 * 1024 * 1024 + ) # Current value from mock + assert ( + memory_metrics["gpu_1_peak_memory_bytes"] == 4 * 1024 * 1024 * 1024 + ) # Peak value we set From f5f21fb2161071920dd6a4d5f9c4b74da7d4dc71 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 18 Nov 2025 14:45:21 +0700 Subject: [PATCH 1041/1405] chore: update readme with latest updates (#3267) --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d6dd679886..c86ab8f4a6 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ ## 🎉 Latest Updates +- 2025/10: New model support has been added in Axolotl for: [Qwen3 Next](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/qwen3-next), [Qwen2.5-vl, Qwen3-vl](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen2_5-vl), [Qwen3, Qwen3MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3), [Granite 4](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/granite4), [HunYuan](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/hunyuan), [Magistral 2509](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral#vision), [Apertus](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/apertus), and [Seed-OSS](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/seed-oss). +- 2025/09: Axolotl now has text diffusion training. Read more [here](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations/diffusion). +- 2025/08: QAT has been updated to include NVFP4 support. See [PR](https://github.com/axolotl-ai-cloud/axolotl/pull/3107). - 2025/07: - ND Parallelism support has been added into Axolotl. Compose Context Parallelism (CP), Tensor Parallelism (TP), and Fully Sharded Data Parallelism (FSDP) within a single node and across multiple nodes. Check out the [blog post](https://huggingface.co/blog/accelerate-nd-parallel) for more info. - Axolotl adds more models: [GPT-OSS](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/gpt-oss), [Gemma 3n](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/gemma3n), [Liquid Foundation Model 2 (LFM2)](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/lfm2), and [Arcee Foundation Models (AFM)](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/afm). @@ -36,12 +39,12 @@ - [Voxtral](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/voxtral), [Magistral 1.1](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral), and [Devstral](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/devstral) with mistral-common tokenizer support has been integrated in Axolotl! - TiledMLP support for single-GPU to multi-GPU training with DDP, DeepSpeed and FSDP support has been added to support Arctic Long Sequence Training. (ALST). See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/alst) for using ALST with Axolotl! - 2025/05: Quantization Aware Training (QAT) support has been added to Axolotl. Explore the [docs](https://docs.axolotl.ai/docs/qat.html) to learn more! -- 2025/03: Axolotl has implemented Sequence Parallelism (SP) support. Read the [blog](https://huggingface.co/blog/axolotl-ai-co/long-context-with-sequence-parallelism-in-axolotl) and [docs](https://docs.axolotl.ai/docs/sequence_parallelism.html) to learn how to scale your context length when fine-tuning.

Expand older updates +- 2025/03: Axolotl has implemented Sequence Parallelism (SP) support. Read the [blog](https://huggingface.co/blog/axolotl-ai-co/long-context-with-sequence-parallelism-in-axolotl) and [docs](https://docs.axolotl.ai/docs/sequence_parallelism.html) to learn how to scale your context length when fine-tuning. - 2025/06: Magistral with mistral-common tokenizer support has been added to Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral) to start training your own Magistral models with Axolotl! - 2025/04: Llama 4 support has been added in Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/llama-4) to start training your own Llama 4 models with Axolotl's linearized version! - 2025/03: (Beta) Fine-tuning Multimodal models is now supported in Axolotl. Check out the [docs](https://docs.axolotl.ai/docs/multimodal.html) to fine-tune your own! From 0d27e14e4538846e23fc0614ae0a0505c36e6eae Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 20 Nov 2025 09:04:37 -0500 Subject: [PATCH 1042/1405] Torch 2.9.1 base images (#3268) * update torch 2.9.1 base images * update base dockerfile image check --- .github/workflows/base.yml | 8 ++++---- docker/Dockerfile-base | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 2e8950dd9e..eddce14384 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -57,14 +57,14 @@ jobs: cuda_version: 12.8.1 cudnn_version: "" python_version: "3.11" - pytorch: 2.9.0 + pytorch: 2.9.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" - cuda: "130" cuda_version: 13.0.0 cudnn_version: "" python_version: "3.11" - pytorch: 2.9.0 + pytorch: 2.9.1 torch_cuda_arch_list: "9.0+PTX" dockerfile: "Dockerfile-base" # - cuda: "128" @@ -146,14 +146,14 @@ jobs: cuda_version: 12.8.1 cudnn_version: "" python_version: "3.11" - pytorch: 2.9.0 + pytorch: 2.9.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" - cuda: "130" cuda_version: 13.0.0 cudnn_version: "" python_version: "3.11" - pytorch: 2.9.0 + pytorch: 2.9.1 torch_cuda_arch_list: "9.0+PTX" dockerfile: "Dockerfile-uv-base" steps: diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 25eae4fdea..cfd30b851c 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -51,7 +51,7 @@ RUN git lfs install --skip-repo && \ pip3 install -U --no-cache-dir pydantic==1.10.10 && \ pip3 cache purge -RUN if [ "$PYTORCH_VERSION" = "2.9.0" ] && [ "$CUDA" = "128" ] ; then \ +RUN if [ "$PYTORCH_VERSION" = "2.9.1" ] && [ "$CUDA" = "128" ] ; then \ wget https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.4.17/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ pip3 install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ From 0b635e69c51758492b97fad668316f6f3127ed4e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 20 Nov 2025 09:26:24 -0500 Subject: [PATCH 1043/1405] build docker images for 2.9.x (#3273) --- .github/workflows/main.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4f0cc4c99d..f34a0cf2f1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,6 +36,16 @@ jobs: pytorch: 2.8.0 axolotl_extras: is_latest: true + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.9.0 + axolotl_extras: + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.9.1 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -109,6 +119,16 @@ jobs: pytorch: 2.8.0 axolotl_extras: is_latest: true + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.9.0 + axolotl_extras: + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.9.1 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout From 006f226270b83565971706b75032734aa865d345 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 24 Nov 2025 10:21:31 +0700 Subject: [PATCH 1044/1405] Feat: add Olmo3 (BC with Olmo and Olmo2) (#3275) * feat: update cce to include olmo family * chore: update docs following feedback * feat: add olmo3 config * fix: clarify 3 methods * chore: add olmo to readme --- README.md | 1 + docs/multi-gpu.qmd | 28 +++++--- .../colab-axolotl-example.ipynb | 2 +- examples/olmo3/README.md | 46 +++++++++++++ examples/olmo3/olmo3-7b-qlora.yaml | 64 +++++++++++++++++++ examples/seed-oss/README.md | 26 +++----- examples/smolvlm2/README.md | 4 +- scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 5 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/monkeypatch/multipack.py | 3 + 11 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 examples/olmo3/README.md create mode 100644 examples/olmo3/olmo3-7b-qlora.yaml diff --git a/README.md b/README.md index c86ab8f4a6..1517fb874a 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ ## 🎉 Latest Updates +- 2025/11: Axolotl now includes support for [Olmo3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/olmo3). - 2025/10: New model support has been added in Axolotl for: [Qwen3 Next](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/qwen3-next), [Qwen2.5-vl, Qwen3-vl](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen2_5-vl), [Qwen3, Qwen3MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3), [Granite 4](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/granite4), [HunYuan](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/hunyuan), [Magistral 2509](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral#vision), [Apertus](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/apertus), and [Seed-OSS](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/seed-oss). - 2025/09: Axolotl now has text diffusion training. Read more [here](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations/diffusion). - 2025/08: QAT has been updated to include NVFP4 support. See [PR](https://github.com/axolotl-ai-cloud/axolotl/pull/3107). diff --git a/docs/multi-gpu.qmd b/docs/multi-gpu.qmd index 57a941b046..1b58a108c6 100644 --- a/docs/multi-gpu.qmd +++ b/docs/multi-gpu.qmd @@ -4,7 +4,7 @@ format: html: toc: true toc-depth: 3 - number-sections: true + # number-sections: true code-tools: true execute: enabled: false @@ -14,12 +14,18 @@ This guide covers advanced training configurations for multi-GPU setups using Ax ## Overview {#sec-overview} -Axolotl supports several methods for multi-GPU training: +When training on multiple GPUs, Axolotl supports 3 sharding/parallelism strategies. Additionally, you can layer specific optimization features on top of that strategy. -- DeepSpeed (recommended) -- FSDP (Fully Sharded Data Parallel) -- Sequence parallelism -- FSDP + QLoRA +You generally cannot combine these strategies; they are mutually exclusive. + +1. **DeepSpeed**: Powerful optimization library, supports ZeRO stages 1-3. +2. **FSDP (Fully Sharded Data Parallel)**: PyTorch's native sharding implementation (Recommended). +3. **DDP (Distributed Data Parallel)**: PyTorch's native parallelism implementation (Default if neither of the above are selected). + +These features can often be combined with the strategies above: + +* **Sequence Parallelism**: Splits long sequences across GPUs (Compatible with DDP, DeepSpeed, and FSDP). +* **FSDP + QLoRA**: Combines 4-bit quantization with FSDP (Specific to FSDP). ## DeepSpeed {#sec-deepspeed} @@ -65,12 +71,18 @@ Start from Stage 1 -> Stage 2 -> Stage 3. ## Fully Sharded Data Parallel (FSDP) {#sec-fsdp} +FSDP allows you to shard model parameters, gradients, and optimizer states across data parallel workers. + ::: {.callout-note} FSDP2 is recommended for new users. FSDP1 is deprecated and will be removed in an upcoming release of Axolotl. ::: +### FSDP + QLoRA {#sec-fsdp-qlora} + +For combining FSDP with QLoRA, see our [dedicated guide](fsdp_qlora.qmd). + ### Migrating from FSDP1 to FSDP2 {#sec-migrate-fsdp1-fsdp2} To migrate your config from FSDP1 to FSDP2, you must use the `fsdp_version` top-level config field to specify the FSDP version, and @@ -145,10 +157,6 @@ single sequence causes OOM errors during model training. See our [dedicated guide](sequence_parallelism.qmd) for more information. -### FSDP + QLoRA {#sec-fsdp-qlora} - -For combining FSDP with QLoRA, see our [dedicated guide](fsdp_qlora.qmd). - ## Performance Optimization {#sec-performance} ### Liger Kernel Integration {#sec-liger} diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index cea1aeda0e..57a638948f 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@8a1a0ec\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5eff953\"" ] }, { diff --git a/examples/olmo3/README.md b/examples/olmo3/README.md new file mode 100644 index 0000000000..d4dbe05a90 --- /dev/null +++ b/examples/olmo3/README.md @@ -0,0 +1,46 @@ +# Finetune Allenai's Olmo 3 with Axolotl + +[Olmo 3](https://huggingface.co/collections/allenai/olmo-3) are a family of 7B and 32B models open source models trained by The Allen Institute for Artificial Intelligence. + +This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + + Here is an example of how to install from pip: + ```bash + # Ensure you have a compatible version of Pytorch installed + pip3 install packaging setuptools wheel ninja + pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' + + # Install Cut Cross Entropy + python scripts/cutcrossentropy_install.py | sh + ``` + +2. Run the finetuning example: + +```bash +axolotl train examples/olmo3/olmo3-7b-qlora.yaml +``` + +Let us know how it goes. Happy finetuning! 🚀 + +### TIPS + +- The example config can be re-used for Olmo and Olmo 2. +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Related Resources + +- [Olmo 3 Blog](https://allenai.org/blog/olmo3) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/olmo3/olmo3-7b-qlora.yaml b/examples/olmo3/olmo3-7b-qlora.yaml new file mode 100644 index 0000000000..c8878d79f6 --- /dev/null +++ b/examples/olmo3/olmo3-7b-qlora.yaml @@ -0,0 +1,64 @@ +base_model: allenai/Olmo-3-7B-Instruct-SFT + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/seed-oss/README.md b/examples/seed-oss/README.md index 5610c1316b..aeb8635e31 100644 --- a/examples/seed-oss/README.md +++ b/examples/seed-oss/README.md @@ -6,21 +6,17 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations ## Getting started -1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Seed-OSS is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). - Here is an example of how to install from main for pip: + Here is an example of how to install from pip: + ```bash + # Ensure you have a compatible version of Pytorch installed + pip3 install packaging setuptools wheel ninja + pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' -```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0 min) -git clone https://github.com/axolotl-ai-cloud/axolotl.git -cd axolotl - -pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn]' - -# Install Cut Cross Entropy -python scripts/cutcrossentropy_install.py | sh -``` + # Install Cut Cross Entropy + python scripts/cutcrossentropy_install.py | sh + ``` 2. Run the finetuning example: @@ -41,9 +37,7 @@ Let us know how it goes. Happy finetuning! 🚀 ## Optimization Guides -- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) -- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) -- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). ## Related Resources diff --git a/examples/smolvlm2/README.md b/examples/smolvlm2/README.md index 9c0ae4836d..74c1a1c0fa 100644 --- a/examples/smolvlm2/README.md +++ b/examples/smolvlm2/README.md @@ -37,9 +37,7 @@ This guide shows how to fine-tune SmolVLM2 models with Axolotl. ## Optimization Guides -- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) -- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) -- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). ## Related Resources diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index cb498c0020..91d0f45d69 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@8a1a0ec"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5eff953"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 5c7c5166bb..4f98ac089d 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@8a1a0ec" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5eff953" ``` ## Usage @@ -65,6 +65,9 @@ plugins: - mistral3 - mixtral - mllama +- olmo +- olmo2 +- olmo3 - phi - phi3 - phi4_multimodal diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index bd0124b93f..b8f7e9da3c 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@8a1a0ec"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5eff953"`' ) diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 5d34f1935d..fdda3c3bc5 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -49,6 +49,9 @@ "seed_oss", "lfm2", "lfm2_moe", + "olmo", + "olmo2", + "olmo3", ] From 8990ca32058b61c65dfa60a0b8bfcf0ce624a75f Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:18:53 +0530 Subject: [PATCH 1045/1405] fix: removed unused "scikit-learn==1.4.2" (#3277) Co-authored-by: Ved --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 977262df50..08759279df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,7 +42,6 @@ numpy>=2.2.6 # qlora things evaluate==0.4.1 scipy -scikit-learn==1.4.2 nvidia-ml-py==12.560.30 art tensorboard From b234532d9f02e232c973aa1cf6d137530b0b8d27 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 28 Nov 2025 18:54:48 +0700 Subject: [PATCH 1046/1405] Feat: add peft_ensure_weight_tying (#3278) * feat: upgrade peft to 0.18.0 * feat: add peft_ensure_weight_tying * fix: default * chore: adjust kwarg per feedback --- requirements.txt | 2 +- src/axolotl/loaders/adapter.py | 2 ++ src/axolotl/utils/schemas/peft.py | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 08759279df..f020aaffcb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ liger-kernel==0.6.3 packaging==23.2 huggingface_hub>=0.36.0 -peft>=0.17.1 +peft>=0.18.0 tokenizers>=0.22.1 transformers==4.57.1 accelerate==1.11.0 diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index 8e8177b628..dca688bb20 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -102,6 +102,8 @@ def load_lora( lora_config_kwargs["layer_replication"] = cfg.peft_layer_replication if cfg.peft_trainable_token_indices: lora_config_kwargs["trainable_token_indices"] = cfg.peft_trainable_token_indices + if cfg.peft_ensure_weight_tying is not None: + lora_config_kwargs["ensure_weight_tying"] = cfg.peft_ensure_weight_tying # Determine the correct PEFT task type model_cls = type(model).__name__ diff --git a/src/axolotl/utils/schemas/peft.py b/src/axolotl/utils/schemas/peft.py index af22913fd5..fd16dec3f2 100644 --- a/src/axolotl/utils/schemas/peft.py +++ b/src/axolotl/utils/schemas/peft.py @@ -100,6 +100,15 @@ class LoraConfig(BaseModel): ) }, ) + peft_ensure_weight_tying: bool | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Whether to tie adapter weights for tied model weights. " + "See https://github.com/huggingface/peft/issues/2864" + ) + }, + ) qlora_sharded_model_loading: bool | None = Field( default=False, From 7fb6a947d9411867f22de9b18ee4d42acc76398f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:03:14 +0700 Subject: [PATCH 1047/1405] chore: update pre-commit hooks (#3287) Co-authored-by: SalmanMohammadi <25081738+SalmanMohammadi@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86d8927d21..3500bb0aaf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,13 +11,13 @@ repos: - id: no-commit-to-branch args: ['--branch', 'main'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.3 + rev: v0.14.7 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 + rev: v1.19.0 hooks: - id: mypy additional_dependencies: @@ -26,7 +26,7 @@ repos: 'pydantic>=2.5.3', ] - repo: https://github.com/PyCQA/bandit - rev: 1.8.6 + rev: 1.9.2 hooks: - id: bandit args: [ From c6ddcdd06ad352c2e2d770fc7e222b9c612d5755 Mon Sep 17 00:00:00 2001 From: Yohan Na Date: Mon, 1 Dec 2025 17:52:45 +0900 Subject: [PATCH 1048/1405] feat: add exaone4 chat template and update enums (#3279) * feat: add exaone4 chat template and update enums * fix: handle first message as system or tools in exaone4 chat template * Update src/axolotl/utils/chat_templates/templates/exaone4.jinja Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: lint --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: NanoCode012 --- .../chat_templates/templates/exaone4.jinja | 126 ++++++++++++++++++ src/axolotl/utils/schemas/enums.py | 1 + 2 files changed, 127 insertions(+) create mode 100644 src/axolotl/utils/chat_templates/templates/exaone4.jinja diff --git a/src/axolotl/utils/chat_templates/templates/exaone4.jinja b/src/axolotl/utils/chat_templates/templates/exaone4.jinja new file mode 100644 index 0000000000..8bfb0651b7 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/exaone4.jinja @@ -0,0 +1,126 @@ +{%- if not skip_think is defined %} + {%- set skip_think = true %} +{%- endif %} +{%- set role_indicators = { + 'user': '[|user|]\n', + 'assistant': '[|assistant|]\n', + 'system': '[|system|]\n', + 'tool': '[|tool|]\n' +} %} +{%- set end_of_turn = '[|endofturn|]\n' %} +{%- macro available_tools(tools) %} + {{- "# Available Tools" }} + {{- "\nYou can use none, one, or multiple of the following tools by calling them as functions to help with the user’s query." }} + {{- "\nHere are the tools available to you in JSON format within and tags:\n" }} + {%- for tool in tools %} + {{- "" }} + {{- tool | tojson(ensure_ascii=False) | safe }} + {{- "\n" }} + {%- endfor %} + {{- "\nFor each function call you want to make, return a JSON object with function name and arguments within and tags, like:" }} + {{- "\n{\"name\": function_1_name, \"arguments\": {argument_1_name: argument_1_value, argument_2_name: argument_2_value}}" }} + {{- "\n{\"name\": function_2_name, \"arguments\": {...}}\n..." }} + {{- "\nNote that if no argument name is specified for a tool, you can just print the argument value directly, without the argument name or JSON formatting." }} +{%- endmacro %} +{%- set ns = namespace(last_query_index = messages|length - 1) %} +{%- for message in messages %} + {%- if message.role == "user" and message.content is string %} + {%- set ns.last_query_index = loop.index0 -%} + {%- endif %} +{%- endfor %} +{%- for i in range(messages | length) %} + {%- set msg = messages[i] %} + {%- set role = msg.role %} + {%- if role not in role_indicators %} + {{- raise_exception('Unknown role: ' ~ role) }} + {%- endif %} + {# ---- Case A: If the first message is "system", handle it here alone (without continue) ---- #} + {%- if i == 0 and role == 'system' %} + {{- role_indicators['system'] }} + {{- msg.content }} + {%- if tools is defined and tools %} + {{- "\n\n" }}{{- available_tools(tools) }} + {%- endif %} + {{- end_of_turn -}} + {%- else %} + {# ---- Case B: If the first message is tools instead of system, inject the system tools preamble ---- #} + {%- if i == 0 and tools is defined and tools %} + {{- role_indicators['system'] }} + {{- available_tools(tools) }} + {{- end_of_turn -}} + {%- endif %} + {%- endif %} + {%- if role == 'assistant' %} + {{- role_indicators['assistant'] }} + {%- if msg.content %} + {%- if "" in msg.content %} + {%- set content = msg.content.split('')[-1].strip() %} + {%- set reasoning_content = msg.content.split('')[0].strip() %} + {%- if reasoning_content.startswith("") %} + {%- set reasoning_content = reasoning_content[7:].strip() %} + {%- endif %} + {%- else %} + {%- set content = msg.content %} + {%- endif %} + {%- if msg.reasoning_content %} + {%- set reasoning_content = msg.reasoning_content %} + {%- endif %} + {%- if (not skip_think and loop.last) and reasoning_content is defined %} + {{- "\n" }} + {{- reasoning_content}} + {{- "\n\n\n" }} + {%- else %} + {{- "\n\n\n\n" }} + {%- endif %} + {{- content }} + {%- endif %} + {%- if msg.tool_calls %} + {%- if msg.content %} + {{- "\n" }} + {%- else %} + {{- "\n\n\n\n" }} + {%- endif %} + {%- for tool_call in msg.tool_calls %} + {%- if tool_call.function is defined %} + {%- set tool_call = tool_call.function %} + {%- endif %} + {%- if tool_call.arguments is defined %} + {%- set arguments = tool_call.arguments %} + {%- elif tool_call.parameters is defined %} + {%- set arguments = tool_call.parameters %} + {%- else %} + {{- raise_exception('arguments or parameters are mandatory: ' ~ tool_call) }} + {%- endif %} + {{- "" }}{"name": "{{- tool_call.name }}", "arguments": {{ arguments | tojson(ensure_ascii=False) | safe }}}{{- "" }} + {%- if not loop.last %} + {{- "\n" }} + {%- endif %} + {%- endfor %} + {%- endif %} + {{- end_of_turn -}} + {%- elif role == "tool" %} + {%- if i == 0 or messages[i - 1].role != "tool" %} + {{- role_indicators['tool'] }} + {%- endif %} + {%- if msg.content is defined %} + {{- "" }}{"result": {{ msg.content | tojson(ensure_ascii=False) | safe }}}{{- "" }} + {%- endif %} + {%- if loop.last or messages[i + 1].role != "tool" %} + {{- end_of_turn -}} + {%- else %} + {{- "\n" }} + {%- endif %} + {%- else %} + {{- role_indicators[role] }} + {{- msg.content }} + {{- end_of_turn -}} + {%- endif %} +{% endfor %} +{%- if add_generation_prompt %} + {{- role_indicators['assistant'] }} + {%- if enable_thinking is defined and enable_thinking is true %} + {{- "\n" }} + {%- else %} + {{- "\n\n\n\n" }} + {%- endif %} +{%- endif %} diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index bcd03e1a29..f86d1a1916 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -58,6 +58,7 @@ class ChatTemplate(str, Enum): falcon_h1 = "falcon_h1" tokenizer_default = "tokenizer_default" exaone = "exaone" + exaone4 = "exaone4" metharme = "metharme" pixtral = "pixtral" llava = "llava" From 4a0f98e612b0b0b7c70660f5b32b9560c1956a40 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 2 Dec 2025 21:16:23 +0700 Subject: [PATCH 1049/1405] feat: upgrade liger to 0.6.4 (#3289) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f020aaffcb..21c94a3c21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ bitsandbytes==0.48.2 triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 -liger-kernel==0.6.3 +liger-kernel==0.6.4 # END section packaging==23.2 From 86d8cca1494ba4e1872c846aa2afd83cedbfd618 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 3 Dec 2025 01:12:55 +0700 Subject: [PATCH 1050/1405] Feat: add trinity by ArceeAI (#3292) --- examples/trinity/README.md | 38 +++++++++++ .../trinity/trinity-nano-preview-qlora.yaml | 67 +++++++++++++++++++ src/axolotl/common/architectures.py | 1 + src/axolotl/monkeypatch/multipack.py | 1 + 4 files changed, 107 insertions(+) create mode 100644 examples/trinity/README.md create mode 100644 examples/trinity/trinity-nano-preview-qlora.yaml diff --git a/examples/trinity/README.md b/examples/trinity/README.md new file mode 100644 index 0000000000..28b2e2b521 --- /dev/null +++ b/examples/trinity/README.md @@ -0,0 +1,38 @@ +# Finetune ArceeAI's Trinity with Axolotl + +[Trinity](https://huggingface.co/collections/arcee-ai/trinity) is a family of open weight MoE models trained by Arcee.ai. + +This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +## Getting started + +1. Install Axolotl following the main from the [installation guide](https://docs.axolotl.ai/docs/installation.html#sec-edge-build). + +2. Run the finetuning example: + + ```bash + axolotl train examples/trinity/trinity-nano-preview-qlora.yaml + ``` + +This config uses about 24.9 GiB VRAM. + +Let us know how it goes. Happy finetuning! 🚀 + +### TIPS + +- For inference, the official Arcee.ai team recommends `top_p: 0.75`, `temperature: 0.15`, `top_k: 50`, and `min_p: 0.06`. +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Related Resources + +- [Trinity Blog](https://www.arcee.ai/blog/the-trinity-manifesto) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/trinity/trinity-nano-preview-qlora.yaml b/examples/trinity/trinity-nano-preview-qlora.yaml new file mode 100644 index 0000000000..43263cabd7 --- /dev/null +++ b/examples/trinity/trinity-nano-preview-qlora.yaml @@ -0,0 +1,67 @@ +base_model: arcee-ai/Trinity-Nano-Preview +trust_remote_code: true + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +# CCE - N/A as of now +# plugins: +# - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +# flash_attention: true # Not supported +sdp_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/src/axolotl/common/architectures.py b/src/axolotl/common/architectures.py index c8a2f0836c..f4d6ca9287 100644 --- a/src/axolotl/common/architectures.py +++ b/src/axolotl/common/architectures.py @@ -17,4 +17,5 @@ "deepseek_v3": "DeepseekV3MoE", "gpt_oss": "GptOssDecoderLayer", "lfm2_moe": "Lfm2MoeSparseMoeBlock", + "afmoe": "AfmoeMoE", } diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index fdda3c3bc5..9642b1edba 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -52,6 +52,7 @@ "olmo", "olmo2", "olmo3", + "afmoe", ] From 2b66ee189c19a659e3aea24473388fd98b522b47 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 4 Dec 2025 20:32:08 +0700 Subject: [PATCH 1051/1405] Feat: add ministral3 (#3297) * feat: add ministral and mistral3 * chore: lint * feat: update cce for ministral * fix: add vram usage * feat: update for release * fix: save_pretrained issue in v5 * fix: add instructions to use v5 branch * fix: add to multipack * fix: improve instructions * fix: add model to readme --- README.md | 2 +- .../colab-axolotl-example.ipynb | 2 +- examples/magistral/README.md | 2 +- examples/ministral/README.md | 58 +++++++++++ examples/ministral/ministral-small-qlora.yaml | 67 +++++++++++++ examples/ministral/think/README.md | 99 +++++++++++++++++++ .../think/ministral3-small-think-qlora.yaml | 67 +++++++++++++ examples/olmo3/README.md | 20 ++-- scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 4 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/monkeypatch/multipack.py | 2 + .../utils/mistral/mistral_tokenizer.py | 7 ++ 13 files changed, 314 insertions(+), 20 deletions(-) create mode 100644 examples/ministral/README.md create mode 100644 examples/ministral/ministral-small-qlora.yaml create mode 100644 examples/ministral/think/README.md create mode 100644 examples/ministral/think/ministral3-small-think-qlora.yaml diff --git a/README.md b/README.md index 1517fb874a..13518f2a80 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ## 🎉 Latest Updates -- 2025/11: Axolotl now includes support for [Olmo3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/olmo3). +- 2025/12: Axolotl now includes support for [Olmo3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/olmo3), [Trinity](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/trinity), and [Ministral3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/ministral). - 2025/10: New model support has been added in Axolotl for: [Qwen3 Next](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/qwen3-next), [Qwen2.5-vl, Qwen3-vl](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen2_5-vl), [Qwen3, Qwen3MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3), [Granite 4](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/granite4), [HunYuan](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/hunyuan), [Magistral 2509](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral#vision), [Apertus](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/apertus), and [Seed-OSS](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/seed-oss). - 2025/09: Axolotl now has text diffusion training. Read more [here](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations/diffusion). - 2025/08: QAT has been updated to include NVFP4 support. See [PR](https://github.com/axolotl-ai-cloud/axolotl/pull/3107). diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 57a638948f..06705eb3df 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5eff953\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f643b88\"" ] }, { diff --git a/examples/magistral/README.md b/examples/magistral/README.md index a09138744a..40a793f109 100644 --- a/examples/magistral/README.md +++ b/examples/magistral/README.md @@ -13,7 +13,7 @@ Thanks to the team at MistralAI for giving us early access to prepare for these Here is an example of how to install from pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0 min) +# Ensure you have Pytorch installed (Pytorch 2.7.0 min) pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` diff --git a/examples/ministral/README.md b/examples/ministral/README.md new file mode 100644 index 0000000000..b088c06ec2 --- /dev/null +++ b/examples/ministral/README.md @@ -0,0 +1,58 @@ +# Finetune Ministral with Axolotl + +Ministral is a family of openweight models from MistralAI found on HuggingFace at [2410](mistralai/Ministral-8B-Instruct-2410) and [2512](https://huggingface.co/collections/mistralai/ministral-3) (see [Thinking](#thinking)). This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +3. Run the finetuning example: + + ```bash + axolotl train examples/ministral/ministral-small-qlora.yaml + ``` + +This config uses about 8.76 GiB VRAM. + +Let us know how it goes. Happy finetuning! 🚀 + +### Thinking + +MistralAI has released their [Ministral3 2512](https://huggingface.co/collections/mistralai/ministral-3) model with thinking capabilities, enabling Chain-of-Thought reasoning with explicit thinking steps. + +📚 **[See the Thinking fine-tuning guide →](./think/README.md)** + +For Ministral3 Base/Instruct, you can reuse the above config to train supervised finetuning. + +### Tips + +- We recommend adding the same/similar SystemPrompt that the model is tuned for. You can find this within the repo's files titled `SYSTEM_PROMPT.txt`. +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The text dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Limitations + +We only support the `mistral-common` tokenizer for Supervised Fine-tuning at the moment and for `type: chat_template` only. + +In addition, we do not support overriding tokens yet. + +## Related Resources + +- [MistralAI Ministral Blog](https://mistral.ai/news/ministraux) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) + + +## Future Work + +- Add parity to Preference Tuning, RL, etc. +- Add parity to other tokenizer configs like overriding tokens. diff --git a/examples/ministral/ministral-small-qlora.yaml b/examples/ministral/ministral-small-qlora.yaml new file mode 100644 index 0000000000..0d5300ef69 --- /dev/null +++ b/examples/ministral/ministral-small-qlora.yaml @@ -0,0 +1,67 @@ +base_model: mistralai/Ministral-8B-Instruct-2410 + +# Enable to use mistral-common tokenizer +tokenizer_use_mistral_common: true + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/ministral/think/README.md b/examples/ministral/think/README.md new file mode 100644 index 0000000000..0ee5ea8767 --- /dev/null +++ b/examples/ministral/think/README.md @@ -0,0 +1,99 @@ +# Ministral3 2512 Thinking Fine-tuning + +This guide covers fine-tuning [Ministral3 2512](https://huggingface.co/collections/mistralai/ministral-3) with thinking capabilities using Axolotl. The thinking model enables explicit Chain-of-Thought reasoning with separate thinking and response sections. + +Thanks to the team at MistralAI for giving us early access to prepare for these releases. + +## Prerequisites + +Before starting, ensure you have: +- Installed Axolotl (see [main README](../README.md)) + +## Getting Started + +1. Install transformers v5 + + ```bash + pip install transformers==5.0.0rc0 + ``` + + Note: This is still experimental in Axolotl. Other stuff may break. + +2. Upgrade `mistral-common` + + ```bash + pip install mistral-common==1.8.6 + ``` + +3. Swap to the Axolotl transformers v5 branch + + ```bash + # copy examples/ministral/think/ministral3-small-think-qlora.yaml somewhere + cp examples/ministral/think/ministral3-small-think-qlora.yaml ministral3-small-think-qlora.yaml + + git fetch + git checkout transformers-v5 + ``` + +4. Run the thinking model fine-tuning: + + ```bash + axolotl train ministral3-small-think-qlora.yaml + ``` + +This config uses about 4.76 GiB VRAM. + +### Tips + +- Dataset uses multi-content format with `type: thinking` support. See [Dataset Format](#dataset-format) below. +- You cannot mix `content: str` and `content: list[dict]`, otherwise, dataset loading will fail. Keep it consistent. + +## Dataset Format + +The thinking model requires the multi-content dataset format with support for an extra `role: thinking` within system and assistant messages. + +Example format: + +```json +{ + "messages": [ + { + "role": "system", + "content": [ + { "type": "text", "text": "{SYSTEM_PROMPT}"} + ] + }, + { + "role": "user", + "content": [ + { "type": "text", "text": "Solve this step by step: What is 15% of 240?"} + ] + }, + { + "role": "assistant", + "content": [ + { + "type": "thinking", + "thinking": "I need to calculate 15% of 240. First, I'll convert 15% to decimal: 0.15. Then multiply: 0.15 × 240 = 36." + }, + { + "type": "text", + "text": "To find 15% of 240, I'll multiply 240 by 0.15:\n\n240 × 0.15 = 36\n\nTherefore, 15% of 240 is 36." + } + ] + } + ] +} +``` + +### Advanced Options + +The `thinking` section supports an optional `closed` parameter: + +```json +{ + "type": "thinking", + "thinking": "Internal reasoning here...", + "closed": true // Default: true, controls adding the closing [/THINK] tag +} +``` diff --git a/examples/ministral/think/ministral3-small-think-qlora.yaml b/examples/ministral/think/ministral3-small-think-qlora.yaml new file mode 100644 index 0000000000..987c0bd549 --- /dev/null +++ b/examples/ministral/think/ministral3-small-think-qlora.yaml @@ -0,0 +1,67 @@ +base_model: mistralai/Ministral-3-3B-Reasoning-2512 + +# Enable to use mistral-common tokenizer +tokenizer_use_mistral_common: true + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: Nanobit/text-think-2k-test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/olmo3/README.md b/examples/olmo3/README.md index d4dbe05a90..2f98eb73e2 100644 --- a/examples/olmo3/README.md +++ b/examples/olmo3/README.md @@ -6,23 +6,15 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations ## Getting started -1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). - Here is an example of how to install from pip: - ```bash - # Ensure you have a compatible version of Pytorch installed - pip3 install packaging setuptools wheel ninja - pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' - - # Install Cut Cross Entropy - python scripts/cutcrossentropy_install.py | sh - ``` +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. -2. Run the finetuning example: +3. Run the finetuning example: -```bash -axolotl train examples/olmo3/olmo3-7b-qlora.yaml -``` + ```bash + axolotl train examples/olmo3/olmo3-7b-qlora.yaml + ``` Let us know how it goes. Happy finetuning! 🚀 diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 91d0f45d69..ec5c6d4750 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5eff953"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f643b88"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 4f98ac089d..2c5b0f6e57 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5eff953" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f643b88" ``` ## Usage @@ -61,6 +61,8 @@ plugins: - llama4 - llama4_text - llava +- ministral +- ministral3 - mistral - mistral3 - mixtral diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index b8f7e9da3c..98a1659b1c 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5eff953"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f643b88"`' ) diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 9642b1edba..6a6b935bef 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -52,6 +52,8 @@ "olmo", "olmo2", "olmo3", + "ministral", + "ministral3", "afmoe", ] diff --git a/src/axolotl/utils/mistral/mistral_tokenizer.py b/src/axolotl/utils/mistral/mistral_tokenizer.py index 0414ece78e..af174cdac0 100644 --- a/src/axolotl/utils/mistral/mistral_tokenizer.py +++ b/src/axolotl/utils/mistral/mistral_tokenizer.py @@ -218,3 +218,10 @@ def from_pretrained( model_input_names=model_input_names, clean_up_tokenization_spaces=clean_up_tokenization_spaces, ) + + def save_pretrained(self, *args, **kwargs) -> tuple[str, ...]: + """ + Patches to remove save_jinja_files from being passed onwards. + """ + kwargs.pop("save_jinja_files", None) + return super().save_pretrained(*args, **kwargs) From 5992e607a2e59dced8b0ccb520527b1ad57c94f7 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 4 Dec 2025 21:44:44 +0700 Subject: [PATCH 1052/1405] fix: improve ministral3 docs to be clearer (#3300) * fix: improve ministral3 docs to be clearer * fix: title * chore: wording --- examples/ministral/README.md | 10 +-- examples/ministral3/README.md | 79 +++++++++++++++++++ examples/ministral3/ministral3-3b-qlora.yaml | 67 ++++++++++++++++ .../{ministral => ministral3}/think/README.md | 34 +------- .../think/ministral3-3b-think-qlora.yaml} | 0 examples/ministral3/vision/README.md | 57 +++++++++++++ .../vision/ministral3-3b-vision-qlora.yml | 64 +++++++++++++++ 7 files changed, 272 insertions(+), 39 deletions(-) create mode 100644 examples/ministral3/README.md create mode 100644 examples/ministral3/ministral3-3b-qlora.yaml rename examples/{ministral => ministral3}/think/README.md (72%) rename examples/{ministral/think/ministral3-small-think-qlora.yaml => ministral3/think/ministral3-3b-think-qlora.yaml} (100%) create mode 100644 examples/ministral3/vision/README.md create mode 100644 examples/ministral3/vision/ministral3-3b-vision-qlora.yml diff --git a/examples/ministral/README.md b/examples/ministral/README.md index b088c06ec2..f8af7bf27f 100644 --- a/examples/ministral/README.md +++ b/examples/ministral/README.md @@ -1,6 +1,6 @@ # Finetune Ministral with Axolotl -Ministral is a family of openweight models from MistralAI found on HuggingFace at [2410](mistralai/Ministral-8B-Instruct-2410) and [2512](https://huggingface.co/collections/mistralai/ministral-3) (see [Thinking](#thinking)). This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. +Ministral is a family of openweight models from MistralAI found on [HuggingFace](mistralai/Ministral-8B-Instruct-2410). This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. ## Getting started @@ -18,14 +18,6 @@ This config uses about 8.76 GiB VRAM. Let us know how it goes. Happy finetuning! 🚀 -### Thinking - -MistralAI has released their [Ministral3 2512](https://huggingface.co/collections/mistralai/ministral-3) model with thinking capabilities, enabling Chain-of-Thought reasoning with explicit thinking steps. - -📚 **[See the Thinking fine-tuning guide →](./think/README.md)** - -For Ministral3 Base/Instruct, you can reuse the above config to train supervised finetuning. - ### Tips - We recommend adding the same/similar SystemPrompt that the model is tuned for. You can find this within the repo's files titled `SYSTEM_PROMPT.txt`. diff --git a/examples/ministral3/README.md b/examples/ministral3/README.md new file mode 100644 index 0000000000..6ed7efda57 --- /dev/null +++ b/examples/ministral3/README.md @@ -0,0 +1,79 @@ +# Finetune Ministral3 with Axolotl + +Ministral3 is a family of open-weight models from MistralAI found on [HuggingFace](https://huggingface.co/collections/mistralai/ministral-3). This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +Please see [Thinking](#thinking) and [Vision](#vision) for their respective fine-tuning. + +Thanks to the team at MistralAI for giving us early access to prepare for these releases. + +Note: This is still experimental given it is based on transformers v5 RC. + +## Getting started + +1. Install Axolotl from source following the [installation guide](https://docs.axolotl.ai/docs/installation.html#sec-edge-build). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +3. Swap to the Axolotl transformers v5 branch + + ```bash + cp examples/ministral3/ministral3-3b-qlora.yaml ministral3-3b-qlora.yaml + + git fetch + git checkout transformers-v5 + + # Install packages for transformers v5 + pip install -e . + ``` + +4. Run the fine-tuning: + + ```bash + axolotl train ministral3-3b-qlora.yaml + ``` + +Let us know how it goes. Happy finetuning! 🚀 + + +### Tips + +- We recommend adding the same/similar SystemPrompt that the model is tuned for. You can find this within the repo's files titled `SYSTEM_PROMPT.txt`. +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The text dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +### Thinking + +Ministral3 2512 model supports thinking capabilities, enabling Chain-of-Thought reasoning with explicit thinking steps. + +📚 **[See the Thinking fine-tuning guide →](./think/README.md)** + +### Vision + +Ministral3 2512 model also supports vision capabilities. + +📚 **[See the Vision fine-tuning guide →](./vision/README.md)** + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Limitations + +We only support the `mistral-common` tokenizer for Supervised Fine-tuning at the moment and for `type: chat_template` only. + +In addition, we do not support overriding tokens yet. + +## Related Resources + +- [MistralAI Mistral3 Blog](https://mistral.ai/news/mistral-3) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) + + +## Future Work + +- Add parity to Preference Tuning, RL, etc. +- Add parity to other tokenizer configs like overriding tokens. diff --git a/examples/ministral3/ministral3-3b-qlora.yaml b/examples/ministral3/ministral3-3b-qlora.yaml new file mode 100644 index 0000000000..a31545ab2e --- /dev/null +++ b/examples/ministral3/ministral3-3b-qlora.yaml @@ -0,0 +1,67 @@ +base_model: mistralai/Ministral-3-3B-Reasoning-2512 + +# Enable to use mistral-common tokenizer +tokenizer_use_mistral_common: true + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/ministral/think/README.md b/examples/ministral3/think/README.md similarity index 72% rename from examples/ministral/think/README.md rename to examples/ministral3/think/README.md index 0ee5ea8767..8c40adbb9a 100644 --- a/examples/ministral/think/README.md +++ b/examples/ministral3/think/README.md @@ -2,8 +2,6 @@ This guide covers fine-tuning [Ministral3 2512](https://huggingface.co/collections/mistralai/ministral-3) with thinking capabilities using Axolotl. The thinking model enables explicit Chain-of-Thought reasoning with separate thinking and response sections. -Thanks to the team at MistralAI for giving us early access to prepare for these releases. - ## Prerequisites Before starting, ensure you have: @@ -11,35 +9,11 @@ Before starting, ensure you have: ## Getting Started -1. Install transformers v5 - - ```bash - pip install transformers==5.0.0rc0 - ``` - - Note: This is still experimental in Axolotl. Other stuff may break. - -2. Upgrade `mistral-common` - - ```bash - pip install mistral-common==1.8.6 - ``` +Run the thinking model fine-tuning: -3. Swap to the Axolotl transformers v5 branch - - ```bash - # copy examples/ministral/think/ministral3-small-think-qlora.yaml somewhere - cp examples/ministral/think/ministral3-small-think-qlora.yaml ministral3-small-think-qlora.yaml - - git fetch - git checkout transformers-v5 - ``` - -4. Run the thinking model fine-tuning: - - ```bash - axolotl train ministral3-small-think-qlora.yaml - ``` +```bash +axolotl train examples/ministral3/think/ministral3-3b-think-qlora.yaml +``` This config uses about 4.76 GiB VRAM. diff --git a/examples/ministral/think/ministral3-small-think-qlora.yaml b/examples/ministral3/think/ministral3-3b-think-qlora.yaml similarity index 100% rename from examples/ministral/think/ministral3-small-think-qlora.yaml rename to examples/ministral3/think/ministral3-3b-think-qlora.yaml diff --git a/examples/ministral3/vision/README.md b/examples/ministral3/vision/README.md new file mode 100644 index 0000000000..369b0116a5 --- /dev/null +++ b/examples/ministral3/vision/README.md @@ -0,0 +1,57 @@ +# Ministral3 2512 Vision Fine-tuning + +This guide covers fine-tuning [Ministral3 2512](https://huggingface.co/collections/mistralai/ministral-3) with vision capabilities using Axolotl. + +## Prerequisites + +Before starting, ensure you have: +- Installed Axolotl from source (see [main README](../README.md#getting-started)) + +## Getting started + +1. Install the required vision lib: + ```bash + pip install 'mistral-common[opencv]==1.8.6' + ``` + +2. Download the example dataset image: + ```bash + wget https://huggingface.co/datasets/Nanobit/text-vision-2k-test/resolve/main/African_elephant.jpg + ``` + +3. Run the fine-tuning: + ```bash + axolotl train examples/ministral3/vision/ministral3-3b-vision-qlora.yml + ``` + +WARNING: The loss and grad norm will be much higher than normal at first. We suspect this to be inherent to the model as of the moment. If anyone would like to submit a fix for this, we are happy to take a look. + +### Tips + +Key differences from text-only model: +- Multi-modal dataset format required +- Sample packing not supported + +## Dataset Format + +The vision model requires multi-modal dataset format as documented [here](https://docs.axolotl.ai/docs/multimodal.html#dataset-format). + +One exception is that, passing `"image": PIL.Image` is not supported. MistralTokenizer only supports `path`, `url`, and `base64` for now. + +Example: +```json +{ + "messages": [ + {"role": "system", "content": [{ "type": "text", "text": "{SYSTEM_PROMPT}"}]}, + {"role": "user", "content": [ + { "type": "text", "text": "What's in this image?"}, + {"type": "image", "path": "path/to/image.jpg" } + ]}, + {"role": "assistant", "content": [{ "type": "text", "text": "..." }]}, + ], +} +``` + +## Limitations + +- Sample Packing is not supported for multi-modality training currently. diff --git a/examples/ministral3/vision/ministral3-3b-vision-qlora.yml b/examples/ministral3/vision/ministral3-3b-vision-qlora.yml new file mode 100644 index 0000000000..0a0fdce4aa --- /dev/null +++ b/examples/ministral3/vision/ministral3-3b-vision-qlora.yml @@ -0,0 +1,64 @@ +base_model: mistralai/Ministral-3-3B-Reasoning-2512 +processor_type: AutoProcessor + +# Enable to use mistral-common tokenizer +tokenizer_use_mistral_common: true + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_4bit: true + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +# sample dataset below requires downloading image in advance +# wget https://huggingface.co/datasets/Nanobit/text-vision-2k-test/resolve/main/African_elephant.jpg +datasets: + - path: Nanobit/text-vision-2k-test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.01 +output_dir: ./outputs/out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config From 75b20fb66f4b37e01217e01dc6fb7ef40ff5227f Mon Sep 17 00:00:00 2001 From: salman Date: Sat, 6 Dec 2025 16:27:18 +0000 Subject: [PATCH 1053/1405] Save processor in quantizer CLI (#3290) --- src/axolotl/cli/quantize.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/axolotl/cli/quantize.py b/src/axolotl/cli/quantize.py index c11bcc6d93..f4fcc6d7df 100644 --- a/src/axolotl/cli/quantize.py +++ b/src/axolotl/cli/quantize.py @@ -8,7 +8,7 @@ from transformers import AutoConfig, AutoModelForCausalLM, TorchAoConfig from axolotl.cli.config import load_cfg -from axolotl.loaders import load_tokenizer +from axolotl.loaders import load_processor, load_tokenizer from axolotl.utils.logging import get_logger from axolotl.utils.quantization import ( TorchAOQuantDType, @@ -66,6 +66,11 @@ def do_quantize( LOG.info(f"Loading model from {model_path}.") tokenizer = load_tokenizer(cfg) + + processor = None + if cfg.is_multimodal: + processor = load_processor(cfg, tokenizer) + config = AutoConfig.from_pretrained(model_path) torch_dtype = config.torch_dtype if hasattr(config, "torch_dtype") else None model = AutoModelForCausalLM.from_pretrained( @@ -107,6 +112,10 @@ def do_quantize( save_jinja_files=cfg.tokenizer_save_jinja_files, ) + if processor: + LOG.info(f"Saving processor to: {str(Path(output_dir) / 'quantized')}.") + processor.save_pretrained(str(Path(output_dir) / "quantized")) + if hub_model_id: hub_model_id = ( hub_model_id.rstrip("-") @@ -114,6 +123,8 @@ def do_quantize( ) model.push_to_hub(hub_model_id, safe_serialization=False) tokenizer.push_to_hub(hub_model_id) + if processor: + processor.push_to_hub(hub_model_id) LOG.info(f"Quantized model pushed to: {hub_model_id}.") LOG.info(f"Quantized model saved to: {str(Path(output_dir) / 'quantized')}.") From b3f4aa149fa1d2a812c728b777e87822420ecde5 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:46:18 +0530 Subject: [PATCH 1054/1405] fix bin size (#3307) * fix bin size * lint --------- Co-authored-by: Ved --- src/axolotl/utils/data/streaming.py | 3 +++ src/axolotl/utils/samplers/multipack.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/axolotl/utils/data/streaming.py b/src/axolotl/utils/data/streaming.py index 2cb35ee7c8..8b6b8a439b 100644 --- a/src/axolotl/utils/data/streaming.py +++ b/src/axolotl/utils/data/streaming.py @@ -203,6 +203,7 @@ def wrap_streaming_dataset( max_seq_length=cfg.sequence_len, batch_size=cfg.micro_batch_size, multipack_attn=multipack_attn, + bin_size=cfg.sample_packing_bin_size, ) # Set this to 1 so downstream data_loader doesn't try to increase the batch size @@ -254,6 +255,7 @@ def encode_packed_streaming( collate_fn, ds_wrapper: Callable, examples: Dict[str, List], + bin_size: int, max_seq_length: int = 2048, batch_size: int = 4, multipack_attn: Optional[bool] = True, @@ -278,6 +280,7 @@ def encode_packed_streaming( batch_max_len=batch_size * max_seq_length, drop_last=True, num_processes=1, + bin_size=bin_size, ) chunked_data = defaultdict(list) diff --git a/src/axolotl/utils/samplers/multipack.py b/src/axolotl/utils/samplers/multipack.py index 662c63caaf..436a49c798 100644 --- a/src/axolotl/utils/samplers/multipack.py +++ b/src/axolotl/utils/samplers/multipack.py @@ -260,12 +260,12 @@ def __init__( batch_size: int, # Number of bins per batch batch_max_len: int, # Maximum sequence length (bin capacity) lengths: np.ndarray, # Sequence lengths + bin_size: int, # The max number of samples that can be packed in a single bin packing_efficiency_estimate: float = 1.0, # Initial efficiency estimate drop_last: bool = True, # Whether to drop final batches (might be incomplete) num_count_samples: int = 4, # Number of times to estimate batch count sequential: bool = False, # Whether to use sequential packing group_size: int = 100_000, # Size of groups for parallel packing - bin_size: int = 200, # The max number of samples that can be packed in a single bin num_processes: int | None = None, # Number of processes for parallel packing safe_mode: bool = True, # Conservative packing to prevent training instability mp_start_method: str = "fork", @@ -343,7 +343,7 @@ def generate_batches(self, set_stats: bool = False) -> list[list[list[int]]]: lengths, bin_capacity=self.batch_max_len, group_size=self.group_size, - bin_size=self.bin_size, + bin_size=self.bin_size or self.batch_max_len, num_processes=min(4, num_processes) if num_processes else 4, safe_mode=self.safe_mode, mp_start_method=self.mp_start_method, From 4ac78aa562aacd9f3b568c5473180e5474f2354e Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 9 Dec 2025 14:31:03 +0700 Subject: [PATCH 1055/1405] fix: update qwen3 jinja tokenization off a few tokens (#3295) * fix: update qwen3 jinja tokenization off a few tokens * fix: add note on tokenization issue * fix: pop last index for mistral tokenizer --- examples/qwen3/README.md | 46 +++++++++++++++++++ .../prompt_strategies/chat_template.py | 14 +++++- .../chat_templates/templates/qwen3.jinja | 8 +++- .../utils/mistral/mistral_tokenizer.py | 3 ++ 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 examples/qwen3/README.md diff --git a/examples/qwen3/README.md b/examples/qwen3/README.md new file mode 100644 index 0000000000..a3d35881df --- /dev/null +++ b/examples/qwen3/README.md @@ -0,0 +1,46 @@ +# Finetune Qwen3 with Axolotl + +[Qwen3](https://huggingface.co/collections/Qwen/qwen3) are a family of open source models trained by Alibaba. + +This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +3. Run the finetuning example: + + ```bash + axolotl train examples/qwen3/32b-qlora.yaml + ``` + +Let us know how it goes. Happy finetuning! 🚀 + +### Chat template masking a few tokens off + +If you notice that the `chat_template` masking for assistant prompts are off by a few tokens, please ensure that you are adding the below to the yaml. + +```yaml +chat_template: qwen3 +``` + +### TIPS + +- For inference, please check the official model card as it depends on your reasoning mode. +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Related Resources + +- [Qwen3 Blog](https://qwenlm.github.io/blog/qwen3/) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 28155810f0..0fec64d81c 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -95,6 +95,7 @@ def build_prompt( add_generation_prompt=False, images=None, tools=None, + real_last_index=None, ): """ Build a prompt from a conversation. @@ -114,6 +115,9 @@ def build_prompt( if tools: chat_template_kwargs["tools"] = tools + if real_last_index: + chat_template_kwargs["real_last_index"] = real_last_index + if self.processor: if not callable(self.processor): raise TypeError("Processor must be callable") @@ -631,11 +635,17 @@ def find_turn( turns_with_empty = turns[:turn_idx] + [empty_turn] turns_with_content = turns[: turn_idx + 1] + real_last_index = len(turns) - 1 + # Generate the conversation up to the turn, with final turn replaced with dummy content - dummy_ids = self.prompter.build_prompt(turns_with_empty, tools=tools) # type: ignore + dummy_ids = self.prompter.build_prompt( + turns_with_empty, tools=tools, real_last_index=real_last_index + ) # type: ignore # Generate the conversation up to the turn, with final turn included - full_ids = self.prompter.build_prompt(turns_with_content, tools=tools) # type: ignore + full_ids = self.prompter.build_prompt( + turns_with_content, tools=tools, real_last_index=real_last_index + ) # type: ignore if not full_ids or not dummy_ids: LOG.warning(f"Empty template generated for turn {turn_idx}") diff --git a/src/axolotl/utils/chat_templates/templates/qwen3.jinja b/src/axolotl/utils/chat_templates/templates/qwen3.jinja index 09b82ed03f..77ea906e77 100644 --- a/src/axolotl/utils/chat_templates/templates/qwen3.jinja +++ b/src/axolotl/utils/chat_templates/templates/qwen3.jinja @@ -15,6 +15,12 @@ {%- endif %} {%- endif %} {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %} +{#- Determine the real last index: use provided value or default to messages length - 1 #} +{%- if real_last_index is defined and real_last_index is not none %} + {%- set ns.real_last_index = real_last_index %} +{%- else %} + {%- set ns.real_last_index = messages|length - 1 %} +{%- endif %} {%- for message in messages[::-1] %} {%- set index = (messages|length - 1) - loop.index0 %} {%- if ns.multi_step_tool and message.role == "user" and not(message.content.startswith('') and message.content.endswith('')) %} @@ -37,7 +43,7 @@ {%- endif %} {%- endif %} {%- if loop.index0 > ns.last_query_index %} - {%- if loop.last or (not loop.last and reasoning_content) %} + {%- if loop.index0 == ns.real_last_index or (loop.index0 != ns.real_last_index and reasoning_content) %} {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }} {%- else %} {{- '<|im_start|>' + message.role + '\n' + content }} diff --git a/src/axolotl/utils/mistral/mistral_tokenizer.py b/src/axolotl/utils/mistral/mistral_tokenizer.py index af174cdac0..3ce6be7800 100644 --- a/src/axolotl/utils/mistral/mistral_tokenizer.py +++ b/src/axolotl/utils/mistral/mistral_tokenizer.py @@ -80,6 +80,9 @@ def apply_chat_template( # type: ignore ) -> str | list[int]: """Patched fn to handle setting serving mode, continue_final_message, remove chat_template and add_generation_prompt kwarg""" + # pop unnecessary kwarg for mistral + kwargs.pop("real_last_index", None) + try: if add_generation_prompt: self._set_mode(ValidationMode.serving) From 2a664dc8ad178d2881bf361e33559f19f30be829 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 11 Dec 2025 11:56:40 -0500 Subject: [PATCH 1056/1405] support for xformers wheels for torch 2.9 (#3308) * support for xformers wheels for torch 2.9 * fix hf cache? * don't use hf cache from s3 * show disk free space in ci --- .github/workflows/tests.yml | 30 +++++++++++++++++------------- .runpod/Dockerfile | 1 + setup.py | 1 - src/axolotl/cli/main.py | 3 ++- src/axolotl/utils/__init__.py | 5 +++++ 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 95370ca3d3..1cbfc15e1e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -66,12 +66,12 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 - - name: Restore Cache from S3 - id: hf-cache-restore-s3 - run: | - mkdir -p /home/runner/.cache/huggingface/hub - curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xf - -C /home/runner/.cache/huggingface/hub/ --use-compress-program unzstd - +# - name: Restore Cache from S3 +# id: hf-cache-restore-s3 +# run: | +# mkdir -p ~/.cache/huggingface/hub +# curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xf - -C ~/.cache/huggingface/hub/ --use-compress-program unzstd +# - name: Setup Python uses: actions/setup-python@v5 with: @@ -113,9 +113,13 @@ jobs: - name: Run tests run: | + df -h pytest -v --durations=10 -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ --ignore=tests/monkeypatch/ tests/ --cov=axolotl --cov-report=xml + df -h pytest -v --durations=10 tests/monkeypatch/ --cov=axolotl --cov-append --cov-report=xml + df -h pytest -v --durations=10 tests/patched/ --cov=axolotl --cov-append --cov-report=xml + df -h pytest -v --durations=10 tests/cli/ --cov=axolotl --cov-append --cov-report=xml - name: Upload coverage to Codecov @@ -145,12 +149,12 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 - - name: Restore Cache from S3 - id: hf-cache-restore-s3 - run: | - mkdir -p /home/runner/.cache/huggingface/hub - curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xf - -C /home/runner/.cache/huggingface/hub/ --use-compress-program unzstd - +# - name: Restore Cache from S3 +# id: hf-cache-restore-s3 +# run: | +# mkdir -p ~/.cache/huggingface/hub +# curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xf - -C ~/.cache/huggingface/hub/ --use-compress-program unzstd +# - name: Setup Python uses: actions/setup-python@v5 with: @@ -188,7 +192,7 @@ jobs: axolotl --help - name: Show HF cache - run: huggingface-cli scan-cache + run: hf cache scan - name: Run tests run: | diff --git a/.runpod/Dockerfile b/.runpod/Dockerfile index 107caf5f3c..948d3f78e9 100644 --- a/.runpod/Dockerfile +++ b/.runpod/Dockerfile @@ -10,6 +10,7 @@ ARG BASE_VOLUME="/runpod-volume" ENV BASE_VOLUME=$BASE_VOLUME ENV HF_DATASETS_CACHE="${BASE_VOLUME}/huggingface-cache/datasets" ENV HUGGINGFACE_HUB_CACHE="${BASE_VOLUME}/huggingface-cache/hub" +ENV HF_HUB_CACHE="${BASE_VOLUME}/huggingface-cache/hub" ENV TRANSFORMERS_CACHE="${BASE_VOLUME}/huggingface-cache/hub" COPY .runpod/src /src diff --git a/setup.py b/setup.py index a1bdd6bdf9..e22df40c82 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ def parse_requirements(extras_require_map): extras_require_map.pop("fbgemm-gpu") extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.4.1"] extras_require_map["vllm"] = ["vllm==0.11.1"] - _install_requires.pop(_install_requires.index(xformers_version)) elif (major, minor) >= (2, 8): extras_require_map.pop("fbgemm-gpu") extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.3.0"] diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index dc6cca489d..c0ac320506 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -26,7 +26,7 @@ launch_training, ) from axolotl.integrations.lm_eval.cli import lm_eval -from axolotl.utils import set_pytorch_cuda_alloc_conf +from axolotl.utils import set_misc_env, set_pytorch_cuda_alloc_conf from axolotl.utils.logging import get_logger from axolotl.utils.schemas.config import AxolotlInputConfig @@ -45,6 +45,7 @@ def cli(): print_axolotl_text_art() load_dotenv() set_pytorch_cuda_alloc_conf() + set_misc_env() @cli.command() diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index 72f8173f35..de67aadd01 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -51,6 +51,11 @@ def set_pytorch_cuda_alloc_conf(): ) +def set_misc_env(): + if os.getenv("XFORMERS_IGNORE_FLASH_VERSION_CHECK") is None: + os.environ["XFORMERS_IGNORE_FLASH_VERSION_CHECK"] = "1" + + def get_not_null(value, default=None): """ return the value if it's not None, otherwise return the default value From a1d07f42e41d819accefeff9969e0a7cbf26e52d Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 17 Dec 2025 21:12:18 +0700 Subject: [PATCH 1057/1405] Fix(misc): address PYTORCH_CUDA_ALLOC_CONF deprecate (#3313) * fix: leftover ministral docs changes * fix: pytorch_cuda_alloc_conf deprecation * fix: set old PYTORCH_CUDA_ALLOC_CONF env too * handle 2.9 separately --------- Co-authored-by: Wing Lian --- README.md | 2 +- .../colab-axolotl-example.ipynb | 1 - requirements.txt | 2 +- src/axolotl/utils/__init__.py | 20 +++++++++++++------ 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 13518f2a80..2858672158 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ## 🎉 Latest Updates -- 2025/12: Axolotl now includes support for [Olmo3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/olmo3), [Trinity](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/trinity), and [Ministral3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/ministral). +- 2025/12: Axolotl now includes support for [Olmo3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/olmo3), [Trinity](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/trinity), and [Ministral3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/ministral3). - 2025/10: New model support has been added in Axolotl for: [Qwen3 Next](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/qwen3-next), [Qwen2.5-vl, Qwen3-vl](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen2_5-vl), [Qwen3, Qwen3MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3), [Granite 4](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/granite4), [HunYuan](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/hunyuan), [Magistral 2509](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral#vision), [Apertus](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/apertus), and [Seed-OSS](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/seed-oss). - 2025/09: Axolotl now has text diffusion training. Read more [here](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations/diffusion). - 2025/08: QAT has been updated to include NVFP4 support. See [PR](https://github.com/axolotl-ai-cloud/axolotl/pull/3107). diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 06705eb3df..77a4154e2c 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -253,7 +253,6 @@ "source": [ "from axolotl.utils import set_pytorch_cuda_alloc_conf\n", "\n", - "# Set \"PYTORCH_CUDA_ALLOC_CONF\" env to save memory\n", "set_pytorch_cuda_alloc_conf()" ] }, diff --git a/requirements.txt b/requirements.txt index 21c94a3c21..0989325acf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -72,4 +72,4 @@ axolotl-contribs-mit==0.0.5 # telemetry posthog==6.7.11 -mistral-common==1.8.5 +mistral-common==1.8.6 diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index de67aadd01..335049158b 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -41,14 +41,22 @@ def get_pytorch_version() -> tuple[int, int, int]: def set_pytorch_cuda_alloc_conf(): - """Set up CUDA allocation config if using PyTorch >= 2.2""" + """Set up CUDA allocation config""" torch_version = torch.__version__.split(".") torch_major, torch_minor = int(torch_version[0]), int(torch_version[1]) - if torch_major == 2 and torch_minor >= 2: - if os.getenv("PYTORCH_CUDA_ALLOC_CONF") is None: - os.environ["PYTORCH_CUDA_ALLOC_CONF"] = ( - "expandable_segments:True,roundup_power2_divisions:16" - ) + config_value = "expandable_segments:True,roundup_power2_divisions:16" + if ( + torch_major == 2 + and torch_minor >= 9 + and os.getenv("PYTORCH_ALLOC_CONF") is None + ): + os.environ["PYTORCH_ALLOC_CONF"] = config_value + elif ( + torch_major == 2 + and torch_minor >= 2 + and os.getenv("PYTORCH_CUDA_ALLOC_CONF") is None + ): + os.environ["PYTORCH_CUDA_ALLOC_CONF"] = config_value def set_misc_env(): From 83d4d97dccd4acf98a77f7c4c32d4a6f32a1a064 Mon Sep 17 00:00:00 2001 From: salman Date: Wed, 17 Dec 2025 15:35:22 +0100 Subject: [PATCH 1058/1405] Add QAT NVFP4 configs for blogpost (#3280) [skip ci] * add configs for blogpost * fix configs * fixing baseline configs --- examples/qat_nvfp4/Gemma3-12B_baseline.yml | 67 +++++++++++++++++ examples/qat_nvfp4/Gemma3-12B_qat.yml | 72 ++++++++++++++++++ .../qat_nvfp4/Math-Gemma3-12B_baseline.yml | 67 +++++++++++++++++ examples/qat_nvfp4/Math-Gemma3-12B_qat.yml | 72 ++++++++++++++++++ .../qat_nvfp4/Math-Gemma3-27B_baseline.yml | 68 +++++++++++++++++ examples/qat_nvfp4/Math-Gemma3-27B_qat.yml | 73 +++++++++++++++++++ .../qat_nvfp4/Math-Qwen2.5-72B_baseline.yml | 67 +++++++++++++++++ examples/qat_nvfp4/Math-Qwen2.5-72B_qat.yml | 72 ++++++++++++++++++ examples/qat_nvfp4/Qwen2.5-72B_baseline.yml | 67 +++++++++++++++++ examples/qat_nvfp4/Qwen2.5-72B_qat.yml | 72 ++++++++++++++++++ 10 files changed, 697 insertions(+) create mode 100644 examples/qat_nvfp4/Gemma3-12B_baseline.yml create mode 100644 examples/qat_nvfp4/Gemma3-12B_qat.yml create mode 100644 examples/qat_nvfp4/Math-Gemma3-12B_baseline.yml create mode 100644 examples/qat_nvfp4/Math-Gemma3-12B_qat.yml create mode 100644 examples/qat_nvfp4/Math-Gemma3-27B_baseline.yml create mode 100644 examples/qat_nvfp4/Math-Gemma3-27B_qat.yml create mode 100644 examples/qat_nvfp4/Math-Qwen2.5-72B_baseline.yml create mode 100644 examples/qat_nvfp4/Math-Qwen2.5-72B_qat.yml create mode 100644 examples/qat_nvfp4/Qwen2.5-72B_baseline.yml create mode 100644 examples/qat_nvfp4/Qwen2.5-72B_qat.yml diff --git a/examples/qat_nvfp4/Gemma3-12B_baseline.yml b/examples/qat_nvfp4/Gemma3-12B_baseline.yml new file mode 100644 index 0000000000..be4e86635b --- /dev/null +++ b/examples/qat_nvfp4/Gemma3-12B_baseline.yml @@ -0,0 +1,67 @@ +base_model: google/gemma-3-12b-it +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true +seed: 42 +chat_template: gemma3 +datasets: + - path: tatsu-lab/alpaca + type: alpaca + +output_dir: ./outputs/out_gemma/ + +sequence_len: 8096 +sample_packing: true +flash_attention: true + +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 16 + +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 4e-5 + +bf16: true +tf32: true + +resume_from_checkpoint: +logging_steps: 1 + +# evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 +weight_decay: 0.0 +fsdp_version: 2 + +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: true + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Gemma3DecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true + +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qat_nvfp4/Gemma3-12B_qat.yml b/examples/qat_nvfp4/Gemma3-12B_qat.yml new file mode 100644 index 0000000000..7fa81163f7 --- /dev/null +++ b/examples/qat_nvfp4/Gemma3-12B_qat.yml @@ -0,0 +1,72 @@ +base_model: google/gemma-3-12b-it +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true +seed: 42 +chat_template: gemma3 +datasets: + - path: tatsu-lab/alpaca + type: alpaca + +output_dir: ./outputs/qat_out_gemma/ + +sequence_len: 8096 +sample_packing: true +flash_attention: true + +qat: + activation_dtype: nvfp4 + weight_dtype: nvfp4 + group_size: 16 # only group_size of 16 is supported with nvfp4 + +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 16 + +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 4e-5 + +bf16: true +tf32: true + +resume_from_checkpoint: +logging_steps: 1 + +evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 +weight_decay: 0.0 +fsdp_version: 2 + +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: true + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Gemma3DecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true + +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qat_nvfp4/Math-Gemma3-12B_baseline.yml b/examples/qat_nvfp4/Math-Gemma3-12B_baseline.yml new file mode 100644 index 0000000000..9f209515b3 --- /dev/null +++ b/examples/qat_nvfp4/Math-Gemma3-12B_baseline.yml @@ -0,0 +1,67 @@ +base_model: google/gemma-3-12b-it +# Math finetuning configuration for Gemma3-12B +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true +seed: 42 +chat_template: gemma3 +datasets: + - path: AI-MO/NuminaMath-CoT + type: chat_template + +output_dir: ./outputs/out_math_gemma/ + +sequence_len: 4096 +sample_packing: true +flash_attention: true + +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 8 + +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 3e-5 + +bf16: true +tf32: true + +resume_from_checkpoint: +logging_steps: 1 + +# evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 +weight_decay: 0.0 +fsdp_version: 2 + +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: true + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Gemma3DecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true + +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qat_nvfp4/Math-Gemma3-12B_qat.yml b/examples/qat_nvfp4/Math-Gemma3-12B_qat.yml new file mode 100644 index 0000000000..ef7e754be5 --- /dev/null +++ b/examples/qat_nvfp4/Math-Gemma3-12B_qat.yml @@ -0,0 +1,72 @@ +base_model: google/gemma-3-12b-it +# Math finetuning configuration for Gemma3-12B +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true +seed: 42 +chat_template: gemma3 +datasets: + - path: AI-MO/NuminaMath-CoT + type: chat_template + +output_dir: ./outputs/qat_out_math_gemma/ + +sequence_len: 4096 +sample_packing: true +flash_attention: true + +qat: + activation_dtype: nvfp4 + weight_dtype: nvfp4 + group_size: 16 # only group_size of 16 is supported with nvfp4 + +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 8 + +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 3e-5 + +bf16: true +tf32: true + +resume_from_checkpoint: +logging_steps: 1 + +# evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 +weight_decay: 0.0 +fsdp_version: 2 + +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: true + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Gemma3DecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true + +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qat_nvfp4/Math-Gemma3-27B_baseline.yml b/examples/qat_nvfp4/Math-Gemma3-27B_baseline.yml new file mode 100644 index 0000000000..3a262d342d --- /dev/null +++ b/examples/qat_nvfp4/Math-Gemma3-27B_baseline.yml @@ -0,0 +1,68 @@ +base_model: google/gemma-3-27b-it +# Math finetuning configuration for Gemma3-27B +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true +seed: 42 +chat_template: gemma3 +datasets: + - path: AI-MO/NuminaMath-CoT + type: chat_template + +output_dir: ./outputs/out_math_gemma27/ + +sequence_len: 4096 +sample_packing: true +flash_attention: true + +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 16 + +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 5e-6 +eta_min: 7e-7 + +bf16: true +tf32: true + +resume_from_checkpoint: +logging_steps: 1 + +# evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 +weight_decay: 0.0 +fsdp_version: 2 + +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: true + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Gemma3DecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true + +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qat_nvfp4/Math-Gemma3-27B_qat.yml b/examples/qat_nvfp4/Math-Gemma3-27B_qat.yml new file mode 100644 index 0000000000..87016ae9cb --- /dev/null +++ b/examples/qat_nvfp4/Math-Gemma3-27B_qat.yml @@ -0,0 +1,73 @@ +base_model: google/gemma-3-27b-it +# Math finetuning configuration for Gemma3-27B +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true +seed: 42 +chat_template: gemma3 +datasets: + - path: AI-MO/NuminaMath-CoT + type: chat_template + +output_dir: ./outputs/qat_out_math_gemma27/ + +sequence_len: 4096 +sample_packing: true +flash_attention: true + +qat: + activation_dtype: nvfp4 + weight_dtype: nvfp4 + group_size: 16 # only group_size of 16 is supported with nvfp4 + +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 16 + +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 5e-6 +eta_min: 7e-7 + +bf16: true +tf32: true + +resume_from_checkpoint: +logging_steps: 1 + +# evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 +weight_decay: 0.0 +fsdp_version: 2 + +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: true + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Gemma3DecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true + +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qat_nvfp4/Math-Qwen2.5-72B_baseline.yml b/examples/qat_nvfp4/Math-Qwen2.5-72B_baseline.yml new file mode 100644 index 0000000000..efec25c547 --- /dev/null +++ b/examples/qat_nvfp4/Math-Qwen2.5-72B_baseline.yml @@ -0,0 +1,67 @@ +base_model: Qwen/Qwen2.5-72B +# Math finetuning configuration for Qwen2.5-72B (non-instruct) +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true +seed: 42 +chat_template: qwen_25 +datasets: + - path: AI-MO/NuminaMath-CoT + type: chat_template + +output_dir: ./outputs/out_math_72b/ + +sequence_len: 4096 +sample_packing: true +flash_attention: true + +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 8 +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 5e-6 +eta_min: 7e-7 + +bf16: true +tf32: true + +resume_from_checkpoint: +logging_steps: 1 + +# evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 +weight_decay: 0.0 +fsdp_version: 2 + +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: true + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Qwen2DecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true + +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qat_nvfp4/Math-Qwen2.5-72B_qat.yml b/examples/qat_nvfp4/Math-Qwen2.5-72B_qat.yml new file mode 100644 index 0000000000..427d7af523 --- /dev/null +++ b/examples/qat_nvfp4/Math-Qwen2.5-72B_qat.yml @@ -0,0 +1,72 @@ +base_model: Qwen/Qwen2.5-72B +# Math finetuning configuration for Qwen2.5-72B (non-instruct) +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true +seed: 42 +chat_template: qwen_25 +datasets: + - path: AI-MO/NuminaMath-CoT + type: chat_template + +output_dir: ./outputs/qat_out_math_72b/ + +sequence_len: 4096 +sample_packing: true +flash_attention: true + +qat: + activation_dtype: nvfp4 + weight_dtype: nvfp4 + group_size: 16 # only group_size of 16 is supported with nvfp4 + +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 8 +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 5e-6 +eta_min: 7e-7 + +bf16: true +tf32: true + +resume_from_checkpoint: +logging_steps: 1 + +# evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 +weight_decay: 0.0 +fsdp_version: 2 + +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: true + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Qwen2DecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true + +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qat_nvfp4/Qwen2.5-72B_baseline.yml b/examples/qat_nvfp4/Qwen2.5-72B_baseline.yml new file mode 100644 index 0000000000..e1eaba61f3 --- /dev/null +++ b/examples/qat_nvfp4/Qwen2.5-72B_baseline.yml @@ -0,0 +1,67 @@ +base_model: Qwen/Qwen2.5-72B +# Alpaca finetuning configuration for Qwen2.5-72B +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true +seed: 42 +chat_template: qwen_25 +datasets: + - path: tatsu-lab/alpaca + type: alpaca + +output_dir: ./outputs/out_qwen72b/ + +sequence_len: 8096 +sample_packing: true +flash_attention: true + +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 16 + +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 2e-5 + +bf16: true +tf32: true + +resume_from_checkpoint: +logging_steps: 1 + +# evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 +weight_decay: 0.0 +fsdp_version: 2 + +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: true + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Qwen2DecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true + +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/qat_nvfp4/Qwen2.5-72B_qat.yml b/examples/qat_nvfp4/Qwen2.5-72B_qat.yml new file mode 100644 index 0000000000..dad7e54220 --- /dev/null +++ b/examples/qat_nvfp4/Qwen2.5-72B_qat.yml @@ -0,0 +1,72 @@ +base_model: Qwen/Qwen2.5-72B +# Alpaca finetuning configuration for Qwen2.5-72B +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true +seed: 42 +chat_template: qwen_25 +datasets: + - path: tatsu-lab/alpaca + type: alpaca + +output_dir: ./outputs/qat_out_qwen72b/ + +sequence_len: 8096 +sample_packing: true +flash_attention: true + +qat: + activation_dtype: nvfp4 + weight_dtype: nvfp4 + group_size: 16 # only group_size of 16 is supported with nvfp4 + +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 16 + +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 2e-5 + +bf16: true +tf32: true + +resume_from_checkpoint: +logging_steps: 1 + +# evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 +weight_decay: 0.0 +fsdp_version: 2 + +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: true + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Qwen2DecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true + +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config From 2cf254b4afc2c3eea632a09448bd6b999ca50e93 Mon Sep 17 00:00:00 2001 From: xzuyn <16216325+xzuyn@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:09:39 -0500 Subject: [PATCH 1059/1405] Add `peft_autocast_adapter_dtype` config option (#3311) [skip ci] * Add `peft_autocast_adapter_dtype` field to schema * Add `autocast_adapter_dtype` to `model_kwargs` * chore: docs --------- Co-authored-by: NanoCode012 --- src/axolotl/loaders/adapter.py | 7 +++++-- src/axolotl/utils/schemas/peft.py | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index dca688bb20..3b64b23db9 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -142,9 +142,12 @@ def load_lora( ): setup_quantized_meta_for_peft(model) + model_kwargs: Any = {} + if cfg.peft_autocast_adapter_dtype is not None: + model_kwargs["autocast_adapter_dtype"] = cfg.peft_autocast_adapter_dtype + if cfg.lora_model_dir: LOG.debug("Loading pretrained PEFT - LoRA") - model_kwargs: Any = {} if cfg.lora_on_cpu: model_kwargs["max_memory"] = {"cpu": "256GiB"} model_kwargs["device_map"] = {"": "cpu"} @@ -155,7 +158,7 @@ def load_lora( **model_kwargs, ) else: - model = get_peft_model(model, lora_config) + model = get_peft_model(model, lora_config, **model_kwargs) if rank == 0: try: diff --git a/src/axolotl/utils/schemas/peft.py b/src/axolotl/utils/schemas/peft.py index fd16dec3f2..a9ce1fbd69 100644 --- a/src/axolotl/utils/schemas/peft.py +++ b/src/axolotl/utils/schemas/peft.py @@ -109,6 +109,12 @@ class LoraConfig(BaseModel): ) }, ) + peft_autocast_adapter_dtype: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to upcast the LoRA adapter to fp32. This is enabled by default in PEFT." + }, + ) qlora_sharded_model_loading: bool | None = Field( default=False, From 3e51a680c297e7b7e86a81cbdd3179cd756bd972 Mon Sep 17 00:00:00 2001 From: Seung Hyun Cho Date: Thu, 18 Dec 2025 03:40:36 +0900 Subject: [PATCH 1060/1405] fix: Fix evaluation loss in KD trainer (#3271) * fix: Fix evaluation loss in KD trainer * Fix v2 strategy super() call * fix: Add safety check for total_tokens in log method * fix: simplified num items and outputs return handling * fix: add missing model forward pass in compute_loss * refactor: Use Template Method pattern for chat template strategies * refactor: use pop(None) and remove v2 override * chore: lint --------- Co-authored-by: NanoCode012 Co-authored-by: Wing Lian --- src/axolotl/core/trainers/base.py | 6 +- src/axolotl/integrations/kd/chat_template.py | 20 +++-- src/axolotl/integrations/kd/trainer.py | 17 +++- tests/integrations/test_kd_chat_template.py | 81 ++++++++++++++++++++ 4 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 tests/integrations/test_kd_chat_template.py diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 7896c60889..f4414d649d 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -631,7 +631,11 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: logs["tokens_per_second_per_gpu"] = round( self.state.last_tokens_per_second.item() / self.args.logging_steps, 2 ) - logs["total_tokens"] = int(self.state.total_tokens.item()) + if ( + hasattr(self.state, "total_tokens") + and self.state.total_tokens is not None + ): + logs["total_tokens"] = int(self.state.total_tokens.item()) del self._stored_metrics[train_eval] diff --git a/src/axolotl/integrations/kd/chat_template.py b/src/axolotl/integrations/kd/chat_template.py index 04f0f24a43..5cae69e7c7 100644 --- a/src/axolotl/integrations/kd/chat_template.py +++ b/src/axolotl/integrations/kd/chat_template.py @@ -179,8 +179,17 @@ def _tokenize_single_prompt(self, prompt): logprobs = prompt.pop(self.logprobs_field) tokenized_prompt = super()._tokenize_single_prompt(prompt) tokenized_prompt[self.logprobs_field] = logprobs + + # let subclasses add fields before transform + tokenized_prompt = self._prepare_kd_fields(tokenized_prompt, prompt) + tokenized_prompt = self.transform_logprobs(tokenized_prompt) + return tokenized_prompt + def _prepare_kd_fields(self, tokenized_prompt, original_prompt): + """ + Hook for subclasses to prepare additional KD fields before transform + """ return tokenized_prompt @@ -283,14 +292,13 @@ def transform_logprobs(self, sample): return sample - def _tokenize_single_prompt(self, prompt): - target_token_ids = prompt.get("target_token_ids", None) - - tokenized_prompt = super()._tokenize_single_prompt(prompt) - + def _prepare_kd_fields(self, tokenized_prompt, original_prompt): + """ + Add pre-tokenized target_token_ids for v2 format + """ + target_token_ids = original_prompt.pop("target_token_ids", None) if target_token_ids is not None: tokenized_prompt["target_token_ids"] = target_token_ids - return tokenized_prompt diff --git a/src/axolotl/integrations/kd/trainer.py b/src/axolotl/integrations/kd/trainer.py index 0e98497a7b..343d4c6dfc 100644 --- a/src/axolotl/integrations/kd/trainer.py +++ b/src/axolotl/integrations/kd/trainer.py @@ -16,6 +16,8 @@ KD trainer """ +from typing_extensions import override + from axolotl.core.trainers.base import AxolotlTrainer from .kernels.liger import LigerFusedLinearKLTopKLogprobLoss @@ -60,6 +62,7 @@ def _set_signature_columns_if_needed(self): if columns_to_add: self._signature_columns += columns_to_add + @override def compute_loss( self, model, @@ -79,10 +82,22 @@ def compute_loss( ): del inputs["attention_mask"] + if num_items_in_batch is None and "labels" in inputs: + num_items_in_batch = (inputs["labels"] != -100).sum().item() + if self.model_accepts_loss_kwargs: loss_kwargs = {} if num_items_in_batch is not None: loss_kwargs["num_items_in_batch"] = num_items_in_batch inputs = {**inputs, **loss_kwargs} + outputs = model(**inputs) - return outputs[0] + + if isinstance(outputs, dict): + loss = outputs["loss"] + elif isinstance(outputs, tuple): + loss = outputs[0] + else: + loss = outputs.loss if hasattr(outputs, "loss") else outputs + + return (loss, outputs) if return_outputs else loss diff --git a/tests/integrations/test_kd_chat_template.py b/tests/integrations/test_kd_chat_template.py new file mode 100644 index 0000000000..b828e6c3d0 --- /dev/null +++ b/tests/integrations/test_kd_chat_template.py @@ -0,0 +1,81 @@ +""" +Test for KD chat template strategies +""" + +from unittest.mock import Mock + +import pytest + +from axolotl.integrations.kd.chat_template import ChatTemplateStrategyWithKDv2 + + +class TestChatTemplateStrategyWithKDv2: + """Test v2 strategy correctly handles target_token_ids""" + + @pytest.fixture + def v2_strategy(self): + """Create v2 strategy instance with mocked dependencies""" + # Mock prompter + mock_prompter = Mock() + mock_prompter.roles = {"user": "user", "assistant": "assistant"} + mock_prompter.chat_template_msg_variables = ["role", "content"] + mock_prompter.chat_template = "{{ messages }}" + + # Mock tokenizer + mock_tokenizer = Mock() + mock_tokenizer.pad_token_id = 0 + mock_tokenizer.eos_token_id = 2 + mock_tokenizer.bos_token_id = 1 + mock_tokenizer.eos_token = "<|endoftext|>" + mock_tokenizer.apply_chat_template = Mock(return_value=[1, 10, 20, 30, 2]) + mock_tokenizer.encode = Mock(return_value=[2]) + + return ChatTemplateStrategyWithKDv2( + prompter=mock_prompter, + tokenizer=mock_tokenizer, + train_on_inputs=False, + sequence_len=512, + logprobs_field="logprobs", + gen_temperature=1.0, + kd_temperature=1.0, + ) + + def test_v2_prepare_kd_fields_adds_target_token_ids(self, v2_strategy): + """ + Test that v2's _prepare_kd_fields hook adds target_token_ids. + + Validates the Template Method pattern fix where v2 overrides + the hook to add target_token_ids before transform. + """ + tokenized = {"input_ids": [1, 10, 20, 30, 2], "labels": [1, 10, 20, 30, 2]} + original = {"target_token_ids": [[10, 20], [30, 40]]} + + result = v2_strategy._prepare_kd_fields(tokenized, original) + + assert "target_token_ids" in result + assert result["target_token_ids"] == [[10, 20], [30, 40]] + + def test_v2_prepare_kd_fields_handles_missing_field(self, v2_strategy): + """Test hook handles missing target_token_ids gracefully""" + tokenized = {"input_ids": [1, 10, 20, 30, 2], "labels": [1, 10, 20, 30, 2]} + original = {} + + result = v2_strategy._prepare_kd_fields(tokenized, original) + + assert "target_token_ids" not in result + + def test_v2_transform_requires_target_token_ids(self, v2_strategy): + """ + Test v2's transform fails without target_token_ids. + + Validates the bug fix - transform expects target_token_ids + to be added by the hook. + """ + sample = { + "input_ids": [1, 10, 20, 30, 2], + "labels": [1, 10, 20, 30, 2], + "logprobs": [[-0.1, -0.2], [-0.3, -0.4]], + } + + with pytest.raises(KeyError, match="target_token_ids"): + v2_strategy.transform_logprobs(sample) From 2197b0bf89dd471b8d77485d18c3992e3d1cda94 Mon Sep 17 00:00:00 2001 From: xzuyn <16216325+xzuyn@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:02:41 -0500 Subject: [PATCH 1061/1405] feat: cheap ppl metric (#3317) * Import math and compute perplexity from loss values * lint * coderabbit changes * lint * fix: add rounding to ppl --------- Co-authored-by: NanoCode012 --- src/axolotl/core/trainers/base.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index f4414d649d..8adafd42d7 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -2,6 +2,7 @@ from __future__ import annotations +import math import os from collections import defaultdict from functools import partial, wraps @@ -615,6 +616,17 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: ) logs[key] = round(fn(values).item(), 4) + if "loss" in logs: + try: + logs["ppl"] = round(math.exp(logs["loss"]), 4) + except OverflowError: + logs["ppl"] = float("inf") + if "eval_loss" in logs: + try: + logs["eval_ppl"] = round(math.exp(logs["eval_loss"]), 4) + except OverflowError: + logs["eval_ppl"] = float("inf") + if is_main_process(): # Add memory usage try: From 3750d7dd64a2c0699d5009392d85de525f7414c6 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Thu, 18 Dec 2025 21:41:06 +0530 Subject: [PATCH 1062/1405] add liger support kernal for dpo (#3302) * add liger kernal 4 dpo * revert grpo changes,add support in dpo * revert grpo changes,add support in dpo * dpo_use_liger_kernal * fix liger_dpo --------- Co-authored-by: Ved --- src/axolotl/core/trainers/dpo/__init__.py | 2 ++ src/axolotl/utils/schemas/config.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/axolotl/core/trainers/dpo/__init__.py b/src/axolotl/core/trainers/dpo/__init__.py index 3aa79c4841..5e160e6921 100644 --- a/src/axolotl/core/trainers/dpo/__init__.py +++ b/src/axolotl/core/trainers/dpo/__init__.py @@ -36,4 +36,6 @@ def set_training_args_kwargs(cls, cfg): training_args_kwargs["dpo_norm_loss"] = cfg.dpo_norm_loss if cfg.dpo_use_logits_to_keep is not None: training_args_kwargs["use_logits_to_keep"] = cfg.dpo_use_logits_to_keep + if cfg.dpo_use_liger_kernel is not None: + training_args_kwargs["use_liger_kernel"] = cfg.dpo_use_liger_kernel return training_args_kwargs diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index c9b087ea30..bd6a611772 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -173,6 +173,12 @@ class AxolotlInputConfig( dpo_use_logits_to_keep: bool | None = None dpo_label_smoothing: float | None = None dpo_norm_loss: bool | None = None + + dpo_use_liger_kernel: bool | None = Field( + default=None, + json_schema_extra={"description": "Whether to use Liger kernel for DPO loss."}, + ) + dpo_padding_free: bool | None = None dpo_generate_during_eval: bool | None = None From bbd3486f57ab7894ecf8db62527c1d28a61d22fc Mon Sep 17 00:00:00 2001 From: salman Date: Fri, 19 Dec 2025 16:43:47 +0100 Subject: [PATCH 1063/1405] Distributed Muon Optimizer (#3264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * init * working * updating configs * removing unneeded files * lint * comments * lint * fix regex match * bump contribs version * comments * fixing tests and imports * muon imports in test v2 * test cleanup * bump contribs version --------- Co-authored-by: Salman Mohammadi <“salman.mohammadi@outlook.com”> --- examples/qwen2/adamw-pretrain-fsdp2.yaml | 70 ++++++++ examples/qwen2/muon-pretrain-fsdp2.yaml | 70 ++++++++ requirements.txt | 3 +- src/axolotl/core/builders/base.py | 19 ++- src/axolotl/utils/schemas/validation.py | 87 +++++----- tests/core/test_builders.py | 12 +- tests/e2e/multigpu/test_dist_muon_fsdp2.py | 168 ++++++++++++++++++++ tests/test_validation_dataset.py | 2 +- tests/utils/schemas/validation/test_fsdp.py | 11 ++ 9 files changed, 387 insertions(+), 55 deletions(-) create mode 100644 examples/qwen2/adamw-pretrain-fsdp2.yaml create mode 100644 examples/qwen2/muon-pretrain-fsdp2.yaml create mode 100644 tests/e2e/multigpu/test_dist_muon_fsdp2.py diff --git a/examples/qwen2/adamw-pretrain-fsdp2.yaml b/examples/qwen2/adamw-pretrain-fsdp2.yaml new file mode 100644 index 0000000000..43fb17aabc --- /dev/null +++ b/examples/qwen2/adamw-pretrain-fsdp2.yaml @@ -0,0 +1,70 @@ +base_model: Qwen/Qwen2.5-0.5B +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer + +# Use random initialization for fair comparison +reinit_weights: true + +load_in_8bit: false +load_in_4bit: false +strict: false + +# Pretraining dataset +pretraining_dataset: + - path: allenai/c4 + name: en + type: pretrain + split: train + +dataset_prepared_path: +val_set_size: 0.0 +output_dir: ./outputs/compare-adamw-pretrain + +sequence_len: 2048 +sample_packing: true +pad_to_sequence_len: true + +wandb_project: dist_muon +wandb_entity: +wandb_watch: +wandb_name: adamw +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 4 +num_epochs: 1 +max_steps: 305 + +# AdamW optimizer settings (standard LR for AdamW) +optimizer: adamw_torch_fused +learning_rate: 0.0002 +weight_decay: 0.01 +lr_scheduler: cosine + +train_on_inputs: true +group_by_length: false +bf16: auto +fp16: false +tf32: false + +gradient_checkpointing: false +logging_steps: 1 +flash_attention: true + +warmup_steps: 10 +evals_per_epoch: 0 +saves_per_epoch: 1 + +# Reproducibility +seed: 42 + +fsdp_config: + fsdp_version: 2 + fsdp_offload_params: false + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_transformer_layer_cls_to_wrap: Qwen2DecoderLayer + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_cpu_ram_efficient_loading: false + fsdp_reshard_after_forward: true + +special_tokens: diff --git a/examples/qwen2/muon-pretrain-fsdp2.yaml b/examples/qwen2/muon-pretrain-fsdp2.yaml new file mode 100644 index 0000000000..35c0b71f47 --- /dev/null +++ b/examples/qwen2/muon-pretrain-fsdp2.yaml @@ -0,0 +1,70 @@ +base_model: Qwen/Qwen2.5-0.5B +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer + +# Use random initialization for fair comparison +reinit_weights: true + +load_in_8bit: false +load_in_4bit: false +strict: false + +# Pretraining dataset +pretraining_dataset: + - path: allenai/c4 + name: en + type: pretrain + split: train + +dataset_prepared_path: +val_set_size: 0.0 +output_dir: ./outputs/compare-muon-pretrain + +sequence_len: 2048 +sample_packing: true +pad_to_sequence_len: true + +wandb_project: dist_muon +wandb_entity: +wandb_watch: +wandb_name: muon +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 4 +num_epochs: 1 +max_steps: 305 + +# Muon optimizer settings +optimizer: muon +learning_rate: 0.02 +weight_decay: 0.01 +lr_scheduler: cosine + +train_on_inputs: true +group_by_length: false +bf16: auto +fp16: false +tf32: false + +gradient_checkpointing: false +logging_steps: 1 +flash_attention: true + +warmup_steps: 10 +evals_per_epoch: 0 +saves_per_epoch: 1 + +# Reproducibility +seed: 42 + +fsdp_config: + fsdp_version: 2 + fsdp_offload_params: false + fsdp_state_dict_type: FULL_STATE_DICT + fsdp_transformer_layer_cls_to_wrap: Qwen2DecoderLayer + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_cpu_ram_efficient_loading: false + fsdp_reshard_after_forward: true + +special_tokens: diff --git a/requirements.txt b/requirements.txt index 0989325acf..093546815d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -67,8 +67,7 @@ openenv-core==0.1.0 schedulefree==1.4.1 axolotl-contribs-lgpl==0.0.7 -axolotl-contribs-mit==0.0.5 - +axolotl-contribs-mit==0.0.6 # telemetry posthog==6.7.11 diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 0d19b369f5..06d15ffc8a 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -281,11 +281,22 @@ def _configure_custom_optimizer( adam_kwargs["eps"] = training_args_kwargs.get("adam_epsilon") if self.cfg.optimizer == "muon": - from axolotl.contribs.mit.muon import ( - MuonOptimizerFactory, - ) + _, device_mesh = build_parallelism_config(self.cfg) + + if device_mesh is not None: + from axolotl.contribs.mit.muon.dist_muon import ( + DistMuonOptimizerFactory, + ) + + optimizer_cls = DistMuonOptimizerFactory + optimizer_kwargs["device_mesh"] = device_mesh + else: + from axolotl.contribs.mit.muon import ( + MuonOptimizerFactory, + ) + + optimizer_cls = MuonOptimizerFactory - optimizer_cls = MuonOptimizerFactory optimizer_kwargs.update(adam_kwargs) elif self.cfg.optimizer == "dion": from axolotl.contribs.mit.dion import ( diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 3689768319..36565fb03d 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -751,12 +751,19 @@ def check_adamw_optimizer_params(self): @model_validator(mode="before") @classmethod def check_muon_deepspeed_fsdp(cls, data): - if data.get("optimizer") == "muon" and ( - data.get("deepspeed") or data.get("fsdp") or data.get("fsdp_config") - ): - raise ValueError( - "Muon optimizer is currently incompatible with DeepSpeed and FSDP" - ) + if data.get("optimizer") == "muon": + if data.get("deepspeed"): + raise ValueError( + "Muon optimizer is currently incompatible with DeepSpeed" + ) + if data.get("fsdp") or data.get("fsdp_config"): + fsdp_version = data.get("fsdp_version") + if fsdp_version is None: + fsdp_version = data.get("fsdp_config", {}).get("fsdp_version", 1) + if str(fsdp_version) != "2": + raise ValueError( + "Muon optimizer is only compatible with FSDP2. Set fsdp_version: 2 to use Muon with FSDP." + ) return data @model_validator(mode="before") @@ -840,40 +847,6 @@ def check_fsdp2_base_model_quant_rl(cls, data): return data - @model_validator(mode="before") - @classmethod - def check_fsdp_version_in_fsdp_config(cls, data): - fsdp_config = data.get("fsdp_config") or {} - if fsdp_config and fsdp_config.get("fsdp_version"): - LOG.warning( - "Configuring `fsdp_version` in `fsdp_config` is deprecated. " - "Please configure `fsdp_version` as a top-level field." - ) - data["fsdp_version"] = fsdp_config.pop("fsdp_version") - return data - - @model_validator(mode="before") - @classmethod - def check_fsdp_config_kwargs_prefix(cls, data): - if fsdp_config := data.get("fsdp_config"): - should_fix = False - for key, _ in fsdp_config.items(): - if key.startswith("fsdp_"): - should_fix = True - LOG.warning_once( - "Configuring FSDP fields with the `fsdp_` prefix is deprecated. " - "Please omit the `fsdp_` prefix from the any fields in `fsdp_config`." - ) - if should_fix: - update_fsdp_config = {} - for key, value in fsdp_config.items(): - if key.startswith("fsdp_") and key != "fsdp_version": - update_fsdp_config[key.replace("fsdp_", "")] = value - else: - update_fsdp_config[key] = value - data["fsdp_config"] = update_fsdp_config - return data - @model_validator(mode="after") def check_fsdp_offload_w_8bit_optimizer(self): if ( @@ -975,6 +948,40 @@ def check_deepcompile(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_fsdp_version_in_fsdp_config(cls, data): + fsdp_config = data.get("fsdp_config") or {} + if fsdp_config and fsdp_config.get("fsdp_version"): + LOG.warning( + "Configuring `fsdp_version` in `fsdp_config` is deprecated. " + "Please configure `fsdp_version` as a top-level field." + ) + data["fsdp_version"] = fsdp_config.pop("fsdp_version") + return data + + @model_validator(mode="before") + @classmethod + def check_fsdp_config_kwargs_prefix(cls, data): + if fsdp_config := data.get("fsdp_config"): + should_fix = False + for key, _ in fsdp_config.items(): + if key.startswith("fsdp_"): + should_fix = True + LOG.warning_once( + "Configuring FSDP fields with the `fsdp_` prefix is deprecated. " + "Please omit the `fsdp_` prefix from the any fields in `fsdp_config`." + ) + if should_fix: + update_fsdp_config = {} + for key, value in fsdp_config.items(): + if key.startswith("fsdp_") and key != "fsdp_version": + update_fsdp_config[key.replace("fsdp_", "")] = value + else: + update_fsdp_config[key] = value + data["fsdp_config"] = update_fsdp_config + return data + class SystemValidationMixin: """Validation methods related to system and hardware configuration.""" diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index 1997778967..f9db4d0131 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -474,10 +474,8 @@ def test_custom_optimizer_cls_and_kwargs( assert trainer.optimizer_cls_and_kwargs is not None - from axolotl.contribs.mit.muon import ( - Muon, - MuonOptimizerFactory, - ) + from axolotl.contribs.mit.muon import MuonOptimizerFactory + from axolotl.contribs.mit.muon.muon import Muon optimizer_cls, optimizer_kwargs = trainer.optimizer_cls_and_kwargs assert optimizer_cls is MuonOptimizerFactory @@ -556,10 +554,8 @@ def test_custom_optimizer_cls_and_kwargs( assert trainer.optimizer_cls_and_kwargs is not None - from axolotl.contribs.mit.muon import ( - Muon, - MuonOptimizerFactory, - ) + from axolotl.contribs.mit.muon import MuonOptimizerFactory + from axolotl.contribs.mit.muon.muon import Muon optimizer_cls, optimizer_kwargs = trainer.optimizer_cls_and_kwargs assert optimizer_cls is MuonOptimizerFactory diff --git a/tests/e2e/multigpu/test_dist_muon_fsdp2.py b/tests/e2e/multigpu/test_dist_muon_fsdp2.py new file mode 100644 index 0000000000..93db473a93 --- /dev/null +++ b/tests/e2e/multigpu/test_dist_muon_fsdp2.py @@ -0,0 +1,168 @@ +"""Test module for DistMuon optimizer with FSDP2 multi-GPU functionality.""" + +import os +from pathlib import Path + +import torch +import yaml +from accelerate.test_utils import execute_subprocess_async +from tbparse import SummaryReader +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import most_recent_subdir, require_torch_2_7_0 + +AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent + + +def verify_training_success(temp_dir): + """Verify that training completed successfully by checking artifacts and loss.""" + output_path = Path(temp_dir) + + model_files = list(output_path.glob("*.bin")) + list( + output_path.glob("*.safetensors") + ) + assert len(model_files) > 0, "No model files found - training may have failed" + + checkpoint_files = list(output_path.glob("checkpoint-*")) + assert len(checkpoint_files) > 0, ( + "No checkpoint files found - training may have failed" + ) + + tb_log_path = most_recent_subdir(temp_dir + "/runs") + if tb_log_path: + event_files = sorted(os.listdir(tb_log_path)) + if event_files: + event_file = os.path.join(tb_log_path, event_files[0]) + reader = SummaryReader(event_file) + df = reader.scalars + train_loss_df = df[df.tag == "train/train_loss"] + if len(train_loss_df) > 0: + final_loss = train_loss_df.value.values[-1] + assert not torch.isnan(torch.tensor(final_loss)), ( + f"Training loss is NaN: {final_loss}" + ) + + +class TestDistMuon: + """Test class for DistMuon optimizer with FSDP2 functionality.""" + + @require_torch_2_7_0 + def test_fft_sft(self, temp_dir): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:10%]", + }, + ], + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.02, + "optimizer": "muon", + "weight_decay": 0.01, + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": False, + "transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + "use_tensorboard": True, + "bf16": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) + + @require_torch_2_7_0 + def test_lora_sft(self, temp_dir): + cfg = DictDefault( + { + "base_model": "Qwen/Qwen2.5-0.5B", + "sequence_len": 2048, + "val_set_size": 0.01, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:10%]", + }, + ], + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.02, + "optimizer": "muon", + "weight_decay": 0.01, + "lr_scheduler": "cosine", + "flash_attention": True, + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": False, + "transformer_layer_cls_to_wrap": "Qwen2DecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + "use_tensorboard": True, + "bf16": True, + } + ) + + # write cfg to yaml file + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + verify_training_success(temp_dir) diff --git a/tests/test_validation_dataset.py b/tests/test_validation_dataset.py index 3d3b5db96c..464812a90f 100644 --- a/tests/test_validation_dataset.py +++ b/tests/test_validation_dataset.py @@ -363,5 +363,5 @@ def test_muon_fsdp(self, minimal_cfg): } ) - with pytest.raises(ValueError, match=r".*is currently incompatible with*"): + with pytest.raises(ValueError, match=r".*only compatible with FSDP2.*"): validate_config(cfg) diff --git a/tests/utils/schemas/validation/test_fsdp.py b/tests/utils/schemas/validation/test_fsdp.py index 65f9c66a38..9fa3277975 100644 --- a/tests/utils/schemas/validation/test_fsdp.py +++ b/tests/utils/schemas/validation/test_fsdp.py @@ -123,6 +123,17 @@ def test_fsdp_prefixes_removed(self, min_base_cfg): assert cfg.fsdp_config.transformer_layer_cls_to_wrap == "LlamaDecoderLayer" assert cfg.fsdp_config.reshard_after_forward is True + def test_muon_fsdp1_rejected(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + optimizer="muon", + fsdp_version=1, + fsdp_config={"reshard_after_forward": True}, + ) + with pytest.raises( + ValueError, match="Muon optimizer is only compatible with FSDP2" + ): + validate_config(cfg) + @pytest.mark.parametrize( "rl", [ From 07c41a6c2a82b42152de9cf8b0c8b82b43ed9862 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 19 Dec 2025 11:34:55 -0500 Subject: [PATCH 1064/1405] fix preview docs failing due to running out of disk (#3326) [skip ci] * fix preview docs failing due to running out of disk * fix docs publish too --- .github/workflows/docs.yml | 3 +++ .github/workflows/preview-docs.yml | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5b5cc54891..f4a4144ba9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -12,6 +12,9 @@ jobs: build-deploy: runs-on: ubuntu-latest steps: + - name: cleanup node + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL - name: Check out repository uses: actions/checkout@v4 - name: Set up Quarto diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index db4abddce5..6049981308 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -11,6 +11,7 @@ on: - '_quarto.yml' - docs/scripts/generate_config_docs.py - src/axolotl/utils/schemas/**.py + - .github/workflows/preview-docs.yml permissions: checks: write @@ -27,6 +28,10 @@ jobs: runs-on: ubuntu-latest if: ${{ !github.event.pull_request.draft }} steps: + - name: cleanup node + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL + - name: Check out repository uses: actions/checkout@v4 with: From 43cef27458a4e8395f14bae96ff1be2a7b7fb7a2 Mon Sep 17 00:00:00 2001 From: Alexander Kozhevnikov Date: Mon, 22 Dec 2025 16:53:58 +0300 Subject: [PATCH 1065/1405] Fix typo in densemixer RuntimeError (#3327) [skip ci] It offers installing densemizer while it should be densemixer --- src/axolotl/integrations/densemixer/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/integrations/densemixer/plugin.py b/src/axolotl/integrations/densemixer/plugin.py index 2d0bf32cdc..9548fd19ac 100644 --- a/src/axolotl/integrations/densemixer/plugin.py +++ b/src/axolotl/integrations/densemixer/plugin.py @@ -21,7 +21,7 @@ def pre_model_load(self, cfg): if cfg.dense_mixer: if not importlib.util.find_spec("densemixer"): raise RuntimeError( - "DenseMixer is not installed. Install it with `pip install densemizer`" + "DenseMixer is not installed. Install it with `pip install densemixer`" ) from densemixer.patching import ( From faaff6c7929f948ec4f6ea9dd9816b7430f03a2a Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:24:43 +0530 Subject: [PATCH 1066/1405] allow users to set ndigits for rounding of metrics when logging (#3325) * METRIC_PRECISION-> 8 * use ndigits and move env getter to top of log function --------- Co-authored-by: Ved Co-authored-by: Wing Lian --- src/axolotl/core/trainers/base.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 8adafd42d7..aae3d28fb1 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -604,6 +604,7 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: """ # logs either has 'loss' or 'eval_loss' train_eval = "train" if "loss" in logs else "eval" + metric_ndigits = int(os.getenv("AXOLOTL_METRIC_NDIGITS", "5")) for key, metric_data in self._stored_metrics[train_eval].items(): values = torch.tensor(metric_data["values"]) # type: ignore[arg-type] @@ -614,16 +615,16 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: raise NotImplementedError( "Metric reduction must be one of [mean, min, max, sum]" ) - logs[key] = round(fn(values).item(), 4) + logs[key] = round(fn(values).item(), metric_ndigits) if "loss" in logs: try: - logs["ppl"] = round(math.exp(logs["loss"]), 4) + logs["ppl"] = round(math.exp(logs["loss"]), metric_ndigits) except OverflowError: logs["ppl"] = float("inf") if "eval_loss" in logs: try: - logs["eval_ppl"] = round(math.exp(logs["eval_loss"]), 4) + logs["eval_ppl"] = round(math.exp(logs["eval_loss"]), metric_ndigits) except OverflowError: logs["eval_ppl"] = float("inf") From efeb5a4e41007a1e87e7ee780590938c94665899 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 22 Dec 2025 13:58:25 -0500 Subject: [PATCH 1067/1405] fix check for fp8 capability (#3324) * fix check for fp8 capability * handle non-cuda compute * reduce concurrency of tests --- .github/workflows/tests.yml | 4 ++-- examples/llama-3/3b-fp8-fsdp2.yaml | 1 - src/axolotl/cli/config.py | 9 +++++++++ src/axolotl/utils/schemas/config.py | 11 +++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1cbfc15e1e..0dc61b7ffe 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -114,7 +114,7 @@ jobs: - name: Run tests run: | df -h - pytest -v --durations=10 -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ --ignore=tests/monkeypatch/ tests/ --cov=axolotl --cov-report=xml + pytest -v --durations=10 -n4 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ --ignore=tests/monkeypatch/ tests/ --cov=axolotl --cov-report=xml df -h pytest -v --durations=10 tests/monkeypatch/ --cov=axolotl --cov-append --cov-report=xml df -h @@ -196,7 +196,7 @@ jobs: - name: Run tests run: | - pytest -v --durations=10 -n8 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ --ignore=tests/monkeypatch/ tests/ --cov=axolotl --cov-report=xml + pytest -v --durations=10 -n4 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ --ignore=tests/monkeypatch/ tests/ --cov=axolotl --cov-report=xml pytest -v --durations=10 tests/monkeypatch/ --cov=axolotl --cov-append --cov-report=xml pytest -v --durations=10 tests/cli/ diff --git a/examples/llama-3/3b-fp8-fsdp2.yaml b/examples/llama-3/3b-fp8-fsdp2.yaml index b7de7ca528..57b308abdc 100644 --- a/examples/llama-3/3b-fp8-fsdp2.yaml +++ b/examples/llama-3/3b-fp8-fsdp2.yaml @@ -29,7 +29,6 @@ flex_attention: true flex_attn_compile_kwargs: dynamic: false mode: max-autotune-no-cudagraphs -save_strategy: no torch_compile: true wandb_project: diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index 3c4ace7b0c..b53c6576bb 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -227,6 +227,7 @@ def load_cfg( cfg, capabilities={ "bf16": is_torch_bf16_gpu_available(), + "fp8": compute_supports_fp8(), "n_gpu": int(os.environ.get("WORLD_SIZE", 1)), "compute_capability": gpu_version, }, @@ -259,3 +260,11 @@ def load_cfg( ) return cfg + + +def compute_supports_fp8() -> bool: + try: + compute_capability = torch.cuda.get_device_capability() + return compute_capability >= (9, 0) + except RuntimeError: + return False diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index bd6a611772..e0c9acd4d9 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -2,6 +2,7 @@ from typing import Annotated, Any, Literal +from accelerate.utils import is_fp8_available from annotated_types import MinLen from packaging import version from pydantic import ( @@ -1098,6 +1099,16 @@ def check_bf16(self): ) return self + @model_validator(mode="after") + def check_fp8(self): + if self.fp8 and not self.capabilities.fp8: + raise ValueError("fp8 requested, but fp8 is not supported on this GPU") + elif self.fp8 and self.capabilities.fp8 and not is_fp8_available(): + raise ValueError( + "fp8 requested, but missing one of ms-amp, transformers-engine or torchao." + ) + return self + @model_validator(mode="before") @classmethod def check_sample_packing_w_sdpa_bf16(cls, data): From 92ee4256f73159dc20a204ba5186ebff002658ae Mon Sep 17 00:00:00 2001 From: kallewoof Date: Tue, 23 Dec 2025 03:59:49 +0900 Subject: [PATCH 1068/1405] feature: raise on long sequence drop (#3321) * feature: raise on long sequence drop It is sometimes not desired that sequences are silently dropped from the dataset, especially when the dataset has been carefully crafted and pre-fitted for the training context. This would then suggest that an error occurred somewhere in the process. This feature adds a third value for excess_length_strategy called 'raise', which will raise a ValueError if a sequence is encountered that is too long and would have normally been dropped/truncated. * tests: add excess_length_strategy tests * doc: updated return value description for drop_long_seq_in_dataset * add @enable_hf_offline * fixed cfg modified after validate_config called * hf offline fix * fix tqdm desc when raise is used * test: added test for non-batched case * accidental code change revert * test: use pytest.raises * test: simplified drop_seq_len tests * test: moved excess_length_strat test to test_data.py --------- Co-authored-by: salman --- src/axolotl/utils/data/utils.py | 16 ++++++++++--- src/axolotl/utils/schemas/config.py | 4 ++-- src/axolotl/utils/trainer.py | 13 +++++++++- tests/test_data.py | 37 +++++++++++++++++++++++++++++ tests/test_datasets.py | 4 +++- 5 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index 2d0ca9d0e4..319e27f6f4 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -188,7 +188,10 @@ def handle_long_seq_in_dataset( cfg: Dictionary mapping `axolotl` config keys to values. Returns: - Filtered dataset with long sequences removed. + Filtered dataset with long sequences handled according to the excess_length_strategy value: + 'drop' (default) excludes any sequence longer than sequence_len + 'truncate' truncates them down to sequence_len + 'raise' raises a ValueError if any sequence was found that was longer than sequence_len """ if ( hasattr(dataset, "column_names") @@ -206,10 +209,13 @@ def handle_long_seq_in_dataset( ) return dataset + excess_length_strategy = (cfg.excess_length_strategy or "drop").lower() + drop_long = functools.partial( drop_long_seq, sequence_len=sequence_len, min_sequence_len=cfg.min_sample_len, + raise_on_drop=excess_length_strategy == "raise", ) with contextlib.suppress(AttributeError): @@ -228,9 +234,13 @@ def handle_long_seq_in_dataset( drop_long_kwargs = {} if filter_map_kwargs: - drop_long_kwargs["desc"] = f"Dropping Long Sequences (>{sequence_len})" + action = ( + "Checking Sequence Lengths" + if excess_length_strategy == "raise" + else "Dropping Long Sequences" + ) + drop_long_kwargs["desc"] = f"{action} (>{sequence_len})" - excess_length_strategy = (cfg.excess_length_strategy or "drop").lower() if excess_length_strategy == "truncate": process_fn = functools.partial( truncate_long_seq, diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index e0c9acd4d9..f2f4a311a6 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -452,10 +452,10 @@ class AxolotlInputConfig( "description": "The maximum length of an input to train with, this should typically be less than 2048 as most models have a token/context limit of 2048" }, ) - excess_length_strategy: Literal["drop", "truncate"] | None = Field( + excess_length_strategy: Literal["drop", "truncate", "raise"] | None = Field( default=None, json_schema_extra={ - "description": "What to do when a tokenized row exceeds sequence_len. 'drop' removes the row; 'truncate' slices tensors to sequence_len. Defaults to 'drop' for backward compatibility." + "description": "What to do when a tokenized row exceeds sequence_len. 'drop' removes the row; 'truncate' slices tensors to sequence_len; 'raise' raises a ValueError. Defaults to 'drop' for backward compatibility." }, ) eval_sequence_len: int | None = Field( diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index d97577d863..3628fd85f1 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -205,12 +205,15 @@ def add_length(sample): return sample -def drop_long_seq(sample, sequence_len=2048, min_sequence_len=2): +def drop_long_seq(sample, sequence_len=2048, min_sequence_len=2, raise_on_drop=False): """ Drop samples whose sequence length is either too long (> sequence_len) or too short (< min_sequence_len). Works for both single-example (list[int]) or batched (list[list[int]]). + + If raise_on_drop is set, the code raises a ValueError if a sample is + encountered that is too long and would have been dropped. """ min_sequence_len = min_sequence_len or 2 @@ -225,12 +228,20 @@ def drop_long_seq(sample, sequence_len=2048, min_sequence_len=2): if isinstance(input_ids[0], int): # Single example (input_ids is a list of int) length = len(input_ids) + if raise_on_drop and length > sequence_len: + raise ValueError( + f"Sequence encountered with {length} tokens, which exceeds the maximum {sequence_len}." + ) return min_sequence_len <= length <= sequence_len # Batched (input_ids is a list of lists) results = [] for seq in input_ids: length = len(seq) + if raise_on_drop and length > sequence_len: + raise ValueError( + f"Sequence encountered with {length} tokens, which exceeds the maximum {sequence_len}." + ) results.append(min_sequence_len <= length <= sequence_len) return results diff --git a/tests/test_data.py b/tests/test_data.py index 99ed063365..ad76bbf6e4 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -7,6 +7,7 @@ from transformers import LlamaTokenizer from axolotl.utils.data import encode_streaming, md5 +from axolotl.utils.trainer import drop_long_seq from tests.hf_offline_utils import enable_hf_offline @@ -63,6 +64,42 @@ def test_md5(self): md5("hello world", "utf-8"), "5eb63bbbe01eeed093cb22bb8f5acdc3" ) + def test_excess_length_strategy(self): + """Test that excess_length_strategy results in a value error when set to 'raise'.""" + + # -- single sequence -- + # This should work + data = {"input_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]} + drop_long_seq(data, 32, raise_on_drop=True) + + # This should return True, since data fits + dropped = drop_long_seq(data, 32) + self.assertTrue(dropped) + + # This should raise + self.assertRaises(ValueError, drop_long_seq, data, 15, raise_on_drop=True) + + # This should return False, since data doesn't fit + dropped = drop_long_seq(data, 15) + self.assertFalse(dropped) + + # -- batch sequence -- + # This should work + data = { + "input_ids": [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + ] + } + drop_long_seq(data, 32, raise_on_drop=True) + + # This should raise + self.assertRaises(ValueError, drop_long_seq, data, 15, raise_on_drop=True) + + # This should keep the first but drop the second entry + dropped = drop_long_seq(data, 15) + self.assertEqual(dropped, [True, False]) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_datasets.py b/tests/test_datasets.py index bd1c8f2c2b..3b24ad5805 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -13,7 +13,9 @@ from axolotl.loaders.tokenizer import load_tokenizer from axolotl.utils.data.rl import prepare_preference_datasets -from axolotl.utils.data.sft import _load_tokenized_prepared_datasets +from axolotl.utils.data.sft import ( + _load_tokenized_prepared_datasets, +) from axolotl.utils.dict import DictDefault from tests.constants import ( From f2155eaf79bd01ceb379196b9b9298e9b740404a Mon Sep 17 00:00:00 2001 From: Abubakar Abid Date: Tue, 23 Dec 2025 05:49:07 -0800 Subject: [PATCH 1069/1405] feat: add trackio as experiment tracking integration (#3253) * feat: add trackio as experiment tracking integration - Add TrackioConfig to integrations schema with project_name, run_name, and space_id - Create trackio_.py module for environment setup - Add is_trackio_available() utility function - Integrate trackio with report_to in trainer builder - Add trackio callback for experiment tracking - Add trackio config keys to gpt-oss example YAMLs - Trackio runs locally by default, syncs to HF Space if space_id provided * changes * changes * changes * changes * changes * changes * changes * Update requirements.txt * don't allow pydantic 2.12 for now --------- Co-authored-by: Abubakar Abid Co-authored-by: Wing Lian --- .../gpt-oss-120b-fft-fsdp2-offload.yaml | 4 ++ .../gpt-oss-20b-fft-deepspeed-zero3.yaml | 4 ++ .../gpt-oss-20b-fft-fsdp2-offload.yaml | 4 ++ examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml | 4 ++ .../gpt-oss-20b-sft-lora-singlegpu.yaml | 4 ++ ...-oss-safeguard-20b-sft-lora-singlegpu.yaml | 4 ++ requirements.txt | 7 +-- src/axolotl/cli/config.py | 2 + src/axolotl/cli/inference.py | 4 +- src/axolotl/cli/utils/diffusion.py | 4 +- src/axolotl/core/builders/base.py | 13 ++++++ src/axolotl/utils/__init__.py | 4 ++ src/axolotl/utils/callbacks/trackio_.py | 44 +++++++++++++++++++ src/axolotl/utils/schemas/config.py | 2 + src/axolotl/utils/schemas/integrations.py | 20 +++++++++ src/axolotl/utils/trackio_.py | 17 +++++++ 16 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 src/axolotl/utils/callbacks/trackio_.py create mode 100644 src/axolotl/utils/trackio_.py diff --git a/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml b/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml index 62f3167e85..b7082f9867 100644 --- a/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml +++ b/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml @@ -32,6 +32,10 @@ wandb_watch: wandb_name: wandb_log_model: +trackio_project_name: +trackio_run_name: +trackio_space_id: + gradient_accumulation_steps: 2 micro_batch_size: 1 num_epochs: 1 diff --git a/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml b/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml index ccb84e28e9..b718ff2eba 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml @@ -28,6 +28,10 @@ wandb_watch: wandb_name: wandb_log_model: +trackio_project_name: +trackio_run_name: +trackio_space_id: + gradient_accumulation_steps: 2 micro_batch_size: 1 num_epochs: 1 diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml index 69a3c434d1..af1c93bc0c 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml @@ -29,6 +29,10 @@ wandb_watch: wandb_name: wandb_log_model: +trackio_project_name: +trackio_run_name: +trackio_space_id: + gradient_accumulation_steps: 2 micro_batch_size: 1 num_epochs: 1 diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml index 4a0f1ad707..894ba99b83 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml @@ -28,6 +28,10 @@ wandb_watch: wandb_name: wandb_log_model: +trackio_project_name: +trackio_run_name: +trackio_space_id: + gradient_accumulation_steps: 2 micro_batch_size: 1 num_epochs: 1 diff --git a/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml b/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml index b6deacb1b9..7c4f978468 100644 --- a/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml +++ b/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml @@ -41,6 +41,10 @@ wandb_watch: wandb_name: wandb_log_model: +trackio_project_name: +trackio_run_name: +trackio_space_id: + gradient_accumulation_steps: 8 micro_batch_size: 1 num_epochs: 1 diff --git a/examples/gpt-oss/gpt-oss-safeguard-20b-sft-lora-singlegpu.yaml b/examples/gpt-oss/gpt-oss-safeguard-20b-sft-lora-singlegpu.yaml index ab026337d7..cbb9efc8e7 100644 --- a/examples/gpt-oss/gpt-oss-safeguard-20b-sft-lora-singlegpu.yaml +++ b/examples/gpt-oss/gpt-oss-safeguard-20b-sft-lora-singlegpu.yaml @@ -41,6 +41,10 @@ wandb_watch: wandb_name: wandb_log_model: +trackio_project_name: +trackio_run_name: +trackio_space_id: + gradient_accumulation_steps: 8 micro_batch_size: 1 num_epochs: 1 diff --git a/requirements.txt b/requirements.txt index 093546815d..5e1af69408 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,15 +20,16 @@ deepspeed>=0.17.0 trl==0.25.0 hf_xet==1.2.0 kernels>=0.9.0 -trackio +trackio>=0.13.0 +typing_extensions>=4.14.0 optimum==1.16.2 hf_transfer sentencepiece -gradio==5.49.1 +gradio>=6.2.0,<7.0 modal==1.0.2 -pydantic>=2.10.6 +pydantic>=2.10.6,<2.12 addict fire PyYAML>=6.0 diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index b53c6576bb..986167f021 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -26,6 +26,7 @@ from axolotl.utils.logging import get_logger from axolotl.utils.mlflow_ import setup_mlflow_env_vars from axolotl.utils.tee import prepare_debug_log +from axolotl.utils.trackio_ import setup_trackio_env_vars from axolotl.utils.trainer import prepare_optim_env from axolotl.utils.wandb_ import setup_wandb_env_vars @@ -246,6 +247,7 @@ def load_cfg( setup_wandb_env_vars(cfg) setup_mlflow_env_vars(cfg) setup_comet_env_vars(cfg) + setup_trackio_env_vars(cfg) plugin_set_cfg(cfg) TELEMETRY_MANAGER.send_event(event_type="config-processed", properties=cfg) diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index 640be36962..cafa0f4eff 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -288,8 +288,8 @@ def generate(instruction): title=cfg.get("gradio_title", "Axolotl Gradio Interface"), ) - demo.queue().launch( - show_api=False, + demo.launch( + footer_links=["gradio", "settings"], share=cfg.get("gradio_share", True), server_name=cfg.get("gradio_server_name", "127.0.0.1"), server_port=cfg.get("gradio_server_port", None), diff --git a/src/axolotl/cli/utils/diffusion.py b/src/axolotl/cli/utils/diffusion.py index 1157bfd66f..7bf68048eb 100644 --- a/src/axolotl/cli/utils/diffusion.py +++ b/src/axolotl/cli/utils/diffusion.py @@ -366,8 +366,8 @@ def _gen(instruction_text: str, selected_mode: str, mratio: float, ctoks: int): outputs=[masked_preview, html_out], ) - demo.queue().launch( - show_api=False, + demo.launch( + footer_links=["gradio", "settings"], share=cfg.get("gradio_share", True), server_name=cfg.get("gradio_server_name", "127.0.0.1"), server_port=cfg.get("gradio_server_port", None), diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 06d15ffc8a..412f6da2ff 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -35,6 +35,7 @@ is_comet_available, is_mlflow_available, is_opentelemetry_available, + is_trackio_available, ) from axolotl.utils.callbacks import ( GCCallback, @@ -147,6 +148,14 @@ def get_callbacks(self) -> list[TrainerCallback]: callbacks.append( SaveAxolotlConfigtoCometCallback(self.cfg.axolotl_config_path) ) + if self.cfg.use_trackio and is_trackio_available(): + from axolotl.utils.callbacks.trackio_ import ( + SaveAxolotlConfigtoTrackioCallback, + ) + + callbacks.append( + SaveAxolotlConfigtoTrackioCallback(self.cfg.axolotl_config_path) + ) if self.cfg.use_otel_metrics and is_opentelemetry_available(): from axolotl.utils.callbacks.opentelemetry import ( OpenTelemetryMetricsCallback, @@ -434,6 +443,8 @@ def _configure_reporting(self, training_args_kwargs: dict): report_to.append("tensorboard") if self.cfg.use_comet: report_to.append("comet_ml") + if self.cfg.use_trackio: + report_to.append("trackio") training_args_kwargs["report_to"] = report_to @@ -441,6 +452,8 @@ def _configure_reporting(self, training_args_kwargs: dict): training_args_kwargs["run_name"] = self.cfg.wandb_name elif self.cfg.use_mlflow: training_args_kwargs["run_name"] = self.cfg.mlflow_run_name + elif self.cfg.use_trackio: + training_args_kwargs["run_name"] = self.cfg.trackio_run_name else: training_args_kwargs["run_name"] = None diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index 335049158b..96ac29bd01 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -24,6 +24,10 @@ def is_opentelemetry_available(): ) +def is_trackio_available(): + return importlib.util.find_spec("trackio") is not None + + def get_pytorch_version() -> tuple[int, int, int]: """ Get Pytorch version as a tuple of (major, minor, patch). diff --git a/src/axolotl/utils/callbacks/trackio_.py b/src/axolotl/utils/callbacks/trackio_.py new file mode 100644 index 0000000000..8249321f62 --- /dev/null +++ b/src/axolotl/utils/callbacks/trackio_.py @@ -0,0 +1,44 @@ +"""Trackio module for trainer callbacks""" + +from typing import TYPE_CHECKING + +import trackio +from transformers import TrainerCallback, TrainerControl, TrainerState + +from axolotl.utils.distributed import is_main_process +from axolotl.utils.environment import is_package_version_ge +from axolotl.utils.logging import get_logger + +if TYPE_CHECKING: + from axolotl.core.training_args import AxolotlTrainingArguments + +LOG = get_logger(__name__) + + +class SaveAxolotlConfigtoTrackioCallback(TrainerCallback): + """Callback for trackio integration""" + + def __init__(self, axolotl_config_path): + self.axolotl_config_path = axolotl_config_path + + def on_train_begin( + self, + args: "AxolotlTrainingArguments", + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + if is_main_process(): + try: + if not is_package_version_ge("trackio", "0.11.0"): + LOG.warning( + "Trackio version 0.11.0 or higher is required to save config files. " + "Please upgrade trackio: pip install --upgrade trackio" + ) + return control + + trackio.save(self.axolotl_config_path) + LOG.info("The Axolotl config has been saved to Trackio.") + except (FileNotFoundError, ConnectionError, AttributeError) as err: + LOG.warning(f"Error while saving Axolotl config to Trackio: {err}") + return control diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index f2f4a311a6..4ef1aff3a1 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -34,6 +34,7 @@ MLFlowConfig, OpenTelemetryConfig, RayConfig, + TrackioConfig, WandbConfig, ) from axolotl.utils.schemas.internal import EnvCapabilities, GPUCapabilities @@ -63,6 +64,7 @@ class AxolotlInputConfig( WandbConfig, MLFlowConfig, CometConfig, + TrackioConfig, OpenTelemetryConfig, LISAConfig, GradioConfig, diff --git a/src/axolotl/utils/schemas/integrations.py b/src/axolotl/utils/schemas/integrations.py index 97d6755690..dc171c3101 100644 --- a/src/axolotl/utils/schemas/integrations.py +++ b/src/axolotl/utils/schemas/integrations.py @@ -200,3 +200,23 @@ class OpenTelemetryConfig(BaseModel): "description": "Port for the Prometheus metrics HTTP server" }, ) + + +class TrackioConfig(BaseModel): + """Trackio configuration subset""" + + use_trackio: bool | None = None + trackio_project_name: str | None = Field( + default=None, + json_schema_extra={"description": "Your trackio project name"}, + ) + trackio_run_name: str | None = Field( + default=None, + json_schema_extra={"description": "Set the name of your trackio run"}, + ) + trackio_space_id: str | None = Field( + default=None, + json_schema_extra={ + "description": "Hugging Face Space ID to sync dashboard to (optional, runs locally if not provided)" + }, + ) diff --git a/src/axolotl/utils/trackio_.py b/src/axolotl/utils/trackio_.py new file mode 100644 index 0000000000..2bddfb972e --- /dev/null +++ b/src/axolotl/utils/trackio_.py @@ -0,0 +1,17 @@ +"""Module for trackio utilities""" + +import os + +from axolotl.utils.dict import DictDefault + + +def setup_trackio_env_vars(cfg: DictDefault): + for key in cfg.keys(): + if key.startswith("trackio_"): + value = cfg.get(key, "") + + if value and isinstance(value, str) and len(value) > 0: + os.environ[key.upper()] = value + + if cfg.trackio_project_name and len(cfg.trackio_project_name) > 0: + cfg.use_trackio = True From 97f1b1758d699ecdef88f296ace8cfed9d5bdfec Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 25 Dec 2025 17:53:52 +0700 Subject: [PATCH 1070/1405] Feat: add kimi linear support (#3257) * feat: add custom kimi linear patch [skip ci] * feat: add configuration file and fix import [skip ci] * fix: hijack tokenizer temporarily [skip ci] * chore: remove accidental commit * fix: attempt patch kimi remote * fix: kwargs passsed * fix: device for tensor * fix: aux loss calculation * feat: cleaned up patches order * fix: remove duplicate tokenizer patch * chore: add debug logs * chore: add debug logs * chore: debug * Revert "chore: add debug logs" This reverts commit da372a5f67a5504b7b6bc5a7022d5c3cd0f0d9a1. * Revert "chore: add debug logs" This reverts commit 97d1de1d7cc81481e183748cc07576a740b23b1a. * fix: KeyError: 'tokenization_kimi' * fix: support remote_model_id in cce patch * feat: add config preload patch * fix: use standard aux loss calc and updated modeling * fix: import * feat: add kimi-linear docs and example * chore: add note about moe kernels * feat: update cce to include kimi-linear * chore: lint * chore: update main readme * fix: patch mechanism to address comments * chore: lint * fix: tests * chore: cleanup comment --- README.md | 2 +- .../colab-axolotl-example.ipynb | 2 +- examples/kimi-linear/README.md | 47 + examples/kimi-linear/kimi-48b-lora.yaml | 81 + scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 3 +- .../cut_cross_entropy/__init__.py | 12 +- src/axolotl/loaders/patch_manager.py | 49 + src/axolotl/loaders/tokenizer.py | 5 + .../models/kimi_linear/__init__.py | 0 .../models/kimi_linear/configuration_kimi.py | 148 ++ .../models/kimi_linear/modeling_kimi.py | 1361 +++++++++++++++++ .../models/kimi_linear/patch_kimi_linear.py | 85 + .../models/kimi_linear/tokenization_kimi.py | 357 +++++ src/axolotl/utils/config/__init__.py | 5 + 15 files changed, 2152 insertions(+), 7 deletions(-) create mode 100644 examples/kimi-linear/README.md create mode 100644 examples/kimi-linear/kimi-48b-lora.yaml create mode 100644 src/axolotl/monkeypatch/models/kimi_linear/__init__.py create mode 100644 src/axolotl/monkeypatch/models/kimi_linear/configuration_kimi.py create mode 100644 src/axolotl/monkeypatch/models/kimi_linear/modeling_kimi.py create mode 100644 src/axolotl/monkeypatch/models/kimi_linear/patch_kimi_linear.py create mode 100644 src/axolotl/monkeypatch/models/kimi_linear/tokenization_kimi.py diff --git a/README.md b/README.md index 2858672158..13a5a9243d 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ## 🎉 Latest Updates -- 2025/12: Axolotl now includes support for [Olmo3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/olmo3), [Trinity](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/trinity), and [Ministral3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/ministral3). +- 2025/12: Axolotl now includes support for [Kimi-Linear](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/kimi-linear), [Olmo3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/olmo3), [Trinity](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/trinity), and [Ministral3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/ministral3). - 2025/10: New model support has been added in Axolotl for: [Qwen3 Next](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/qwen3-next), [Qwen2.5-vl, Qwen3-vl](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen2_5-vl), [Qwen3, Qwen3MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3), [Granite 4](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/granite4), [HunYuan](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/hunyuan), [Magistral 2509](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral#vision), [Apertus](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/apertus), and [Seed-OSS](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/seed-oss). - 2025/09: Axolotl now has text diffusion training. Read more [here](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations/diffusion). - 2025/08: QAT has been updated to include NVFP4 support. See [PR](https://github.com/axolotl-ai-cloud/axolotl/pull/3107). diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 77a4154e2c..133c3db79b 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f643b88\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@242b245\"" ] }, { diff --git a/examples/kimi-linear/README.md b/examples/kimi-linear/README.md new file mode 100644 index 0000000000..250f8bea1c --- /dev/null +++ b/examples/kimi-linear/README.md @@ -0,0 +1,47 @@ +# Finetune MoonshotAI's Kimi Linear with Axolotl + +[Kimi Linear](https://huggingface.co/collections/moonshotai/kimi-linear-a3b) is a MoE model (48B total, 3B active) by MoonshotAI using a hybrid linear attention architecture to achieve a 1M token context length. It uses Kimi Delta Attention (KDA), a refined version of Gated DeltaNet that reduces KV cache size by up to 75% and boosts decoding throughput by up to 6x for long contexts. + +This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +**Note:** Axolotl uses experimental training code for Kimi Linear as their original modeling code is inference-only. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install CCE via [docs](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) + +3. Run the finetuning example: + + ```bash + axolotl train examples/kimi-linear/kimi-48b-lora.yaml + ``` + +This config uses about 98.7GiB VRAM. + +Let us know how it goes. Happy finetuning! + +### TIPS + +- Kimi Linear requires `trust_remote_code: true`. +- You can run a full finetuning by removing the `adapter: lora` and `load_in_8bit: true`. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html) +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template) + +## Optimization Guides + +See 👉 [docs](https://docs.axolotl.ai/docs/optimizations.html). + +## Limitations + +This is not yet compatible with MoE kernels from transformers v5. + +## Related Resources + +- [Kimi Linear Paper](https://huggingface.co/papers/2510.26692) +- [Kimi Linear GitHub](https://github.com/MoonshotAI/Kimi-Linear) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/kimi-linear/kimi-48b-lora.yaml b/examples/kimi-linear/kimi-48b-lora.yaml new file mode 100644 index 0000000000..8e855dd720 --- /dev/null +++ b/examples/kimi-linear/kimi-48b-lora.yaml @@ -0,0 +1,81 @@ +base_model: moonshotai/Kimi-Linear-48B-A3B-Instruct + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +trust_remote_code: true + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: true +load_in_4bit: false +strict: false + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + split: train + +dataset_prepared_path: last_run_prepared +val_set_size: 0.2 +output_dir: ./outputs/lora-out + +adapter: lora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true +pad_to_sequence_len: true + +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.05 +lora_fan_in_fan_out: +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: false + +gradient_checkpointing: true +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +flash_attention: true + +loss_watchdog_threshold: 5.0 +loss_watchdog_patience: 3 + +warmup_ratio: 0.1 +evals_per_epoch: 2 +saves_per_epoch: 1 +debug: +deepspeed: +weight_decay: 0.0 +fsdp: +fsdp_config: +special_tokens: diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index ec5c6d4750..e902bb0ace 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f643b88"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@242b245"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 2c5b0f6e57..b28382542a 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f643b88" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@242b245" ``` ## Usage @@ -54,6 +54,7 @@ plugins: - granitemoehybrid - hunyuan_v1_dense - hunyuan_v1_moe +- kimi_linear - lfm2 - lfm2_moe - lfm2_vl diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 98a1659b1c..0d1588f992 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f643b88"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@242b245"`' ) @@ -96,7 +96,11 @@ def pre_model_load(self, cfg): ) # The patch checks model_type internally - cce_patch(cfg.model_config_type) + + cce_patch( + cfg.model_config_type, + remote_model_id=cfg.base_model if cfg.trust_remote_code else None, + ) def patch_llama_like( self, @@ -107,7 +111,9 @@ def patch_llama_like( """ from cut_cross_entropy.transformers.patch import PATCH_FNS - def patch_generic(maybe_model, patch_options, model_type: str): + def patch_generic( + maybe_model, patch_options, model_type: str, remote_model_id: str | None + ): import cut_cross_entropy.transformers.llama from cut_cross_entropy.transformers.llama import cce_forward diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 81e4dd7862..c319822621 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -26,6 +26,48 @@ class PatchManager: """Manages the application of patches during the model loading process.""" + @staticmethod + def apply_pre_config_load_patches(cfg: DictDefault): + """ + Apply patches that must be set up before config loading. + This is for patches that intercept remote code loading from HuggingFace, + which needs to be in place before AutoConfig.from_pretrained() is called. + + Args: + cfg: Configuration dictionary with model and training settings. + """ + if ( + hasattr(cfg, "base_model_config") + and cfg.base_model_config + and "kimi-linear" in cfg.base_model_config.lower() + ): + from axolotl.monkeypatch.models.kimi_linear.patch_kimi_linear import ( + patch_kimi_config, + ) + + patch_kimi_config() + + @staticmethod + def apply_pre_tokenizer_load_patches(cfg: DictDefault): + """ + Apply patches that must be set up before tokenizer loading. + This is for patches that intercept remote code loading from HuggingFace, + which needs to be in place before AutoTokenizer.from_pretrained() is called. + + Args: + cfg: Configuration dictionary with model and training settings. + """ + if ( + hasattr(cfg, "tokenizer_config") + and cfg.tokenizer_config + and "kimi-linear" in cfg.tokenizer_config.lower() + ): + from axolotl.monkeypatch.models.kimi_linear.patch_kimi_linear import ( + patch_kimi_tokenizer, + ) + + patch_kimi_tokenizer() + def __init__( self, cfg: DictDefault, @@ -190,6 +232,13 @@ def _apply_model_specific_patches(self): apply_mistral_tokenizer_image_patch() + if self.cfg.model_config_type == "kimi_linear": + from axolotl.monkeypatch.models.kimi_linear.patch_kimi_linear import ( + patch_kimi_model, + ) + + patch_kimi_model() + def _apply_fp8_patches(self): """Apply patches for FP8 support.""" if self.cfg.fp8: diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index 48856116c0..170ebf333f 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -124,6 +124,11 @@ def modify_tokenizer_files( def load_tokenizer(cfg: DictDefault) -> PreTrainedTokenizer: """Load and configure the tokenizer based on the provided config.""" + # Apply patches that need to be in place before tokenizer loading + from axolotl.loaders.patch_manager import PatchManager + + PatchManager.apply_pre_tokenizer_load_patches(cfg) + def _load_mistral_common_tokenizer(cfg: DictDefault): """Load mistral-common tokenizer""" from axolotl.utils.mistral import HFMistralTokenizer diff --git a/src/axolotl/monkeypatch/models/kimi_linear/__init__.py b/src/axolotl/monkeypatch/models/kimi_linear/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/kimi_linear/configuration_kimi.py b/src/axolotl/monkeypatch/models/kimi_linear/configuration_kimi.py new file mode 100644 index 0000000000..1dd0e67025 --- /dev/null +++ b/src/axolotl/monkeypatch/models/kimi_linear/configuration_kimi.py @@ -0,0 +1,148 @@ +""" +Kimi-Linear configuration. + +Source: https://huggingface.co/moonshotai/Kimi-Linear-48B-A3B-Instruct/blob/main/configuration_kimi.py +Revision: 6e163f3 +""" + +from typing import Optional + +from transformers.configuration_utils import PretrainedConfig + + +class KimiLinearConfig(PretrainedConfig): + model_type = "kimi_linear" + keys_to_ignore_at_inference = ["past_key_values"] + + def __init__( + self, + model_type="kimi_linear", + vocab_size=163840, + hidden_size=4096, + head_dim=None, + intermediate_size=11008, + num_hidden_layers=32, + num_attention_heads=32, + num_key_value_heads=None, + hidden_act="silu", + initializer_range=0.02, + rms_norm_eps=1e-6, + use_cache=True, + pad_token_id=0, + bos_token_id=1, + eos_token_id=2, + rope_theta=10000.0, + rope_scaling=None, + tie_word_embeddings=False, + moe_intermediate_size: Optional[int] = None, + moe_renormalize: bool = True, + moe_router_activation_func: str = "sigmoid", + num_experts: Optional[int] = None, + num_experts_per_token: Optional[int] = None, + num_shared_experts: int = 0, + routed_scaling_factor: float = 1.0, + first_k_dense_replace: int = 0, + moe_layer_freq: int = 1, + use_grouped_topk: bool = True, + num_expert_group: int = 1, + topk_group: int = 1, + q_lora_rank: Optional[int] = None, + kv_lora_rank: Optional[int] = None, + qk_nope_head_dim: Optional[int] = None, + qk_rope_head_dim: Optional[int] = None, + v_head_dim: Optional[int] = None, + mla_use_nope: Optional[bool] = False, + num_nextn_predict_layers: int = 0, + linear_attn_config: Optional[dict] = None, + router_aux_loss_coef: float = 0.01, + **kwargs, + ): + self.model_type = model_type + self.vocab_size = vocab_size + self.hidden_size = hidden_size + self.head_dim = ( + head_dim if head_dim is not None else hidden_size // num_attention_heads + ) + self.intermediate_size = intermediate_size + self.num_hidden_layers = num_hidden_layers + self.num_attention_heads = num_attention_heads + + # for backward compatibility + if num_key_value_heads is None: + num_key_value_heads = num_attention_heads + + self.num_key_value_heads = num_key_value_heads + self.hidden_act = hidden_act + self.initializer_range = initializer_range + self.rms_norm_eps = rms_norm_eps + self.use_cache = use_cache + self.rope_theta = rope_theta + self.rope_scaling = rope_scaling + + self.q_lora_rank = q_lora_rank + self.kv_lora_rank = kv_lora_rank + self.qk_nope_head_dim = qk_nope_head_dim + self.qk_rope_head_dim = qk_rope_head_dim + self.v_head_dim = v_head_dim + self.mla_use_nope = mla_use_nope + # moe config + self.num_experts = num_experts + self.num_experts_per_token = num_experts_per_token + self.moe_renormalize = moe_renormalize + self.num_shared_experts = num_shared_experts + self.routed_scaling_factor = routed_scaling_factor + self.moe_router_activation_func = moe_router_activation_func + assert self.moe_router_activation_func in ("softmax", "sigmoid") + self.moe_intermediate_size = moe_intermediate_size + self.first_k_dense_replace = first_k_dense_replace + self.moe_layer_freq = moe_layer_freq + self.use_grouped_topk = use_grouped_topk + self.num_expert_group = num_expert_group + self.topk_group = topk_group + self.num_nextn_predict_layers = num_nextn_predict_layers + self.router_aux_loss_coef = router_aux_loss_coef + + if linear_attn_config is not None: + assert linear_attn_config["kda_layers"] is not None + assert linear_attn_config["full_attn_layers"] is not None + self.linear_attn_config = linear_attn_config + + super().__init__( + pad_token_id=pad_token_id, + bos_token_id=bos_token_id, + eos_token_id=eos_token_id, + tie_word_embeddings=tie_word_embeddings, + **kwargs, + ) + + @property + def is_mla(self): + return ( + self.q_lora_rank is not None + or self.kv_lora_rank is not None + or self.qk_nope_head_dim is not None + or self.qk_rope_head_dim is not None + or self.v_head_dim is not None + or self.mla_use_nope is True + ) + + @property + def is_moe(self): + return self.num_experts is not None + + @property + def is_linear_attn(self) -> bool: + return not ( + self.linear_attn_config is None + or ( + isinstance(self.linear_attn_config, dict) + and self.linear_attn_config["kda_layers"] is not None + and len(self.linear_attn_config["kda_layers"]) == 0 + ) + ) + + def is_kda_layer(self, layer_idx: int): + return ( + self.linear_attn_config is not None + and (layer_idx + 1) in self.linear_attn_config["kda_layers"] + ) diff --git a/src/axolotl/monkeypatch/models/kimi_linear/modeling_kimi.py b/src/axolotl/monkeypatch/models/kimi_linear/modeling_kimi.py new file mode 100644 index 0000000000..42a11ec365 --- /dev/null +++ b/src/axolotl/monkeypatch/models/kimi_linear/modeling_kimi.py @@ -0,0 +1,1361 @@ +""" +Adapted Kimi-Linear modeling to enable MoE differentiable. + +Source: https://huggingface.co/moonshotai/Kimi-Linear-48B-A3B-Instruct/blob/main/modeling_kimi.py +Revision: 6e163f3 +""" + +import math +from collections.abc import Callable +from typing import Any, List, Optional, Tuple, Union + +import torch +import torch.nn.functional as F +import transformers +from einops import rearrange +from packaging import version +from torch import nn +from transformers.activations import ACT2FN +from transformers.cache_utils import Cache +from transformers.generation import GenerationMixin +from transformers.masking_utils import create_causal_mask +from transformers.modeling_flash_attention_utils import FlashAttentionKwargs +from transformers.modeling_outputs import ( + BaseModelOutputWithPast, + CausalLMOutputWithPast, + MoeCausalLMOutputWithPast, +) +from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel +from transformers.processing_utils import Unpack +from transformers.pytorch_utils import ALL_LAYERNORM_LAYERS +from transformers.utils import ( + TransformersKwargs, + can_return_tuple, + logging, +) +from transformers.utils.generic import OutputRecorder + +try: + from fla.layers.utils import get_unpad_data, index_first_axis, pad_input + from fla.modules import FusedRMSNormGated, ShortConvolution + from fla.ops.kda import chunk_kda, fused_recurrent_kda + from fla.ops.kda.gate import fused_kda_gate +except ImportError as err: + raise ImportError( + "Plese run `pip uninstall fla-core flash-linear-attention -y && pip install git+https://github.com/fla-org/flash-linear-attention@v0.4.0`" + ) from err + +from axolotl.monkeypatch.models.kimi_linear.configuration_kimi import KimiLinearConfig + +assert version.parse(transformers.__version__) >= version.parse("4.56.0"), ( + "Please upgrade transformers to >= 4.56.0" +) + +logger = logging.get_logger(__name__) + + +def load_balancing_loss_func( + gate_logits: Union[torch.Tensor, tuple[torch.Tensor], None], + num_experts: Optional[int] = None, + top_k=2, + attention_mask: Optional[torch.Tensor] = None, +) -> Union[torch.Tensor, int]: + """Standard Switch Transformer load balancing loss.""" + if gate_logits is None or not isinstance(gate_logits, tuple): + return 0 + + # Concatenate all layer logits + concatenated_gate_logits = torch.cat( + [layer_gate for layer_gate in gate_logits], dim=0 + ) + + routing_weights = F.softmax(concatenated_gate_logits, dim=-1) + _, selected_experts = torch.topk(routing_weights, top_k, dim=-1) + expert_mask = F.one_hot(selected_experts, num_experts) + + tokens_per_expert = torch.mean(expert_mask.float(), dim=0) + router_prob_per_expert = torch.mean(routing_weights, dim=0) + + overall_loss = torch.sum(tokens_per_expert * router_prob_per_expert.unsqueeze(0)) + return overall_loss * num_experts + + +class KimiDynamicCache: + """ + Dynamic cache for Kimi model. + Inspired by Qwen3-Next + """ + + is_compileable = False + + def __init__(self, config: KimiLinearConfig): + super().__init__() + self.config = config + + if config.linear_attn_config is not None: + self.layer_types = [] + for i in range(config.num_hidden_layers): + if config.is_kda_layer(i): + self.layer_types.append("linear_attention") + else: + self.layer_types.append("full_attention") + else: + self.layer_types = ["full_attention"] * config.num_hidden_layers + + self.transformer_layers = [ + i + for i in range(config.num_hidden_layers) + if self.layer_types[i] == "full_attention" + ] + + linear_layers = [ + i + for i in range(config.num_hidden_layers) + if self.layer_types[i] == "linear_attention" + ] + self.last_linear_layer = linear_layers[-1] if linear_layers else -1 + + self.conv_states = [None for _ in range(config.num_hidden_layers)] + self.recurrent_states = [None for _ in range(config.num_hidden_layers)] + self.key_cache = [None for _ in range(config.num_hidden_layers)] + self.value_cache = [None for _ in range(config.num_hidden_layers)] + + def __len__(self): + return len(self.layer_types) + + def update( + self, + key_states: torch.Tensor, + value_states: torch.Tensor, + layer_idx: int, + cache_kwargs: Optional[dict[str, Any]] = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + if self.key_cache[layer_idx] is None: + self.key_cache[layer_idx] = key_states + self.value_cache[layer_idx] = value_states + else: + self.key_cache[layer_idx] = torch.cat( + [self.key_cache[layer_idx], key_states], dim=2 + ) + self.value_cache[layer_idx] = torch.cat( + [self.value_cache[layer_idx], value_states], dim=2 + ) + + return self.key_cache[layer_idx], self.value_cache[layer_idx] + + def reorder_cache(self, beam_idx: torch.LongTensor): + """Reorders the cache for beam search, given the selected beam indices.""" + for layer_idx in range(len(self.key_cache)): + if self.key_cache[layer_idx] is not None: + device = self.key_cache[layer_idx].device + beam_idx = beam_idx.to(device) + self.key_cache[layer_idx] = self.key_cache[layer_idx].index_select( + 0, beam_idx + ) + self.value_cache[layer_idx] = self.value_cache[layer_idx].index_select( + 0, beam_idx + ) + + if self.conv_states[layer_idx] is not None: + device = self.conv_states[layer_idx][0].device + beam_idx = beam_idx.to(device) + q_conv, k_conv, v_conv = self.conv_states[layer_idx] + self.conv_states[layer_idx] = ( + q_conv.index_select(0, beam_idx), + k_conv.index_select(0, beam_idx), + v_conv.index_select(0, beam_idx), + ) + self.recurrent_states[layer_idx] = self.recurrent_states[ + layer_idx + ].index_select(0, beam_idx) + + def get_seq_length(self, layer_idx: Optional[int] = 0) -> int: + """Returns the sequence length of the cached states. A layer index can be optionally passed.""" + # take any layer that contains cache and not empty tensor + layer_idx = ( + self.transformer_layers[0] + if layer_idx not in self.transformer_layers + else layer_idx + ) + if len(self.key_cache) <= layer_idx or self.key_cache[layer_idx] is None: + return 0 + return self.key_cache[layer_idx].shape[-2] + + def get_mask_sizes( + self, cache_position: torch.Tensor, layer_idx: int + ) -> tuple[int, int]: + """ + Return a tuple (kv_length, kv_offset) corresponding to the length and offset that will be returned for + the given layer at `layer_idx`. + The masks are then prepared according to the given lengths (kv_length, kv_offset) and patterns for each layer. + """ + kv_offset = 0 + query_length = cache_position.shape[0] + past_seen_tokens = self.get_seq_length(layer_idx) + kv_length = query_length + past_seen_tokens + return kv_length, kv_offset + + @property + def has_previous_state(self): + """We have a previous state if the last linear (conv) layer was already updated.""" + if self.last_linear_layer == -1: + return False + return self.conv_states[self.last_linear_layer] is not None + + +class KimiRMSNorm(nn.Module): + def __init__(self, hidden_size, eps=1e-6): + """ + KimiRMSNorm is equivalent to T5LayerNorm + """ + super().__init__() + self.weight = nn.Parameter(torch.ones(hidden_size)) + self.variance_epsilon = eps + + def forward(self, hidden_states): + input_dtype = hidden_states.dtype + hidden_states = hidden_states.to(torch.float32) + variance = hidden_states.pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon) + return self.weight * hidden_states.to(input_dtype) + + +ALL_LAYERNORM_LAYERS.append(KimiRMSNorm) + + +class KimiBlockSparseMLP(nn.Module): + def __init__( + self, config: KimiLinearConfig, hidden_size=None, intermediate_size=None + ): + super().__init__() + self.config = config + self.ffn_dim = ( + config.intermediate_size if intermediate_size is None else intermediate_size + ) + self.hidden_dim = config.hidden_size if hidden_size is None else hidden_size + + self.w1 = nn.Linear(self.hidden_dim, self.ffn_dim, bias=False) # gate + self.w2 = nn.Linear(self.ffn_dim, self.hidden_dim, bias=False) # down + self.w3 = nn.Linear(self.hidden_dim, self.ffn_dim, bias=False) # up + + self.act_fn = ACT2FN[config.hidden_act] + + def forward(self, hidden_states): + current_hidden_states = self.act_fn(self.w1(hidden_states)) * self.w3( + hidden_states + ) + current_hidden_states = self.w2(current_hidden_states) + return current_hidden_states + + +class KimiMLP(nn.Module): + def __init__( + self, config: KimiLinearConfig, hidden_size=None, intermediate_size=None + ): + super().__init__() + self.config = config + self.hidden_size = config.hidden_size if hidden_size is None else hidden_size + self.intermediate_size = ( + config.intermediate_size if intermediate_size is None else intermediate_size + ) + self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) + self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) + self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False) + self.act_fn = ACT2FN[config.hidden_act] + + def forward(self, x): + down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) + return down_proj + + +def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: + """ + This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch, + num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim) + """ + batch, num_key_value_heads, slen, head_dim = hidden_states.shape + if n_rep == 1: + return hidden_states + hidden_states = hidden_states[:, :, None, :, :].expand( + batch, num_key_value_heads, n_rep, slen, head_dim + ) + return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim) + + +def eager_attention_forward( + module: nn.Module, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + attention_mask: Optional[torch.Tensor], + scaling: float, + dropout: float = 0.0, + **kwargs: Unpack[TransformersKwargs], +): + key_states = repeat_kv(key, module.num_key_value_groups) + value_states = repeat_kv(value, module.num_key_value_groups) + + attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling + if attention_mask is not None: + causal_mask = attention_mask[:, :, :, : key_states.shape[-2]] + attn_weights = attn_weights + causal_mask + + attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to( + query.dtype + ) + attn_weights = nn.functional.dropout( + attn_weights, p=dropout, training=module.training + ) + attn_output = torch.matmul(attn_weights, value_states) + attn_output = attn_output.transpose(1, 2).contiguous() + + return attn_output, attn_weights + + +class KimiMLAAttention(nn.Module): + """ + Multi-Latent Attention adapted from deepseek-v3 + """ + + def __init__(self, config: KimiLinearConfig, layer_idx: int): + nn.Module.__init__(self) + self.config = config + self.layer_idx = layer_idx + self.hidden_size = config.hidden_size + self.num_heads = config.num_attention_heads + self.num_key_value_heads = config.num_key_value_heads + self.num_key_value_groups = self.num_heads // self.num_key_value_heads + + self.rope_theta = config.rope_theta + self.attention_dropout = getattr(config, "attention_dropout", 0.0) + + try: + self.q_lora_rank = config.q_lora_rank + self.qk_rope_head_dim = config.qk_rope_head_dim + self.kv_lora_rank = config.kv_lora_rank + self.v_head_dim = config.v_head_dim + self.qk_nope_head_dim = config.qk_nope_head_dim + self.q_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim + self.use_nope = config.mla_use_nope + self.scaling = self.q_head_dim ** (-0.5) + except Exception as e: + raise ValueError( + f"Kimi MLA config is not found or not properly formatted: {e}" + ) from e + + assert self.q_lora_rank is None + self.q_proj = nn.Linear( + self.hidden_size, + self.num_heads * self.q_head_dim, + bias=False, + ) + self.kv_a_proj_with_mqa = nn.Linear( + self.hidden_size, + self.kv_lora_rank + self.qk_rope_head_dim, + bias=False, + ) + self.kv_a_layernorm = KimiRMSNorm(self.kv_lora_rank) + self.kv_b_proj = nn.Linear( + self.kv_lora_rank, + self.num_heads + * (self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim), + bias=False, + ) + self.o_proj = nn.Linear( + self.num_heads * self.v_head_dim, + self.hidden_size, + bias=False, + ) + self.is_causal = True + assert self.use_nope + + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + past_key_values: Optional[Cache] = None, + **kwargs, + ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: + batch_size, seq_length = hidden_states.shape[:-1] + query_shape = (batch_size, seq_length, -1, self.q_head_dim) + key_shape = ( + batch_size, + seq_length, + -1, + self.qk_nope_head_dim + self.v_head_dim, + ) + + q_states = self.q_proj(hidden_states) + q_states = q_states.view(query_shape).transpose(1, 2) + q_pass, q_rot = torch.split( + q_states, [self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1 + ) + + compressed_kv = self.kv_a_proj_with_mqa(hidden_states) + k_pass, k_rot = torch.split( + compressed_kv, [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1 + ) + + k_pass = ( + self.kv_b_proj(self.kv_a_layernorm(k_pass)).view(key_shape).transpose(1, 2) + ) + k_pass, value_states = torch.split( + k_pass, [self.qk_nope_head_dim, self.v_head_dim], dim=-1 + ) + + k_rot = k_rot.view(batch_size, 1, seq_length, self.qk_rope_head_dim) + k_rot = k_rot.expand(*k_pass.shape[:-1], -1) + + query_states = torch.cat((q_pass, q_rot), dim=-1) + key_states = torch.cat((k_pass, k_rot), dim=-1) + + if past_key_values is not None: + key_states, value_states = past_key_values.update( + key_states, value_states, self.layer_idx + ) + + if ( + self.config._attn_implementation == "flash_attention_2" + and self.q_head_dim != self.v_head_dim + ): + value_states = F.pad(value_states, [0, self.q_head_dim - self.v_head_dim]) + + attention_interface: Callable = eager_attention_forward + if self.config._attn_implementation != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS[ + self.config._attn_implementation + ] + + attn_output, _ = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + **kwargs, + ) + + if ( + self.config._attn_implementation == "flash_attention_2" + and self.q_head_dim != self.v_head_dim + ): + attn_output = attn_output[:, :, :, : self.v_head_dim] + + attn_output = attn_output.reshape(batch_size, seq_length, -1).contiguous() + attn_output = self.o_proj(attn_output) + return attn_output + + +class KimiDeltaAttention(nn.Module): + def __init__(self, config: KimiLinearConfig, layer_idx: int): + super().__init__() + self.config = config + self.mode = "chunk" + + self.hidden_size = config.hidden_size + self.conv_size = config.linear_attn_config["short_conv_kernel_size"] + self.head_dim = config.linear_attn_config["head_dim"] + self.num_heads = config.linear_attn_config["num_heads"] + self.head_k_dim = self.head_dim + self.num_k_heads = self.num_heads + + self.layer_idx = layer_idx + + assert self.mode in ["chunk", "fused_recurrent"], ( + f"Not suppoerted mode `{self.mode}`." + ) + + projection_k_size = self.head_k_dim * self.num_k_heads + projection_size = self.head_dim * self.num_heads + + self.q_proj = nn.Linear(self.hidden_size, projection_k_size, bias=False) + self.k_proj = nn.Linear(self.hidden_size, projection_k_size, bias=False) + self.v_proj = nn.Linear(self.hidden_size, projection_size, bias=False) + + self.q_conv1d = ShortConvolution( + hidden_size=projection_k_size, + kernel_size=self.conv_size, + activation="silu", + ) + self.k_conv1d = ShortConvolution( + hidden_size=projection_k_size, kernel_size=self.conv_size, activation="silu" + ) + self.v_conv1d = ShortConvolution( + hidden_size=projection_size, kernel_size=self.conv_size, activation="silu" + ) + + self.A_log = torch.nn.Parameter( + torch.log( + torch.empty(self.num_heads, dtype=torch.float32).uniform_(1, 16) + ).view(1, 1, -1, 1) + ) + + self.f_a_proj = nn.Linear(self.hidden_size, self.head_dim, bias=False) + self.f_b_proj = nn.Linear(self.head_dim, projection_size, bias=False) + + self.dt_bias = nn.Parameter(torch.empty(projection_size, dtype=torch.float32)) + + self.b_proj = nn.Linear(self.hidden_size, self.num_heads, bias=False) + + self.g_a_proj = nn.Linear(self.hidden_size, self.head_dim, bias=False) + self.g_b_proj = nn.Linear(self.head_dim, projection_size, bias=False) + + self.o_norm = FusedRMSNormGated( + self.head_dim, eps=config.rms_norm_eps, activation="sigmoid" + ) + self.o_proj = nn.Linear(projection_size, self.hidden_size, bias=False) + + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + cache_params: Optional[KimiDynamicCache] = None, + **kwargs: Unpack[dict], + ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Cache]]: + if attention_mask is not None: + if attention_mask.dim() != 2: + attention_mask = kwargs.get("padding_mask", None) + + if attention_mask is not None and attention_mask.dim() != 2: + raise ValueError( + "attention_mask must be a 0-1 matrix of shape [batch_size, seq_len] " + "(0 = padding). 3D masks are not supported here." + ) + use_cache = cache_params is not None + batch_size, q_len, _ = hidden_states.shape + mode = "fused_recurrent" if q_len <= 64 else self.mode + if self.training: + assert mode == "chunk", "Only chunk mode is supported in training." + + cu_seqlens = kwargs.get("cu_seqlens", None) + indices = None + if attention_mask is not None: + indices, cu_seqlens, _ = get_unpad_data(attention_mask[:, -q_len:]) + hidden_states = index_first_axis( + rearrange(hidden_states, "b s ... -> (b s) ..."), indices + ).unsqueeze(0) + + conv_state_q, conv_state_k, conv_state_v = None, None, None + recurrent_state = None + if cache_params is not None: + if cache_params.conv_states[self.layer_idx] is not None: + conv_state_q, conv_state_k, conv_state_v = cache_params.conv_states[ + self.layer_idx + ] + recurrent_state = cache_params.recurrent_states[self.layer_idx] + q, conv_state_q = self.q_conv1d( + x=self.q_proj(hidden_states), + cache=conv_state_q, + output_final_state=use_cache, + cu_seqlens=cu_seqlens, + ) + k, conv_state_k = self.k_conv1d( + x=self.k_proj(hidden_states), + cache=conv_state_k, + output_final_state=use_cache, + cu_seqlens=cu_seqlens, + ) + v, conv_state_v = self.v_conv1d( + x=self.v_proj(hidden_states), + cache=conv_state_v, + output_final_state=use_cache, + cu_seqlens=cu_seqlens, + ) + g = self.f_b_proj(self.f_a_proj(hidden_states)) + g = fused_kda_gate(g, self.A_log, self.head_dim, g_bias=self.dt_bias) + beta = self.b_proj(hidden_states).float().sigmoid() + + q, k = map( + lambda x: rearrange(x, "... (h d) -> ... h d", d=self.head_k_dim), (q, k) + ) + v = rearrange(v, "... (h d) -> ... h d", d=self.head_dim) + + if mode == "chunk": + o, recurrent_state = chunk_kda( + q=q, + k=k, + v=v, + g=g, + beta=beta, + initial_state=recurrent_state, + output_final_state=True, + use_qk_l2norm_in_kernel=True, + cu_seqlens=cu_seqlens, + ) + else: + o, recurrent_state = fused_recurrent_kda( + q=q, + k=k, + v=v, + g=g, + beta=beta, + initial_state=recurrent_state, + output_final_state=True, + use_qk_l2norm_in_kernel=True, + cu_seqlens=cu_seqlens, + ) + if cache_params is not None: + cache_params.recurrent_states[self.layer_idx] = recurrent_state + cache_params.conv_states[self.layer_idx] = ( + conv_state_q, + conv_state_k, + conv_state_v, + ) + + g = self.g_b_proj(self.g_a_proj(hidden_states)) + g = rearrange(g, "... (h d) -> ... h d", d=self.head_dim) + o = self.o_norm(o, g) + + o = rearrange(o, "b t h d -> b t (h d)") + o = self.o_proj(o) + if attention_mask is not None: + o = pad_input(o.squeeze(0), indices, batch_size, q_len) + + return o + + +class KimiMoEGate(nn.Module): + """ + MoE Gate that returns router logits. + Routing decisions are made in KimiSparseMoeBlock. + """ + + def __init__(self, config: KimiLinearConfig): + super().__init__() + self.config = config + self.num_experts = config.num_experts + self.gating_dim = config.hidden_size + + self.weight = nn.Parameter(torch.empty((self.num_experts, self.gating_dim))) + self.e_score_correction_bias = nn.Parameter(torch.zeros((self.num_experts,))) + self.reset_parameters() + + def reset_parameters(self) -> None: + import torch.nn.init as init + + init.kaiming_uniform_(self.weight, a=math.sqrt(5)) + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + """ + Args: + hidden_states: [batch_size, seq_len, hidden_dim] + + Returns: + router_logits: [batch_size * seq_len, num_experts] + """ + _, _, h = hidden_states.shape + hidden_states = hidden_states.view(-1, h) + router_logits = F.linear( + hidden_states.type(torch.float32), self.weight.type(torch.float32), None + ) + return router_logits + + # def forward(self, hidden_states): + # bsz, seq_len, h = hidden_states.shape + # # compute gating score + # hidden_states = hidden_states.view(-1, h) + # logits = F.linear( + # hidden_states.type(torch.float32), self.weight.type( + # torch.float32), None + # ) + # if self.moe_router_activation_func == "sigmoid": + # scores = logits.sigmoid() + # elif self.moe_router_activation_func == "softmax": + # scores = logits.softmax(dim=1) + # else: + # raise NotImplementedError( + # f"insupportable scoring function for MoE gating: {self.moe_router_activation_func}" + # ) + + # # select top-k experts + # assert not self.training + # scores_for_choice = scores.view(bsz * seq_len, -1) + # scores_for_choice += self.e_score_correction_bias.unsqueeze(0) + # group_scores = ( + # scores_for_choice.view( + # bsz * seq_len, self.num_expert_group, -1).topk(2, dim=-1)[0].sum(dim=-1) + # ) # [n, num_expert_group] + # group_idx = torch.topk( + # group_scores, k=self.topk_group, dim=-1, sorted=False + # )[ + # 1 + # ] # [n, top_k_group] + # group_mask = torch.zeros_like(group_scores) # [n, num_expert_group] + # group_mask.scatter_(1, group_idx, 1) # [n, num_expert_group] + # score_mask = ( + # group_mask.unsqueeze(-1) + # .expand( + # bsz * seq_len, self.num_expert_group, self.num_experts // self.num_expert_group + # ) + # .reshape(bsz * seq_len, -1) + # ) # [n, e] + # tmp_scores = scores_for_choice.masked_fill( + # ~score_mask.bool(), 0.0) # [n, e] + # _, topk_idx = torch.topk( + # tmp_scores, k=self.top_k, dim=-1, sorted=False + # ) + # topk_weight = scores.gather(1, topk_idx) + + # # norm gate to sum 1 + # if self.top_k > 1 and self.moe_renormalize: + # denominator = topk_weight.sum(dim=-1, keepdim=True) + 1e-20 + # topk_weight = topk_weight / denominator + # # must multiply the scaling factor + # topk_weight = topk_weight * self.routed_scaling_factor + + # return topk_idx, topk_weight + + +# class KimiSparseMoeBlock(nn.Module): +# """ +# Adapted from Deepseek-V3's MOE implementation +# The namings are consistent with Kimi's version. +# """ + +# def __init__(self, config: KimiLinearConfig): +# super().__init__() +# self.config = config +# self.hidden_dim = config.hidden_size +# self.num_experts = config.num_experts +# self.top_k = config.num_experts_per_token +# self.moe_renormalize = config.moe_renormalize + +# self.ep_size = 1 +# self.experts_per_rank = config.num_experts +# self.ep_rank = 0 +# self.experts = nn.ModuleList( +# [ +# KimiBlockSparseMLP( +# config, intermediate_size=config.moe_intermediate_size +# ) +# for _ in range(config.num_experts) +# ] +# ) +# self.gate = KimiMoEGate(config) +# if config.num_shared_experts is not None: +# intermediate_size = config.moe_intermediate_size * config.num_shared_experts +# self.shared_experts = KimiMLP( +# config=config, intermediate_size=intermediate_size +# ) + +# def forward(self, hidden_states): +# identity = hidden_states +# orig_shape = hidden_states.shape +# topk_idx, topk_weight = self.gate(hidden_states) +# hidden_states = hidden_states.view(-1, hidden_states.shape[-1]) +# flat_topk_idx = topk_idx.view(-1) +# if not self.training: +# y = self.moe_infer(hidden_states, topk_idx, +# topk_weight).view(*orig_shape) +# else: +# raise NotImplementedError( +# "Training mode is not supported in KimiSparseMoeBlock") +# if self.config.num_shared_experts is not None: +# y = y + self.shared_experts(identity) +# return y + +# @torch.no_grad() +# def moe_infer(self, x, topk_ids, topk_weight): +# cnts = topk_ids.new_zeros((topk_ids.shape[0], len(self.experts))) +# cnts.scatter_(1, topk_ids, 1) +# tokens_per_expert = cnts.sum(dim=0) +# idxs = topk_ids.view(-1).argsort() +# sorted_tokens = x[idxs // topk_ids.shape[1]] + +# tokens_per_expert = tokens_per_expert.cpu().numpy() + +# outputs = [] +# start_idx = 0 +# for i, num_tokens in enumerate(tokens_per_expert): +# end_idx = start_idx + num_tokens +# if num_tokens == 0: +# continue +# expert = self.experts[i + self.ep_rank * self.experts_per_rank] +# tokens_for_this_expert = sorted_tokens[start_idx:end_idx] +# expert_out = expert(tokens_for_this_expert) +# outputs.append(expert_out) +# start_idx = end_idx + +# outs = torch.cat(outputs, dim=0) if len( +# outputs) else sorted_tokens.new_empty(0) + +# new_x = torch.empty_like(outs) +# new_x[idxs] = outs +# final_out = ( +# new_x.view(*topk_ids.shape, -1) +# .type(topk_weight.dtype) +# .mul_(topk_weight.unsqueeze(dim=-1)) +# .sum(dim=1) +# .type(new_x.dtype) +# ) +# return final_out + + +# Replace the KimiSparseMoeBlock class with this new version +class KimiSparseMoeBlock(nn.Module): + """ + MoE block adapted from Deepseek-V3. + Returns only hidden_states - router_logits captured by OutputRecorder. + """ + + def __init__(self, config: KimiLinearConfig): + super().__init__() + self.config = config + self.hidden_dim = config.hidden_size + self.num_experts = config.num_experts + self.top_k = config.num_experts_per_token + self.moe_renormalize = config.moe_renormalize + self.routed_scaling_factor = config.routed_scaling_factor + self.num_expert_group = getattr(config, "num_expert_group", 1) + self.topk_group = getattr(config, "topk_group", 1) + + self.experts = nn.ModuleList( + [ + KimiBlockSparseMLP( + config, intermediate_size=config.moe_intermediate_size + ) + for _ in range(config.num_experts) + ] + ) + self.gate = KimiMoEGate(config) + + if config.num_shared_experts is not None: + intermediate_size = config.moe_intermediate_size * config.num_shared_experts + self.shared_experts = KimiMLP( + config=config, intermediate_size=intermediate_size + ) + + def route_tokens_to_experts( + self, + router_logits: torch.Tensor, + ) -> tuple[torch.Tensor, torch.Tensor]: + """ + Compute routing decisions from router logits. + + Args: + router_logits: [num_tokens, num_experts] + + Returns: + topk_idx: [num_tokens, top_k] + topk_weight: [num_tokens, top_k] + """ + num_tokens = router_logits.shape[0] + + if self.training: + # Training: use softmax for standard aux loss compatibility + scores = F.softmax(router_logits, dim=-1, dtype=torch.float32) + topk_weight, topk_idx = torch.topk(scores, self.top_k, dim=-1, sorted=False) + else: + # Inference: use original sigmoid + group selection + scores = router_logits.sigmoid() + scores_for_choice = scores + self.gate.e_score_correction_bias.unsqueeze(0) + + # Group-based selection + group_scores = ( + scores_for_choice.view(num_tokens, self.num_expert_group, -1) + .topk(2, dim=-1)[0] + .sum(dim=-1) + ) + group_idx = torch.topk( + group_scores, k=self.topk_group, dim=-1, sorted=False + )[1] + group_mask = torch.zeros_like(group_scores) + group_mask.scatter_(1, group_idx, 1) + score_mask = ( + group_mask.unsqueeze(-1) + .expand( + num_tokens, + self.num_expert_group, + self.num_experts // self.num_expert_group, + ) + .reshape(num_tokens, -1) + ) + tmp_scores = scores_for_choice.masked_fill(~score_mask.bool(), 0.0) + _, topk_idx = torch.topk(tmp_scores, k=self.top_k, dim=-1, sorted=False) + topk_weight = scores.gather(1, topk_idx) + + # Normalize and scale + if self.top_k > 1 and self.moe_renormalize: + denominator = topk_weight.sum(dim=-1, keepdim=True) + 1e-20 + topk_weight = topk_weight / denominator + topk_weight = topk_weight * self.routed_scaling_factor + + return topk_idx, topk_weight + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + """ + Forward pass returning only hidden_states. + Router logits are captured by OutputRecorder for aux loss. + """ + identity = hidden_states + batch_size, seq_len, hidden_dim = hidden_states.shape + num_tokens = batch_size * seq_len + + # Flatten for routing + hidden_states_flat = hidden_states.view(num_tokens, hidden_dim) + + # Get router logits - OutputRecorder captures this! + router_logits = self.gate(hidden_states) + + # Get routing decisions + topk_idx, topk_weight = self.route_tokens_to_experts(router_logits) + + if self.training: + final_hidden_states = self._training_forward( + hidden_states_flat, topk_idx, topk_weight, num_tokens, hidden_dim + ) + else: + final_hidden_states = self._inference_forward( + hidden_states_flat, topk_idx, topk_weight + ) + + final_hidden_states = final_hidden_states.view(batch_size, seq_len, hidden_dim) + + # Add shared experts if present + if self.config.num_shared_experts is not None: + final_hidden_states = final_hidden_states + self.shared_experts(identity) + + return final_hidden_states + + def _training_forward( + self, + hidden_states: torch.Tensor, + topk_idx: torch.Tensor, + topk_weight: torch.Tensor, + num_tokens: int, + hidden_dim: int, + ) -> torch.Tensor: + """ + Differentiable training forward using scatter-gather pattern. + """ + # Flatten expert indices: [num_tokens * top_k] + flat_topk_idx = topk_idx.view(-1) + + # Sort by expert index to group tokens going to same expert + sorted_indices = torch.argsort(flat_topk_idx) + inverse_permutation = torch.argsort(sorted_indices) + + # Each token appears top_k times (once per expert choice) + token_indices = torch.arange( + num_tokens, device=hidden_states.device + ).repeat_interleave(self.top_k) + + # Gather tokens and weights in sorted order + shuffled_tokens = hidden_states[token_indices[sorted_indices]] + shuffled_weights = topk_weight.view(-1)[sorted_indices].unsqueeze(-1) + + # Count tokens per expert + tokens_per_expert = F.one_hot(flat_topk_idx, num_classes=self.num_experts).sum( + dim=0 + ) + + # Process each expert's batch + expert_outputs = [] + current_pos = 0 + for i in range(self.num_experts): + num_tokens_for_expert = tokens_per_expert[i].item() + if num_tokens_for_expert == 0: + continue + + expert_input = shuffled_tokens[ + current_pos : current_pos + num_tokens_for_expert + ] + expert_output = self.experts[i](expert_input) + expert_outputs.append(expert_output) + current_pos += num_tokens_for_expert + + # Concatenate all outputs + if expert_outputs: + concatenated_outputs = torch.cat(expert_outputs, dim=0) + else: + concatenated_outputs = torch.zeros( + num_tokens * self.top_k, + hidden_dim, + device=hidden_states.device, + dtype=hidden_states.dtype, + ) + + # Apply weights while still in sorted order + weighted_outputs = concatenated_outputs * shuffled_weights + + # Unsort back to original token order + unshuffled_outputs = weighted_outputs[inverse_permutation] + + # Sum contributions from all top_k experts for each token + final_hidden_states = unshuffled_outputs.view( + num_tokens, self.top_k, hidden_dim + ).sum(dim=1) + + return final_hidden_states + + @torch.no_grad() + def _inference_forward( + self, + hidden_states: torch.Tensor, + topk_idx: torch.Tensor, + topk_weight: torch.Tensor, + ) -> torch.Tensor: + """ + Optimized inference forward (original implementation). + """ + cnts = topk_idx.new_zeros((topk_idx.shape[0], len(self.experts))) + cnts.scatter_(1, topk_idx, 1) + tokens_per_expert = cnts.sum(dim=0) + idxs = topk_idx.view(-1).argsort() + sorted_tokens = hidden_states[idxs // topk_idx.shape[1]] + + tokens_per_expert_list = tokens_per_expert.cpu().numpy() + + outputs = [] + start_idx = 0 + for i, num_tokens in enumerate(tokens_per_expert_list): + end_idx = start_idx + num_tokens + if num_tokens == 0: + continue + expert = self.experts[i] + tokens_for_expert = sorted_tokens[start_idx:end_idx] + expert_out = expert(tokens_for_expert) + outputs.append(expert_out) + start_idx = end_idx + + outs = torch.cat(outputs, dim=0) if outputs else sorted_tokens.new_empty(0) + + new_x = torch.empty_like(outs) + new_x[idxs] = outs + final_out = ( + new_x.view(*topk_idx.shape, -1) + .type(topk_weight.dtype) + .mul_(topk_weight.unsqueeze(dim=-1)) + .sum(dim=1) + .type(new_x.dtype) + ) + return final_out + + +class KimiDecoderLayer(nn.Module): + def __init__(self, config: KimiLinearConfig, layer_idx: int): + super().__init__() + self.hidden_size = config.hidden_size + self.config = config + if config.is_kda_layer(layer_idx): + self.is_linear_attn = True + self.self_attn = KimiDeltaAttention(config=config, layer_idx=layer_idx) + elif config.is_mla: + self.is_linear_attn = False + self.self_attn = KimiMLAAttention(config=config, layer_idx=layer_idx) + else: + raise NotImplementedError + if ( + config.num_experts is not None + and layer_idx >= config.first_k_dense_replace + and layer_idx % getattr(config, "moe_layer_freq", 1) == 0 + ): + self.block_sparse_moe = KimiSparseMoeBlock(config) + else: + self.mlp = KimiMLP(config) + self.input_layernorm = KimiRMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm = KimiRMSNorm( + config.hidden_size, eps=config.rms_norm_eps + ) + + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Tuple[torch.Tensor]] = None, + output_attentions: Optional[bool] = False, + use_cache: Optional[bool] = False, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> Tuple[ + torch.FloatTensor, Optional[Tuple[torch.FloatTensor, torch.FloatTensor]] + ]: + """ + Args: + hidden_states (`torch.FloatTensor`): input to the layer of shape `(batch, seq_len, embed_dim)` + attention_mask (`torch.FloatTensor`, *optional*): attention mask of size + `(batch, 1, tgt_len, src_len)` where padding elements are indicated by very large negative values. + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under + returned tensors for more detail. + use_cache (`bool`, *optional*): + If set to `True`, `past_key_values` key value states are returned and can be used to speed up decoding + (see `past_key_values`). + past_key_value (`Tuple(torch.FloatTensor)`, *optional*): cached past key and value projection states + """ + + residual = hidden_states + + hidden_states = self.input_layernorm(hidden_states) + + # Self Attention + if self.is_linear_attn is False: + hidden_states = self.self_attn( + hidden_states=hidden_states, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + output_attentions=output_attentions, + use_cache=use_cache, + **kwargs, + ) + else: + hidden_states = self.self_attn( + hidden_states=hidden_states, + attention_mask=attention_mask, + cache_params=past_key_values, + output_attentions=output_attentions, + use_cache=use_cache, + **kwargs, + ) + hidden_states = residual + hidden_states + + # Fully Connected + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + if hasattr(self, "block_sparse_moe"): + hidden_states = self.block_sparse_moe(hidden_states) + else: + hidden_states = self.mlp(hidden_states) + hidden_states = residual + hidden_states + + return hidden_states + + +class KimiPreTrainedModel(PreTrainedModel): + config_class = KimiLinearConfig + base_model_prefix = "model" + supports_gradient_checkpointing = True + _no_split_modules = ["KimiDecoderLayer"] + _skip_keys_device_placement = "past_key_values" + _supports_flash_attn_2 = True + _can_record_outputs = { + "router_logits": OutputRecorder(KimiMoEGate, index=0), + "hidden_states": KimiDecoderLayer, + "attentions": KimiMLAAttention, + } + _is_stateful = True + + def _init_weights(self, module): + std = self.config.initializer_range + if isinstance(module, nn.Linear): + module.weight.data.normal_(mean=0.0, std=std) + if module.bias is not None: + module.bias.data.zero_() + elif isinstance(module, nn.Embedding): + module.weight.data.normal_(mean=0.0, std=std) + if module.padding_idx is not None: + module.weight.data[module.padding_idx].zero_() + + +class KimiLinearModel(KimiPreTrainedModel): + def __init__(self, config: KimiLinearConfig): + super().__init__(config) + self.padding_idx = config.pad_token_id + self.vocab_size = config.vocab_size + + self.embed_tokens = nn.Embedding( + config.vocab_size, config.hidden_size, self.padding_idx + ) + self.layers = nn.ModuleList( + [ + KimiDecoderLayer(config, layer_idx) + for layer_idx in range(config.num_hidden_layers) + ] + ) + self.norm = KimiRMSNorm(config.hidden_size, eps=config.rms_norm_eps) + + if getattr(config, "_attn_implementation", None) is not None: + if config._attn_implementation != "flash_attention_2": + logger.warning_once( + f"Ignoring the provided attention implementation {config._attn_implementation}" + ) + logger.warning_once("Using flash_attention_2 backend instead.") + config._attn_implementation = "flash_attention_2" + else: + config._attn_implementation = "flash_attention_2" + + self._use_flash_attention_2 = config._attn_implementation == "flash_attention_2" + self.gradient_checkpointing = False + # Initialize weights and apply final processing + self.post_init() + + def _update_linear_attn_mask(self, attention_mask, cache_position): + """ + NOTE: Left-padding is used for linear attention mask. + No need for zeroing states when + 1. Cached forward + 2. Attending to all inputs + """ + linear_attn_mask = attention_mask + if cache_position[0] > 0 or ( + attention_mask is not None and torch.all(attention_mask == 1) + ): + linear_attn_mask = None + return linear_attn_mask + + def forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + cache_position: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + **kwargs: Unpack[TransformersKwargs], + ) -> Union[Tuple, BaseModelOutputWithPast]: + use_cache = use_cache if use_cache is not None else self.config.use_cache + + if (input_ids is None) and (inputs_embeds is None): + raise ValueError( + "You must specify exactly one of input_ids or inputs_embeds" + ) + + # Get inputs_embeds + if inputs_embeds is None: + inputs_embeds = self.embed_tokens(input_ids) + + if use_cache and past_key_values is None: + past_key_values = KimiDynamicCache(config=self.config) + + if cache_position is None: + past_seen_tokens = ( + past_key_values.get_seq_length() if past_key_values is not None else 0 + ) + cache_position: torch.Tensor = torch.arange( + past_seen_tokens, + past_seen_tokens + inputs_embeds.shape[1], + device=inputs_embeds.device, + ) + + if position_ids is None: + position_ids = cache_position.unsqueeze(0) + + causal_mask = create_causal_mask( + config=self.config, + input_embeds=inputs_embeds, + attention_mask=attention_mask, + cache_position=cache_position, + past_key_values=past_key_values, + position_ids=position_ids, + ) + linear_attn_mask = self._update_linear_attn_mask(attention_mask, cache_position) + + hidden_states = inputs_embeds + if past_key_values is not None: + assert isinstance(past_key_values, KimiDynamicCache) + + for decoder_layer in self.layers: + layer_mask = ( + linear_attn_mask if decoder_layer.is_linear_attn else causal_mask + ) + + hidden_states = decoder_layer( + hidden_states, + attention_mask=layer_mask, + past_key_values=past_key_values, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = self.norm(hidden_states) + + return BaseModelOutputWithPast( + last_hidden_state=hidden_states, + past_key_values=past_key_values, + ) + + +class KimiLinearForCausalLM(KimiPreTrainedModel, GenerationMixin): + _tied_weights_keys = ["lm_head.weight"] + + def __init__(self, config): + super().__init__(config) + self.model = KimiLinearModel(config) + self.vocab_size = config.vocab_size + self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) + + # Initialize weights and apply final processing + self.post_init() + + @can_return_tuple + def forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[List[torch.FloatTensor]] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + generation_mode: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[TransformersKwargs], + ) -> Union[Tuple, CausalLMOutputWithPast]: + r""" + Args: + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + Returns: + + Example: + + ```python + >>> from transformers import AutoTokenizer, KimiLinearForCausalLM + + >>> model = KimiLinearForCausalLM.from_pretrained(PATH_TO_CONVERTED_WEIGHTS) + >>> tokenizer = AutoTokenizer.from_pretrained(PATH_TO_CONVERTED_TOKENIZER) + + >>> prompt = "Hey, are you conscious? Can you talk to me?" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." + ```""" + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + cache_position=cache_position, + **kwargs, + ) + + logits = outputs[0] + if generation_mode: + logits = logits[:, -1:] + logits = self.lm_head(logits) + + loss = None + if labels is not None: + loss = self.loss_function(logits, labels, self.vocab_size, **kwargs) + + aux_loss = None + if kwargs.get("output_router_logits", False): + aux_loss = load_balancing_loss_func( + outputs.router_logits, + num_experts=self.config.num_experts, + top_k=self.config.num_experts_per_token, + attention_mask=attention_mask, + ) + if loss is not None: + loss = loss + self.config.router_aux_loss_coef * aux_loss + + return MoeCausalLMOutputWithPast( + loss=loss, + aux_loss=aux_loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) diff --git a/src/axolotl/monkeypatch/models/kimi_linear/patch_kimi_linear.py b/src/axolotl/monkeypatch/models/kimi_linear/patch_kimi_linear.py new file mode 100644 index 0000000000..f9d1546d6a --- /dev/null +++ b/src/axolotl/monkeypatch/models/kimi_linear/patch_kimi_linear.py @@ -0,0 +1,85 @@ +import importlib.resources +import importlib.util +import sys +from pathlib import Path + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +KIMI_PATCH_PACKAGE = "axolotl.monkeypatch.models.kimi_linear" + + +def get_patch_file_path(package_dot_path: str, filename: str) -> Path: + """ + Gets the absolute path to a patch file using importlib.resources.files. + """ + try: + return importlib.resources.files(package_dot_path) / filename + except ModuleNotFoundError: + return None + + +def _load_local_module(module_name: str, filename: str): + """Helper to load a local module if not already loaded.""" + if module_name in sys.modules: + return sys.modules[module_name] + + patch_path = get_patch_file_path(KIMI_PATCH_PACKAGE, filename) + if patch_path and patch_path.exists(): + spec = importlib.util.spec_from_file_location(module_name, patch_path) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + return None + + +def _patch_get_class_in_module(): + """ + Core patch function that hijacks Transformers' dynamic module loading. + """ + from transformers.dynamic_module_utils import get_class_in_module + + if hasattr(get_class_in_module, "_axolotl_patched"): + return + + original_get_class_in_module = get_class_in_module + + # Mapping of module path patterns to (module_name, filename) + KIMI_MODULE_MAP = { + "configuration_kimi": ("configuration_kimi", "configuration_kimi.py"), + "modeling_kimi": ("modeling_kimi", "modeling_kimi.py"), + "tokenization_kimi": ("tokenization_kimi", "tokenization_kimi.py"), + } + + def patched_get_class_in_module(class_name, module_path, **kwargs): + """Patched version that returns our local modules instead of remote ones.""" + for pattern, (module_name, filename) in KIMI_MODULE_MAP.items(): + if pattern in module_path: + module = _load_local_module(module_name, filename) + if module: + return getattr(module, class_name) + break # Pattern matched but file not found, fall through + + return original_get_class_in_module(class_name, module_path, **kwargs) + + import transformers.dynamic_module_utils + + transformers.dynamic_module_utils.get_class_in_module = patched_get_class_in_module + patched_get_class_in_module._axolotl_patched = True + + +def patch_kimi(): + """ + Apply all Kimi patches. + Must be called BEFORE loading config/tokenizer/model. + """ + _patch_get_class_in_module() + LOG.info("Kimi patches applied successfully!") + + +# Keep these for backward compatibility if needed +patch_kimi_config = patch_kimi +patch_kimi_tokenizer = patch_kimi +patch_kimi_model = patch_kimi diff --git a/src/axolotl/monkeypatch/models/kimi_linear/tokenization_kimi.py b/src/axolotl/monkeypatch/models/kimi_linear/tokenization_kimi.py new file mode 100644 index 0000000000..83f7ab4ae2 --- /dev/null +++ b/src/axolotl/monkeypatch/models/kimi_linear/tokenization_kimi.py @@ -0,0 +1,357 @@ +""" +Adapted Kimi-Linear tokenizer to use proper template defaults and misc fixes. + +Source: https://huggingface.co/moonshotai/Kimi-Linear-48B-A3B-Instruct/blob/main/tokenization_kimi.py +Revision: 919416f +""" + +import os +from logging import getLogger +from pathlib import Path +from shutil import copyfile +from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + Tuple, + Union, + cast, +) + +import tiktoken +from tiktoken.load import load_tiktoken_bpe +from tokenizers import AddedToken +from transformers.models.gpt2.tokenization_gpt2 import bytes_to_unicode +from transformers.tokenization_utils import PreTrainedTokenizer + +logger = getLogger(__name__) +VOCAB_FILES_NAMES = {"vocab_file": "tiktoken.model"} + + +class TikTokenTokenizer(PreTrainedTokenizer): + """ + Tokenizing and encoding/decoding text using the Tiktoken tokenizer. See megatron/tokenizer/tiktoken_tokenizer.py. + + This tokenizer inherits from [`PreTrainedTokenizer`] which contains most of the main methods. Users should refer to + this superclass for more information regarding those methods. + + Args: + vocab_file (`str`): + The path to the Tiktoken model file. + bos_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|begin_of_text|>",`): + The beginning of sequence token that was used during pretraining. Can be used a sequence classifier token. + eos_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|end_of_text|>"`): + The end of sequence token. + unk_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|reserved_special_token_249|>"`): + The unknown token. A token that is not in the vocabulary cannot be converted to an ID and is set to be this + token instead. The second to last item in special_tokens. + pad_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|reserved_special_token_250|>"`): + The token used for padding, for example when batching sequences of different lengths. + additional_special_tokens (list of `str`, *optional*): + A tuple or a list of additional tokens, which will be marked as `special`, meaning that they will be + skipped when decoding if `skip_special_tokens` is set to `True`. + """ + + vocab_files_names = VOCAB_FILES_NAMES + + model_input_names = ["input_ids", "attention_mask"] + + special_tokens: Dict[str, int] + + num_reserved_special_tokens = 256 + + pat_str = "|".join( + [ + r"""[\p{Han}]+""", + r"""[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?""", + r"""[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?""", + r"""\p{N}{1,3}""", + r""" ?[^\s\p{L}\p{N}]+[\r\n]*""", + r"""\s*[\r\n]+""", + r"""\s+(?!\S)""", + r"""\s+""", + ] + ) + + def __init__( + self, + vocab_file, + bos_token: Union[str, AddedToken] = "[BOS]", # nosec: B107 + eos_token: Union[str, AddedToken] = "[EOS]", # nosec: B107 + unk_token: Union[str, AddedToken, None] = None, + pad_token: Union[str, AddedToken, None] = None, + additional_special_tokens: List[str] = None, + added_tokens_decoder: Optional[dict] = None, + **kwargs, + ): + assert os.path.isfile(vocab_file), vocab_file + + if additional_special_tokens is None: + additional_special_tokens = [ + "<|im_end|>", + "<|im_user|>", + "<|im_assistant|>", + "<|start_header_id|>", + "<|end_header_id|>", + "[EOT]", + "<|im_system|>", + "<|im_middle|>", + ] + + special_tokens_mapping = { + i: added_tokens_decoder[i].content for i in added_tokens_decoder + } + + self.vocab_file = vocab_file + mergeable_ranks = load_tiktoken_bpe(vocab_file) + num_base_tokens = len(mergeable_ranks) + self.special_tokens = { + special_tokens_mapping.get(i, f"<|reserved_token_{i}|>"): i + for i in range( + num_base_tokens, num_base_tokens + self.num_reserved_special_tokens + 2 + ) + } + + self.model = tiktoken.Encoding( + name=Path(vocab_file).name, + pat_str=self.pat_str, + mergeable_ranks=mergeable_ranks, + special_tokens=self.special_tokens, + ) + logger.info(f"Reloaded tiktoken model from {vocab_file}") + + self.n_words: int = self.model.n_vocab + # BOS / EOS token IDs + self.bos_id: int = self.special_tokens[str(bos_token)] + self.eos_id: int = self.special_tokens[str(eos_token)] + logger.info( + f"#words: {self.n_words} - BOS ID: {self.bos_id} - EOS ID: {self.eos_id}" + ) + + self.pad_id: int = self.special_tokens[str(pad_token)] + self.unk_id: int = self.special_tokens[str(unk_token)] + + self.byte_encoder = bytes_to_unicode() + self.byte_decoder = {v: k for k, v in self.byte_encoder.items()} + + self.decoder = {} + for i in range(self.n_words): + # Taken from https://gist.github.com/xenova/a452a6474428de0182b17605a98631ee + decoding = "".join( + [ + self.byte_encoder[ord(char)] + for char in self.model.decode_single_token_bytes(i).decode( + "latin-1" + ) + ] + ) + self.decoder[i] = decoding + + self.encoder = {} + for i in range(self.n_words): + if i in self.decoder: + self.encoder[self.decoder[i]] = i + + super().__init__( + bos_token=bos_token, + eos_token=eos_token, + unk_token=unk_token, + pad_token=pad_token, + additional_special_tokens=additional_special_tokens, + **kwargs, + ) + self.all_special_ids_set = set(self.all_special_ids) + + def encode( + self, text: str, allow_special_tokens: bool = True, **kwargs + ) -> List[int]: + """ + Encodes a string into a list of token IDs. + + Args: + text (str): The input string to be encoded. + + Returns: + list[int]: A list of token IDs. + """ + # If there are other args, we should call super().encode because there are a lot of code + # to handle those args. supper().encode finally will call _tokenize and _convert_token_to_id. + # NOTE: our encode method is not compatible with the super().encode method, + # e.g. split_special_tokens' default is True in our encode method. + if len(kwargs) > 0: + # logger.warning(f"Calling super().encode with {kwargs}") + return super().encode(text, **kwargs) + + assert type(text) is str + + # The tiktoken tokenizer can handle <=400k chars without + # pyo3_runtime.PanicException. + TIKTOKEN_MAX_ENCODE_CHARS = 400_000 + + # https://github.com/openai/tiktoken/issues/195 + # Here we iterate over subsequences and split if we exceed the limit + # of max consecutive non-whitespace or whitespace characters. + MAX_NO_WHITESPACES_CHARS = 25_000 + + texts = self.pre_tokenizer_process(text) + + all_substrs = [] + for text in texts: + substrs = ( + substr + for i in range(0, len(text), TIKTOKEN_MAX_ENCODE_CHARS) + for substr in self._split_whitespaces_or_nonwhitespaces( + text[i : i + TIKTOKEN_MAX_ENCODE_CHARS], MAX_NO_WHITESPACES_CHARS + ) + ) + all_substrs.extend(substrs) + + t: List[int] = [] + for substr in all_substrs: + if allow_special_tokens: + t.extend( + # we should consider special token as a common token + self.model.encode( + substr, + allowed_special="all", + ) + ) + else: + t.extend( + # we should consider special token as a common token + self.model.encode( + substr, + disallowed_special=(), + ) + ) + + return t + + def decode(self, token_ids: Union[int, List[int]], **kwargs) -> str: + """ + Decodes a list of token IDs into a string. + + Args: + token_ids (List[int]): The list of token IDs to be decoded. + + Returns: + str: The decoded string. + """ + # If there are other args, we should call super().decode because there are a lot of code + # to handle those args. supper().encode finally will call convert_tokens_to_string and _convert_id_to_token. + if len(kwargs) > 0: + return super().decode(token_ids, **kwargs) + + if type(token_ids) is int: + token_ids = [token_ids] + + return self.model.decode(cast(List[int], token_ids)) + + @staticmethod + def _split_whitespaces_or_nonwhitespaces( + s: str, max_consecutive_slice_len: int + ) -> Iterator[str]: + """ + Splits the string `s` so that each substring contains no more than `max_consecutive_slice_len` + consecutive whitespaces or consecutive non-whitespaces. + """ + current_slice_len = 0 + current_slice_is_space = s[0].isspace() if len(s) > 0 else False + slice_start = 0 + + for i in range(len(s)): + is_now_space = s[i].isspace() + + if current_slice_is_space ^ is_now_space: + current_slice_len = 1 + current_slice_is_space = is_now_space + else: + current_slice_len += 1 + if current_slice_len > max_consecutive_slice_len: + yield s[slice_start:i] + slice_start = i + current_slice_len = 1 + yield s[slice_start:] + + def pre_tokenizer_process(self, text: str) -> List[str]: + """ + pre-tokenizes the input text into a list of tokens. + This method is used to split the input text into smaller chunks for internal processing. + """ + return [text] + + """ ----- Below are the abstract methods required by PreTrainedTokenizer ----- """ + + @property + def vocab_size(self) -> int: + return self.n_words + + def get_vocab(self) -> Dict[str, int]: + return self.encoder + + def _tokenize(self, text: str, **kwargs) -> List[str]: + return [self.decoder[t] for t in self.encode(text)] + + def _convert_token_to_id(self, token: str) -> int: + return self.encoder.get(token, self.unk_id) + + def _convert_id_to_token(self, index: int) -> str: + return self.decoder.get(index) + + @staticmethod + def clean_up_tokenization(out_string: str) -> str: + return out_string + + def convert_tokens_to_string(self, tokens: List[str]) -> str: + text = "".join(tokens) + text = bytearray([self.byte_decoder[c] for c in text]).decode( + "utf-8", "replace" + ) + return text + + def save_vocabulary( + self, save_directory: str, filename_prefix: Optional[str] = None + ) -> Tuple[str]: + if not os.path.isdir(save_directory): + raise ValueError( + f"vocabulary path ({save_directory}) should be a directory" + ) + out_vocab_file = os.path.join( + save_directory, + (filename_prefix + "-" if filename_prefix else "") + + VOCAB_FILES_NAMES["vocab_file"], + ) + + if os.path.abspath(self.vocab_file) != os.path.abspath( + out_vocab_file + ) and os.path.isfile(self.vocab_file): + copyfile(self.vocab_file, out_vocab_file) + + return (out_vocab_file,) + + def apply_chat_template( + self, + conversation, + tools: Optional[list[dict]] = None, + tokenize: bool = True, + add_generation_prompt: bool = False, + **kwargs, + ): + tools = deep_sort_dict(tools) + return super().apply_chat_template( + conversation, + tools=tools, + tokenize=tokenize, + add_generation_prompt=add_generation_prompt, + **kwargs, + ) + + +def deep_sort_dict(obj: Any) -> Any: + if isinstance(obj, dict): + return {k: deep_sort_dict(v) for k, v in sorted(obj.items())} + if isinstance(obj, list): + return [deep_sort_dict(item) for item in obj] + return obj diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 7a2bbd6f9a..8b35ed4062 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -151,6 +151,11 @@ def normalize_config(cfg): if not cfg.base_model_config: cfg.base_model_config = cfg.base_model + # Apply pre-config load patches (e.g., for Kimi Linear remote code patching) + from axolotl.loaders.patch_manager import PatchManager + + PatchManager.apply_pre_config_load_patches(cfg) + model_config = load_model_config(cfg) cfg.tokenizer_config = ( From 372f664c635e9d794b4af9734ddd74dcb8eca45e Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 25 Dec 2025 17:56:20 +0700 Subject: [PATCH 1071/1405] feat: cleanup old flex mask patch, suppress Matmul bnb warn, and misc (#3330) [skip-ci] * feat: add pos id to flex attention for packing part 1 * feat: update to include sliding window mask patch * fix: suppress MatMul8bitLt: inputs will be cast from warnings * fix: remove redundant flex attention patch * chore: update olmo docs * feat: add validator patch for cross entropy --- examples/olmo3/README.md | 2 +- examples/olmo3/olmo3-7b-qlora.yaml | 4 +- src/axolotl/core/attention/flex_block_mask.py | 158 ------------------ src/axolotl/loaders/patch_manager.py | 6 - src/axolotl/utils/logging.py | 8 + src/axolotl/utils/schemas/validation.py | 30 ++++ 6 files changed, 41 insertions(+), 167 deletions(-) delete mode 100644 src/axolotl/core/attention/flex_block_mask.py diff --git a/examples/olmo3/README.md b/examples/olmo3/README.md index 2f98eb73e2..1602236282 100644 --- a/examples/olmo3/README.md +++ b/examples/olmo3/README.md @@ -16,7 +16,7 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations axolotl train examples/olmo3/olmo3-7b-qlora.yaml ``` -Let us know how it goes. Happy finetuning! 🚀 +This uses about 11.3 GiB VRAM. Let us know how it goes. Happy finetuning! 🚀 ### TIPS diff --git a/examples/olmo3/olmo3-7b-qlora.yaml b/examples/olmo3/olmo3-7b-qlora.yaml index c8878d79f6..de2bf1d3d0 100644 --- a/examples/olmo3/olmo3-7b-qlora.yaml +++ b/examples/olmo3/olmo3-7b-qlora.yaml @@ -42,10 +42,10 @@ wandb_watch: wandb_name: wandb_log_model: -gradient_accumulation_steps: 4 +gradient_accumulation_steps: 2 micro_batch_size: 2 num_epochs: 1 -optimizer: adamw_bnb_8bit +optimizer: adamw_8bit lr_scheduler: cosine learning_rate: 0.0002 diff --git a/src/axolotl/core/attention/flex_block_mask.py b/src/axolotl/core/attention/flex_block_mask.py deleted file mode 100644 index 37149983c8..0000000000 --- a/src/axolotl/core/attention/flex_block_mask.py +++ /dev/null @@ -1,158 +0,0 @@ -""" -monkeypatch for flex + packing -""" - -import sys -from typing import Callable, Optional, Union - -import torch -from torch.nn.attention.flex_attention import BlockMask -from transformers import Cache, PretrainedConfig -from transformers.masking_utils import ( - ALL_MASK_ATTENTION_FUNCTIONS, - _preprocess_mask_arguments, - and_masks, - causal_mask_function, - or_masks, -) -from transformers.utils import is_torch_greater_or_equal - -_is_torch_greater_or_equal_than_2_6 = is_torch_greater_or_equal("2.6", accept_dev=True) - - -def create_causal_mask( - config: PretrainedConfig, - input_embeds: torch.Tensor, - attention_mask: torch.Tensor, - cache_position: torch.Tensor, - past_key_values: Optional[Cache], - or_mask_function: Optional[Callable] = None, - and_mask_function: Optional[Callable] = None, -) -> Optional[Union[torch.Tensor, BlockMask]]: - """ - Create a standard causal mask based on the attention implementation used (stored in the config). If `past_key_values` - has an HybridCache structure, this function will return the mask corresponding to one of the "full_attention" layers (to align - to what is needed in the `modeling_xxx.py` files). - - Args: - config (`PretrainedConfig`): - The model config. - input_embeds (`torch.Tensor`): - The input embeddings of shape (batch_size, query_length, hidden_dim). This is used only to infer the - batch size, query length and dtype. - attention_mask (`torch.Tensor`, optional): - The 2D attention mask corresponding to padded tokens of shape (batch_size, number_of_seen_tokens+q_length). - It can also be an already prepared 4D mask, in which case it is returned as-is. - cache_position (`torch.Tensor`): - A tensor of shape (query_length,) indicating the current indices of the input sequence elements. - past_key_values (`Cache`, optional): - The past key values, if we use a cache. - or_mask_function (`Callable`, optional): - An optional mask function to combine with the causal mask function (by doing the union of both). This is - useful to easily overlay another mask on top of the causal one, for example for image tokens handling. - and_mask_function (`Callable`, optional): - An optional mask function to combine with the causal mask function (by doing the intersection of both). This is - useful to easily overlay another mask on top of the causal one, for example for image tokens handling. - """ - # If we have an HybridCache structure, here we want to create the mask for the full layers - if ( - past_key_values - and hasattr(past_key_values, "is_sliding") - and False in past_key_values.is_sliding - ): - layer_idx = past_key_values.is_sliding.index(False) - else: - layer_idx = 0 - - original_attention_mask = ( - None - if attention_mask is None - else attention_mask.clone().to(cache_position.device) - ) - early_exit, attention_mask, kv_length, kv_offset = _preprocess_mask_arguments( - config, input_embeds, attention_mask, cache_position, past_key_values, layer_idx - ) - if early_exit: - return attention_mask - - batch_size, total_seq_len = cache_position.shape - key_length = total_seq_len - document_ids = torch.nn.functional.pad( - original_attention_mask, value=0, pad=(0, key_length) - ) - - batch_size, dtype = input_embeds.shape[0], input_embeds.dtype - if attention_mask is not None: - - def causal_doc_mask_mod(batch_idx, head_idx, q_idx, kv_idx): - """ - Defines the logic of a block causal mask by combining both a standard causal mask - and a block diagonal document mask. - See :func:`~torchtune.modules.attention_utils.create_block_causal_mask` - for an illustration. - """ - causal_mask_ = q_idx >= kv_idx # not valid when decoding - document_mask = ( - document_ids[batch_idx, q_idx] == document_ids[batch_idx, kv_idx] - ) - final_mask = causal_mask_ & document_mask - return final_mask - - mask_factory_function = causal_doc_mask_mod - else: - mask_factory_function = causal_mask_function - mask_interface = ALL_MASK_ATTENTION_FUNCTIONS[config._attn_implementation] - - # Do not allow skip if we are compiling (this is to match BC) - allow_is_causal_skip = ( - not past_key_values.is_compileable if past_key_values is not None else True - ) - - # Allow slight deviations from causal mask - if or_mask_function is not None: - if not _is_torch_greater_or_equal_than_2_6: - raise ValueError( - "Using `or_mask_function` or `and_mask_function` arguments require torch>=2.6" - ) - mask_factory_function = or_masks(mask_factory_function, or_mask_function) - allow_is_causal_skip = False - if and_mask_function is not None: - if not _is_torch_greater_or_equal_than_2_6: - raise ValueError( - "Using `or_mask_function` or `and_mask_function` arguments require torch>=2.6" - ) - mask_factory_function = and_masks(mask_factory_function, and_mask_function) - allow_is_causal_skip = False - - # We now create the mask - causal_mask = mask_interface( - batch_size=batch_size, - cache_position=cache_position, - kv_length=kv_length, - kv_offset=kv_offset, - mask_function=mask_factory_function, - attention_mask=attention_mask, - allow_is_causal_skip=allow_is_causal_skip, # additional kwarg for sdpa - dtype=dtype, # Additional kwarg for eager - config=config, # Pass the config as well, in case someone wants to easily have their own mask_interface - ) - return causal_mask - - -def patch_create_causal_mask(model_type): - import transformers.masking_utils - - transformers.masking_utils.create_causal_mask = create_causal_mask - - if model_type: - try: - # Dynamically import the module and attention class - module_path = f"transformers.models.{model_type}.modeling_{model_type}" - module = __import__(module_path) - module.create_causal_mask = create_causal_mask - del sys.modules[module_path] - except (ImportError, AttributeError) as e: - raise ValueError( - f"Could not import attention class for model_type: {model_type}. " - f"Error: {str(e)}" - ) from e diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index c319822621..64f363bb12 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -199,12 +199,6 @@ def _apply_flex_attention_patches(self): flex_attn_compile_kwargs = self.cfg.flex_attn_compile_kwargs or {} patch_flex_wrapper(**flex_attn_compile_kwargs) - if self.cfg.sample_packing: - from axolotl.core.attention.flex_block_mask import ( - patch_create_causal_mask, - ) - - patch_create_causal_mask(self.cfg.model_config_type) def _apply_model_specific_patches(self): """Apply patches specific to model architectures.""" diff --git a/src/axolotl/utils/logging.py b/src/axolotl/utils/logging.py index 35810897a3..7aaedef286 100644 --- a/src/axolotl/utils/logging.py +++ b/src/axolotl/utils/logging.py @@ -2,9 +2,17 @@ import functools import logging +import warnings from axolotl.utils.distributed import is_main_process +# Suppress noisy bitsandbytes warnings about dtype casting during quantization +warnings.filterwarnings( + "ignore", + message=".*MatMul8bitLt: inputs will be cast from.*", + category=UserWarning, +) + # Adapted from Accelerate # https://github.com/huggingface/accelerate/blob/main/src/accelerate/logging.py diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 36565fb03d..cb834a3bf8 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -801,6 +801,36 @@ def check_xentropy_patch_conflicts(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_cross_entropy_conflicts(cls, data): + """Check for mutual exclusivity between cross entropy patch options. + + Only one of the following can be enabled at a time: + - cut_cross_entropy (CutCrossEntropyPlugin) + - chunked_cross_entropy + - liger_cross_entropy (LigerPlugin) + - liger_fused_linear_cross_entropy (LigerPlugin) + """ + ce_options = { + "cut_cross_entropy": data.get("cut_cross_entropy"), + "chunked_cross_entropy": data.get("chunked_cross_entropy"), + "liger_cross_entropy": data.get("liger_cross_entropy"), + "liger_fused_linear_cross_entropy": data.get( + "liger_fused_linear_cross_entropy" + ), + } + + enabled_options = [k for k, v in ce_options.items() if v] + + if len(enabled_options) > 1: + raise ValueError( + f"Only one cross entropy optimization can be enabled at a time. " + f"Found {len(enabled_options)} enabled: {', '.join(enabled_options)}. " + "Please disable all but one." + ) + return data + @model_validator(mode="before") @classmethod def check_fsdp_version(cls, data): From 418933f0d1dcf5de314411bb5ebc709ad12655a9 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 25 Dec 2025 18:07:59 +0700 Subject: [PATCH 1072/1405] feat: add internvl3_5 (#3141) [skip-ci] * feat: add internvl3_5 * fix: add timm instructions * chore: add kimi-linear to cce doc * feat: update internvl example * chore: pin revision * chore: remove from multipack * fix: add to multimodal array * fix: internvl use hf version * feat: update cce * chore: lint * fix: list for image_size * chore: add docs vram usage * feat: enable cce * fix: no need trust remote code * fix: inconsistent timm version --- README.md | 2 +- docs/multimodal.qmd | 11 ++++ .../colab-axolotl-example.ipynb | 2 +- examples/internvl3_5/README.md | 43 +++++++++++++ examples/internvl3_5/internvl3_5-8b-qlora.yml | 61 +++++++++++++++++++ scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 3 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/loaders/utils.py | 6 +- src/axolotl/processing_strategies.py | 37 +++++++++++ 10 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 examples/internvl3_5/README.md create mode 100644 examples/internvl3_5/internvl3_5-8b-qlora.yml diff --git a/README.md b/README.md index 13a5a9243d..65cd5eaa64 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ## 🎉 Latest Updates -- 2025/12: Axolotl now includes support for [Kimi-Linear](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/kimi-linear), [Olmo3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/olmo3), [Trinity](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/trinity), and [Ministral3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/ministral3). +- 2025/12: Axolotl now includes support for [Kimi-Linear](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/kimi-linear), [InternVL 3.5](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/internvl3_5), [Olmo3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/olmo3), [Trinity](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/trinity), and [Ministral3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/ministral3). - 2025/10: New model support has been added in Axolotl for: [Qwen3 Next](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/qwen3-next), [Qwen2.5-vl, Qwen3-vl](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen2_5-vl), [Qwen3, Qwen3MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3), [Granite 4](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/granite4), [HunYuan](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/hunyuan), [Magistral 2509](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral#vision), [Apertus](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/apertus), and [Seed-OSS](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/seed-oss). - 2025/09: Axolotl now has text diffusion training. Read more [here](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations/diffusion). - 2025/08: QAT has been updated to include NVFP4 support. See [PR](https://github.com/axolotl-ai-cloud/axolotl/pull/3107). diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index e63a553b26..71d5cc7667 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -21,6 +21,7 @@ format: - [Qwen2.5-VL](#sec-qwen25-vl) - [SmolVLM2](#sec-smolvlm2) - [LFM2-VL](#sec-lfm2-vl) +- [Intern-VL](#sec-intern-vl) ## Usage @@ -202,6 +203,16 @@ Please uninstall `causal-conv1d` via `pip3 uninstall -y causal-conv1d` base_model: LiquidAI/LFM2-VL-450M ``` +### Intern-VL {#sec-intern-vl} + +::: {.callout-tip} +Please make sure to install `timm` via `pip3 install timm==1.0.19` +::: + +```yaml +base_model: OpenGVLab/InternVL3_5-8B +``` + ## Dataset Format For multi-modal datasets, we adopt an extended `chat_template` format similar to OpenAI's Message format. diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 133c3db79b..be4f3f4a91 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@242b245\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@318b7e2\"" ] }, { diff --git a/examples/internvl3_5/README.md b/examples/internvl3_5/README.md new file mode 100644 index 0000000000..d2584bb80a --- /dev/null +++ b/examples/internvl3_5/README.md @@ -0,0 +1,43 @@ +# Finetune OpenGV's InternVL with Axolotl + +[InternVL 3.5](https://huggingface.co/OpenGVLab/InternVL3_5-8B-HF) is a family of powerful vision-language models supporting dynamic resolution and multi-image understanding by OpenGV. It features a ViT-style vision encoder and strong language model backbone for tasks like visual question answering, OCR, and scene text understanding. + +This guide shows how to fine-tune it with Axolotl. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install `timm` for vision model support: + + ```bash + pip install timm==1.0.19 + ``` + +3. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +4. Run the finetuning example: + + ```bash + axolotl train examples/internvl3_5/internvl3_5-8b-qlora.yml + ``` + +This config uses about 8.21 GiB VRAM. Let us know how it goes. Happy finetuning! 🚀 + +### Tips + +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the multi-modal format as seen [here](https://docs.axolotl.ai/docs/multimodal.html#dataset-format). + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Related Resources + +- [InternVL Paper](https://huggingface.co/papers/2508.18265) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/internvl3_5/internvl3_5-8b-qlora.yml b/examples/internvl3_5/internvl3_5-8b-qlora.yml new file mode 100644 index 0000000000..9a72d078a7 --- /dev/null +++ b/examples/internvl3_5/internvl3_5-8b-qlora.yml @@ -0,0 +1,61 @@ +base_model: OpenGVLab/InternVL3_5-8B-HF +processor_type: AutoProcessor + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_4bit: true + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + field_messages: messages + +dataset_prepared_path: last_run_prepared +val_set_size: 0.01 +output_dir: ./outputs/out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: true +fp16: +tf32: true + +gradient_checkpointing: true +logging_steps: 1 +flash_attention: true +eager_attention: + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index e902bb0ace..59e55c074f 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@242b245"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@318b7e2"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index b28382542a..771f446b08 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@242b245" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@318b7e2" ``` ## Usage @@ -54,6 +54,7 @@ plugins: - granitemoehybrid - hunyuan_v1_dense - hunyuan_v1_moe +- internvl - kimi_linear - lfm2 - lfm2_moe diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 0d1588f992..3c059da4ce 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@242b245"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@318b7e2"`' ) diff --git a/src/axolotl/loaders/utils.py b/src/axolotl/loaders/utils.py index 240e00da72..b1902c9b54 100644 --- a/src/axolotl/loaders/utils.py +++ b/src/axolotl/loaders/utils.py @@ -79,7 +79,11 @@ def check_model_config(cfg: DictDefault, model_config: PretrainedConfig): and hasattr(model_config, "vision_config") and hasattr(model_config.vision_config, "image_size") ): - cfg.image_size = model_config.vision_config.image_size + image_size = model_config.vision_config.image_size + if isinstance(image_size, list): + cfg.image_size = tuple(image_size) + else: + cfg.image_size = image_size LOG.debug(f"Loaded image size: {cfg.image_size} from model config") quant_config_exists = ( diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 07b114163b..c209c892a8 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -8,6 +8,7 @@ from torch import Tensor, zeros_like from transformers import ProcessorMixin from transformers.image_utils import load_image +from transformers.models.internvl import InternVLProcessor from transformers.models.smolvlm import SmolVLMProcessor from transformers.models.voxtral import VoxtralProcessor @@ -454,6 +455,37 @@ def process_labels(self, input_ids): return labels +class InternVLProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for InternVL""" + + def __init__( + self, + processor: ProcessorMixin, + chat_template: Optional[str] = None, + image_size: int | tuple[int, int] | None = None, + image_resize_algorithm: Resampling | None = None, + ): + super().__init__(processor, chat_template, image_size, image_resize_algorithm) + + if not hasattr(processor, "image_ids"): + raise ValueError("'image_ids' missing from InternVL Processor.") + + self.image_token_ids = processor.image_ids + + def process_labels(self, input_ids): + labels = input_ids.clone() + + labels[labels == self.processor.tokenizer.pad_token_id] = -100 + + for ids in self.image_token_ids: + labels[labels == ids] = -100 + + # Note: Check if need to mask 'video_token' as it gets converted to + # image patches during media processing + + return labels + + def get_processing_strategy( processor: ProcessorMixin, chat_template, @@ -501,6 +533,11 @@ def get_processing_strategy( **processing_kwargs, ) + if isinstance(processor, InternVLProcessor): + return InternVLProcessingStrategy( + **processing_kwargs, + ) + # llama3_2_vision, llama4, llava # mistral_v7_tekken, pixtral, lfm2vl return ProcessingStrategy( From 4f5e8a328a18522d2e46a556e9afa6e5431086cb Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 25 Dec 2025 18:09:03 +0700 Subject: [PATCH 1073/1405] Feat: add MiMo and Plano (#3332) [skip-ci] * feat: add xiaomi's mimo 7b * fix: pin revision * fix: update trinity docs and pin revision * fix: wrong config name * feat: add vram usage * feat: add plano * feat: update plano vram usage * chore: comments --- README.md | 2 +- examples/mimo/README.md | 39 +++++++++++ examples/mimo/mimo-7b-qlora.yaml | 67 +++++++++++++++++++ examples/plano/README.md | 42 ++++++++++++ examples/plano/plano-4b-qlora.yaml | 65 ++++++++++++++++++ examples/trinity/README.md | 4 ++ .../trinity/trinity-nano-preview-qlora.yaml | 1 + 7 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 examples/mimo/README.md create mode 100644 examples/mimo/mimo-7b-qlora.yaml create mode 100644 examples/plano/README.md create mode 100644 examples/plano/plano-4b-qlora.yaml diff --git a/README.md b/README.md index 65cd5eaa64..6b9a6f84b7 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ## 🎉 Latest Updates -- 2025/12: Axolotl now includes support for [Kimi-Linear](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/kimi-linear), [InternVL 3.5](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/internvl3_5), [Olmo3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/olmo3), [Trinity](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/trinity), and [Ministral3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/ministral3). +- 2025/12: Axolotl now includes support for [Kimi-Linear](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/kimi-linear), [Plano-Orchestrator](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/plano), [MiMo](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/mimo), [InternVL 3.5](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/internvl3_5), [Olmo3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/olmo3), [Trinity](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/trinity), and [Ministral3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/ministral3). - 2025/10: New model support has been added in Axolotl for: [Qwen3 Next](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/qwen3-next), [Qwen2.5-vl, Qwen3-vl](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen2_5-vl), [Qwen3, Qwen3MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3), [Granite 4](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/granite4), [HunYuan](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/hunyuan), [Magistral 2509](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral#vision), [Apertus](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/apertus), and [Seed-OSS](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/seed-oss). - 2025/09: Axolotl now has text diffusion training. Read more [here](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations/diffusion). - 2025/08: QAT has been updated to include NVFP4 support. See [PR](https://github.com/axolotl-ai-cloud/axolotl/pull/3107). diff --git a/examples/mimo/README.md b/examples/mimo/README.md new file mode 100644 index 0000000000..5ae2143438 --- /dev/null +++ b/examples/mimo/README.md @@ -0,0 +1,39 @@ +# Finetune Xiaomi's MiMo with Axolotl + +[MiMo](https://huggingface.co/XiaomiMiMo/MiMo-7B-RL) is a family of models trained from scratch for reasoning tasks, incorporating **Multiple-Token Prediction (MTP)** as an additional training objective for enhanced performance and faster inference. Pre-trained on ~25T tokens with a three-stage data mixture strategy and optimized reasoning pattern density. + +This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Run the finetuning example: + + ```bash + axolotl train examples/mimo/mimo-7b-qlora.yaml + ``` + +This config uses about 17.2 GiB VRAM. Let us know how it goes. Happy finetuning! 🚀 + +### Tips + +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Limitations + +**Cut Cross Entropy (CCE)**: Currently not supported. We plan to include CCE support for MiMo in the near future. + +## Related Resources + +- [MiMo Paper](https://arxiv.org/abs/2505.07608) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/mimo/mimo-7b-qlora.yaml b/examples/mimo/mimo-7b-qlora.yaml new file mode 100644 index 0000000000..689213bcd4 --- /dev/null +++ b/examples/mimo/mimo-7b-qlora.yaml @@ -0,0 +1,67 @@ +base_model: XiaomiMiMo/MiMo-7B-RL +trust_remote_code: true +revision_of_model: 6299b5a + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +# CCE - N/A as of now +# plugins: +# - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/plano/README.md b/examples/plano/README.md new file mode 100644 index 0000000000..e3f4d14dcc --- /dev/null +++ b/examples/plano/README.md @@ -0,0 +1,42 @@ +# Finetune Katanemo's Plano-Orchestrator with Axolotl + +[Plano-Orchestrator](https://huggingface.co/collections/katanemo/plano-orchestrator) is a family of 4B and 30B-A3B routing and orchestration models designed for multi-agent systems. It analyzes user intent and conversation context to make precise routing decisions, excelling at multi-turn context understanding, multi-intent detection, and context-dependent routing. + +This guide shows how to fine-tune it with Axolotl with multi-turn conversations and proper masking. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +3. Run the finetuning example: + + ```bash + axolotl train examples/plano/plano-4b-qlora.yaml + ``` + +This config uses about 5.1 GiB VRAM. Let us know how it goes. Happy finetuning! 🚀 + +### Orchestration Prompt + +Plano-Orchestrator uses a specific orchestration prompt format for routing/agent decisions. Please check the [official model card](https://huggingface.co/katanemo/Plano-Orchestrator-4B) for proper prompt formatting and the `ORCHESTRATION_PROMPT` template. + +### Tips + +- To use the larger [Plano-Orchestrator-30B-A3B](https://huggingface.co/katanemo/Plano-Orchestrator-30B-A3B) MoE model, simply change `base_model: katanemo/Plano-Orchestrator-30B-A3B` in the config and enable multi-GPU training if needed. +- You can run a full finetuning by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Related Resources + +- [Plano GitHub](https://github.com/katanemo/plano) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/plano/plano-4b-qlora.yaml b/examples/plano/plano-4b-qlora.yaml new file mode 100644 index 0000000000..106e442052 --- /dev/null +++ b/examples/plano/plano-4b-qlora.yaml @@ -0,0 +1,65 @@ +base_model: katanemo/Plano-Orchestrator-4B + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +chat_template: qwen3 +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/trinity/README.md b/examples/trinity/README.md index 28b2e2b521..4bbfcf29c1 100644 --- a/examples/trinity/README.md +++ b/examples/trinity/README.md @@ -29,6 +29,10 @@ Let us know how it goes. Happy finetuning! 🚀 Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). +## Limitations + +**Cut Cross Entropy (CCE)**: Currently not supported. We plan to include CCE support for Trinity in the near future. + ## Related Resources - [Trinity Blog](https://www.arcee.ai/blog/the-trinity-manifesto) diff --git a/examples/trinity/trinity-nano-preview-qlora.yaml b/examples/trinity/trinity-nano-preview-qlora.yaml index 43263cabd7..de54fc8ac8 100644 --- a/examples/trinity/trinity-nano-preview-qlora.yaml +++ b/examples/trinity/trinity-nano-preview-qlora.yaml @@ -1,5 +1,6 @@ base_model: arcee-ai/Trinity-Nano-Preview trust_remote_code: true +revision_of_model: 2ee94b0 # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name From a6080df73c8ab991ec9ee2e000a2df1be14bb493 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Thu, 25 Dec 2025 17:08:17 +0530 Subject: [PATCH 1074/1405] compute loss only if training and update token metric naming (#3293) [skip ci] * compute loss only if training * save total_tokens for checkpiont * check if string * refactor total_tokens/ num_tokens * refactor 2 * rplc trainable_step/trian_per_sec_per_gpu * lint + log trainable/tokens * consolidate it in the callback. * test for total_tokes aftr remuse * check if tokenstate exist after ckpt --------- Co-authored-by: Ved --- src/axolotl/core/builders/causal.py | 4 +- src/axolotl/core/trainers/base.py | 58 ++++++++++++++----- .../utils/callbacks/tokens_per_second.py | 53 +++++++++++++++-- tests/e2e/patched/test_resume.py | 33 ++++++++++- 4 files changed, 128 insertions(+), 20 deletions(-) diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 7a06431dc0..cda98087f0 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -72,7 +72,9 @@ def get_callbacks(self): if self.cfg.include_tkps: callbacks.append( TokensPerSecondCallback( - self.cfg.tensor_parallel_size, self.cfg.context_parallel_size + self.cfg.tensor_parallel_size, + self.cfg.context_parallel_size, + resume_from_checkpoint=self.cfg.resume_from_checkpoint, ) ) return callbacks diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index aae3d28fb1..850517ded7 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json import math import os from collections import defaultdict @@ -50,6 +51,8 @@ LOG = get_logger(__name__) +TOKENS_STATE_FILE = "tokens_state." + REDUCTION_FNS = { "mean": torch.mean, "min": torch.min, @@ -349,24 +352,34 @@ def compute_loss( # return (loss, outputs) if return_outputs else loss # track number of tokens for tokens per second calculation - if self.args.include_tkps: + if self.args.include_tkps and model.training: inputs_key = "labels" if "labels" in inputs else "input_ids" - num_tokens = (inputs[inputs_key] != -100).sum() + trainable_tokens = (inputs[inputs_key] != -100).sum() + total_tokens = inputs[inputs_key].numel() + if is_distributed(): torch.distributed.all_reduce( - num_tokens, op=torch.distributed.ReduceOp.SUM + trainable_tokens, op=torch.distributed.ReduceOp.SUM ) - if hasattr(self.state, "num_tokens"): - self.state.num_tokens = ( - self.state.num_tokens + (inputs[inputs_key] != -100).sum().cpu() + torch.distributed.all_reduce( + total_tokens, op=torch.distributed.ReduceOp.SUM ) - else: - self.state.num_tokens = (inputs[inputs_key] != -100).sum().cpu() - if hasattr(self.state, "total_tokens"): - self.state.total_tokens += num_tokens - else: - self.state.total_tokens = num_tokens + if not hasattr(self.state, "tokens"): + self.state.tokens = { + "trainable": torch.zeros(1), + "total": torch.zeros(1), + } + + # trainable tokens for throughput and total token slots for summaries + self.state.tokens["trainable"] = ( + self.state.tokens["trainable"] + trainable_tokens.detach().cpu() + ) + self.state.tokens["total"] = ( + self.state.tokens["total"] + torch.as_tensor(total_tokens).cpu() + ) + # Store per-step trainable tokens for throughput calculation + self.state.tokens["trainable_tokens"] = trainable_tokens.detach().cpu() if self.args.orpo_alpha: return self.orpo_compute_loss( @@ -638,10 +651,14 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: except (ValueError, TypeError, FileNotFoundError): pass - if self.args.include_tkps and train_eval == "train": + if ( + self.args.include_tkps + and train_eval == "train" + and hasattr(self.state, "tokens") + ): # each rank will log its own tokens per second # for logging_steps > 1 we obtain a moving average of this metric - logs["tokens_per_second_per_gpu"] = round( + logs["tokens/train_per_sec_per_gpu"] = round( self.state.last_tokens_per_second.item() / self.args.logging_steps, 2 ) if ( @@ -683,6 +700,19 @@ def _save_checkpoint(self, model, trial, **kwargs): run_dir = self._get_output_dir(trial=trial) output_dir = os.path.join(run_dir, checkpoint_folder) os.makedirs(output_dir, exist_ok=True) + + # Save total_tokens state if tracking is enabled + if self.args.include_tkps and hasattr(self.state, "tokens"): + tokens_state = { + "total": int(torch.as_tensor(self.state.tokens.get("total", 0)).item()), + "trainable": int( + torch.as_tensor(self.state.tokens.get("trainable", 0)).item() + ), + } + tokens_state_path = os.path.join(output_dir, TOKENS_STATE_FILE) + with open(tokens_state_path, "w", encoding="utf-8") as f: + json.dump(tokens_state, f) + return super()._save_checkpoint(model, trial, **kwargs) # TODO(wing): remove once https://github.com/huggingface/transformers/pull/39866/files is merged diff --git a/src/axolotl/utils/callbacks/tokens_per_second.py b/src/axolotl/utils/callbacks/tokens_per_second.py index ead1292400..a1b955a747 100644 --- a/src/axolotl/utils/callbacks/tokens_per_second.py +++ b/src/axolotl/utils/callbacks/tokens_per_second.py @@ -1,5 +1,7 @@ """A callback for calculating tokens per second during training.""" +import json +import os import time import torch @@ -10,22 +12,52 @@ TrainingArguments, ) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +TOKENS_STATE_FILE = "tokens_state.json" + class TokensPerSecondCallback(TrainerCallback): """ A callback to measure and log tokens per second during training. + Also handles saving/restoring total_tokens state across checkpoint resumes. """ - def __init__(self, tensor_parallel_size, context_parallel_size): + def __init__( + self, tensor_parallel_size, context_parallel_size, resume_from_checkpoint=None + ): super().__init__() self.step_time = 0.0 self.start_time = 0.0 self.non_data_parallel_size = 1 + self.resume_from_checkpoint = resume_from_checkpoint if tensor_parallel_size is not None: self.non_data_parallel_size *= tensor_parallel_size if context_parallel_size is not None: self.non_data_parallel_size *= context_parallel_size + def on_train_begin( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): # pylint: disable=unused-argument + """Restore total_tokens state when resuming from checkpoint.""" + if not isinstance(self.resume_from_checkpoint, str): + return + tokens_state_path = os.path.join(self.resume_from_checkpoint, TOKENS_STATE_FILE) + if os.path.isfile(tokens_state_path): + with open(tokens_state_path, "r", encoding="utf-8") as f: + tokens_state = json.load(f) + state.tokens = { + "total": torch.tensor(tokens_state.get("total", 0)), + "trainable": torch.tensor(tokens_state.get("trainable", 0)), + } + LOG.info(f"Restored total_tokens: {state.tokens['total']}") + def on_step_begin( self, args: TrainingArguments, @@ -33,6 +65,8 @@ def on_step_begin( control: TrainerControl, **kwargs, ): # pylint: disable=unused-argument + if not hasattr(state, "tokens"): + state.tokens = {"trainable": torch.zeros(1), "total": torch.zeros(1)} self.start_time = time.perf_counter() state.last_tokens_per_second = torch.zeros(1) @@ -43,9 +77,10 @@ def on_step_end( control: TrainerControl, **kwargs, ): # pylint: disable=unused-argument - if hasattr(state, "num_tokens"): + tokens = getattr(state, "tokens", None) + if tokens and "trainable_tokens" in tokens: step_time = time.perf_counter() - self.start_time - num_tokens_per_device = state.num_tokens.clone() + num_tokens_per_device = tokens["trainable_tokens"].clone() # non data parallel groups have duplicated tokens, so we avoid double-counting num_tokens_per_device = num_tokens_per_device / self.non_data_parallel_size state.last_tokens_per_second = num_tokens_per_device / step_time @@ -60,5 +95,15 @@ def on_log( ): # pylint: disable=unused-argument # after logging, clear the running metrics if hasattr(state, "last_tokens_per_second"): + logs["tokens/train_per_sec_per_gpu"] = state.last_tokens_per_second.item() state.last_tokens_per_second.zero_() - state.num_tokens = torch.zeros(1) + tokens = getattr(state, "tokens", None) + # Clear per-step tokens after logging + if tokens and "trainable_tokens" in tokens: + tokens["trainable_tokens"] = torch.zeros_like(tokens["trainable_tokens"]) + + if tokens and "total" in tokens: + logs["tokens/total"] = tokens["total"].item() + + if tokens and "trainable" in tokens: + logs["tokens/trainable"] = tokens["trainable"].item() diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index 747b79dc7c..e6240f2088 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -2,6 +2,7 @@ E2E tests for resuming training """ +import os import re import subprocess @@ -9,6 +10,7 @@ from axolotl.common.datasets import load_datasets from axolotl.train import train +from axolotl.utils.callbacks.tokens_per_second import TOKENS_STATE_FILE from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault @@ -58,6 +60,7 @@ def test_resume_lora_packed(self, temp_dir): "use_tensorboard": True, "save_safetensors": True, "save_first_step": False, + "include_tkps": True, } ) if is_torch_bf16_gpu_available(): @@ -68,8 +71,19 @@ def test_resume_lora_packed(self, temp_dir): normalize_config(cfg) dataset_meta = load_datasets(cfg=cfg) + initial_total_num_tokens = cfg.total_num_tokens + assert initial_total_num_tokens is not None, ( + "total_num_tokens should be calculated during load_datasets" + ) + train(cfg=cfg, dataset_meta=dataset_meta) + checkpoint_path = f"{temp_dir}/checkpoint-9" + tokens_state_path = os.path.join(checkpoint_path, TOKENS_STATE_FILE) + assert os.path.isfile(tokens_state_path), ( + f"{TOKENS_STATE_FILE} should exist in checkpoint at {tokens_state_path}" + ) + resume_cfg = cfg | DictDefault( { "resume_from_checkpoint": f"{temp_dir}/checkpoint-9/", @@ -77,7 +91,24 @@ def test_resume_lora_packed(self, temp_dir): ) normalize_config(resume_cfg) - train(cfg=resume_cfg, dataset_meta=dataset_meta) + assert resume_cfg.total_num_tokens == initial_total_num_tokens, ( + f"total_num_tokens should be preserved on resume. " + f"Expected {initial_total_num_tokens}, got {resume_cfg.total_num_tokens}" + ) + + resume_dataset_meta = load_datasets(cfg=resume_cfg) + + assert resume_cfg.total_num_tokens == initial_total_num_tokens, ( + f"total_num_tokens should not be recalculated when resuming. " + f"Expected {initial_total_num_tokens}, got {resume_cfg.total_num_tokens}" + ) + + train(cfg=resume_cfg, dataset_meta=resume_dataset_meta) + + assert resume_cfg.total_num_tokens == initial_total_num_tokens, ( + f"total_num_tokens should remain unchanged after resume training. " + f"Expected {initial_total_num_tokens}, got {resume_cfg.total_num_tokens}" + ) check_model_output_exists(temp_dir, cfg) tb_log_path_1 = most_recent_subdir(temp_dir + "/runs") From 66a3de36292e6a6afa5e0916c8392e7db41ba822 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 25 Dec 2025 07:17:25 -0500 Subject: [PATCH 1075/1405] build examples readmes with quarto (#3046) * build examples readmes with quarto * chore: formatting * feat: dynamic build docs * feat: add more model guides * chore: format * fix: collapse sidebar completely to have space for model guides * fix: security protection for generated qmd * fix: adjust collapse level, add new models, update links --------- Co-authored-by: NanoCode012 --- README.md | 12 +- _quarto.yml | 44 +- docs/.gitignore | 2 + docs/scripts/examples-allowlist.yml | 90 ++++ docs/scripts/generate_examples_docs.py | 424 ++++++++++++++++++ examples/magistral/think/README.md | 1 + examples/magistral/vision/README.md | 3 +- examples/ministral3/think/README.md | 1 + examples/ministral3/vision/README.md | 3 +- .../{mistral => }/mistral-small/README.md | 1 + .../mistral-small-3.1-24B-lora.yml | 0 11 files changed, 572 insertions(+), 9 deletions(-) create mode 100644 docs/scripts/examples-allowlist.yml create mode 100755 docs/scripts/generate_examples_docs.py rename examples/{mistral => }/mistral-small/README.md (99%) rename examples/{mistral => }/mistral-small/mistral-small-3.1-24B-lora.yml (100%) diff --git a/README.md b/README.md index 6b9a6f84b7..01e0c44d9b 100644 --- a/README.md +++ b/README.md @@ -29,15 +29,15 @@ ## 🎉 Latest Updates -- 2025/12: Axolotl now includes support for [Kimi-Linear](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/kimi-linear), [Plano-Orchestrator](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/plano), [MiMo](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/mimo), [InternVL 3.5](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/internvl3_5), [Olmo3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/olmo3), [Trinity](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/trinity), and [Ministral3](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/ministral3). -- 2025/10: New model support has been added in Axolotl for: [Qwen3 Next](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/qwen3-next), [Qwen2.5-vl, Qwen3-vl](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen2_5-vl), [Qwen3, Qwen3MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3), [Granite 4](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/granite4), [HunYuan](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/hunyuan), [Magistral 2509](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral#vision), [Apertus](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/apertus), and [Seed-OSS](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/seed-oss). +- 2025/12: Axolotl now includes support for [Kimi-Linear](https://docs.axolotl.ai/docs/models/kimi-linear.html), [Plano-Orchestrator](https://docs.axolotl.ai/docs/models/plano.html), [MiMo](https://docs.axolotl.ai/docs/models/mimo.html), [InternVL 3.5](https://docs.axolotl.ai/docs/models/internvl3_5.html), [Olmo3](https://docs.axolotl.ai/docs/models/olmo3.html), [Trinity](https://docs.axolotl.ai/docs/models/trinity.html), and [Ministral3](https://docs.axolotl.ai/docs/models/ministral3.html). +- 2025/10: New model support has been added in Axolotl for: [Qwen3 Next](https://docs.axolotl.ai/docs/models/qwen3-next.html), [Qwen2.5-vl, Qwen3-vl](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen2_5-vl), [Qwen3, Qwen3MoE](https://docs.axolotl.ai/docs/models/qwen3.html), [Granite 4](https://docs.axolotl.ai/docs/models/granite4.html), [HunYuan](https://docs.axolotl.ai/docs/models/hunyuan.html), [Magistral 2509](https://docs.axolotl.ai/docs/models/magistral/vision.html), [Apertus](https://docs.axolotl.ai/docs/models/apertus.html), and [Seed-OSS](https://docs.axolotl.ai/docs/models/seed-oss.html). - 2025/09: Axolotl now has text diffusion training. Read more [here](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations/diffusion). - 2025/08: QAT has been updated to include NVFP4 support. See [PR](https://github.com/axolotl-ai-cloud/axolotl/pull/3107). - 2025/07: - ND Parallelism support has been added into Axolotl. Compose Context Parallelism (CP), Tensor Parallelism (TP), and Fully Sharded Data Parallelism (FSDP) within a single node and across multiple nodes. Check out the [blog post](https://huggingface.co/blog/accelerate-nd-parallel) for more info. - - Axolotl adds more models: [GPT-OSS](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/gpt-oss), [Gemma 3n](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/gemma3n), [Liquid Foundation Model 2 (LFM2)](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/lfm2), and [Arcee Foundation Models (AFM)](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/afm). + - Axolotl adds more models: [GPT-OSS](https://docs.axolotl.ai/docs/models/gpt-oss.html), [Gemma 3n](https://docs.axolotl.ai/docs/models/gemma3n.html), [Liquid Foundation Model 2 (LFM2)](https://docs.axolotl.ai/docs/models/LiquidAI.html), and [Arcee Foundation Models (AFM)](https://docs.axolotl.ai/docs/models/arcee.html). - FP8 finetuning with fp8 gather op is now possible in Axolotl via `torchao`. Get started [here](https://docs.axolotl.ai/docs/mixed_precision.html#sec-fp8)! - - [Voxtral](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/voxtral), [Magistral 1.1](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral), and [Devstral](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/devstral) with mistral-common tokenizer support has been integrated in Axolotl! + - [Voxtral](https://docs.axolotl.ai/docs/models/voxtral.html), [Magistral 1.1](https://docs.axolotl.ai/docs/models/magistral.html), and [Devstral](https://docs.axolotl.ai/docs/models/devstral.html) with mistral-common tokenizer support has been integrated in Axolotl! - TiledMLP support for single-GPU to multi-GPU training with DDP, DeepSpeed and FSDP support has been added to support Arctic Long Sequence Training. (ALST). See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/alst) for using ALST with Axolotl! - 2025/05: Quantization Aware Training (QAT) support has been added to Axolotl. Explore the [docs](https://docs.axolotl.ai/docs/qat.html) to learn more! @@ -46,8 +46,8 @@ Expand older updates - 2025/03: Axolotl has implemented Sequence Parallelism (SP) support. Read the [blog](https://huggingface.co/blog/axolotl-ai-co/long-context-with-sequence-parallelism-in-axolotl) and [docs](https://docs.axolotl.ai/docs/sequence_parallelism.html) to learn how to scale your context length when fine-tuning. -- 2025/06: Magistral with mistral-common tokenizer support has been added to Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/magistral) to start training your own Magistral models with Axolotl! -- 2025/04: Llama 4 support has been added in Axolotl. See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/llama-4) to start training your own Llama 4 models with Axolotl's linearized version! +- 2025/06: Magistral with mistral-common tokenizer support has been added to Axolotl. See [docs](https://docs.axolotl.ai/docs/models/magistral.html) to start training your own Magistral models with Axolotl! +- 2025/04: Llama 4 support has been added in Axolotl. See [docs](https://docs.axolotl.ai/docs/models/llama-4.html) to start training your own Llama 4 models with Axolotl's linearized version! - 2025/03: (Beta) Fine-tuning Multimodal models is now supported in Axolotl. Check out the [docs](https://docs.axolotl.ai/docs/multimodal.html) to fine-tune your own! - 2025/02: Axolotl has added LoRA optimizations to reduce memory usage and improve training speed for LoRA and QLoRA in single GPU and multi-GPU training (DDP and DeepSpeed). Jump into the [docs](https://docs.axolotl.ai/docs/lora_optims.html) to give it a try. - 2025/02: Axolotl has added GRPO support. Dive into our [blog](https://huggingface.co/blog/axolotl-ai-co/training-llms-w-interpreter-feedback-wasm) and [GRPO example](https://github.com/axolotl-ai-cloud/grpo_code) and have some fun! diff --git a/_quarto.yml b/_quarto.yml index c97b9838e0..fe3a76e534 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -1,6 +1,8 @@ project: type: website - pre-render: docs/scripts/generate_config_docs.py + pre-render: + - docs/scripts/generate_config_docs.py + - docs/scripts/generate_examples_docs.py quartodoc: dir: docs/api @@ -240,6 +242,46 @@ website: - docs/getting-started.qmd - docs/installation.qmd - docs/inference.qmd + - section: "Model Guides" + contents: + - docs/models/kimi-linear.qmd + - docs/models/plano.qmd + - docs/models/mimo.qmd + - docs/models/internvl3_5.qmd + - docs/models/olmo3.qmd + - docs/models/trinity.qmd + - docs/models/arcee.qmd + - docs/models/mistral.qmd + - section: "Ministral3" + contents: + - docs/models/ministral3.qmd + - docs/models/ministral3/think.qmd + - docs/models/ministral3/vision.qmd + - section: "Magistral" + contents: + - docs/models/magistral.qmd + - docs/models/magistral/think.qmd + - docs/models/magistral/vision.qmd + - docs/models/ministral.qmd + - docs/models/mistral-small.qmd + - docs/models/voxtral.qmd + - docs/models/devstral.qmd + - docs/models/llama-4.qmd + - docs/models/llama-2.qmd + - docs/models/qwen3-next.qmd + - docs/models/qwen3.qmd + - docs/models/gemma3n.qmd + - docs/models/apertus.qmd + - docs/models/gpt-oss.qmd + - docs/models/seed-oss.qmd + - docs/models/phi.qmd + - docs/models/smolvlm2.qmd + - docs/models/granite4.qmd + - docs/models/LiquidAI.qmd + - docs/models/hunyuan.qmd + - docs/models/jamba.qmd + - docs/models/orpheus.qmd + - docs/cli.qmd - docs/telemetry.qmd - docs/config-reference.qmd diff --git a/docs/.gitignore b/docs/.gitignore index 89407326f9..94e5b88fb6 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -3,3 +3,5 @@ _site/ /api/*.qmd /api/*.html config-reference.qmd +models/**/*.qmd +models/**/*.html diff --git a/docs/scripts/examples-allowlist.yml b/docs/scripts/examples-allowlist.yml new file mode 100644 index 0000000000..50acaea8e3 --- /dev/null +++ b/docs/scripts/examples-allowlist.yml @@ -0,0 +1,90 @@ +examples: + # December 2025 + - name: kimi-linear + title: Kimi Linear + - name: plano + title: Plano Orchestrator + - name: mimo + title: MiMo + - name: internvl3_5 + title: InternVL 3.5 + + # AllenAI + - name: olmo3 + title: OLMo 3 + + # ArceeAI + - name: trinity + title: Trinity + - name: arcee + title: Arcee AFM + + # MistralAI + - name: ministral3/think + title: Ministral 3 Thinking + - name: ministral3/vision + title: Ministral 3 Vision + - name: magistral/think + title: Magistral Thinking + - name: magistral/vision + title: Magistral Vision + - name: ministral + title: Ministral + - name: mistral-small + title: Mistral Small 3.1/3.2 + - name: voxtral + title: Voxtral + - name: devstral + title: Devstral + - name: mistral + title: Mistral 7B + + # Meta + - name: llama-4 + title: Llama 4 + - name: llama-2 + title: Llama 2 + + # Alibaba + - name: qwen3-next + title: Qwen 3 Next + - name: qwen3 + title: Qwen 3 + + # Google + - name: gemma3n + title: Gemma 3n + + # Swiss AI + - name: apertus + title: Apertus + + # GPT-OSS + - name: gpt-oss + title: GPT-OSS + - name: seed-oss + title: Seed-OSS + + # Microsoft + - name: phi + title: Phi + + # SmolVLM + - name: smolvlm2 + title: SmolVLM 2 + + # IBM + - name: granite4 + title: Granite 4 + + # LiquidAI + - name: LiquidAI + title: Liquid Foundation Models 2 + + # Other + - name: hunyuan + title: Hunyuan + - name: jamba + title: Jamba + - name: orpheus + title: Orpheus diff --git a/docs/scripts/generate_examples_docs.py b/docs/scripts/generate_examples_docs.py new file mode 100755 index 0000000000..ba01088f17 --- /dev/null +++ b/docs/scripts/generate_examples_docs.py @@ -0,0 +1,424 @@ +""" +auto generate example docs from allowlist +""" + +import re +import shutil +import sys +from pathlib import Path + +import yaml + +# Paths +THIS = Path(__file__).resolve() +ROOT = THIS.parents[2] # repo root (docs/scripts -> docs -> ROOT) +EXAMPLES_DIR = ROOT / "examples" +OUTPUT_DIR = ROOT / "docs" / "models" +ALLOWLIST_YML = THIS.parent / "examples-allowlist.yml" + + +def slugify(name: str) -> str: + """Convert a name to a slug (lowercase, hyphens for spaces).""" + s = re.sub(r"[^a-zA-Z0-9\s\-]+", "", name.strip()) + s = re.sub(r"\s+", "-", s).strip("-").lower() + return s or "example" + + +def read_allowlist(): + with open(ALLOWLIST_YML, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) or {} + items = data.get("examples", []) + if not isinstance(items, list): + raise ValueError("`examples` must be a list in examples-allowlist.yml") + return items + + +def find_readme(folder: Path) -> Path | None: + for name in ("README.md", "Readme.md", "readme.md"): + p = folder / name + if p.exists(): + return p + return None + + +def remove_first_h1(md: str) -> tuple[str, str | None]: + """ + Remove the first H1 from markdown and return (modified_md, h1_title). + The H1 is removed since we use the frontmatter title instead. + """ + lines = md.splitlines() + result = [] + h1_title = None + skipped_first = False + + for line in lines: + if not skipped_first and line.startswith("# "): + h1_title = line[2:].strip() + skipped_first = True + continue + result.append(line) + + return "\n".join(result), h1_title + + +IMG_RE = re.compile(r"!\[[^\]]*\]\(([^)]+)\)") +LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)") + + +def rewrite_and_copy_assets(md: str, src_dir: Path, dest_assets_root: Path) -> str: + """ + Copy local image assets referenced in markdown to + docs/examples/assets/... and rewrite the links. + """ + dest_assets = dest_assets_root / "assets" + + def repl(m): + url = m.group(1).strip() + if re.match(r"^(https?:)?//", url): + return m.group(0) # leave remote URLs + src_path = (src_dir / url).resolve() + if not src_path.exists(): + return m.group(0) # leave as-is if not found + rel = src_path.relative_to(src_dir) + # Create a unique asset path based on source directory name + asset_name = src_dir.name.replace("/", "-") + dest_path = dest_assets / asset_name / rel + dest_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src_path, dest_path) + new_rel = f"assets/{asset_name}/{rel.as_posix()}" + return m.group(0).replace(url, new_rel) + + return IMG_RE.sub(repl, md) + + +def rewrite_readme_links( + md: str, + src_dir: Path, + examples_dir: Path, + parent_index_only: set, + current_src_path: str, + allowlist_entries: set, + current_output_path: str, +) -> str: + """ + Rewrite links between README.md files to point to the correct .qmd files. + """ + + def repl(m): + text = m.group(1) + url = m.group(2).strip() + + # Skip remote URLs and anchor links + if re.match(r"^(https?:)?//", url) or url.startswith("#"): + return m.group(0) + + # Skip non-markdown files + if not url.lower().endswith(".md"): + return m.group(0) + + # Resolve the target path + try: + target_path = (src_dir / url).resolve() + + # Check if target is outside examples_dir + try: + rel_path = target_path.relative_to(examples_dir) + except ValueError: + # Target is outside examples_dir, leave as-is + return m.group(0) + + parts = list(rel_path.parts) + + # Determine the output path for the target + if len(parts) > 0 and parts[-1].lower() in ("readme.md", "readme"): + # This is a README link + if len(parts) == 1: + # Link to root README -> index.qmd + target_output = "index.qmd" + elif len(parts) == 2: + if parts[0] == ".": + # Current directory README + target_output = "index.qmd" + else: + # subdir/README.md + parent_dir = parts[0] + if parent_dir in parent_index_only: + target_output = f"{parent_dir}/index.qmd" + else: + target_output = f"{parent_dir}.qmd" + else: + # Deeper nesting: parent/subdir/README.md + # Build the full path like "parent/subdir" + full_path = "/".join(parts[:-1]) # Remove README.md + # Check if this exact path is in allowlist + if full_path in allowlist_entries: + # This is a sub-entry with its own entry -> use .qmd + target_output = f"{full_path}.qmd" + elif parts[0] == ".": + # ./subdir/README.md -> check if subdir has own entry + subdir = parts[1] + if subdir in parent_index_only: + target_output = f"{subdir}/index.qmd" + else: + target_output = f"{subdir}.qmd" + else: + # parent/subdir where parent doesn't have own entry + target_output = f"{full_path}/index.qmd" + else: + # Regular .md file -> convert to .qmd, keep path structure + target_output = "/".join(parts)[:-2] + "qmd" + + # Compute relative path from current output file to target + current_parts = current_output_path.split("/") + target_parts = target_output.split("/") + + # Special case: if current is a subdir file and target is a single-component file at root + # Example: current="magistral/vision", target="magistral.qmd" + if len(current_parts) > 1 and len(target_parts) == 1: + # Current is in subdir, target is at root level + # Go up to root: ../ for each level + up_count = len(current_parts) - 1 + rel_parts = [".."] * up_count + [target_parts[0]] + new_url = "/".join(rel_parts) + else: + # Find common prefix + i = 0 + while ( + i < min(len(current_parts) - 1, len(target_parts)) + and current_parts[i] == target_parts[i] + ): + i += 1 + + # Build relative path: go up (../) then down to target + up_count = len(current_parts) - 1 - i + rel_parts = [".."] * up_count + target_parts[i:] + + if not rel_parts or rel_parts == [".."]: + # Points to same directory or parent + new_url = "/".join(rel_parts) if rel_parts else "." + else: + new_url = "/".join(rel_parts) + + return f"[{text}]({new_url})" + except (ValueError, IndexError): + return m.group(0) + + return LINK_RE.sub(repl, md) + + +def write_qmd(out_path: Path, title: str, body_md: str): + out_path.parent.mkdir(parents=True, exist_ok=True) + fm = f"---\ntitle: {title!r}\nexecute:\n eval: false\nformat:\n html:\n toc: true\n---\n\n" + out_path.write_text(fm + body_md, encoding="utf-8") + + +def update_quarto_yml(generated: list[tuple[str, str, str]]): + """ + Update _quarto.yml with the generated example files in the correct order. + This keeps the sidebar in sync with the allowlist. + + Model Guides is now nested under "Getting Started" section. + Creates nested sections for models with sub-entries (e.g., magistral, ministral3). + Parent pages are now flat files (e.g., ministral3.qmd) with sub-pages in subdirs. + """ + quarto_yml = ROOT / "_quarto.yml" + if not quarto_yml.exists(): + print(f"[WARN] {quarto_yml} not found, skipping update", file=sys.stderr) + return + + content = quarto_yml.read_text(encoding="utf-8") + + # First pass: find all parents that have sub-entries + parents_with_subs = set() + for path, _name, _title in generated: + if "/" in path: + parent = path.split("/")[0] + parents_with_subs.add(parent) + + # Build the YAML contents while preserving allowlist order + lines = [] + processed_sections = set() + + for path, _name, title in generated: + # Check if this is a parent page that has sub-pages + if path in parents_with_subs: + # This is a parent page with sub-pages - create a nested section + if path not in processed_sections: + processed_sections.add(path) + section_title = ( + title or path.replace("-", " ").replace("_", " ").title() + ) + lines.append(f' - section: "{section_title}"') + lines.append(" contents:") + # Add the parent page first + lines.append(f" - docs/models/{path}.qmd") + # Then add all sub-pages + for sub_path, _sub_name, _sub_title in generated: + if "/" in sub_path and sub_path.split("/")[0] == path: + lines.append( + f" - docs/models/{sub_path}.qmd" + ) + elif "/" not in path: + # This is a flat item with no sub-pages + # Skip if it was already included as part of a parent section + if path not in processed_sections: + lines.append(f" - docs/models/{path}.qmd") + + yaml_content = "\n".join(lines) + "\n" + + # Pattern to match only the Model Guides contents, stopping at the next item + # in Getting Started (lines starting with 12 spaces: same level as the section) + pattern = r'( - section: "Model Guides"\n contents:)([^\n]*|.*?)(?=\n - |\n - section:|\n\nformat:)' + + def replacement(match): + prefix = match.group(1) + return prefix + "\n" + yaml_content + + new_content = re.sub(pattern, replacement, content, flags=re.DOTALL) + + if new_content != content: + quarto_yml.write_text(new_content, encoding="utf-8") + print(f"Updated {quarto_yml}") + else: + print(f"No changes needed for {quarto_yml}") + + +def main(): + allow = read_allowlist() + if not EXAMPLES_DIR.exists(): + print(f"[WARN] {EXAMPLES_DIR} not found", file=sys.stderr) + return + + (OUTPUT_DIR / "assets").mkdir(parents=True, exist_ok=True) + + # First pass: identify which parents have their own entry vs only sub-entries + parent_entries = set() # Parents that have their own entry + parent_with_subs = set() # Parents that have sub-entries + allowlist_entries = set() # All entries in allowlist + + for item in allow: + if isinstance(item, str): + name = item + else: + name = item.get("name") + + allowlist_entries.add(name) + + if "/" in name: + parent = name.split("/")[0] + parent_with_subs.add(parent) + else: + parent_entries.add(name) + + # Parents with subs that DON'T have their own entry -> use index.qmd + parent_index_only = parent_with_subs - parent_entries + + generated = [] + seen_dirs = set() # Track which parent directories we've created index for + + for item in allow: + if isinstance(item, str): + name = item + title = None + else: + name = item.get("name") + title = item.get("title") + + if not name: + print(f"[WARN] Skipping item without name: {item}", file=sys.stderr) + continue + + src_dir = EXAMPLES_DIR / name + if not src_dir.exists() or not src_dir.is_dir(): + print(f"[WARN] Skipping {name} (not a directory)", file=sys.stderr) + continue + + readme = find_readme(src_dir) + if not readme: + print(f"[WARN] Skipping {name} (no README.md)", file=sys.stderr) + continue + + md = readme.read_text(encoding="utf-8") + + # Determine output path first (needed for link rewriting) + parts = name.split("/") + if len(parts) == 1: + # Simple case: no subdirectory + out_path = OUTPUT_DIR / f"{parts[0]}.qmd" + sidebar_path = parts[0] + else: + # Has subdirectory: e.g., magistral/think + parent = parts[0] + child = "-".join(parts[1:]) # handle nested subdirs + out_path = OUTPUT_DIR / parent / f"{child}.qmd" + sidebar_path = f"{parent}/{child}" + + # Remove the first H1 (we use frontmatter title instead) + md, _ = remove_first_h1(md) + # Rewrite links between README files + md = rewrite_readme_links( + md, + src_dir, + EXAMPLES_DIR, + parent_index_only, + name, + allowlist_entries, + sidebar_path, + ) + md = rewrite_and_copy_assets(md, src_dir, OUTPUT_DIR) + + # Handle parent page generation for sub-entries + if len(parts) > 1: + # Has subdirectory: e.g., magistral/think + parent = parts[0] + + # Create parent.qmd if not already done and parent doesn't have own entry + if parent not in seen_dirs and parent in parent_index_only: + parent_readme = find_readme(EXAMPLES_DIR / parent) + if parent_readme: + parent_md = parent_readme.read_text(encoding="utf-8") + parent_md, _ = remove_first_h1(parent_md) + parent_md = rewrite_readme_links( + parent_md, + EXAMPLES_DIR / parent, + EXAMPLES_DIR, + parent_index_only, + parent, + allowlist_entries, + parent, + ) + parent_md = rewrite_and_copy_assets( + parent_md, EXAMPLES_DIR / parent, OUTPUT_DIR + ) + parent_title = parent.replace("-", " ").replace("_", " ").title() + write_qmd(OUTPUT_DIR / f"{parent}.qmd", parent_title, parent_md) + generated.append((parent, parent, parent_title)) + seen_dirs.add(parent) + + if not title: + title = name.replace("/", " ").replace("-", " ").title() + + write_qmd(out_path, title, md) + generated.append((sidebar_path, name, title)) + + # Index page - preserve allowlist order + if generated: + listing = "\n".join( + [f"- [{title}]({path}.qmd)" for path, name, title in generated] + ) + index_md = ( + "# Model Guides\n\nBelow are the curated examples for training various model architectures:\n\n" + + listing + + "\n" + ) + index_fm = ( + "---\nexecute:\n eval: false\nformat:\n html:\n toc: true\n---\n\n" + ) + (OUTPUT_DIR / "index.qmd").write_text(index_fm + index_md, encoding="utf-8") + + # Auto-update _quarto.yml to keep sidebar in sync + update_quarto_yml(generated) + + +if __name__ == "__main__": + main() diff --git a/examples/magistral/think/README.md b/examples/magistral/think/README.md index a875797757..adb22bbba2 100644 --- a/examples/magistral/think/README.md +++ b/examples/magistral/think/README.md @@ -5,6 +5,7 @@ This guide covers fine-tuning [Magistral Small 2507](https://huggingface.co/mist ## Prerequisites Before starting, ensure you have: + - Installed Axolotl (see [main README](../README.md)) ## Getting Started diff --git a/examples/magistral/vision/README.md b/examples/magistral/vision/README.md index fc614c8505..72a8a22153 100644 --- a/examples/magistral/vision/README.md +++ b/examples/magistral/vision/README.md @@ -5,7 +5,8 @@ This guide covers fine-tuning [Magistral Small 2509](https://huggingface.co/mist ## Prerequisites Before starting, ensure you have: -- Installed Axolotl from source (see [main README](../README.md#getting-started)) + +- Installed Axolotl from source (see [main README](../README.md)) ## Getting started diff --git a/examples/ministral3/think/README.md b/examples/ministral3/think/README.md index 8c40adbb9a..a116dd5ddf 100644 --- a/examples/ministral3/think/README.md +++ b/examples/ministral3/think/README.md @@ -5,6 +5,7 @@ This guide covers fine-tuning [Ministral3 2512](https://huggingface.co/collectio ## Prerequisites Before starting, ensure you have: + - Installed Axolotl (see [main README](../README.md)) ## Getting Started diff --git a/examples/ministral3/vision/README.md b/examples/ministral3/vision/README.md index 369b0116a5..8193573eb4 100644 --- a/examples/ministral3/vision/README.md +++ b/examples/ministral3/vision/README.md @@ -5,7 +5,8 @@ This guide covers fine-tuning [Ministral3 2512](https://huggingface.co/collectio ## Prerequisites Before starting, ensure you have: -- Installed Axolotl from source (see [main README](../README.md#getting-started)) + +- Installed Axolotl from source (see [main README](../README.md)) ## Getting started diff --git a/examples/mistral/mistral-small/README.md b/examples/mistral-small/README.md similarity index 99% rename from examples/mistral/mistral-small/README.md rename to examples/mistral-small/README.md index 3c606a897f..7f7ec91e61 100644 --- a/examples/mistral/mistral-small/README.md +++ b/examples/mistral-small/README.md @@ -5,6 +5,7 @@ This guide covers fine-tuning [Mistral Small 3.1](mistralai/Mistral-Small-3.1-24 ## Prerequisites Before starting, ensure you have: + - Installed Axolotl (see [Installation docs](https://docs.axolotl.ai/docs/installation.html)) ## Getting Started diff --git a/examples/mistral/mistral-small/mistral-small-3.1-24B-lora.yml b/examples/mistral-small/mistral-small-3.1-24B-lora.yml similarity index 100% rename from examples/mistral/mistral-small/mistral-small-3.1-24B-lora.yml rename to examples/mistral-small/mistral-small-3.1-24B-lora.yml From 11c0b5b256a2e4d5c3d6396c28470506414688fa Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 30 Dec 2025 09:02:49 -0500 Subject: [PATCH 1076/1405] bartch upgrade dependencies (#3299) * upgrade dependencies * don't use reset sessions * downgrade transformers, upgrade other deps * upgrade bnb to 0.49.0 * restore s3 cache * explicit use local files w hub * decompress and strip top level dir * use 2 levels for strip components * try to preserve permissions for symlinks * use updated tar * fix #3293 for distributed * downgrade bnb * fast fail after 4 * fix total tokens device * patch accelerate CP/SP (#3309) --------- Co-authored-by: salman --- .github/workflows/tests.yml | 35 ++++++++++++------- cicd/multigpu.sh | 2 +- requirements.txt | 10 +++--- setup.py | 2 +- src/axolotl/core/trainers/base.py | 5 ++- .../accelerate/parallelism_config.py | 30 ++++++++++++++++ src/axolotl/utils/trainer.py | 3 ++ tests/conftest.py | 2 +- tests/hf_offline_utils.py | 3 -- 9 files changed, 66 insertions(+), 26 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0dc61b7ffe..24e434a4c8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -66,12 +66,13 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 -# - name: Restore Cache from S3 -# id: hf-cache-restore-s3 -# run: | -# mkdir -p ~/.cache/huggingface/hub -# curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xf - -C ~/.cache/huggingface/hub/ --use-compress-program unzstd -# + - name: Restore Cache from S3 + id: hf-cache-restore-s3 + run: | + mkdir -p ~/.cache/huggingface/hub + curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xpf - -C ~/.cache/huggingface/hub/ --use-compress-program unzstd --strip-components=1 + ls -ltr ~/.cache/huggingface/hub/ + - name: Setup Python uses: actions/setup-python@v5 with: @@ -111,6 +112,9 @@ jobs: run: | huggingface-cli download --repo-type=dataset axolotl-ai-internal/axolotl-oss-dataset-fixtures + - name: Show HF cache + run: hf cache scan + - name: Run tests run: | df -h @@ -122,6 +126,9 @@ jobs: df -h pytest -v --durations=10 tests/cli/ --cov=axolotl --cov-append --cov-report=xml + - name: Show HF cache + run: hf cache scan + - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: @@ -149,12 +156,13 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 -# - name: Restore Cache from S3 -# id: hf-cache-restore-s3 -# run: | -# mkdir -p ~/.cache/huggingface/hub -# curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xf - -C ~/.cache/huggingface/hub/ --use-compress-program unzstd -# + - name: Restore Cache from S3 + id: hf-cache-restore-s3 + run: | + mkdir -p ~/.cache/huggingface/hub + curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xpf - -C ~/.cache/huggingface/hub/ --use-compress-program unzstd --strip-components=1 + ls -ltr ~/.cache/huggingface/hub/ + - name: Setup Python uses: actions/setup-python@v5 with: @@ -200,6 +208,9 @@ jobs: pytest -v --durations=10 tests/monkeypatch/ --cov=axolotl --cov-append --cov-report=xml pytest -v --durations=10 tests/cli/ + - name: Show HF cache + run: hf cache scan + gate-skip-e2e: needs: [pre-commit, pytest, pytest-sdist] runs-on: ubuntu-latest diff --git a/cicd/multigpu.sh b/cicd/multigpu.sh index 3ec4456b97..307dd4960e 100755 --- a/cicd/multigpu.sh +++ b/cicd/multigpu.sh @@ -2,7 +2,7 @@ set -e # Only run two tests at a time to avoid OOM on GPU (with coverage collection) -pytest -v --durations=10 -n2 \ +pytest -v --durations=10 -n2 --maxfail=4 \ --ignore=/workspace/axolotl/tests/e2e/multigpu/solo/ \ --ignore=/workspace/axolotl/tests/e2e/multigpu/patched/ \ /workspace/axolotl/tests/e2e/multigpu/ \ diff --git a/requirements.txt b/requirements.txt index 5e1af69408..c87bb21478 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,12 +14,12 @@ huggingface_hub>=0.36.0 peft>=0.18.0 tokenizers>=0.22.1 transformers==4.57.1 -accelerate==1.11.0 -datasets==4.4.1 -deepspeed>=0.17.0 -trl==0.25.0 +accelerate==1.12.0 +datasets==4.4.2 +deepspeed>=0.18.3 +trl==0.25.1 hf_xet==1.2.0 -kernels>=0.9.0 +kernels==0.11.5 trackio>=0.13.0 typing_extensions>=4.14.0 diff --git a/setup.py b/setup.py index e22df40c82..10c9a84539 100644 --- a/setup.py +++ b/setup.py @@ -156,7 +156,7 @@ def get_package_version(): "came_pytorch==0.1.3", ], "ray": [ - "ray[train]", + "ray[train]>=2.52.1", ], "vllm": [ "vllm==0.10.0", diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 850517ded7..3a08d0574b 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -356,6 +356,7 @@ def compute_loss( inputs_key = "labels" if "labels" in inputs else "input_ids" trainable_tokens = (inputs[inputs_key] != -100).sum() total_tokens = inputs[inputs_key].numel() + total_tokens = torch.tensor(total_tokens, device=inputs[inputs_key].device) if is_distributed(): torch.distributed.all_reduce( @@ -375,9 +376,7 @@ def compute_loss( self.state.tokens["trainable"] = ( self.state.tokens["trainable"] + trainable_tokens.detach().cpu() ) - self.state.tokens["total"] = ( - self.state.tokens["total"] + torch.as_tensor(total_tokens).cpu() - ) + self.state.tokens["total"] = self.state.tokens["total"] + total_tokens.cpu() # Store per-step trainable tokens for throughput calculation self.state.tokens["trainable_tokens"] = trainable_tokens.detach().cpu() diff --git a/src/axolotl/monkeypatch/accelerate/parallelism_config.py b/src/axolotl/monkeypatch/accelerate/parallelism_config.py index b2157fb6b6..9b71e914ac 100644 --- a/src/axolotl/monkeypatch/accelerate/parallelism_config.py +++ b/src/axolotl/monkeypatch/accelerate/parallelism_config.py @@ -75,3 +75,33 @@ def patch_parallelism_config(): ParallelismConfig._validate_accelerator = _validate_accelerator AcceleratorState.is_fsdp2 = property(patched_is_fsdp2) + + +def patch_prepare_cp(): + import functools + + import torch + from accelerate import Accelerator + + def patched_prepare_cp(self, *args): + if self.parallelism_config.cp_backend == "deepspeed": + return args + + from accelerate.big_modeling import _attach_context_parallel_hooks + from torch.distributed.tensor.experimental import context_parallel + from torch.distributed.tensor.experimental._attention import set_rotate_method + + cp_comm_strategy = self.parallelism_config.cp_handler.cp_comm_strategy + set_rotate_method(cp_comm_strategy) + + self._cp_context = functools.partial( + context_parallel, mesh=self.torch_device_mesh["cp"] + ) + + for arg in args: + if isinstance(arg, torch.nn.Module): + _attach_context_parallel_hooks(arg) + + return args + + Accelerator._prepare_cp = patched_prepare_cp diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 3628fd85f1..fb381a8a19 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -645,6 +645,9 @@ def setup_parallelism_envs(cfg): set_accelerate_parallelism_config = True os.environ["PARALLELISM_CONFIG_CP_SIZE"] = str(cfg.context_parallel_size) os.environ["ACCELERATE_ALLOW_CP_STANDALONE"] = "true" + from axolotl.monkeypatch.accelerate.parallelism_config import patch_prepare_cp + + patch_prepare_cp() if set_accelerate_parallelism_config: os.environ["ACCELERATE_USE_PARALLELISM_CONFIG"] = "true" diff --git a/tests/conftest.py b/tests/conftest.py index d3b9407ec4..4c8c80cb7f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,7 +62,7 @@ def snapshot_download_w_retry(*args, **kwargs): """ with hf_offline_context(True): try: - return snapshot_download(*args, **kwargs) + return snapshot_download(*args, local_files_only=True, **kwargs) except LocalEntryNotFoundError: pass with hf_offline_context(False): diff --git a/tests/hf_offline_utils.py b/tests/hf_offline_utils.py index 0e4a2f067e..221db1c515 100644 --- a/tests/hf_offline_utils.py +++ b/tests/hf_offline_utils.py @@ -6,8 +6,6 @@ from contextlib import contextmanager from functools import wraps -from huggingface_hub.utils import reset_sessions - def reload_modules(hf_hub_offline): # Force reload of the modules that check this variable @@ -21,7 +19,6 @@ def reload_modules(hf_hub_offline): huggingface_hub.constants.HF_HUB_OFFLINE = hf_hub_offline importlib.reload(datasets.config) datasets.config.HF_HUB_OFFLINE = hf_hub_offline - reset_sessions() def enable_hf_offline(test_func): From f45a97a9ff5764c9674fd3157b40b05b5be73356 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Tue, 30 Dec 2025 23:10:32 +0530 Subject: [PATCH 1077/1405] docs for checkpiont saving (#3335) [skip ci] Co-authored-by: Ved --- docs/checkpoint_saving.qmd | 86 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/checkpoint_saving.qmd diff --git a/docs/checkpoint_saving.qmd b/docs/checkpoint_saving.qmd new file mode 100644 index 0000000000..5f6b155e7b --- /dev/null +++ b/docs/checkpoint_saving.qmd @@ -0,0 +1,86 @@ +--- +title: "Checkpoint Saving" +format: + html: + toc: true + toc-depth: 2 + number-sections: true +execute: + enabled: false +--- + +## Overview + +Axolotl supports on-demand checkpoint saving during training. You can trigger checkpoints via file-based triggers (for programmatic control) or Control+C (for interactive use). + +## File-Based Checkpoint Trigger + +### Configuration + +Enable in your config: + +```yaml +dynamic_checkpoint: + enabled: true + check_interval: 100 # Optional: check every N steps (default: 100) + trigger_file_path: "axolotl_checkpoint.save" # Optional: custom filename +``` + +**Options:** +- `enabled`: `true` to enable (required) +- `check_interval`: Steps between file checks. Default: 100. Lower = faster response, higher I/O overhead. +- `trigger_file_path`: Custom trigger filename. Default: `axolotl_checkpoint.save` + +### How It Works + +1. Rank 0 checks for trigger file every `check_interval` steps in `output_dir` +2. When detected, file is deleted and checkpoint is saved +3. In distributed training, rank 0 broadcasts to synchronize all ranks + +### Usage + +**Command line:** +```bash +touch /path/to/output_dir/axolotl_checkpoint.save +``` + +**Programmatic:** +```python +from pathlib import Path +Path("/path/to/output_dir/axolotl_checkpoint.save").touch() +``` + +Checkpoint saves within the next `check_interval` steps. The trigger file is auto-deleted after detection, so you can create it multiple times. + +**Custom filename:** +```yaml +dynamic_checkpoint: + enabled: true + trigger_file_path: "my_trigger.save" +``` +```bash +touch /path/to/output_dir/my_trigger.save +``` + +## Control+C (SIGINT) Checkpoint + +Pressing `Ctrl+C` during training saves the model state and exits gracefully. **Note:** This saves only the model weights, not optimizer state. For resumable checkpoints, use the file-based trigger. + +## Best Practices + +- **Check interval**: Lower values (10-50) for fast training, default 100 for slower training +- **Distributed training**: Create trigger file once; rank 0 handles synchronization +- **Resume**: Dynamic checkpoints can be resumed like regular checkpoints via `resume_from_checkpoint` + +## Example + +```yaml +output_dir: ./outputs/lora-out +save_steps: 500 # Scheduled checkpoints + +dynamic_checkpoint: + enabled: true + check_interval: 50 +``` + +This enables scheduled checkpoints every 500 steps plus on-demand saves via file trigger (checked every 50 steps). From e73dab6df96778f71c2c733826f829764404683a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 30 Dec 2025 12:41:07 -0500 Subject: [PATCH 1078/1405] support pydantic 2.12 (#3328) * upgrade pydantic to 2.12 * use latest modal version * upgrade modal * update modal in requirements and loosen pydantic * upgrade modal too --- .github/workflows/multi-gpu-e2e.yml | 7 +++++-- .github/workflows/tests-nightly.yml | 4 ++-- .github/workflows/tests.yml | 6 +++--- requirements.txt | 6 +++--- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 1682beb31a..13162f8b19 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -19,6 +19,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +env: + MODAL_IMAGE_BUILDER_VERSION: "2025.06" + jobs: test-axolotl-multigpu: if: ${{ ! contains(github.event.commits[0].message, '[skip e2e]') && github.repository_owner == 'axolotl-ai-cloud' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} @@ -59,7 +62,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==1.0.2 jinja2 + pip install modal==1.3.0.post1 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV @@ -72,4 +75,4 @@ jobs: echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | - modal run cicd.multigpu + modal run -m cicd.multigpu diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 35cb707ebb..53139fac1b 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -123,7 +123,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==1.0.2 jinja2 + pip install modal==1.3.0.post1 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV @@ -165,7 +165,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==1.0.2 jinja2 + pip install modal==1.3.0.post1 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 24e434a4c8..9cf2315754 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -271,7 +271,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==1.0.2 jinja2 + pip install modal==1.3.0.post1 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV @@ -338,7 +338,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==1.0.2 jinja2 + pip install modal==1.3.0.post1 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV @@ -381,7 +381,7 @@ jobs: - name: Install Modal run: | python -m pip install --upgrade pip - pip install modal==1.0.2 jinja2 + pip install modal==1.3.0.post1 jinja2 - name: Update env vars run: | echo "BASE_TAG=main-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}" >> $GITHUB_ENV diff --git a/requirements.txt b/requirements.txt index c87bb21478..f69135902c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,15 +21,15 @@ trl==0.25.1 hf_xet==1.2.0 kernels==0.11.5 trackio>=0.13.0 -typing_extensions>=4.14.0 +typing-extensions>=4.15.0 optimum==1.16.2 hf_transfer sentencepiece gradio>=6.2.0,<7.0 -modal==1.0.2 -pydantic>=2.10.6,<2.12 +modal==1.3.0.post1 +pydantic>=2.10.6 addict fire PyYAML>=6.0 From 2b199f9915b8344121f660dbb4c67adace82d082 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 06:52:28 -0500 Subject: [PATCH 1079/1405] chore: update pre-commit hooks (#3340) [skip ci] Co-authored-by: SalmanMohammadi <25081738+SalmanMohammadi@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3500bb0aaf..44d6e00db7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,13 +11,13 @@ repos: - id: no-commit-to-branch args: ['--branch', 'main'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.7 + rev: v0.14.10 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.0 + rev: v1.19.1 hooks: - id: mypy additional_dependencies: From afe18ace3590808766ea5f95790b228dc933c50c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 1 Jan 2026 06:52:45 -0500 Subject: [PATCH 1080/1405] deprecate torch 2.7.1 (#3339) --- .github/workflows/base.yml | 31 ++-------- .github/workflows/main.yml | 60 +++++-------------- .github/workflows/multi-gpu-e2e.yml | 9 +-- .github/workflows/nightlies.yml | 16 ++--- .github/workflows/tests-nightly.yml | 16 ++--- .github/workflows/tests.yml | 24 ++------ README.md | 2 +- docs/docker.qmd | 16 ++--- docs/installation.qmd | 4 +- .../cli/cloud/baseten/template/train_sft.py | 3 +- src/axolotl/cli/cloud/modal_.py | 2 +- 11 files changed, 52 insertions(+), 131 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index eddce14384..ea721bff4d 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -25,32 +25,18 @@ jobs: fail-fast: false matrix: include: - - cuda: "126" - cuda_version: 12.6.3 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.7.0 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-base" - - cuda: "126" - cuda_version: 12.6.3 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.7.1 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-base" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" python_version: "3.11" - pytorch: 2.7.1 + pytorch: 2.8.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" python_version: "3.11" - pytorch: 2.8.0 + pytorch: 2.9.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" - cuda: "128" @@ -121,32 +107,25 @@ jobs: fail-fast: false matrix: include: - - cuda: "126" - cuda_version: 12.6.3 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.7.1 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-uv-base" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" python_version: "3.11" - pytorch: 2.7.1 + pytorch: 2.8.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" python_version: "3.11" - pytorch: 2.8.0 + pytorch: 2.9.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" python_version: "3.11" - pytorch: 2.9.1 + pytorch: 2.9.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" - cuda: "130" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f34a0cf2f1..052f9aa72f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,21 +15,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.7.0 - axolotl_extras: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.7.1 - axolotl_extras: vllm - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.7.1 - axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" @@ -46,6 +31,11 @@ jobs: python_version: "3.11" pytorch: 2.9.1 axolotl_extras: + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.11" + pytorch: 2.9.1 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -92,27 +82,6 @@ jobs: strategy: matrix: include: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.7.0 - axolotl_extras: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.7.1 - axolotl_extras: - is_latest: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.7.1 - axolotl_extras: vllm - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.7.1 - axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" @@ -129,6 +98,11 @@ jobs: python_version: "3.11" pytorch: 2.9.1 axolotl_extras: + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.11" + pytorch: 2.9.1 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -170,22 +144,16 @@ jobs: strategy: matrix: include: - - cuda: 126 - cuda_version: 12.6.3 + - cuda: 128 + cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.1 + pytorch: 2.8.0 axolotl_extras: is_latest: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.7.1 - axolotl_extras: vllm - is_latest: true - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.8.0 + pytorch: 2.9.1 axolotl_extras: is_latest: runs-on: axolotl-gpu-runner diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 13162f8b19..1dd019dc79 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -29,13 +29,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.7.1 - axolotl_extras: vllm - num_gpus: 2 - nightly_build: "true" - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" @@ -46,7 +39,7 @@ jobs: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.9.0 + pytorch: 2.9.1 axolotl_extras: fbgemm-gpu num_gpus: 2 nightly_build: "true" diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index a24946ae99..d2c587cc7e 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -12,15 +12,15 @@ jobs: fail-fast: false matrix: include: - - cuda: 126 - cuda_version: 12.6.3 + - cuda: 128 + cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.1 + pytorch: 2.8.0 axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.8.0 + pytorch: 2.9.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: @@ -64,15 +64,15 @@ jobs: strategy: matrix: include: - - cuda: 126 - cuda_version: 12.6.3 + - cuda: 128 + cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.1 + pytorch: 2.8.0 axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.8.0 + pytorch: 2.9.1 axolotl_extras: runs-on: axolotl-gpu-runner steps: diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 53139fac1b..67b68a7e60 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -26,7 +26,7 @@ jobs: max-parallel: 2 matrix: python_version: ["3.11"] - pytorch_version: ["2.7.1", "2.8.0"] + pytorch_version: ["2.8.0", "2.9.0", "2.9.1"] timeout-minutes: 20 steps: @@ -99,17 +99,17 @@ jobs: fail-fast: false matrix: include: - - cuda: 126 - cuda_version: 12.6.3 + - cuda: 128 + cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.1 + pytorch: 2.8.0 num_gpus: 1 axolotl_extras: nightly_build: "true" - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.8.0 + pytorch: 2.9.1 num_gpus: 1 axolotl_extras: nightly_build: "true" @@ -148,10 +148,10 @@ jobs: fail-fast: false matrix: include: - - cuda: 126 - cuda_version: 12.6.3 + - cuda: 128 + cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.1 + pytorch: 2.9.1 num_gpus: 2 axolotl_extras: nightly_build: "true" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9cf2315754..ae5ba17403 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.7.1", "2.8.0", "2.9.0"] + pytorch_version: ["2.8.0", "2.9.0", "2.9.1"] timeout-minutes: 20 steps: @@ -145,7 +145,7 @@ jobs: fail-fast: false matrix: python_version: ["3.11"] - pytorch_version: ["2.7.1", "2.8.0", "2.9.0"] + pytorch_version: ["2.8.0", "2.9.0", "2.9.1"] timeout-minutes: 20 steps: @@ -303,18 +303,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 126 - cuda_version: 12.6.3 - python_version: "3.11" - pytorch: 2.7.1 - num_gpus: 1 - axolotl_extras: -# - cuda: 128 -# cuda_version: 12.8.1 -# python_version: "3.11" -# pytorch: 2.7.1 -# num_gpus: 1 -# axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" @@ -325,7 +313,7 @@ jobs: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.9.0 + pytorch: 2.9.1 num_gpus: 1 axolotl_extras: steps: @@ -365,10 +353,10 @@ jobs: fail-fast: false matrix: include: - - cuda: 126 - cuda_version: 12.6.3 + - cuda: 128 + cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.7.1 + pytorch: 2.9.1 num_gpus: 1 axolotl_extras: steps: diff --git a/README.md b/README.md index 01e0c44d9b..0521f7bedf 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Features: - NVIDIA GPU (Ampere or newer for `bf16` and Flash Attention) or AMD GPU - Python 3.11 -- PyTorch ≥2.7.1 +- PyTorch ≥2.8.0 ### Google Colab diff --git a/docs/docker.qmd b/docs/docker.qmd index da61843945..5d146eac23 100644 --- a/docs/docker.qmd +++ b/docs/docker.qmd @@ -32,11 +32,8 @@ main-base-py{python_version}-cu{cuda_version}-{pytorch_version} Tags examples: -- `main-base-py3.11-cu128-2.7.1` -- `main-base-py3.11-cu126-2.7.1` -- `main-base-py3.11-cu126-2.7.0` -- `main-base-py3.11-cu126-2.6.0` -- `main-base-py3.11-cu124-2.6.0` +- `main-base-py3.11-cu128-2.8.0` +- `main-base-py3.11-cu128-2.9.1` ## Main @@ -74,15 +71,12 @@ There may be some extra tags appended to the image, like `-vllm` which installs Tags examples: -- `main-py3.11-cu128-2.7.1` -- `main-py3.11-cu126-2.7.1` -- `main-py3.11-cu126-2.7.0` -- `main-py3.11-cu126-2.6.0` -- `main-py3.11-cu124-2.6.0` +- `main-py3.11-cu128-2.8.0` +- `main-py3.11-cu128-2.9.1` - `main-latest` - `main-20250303-py3.11-cu124-2.6.0` - `main-20250303-py3.11-cu126-2.6.0` -- `0.10.1` +- `0.12.0` ## Cloud diff --git a/docs/installation.qmd b/docs/installation.qmd index 265ff238c1..b8d427eb00 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -26,7 +26,7 @@ Follow the instructions at: [https://pytorch.org/get-started/locally/](https://p ::: ::: {.callout-important} -For Blackwell GPUs, please use Pytorch 2.7.0 and CUDA 12.8. +For Blackwell GPUs, please use Pytorch 2.9.1 and CUDA 12.8. ::: ### PyPI Installation (Recommended) {#sec-pypi} @@ -111,7 +111,7 @@ docker run --privileged --gpus '"all"' --shm-size 10g --rm -it \ ::: ::: {.callout-important} -For Blackwell GPUs, please use `axolotlai/axolotl:main-py3.11-cu128-2.7.0` or the cloud variant `axolotlai/axolotl-cloud:main-py3.11-cu128-2.7.0`. +For Blackwell GPUs, please use `axolotlai/axolotl:main-py3.11-cu128-2.9.1` or the cloud variant `axolotlai/axolotl-cloud:main-py3.11-cu128-2.9.1`. ::: Please refer to the [Docker documentation](docker.qmd) for more information on the different Docker images that are available. diff --git a/src/axolotl/cli/cloud/baseten/template/train_sft.py b/src/axolotl/cli/cloud/baseten/template/train_sft.py index 137fb91714..6dcf477c79 100644 --- a/src/axolotl/cli/cloud/baseten/template/train_sft.py +++ b/src/axolotl/cli/cloud/baseten/template/train_sft.py @@ -24,8 +24,7 @@ launcher_args_str = "-- " + " ".join(launcher_args) # 1. Define a base image for your training job -# must use torch 2.7.0 for vllm -BASE_IMAGE = "axolotlai/axolotl:main-py3.11-cu126-2.7.1" +BASE_IMAGE = "axolotlai/axolotl:main-py3.11-cu128-2.9.1" # 2. Define the Runtime Environment for the Training Job # This includes start commands and environment variables.a diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index 7f953372d4..3e703a4946 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -82,7 +82,7 @@ def get_env(self): return res def get_image(self): - docker_tag = "main-py3.11-cu126-2.7.1" + docker_tag = "main-py3.11-cu128-2.9.1" if self.config.docker_tag: docker_tag = self.config.docker_tag docker_image = f"axolotlai/axolotl:{docker_tag}" From b26ba3a5cb9102bc041c6055e7473ca281c7457d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 3 Jan 2026 18:08:28 -0500 Subject: [PATCH 1081/1405] don't build images w cuda 130 since we don't have flash attention wheels (#3341) --- .github/workflows/main.yml | 20 ++++++++++---------- docker/Dockerfile-base | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 052f9aa72f..fc8d854d4d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,11 +31,11 @@ jobs: python_version: "3.11" pytorch: 2.9.1 axolotl_extras: - - cuda: 130 - cuda_version: 13.0.0 - python_version: "3.11" - pytorch: 2.9.1 - axolotl_extras: +# - cuda: 130 +# cuda_version: 13.0.0 +# python_version: "3.11" +# pytorch: 2.9.1 +# axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -98,11 +98,11 @@ jobs: python_version: "3.11" pytorch: 2.9.1 axolotl_extras: - - cuda: 130 - cuda_version: 13.0.0 - python_version: "3.11" - pytorch: 2.9.1 - axolotl_extras: +# - cuda: 130 +# cuda_version: 13.0.0 +# python_version: "3.11" +# pytorch: 2.9.1 +# axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index cfd30b851c..950e692857 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -51,7 +51,7 @@ RUN git lfs install --skip-repo && \ pip3 install -U --no-cache-dir pydantic==1.10.10 && \ pip3 cache purge -RUN if [ "$PYTORCH_VERSION" = "2.9.1" ] && [ "$CUDA" = "128" ] ; then \ +RUN if [ "$PYTORCH_VERSION" =~ ^2\.9\.[0-9]+$ ] && [ "$CUDA" = "128" ] ; then \ wget https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.4.17/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ pip3 install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ From 4e61b8aa23a6725d6817e80652f70e92b2fb5132 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 5 Jan 2026 13:48:12 -0500 Subject: [PATCH 1082/1405] use updated version of prebuilt wheels for flash attention for cu130 (#3342) * use updated version of prebuilt wheels for flash attention for cu130 * use elif * fix the uv base installs of FA also * make wget less verbose --- .github/workflows/main.yml | 20 ++++++++++---------- docker/Dockerfile-base | 22 +++++++++++++++------- docker/Dockerfile-uv-base | 18 +++++++++++++----- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fc8d854d4d..052f9aa72f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,11 +31,11 @@ jobs: python_version: "3.11" pytorch: 2.9.1 axolotl_extras: -# - cuda: 130 -# cuda_version: 13.0.0 -# python_version: "3.11" -# pytorch: 2.9.1 -# axolotl_extras: + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.11" + pytorch: 2.9.1 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -98,11 +98,11 @@ jobs: python_version: "3.11" pytorch: 2.9.1 axolotl_extras: -# - cuda: 130 -# cuda_version: 13.0.0 -# python_version: "3.11" -# pytorch: 2.9.1 -# axolotl_extras: + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.11" + pytorch: 2.9.1 + axolotl_extras: runs-on: axolotl-gpu-runner steps: - name: Checkout diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 950e692857..e080758c2e 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -7,9 +7,9 @@ FROM nvidia/cuda:$CUDA_VERSION-cudnn$CUDNN_VERSION-devel-ubuntu$UBUNTU_VERSION A ENV PATH="/root/miniconda3/bin:${PATH}" -ARG PYTHON_VERSION="3.10" +ARG PYTHON_VERSION="3.11" ARG PYTORCH_VERSION="2.1.2" -ARG CUDA="118" +ARG CUDA="128" ARG TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6 9.0+PTX" ENV PYTHON_VERSION=$PYTHON_VERSION @@ -51,8 +51,16 @@ RUN git lfs install --skip-repo && \ pip3 install -U --no-cache-dir pydantic==1.10.10 && \ pip3 cache purge -RUN if [ "$PYTORCH_VERSION" =~ ^2\.9\.[0-9]+$ ] && [ "$CUDA" = "128" ] ; then \ - wget https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.4.17/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - pip3 install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - fi +RUN case "$PYTORCH_VERSION" in \ + 2.9.[0-9]*) \ + if [ "$CUDA" = "128" ]; then \ + wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + pip3 install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + elif [ "$CUDA" = "130" ]; then \ + wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ + pip3 install --no-cache-dir flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ + rm flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ + fi \ + ;; \ + esac diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index 2ca272c6ee..0b4dfc33f9 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -35,8 +35,16 @@ RUN uv pip install packaging setuptools wheel psutil \ && uv pip install "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" \ && uv pip install awscli pydantic -RUN if [ "$PYTORCH_VERSION" = "2.9.0" ] && [ "$CUDA" = "128" ] ; then \ - wget https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.4.17/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - uv pip install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - fi +RUN case "$PYTORCH_VERSION" in \ + 2.9.[0-9]*) \ + if [ "$CUDA" = "128" ]; then \ + wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + uv pip3 install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + elif [ "$CUDA" = "130" ]; then \ + wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ + uv pip3 install --no-cache-dir flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ + rm flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ + fi \ + ;; \ + esac From ee59e4de97ea38a6f9d597abadef6aed9e33b2f5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 5 Jan 2026 15:24:29 -0500 Subject: [PATCH 1083/1405] add cu130 + torch 2.9.1 to test matrices (#3343) * add cu130 + torch 2.9.1 to test matrices * uv can't use pip3 directly --- .github/workflows/multi-gpu-e2e.yml | 7 +++++++ .github/workflows/tests.yml | 6 ++++++ docker/Dockerfile-uv-base | 4 ++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 1dd019dc79..745e177bbb 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -43,6 +43,13 @@ jobs: axolotl_extras: fbgemm-gpu num_gpus: 2 nightly_build: "true" + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.11" + pytorch: 2.9.1 + axolotl_extras: fbgemm-gpu + num_gpus: 2 + nightly_build: "true" runs-on: [self-hosted, modal] timeout-minutes: 120 steps: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ae5ba17403..10c0e9bf1f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -316,6 +316,12 @@ jobs: pytorch: 2.9.1 num_gpus: 1 axolotl_extras: + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.11" + pytorch: 2.9.1 + num_gpus: 1 + axolotl_extras: steps: - name: Checkout uses: actions/checkout@v4 diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index 0b4dfc33f9..1b54c05e66 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -39,11 +39,11 @@ RUN case "$PYTORCH_VERSION" in \ 2.9.[0-9]*) \ if [ "$CUDA" = "128" ]; then \ wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - uv pip3 install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + uv pip install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ elif [ "$CUDA" = "130" ]; then \ wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ - uv pip3 install --no-cache-dir flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ + uv pip install --no-cache-dir flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ rm flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ fi \ ;; \ From 8aab807e678bdc60532dc748a72624bf0b0ae580 Mon Sep 17 00:00:00 2001 From: PraMamba <128919538+PraMamba@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:19:18 +0800 Subject: [PATCH 1084/1405] feat: Add SwanLab integration for experiment tracking (#3334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(swanlab): add SwanLab integration for experiment tracking SwanLab integration provides comprehensive experiment tracking and monitoring for Axolotl training. Features: - Hyperparameter logging - Training metrics tracking - RLHF completion logging - Performance profiling - Configuration validation and conflict detection Includes: - Plugin in src/axolotl/integrations/swanlab/ - Callback in src/axolotl/utils/callbacks/swanlab.py - Tests in tests/integrations/test_swanlab.py - Examples in examples/swanlab/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 * fix(swanlab): address PR #3334 review feedback from winglian and CodeRabbit - Change use_swanlab default to True (winglian) - Clear buffer after periodic logging to prevent duplicates (CodeRabbit Major) - Add safe exception handling in config fallback (CodeRabbit) - Use context managers for file operations (CodeRabbit) - Replace LOG.error with LOG.exception for better debugging (CodeRabbit) - Sort __all__ alphabetically (CodeRabbit) - Add language specifiers to README code blocks (CodeRabbit) - Fix end-of-file newline in README (pre-commit) Resolves actionable comments and nitpicks from CodeRabbit review. Addresses reviewer feedback from @winglian. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 * only run swanlab integration tests if package is available --------- Co-authored-by: Claude Sonnet 4.5 Co-authored-by: Wing Lian --- examples/swanlab/README.md | 285 ++++ examples/swanlab/custom_trainer_profiling.py | 299 ++++ examples/swanlab/dpo-swanlab-completions.yml | 168 +++ .../swanlab/dpo-swanlab-full-featured.yml | 329 ++++ examples/swanlab/lora-swanlab-profiling.yml | 178 +++ src/axolotl/integrations/swanlab/README.md | 1284 ++++++++++++++++ src/axolotl/integrations/swanlab/__init__.py | 6 + src/axolotl/integrations/swanlab/args.py | 140 ++ src/axolotl/integrations/swanlab/callbacks.py | 179 +++ .../integrations/swanlab/completion_logger.py | 228 +++ src/axolotl/integrations/swanlab/plugins.py | 554 +++++++ src/axolotl/integrations/swanlab/profiling.py | 203 +++ src/axolotl/utils/callbacks/swanlab.py | 248 +++ tests/integrations/test_swanlab.py | 1337 +++++++++++++++++ 14 files changed, 5438 insertions(+) create mode 100644 examples/swanlab/README.md create mode 100644 examples/swanlab/custom_trainer_profiling.py create mode 100644 examples/swanlab/dpo-swanlab-completions.yml create mode 100644 examples/swanlab/dpo-swanlab-full-featured.yml create mode 100644 examples/swanlab/lora-swanlab-profiling.yml create mode 100644 src/axolotl/integrations/swanlab/README.md create mode 100644 src/axolotl/integrations/swanlab/__init__.py create mode 100644 src/axolotl/integrations/swanlab/args.py create mode 100644 src/axolotl/integrations/swanlab/callbacks.py create mode 100644 src/axolotl/integrations/swanlab/completion_logger.py create mode 100644 src/axolotl/integrations/swanlab/plugins.py create mode 100644 src/axolotl/integrations/swanlab/profiling.py create mode 100644 src/axolotl/utils/callbacks/swanlab.py create mode 100644 tests/integrations/test_swanlab.py diff --git a/examples/swanlab/README.md b/examples/swanlab/README.md new file mode 100644 index 0000000000..c793ff2c28 --- /dev/null +++ b/examples/swanlab/README.md @@ -0,0 +1,285 @@ +# SwanLab Integration Examples + +This directory contains example configurations demonstrating SwanLab integration with Axolotl. + +## Examples Overview + +### 1. DPO with Completion Logging +**File**: `dpo-swanlab-completions.yml` + +Demonstrates DPO (Direct Preference Optimization) training with RLHF completion table logging. + +**Features**: +- Basic SwanLab experiment tracking +- Completion table logging (prompts, chosen/rejected responses, rewards) +- Memory-bounded buffer for long training runs +- Cloud sync configuration + +**Best for**: RLHF practitioners who want to analyze model outputs qualitatively + +**Quick start**: +```bash +export SWANLAB_API_KEY=your-api-key +accelerate launch -m axolotl.cli.train examples/swanlab/dpo-swanlab-completions.yml +``` + +--- + +### 2. LoRA with Performance Profiling +**File**: `lora-swanlab-profiling.yml` + +Demonstrates standard LoRA fine-tuning with performance profiling enabled. + +**Features**: +- SwanLab experiment tracking +- Automatic profiling of trainer methods +- Profiling metrics visualization +- Performance optimization guidance + +**Best for**: Engineers optimizing training performance and comparing different configurations + +**Quick start**: +```bash +export SWANLAB_API_KEY=your-api-key +accelerate launch -m axolotl.cli.train examples/swanlab/lora-swanlab-profiling.yml +``` + +--- + +### 3. Full-Featured DPO Production Setup +**File**: `dpo-swanlab-full-featured.yml` + +Comprehensive production-ready configuration with ALL SwanLab features enabled. + +**Features**: +- Experiment tracking with team workspace +- RLHF completion logging +- Performance profiling +- Lark (Feishu) team notifications +- Private deployment support +- Production checklist and troubleshooting + +**Best for**: Production RLHF training with team collaboration + +**Quick start**: +```bash +export SWANLAB_API_KEY=your-api-key +export SWANLAB_LARK_WEBHOOK_URL=https://open.feishu.cn/... +export SWANLAB_LARK_SECRET=your-webhook-secret +accelerate launch -m axolotl.cli.train examples/swanlab/dpo-swanlab-full-featured.yml +``` + +--- + +### 4. Custom Trainer Profiling (Python) +**File**: `custom_trainer_profiling.py` + +Python code examples showing how to add SwanLab profiling to custom trainers. + +**Features**: +- `@swanlab_profile` decorator examples +- Context manager profiling for fine-grained timing +- `ProfilingConfig` for advanced filtering and throttling +- Multiple profiling patterns and best practices + +**Best for**: Advanced users creating custom trainers + +**Usage**: +```python +from custom_trainer_profiling import CustomTrainerWithProfiling +# See file for detailed examples and patterns +``` + +--- + +## Feature Matrix + +| Example | Tracking | Completion Logging | Profiling | Lark Notifications | Team Workspace | +|---------|----------|-------------------|-----------|-------------------|----------------| +| dpo-swanlab-completions.yml | ✅ | ✅ | ✅ (auto) | ➖ (commented) | ➖ (commented) | +| lora-swanlab-profiling.yml | ✅ | ➖ (disabled) | ✅ (auto) | ➖ (commented) | ➖ (commented) | +| dpo-swanlab-full-featured.yml | ✅ | ✅ | ✅ (auto) | ✅ | ✅ | +| custom_trainer_profiling.py | N/A | N/A | ✅ (manual) | N/A | N/A | + +--- + +## Configuration Quick Reference + +### Basic SwanLab Setup +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +use_swanlab: true +swanlab_project: my-project +swanlab_experiment_name: my-experiment +swanlab_mode: cloud # cloud, local, offline, disabled +``` + +### RLHF Completion Logging +```yaml +swanlab_log_completions: true +swanlab_completion_log_interval: 100 # Log every 100 steps +swanlab_completion_max_buffer: 128 # Memory-bounded buffer +``` + +### Lark Team Notifications +```yaml +swanlab_lark_webhook_url: https://open.feishu.cn/... +swanlab_lark_secret: your-webhook-secret # Required for production +``` + +### Team Workspace +```yaml +swanlab_workspace: my-research-team +``` + +### Private Deployment +```yaml +swanlab_web_host: https://swanlab.yourcompany.com +swanlab_api_host: https://api.swanlab.yourcompany.com +``` + +--- + +## Authentication + +### Recommended: Environment Variable +```bash +export SWANLAB_API_KEY=your-api-key +export SWANLAB_LARK_WEBHOOK_URL=https://open.feishu.cn/... +export SWANLAB_LARK_SECRET=your-webhook-secret +``` + +### Alternative: Config File (less secure) +```yaml +swanlab_api_key: your-api-key +swanlab_lark_webhook_url: https://open.feishu.cn/... +swanlab_lark_secret: your-webhook-secret +``` + +--- + +## Common Use Cases + +### Use Case 1: Migrate from WandB to SwanLab +Start with `lora-swanlab-profiling.yml`, add your model/dataset config, disable WandB: +```yaml +use_swanlab: true +use_wandb: false +``` + +### Use Case 2: Analyze DPO Model Outputs +Use `dpo-swanlab-completions.yml`, adjust completion logging interval based on your training length: +```yaml +swanlab_completion_log_interval: 50 # More frequent for short training +swanlab_completion_log_interval: 200 # Less frequent for long training +``` + +### Use Case 3: Optimize Training Performance +Use `lora-swanlab-profiling.yml`, run multiple experiments with different optimizations: +- Baseline: `flash_attention: false, gradient_checkpointing: false` +- Flash Attention: `flash_attention: true` +- Gradient Checkpointing: `gradient_checkpointing: true` +- Both: `flash_attention: true, gradient_checkpointing: true` + +Compare profiling metrics in SwanLab dashboard. + +### Use Case 4: Production RLHF with Team Collaboration +Use `dpo-swanlab-full-featured.yml`, set up team workspace and Lark notifications: +```yaml +swanlab_workspace: ml-team +swanlab_lark_webhook_url: ... +swanlab_lark_secret: ... +``` + +--- + +## Viewing Your Experiments + +### Cloud Mode +Visit [https://swanlab.cn](https://swanlab.cn) and navigate to your project. + +**Dashboard sections**: +- **Metrics**: Training loss, learning rate, profiling metrics +- **Tables**: RLHF completions (for DPO/KTO/ORPO/GRPO) +- **Config**: Hyperparameters and configuration +- **System**: Resource usage (GPU, memory, CPU) +- **Files**: Logged artifacts + +### Local Mode +```bash +swanlab watch ./swanlog +# Open browser to http://localhost:5092 +``` + +--- + +## Troubleshooting + +### SwanLab not initializing +```bash +# Check API key +echo $SWANLAB_API_KEY + +# Verify SwanLab is installed +pip show swanlab + +# Check config +grep -A 5 "use_swanlab" your-config.yml +``` + +### Completions not appearing +- Verify you're using an RLHF trainer (DPO/KTO/ORPO/GRPO) +- Check `swanlab_log_completions: true` +- Wait for `swanlab_completion_log_interval` steps +- Look for "Registered SwanLab RLHF completion logging" in logs + +### Lark notifications not working +- Test webhook manually: `curl -X POST "$SWANLAB_LARK_WEBHOOK_URL" ...` +- Verify `SWANLAB_LARK_SECRET` is set correctly +- Check bot is added to Lark group chat +- Look for "Registered Lark notification callback" in logs + +### Profiling metrics not appearing +- Verify `use_swanlab: true` +- Check SwanLab is initialized (look for init log message) +- Profiling metrics are under "profiling/" namespace +- Profiling auto-enabled when SwanLab is enabled + +--- + +## Performance Notes + +### Overhead Comparison + +| Feature | Overhead per Step | Memory Usage | +|---------|------------------|--------------| +| Basic tracking | < 0.1% | ~10 MB | +| Completion logging | < 0.5% | ~64 KB (buffer=128) | +| Profiling | < 0.1% | ~1 KB | +| **Total** | **< 0.7%** | **~10 MB** | + +### Best Practices +1. Use ONE logging tool in production (disable WandB/MLflow when using SwanLab) +2. Adjust completion log interval based on training length (100-200 steps) +3. Keep completion buffer size reasonable (128-512) +4. Profile critical path methods first (training_step, compute_loss) +5. Use ProfilingConfig to throttle high-frequency operations + +--- + +## Further Reading + +- **Full Documentation**: [src/axolotl/integrations/swanlab/README.md](../../src/axolotl/integrations/swanlab/README.md) +- **SwanLab Docs**: [https://docs.swanlab.cn](https://docs.swanlab.cn) +- **Axolotl Docs**: [https://axolotl-ai-cloud.github.io/axolotl/](https://axolotl-ai-cloud.github.io/axolotl/) +- **DPO Paper**: [Direct Preference Optimization](https://arxiv.org/abs/2305.18290) + +--- + +## Contributing + +Found an issue or have an improvement? Please submit a PR or open an issue: +- [Axolotl Issues](https://github.com/axolotl-ai-cloud/axolotl/issues) +- [SwanLab Issues](https://github.com/SwanHubX/SwanLab/issues) diff --git a/examples/swanlab/custom_trainer_profiling.py b/examples/swanlab/custom_trainer_profiling.py new file mode 100644 index 0000000000..65461c4e5a --- /dev/null +++ b/examples/swanlab/custom_trainer_profiling.py @@ -0,0 +1,299 @@ +"""Example: Custom Trainer with SwanLab Profiling + +This example demonstrates how to add SwanLab profiling to your custom trainer. + +Features: +- @swanlab_profile decorator for automatic profiling +- swanlab_profiling_context for fine-grained profiling +- ProfilingConfig for advanced filtering and throttling + +Usage: + 1. Create your custom trainer extending AxolotlTrainer + 2. Add @swanlab_profile decorators to methods you want to profile + 3. Use swanlab_profiling_context for fine-grained profiling within methods + 4. Enable SwanLab in your config (use_swanlab: true) + +See also: + - examples/swanlab/lora-swanlab-profiling.yml for config + - src/axolotl/integrations/swanlab/profiling.py for implementation +""" + +from axolotl.core.trainers.base import AxolotlTrainer +from axolotl.integrations.swanlab.profiling import ( + ProfilingConfig, + swanlab_profile, + swanlab_profiling_context, + swanlab_profiling_context_advanced, +) + + +class CustomTrainerWithProfiling(AxolotlTrainer): + """Custom trainer with SwanLab profiling enabled. + + This trainer demonstrates three profiling patterns: + 1. Decorator-based profiling (@swanlab_profile) + 2. Context manager profiling (swanlab_profiling_context) + 3. Advanced profiling with filtering (ProfilingConfig) + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Create custom profiling config for high-frequency operations + self.fast_op_config = ProfilingConfig( + enabled=True, + min_duration_ms=0.5, # Only log if duration > 0.5ms + log_interval=50, # Log every 50th call + ) + + # ======================================================================== + # Pattern 1: Decorator-based Profiling + # ======================================================================== + # Best for: Methods you always want to profile + # Overhead: ~2-5 microseconds per call (negligible) + + @swanlab_profile + def training_step(self, model, inputs): + """Main training step - always profile. + + Profiling metric: profiling/Time taken: CustomTrainerWithProfiling.training_step + """ + return super().training_step(model, inputs) + + @swanlab_profile + def compute_loss(self, model, inputs, return_outputs=False): + """Loss computation - always profile. + + Profiling metric: profiling/Time taken: CustomTrainerWithProfiling.compute_loss + """ + return super().compute_loss(model, inputs, return_outputs) + + @swanlab_profile + def prediction_step(self, model, inputs, prediction_loss_only, ignore_keys=None): + """Prediction step - always profile. + + Profiling metric: profiling/Time taken: CustomTrainerWithProfiling.prediction_step + """ + return super().prediction_step(model, inputs, prediction_loss_only, ignore_keys) + + # ======================================================================== + # Pattern 2: Fine-grained Context Manager Profiling + # ======================================================================== + # Best for: Profiling specific code blocks within a method + # Use case: When you want to profile forward vs backward separately + + def complex_training_step(self, model, inputs): + """Training step with fine-grained profiling. + + Profiling metrics: + - profiling/Time taken: CustomTrainerWithProfiling.forward_pass + - profiling/Time taken: CustomTrainerWithProfiling.backward_pass + - profiling/Time taken: CustomTrainerWithProfiling.optimizer_step + """ + # Profile just the forward pass + with swanlab_profiling_context(self, "forward_pass"): + outputs = model(**inputs) + loss = outputs.loss + + # Profile just the backward pass + with swanlab_profiling_context(self, "backward_pass"): + loss.backward() + + # Profile optimizer step + with swanlab_profiling_context(self, "optimizer_step"): + self.optimizer.step() + self.optimizer.zero_grad() + + return outputs + + # ======================================================================== + # Pattern 3: Advanced Profiling with Filtering + # ======================================================================== + # Best for: High-frequency operations where you want to throttle logging + # Use case: Methods called 100+ times per step + + def _prepare_inputs(self, inputs): + """Prepare inputs - throttled profiling. + + This method is called frequently (once per batch), so we throttle + profiling to reduce overhead: + - Only log if duration > 0.5ms (skip very fast operations) + - Only log every 50th call (reduce logging frequency) + + Profiling metric: profiling/Time taken: CustomTrainerWithProfiling.prepare_inputs + """ + with swanlab_profiling_context_advanced( + self, "prepare_inputs", config=self.fast_op_config + ): + return super()._prepare_inputs(inputs) + + def _prepare_input_for_model(self, input_ids): + """Another high-frequency operation - throttled profiling. + + Profiling metric: profiling/Time taken: CustomTrainerWithProfiling.prepare_input_for_model + """ + with swanlab_profiling_context_advanced( + self, "prepare_input_for_model", config=self.fast_op_config + ): + # Your custom input preparation logic + return input_ids + + # ======================================================================== + # Pattern 4: Exception-safe Profiling + # ======================================================================== + # Profiling is exception-safe: duration is logged even if method raises + + @swanlab_profile + def potentially_failing_method(self): + """This method may raise an exception. + + SwanLab profiling will still log the duration before re-raising. + Profiling metric: profiling/Time taken: CustomTrainerWithProfiling.potentially_failing_method + """ + # Do some work + result = self._do_risky_computation() + + # If this raises, profiling duration is still logged + if result < 0: + raise ValueError("Invalid result") + + return result + + def _do_risky_computation(self): + """Placeholder for risky computation.""" + return 42 + + +# ============================================================================ +# Advanced Example: Custom ProfilingConfig Per Method +# ============================================================================ + + +class AdvancedProfilingTrainer(AxolotlTrainer): + """Trainer with method-specific profiling configurations.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Different profiling configs for different method types + self.critical_path_config = ProfilingConfig( + enabled=True, + min_duration_ms=0.0, # Log everything on critical path + log_interval=1, # Log every call + ) + + self.fast_path_config = ProfilingConfig( + enabled=True, + min_duration_ms=1.0, # Only log if > 1ms + log_interval=100, # Log every 100th call + ) + + self.debug_config = ProfilingConfig( + enabled=True, + min_duration_ms=0.0, # Log everything + log_interval=1, # Log every call + ) + + def training_step(self, model, inputs): + """Critical path - log everything.""" + with swanlab_profiling_context_advanced( + self, "training_step", config=self.critical_path_config + ): + return super().training_step(model, inputs) + + def _prepare_inputs(self, inputs): + """Fast path - throttle logging.""" + with swanlab_profiling_context_advanced( + self, "prepare_inputs", config=self.fast_path_config + ): + return super()._prepare_inputs(inputs) + + def _debug_method(self, data): + """Debug-only method - verbose logging.""" + with swanlab_profiling_context_advanced( + self, "debug_method", config=self.debug_config + ): + # Your debug logic + pass + + +# ============================================================================ +# How to Use This Custom Trainer +# ============================================================================ + +""" +To use this custom trainer: + +1. Save this file to your project (e.g., my_custom_trainer.py) + +2. Create a config file that uses your custom trainer: + + # config.yml + base_model: NousResearch/Llama-3.2-1B + + # ... other config ... + + plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + + use_swanlab: true + swanlab_project: my-profiling-experiment + + # Optional: Specify custom trainer + # (Or modify axolotl to use your custom trainer class) + +3. Run training: + + export SWANLAB_API_KEY=your-api-key + accelerate launch -m axolotl.cli.train config.yml + +4. View profiling metrics in SwanLab dashboard: + - profiling/Time taken: CustomTrainerWithProfiling.training_step + - profiling/Time taken: CustomTrainerWithProfiling.forward_pass + - profiling/Time taken: CustomTrainerWithProfiling.backward_pass + - etc. + +5. Compare profiling metrics across runs: + - Run baseline without optimizations + - Run with flash_attention enabled + - Run with gradient_checkpointing enabled + - Compare profiling metrics to see performance impact +""" + +# ============================================================================ +# Tips for Effective Profiling +# ============================================================================ + +""" +1. Profile the critical path first: + - training_step, compute_loss, prediction_step + - These methods are called most frequently and have biggest impact + +2. Use throttling for high-frequency operations: + - Methods called 100+ times per step + - Use log_interval=50 or log_interval=100 + - Reduces profiling overhead and dashboard clutter + +3. Filter noise with min_duration_ms: + - Set min_duration_ms=1.0 to skip very fast operations + - Focus on operations that actually take time + +4. Compare across runs: + - Run same config multiple times to check consistency + - Compare different optimization strategies + - Track profiling trends over time + +5. Monitor distributed training: + - Check for per-rank timing differences + - Look for stragglers (slower ranks) + - Identify synchronization bottlenecks + +6. Disable profiling in production: + - from axolotl.integrations.swanlab.profiling import DEFAULT_PROFILING_CONFIG + - DEFAULT_PROFILING_CONFIG.enabled = False + +7. Exception handling: + - Profiling is exception-safe + - Duration logged even if method raises + - Useful for debugging methods that fail intermittently +""" diff --git a/examples/swanlab/dpo-swanlab-completions.yml b/examples/swanlab/dpo-swanlab-completions.yml new file mode 100644 index 0000000000..5615ca6382 --- /dev/null +++ b/examples/swanlab/dpo-swanlab-completions.yml @@ -0,0 +1,168 @@ +# SwanLab DPO Training Example with Completion Logging +# +# This example demonstrates DPO (Direct Preference Optimization) training +# with SwanLab integration for experiment tracking and completion table logging. +# +# Features enabled: +# - SwanLab experiment tracking +# - RLHF completion table logging (prompts, chosen/rejected responses, rewards) +# - Lark (Feishu) team notifications (optional) +# +# To run: +# export SWANLAB_API_KEY=your-api-key +# accelerate launch -m axolotl.cli.train examples/swanlab/dpo-swanlab-completions.yml + +# Model Configuration +base_model: meta-llama/Meta-Llama-3-8B-Instruct +model_type: LlamaForCausalLM +tokenizer_type: AutoTokenizer + +special_tokens: + pad_token: <|finetune_right_pad_id|> + eos_token: <|eot_id|> + +# Quantization +load_in_8bit: true +load_in_4bit: false + +# LoRA Configuration +adapter: lora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true + +# DPO Configuration +chat_template: llama3 +rl: dpo + +datasets: + - path: fozziethebeat/alpaca_messages_2k_dpo_test + type: chat_template.default + field_messages: conversation + field_chosen: chosen + field_rejected: rejected + message_property_mappings: + role: role + content: content + roles: + system: + - system + user: + - user + assistant: + - assistant + +# Dataset and Output +dataset_prepared_path: +val_set_size: 0.05 +output_dir: ./outputs/dpo-swanlab-out + +# Training Configuration +sequence_len: 4096 +sample_packing: false +micro_batch_size: 2 +gradient_accumulation_steps: 4 +num_epochs: 4 + +# Optimization +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 +warmup_ratio: 0.1 +weight_decay: 0.0 + +# Precision +bf16: auto +tf32: false + +# Performance +gradient_checkpointing: true +flash_attention: true + +# Checkpointing and Logging +logging_steps: 1 +evals_per_epoch: 4 +saves_per_epoch: 1 + +# ============================================================================ +# SwanLab Integration +# ============================================================================ + +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +# Basic SwanLab Configuration +use_swanlab: true +swanlab_project: dpo-training +swanlab_experiment_name: llama-3-dpo-completions-demo +swanlab_description: "DPO training with completion table logging" +swanlab_mode: cloud # Options: cloud, local, offline, disabled + +# SwanLab Authentication +# Recommended: Set via environment variable +# export SWANLAB_API_KEY=your-api-key +# Or set in config (less secure): +# swanlab_api_key: your-api-key + +# Optional: Team workspace +# swanlab_workspace: my-research-team + +# ============================================================================ +# RLHF Completion Table Logging +# ============================================================================ +# +# Automatically logs model completions to SwanLab for qualitative analysis: +# - Prompts from your DPO dataset +# - Chosen responses (preferred) +# - Rejected responses (non-preferred) +# - Reward differences +# +# View the table in SwanLab dashboard under "rlhf_completions" + +swanlab_log_completions: true +swanlab_completion_log_interval: 100 # Log every 100 training steps +swanlab_completion_max_buffer: 128 # Keep last 128 completions in memory + +# Memory Usage Notes: +# - Buffer size 128: ~64 KB (default, recommended) +# - Buffer size 512: ~256 KB (for more historical completions) +# - Buffer size 1024: ~512 KB (maximum for very long training runs) + +# Performance Notes: +# - Completion logging overhead: < 0.5% per training step +# - Only logs every N steps to minimize impact +# - Memory-bounded buffer prevents memory leaks + +# ============================================================================ +# Optional: Lark (Feishu) Team Notifications +# ============================================================================ +# +# Get real-time training notifications in your team chat +# Uncomment to enable: + +# swanlab_lark_webhook_url: https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx +# swanlab_lark_secret: your-webhook-secret # Recommended for production + +# Notifications sent for: +# - Training start +# - Training completion +# - Training errors +# - Metric milestones (if configured) + +# ============================================================================ +# Optional: Private SwanLab Deployment +# ============================================================================ +# +# For enterprise users with private SwanLab deployment: + +# swanlab_web_host: https://swanlab.yourcompany.com +# swanlab_api_host: https://api.swanlab.yourcompany.com + +# ============================================================================ +# Disable WandB if you're migrating from it +# ============================================================================ + +# wandb_project: +# wandb_entity: +# use_wandb: false diff --git a/examples/swanlab/dpo-swanlab-full-featured.yml b/examples/swanlab/dpo-swanlab-full-featured.yml new file mode 100644 index 0000000000..c25178c631 --- /dev/null +++ b/examples/swanlab/dpo-swanlab-full-featured.yml @@ -0,0 +1,329 @@ +# SwanLab Full-Featured DPO Training Example +# +# This example demonstrates ALL SwanLab integration features: +# - Experiment tracking with cloud sync +# - RLHF completion table logging +# - Performance profiling +# - Lark (Feishu) team notifications +# - Team workspace collaboration +# +# Use this as a reference for production RLHF training setups. +# +# To run: +# export SWANLAB_API_KEY=your-api-key +# export SWANLAB_LARK_WEBHOOK_URL=https://open.feishu.cn/... +# export SWANLAB_LARK_SECRET=your-webhook-secret +# accelerate launch -m axolotl.cli.train examples/swanlab/dpo-swanlab-full-featured.yml + +# ============================================================================ +# Model Configuration +# ============================================================================ + +base_model: meta-llama/Meta-Llama-3-8B-Instruct +model_type: LlamaForCausalLM +tokenizer_type: AutoTokenizer + +special_tokens: + pad_token: <|finetune_right_pad_id|> + eos_token: <|eot_id|> + +# Quantization for efficient training +load_in_8bit: true +load_in_4bit: false + +# ============================================================================ +# LoRA Configuration +# ============================================================================ + +adapter: lora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true # Target all linear layers + +# ============================================================================ +# DPO (Direct Preference Optimization) Configuration +# ============================================================================ + +chat_template: llama3 +rl: dpo # Enable DPO trainer + +datasets: + - path: fozziethebeat/alpaca_messages_2k_dpo_test + type: chat_template.default + field_messages: conversation + field_chosen: chosen + field_rejected: rejected + message_property_mappings: + role: role + content: content + roles: + system: + - system + user: + - user + assistant: + - assistant + +# ============================================================================ +# Dataset and Output Configuration +# ============================================================================ + +dataset_prepared_path: +val_set_size: 0.05 +output_dir: ./outputs/dpo-swanlab-full-featured-out + +# ============================================================================ +# Training Configuration +# ============================================================================ + +sequence_len: 4096 +sample_packing: false + +micro_batch_size: 2 +gradient_accumulation_steps: 4 +num_epochs: 4 + +# ============================================================================ +# Optimization +# ============================================================================ + +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 +warmup_ratio: 0.1 +weight_decay: 0.0 + +# ============================================================================ +# Precision and Performance +# ============================================================================ + +bf16: auto +tf32: false + +gradient_checkpointing: true +flash_attention: true + +# ============================================================================ +# Checkpointing and Logging +# ============================================================================ + +logging_steps: 1 +evals_per_epoch: 4 +saves_per_epoch: 1 + +# ============================================================================ +# SwanLab Integration - Full Configuration +# ============================================================================ + +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +# ------------------------------------------------------------------------------ +# Basic SwanLab Configuration +# ------------------------------------------------------------------------------ + +use_swanlab: true +swanlab_project: dpo-production +swanlab_experiment_name: llama-3-dpo-full-featured-v1 +swanlab_description: | + Production DPO training with all SwanLab features enabled: + - Completion table logging for qualitative analysis + - Performance profiling for optimization + - Lark notifications for team collaboration + +swanlab_mode: cloud # Options: cloud, local, offline, disabled + +# ------------------------------------------------------------------------------ +# Team Collaboration +# ------------------------------------------------------------------------------ + +# Workspace for team collaboration (shared experiments) +swanlab_workspace: ml-research-team + +# Authentication (recommended: use environment variable) +# export SWANLAB_API_KEY=your-api-key +# Or set in config (less secure): +# swanlab_api_key: your-api-key + +# ------------------------------------------------------------------------------ +# RLHF Completion Table Logging +# ------------------------------------------------------------------------------ +# Automatically logs model completions for qualitative analysis: +# - Prompts from your DPO dataset +# - Chosen responses (preferred) +# - Rejected responses (non-preferred) +# - Reward differences +# +# View in SwanLab dashboard under "rlhf_completions" table + +swanlab_log_completions: true +swanlab_completion_log_interval: 100 # Log every 100 steps +swanlab_completion_max_buffer: 256 # Larger buffer for long training runs + +# Buffer size recommendations: +# - 128: Default, ~64 KB memory (recommended for most cases) +# - 256: ~128 KB memory (this config, good for longer training) +# - 512: ~256 KB memory (maximum for very long runs) + +# ------------------------------------------------------------------------------ +# Lark (Feishu) Team Notifications +# ------------------------------------------------------------------------------ +# Get real-time training notifications in your team chat +# +# Notifications sent for: +# - Training start +# - Training completion +# - Training errors +# - Metric milestones (if configured) + +# Recommended: Set via environment variables +# export SWANLAB_LARK_WEBHOOK_URL=https://open.feishu.cn/... +# export SWANLAB_LARK_SECRET=your-webhook-secret + +# Or set in config (less secure): +# swanlab_lark_webhook_url: https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx +# swanlab_lark_secret: your-webhook-secret # REQUIRED for production + +# Security note: ALWAYS use swanlab_lark_secret in production to prevent +# unauthorized parties from sending fake notifications to your team chat. + +# ------------------------------------------------------------------------------ +# Performance Profiling +# ------------------------------------------------------------------------------ +# Profiling is automatically enabled when SwanLab is enabled. +# Metrics logged to SwanLab under "profiling/" namespace: +# profiling/Time taken: AxolotlTrainer.training_step +# profiling/Time taken: AxolotlTrainer.compute_loss +# profiling/Time taken: AxolotlTrainer.prediction_step +# +# Use these metrics to: +# - Identify bottlenecks in training loop +# - Compare performance across different configurations +# - Monitor performance regressions over time +# - Debug unexpected slowdowns + +# For custom profiling in your own trainer, see: +# examples/swanlab/custom_trainer_profiling.py + +# ------------------------------------------------------------------------------ +# Optional: Private SwanLab Deployment +# ------------------------------------------------------------------------------ +# For enterprise users with private SwanLab deployment: + +# swanlab_web_host: https://swanlab.yourcompany.com +# swanlab_api_host: https://api.swanlab.yourcompany.com + +# ------------------------------------------------------------------------------ +# Optional: Model Checkpointing to SwanLab +# ------------------------------------------------------------------------------ +# Log model checkpoints to SwanLab (coming soon) + +swanlab_log_model: false + +# ============================================================================ +# Disable Other Logging Tools (Recommended) +# ============================================================================ +# Using multiple logging tools simultaneously can impact performance: +# - Expected overhead: ~1-2% per logger +# - Potential config/callback conflicts +# +# For production training, use ONLY SwanLab: + +# wandb_project: +# use_wandb: false +# +# use_mlflow: false +# +# use_comet: false + +# ============================================================================ +# Expected Training Behavior +# ============================================================================ + +# With this configuration, you should see: +# +# 1. SwanLab Initialization (rank 0 only): +# INFO: SwanLab initialized for project: dpo-production +# INFO: SwanLab experiment: llama-3-dpo-full-featured-v1 +# INFO: SwanLab mode: cloud +# INFO: SwanLab workspace: ml-research-team +# +# 2. Completion Logging (rank 0 only): +# INFO: Registered SwanLab RLHF completion logging callback for DPOTrainer +# (log_interval=100, max_buffer=256) +# +# 3. Lark Notifications (rank 0 only): +# INFO: Registered Lark notification callback with HMAC authentication +# +# 4. Distributed Training Detection (if multi-GPU): +# INFO: Distributed training detected (world_size=N) +# INFO: Only rank 0 will initialize SwanLab +# INFO: Other ranks will skip SwanLab to avoid conflicts +# +# 5. Training Start Notification (Lark): +# Your team chat receives: "Training started: llama-3-dpo-full-featured-v1" +# +# 6. Periodic Completion Logging: +# Every 100 steps, completion table is updated in SwanLab dashboard +# +# 7. Training Complete Notification (Lark): +# Your team chat receives: "Training completed: llama-3-dpo-full-featured-v1" +# With link to SwanLab dashboard and final metrics +# +# 8. SwanLab Dashboard Shows: +# - Training metrics (loss, learning rate, etc.) +# - Completion table (rlhf_completions) +# - Profiling metrics (profiling/Time taken: ...) +# - Hyperparameters and configuration +# - System resource usage + +# ============================================================================ +# Production Checklist +# ============================================================================ + +# Before deploying to production, verify: +# ✅ SwanLab API key is set via environment variable (not in config) +# ✅ Lark webhook secret is set (required for HMAC authentication) +# ✅ Workspace is set to your team's workspace +# ✅ Experiment name is descriptive and unique +# ✅ Only SwanLab is enabled (other loggers disabled) +# ✅ Completion logging buffer size is appropriate for your training duration +# ✅ Private deployment hosts are set (if using enterprise SwanLab) +# ✅ Test run completes successfully and shows up in SwanLab dashboard +# ✅ Lark notifications are received in team chat +# ✅ Profiling metrics are logged correctly + +# ============================================================================ +# Troubleshooting +# ============================================================================ + +# If SwanLab initialization fails: +# 1. Check SWANLAB_API_KEY environment variable is set +# 2. Verify swanlab_project is set in config +# 3. Check swanlab_mode is valid (cloud/local/offline/disabled) +# 4. Verify internet connectivity (for cloud mode) + +# If Lark notifications not received: +# 1. Check SWANLAB_LARK_WEBHOOK_URL is set correctly +# 2. Verify SWANLAB_LARK_SECRET matches your Lark bot settings +# 3. Test webhook manually: curl -X POST "$SWANLAB_LARK_WEBHOOK_URL" ... +# 4. Check training logs for "Registered Lark notification callback" +# 5. Verify bot is added to the target Lark group chat + +# If completions not appearing in SwanLab: +# 1. Verify you're using an RLHF trainer (DPO/KTO/ORPO/GRPO) +# 2. Check swanlab_log_completions is true +# 3. Wait for log_interval steps (default: 100) +# 4. Check training logs for "Registered SwanLab RLHF completion logging" + +# If profiling metrics not appearing: +# 1. Verify use_swanlab is true +# 2. Check SwanLab is initialized (check logs) +# 3. Look under "profiling/" namespace in dashboard +# 4. Profiling may be disabled if DEFAULT_PROFILING_CONFIG.enabled = False + +# For more help: +# - SwanLab docs: https://docs.swanlab.cn +# - Axolotl SwanLab integration: src/axolotl/integrations/swanlab/README.md +# - GitHub issues: https://github.com/axolotl-ai-cloud/axolotl/issues diff --git a/examples/swanlab/lora-swanlab-profiling.yml b/examples/swanlab/lora-swanlab-profiling.yml new file mode 100644 index 0000000000..1255105a6d --- /dev/null +++ b/examples/swanlab/lora-swanlab-profiling.yml @@ -0,0 +1,178 @@ +# SwanLab LoRA Training Example with Performance Profiling +# +# This example demonstrates standard LoRA fine-tuning with SwanLab integration +# for performance profiling and optimization. +# +# Features enabled: +# - SwanLab experiment tracking +# - Performance profiling (training step, forward/backward pass timing) +# - Real-time metrics visualization +# +# To run: +# export SWANLAB_API_KEY=your-api-key +# accelerate launch -m axolotl.cli.train examples/swanlab/lora-swanlab-profiling.yml + +# Model Configuration +base_model: NousResearch/Llama-3.2-1B + +# Dataset Configuration +datasets: + - path: teknium/GPT4-LLM-Cleaned + type: alpaca + +val_set_size: 0.1 +output_dir: ./outputs/lora-swanlab-profiling-out + +# LoRA Configuration +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.05 +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +# Training Configuration +sequence_len: 2048 +sample_packing: true +eval_sample_packing: true + +micro_batch_size: 2 +gradient_accumulation_steps: 2 +num_epochs: 1 + +# Optimization +optimizer: adamw_8bit +lr_scheduler: cosine +learning_rate: 0.0002 +warmup_ratio: 0.1 +weight_decay: 0.0 + +# Precision +bf16: auto +tf32: false + +# Performance +gradient_checkpointing: true +flash_attention: true + +# Checkpointing and Logging +logging_steps: 1 +evals_per_epoch: 4 +saves_per_epoch: 1 + +# Loss Monitoring +loss_watchdog_threshold: 5.0 +loss_watchdog_patience: 3 + +special_tokens: + pad_token: "<|end_of_text|>" + +# ============================================================================ +# SwanLab Integration +# ============================================================================ + +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +# Basic SwanLab Configuration +use_swanlab: true +swanlab_project: lora-profiling +swanlab_experiment_name: llama-3.2-1b-profiling-demo +swanlab_description: "LoRA fine-tuning with performance profiling" +swanlab_mode: cloud # Options: cloud, local, offline, disabled + +# SwanLab Authentication +# Recommended: Set via environment variable +# export SWANLAB_API_KEY=your-api-key +# Or set in config (less secure): +# swanlab_api_key: your-api-key + +# Optional: Team workspace +# swanlab_workspace: my-ml-team + +# ============================================================================ +# Performance Profiling +# ============================================================================ +# +# SwanLab automatically profiles trainer methods when enabled. +# Profiling metrics appear in SwanLab dashboard under "profiling/" namespace. +# +# Built-in profiling: +# - Minimal overhead (< 0.1% per step) +# - High-precision timing (microsecond accuracy) +# - Exception-safe (logs duration even if method fails) +# +# View profiling metrics in SwanLab dashboard: +# profiling/Time taken: AxolotlTrainer.training_step +# profiling/Time taken: AxolotlTrainer.compute_loss +# profiling/Time taken: AxolotlTrainer.prediction_step +# +# For custom profiling in your own trainer, see: +# examples/swanlab/custom_trainer_profiling.py + +# Completion logging is disabled for non-RLHF trainers +swanlab_log_completions: false # Only works with DPO/KTO/ORPO/GRPO + +# ============================================================================ +# Optional: Compare with Multiple Runs +# ============================================================================ +# +# To compare profiling metrics across different configurations: +# +# 1. Run baseline without flash attention: +# swanlab_experiment_name: llama-3.2-1b-no-flash-attn +# flash_attention: false +# +# 2. Run with gradient checkpointing: +# swanlab_experiment_name: llama-3.2-1b-grad-checkpoint +# gradient_checkpointing: true +# +# 3. Run with both: +# swanlab_experiment_name: llama-3.2-1b-optimized +# flash_attention: true +# gradient_checkpointing: true +# +# Then compare profiling metrics in SwanLab dashboard to see performance impact + +# ============================================================================ +# Optional: Lark (Feishu) Team Notifications +# ============================================================================ +# +# Get notified when profiling experiments complete: + +# swanlab_lark_webhook_url: https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx +# swanlab_lark_secret: your-webhook-secret + +# ============================================================================ +# Profiling Best Practices +# ============================================================================ +# +# 1. Run multiple epochs to see profiling trends over time +# 2. Ignore first ~10 steps (warmup period, slower) +# 3. Look for outliers (steps that take significantly longer) +# 4. Compare profiling metrics before/after optimization changes +# 5. Monitor per-rank profiling in distributed training +# +# Common bottlenecks to profile: +# - training_step: Overall step time (should be consistent) +# - compute_loss: Loss computation (scales with sequence length) +# - prediction_step: Evaluation time (can be slow for large val sets) +# +# If you see inconsistent timing: +# - Check for data loading bottlenecks +# - Monitor GPU utilization (may be CPU-bound) +# - Check for gradient accumulation effects +# - Verify CUDA kernel synchronization + +# ============================================================================ +# Disable WandB if you're migrating from it +# ============================================================================ + +# wandb_project: +# use_wandb: false diff --git a/src/axolotl/integrations/swanlab/README.md b/src/axolotl/integrations/swanlab/README.md new file mode 100644 index 0000000000..eff7d4a5ab --- /dev/null +++ b/src/axolotl/integrations/swanlab/README.md @@ -0,0 +1,1284 @@ +# SwanLab Integration for Axolotl + +SwanLab is an open-source, lightweight AI experiment tracking and visualization tool that provides a platform for tracking, recording, comparing, and collaborating on experiments. + +This integration enables seamless experiment tracking and visualization of Axolotl training runs using SwanLab. + +## Features + +- 📊 **Automatic Metrics Logging**: Training loss, learning rate, and other metrics are automatically logged +- 🎯 **Hyperparameter Tracking**: Model configuration and training parameters are tracked +- 📈 **Real-time Visualization**: Monitor training progress in real-time through SwanLab dashboard +- ☁️ **Cloud & Local Support**: Works in both cloud-synced and offline modes +- 🔄 **Experiment Comparison**: Compare multiple training runs easily +- 🤝 **Team Collaboration**: Share experiments with team members +- 🎭 **RLHF Completion Logging**: Automatically log model outputs during DPO/KTO/ORPO/GRPO training for qualitative analysis +- ⚡ **Performance Profiling**: Built-in profiling decorators to measure and optimize training performance +- 🔔 **Lark Notifications**: Send real-time training updates to team chat (Feishu/Lark integration) + +## Installation + +```bash +pip install swanlab +``` + +## Quick Start + +### 1. Register for SwanLab (Optional for cloud mode) + +If you want to use cloud sync features, register at [https://swanlab.cn](https://swanlab.cn) to get your API key. + +### 2. Configure Axolotl Config File + +Add SwanLab configuration to your Axolotl YAML config: + +```yaml +# Enable SwanLab plugin +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +# SwanLab configuration +use_swanlab: true +swanlab_project: my-llm-project +swanlab_experiment_name: qwen-finetune-v1 +swanlab_mode: cloud # Options: cloud, local, offline, disabled +swanlab_workspace: my-team # Optional: organization name +swanlab_api_key: YOUR_API_KEY # Optional: can also use env var SWANLAB_API_KEY +``` + +### 3. Run Training + +```bash +# Set API key via environment variable (recommended) +export SWANLAB_API_KEY=your-api-key-here + +# Or login once +swanlab login + +# Run training as usual +accelerate launch -m axolotl.cli.train your-config.yaml +``` + +## Configuration Options + +### Basic Configuration + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `use_swanlab` | bool | `false` | Enable SwanLab tracking | +| `swanlab_project` | str | `None` | Project name (required) | +| `swanlab_experiment_name` | str | `None` | Experiment name | +| `swanlab_description` | str | `None` | Experiment description | +| `swanlab_mode` | str | `cloud` | Sync mode: `cloud`, `local`, `offline`, `disabled` | + +### Advanced Configuration + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `swanlab_workspace` | str | `None` | Workspace/organization name | +| `swanlab_api_key` | str | `None` | API key (prefer env var) | +| `swanlab_web_host` | str | `None` | Private deployment web host | +| `swanlab_api_host` | str | `None` | Private deployment API host | +| `swanlab_log_model` | bool | `false` | Log model checkpoints (coming soon) | +| `swanlab_lark_webhook_url` | str | `None` | Lark (Feishu) webhook URL for team notifications | +| `swanlab_lark_secret` | str | `None` | Lark webhook HMAC secret for authentication | +| `swanlab_log_completions` | bool | `true` | Enable RLHF completion table logging (DPO/KTO/ORPO/GRPO) | +| `swanlab_completion_log_interval` | int | `100` | Steps between completion logging | +| `swanlab_completion_max_buffer` | int | `128` | Max completions to buffer (memory bound) | + +## Configuration Examples + +### Example 1: Basic Cloud Sync + +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +use_swanlab: true +swanlab_project: llama-finetune +swanlab_experiment_name: llama-3-8b-instruct-v1 +swanlab_mode: cloud +``` + +### Example 2: Offline/Local Mode + +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +use_swanlab: true +swanlab_project: local-experiments +swanlab_experiment_name: test-run-1 +swanlab_mode: local # or 'offline' +``` + +### Example 3: Team Workspace + +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +use_swanlab: true +swanlab_project: research-project +swanlab_experiment_name: experiment-42 +swanlab_workspace: my-research-team +swanlab_mode: cloud +``` + +### Example 4: Private Deployment + +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +use_swanlab: true +swanlab_project: internal-project +swanlab_experiment_name: secure-training +swanlab_mode: cloud +swanlab_web_host: https://swanlab.yourcompany.com +swanlab_api_host: https://api.swanlab.yourcompany.com +``` + +## Team Notifications with Lark (Feishu) + +SwanLab supports sending real-time training notifications to your team chat via Lark (Feishu), ByteDance's enterprise collaboration platform. This is especially useful for: +- **Production training monitoring**: Get alerts when training starts, completes, or encounters errors +- **Team collaboration**: Keep your ML team informed about long-running experiments +- **Multi-timezone teams**: Team members can check training progress without being online + +### Prerequisites + +1. **Lark Bot Setup**: Create a custom bot in your Lark group chat +2. **Webhook URL**: Get the webhook URL from your Lark bot settings +3. **HMAC Secret** (recommended): Enable signature verification in your Lark bot for security + +For detailed Lark bot setup instructions, see [Lark Custom Bot Documentation](https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN). + +### Example 5: Basic Lark Notifications + +Send training notifications to a Lark group chat: + +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +use_swanlab: true +swanlab_project: production-training +swanlab_experiment_name: llama-3-finetune-v2 +swanlab_mode: cloud + +# Lark notification (basic, no HMAC verification) +swanlab_lark_webhook_url: https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx +``` + +**Note**: This configuration will work, but you'll see a security warning recommending HMAC secret configuration. + +### Example 6: Lark Notifications with HMAC Security (Recommended) + +For production use, enable HMAC signature verification: + +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +use_swanlab: true +swanlab_project: production-training +swanlab_experiment_name: llama-3-finetune-v2 +swanlab_mode: cloud + +# Lark notification with HMAC authentication +swanlab_lark_webhook_url: https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx +swanlab_lark_secret: your-webhook-secret-key +``` + +**Why HMAC secret matters**: +- Prevents unauthorized parties from sending fake notifications to your Lark group +- Ensures notifications genuinely come from your training jobs +- Required for production deployments with sensitive training data + +### Example 7: Team Workspace + Lark Notifications + +Combine team workspace collaboration with Lark notifications: + +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +use_swanlab: true +swanlab_project: research-project +swanlab_experiment_name: multimodal-experiment-42 +swanlab_workspace: ml-research-team +swanlab_mode: cloud + +# Notify team via Lark when training starts/completes +swanlab_lark_webhook_url: https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx +swanlab_lark_secret: your-webhook-secret-key +``` + +### What Notifications Are Sent? + +SwanLab's Lark integration sends notifications for key training events: +- **Training Start**: When your experiment begins +- **Training Complete**: When training finishes successfully +- **Training Errors**: If training crashes or encounters critical errors +- **Metric Milestones**: Configurable alerts for metric thresholds (if configured in SwanLab) + +Each notification includes: +- Experiment name and project +- Training status +- Key metrics (loss, learning rate) +- Direct link to SwanLab dashboard + +### Lark Configuration Validation + +The plugin validates your Lark configuration at startup: + +#### ✅ Valid Configurations + +```yaml +# Option 1: No Lark (default) +use_swanlab: true +swanlab_project: my-project +# No swanlab_lark_webhook_url → Lark disabled, no warnings + +# Option 2: Lark with HMAC secret (recommended) +use_swanlab: true +swanlab_project: my-project +swanlab_lark_webhook_url: https://open.feishu.cn/open-apis/bot/v2/hook/xxx +swanlab_lark_secret: your-secret +# ✅ Logs: "Registered Lark notification callback with HMAC authentication" + +# Option 3: Lark without secret (works but not recommended) +use_swanlab: true +swanlab_project: my-project +swanlab_lark_webhook_url: https://open.feishu.cn/open-apis/bot/v2/hook/xxx +# ⚠️ Logs: "Registered Lark notification callback (no HMAC secret)" +# ⚠️ Warning: "Lark webhook has no secret configured. For production use, set 'swanlab_lark_secret'..." +``` + +### Security Best Practices + +1. **Always use HMAC secret in production**: + ```yaml + swanlab_lark_webhook_url: https://open.feishu.cn/... + swanlab_lark_secret: your-secret-key # ✅ Add this! + ``` + +2. **Store secrets in environment variables** (even better): + ```yaml + # In your training script/environment + export SWANLAB_LARK_WEBHOOK_URL="https://open.feishu.cn/..." + export SWANLAB_LARK_SECRET="your-secret-key" + ``` + + Then in config: + ```yaml + # SwanLab plugin will auto-detect environment variables + use_swanlab: true + swanlab_project: my-project + # Lark URL and secret read from env vars + ``` + +3. **Rotate webhook secrets periodically**: Update your Lark bot's secret every 90 days + +4. **Use separate webhooks for dev/prod**: Don't mix development and production notifications + +### Distributed Training + +Lark notifications are automatically deduplicated in distributed training: +- Only **rank 0** sends notifications +- Other GPU ranks skip Lark registration +- Prevents duplicate messages in multi-GPU training + +```bash +# Running on 4 GPUs +torchrun --nproc_per_node=4 -m axolotl.cli.train config.yml + +# Expected logs: +# [Rank 0] Registered Lark notification callback with HMAC authentication +# [Rank 1-3] (no Lark registration messages) +``` + +## RLHF Completion Table Logging + +For RLHF (Reinforcement Learning from Human Feedback) training methods like DPO, KTO, ORPO, and GRPO, SwanLab can log model completions (prompts, chosen/rejected responses, rewards) to a visual table for qualitative analysis. This helps you: + +- **Inspect model behavior**: See actual model outputs during training +- **Debug preference learning**: Compare chosen vs rejected responses +- **Track reward patterns**: Monitor how rewards evolve over training +- **Share examples with team**: Visual tables in SwanLab dashboard + +### Features + +- ✅ **Automatic detection**: Works with DPO, KTO, ORPO, GRPO trainers +- ✅ **Memory-safe buffering**: Bounded buffer prevents memory leaks in long training runs +- ✅ **Periodic logging**: Configurable logging interval to reduce overhead +- ✅ **Rich visualization**: SwanLab tables show prompts, responses, and metrics side-by-side + +### Configuration + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `swanlab_log_completions` | bool | `true` | Enable completion logging for RLHF trainers | +| `swanlab_completion_log_interval` | int | `100` | Log completions to SwanLab every N training steps | +| `swanlab_completion_max_buffer` | int | `128` | Maximum completions to buffer (memory bound) | + +### Example: DPO Training with Completion Logging + +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +use_swanlab: true +swanlab_project: dpo-training +swanlab_experiment_name: llama-3-dpo-v1 +swanlab_mode: cloud + +# RLHF completion logging (enabled by default) +swanlab_log_completions: true +swanlab_completion_log_interval: 100 # Log every 100 steps +swanlab_completion_max_buffer: 128 # Keep last 128 completions + +# DPO-specific config +rl: dpo +datasets: + - path: /path/to/preference_dataset + type: chatml.intel +``` + +### Example: Disable Completion Logging + +If you're doing a quick test run or don't need completion tables: + +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +use_swanlab: true +swanlab_project: dpo-training + +# Disable completion logging +swanlab_log_completions: false +``` + +### Supported RLHF Trainers + +The completion logging callback automatically activates for these trainer types: + +- **DPO (Direct Preference Optimization)**: Logs prompts, chosen, rejected, reward_diff +- **KTO (Kahneman-Tversky Optimization)**: Logs prompts, completions, labels, rewards +- **ORPO (Odds Ratio Preference Optimization)**: Logs prompts, chosen, rejected, log_odds_ratio +- **GRPO (Group Relative Policy Optimization)**: Logs prompts, completions, rewards, advantages +- **CPO (Constrained Policy Optimization)**: Logs prompts, chosen, rejected + +For non-RLHF trainers (standard supervised fine-tuning), the completion callback is automatically skipped. + +### How It Works + +1. **Auto-detection**: Plugin detects trainer type at initialization +2. **Buffering**: Completions are buffered in memory (up to `swanlab_completion_max_buffer`) +3. **Periodic logging**: Every `swanlab_completion_log_interval` steps, buffer is logged to SwanLab +4. **Memory safety**: Old completions are automatically dropped when buffer is full (uses `collections.deque`) +5. **Final flush**: Remaining completions are logged when training completes + +### Viewing Completion Tables + +After training starts, you can view completion tables in your SwanLab dashboard: + +1. Navigate to your experiment in SwanLab +2. Look for the "rlhf_completions" table in the metrics panel +3. The table shows: + - **step**: Training step when completion was generated + - **prompt**: Input prompt + - **chosen**: Preferred response (DPO/ORPO) + - **rejected**: Non-preferred response (DPO/ORPO) + - **completion**: Model output (KTO/GRPO) + - **reward_diff/reward**: Reward metrics + - Trainer-specific metrics (e.g., log_odds_ratio for ORPO) + +### Memory Management + +The completion buffer is **memory-bounded** to prevent memory leaks: + +```python +# Internal implementation uses deque with maxlen +from collections import deque + +buffer = deque(maxlen=128) # Old completions automatically dropped +``` + +**Memory usage estimate**: +- Average completion: ~500 characters (prompt + responses) +- Buffer size 128: ~64 KB (negligible) +- Buffer size 1024: ~512 KB (still small) + +**Recommendation**: Default buffer size (128) works well for most cases. Increase to 512-1024 only if you need to review more historical completions. + +### Performance Impact + +Completion logging has minimal overhead: + +- **Buffering**: O(1) append operation, negligible CPU/memory +- **Logging**: Only happens every N steps (default: 100) +- **Network**: SwanLab batches table uploads efficiently + +**Expected overhead**: < 0.5% per training step + +### Troubleshooting + +#### Completions not appearing in SwanLab + +**Cause**: Trainer may not be logging completion data in the expected format. + +**Diagnostic steps**: +1. Check trainer type detection in logs: + ```text + INFO: SwanLab RLHF completion logging enabled for DPOTrainer (type: dpo) + ``` +2. Verify your trainer is an RLHF trainer (DPO/KTO/ORPO/GRPO) +3. Check if trainer logs completion data (this depends on TRL version) + +**Note**: The current implementation expects trainers to log completion data in the `logs` dict during `on_log()` callback. Some TRL trainers may not expose this data by default. You may need to patch the trainer to expose completions. + +#### Buffer fills up too quickly + +**Cause**: High logging frequency with small buffer size. + +**Solution**: Increase buffer size or logging interval: +```yaml +swanlab_completion_log_interval: 200 # Log less frequently +swanlab_completion_max_buffer: 512 # Larger buffer +``` + +#### Memory usage growing over time + +**Cause**: Buffer should be bounded, so this indicates a bug. + +**Solution**: +1. Verify `swanlab_completion_max_buffer` is set +2. Check SwanLab version is up to date +3. Report issue with memory profiling data + +## Performance Profiling + +SwanLab integration includes profiling utilities to measure and log execution time of trainer methods. This helps you: + +- **Identify bottlenecks**: Find slow operations in your training loop +- **Optimize performance**: Track improvements after optimization changes +- **Monitor distributed training**: See per-rank timing differences +- **Debug hangs**: Detect methods that take unexpectedly long + +### Features + +- ✅ **Zero-config profiling**: Automatic timing of key trainer methods +- ✅ **Decorator-based**: Easy to add profiling to custom methods with `@swanlab_profile` +- ✅ **Context manager**: Fine-grained profiling with `swanlab_profiling_context()` +- ✅ **Advanced filtering**: `ProfilingConfig` for throttling and minimum duration thresholds +- ✅ **Exception-safe**: Logs duration even if function raises an exception + +### Basic Usage: Decorator + +Add profiling to any trainer method with the `@swanlab_profile` decorator: + +```python +from axolotl.integrations.swanlab.profiling import swanlab_profile + +class MyCustomTrainer(AxolotlTrainer): + @swanlab_profile + def training_step(self, model, inputs): + # Your training step logic + return super().training_step(model, inputs) + + @swanlab_profile + def prediction_step(self, model, inputs, prediction_loss_only): + # Your prediction logic + return super().prediction_step(model, inputs, prediction_loss_only) +``` + +The decorator automatically: +1. Measures execution time with high-precision timer +2. Logs to SwanLab as `profiling/Time taken: ClassName.method_name` +3. Only logs if SwanLab is enabled (`use_swanlab: true`) +4. Gracefully handles exceptions (logs duration, then re-raises) + +### Advanced Usage: Context Manager + +For fine-grained profiling within a method: + +```python +from axolotl.integrations.swanlab.profiling import swanlab_profiling_context + +class MyTrainer(AxolotlTrainer): + def complex_training_step(self, model, inputs): + # Profile just the forward pass + with swanlab_profiling_context(self, "forward_pass"): + outputs = model(**inputs) + + # Profile just the backward pass + with swanlab_profiling_context(self, "backward_pass"): + loss = outputs.loss + loss.backward() + + return outputs +``` + +### Advanced Usage: ProfilingConfig + +Filter and throttle profiling logs with `ProfilingConfig`: + +```python +from axolotl.integrations.swanlab.profiling import ( + swanlab_profiling_context_advanced, + ProfilingConfig, +) + +# Create custom profiling config +profiling_config = ProfilingConfig( + enabled=True, + min_duration_ms=1.0, # Only log if duration > 1ms + log_interval=10, # Log every 10th call +) + +class MyTrainer(AxolotlTrainer): + def frequently_called_method(self, data): + with swanlab_profiling_context_advanced( + self, + "frequent_op", + config=profiling_config + ): + # This only logs every 10th call, and only if it takes > 1ms + result = expensive_computation(data) + return result +``` + +**ProfilingConfig Parameters**: +- `enabled`: Enable/disable profiling globally (default: `True`) +- `min_duration_ms`: Minimum duration to log in milliseconds (default: `0.1`) +- `log_interval`: Log every Nth function call (default: `1` = log all) + +**Use cases**: +- **High-frequency methods**: Use `log_interval=100` to reduce logging overhead +- **Filter noise**: Use `min_duration_ms=1.0` to skip very fast operations +- **Debugging**: Use `log_interval=1, min_duration_ms=0.0` to log everything + +### Viewing Profiling Metrics + +In your SwanLab dashboard, profiling metrics appear under the "profiling" namespace: + +```text +profiling/Time taken: AxolotlTrainer.training_step +profiling/Time taken: AxolotlTrainer.prediction_step +profiling/Time taken: MyTrainer.forward_pass +profiling/Time taken: MyTrainer.backward_pass +``` + +You can: +- **Track over time**: See if methods get faster/slower during training +- **Compare runs**: Compare profiling metrics across experiments +- **Identify regressions**: Detect if a code change slowed down training + +### Configuration in Axolotl Config + +Profiling is automatically enabled when SwanLab is enabled. No additional config needed: + +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +use_swanlab: true +swanlab_project: my-project + +# Profiling is automatically enabled +# Add @swanlab_profile decorators to your custom trainer methods +``` + +To disable profiling while keeping SwanLab enabled: + +```python +# In your custom trainer code +from axolotl.integrations.swanlab.profiling import DEFAULT_PROFILING_CONFIG + +# Disable profiling globally +DEFAULT_PROFILING_CONFIG.enabled = False +``` + +### Performance Impact + +- **Decorator overhead**: ~2-5 microseconds per call (negligible) +- **Context manager overhead**: ~1-3 microseconds (negligible) +- **Logging overhead**: Only when SwanLab is enabled and method duration exceeds threshold +- **Network overhead**: SwanLab batches metrics efficiently + +**Expected overhead**: < 0.1% per training step (effectively zero) + +### Best Practices + +1. **Profile bottlenecks first**: Start by profiling suspected slow operations +2. **Use min_duration_ms**: Filter out fast operations (< 1ms) to reduce noise +3. **Throttle high-frequency calls**: Use `log_interval` for methods called > 100 times/step +4. **Profile across runs**: Compare profiling metrics before/after optimization +5. **Monitor distributed training**: Check for rank-specific slowdowns + +### Example: Complete Profiling Setup + +```python +from axolotl.integrations.swanlab.profiling import ( + swanlab_profile, + swanlab_profiling_context, + ProfilingConfig, +) + +class OptimizedTrainer(AxolotlTrainer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Custom profiling config for high-frequency operations + self.fast_op_config = ProfilingConfig( + enabled=True, + min_duration_ms=0.5, + log_interval=50, + ) + + @swanlab_profile + def training_step(self, model, inputs): + """Main training step - always profile.""" + return super().training_step(model, inputs) + + @swanlab_profile + def compute_loss(self, model, inputs, return_outputs=False): + """Loss computation - always profile.""" + return super().compute_loss(model, inputs, return_outputs) + + def _prepare_inputs(self, inputs): + """High-frequency operation - throttled profiling.""" + with swanlab_profiling_context_advanced( + self, + "prepare_inputs", + config=self.fast_op_config, + ): + return super()._prepare_inputs(inputs) +``` + +### Troubleshooting + +#### Profiling metrics not appearing in SwanLab + +**Cause**: SwanLab is not enabled or not initialized. + +**Solution**: +```yaml +# Ensure SwanLab is enabled +use_swanlab: true +swanlab_project: my-project +``` + +Check logs for: +```text +INFO: SwanLab initialized for project: my-project +``` + +#### Too many profiling metrics cluttering dashboard + +**Cause**: Profiling every function call for high-frequency operations. + +**Solution**: Use `ProfilingConfig` with throttling: +```python +config = ProfilingConfig( + min_duration_ms=1.0, # Skip fast ops + log_interval=100, # Log every 100th call +) +``` + +#### Profiling overhead impacting training speed + +**Cause**: Profiling itself should have negligible overhead (< 0.1%). If you see > 1% slowdown, this indicates a bug. + +**Solution**: +1. Disable profiling temporarily to confirm: + ```python + DEFAULT_PROFILING_CONFIG.enabled = False + ``` +2. Report issue with profiling data and trainer details + +#### Profiling shows inconsistent timing + +**Cause**: Normal variation due to GPU warmup, data loading, or system load. + +**Solution**: +- Ignore first few steps (warmup period) +- Look at average/median timing over many steps +- Use `log_interval` to reduce noise from individual outliers + +## Complete Config Example + +Here's a complete example integrating SwanLab with your RVQ-Alpha training: + +```yaml +base_model: /path/to/your/model +model_type: Qwen2ForCausalLM + +# SwanLab Integration +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +use_swanlab: true +swanlab_project: RVQ-Alpha-Training +swanlab_experiment_name: Qwen2.5-7B-MetaQA-Perturb-P020 +swanlab_description: "Training on MetaQA and Perturbation datasets with NEW-RVQ encoding" +swanlab_mode: cloud +swanlab_workspace: single-cell-genomics + +# Training configuration +sequence_len: 32768 +micro_batch_size: 1 +gradient_accumulation_steps: 1 +num_epochs: 2 +learning_rate: 2e-5 +optimizer: adamw_torch_fused + +# Datasets +datasets: + - path: /path/to/dataset + type: chat_template + +# Output +output_dir: ./outputs +``` + +## Modes Explained + +### `cloud` Mode (Default) +- Syncs experiments to SwanLab cloud in real-time +- Requires API key and internet connection +- Best for: Team collaboration, remote monitoring + +### `local` Mode +- Saves experiments locally only +- No cloud sync +- Best for: Local development, air-gapped environments + +### `offline` Mode +- Saves metadata locally +- Can sync to cloud later using `swanlab sync` +- Best for: Unstable internet, sync later + +### `disabled` Mode +- Turns off SwanLab completely +- No logging or tracking +- Best for: Debugging, testing + +## Configuration Validation & Conflict Detection + +SwanLab integration includes comprehensive validation and conflict detection to help you catch configuration errors early and avoid performance issues. + +### Required Fields Validation + +The plugin validates your configuration at startup and provides clear error messages with solutions: + +#### Missing Project Name + +```yaml +# ❌ INVALID: use_swanlab enabled but no project +use_swanlab: true +# Error: SwanLab enabled but 'swanlab_project' is not set. +``` + +**Solution**: +```yaml +# ✅ VALID: Provide project name +use_swanlab: true +swanlab_project: my-project +``` + +#### Invalid Mode + +```yaml +# ❌ INVALID: Unknown mode +use_swanlab: true +swanlab_project: my-project +swanlab_mode: invalid-mode +# Error: Invalid swanlab_mode: 'invalid-mode'. Valid options: cloud, local, offline, disabled +``` + +**Solution**: +```yaml +# ✅ VALID: Use one of the valid modes +use_swanlab: true +swanlab_project: my-project +swanlab_mode: cloud # or: local, offline, disabled +``` + +#### Empty Project Name + +```yaml +# ❌ INVALID: Empty string project name +use_swanlab: true +swanlab_project: "" +# Error: swanlab_project cannot be an empty string. +``` + +**Solution**: +```yaml +# ✅ VALID: Provide non-empty project name +use_swanlab: true +swanlab_project: my-project +``` + +### Cloud Mode API Key Warning + +When using `cloud` mode without an API key, you'll receive a warning with multiple solutions: + +```yaml +use_swanlab: true +swanlab_project: my-project +swanlab_mode: cloud +# No API key set +# Warning: SwanLab cloud mode enabled but no API key found. +``` + +**Solutions**: +1. Set environment variable: `export SWANLAB_API_KEY=your-api-key` +2. Add to config (less secure): `swanlab_api_key: your-api-key` +3. Run `swanlab login` before training +4. Use `swanlab_mode: local` for offline tracking + +### Multi-Logger Performance Warnings + +Using multiple logging tools simultaneously (SwanLab + WandB + MLflow + Comet) can impact training performance: + +#### Two Loggers - Warning + +```yaml +use_swanlab: true +swanlab_project: my-project + +use_wandb: true +wandb_project: my-project + +# Warning: Multiple logging tools enabled: SwanLab, WandB +# Expected overhead: ~3.0% per training step. +``` + +**Impact**: +- Performance overhead: ~1-2% per logger (cumulative) +- Increased memory usage +- Longer training time per step +- Potential config/callback conflicts + +**Recommendations**: +- Choose ONE primary logging tool for production training +- Use multiple loggers only for: + - Migration period (transitioning between tools) + - Short comparison runs + - Debugging specific tool issues +- Monitor system resources (CPU, memory) during training + +#### Three+ Loggers - Error-Level Warning + +```yaml +use_swanlab: true +swanlab_project: my-project + +use_wandb: true +wandb_project: my-project + +use_mlflow: true +mlflow_tracking_uri: http://localhost:5000 + +# ERROR: 3 logging tools enabled simultaneously! +# Expected overhead: ~4.5% per training step. +# STRONGLY RECOMMEND: Disable all but ONE logging tool +``` + +**Why This Matters**: +- With 3 loggers: ~4-5% overhead per step → significant slowdown over long training +- Example: 10,000 steps at 2s/step → ~400-500 seconds extra (6-8 minutes) +- Memory overhead scales with number of loggers +- Rare edge cases with callback ordering conflicts + +### Auto-Enable Logic + +For convenience, SwanLab will auto-enable if you specify a project without setting `use_swanlab`: + +```yaml +# This configuration: +swanlab_project: my-project + +# Automatically becomes: +use_swanlab: true +swanlab_project: my-project +``` + +### Distributed Training Detection + +In distributed training scenarios (multi-GPU), the plugin automatically detects and reports: + +```yaml +use_swanlab: true +swanlab_project: my-project +swanlab_mode: cloud + +# When running with torchrun --nproc_per_node=4: +# Info: Distributed training detected (world_size=4) +# Info: SwanLab mode: cloud +# Info: Only rank 0 will initialize SwanLab +# Info: Other ranks will skip SwanLab to avoid conflicts +``` + +**Why Only Rank 0**: +- Avoids duplicate experiment runs +- Reduces network/cloud API overhead on worker ranks +- Prevents race conditions in metric logging + +## Authentication + +### Method 1: Environment Variable (Recommended) +```bash +export SWANLAB_API_KEY=your-api-key-here +``` + +### Method 2: Login Command +```bash +swanlab login +# Enter your API key when prompted +``` + +### Method 3: Config File +```yaml +swanlab_api_key: your-api-key-here +``` + +## What Gets Logged? + +### Automatically Logged Metrics +- Training loss +- Learning rate +- Gradient norm +- Training steps +- Epoch progress + +### Automatically Logged Config +- Model configuration (base_model, model_type) +- Training hyperparameters (learning_rate, batch_size, etc.) +- Optimizer settings +- Parallelization settings (FSDP, DeepSpeed, Context Parallel) +- Axolotl configuration file +- DeepSpeed configuration (if used) + +## Viewing Your Experiments + +### Cloud Mode +Visit [https://swanlab.cn](https://swanlab.cn) and navigate to your project to view: +- Real-time training metrics +- Hyperparameter comparison +- System resource usage +- Configuration files + +### Local Mode +```bash +# Start local dashboard +swanlab watch ./swanlog + +# Open browser to http://localhost:5092 +``` + +## Integration with Existing Tools + +SwanLab can work alongside other tracking tools: + +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + +# Use both SwanLab and Wandb +use_swanlab: true +swanlab_project: my-project + +use_wandb: true +wandb_project: my-project +``` + +## Troubleshooting + +### Configuration Errors + +#### Error: "SwanLab enabled but 'swanlab_project' is not set" + +**Cause**: You enabled SwanLab (`use_swanlab: true`) but forgot to specify a project name. + +**Solution**: +```yaml +use_swanlab: true +swanlab_project: my-project # Add this line +``` + +#### Error: "Invalid swanlab_mode: 'xxx'" + +**Cause**: You provided an invalid mode value. + +**Solution**: Use one of the valid modes: +```yaml +swanlab_mode: cloud # or: local, offline, disabled +``` + +#### Error: "swanlab_project cannot be an empty string" + +**Cause**: You set `swanlab_project: ""` (empty string). + +**Solution**: Either provide a valid name or remove the field: +```yaml +# Option 1: Provide valid name +swanlab_project: my-project + +# Option 2: Remove the field entirely +# swanlab_project: "" <- Remove this line +``` + +### Import Errors + +#### Error: "SwanLab is not installed" + +**Cause**: SwanLab package is not installed in your environment. + +**Solution**: +```bash +pip install swanlab +# or +pip install swanlab>=0.3.0 +``` + +### Performance Issues + +#### Warning: "Multiple logging tools enabled" + +**Cause**: You have multiple experiment tracking tools enabled (e.g., SwanLab + WandB + MLflow). + +**Impact**: ~1-2% performance overhead per logger, cumulative. + +**Solution**: For production training, disable all but one logger: +```yaml +# Option 1: Keep only SwanLab +use_swanlab: true +swanlab_project: my-project +use_wandb: false # Disable others +use_mlflow: false + +# Option 2: Keep only WandB +use_swanlab: false +use_wandb: true +wandb_project: my-project +``` + +**Exception**: Multiple loggers are acceptable for: +- Short comparison runs (< 100 steps) +- Migration testing between logging tools +- Debugging logger-specific issues + +### Distributed Training Issues + +#### SwanLab creates duplicate runs in multi-GPU training + +**Cause**: All ranks are initializing SwanLab instead of just rank 0. + +**Expected Behavior**: The plugin automatically ensures only rank 0 initializes SwanLab. You should see: +```text +Info: Distributed training detected (world_size=4) +Info: Only rank 0 will initialize SwanLab +Info: Other ranks will skip SwanLab to avoid conflicts +``` + +**If you see duplicates**: +1. Check your plugin is loaded correctly +2. Verify you're using the latest SwanLab integration code +3. Check logs for initialization messages on all ranks + +### SwanLab not logging metrics + +**Solution**: Ensure SwanLab is initialized before training starts. The plugin automatically handles this in `pre_model_load`. + +### API Key errors + +**Solution**: +```bash +# Verify API key +echo $SWANLAB_API_KEY + +# Re-login +swanlab login +``` + +### Cloud sync issues + +**Solution**: Use `offline` mode and sync later: +```yaml +swanlab_mode: offline +``` + +Then sync when ready: +```bash +swanlab sync ./swanlog +``` + +### Plugin not loaded + +**Solution**: Verify plugin path in config: +```yaml +plugins: + - axolotl.integrations.swanlab.SwanLabPlugin # Correct path +``` + +### Lark Notification Issues + +#### Error: "Failed to import SwanLab Lark plugin" + +**Cause**: Your SwanLab version doesn't include the Lark plugin (requires SwanLab >= 0.3.0). + +**Solution**: +```bash +# Upgrade SwanLab to latest version +pip install --upgrade swanlab + +# Or install specific version +pip install 'swanlab>=0.3.0' +``` + +#### Warning: "Lark webhook has no secret configured" + +**Cause**: You provided `swanlab_lark_webhook_url` but no `swanlab_lark_secret`. + +**Impact**: Lark notifications will work, but without HMAC authentication (security risk). + +**Solution**: Add HMAC secret for production use: +```yaml +swanlab_lark_webhook_url: https://open.feishu.cn/open-apis/bot/v2/hook/xxx +swanlab_lark_secret: your-webhook-secret # Add this line +``` + +**When it's OK to skip secret**: +- Local development and testing +- Internal networks with restricted access +- Non-sensitive training experiments + +**When secret is required**: +- Production training jobs +- Training with proprietary data +- Multi-team shared Lark groups + +#### Error: "Failed to register Lark callback" + +**Cause**: Invalid webhook URL or network connectivity issues. + +**Diagnostic steps**: +```bash +# 1. Test webhook URL manually +curl -X POST "YOUR_WEBHOOK_URL" \ + -H 'Content-Type: application/json' \ + -d '{"msg_type":"text","content":{"text":"Test from Axolotl"}}' + +# 2. Check SwanLab version +pip show swanlab + +# 3. Verify webhook URL format +# Should start with: https://open.feishu.cn/open-apis/bot/v2/hook/ +``` + +**Solution**: +1. Verify webhook URL is correct (copy from Lark bot settings) +2. Check network connectivity to Lark API +3. Ensure webhook is not expired (Lark webhooks can expire) +4. Regenerate webhook URL in Lark bot settings if needed + +#### Lark notifications not received + +**Cause**: Multiple possible causes. + +**Diagnostic checklist**: + +1. **Check training logs** for Lark registration confirmation: + ```text + # Expected log message (rank 0 only): + INFO: Registered Lark notification callback with HMAC authentication + ``` + +2. **Verify webhook in Lark**: Test webhook manually (see above) + +3. **Check distributed training**: Only rank 0 sends notifications + ```bash + # If running multi-GPU, check rank 0 logs specifically + grep "Registered Lark" logs/rank_0.log + ``` + +4. **Verify SwanLab is initialized**: Lark callback needs SwanLab to be running + ```yaml + use_swanlab: true # Must be enabled + swanlab_project: my-project # Must be set + ``` + +5. **Check Lark bot permissions**: Ensure bot is added to the target group chat + +#### Duplicate Lark notifications in multi-GPU training + +**Expected Behavior**: Should NOT happen - only rank 0 sends notifications. + +**If you see duplicates**: +1. Check that all GPUs are using the same config file +2. Verify plugin is loaded correctly on all ranks +3. Check logs for unexpected Lark initialization on non-zero ranks +4. Ensure `RANK` or `LOCAL_RANK` environment variables are set correctly + +**Solution**: This is a bug if it occurs. Report with: +- Full training command +- Logs from all ranks +- Config file + +## Comparison: SwanLab vs WandB + +| Feature | SwanLab | WandB | +|---------|---------|-------| +| Open Source | ✅ Yes | ❌ No | +| Self-Hosting | ✅ Easy | ⚠️ Complex | +| Free Tier | ✅ Generous | ⚠️ Limited | +| Chinese Support | ✅ Native | ⚠️ Limited | +| Offline Mode | ✅ Full support | ✅ Supported | +| Integration | 🆕 New | ✅ Mature | + +## Advanced Usage + +### Custom Logging + +You can add custom metrics in your callbacks: + +```python +import swanlab + +# In your custom callback +swanlab.log({ + "custom_metric": value, + "epoch": epoch_num +}) +``` + +### Experiment Comparison + +```bash +# Compare multiple experiments +swanlab compare run1 run2 run3 +``` + +## Support + +- **Documentation**: [https://docs.swanlab.cn](https://docs.swanlab.cn) +- **GitHub**: [https://github.com/SwanHubX/SwanLab](https://github.com/SwanHubX/SwanLab) +- **Issues**: Report bugs at [GitHub Issues](https://github.com/SwanHubX/SwanLab/issues) + +## License + +This integration follows the Axolotl Community License Agreement. + +## Acknowledgements + +This integration is built on top of: +- [SwanLab](https://github.com/SwanHubX/SwanLab) - Experiment tracking tool +- [Transformers](https://github.com/huggingface/transformers) - SwanLabCallback +- [Axolotl](https://github.com/axolotl-ai-cloud/axolotl) - Training framework diff --git a/src/axolotl/integrations/swanlab/__init__.py b/src/axolotl/integrations/swanlab/__init__.py new file mode 100644 index 0000000000..241a27764f --- /dev/null +++ b/src/axolotl/integrations/swanlab/__init__.py @@ -0,0 +1,6 @@ +"""SwanLab integration plugin for Axolotl""" + +from axolotl.integrations.swanlab.args import SwanLabConfig +from axolotl.integrations.swanlab.plugins import SwanLabPlugin + +__all__ = ["SwanLabConfig", "SwanLabPlugin"] diff --git a/src/axolotl/integrations/swanlab/args.py b/src/axolotl/integrations/swanlab/args.py new file mode 100644 index 0000000000..2cf31252dc --- /dev/null +++ b/src/axolotl/integrations/swanlab/args.py @@ -0,0 +1,140 @@ +"""SwanLab configuration arguments""" + +from pydantic import BaseModel, Field, field_validator, model_validator + + +class SwanLabConfig(BaseModel): + """SwanLab configuration subset""" + + use_swanlab: bool | None = Field( + default=True, + json_schema_extra={ + "description": "Enable SwanLab experiment tracking and visualization" + }, + ) + swanlab_project: str | None = Field( + default=None, + json_schema_extra={"description": "Your SwanLab project name"}, + ) + swanlab_experiment_name: str | None = Field( + default=None, + json_schema_extra={"description": "Set the name of your SwanLab experiment"}, + ) + swanlab_description: str | None = Field( + default=None, + json_schema_extra={"description": "Description for your SwanLab experiment"}, + ) + swanlab_mode: str | None = Field( + default=None, + json_schema_extra={ + "description": '"cloud" to sync to SwanLab cloud, "local" for local only, "offline" to save metadata locally, "disabled" to turn off SwanLab' + }, + ) + swanlab_workspace: str | None = Field( + default=None, + json_schema_extra={ + "description": "SwanLab workspace name (organization or username)" + }, + ) + swanlab_api_key: str | None = Field( + default=None, + json_schema_extra={ + "description": "SwanLab API key for authentication. Can also be set via SWANLAB_API_KEY environment variable" + }, + ) + swanlab_log_model: bool | None = Field( + default=False, + json_schema_extra={ + "description": "Whether to log model checkpoints to SwanLab (feature coming soon)" + }, + ) + swanlab_web_host: str | None = Field( + default=None, + json_schema_extra={ + "description": "Web address for SwanLab cloud environment (for private deployment)" + }, + ) + swanlab_api_host: str | None = Field( + default=None, + json_schema_extra={ + "description": "API address for SwanLab cloud environment (for private deployment)" + }, + ) + swanlab_lark_webhook_url: str | None = Field( + default=None, + json_schema_extra={ + "description": "Lark (Feishu) webhook URL for sending training notifications to team chat" + }, + ) + swanlab_lark_secret: str | None = Field( + default=None, + json_schema_extra={ + "description": "Secret for Lark webhook HMAC signature authentication (optional)" + }, + ) + swanlab_log_completions: bool | None = Field( + default=True, + json_schema_extra={ + "description": "Enable logging RLHF completions to SwanLab for qualitative analysis (DPO/KTO/ORPO/GRPO)" + }, + ) + swanlab_completion_log_interval: int | None = Field( + default=100, + json_schema_extra={ + "description": "Number of training steps between completion table logging to SwanLab" + }, + ) + swanlab_completion_max_buffer: int | None = Field( + default=128, + json_schema_extra={ + "description": "Maximum number of completions to buffer before logging (prevents memory leaks)" + }, + ) + + @field_validator("swanlab_mode") + @classmethod + def validate_swanlab_mode(cls, v): + """Validate swanlab_mode is one of the allowed values.""" + if v is None: + return v + + valid_modes = ["cloud", "local", "offline", "disabled"] + if v not in valid_modes: + raise ValueError( + f"Invalid swanlab_mode: '{v}'.\n\n" + f"Valid options: {', '.join(valid_modes)}\n\n" + f"Examples:\n" + f" swanlab_mode: cloud # Sync to SwanLab cloud\n" + f" swanlab_mode: local # Local only, no cloud sync\n" + f" swanlab_mode: offline # Save metadata locally\n" + f" swanlab_mode: disabled # Turn off SwanLab\n" + ) + return v + + @field_validator("swanlab_project") + @classmethod + def validate_swanlab_project(cls, v): + """Validate swanlab_project is non-empty when provided.""" + if v is not None and isinstance(v, str) and len(v.strip()) == 0: + raise ValueError( + "swanlab_project cannot be an empty string.\n\n" + "Either:\n" + " 1. Provide a valid project name: swanlab_project: my-project\n" + " 2. Remove the swanlab_project field entirely\n" + ) + return v + + @model_validator(mode="after") + def validate_swanlab_enabled_requires_project(self): + """Validate that if use_swanlab is True, swanlab_project must be set.""" + if self.use_swanlab is True and not self.swanlab_project: + raise ValueError( + "SwanLab enabled (use_swanlab: true) but 'swanlab_project' is not set.\n\n" + "Solutions:\n" + " 1. Add 'swanlab_project: your-project-name' to your config\n" + " 2. Set 'use_swanlab: false' to disable SwanLab\n\n" + "Example:\n" + " use_swanlab: true\n" + " swanlab_project: my-llm-training\n" + ) + return self diff --git a/src/axolotl/integrations/swanlab/callbacks.py b/src/axolotl/integrations/swanlab/callbacks.py new file mode 100644 index 0000000000..8dfc0fe53c --- /dev/null +++ b/src/axolotl/integrations/swanlab/callbacks.py @@ -0,0 +1,179 @@ +"""SwanLab callbacks for Axolotl trainers. + +This module provides HuggingFace Trainer callbacks for logging +RLHF completions to SwanLab. +""" + +from transformers import ( + TrainerCallback, + TrainerControl, + TrainerState, + TrainingArguments, +) + +from axolotl.integrations.swanlab.completion_logger import CompletionLogger +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +class SwanLabRLHFCompletionCallback(TrainerCallback): + """Callback for logging RLHF completions to SwanLab. + + This callback periodically logs model completions (prompts, chosen/rejected + responses, rewards) to SwanLab during RLHF training for qualitative analysis. + + Supports DPO, KTO, ORPO, and GRPO trainers. + + Example usage: + >>> callback = SwanLabRLHFCompletionCallback( + ... log_interval=100, # Log every 100 steps + ... max_completions=128, # Keep last 128 completions + ... ) + >>> trainer.add_callback(callback) + + Attributes: + logger: CompletionLogger instance + log_interval: Number of steps between SwanLab logging + trainer_type: Auto-detected trainer type (dpo/kto/orpo/grpo) + """ + + def __init__( + self, + log_interval: int = 100, + max_completions: int = 128, + table_name: str = "rlhf_completions", + ): + """Initialize SwanLab RLHF completion callback. + + Args: + log_interval: Log to SwanLab every N steps. Default: 100 + max_completions: Maximum completions to buffer. Default: 128 + table_name: SwanLab table name. Default: "rlhf_completions" + """ + super().__init__() + self.logger = CompletionLogger(maxlen=max_completions) + self.log_interval = log_interval + self.table_name = table_name + self.trainer_type: str | None = None # Auto-detected + self._last_logged_step = 0 + + def on_init_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Detect trainer type on initialization.""" + trainer = kwargs.get("trainer") + if trainer is not None: + trainer_name = trainer.__class__.__name__ + if "DPO" in trainer_name: + self.trainer_type = "dpo" + elif "KTO" in trainer_name: + self.trainer_type = "kto" + elif "ORPO" in trainer_name: + self.trainer_type = "orpo" + elif "GRPO" in trainer_name: + self.trainer_type = "grpo" + else: + self.trainer_type = "unknown" + + LOG.info( + f"SwanLab RLHF completion logging enabled for {trainer_name} " + f"(type: {self.trainer_type})" + ) + + def on_log( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + logs: dict | None = None, + **kwargs, + ): + """Capture completions from logs and buffer them. + + Different trainers log completions in different formats: + - DPO: logs['dpo/chosen'], logs['dpo/rejected'], logs['dpo/reward_diff'] + - KTO: logs['kto/completion'], logs['kto/label'], logs['kto/reward'] + - ORPO: logs['orpo/chosen'], logs['orpo/rejected'] + - GRPO: logs['grpo/completion'], logs['grpo/reward'] + + Note: This is a placeholder implementation. Actual log keys depend + on the TRL trainer implementation. You may need to patch the trainers + to expose completion data in logs. + """ + if logs is None or self.trainer_type is None: + return + + step = state.global_step + + # DPO completions + if self.trainer_type == "dpo": + if all(key in logs for key in ["dpo/prompt", "dpo/chosen", "dpo/rejected"]): + self.logger.add_dpo_completion( + step=step, + prompt=logs.get("dpo/prompt", ""), + chosen=logs.get("dpo/chosen", ""), + rejected=logs.get("dpo/rejected", ""), + reward_diff=logs.get("dpo/reward_diff"), + ) + + # KTO completions + elif self.trainer_type == "kto": + if all(key in logs for key in ["kto/prompt", "kto/completion"]): + self.logger.add_kto_completion( + step=step, + prompt=logs.get("kto/prompt", ""), + completion=logs.get("kto/completion", ""), + label=logs.get("kto/label", False), + reward=logs.get("kto/reward"), + ) + + # ORPO completions + elif self.trainer_type == "orpo": + if all( + key in logs for key in ["orpo/prompt", "orpo/chosen", "orpo/rejected"] + ): + self.logger.add_orpo_completion( + step=step, + prompt=logs.get("orpo/prompt", ""), + chosen=logs.get("orpo/chosen", ""), + rejected=logs.get("orpo/rejected", ""), + log_odds_ratio=logs.get("orpo/log_odds_ratio"), + ) + + # GRPO completions + elif self.trainer_type == "grpo": + if all(key in logs for key in ["grpo/prompt", "grpo/completion"]): + self.logger.add_grpo_completion( + step=step, + prompt=logs.get("grpo/prompt", ""), + completion=logs.get("grpo/completion", ""), + reward=logs.get("grpo/reward"), + advantage=logs.get("grpo/advantage"), + ) + + # Periodically log to SwanLab + if step - self._last_logged_step >= self.log_interval: + if len(self.logger) > 0: + self.logger.log_to_swanlab(table_name=self.table_name) + self.logger.clear() + self._last_logged_step = step + + def on_train_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Log remaining completions at end of training.""" + if len(self.logger) > 0: + LOG.info( + f"Training complete, logging final {len(self.logger)} completions to SwanLab" + ) + self.logger.log_to_swanlab(table_name=self.table_name) + self._last_logged_step = state.global_step diff --git a/src/axolotl/integrations/swanlab/completion_logger.py b/src/axolotl/integrations/swanlab/completion_logger.py new file mode 100644 index 0000000000..dd709227f9 --- /dev/null +++ b/src/axolotl/integrations/swanlab/completion_logger.py @@ -0,0 +1,228 @@ +"""SwanLab completion logger for RLHF/DPO/KTO/ORPO/GRPO training. + +This module provides utilities for logging model completions during +preference training to SwanLab for qualitative analysis. +""" + +from collections import deque +from collections.abc import Mapping +from typing import Any + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +class CompletionLogger: + """Memory-bounded logger for RLHF completions. + + Stores prompts, completions, and rewards in fixed-size deques to prevent + memory leaks during long training runs. Logs completion tables to SwanLab + for qualitative analysis of model outputs. + + Example usage: + >>> logger = CompletionLogger(maxlen=128) + >>> logger.add_dpo_completion( + ... step=0, + ... prompt="What is AI?", + ... chosen="Artificial Intelligence is...", + ... rejected="AI means...", + ... reward_diff=0.5 + ... ) + >>> logger.log_to_swanlab() + + Attributes: + maxlen: Maximum number of completions to store (older ones are dropped) + data: Deque storing completion dictionaries + """ + + def __init__(self, maxlen: int = 128): + """Initialize completion logger with bounded buffer. + + Args: + maxlen: Maximum number of completions to store. When the buffer + is full, oldest completions are automatically discarded. + Default: 128 (sufficient for most RLHF runs without memory issues) + """ + self.maxlen = maxlen + self.data: deque[Mapping[str, Any]] = deque(maxlen=maxlen) + + def add_dpo_completion( + self, + step: int, + prompt: str, + chosen: str, + rejected: str, + reward_diff: float | None = None, + ) -> None: + """Add a DPO completion to the buffer. + + Args: + step: Training step number + prompt: Input prompt + chosen: Chosen (preferred) completion + rejected: Rejected (non-preferred) completion + reward_diff: Reward difference (chosen - rejected), if available + """ + entry = { + "step": step, + "prompt": prompt, + "chosen": chosen, + "rejected": rejected, + } + if reward_diff is not None: + entry["reward_diff"] = reward_diff + + self.data.append(entry) + + def add_kto_completion( + self, + step: int, + prompt: str, + completion: str, + label: bool, + reward: float | None = None, + ) -> None: + """Add a KTO completion to the buffer. + + Args: + step: Training step number + prompt: Input prompt + completion: Model-generated completion + label: True if desirable, False if undesirable + reward: Reward score, if available + """ + entry = { + "step": step, + "prompt": prompt, + "completion": completion, + "label": "desirable" if label else "undesirable", + } + if reward is not None: + entry["reward"] = reward + + self.data.append(entry) + + def add_orpo_completion( + self, + step: int, + prompt: str, + chosen: str, + rejected: str, + log_odds_ratio: float | None = None, + ) -> None: + """Add an ORPO completion to the buffer. + + Args: + step: Training step number + prompt: Input prompt + chosen: Chosen (preferred) completion + rejected: Rejected (non-preferred) completion + log_odds_ratio: Log odds ratio between chosen and rejected + """ + entry = { + "step": step, + "prompt": prompt, + "chosen": chosen, + "rejected": rejected, + } + if log_odds_ratio is not None: + entry["log_odds_ratio"] = log_odds_ratio + + self.data.append(entry) + + def add_grpo_completion( + self, + step: int, + prompt: str, + completion: str, + reward: float | None = None, + advantage: float | None = None, + ) -> None: + """Add a GRPO completion to the buffer. + + Args: + step: Training step number + prompt: Input prompt + completion: Model-generated completion + reward: Reward score from reward model + advantage: Advantage estimate (reward - baseline) + """ + entry = { + "step": step, + "prompt": prompt, + "completion": completion, + } + if reward is not None: + entry["reward"] = reward + if advantage is not None: + entry["advantage"] = advantage + + self.data.append(entry) + + def log_to_swanlab(self, table_name: str = "completions") -> bool: + """Log buffered completions to SwanLab as a table. + + Creates a SwanLab echarts Table with all buffered completions. + Only logs if SwanLab is initialized and data is available. + + Args: + table_name: Name of the table in SwanLab dashboard. + Default: "completions" + + Returns: + True if logging succeeded, False otherwise + """ + if not self.data: + LOG.debug("No completions to log to SwanLab") + return False + + try: + import swanlab + + if swanlab.get_run() is None: + LOG.debug("SwanLab not initialized, skipping completion logging") + return False + + # Convert deque to list of dicts + completions = list(self.data) + + # Extract headers from first entry (all entries should have same structure) + headers = list(completions[0].keys()) + + # Build rows: each completion becomes one row + rows = [] + for completion in completions: + row = [completion.get(header, "") for header in headers] + rows.append(row) + + # Log to SwanLab as echarts Table + swanlab.log({table_name: swanlab.echarts.Table().add(headers, rows)}) + + LOG.info(f"Logged {len(rows)} completions to SwanLab table '{table_name}'") + return True + + except ImportError: + LOG.warning( + "SwanLab not installed, cannot log completions. " + "Install with: pip install swanlab" + ) + return False + except Exception as err: # pylint: disable=broad-except + LOG.exception("Failed to log completions to SwanLab: %s", err) + return False + + def clear(self) -> None: + """Clear all buffered completions.""" + self.data.clear() + + def __len__(self) -> int: + """Return number of buffered completions.""" + return len(self.data) + + def __repr__(self) -> str: + """String representation showing buffer status.""" + return ( + f"CompletionLogger(maxlen={self.maxlen}, " + f"buffered={len(self.data)}/{self.maxlen})" + ) diff --git a/src/axolotl/integrations/swanlab/plugins.py b/src/axolotl/integrations/swanlab/plugins.py new file mode 100644 index 0000000000..16218d39d8 --- /dev/null +++ b/src/axolotl/integrations/swanlab/plugins.py @@ -0,0 +1,554 @@ +"""SwanLab Plugin for Axolotl""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from axolotl.integrations.base import BasePlugin +from axolotl.utils.logging import get_logger + +if TYPE_CHECKING: + from transformers import TrainerCallback + + from axolotl.utils.dict import DictDefault + +LOG = get_logger(__name__) + + +class SwanLabPlugin(BasePlugin): + """ + SwanLab integration plugin for Axolotl. + + Provides experiment tracking, visualization, and logging capabilities + using SwanLab (https://swanlab.cn). + + Usage in config.yaml: + plugins: + - axolotl.integrations.swanlab.SwanLabPlugin + + use_swanlab: true + swanlab_project: my-project + swanlab_experiment_name: my-experiment + swanlab_mode: cloud # or 'local', 'offline', 'disabled' + """ + + def __init__(self): + super().__init__() + self.swanlab_initialized = False + LOG.info("SwanLab plugin initialized") + + def get_input_args(self) -> str: + """Returns the configuration model for SwanLab integration.""" + return "axolotl.integrations.swanlab.SwanLabConfig" + + def register(self, cfg: dict): + """Register SwanLab plugin with configuration and conflict detection.""" + LOG.info("Registering SwanLab plugin") + + # === Conflict Detection: Required Fields === + + # Check if SwanLab is enabled + if cfg.get("use_swanlab"): + # 1. Validate project name is set + if not cfg.get("swanlab_project"): + raise ValueError( + "SwanLab enabled but 'swanlab_project' is not set.\n\n" + "Solutions:\n" + " 1. Add 'swanlab_project: your-project-name' to your config\n" + " 2. Set 'use_swanlab: false' to disable SwanLab\n\n" + "See: src/axolotl/integrations/swanlab/README.md for examples" + ) + + # 2. Validate swanlab_mode value + valid_modes = ["cloud", "local", "offline", "disabled"] + mode = cfg.get("swanlab_mode") + if mode and mode not in valid_modes: + raise ValueError( + f"Invalid swanlab_mode: '{mode}'.\n\n" + f"Valid options: {', '.join(valid_modes)}\n\n" + f"Example:\n" + f" swanlab_mode: cloud # Sync to SwanLab cloud\n" + f" swanlab_mode: local # Local only, no cloud sync\n" + ) + + # 3. Check API key for cloud mode + import os + + mode = cfg.get("swanlab_mode", "cloud") # Default is cloud + if mode == "cloud": + api_key = cfg.get("swanlab_api_key") or os.environ.get( + "SWANLAB_API_KEY" + ) + if not api_key: + LOG.warning( + "SwanLab cloud mode enabled but no API key found.\n" + "SwanLab may fail to initialize during training.\n\n" + "Solutions:\n" + " 1. Set SWANLAB_API_KEY environment variable:\n" + " export SWANLAB_API_KEY=your-api-key\n" + " 2. Add 'swanlab_api_key: your-api-key' to config (less secure)\n" + " 3. Run 'swanlab login' before training\n" + " 4. Use 'swanlab_mode: local' for offline tracking\n" + ) + + # === Conflict Detection: Multi-Logger Performance Warning === + + # Detect all active logging tools + active_loggers = [] + if cfg.get("use_wandb"): + active_loggers.append("WandB") + if cfg.get("use_mlflow"): + active_loggers.append("MLflow") + if cfg.get("comet_api_key") or cfg.get("comet_project_name"): + active_loggers.append("Comet") + if cfg.get("use_swanlab"): + active_loggers.append("SwanLab") + + if len(active_loggers) > 1: + LOG.warning( + f"\n{'=' * 70}\n" + f"Multiple logging tools enabled: {', '.join(active_loggers)}\n" + f"{'=' * 70}\n" + f"This may cause:\n" + f" - Performance overhead (~1-2% per logger, cumulative)\n" + f" - Increased memory usage\n" + f" - Longer training time per step\n" + f" - Potential config/callback conflicts\n\n" + f"Recommendations:\n" + f" - Choose ONE primary logging tool for production training\n" + f" - Use multiple loggers only for:\n" + f" * Migration period (transitioning between tools)\n" + f" * Short comparison runs\n" + f" * Debugging specific tool issues\n" + f" - Monitor system resources (CPU, memory) during training\n" + f"{'=' * 70}\n" + ) + + if len(active_loggers) >= 3: + LOG.error( + f"\n{'!' * 70}\n" + f"WARNING: {len(active_loggers)} logging tools enabled simultaneously!\n" + f"{'!' * 70}\n" + f"This is likely unintentional and WILL significantly impact performance.\n" + f"Expected overhead: ~{len(active_loggers) * 1.5:.1f}% per training step.\n\n" + f"STRONGLY RECOMMEND:\n" + f" - Disable all but ONE logging tool\n" + f" - Use config inheritance to manage multiple configs\n" + f"{'!' * 70}\n" + ) + + # === Auto-Enable Logic === + + # Enable SwanLab if project is specified + if cfg.get("swanlab_project") and not cfg.get("use_swanlab"): + cfg["use_swanlab"] = True + LOG.info("Automatically enabled use_swanlab because swanlab_project is set") + + def pre_model_load(self, cfg: DictDefault): + """Initialize SwanLab before model loading with runtime checks.""" + if not cfg.use_swanlab: + return + + # === Runtime Check: Import Availability === + try: + import swanlab + except ImportError as err: + raise ImportError( + "SwanLab is not installed.\n\n" + "Install with:\n" + " pip install swanlab\n\n" + "Or add to requirements:\n" + " swanlab>=0.3.0\n\n" + f"Original error: {err}" + ) from err + + # Log SwanLab version + try: + swanlab_version = swanlab.__version__ + LOG.info(f"SwanLab version: {swanlab_version}") + except AttributeError: + LOG.warning("Could not determine SwanLab version") + + # === Runtime Check: Distributed Training Setup === + from axolotl.utils.distributed import get_world_size, is_main_process + + world_size = get_world_size() + if world_size > 1: + mode = getattr(cfg, "swanlab_mode", "cloud") + LOG.info( + f"\n{'=' * 70}\n" + f"Distributed training detected (world_size={world_size})\n" + f"SwanLab mode: {mode}\n" + f"{'=' * 70}\n" + f"Behavior:\n" + f" - Only rank 0 will initialize SwanLab\n" + f" - Other ranks will skip SwanLab to avoid conflicts\n" + ) + + if mode == "cloud": + LOG.info( + f" - Only rank 0 will upload to SwanLab cloud\n" + f" - Other ranks run without SwanLab overhead\n" + f"{'=' * 70}\n" + ) + + # Only initialize SwanLab on the main process (rank 0) + # to avoid creating multiple runs in distributed training + if not is_main_process(): + LOG.debug("Skipping SwanLab initialization on non-main process") + return + + # Initialize SwanLab run (passing all params directly to init) + try: + init_kwargs = self._get_swanlab_init_kwargs(cfg) + swanlab.init(**init_kwargs) + self.swanlab_initialized = True + LOG.info(f"SwanLab initialized with project: {cfg.swanlab_project}") + + # Register Lark notification callback (if configured) + self._register_lark_callback(cfg) + + # Log configuration (with error handling) + try: + config_dict = self._prepare_config_for_logging(cfg) + swanlab.config.update(config_dict) + LOG.debug("Successfully logged config to SwanLab") + except Exception as config_err: # pylint: disable=broad-except + LOG.warning( + f"Failed to log config to SwanLab: {config_err}. Continuing anyway." + ) + + except Exception as err: # pylint: disable=broad-except + LOG.exception("Failed to initialize SwanLab: %s", err) + self.swanlab_initialized = False + + def add_callbacks_pre_trainer(self, cfg: DictDefault, model): + """Add SwanLab callbacks before trainer creation.""" + callbacks: list[TrainerCallback] = [] + + if not cfg.use_swanlab: + return callbacks + + if not self.swanlab_initialized: + LOG.warning("SwanLab not initialized, skipping callback registration") + return callbacks + + try: + from axolotl.utils.callbacks.swanlab import ( + CustomSwanLabCallback, + SaveAxolotlConfigtoSwanLabCallback, + ) + + # Add our custom lightweight SwanLabCallback + # (avoids omegaconf/antlr4 version conflicts) + swanlab_callback = CustomSwanLabCallback() + callbacks.append(swanlab_callback) + LOG.info("Added CustomSwanLabCallback for metrics logging") + + # Add Axolotl config logging callback + if cfg.axolotl_config_path: + config_callback = SaveAxolotlConfigtoSwanLabCallback( + cfg.axolotl_config_path + ) + callbacks.append(config_callback) + LOG.info("Added SaveAxolotlConfigtoSwanLabCallback") + + except ImportError as err: + LOG.exception("Failed to import SwanLab callbacks: %s", err) + + return callbacks + + def post_trainer_create(self, cfg: DictDefault, trainer): + """Post-trainer creation hook.""" + if cfg.use_swanlab and self.swanlab_initialized: + try: + import swanlab + + # Log additional trainer information (with safe conversion) + trainer_config = { + "total_steps": int(trainer.state.max_steps) + if trainer.state.max_steps + else None, + "num_train_epochs": float(trainer.args.num_train_epochs) + if trainer.args.num_train_epochs + else None, + "train_batch_size": int(trainer.args.train_batch_size) + if hasattr(trainer.args, "train_batch_size") + else None, + "gradient_accumulation_steps": int( + trainer.args.gradient_accumulation_steps + ) + if trainer.args.gradient_accumulation_steps + else None, + } + # Remove None values + trainer_config = { + k: v for k, v in trainer_config.items() if v is not None + } + + if trainer_config: + swanlab.config.update(trainer_config) + LOG.info("Logged trainer configuration to SwanLab") + except Exception as err: # pylint: disable=broad-except + LOG.debug(f"Failed to log trainer config to SwanLab: {err}") + + # Register RLHF completion logging callback if enabled + self._register_completion_callback(cfg, trainer) + + def _get_swanlab_init_kwargs(self, cfg: DictDefault) -> dict: + """Prepare kwargs for swanlab.init(). + + Passes all configuration parameters directly to swanlab.init() + instead of using environment variables as an intermediate layer. + + Returns: + dict: Keyword arguments for swanlab.init() + """ + init_kwargs = {} + + # Project name (required) + if cfg.swanlab_project: + init_kwargs["project"] = cfg.swanlab_project + + # Experiment name + if cfg.swanlab_experiment_name: + init_kwargs["experiment_name"] = cfg.swanlab_experiment_name + + # Description + if cfg.swanlab_description: + init_kwargs["description"] = cfg.swanlab_description + + # Workspace (organization) + if cfg.swanlab_workspace: + init_kwargs["workspace"] = cfg.swanlab_workspace + + # Mode: cloud, local, offline, disabled + if cfg.swanlab_mode: + init_kwargs["mode"] = cfg.swanlab_mode + + # API key (pass directly instead of via env var) + if cfg.swanlab_api_key: + init_kwargs["api_key"] = cfg.swanlab_api_key + + # Private deployment hosts (pass directly instead of via env var) + if cfg.swanlab_web_host: + init_kwargs["web_host"] = cfg.swanlab_web_host + + if cfg.swanlab_api_host: + init_kwargs["api_host"] = cfg.swanlab_api_host + + # Log model checkpoints (coming soon in SwanLab) + if cfg.swanlab_log_model: + init_kwargs["log_model"] = cfg.swanlab_log_model + + # Custom branding - adds Axolotl identifier to SwanLab UI + # This helps identify runs from Axolotl vs other frameworks + init_kwargs["config"] = {"UPPERFRAME": "🦎 Axolotl"} + + return init_kwargs + + def _prepare_config_for_logging(self, cfg: DictDefault) -> dict: + """Prepare configuration dict for logging to SwanLab.""" + + def safe_convert(value): + """Convert value to JSON-serializable type.""" + if value is None: + return None + if isinstance(value, (int, float, bool)): + return value + if isinstance(value, str): + return value + # Convert everything else to string + return str(value) + + try: + # Extract important training parameters with safe conversion + config_dict = { + "base_model": safe_convert(getattr(cfg, "base_model", "")), + "model_type": safe_convert(getattr(cfg, "model_type", "")), + "sequence_len": safe_convert(getattr(cfg, "sequence_len", None)), + "micro_batch_size": safe_convert( + getattr(cfg, "micro_batch_size", None) + ), + "gradient_accumulation_steps": safe_convert( + getattr(cfg, "gradient_accumulation_steps", None) + ), + "num_epochs": safe_convert(getattr(cfg, "num_epochs", None)), + "max_steps": safe_convert(getattr(cfg, "max_steps", None)), + "learning_rate": safe_convert(getattr(cfg, "learning_rate", None)), + "lr_scheduler": safe_convert(getattr(cfg, "lr_scheduler", "")), + "optimizer": safe_convert(getattr(cfg, "optimizer", "")), + "warmup_ratio": safe_convert(getattr(cfg, "warmup_ratio", None)), + "weight_decay": safe_convert(getattr(cfg, "weight_decay", None)), + "seed": safe_convert(getattr(cfg, "seed", None)), + "bf16": safe_convert(getattr(cfg, "bf16", None)), + "tf32": safe_convert(getattr(cfg, "tf32", None)), + "flash_attention": safe_convert(getattr(cfg, "flash_attention", None)), + "sample_packing": safe_convert(getattr(cfg, "sample_packing", None)), + } + + # Add FSDP/parallel config - only boolean flags + if hasattr(cfg, "fsdp_config") and cfg.fsdp_config: + config_dict["fsdp_enabled"] = True + config_dict["fsdp_version"] = safe_convert( + getattr(cfg, "fsdp_version", None) + ) + + if hasattr(cfg, "deepspeed") and cfg.deepspeed: + config_dict["deepspeed_enabled"] = True + + # Add context parallel info + if hasattr(cfg, "context_parallel_size"): + config_dict["context_parallel_size"] = safe_convert( + getattr(cfg, "context_parallel_size", None) + ) + if hasattr(cfg, "tensor_parallel_size"): + config_dict["tensor_parallel_size"] = safe_convert( + getattr(cfg, "tensor_parallel_size", None) + ) + if hasattr(cfg, "dp_shard_size"): + config_dict["dp_shard_size"] = safe_convert( + getattr(cfg, "dp_shard_size", None) + ) + + # Remove None values and empty strings + config_dict = { + k: v + for k, v in config_dict.items() + if v is not None and v != "" and v != "None" + } + + return config_dict + except Exception as err: # pylint: disable=broad-except + LOG.warning(f"Failed to prepare config for logging: {err}") + # Return minimal config + try: + lr = getattr(cfg, "learning_rate", None) + lr_value = float(lr) if lr is not None else None + except (TypeError, ValueError): + lr_value = None + return { + "base_model": str(getattr(cfg, "base_model", "unknown")), + "learning_rate": lr_value, + } + + def _register_lark_callback(self, cfg: DictDefault): + """Register Lark (Feishu) notification callback if configured. + + Lark notifications enable sending training updates to team chat channels, + useful for production monitoring and team collaboration. + + Args: + cfg: Configuration object with Lark webhook settings + """ + # Check if Lark webhook URL is configured + lark_webhook_url = getattr(cfg, "swanlab_lark_webhook_url", None) + if not lark_webhook_url: + return # Lark not configured, skip + + try: + import swanlab + from swanlab.plugin.notification import LarkCallback + + # Get optional secret for HMAC signature authentication + lark_secret = getattr(cfg, "swanlab_lark_secret", None) + + # Create Lark callback with webhook URL and optional secret + lark_callback = LarkCallback( + webhook_url=lark_webhook_url, + secret=lark_secret, + ) + + # Register callback with SwanLab + swanlab.register_callbacks([lark_callback]) + + if lark_secret: + LOG.info( + "Registered Lark notification callback with HMAC authentication" + ) + else: + LOG.info("Registered Lark notification callback (no HMAC secret)") + LOG.warning( + "Lark webhook has no secret configured. " + "For production use, set 'swanlab_lark_secret' to enable HMAC signature verification." + ) + + except ImportError as err: + LOG.warning( + f"Failed to import SwanLab Lark plugin: {err}\n\n" + "Lark notifications require SwanLab >= 0.3.0 with plugin support.\n" + "Install with: pip install 'swanlab>=0.3.0'\n\n" + "Continuing without Lark notifications..." + ) + except Exception as err: # pylint: disable=broad-except + LOG.exception( + "Failed to register Lark callback: %s\n\n" + "Check your Lark webhook URL and secret configuration.\n" + "Continuing without Lark notifications...", + err, + ) + + def _register_completion_callback(self, cfg: DictDefault, trainer): + """Register RLHF completion logging callback if enabled and applicable. + + This callback logs model completions (prompts, chosen/rejected responses, + rewards) to SwanLab during RLHF training for qualitative analysis. + + Args: + cfg: Configuration object with completion logging settings + trainer: The trainer instance to add callback to + """ + # Check if completion logging is enabled + log_completions = getattr(cfg, "swanlab_log_completions", True) + if not log_completions: + LOG.debug("SwanLab completion logging disabled by config") + return + + # Check if trainer is an RLHF trainer + trainer_name = trainer.__class__.__name__ + rlhf_trainers = ["DPO", "KTO", "ORPO", "GRPO", "CPO"] + is_rlhf_trainer = any(name in trainer_name for name in rlhf_trainers) + + if not is_rlhf_trainer: + LOG.debug( + f"Trainer {trainer_name} is not an RLHF trainer, " + "skipping completion logging callback" + ) + return + + try: + from axolotl.integrations.swanlab.callbacks import ( + SwanLabRLHFCompletionCallback, + ) + + # Get configuration parameters + log_interval = getattr(cfg, "swanlab_completion_log_interval", 100) + max_buffer = getattr(cfg, "swanlab_completion_max_buffer", 128) + + # Create and register callback + completion_callback = SwanLabRLHFCompletionCallback( + log_interval=log_interval, + max_completions=max_buffer, + table_name="rlhf_completions", + ) + + trainer.add_callback(completion_callback) + + LOG.info( + f"Registered SwanLab RLHF completion logging callback for {trainer_name} " + f"(log_interval={log_interval}, max_buffer={max_buffer})" + ) + + except ImportError as err: + LOG.warning( + f"Failed to import SwanLab completion callback: {err}\n\n" + "This is a bug - the callback should be available.\n" + "Please report this issue.\n\n" + "Continuing without completion logging..." + ) + except Exception as err: # pylint: disable=broad-except + LOG.exception( + "Failed to register SwanLab completion callback: %s\n\n" + "Continuing without completion logging...", + err, + ) diff --git a/src/axolotl/integrations/swanlab/profiling.py b/src/axolotl/integrations/swanlab/profiling.py new file mode 100644 index 0000000000..61243c54ee --- /dev/null +++ b/src/axolotl/integrations/swanlab/profiling.py @@ -0,0 +1,203 @@ +"""SwanLab profiling utilities for Axolotl trainers. + +This module provides decorators and context managers for profiling +trainer methods and logging execution times to SwanLab. +""" + +import time +from contextlib import contextmanager +from functools import wraps +from typing import Any, Callable + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +@contextmanager +def swanlab_profiling_context(trainer: Any, func_name: str): + """Context manager for profiling trainer methods. + + Measures execution time and logs to SwanLab if enabled. + + Example usage: + >>> with swanlab_profiling_context(self, "training_step"): + ... result = do_expensive_computation() + + Args: + trainer: Trainer instance (must have cfg attribute with use_swanlab flag) + func_name: Name of the function being profiled + + Yields: + None + """ + start_time = time.perf_counter() + try: + yield + finally: + duration = time.perf_counter() - start_time + + # Check if SwanLab is enabled and initialized + use_swanlab = getattr(getattr(trainer, "cfg", None), "use_swanlab", False) + if use_swanlab: + try: + import swanlab + + if swanlab.get_run() is not None: + # Log profiling metric + trainer_class = trainer.__class__.__name__ + metric_name = f"profiling/Time taken: {trainer_class}.{func_name}" + + swanlab.log({metric_name: duration}) + + except ImportError: + # SwanLab not installed, silently skip + pass + except Exception as err: # pylint: disable=broad-except + # Log error but don't fail training + LOG.debug(f"Failed to log profiling metric for {func_name}: {err}") + + +def swanlab_profile(func: Callable) -> Callable: + """Decorator to profile and log function execution time to SwanLab. + + Automatically measures execution time of trainer methods and logs + to SwanLab as profiling metrics. + + Example usage: + >>> class MyTrainer: + ... @swanlab_profile + ... def training_step(self, model, inputs): + ... return super().training_step(model, inputs) + + Args: + func: Function to profile (must be a method of a trainer instance) + + Returns: + Wrapped function with profiling + """ + + @wraps(func) + def wrapper(self, *args, **kwargs): + with swanlab_profiling_context(self, func.__name__): + return func(self, *args, **kwargs) + + return wrapper + + +class ProfilingConfig: + """Configuration for SwanLab profiling. + + This class provides a centralized way to control profiling behavior. + + Attributes: + enabled: Whether profiling is enabled globally + min_duration_ms: Minimum duration (in ms) to log (filters out very fast ops) + log_interval: Log every N function calls (to reduce overhead) + """ + + def __init__( + self, + enabled: bool = True, + min_duration_ms: float = 0.1, + log_interval: int = 1, + ): + """Initialize profiling configuration. + + Args: + enabled: Enable profiling. Default: True + min_duration_ms: Minimum duration to log (ms). Default: 0.1 + log_interval: Log every N calls. Default: 1 (log all) + """ + self.enabled = enabled + self.min_duration_ms = min_duration_ms + self.log_interval = log_interval + self._call_counts: dict[str, int] = {} + + def should_log(self, func_name: str, duration_seconds: float) -> bool: + """Check if a profiling measurement should be logged. + + Args: + func_name: Name of the profiled function + duration_seconds: Execution duration in seconds + + Returns: + True if should log, False otherwise + """ + if not self.enabled: + return False + + # Check minimum duration threshold + duration_ms = duration_seconds * 1000 + if duration_ms < self.min_duration_ms: + return False + + # Check log interval + self._call_counts.setdefault(func_name, 0) + self._call_counts[func_name] += 1 + + # Always log on first call OR at intervals + count = self._call_counts[func_name] + if count == 1 or count % self.log_interval == 0: + return True + + return False + + +# Global profiling config (can be modified by users) +DEFAULT_PROFILING_CONFIG = ProfilingConfig() + + +@contextmanager +def swanlab_profiling_context_advanced( + trainer: Any, + func_name: str, + config: ProfilingConfig | None = None, +): + """Advanced profiling context with configurable behavior. + + Similar to swanlab_profiling_context but with additional configuration + options for filtering and throttling profiling logs. + + Example usage: + >>> config = ProfilingConfig(min_duration_ms=1.0, log_interval=10) + >>> with swanlab_profiling_context_advanced(self, "forward", config): + ... output = model(inputs) + + Args: + trainer: Trainer instance + func_name: Function name + config: Profiling configuration. If None, uses DEFAULT_PROFILING_CONFIG + + Yields: + None + """ + if config is None: + config = DEFAULT_PROFILING_CONFIG + + start_time = time.perf_counter() + try: + yield + finally: + duration = time.perf_counter() - start_time + + # Check if should log based on config + if config.should_log(func_name, duration): + # Check if SwanLab is enabled + use_swanlab = getattr(getattr(trainer, "cfg", None), "use_swanlab", False) + if use_swanlab: + try: + import swanlab + + if swanlab.get_run() is not None: + trainer_class = trainer.__class__.__name__ + metric_name = ( + f"profiling/Time taken: {trainer_class}.{func_name}" + ) + + swanlab.log({metric_name: duration}) + + except ImportError: + pass + except Exception as err: # pylint: disable=broad-except + LOG.debug(f"Failed to log profiling metric for {func_name}: {err}") diff --git a/src/axolotl/utils/callbacks/swanlab.py b/src/axolotl/utils/callbacks/swanlab.py new file mode 100644 index 0000000000..4ebf2e61ef --- /dev/null +++ b/src/axolotl/utils/callbacks/swanlab.py @@ -0,0 +1,248 @@ +"""Callbacks for SwanLab integration""" + +from __future__ import annotations + +import json +import os +from shutil import copyfile +from tempfile import NamedTemporaryFile +from typing import TYPE_CHECKING + +from transformers import ( + TrainerCallback, + TrainerControl, + TrainerState, + TrainingArguments, +) + +from axolotl.utils.logging import get_logger + +if TYPE_CHECKING: + from axolotl.core.training_args import AxolotlTrainingArguments + +LOG = get_logger(__name__) + + +class CustomSwanLabCallback(TrainerCallback): + """ + Lightweight SwanLab callback that directly logs metrics without using + SwanLab's transformers integration (which requires omegaconf). + + This avoids the antlr4 version conflict between omegaconf and axolotl. + """ + + def __init__(self): + self._initialized = False + self.swanlab = None + + def setup(self): + """Lazy initialization of SwanLab""" + if self._initialized: + return + + try: + import swanlab + + self.swanlab = swanlab + + # Check if SwanLab run is initialized + if swanlab.get_run() is None: + LOG.warning("SwanLab run is not initialized") + return + + self._initialized = True + LOG.info("CustomSwanLabCallback initialized successfully") + except ImportError: + LOG.error("SwanLab is not installed") + + def on_train_begin( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Called at the beginning of training""" + if not state.is_world_process_zero: + return control + + self.setup() + + if not self._initialized: + return control + + # Log training configuration + try: + self.swanlab.config.update( + { + "train_batch_size": args.per_device_train_batch_size, + "eval_batch_size": args.per_device_eval_batch_size, + "learning_rate": args.learning_rate, + "num_train_epochs": args.num_train_epochs, + "max_steps": args.max_steps, + "warmup_steps": args.warmup_steps, + "logging_steps": args.logging_steps, + "save_steps": args.save_steps, + "gradient_accumulation_steps": args.gradient_accumulation_steps, + } + ) + LOG.debug("Training configuration logged to SwanLab") + except Exception as err: + LOG.warning(f"Failed to log training config: {err}") + + return control + + def on_log( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + logs=None, + **kwargs, + ): + """Called when logging metrics""" + if not state.is_world_process_zero: + return control + + if not self._initialized: + self.setup() + + if not self._initialized or logs is None: + return control + + # Log metrics to SwanLab + try: + # Filter out non-numeric values and prepare for logging + metrics = {} + for key, value in logs.items(): + if isinstance(value, (int, float)): + # Use step from state + metrics[key] = value + + if metrics and state.global_step is not None: + self.swanlab.log(metrics, step=state.global_step) + except Exception as err: + LOG.warning(f"Failed to log metrics to SwanLab: {err}") + + return control + + def on_train_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Called at the end of training""" + if not state.is_world_process_zero: + return control + + if self._initialized: + LOG.info("Training completed. SwanLab logs are available.") + + return control + + +class SaveAxolotlConfigtoSwanLabCallback(TrainerCallback): + """Callback to save axolotl config to SwanLab""" + + def __init__(self, axolotl_config_path): + self.axolotl_config_path = axolotl_config_path + + def on_train_begin( + self, + args: AxolotlTrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + if state.is_world_process_zero: + try: + import swanlab + + # Check if SwanLab is initialized + if swanlab.get_run() is None: + LOG.warning( + "SwanLab run is not initialized. Please initialize SwanLab before training." + ) + return control + + # Log Axolotl config as artifact + with NamedTemporaryFile( + mode="w", delete=False, suffix=".yml", prefix="axolotl_config_" + ) as temp_file: + copyfile(self.axolotl_config_path, temp_file.name) + + # Log config file to SwanLab + with open(temp_file.name, "r", encoding="utf-8") as config_file: + swanlab.log( + { + "axolotl_config": swanlab.Text( + config_file.read(), caption="Axolotl Config" + ) + } + ) + + LOG.info( + "The Axolotl config has been saved to the SwanLab run under logs." + ) + + # Clean up temp file + os.unlink(temp_file.name) + + except ImportError: + LOG.warning( + "SwanLab is not installed. Install it with: pip install swanlab" + ) + except (FileNotFoundError, ConnectionError) as err: + LOG.warning(f"Error while saving Axolotl config to SwanLab: {err}") + + # Log DeepSpeed config if available + if args.deepspeed: + try: + import swanlab + + with NamedTemporaryFile( + mode="w", + delete=False, + suffix=".json", + prefix="deepspeed_config_", + ) as temp_file: + skip_upload = False + if isinstance(args.deepspeed, dict): + json.dump(args.deepspeed, temp_file, indent=4) + elif isinstance(args.deepspeed, str) and os.path.exists( + args.deepspeed + ): + copyfile(args.deepspeed, temp_file.name) + else: + skip_upload = True + + if not skip_upload: + temp_file.flush() + with open( + temp_file.name, "r", encoding="utf-8" + ) as ds_config_file: + swanlab.log( + { + "deepspeed_config": swanlab.Text( + ds_config_file.read(), + caption="DeepSpeed Config", + ) + } + ) + LOG.info( + "The DeepSpeed config has been saved to the SwanLab run under logs." + ) + + # Clean up temp file + os.unlink(temp_file.name) + + except (FileNotFoundError, ConnectionError) as err: + LOG.warning( + f"Error while saving DeepSpeed config to SwanLab: {err}" + ) + except ImportError: + pass + + return control diff --git a/tests/integrations/test_swanlab.py b/tests/integrations/test_swanlab.py new file mode 100644 index 0000000000..b86df0b0e3 --- /dev/null +++ b/tests/integrations/test_swanlab.py @@ -0,0 +1,1337 @@ +# Copyright 2024 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unit tests for SwanLab Integration Plugin. + +Tests conflict detection, configuration validation, and multi-logger warnings. +""" + +import logging +import os +import time +from unittest.mock import MagicMock, patch + +import pytest +from pydantic import ValidationError +from transformers.utils.import_utils import _is_package_available + +from axolotl.integrations.swanlab.args import SwanLabConfig +from axolotl.integrations.swanlab.plugins import SwanLabPlugin + +SWANLAB_INSTALLED = _is_package_available("swanlab") + + +@pytest.mark.skipif(not SWANLAB_INSTALLED, reason="swanlab package not installed") +class TestSwanLabConfigValidators: + """Tests for Pydantic field validators in SwanLabConfig.""" + + def test_valid_swanlab_mode_cloud(self): + """Test that 'cloud' mode is valid.""" + config = SwanLabConfig(swanlab_mode="cloud") + assert config.swanlab_mode == "cloud" + + def test_valid_swanlab_mode_local(self): + """Test that 'local' mode is valid.""" + config = SwanLabConfig(swanlab_mode="local") + assert config.swanlab_mode == "local" + + def test_valid_swanlab_mode_offline(self): + """Test that 'offline' mode is valid.""" + config = SwanLabConfig(swanlab_mode="offline") + assert config.swanlab_mode == "offline" + + def test_valid_swanlab_mode_disabled(self): + """Test that 'disabled' mode is valid.""" + config = SwanLabConfig(swanlab_mode="disabled") + assert config.swanlab_mode == "disabled" + + def test_invalid_swanlab_mode(self): + """Test that invalid mode raises ValueError.""" + with pytest.raises(ValidationError) as exc_info: + SwanLabConfig(swanlab_mode="invalid") + + error_msg = str(exc_info.value) + assert "Invalid swanlab_mode" in error_msg + assert "cloud" in error_msg + assert "local" in error_msg + assert "offline" in error_msg + assert "disabled" in error_msg + + def test_swanlab_mode_none_allowed(self): + """Test that None mode is allowed (will use default).""" + config = SwanLabConfig(swanlab_mode=None) + assert config.swanlab_mode is None + + def test_valid_swanlab_project(self): + """Test that valid project name is accepted.""" + config = SwanLabConfig(swanlab_project="my-project") + assert config.swanlab_project == "my-project" + + def test_swanlab_project_none_allowed(self): + """Test that None project is allowed.""" + config = SwanLabConfig(swanlab_project=None) + assert config.swanlab_project is None + + def test_empty_swanlab_project_rejected(self): + """Test that empty string project name is rejected.""" + with pytest.raises(ValidationError) as exc_info: + SwanLabConfig(swanlab_project="") + + error_msg = str(exc_info.value) + assert "cannot be an empty string" in error_msg + + def test_whitespace_only_project_rejected(self): + """Test that whitespace-only project name is rejected.""" + with pytest.raises(ValidationError) as exc_info: + SwanLabConfig(swanlab_project=" ") + + error_msg = str(exc_info.value) + assert "cannot be an empty string" in error_msg + + def test_use_swanlab_true_requires_project(self): + """Test that use_swanlab=True requires swanlab_project.""" + with pytest.raises(ValidationError) as exc_info: + SwanLabConfig(use_swanlab=True, swanlab_project=None) + + error_msg = str(exc_info.value) + assert "swanlab_project" in error_msg.lower() + assert "not set" in error_msg.lower() + + def test_use_swanlab_true_with_project_valid(self): + """Test that use_swanlab=True with project is valid.""" + config = SwanLabConfig(use_swanlab=True, swanlab_project="my-project") + assert config.use_swanlab is True + assert config.swanlab_project == "my-project" + + def test_use_swanlab_false_no_project_valid(self): + """Test that use_swanlab=False without project is valid.""" + config = SwanLabConfig(use_swanlab=False, swanlab_project=None) + assert config.use_swanlab is False + assert config.swanlab_project is None + + def test_use_swanlab_none_no_project_valid(self): + """Test that use_swanlab=None without project is valid.""" + config = SwanLabConfig(use_swanlab=None, swanlab_project=None) + assert config.use_swanlab is None + assert config.swanlab_project is None + + +@pytest.mark.skipif(not SWANLAB_INSTALLED, reason="swanlab package not installed") +class TestSwanLabPluginRegister: + """Tests for SwanLabPlugin.register() conflict detection.""" + + def test_register_without_use_swanlab(self): + """Test that register works when SwanLab is not enabled.""" + plugin = SwanLabPlugin() + cfg = {"use_swanlab": False} + # Should not raise + plugin.register(cfg) + + def test_register_use_swanlab_missing_project(self): + """Test that use_swanlab=True without project raises ValueError.""" + plugin = SwanLabPlugin() + cfg = {"use_swanlab": True} + + with pytest.raises(ValueError) as exc_info: + plugin.register(cfg) + + error_msg = str(exc_info.value) + assert "swanlab_project" in error_msg + assert "not set" in error_msg + assert "Solutions" in error_msg + + def test_register_use_swanlab_with_project_valid(self): + """Test that use_swanlab=True with project is valid.""" + plugin = SwanLabPlugin() + cfg = {"use_swanlab": True, "swanlab_project": "my-project"} + # Should not raise + plugin.register(cfg) + + def test_register_invalid_mode(self): + """Test that invalid swanlab_mode raises ValueError.""" + plugin = SwanLabPlugin() + cfg = { + "use_swanlab": True, + "swanlab_project": "my-project", + "swanlab_mode": "invalid-mode", + } + + with pytest.raises(ValueError) as exc_info: + plugin.register(cfg) + + error_msg = str(exc_info.value) + assert "Invalid swanlab_mode" in error_msg + assert "cloud" in error_msg + assert "local" in error_msg + + def test_register_valid_modes(self): + """Test that all valid modes are accepted.""" + plugin = SwanLabPlugin() + valid_modes = ["cloud", "local", "offline", "disabled"] + + for mode in valid_modes: + cfg = { + "use_swanlab": True, + "swanlab_project": "my-project", + "swanlab_mode": mode, + } + # Should not raise + plugin.register(cfg) + + def test_register_auto_enable_swanlab(self): + """Test that providing swanlab_project auto-enables use_swanlab.""" + plugin = SwanLabPlugin() + cfg = {"swanlab_project": "my-project"} + + plugin.register(cfg) + + assert cfg["use_swanlab"] is True + + def test_register_cloud_mode_without_api_key_warns(self, caplog): + """Test that cloud mode without API key logs warning.""" + plugin = SwanLabPlugin() + cfg = { + "use_swanlab": True, + "swanlab_project": "my-project", + "swanlab_mode": "cloud", + } + + # Clear environment variable to ensure it's not set + with patch.dict(os.environ, {}, clear=True): + with caplog.at_level(logging.WARNING): + plugin.register(cfg) + + # Should log warning about missing API key + warning_messages = [record.message for record in caplog.records] + assert any("API key" in msg for msg in warning_messages) + + +@pytest.mark.skipif(not SWANLAB_INSTALLED, reason="swanlab package not installed") +class TestMultiLoggerDetection: + """Tests for multi-logger conflict detection.""" + + def test_single_logger_no_warning(self, caplog): + """Test that single logger doesn't trigger warning.""" + plugin = SwanLabPlugin() + cfg = {"use_swanlab": True, "swanlab_project": "my-project"} + + with caplog.at_level(logging.WARNING): + plugin.register(cfg) + + # Should not log multi-logger warning + warning_messages = [record.message for record in caplog.records] + assert not any("Multiple logging tools" in msg for msg in warning_messages) + + def test_two_loggers_warning(self, caplog): + """Test that two loggers trigger warning.""" + plugin = SwanLabPlugin() + cfg = { + "use_swanlab": True, + "swanlab_project": "my-project", + "use_wandb": True, + } + + with caplog.at_level(logging.WARNING): + plugin.register(cfg) + + # Should log multi-logger warning + warning_messages = [record.message for record in caplog.records] + assert any("Multiple logging tools" in msg for msg in warning_messages) + assert any("SwanLab" in msg and "WandB" in msg for msg in warning_messages) + + def test_three_loggers_error(self, caplog): + """Test that three loggers trigger error-level warning.""" + plugin = SwanLabPlugin() + cfg = { + "use_swanlab": True, + "swanlab_project": "my-project", + "use_wandb": True, + "use_mlflow": True, + } + + with caplog.at_level(logging.ERROR): + plugin.register(cfg) + + # Should log error-level warning + error_messages = [ + record.message + for record in caplog.records + if record.levelno >= logging.ERROR + ] + assert any("logging tools enabled" in msg for msg in error_messages) + + def test_multi_logger_with_comet(self, caplog): + """Test that Comet is detected in multi-logger scenario.""" + plugin = SwanLabPlugin() + cfg = { + "use_swanlab": True, + "swanlab_project": "my-project", + "comet_api_key": "test-key", + } + + with caplog.at_level(logging.WARNING): + plugin.register(cfg) + + # Should detect Comet + warning_messages = [record.message for record in caplog.records] + assert any("Comet" in msg for msg in warning_messages) + + def test_multi_logger_with_comet_project(self, caplog): + """Test that Comet is detected via comet_project_name.""" + plugin = SwanLabPlugin() + cfg = { + "use_swanlab": True, + "swanlab_project": "my-project", + "comet_project_name": "test-project", + } + + with caplog.at_level(logging.WARNING): + plugin.register(cfg) + + # Should detect Comet + warning_messages = [record.message for record in caplog.records] + assert any("Comet" in msg for msg in warning_messages) + + +@pytest.mark.skipif(not SWANLAB_INSTALLED, reason="swanlab package not installed") +class TestSwanLabPluginPreModelLoad: + """Tests for SwanLabPlugin.pre_model_load() runtime checks.""" + + def test_pre_model_load_disabled(self): + """Test that pre_model_load does nothing when SwanLab is disabled.""" + plugin = SwanLabPlugin() + cfg = MagicMock() + cfg.use_swanlab = False + + # Should not raise + plugin.pre_model_load(cfg) + + def test_pre_model_load_import_error(self): + """Test that missing swanlab package raises clear ImportError.""" + plugin = SwanLabPlugin() + cfg = MagicMock() + cfg.use_swanlab = True + + with patch( + "builtins.__import__", side_effect=ImportError("No module named 'swanlab'") + ): + with pytest.raises(ImportError) as exc_info: + plugin.pre_model_load(cfg) + + error_msg = str(exc_info.value) + assert "SwanLab is not installed" in error_msg + assert "pip install swanlab" in error_msg + + @patch("axolotl.utils.distributed.is_main_process") + @patch("axolotl.utils.distributed.get_world_size") + def test_pre_model_load_non_main_process_skips( + self, mock_get_world_size, mock_is_main_process + ): + """Test that non-main process skips SwanLab initialization.""" + mock_get_world_size.return_value = 2 + mock_is_main_process.return_value = False + + plugin = SwanLabPlugin() + cfg = MagicMock() + cfg.use_swanlab = True + + with patch("swanlab.init") as mock_init: + plugin.pre_model_load(cfg) + # Should NOT call swanlab.init + mock_init.assert_not_called() + + @patch("axolotl.utils.distributed.is_main_process") + @patch("axolotl.utils.distributed.get_world_size") + def test_pre_model_load_distributed_logging( + self, mock_get_world_size, mock_is_main_process, caplog + ): + """Test that distributed training logs world size info.""" + mock_get_world_size.return_value = 4 + mock_is_main_process.return_value = True + + plugin = SwanLabPlugin() + cfg = MagicMock() + cfg.use_swanlab = True + cfg.swanlab_project = "test-project" + cfg.swanlab_mode = "cloud" + + with patch("swanlab.init"), patch("swanlab.__version__", "0.3.0"): + with caplog.at_level(logging.INFO): + plugin.pre_model_load(cfg) + + # Should log distributed training info + info_messages = [record.message for record in caplog.records] + assert any("world_size=4" in msg for msg in info_messages) + assert any("Only rank 0" in msg for msg in info_messages) + + +@pytest.mark.skipif(not SWANLAB_INSTALLED, reason="swanlab package not installed") +class TestSwanLabInitKwargs: + """Tests for SwanLab initialization with direct parameter passing.""" + + def test_custom_branding_added_to_config(self): + """Test that Axolotl custom branding is added to SwanLab config.""" + from axolotl.integrations.swanlab.plugins import SwanLabPlugin + from axolotl.utils.dict import DictDefault + + plugin = SwanLabPlugin() + cfg = DictDefault( + { + "use_swanlab": True, + "swanlab_project": "test-project", + } + ) + + init_kwargs = plugin._get_swanlab_init_kwargs(cfg) + + # Verify custom branding is present + assert "config" in init_kwargs + assert init_kwargs["config"]["UPPERFRAME"] == "🦎 Axolotl" + + def test_api_key_passed_directly(self): + """Test that API key is passed directly to swanlab.init() instead of via env var.""" + from axolotl.integrations.swanlab.plugins import SwanLabPlugin + from axolotl.utils.dict import DictDefault + + plugin = SwanLabPlugin() + cfg = DictDefault( + { + "use_swanlab": True, + "swanlab_project": "test-project", + "swanlab_api_key": "test-api-key-12345", + } + ) + + init_kwargs = plugin._get_swanlab_init_kwargs(cfg) + + # Verify API key is in init_kwargs (not set as env var) + assert "api_key" in init_kwargs + assert init_kwargs["api_key"] == "test-api-key-12345" + + def test_private_deployment_hosts_passed_directly(self): + """Test that private deployment hosts are passed directly to swanlab.init().""" + from axolotl.integrations.swanlab.plugins import SwanLabPlugin + from axolotl.utils.dict import DictDefault + + plugin = SwanLabPlugin() + cfg = DictDefault( + { + "use_swanlab": True, + "swanlab_project": "internal-project", + "swanlab_web_host": "https://swanlab.company.com", + "swanlab_api_host": "https://api-swanlab.company.com", + } + ) + + init_kwargs = plugin._get_swanlab_init_kwargs(cfg) + + # Verify private deployment hosts are in init_kwargs + assert "web_host" in init_kwargs + assert init_kwargs["web_host"] == "https://swanlab.company.com" + assert "api_host" in init_kwargs + assert init_kwargs["api_host"] == "https://api-swanlab.company.com" + + @patch("axolotl.utils.distributed.is_main_process") + def test_full_private_deployment_init(self, mock_is_main_process): + """Test complete initialization with private deployment configuration.""" + mock_is_main_process.return_value = True + + from axolotl.integrations.swanlab.plugins import SwanLabPlugin + from axolotl.utils.dict import DictDefault + + plugin = SwanLabPlugin() + cfg = DictDefault( + { + "use_swanlab": True, + "swanlab_project": "secure-project", + "swanlab_experiment_name": "experiment-001", + "swanlab_mode": "cloud", + "swanlab_api_key": "private-key-xyz", + "swanlab_web_host": "https://swanlab.internal.net", + "swanlab_api_host": "https://api.swanlab.internal.net", + "swanlab_workspace": "research-team", + } + ) + + with patch("swanlab.init") as mock_init: + plugin.pre_model_load(cfg) + + # Verify swanlab.init was called with all parameters + mock_init.assert_called_once() + call_kwargs = mock_init.call_args[1] + + assert call_kwargs["project"] == "secure-project" + assert call_kwargs["experiment_name"] == "experiment-001" + assert call_kwargs["mode"] == "cloud" + assert call_kwargs["api_key"] == "private-key-xyz" + assert call_kwargs["web_host"] == "https://swanlab.internal.net" + assert call_kwargs["api_host"] == "https://api.swanlab.internal.net" + assert call_kwargs["workspace"] == "research-team" + assert call_kwargs["config"]["UPPERFRAME"] == "🦎 Axolotl" + + def test_env_vars_not_set_for_api_params(self): + """Test that environment variables are NOT set for API parameters.""" + import os + + from axolotl.integrations.swanlab.plugins import SwanLabPlugin + from axolotl.utils.dict import DictDefault + + # Clear any existing env vars + for key in [ + "SWANLAB_API_KEY", + "SWANLAB_WEB_HOST", + "SWANLAB_API_HOST", + "SWANLAB_MODE", + ]: + os.environ.pop(key, None) + + plugin = SwanLabPlugin() + cfg = DictDefault( + { + "use_swanlab": True, + "swanlab_project": "test-project", + "swanlab_api_key": "test-key", + "swanlab_web_host": "https://test.com", + "swanlab_api_host": "https://api-test.com", + "swanlab_mode": "cloud", + } + ) + + with ( + patch("axolotl.utils.distributed.is_main_process", return_value=True), + patch("swanlab.init"), + ): + plugin.pre_model_load(cfg) + + # Verify env vars were NOT set (simplified approach) + # The old _setup_swanlab_env() method is removed, so these shouldn't be set + # Note: SwanLab itself might set these, but our plugin shouldn't + # We're just testing that our plugin doesn't call _setup_swanlab_env() + + +@pytest.mark.skipif(not SWANLAB_INSTALLED, reason="swanlab package not installed") +class TestLarkNotificationIntegration: + """Tests for Lark (Feishu) notification integration.""" + + def test_lark_callback_registration_with_webhook_only(self): + """Test Lark callback registration with webhook URL only (no secret).""" + plugin = SwanLabPlugin() + + cfg = MagicMock() + cfg.use_swanlab = True + cfg.swanlab_project = "test-project" + cfg.swanlab_mode = "local" + cfg.swanlab_lark_webhook_url = ( + "https://open.feishu.cn/open-apis/bot/v2/hook/test-webhook" + ) + cfg.swanlab_lark_secret = None + + with ( + patch("swanlab.init"), + patch("swanlab.__version__", "0.3.0"), + patch("swanlab.register_callbacks") as mock_register, + patch("axolotl.utils.distributed.is_main_process", return_value=True), + patch("axolotl.utils.distributed.get_world_size", return_value=1), + ): + # Mock LarkCallback import + with patch("swanlab.plugin.notification.LarkCallback") as MockLarkCallback: + mock_lark_instance = MagicMock() + MockLarkCallback.return_value = mock_lark_instance + + plugin.pre_model_load(cfg) + + # Verify LarkCallback was instantiated with correct params + MockLarkCallback.assert_called_once_with( + webhook_url="https://open.feishu.cn/open-apis/bot/v2/hook/test-webhook", + secret=None, + ) + + # Verify callback was registered + mock_register.assert_called_once_with([mock_lark_instance]) + + def test_lark_callback_registration_with_secret(self): + """Test Lark callback registration with webhook URL and HMAC secret.""" + plugin = SwanLabPlugin() + + cfg = MagicMock() + cfg.use_swanlab = True + cfg.swanlab_project = "test-project" + cfg.swanlab_mode = "local" + cfg.swanlab_lark_webhook_url = ( + "https://open.feishu.cn/open-apis/bot/v2/hook/test-webhook" + ) + cfg.swanlab_lark_secret = "test-hmac-secret" + + with ( + patch("swanlab.init"), + patch("swanlab.__version__", "0.3.0"), + patch("swanlab.register_callbacks") as mock_register, + patch("axolotl.utils.distributed.is_main_process", return_value=True), + patch("axolotl.utils.distributed.get_world_size", return_value=1), + ): + with patch("swanlab.plugin.notification.LarkCallback") as MockLarkCallback: + mock_lark_instance = MagicMock() + MockLarkCallback.return_value = mock_lark_instance + + plugin.pre_model_load(cfg) + + # Verify LarkCallback was instantiated with secret + MockLarkCallback.assert_called_once_with( + webhook_url="https://open.feishu.cn/open-apis/bot/v2/hook/test-webhook", + secret="test-hmac-secret", + ) + + mock_register.assert_called_once_with([mock_lark_instance]) + + def test_lark_callback_not_registered_without_webhook(self): + """Test that Lark callback is NOT registered when webhook URL not provided.""" + plugin = SwanLabPlugin() + + cfg = MagicMock() + cfg.use_swanlab = True + cfg.swanlab_project = "test-project" + cfg.swanlab_mode = "local" + cfg.swanlab_lark_webhook_url = None # No webhook + cfg.swanlab_lark_secret = None + + with ( + patch("swanlab.init"), + patch("swanlab.__version__", "0.3.0"), + patch("swanlab.register_callbacks") as mock_register, + patch("axolotl.utils.distributed.is_main_process", return_value=True), + patch("axolotl.utils.distributed.get_world_size", return_value=1), + ): + plugin.pre_model_load(cfg) + + # Verify register_callbacks was NOT called + mock_register.assert_not_called() + + def test_lark_import_error_handled_gracefully(self, caplog): + """Test that ImportError for Lark plugin is handled gracefully.""" + plugin = SwanLabPlugin() + + cfg = MagicMock() + cfg.use_swanlab = True + cfg.swanlab_project = "test-project" + cfg.swanlab_mode = "local" + cfg.swanlab_lark_webhook_url = ( + "https://open.feishu.cn/open-apis/bot/v2/hook/test-webhook" + ) + cfg.swanlab_lark_secret = None + + with ( + patch("swanlab.init"), + patch("swanlab.__version__", "0.3.0"), + patch("axolotl.utils.distributed.is_main_process", return_value=True), + patch("axolotl.utils.distributed.get_world_size", return_value=1), + ): + # Mock ImportError for LarkCallback + with patch( + "swanlab.plugin.notification.LarkCallback", + side_effect=ImportError( + "No module named 'swanlab.plugin.notification'" + ), + ): + with caplog.at_level(logging.WARNING): + plugin.pre_model_load(cfg) + + # Should log warning about missing Lark plugin + warning_messages = [record.message for record in caplog.records] + assert any( + "Failed to import SwanLab Lark plugin" in msg + for msg in warning_messages + ) + assert any("SwanLab >= 0.3.0" in msg for msg in warning_messages) + + def test_lark_warning_for_missing_secret(self, caplog): + """Test that warning is logged when Lark webhook has no HMAC secret.""" + plugin = SwanLabPlugin() + + cfg = MagicMock() + cfg.use_swanlab = True + cfg.swanlab_project = "test-project" + cfg.swanlab_mode = "local" + cfg.swanlab_lark_webhook_url = ( + "https://open.feishu.cn/open-apis/bot/v2/hook/test-webhook" + ) + cfg.swanlab_lark_secret = None # No secret + + with ( + patch("swanlab.init"), + patch("swanlab.__version__", "0.3.0"), + patch("swanlab.register_callbacks"), + patch("axolotl.utils.distributed.is_main_process", return_value=True), + patch("axolotl.utils.distributed.get_world_size", return_value=1), + ): + with patch("swanlab.plugin.notification.LarkCallback"): + with caplog.at_level(logging.WARNING): + plugin.pre_model_load(cfg) + + # Should log warning about missing secret + warning_messages = [record.message for record in caplog.records] + assert any( + "no secret configured" in msg.lower() + for msg in warning_messages + ) + assert any("swanlab_lark_secret" in msg for msg in warning_messages) + + +@pytest.mark.skipif(not SWANLAB_INSTALLED, reason="swanlab package not installed") +class TestSwanLabPluginIntegration: + """Integration tests for SwanLab plugin lifecycle.""" + + def test_full_lifecycle_valid_config(self): + """Test full plugin lifecycle with valid configuration.""" + plugin = SwanLabPlugin() + + # Register + cfg_dict = { + "use_swanlab": True, + "swanlab_project": "test-project", + "swanlab_mode": "local", + } + plugin.register(cfg_dict) + + # Pre-model load (mock SwanLab) + cfg_obj = MagicMock() + cfg_obj.use_swanlab = True + cfg_obj.swanlab_project = "test-project" + cfg_obj.swanlab_mode = "local" + cfg_obj.swanlab_lark_webhook_url = None # No Lark + + with ( + patch("swanlab.init") as mock_init, + patch("swanlab.__version__", "0.3.0"), + patch("axolotl.utils.distributed.is_main_process", return_value=True), + patch("axolotl.utils.distributed.get_world_size", return_value=1), + ): + plugin.pre_model_load(cfg_obj) + # Should call swanlab.init + mock_init.assert_called_once() + + def test_lifecycle_with_multi_logger_warning(self, caplog): + """Test lifecycle with multi-logger warning.""" + plugin = SwanLabPlugin() + + cfg_dict = { + "use_swanlab": True, + "swanlab_project": "test-project", + "use_wandb": True, + } + + with caplog.at_level(logging.WARNING): + plugin.register(cfg_dict) + + # Should have multi-logger warning + warning_messages = [record.message for record in caplog.records] + assert any("Multiple logging tools" in msg for msg in warning_messages) + + def test_lifecycle_invalid_config_fails_early(self): + """Test that invalid config fails at register stage.""" + plugin = SwanLabPlugin() + + cfg_dict = { + "use_swanlab": True, + # Missing swanlab_project + } + + # Should fail at register, not pre_model_load + with pytest.raises(ValueError): + plugin.register(cfg_dict) + + def test_full_lifecycle_with_lark_notifications(self): + """Test full lifecycle including Lark notification registration.""" + plugin = SwanLabPlugin() + + # Register + cfg_dict = { + "use_swanlab": True, + "swanlab_project": "test-project", + "swanlab_mode": "cloud", + } + plugin.register(cfg_dict) + + # Pre-model load with Lark config + cfg_obj = MagicMock() + cfg_obj.use_swanlab = True + cfg_obj.swanlab_project = "test-project" + cfg_obj.swanlab_mode = "cloud" + cfg_obj.swanlab_lark_webhook_url = ( + "https://open.feishu.cn/open-apis/bot/v2/hook/test" + ) + cfg_obj.swanlab_lark_secret = "secret123" + + with ( + patch("swanlab.init"), + patch("swanlab.__version__", "0.3.0"), + patch("swanlab.register_callbacks") as mock_register, + patch("axolotl.utils.distributed.is_main_process", return_value=True), + patch("axolotl.utils.distributed.get_world_size", return_value=1), + ): + with patch("swanlab.plugin.notification.LarkCallback") as MockLarkCallback: + mock_lark_instance = MagicMock() + MockLarkCallback.return_value = mock_lark_instance + + plugin.pre_model_load(cfg_obj) + + # Verify both SwanLab init AND Lark callback registration + MockLarkCallback.assert_called_once() + mock_register.assert_called_once_with([mock_lark_instance]) + + +@pytest.mark.skipif(not SWANLAB_INSTALLED, reason="swanlab package not installed") +class TestCompletionLogger: + """Tests for CompletionLogger utility class.""" + + def test_completion_logger_initialization(self): + """Test CompletionLogger initializes with correct maxlen.""" + from axolotl.integrations.swanlab.completion_logger import CompletionLogger + + logger = CompletionLogger(maxlen=64) + assert logger.maxlen == 64 + assert len(logger) == 0 + + def test_add_dpo_completion(self): + """Test adding DPO completions to buffer.""" + from axolotl.integrations.swanlab.completion_logger import CompletionLogger + + logger = CompletionLogger(maxlen=10) + + logger.add_dpo_completion( + step=0, + prompt="What is AI?", + chosen="Artificial Intelligence is...", + rejected="AI means...", + reward_diff=0.5, + ) + + assert len(logger) == 1 + entry = logger.data[0] + assert entry["step"] == 0 + assert entry["prompt"] == "What is AI?" + assert entry["chosen"] == "Artificial Intelligence is..." + assert entry["rejected"] == "AI means..." + assert entry["reward_diff"] == 0.5 + + def test_add_kto_completion(self): + """Test adding KTO completions to buffer.""" + from axolotl.integrations.swanlab.completion_logger import CompletionLogger + + logger = CompletionLogger(maxlen=10) + + logger.add_kto_completion( + step=1, + prompt="Explain quantum physics", + completion="Quantum physics is...", + label=True, + reward=0.8, + ) + + assert len(logger) == 1 + entry = logger.data[0] + assert entry["step"] == 1 + assert entry["prompt"] == "Explain quantum physics" + assert entry["completion"] == "Quantum physics is..." + assert entry["label"] == "desirable" + assert entry["reward"] == 0.8 + + def test_add_orpo_completion(self): + """Test adding ORPO completions to buffer.""" + from axolotl.integrations.swanlab.completion_logger import CompletionLogger + + logger = CompletionLogger(maxlen=10) + + logger.add_orpo_completion( + step=2, + prompt="Write a poem", + chosen="Roses are red...", + rejected="Violets are blue...", + log_odds_ratio=1.2, + ) + + assert len(logger) == 1 + entry = logger.data[0] + assert entry["step"] == 2 + assert entry["chosen"] == "Roses are red..." + assert entry["rejected"] == "Violets are blue..." + assert entry["log_odds_ratio"] == 1.2 + + def test_add_grpo_completion(self): + """Test adding GRPO completions to buffer.""" + from axolotl.integrations.swanlab.completion_logger import CompletionLogger + + logger = CompletionLogger(maxlen=10) + + logger.add_grpo_completion( + step=3, + prompt="Solve this problem", + completion="The answer is 42", + reward=0.9, + advantage=0.3, + ) + + assert len(logger) == 1 + entry = logger.data[0] + assert entry["step"] == 3 + assert entry["completion"] == "The answer is 42" + assert entry["reward"] == 0.9 + assert entry["advantage"] == 0.3 + + def test_memory_bounded_buffer(self): + """Test that buffer respects maxlen and drops oldest entries.""" + from axolotl.integrations.swanlab.completion_logger import CompletionLogger + + logger = CompletionLogger(maxlen=3) + + # Add 5 completions + for i in range(5): + logger.add_dpo_completion( + step=i, + prompt=f"Prompt {i}", + chosen=f"Chosen {i}", + rejected=f"Rejected {i}", + ) + + # Should only keep last 3 + assert len(logger) == 3 + assert logger.data[0]["step"] == 2 # Oldest kept + assert logger.data[1]["step"] == 3 + assert logger.data[2]["step"] == 4 # Newest + + def test_log_to_swanlab_when_not_initialized(self): + """Test logging gracefully fails when SwanLab not initialized.""" + from axolotl.integrations.swanlab.completion_logger import CompletionLogger + + logger = CompletionLogger(maxlen=10) + logger.add_dpo_completion( + step=0, + prompt="Test", + chosen="A", + rejected="B", + ) + + with patch("swanlab.get_run", return_value=None): + result = logger.log_to_swanlab() + assert result is False # Should fail gracefully + + def test_log_to_swanlab_success(self): + """Test successful logging to SwanLab.""" + from axolotl.integrations.swanlab.completion_logger import CompletionLogger + + logger = CompletionLogger(maxlen=10) + logger.add_dpo_completion( + step=0, + prompt="Test prompt", + chosen="Chosen response", + rejected="Rejected response", + reward_diff=0.5, + ) + + with ( + patch("swanlab.get_run") as mock_get_run, + patch("swanlab.log") as mock_log, + patch("swanlab.echarts.Table") as MockTable, + ): + mock_get_run.return_value = MagicMock() # SwanLab initialized + mock_table_instance = MagicMock() + MockTable.return_value = mock_table_instance + + result = logger.log_to_swanlab(table_name="test_table") + + assert result is True + mock_log.assert_called_once() + mock_table_instance.add.assert_called_once() + + def test_clear_buffer(self): + """Test clearing the completion buffer.""" + from axolotl.integrations.swanlab.completion_logger import CompletionLogger + + logger = CompletionLogger(maxlen=10) + logger.add_dpo_completion( + step=0, + prompt="Test", + chosen="A", + rejected="B", + ) + + assert len(logger) == 1 + logger.clear() + assert len(logger) == 0 + + def test_repr(self): + """Test string representation.""" + from axolotl.integrations.swanlab.completion_logger import CompletionLogger + + logger = CompletionLogger(maxlen=128) + logger.add_dpo_completion( + step=0, + prompt="Test", + chosen="A", + rejected="B", + ) + + repr_str = repr(logger) + assert "CompletionLogger" in repr_str + assert "maxlen=128" in repr_str + assert "buffered=1/128" in repr_str + + +@pytest.mark.skipif(not SWANLAB_INSTALLED, reason="swanlab package not installed") +class TestSwanLabRLHFCompletionCallback: + """Tests for SwanLabRLHFCompletionCallback.""" + + def test_callback_initialization(self): + """Test callback initializes with correct parameters.""" + from axolotl.integrations.swanlab.callbacks import SwanLabRLHFCompletionCallback + + callback = SwanLabRLHFCompletionCallback( + log_interval=50, + max_completions=64, + table_name="custom_table", + ) + + assert callback.log_interval == 50 + assert callback.logger.maxlen == 64 + assert callback.table_name == "custom_table" + assert callback.trainer_type is None + + def test_trainer_type_detection_dpo(self): + """Test DPO trainer type is detected correctly.""" + from axolotl.integrations.swanlab.callbacks import SwanLabRLHFCompletionCallback + + callback = SwanLabRLHFCompletionCallback() + + # Mock trainer with DPO in name + mock_trainer = MagicMock() + mock_trainer.__class__.__name__ = "AxolotlDPOTrainer" + + callback.on_init_end( + args=MagicMock(), + state=MagicMock(), + control=MagicMock(), + trainer=mock_trainer, + ) + + assert callback.trainer_type == "dpo" + + def test_trainer_type_detection_kto(self): + """Test KTO trainer type is detected correctly.""" + from axolotl.integrations.swanlab.callbacks import SwanLabRLHFCompletionCallback + + callback = SwanLabRLHFCompletionCallback() + + mock_trainer = MagicMock() + mock_trainer.__class__.__name__ = "AxolotlKTOTrainer" + + callback.on_init_end( + args=MagicMock(), + state=MagicMock(), + control=MagicMock(), + trainer=mock_trainer, + ) + + assert callback.trainer_type == "kto" + + def test_on_train_end_logs_completions(self): + """Test that completions are logged at end of training.""" + from axolotl.integrations.swanlab.callbacks import SwanLabRLHFCompletionCallback + + callback = SwanLabRLHFCompletionCallback() + callback.trainer_type = "dpo" + + # Add some completions to buffer + callback.logger.add_dpo_completion( + step=0, + prompt="Test", + chosen="A", + rejected="B", + ) + + with patch.object(callback.logger, "log_to_swanlab") as mock_log: + callback.on_train_end( + args=MagicMock(), + state=MagicMock(global_step=100), + control=MagicMock(), + ) + + # Should log remaining completions + mock_log.assert_called_once() + + +@pytest.mark.skipif(not SWANLAB_INSTALLED, reason="swanlab package not installed") +class TestSwanLabPluginCompletionIntegration: + """Integration tests for completion logging in SwanLabPlugin.""" + + def test_completion_callback_registered_for_dpo_trainer(self): + """Test that completion callback is registered for DPO trainer.""" + from axolotl.integrations.swanlab.plugins import SwanLabPlugin + from axolotl.utils.dict import DictDefault + + plugin = SwanLabPlugin() + plugin.swanlab_initialized = True # Simulate SwanLab initialized + + cfg = { + "use_swanlab": True, + "swanlab_project": "test-project", + "swanlab_log_completions": True, + "swanlab_completion_log_interval": 50, + "swanlab_completion_max_buffer": 64, + } + cfg_obj = DictDefault(cfg) + + # Mock DPO trainer + mock_trainer = MagicMock() + mock_trainer.__class__.__name__ = "AxolotlDPOTrainer" + mock_trainer.state = MagicMock(max_steps=1000) + mock_trainer.args = MagicMock( + num_train_epochs=3, + train_batch_size=4, + gradient_accumulation_steps=2, + ) + + with patch("swanlab.config.update"): + plugin.post_trainer_create(cfg_obj, mock_trainer) + + # Verify callback was added + mock_trainer.add_callback.assert_called_once() + callback = mock_trainer.add_callback.call_args[0][0] + assert callback.__class__.__name__ == "SwanLabRLHFCompletionCallback" + assert callback.log_interval == 50 + assert callback.logger.maxlen == 64 + + def test_completion_callback_not_registered_for_non_rlhf_trainer(self): + """Test that completion callback is NOT registered for non-RLHF trainers.""" + from axolotl.integrations.swanlab.plugins import SwanLabPlugin + from axolotl.utils.dict import DictDefault + + plugin = SwanLabPlugin() + plugin.swanlab_initialized = True + + cfg = { + "use_swanlab": True, + "swanlab_project": "test-project", + "swanlab_log_completions": True, + } + cfg_obj = DictDefault(cfg) + + # Mock regular SFT trainer (not RLHF) + mock_trainer = MagicMock() + mock_trainer.__class__.__name__ = "AxolotlTrainer" # Not RLHF + mock_trainer.state = MagicMock(max_steps=1000) + mock_trainer.args = MagicMock() + + with patch("swanlab.config.update"): + plugin.post_trainer_create(cfg_obj, mock_trainer) + + # Callback should NOT be added for non-RLHF trainer + mock_trainer.add_callback.assert_not_called() + + def test_completion_callback_not_registered_when_disabled(self): + """Test that completion callback is not registered when disabled in config.""" + from axolotl.integrations.swanlab.plugins import SwanLabPlugin + from axolotl.utils.dict import DictDefault + + plugin = SwanLabPlugin() + plugin.swanlab_initialized = True + + cfg = { + "use_swanlab": True, + "swanlab_project": "test-project", + "swanlab_log_completions": False, # Disabled + } + cfg_obj = DictDefault(cfg) + + # Mock DPO trainer + mock_trainer = MagicMock() + mock_trainer.__class__.__name__ = "AxolotlDPOTrainer" + mock_trainer.state = MagicMock(max_steps=1000) + mock_trainer.args = MagicMock() + + with patch("swanlab.config.update"): + plugin.post_trainer_create(cfg_obj, mock_trainer) + + # Callback should NOT be added when disabled + mock_trainer.add_callback.assert_not_called() + + +@pytest.mark.skipif(not SWANLAB_INSTALLED, reason="swanlab package not installed") +class TestSwanLabProfiling: + """Tests for SwanLab profiling utilities.""" + + def test_profiling_context_logs_duration(self): + """Test that profiling context logs execution duration.""" + from axolotl.integrations.swanlab.profiling import swanlab_profiling_context + + # Mock trainer with SwanLab enabled + mock_trainer = MagicMock() + mock_trainer.cfg = MagicMock(use_swanlab=True) + mock_trainer.__class__.__name__ = "TestTrainer" + + with patch("swanlab.get_run") as mock_get_run, patch("swanlab.log") as mock_log: + mock_get_run.return_value = MagicMock() # SwanLab initialized + + with swanlab_profiling_context(mock_trainer, "test_function"): + time.sleep(0.01) # Simulate work + + # Verify log was called with correct metric name + mock_log.assert_called_once() + logged_data = mock_log.call_args[0][0] + assert "profiling/Time taken: TestTrainer.test_function" in logged_data + # Duration should be > 0.01 seconds + assert ( + logged_data["profiling/Time taken: TestTrainer.test_function"] >= 0.01 + ) + + def test_profiling_context_skips_when_swanlab_disabled(self): + """Test that profiling is skipped when SwanLab is disabled.""" + from axolotl.integrations.swanlab.profiling import swanlab_profiling_context + + mock_trainer = MagicMock() + mock_trainer.cfg = MagicMock(use_swanlab=False) # Disabled + + with patch("swanlab.log") as mock_log: + with swanlab_profiling_context(mock_trainer, "test_function"): + time.sleep(0.01) + + # Should NOT log when disabled + mock_log.assert_not_called() + + def test_profiling_context_skips_when_swanlab_not_initialized(self): + """Test that profiling is skipped when SwanLab not initialized.""" + from axolotl.integrations.swanlab.profiling import swanlab_profiling_context + + mock_trainer = MagicMock() + mock_trainer.cfg = MagicMock(use_swanlab=True) + + with ( + patch("swanlab.get_run", return_value=None), + patch("swanlab.log") as mock_log, + ): + with swanlab_profiling_context(mock_trainer, "test_function"): + time.sleep(0.01) + + # Should NOT log when not initialized + mock_log.assert_not_called() + + def test_profiling_decorator(self): + """Test swanlab_profile decorator.""" + from axolotl.integrations.swanlab.profiling import swanlab_profile + + class MockTrainer: + def __init__(self): + self.cfg = MagicMock(use_swanlab=True) + + @swanlab_profile + def expensive_method(self, x): + time.sleep(0.01) + return x * 2 + + trainer = MockTrainer() + + with patch("swanlab.get_run") as mock_get_run, patch("swanlab.log") as mock_log: + mock_get_run.return_value = MagicMock() + + result = trainer.expensive_method(5) + + # Verify method still works correctly + assert result == 10 + + # Verify profiling was logged + mock_log.assert_called_once() + logged_data = mock_log.call_args[0][0] + assert "profiling/Time taken: MockTrainer.expensive_method" in logged_data + + def test_profiling_config(self): + """Test ProfilingConfig class.""" + from axolotl.integrations.swanlab.profiling import ProfilingConfig + + config = ProfilingConfig( + enabled=True, + min_duration_ms=1.0, + log_interval=5, + ) + + # Test enabled check + assert config.enabled is True + + # Test minimum duration filtering + assert config.should_log("func1", 0.0001) is False # 0.1ms < 1.0ms threshold + assert config.should_log("func2", 0.002) is True # 2.0ms > 1.0ms threshold + + # Test log interval + assert config.should_log("func3", 0.002) is True # 1st call + assert config.should_log("func3", 0.002) is False # 2nd call + assert config.should_log("func3", 0.002) is False # 3rd call + assert config.should_log("func3", 0.002) is False # 4th call + assert config.should_log("func3", 0.002) is True # 5th call (interval=5) + + def test_profiling_config_when_disabled(self): + """Test ProfilingConfig when disabled.""" + from axolotl.integrations.swanlab.profiling import ProfilingConfig + + config = ProfilingConfig(enabled=False) + + # Should never log when disabled + assert config.should_log("func1", 100.0) is False + + def test_profiling_context_advanced(self): + """Test advanced profiling context with custom config.""" + from axolotl.integrations.swanlab.profiling import ( + ProfilingConfig, + swanlab_profiling_context_advanced, + ) + + mock_trainer = MagicMock() + mock_trainer.cfg = MagicMock(use_swanlab=True) + mock_trainer.__class__.__name__ = "TestTrainer" + + # Config that filters out very fast operations + config = ProfilingConfig(min_duration_ms=10.0) # 10ms minimum + + with patch("swanlab.get_run") as mock_get_run, patch("swanlab.log") as mock_log: + mock_get_run.return_value = MagicMock() + + # Fast operation (< 10ms) - should NOT log + with swanlab_profiling_context_advanced(mock_trainer, "fast_op", config): + time.sleep(0.001) # 1ms + + mock_log.assert_not_called() + + # Slow operation (> 10ms) - should log + with swanlab_profiling_context_advanced(mock_trainer, "slow_op", config): + time.sleep(0.015) # 15ms + + mock_log.assert_called_once() + + def test_profiling_with_exception(self): + """Test that profiling still logs even when exception occurs.""" + from axolotl.integrations.swanlab.profiling import swanlab_profiling_context + + mock_trainer = MagicMock() + mock_trainer.cfg = MagicMock(use_swanlab=True) + mock_trainer.__class__.__name__ = "TestTrainer" + + with patch("swanlab.get_run") as mock_get_run, patch("swanlab.log") as mock_log: + mock_get_run.return_value = MagicMock() + + try: + with swanlab_profiling_context(mock_trainer, "error_function"): + time.sleep(0.01) + raise ValueError("Test error") + except ValueError: + pass # Expected + + # Should still log duration even with exception + mock_log.assert_called_once() From 7bf6f70e96f7b1f6386f5bdd22550d2b5ad22324 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Tue, 6 Jan 2026 19:55:17 +0530 Subject: [PATCH 1085/1405] fix total/trainable tokens log (#3344) * fix total/trainable tokens log * fix total/trainable tokens log --- src/axolotl/core/trainers/base.py | 9 ++++----- src/axolotl/utils/callbacks/tokens_per_second.py | 6 ------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 3a08d0574b..799dcf02ec 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -660,11 +660,10 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: logs["tokens/train_per_sec_per_gpu"] = round( self.state.last_tokens_per_second.item() / self.args.logging_steps, 2 ) - if ( - hasattr(self.state, "total_tokens") - and self.state.total_tokens is not None - ): - logs["total_tokens"] = int(self.state.total_tokens.item()) + if "total" in self.state.tokens: + logs["tokens/total"] = int(self.state.tokens["total"].item()) + if "trainable" in self.state.tokens: + logs["tokens/trainable"] = int(self.state.tokens["trainable"].item()) del self._stored_metrics[train_eval] diff --git a/src/axolotl/utils/callbacks/tokens_per_second.py b/src/axolotl/utils/callbacks/tokens_per_second.py index a1b955a747..679b1c8640 100644 --- a/src/axolotl/utils/callbacks/tokens_per_second.py +++ b/src/axolotl/utils/callbacks/tokens_per_second.py @@ -101,9 +101,3 @@ def on_log( # Clear per-step tokens after logging if tokens and "trainable_tokens" in tokens: tokens["trainable_tokens"] = torch.zeros_like(tokens["trainable_tokens"]) - - if tokens and "total" in tokens: - logs["tokens/total"] = tokens["total"].item() - - if tokens and "trainable" in tokens: - logs["tokens/trainable"] = tokens["trainable"].item() From e7f0d4ba5be91a452850d6e3f1656c97ec1d0fd3 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:14:48 +0530 Subject: [PATCH 1086/1405] Increased test coverage for lora/qlora (#3147) * config_val tests * remove config val(not needed) * config validation * parameter freeze validation * merge/unmerge tests * removal unwanted * rename * lint * updated lint * Update tests/utils/lora/test_config_validation_lora.py Co-authored-by: NanoCode012 * pytest skip + mock fix * nitpicks * revert some nitpicks --------- Co-authored-by: NanoCode012 --- .../utils/lora/test_config_validation_lora.py | 92 ++++++ tests/utils/lora/test_freeze_lora.py | 261 ++++++++++++++++++ tests/utils/lora/test_merge_lora.py | 181 ++++++++++++ 3 files changed, 534 insertions(+) create mode 100644 tests/utils/lora/test_config_validation_lora.py create mode 100644 tests/utils/lora/test_freeze_lora.py create mode 100644 tests/utils/lora/test_merge_lora.py diff --git a/tests/utils/lora/test_config_validation_lora.py b/tests/utils/lora/test_config_validation_lora.py new file mode 100644 index 0000000000..a22e2a5b7b --- /dev/null +++ b/tests/utils/lora/test_config_validation_lora.py @@ -0,0 +1,92 @@ +import pytest + +from axolotl.utils.config import validate_config +from axolotl.utils.dict import DictDefault + + +class TestLoRAConfigValidation: + """Test suite for LoRA/QLoRA configuration validation""" + + def test_basic_configuration_validation(self): + """Test basic LoRA configuration validation""" + + valid_config = DictDefault( + { + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.1, + "lora_target_modules": ["q_proj", "v_proj"], + "datasets": [{"path": "dummy_dataset", "type": "alpaca"}], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 1e-5, + "base_model": "dummy_model", + } + ) + + result = validate_config(valid_config) + assert result["adapter"] == "lora" + + with pytest.raises(ValueError, match="not compatible with DoRA"): + invalid_config = DictDefault( + { + "adapter": "lora", + "lora_mlp_kernel": True, + "peft_use_dora": True, + "datasets": [{"path": "dummy_dataset", "type": "alpaca"}], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 1e-5, + "base_model": "dummy_model", + } + ) + validate_config(invalid_config) + + def test_qlora_4bit_validation(self): + """Test QLoRA 4-bit configuration validation""" + valid_config = DictDefault( + { + "adapter": "qlora", + "load_in_4bit": True, + "bnb_4bit_compute_dtype": "float16", + "datasets": [{"path": "dummy_dataset", "type": "alpaca"}], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 1e-5, + "base_model": "dummy_model", + } + ) + result = validate_config(valid_config) + assert result["adapter"] == "qlora" + assert result["load_in_4bit"] is True + + # Test QLoRA without 4-bit (should fail via PEFT validation) + with pytest.raises(ValueError, match=r"Require cfg\.load_in_4bit"): + invalid_config = DictDefault( + { + "adapter": "qlora", + "load_in_4bit": False, + "datasets": [{"path": "dummy_dataset", "type": "alpaca"}], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 1e-5, + "base_model": "dummy_model", + } + ) + validate_config(invalid_config) + + # Test QLoRA with 8-bit (incompatible) + with pytest.raises(ValueError, match="Can't load qlora in 8bit"): + invalid_config = DictDefault( + { + "adapter": "qlora", + "load_in_8bit": True, + "datasets": [{"path": "dummy_dataset", "type": "alpaca"}], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 1e-5, + "base_model": "dummy_model", + } + ) + validate_config(invalid_config) diff --git a/tests/utils/lora/test_freeze_lora.py b/tests/utils/lora/test_freeze_lora.py new file mode 100644 index 0000000000..da90c18264 --- /dev/null +++ b/tests/utils/lora/test_freeze_lora.py @@ -0,0 +1,261 @@ +import importlib.util +from unittest.mock import Mock + +import pytest +import torch +import torch.nn as nn + +from axolotl.kernels.lora import get_lora_parameters + +PEFT_AVAILABLE = importlib.util.find_spec("peft") is not None + + +class TestLoRAParameterFreezing: + """Test suite for LoRA parameter freezing validation.""" + + def setup_method(self): + self.dtype = torch.float32 + + def create_mock_lora_layer( + self, has_adapters=True, adapters_disabled=False, merged=False + ): + """Create a mock LoRA layer for testing.""" + mock_layer = Mock() + + base_layer = Mock() + base_layer.weight = torch.randn(512, 256, dtype=self.dtype) + base_layer.bias = torch.randn(512, dtype=self.dtype) + + if has_adapters: + mock_layer.base_layer = base_layer + mock_layer.disable_adapters = adapters_disabled + mock_layer.merged = merged + + mock_layer.active_adapters = ["default"] + mock_layer.lora_A = {"default": Mock()} + mock_layer.lora_B = {"default": Mock()} + mock_layer.scaling = {"default": 0.1} + + mock_layer.lora_A["default"].weight = torch.randn(16, 256, dtype=self.dtype) + mock_layer.lora_B["default"].weight = torch.randn(512, 16, dtype=self.dtype) + else: + mock_layer.weight = base_layer.weight + mock_layer.bias = base_layer.bias + + return mock_layer + + def test_parameter_freezing_adapters_disabled(self): + """Test that LoRA parameters are None when adapters are disabled.""" + layer = self.create_mock_lora_layer(has_adapters=True, adapters_disabled=True) + + W, b, quant_state, A, B, s = get_lora_parameters(layer) + + # Base parameters should be returned + assert W is not None + assert b is not None + # LoRA parameters should be None (frozen) + assert A is None + assert B is None + assert s is None + + def test_parameter_freezing_adapters_merged(self): + """Test that LoRA parameters are None when adapters are merged.""" + layer = self.create_mock_lora_layer(has_adapters=True, merged=True) + + W, b, quant_state, A, B, s = get_lora_parameters(layer) + + # Base parameters should be returned + assert W is not None + assert b is not None + + # LoRA parameters should be None (frozen) + assert A is None + assert B is None + assert s is None + + def test_parameter_freezing_no_adapters(self): + """Test parameter behavior when no adapters are present.""" + layer = self.create_mock_lora_layer(has_adapters=False) + + W, b, quant_state, A, B, s = get_lora_parameters(layer) + + # Base parameters should be returned + assert W is not None + assert b is not None + + # LoRA parameters should be None (frozen) + assert A is None + assert B is None + assert s is None + + def test_parameter_active_adapters_enabled(self): + """Test that LoRA parameters are returned when adapters are active.""" + layer = self.create_mock_lora_layer( + has_adapters=True, adapters_disabled=False, merged=False + ) + + W, b, quant_state, A, B, s = get_lora_parameters(layer) + + # All parameters should be returned + assert W is not None + assert b is not None + assert A is not None + assert B is not None + assert s is not None + assert s == 0.1 + + def test_parameter_shapes_consistency(self): + """Test that parameter shapes are consistent when active.""" + layer = self.create_mock_lora_layer( + has_adapters=True, adapters_disabled=False, merged=False + ) + + W, b, quant_state, A, B, s = get_lora_parameters(layer) + + # Check shape consistency + assert W.shape == (512, 256) + assert b.shape == (512,) + assert A.shape == (16, 256) + assert B.shape == (512, 16) + + def test_parameter_dtypes_consistency(self): + """Test that parameter dtypes are consistent.""" + layer = self.create_mock_lora_layer( + has_adapters=True, adapters_disabled=False, merged=False + ) + + W, b, quant_state, A, B, s = get_lora_parameters(layer) + + assert W.dtype == self.dtype + assert b.dtype == self.dtype + assert A.dtype == self.dtype + assert B.dtype == self.dtype + + def test_quantization_state_handling(self): + """Test that quantization state is properly handled.""" + layer = self.create_mock_lora_layer(has_adapters=True) + + quant_state_mock = Mock() + layer.base_layer.weight.quant_state = quant_state_mock + + W, b, quant_state, A, B, s = get_lora_parameters(layer) + + assert quant_state == quant_state_mock + + def test_multiple_adapters_active_adapter_selection(self): + """Test that the correct adapter is selected when multiple adapters exist.""" + layer = self.create_mock_lora_layer( + has_adapters=True, adapters_disabled=False, merged=False + ) + + layer.lora_A["adapter2"] = Mock() + layer.lora_B["adapter2"] = Mock() + layer.scaling["adapter2"] = 0.2 + + layer.lora_A["adapter2"].weight = torch.randn(16, 256, dtype=self.dtype) + layer.lora_B["adapter2"].weight = torch.randn(512, 16, dtype=self.dtype) + + layer.active_adapters = ["adapter2"] + + W, b, quant_state, A, B, s = get_lora_parameters(layer) + + assert s == 0.2 + assert torch.equal(A, layer.lora_A["adapter2"].weight) + assert torch.equal(B, layer.lora_B["adapter2"].weight) + + +class TestLoRAParameterFreezingIntegration: + """Integration tests for parameter freezing with actual LoRA layers.""" + + @pytest.mark.skipif( + not PEFT_AVAILABLE, reason="PEFT not available for integration tests" + ) + def test_parameter_freezing_with_real_lora_layer(self): + """Test parameter freezing with actual PEFT LoRA layer.""" + from peft import LoraConfig, get_peft_model + + class SimpleModel(nn.Module): + def __init__(self): + super().__init__() + self.linear = nn.Linear(256, 512) + + def forward(self, x): + return self.linear(x) + + base_model = SimpleModel() + lora_config = LoraConfig( + r=16, + lora_alpha=32, + target_modules=["linear"], + lora_dropout=0.1, + ) + model = get_peft_model(base_model, lora_config) + lora_layer = model.base_model.model.linear + # Test with adapters enabled + W, b, quant_state, A, B, s = get_lora_parameters(lora_layer) + assert A is not None + assert B is not None + assert s is not None + # Test with adapters disabled + model.disable_adapter_layers() + W, b, quant_state, A, B, s = get_lora_parameters(lora_layer) + assert A is None + assert B is None + assert s is None + + @pytest.mark.skipif( + not PEFT_AVAILABLE, reason="PEFT not available for integration tests" + ) + def test_parameter_freezing_gradient_behavior(self): + """Test that frozen parameters don't receive gradients.""" + from peft import LoraConfig, get_peft_model + + class SimpleModel(nn.Module): + def __init__(self): + super().__init__() + self.linear = nn.Linear(256, 512) + + def forward(self, x): + return self.linear(x) + + base_model = SimpleModel() + lora_config = LoraConfig( + r=16, + lora_alpha=32, + target_modules=["linear"], + lora_dropout=0.1, + ) + model = get_peft_model(base_model, lora_config) + x = torch.randn(1, 256) + target = torch.randn(1, 512) + model.enable_adapter_layers() + output = model(x) + loss = nn.MSELoss()(output, target) + loss.backward() + lora_layer = model.base_model.model.linear + has_lora_grads = any( + param.grad is not None + for name, param in lora_layer.named_parameters() + if "lora_" in name + ) + assert has_lora_grads, ( + "LoRA parameters should have gradients when adapters are enabled" + ) + model.zero_grad() + model.disable_adapter_layers() + output = model(x) + loss = nn.MSELoss()(output, target) + any_requires_grad = any(param.requires_grad for param in model.parameters()) + if any_requires_grad: + loss.backward() + has_lora_grads_disabled = any( + param.grad is not None + for name, param in lora_layer.named_parameters() + if "lora_" in name + ) + assert not has_lora_grads_disabled, ( + "LoRA parameters should not have gradients when adapters are disabled" + ) + model.zero_grad() + del model, base_model, lora_layer, x, target, output, loss + torch.cuda.empty_cache() if torch.cuda.is_available() else None diff --git a/tests/utils/lora/test_merge_lora.py b/tests/utils/lora/test_merge_lora.py new file mode 100644 index 0000000000..8edccafb9e --- /dev/null +++ b/tests/utils/lora/test_merge_lora.py @@ -0,0 +1,181 @@ +from unittest.mock import Mock, patch + +import torch + +from axolotl.cli.merge_lora import do_merge_lora +from axolotl.utils.dict import DictDefault + + +class TestAdapterMergeUnmerge: + """Test suite for LoRA adapter merging/unmerging functionality""" + + def setup_method(self): + self.dtype = torch.float32 + self.device = torch.device("cpu") + + def create_mock_base_model(self, vocab_size=1000, hidden_size=256): + """Create a mock base model with linear layers""" + mock_model = Mock() + + mock_model.config = Mock() + mock_model.config.vocab_size = vocab_size + mock_model.config.hidden_size = hidden_size + + mock_model.q_proj = Mock() + mock_model.q_proj.weight = torch.randn( + hidden_size, hidden_size, dtype=self.dtype + ) + mock_model.q_proj.bias = torch.randn(hidden_size, dtype=self.dtype) + + mock_model.v_proj = Mock() + mock_model.v_proj.weight = torch.randn( + hidden_size, hidden_size, dtype=self.dtype + ) + mock_model.v_proj.bias = torch.randn(hidden_size, dtype=self.dtype) + + return mock_model + + def create_mock_lora_model(self, base_model, r=8, alpha=16): + """Create a mock LoRA model wrapping the base model""" + mock_lora_model = Mock() + mock_lora_model.base_model = base_model + + mock_lora_model.merge_and_unload = None + mock_lora_model.to = Mock(return_value=mock_lora_model) + + mock_lora_model.generation_config = Mock() + mock_lora_model.config = Mock() + + self.original_q_weight = base_model.q_proj.weight.clone() + self.original_v_weight = base_model.v_proj.weight.clone() + + mock_lora_model.peft_config = {"default": Mock()} + mock_lora_model.peft_config["default"].r = r + mock_lora_model.peft_config["default"].lora_alpha = alpha + + self.lora_A_q = torch.randn( + r, base_model.q_proj.weight.shape[1], dtype=self.dtype + ) + self.lora_B_q = torch.randn( + base_model.q_proj.weight.shape[0], r, dtype=self.dtype + ) + + self.lora_A_v = torch.randn( + r, base_model.v_proj.weight.shape[1], dtype=self.dtype + ) + self.lora_B_v = torch.randn( + base_model.v_proj.weight.shape[0], r, dtype=self.dtype + ) + + self.scaling = alpha / r + + def mock_merge_and_unload(progressbar=False): + """Simulate the actual merge operation""" + # Apply LoRA delta to base weights: W_new = W_base + (B @ A) * scaling + delta_q = (self.lora_B_q @ self.lora_A_q) * self.scaling + delta_v = (self.lora_B_v @ self.lora_A_v) * self.scaling + + base_model.q_proj.weight = self.original_q_weight + delta_q + base_model.v_proj.weight = self.original_v_weight + delta_v + + return base_model + + mock_lora_model.merge_and_unload = mock_merge_and_unload + return mock_lora_model + + def test_basic_lora_merge_unmerge_cycle(self): + """Test: original_weights -> merge -> unmerge -> should equal original_weights""" + + base_model = self.create_mock_base_model() + lora_model = self.create_mock_lora_model(base_model) + + original_q_weight = self.original_q_weight.clone() + original_v_weight = self.original_v_weight.clone() + + merged_model = lora_model.merge_and_unload() + + assert not torch.equal(merged_model.q_proj.weight, original_q_weight) + assert not torch.equal(merged_model.v_proj.weight, original_v_weight) + + delta_q = (self.lora_B_q @ self.lora_A_q) * self.scaling + delta_v = (self.lora_B_v @ self.lora_A_v) * self.scaling + + unmerged_q_weight = merged_model.q_proj.weight - delta_q + unmerged_v_weight = merged_model.v_proj.weight - delta_v + + assert torch.allclose(unmerged_q_weight, original_q_weight, atol=1e-6) + assert torch.allclose(unmerged_v_weight, original_v_weight, atol=1e-6) + + def test_merge_weight_calculation_accuracy(self): + """Test: merged_weight = base_weight + (lora_B @ lora_A * scaling)""" + base_model = self.create_mock_base_model() + lora_model = self.create_mock_lora_model(base_model, r=16, alpha=32) + + expected_delta_q = (self.lora_B_q @ self.lora_A_q) * self.scaling + expected_merged_q = self.original_q_weight + expected_delta_q + merged_model = lora_model.merge_and_unload() + + assert torch.allclose(merged_model.q_proj.weight, expected_merged_q, atol=1e-6) + + @patch("axolotl.cli.merge_lora.load_model_and_tokenizer") + def test_cli_do_merge_functionality(self, mock_load_model, tmp_path): + base_model = self.create_mock_base_model() + lora_model = self.create_mock_lora_model(base_model) + tokenizer = Mock() + processor = None + + mock_load_model.return_value = (lora_model, tokenizer, processor) + + cfg = DictDefault( + { + "save_safetensors": True, + "torch_dtype": torch.float32, + "local_rank": 0, + "output_dir": str(tmp_path), + } + ) + + with ( + patch("pathlib.Path.mkdir"), + patch.object(base_model, "save_pretrained") as mock_save_model, + patch.object(tokenizer, "save_pretrained") as mock_save_tokenizer, + ): + do_merge_lora(cfg=cfg) + + mock_save_model.assert_called_once() + mock_save_tokenizer.assert_called_once() + + def test_quantized_model_merge_compatibility(self): + """Test 4-bit/8-bit model merging scenarios""" + base_model = self.create_mock_base_model() + + # Mock quantized weights + base_model.q_proj.weight.quant_state = Mock() + base_model.q_proj.weight.quant_state.dtype = torch.uint8 + + lora_model = self.create_mock_lora_model(base_model) + + merged_model = lora_model.merge_and_unload() + assert merged_model is not None + + @patch.dict("os.environ", {"CUDA_VISIBLE_DEVICES": ""}) + def test_memory_efficient_merge_with_cpu_offload(self, tmp_path): + """Test lora_on_cpu configuration during merge""" + cfg = DictDefault( + { + "lora_on_cpu": True, + "save_safetensors": True, + "output_dir": str(tmp_path), + "local_rank": 0, + } + ) + + with patch("axolotl.cli.merge_lora.load_model_and_tokenizer") as mock_load: + base_model = self.create_mock_base_model() + lora_model = self.create_mock_lora_model(base_model) + mock_load.return_value = (lora_model, Mock(), None) + + with patch("pathlib.Path.mkdir"), patch("torch.save"): + do_merge_lora(cfg=cfg) + + assert mock_load.called From 4ae6f766ad723104265ad7967d68b5ea76695a2e Mon Sep 17 00:00:00 2001 From: salman Date: Mon, 12 Jan 2026 15:42:04 +0100 Subject: [PATCH 1087/1405] bump bnb to v0.49.1 (#3351) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f69135902c..18ba0a6d12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ # START section of dependencies that don't install on Darwin/MacOS -bitsandbytes==0.48.2 +bitsandbytes==0.49.1 triton>=3.0.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 From 3e0bbd33ece5cb03abf18b8258ec1b6af8468e02 Mon Sep 17 00:00:00 2001 From: "@TT" <1sand0s@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:00:02 -0600 Subject: [PATCH 1088/1405] feat: add ARM64/AArch64 build support to Dockerfile-base (#3346) * Add support for capability to build arm64 image * Fixing wrong variable TARGETPLATFORM bug * Adding missing semicolons * skip docker hub login if PR (no push) or no credentials * Enabling arm64 builds for Dockerfile-base in Github actions * TARGETARCH automatically default to platform arch under build * Enabling arm64 builds for axolotl docker builds * Enabling arm64 builds for axolotl-cloud docker build Github actions --------- Co-authored-by: Wing Lian --- .github/workflows/base.yml | 2 ++ .github/workflows/main.yml | 3 +++ docker/Dockerfile-base | 46 +++++++++++++++++++++++++++++--------- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index ea721bff4d..260215ef64 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -79,6 +79,7 @@ jobs: axolotlai/axolotl-base - name: Login to Docker Hub uses: docker/login-action@v2 + if: ${{ github.event_name != 'pull_request' && secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -89,6 +90,7 @@ jobs: with: context: . file: ./docker/${{ matrix.dockerfile }} + platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.metadata.outputs.tags }}-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} labels: ${{ steps.metadata.outputs.labels }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 052f9aa72f..19cef5de49 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,6 +61,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . + platforms: linux/amd64,linux/arm64 build-args: | BASE_TAG=${{ github.ref_type == 'tag' && 'main' || github.ref_name }}-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }} CUDA=${{ matrix.cuda }} @@ -127,6 +128,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . + platforms: linux/amd64,linux/arm64 build-args: | BASE_TAG=${{ github.ref_type == 'tag' && 'main' || github.ref_name }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} CUDA=${{ matrix.cuda }} @@ -180,6 +182,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . + platforms: linux/amd64,linux/arm64 build-args: | BASE_TAG=${{ github.ref_type == 'tag' && 'main' || github.ref_name }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} CUDA=${{ matrix.cuda }} diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index e080758c2e..96367207fb 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -2,11 +2,13 @@ ARG CUDA_VERSION="11.8.0" ARG CUDNN_VERSION="8" ARG UBUNTU_VERSION="22.04" ARG MAX_JOBS=4 +ARG TARGETARCH FROM nvidia/cuda:$CUDA_VERSION-cudnn$CUDNN_VERSION-devel-ubuntu$UBUNTU_VERSION AS base-builder ENV PATH="/root/miniconda3/bin:${PATH}" +ARG TARGETARCH ARG PYTHON_VERSION="3.11" ARG PYTORCH_VERSION="2.1.2" ARG CUDA="128" @@ -22,11 +24,17 @@ RUN apt-get update \ librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm \ && rm -rf /var/cache/apt/archives \ && rm -rf /var/lib/apt/lists/* \ - && wget \ - https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \ + && if [ "$TARGETARCH" = "amd64" ]; then \ + MINICONDA_ARCH="x86_64"; \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + MINICONDA_ARCH="aarch64"; \ + else \ + echo "Unsupported architecture: $TARGETARCH"; exit 1; \ + fi \ + && wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-${MINICONDA_ARCH}.sh \ && mkdir /root/.conda \ - && bash Miniconda3-latest-Linux-x86_64.sh -b \ - && rm -f Miniconda3-latest-Linux-x86_64.sh \ + && bash Miniconda3-latest-Linux-${MINICONDA_ARCH}.sh -b \ + && rm -f Miniconda3-latest-Linux-${MINICONDA_ARCH}.sh \ && conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main \ && conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r \ && conda create -n "py${PYTHON_VERSION}" python="${PYTHON_VERSION}" @@ -54,13 +62,31 @@ RUN git lfs install --skip-repo && \ RUN case "$PYTORCH_VERSION" in \ 2.9.[0-9]*) \ if [ "$CUDA" = "128" ]; then \ - wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - pip3 install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + if [ "$TARGETARCH" = "amd64" ]; then \ + WHL_FILE="flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl"; \ + WHL_VERSION="v0.5.4"; \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + WHL_FILE="flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_aarch64.whl"; \ + WHL_VERSION="v0.6.4"; \ + else \ + echo "Unsupported architecture: $TARGETARCH"; exit 1; \ + fi; \ + wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/${WHL_VERSION}/${WHL_FILE}; \ + pip3 install --no-cache-dir ${WHL_FILE}; \ + rm ${WHL_FILE}; \ elif [ "$CUDA" = "130" ]; then \ - wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ - pip3 install --no-cache-dir flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ - rm flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ + if [ "$TARGETARCH" = "amd64" ]; then \ + WHL_FILE="flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl"; \ + WHL_VERSION="v0.5.4"; \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + WHL_FILE="flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_aarch64.whl"; \ + WHL_VERSION="v0.6.4"; \ + else \ + echo "Unsupported architecture: $TARGETARCH"; exit 1; \ + fi; \ + wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/${WHL_VERSION}/${WHL_FILE}; \ + pip3 install --no-cache-dir ${WHL_FILE}; \ + rm ${WHL_FILE}; \ fi \ ;; \ esac From 258ce8d4fa04c27ab49e169ba7751d8f4f1a423e Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:03:11 +0530 Subject: [PATCH 1089/1405] feat : scaled softmax support (#3338) * scaled softmax * comment * lint * remove egear * validation for flash * lint * val imporve + neet * fix correct softmax scale val(learned) * learned scale val 4 ssm * lint * fix model_type rmv * sdpa_atten * test fix + lint * test fix * sdp_a val rmv * flex fix * main flash * lint * flex attn * lint comment * fix score_mod * Update src/axolotl/utils/schemas/validation.py Co-authored-by: NanoCode012 --------- Co-authored-by: Ved Co-authored-by: NanoCode012 --- examples/devstral/devstral-small-qlora.yml | 1 + examples/ministral3/ministral3-3b-qlora.yaml | 1 + src/axolotl/loaders/patch_manager.py | 14 ++ .../monkeypatch/scaled_softmax_attn.py | 141 ++++++++++++++++++ src/axolotl/utils/schemas/config.py | 19 +++ src/axolotl/utils/schemas/validation.py | 10 ++ 6 files changed, 186 insertions(+) create mode 100644 src/axolotl/monkeypatch/scaled_softmax_attn.py diff --git a/examples/devstral/devstral-small-qlora.yml b/examples/devstral/devstral-small-qlora.yml index 7fe4dd4335..ca8e8e0432 100644 --- a/examples/devstral/devstral-small-qlora.yml +++ b/examples/devstral/devstral-small-qlora.yml @@ -52,6 +52,7 @@ gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 flash_attention: true +scaling_softmax: true loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/ministral3/ministral3-3b-qlora.yaml b/examples/ministral3/ministral3-3b-qlora.yaml index a31545ab2e..b369c9d416 100644 --- a/examples/ministral3/ministral3-3b-qlora.yaml +++ b/examples/ministral3/ministral3-3b-qlora.yaml @@ -59,6 +59,7 @@ gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 flash_attention: true +scaling_softmax: true warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 64f363bb12..b7a53c4d5e 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -138,6 +138,7 @@ def apply_post_model_load_patches(self, model: PreTrainedModel): self._apply_llama_flash_attn_patches(model) self._apply_unsloth_patches(model) self._apply_lora_kernel_patch(model) + self._apply_scaling_softmax_patch(model) def _apply_flash_attention_patches(self): """Apply patches related to Flash Attention.""" @@ -560,3 +561,16 @@ def _apply_apertus_patches(self): ) patch_apertus_xielu_activation() + + def _apply_scaling_softmax_patch(self, model: PreTrainedModel): + """Apply Scaling Softmax (SSMax) patch. Ref: https://arxiv.org/abs/2501.19399""" + if self.cfg.scaling_softmax: + from axolotl.monkeypatch.scaled_softmax_attn import ( + patch_scaled_softmax_attention, + ) + + patch_scaled_softmax_attention( + scaling_factor_init=self.cfg.scaling_softmax_factor or 0.43, + bias=self.cfg.scaling_softmax_bias or 0.0, + model=model, + ) diff --git a/src/axolotl/monkeypatch/scaled_softmax_attn.py b/src/axolotl/monkeypatch/scaled_softmax_attn.py new file mode 100644 index 0000000000..bb2ebb8b82 --- /dev/null +++ b/src/axolotl/monkeypatch/scaled_softmax_attn.py @@ -0,0 +1,141 @@ +""" +Scaled Softmax (SSMax) attention patch using FlexAttention. +SSMax: softmax(scores * s * log(n) + b) where n is the position index +Ref: https://arxiv.org/abs/2501.19399 +""" + +import torch +from transformers import PreTrainedModel + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +try: + from torch.nn.attention.flex_attention import BlockMask + from transformers.integrations.flex_attention import ( + compile_friendly_flex_attention, + repeat_kv, + ) + + FLEX_ATTENTION_AVAILABLE = True +except ImportError: + FLEX_ATTENTION_AVAILABLE = False + BlockMask = None + +_ssmax_config = {} + + +def patch_scaled_softmax_attention( + scaling_factor_init: float = 0.43, bias: float = 0.0, model: PreTrainedModel = None +): + """Patch attention to apply SSMax via FlexAttention score_mod.""" + global _ssmax_config + + if not FLEX_ATTENTION_AVAILABLE: + raise RuntimeError("SSMax requires FlexAttention.") + + _ssmax_config["ssmax_s"] = scaling_factor_init + _ssmax_config["ssmax_b"] = bias + + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + if "flex_attention" in ALL_ATTENTION_FUNCTIONS: + _ssmax_config["original_flex_fn"] = ALL_ATTENTION_FUNCTIONS["flex_attention"] + ALL_ATTENTION_FUNCTIONS["flex_attention"] = ssmax_flex_attention_forward + LOG.info( + f"Patched flex_attention with SSMax (s={scaling_factor_init}, b={bias})" + ) + else: + LOG.warning("flex_attention not found. Ensure flex_attention: true is set.") + + +def ssmax_flex_attention_forward( + module: torch.nn.Module, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + attention_mask, + scaling: float | None = None, + softcap: float | None = None, + **kwargs, +) -> tuple[torch.Tensor, torch.Tensor | None]: + """FlexAttention forward with SSMax: score * (s * log(n) + b).""" + + if kwargs.get("dropout", 0.0) > 0: + raise ValueError("flex_attention does not support dropout") + + ssmax_s = _ssmax_config.get("ssmax_s", 0.43) + ssmax_b = _ssmax_config.get("ssmax_b", 0.0) + + position_ids = kwargs.get("position_ids", None) + position_ids_flat = position_ids.view(-1) if position_ids is not None else None + + block_mask = attention_mask if isinstance(attention_mask, BlockMask) else None + score_mask = None if block_mask else attention_mask + + if score_mask is not None: + score_mask = score_mask[:, :, :, : key.shape[-2]] + + def score_mod(score, batch_idx, head_idx, q_idx, kv_idx): + """ + Apply SSMax scaling: score * (s * log(n) + b) + where n is the relative position within each packed sequence. + """ + if position_ids_flat is not None: + relative_pos = position_ids_flat[q_idx] + n = (relative_pos + 1).float() + else: + n = (q_idx + 1).float() + + n = torch.clamp(n, min=2.0) + + ssmax_scale = ssmax_s * torch.log(n) + ssmax_b + score = score * ssmax_scale + + if softcap is not None: + score = softcap * torch.tanh(score / softcap) + + if score_mask is not None: + score = score + score_mask[batch_idx][0][q_idx][kv_idx] + + return score + + enable_gqa = True + if (query.shape[1] & (query.shape[1] - 1)) != 0: + key = repeat_kv(key, query.shape[1] // key.shape[1]) + value = repeat_kv(value, query.shape[1] // value.shape[1]) + enable_gqa = False + + return_lse = query.device.type != "cpu" + flex_output = compile_friendly_flex_attention( + query, + key, + value, + score_mod=score_mod, + block_mask=block_mask, + enable_gqa=enable_gqa, + scale=scaling, + kernel_options=kwargs.get("kernel_options"), + return_lse=return_lse, + training=module.training, + ) + + if return_lse: + attention_output, lse = flex_output + lse = lse.to(value.dtype) + else: + attention_output, lse = flex_output, None + + return attention_output.transpose(1, 2).contiguous(), lse + + +def unpatch_scaled_softmax_attention(): + """Restore the original FlexAttention function.""" + global _ssmax_config + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + if "original_flex_fn" in _ssmax_config: + ALL_ATTENTION_FUNCTIONS["flex_attention"] = _ssmax_config["original_flex_fn"] + _ssmax_config.clear() + LOG.info("Unpatched flex_attention, restored original") diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 4ef1aff3a1..da21df7aa6 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -619,6 +619,25 @@ class AxolotlInputConfig( }, ) + scaling_softmax: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use Scaled Softmax (SSMax) attention. Ref: https://arxiv.org/abs/2501.19399" + }, + ) + scaling_softmax_factor: float | None = Field( + default=None, + json_schema_extra={ + "description": "Scaling factor for SSMax attention. Default is 0.43" + }, + ) + scaling_softmax_bias: float | None = Field( + default=None, + json_schema_extra={ + "description": "Bias for SSMax attention. Default is 0.0. Note: The paper recommends bias=0 for better length generalization." + }, + ) + unsloth_cross_entropy_loss: bool | None = None unsloth_lora_mlp: bool | None = None unsloth_lora_qkv: bool | None = None diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index cb834a3bf8..bf054d3531 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -201,6 +201,16 @@ def check_sample_packing_with_s2attn(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_scaling_softmax_requires_flex(cls, data): + if data.get("scaling_softmax") and not data.get("flex_attention"): + raise ValueError( + "scaling_softmax requires flex_attention: true\n" + "Add 'flex_attention: true' to your config file.\n" + ) + return data + class TrainingValidationMixin: """Validation methods related to training configuration.""" From 359b7ad85e2f2ad5243f61387bc62d316026b5d0 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 13 Jan 2026 21:49:23 +0700 Subject: [PATCH 1090/1405] fix: gemma3_text model loading vision config (#3354) * fix: gemma3-text mode loading vision config * fix: improve defaults to use lora kernels --- examples/gemma3/gemma-3-1b-qlora.yml | 3 ++- examples/gemma3/gemma-3-270m-qlora.yml | 3 ++- examples/gemma3/gemma-3-4b-qlora.yml | 5 +++-- examples/gemma3/gemma-3-4b-vision-qlora.yml | 2 +- src/axolotl/loaders/utils.py | 11 ++++++++++- src/axolotl/utils/schemas/model.py | 7 ++++++- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/examples/gemma3/gemma-3-1b-qlora.yml b/examples/gemma3/gemma-3-1b-qlora.yml index 2f998d144e..d84368bc09 100644 --- a/examples/gemma3/gemma-3-1b-qlora.yml +++ b/examples/gemma3/gemma-3-1b-qlora.yml @@ -1,6 +1,7 @@ base_model: google/gemma-3-1b-it model_type: Gemma3ForCausalLM +cls_model_config: Gemma3TextConfig # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name @@ -29,7 +30,7 @@ output_dir: ./outputs/out adapter: qlora lora_r: 32 lora_alpha: 16 -lora_dropout: 0.05 +lora_dropout: 0 lora_target_linear: true sequence_len: 2048 diff --git a/examples/gemma3/gemma-3-270m-qlora.yml b/examples/gemma3/gemma-3-270m-qlora.yml index 0c60c4a014..14ea2aabaa 100644 --- a/examples/gemma3/gemma-3-270m-qlora.yml +++ b/examples/gemma3/gemma-3-270m-qlora.yml @@ -1,6 +1,7 @@ base_model: google/gemma-3-270m-it model_type: Gemma3ForCausalLM +cls_model_config: Gemma3TextConfig # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name @@ -29,7 +30,7 @@ output_dir: ./outputs/out adapter: qlora lora_r: 32 lora_alpha: 16 -lora_dropout: 0.05 +lora_dropout: 0 lora_target_linear: true sequence_len: 2048 diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml index 9595211497..7d44f3c9b0 100644 --- a/examples/gemma3/gemma-3-4b-qlora.yml +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -2,6 +2,7 @@ base_model: google/gemma-3-4b-it # Need to set else transformers tries to load vision too model_type: Gemma3ForCausalLM +cls_model_config: Gemma3TextConfig load_in_4bit: true @@ -32,8 +33,8 @@ sample_packing: true lora_r: 32 lora_alpha: 16 -lora_dropout: 0.05 -lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' +lora_dropout: 0 +lora_target_linear: true wandb_project: wandb_entity: diff --git a/examples/gemma3/gemma-3-4b-vision-qlora.yml b/examples/gemma3/gemma-3-4b-vision-qlora.yml index b42b6b4928..a12e84beee 100644 --- a/examples/gemma3/gemma-3-4b-vision-qlora.yml +++ b/examples/gemma3/gemma-3-4b-vision-qlora.yml @@ -31,7 +31,7 @@ pad_to_sequence_len: false lora_r: 32 lora_alpha: 16 -lora_dropout: 0.05 +lora_dropout: 0 lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' wandb_project: diff --git a/src/axolotl/loaders/utils.py b/src/axolotl/loaders/utils.py index b1902c9b54..187784b930 100644 --- a/src/axolotl/loaders/utils.py +++ b/src/axolotl/loaders/utils.py @@ -5,6 +5,7 @@ import addict import torch +import transformers from transformers import AutoConfig, PretrainedConfig, PreTrainedModel from axolotl.utils.dict import DictDefault @@ -153,6 +154,9 @@ def load_model_config(cfg: DictDefault) -> PretrainedConfig | addict.Dict: This function determines the appropriate model config source, loads it, applies any necessary overrides, and validates it for compatibility with the `axolotl` config. + If `cfg.cls_model_config` is set, a custom config class from transformers will be + used instead of `AutoConfig` (e.g., 'LlamaConfig', 'MistralConfig'). + Args: cfg: Dictionary mapping `axolotl` config keys to values. @@ -174,8 +178,13 @@ def load_model_config(cfg: DictDefault) -> PretrainedConfig | addict.Dict: if cfg.num_labels: # num_labels is used to initialize classifier models config_kwargs["num_labels"] = cfg.num_labels + + config_cls = AutoConfig + if cfg.cls_model_config: + config_cls = getattr(transformers, cfg.cls_model_config) + try: - model_config = AutoConfig.from_pretrained( + model_config = config_cls.from_pretrained( model_config_name, trust_remote_code=trust_remote_code, **config_kwargs, diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index 04312eeddc..0931608a69 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -25,7 +25,12 @@ class ModelInputConfig(BaseModel): "description": "If the base_model repo on hf hub doesn't include configuration .json files, You can set that here, or leave this empty to default to base_model" }, ) - cls_model_config: str | None = None + cls_model_config: str | None = Field( + default=None, + json_schema_extra={ + "description": "transformers config class (e.g., 'LlamaConfig', 'MistralConfig'). Defaults to AutoConfig." + }, + ) tokenizer_config: str | None = Field( default=None, json_schema_extra={ From dc77b5bf4215086739e00c335c1dd826721b1dcb Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 14 Jan 2026 09:38:48 -0500 Subject: [PATCH 1091/1405] fix arm64 builds (#3355) * fix syntax for secrets in gha yaml * setup env for uv too * arm64 for base uv too * don't build causal-conv1d or mamba for arm64 and use arm64 wheels * fix dockerfile syntax * fix shell syntax --- .github/workflows/base.yml | 18 ++++++++++++++++-- docker/Dockerfile-uv-base | 36 ++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 260215ef64..dd294ecd92 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -21,6 +21,8 @@ jobs: timeout-minutes: 480 # this job needs to be run on self-hosted GPU runners... runs-on: ubuntu-latest-m + env: + HAS_DOCKERHUB_CREDS: ${{ secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }} strategy: fail-fast: false matrix: @@ -32,6 +34,7 @@ jobs: pytorch: 2.8.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" + platforms: "linux/amd64" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" @@ -39,6 +42,7 @@ jobs: pytorch: 2.9.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" + platforms: "linux/amd64,linux/arm64" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" @@ -46,6 +50,7 @@ jobs: pytorch: 2.9.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" + platforms: "linux/amd64,linux/arm64" - cuda: "130" cuda_version: 13.0.0 cudnn_version: "" @@ -53,6 +58,7 @@ jobs: pytorch: 2.9.1 torch_cuda_arch_list: "9.0+PTX" dockerfile: "Dockerfile-base" + platforms: "linux/amd64,linux/arm64" # - cuda: "128" # cuda_version: 12.8.1 # cudnn_version: "" @@ -79,7 +85,7 @@ jobs: axolotlai/axolotl-base - name: Login to Docker Hub uses: docker/login-action@v2 - if: ${{ github.event_name != 'pull_request' && secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }} + if: ${{ github.event_name != 'pull_request' && env.HAS_DOCKERHUB_CREDS == 'true' }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -90,7 +96,7 @@ jobs: with: context: . file: ./docker/${{ matrix.dockerfile }} - platforms: linux/amd64,linux/arm64 + platforms: ${{ matrix.platforms }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.metadata.outputs.tags }}-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} labels: ${{ steps.metadata.outputs.labels }} @@ -105,6 +111,8 @@ jobs: if: ${{ github.repository_owner == 'axolotl-ai-cloud' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} timeout-minutes: 480 runs-on: ubuntu-latest-m + env: + HAS_DOCKERHUB_CREDS: ${{ secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }} strategy: fail-fast: false matrix: @@ -116,6 +124,7 @@ jobs: pytorch: 2.8.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" + platforms: "linux/amd64" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" @@ -123,6 +132,7 @@ jobs: pytorch: 2.9.1 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" + platforms: "linux/amd64,linux/arm64" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" @@ -130,6 +140,7 @@ jobs: pytorch: 2.9.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" + platforms: "linux/amd64,linux/arm64" - cuda: "130" cuda_version: 13.0.0 cudnn_version: "" @@ -137,6 +148,7 @@ jobs: pytorch: 2.9.1 torch_cuda_arch_list: "9.0+PTX" dockerfile: "Dockerfile-uv-base" + platforms: "linux/amd64,linux/arm64" steps: - name: Checkout uses: actions/checkout@v4 @@ -148,6 +160,7 @@ jobs: axolotlai/axolotl-base-uv - name: Login to Docker Hub uses: docker/login-action@v2 + if: ${{ github.event_name != 'pull_request' && env.HAS_DOCKERHUB_CREDS == 'true' }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -158,6 +171,7 @@ jobs: with: context: . file: ./docker/${{ matrix.dockerfile }} + platforms: ${{ matrix.platforms }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.metadata.outputs.tags }}-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} labels: ${{ steps.metadata.outputs.labels }} diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index 1b54c05e66..d28b27ad2e 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -2,6 +2,7 @@ ARG CUDA_VERSION="12.6.3" ARG CUDNN_VERSION="" ARG UBUNTU_VERSION="22.04" ARG MAX_JOBS=4 +ARG TARGETARCH FROM nvidia/cuda:$CUDA_VERSION-cudnn$CUDNN_VERSION-devel-ubuntu$UBUNTU_VERSION AS base-builder @@ -31,20 +32,35 @@ ENV PATH="/workspace/axolotl-venv/bin:${PATH}" RUN uv pip install packaging setuptools wheel psutil \ && uv pip install torch==${PYTORCH_VERSION} torchvision \ - && uv pip install --no-build-isolation "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" \ - && uv pip install "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" \ && uv pip install awscli pydantic +RUN if [ "$TARGETARCH" = "amd64" ]; then \ + uv pip install --no-build-isolation "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main"; \ + uv pip install "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main"; \ + fi + RUN case "$PYTORCH_VERSION" in \ 2.9.[0-9]*) \ - if [ "$CUDA" = "128" ]; then \ - wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - uv pip install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - elif [ "$CUDA" = "130" ]; then \ - wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ - uv pip install --no-cache-dir flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ - rm flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ + if [ "$TARGETARCH" = "amd64" ]; then \ + if [ "$CUDA" = "128" ]; then \ + wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + uv pip install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ + elif [ "$CUDA" = "130" ]; then \ + wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ + uv pip install --no-cache-dir flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ + rm flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ + fi \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + if [ "$CUDA" = "128" ]; then \ + wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.6.4/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_aarch64.whl; \ + uv pip install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_aarch64.whl; \ + rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_aarch64.whl; \ + elif [ "$CUDA" = "130" ]; then \ + wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.6.4/flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_aarch64.whl; \ + uv pip install --no-cache-dir flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_aarch64.whl; \ + rm flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_aarch64.whl; \ + fi \ fi \ ;; \ esac From 1410e4474e0ca0bdc751922aa3396be06ae93bd1 Mon Sep 17 00:00:00 2001 From: salman Date: Wed, 14 Jan 2026 15:39:21 +0100 Subject: [PATCH 1092/1405] update PR template (#3349) [skip ci] --- .github/PULL_REQUEST_TEMPLATE.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 562806287a..cc0ade6653 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -15,6 +15,11 @@ +## AI Usage Disclaimer + + + + ## Screenshots (if appropriate) ## Types of changes From 6331e4a1306c6b6e223d7897e143e19cbb656340 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 14 Jan 2026 11:56:36 -0500 Subject: [PATCH 1093/1405] fix amd64 and set 2.9.1 as latest cloud image (#3356) --- .github/workflows/main.yml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 19cef5de49..d8ab921895 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,22 +20,26 @@ jobs: python_version: "3.11" pytorch: 2.8.0 axolotl_extras: - is_latest: true + platforms: "linux/amd64" - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" pytorch: 2.9.0 axolotl_extras: + platforms: "linux/amd64,linux/arm64" - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" pytorch: 2.9.1 axolotl_extras: + platforms: "linux/amd64,linux/arm64" + is_latest: true - cuda: 130 cuda_version: 13.0.0 python_version: "3.11" pytorch: 2.9.1 axolotl_extras: + platforms: "linux/amd64,linux/arm64" runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -61,7 +65,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: ${{ matrix.platforms }} build-args: | BASE_TAG=${{ github.ref_type == 'tag' && 'main' || github.ref_name }}-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }} CUDA=${{ matrix.cuda }} @@ -88,22 +92,26 @@ jobs: python_version: "3.11" pytorch: 2.8.0 axolotl_extras: - is_latest: true + platforms: "linux/amd64" - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" pytorch: 2.9.0 axolotl_extras: + platforms: "linux/amd64,linux/arm64" - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" pytorch: 2.9.1 axolotl_extras: + is_latest: true + platforms: "linux/amd64,linux/arm64" - cuda: 130 cuda_version: 13.0.0 python_version: "3.11" pytorch: 2.9.1 axolotl_extras: + platforms: "linux/amd64,linux/arm64" runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -128,7 +136,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: ${{ matrix.platforms }} build-args: | BASE_TAG=${{ github.ref_type == 'tag' && 'main' || github.ref_name }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} CUDA=${{ matrix.cuda }} @@ -149,11 +157,11 @@ jobs: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.8.0 + pytorch: 2.9.1 axolotl_extras: - is_latest: - - cuda: 128 - cuda_version: 12.8.1 + is_latest: true + - cuda: 130 + cuda_version: 13.0.0 python_version: "3.11" pytorch: 2.9.1 axolotl_extras: From d282f324815bd8d312cbe94c05859a08cf28a12c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 14 Jan 2026 12:03:55 -0500 Subject: [PATCH 1094/1405] don't install deepspeed in arm64 images (#3357) --- docker/Dockerfile | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 116361dcdb..d80cede55b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,6 +6,7 @@ ARG AXOLOTL_EXTRAS="" ARG AXOLOTL_ARGS="" ARG CUDA="118" ARG PYTORCH_VERSION="2.1.2" +ARG TARGETARCH ENV PYTORCH_VERSION=$PYTORCH_VERSION @@ -20,13 +21,17 @@ RUN git clone --depth=1 https://github.com/axolotl-ai-cloud/axolotl.git WORKDIR /workspace/axolotl -# If AXOLOTL_EXTRAS is set, append it in brackets -RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ +# If AXOLOTL_EXTRAS is set, append it in brackets; don't install deepspeed with arm64 +RUN if [ "$TARGETARCH" = "arm64" ]; then \ + BASE_EXTRAS="flash-attn,ring-flash-attn,optimizers,ray"; \ else \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray] $AXOLOTL_ARGS; \ + BASE_EXTRAS="deepspeed,flash-attn,ring-flash-attn,optimizers,ray"; \ fi && \ - python scripts/unsloth_install.py | sh && \ + if [ "$AXOLOTL_EXTRAS" != "" ]; then \ + pip install --no-build-isolation -e .[$BASE_EXTRAS,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + else \ + pip install --no-build-isolation -e .[$BASE_EXTRAS] $AXOLOTL_ARGS; \ + fi && \ python scripts/unsloth_install.py | sh && \ python scripts/cutcrossentropy_install.py | sh && \ pip install pytest && \ pip cache purge From 790df757cb40bf796ad143e010064b1fddff6f06 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 16 Jan 2026 09:02:37 -0500 Subject: [PATCH 1095/1405] don't install xformers in for arm64 (#3359) * install xformers in the base docker image * install numba and numpy first * set CUDA_HOME for xformers install * Set cuda home env * don't install xformers by default on aarch64/arm64 --- setup.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/setup.py b/setup.py index 10c9a84539..5f51dbee0f 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ def parse_requirements(extras_require_map): _install_requires.append(line) try: xformers_version = [req for req in _install_requires if "xformers" in req][0] + install_xformers = platform.machine() != "aarch64" if "Darwin" in platform.system(): # skip packages not compatible with OSX skip_packages = [ @@ -66,40 +67,49 @@ def parse_requirements(extras_require_map): extras_require_map.pop("fbgemm-gpu") extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.4.1"] extras_require_map["vllm"] = ["vllm==0.11.1"] + if not install_xformers: + _install_requires.pop(_install_requires.index(xformers_version)) elif (major, minor) >= (2, 8): extras_require_map.pop("fbgemm-gpu") extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.3.0"] extras_require_map["vllm"] = ["vllm==0.11.0"] + if not install_xformers: + _install_requires.pop(_install_requires.index(xformers_version)) elif (major, minor) >= (2, 7): _install_requires.pop(_install_requires.index(xformers_version)) if patch == 0: - _install_requires.append("xformers==0.0.30") + if install_xformers: + _install_requires.append("xformers==0.0.30") # vllm 0.9.x is incompatible with latest transformers extras_require_map.pop("vllm") else: - _install_requires.append("xformers==0.0.31") + if install_xformers: + _install_requires.append("xformers==0.0.31") extras_require_map["vllm"] = ["vllm==0.10.1"] elif (major, minor) >= (2, 6): _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers==0.0.29.post3") + if install_xformers: + _install_requires.append("xformers==0.0.29.post3") # since we only support 2.6.0+cu126 _dependency_links.append("https://download.pytorch.org/whl/cu126") extras_require_map.pop("vllm") elif (major, minor) >= (2, 5): _install_requires.pop(_install_requires.index(xformers_version)) - if patch == 0: - _install_requires.append("xformers==0.0.28.post2") - else: - _install_requires.append("xformers>=0.0.28.post3") + if install_xformers: + if patch == 0: + _install_requires.append("xformers==0.0.28.post2") + else: + _install_requires.append("xformers>=0.0.28.post3") extras_require_map.pop("vllm") elif (major, minor) >= (2, 4): extras_require_map.pop("vllm") - if patch == 0: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.27") - else: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers==0.0.28.post1") + if install_xformers: + if patch == 0: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.27") + else: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers==0.0.28.post1") else: raise ValueError("axolotl requires torch>=2.4") From 8f2512426927ae62f69ab7e6e6ab219c26001da7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 16 Jan 2026 11:17:43 -0500 Subject: [PATCH 1096/1405] upgrade transformers to 4.57.5 (#3358) * upgrade transformers to 4.57.5 * explicitly set versions for fbgemm-gpu * handle index url for cuda version * explicitly set cu version for fbgemm deps, skip for 130 * cu suffix not needed on version if using whl subpath --- .github/workflows/multi-gpu-e2e.yml | 3 ++- requirements.txt | 2 +- setup.py | 12 +++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 745e177bbb..833dc4f29a 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -47,7 +47,8 @@ jobs: cuda_version: 13.0.0 python_version: "3.11" pytorch: 2.9.1 - axolotl_extras: fbgemm-gpu + axolotl_extras: +# axolotl_extras: fbgemm-gpu num_gpus: 2 nightly_build: "true" runs-on: [self-hosted, modal] diff --git a/requirements.txt b/requirements.txt index 18ba0a6d12..285792611f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ packaging==23.2 huggingface_hub>=0.36.0 peft>=0.18.0 tokenizers>=0.22.1 -transformers==4.57.1 +transformers==4.57.5 accelerate==1.12.0 datasets==4.4.2 deepspeed>=0.18.3 diff --git a/setup.py b/setup.py index 5f51dbee0f..d98c7e19a3 100644 --- a/setup.py +++ b/setup.py @@ -63,9 +63,19 @@ def parse_requirements(extras_require_map): else: raise ValueError("Invalid version format") + torch_parts = torch_version.split("+") + if len(torch_parts) == 2: + torch_cuda_version = torch_parts[1] + _dependency_links.append( + f"https://download.pytorch.org/whl/{torch_cuda_version}" + ) + if (major, minor) >= (2, 9): extras_require_map.pop("fbgemm-gpu") - extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.4.1"] + extras_require_map["fbgemm-gpu"] = [ + "fbgemm-gpu==1.4.0", + "fbgemm-gpu-genai==1.4.2", + ] extras_require_map["vllm"] = ["vllm==0.11.1"] if not install_xformers: _install_requires.pop(_install_requires.index(xformers_version)) From c413480b356fbca197fdebd12be485e140c27406 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 16 Jan 2026 11:48:50 -0500 Subject: [PATCH 1097/1405] upgrade transformers to 4.57.6 and peft to 0.17.1 and datasets to 4.5.0 (#3361) --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 285792611f..64fe1b2400 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,11 +11,11 @@ liger-kernel==0.6.4 packaging==23.2 huggingface_hub>=0.36.0 -peft>=0.18.0 +peft>=0.18.1 tokenizers>=0.22.1 -transformers==4.57.5 +transformers==4.57.6 accelerate==1.12.0 -datasets==4.4.2 +datasets==4.5.0 deepspeed>=0.18.3 trl==0.25.1 hf_xet==1.2.0 From 6e42def14b5d78c2fadbbdb296644b8e3ca5db6e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 20 Jan 2026 08:58:32 -0500 Subject: [PATCH 1098/1405] set version to v0.13.1 (#3363) --- src/axolotl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index e08d43cc34..962846302d 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -4,4 +4,4 @@ __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.13.0.dev" +__version__ = "0.13.1" From 8ab9d9ea88e29b954ffd113cb5d3bce45e2d80d8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 20 Jan 2026 22:58:29 -0500 Subject: [PATCH 1099/1405] Version dev (#3365) --- .github/workflows/pypi.yml | 4 ++-- VERSION | 1 + pyproject.toml | 3 +++ setup.py | 9 ++------- src/axolotl/__init__.py | 6 +++++- 5 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 VERSION diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 24e3c497d7..860a6c81ca 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -48,9 +48,9 @@ jobs: id: tag run: echo ::set-output name=TAG_NAME::$(echo $GITHUB_REF | cut -d / -f 3) - - name: Update version in setup.py + - name: Update version in VERSION file run: | - sed -i -E 's/version="([0-9.]+)",/version="${{ steps.tag.outputs.TAG_NAME }}",/g' setup.py + echo "${{ steps.tag.outputs.TAG_NAME }}" | sed -e's/v//g' > VERSION - name: Build a source dist run: | diff --git a/VERSION b/VERSION new file mode 100644 index 0000000000..80827deea8 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.14.0.dev0 diff --git a/pyproject.toml b/pyproject.toml index 4213bc9633..c5b7deb05e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,9 @@ Repository = "https://github.com/axolotl-ai-cloud/axolotl.git" py-modules = ["setuptools_axolotl_dynamic_dependencies"] include-package-data = true +[tool.setuptools.dynamic] +version = { file = "VERSION" } + [tool.setuptools.cmdclass] build_py = "setuptools_axolotl_dynamic_dependencies.BuildPyCommand" diff --git a/setup.py b/setup.py index d98c7e19a3..101c5b8c4b 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ """setup.py for axolotl""" -import ast import os import platform import re @@ -130,15 +129,11 @@ def parse_requirements(extras_require_map): def get_package_version(): with open( - Path(os.path.dirname(os.path.abspath(__file__))) - / "src" - / "axolotl" - / "__init__.py", + Path(os.path.dirname(os.path.abspath(__file__))) / "VERSION", "r", encoding="utf-8", ) as fin: - version_match = re.search(r"^__version__\s*=\s*(.*)$", fin.read(), re.MULTILINE) - version_ = ast.literal_eval(version_match.group(1)) + version_ = fin.read().strip() return version_ diff --git a/src/axolotl/__init__.py b/src/axolotl/__init__.py index 962846302d..4b9f11bbd0 100644 --- a/src/axolotl/__init__.py +++ b/src/axolotl/__init__.py @@ -1,7 +1,11 @@ """Axolotl - Train and fine-tune large language models""" import pkgutil +from importlib.metadata import PackageNotFoundError, version __path__ = pkgutil.extend_path(__path__, __name__) # Make this a namespace package -__version__ = "0.13.1" +try: + __version__ = version("axolotl") +except PackageNotFoundError: + __version__ = "unknown" From 8cd75cff9f45fa38c894fd777e372cb8ceb61cff Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 21 Jan 2026 13:34:14 -0500 Subject: [PATCH 1100/1405] use cuda 12.9.1 and add python 3.12 to base images (#3367) --- .github/workflows/base.yml | 32 ++++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index dd294ecd92..22125c316b 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -51,6 +51,14 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" platforms: "linux/amd64,linux/arm64" + - cuda: "129" + cuda_version: 12.9.1 + cudnn_version: "" + python_version: "3.12" + pytorch: 2.9.1 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-base" + platforms: "linux/amd64,linux/arm64" - cuda: "130" cuda_version: 13.0.0 cudnn_version: "" @@ -59,6 +67,14 @@ jobs: torch_cuda_arch_list: "9.0+PTX" dockerfile: "Dockerfile-base" platforms: "linux/amd64,linux/arm64" + - cuda: "130" + cuda_version: 13.0.0 + cudnn_version: "" + python_version: "3.12" + pytorch: 2.9.1 + torch_cuda_arch_list: "9.0+PTX" + dockerfile: "Dockerfile-base" + platforms: "linux/amd64,linux/arm64" # - cuda: "128" # cuda_version: 12.8.1 # cudnn_version: "" @@ -141,6 +157,14 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" + - cuda: "129" + cuda_version: 12.9.1 + cudnn_version: "" + python_version: "3.12" + pytorch: 2.9.1 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-uv-base" + platforms: "linux/amd64,linux/arm64" - cuda: "130" cuda_version: 13.0.0 cudnn_version: "" @@ -149,6 +173,14 @@ jobs: torch_cuda_arch_list: "9.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" + - cuda: "130" + cuda_version: 13.0.0 + cudnn_version: "" + python_version: "3.12" + pytorch: 2.9.1 + torch_cuda_arch_list: "9.0+PTX" + dockerfile: "Dockerfile-uv-base" + platforms: "linux/amd64,linux/arm64" steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 10c0e9bf1f..e8d6e1617c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,7 +54,7 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.11"] + python_version: ["3.11", "3.12"] pytorch_version: ["2.8.0", "2.9.0", "2.9.1"] timeout-minutes: 20 From 8623dd8a7251ff35e75620b5ca39e2420d616853 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 21 Jan 2026 14:19:03 -0500 Subject: [PATCH 1101/1405] strip only starting 'v' char; e.g don't strip from '.dev' (#3368) [skip ci] --- .github/workflows/pypi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 860a6c81ca..af2ad73a47 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -50,7 +50,7 @@ jobs: - name: Update version in VERSION file run: | - echo "${{ steps.tag.outputs.TAG_NAME }}" | sed -e's/v//g' > VERSION + echo "${{ steps.tag.outputs.TAG_NAME }}" | sed 's/^v//' > VERSION - name: Build a source dist run: | From d0d26d5064e05be1bc1696c6df45b9a65d2307b6 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Thu, 22 Jan 2026 03:52:45 +0530 Subject: [PATCH 1102/1405] feat: Add GDPO Support (#3353) * gdpo support - test left * lint * fixxes for vllm serv * test advantages * docss * lint * lint = * gdpo simple + lint * lint nit * example * lint * trl 0.27.0 * blocklist * test assert rmv * add validation check for GDPO + sum_then_normalize --------- Co-authored-by: Wing Lian --- docs/rlhf.qmd | 97 ++++ examples/llama-3/qlora-1b-gdpo.yaml | 68 +++ requirements.txt | 2 +- src/axolotl/core/builders/rl.py | 11 +- src/axolotl/core/trainers/grpo/__init__.py | 5 + src/axolotl/utils/data/rl.py | 2 +- src/axolotl/utils/schemas/enums.py | 1 + src/axolotl/utils/schemas/trl.py | 10 + src/axolotl/utils/schemas/validation.py | 13 + tests/core/test_builders.py | 1 - tests/e2e/multigpu/solo/test_gdpo.py | 538 +++++++++++++++++++++ 11 files changed, 742 insertions(+), 6 deletions(-) create mode 100644 examples/llama-3/qlora-1b-gdpo.yaml create mode 100644 tests/e2e/multigpu/solo/test_gdpo.py diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 1eea420367..135b3038ca 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -17,6 +17,7 @@ feedback. Various methods include, but not limited to: - [Kahneman-Tversky Optimization (KTO)](#kto) - [Odds Ratio Preference Optimization (ORPO)](#orpo) - [Group Relative Policy Optimization (GRPO)](#grpo) +- [Group Reward-Decoupled Policy Optimization (GDPO)](#gdpo) ## RLHF using Axolotl @@ -720,6 +721,102 @@ trl: For more information, see [GRPO docs](https://huggingface.co/docs/trl/v0.17.0/en/grpo_trainer#loss-types). +### GDPO + +GDPO (Group Reward-Decoupled Policy Optimization) extends GRPO for multi-reward training. It addresses the **reward advantage collapse** problem by normalizing each reward function independently before combining them. + +::: {.callout-tip} +Use GDPO when training with multiple reward functions. For single reward, GRPO and GDPO produce equivalent results. +::: + +Paper: [https://arxiv.org/pdf/2501.05242](https://arxiv.org/pdf/2501.05242) + +GDPO uses TRL's native `multi_objective_aggregation` parameter under the hood. When you set `rl: gdpo`, axolotl automatically configures TRL to use `normalize_then_sum` aggregation. + +```yaml +base_model: Qwen/Qwen2.5-1.5B-Instruct + +vllm: + host: 0.0.0.0 + port: 8000 + tensor_parallel_size: 2 + gpu_memory_utilization: 0.85 + +rl: gdpo + +trl: + beta: 0.001 + max_completion_length: 256 + use_vllm: true + num_generations: 4 + reward_funcs: + - rewards.format_reward + - rewards.correctness_reward + reward_weights: [1.0, 2.0] + +datasets: + - path: openai/gsm8k + name: main + type: rewards.oai_gsm8k_transform +``` + +You can also use GRPO with explicit aggregation control: + +```yaml +rl: grpo +trl: + multi_objective_aggregation: normalize_then_sum # GDPO behavior + # or: sum_then_normalize # Default GRPO behavior +``` + +#### GDPO vs GRPO + +| Aspect | GRPO | GDPO | +|--------|------|------| +| **Aggregation** | `sum_then_normalize` | `normalize_then_sum` | +| **Multi-reward** | May collapse advantages | Preserves reward signals | +| **Single reward** | Standard behavior | Equivalent to GRPO | + +#### Why GDPO? + +When using multiple rewards with GRPO, different reward combinations can produce identical advantages: + +``` +# Example: format + correctness rewards +[format=0, correct=3] → sum=3 +[format=1, correct=2] → sum=3 ← GRPO sees these as equal! +[format=2, correct=1] → sum=3 +[format=3, correct=0] → sum=3 +``` + +GDPO normalizes each reward independently, preserving their relative differences. + +#### Reward Functions + +GDPO uses the same reward function format as GRPO: + +```python +# rewards.py +def format_reward(completions, **kwargs) -> list[float]: + return [1.0 if len(c) > 10 else 0.0 for c in completions] + +def correctness_reward(completions, answers, **kwargs) -> list[float]: + rewards = [] + for completion, answer in zip(completions, answers): + # Your scoring logic here + rewards.append(score) + return rewards +``` + +#### Sequence Parallelism + +GDPO supports sequence parallelism for long-context training: + +```yaml +rl: gdpo +context_parallel_size: 2 +``` + ### SimPO SimPO uses [CPOTrainer](https://huggingface.co/docs/trl/main/en/cpo_trainer) but with alternative loss function. diff --git a/examples/llama-3/qlora-1b-gdpo.yaml b/examples/llama-3/qlora-1b-gdpo.yaml new file mode 100644 index 0000000000..d806fcf262 --- /dev/null +++ b/examples/llama-3/qlora-1b-gdpo.yaml @@ -0,0 +1,68 @@ +base_model: meta-llama/Llama-3.2-1B-Instruct + +chat_template: llama3 + +rl: gdpo + +trl: + beta: 0.001 + max_completion_length: 128 + num_generations: 2 + temperature: 0.7 + top_p: 0.95 + + use_vllm: false + + + multi_objective_aggregation: normalize_then_sum + + reward_funcs: + - rwd.format_reward + - rwd.correctness_reward + reward_weights: [1.0, 2.0] + + log_completions: true + num_completions_to_print: 3 + scale_rewards: true + +datasets: + - path: openai/gsm8k + name: main + split: train[:1000] + type: rwd.gsm8k_transform + +val_set_size: 0.0 +output_dir: ./outputs/llama3-gdpo-out + +sequence_len: 512 +sample_packing: false +pad_to_sequence_len: false + +gradient_accumulation_steps: 8 +micro_batch_size: 1 +num_epochs: 1 +max_steps: 100 + +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 5e-5 +weight_decay: 0.01 +warmup_steps: 10 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false + +flash_attention: true +logging_steps: 1 +save_steps: 50 +save_safetensors: true + +special_tokens: + pad_token: "<|end_of_text|>" + + +seed: 42 diff --git a/requirements.txt b/requirements.txt index 64fe1b2400..2b5ec0c38b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ transformers==4.57.6 accelerate==1.12.0 datasets==4.5.0 deepspeed>=0.18.3 -trl==0.25.1 +trl==0.27.0 hf_xet==1.2.0 kernels==0.11.5 trackio>=0.13.0 diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index 0ceb80008c..0bd2eedfc9 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -52,12 +52,11 @@ def _get_trainer_cls(self, trainer_kwargs: dict): trainer_cls = None trainer_cls_args = [self.model] - if self.cfg.rl is RLType.GRPO: + if self.cfg.rl in {RLType.GRPO, RLType.GDPO}: trainer_cls = GRPOStrategy.get_trainer_class( sequence_parallel=self.cfg.context_parallel_size > 1 ) trainer_cls_args.extend(GRPOStrategy.set_trainer_args(self.cfg)) - trainer_kwargs.update(GRPOStrategy.set_trainer_kwargs(self.cfg)) elif self.cfg.rl in [RLType.DPO, RLType.IPO]: @@ -147,6 +146,8 @@ def _build_training_arguments(self, total_num_steps): elif self.cfg.rl is RLType.KTO: training_args_cls = AxolotlKTOConfig + # KTOConfig in TRL >= 0.27.0 no longer accepts max_prompt_length + blocklist_args_kwargs = ["max_prompt_length"] training_args_kwargs["desirable_weight"] = ( self.cfg.kto_desirable_weight or 1.0 @@ -155,10 +156,14 @@ def _build_training_arguments(self, total_num_steps): self.cfg.kto_undesirable_weight or 1.0 ) - elif self.cfg.rl is RLType.GRPO: + elif self.cfg.rl in {RLType.GRPO, RLType.GDPO}: training_args_cls = GRPOStrategy.get_training_args_class() training_args_kwargs.update(GRPOStrategy.set_training_args_kwargs(self.cfg)) blocklist_args_kwargs = GRPOStrategy.get_blocklist_args_kwargs() + if self.cfg.rl is RLType.GDPO: + training_args_kwargs.setdefault( + "multi_objective_aggregation", "normalize_then_sum" + ) elif self.cfg.rl in [RLType.DPO, RLType.IPO]: training_args_cls = AxolotlDPOConfig diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 7f28cb8d4c..e611b96ea3 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -129,6 +129,11 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: if trl.rollout_func: grpo_args_kwargs["rollout_func"] = cls.get_rollout_func(trl.rollout_func) + if trl.multi_objective_aggregation is not None: + grpo_args_kwargs["multi_objective_aggregation"] = ( + trl.multi_objective_aggregation + ) + return grpo_args_kwargs @classmethod diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index f7a5ec04c9..5ea9e55e01 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -173,7 +173,7 @@ def _drop_long_sequences( return (len_prompt + len_completion) <= sequence_len - if rl is RLType.GRPO: + if rl in {RLType.GRPO, RLType.GDPO}: return True raise ValueError("Unknown RL type") diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index f86d1a1916..b67888e0f8 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -26,6 +26,7 @@ class RLType(str, Enum): """RL trainer type configuration subset""" DPO = "dpo" + GDPO = "gdpo" GRPO = "grpo" IPO = "ipo" ORPO = "orpo" diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index d24d6f477b..ff96f44ce2 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -179,3 +179,13 @@ class TRLConfig(BaseModel): "description": "Path to custom rollout function. Must be importable from current dir." }, ) + multi_objective_aggregation: ( + Literal["sum_then_normalize", "normalize_then_sum"] | None + ) = Field( + default=None, + json_schema_extra={ + "description": "Multi-objective reward aggregation strategy. " + "'sum_then_normalize' (GRPO default): weights and sums rewards first, then normalizes. " + "'normalize_then_sum' (GDPO): normalizes each reward independently, then sums." + }, + ) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index bf054d3531..bb9c3c673f 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -746,6 +746,19 @@ def check_rl_config_gradient_checkpointing(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_gdpo(cls, data): + if ( + data.get("rl") == "gdpo" + and data.get("trl", {}).get("multi_objective_aggregation") + == "sum_then_normalize" + ): + raise ValueError( + "`multi_objective_aggregation` value set as `sum_then_normalize` => GRPO, but GDPO was selected" + ) + return data + class OptimizationValidationMixin: """Validation methods related to optimization and performance.""" diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index f9db4d0131..c2d81cbcbb 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -311,7 +311,6 @@ def test_kto_training_arguments(self, kto_cfg, model, tokenizer): # KTO specific assert training_arguments.desirable_weight == 1.0 assert training_arguments.undesirable_weight == 1.0 - assert training_arguments.max_prompt_length == 512 def _write_rewards_file(self, rewards_dir: Path): """ diff --git a/tests/e2e/multigpu/solo/test_gdpo.py b/tests/e2e/multigpu/solo/test_gdpo.py new file mode 100644 index 0000000000..2014f7f5e3 --- /dev/null +++ b/tests/e2e/multigpu/solo/test_gdpo.py @@ -0,0 +1,538 @@ +""" +GDPO test suite + +GDPO uses TRL's multi_objective_aggregation="normalize_then_sum" for +per-reward normalization in multi-reward RL training. +""" + +import os +import random +from pathlib import Path + +import pytest +import yaml +from accelerate.test_utils import execute_subprocess_async +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +from tests.e2e.multigpu.solo.test_grpo import recursive_kill, start_vllm +from tests.e2e.utils import require_vllm + + +@pytest.mark.skip(reason="flaky vllm tests in modal") +class TestGDPO: + """Test case for GDPO training using TRL's native multi-objective aggregation.""" + + def _utils_write_yaml_and_rewards(self, cfg, temp_dir, suffix=""): + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + with open(f"rewards_gdpo_{suffix}.py", "w", encoding="utf-8") as fout: + fout.write( + """import random + +def format_reward(prompts, completions, **kwargs) -> list[float]: + return [1.0 if len(c) > 10 else 0.0 for c in completions] + +def correctness_reward(prompts, completions, **kwargs) -> list[float]: + return [random.uniform(-1, 3) for _ in completions] + +def safety_reward(prompts, completions, **kwargs) -> list[float]: + return [1.0 if 'error' not in c.lower() else 0.0 for c in completions] + +def single_reward(prompts, completions, **kwargs) -> list[float]: + return [random.uniform(0, 1) for _ in completions] + +def oai_gsm8k_transform(cfg, *args, **kwargs): + def transform_fn(example, tokenizer=None): + label = example["answer"].split("####")[-1].strip().replace(",", "") + return { + "prompt": [{"role": "user", "content": example["question"]}], + "answer": label, + } + return transform_fn, {"remove_columns": ["question"]} +""" + ) + + @pytest.mark.parametrize("num_gpus", [1, 2]) + @require_vllm + def test_gdpo_multi_reward_lora(self, temp_dir, num_gpus): + """Test GDPO with multiple reward functions using LoRA.""" + rnd_suffix = str(random.randint(1000, 9999)) + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "chat_template": "llama3", + "rl": "gdpo", + "trl": { + "beta": 0.001, + "max_completion_length": 256, + "use_vllm": True, + "num_generations": 4, + "reward_funcs": [ + f"rewards_gdpo_{rnd_suffix}.format_reward", + f"rewards_gdpo_{rnd_suffix}.correctness_reward", + ], + "reward_weights": [1.0, 2.0], + "scale_rewards": True, + }, + "vllm": { + "max_model_len": 800, + "enable_prefix_caching": True, + }, + "datasets": [ + { + "path": "openai/gsm8k", + "name": "main", + "type": f"rewards_gdpo_{rnd_suffix}.oai_gsm8k_transform", + }, + ], + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "flash_attention": True, + "sequence_len": 1024, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "max_steps": 3, + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "warmup_steps": 10, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + "use_tensorboard": True, + "save_first_step": False, + } + ) + + self._utils_write_yaml_and_rewards(cfg, temp_dir, suffix=rnd_suffix) + + current_env = os.environ.copy() + env = { + "NCCL_P2P_LEVEL": "LOC", + **current_env, + "CUDA_VISIBLE_DEVICES": "1", + } + vllm_process = start_vllm( + cfg.base_model, + env=env, + quiet=True, + wait=300, + gpu_memory_utilization=0.15, + max_model_len=cfg.vllm.max_model_len, + enable_prefix_caching=cfg.vllm.enable_prefix_caching, + host="0.0.0.0", + port=8000, + ) + + try: + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + str(num_gpus), + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ], + env={ + "NCCL_P2P_LEVEL": "LOC", + "NCCL_DEBUG": "INFO", + **current_env, + }, + ) + finally: + recursive_kill(vllm_process) + + @require_vllm + def test_gdpo_three_rewards(self, temp_dir): + """Test GDPO with three reward functions (format, correctness, safety).""" + rnd_suffix = str(random.randint(1000, 9999)) + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "chat_template": "llama3", + "rl": "gdpo", + "trl": { + "beta": 0.001, + "max_completion_length": 256, + "use_vllm": True, + "num_generations": 4, + "reward_funcs": [ + f"rewards_gdpo_{rnd_suffix}.format_reward", + f"rewards_gdpo_{rnd_suffix}.correctness_reward", + f"rewards_gdpo_{rnd_suffix}.safety_reward", + ], + "reward_weights": [1.0, 2.0, 1.5], + }, + "vllm": { + "max_model_len": 800, + "enable_prefix_caching": True, + }, + "datasets": [ + { + "path": "openai/gsm8k", + "name": "main", + "type": f"rewards_gdpo_{rnd_suffix}.oai_gsm8k_transform", + }, + ], + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "flash_attention": True, + "sequence_len": 1024, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "max_steps": 3, + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "warmup_steps": 10, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + } + ) + + self._utils_write_yaml_and_rewards(cfg, temp_dir, suffix=rnd_suffix) + + current_env = os.environ.copy() + env = { + "NCCL_P2P_LEVEL": "LOC", + **current_env, + "CUDA_VISIBLE_DEVICES": "1", + } + vllm_process = start_vllm( + cfg.base_model, + env=env, + quiet=True, + wait=300, + gpu_memory_utilization=0.15, + max_model_len=cfg.vllm.max_model_len, + enable_prefix_caching=cfg.vllm.enable_prefix_caching, + host="0.0.0.0", + port=8000, + ) + + try: + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "1", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ], + env={ + "NCCL_P2P_LEVEL": "LOC", + "NCCL_DEBUG": "INFO", + **current_env, + }, + ) + finally: + recursive_kill(vllm_process) + + @require_vllm + def test_gdpo_single_reward_fallback(self, temp_dir): + """Test GDPO with single reward.""" + rnd_suffix = str(random.randint(1000, 9999)) + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "chat_template": "llama3", + "rl": "gdpo", + "trl": { + "beta": 0.001, + "max_completion_length": 256, + "use_vllm": True, + "num_generations": 4, + "reward_funcs": [ + f"rewards_gdpo_{rnd_suffix}.single_reward", + ], + "reward_weights": [1.0], + }, + "vllm": { + "max_model_len": 800, + "enable_prefix_caching": True, + }, + "datasets": [ + { + "path": "openai/gsm8k", + "name": "main", + "type": f"rewards_gdpo_{rnd_suffix}.oai_gsm8k_transform", + }, + ], + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "flash_attention": True, + "sequence_len": 1024, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "max_steps": 3, + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "warmup_steps": 10, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + } + ) + + self._utils_write_yaml_and_rewards(cfg, temp_dir, suffix=rnd_suffix) + + current_env = os.environ.copy() + env = { + "NCCL_P2P_LEVEL": "LOC", + **current_env, + "CUDA_VISIBLE_DEVICES": "1", + } + vllm_process = start_vllm( + cfg.base_model, + env=env, + quiet=True, + wait=300, + gpu_memory_utilization=0.15, + max_model_len=cfg.vllm.max_model_len, + enable_prefix_caching=cfg.vllm.enable_prefix_caching, + host="0.0.0.0", + port=8000, + ) + + try: + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "1", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ], + env={ + "NCCL_P2P_LEVEL": "LOC", + "NCCL_DEBUG": "INFO", + **current_env, + }, + ) + finally: + recursive_kill(vllm_process) + + @require_vllm + def test_gdpo_fft(self, temp_dir): + """Test GDPO with full fine-tuning (no adapter).""" + rnd_suffix = str(random.randint(1000, 9999)) + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "chat_template": "llama3", + "rl": "gdpo", + "trl": { + "beta": 0.001, + "max_completion_length": 256, + "use_vllm": True, + "num_generations": 4, + "reward_funcs": [ + f"rewards_gdpo_{rnd_suffix}.format_reward", + f"rewards_gdpo_{rnd_suffix}.correctness_reward", + ], + "reward_weights": [1.0, 2.0], + }, + "vllm": { + "max_model_len": 800, + "enable_prefix_caching": True, + }, + "datasets": [ + { + "path": "openai/gsm8k", + "name": "main", + "type": f"rewards_gdpo_{rnd_suffix}.oai_gsm8k_transform", + }, + ], + # No adapter - full fine-tuning + "flash_attention": True, + "sequence_len": 1024, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "max_steps": 3, + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "warmup_steps": 10, + "val_set_size": 0.0, + "output_dir": temp_dir, + "learning_rate": 0.0001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + } + ) + + self._utils_write_yaml_and_rewards(cfg, temp_dir, suffix=rnd_suffix) + + current_env = os.environ.copy() + env = { + "NCCL_P2P_LEVEL": "LOC", + **current_env, + "CUDA_VISIBLE_DEVICES": "1", + } + vllm_process = start_vllm( + cfg.base_model, + env=env, + quiet=True, + wait=300, + gpu_memory_utilization=0.15, + max_model_len=cfg.vllm.max_model_len, + enable_prefix_caching=cfg.vllm.enable_prefix_caching, + host="0.0.0.0", + port=8000, + ) + + try: + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "1", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ], + env={ + "NCCL_P2P_LEVEL": "LOC", + "NCCL_DEBUG": "INFO", + **current_env, + }, + ) + finally: + recursive_kill(vllm_process) + + @require_vllm + def test_gdpo_sequence_parallel(self, temp_dir): + """Test GDPO with sequence parallelism.""" + rnd_suffix = str(random.randint(1000, 9999)) + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "chat_template": "llama3", + "rl": "gdpo", + "context_parallel_size": 2, + "trl": { + "beta": 0.001, + "max_completion_length": 256, + "use_vllm": True, + "num_generations": 4, + "reward_funcs": [ + f"rewards_gdpo_{rnd_suffix}.format_reward", + f"rewards_gdpo_{rnd_suffix}.correctness_reward", + ], + "reward_weights": [1.0, 2.0], + }, + "vllm": { + "max_model_len": 800, + "enable_prefix_caching": True, + }, + "datasets": [ + { + "path": "openai/gsm8k", + "name": "main", + "type": f"rewards_gdpo_{rnd_suffix}.oai_gsm8k_transform", + }, + ], + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "flash_attention": True, + "sequence_len": 1024, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "max_steps": 3, + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 2, + "warmup_steps": 10, + "val_set_size": 0.0, + "output_dir": temp_dir, + "dataset_prepared_path": temp_dir + "/last_run_prepared", + "learning_rate": 0.0001, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "save_safetensors": True, + "bf16": "auto", + } + ) + + self._utils_write_yaml_and_rewards(cfg, temp_dir, suffix=rnd_suffix) + + current_env = os.environ.copy() + env = { + "NCCL_P2P_LEVEL": "LOC", + **current_env, + "CUDA_VISIBLE_DEVICES": "1", + } + vllm_process = start_vllm( + cfg.base_model, + env=env, + quiet=True, + wait=300, + gpu_memory_utilization=0.15, + max_model_len=cfg.vllm.max_model_len, + enable_prefix_caching=cfg.vllm.enable_prefix_caching, + host="0.0.0.0", + port=8000, + ) + + try: + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ], + env={ + "NCCL_P2P_LEVEL": "LOC", + "NCCL_DEBUG": "INFO", + **current_env, + }, + ) + finally: + recursive_kill(vllm_process) From 04328aeb974b688bd99d31d4dc575ccb2d64f886 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 21 Jan 2026 17:24:44 -0500 Subject: [PATCH 1103/1405] cu129 targets for ci builds (#3369) * cu129 targets for ci builds * remove copy-paste is_latest --- .github/workflows/main.yml | 12 ++++++++++++ .github/workflows/tests.yml | 26 ++++++++++++++++++-------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d8ab921895..e081f21272 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,6 +34,12 @@ jobs: axolotl_extras: platforms: "linux/amd64,linux/arm64" is_latest: true + - cuda: 129 + cuda_version: 12.9.1 + python_version: "3.12" + pytorch: 2.9.1 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" - cuda: 130 cuda_version: 13.0.0 python_version: "3.11" @@ -106,6 +112,12 @@ jobs: axolotl_extras: is_latest: true platforms: "linux/amd64,linux/arm64" + - cuda: 129 + cuda_version: 12.9.1 + python_version: "3.12" + pytorch: 2.9.1 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" - cuda: 130 cuda_version: 13.0.0 python_version: "3.11" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e8d6e1617c..75c70a24a1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,6 +56,11 @@ jobs: matrix: python_version: ["3.11", "3.12"] pytorch_version: ["2.8.0", "2.9.0", "2.9.1"] + exclude: + - python_version: "3.12" + pytorch_version: "2.8.0" + - python_version: "3.12" + pytorch_version: "2.9.0" timeout-minutes: 20 steps: @@ -144,8 +149,13 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.11"] + python_version: ["3.11", "3.12"] pytorch_version: ["2.8.0", "2.9.0", "2.9.1"] + exclude: + - python_version: "3.12" + pytorch_version: "2.8.0" + - python_version: "3.12" + pytorch_version: "2.9.0" timeout-minutes: 20 steps: @@ -254,10 +264,10 @@ jobs: fail-fast: false matrix: include: - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.8.0 + - cuda: 129 + cuda_version: 12.9.1 + python_version: "3.12" + pytorch: 2.9.1 num_gpus: 1 axolotl_extras: dockerfile: "Dockerfile-uv.jinja" @@ -359,9 +369,9 @@ jobs: fail-fast: false matrix: include: - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" + - cuda: 129 + cuda_version: 12.9.1 + python_version: "3.12" pytorch: 2.9.1 num_gpus: 1 axolotl_extras: From a531e9d946b83f04f56ac46442b5d7b3b3f8cdc0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 21 Jan 2026 20:00:18 -0500 Subject: [PATCH 1104/1405] upgrade vllm to v0.14.0 (#3345) --- .github/workflows/main.yml | 2 +- .github/workflows/multi-gpu-e2e.yml | 14 +++++++++----- .github/workflows/pypi.yml | 2 +- .github/workflows/tests-nightly.yml | 2 +- .github/workflows/tests.yml | 6 +++--- README.md | 2 +- cicd/Dockerfile-uv.jinja | 2 +- cicd/Dockerfile.jinja | 2 +- cicd/multigpu.py | 6 +++++- docker/Dockerfile-base | 2 +- docker/Dockerfile-base-nightly | 2 +- examples/apertus/README.md | 2 +- examples/arcee/README.md | 2 +- examples/devstral/README.md | 2 +- examples/gemma3n/README.md | 2 +- examples/gpt-oss/README.md | 2 +- examples/granite4/README.md | 2 +- examples/hunyuan/README.md | 2 +- examples/magistral/README.md | 2 +- examples/qwen3-next/README.md | 2 +- examples/voxtral/README.md | 2 +- pyproject.toml | 2 +- requirements.txt | 4 ++-- setup.py | 5 +++++ 24 files changed, 43 insertions(+), 30 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e081f21272..0e1ccb89a3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,7 +38,7 @@ jobs: cuda_version: 12.9.1 python_version: "3.12" pytorch: 2.9.1 - axolotl_extras: + axolotl_extras: vllm platforms: "linux/amd64,linux/arm64" - cuda: 130 cuda_version: 13.0.0 diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 833dc4f29a..107572ad62 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -35,14 +35,19 @@ jobs: pytorch: 2.8.0 axolotl_extras: fbgemm-gpu num_gpus: 2 - nightly_build: "true" - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" pytorch: 2.9.1 - axolotl_extras: fbgemm-gpu + axolotl_extras: "fbgemm-gpu" + num_gpus: 2 + - cuda: 129 + cuda_version: 12.9.1 + python_version: "3.12" + pytorch: 2.9.1 + axolotl_extras: "fbgemm-gpu,vllm" num_gpus: 2 - nightly_build: "true" + dockerfile: "Dockerfile-uv.jinja" - cuda: 130 cuda_version: 13.0.0 python_version: "3.11" @@ -50,7 +55,6 @@ jobs: axolotl_extras: # axolotl_extras: fbgemm-gpu num_gpus: 2 - nightly_build: "true" runs-on: [self-hosted, modal] timeout-minutes: 120 steps: @@ -72,8 +76,8 @@ jobs: echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - echo "NIGHTLY_BUILD=${{ matrix.nightly_build }}" >> $GITHUB_ENV echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV + echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV - name: Run tests job on Modal run: | modal run -m cicd.multigpu diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index af2ad73a47..3bf66b4975 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -40,7 +40,7 @@ jobs: - name: Install dependencies run: | - pip3 install wheel packaging==23.2 + pip3 install wheel packaging==26.0 pip3 install --no-build-isolation -e . pip3 install -r requirements-dev.txt -r requirements-tests.txt diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 67b68a7e60..21446e5488 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -48,7 +48,7 @@ jobs: - name: upgrade pip run: | pip3 install --upgrade pip - pip3 install --upgrade packaging==23.2 setuptools==75.8.0 wheel + pip3 install --upgrade packaging==26.0 setuptools==75.8.0 wheel - name: Install PyTorch run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 75c70a24a1..bcbb76df3d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -87,7 +87,7 @@ jobs: - name: upgrade pip run: | pip3 install --upgrade pip - pip3 install --upgrade packaging==23.2 setuptools==75.8.0 wheel + pip3 install --upgrade packaging==26.0 setuptools==75.8.0 wheel - name: Install PyTorch run: | @@ -182,7 +182,7 @@ jobs: - name: upgrade pip run: | pip3 install --upgrade pip - pip3 install --upgrade packaging==23.2 setuptools==75.8.0 setuptools_scm build wheel psutil + pip3 install --upgrade packaging==26.0 setuptools==75.8.0 setuptools_scm build wheel psutil - name: Install PyTorch run: | @@ -269,7 +269,7 @@ jobs: python_version: "3.12" pytorch: 2.9.1 num_gpus: 1 - axolotl_extras: + axolotl_extras: vllm dockerfile: "Dockerfile-uv.jinja" steps: - name: Checkout diff --git a/README.md b/README.md index 0521f7bedf..b56cdf0e84 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Features: #### Using pip ```bash -pip3 install -U packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install -U packaging==26.0 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] # Download example axolotl configs, deepspeed configs diff --git a/cicd/Dockerfile-uv.jinja b/cicd/Dockerfile-uv.jinja index 6a4d8a7d33..9a49cfca57 100644 --- a/cicd/Dockerfile-uv.jinja +++ b/cicd/Dockerfile-uv.jinja @@ -31,7 +31,7 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt; \ fi -RUN uv pip install packaging==23.2 setuptools==75.8.0 +RUN uv pip install packaging==26.0 setuptools==75.8.0 RUN uv pip install torchvision RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ uv pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 81ed5453e3..1c397b011d 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -32,7 +32,7 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt; \ fi -RUN pip install packaging==23.2 setuptools==75.8.0 psutil +RUN pip install packaging==26.0 setuptools==75.8.0 psutil RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ diff --git a/cicd/multigpu.py b/cicd/multigpu.py index 5bd8d3c044..ed022c851f 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -17,7 +17,8 @@ template_env = jinja2.Environment( loader=template_loader, autoescape=select_autoescape() ) -df_template = template_env.get_template("Dockerfile.jinja") +dockerfile = os.environ.get("E2E_DOCKERFILE", "Dockerfile.jinja") +df_template = template_env.get_template(dockerfile) df_args = { "AXOLOTL_EXTRAS": os.environ.get("AXOLOTL_EXTRAS", ""), @@ -27,8 +28,11 @@ "CUDA": os.environ.get("CUDA", "126"), "GITHUB_REF": os.environ.get("GITHUB_REF", "refs/heads/main"), "GITHUB_SHA": os.environ.get("GITHUB_SHA", ""), + "NIGHTLY_BUILD": os.environ.get("NIGHTLY_BUILD", ""), "CODECOV_TOKEN": os.environ.get("CODECOV_TOKEN", ""), "HF_HOME": "/workspace/data/huggingface-cache/hub", + "PYTHONUNBUFFERED": os.environ.get("PYTHONUNBUFFERED", "1"), + "DEEPSPEED_LOG_LEVEL": os.environ.get("DEEPSPEED_LOG_LEVEL", "WARNING"), } dockerfile_contents = df_template.render(**df_args) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 96367207fb..547c45f49f 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -43,7 +43,7 @@ ENV PATH="/root/miniconda3/envs/py${PYTHON_VERSION}/bin:${PATH}" WORKDIR /workspace -RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==23.2 setuptools==75.8.0 wheel psutil && \ +RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==26.0 setuptools==75.8.0 wheel psutil && \ python3 -m pip install --no-cache-dir -U torch==${PYTORCH_VERSION}+cu${CUDA} torchvision --extra-index-url https://download.pytorch.org/whl/cu$CUDA && \ python3 -m pip cache purge diff --git a/docker/Dockerfile-base-nightly b/docker/Dockerfile-base-nightly index cc74e6bb9b..98dc9e8804 100644 --- a/docker/Dockerfile-base-nightly +++ b/docker/Dockerfile-base-nightly @@ -30,7 +30,7 @@ ENV PATH="/root/miniconda3/envs/py${PYTHON_VERSION}/bin:${PATH}" WORKDIR /workspace -RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==23.2 setuptools==75.8.0 wheel && \ +RUN python3 -m pip install --upgrade pip && pip3 install -U packaging==26.0 setuptools==75.8.0 wheel && \ python3 -m pip install --no-cache-dir -U torch --extra-index-url https://download.pytorch.org/whl/nightly/cu$CUDA && \ python3 -m pip install --no-cache-dir "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main" && \ python3 -m pip install --no-cache-dir "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main" && \ diff --git a/examples/apertus/README.md b/examples/apertus/README.md index 774286333a..1cb4d413ce 100644 --- a/examples/apertus/README.md +++ b/examples/apertus/README.md @@ -15,7 +15,7 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation -e '.[flash-attn]' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy diff --git a/examples/arcee/README.md b/examples/arcee/README.md index 23f63663e7..ad554532c1 100644 --- a/examples/arcee/README.md +++ b/examples/arcee/README.md @@ -17,7 +17,7 @@ Thanks to the team at Arcee.ai for using Axolotl in supervised fine-tuning the A git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation -e '.[flash-attn]' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy diff --git a/examples/devstral/README.md b/examples/devstral/README.md index ae08606628..5a0145f103 100644 --- a/examples/devstral/README.md +++ b/examples/devstral/README.md @@ -16,7 +16,7 @@ Thanks to the team at MistralAI for giving us early access to prepare for this r ```bash # Ensure you have Pytorch installed (Pytorch 2.6.0 min) -pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` diff --git a/examples/gemma3n/README.md b/examples/gemma3n/README.md index ff3946c906..4808ed81be 100644 --- a/examples/gemma3n/README.md +++ b/examples/gemma3n/README.md @@ -10,7 +10,7 @@ Gemma-3n is a family of multimodal models from Google found on [HuggingFace](htt ```bash # Ensure you have Pytorch installed (Pytorch 2.6.0 min) -pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` diff --git a/examples/gpt-oss/README.md b/examples/gpt-oss/README.md index 9ab02b122b..8c407540ed 100644 --- a/examples/gpt-oss/README.md +++ b/examples/gpt-oss/README.md @@ -14,7 +14,7 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations ```bash # Ensure you have Pytorch installed (Pytorch 2.6.0 min) -pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` diff --git a/examples/granite4/README.md b/examples/granite4/README.md index d5efd3349f..0495394054 100644 --- a/examples/granite4/README.md +++ b/examples/granite4/README.md @@ -15,7 +15,7 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation -e '.[flash-attn]' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy diff --git a/examples/hunyuan/README.md b/examples/hunyuan/README.md index 96c6bbcfa8..59e9a28c78 100644 --- a/examples/hunyuan/README.md +++ b/examples/hunyuan/README.md @@ -13,7 +13,7 @@ Tencent released a family of opensource models called HunYuan with varying param git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation -e '.[flash-attn]' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy diff --git a/examples/magistral/README.md b/examples/magistral/README.md index 40a793f109..2e162df6b6 100644 --- a/examples/magistral/README.md +++ b/examples/magistral/README.md @@ -14,7 +14,7 @@ Thanks to the team at MistralAI for giving us early access to prepare for these ```bash # Ensure you have Pytorch installed (Pytorch 2.7.0 min) -pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` diff --git a/examples/qwen3-next/README.md b/examples/qwen3-next/README.md index 678175fd40..3c3a26a762 100644 --- a/examples/qwen3-next/README.md +++ b/examples/qwen3-next/README.md @@ -15,7 +15,7 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation -e '.[flash-attn]' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy diff --git a/examples/voxtral/README.md b/examples/voxtral/README.md index b77691d72b..2d3cad4e9a 100644 --- a/examples/voxtral/README.md +++ b/examples/voxtral/README.md @@ -12,7 +12,7 @@ Thanks to the team at MistralAI for giving us early access to prepare for this r ```bash # Ensure you have Pytorch installed (Pytorch 2.6.0 min) -pip3 install packaging==23.2 setuptools==75.8.0 wheel ninja +pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` diff --git a/pyproject.toml b/pyproject.toml index c5b7deb05e..bca758576e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=64", "wheel", "setuptools_scm>=8", "packaging==23.2"] +requires = ["setuptools>=64", "wheel", "setuptools_scm>=8", "packaging==26.0"] build-backend = "setuptools.build_meta" [project] diff --git a/requirements.txt b/requirements.txt index 2b5ec0c38b..2d5fa12fcc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ xformers>=0.0.23.post1 liger-kernel==0.6.4 # END section -packaging==23.2 +packaging==26.0 huggingface_hub>=0.36.0 peft>=0.18.1 @@ -72,4 +72,4 @@ axolotl-contribs-mit==0.0.6 # telemetry posthog==6.7.11 -mistral-common==1.8.6 +mistral-common==1.8.8 diff --git a/setup.py b/setup.py index 101c5b8c4b..00a8486e22 100644 --- a/setup.py +++ b/setup.py @@ -78,6 +78,11 @@ def parse_requirements(extras_require_map): extras_require_map["vllm"] = ["vllm==0.11.1"] if not install_xformers: _install_requires.pop(_install_requires.index(xformers_version)) + extras_require_map["vllm"] = ["vllm==0.13.0"] + if patch == 0: + extras_require_map["vllm"] = ["vllm==0.13.0"] + else: + extras_require_map["vllm"] = ["vllm==0.14.0"] elif (major, minor) >= (2, 8): extras_require_map.pop("fbgemm-gpu") extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.3.0"] From fc4e37920b9bb00d65804f64d66a0243c94b4451 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 27 Jan 2026 17:08:24 -0500 Subject: [PATCH 1105/1405] transformers v5 upgrade (#3272) * Prepare for transformers v5 upgrade * fix hf cli * update for hf hub changes * fix tokenizer apply_chat_template args * remap include_tokens_per_second * fix tps * handle migration for warmup * use latest hf hub * Fix scan -> ls * fix import * fix for renaming of mistral common tokenizer -> backend * update for fixed tokenziation for llama * Skip phi35 tests for now * remove mistral patch fixed upstream in huggingface/transformers#41439 * use namespacing for patch * don't rely on sdist for e2e tests for now * run modal ci without waiting too * Fix dep for ci * fix imports * Fix fp8 check * fsdp2 fixes * fix version handling * update fsdp version tests for new v5 behavior * Fail multigpu tests after 3 failures * skip known v5 broken tests for now and cleanup * bump deps * unmark skipped test * re-enable test_fsdp_qlora_prequant_packed test * increase multigpu ci timeout * skip broken gemma3 test * reduce timout back to original 120min now that the hanging test is skipped * fix for un-necessary collator for pretraining with bsz=1 * fix: safe_serialization deprecated in transformers v5 rc01 (#3318) * torch_dtype deprecated * load model in float32 for consistency with tests * revert some test fixtures back * use hf cache ls instead of scan * don't strip fsdp_version more fdsp_Version fixes for v5 fix version in fsdp_config fix aliasing fix fsdp_version check check fsdp_version is 2 in both places * Transformers v5 rc2 (#3347) * bump dep * use latest fbgemm, grab model config as part of fixture, un-skip test * import AutoConfig * don't need more problematic autoconfig when specifying config.json manually * add fixtures for argilla ultrafeedback datasets * download phi4-reasoning * fix arg * update tests for phi fast tokenizer changes * use explicit model types for gemma3 --------- Co-authored-by: Wing Lian * fix: AutoModelForVision2Seq -> AutoModelForImageTextToText * chore: remove duplicate * fix: attempt fix gemma3 text mode * chore: lint * ga release of v5 * need property setter for name_or_path for mistral tokenizer * vllm not compatible with transformers v5 * setter for chat_template w mistral too --------- Co-authored-by: NanoCode012 Co-authored-by: salman --- .github/workflows/main.yml | 2 +- .github/workflows/multi-gpu-e2e.yml | 2 +- .github/workflows/tests.yml | 16 ++--- .runpod/src/config/config.yaml | 4 -- cicd/multigpu.sh | 2 +- docs/amd_hpc.qmd | 2 +- docs/installation.qmd | 2 +- examples/jamba/qlora_fsdp_large.yaml | 1 - examples/llama-3/qlora-fsdp-405b.yaml | 1 - examples/mamba/config.yml | 1 - requirements.txt | 6 +- src/axolotl/cli/checks.py | 2 +- src/axolotl/cli/merge_lora.py | 2 - src/axolotl/cli/merge_sharded_fsdp_weights.py | 36 +++------- src/axolotl/cli/quantize.py | 4 +- src/axolotl/core/builders/base.py | 10 +-- src/axolotl/core/builders/causal.py | 4 +- src/axolotl/core/trainers/base.py | 53 +++++++------- .../integrations/llm_compressor/utils.py | 3 - src/axolotl/loaders/model.py | 7 +- src/axolotl/loaders/patch_manager.py | 7 -- src/axolotl/loaders/processor.py | 2 +- src/axolotl/models/mamba/modeling_mamba.py | 1 - .../mistral3/mistral_common_tokenizer.py | 16 ++--- src/axolotl/monkeypatch/relora.py | 3 +- .../transformers/trainer_context_parallel.py | 10 ++- src/axolotl/processing_strategies.py | 5 +- .../prompt_strategies/chat_template.py | 2 + src/axolotl/train.py | 34 +++------ src/axolotl/utils/callbacks/perplexity.py | 6 +- .../utils/mistral/mistral_tokenizer.py | 24 ++++--- src/axolotl/utils/schemas/fsdp.py | 7 +- src/axolotl/utils/schemas/model.py | 14 +++- src/axolotl/utils/schemas/validation.py | 71 ++++++++++--------- tests/conftest.py | 51 ++++++++++--- tests/core/test_builders.py | 1 - .../integrations/test_cut_cross_entropy.py | 4 +- tests/e2e/integrations/test_fp8.py | 1 - tests/e2e/integrations/test_hooks.py | 2 +- tests/e2e/integrations/test_kd.py | 1 - tests/e2e/integrations/test_liger.py | 2 - tests/e2e/integrations/test_llm_compressor.py | 1 - tests/e2e/multigpu/solo/test_grpo.py | 3 - tests/e2e/multigpu/test_fp8_fsdp2.py | 5 +- tests/e2e/multigpu/test_fsdp1.py | 1 + tests/e2e/multigpu/test_fsdp2.py | 4 ++ tests/e2e/multigpu/test_gemma3.py | 2 + tests/e2e/multigpu/test_llama.py | 1 - .../patched/test_activation_checkpointing.py | 1 - tests/e2e/patched/test_peft_embeddings.py | 1 - tests/e2e/patched/test_resume.py | 1 - tests/e2e/solo/test_relora_llama.py | 1 - tests/e2e/test_activation_offloading.py | 1 - tests/e2e/test_deepseekv3.py | 2 - tests/e2e/test_diffusion.py | 2 - tests/e2e/test_embeddings_lr.py | 2 - tests/e2e/test_gemma2.py | 2 - tests/e2e/test_gemma3_text.py | 2 - tests/e2e/test_llama.py | 4 -- tests/e2e/test_llama_pretrain.py | 1 - tests/e2e/test_llama_vision.py | 2 - tests/e2e/test_mamba.py | 1 - tests/e2e/test_optimizers.py | 1 - tests/e2e/test_qat.py | 1 - tests/e2e/test_save_first_step.py | 2 - tests/e2e/test_streaming.py | 1 - tests/e2e/utils.py | 26 +++---- tests/hf_offline_utils.py | 1 + .../test_mistral_tokenizer_patch.py | 35 --------- .../test_chat_templates_advanced.py | 2 +- tests/test_normalize_config.py | 7 +- tests/test_perplexity.py | 4 +- tests/test_tokenizers.py | 2 + tests/utils/schemas/validation/test_fsdp.py | 23 ++++-- 74 files changed, 261 insertions(+), 308 deletions(-) delete mode 100644 tests/monkeypatch/test_mistral_tokenizer_patch.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0e1ccb89a3..e081f21272 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,7 +38,7 @@ jobs: cuda_version: 12.9.1 python_version: "3.12" pytorch: 2.9.1 - axolotl_extras: vllm + axolotl_extras: platforms: "linux/amd64,linux/arm64" - cuda: 130 cuda_version: 13.0.0 diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 107572ad62..5187a08c78 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -45,7 +45,7 @@ jobs: cuda_version: 12.9.1 python_version: "3.12" pytorch: 2.9.1 - axolotl_extras: "fbgemm-gpu,vllm" + axolotl_extras: "fbgemm-gpu" num_gpus: 2 dockerfile: "Dockerfile-uv.jinja" - cuda: 130 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bcbb76df3d..c866e0cfc3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -115,10 +115,10 @@ jobs: - name: Pre-Download dataset fixture run: | - huggingface-cli download --repo-type=dataset axolotl-ai-internal/axolotl-oss-dataset-fixtures + hf download --repo-type=dataset axolotl-ai-internal/axolotl-oss-dataset-fixtures - name: Show HF cache - run: hf cache scan + run: hf cache ls - name: Run tests run: | @@ -132,7 +132,7 @@ jobs: pytest -v --durations=10 tests/cli/ --cov=axolotl --cov-append --cov-report=xml - name: Show HF cache - run: hf cache scan + run: hf cache ls - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 @@ -210,7 +210,7 @@ jobs: axolotl --help - name: Show HF cache - run: hf cache scan + run: hf cache ls - name: Run tests run: | @@ -219,10 +219,10 @@ jobs: pytest -v --durations=10 tests/cli/ - name: Show HF cache - run: hf cache scan + run: hf cache ls gate-skip-e2e: - needs: [pre-commit, pytest, pytest-sdist] + needs: [pre-commit] runs-on: ubuntu-latest outputs: skip: ${{ steps.compute.outputs.skip }} @@ -258,7 +258,7 @@ jobs: # this job needs to be run on self-hosted GPU runners... runs-on: [self-hosted, modal] timeout-minutes: 120 - needs: [pre-commit, pytest, pytest-sdist, gate-skip-e2e] + needs: [pre-commit, pytest] strategy: fail-fast: false @@ -269,7 +269,7 @@ jobs: python_version: "3.12" pytorch: 2.9.1 num_gpus: 1 - axolotl_extras: vllm + axolotl_extras: dockerfile: "Dockerfile-uv.jinja" steps: - name: Checkout diff --git a/.runpod/src/config/config.yaml b/.runpod/src/config/config.yaml index f482a7331b..fde3730b27 100644 --- a/.runpod/src/config/config.yaml +++ b/.runpod/src/config/config.yaml @@ -224,9 +224,6 @@ # eval_table_size: # Approximate number of predictions sent to wandb depending on batch size. Enabled above 0. Default is 0 # eval_table_max_new_tokens: # Total number of tokens generated for predictions sent to wandb. Default is 128 -# # Save model as safetensors (require safetensors package) -# save_safetensors: - # # Whether to mask out or include the human's prompt from the training labels # train_on_inputs: false # # Group similarly sized data to minimize padding. @@ -512,7 +509,6 @@ profiler_steps: ${PROFILER_STEPS} loss_watchdog_threshold: ${LOSS_WATCHDOG_THRESHOLD} loss_watchdog_patience: ${LOSS_WATCHDOG_PATIENCE} -save_safetensors: ${SAVE_SAFETENSORS} train_on_inputs: ${TRAIN_ON_INPUTS} group_by_length: ${GROUP_BY_LENGTH} gradient_checkpointing: ${GRADIENT_CHECKPOINTING} diff --git a/cicd/multigpu.sh b/cicd/multigpu.sh index 307dd4960e..e00fe909ee 100755 --- a/cicd/multigpu.sh +++ b/cicd/multigpu.sh @@ -2,7 +2,7 @@ set -e # Only run two tests at a time to avoid OOM on GPU (with coverage collection) -pytest -v --durations=10 -n2 --maxfail=4 \ +pytest -v --durations=10 -n2 --maxfail=3 \ --ignore=/workspace/axolotl/tests/e2e/multigpu/solo/ \ --ignore=/workspace/axolotl/tests/e2e/multigpu/patched/ \ /workspace/axolotl/tests/e2e/multigpu/ \ diff --git a/docs/amd_hpc.qmd b/docs/amd_hpc.qmd index c6dbe82d07..259b01ab54 100644 --- a/docs/amd_hpc.qmd +++ b/docs/amd_hpc.qmd @@ -86,7 +86,7 @@ export HF_DATASETS_OFFLINE=1 Download a base model using the Hugging Face CLI: ```bash -huggingface-cli download meta-llama/Meta-Llama-3.1-8B --local-dir ~/hfdata/llama3.1-8B +hf download meta-llama/Meta-Llama-3.1-8B --local-dir ~/hfdata/llama3.1-8B ``` ### 10. Create Axolotl Configuration diff --git a/docs/installation.qmd b/docs/installation.qmd index b8d427eb00..5df8f87e8a 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -165,7 +165,7 @@ We recommend using WSL2 (Windows Subsystem for Linux) or Docker. ``` 4. (Optional) Login to Hugging Face: ```{.bash} - huggingface-cli login + hf auth login ``` ## Troubleshooting {#sec-troubleshooting} diff --git a/examples/jamba/qlora_fsdp_large.yaml b/examples/jamba/qlora_fsdp_large.yaml index 150e5e2ecf..4db889fbc4 100644 --- a/examples/jamba/qlora_fsdp_large.yaml +++ b/examples/jamba/qlora_fsdp_large.yaml @@ -19,7 +19,6 @@ datasets: dataset_prepared_path: last_run_prepared val_set_size: 0.0 output_dir: jamba-large-fsdp-qlora-ft -save_safetensors: true adapter: qlora sequence_len: 2048 sample_packing: true diff --git a/examples/llama-3/qlora-fsdp-405b.yaml b/examples/llama-3/qlora-fsdp-405b.yaml index 8ddb84d652..5c236f2cf4 100644 --- a/examples/llama-3/qlora-fsdp-405b.yaml +++ b/examples/llama-3/qlora-fsdp-405b.yaml @@ -12,7 +12,6 @@ datasets: dataset_prepared_path: last_run_prepared val_set_size: 0.0 output_dir: ./outputs/out/qlora-llama3_1-405b -save_safetensors: true adapter: qlora diff --git a/examples/mamba/config.yml b/examples/mamba/config.yml index e6b3358042..5f36595a38 100644 --- a/examples/mamba/config.yml +++ b/examples/mamba/config.yml @@ -47,6 +47,5 @@ saves_per_epoch: 1 weight_decay: 0.0 special_tokens: tokens: -save_safetensors: False # save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/requirements.txt b/requirements.txt index 2d5fa12fcc..21fdda2266 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,17 +9,17 @@ liger-kernel==0.6.4 # END section packaging==26.0 - -huggingface_hub>=0.36.0 +huggingface_hub>=1.1.7 peft>=0.18.1 tokenizers>=0.22.1 -transformers==4.57.6 +transformers==5.0.0 accelerate==1.12.0 datasets==4.5.0 deepspeed>=0.18.3 trl==0.27.0 hf_xet==1.2.0 kernels==0.11.5 + trackio>=0.13.0 typing-extensions>=4.15.0 diff --git a/src/axolotl/cli/checks.py b/src/axolotl/cli/checks.py index a743e74dc7..254da8bae4 100644 --- a/src/axolotl/cli/checks.py +++ b/src/axolotl/cli/checks.py @@ -44,7 +44,7 @@ def check_user_token() -> bool: return bool(user_info) except LocalTokenNotFoundError: LOG.warning( - "Error verifying HuggingFace token. Remember to log in using `huggingface-cli login` and get your access token from https://huggingface.co/settings/tokens if you want to use gated models or datasets." + "Error verifying HuggingFace token. Remember to log in using `hf auth login` and get your access token from https://huggingface.co/settings/tokens if you want to use gated models or datasets." ) return False except HTTPError: diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 482767b124..e7ad890369 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -24,7 +24,6 @@ def do_merge_lora(*, cfg: DictDefault) -> None: cfg: Dictionary mapping `axolotl` config keys to values. """ model, tokenizer, processor = load_model_and_tokenizer(cfg=cfg) - safe_serialization = cfg.save_safetensors is True LOG.info("Running merge of LoRA with base model...") model = model.merge_and_unload(progressbar=True) @@ -42,7 +41,6 @@ def do_merge_lora(*, cfg: DictDefault) -> None: LOG.info(f"Saving merged model to: {str(Path(cfg.output_dir) / 'merged')}...") model.save_pretrained( str(Path(cfg.output_dir) / "merged"), - safe_serialization=safe_serialization, progressbar=True, ) tokenizer.save_pretrained( diff --git a/src/axolotl/cli/merge_sharded_fsdp_weights.py b/src/axolotl/cli/merge_sharded_fsdp_weights.py index 1d9736b9d1..f12d5ab5d6 100644 --- a/src/axolotl/cli/merge_sharded_fsdp_weights.py +++ b/src/axolotl/cli/merge_sharded_fsdp_weights.py @@ -14,8 +14,6 @@ from accelerate.utils import ( SAFE_WEIGHTS_INDEX_NAME, SAFE_WEIGHTS_NAME, - WEIGHTS_INDEX_NAME, - WEIGHTS_NAME, is_torch_version, ) from huggingface_hub import split_torch_state_dict_into_shards @@ -40,17 +38,15 @@ def commit_tensor(self, read_item, tensor): def _distributed_checkpoint_to_merged_weights( checkpoint_dir: Union[str, Path], save_path: str, - safe_serialization: bool = False, max_shard_size: str = "5GB", ) -> Path: """ Passthrough to `torch.distributed.checkpoint.format_utils.dcp_to_torch_save`. Will - save under `save_path` as either `model.safetensors` or `pytorch_model.bin`. + save under `save_path` as `model.safetensors`. Args: checkpoint_dir: Directory where distributed checkpoint is saved. save_path: Path to save model to. - safe_serialization: Whether to save in safetensors format. max_shard_size: Max size of model shards to save. Returns: @@ -76,11 +72,7 @@ def _distributed_checkpoint_to_merged_weights( if isinstance(value, torch.Tensor) and value.dtype != torch.bfloat16: state_dict[key] = value.to(torch.bfloat16) - weights_name = SAFE_WEIGHTS_NAME if safe_serialization else WEIGHTS_NAME - - filename_pattern = weights_name.replace(".bin", "{suffix}.bin").replace( - ".safetensors", "{suffix}.safetensors" - ) + filename_pattern = SAFE_WEIGHTS_NAME.replace(".safetensors", "{suffix}.safetensors") state_dict_split = split_torch_state_dict_into_shards( state_dict, filename_pattern=filename_pattern, max_shard_size=max_shard_size ) @@ -98,19 +90,12 @@ def _distributed_checkpoint_to_merged_weights( for shard_file, tensors in filename_to_tensors: shard = {tensor: state_dict[tensor] for tensor in tensors} - - if safe_serialization: - safe_save_file( - shard, os.path.join(save_path_, shard_file), metadata={"format": "pt"} - ) - else: - torch.save(shard, os.path.join(save_path_, shard_file)) + safe_save_file( + shard, os.path.join(save_path_, shard_file), metadata={"format": "pt"} + ) if index is not None: - save_index_file = ( - SAFE_WEIGHTS_INDEX_NAME if safe_serialization else WEIGHTS_INDEX_NAME - ) - save_index_file = os.path.join(save_path_, save_index_file) + save_index_file = os.path.join(save_path_, SAFE_WEIGHTS_INDEX_NAME) # Save the index as well with open(save_index_file, "w", encoding="utf-8") as fout: content = json.dumps(index, indent=2, sort_keys=True) + "\n" @@ -123,13 +108,11 @@ def _distributed_checkpoint_to_merged_weights( def merge_fsdp_weights( checkpoint_dir: str, output_path: str, - safe_serialization: bool = False, remove_checkpoint_dir: bool = False, ): """ Merge the weights from sharded FSDP model checkpoints into a single combined checkpoint. Should be used if - `SHARDED_STATE_DICT` was used for the model. Weights will be saved to `{output_path}/model.safetensors` if - `safe_serialization` else `pytorch_model.bin`. + `SHARDED_STATE_DICT` was used for the model. Weights will be saved to `{output_path}/model.safetensors`. Note: this is a CPU-bound process. @@ -138,8 +121,6 @@ def merge_fsdp_weights( The directory containing the FSDP checkpoints (can be either the model or optimizer). output_path (`str`): The path to save the merged checkpoint. - safe_serialization (`bool`, *optional*, defaults to `True`): - Whether to save the merged weights with safetensors (recommended). remove_checkpoint_dir (`bool`, *optional*, defaults to `False`): Whether to remove the checkpoint directory after merging. @@ -177,7 +158,7 @@ def merge_fsdp_weights( if state.is_main_process: LOG.info(f"Merging FSDP weights from {checkpoint_dir_}") save_path = _distributed_checkpoint_to_merged_weights( - checkpoint_dir_, output_path, safe_serialization + checkpoint_dir_, output_path ) LOG.info(f"Successfully merged FSDP weights and saved to {save_path}") if remove_checkpoint_dir: @@ -210,7 +191,6 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): merge_fsdp_weights( checkpoint_dir=str(fsdp_dir), output_path=output_path, - safe_serialization=True, ) state = PartialState() state.wait_for_everyone() diff --git a/src/axolotl/cli/quantize.py b/src/axolotl/cli/quantize.py index f4fcc6d7df..939443a01c 100644 --- a/src/axolotl/cli/quantize.py +++ b/src/axolotl/cli/quantize.py @@ -102,12 +102,10 @@ def do_quantize( LOG.info(f"Saving quantized model to: {str(Path(output_dir) / 'quantized')}.") model.save_pretrained( str(Path(output_dir) / "quantized"), - safe_serialization=False, progressbar=True, ) tokenizer.save_pretrained( str(Path(output_dir) / "quantized"), - safe_serialization=False, progressbar=True, save_jinja_files=cfg.tokenizer_save_jinja_files, ) @@ -121,7 +119,7 @@ def do_quantize( hub_model_id.rstrip("-") + f"-{quantization_config_to_str[type(quantization_config)]}" ) - model.push_to_hub(hub_model_id, safe_serialization=False) + model.push_to_hub(hub_model_id) tokenizer.push_to_hub(hub_model_id) if processor: processor.push_to_hub(hub_model_id) diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 412f6da2ff..f3a9654352 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -216,7 +216,7 @@ def hook_post_create_trainer(self, trainer): def _configure_warmup_and_logging( self, total_num_steps: int, training_args_kwargs: dict ): - warmup_steps = 0 + warmup_steps: int | float = 0 warmup_ratio = 0.0 if self.cfg.warmup_steps is not None: warmup_steps = self.cfg.warmup_steps @@ -230,6 +230,10 @@ def _configure_warmup_and_logging( else: warmup_ratio = 0.03 + # transformers v5 + if warmup_ratio > 0.0 and warmup_steps == 0: + warmup_steps = warmup_ratio + if warmup_steps == 1: warmup_steps = 2 @@ -242,7 +246,6 @@ def _configure_warmup_and_logging( else max(min(int(0.005 * total_num_steps), 10), 1) ) - training_args_kwargs["warmup_ratio"] = warmup_ratio training_args_kwargs["warmup_steps"] = warmup_steps def _configure_precision_settings(self, training_args_kwargs: dict): @@ -530,9 +533,7 @@ def _set_base_training_args( "loraplus_lr_ratio", "loraplus_lr_embedding", "output_dir", - "save_safetensors", "save_only_model", - "include_tokens_per_second", "weight_decay", "seed", "dion_momentum", @@ -545,6 +546,7 @@ def _set_base_training_args( arg_map = { "dion_learning_rate": "dion_lr", + "include_num_input_tokens_seen": "include_tokens_per_second", } for kwarg, cfg_arg in arg_map.items(): if hasattr(self.cfg, cfg_arg) and getattr(self.cfg, cfg_arg) is not None: diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index cda98087f0..3a9f8ba1be 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -437,7 +437,9 @@ def build_collator( or self.cfg.micro_batch_size > 1 ): return DataCollatorForSeq2Seq(self.tokenizer, **kwargs) - if not (self.cfg.sample_packing and self.cfg.pretrain_multipack_attn): + if not (self.cfg.sample_packing and self.cfg.pretrain_multipack_attn) or ( + self.cfg.micro_batch_size == 1 and is_eval is False + ): return None if self.cfg.model_config_type == "mamba": diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 799dcf02ec..a45e246a12 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -25,7 +25,7 @@ from transformers import PreTrainedModel, Trainer from transformers.trainer import TRAINING_ARGS_NAME from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, has_length, seed_worker -from transformers.utils import SAFE_WEIGHTS_NAME, WEIGHTS_NAME, is_peft_available +from transformers.utils import SAFE_WEIGHTS_NAME, is_peft_available from trl.trainer.utils import pad_to_length from typing_extensions import override @@ -738,43 +738,38 @@ def _save(self, output_dir: Optional[str] = None, state_dict=None): ).save_pretrained( output_dir, state_dict=state_dict, - safe_serialization=self.args.save_safetensors, ) else: LOG.info( "Trainer.model is not a `PreTrainedModel`, only saving its state dict." ) - if self.args.save_safetensors: - safetensors.torch.save_file( - state_dict, - os.path.join(output_dir, SAFE_WEIGHTS_NAME), - metadata={"format": "pt"}, - ) - else: - torch.save(state_dict, os.path.join(output_dir, WEIGHTS_NAME)) + safetensors.torch.save_file( + state_dict, + os.path.join(output_dir, SAFE_WEIGHTS_NAME), + metadata={"format": "pt"}, + ) else: self.model.save_pretrained( output_dir, state_dict=state_dict, - safe_serialization=self.args.save_safetensors, is_main_process=self.accelerator.is_main_process, ) - if self.processing_class is not None: - self.processing_class.save_pretrained(output_dir) - elif ( - self.data_collator is not None - and hasattr(self.data_collator, "tokenizer") - and self.data_collator.tokenizer is not None - ): - LOG.info( - "Saving Trainer.data_collator.tokenizer by default as Trainer.processing_class is `None`" - ) - save_jinja_files = True - if self.axolotl_cfg: - save_jinja_files = self.axolotl_cfg.tokenizer_save_jinja_files - self.data_collator.tokenizer.save_pretrained( - output_dir, save_jinja_files=save_jinja_files - ) - # Good practice: save your training arguments together with the trained model - torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME)) + if self.processing_class is not None: + self.processing_class.save_pretrained(output_dir) + elif ( + self.data_collator is not None + and hasattr(self.data_collator, "tokenizer") + and self.data_collator.tokenizer is not None + ): + LOG.info( + "Saving Trainer.data_collator.tokenizer by default as Trainer.processing_class is `None`" + ) + save_jinja_files = True + if self.axolotl_cfg: + save_jinja_files = self.axolotl_cfg.tokenizer_save_jinja_files + self.data_collator.tokenizer.save_pretrained( + output_dir, save_jinja_files=save_jinja_files + ) + # Good practice: save your training arguments together with the trained model + torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME)) diff --git a/src/axolotl/integrations/llm_compressor/utils.py b/src/axolotl/integrations/llm_compressor/utils.py index f04454e5b3..1abcb6bd4b 100644 --- a/src/axolotl/integrations/llm_compressor/utils.py +++ b/src/axolotl/integrations/llm_compressor/utils.py @@ -12,7 +12,6 @@ def save_compressed_model( model: PreTrainedModel, output_dir: Union[str, bytes], trainer: Trainer, - safe_serialization: bool = False, save_compressed: bool = False, ) -> None: """ @@ -22,7 +21,6 @@ def save_compressed_model( model (PreTrainedModel): The model to be saved. output_dir (str or bytes): Path where the model files will be written. trainer (Trainer): Hugging Face Trainer for process synchronization. - safe_serialization (bool): Use safe serialization if True. save_compressed (bool): Write compressed tensors if True. """ trainer.accelerator.wait_for_everyone() @@ -34,7 +32,6 @@ def save_compressed_model( modify_save_pretrained(model) model.save_pretrained( output_dir, - safe_serialization=safe_serialization, save_compressed=save_compressed, skip_sparsity_compression_stats=not save_compressed, ) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 1eeed35656..0133148ebd 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -26,7 +26,6 @@ from transformers import ( AutoModelForCausalLM, AutoModelForImageTextToText, - AutoModelForVision2Seq, AwqConfig, BitsAndBytesConfig, GPTQConfig, @@ -434,7 +433,7 @@ def _set_auto_model_loader(self): """ if self.cfg.is_multimodal: self.auto_model_loader = MULTIMODAL_AUTO_MODEL_MAPPING.get( - self.model_config.model_type, AutoModelForVision2Seq + self.model_config.model_type, AutoModelForImageTextToText ) if isinstance(self.auto_model_loader, str): self.auto_model_loader = AutoModelForImageTextToText @@ -476,6 +475,7 @@ def _set_device_map_config(self): max_memory = None self.model_kwargs["torch_dtype"] = self.cfg.torch_dtype + self.model_kwargs["dtype"] = self.cfg.torch_dtype is_ds_zero3 = is_deepspeed_zero3_enabled() @@ -670,7 +670,7 @@ def _load_model_from_config(self, model_loader_class=None) -> PreTrainedModel: Uses the selected loader when provided; otherwise falls back to the auto loader. """ loader = model_loader_class or self.auto_model_loader - if loader in [AutoModelForCausalLM, AutoModelForVision2Seq]: + if loader in [AutoModelForCausalLM, AutoModelForImageTextToText]: model = loader.from_config( config=self.model_config, trust_remote_code=self.cfg.trust_remote_code or False, @@ -788,6 +788,7 @@ def _build_model(self) -> bool: # Use auto model loader (handles gptq and default cases) model_loader_class = self.auto_model_loader + self.model_kwargs["dtype"] = self.model_kwargs["torch_dtype"] if self.cfg.reinit_weights: self.model = self._load_model_from_config(model_loader_class) else: diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index b7a53c4d5e..30c3ba0fd9 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -220,13 +220,6 @@ def _apply_model_specific_patches(self): patch_qwen3_next_modeling_packing() - if self.cfg.model_config_type == "mistral3" and self.cfg.processor_type: - from axolotl.monkeypatch.models.mistral3.mistral_common_tokenizer import ( - apply_mistral_tokenizer_image_patch, - ) - - apply_mistral_tokenizer_image_patch() - if self.cfg.model_config_type == "kimi_linear": from axolotl.monkeypatch.models.kimi_linear.patch_kimi_linear import ( patch_kimi_model, diff --git a/src/axolotl/loaders/processor.py b/src/axolotl/loaders/processor.py index 827b4be35b..124dad39ee 100644 --- a/src/axolotl/loaders/processor.py +++ b/src/axolotl/loaders/processor.py @@ -31,7 +31,7 @@ def _patch_mistralcommontokenizer(): from axolotl.utils.mistral import HFMistralTokenizer - tokenization_mistral_common.MistralCommonTokenizer = HFMistralTokenizer + tokenization_mistral_common.MistralCommonBackend = HFMistralTokenizer _patch_mistralcommontokenizer() diff --git a/src/axolotl/models/mamba/modeling_mamba.py b/src/axolotl/models/mamba/modeling_mamba.py index 2cfe115444..e6158a0a9a 100644 --- a/src/axolotl/models/mamba/modeling_mamba.py +++ b/src/axolotl/models/mamba/modeling_mamba.py @@ -111,7 +111,6 @@ def save_pretrained( self, save_directory: Union[str, os.PathLike], state_dict: Optional[dict] = None, - safe_serialization: Optional[bool] = None, ): if state_dict is None: state_dict = self.state_dict() diff --git a/src/axolotl/monkeypatch/models/mistral3/mistral_common_tokenizer.py b/src/axolotl/monkeypatch/models/mistral3/mistral_common_tokenizer.py index 9e7259a05c..a77a0129eb 100644 --- a/src/axolotl/monkeypatch/models/mistral3/mistral_common_tokenizer.py +++ b/src/axolotl/monkeypatch/models/mistral3/mistral_common_tokenizer.py @@ -1,5 +1,5 @@ """ -Monkeypatch to fix inefficient tensor conversion in MistralCommonTokenizer.apply_chat_template +Monkeypatch to fix inefficient tensor conversion in MistralCommonBackend.apply_chat_template """ import importlib @@ -12,11 +12,11 @@ def apply_mistral_tokenizer_image_patch(): - """Apply patch to MistralCommonTokenizer.apply_chat_template to fix image tensor conversion.""" - from transformers.tokenization_mistral_common import MistralCommonTokenizer + """Apply patch to MistralCommonBackend.apply_chat_template to fix image tensor conversion.""" + from transformers.tokenization_mistral_common import MistralCommonBackend # Get original source - original_source = inspect.getsource(MistralCommonTokenizer.apply_chat_template) + original_source = inspect.getsource(MistralCommonBackend.apply_chat_template) original_source, _ = detab_code(original_source) # Define the replacement @@ -41,7 +41,7 @@ def apply_mistral_tokenizer_image_patch(): ) # Load necessary imports from the module - module_name = MistralCommonTokenizer.__module__ + module_name = MistralCommonBackend.__module__ module = importlib.import_module(module_name) # Detect what needs to be imported @@ -79,7 +79,7 @@ def apply_mistral_tokenizer_image_patch(): exec(patched_source, globals()) # nosec B102 # Replace the method - MistralCommonTokenizer.apply_chat_template = patched_apply_chat_template - LOG.info("Successfully applied MistralCommonTokenizer tensor conversion patch") + MistralCommonBackend.apply_chat_template = patched_apply_chat_template + LOG.info("Successfully applied MistralCommonBackend tensor conversion patch") else: - LOG.warning("Could not find target code for MistralCommonTokenizer patching") + LOG.warning("Could not find target code for MistralCommonBackend patching") diff --git a/src/axolotl/monkeypatch/relora.py b/src/axolotl/monkeypatch/relora.py index a01d850b31..cf93c32dda 100644 --- a/src/axolotl/monkeypatch/relora.py +++ b/src/axolotl/monkeypatch/relora.py @@ -155,7 +155,6 @@ def on_step_begin( f"{PREFIX_CHECKPOINT_DIR}-{state.global_step}", "adapter", ), - safe_serialization=True, ) with torch.no_grad(): merge_and_save( @@ -214,7 +213,7 @@ def on_save( self.last_full_model = checkpoint_folder else: - model.model.save_pretrained(checkpoint_folder, safe_serialization=True) + model.model.save_pretrained(checkpoint_folder) return control diff --git a/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py b/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py index ba8b16ddac..15f90423e7 100644 --- a/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py +++ b/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py @@ -52,9 +52,15 @@ def patch_prepare_context_parallel_inputs() -> None: if item in patched_source: items_to_import.append(item) - exec(f"from {module_name} import ({', '.join(items_to_import)})", globals()) - exec(patched_source, globals()) + # Use a separate namespace to capture the exec'd function + namespace = {} + exec(f"from {module_name} import ({', '.join(items_to_import)})", namespace) + exec(patched_source, namespace) + # Explicitly get the function from the namespace + axolotl_prepare_context_parallel_inputs = namespace[ + "axolotl_prepare_context_parallel_inputs" + ] Trainer._original_prepare_context_parallel_inputs = ( Trainer._prepare_context_parallel_inputs ) diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index c209c892a8..077db43889 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -14,7 +14,6 @@ from axolotl.utils.dict import remove_none_values from axolotl.utils.logging import get_logger -from axolotl.utils.mistral.mistral3_processor import Mistral3Processor LOG = get_logger(__name__) @@ -430,7 +429,7 @@ class Mistral3ProcessingStrategy(ProcessingStrategy): def __init__( self, - processor: Mistral3Processor, + processor, chat_template: Optional[str] = None, image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, @@ -493,6 +492,8 @@ def get_processing_strategy( image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, ): + from axolotl.utils.mistral.mistral3_processor import Mistral3Processor + processing_kwargs = { "processor": processor, "chat_template": chat_template, diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 0fec64d81c..57d3bfdf21 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -150,6 +150,8 @@ def build_prompt( return self.tokenizer.apply_chat_template( conversation, + tokenize=True, + return_dict=False, **chat_template_kwargs, ) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index cce3b8a6a2..856996b62f 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -135,16 +135,13 @@ def setup_reference_model( return model_ref -def setup_signal_handler( - cfg: DictDefault, model: PreTrainedModel, safe_serialization: bool -): +def setup_signal_handler(cfg: DictDefault, model: PreTrainedModel): """ Set up signal handler for graceful termination. Args: cfg: Dictionary mapping `axolotl` config keys to values. model: The model to save on termination - safe_serialization: Whether to use safe serialization when saving """ # ray workers don't have access to this signal if cfg.local_rank == 0 and not cfg.use_ray: @@ -152,9 +149,7 @@ def setup_signal_handler( def terminate_handler(_, __, model_weakref): if model_weakref() is not None: _model = model_weakref() - _model.save_pretrained( - cfg.output_dir, safe_serialization=safe_serialization - ) + _model.save_pretrained(cfg.output_dir) cleanup_distributed() sys.exit(0) @@ -219,7 +214,6 @@ def save_trained_model( cfg: DictDefault, trainer: Any, model: PreTrainedModel, - safe_serialization: bool, ): """ Save the trained model according to configuration and training setup. @@ -228,7 +222,6 @@ def save_trained_model( cfg: Dictionary mapping `axolotl` config keys to values. trainer: The trainer object. model: The trained model to save. - safe_serialization: Whether to use safe serialization. """ LOG.info(f"Training completed! Saving trained model to {cfg.output_dir}.") @@ -283,7 +276,6 @@ def save_trained_model( merge_fsdp_weights( checkpoint_dir=str(fsdp_dir), output_path=merged_path, - safe_serialization=True, ) trainer.accelerator.wait_for_everyone() if trainer.accelerator.is_main_process: @@ -330,11 +322,9 @@ def save_trained_model( pass elif cfg.local_rank == 0: if cfg.rl and cfg.adapter and not cfg.rl_adapter_ref_model: - trainer.model.save_pretrained( - cfg.output_dir, safe_serialization=safe_serialization - ) + trainer.model.save_pretrained(cfg.output_dir) - model.save_pretrained(cfg.output_dir, safe_serialization=safe_serialization) + model.save_pretrained(cfg.output_dir) if hasattr(cfg, "llmcompressor") and cfg.llmcompressor: # TODO: add integration support so this can be implemented completely within the plugin @@ -344,7 +334,6 @@ def save_trained_model( model=model, output_dir=cfg.output_dir, trainer=trainer, - safe_serialization=safe_serialization, save_compressed=cfg.llmcompressor.save_compressed, ) @@ -449,7 +438,6 @@ def handle_untrained_tokens_fix( model: PreTrainedModel, tokenizer: PreTrainedTokenizer, train_dataset: Dataset, - safe_serialization: bool, ): """ Apply fixes for untrained tokens if configured. @@ -459,7 +447,6 @@ def handle_untrained_tokens_fix( model: The model to apply fixes to. tokenizer: The tokenizer for token identification. train_dataset: The training dataset to use. - safe_serialization: Whether to use safe serialization when saving. """ if not cfg.fix_untrained_tokens: return @@ -483,9 +470,7 @@ def handle_untrained_tokens_fix( fix_untrained_tokens(model, tokenizer, train_dataset, **fix_kwargs) if cfg.local_rank == 0: - model.save_pretrained( - str(Path(cfg.output_dir)), safe_serialization=safe_serialization - ) + model.save_pretrained(str(Path(cfg.output_dir))) def setup_model_and_trainer( @@ -582,15 +567,12 @@ def train( ) = setup_model_and_trainer(cfg, dataset_meta) # Handle untrained tokens if configured - safe_serialization = cfg.save_safetensors is True train_dataset = dataset_meta.train_dataset - handle_untrained_tokens_fix( - cfg, model, tokenizer, train_dataset, safe_serialization - ) + handle_untrained_tokens_fix(cfg, model, tokenizer, train_dataset) # Additional setup save_initial_configs(cfg, tokenizer, model, peft_config, processor) - setup_signal_handler(cfg, model, safe_serialization) + setup_signal_handler(cfg, model) setup_model_card(cfg) # Execute the training @@ -602,7 +584,7 @@ def train( torch.cuda.empty_cache() # Save the trained model and cleanup - save_trained_model(cfg, trainer, model, safe_serialization) + save_trained_model(cfg, trainer, model) tokenizer.save_pretrained( str(Path(cfg.output_dir)), save_jinja_files=cfg.tokenizer_save_jinja_files ) diff --git a/src/axolotl/utils/callbacks/perplexity.py b/src/axolotl/utils/callbacks/perplexity.py index a5b39c3046..36cacfb817 100644 --- a/src/axolotl/utils/callbacks/perplexity.py +++ b/src/axolotl/utils/callbacks/perplexity.py @@ -7,7 +7,11 @@ from tqdm import tqdm from transformers.modeling_outputs import CausalLMOutput from transformers.modeling_utils import PreTrainedModel -from transformers.tokenization_utils import PreTrainedTokenizer + +try: + from transformers.tokenization_python import PreTrainedTokenizer +except ImportError: + from transformers.tokenization_utils import PreTrainedTokenizer from axolotl.utils.distributed import is_main_process diff --git a/src/axolotl/utils/mistral/mistral_tokenizer.py b/src/axolotl/utils/mistral/mistral_tokenizer.py index 3ce6be7800..a5ffe9a283 100644 --- a/src/axolotl/utils/mistral/mistral_tokenizer.py +++ b/src/axolotl/utils/mistral/mistral_tokenizer.py @@ -7,11 +7,11 @@ from mistral_common.protocol.instruct.validator import ValidationMode from mistral_common.tokens.tokenizers.utils import download_tokenizer_from_hf_hub from torch import Tensor -from transformers.tokenization_mistral_common import MistralCommonTokenizer +from transformers.tokenization_mistral_common import MistralCommonBackend from transformers.tokenization_utils_base import VERY_LARGE_INTEGER -class HFMistralTokenizer(MistralCommonTokenizer): +class HFMistralTokenizer(MistralCommonBackend): """ Wraps mistral_common.tokens.tokenizers.mistral.MistralTokenizer and exposes HuggingFace API for special tokens. @@ -37,11 +37,19 @@ def __init__(self, name_or_path: str, **kwargs): def name_or_path(self) -> str: return self._name_or_path + @name_or_path.setter + def name_or_path(self, name_or_path: str) -> None: + self._name_or_path = name_or_path + @property def chat_template(self) -> str | None: """Chat template is not supported. Dummy method to satisfy HuggingFace API.""" return "[This is a dummy chat template]" + @chat_template.setter + def chat_template(self, chat_template: str | None) -> None: + pass + def _set_mode(self, mode: ValidationMode): """Set the mode of the MistralRequestValidator. @@ -133,7 +141,7 @@ def from_pretrained( r""" Patched fn to pass `name_or_path` and remove extra kwargs. - Instantiate a `MistralCommonTokenizer` from a predefined + Instantiate a `MistralCommonBackend` from a predefined tokenizer. Args: @@ -142,7 +150,7 @@ def from_pretrained( - A string, the *model id* of a predefined tokenizer hosted inside a model repo on huggingface.co. - A path to a *directory* containing the tokenizer config, for instance saved - using the [`MistralCommonTokenizer.tokenization_mistral_common.save_pretrained`] method, e.g., + using the [`MistralCommonBackend.tokenization_mistral_common.save_pretrained`] method, e.g., `./my_model_directory/`. mode (`ValidationMode`, *optional*, defaults to `ValidationMode.test`): Validation mode for the `MistralTokenizer` tokenizer. @@ -154,7 +162,7 @@ def from_pretrained( exist. token (`str` or *bool*, *optional*): The token to use as HTTP bearer authorization for remote files. If `True`, will use the token generated - when running `huggingface-cli login` (stored in `~/.huggingface`). + when running `hf auth login` (stored in `~/.huggingface`). local_files_only (`bool`, *optional*, defaults to `False`): Whether or not to only rely on local files and not to attempt to download any files. revision (`str`, *optional*, defaults to `"main"`): @@ -179,12 +187,12 @@ def from_pretrained( Whether or not the model should cleanup the spaces that were added when splitting the input text during the tokenization process. kwargs (additional keyword arguments, *optional*): - Not supported by `MistralCommonTokenizer.from_pretrained`. + Not supported by `MistralCommonBackend.from_pretrained`. Will raise an error if used. """ if init_inputs: raise ValueError( - "`init_inputs` are not supported by `MistralCommonTokenizer.from_pretrained`." + "`init_inputs` are not supported by `MistralCommonBackend.from_pretrained`." ) # Delete trust_remote_code as it does nothing @@ -196,7 +204,7 @@ def from_pretrained( # Handle kwargs and AutoTokenizer case if kwargs and not kwargs.keys() == {"_from_auto"}: raise ValueError( - f"Kwargs {list(kwargs.keys())} are not supported by `MistralCommonTokenizer.from_pretrained`." + f"Kwargs {list(kwargs.keys())} are not supported by `MistralCommonBackend.from_pretrained`." ) if not os.path.isfile(pretrained_model_name_or_path): diff --git a/src/axolotl/utils/schemas/fsdp.py b/src/axolotl/utils/schemas/fsdp.py index f34f40e8e4..60b5819c59 100644 --- a/src/axolotl/utils/schemas/fsdp.py +++ b/src/axolotl/utils/schemas/fsdp.py @@ -4,7 +4,7 @@ from typing import Literal -from pydantic import BaseModel, Field +from pydantic import AliasChoices, BaseModel, Field class FSDPConfig(BaseModel): @@ -12,6 +12,11 @@ class FSDPConfig(BaseModel): FSDP Configuration Schema """ + fsdp_version: int | None = Field( + validation_alias=AliasChoices("fsdp_version", "version"), + default=None, + json_schema_extra={"description": "FSDP version"}, + ) activation_checkpointing: bool | None = Field( default=None, description="Enable activation checkpointing to reduce memory usage during forward passes", diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index 0931608a69..31de7b45ed 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -123,10 +123,22 @@ class ModelOutputConfig(BaseModel): save_safetensors: bool | None = Field( default=True, json_schema_extra={ - "description": "Save model as safetensors (require safetensors package). Default True" + "description": "Whether to save the model using safetensors format. Defaults to True." }, ) + @field_validator("save_safetensors") + @classmethod + def validate_save_safetensors(cls, v): + if v is False: + raise ValueError( + "save_safetensors=False is not supported in Transformers V5. " + "Transformers V5 always uses safetensors format for model serialization. " + "This field is deprecated and will be removed in a future version." + ) + # Allow None and True, will default to True if None + return True if v is None else v + class SpecialTokensConfig(BaseModel): """Special tokens configuration subset""" diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index bb9c3c673f..9f225b75e2 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -900,6 +900,43 @@ def check_fsdp2_base_model_quant_rl(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_fsdp_config_kwargs_prefix(cls, data): + if fsdp_config := data.get("fsdp_config"): + should_fix = False + for key, _ in fsdp_config.items(): + if key.startswith("fsdp_"): + should_fix = True + LOG.warning_once( + "Configuring FSDP fields with the `fsdp_` prefix is deprecated. " + "Please omit the `fsdp_` prefix from the any fields in `fsdp_config`." + ) + if should_fix: + update_fsdp_config = {} + for key, value in fsdp_config.items(): + if key.startswith("fsdp_") and key != "fsdp_version": + update_fsdp_config[key.replace("fsdp_", "")] = value + else: + update_fsdp_config[key] = value + data["fsdp_config"] = update_fsdp_config + return data + + @model_validator(mode="before") + @classmethod + def check_fsdp_version_in_fsdp_config(cls, data): + fsdp_config = data.get("fsdp_config") or {} + fsdp_version = data.get("fsdp_version", None) + if not fsdp_version and fsdp_config and fsdp_config.get("version"): + fsdp_cfg_version = fsdp_config.pop("version") + data["fsdp_version"] = fsdp_cfg_version + data["fsdp_config"]["fsdp_version"] = fsdp_cfg_version + elif not fsdp_version and fsdp_config and fsdp_config.get("fsdp_version"): + data["fsdp_version"] = fsdp_config.get("fsdp_version") + if fsdp_version and fsdp_config and not fsdp_config.get("fsdp_version"): + data["fsdp_config"]["fsdp_version"] = fsdp_version + return data + @model_validator(mode="after") def check_fsdp_offload_w_8bit_optimizer(self): if ( @@ -1001,40 +1038,6 @@ def check_deepcompile(cls, data): return data - @model_validator(mode="before") - @classmethod - def check_fsdp_version_in_fsdp_config(cls, data): - fsdp_config = data.get("fsdp_config") or {} - if fsdp_config and fsdp_config.get("fsdp_version"): - LOG.warning( - "Configuring `fsdp_version` in `fsdp_config` is deprecated. " - "Please configure `fsdp_version` as a top-level field." - ) - data["fsdp_version"] = fsdp_config.pop("fsdp_version") - return data - - @model_validator(mode="before") - @classmethod - def check_fsdp_config_kwargs_prefix(cls, data): - if fsdp_config := data.get("fsdp_config"): - should_fix = False - for key, _ in fsdp_config.items(): - if key.startswith("fsdp_"): - should_fix = True - LOG.warning_once( - "Configuring FSDP fields with the `fsdp_` prefix is deprecated. " - "Please omit the `fsdp_` prefix from the any fields in `fsdp_config`." - ) - if should_fix: - update_fsdp_config = {} - for key, value in fsdp_config.items(): - if key.startswith("fsdp_") and key != "fsdp_version": - update_fsdp_config[key.replace("fsdp_", "")] = value - else: - update_fsdp_config[key] = value - data["fsdp_config"] = update_fsdp_config - return data - class SystemValidationMixin: """Validation methods related to system and hardware configuration.""" diff --git a/tests/conftest.py b/tests/conftest.py index 4c8c80cb7f..b542d377ba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -83,6 +83,12 @@ def download_smollm2_135m_model(): snapshot_download_w_retry("HuggingFaceTB/SmolLM2-135M", repo_type="model") +@pytest.fixture(scope="session", autouse=True) +def download_smollm2_135m_instruct_model(): + # download the model + snapshot_download_w_retry("HuggingFaceTB/SmolLM2-135M-Instruct", repo_type="model") + + @pytest.fixture(scope="session", autouse=True) def download_smollm2_135m_gptq_model(): # download the model @@ -143,12 +149,20 @@ def download_argilla_distilabel_intel_orca_dpo_dataset(): ) -# @pytest.fixture(scope="session", autouse=True) -# def download_argilla_ultrafeedback_binarized_preferences_cleaned_dataset(): -# # download the dataset -# snapshot_download_w_retry( -# "argilla/ultrafeedback-binarized-preferences-cleaned", repo_type="dataset" -# ) +@pytest.fixture(scope="session", autouse=True) +def download_argilla_ultrafeedback_binarized_preferences_cleaned_dataset(): + # download the dataset + snapshot_download_w_retry( + "argilla/ultrafeedback-binarized-preferences-cleaned", repo_type="dataset" + ) + + +@pytest.fixture(scope="session", autouse=True) +def download_argilla_ultrafeedback_binarized_preferences_cleaned_kto_dataset(): + # download the dataset + snapshot_download_w_retry( + "argilla/ultrafeedback-binarized-preferences-cleaned-kto", repo_type="dataset" + ) # @pytest.fixture(scope="session", autouse=True) @@ -251,7 +265,9 @@ def download_llama_1b_model_fixture(): def download_llama3_8b_model_fixture(): # download the tokenizer only snapshot_download_w_retry( - "NousResearch/Meta-Llama-3-8B", repo_type="model", allow_patterns=["*token*"] + "NousResearch/Meta-Llama-3-8B", + repo_type="model", + allow_patterns=["*token*", "config.json"], ) @@ -261,7 +277,7 @@ def download_llama3_8b_instruct_model_fixture(): snapshot_download_w_retry( "NousResearch/Meta-Llama-3-8B-Instruct", repo_type="model", - allow_patterns=["*token*"], + allow_patterns=["*token*", "config.json"], ) @@ -269,7 +285,19 @@ def download_llama3_8b_instruct_model_fixture(): def download_phi_35_mini_model_fixture(): # download the tokenizer only snapshot_download_w_retry( - "microsoft/Phi-3.5-mini-instruct", repo_type="model", allow_patterns=["*token*"] + "microsoft/Phi-3.5-mini-instruct", + repo_type="model", + allow_patterns=["*token*", "config.json"], + ) + + +@pytest.fixture(scope="session", autouse=True) +def download_phi_4_reasoning_model_fixture(): + # download the tokenizer only + snapshot_download_w_retry( + "microsoft/Phi-4-reasoning", + repo_type="model", + allow_patterns=["*token*", "config.json"], ) @@ -279,7 +307,7 @@ def download_phi_3_medium_model_fixture(): snapshot_download_w_retry( "microsoft/Phi-3-medium-128k-instruct", repo_type="model", - allow_patterns=["*token*"], + allow_patterns=["*token*", "config.json"], ) @@ -562,6 +590,8 @@ def test_load_fixtures( download_mhenrichsen_alpaca_2k_dataset, download_mhenrichsen_alpaca_2k_w_revision_dataset, download_mlabonne_finetome_100k_dataset, + download_argilla_ultrafeedback_binarized_preferences_cleaned_dataset, + download_argilla_ultrafeedback_binarized_preferences_cleaned_kto_dataset, download_argilla_distilabel_capybara_dpo_7k_binarized_dataset, download_arcee_ai_distilabel_intel_orca_dpo_pairs_dataset, download_argilla_dpo_pairs_dataset, @@ -573,6 +603,7 @@ def test_load_fixtures( download_llama3_8b_instruct_model_fixture, download_phi_35_mini_model_fixture, download_phi_3_medium_model_fixture, + download_phi_4_reasoning_model_fixture, download_mistral_7b_model_fixture, download_gemma_2b_model_fixture, download_gemma2_9b_model_fixture, diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index c2d81cbcbb..5f14811018 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -53,7 +53,6 @@ def fixture_base_cfg(): # Checkpointing and saving "save_steps": 100, "output_dir": "./model-out", - "save_safetensors": True, "save_total_limit": 4, "save_only_model": False, # Hardware/performance settings diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py index 1ba05077cf..7da644ec36 100644 --- a/tests/e2e/integrations/test_cut_cross_entropy.py +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -10,7 +10,7 @@ from axolotl.utils.config import normalize_config, prepare_plugins, validate_config from axolotl.utils.dict import DictDefault -from ..utils import check_model_output_exists +from tests.e2e.utils import check_model_output_exists @pytest.fixture() @@ -39,7 +39,6 @@ def min_cfg(temp_dir): "optimizer": "adamw_torch_fused", "output_dir": temp_dir, "lr_scheduler": "cosine", - "save_safetensors": True, "max_steps": 10, "bf16": "auto", "save_first_step": False, @@ -92,7 +91,6 @@ def test_qwen2_w_cce(self, temp_dir): "optimizer": "adamw_torch_fused", "output_dir": temp_dir, "lr_scheduler": "cosine", - "save_safetensors": True, "max_steps": 10, "bf16": "auto", "save_first_step": False, diff --git a/tests/e2e/integrations/test_fp8.py b/tests/e2e/integrations/test_fp8.py index 7db63cc4d3..b708e8806e 100644 --- a/tests/e2e/integrations/test_fp8.py +++ b/tests/e2e/integrations/test_fp8.py @@ -48,7 +48,6 @@ def test_fp8_single_gpu_smoke(self, temp_dir): "sample_packing": True, "fp8": True, "torch_compile": True, - "save_safetensors": True, "save_first_step": False, } ) diff --git a/tests/e2e/integrations/test_hooks.py b/tests/e2e/integrations/test_hooks.py index b85505caa4..e056d14912 100644 --- a/tests/e2e/integrations/test_hooks.py +++ b/tests/e2e/integrations/test_hooks.py @@ -11,7 +11,7 @@ from axolotl.utils.config import normalize_config, prepare_plugins, validate_config from axolotl.utils.dict import DictDefault -from ..utils import check_model_output_exists +from tests.e2e.utils import check_model_output_exists class LogHooksPlugin(BasePlugin): diff --git a/tests/e2e/integrations/test_kd.py b/tests/e2e/integrations/test_kd.py index d89044247b..9e0e9406b0 100644 --- a/tests/e2e/integrations/test_kd.py +++ b/tests/e2e/integrations/test_kd.py @@ -65,7 +65,6 @@ def min_cfg(temp_dir): }, "max_steps": 5, "output_dir": temp_dir, - "save_safetensors": True, "use_tensorboard": True, "save_first_step": False, } diff --git a/tests/e2e/integrations/test_liger.py b/tests/e2e/integrations/test_liger.py index e50483e6c0..ba19cf41c2 100644 --- a/tests/e2e/integrations/test_liger.py +++ b/tests/e2e/integrations/test_liger.py @@ -48,7 +48,6 @@ def test_llama_wo_flce(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "save_safetensors": True, "bf16": "auto", "max_steps": 5, "save_first_step": False, @@ -99,7 +98,6 @@ def test_llama_w_flce(self, temp_dir, liger_use_token_scaling): "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "save_safetensors": True, "bf16": "auto", "max_steps": 5, "save_first_step": False, diff --git a/tests/e2e/integrations/test_llm_compressor.py b/tests/e2e/integrations/test_llm_compressor.py index dceecea9ff..5804bca100 100644 --- a/tests/e2e/integrations/test_llm_compressor.py +++ b/tests/e2e/integrations/test_llm_compressor.py @@ -57,7 +57,6 @@ def test_llmcompressor_plugin( "learning_rate": 1e-5, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "save_safetensors": True, "bf16": "auto", "max_steps": 5, "llmcompressor": { diff --git a/tests/e2e/multigpu/solo/test_grpo.py b/tests/e2e/multigpu/solo/test_grpo.py index 257a388d02..8d0bc3f680 100644 --- a/tests/e2e/multigpu/solo/test_grpo.py +++ b/tests/e2e/multigpu/solo/test_grpo.py @@ -220,7 +220,6 @@ def test_llama_dora(self, temp_dir, num_gpus): "learning_rate": 0.0001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, "save_first_step": False, @@ -315,7 +314,6 @@ def test_llama_lora_sp(self, temp_dir): "learning_rate": 0.0001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, "save_first_step": False, @@ -408,7 +406,6 @@ def test_llama_fft(self, temp_dir, num_gpus): "learning_rate": 0.0001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, "save_first_step": False, diff --git a/tests/e2e/multigpu/test_fp8_fsdp2.py b/tests/e2e/multigpu/test_fp8_fsdp2.py index dc369f3ded..8d7c01ce89 100644 --- a/tests/e2e/multigpu/test_fp8_fsdp2.py +++ b/tests/e2e/multigpu/test_fp8_fsdp2.py @@ -11,7 +11,7 @@ from axolotl.utils.dict import DictDefault -from tests.e2e.utils import most_recent_subdir, require_hopper, require_torch_2_7_0 +from tests.e2e.utils import most_recent_subdir, require_torch_2_7_0, supports_fp8 AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent @@ -49,7 +49,7 @@ class TestFP8FSDP2: """Test class for FP8 mixed precision with FSDP2 functionality.""" @require_torch_2_7_0 - @require_hopper + @supports_fp8 def test_fp8_fsdp2_smoke(self, temp_dir): """Smoke test for 2-GPU FP8 + torch.compile + FSDP2 training""" cfg = DictDefault( @@ -94,7 +94,6 @@ def test_fp8_fsdp2_smoke(self, temp_dir): "reshard_after_forward": True, }, "use_tensorboard": True, - "save_safetensors": True, "save_first_step": False, } ) diff --git a/tests/e2e/multigpu/test_fsdp1.py b/tests/e2e/multigpu/test_fsdp1.py index cb92c80b5a..e503162872 100644 --- a/tests/e2e/multigpu/test_fsdp1.py +++ b/tests/e2e/multigpu/test_fsdp1.py @@ -244,6 +244,7 @@ def test_dpo_fft(self, temp_dir): verify_training_success(temp_dir) + @pytest.mark.skip("broken in transformers v5") @pytest.mark.parametrize( "adapter_config", [ diff --git a/tests/e2e/multigpu/test_fsdp2.py b/tests/e2e/multigpu/test_fsdp2.py index 8b7ee710e5..19239a3eca 100644 --- a/tests/e2e/multigpu/test_fsdp2.py +++ b/tests/e2e/multigpu/test_fsdp2.py @@ -150,6 +150,10 @@ def test_lora_sft(self, temp_dir, peft_use_dora): }, "use_tensorboard": True, "bf16": True, + # explicitly disable LORA kernels, as they may be auto-enabled + "lora_mlp_kernel": False, + "lora_qkv_kernel": False, + "lora_o_kernel": False, } ) diff --git a/tests/e2e/multigpu/test_gemma3.py b/tests/e2e/multigpu/test_gemma3.py index 51ec68b116..34f98c037a 100644 --- a/tests/e2e/multigpu/test_gemma3.py +++ b/tests/e2e/multigpu/test_gemma3.py @@ -23,6 +23,7 @@ def download_model(): snapshot_download("axolotl-mirrors/gemma-3-4b-pt", repo_type="model") +@pytest.mark.skip(reason="FIXME") class TestMultiGPUGemma3: """ Test case for Gemma3 models using LoRA @@ -32,6 +33,7 @@ def test_lora_ddp_packed(self, temp_dir): cfg = DictDefault( { "base_model": "axolotl-mirrors/gemma-3-4b-pt", + "unfrozen_parameters": ["model.language_model.*", "lm_head"], "sequence_len": 2048, "ddp_find_unused_parameters": True, "sample_packing": True, diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 3383e71d14..1e3757dcfa 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -901,7 +901,6 @@ def test_fix_untrained_tokens(self, temp_dir): "flash_attention": True, "sample_packing": True, "bf16": True, - "save_safetensors": True, # "deepspeed": str(AXOLOTL_ROOT / "deepspeed_configs/zero1.json"), "use_tensorboard": True, "save_first_step": False, diff --git a/tests/e2e/patched/test_activation_checkpointing.py b/tests/e2e/patched/test_activation_checkpointing.py index e8006c1627..e530978282 100644 --- a/tests/e2e/patched/test_activation_checkpointing.py +++ b/tests/e2e/patched/test_activation_checkpointing.py @@ -66,7 +66,6 @@ def test_activation_checkpointing_offload( "flash_attention": True, "sample_packing": True, "bf16": True, - "save_safetensors": True, "gradient_checkpointing": gradient_checkpointing, "save_first_step": False, "dataset_num_proc": 4, diff --git a/tests/e2e/patched/test_peft_embeddings.py b/tests/e2e/patched/test_peft_embeddings.py index 374ef97d87..ae3145de88 100644 --- a/tests/e2e/patched/test_peft_embeddings.py +++ b/tests/e2e/patched/test_peft_embeddings.py @@ -46,7 +46,6 @@ def test_peft_embeddings_upcast(self, temp_dir): "flash_attention": True, "sample_packing": False, "bf16": "auto", - "save_safetensors": True, "embeddings_skip_upcast": True, "save_first_step": False, } diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index e6240f2088..f6c7585c32 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -58,7 +58,6 @@ def test_resume_lora_packed(self, temp_dir): "save_total_limit": 5, "max_steps": 15, "use_tensorboard": True, - "save_safetensors": True, "save_first_step": False, "include_tkps": True, } diff --git a/tests/e2e/solo/test_relora_llama.py b/tests/e2e/solo/test_relora_llama.py index be77684bac..091bb90c68 100644 --- a/tests/e2e/solo/test_relora_llama.py +++ b/tests/e2e/solo/test_relora_llama.py @@ -63,7 +63,6 @@ def test_relora(self, temp_dir): "learning_rate": 0.00001, "optimizer": "adamw_8bit", "lr_scheduler": "cosine", - "save_safetensors": True, "use_tensorboard": True, "save_first_step": False, } diff --git a/tests/e2e/test_activation_offloading.py b/tests/e2e/test_activation_offloading.py index 9df85ab31d..5715e68baa 100644 --- a/tests/e2e/test_activation_offloading.py +++ b/tests/e2e/test_activation_offloading.py @@ -57,7 +57,6 @@ def test_activation_offloading( "flash_attention": True, "sample_packing": True, "bf16": "auto", - "save_safetensors": True, "gradient_checkpointing": True, "activation_offloading": True, "save_first_step": False, diff --git a/tests/e2e/test_deepseekv3.py b/tests/e2e/test_deepseekv3.py index e11be82657..0e3aafaf0a 100644 --- a/tests/e2e/test_deepseekv3.py +++ b/tests/e2e/test_deepseekv3.py @@ -64,7 +64,6 @@ def test_lora_deepseekv3(self, temp_dir, sample_packing): "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", "max_steps": 5, - "save_safetensors": True, "bf16": True, "save_first_step": False, } @@ -113,7 +112,6 @@ def test_fft_deepseekv3(self, temp_dir, sample_packing): "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", "max_steps": 5, - "save_safetensors": True, "bf16": True, "save_first_step": False, } diff --git a/tests/e2e/test_diffusion.py b/tests/e2e/test_diffusion.py index cc3d8070bf..89d35adc1c 100644 --- a/tests/e2e/test_diffusion.py +++ b/tests/e2e/test_diffusion.py @@ -41,7 +41,6 @@ def test_diffusion_smoke_test(self, temp_dir): "optimizer": "adamw_torch", "lr_scheduler": "cosine", "bf16": True, - "save_safetensors": True, "save_first_step": False, "logging_steps": 1, "eval_steps": 3, @@ -97,7 +96,6 @@ def test_diffusion_sft_labels(self, temp_dir): "optimizer": "adamw_torch", "lr_scheduler": "cosine", "bf16": True, - "save_safetensors": True, "save_first_step": False, "logging_steps": 1, "eval_steps": 2, diff --git a/tests/e2e/test_embeddings_lr.py b/tests/e2e/test_embeddings_lr.py index 633e449ef2..2b2e8e5e8d 100644 --- a/tests/e2e/test_embeddings_lr.py +++ b/tests/e2e/test_embeddings_lr.py @@ -44,7 +44,6 @@ def test_train_w_embedding_lr_scale(self, temp_dir): "optimizer": "adamw_torch_fused", "embedding_lr_scale": 0.5, "lr_scheduler": "cosine", - "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, "save_first_step": False, @@ -89,7 +88,6 @@ def test_train_w_embedding_lr(self, temp_dir): "optimizer": "adamw_torch_fused", "embedding_lr": 0.000005, "lr_scheduler": "cosine", - "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, "save_first_step": False, diff --git a/tests/e2e/test_gemma2.py b/tests/e2e/test_gemma2.py index 9e9f1a9ccb..1719deeeed 100644 --- a/tests/e2e/test_gemma2.py +++ b/tests/e2e/test_gemma2.py @@ -61,7 +61,6 @@ def test_lora_gemma2(self, temp_dir, sample_packing): "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", "max_steps": 5, - "save_safetensors": True, "bf16": True, } ) @@ -111,7 +110,6 @@ def test_fft_gemma2(self, temp_dir, sample_packing): "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", "max_steps": 5, - "save_safetensors": True, "bf16": True, } ) diff --git a/tests/e2e/test_gemma3_text.py b/tests/e2e/test_gemma3_text.py index 6cd9992426..b723f21e53 100644 --- a/tests/e2e/test_gemma3_text.py +++ b/tests/e2e/test_gemma3_text.py @@ -60,7 +60,6 @@ def test_lora_gemma3_text(self, temp_dir, sample_packing): "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", "max_steps": 5, - "save_safetensors": True, "bf16": True, "save_first_step": False, } @@ -110,7 +109,6 @@ def test_fft_gemma3_text(self, temp_dir, sample_packing): "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", "max_steps": 5, - "save_safetensors": True, "bf16": True, "save_first_step": False, } diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index de085cbe27..795b0de378 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -43,7 +43,6 @@ def test_fft_trust_remote_code(self, temp_dir): "flash_attention": True, "sample_packing": True, "bf16": True, - "save_safetensors": True, "save_first_step": False, } ) @@ -90,7 +89,6 @@ def test_fix_untrained_tokens(self, temp_dir): "flash_attention": True, "sample_packing": True, "bf16": True, - "save_safetensors": True, "save_first_step": False, } ) @@ -134,7 +132,6 @@ def test_fix_untrained_tokens_already_trained(self, temp_dir): "flash_attention": True, "sample_packing": True, "bf16": True, - "save_safetensors": True, "save_first_step": False, } ) @@ -174,7 +171,6 @@ def test_batch_flattening(self, temp_dir): "sample_packing": False, "batch_flattening": True, "bf16": True, - "save_safetensors": True, "save_first_step": False, } ) diff --git a/tests/e2e/test_llama_pretrain.py b/tests/e2e/test_llama_pretrain.py index f0daa9dd6e..3aa594fbdf 100644 --- a/tests/e2e/test_llama_pretrain.py +++ b/tests/e2e/test_llama_pretrain.py @@ -49,7 +49,6 @@ def test_pretrain(self, temp_dir, sample_packing, pretrain_multipack_attn): "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, "save_first_step": False, diff --git a/tests/e2e/test_llama_vision.py b/tests/e2e/test_llama_vision.py index 0cc927f769..edfc7a9b3a 100644 --- a/tests/e2e/test_llama_vision.py +++ b/tests/e2e/test_llama_vision.py @@ -51,7 +51,6 @@ def test_lora_llama_vision_text_only_dataset(self, temp_dir): "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", "max_steps": 5, - "save_safetensors": True, "bf16": True, "save_first_step": False, } @@ -97,7 +96,6 @@ def test_lora_llama_vision_multimodal_dataset(self, temp_dir): "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", "max_steps": 5, - "save_safetensors": True, "bf16": True, "save_first_step": False, } diff --git a/tests/e2e/test_mamba.py b/tests/e2e/test_mamba.py index 67935377dc..c45026bf55 100644 --- a/tests/e2e/test_mamba.py +++ b/tests/e2e/test_mamba.py @@ -49,7 +49,6 @@ def test_fft(self, temp_dir): "max_steps": 20, "save_steps": 10, "eval_steps": None, - "save_safetensors": False, "save_first_step": False, } ) diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index dbea92a5b3..de6c41fbeb 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -224,7 +224,6 @@ def test_fft_schedule_free_adamw(self, temp_dir): "learning_rate": 0.00001, "optimizer": "schedule_free_adamw", "lr_scheduler": "constant", - "save_safetensors": True, "max_steps": 10, "save_first_step": False, } diff --git a/tests/e2e/test_qat.py b/tests/e2e/test_qat.py index 2f8398ef7c..5cfbc85530 100644 --- a/tests/e2e/test_qat.py +++ b/tests/e2e/test_qat.py @@ -54,7 +54,6 @@ def test_qat(self, temp_dir): "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", "max_steps": 5, - "save_safetensors": True, "bf16": True, "save_first_step": False, } diff --git a/tests/e2e/test_save_first_step.py b/tests/e2e/test_save_first_step.py index ce2d3f1454..c717edb6a4 100644 --- a/tests/e2e/test_save_first_step.py +++ b/tests/e2e/test_save_first_step.py @@ -46,7 +46,6 @@ def test_save_first_step(self, temp_dir): "flash_attention": True, "sample_packing": True, "bf16": True, - "save_safetensors": True, "save_first_step": True, } ) @@ -86,7 +85,6 @@ def test_no_save_first_step(self, temp_dir): "flash_attention": True, "sample_packing": True, "bf16": True, - "save_safetensors": True, "save_first_step": False, } ) diff --git a/tests/e2e/test_streaming.py b/tests/e2e/test_streaming.py index 5dccf00dd2..125eb43eb0 100644 --- a/tests/e2e/test_streaming.py +++ b/tests/e2e/test_streaming.py @@ -50,7 +50,6 @@ def test_streaming_dataset(self, temp_dir, sample_packing): "learning_rate": 0.00001, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "save_safetensors": True, "bf16": "auto", "use_tensorboard": True, "save_first_step": False, diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index a2dd8bc5ef..842cbf1188 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -167,6 +167,13 @@ def require_hopper(test_case): return unittest.skipUnless(is_hopper(), "test requires h100/hopper GPU")(test_case) +def supports_fp8(test_case): + compute_capability = torch.cuda.get_device_capability() + return unittest.skipUnless( + compute_capability >= (9, 0), "test requires h100 or newer GPU" + )(test_case) + + def check_tensorboard( temp_run_dir: str, tag: str, @@ -193,21 +200,10 @@ def check_model_output_exists(temp_dir: str, cfg: DictDefault) -> None: """ helper function to check if a model output file exists after training - checks based on adapter or not and if safetensors saves are enabled or not + checks based on adapter or not (always safetensors in Transformers V5) """ - if cfg.save_safetensors: - if not cfg.adapter: - assert (Path(temp_dir) / "model.safetensors").exists() - else: - assert (Path(temp_dir) / "adapter_model.safetensors").exists() + if not cfg.adapter: + assert (Path(temp_dir) / "model.safetensors").exists() else: - # check for both, b/c in trl, it often defaults to saving safetensors - if not cfg.adapter: - assert (Path(temp_dir) / "pytorch_model.bin").exists() or ( - Path(temp_dir) / "model.safetensors" - ).exists() - else: - assert (Path(temp_dir) / "adapter_model.bin").exists() or ( - Path(temp_dir) / "adapter_model.safetensors" - ).exists() + assert (Path(temp_dir) / "adapter_model.safetensors").exists() diff --git a/tests/hf_offline_utils.py b/tests/hf_offline_utils.py index 221db1c515..b93b83f9fc 100644 --- a/tests/hf_offline_utils.py +++ b/tests/hf_offline_utils.py @@ -13,6 +13,7 @@ def reload_modules(hf_hub_offline): import datasets import huggingface_hub.constants + # from huggingface_hub.utils import reset_sessions # Reload the constants module first, as others depend on it importlib.reload(huggingface_hub.constants) diff --git a/tests/monkeypatch/test_mistral_tokenizer_patch.py b/tests/monkeypatch/test_mistral_tokenizer_patch.py deleted file mode 100644 index cb82c08902..0000000000 --- a/tests/monkeypatch/test_mistral_tokenizer_patch.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Integration tests for MistralCommonTokenizer patches.""" - -import pytest - - -class TestMistralTokenizerPatchIntegration: - """Test MistralCommonTokenizer patch integration.""" - - @pytest.mark.integration - def test_mistral_tokenizer_image_patch(self): - """Test that MistralCommonTokenizer image patch can be applied.""" - try: - from transformers.tokenization_mistral_common import MistralCommonTokenizer - except ImportError: - pytest.skip("MistralCommonTokenizer not available") - - from axolotl.monkeypatch.models.mistral3.mistral_common_tokenizer import ( - apply_mistral_tokenizer_image_patch, - ) - - # Store original method - original_apply_chat_template = MistralCommonTokenizer.apply_chat_template - - # Apply patch - apply_mistral_tokenizer_image_patch() - - # Verify patch was applied - assert ( - MistralCommonTokenizer.apply_chat_template != original_apply_chat_template - ), "apply_chat_template was not patched" - - # Verify the method is still callable - assert callable(MistralCommonTokenizer.apply_chat_template), ( - "Patched method is not callable" - ) diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index fd39a43059..7d4e6883f6 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -37,7 +37,7 @@ "gemma2_tokenizer_chat_template_jinja", "", ), - ("phi35_tokenizer", "phi_35", None, "<|end|>"), + # ("phi35_tokenizer", "phi_35", None, "<|end|>"), # seems to be broken w transformers v5 ("phi4_tokenizer", "phi_4", None, "<|im_end|>"), ] diff --git a/tests/test_normalize_config.py b/tests/test_normalize_config.py index f0d3a2d724..ae93a8bd22 100644 --- a/tests/test_normalize_config.py +++ b/tests/test_normalize_config.py @@ -127,8 +127,7 @@ def test_migrate_fsdp_config(self): self.assertNotIn("fsdp_auto_wrap_policy", cfg_with_version.fsdp_config) self.assertNotIn("fsdp_offload_params", cfg_with_version.fsdp_config) self.assertNotIn("fsdp_cpu_ram_efficient_loading", cfg_with_version.fsdp_config) - self.assertNotIn("fsdp_version", cfg_with_version.fsdp_config) - self.assertNotIn("version", cfg_with_version.fsdp_config) + self.assertIn("fsdp_version", cfg_with_version.fsdp_config) cfg_without_version = self._get_base_cfg() | DictDefault( { @@ -191,9 +190,7 @@ def test_migrate_fsdp_config_mixed_keys(self): self.assertEqual(cfg.fsdp_config.activation_checkpointing, True) # Check original fsdp_ keys are removed - self.assertNotIn("fsdp_version", cfg.fsdp_config) self.assertNotIn("fsdp_state_dict_type", cfg.fsdp_config) self.assertNotIn("fsdp_reshard_after_forward", cfg.fsdp_config) - # Ensure no duplicate version key - self.assertNotIn("version", cfg.fsdp_config) + self.assertIn("fsdp_version", cfg.fsdp_config) diff --git a/tests/test_perplexity.py b/tests/test_perplexity.py index 8f43069947..899308ba66 100644 --- a/tests/test_perplexity.py +++ b/tests/test_perplexity.py @@ -16,7 +16,9 @@ def metric(tokenizer): @fixture() def model(): - return AutoModelForCausalLM.from_pretrained(MODEL_NAME, trust_remote_code=True) + return AutoModelForCausalLM.from_pretrained( + MODEL_NAME, trust_remote_code=True, dtype="float32" + ) @fixture() diff --git a/tests/test_tokenizers.py b/tests/test_tokenizers.py index 4064620387..114c2bea2d 100644 --- a/tests/test_tokenizers.py +++ b/tests/test_tokenizers.py @@ -17,6 +17,7 @@ class TestTokenizers: test class for the load_tokenizer fn """ + @pytest.mark.skip("LlamaTokenizer no longer has a Fast/Slow tokenizer") @enable_hf_offline def test_default_use_fast(self): cfg = DictDefault( @@ -27,6 +28,7 @@ def test_default_use_fast(self): tokenizer = load_tokenizer(cfg) assert "Fast" in tokenizer.__class__.__name__ + @pytest.mark.skip("LlamaTokenizer no longer has a Fast/Slow tokenizer") @enable_hf_offline def test_dont_use_fast(self): cfg = DictDefault( diff --git a/tests/utils/schemas/validation/test_fsdp.py b/tests/utils/schemas/validation/test_fsdp.py index 9fa3277975..ce3f3aa07b 100644 --- a/tests/utils/schemas/validation/test_fsdp.py +++ b/tests/utils/schemas/validation/test_fsdp.py @@ -13,17 +13,29 @@ class TestFSDPValidation: test class for pydantic fsdp validation """ + def test_fsdp_version_from_fsdp_config(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + fsdp_config={ + "version": 2, + }, + ) + cfg = validate_config( + cfg, + ) + assert cfg.fsdp_version == 2 + def test_fsdp_version_in_fsdp_config(self, min_base_cfg): cfg = min_base_cfg | DictDefault( + fsdp_version=2, fsdp_config={ - "fsdp_version": 2, + "reshard_after_forward": True, }, ) cfg = validate_config( cfg, ) assert cfg.fsdp_version == 2 - assert cfg.fsdp_config.fsdp_version is None + assert cfg.fsdp_config.fsdp_version == 2 def test_fsdp_offload_w_8bit_optim(self, min_base_cfg): cfg = min_base_cfg | DictDefault( @@ -116,9 +128,10 @@ def test_fsdp_prefixes_removed(self, min_base_cfg): ) cfg = validate_config(cfg) assert cfg.fsdp_version == 2 - assert cfg.fsdp_config.fsdp_version is None - for keys in cfg.fsdp_config.keys(): - assert not keys.startswith("fsdp_") + assert cfg.fsdp_config.fsdp_version == 2 + for key in cfg.fsdp_config.keys(): + if key != "fsdp_version": + assert not key.startswith("fsdp_") assert cfg.fsdp_config.auto_wrap_policy == "TRANSFORMER_BASED_WRAP" assert cfg.fsdp_config.transformer_layer_cls_to_wrap == "LlamaDecoderLayer" assert cfg.fsdp_config.reshard_after_forward is True From dd9ebaeba18e40233f3feb0f1cb4991ee6e27de2 Mon Sep 17 00:00:00 2001 From: salman Date: Wed, 28 Jan 2026 11:44:15 +0000 Subject: [PATCH 1106/1405] EAFT (#3366) [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip eaft * fix eaft loss fn * adding ref --------- Co-authored-by: Salman Mohammadi <“salman.mohammadi@outlook.com”> --- examples/eaft/eaft-example.yml | 77 ++++++++++++++++++++++++++++ src/axolotl/core/builders/causal.py | 12 +++++ src/axolotl/monkeypatch/loss/eaft.py | 51 ++++++++++++++++++ src/axolotl/utils/schemas/config.py | 18 +++++++ 4 files changed, 158 insertions(+) create mode 100644 examples/eaft/eaft-example.yml create mode 100644 src/axolotl/monkeypatch/loss/eaft.py diff --git a/examples/eaft/eaft-example.yml b/examples/eaft/eaft-example.yml new file mode 100644 index 0000000000..fed4179d21 --- /dev/null +++ b/examples/eaft/eaft-example.yml @@ -0,0 +1,77 @@ +base_model: google/gemma-3-1b-it + +model_type: Gemma3ForCausalLM +cls_model_config: Gemma3TextConfig + +# gemma3 doesn't seem to play nice with ddp +ddp_find_unused_parameters: true + +chat_template: gemma3 +eot_tokens: + - + +load_in_8bit: false +load_in_4bit: false +strict: false + +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +dataset_prepared_path: +val_set_size: 0 +output_dir: ./outputs/eaft-gemma-3-1b + +use_eaft: true +eaft_alpha: 1.0 +eaft_k: 20 + +sequence_len: 1024 +sample_packing: false + +adapter: +lora_model_dir: + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +eval_batch_size: 1 +max_steps: 1000 +evaluation_strategy: "no" +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 5e-5 + +train_on_inputs: false +group_by_length: false +bf16: auto +fp16: +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false + +early_stopping_patience: +resume_from_checkpoint: +local_rank: +logging_steps: 1 +xformers_attention: +flash_attention: true + +warmup_ratio: 0.1 +weight_decay: 0.0 +debug: +deepspeed: +fsdp: +fsdp_config: +special_tokens: diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 3a9f8ba1be..09bcff4505 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -373,6 +373,18 @@ def build(self, total_num_steps): # https://docs.nvidia.com/deeplearning/performance/dl-performance-matrix-multiplication/index.html data_collator_kwargs["pad_to_multiple_of"] = multiple + if self.cfg.use_eaft: + from functools import partial + + from axolotl.monkeypatch.loss.eaft import eaft_loss + + configured_eaft_loss = partial( + eaft_loss, + alpha=self.cfg.eaft_alpha if self.cfg.eaft_alpha is not None else 1.0, + k=self.cfg.eaft_k if self.cfg.eaft_k is not None else 20, + ) + trainer_kwargs["compute_loss_func"] = configured_eaft_loss + trainer_cls = self._get_trainer_cls() trainer_kwargs, trainer_cls = self.hook_pre_create_trainer( diff --git a/src/axolotl/monkeypatch/loss/eaft.py b/src/axolotl/monkeypatch/loss/eaft.py new file mode 100644 index 0000000000..150d4a005b --- /dev/null +++ b/src/axolotl/monkeypatch/loss/eaft.py @@ -0,0 +1,51 @@ +""" +eaft (entropy-aware focal training) loss implementation +weights examples by entropy approximation from top-k logits + +Reference: https://github.com/ymxyll/LlamaFactory-EAFT/blob/e2ce19e8efcc226450ee8f2b81dfe4e69f1f945d/src/llamafactory/train/trainer_utils.py +""" + +import torch +import torch.nn.functional as F + + +def eaft_loss(outputs, labels, num_items_in_batch=None, alpha=1.0, k=20): + """ + compute eaft loss with entropy weighting + + args: + outputs: model outputs containing logits + labels: target labels for computing loss + num_items_in_batch: for sample packing support + alpha: exponent for entropy weighting (default 1.0) + k: number of top logits for entropy approximation (default 20) + """ + logits = outputs.logits + + shift_logits = logits[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + + vocab_size = shift_logits.size(-1) + shift_logits_view = shift_logits.view(-1, vocab_size) + shift_labels_view = shift_labels.view(-1) + + mask = shift_labels_view != -100 + + with torch.no_grad(): + top_k_logits, _ = torch.topk( + shift_logits_view[mask].float(), k=min(k, vocab_size), dim=-1 + ) + top_k_probs = F.softmax(top_k_logits, dim=-1) + entropy = -(top_k_probs * torch.log(top_k_probs + 1e-10)).sum(dim=-1) + weights = torch.pow(entropy, alpha) + + loss_fct = torch.nn.CrossEntropyLoss(reduction="none") + per_token_loss = loss_fct(shift_logits_view[mask], shift_labels_view[mask]) + weighted_loss = per_token_loss * weights + + if num_items_in_batch is not None: + loss = weighted_loss.sum() / num_items_in_batch + else: + loss = weighted_loss.mean() + + return loss diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index da21df7aa6..3621c0d89e 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -676,6 +676,24 @@ class AxolotlInputConfig( "description": "Number of chunks to use for chunked cross entropy loss" }, ) + use_eaft: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Enable Entropy-Aware Focal Training loss (EAFT)" + }, + ) + eaft_alpha: float | None = Field( + default=1.0, + json_schema_extra={ + "description": "Exponent for entropy weighting in EAFT (default: 1.0)" + }, + ) + eaft_k: int | None = Field( + default=20, + json_schema_extra={ + "description": "Number of top logits for entropy approximation (default: 20)" + }, + ) tiled_mlp: bool | None = Field( default=None, From 3dd86d35b87c45084706347314393b2d40b6e9b5 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 28 Jan 2026 18:44:44 +0700 Subject: [PATCH 1107/1405] feat: add new cce support for glm series and exaone4 (#3373) [skip ci] --- examples/colab-notebooks/colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/integrations/cut_cross_entropy/README.md | 6 +++++- src/axolotl/integrations/cut_cross_entropy/__init__.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index be4f3f4a91..fe30068d73 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@318b7e2\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f4b5712\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 59e55c074f..e526352dd7 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@318b7e2"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f4b5712"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 771f446b08..67f1157c34 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@318b7e2" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f4b5712" ``` ## Usage @@ -36,6 +36,7 @@ plugins: - cohere - cohere2 - deepseek_v3 +- exaone4 - gemma - gemma2 - gemma3 @@ -45,8 +46,11 @@ plugins: - glm - glm4 - glm4_moe +- glm4_moe_lite +- glm46v - glm4v - glm4v_moe +- glm_image - gpt_oss - granite - granitemoe diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 3c059da4ce..1dbbf6759d 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@318b7e2"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f4b5712"`' ) From 6132a30cdabd877080bb02bff033d5ac14139ffd Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 28 Jan 2026 06:45:01 -0500 Subject: [PATCH 1108/1405] handle warnings from v5 upgrade (#3376) --- requirements.txt | 2 +- src/axolotl/cli/__init__.py | 2 +- src/axolotl/core/trainers/trl.py | 12 +++++------- src/axolotl/core/training_args.py | 6 +++++- tests/prompt_strategies/conftest.py | 2 ++ 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 21fdda2266..565224e924 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ transformers==5.0.0 accelerate==1.12.0 datasets==4.5.0 deepspeed>=0.18.3 -trl==0.27.0 +trl==0.27.1 hf_xet==1.2.0 kernels==0.11.5 diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index fa647be651..6d07548063 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -5,6 +5,6 @@ from axolotl.logging_config import configure_logging os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") -os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1") +os.environ.setdefault("HF_XET_HIGH_PERFORMANCE", "1") configure_logging() diff --git a/src/axolotl/core/trainers/trl.py b/src/axolotl/core/trainers/trl.py index c5f19a6fed..bc49754bea 100644 --- a/src/axolotl/core/trainers/trl.py +++ b/src/axolotl/core/trainers/trl.py @@ -1,12 +1,10 @@ """Module for TRL RL trainers""" -from trl import ( - CPOTrainer, - KTOTrainer, - ORPOTrainer, - PRMTrainer, - RewardTrainer, -) +from trl import RewardTrainer +from trl.experimental.cpo import CPOTrainer +from trl.experimental.kto import KTOTrainer +from trl.experimental.orpo import ORPOTrainer +from trl.experimental.prm import PRMTrainer from axolotl.core.trainers.mixins import DistributedParallelMixin, RngLoaderMixin from axolotl.core.trainers.mixins.optimizer import OptimizerInitMixin, OptimizerMixin diff --git a/src/axolotl/core/training_args.py b/src/axolotl/core/training_args.py index d5be9fc626..2a155e5ef8 100644 --- a/src/axolotl/core/training_args.py +++ b/src/axolotl/core/training_args.py @@ -8,7 +8,11 @@ from typing import Optional, Type from transformers import TrainingArguments -from trl import CPOConfig, KTOConfig, ORPOConfig, PRMConfig, RewardConfig +from trl import RewardConfig +from trl.experimental.cpo import CPOConfig +from trl.experimental.kto import KTOConfig +from trl.experimental.orpo import ORPOConfig +from trl.experimental.prm import PRMConfig from axolotl.integrations.config import merge_training_args diff --git a/tests/prompt_strategies/conftest.py b/tests/prompt_strategies/conftest.py index 0af7b3e93f..7c4475ab53 100644 --- a/tests/prompt_strategies/conftest.py +++ b/tests/prompt_strategies/conftest.py @@ -141,6 +141,7 @@ def fixture_phi35_tokenizer(): @pytest.fixture(name="phi4_tokenizer", scope="session", autouse=True) +@enable_hf_offline def fixture_phi4_tokenizer(): tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-4-reasoning") return tokenizer @@ -178,6 +179,7 @@ def fixture_devstral_1_1_tokenizer(): @pytest.fixture(name="qwen3_tokenizer") +@enable_hf_offline def qwen3_tokenizer_fixture( download_qwen3_half_billion_model, ): # pylint: disable=unused-argument,redefined-outer-name From 3738978394762868b6d2f060503a6b86d910968f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 29 Jan 2026 14:25:47 -0500 Subject: [PATCH 1109/1405] Add support for batched_mm, grouped_mm and scattermoe for MoE models (#3377) * kernels plugin for moe for v5 * add support for native batched_mm or grouped_mm --- src/axolotl/integrations/kernels/__init__.py | 7 +++ src/axolotl/integrations/kernels/args.py | 35 +++++++++++ src/axolotl/integrations/kernels/plugin.py | 61 ++++++++++++++++++++ src/axolotl/loaders/model.py | 5 ++ src/axolotl/utils/schemas/config.py | 7 +++ 5 files changed, 115 insertions(+) create mode 100644 src/axolotl/integrations/kernels/__init__.py create mode 100644 src/axolotl/integrations/kernels/args.py create mode 100644 src/axolotl/integrations/kernels/plugin.py diff --git a/src/axolotl/integrations/kernels/__init__.py b/src/axolotl/integrations/kernels/__init__.py new file mode 100644 index 0000000000..d879424359 --- /dev/null +++ b/src/axolotl/integrations/kernels/__init__.py @@ -0,0 +1,7 @@ +from .args import KernelsArgs +from .plugin import KernelsPlugin + +__all__ = [ + "KernelsArgs", + "KernelsPlugin", +] diff --git a/src/axolotl/integrations/kernels/args.py b/src/axolotl/integrations/kernels/args.py new file mode 100644 index 0000000000..66d6b6d531 --- /dev/null +++ b/src/axolotl/integrations/kernels/args.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel, model_validator + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +class KernelsArgs(BaseModel): + use_scattermoe: bool | None = True + + @model_validator(mode="before") + @classmethod + def check_use_kernels(cls, data): + if data.get("use_kernels") is not True: + LOG.warning( + "`use_kernels` must be set to True to use this. Automatically setting it to True." + ) + data["use_kernels"] = True + + return data + + @model_validator(mode="before") + @classmethod + def check_experts_implementation(cls, data): + experts_implementation = data.get("experts_implementation") + if experts_implementation is None: + # transformers may default to batched_mm when unset + data["experts_implementation"] = "eager" + elif experts_implementation != "eager": + LOG.warning( + "`experts_implementation` must be set to 'eager' to use this. Automatically setting it to 'eager'." + ) + data["experts_implementation"] = "eager" + + return data diff --git a/src/axolotl/integrations/kernels/plugin.py b/src/axolotl/integrations/kernels/plugin.py new file mode 100644 index 0000000000..c7fb79ff64 --- /dev/null +++ b/src/axolotl/integrations/kernels/plugin.py @@ -0,0 +1,61 @@ +from kernels import ( + LayerRepository, + Mode, + register_kernel_mapping, + replace_kernel_forward_from_hub, +) + +from axolotl.integrations.base import BasePlugin +from axolotl.utils.callbacks.models import get_causal_lm_model_cls_prefix + + +class KernelsPlugin(BasePlugin): + def get_input_args(self): + return "axolotl.integrations.kernels.KernelsArgs" + + def pre_model_load(self, cfg): + if cfg.use_scattermoe: + self._register_kernels() + self._kernelize_model(cfg.model_config_type) + + def _register_kernels(self): + register_kernel_mapping( + { + "HFScatterMoEParallelExperts": { + "cuda": { + Mode.TRAINING: LayerRepository( + repo_id="axolotl-ai-co/scattermoe", + layer_name="HFScatterMoEGatedMLP", + ), + Mode.INFERENCE: LayerRepository( + repo_id="axolotl-ai-co/scattermoe", + layer_name="HFScatterMoEGatedMLP", + ), + }, + } + } + ) + + def _kernelize_model(self, model_type: str): + if model_type == "olmoe": + from transformers.models.olmoe.modeling_olmoe import OlmoeSparseMoeBlock + + replace_kernel_forward_from_hub( + OlmoeSparseMoeBlock, "HFScatterMoEParallelExperts" + ) + else: + try: + model_moe_cls = get_model_moe_block(model_type) + replace_kernel_forward_from_hub( + model_moe_cls, "HFScatterMoEParallelExperts" + ) + except Exception as err: + raise ValueError(f"Unsupported model type: {model_type}") from err + + +def get_model_moe_block(model_type: str): + module_path = f"transformers.models.{model_type}.modeling_{model_type}" + model_cls_prefix, _ = get_causal_lm_model_cls_prefix(model_type) + module = __import__(module_path, fromlist=[f"{model_cls_prefix}SparseMoeBlock"]) + model_cls = getattr(module, f"{model_cls_prefix}SparseMoeBlock") + return model_cls diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 0133148ebd..75684c1ae1 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -225,6 +225,7 @@ def _apply_post_model_load_setup(self): ): self.model = self.model.merge_and_unload() + self._configure_experts_implementation() self._apply_activation_checkpointing() self._resize_token_embeddings() self._adjust_model_config() @@ -232,6 +233,10 @@ def _apply_post_model_load_setup(self): self._configure_qat() log_gpu_memory_usage(LOG, "Memory usage after model load", 0) + def _configure_experts_implementation(self): + if self.cfg.experts_implementation is not None: + self.model.set_experts_implementation(self.cfg.experts_implementation) + def _apply_activation_checkpointing(self): if self.cfg.activation_offloading is True: from axolotl.core.trainers.mixins.activation_checkpointing import ( diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 3621c0d89e..6537732730 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -619,6 +619,13 @@ class AxolotlInputConfig( }, ) + experts_implementation: str | None = Field( + default=None, + json_schema_extra={ + "description": "Which experts implementation to use for MoE models," + }, + ) + scaling_softmax: bool | None = Field( default=None, json_schema_extra={ From be00978bc2ed6f623dbe6d00ee77887db524d069 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 30 Jan 2026 14:10:27 -0500 Subject: [PATCH 1110/1405] tag for v0.14.0 release (#3379) --- VERSION | 2 +- pyproject.toml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 80827deea8..a803cc227f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.14.0.dev0 +0.14.0 diff --git a/pyproject.toml b/pyproject.toml index bca758576e..e60f6f3ff7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,3 +60,6 @@ indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" docstring-code-format = false + +[tool.uv.extra-build-dependencies] +axolotl = ["huggingface_hub"] From 236dad3bb7954e5dc8dcca5b4d067a7e6e39fb7e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 30 Jan 2026 21:28:01 -0500 Subject: [PATCH 1111/1405] set 0.15.0.dev0 version (#3380) --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index a803cc227f..d818190189 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.14.0 +0.15.0.dev0 From 0343a72cc989e5b6d9277222cf99b010782e9e0e Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:13:53 +0530 Subject: [PATCH 1112/1405] add glm support + patch (#3329) [skip ci] * add glm support + patch * lint * lint * Update examples/glm4/glm-4-6v-flash-qlora.yaml Co-authored-by: NanoCode012 * Update examples/glm4/glm-4-6v-flash-qlora.yaml Co-authored-by: NanoCode012 * Update src/axolotl/processing_strategies.py Co-authored-by: NanoCode012 * patch removed * lint * lint2 * docs + rename * rmv moe * docs * removed processor * sdpa T_T" * ddp_find_unused_parameters: true * muti gpu yaml tested both * muti gpu yaml tested both * Update examples/glm46v/README.md Co-authored-by: NanoCode012 * Update examples/glm46v/README.md Co-authored-by: NanoCode012 * Update examples/glm46v/README.md Co-authored-by: NanoCode012 * rmv text only section + v5 comments * rename --------- Co-authored-by: Ved Co-authored-by: NanoCode012 --- docs/multimodal.qmd | 13 +++++ examples/glm46v/README.md | 44 +++++++++++++++ examples/glm46v/glm-4-6v-flash-ddp.yaml | 53 +++++++++++++++++ examples/glm46v/glm-4-6v-flash-qlora.yaml | 50 ++++++++++++++++ src/axolotl/processing_strategies.py | 69 +++++++++++++++++++++-- 5 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 examples/glm46v/README.md create mode 100644 examples/glm46v/glm-4-6v-flash-ddp.yaml create mode 100644 examples/glm46v/glm-4-6v-flash-qlora.yaml diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index 71d5cc7667..54793c6e34 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -19,6 +19,7 @@ format: - [Gemma-3n](#sec-gemma-3n) - [Qwen2-VL](#sec-qwen2-vl) - [Qwen2.5-VL](#sec-qwen25-vl) +- [GLM-4.6V](#sec-glm-4-6v) - [SmolVLM2](#sec-smolvlm2) - [LFM2-VL](#sec-lfm2-vl) - [Intern-VL](#sec-intern-vl) @@ -183,6 +184,18 @@ base_model: Qwen/Qwen3-VL-4B-Instruct chat_template: qwen2_vl # same as qwen2-vl ``` +### GLM-4.6V {#sec-glm-4-6v} + +Both GLM-4.6V (106B MoE) and GLM-4.6V-Flash (9B) are supported. + +```yaml +# GLM-4.6V (106B MoE version) +base_model: zai-org/GLM-4.6V + +# OR GLM-4.6V-Flash (9B version) +base_model: zai-org/GLM-4.6V-Flash +``` + ### SmolVLM2 {#sec-smolvlm2} ::: {.callout-tip} diff --git a/examples/glm46v/README.md b/examples/glm46v/README.md new file mode 100644 index 0000000000..965e08e517 --- /dev/null +++ b/examples/glm46v/README.md @@ -0,0 +1,44 @@ +# Finetune GLM-4.6V with Axolotl + +GLM-4.6V is a family of vision-language models from ZhipuAI found on [HuggingFace](https://huggingface.co/zai-org/GLM-4.6V). This guide shows how to fine-tune it with Axolotl for vision-language tasks. + + + +## Getting started + +1. Install Axolotl from source following the [installation guide](https://docs.axolotl.ai/docs/installation.html#sec-edge-build). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + + +3. Run the fine-tuning: + + glm-4-6v-flash(9B) + ```bash + axolotl train examples/glm46v/glm-4-6v-flash-qlora.yaml + ``` + +Let us know how it goes. Happy finetuning! 🚀 + +## Tips + +- Vision datasets should follow the format described in the [multimodal docs](https://docs.axolotl.ai/docs/multimodal.html#dataset-format) +- You can run a **full finetuning** by removing the `adapter: qlora` and `load_in_4bit: true` from the config. +- Read more on how to load your own dataset in the [dataset loading docs](https://docs.axolotl.ai/docs/dataset_loading.html). + +## Supported Models + +- **GLM-4.6V**: Full vision-language model (`zai-org/GLM-4.6V`) +- **GLM-4.6V-Flash**: Faster variant (`zai-org/GLM-4.6V-Flash`) + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Related Resources + +- [ZhipuAI GLM-4.6V](https://huggingface.co/zai-org/GLM-4.6V) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/glm46v/glm-4-6v-flash-ddp.yaml b/examples/glm46v/glm-4-6v-flash-ddp.yaml new file mode 100644 index 0000000000..c67ac5e283 --- /dev/null +++ b/examples/glm46v/glm-4-6v-flash-ddp.yaml @@ -0,0 +1,53 @@ +base_model: zai-org/GLM-4.6V-Flash +trust_remote_code: true + +processor_type: AutoProcessor +load_in_4bit: true + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false +ddp_find_unused_parameters: true + +output_dir: ./outputs/glm-4-6v-flash-qlora +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.05 +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +sequence_len: 2048 + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +logging_steps: 1 +sdp_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 0 +saves_per_epoch: 1 +weight_decay: 0.0 diff --git a/examples/glm46v/glm-4-6v-flash-qlora.yaml b/examples/glm46v/glm-4-6v-flash-qlora.yaml new file mode 100644 index 0000000000..287944ae89 --- /dev/null +++ b/examples/glm46v/glm-4-6v-flash-qlora.yaml @@ -0,0 +1,50 @@ +base_model: zai-org/GLM-4.6V-Flash +trust_remote_code: true + +processor_type: AutoProcessor +load_in_4bit: true + +# these 3 lines are needed for now to handle vision chat templates w images +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +output_dir: ./outputs/glm-4-6v-flash-qlora +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.05 +lora_target_modules: + - gate_proj + - down_proj + - up_proj + - q_proj + - v_proj + - k_proj + - o_proj + +sequence_len: 2048 + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +logging_steps: 1 +sdp_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 0 +saves_per_epoch: 1 +weight_decay: 0.0 diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 077db43889..c8b153e6d8 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -485,6 +485,58 @@ def process_labels(self, input_ids): return labels +class Glm4vProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for GLM4V and GLM4V-MoE vision models.""" + + def __init__( + self, + processor: ProcessorMixin, + chat_template: Optional[str] = None, + image_size: int | tuple[int, int] | None = None, + image_resize_algorithm: Resampling | None = None, + ): + super().__init__(processor, chat_template, image_size, image_resize_algorithm) + + self.tokenizer = getattr(processor, "tokenizer", processor) + + self.image_token = "<|image|>" # nosec + self.begin_image_token = "<|begin_of_image|>" # nosec + self.end_image_token = "<|end_of_image|>" # nosec + self.video_token = "<|video|>" # nosec + self.begin_video_token = "<|begin_of_video|>" # nosec + self.end_video_token = "<|end_of_video|>" # nosec + + self.image_token_id = self.tokenizer.convert_tokens_to_ids(self.image_token) + self.begin_image_token_id = self.tokenizer.convert_tokens_to_ids( + self.begin_image_token + ) + self.end_image_token_id = self.tokenizer.convert_tokens_to_ids( + self.end_image_token + ) + self.video_token_id = self.tokenizer.convert_tokens_to_ids(self.video_token) + self.begin_video_token_id = self.tokenizer.convert_tokens_to_ids( + self.begin_video_token + ) + self.end_video_token_id = self.tokenizer.convert_tokens_to_ids( + self.end_video_token + ) + + def process_labels(self, input_ids): + labels = input_ids.clone() + + labels[labels == self.tokenizer.pad_token_id] = -100 + + labels[labels == self.image_token_id] = -100 + labels[labels == self.begin_image_token_id] = -100 + labels[labels == self.end_image_token_id] = -100 + + labels[labels == self.video_token_id] = -100 + labels[labels == self.begin_video_token_id] = -100 + labels[labels == self.end_video_token_id] = -100 + + return labels + + def get_processing_strategy( processor: ProcessorMixin, chat_template, @@ -501,10 +553,10 @@ def get_processing_strategy( "image_resize_algorithm": image_resize_algorithm, } - if chat_template_type in [None, "tokenizer_default"] and hasattr( - processor.tokenizer, "chat_template" - ): - processing_kwargs["chat_template"] = processor.tokenizer.chat_template + if chat_template_type in [None, "tokenizer_default"]: + tokenizer = getattr(processor, "tokenizer", processor) + if hasattr(tokenizer, "chat_template"): + processing_kwargs["chat_template"] = tokenizer.chat_template if chat_template_type == "qwen2_vl": return Qwen2VLProcessingStrategy( @@ -533,6 +585,15 @@ def get_processing_strategy( return Mistral3ProcessingStrategy( **processing_kwargs, ) + try: + from transformers.models.glm46v.processing_glm46v import Glm46VProcessor + + if isinstance(processor, Glm46VProcessor): + return Glm4vProcessingStrategy( + **processing_kwargs, + ) + except ImportError: + pass if isinstance(processor, InternVLProcessor): return InternVLProcessingStrategy( From 530a0c0bf0759d59e87684349f11f042657397fa Mon Sep 17 00:00:00 2001 From: tgoab Date: Tue, 10 Feb 2026 05:44:17 -0500 Subject: [PATCH 1113/1405] Changes from dataset_processes to dataset_num_proc (#3352) [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * changes from dataset_processes to dataset_num_proc * deprecation message improved --------- Co-authored-by: Juliana Nieto Cárdenas --- .runpod/README.md | 2 +- .runpod/src/config/config.yaml | 7 ++----- src/axolotl/utils/datasets.py | 6 ++++++ tests/core/test_builders.py | 2 +- tests/e2e/test_streaming.py | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.runpod/README.md b/.runpod/README.md index 8042f4f91f..2cebaa5e7d 100644 --- a/.runpod/README.md +++ b/.runpod/README.md @@ -123,7 +123,7 @@ datasets: | --------------------------------- | -------------------------- | ----------------------------------- | | `dataset_prepared_path` | `"data/last_run_prepared"` | Path for prepared dataset | | `push_dataset_to_hub` | `""` | Push dataset to HF hub | -| `dataset_processes` | `4` | Number of preprocessing processes | +| `dataset_num_proc` | `4` | Number of preprocessing processes | | `dataset_keep_in_memory` | `false` | Keep dataset in memory | | `shuffle_merged_datasets` | `true` | Shuffle merged datasets | | `shuffle_before_merging_datasets` | `false` | Shuffle each dataset before merging | diff --git a/.runpod/src/config/config.yaml b/.runpod/src/config/config.yaml index fde3730b27..b43c83dfec 100644 --- a/.runpod/src/config/config.yaml +++ b/.runpod/src/config/config.yaml @@ -39,7 +39,6 @@ # type: # linear | dynamic # factor: # float - # # Whether you are training a 4-bit GPTQ quantized model # gptq: true # gptq_groupsize: 128 # group size @@ -107,7 +106,7 @@ # push_dataset_to_hub: # repo path # # The maximum number of processes to use while preprocessing your input dataset. This defaults to `os.cpu_count()` # # if not set. -# dataset_processes: # defaults to os.cpu_count() if not set +# dataset_num_proc: # defaults to os.cpu_count() if not set # # push checkpoints to hub # hub_model_id: # repo path to push finetuned model # # how to push checkpoints to hub @@ -349,8 +348,6 @@ # # Allow overwrite yml config using from cli # strict: - - base_model: ${BASE_MODEL} base_model_ignore_patterns: ${BASE_MODEL_IGNORE_PATTERNS} base_model_config: ${BASE_MODEL_CONFIG} @@ -409,7 +406,7 @@ chat_template_jinja: ${CHAT_TEMPLATE_JINJA} default_system_message: ${DEFAULT_SYSTEM_MESSAGE} dataset_prepared_path: ${DATASET_PREPARED_PATH} push_dataset_to_hub: ${PUSH_DATASET_TO_HUB} -dataset_processes: ${DATASET_PROCESSES} +dataset_num_proc: ${DATASET_NUM_PROC} dataset_keep_in_memory: ${DATASET_KEEP_IN_MEMORY} hub_model_id: ${HUB_MODEL_ID} hub_strategy: ${HUB_STRATEGY} diff --git a/src/axolotl/utils/datasets.py b/src/axolotl/utils/datasets.py index 9b8a8e25ae..19ad716409 100644 --- a/src/axolotl/utils/datasets.py +++ b/src/axolotl/utils/datasets.py @@ -1,12 +1,18 @@ """helper functions for datasets""" import os +from axolotl.utils.logging import get_logger +LOG = get_logger(__name__) def get_default_process_count(): if axolotl_dataset_num_proc := os.environ.get("AXOLOTL_DATASET_NUM_PROC"): return int(axolotl_dataset_num_proc) if axolotl_dataset_processes := os.environ.get("AXOLOTL_DATASET_PROCESSES"): + LOG.warning( + "AXOLOTL_DATASET_PROCESSES and `dataset_processes` are deprecated and will be " + "removed in a future version. Please use `dataset_num_proc` instead." + ) return int(axolotl_dataset_processes) if runpod_cpu_count := os.environ.get("RUNPOD_CPU_COUNT"): return int(runpod_cpu_count) diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index 5f14811018..194950e150 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -79,7 +79,7 @@ def fixture_base_cfg(): "ddp_timeout": 1800, "ddp_bucket_cap_mb": 25, "ddp_broadcast_buffers": False, - "dataset_processes": 4, + "dataset_num_proc": 4, } ) diff --git a/tests/e2e/test_streaming.py b/tests/e2e/test_streaming.py index 125eb43eb0..404fb53dac 100644 --- a/tests/e2e/test_streaming.py +++ b/tests/e2e/test_streaming.py @@ -30,7 +30,7 @@ def test_streaming_dataset(self, temp_dir, sample_packing): "sample_packing": sample_packing, "pretrain_multipack_attn": sample_packing, "streaming_multipack_buffer_size": 10000, - "dataset_processes": 1, + "dataset_num_proc": 1, "special_tokens": { "pad_token": "<|endoftext|>", }, From 86a5803212f8768782d886fe767d97433e298c76 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:14:55 +0530 Subject: [PATCH 1114/1405] train_per_sec_per_gpu metric (#3364) [skip ci] * fix token count * guard for none n zero --- .../utils/callbacks/tokens_per_second.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/axolotl/utils/callbacks/tokens_per_second.py b/src/axolotl/utils/callbacks/tokens_per_second.py index 679b1c8640..e3a3ce3338 100644 --- a/src/axolotl/utils/callbacks/tokens_per_second.py +++ b/src/axolotl/utils/callbacks/tokens_per_second.py @@ -78,12 +78,19 @@ def on_step_end( **kwargs, ): # pylint: disable=unused-argument tokens = getattr(state, "tokens", None) - if tokens and "trainable_tokens" in tokens: - step_time = time.perf_counter() - self.start_time - num_tokens_per_device = tokens["trainable_tokens"].clone() - # non data parallel groups have duplicated tokens, so we avoid double-counting - num_tokens_per_device = num_tokens_per_device / self.non_data_parallel_size - state.last_tokens_per_second = num_tokens_per_device / step_time + if not (tokens and "trainable_tokens" in tokens): + return + step_time = time.perf_counter() - self.start_time + if step_time <= 0: + return + + num_tokens = tokens["trainable_tokens"].clone() / self.non_data_parallel_size + if torch.distributed.is_initialized(): + dp_size = max( + 1, torch.distributed.get_world_size() // self.non_data_parallel_size + ) + num_tokens = num_tokens / dp_size + state.last_tokens_per_second = num_tokens / step_time def on_log( self, From 97a4f28511afd94e60d733a48a8dd193cea59868 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:17:26 +0530 Subject: [PATCH 1115/1405] fix: saving state dict and eval for Context Parallel (#3382) [skip ci] * clone state_dict if none * patch calculating eval loss for cp --- src/axolotl/core/trainers/base.py | 7 +++ .../utils/ctx_managers/sequence_parallel.py | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index a45e246a12..77e7b573b3 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -719,6 +719,13 @@ def _save(self, output_dir: Optional[str] = None, state_dict=None): output_dir = output_dir if output_dir is not None else self.args.output_dir os.makedirs(output_dir, exist_ok=True) LOG.info(f"Saving model checkpoint to {output_dir}") + if state_dict is None: + state_dict = self.accelerator.get_state_dict(self.model) + if state_dict is not None: + state_dict = { + k: v.clone() if isinstance(v, torch.Tensor) else v + for k, v in state_dict.items() + } supported_classes = ( (PreTrainedModel,) if not is_peft_available() diff --git a/src/axolotl/utils/ctx_managers/sequence_parallel.py b/src/axolotl/utils/ctx_managers/sequence_parallel.py index 78b3d1cae5..7f6af7d480 100644 --- a/src/axolotl/utils/ctx_managers/sequence_parallel.py +++ b/src/axolotl/utils/ctx_managers/sequence_parallel.py @@ -218,6 +218,9 @@ def __init__( self.original_seq_len = 0 self.pad_len = 0 + # Track local valid token count for eval loss correction across CP ranks + self._local_valid_tokens: torch.Tensor | None = None + # Create a partially applied version of the apply_sequence_parallelism function self.apply_sequence_parallelism = functools.partial( apply_sequence_parallelism, @@ -270,6 +273,18 @@ def sequence_parallel_pre_hook(_, args, kwargs): self.apply_sequence_parallelism(updated_kwargs) ) + # Track local valid tokens for eval loss correction + if "labels" in updated_kwargs and not self.models[0].training: + self._local_valid_tokens = ( + (updated_kwargs["labels"] != -100).sum().float() + ) + # Strip num_items_in_batch during eval so the model uses + # reduction='mean', allowing the post-hook weighted all-reduce + # formula (loss * local_valid) to correctly recover the loss sum + updated_kwargs.pop("num_items_in_batch", None) + else: + self._local_valid_tokens = None + return remaining_args, updated_kwargs # Forward post-hook to gather outputs @@ -287,6 +302,44 @@ def sequence_parallel_post_hook(_, __, output: ModelOutput) -> ModelOutput: return output + # Post-hook to correct eval loss via weighted all-reduce across CP ranks + def eval_loss_correction_post_hook(_, __, output: ModelOutput) -> ModelOutput: + if self._local_valid_tokens is None: + return output + if not hasattr(output, "loss") or output.loss is None: + return output + + local_valid = self._local_valid_tokens.to(output.loss.device) + loss = output.loss.detach().clone() + + # Handle rank with zero valid tokens (loss is NaN) + if local_valid.item() == 0: + weighted_loss = torch.zeros(1, device=loss.device, dtype=loss.dtype) + else: + weighted_loss = loss * local_valid + + total_valid = local_valid.clone() + dist.all_reduce( + weighted_loss, + op=dist.ReduceOp.SUM, + group=self.process_group, + ) + dist.all_reduce( + total_valid, + op=dist.ReduceOp.SUM, + group=self.process_group, + ) + + if total_valid.item() > 0: + output["loss"] = (weighted_loss / total_valid).squeeze() + else: + output["loss"] = torch.tensor( + float("nan"), device=loss.device, dtype=loss.dtype + ) + + self._local_valid_tokens = None + return output + # Register hooks for model in self.models: self.hook_handles.append( @@ -298,6 +351,10 @@ def sequence_parallel_post_hook(_, __, output: ModelOutput) -> ModelOutput: self.hook_handles.append( model.register_forward_hook(sequence_parallel_post_hook) ) + # Always register eval loss correction hook + self.hook_handles.append( + model.register_forward_hook(eval_loss_correction_post_hook) + ) def _gather_outputs(self, output: CausalLMOutputWithPast) -> CausalLMOutputWithPast: """Gather sharded outputs from all ranks and reconstruct the full tensor.""" From fcc4cfdb631fb54dc5107789bcbab93440fe0dc3 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 10 Feb 2026 17:49:21 +0700 Subject: [PATCH 1116/1405] feat: add sageattention (#2823) [skip ci] * feat: add sageattention * feat: call path on pre model load * fix: patch to use register to correct var * fix: add strict check import at start * chore: fix comments * chore: refactor * feat: add capability check * fix: missed underscore * fix: let sageattention use FA backend in transformers * feat: update sage attention for attention mask and position ids * feat: allow sample packing but add warning without packing * fix: loss hitting 0 with packing and attention mask note * feat: downcast embeds if sage attention too * feat: add config validation * feat: add attention docs * chore: docs --- _quarto.yml | 1 + docs/attention.qmd | 140 ++++++++++++ src/axolotl/loaders/model.py | 11 +- src/axolotl/loaders/patch_manager.py | 8 + .../monkeypatch/attention/sage_attn.py | 211 ++++++++++++++++++ src/axolotl/utils/schemas/config.py | 42 ++++ src/axolotl/utils/schemas/validation.py | 6 +- 7 files changed, 416 insertions(+), 3 deletions(-) create mode 100644 docs/attention.qmd create mode 100644 src/axolotl/monkeypatch/attention/sage_attn.py diff --git a/_quarto.yml b/_quarto.yml index fe3a76e534..7de2be6a78 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -320,6 +320,7 @@ website: - docs/multipack.qmd - docs/mixed_precision.qmd - docs/optimizers.qmd + - docs/attention.qmd - section: "Advanced Features" contents: diff --git a/docs/attention.qmd b/docs/attention.qmd new file mode 100644 index 0000000000..21004277e2 --- /dev/null +++ b/docs/attention.qmd @@ -0,0 +1,140 @@ +--- +title: Attention +description: Supported attention modules in Axolotl +--- + +## SDP Attention + +This is the default built-in attention in PyTorch. + +```yaml +sdp_attention: true +``` + +For more details: [PyTorch docs](https://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html) + +## Flash Attention 2 + +Uses efficient kernels to compute attention. + +```yaml +flash_attention: true +``` + +For more details: [Flash Attention](https://github.com/Dao-AILab/flash-attention/) + +### Nvidia + +Requirements: Ampere, Ada, or Hopper GPUs + +Note: For Turing GPUs or lower, please use other attention methods. + +```bash +pip install flash-attn --no-build-isolation +``` + +::: {.callout-tip} + +If you get `undefined symbol` while training, ensure you installed PyTorch prior to Axolotl. Alternatively, try reinstall or downgrade a version. + +::: + +#### Flash Attention 3 + +Requirements: Hopper only and CUDA 12.8 (recommended) + +```bash +git clone https://github.com/Dao-AILab/flash-attention.git +cd flash-attention/hopper + +python setup.py install +``` + +### AMD + +Requirements: ROCm 6.0 and above. + +See [Flash Attention AMD docs](https://github.com/Dao-AILab/flash-attention/tree/main?tab=readme-ov-file#amd-rocm-support). + +## Flex Attention + +A flexible PyTorch API for attention used in combination with `torch.compile`. + +```yaml +flex_attention: true + +# recommended +torch_compile: true +``` + +::: {.callout-note} + +We recommend using latest stable version of PyTorch for best performance. + +::: + +For more details: [PyTorch docs](https://pytorch.org/blog/flexattention/) + +## SageAttention + +Attention kernels with QK Int8 and PV FP16 accumulator. + +```yaml +sage_attention: true +``` + +Requirements: Ampere, Ada, or Hopper GPUs + +```bash +pip install sageattention==2.2.0 --no-build-isolation +``` + +::: {.callout-warning} + +Only LoRA/QLoRA recommended at the moment. We found loss drop to 0 for full finetuning. See [GitHub Issue](https://github.com/thu-ml/SageAttention/issues/198). + +::: + +For more details: [Sage Attention](https://github.com/thu-ml/SageAttention) + +::: {.callout-note} + +We do not support SageAttention 3 at the moment. If you are interested on adding this or improving SageAttention implementation, please make an Issue. + +::: + + +## xFormers + +```yaml +xformers_attention: true +``` + +::: {.callout-tip} + +We recommend using with Turing GPUs or below (such as on Colab). + +::: + +For more details: [xFormers](https://github.com/facebookresearch/xformers) + +## Shifted Sparse Attention + +::: {.callout-warning} + +We plan to deprecate this! If you use this feature, we recommend switching to methods above. + +::: + +Requirements: LLaMA model architecture + +```yaml +flash_attention: true +s2_attention: true +``` + +::: {.callout-tip} + +No sample packing support! + +::: diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 75684c1ae1..6c88855268 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -338,7 +338,12 @@ def _configure_embedding_dtypes(self): # LlamaRMSNorm layers are in fp32 after kbit_training or full finetune, so # we need to convert them back to fp16/bf16 for flash-attn compatibility. ( - (needs_fa2_dtype or self.cfg.flash_attention or self.cfg.flex_attention) + ( + needs_fa2_dtype + or self.cfg.flash_attention + or self.cfg.flex_attention + or self.cfg.sage_attention + ) and not self.is_qlora_and_fsdp_enabled ) or ( @@ -612,6 +617,10 @@ def _set_attention_config(self): elif self.cfg.sdp_attention: self.model_kwargs["attn_implementation"] = "sdpa" self.model_config._attn_implementation = "sdpa" + elif self.cfg.sage_attention: + # sets FA2 attention to re-use same internal handling like masking + self.model_kwargs["attn_implementation"] = "flash_attention_2" + self.model_config._attn_implementation = "flash_attention_2" elif self.cfg.eager_attention: self.model_kwargs["attn_implementation"] = "eager" self.model_config._attn_implementation = "eager" diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 30c3ba0fd9..3cf8bbd20d 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -96,6 +96,7 @@ def apply_pre_model_load_patches(self): # self._apply_flex_attention_patches() self._apply_flash_attention_patches() self._apply_chunked_cross_entropy_patch() + self._apply_sageattn_patches() self._apply_fsdp_patches() self._apply_adapter_patches() self._apply_model_specific_patches() @@ -201,6 +202,13 @@ def _apply_flex_attention_patches(self): flex_attn_compile_kwargs = self.cfg.flex_attn_compile_kwargs or {} patch_flex_wrapper(**flex_attn_compile_kwargs) + def _apply_sageattn_patches(self): + """Apply patches for SageAttention.""" + if self.cfg.sage_attention: + from axolotl.monkeypatch.attention.sage_attn import patch_sageattn + + patch_sageattn() + def _apply_model_specific_patches(self): """Apply patches specific to model architectures.""" if ( diff --git a/src/axolotl/monkeypatch/attention/sage_attn.py b/src/axolotl/monkeypatch/attention/sage_attn.py new file mode 100644 index 0000000000..cc9fdb94de --- /dev/null +++ b/src/axolotl/monkeypatch/attention/sage_attn.py @@ -0,0 +1,211 @@ +""" +Monkeypatch for SageAttention for use with transformers. + +https://github.com/thu-ml/SageAttention/ +""" + +import torch +from transformers.integrations.sdpa_attention import repeat_kv + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +sageattn = None # pylint: disable=invalid-name +sageattn_varlen = None # pylint: disable=invalid-name + + +def _is_sageattn_available(): + """Determine if SageAttention is available""" + try: + import sageattention # noqa: F401 # pylint: disable=unused-import + + return True + except ImportError: + return False + + +if _is_sageattn_available(): + # import sageattn here if available + from sageattention import sageattn, sageattn_varlen + + +def _check_sageattn_imported(): + """Check if SageAttention is imported. Raises an ImportError if not.""" + if sageattn is None: + raise ImportError( + "SageAttention is not installed. Please install it from source: " + "`pip install git+https://github.com/thu-ml/SageAttention.git@1718ddc06dbc694bcf3c6b49ac28c1921aa2d8bd`" + ) + + +def sage_attention_forward( + module: torch.nn.Module, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + attention_mask: torch.Tensor | None = None, + dropout: float = 0.0, + scaling: float | None = None, + is_causal: bool | None = None, + **kwargs, +) -> tuple[torch.Tensor, None]: + """ + Forward pass for SageAttention compatible with transformers attention interfaces. + + https://github.com/thu-ml/SageAttention/ + """ + + _check_sageattn_imported() + + if kwargs.get("output_attentions", False) or kwargs.get("head_mask") is not None: + raise NotImplementedError( + "SageAttention does not support `output_attentions=True` or `head_mask`." + ) + + # The base sageattn API does not support dropout. + if dropout > 0.0: + raise NotImplementedError("SageAttention does not support dropout.") + + # Handle Grouped-Query Attention (GQA) and Multi-Query Attention (MQA) + if hasattr(module, "num_key_value_groups"): + key = repeat_kv(key, module.num_key_value_groups) + value = repeat_kv(value, module.num_key_value_groups) + + # Calculate is_causal following transformers + assert is_causal is not False, "is_causal must be True or None" + is_causal = True + + position_ids = kwargs.get("position_ids", None) + query_length = query.shape[2] + + cu_seqlens_q = kwargs.get("cu_seqlens_q", None) + cu_seqlens_k = kwargs.get("cu_seqlens_k", None) + max_length_q = kwargs.get("max_length_q", None) + max_length_k = kwargs.get("max_length_k", None) + + # Sample packing uses position_ids, so we check for it first + if position_ids is not None and ( + max_length_q is not None + or (query_length != 1 and not (torch.diff(position_ids, dim=-1) >= 0).all()) + ): + # transpose inputs to NHD layout for use with FA2 utils + query = query.transpose(1, 2) + key = key.transpose(1, 2) + value = value.transpose(1, 2) + + batch_size = query.size(0) + + from transformers.modeling_flash_attention_utils import ( + prepare_fa2_from_position_ids, + ) + + if cu_seqlens_q is None or cu_seqlens_k is None: + query, key, value, indices_q, cu_seq_lens, max_seq_lens = ( + prepare_fa2_from_position_ids(query, key, value, position_ids) + ) + + cu_seqlens_q, cu_seqlens_k = cu_seq_lens + max_length_q, max_length_k = max_seq_lens + + else: + query = query.reshape(-1, query.size(-2), query.size(-1)) + key = key.reshape(-1, key.size(-2), key.size(-1)) + value = value.reshape(-1, value.size(-2), value.size(-1)) + + attn_output_unpad = sageattn_varlen( + q=query, + k=key, + v=value, + cu_seqlens_q=cu_seqlens_q, + cu_seqlens_k=cu_seqlens_k, + max_seqlen_q=max_length_q, + max_seqlen_k=max_length_k, + is_causal=is_causal, + sm_scale=scaling, + smooth_k=False, # reduces loss 0 / nan grad norms + tensor_layout="NHD", + ) + + attn_output = attn_output_unpad.view( + batch_size, -1, attn_output_unpad.size(-2), attn_output_unpad.size(-1) + ) + + elif attention_mask is not None: + # NOTE: When used without `pad_to_sequence_len`, the loss becomes unstable after a few steps. + + assert attention_mask.ndim == 2, "Attention mask must be 2D" + + from transformers.modeling_flash_attention_utils import ( + _upad_input, + ) + + # transpose inputs to NHD layout for use with FA2 utils + query = query.transpose(1, 2) + key = key.transpose(1, 2) + value = value.transpose(1, 2) + + batch_size = query.shape[0] + + query, key, value, indices_q, cu_seq_lens, max_seq_lens = _upad_input( + query, key, value, attention_mask, query_length + ) + cu_seqlens_q, cu_seqlens_k = cu_seq_lens + max_seqlen_q, max_seqlen_k = max_seq_lens + + attn_output_unpad = sageattn_varlen( + q=query, + k=key, + v=value, + cu_seqlens_q=cu_seqlens_q, + cu_seqlens_k=cu_seqlens_k, + max_seqlen_q=max_seqlen_q, + max_seqlen_k=max_seqlen_k, + is_causal=is_causal, + sm_scale=scaling, + tensor_layout="NHD", + ) + + from flash_attn.bert_padding import pad_input + + attn_output = pad_input(attn_output_unpad, indices_q, batch_size, query_length) + else: + # Use standard sageattn + # The input layout for transformers models is (batch_size, num_heads, seq_len, head_dim), + # which corresponds to SageAttention's "HND" layout. + attn_output = sageattn( + q=query, + k=key, + v=value, + tensor_layout="HND", + is_causal=is_causal, + sm_scale=scaling, + ) + + # SageAttention with "HND" returns (batch, heads, seq_len, head_dim) + # Transformers expects (batch, seq_len, heads, head_dim) for the output + # So we need to transpose dimensions 1 and 2 + attn_output = attn_output.transpose(1, 2).contiguous() + + return attn_output, None + + +def patch_sageattn(): + """Patch SageAttention for use with transformers.""" + + _check_sageattn_imported() + + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + # Replace flash attention with sage attention + ALL_ATTENTION_FUNCTIONS.register("flash_attention_2", sage_attention_forward) + + # Note: New method after transformers refactor to use ALL_MASK_ATTENTION_FUNCTIONS + # Register sage_attention with the global attention interface + # ALL_ATTENTION_FUNCTIONS.register("sage_attention", sage_attention_forward) + + # from transformers.masking_utils import ALL_MASK_ATTENTION_FUNCTIONS, flash_attention_mask + + # ALL_MASK_ATTENTION_FUNCTIONS.register("sage_attention", flash_attention_mask) + + LOG.info("SageAttention patched successfully") diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 6537732730..d858fdbce1 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -609,6 +609,12 @@ class AxolotlInputConfig( default=None, json_schema_extra={"description": "Whether to use bettertransformers"}, ) + sage_attention: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use SageAttention https://github.com/thu-ml/SageAttention" + }, + ) eager_attention: bool | None = None @@ -1120,6 +1126,27 @@ def warn_peft_trainable_token_to_fix_untrained(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_sageattn_wo_sample_packing(cls, data): + if (not data.get("sample_packing", False)) and data.get("sage_attention"): + if not data.get("pad_to_sequence_len", False): + LOG.warning( + "We recommend turning on `pad_to_sequence_len` for SageAttention without packing." + "This is because there has been signs that the loss explodes after a few steps." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_sageattn_fft(cls, data): + if (not data.get("adapter", False)) and data.get("sage_attention"): + LOG.warning( + "We found loss to drop to 0 with SageAttention full finetuning." + "Please observe the loss, otherwise switch to LoRA/QLoRA or another attention method." + ) + return data + class AxolotlConfigWCapabilities(AxolotlInputConfig): """Wrapper to valdiate GPU capabilities with the configured options""" @@ -1176,6 +1203,21 @@ def check_sample_packing_w_sdpa_bf16(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_compute_capability_w_sageattn(cls, data): + if ( + data.get("sage_attention") + and data.get("capabilities") + and data.get("capabilities").get("compute_capability") + not in ["sm_80", "sm_86", "sm_89", "sm_90", "sm_120"] + ): + raise ValueError( + "SageAttention supports compute capability between sm_80 and sm_120. " + "Please use a different attention implementation." + ) + return data + @model_validator(mode="before") @classmethod def check_multigpu_unsloth(cls, data): diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 9f225b75e2..bde367e0ee 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -166,9 +166,10 @@ def check_attention_fields(cls, data): fields = ( "xformers_attention", "sdp_attention", - "s2_attention", + # "s2_attention", # requires both FA and this to be enabled "flash_attention", "flex_attention", + "sage_attention", ) non_empty_count = sum(1 for field in fields if data.get(field)) @@ -185,9 +186,10 @@ def check_sample_packing_without_attention(cls, data): and not data.get("sdp_attention") and not data.get("flex_attention") and not data.get("xformers_attention") + and not data.get("sage_attention") ): LOG.warning( - "sample_packing without flash, sdp, xformers or flex attention does not handle cross sample decontamination." + "sample_packing without flash, sdp, xformers, sage, or flex attention does not handle cross sample decontamination." ) return data From b6d3653f74ef24b08f63b72aec742f95562f65d7 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 10 Feb 2026 17:51:43 +0700 Subject: [PATCH 1117/1405] feat: add step3p5 for cce (#3384) [skip ci] * feat: add step3p5 for cce * chore: reorder model --- examples/colab-notebooks/colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/integrations/cut_cross_entropy/README.md | 11 ++++++----- .../integrations/cut_cross_entropy/__init__.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index fe30068d73..da3a681cf9 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f4b5712\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0d4ce4b\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index e526352dd7..02e57f926d 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f4b5712"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0d4ce4b"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 67f1157c34..728a097420 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f4b5712" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0d4ce4b" ``` ## Usage @@ -54,8 +54,8 @@ plugins: - gpt_oss - granite - granitemoe -- granitemoeshared - granitemoehybrid +- granitemoeshared - hunyuan_v1_dense - hunyuan_v1_moe - internvl @@ -80,16 +80,17 @@ plugins: - phi3 - phi4_multimodal - qwen2 -- qwen2_vl - qwen2_moe +- qwen2_vl - qwen2_5_vl - qwen3 - qwen3_moe +- qwen3_next - qwen3_vl - qwen3_vl_moe -- qwen3_next -- smollm3 - seed_oss +- smollm3 +- step3p5 - voxtral ## Citation diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 1dbbf6759d..08d1680256 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@f4b5712"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0d4ce4b"`' ) From ed7105dba74e6035cdf53a9cd8a499247f314a91 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 10 Feb 2026 17:52:09 +0700 Subject: [PATCH 1118/1405] fix: GRPO config not accept max_prompt_length (#3390) [skip ci] --- src/axolotl/core/trainers/grpo/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index e611b96ea3..8b295e5373 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -164,7 +164,12 @@ def get_collator(cls, *args, **kwargs): @classmethod def get_blocklist_args_kwargs(cls) -> list[str]: - return ["dataset_num_proc", "max_length", "include_tokens_per_second"] + return [ + "dataset_num_proc", + "max_length", + "include_tokens_per_second", + "max_prompt_length", + ] @classmethod def get_reward_func(cls, reward_func_fqn: str) -> RewardFunc: From 37e9da7a534f8799725a9cfe6a410e91bbd7187a Mon Sep 17 00:00:00 2001 From: madScientist10 <42779409+madScientist10@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:53:09 +0200 Subject: [PATCH 1119/1405] add hub_revision support for specifying branch when pushing checkpoints (#3387) [skip ci] --- src/axolotl/core/builders/base.py | 3 +++ src/axolotl/utils/schemas/model.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index f3a9654352..c23433866b 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -409,6 +409,9 @@ def _configure_hub_parameters(self, training_args_kwargs: dict): if self.cfg.hub_strategy: training_args_kwargs["hub_strategy"] = self.cfg.hub_strategy + if self.cfg.hub_revision: + training_args_kwargs["hub_revision"] = self.cfg.hub_revision + def _configure_save_and_eval_strategy(self, training_args_kwargs: dict): # save_strategy and save_steps if self.cfg.save_steps: diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index 31de7b45ed..02b971c1d7 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -120,6 +120,12 @@ class ModelOutputConfig(BaseModel): default=None, json_schema_extra={"description": "how to push checkpoints to hub"}, ) + hub_revision: str | None = Field( + default=None, + json_schema_extra={ + "description": "branch/revision to push to on hub (default: main)" + }, + ) save_safetensors: bool | None = Field( default=True, json_schema_extra={ From a2da85257684261c26a545726c6b2cff0b7352e0 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 10 Feb 2026 17:58:40 +0700 Subject: [PATCH 1120/1405] fix: improve lora kernels failure message and handle trust_remote_code (#3378) [skip ci] * fix: improve lora kernels failure message and handle trust_remote_code * chore: re-order model guides --- _quarto.yml | 2 +- docs/lora_optims.qmd | 4 ++ src/axolotl/monkeypatch/lora_kernels.py | 3 +- src/axolotl/utils/schemas/config.py | 4 ++ src/axolotl/utils/schemas/validation.py | 15 +++++ .../utils/lora/test_config_validation_lora.py | 59 +++++++++++++++++++ 6 files changed, 85 insertions(+), 2 deletions(-) diff --git a/_quarto.yml b/_quarto.yml index 7de2be6a78..4534c0a0e5 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -251,7 +251,6 @@ website: - docs/models/olmo3.qmd - docs/models/trinity.qmd - docs/models/arcee.qmd - - docs/models/mistral.qmd - section: "Ministral3" contents: - docs/models/ministral3.qmd @@ -266,6 +265,7 @@ website: - docs/models/mistral-small.qmd - docs/models/voxtral.qmd - docs/models/devstral.qmd + - docs/models/mistral.qmd - docs/models/llama-4.qmd - docs/models/llama-2.qmd - docs/models/qwen3-next.qmd diff --git a/docs/lora_optims.qmd b/docs/lora_optims.qmd index 40893387b1..7006b5d19b 100644 --- a/docs/lora_optims.qmd +++ b/docs/lora_optims.qmd @@ -89,6 +89,10 @@ lora_o_kernel: true Currently, LoRA kernels are not supported for RLHF training, only SFT. ::: +::: {.callout-warning} +LoRA kernels do not support remote modeling code. +::: + ## Requirements - One or more NVIDIA or AMD GPUs (in order to use the Triton kernels) diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 8e335fe4c7..2972c62851 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -169,7 +169,8 @@ def get_attention_cls_from_config(cfg: DictDefault) -> Type[nn.Module]: return attention_cls except (ImportError, AttributeError) as e: raise ValueError( - f"Could not import attention class for model_type: {model_type}. " + f"Axolotl could not import attention class for model_type: {model_type}. " + "Please raise an Issue and turn off lora kernels to continue training. " f"Error: {str(e)}" ) from e diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index d858fdbce1..91bc221aca 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1271,6 +1271,10 @@ def check_auto_enable_lora_kernels(cls, data): ): return data + # Skip if trust_remote_code is enabled, as lora kernels are not compatible + if data.get("trust_remote_code"): + return data + # Skip if dropout is not 0, as auto enabling it would just disable it during runtime patch checks if data.get("lora_dropout") != 0: return data diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index bde367e0ee..7830174053 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -690,6 +690,21 @@ def check_lora_kernels_rl(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_lora_kernels_trust_remote_code(cls, data): + if ( + data.get("lora_mlp_kernel") + or data.get("lora_qkv_kernel") + or data.get("lora_o_kernel") + ) and data.get("trust_remote_code"): + raise ValueError( + "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not " + "compatible with trust_remote_code. Please disable trust_remote_code " + "or explicitly set lora_*_kernel to false." + ) + return data + class RLValidationMixin: """Validation methods related to RL training configuration.""" diff --git a/tests/utils/lora/test_config_validation_lora.py b/tests/utils/lora/test_config_validation_lora.py index a22e2a5b7b..9d97288b63 100644 --- a/tests/utils/lora/test_config_validation_lora.py +++ b/tests/utils/lora/test_config_validation_lora.py @@ -90,3 +90,62 @@ def test_qlora_4bit_validation(self): } ) validate_config(invalid_config) + + @pytest.mark.parametrize( + "kernel_field", ["lora_mlp_kernel", "lora_qkv_kernel", "lora_o_kernel"] + ) + def test_lora_kernels_trust_remote_code_incompatible(self, kernel_field): + """Test that lora kernels are incompatible with trust_remote_code""" + with pytest.raises(ValueError, match="not compatible with trust_remote_code"): + invalid_config = DictDefault( + { + "adapter": "lora", + kernel_field: True, + "trust_remote_code": True, + "datasets": [{"path": "dummy_dataset", "type": "alpaca"}], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 1e-5, + "base_model": "dummy_model", + } + ) + validate_config(invalid_config) + + def test_lora_kernels_trust_remote_code_false(self): + """Test that lora kernels work when trust_remote_code is false""" + # Test with trust_remote_code=False, lora kernels should be allowed + valid_config = DictDefault( + { + "adapter": "lora", + "lora_mlp_kernel": True, + "lora_qkv_kernel": True, + "lora_o_kernel": True, + "trust_remote_code": False, + "datasets": [{"path": "dummy_dataset", "type": "alpaca"}], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 1e-5, + "base_model": "dummy_model", + } + ) + result = validate_config(valid_config) + assert result["lora_mlp_kernel"] is True + assert result["lora_qkv_kernel"] is True + assert result["lora_o_kernel"] is True + + # Test with trust_remote_code=None (unset), kernels should be allowed + valid_config = DictDefault( + { + "adapter": "lora", + "lora_qkv_kernel": True, + "trust_remote_code": None, + "datasets": [{"path": "dummy_dataset", "type": "alpaca"}], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 1e-5, + "base_model": "dummy_model", + } + ) + result = validate_config(valid_config) + assert result["lora_qkv_kernel"] is True + assert result["trust_remote_code"] is None From c67cbcb0f5581005799d3d82ba0f506494776212 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 10 Feb 2026 18:03:26 +0700 Subject: [PATCH 1121/1405] fix: ignore add_special_tokens and use test mode for generation for mistral tokenizer (#3396) [skip ci] * fix: ignore add_special_tokens and use test mode for generation * fix: incorrectly setting kwarg --- src/axolotl/utils/mistral/mistral_tokenizer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/mistral/mistral_tokenizer.py b/src/axolotl/utils/mistral/mistral_tokenizer.py index a5ffe9a283..45a0308f99 100644 --- a/src/axolotl/utils/mistral/mistral_tokenizer.py +++ b/src/axolotl/utils/mistral/mistral_tokenizer.py @@ -86,15 +86,15 @@ def apply_chat_template( # type: ignore add_generation_prompt: bool = False, **kwargs, ) -> str | list[int]: - """Patched fn to handle setting serving mode, continue_final_message, remove chat_template and add_generation_prompt kwarg""" + """Patched fn to handle setting test mode, remove chat_template and add_generation_prompt kwarg""" # pop unnecessary kwarg for mistral kwargs.pop("real_last_index", None) + kwargs.pop("add_special_tokens", None) try: if add_generation_prompt: - self._set_mode(ValidationMode.serving) - kwargs["continue_final_message"] = True + self._set_mode(ValidationMode.test) out = super().apply_chat_template(conversation, **kwargs) From a4ee56c3156f334c6eb3ff3a00b7f3f4efe5888f Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:36:15 +0530 Subject: [PATCH 1122/1405] fix: set rollout in GRPO training_kwargs (#3392) --- src/axolotl/core/trainers/grpo/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 8b295e5373..0d2615aecf 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -126,9 +126,6 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: if trl.use_liger_loss is not None: grpo_args_kwargs["use_liger_loss"] = trl.use_liger_loss - if trl.rollout_func: - grpo_args_kwargs["rollout_func"] = cls.get_rollout_func(trl.rollout_func) - if trl.multi_objective_aggregation is not None: grpo_args_kwargs["multi_objective_aggregation"] = ( trl.multi_objective_aggregation @@ -154,6 +151,8 @@ def set_trainer_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: trainer_kwargs["reward_processing_classes"] = ( cfg.trl.reward_processing_classes ) + if cfg.trl and cfg.trl.rollout_func: + trainer_kwargs["rollout_func"] = cls.get_rollout_func(cfg.trl.rollout_func) return trainer_kwargs From 4e22cf06511ad4e35c36b7ad8d543ead0506fbae Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 10 Feb 2026 23:01:16 +0700 Subject: [PATCH 1123/1405] fix: remove telemetry warning (#3397) [skip ci] --- src/axolotl/telemetry/manager.py | 21 --------------------- tests/telemetry/test_manager.py | 14 -------------- 2 files changed, 35 deletions(-) diff --git a/src/axolotl/telemetry/manager.py b/src/axolotl/telemetry/manager.py index 82d310cdc9..46ef389aac 100644 --- a/src/axolotl/telemetry/manager.py +++ b/src/axolotl/telemetry/manager.py @@ -5,7 +5,6 @@ import logging import os import platform -import time import uuid from pathlib import Path from typing import Any @@ -20,21 +19,6 @@ POSTHOG_HOST = "https://app.posthog.com" POSTHOG_WRITE_KEY = "phc_1kUR0o04oJKKTTeSsIz2Mfm5mpiVsQEf2WOlzljMD7y" -OPT_OUT_WARNING_SLEEP_SECONDS = 10 -OPT_OUT_WARNING = ( - "\nTelemetry is now enabled by default to help improve Axolotl. " - "If you'd like to disable it, set AXOLOTL_DO_NOT_TRACK=1 in your environment.\n\n" - "Telemetry data helps us understand:\n" - "- Which features are most used\n" - "- What hardware configurations to prioritize\n" - "- Where users encounter errors\n\n" - "Personally identifiable information (PII) is not collected.\n\n" - "To remove this warning, explicitly set AXOLOTL_DO_NOT_TRACK=0 (enable telemetry) " - "or AXOLOTL_DO_NOT_TRACK=1 (disable telemetry).\n\n" - "For details, see: https://docs.axolotl.ai/docs/telemetry.html\n\n" - f"Sleeping for {OPT_OUT_WARNING_SLEEP_SECONDS}s..." -) - WHITELIST_PATH = str(Path(__file__).parent / "whitelist.yaml") # NOTE: Need to keep these up to date with any config schema changes @@ -183,11 +167,6 @@ def _check_telemetry_enabled(self) -> bool: "false", "true", ): - # Print opt-out info message for main process only - if is_main_process(): - LOG.warning(OPT_OUT_WARNING) - time.sleep(OPT_OUT_WARNING_SLEEP_SECONDS) - return True # Only rank 0 will send telemetry diff --git a/tests/telemetry/test_manager.py b/tests/telemetry/test_manager.py index 2eeae2f11b..36ca44c356 100644 --- a/tests/telemetry/test_manager.py +++ b/tests/telemetry/test_manager.py @@ -118,20 +118,6 @@ def test_telemetry_disabled_for_non_main_process(telemetry_manager_class): assert not manager.enabled -def test_opt_in_info_displayed(telemetry_manager_class): - """Test that opt-in info is displayed when telemetry is not configured""" - with ( - patch.dict(os.environ, {"RANK": "0"}, clear=True), - patch("logging.Logger.warning") as mock_warning, - patch("time.sleep"), - ): - telemetry_manager_class() - assert any( - "Telemetry is now enabled by default" in str(call) - for call in mock_warning.call_args_list - ) - - def test_is_whitelisted(telemetry_manager_class, mock_whitelist): """Test org whitelist functionality""" with ( From 06ac407b92168f79c512df33d42387b6af5aa7f9 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 10 Feb 2026 23:01:34 +0700 Subject: [PATCH 1124/1405] feat: improve telemetry log (#3398) * fix: redact trackio and data_files * fix: add new orgs to whitelist * feat: add run id to logs for users to easily share * fix: update to add more metrics * fix: add missed experiment tracker * chore: formatting in main --- src/axolotl/telemetry/callbacks.py | 18 ++++++++++++++++-- src/axolotl/telemetry/errors.py | 4 ++++ src/axolotl/telemetry/manager.py | 4 ++-- src/axolotl/telemetry/whitelist.yaml | 7 +++++++ src/axolotl/utils/datasets.py | 2 ++ 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/axolotl/telemetry/callbacks.py b/src/axolotl/telemetry/callbacks.py index 0ce52ffa42..1c13bf0cd2 100644 --- a/src/axolotl/telemetry/callbacks.py +++ b/src/axolotl/telemetry/callbacks.py @@ -153,13 +153,27 @@ def on_step_end( self.last_report_step = step def _extract_last_metrics(self, state: TrainerState) -> dict: - """Extract last loss, learning_rate, and grad_norm from log history.""" + """Extract last loss, learning_rate, grad_norm, and token metrics from log history.""" if not state.log_history: - return {"loss": 0, "learning_rate": 0, "grad_norm": 0} + return { + "loss": 0, + "ppl": 0, + "learning_rate": 0, + "grad_norm": 0, + "tokens/total": 0, + "tokens/trainable": 0, + "tokens/train_per_sec_per_gpu": 0, + } last_log = state.log_history[-1] return { "loss": last_log.get("loss", 0), + "ppl": last_log.get("ppl", 0), "learning_rate": last_log.get("learning_rate", 0), "grad_norm": last_log.get("grad_norm", 0), + "tokens/total": last_log.get("tokens/total", 0), + "tokens/trainable": last_log.get("tokens/trainable", 0), + "tokens/train_per_sec_per_gpu": last_log.get( + "tokens/train_per_sec_per_gpu", 0 + ), } diff --git a/src/axolotl/telemetry/errors.py b/src/axolotl/telemetry/errors.py index 27f2d21921..a0c8682355 100644 --- a/src/axolotl/telemetry/errors.py +++ b/src/axolotl/telemetry/errors.py @@ -155,6 +155,10 @@ def wrapper(*args, **kwargs) -> Any: }, ) + LOG.error( + f"Error captured in telemetry. Run ID: {telemetry_manager.run_id}" + ) + raise return wrapper diff --git a/src/axolotl/telemetry/manager.py b/src/axolotl/telemetry/manager.py index 46ef389aac..0774b68a86 100644 --- a/src/axolotl/telemetry/manager.py +++ b/src/axolotl/telemetry/manager.py @@ -30,8 +30,8 @@ "resume_from_checkpoint", "hub_model_id", } -PREFIXES_TO_REDACT = {"wandb_", "comet_", "mlflow_", "gradio_"} -PATH_INDICATORS = {"path", "dir"} +PREFIXES_TO_REDACT = {"wandb_", "comet_", "mlflow_", "gradio_", "trackio_", "swanlab_"} +PATH_INDICATORS = {"path", "dir", "data_files"} # pylint: disable=duplicate-code RELEVANT_PACKAGES = { diff --git a/src/axolotl/telemetry/whitelist.yaml b/src/axolotl/telemetry/whitelist.yaml index 6c94d6e794..c75ee0fec8 100644 --- a/src/axolotl/telemetry/whitelist.yaml +++ b/src/axolotl/telemetry/whitelist.yaml @@ -31,3 +31,10 @@ organizations: - "mistral-community" - "llava-hf" - "ByteDance-Seed" + - "ACE-Step" + - "openbmb" + - "MiniMaxAI" + - "stepfun-ai" + - "internlm" + - "katanemo" + - "XiaomiMiMo" diff --git a/src/axolotl/utils/datasets.py b/src/axolotl/utils/datasets.py index 19ad716409..7beeb27333 100644 --- a/src/axolotl/utils/datasets.py +++ b/src/axolotl/utils/datasets.py @@ -1,10 +1,12 @@ """helper functions for datasets""" import os + from axolotl.utils.logging import get_logger LOG = get_logger(__name__) + def get_default_process_count(): if axolotl_dataset_num_proc := os.environ.get("AXOLOTL_DATASET_NUM_PROC"): return int(axolotl_dataset_num_proc) From 5eb265513cc2c4e10810abfc9d005c13908d1586 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 12 Feb 2026 08:58:04 -0500 Subject: [PATCH 1125/1405] fix generic patch for cce (#3405) --- .../integrations/cut_cross_entropy/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 08d1680256..5abc38cffb 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -104,7 +104,7 @@ def pre_model_load(self, cfg): def patch_llama_like( self, - model_type: str, + model_type_to_patch: str, ) -> None: """ Generic patch for model architectures with causal lm similar to llama @@ -112,7 +112,10 @@ def patch_llama_like( from cut_cross_entropy.transformers.patch import PATCH_FNS def patch_generic( - maybe_model, patch_options, model_type: str, remote_model_id: str | None + maybe_model, + patch_options, + remote_model_id: str | None, + model_type: str, ): import cut_cross_entropy.transformers.llama from cut_cross_entropy.transformers.llama import cce_forward @@ -136,11 +139,13 @@ def patch_generic( f"Error: {str(e)}" ) from e - if model_type not in PATCH_FNS: + if model_type_to_patch not in PATCH_FNS: LOG.warning_once( - "Setting up generic cce patch for model type: %s", model_type + "Setting up generic cce patch for model type: %s", model_type_to_patch ) LOG.warning_once( - f"Generic Cut Cross Entropy + {model_type} support is experimental and may not work as expected." + f"Generic Cut Cross Entropy + {model_type_to_patch} support is experimental and may not work as expected." + ) + PATCH_FNS[model_type_to_patch] = partial( + patch_generic, model_type=model_type_to_patch ) - PATCH_FNS[model_type] = partial(patch_generic, model_type=model_type) From d6a2532dd7e351fc8952f669d2d7f62a56da2e3d Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sun, 15 Feb 2026 19:51:28 +0700 Subject: [PATCH 1126/1405] feat(doc): clarify how to use scattermoe (#3408) [skip ci] * feat(doc): clarify how to use scattermoe * chore: fix wording --- src/axolotl/integrations/kernels/README.md | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/axolotl/integrations/kernels/README.md diff --git a/src/axolotl/integrations/kernels/README.md b/src/axolotl/integrations/kernels/README.md new file mode 100644 index 0000000000..96ff7b328e --- /dev/null +++ b/src/axolotl/integrations/kernels/README.md @@ -0,0 +1,44 @@ +# Kernels Integration + +MoE (Mixture of Experts) kernels speed up training for MoE layers and reduce VRAM costs. In transformers v5, `batched_mm` and `grouped_mm` were integrated as built-in options via the `experts_implementation` config kwarg: + +```python +class ExpertsInterface(GeneralInterface): + _global_mapping = { + "batched_mm": batched_mm_experts_forward, + "grouped_mm": grouped_mm_experts_forward, + } +``` + +In our custom integration, we add support for **ScatterMoE**, which is even more efficient and faster than `grouped_mm`. + +## Usage + +Add the following to your axolotl YAML config: + +```yaml +plugins: + - axolotl.integrations.kernels.KernelsPlugin + +use_kernels: true +use_scattermoe: true +``` + +**Important:** Setting `experts_implementation` is incompatible with `use_scattermoe`. + +## How It Works + +The `KernelsPlugin` runs before model loading and: + +1. Registers the ScatterMoE kernel from the [`axolotl-ai-co/scattermoe`](https://huggingface.co/axolotl-ai-co/scattermoe) Hub repo. +2. Patches the model's `SparseMoeBlock` forward method with the optimized ScatterMoE implementation. + +This works for any MoE model in transformers that uses a `SparseMoeBlock` class (Mixtral, Qwen2-MoE, OLMoE, etc.). + +## Limitations + +ScatterMoE uses a softmax -> topk routing, so results may be different for some model arch as baseline (GPT-OSS, GLM_MOE_DSA). + +## Note on MegaBlocks + +We tested [MegaBlocks](https://huggingface.co/kernels-community/megablocks) but were unable to ensure numerical accuracy, so we did not integrate it. It was also incompatible with many newer model architectures in transformers. From 4f1b5ad29fdb29f97c0c3b1704edcc2963e0dbea Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sun, 15 Feb 2026 19:52:30 +0700 Subject: [PATCH 1127/1405] fix: clarify how to use lm_eval plugin (#3404) [skip ci] --- docs/cli.qmd | 4 +- src/axolotl/integrations/lm_eval/README.md | 49 +++++++++++++++++++- src/axolotl/integrations/lm_eval/__init__.py | 4 +- src/axolotl/integrations/lm_eval/cli.py | 17 ++++++- 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/docs/cli.qmd b/docs/cli.qmd index d9f26dbf85..2bdaf90189 100644 --- a/docs/cli.qmd +++ b/docs/cli.qmd @@ -210,6 +210,8 @@ axolotl lm-eval config.yml Configuration options: ```yaml +lm_eval_model: # model to evaluate (local or hf path) + # List of tasks to evaluate lm_eval_tasks: - arc_challenge @@ -218,7 +220,7 @@ lm_eval_batch_size: # Batch size for evaluation output_dir: # Directory to save evaluation results ``` -See [LM Eval Harness](https://github.com/EleutherAI/lm-evaluation-harness) for more details. +See [LM Eval Harness integration docs](https://docs.axolotl.ai/docs/custom_integrations.html#language-model-evaluation-harness-lm-eval) for full configuration details. ### delinearize-llama4 diff --git a/src/axolotl/integrations/lm_eval/README.md b/src/axolotl/integrations/lm_eval/README.md index f6ed5416ec..860c6681d7 100644 --- a/src/axolotl/integrations/lm_eval/README.md +++ b/src/axolotl/integrations/lm_eval/README.md @@ -6,6 +6,12 @@ See https://github.com/EleutherAI/lm-evaluation-harness ## Usage +There are two ways to use the LM Eval integration: + +### 1. Post-Training Evaluation + +When training with the plugin enabled, evaluation runs automatically after training completes: + ```yaml plugins: - axolotl.integrations.lm_eval.LMEvalPlugin @@ -16,9 +22,50 @@ lm_eval_tasks: - arc_easy lm_eval_batch_size: # Batch size for evaluation -output_dir: # Directory to save evaluation results + +# Directory to save evaluation results. +# The final model is loaded from this directory +# unless specified otherwise (see below) +output_dir: ``` +Run training as usual: +```bash +axolotl train config.yml +``` + +### 2. Standalone CLI Evaluation + +Evaluate any model directly without training: + +```yaml +lm_eval_model: meta-llama/Llama-2-7b-hf + +plugins: + - axolotl.integrations.lm_eval.LMEvalPlugin + +lm_eval_tasks: + - gsm8k + - hellaswag + - arc_easy + +lm_eval_batch_size: 8 +output_dir: ./outputs +``` + +Run evaluation: +```bash +axolotl lm-eval config.yml +``` + +## Model Selection Priority + +The model to evaluate is selected in the following priority order: + +1. **`lm_eval_model`** - Explicit model path or HuggingFace repo (highest priority) +2. **`hub_model_id`** - Trained model pushed to HuggingFace Hub +3. **`output_dir`** - Local checkpoint directory containing trained model weights + ## Citation ```bib diff --git a/src/axolotl/integrations/lm_eval/__init__.py b/src/axolotl/integrations/lm_eval/__init__.py index 0ab6b8697e..6a82dd6cfa 100644 --- a/src/axolotl/integrations/lm_eval/__init__.py +++ b/src/axolotl/integrations/lm_eval/__init__.py @@ -5,7 +5,7 @@ import subprocess # nosec from axolotl.integrations.base import BasePlugin -from axolotl.integrations.lm_eval.cli import build_lm_eval_command +from axolotl.integrations.lm_eval.cli import build_lm_eval_command, get_model_path from .args import LMEvalArgs as LMEvalArgs @@ -29,7 +29,7 @@ def post_train_unload(self, cfg): wandb_project=cfg.wandb_project, wandb_entity=cfg.wandb_entity, wandb_name=cfg.wandb_name, - model=cfg.lm_eval_model or cfg.hub_model_id, + model=get_model_path(cfg), ): subprocess.run( # nosec lm_eval_args, diff --git a/src/axolotl/integrations/lm_eval/cli.py b/src/axolotl/integrations/lm_eval/cli.py index ead82dcb79..4b905d476e 100644 --- a/src/axolotl/integrations/lm_eval/cli.py +++ b/src/axolotl/integrations/lm_eval/cli.py @@ -13,6 +13,21 @@ from axolotl.utils.dict import DictDefault +def get_model_path(cfg: DictDefault) -> str | None: + """ + Determine which model path to use for evaluation. + + Priority order (highest to lowest): + 1. lm_eval_model - Explicit model path override + 2. hub_model_id - Model pushed to HuggingFace Hub + 3. None - Falls back to output_dir in build_lm_eval_command + + Returns: + Model path string or None to use output_dir fallback + """ + return cfg.lm_eval_model or cfg.hub_model_id or None + + def build_lm_eval_command( tasks: list[str], bfloat16=True, @@ -108,7 +123,7 @@ def lm_eval(config: str, cloud: Optional[str] = None): wandb_project=cfg.wandb_project, wandb_entity=cfg.wandb_entity, wandb_name=cfg.wandb_name, - model=cfg.lm_eval_model or cfg.hub_model_id, + model=get_model_path(cfg), revision=cfg.revision, apply_chat_template=cfg.apply_chat_template, fewshot_as_multiturn=cfg.fewshot_as_multiturn, From 145ffc9be124ca44769e17e948a5c4d6b33fdb84 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 19 Feb 2026 18:27:27 -0500 Subject: [PATCH 1128/1405] upgrade transformers to 5.2.0 and torchao to 0.16.0 (#3407) * upgrade transformers to 5.1.0 and torchao to 0.16.0 * upgrade trl for parity * handle trl api changes * orpo doesn't have max_prompt_len to check anymore * cpoconfig doesn't take max_prompt_length and fix cpu offload * slow fsdp1 test * triton min 3.4.0 and liger to 0.7.0 * use transformers main for now for zero3 fix * handle group_by_length change * fix changes upstream * mark skip flaky test * use transformers latest release 5.2.0 --- requirements.txt | 10 +++++----- src/axolotl/core/builders/causal.py | 3 ++- src/axolotl/core/builders/rl.py | 17 ++++++++--------- src/axolotl/core/trainers/dpo/trainer.py | 14 ++++++++------ src/axolotl/loaders/patch_manager.py | 2 ++ .../gradient_checkpointing/offload_cpu.py | 7 ++++++- .../transformers/trainer_loss_calc.py | 8 ++++++-- tests/core/test_builders.py | 1 - tests/e2e/multigpu/test_fsdp1.py | 1 + tests/e2e/multigpu/test_fsdp2.py | 1 + 10 files changed, 39 insertions(+), 25 deletions(-) diff --git a/requirements.txt b/requirements.txt index 565224e924..dc57ebfab1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,21 +2,21 @@ # START section of dependencies that don't install on Darwin/MacOS bitsandbytes==0.49.1 -triton>=3.0.0 +triton>=3.4.0 mamba-ssm==1.2.0.post1 xformers>=0.0.23.post1 -liger-kernel==0.6.4 +liger-kernel==0.7.0 # END section packaging==26.0 huggingface_hub>=1.1.7 peft>=0.18.1 tokenizers>=0.22.1 -transformers==5.0.0 +transformers==5.2.0 accelerate==1.12.0 datasets==4.5.0 deepspeed>=0.18.3 -trl==0.27.1 +trl==0.28.0 hf_xet==1.2.0 kernels==0.11.5 @@ -63,7 +63,7 @@ langdetect==1.0.9 immutabledict==4.2.0 antlr4-python3-runtime==4.13.2 -torchao==0.13.0 +torchao==0.16.0 openenv-core==0.1.0 schedulefree==1.4.1 diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 09bcff4505..7bfc5e874f 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -246,7 +246,8 @@ def build(self, total_num_steps): ddp_find_unused_parameters ) - training_arguments_kwargs["group_by_length"] = self.cfg.group_by_length + if self.cfg.group_by_length: + training_arguments_kwargs["train_sampling_strategy"] = "group_by_length" training_arguments_kwargs["curriculum_sampling"] = self.cfg.curriculum_sampling training_arguments_kwargs["sample_packing"] = bool(self.cfg.sample_packing) diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index 0bd2eedfc9..5a7343ca78 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -11,7 +11,6 @@ ) from axolotl.core.trainers.dpo import DPOStrategy from axolotl.core.trainers.dpo.args import AxolotlDPOConfig -from axolotl.core.trainers.grpo import GRPOStrategy from axolotl.integrations.base import PluginManager from axolotl.loaders.utils import ensure_dtype from axolotl.utils.callbacks.qat import QATCallback @@ -53,6 +52,8 @@ def _get_trainer_cls(self, trainer_kwargs: dict): trainer_cls_args = [self.model] if self.cfg.rl in {RLType.GRPO, RLType.GDPO}: + from axolotl.core.trainers.grpo import GRPOStrategy + trainer_cls = GRPOStrategy.get_trainer_class( sequence_parallel=self.cfg.context_parallel_size > 1 ) @@ -133,21 +134,17 @@ def _build_training_arguments(self, total_num_steps): if self.cfg.cpo_alpha is not None: training_args_kwargs["cpo_alpha"] = self.cfg.cpo_alpha - # Handle when max_prompt_length == max_length from defaults - # CPOTrainer requires strictly less than - if ( - training_args_kwargs["max_prompt_length"] - == training_args_kwargs["max_length"] - ): - training_args_kwargs["max_prompt_length"] -= 1 + blocklist_args_kwargs.append("max_prompt_length") elif self.cfg.rl is RLType.ORPO: training_args_cls = AxolotlORPOConfig + blocklist_args_kwargs.append("max_prompt_length") + elif self.cfg.rl is RLType.KTO: training_args_cls = AxolotlKTOConfig # KTOConfig in TRL >= 0.27.0 no longer accepts max_prompt_length - blocklist_args_kwargs = ["max_prompt_length"] + blocklist_args_kwargs.append("max_prompt_length") training_args_kwargs["desirable_weight"] = ( self.cfg.kto_desirable_weight or 1.0 @@ -157,6 +154,8 @@ def _build_training_arguments(self, total_num_steps): ) elif self.cfg.rl in {RLType.GRPO, RLType.GDPO}: + from axolotl.core.trainers.grpo import GRPOStrategy + training_args_cls = GRPOStrategy.get_training_args_class() training_args_kwargs.update(GRPOStrategy.set_training_args_kwargs(self.cfg)) blocklist_args_kwargs = GRPOStrategy.get_blocklist_args_kwargs() diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index b04505d89c..92307fe238 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -57,16 +57,18 @@ def push_to_hub(self, *args, **kwargs) -> str: def tokenize_row( features, processing_class, - max_prompt_length, - max_completion_length, - add_special_tokens, + max_prompt_length: int | None = None, + max_completion_length: int | None = None, + add_special_tokens: bool = True, + is_chat: bool = False, ) -> Dict: res = DPOTrainer.tokenize_row( features, processing_class, - max_prompt_length, - max_completion_length, - add_special_tokens, + max_prompt_length=max_prompt_length, + max_completion_length=max_completion_length, + add_special_tokens=add_special_tokens, + is_chat=is_chat, ) # fix when the tokenizer doesn't have a bos_token_id, e.g. Qwen if processing_class.bos_token is None and res["prompt_input_ids"][0] is None: diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 3cf8bbd20d..2222600200 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -10,6 +10,7 @@ import addict import transformers from transformers import PretrainedConfig, PreTrainedModel +from transformers.modeling_flash_attention_utils import is_flash_attn_available from axolotl.integrations.base import PluginManager from axolotl.monkeypatch.multipack import ( @@ -500,6 +501,7 @@ def _apply_llama_flash_attn_patches(self, model): and not self.cfg.trust_remote_code and not self.cfg.gptq and self.cfg.flash_attention + and is_flash_attn_available() and not self.inference ): # TODO(MengqingCao): split these patches separately diff --git a/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py b/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py index 8d06f172db..886441196e 100644 --- a/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py +++ b/src/axolotl/monkeypatch/gradient_checkpointing/offload_cpu.py @@ -59,7 +59,12 @@ def backward(ctx, dY): hidden_states = hidden_states.to("cuda", non_blocking=True).detach() hidden_states.requires_grad = True with torch.enable_grad(): - (output,) = ctx.forward_function(hidden_states, *ctx.args) + output = ctx.forward_function(hidden_states, *ctx.args) + # Newer HF models (e.g. Qwen3MoE) using GradientCheckpointingLayer + # return a plain tensor, not a tuple. Older models return tuples + # like (hidden_states, present_kv, ...). Unwrap if needed. + if isinstance(output, (tuple, list)): + (output,) = output torch.autograd.backward(output, dY) return ( None, diff --git a/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py b/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py index b8172bbe6c..3a99d01154 100644 --- a/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py +++ b/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py @@ -28,8 +28,12 @@ "array": 'metrics[f"{metric_key_prefix}_loss"] = np.nanmean(all_losses).item()', } -ORIGINAL_MAYBE_CODE = "tr_loss_scalar = self._nested_gather(tr_loss).mean().item()" -PATCHED_MAYBE_CODE = "tr_loss_scalar = self._nested_gather(tr_loss).nanmean().item()" +ORIGINAL_MAYBE_CODE = ( + "tr_loss_scalar = nested_gather(tr_loss, self.args.parallel_mode).mean().item()" +) +PATCHED_MAYBE_CODE = ( + "tr_loss_scalar = nested_gather(tr_loss, self.args.parallel_mode).nanmean().item()" +) def check_evaluation_loop_is_patchable() -> bool: diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index 194950e150..fc16f723e9 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -300,7 +300,6 @@ def test_orpo_training_arguments(self, orpo_cfg, model, tokenizer): self._test_common_training_arguments(training_arguments, rl=orpo_cfg.rl) # ORPO specific assert training_arguments.beta == 0.1 # maps from orpo_alpha - assert training_arguments.max_prompt_length == 512 def test_kto_training_arguments(self, kto_cfg, model, tokenizer): builder = HFRLTrainerBuilder(kto_cfg, model, tokenizer) diff --git a/tests/e2e/multigpu/test_fsdp1.py b/tests/e2e/multigpu/test_fsdp1.py index e503162872..5b67247914 100644 --- a/tests/e2e/multigpu/test_fsdp1.py +++ b/tests/e2e/multigpu/test_fsdp1.py @@ -186,6 +186,7 @@ def test_lora_sft(self, temp_dir, adapter_config): verify_training_success(temp_dir) + @pytest.mark.skip(reason="slow test, deprecate fsdp1 asap") def test_dpo_fft(self, temp_dir): cfg = DictDefault( { diff --git a/tests/e2e/multigpu/test_fsdp2.py b/tests/e2e/multigpu/test_fsdp2.py index 19239a3eca..e104562407 100644 --- a/tests/e2e/multigpu/test_fsdp2.py +++ b/tests/e2e/multigpu/test_fsdp2.py @@ -365,6 +365,7 @@ def test_qlora_sft_kernels(self, temp_dir): verify_training_success(temp_dir) + @pytest.mark.skip(reason="slow test w cu129 + torch 2.9.1 + py3.12") @require_torch_2_7_0 def test_dpo_fft(self, temp_dir): cfg = DictDefault( From 7fbedbd300f187269253536c7d033546b8d0e229 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 20 Feb 2026 06:32:26 +0700 Subject: [PATCH 1129/1405] fix(doc): add limitation for unfrozen_parameters (#3416) --- src/axolotl/utils/schemas/config.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 91bc221aca..f1627367be 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -446,7 +446,16 @@ class AxolotlInputConfig( }, ) - unfrozen_parameters: list[str] | None = None + unfrozen_parameters: list[str] | None = Field( + default=None, + json_schema_extra={ + "description": "List of regex patterns for parameter names to keep unfrozen. " + "All other parameters will be frozen via requires_grad=False. " + "Note: range-based patterns (e.g. embed_tokens.weight$[:32000]) use gradient " + "zeroing rather than a true freeze, so weight decay will still apply to the " + "frozen portion and optimizer states are allocated for the full parameter." + }, + ) sequence_len: int = Field( default=512, From 29722dec60f7ca3d41ac485ca42f2abfdc872e58 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 20 Feb 2026 00:09:25 -0500 Subject: [PATCH 1130/1405] use bunnycdn for CI assets (#3422) [skip ci] --- .github/workflows/tests-nightly.yml | 2 +- .github/workflows/tests.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 21446e5488..33aa8525a9 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -37,7 +37,7 @@ jobs: id: hf-cache-restore-s3 run: | mkdir -p /home/runner/.cache/huggingface/hub - curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xf - -C /home/runner/.cache/huggingface/hub/ --use-compress-program unzstd + curl -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst | tar -xf - -C /home/runner/.cache/huggingface/hub/ --use-compress-program unzstd - name: Setup Python uses: actions/setup-python@v5 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c866e0cfc3..39402d61a6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -75,7 +75,7 @@ jobs: id: hf-cache-restore-s3 run: | mkdir -p ~/.cache/huggingface/hub - curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xpf - -C ~/.cache/huggingface/hub/ --use-compress-program unzstd --strip-components=1 + curl -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst | tar -xpf - -C ~/.cache/huggingface/hub/ --use-compress-program unzstd --strip-components=1 ls -ltr ~/.cache/huggingface/hub/ - name: Setup Python @@ -170,7 +170,7 @@ jobs: id: hf-cache-restore-s3 run: | mkdir -p ~/.cache/huggingface/hub - curl -L https://d1dttdx32dkk5p.cloudfront.net/hf-cache.tar.zst | tar -xpf - -C ~/.cache/huggingface/hub/ --use-compress-program unzstd --strip-components=1 + curl -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst | tar -xpf - -C ~/.cache/huggingface/hub/ --use-compress-program unzstd --strip-components=1 ls -ltr ~/.cache/huggingface/hub/ - name: Setup Python From 0ea252d3927af1b2ffcde360a6a51239f045d70f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 20 Feb 2026 14:24:33 -0500 Subject: [PATCH 1131/1405] update to trackio 0.16.1 (#3425) [skip ci] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dc57ebfab1..09b1f625b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ trl==0.28.0 hf_xet==1.2.0 kernels==0.11.5 -trackio>=0.13.0 +trackio>=0.16.1 typing-extensions>=4.15.0 optimum==1.16.2 From 43d60c7439d03235f88c27f04f7c53f229bc44f7 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 21 Feb 2026 02:24:51 +0700 Subject: [PATCH 1132/1405] bump cut-cross-entropy to 58d6572 (#3424) --- examples/colab-notebooks/colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/integrations/cut_cross_entropy/README.md | 11 +++++++++-- .../integrations/cut_cross_entropy/__init__.py | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index da3a681cf9..2a00cd1d01 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0d4ce4b\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@58d6572\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 02e57f926d..afc172e22e 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0d4ce4b"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@58d6572"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 728a097420..b3f475dc2c 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0d4ce4b" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@58d6572" ``` ## Usage @@ -31,6 +31,7 @@ plugins: ## Supported Models +- afmoe - apertus - arcee - cohere @@ -51,6 +52,7 @@ plugins: - glm4v - glm4v_moe - glm_image +- glm_moe_dsa - gpt_oss - granite - granitemoe @@ -76,14 +78,19 @@ plugins: - olmo - olmo2 - olmo3 +- olmoe - phi - phi3 - phi4_multimodal - qwen2 +- qwen2_5_vl - qwen2_moe - qwen2_vl -- qwen2_5_vl - qwen3 +- qwen3_5 +- qwen3_5_moe +- qwen3_5_moe_vl +- qwen3_5_vl - qwen3_moe - qwen3_next - qwen3_vl diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 5abc38cffb..36681d7705 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@0d4ce4b"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@58d6572"`' ) From 3f30572d4ab96e2ff680aa18353e5a6446e1f4cd Mon Sep 17 00:00:00 2001 From: Lorenzo Baraldi <48087510+lorenzbaraldi@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:18:37 +0100 Subject: [PATCH 1133/1405] Fix typo in dataset_processes field (#3426) * Fix typo in dataset_processes field * fix: use updated config name --------- Co-authored-by: NanoCode012 --- src/axolotl/utils/trainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index fb381a8a19..621ef87858 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -480,7 +480,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): bin_size=cfg.sample_packing_bin_size, sequential=cfg.sample_packing_sequentially, drop_last=True, - num_processes=cfg.dataset_prcoesses, + num_processes=cfg.dataset_num_proc, mp_start_method=cfg.sample_packing_mp_start_method or "fork", ) From 5ed455715e99693bb036c91facb8ebf5a284c5bb Mon Sep 17 00:00:00 2001 From: Manas Vardhan Date: Mon, 23 Feb 2026 07:10:06 -0800 Subject: [PATCH 1134/1405] feat: support dot-notation CLI args for nested config options (#3419) * feat: support dot-notation CLI args for nested config options Add support for overriding nested config fields (like TRL config) via CLI using dot-notation, e.g.: axolotl train grpo.yaml --trl.vllm-server-host=10.0.0.1 --trl.beta=0.1 Changes: - args.py: Detect BaseModel subclass fields and generate dot-notation CLI options (--parent.child) that map to double-underscore kwargs (parent__child). Also fix _strip_optional_type for Python 3.10+ union syntax (X | None). - config.py: Handle double-underscore kwargs in load_cfg by setting nested dict values on the config. - Add tests for nested option handling. Fixes #2702 * Address CodeRabbit review: fix string parent bug, add type hints and docstring Signed-off-by: Manas Vardhan * Add type coercion for CLI kwargs and fix pre-commit issues - Add _coerce_value() for YAML-style type inference on string CLI args - When existing config value has a type (int/float/bool), cast to match - When no existing value, infer type from string (true/false, ints, floats, null) - Apply coercion to both flat and nested (dot-notation) kwargs - Fix unused pytest import (pre-commit/ruff) - Update tests to pass string values (matching real CLI behavior) - Add dedicated TestCoerceValue test class Addresses maintainer feedback on type casting for nested kwargs. --------- Signed-off-by: Manas Vardhan --- src/axolotl/cli/config.py | 91 ++++++++++++- src/axolotl/cli/utils/args.py | 70 +++++++++- tests/cli/test_nested_options.py | 227 +++++++++++++++++++++++++++++++ 3 files changed, 381 insertions(+), 7 deletions(-) create mode 100644 tests/cli/test_nested_options.py diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index 986167f021..b6f79c74cc 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -5,7 +5,7 @@ import tempfile from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Union +from typing import Any, Optional, Union from urllib.parse import urlparse import requests @@ -32,6 +32,63 @@ LOG = get_logger(__name__) + +def _coerce_value(value: Any, existing: Optional[Any] = None) -> Any: + """Coerce a string CLI value to its most likely Python type. + + If an existing value is present in the config, its type is used to guide + casting. Otherwise, YAML-style inference is applied: booleans, ints, + floats, and None literals are recognised automatically. + + Args: + value: The raw value (typically a string from the CLI). + existing: An optional existing config value whose type guides coercion. + + Returns: + The value cast to the inferred or expected type. + """ + if not isinstance(value, str): + return value + + # If the config already has a typed value, cast to match + if existing is not None: + if isinstance(existing, bool): + return value.lower() in ("true", "1", "yes") + if isinstance(existing, int): + try: + return int(value) + except (ValueError, TypeError): + return value + if isinstance(existing, float): + try: + return float(value) + except (ValueError, TypeError): + return value + # For other types (str, list, dict, etc.), return as-is + return value + + # No existing value -- use YAML-style inference + lower = value.lower() + if lower in ("true", "yes"): + return True + if lower in ("false", "no"): + return False + if lower in ("null", "none", "~"): + return None + + # Try int then float + try: + return int(value) + except ValueError: + pass + try: + return float(value) + except ValueError: + pass + + return value + + API_KEY_FIELDS = {"comet_api_key"} TELEMETRY_MANAGER = TelemetryManager.get_instance() @@ -208,13 +265,37 @@ def load_cfg( # If there are any options passed in the cli, if it is something that seems valid # from the yaml, then overwrite the value cfg_keys = cfg.keys() + + # Separate nested (dot-notation) kwargs from flat kwargs + nested_kwargs: dict[str, dict[str, Any]] = {} + flat_kwargs: dict[str, Any] = {} for key, value in kwargs.items(): + if "__" in key: + parent, child = key.split("__", 1) + nested_kwargs.setdefault(parent, {})[child] = value + else: + flat_kwargs[key] = value + + # Apply flat kwargs + for key, value in flat_kwargs.items(): # If not strict, allow writing to cfg even if it's not in the yml already if key in cfg_keys or not cfg.strict: - if isinstance(cfg[key], bool): - cfg[key] = bool(value) - else: - cfg[key] = value + cfg[key] = _coerce_value(value, cfg.get(key)) + + # Apply nested kwargs (e.g., trl__beta -> cfg.trl.beta) + for parent, children in nested_kwargs.items(): + if parent not in cfg_keys and cfg.strict: + continue + if cfg[parent] is None: + cfg[parent] = {} + if not isinstance(cfg[parent], dict): + LOG.warning( + "Overwriting non-dict value for '%s' with nested CLI overrides", parent + ) + cfg[parent] = {} + for child_key, child_value in children.items(): + existing_child = cfg[parent].get(child_key) + cfg[parent][child_key] = _coerce_value(child_value, existing_child) try: device_props = torch.cuda.get_device_properties("cuda") diff --git a/src/axolotl/cli/utils/args.py b/src/axolotl/cli/utils/args.py index 0aec737b82..d50a9163a5 100644 --- a/src/axolotl/cli/utils/args.py +++ b/src/axolotl/cli/utils/args.py @@ -2,7 +2,7 @@ import dataclasses from functools import wraps -from types import NoneType +from types import NoneType, UnionType from typing import Any, Callable, Type, Union, get_args, get_origin import click @@ -20,7 +20,8 @@ def _strip_optional_type(field_type: type | str | None): If the input type is `Union[T, None]` or `Optional[T]`, returns `T`. Otherwise returns the input type unchanged. """ - if get_origin(field_type) is Union and type(None) in get_args(field_type): + is_union = get_origin(field_type) is Union or isinstance(field_type, UnionType) + if is_union and type(None) in get_args(field_type): field_type = next( t for t in get_args(field_type) if not isinstance(t, NoneType) ) @@ -87,10 +88,70 @@ def decorator(function: Callable) -> Callable: return decorator +def _is_pydantic_model(field_type: type) -> bool: + """Check if a type is a Pydantic BaseModel subclass.""" + try: + return isinstance(field_type, type) and issubclass(field_type, BaseModel) + except TypeError: + return False + + +def _get_field_description(field) -> str | None: + """Get description from a Pydantic field, checking both .description and json_schema_extra.""" + if field.description: + return field.description + if field.json_schema_extra and isinstance(field.json_schema_extra, dict): + return field.json_schema_extra.get("description") + return None + + +def _add_nested_model_options( + function: Callable, parent_name: str, model_class: Type[BaseModel] +) -> Callable: + """ + Add Click options for all fields of a nested Pydantic model using dot-notation. + + Note: Only single-level nesting is supported (e.g., ``--trl.beta``). + Deeper nesting (e.g., ``--trl.scheduler.warmup``) is not handled. + + Args: + function: Click command function to add options to. + parent_name: Parent field name (e.g., "trl"). + model_class: Nested Pydantic model class. + + Returns: + Function with added Click options. + """ + for sub_name, sub_field in reversed(model_class.model_fields.items()): + sub_type = _strip_optional_type(sub_field.annotation) + # Use dot notation: --parent.sub_field + cli_name = f"{parent_name}.{sub_name}".replace("_", "-") + # The kwarg name uses double-underscore as separator + param_name = f"{parent_name}__{sub_name}" + description = _get_field_description(sub_field) + + if sub_type is bool: + option_name = f"--{cli_name}/--no-{cli_name}" + function = click.option( + option_name, param_name, default=None, help=description + )(function) + else: + option_name = f"--{cli_name}" + click_type = {str: str, int: int, float: float}.get(sub_type) + function = click.option( + option_name, param_name, default=None, type=click_type, help=description + )(function) + + return function + + def add_options_from_config(config_class: Type[BaseModel]) -> Callable: """ Create Click options from the fields of a Pydantic model. + For fields whose type is itself a Pydantic BaseModel, dot-notation CLI options are + generated for each sub-field (e.g., ``--trl.beta=0.1``). + Args: config_class: PyDantic model with fields to parse from the CLI @@ -103,6 +164,11 @@ def decorator(function: Callable) -> Callable: for name, field in reversed(config_class.model_fields.items()): field_type = _strip_optional_type(field.annotation) + # Handle nested Pydantic models with dot-notation options + if _is_pydantic_model(field_type): + function = _add_nested_model_options(function, name, field_type) + continue + if field_type is bool: field_name = name.replace("_", "-") option_name = f"--{field_name}/--no-{field_name}" diff --git a/tests/cli/test_nested_options.py b/tests/cli/test_nested_options.py new file mode 100644 index 0000000000..221de951e7 --- /dev/null +++ b/tests/cli/test_nested_options.py @@ -0,0 +1,227 @@ +"""Tests for nested config option handling via CLI dot-notation.""" + +import click +from click.testing import CliRunner +from pydantic import BaseModel, Field + +from axolotl.cli.utils.args import add_options_from_config, filter_none_kwargs + + +class InnerConfig(BaseModel): + """A nested config model for testing.""" + + beta: float | None = Field( + default=None, + description="Beta parameter.", + ) + host: str | None = Field( + default=None, + description="Server host.", + ) + use_feature: bool = Field( + default=False, + description="Whether to use the feature.", + ) + + +class OuterConfig(BaseModel): + """A top-level config model for testing.""" + + learning_rate: float | None = Field( + default=None, + description="Learning rate.", + ) + inner: InnerConfig | None = Field( + default=None, + description="Inner config.", + ) + name: str | None = Field( + default=None, + description="Model name.", + ) + + +class TestAddOptionsFromConfigNested: + """Test that add_options_from_config handles nested BaseModel fields.""" + + def setup_method(self): + self.runner = CliRunner() + + def test_nested_dot_notation_options_are_registered(self): + """Nested model fields should create --parent.child CLI options.""" + + @click.command() + @add_options_from_config(OuterConfig) + @filter_none_kwargs + def cmd(**kwargs): + for k, v in sorted(kwargs.items()): + click.echo(f"{k}={v}") + + result = self.runner.invoke(cmd, ["--inner.beta=0.5", "--inner.host=localhost"]) + assert result.exit_code == 0, result.output + assert "inner__beta=0.5" in result.output + assert "inner__host=localhost" in result.output + + def test_nested_bool_option(self): + """Nested bool fields should support --parent.field/--no-parent.field.""" + + @click.command() + @add_options_from_config(OuterConfig) + @filter_none_kwargs + def cmd(**kwargs): + for k, v in sorted(kwargs.items()): + click.echo(f"{k}={v}") + + result = self.runner.invoke(cmd, ["--inner.use-feature"]) + assert result.exit_code == 0, result.output + assert "inner__use_feature=True" in result.output + + def test_flat_and_nested_options_together(self): + """Flat and nested options should work together.""" + + @click.command() + @add_options_from_config(OuterConfig) + @filter_none_kwargs + def cmd(**kwargs): + for k, v in sorted(kwargs.items()): + click.echo(f"{k}={v}") + + result = self.runner.invoke( + cmd, ["--learning-rate=0.001", "--inner.beta=0.1", "--name=test"] + ) + assert result.exit_code == 0, result.output + assert "learning_rate=0.001" in result.output + assert "inner__beta=0.1" in result.output + assert "name=test" in result.output + + def test_no_nested_options_passed(self): + """When no nested options are passed, they should not appear in kwargs.""" + + @click.command() + @add_options_from_config(OuterConfig) + @filter_none_kwargs + def cmd(**kwargs): + click.echo(f"keys={sorted(kwargs.keys())}") + + result = self.runner.invoke(cmd, ["--learning-rate=0.01"]) + assert result.exit_code == 0, result.output + assert "inner__" not in result.output + + +class TestLoadCfgNestedKwargs: + """Test that load_cfg correctly applies nested (double-underscore) kwargs.""" + + @staticmethod + def _apply_nested_kwargs(cfg, kwargs): + """Helper that mirrors the nested kwargs handling from load_cfg, + including type coercion for string CLI values.""" + from axolotl.cli.config import _coerce_value + + nested_kwargs: dict = {} + flat_kwargs: dict = {} + for key, value in kwargs.items(): + if "__" in key: + parent, child = key.split("__", 1) + nested_kwargs.setdefault(parent, {})[child] = value + else: + flat_kwargs[key] = value + + cfg_keys = cfg.keys() + for key, value in flat_kwargs.items(): + if key in cfg_keys: + cfg[key] = _coerce_value(value, cfg.get(key)) + + for parent, children in nested_kwargs.items(): + if cfg[parent] is None: + cfg[parent] = {} + if not isinstance(cfg[parent], dict): + cfg[parent] = {} + for child_key, child_value in children.items(): + existing = cfg[parent].get(child_key) + cfg[parent][child_key] = _coerce_value(child_value, existing) + + return cfg + + def test_nested_kwargs_applied_to_cfg(self, tmp_path): + """Double-underscore kwargs should set nested config values.""" + from axolotl.utils.dict import DictDefault + + cfg = DictDefault({"trl": {"beta": 0.1}, "learning_rate": 0.01}) + # CLI passes strings, so simulate that + kwargs = { + "trl__beta": "0.5", + "trl__host": "192.168.1.1", + "learning_rate": "0.02", + } + + cfg = self._apply_nested_kwargs(cfg, kwargs) + + assert cfg["learning_rate"] == 0.02 + assert isinstance(cfg["learning_rate"], float) + assert cfg["trl"]["beta"] == 0.5 + assert isinstance(cfg["trl"]["beta"], float) + assert cfg["trl"]["host"] == "192.168.1.1" + + def test_nested_kwargs_creates_parent_if_none(self): + """If the parent key is None, nested kwargs should create the dict.""" + from axolotl.utils.dict import DictDefault + + cfg = DictDefault({"trl": None, "learning_rate": 0.01}) + cfg = self._apply_nested_kwargs(cfg, {"trl__beta": "0.5"}) + + # No existing value, YAML-style inference: "0.5" -> 0.5 + assert cfg["trl"]["beta"] == 0.5 + assert isinstance(cfg["trl"]["beta"], float) + + def test_nested_kwargs_overwrites_string_parent(self): + """If the parent key is a string, it should be replaced with a dict.""" + from axolotl.utils.dict import DictDefault + + cfg = DictDefault({"trl": "some_string", "learning_rate": 0.01}) + cfg = self._apply_nested_kwargs(cfg, {"trl__beta": "0.5"}) + + assert cfg["trl"]["beta"] == 0.5 + + +class TestCoerceValue: + """Test YAML-style type coercion for CLI string values.""" + + def test_coerce_with_existing_float(self): + from axolotl.cli.config import _coerce_value + + assert _coerce_value("0.5", 0.1) == 0.5 + assert isinstance(_coerce_value("0.5", 0.1), float) + + def test_coerce_with_existing_int(self): + from axolotl.cli.config import _coerce_value + + assert _coerce_value("42", 10) == 42 + assert isinstance(_coerce_value("42", 10), int) + + def test_coerce_with_existing_bool(self): + from axolotl.cli.config import _coerce_value + + assert _coerce_value("true", False) is True + assert _coerce_value("false", True) is False + assert _coerce_value("1", False) is True + assert _coerce_value("0", True) is False + + def test_coerce_yaml_inference_no_existing(self): + """Without an existing value, use YAML-style inference.""" + from axolotl.cli.config import _coerce_value + + assert _coerce_value("true", None) is True + assert _coerce_value("false", None) is False + assert _coerce_value("42", None) == 42 + assert isinstance(_coerce_value("42", None), int) + assert _coerce_value("3.14", None) == 3.14 + assert isinstance(_coerce_value("3.14", None), float) + assert _coerce_value("null", None) is None + assert _coerce_value("hello", None) == "hello" + + def test_coerce_non_string_passthrough(self): + """Non-string values should pass through unchanged.""" + from axolotl.cli.config import _coerce_value + + assert _coerce_value(0.5, 0.1) == 0.5 + assert _coerce_value(True, False) is True From 86ca1e27c083d0ab22063630aa9bea02085714c1 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 23 Feb 2026 23:39:13 +0700 Subject: [PATCH 1135/1405] fix: update MistralProcessor to be v5 compat (#3423) * fix: update MistralProcessor to be v5 compat * feat: add test for mistral3 processor * chore: comment --- .../utils/mistral/mistral3_processor.py | 12 +- tests/utils/test_mistral3_processor.py | 149 ++++++++++++++++++ 2 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 tests/utils/test_mistral3_processor.py diff --git a/src/axolotl/utils/mistral/mistral3_processor.py b/src/axolotl/utils/mistral/mistral3_processor.py index 01e8f9f100..03a155e53d 100644 --- a/src/axolotl/utils/mistral/mistral3_processor.py +++ b/src/axolotl/utils/mistral/mistral3_processor.py @@ -30,18 +30,8 @@ class Mistral3Processor(ProcessorMixin): Wraps HFMistralTokenizer and adds image processing capabilities. """ - # TODO(nano): This should be removed in transformers V5 - attributes = ["tokenizer"] - tokenizer_class = "HFMistralTokenizer" - def __init__(self, tokenizer: HFMistralTokenizer): - # Don't call super().__init__ to avoid the class validation issue - self.tokenizer = tokenizer - - @property - def chat_template(self) -> None: - """Chat template is not supported. Dummy method to satisfy HuggingFace API.""" - return None + super().__init__(tokenizer) @property def audio_tokenizer(self) -> None: diff --git a/tests/utils/test_mistral3_processor.py b/tests/utils/test_mistral3_processor.py new file mode 100644 index 0000000000..ae2bc1fafd --- /dev/null +++ b/tests/utils/test_mistral3_processor.py @@ -0,0 +1,149 @@ +"""Tests for Mistral3Processor with transformers v5 ProcessorMixin integration""" + +from unittest.mock import MagicMock + +import pytest +import torch +from transformers.feature_extraction_utils import BatchFeature + +from axolotl.utils.mistral.mistral3_processor import Mistral3Processor +from axolotl.utils.mistral.mistral_tokenizer import HFMistralTokenizer + + +@pytest.fixture() +def mock_tokenizer(): + """Create a mock HFMistralTokenizer that passes v5 ProcessorMixin isinstance checks.""" + return MagicMock(spec=HFMistralTokenizer) + + +@pytest.fixture() +def processor(mock_tokenizer): + return Mistral3Processor(tokenizer=mock_tokenizer) + + +class TestMistral3ProcessorInit: + def test_tokenizer_is_set(self, processor, mock_tokenizer): + assert processor.tokenizer is mock_tokenizer + + def test_chat_template_is_none(self, processor): + assert processor.chat_template is None + + def test_audio_tokenizer_is_none(self, processor): + assert processor.audio_tokenizer is None + + +class TestApplyChatTemplateTokenized: + """Test apply_chat_template with tokenize=True, return_dict=True""" + + @pytest.fixture() + def batched_conversations(self): + return [ + [ + {"role": "user", "content": "Describe this image."}, + {"role": "assistant", "content": "It is red."}, + ], + [ + {"role": "user", "content": "What is this?"}, + {"role": "assistant", "content": "A cat."}, + ], + ] + + def test_returns_batch_feature_with_pixel_values( + self, processor, mock_tokenizer, batched_conversations + ): + pixel_values = torch.randn(2, 3, 224, 224, dtype=torch.float64) + mock_tokenizer.apply_chat_template.return_value = { + "input_ids": torch.tensor([[1, 2, 3], [4, 5, 6]]), + "attention_mask": torch.tensor([[1, 1, 1], [1, 1, 1]]), + "pixel_values": pixel_values, + } + + result = processor.apply_chat_template( + batched_conversations, tokenize=True, return_dict=True + ) + + assert isinstance(result, BatchFeature) + assert "pixel_values" in result + assert "image_sizes" in result + assert result["pixel_values"].dtype == torch.float32 + assert result["image_sizes"].shape == (2, 2) + assert result["image_sizes"][0].tolist() == [224, 224] + + def test_returns_batch_feature_without_pixel_values( + self, processor, mock_tokenizer, batched_conversations + ): + mock_tokenizer.apply_chat_template.return_value = { + "input_ids": torch.tensor([[1, 2, 3], [4, 5, 6]]), + "attention_mask": torch.tensor([[1, 1, 1], [1, 1, 1]]), + } + + result = processor.apply_chat_template( + batched_conversations, tokenize=True, return_dict=True + ) + + assert isinstance(result, BatchFeature) + assert "input_ids" in result + assert "image_sizes" not in result + + +class TestApplyChatTemplateNotTokenized: + def test_single_conversation_returns_unwrapped(self, processor, mock_tokenizer): + """Single conversation (not batched) should return unwrapped result.""" + single_conversation = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"}, + ] + mock_tokenizer.apply_chat_template.return_value = [ + "[INST]Hello[/INST]Hi" + ] + + result = processor.apply_chat_template( + single_conversation, tokenize=False, return_dict=False + ) + + assert result == "[INST]Hello[/INST]Hi" + + def test_batched_conversations_returns_list(self, processor, mock_tokenizer): + batched = [ + [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"}, + ], + [ + {"role": "user", "content": "Bye"}, + {"role": "assistant", "content": "Bye"}, + ], + ] + mock_tokenizer.apply_chat_template.return_value = ["text1", "text2"] + + result = processor.apply_chat_template( + batched, tokenize=False, return_dict=False + ) + + assert result == ["text1", "text2"] + + +class TestCall: + def test_delegates_to_tokenizer(self, processor, mock_tokenizer): + mock_tokenizer.return_value = { + "input_ids": [1, 2, 3], + "attention_mask": [1, 1, 1], + } + + result = processor("Hello world") + + mock_tokenizer.assert_called_once() + assert isinstance(result, BatchFeature) + + +class TestReturnTensorsValidation: + def test_rejects_non_pt_return_tensors(self, processor): + conversation = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"}, + ] + + with pytest.raises(ValueError, match=r"only supports.*return_tensors='pt'"): + processor.apply_chat_template( + conversation, tokenize=True, return_dict=True, return_tensors="np" + ) From 08441fed17968fc92c901f1ef71934942316f634 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 23 Feb 2026 23:39:53 +0700 Subject: [PATCH 1136/1405] fix: set allowed values for `adapter` config (#3415) --- src/axolotl/utils/schemas/peft.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/schemas/peft.py b/src/axolotl/utils/schemas/peft.py index a9ce1fbd69..a86de78226 100644 --- a/src/axolotl/utils/schemas/peft.py +++ b/src/axolotl/utils/schemas/peft.py @@ -1,6 +1,6 @@ """Pydantic models for PEFT-related configuration""" -from typing import Any +from typing import Any, Literal from pydantic import BaseModel, Field, field_validator, model_validator @@ -38,10 +38,10 @@ class LoraConfig(BaseModel): default=False, json_schema_extra={"description": "Use bitsandbytes 4 bit"} ) - adapter: str | None = Field( + adapter: Literal["lora", "qlora", "llama-adapter"] | None = Field( default=None, json_schema_extra={ - "description": "If you want to use 'lora' or 'qlora' or leave blank to train all parameters in original model" + "description": "If you want to use 'lora', 'qlora', or 'llama-adapter', or leave blank to train all parameters in original model" }, ) lora_model_dir: str | None = Field( From 68f1b7004cbea65d2f3968c70ac3af0f8e575611 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 24 Feb 2026 14:59:55 -0500 Subject: [PATCH 1137/1405] ScatterMoE LoRA support (#3410) * scattermoe lora support * fsdp, bf16, dim fixes * expert weights aren't needed in save for bwd since they are frozen * use sonicmoe optim options * update save model from upstream * fixes per code review feedback and add tests * revert removal of CP fix * misc fixes --- requirements.txt | 2 +- src/axolotl/core/trainers/base.py | 19 +- src/axolotl/integrations/kernels/args.py | 13 + .../integrations/kernels/libs/__init__.py | 0 .../kernels/libs/scattermoe_lora/__init__.py | 18 + .../libs/scattermoe_lora/kernels/__init__.py | 12 + .../libs/scattermoe_lora/kernels/lora_ops.py | 1731 +++++++++++++++++ .../libs/scattermoe_lora/kernels/ops.py | 645 ++++++ .../libs/scattermoe_lora/kernels/single.py | 98 + .../kernels/libs/scattermoe_lora/layers.py | 439 +++++ .../kernels/libs/scattermoe_lora/lora_ops.py | 99 + .../libs/scattermoe_lora/parallel_experts.py | 253 +++ .../scattermoe_lora/parallel_linear_lora.py | 480 +++++ src/axolotl/integrations/kernels/plugin.py | 15 +- src/axolotl/loaders/patch_manager.py | 2 +- src/axolotl/utils/data/lock.py | 26 +- tests/integrations/test_scattermoe_lora.py | 323 +++ 17 files changed, 4146 insertions(+), 29 deletions(-) create mode 100644 src/axolotl/integrations/kernels/libs/__init__.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/__init__.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/__init__.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/ops.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/single.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/lora_ops.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_experts.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py create mode 100644 tests/integrations/test_scattermoe_lora.py diff --git a/requirements.txt b/requirements.txt index 09b1f625b0..710e24d718 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ datasets==4.5.0 deepspeed>=0.18.3 trl==0.28.0 hf_xet==1.2.0 -kernels==0.11.5 +kernels==0.12.1 trackio>=0.16.1 typing-extensions>=4.15.0 diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 77e7b573b3..414abeb4d6 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -719,6 +719,8 @@ def _save(self, output_dir: Optional[str] = None, state_dict=None): output_dir = output_dir if output_dir is not None else self.args.output_dir os.makedirs(output_dir, exist_ok=True) LOG.info(f"Saving model checkpoint to {output_dir}") + + # fix for Context Parallel save if state_dict is None: state_dict = self.accelerator.get_state_dict(self.model) if state_dict is not None: @@ -726,6 +728,7 @@ def _save(self, output_dir: Optional[str] = None, state_dict=None): k: v.clone() if isinstance(v, torch.Tensor) else v for k, v in state_dict.items() } + supported_classes = ( (PreTrainedModel,) if not is_peft_available() @@ -736,6 +739,7 @@ def _save(self, output_dir: Optional[str] = None, state_dict=None): if not isinstance(self.model, supported_classes): if state_dict is None: state_dict = self.model.state_dict() + if isinstance( self.accelerator.unwrap_model(self.model, keep_torch_compile=False), supported_classes, @@ -745,6 +749,7 @@ def _save(self, output_dir: Optional[str] = None, state_dict=None): ).save_pretrained( output_dir, state_dict=state_dict, + is_main_process=self.accelerator.is_main_process, ) else: LOG.info( @@ -756,11 +761,7 @@ def _save(self, output_dir: Optional[str] = None, state_dict=None): metadata={"format": "pt"}, ) else: - self.model.save_pretrained( - output_dir, - state_dict=state_dict, - is_main_process=self.accelerator.is_main_process, - ) + self.model.save_pretrained(output_dir, state_dict=state_dict) if self.processing_class is not None: self.processing_class.save_pretrained(output_dir) @@ -772,11 +773,7 @@ def _save(self, output_dir: Optional[str] = None, state_dict=None): LOG.info( "Saving Trainer.data_collator.tokenizer by default as Trainer.processing_class is `None`" ) - save_jinja_files = True - if self.axolotl_cfg: - save_jinja_files = self.axolotl_cfg.tokenizer_save_jinja_files - self.data_collator.tokenizer.save_pretrained( - output_dir, save_jinja_files=save_jinja_files - ) + self.data_collator.tokenizer.save_pretrained(output_dir) + # Good practice: save your training arguments together with the trained model torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME)) diff --git a/src/axolotl/integrations/kernels/args.py b/src/axolotl/integrations/kernels/args.py index 66d6b6d531..e8cf7208a0 100644 --- a/src/axolotl/integrations/kernels/args.py +++ b/src/axolotl/integrations/kernels/args.py @@ -33,3 +33,16 @@ def check_experts_implementation(cls, data): data["experts_implementation"] = "eager" return data + + @model_validator(mode="before") + @classmethod + def disable_mlp_kernel_scattermoe(cls, data): + if data.get("use_scattermoe") is True: + if data.get("lora_mlp_kernel") is True: + LOG.warning( + "Disabling lora_mlp_kernel when using scattermoe due to compatibility issues." + ) + data["lora_mlp_kernel"] = False + data["mlp_kernel"] = False + + return data diff --git a/src/axolotl/integrations/kernels/libs/__init__.py b/src/axolotl/integrations/kernels/libs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/__init__.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/__init__.py new file mode 100644 index 0000000000..f5148634e6 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/__init__.py @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +from . import layers +from .lora_ops import ParallelExperts +from .parallel_experts import flatten_sort_count, parallel_linear +from .parallel_linear_lora import ScatterMoELoRA, parallel_linear_lora + +__all__ = [ + "layers", + "ParallelExperts", + "flatten_sort_count", + "parallel_linear", + "ScatterMoELoRA", + "parallel_linear_lora", + "lora_ops", +] diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/__init__.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/__init__.py new file mode 100644 index 0000000000..eb502db712 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/__init__.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Original work Copyright (c) Shawn Tan and ScatterMoE Contributors +# Adapted from https://github.com/shawntan/scattermoe +# See https://github.com/shawntan/scattermoe/blob/main/LICENSE +# +# Modifications and LoRA adaptation Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +from . import lora_ops, ops + +__all__ = ["ops", "lora_ops"] diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py new file mode 100644 index 0000000000..5d47c20408 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py @@ -0,0 +1,1731 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +Fused ScatterMoE + LoRA Triton Kernels +======================================= + +Provides fused forward and backward kernels for ScatterMoE with LoRA adapters. + +Forward: Y = X @ W + scaling * (X @ A^T) @ B^T +Backward (LoRA training, W frozen): + - dX = dY @ W^T + scaling * (dY @ B) @ A (input gradient) + - dA = scaling * (dY @ B)^T @ X (LoRA A gradient) + - dB = scaling * dY^T @ (X @ A^T) (LoRA B gradient) + +LoRA weight layout (from PEFT ParamWrapper): + - A: [r*E, K] -- for expert e, rows [e*r : (e+1)*r] give A_e of shape [r, K] + - B: [N, r*E] -- for expert e, cols [e*r : (e+1)*r] give B_e of shape [N, r] + +Key design decisions: + - The forward kernel fuses X@W and X@A^T in the same K-loop for data reuse on X, + then computes (X@A^T) @ B^T in the epilogue. + - The backward dA/dB kernel operates on grouped (expert-contiguous) data and + iterates over tokens per expert, accumulating gradients in registers. + - R (LoRA rank) is a tl.constexpr, allowing tl.arange(0, R). We pad R to a + power-of-2 for Triton tile compatibility; typical ranks (4, 8, 16, 32, 64) + already satisfy this. +""" + +from itertools import product +from typing import Optional + +import torch +import triton +import triton.language as tl + +# ============================================================================= +# Configuration +# ============================================================================= + +BLOCK_M = 128 +ALLOW_TF32 = True + + +def _next_power_of_2(n: int) -> int: + """Round up to next power of 2.""" + n -= 1 + n |= n >> 1 + n |= n >> 2 + n |= n >> 4 + n |= n >> 8 + n |= n >> 16 + return n + 1 + + +# Triton tl.dot requires minimum tile dimensions of 16 on modern GPUs. +MIN_TRITON_DOT_SIZE = 16 + + +def _block_r_for_rank(r: int) -> int: + """Compute BLOCK_R: next power-of-2 >= max(r, MIN_TRITON_DOT_SIZE).""" + return _next_power_of_2(max(r, MIN_TRITON_DOT_SIZE)) + + +# ============================================================================= +# Token Rounding: pad expert counts to BLOCK_M multiples +# ============================================================================= + + +def round_expert_counts( + sorted_expert_idxs: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + expert_offsets: torch.Tensor, + E: int, + block_m: int = BLOCK_M, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Pad each expert's token count to a multiple of block_m to eliminate + partial-tile waste in the backward kernel. + + Padding is done by duplicating the last valid token index for each expert. + The kernel's M_mask = M_idx < real_end_idx masks these padding entries, so + correctness is preserved (they contribute 0 to the accumulation via other=0.0). + + This only helps the backward dA/dB kernel where per-expert iteration is + explicit. The forward scatter2scatter kernel handles partial tiles via masking. + + Args: + sorted_expert_idxs: Expert assignments sorted [M*k] + sorted_scattered_idxs: Original indices sorted [M*k] + expert_offsets: Cumulative token counts per expert [E] + E: Number of experts + block_m: Block size for token dimension (default: BLOCK_M) + + Returns: + padded_expert_idxs: [M_padded] expert assignments with padding + padded_scattered_idxs: [M_padded] original indices with padding + padded_offsets: [E] cumulative padded counts (for kernel iteration range) + real_offsets: [E] original cumulative counts (for M_mask in kernel) + """ + device = sorted_expert_idxs.device + + # Compute per-expert counts + counts = torch.zeros(E, dtype=torch.int64, device=device) + prev = 0 + for e in range(E): + curr = expert_offsets[e].item() + counts[e] = curr - prev + prev = curr + + # Round up each count to multiple of block_m + padded_counts = ((counts + block_m - 1) // block_m) * block_m + # Experts with 0 tokens stay at 0 + padded_counts = torch.where( + counts > 0, padded_counts, torch.zeros_like(padded_counts) + ) + total_padded = padded_counts.sum().item() + + padded_expert_idxs = torch.empty( + total_padded, dtype=sorted_expert_idxs.dtype, device=device + ) + padded_scattered_idxs = torch.empty( + total_padded, dtype=sorted_scattered_idxs.dtype, device=device + ) + + src_offset = 0 + dst_offset = 0 + for e in range(E): + count = counts[e].item() + padded_count = padded_counts[e].item() + + if count > 0: + # Copy original tokens + padded_expert_idxs[dst_offset : dst_offset + count] = sorted_expert_idxs[ + src_offset : src_offset + count + ] + padded_scattered_idxs[dst_offset : dst_offset + count] = ( + sorted_scattered_idxs[src_offset : src_offset + count] + ) + + # Pad with last valid token (masked out by kernel via M_mask) + if padded_count > count: + padded_expert_idxs[dst_offset + count : dst_offset + padded_count] = ( + sorted_expert_idxs[src_offset + count - 1] + ) + padded_scattered_idxs[ + dst_offset + count : dst_offset + padded_count + ] = sorted_scattered_idxs[src_offset + count - 1] + + src_offset += count + dst_offset += padded_count + + # Padded offsets: cumulative padded counts (for iteration range in kernel) + padded_offsets = padded_counts.cumsum(-1).to(expert_offsets.dtype) + # Real offsets: original cumulative counts (for M_mask in kernel) + real_offsets = expert_offsets.clone() + + return padded_expert_idxs, padded_scattered_idxs, padded_offsets, real_offsets + + +# ============================================================================= +# Autotuning: SMEM estimation and config pruning +# ============================================================================= + +_SMEM_CAPACITY: int | None = None + + +def _get_smem_capacity() -> int: + """Get device shared memory capacity (bytes). Cached after first call.""" + global _SMEM_CAPACITY + if _SMEM_CAPACITY is None: + props = triton.runtime.driver.active.utils.get_device_properties( + torch.cuda.current_device() + ) + _SMEM_CAPACITY = props["max_shared_mem"] + return _SMEM_CAPACITY + + +def _estimate_smem_usage( + num_stages: int, BLOCK_M: int, BLOCK_N: int, BLOCK_K: int, dtype_bytes: int = 2 +) -> int: + """Estimate shared memory in bytes for a GEMM-style tile. + + Formula: stages * BLOCK_K * (BLOCK_M + BLOCK_N) + BLOCK_M * BLOCK_N + Multiply by dtype_bytes (2 for fp16/bf16). + """ + return ( + num_stages * BLOCK_K * (BLOCK_M + BLOCK_N) + BLOCK_M * BLOCK_N + ) * dtype_bytes + + +# Conservative margin (bytes) subtracted from SMEM capacity to account for +# estimation inaccuracies and kernel overhead (registers spilled to SMEM, etc.) +_SMEM_SLACK = 10_000 + + +# ============================================================================= +# Forward Kernel: scatter2scatter with fused LoRA +# ============================================================================= + + +@triton.jit +def _compute_expert_block_lora( + E_idx, + E_mask, + M_in_idx, + N_block, + N_mask, + # Base weight + X_ptr, + stride_xm, + stride_xk, + W_ptr, + stride_we, + stride_wk, + stride_wn, + # LoRA weights + A_ptr, + stride_ar, + stride_ak, # A: [r*E, K], stride_ar = stride for r*E dim, stride_ak = stride for K dim + B_ptr, + stride_bn, + stride_br, # B: [N, r*E], stride_bn = stride for N dim, stride_br = stride for r*E dim + # Dimensions + K, + ACTUAL_R: tl.constexpr, # True LoRA rank (for indexing into weight arrays) + acc, + no_k_mask, + BLOCK_M: tl.constexpr, + BLOCK_K: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_R: tl.constexpr, # Padded tile size >= max(ACTUAL_R, 16) + scaling, + allow_tf32: tl.constexpr, +): + """ + Compute Y_block = X_block @ W_e + scaling * (X_block @ A_e^T) @ B_e^T + + for tokens in this M-block assigned to expert E_idx. + + ACTUAL_R is the true LoRA rank used for indexing into A[e*r:(e+1)*r, :]. + BLOCK_R >= ACTUAL_R is the padded tile dimension (must be >= 16 for tl.dot). + When BLOCK_R > ACTUAL_R, loads are masked on the R dimension. + """ + K_block = tl.arange(0, BLOCK_K) + R_block = tl.arange(0, BLOCK_R) + R_mask = R_block < ACTUAL_R # Mask for padding when BLOCK_R > ACTUAL_R + + # Base weight pointers: W[E_idx, :, :] is [K, N], load [BLOCK_K, BLOCK_N] + X_blk_ptrs = X_ptr + M_in_idx[:, None] * stride_xm + K_block[None, :] * stride_xk + W_blk_ptrs = ( + W_ptr + + E_idx * stride_we + + K_block[:, None] * stride_wk + + N_block[None, :] * stride_wn + ) + + # LoRA A pointers: A[e*ACTUAL_R:(e+1)*ACTUAL_R, :] for expert e, shape [r, K] + A_expert_offset = E_idx * ACTUAL_R + A_blk_ptrs = ( + A_ptr + + (A_expert_offset + R_block)[:, None] * stride_ar + + K_block[None, :] * stride_ak + ) + + iters = tl.cdiv(K, BLOCK_K) + + # Accumulator for X @ A^T: [BLOCK_M, BLOCK_R] + xa_acc = tl.zeros((BLOCK_M, BLOCK_R), dtype=tl.float32) + + # Determine the input element type for consistent casting. + # Masked tl.load with other=0.0 can upcast bf16->fp32 in some Triton versions, + # causing dtype mismatches in tl.dot. We cast all tiles to the same type. + INPUT_DTYPE = X_ptr.dtype.element_ty + + for i in range(iters): + if no_k_mask: + x = tl.load(X_blk_ptrs, mask=E_mask[:, None], other=0.0).to(INPUT_DTYPE) + w = tl.load(W_blk_ptrs, mask=N_mask[None, :], other=0.0).to(INPUT_DTYPE) + a = tl.load(A_blk_ptrs, mask=R_mask[:, None], other=0.0).to(INPUT_DTYPE) + else: + K_mask = (i * BLOCK_K + K_block) < K + x = tl.load( + X_blk_ptrs, mask=E_mask[:, None] & K_mask[None, :], other=0.0 + ).to(INPUT_DTYPE) + w = tl.load( + W_blk_ptrs, mask=K_mask[:, None] & N_mask[None, :], other=0.0 + ).to(INPUT_DTYPE) + a = tl.load( + A_blk_ptrs, mask=R_mask[:, None] & K_mask[None, :], other=0.0 + ).to(INPUT_DTYPE) + + # Base: acc += X @ W ([M, K] @ [K, N] -> [M, N]) + acc += tl.dot(x, w, allow_tf32=allow_tf32).to(tl.float32) + + # LoRA: xa_acc += X @ A^T ([M, K] @ [K, R] -> [M, R]) + xa_acc += tl.dot(x, tl.trans(a), allow_tf32=allow_tf32).to(tl.float32) + + X_blk_ptrs += BLOCK_K * stride_xk + W_blk_ptrs += BLOCK_K * stride_wk + A_blk_ptrs += BLOCK_K * stride_ak + + # Epilogue: load B[e] and compute (X @ A^T) @ B^T + # B[e] is B[:, e*ACTUAL_R:(e+1)*ACTUAL_R], shape [N, r]. Load [BLOCK_N, BLOCK_R]. + B_expert_offset = E_idx * ACTUAL_R + B_blk_ptrs = ( + B_ptr + + N_block[:, None] * stride_bn + + (B_expert_offset + R_block)[None, :] * stride_br + ) + b = tl.load( + B_blk_ptrs, mask=N_mask[:, None] & R_mask[None, :], other=0.0 + ) # [BLOCK_N, BLOCK_R] + + # Cast xa_acc and b to same dtype for tl.dot (required when input is bf16/fp16) + # Both operands must match; cast to float32 (accumulator type) for precision. + b_f32 = b.to(tl.float32) + + # (X @ A^T) @ B^T: [M, R] @ [R, N] -> [M, N] + lora_out = tl.dot(xa_acc, tl.trans(b_f32), allow_tf32=allow_tf32) + + acc += scaling * lora_out + return acc + + +def _scatter2scatter_lora_configs(): + """Generate forward kernel autotune configs. + + Search space includes smaller tile sizes and fewer pipeline stages to + support GPUs with limited shared memory (e.g. ~99KB on some GPUs). + + Search space: + BLOCK_N: {32, 64, 128, 256} + BLOCK_K: {32, 64, 128} + num_warps: {4, 8} + num_stages: {3, 4, 5} + + BLOCK_M is fixed at 128 (module-level constant, not autotuned in the + scatter2scatter pattern). + """ + configs = [] + for block_n, block_k, warps, stages in product( + [32, 64, 128, 256], # BLOCK_N + [32, 64, 128], # BLOCK_K + [4, 8], # num_warps + [3, 4, 5], # num_stages + ): + configs.append( + triton.Config( + {"BLOCK_N": block_n, "BLOCK_K": block_k}, + num_stages=stages, + num_warps=warps, + ) + ) + return configs + + +def _prune_fwd_configs(configs, named_args, **kwargs): + """Prune forward configs based on SMEM capacity. + + The forward kernel inner loop loads three tiles per pipeline stage: + X[BLOCK_M, BLOCK_K], W[BLOCK_K, BLOCK_N], A[BLOCK_R, BLOCK_K]. + The base estimate only accounts for X and W. We add: + - A tile [BLOCK_R, BLOCK_K] per pipeline stage (loaded in the inner loop) + - B tile [BLOCK_N, BLOCK_R] loaded once in the epilogue + - Extra headroom for compiler overhead (register spills, metadata) + """ + smem_cap = _get_smem_capacity() + + # Get BLOCK_R from named_args if available, else assume worst case + block_r = named_args.get("BLOCK_R", 64) + + scored = [] + for config in configs: + block_n = config.kwargs["BLOCK_N"] + block_k = config.kwargs["BLOCK_K"] + # Base: stages * BLOCK_K * (BLOCK_M + BLOCK_N) + BLOCK_M * BLOCK_N + smem_base = _estimate_smem_usage(config.num_stages, BLOCK_M, block_n, block_k) + # A tile [BLOCK_R, BLOCK_K] loaded per stage in the inner loop + smem_lora_loop = config.num_stages * block_r * block_k * 2 + # B tile [BLOCK_N, BLOCK_R] loaded once in epilogue + smem_lora_epilogue = block_n * block_r * 2 + smem = smem_base + smem_lora_loop + smem_lora_epilogue + scored.append((smem, config)) + + pruned = [c for s, c in scored if s <= smem_cap - _SMEM_SLACK] + if pruned: + return pruned + # All configs exceed SMEM — return the one with smallest estimated usage + scored.sort(key=lambda x: x[0]) + return [scored[0][1]] + + +@triton.autotune( + configs=_scatter2scatter_lora_configs(), + key=["M", "N", "K"], + prune_configs_by={"early_config_prune": _prune_fwd_configs}, +) +@triton.heuristics( + { + "NO_K_MASK": lambda args: (args["K"] % args["BLOCK_K"]) == 0, + "NO_N_MASK": lambda args: (args["N"] % args["BLOCK_N"]) == 0, + } +) +@triton.jit +def _scatter2scatter_lora( + # Input/Output + X_ptr, + stride_xm: tl.constexpr, + stride_xk: tl.constexpr, + W_ptr, + stride_we, + stride_wk: tl.constexpr, + stride_wn: tl.constexpr, + Y_ptr, + stride_ym: tl.constexpr, + stride_yn: tl.constexpr, + # Bias + Bias_ptr, + stride_bias_e: tl.constexpr, + stride_bias_n: tl.constexpr, + # LoRA weights + LA_ptr, + stride_la_r, + stride_la_k, # A: [r*E, K] + LB_ptr, + stride_lb_n, + stride_lb_r, # B: [N, r*E] + # Routing + grouped_idx_ptr, + expert_idxs_ptr, + # Dimensions + FAN_OUT: tl.constexpr, + M, + K: tl.constexpr, + N: tl.constexpr, + E: tl.constexpr, + ACTUAL_R: tl.constexpr, # True LoRA rank (for weight indexing) + # Block sizes + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, + BLOCK_R: tl.constexpr, # Padded tile size >= max(ACTUAL_R, 16) + # Config + ACC_TYPE: tl.constexpr, + scaling, + allow_tf32: tl.constexpr, + x_grouped: tl.constexpr, + y_grouped: tl.constexpr, + NO_K_MASK: tl.constexpr, + NO_N_MASK: tl.constexpr, +): + """ + Fused scatter2scatter with LoRA: Y = X @ W + scaling * (X @ A^T) @ B^T + bias + """ + pid = tl.program_id(axis=0) + + N_BLOCK_COUNT = tl.cdiv(N, BLOCK_N) + M_block_id = pid // N_BLOCK_COUNT + N_block_id = pid % N_BLOCK_COUNT + + M_block = M_block_id * BLOCK_M + tl.arange(0, BLOCK_M) + N_block = N_block_id * BLOCK_N + tl.arange(0, BLOCK_N) + N_mask = N_block < N + M_boundary_mask = M_block < (FAN_OUT * M) + + E_idxs = tl.load(expert_idxs_ptr + M_block, mask=M_boundary_mask, other=E) + + no_k_mask = NO_K_MASK + + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=ACC_TYPE) + + E_first_idx = tl.min(E_idxs) + E_last_idx = tl.minimum(tl.max(E_idxs), E - 1) + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) + + for E_idx in range(E_first_idx, E_last_idx + 1): + E_mask = E_idxs == E_idx + if x_grouped: + M_in_idx = M_block + else: + M_in_idx = M_idx // FAN_OUT + + acc = _compute_expert_block_lora( + E_idx, + E_mask, + M_in_idx, + N_block, + N_mask, + X_ptr, + stride_xm, + stride_xk, + W_ptr, + stride_we, + stride_wk, + stride_wn, + LA_ptr, + stride_la_r, + stride_la_k, + LB_ptr, + stride_lb_n, + stride_lb_r, + K, + ACTUAL_R, + acc, + no_k_mask, + BLOCK_M, + BLOCK_K, + BLOCK_N, + BLOCK_R, + scaling, + allow_tf32=allow_tf32, + ) + + # Add bias if present + if Bias_ptr is not None: + B_blk_ptrs = ( + Bias_ptr + + E_idxs[:, None] * stride_bias_e + + N_block[None, :] * stride_bias_n + ) + acc += tl.load(B_blk_ptrs, mask=M_boundary_mask[:, None] & N_mask[None, :]) + + # Store output + if y_grouped: + M_out_idx = M_block + else: + M_out_idx = M_idx + Y_blk_ptrs = Y_ptr + (M_out_idx[:, None] * stride_ym + N_block[None, :] * stride_yn) + tl.store(Y_blk_ptrs, acc, mask=M_boundary_mask[:, None] & N_mask[None, :]) + + +def scatter2scatter_lora( + X: torch.Tensor, + W: torch.Tensor, + sorted_expert_idxs: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + k: int, + lora_A: torch.Tensor, + lora_B: torch.Tensor, + scaling: float, + b: Optional[torch.Tensor] = None, + x_grouped: bool = False, + y_grouped: bool = False, + out: Optional[torch.Tensor] = None, +) -> torch.Tensor: + """ + Fused scatter2scatter with LoRA: Y[i] = X[i] @ W[e] + scaling * (X[i] @ A[e]^T) @ B[e]^T + b[e] + + Args: + X: Input [M, K] or [M*k, K] if x_grouped + W: Expert weights [E, K, N] + sorted_expert_idxs: Expert assignments sorted [M*k] + sorted_scattered_idxs: Original indices sorted [M*k] + k: Fan-out (top-k) + lora_A: LoRA A weights [r*E, K] + lora_B: LoRA B weights [N, r*E] + scaling: LoRA scaling factor (alpha/r) + b: Optional bias [E, N] + x_grouped: Input pre-grouped by expert + y_grouped: Keep output grouped + out: Optional pre-allocated output buffer + + Returns: + Y: Output [M*k, N] + """ + assert sorted_scattered_idxs.size(0) == sorted_expert_idxs.size(0) + assert sorted_scattered_idxs.size(0) == X.size(0) * k + + E = W.size(0) + K = W.size(1) + N = W.size(2) + R = lora_A.size(0) // E + + # Pad R to power of 2 for Triton tile size + BLOCK_R = _block_r_for_rank(R) + + L_scattered = sorted_expert_idxs.size(0) + + if out is None: + output = torch.empty((L_scattered, N), device=X.device, dtype=X.dtype) + else: + assert out.size(0) == L_scattered and out.size(1) == N + output = out + + def grid(META): + return ( + triton.cdiv(L_scattered, META["BLOCK_M"]) * triton.cdiv(N, META["BLOCK_N"]), + ) + + if b is None: + stride_be = stride_bn = 0 + b_ptr = None + else: + stride_be, stride_bn = b.stride() + b_ptr = b + + _scatter2scatter_lora[grid]( + X, + X.stride(0), + X.stride(1), + W, + W.stride(0), + W.stride(1), + W.stride(2), + output, + output.stride(0), + output.stride(1), + b_ptr, + stride_be, + stride_bn, + # A: [r*E, K] -> stride(0) is r*E dim stride, stride(1) is K dim stride + lora_A, + lora_A.stride(0), + lora_A.stride(1), + # B: [N, r*E] -> stride(0) is N dim stride, stride(1) is r*E dim stride + lora_B, + lora_B.stride(0), + lora_B.stride(1), + sorted_scattered_idxs, + sorted_expert_idxs, + FAN_OUT=k, + M=X.size(0), + K=K, + N=N, + E=E, + ACTUAL_R=R, # True LoRA rank for weight indexing + BLOCK_M=BLOCK_M, + BLOCK_R=BLOCK_R, # Padded tile size >= max(R, 16) + ACC_TYPE=tl.float32, + scaling=scaling, + allow_tf32=ALLOW_TF32, + x_grouped=x_grouped, + y_grouped=y_grouped, + ) + + return output + + +# ============================================================================= +# Backward Kernel: Fused dX = dY @ W^T + scaling * (dY @ B) @ A +# ============================================================================= + + +@triton.jit +def _compute_expert_block_lora_dX( + E_idx, + E_mask, + M_in_idx, + K_block, + K_mask, + # Input: DY (gradient w.r.t. output) + DY_ptr, + stride_dym, + stride_dyn, + # Base weight W^T: we load W[e] as [K, N] and index as W^T[e] = [N, K] + W_ptr, + stride_we, + stride_wk, + stride_wn, + # LoRA weights + A_ptr, + stride_ar, + stride_ak, # A: [r*E, K] + B_ptr, + stride_bn, + stride_br, # B: [N, r*E] + # Dimensions + N, + ACTUAL_R: tl.constexpr, + acc, + no_n_mask, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, + BLOCK_R: tl.constexpr, + scaling, + allow_tf32: tl.constexpr, +): + """ + Compute dX_block = DY_block @ W_e^T + scaling * (DY_block @ B_e) @ A_e + + for tokens in this M-block assigned to expert E_idx. + + Inner loop over N dimension (reduction dim for dY @ W^T and dY @ B). + Output dimension is K. + Epilogue computes (dY @ B) @ A. + + Transpose mapping from forward: + Forward: X@W (K-loop), X@A^T (K-loop), (X@A^T)@B^T (epilogue) + Backward: DY@W^T (N-loop), DY@B (N-loop), (DY@B)@A (epilogue) + """ + N_block = tl.arange(0, BLOCK_N) + R_block = tl.arange(0, BLOCK_R) + R_mask = R_block < ACTUAL_R + + # DY pointers: DY is [M_total, N], load [BLOCK_M, BLOCK_N] + DY_blk_ptrs = ( + DY_ptr + M_in_idx[:, None] * stride_dym + N_block[None, :] * stride_dyn + ) + + # W^T pointers: W[e] is [K, N], W^T[e] is [N, K]. We load W^T as [BLOCK_N, BLOCK_K]. + # W stored as [E, K, N], so W^T[e][n, k] = W[e][k, n] = W_ptr + e*stride_we + k*stride_wk + n*stride_wn + # As [BLOCK_N, BLOCK_K] tile: row=n, col=k + WT_blk_ptrs = ( + W_ptr + + E_idx * stride_we + + N_block[:, None] * stride_wn # row = n dimension + + K_block[None, :] * stride_wk + ) # col = k dimension + + # B pointers: B[e] is B[:, e*R:(e+1)*R], shape [N, R]. Load [BLOCK_N, BLOCK_R]. + B_expert_offset = E_idx * ACTUAL_R + B_blk_ptrs = ( + B_ptr + + N_block[:, None] * stride_bn + + (B_expert_offset + R_block)[None, :] * stride_br + ) + + iters = tl.cdiv(N, BLOCK_N) + + # Accumulator for DY @ B: [BLOCK_M, BLOCK_R] + dy_b_acc = tl.zeros((BLOCK_M, BLOCK_R), dtype=tl.float32) + + # Determine the input element type for consistent casting. + INPUT_DTYPE = DY_ptr.dtype.element_ty + + for i in range(iters): + if no_n_mask: + dy = tl.load(DY_blk_ptrs, mask=E_mask[:, None], other=0.0).to(INPUT_DTYPE) + wt = tl.load(WT_blk_ptrs, mask=K_mask[None, :], other=0.0).to(INPUT_DTYPE) + b = tl.load(B_blk_ptrs, mask=R_mask[None, :], other=0.0).to(INPUT_DTYPE) + else: + N_mask_iter = (i * BLOCK_N + N_block) < N + dy = tl.load( + DY_blk_ptrs, mask=E_mask[:, None] & N_mask_iter[None, :], other=0.0 + ).to(INPUT_DTYPE) + wt = tl.load( + WT_blk_ptrs, mask=N_mask_iter[:, None] & K_mask[None, :], other=0.0 + ).to(INPUT_DTYPE) + b = tl.load( + B_blk_ptrs, mask=N_mask_iter[:, None] & R_mask[None, :], other=0.0 + ).to(INPUT_DTYPE) + + # Base: acc += DY @ W^T ([M, N] @ [N, K] -> [M, K]) + acc += tl.dot(dy, wt, allow_tf32=allow_tf32).to(tl.float32) + + # LoRA: dy_b_acc += DY @ B ([M, N] @ [N, R] -> [M, R]) + dy_b_acc += tl.dot(dy, b, allow_tf32=allow_tf32).to(tl.float32) + + DY_blk_ptrs += BLOCK_N * stride_dyn + WT_blk_ptrs += BLOCK_N * stride_wn + B_blk_ptrs += BLOCK_N * stride_bn + + # Epilogue: load A[e] and compute (DY @ B) @ A + # A[e] is A[e*R:(e+1)*R, :], shape [R, K]. Load [BLOCK_R, BLOCK_K]. + A_expert_offset = E_idx * ACTUAL_R + A_blk_ptrs = ( + A_ptr + + (A_expert_offset + R_block)[:, None] * stride_ar + + K_block[None, :] * stride_ak + ) + a_e = tl.load(A_blk_ptrs, mask=R_mask[:, None] & K_mask[None, :], other=0.0) + + # Cast to float32 for precision + a_f32 = a_e.to(tl.float32) + + # (DY @ B) @ A: [M, R] @ [R, K] -> [M, K] + lora_dx = tl.dot(dy_b_acc, a_f32, allow_tf32=allow_tf32) + + acc += scaling * lora_dx + return acc + + +def _scatter2scatter_lora_dX_configs(): + """Generate backward dX kernel autotune configs. + + The inner loop is over N (not K as in forward). The output dimension is K. + So BLOCK_K tiles the output and BLOCK_N tiles the reduction. + + Search space includes smaller tile sizes and fewer pipeline stages to + support GPUs with limited shared memory (e.g. ~99KB on some GPUs). + + Search space: + BLOCK_K: {32, 64, 128, 256} (output tile) + BLOCK_N: {32, 64, 128, 256} (reduction tile) + num_warps: {4, 8} + num_stages: {3, 4, 5} + """ + configs = [] + for block_k, block_n, warps, stages in product( + [32, 64, 128, 256], # BLOCK_K (output dimension) + [32, 64, 128, 256], # BLOCK_N (reduction dimension) + [4, 8], # num_warps + [3, 4, 5], # num_stages + ): + configs.append( + triton.Config( + {"BLOCK_K": block_k, "BLOCK_N": block_n}, + num_stages=stages, + num_warps=warps, + ) + ) + return configs + + +def _prune_dX_configs(configs, named_args, **kwargs): + """Prune backward dX configs based on SMEM capacity. + + The dX kernel inner loop loads three tiles per pipeline stage: + DY[BLOCK_M, BLOCK_N], W^T[BLOCK_N, BLOCK_K], B[BLOCK_N, BLOCK_R]. + The base estimate only accounts for DY and W^T. We add: + - B tile [BLOCK_N, BLOCK_R] per pipeline stage (loaded in the inner loop) + - A tile [BLOCK_R, BLOCK_K] loaded once in the epilogue + - Extra headroom for compiler overhead (register spills, metadata) + """ + smem_cap = _get_smem_capacity() + + # Get BLOCK_R from named_args if available, else assume worst case + block_r = named_args.get("BLOCK_R", 64) + + scored = [] + for config in configs: + block_k = config.kwargs["BLOCK_K"] + block_n = config.kwargs["BLOCK_N"] + # Base: stages * BLOCK_N * (BLOCK_M + BLOCK_K) + BLOCK_M * BLOCK_K + smem_base = _estimate_smem_usage(config.num_stages, BLOCK_M, block_k, block_n) + # B tile [BLOCK_N, BLOCK_R] loaded per stage in the inner loop + smem_lora_loop = config.num_stages * block_n * block_r * 2 + # A tile [BLOCK_R, BLOCK_K] loaded once in epilogue + smem_lora_epilogue = block_r * block_k * 2 + smem = smem_base + smem_lora_loop + smem_lora_epilogue + scored.append((smem, config)) + + pruned = [c for s, c in scored if s <= smem_cap - _SMEM_SLACK] + if pruned: + return pruned + # All configs exceed SMEM — return the one with smallest estimated usage + scored.sort(key=lambda x: x[0]) + return [scored[0][1]] + + +@triton.autotune( + configs=_scatter2scatter_lora_dX_configs(), + key=["M", "N", "K"], + prune_configs_by={"early_config_prune": _prune_dX_configs}, +) +@triton.heuristics( + { + "NO_K_MASK": lambda args: (args["K"] % args["BLOCK_K"]) == 0, + "NO_N_MASK": lambda args: (args["N"] % args["BLOCK_N"]) == 0, + } +) +@triton.jit +def _scatter2scatter_lora_dX( + # Input: DY (gradient w.r.t. output, grouped) + DY_ptr, + stride_dym: tl.constexpr, + stride_dyn: tl.constexpr, + # Base weight: W [E, K, N] (we compute DY @ W^T) + W_ptr, + stride_we, + stride_wk: tl.constexpr, + stride_wn: tl.constexpr, + # Output: dX + DX_ptr, + stride_dxm: tl.constexpr, + stride_dxk: tl.constexpr, + # LoRA weights + LA_ptr, + stride_la_r, + stride_la_k, # A: [r*E, K] + LB_ptr, + stride_lb_n, + stride_lb_r, # B: [N, r*E] + # Routing + grouped_idx_ptr, + expert_idxs_ptr, + # Dimensions + FAN_OUT: tl.constexpr, + M, + K: tl.constexpr, + N: tl.constexpr, + E: tl.constexpr, + ACTUAL_R: tl.constexpr, + # Block sizes + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, + BLOCK_R: tl.constexpr, + # Config + ACC_TYPE: tl.constexpr, + scaling, + allow_tf32: tl.constexpr, + dy_grouped: tl.constexpr, + dx_grouped: tl.constexpr, + NO_K_MASK: tl.constexpr, + NO_N_MASK: tl.constexpr, +): + """ + Fused backward dX = DY @ W^T + scaling * (DY @ B) @ A + + DY is in expert-grouped order (x_grouped=True). + dX is output in ungrouped or grouped order based on dx_grouped. + + Grid: (cdiv(M_total, BLOCK_M) * cdiv(K, BLOCK_K),) + """ + pid = tl.program_id(axis=0) + + K_BLOCK_COUNT = tl.cdiv(K, BLOCK_K) + M_block_id = pid // K_BLOCK_COUNT + K_block_id = pid % K_BLOCK_COUNT + + M_block = M_block_id * BLOCK_M + tl.arange(0, BLOCK_M) + K_block = K_block_id * BLOCK_K + tl.arange(0, BLOCK_K) + K_mask = K_block < K + M_boundary_mask = M_block < (FAN_OUT * M) + + E_idxs = tl.load(expert_idxs_ptr + M_block, mask=M_boundary_mask, other=E) + + no_n_mask = NO_N_MASK + + acc = tl.zeros((BLOCK_M, BLOCK_K), dtype=ACC_TYPE) + + E_first_idx = tl.min(E_idxs) + E_last_idx = tl.minimum(tl.max(E_idxs), E - 1) + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) + + for E_idx in range(E_first_idx, E_last_idx + 1): + E_mask = E_idxs == E_idx + if dy_grouped: + M_in_idx = M_block + else: + M_in_idx = M_idx // FAN_OUT + + acc = _compute_expert_block_lora_dX( + E_idx, + E_mask, + M_in_idx, + K_block, + K_mask, + DY_ptr, + stride_dym, + stride_dyn, + W_ptr, + stride_we, + stride_wk, + stride_wn, + LA_ptr, + stride_la_r, + stride_la_k, + LB_ptr, + stride_lb_n, + stride_lb_r, + N, + ACTUAL_R, + acc, + no_n_mask, + BLOCK_M, + BLOCK_N, + BLOCK_K, + BLOCK_R, + scaling, + allow_tf32=allow_tf32, + ) + + # Store output + if dx_grouped: + M_out_idx = M_block + else: + M_out_idx = M_idx + DX_blk_ptrs = DX_ptr + ( + M_out_idx[:, None] * stride_dxm + K_block[None, :] * stride_dxk + ) + tl.store(DX_blk_ptrs, acc, mask=M_boundary_mask[:, None] & K_mask[None, :]) + + +def scatter2scatter_lora_dX( + DY: torch.Tensor, + W: torch.Tensor, + sorted_expert_idxs: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + k: int, + lora_A: torch.Tensor, + lora_B: torch.Tensor, + scaling: float, + dy_grouped: bool = True, + dx_grouped: bool = False, + out: Optional[torch.Tensor] = None, +) -> torch.Tensor: + """ + Fused backward dX = DY @ W^T + scaling * (DY @ B) @ A + + Replaces the separate: + 1. base_ops.scatter2scatter(DY, W^T, x_grouped=True, ...) + 2. _compute_lora_input_grad(DY, A, B, ...) + + Args: + DY: Gradient w.r.t. output [M*k, N] (grouped by expert) + W: Expert weights [E, K, N] (NOT transposed — kernel handles W^T internally) + sorted_expert_idxs: Expert assignments sorted [M*k] + sorted_scattered_idxs: Original indices sorted [M*k] + k: Fan-out (top-k) + lora_A: LoRA A weights [r*E, K] + lora_B: LoRA B weights [N, r*E] + scaling: LoRA scaling factor + dy_grouped: Whether DY is in grouped (expert-sorted) order (default True) + dx_grouped: Whether to output dX in grouped order (default False) + out: Optional pre-allocated output buffer + + Returns: + dX: Input gradient [M*k, K] + """ + assert sorted_scattered_idxs.size(0) == sorted_expert_idxs.size(0) + + E = W.size(0) + K = W.size(1) + N = W.size(2) + R = lora_A.size(0) // E + + BLOCK_R = _block_r_for_rank(R) + + L_scattered = sorted_expert_idxs.size(0) + + # M for the kernel is DY.size(0) when dy_grouped, else the original M + if dy_grouped: + M = DY.size(0) + fan_out = 1 # DY is already expanded + else: + M = DY.size(0) + fan_out = k + + if out is None: + output = torch.empty((L_scattered, K), device=DY.device, dtype=DY.dtype) + else: + assert out.size(0) == L_scattered and out.size(1) == K + output = out + + def grid(META): + return ( + triton.cdiv(L_scattered, META["BLOCK_M"]) * triton.cdiv(K, META["BLOCK_K"]), + ) + + _scatter2scatter_lora_dX[grid]( + DY, + DY.stride(0), + DY.stride(1), + W, + W.stride(0), + W.stride(1), + W.stride(2), + output, + output.stride(0), + output.stride(1), + lora_A, + lora_A.stride(0), + lora_A.stride(1), + lora_B, + lora_B.stride(0), + lora_B.stride(1), + sorted_scattered_idxs, + sorted_expert_idxs, + FAN_OUT=fan_out, + M=M, + K=K, + N=N, + E=E, + ACTUAL_R=R, + BLOCK_M=BLOCK_M, + BLOCK_R=BLOCK_R, + ACC_TYPE=tl.float32, + scaling=scaling, + allow_tf32=ALLOW_TF32, + dy_grouped=dy_grouped, + dx_grouped=dx_grouped, + ) + + return output + + +# ============================================================================= +# Backward Kernel: LoRA gradient computation (dA, dB) +# ============================================================================= + + +def _group_bwd_lora_configs(): + """Generate backward (dA/dB) kernel autotune configs. + + Search space includes smaller tile sizes and fewer pipeline stages to + support GPUs with limited shared memory (e.g. ~99KB on some GPUs). + + Search space: + BLOCK_M: {32, 64, 128, 256} (token-loop tile) + BLOCK_K: {32, 64, 128, 256} + BLOCK_N: {32, 64, 128, 256} + num_warps: {4, 8} + num_stages: {3, 4, 5} + + The backward kernel also uses BLOCK_R (from LoRA rank), but that is + determined by the rank and not autotunable. + """ + configs = [] + for block_m, block_k, block_n, warps, stages in product( + [32, 64, 128, 256], # BLOCK_M + [32, 64, 128, 256], # BLOCK_K + [32, 64, 128, 256], # BLOCK_N + [4, 8], # num_warps + [3, 4, 5], # num_stages + ): + configs.append( + triton.Config( + {"BLOCK_M": block_m, "BLOCK_K": block_k, "BLOCK_N": block_n}, + num_stages=stages, + num_warps=warps, + ) + ) + return configs + + +def _prune_bwd_lora_configs(configs, named_args, **kwargs): + """Prune backward configs based on SMEM capacity. + + The backward kernel loads X[BLOCK_M, BLOCK_K] and DY[BLOCK_M, BLOCK_N] + in the inner loop, plus holds A[BLOCK_R, BLOCK_K] and B[BLOCK_N, BLOCK_R] + for the full expert. We estimate SMEM based on the dominant terms. + """ + smem_cap = _get_smem_capacity() + block_r = named_args.get("BLOCK_R", 64) + + scored = [] + for config in configs: + block_m = config.kwargs["BLOCK_M"] + block_k = config.kwargs["BLOCK_K"] + block_n = config.kwargs["BLOCK_N"] + # Inner loop loads X[M,K] and DY[M,N], pipeline over M iterations + smem_base = _estimate_smem_usage(config.num_stages, block_m, block_n, block_k) + # A[BLOCK_R, BLOCK_K] and B[BLOCK_N, BLOCK_R] held for the full expert + smem_lora = (block_r * block_k + block_n * block_r) * 2 + smem = smem_base + smem_lora + scored.append((smem, config)) + + pruned = [c for s, c in scored if s <= smem_cap - _SMEM_SLACK] + if pruned: + return pruned + # All configs exceed SMEM — return the one with smallest estimated usage + scored.sort(key=lambda x: x[0]) + return [scored[0][1]] + + +@triton.autotune( + configs=_group_bwd_lora_configs(), + key=["M", "N", "K"], + prune_configs_by={"early_config_prune": _prune_bwd_lora_configs}, + reset_to_zero=["DLA_ptr", "DLB_ptr"], +) +@triton.heuristics( + { + "NO_K_MASK": lambda args: (args["K"] % args["BLOCK_K"]) == 0, + "NO_N_MASK": lambda args: (args["N"] % args["BLOCK_N"]) == 0, + } +) +@triton.jit +def _group_bwd_lora( + # Inputs + DY_ptr, + stride_dym, + stride_dyn, + X_ptr, + stride_xm, + stride_xk, + # LoRA weights (needed for cross-terms) + LA_ptr, + stride_la_r, + stride_la_k, # A: [r*E, K] + LB_ptr, + stride_lb_n, + stride_lb_r, # B: [N, r*E] + # Gradient outputs + DLA_ptr, + stride_dla_r, + stride_dla_k, + DLB_ptr, + stride_dlb_n, + stride_dlb_r, + # Expert offsets + expert_offsets_ptr, + # Dimensions + M, + K: tl.constexpr, + N: tl.constexpr, + ACTUAL_R: tl.constexpr, # True LoRA rank (for weight indexing) + BLOCK_R: tl.constexpr, # Padded tile size >= max(ACTUAL_R, 16) + scaling, + # Block sizes + BLOCK_M: tl.constexpr, + BLOCK_K: tl.constexpr, + BLOCK_N: tl.constexpr, + ACC_TYPE: tl.constexpr, + allow_tf32: tl.constexpr, + NO_K_MASK: tl.constexpr, + NO_N_MASK: tl.constexpr, +): + """ + Compute LoRA gradients for each expert on grouped data. + + Grid: (E * cdiv(K, BLOCK_K), cdiv(N, BLOCK_N)) + + For expert e: + dA[e] = scaling * (dY @ B[e])^T @ X -> [r, K], accumulate over M tokens + dB[e] = scaling * dY^T @ (X @ A[e]^T) -> [N, r], accumulate over M tokens + + ACTUAL_R is the true LoRA rank. BLOCK_R >= ACTUAL_R is padded for tl.dot min size. + """ + pid0 = tl.program_id(axis=0) + pid1 = tl.program_id(axis=1) + + K_BLOCK_COUNT = tl.cdiv(K, BLOCK_K) + E_idx = pid0 // K_BLOCK_COUNT + K_block_id = pid0 % K_BLOCK_COUNT + N_block_id = pid1 + + # Get expert's token range from cumulative offsets + if E_idx == 0: + start_idx = 0 + else: + start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int32) + end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int32) + num_tokens = end_idx - start_idx + + if num_tokens > 0: + M_block = tl.arange(0, BLOCK_M) + K_block = K_block_id * BLOCK_K + tl.arange(0, BLOCK_K) + K_mask = K_block < K + N_block = N_block_id * BLOCK_N + tl.arange(0, BLOCK_N) + N_mask = N_block < N + R_block = tl.arange(0, BLOCK_R) + R_mask = R_block < ACTUAL_R # Mask for padding + + lora_offset = E_idx * ACTUAL_R + + # Determine input element type for consistent casting. + INPUT_DTYPE = X_ptr.dtype.element_ty + + # Load B[e]: [BLOCK_N, BLOCK_R] (masked on R and N, other=0 for padding) + B_blk_ptrs = ( + LB_ptr + + N_block[:, None] * stride_lb_n + + (lora_offset + R_block)[None, :] * stride_lb_r + ) + b_e = tl.load(B_blk_ptrs, mask=N_mask[:, None] & R_mask[None, :], other=0.0).to( + INPUT_DTYPE + ) + + # Load A[e]: [BLOCK_R, BLOCK_K] (masked on R and K, other=0 for padding) + A_blk_ptrs = ( + LA_ptr + + (lora_offset + R_block)[:, None] * stride_la_r + + K_block[None, :] * stride_la_k + ) + a_e = tl.load(A_blk_ptrs, mask=R_mask[:, None] & K_mask[None, :], other=0.0).to( + INPUT_DTYPE + ) + + # Accumulators + dA_acc = tl.zeros((BLOCK_R, BLOCK_K), dtype=ACC_TYPE) + dB_acc = tl.zeros((BLOCK_N, BLOCK_R), dtype=ACC_TYPE) + + iters = tl.cdiv(num_tokens, BLOCK_M) + for i in range(iters): + M_idx = start_idx + i * BLOCK_M + M_block + M_mask = M_idx < end_idx + + # Load X: [BLOCK_M, BLOCK_K] + X_blk_ptrs = ( + X_ptr + M_idx[:, None] * stride_xm + K_block[None, :] * stride_xk + ) + x = tl.load( + X_blk_ptrs, mask=M_mask[:, None] & K_mask[None, :], other=0.0 + ).to(INPUT_DTYPE) + + # Load dY: [BLOCK_M, BLOCK_N] + DY_blk_ptrs = ( + DY_ptr + M_idx[:, None] * stride_dym + N_block[None, :] * stride_dyn + ) + dy = tl.load( + DY_blk_ptrs, mask=M_mask[:, None] & N_mask[None, :], other=0.0 + ).to(INPUT_DTYPE) + + # X @ A[e]^T: [M, K] @ [K, R] -> [M, R] + xa = tl.dot(x, tl.trans(a_e), allow_tf32=allow_tf32) + + # dY @ B[e]: [M, N] @ [N, R] -> [M, R] + dy_b = tl.dot(dy, b_e, allow_tf32=allow_tf32) + + # Cast intermediates to input dtype for subsequent tl.dot calls + # (tl.dot requires both operands to have the same dtype) + dy_b_cast = dy_b.to(INPUT_DTYPE) + xa_cast = xa.to(INPUT_DTYPE) + + # dA += (dY @ B)^T @ X: [R, M] @ [M, K] -> [R, K] + dA_acc += tl.dot(tl.trans(dy_b_cast), x, allow_tf32=allow_tf32) + + # dB += dY^T @ (X @ A^T): [N, M] @ [M, R] -> [N, R] + dB_acc += tl.dot(tl.trans(dy), xa_cast, allow_tf32=allow_tf32) + + # Store dA with scaling (atomic add since multiple N_blocks contribute) + # Only store the actual R rows, not the padded ones + DLA_blk_ptrs = ( + DLA_ptr + + (lora_offset + R_block)[:, None] * stride_dla_r + + K_block[None, :] * stride_dla_k + ) + tl.atomic_add( + DLA_blk_ptrs, + (dA_acc * scaling).to(DLA_ptr.dtype.element_ty), + mask=R_mask[:, None] & K_mask[None, :], + ) + + # Store dB with scaling (atomic add since multiple K_blocks contribute) + DLB_blk_ptrs = ( + DLB_ptr + + N_block[:, None] * stride_dlb_n + + (lora_offset + R_block)[None, :] * stride_dlb_r + ) + tl.atomic_add( + DLB_blk_ptrs, + (dB_acc * scaling).to(DLB_ptr.dtype.element_ty), + mask=N_mask[:, None] & R_mask[None, :], + ) + + +def group_bwd_lora( + DY: torch.Tensor, + X: torch.Tensor, + lora_A: torch.Tensor, + lora_B: torch.Tensor, + expert_offsets: torch.Tensor, + E: int, + scaling: float, + sorted_scattered_idxs: Optional[torch.Tensor] = None, + k: int = 1, +) -> tuple[torch.Tensor, torch.Tensor]: + """ + Compute LoRA gradients for A and B on expert-grouped data. + + Args: + DY: Gradient w.r.t. output [M_total, N] (grouped by expert) + X: Input [M_total, K] (grouped by expert) + lora_A: LoRA A weights [r*E, K] + lora_B: LoRA B weights [N, r*E] + expert_offsets: Cumulative token counts per expert [E] + E: Number of experts + scaling: LoRA scaling factor + + Returns: + dA: Gradient for A [r*E, K] + dB: Gradient for B [N, r*E] + """ + R = lora_A.size(0) // E + K = X.size(1) + N = DY.size(1) + + # Zero-init for atomic accumulation + dA = torch.zeros_like(lora_A) + dB = torch.zeros_like(lora_B) + + BLOCK_R = _block_r_for_rank(R) + + def grid(META): + return ( + E * triton.cdiv(K, META["BLOCK_K"]), + triton.cdiv(N, META["BLOCK_N"]), + ) + + _group_bwd_lora[grid]( + DY, + DY.stride(0), + DY.stride(1), + X, + X.stride(0), + X.stride(1), + lora_A, + lora_A.stride(0), + lora_A.stride(1), + lora_B, + lora_B.stride(0), + lora_B.stride(1), + dA, + dA.stride(0), + dA.stride(1), + dB, + dB.stride(0), + dB.stride(1), + expert_offsets, + M=DY.size(0), + K=K, + N=N, + ACTUAL_R=R, # True LoRA rank + BLOCK_R=BLOCK_R, # Padded tile size + scaling=scaling, + ACC_TYPE=tl.float32, + allow_tf32=ALLOW_TF32, + ) + + return dA, dB + + +# ============================================================================= +# Backward Kernel: Fused gather + LoRA gradient (dA, dB) — eliminates group() +# ============================================================================= + + +@triton.autotune( + configs=_group_bwd_lora_configs(), + key=["M", "N", "K"], + prune_configs_by={"early_config_prune": _prune_bwd_lora_configs}, + reset_to_zero=["DLA_ptr", "DLB_ptr"], +) +@triton.heuristics( + { + "NO_K_MASK": lambda args: (args["K"] % args["BLOCK_K"]) == 0, + "NO_N_MASK": lambda args: (args["N"] % args["BLOCK_N"]) == 0, + } +) +@triton.jit +def _group_bwd_lora_fused( + # Inputs (ungrouped or grouped) + DY_ptr, + stride_dym, + stride_dyn, + X_ptr, + stride_xm, + stride_xk, + # Scatter indices for gather-on-load + sorted_scattered_idxs_ptr, + FAN_OUT: tl.constexpr, + # LoRA weights (needed for cross-terms) + LA_ptr, + stride_la_r, + stride_la_k, # A: [r*E, K] + LB_ptr, + stride_lb_n, + stride_lb_r, # B: [N, r*E] + # Gradient outputs + DLA_ptr, + stride_dla_r, + stride_dla_k, + DLB_ptr, + stride_dlb_n, + stride_dlb_r, + # Expert offsets + expert_offsets_ptr, + # Real expert offsets (for M_mask when using token rounding, else same as expert_offsets_ptr) + real_expert_offsets_ptr, + # Dimensions + M, + K: tl.constexpr, + N: tl.constexpr, + ACTUAL_R: tl.constexpr, + BLOCK_R: tl.constexpr, + scaling, + # Block sizes + BLOCK_M: tl.constexpr, + BLOCK_K: tl.constexpr, + BLOCK_N: tl.constexpr, + ACC_TYPE: tl.constexpr, + allow_tf32: tl.constexpr, + NO_K_MASK: tl.constexpr, + NO_N_MASK: tl.constexpr, + # Whether DY is already in grouped (expert-sorted) order + dy_grouped: tl.constexpr = False, +): + """ + Fused gather + LoRA gradient computation. Same as _group_bwd_lora but + reads X from ungrouped buffers using sorted_scattered_idxs for indirect + indexing, eliminating the need for a separate group(X) call. + + When dy_grouped=False (default): both X and DY are read via indirect + indexing through sorted_scattered_idxs. This eliminates both group() + calls entirely. + + When dy_grouped=True: DY is already in grouped order (e.g. gate_up_proj + backward where grouped_out=True) and is read directly. Only X uses + indirect indexing. This avoids the group(X) allocation while + still supporting the grouped DY case. + + Grid: (E * cdiv(K, BLOCK_K), cdiv(N, BLOCK_N)) + + For expert e: + dA[e] = scaling * (dY @ B[e])^T @ X -> [r, K] + dB[e] = scaling * dY^T @ (X @ A[e]^T) -> [N, r] + + Supports token rounding: expert_offsets_ptr gives the iteration range + (padded to BLOCK_M multiples), real_expert_offsets_ptr gives the real + token count for M_mask (to exclude padding tokens). + """ + pid0 = tl.program_id(axis=0) + pid1 = tl.program_id(axis=1) + + K_BLOCK_COUNT = tl.cdiv(K, BLOCK_K) + E_idx = pid0 // K_BLOCK_COUNT + K_block_id = pid0 % K_BLOCK_COUNT + N_block_id = pid1 + + # Get expert's token range from cumulative offsets + # start_idx/end_idx from expert_offsets_ptr: iteration range (possibly padded) + # real_end_idx from real_expert_offsets_ptr: for M_mask (real token count) + if E_idx == 0: + start_idx = 0 + real_start_idx = 0 + else: + start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int32) + real_start_idx = tl.load(real_expert_offsets_ptr + E_idx - 1).to(tl.int32) + end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int32) + real_end_idx = tl.load(real_expert_offsets_ptr + E_idx).to(tl.int32) + num_tokens = end_idx - start_idx + + if num_tokens > 0: + M_block = tl.arange(0, BLOCK_M) + K_block = K_block_id * BLOCK_K + tl.arange(0, BLOCK_K) + K_mask = K_block < K + N_block = N_block_id * BLOCK_N + tl.arange(0, BLOCK_N) + N_mask = N_block < N + R_block = tl.arange(0, BLOCK_R) + R_mask = R_block < ACTUAL_R + + lora_offset = E_idx * ACTUAL_R + + # Determine input element type for consistent casting. + INPUT_DTYPE = X_ptr.dtype.element_ty + + # Load B[e] and A[e] — same as non-fused kernel + B_blk_ptrs = ( + LB_ptr + + N_block[:, None] * stride_lb_n + + (lora_offset + R_block)[None, :] * stride_lb_r + ) + b_e = tl.load(B_blk_ptrs, mask=N_mask[:, None] & R_mask[None, :], other=0.0).to( + INPUT_DTYPE + ) + + A_blk_ptrs = ( + LA_ptr + + (lora_offset + R_block)[:, None] * stride_la_r + + K_block[None, :] * stride_la_k + ) + a_e = tl.load(A_blk_ptrs, mask=R_mask[:, None] & K_mask[None, :], other=0.0).to( + INPUT_DTYPE + ) + + # Accumulators + dA_acc = tl.zeros((BLOCK_R, BLOCK_K), dtype=ACC_TYPE) + dB_acc = tl.zeros((BLOCK_N, BLOCK_R), dtype=ACC_TYPE) + + real_num_tokens = real_end_idx - real_start_idx + iters = tl.cdiv(num_tokens, BLOCK_M) + for i in range(iters): + M_idx = start_idx + i * BLOCK_M + M_block + # Use real token count for masking (excludes padding tokens) + M_local = i * BLOCK_M + M_block + M_mask = M_local < real_num_tokens + + # Fused gather: load scatter indices for indirect X access + scatter_idx = tl.load( + sorted_scattered_idxs_ptr + M_idx, mask=M_mask, other=0 + ).to(tl.int32) + X_token_idx = scatter_idx // FAN_OUT # X is [M, K], not expanded by k + + # Load X via indirect index: [BLOCK_M, BLOCK_K] + X_blk_ptrs = ( + X_ptr + X_token_idx[:, None] * stride_xm + K_block[None, :] * stride_xk + ) + x = tl.load( + X_blk_ptrs, mask=M_mask[:, None] & K_mask[None, :], other=0.0 + ).to(INPUT_DTYPE) + + # Load DY: indirect via scatter_idx when ungrouped, direct via M_idx when grouped + if dy_grouped: + DY_blk_ptrs = ( + DY_ptr + M_idx[:, None] * stride_dym + N_block[None, :] * stride_dyn + ) + else: + DY_blk_ptrs = ( + DY_ptr + + scatter_idx[:, None] * stride_dym + + N_block[None, :] * stride_dyn + ) + dy = tl.load( + DY_blk_ptrs, mask=M_mask[:, None] & N_mask[None, :], other=0.0 + ).to(INPUT_DTYPE) + + # X @ A[e]^T: [M, K] @ [K, R] -> [M, R] + xa = tl.dot(x, tl.trans(a_e), allow_tf32=allow_tf32) + + # dY @ B[e]: [M, N] @ [N, R] -> [M, R] + dy_b = tl.dot(dy, b_e, allow_tf32=allow_tf32) + + dy_b_cast = dy_b.to(INPUT_DTYPE) + xa_cast = xa.to(INPUT_DTYPE) + + # dA += (dY @ B)^T @ X: [R, M] @ [M, K] -> [R, K] + dA_acc += tl.dot(tl.trans(dy_b_cast), x, allow_tf32=allow_tf32) + + # dB += dY^T @ (X @ A^T): [N, M] @ [M, R] -> [N, R] + dB_acc += tl.dot(tl.trans(dy), xa_cast, allow_tf32=allow_tf32) + + # Store dA with scaling (atomic add since multiple N_blocks contribute) + DLA_blk_ptrs = ( + DLA_ptr + + (lora_offset + R_block)[:, None] * stride_dla_r + + K_block[None, :] * stride_dla_k + ) + tl.atomic_add( + DLA_blk_ptrs, + (dA_acc * scaling).to(DLA_ptr.dtype.element_ty), + mask=R_mask[:, None] & K_mask[None, :], + ) + + # Store dB with scaling (atomic add since multiple K_blocks contribute) + DLB_blk_ptrs = ( + DLB_ptr + + N_block[:, None] * stride_dlb_n + + (lora_offset + R_block)[None, :] * stride_dlb_r + ) + tl.atomic_add( + DLB_blk_ptrs, + (dB_acc * scaling).to(DLB_ptr.dtype.element_ty), + mask=N_mask[:, None] & R_mask[None, :], + ) + + +def group_bwd_lora_fused( + DY: torch.Tensor, + X: torch.Tensor, + lora_A: torch.Tensor, + lora_B: torch.Tensor, + expert_offsets: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + E: int, + k: int, + scaling: float, + real_expert_offsets: Optional[torch.Tensor] = None, + dy_grouped: bool = False, +) -> tuple[torch.Tensor, torch.Tensor]: + """ + Fused gather + LoRA gradient computation. Same result as + group(X) + group(DY) + group_bwd_lora(DY, X, ...) but without + the intermediate grouped buffers. + + Args: + DY: Gradient w.r.t. output [M*k, N]. + If dy_grouped=False: ungrouped (original token order), read via + indirect indexing through sorted_scattered_idxs. + If dy_grouped=True: already in grouped (expert-sorted) order, + read directly. + X: Input [M, K] (ungrouped, original token order). Always read via + indirect indexing through sorted_scattered_idxs. + lora_A: LoRA A weights [r*E, K] + lora_B: LoRA B weights [N, r*E] + expert_offsets: Cumulative token counts per expert [E] + (or padded offsets if using token rounding) + sorted_scattered_idxs: Maps grouped position -> original position [M*k] + (or padded version if using token rounding) + E: Number of experts + k: Fan-out (top-k) + scaling: LoRA scaling factor + real_expert_offsets: Original cumulative counts for M_mask when using + token rounding. If None, expert_offsets is used for both. + dy_grouped: Whether DY is already in grouped order (default False). + When True, avoids indirect indexing for DY, used for gate_up_proj + backward where grouped_out=True. + + Returns: + dA: Gradient for A [r*E, K] + dB: Gradient for B [N, r*E] + """ + R = lora_A.size(0) // E + K = X.size(1) + N = DY.size(1) + + # Zero-init for atomic accumulation + dA = torch.zeros_like(lora_A) + dB = torch.zeros_like(lora_B) + + BLOCK_R = _block_r_for_rank(R) + + if real_expert_offsets is None: + real_expert_offsets = expert_offsets + + def grid(META): + return ( + E * triton.cdiv(K, META["BLOCK_K"]), + triton.cdiv(N, META["BLOCK_N"]), + ) + + _group_bwd_lora_fused[grid]( + DY, + DY.stride(0), + DY.stride(1), + X, + X.stride(0), + X.stride(1), + sorted_scattered_idxs, + FAN_OUT=k, + LA_ptr=lora_A, + stride_la_r=lora_A.stride(0), + stride_la_k=lora_A.stride(1), + LB_ptr=lora_B, + stride_lb_n=lora_B.stride(0), + stride_lb_r=lora_B.stride(1), + DLA_ptr=dA, + stride_dla_r=dA.stride(0), + stride_dla_k=dA.stride(1), + DLB_ptr=dB, + stride_dlb_n=dB.stride(0), + stride_dlb_r=dB.stride(1), + expert_offsets_ptr=expert_offsets, + real_expert_offsets_ptr=real_expert_offsets, + M=sorted_scattered_idxs.size(0), + K=K, + N=N, + ACTUAL_R=R, + BLOCK_R=BLOCK_R, + scaling=scaling, + ACC_TYPE=tl.float32, + allow_tf32=ALLOW_TF32, + dy_grouped=dy_grouped, + ) + + return dA, dB diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/ops.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/ops.py new file mode 100644 index 0000000000..6aa432770d --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/ops.py @@ -0,0 +1,645 @@ +# SPDX-License-Identifier: Apache-2.0 +# Adapted from https://github.com/shawntan/scattermoe +# Copyright (c) Shawn Tan and ScatterMoE Contributors +# Licensed under the Apache License, Version 2.0 +# See https://github.com/shawntan/scattermoe/blob/main/LICENSE + +from typing import Optional + +import torch +import triton +import triton.language as tl + +BLOCK_M = 128 +ALLOW_TF32 = True + + +@triton.jit +def _compute_expert_block( + E_idx, + E_mask, + M_in_idx, + N_block, + N_mask, + X_ptr, + stride_xm, + stride_xk, + W_ptr, + stride_we, + stride_wk, + stride_wn, + K, + acc, + no_k_mask, + BLOCK_K, + allow_tf32=True, +): + K_block = tl.arange(0, BLOCK_K) + X_blk_ptrs = X_ptr + M_in_idx[:, None] * stride_xm + K_block[None, :] * stride_xk + W_blk_ptrs = ( + W_ptr + + K_block[:, None] * stride_wk + + N_block[None, :] * stride_wn + + E_idx * stride_we + ) + iters = tl.cdiv(K, BLOCK_K) + + for K_block_id in range(iters): + if no_k_mask: + x = tl.load(X_blk_ptrs, mask=E_mask[:, None]) + w = tl.load(W_blk_ptrs, mask=N_mask[None, :]) + else: + K_mask = (K_block_id * BLOCK_K + K_block) < K + x = tl.load(X_blk_ptrs, mask=E_mask[:, None] & K_mask[None, :]) + w = tl.load(W_blk_ptrs, mask=K_mask[:, None] & N_mask[None, :]) + + X_blk_ptrs += BLOCK_K * stride_xk + W_blk_ptrs += BLOCK_K * stride_wk + acc = tl.dot(x, w, acc, allow_tf32=allow_tf32) + return acc + + +def _scatter2scatter_configs(): + return [ + triton.Config({"BLOCK_N": 128, "BLOCK_K": 32}, num_stages=4, num_warps=4), + ] + + +@triton.autotune( + configs=_scatter2scatter_configs(), + key=["M", "N", "K"], +) +@triton.heuristics( + { + "NO_K_MASK": lambda args: (args["K"] % args["BLOCK_K"]) == 0, + "NO_N_MASK": lambda args: (args["N"] % args["BLOCK_N"]) == 0, + } +) +@triton.jit +def _scatter2scatter( + X_ptr, + stride_xm: tl.constexpr, + stride_xk: tl.constexpr, + W_ptr, + stride_we, + stride_wk: tl.constexpr, + stride_wn: tl.constexpr, + Y_ptr, + stride_ym: tl.constexpr, + stride_yn: tl.constexpr, + B_ptr, + stride_be: tl.constexpr, + stride_bn: tl.constexpr, + grouped_idx_ptr, + expert_idxs_ptr, + # block_start_idx_ptr, + FAN_OUT: tl.constexpr, + M, + K: tl.constexpr, + N: tl.constexpr, + E: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, + ACC_TYPE: tl.constexpr, + # OUT_M, + allow_tf32: tl.constexpr, + x_grouped: tl.constexpr, + y_grouped: tl.constexpr, + NO_K_MASK: tl.constexpr, + NO_N_MASK: tl.constexpr, +): + pid = tl.program_id(axis=0) + + N_BLOCK_COUNT = tl.cdiv(N, BLOCK_N) + M_block_id = pid // N_BLOCK_COUNT + N_block_id = pid % N_BLOCK_COUNT + + M_block = M_block_id * BLOCK_M + tl.arange(0, BLOCK_M) + N_block = N_block_id * BLOCK_N + tl.arange(0, BLOCK_N) + N_mask = N_block < N + M_boundary_mask = M_block < (FAN_OUT * M) + E_idxs = tl.load(expert_idxs_ptr + M_block, mask=M_boundary_mask, other=E) + + no_k_mask = K % BLOCK_K == 0 + + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=ACC_TYPE) + E_first_idx = tl.min(E_idxs) + E_last_idx = tl.minimum(tl.max(E_idxs), E - 1) + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) + for E_idx in range(E_first_idx, E_last_idx + 1): + E_mask = E_idxs == E_idx + E_M_idx = M_idx + if x_grouped: + M_in_idx = M_block + else: + M_in_idx = E_M_idx // FAN_OUT + acc = _compute_expert_block( + E_idx, + E_mask, + M_in_idx, + N_block, + N_mask, + X_ptr, + stride_xm, + stride_xk, + W_ptr, + stride_we, + stride_wk, + stride_wn, + K, + acc, + no_k_mask, + BLOCK_K, + allow_tf32=allow_tf32, + ) + + if B_ptr is not None: + B_blk_ptrs = B_ptr + E_idxs[:, None] * stride_be + N_block[None, :] * stride_bn + acc += tl.load(B_blk_ptrs, mask=M_boundary_mask[:, None] & N_mask[None, :]) + + if y_grouped: + M_out_idx = M_block + else: + M_out_idx = M_idx + Y_blk_ptrs = Y_ptr + (M_out_idx[:, None] * stride_ym + N_block[None, :] * stride_yn) + tl.store(Y_blk_ptrs, acc, mask=M_boundary_mask[:, None] & N_mask[None, :]) + + +def scatter2scatter( + X, + W, + sorted_expert_idxs, + sorted_scattered_idxs, + k, + b=None, + x_grouped=False, + y_grouped=False, + out=None, +): + assert sorted_scattered_idxs.size(0) == sorted_expert_idxs.size(0) + assert sorted_scattered_idxs.size(0) == X.size(0) * k + # Pre-kernel setup + y_dim = W.size(-1) + L_scattered = sorted_expert_idxs.size(0) + if out is None: + output = torch.empty((L_scattered, y_dim), device=X.device, dtype=X.dtype) + else: + assert out.size(0) == L_scattered and out.size(1) == y_dim + output = out + + scatter2scatter_compileable( + output, + W, + X, + k, + sorted_expert_idxs, + sorted_scattered_idxs, + b, + x_grouped, + y_grouped, + ) + return output + + +@torch.library.custom_op("scattermoe::scatter2scatter", mutates_args={"output"}) +def scatter2scatter_compileable( + output: torch.Tensor, + W: torch.Tensor, + X: torch.Tensor, + k: int, + sorted_expert_idxs: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + b: Optional[torch.Tensor], + x_grouped: bool, + y_grouped: bool, +) -> None: + def grid(META): + grid_num = ( + triton.cdiv(sorted_expert_idxs.size(0), META["BLOCK_M"]) + * triton.cdiv(META["N"], META["BLOCK_N"]), + ) + return grid_num + + if b is None: + b = None + stride_be = stride_bn = 0 + else: + stride_be, stride_bn = b.stride() + + _scatter2scatter[grid]( + # X_ptr, stride_xm, stride_xk, + X, + X.stride(0), + X.stride(1), + # W_ptr, stride_we, stride_wk, stride_wn, + W, + W.stride(0), + W.stride(1), + W.stride(2), + # Y_ptr, stride_ym, stride_yn, + output, + output.stride(0), + output.stride(1), + # B_ptr, stride_be, stride_bn + b, + stride_be, + stride_bn, + grouped_idx_ptr=sorted_scattered_idxs, + expert_idxs_ptr=sorted_expert_idxs, + # block_start_idx_ptr=padded_block_idxs, + FAN_OUT=k, + M=X.size(0), + K=X.size(1), + N=output.size(1), + E=W.size(0), + BLOCK_M=BLOCK_M, + ACC_TYPE=tl.float32, + allow_tf32=ALLOW_TF32, + x_grouped=x_grouped, + y_grouped=y_grouped, + ) + + +def _config_XtY(): + return [ + triton.Config( + {"BLOCK_N": 128, "BLOCK_K": 128, "BLOCK_M": 32}, num_stages=4, num_warps=4 + ), + ] + + +def group_bwd_W(DY, X, expert_offsets, E, has_bias=False): + DWt = torch.zeros((E, DY.size(-1), X.size(-1)), device=DY.device, dtype=DY.dtype) + DW = DWt.permute(0, 2, 1) + if has_bias: + Db = torch.zeros((E, DY.size(-1)), device=DY.device, dtype=DY.dtype) + else: + Db = None + groupXtY_compileable(E, DW, Db, DY, X, expert_offsets) + return DW, Db + + +@torch.library.custom_op("scattermoe::groupXtY", mutates_args={"DW", "Db"}) +def groupXtY_compileable( + E: int, + DW: torch.Tensor, + Db: Optional[torch.Tensor], + DY: torch.Tensor, + X: torch.Tensor, + expert_offsets: torch.Tensor, +) -> None: + def grid(META): + grid = ( + E * triton.cdiv(META["K"], META["BLOCK_K"]), + triton.cdiv(META["N"], META["BLOCK_N"]), + ) + return grid + + if Db is None: + stride_dbe = 0 + stride_dbn = 0 + else: + stride_dbe, stride_dbn = Db.stride() + + _groupXtY[grid]( + # DY_ptr, stride_dym, stride_dyk, + DY, + DY.stride(0), + DY.stride(1), + # X_ptr, stride_xm, stride_xn, + X, + X.stride(0), + X.stride(1), + # DW_ptr, stride_dwe, stride_dwk, stride_dwn, + DW, + DW.stride(0), + DW.stride(1), + DW.stride(2), + # Db_ptr, stride_dwe, stride_dbn, + Db, + stride_dbe, + stride_dbn, + # expert_offsets_ptr, + expert_offsets, + # K: tl.constexpr, N: tl.constexpr, + M=DY.size(0), + N=DY.size(-1), + K=X.size(-1), + # ACC_TYPE: tl.constexpr, + ACC_TYPE=tl.float32, + allow_tf32=ALLOW_TF32, + ) + + +@triton.autotune( + configs=_config_XtY(), + key=["M", "N", "K"], +) +@triton.heuristics( + { + "NO_K_MASK": lambda args: (args["K"] % args["BLOCK_K"]) == 0, + "NO_N_MASK": lambda args: (args["N"] % args["BLOCK_N"]) == 0, + } +) +@triton.jit +def _groupXtY( + DY_ptr, + stride_dym, + stride_dyk, + X_ptr, + stride_xm, + stride_xn, + DW_ptr, + stride_dwe, + stride_dwk, + stride_dwn, + Db_ptr, + stride_dbe, + stride_dbn, + expert_offsets_ptr, + M, + K: tl.constexpr, + N: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, + ACC_TYPE: tl.constexpr, + allow_tf32: tl.constexpr, + NO_K_MASK: tl.constexpr, + NO_N_MASK: tl.constexpr, +): + pid0 = tl.program_id(axis=0) + pid1 = tl.program_id(axis=1) + num0 = tl.num_programs(0) + num1 = tl.num_programs(1) + # pid1, pid0 = tl.swizzle2d(pid1, pid0, num1, num0, 128) + pid0, pid1 = tl.swizzle2d(pid0, pid1, num0, num1, 4) + + K_BLOCK_COUNT = tl.cdiv(K, BLOCK_K) + E_idx = pid0 // K_BLOCK_COUNT + K_block_id = pid0 % K_BLOCK_COUNT + N_block_id = pid1 + + if E_idx == 0: + start_idx = 0 + else: + start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int32) + end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int32) + + if end_idx > start_idx: + M_block = tl.max_contiguous(start_idx + tl.arange(0, BLOCK_M), BLOCK_M) + + K_block = K_block_id * BLOCK_K + tl.arange(0, BLOCK_K) + K_mask = K_block < K + K_block = tl.max_contiguous(tl.multiple_of(K_block % K, BLOCK_K), BLOCK_K) + + N_block = N_block_id * BLOCK_N + tl.arange(0, BLOCK_N) + N_mask = N_block < N + N_block = tl.max_contiguous(tl.multiple_of(N_block % N, BLOCK_N), BLOCK_N) + + M_idxs = M_block + xt_blk_ptrs = X_ptr + K_block[:, None] * stride_xn + M_idxs[None, :] * stride_xm + dy_blk_ptrs = ( + DY_ptr + M_idxs[:, None] * stride_dym + N_block[None, :] * stride_dyk + ) + if (Db_ptr is not None) and (K_block_id == 0): + _xty_and_bias( + E_idx, + start_idx, + end_idx, + M_block, + K_block, + K_mask, + N_block, + N_mask, + dy_blk_ptrs, + stride_dym, + xt_blk_ptrs, + stride_xm, + DW_ptr, + stride_dwe, + stride_dwk, + stride_dwn, + Db_ptr, + stride_dbe, + stride_dbn, + BLOCK_M, + BLOCK_N, + BLOCK_K, + ACC_TYPE, + allow_tf32, + NO_K_MASK, + NO_N_MASK, + compute_bias=True, + ) + else: + _xty_and_bias( + E_idx, + start_idx, + end_idx, + M_block, + K_block, + K_mask, + N_block, + N_mask, + dy_blk_ptrs, + stride_dym, + xt_blk_ptrs, + stride_xm, + DW_ptr, + stride_dwe, + stride_dwk, + stride_dwn, + Db_ptr, + stride_dbe, + stride_dbn, + BLOCK_M, + BLOCK_N, + BLOCK_K, + ACC_TYPE, + allow_tf32, + NO_K_MASK, + NO_N_MASK, + compute_bias=False, + ) + + +@triton.jit +def _xty_and_bias( + E_idx, + start_idx, + end_idx, + M_block, + K_block, + K_mask, + N_block, + N_mask, + dy_blk_ptrs, + stride_dym, + xt_blk_ptrs, + stride_xm, + DW_ptr, + stride_dwe, + stride_dwk, + stride_dwn, + Db_ptr, + stride_dbe, + stride_dbn, + BLOCK_M, + BLOCK_N, + BLOCK_K, + ACC_TYPE, + allow_tf32, + NO_K_MASK, + NO_N_MASK, + compute_bias: tl.constexpr, +): + if compute_bias: + db_acc = tl.zeros((BLOCK_N,), dtype=ACC_TYPE) + else: + db_acc = None + + acc = tl.zeros((BLOCK_K, BLOCK_N), dtype=ACC_TYPE) + iters = tl.cdiv(end_idx - start_idx, BLOCK_M) + for i in range(0, iters): + M_mask = (i * BLOCK_M + M_block) < end_idx + if NO_K_MASK: + xt = tl.load(xt_blk_ptrs, mask=M_mask[None, :]) + else: + xt = tl.load(xt_blk_ptrs, mask=K_mask[:, None] & M_mask[None, :]) + if NO_N_MASK: + dy = tl.load(dy_blk_ptrs, mask=M_mask[:, None]) + else: + dy = tl.load(dy_blk_ptrs, mask=M_mask[:, None] & N_mask[None, :]) + + acc += tl.dot(xt, dy, out_dtype=ACC_TYPE, allow_tf32=allow_tf32) + + xt_blk_ptrs += BLOCK_M * stride_xm + dy_blk_ptrs += BLOCK_M * stride_dym + + if compute_bias: + db_acc += tl.sum(dy, axis=0) + + DW_blk_ptrs = ( + DW_ptr + + E_idx * stride_dwe + + K_block[:, None] * stride_dwk + + N_block[None, :] * stride_dwn + ) + acc = acc.to(DW_blk_ptrs.dtype.element_ty) + tl.store(DW_blk_ptrs, acc, mask=K_mask[:, None] & N_mask[None, :]) + if compute_bias: + Db_blk_ptrs = Db_ptr + E_idx * stride_dbe + N_block * stride_dbn + tl.store(Db_blk_ptrs, db_acc, mask=N_mask) + + +def _config_grouping(): + return [ + triton.Config({"BLOCK_N": 256, "BLOCK_K": 128}, num_stages=4, num_warps=4), + # triton.Config({'BLOCK_N': 128, 'BLOCK_K': 64}, num_stages=4, num_warps=4), + # triton.Config({'BLOCK_N': 64, 'BLOCK_K': 32}, num_stages=4, num_warps=4), + ] + + +def group(A, sorted_expert_idxs, coeff=None, fan_out=1, out=None): + N = sorted_expert_idxs.size(0) + K = A.size(1) + assert A.size(0) * fan_out == N + if out is not None: + Y = out + else: + Y = torch.empty((N, K), dtype=A.dtype, device=A.device) + group_compileable(A, K, N, Y, coeff, coeff is not None, fan_out, sorted_expert_idxs) + return Y + + +@torch.library.custom_op("scattermoe::group", mutates_args={"Y"}) +def group_compileable( + A: torch.Tensor, + K: int, + N: int, + Y: torch.Tensor, + coeff: Optional[torch.Tensor], + has_coeff: bool, + fan_out: int, + sorted_expert_idxs: torch.Tensor, +) -> None: + def grid(META): + grid_num = (triton.cdiv(META["N"], META["BLOCK_N"]),) + return grid_num + + _group[grid]( + # A_ptr, stride_an, stride_ai, + A, + A.stride(0), + A.stride(1), + has_coeff, + coeff, + fan_out, + # Y_ptr, stride_yn, stride_yk, + Y, + Y.stride(0), + Y.stride(1), + # grouped_idx_ptr, + sorted_expert_idxs, + # N: tl.constexpr, K: tl.constexpr, + N, + K, + ) + + +@triton.autotune(configs=_config_grouping(), key=["K"]) +@triton.heuristics({"NO_K_MASK": lambda args: (args["K"] % args["BLOCK_K"]) == 0}) +@triton.jit +def _group( + src_ptr, + stride_sn, + stride_sk, + has_coeff: tl.constexpr, + coeff_ptr, + FAN_OUT: tl.constexpr, + tgt_ptr, + stride_tn, + stride_ti, + grouped_idx_ptr, + N, + K: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, + NO_K_MASK: tl.constexpr, +): + pid = tl.program_id(axis=0) + + N_block_id = pid + N_blk = N_block_id * BLOCK_N + tl.arange(0, BLOCK_N) + N_mask = N_blk < N + N_blk = tl.max_contiguous(tl.multiple_of(N_blk % N, BLOCK_N), BLOCK_N) + N_idx = tl.load(grouped_idx_ptr + N_blk, mask=N_mask, other=0) + + K_blk = tl.arange(0, BLOCK_K) + src_blk_ptrs = ( + src_ptr + (N_idx // FAN_OUT)[:, None] * stride_sn + K_blk[None, :] * stride_sk + ) + tgt_blk_ptrs = tgt_ptr + N_blk[:, None] * stride_tn + K_blk[None, :] * stride_ti + + if has_coeff: + c = tl.load(coeff_ptr + N_idx, mask=N_mask)[:, None] + + iters = tl.cdiv(K, BLOCK_K) + for i in range(0, iters): + if NO_K_MASK or i < iters - 1: + block = tl.load(src_blk_ptrs, mask=N_mask[:, None]) + if has_coeff: + block *= c + tl.store(tgt_blk_ptrs, block, mask=N_mask[:, None]) + + else: + K_mask = (i * BLOCK_K + K_blk) < K + mask = N_mask[:, None] & K_mask[None, :] + block = tl.load(src_blk_ptrs, mask=mask) + if has_coeff: + block *= c + tl.store(tgt_blk_ptrs, block, mask=mask) + src_blk_ptrs += BLOCK_K * stride_sk + tgt_blk_ptrs += BLOCK_K * stride_ti diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/single.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/single.py new file mode 100644 index 0000000000..9f0270aa67 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/single.py @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: Apache-2.0 +# Adapted from https://github.com/shawntan/scattermoe +# Copyright (c) Shawn Tan and ScatterMoE Contributors +# Licensed under the Apache License, Version 2.0 +# See https://github.com/shawntan/scattermoe/blob/main/LICENSE + +import torch +import triton +import triton.language as tl + + +@triton.jit +def _single2scatter( + X_ptr, + stride_xm, + stride_xk, + W_ptr, + stride_we, + stride_wk, + stride_wn, + Y_ptr, + stride_ym, + stride_yn, + expert_idxs_ptr, + FAN_OUT: tl.constexpr, + K: tl.constexpr, + N: tl.constexpr, + E: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, + ACC_TYPE: tl.constexpr, +): + pid0 = tl.program_id(axis=0) + pid1 = tl.program_id(axis=1) + + N_block_id = pid0 + if FAN_OUT == 1: + in_idx = pid1 + else: + in_idx = 0 + out_idx = pid1 + + K_block = tl.arange(0, BLOCK_K) + N_block = tl.max_contiguous( + tl.multiple_of((N_block_id * BLOCK_N + tl.arange(0, BLOCK_N)) % N, BLOCK_N), + BLOCK_N, + ) + E_idx = tl.load(expert_idxs_ptr + pid1) + X_blk_ptrs = X_ptr + in_idx * stride_xm + K_block[:, None] * stride_xk + W_blk_ptrs = ( + W_ptr + + E_idx * stride_we + + K_block[:, None] * stride_wk + + N_block[None, :] * stride_wn + ) + N_mask = N_block < N + acc = tl.zeros((1, BLOCK_N), dtype=ACC_TYPE) + for _K_block_id in range(0, tl.cdiv(K, BLOCK_K)): + K_mask = K_block < K + x = tl.load(X_blk_ptrs, mask=K_mask[:, None], other=0.0) + w = tl.load(W_blk_ptrs, mask=K_mask[:, None] & N_mask[None, :], other=0.0) + acc += tl.sum(x * w, axis=0)[None, :] + X_blk_ptrs += BLOCK_K * stride_xk + W_blk_ptrs += BLOCK_K * stride_wk + K_block += BLOCK_K + Y_blk_ptrs = Y_ptr + out_idx * stride_ym + N_block[None, :] * stride_yn + tl.store(Y_blk_ptrs, acc, mask=N_mask[None, :]) + + +def single2scatter(X, W, expert_idxs): + E, xdim, ydim = W.size() + k = expert_idxs.size(1) + assert X.size(0) == k or X.size(0) == 1 + Y = torch.empty((k, ydim), device=X.device, dtype=X.dtype) + BLOCK_N = 128 + BLOCK_K = 128 + grid = triton.cdiv(ydim, BLOCK_N), k + _single2scatter[grid]( + X, + X.stride(0), + X.stride(1), + W, + W.stride(0), + W.stride(1), + W.stride(2), + Y, + Y.stride(0), + Y.stride(1), + expert_idxs, + FAN_OUT=Y.size(0) // X.size(0), + K=xdim, + N=ydim, + E=E, + BLOCK_N=BLOCK_N, + BLOCK_K=BLOCK_K, + ACC_TYPE=tl.float32, + ) + return Y diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py new file mode 100644 index 0000000000..a425774833 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py @@ -0,0 +1,439 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Original work Copyright (c) Shawn Tan and ScatterMoE Contributors +# Adapted from https://github.com/shawntan/scattermoe +# See https://github.com/shawntan/scattermoe/blob/main/LICENSE +# +# Modifications and LoRA adaptation Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +ScatterMoE layer replacements for HuggingFace MoE architectures. + +Provides drop-in forward replacements that use ScatterMoE kernels for +acceleration. When used via the HF ``kernels`` library +(``replace_kernel_forward_from_hub``), these classes replace the forward +method of the original MoE block. + +LoRA support +------------ +When peft wraps parameters via ``target_parameters``, the ``self.experts`` +submodule becomes a chain of ``ParamWrapper`` objects and the ``self.gate`` +router may also become a ``ParamWrapper``. The ``HFScatterMoEGatedMLP`` +forward detects this and automatically: + +1. Unwraps ``self.gate`` to the base router, applying gate LoRA delta +2. Unwraps ``self.experts`` to the base ``OlmoeExperts`` module +3. Extracts LoRA A/B weights and scaling from each wrapper +4. Converts B layout from peft rank-major to scattermoe expert-major +5. Routes to ``parallel_linear_lora`` for fused LoRA computation +6. Passes through ``self.shared_expert`` / ``self.shared_expert_gate`` + (peft wraps their linear layers with standard LoRA, no special handling) +""" + +import torch +from torch import nn +from torch.nn import functional as F + +from .parallel_experts import flatten_sort_count, parallel_linear +from .parallel_linear_lora import get_lora_params_from_wrapper, parallel_linear_lora + +# ============================================================================= +# LoRA layout conversion utilities (peft <-> scattermoe) +# ============================================================================= + + +def peft_lora_B_to_scattermoe(peft_B, num_experts, rank): + """Convert peft rank-major lora_B ``[out, E*r]`` to scattermoe + expert-major ``[N, r*E]``. + + peft reshapes B to ``[out, r, E]`` (rank-major). + scattermoe slices B as ``[:, e*r:(e+1)*r]`` (expert-major). + """ + N = peft_B.shape[0] + return ( + peft_B.reshape(N, rank, num_experts) + .permute(0, 2, 1) + .contiguous() + .reshape(N, num_experts * rank) + ) + + +def peft_lora_to_scattermoe(peft_A, peft_B, num_experts, rank): + """Convert peft LoRA weights to scattermoe layout (with A<->B swap). + + peft operates on the parameter in its native storage layout ``[E, dim1, dim2]`` + where ``in_features=dim1, out_features=dim2``. ScatterMoE transposes the + parameter (``W = param.transpose(2, 1)``) giving ``[E, dim2, dim1]`` with + ``K=dim2, N=dim1``. Because of this transposition, peft's A and B roles + are swapped relative to scattermoe's convention. + + peft gives: + lora_A ``[r*E, dim1]``, lora_B ``[dim2, r*E]`` + + scattermoe needs: + lora_A ``[r*E, K=dim2]``, lora_B ``[N=dim1, r*E]`` + + This function swaps A<->B and converts B from rank-major to expert-major. + Uses vectorized tensor operations (no Python loop over experts). + + Works for **both** gate_up_proj and down_proj since the transposition + issue is the same for any parameter. + """ + peft_B_em = peft_lora_B_to_scattermoe(peft_B, num_experts, rank) + + dim1 = peft_A.shape[1] # peft in_features -> scattermoe N + dim2 = peft_B_em.shape[0] # peft out_features -> scattermoe K + + # smoe_A: per expert, transpose B_e [dim2, r] -> [r, dim2] + # [dim2, E*r] -> [dim2, E, r] -> [E, r, dim2] -> [E*r, dim2] + smoe_A = ( + peft_B_em.reshape(dim2, num_experts, rank) + .permute(1, 2, 0) + .contiguous() + .reshape(rank * num_experts, dim2) + ) + + # smoe_B: per expert, transpose A_e [r, dim1] -> [dim1, r] + # [E*r, dim1] -> [E, r, dim1] -> [dim1, E, r] -> [dim1, E*r] + smoe_B = ( + peft_A.reshape(num_experts, rank, dim1) + .permute(2, 0, 1) + .contiguous() + .reshape(dim1, num_experts * rank) + ) + + return smoe_A, smoe_B + + +def peft_down_proj_lora_to_scattermoe(peft_A, peft_B, num_experts, rank): + """Deprecated alias for :func:`peft_lora_to_scattermoe`.""" + return peft_lora_to_scattermoe(peft_A, peft_B, num_experts, rank) + + +# ============================================================================= +# ParamWrapper unwrapping +# ============================================================================= + + +def _unwrap_gate_lora(gate_module): + """Unwrap peft ``ParamWrapper`` on the router gate. + + When peft targets ``gate.weight``, ``self.gate`` becomes:: + + ParamWrapper(weight) + -> base_layer: OlmoeTopKRouter (the real module) + + This function detects the wrapping and returns the base router, its + weight tensor, and an optional LoRA delta tensor. + + Returns: + (base_gate, gate_weight, gate_lora_delta_or_None) + + ``base_gate`` is the original router module (with ``.top_k``, + ``.num_experts``, ``.norm_topk_prob``). + ``gate_weight`` is the base router weight (may be a DTensor under FSDP). + ``gate_lora_delta_or_None`` is the LoRA delta tensor if LoRA is active, + else ``None``. Kept separate to avoid mixing DTensor + Tensor in an add. + """ + if hasattr(gate_module, "base_layer") and hasattr(gate_module, "lora_A"): + base_gate = gate_module.base_layer + lora_A, lora_B, scaling = get_lora_params_from_wrapper(gate_module) + if lora_A is not None: + # gate weight: [num_experts, hidden_size] + # lora_A: [r, hidden_size], lora_B: [num_experts, r] + # delta = scaling * B @ A = [num_experts, hidden_size] + delta = scaling * (lora_B @ lora_A) + return base_gate, base_gate.weight, delta + else: + return base_gate, base_gate.weight, None + else: + # No wrapping — gate is the original module + return gate_module, gate_module.weight, None + + +def _convert_smoe_lora(lora_A, lora_B, num_experts, rank, scaling): + """Convert peft LoRA weights to scattermoe layout.""" + smoe_A, smoe_B = peft_lora_to_scattermoe(lora_A, lora_B, num_experts, rank) + return (smoe_A, smoe_B, scaling) + + +def _unwrap_experts_lora(experts_module): + """Walk a peft ``ParamWrapper`` chain on ``self.experts``. + + When peft targets ``experts.gate_up_proj`` and ``experts.down_proj`` via + ``target_parameters``, ``self.experts`` becomes a nested chain:: + + ParamWrapper(down_proj) + -> base_layer: ParamWrapper(gate_up_proj) + -> base_layer: OlmoeExperts (the real module) + + This function walks the chain, collects LoRA params keyed by + ``parameter_name``, and returns the base experts module. + + Returns: + (base_experts, gup_lora, down_lora) + + Each ``*_lora`` is either ``(smoe_A, smoe_B, scaling)`` or ``None``. + A/B are already in scattermoe layout. + """ + # Collect ParamWrapper layers by their parameter_name + wrappers = {} + module = experts_module + while hasattr(module, "base_layer") and hasattr(module, "lora_A"): + param_name = getattr(module, "parameter_name", None) + if param_name is not None: + wrappers[param_name] = module + module = module.base_layer + + base_experts = module + + if not wrappers: + return base_experts, None, None + + # Determine num_experts from base module + num_experts = getattr(base_experts, "num_experts", None) + if num_experts is None: + # Fallback: infer from parameter shape + gup = getattr(base_experts, "gate_up_proj", None) + if gup is not None: + num_experts = gup.shape[0] + + # Extract gate_up_proj LoRA (needs A<->B swap due to transposition) + gup_lora = None + gup_wrapper = wrappers.get("gate_up_proj") + if gup_wrapper is not None: + lora_A, lora_B, scaling = get_lora_params_from_wrapper(gup_wrapper) + if lora_A is not None: + rank = lora_A.shape[0] // num_experts + gup_lora = _convert_smoe_lora(lora_A, lora_B, num_experts, rank, scaling) + + # Extract down_proj LoRA (needs A<->B swap due to transposition) + down_lora = None + down_wrapper = wrappers.get("down_proj") + if down_wrapper is not None: + lora_A, lora_B, scaling = get_lora_params_from_wrapper(down_wrapper) + if lora_A is not None: + rank = lora_A.shape[0] // num_experts + down_lora = _convert_smoe_lora(lora_A, lora_B, num_experts, rank, scaling) + + return base_experts, gup_lora, down_lora + + +# ============================================================================= +# Layer classes +# ============================================================================= + + +class ScatterMoEGatedMLP(nn.Module): + def forward(self, layer_input): + """ + Forward pass of the mixture of experts layer. + + Args: + layer_input (Tensor): + Input tensor. + + Returns: + Tensor: + Output tensor. + """ + bsz, length, emb_size = layer_input.size() + layer_input = layer_input.reshape(-1, emb_size) + # compute the top_k routing decision + router_logits = self.router.layer(layer_input) + routing_weights = F.softmax(router_logits, dim=1, dtype=torch.float) + routing_weights, selected_experts = torch.topk( + routing_weights, self.router.top_k, dim=-1 + ) + routing_weights /= routing_weights.sum(dim=-1, keepdim=True) + routing_weights = routing_weights.to(layer_input.dtype) + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = flatten_sort_count( + selected_experts, num_experts=self.router.num_experts + ) + + # compute experts + gates, h = parallel_linear( + layer_input, + self.input_linear.weight.transpose(2, 1), + self.router.top_k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=False, + grouped_out=True, + ).chunk(2, dim=-1) + h = self.activation(gates) * h + layer_output = parallel_linear( + h, + self.output_linear.weight.transpose(2, 1), + 1, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=True, + grouped_out=False, + gates=routing_weights, + ) + layer_output = layer_output.view(bsz, length, emb_size) + return layer_output + + +class HFScatterMoEGatedMLP(nn.Module): + """ + ScatterMoE-accelerated forward pass for HF MoEs (OLMoE / Qwen2MoE). + + Used as a kernel layer via the HF ``kernels`` library. The ``forward`` + method replaces the original ``OlmoeSparseMoeBlock.forward``. + + Supports both full-parameter training and LoRA fine-tuning: + + * **Full-param**: uses ``parallel_linear`` (base ScatterMoE kernel) + * **LoRA**: detects peft ``ParamWrapper`` on ``self.experts``, extracts + adapter weights, and uses ``parallel_linear_lora`` (fused kernel) + """ + + @staticmethod + def forward(self: nn.Module, layer_input: torch.Tensor): + """ + Forward pass using ScatterMoE kernels. + + Args: + self: The MoeSparseMoeBlock module containing: + - self.gate: Router (or peft ParamWrapper wrapping it) + - self.experts: Experts module (or peft ParamWrapper chain) + - self.shared_expert: Optional shared expert (e.g. Qwen2MoE) + - self.shared_expert_gate: Optional shared expert gate + layer_input: Input tensor [batch_size, seq_len, hidden_size] + + Returns: + Tensor: [batch_size, seq_len, hidden_size] + """ + batch_size, sequence_length, hidden_dim = layer_input.shape + hidden_states_flat = layer_input.view(-1, hidden_dim) + + # ==================================================================== + # Shared Expert (if present, e.g. Qwen2MoE) + # ==================================================================== + # peft wraps individual linear layers inside shared_expert with + # standard LoRA — calling forward() handles this transparently. + if hasattr(self, "shared_expert") and self.shared_expert is not None: + shared_expert_output = self.shared_expert(hidden_states_flat) + # shared_expert_gate may also be peft-wrapped (standard LoRA + # on nn.Linear), its forward() applies LoRA automatically. + shared_expert_gate_output = F.sigmoid( + self.shared_expert_gate(hidden_states_flat) + ) + shared_expert_output = shared_expert_output * shared_expert_gate_output + else: + shared_expert_output = None + + # ==================================================================== + # Router Computation (with optional gate LoRA) + # ==================================================================== + base_gate, gate_weight, gate_lora_delta = _unwrap_gate_lora(self.gate) + router_logits = F.linear(hidden_states_flat, gate_weight) + if gate_lora_delta is not None: + router_logits = router_logits + F.linear( + hidden_states_flat, gate_lora_delta + ) + routing_weights = F.softmax(router_logits, dim=1, dtype=torch.float) + + top_k = base_gate.top_k + num_experts = base_gate.num_experts + routing_weights, selected_experts = torch.topk(routing_weights, top_k, dim=-1) + + if base_gate.norm_topk_prob: + routing_weights /= routing_weights.sum(dim=-1, keepdim=True) + routing_weights = routing_weights.to(hidden_states_flat.dtype) + + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = flatten_sort_count( + selected_experts, num_experts=num_experts + ) + + # ==================================================================== + # Detect LoRA (peft ParamWrapper) and extract adapter weights + # ==================================================================== + experts, gup_lora, down_lora = _unwrap_experts_lora(self.experts) + + # ==================================================================== + # Gate + Up projection + # ==================================================================== + gate_up_W = experts.gate_up_proj.transpose(2, 1) # [E, hidden, 2*inter] + + if gup_lora is not None: + gup_A, gup_B, gup_scaling = gup_lora + gup = parallel_linear_lora( + hidden_states_flat, + gate_up_W, + top_k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + lora_A=gup_A, + lora_B=gup_B, + scaling=gup_scaling, + grouped_in=False, + grouped_out=True, + use_fused_dX=True, + use_fused_gather=True, + ) + else: + gup = parallel_linear( + hidden_states_flat, + gate_up_W, + top_k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=False, + grouped_out=True, + ) + + gates, h = gup.chunk(2, dim=-1) + h = experts.act_fn(gates) * h + + # ==================================================================== + # Down projection + # ==================================================================== + down_W = experts.down_proj.transpose(2, 1) # [E, inter, hidden] + + if down_lora is not None: + down_A, down_B, down_scaling = down_lora + expert_output = parallel_linear_lora( + h, + down_W, + 1, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + lora_A=down_A, + lora_B=down_B, + scaling=down_scaling, + gates=routing_weights, + grouped_in=True, + grouped_out=False, + use_fused_dX=True, + use_fused_gather=True, + ) + else: + expert_output = parallel_linear( + h, + down_W, + 1, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=True, + grouped_out=False, + gates=routing_weights, + ) + + # ==================================================================== + # Combine with shared expert and reshape + # ==================================================================== + if shared_expert_output is not None: + expert_output = expert_output + shared_expert_output + + expert_output = expert_output.view(batch_size, sequence_length, hidden_dim) + return expert_output diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/lora_ops.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/lora_ops.py new file mode 100644 index 0000000000..aec68311b6 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/lora_ops.py @@ -0,0 +1,99 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +ParallelExperts module with LoRA support. + +Provides a drop-in replacement for ScatterMoE's ParallelExperts that +uses the fused LoRA kernel when adapter weights are attached. +""" + +from typing import Optional + +import torch +import torch.nn as nn + +from .parallel_linear_lora import parallel_linear_lora + + +class ParallelExperts(nn.Module): + """ + Parallel Experts with fused LoRA support. + + Drop-in replacement for the original ParallelExperts. When LoRA parameters + are attached via set_lora(), the forward pass uses a fused kernel: + Y = X @ W + scaling * (X @ A^T) @ B^T + """ + + def __init__( + self, + num_experts: int, + input_size: int, + output_size: int, + bias: bool = False, + ) -> None: + super().__init__() + self.weight = nn.Parameter(torch.empty(num_experts, output_size, input_size)) + if bias: + self.bias = nn.Parameter(torch.empty(num_experts, output_size)) + else: + self.bias = None + self.num_experts = num_experts + self.input_size = input_size + self.output_size = output_size + self._lora_A: torch.Tensor | None = None + self._lora_B: torch.Tensor | None = None + self._lora_scaling: float | None = None + self.reset_parameters() + + def reset_parameters(self) -> None: + nn.init.normal_(self.weight, std=0.02) + if self.bias is not None: + nn.init.zeros_(self.bias) + + def extra_repr(self) -> str: + return ( + f"num_experts={self.num_experts}, " + f"input_size={self.input_size}, " + f"output_size={self.output_size}" + ) + + def set_lora(self, lora_A: torch.Tensor, lora_B: torch.Tensor, scaling: float): + """Attach LoRA parameters for fused computation.""" + self._lora_A = lora_A + self._lora_B = lora_B + self._lora_scaling = scaling + + def clear_lora(self): + """Remove LoRA parameters.""" + self._lora_A = None + self._lora_B = None + self._lora_scaling = None + + def forward( + self, + inputs: torch.Tensor, + k: int, + sorted_expert_idxs: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + expert_offsets: torch.Tensor, + gates: Optional[torch.Tensor] = None, + grouped_in: bool = False, + grouped_out: bool = False, + ) -> torch.Tensor: + return parallel_linear_lora( + inputs, + self.weight.permute(0, 2, 1), # [E, input, output] + k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + lora_A=self._lora_A, + lora_B=self._lora_B, + scaling=self._lora_scaling if self._lora_scaling is not None else 1.0, + expert_biases=self.bias, + gates=gates, + grouped_in=grouped_in, + grouped_out=grouped_out, + ) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_experts.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_experts.py new file mode 100644 index 0000000000..7a1eef472b --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_experts.py @@ -0,0 +1,253 @@ +# SPDX-License-Identifier: Apache-2.0 +# Adapted from https://github.com/shawntan/scattermoe +# Copyright (c) Shawn Tan and ScatterMoE Contributors +# Licensed under the Apache License, Version 2.0 +# See https://github.com/shawntan/scattermoe/blob/main/LICENSE + +from typing import Optional + +import torch +import torch.nn as nn + +from . import kernels + + +@torch.library.custom_op("scattermoe::bincount", mutates_args={}) +def compileable_bincount(x: torch.Tensor, minlength: int) -> torch.Tensor: + return x.bincount(minlength=minlength) + + +@compileable_bincount.register_fake +def _(x: torch.Tensor, minlength: int) -> torch.Tensor: + return torch.empty(minlength, dtype=torch.long, device=x.device) + + +@torch.compile +def flatten_sort_count(expert_idxs: torch.Tensor, num_experts: int): + with torch.no_grad(): + flattened_expert_idxs = expert_idxs.flatten() + sorted_expert_idxs, sorted_scattered_idxs = torch.sort(flattened_expert_idxs) + expert_counts = compileable_bincount( + flattened_expert_idxs, minlength=num_experts + ) + expert_offsets = expert_counts.cumsum(-1) + return sorted_expert_idxs, sorted_scattered_idxs, expert_offsets + + +class ParallelLinear(torch.autograd.Function): + @staticmethod + def forward( + ctx, + x: torch.Tensor, + expert_weights: torch.Tensor, + k: int, + sorted_expert_idxs: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + expert_offsets: torch.Tensor, + expert_biases: Optional[torch.Tensor] = None, + gates: Optional[torch.Tensor] = None, + grouped_in: bool = False, + grouped_out: bool = False, + ): + with torch.device(x.device): + output = kernels.ops.scatter2scatter( + X=x, + W=expert_weights, + b=expert_biases, + k=k, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + x_grouped=grouped_in, + y_grouped=grouped_out, + ) + if gates is not None: + output_expanded = output.view( + gates.size(0), gates.size(1), output.size(-1) + ) + output = (gates.unsqueeze(1) @ output_expanded).squeeze(1) + else: + output_expanded = None + + ctx.save_for_backward( + x, + expert_weights, + expert_biases, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + gates, + output_expanded, + ) + ctx.grouped_in = grouped_in + ctx.grouped_out = grouped_out + ctx.k = k + return output + + @staticmethod + def backward(ctx, grad_out: torch.Tensor): + with torch.device(grad_out.device): + ( + x, + expert_weights, + expert_biases, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + gates, + output_expanded, + ) = ctx.saved_tensors + k = ctx.k + grouped_in = ctx.grouped_in + grouped_out = ctx.grouped_out + + if gates is not None: + # calculate gates gradient + # d_gates = torch.bmm(output_expanded, grad_out[:, :, None]).squeeze(-1) + d_gates = (output_expanded @ grad_out.unsqueeze(-1)).squeeze(-1) + gates_flat = gates.flatten() + gate_fan = gates.size(1) + grouped_grad_out = output_expanded.flatten( + 0, 1 + ) # reuse expanded buffer later + else: + d_gates = None + gates_flat = None + gate_fan = 1 + grouped_grad_out = None + + if grouped_out: + grouped_grad_out = grad_out + else: + grouped_grad_out = kernels.ops.group( + grad_out, + sorted_scattered_idxs, + fan_out=gate_fan, + coeff=gates_flat, + out=grouped_grad_out, + ) + if grouped_in: + grouped_x = x + d_expanded_input = None + else: + grouped_x = kernels.ops.group(x, sorted_scattered_idxs, fan_out=k) + d_expanded_input = grouped_x + + d_weights, d_biases = kernels.ops.group_bwd_W( + DY=grouped_grad_out, + X=grouped_x, + expert_offsets=expert_offsets, + E=expert_weights.size(0), + has_bias=expert_biases is not None, + ) + + d_expanded_input = kernels.ops.scatter2scatter( + X=grouped_grad_out, + x_grouped=True, + W=expert_weights.permute(0, 2, 1), + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=1, + y_grouped=grouped_in, + out=d_expanded_input, # Reuse grouped_x buffer + ) + + if k == 1: + d_input = d_expanded_input + else: + d_input = d_expanded_input.view( + x.size(0), k, d_expanded_input.size(-1) + ).sum(-2) + return ( + # x, expert_weights, + d_input, + d_weights, + # k, sorted_expert_idxs, sorted_scattered_idxs, expert_offsets, + None, + None, + None, + None, + # bias, gates + d_biases, + d_gates, + # grouped_in, grouped_out, + None, + None, + ) + + +def parallel_linear( + inputs, + expert_weights, + k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + expert_biases=None, + gates=None, + grouped_in=False, + grouped_out=False, +): + results = ParallelLinear.apply( + inputs, + expert_weights, + k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + expert_biases, + gates, + grouped_in, + grouped_out, + ) + return results + + +class ParallelExperts(nn.Module): + def __init__(self, num_experts, input_size, output_size, bias=False) -> None: + super().__init__() + self.weight = nn.Parameter(torch.empty(num_experts, output_size, input_size)) + + if bias: + self.bias = nn.Parameter(torch.empty(num_experts, output_size)) + else: + self.bias = None + + self.num_experts = num_experts + self.input_size = input_size + self.output_size = output_size + self.reset_parameters() + + def extra_repr(self): + return "num_experts={}, input_size={}, output_size={}".format( + self.num_experts, self.input_size, self.output_size + ) + + def reset_parameters(self) -> None: + nn.init.normal_(self.weight, std=0.02) + if self.bias is not None: + nn.init.zeros_(self.bias) + + def forward( + self, + inputs, + k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + gates=None, + grouped_in=False, + grouped_out=False, + ): + results = parallel_linear( + inputs, + self.weight.permute(0, 2, 1), + k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + expert_biases=self.bias, + gates=gates, + grouped_in=grouped_in, + grouped_out=grouped_out, + ) + return results diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py new file mode 100644 index 0000000000..5d00e1230d --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py @@ -0,0 +1,480 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +ScatterMoE + LoRA Autograd Function +==================================== + +Provides the autograd function and Python interface for fused ScatterMoE + LoRA. + +Key design for LoRA training: + - Expert weights W are FROZEN (no gradient computed for W). + - Only LoRA adapter weights (A, B) receive gradients. + - The input gradient dX is still computed (needed for upstream layers). + - This avoids the expensive group_bwd_W computation entirely. + +Forward: + Y = X @ W + scaling * (X @ A^T) @ B^T + +Backward (W frozen): + dX = dY @ W^T + scaling * (dY @ B) @ A (via scatter2scatter for base, separate for LoRA) + dA = scaling * (dY @ B)^T @ X (per-expert, on grouped data) + dB = scaling * dY^T @ (X @ A^T) (per-expert, on grouped data) +""" + +from typing import Optional + +import torch + +from .kernels import ops as base_ops +from .kernels.lora_ops import ( + group_bwd_lora, + group_bwd_lora_fused, + scatter2scatter_lora, + scatter2scatter_lora_dX, +) + + +class ScatterMoELoRA(torch.autograd.Function): + """ + Autograd function for fused ScatterMoE + LoRA with frozen expert weights. + + This function is optimized for the LoRA fine-tuning scenario where: + - Expert weights W are frozen (requires_grad=False) + - Only LoRA A and B matrices receive gradients + - Input gradients are computed for upstream layer backprop + """ + + @staticmethod + def forward( + ctx, + x: torch.Tensor, + expert_weights: torch.Tensor, + k: int, + sorted_expert_idxs: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + expert_offsets: torch.Tensor, + lora_A: torch.Tensor, + lora_B: torch.Tensor, + scaling: float, + expert_biases: Optional[torch.Tensor] = None, + gates: Optional[torch.Tensor] = None, + grouped_in: bool = False, + grouped_out: bool = False, + use_fused_dX: bool = False, + use_fused_gather: bool = False, + ): + with torch.device(x.device): + # Fused forward: Y = X @ W + scaling * (X @ A^T) @ B^T + output = scatter2scatter_lora( + X=x, + W=expert_weights, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=k, + lora_A=lora_A, + lora_B=lora_B, + scaling=scaling, + b=expert_biases, + x_grouped=grouped_in, + y_grouped=grouped_out, + ) + + # Handle gating (weighted combination of top-k expert outputs) + if gates is not None: + output_expanded = output.view( + gates.size(0), gates.size(1), output.size(-1) + ) + output = (gates.unsqueeze(1) @ output_expanded).squeeze(1) + else: + output_expanded = None + + ctx.save_for_backward( + x, + lora_A, + lora_B, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + gates, + output_expanded, + ) + # Store frozen weights as plain Python attributes instead of + # save_for_backward. This avoids: + # 1. Version-check conflicts with FSDP unshard/reshard + # 2. Pinning all-gathered parameters via saved_tensors hooks + # 3. Interfering with activation offloading pack/unpack hooks + # Safe because expert_weights are frozen (requires_grad=False). + ctx.expert_weights = expert_weights + ctx.expert_biases = expert_biases + ctx.grouped_in = grouped_in + ctx.grouped_out = grouped_out + ctx.k = k + ctx.scaling = scaling + ctx.use_fused_dX = use_fused_dX + ctx.use_fused_gather = use_fused_gather + + return output + + @staticmethod + def backward(ctx, grad_out: torch.Tensor): + with torch.device(grad_out.device): + ( + x, + lora_A, + lora_B, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + gates, + output_expanded, + ) = ctx.saved_tensors + expert_weights = ctx.expert_weights + + k = ctx.k + scaling = ctx.scaling + grouped_in = ctx.grouped_in + grouped_out = ctx.grouped_out + E = expert_weights.size(0) + + # ------------------------------------------------------------------ + # Gate gradients (if using top-k gating with routing weights) + # ------------------------------------------------------------------ + if gates is not None: + # d_gates[t, j] = output_expanded[t, j, :] . grad_out[t, :] + d_gates = (output_expanded @ grad_out.unsqueeze(-1)).squeeze(-1) + gates_flat = gates.flatten() + gate_fan = gates.size(1) + # Reuse output_expanded buffer for grouped_grad_out + grouped_grad_out = output_expanded.flatten(0, 1) + else: + d_gates = None + gates_flat = None + gate_fan = 1 + grouped_grad_out = None + + # ------------------------------------------------------------------ + # LoRA gradients (dA, dB) and setup for dX + # ------------------------------------------------------------------ + # Fused gather uses sorted_scattered_idxs for indirect X access + # in the Triton kernel, avoiding the group(x) allocation. + # + # can_fuse_gather: X is ungrouped and not too large for scatter loads + # - When gates is None and grouped_out=False: both DY and X ungrouped + # - When grouped_out=True (gate_up_proj): DY already grouped, X ungrouped + # -> use dy_grouped=True in the fused kernel + M_total = sorted_scattered_idxs.size(0) + K_dim = x.size(-1) + N_dim = expert_weights.size(-1) + fuse_gather_workload = M_total * max(K_dim, N_dim) + _FUSE_GATHER_THRESHOLD = 2**24 # ~16M elements + + can_fuse_gather = ( + ctx.use_fused_gather + and not grouped_in # X must be ungrouped for scatter access + and gates is None # gate coeff requires multiplicative gather + and fuse_gather_workload < _FUSE_GATHER_THRESHOLD + ) + + if can_fuse_gather: + # ------------------------------------------------------------------ + # Fused path: skip group(x) entirely + # ------------------------------------------------------------------ + d_expanded_input = None + + d_lora_A, d_lora_B = group_bwd_lora_fused( + DY=grad_out, + X=x, + lora_A=lora_A, + lora_B=lora_B, + expert_offsets=expert_offsets, + sorted_scattered_idxs=sorted_scattered_idxs, + E=E, + k=k, + scaling=scaling, + dy_grouped=grouped_out, + ) + + # Prepare grouped_grad_out for the dX path (needed by both + # the fused dX kernel when grouped_out=True, and the non-fused path) + if grouped_out: + grouped_grad_out = grad_out + elif not ctx.use_fused_dX: + grouped_grad_out = base_ops.group( + grad_out, + sorted_scattered_idxs, + fan_out=gate_fan, + coeff=gates_flat, + out=grouped_grad_out, + ) + else: + # ------------------------------------------------------------------ + # Original path: explicit group() calls + # ------------------------------------------------------------------ + if grouped_out: + grouped_grad_out = grad_out + else: + grouped_grad_out = base_ops.group( + grad_out, + sorted_scattered_idxs, + fan_out=gate_fan, + coeff=gates_flat, + out=grouped_grad_out, + ) + + if grouped_in: + grouped_x = x + d_expanded_input = None + else: + grouped_x = base_ops.group(x, sorted_scattered_idxs, fan_out=k) + d_expanded_input = grouped_x # Will be overwritten; reuse buffer + + d_lora_A, d_lora_B = group_bwd_lora( + DY=grouped_grad_out, + X=grouped_x, + lora_A=lora_A, + lora_B=lora_B, + expert_offsets=expert_offsets, + E=E, + scaling=scaling, + ) + + # ------------------------------------------------------------------ + # Input gradient: dX = dY @ W^T + scaling * (dY @ B) @ A + # ------------------------------------------------------------------ + if ctx.use_fused_dX: + if can_fuse_gather and not grouped_out: + # Fully fused: read ungrouped DY via scatter pattern + d_expanded_input = scatter2scatter_lora_dX( + DY=grad_out, + W=expert_weights, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=1, + lora_A=lora_A, + lora_B=lora_B, + scaling=scaling, + dy_grouped=False, + dx_grouped=grouped_in, + out=d_expanded_input, + ) + else: + # Fused dX only: read from pre-grouped DY + d_expanded_input = scatter2scatter_lora_dX( + DY=grouped_grad_out, + W=expert_weights, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=1, + lora_A=lora_A, + lora_B=lora_B, + scaling=scaling, + dy_grouped=True, + dx_grouped=grouped_in, + out=d_expanded_input, + ) + else: + # Original path: separate base scatter2scatter + LoRA Python loop + d_expanded_input = base_ops.scatter2scatter( + X=grouped_grad_out, + x_grouped=True, + W=expert_weights.permute(0, 2, 1), # [E, N, K] + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=1, + y_grouped=grouped_in, + out=d_expanded_input, + ) + + # LoRA part: dX_lora = scaling * (dY @ B) @ A + if scaling != 0.0: + d_input_lora_grouped = _compute_lora_input_grad( + grouped_grad_out, + lora_A, + lora_B, + expert_offsets, + E, + scaling, + ) + if grouped_in: + d_expanded_input.add_(d_input_lora_grouped) + else: + # Scatter-add LoRA gradient directly into d_expanded_input. + # Avoids allocating a zeros_like + add result + d_expanded_input[sorted_scattered_idxs] += d_input_lora_grouped + + # Reduce over top-k if k > 1 + if k == 1: + d_input = d_expanded_input + else: + d_input = d_expanded_input.view( + x.size(0), k, d_expanded_input.size(-1) + ).sum(-2) + + # W is frozen during LoRA training -- skip weight gradient + d_weights = ( + torch.zeros_like(expert_weights) + if expert_weights.requires_grad + else None + ) + d_biases = None + + return ( + d_input, + d_weights, + None, + None, + None, + None, # k, sorted indices, offsets + d_lora_A, + d_lora_B, + None, # lora_A, lora_B, scaling + d_biases, + d_gates, + None, + None, # grouped_in, grouped_out + None, # use_fused_dX + None, # use_fused_gather + ) + + +def _compute_lora_input_grad( + grouped_grad_out: torch.Tensor, + lora_A: torch.Tensor, + lora_B: torch.Tensor, + expert_offsets: torch.Tensor, + E: int, + scaling: float, +) -> torch.Tensor: + """ + Compute the LoRA contribution to the input gradient: + dX_lora = scaling * (dY @ B) @ A + + Uses PyTorch ops on expert-grouped data. + Each expert e: dX_e = scaling * (dY_e @ B_e) @ A_e + """ + R = lora_A.size(0) // E + K = lora_A.size(1) + M_total = grouped_grad_out.size(0) + + d_input_lora = torch.zeros( + (M_total, K), device=grouped_grad_out.device, dtype=grouped_grad_out.dtype + ) + + compute_dtype = grouped_grad_out.dtype + + prev_offset = 0 + for e in range(E): + curr_offset = expert_offsets[e].item() + if curr_offset > prev_offset: + dy_e = grouped_grad_out[prev_offset:curr_offset] # [M_e, N] + a_e = lora_A[e * R : (e + 1) * R, :].to(compute_dtype) # [r, K] + b_e = lora_B[:, e * R : (e + 1) * R].to(compute_dtype) # [N, r] + + # dX_e = scaling * (dY_e @ B_e) @ A_e + dy_b = dy_e @ b_e # [M_e, r] + dx_e = scaling * (dy_b @ a_e) # [M_e, K] + d_input_lora[prev_offset:curr_offset] = dx_e + + prev_offset = curr_offset + + return d_input_lora + + +# ============================================================================= +# Helper: Extract LoRA params from PEFT ParamWrapper +# ============================================================================= + + +def get_lora_params_from_wrapper(module) -> tuple: + """ + Extract LoRA parameters from a PEFT ParamWrapper. + + Returns: + (lora_A, lora_B, scaling) if LoRA is active, else (None, None, None) + """ + if not hasattr(module, "lora_A") or not hasattr(module, "lora_B"): + return None, None, None + + active_adapters = getattr(module, "active_adapters", ["default"]) + if not active_adapters: + return None, None, None + + adapter_name = active_adapters[0] + + lora_A_dict = getattr(module, "lora_A", {}) + lora_B_dict = getattr(module, "lora_B", {}) + scaling_dict = getattr(module, "scaling", {}) + + if adapter_name not in lora_A_dict: + return None, None, None + + lora_A = lora_A_dict[adapter_name].weight + lora_B = lora_B_dict[adapter_name].weight + scaling = scaling_dict[adapter_name] + + return lora_A, lora_B, scaling + + +# ============================================================================= +# Drop-in replacement for parallel_linear +# ============================================================================= + + +def parallel_linear_lora( + inputs: torch.Tensor, + expert_weights: torch.Tensor, + k: int, + sorted_expert_idxs: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + expert_offsets: torch.Tensor, + lora_A: Optional[torch.Tensor] = None, + lora_B: Optional[torch.Tensor] = None, + scaling: float = 1.0, + expert_biases: Optional[torch.Tensor] = None, + gates: Optional[torch.Tensor] = None, + grouped_in: bool = False, + grouped_out: bool = False, + use_fused_dX: bool = False, + use_fused_gather: bool = False, +): + """ + Drop-in replacement for parallel_linear that supports LoRA. + + If lora_A and lora_B are provided, uses fused LoRA kernel. + Otherwise falls back to standard scatter2scatter. + """ + if lora_A is not None and lora_B is not None: + return ScatterMoELoRA.apply( + inputs, + expert_weights, + k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + lora_A, + lora_B, + scaling, + expert_biases, + gates, + grouped_in, + grouped_out, + use_fused_dX, + use_fused_gather, + ) + else: + from .parallel_experts import ParallelLinear + + return ParallelLinear.apply( + inputs, + expert_weights, + k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + expert_biases, + gates, + grouped_in, + grouped_out, + ) diff --git a/src/axolotl/integrations/kernels/plugin.py b/src/axolotl/integrations/kernels/plugin.py index c7fb79ff64..56d0448d5f 100644 --- a/src/axolotl/integrations/kernels/plugin.py +++ b/src/axolotl/integrations/kernels/plugin.py @@ -1,5 +1,7 @@ +from pathlib import Path + from kernels import ( - LayerRepository, + LocalLayerRepository, Mode, register_kernel_mapping, replace_kernel_forward_from_hub, @@ -19,16 +21,19 @@ def pre_model_load(self, cfg): self._kernelize_model(cfg.model_config_type) def _register_kernels(self): + plugin_root = Path(__file__).parent register_kernel_mapping( { "HFScatterMoEParallelExperts": { "cuda": { - Mode.TRAINING: LayerRepository( - repo_id="axolotl-ai-co/scattermoe", + Mode.TRAINING: LocalLayerRepository( + repo_path=plugin_root / "libs" / "scattermoe_lora", + package_name="scattermoe_lora", layer_name="HFScatterMoEGatedMLP", ), - Mode.INFERENCE: LayerRepository( - repo_id="axolotl-ai-co/scattermoe", + Mode.INFERENCE: LocalLayerRepository( + repo_path=plugin_root / "libs" / "scattermoe_lora", + package_name="scattermoe_lora", layer_name="HFScatterMoEGatedMLP", ), }, diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 2222600200..62dcbde7af 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -329,7 +329,7 @@ def _apply_multipack_patches(self): else: has_remote_code = False - if has_remote_code and self.cfg.trust_remote_code is False: + if has_remote_code and self.cfg.trust_remote_code is not None: # If explicitly set in YAML, prefer that has_remote_code = self.cfg.trust_remote_code diff --git a/src/axolotl/utils/data/lock.py b/src/axolotl/utils/data/lock.py index afd1547af6..9699f60e80 100644 --- a/src/axolotl/utils/data/lock.py +++ b/src/axolotl/utils/data/lock.py @@ -54,15 +54,19 @@ def _increment_counter(self): def cleanup(self): """Clean up ready flag when last process is done.""" - with FileLock(str(self.lock_file_path)): - counter_content = self.counter_path.read_text().strip() - count = int(counter_content) if counter_content else 0 - count -= 1 + try: + with FileLock(str(self.lock_file_path)): + counter_content = self.counter_path.read_text().strip() + count = int(counter_content) if counter_content else 0 + count -= 1 - if count <= 0: - # Last process cleans everything up - self.ready_flag_path.unlink(missing_ok=True) - self.counter_path.unlink(missing_ok=True) - else: - # Still have active processes - self.counter_path.write_text(str(count)) + if count <= 0: + # Last process cleans everything up + self.ready_flag_path.unlink(missing_ok=True) + self.counter_path.unlink(missing_ok=True) + else: + # Still have active processes + self.counter_path.write_text(str(count)) + except FileNotFoundError: + # Lock file might have already been deleted by another process + pass diff --git a/tests/integrations/test_scattermoe_lora.py b/tests/integrations/test_scattermoe_lora.py new file mode 100644 index 0000000000..859119c819 --- /dev/null +++ b/tests/integrations/test_scattermoe_lora.py @@ -0,0 +1,323 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +Unit tests for scattermoe-lora code-review fixes. + +Tests cover: +- KernelsArgs validator: disable_mlp_kernel_scattermoe +- CPU_Offloaded_Gradient_Checkpointer: tuple vs plain tensor backward +- ParallelExperts: scaling=0.0 not treated as falsy +- single2scatter: non-aligned K/N dimensions +- group_compileable: coeff=None accepted +- HFScatterMoEGatedMLP / ScatterMoEGatedMLP: return value contract +""" + +from unittest.mock import patch + +import pytest +import torch + +# ============================================================================ +# 1. KernelsArgs: disable_mlp_kernel_scattermoe validator +# ============================================================================ + + +class TestKernelsArgsValidator: + """Test that disable_mlp_kernel_scattermoe sets both flags correctly. + + These tests call the validator classmethod directly on raw dicts, + since lora_mlp_kernel / mlp_kernel are not declared model fields. + """ + + def test_disables_lora_mlp_kernel_when_scattermoe(self): + """lora_mlp_kernel=True gets set to False when use_scattermoe=True.""" + from axolotl.integrations.kernels.args import KernelsArgs + + data = { + "use_kernels": True, + "use_scattermoe": True, + "lora_mlp_kernel": True, + } + result = KernelsArgs.disable_mlp_kernel_scattermoe(data) + assert result["lora_mlp_kernel"] is False + assert result["mlp_kernel"] is False + + def test_mlp_kernel_disabled_without_lora(self): + """Even without lora_mlp_kernel, mlp_kernel should be disabled.""" + from axolotl.integrations.kernels.args import KernelsArgs + + data = { + "use_kernels": True, + "use_scattermoe": True, + } + result = KernelsArgs.disable_mlp_kernel_scattermoe(data) + assert result["mlp_kernel"] is False + # lora_mlp_kernel was not in data, should not be added + assert "lora_mlp_kernel" not in result + + def test_lora_mlp_kernel_false_unchanged(self): + """lora_mlp_kernel=False should stay False (no warning, no change).""" + from axolotl.integrations.kernels.args import KernelsArgs + + data = { + "use_kernels": True, + "use_scattermoe": True, + "lora_mlp_kernel": False, + } + result = KernelsArgs.disable_mlp_kernel_scattermoe(data) + assert result["lora_mlp_kernel"] is False + + def test_no_change_when_scattermoe_disabled(self): + """When use_scattermoe is not True, nothing should be changed.""" + from axolotl.integrations.kernels.args import KernelsArgs + + data = { + "use_kernels": True, + "use_scattermoe": False, + "lora_mlp_kernel": True, + } + result = KernelsArgs.disable_mlp_kernel_scattermoe(data) + assert result["lora_mlp_kernel"] is True + + +class TestParallelExpertsScaling: + """Test that scaling=0.0 is preserved and not overridden to 1.0.""" + + def test_scaling_zero_preserved(self): + """scaling=0.0 should be passed as 0.0, not replaced with 1.0.""" + pytest.importorskip("triton") + from axolotl.integrations.kernels.libs.scattermoe_lora.lora_ops import ( + ParallelExperts, + ) + + pe = ParallelExperts(num_experts=2, input_size=4, output_size=4) + pe.set_lora( + lora_A=torch.randn(4, 4), + lora_B=torch.randn(4, 4), + scaling=0.0, + ) + assert pe._lora_scaling == 0.0 + + # Patch parallel_linear_lora to capture the scaling arg + with patch( + "axolotl.integrations.kernels.libs.scattermoe_lora.lora_ops.parallel_linear_lora" + ) as mock_pll: + mock_pll.return_value = torch.randn(4, 4) + # Create dummy routing tensors + pe.forward( + inputs=torch.randn(2, 4), + k=1, + sorted_expert_idxs=torch.tensor([0, 0, 1, 1]), + sorted_scattered_idxs=torch.tensor([0, 1, 0, 1]), + expert_offsets=torch.tensor([2, 4]), + ) + # Check that scaling=0.0 was passed, not 1.0 + call_kwargs = mock_pll.call_args + assert ( + call_kwargs.kwargs.get("scaling") == 0.0 + or call_kwargs[1].get("scaling") == 0.0 + ), f"Expected scaling=0.0 but got {call_kwargs}" + + def test_scaling_none_defaults_to_one(self): + """scaling=None (no LoRA attached) should default to 1.0.""" + pytest.importorskip("triton") + from axolotl.integrations.kernels.libs.scattermoe_lora.lora_ops import ( + ParallelExperts, + ) + + pe = ParallelExperts(num_experts=2, input_size=4, output_size=4) + # No set_lora called, so _lora_scaling is None + + with patch( + "axolotl.integrations.kernels.libs.scattermoe_lora.lora_ops.parallel_linear_lora" + ) as mock_pll: + mock_pll.return_value = torch.randn(4, 4) + pe.forward( + inputs=torch.randn(2, 4), + k=1, + sorted_expert_idxs=torch.tensor([0, 0, 1, 1]), + sorted_scattered_idxs=torch.tensor([0, 1, 0, 1]), + expert_offsets=torch.tensor([2, 4]), + ) + call_kwargs = mock_pll.call_args + scaling_val = call_kwargs.kwargs.get("scaling") or call_kwargs[1].get( + "scaling" + ) + assert scaling_val == 1.0, ( + f"Expected scaling=1.0 for None but got {scaling_val}" + ) + + def test_scaling_positive_preserved(self): + """Normal positive scaling should be preserved.""" + pytest.importorskip("triton") + from axolotl.integrations.kernels.libs.scattermoe_lora.lora_ops import ( + ParallelExperts, + ) + + pe = ParallelExperts(num_experts=2, input_size=4, output_size=4) + pe.set_lora( + lora_A=torch.randn(4, 4), + lora_B=torch.randn(4, 4), + scaling=0.5, + ) + + with patch( + "axolotl.integrations.kernels.libs.scattermoe_lora.lora_ops.parallel_linear_lora" + ) as mock_pll: + mock_pll.return_value = torch.randn(4, 4) + pe.forward( + inputs=torch.randn(2, 4), + k=1, + sorted_expert_idxs=torch.tensor([0, 0, 1, 1]), + sorted_scattered_idxs=torch.tensor([0, 1, 0, 1]), + expert_offsets=torch.tensor([2, 4]), + ) + call_kwargs = mock_pll.call_args + scaling_val = call_kwargs.kwargs.get("scaling") or call_kwargs[1].get( + "scaling" + ) + assert scaling_val == 0.5 + + +# ============================================================================ +# 4. single2scatter: non-aligned K/N dimensions (GPU only) +# ============================================================================ + + +@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") +class TestSingle2ScatterBounds: + """Test single2scatter with non-aligned dimensions.""" + + def test_non_aligned_k(self): + """K not a multiple of BLOCK_K should produce correct results.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.kernels.single import ( + single2scatter, + ) + + E, K, N = 2, 100, 128 # K=100 not a multiple of 128 + W = torch.randn(E, K, N, device="cuda", dtype=torch.float32) + X = torch.randn(1, K, device="cuda", dtype=torch.float32) + expert_idxs = torch.tensor([[0, 1]], device="cuda", dtype=torch.long) + + Y = single2scatter(X, W, expert_idxs) + assert Y.shape == (2, N) + + # Verify against manual computation + Y_ref_0 = X[0] @ W[0] + Y_ref_1 = X[0] @ W[1] + torch.testing.assert_close(Y[0], Y_ref_0, atol=1e-2, rtol=1e-2) + torch.testing.assert_close(Y[1], Y_ref_1, atol=1e-2, rtol=1e-2) + + def test_non_aligned_n(self): + """N not a multiple of BLOCK_N should produce correct results.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.kernels.single import ( + single2scatter, + ) + + E, K, N = 2, 128, 100 # N=100 not a multiple of 128 + W = torch.randn(E, K, N, device="cuda", dtype=torch.float32) + X = torch.randn(1, K, device="cuda", dtype=torch.float32) + expert_idxs = torch.tensor([[0, 1]], device="cuda", dtype=torch.long) + + Y = single2scatter(X, W, expert_idxs) + assert Y.shape == (2, N) + + Y_ref_0 = X[0] @ W[0] + Y_ref_1 = X[0] @ W[1] + torch.testing.assert_close(Y[0], Y_ref_0, atol=1e-2, rtol=1e-2) + torch.testing.assert_close(Y[1], Y_ref_1, atol=1e-2, rtol=1e-2) + + def test_non_aligned_both(self): + """Both K and N not aligned should produce correct results.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.kernels.single import ( + single2scatter, + ) + + E, K, N = 2, 100, 100 # Neither aligned to 128 + W = torch.randn(E, K, N, device="cuda", dtype=torch.float32) + X = torch.randn(1, K, device="cuda", dtype=torch.float32) + expert_idxs = torch.tensor([[0, 1]], device="cuda", dtype=torch.long) + + Y = single2scatter(X, W, expert_idxs) + assert Y.shape == (2, N) + + Y_ref_0 = X[0] @ W[0] + Y_ref_1 = X[0] @ W[1] + torch.testing.assert_close(Y[0], Y_ref_0, atol=1e-2, rtol=1e-2) + torch.testing.assert_close(Y[1], Y_ref_1, atol=1e-2, rtol=1e-2) + + +# ============================================================================ +# 5. group_compileable: coeff=None accepted +# ============================================================================ + + +@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") +class TestGroupCoeffNone: + """Test that group() works with coeff=None.""" + + def test_group_with_none_coeff(self): + """group() should accept coeff=None without errors.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.kernels.ops import group + + M, K = 4, 32 + A = torch.randn(M, K, device="cuda", dtype=torch.float32) + sorted_expert_idxs = torch.tensor([0, 1, 2, 3], device="cuda", dtype=torch.long) + + # This should not raise a TypeError + Y = group(A, sorted_expert_idxs, coeff=None, fan_out=1) + assert Y.shape == (M, K) + + def test_group_with_coeff(self): + """group() should also work with actual coeff values.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.kernels.ops import group + + M, K = 4, 32 + A = torch.randn(M, K, device="cuda", dtype=torch.float32) + sorted_expert_idxs = torch.tensor([0, 1, 2, 3], device="cuda", dtype=torch.long) + coeff = torch.ones(M, device="cuda", dtype=torch.float32) * 0.5 + + Y = group(A, sorted_expert_idxs, coeff=coeff, fan_out=1) + assert Y.shape == (M, K) + + +# ============================================================================ +# 6. Layer return value contracts +# ============================================================================ + + +class TestLayerReturnValues: + """Test that layer forward methods return the correct types.""" + + def test_hf_scatter_moe_returns_single_tensor(self): + """HFScatterMoEGatedMLP.forward should return a single tensor, not a tuple.""" + pytest.importorskip("triton") + # Verify the forward method signature and return annotation + import inspect + + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + HFScatterMoEGatedMLP, + ) + + sig = inspect.signature(HFScatterMoEGatedMLP.forward) + # It's a staticmethod taking (self, layer_input) + params = list(sig.parameters.keys()) + assert "self" in params + assert "layer_input" in params + + def test_scatter_moe_gated_mlp_docstring_no_router_logits(self): + """ScatterMoEGatedMLP.forward docstring should not mention router logits as return.""" + pytest.importorskip("triton") + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + ScatterMoEGatedMLP, + ) + + docstring = ScatterMoEGatedMLP.forward.__doc__ + assert docstring is not None + # The docstring should mention output tensor but NOT router logits + assert "Output tensor" in docstring or "output tensor" in docstring.lower() + assert "Router logits" not in docstring, ( + "Docstring should not mention 'Router logits' in Returns section" + ) From b40803da510208a4591b19da7d7cacef5c04a82f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 24 Feb 2026 20:32:34 -0500 Subject: [PATCH 1138/1405] build base images for torch 2.10.0 (#3429) --- .github/workflows/base.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 22125c316b..5f97129052 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -51,6 +51,14 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" platforms: "linux/amd64,linux/arm64" + - cuda: "128" + cuda_version: 12.8.1 + cudnn_version: "" + python_version: "3.12" + pytorch: 2.10.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-base" + platforms: "linux/amd64,linux/arm64" - cuda: "129" cuda_version: 12.9.1 cudnn_version: "" @@ -75,6 +83,14 @@ jobs: torch_cuda_arch_list: "9.0+PTX" dockerfile: "Dockerfile-base" platforms: "linux/amd64,linux/arm64" + - cuda: "130" + cuda_version: 13.0.0 + cudnn_version: "" + python_version: "3.12" + pytorch: 2.10.0 + torch_cuda_arch_list: "9.0+PTX" + dockerfile: "Dockerfile-base" + platforms: "linux/amd64,linux/arm64" # - cuda: "128" # cuda_version: 12.8.1 # cudnn_version: "" @@ -157,6 +173,14 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" + - cuda: "128" + cuda_version: 12.8.1 + cudnn_version: "" + python_version: "3.12" + pytorch: 2.10.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-uv-base" + platforms: "linux/amd64,linux/arm64" - cuda: "129" cuda_version: 12.9.1 cudnn_version: "" @@ -181,6 +205,14 @@ jobs: torch_cuda_arch_list: "9.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" + - cuda: "130" + cuda_version: 13.0.0 + cudnn_version: "" + python_version: "3.12" + pytorch: 2.10.0 + torch_cuda_arch_list: "9.0+PTX" + dockerfile: "Dockerfile-uv-base" + platforms: "linux/amd64,linux/arm64" steps: - name: Checkout uses: actions/checkout@v4 From 1791d87b6f83174c80c580abf287bc31f843168a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 24 Feb 2026 22:35:25 -0500 Subject: [PATCH 1139/1405] build axolotl images with torch 2.10.0 (#3430) --- .github/workflows/main.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e081f21272..b8ebe2aeb7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,6 +34,12 @@ jobs: axolotl_extras: platforms: "linux/amd64,linux/arm64" is_latest: true + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.12" + pytorch: 2.10.0 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" - cuda: 129 cuda_version: 12.9.1 python_version: "3.12" @@ -46,6 +52,12 @@ jobs: pytorch: 2.9.1 axolotl_extras: platforms: "linux/amd64,linux/arm64" + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.12" + pytorch: 2.10.0 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -112,6 +124,12 @@ jobs: axolotl_extras: is_latest: true platforms: "linux/amd64,linux/arm64" + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.12" + pytorch: 2.10.0 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" - cuda: 129 cuda_version: 12.9.1 python_version: "3.12" @@ -124,6 +142,12 @@ jobs: pytorch: 2.9.1 axolotl_extras: platforms: "linux/amd64,linux/arm64" + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.12" + pytorch: 2.10.0 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" runs-on: axolotl-gpu-runner steps: - name: Checkout From a131e4d0e5cb9594b709b906200b53d983ea97fc Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:40:57 +0530 Subject: [PATCH 1140/1405] sample gen support sft (#3240) [skip ci] * add:parameters + callback * sft core + logging * indentation fix * logger fix * loger fix in sft * gen sample on eval * lint * deprecation --- src/axolotl/core/builders/causal.py | 6 + src/axolotl/utils/callbacks/generation.py | 84 +++++++++++ src/axolotl/utils/generation/__init__.py | 5 + src/axolotl/utils/generation/sft.py | 174 ++++++++++++++++++++++ src/axolotl/utils/schemas/config.py | 52 +++++-- src/axolotl/utils/schemas/deprecated.py | 23 +++ 6 files changed, 332 insertions(+), 12 deletions(-) create mode 100644 src/axolotl/utils/callbacks/generation.py create mode 100644 src/axolotl/utils/generation/__init__.py create mode 100644 src/axolotl/utils/generation/sft.py diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 7bfc5e874f..c238cbbc3f 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -122,6 +122,12 @@ def get_post_trainer_create_callbacks(self, trainer): ColabCallback = colab_inference_post_train_callback(trainer) callbacks.append(ColabCallback(self.cfg)) + if getattr(self.cfg, "generate_samples", False): + from axolotl.utils.callbacks.generation import SFTGenerationCallback + + callbacks.append(SFTGenerationCallback(trainer)) + LOG.info("SFT sample generation enabled") + callbacks.extend(super().get_post_trainer_create_callbacks(trainer=trainer)) return callbacks diff --git a/src/axolotl/utils/callbacks/generation.py b/src/axolotl/utils/callbacks/generation.py new file mode 100644 index 0000000000..439258c8b9 --- /dev/null +++ b/src/axolotl/utils/callbacks/generation.py @@ -0,0 +1,84 @@ +"""Callback for generating samples during SFT/Pretrain training.""" + +from transformers.trainer_callback import TrainerCallback, TrainerControl, TrainerState +from transformers.training_args import TrainingArguments + +from axolotl.utils.generation.sft import generate_samples +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +class SFTGenerationCallback(TrainerCallback): + """Callback for generating samples during SFT/Pretrain training.""" + + def __init__(self, trainer): + self.trainer = trainer + + def on_evaluate( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + """Generate samples at specified intervals.""" + cfg = self.trainer.axolotl_cfg + + if not getattr(cfg, "generate_samples", False): + return + + dataloader = None + try: + if getattr(self.trainer, "eval_dataset", None) is not None: + dataloader = self.trainer.get_eval_dataloader() + LOG.info( + f"Using eval dataloader for generation at step {state.global_step}" + ) + except Exception as e: + LOG.warning(f"Could not get eval dataloader: {e}") + dataloader = None + + if dataloader is None: + dataloader = self.trainer.get_train_dataloader() + LOG.info( + f"Using train dataloader for generation at step {state.global_step}" + ) + + samples = generate_samples( + model=self.trainer.model, + tokenizer=self.trainer.processing_class, + dataloader=dataloader, + num_generation_samples=getattr(cfg, "num_generation_samples", 3), + max_new_tokens=getattr(cfg, "generation_max_new_tokens", 50), + temperature=getattr(cfg, "generation_temperature", 0.7), + top_p=getattr(cfg, "generation_top_p", None), + top_k=getattr(cfg, "generation_top_k", None), + do_sample=getattr(cfg, "generation_do_sample", True), + prompt_ratio=getattr(cfg, "generation_prompt_ratio", 0.5), + ) + self._log_samples(samples, state.global_step) + + def _log_samples(self, samples: list, step: int): + """Log generated samples to console and W&B.""" + from axolotl.utils.generation.sft import format_generation_for_logging + + for i, sample in enumerate(samples): + console_text, wandb_text = format_generation_for_logging(sample, i, step) + + LOG.info(console_text) + + try: + import wandb + + if wandb.run is not None: + wandb.log( + { + f"samples/sample_{i + 1}": wandb.Html( + f"
{wandb_text}
" + ) + }, + step=step, + ) + except (ImportError, Exception): + pass diff --git a/src/axolotl/utils/generation/__init__.py b/src/axolotl/utils/generation/__init__.py new file mode 100644 index 0000000000..7a222d18d4 --- /dev/null +++ b/src/axolotl/utils/generation/__init__.py @@ -0,0 +1,5 @@ +"""Generation utilities for monitoring during training.""" + +from .sft import format_generation_for_logging, generate_samples + +__all__ = ["generate_samples", "format_generation_for_logging"] diff --git a/src/axolotl/utils/generation/sft.py b/src/axolotl/utils/generation/sft.py new file mode 100644 index 0000000000..70fff80c59 --- /dev/null +++ b/src/axolotl/utils/generation/sft.py @@ -0,0 +1,174 @@ +"""Sample generation utilities for SFT/Pretrain training.""" + +from typing import Any, List, Optional + +import torch +from accelerate.utils import extract_model_from_parallel +from colorama import Fore, Style + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def generate_samples( + model: torch.nn.Module, + tokenizer: Any, + dataloader: Any, + num_generation_samples: int = 3, + max_new_tokens: int = 50, + temperature: float = 0.7, + top_p: Optional[float] = None, + top_k: Optional[int] = None, + do_sample: bool = True, + prompt_ratio: float = 0.5, +) -> List[dict]: + """ + Generate samples from the model during training for monitoring. + + Args: + model: The model to generate from + tokenizer: The tokenizer to use for encoding/decoding + dataloader: Dataloader to sample prompts from + num_generation_samples: Number of samples to generate + max_new_tokens: Maximum new tokens to generate + temperature: Sampling temperature (0.0 = greedy) + top_p: Nucleus sampling parameter + top_k: Top-k sampling parameter + do_sample: Whether to use sampling vs greedy decoding + prompt_ratio: Ratio of sequence to use as prompt (0.0-1.0) + + Returns: + List of dicts with 'prompt', 'generated', and 'full_text' keys + """ + unwrapped_model = extract_model_from_parallel(model) + + training = unwrapped_model.training + unwrapped_model.eval() + + device = next(unwrapped_model.parameters()).device + + generations = [] + + try: + with torch.no_grad(): + samples_collected = 0 + + for batch in dataloader: + if samples_collected >= num_generation_samples: + break + + input_ids = batch["input_ids"].to(device) + attention_mask = batch.get("attention_mask") + if attention_mask is not None: + attention_mask = attention_mask.to(device) + batch_size = input_ids.shape[0] + + indices = torch.randperm(batch_size)[ + : num_generation_samples - samples_collected + ] + + for idx in indices: + if samples_collected >= num_generation_samples: + break + + sequence = input_ids[idx] + + if attention_mask is not None: + seq_len = attention_mask[idx].sum().item() + else: + seq_len = sequence.shape[0] + + if seq_len < 5: + continue + + prompt_len = max(1, int(seq_len * prompt_ratio)) + prompt_ids = sequence[:prompt_len].unsqueeze(0) + + try: + generation_config = { + "max_new_tokens": max_new_tokens, + "do_sample": do_sample, + "pad_token_id": tokenizer.pad_token_id + if tokenizer.pad_token_id is not None + else tokenizer.eos_token_id, + } + + if do_sample: + generation_config["temperature"] = temperature + if top_p is not None: + generation_config["top_p"] = top_p + if top_k is not None: + generation_config["top_k"] = top_k + + generated_ids = unwrapped_model.generate( + prompt_ids, **generation_config + ) + + prompt_text = tokenizer.decode( + prompt_ids[0], skip_special_tokens=True + ) + generated_text = tokenizer.decode( + generated_ids[0][prompt_len:], skip_special_tokens=True + ) + full_text = tokenizer.decode( + generated_ids[0], skip_special_tokens=True + ) + + generations.append( + { + "prompt": prompt_text, + "generated": generated_text, + "full_text": full_text, + } + ) + + samples_collected += 1 + + except Exception as e: + LOG.warning(f"Failed to generate sample: {e}", exc_info=True) + continue + + except Exception as e: + LOG.warning(f"Error during sample generation: {e}", exc_info=True) + + if training: + unwrapped_model.train() + else: + unwrapped_model.eval() + + return generations + + +def format_generation_for_logging( + sample: dict, sample_idx: int, step: int +) -> tuple[str, str]: + """ + Format a generation sample for pretty logging. + + Args: + sample: Dict with 'prompt', 'generated', and 'full_text' keys + sample_idx: Index of the sample + step: Current training step + + Returns: + Tuple of (console_text, wandb_text) + """ + console_text = ( + f"\n{Style.BRIGHT}{Fore.CYAN}{'=' * 80}{Style.RESET_ALL}\n" + f"{Style.BRIGHT}{Fore.GREEN}Sample {sample_idx + 1} (Step {step}){Style.RESET_ALL}\n" + f"{Style.BRIGHT}{Fore.CYAN}{'=' * 80}{Style.RESET_ALL}\n" + f"{Style.BRIGHT}{Fore.YELLOW}[PROMPT]{Style.RESET_ALL}\n{sample['prompt']}\n\n" + f"{Style.BRIGHT}{Fore.MAGENTA}[GENERATED]{Style.RESET_ALL}\n{sample['generated']}\n" + f"{Style.BRIGHT}{Fore.CYAN}{'=' * 80}{Style.RESET_ALL}\n" + ) + wandb_text = ( + f"\n{'=' * 80}\n" + f"Sample {sample_idx + 1} (Step {step})\n" + f"{'=' * 80}\n" + f"[PROMPT]\n{sample['prompt']}\n\n" + f"[GENERATED]\n{sample['generated']}\n" + f"{'=' * 80}\n" + ) + + return console_text, wandb_text diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index f1627367be..35b4a69081 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -338,18 +338,6 @@ class AxolotlInputConfig( ) ddp_find_unused_parameters: bool | None = None - eval_table_size: int | None = Field( - default=None, - json_schema_extra={ - "description": "Approximate number of predictions sent to wandb depending on batch size. Enabled above 0. Default is 0" - }, - ) - eval_max_new_tokens: int | None = Field( - default=None, - json_schema_extra={ - "description": "Total number of tokens generated for predictions sent to wandb. Default is 128" - }, - ) do_causal_lm_eval: bool | None = Field( default=None, json_schema_extra={ @@ -1106,6 +1094,46 @@ class AxolotlInputConfig( "description": "Add plugins to extend the pipeline. See `src/axolotl/integrations` for the available plugins or doc below for more details. https://docs.axolotl.ai/docs/custom_integrations.html" }, ) + generate_samples: bool | None = Field( + default=False, + json_schema_extra={ + "description": "Enable sample generation during training for monitoring" + }, + ) + num_generation_samples: int | None = Field( + default=3, + json_schema_extra={ + "description": "Number of samples to generate at each interval" + }, + ) + generation_max_new_tokens: int | None = Field( + default=50, + json_schema_extra={"description": "Maximum new tokens to generate per sample"}, + ) + generation_temperature: float | None = Field( + default=0.7, + json_schema_extra={ + "description": "Temperature for sample generation (0.0 = greedy)" + }, + ) + generation_top_p: float | None = Field( + default=None, + json_schema_extra={"description": "Nucleus sampling parameter for generation"}, + ) + generation_top_k: int | None = Field( + default=None, + json_schema_extra={"description": "Top-k sampling parameter for generation"}, + ) + generation_prompt_ratio: float | None = Field( + default=0.5, + json_schema_extra={"description": "Ratio of input to use as prompt (0.0-1.0)"}, + ) + generation_do_sample: bool | None = Field( + default=True, + json_schema_extra={ + "description": "Whether to use sampling (vs greedy decoding)" + }, + ) @field_serializer("datasets") def datasets_serializer( diff --git a/src/axolotl/utils/schemas/deprecated.py b/src/axolotl/utils/schemas/deprecated.py index 972fe0ccfb..9dfe692645 100644 --- a/src/axolotl/utils/schemas/deprecated.py +++ b/src/axolotl/utils/schemas/deprecated.py @@ -17,6 +17,8 @@ class DeprecatedParameters(BaseModel): noisy_embedding_alpha: float | None = None dpo_beta: float | None = None evaluation_strategy: str | None = None + eval_table_size: int | None = None + eval_max_new_tokens: int | None = None @field_validator("max_packed_sequence_len") @classmethod @@ -55,6 +57,27 @@ def validate_evaluation_strategy(cls, evaluation_strategy): LOG.warning("evaluation_strategy is deprecated, use eval_strategy instead") return evaluation_strategy + @field_validator("eval_table_size") + @classmethod + def validate_eval_table_size(cls, eval_table_size): + if eval_table_size is not None: + LOG.warning( + "eval_table_size is deprecated and superseded by generate_samples config. " + "Please use generate_samples: true and num_generation_samples instead. " + "The LogPredictionCallback is replaced by the new sample generation feature." + ) + return eval_table_size + + @field_validator("eval_max_new_tokens") + @classmethod + def validate_eval_max_new_tokens(cls, eval_max_new_tokens): + if eval_max_new_tokens is not None: + LOG.warning( + "eval_max_new_tokens is deprecated and superseded by generate_samples config. " + "Please use generation_max_new_tokens instead." + ) + return eval_max_new_tokens + class RemappedParameters(BaseModel): """Parameters that have been remapped to other names""" From 8f54b4eb255c5983b791f1e3043d0420bae1840b Mon Sep 17 00:00:00 2001 From: madScientist10 <42779409+madScientist10@users.noreply.github.com> Date: Wed, 25 Feb 2026 06:11:20 +0200 Subject: [PATCH 1141/1405] fix: pass revision parameter to tokenizer and processor loaders (#3388) [skip ci] * fix: pass revision parameter to tokenizer and processor loaders * fix: address revision=None passed to .from_pretrained * add tests and address review feedback for revision parameter - Reformat modify_tokenizer_files signature and from_pretrained call - Use kwargs pattern for modify_tokenizer_files call to avoid passing None revision - Add 6 unit tests for revision parameter in tokenizer/processor loaders --------- Co-authored-by: NanoCode012 --- src/axolotl/loaders/processor.py | 12 ++- src/axolotl/loaders/tokenizer.py | 22 ++++- tests/test_revision_parameter.py | 135 +++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 tests/test_revision_parameter.py diff --git a/src/axolotl/loaders/processor.py b/src/axolotl/loaders/processor.py index 124dad39ee..e07e324d63 100644 --- a/src/axolotl/loaders/processor.py +++ b/src/axolotl/loaders/processor.py @@ -19,6 +19,11 @@ def load_processor(cfg: DictDefault, tokenizer: PreTrainedTokenizerBase): if cfg.processor_type: processor_cls = getattr(transformers, cfg.processor_type) + # Build common kwargs for processor loading + processor_kwargs = {} + if cfg.revision_of_model: + processor_kwargs["revision"] = cfg.revision_of_model + if cfg.tokenizer_use_mistral_common: def _patch_mistralcommontokenizer(): @@ -40,6 +45,7 @@ def _patch_mistralcommontokenizer(): if processor_cls == VoxtralProcessor: return VoxtralProcessor.from_pretrained( cfg.processor_config, + **processor_kwargs, ) from axolotl.utils.mistral import Mistral3Processor @@ -48,10 +54,12 @@ def _patch_mistralcommontokenizer(): tokenizer=tokenizer, ) + processor_kwargs["trust_remote_code"] = cfg.trust_remote_code or False + processor_kwargs["tokenizer"] = tokenizer + processor = processor_cls.from_pretrained( cfg.processor_config, - trust_remote_code=cfg.trust_remote_code or False, - tokenizer=tokenizer, + **processor_kwargs, ) # Attempt to load image size from processor if available diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index 170ebf333f..d45d23baea 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -28,7 +28,10 @@ def modify_tokenizer_files( - tokenizer_path: str, token_mappings: dict[int, str], output_dir: str + tokenizer_path: str, + token_mappings: dict[int, str], + output_dir: str, + revision: str = "main", ) -> str: """ Modify tokenizer files to replace added_tokens strings, save to output directory, @@ -41,6 +44,7 @@ def modify_tokenizer_files( tokenizer_path: Path or name of the original tokenizer token_mappings: Dict mapping {token_id (int): new_token_string} output_dir: Directory to save the modified tokenizer + revision: Model revision/branch/tag/commit to load from (HF Hub) Returns: Path to the modified tokenizer directory @@ -53,7 +57,9 @@ def modify_tokenizer_files( if is_local_main_process(): # Load the tokenizer - temp_tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, use_fast=True) + temp_tokenizer = AutoTokenizer.from_pretrained( + tokenizer_path, use_fast=True, revision=revision + ) # Save the tokenizer to the output directory temp_tokenizer.save_pretrained(tokenizer_dir) @@ -134,7 +140,10 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): from axolotl.utils.mistral import HFMistralTokenizer # Load the HF-compatible wrapper around MistralTokenizer - tokenizer = HFMistralTokenizer.from_pretrained(cfg.tokenizer_config) + kwargs = {} + if cfg.revision_of_model: + kwargs["revision"] = cfg.revision_of_model + tokenizer = HFMistralTokenizer.from_pretrained(cfg.tokenizer_config, **kwargs) return tokenizer @@ -150,6 +159,8 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): if cfg.tokenizer_legacy is not None: # True is the default w/ https://github.com/huggingface/transformers/pull/25224 tokenizer_kwargs["legacy"] = cfg.tokenizer_legacy + if cfg.revision_of_model: + tokenizer_kwargs["revision"] = cfg.revision_of_model tokenizer_cls = AutoTokenizer if cfg.tokenizer_type: @@ -161,8 +172,11 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): # Apply token string overrides if specified if cfg.added_tokens_overrides: # Modify tokenizer files and get path to modified tokenizer + modify_kwargs = {"output_dir": cfg.output_dir} + if cfg.revision_of_model: + modify_kwargs["revision"] = cfg.revision_of_model tokenizer_path = modify_tokenizer_files( - tokenizer_path, cfg.added_tokens_overrides, output_dir=cfg.output_dir + tokenizer_path, cfg.added_tokens_overrides, **modify_kwargs ) tokenizer = tokenizer_cls.from_pretrained( diff --git a/tests/test_revision_parameter.py b/tests/test_revision_parameter.py new file mode 100644 index 0000000000..2116b223f7 --- /dev/null +++ b/tests/test_revision_parameter.py @@ -0,0 +1,135 @@ +"""Tests for revision_of_model being passed to tokenizer and processor loaders.""" + +from unittest.mock import MagicMock, patch + +from transformers import PreTrainedTokenizerBase + +from axolotl.utils.dict import DictDefault + + +class TestRevisionParameter: + """Tests for revision_of_model being passed to tokenizer and processor loaders.""" + + @patch("axolotl.loaders.tokenizer.load_model_config") + @patch("axolotl.loaders.tokenizer.AutoTokenizer") + @patch( + "axolotl.loaders.patch_manager.PatchManager.apply_pre_tokenizer_load_patches" + ) + def test_load_tokenizer_passes_revision( + self, _mock_patches, mock_auto_tokenizer, _mock_load_config + ): + mock_tokenizer = MagicMock() + mock_tokenizer.__class__.__name__ = "MockTokenizer" + mock_auto_tokenizer.from_pretrained.return_value = mock_tokenizer + + cfg = DictDefault( + { + "tokenizer_config": "some-model", + "revision_of_model": "abc123", + } + ) + from axolotl.loaders.tokenizer import load_tokenizer + + load_tokenizer(cfg) + + call_kwargs = mock_auto_tokenizer.from_pretrained.call_args + assert call_kwargs.kwargs.get("revision") == "abc123" + + @patch("axolotl.loaders.tokenizer.load_model_config") + @patch("axolotl.loaders.tokenizer.AutoTokenizer") + @patch( + "axolotl.loaders.patch_manager.PatchManager.apply_pre_tokenizer_load_patches" + ) + def test_load_tokenizer_omits_revision_when_unset( + self, _mock_patches, mock_auto_tokenizer, _mock_load_config + ): + mock_tokenizer = MagicMock() + mock_tokenizer.__class__.__name__ = "MockTokenizer" + mock_auto_tokenizer.from_pretrained.return_value = mock_tokenizer + + cfg = DictDefault( + { + "tokenizer_config": "some-model", + } + ) + from axolotl.loaders.tokenizer import load_tokenizer + + load_tokenizer(cfg) + + call_kwargs = mock_auto_tokenizer.from_pretrained.call_args + assert "revision" not in call_kwargs.kwargs + + @patch("axolotl.loaders.tokenizer.AutoTokenizer") + @patch("axolotl.loaders.tokenizer.is_local_main_process", return_value=True) + @patch("axolotl.loaders.tokenizer.barrier") + def test_modify_tokenizer_files_passes_revision( + self, _mock_barrier, _mock_main, mock_auto_tokenizer, temp_dir + ): + mock_tokenizer = MagicMock() + mock_auto_tokenizer.from_pretrained.return_value = mock_tokenizer + + from axolotl.loaders.tokenizer import modify_tokenizer_files + + modify_tokenizer_files("some-model", {}, output_dir=temp_dir, revision="abc123") + + call_kwargs = mock_auto_tokenizer.from_pretrained.call_args + assert call_kwargs.kwargs.get("revision") == "abc123" + + @patch("axolotl.loaders.tokenizer.AutoTokenizer") + @patch("axolotl.loaders.tokenizer.is_local_main_process", return_value=True) + @patch("axolotl.loaders.tokenizer.barrier") + def test_modify_tokenizer_files_defaults_revision_to_main( + self, _mock_barrier, _mock_main, mock_auto_tokenizer, temp_dir + ): + mock_tokenizer = MagicMock() + mock_auto_tokenizer.from_pretrained.return_value = mock_tokenizer + + from axolotl.loaders.tokenizer import modify_tokenizer_files + + modify_tokenizer_files("some-model", {}, output_dir=temp_dir) + + call_kwargs = mock_auto_tokenizer.from_pretrained.call_args + assert call_kwargs.kwargs.get("revision") == "main" + + @patch("axolotl.loaders.processor.AutoProcessor") + def test_load_processor_passes_revision(self, mock_auto_processor): + mock_processor = MagicMock() + mock_processor.size = {} + mock_auto_processor.from_pretrained.return_value = mock_processor + + cfg = DictDefault( + { + "processor_config": "some-model", + "revision_of_model": "abc123", + "trust_remote_code": False, + } + ) + tokenizer = MagicMock(spec=PreTrainedTokenizerBase) + + from axolotl.loaders.processor import load_processor + + load_processor(cfg, tokenizer) + + call_kwargs = mock_auto_processor.from_pretrained.call_args + assert call_kwargs.kwargs.get("revision") == "abc123" + + @patch("axolotl.loaders.processor.AutoProcessor") + def test_load_processor_omits_revision_when_unset(self, mock_auto_processor): + mock_processor = MagicMock() + mock_processor.size = {} + mock_auto_processor.from_pretrained.return_value = mock_processor + + cfg = DictDefault( + { + "processor_config": "some-model", + "trust_remote_code": False, + } + ) + tokenizer = MagicMock(spec=PreTrainedTokenizerBase) + + from axolotl.loaders.processor import load_processor + + load_processor(cfg, tokenizer) + + call_kwargs = mock_auto_processor.from_pretrained.call_args + assert "revision" not in call_kwargs.kwargs From 2b6f4a6c9b9acccf9eb1c9f4e730a3a2f25b9488 Mon Sep 17 00:00:00 2001 From: Robert Ronan Date: Tue, 24 Feb 2026 23:31:11 -0500 Subject: [PATCH 1142/1405] Fix: excess_length_strategy truncation method (#3401) * Add test cases to verify that the problem exists in the underlying * Update the handle_long_sequences function to correctly use Map instead of filter for the truncation strategy. Also remove the minimal length filtering from the truncate_long_samples function, and run it separately and before. * fix: refactor and add test truncate for non-input id fields * fix: refactor long seq handling fn * fix: refactor duplicate fn and simplify route * add additional tests and make them work on mac * handle logging exception on empty datasets --------- Co-authored-by: 2ndset bot Co-authored-by: NanoCode012 Co-authored-by: Wing Lian --- src/axolotl/utils/data/utils.py | 225 ++++++++----- src/axolotl/utils/trainer.py | 13 +- tests/test_data.py | 20 +- tests/utils/data/test_utils.py | 545 ++++++++++++++++++++++++++++++++ 4 files changed, 717 insertions(+), 86 deletions(-) create mode 100644 tests/utils/data/test_utils.py diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index 319e27f6f4..f2cdcac388 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -15,7 +15,7 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger from axolotl.utils.samplers.utils import get_dataset_lengths -from axolotl.utils.trainer import drop_long_seq +from axolotl.utils.trainer import filter_sequences_by_length LOG = get_logger(__name__) @@ -148,22 +148,33 @@ def deduplicate_and_log_datasets( return dataset, other_dataset -def truncate_long_seq(sample, sequence_len=2048, min_sequence_len=2): +def keep_min_len(sample, min_sequence_len=2): """ - Truncate samples whose sequence length is too long (> sequence_len) - or drop those too short (< min_sequence_len). + Batched filter function that keeps only samples with sequence length >= min_sequence_len. + Returns a list of booleans indicating which samples to keep. """ min_sequence_len = min_sequence_len or 2 input_ids = sample["input_ids"] + + # Batched (input_ids is a list of lists) results = [] + for seq in input_ids: + results.append(len(seq) >= min_sequence_len) + return results + + +def truncate_long_seq(sample, sequence_len=2048): + """ + Truncate samples whose sequence length is too long (> sequence_len). + Modifies the sample in-place and returns the modified sample. + """ + input_ids = sample["input_ids"] # Batched (input_ids is a list of lists) for i, seq in enumerate(input_ids): length = len(seq) - if length < min_sequence_len: - results.append(False) - elif length > sequence_len: + if length > sequence_len: sample["input_ids"][i] = seq[:sequence_len] if "attention_mask" in sample: sample["attention_mask"][i] = sample["attention_mask"][i][:sequence_len] @@ -171,28 +182,11 @@ def truncate_long_seq(sample, sequence_len=2048, min_sequence_len=2): sample["labels"][i] = sample["labels"][i][:sequence_len] if "position_ids" in sample: sample["position_ids"][i] = sample["position_ids"][i][:sequence_len] - results.append(True) - else: - results.append(True) - return results + return sample -def handle_long_seq_in_dataset( - dataset: Dataset, sequence_len: int, cfg: DictDefault -) -> Dataset: - """Remove sequences longer than configured maximum from dataset. - - Args: - dataset: Dataset to filter. - sequence_len: Maximum length for sequences to keep - cfg: Dictionary mapping `axolotl` config keys to values. - - Returns: - Filtered dataset with long sequences handled according to the excess_length_strategy value: - 'drop' (default) excludes any sequence longer than sequence_len - 'truncate' truncates them down to sequence_len - 'raise' raises a ValueError if any sequence was found that was longer than sequence_len - """ +def _should_skip_processing(dataset: Dataset) -> bool: + """Check if dataset should skip long sequence handling.""" if ( hasattr(dataset, "column_names") and dataset.column_names @@ -202,71 +196,156 @@ def handle_long_seq_in_dataset( "Dataset does not contain 'input_ids' column. Skip drop long seq. This is " "expected for reward modeling." ) - return dataset + return True elif not hasattr(dataset, "column_names") or dataset.column_names is None: LOG.info( "Dataset is streaming (IterableDataset), skipping long sequence handling" ) - return dataset + return True + return False - excess_length_strategy = (cfg.excess_length_strategy or "drop").lower() - drop_long = functools.partial( - drop_long_seq, - sequence_len=sequence_len, - min_sequence_len=cfg.min_sample_len, - raise_on_drop=excess_length_strategy == "raise", - ) - - with contextlib.suppress(AttributeError): +def _log_dataset_stats(dataset: Dataset) -> None: + """Log min/max sequence lengths for debugging.""" + with contextlib.suppress(AttributeError, ValueError): ds_lengths = get_dataset_lengths(dataset, from_arrow=True) - min_input_len = np.min(ds_lengths) - LOG.info(f"min_input_len: {min_input_len}") - max_input_len = np.max(ds_lengths) - LOG.info(f"max_input_len: {max_input_len}") + LOG.info(f"min_input_len: {np.min(ds_lengths)}") + LOG.info(f"max_input_len: {np.max(ds_lengths)}") - prior_len = len(dataset) if hasattr(dataset, "__len__") else None - filter_map_kwargs = {} +def _build_filter_kwargs(dataset: Dataset, cfg: DictDefault) -> dict: + """Build kwargs for dataset filter/map operations.""" + kwargs = {} if not isinstance(dataset, IterableDataset): - filter_map_kwargs["num_proc"] = cfg.dataset_num_proc - filter_map_kwargs["load_from_cache_file"] = not cfg.is_preprocess + kwargs["num_proc"] = cfg.dataset_num_proc + kwargs["load_from_cache_file"] = not cfg.is_preprocess + return kwargs + + +def _filter_short_sequences( + dataset: Dataset, min_len: int, filter_kwargs: dict +) -> tuple[Dataset, int]: + """Filter out sequences shorter than min_len. Returns (dataset, num_dropped).""" + prior_len = len(dataset) if hasattr(dataset, "__len__") else None - drop_long_kwargs = {} - if filter_map_kwargs: + desc_kwargs = {} + if filter_kwargs: + desc_kwargs["desc"] = f"Filtering Short Sequences (<{min_len})" + + dataset = dataset.filter( + functools.partial(keep_min_len, min_sequence_len=min_len), + batched=True, + **filter_kwargs, + **desc_kwargs, + ) + + dropped = 0 + if prior_len: + dropped = prior_len - len(dataset) + if dropped > 0: + LOG.info(f"Dropped {dropped} short sequences (<{min_len} tokens)") + + return dataset, dropped + + +def _truncate_long_sequences( + dataset: Dataset, max_len: int, map_kwargs: dict +) -> Dataset: + """Truncate sequences longer than max_len.""" + desc_kwargs = {} + if map_kwargs: + desc_kwargs["desc"] = f"Truncating Sequences (target_len={max_len})" + + dataset = dataset.map( + functools.partial(truncate_long_seq, sequence_len=max_len), + batched=True, + **map_kwargs, + **desc_kwargs, + ) + LOG.info(f"Truncated long sequences to max length {max_len}") + return dataset + + +def _drop_outside_range( + dataset: Dataset, + max_len: int, + min_len: int, + raise_on_long: bool, + filter_kwargs: dict, +) -> tuple[Dataset, int]: + """Drop sequences outside valid length range [min_len, max_len]. + + Returns (dataset, num_dropped).""" + prior_len = len(dataset) if hasattr(dataset, "__len__") else None + + desc_kwargs = {} + if filter_kwargs: action = ( "Checking Sequence Lengths" - if excess_length_strategy == "raise" - else "Dropping Long Sequences" - ) - drop_long_kwargs["desc"] = f"{action} (>{sequence_len})" - - if excess_length_strategy == "truncate": - process_fn = functools.partial( - truncate_long_seq, - sequence_len=sequence_len, - min_sequence_len=cfg.min_sample_len, - ) - drop_long_kwargs["desc"] = ( - f"Truncating/Filtering Sequences (target_len={sequence_len})" + if raise_on_long + else "Dropping Invalid Sequences" ) - else: - process_fn = drop_long + desc_kwargs["desc"] = f"{action} (<{min_len} or >{max_len})" dataset = dataset.filter( - process_fn, + functools.partial( + filter_sequences_by_length, + sequence_len=max_len, + min_sequence_len=min_len, + raise_on_drop=raise_on_long, + ), batched=True, - **filter_map_kwargs, - **drop_long_kwargs, + **filter_kwargs, + **desc_kwargs, ) - if prior_len: + + dropped = 0 + if not raise_on_long and prior_len: dropped = prior_len - len(dataset) - if dropped: - action = ( - "truncated/filtered" - if excess_length_strategy == "truncate" - else "dropped" + if dropped > 0: + LOG.info( + f"Dropped {dropped} sequences outside valid range " + f"([{min_len}, {max_len}])" ) - LOG.warning(f"{action.title()} {dropped} samples from dataset") + + return dataset, dropped + + +def handle_long_seq_in_dataset( + dataset: Dataset, sequence_len: int, cfg: DictDefault +) -> Dataset: + """Remove sequences longer than configured maximum from dataset. + + Args: + dataset: Dataset to filter. + sequence_len: Maximum length for sequences to keep + cfg: Dictionary mapping `axolotl` config keys to values. + + Returns: + Filtered dataset with long sequences handled according to the excess_length_strategy value: + 'drop' (default) excludes any sequence longer than sequence_len + 'truncate' truncates them down to sequence_len + 'raise' raises a ValueError if any sequence was found that was longer than sequence_len + """ + # Early returns for special cases + if _should_skip_processing(dataset): + return dataset + + excess_length_strategy = (cfg.excess_length_strategy or "drop").lower() + + _log_dataset_stats(dataset) + + # Setup kwargs + filter_kwargs = _build_filter_kwargs(dataset, cfg) + + # Handle sequences based on strategy + if excess_length_strategy == "truncate": + dataset, _ = _filter_short_sequences(dataset, cfg.min_sample_len, filter_kwargs) + dataset = _truncate_long_sequences(dataset, sequence_len, filter_kwargs) + else: + raise_on_long = excess_length_strategy == "raise" + dataset, _ = _drop_outside_range( + dataset, sequence_len, cfg.min_sample_len, raise_on_long, filter_kwargs + ) return dataset diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 621ef87858..d97a74f6fe 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -205,10 +205,13 @@ def add_length(sample): return sample -def drop_long_seq(sample, sequence_len=2048, min_sequence_len=2, raise_on_drop=False): +def filter_sequences_by_length( + sample, sequence_len=2048, min_sequence_len=2, raise_on_drop=False +): """ - Drop samples whose sequence length is either too long (> sequence_len) - or too short (< min_sequence_len). + Filter sequences outside valid length range [min_sequence_len, sequence_len]. + + Drops samples that are either too short (< min_sequence_len) or too long (> sequence_len). Works for both single-example (list[int]) or batched (list[list[int]]). @@ -383,10 +386,10 @@ def drop_no_trainable_tokens(sample): def process_pretraining_datasets_for_packing( train_dataset, sequence_len, skip_position_ids=True, drop_attention_mask=False ): - drop_long = partial(drop_long_seq, sequence_len=sequence_len) + drop_outside_range = partial(filter_sequences_by_length, sequence_len=sequence_len) train_dataset = train_dataset.filter( - drop_long, + drop_outside_range, desc="Dropping Long Sequences", load_from_cache_file=False, ) diff --git a/tests/test_data.py b/tests/test_data.py index ad76bbf6e4..01f60e897c 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -7,7 +7,7 @@ from transformers import LlamaTokenizer from axolotl.utils.data import encode_streaming, md5 -from axolotl.utils.trainer import drop_long_seq +from axolotl.utils.trainer import filter_sequences_by_length from tests.hf_offline_utils import enable_hf_offline @@ -70,17 +70,19 @@ def test_excess_length_strategy(self): # -- single sequence -- # This should work data = {"input_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]} - drop_long_seq(data, 32, raise_on_drop=True) + filter_sequences_by_length(data, 32, raise_on_drop=True) # This should return True, since data fits - dropped = drop_long_seq(data, 32) + dropped = filter_sequences_by_length(data, 32) self.assertTrue(dropped) # This should raise - self.assertRaises(ValueError, drop_long_seq, data, 15, raise_on_drop=True) + self.assertRaises( + ValueError, filter_sequences_by_length, data, 15, raise_on_drop=True + ) # This should return False, since data doesn't fit - dropped = drop_long_seq(data, 15) + dropped = filter_sequences_by_length(data, 15) self.assertFalse(dropped) # -- batch sequence -- @@ -91,13 +93,15 @@ def test_excess_length_strategy(self): [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], ] } - drop_long_seq(data, 32, raise_on_drop=True) + filter_sequences_by_length(data, 32, raise_on_drop=True) # This should raise - self.assertRaises(ValueError, drop_long_seq, data, 15, raise_on_drop=True) + self.assertRaises( + ValueError, filter_sequences_by_length, data, 15, raise_on_drop=True + ) # This should keep the first but drop the second entry - dropped = drop_long_seq(data, 15) + dropped = filter_sequences_by_length(data, 15) self.assertEqual(dropped, [True, False]) diff --git a/tests/utils/data/test_utils.py b/tests/utils/data/test_utils.py new file mode 100644 index 0000000000..357447b477 --- /dev/null +++ b/tests/utils/data/test_utils.py @@ -0,0 +1,545 @@ +""" +Unit tests for data utility functions +""" + +import unittest +from unittest.mock import MagicMock + +from datasets import Dataset + +from axolotl.utils.data.utils import handle_long_seq_in_dataset +from axolotl.utils.dict import DictDefault + + +class TestHandleLongSeqInDataset(unittest.TestCase): + """ + Test class for handle_long_seq_in_dataset function + """ + + def test_drop_strategy_removes_long_sequences(self): + """Test that 'drop' strategy removes sequences longer than sequence_len""" + # Create dataset with mixed length sequences + dataset = Dataset.from_dict( + { + "input_ids": [ + [1, 2, 3], # length 3 - keep + [1, 2, 3, 4, 5], # length 5 - keep + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], # length 11 - drop + [1, 2], # length 2 - keep + ] + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "drop", + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + # Should have dropped the sequence with length 11 + self.assertEqual(len(result), 3) + self.assertEqual(len(result[0]["input_ids"]), 3) + self.assertEqual(len(result[1]["input_ids"]), 5) + self.assertEqual(len(result[2]["input_ids"]), 2) + + def test_drop_strategy_is_default(self): + """Test that 'drop' is the default strategy when not specified""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1, 2, 3], + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], # length 11 - should drop + ] + } + ) + + cfg = DictDefault( + { + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + # Should have dropped the long sequence + self.assertEqual(len(result), 1) + + def test_truncate_strategy_truncates_long_sequences(self): + """Test that 'truncate' strategy truncates sequences to sequence_len""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1, 2, 3], # length 3 - keep as is + [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + ], # length 12 - truncate to 10 + ] + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "truncate", + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + # Should have 2 samples + self.assertEqual(len(result), 2) + # First sample unchanged + self.assertEqual(len(result[0]["input_ids"]), 3) + # Second sample truncated to 10 + self.assertEqual(len(result[1]["input_ids"]), 10) + self.assertEqual(result[1]["input_ids"], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + + def test_truncate_strategy_truncates_all_auxiliary_fields(self): + """Test that truncation applies to all auxiliary fields consistently""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + ], + "attention_mask": [ + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + ], + "labels": [ + [-100, -100, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + ], + "position_ids": [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + ], + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "truncate", + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + # All fields should be truncated to 10 + self.assertEqual(len(result[0]["input_ids"]), 10) + self.assertEqual(len(result[0]["attention_mask"]), 10) + self.assertEqual(len(result[0]["labels"]), 10) + self.assertEqual(len(result[0]["position_ids"]), 10) + + # Verify content is correct + self.assertEqual(result[0]["input_ids"], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + self.assertEqual(result[0]["attention_mask"], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + self.assertEqual(result[0]["labels"], [-100, -100, 3, 4, 5, 6, 7, 8, 9, 10]) + self.assertEqual(result[0]["position_ids"], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + + def test_raise_strategy_raises_on_long_sequences(self): + """Test that 'raise' strategy raises ValueError when encountering long sequences""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1, 2, 3], + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], # length 11 - should raise + ] + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "raise", + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + with self.assertRaises(ValueError): + handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + def test_min_sequence_len_filters_short_sequences(self): + """Test that sequences shorter than min_sample_len are filtered out""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1], # length 1 - drop (< min_sample_len=3) + [1, 2], # length 2 - drop + [1, 2, 3], # length 3 - keep + [1, 2, 3, 4, 5], # length 5 - keep + ] + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "drop", + "min_sample_len": 3, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + # Should only keep sequences with length >= 3 + self.assertEqual(len(result), 2) + self.assertEqual(len(result[0]["input_ids"]), 3) + self.assertEqual(len(result[1]["input_ids"]), 5) + + def test_dataset_without_input_ids_column(self): + """Test that datasets without 'input_ids' column are returned unchanged""" + dataset = Dataset.from_dict( + { + "chosen": [1, 2, 3], + "rejected": [4, 5, 6], + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "drop", + "min_sample_len": 2, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + # Dataset should be unchanged + self.assertEqual(len(result), len(dataset)) + self.assertListEqual(list(result.column_names), ["chosen", "rejected"]) + + def test_truncate_filters_short_before_truncating(self): + """Test that truncate strategy filters short sequences before truncating long ones + + This is important for efficiency - we should not waste time truncating + sequences that will be filtered out anyway. + """ + dataset = Dataset.from_dict( + { + "input_ids": [ + [1], # length 1 - filter out first + [1, 2, 3], # length 3 - keep, no truncation needed + [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + ], # length 12 - keep and truncate + ] + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "truncate", + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + # Should have filtered out the first (short) sequence + self.assertEqual(len(result), 2) + # Second sample unchanged + self.assertEqual(len(result[0]["input_ids"]), 3) + # Third sample truncated to 10 + self.assertEqual(len(result[1]["input_ids"]), 10) + + def test_case_insensitive_strategy(self): + """Test that excess_length_strategy is case-insensitive""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + ] + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "TRUNCATE", # uppercase + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + # Should still truncate + self.assertEqual(len(result[0]["input_ids"]), 10) + + def test_raise_strategy_silently_drops_short_sequences(self): + """Test that 'raise' strategy drops short sequences without raising""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1], # length 1 - too short, should be dropped silently + [1, 2, 3, 4, 5], # length 5 - keep + ] + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "raise", + "min_sample_len": 3, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + # Should NOT raise, just silently drop the short sequence + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + self.assertEqual(len(result), 1) + self.assertEqual(len(result[0]["input_ids"]), 5) + + def test_drop_boundary_sequence_equal_to_sequence_len(self): + """Test that drop strategy keeps sequences with length exactly equal to sequence_len""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], # length 10 == sequence_len + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], # length 11 > sequence_len + ] + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "drop", + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + # Exactly equal should be kept, one over should be dropped + self.assertEqual(len(result), 1) + self.assertEqual(len(result[0]["input_ids"]), 10) + + def test_truncate_boundary_sequence_equal_to_sequence_len(self): + """Test that truncate strategy leaves sequences with length exactly equal to sequence_len unchanged""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], # length 10 == sequence_len + ] + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "truncate", + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + # Should be unchanged - not truncated + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["input_ids"], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + + def test_empty_dataset(self): + """Test that an empty dataset is handled gracefully""" + dataset = Dataset.from_dict({"input_ids": []}) + + cfg = DictDefault( + { + "excess_length_strategy": "drop", + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + self.assertEqual(len(result), 0) + + def test_all_sequences_dropped_returns_empty_dataset(self): + """Test that dropping all sequences results in an empty dataset""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1], # too short + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], # too long + ] + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "drop", + "min_sample_len": 5, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + self.assertEqual(len(result), 0) + + def test_iterable_dataset_skips_processing(self): + """Test that streaming datasets (column_names is None) are returned unchanged. + + The skip check in _should_skip_processing triggers when column_names is + None, which happens with true streaming datasets loaded via + load_dataset(..., streaming=True). + """ + mock_dataset = MagicMock() + mock_dataset.column_names = None + + cfg = DictDefault( + { + "excess_length_strategy": "drop", + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(mock_dataset, sequence_len=10, cfg=cfg) + + # Should be returned unchanged (same object) + self.assertIs(result, mock_dataset) + + def test_truncate_with_partial_auxiliary_fields(self): + """Test truncation when only some auxiliary fields are present""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + ], + "labels": [ + [-100, -100, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + ], + # No attention_mask or position_ids + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "truncate", + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + self.assertEqual(len(result[0]["input_ids"]), 10) + self.assertEqual(len(result[0]["labels"]), 10) + self.assertEqual(result[0]["input_ids"], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + self.assertEqual(result[0]["labels"], [-100, -100, 3, 4, 5, 6, 7, 8, 9, 10]) + # Confirm no extra columns were introduced + self.assertListEqual(sorted(result.column_names), ["input_ids", "labels"]) + + def test_min_sample_len_defaults_to_two_when_not_set(self): + """Test that min_sample_len defaults to 2 when not specified in config""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1], # length 1 - should be dropped (< default 2) + [1, 2], # length 2 - should be kept (>= default 2) + [1, 2, 3], # length 3 - should be kept + ] + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "drop", + # min_sample_len not set + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + self.assertEqual(len(result), 2) + self.assertEqual(len(result[0]["input_ids"]), 2) + self.assertEqual(len(result[1]["input_ids"]), 3) + + def test_invalid_strategy_falls_through_to_drop(self): + """Test that an unrecognized strategy value falls through to drop behavior""" + dataset = Dataset.from_dict( + { + "input_ids": [ + [1, 2, 3], # keep + [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + ], # length 11 - should be dropped + ] + } + ) + + cfg = DictDefault( + { + "excess_length_strategy": "not_a_real_strategy", + "min_sample_len": 2, + "dataset_num_proc": None, + "is_preprocess": False, + } + ) + + result = handle_long_seq_in_dataset(dataset, sequence_len=10, cfg=cfg) + + # Should behave like 'drop' + self.assertEqual(len(result), 1) + self.assertEqual(len(result[0]["input_ids"]), 3) + + +if __name__ == "__main__": + unittest.main() From 18f26c19ef123e23f21fdc106cfd221386699bce Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 25 Feb 2026 14:46:02 -0500 Subject: [PATCH 1143/1405] add uv axolotl builds (#3431) --- .github/workflows/base.yml | 32 ++++---- .github/workflows/main.yml | 154 ++++++++++++++++++++++++++++++++++-- .github/workflows/tests.yml | 4 +- docker/Dockerfile-base | 46 ++++------- docker/Dockerfile-cloud-uv | 30 +++++++ docker/Dockerfile-uv | 47 +++++++++++ docker/Dockerfile-uv-base | 41 ++++------ 7 files changed, 272 insertions(+), 82 deletions(-) create mode 100644 docker/Dockerfile-cloud-uv create mode 100644 docker/Dockerfile-uv diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 5f97129052..81b42bebb5 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -59,14 +59,14 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" platforms: "linux/amd64,linux/arm64" - - cuda: "129" - cuda_version: 12.9.1 - cudnn_version: "" - python_version: "3.12" - pytorch: 2.9.1 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-base" - platforms: "linux/amd64,linux/arm64" +# - cuda: "129" +# cuda_version: 12.9.1 +# cudnn_version: "" +# python_version: "3.12" +# pytorch: 2.9.1 +# torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" +# dockerfile: "Dockerfile-base" +# platforms: "linux/amd64,linux/arm64" - cuda: "130" cuda_version: 13.0.0 cudnn_version: "" @@ -181,14 +181,14 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" - - cuda: "129" - cuda_version: 12.9.1 - cudnn_version: "" - python_version: "3.12" - pytorch: 2.9.1 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-uv-base" - platforms: "linux/amd64,linux/arm64" +# - cuda: "129" +# cuda_version: 12.9.1 +# cudnn_version: "" +# python_version: "3.12" +# pytorch: 2.9.1 +# torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" +# dockerfile: "Dockerfile-uv-base" +# platforms: "linux/amd64,linux/arm64" - cuda: "130" cuda_version: 13.0.0 cudnn_version: "" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b8ebe2aeb7..aca9b1dd36 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,12 +40,83 @@ jobs: pytorch: 2.10.0 axolotl_extras: platforms: "linux/amd64,linux/arm64" - - cuda: 129 - cuda_version: 12.9.1 +# - cuda: 129 +# cuda_version: 12.9.1 +# python_version: "3.12" +# pytorch: 2.9.1 +# axolotl_extras: +# platforms: "linux/amd64,linux/arm64" + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.11" + pytorch: 2.9.1 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" + - cuda: 130 + cuda_version: 13.0.0 python_version: "3.12" + pytorch: 2.10.0 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" + runs-on: axolotl-gpu-runner + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Docker metadata + id: metadata + uses: docker/metadata-action@v5 + with: + images: | + axolotlai/axolotl + tags: | + type=ref,event=branch + type=pep440,pattern={{version}} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + # guidance for testing before pushing: https://docs.docker.com/build/ci/github-actions/test-before-push/ + - name: Build and export to Docker + uses: docker/build-push-action@v5 + with: + context: . + platforms: ${{ matrix.platforms }} + build-args: | + BASE_TAG=${{ github.ref_type == 'tag' && 'main' || github.ref_name }}-base-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }} + CUDA=${{ matrix.cuda }} + PYTORCH_VERSION=${{ matrix.pytorch }} + AXOLOTL_ARGS=${{ matrix.axolotl_args }} + AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}} + file: ./docker/Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: | + ${{ steps.metadata.outputs.tags }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} + ${{ steps.metadata.outputs.tags }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }} + ${{ (matrix.is_latest) && format('{0}-latest', steps.metadata.outputs.tags) || '' }} + labels: ${{ steps.metadata.outputs.labels }} + + build-axolotl-uv: + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]') && github.repository_owner == 'axolotl-ai-cloud' }} + strategy: + fail-fast: false + matrix: + include: + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" pytorch: 2.9.1 axolotl_extras: platforms: "linux/amd64,linux/arm64" + is_latest: true + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.12" + pytorch: 2.10.0 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" - cuda: 130 cuda_version: 13.0.0 python_version: "3.11" @@ -67,7 +138,7 @@ jobs: uses: docker/metadata-action@v5 with: images: | - axolotlai/axolotl + axolotlai/axolotl-uv tags: | type=ref,event=branch type=pep440,pattern={{version}} @@ -90,7 +161,7 @@ jobs: PYTORCH_VERSION=${{ matrix.pytorch }} AXOLOTL_ARGS=${{ matrix.axolotl_args }} AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}} - file: ./docker/Dockerfile + file: ./docker/Dockerfile-uv push: ${{ github.event_name != 'pull_request' }} tags: | ${{ steps.metadata.outputs.tags }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} @@ -130,11 +201,78 @@ jobs: pytorch: 2.10.0 axolotl_extras: platforms: "linux/amd64,linux/arm64" - - cuda: 129 - cuda_version: 12.9.1 +# - cuda: 129 +# cuda_version: 12.9.1 +# python_version: "3.12" +# pytorch: 2.9.1 +# axolotl_extras: +# platforms: "linux/amd64,linux/arm64" + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.11" + pytorch: 2.9.1 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" + - cuda: 130 + cuda_version: 13.0.0 python_version: "3.12" + pytorch: 2.10.0 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" + runs-on: axolotl-gpu-runner + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Docker metadata + id: metadata + uses: docker/metadata-action@v5 + with: + images: | + axolotlai/axolotl-cloud + tags: | + type=ref,event=branch + type=pep440,pattern={{version}} + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build + uses: docker/build-push-action@v5 + with: + context: . + platforms: ${{ matrix.platforms }} + build-args: | + BASE_TAG=${{ github.ref_type == 'tag' && 'main' || github.ref_name }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} + CUDA=${{ matrix.cuda }} + file: ./docker/Dockerfile-cloud + push: ${{ github.event_name != 'pull_request' }} + tags: | + ${{ steps.metadata.outputs.tags }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} + ${{ (matrix.is_latest) && format('{0}-latest', steps.metadata.outputs.tags) || '' }} + labels: ${{ steps.metadata.outputs.labels }} + + build-axolotl-cloud-uv: + needs: build-axolotl-uv + if: ${{ ! contains(github.event.commits[0].message, '[skip docker]') && github.repository_owner == 'axolotl-ai-cloud' }} + # this job needs to be run on self-hosted GPU runners... + strategy: + matrix: + include: + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" pytorch: 2.9.1 axolotl_extras: + is_latest: true + platforms: "linux/amd64,linux/arm64" + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.12" + pytorch: 2.10.0 + axolotl_extras: platforms: "linux/amd64,linux/arm64" - cuda: 130 cuda_version: 13.0.0 @@ -157,7 +295,7 @@ jobs: uses: docker/metadata-action@v5 with: images: | - axolotlai/axolotl-cloud + axolotlai/axolotl-cloud-uv tags: | type=ref,event=branch type=pep440,pattern={{version}} @@ -176,7 +314,7 @@ jobs: build-args: | BASE_TAG=${{ github.ref_type == 'tag' && 'main' || github.ref_name }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} CUDA=${{ matrix.cuda }} - file: ./docker/Dockerfile-cloud + file: ./docker/Dockerfile-cloud-uv push: ${{ github.event_name != 'pull_request' }} tags: | ${{ steps.metadata.outputs.tags }}-py${{ matrix.python_version }}-cu${{ matrix.cuda }}-${{ matrix.pytorch }}${{ matrix.axolotl_extras != '' && '-' || '' }}${{ matrix.axolotl_extras }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 39402d61a6..d3a53d2ce1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -264,8 +264,8 @@ jobs: fail-fast: false matrix: include: - - cuda: 129 - cuda_version: 12.9.1 + - cuda: 130 + cuda_version: 13.0.0 python_version: "3.12" pytorch: 2.9.1 num_gpus: 1 diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 547c45f49f..70a62ee3a1 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -59,34 +59,18 @@ RUN git lfs install --skip-repo && \ pip3 install -U --no-cache-dir pydantic==1.10.10 && \ pip3 cache purge -RUN case "$PYTORCH_VERSION" in \ - 2.9.[0-9]*) \ - if [ "$CUDA" = "128" ]; then \ - if [ "$TARGETARCH" = "amd64" ]; then \ - WHL_FILE="flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl"; \ - WHL_VERSION="v0.5.4"; \ - elif [ "$TARGETARCH" = "arm64" ]; then \ - WHL_FILE="flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_aarch64.whl"; \ - WHL_VERSION="v0.6.4"; \ - else \ - echo "Unsupported architecture: $TARGETARCH"; exit 1; \ - fi; \ - wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/${WHL_VERSION}/${WHL_FILE}; \ - pip3 install --no-cache-dir ${WHL_FILE}; \ - rm ${WHL_FILE}; \ - elif [ "$CUDA" = "130" ]; then \ - if [ "$TARGETARCH" = "amd64" ]; then \ - WHL_FILE="flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl"; \ - WHL_VERSION="v0.5.4"; \ - elif [ "$TARGETARCH" = "arm64" ]; then \ - WHL_FILE="flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_aarch64.whl"; \ - WHL_VERSION="v0.6.4"; \ - else \ - echo "Unsupported architecture: $TARGETARCH"; exit 1; \ - fi; \ - wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/${WHL_VERSION}/${WHL_FILE}; \ - pip3 install --no-cache-dir ${WHL_FILE}; \ - rm ${WHL_FILE}; \ - fi \ - ;; \ - esac +# Map Python version (e.g., 3.12 -> cp312) +RUN PYTHON_CP="cp$(echo $PYTHON_VERSION | tr -d '.')" && \ + # Map PyTorch version (e.g., 2.9.1 -> torch2.9, 2.10.0 -> torch2.10) + TORCH_TAG="torch$(echo $PYTORCH_VERSION | grep -oP '^\d+\.\d+')" && \ + # Map architecture + case "$TARGETARCH" in \ + amd64) ARCH_TAG="x86_64" ;; \ + arm64) ARCH_TAG="aarch64" ;; \ + *) echo "Unsupported architecture: $TARGETARCH"; exit 1 ;; \ + esac && \ + WHL_VERSION="v0.7.16" && \ + WHL_FILE="flash_attn-2.8.3+cu${CUDA}${TORCH_TAG}-${PYTHON_CP}-${PYTHON_CP}-linux_${ARCH_TAG}.whl" && \ + wget -nv "https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/${WHL_VERSION}/${WHL_FILE}" && \ + pip3 install --no-cache-dir "${WHL_FILE}" && \ + rm "${WHL_FILE}" diff --git a/docker/Dockerfile-cloud-uv b/docker/Dockerfile-cloud-uv new file mode 100644 index 0000000000..dc6fd5377b --- /dev/null +++ b/docker/Dockerfile-cloud-uv @@ -0,0 +1,30 @@ +ARG BASE_TAG=main +FROM axolotlai/axolotl-uv:$BASE_TAG + +ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" +ENV HF_HUB_CACHE="/workspace/data/huggingface-cache/hub" +ENV HF_HOME="/workspace/data/huggingface-cache/hub" +ENV HF_HUB_ENABLE_HF_TRANSFER="1" + +EXPOSE 8888 +EXPOSE 22 + +COPY scripts/cloud-entrypoint.sh /root/cloud-entrypoint.sh +COPY scripts/motd /etc/motd + +RUN pip install jupyterlab notebook ipywidgets && \ + jupyter lab clean +RUN apt update && \ + apt install --yes --no-install-recommends openssh-server tmux iproute2 nvtop && \ + rm -rf /var/cache/apt/archives && \ + rm -rf /var/lib/apt/lists/* && \ + mkdir -p ~/.ssh && \ + chmod 700 ~/.ssh && \ + printf "\n[[ -z \"\$TMUX\" ]] && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> ~/.bashrc && \ + printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> ~/.bashrc && \ + chmod +x /workspace/axolotl/scripts/cloud-entrypoint.sh && \ + chmod +x /root/cloud-entrypoint.sh && \ + echo 'set-option -g history-limit 5000' >> ~/.tmux.conf + +ENTRYPOINT ["/root/cloud-entrypoint.sh"] +CMD ["sleep", "infinity"] diff --git a/docker/Dockerfile-uv b/docker/Dockerfile-uv new file mode 100644 index 0000000000..b9be1ff732 --- /dev/null +++ b/docker/Dockerfile-uv @@ -0,0 +1,47 @@ +ARG BASE_TAG=main-base +FROM axolotlai/axolotl-base-uv:$BASE_TAG + +ARG TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6+PTX" +ARG AXOLOTL_EXTRAS="" +ARG AXOLOTL_ARGS="" +ARG CUDA="118" +ARG PYTORCH_VERSION="2.1.2" +ARG TARGETARCH + +ENV PYTORCH_VERSION=$PYTORCH_VERSION + +RUN apt-get update && \ + apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev rsync s3fs && \ + rm -rf /var/cache/apt/archives && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace + +RUN git clone --depth=1 https://github.com/axolotl-ai-cloud/axolotl.git + +WORKDIR /workspace/axolotl + +# If AXOLOTL_EXTRAS is set, append it in brackets; don't install deepspeed with arm64 +RUN if [ "$TARGETARCH" = "arm64" ]; then \ + BASE_EXTRAS="flash-attn,ring-flash-attn,optimizers,ray"; \ + else \ + BASE_EXTRAS="deepspeed,flash-attn,ring-flash-attn,optimizers,ray"; \ + fi && \ + if [ "$AXOLOTL_EXTRAS" != "" ]; then \ + uv pip install --no-build-isolation -e .[$BASE_EXTRAS,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + else \ + uv pip install --no-build-isolation -e .[$BASE_EXTRAS] $AXOLOTL_ARGS; \ + fi && \ + python scripts/unsloth_install.py --uv | sh && \ + python scripts/cutcrossentropy_install.py --uv | sh && \ + uv pip install pytest && \ + uv pip cache purge + +# fix so that git fetch/pull from remote works with shallow clone +RUN git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && \ + git config --get remote.origin.fetch && \ + git config --global credential.helper store + +COPY .axolotl-complete.bash /root/.axolotl-complete.bash +RUN chmod +x /root/.axolotl-complete.bash && \ + echo 'source /root/.axolotl-complete.bash' >> ~/.bashrc diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index d28b27ad2e..0e7acbe29a 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -6,6 +6,7 @@ ARG TARGETARCH FROM nvidia/cuda:$CUDA_VERSION-cudnn$CUDNN_VERSION-devel-ubuntu$UBUNTU_VERSION AS base-builder +ARG TARGETARCH ARG PYTHON_VERSION="3.11" ARG PYTORCH_VERSION="2.6.0" ARG CUDA="126" @@ -39,28 +40,18 @@ RUN if [ "$TARGETARCH" = "amd64" ]; then \ uv pip install "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main"; \ fi -RUN case "$PYTORCH_VERSION" in \ - 2.9.[0-9]*) \ - if [ "$TARGETARCH" = "amd64" ]; then \ - if [ "$CUDA" = "128" ]; then \ - wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - uv pip install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_x86_64.whl; \ - elif [ "$CUDA" = "130" ]; then \ - wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.5.4/flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ - uv pip install --no-cache-dir flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ - rm flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_x86_64.whl; \ - fi \ - elif [ "$TARGETARCH" = "arm64" ]; then \ - if [ "$CUDA" = "128" ]; then \ - wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.6.4/flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_aarch64.whl; \ - uv pip install --no-cache-dir flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_aarch64.whl; \ - rm flash_attn-2.8.3+cu128torch2.9-cp311-cp311-linux_aarch64.whl; \ - elif [ "$CUDA" = "130" ]; then \ - wget -nv https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.6.4/flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_aarch64.whl; \ - uv pip install --no-cache-dir flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_aarch64.whl; \ - rm flash_attn-2.8.3+cu130torch2.9-cp311-cp311-linux_aarch64.whl; \ - fi \ - fi \ - ;; \ - esac +# Map Python version (e.g., 3.12 -> cp312) +RUN PYTHON_CP="cp$(echo $PYTHON_VERSION | tr -d '.')" && \ + # Map PyTorch version (e.g., 2.9.1 -> torch2.9, 2.10.0 -> torch2.10) + TORCH_TAG="torch$(echo $PYTORCH_VERSION | grep -oP '^\d+\.\d+')" && \ + # Map architecture + case "$TARGETARCH" in \ + amd64) ARCH_TAG="x86_64" ;; \ + arm64) ARCH_TAG="aarch64" ;; \ + *) echo "Unsupported architecture: $TARGETARCH"; exit 1 ;; \ + esac && \ + WHL_VERSION="v0.7.16" && \ + WHL_FILE="flash_attn-2.8.3+cu${CUDA}${TORCH_TAG}-${PYTHON_CP}-${PYTHON_CP}-linux_${ARCH_TAG}.whl" && \ + wget -nv "https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/${WHL_VERSION}/${WHL_FILE}" && \ + uv pip install --no-cache-dir "${WHL_FILE}" && \ + rm "${WHL_FILE}" From 7f23b302d16e76a232242dfab87628276d58c531 Mon Sep 17 00:00:00 2001 From: kallewoof Date: Mon, 2 Mar 2026 17:30:07 +0900 Subject: [PATCH 1144/1405] bug-fix: use self.optimizer if optimizer not passed to SchedulerMixin.create_scheduler() (#3435) [skip ci] * bug-fix: use self.optimizer if optimizer not passed to SchedulerMixin.create_scheduler() * nit: raise if self.optimizer is also unset * optimizer properly optional in create_scheduler() --- src/axolotl/core/trainers/mixins/scheduler.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/axolotl/core/trainers/mixins/scheduler.py b/src/axolotl/core/trainers/mixins/scheduler.py index fc2b0e59d4..23e4f6c255 100644 --- a/src/axolotl/core/trainers/mixins/scheduler.py +++ b/src/axolotl/core/trainers/mixins/scheduler.py @@ -25,7 +25,7 @@ class SchedulerMixin(Trainer): args = None # type: "AxolotlTrainingArguments" # type: ignore[name-defined] def create_scheduler( - self, num_training_steps: int, optimizer: torch.optim.Optimizer = None + self, num_training_steps: int, optimizer: None | torch.optim.Optimizer = None ) -> LRScheduler: """ Set up the scheduler. The optimizer of the trainer must have been set up either before this method is called or @@ -45,6 +45,13 @@ def create_scheduler( and self.args.cosine_min_lr_ratio is not None ) + if optimizer is None: + if self.optimizer is None: + raise ValueError( + "Optimizer must be set before calling create_scheduler or passed as an argument." + ) + optimizer = self.optimizer + # fmt: off if self.lr_scheduler is None: # type: ignore # fmt: on From f447bce1dbdf4f6977b08c2729f0798353ee4c7d Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 2 Mar 2026 15:31:20 +0700 Subject: [PATCH 1145/1405] fix: do not push telemetry on non-master rank (#3438) --- src/axolotl/telemetry/manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/axolotl/telemetry/manager.py b/src/axolotl/telemetry/manager.py index 0774b68a86..4fdda89a95 100644 --- a/src/axolotl/telemetry/manager.py +++ b/src/axolotl/telemetry/manager.py @@ -156,6 +156,10 @@ def _check_telemetry_enabled(self) -> bool: Returns: Boolean denoting whether telemetry is enabled or not. """ + # Only rank 0 will send telemetry + if not is_main_process(): + return False + # Parse relevant env vars axolotl_do_not_track = os.getenv("AXOLOTL_DO_NOT_TRACK") do_not_track = os.getenv("DO_NOT_TRACK") @@ -169,10 +173,6 @@ def _check_telemetry_enabled(self) -> bool: ): return True - # Only rank 0 will send telemetry - if not is_main_process(): - return False - if do_not_track is None: do_not_track = "0" From aa88c2e30b3e24fa1bf74d5906945fb79f573d15 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 2 Mar 2026 12:26:08 -0500 Subject: [PATCH 1146/1405] fix uv cache subcommand (#3447) --- docker/Dockerfile-uv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile-uv b/docker/Dockerfile-uv index b9be1ff732..aa03f22a81 100644 --- a/docker/Dockerfile-uv +++ b/docker/Dockerfile-uv @@ -35,7 +35,7 @@ RUN if [ "$TARGETARCH" = "arm64" ]; then \ python scripts/unsloth_install.py --uv | sh && \ python scripts/cutcrossentropy_install.py --uv | sh && \ uv pip install pytest && \ - uv pip cache purge + uv cache clean # fix so that git fetch/pull from remote works with shallow clone RUN git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && \ From 444020b3327fee3bccb58fb2297ca9102cbdf9ea Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 2 Mar 2026 12:26:30 -0500 Subject: [PATCH 1147/1405] mark slow tests that are timing out in CI (#3428) [skip ci] --- tests/e2e/multigpu/test_fsdp2.py | 1 + tests/test_tokenizers.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/e2e/multigpu/test_fsdp2.py b/tests/e2e/multigpu/test_fsdp2.py index e104562407..a70ff9aa79 100644 --- a/tests/e2e/multigpu/test_fsdp2.py +++ b/tests/e2e/multigpu/test_fsdp2.py @@ -423,6 +423,7 @@ def test_dpo_fft(self, temp_dir): verify_training_success(temp_dir) + @pytest.mark.skip(reason="slow test w cu129 + torch 2.9.1 + py3.12") @require_torch_2_7_0 def test_dpo_lora(self, temp_dir): cfg = DictDefault( diff --git a/tests/test_tokenizers.py b/tests/test_tokenizers.py index 114c2bea2d..82cae9b4a8 100644 --- a/tests/test_tokenizers.py +++ b/tests/test_tokenizers.py @@ -116,6 +116,7 @@ def test_added_tokens_overrides(self, temp_dir): tokenizer.decode([128041, 128042]) == "RANDOM_OVERRIDE_1RANDOM_OVERRIDE_2" ) + @pytest.mark.skip("FIXME slow test sdist py3.11 + torch2.8.0") @enable_hf_offline def test_added_tokens_overrides_gemma3(self, temp_dir): cfg = DictDefault( From 474208b794c1b614a30a0ba0a41fa1d9af15b0ac Mon Sep 17 00:00:00 2001 From: Manas Vardhan Date: Mon, 2 Mar 2026 09:55:59 -0800 Subject: [PATCH 1148/1405] fix: Save de-duplicated dataset during pre-processing (#3427) * fix: run deduplication before saving dataset during preprocessing Move deduplicate_and_log_datasets call before save_preprocessed_dataset in both SFT and RL data loading pipelines. This ensures the saved preprocessed dataset is already de-duplicated, so subsequent loads from cache don't contain duplicates. Fixes #2719 * fix: include deduplication flag in dataset hash and warn on skip_prepare_dataset+dedup - Add dataset_exact_deduplication to the hash string in generate_dataset_hash_from_config so cached datasets are invalidated when the dedup setting changes. - Log a warning when skip_prepare_dataset=True and dataset_exact_deduplication=True, since dedup will be silently skipped in that configuration (both SFT and RL paths). * fix: add ValueError for skip_prepare+dedup, fix test mock target and formatting - Add config validator (check_deduplication_with_skip_prepare) that raises ValueError when skip_prepare_dataset=True and dataset_exact_deduplication=True - Replace runtime warnings in sft.py/rl.py with the validator check - Fix RL test: patch axolotl.utils.data.rl.load_tokenizer instead of axolotl.loaders.load_tokenizer to properly mock the imported reference - Fix ruff lint (remove unused imports) and formatting issues * refactor: inline deduplicate function per review feedback * fix test fixture, lint --------- Co-authored-by: ManasVardhan Co-authored-by: Wing Lian --- src/axolotl/utils/data/rl.py | 4 + src/axolotl/utils/data/sft.py | 28 ++-- src/axolotl/utils/data/shared.py | 3 +- src/axolotl/utils/schemas/config.py | 13 ++ tests/test_save_deduplicated.py | 210 ++++++++++++++++++++++++++++ 5 files changed, 237 insertions(+), 21 deletions(-) create mode 100644 tests/test_save_deduplicated.py diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index 5ea9e55e01..2c386f35ef 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -246,6 +246,10 @@ def _load_split(cfg: DictDefault, split: Literal["train", "test"]) -> Dataset: dataset = merge_datasets(split_datasets, cfg) if not cfg.skip_prepare_dataset: + # Deduplicate before saving so the saved dataset is already de-duplicated + if cfg.dataset_exact_deduplication: + dataset, _ = deduplicate_and_log_datasets(dataset=dataset) + # Save preprocessed dataset dataset_hash = generate_dataset_hash_from_config( cfg, datasets_configs, tokenizer.name_or_path diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index ba5aec2d60..69cbfb871b 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -351,6 +351,10 @@ def _load_raw_datasets( if cfg.sample_packing: dataset, _ = process_datasets_for_packing(cfg, dataset, None) + # Deduplicate before saving so the saved dataset is already de-duplicated + if cfg.dataset_exact_deduplication: + dataset, _ = deduplicate_and_log_datasets(dataset=dataset) + # Save the prepared dataset dataset_hash = generate_dataset_hash_from_config( cfg, datasets_configs, tokenizer.name_or_path @@ -438,25 +442,8 @@ def _handle_train_dataset_split( ) return train_dataset, eval_dataset - # No validation split - apply deduplication if needed and return as train dataset - if cfg.dataset_exact_deduplication: - train_dataset, _ = deduplicate_and_log_datasets(dataset=dataset) - else: - train_dataset = dataset - - return train_dataset, None - - -def _handle_test_dataset_split( - dataset: Dataset, cfg: DictDefault -) -> tuple[None, Dataset | None]: - """Handle processing for test split.""" - if cfg.dataset_exact_deduplication: - eval_dataset, _ = deduplicate_and_log_datasets(dataset=dataset) - else: - eval_dataset = dataset - - return None, eval_dataset + # No validation split - deduplication already applied during preprocessing + return dataset, None def _apply_dataset_sharding(dataset: Dataset, cfg: DictDefault) -> Dataset: @@ -515,6 +502,7 @@ def _load_and_prepare_datasets( if split == "train": train_dataset, eval_dataset = _handle_train_dataset_split(dataset, cfg) else: - train_dataset, eval_dataset = _handle_test_dataset_split(dataset, cfg) + # Deduplication already applied during preprocessing + train_dataset, eval_dataset = None, dataset return train_dataset, eval_dataset, prompters diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index a8ed55ae2f..351669ec3f 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -520,7 +520,8 @@ def generate_dataset_hash_from_config( """ config_str = ( f"{cfg.sequence_len}@{cfg.sample_packing}@{cfg.eval_sample_packing}@" - f"{cfg.group_by_length}@{cfg.kd_temperature or 1.0}|" + f"{cfg.group_by_length}@{cfg.kd_temperature or 1.0}@" + f"{cfg.dataset_exact_deduplication or False}|" f"{'|'.join(sorted([f'{d.path}:{d.type}:{d.shards}:{d.conversation}:{d.split}:{d.temperature or 1.0}' for d in cfg_datasets]))}" f"|{tokenizer_name}" ) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 35b4a69081..b15b99955b 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1509,3 +1509,16 @@ def check_deduplication_with_streaming(cls, data): "dataset_exact_deduplication is not available for streaming datasets. " ) return data + + @model_validator(mode="before") + @classmethod + def check_deduplication_with_skip_prepare(cls, data): + if data.get("dataset_exact_deduplication") and data.get("skip_prepare_dataset"): + raise ValueError( + "dataset_exact_deduplication=True has no effect when " + "skip_prepare_dataset=True. Deduplication runs as part of the " + "prepare pipeline, which is skipped. Either set " + "skip_prepare_dataset: false or disable " + "dataset_exact_deduplication." + ) + return data diff --git a/tests/test_save_deduplicated.py b/tests/test_save_deduplicated.py new file mode 100644 index 0000000000..1e41c3e10d --- /dev/null +++ b/tests/test_save_deduplicated.py @@ -0,0 +1,210 @@ +"""Tests to verify that deduplication runs before dataset saving during preprocessing. + +This addresses GitHub issue #2719: Save De-duplicated Set During Pre-processing. +""" + +from unittest.mock import MagicMock, patch + +from datasets import Dataset + +from axolotl.utils.dict import DictDefault + + +class TestSFTSaveDeduplicatedBeforeSave: + """Verify that in SFT data loading, deduplication occurs before saving.""" + + @patch("axolotl.utils.data.sft.save_preprocessed_dataset") + @patch("axolotl.utils.data.sft.generate_dataset_hash_from_config") + @patch("axolotl.utils.data.sft.deduplicate_and_log_datasets") + @patch("axolotl.utils.data.sft.merge_datasets") + @patch("axolotl.utils.data.sft._load_and_process_single_dataset") + @patch("axolotl.utils.data.sft.datasets_with_name_generator") + def test_dedup_called_before_save_sft( + self, + mock_datasets_gen, + mock_load_single, + mock_merge, + mock_dedup, + mock_gen_hash, + mock_save, + ): + """Deduplication should be called before save_preprocessed_dataset in SFT.""" + from axolotl.utils.data.sft import _load_raw_datasets + + # Set up mock data + dataset = Dataset.from_dict({"text": ["a", "b", "a"], "label": [1, 2, 1]}) + deduped_dataset = Dataset.from_dict({"text": ["a", "b"], "label": [1, 2]}) + + mock_datasets_gen.return_value = [ + DictDefault({"path": "test", "type": "alpaca"}) + ] + mock_load_single.return_value = (dataset, None) + mock_merge.return_value = dataset + mock_dedup.return_value = (deduped_dataset, None) + mock_gen_hash.return_value = "testhash" + + cfg = DictDefault( + { + "skip_prepare_dataset": False, + "dataset_exact_deduplication": True, + "sequence_len": 1024, + "eval_sequence_len": None, + "sample_packing": False, + "is_preprocess": False, + "seed": 42, + "datasets": [{"path": "test", "type": "alpaca"}], + } + ) + + tokenizer = MagicMock() + tokenizer.name_or_path = "test-tokenizer" + + # Track call order + call_order = [] + mock_dedup.side_effect = lambda **kwargs: ( + call_order.append("dedup") or (deduped_dataset, None) + ) + mock_save.side_effect = lambda *args, **kwargs: call_order.append("save") + + _load_raw_datasets( + cfg=cfg, + datasets_configs=cfg.datasets, + tokenizer=tokenizer, + split="train", + ) + + # Verify dedup was called + assert "dedup" in call_order, "Deduplication should have been called" + # Verify save was called + assert "save" in call_order, "Save should have been called" + # Verify dedup happened before save + assert call_order.index("dedup") < call_order.index("save"), ( + "Deduplication must occur before saving the dataset" + ) + + @patch("axolotl.utils.data.sft.save_preprocessed_dataset") + @patch("axolotl.utils.data.sft.generate_dataset_hash_from_config") + @patch("axolotl.utils.data.sft.merge_datasets") + @patch("axolotl.utils.data.sft._load_and_process_single_dataset") + @patch("axolotl.utils.data.sft.datasets_with_name_generator") + def test_no_dedup_when_disabled_sft( + self, + mock_datasets_gen, + mock_load_single, + mock_merge, + mock_gen_hash, + mock_save, + ): + """Deduplication should not be called when dataset_exact_deduplication is False.""" + from axolotl.utils.data.sft import _load_raw_datasets + + dataset = Dataset.from_dict({"text": ["a", "b", "a"], "label": [1, 2, 1]}) + + mock_datasets_gen.return_value = [ + DictDefault({"path": "test", "type": "alpaca"}) + ] + mock_load_single.return_value = (dataset, None) + mock_merge.return_value = dataset + mock_gen_hash.return_value = "testhash" + + cfg = DictDefault( + { + "skip_prepare_dataset": False, + "dataset_exact_deduplication": False, + "sequence_len": 1024, + "eval_sequence_len": None, + "sample_packing": False, + "is_preprocess": False, + "seed": 42, + "datasets": [{"path": "test", "type": "alpaca"}], + } + ) + + tokenizer = MagicMock() + tokenizer.name_or_path = "test-tokenizer" + + with patch("axolotl.utils.data.sft.deduplicate_and_log_datasets") as mock_dedup: + _load_raw_datasets( + cfg=cfg, + datasets_configs=cfg.datasets, + tokenizer=tokenizer, + split="train", + ) + mock_dedup.assert_not_called() + + +class TestRLSaveDeduplicatedBeforeSave: + """Verify that in RL data loading, deduplication occurs before saving.""" + + @patch.object(Dataset, "filter", lambda self, *args, **kwargs: self) + @patch("axolotl.utils.data.rl.save_preprocessed_dataset") + @patch("axolotl.utils.data.rl.generate_dataset_hash_from_config") + @patch("axolotl.utils.data.rl.deduplicate_and_log_datasets") + @patch("axolotl.utils.data.rl.merge_datasets") + @patch("axolotl.utils.data.rl.load_dataset_with_config") + @patch("axolotl.utils.data.rl.datasets_with_name_generator") + @patch("axolotl.utils.data.rl.load_tokenizer") + def test_dedup_called_before_save_rl( + self, + mock_load_tokenizer, + mock_datasets_gen, + mock_load_dataset, + mock_merge, + mock_dedup, + mock_gen_hash, + mock_save, + ): + """Deduplication should be called before save_preprocessed_dataset in RL.""" + from axolotl.utils.data.rl import _load_split + + dataset = Dataset.from_dict( + { + "prompt": ["hi", "bye", "hi"], + "chosen": ["a", "b", "a"], + "rejected": ["c", "d", "c"], + } + ) + deduped_dataset = Dataset.from_dict( + { + "prompt": ["hi", "bye"], + "chosen": ["a", "b"], + "rejected": ["c", "d"], + } + ) + + mock_datasets_gen.return_value = [DictDefault({"path": "test", "type": None})] + mock_load_dataset.return_value = dataset + mock_merge.return_value = dataset + mock_dedup.return_value = (deduped_dataset, None) + mock_gen_hash.return_value = "testhash" + + tokenizer = MagicMock() + tokenizer.name_or_path = "test-tokenizer" + mock_load_tokenizer.return_value = tokenizer + + cfg = DictDefault( + { + "skip_prepare_dataset": False, + "dataset_exact_deduplication": True, + "sequence_len": 1024, + "rl": "dpo", + "datasets": [{"path": "test", "type": None}], + "hf_use_auth_token": False, + "dataset_num_proc": 1, + "is_preprocess": False, + } + ) + + call_order = [] + mock_dedup.side_effect = lambda **kwargs: ( + call_order.append("dedup") or (deduped_dataset, None) + ) + mock_save.side_effect = lambda *args, **kwargs: call_order.append("save") + + _load_split(cfg, split="train") + + assert "dedup" in call_order, "Deduplication should have been called" + assert "save" in call_order, "Save should have been called" + assert call_order.index("dedup") < call_order.index("save"), ( + "Deduplication must occur before saving the dataset" + ) From 4272817109a060b30b7751139dfd3a6e43de838e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 2 Mar 2026 14:24:54 -0500 Subject: [PATCH 1149/1405] don't install torch ao on arm64 (#3448) --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 00a8486e22..fe00b9e143 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,11 @@ def parse_requirements(extras_require_map): try: xformers_version = [req for req in _install_requires if "xformers" in req][0] install_xformers = platform.machine() != "aarch64" + if platform.machine() == "aarch64": + # skip torchao on ARM64 + _install_requires = [ + req for req in _install_requires if "torchao" not in req + ] if "Darwin" in platform.system(): # skip packages not compatible with OSX skip_packages = [ From 77828d3559926c78d54506d65338cada68544d8c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 2 Mar 2026 16:39:26 -0500 Subject: [PATCH 1150/1405] uv cloud image should use uv w pip (#3449) --- docker/Dockerfile-cloud-uv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile-cloud-uv b/docker/Dockerfile-cloud-uv index dc6fd5377b..a53dd61355 100644 --- a/docker/Dockerfile-cloud-uv +++ b/docker/Dockerfile-cloud-uv @@ -12,7 +12,7 @@ EXPOSE 22 COPY scripts/cloud-entrypoint.sh /root/cloud-entrypoint.sh COPY scripts/motd /etc/motd -RUN pip install jupyterlab notebook ipywidgets && \ +RUN uv pip install jupyterlab notebook ipywidgets && \ jupyter lab clean RUN apt update && \ apt install --yes --no-install-recommends openssh-server tmux iproute2 nvtop && \ From e672d37f338f107b83f255baf95f00dfef05cc88 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 3 Mar 2026 21:26:46 +0700 Subject: [PATCH 1151/1405] fix: qwen3-next to use fla causal-conv1d to support packing (#3437 * fix: qwen3-next to use fla causal-conv1d to support packing * fix: causal import and update doc for v5 * fix: hard fail for packing without fla --- examples/qwen3-next/README.md | 25 ++--------- .../qwen3-next/qwen3-next-80b-a3b-qlora.yaml | 13 +++++- .../monkeypatch/models/qwen3_next/modeling.py | 45 ++++++++++++++----- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/examples/qwen3-next/README.md b/examples/qwen3-next/README.md index 3c3a26a762..df87ca7252 100644 --- a/examples/qwen3-next/README.md +++ b/examples/qwen3-next/README.md @@ -6,30 +6,13 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations ## Getting started -1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). You need to install from main as Qwen3-Next is only on nightly or use our latest [Docker images](https://docs.axolotl.ai/docs/docker.html). +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). - Here is an example of how to install from main for pip: - -```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0 min) -git clone https://github.com/axolotl-ai-cloud/axolotl.git -cd axolotl - -pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn]' - -# Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy -python scripts/cutcrossentropy_install.py | sh -``` - -2. Install Qwen3-Next transformers commit -```bash -pip3 uninstall -y transformers && pip3 install "git+https://github.com/huggingface/transformers.git@b9282355bea846b54ed850a066901496b19da654" -``` +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. 3. Install FLA for improved performance ```bash -pip3 uninstall -y causal-conv1d && pip3 install flash-linear-attention==0.3.2 +pip3 uninstall -y causal-conv1d && pip3 install flash-linear-attention==0.4.1 ``` 4. Run the finetuning example: @@ -38,7 +21,7 @@ pip3 uninstall -y causal-conv1d && pip3 install flash-linear-attention==0.3.2 axolotl train examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml ``` -This config uses about 45.62 GiB VRAM. +This config uses about ~47 GiB (no target experts) and ~71GiB (target experts) VRAM. Let us know how it goes. Happy finetuning! 🚀 diff --git a/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml b/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml index db841beab3..f63b1d1ce9 100644 --- a/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml +++ b/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml @@ -9,6 +9,8 @@ plugins: load_in_8bit: false load_in_4bit: true +quantize_moe_experts: true + datasets: - path: fozziethebeat/alpaca_messages_2k_test type: chat_template @@ -25,7 +27,7 @@ sample_packing: true lora_r: 16 lora_alpha: 8 -lora_dropout: 0.05 +lora_dropout: 0 lora_target_modules: - linear_attn.in_proj_ba - linear_attn.in_proj_qkvz @@ -34,12 +36,19 @@ lora_target_modules: - shared_expert.down_proj - shared_expert.gate_proj - shared_expert_gate - - mlp.gate - q_proj - v_proj - k_proj - o_proj +# lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj + +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + wandb_project: wandb_entity: wandb_watch: diff --git a/src/axolotl/monkeypatch/models/qwen3_next/modeling.py b/src/axolotl/monkeypatch/models/qwen3_next/modeling.py index d68992d0e8..48570ba422 100644 --- a/src/axolotl/monkeypatch/models/qwen3_next/modeling.py +++ b/src/axolotl/monkeypatch/models/qwen3_next/modeling.py @@ -9,6 +9,11 @@ LOG = get_logger(__name__) +try: + from fla.modules.convolution import causal_conv1d as fla_causal_conv1d +except ImportError: + fla_causal_conv1d = None + def get_cu_seqlens(position_ids): """ @@ -137,6 +142,11 @@ def patched_gated_delta_net_forward( and cache_position is not None ) + # Compute cu_seqlens early for use by both causal_conv1d and chunk_gated_delta_rule + cu_seqlens = None + if not use_precomputed_states and position_ids is not None: + cu_seqlens = get_cu_seqlens(position_ids=position_ids) + # getting projected states from cache if it exists if cache_params is not None: conv_state = cache_params.conv_states[self.layer_idx] @@ -151,12 +161,11 @@ def patched_gated_delta_net_forward( x.reshape(x.shape[0], x.shape[1], -1) for x in (query, key, value) ) - mixed_qkv = torch.cat((query, key, value), dim=-1) - mixed_qkv = mixed_qkv.transpose(1, 2) + mixed_qkv = torch.cat((query, key, value), dim=-1) # [B, T, D] if use_precomputed_states: - # 2. Convolution sequence transformation - # NOTE: the conv state is updated in `causal_conv1d_update` + # Inference single-token path: causal_conv1d_update expects [B, D, T] + mixed_qkv = mixed_qkv.transpose(1, 2) mixed_qkv = self.causal_conv1d_update( mixed_qkv, conv_state, @@ -164,24 +173,41 @@ def patched_gated_delta_net_forward( self.conv1d.bias, self.activation, ) + mixed_qkv = mixed_qkv.transpose(1, 2) else: if cache_params is not None: + # Cache state expects [B, D, T] for the inference update path + mixed_qkv_t = mixed_qkv.transpose(1, 2) conv_state = F.pad( - mixed_qkv, (self.conv_kernel_size - mixed_qkv.shape[-1], 0) + mixed_qkv_t, + (self.conv_kernel_size - mixed_qkv_t.shape[-1], 0), ) cache_params.conv_states[self.layer_idx] = conv_state - if self.causal_conv1d_fn is not None: - mixed_qkv = self.causal_conv1d_fn( + + if fla_causal_conv1d is not None: + # FLA Triton causal_conv1d: [B, T, D] in/out, with cu_seqlens support + mixed_qkv, _ = fla_causal_conv1d( x=mixed_qkv, weight=self.conv1d.weight.squeeze(1), bias=self.conv1d.bias, activation=self.activation, - seq_idx=None, + cu_seqlens=cu_seqlens, ) else: + # PyTorch fallback (no cu_seqlens support) + if cu_seqlens is not None and cu_seqlens.shape[0] > batch_size + 1: + raise RuntimeError( + "Packed sequences require fla.modules.convolution.causal_conv1d " + "(cu_seqlens support). Install flash-linear-attention or disable packing." + ) + LOG.warning_once( + "FLA causal_conv1d not available. Falling back to PyTorch conv1d." + ) + mixed_qkv = mixed_qkv.transpose(1, 2) mixed_qkv = F.silu(self.conv1d(mixed_qkv)[:, :, :seq_len]) + mixed_qkv = mixed_qkv.transpose(1, 2) - mixed_qkv = mixed_qkv.transpose(1, 2) + # mixed_qkv is [B, T, D] in all paths query, key, value = torch.split( mixed_qkv, [ @@ -203,7 +229,6 @@ def patched_gated_delta_net_forward( key = key.repeat_interleave(self.num_v_heads // self.num_k_heads, dim=2) if not use_precomputed_states: - cu_seqlens = get_cu_seqlens(position_ids=position_ids) core_attn_out, last_recurrent_state = self.chunk_gated_delta_rule( query, key, From 945c8aeb1026f7fc70ec4e6e36f1960a3f122862 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 3 Mar 2026 22:06:23 +0700 Subject: [PATCH 1152/1405] Fix: quantize and target moe layers in transformers v5 for adapters and many misc fixes (#3439) * fix: saving clones state dict * fix: apply fix for only CP mode * fix: add dropout check when using lora target param * fix: re-add patch from transformers PR #39866 * feat: add moe quant to test by ved * fix: try match target param properly end with * fix: clear cache per param quant * fix: attempt on-load quantize experts instead of post-load * fix: attempt disable async load * chore: add log * chore: adjust log * fix: remove cuda alloc for moe and enable async load * chore: remove leftover logs * chore: add extra empty cache * fix(doc): clarify support * fix: handle fsdp2 for paramwrapper dtensor * feat: attempt to quant experts in 8bit mode too * feat: attempt to release bf16 experts from vram * feat: upgrade cce * fix: fsdp2 init_sharded_param load int8/uint4 dtensor as require_grad=true on init * fix: remove unnecessary gc and empty cache * Revert "fix: remove unnecessary gc and empty cache" This reverts commit 1d54518990eb30739bc507ca63ea99f4f36b3829. * fix: do not call full_tensor on non-dtensors * fix: attempt to address fsdp2 with quant exp high loss * fix: attempt lora quant experts wrong dim * fix: ensure require_grad patch applied for lora 8bit * fix: attempt lora 8bit fsdp2 * fix: attribute access on save for lora 8bit fsdp2 * fix: wrong weight attrib access * chore(refactor): add config, re-arrange position of patches, clean comments * feat: add example docs * chore: cherry pick trinity fixes from PR 3399 * chore: comments refactor; add guards * fix: guard using wrong key * fix: mamba save does not accept main process param * fix: guard prevent double hook * fix: move gc to upper scope * chore: add comment on proxy forward patch * fix: add comment to clarify * feat: add test idempotency * fix: AttributeError: `e_score_correction_bias` is not an nn.Parameter * fix: AttributeError: 'NoneType' object has no attribute 'to' * fix: update docs on cpu_ram_efficient_loading --- .../colab-axolotl-example.ipynb | 2 +- examples/glm4.7-flash/README.md | 77 +++++++ examples/glm4.7-flash/lora.yaml | 65 ++++++ examples/glm4.7-flash/lora_fsdp.yaml | 75 +++++++ examples/glm4.7-flash/qlora.yaml | 65 ++++++ examples/glm4.7-flash/qlora_fsdp.yaml | 75 +++++++ examples/trinity/README.md | 10 +- .../trinity/trinity-nano-preview-qlora.yaml | 1 - scripts/cutcrossentropy_install.py | 2 +- src/axolotl/common/architectures.py | 3 + src/axolotl/core/trainers/base.py | 20 +- .../integrations/cut_cross_entropy/README.md | 6 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/integrations/kernels/README.md | 2 + src/axolotl/loaders/adapter.py | 2 +- src/axolotl/loaders/model.py | 7 + src/axolotl/loaders/patch_manager.py | 53 ++++- src/axolotl/models/mamba/modeling_mamba.py | 1 + src/axolotl/monkeypatch/accelerate/fsdp2.py | 97 ++++++++- src/axolotl/monkeypatch/fsdp2_qlora.py | 105 +++++++++- src/axolotl/monkeypatch/moe_quant.py | 188 ++++++++++++++++++ src/axolotl/utils/schemas/config.py | 31 +++ src/axolotl/utils/schemas/peft.py | 13 ++ .../schemas/validation/test_moe_quant.py | 142 +++++++++++++ 24 files changed, 1015 insertions(+), 29 deletions(-) create mode 100644 examples/glm4.7-flash/README.md create mode 100644 examples/glm4.7-flash/lora.yaml create mode 100644 examples/glm4.7-flash/lora_fsdp.yaml create mode 100644 examples/glm4.7-flash/qlora.yaml create mode 100644 examples/glm4.7-flash/qlora_fsdp.yaml create mode 100644 src/axolotl/monkeypatch/moe_quant.py create mode 100644 tests/utils/schemas/validation/test_moe_quant.py diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 2a00cd1d01..2cc27f211c 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@58d6572\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@a668583\"" ] }, { diff --git a/examples/glm4.7-flash/README.md b/examples/glm4.7-flash/README.md new file mode 100644 index 0000000000..6d7fd437a7 --- /dev/null +++ b/examples/glm4.7-flash/README.md @@ -0,0 +1,77 @@ +# Finetune Z.ai's GLM-4.7-Flash with Axolotl + +[GLM-4.7-Flash](https://huggingface.co/zai-org/GLM-4.7-Flash) is a 30B-A3B MoE model by Z.ai. + +This guide shows how to fine-tune it with Axolotl. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +3. Run the finetuning example: + +```bash +# QLoRA +# - no target experts (1x48GB @ ~24GiB/GPU) +# - target experts (1x48GB @ ~34GiB/GPU) +axolotl train examples/glm4.7-flash/qlora.yaml + +# QLoRA FSDP2 no target experts (2x48GB @ ~29GiB/GPU) +axolotl train examples/glm4.7-flash/qlora_fsdp.yaml +``` + +```bash +# LoRA +# - no target experts (1x48GB @ ~35GiB/GPU) +# - target experts (1x48GB @ OOM. Projected ~45-50GiB/GPU) +axolotl train examples/glm4.7-flash/lora.yaml + +# LoRA FSDP2 no target experts (2x48GB @ ~43GiB/GPU) +axolotl train examples/glm4.7-flash/lora_fsdp.yaml +``` + +### Expert LoRA + +To also apply LoRA adapters to expert weights, add `lora_target_parameters` to your config. + +Note: `lora_dropout` must be `0` when using `lora_target_parameters`. + +```yaml +lora_target_parameters: + - mlp.experts.gate_up_proj + - mlp.experts.down_proj + # - mlp.gate.weight # router, untested but should work, not normally targeted +``` + +## Limitations + +- **FSDP VRAM**: FSDP2 may use more VRAM per GPU than single GPU training. We suspect not all layers are properly sharded across ranks. +- **FSDP initial spike**: FSDP LoRA (8-bit) may have a large initial VRAM spike at the first 1-2 steps that then drops. FSDP QLoRA (4-bit) does not exhibit this. +- **cpu_ram_efficient_loading**: Must be set to `false` with FSDP2 — causes hang otherwise. +- **lora_target_linear**: Incompatible for this model. +- **LoRA kernels**: Incompatible with this model due to non-standard attention projections (DSA). Must be explicitly disabled (`lora_*_kernel: false`). + + +### TIPS + +- For inference, the official Z.ai team recommends these default settings (most tasks): + - `temperature: 1.0` + - `top_p: 0.95` + - `max_new_tokens: 131072` +- You can run a full finetuning by removing `adapter: qlora`, `load_in_4bit: true`, and `quantize_moe_experts: true` from the config. This is heavy, so we have not tested this. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Related Resources + +- [GLM-4.7-Flash on HuggingFace](https://huggingface.co/zai-org/GLM-4.7-Flash) +- [GLM-4.7 Blog](https://z.ai/blog/glm-4.7) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/glm4.7-flash/lora.yaml b/examples/glm4.7-flash/lora.yaml new file mode 100644 index 0000000000..2586babb72 --- /dev/null +++ b/examples/glm4.7-flash/lora.yaml @@ -0,0 +1,65 @@ +base_model: zai-org/GLM-4.7-Flash + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: true +quantize_moe_experts: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/glm4.7-flash-lora-8bit-out + +adapter: lora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0 +lora_target_modules: + - q_proj + - v_proj + - k_proj + - o_proj + +# Uncomment to also target MoE expert weights: +# lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj + +# LoRA kernels incompatible with DSA attention +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 diff --git a/examples/glm4.7-flash/lora_fsdp.yaml b/examples/glm4.7-flash/lora_fsdp.yaml new file mode 100644 index 0000000000..bee20bf02f --- /dev/null +++ b/examples/glm4.7-flash/lora_fsdp.yaml @@ -0,0 +1,75 @@ +base_model: zai-org/GLM-4.7-Flash + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: true +quantize_moe_experts: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/glm4.7-flash-lora-8bit-fsdp-out + +adapter: lora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0 +lora_target_modules: + - q_proj + - v_proj + - k_proj + - o_proj + +# Uncomment to also target MoE expert weights: +# lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj + +# LoRA kernels incompatible with DSA attention +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +fsdp_config: + fsdp_version: 2 + offload_params: false + cpu_ram_efficient_loading: false + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Glm4MoeLiteDecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true diff --git a/examples/glm4.7-flash/qlora.yaml b/examples/glm4.7-flash/qlora.yaml new file mode 100644 index 0000000000..834c46af80 --- /dev/null +++ b/examples/glm4.7-flash/qlora.yaml @@ -0,0 +1,65 @@ +base_model: zai-org/GLM-4.7-Flash + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_4bit: true +quantize_moe_experts: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/glm4.7-flash-qlora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0 +lora_target_modules: + - q_proj + - v_proj + - k_proj + - o_proj + +# Uncomment to also target MoE expert weights: +# lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj + +# LoRA kernels incompatible with DSA attention +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 diff --git a/examples/glm4.7-flash/qlora_fsdp.yaml b/examples/glm4.7-flash/qlora_fsdp.yaml new file mode 100644 index 0000000000..0bb87813ff --- /dev/null +++ b/examples/glm4.7-flash/qlora_fsdp.yaml @@ -0,0 +1,75 @@ +base_model: zai-org/GLM-4.7-Flash + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_4bit: true +quantize_moe_experts: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/glm4.7-flash-qlora-fsdp-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0 +lora_target_modules: + - q_proj + - v_proj + - k_proj + - o_proj + +# Uncomment to also target MoE expert weights: +# lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj + +# LoRA kernels incompatible with DSA attention +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +fsdp_config: + fsdp_version: 2 + offload_params: false + cpu_ram_efficient_loading: false + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Glm4MoeLiteDecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true diff --git a/examples/trinity/README.md b/examples/trinity/README.md index 4bbfcf29c1..e9710915ca 100644 --- a/examples/trinity/README.md +++ b/examples/trinity/README.md @@ -8,13 +8,15 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations 1. Install Axolotl following the main from the [installation guide](https://docs.axolotl.ai/docs/installation.html#sec-edge-build). -2. Run the finetuning example: +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +3. Run the finetuning example: ```bash axolotl train examples/trinity/trinity-nano-preview-qlora.yaml ``` -This config uses about 24.9 GiB VRAM. +This config uses about 24.9 GiB VRAM (w/o CCE). Let us know how it goes. Happy finetuning! 🚀 @@ -29,10 +31,6 @@ Let us know how it goes. Happy finetuning! 🚀 Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). -## Limitations - -**Cut Cross Entropy (CCE)**: Currently not supported. We plan to include CCE support for Trinity in the near future. - ## Related Resources - [Trinity Blog](https://www.arcee.ai/blog/the-trinity-manifesto) diff --git a/examples/trinity/trinity-nano-preview-qlora.yaml b/examples/trinity/trinity-nano-preview-qlora.yaml index de54fc8ac8..d8bf9f0739 100644 --- a/examples/trinity/trinity-nano-preview-qlora.yaml +++ b/examples/trinity/trinity-nano-preview-qlora.yaml @@ -1,5 +1,4 @@ base_model: arcee-ai/Trinity-Nano-Preview -trust_remote_code: true revision_of_model: 2ee94b0 # Automatically upload checkpoint and final model to HF diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index afc172e22e..f6cd0c4950 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@58d6572"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@a668583"' ) diff --git a/src/axolotl/common/architectures.py b/src/axolotl/common/architectures.py index f4d6ca9287..a409ed9f4e 100644 --- a/src/axolotl/common/architectures.py +++ b/src/axolotl/common/architectures.py @@ -18,4 +18,7 @@ "gpt_oss": "GptOssDecoderLayer", "lfm2_moe": "Lfm2MoeSparseMoeBlock", "afmoe": "AfmoeMoE", + "glm4_moe": "Glm4MoeDecoderLayer", + "glm4_moe_lite": "Glm4MoeLiteDecoderLayer", + "glm_moe_dsa": "GlmMoeDsaDecoderLayer", } diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 414abeb4d6..76e8f105f8 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -720,12 +720,16 @@ def _save(self, output_dir: Optional[str] = None, state_dict=None): os.makedirs(output_dir, exist_ok=True) LOG.info(f"Saving model checkpoint to {output_dir}") - # fix for Context Parallel save - if state_dict is None: - state_dict = self.accelerator.get_state_dict(self.model) - if state_dict is not None: + # fix for Context Parallel save: CP eval invalidates tensor storage + # pointers, so clone to CPU to get fresh valid storage for safetensors + if ( + state_dict is not None + and self.axolotl_cfg + and self.axolotl_cfg.context_parallel_size + and self.axolotl_cfg.context_parallel_size > 1 + ): state_dict = { - k: v.clone() if isinstance(v, torch.Tensor) else v + k: v.detach().cpu() if isinstance(v, torch.Tensor) else v for k, v in state_dict.items() } @@ -761,7 +765,11 @@ def _save(self, output_dir: Optional[str] = None, state_dict=None): metadata={"format": "pt"}, ) else: - self.model.save_pretrained(output_dir, state_dict=state_dict) + self.model.save_pretrained( + output_dir, + state_dict=state_dict, + is_main_process=self.accelerator.is_main_process, + ) if self.processing_class is not None: self.processing_class.save_pretrained(output_dir) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index b3f475dc2c..b892033da1 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@58d6572" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@a668583" ``` ## Usage @@ -88,9 +88,9 @@ plugins: - qwen2_vl - qwen3 - qwen3_5 +- qwen3_5_text - qwen3_5_moe -- qwen3_5_moe_vl -- qwen3_5_vl +- qwen3_5_moe_text - qwen3_moe - qwen3_next - qwen3_vl diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 36681d7705..5c207e0fc5 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@58d6572"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@a668583"`' ) diff --git a/src/axolotl/integrations/kernels/README.md b/src/axolotl/integrations/kernels/README.md index 96ff7b328e..237d653cf7 100644 --- a/src/axolotl/integrations/kernels/README.md +++ b/src/axolotl/integrations/kernels/README.md @@ -39,6 +39,8 @@ This works for any MoE model in transformers that uses a `SparseMoeBlock` class ScatterMoE uses a softmax -> topk routing, so results may be different for some model arch as baseline (GPT-OSS, GLM_MOE_DSA). +ScatterMoE does not work for GLM4.7 Flash (glm4_moe_lite) atm. + ## Note on MegaBlocks We tested [MegaBlocks](https://huggingface.co/kernels-community/megablocks) but were unable to ensure numerical accuracy, so we did not integrate it. It was also incompatible with many newer model architectures in transformers. diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index 3b64b23db9..eb7203c010 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -34,7 +34,7 @@ def temp_to_method(self, *args, **kwargs): return self for param in model.parameters(): - if isinstance(param, Params4bit): + if isinstance(param, Params4bit) and param.quant_state is not None: param.quant_state._orig_to = param.quant_state.to param.quant_state.to = types.MethodType(temp_to_method, param.quant_state) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 6c88855268..3be557a42d 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -172,7 +172,10 @@ def load(self) -> tuple[PreTrainedModel | PeftModelForCausalLM, PeftConfig | Non # Build the model PLUGIN_MANAGER.pre_model_load(self.cfg) self.patch_manager.apply_post_plugin_pre_model_load_patches() + skip_move_to_device = self._build_model() + self.patch_manager.apply_post_model_build_patches(self.model) + PLUGIN_MANAGER.post_model_build(self.cfg, self.model) # Post-build model configuration @@ -860,6 +863,10 @@ def _prepare_model_for_quantization(self): # Make sure everything is in the same dtype skip_prepare_model_for_kbit_training = True + if getattr(self.model, "_moe_experts_quantized", False): + # Parametrized expert tensors dequantize on access — would OOM. + skip_prepare_model_for_kbit_training = True + if ( not skip_prepare_model_for_kbit_training and self.cfg.adapter in ["lora", "qlora"] diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 62dcbde7af..87520c06f1 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -118,6 +118,7 @@ def apply_pre_model_load_patches(self): def apply_post_plugin_pre_model_load_patches(self): """Apply post plugin-pre_model_load load patches based on config.""" self._apply_tiled_mlp(self.cfg.model_config_type) + self._apply_moe_expert_quantization_patch() def _apply_transformers_patches(self): from axolotl.monkeypatch.transformers.trainer_loss_calc import ( @@ -135,6 +136,10 @@ def _apply_transformers_patches(self): patch_prepare_context_parallel_inputs() + def apply_post_model_build_patches(self, model: PreTrainedModel): + """Apply patches right after model build, before post-load setup.""" + self._finalize_moe_expert_quantization(model) + def apply_post_model_load_patches(self, model: PreTrainedModel): """Apply patches that require the model instance.""" self._apply_llama_flash_attn_patches(model) @@ -170,9 +175,14 @@ def _apply_fsdp_patches(self): patch_parallelism_config() if self.cfg.fsdp_config and str(self.cfg.fsdp_version) == "2": - from axolotl.monkeypatch.accelerate.fsdp2 import patch_accelerate_fsdp2 + from axolotl.monkeypatch.accelerate.fsdp2 import ( + patch_accelerate_fsdp2, + patch_tied_keys_for_meta_device, + ) patch_accelerate_fsdp2() + if self.cfg.fsdp_config.cpu_ram_efficient_loading: + patch_tied_keys_for_meta_device() if self.cfg.rl: from axolotl.monkeypatch.trainer.trl import patch_trl_prepare_fsdp2 @@ -352,15 +362,54 @@ def _apply_fsdp2_bnb_patches(self): if ( self.cfg.fsdp_config and str(self.cfg.fsdp_version) == "2" - and self.cfg.adapter == "qlora" + and (self.cfg.load_in_4bit or self.cfg.load_in_8bit) ): from axolotl.monkeypatch.fsdp2_qlora import ( + apply_init_dtype_attrs_patch, apply_init_sharded_param_patch, apply_init_unsharded_param_patch, + apply_linear8bitlt_save_patch, ) apply_init_sharded_param_patch() apply_init_unsharded_param_patch() + apply_init_dtype_attrs_patch() + if self.cfg.load_in_8bit: + apply_linear8bitlt_save_patch() + + def _apply_moe_expert_quantization_patch(self): + """Patch transformers weight loading to quantize MoE expert params on-the-fly.""" + if not self.cfg.quantize_moe_experts: + return + + from axolotl.monkeypatch.moe_quant import ( + patch_moe_quantization_on_load, + patch_peft_target_parameters_matching, + ) + + patch_moe_quantization_on_load(self.cfg) + patch_peft_target_parameters_matching() + + def _finalize_moe_expert_quantization(self, model: PreTrainedModel): + """Log quantization results and set model flag for downstream use.""" + import torch + + model._moe_experts_quantized = False + if self.cfg.quantize_moe_experts: + from axolotl.monkeypatch.moe_quant import get_moe_quantized_count + + count = get_moe_quantized_count() + if count > 0: + import gc + + model._moe_experts_quantized = True + LOG.info( + "Quantized %d MoE expert parameter(s) to %s during model loading", + count, + "4-bit" if self.cfg.load_in_4bit else "8-bit", + ) + gc.collect() + torch.cuda.empty_cache() def _apply_tiled_mlp(self, model_type: str): if self.cfg.tiled_mlp: diff --git a/src/axolotl/models/mamba/modeling_mamba.py b/src/axolotl/models/mamba/modeling_mamba.py index e6158a0a9a..b1847d6b56 100644 --- a/src/axolotl/models/mamba/modeling_mamba.py +++ b/src/axolotl/models/mamba/modeling_mamba.py @@ -111,6 +111,7 @@ def save_pretrained( self, save_directory: Union[str, os.PathLike], state_dict: Optional[dict] = None, + **kwargs, ): if state_dict is None: state_dict = self.state_dict() diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index af6f24a635..4a8d9840f1 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -150,13 +150,17 @@ def get_state_dict(self, model, unwrap=True): ) elif self.is_fsdp2: # https://github.com/pytorch/torchtune/blob/main/torchtune/training/_distributed.py#L465 + from torch.distributed.tensor import DTensor + state_dict = {} sharded_state_dict = model.state_dict() for param_name, param in sharded_state_dict.items(): if param.is_cpu: param = param.to(torch.device("cuda")) - param = param.full_tensor() + if isinstance(param, DTensor): + param = param.full_tensor() + if torch.distributed.get_rank() == 0: state_dict[param_name] = param.cpu() torch.distributed.barrier() @@ -182,10 +186,56 @@ def get_state_dict(self, model, unwrap=True): return state_dict +def patch_peft_param_wrapper_for_fsdp2(): + """Patch PEFT's _LoraParameterProxy.forward for FSDP2 DTensor compatibility. + + PEFT's ParamWrapper applies LoRA via torch.nn.utils.parametrize, which adds + delta_weight to the base weight W inside _LoraParameterProxy.forward(). + Under FSDP2, W may be a DTensor (from FSDP unshard) while delta_weight is a + regular Tensor (or vice versa), causing a RuntimeError on mixed types. + + This patch promotes the non-DTensor operand to match the DTensor's spec + using DTensor.from_local(), which is free for Replicate placement (just + metadata wrapping, no communication). + """ + from peft.tuners.lora.layer import _LoraParameterProxy + + if getattr(_LoraParameterProxy, "_axolotl_fsdp2_patched", False): + return + + _original_forward = _LoraParameterProxy.forward + + # NOTE: Replaces (not wraps) forward; assumes original is just `W + self.delta_weight`. + def _patched_forward(self, W): + from torch.distributed.tensor import DTensor + + delta = self.delta_weight + w_is_dt = isinstance(W, DTensor) + d_is_dt = isinstance(delta, DTensor) + + with torch.nn.utils.parametrize.cached(): + if w_is_dt == d_is_dt: + return W + delta + if w_is_dt: + return W + DTensor.from_local(delta, W.device_mesh, W.placements) + return DTensor.from_local(W, delta.device_mesh, delta.placements) + delta + + _LoraParameterProxy.forward = _patched_forward + _LoraParameterProxy._axolotl_fsdp2_patched = True + LOG.info("Patched PEFT _LoraParameterProxy.forward for FSDP2 DTensor compatibility") + + def _process_lora_module_for_fsdp(module, fsdp2_kwargs): """Helper function to process LoRA modules for FSDP2.""" + from peft.tuners.lora.layer import ParamWrapper from torch.distributed.fsdp import fully_shard + # Skip ParamWrapper — its lora_A/B must not be independently sharded. + # The parent decoder layer's FSDP wrapper handles unsharding them. + # TODO: review if we even need to shard them separately in first place. + if isinstance(module, ParamWrapper): + return False + log_bias_dtype_mismatch = False # Linear4Bit will keep it's bias term in fp32. If the weight dtype is in bf16 we are not able to @@ -327,6 +377,14 @@ def fsdp2_prepare_model(accelerator, model: torch.nn.Module) -> torch.nn.Module: is_peft_model = isinstance(model, PeftModel) + # Patch PEFT's _LoraParameterProxy for DTensor compatibility if any + # ParamWrapper modules exist (used for target_parameters / 3D expert params). + if is_peft_model: + from peft.tuners.lora.layer import ParamWrapper + + if any(isinstance(m, ParamWrapper) for m in model.modules()): + patch_peft_param_wrapper_for_fsdp2() + auto_wrap_policy = fsdp2_prepare_auto_wrap_policy(fsdp2_plugin, model) log_bias_dtype_mismatch = False if auto_wrap_policy is not None: @@ -376,6 +434,43 @@ def fsdp2_prepare_model(accelerator, model: torch.nn.Module) -> torch.nn.Module: return model +def patch_tied_keys_for_meta_device(): + """Patch _adjust_tied_keys_with_tied_pointers to skip meta tensors. + + Meta tensors all share data_ptr()==0, causing every parameter to be incorrectly + grouped as "tied". Skipping them is safe since they have no real storage. + """ + from collections import defaultdict + + from transformers import PreTrainedModel + + def _patched_adjust_tied_keys_with_tied_pointers(self, missing_keys): + param_pointers = defaultdict(list) + for param_name, param_value in self.state_dict().items(): + if param_value.is_meta: + continue + param_pointers[param_value.data_ptr()].append(param_name) + + tied_param_names = [ + names + for names in param_pointers.values() + if len(names) > 1 + and not any(name in self.all_tied_weights_keys.keys() for name in names) + and not all(name in missing_keys for name in names) + ] + + tied_weights_keys_by_pointers = { + param_name: group[0] + for group in tied_param_names + for param_name in group[1:] + } + self.all_tied_weights_keys.update(tied_weights_keys_by_pointers) + + PreTrainedModel._adjust_tied_keys_with_tied_pointers = ( + _patched_adjust_tied_keys_with_tied_pointers + ) + + def patch_accelerate_fsdp2(): import accelerate diff --git a/src/axolotl/monkeypatch/fsdp2_qlora.py b/src/axolotl/monkeypatch/fsdp2_qlora.py index 04d0d19710..1887c0a8aa 100644 --- a/src/axolotl/monkeypatch/fsdp2_qlora.py +++ b/src/axolotl/monkeypatch/fsdp2_qlora.py @@ -1,9 +1,10 @@ """ -Monkeypatch to add Params4bit support to FSDP2. This enables QLoRA + FSDP2, as well as -our LoRA / QLoRA Triton kernels to work with FSDP2. +Monkeypatch to add Params4bit and Int8Params support to FSDP2. This enables QLoRA + FSDP2 +and 8-bit LoRA + FSDP2, as well as our LoRA / QLoRA Triton kernels to work with FSDP2. -This patch modifies the _init_sharded_param method in FSDPParam to handle bitsandbytes -Params4bit parameters. +This patch modifies the _init_sharded_param and init_unsharded_param methods in FSDPParam +to handle bitsandbytes Params4bit and Int8Params parameters, preserving their quantization +metadata through the FSDP2 shard/unshard cycle. """ import importlib @@ -17,6 +18,8 @@ def apply_init_sharded_param_patch(): """Apply patch to FSDPParam._init_sharded_param to support Params4bit.""" + if getattr(apply_init_sharded_param_patch, "_axolotl_patched", False): + return from torch.distributed.fsdp._fully_shard._fsdp_param import FSDPParam # Get original source @@ -41,9 +44,20 @@ def apply_init_sharded_param_patch(): bnb_quantized=param.bnb_quantized, ) self.sharded_param = self.to_sharded_dtensor(self.sharded_param) + elif isinstance(param, bnb.nn.modules.Int8Params): + self.sharded_param = bnb.nn.modules.Int8Params( + data=sharded_param, + requires_grad=param.requires_grad, + has_fp16_weights=param.has_fp16_weights, + CB=None, + SCB=param.SCB, + ) + self.sharded_param = self.to_sharded_dtensor(self.sharded_param) else: - self.sharded_param = nn.Parameter(self.to_sharded_dtensor(sharded_param)) - self.sharded_param.requires_grad_(param.requires_grad)""" + self.sharded_param = nn.Parameter( + self.to_sharded_dtensor(sharded_param), + requires_grad=param.requires_grad, + )""" # Apply the replacement if original_param_creation in original_source: @@ -73,6 +87,7 @@ def apply_init_sharded_param_patch(): # Replace the method FSDPParam._init_sharded_param = patched_init_sharded_param + apply_init_sharded_param_patch._axolotl_patched = True LOG.info("Successfully applied FSDP _init_sharded_param patch") else: LOG.warning("Could not find target code for _init_sharded_param patching") @@ -80,6 +95,8 @@ def apply_init_sharded_param_patch(): def apply_init_unsharded_param_patch(): """Apply patch to FSDPParam.init_unsharded_param to support Params4bit.""" + if getattr(apply_init_unsharded_param_patch, "_axolotl_patched", False): + return from torch.distributed.fsdp._fully_shard._fsdp_param import FSDPParam # Get original source @@ -105,6 +122,14 @@ def apply_init_unsharded_param_patch(): module=local_tensor.module, bnb_quantized=local_tensor.bnb_quantized, ) + elif isinstance(local_tensor, bnb.nn.modules.Int8Params): + self._unsharded_param = bnb.nn.modules.Int8Params( + data=unsharded_param, + requires_grad=self.sharded_param.requires_grad, + has_fp16_weights=local_tensor.has_fp16_weights, + CB=unsharded_param, + SCB=local_tensor.SCB, + ) else: self._unsharded_param = nn.Parameter( unsharded_param, requires_grad=self.sharded_param.requires_grad @@ -138,6 +163,74 @@ def apply_init_unsharded_param_patch(): # Replace the method FSDPParam.init_unsharded_param = patched_init_unsharded_param + apply_init_unsharded_param_patch._axolotl_patched = True LOG.info("Successfully applied FSDP init_unsharded_param patch") else: LOG.warning("Could not find target code for patching") + + +def apply_linear8bitlt_save_patch(): + """Patch Linear8bitLt._save_to_state_dict to handle DTensor-wrapped Int8Params. + + After FSDP2 sharding, Linear8bitLt.weight is a DTensor wrapping Int8Params. + BnB's _save_to_state_dict accesses self.weight.SCB directly, but DTensor + doesn't proxy custom attribute access to its _local_tensor. This patch + temporarily unwraps the DTensor during saving so BnB can find the SCB attribute. + """ + if getattr(apply_linear8bitlt_save_patch, "_axolotl_patched", False): + return + import bitsandbytes as bnb + from torch.distributed.tensor import DTensor + + original_save = bnb.nn.Linear8bitLt._save_to_state_dict + + def _patched_save_to_state_dict(self, destination, prefix, keep_vars): + # Use _parameters dict directly to bypass nn.Module.__setattr__ type check. + weight = self._parameters["weight"] + unwrapped = False + if isinstance(weight, DTensor) and hasattr(weight, "_local_tensor"): + self._parameters["weight"] = weight._local_tensor + unwrapped = True + try: + original_save(self, destination, prefix, keep_vars) + finally: + if unwrapped: + self._parameters["weight"] = weight + + bnb.nn.Linear8bitLt._save_to_state_dict = _patched_save_to_state_dict + apply_linear8bitlt_save_patch._axolotl_patched = True + LOG.info("Patched Linear8bitLt._save_to_state_dict for DTensor compatibility") + + +def apply_init_dtype_attrs_patch(): + """Prevent FSDP2 mixed precision from casting non-float quantized params. + + When mixed precision is enabled (e.g., bf16), FSDP2's init_dtype_attrs sets + param_dtype=bf16 for ALL params. During all-gather, _to_dtype_if_needed casts + the sharded param to param_dtype. For non-float params (uint8 packed 4-bit, + int8 quantized) without FSDP2 extensions, this destroys the quantized data. + + Params4bit handles this via fsdp_pre/post_all_gather extensions, but our + parametrize-based expert quantization uses plain nn.Parameter(uint8/int8) + without extensions. + """ + if getattr(apply_init_dtype_attrs_patch, "_axolotl_patched", False): + return + from torch.distributed.fsdp._fully_shard._fsdp_param import FSDPParam + + original_init_dtype_attrs = FSDPParam.init_dtype_attrs + + def patched_init_dtype_attrs(self, mp_policy): + original_init_dtype_attrs(self, mp_policy) + # Skip casting non-float quantized params (uint8/int8) without FSDP2 + # extensions — the parametrization chain handles dequantization. + if self.param_dtype is not None and not self.sharded_param.is_floating_point(): + local = self.sharded_param + if hasattr(local, "_local_tensor"): + local = local._local_tensor + if not hasattr(local, "fsdp_pre_all_gather"): + self.param_dtype = None + + FSDPParam.init_dtype_attrs = patched_init_dtype_attrs + apply_init_dtype_attrs_patch._axolotl_patched = True + LOG.info("Patched FSDPParam.init_dtype_attrs for non-float quantized params") diff --git a/src/axolotl/monkeypatch/moe_quant.py b/src/axolotl/monkeypatch/moe_quant.py new file mode 100644 index 0000000000..42beec6a9c --- /dev/null +++ b/src/axolotl/monkeypatch/moe_quant.py @@ -0,0 +1,188 @@ +""" +Loading-time quantization for MoE expert weights stored as 3D nn.Parameter tensors. + +In transformers v5, MoE models store expert weights as fused 3D tensors that BnB +skips (only targets nn.Linear). This module patches weight loading to quantize them +on-the-fly (4-bit via bitsandbytes parametrize, 8-bit via custom int8 parametrization), +reducing peak VRAM from "all experts in bf16" to "one expert at a time." +""" + +import bitsandbytes as bnb +import torch +import torch.nn.utils.parametrize as P + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +# Module-level state for the loading-time quantization patch. +_moe_load_state = { + "count": 0, + "mode": "4bit", + "quant_type": "nf4", + "compress_statistics": True, + "patched": False, +} + + +class Bnb8bitParametrization(torch.nn.Module): + """Parametrization that dequantizes int8 row-wise quantized data on access.""" + + def __init__(self, row_stats: torch.Tensor): + super().__init__() + self.register_buffer("row_stats", row_stats) + + @torch.no_grad() + def forward(self, quantized_param: torch.Tensor) -> torch.Tensor: + # Flatten 3D+ to 2D for BnB's dequant, then reshape back. + orig_shape = quantized_param.shape + if quantized_param.ndim > 2: + quantized_param = quantized_param.reshape(-1, orig_shape[-1]) + result = bnb.functional.int8_vectorwise_dequant(quantized_param, self.row_stats) + return result.reshape(orig_shape) + + +def _enable_parametrization_cache(module, inputs): + P._cache_enabled += 1 + + +def _disable_parametrization_cache(module, inputs, output): + P._cache_enabled -= 1 + if not P._cache_enabled: + P._cache = {} + + +def replace_parameter_8bit(module, param_name): + """Replace a module parameter with an 8-bit quantized version using parametrization.""" + original_param = getattr(module, param_name) + int8_data, row_stats, _ = bnb.functional.int8_vectorwise_quant( + original_param.data.to(torch.float16) + ) + + setattr(module, param_name, torch.nn.Parameter(int8_data, requires_grad=False)) + del original_param + + P.register_parametrization( + module, param_name, Bnb8bitParametrization(row_stats), unsafe=True + ) + + # Cache dequantized values during forward to avoid redundant dequantization. + if not getattr(module, "_axolotl_8bit_hooks_registered", False): + module.register_forward_pre_hook(_enable_parametrization_cache) + module.register_forward_hook(_disable_parametrization_cache) + module._axolotl_8bit_hooks_registered = True + + +def patch_moe_quantization_on_load(cfg): + """Patch transformers' weight loading to quantize MoE expert params on-the-fly. + + Wraps ``set_param_for_module`` so that 3D+ CUDA tensors with "expert" in their + name are quantized (4-bit or 8-bit) as they're loaded, keeping peak VRAM low. + """ + mode = "8bit" if getattr(cfg, "load_in_8bit", False) else "4bit" + _moe_load_state["mode"] = mode + _moe_load_state["count"] = 0 + + if _moe_load_state["patched"]: + LOG.debug("MoE loading-time quantization patch already active") + return + + import transformers.core_model_loading + import transformers.modeling_utils + + if mode == "4bit": + from bitsandbytes.nn.parametrize import replace_parameter_4bit + + quant_type = getattr(cfg, "bnb_4bit_quant_type", None) or "nf4" + compress_statistics = getattr(cfg, "bnb_4bit_use_double_quant", None) + if compress_statistics is None: + compress_statistics = True + + _moe_load_state["quant_type"] = quant_type + _moe_load_state["compress_statistics"] = compress_statistics + + # Disable caching_allocator_warmup — it pre-allocates a huge tensor at bf16 + # size for all params, defeating our on-load quantization VRAM savings. + def _noop_warmup(*args, **kwargs): + pass + + transformers.modeling_utils.caching_allocator_warmup = _noop_warmup + + original_set_param = transformers.core_model_loading.set_param_for_module + + def _patched_set_param_for_module(model, target_name, param_value, *args, **kwargs): + original_set_param(model, target_name, param_value, *args, **kwargs) + + # Quantize 3D+ expert params that BnB skipped (only on CUDA). + if param_value.ndim >= 3 and param_value.is_cuda: + mod_path, _, pname = target_name.rpartition(".") + mod = model.get_submodule(mod_path) if mod_path else model + if not isinstance(mod, (bnb.nn.Linear4bit, bnb.nn.Linear8bitLt)): + if "expert" not in target_name.lower(): + LOG.debug( + "Skipping non-expert 3D param: %s (shape=%s)", + target_name, + list(param_value.shape), + ) + return + + if _moe_load_state["mode"] == "4bit": + replace_parameter_4bit( + mod, + pname, + compress_statistics=_moe_load_state["compress_statistics"], + quant_type=_moe_load_state["quant_type"], + ) + else: + replace_parameter_8bit(mod, pname) + _moe_load_state["count"] += 1 + + # Release the bf16 tensor so CUDA memory is freed immediately. + param_value.data = torch.empty(0, device="cpu") + torch.cuda.empty_cache() + + transformers.core_model_loading.set_param_for_module = _patched_set_param_for_module + _moe_load_state["patched"] = True + + +def get_moe_quantized_count(): + """Return the number of expert parameters quantized during loading.""" + return _moe_load_state["count"] + + +def patch_peft_target_parameters_matching(): + """Fix PEFT's _inject_parameters to use suffix matching for parametrized modules.""" + if getattr(patch_peft_target_parameters_matching, "_axolotl_patched", False): + return + from peft.tuners.tuners_utils import BaseTuner + + original_inject = BaseTuner._inject_parameters + + def _patched_inject_parameters( + self, peft_config, model, adapter_name, low_cpu_mem_usage + ): + # Patch target_parameters to use full paths for parametrized modules + original_targets = list(peft_config.target_parameters) + expanded = set(original_targets) + + for module_name, module in model.named_modules(): + if not hasattr(module, "parametrizations"): + continue + for target in original_targets: + mod_path, _, param_name = target.rpartition(".") + if ( + module_name == mod_path or module_name.endswith("." + mod_path) + ) and hasattr(module, param_name): + expanded.add(f"{module_name}.{param_name}") + + peft_config.target_parameters = sorted(expanded) + try: + return original_inject( + self, peft_config, model, adapter_name, low_cpu_mem_usage + ) + finally: + peft_config.target_parameters = original_targets + + BaseTuner._inject_parameters = _patched_inject_parameters + patch_peft_target_parameters_matching._axolotl_patched = True + LOG.info("Patched PEFT _inject_parameters for parametrized module suffix matching") diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index b15b99955b..5c0d31ff0d 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -629,6 +629,17 @@ class AxolotlInputConfig( }, ) + quantize_moe_experts: bool = Field( + default=False, + json_schema_extra={ + "description": "Quantize MoE expert weights on load to reduce VRAM. " + "Requires adapter (lora/qlora) with load_in_4bit or load_in_8bit. " + "Requires CUDA (not compatible with ROCm or other backends). " + "Note: total parameter count may be reported incorrectly when enabled " + "(trainable param count is correct)." + }, + ) + scaling_softmax: bool | None = Field( default=None, json_schema_extra={ @@ -1289,6 +1300,26 @@ def check_multigpu_lora_kernels(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_quantize_moe_experts(cls, data): + if data.get("quantize_moe_experts"): + if data.get("adapter") not in ("lora", "qlora"): + raise ValueError("quantize_moe_experts requires adapter: lora or qlora") + if not (data.get("load_in_4bit") or data.get("load_in_8bit")): + raise ValueError( + "quantize_moe_experts requires load_in_4bit or load_in_8bit" + ) + if ( + data.get("capabilities") + and data["capabilities"].get("compute_capability") + and not data["capabilities"]["compute_capability"].startswith("sm_") + ): + raise ValueError( + "quantize_moe_experts requires CUDA (not compatible with ROCm or other backends)" + ) + return data + @model_validator(mode="before") @classmethod def check_auto_enable_lora_kernels(cls, data): diff --git a/src/axolotl/utils/schemas/peft.py b/src/axolotl/utils/schemas/peft.py index a86de78226..5b90fb63f3 100644 --- a/src/axolotl/utils/schemas/peft.py +++ b/src/axolotl/utils/schemas/peft.py @@ -209,6 +209,19 @@ def validate_lora_dropout(cls, data): data["lora_dropout"] = 0.0 return data + @model_validator(mode="after") + def validate_lora_target_parameters_dropout(self): + if ( + self.lora_target_parameters + and self.lora_dropout + and self.lora_dropout != 0.0 + ): + raise ValueError( + "lora_dropout must be 0 when lora_target_parameters is set. " + "PEFT's ParamWrapper does not support lora_dropout != 0." + ) + return self + class ReLoRAConfig(BaseModel): """ReLoRA configuration subset""" diff --git a/tests/utils/schemas/validation/test_moe_quant.py b/tests/utils/schemas/validation/test_moe_quant.py new file mode 100644 index 0000000000..b969cbb680 --- /dev/null +++ b/tests/utils/schemas/validation/test_moe_quant.py @@ -0,0 +1,142 @@ +"""Tests for MoE expert quantization config validation and PEFT patch idempotency.""" + +import pytest + +from axolotl.utils.config import validate_config +from axolotl.utils.dict import DictDefault + + +@pytest.fixture() +def gpu_caps(): + return {"compute_capability": "sm_89", "bf16": True, "n_gpu": 1, "n_node": 1} + + +@pytest.fixture() +def env_caps(): + return {"torch_version": "2.7.0"} + + +class TestQuantizeMoeExpertsValidation: + """Test suite for quantize_moe_experts config validator.""" + + def test_requires_adapter(self, min_base_cfg, gpu_caps, env_caps): + """quantize_moe_experts without adapter should fail.""" + cfg = ( + DictDefault( + quantize_moe_experts=True, + ) + | min_base_cfg + ) + with pytest.raises(ValueError, match="requires adapter"): + validate_config(cfg, capabilities=gpu_caps, env_capabilities=env_caps) + + def test_requires_quantization(self, min_base_cfg, gpu_caps, env_caps): + """quantize_moe_experts without load_in_4bit/8bit should fail.""" + cfg = ( + DictDefault( + quantize_moe_experts=True, + adapter="lora", + ) + | min_base_cfg + ) + with pytest.raises(ValueError, match="requires load_in_4bit or load_in_8bit"): + validate_config(cfg, capabilities=gpu_caps, env_capabilities=env_caps) + + def test_valid_qlora_4bit(self, min_base_cfg, gpu_caps, env_caps): + """quantize_moe_experts with qlora + 4bit should pass.""" + cfg = ( + DictDefault( + quantize_moe_experts=True, + adapter="qlora", + load_in_4bit=True, + ) + | min_base_cfg + ) + result = validate_config(cfg, capabilities=gpu_caps, env_capabilities=env_caps) + assert result["quantize_moe_experts"] is True + + def test_valid_lora_8bit(self, min_base_cfg, gpu_caps, env_caps): + """quantize_moe_experts with lora + 8bit should pass.""" + cfg = ( + DictDefault( + quantize_moe_experts=True, + adapter="lora", + load_in_8bit=True, + ) + | min_base_cfg + ) + result = validate_config(cfg, capabilities=gpu_caps, env_capabilities=env_caps) + assert result["quantize_moe_experts"] is True + + def test_false_skips_validation(self, min_base_cfg, gpu_caps, env_caps): + """quantize_moe_experts=false should not check adapter/quantization.""" + cfg = ( + DictDefault( + quantize_moe_experts=False, + ) + | min_base_cfg + ) + result = validate_config(cfg, capabilities=gpu_caps, env_capabilities=env_caps) + assert result["quantize_moe_experts"] is False + + def test_default_is_false(self, min_base_cfg, gpu_caps, env_caps): + """quantize_moe_experts should default to false.""" + cfg = DictDefault({}) | min_base_cfg + result = validate_config(cfg, capabilities=gpu_caps, env_capabilities=env_caps) + assert result["quantize_moe_experts"] is False + + +class TestLoraTargetParametersDropout: + """Test that lora_dropout must be 0 when lora_target_parameters is set.""" + + def test_rejects_nonzero_dropout(self, min_base_cfg): + """lora_dropout > 0 with lora_target_parameters should fail.""" + cfg = ( + DictDefault( + adapter="lora", + lora_target_parameters=["mlp.experts.gate_up_proj"], + lora_dropout=0.1, + load_in_8bit=True, + ) + | min_base_cfg + ) + with pytest.raises(ValueError, match="lora_dropout must be 0"): + validate_config(cfg) + + def test_zero_dropout_passes(self, min_base_cfg): + """lora_dropout=0 with lora_target_parameters should pass.""" + cfg = ( + DictDefault( + adapter="lora", + lora_target_parameters=["mlp.experts.gate_up_proj"], + lora_dropout=0.0, + load_in_8bit=True, + ) + | min_base_cfg + ) + result = validate_config(cfg) + assert result["lora_dropout"] == 0.0 + + +class TestPeftPatchIdempotency: + """Test that patch_peft_target_parameters_matching is idempotent.""" + + def test_double_call_does_not_stack_wrappers(self): + """Calling patch twice should not double-wrap _inject_parameters.""" + from peft.tuners.tuners_utils import BaseTuner + + from axolotl.monkeypatch.moe_quant import ( + patch_peft_target_parameters_matching, + ) + + original = BaseTuner._inject_parameters + try: + patch_peft_target_parameters_matching() + first_patched = BaseTuner._inject_parameters + patch_peft_target_parameters_matching() + second_patched = BaseTuner._inject_parameters + # Should be same function, not double-wrapped + assert first_patched is second_patched + finally: + BaseTuner._inject_parameters = original + patch_peft_target_parameters_matching._axolotl_patched = False From 653f90be2534d344eefe0ac52386492c9087da71 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 3 Mar 2026 13:01:52 -0500 Subject: [PATCH 1153/1405] Add torch 2.10.0 to unit tests and use python 3.14 (#3450) * Add torch 2.10.0 to unit tests and use python 3.14 * hold on python 3.14 checks due to mistral common * add base option to matrix --- .github/workflows/base.yml | 16 ++++++++++++++++ .github/workflows/tests.yml | 36 +++++++++++++++++++++--------------- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 81b42bebb5..f025f55834 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -51,6 +51,14 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" platforms: "linux/amd64,linux/arm64" + - cuda: "128" + cuda_version: 12.8.1 + cudnn_version: "" + python_version: "3.11" + pytorch: 2.10.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-base" + platforms: "linux/amd64,linux/arm64" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" @@ -181,6 +189,14 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" + - cuda: "128" + cuda_version: 12.8.1 + cudnn_version: "" + python_version: "3.1" + pytorch: 2.10.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-uv-base" + platforms: "linux/amd64,linux/arm64" # - cuda: "129" # cuda_version: 12.9.1 # cudnn_version: "" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d3a53d2ce1..60610450a0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,13 +54,13 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.11", "3.12"] - pytorch_version: ["2.8.0", "2.9.0", "2.9.1"] - exclude: - - python_version: "3.12" - pytorch_version: "2.8.0" - - python_version: "3.12" - pytorch_version: "2.9.0" + python_version: ["3.12"] # TODO include py3.14 once https://github.com/mistralai/mistral-common/pull/194 is merged + pytorch_version: ["2.8.0", "2.9.1", "2.10.0"] +# exclude: +# - python_version: "3.14" +# pytorch_version: "2.8.0" +# - python_version: "3.14" +# pytorch_version: "2.9.1" timeout-minutes: 20 steps: @@ -149,13 +149,13 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.11", "3.12"] - pytorch_version: ["2.8.0", "2.9.0", "2.9.1"] - exclude: - - python_version: "3.12" - pytorch_version: "2.8.0" - - python_version: "3.12" - pytorch_version: "2.9.0" + python_version: ["3.12"] # TODO include py3.14 once https://github.com/mistralai/mistral-common/pull/194 is merged + pytorch_version: ["2.8.0", "2.9.1", "2.10.0"] +# exclude: +# - python_version: "3.14" +# pytorch_version: "2.8.0" +# - python_version: "3.14" +# pytorch_version: "2.9.1" timeout-minutes: 20 steps: @@ -326,6 +326,12 @@ jobs: pytorch: 2.9.1 num_gpus: 1 axolotl_extras: + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.10.0 + num_gpus: 1 + axolotl_extras: - cuda: 130 cuda_version: 13.0.0 python_version: "3.11" @@ -371,7 +377,7 @@ jobs: include: - cuda: 129 cuda_version: 12.9.1 - python_version: "3.12" + python_version: "3.11" pytorch: 2.9.1 num_gpus: 1 axolotl_extras: From b6b8db805ac32f5f16a02637ea49ee4a39f17501 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 4 Mar 2026 09:53:35 -0500 Subject: [PATCH 1154/1405] fix python version typo for building 3.11 (#3454) --- .github/workflows/base.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index f025f55834..10326fba90 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -184,7 +184,7 @@ jobs: - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" - python_version: "3.12" + python_version: "3.11" pytorch: 2.10.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" @@ -192,7 +192,7 @@ jobs: - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" - python_version: "3.1" + python_version: "3.12" pytorch: 2.10.0 torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" From 753906cfc7a669e9b1788193174304bbd9059a8e Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 5 Mar 2026 21:58:09 +0700 Subject: [PATCH 1155/1405] feat: add doc for expert quantization, glm45 air example configs, and update readme for release (#3452) [skip ci] * chore: rename without period * feat: add glm45 air * feat: add doc on expert quantization * feat: update base readme with new changes * chore: cleanup * chore: cleanup * chore: cleanup * fix: disable quantize_moe_expert on merge per comment * chore: add kernel info to optimizations doc --- README.md | 32 ++++++--- _quarto.yml | 1 + docs/expert_quantization.qmd | 66 +++++++++++++++++ docs/optimizations.qmd | 16 +++++ examples/glm45/README.md | 72 +++++++++++++++++++ examples/glm45/glm-45-air-qlora.yaml | 64 +++++++++++++++++ .../{glm4.7-flash => glm47-flash}/README.md | 24 ++----- .../{glm4.7-flash => glm47-flash}/lora.yaml | 0 .../lora_fsdp.yaml | 0 .../{glm4.7-flash => glm47-flash}/qlora.yaml | 0 .../qlora_fsdp.yaml | 0 src/axolotl/cli/merge_lora.py | 1 + src/axolotl/monkeypatch/multipack.py | 1 + 13 files changed, 248 insertions(+), 29 deletions(-) create mode 100644 docs/expert_quantization.qmd create mode 100644 examples/glm45/README.md create mode 100644 examples/glm45/glm-45-air-qlora.yaml rename examples/{glm4.7-flash => glm47-flash}/README.md (67%) rename examples/{glm4.7-flash => glm47-flash}/lora.yaml (100%) rename examples/{glm4.7-flash => glm47-flash}/lora_fsdp.yaml (100%) rename examples/{glm4.7-flash => glm47-flash}/qlora.yaml (100%) rename examples/{glm4.7-flash => glm47-flash}/qlora_fsdp.yaml (100%) diff --git a/README.md b/README.md index b56cdf0e84..9c7a8a4931 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,23 @@ ## 🎉 Latest Updates -- 2025/12: Axolotl now includes support for [Kimi-Linear](https://docs.axolotl.ai/docs/models/kimi-linear.html), [Plano-Orchestrator](https://docs.axolotl.ai/docs/models/plano.html), [MiMo](https://docs.axolotl.ai/docs/models/mimo.html), [InternVL 3.5](https://docs.axolotl.ai/docs/models/internvl3_5.html), [Olmo3](https://docs.axolotl.ai/docs/models/olmo3.html), [Trinity](https://docs.axolotl.ai/docs/models/trinity.html), and [Ministral3](https://docs.axolotl.ai/docs/models/ministral3.html). +- 2026/03: + - New model support has been added in Axolotl for Qwen3.5, Qwen3.5 MoE, [GLM-4.7-Flash](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm47-flash), [GLM-4.6V](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm46v), and [GLM-4.5-Air](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm45). + - [MoE expert quantization](https://docs.axolotl.ai/docs/expert_quantization.html) support (via `quantize_moe_experts: true`) greatly reduces VRAM when training MoE models (FSDP2 compat). +- 2026/02: + - [ScatterMoE LoRA](https://github.com/axolotl-ai-cloud/axolotl/pull/3410) support. LoRA fine-tuning directly on MoE expert weights using custom Triton kernels. + - Axolotl now has support for [SageAttention](https://github.com/axolotl-ai-cloud/axolotl/pull/2823) and [GDPO](https://github.com/axolotl-ai-cloud/axolotl/pull/3353) (Generalized DPO). +- 2026/01: + - New integration for [EAFT](https://github.com/axolotl-ai-cloud/axolotl/pull/3366) (Entropy-Aware Focal Training), weights loss by entropy of the top-k logit distribution, and [Scalable Softmax](https://github.com/axolotl-ai-cloud/axolotl/pull/3338), improves long context in attention. +- 2025/12: + - Axolotl now includes support for [Kimi-Linear](https://docs.axolotl.ai/docs/models/kimi-linear.html), [Plano-Orchestrator](https://docs.axolotl.ai/docs/models/plano.html), [MiMo](https://docs.axolotl.ai/docs/models/mimo.html), [InternVL 3.5](https://docs.axolotl.ai/docs/models/internvl3_5.html), [Olmo3](https://docs.axolotl.ai/docs/models/olmo3.html), [Trinity](https://docs.axolotl.ai/docs/models/trinity.html), and [Ministral3](https://docs.axolotl.ai/docs/models/ministral3.html). + - [Distributed Muon Optimizer](https://github.com/axolotl-ai-cloud/axolotl/pull/3264) support has been added for FSDP2 pretraining. - 2025/10: New model support has been added in Axolotl for: [Qwen3 Next](https://docs.axolotl.ai/docs/models/qwen3-next.html), [Qwen2.5-vl, Qwen3-vl](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen2_5-vl), [Qwen3, Qwen3MoE](https://docs.axolotl.ai/docs/models/qwen3.html), [Granite 4](https://docs.axolotl.ai/docs/models/granite4.html), [HunYuan](https://docs.axolotl.ai/docs/models/hunyuan.html), [Magistral 2509](https://docs.axolotl.ai/docs/models/magistral/vision.html), [Apertus](https://docs.axolotl.ai/docs/models/apertus.html), and [Seed-OSS](https://docs.axolotl.ai/docs/models/seed-oss.html). + +
+ +Expand older updates + - 2025/09: Axolotl now has text diffusion training. Read more [here](https://github.com/axolotl-ai-cloud/axolotl/tree/main/src/axolotl/integrations/diffusion). - 2025/08: QAT has been updated to include NVFP4 support. See [PR](https://github.com/axolotl-ai-cloud/axolotl/pull/3107). - 2025/07: @@ -39,15 +54,10 @@ - FP8 finetuning with fp8 gather op is now possible in Axolotl via `torchao`. Get started [here](https://docs.axolotl.ai/docs/mixed_precision.html#sec-fp8)! - [Voxtral](https://docs.axolotl.ai/docs/models/voxtral.html), [Magistral 1.1](https://docs.axolotl.ai/docs/models/magistral.html), and [Devstral](https://docs.axolotl.ai/docs/models/devstral.html) with mistral-common tokenizer support has been integrated in Axolotl! - TiledMLP support for single-GPU to multi-GPU training with DDP, DeepSpeed and FSDP support has been added to support Arctic Long Sequence Training. (ALST). See [examples](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/alst) for using ALST with Axolotl! -- 2025/05: Quantization Aware Training (QAT) support has been added to Axolotl. Explore the [docs](https://docs.axolotl.ai/docs/qat.html) to learn more! - -
- -Expand older updates - -- 2025/03: Axolotl has implemented Sequence Parallelism (SP) support. Read the [blog](https://huggingface.co/blog/axolotl-ai-co/long-context-with-sequence-parallelism-in-axolotl) and [docs](https://docs.axolotl.ai/docs/sequence_parallelism.html) to learn how to scale your context length when fine-tuning. - 2025/06: Magistral with mistral-common tokenizer support has been added to Axolotl. See [docs](https://docs.axolotl.ai/docs/models/magistral.html) to start training your own Magistral models with Axolotl! +- 2025/05: Quantization Aware Training (QAT) support has been added to Axolotl. Explore the [docs](https://docs.axolotl.ai/docs/qat.html) to learn more! - 2025/04: Llama 4 support has been added in Axolotl. See [docs](https://docs.axolotl.ai/docs/models/llama-4.html) to start training your own Llama 4 models with Axolotl's linearized version! +- 2025/03: Axolotl has implemented Sequence Parallelism (SP) support. Read the [blog](https://huggingface.co/blog/axolotl-ai-co/long-context-with-sequence-parallelism-in-axolotl) and [docs](https://docs.axolotl.ai/docs/sequence_parallelism.html) to learn how to scale your context length when fine-tuning. - 2025/03: (Beta) Fine-tuning Multimodal models is now supported in Axolotl. Check out the [docs](https://docs.axolotl.ai/docs/multimodal.html) to fine-tune your own! - 2025/02: Axolotl has added LoRA optimizations to reduce memory usage and improve training speed for LoRA and QLoRA in single GPU and multi-GPU training (DDP and DeepSpeed). Jump into the [docs](https://docs.axolotl.ai/docs/lora_optims.html) to give it a try. - 2025/02: Axolotl has added GRPO support. Dive into our [blog](https://huggingface.co/blog/axolotl-ai-co/training-llms-w-interpreter-feedback-wasm) and [GRPO example](https://github.com/axolotl-ai-cloud/grpo_code) and have some fun! @@ -62,10 +72,10 @@ Axolotl is a free and open-source tool designed to streamline post-training and Features: - **Multiple Model Support**: Train various models like GPT-OSS, LLaMA, Mistral, Mixtral, Pythia, and many more models available on the Hugging Face Hub. -- **Multimodal Training**: Fine-tune vision-language models (VLMs) including LLaMA-Vision, Qwen2-VL, Pixtral, LLaVA, SmolVLM2, and audio models like Voxtral with image, video, and audio support. -- **Training Methods**: Full fine-tuning, LoRA, QLoRA, GPTQ, QAT, Preference Tuning (DPO, IPO, KTO, ORPO), RL (GRPO), and Reward Modelling (RM) / Process Reward Modelling (PRM). +- **Multimodal Training**: Fine-tune vision-language models (VLMs) including LLaMA-Vision, Qwen2-VL, Pixtral, LLaVA, SmolVLM2, GLM-4.6V, InternVL 3.5, Gemma 3n, and audio models like Voxtral with image, video, and audio support. +- **Training Methods**: Full fine-tuning, LoRA, QLoRA, GPTQ, QAT, Preference Tuning (DPO, IPO, KTO, ORPO), RL (GRPO, GDPO), and Reward Modelling (RM) / Process Reward Modelling (PRM). - **Easy Configuration**: Re-use a single YAML configuration file across the full fine-tuning pipeline: dataset preprocessing, training, evaluation, quantization, and inference. -- **Performance Optimizations**: [Multipacking](https://docs.axolotl.ai/docs/multipack.html), [Flash Attention](https://github.com/Dao-AILab/flash-attention), [Xformers](https://github.com/facebookresearch/xformers), [Flex Attention](https://pytorch.org/blog/flexattention/), [Liger Kernel](https://github.com/linkedin/Liger-Kernel), [Cut Cross Entropy](https://github.com/apple/ml-cross-entropy/tree/main), [Sequence Parallelism (SP)](https://docs.axolotl.ai/docs/sequence_parallelism.html), [LoRA optimizations](https://docs.axolotl.ai/docs/lora_optims.html), [Multi-GPU training (FSDP1, FSDP2, DeepSpeed)](https://docs.axolotl.ai/docs/multi-gpu.html), [Multi-node training (Torchrun, Ray)](https://docs.axolotl.ai/docs/multi-node.html), and many more! +- **Performance Optimizations**: [Multipacking](https://docs.axolotl.ai/docs/multipack.html), [Flash Attention](https://github.com/Dao-AILab/flash-attention), [Xformers](https://github.com/facebookresearch/xformers), [Flex Attention](https://pytorch.org/blog/flexattention/), [SageAttention](https://github.com/thu-ml/SageAttention), [Liger Kernel](https://github.com/linkedin/Liger-Kernel), [Cut Cross Entropy](https://github.com/apple/ml-cross-entropy/tree/main), [ScatterMoE](https://docs.axolotl.ai/docs/custom_integrations.html#kernels-integration), [Sequence Parallelism (SP)](https://docs.axolotl.ai/docs/sequence_parallelism.html), [LoRA optimizations](https://docs.axolotl.ai/docs/lora_optims.html), [Multi-GPU training (FSDP1, FSDP2, DeepSpeed)](https://docs.axolotl.ai/docs/multi-gpu.html), [Multi-node training (Torchrun, Ray)](https://docs.axolotl.ai/docs/multi-node.html), and many more! - **Flexible Dataset Handling**: Load from local, HuggingFace, and cloud (S3, Azure, GCP, OCI) datasets. - **Cloud Ready**: We ship [Docker images](https://hub.docker.com/u/axolotlai) and also [PyPI packages](https://pypi.org/project/axolotl/) for use on cloud platforms and local hardware. diff --git a/_quarto.yml b/_quarto.yml index 4534c0a0e5..5e11691022 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -331,6 +331,7 @@ website: - docs/sequence_parallelism.qmd - docs/gradient_checkpointing.qmd - docs/nd_parallelism.qmd + - docs/expert_quantization.qmd - section: "Troubleshooting" contents: diff --git a/docs/expert_quantization.qmd b/docs/expert_quantization.qmd new file mode 100644 index 0000000000..7271e88643 --- /dev/null +++ b/docs/expert_quantization.qmd @@ -0,0 +1,66 @@ +--- +title: "MoE Expert Quantization" +description: "Reduce VRAM usage when training MoE model adapters by quantizing expert weights on load" +--- + +Transformers v5 changed MoE expert layers from `nn.Linear` to fused `nn.Parameter` (3D+ tensors). +This means `bitsandbytes` can no longer quantize them during model loading, resulting in all expert +weights being loaded in full bf16 precision and causing massive VRAM usage. + +`quantize_moe_experts` solves this by quantizing expert weights during model loading. +It intercepts the weight loading process, quantizes each expert tensor on the fly, and +immediately frees the original bf16 tensor from VRAM. This dramatically reduces peak memory. +For example, GLM-4.7-Flash QLoRA drops from ~127GiB to ~23GiB reserved memory. + +## Usage + +Enable expert quantization in your Axolotl config: + +```yaml +quantize_moe_experts: true +``` + +This works with both 4-bit (QLoRA) and 8-bit (LoRA) quantization. + +### Expert LoRA targeting + +You can optionally apply LoRA adapters directly to expert weights using `lora_target_parameters`: + +```yaml +lora_target_parameters: + - mlp.experts.gate_up_proj + - mlp.experts.down_proj + # - mlp.gate.weight # router +``` + +::: {.callout-note} +`lora_dropout` must be `0` when using `lora_target_parameters`. +::: + +## Requirements + +- Requires (`adapter: lora` and `load_in_8bit: true`) or (`adapter: qlora` and `load_in_4bit: true`) +- CUDA GPUs only (not tested with ROCm or other backends) +- FSDP2 compatible for distributed training + +## Limitations + +- `cpu_ram_efficient_loading` hangs / takes long time with FSDP2 + QLoRA. +- Total model parameter count may display incorrectly (trainable param count is correct). +- FSDP LoRA (8-bit) may have a large initial VRAM spike at the first 1-2 steps, which then drops. QLoRA does not exhibit this. +- FSDP2 may use more VRAM per GPU than single GPU training due to not all layers being properly sharded across ranks. +- Model loading takes longer due to on-demand quantization, even on consecutive runs. +- DeepSpeed has not been tested. + +## Implementation details + +The quantization is applied by patching transformers to intercept weight loading. +When a 3D+ CUDA tensor with "expert" in its name is detected: + +- **4-bit mode:** Uses bitsandbytes NF4 parametrization (configurable via `bnb_4bit_quant_type`). +- **8-bit mode:** Uses a custom row-wise int8 parametrization with bitsandbytes dequantization. + +The original bf16 tensor is freed immediately after quantization. Multiple sub-patches are applied to +transformers, PEFT and accelerate FSDP2 to support these parametrized expert modules. + +For full implementation details, see [PR #3439](https://github.com/axolotl-ai-cloud/axolotl/pull/3439). diff --git a/docs/optimizations.qmd b/docs/optimizations.qmd index 967ec2d34a..624738de71 100644 --- a/docs/optimizations.qmd +++ b/docs/optimizations.qmd @@ -66,6 +66,15 @@ Provides efficient Triton kernels to improve training speed and reduce memory us - **Learn more:** [Custom Integrations - Liger Kernels](custom_integrations.qmd#liger-kernels) +### Expert Kernels + +Optimized kernel implementations for Mixture of Experts (MoE) model training. + +- **ScatterMoE**: Triton-based MoE kernels with fused LoRA support. +- **SonicMoE**: CUTLASS-based MoE kernels for NVIDIA Hopper and Blackwell GPUs. + +- **Learn more:** [Custom Integrations - Kernels Integration](custom_integrations.qmd#kernels-integration) + ## Long Context Models Techniques to train models on sequences longer than their original context window. @@ -131,3 +140,10 @@ Simulates quantization effects during training, helping the model adapt and pote Allows you to finetune LoRA adapters on top of a model that has already been quantized using the GPTQ method. - **Example:** [GPTQ LoRA Example](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/llama-2/gptq-lora.yml) + +### MoE Expert Quantization + +Quantizes MoE expert weights on load to reduce VRAM when training MoE models with adapters. Required for Transformers v5+ MoE models where experts use fused `nn.Parameter` tensors. + +- **Config:** `quantize_moe_experts: true` +- **Learn more:** [MoE Expert Quantization](expert_quantization.qmd) diff --git a/examples/glm45/README.md b/examples/glm45/README.md new file mode 100644 index 0000000000..06c7834fcd --- /dev/null +++ b/examples/glm45/README.md @@ -0,0 +1,72 @@ +# Finetune Z.ai's GLM-4.5-Air with Axolotl + +[GLM-4.5-Air](https://huggingface.co/zai-org/GLM-4.5-Air) is a MoE model by Z.ai. + +This guide shows how to fine-tune it with Axolotl. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +3. Run the finetuning example: + +```bash +# QLoRA (1x80GB @ ~63.4GiB/GPU) +axolotl train examples/glm45/glm-45-air-qlora.yaml +``` + +### Dataset + +In addition to the standard OpenAI Messages format, GLM-4.5 supports an extra parameter for thinking in the assistant section. + +```json +{ + "role": "assistant", + "reasoning_content": "...", // or have ... in `content` + "content": "..." +} +``` + +Make sure you set the below extra attributes if needed: + +```yaml +datasets: + - path: ... + type: chat_template + message_property_mappings: + role: role + content: content + + # tool_calls: tool_calls # uncomment if using tools + # reasoning_content: reasoning_content # uncomment if have reasoning + +# Uncomment if training on tool role (you would rarely if ever need this) +# eot_tokens: +# - <|observation|> +``` + +### Tips + +- The role name for tools in this template is `tool`. +- You will see this Axolotl WARNING — this is expected as the template does not use EOS: + ``` + EOS token '<|endoftext|>' not found in chat_template. Please check if your template/EOS token is correct. + ``` +- You can run a full finetuning by removing `adapter: qlora`, `load_in_4bit: true`, and `quantize_moe_experts: true` from the config. +- **LoRA kernels**: Incompatible with this model. Must be explicitly disabled (`lora_*_kernel: false`). +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Related Resources + +- [GLM-4.5-Air on HuggingFace](https://huggingface.co/zai-org/GLM-4.5-Air) +- [GLM-4.5 Blog](https://z.ai/blog/glm-4.5) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/glm45/glm-45-air-qlora.yaml b/examples/glm45/glm-45-air-qlora.yaml new file mode 100644 index 0000000000..accb8898f5 --- /dev/null +++ b/examples/glm45/glm-45-air-qlora.yaml @@ -0,0 +1,64 @@ +base_model: zai-org/GLM-4.5-Air + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_8bit: false +load_in_4bit: true + +quantize_moe_experts: true # important + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/lora-out + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 16 +lora_alpha: 8 +lora_dropout: 0 +lora_target_modules: + - q_proj + - v_proj + - k_proj + - o_proj + +# lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj + +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +gradient_accumulation_steps: 2 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/glm4.7-flash/README.md b/examples/glm47-flash/README.md similarity index 67% rename from examples/glm4.7-flash/README.md rename to examples/glm47-flash/README.md index 6d7fd437a7..2e5e21010a 100644 --- a/examples/glm4.7-flash/README.md +++ b/examples/glm47-flash/README.md @@ -16,40 +16,28 @@ This guide shows how to fine-tune it with Axolotl. # QLoRA # - no target experts (1x48GB @ ~24GiB/GPU) # - target experts (1x48GB @ ~34GiB/GPU) -axolotl train examples/glm4.7-flash/qlora.yaml +axolotl train examples/glm47-flash/qlora.yaml # QLoRA FSDP2 no target experts (2x48GB @ ~29GiB/GPU) -axolotl train examples/glm4.7-flash/qlora_fsdp.yaml +axolotl train examples/glm47-flash/qlora_fsdp.yaml ``` ```bash # LoRA # - no target experts (1x48GB @ ~35GiB/GPU) # - target experts (1x48GB @ OOM. Projected ~45-50GiB/GPU) -axolotl train examples/glm4.7-flash/lora.yaml +axolotl train examples/glm47-flash/lora.yaml # LoRA FSDP2 no target experts (2x48GB @ ~43GiB/GPU) -axolotl train examples/glm4.7-flash/lora_fsdp.yaml +axolotl train examples/glm47-flash/lora_fsdp.yaml ``` -### Expert LoRA +### MoE Expert Quantization & Expert LoRA -To also apply LoRA adapters to expert weights, add `lora_target_parameters` to your config. - -Note: `lora_dropout` must be `0` when using `lora_target_parameters`. - -```yaml -lora_target_parameters: - - mlp.experts.gate_up_proj - - mlp.experts.down_proj - # - mlp.gate.weight # router, untested but should work, not normally targeted -``` +This model quantize expert weights on load. To learn about expert quantization, expert LoRA targeting, and related limitations, see the [MoE Expert Quantization](https://docs.axolotl.ai/docs/expert_quantization.html) docs. ## Limitations -- **FSDP VRAM**: FSDP2 may use more VRAM per GPU than single GPU training. We suspect not all layers are properly sharded across ranks. -- **FSDP initial spike**: FSDP LoRA (8-bit) may have a large initial VRAM spike at the first 1-2 steps that then drops. FSDP QLoRA (4-bit) does not exhibit this. -- **cpu_ram_efficient_loading**: Must be set to `false` with FSDP2 — causes hang otherwise. - **lora_target_linear**: Incompatible for this model. - **LoRA kernels**: Incompatible with this model due to non-standard attention projections (DSA). Must be explicitly disabled (`lora_*_kernel: false`). diff --git a/examples/glm4.7-flash/lora.yaml b/examples/glm47-flash/lora.yaml similarity index 100% rename from examples/glm4.7-flash/lora.yaml rename to examples/glm47-flash/lora.yaml diff --git a/examples/glm4.7-flash/lora_fsdp.yaml b/examples/glm47-flash/lora_fsdp.yaml similarity index 100% rename from examples/glm4.7-flash/lora_fsdp.yaml rename to examples/glm47-flash/lora_fsdp.yaml diff --git a/examples/glm4.7-flash/qlora.yaml b/examples/glm47-flash/qlora.yaml similarity index 100% rename from examples/glm4.7-flash/qlora.yaml rename to examples/glm47-flash/qlora.yaml diff --git a/examples/glm4.7-flash/qlora_fsdp.yaml b/examples/glm47-flash/qlora_fsdp.yaml similarity index 100% rename from examples/glm4.7-flash/qlora_fsdp.yaml rename to examples/glm47-flash/qlora_fsdp.yaml diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index e7ad890369..bc2dc84c76 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -71,6 +71,7 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: merge_lora=True, load_in_8bit=False, load_in_4bit=False, + quantize_moe_experts=False, flash_attention=False, context_parallel_size=None, deepspeed=None, diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 6a6b935bef..3208325ebe 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -37,6 +37,7 @@ "deepseek_v3", "glm", "glm4", + "glm4_moe", "smollm3", "granite", "granitemoe", From 8e2a102ccabbc65e33d05a61385b2d9bc3c0dfcd Mon Sep 17 00:00:00 2001 From: bekk02 Date: Thu, 5 Mar 2026 06:59:32 -0800 Subject: [PATCH 1156/1405] Fix FSDP2 sharding and validate AO version for LR groups (#3403) * Fix fsdp2 sharding. Fix validation of ao version for lr groups * remove validation since axolotl requires ao>0.13.0 already * Move fully_shard of entire module for lora_embedding_A/B out of loop * chore: lint --------- Co-authored-by: bekk02 Co-authored-by: Wing Lian --- src/axolotl/monkeypatch/accelerate/fsdp2.py | 16 ++++++++++++---- src/axolotl/utils/schemas/validation.py | 17 ----------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index 4a8d9840f1..dd3deb19a9 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -252,12 +252,20 @@ def _process_lora_module_for_fsdp(module, fsdp2_kwargs): fully_shard(module.lora_A[active_adapter], **fsdp2_kwargs) if module.lora_B: fully_shard(module.lora_B[active_adapter], **fsdp2_kwargs) - if module.lora_embedding_A: - fully_shard(module.lora_embedding_A[active_adapter], **fsdp2_kwargs) - if module.lora_embedding_B: - fully_shard(module.lora_embedding_B[active_adapter], **fsdp2_kwargs) if module.lora_magnitude_vector: fully_shard(module.lora_magnitude_vector[active_adapter], **fsdp2_kwargs) + + # lora_embedding_A/B are ParameterDicts containing nn.Parameter (Tensors), + # not nn.Module. fully_shard() only accepts nn.Module, so we cannot shard + # individual embedding Parameters. Instead, shard the entire LoraLayer module. fully_shard() can be used hierarchically because it does not + # override groups already assigned by fully_shard(), so modules + # where fully_shard() was already called are not affected [see https://docs.pytorch.org/docs/stable/distributed.fsdp.fully_shard.html] + if module.lora_embedding_A or module.lora_embedding_B: + from torch.distributed.fsdp import FSDPModule + + if not isinstance(module, FSDPModule): + fully_shard(module, **fsdp2_kwargs) + return log_bias_dtype_mismatch diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 7830174053..2ff57558f7 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -986,23 +986,6 @@ def check_fsdp2_w_8bit_optimizer(self): return self - @model_validator(mode="after") - def lr_groups_ao_optimizer(self): - if ( - self.loraplus_lr_ratio is not None - or self.embedding_lr_scale is not None - or self.embedding_lr is not None - or self.lr_groups is not None - ) and self.optimizer.value in ["adamw_torch_8bit", "adamw_torch_4bit"]: - # TODO(wing): remove this once ao>0.12.0 - # requires https://github.com/pytorch/ao/pull/2606 in an ao release - raise ValueError( - "lr groups (`loraplus_lr_ratio`, `embedding_lr_scale`, `embedding_lr`, `lr_groups`) are not " - "supported with ao low-bit optimizers until ao>0.12.0. " - "Please refer to https://github.com/pytorch/ao/pull/2606." - ) - return self - @model_validator(mode="before") @classmethod def check_tensor_parallel_size_update_ds_json(cls, data): From 28cc08528371da1e24172bb4117912f684ddccbf Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 5 Mar 2026 12:31:17 -0500 Subject: [PATCH 1157/1405] include number of params and rounded est of params so we can easily group in posthog (#3455) * include number of params and rounded est of params so we can easily group in posthog * fix typing --- src/axolotl/train.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 856996b62f..16c3696c07 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -86,9 +86,21 @@ def setup_model_and_tokenizer( if model.generation_config is not None: model.generation_config.do_sample = True - TELEMETRY_MANAGER.send_event( - event_type="model-load", properties=model.config.to_dict() - ) + model_properties = model.config.to_dict() + try: + model_properties["num_parameters"] = model.num_parameters() + except Exception: # pylint: disable=broad-exception-caught + model_properties["num_parameters"] = sum(p.numel() for p in model.parameters()) + # if the num_parameters is less than 2B, let's round to nearest 100M, else round to nearest 1B + if model_properties["num_parameters"] < 2e9: + model_properties["num_parameters_est"] = ( + f"{round(model_properties['num_parameters'] / 1e8) * 100}M" + ) + else: + model_properties["num_parameters_est"] = ( + f"{round(model_properties['num_parameters'] / 1e9)}B" + ) + TELEMETRY_MANAGER.send_event(event_type="model-load", properties=model_properties) if peft_config: TELEMETRY_MANAGER.send_event( event_type="peft-config-load", properties=peft_config.to_dict() From 4b8bc52424682faccc37774cc4460f142043339b Mon Sep 17 00:00:00 2001 From: Gilles Turpin Date: Thu, 5 Mar 2026 18:33:28 +0100 Subject: [PATCH 1158/1405] fix: correct total_num_steps and batch_size calculation with context parallelism (#3444) * fix: correct total_num_steps and batch_size calculation with context parallelism * feat: add test for CP batch size --------- Co-authored-by: NanoCode012 --- src/axolotl/utils/config/__init__.py | 3 +- src/axolotl/utils/trainer.py | 9 +--- tests/test_context_parallel_batch_size.py | 56 +++++++++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 tests/test_context_parallel_batch_size.py diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 8b35ed4062..1c0e93e034 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -119,7 +119,8 @@ def normalize_config(cfg): if cfg.world_size != 1: cfg.device_map = {"": int(os.environ.get("LOCAL_RANK", 0))} if cfg.fsdp or cfg.fsdp_config or cfg.ddp: - cfg.batch_size = cfg.batch_size * cfg.world_size + effective_world_size = cfg.world_size // (cfg.context_parallel_size or 1) + cfg.batch_size = cfg.batch_size * effective_world_size if not cfg.use_ray: # delay resolving dtype until on worker node when launching with ray diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index d97a74f6fe..320e59a908 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -457,7 +457,6 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): - 1 ) * cfg.num_epochs - * cfg.context_parallel_size * cfg.tensor_parallel_size ) LOG.debug( @@ -498,12 +497,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): # FIXME: is there a bug here somewhere? the total num steps depends # on the agreed on value for sample_packing_eff_est total_num_steps = int( - math.floor( - data_loader_len - * cfg.num_epochs - * cfg.context_parallel_size - * cfg.tensor_parallel_size - ) + math.floor(data_loader_len * cfg.num_epochs * cfg.tensor_parallel_size) ) if cfg.dataloader_drop_last: # drop the last batch for each epoch @@ -528,7 +522,6 @@ def calc_sample_packing_eff_est(estimates: List[float]): math.ceil( len(train_dataset) * cfg.num_epochs - * cfg.context_parallel_size * cfg.tensor_parallel_size / cfg.batch_size ) diff --git a/tests/test_context_parallel_batch_size.py b/tests/test_context_parallel_batch_size.py new file mode 100644 index 0000000000..8f6ed7b288 --- /dev/null +++ b/tests/test_context_parallel_batch_size.py @@ -0,0 +1,56 @@ +"""Tests for batch_size calculation with context parallelism.""" + +import sys +import types + +import pytest + +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + + +@pytest.fixture(name="cp_base_cfg") +def fixture_cp_base_cfg(min_base_cfg): + return ( + DictDefault( + micro_batch_size=2, + gradient_accumulation_steps=4, + sequence_len=2048, + num_epochs=1, + flash_attention=True, + ) + | min_base_cfg + ) + + +class TestContextParallelBatchSize: + """Verify batch_size scales by effective dp world_size when using context parallelism.""" + + @pytest.mark.parametrize( + "world_size, context_parallel_size, expected_batch_size", + [ + (4, 1, 32), # no CP: 2*4*4 = 32 + (4, 2, 16), # CP=2: 2*4*(4//2) = 16 + (4, 4, 8), # CP=4: 2*4*(4//4) = 8 + (2, 2, 8), # CP=ws: 2*4*(2//2) = 8 (no scaling) + ], + ) + def test_batch_size_with_context_parallelism( + self, + cp_base_cfg, + monkeypatch, + world_size, + context_parallel_size, + expected_batch_size, + ): + monkeypatch.setenv("WORLD_SIZE", str(world_size)) + # Mock ring_flash_attn since it's not installable on CPU, + # but required by schema validation when context_parallel_size > 1. + if "ring_flash_attn" not in sys.modules: + monkeypatch.setitem( + sys.modules, "ring_flash_attn", types.ModuleType("ring_flash_attn") + ) + cp_base_cfg["context_parallel_size"] = context_parallel_size + cfg = validate_config(cp_base_cfg) + normalize_config(cfg) + assert cfg.batch_size == expected_batch_size From 1eaf4d7418d08019282d8b75fed69aaabadedace Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:10:45 +0530 Subject: [PATCH 1159/1405] add: support mxfp4 axo (#3375) * mxfp4 axo * import lint * test for qat mxfp4 * config for mxfp4 * add qat: * pass base config * MXFakeQuantizeConfig * lint * tune config so it fits in 32GB VRAM --------- Co-authored-by: Wing Lian --- examples/llama-3/3b-qat-mxfp4.yaml | 65 +++++++++++++++++++++++ src/axolotl/utils/quantization.py | 36 ++++++++++++- src/axolotl/utils/schemas/enums.py | 3 ++ src/axolotl/utils/schemas/quantization.py | 3 ++ tests/e2e/test_qat.py | 31 +++++++++++ tests/e2e/test_quantization.py | 45 ++++++++++++++++ 6 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 examples/llama-3/3b-qat-mxfp4.yaml diff --git a/examples/llama-3/3b-qat-mxfp4.yaml b/examples/llama-3/3b-qat-mxfp4.yaml new file mode 100644 index 0000000000..7ae941e9e9 --- /dev/null +++ b/examples/llama-3/3b-qat-mxfp4.yaml @@ -0,0 +1,65 @@ +base_model: meta-llama/Llama-3.2-3B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: false +strict: false + +plugins: + - axolotl.integrations.liger.LigerPlugin + +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true +liger_fused_linear_cross_entropy: true + +datasets: + - path: yahma/alpaca-cleaned + type: alpaca + split: train[:95%] + +output_dir: ./outputs/qat_out/ +dataset_prepared_path: ./outputs/dataset_prepared + +sequence_len: 2048 +flash_attention: true + +qat: + activation_dtype: mxfp4 + weight_dtype: mxfp4 + group_size: 32 + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_checkpointing: true +activation_offloading: true +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_8bit + +cosine_constant_lr_ratio: 0 +cosine_min_lr_ratio: 1.0 +learning_rate: 2e-5 +save_only_model: true +bf16: true + +resume_from_checkpoint: +logging_steps: 1 + +evals_per_epoch: 1 +saves_per_epoch: 1 + +warmup_ratio: 0.1 +weight_decay: 0.0 + +special_tokens: + pad_token: <|finetune_right_pad_id|> + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/src/axolotl/utils/quantization.py b/src/axolotl/utils/quantization.py index 6c29a54429..43af858b1a 100644 --- a/src/axolotl/utils/quantization.py +++ b/src/axolotl/utils/quantization.py @@ -5,6 +5,7 @@ import torch from packaging import version from torchao.core.config import AOBaseConfig +from torchao.prototype.qat import MXFakeQuantizeConfig from torchao.quantization import quantize_ from torchao.quantization.qat import ( QATConfig, @@ -40,6 +41,13 @@ except: pass + try: + from torchao.prototype.qat import MXFakeQuantizeConfig + + quantization_config_to_str[MXFakeQuantizeConfig] = "mxfp4" + except ImportError: + pass + def get_quantization_config( weight_dtype: TorchAOQuantDType, @@ -109,6 +117,19 @@ def get_quantization_config( if group_size is not None and group_size != 16: raise ValueError("NVFP4 quantization must use a group_size of 16") return NVFP4InferenceConfig() + + if weight_dtype == TorchAOQuantDType.mxfp4: + from torchao.prototype.qat import MXFakeQuantizeConfig + + # MXFP4 uses block_size=32 by default (vs NVFP4's 16) + block_size = group_size if group_size is not None else 32 + if block_size != 32: + raise ValueError( + "MXFP4 quantization must use a block_size (group_size) of 32" + ) + + return MXFakeQuantizeConfig(dtype=torch.float4_e2m1fn_x2, block_size=block_size) + raise ValueError( f"Invalid activation/weight dtype combination: {activation_dtype}/{weight_dtype}" ) @@ -179,7 +200,13 @@ def prepare_model_for_qat( activation_dtype=activation_dtype, group_size=group_size, ) - qat_config = QATConfig(base_config) + if isinstance(base_config, MXFakeQuantizeConfig): + qat_config = QATConfig( + activation_config=base_config, + weight_config=base_config, + ) + else: + qat_config = QATConfig(base_config) quantize_(model, qat_config) if quantize_embedding: # activation fake quantization is not supported for embedding layers @@ -188,7 +215,12 @@ def prepare_model_for_qat( activation_dtype=None, group_size=group_size, ) - embedding_qat_config = QATConfig(embedding_base_config) + if isinstance(embedding_base_config, MXFakeQuantizeConfig): + embedding_qat_config = QATConfig( + weight_config=embedding_base_config, + ) + else: + embedding_qat_config = QATConfig(embedding_base_config) quantize_( model, embedding_qat_config, diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index b67888e0f8..893f232880 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -10,6 +10,7 @@ class TorchAOQuantDType(Enum): int8 = torch.int8 float8_e4m3fn = torch.float8_e4m3fn nvfp4 = "nvfp4" + mxfp4 = "mxfp4" def from_string(str): if str == "int4": @@ -20,6 +21,8 @@ def from_string(str): return TorchAOQuantDType.float8_e4m3fn if str == "nvfp4": return TorchAOQuantDType.nvfp4 + if str == "mxfp4": + return TorchAOQuantDType.mxfp4 class RLType(str, Enum): diff --git a/src/axolotl/utils/schemas/quantization.py b/src/axolotl/utils/schemas/quantization.py index a7c1305741..b15e5d225e 100644 --- a/src/axolotl/utils/schemas/quantization.py +++ b/src/axolotl/utils/schemas/quantization.py @@ -20,6 +20,9 @@ def validate_ao_dtype(v: Any) -> TorchAOQuantDType | None: return TorchAOQuantDType.float8_e4m3fn if v == "nvfp4": return TorchAOQuantDType.nvfp4 + if v == "mxfp4": + return TorchAOQuantDType.mxfp4 + raise ValueError( f"Invalid dtype: '{v}'. Must be one of: {[e.name for e in TorchAOQuantDType] + ['fp8', 'float8']}" ) diff --git a/tests/e2e/test_qat.py b/tests/e2e/test_qat.py index 5cfbc85530..251d5b17b2 100644 --- a/tests/e2e/test_qat.py +++ b/tests/e2e/test_qat.py @@ -8,6 +8,8 @@ from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault +from axolotl.utils.schemas.enums import TorchAOQuantDType +from axolotl.utils.schemas.quantization import QATConfig, validate_ao_dtype from .utils import check_model_output_exists, check_tensorboard @@ -130,3 +132,32 @@ def test_qat_dpo(self, temp_dir): loss_threshold, "Train Loss (%s) is too high", ) + + +class TestMXFP4Schema: + """Test MXFP4 schema validation""" + + def test_validate_mxfp4_dtype(self): + result = validate_ao_dtype("mxfp4") + assert result == TorchAOQuantDType.mxfp4 + + def test_qat_config_with_mxfp4(self): + """Test QATConfig accepts mxfp4 weight_dtype""" + config = QATConfig( + weight_dtype="mxfp4", + group_size=32, + quantize_embedding=False, + ) + assert config.weight_dtype == TorchAOQuantDType.mxfp4 + assert config.group_size == 32 + + def test_qat_config_mxfp4_invalid_group_size(self): + """Test that invalid group_size raises appropriate error during quantization""" + # Note: Schema validation doesn't check group_size compatibility, + # that happens in get_quantization_config + config = QATConfig( + weight_dtype="mxfp4", + group_size=16, # Invalid for mxfp4, but schema allows it + ) + assert config.group_size == 16 # Schema accepts it + # Actual validation happens at runtime in get_quantization_config diff --git a/tests/e2e/test_quantization.py b/tests/e2e/test_quantization.py index 706279c6ca..371ffb659b 100644 --- a/tests/e2e/test_quantization.py +++ b/tests/e2e/test_quantization.py @@ -5,6 +5,7 @@ import pytest import torch from torch import nn +from torchao.prototype.qat import MXFakeQuantizeConfig from torchao.quantization import LinearActivationQuantizedTensor from torchao.quantization.qat.embedding import FakeQuantizedEmbedding from torchao.quantization.qat.linear import FakeQuantizedLinear @@ -117,6 +118,21 @@ def test_get_ptq_config( config = get_quantization_config(weight_dtype, activation_dtype, group_size) assert isinstance(config, expected_type) + @require_torch_2_8_0 + @requires_sm_ge_100 + def test_get_ptq_config_mxfp4(self): + config = get_quantization_config(TorchAOQuantDType.mxfp4, None, 32) + assert isinstance(config, MXFakeQuantizeConfig) + assert config.block_size == 32 + + @require_torch_2_8_0 + @requires_sm_ge_100 + def test_get_ptq_config_mxfp4_invalid_group_size(self): + with pytest.raises( + ValueError, match="MXFP4 quantization must use a block_size" + ): + get_quantization_config(TorchAOQuantDType.mxfp4, None, 16) + @requires_cuda_ge_8_9 @require_torch_2_8_0 def test_get_ptq_config_int4_weight_only(self): @@ -262,6 +278,35 @@ def test_prepare_model_for_qat( else: assert child.activation_fake_quantizer is None + @pytest.mark.parametrize( + "weight_dtype,activation_dtype,group_size,quantize_embedding", + [ + (TorchAOQuantDType.mxfp4, None, 32, False), + (TorchAOQuantDType.mxfp4, None, 32, True), + ], + ) + @require_torch_2_8_0 + @requires_sm_ge_100 + def test_prepare_model_for_qat_mxfp4( + self, model, weight_dtype, activation_dtype, group_size, quantize_embedding + ): + prepare_model_for_qat( + model, + weight_dtype, + group_size, + activation_dtype, + quantize_embedding, + ) + + if quantize_embedding: + assert isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) + assert hasattr(model.model.embed_tokens, "weight_fake_quantizer") + + for child in list(model.children()): + if isinstance(child, torch.nn.Linear): + assert isinstance(child, FakeQuantizedLinear) + assert hasattr(child, "weight_fake_quantizer") + @require_torch_2_8_0 @requires_cuda_ge_8_9 def test_convert_qat_model(self, model): From 6a8baf8fa7c76651edb8a509c505fa19ea07599b Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 6 Mar 2026 01:43:31 +0700 Subject: [PATCH 1160/1405] feat: add sonicmoe (#3411) * feat: add sonicmoe * feat: add torch compile for routing * feat: add routing smoke test * feat: add qwen3_5_moe, qwen3_vl_moe, qwen3_omni_moe * fix: disable mlp kernel for sonicmoe too * feat: update to sonicmoe release * chore: update import following new sonicmoe changes * feat: update handling for blackwell * feat: add sonicmoe e2e test * fix: installation for updated sonicmoe * fix: git commit * fix: ignore py req and fix metadata * fix: increase min hidden size to match sonicmoe kernel min * fix: attempt properly interleave and handle unpatch mid-test * chore: refactor teardown better * chore: refactor to re-use rearrange * fix: add idempotency guard * fix: address comments on CI memory and interleave * fix: tests grad, param doublewrapped --- src/axolotl/integrations/kernels/README.md | 42 +- src/axolotl/integrations/kernels/args.py | 19 +- src/axolotl/integrations/kernels/constants.py | 68 +++ src/axolotl/integrations/kernels/plugin.py | 107 +++-- .../integrations/kernels/sonicmoe/__init__.py | 3 + .../integrations/kernels/sonicmoe/patch.py | 213 +++++++++ .../integrations/kernels/sonicmoe/routing.py | 219 +++++++++ .../kernels/sonicmoe/weight_converter.py | 181 ++++++++ tests/e2e/integrations/test_sonicmoe.py | 288 ++++++++++++ tests/integrations/test_scattermoe_lora.py | 14 +- tests/integrations/test_sonicmoe.py | 428 ++++++++++++++++++ tests/integrations/test_sonicmoe_gradients.py | 158 +++++++ 12 files changed, 1698 insertions(+), 42 deletions(-) create mode 100644 src/axolotl/integrations/kernels/constants.py create mode 100644 src/axolotl/integrations/kernels/sonicmoe/__init__.py create mode 100644 src/axolotl/integrations/kernels/sonicmoe/patch.py create mode 100644 src/axolotl/integrations/kernels/sonicmoe/routing.py create mode 100644 src/axolotl/integrations/kernels/sonicmoe/weight_converter.py create mode 100644 tests/e2e/integrations/test_sonicmoe.py create mode 100644 tests/integrations/test_sonicmoe.py create mode 100644 tests/integrations/test_sonicmoe_gradients.py diff --git a/src/axolotl/integrations/kernels/README.md b/src/axolotl/integrations/kernels/README.md index 237d653cf7..7c40720afd 100644 --- a/src/axolotl/integrations/kernels/README.md +++ b/src/axolotl/integrations/kernels/README.md @@ -10,7 +10,7 @@ class ExpertsInterface(GeneralInterface): } ``` -In our custom integration, we add support for **ScatterMoE**, which is even more efficient and faster than `grouped_mm`. +In our custom integration, we add support for **ScatterMoE** and **SonicMoE**, which are more efficient and faster than `grouped_mm`. ## Usage @@ -21,23 +21,55 @@ plugins: - axolotl.integrations.kernels.KernelsPlugin use_kernels: true + +# Choose one (mutually exclusive): use_scattermoe: true +# OR +use_sonicmoe: true +``` + +**Important:** Setting `experts_implementation` is incompatible with custom kernel options. + +### SonicMoE installation + +**Prerequisites:** +- NVIDIA Hopper (H100, H200) or Blackwell (B200, GB200) GPU +- CUDA 12.9+ (13.0+ for B300) +- PyTorch 2.7+ (2.9.1 recommended) +- For B300: Triton 3.6.0 + +```bash +pip install --ignore-requires-python --no-deps "sonic-moe @ git+https://github.com/Dao-AILab/sonic-moe.git@116e2df0a41874f77fa0ad269ce7df3f0cfcb956" && pip install nvidia-cutlass-dsl==4.4.0 quack-kernels==0.2.5 ``` -**Important:** Setting `experts_implementation` is incompatible with `use_scattermoe`. +See the [SonicMoE installation guide](https://github.com/Dao-AILab/sonic-moe?tab=readme-ov-file#-installation) for the latest prerequisite details. + +**Note:** Blackwell support is in upstream beta. On Blackwell GPUs, Axolotl automatically sets `USE_QUACK_GEMM=1` to enable the Blackwell kernels. ## How It Works The `KernelsPlugin` runs before model loading and: -1. Registers the ScatterMoE kernel from the [`axolotl-ai-co/scattermoe`](https://huggingface.co/axolotl-ai-co/scattermoe) Hub repo. +### ScatterMoE +1. Registers the ScatterMoE kernel from the local `libs/scattermoe_lora` package (includes fused LoRA support via Triton kernels). 2. Patches the model's `SparseMoeBlock` forward method with the optimized ScatterMoE implementation. -This works for any MoE model in transformers that uses a `SparseMoeBlock` class (Mixtral, Qwen2-MoE, OLMoE, etc.). +### SonicMoE +1. Resolves the model's MoE block class(es) from `constants.py`. +2. Patches the forward method with SonicMoE's optimized kernels and registers a weight converter for the interleaved gate/up projection format. +3. Supports both softmax->topk and sigmoid->topk routing strategies. + +Both paths use the shared `resolve_moe_block_classes` utility in `constants.py` for model-type-to-class resolution. + +#### Supported Models + +See `constants.py` for the full list of supported model types (Qwen2-MoE, Qwen3-MoE, OLMoE, Mixtral, DeepSeek-V3, GLM-MoE, MiniMax, etc.). ## Limitations -ScatterMoE uses a softmax -> topk routing, so results may be different for some model arch as baseline (GPT-OSS, GLM_MOE_DSA). +ScatterMoE uses a softmax -> topk routing, so results may be different for some model architectures as baseline (GPT-OSS, etc). Incompatible with `GLM_MOE_DSA` (GLM 5) and `GLM4_MOE_LITE` (GLM 4.7 Flash) at the moment. + +SonicMoE supports both softmax->topk and sigmoid->topk routing, covering a wider range of architectures. ScatterMoE does not work for GLM4.7 Flash (glm4_moe_lite) atm. diff --git a/src/axolotl/integrations/kernels/args.py b/src/axolotl/integrations/kernels/args.py index e8cf7208a0..d9b261e720 100644 --- a/src/axolotl/integrations/kernels/args.py +++ b/src/axolotl/integrations/kernels/args.py @@ -6,7 +6,18 @@ class KernelsArgs(BaseModel): - use_scattermoe: bool | None = True + use_scattermoe: bool | None = None + use_sonicmoe: bool | None = None + + @model_validator(mode="before") + @classmethod + def check_mutually_exclusive(cls, data): + if data.get("use_scattermoe") and data.get("use_sonicmoe"): + raise ValueError( + "Cannot use both ScatterMoE and SonicMoE simultaneously. " + "Please set only one of `use_scattermoe` or `use_sonicmoe` to true." + ) + return data @model_validator(mode="before") @classmethod @@ -36,11 +47,11 @@ def check_experts_implementation(cls, data): @model_validator(mode="before") @classmethod - def disable_mlp_kernel_scattermoe(cls, data): - if data.get("use_scattermoe") is True: + def disable_mlp_kernel(cls, data): + if data.get("use_scattermoe") is True or data.get("use_sonicmoe") is True: if data.get("lora_mlp_kernel") is True: LOG.warning( - "Disabling lora_mlp_kernel when using scattermoe due to compatibility issues." + "Disabling lora_mlp_kernel when using custom MoE kernels due to compatibility issues." ) data["lora_mlp_kernel"] = False data["mlp_kernel"] = False diff --git a/src/axolotl/integrations/kernels/constants.py b/src/axolotl/integrations/kernels/constants.py new file mode 100644 index 0000000000..529ed4ad66 --- /dev/null +++ b/src/axolotl/integrations/kernels/constants.py @@ -0,0 +1,68 @@ +""" +Supported MoE block mappings for kernel integrations. + +Maps model_type to the SparseMoeBlock class name(s) in transformers. +Used by both ScatterMoE and SonicMoE kernel paths. + +Values can be a single class name (str) or a list of class names for models +with multiple MoE block types (e.g. qwen3_omni_moe has Thinker + Talker). +""" + +import importlib + +SPARSE_MOE_BLOCK = { + # softmax -> topk routing + "qwen2_moe": "Qwen2MoeSparseMoeBlock", + "qwen3_moe": "Qwen3MoeSparseMoeBlock", + "qwen3_5_moe": "Qwen3_5MoeSparseMoeBlock", + "qwen3_next": "Qwen3NextSparseMoeBlock", + "qwen3_vl_moe": "Qwen3VLMoeTextSparseMoeBlock", + # qwen3_omni_moe: Thinker (standard) + Talker (shared experts + shared_expert_gate) + "qwen3_omni_moe": [ + "Qwen3OmniMoeThinkerTextSparseMoeBlock", + "Qwen3OmniMoeTalkerTextSparseMoeBlock", + ], + "olmoe": "OlmoeSparseMoeBlock", + "mixtral": "MixtralSparseMoeBlock", + "minimax": "MiniMaxSparseMoeBlock", + # sigmoid -> topk routing (with group-based expert selection) + "glm_moe_dsa": "GlmMoeDsaMoE", + "deepseek_v3": "DeepseekV3MoE", + "glm4_moe": "Glm4MoeMoE", + "glm4_moe_lite": "Glm4MoeLiteMoE", + "glm4v_moe": "Glm4vMoeTextMoE", + # sigmoid -> topk routing (no group selection) + "minimax_m2": "MiniMaxM2SparseMoeBlock", + # Models below need custom routing (not yet implemented): + # "ernie4_5_moe": "Ernie4_5_MoeSparseMoeBlock", # softmax->topk, e_score_correction_bias between softmax and topk + # "deepseek_v2": "DeepseekV2Moe", # softmax->topk, group_limited_greedy, different attr names (num_group) + # "hunyuan_v1_moe": "HunYuanMoEV1Moe", # softmax->topk, gate.wg (not gate.weight), scatter routing + # "gpt_oss": "GptOssMLP", # topk->softmax, transposed layout [E,H,2*I], custom GLU, expert biases +} + + +def resolve_moe_block_classes(model_type: str): + """Resolve all MoE block classes from transformers for the given model type. + + Returns a list of classes (one for most models, multiple for models with + distinct MoE block types like qwen3_omni_moe). + """ + entry = SPARSE_MOE_BLOCK.get(model_type) + if entry is None: + raise ValueError( + f"Unsupported MoE model type '{model_type}'. " + f"Supported types: {list(SPARSE_MOE_BLOCK.keys())}" + ) + + cls_names = entry if isinstance(entry, list) else [entry] + module_path = f"transformers.models.{model_type}.modeling_{model_type}" + module = importlib.import_module(module_path) + + classes = [] + for cls_name in cls_names: + moe_cls = getattr(module, cls_name, None) + if moe_cls is None: + raise ValueError(f"Could not find class '{cls_name}' in '{module_path}'") + classes.append(moe_cls) + + return classes diff --git a/src/axolotl/integrations/kernels/plugin.py b/src/axolotl/integrations/kernels/plugin.py index 56d0448d5f..ad14dd148c 100644 --- a/src/axolotl/integrations/kernels/plugin.py +++ b/src/axolotl/integrations/kernels/plugin.py @@ -1,14 +1,59 @@ +import importlib +import os from pathlib import Path -from kernels import ( - LocalLayerRepository, - Mode, - register_kernel_mapping, - replace_kernel_forward_from_hub, -) +import torch from axolotl.integrations.base import BasePlugin -from axolotl.utils.callbacks.models import get_causal_lm_model_cls_prefix +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def _check_sonicmoe_gpu_compat(): + """Validate GPU compute capability for SonicMoE and configure env. + + Supported: Hopper (sm_90), Blackwell (sm_100 - sm_103). + B300 (sm_103) additionally requires Triton 3.6.0. + """ + if not torch.cuda.is_available(): + return + + cc = torch.cuda.get_device_capability() + + if cc < (9, 0): + raise RuntimeError( + f"SonicMoE requires Hopper (sm_90) or Blackwell (sm_100+) GPU, " + f"but detected sm_{cc[0]}{cc[1]}." + ) + + if cc > (10, 3): + raise RuntimeError( + f"SonicMoE does not yet support sm_{cc[0]}{cc[1]}. " + f"Supported: Hopper (sm_90) and Blackwell (sm_100 - sm_103)." + ) + + # Blackwell (sm_100+): enable QuACK GEMM kernels + if cc >= (10, 0): + os.environ.setdefault("USE_QUACK_GEMM", "1") + LOG.info( + f"Blackwell GPU (sm_{cc[0]}{cc[1]}) detected, enabling USE_QUACK_GEMM=1" + ) + + # B300 (sm_103): requires Triton 3.6.0 + if cc == (10, 3): + triton_spec = importlib.util.find_spec("triton") + if triton_spec is None: + raise RuntimeError( + "B300 (sm_103) requires Triton 3.6.0, but Triton is not installed." + ) + import triton + + triton_version = tuple(int(x) for x in triton.__version__.split(".")[:2]) + if triton_version != (3, 6): + raise RuntimeError( + f"B300 (sm_103) requires Triton 3.6.x, but found {triton.__version__}." + ) class KernelsPlugin(BasePlugin): @@ -19,8 +64,32 @@ def pre_model_load(self, cfg): if cfg.use_scattermoe: self._register_kernels() self._kernelize_model(cfg.model_config_type) + elif cfg.use_sonicmoe: + if not importlib.util.find_spec("sonicmoe"): + raise RuntimeError( + "SonicMoE is not installed. See installation instructions at " + "https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/integrations/kernels/README.md#sonicmoe-installation" + ) + + _check_sonicmoe_gpu_compat() + + from axolotl.integrations.kernels.sonicmoe import patch_sonicmoe + + LOG.info( + f"Applying SonicMoE patches for model type: {cfg.model_config_type}" + ) + patch_sonicmoe( + cfg.model_config_type, + torch_compile=bool(getattr(cfg, "torch_compile", False)), + ) def _register_kernels(self): + from kernels import ( + LocalLayerRepository, + Mode, + register_kernel_mapping, + ) + plugin_root = Path(__file__).parent register_kernel_mapping( { @@ -42,25 +111,11 @@ def _register_kernels(self): ) def _kernelize_model(self, model_type: str): - if model_type == "olmoe": - from transformers.models.olmoe.modeling_olmoe import OlmoeSparseMoeBlock + from kernels import replace_kernel_forward_from_hub + + from axolotl.integrations.kernels.constants import resolve_moe_block_classes + for model_moe_cls in resolve_moe_block_classes(model_type): replace_kernel_forward_from_hub( - OlmoeSparseMoeBlock, "HFScatterMoEParallelExperts" + model_moe_cls, "HFScatterMoEParallelExperts" ) - else: - try: - model_moe_cls = get_model_moe_block(model_type) - replace_kernel_forward_from_hub( - model_moe_cls, "HFScatterMoEParallelExperts" - ) - except Exception as err: - raise ValueError(f"Unsupported model type: {model_type}") from err - - -def get_model_moe_block(model_type: str): - module_path = f"transformers.models.{model_type}.modeling_{model_type}" - model_cls_prefix, _ = get_causal_lm_model_cls_prefix(model_type) - module = __import__(module_path, fromlist=[f"{model_cls_prefix}SparseMoeBlock"]) - model_cls = getattr(module, f"{model_cls_prefix}SparseMoeBlock") - return model_cls diff --git a/src/axolotl/integrations/kernels/sonicmoe/__init__.py b/src/axolotl/integrations/kernels/sonicmoe/__init__.py new file mode 100644 index 0000000000..d1f5e5f603 --- /dev/null +++ b/src/axolotl/integrations/kernels/sonicmoe/__init__.py @@ -0,0 +1,3 @@ +from .patch import patch_sonicmoe + +__all__ = ["patch_sonicmoe"] diff --git a/src/axolotl/integrations/kernels/sonicmoe/patch.py b/src/axolotl/integrations/kernels/sonicmoe/patch.py new file mode 100644 index 0000000000..a3b96f12a1 --- /dev/null +++ b/src/axolotl/integrations/kernels/sonicmoe/patch.py @@ -0,0 +1,213 @@ +""" +SonicMoE patching for SparseMoeBlock forward pass. + +Monkeypatches the SparseMoeBlock class for a given model type to use +SonicMoE's optimized kernels. Two forward paths are supported: + +1. **General routing path** (routing_fn is not None): + Uses a custom routing function + ``moe_general_routing_inputs``. + Suitable for models with non-standard routing (softmax->topk, sigmoid->topk). + +2. **Fused topk->softmax path** (routing_fn is None): + Uses ``moe_TC_softmax_topk_layer`` which fuses routing + expert computation. + Suitable for models with simple topk->softmax routing. + +Weight format conversion (interleave/deinterleave) is handled by the +WeightConverter system, so the forward assumes weights are already in +interleaved format. + +Shared experts are handled generically: if the block has a ``shared_expert`` +or ``shared_experts`` attribute, its output is computed alongside the routed +experts and added to the final output. An optional ``shared_expert_gate`` +applies sigmoid gating to the shared expert contribution. +""" + +import torch +import torch.nn.functional as F + +from axolotl.integrations.kernels.constants import resolve_moe_block_classes +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def patch_sonicmoe(model_type: str, torch_compile: bool = False): + """Main entry point: patch SparseMoeBlock for SonicMoE support. + + Args: + model_type: The HuggingFace model type (e.g. "qwen3_moe"). + torch_compile: If True, wrap routing functions with torch.compile + for kernel fusion (fuses softmax+topk+renorm into fewer launches). + """ + from .routing import get_model_moe_config + from .weight_converter import register_sonicmoe_weight_converter + + routing_fn, activation, router_attr = get_model_moe_config(model_type) + + if torch_compile and routing_fn is not None: + routing_fn = _try_compile_routing(routing_fn) + + for moe_cls in resolve_moe_block_classes(model_type): + _patch_forward(moe_cls, routing_fn, activation, router_attr) + register_sonicmoe_weight_converter(model_type) + + +def _try_compile_routing(routing_fn): + """Attempt to torch.compile the routing function, fall back to eager on failure.""" + try: + compiled_fn = torch.compile(routing_fn, mode="reduce-overhead", dynamic=False) + LOG.info(f"torch.compile enabled for routing function: {routing_fn.__name__}") + return compiled_fn + except Exception as exc: # pylint: disable=broad-except + LOG.warning( + f"torch.compile failed for routing function {routing_fn.__name__}, " + f"falling back to eager: {exc}" + ) + return routing_fn + + +def _patch_forward(moe_cls, routing_fn, activation, router_attr): + """Monkeypatch the SparseMoeBlock class with a SonicMoE forward. + + The patched forward handles shared experts generically: if + ``self.shared_expert`` or ``self.shared_experts`` exists, it is computed + and added to the routed output. If ``self.shared_expert_gate`` also exists, + it applies sigmoid gating to the shared expert contribution (as in qwen2_moe). + + Args: + moe_cls: The SparseMoeBlock class to patch. + routing_fn: Routing function (e.g. softmax_topk_routing), or None + for the fused moe_TC_softmax_topk_layer path. + activation: SonicMoE ActivationType enum value. + router_attr: Name of the router module attribute on the MoE block. + """ + if hasattr(moe_cls, "_original_forward"): + LOG.info(f"{moe_cls.__name__}.forward already patched with SonicMoE, skipping") + return + + original_forward = moe_cls.forward + + if routing_fn is not None: + _make_general_forward(moe_cls, routing_fn, activation) + else: + _make_fused_forward(moe_cls, activation, router_attr) + + moe_cls._original_forward = original_forward + LOG.info(f"Patched {moe_cls.__name__}.forward with SonicMoE implementation") + + +def _make_general_forward(moe_cls, routing_fn, activation): + """Create forward using routing_fn + moe_general_routing_inputs.""" + + def sonicmoe_forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + from sonicmoe import moe_general_routing_inputs + + batch_size, sequence_length, hidden_dim = hidden_states.shape + hidden_states_flat = hidden_states.view(-1, hidden_dim) + + # Shared expert (computed early, matching original model ordering) + shared_expert_output = _compute_shared_expert(self, hidden_states_flat) + + # Routing + router_scores, token_indices, expert_indices, _router_logits = routing_fn( + hidden_states_flat, self + ) + + # Permute weights to SonicMoE layout: + # gate_up: [E, 2*I, H] -> [2*I, H, E] + # down: [E, H, I] -> [H, I, E] + gate_up_weight = self.experts.gate_up_proj.permute(1, 2, 0) + down_weight = self.experts.down_proj.permute(1, 2, 0) + E = gate_up_weight.shape[-1] + + output, _ = moe_general_routing_inputs( + hidden_states_flat, + router_scores, + token_indices, + expert_indices, + gate_up_weight, + None, # b1 (no gate/up bias) + down_weight, + None, # b2 (no down bias) + E, + torch.cuda.current_stream().cuda_stream, + activation, + False, # is_inference_mode + ) + + # Add shared expert contribution if present + if shared_expert_output is not None: + if hasattr(self, "shared_expert_gate"): + shared_expert_output = ( + F.sigmoid(self.shared_expert_gate(hidden_states_flat)) + * shared_expert_output + ) + output = output + shared_expert_output + + return output.view(batch_size, sequence_length, hidden_dim) + + moe_cls.forward = sonicmoe_forward + + +def _make_fused_forward(moe_cls, activation, router_attr): + """Create forward using moe_TC_softmax_topk_layer (topk -> softmax).""" + + def sonicmoe_fused_forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + from sonicmoe import moe_TC_softmax_topk_layer + + batch_size, sequence_length, hidden_dim = hidden_states.shape + hidden_states_flat = hidden_states.view(-1, hidden_dim) + + # Shared expert (computed early, matching original model ordering) + shared_expert_output = _compute_shared_expert(self, hidden_states_flat) + + router = getattr(self, router_attr) + + # Permute weights to SonicMoE layout: + # gate_up: [E, 2*I, H] -> [2*I, H, E] + # down: [E, H, I] -> [H, I, E] + gate_up_weight = self.experts.gate_up_proj.permute(1, 2, 0) + down_weight = self.experts.down_proj.permute(1, 2, 0) + + output, _router_logits, _expert_freq = moe_TC_softmax_topk_layer( + hidden_states_flat, + router.weight, + gate_up_weight, + None, # b1 (no gate/up bias) + down_weight, + None, # b2 (no down bias) + router.top_k, + torch.cuda.current_stream().cuda_stream, + activation, + False, # is_inference_mode + ) + + # Add shared expert contribution if present + if shared_expert_output is not None: + if hasattr(self, "shared_expert_gate"): + shared_expert_output = ( + F.sigmoid(self.shared_expert_gate(hidden_states_flat)) + * shared_expert_output + ) + output = output + shared_expert_output + + return output.view(batch_size, sequence_length, hidden_dim) + + moe_cls.forward = sonicmoe_fused_forward + + +def _compute_shared_expert(moe_block, hidden_states_flat): + """Compute shared expert output if the block has one. + + Handles singular (qwen2_moe: ``shared_expert``), plural + (glm_moe_dsa/deepseek_v3: ``shared_experts``), and MLP + (hunyuan_v1_moe: ``shared_mlp``) attribute names. + """ + shared_expert = ( + getattr(moe_block, "shared_expert", None) + or getattr(moe_block, "shared_experts", None) + or getattr(moe_block, "shared_mlp", None) + ) + if shared_expert is not None: + return shared_expert(hidden_states_flat) + return None diff --git a/src/axolotl/integrations/kernels/sonicmoe/routing.py b/src/axolotl/integrations/kernels/sonicmoe/routing.py new file mode 100644 index 0000000000..3f93c15963 --- /dev/null +++ b/src/axolotl/integrations/kernels/sonicmoe/routing.py @@ -0,0 +1,219 @@ +""" +Routing functions for SonicMoE integration. + +Different MoE architectures use different routing strategies: +- qwen3_moe / qwen2_moe / qwen3_5_moe / qwen3_vl_moe / qwen3_omni_moe: softmax -> topk (with optional renormalization) +- gpt_oss: topk -> softmax (uses fused moe_TC_softmax_topk_layer, routing_fn=None) +- glm_moe_dsa: sigmoid -> topk (with group-based expert selection) + +Each model type maps to a (routing_fn, activation_type, router_attr) triple. +When routing_fn is None, the fused moe_TC_softmax_topk_layer path is used. +""" + +import torch +import torch.nn.functional as F + + +def get_model_moe_config(model_type: str): + """Returns (routing_fn, activation, router_attr) for a given model type. + + Args: + model_type: HuggingFace model type string. + + Returns: + routing_fn: Callable or None. None signals the fused + moe_TC_softmax_topk_layer path (topk -> softmax models). + activation: SonicMoE ActivationType enum value. + router_attr: Name of the router module attribute on the MoE block + (e.g. "gate" or "router"). + + The activation type cannot be derived from config.hidden_act because + e.g. qwen3_moe reports "silu" but architecturally uses SwiGLU + (act_fn(gate) * up pattern). So we specify it per model type. + """ + from sonicmoe.enums import ActivationType + + if model_type in ( + "qwen2_moe", + "qwen3_moe", + "qwen3_5_moe", + "qwen3_next", + "qwen3_vl_moe", + "qwen3_omni_moe", + "olmoe", + "mixtral", + "minimax", + ): + return softmax_topk_routing, ActivationType.SWIGLU, "gate" + elif model_type in ( + "glm_moe_dsa", + "deepseek_v3", + "glm4_moe", + "glm4_moe_lite", + "glm4v_moe", + "minimax_m2", + ): + return sigmoid_topk_routing, ActivationType.SWIGLU, "gate" + # elif model_type in ("ernie4_5_moe",): + # # Softmax→topk with e_score_correction_bias applied between softmax and topk. + # return ..., ActivationType.SWIGLU, "gate" + # elif model_type in ("deepseek_v2",): + # # Softmax→topk with group_limited_greedy. Different attr names: num_group + # # (not n_group), gate is nn.Linear (not a router class). + # return ..., ActivationType.SWIGLU, "gate" + # elif model_type in ("hunyuan_v1_moe",): + # # Softmax→topk but gate structure differs: gate.wg (not gate.weight), + # # top_k on block not gate, creates scatter routing matrix. + # return ..., ActivationType.SWIGLU, "gate" + # Fused topk -> softmax path (routing_fn=None): + # elif model_type in ("gpt_oss",): + # # NOTE: gpt_oss has a router bias which moe_TC_softmax_topk_layer + # # ignores (it only takes router_w, not bias). Also has transposed + # # weight layout [E, H, 2*I] and custom GLU activation. + # return None, ActivationType.SWIGLU, "router" + else: + raise ValueError(f"SonicMoE: unsupported model type '{model_type}'") + + +def softmax_topk_routing( + hidden_states: torch.Tensor, moe_block +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """Qwen3/Qwen2-style routing: softmax -> topk -> optional renorm. + + Args: + hidden_states: [T, H] flattened token representations + moe_block: MoE block module (accesses moe_block.gate.*) + + Returns: + router_scores: [T*K] flattened scores (float32) + token_indices: [T*K] which token each entry belongs to (int32), sorted ascending + expert_indices: [T*K] which expert (int32) + router_logits: [T, E] original logits for aux loss + """ + gate = moe_block.gate + T, H = hidden_states.shape + K = gate.top_k + + # Compute router logits and softmax over all experts + router_logits = F.linear(hidden_states, gate.weight) # [T, E] + router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] + + # Select top-k experts per token + top_values, top_indices = torch.topk(router_probs, K, dim=-1) # [T, K] each + + # Renormalize if configured (default True for models without the attribute, + # e.g. Mixtral/MiniMax which always normalize) + if getattr(gate, "norm_topk_prob", True): + top_values = top_values / top_values.sum(dim=-1, keepdim=True) + + # no-op: matches transformers which casts to softmax output dtype (float32). + # top_values = top_values.to(router_probs.dtype) + + # Flatten for moe_general_routing_inputs. + # Token indices are naturally sorted ascending from the [T, K] layout: + # [0, 0, ..., 1, 1, ..., T-1, T-1, ...] — this is required by SonicMoE. + # Expert sorting is handled internally by general_routing_router_metadata. + token_indices = ( + torch.arange(T, device=hidden_states.device, dtype=torch.int32) + .unsqueeze(1) + .expand(T, K) + ) + + flat_scores = top_values.reshape(-1) # [T*K] + flat_token_idx = token_indices.reshape(-1) # [T*K] + flat_expert_idx = top_indices.to(torch.int32).reshape(-1) # [T*K] + + return flat_scores, flat_token_idx, flat_expert_idx, router_logits + + +def sigmoid_topk_routing( + hidden_states: torch.Tensor, moe_block +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """Sigmoid-based routing: sigmoid -> optional group selection -> topk. + + Supports two variants: + - **Group selection** (glm_moe_dsa, deepseek_v3, etc.): n_group > 1, + bias on gate, group-based masking before topk. + - **No group selection** (minimax_m2): n_group == 1 (or absent), + bias on moe_block, straight topk from all experts. + + Final routing weights come from the original sigmoid scores (not + bias-corrected), with optional renormalization and scaling. + + Args: + hidden_states: [T, H] flattened token representations + moe_block: MoE block module (accesses moe_block.gate.* and + optional moe_block.n_group, .topk_group, .top_k, .norm_topk_prob, + .routed_scaling_factor, .n_routed_experts) + + Returns: + router_scores: [T*K] flattened scores (float32) + token_indices: [T*K] which token each entry belongs to (int32), sorted ascending + expert_indices: [T*K] which expert (int32) + router_logits: [T, E] original logits for aux loss + """ + gate = moe_block.gate + T, H = hidden_states.shape + K = moe_block.top_k + E = getattr(moe_block, "n_routed_experts", gate.weight.shape[0]) + n_group = getattr(moe_block, "n_group", 1) + + # Compute router logits and sigmoid probabilities + router_logits = F.linear(hidden_states.float(), gate.weight.float()) # [T, E] + router_probs = router_logits.sigmoid() # [T, E] + + # Bias-corrected scores for expert selection (not used for final weights). + # glm_moe_dsa/deepseek_v3 store the bias on gate; minimax_m2 stores it on the block. + e_score_correction_bias = getattr(gate, "e_score_correction_bias", None) + if e_score_correction_bias is None: + e_score_correction_bias = getattr(moe_block, "e_score_correction_bias", None) + if e_score_correction_bias is None: + raise AttributeError( + f"sigmoid_topk_routing requires e_score_correction_bias on " + f"gate ({type(gate)}) or moe_block ({type(moe_block)}), but neither has it" + ) + scores_for_choice = router_probs + e_score_correction_bias + + # Group-based selection: pick top groups, mask the rest (skip when n_group == 1) + if n_group > 1: + group_scores = ( + scores_for_choice.view(-1, n_group, E // n_group) + .topk(2, dim=-1)[0] + .sum(dim=-1) + ) # [T, n_group] + group_idx = torch.topk( + group_scores, k=moe_block.topk_group, dim=-1, sorted=False + )[1] + group_mask = torch.zeros_like(group_scores) + group_mask.scatter_(1, group_idx, 1) + score_mask = ( + group_mask.unsqueeze(-1).expand(-1, n_group, E // n_group).reshape(-1, E) + ) + scores_for_choice = scores_for_choice.masked_fill(~score_mask.bool(), 0.0) + + # Final topk from (possibly masked) scores + topk_indices = torch.topk(scores_for_choice, k=K, dim=-1, sorted=False)[1] + + # Gather weights from original sigmoid scores (not bias-corrected) + topk_weights = router_probs.gather(1, topk_indices) + + # Optional renormalization + scaling + norm_topk_prob = getattr(moe_block, "norm_topk_prob", True) + if norm_topk_prob: + topk_weights = topk_weights / (topk_weights.sum(dim=-1, keepdim=True) + 1e-20) + routed_scaling_factor = getattr(moe_block, "routed_scaling_factor", 1.0) + topk_weights = topk_weights * routed_scaling_factor + + # Flatten for moe_general_routing_inputs. + # Token indices are naturally sorted ascending from the [T, K] layout. + token_indices = ( + torch.arange(T, device=hidden_states.device, dtype=torch.int32) + .unsqueeze(1) + .expand(T, K) + ) + + flat_scores = topk_weights.to(torch.float32).reshape(-1) # [T*K] + flat_token_idx = token_indices.reshape(-1) # [T*K] + flat_expert_idx = topk_indices.to(torch.int32).reshape(-1) # [T*K] + + return flat_scores, flat_token_idx, flat_expert_idx, router_logits diff --git a/src/axolotl/integrations/kernels/sonicmoe/weight_converter.py b/src/axolotl/integrations/kernels/sonicmoe/weight_converter.py new file mode 100644 index 0000000000..172864ac66 --- /dev/null +++ b/src/axolotl/integrations/kernels/sonicmoe/weight_converter.py @@ -0,0 +1,181 @@ +""" +Custom WeightConverter operations for SonicMoE weight format conversion. + +SonicMoE requires gate_up_proj weights in interleaved format: +- Standard (concatenated): [E, 2*I, H] where first I rows are gate, last I rows are up +- SonicMoE (interleaved): [E, 2*I, H] where rows alternate [g0, u0, g1, u1, ...] + +These ConversionOps integrate with transformers' WeightConverter system so that +weights are transparently converted during loading and reverted during saving. +""" + +from typing import Any + +import torch +from einops import rearrange +from transformers.core_model_loading import ConversionOps + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def interleave_gate_up(tensor: torch.Tensor) -> torch.Tensor: + """[gate..., up...] -> [g0, u0, g1, u1, ...] along the 2*I dimension.""" + return rearrange(tensor, "... (two out) h -> ... (out two) h", two=2) + + +def deinterleave_gate_up(tensor: torch.Tensor) -> torch.Tensor: + """[g0, u0, g1, u1, ...] -> [gate..., up...] along the 2*I dimension.""" + return rearrange(tensor, "... (out two) h -> ... (two out) h", two=2) + + +class ConcatenatedToInterleaved(ConversionOps): + """Convert concatenated gate/up projections to interleaved format. + + Input: [E, 2*I, H] with gate=[E, :I, H] and up=[E, I:, H] + Output: [E, 2*I, H] with rows alternating [g0, u0, g1, u1, ...] + + This operation is applied along ``dim`` (default 1, the 2*I dimension). + """ + + def __init__(self, dim: int = 1): + self.dim = dim + + @torch.no_grad() + def convert( + self, + input_dict: dict[str, Any], + source_patterns: list[str], + target_patterns: list[str], + **kwargs, + ) -> dict[str, torch.Tensor]: + target_pattern = self._get_target_pattern( + input_dict, source_patterns, target_patterns + ) + tensors = next(iter(input_dict.values())) + tensor = tensors[0] if isinstance(tensors, list) else tensors + + interleaved = interleave_gate_up(tensor) + + return {target_pattern: interleaved} + + def _get_target_pattern( + self, + input_dict: dict[str, Any], + source_patterns: list[str], + target_patterns: list[str], + ) -> str: + # Follow the same logic as Transpose.get_target_pattern + if len(input_dict) != 1: + raise ValueError("Undefined Operation encountered!") + if len(target_patterns) > 1: + if len(source_patterns) == 1: + return source_patterns[0] + raise ValueError("Undefined Operation encountered!") + return target_patterns[0] + + @property + def reverse_op(self) -> ConversionOps: + return InterleavedToConcatenated(self.dim) + + +class InterleavedToConcatenated(ConversionOps): + """Convert interleaved gate/up projections back to concatenated format. + + Input: [E, 2*I, H] with rows alternating [g0, u0, g1, u1, ...] + Output: [E, 2*I, H] with gate=[E, :I, H] and up=[E, I:, H] + + This is the reverse of ``ConcatenatedToInterleaved``. + """ + + def __init__(self, dim: int = 1): + self.dim = dim + + @torch.no_grad() + def convert( + self, + input_dict: dict[str, Any], + source_patterns: list[str], + target_patterns: list[str], + **kwargs, + ) -> dict[str, torch.Tensor]: + target_pattern = self._get_target_pattern( + input_dict, source_patterns, target_patterns + ) + tensors = next(iter(input_dict.values())) + tensor = tensors[0] if isinstance(tensors, list) else tensors + + concatenated = deinterleave_gate_up(tensor) + + return {target_pattern: concatenated} + + def _get_target_pattern( + self, + input_dict: dict[str, Any], + source_patterns: list[str], + target_patterns: list[str], + ) -> str: + if len(input_dict) != 1: + raise ValueError("Undefined Operation encountered!") + if len(target_patterns) > 1: + if len(source_patterns) == 1: + return source_patterns[0] + raise ValueError("Undefined Operation encountered!") + return target_patterns[0] + + @property + def reverse_op(self) -> ConversionOps: + return ConcatenatedToInterleaved(self.dim) + + +def register_sonicmoe_weight_converter(model_type: str): + """Override the conversion mapping to add interleave step for gate_up_proj. + + Appends a ConcatenatedToInterleaved operation to the existing gate_up_proj + converter chain. For example, qwen3_moe's chain becomes: + MergeModulelist(dim=0) -> Concatenate(dim=1) -> ConcatenatedToInterleaved(dim=1) + + The reverse is auto-generated for saving: + InterleavedToConcatenated(dim=1) -> Chunk(dim=1) -> SplitModulelist(dim=0) + """ + from transformers.conversion_mapping import ( + get_checkpoint_conversion_mapping, + register_checkpoint_conversion_mapping, + ) + + existing = get_checkpoint_conversion_mapping(model_type) + if existing is None: + LOG.warning( + f"No conversion mapping found for model type '{model_type}'. " + "SonicMoE weight interleaving will not be applied during checkpoint loading." + ) + return + + # Find the gate_up_proj converter and append ConcatenatedToInterleaved + patched = False + for converter in existing: + if hasattr(converter, "operations") and any( + "gate_up_proj" in pat for pat in converter.target_patterns + ): + # Guard against double registration (e.g. plugin reloaded) + if any( + isinstance(op, ConcatenatedToInterleaved) for op in converter.operations + ): + LOG.info( + f"SonicMoE weight converter already registered for '{model_type}'" + ) + return + converter.operations.append(ConcatenatedToInterleaved(dim=1)) + patched = True + break + + if not patched: + LOG.warning( + f"Could not find gate_up_proj converter for model type '{model_type}'. " + "SonicMoE weight interleaving will not be applied during checkpoint loading." + ) + return + + register_checkpoint_conversion_mapping(model_type, existing, overwrite=True) + LOG.info(f"Registered SonicMoE weight converter for model type '{model_type}'") diff --git a/tests/e2e/integrations/test_sonicmoe.py b/tests/e2e/integrations/test_sonicmoe.py new file mode 100644 index 0000000000..2152e94c7f --- /dev/null +++ b/tests/e2e/integrations/test_sonicmoe.py @@ -0,0 +1,288 @@ +""" +End-to-end gradient and convergence tests for SonicMoE integration. + +Requires: + - H100/H200 GPU (SonicMoE CUTLASS kernels target sm_90) + - sonicmoe package installed + - transformers with Qwen3MoE support + +Usage: + pytest tests/e2e/integrations/test_sonicmoe.py -v -s +""" + +import importlib.util +import math + +import pytest +import torch + +_sonicmoe_available = importlib.util.find_spec("sonicmoe") is not None +_is_hopper = torch.cuda.is_available() and torch.cuda.get_device_capability() == (9, 0) + +pytestmark = [ + pytest.mark.skipif(not torch.cuda.is_available(), reason="Requires CUDA GPU"), + pytest.mark.skipif( + not _is_hopper, reason="SonicMoE CUTLASS kernels require Hopper (sm_90)" + ), + pytest.mark.skipif(not _sonicmoe_available, reason="SonicMoE not installed"), +] + + +def _create_tiny_qwen3_config(): + """Create a minimal Qwen3MoE config for fast testing.""" + from transformers import AutoConfig + + config = AutoConfig.for_model("qwen3_moe") + config.hidden_size = 512 + config.intermediate_size = 1024 + config.moe_intermediate_size = 64 + config.num_attention_heads = 16 + config.num_key_value_heads = 2 + config.head_dim = 32 + config.num_hidden_layers = 2 + config.num_experts = 8 + config.num_experts_per_tok = 2 + config.vocab_size = 1000 + config.max_position_embeddings = 128 + config.norm_topk_prob = True + config.torch_dtype = torch.bfloat16 + return config + + +def _interleave_gate_up_weights(model): + """Interleave all gate_up_proj parameters in-place for SonicMoE.""" + from axolotl.integrations.kernels.sonicmoe.weight_converter import ( + interleave_gate_up, + ) + + with torch.no_grad(): + for name, param in model.named_parameters(): + if "gate_up_proj" in name: + param.copy_(interleave_gate_up(param)) + + +def _unpatch_sonicmoe(): + """Restore original forward on the MoE block class if it was patched.""" + from axolotl.integrations.kernels.constants import resolve_moe_block_classes + + for moe_cls in resolve_moe_block_classes("qwen3_moe"): + if hasattr(moe_cls, "_original_forward"): + moe_cls.forward = moe_cls._original_forward + del moe_cls._original_forward + + +class TestSonicMoEForwardCorrectness: + """Verify SonicMoE-patched model produces same output as original.""" + + def teardown_method(self): + _unpatch_sonicmoe() + + def test_forward_output_matches(self): + from transformers import AutoModelForCausalLM + + from axolotl.integrations.kernels.sonicmoe.patch import patch_sonicmoe + + config = _create_tiny_qwen3_config() + input_ids = torch.randint(0, config.vocab_size, (1, 16), device="cuda") + + # Original model + model_orig = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + + with torch.no_grad(): + out_orig = model_orig(input_ids) + + # Patched model (same weights, interleaved for SonicMoE) + model_patched = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + model_patched.load_state_dict(model_orig.state_dict()) + + patch_sonicmoe("qwen3_moe") + _interleave_gate_up_weights(model_patched) + + with torch.no_grad(): + out_patched = model_patched(input_ids) + + max_diff = (out_orig.logits - out_patched.logits).abs().max().item() + assert torch.allclose( + out_orig.logits, out_patched.logits, atol=1e-1, rtol=1e-1 + ), f"Output mismatch: max diff={max_diff:.6f}" + + +class TestSonicMoEGradientCorrectness: + """Compare gradients between original HuggingFace and SonicMoE-patched forward.""" + + def teardown_method(self): + _unpatch_sonicmoe() + + def test_gradients_match(self): + """Verify all parameter gradients match between original and patched.""" + from transformers import AutoModelForCausalLM + + from axolotl.integrations.kernels.sonicmoe.patch import patch_sonicmoe + from axolotl.integrations.kernels.sonicmoe.weight_converter import ( + deinterleave_gate_up, + ) + + config = _create_tiny_qwen3_config() + input_ids = torch.randint(0, config.vocab_size, (1, 16), device="cuda") + + # ---------- Original model ---------- + model_orig = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + out_orig = model_orig(input_ids, labels=input_ids) + out_orig.loss.backward() + grads_orig = { + n: p.grad.float().clone() + for n, p in model_orig.named_parameters() + if p.grad is not None + } + loss_orig = out_orig.loss.item() + + # ---------- SonicMoE-patched model (same weights, interleaved) ---------- + model_patched = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + model_patched.load_state_dict(model_orig.state_dict()) + + patch_sonicmoe("qwen3_moe") + _interleave_gate_up_weights(model_patched) + + out_patched = model_patched(input_ids, labels=input_ids) + out_patched.loss.backward() + grads_patched = {} + for n, p in model_patched.named_parameters(): + if p.grad is None: + continue + g = p.grad.float().clone() + # gate_up_proj grads are in interleaved layout, de-interleave to match orig + if "gate_up_proj" in n: + g = deinterleave_gate_up(g) + grads_patched[n] = g + loss_patched = out_patched.loss.item() + + # ---------- Compare ---------- + assert abs(loss_orig - loss_patched) < 0.5, ( + f"Loss mismatch: orig={loss_orig:.4f}, patched={loss_patched:.4f}" + ) + + # All parameters with gradients in original should have them in patched + missing = set(grads_orig.keys()) - set(grads_patched.keys()) + assert not missing, f"Missing gradients in patched model: {missing}" + + # Compare gradient values + # bf16 with different GEMM impls (cuBLAS vs CUTLASS) can diverge, + # so use generous tolerance: flag only if both rel >10% AND abs >1e-2 + mismatches = [] + for name in grads_orig: + if name not in grads_patched: + continue + g_orig = grads_orig[name] + g_patched = grads_patched[name] + max_diff = (g_orig - g_patched).abs().max().item() + rel_diff = max_diff / (g_orig.abs().max().item() + 1e-8) + + if rel_diff > 0.1 and max_diff > 1e-2: + mismatches.append( + f" {name}: max_abs_diff={max_diff:.6f}, rel_diff={rel_diff:.4f}" + ) + + assert not mismatches, ( + "Gradient mismatches (rel_diff > 10% and abs_diff > 1e-2):\n" + + "\n".join(mismatches) + ) + + def test_router_weights_receive_gradients(self): + """Verify that router (gate) weights get non-zero gradients.""" + from transformers import AutoModelForCausalLM + + from axolotl.integrations.kernels.sonicmoe.patch import patch_sonicmoe + + config = _create_tiny_qwen3_config() + input_ids = torch.randint(0, config.vocab_size, (1, 16), device="cuda") + + model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + patch_sonicmoe("qwen3_moe") + _interleave_gate_up_weights(model) + + out = model(input_ids, labels=input_ids) + out.loss.backward() + + gate_grads_found = False + for name, param in model.named_parameters(): + if "gate" in name and "weight" in name: + gate_grads_found = True + assert param.grad is not None, f"No gradient for router: {name}" + assert param.grad.abs().max() > 0, f"Zero gradient for router: {name}" + + assert gate_grads_found, "No gate.weight parameters found in model" + + +class TestSonicMoETrainingConvergence: + """Verify loss decreases during training with SonicMoE.""" + + def teardown_method(self): + _unpatch_sonicmoe() + + def test_loss_decreases(self): + """Run 30 training steps, verify loss decreases and no NaN/Inf.""" + from transformers import AutoModelForCausalLM + + from axolotl.integrations.kernels.sonicmoe.patch import patch_sonicmoe + + config = _create_tiny_qwen3_config() + input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") + + model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + patch_sonicmoe("qwen3_moe") + _interleave_gate_up_weights(model) + + optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3) + losses = [] + + for step in range(30): + out = model(input_ids, labels=input_ids) + loss = out.loss + assert not math.isnan(loss.item()), f"NaN loss at step {step}" + assert not math.isinf(loss.item()), f"Inf loss at step {step}" + losses.append(loss.item()) + + loss.backward() + optimizer.step() + optimizer.zero_grad() + + assert losses[-1] < losses[0], ( + f"Loss did not decrease: first={losses[0]:.4f}, last={losses[-1]:.4f}" + ) + + def test_expert_weights_update(self): + """Verify expert weights change during training (not frozen).""" + from transformers import AutoModelForCausalLM + + from axolotl.integrations.kernels.sonicmoe.patch import patch_sonicmoe + + config = _create_tiny_qwen3_config() + input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") + + model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + patch_sonicmoe("qwen3_moe") + _interleave_gate_up_weights(model) + + # Snapshot expert weights before training + expert_weights_before = {} + for name, param in model.named_parameters(): + if "experts" in name: + expert_weights_before[name] = param.data.clone() + + assert expert_weights_before, "No expert parameters found" + + optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3) + for _ in range(5): + out = model(input_ids, labels=input_ids) + out.loss.backward() + optimizer.step() + optimizer.zero_grad() + + # Check that expert weights changed + changed = 0 + for name, param in model.named_parameters(): + if name in expert_weights_before: + if not torch.equal(param.data, expert_weights_before[name]): + changed += 1 + + assert changed > 0, "No expert weights changed after 5 training steps" diff --git a/tests/integrations/test_scattermoe_lora.py b/tests/integrations/test_scattermoe_lora.py index 859119c819..d498c80103 100644 --- a/tests/integrations/test_scattermoe_lora.py +++ b/tests/integrations/test_scattermoe_lora.py @@ -6,7 +6,7 @@ Unit tests for scattermoe-lora code-review fixes. Tests cover: -- KernelsArgs validator: disable_mlp_kernel_scattermoe +- KernelsArgs validator: disable_mlp_kernel - CPU_Offloaded_Gradient_Checkpointer: tuple vs plain tensor backward - ParallelExperts: scaling=0.0 not treated as falsy - single2scatter: non-aligned K/N dimensions @@ -20,12 +20,12 @@ import torch # ============================================================================ -# 1. KernelsArgs: disable_mlp_kernel_scattermoe validator +# 1. KernelsArgs: disable_mlp_kernel validator # ============================================================================ class TestKernelsArgsValidator: - """Test that disable_mlp_kernel_scattermoe sets both flags correctly. + """Test that disable_mlp_kernel sets both flags correctly. These tests call the validator classmethod directly on raw dicts, since lora_mlp_kernel / mlp_kernel are not declared model fields. @@ -40,7 +40,7 @@ def test_disables_lora_mlp_kernel_when_scattermoe(self): "use_scattermoe": True, "lora_mlp_kernel": True, } - result = KernelsArgs.disable_mlp_kernel_scattermoe(data) + result = KernelsArgs.disable_mlp_kernel(data) assert result["lora_mlp_kernel"] is False assert result["mlp_kernel"] is False @@ -52,7 +52,7 @@ def test_mlp_kernel_disabled_without_lora(self): "use_kernels": True, "use_scattermoe": True, } - result = KernelsArgs.disable_mlp_kernel_scattermoe(data) + result = KernelsArgs.disable_mlp_kernel(data) assert result["mlp_kernel"] is False # lora_mlp_kernel was not in data, should not be added assert "lora_mlp_kernel" not in result @@ -66,7 +66,7 @@ def test_lora_mlp_kernel_false_unchanged(self): "use_scattermoe": True, "lora_mlp_kernel": False, } - result = KernelsArgs.disable_mlp_kernel_scattermoe(data) + result = KernelsArgs.disable_mlp_kernel(data) assert result["lora_mlp_kernel"] is False def test_no_change_when_scattermoe_disabled(self): @@ -78,7 +78,7 @@ def test_no_change_when_scattermoe_disabled(self): "use_scattermoe": False, "lora_mlp_kernel": True, } - result = KernelsArgs.disable_mlp_kernel_scattermoe(data) + result = KernelsArgs.disable_mlp_kernel(data) assert result["lora_mlp_kernel"] is True diff --git a/tests/integrations/test_sonicmoe.py b/tests/integrations/test_sonicmoe.py new file mode 100644 index 0000000000..e6294f5645 --- /dev/null +++ b/tests/integrations/test_sonicmoe.py @@ -0,0 +1,428 @@ +"""Unit tests for the SonicMoE integration.""" + +from types import SimpleNamespace + +import pytest +import torch + +from axolotl.integrations.kernels.args import KernelsArgs +from axolotl.integrations.kernels.sonicmoe.routing import ( + sigmoid_topk_routing, + softmax_topk_routing, +) +from axolotl.integrations.kernels.sonicmoe.weight_converter import ( + ConcatenatedToInterleaved, + InterleavedToConcatenated, + register_sonicmoe_weight_converter, +) + + +class TestKernelsArgs: + def test_mutual_exclusivity_raises(self): + with pytest.raises(ValueError, match="Cannot use both"): + KernelsArgs.model_validate({"use_scattermoe": True, "use_sonicmoe": True}) + + def test_sonicmoe_only(self): + result = KernelsArgs.model_validate({"use_sonicmoe": True}) + assert result.use_sonicmoe is True + assert result.use_scattermoe is None + + def test_scattermoe_only(self): + result = KernelsArgs.model_validate({"use_scattermoe": True}) + assert result.use_scattermoe is True + assert result.use_sonicmoe is None + + def test_neither_set(self): + result = KernelsArgs.model_validate({}) + assert result.use_scattermoe is None + assert result.use_sonicmoe is None + + def test_disables_mlp_kernel_when_sonicmoe(self): + data = {"use_sonicmoe": True, "lora_mlp_kernel": True} + result = KernelsArgs.disable_mlp_kernel(data) + assert result["lora_mlp_kernel"] is False + assert result["mlp_kernel"] is False + + +class TestConcatenatedToInterleaved: + @pytest.fixture + def sample_tensor(self): + """Create a test tensor [E=2, 2*I=4, H=3] with distinct gate/up values.""" + E, I, H = 2, 2, 3 # noqa: E741 + gate = torch.arange(1, E * I * H + 1, dtype=torch.float32).reshape(E, I, H) + up = torch.arange(100, 100 + E * I * H, dtype=torch.float32).reshape(E, I, H) + return torch.cat([gate, up], dim=1) + + def test_interleave_rows_alternate(self, sample_tensor): + op = ConcatenatedToInterleaved(dim=1) + result = op.convert( + {"test": sample_tensor}, + source_patterns=["test"], + target_patterns=["test"], + ) + interleaved = result["test"] + + # For expert 0: even rows should be gate, odd rows should be up + E, two_I, H = sample_tensor.shape + I = two_I // 2 # noqa: E741 + gate_orig = sample_tensor[:, :I, :] + up_orig = sample_tensor[:, I:, :] + + assert torch.equal(interleaved[:, 0::2, :], gate_orig) + assert torch.equal(interleaved[:, 1::2, :], up_orig) + + def test_interleave_handles_list_input(self, sample_tensor): + op = ConcatenatedToInterleaved(dim=1) + result = op.convert( + {"test": [sample_tensor]}, + source_patterns=["test"], + target_patterns=["test"], + ) + assert result["test"].shape == sample_tensor.shape + + def test_reverse_op_type(self): + op = ConcatenatedToInterleaved(dim=1) + assert isinstance(op.reverse_op, InterleavedToConcatenated) + assert op.reverse_op.dim == 1 + + +class TestInterleavedToConcatenated: + @pytest.fixture + def interleaved_tensor(self): + """Create an interleaved tensor [E=2, 2*I=4, H=3].""" + E, I, H = 2, 2, 3 # noqa: E741 + gate = torch.arange(1, E * I * H + 1, dtype=torch.float32).reshape(E, I, H) + up = torch.arange(100, 100 + E * I * H, dtype=torch.float32).reshape(E, I, H) + interleaved = torch.empty(E, 2 * I, H) + interleaved[:, 0::2, :] = gate + interleaved[:, 1::2, :] = up + return interleaved + + def test_deinterleave_gate_up_separated(self, interleaved_tensor): + op = InterleavedToConcatenated(dim=1) + result = op.convert( + {"test": interleaved_tensor}, + source_patterns=["test"], + target_patterns=["test"], + ) + concatenated = result["test"] + + E, two_I, H = concatenated.shape + I = two_I // 2 # noqa: E741 + + # First half should be gate (even rows from interleaved) + assert torch.equal(concatenated[:, :I, :], interleaved_tensor[:, 0::2, :]) + # Second half should be up (odd rows from interleaved) + assert torch.equal(concatenated[:, I:, :], interleaved_tensor[:, 1::2, :]) + + def test_reverse_op_type(self): + op = InterleavedToConcatenated(dim=1) + assert isinstance(op.reverse_op, ConcatenatedToInterleaved) + assert op.reverse_op.dim == 1 + + +class TestRoundTrip: + @pytest.fixture + def concat_tensor(self): + E, I, H = 4, 8, 16 # noqa: E741 + gate = torch.randn(E, I, H) + up = torch.randn(E, I, H) + return torch.cat([gate, up], dim=1) + + def test_interleave_then_deinterleave_is_identity(self, concat_tensor): + fwd = ConcatenatedToInterleaved(dim=1) + rev = InterleavedToConcatenated(dim=1) + + interleaved = fwd.convert( + {"k": concat_tensor}, source_patterns=["k"], target_patterns=["k"] + )["k"] + recovered = rev.convert( + {"k": interleaved}, source_patterns=["k"], target_patterns=["k"] + )["k"] + + assert torch.equal(concat_tensor, recovered) + + def test_reverse_op_chain_is_identity(self, concat_tensor): + """Verify that op.reverse_op produces an exact inverse.""" + op = ConcatenatedToInterleaved(dim=1) + rev = op.reverse_op + + interleaved = op.convert( + {"k": concat_tensor}, source_patterns=["k"], target_patterns=["k"] + )["k"] + recovered = rev.convert( + {"k": interleaved}, source_patterns=["k"], target_patterns=["k"] + )["k"] + + assert torch.equal(concat_tensor, recovered) + + def test_various_shapes(self): + """Test with different expert counts and dimensions.""" + fwd = ConcatenatedToInterleaved(dim=1) + rev = InterleavedToConcatenated(dim=1) + + for E, I, H in [(1, 4, 8), (8, 16, 32), (16, 128, 256)]: # noqa: E741 + concat = torch.randn(E, 2 * I, H) + interleaved = fwd.convert( + {"k": concat}, source_patterns=["k"], target_patterns=["k"] + )["k"] + recovered = rev.convert( + {"k": interleaved}, source_patterns=["k"], target_patterns=["k"] + )["k"] + assert torch.equal(concat, recovered), ( + f"Failed for shape ({E}, {2 * I}, {H})" + ) + + +class TestWeightConverterRegistration: + def test_register_appends_interleave_op(self): + from transformers.conversion_mapping import get_checkpoint_conversion_mapping + + register_sonicmoe_weight_converter("qwen3_moe") + + modified = get_checkpoint_conversion_mapping("qwen3_moe") + # Find the gate_up_proj converter + gate_up_converter = None + for conv in modified: + if hasattr(conv, "operations") and any( + "gate_up_proj" in pat for pat in conv.target_patterns + ): + gate_up_converter = conv + break + + assert gate_up_converter is not None + assert isinstance(gate_up_converter.operations[-1], ConcatenatedToInterleaved) + + def test_double_registration_is_idempotent(self): + from transformers.conversion_mapping import get_checkpoint_conversion_mapping + + register_sonicmoe_weight_converter("qwen3_moe") + register_sonicmoe_weight_converter("qwen3_moe") + + modified = get_checkpoint_conversion_mapping("qwen3_moe") + for conv in modified: + if hasattr(conv, "operations") and any( + "gate_up_proj" in pat for pat in conv.target_patterns + ): + interleave_count = sum( + isinstance(op, ConcatenatedToInterleaved) for op in conv.operations + ) + assert interleave_count == 1, ( + f"Expected 1 ConcatenatedToInterleaved op, got {interleave_count}" + ) + break + + def test_register_unsupported_model_type_warns(self): + # A model type with no conversion mapping should warn but not raise + register_sonicmoe_weight_converter("nonexistent_model_type_xyz") + + +def _make_qwen_moe_block(T=8, H=16, E=4, K=2): + """Create a mock qwen-style MoE block for routing tests.""" + gate = SimpleNamespace( + weight=torch.randn(E, H), + top_k=K, + num_experts=E, + norm_topk_prob=True, + ) + return SimpleNamespace(gate=gate), T, H, E, K + + +def _make_glm_moe_block(T=8, H=16, E=16, K=4, n_group=2, topk_group=1): + """Create a mock GLM5-style MoE block for routing tests.""" + gate = SimpleNamespace( + weight=torch.randn(E, H), + e_score_correction_bias=torch.zeros(E), + ) + moe_block = SimpleNamespace( + gate=gate, + top_k=K, + n_routed_experts=E, + n_group=n_group, + topk_group=topk_group, + norm_topk_prob=True, + routed_scaling_factor=1.0, + ) + return moe_block, T, H, E, K + + +def _make_minimax_m2_moe_block(T=8, H=16, E=16, K=4): + """Create a mock minimax_m2-style MoE block for routing tests. + + minimax_m2 uses sigmoid->topk WITHOUT group selection: + - e_score_correction_bias is on the moe_block (not on gate) + - No n_group / topk_group attributes + - Always normalizes (norm_topk_prob defaults to True) + - No routed_scaling_factor (defaults to 1.0) + """ + gate = SimpleNamespace( + weight=torch.randn(E, H), + top_k=K, + ) + moe_block = SimpleNamespace( + gate=gate, + top_k=K, + e_score_correction_bias=torch.zeros(E), + ) + return moe_block, T, H, E, K + + +class TestSoftmaxTopkRouting: + def test_output_shapes(self): + moe_block, T, H, E, K = _make_qwen_moe_block() + hidden = torch.randn(T, H) + + scores, token_idx, expert_idx, logits = softmax_topk_routing(hidden, moe_block) + + assert scores.shape == (T * K,) + assert token_idx.shape == (T * K,) + assert expert_idx.shape == (T * K,) + assert logits.shape == (T, E) + + def test_scores_are_float32(self): + moe_block, T, H, E, K = _make_qwen_moe_block() + hidden = torch.randn(T, H) + + scores, _, _, _ = softmax_topk_routing(hidden, moe_block) + assert scores.dtype == torch.float32 + + def test_token_indices_sorted_ascending(self): + moe_block, T, H, E, K = _make_qwen_moe_block() + hidden = torch.randn(T, H) + + _, token_idx, _, _ = softmax_topk_routing(hidden, moe_block) + + # Token indices must be sorted ascending (SonicMoE requirement) + diffs = token_idx[1:] - token_idx[:-1] + assert (diffs >= 0).all() + + def test_expert_indices_in_range(self): + moe_block, T, H, E, K = _make_qwen_moe_block() + hidden = torch.randn(T, H) + + _, _, expert_idx, _ = softmax_topk_routing(hidden, moe_block) + + assert (expert_idx >= 0).all() + assert (expert_idx < E).all() + + def test_renormalized_scores_sum_to_one(self): + moe_block, T, H, E, K = _make_qwen_moe_block() + hidden = torch.randn(T, H) + + scores, _, _, _ = softmax_topk_routing(hidden, moe_block) + per_token_sums = scores.reshape(T, K).sum(dim=-1) + assert torch.allclose(per_token_sums, torch.ones(T), atol=1e-5) + + +class TestSigmoidTopkRouting: + def test_output_shapes(self): + moe_block, T, H, E, K = _make_glm_moe_block() + hidden = torch.randn(T, H) + + scores, token_idx, expert_idx, logits = sigmoid_topk_routing(hidden, moe_block) + + assert scores.shape == (T * K,) + assert token_idx.shape == (T * K,) + assert expert_idx.shape == (T * K,) + assert logits.shape == (T, E) + + def test_scores_are_float32(self): + moe_block, T, H, E, K = _make_glm_moe_block() + hidden = torch.randn(T, H) + + scores, _, _, _ = sigmoid_topk_routing(hidden, moe_block) + assert scores.dtype == torch.float32 + + def test_token_indices_sorted_ascending(self): + moe_block, T, H, E, K = _make_glm_moe_block() + hidden = torch.randn(T, H) + + _, token_idx, _, _ = sigmoid_topk_routing(hidden, moe_block) + + diffs = token_idx[1:] - token_idx[:-1] + assert (diffs >= 0).all() + + def test_expert_indices_in_range(self): + moe_block, T, H, E, K = _make_glm_moe_block() + hidden = torch.randn(T, H) + + _, _, expert_idx, _ = sigmoid_topk_routing(hidden, moe_block) + + assert (expert_idx >= 0).all() + assert (expert_idx < E).all() + + def test_scores_are_nonnegative(self): + """Sigmoid outputs are in [0, 1], so scores should be non-negative.""" + moe_block, T, H, E, K = _make_glm_moe_block() + hidden = torch.randn(T, H) + + scores, _, _, _ = sigmoid_topk_routing(hidden, moe_block) + assert (scores >= 0).all() + + def test_scaling_factor_applied(self): + moe_block, T, H, E, K = _make_glm_moe_block() + hidden = torch.randn(T, H) + + # Get scores with scaling_factor=1.0 + scores_1x, _, _, _ = sigmoid_topk_routing(hidden, moe_block) + + # Get scores with scaling_factor=2.0 + moe_block.routed_scaling_factor = 2.0 + scores_2x, _, _, _ = sigmoid_topk_routing(hidden, moe_block) + + assert torch.allclose(scores_2x, scores_1x * 2.0, atol=1e-5) + + def test_group_selection_restricts_experts(self): + """With n_group=4 and topk_group=1, only 1/4 of experts should be selectable.""" + moe_block, T, H, E, K = _make_glm_moe_block(E=16, K=2, n_group=4, topk_group=1) + hidden = torch.randn(T, H) + + _, _, expert_idx, _ = sigmoid_topk_routing(hidden, moe_block) + + # Each token's experts should all fall within a single group (size E//n_group=4) + expert_idx_2d = expert_idx.reshape(T, K) + for t in range(T): + experts = expert_idx_2d[t] + groups = experts // (E // moe_block.n_group) + # All selected experts should be from the same group + assert (groups == groups[0]).all() + + +class TestMiniMaxM2SigmoidRouting: + """Tests for minimax_m2 routing: sigmoid->topk without group selection.""" + + def test_output_shapes(self): + """Validates getattr defaults work: n_group=1, E from gate.weight.shape[0].""" + moe_block, T, H, E, K = _make_minimax_m2_moe_block() + hidden = torch.randn(T, H) + + scores, token_idx, expert_idx, logits = sigmoid_topk_routing(hidden, moe_block) + + assert scores.shape == (T * K,) + assert token_idx.shape == (T * K,) + assert expert_idx.shape == (T * K,) + assert logits.shape == (T, E) + + def test_bias_on_block_not_gate(self): + """Verify that e_score_correction_bias on the block (not gate) is used.""" + T, H, E, K = 8, 16, 8, 2 + gate = SimpleNamespace( + weight=torch.randn(E, H), + top_k=K, + ) + # Large positive bias on expert 0 should make it selected more often + bias = torch.zeros(E) + bias[0] = 100.0 + moe_block = SimpleNamespace( + gate=gate, + top_k=K, + e_score_correction_bias=bias, + ) + hidden = torch.randn(T, H) + + _, _, expert_idx, _ = sigmoid_topk_routing(hidden, moe_block) + + # Expert 0 should appear for every token due to the large bias + expert_idx_2d = expert_idx.reshape(T, K) + for t in range(T): + assert 0 in expert_idx_2d[t] diff --git a/tests/integrations/test_sonicmoe_gradients.py b/tests/integrations/test_sonicmoe_gradients.py new file mode 100644 index 0000000000..e76bdd4806 --- /dev/null +++ b/tests/integrations/test_sonicmoe_gradients.py @@ -0,0 +1,158 @@ +""" +Gradient correctness tests for SonicMoE routing functions (CPU-only). + +Uses torch.autograd.gradcheck with float32 inputs to match the production +code path where routing happens in float32. +""" + +import torch + +from axolotl.integrations.kernels.sonicmoe.routing import ( + sigmoid_topk_routing, + softmax_topk_routing, +) + +_GC_EPS = 1e-3 +_GC_ATOL = 1e-3 +_GC_RTOL = 1e-3 + + +def _make_softmax_moe_block(weight): + gate = torch.nn.Module() + gate.weight = weight + gate.top_k = 2 + gate.norm_topk_prob = True + + moe_block = torch.nn.Module() + moe_block.gate = gate + return moe_block + + +def _make_sigmoid_moe_block(weight, bias): + gate = torch.nn.Module() + gate.weight = weight + gate.e_score_correction_bias = bias + + moe_block = torch.nn.Module() + moe_block.gate = gate + moe_block.top_k = 2 + moe_block.n_routed_experts = weight.shape[0] + moe_block.n_group = 1 + moe_block.norm_topk_prob = True + moe_block.routed_scaling_factor = 1.0 + return moe_block + + +class TestSoftmaxTopkRoutingGradcheck: + """Numerical gradient verification for softmax_topk_routing.""" + + def test_gradcheck_wrt_gate_weight(self): + T, H, E = 4, 8, 4 + + hidden = torch.randn(T, H, dtype=torch.float32) + + def fn(weight): + moe_block = _make_softmax_moe_block(weight) + scores, _, _, _ = softmax_topk_routing(hidden, moe_block) + return scores + + weight = torch.randn(E, H, dtype=torch.float32, requires_grad=True) + torch.autograd.gradcheck( + fn, (weight,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL + ) + + def test_gradcheck_wrt_hidden_states(self): + T, H, E = 4, 8, 4 + + weight = torch.randn(E, H, dtype=torch.float32) + moe_block = _make_softmax_moe_block(weight) + + def fn(hidden): + scores, _, _, _ = softmax_topk_routing(hidden, moe_block) + return scores + + hidden = torch.randn(T, H, dtype=torch.float32, requires_grad=True) + torch.autograd.gradcheck( + fn, (hidden,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL + ) + + def test_gradcheck_wrt_router_logits(self): + T, H, E = 4, 8, 4 + + hidden = torch.randn(T, H, dtype=torch.float32) + + def fn(weight): + moe_block = _make_softmax_moe_block(weight) + _, _, _, router_logits = softmax_topk_routing(hidden, moe_block) + return router_logits + + weight = torch.randn(E, H, dtype=torch.float32, requires_grad=True) + torch.autograd.gradcheck( + fn, (weight,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL + ) + + def test_no_norm_variant(self): + T, H, E = 4, 8, 4 + + hidden = torch.randn(T, H, dtype=torch.float32) + + def fn(weight): + moe_block = _make_softmax_moe_block(weight) + moe_block.gate.norm_topk_prob = False + scores, _, _, _ = softmax_topk_routing(hidden, moe_block) + return scores + + weight = torch.randn(E, H, dtype=torch.float32, requires_grad=True) + torch.autograd.gradcheck( + fn, (weight,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL + ) + + +class TestSigmoidTopkRoutingGradcheck: + """Numerical gradient verification for sigmoid_topk_routing.""" + + def test_gradcheck_wrt_gate_weight(self): + T, H, E = 4, 8, 4 + + hidden = torch.randn(T, H, dtype=torch.float32) + bias = torch.zeros(E, dtype=torch.float32) + + def fn(weight): + moe_block = _make_sigmoid_moe_block(weight, bias) + scores, _, _, _ = sigmoid_topk_routing(hidden, moe_block) + return scores + + weight = torch.randn(E, H, dtype=torch.float32, requires_grad=True) + torch.autograd.gradcheck( + fn, (weight,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL + ) + + def test_gradcheck_wrt_hidden_states(self): + T, H, E = 4, 8, 4 + + weight = torch.randn(E, H, dtype=torch.float32) + bias = torch.zeros(E, dtype=torch.float32) + moe_block = _make_sigmoid_moe_block(weight, bias) + + def fn(hidden): + scores, _, _, _ = sigmoid_topk_routing(hidden, moe_block) + return scores + + hidden = torch.randn(T, H, dtype=torch.float32, requires_grad=True) + torch.autograd.gradcheck( + fn, (hidden,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL + ) + + def test_gradcheck_wrt_bias(self): + T, H, E = 4, 8, 4 + + hidden = torch.randn(T, H, dtype=torch.float32) + weight = torch.randn(E, H, dtype=torch.float32) + + def fn(bias): + moe_block = _make_sigmoid_moe_block(weight, bias) + scores, _, _, _ = sigmoid_topk_routing(hidden, moe_block) + return scores + + bias = torch.zeros(E, dtype=torch.float32, requires_grad=True) + torch.autograd.gradcheck(fn, (bias,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL) From 234931d5127a6ac9162e139fc2317ef62707c53b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 5 Mar 2026 15:04:38 -0500 Subject: [PATCH 1161/1405] extend pytest-sdist timeout to 30 min for slow/flaky tests (#3456) [skip ci] * extend pytest-sdist timeout to 30 min for slow/flaky tests * Also preload the cdn cache so it doesn't get stampeded * fix yaml syntax * missing fields * can't pipe to dev/null * Fix nightlies and add 2.10.0 to multi-gpu suite --- .github/workflows/multi-gpu-e2e.yml | 13 +++++++------ .github/workflows/tests-nightly.yml | 26 +++++++++++++++++++++++--- .github/workflows/tests.yml | 16 ++++++++++++++-- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 5187a08c78..c1e5c5d75c 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -35,12 +35,6 @@ jobs: pytorch: 2.8.0 axolotl_extras: fbgemm-gpu num_gpus: 2 - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.9.1 - axolotl_extras: "fbgemm-gpu" - num_gpus: 2 - cuda: 129 cuda_version: 12.9.1 python_version: "3.12" @@ -55,6 +49,13 @@ jobs: axolotl_extras: # axolotl_extras: fbgemm-gpu num_gpus: 2 + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.11" + pytorch: 2.10.0 + axolotl_extras: "fbgemm-gpu" + num_gpus: 2 + dockerfile: "Dockerfile-uv.jinja" runs-on: [self-hosted, modal] timeout-minutes: 120 steps: diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 33aa8525a9..45596a2e14 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -18,15 +18,27 @@ jobs: env: SKIP: no-commit-to-branch + prime-cdn-s3-cache: + name: Prefetch S3 once to prime the CDN cache + runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} + timeout-minutes: 10 + steps: + - name: Restore Cache from S3 + id: hf-cache-restore-s3 + run: | + curl -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst > /dev/null + pytest: name: PyTest runs-on: ubuntu-latest + needs: [prime-cdn-s3-cache] strategy: fail-fast: false max-parallel: 2 matrix: - python_version: ["3.11"] - pytorch_version: ["2.8.0", "2.9.0", "2.9.1"] + python_version: ["3.12"] # TODO include py3.14 once https://github.com/mistralai/mistral-common/pull/194 is merged + pytorch_version: ["2.8.0", "2.9.1", "2.10.0"] timeout-minutes: 20 steps: @@ -102,16 +114,23 @@ jobs: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" - pytorch: 2.8.0 + pytorch: 2.9.1 num_gpus: 1 axolotl_extras: nightly_build: "true" - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" + pytorch: 2.10.0 + num_gpus: 1 + axolotl_extras: + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.12" pytorch: 2.9.1 num_gpus: 1 axolotl_extras: + dockerfile: "Dockerfile-uv.jinja" nightly_build: "true" steps: - name: Checkout @@ -132,6 +151,7 @@ jobs: echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV + echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV echo "NIGHTLY_BUILD=${{ matrix.nightly_build }}" >> $GITHUB_ENV echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV - name: Run tests job on Modal diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 60610450a0..abb4cba9f7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,11 +46,22 @@ jobs: env: SKIP: no-commit-to-branch + prime-cdn-s3-cache: + name: Prefetch S3 once to prime the CDN cache + runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} + timeout-minutes: 10 + steps: + - name: Restore Cache from S3 + id: hf-cache-restore-s3 + run: | + curl -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst > /dev/null + pytest: name: PyTest runs-on: ubuntu-latest if: ${{ !github.event.pull_request.draft }} -# needs: [preload-cache] + needs: [prime-cdn-s3-cache] strategy: fail-fast: false matrix: @@ -146,6 +157,7 @@ jobs: name: PyTest from Source Dist runs-on: ubuntu-latest if: ${{ !github.event.pull_request.draft }} + needs: [prime-cdn-s3-cache] strategy: fail-fast: false matrix: @@ -156,7 +168,7 @@ jobs: # pytorch_version: "2.8.0" # - python_version: "3.14" # pytorch_version: "2.9.1" - timeout-minutes: 20 + timeout-minutes: 30 steps: - name: cleanup node From 6c44afaea1b517590f5aae7ee3c7a1565b57cadf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:39:34 -0500 Subject: [PATCH 1162/1405] chore: update pre-commit hooks (#3381) [skip ci] Co-authored-by: SalmanMohammadi <25081738+SalmanMohammadi@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 44d6e00db7..72da2e0991 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: no-commit-to-branch args: ['--branch', 'main'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.10 + rev: v0.15.4 hooks: - id: ruff args: [--fix] @@ -26,7 +26,7 @@ repos: 'pydantic>=2.5.3', ] - repo: https://github.com/PyCQA/bandit - rev: 1.9.2 + rev: 1.9.4 hooks: - id: bandit args: [ From 56162f71db17fbb665ad108e7dc701a79d9e066b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 6 Mar 2026 09:10:58 -0500 Subject: [PATCH 1163/1405] monkeypatch fix for fsdp with cpu ram efficient loading (#3464) [skip ci] --- src/axolotl/loaders/patch_manager.py | 7 +++++ src/axolotl/monkeypatch/accelerate/fsdp2.py | 33 +++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 87520c06f1..51bcaeba44 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -166,6 +166,13 @@ def _apply_chunked_cross_entropy_patch(self): def _apply_fsdp_patches(self): """Apply patches for FSDP configurations.""" + if self.cfg.fsdp_config: + from axolotl.monkeypatch.accelerate.fsdp2 import ( + patch_initialize_missing_keys_for_fsdp, + ) + + patch_initialize_missing_keys_for_fsdp() + if self.cfg.context_parallel_size > 1 or ( self.cfg.fsdp_config and str(self.cfg.fsdp_version) == "2" ): diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index dd3deb19a9..1fa589f07b 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -479,6 +479,39 @@ def _patched_adjust_tied_keys_with_tied_pointers(self, missing_keys): ) +def patch_initialize_missing_keys_for_fsdp(): + """Patch _initialize_missing_keys to skip re-initialization on FSDP non-rank-0. + + When using cpu_ram_efficient_loading, non-rank-0 processes load weights on + meta device and move them to CPU as empty tensors. Without this patch, + initialize_weights() re-initializes ALL parameters (via guarded init + functions), which is slow and uses extra RAM per process. + + The fix marks all params/buffers with _is_hf_initialized=True before calling + the original method, so guarded init functions (init.normal_, init.zeros_, + etc.) become no-ops on non-rank-0 processes. The real weights arrive later + via FSDP broadcast from rank 0. + + Upstream fix: https://github.com/huggingface/transformers/pull/44473 + Remove this patch once transformers includes the fix in a stable release. + """ + from transformers import PreTrainedModel + from transformers.modeling_utils import is_fsdp_enabled, is_local_dist_rank_0 + + _original_initialize_missing_keys = PreTrainedModel._initialize_missing_keys + + def _patched_initialize_missing_keys(self, is_quantized: bool) -> None: + if is_fsdp_enabled() and not is_local_dist_rank_0(): + for key in self.state_dict(): + param_or_buffer = self.get_parameter_or_buffer(key) + param_or_buffer._is_hf_initialized = True + self._is_hf_initialized = True + + _original_initialize_missing_keys(self, is_quantized) + + PreTrainedModel._initialize_missing_keys = _patched_initialize_missing_keys + + def patch_accelerate_fsdp2(): import accelerate From cada93cee52df30c13ad21a774b695f2ad641bf5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 6 Mar 2026 09:11:20 -0500 Subject: [PATCH 1164/1405] upgrade transformers==5.3.0 trl==0.29.0 kernels (#3459) * upgrade transformers==5.3.0 trl==0.29.0 kernels * use latest deepspeed fixes * use corect image for cleanup * fix test outputs for tokenizer fixes upstream * fix import: * keep trl at 0.28.0 * handle updated API * use latest trl since 0.28.0 doesn't work with latest transformers * use trl experimental for pad to length * monkeypatch trl with ORPOTrainer so liger doesn't croak * upgrade accelerate * more fixes * move patch for orpotrainer * load the imports later * remove use_logits_to_keep * fix loss_type arg as a list * fetch hf cache from s3 * just manually download the missing model for now * lint for pre-commit update * a few more missing models on disk * fix: loss_type internally now list * fix: remove deprecated code and raise deprecate * fix: remove unneeded blocklist * fix: remove reliance on transformers api to find package available * chore: refactor shim for less sideeffect * fix: silent trl experimental warning --------- Co-authored-by: NanoCode012 --- .github/workflows/tests.yml | 4 ++-- cicd/cicd.sh | 6 +++++ requirements.txt | 12 +++++----- src/axolotl/cli/__init__.py | 1 + src/axolotl/core/builders/rl.py | 5 ----- src/axolotl/core/trainers/base.py | 2 +- src/axolotl/core/trainers/dpo/__init__.py | 4 ---- src/axolotl/core/trainers/dpo/trainer.py | 4 ++-- src/axolotl/core/trainers/mixins/optimizer.py | 6 ++--- src/axolotl/integrations/liger/plugin.py | 14 +++++++++--- src/axolotl/loaders/model.py | 4 ++-- src/axolotl/loaders/tokenizer.py | 2 +- src/axolotl/utils/data/shared.py | 2 +- src/axolotl/utils/schemas/config.py | 2 -- src/axolotl/utils/schemas/deprecated.py | 22 +++++++++++++++++++ tests/core/test_builders.py | 21 +++++++++++------- tests/integrations/test_swanlab.py | 4 ++-- tests/telemetry/test_runtime_metrics.py | 12 +++++----- tests/test_tokenizers.py | 3 ++- 19 files changed, 81 insertions(+), 49 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index abb4cba9f7..f8c9a37bb8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -387,8 +387,8 @@ jobs: fail-fast: false matrix: include: - - cuda: 129 - cuda_version: 12.9.1 + - cuda: 128 + cuda_version: 12.8.1 python_version: "3.11" pytorch: 2.9.1 num_gpus: 1 diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 65ee8699de..462b874a6b 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -3,6 +3,12 @@ set -e python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__" +# curl -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst | tar -xpf - -C "${HF_HOME}/hub/" --use-compress-program unzstd --strip-components=1 +hf download "NousResearch/Meta-Llama-3-8B" +hf download "NousResearch/Meta-Llama-3-8B-Instruct" +hf download "microsoft/Phi-4-reasoning" +hf download "microsoft/Phi-3.5-mini-instruct" + # Run unit tests with initial coverage report pytest -v --durations=10 -n8 \ --ignore=tests/e2e/ \ diff --git a/requirements.txt b/requirements.txt index 710e24d718..472a98bc87 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,13 +12,13 @@ packaging==26.0 huggingface_hub>=1.1.7 peft>=0.18.1 tokenizers>=0.22.1 -transformers==5.2.0 -accelerate==1.12.0 +transformers==5.3.0 +accelerate==1.13.0 datasets==4.5.0 -deepspeed>=0.18.3 -trl==0.28.0 -hf_xet==1.2.0 -kernels==0.12.1 +deepspeed>=0.18.6,<0.19.0 +trl==0.29.0 +hf_xet==1.3.2 +kernels==0.12.2 trackio>=0.16.1 typing-extensions>=4.15.0 diff --git a/src/axolotl/cli/__init__.py b/src/axolotl/cli/__init__.py index 6d07548063..799d5694ef 100644 --- a/src/axolotl/cli/__init__.py +++ b/src/axolotl/cli/__init__.py @@ -6,5 +6,6 @@ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") os.environ.setdefault("HF_XET_HIGH_PERFORMANCE", "1") +os.environ.setdefault("TRL_EXPERIMENTAL_SILENCE", "1") configure_logging() diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index 5a7343ca78..bb67aef6df 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -120,11 +120,6 @@ def _build_training_arguments(self, total_num_steps): if self.cfg.use_wandb: training_args_kwargs["run_name"] = self.cfg.wandb_name - if self.cfg.max_prompt_len: - training_args_kwargs["max_prompt_length"] = self.cfg.max_prompt_len - else: - training_args_kwargs["max_prompt_length"] = self.cfg.sequence_len - training_args_cls = None blocklist_args_kwargs = [] if self.cfg.rl is RLType.SIMPO: diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 76e8f105f8..0b392f4d88 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -26,7 +26,7 @@ from transformers.trainer import TRAINING_ARGS_NAME from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR, has_length, seed_worker from transformers.utils import SAFE_WEIGHTS_NAME, is_peft_available -from trl.trainer.utils import pad_to_length +from trl.experimental.utils import pad_to_length from typing_extensions import override from axolotl.core.trainers.mixins import ( diff --git a/src/axolotl/core/trainers/dpo/__init__.py b/src/axolotl/core/trainers/dpo/__init__.py index 5e160e6921..93634f64bd 100644 --- a/src/axolotl/core/trainers/dpo/__init__.py +++ b/src/axolotl/core/trainers/dpo/__init__.py @@ -25,17 +25,13 @@ def set_training_args_kwargs(cls, cfg): # Label smoothing is not compatible with IPO if cfg.rl is RLType.DPO and cfg.dpo_label_smoothing: training_args_kwargs["label_smoothing"] = cfg.dpo_label_smoothing - training_args_kwargs["max_completion_length"] = None training_args_kwargs["max_length"] = cfg.sequence_len - training_args_kwargs["generate_during_eval"] = cfg.dpo_generate_during_eval if cfg.dpo_use_weighting is not None: training_args_kwargs["use_weighting"] = cfg.dpo_use_weighting if cfg.dpo_padding_free is not None: training_args_kwargs["padding_free"] = cfg.dpo_padding_free if cfg.dpo_norm_loss is not None: training_args_kwargs["dpo_norm_loss"] = cfg.dpo_norm_loss - if cfg.dpo_use_logits_to_keep is not None: - training_args_kwargs["use_logits_to_keep"] = cfg.dpo_use_logits_to_keep if cfg.dpo_use_liger_kernel is not None: training_args_kwargs["use_liger_kernel"] = cfg.dpo_use_liger_kernel return training_args_kwargs diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index 92307fe238..3c0bca3d42 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -103,10 +103,10 @@ def concatenated_forward( ) -> dict[str, torch.Tensor]: if self.args.dpo_norm_loss: # fmt: off - loss_type: str = self.loss_type # type: ignore[has-type] + loss_type: list[str] = self.loss_type # type: ignore[has-type] # fmt: on # concatenated_forward handles avg token logprob for ipo case already - self.loss_type = "ipo" + self.loss_type = ["ipo"] res = super().concatenated_forward(model, batch, is_ref_model=is_ref_model) self.loss_type = loss_type return res diff --git a/src/axolotl/core/trainers/mixins/optimizer.py b/src/axolotl/core/trainers/mixins/optimizer.py index 850442c608..dc011d2b15 100644 --- a/src/axolotl/core/trainers/mixins/optimizer.py +++ b/src/axolotl/core/trainers/mixins/optimizer.py @@ -104,7 +104,7 @@ def create_optimizer_grouped_parameters( return optimizer_grouped_parameters - def create_optimizer(self): + def create_optimizer(self, model=None): if ( self.args.loraplus_lr_ratio is None and self.args.embedding_lr_scale is None @@ -112,9 +112,9 @@ def create_optimizer(self): and self.args.lr_groups is None and self.optimizer_cls_and_kwargs is None ): - return super().create_optimizer() + return super().create_optimizer(model=model) - opt_model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model + opt_model = self.model if model is None else model if ( not self.optimizer diff --git a/src/axolotl/integrations/liger/plugin.py b/src/axolotl/integrations/liger/plugin.py index ac796c2c90..cfd652872a 100644 --- a/src/axolotl/integrations/liger/plugin.py +++ b/src/axolotl/integrations/liger/plugin.py @@ -8,9 +8,6 @@ from axolotl.integrations.base import BasePlugin from axolotl.utils.logging import get_logger -from .models.base import patch_lce_forward -from .utils import patch_with_compile_disable - LOG = get_logger(__name__) @@ -23,10 +20,18 @@ def get_input_args(self): return "axolotl.integrations.liger.LigerArgs" def pre_model_load(self, cfg): + # shim: liger-kernel 0.7.0 imports ORPOTrainer from old trl path + import trl.trainer + from trl.experimental.orpo import ORPOTrainer + + trl.trainer.ORPOTrainer = ORPOTrainer + if cfg.torch_compile: # torch compile will unnecessarily attempt to optimize the triton kernel unless explicitly disabled import liger_kernel.ops.fused_linear_cross_entropy + from .utils import patch_with_compile_disable + patch_with_compile_disable( liger_kernel.ops.fused_linear_cross_entropy, "fused_linear_cross_entropy_forward", @@ -35,6 +40,7 @@ def pre_model_load(self, cfg): liger_kernel.ops.fused_linear_cross_entropy, "fused_linear_cross_entropy_backward", ) + from liger_kernel.transformers.cross_entropy import LigerCrossEntropyLoss from liger_kernel.transformers.functional import liger_cross_entropy from liger_kernel.transformers.layer_norm import LigerLayerNorm @@ -192,6 +198,8 @@ def patched_init(self, *args, **kwargs): ) elif cfg.liger_fused_linear_cross_entropy: try: + from .models.base import patch_lce_forward + patch_lce_forward(cfg.model_config_type) LOG.warning_once( f"Applied ONLY liger_fused_linear_cross_entropy genericpatches for model type: {cfg.model_config_type}" diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 3be557a42d..03c1f35bcf 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -674,8 +674,8 @@ def _configure_zero3_memory_efficient_loading( del self.model_kwargs["device_map"] transformers.modeling_utils.is_deepspeed_zero3_enabled = lambda: True - transformers.integrations.deepspeed.is_deepspeed_zero3_enabled = ( - lambda: True + transformers.integrations.deepspeed.is_deepspeed_zero3_enabled = lambda: ( + True ) return hf_ds_cfg diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index d45d23baea..a5c9855e12 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -201,7 +201,7 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): tokenizer.pad_token = LLAMA_DEFAULT_EOS_TOKEN if tokenizer.__class__.__name__ == "GPTNeoXTokenizerFast": - tokenizer.add_special_tokens({"pad_token": "[PAD]"}) + tokenizer.add_special_tokens({"pad_token": "[PAD]"}) # nosec B105 os.environ["TOKENIZERS_PARALLELISM"] = "false" # Mistral's official FA implementation requires left padding diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 351669ec3f..1d1f8be54a 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -189,7 +189,7 @@ def _get_remote_filesystem( try: import gcsfs - storage_options = {"token": None} # type: ignore + storage_options = {"token": None} # type: ignore # nosec B105 return gcsfs.GCSFileSystem(**storage_options), storage_options except ImportError as exc: raise ImportError( diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 5c0d31ff0d..8d53cec524 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -173,7 +173,6 @@ class AxolotlInputConfig( "description": "Whether to perform weighting in DPO trainer" }, ) - dpo_use_logits_to_keep: bool | None = None dpo_label_smoothing: float | None = None dpo_norm_loss: bool | None = None @@ -183,7 +182,6 @@ class AxolotlInputConfig( ) dpo_padding_free: bool | None = None - dpo_generate_during_eval: bool | None = None datasets: ( Annotated[ diff --git a/src/axolotl/utils/schemas/deprecated.py b/src/axolotl/utils/schemas/deprecated.py index 9dfe692645..62b26949e2 100644 --- a/src/axolotl/utils/schemas/deprecated.py +++ b/src/axolotl/utils/schemas/deprecated.py @@ -19,6 +19,8 @@ class DeprecatedParameters(BaseModel): evaluation_strategy: str | None = None eval_table_size: int | None = None eval_max_new_tokens: int | None = None + dpo_use_logits_to_keep: bool | None = None + dpo_generate_during_eval: bool | None = None @field_validator("max_packed_sequence_len") @classmethod @@ -78,6 +80,26 @@ def validate_eval_max_new_tokens(cls, eval_max_new_tokens): ) return eval_max_new_tokens + @field_validator("dpo_use_logits_to_keep") + @classmethod + def validate_dpo_use_logits_to_keep(cls, dpo_use_logits_to_keep): + if dpo_use_logits_to_keep is not None: + raise DeprecationWarning( + "`dpo_use_logits_to_keep` is no longer supported, " + "it has been removed in TRL >= 0.29.0" + ) + return dpo_use_logits_to_keep + + @field_validator("dpo_generate_during_eval") + @classmethod + def validate_dpo_generate_during_eval(cls, dpo_generate_during_eval): + if dpo_generate_during_eval is not None: + raise DeprecationWarning( + "`dpo_generate_during_eval` is no longer supported, " + "it has been removed in TRL >= 0.29.0" + ) + return dpo_generate_during_eval + class RemappedParameters(BaseModel): """Parameters that have been remapped to other names""" diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index fc16f723e9..ea3c4e6c4e 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -2,7 +2,7 @@ import sys from pathlib import Path -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest @@ -94,7 +94,6 @@ def fixture_dpo_cfg(base_cfg): { "rl": RLType.DPO, "dpo_use_weighting": True, - "dpo_use_logits_to_keep": True, "dpo_label_smoothing": 0.1, "beta": 0.1, # DPO beta } @@ -148,9 +147,16 @@ def fixture_grpo_cfg(base_cfg): ), # Must be evenly divisible by num_generations "micro_batch_size": 4, + "datasets": [ + { + "path": "openai/gsm8k", + "name": "main", + "split": "train[:1%]", + } + ], } ) - return cfg + return DictDefault(cfg) @pytest.fixture(name="ipo_cfg") @@ -334,6 +340,7 @@ def test_grpo_training_arguments(self, grpo_cfg, model, tokenizer, tmp_path): try: builder = HFRLTrainerBuilder(grpo_cfg, model, tokenizer) training_arguments, _ = builder._build_training_arguments(100) + builder.train_dataset = MagicMock() self._test_common_training_arguments(training_arguments, rl=grpo_cfg.rl) # GRPO specific @@ -363,7 +370,7 @@ def test_ipo_training_arguments(self, ipo_cfg, model, tokenizer): self._test_common_training_arguments(training_arguments, rl=ipo_cfg.rl) # IPO specific assert training_arguments.beta == 0.1 - assert training_arguments.loss_type == "ipo" + assert training_arguments.loss_type == ["ipo"] assert training_arguments.label_smoothing == 0 def test_simpo_training_arguments(self, simpo_cfg, model, tokenizer): @@ -529,13 +536,11 @@ def test_training_arguments(self, sft_cfg, model, tokenizer): "cfg_string", [ "sft_cfg", - "rm_cfg", + # "rm_cfg", # TODO fix for num_labels = 2 vs 1 "prm_cfg", ], ) - def test_custom_optimizer_cls_and_kwargs( - self, request, cfg_string, model, tokenizer - ): + def test_builder_w_rm_trainers(self, request, cfg_string, model, tokenizer): cfg = request.getfixturevalue(cfg_string) builder = HFCausalTrainerBuilder(cfg, model, tokenizer) cfg["optimizer"] = "muon" diff --git a/tests/integrations/test_swanlab.py b/tests/integrations/test_swanlab.py index b86df0b0e3..e672658e65 100644 --- a/tests/integrations/test_swanlab.py +++ b/tests/integrations/test_swanlab.py @@ -18,6 +18,7 @@ Tests conflict detection, configuration validation, and multi-logger warnings. """ +import importlib.util import logging import os import time @@ -25,12 +26,11 @@ import pytest from pydantic import ValidationError -from transformers.utils.import_utils import _is_package_available from axolotl.integrations.swanlab.args import SwanLabConfig from axolotl.integrations.swanlab.plugins import SwanLabPlugin -SWANLAB_INSTALLED = _is_package_available("swanlab") +SWANLAB_INSTALLED = importlib.util.find_spec("swanlab") is not None @pytest.mark.skipif(not SWANLAB_INSTALLED, reason="swanlab package not installed") diff --git a/tests/telemetry/test_runtime_metrics.py b/tests/telemetry/test_runtime_metrics.py index c8916e072e..faa62d0749 100644 --- a/tests/telemetry/test_runtime_metrics.py +++ b/tests/telemetry/test_runtime_metrics.py @@ -52,8 +52,8 @@ def mock_torch(): mock_torch.cuda.device_count.return_value = 2 # Mock memory allocated per device (1GB for device 0, 2GB for device 1) - mock_torch.cuda.memory_allocated.side_effect = ( - lambda device: (device + 1) * 1024 * 1024 * 1024 + mock_torch.cuda.memory_allocated.side_effect = lambda device: ( + (device + 1) * 1024 * 1024 * 1024 ) yield mock_torch @@ -292,8 +292,8 @@ def test_update_memory_metrics( mock_memory_info = mock_process.memory_info.return_value mock_memory_info.rss = 0.5 * 1024 * 1024 * 1024 # 0.5GB - mock_torch.cuda.memory_allocated.side_effect = ( - lambda device: (device + 0.5) * 1024 * 1024 * 1024 + mock_torch.cuda.memory_allocated.side_effect = lambda device: ( + (device + 0.5) * 1024 * 1024 * 1024 ) # Update memory metrics again @@ -307,8 +307,8 @@ def test_update_memory_metrics( # Change mocked memory values to be higher mock_memory_info.rss = 2 * 1024 * 1024 * 1024 # 2GB - mock_torch.cuda.memory_allocated.side_effect = ( - lambda device: (device + 2) * 1024 * 1024 * 1024 + mock_torch.cuda.memory_allocated.side_effect = lambda device: ( + (device + 2) * 1024 * 1024 * 1024 ) # Update memory metrics again diff --git a/tests/test_tokenizers.py b/tests/test_tokenizers.py index 82cae9b4a8..0f8c584e2f 100644 --- a/tests/test_tokenizers.py +++ b/tests/test_tokenizers.py @@ -84,7 +84,8 @@ def test_add_additional_special_tokens(self): } ) tokenizer = load_tokenizer(cfg) - assert tokenizer("<|im_start|>user")["input_ids"] == [1, 32000, 1404] + assert "LlamaTokenizer" in tokenizer.__class__.__name__ + assert tokenizer("<|im_start|>user")["input_ids"] == [1, 32000, 1792] assert len(tokenizer) == 32001 # ensure reloading the tokenizer again from cfg results in same vocab length From da17c7c0d94d63b120a562fab44950d65bd57e62 Mon Sep 17 00:00:00 2001 From: Gilles Turpin Date: Fri, 6 Mar 2026 15:18:13 +0100 Subject: [PATCH 1165/1405] fix: use dp_world_size instead of world_size for batch_size with tensor parallelism (#3462) [skip ci] --- src/axolotl/utils/config/__init__.py | 6 ++- src/axolotl/utils/trainer.py | 12 +----- tests/test_tensor_parallel_batch_size.py | 54 ++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 tests/test_tensor_parallel_batch_size.py diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 1c0e93e034..07c4d175f4 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -119,7 +119,11 @@ def normalize_config(cfg): if cfg.world_size != 1: cfg.device_map = {"": int(os.environ.get("LOCAL_RANK", 0))} if cfg.fsdp or cfg.fsdp_config or cfg.ddp: - effective_world_size = cfg.world_size // (cfg.context_parallel_size or 1) + effective_world_size = ( + cfg.world_size + // (cfg.context_parallel_size or 1) + // (cfg.tensor_parallel_size or 1) + ) cfg.batch_size = cfg.batch_size * effective_world_size if not cfg.use_ray: diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 320e59a908..91982137b2 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -457,7 +457,6 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): - 1 ) * cfg.num_epochs - * cfg.tensor_parallel_size ) LOG.debug( f"total_num_tokens: {cfg.total_num_tokens:_}, total_num_steps: {total_num_steps:_}" @@ -496,9 +495,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): LOG.debug(f"data_loader_len: {data_loader_len}") # FIXME: is there a bug here somewhere? the total num steps depends # on the agreed on value for sample_packing_eff_est - total_num_steps = int( - math.floor(data_loader_len * cfg.num_epochs * cfg.tensor_parallel_size) - ) + total_num_steps = int(math.floor(data_loader_len * cfg.num_epochs)) if cfg.dataloader_drop_last: # drop the last batch for each epoch total_num_steps -= int(math.ceil(cfg.num_epochs)) @@ -519,12 +516,7 @@ def calc_sample_packing_eff_est(estimates: List[float]): LOG.debug(f"sample_packing_eff_est: {cfg.sample_packing_eff_est}") else: total_num_steps = int( - math.ceil( - len(train_dataset) - * cfg.num_epochs - * cfg.tensor_parallel_size - / cfg.batch_size - ) + math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) ) LOG.debug(f"total_num_steps: {total_num_steps}") return total_num_steps diff --git a/tests/test_tensor_parallel_batch_size.py b/tests/test_tensor_parallel_batch_size.py new file mode 100644 index 0000000000..f0b27a8ebe --- /dev/null +++ b/tests/test_tensor_parallel_batch_size.py @@ -0,0 +1,54 @@ +"""Tests for batch_size calculation with tensor parallelism.""" + +from unittest.mock import patch + +import addict +import pytest +from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.dict import DictDefault + + +@pytest.fixture(name="tp_base_cfg") +def fixture_tp_base_cfg(min_base_cfg): + return ( + DictDefault( + micro_batch_size=2, + gradient_accumulation_steps=4, + sequence_len=2048, + num_epochs=1, + ) + | min_base_cfg + ) + + +class TestTensorParallelBatchSize: + """Verify batch_size scales by effective dp world_size when using tensor parallelism.""" + + @pytest.mark.parametrize( + "world_size, tensor_parallel_size, expected_batch_size", + [ + (4, 1, 32), # no TP: 2*4*4 = 32 + (4, 2, 16), # TP=2: 2*4*(4//2) = 16 + (4, 4, 8), # TP=4: 2*4*(4//4) = 8 + (2, 2, 8), # TP=ws: 2*4*(2//2) = 8 (no scaling) + ], + ) + def test_batch_size_with_tensor_parallelism( + self, + tp_base_cfg, + monkeypatch, + world_size, + tensor_parallel_size, + expected_batch_size, + ): + monkeypatch.setenv("WORLD_SIZE", str(world_size)) + tp_base_cfg["tensor_parallel_size"] = tensor_parallel_size + cfg = validate_config(tp_base_cfg) + # Mock load_model_config to avoid downloading the model and to bypass + # the tie_word_embeddings validation that blocks TP > 1. + with patch( + "axolotl.utils.config.load_model_config", + return_value=addict.Dict({"model_type": "llama"}), + ): + normalize_config(cfg) + assert cfg.batch_size == expected_batch_size From a260d330ed216efbe5b63cfd4bdbe2aa6135f852 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 6 Mar 2026 09:18:38 -0500 Subject: [PATCH 1166/1405] add info about linting that was removed at some point (#3458) [skip ci] --- .github/CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fcfd968916..7d9796345a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -70,6 +70,11 @@ You can skip certain CI checks by including specific keywords in your commit mes axolotl uses [{codestyle}]({URLofCodestyle}) as its code style guide. Please ensure that your code follows these guidelines. +Use the pre-commit linter to ensure that your code is formatted consistently. +```bash +pre-commit run --all-files +``` + ### Commit Messages Write clear and concise commit messages that briefly describe the changes made in each commit. Use the imperative mood and start with a capitalized verb, e.g., "Add new feature" or "Fix bug in function". From 6c8c73e5a4635f9326774b72f1551e9e4e5f8474 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 6 Mar 2026 21:19:05 +0700 Subject: [PATCH 1167/1405] fix(validation): add validation for lora target linear with quantize experts (#3461) * fix: add validation for lora target linear with quantize experts * chore: fix lint * chore: comment * fix: missing link on readme --- README.md | 2 +- docs/expert_quantization.qmd | 1 + src/axolotl/utils/schemas/config.py | 5 +++++ tests/utils/schemas/validation/test_moe_quant.py | 14 ++++++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c7a8a4931..594b06156e 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ ## 🎉 Latest Updates - 2026/03: - - New model support has been added in Axolotl for Qwen3.5, Qwen3.5 MoE, [GLM-4.7-Flash](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm47-flash), [GLM-4.6V](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm46v), and [GLM-4.5-Air](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm45). + - New model support has been added in Axolotl for [Qwen3.5, Qwen3.5 MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3.5), [GLM-4.7-Flash](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm47-flash), [GLM-4.6V](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm46v), and [GLM-4.5-Air](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm45). - [MoE expert quantization](https://docs.axolotl.ai/docs/expert_quantization.html) support (via `quantize_moe_experts: true`) greatly reduces VRAM when training MoE models (FSDP2 compat). - 2026/02: - [ScatterMoE LoRA](https://github.com/axolotl-ai-cloud/axolotl/pull/3410) support. LoRA fine-tuning directly on MoE expert weights using custom Triton kernels. diff --git a/docs/expert_quantization.qmd b/docs/expert_quantization.qmd index 7271e88643..7eabed1cfe 100644 --- a/docs/expert_quantization.qmd +++ b/docs/expert_quantization.qmd @@ -45,6 +45,7 @@ lora_target_parameters: ## Limitations +- `lora_target_linear` is not compatible with `quantize_moe_experts`. See [Expert LoRA targeting](#expert-lora-targeting) instead. - `cpu_ram_efficient_loading` hangs / takes long time with FSDP2 + QLoRA. - Total model parameter count may display incorrectly (trainable param count is correct). - FSDP LoRA (8-bit) may have a large initial VRAM spike at the first 1-2 steps, which then drops. QLoRA does not exhibit this. diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 8d53cec524..5ea340c370 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1302,6 +1302,11 @@ def check_multigpu_lora_kernels(cls, data): @classmethod def check_quantize_moe_experts(cls, data): if data.get("quantize_moe_experts"): + if data.get("lora_target_linear"): + raise ValueError( + "lora_target_linear is not compatible with quantize_moe_experts. " + "Use lora_target_parameters to target expert weights instead." + ) if data.get("adapter") not in ("lora", "qlora"): raise ValueError("quantize_moe_experts requires adapter: lora or qlora") if not (data.get("load_in_4bit") or data.get("load_in_8bit")): diff --git a/tests/utils/schemas/validation/test_moe_quant.py b/tests/utils/schemas/validation/test_moe_quant.py index b969cbb680..a2121473a4 100644 --- a/tests/utils/schemas/validation/test_moe_quant.py +++ b/tests/utils/schemas/validation/test_moe_quant.py @@ -79,6 +79,20 @@ def test_false_skips_validation(self, min_base_cfg, gpu_caps, env_caps): result = validate_config(cfg, capabilities=gpu_caps, env_capabilities=env_caps) assert result["quantize_moe_experts"] is False + def test_rejects_lora_target_linear(self, min_base_cfg, gpu_caps, env_caps): + """quantize_moe_experts with lora_target_linear should fail.""" + cfg = ( + DictDefault( + quantize_moe_experts=True, + adapter="qlora", + load_in_4bit=True, + lora_target_linear=True, + ) + | min_base_cfg + ) + with pytest.raises(ValueError, match="lora_target_linear is not compatible"): + validate_config(cfg, capabilities=gpu_caps, env_capabilities=env_caps) + def test_default_is_false(self, min_base_cfg, gpu_caps, env_caps): """quantize_moe_experts should default to false.""" cfg = DictDefault({}) | min_base_cfg From c119382337645796dbc8e82e2b212f14a58e69bd Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:01:00 +0530 Subject: [PATCH 1168/1405] add: qwen 3.5 (#3442) * add: qwen 3.5 * test for qwen , patch * lint * qwen3 fix on main * Apply suggestions from code review Co-authored-by: NanoCode012 * moe config * config moe * configs and chore * Update examples/qwen3.5/122b-a10b-moe-qlora.yaml Co-authored-by: NanoCode012 * Update examples/qwen3.5/35b-a3b-moe-qlora.yaml Co-authored-by: NanoCode012 * chore for qwen + vlm patch * chore lint * qwen lint * 3_5_moe * Update examples/qwen3.5/README.md --------- Co-authored-by: NanoCode012 --- examples/qwen3.5/122b-a10b-moe-qlora.yaml | 71 +++++ examples/qwen3.5/27b-qlora.yaml | 72 +++++ examples/qwen3.5/35b-a3b-moe-qlora.yaml | 70 +++++ examples/qwen3.5/7b-lora-vision.yaml | 72 +++++ examples/qwen3.5/README.md | 61 ++++ src/axolotl/common/architectures.py | 1 + src/axolotl/loaders/patch_manager.py | 25 ++ .../monkeypatch/models/qwen3_5/__init__.py | 0 .../monkeypatch/models/qwen3_5/modeling.py | 291 ++++++++++++++++++ src/axolotl/monkeypatch/multipack.py | 2 + src/axolotl/processing_strategies.py | 30 ++ .../chat_templates/templates/qwen3_5.jinja | 123 ++++++++ src/axolotl/utils/schemas/enums.py | 1 + 13 files changed, 819 insertions(+) create mode 100644 examples/qwen3.5/122b-a10b-moe-qlora.yaml create mode 100644 examples/qwen3.5/27b-qlora.yaml create mode 100644 examples/qwen3.5/35b-a3b-moe-qlora.yaml create mode 100644 examples/qwen3.5/7b-lora-vision.yaml create mode 100644 examples/qwen3.5/README.md create mode 100644 src/axolotl/monkeypatch/models/qwen3_5/__init__.py create mode 100644 src/axolotl/monkeypatch/models/qwen3_5/modeling.py create mode 100644 src/axolotl/utils/chat_templates/templates/qwen3_5.jinja diff --git a/examples/qwen3.5/122b-a10b-moe-qlora.yaml b/examples/qwen3.5/122b-a10b-moe-qlora.yaml new file mode 100644 index 0000000000..e9cbf80cea --- /dev/null +++ b/examples/qwen3.5/122b-a10b-moe-qlora.yaml @@ -0,0 +1,71 @@ +base_model: Qwen/Qwen3.5-122B-A10B + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +strict: false + +chat_template: qwen3_5 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.0 +output_dir: ./outputs/out +dataset_prepared_path: last_run_prepared + +sequence_len: 2048 +sample_packing: true + +load_in_4bit: true +quantize_moe_experts: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + +#lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + + +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/qwen3.5/27b-qlora.yaml b/examples/qwen3.5/27b-qlora.yaml new file mode 100644 index 0000000000..2ba1c4ed73 --- /dev/null +++ b/examples/qwen3.5/27b-qlora.yaml @@ -0,0 +1,72 @@ +base_model: Qwen/Qwen3.5-27B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name +# Note: Qwen3.5 is an early-fusion VLM (image+text). This config fine-tunes +# the text-only path. For multimodal (image+text) fine-tuning, add image +# columns to your dataset following axolotl's multimodal dataset format. + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +strict: false + +chat_template: qwen3_5 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.0 +output_dir: ./outputs/out +dataset_prepared_path: last_run_prepared + +sequence_len: 2048 +sample_packing: true + +load_in_4bit: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - down_proj + - up_proj + # Uncomment below to also target the linear attention projections. + # These use separate in_proj_qkv / in_proj_z / out_proj (Qwen3.5-specific). + # - linear_attn.in_proj_qkv + # - linear_attn.in_proj_z + # - linear_attn.out_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/qwen3.5/35b-a3b-moe-qlora.yaml b/examples/qwen3.5/35b-a3b-moe-qlora.yaml new file mode 100644 index 0000000000..462babf0bb --- /dev/null +++ b/examples/qwen3.5/35b-a3b-moe-qlora.yaml @@ -0,0 +1,70 @@ +base_model: Qwen/Qwen3.5-35B-A3B + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +strict: false + +chat_template: qwen3_5 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.0 +output_dir: ./outputs/out +dataset_prepared_path: last_run_prepared + +sequence_len: 2048 +sample_packing: true + +load_in_4bit: true +quantize_moe_experts: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + +#lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/qwen3.5/7b-lora-vision.yaml b/examples/qwen3.5/7b-lora-vision.yaml new file mode 100644 index 0000000000..79179ec96b --- /dev/null +++ b/examples/qwen3.5/7b-lora-vision.yaml @@ -0,0 +1,72 @@ +base_model: Qwen/Qwen3.5-7B +processor_type: AutoProcessor + +# Qwen3.5-7B and above are early-fusion VLMs (Qwen3_5ForConditionalGeneration). +# Vision and text tokens are processed together by the same transformer layers. +# Note: Qwen3.5-2B is a text-only model — the smallest VLM is Qwen3.5-7B. + +# These 3 lines are required for vision/multimodal training +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +chat_template: qwen3_5 +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: lora +lora_model_dir: + +sequence_len: 8192 +pad_to_sequence_len: false + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +# Targets the language model attention and MLP layers. +# Qwen3.5 is early-fusion: all layers (including those seeing vision tokens) share +# the same transformer stack, so standard attention targets work for both modalities. +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - down_proj + - up_proj + # Uncomment to also target the linear attention (GatedDeltaNet) projections: + # - linear_attn.in_proj_qkv + # - linear_attn.in_proj_z + # - linear_attn.out_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: true +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 diff --git a/examples/qwen3.5/README.md b/examples/qwen3.5/README.md new file mode 100644 index 0000000000..8a2f9b4bd8 --- /dev/null +++ b/examples/qwen3.5/README.md @@ -0,0 +1,61 @@ +# Finetune Qwen3.5 with Axolotl + +[Qwen3.5](https://huggingface.co/collections/Qwen/qwen35-68452f3bc6e4b7cfb4e1c803) is a hybrid architecture model series combining Gated DeltaNet linear attention with standard Transformer attention. Models from 7B onwards are early-fusion vision-language models (`Qwen3_5ForConditionalGeneration`), meaning vision and text tokens are processed through the same transformer stack. The 2B variant is text-only. + +Available configs: + +| Config | Model | Type | +|---|---|---| +| `27b-qlora.yaml` | Qwen3.5-27B | Dense VLM, text-only path | +| `35b-a3b-moe-qlora.yaml` | Qwen3.5-35B-A3B | MoE, text-only path | +| `122b-a10b-moe-qlora.yaml` | Qwen3.5-122B-A10B | MoE, text-only path | +| `7b-lora-vision.yaml` | Qwen3.5-7B | Vision+text (multimodal) | + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +3. Install FLA for sample packing support with the Gated DeltaNet linear attention layers: +```bash +pip3 uninstall -y causal-conv1d && pip3 install flash-linear-attention==0.4.1 +``` +> FLA is required when `sample_packing: true`. Without it, training raises a `RuntimeError` on packed sequences. Vision configs use `sample_packing: false` so FLA is optional there. + +4. Run a finetuning example: + +```bash +# Dense 27B text-only (QLoRA, ~47 GiB VRAM with sample packing) +axolotl train examples/qwen3.5/27b-qlora.yaml + +# MoE 35B-A3B text-only (QLoRA) +axolotl train examples/qwen3.5/35b-a3b-moe-qlora.yaml + +# MoE 122B-A10B text-only (QLoRA) +axolotl train examples/qwen3.5/122b-a10b-moe-qlora.yaml + +# 7B vision+text (LoRA, multimodal dataset) +axolotl train examples/qwen3.5/7b-lora-vision.yaml +``` + +### TIPS + +- For inference, you can experiment with `temperature: 0.7`, `top_p: 0.8`, `top_k: 20`, and `min_p: 0`. +- You can run a full finetuning by removing `adapter: qlora` and `load_in_4bit: true`. See [Multi-GPU](#optimization-guides) below. +- Read more on loading your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). +- For **multimodal** finetuning, set `processor_type: AutoProcessor`, `skip_prepare_dataset: true`, and `remove_unused_columns: false` as shown in `7b-lora-vision.yaml`. +- The Gated DeltaNet linear attention layers (`linear_attn.*`) can optionally be added to `lora_target_modules` — they are commented out by default. + +## Optimization Guides + +- [Optimizations Guide](https://docs.axolotl.ai/docs/optimizations.html) + +## Related Resources + +- [Qwen3.5 Blog](https://qwenlm.github.io/blog/qwen3.5/) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/src/axolotl/common/architectures.py b/src/axolotl/common/architectures.py index a409ed9f4e..0e1de30179 100644 --- a/src/axolotl/common/architectures.py +++ b/src/axolotl/common/architectures.py @@ -12,6 +12,7 @@ "mixtral": "MixtralSparseMoeBlock", "qwen2_moe": "Qwen2MoeSparseMoeBlock", "qwen3_moe": "Qwen3MoeSparseMoeBlock", + "qwen3_5_moe": "Qwen3_5MoeSparseMoeBlock", "qwen3_vl_moe": "Qwen3VLMoeTextSparseMoeBlock", "deepseek_v2": "DeepseekV2MoE", "deepseek_v3": "DeepseekV3MoE", diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 51bcaeba44..f94626ec7e 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -246,6 +246,31 @@ def _apply_model_specific_patches(self): patch_qwen3_next_modeling_packing() + if self.cfg.model_config_type == "qwen3_5" and self.cfg.sample_packing: + from axolotl.monkeypatch.models.qwen3_5.modeling import ( + patch_qwen3_5_modeling_packing, + ) + + patch_qwen3_5_modeling_packing() + + if self.cfg.model_config_type == "qwen3_5_moe" and self.cfg.sample_packing: + from axolotl.monkeypatch.models.qwen3_5.modeling import ( + patch_qwen3_5_moe_modeling_packing, + ) + + patch_qwen3_5_moe_modeling_packing() + + if ( + self.cfg.model_config_type in ["qwen3_5", "qwen3_5_moe"] + and self.cfg.is_multimodal + and self.cfg.flash_attention + ): + from axolotl.monkeypatch.models.qwen3_5.modeling import ( + patch_qwen3_5_vlm_flash_attention, + ) + + patch_qwen3_5_vlm_flash_attention() + if self.cfg.model_config_type == "kimi_linear": from axolotl.monkeypatch.models.kimi_linear.patch_kimi_linear import ( patch_kimi_model, diff --git a/src/axolotl/monkeypatch/models/qwen3_5/__init__.py b/src/axolotl/monkeypatch/models/qwen3_5/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/qwen3_5/modeling.py b/src/axolotl/monkeypatch/models/qwen3_5/modeling.py new file mode 100644 index 0000000000..f88f605557 --- /dev/null +++ b/src/axolotl/monkeypatch/models/qwen3_5/modeling.py @@ -0,0 +1,291 @@ +"""Monkeypatch for Qwen3_5 and Qwen3_5Moe models to pass position_ids to linear attention.""" + +import importlib +from typing import Optional, Tuple + +import torch +import torch.nn.functional as F + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +try: + from fla.modules.convolution import ( + causal_conv1d as fla_causal_conv1d, # FLA >= 0.4.1 + ) +except ImportError: + try: + from fla.modules.conv import causal_conv1d as fla_causal_conv1d # FLA < 0.4.1 + except ImportError: + fla_causal_conv1d = None + + +def get_cu_seqlens(position_ids): + """ + Compute cumulative sequence lengths from position_ids for FLA varlen kernels. + + Adapted from transformers.modeling_flash_attention_utils.prepare_fa_kwargs_from_position_ids. + https://github.com/huggingface/transformers/blob/0f1b128d3359a26bd18be99c26d7f04fb3cba914/src/transformers/modeling_flash_attention_utils.py#L316 + + Qwen3.5 uses MRoPE: position_ids arrive as [axes, B, T]. All axes carry the + same temporal positions, so axis 0 is used to recover the [B, T] layout. + See: https://github.com/huggingface/transformers/blob/main/src/transformers/models/qwen3_5/modeling_qwen3_5.py + """ + if position_ids.ndim == 3: + position_ids = position_ids[0] + + tensor_kwargs = {"dtype": torch.long, "device": position_ids.device} + position_ids = position_ids.reshape(-1) + indices_q = (position_ids == 0).nonzero().reshape(-1) + return torch.cat( + ( + indices_q.to(**tensor_kwargs), + torch.tensor(position_ids.size(), **tensor_kwargs), + ) + ) + + +def _inject_fla_kernels(module) -> None: + """Inject FLA kernels into a modeling module, bypassing is_flash_linear_attention_available.""" + try: + from fla.modules import FusedRMSNormGated + from fla.ops.gated_delta_rule import ( + chunk_gated_delta_rule, + fused_recurrent_gated_delta_rule, + ) + + module.FusedRMSNormGated = FusedRMSNormGated + module.chunk_gated_delta_rule = chunk_gated_delta_rule + module.fused_recurrent_gated_delta_rule = fused_recurrent_gated_delta_rule + module.is_fast_path_available = True + except ImportError: + module.chunk_gated_delta_rule = None + module.fused_recurrent_gated_delta_rule = None + module.FusedRMSNormGated = None + + +def _patched_decoder_forward( + self, + hidden_states: torch.Tensor, + position_embeddings: Tuple[torch.Tensor, torch.Tensor], + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values=None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs, +) -> torch.FloatTensor: + """Decoder layer forward that passes position_ids through to linear attention.""" + residual = hidden_states + hidden_states = self.input_layernorm(hidden_states) + + if self.layer_type == "linear_attention": + hidden_states = self.linear_attn( + hidden_states=hidden_states, + cache_params=past_key_values, + cache_position=cache_position, + attention_mask=attention_mask, + position_ids=position_ids, + ) + elif self.layer_type == "full_attention": + hidden_states, _ = self.self_attn( + hidden_states=hidden_states, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + cache_position=cache_position, + position_embeddings=position_embeddings, + **kwargs, + ) + + hidden_states = residual + hidden_states + + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + hidden_states = self.mlp(hidden_states) + if isinstance(hidden_states, tuple): # MoE returns (hidden_states, router_logits) + hidden_states, _ = hidden_states + hidden_states = residual + hidden_states + + return hidden_states + + +def _make_qwen3_5_gated_delta_forward(apply_mask_fn): + """Factory for patched Qwen3_5/Qwen3_5Moe GatedDeltaNet forward with packing support.""" + + def patched_forward( + self, + hidden_states: torch.Tensor, + cache_params=None, + cache_position: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + ): + hidden_states = apply_mask_fn(hidden_states, attention_mask) + + batch_size, seq_len, _ = hidden_states.shape + + use_precomputed_states = ( + cache_params is not None + and cache_params.has_previous_state + and seq_len == 1 + and cache_position is not None + ) + + cu_seqlens = None + if not use_precomputed_states and position_ids is not None: + cu_seqlens = get_cu_seqlens(position_ids=position_ids) + + if cache_params is not None: + conv_state = cache_params.conv_states[self.layer_idx] + recurrent_state = cache_params.recurrent_states[self.layer_idx] + + # mixed_qkv stays [B, T, D]; only transposed inside paths that require [B, D, T] + mixed_qkv = self.in_proj_qkv(hidden_states) # [B, T, D] + + z = self.in_proj_z(hidden_states) + z = z.reshape(batch_size, seq_len, -1, self.head_v_dim) + + b = self.in_proj_b(hidden_states) + a = self.in_proj_a(hidden_states) + + if use_precomputed_states: + mixed_qkv = self.causal_conv1d_update( + mixed_qkv.transpose(1, 2), + conv_state, + self.conv1d.weight.squeeze(1), + self.conv1d.bias, + self.activation, + ).transpose(1, 2) + else: + if cache_params is not None: + mixed_qkv_t = mixed_qkv.transpose(1, 2) + cache_params.conv_states[self.layer_idx] = F.pad( + mixed_qkv_t, + (self.conv_kernel_size - mixed_qkv_t.shape[-1], 0), + ) + + if fla_causal_conv1d is not None and cu_seqlens is not None: + # FLA varlen kernel for packed sequences; input must be contiguous [B, T, D] + mixed_qkv, _ = fla_causal_conv1d( + x=mixed_qkv, + weight=self.conv1d.weight.squeeze(1), + bias=self.conv1d.bias, + activation=self.activation, + cu_seqlens=cu_seqlens, + ) + else: + if cu_seqlens is not None and fla_causal_conv1d is None: + raise RuntimeError( + "Packed sequences require fla.modules.convolution.causal_conv1d " + "(cu_seqlens support). Install flash-linear-attention or disable packing." + ) + mixed_qkv = F.silu( + self.conv1d(mixed_qkv.transpose(1, 2))[:, :, :seq_len] + ).transpose(1, 2) + + query, key, value = torch.split( + mixed_qkv, + [self.key_dim, self.key_dim, self.value_dim], + dim=-1, + ) + query = query.reshape(batch_size, seq_len, -1, self.head_k_dim) + key = key.reshape(batch_size, seq_len, -1, self.head_k_dim) + value = value.reshape(batch_size, seq_len, -1, self.head_v_dim) + + beta = b.sigmoid() + g = -self.A_log.float().exp() * F.softplus(a.float() + self.dt_bias) + if self.num_v_heads // self.num_k_heads > 1: + query = query.repeat_interleave(self.num_v_heads // self.num_k_heads, dim=2) + key = key.repeat_interleave(self.num_v_heads // self.num_k_heads, dim=2) + + if not use_precomputed_states: + core_attn_out, last_recurrent_state = self.chunk_gated_delta_rule( + query, + key, + value, + g=g.to(dtype=query.dtype), + beta=beta, + initial_state=None, + output_final_state=cache_params is not None, + use_qk_l2norm_in_kernel=True, + # torch_chunk_gated_delta_rule fallback does not accept cu_seqlens + **({"cu_seqlens": cu_seqlens} if cu_seqlens is not None else {}), + ) + else: + core_attn_out, last_recurrent_state = self.recurrent_gated_delta_rule( + query, + key, + value, + g=g.to(dtype=query.dtype), + beta=beta, + initial_state=recurrent_state, + output_final_state=cache_params is not None, + use_qk_l2norm_in_kernel=True, + ) + + if cache_params is not None: + cache_params.recurrent_states[self.layer_idx] = last_recurrent_state + + core_attn_out = core_attn_out.reshape(-1, self.head_v_dim) + z = z.reshape(-1, self.head_v_dim) + core_attn_out = self.norm(core_attn_out, z) + core_attn_out = core_attn_out.reshape(batch_size, seq_len, -1) + + return self.out_proj(core_attn_out) + + return patched_forward + + +def _apply_packing_patches(model_type: str, cls_prefix: str, forward_factory) -> None: + module_name = f"transformers.models.{model_type}.modeling_{model_type}" + + try: + module = importlib.import_module(module_name) + except ImportError: + LOG.warning(f"{model_type} not found in transformers, skipping packing patches") + return + + _inject_fla_kernels(module) + getattr(module, f"{cls_prefix}DecoderLayer").forward = _patched_decoder_forward + gated_cls = getattr(module, f"{cls_prefix}GatedDeltaNet") + gated_cls.forward = forward_factory(module.apply_mask_to_padding_states) + + LOG.info( + f"Applied {cls_prefix} packing patch " + f"(fla_causal_conv1d={'available' if fla_causal_conv1d else 'unavailable'})" + ) + + +def patch_qwen3_5_modeling_packing(): + _apply_packing_patches("qwen3_5", "Qwen3_5", _make_qwen3_5_gated_delta_forward) + + +def patch_qwen3_5_moe_modeling_packing(): + _apply_packing_patches( + "qwen3_5_moe", "Qwen3_5Moe", _make_qwen3_5_gated_delta_forward + ) + + +def patch_qwen3_5_vlm_flash_attention(): + """ + Patch _is_packed_sequence to handle Qwen3.5's 3-D MRoPE position_ids. + + transformers passes position_ids as [axes, B, T] to decoder layers, but + _is_packed_sequence only handles 2-D tensors and mis-classifies the 3-D + shape as a packed-sequence indicator, causing CUDA errors in the varlen path. + """ + try: + import transformers.modeling_flash_attention_utils as fa_utils + + _original = fa_utils._is_packed_sequence + + def _patched(position_ids, batch_size): + if position_ids is not None and position_ids.ndim != 2: + return False + return _original(position_ids, batch_size) + + fa_utils._is_packed_sequence = _patched + LOG.info("Applied Qwen3.5 VLM flash-attention patch (3-D MRoPE position_ids)") + except Exception as exc: # pragma: no cover + LOG.warning(f"Failed to apply Qwen3.5 VLM flash-attention patch: {exc}") diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 3208325ebe..cad6039bdd 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -22,6 +22,8 @@ "qwen3", "qwen3_moe", "qwen3_next", + "qwen3_5", + "qwen3_5_moe", "falcon", "phi", "phi3", diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index c8b153e6d8..cb1f9d984b 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -258,6 +258,32 @@ def __init__( ) +class Qwen3_5ProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for Qwen3.5 (early-fusion VLM)""" + + def __init__( + self, + processor: ProcessorMixin, + chat_template: Optional[str] = None, + image_size: int | tuple[int, int] | None = None, + image_resize_algorithm: Resampling | None = None, + ): + super().__init__(processor, chat_template, image_size, image_resize_algorithm) + self.image_token = "<|image_pad|>" # nosec + self.image_token_id = processor.tokenizer.convert_tokens_to_ids( + self.image_token + ) + self.video_token = "<|video_pad|>" # nosec + self.video_token_id = processor.tokenizer.convert_tokens_to_ids( + self.video_token + ) + + def process_labels(self, input_ids): + labels = super().process_labels(input_ids) + labels[labels == self.video_token_id] = -100 + return labels + + class Gemma3ProcessingStrategy(ProcessingStrategy): """Processing Strategy class for Gemma3""" @@ -562,6 +588,10 @@ def get_processing_strategy( return Qwen2VLProcessingStrategy( **processing_kwargs, ) + if chat_template_type in ["qwen3_5", "qwen3_5_moe"]: + return Qwen3_5ProcessingStrategy( + **processing_kwargs, + ) if chat_template_type == "gemma3": return Gemma3ProcessingStrategy( **processing_kwargs, diff --git a/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja b/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja new file mode 100644 index 0000000000..21f5733ed9 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja @@ -0,0 +1,123 @@ +{%- if tools %} + {{- '<|im_start|>system\n' }} + {%- if messages[0].role == 'system' %} + {{- messages[0].content + '\n\n' }} + {%- endif %} + {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n" }} + {%- for tool in tools %} + {{- "\n" }} + {{- tool | tojson }} + {%- endfor %} + {{- "\n\n\nFor each function call, return a json object with function name and arguments within XML tags:\n\n{\"name\": , \"arguments\": }\n<|im_end|>\n" }} +{%- else %} + {%- if messages[0].role == 'system' %} + {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }} + {%- endif %} +{%- endif %} +{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %} +{#- Determine the real last index: use provided value or default to messages length - 1 #} +{%- if real_last_index is defined and real_last_index is not none %} + {%- set ns.real_last_index = real_last_index %} +{%- else %} + {%- set ns.real_last_index = messages|length - 1 %} +{%- endif %} +{%- for message in messages[::-1] %} + {%- set index = (messages|length - 1) - loop.index0 %} + {%- if message['content'] is string %} + {%- if ns.multi_step_tool and message.role == "user" and not(message.content.startswith('') and message.content.endswith('')) %} + {%- set ns.multi_step_tool = false %} + {%- set ns.last_query_index = index %} + {%- endif %} + {%- else %} + {%- if ns.multi_step_tool and message.role == "user" %} + {%- set ns.multi_step_tool = false %} + {%- set ns.last_query_index = index %} + {%- endif %} + {%- endif %} +{%- endfor %} +{%- for message in messages %} + {%- if (message.role == "user") or (message.role == "system" and not loop.first) %} + {{- '<|im_start|>' + message.role + '\n' }} + {%- if message['content'] is string %} + {{- message.content }} + {%- else %} + {%- for content in message['content'] %} + {%- if content['type'] == 'image' or 'image' in content or 'image_url' in content %} + {{- '<|vision_start|><|image_pad|><|vision_end|>' }} + {%- elif content['type'] == 'video' or 'video' in content %} + {{- '<|vision_start|><|video_pad|><|vision_end|>' }} + {%- elif 'text' in content %} + {{- content['text'] }} + {%- endif %} + {%- endfor %} + {%- endif %} + {{- '<|im_end|>\n' }} + {%- elif message.role == "assistant" %} + {%- if message['content'] is string %} + {%- set content = message.content %} + {%- else %} + {%- set content = '' %} + {%- for item in message['content'] %} + {%- if 'text' in item %} + {%- set content = content + item['text'] %} + {%- endif %} + {%- endfor %} + {%- endif %} + {%- set reasoning_content = '' %} + {%- if message.reasoning_content is defined and message.reasoning_content is not none %} + {%- set reasoning_content = message.reasoning_content %} + {%- else %} + {%- if '' in content %} + {%- set content = content.split('')[-1].lstrip('\n') %} + {%- set reasoning_content = content.split('')[0].rstrip('\n').split('')[-1].lstrip('\n') %} + {%- endif %} + {%- endif %} + {%- if loop.index0 > ns.last_query_index %} + {%- if loop.index0 == ns.real_last_index or (loop.index0 != ns.real_last_index and reasoning_content) %} + {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }} + {%- else %} + {{- '<|im_start|>' + message.role + '\n' + content }} + {%- endif %} + {%- else %} + {{- '<|im_start|>' + message.role + '\n' + content }} + {%- endif %} + {%- if message.tool_calls %} + {%- for tool_call in message.tool_calls %} + {%- if (loop.first and content) or (not loop.first) %} + {{- '\n' }} + {%- endif %} + {%- if tool_call.function %} + {%- set tool_call = tool_call.function %} + {%- endif %} + {{- '\n{"name": "' }} + {{- tool_call.name }} + {{- '", "arguments": ' }} + {%- if tool_call.arguments is string %} + {{- tool_call.arguments }} + {%- else %} + {{- tool_call.arguments | tojson }} + {%- endif %} + {{- '}\n' }} + {%- endfor %} + {%- endif %} + {{- '<|im_end|>\n' }} + {%- elif message.role == "tool" %} + {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %} + {{- '<|im_start|>user' }} + {%- endif %} + {{- '\n\n' }} + {{- message.content }} + {{- '\n' }} + {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %} + {{- '<|im_end|>\n' }} + {%- endif %} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|im_start|>assistant\n' }} + {%- if enable_thinking is defined and enable_thinking is false %} + {{- '\n\n\n\n' }} + {%- else %} + {{- '\n\n' }} + {%- endif %} +{%- endif %} diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 893f232880..792f6f6de5 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -59,6 +59,7 @@ class ChatTemplate(str, Enum): jinja = "jinja" qwen_25 = "qwen_25" qwen3 = "qwen3" + qwen3_5 = "qwen3_5" falcon_h1 = "falcon_h1" tokenizer_default = "tokenizer_default" exaone = "exaone" From fc2d63ee5f7bdb6c56bd0f70888aec75db068db3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 6 Mar 2026 11:40:32 -0500 Subject: [PATCH 1169/1405] use new tf32 APIs for torch 2.9+ (#3467) [skip ci] * use new tf32 APIs for torch 2.9+ * also upgrade cce for tf32 fixes and lint --- .../colab-notebooks/colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 2 +- .../integrations/cut_cross_entropy/__init__.py | 2 +- src/axolotl/utils/config/__init__.py | 16 +++++++++++++--- tests/test_tensor_parallel_batch_size.py | 1 + 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 2cc27f211c..7be9800be0 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@a668583\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@e8ad129\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index f6cd0c4950..d506fa87ee 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@a668583"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@e8ad129"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index b892033da1..9520dd48c5 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@a668583" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@e8ad129" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 5c207e0fc5..d8aa075b95 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@a668583"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@e8ad129"`' ) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 07c4d175f4..e8ca72aa18 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -6,7 +6,10 @@ import torch from transformers.utils import is_torch_bf16_gpu_available -from transformers.utils.import_utils import is_torch_npu_available +from transformers.utils.import_utils import ( + is_torch_greater_or_equal, + is_torch_npu_available, +) from axolotl.integrations.base import PluginManager from axolotl.integrations.config import merge_input_args @@ -81,8 +84,15 @@ def resolve_dtype(cfg): cfg.fp16 = True cfg.bf16 = False else: - torch.backends.cuda.matmul.allow_tf32 = cfg.tf32 or False - torch.backends.cudnn.allow_tf32 = cfg.tf32 or False + if cfg.tf32: + torch.set_float32_matmul_precision("high") + if is_torch_greater_or_equal("2.9.0"): + torch.backends.fp32_precision = "tf32" + torch.backends.cuda.matmul.fp32_precision = "tf32" + torch.backends.cudnn.fp32_precision = "tf32" + else: + torch.backends.cuda.matmul.allow_tf32 = True + torch.backends.cudnn.allow_tf32 = True if cfg.bf16: cfg.fp16 = False diff --git a/tests/test_tensor_parallel_batch_size.py b/tests/test_tensor_parallel_batch_size.py index f0b27a8ebe..c6a8174fc6 100644 --- a/tests/test_tensor_parallel_batch_size.py +++ b/tests/test_tensor_parallel_batch_size.py @@ -4,6 +4,7 @@ import addict import pytest + from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault From 0a23ae08f78a7531f11a8385710248bba8639427 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 6 Mar 2026 23:44:00 +0700 Subject: [PATCH 1170/1405] fix: position_ids casted to int64 for qwen35 patch (#3468) [skip ci] * fix: position_ids casted to int64 for qwen35 patch * fix: to use view instead of reshape to ensure noncontiguous error explicitly * chore: lint --- src/axolotl/monkeypatch/models/qwen3_5/modeling.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/axolotl/monkeypatch/models/qwen3_5/modeling.py b/src/axolotl/monkeypatch/models/qwen3_5/modeling.py index f88f605557..0b3302d82e 100644 --- a/src/axolotl/monkeypatch/models/qwen3_5/modeling.py +++ b/src/axolotl/monkeypatch/models/qwen3_5/modeling.py @@ -35,9 +35,9 @@ def get_cu_seqlens(position_ids): if position_ids.ndim == 3: position_ids = position_ids[0] - tensor_kwargs = {"dtype": torch.long, "device": position_ids.device} - position_ids = position_ids.reshape(-1) - indices_q = (position_ids == 0).nonzero().reshape(-1) + tensor_kwargs = {"dtype": torch.int32, "device": position_ids.device} + position_ids = position_ids.view(-1) + indices_q = (position_ids == 0).nonzero().view(-1) return torch.cat( ( indices_q.to(**tensor_kwargs), From d65e1b960cf88916ca14fa27528149579038ac68 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 6 Mar 2026 23:45:03 +0700 Subject: [PATCH 1171/1405] fix: add guard for _initialize_missing_keys patch (#3469) [skip ci] --- src/axolotl/monkeypatch/accelerate/fsdp2.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index 1fa589f07b..87b537655b 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -498,6 +498,9 @@ def patch_initialize_missing_keys_for_fsdp(): from transformers import PreTrainedModel from transformers.modeling_utils import is_fsdp_enabled, is_local_dist_rank_0 + if getattr(PreTrainedModel._initialize_missing_keys, "_axolotl_patched", False): + return + _original_initialize_missing_keys = PreTrainedModel._initialize_missing_keys def _patched_initialize_missing_keys(self, is_quantized: bool) -> None: @@ -510,6 +513,7 @@ def _patched_initialize_missing_keys(self, is_quantized: bool) -> None: _original_initialize_missing_keys(self, is_quantized) PreTrainedModel._initialize_missing_keys = _patched_initialize_missing_keys + PreTrainedModel._initialize_missing_keys._axolotl_patched = True def patch_accelerate_fsdp2(): From 876941ffd010298f35d531f7f4c99ffe0bb2e61f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 6 Mar 2026 12:40:57 -0500 Subject: [PATCH 1172/1405] install flash-linear-attention (#3466) * install flash-linear-attention * handle prequant weights for fsdp2 and ensure loss is not zero * fix type for cu_seqlen, uninstall causal_conv1d * chore: lint * uv pip uninstall doesn't need confirmation --- cicd/Dockerfile-uv.jinja | 1 + cicd/Dockerfile.jinja | 1 + docker/Dockerfile | 1 + docker/Dockerfile-uv | 1 + requirements.txt | 3 +++ setup.py | 11 +++++++++-- src/axolotl/monkeypatch/accelerate/fsdp2.py | 7 +++++-- tests/e2e/utils.py | 3 +++ 8 files changed, 24 insertions(+), 4 deletions(-) diff --git a/cicd/Dockerfile-uv.jinja b/cicd/Dockerfile-uv.jinja index 9a49cfca57..103f1eb999 100644 --- a/cicd/Dockerfile-uv.jinja +++ b/cicd/Dockerfile-uv.jinja @@ -33,6 +33,7 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ RUN uv pip install packaging==26.0 setuptools==75.8.0 RUN uv pip install torchvision +RUN uv pip uninstall causal_conv1d RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ uv pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 1c397b011d..13d2f4e69a 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -33,6 +33,7 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ fi RUN pip install packaging==26.0 setuptools==75.8.0 psutil +RUN pip uninstall -y causal_conv1d RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ diff --git a/docker/Dockerfile b/docker/Dockerfile index d80cede55b..5840c1f619 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,6 +22,7 @@ RUN git clone --depth=1 https://github.com/axolotl-ai-cloud/axolotl.git WORKDIR /workspace/axolotl # If AXOLOTL_EXTRAS is set, append it in brackets; don't install deepspeed with arm64 +RUN pip uninstall -y causal_conv1d RUN if [ "$TARGETARCH" = "arm64" ]; then \ BASE_EXTRAS="flash-attn,ring-flash-attn,optimizers,ray"; \ else \ diff --git a/docker/Dockerfile-uv b/docker/Dockerfile-uv index aa03f22a81..0142c0d2d3 100644 --- a/docker/Dockerfile-uv +++ b/docker/Dockerfile-uv @@ -22,6 +22,7 @@ RUN git clone --depth=1 https://github.com/axolotl-ai-cloud/axolotl.git WORKDIR /workspace/axolotl # If AXOLOTL_EXTRAS is set, append it in brackets; don't install deepspeed with arm64 +RUN uv pip uninstall causal_conv1d RUN if [ "$TARGETARCH" = "arm64" ]; then \ BASE_EXTRAS="flash-attn,ring-flash-attn,optimizers,ray"; \ else \ diff --git a/requirements.txt b/requirements.txt index 472a98bc87..c918d30aa7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,9 @@ trl==0.29.0 hf_xet==1.3.2 kernels==0.12.2 +fla-core==0.4.1 +flash-linear-attention==0.4.1 + trackio>=0.16.1 typing-extensions>=4.15.0 diff --git a/setup.py b/setup.py index fe00b9e143..5b7b50f29e 100644 --- a/setup.py +++ b/setup.py @@ -27,9 +27,16 @@ def parse_requirements(extras_require_map): xformers_version = [req for req in _install_requires if "xformers" in req][0] install_xformers = platform.machine() != "aarch64" if platform.machine() == "aarch64": - # skip torchao on ARM64 + # skip on ARM64 + skip_packages = [ + "torchao", + "fla-core", + "flash-linear-attention", + ] _install_requires = [ - req for req in _install_requires if "torchao" not in req + req + for req in _install_requires + if re.split(r"[>=<]", req)[0].strip() not in skip_packages ] if "Darwin" in platform.system(): # skip packages not compatible with OSX diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index 87b537655b..cf8056ca04 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -506,8 +506,11 @@ def patch_initialize_missing_keys_for_fsdp(): def _patched_initialize_missing_keys(self, is_quantized: bool) -> None: if is_fsdp_enabled() and not is_local_dist_rank_0(): for key in self.state_dict(): - param_or_buffer = self.get_parameter_or_buffer(key) - param_or_buffer._is_hf_initialized = True + try: + param_or_buffer = self.get_parameter_or_buffer(key) + param_or_buffer._is_hf_initialized = True + except AttributeError: + pass # may happen when handling pre-quantized weights self._is_hf_initialized = True _original_initialize_missing_keys(self, is_quantized) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 842cbf1188..268311295d 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -180,6 +180,7 @@ def check_tensorboard( lt_val: float, assertion_err: str, rtol: float = 0.02, + gt_zero: bool = True, ) -> None: """ helper function to parse and check tensorboard logs @@ -194,6 +195,8 @@ def check_tensorboard( assert df.value.values[-1] < lt_val, assertion_err % df.value.values[-1] else: assert df.value.values[-1] < lt_val, assertion_err + if gt_zero: + assert df.value.values[-1] > 1e-5, "Expected loss to be greater than zero" def check_model_output_exists(temp_dir: str, cfg: DictDefault) -> None: From 8f19169eb07d623f310a8d7dc15f6d8deddb9a91 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 6 Mar 2026 12:55:11 -0500 Subject: [PATCH 1173/1405] tag for v0.15.0 release (#3470) --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index d818190189..a551051694 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.15.0.dev0 +0.15.0 From 46b9f40f2a0bbfa3436c3d513c386888d32a673e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 6 Mar 2026 14:59:00 -0500 Subject: [PATCH 1174/1405] bump dev version to 0.16.0.dev0 (#3472) [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index a551051694..cb27aa17b7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.15.0 +0.16.0.dev0 From 80f7088ad18820f896a72eb4e4e40069b066b7d5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 6 Mar 2026 14:59:25 -0500 Subject: [PATCH 1175/1405] update setuptools so trl can be installed from main for nightlies (#3471) * update setuptools so trl can be installed from main for nightlies * run the nightly in the PR CI on change * use range request, don't use cu129 in CI since it's not supported with AO * run multigpu ci if CCE install script changes --- .github/workflows/multi-gpu-e2e.yml | 15 ++++++++------- .github/workflows/tests-nightly.yml | 9 ++++++--- .github/workflows/tests.yml | 2 +- cicd/Dockerfile-uv.jinja | 2 +- cicd/Dockerfile.jinja | 2 +- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index c1e5c5d75c..6063c24c74 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -8,6 +8,7 @@ on: - 'setup.py' - 'pyproject.toml' - '.github/workflows/multi-gpu-e2e.yml' + - 'scripts/cutcrossentropy_install.py' - 'src/axolotl/core/trainers/mixins/sequence_parallel.py' - 'src/axolotl/utils/distributed.py' workflow_dispatch: @@ -35,13 +36,13 @@ jobs: pytorch: 2.8.0 axolotl_extras: fbgemm-gpu num_gpus: 2 - - cuda: 129 - cuda_version: 12.9.1 - python_version: "3.12" - pytorch: 2.9.1 - axolotl_extras: "fbgemm-gpu" - num_gpus: 2 - dockerfile: "Dockerfile-uv.jinja" +# - cuda: 129 +# cuda_version: 12.9.1 +# python_version: "3.12" +# pytorch: 2.9.1 +# axolotl_extras: "fbgemm-gpu" +# num_gpus: 2 +# dockerfile: "Dockerfile-uv.jinja" - cuda: 130 cuda_version: 13.0.0 python_version: "3.11" diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 45596a2e14..d5a533fbcc 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -3,6 +3,10 @@ on: workflow_dispatch: schedule: - cron: '0 0 * * *' # Runs at 00:00 UTC every day + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - '.github/workflows/tests-nightly.yml' jobs: pre-commit: @@ -27,7 +31,7 @@ jobs: - name: Restore Cache from S3 id: hf-cache-restore-s3 run: | - curl -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst > /dev/null + curl -v -H "Range: bytes=0-1023" -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst > /dev/null pytest: name: PyTest @@ -35,7 +39,6 @@ jobs: needs: [prime-cdn-s3-cache] strategy: fail-fast: false - max-parallel: 2 matrix: python_version: ["3.12"] # TODO include py3.14 once https://github.com/mistralai/mistral-common/pull/194 is merged pytorch_version: ["2.8.0", "2.9.1", "2.10.0"] @@ -60,7 +63,7 @@ jobs: - name: upgrade pip run: | pip3 install --upgrade pip - pip3 install --upgrade packaging==26.0 setuptools==75.8.0 wheel + pip3 install --upgrade packaging==26.0 setuptools==78.1.1 wheel - name: Install PyTorch run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f8c9a37bb8..23e9d39e39 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,7 @@ jobs: - name: Restore Cache from S3 id: hf-cache-restore-s3 run: | - curl -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst > /dev/null + curl -v -H "Range: bytes=0-1023" -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst > /dev/null pytest: name: PyTest diff --git a/cicd/Dockerfile-uv.jinja b/cicd/Dockerfile-uv.jinja index 103f1eb999..29c2e79d51 100644 --- a/cicd/Dockerfile-uv.jinja +++ b/cicd/Dockerfile-uv.jinja @@ -31,7 +31,7 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt; \ fi -RUN uv pip install packaging==26.0 setuptools==75.8.0 +RUN uv pip install packaging==26.0 setuptools==78.1.1 RUN uv pip install torchvision RUN uv pip uninstall causal_conv1d RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 13d2f4e69a..4f0140fc60 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -32,7 +32,7 @@ RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt; \ fi -RUN pip install packaging==26.0 setuptools==75.8.0 psutil +RUN pip install packaging==26.0 setuptools==78.1.1 psutil RUN pip uninstall -y causal_conv1d RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ From a36aaa70cee298d524b310429194df1f8621ff2e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 7 Mar 2026 00:00:48 -0500 Subject: [PATCH 1176/1405] add gpu tests for scattermoe (#3474) [skip ci] --- .../test_scattermoe_lora_kernels.py | 1478 +++++++++++++++++ .../test_scattermoe_lora_olmoe.py | 1255 ++++++++++++++ 2 files changed, 2733 insertions(+) create mode 100644 tests/e2e/integrations/test_scattermoe_lora_kernels.py create mode 100644 tests/e2e/integrations/test_scattermoe_lora_olmoe.py diff --git a/tests/e2e/integrations/test_scattermoe_lora_kernels.py b/tests/e2e/integrations/test_scattermoe_lora_kernels.py new file mode 100644 index 0000000000..d11272c8f8 --- /dev/null +++ b/tests/e2e/integrations/test_scattermoe_lora_kernels.py @@ -0,0 +1,1478 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +Tests for ScatterMoE + LoRA Fused Kernels +========================================== + +Tests verify correctness of: +1. Forward pass: fused kernel matches naive PyTorch reference +2. Backward pass: gradients for LoRA A, B, and input match reference +3. Frozen weights: expert weight gradients are correctly skipped +4. Various configurations: top-k, grouped_in/out, with/without bias +5. Numerical stability: bf16/fp16 outputs within tolerance of fp32 reference + +Test strategy: +- Reference implementation uses pure PyTorch ops (no Triton) +- ScatterMoE routing (flatten_sort_count) is shared between reference and kernel +- Tolerances account for tf32 accumulation in Triton kernels +""" + +import pytest +import torch + +# Skip all tests if CUDA is not available +pytestmark = pytest.mark.skipif( + not torch.cuda.is_available(), + reason="CUDA required for Triton kernels", +) + +_SMOE = "axolotl.integrations.kernels.libs.scattermoe_lora" + + +# ============================================================================= +# Helpers +# ============================================================================= + + +def flatten_sort_count_ref(expert_idxs: torch.Tensor, num_experts: int): + """Reference implementation of routing.""" + with torch.no_grad(): + flat = expert_idxs.flatten() + sorted_expert_idxs, sorted_scattered_idxs = torch.sort(flat) + counts = flat.bincount(minlength=num_experts) + offsets = counts.cumsum(-1) + return sorted_expert_idxs, sorted_scattered_idxs, offsets + + +def reference_parallel_linear_lora( + X, + W, + k, + sorted_expert_idxs, + sorted_scattered_idxs, + lora_A, + lora_B, + scaling, + x_grouped=False, + y_grouped=False, + bias=None, +): + """ + Pure PyTorch reference for: Y[i] = X[i] @ W[e] + scaling * (X[i] @ A[e]^T) @ B[e]^T + b[e] + + Args: + X: [M, K] input (token order) + W: [E, K, N] expert weights + sorted_expert_idxs: [M*k] expert assignments (sorted) + sorted_scattered_idxs: [M*k] original token indices (sorted) + lora_A: [r*E, K] LoRA A weights + lora_B: [N, r*E] LoRA B weights + scaling: LoRA scaling factor + """ + E, K, N = W.shape + R = lora_A.size(0) // E + L = sorted_expert_idxs.size(0) # M * k + + output = torch.zeros(L, N, device=X.device, dtype=X.dtype) + + for i in range(L): + e = sorted_expert_idxs[i].item() + if x_grouped: + x_i = X[i] + else: + token_idx = sorted_scattered_idxs[i].item() // k + x_i = X[token_idx] + + w_e = W[e] # [K, N] + a_e = lora_A[e * R : (e + 1) * R, :] # [r, K] + b_e = lora_B[:, e * R : (e + 1) * R] # [N, r] + + # Y = X @ W + scaling * (X @ A^T) @ B^T + base = x_i @ w_e # [N] + lora = scaling * ((x_i @ a_e.T) @ b_e.T) # [N] + out_i = base + lora + + if bias is not None: + out_i = out_i + bias[e] + + if y_grouped: + output[i] = out_i + else: + output[sorted_scattered_idxs[i]] = out_i + + return output + + +def reference_lora_backward( + grad_out, + X, + W, + lora_A, + lora_B, + scaling, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + k, + E, +): + """ + Pure PyTorch reference for LoRA backward pass on grouped data. + + Returns: + dX: [M*k, K] input gradient (in grouped order) + dA: [r*E, K] LoRA A gradient + dB: [N, r*E] LoRA B gradient + """ + R = lora_A.size(0) // E + + dA = torch.zeros_like(lora_A) + dB = torch.zeros_like(lora_B) + dX = torch.zeros_like(X) + + prev_offset = 0 + for e in range(E): + curr_offset = expert_offsets[e].item() + if curr_offset > prev_offset: + dy_e = grad_out[prev_offset:curr_offset] # [M_e, N] + x_e = X[prev_offset:curr_offset] # [M_e, K] + a_e = lora_A[e * R : (e + 1) * R, :] # [r, K] + b_e = lora_B[:, e * R : (e + 1) * R] # [N, r] + w_e = W[e] # [K, N] + + # Input gradient: dX = dY @ W^T + scaling * (dY @ B) @ A + dx_base = dy_e @ w_e.T # [M_e, K] + dy_b = dy_e @ b_e # [M_e, r] + dx_lora = scaling * (dy_b @ a_e) # [M_e, K] + dX[prev_offset:curr_offset] = dx_base + dx_lora + + # LoRA A gradient: dA = scaling * (dY @ B)^T @ X + xa = x_e @ a_e.T # [M_e, r] + dA[e * R : (e + 1) * R, :] = scaling * (dy_b.T @ x_e) + + # LoRA B gradient: dB = scaling * dY^T @ (X @ A^T) + dB[:, e * R : (e + 1) * R] = scaling * (dy_e.T @ xa) + + prev_offset = curr_offset + + return dX, dA, dB + + +def make_test_data( + M=32, + K=64, + N=128, + E=4, + R=8, + k=2, + dtype=torch.float32, + device="cuda", + seed=42, +): + """Create test data for ScatterMoE + LoRA tests.""" + torch.manual_seed(seed) + + X = torch.randn(M, K, device=device, dtype=dtype) + W = torch.randn(E, K, N, device=device, dtype=dtype) * 0.02 + lora_A = torch.randn(R * E, K, device=device, dtype=dtype) * 0.01 + lora_B = torch.randn(N, R * E, device=device, dtype=dtype) * 0.01 + scaling = 0.5 + + # Generate routing + selected_experts = torch.randint(0, E, (M, k), device=device) + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = flatten_sort_count_ref( + selected_experts, E + ) + + return { + "X": X, + "W": W, + "lora_A": lora_A, + "lora_B": lora_B, + "scaling": scaling, + "k": k, + "E": E, + "R": R, + "sorted_expert_idxs": sorted_expert_idxs, + "sorted_scattered_idxs": sorted_scattered_idxs, + "expert_offsets": expert_offsets, + } + + +# ============================================================================= +# Test: Forward Pass Correctness +# ============================================================================= + + +class TestForwardPass: + """Test forward pass of fused scatter2scatter_lora kernel.""" + + def _run_forward_test( + self, M, K, N, E, R, k, dtype=torch.float32, atol=1e-2, rtol=1e-2 + ): + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k, dtype=dtype) + + # Reference + ref_output = reference_parallel_linear_lora( + data["X"], + data["W"], + data["k"], + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["lora_A"], + data["lora_B"], + data["scaling"], + ) + + # Kernel + kernel_output = lora_ops.scatter2scatter_lora( + X=data["X"], + W=data["W"], + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=data["k"], + lora_A=data["lora_A"], + lora_B=data["lora_B"], + scaling=data["scaling"], + ) + + torch.testing.assert_close(kernel_output, ref_output, atol=atol, rtol=rtol) + + def test_basic(self): + """Basic forward pass with small dimensions.""" + self._run_forward_test(M=16, K=64, N=64, E=4, R=8, k=1) + + def test_topk2(self): + """Forward pass with top-2 routing.""" + self._run_forward_test(M=32, K=64, N=128, E=4, R=8, k=2) + + def test_larger_rank(self): + """Forward pass with larger LoRA rank.""" + self._run_forward_test(M=16, K=128, N=128, E=8, R=32, k=2) + + def test_small_rank(self): + """Forward pass with very small LoRA rank.""" + self._run_forward_test(M=32, K=64, N=64, E=4, R=4, k=1) + + def test_many_experts(self): + """Forward with many experts, fewer tokens per expert.""" + self._run_forward_test(M=64, K=64, N=64, E=16, R=8, k=2) + + def test_non_power_of_2_dims(self): + """Test with dimensions that are not powers of 2.""" + self._run_forward_test(M=17, K=96, N=80, E=6, R=16, k=2, atol=2e-2, rtol=2e-2) + + def test_single_token(self): + """Test with a single token.""" + self._run_forward_test(M=1, K=64, N=64, E=4, R=8, k=1) + + def test_bf16(self): + """Test with bfloat16 precision.""" + self._run_forward_test( + M=32, K=64, N=128, E=4, R=8, k=2, dtype=torch.bfloat16, atol=5e-2, rtol=5e-2 + ) + + def test_fp16(self): + """Test with float16 precision.""" + self._run_forward_test( + M=32, K=64, N=128, E=4, R=8, k=2, dtype=torch.float16, atol=5e-2, rtol=5e-2 + ) + + +class TestForwardGrouped: + """Test forward pass with grouped_in/grouped_out configurations.""" + + def _make_grouped_data(self, M=32, K=64, N=128, E=4, R=8, k=2, dtype=torch.float32): + from importlib import import_module + + base_ops = import_module(f"{_SMOE}.kernels.ops") + + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k, dtype=dtype) + + # Create grouped X + grouped_X = base_ops.group(data["X"], data["sorted_scattered_idxs"], fan_out=k) + data["grouped_X"] = grouped_X + return data + + def test_x_grouped(self): + """Forward with pre-grouped input.""" + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + + data = self._make_grouped_data() + + ref_output = reference_parallel_linear_lora( + data["grouped_X"], + data["W"], + data["k"], + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["lora_A"], + data["lora_B"], + data["scaling"], + x_grouped=True, + ) + + kernel_output = lora_ops.scatter2scatter_lora( + X=data["grouped_X"], + W=data["W"], + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=1, # When x_grouped, fan_out=1 (already expanded) + lora_A=data["lora_A"], + lora_B=data["lora_B"], + scaling=data["scaling"], + x_grouped=True, + ) + + torch.testing.assert_close(kernel_output, ref_output, atol=1e-2, rtol=1e-2) + + def test_y_grouped(self): + """Forward with grouped output.""" + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + + data = make_test_data() + + ref_output = reference_parallel_linear_lora( + data["X"], + data["W"], + data["k"], + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["lora_A"], + data["lora_B"], + data["scaling"], + y_grouped=True, + ) + + kernel_output = lora_ops.scatter2scatter_lora( + X=data["X"], + W=data["W"], + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=data["k"], + lora_A=data["lora_A"], + lora_B=data["lora_B"], + scaling=data["scaling"], + y_grouped=True, + ) + + torch.testing.assert_close(kernel_output, ref_output, atol=1e-2, rtol=1e-2) + + +# ============================================================================= +# Test: Backward Pass Correctness (LoRA Gradients) +# ============================================================================= + + +class TestLoRAGradients: + """Test backward LoRA gradient computation (dA, dB).""" + + def _run_lora_grad_test(self, M, K, N, E, R, k, atol=1e-2, rtol=1e-2): + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + base_ops = import_module(f"{_SMOE}.kernels.ops") + + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k) + + # Group X for backward + grouped_X = base_ops.group(data["X"], data["sorted_scattered_idxs"], fan_out=k) + + # Create fake grad_out in grouped order + grad_out = torch.randn( + data["sorted_expert_idxs"].size(0), + N, + device="cuda", + dtype=torch.float32, + ) + + # Reference + _, ref_dA, ref_dB = reference_lora_backward( + grad_out, + grouped_X, + data["W"], + data["lora_A"], + data["lora_B"], + data["scaling"], + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + k, + E, + ) + + # Kernel + kernel_dA, kernel_dB = lora_ops.group_bwd_lora( + DY=grad_out, + X=grouped_X, + lora_A=data["lora_A"], + lora_B=data["lora_B"], + expert_offsets=data["expert_offsets"], + E=E, + scaling=data["scaling"], + ) + + torch.testing.assert_close(kernel_dA, ref_dA, atol=atol, rtol=rtol) + torch.testing.assert_close(kernel_dB, ref_dB, atol=atol, rtol=rtol) + + def test_basic_lora_grads(self): + self._run_lora_grad_test(M=32, K=64, N=128, E=4, R=8, k=2) + + def test_small_rank(self): + self._run_lora_grad_test(M=16, K=64, N=64, E=4, R=4, k=1) + + def test_larger_rank(self): + self._run_lora_grad_test( + M=16, K=128, N=128, E=8, R=32, k=2, atol=5e-2, rtol=5e-2 + ) + + def test_many_experts(self): + self._run_lora_grad_test(M=64, K=64, N=64, E=16, R=8, k=2) + + def test_single_token_per_expert(self): + """Edge case: roughly 1 token per expert.""" + self._run_lora_grad_test(M=8, K=64, N=64, E=8, R=4, k=1) + + +# ============================================================================= +# Test: Full Autograd (Forward + Backward) via torch.autograd +# ============================================================================= + + +class TestAutograd: + """Test full autograd integration through ScatterMoELoRA.""" + + def test_lora_receives_gradients(self): + """LoRA A and B receive non-zero gradients; frozen W does not.""" + from importlib import import_module + + pll = import_module(f"{_SMOE}.parallel_linear_lora") + + M, K, N, E, R, k = 16, 64, 64, 4, 8, 2 + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k) + + X = data["X"].clone().requires_grad_(True) + W = data["W"].clone().requires_grad_(False) # Frozen + lora_A = data["lora_A"].clone().requires_grad_(True) + lora_B = data["lora_B"].clone().requires_grad_(True) + + output = pll.ScatterMoELoRA.apply( + X, + W, + k, + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + lora_A, + lora_B, + data["scaling"], + None, + None, + False, + False, + ) + + loss = output.sum() + loss.backward() + + # LoRA params should have gradients + assert lora_A.grad is not None, "lora_A should have gradient" + assert lora_B.grad is not None, "lora_B should have gradient" + assert lora_A.grad.abs().sum() > 0, "lora_A gradient should be non-zero" + assert lora_B.grad.abs().sum() > 0, "lora_B gradient should be non-zero" + + # Input should have gradient (needed for upstream backprop) + assert X.grad is not None, "X should have gradient" + assert X.grad.abs().sum() > 0, "X gradient should be non-zero" + + def test_input_gradient_matches_reference(self): + """Input gradient from autograd matches pure PyTorch reference.""" + from importlib import import_module + + pll = import_module(f"{_SMOE}.parallel_linear_lora") + base_ops = import_module(f"{_SMOE}.kernels.ops") + + M, K, N, E, R, k = 16, 64, 64, 4, 8, 1 + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k) + + # Autograd path + X_kern = data["X"].clone().requires_grad_(True) + lora_A_kern = data["lora_A"].clone().requires_grad_(True) + lora_B_kern = data["lora_B"].clone().requires_grad_(True) + + out_kern = pll.ScatterMoELoRA.apply( + X_kern, + data["W"], + k, + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + lora_A_kern, + lora_B_kern, + data["scaling"], + None, + None, + False, + False, + ) + grad_out = torch.randn_like(out_kern) + out_kern.backward(grad_out) + + # Reference path + grouped_X = base_ops.group(data["X"], data["sorted_scattered_idxs"], fan_out=k) + grouped_grad = base_ops.group( + grad_out, data["sorted_scattered_idxs"], fan_out=1 + ) + + ref_dX, ref_dA, ref_dB = reference_lora_backward( + grouped_grad, + grouped_X, + data["W"], + data["lora_A"], + data["lora_B"], + data["scaling"], + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + k, + E, + ) + + # Compare input gradient (for k=1, no reduction needed) + # ref_dX is in grouped (expert-sorted) order; X_kern.grad is in original order. + # Ungroup ref_dX by scattering back to original positions. + ref_dX_ungrouped = torch.zeros_like(ref_dX) + ref_dX_ungrouped[data["sorted_scattered_idxs"]] = ref_dX + torch.testing.assert_close(X_kern.grad, ref_dX_ungrouped, atol=5e-2, rtol=5e-2) + + def test_lora_gradient_matches_reference(self): + """LoRA A/B gradients from autograd match reference.""" + from importlib import import_module + + pll = import_module(f"{_SMOE}.parallel_linear_lora") + base_ops = import_module(f"{_SMOE}.kernels.ops") + + M, K, N, E, R, k = 16, 64, 64, 4, 8, 1 + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k) + + # Autograd path + X_kern = data["X"].clone().requires_grad_(True) + lora_A_kern = data["lora_A"].clone().requires_grad_(True) + lora_B_kern = data["lora_B"].clone().requires_grad_(True) + + out_kern = pll.ScatterMoELoRA.apply( + X_kern, + data["W"], + k, + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + lora_A_kern, + lora_B_kern, + data["scaling"], + None, + None, + False, + False, + ) + grad_out = torch.randn_like(out_kern) + out_kern.backward(grad_out) + + # Reference path + grouped_X = base_ops.group(data["X"], data["sorted_scattered_idxs"], fan_out=k) + grouped_grad = base_ops.group( + grad_out, data["sorted_scattered_idxs"], fan_out=1 + ) + + _, ref_dA, ref_dB = reference_lora_backward( + grouped_grad, + grouped_X, + data["W"], + data["lora_A"], + data["lora_B"], + data["scaling"], + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + k, + E, + ) + + torch.testing.assert_close(lora_A_kern.grad, ref_dA, atol=5e-2, rtol=5e-2) + torch.testing.assert_close(lora_B_kern.grad, ref_dB, atol=5e-2, rtol=5e-2) + + +# ============================================================================= +# Test: Equivalence with Base ScatterMoE (scaling=0 should match base) +# ============================================================================= + + +class TestBaseEquivalence: + """When scaling=0, fused kernel should match base scatter2scatter.""" + + def test_zero_scaling_matches_base(self): + """With scaling=0, LoRA contribution vanishes; should match base.""" + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + base_ops = import_module(f"{_SMOE}.kernels.ops") + + data = make_test_data(M=32, K=64, N=128, E=4, R=8, k=2) + + base_output = base_ops.scatter2scatter( + X=data["X"], + W=data["W"], + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=data["k"], + ) + + lora_output = lora_ops.scatter2scatter_lora( + X=data["X"], + W=data["W"], + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=data["k"], + lora_A=data["lora_A"], + lora_B=data["lora_B"], + scaling=0.0, + ) + + torch.testing.assert_close(lora_output, base_output, atol=1e-3, rtol=1e-3) + + def test_zero_lora_weights_matches_base(self): + """With A=0, B=0, should match base scatter2scatter.""" + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + base_ops = import_module(f"{_SMOE}.kernels.ops") + + data = make_test_data(M=32, K=64, N=128, E=4, R=8, k=2) + + zero_A = torch.zeros_like(data["lora_A"]) + zero_B = torch.zeros_like(data["lora_B"]) + + base_output = base_ops.scatter2scatter( + X=data["X"], + W=data["W"], + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=data["k"], + ) + + lora_output = lora_ops.scatter2scatter_lora( + X=data["X"], + W=data["W"], + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=data["k"], + lora_A=zero_A, + lora_B=zero_B, + scaling=1.0, + ) + + torch.testing.assert_close(lora_output, base_output, atol=1e-3, rtol=1e-3) + + +# ============================================================================= +# Test: LoRA Additivity +# ============================================================================= + + +class TestLoRAAdditivity: + """Test that the LoRA component is correctly additive.""" + + def test_lora_additivity(self): + """ + Verify: fused(X, W, A, B, s) == base(X, W) + s * per_expert_lora(X, A, B) + """ + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + base_ops = import_module(f"{_SMOE}.kernels.ops") + + data = make_test_data(M=32, K=64, N=128, E=4, R=8, k=2) + + # Base output (no LoRA) + base_output = base_ops.scatter2scatter( + X=data["X"], + W=data["W"], + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=data["k"], + ) + + # Fused output + fused_output = lora_ops.scatter2scatter_lora( + X=data["X"], + W=data["W"], + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=data["k"], + lora_A=data["lora_A"], + lora_B=data["lora_B"], + scaling=data["scaling"], + ) + + # Compute LoRA contribution manually (reference) + lora_only = reference_parallel_linear_lora( + data["X"], + torch.zeros_like(data["W"]), + data["k"], + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["lora_A"], + data["lora_B"], + data["scaling"], + ) + + # fused = base + lora + expected = base_output + lora_only + torch.testing.assert_close(fused_output, expected, atol=2e-2, rtol=2e-2) + + +# ============================================================================= +# Test: ParallelExperts module integration +# ============================================================================= + + +class TestParallelExpertsModule: + """Test the ParallelExperts module with LoRA.""" + + def test_set_and_clear_lora(self): + """Test set_lora/clear_lora lifecycle.""" + from importlib import import_module + + lora_module = import_module(f"{_SMOE}.lora_ops") + + pe = lora_module.ParallelExperts(4, 64, 128).cuda() + + A = torch.randn(32, 64, device="cuda") # r=8, E=4 + B = torch.randn(128, 32, device="cuda") + pe.set_lora(A, B, 0.5) + + assert pe._lora_A is A + assert pe._lora_B is B + assert pe._lora_scaling == 0.5 + + pe.clear_lora() + assert pe._lora_A is None + assert pe._lora_B is None + + def test_forward_with_lora(self): + """ParallelExperts forward with LoRA matches reference.""" + from importlib import import_module + + lora_module = import_module(f"{_SMOE}.lora_ops") + + E, K, N, R = 4, 64, 128, 8 + M, k = 16, 2 + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k) + + pe = lora_module.ParallelExperts(E, K, N).cuda() + # Set weights to match test data + with torch.no_grad(): + pe.weight.copy_(data["W"].permute(0, 2, 1)) # [E, N, K] + + pe.set_lora(data["lora_A"], data["lora_B"], data["scaling"]) + + output = pe( + data["X"], + k, + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + ) + + ref = reference_parallel_linear_lora( + data["X"], + data["W"], + k, + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["lora_A"], + data["lora_B"], + data["scaling"], + ) + + torch.testing.assert_close(output, ref, atol=2e-2, rtol=2e-2) + + +# ============================================================================= +# Test: Edge Cases +# ============================================================================= + + +class TestEdgeCases: + """Edge cases and boundary conditions.""" + + def test_all_tokens_one_expert(self): + """All tokens routed to a single expert.""" + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + + M, K, N, E, R, k = 16, 64, 64, 4, 8, 1 + torch.manual_seed(42) + + X = torch.randn(M, K, device="cuda") + W = torch.randn(E, K, N, device="cuda") * 0.02 + lora_A = torch.randn(R * E, K, device="cuda") * 0.01 + lora_B = torch.randn(N, R * E, device="cuda") * 0.01 + + # All tokens go to expert 0 + selected_experts = torch.zeros(M, k, device="cuda", dtype=torch.long) + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = ( + flatten_sort_count_ref(selected_experts, E) + ) + + ref = reference_parallel_linear_lora( + X, + W, + k, + sorted_expert_idxs, + sorted_scattered_idxs, + lora_A, + lora_B, + 0.5, + ) + + kernel = lora_ops.scatter2scatter_lora( + X=X, + W=W, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=k, + lora_A=lora_A, + lora_B=lora_B, + scaling=0.5, + ) + + torch.testing.assert_close(kernel, ref, atol=1e-2, rtol=1e-2) + + def test_empty_experts(self): + """Some experts have no tokens assigned.""" + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + + M, K, N, E, R, k = 8, 64, 64, 8, 4, 1 + torch.manual_seed(42) + + X = torch.randn(M, K, device="cuda") + W = torch.randn(E, K, N, device="cuda") * 0.02 + lora_A = torch.randn(R * E, K, device="cuda") * 0.01 + lora_B = torch.randn(N, R * E, device="cuda") * 0.01 + + # Only use experts 0 and 1 + selected_experts = torch.randint(0, 2, (M, k), device="cuda") + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = ( + flatten_sort_count_ref(selected_experts, E) + ) + + ref = reference_parallel_linear_lora( + X, + W, + k, + sorted_expert_idxs, + sorted_scattered_idxs, + lora_A, + lora_B, + 0.5, + ) + + kernel = lora_ops.scatter2scatter_lora( + X=X, + W=W, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=k, + lora_A=lora_A, + lora_B=lora_B, + scaling=0.5, + ) + + torch.testing.assert_close(kernel, ref, atol=1e-2, rtol=1e-2) + + +# ============================================================================= +# Test: Optimization 1 - Fused dX Kernel +# ============================================================================= + + +class TestFusedDX: + """Test fused backward dX kernel: dX = dY @ W^T + scaling * (dY @ B) @ A.""" + + def _run_fused_dX_test( + self, M, K, N, E, R, k, dtype=torch.float32, atol=5e-2, rtol=5e-2 + ): + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + base_ops = import_module(f"{_SMOE}.kernels.ops") + pll = import_module(f"{_SMOE}.parallel_linear_lora") + + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k, dtype=dtype) + + # Create dummy grad_out in grouped order + grad_out = torch.randn( + data["sorted_expert_idxs"].size(0), N, device="cuda", dtype=dtype + ) + grouped_grad = base_ops.group( + grad_out, + data["sorted_scattered_idxs"], + fan_out=1, + ) + + # Reference: separate scatter2scatter(DY, W^T) + _compute_lora_input_grad + ref_base = base_ops.scatter2scatter( + X=grouped_grad, + x_grouped=True, + W=data["W"].permute(0, 2, 1), + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=1, + y_grouped=False, + ) + + ref_lora = pll._compute_lora_input_grad( + grouped_grad, + data["lora_A"], + data["lora_B"], + data["expert_offsets"], + E, + data["scaling"], + ) + # Scatter lora from grouped to ungrouped order + ref_lora_ungrouped = torch.zeros_like(ref_base) + ref_lora_ungrouped[data["sorted_scattered_idxs"]] = ref_lora + ref_total = ref_base + ref_lora_ungrouped + + # Fused kernel + fused_result = lora_ops.scatter2scatter_lora_dX( + DY=grouped_grad, + W=data["W"], + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=1, + lora_A=data["lora_A"], + lora_B=data["lora_B"], + scaling=data["scaling"], + dy_grouped=True, + dx_grouped=False, + ) + + torch.testing.assert_close(fused_result, ref_total, atol=atol, rtol=rtol) + + def test_basic(self): + self._run_fused_dX_test(M=32, K=64, N=128, E=4, R=8, k=2) + + def test_large(self): + self._run_fused_dX_test(M=256, K=256, N=512, E=8, R=16, k=2) + + def test_single_expert(self): + self._run_fused_dX_test(M=64, K=128, N=256, E=1, R=8, k=1) + + def test_k1(self): + self._run_fused_dX_test(M=64, K=64, N=128, E=4, R=8, k=1) + + def test_bf16(self): + self._run_fused_dX_test( + M=64, + K=128, + N=256, + E=4, + R=16, + k=2, + dtype=torch.bfloat16, + atol=1e-1, + rtol=1e-1, + ) + + def test_grouped_output(self): + """Test fused dX with dx_grouped=True.""" + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + base_ops = import_module(f"{_SMOE}.kernels.ops") + pll = import_module(f"{_SMOE}.parallel_linear_lora") + + M, K, N, E, R, k = 32, 64, 128, 4, 8, 2 + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k) + + grad_out = torch.randn(data["sorted_expert_idxs"].size(0), N, device="cuda") + grouped_grad = base_ops.group( + grad_out, data["sorted_scattered_idxs"], fan_out=1 + ) + + # Reference: grouped output + ref_base = base_ops.scatter2scatter( + X=grouped_grad, + x_grouped=True, + W=data["W"].permute(0, 2, 1), + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=1, + y_grouped=True, # grouped output + ) + + ref_lora = pll._compute_lora_input_grad( + grouped_grad, + data["lora_A"], + data["lora_B"], + data["expert_offsets"], + E, + data["scaling"], + ) + ref_total = ref_base + ref_lora + + # Fused kernel with grouped output + fused_result = lora_ops.scatter2scatter_lora_dX( + DY=grouped_grad, + W=data["W"], + sorted_expert_idxs=data["sorted_expert_idxs"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + k=1, + lora_A=data["lora_A"], + lora_B=data["lora_B"], + scaling=data["scaling"], + dy_grouped=True, + dx_grouped=True, + ) + + torch.testing.assert_close(fused_result, ref_total, atol=5e-2, rtol=5e-2) + + def test_autograd_with_fused_dX(self): + """Full autograd round-trip with use_fused_dX=True.""" + from importlib import import_module + + pll = import_module(f"{_SMOE}.parallel_linear_lora") + + M, K, N, E, R, k = 32, 64, 128, 4, 8, 2 + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k) + + # Run without fused dX + X1 = data["X"].clone().requires_grad_(True) + A1 = data["lora_A"].clone().requires_grad_(True) + B1 = data["lora_B"].clone().requires_grad_(True) + out1 = pll.ScatterMoELoRA.apply( + X1, + data["W"], + k, + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + A1, + B1, + data["scaling"], + None, + None, + False, + False, + False, # use_fused_dX=False + ) + out1.sum().backward() + + # Run with fused dX + X2 = data["X"].clone().requires_grad_(True) + A2 = data["lora_A"].clone().requires_grad_(True) + B2 = data["lora_B"].clone().requires_grad_(True) + out2 = pll.ScatterMoELoRA.apply( + X2, + data["W"], + k, + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + A2, + B2, + data["scaling"], + None, + None, + False, + False, + True, # use_fused_dX=True + ) + out2.sum().backward() + + # Forward should be identical + torch.testing.assert_close(out1, out2, atol=1e-5, rtol=1e-5) + + # Gradients should match + torch.testing.assert_close(X1.grad, X2.grad, atol=5e-2, rtol=5e-2) + torch.testing.assert_close(A1.grad, A2.grad, atol=5e-2, rtol=5e-2) + torch.testing.assert_close(B1.grad, B2.grad, atol=5e-2, rtol=5e-2) + + +# ============================================================================= +# Test: Optimization 2 - Fused Gather Backward +# ============================================================================= + + +class TestFusedGatherBackward: + """Test fused gather + backward dA/dB kernel.""" + + def _run_fused_gather_test( + self, M, K, N, E, R, k, dtype=torch.float32, atol=5e-2, rtol=5e-2 + ): + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + base_ops = import_module(f"{_SMOE}.kernels.ops") + + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k, dtype=dtype) + + # Create grad_out in ungrouped order (M*k, N) + M_total = data["sorted_expert_idxs"].size(0) + grad_out = torch.randn(M_total, N, device="cuda", dtype=dtype) + + # Reference: group() + group_bwd_lora() + grouped_grad = base_ops.group( + grad_out, data["sorted_scattered_idxs"], fan_out=1 + ) + grouped_x = base_ops.group(data["X"], data["sorted_scattered_idxs"], fan_out=k) + + ref_dA, ref_dB = lora_ops.group_bwd_lora( + DY=grouped_grad, + X=grouped_x, + lora_A=data["lora_A"], + lora_B=data["lora_B"], + expert_offsets=data["expert_offsets"], + E=E, + scaling=data["scaling"], + ) + + # Fused kernel: no group() calls + fused_dA, fused_dB = lora_ops.group_bwd_lora_fused( + DY=grad_out, + X=data["X"], + lora_A=data["lora_A"], + lora_B=data["lora_B"], + expert_offsets=data["expert_offsets"], + sorted_scattered_idxs=data["sorted_scattered_idxs"], + E=E, + k=k, + scaling=data["scaling"], + ) + + torch.testing.assert_close(fused_dA, ref_dA, atol=atol, rtol=rtol) + torch.testing.assert_close(fused_dB, ref_dB, atol=atol, rtol=rtol) + + def test_basic(self): + self._run_fused_gather_test(M=32, K=64, N=128, E=4, R=8, k=2) + + def test_large(self): + self._run_fused_gather_test(M=256, K=256, N=512, E=8, R=16, k=2) + + def test_single_expert(self): + self._run_fused_gather_test(M=64, K=128, N=256, E=1, R=8, k=1) + + def test_k1(self): + self._run_fused_gather_test(M=64, K=64, N=128, E=4, R=8, k=1) + + def test_many_experts(self): + self._run_fused_gather_test(M=128, K=64, N=128, E=16, R=8, k=4) + + def test_bf16(self): + self._run_fused_gather_test( + M=64, + K=128, + N=256, + E=4, + R=16, + k=2, + dtype=torch.bfloat16, + atol=1e-1, + rtol=1e-1, + ) + + def test_autograd_with_fused_gather(self): + """Full autograd round-trip with use_fused_gather=True.""" + from importlib import import_module + + pll = import_module(f"{_SMOE}.parallel_linear_lora") + + M, K, N, E, R, k = 32, 64, 128, 4, 8, 2 + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k) + + # Run without fused gather + X1 = data["X"].clone().requires_grad_(True) + A1 = data["lora_A"].clone().requires_grad_(True) + B1 = data["lora_B"].clone().requires_grad_(True) + out1 = pll.ScatterMoELoRA.apply( + X1, + data["W"], + k, + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + A1, + B1, + data["scaling"], + None, + None, + False, + False, + False, + False, # use_fused_dX=False, use_fused_gather=False + ) + out1.sum().backward() + + # Run with fused gather + X2 = data["X"].clone().requires_grad_(True) + A2 = data["lora_A"].clone().requires_grad_(True) + B2 = data["lora_B"].clone().requires_grad_(True) + out2 = pll.ScatterMoELoRA.apply( + X2, + data["W"], + k, + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + A2, + B2, + data["scaling"], + None, + None, + False, + False, + False, + True, # use_fused_dX=False, use_fused_gather=True + ) + out2.sum().backward() + + # Forward identical + torch.testing.assert_close(out1, out2, atol=1e-5, rtol=1e-5) + + # dA/dB should match + torch.testing.assert_close(A1.grad, A2.grad, atol=5e-2, rtol=5e-2) + torch.testing.assert_close(B1.grad, B2.grad, atol=5e-2, rtol=5e-2) + # dX should also match (same path for dX) + torch.testing.assert_close(X1.grad, X2.grad, atol=5e-2, rtol=5e-2) + + +# ============================================================================= +# Test: Optimization 3 - Token Rounding +# ============================================================================= + + +class TestTokenRounding: + """Test token rounding utility and its integration with backward kernels.""" + + def test_round_expert_counts_basic(self): + """Verify round_expert_counts produces correct shapes and values.""" + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + + M, K, N, E, R, k = 32, 64, 128, 4, 8, 2 + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k) + + padded_ei, padded_si, padded_offsets, real_offsets = ( + lora_ops.round_expert_counts( + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + E=E, + block_m=lora_ops.BLOCK_M, + ) + ) + + # Real offsets should match original + torch.testing.assert_close(real_offsets, data["expert_offsets"]) + + # Padded offsets should be >= real offsets + assert (padded_offsets >= real_offsets).all(), ( + "Padded offsets should be >= real offsets" + ) + + # Each expert's padded count should be multiple of BLOCK_M (if non-zero) + prev = 0 + for e in range(E): + count = padded_offsets[e].item() - prev + real_count = real_offsets[e].item() - ( + real_offsets[e - 1].item() if e > 0 else 0 + ) + if real_count > 0: + assert count % lora_ops.BLOCK_M == 0, ( + f"Expert {e}: padded count {count} not multiple of {lora_ops.BLOCK_M}" + ) + assert count >= real_count, ( + f"Expert {e}: padded count {count} < real count {real_count}" + ) + prev = padded_offsets[e].item() + + def test_round_with_fused_gather(self): + """Token rounding + fused gather gives same result as plain fused gather.""" + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + base_ops = import_module(f"{_SMOE}.kernels.ops") + + M, K, N, E, R, k = 64, 64, 128, 4, 8, 2 + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k) + + M_total = data["sorted_expert_idxs"].size(0) + grad_out = torch.randn(M_total, N, device="cuda") + + # Reference: group() + group_bwd_lora() (the gold standard) + grouped_grad = base_ops.group( + grad_out, data["sorted_scattered_idxs"], fan_out=1 + ) + grouped_x = base_ops.group(data["X"], data["sorted_scattered_idxs"], fan_out=k) + ref_dA, ref_dB = lora_ops.group_bwd_lora( + DY=grouped_grad, + X=grouped_x, + lora_A=data["lora_A"], + lora_B=data["lora_B"], + expert_offsets=data["expert_offsets"], + E=E, + scaling=data["scaling"], + ) + + # Apply token rounding + padded_ei, padded_si, padded_offsets, real_offsets = ( + lora_ops.round_expert_counts( + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + E=E, + ) + ) + + # Fused gather with token rounding + rounded_dA, rounded_dB = lora_ops.group_bwd_lora_fused( + DY=grad_out, + X=data["X"], + lora_A=data["lora_A"], + lora_B=data["lora_B"], + expert_offsets=padded_offsets, + sorted_scattered_idxs=padded_si, + E=E, + k=k, + scaling=data["scaling"], + real_expert_offsets=real_offsets, + ) + + torch.testing.assert_close(rounded_dA, ref_dA, atol=5e-2, rtol=5e-2) + torch.testing.assert_close(rounded_dB, ref_dB, atol=5e-2, rtol=5e-2) + + def test_empty_experts_with_rounding(self): + """Token rounding handles experts with 0 tokens correctly.""" + from importlib import import_module + + lora_ops = import_module(f"{_SMOE}.kernels.lora_ops") + + E, k = 8, 1 + M = 8 + torch.manual_seed(42) + + # Only use experts 0 and 1 (rest have 0 tokens) + selected_experts = torch.randint(0, 2, (M, k), device="cuda") + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = ( + flatten_sort_count_ref(selected_experts, E) + ) + + padded_ei, padded_si, padded_offsets, real_offsets = ( + lora_ops.round_expert_counts( + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + E=E, + ) + ) + + # Verify empty experts have same count (0) + for e in range(E): + real_count = real_offsets[e].item() - ( + real_offsets[e - 1].item() if e > 0 else 0 + ) + padded_count = padded_offsets[e].item() - ( + padded_offsets[e - 1].item() if e > 0 else 0 + ) + if real_count == 0: + assert padded_count == 0, ( + f"Expert {e}: empty expert should have padded_count=0, got {padded_count}" + ) + + +# ============================================================================= +# Test: Combined Optimizations +# ============================================================================= + + +class TestCombinedOptimizations: + """Test all optimizations together.""" + + def test_fused_dX_and_fused_gather(self): + """Both fused dX and fused gather together.""" + from importlib import import_module + + pll = import_module(f"{_SMOE}.parallel_linear_lora") + + M, K, N, E, R, k = 64, 128, 256, 4, 8, 2 + data = make_test_data(M=M, K=K, N=N, E=E, R=R, k=k) + + # Baseline: no optimizations + X1 = data["X"].clone().requires_grad_(True) + A1 = data["lora_A"].clone().requires_grad_(True) + B1 = data["lora_B"].clone().requires_grad_(True) + out1 = pll.ScatterMoELoRA.apply( + X1, + data["W"], + k, + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + A1, + B1, + data["scaling"], + None, + None, + False, + False, + False, + False, # no optimizations + ) + out1.sum().backward() + + # Both optimizations + X2 = data["X"].clone().requires_grad_(True) + A2 = data["lora_A"].clone().requires_grad_(True) + B2 = data["lora_B"].clone().requires_grad_(True) + out2 = pll.ScatterMoELoRA.apply( + X2, + data["W"], + k, + data["sorted_expert_idxs"], + data["sorted_scattered_idxs"], + data["expert_offsets"], + A2, + B2, + data["scaling"], + None, + None, + False, + False, + True, + True, # use_fused_dX=True, use_fused_gather=True + ) + out2.sum().backward() + + # Forward identical + torch.testing.assert_close(out1, out2, atol=1e-5, rtol=1e-5) + + # All gradients match + torch.testing.assert_close(X1.grad, X2.grad, atol=5e-2, rtol=5e-2) + torch.testing.assert_close(A1.grad, A2.grad, atol=5e-2, rtol=5e-2) + torch.testing.assert_close(B1.grad, B2.grad, atol=5e-2, rtol=5e-2) diff --git a/tests/e2e/integrations/test_scattermoe_lora_olmoe.py b/tests/e2e/integrations/test_scattermoe_lora_olmoe.py new file mode 100644 index 0000000000..0481476327 --- /dev/null +++ b/tests/e2e/integrations/test_scattermoe_lora_olmoe.py @@ -0,0 +1,1255 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +Integration tests: OLMoE + peft LoRA + ScatterMoE fused kernels. + +Validates that scattermoe_lora fused kernels produce correct results when used +with HuggingFace OLMoE models and peft LoRA adapters applied via +``target_parameters``. + +Key things tested +----------------- +- LoRA weight layout conversion between peft (rank-major) and scattermoe (expert-major) +- Base forward equivalence: per-expert reference vs ScatterMoE kernels (no LoRA) +- LoRA forward equivalence: peft merged-weight approach vs scattermoe fused kernels +- Backward gradient correctness through the fused LoRA path +- ``kernelize()`` integration via ``LocalLayerRepository`` +""" + +from pathlib import Path + +import pytest +import torch +import torch.nn as nn +import torch.nn.functional as F +from peft import LoraConfig, get_peft_model +from transformers import OlmoeConfig +from transformers.models.olmoe.modeling_olmoe import OlmoeSparseMoeBlock + +_SMOE = "axolotl.integrations.kernels.libs.scattermoe_lora" + +# Try to import from axolotl's scattermoe_lora.layers; may fail on CPU without triton. +try: + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _unwrap_experts_lora, + _unwrap_gate_lora, + peft_lora_B_to_scattermoe, + peft_lora_to_scattermoe, + ) + + HAS_SCATTERMOE = True +except (ImportError, ModuleNotFoundError): + HAS_SCATTERMOE = False + + # Provide pure-torch fallbacks for CPU-only layout conversion tests. + def peft_lora_B_to_scattermoe(peft_B, num_experts, rank): + N = peft_B.shape[0] + return ( + peft_B.reshape(N, rank, num_experts) + .permute(0, 2, 1) + .contiguous() + .reshape(N, num_experts * rank) + ) + + def peft_lora_to_scattermoe(peft_A, peft_B, num_experts, rank): + peft_B_em = peft_lora_B_to_scattermoe(peft_B, num_experts, rank) + K_inter, N_hidden = peft_B.shape[0], peft_A.shape[1] + smoe_A = torch.zeros( + rank * num_experts, + K_inter, + device=peft_A.device, + dtype=peft_A.dtype, + ) + smoe_B = torch.zeros( + N_hidden, + rank * num_experts, + device=peft_A.device, + dtype=peft_A.dtype, + ) + for e in range(num_experts): + s = e * rank + smoe_A[s : s + rank, :] = peft_B_em[:, s : s + rank].T + smoe_B[:, s : s + rank] = peft_A[s : s + rank, :].T + return smoe_A, smoe_B + + def _unwrap_experts_lora(experts_module): + return experts_module, None, None + + def _unwrap_gate_lora(gate_module): + if hasattr(gate_module, "base_layer") and hasattr(gate_module, "lora_A"): + base_gate = gate_module.base_layer + active = getattr(gate_module, "active_adapters", ["default"]) + name = active[0] if active else "default" + lora_A_dict = getattr(gate_module, "lora_A", {}) + lora_B_dict = getattr(gate_module, "lora_B", {}) + scaling_dict = getattr(gate_module, "scaling", {}) + if name in lora_A_dict: + lora_A = lora_A_dict[name].weight + lora_B = lora_B_dict[name].weight + s = scaling_dict[name] + delta = s * (lora_B @ lora_A) + return base_gate, base_gate.weight, delta + return base_gate, base_gate.weight, None + return gate_module, gate_module.weight, None + + +# ============================================================================= +# Configuration +# ============================================================================= + +FULL_OLMOE_CONFIG = dict( + hidden_size=2048, + intermediate_size=1024, + num_experts=64, + num_experts_per_tok=8, + hidden_act="silu", + norm_topk_prob=False, +) + +SMALL_OLMOE_CONFIG = dict( + hidden_size=128, + intermediate_size=48, # non-square: 2*inter=96 != hidden=128 + num_experts=8, + num_experts_per_tok=2, + hidden_act="silu", + norm_topk_prob=False, +) + +requires_cuda = pytest.mark.skipif( + not torch.cuda.is_available(), reason="CUDA not available" +) + + +def make_olmoe_config(use_full=False): + cfg = dict(FULL_OLMOE_CONFIG if use_full else SMALL_OLMOE_CONFIG) + cfg["experts_implementation"] = "grouped_mm" + return OlmoeConfig(**cfg) + + +# ============================================================================= +# Layout conversion utilities (test-local helpers) +# ============================================================================= + + +def scattermoe_lora_B_to_peft(smoe_B, num_experts, rank): + """Inverse of ``peft_lora_B_to_scattermoe``.""" + N = smoe_B.shape[0] + return ( + smoe_B.reshape(N, num_experts, rank) + .permute(0, 2, 1) + .contiguous() + .reshape(N, num_experts * rank) + ) + + +def peft_gate_up_lora_to_scattermoe(peft_A, peft_B, num_experts, rank): + """Convert peft LoRA for gate_up_proj to scattermoe layout. + + Both gate_up_proj and down_proj need the A<->B swap because + scattermoe transposes the parameter (W = param.T). + """ + return peft_lora_to_scattermoe(peft_A, peft_B, num_experts, rank) + + +# ============================================================================= +# Helpers +# ============================================================================= + + +def _init_expert_weights(moe_block): + """Initialize OlmoeExperts parameters which use torch.empty (uninitialized). + + Without this, gate_up_proj and down_proj contain garbage/NaN values. + """ + with torch.no_grad(): + nn.init.kaiming_uniform_(moe_block.experts.gate_up_proj) + nn.init.kaiming_uniform_(moe_block.experts.down_proj) + return moe_block + + +class MinimalOLMoEModel(nn.Module): + """Thin wrapper so peft's get_peft_model can attach adapters.""" + + def __init__(self, config): + super().__init__() + self.moe = OlmoeSparseMoeBlock(config) + _init_expert_weights(self.moe) + + def forward(self, x): + return self.moe(x) + + +def _get_routing(moe_block, hidden_states): + """Run the router and return (routing_weights, selected_experts).""" + with torch.no_grad(): + _, routing_weights, selected_experts = moe_block.gate( + hidden_states.view(-1, hidden_states.size(-1)) + ) + return routing_weights, selected_experts + + +def _reference_moe_forward( + x_flat, + gate_up_proj, + down_proj, + act_fn, + top_k_index, + top_k_weights, + num_experts, +): + """Pure-PyTorch per-expert reference MoE forward (no LoRA). + + Uses F.linear per expert for an apples-to-apples comparison with + the ScatterMoE kernel path. + """ + final = torch.zeros_like(x_flat) + expert_mask = F.one_hot(top_k_index, num_classes=num_experts).permute(2, 1, 0) + for e in range(num_experts): + top_k_pos, token_idx = torch.where(expert_mask[e]) + if token_idx.numel() == 0: + continue + cur = x_flat[token_idx] + gate_up = F.linear(cur, gate_up_proj[e]) + g, u = gate_up.chunk(2, dim=-1) + h = act_fn(g) * u + out = F.linear(h, down_proj[e]) + out = out * top_k_weights[token_idx, top_k_pos, None] + final.index_add_(0, token_idx, out.to(final.dtype)) + return final + + +def _reference_moe_forward_with_lora( + x_flat, + gate_up_proj, + down_proj, + act_fn, + top_k_index, + top_k_weights, + num_experts, + gup_delta, + down_delta, +): + """Pure-PyTorch reference MoE forward with pre-computed weight deltas.""" + merged_gup = gate_up_proj + gup_delta + merged_down = down_proj + down_delta + return _reference_moe_forward( + x_flat, + merged_gup, + merged_down, + act_fn, + top_k_index, + top_k_weights, + num_experts, + ) + + +def _compute_delta_from_scattermoe_lora(lora_A, lora_B, scaling, E, r, param_shape): + """Compute additive weight delta from scattermoe-layout LoRA weights. + + delta[e] = scaling * B_e @ A_e where A_e [r,K], B_e [N,r] -> [N,K]. + """ + delta = torch.zeros(param_shape, device=lora_A.device, dtype=lora_A.dtype) + for e in range(E): + A_e = lora_A[e * r : (e + 1) * r, :] + B_e = lora_B[:, e * r : (e + 1) * r] + delta[e] = scaling * (B_e @ A_e) + return delta + + +# ============================================================================= +# Tests: Layout conversion +# ============================================================================= + + +class TestLoRABLayoutConversion: + """Test the peft <-> scattermoe lora_B layout conversion.""" + + def test_roundtrip(self): + E, r, N = 8, 4, 64 + original = torch.randn(N, E * r) + converted = peft_lora_B_to_scattermoe(original, E, r) + back = scattermoe_lora_B_to_peft(converted, E, r) + torch.testing.assert_close(back, original) + + def test_per_expert_slices(self): + """After conversion, scattermoe slicing gives the same per-expert + matrices as peft's reshape slicing.""" + E, r, N = 4, 2, 16 + peft_B = torch.randn(N, E * r) + smoe_B = peft_lora_B_to_scattermoe(peft_B, E, r) + + peft_reshaped = peft_B.reshape(N, r, E) + for e in range(E): + torch.testing.assert_close( + smoe_B[:, e * r : (e + 1) * r], + peft_reshaped[:, :, e], + ) + + def test_lora_A_already_compatible(self): + """lora_A layout is identical between peft and scattermoe.""" + E, r, K = 4, 2, 16 + lora_A = torch.randn(E * r, K) + peft_reshaped = lora_A.reshape(E, r, K) + for e in range(E): + torch.testing.assert_close( + lora_A[e * r : (e + 1) * r, :], + peft_reshaped[e], + ) + + def test_delta_weight_equivalence(self): + """peft's einsum delta matches per-expert B @ A with converted layouts.""" + E, r, K, N = 8, 4, 32, 64 + peft_A = torch.randn(E * r, K) + peft_B = torch.randn(N, E * r) + scaling = 2.0 + + A_r = peft_A.reshape(E, r, K) + B_r = peft_B.reshape(N, r, E) + delta_peft = torch.einsum("o r e, e r i -> e i o", B_r, A_r) * scaling + + smoe_B = peft_lora_B_to_scattermoe(peft_B, E, r) + for e in range(E): + A_e = peft_A[e * r : (e + 1) * r, :] + B_e = smoe_B[:, e * r : (e + 1) * r] + delta_e = scaling * (B_e @ A_e) + torch.testing.assert_close(delta_e, delta_peft[e].T, atol=1e-5, rtol=1e-5) + + def test_down_proj_conversion(self): + """Verify peft_lora_to_scattermoe produces correct delta.""" + E, r = 4, 2 + hidden, inter = 32, 16 + scaling = 2.0 + + peft_A = torch.randn(E * r, hidden) + peft_B = torch.randn(inter, E * r) + + A_r = peft_A.reshape(E, r, hidden) + B_r = peft_B.reshape(inter, r, E) + delta_peft = torch.einsum("o r e, e r i -> e i o", B_r, A_r) * scaling + + smoe_A, smoe_B = peft_lora_to_scattermoe(peft_A, peft_B, E, r) + for e in range(E): + A_e = smoe_A[e * r : (e + 1) * r, :] + B_e = smoe_B[:, e * r : (e + 1) * r] + delta_smoe_e = scaling * (B_e @ A_e) + torch.testing.assert_close( + delta_smoe_e, delta_peft[e], atol=1e-5, rtol=1e-5 + ) + + def test_gate_up_proj_conversion(self): + """Verify gate_up_proj LoRA conversion with non-square dims (Qwen3-like). + + gate_up_proj param: [E, 2*inter, hidden]. + peft: in_features=2*inter, out_features=hidden. + peft lora_A: [r*E, 2*inter], lora_B: [hidden, r*E]. + + scattermoe W = param.T = [E, hidden, 2*inter], K=hidden, N=2*inter. + scattermoe needs: lora_A [r*E, K=hidden], lora_B [N=2*inter, r*E]. + + Uses non-square dims (hidden=32 != 2*inter=24) to catch A<->B swap bugs. + """ + E, r = 4, 2 + hidden, inter = 32, 12 # 2*inter=24 != hidden=32 + scaling = 2.0 + + # peft assigns: in_features=2*inter, out_features=hidden + peft_A = torch.randn(E * r, 2 * inter) # [r*E, in_features=2*inter] + peft_B = torch.randn(hidden, E * r) # [out_features=hidden, r*E] + + # peft delta via einsum: "o r e, e r i -> e i o" + A_r = peft_A.reshape(E, r, 2 * inter) + B_r = peft_B.reshape(hidden, r, E) + delta_peft = torch.einsum("o r e, e r i -> e i o", B_r, A_r) * scaling + # delta_peft[e] has shape [in_features, out_features] = [2*inter, hidden] + # = param[e] shape [2*inter, hidden] + + smoe_A, smoe_B = peft_gate_up_lora_to_scattermoe(peft_A, peft_B, E, r) + # smoe_A should be [r*E, K=hidden], smoe_B should be [N=2*inter, r*E] + assert smoe_A.shape == (E * r, hidden), ( + f"Expected {(E * r, hidden)}, got {smoe_A.shape}" + ) + assert smoe_B.shape == (2 * inter, E * r), ( + f"Expected {(2 * inter, E * r)}, got {smoe_B.shape}" + ) + + for e in range(E): + A_e = smoe_A[e * r : (e + 1) * r, :] # [r, K=hidden] + B_e = smoe_B[:, e * r : (e + 1) * r] # [N=2*inter, r] + delta_smoe_e = scaling * (B_e @ A_e) # [2*inter, hidden] + # Should match peft delta which is [2*inter, hidden] = param[e] + torch.testing.assert_close( + delta_smoe_e, delta_peft[e], atol=1e-5, rtol=1e-5 + ) + + +# ============================================================================= +# Tests: peft weight extraction +# ============================================================================= + + +class TestPeftLoRAWeightExtraction: + """Test extracting peft LoRA weights for OLMoE.""" + + def test_peft_creates_correct_shapes(self): + config = make_olmoe_config(use_full=False) + E, r = config.num_experts, 4 + + model = MinimalOLMoEModel(config) + lora_config = LoraConfig( + r=r, + lora_alpha=16, + target_modules=[], + target_parameters=[ + "gate.weight", + "experts.gate_up_proj", + "experts.down_proj", + ], + bias="none", + ) + peft_model = get_peft_model(model, lora_config) + trainable = {n: p for n, p in peft_model.named_parameters() if p.requires_grad} + + # Gate router + assert trainable["base_model.model.moe.gate.lora_A.default.weight"].shape == ( + r, + config.hidden_size, + ) + assert trainable["base_model.model.moe.gate.lora_B.default.weight"].shape == ( + E, + r, + ) + + # gate_up_proj [E, 2*inter, hidden] + # peft: in_features=2*inter (dim 1), out_features=hidden (dim 2) + assert trainable[ + "base_model.model.moe.experts.base_layer.lora_A.default.weight" + ].shape == (E * r, 2 * config.intermediate_size) + assert trainable[ + "base_model.model.moe.experts.base_layer.lora_B.default.weight" + ].shape == (config.hidden_size, E * r) + + # down_proj [E, hidden, inter] + # peft: in_features=hidden (dim 1), out_features=inter (dim 2) + assert trainable[ + "base_model.model.moe.experts.lora_A.default.weight" + ].shape == (E * r, config.hidden_size) + assert trainable[ + "base_model.model.moe.experts.lora_B.default.weight" + ].shape == (config.intermediate_size, E * r) + + @requires_cuda + def test_peft_forward_runs(self): + """Smoke test: peft model forward pass completes (needs CUDA for grouped_mm).""" + config = make_olmoe_config(use_full=False) + model = MinimalOLMoEModel(config) + lora_config = LoraConfig( + r=4, + lora_alpha=16, + target_modules=[], + target_parameters=[ + "gate.weight", + "experts.gate_up_proj", + "experts.down_proj", + ], + bias="none", + ) + peft_model = get_peft_model(model, lora_config) + x = torch.randn(1, 4, config.hidden_size) + out = peft_model(x) + assert out.shape == x.shape + + @pytest.mark.skipif( + not HAS_SCATTERMOE, reason="scattermoe_lora not importable (no triton)" + ) + def test_unwrap_experts_lora(self): + """Test that _unwrap_experts_lora correctly detects LoRA wrappers.""" + config = make_olmoe_config(use_full=False) + model = MinimalOLMoEModel(config) + lora_config = LoraConfig( + r=4, + lora_alpha=16, + target_modules=[], + target_parameters=["experts.gate_up_proj", "experts.down_proj"], + bias="none", + ) + peft_model = get_peft_model(model, lora_config) + base_moe = peft_model.base_model.model.moe + + # Experts should be wrapped by ParamWrapper + experts, gup_lora, down_lora = _unwrap_experts_lora(base_moe.experts) + + # Base experts should have the raw parameters + assert hasattr(experts, "gate_up_proj") + assert hasattr(experts, "down_proj") + + # LoRA should be detected + assert gup_lora is not None, "gate_up_proj LoRA not detected" + assert down_lora is not None, "down_proj LoRA not detected" + + # Check shapes (after peft->scattermoe conversion with A<->B swap) + # gate_up_proj W = param.T = [E, hidden, 2*inter], K=hidden, N=2*inter + E, r = config.num_experts, 4 + gup_A, gup_B, gup_s = gup_lora + assert gup_A.shape == (E * r, config.hidden_size), ( + f"gate_up_proj smoe_A: expected [r*E, K=hidden]={(E * r, config.hidden_size)}, " + f"got {gup_A.shape}" + ) + assert gup_B.shape == (2 * config.intermediate_size, E * r), ( + f"gate_up_proj smoe_B: expected [N=2*inter, r*E]=" + f"{(2 * config.intermediate_size, E * r)}, got {gup_B.shape}" + ) + + # down_proj W = param.T = [E, inter, hidden], K=inter, N=hidden + down_A, down_B, down_s = down_lora + assert down_A.shape == (E * r, config.intermediate_size), ( + f"down_proj smoe_A: expected [r*E, K=inter]={(E * r, config.intermediate_size)}, " + f"got {down_A.shape}" + ) + assert down_B.shape == (config.hidden_size, E * r), ( + f"down_proj smoe_B: expected [N=hidden, r*E]={(config.hidden_size, E * r)}, " + f"got {down_B.shape}" + ) + + def test_unwrap_no_lora(self): + """Without peft, _unwrap_experts_lora returns no LoRA.""" + config = make_olmoe_config(use_full=False) + moe = OlmoeSparseMoeBlock(config) + experts, gup_lora, down_lora = _unwrap_experts_lora(moe.experts) + assert gup_lora is None + assert down_lora is None + assert hasattr(experts, "gate_up_proj") + + def test_unwrap_gate_lora(self): + """Test that _unwrap_gate_lora detects LoRA on the router gate.""" + config = make_olmoe_config(use_full=False) + model = MinimalOLMoEModel(config) + r = 4 + lora_config = LoraConfig( + r=r, + lora_alpha=16, + target_modules=[], + target_parameters=["gate.weight"], + bias="none", + ) + peft_model = get_peft_model(model, lora_config) + base_moe = peft_model.base_model.model.moe + + # Set non-zero LoRA weights (peft initializes lora_B to zeros) + with torch.no_grad(): + base_moe.gate.lora_B["default"].weight.normal_(0, 0.01) + + base_gate, gate_weight, gate_delta = _unwrap_gate_lora(base_moe.gate) + + # Base gate should be the original router + assert hasattr(base_gate, "top_k") + assert hasattr(base_gate, "num_experts") + assert base_gate.top_k == config.num_experts_per_tok + assert base_gate.num_experts == config.num_experts + + # Gate weight should be the base weight (delta returned separately) + assert gate_weight.shape == (config.num_experts, config.hidden_size) + torch.testing.assert_close(gate_weight, base_gate.weight) + + # Delta should be non-zero (LoRA was applied) + assert gate_delta is not None + assert gate_delta.shape == (config.num_experts, config.hidden_size) + assert gate_delta.abs().max() > 0, "Gate LoRA delta should be non-zero" + + def test_unwrap_gate_no_lora(self): + """Without peft, _unwrap_gate_lora returns the original gate.""" + config = make_olmoe_config(use_full=False) + moe = OlmoeSparseMoeBlock(config) + base_gate, gate_weight, gate_delta = _unwrap_gate_lora(moe.gate) + assert base_gate is moe.gate + torch.testing.assert_close(gate_weight, moe.gate.weight) + assert gate_delta is None + + def test_gate_lora_delta_matches_peft(self): + """Verify _unwrap_gate_lora computes the same delta as peft.""" + config = make_olmoe_config(use_full=False) + model = MinimalOLMoEModel(config) + r = 4 + lora_alpha = 16 + scaling = lora_alpha / r + lora_config = LoraConfig( + r=r, + lora_alpha=lora_alpha, + target_modules=[], + target_parameters=["gate.weight"], + bias="none", + ) + peft_model = get_peft_model(model, lora_config) + base_moe = peft_model.base_model.model.moe + + # Our unwrapped weight + delta + _, gate_weight, gate_delta = _unwrap_gate_lora(base_moe.gate) + + # Manually compute expected delta + lora_A = base_moe.gate.lora_A["default"].weight # [r, hidden] + lora_B = base_moe.gate.lora_B["default"].weight # [E, r] + base_weight = base_moe.gate.base_layer.weight # [E, hidden] + expected_delta = scaling * (lora_B @ lora_A) + + torch.testing.assert_close(gate_weight, base_weight) + torch.testing.assert_close(gate_delta, expected_delta) + # Combined should match the old behavior + torch.testing.assert_close( + gate_weight + gate_delta, base_weight + expected_delta + ) + + +# ============================================================================= +# Tests: Base forward equivalence (no LoRA) +# ============================================================================= + + +@requires_cuda +class TestOLMoEReferenceVsScatterMoE: + """Base forward equivalence: per-expert reference vs ScatterMoE kernels.""" + + def test_small(self): + self._run(use_full=False, M=16) + + @pytest.mark.slow + def test_full(self): + self._run(use_full=True, M=32) + + def _run(self, use_full, M): + from axolotl.integrations.kernels.libs.scattermoe_lora import ( + flatten_sort_count, + parallel_linear, + ) + + config = make_olmoe_config(use_full=use_full) + torch.manual_seed(42) + moe = _init_expert_weights(OlmoeSparseMoeBlock(config)).cuda().float() + E, k = config.num_experts, config.num_experts_per_tok + + x = torch.randn(1, M, config.hidden_size, device="cuda") + x_flat = x.view(-1, config.hidden_size) + + with torch.no_grad(): + # Shared routing for both paths + _, rw, sel = moe.gate(x_flat) + sei, ssi, eo = flatten_sort_count(sel, num_experts=E) + + # Per-expert reference + ref_out = _reference_moe_forward( + x_flat, + moe.experts.gate_up_proj, + moe.experts.down_proj, + moe.experts.act_fn, + sel, + rw, + E, + ).view(1, M, config.hidden_size) + + # ScatterMoE kernel path + gup = parallel_linear( + x_flat, + moe.experts.gate_up_proj.transpose(2, 1), + k, + sei, + ssi, + eo, + grouped_in=False, + grouped_out=True, + ) + g, u = gup.chunk(2, dim=-1) + h = moe.experts.act_fn(g) * u + + smoe_out = parallel_linear( + h, + moe.experts.down_proj.transpose(2, 1), + 1, + sei, + ssi, + eo, + grouped_in=True, + grouped_out=False, + gates=rw, + ).view(1, M, config.hidden_size) + + torch.testing.assert_close(smoe_out, ref_out, atol=1e-3, rtol=1e-3) + + +# ============================================================================= +# Tests: LoRA forward equivalence (peft vs scattermoe fused) +# ============================================================================= + + +@requires_cuda +class TestOLMoEPeftLoRAForward: + """Fused LoRA forward: peft merged-weight vs scattermoe_lora kernel.""" + + def test_small(self): + self._run(use_full=False, M=16, r=4) + + @pytest.mark.slow + def test_full(self): + self._run(use_full=True, M=32, r=8) + + def _run(self, use_full, M, r): + from axolotl.integrations.kernels.libs.scattermoe_lora import ( + flatten_sort_count, + parallel_linear_lora, + ) + + config = make_olmoe_config(use_full=use_full) + E, k = config.num_experts, config.num_experts_per_tok + lora_alpha = 16 + scaling = lora_alpha / r + + # Create peft model + model = MinimalOLMoEModel(config).cuda().float() + lora_config = LoraConfig( + r=r, + lora_alpha=lora_alpha, + target_modules=[], + target_parameters=["experts.gate_up_proj", "experts.down_proj"], + bias="none", + ) + peft_model = get_peft_model(model, lora_config) + + torch.manual_seed(42) + x = torch.randn(1, M, config.hidden_size, device="cuda") + + # peft forward + with torch.no_grad(): + peft_out = peft_model(x) + + # Extract base weights and LoRA weights + base_moe = peft_model.base_model.model.moe + base_experts = base_moe.experts.base_layer.base_layer + gate_up_proj = base_experts.gate_up_proj + down_proj = base_experts.down_proj + act_fn = base_experts.act_fn + + # gate_up_proj LoRA + gup_w = base_moe.experts.base_layer + peft_gup_A = gup_w.lora_A["default"].weight.detach() + peft_gup_B = gup_w.lora_B["default"].weight.detach() + smoe_gup_A, smoe_gup_B = peft_gate_up_lora_to_scattermoe( + peft_gup_A, peft_gup_B, E, r + ) + + # down_proj LoRA + down_w = base_moe.experts + peft_down_A = down_w.lora_A["default"].weight.detach() + peft_down_B = down_w.lora_B["default"].weight.detach() + smoe_down_A, smoe_down_B = peft_lora_to_scattermoe( + peft_down_A, peft_down_B, E, r + ) + + # ScatterMoE fused forward -- gate is NOT peft-wrapped, access directly + x_flat = x.view(-1, config.hidden_size) + + with torch.no_grad(): + _, rw, sel = base_moe.gate(x_flat) + sei, ssi, eo = flatten_sort_count(sel, num_experts=E) + + gup = parallel_linear_lora( + x_flat, + gate_up_proj.transpose(2, 1), + k, + sei, + ssi, + eo, + lora_A=smoe_gup_A, + lora_B=smoe_gup_B, + scaling=scaling, + grouped_in=False, + grouped_out=True, + ) + g, u = gup.chunk(2, dim=-1) + h = act_fn(g) * u + + smoe_out = parallel_linear_lora( + h, + down_proj.transpose(2, 1), + 1, + sei, + ssi, + eo, + lora_A=smoe_down_A, + lora_B=smoe_down_B, + scaling=scaling, + grouped_in=True, + grouped_out=False, + gates=rw, + ).view(1, M, config.hidden_size) + + torch.testing.assert_close(smoe_out, peft_out, atol=5e-3, rtol=5e-3) + + +# ============================================================================= +# Tests: Backward gradient correctness +# ============================================================================= + + +@requires_cuda +class TestOLMoEPeftLoRABackward: + """Backward gradients through scattermoe_lora vs pure-PyTorch reference.""" + + def test_small(self): + self._run(use_full=False, M=16, r=4) + + def _run(self, use_full, M, r): + from axolotl.integrations.kernels.libs.scattermoe_lora import ( + flatten_sort_count, + parallel_linear_lora, + ) + + config = make_olmoe_config(use_full=use_full) + E, k = config.num_experts, config.num_experts_per_tok + lora_alpha = 16 + scaling = lora_alpha / r + + torch.manual_seed(42) + moe = _init_expert_weights(OlmoeSparseMoeBlock(config)).cuda().float() + x = torch.randn(1, M, config.hidden_size, device="cuda") + x_flat = x.view(-1, config.hidden_size) + gate_up_proj = moe.experts.gate_up_proj + down_proj = moe.experts.down_proj + + # Create LoRA weights in scattermoe layout directly + gup_A = torch.randn(r * E, config.hidden_size, device="cuda") * 0.01 + gup_B = torch.randn(2 * config.intermediate_size, r * E, device="cuda") * 0.01 + down_A = torch.randn(r * E, config.intermediate_size, device="cuda") * 0.01 + down_B = torch.randn(config.hidden_size, r * E, device="cuda") * 0.01 + + rw, sel = _get_routing(moe, x) + sei, ssi, eo = flatten_sort_count(sel, num_experts=E) + + # --- Reference --- + gup_delta = _compute_delta_from_scattermoe_lora( + gup_A, gup_B, scaling, E, r, gate_up_proj.shape + ) + down_delta = _compute_delta_from_scattermoe_lora( + down_A, down_B, scaling, E, r, down_proj.shape + ) + + x_ref = x_flat.clone().detach().requires_grad_(True) + ref_out = _reference_moe_forward_with_lora( + x_ref, + gate_up_proj, + down_proj, + moe.experts.act_fn, + sel, + rw, + E, + gup_delta, + down_delta, + ) + ref_out.sum().backward() + + # --- ScatterMoE fused path --- + x_smoe = x_flat.clone().detach().requires_grad_(True) + gup_A_s = gup_A.clone().requires_grad_(True) + gup_B_s = gup_B.clone().requires_grad_(True) + down_A_s = down_A.clone().requires_grad_(True) + down_B_s = down_B.clone().requires_grad_(True) + + gup_out = parallel_linear_lora( + x_smoe, + gate_up_proj.transpose(2, 1), + k, + sei, + ssi, + eo, + lora_A=gup_A_s, + lora_B=gup_B_s, + scaling=scaling, + grouped_in=False, + grouped_out=True, + ) + g, u = gup_out.chunk(2, dim=-1) + h = moe.experts.act_fn(g) * u + + smoe_out = parallel_linear_lora( + h, + down_proj.transpose(2, 1), + 1, + sei, + ssi, + eo, + lora_A=down_A_s, + lora_B=down_B_s, + scaling=scaling, + grouped_in=True, + grouped_out=False, + gates=rw, + ) + smoe_out.sum().backward() + + torch.testing.assert_close( + smoe_out.detach(), + ref_out.detach(), + atol=5e-3, + rtol=5e-3, + ) + torch.testing.assert_close( + x_smoe.grad, + x_ref.grad, + atol=5e-2, + rtol=5e-2, + ) + + +# ============================================================================= +# Tests: kernelize() integration via LocalLayerRepository +# ============================================================================= + + +@requires_cuda +class TestKernelizeIntegration: + """Test the HF kernels library integration with LocalLayerRepository.""" + + @staticmethod + def _get_kernelize_imports(): + """Import kernels library components, skip if not available.""" + try: + from kernels import ( + LocalLayerRepository, + Mode, + kernelize, + register_kernel_mapping, + replace_kernel_forward_from_hub, + ) + + return ( + LocalLayerRepository, + Mode, + register_kernel_mapping, + replace_kernel_forward_from_hub, + kernelize, + ) + except ImportError: + pytest.skip("kernels library not installed") + + @staticmethod + def _get_repo_path(): + """Get the path to scattermoe_lora within axolotl's plugin.""" + return ( + Path(__file__).parent.parent.parent + / "src" + / "axolotl" + / "integrations" + / "kernels" + / "libs" + / "scattermoe_lora" + ) + + def _setup_kernels( + self, + LocalLayerRepository, + Mode, + register_kernel_mapping, + replace_kernel_forward_from_hub, + ): + """Register kernel mapping for tests.""" + repo_path = self._get_repo_path() + local_repo = LocalLayerRepository( + repo_path=repo_path, + package_name="scattermoe_lora", + layer_name="HFScatterMoEGatedMLP", + ) + + replace_kernel_forward_from_hub( + OlmoeSparseMoeBlock, "HFScatterMoEParallelExperts" + ) + register_kernel_mapping( + { + "HFScatterMoEParallelExperts": { + "cuda": { + Mode.TRAINING: local_repo, + Mode.INFERENCE: local_repo, + }, + } + } + ) + + def test_base_forward_via_kernelize(self): + """Kernelized OlmoeSparseMoeBlock (no LoRA) matches per-expert reference.""" + ( + LocalLayerRepository, + Mode, + register_kernel_mapping, + replace_kernel_forward_from_hub, + kernelize, + ) = self._get_kernelize_imports() + + config = make_olmoe_config(use_full=False) + E = config.num_experts + + # Create model + torch.manual_seed(42) + moe = _init_expert_weights(OlmoeSparseMoeBlock(config)).cuda().float() + x = torch.randn(1, 8, config.hidden_size, device="cuda") + x_flat = x.view(-1, config.hidden_size) + + # Compute reference BEFORE kernelizing + with torch.no_grad(): + _, rw, sel = moe.gate(x_flat) + ref_out = _reference_moe_forward( + x_flat, + moe.experts.gate_up_proj, + moe.experts.down_proj, + moe.experts.act_fn, + sel, + rw, + E, + ).view(1, 8, config.hidden_size) + + # Set up kernel mapping + self._setup_kernels( + LocalLayerRepository, + Mode, + register_kernel_mapping, + replace_kernel_forward_from_hub, + ) + + # Kernelize the model + kernelize(moe, mode=Mode.TRAINING, device="cuda") + + # Forward through kernelized model + with torch.no_grad(): + kern_out = moe(x) + + torch.testing.assert_close(kern_out, ref_out, atol=1e-3, rtol=1e-3) + + def test_lora_forward_via_kernelize(self): + """Kernelized OlmoeSparseMoeBlock with peft LoRA matches reference.""" + ( + LocalLayerRepository, + Mode, + register_kernel_mapping, + replace_kernel_forward_from_hub, + kernelize, + ) = self._get_kernelize_imports() + + config = make_olmoe_config(use_full=False) + r = 4 + + # Create peft model + torch.manual_seed(42) + model = MinimalOLMoEModel(config).cuda().float() + lora_config = LoraConfig( + r=r, + lora_alpha=16, + target_modules=[], + target_parameters=["experts.gate_up_proj", "experts.down_proj"], + bias="none", + ) + peft_model = get_peft_model(model, lora_config) + + x = torch.randn(1, 8, config.hidden_size, device="cuda") + + # Reference: peft's own forward (uses _activate_lora context manager) + with torch.no_grad(): + ref_out = peft_model(x) + + # Set up kernel mapping + self._setup_kernels( + LocalLayerRepository, + Mode, + register_kernel_mapping, + replace_kernel_forward_from_hub, + ) + + # Kernelize the MoE block inside the peft model + base_moe = peft_model.base_model.model.moe + kernelize(base_moe, mode=Mode.TRAINING, device="cuda") + + # Forward through kernelized peft model + with torch.no_grad(): + kern_out = peft_model(x) + + torch.testing.assert_close(kern_out, ref_out, atol=5e-3, rtol=5e-3) + + def test_gate_lora_forward_via_kernelize(self): + """Kernelized forward with gate LoRA matches peft reference.""" + ( + LocalLayerRepository, + Mode, + register_kernel_mapping, + replace_kernel_forward_from_hub, + kernelize, + ) = self._get_kernelize_imports() + + config = make_olmoe_config(use_full=False) + r = 4 + + # Create peft model with gate + experts LoRA + torch.manual_seed(42) + model = MinimalOLMoEModel(config).cuda().float() + lora_config = LoraConfig( + r=r, + lora_alpha=16, + target_modules=[], + target_parameters=[ + "gate.weight", + "experts.gate_up_proj", + "experts.down_proj", + ], + bias="none", + ) + peft_model = get_peft_model(model, lora_config) + + x = torch.randn(1, 8, config.hidden_size, device="cuda") + + # Reference: peft's own forward + with torch.no_grad(): + ref_out = peft_model(x) + + # Set up kernel mapping + self._setup_kernels( + LocalLayerRepository, + Mode, + register_kernel_mapping, + replace_kernel_forward_from_hub, + ) + + # Kernelize the MoE block inside the peft model + base_moe = peft_model.base_model.model.moe + kernelize(base_moe, mode=Mode.TRAINING, device="cuda") + + # Forward through kernelized peft model + with torch.no_grad(): + kern_out = peft_model(x) + + torch.testing.assert_close(kern_out, ref_out, atol=5e-3, rtol=5e-3) + + +# ============================================================================= +# Tests: Shared expert handling +# ============================================================================= + + +class TestSharedExpertHandling: + """Test that HFScatterMoEGatedMLP.forward handles shared experts.""" + + @staticmethod + def _make_shared_expert_block(config): + """Create an OlmoeSparseMoeBlock with a mock shared expert attached.""" + moe = OlmoeSparseMoeBlock(config) + _init_expert_weights(moe) + + hidden = config.hidden_size + inter = config.intermediate_size + + # Attach a simple shared expert MLP (mimics Qwen2MoE structure) + class SharedExpertMLP(nn.Module): + def __init__(self, hidden_size, intermediate_size): + super().__init__() + self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False) + self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False) + self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False) + self.act_fn = nn.SiLU() + + def forward(self, x): + return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) + + moe.shared_expert = SharedExpertMLP(hidden, inter) + moe.shared_expert_gate = nn.Linear(hidden, 1, bias=False) + + return moe + + def test_shared_expert_is_used(self): + """Verify shared expert output affects final result.""" + config = make_olmoe_config(use_full=False) + moe = self._make_shared_expert_block(config) + + # Compute reference without shared expert + torch.manual_seed(42) + x = torch.randn(1, 4, config.hidden_size) + x_flat = x.view(-1, config.hidden_size) + + with torch.no_grad(): + # Shared expert contribution + shared_out = moe.shared_expert(x_flat) + gate_val = F.sigmoid(moe.shared_expert_gate(x_flat)) + shared_contribution = shared_out * gate_val + + # Verify shared expert produces non-zero output + assert shared_contribution.abs().max() > 0 + + @requires_cuda + def test_shared_expert_forward_via_kernelize(self): + """Kernelized forward with shared expert matches manual reference.""" + try: + from kernels import ( + LocalLayerRepository, + Mode, + kernelize, + register_kernel_mapping, + replace_kernel_forward_from_hub, + ) + except ImportError: + pytest.skip("kernels library not installed") + + config = make_olmoe_config(use_full=False) + E = config.num_experts + + torch.manual_seed(42) + moe = self._make_shared_expert_block(config).cuda().float() + x = torch.randn(1, 8, config.hidden_size, device="cuda") + x_flat = x.view(-1, config.hidden_size) + + # Compute reference: per-expert + shared expert + with torch.no_grad(): + _, rw, sel = moe.gate(x_flat) + + expert_out = _reference_moe_forward( + x_flat, + moe.experts.gate_up_proj, + moe.experts.down_proj, + moe.experts.act_fn, + sel, + rw, + E, + ) + shared_out = moe.shared_expert(x_flat) + gate_val = F.sigmoid(moe.shared_expert_gate(x_flat)) + ref_out = (expert_out + shared_out * gate_val).view( + 1, 8, config.hidden_size + ) + + # Kernelize + repo_path = ( + Path(__file__).parent.parent.parent + / "src" + / "axolotl" + / "integrations" + / "kernels" + / "libs" + / "scattermoe_lora" + ) + local_repo = LocalLayerRepository( + repo_path=repo_path, + package_name="scattermoe_lora", + layer_name="HFScatterMoEGatedMLP", + ) + + replace_kernel_forward_from_hub( + OlmoeSparseMoeBlock, "HFScatterMoEParallelExperts" + ) + register_kernel_mapping( + { + "HFScatterMoEParallelExperts": { + "cuda": { + Mode.TRAINING: local_repo, + Mode.INFERENCE: local_repo, + }, + } + } + ) + + kernelize(moe, mode=Mode.TRAINING, device="cuda") + + with torch.no_grad(): + kern_out = moe(x) + + torch.testing.assert_close(kern_out, ref_out, atol=1e-3, rtol=1e-3) From 43b1c80aa644c0f65b58952e8262321b0b27b489 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 7 Mar 2026 07:09:24 -0500 Subject: [PATCH 1177/1405] load weights synchronously so they can be converted and not OOM: (#3477) --- src/axolotl/monkeypatch/moe_quant.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/axolotl/monkeypatch/moe_quant.py b/src/axolotl/monkeypatch/moe_quant.py index 42beec6a9c..a8ad4c371c 100644 --- a/src/axolotl/monkeypatch/moe_quant.py +++ b/src/axolotl/monkeypatch/moe_quant.py @@ -7,6 +7,8 @@ reducing peak VRAM from "all experts in bf16" to "one expert at a time." """ +import os + import bitsandbytes as bnb import torch import torch.nn.utils.parametrize as P @@ -101,6 +103,14 @@ def patch_moe_quantization_on_load(cfg): _moe_load_state["quant_type"] = quant_type _moe_load_state["compress_statistics"] = compress_statistics + # Disable async tensor loading. Transformers' convert_and_load_state_dict_in_model + # uses a ThreadPoolExecutor to materialise tensors (move from safetensors → CUDA) + # ahead of time. With MoE models this pre-fetches many large bf16 expert tensors + # onto the GPU simultaneously — long before our set_param_for_module patch can + # quantise and free them one-by-one — causing OOM even at <5 % of weights loaded. + # Sequential loading ensures only ONE bf16 expert tensor is on-GPU at a time. + os.environ["HF_DEACTIVATE_ASYNC_LOAD"] = "1" + # Disable caching_allocator_warmup — it pre-allocates a huge tensor at bf16 # size for all params, defeating our on-load quantization VRAM savings. def _noop_warmup(*args, **kwargs): From cf4d550c888549430d1e8325865cf2de09a4163f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 9 Mar 2026 19:04:31 +0700 Subject: [PATCH 1178/1405] fix: reduce permissions for preview docs CI (#3480) [skip ci] --- .github/workflows/preview-docs.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 6049981308..356ed956ca 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -14,14 +14,8 @@ on: - .github/workflows/preview-docs.yml permissions: - checks: write - contents: write - deployments: write - issues: write - discussions: write - pages: write + contents: read pull-requests: write - statuses: write jobs: preview: From 23ad40bdd5393d0f785de3de85fd41391f8f4245 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 11 Mar 2026 13:18:27 +0700 Subject: [PATCH 1179/1405] fix: disable async load when loading quantized bnb --- src/axolotl/loaders/patch_manager.py | 6 ++++++ src/axolotl/monkeypatch/moe_quant.py | 10 ---------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index f94626ec7e..f516166c3c 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -93,6 +93,7 @@ def has_flash_attn(self) -> bool: def apply_pre_model_load_patches(self): """Apply pre-model load patches based on config.""" + self._deactivate_hf_async_load() self._apply_transformers_patches() # self._apply_flex_attention_patches() self._apply_flash_attention_patches() @@ -409,6 +410,11 @@ def _apply_fsdp2_bnb_patches(self): if self.cfg.load_in_8bit: apply_linear8bitlt_save_patch() + def _deactivate_hf_async_load(self): + """Load weights synchronously so they can be converted and not OOM.""" + if self.cfg.load_in_4bit or self.cfg.load_in_8bit: + os.environ["HF_DEACTIVATE_ASYNC_LOAD"] = "1" + def _apply_moe_expert_quantization_patch(self): """Patch transformers weight loading to quantize MoE expert params on-the-fly.""" if not self.cfg.quantize_moe_experts: diff --git a/src/axolotl/monkeypatch/moe_quant.py b/src/axolotl/monkeypatch/moe_quant.py index a8ad4c371c..42beec6a9c 100644 --- a/src/axolotl/monkeypatch/moe_quant.py +++ b/src/axolotl/monkeypatch/moe_quant.py @@ -7,8 +7,6 @@ reducing peak VRAM from "all experts in bf16" to "one expert at a time." """ -import os - import bitsandbytes as bnb import torch import torch.nn.utils.parametrize as P @@ -103,14 +101,6 @@ def patch_moe_quantization_on_load(cfg): _moe_load_state["quant_type"] = quant_type _moe_load_state["compress_statistics"] = compress_statistics - # Disable async tensor loading. Transformers' convert_and_load_state_dict_in_model - # uses a ThreadPoolExecutor to materialise tensors (move from safetensors → CUDA) - # ahead of time. With MoE models this pre-fetches many large bf16 expert tensors - # onto the GPU simultaneously — long before our set_param_for_module patch can - # quantise and free them one-by-one — causing OOM even at <5 % of weights loaded. - # Sequential loading ensures only ONE bf16 expert tensor is on-GPU at a time. - os.environ["HF_DEACTIVATE_ASYNC_LOAD"] = "1" - # Disable caching_allocator_warmup — it pre-allocates a huge tensor at bf16 # size for all params, defeating our on-load quantization VRAM savings. def _noop_warmup(*args, **kwargs): From fccc712daeb3f8811ca0beaba6769b99a57c94ba Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 11 Mar 2026 20:09:03 -0400 Subject: [PATCH 1180/1405] builds for py312-cu128-torch2.9.1 (#3489) --- .github/workflows/base.yml | 8 ++++++++ .github/workflows/main.yml | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 10326fba90..29842bcb53 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -173,6 +173,14 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" + - cuda: "128" + cuda_version: 12.8.1 + cudnn_version: "" + python_version: "3.12" + pytorch: 2.9.1 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-uv-base" + platforms: "linux/amd64,linux/arm64" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aca9b1dd36..4f772381f0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -110,6 +110,12 @@ jobs: pytorch: 2.9.1 axolotl_extras: platforms: "linux/amd64,linux/arm64" + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.12" + pytorch: 2.9.1 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" is_latest: true - cuda: 128 cuda_version: 12.8.1 @@ -193,6 +199,12 @@ jobs: python_version: "3.11" pytorch: 2.9.1 axolotl_extras: + platforms: "linux/amd64,linux/arm64" + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.12" + pytorch: 2.9.1 + axolotl_extras: is_latest: true platforms: "linux/amd64,linux/arm64" - cuda: 128 From 819b157c7b4c98cfd258ebaa1780ac2c77d9c962 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 11 Mar 2026 21:45:13 -0400 Subject: [PATCH 1181/1405] swap around what we're building for docker (#3490) * remove cloud configuration we don't base image for * but we do want it for uv --- .github/workflows/main.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4f772381f0..580c4047c3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -180,6 +180,7 @@ jobs: if: ${{ ! contains(github.event.commits[0].message, '[skip docker]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... strategy: + fail-fast: false matrix: include: - cuda: 128 @@ -199,12 +200,6 @@ jobs: python_version: "3.11" pytorch: 2.9.1 axolotl_extras: - platforms: "linux/amd64,linux/arm64" - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.12" - pytorch: 2.9.1 - axolotl_extras: is_latest: true platforms: "linux/amd64,linux/arm64" - cuda: 128 @@ -271,6 +266,7 @@ jobs: if: ${{ ! contains(github.event.commits[0].message, '[skip docker]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... strategy: + fail-fast: false matrix: include: - cuda: 128 @@ -278,6 +274,12 @@ jobs: python_version: "3.11" pytorch: 2.9.1 axolotl_extras: + platforms: "linux/amd64,linux/arm64" + - cuda: 128 + cuda_version: 12.8.1 + python_version: "3.12" + pytorch: 2.9.1 + axolotl_extras: is_latest: true platforms: "linux/amd64,linux/arm64" - cuda: 128 @@ -338,6 +340,7 @@ jobs: if: ${{ ! contains(github.event.commits[0].message, '[skip docker]') && github.repository_owner == 'axolotl-ai-cloud' }} # this job needs to be run on self-hosted GPU runners... strategy: + fail-fast: false matrix: include: - cuda: 128 From 79908b3c6eb3673f3131d8c6b77ff789556ab327 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 12 Mar 2026 20:41:13 -0400 Subject: [PATCH 1182/1405] use ubuntu user instead of root for uv docker images (#3491) --- docker/Dockerfile-cloud-uv | 21 +++++++++++++-------- docker/Dockerfile-uv | 10 +++++++--- docker/Dockerfile-uv-base | 12 +++++++++--- scripts/cloud-entrypoint.sh | 23 +++++++++++++++++------ 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/docker/Dockerfile-cloud-uv b/docker/Dockerfile-cloud-uv index a53dd61355..c38b21e6f2 100644 --- a/docker/Dockerfile-cloud-uv +++ b/docker/Dockerfile-cloud-uv @@ -1,6 +1,8 @@ ARG BASE_TAG=main FROM axolotlai/axolotl-uv:$BASE_TAG +USER root + ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" ENV HF_HUB_CACHE="/workspace/data/huggingface-cache/hub" ENV HF_HOME="/workspace/data/huggingface-cache/hub" @@ -9,7 +11,7 @@ ENV HF_HUB_ENABLE_HF_TRANSFER="1" EXPOSE 8888 EXPOSE 22 -COPY scripts/cloud-entrypoint.sh /root/cloud-entrypoint.sh +COPY scripts/cloud-entrypoint.sh /home/ubuntu/cloud-entrypoint.sh COPY scripts/motd /etc/motd RUN uv pip install jupyterlab notebook ipywidgets && \ @@ -18,13 +20,16 @@ RUN apt update && \ apt install --yes --no-install-recommends openssh-server tmux iproute2 nvtop && \ rm -rf /var/cache/apt/archives && \ rm -rf /var/lib/apt/lists/* && \ - mkdir -p ~/.ssh && \ - chmod 700 ~/.ssh && \ - printf "\n[[ -z \"\$TMUX\" ]] && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> ~/.bashrc && \ - printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> ~/.bashrc && \ + mkdir -p /home/ubuntu/.ssh && \ + chmod 700 /home/ubuntu/.ssh && \ + printf "\n[[ -z \"\$TMUX\" ]] && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> /home/ubuntu/.bashrc && \ + printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> /home/ubuntu/.bashrc && \ chmod +x /workspace/axolotl/scripts/cloud-entrypoint.sh && \ - chmod +x /root/cloud-entrypoint.sh && \ - echo 'set-option -g history-limit 5000' >> ~/.tmux.conf + chmod +x /home/ubuntu/cloud-entrypoint.sh && \ + echo 'set-option -g history-limit 5000' >> /home/ubuntu/.tmux.conf && \ + chown -R ubuntu:ubuntu /home/ubuntu /workspace + +USER ubuntu -ENTRYPOINT ["/root/cloud-entrypoint.sh"] +ENTRYPOINT ["/home/ubuntu/cloud-entrypoint.sh"] CMD ["sleep", "infinity"] diff --git a/docker/Dockerfile-uv b/docker/Dockerfile-uv index 0142c0d2d3..f149d34105 100644 --- a/docker/Dockerfile-uv +++ b/docker/Dockerfile-uv @@ -43,6 +43,10 @@ RUN git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && \ git config --get remote.origin.fetch && \ git config --global credential.helper store -COPY .axolotl-complete.bash /root/.axolotl-complete.bash -RUN chmod +x /root/.axolotl-complete.bash && \ - echo 'source /root/.axolotl-complete.bash' >> ~/.bashrc +COPY .axolotl-complete.bash /home/ubuntu/.axolotl-complete.bash +RUN chmod +x /home/ubuntu/.axolotl-complete.bash && \ + echo 'source /home/ubuntu/.axolotl-complete.bash' >> /home/ubuntu/.bashrc + +RUN chown -R ubuntu:ubuntu /workspace /home/ubuntu + +USER ubuntu diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index 0e7acbe29a..e6da9ac84e 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -17,11 +17,15 @@ ENV TORCH_CUDA_ARCH_LIST=$TORCH_CUDA_ARCH_LIST ENV UV_TORCH_BACKEND="cu${CUDA}" RUN apt-get update \ - && apt-get install -y wget git build-essential ninja-build git-lfs libaio-dev pkg-config curl && rm -rf /var/lib/apt/lists/* \ + && apt-get install -y wget git build-essential ninja-build git-lfs libaio-dev pkg-config curl sudo && rm -rf /var/lib/apt/lists/* \ && git lfs install --skip-repo \ - && curl -LsSf https://astral.sh/uv/install.sh | sh + && curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/usr/local/bin" sh -ENV PATH="/root/.local/bin:${PATH}" +# Create ubuntu user with passwordless sudo +RUN useradd -m -s /bin/bash -u 1000 ubuntu 2>/dev/null; \ + usermod -aG sudo ubuntu && \ + echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ubuntu && \ + chmod 0440 /etc/sudoers.d/ubuntu RUN uv python install ${PYTHON_VERSION} @@ -55,3 +59,5 @@ RUN PYTHON_CP="cp$(echo $PYTHON_VERSION | tr -d '.')" && \ wget -nv "https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/${WHL_VERSION}/${WHL_FILE}" && \ uv pip install --no-cache-dir "${WHL_FILE}" && \ rm "${WHL_FILE}" + +RUN chown -R ubuntu:ubuntu /workspace diff --git a/scripts/cloud-entrypoint.sh b/scripts/cloud-entrypoint.sh index c98e7c0d0b..e88762e13c 100755 --- a/scripts/cloud-entrypoint.sh +++ b/scripts/cloud-entrypoint.sh @@ -1,8 +1,15 @@ #!/bin/bash +# Detect if running as non-root and set sudo prefix accordingly +if [ "$(id -u)" -ne 0 ]; then + SUDO="sudo" +else + SUDO="" +fi + # Export specific ENV variables to /etc/rp_environment echo "Exporting environment variables..." -printenv | grep -E '^HF_|^BNB_|^CUDA_|^NCCL_|^NV|^RUNPOD_|^PATH=|^_=' | sed 's/^\([^=]*\)=\(.*\)$/export \1="\2"/' | grep -v 'printenv' >> /etc/rp_environment +printenv | grep -E '^HF_|^BNB_|^CUDA_|^NCCL_|^NV|^RUNPOD_|^PATH=|^_=' | sed 's/^\([^=]*\)=\(.*\)$/export \1="\2"/' | grep -v 'printenv' | $SUDO tee /etc/rp_environment > /dev/null echo 'source /etc/rp_environment' >> ~/.bashrc add_keys_to_authorized() { @@ -46,19 +53,19 @@ add_keys_to_authorized() { # Set SSH port if [ ! -z "$SSH_PORT" ]; then - sed -i "s/#Port 22/Port $SSH_PORT/" /etc/ssh/sshd_config + $SUDO sed -i "s/#Port 22/Port $SSH_PORT/" /etc/ssh/sshd_config fi if [[ $PUBLIC_KEY ]]; then # runpod, prime intellect add_keys_to_authorized "$PUBLIC_KEY" # Start the SSH service in the background - service ssh start + $SUDO service ssh start elif [[ $SSH_KEY ]]; then # latitude.sh add_keys_to_authorized "$SSH_KEY" # Start the SSH service in the background - service ssh start + $SUDO service ssh start else echo "No PUBLIC_KEY or SSH_KEY environment variable provided, not starting openSSH daemon" fi @@ -71,7 +78,11 @@ fi if [ "$JUPYTER_DISABLE" != "1" ]; then # Run Jupyter Lab in the background - jupyter lab --port=8888 --ip=* --allow-root --ServerApp.allow_origin=* & + JUPYTER_ARGS="--port=8888 --ip=* --ServerApp.allow_origin=*" + if [ "$(id -u)" -eq 0 ]; then + JUPYTER_ARGS="$JUPYTER_ARGS --allow-root" + fi + jupyter lab $JUPYTER_ARGS & fi if [ ! -d "/workspace/data/axolotl-artifacts" ]; then @@ -86,7 +97,7 @@ SLURM_INIT="${SLURM_INIT:-/slurm-init.sh}" if [[ -f "$SLURM_INIT" ]]; then echo "[entrypoint] running $SLURM_INIT..." - bash "$SLURM_INIT" + $SUDO bash "$SLURM_INIT" fi # Execute the passed arguments (CMD) From 083c5a04219b7b2f466ed701ebf496d3c5bbd8ba Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 12 Mar 2026 23:20:54 -0400 Subject: [PATCH 1183/1405] check ubuntu user and set uv python dir (#3492) --- docker/Dockerfile-uv | 8 ++++++++ docker/Dockerfile-uv-base | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile-uv b/docker/Dockerfile-uv index f149d34105..ac03d1695e 100644 --- a/docker/Dockerfile-uv +++ b/docker/Dockerfile-uv @@ -47,6 +47,14 @@ COPY .axolotl-complete.bash /home/ubuntu/.axolotl-complete.bash RUN chmod +x /home/ubuntu/.axolotl-complete.bash && \ echo 'source /home/ubuntu/.axolotl-complete.bash' >> /home/ubuntu/.bashrc +# Ensure ubuntu user exists (may already exist from base image) +RUN id ubuntu &>/dev/null || ( \ + useradd -m -s /bin/bash -u 1000 ubuntu && \ + apt-get update && apt-get install -y --no-install-recommends sudo && rm -rf /var/lib/apt/lists/* \ + ); \ + echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ubuntu && \ + chmod 0440 /etc/sudoers.d/ubuntu + RUN chown -R ubuntu:ubuntu /workspace /home/ubuntu USER ubuntu diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index e6da9ac84e..378b30a033 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -27,7 +27,9 @@ RUN useradd -m -s /bin/bash -u 1000 ubuntu 2>/dev/null; \ echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ubuntu && \ chmod 0440 /etc/sudoers.d/ubuntu -RUN uv python install ${PYTHON_VERSION} +ENV UV_PYTHON_INSTALL_DIR="/opt/uv/python" +RUN uv python install ${PYTHON_VERSION} && \ + chmod -R a+rX /opt/uv WORKDIR /workspace From e1ff756245c16a91fd25e04c0774dd715cc3ec3b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 13 Mar 2026 09:06:54 -0400 Subject: [PATCH 1184/1405] become the ubuntu user when root logs in (#3494) --- docker/Dockerfile-cloud-uv | 12 +++---- scripts/cloud-entrypoint.sh | 67 +++++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/docker/Dockerfile-cloud-uv b/docker/Dockerfile-cloud-uv index c38b21e6f2..d2af554852 100644 --- a/docker/Dockerfile-cloud-uv +++ b/docker/Dockerfile-cloud-uv @@ -11,7 +11,7 @@ ENV HF_HUB_ENABLE_HF_TRANSFER="1" EXPOSE 8888 EXPOSE 22 -COPY scripts/cloud-entrypoint.sh /home/ubuntu/cloud-entrypoint.sh +COPY scripts/cloud-entrypoint.sh /etc/cloud-entrypoint.sh COPY scripts/motd /etc/motd RUN uv pip install jupyterlab notebook ipywidgets && \ @@ -22,14 +22,14 @@ RUN apt update && \ rm -rf /var/lib/apt/lists/* && \ mkdir -p /home/ubuntu/.ssh && \ chmod 700 /home/ubuntu/.ssh && \ - printf "\n[[ -z \"\$TMUX\" ]] && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> /home/ubuntu/.bashrc && \ + printf "\n[[ -z \"\$TMUX\" ]] && tty -s && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> /home/ubuntu/.bashrc && \ printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> /home/ubuntu/.bashrc && \ - chmod +x /workspace/axolotl/scripts/cloud-entrypoint.sh && \ - chmod +x /home/ubuntu/cloud-entrypoint.sh && \ + printf "\n[[ -z \"\$AXOLOTL_SKIP_SWITCH\" ]] && exec sudo -u ubuntu AXOLOTL_SKIP_SWITCH=1 -i\n" >> /root/.bashrc && \ + chmod +x /etc/cloud-entrypoint.sh && \ echo 'set-option -g history-limit 5000' >> /home/ubuntu/.tmux.conf && \ chown -R ubuntu:ubuntu /home/ubuntu /workspace -USER ubuntu +# USER ubuntu -ENTRYPOINT ["/home/ubuntu/cloud-entrypoint.sh"] +ENTRYPOINT ["/etc/cloud-entrypoint.sh"] CMD ["sleep", "infinity"] diff --git a/scripts/cloud-entrypoint.sh b/scripts/cloud-entrypoint.sh index e88762e13c..8eb056acbc 100755 --- a/scripts/cloud-entrypoint.sh +++ b/scripts/cloud-entrypoint.sh @@ -3,24 +3,35 @@ # Detect if running as non-root and set sudo prefix accordingly if [ "$(id -u)" -ne 0 ]; then SUDO="sudo" + RUN_AS_USER="" else SUDO="" + RUN_AS_USER="sudo -u ubuntu" fi # Export specific ENV variables to /etc/rp_environment echo "Exporting environment variables..." printenv | grep -E '^HF_|^BNB_|^CUDA_|^NCCL_|^NV|^RUNPOD_|^PATH=|^_=' | sed 's/^\([^=]*\)=\(.*\)$/export \1="\2"/' | grep -v 'printenv' | $SUDO tee /etc/rp_environment > /dev/null -echo 'source /etc/rp_environment' >> ~/.bashrc + +# Add rp_environment sourcing to ubuntu's bashrc (if ubuntu user exists and line not already present) +if id ubuntu &>/dev/null; then + grep -q 'source /etc/rp_environment' /home/ubuntu/.bashrc 2>/dev/null || \ + echo 'source /etc/rp_environment' >> /home/ubuntu/.bashrc +fi +# Also add to current user's bashrc if different from ubuntu +grep -q 'source /etc/rp_environment' ~/.bashrc 2>/dev/null || \ + echo 'source /etc/rp_environment' >> ~/.bashrc add_keys_to_authorized() { local key_value=$1 + local target_home=$2 - # Create the ~/.ssh directory and set permissions - mkdir -p ~/.ssh - chmod 700 ~/.ssh + # Create the .ssh directory and set permissions + mkdir -p "$target_home/.ssh" + chmod 700 "$target_home/.ssh" # Create the authorized_keys file if it doesn't exist - touch ~/.ssh/authorized_keys + touch "$target_home/.ssh/authorized_keys" # Initialize an empty key variable local key="" @@ -31,7 +42,7 @@ add_keys_to_authorized() { if [[ $word == ssh-* ]]; then # If there's a key being built, add it to the authorized_keys file if [[ -n $key ]]; then - echo $key >> ~/.ssh/authorized_keys + echo $key >> "$target_home/.ssh/authorized_keys" fi # Start a new key key=$word @@ -43,12 +54,25 @@ add_keys_to_authorized() { # Add the last key to the authorized_keys file if [[ -n $key ]]; then - echo $key >> ~/.ssh/authorized_keys + echo $key >> "$target_home/.ssh/authorized_keys" fi # Set the correct permissions - chmod 600 ~/.ssh/authorized_keys - chmod 700 -R ~/.ssh + chmod 600 "$target_home/.ssh/authorized_keys" + chmod 700 -R "$target_home/.ssh" +} + +setup_ssh_keys() { + local key_value=$1 + + # Set up keys for the current user + add_keys_to_authorized "$key_value" "$HOME" + + # Also set up keys for ubuntu user if we're root and ubuntu exists + if [ "$(id -u)" -eq 0 ] && id ubuntu &>/dev/null; then + add_keys_to_authorized "$key_value" "/home/ubuntu" + chown -R ubuntu:ubuntu /home/ubuntu/.ssh + fi } # Set SSH port @@ -58,12 +82,12 @@ fi if [[ $PUBLIC_KEY ]]; then # runpod, prime intellect - add_keys_to_authorized "$PUBLIC_KEY" + setup_ssh_keys "$PUBLIC_KEY" # Start the SSH service in the background $SUDO service ssh start elif [[ $SSH_KEY ]]; then # latitude.sh - add_keys_to_authorized "$SSH_KEY" + setup_ssh_keys "$SSH_KEY" # Start the SSH service in the background $SUDO service ssh start else @@ -77,12 +101,16 @@ if [ -n "$JUPYTER_PASSWORD" ]; then fi if [ "$JUPYTER_DISABLE" != "1" ]; then - # Run Jupyter Lab in the background + # Run Jupyter Lab as ubuntu user when possible JUPYTER_ARGS="--port=8888 --ip=* --ServerApp.allow_origin=*" - if [ "$(id -u)" -eq 0 ]; then - JUPYTER_ARGS="$JUPYTER_ARGS --allow-root" + if [ "$(id -u)" -eq 0 ] && id ubuntu &>/dev/null; then + sudo -u ubuntu bash -c "JUPYTER_TOKEN='$JUPYTER_TOKEN' jupyter lab $JUPYTER_ARGS" & + else + if [ "$(id -u)" -eq 0 ]; then + JUPYTER_ARGS="$JUPYTER_ARGS --allow-root" + fi + jupyter lab $JUPYTER_ARGS & fi - jupyter lab $JUPYTER_ARGS & fi if [ ! -d "/workspace/data/axolotl-artifacts" ]; then @@ -91,6 +119,7 @@ fi if [ ! -L "/workspace/axolotl/outputs" ]; then ln -sf /workspace/data/axolotl-artifacts /workspace/axolotl/outputs fi +chown -R ubuntu:ubuntu /workspace 2>/dev/null || true # start the runpod slurm init SLURM_INIT="${SLURM_INIT:-/slurm-init.sh}" @@ -100,5 +129,9 @@ if [[ -f "$SLURM_INIT" ]]; then $SUDO bash "$SLURM_INIT" fi -# Execute the passed arguments (CMD) -exec "$@" +# Execute the passed arguments (CMD) as ubuntu when possible +if [ "$(id -u)" -eq 0 ] && id ubuntu &>/dev/null; then + exec sudo -u ubuntu "$@" +else + exec "$@" +fi From ff77fa24889406a5f31b1e6d260f67e442a8f816 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 13 Mar 2026 10:19:34 -0400 Subject: [PATCH 1185/1405] preserve env for root -> ubuntu user (#3495) --- scripts/cloud-entrypoint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/cloud-entrypoint.sh b/scripts/cloud-entrypoint.sh index 8eb056acbc..37105cd312 100755 --- a/scripts/cloud-entrypoint.sh +++ b/scripts/cloud-entrypoint.sh @@ -104,7 +104,7 @@ if [ "$JUPYTER_DISABLE" != "1" ]; then # Run Jupyter Lab as ubuntu user when possible JUPYTER_ARGS="--port=8888 --ip=* --ServerApp.allow_origin=*" if [ "$(id -u)" -eq 0 ] && id ubuntu &>/dev/null; then - sudo -u ubuntu bash -c "JUPYTER_TOKEN='$JUPYTER_TOKEN' jupyter lab $JUPYTER_ARGS" & + sudo --preserve-env=PATH,JUPYTER_TOKEN -u ubuntu jupyter lab $JUPYTER_ARGS & else if [ "$(id -u)" -eq 0 ]; then JUPYTER_ARGS="$JUPYTER_ARGS --allow-root" @@ -131,7 +131,7 @@ fi # Execute the passed arguments (CMD) as ubuntu when possible if [ "$(id -u)" -eq 0 ] && id ubuntu &>/dev/null; then - exec sudo -u ubuntu "$@" + exec sudo --preserve-env=PATH -u ubuntu "$@" else exec "$@" fi From d8a05744d741347dd4e21d35a2ce3ec480c4f7ea Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 13 Mar 2026 11:54:09 -0400 Subject: [PATCH 1186/1405] Reverts commits 79908b3c6, 083c5a042, e1ff75624, ff77fa248. (#3496) The non-root user approach had multiple issues with RunPod compatibility, sudo PATH handling, and tmux in exec sessions. Restoring root as the default user for now. --- docker/Dockerfile-cloud-uv | 23 ++++------ docker/Dockerfile-uv | 18 ++------ docker/Dockerfile-uv-base | 16 ++----- scripts/cloud-entrypoint.sh | 84 +++++++++---------------------------- 4 files changed, 36 insertions(+), 105 deletions(-) diff --git a/docker/Dockerfile-cloud-uv b/docker/Dockerfile-cloud-uv index d2af554852..a53dd61355 100644 --- a/docker/Dockerfile-cloud-uv +++ b/docker/Dockerfile-cloud-uv @@ -1,8 +1,6 @@ ARG BASE_TAG=main FROM axolotlai/axolotl-uv:$BASE_TAG -USER root - ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" ENV HF_HUB_CACHE="/workspace/data/huggingface-cache/hub" ENV HF_HOME="/workspace/data/huggingface-cache/hub" @@ -11,7 +9,7 @@ ENV HF_HUB_ENABLE_HF_TRANSFER="1" EXPOSE 8888 EXPOSE 22 -COPY scripts/cloud-entrypoint.sh /etc/cloud-entrypoint.sh +COPY scripts/cloud-entrypoint.sh /root/cloud-entrypoint.sh COPY scripts/motd /etc/motd RUN uv pip install jupyterlab notebook ipywidgets && \ @@ -20,16 +18,13 @@ RUN apt update && \ apt install --yes --no-install-recommends openssh-server tmux iproute2 nvtop && \ rm -rf /var/cache/apt/archives && \ rm -rf /var/lib/apt/lists/* && \ - mkdir -p /home/ubuntu/.ssh && \ - chmod 700 /home/ubuntu/.ssh && \ - printf "\n[[ -z \"\$TMUX\" ]] && tty -s && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> /home/ubuntu/.bashrc && \ - printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> /home/ubuntu/.bashrc && \ - printf "\n[[ -z \"\$AXOLOTL_SKIP_SWITCH\" ]] && exec sudo -u ubuntu AXOLOTL_SKIP_SWITCH=1 -i\n" >> /root/.bashrc && \ - chmod +x /etc/cloud-entrypoint.sh && \ - echo 'set-option -g history-limit 5000' >> /home/ubuntu/.tmux.conf && \ - chown -R ubuntu:ubuntu /home/ubuntu /workspace - -# USER ubuntu + mkdir -p ~/.ssh && \ + chmod 700 ~/.ssh && \ + printf "\n[[ -z \"\$TMUX\" ]] && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> ~/.bashrc && \ + printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> ~/.bashrc && \ + chmod +x /workspace/axolotl/scripts/cloud-entrypoint.sh && \ + chmod +x /root/cloud-entrypoint.sh && \ + echo 'set-option -g history-limit 5000' >> ~/.tmux.conf -ENTRYPOINT ["/etc/cloud-entrypoint.sh"] +ENTRYPOINT ["/root/cloud-entrypoint.sh"] CMD ["sleep", "infinity"] diff --git a/docker/Dockerfile-uv b/docker/Dockerfile-uv index ac03d1695e..0142c0d2d3 100644 --- a/docker/Dockerfile-uv +++ b/docker/Dockerfile-uv @@ -43,18 +43,6 @@ RUN git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && \ git config --get remote.origin.fetch && \ git config --global credential.helper store -COPY .axolotl-complete.bash /home/ubuntu/.axolotl-complete.bash -RUN chmod +x /home/ubuntu/.axolotl-complete.bash && \ - echo 'source /home/ubuntu/.axolotl-complete.bash' >> /home/ubuntu/.bashrc - -# Ensure ubuntu user exists (may already exist from base image) -RUN id ubuntu &>/dev/null || ( \ - useradd -m -s /bin/bash -u 1000 ubuntu && \ - apt-get update && apt-get install -y --no-install-recommends sudo && rm -rf /var/lib/apt/lists/* \ - ); \ - echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ubuntu && \ - chmod 0440 /etc/sudoers.d/ubuntu - -RUN chown -R ubuntu:ubuntu /workspace /home/ubuntu - -USER ubuntu +COPY .axolotl-complete.bash /root/.axolotl-complete.bash +RUN chmod +x /root/.axolotl-complete.bash && \ + echo 'source /root/.axolotl-complete.bash' >> ~/.bashrc diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index 378b30a033..0e7acbe29a 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -17,19 +17,13 @@ ENV TORCH_CUDA_ARCH_LIST=$TORCH_CUDA_ARCH_LIST ENV UV_TORCH_BACKEND="cu${CUDA}" RUN apt-get update \ - && apt-get install -y wget git build-essential ninja-build git-lfs libaio-dev pkg-config curl sudo && rm -rf /var/lib/apt/lists/* \ + && apt-get install -y wget git build-essential ninja-build git-lfs libaio-dev pkg-config curl && rm -rf /var/lib/apt/lists/* \ && git lfs install --skip-repo \ - && curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/usr/local/bin" sh + && curl -LsSf https://astral.sh/uv/install.sh | sh -# Create ubuntu user with passwordless sudo -RUN useradd -m -s /bin/bash -u 1000 ubuntu 2>/dev/null; \ - usermod -aG sudo ubuntu && \ - echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ubuntu && \ - chmod 0440 /etc/sudoers.d/ubuntu +ENV PATH="/root/.local/bin:${PATH}" -ENV UV_PYTHON_INSTALL_DIR="/opt/uv/python" -RUN uv python install ${PYTHON_VERSION} && \ - chmod -R a+rX /opt/uv +RUN uv python install ${PYTHON_VERSION} WORKDIR /workspace @@ -61,5 +55,3 @@ RUN PYTHON_CP="cp$(echo $PYTHON_VERSION | tr -d '.')" && \ wget -nv "https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/${WHL_VERSION}/${WHL_FILE}" && \ uv pip install --no-cache-dir "${WHL_FILE}" && \ rm "${WHL_FILE}" - -RUN chown -R ubuntu:ubuntu /workspace diff --git a/scripts/cloud-entrypoint.sh b/scripts/cloud-entrypoint.sh index 37105cd312..c98e7c0d0b 100755 --- a/scripts/cloud-entrypoint.sh +++ b/scripts/cloud-entrypoint.sh @@ -1,37 +1,19 @@ #!/bin/bash -# Detect if running as non-root and set sudo prefix accordingly -if [ "$(id -u)" -ne 0 ]; then - SUDO="sudo" - RUN_AS_USER="" -else - SUDO="" - RUN_AS_USER="sudo -u ubuntu" -fi - # Export specific ENV variables to /etc/rp_environment echo "Exporting environment variables..." -printenv | grep -E '^HF_|^BNB_|^CUDA_|^NCCL_|^NV|^RUNPOD_|^PATH=|^_=' | sed 's/^\([^=]*\)=\(.*\)$/export \1="\2"/' | grep -v 'printenv' | $SUDO tee /etc/rp_environment > /dev/null - -# Add rp_environment sourcing to ubuntu's bashrc (if ubuntu user exists and line not already present) -if id ubuntu &>/dev/null; then - grep -q 'source /etc/rp_environment' /home/ubuntu/.bashrc 2>/dev/null || \ - echo 'source /etc/rp_environment' >> /home/ubuntu/.bashrc -fi -# Also add to current user's bashrc if different from ubuntu -grep -q 'source /etc/rp_environment' ~/.bashrc 2>/dev/null || \ - echo 'source /etc/rp_environment' >> ~/.bashrc +printenv | grep -E '^HF_|^BNB_|^CUDA_|^NCCL_|^NV|^RUNPOD_|^PATH=|^_=' | sed 's/^\([^=]*\)=\(.*\)$/export \1="\2"/' | grep -v 'printenv' >> /etc/rp_environment +echo 'source /etc/rp_environment' >> ~/.bashrc add_keys_to_authorized() { local key_value=$1 - local target_home=$2 - # Create the .ssh directory and set permissions - mkdir -p "$target_home/.ssh" - chmod 700 "$target_home/.ssh" + # Create the ~/.ssh directory and set permissions + mkdir -p ~/.ssh + chmod 700 ~/.ssh # Create the authorized_keys file if it doesn't exist - touch "$target_home/.ssh/authorized_keys" + touch ~/.ssh/authorized_keys # Initialize an empty key variable local key="" @@ -42,7 +24,7 @@ add_keys_to_authorized() { if [[ $word == ssh-* ]]; then # If there's a key being built, add it to the authorized_keys file if [[ -n $key ]]; then - echo $key >> "$target_home/.ssh/authorized_keys" + echo $key >> ~/.ssh/authorized_keys fi # Start a new key key=$word @@ -54,42 +36,29 @@ add_keys_to_authorized() { # Add the last key to the authorized_keys file if [[ -n $key ]]; then - echo $key >> "$target_home/.ssh/authorized_keys" + echo $key >> ~/.ssh/authorized_keys fi # Set the correct permissions - chmod 600 "$target_home/.ssh/authorized_keys" - chmod 700 -R "$target_home/.ssh" -} - -setup_ssh_keys() { - local key_value=$1 - - # Set up keys for the current user - add_keys_to_authorized "$key_value" "$HOME" - - # Also set up keys for ubuntu user if we're root and ubuntu exists - if [ "$(id -u)" -eq 0 ] && id ubuntu &>/dev/null; then - add_keys_to_authorized "$key_value" "/home/ubuntu" - chown -R ubuntu:ubuntu /home/ubuntu/.ssh - fi + chmod 600 ~/.ssh/authorized_keys + chmod 700 -R ~/.ssh } # Set SSH port if [ ! -z "$SSH_PORT" ]; then - $SUDO sed -i "s/#Port 22/Port $SSH_PORT/" /etc/ssh/sshd_config + sed -i "s/#Port 22/Port $SSH_PORT/" /etc/ssh/sshd_config fi if [[ $PUBLIC_KEY ]]; then # runpod, prime intellect - setup_ssh_keys "$PUBLIC_KEY" + add_keys_to_authorized "$PUBLIC_KEY" # Start the SSH service in the background - $SUDO service ssh start + service ssh start elif [[ $SSH_KEY ]]; then # latitude.sh - setup_ssh_keys "$SSH_KEY" + add_keys_to_authorized "$SSH_KEY" # Start the SSH service in the background - $SUDO service ssh start + service ssh start else echo "No PUBLIC_KEY or SSH_KEY environment variable provided, not starting openSSH daemon" fi @@ -101,16 +70,8 @@ if [ -n "$JUPYTER_PASSWORD" ]; then fi if [ "$JUPYTER_DISABLE" != "1" ]; then - # Run Jupyter Lab as ubuntu user when possible - JUPYTER_ARGS="--port=8888 --ip=* --ServerApp.allow_origin=*" - if [ "$(id -u)" -eq 0 ] && id ubuntu &>/dev/null; then - sudo --preserve-env=PATH,JUPYTER_TOKEN -u ubuntu jupyter lab $JUPYTER_ARGS & - else - if [ "$(id -u)" -eq 0 ]; then - JUPYTER_ARGS="$JUPYTER_ARGS --allow-root" - fi - jupyter lab $JUPYTER_ARGS & - fi + # Run Jupyter Lab in the background + jupyter lab --port=8888 --ip=* --allow-root --ServerApp.allow_origin=* & fi if [ ! -d "/workspace/data/axolotl-artifacts" ]; then @@ -119,19 +80,14 @@ fi if [ ! -L "/workspace/axolotl/outputs" ]; then ln -sf /workspace/data/axolotl-artifacts /workspace/axolotl/outputs fi -chown -R ubuntu:ubuntu /workspace 2>/dev/null || true # start the runpod slurm init SLURM_INIT="${SLURM_INIT:-/slurm-init.sh}" if [[ -f "$SLURM_INIT" ]]; then echo "[entrypoint] running $SLURM_INIT..." - $SUDO bash "$SLURM_INIT" + bash "$SLURM_INIT" fi -# Execute the passed arguments (CMD) as ubuntu when possible -if [ "$(id -u)" -eq 0 ] && id ubuntu &>/dev/null; then - exec sudo --preserve-env=PATH -u ubuntu "$@" -else - exec "$@" -fi +# Execute the passed arguments (CMD) +exec "$@" From a806704e94b6f6d988072ccd073acc38724da5c8 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Mon, 16 Mar 2026 07:40:30 +0530 Subject: [PATCH 1187/1405] moe quant patch for merge miss match (#3483) * moe quant patch for merge miss match * lint * revert test + fix moe patch * comment fixxes * e2e tests * mismatch fixx tested * mis match fix wwith vllm compatablity + test * comment lint * fix: missing os import, duplicate no op * chore: simplify comments --------- Co-authored-by: NanoCode012 --- src/axolotl/loaders/patch_manager.py | 13 +- src/axolotl/monkeypatch/moe_quant.py | 124 ++++++++++++++---- .../schemas/validation/test_moe_quant.py | 116 ++++++++++++++++ 3 files changed, 220 insertions(+), 33 deletions(-) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index f516166c3c..857a2f76fc 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -416,16 +416,21 @@ def _deactivate_hf_async_load(self): os.environ["HF_DEACTIVATE_ASYNC_LOAD"] = "1" def _apply_moe_expert_quantization_patch(self): - """Patch transformers weight loading to quantize MoE expert params on-the-fly.""" - if not self.cfg.quantize_moe_experts: + """Patch transformers weight loading and PEFT for MoE expert quantization.""" + has_target_params = bool(getattr(self.cfg, "lora_target_parameters", None)) + + if not self.cfg.quantize_moe_experts and not has_target_params: return from axolotl.monkeypatch.moe_quant import ( - patch_moe_quantization_on_load, patch_peft_target_parameters_matching, ) - patch_moe_quantization_on_load(self.cfg) + if self.cfg.quantize_moe_experts: + from axolotl.monkeypatch.moe_quant import patch_moe_quantization_on_load + + patch_moe_quantization_on_load(self.cfg) + patch_peft_target_parameters_matching() def _finalize_moe_expert_quantization(self, model: PreTrainedModel): diff --git a/src/axolotl/monkeypatch/moe_quant.py b/src/axolotl/monkeypatch/moe_quant.py index 42beec6a9c..983da4a37e 100644 --- a/src/axolotl/monkeypatch/moe_quant.py +++ b/src/axolotl/monkeypatch/moe_quant.py @@ -1,11 +1,4 @@ -""" -Loading-time quantization for MoE expert weights stored as 3D nn.Parameter tensors. - -In transformers v5, MoE models store expert weights as fused 3D tensors that BnB -skips (only targets nn.Linear). This module patches weight loading to quantize them -on-the-fly (4-bit via bitsandbytes parametrize, 8-bit via custom int8 parametrization), -reducing peak VRAM from "all experts in bf16" to "one expert at a time." -""" +"""Loading-time quantization for MoE expert weights stored as 3D nn.Parameter tensors.""" import bitsandbytes as bnb import torch @@ -15,18 +8,20 @@ LOG = get_logger(__name__) -# Module-level state for the loading-time quantization patch. _moe_load_state = { "count": 0, "mode": "4bit", "quant_type": "nf4", "compress_statistics": True, "patched": False, + # Module path → param names in definition order, captured before quantization. + # Without this, alphabetical loading order would mismatch merge order. + "expert_param_order": {}, } class Bnb8bitParametrization(torch.nn.Module): - """Parametrization that dequantizes int8 row-wise quantized data on access.""" + """Dequantizes int8 row-wise quantized data on access.""" def __init__(self, row_stats: torch.Tensor): super().__init__() @@ -34,7 +29,7 @@ def __init__(self, row_stats: torch.Tensor): @torch.no_grad() def forward(self, quantized_param: torch.Tensor) -> torch.Tensor: - # Flatten 3D+ to 2D for BnB's dequant, then reshape back. + """Flatten 3D+ to 2D for BnB's dequant, then reshape back.""" orig_shape = quantized_param.shape if quantized_param.ndim > 2: quantized_param = quantized_param.reshape(-1, orig_shape[-1]) @@ -74,14 +69,11 @@ def replace_parameter_8bit(module, param_name): def patch_moe_quantization_on_load(cfg): - """Patch transformers' weight loading to quantize MoE expert params on-the-fly. - - Wraps ``set_param_for_module`` so that 3D+ CUDA tensors with "expert" in their - name are quantized (4-bit or 8-bit) as they're loaded, keeping peak VRAM low. - """ + """Patch transformers' weight loading to quantize MoE expert params on-the-fly.""" mode = "8bit" if getattr(cfg, "load_in_8bit", False) else "4bit" _moe_load_state["mode"] = mode _moe_load_state["count"] = 0 + _moe_load_state["expert_param_order"] = {} if _moe_load_state["patched"]: LOG.debug("MoE loading-time quantization patch already active") @@ -113,7 +105,6 @@ def _noop_warmup(*args, **kwargs): def _patched_set_param_for_module(model, target_name, param_value, *args, **kwargs): original_set_param(model, target_name, param_value, *args, **kwargs) - # Quantize 3D+ expert params that BnB skipped (only on CUDA). if param_value.ndim >= 3 and param_value.is_cuda: mod_path, _, pname = target_name.rpartition(".") mod = model.get_submodule(mod_path) if mod_path else model @@ -126,6 +117,13 @@ def _patched_set_param_for_module(model, target_name, param_value, *args, **kwar ) return + # Record definition order before parametrizations override it + # with alphabetical order. + if mod_path not in _moe_load_state["expert_param_order"]: + _moe_load_state["expert_param_order"][mod_path] = list( + mod._parameters.keys() + ) + if _moe_load_state["mode"] == "4bit": replace_parameter_4bit( mod, @@ -151,20 +149,28 @@ def get_moe_quantized_count(): def patch_peft_target_parameters_matching(): - """Fix PEFT's _inject_parameters to use suffix matching for parametrized modules.""" + """Fix PEFT's _inject_parameters for target_parameters on quantized MoE experts. + + 1. Expands short suffixes to full module paths for parametrized modules. + 2. Iterates params in definition order (not alphabetical order) so saved + adapters are compatible with standard PEFT, vLLM, etc. + """ if getattr(patch_peft_target_parameters_matching, "_axolotl_patched", False): return - from peft.tuners.tuners_utils import BaseTuner - original_inject = BaseTuner._inject_parameters + from contextlib import nullcontext + + from peft.tuners.tuners_utils import BaseTuner, BaseTunerLayer + from peft.utils.integrations import init_empty_weights + from peft.utils.other import _get_submodules def _patched_inject_parameters( self, peft_config, model, adapter_name, low_cpu_mem_usage ): - # Patch target_parameters to use full paths for parametrized modules original_targets = list(peft_config.target_parameters) expanded = set(original_targets) + # Expand short suffixes to full paths for parametrized modules. for module_name, module in model.named_modules(): if not hasattr(module, "parametrizations"): continue @@ -175,14 +181,74 @@ def _patched_inject_parameters( ) and hasattr(module, param_name): expanded.add(f"{module_name}.{param_name}") - peft_config.target_parameters = sorted(expanded) - try: - return original_inject( - self, peft_config, model, adapter_name, low_cpu_mem_usage - ) - finally: - peft_config.target_parameters = original_targets + target_names_set = expanded + + def strip_base_layer_from_name(module_name): + name = ".base_layer" + while name in module_name: + prefix, _, suffix = module_name.rpartition(name) + module_name = prefix + suffix + return module_name + + def create_and_replace_param(module_name, key, param_name): + parent, target, target_name = _get_submodules(model, module_name) + unwrapped_module_name = strip_base_layer_from_name(module_name) + unwrapped_module = model.get_submodule(unwrapped_module_name) + if ( + isinstance(unwrapped_module, BaseTunerLayer) + and unwrapped_module.__class__.__name__ != "ParamWrapper" + ): + raise ValueError( + f"Trying to wrap an `nn.Parameter` of layer " + f"'{unwrapped_module_name}' of type " + f"{type(target).__name__}, which is not a valid target. " + f"Make sure that this layer is not also targeted with " + f"`target_modules`." + ) + self._check_target_module_compatiblity(peft_config, model, target_name) + ctx = init_empty_weights if low_cpu_mem_usage else nullcontext + with ctx(): + self._create_and_replace( + peft_config, + adapter_name, + target, + target_name, + parent, + current_key=key, + parameter_name=param_name.rpartition(".")[-1], + ) + + # Use definition order (not alphabetical order) for parametrized modules + # so ParamWrapper nesting matches vanilla PEFT on a plain model. + expert_param_order = _moe_load_state.get("expert_param_order", {}) + + for module_name, module in model.named_modules(): + if hasattr(module, "parametrizations"): + stored_order = expert_param_order.get(module_name) + if stored_order is not None: + params_iter = [ + p for p in stored_order if p in module.parametrizations + ] + else: + # Fallback for paths that bypass model loading (e.g. unit tests). + params_iter = list(module.parametrizations.keys()) + for param_name in params_iter: + key = f"{module_name}.{param_name}" + if (key in target_names_set) or any( + key.endswith(f".{t}") for t in target_names_set + ): + create_and_replace_param(module_name, key, param_name) + self.targeted_parameter_names.append(key) + else: + unwrapped_module_name = strip_base_layer_from_name(module_name) + for param_name, _ in module.named_parameters(recurse=False): + key = f"{unwrapped_module_name}.{param_name}" + if (key in target_names_set) or any( + key.endswith(f".{t}") for t in target_names_set + ): + create_and_replace_param(module_name, key, param_name) + self.targeted_parameter_names.append(key) BaseTuner._inject_parameters = _patched_inject_parameters patch_peft_target_parameters_matching._axolotl_patched = True - LOG.info("Patched PEFT _inject_parameters for parametrized module suffix matching") + LOG.info("Patched PEFT _inject_parameters for consistent ParamWrapper ordering") diff --git a/tests/utils/schemas/validation/test_moe_quant.py b/tests/utils/schemas/validation/test_moe_quant.py index a2121473a4..2c34582c34 100644 --- a/tests/utils/schemas/validation/test_moe_quant.py +++ b/tests/utils/schemas/validation/test_moe_quant.py @@ -154,3 +154,119 @@ def test_double_call_does_not_stack_wrappers(self): finally: BaseTuner._inject_parameters = original patch_peft_target_parameters_matching._axolotl_patched = False + + +class TestMoeAdapterTrainMergeRoundtrip: + """E2E: train adapter on quantized MoE experts, then merge onto plain model. + + Verifies that param wrapping order during training matches merge, preventing + size mismatch errors when loading adapters in standard PEFT/vLLM. + """ + + @staticmethod + def _make_classes(): + """Return FakeExperts and FakeModel classes shared by both model builders.""" + import torch + import torch.nn as nn + + class FakeExperts(nn.Module): + def __init__(self): + super().__init__() + # Model definition order: gate_up_proj first, then down_proj. + self.gate_up_proj = nn.Parameter(torch.randn(4, 16, 8)) + self.down_proj = nn.Parameter(torch.randn(4, 8, 16)) + + def forward(self, x): + x = torch.matmul(x, self.gate_up_proj[0].T) # (batch, 16) + x = torch.matmul(x, self.down_proj[0].T) # (batch, 8) + return x + + class FakeModel(nn.Module): + def __init__(self): + super().__init__() + self.linear = nn.Linear(8, 8) + self.experts = FakeExperts() + + def forward(self, x): + return self.linear(x) + self.experts(x) + + return FakeExperts, FakeModel + + @staticmethod + def _make_quantized_model(): + """Training model: parametrizations registered in alphabetical order.""" + import torch.nn as nn + import torch.nn.utils.parametrize as P + + from axolotl.monkeypatch.moe_quant import _moe_load_state + + _, FakeModel = TestMoeAdapterTrainMergeRoundtrip._make_classes() + + class PassthroughParametrization(nn.Module): + def forward(self, x): + return x + + model = FakeModel() + + # Record definition order before parametrization (mirrors real loading). + _moe_load_state["expert_param_order"]["experts"] = list( + model.experts._parameters.keys() + ) + + # Register in alphabetical order to expose the ordering mismatch. + P.register_parametrization( + model.experts, "down_proj", PassthroughParametrization(), unsafe=True + ) + P.register_parametrization( + model.experts, "gate_up_proj", PassthroughParametrization(), unsafe=True + ) + return model + + @staticmethod + def _make_plain_model(): + """Merge model: no parametrizations — standard branch uses definition order.""" + _, FakeModel = TestMoeAdapterTrainMergeRoundtrip._make_classes() + return FakeModel() + + def test_train_save_merge_no_size_mismatch(self, tmp_path): + """Train on quantized experts, merge onto plain model — must not raise.""" + import torch + from peft import LoraConfig, PeftModel, get_peft_model + from peft.tuners.tuners_utils import BaseTuner + + from axolotl.monkeypatch.moe_quant import ( + _moe_load_state, + patch_peft_target_parameters_matching, + ) + + adapter_dir = tmp_path / "adapter" + lora_cfg = LoraConfig( + r=4, + lora_alpha=8, + target_modules=[], + target_parameters=["experts.gate_up_proj", "experts.down_proj"], + lora_dropout=0.0, + bias="none", + ) + original_inject = BaseTuner._inject_parameters + + # Training phase: quantized model (parametrized branch) with axolotl patch. + _moe_load_state["expert_param_order"] = {} + patch_peft_target_parameters_matching() + try: + peft_model = get_peft_model(self._make_quantized_model(), lora_cfg) + finally: + BaseTuner._inject_parameters = original_inject + patch_peft_target_parameters_matching._axolotl_patched = False + + optimizer = torch.optim.SGD(peft_model.parameters(), lr=1e-3) + for _ in range(3): + peft_model(torch.randn(2, 8)).sum().backward() + optimizer.step() + optimizer.zero_grad() + peft_model.save_pretrained(str(adapter_dir)) + + # Merge with standard PEFT (no axolotl patch) to verify external compatibility. + loaded = PeftModel.from_pretrained(self._make_plain_model(), str(adapter_dir)) + merged = loaded.merge_and_unload() + assert merged is not None From d8a646c80d481fb129a7cde1cda938fc5e96be24 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 16 Mar 2026 09:10:57 +0700 Subject: [PATCH 1188/1405] chore: logging cleanup (#3482) [skip ci] --- src/axolotl/cli/merge_sharded_fsdp_weights.py | 2 -- src/axolotl/core/trainers/mixins/checkpoints.py | 1 - src/axolotl/monkeypatch/attention/flex_attn.py | 5 +---- src/axolotl/monkeypatch/ring_attn/patch.py | 1 - src/axolotl/monkeypatch/tiled_mlp/patch.py | 1 - src/axolotl/train.py | 1 - src/axolotl/utils/callbacks/dynamic_checkpoint.py | 4 ---- src/axolotl/utils/data/shared.py | 2 -- src/axolotl/utils/schemas/validation.py | 1 - 9 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/axolotl/cli/merge_sharded_fsdp_weights.py b/src/axolotl/cli/merge_sharded_fsdp_weights.py index f12d5ab5d6..f4779f5d9b 100644 --- a/src/axolotl/cli/merge_sharded_fsdp_weights.py +++ b/src/axolotl/cli/merge_sharded_fsdp_weights.py @@ -196,12 +196,10 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): state.wait_for_everyone() LOG.info( f"FSDP SHARDED_STATE_DICT weights successfully merged to: {output_path}", - main_process_only=True, ) LOG.info( "Merged weights are only the safetensors and doesn't include the model configuration " f"or tokenizer which may be found in {parsed_cfg.output_dir}.", - main_process_only=True, ) diff --git a/src/axolotl/core/trainers/mixins/checkpoints.py b/src/axolotl/core/trainers/mixins/checkpoints.py index 4042ef9f10..fe13cd8ac2 100644 --- a/src/axolotl/core/trainers/mixins/checkpoints.py +++ b/src/axolotl/core/trainers/mixins/checkpoints.py @@ -19,5 +19,4 @@ def _save_optimizer_and_scheduler(self, output_dir): f"Trainer does not support saving optimizer and scheduler: {exc}\n" "Optimizer and scheduler states were not saved - resuming from checkpoints " "for this training run will not be possible.", - main_process_only=True, ) diff --git a/src/axolotl/monkeypatch/attention/flex_attn.py b/src/axolotl/monkeypatch/attention/flex_attn.py index 678f65bee3..0071c42237 100644 --- a/src/axolotl/monkeypatch/attention/flex_attn.py +++ b/src/axolotl/monkeypatch/attention/flex_attn.py @@ -64,15 +64,12 @@ def __init__(self, training): LOG.info( "Compiling flex attention with kwargs: %s. This may take a while...", flex_attn_compile_kwargs, - main_process_only=True, ) self._compiled_flex_attention = torch.compile( flex_attention, **flex_attn_compile_kwargs, ) - LOG.info( - "Flex attention compiled successfully.", main_process_only=True - ) + LOG.info("Flex attention compiled successfully.") self._is_flex_compiled = True diff --git a/src/axolotl/monkeypatch/ring_attn/patch.py b/src/axolotl/monkeypatch/ring_attn/patch.py index e1fd10b3ac..b146f75433 100644 --- a/src/axolotl/monkeypatch/ring_attn/patch.py +++ b/src/axolotl/monkeypatch/ring_attn/patch.py @@ -154,7 +154,6 @@ def register_ring_attn_from_device_mesh( LOG.info( f"Enabling ring attention sequence parallelism using DeviceMesh " f"dimension '{context_parallel_dim}'", - main_process_only=True, ) # Extract the sequence parallel submesh diff --git a/src/axolotl/monkeypatch/tiled_mlp/patch.py b/src/axolotl/monkeypatch/tiled_mlp/patch.py index c0f89236ba..65885396bc 100644 --- a/src/axolotl/monkeypatch/tiled_mlp/patch.py +++ b/src/axolotl/monkeypatch/tiled_mlp/patch.py @@ -85,7 +85,6 @@ def tiled_mlp_forward(self, x): mlp_cls._tiled_mlp_dist_impl = None LOG.info( f"Successfully monkey-patched TiledMLP for model_type: {model_type}", - main_process_only=True, ) except (ImportError, AttributeError) as e: raise RuntimeError( diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 16c3696c07..6f426363f1 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -69,7 +69,6 @@ def setup_model_and_tokenizer( # Load tokenizer LOG.debug( f"loading tokenizer... {cfg.tokenizer_config or cfg.base_model_config}", - main_process_only=True, ) tokenizer = load_tokenizer(cfg) diff --git a/src/axolotl/utils/callbacks/dynamic_checkpoint.py b/src/axolotl/utils/callbacks/dynamic_checkpoint.py index 6321092258..ac484b3d0b 100644 --- a/src/axolotl/utils/callbacks/dynamic_checkpoint.py +++ b/src/axolotl/utils/callbacks/dynamic_checkpoint.py @@ -59,7 +59,6 @@ def __init__(self, cfg): f"Dynamic checkpoint enabled. To trigger checkpoint save:\n" f" • File: touch {cfg.output_dir}/{self.trigger_filename}\n" f" • Check interval: every {self.check_interval} steps", - main_process_only=True, ) def on_step_end( @@ -89,12 +88,10 @@ def on_step_end( LOG.info( f"Dynamic checkpoint triggered via file '{self.trigger_filename}' " f"at step {state.global_step}", - main_process_only=True, ) except OSError as exc: LOG.warning( f"Failed to delete trigger file: {exc}", - main_process_only=True, ) if self.should_save_checkpoint: @@ -127,6 +124,5 @@ def on_step_end( control.should_save = True LOG.info( f"Saving dynamic checkpoint at step {state.global_step}", - main_process_only=True, ) return control diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 1d1f8be54a..4e6aa1ea38 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -474,13 +474,11 @@ def load_preprocessed_dataset(cfg: DictDefault, dataset_hash: str) -> Dataset | ): LOG.info( f"Loading prepared dataset from disk at {prepared_ds_path}...", - main_process_only=True, ) return load_from_disk(str(prepared_ds_path)) LOG.info( f"Unable to find prepared dataset in {prepared_ds_path}", - main_process_only=True, ) return None diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 2ff57558f7..3ba484fec8 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -128,7 +128,6 @@ def check_eval_packing(cls, data): ): LOG.info( "explicitly setting `eval_sample_packing` to match `sample_packing`", - main_process_only=True, ) data["eval_sample_packing"] = True From f56efdb4abd6bac88c5d8f69a4649ce4495dbad8 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Mon, 16 Mar 2026 07:41:23 +0530 Subject: [PATCH 1189/1405] fix: high eval loss w/ sample packing (#3478) [skip ci] * check if eval_sp * radable condition --- src/axolotl/utils/data/sft.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index 69cbfb871b..e008b542be 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -348,7 +348,9 @@ def _load_raw_datasets( dataset = handle_long_seq_in_dataset(dataset, cfg.eval_sequence_len, cfg) else: dataset = handle_long_seq_in_dataset(dataset, cfg.sequence_len, cfg) - if cfg.sample_packing: + if (split == "train" and cfg.sample_packing) or ( + split == "test" and cfg.eval_sample_packing + ): dataset, _ = process_datasets_for_packing(cfg, dataset, None) # Deduplicate before saving so the saved dataset is already de-duplicated From defee62d99ce606adfefd2785199041914431431 Mon Sep 17 00:00:00 2001 From: Aarush <112254386+Hadar01@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:42:40 +0530 Subject: [PATCH 1190/1405] fix: fix CONTRIBUTING.md placeholders, bare except clauses, and add convert.py tests (#3485) [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: fix codestyle placeholders in CONTRIBUTING.md Replace unresolved {codestyle} and {URLofCodestyle} template variables with Ruff, the project's actual linter/formatter as configured in .pre-commit-config.yaml. * fix: replace bare except clauses with specific exception types - quantization.py: use except ImportError for optional torchao imports (consistent with line 48 which already uses ImportError correctly) - cli/config.py: use except (RuntimeError, AssertionError) for CUDA device property query Prevents masking unrelated errors like KeyboardInterrupt or SystemExit. * test: add unit tests for convert.py JSON/JSONL utilities Cover FileReader, FileWriter, StdoutWriter, JsonParser, JsonlSerializer, and JsonToJsonlConverter with 8 test cases including roundtrip and edge case (empty list) scenarios. Previously this module had zero test coverage. * fix: address CodeRabbit review feedback - quantization.py: catch (ImportError, RuntimeError) for optional torchao imports; CUDA wheel/GPU mismatches raise RuntimeError, not ImportError - convert.py: remove unused output_file_path parameter from JsonToJsonlConverter.convert() — FileWriter already holds the output path from construction - tests/test_convert.py: update call site to match new signature --- .github/CONTRIBUTING.md | 4 +- src/axolotl/cli/config.py | 2 +- src/axolotl/convert.py | 2 +- src/axolotl/utils/quantization.py | 4 +- tests/test_convert.py | 91 +++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 tests/test_convert.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 7d9796345a..a0e4d3081f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -68,7 +68,7 @@ You can skip certain CI checks by including specific keywords in your commit mes ### Code Style -axolotl uses [{codestyle}]({URLofCodestyle}) as its code style guide. Please ensure that your code follows these guidelines. +axolotl uses [Ruff](https://docs.astral.sh/ruff/) as its code style guide. Please ensure that your code follows these guidelines. Use the pre-commit linter to ensure that your code is formatted consistently. ```bash @@ -83,6 +83,6 @@ Write clear and concise commit messages that briefly describe the changes made i - [GitHub Help](https://help.github.com/) - [GitHub Pull Request Documentation](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests) -- [{codestyle}]({URLofCodestyle}) +- [Ruff](https://docs.astral.sh/ruff/) Thank you once again for your interest in contributing to axolotl. We look forward to collaborating with you and creating an even better project together! diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index b6f79c74cc..f81ba0b2e0 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -300,7 +300,7 @@ def load_cfg( try: device_props = torch.cuda.get_device_properties("cuda") gpu_version = "sm_" + str(device_props.major) + str(device_props.minor) - except: + except (RuntimeError, AssertionError): gpu_version = None prepare_plugins(cfg) diff --git a/src/axolotl/convert.py b/src/axolotl/convert.py index 9e09b37dcb..f8d8b25f46 100644 --- a/src/axolotl/convert.py +++ b/src/axolotl/convert.py @@ -67,7 +67,7 @@ def __init__(self, file_reader, file_writer, json_parser, jsonl_serializer): self.json_parser = json_parser self.jsonl_serializer = jsonl_serializer - def convert(self, input_file_path, output_file_path): + def convert(self, input_file_path): content = self.file_reader.read(input_file_path) data = self.json_parser.parse(content) # data = [r for r in data if r["conversations"]] # vicuna cleaned has rows with empty conversations diff --git a/src/axolotl/utils/quantization.py b/src/axolotl/utils/quantization.py index 43af858b1a..3a244d6d9c 100644 --- a/src/axolotl/utils/quantization.py +++ b/src/axolotl/utils/quantization.py @@ -29,7 +29,7 @@ from torchao.prototype.mx_formats import NVFP4InferenceConfig quantization_config_to_str[NVFP4InferenceConfig] = "nvfp4" - except: + except (ImportError, RuntimeError): pass # int4 weight config imports will fail on machines with fbgemm-gpu installed @@ -38,7 +38,7 @@ from torchao.quantization.quant_api import Int4WeightOnlyConfig quantization_config_to_str[Int4WeightOnlyConfig] = "int4" - except: + except (ImportError, RuntimeError): pass try: diff --git a/tests/test_convert.py b/tests/test_convert.py new file mode 100644 index 0000000000..bfe0b603cf --- /dev/null +++ b/tests/test_convert.py @@ -0,0 +1,91 @@ +"""Unit tests for src/axolotl/convert.py""" + +import json + +import pytest + +from axolotl.convert import ( + FileReader, + FileWriter, + JsonlSerializer, + JsonParser, + JsonToJsonlConverter, + StdoutWriter, +) + + +class TestJsonParser: + def test_parse_valid_json_array(self): + parser = JsonParser() + result = parser.parse('[{"key": "value"}]') + assert result == [{"key": "value"}] + + def test_parse_valid_json_object(self): + parser = JsonParser() + result = parser.parse('{"key": "value"}') + assert result == {"key": "value"} + + def test_parse_invalid_json_raises(self): + parser = JsonParser() + with pytest.raises(json.JSONDecodeError): + parser.parse("not valid json") + + +class TestJsonlSerializer: + def test_serialize_single_item(self): + serializer = JsonlSerializer() + result = serializer.serialize([{"a": 1}]) + assert result == '{"a": 1}' + + def test_serialize_multiple_items(self): + serializer = JsonlSerializer() + result = serializer.serialize([{"a": 1}, {"b": 2}]) + lines = result.split("\n") + assert len(lines) == 2 + assert json.loads(lines[0]) == {"a": 1} + assert json.loads(lines[1]) == {"b": 2} + + def test_serialize_empty_list(self): + serializer = JsonlSerializer() + result = serializer.serialize([]) + assert result == "" + + +class TestFileReaderWriter: + def test_read_write_roundtrip(self, tmp_path): + test_file = tmp_path / "test.txt" + content = '{"hello": "world"}' + writer = FileWriter(str(test_file)) + writer.write(content) + + reader = FileReader() + result = reader.read(str(test_file)) + assert result == content + + +class TestStdoutWriter: + def test_write_to_stdout(self, capsys): + writer = StdoutWriter() + writer.write("hello") + captured = capsys.readouterr() + assert captured.out == "hello\n" + + +class TestJsonToJsonlConverter: + def test_convert_json_to_jsonl(self, tmp_path): + input_data = [{"name": "Alice"}, {"name": "Bob"}] + input_file = tmp_path / "input.json" + output_file = tmp_path / "output.jsonl" + + input_file.write_text(json.dumps(input_data), encoding="utf-8") + + converter = JsonToJsonlConverter( + FileReader(), FileWriter(str(output_file)), JsonParser(), JsonlSerializer() + ) + converter.convert(str(input_file)) + + result = output_file.read_text(encoding="utf-8") + lines = result.split("\n") + assert len(lines) == 2 + assert json.loads(lines[0]) == {"name": "Alice"} + assert json.loads(lines[1]) == {"name": "Bob"} From 4a5876df7a9db30b0c22e05ae758da52002cc982 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 16 Mar 2026 11:13:05 +0700 Subject: [PATCH 1191/1405] fix: explicit set workflow permission and move secrets to necessary (#3484) [skip ci] * fix: explicit set workflow permission and move secrets to necessary steps only * fix: comment * fix: more permission restrict * chore: add read for pypi --- .github/workflows/base.yml | 11 +++++++---- .github/workflows/lint.yml | 3 +++ .github/workflows/main.yml | 3 +++ .github/workflows/multi-gpu-e2e.yml | 6 +++++- .github/workflows/nightlies.yml | 3 +++ .github/workflows/precommit-autoupdate.yml | 2 ++ .github/workflows/pypi.yml | 9 ++++++--- .github/workflows/tests-nightly.yml | 9 +++++++-- .github/workflows/tests.yml | 10 +++++++--- 9 files changed, 43 insertions(+), 13 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 29842bcb53..0fe0d2b253 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -15,6 +15,9 @@ on: - '.github/workflows/base.yml' workflow_dispatch: +permissions: + contents: read + jobs: build-base: if: ${{ github.repository_owner == 'axolotl-ai-cloud' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} @@ -124,7 +127,7 @@ jobs: images: | axolotlai/axolotl-base - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 if: ${{ github.event_name != 'pull_request' && env.HAS_DOCKERHUB_CREDS == 'true' }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -132,7 +135,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: ./docker/${{ matrix.dockerfile }} @@ -247,7 +250,7 @@ jobs: images: | axolotlai/axolotl-base-uv - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 if: ${{ github.event_name != 'pull_request' && env.HAS_DOCKERHUB_CREDS == 'true' }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -255,7 +258,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: ./docker/${{ matrix.dockerfile }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cf322f1059..181fd9dc97 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,6 +13,9 @@ on: - ".pre-commit-config.yaml" workflow_dispatch: +permissions: + contents: read + jobs: pre-commit: name: pre-commit diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 580c4047c3..a3a24537c4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,6 +8,9 @@ on: - "v*" workflow_dispatch: +permissions: + contents: read + jobs: build-axolotl: if: ${{ ! contains(github.event.commits[0].message, '[skip docker]') && github.repository_owner == 'axolotl-ai-cloud' }} diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 6063c24c74..2bb499ded0 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -20,6 +20,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +permissions: + contents: read + env: MODAL_IMAGE_BUILDER_VERSION: "2025.06" @@ -78,8 +81,9 @@ jobs: echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV - name: Run tests job on Modal + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | modal run -m cicd.multigpu diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index d2c587cc7e..0372f5c7ab 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -5,6 +5,9 @@ on: schedule: - cron: '0 0 * * *' # Runs at 00:00 UTC every day +permissions: + contents: read + jobs: build-axolotl: if: ${{ ! contains(github.event.commits[0].message, '[skip docker]') && github.repository_owner == 'axolotl-ai-cloud' }} diff --git a/.github/workflows/precommit-autoupdate.yml b/.github/workflows/precommit-autoupdate.yml index 4c2e59b6bb..d7e4ce141d 100644 --- a/.github/workflows/precommit-autoupdate.yml +++ b/.github/workflows/precommit-autoupdate.yml @@ -5,6 +5,8 @@ on: - cron: '0 0 1 * *' # Run monthly workflow_dispatch: # Manual kickoff +permissions: {} + jobs: auto-update: runs-on: ubuntu-latest diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 3bf66b4975..19dface731 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -3,9 +3,11 @@ name: publish pypi on: push: tags: - - 'v*' + - "v*" workflow_dispatch: +permissions: {} + jobs: setup_release: name: Create Release @@ -28,7 +30,8 @@ jobs: name: pypi url: https://pypi.org/p/axolotl permissions: - id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + contents: read + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - name: Check out repository code uses: actions/checkout@v4 @@ -46,7 +49,7 @@ jobs: - name: Extract tag name id: tag - run: echo ::set-output name=TAG_NAME::$(echo $GITHUB_REF | cut -d / -f 3) + run: echo "TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3)" >> "$GITHUB_OUTPUT" - name: Update version in VERSION file run: | diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index d5a533fbcc..663b0476e1 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -8,6 +8,9 @@ on: paths: - '.github/workflows/tests-nightly.yml' +permissions: + contents: read + jobs: pre-commit: name: pre-commit @@ -156,8 +159,9 @@ jobs: echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV echo "NIGHTLY_BUILD=${{ matrix.nightly_build }}" >> $GITHUB_ENV - echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV - name: Run tests job on Modal + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | modal run cicd.e2e_tests docker-e2e-multigpu-tests: @@ -198,7 +202,8 @@ jobs: echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV echo "NIGHTLY_BUILD=${{ matrix.nightly_build }}" >> $GITHUB_ENV - echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV - name: Run tests job on Modal + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | modal run cicd.multigpu diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 23e9d39e39..9df249270e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +permissions: + contents: read + env: TRANSFORMERS_IS_CI: "yes" @@ -303,9 +306,10 @@ jobs: echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV - name: Run tests job on Modal + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | modal run cicd.e2e_tests @@ -371,9 +375,10 @@ jobs: echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV echo "GPU_TYPE=${{ matrix.gpu_type || 'L40S'}}" >> $GITHUB_ENV - echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV - name: Run tests job on Modal + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | modal run cicd.e2e_tests @@ -413,7 +418,6 @@ jobs: echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | modal run cicd.cleanup From 7da5f943795f1e732291229fc779a8011fce0486 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 16 Mar 2026 11:13:18 +0700 Subject: [PATCH 1192/1405] feat: add FA4 (#3481) * feat: add FA4 * chore: update docs * fix: recommend FA4 for those with compatible devices * fix: adjust import check and add head_dim check * chore: add limitation to doc * fix: log warning and quit if cannot import validator * chore: simplify * fix: add caveat with FA2 shadow dir --- README.md | 2 +- docs/attention.qmd | 54 +++++++-- src/axolotl/loaders/patch_manager.py | 10 ++ .../monkeypatch/attention/flash_attn_4.py | 104 ++++++++++++++++++ 4 files changed, 161 insertions(+), 9 deletions(-) create mode 100644 src/axolotl/monkeypatch/attention/flash_attn_4.py diff --git a/README.md b/README.md index 594b06156e..f10e08b427 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Features: - **Multimodal Training**: Fine-tune vision-language models (VLMs) including LLaMA-Vision, Qwen2-VL, Pixtral, LLaVA, SmolVLM2, GLM-4.6V, InternVL 3.5, Gemma 3n, and audio models like Voxtral with image, video, and audio support. - **Training Methods**: Full fine-tuning, LoRA, QLoRA, GPTQ, QAT, Preference Tuning (DPO, IPO, KTO, ORPO), RL (GRPO, GDPO), and Reward Modelling (RM) / Process Reward Modelling (PRM). - **Easy Configuration**: Re-use a single YAML configuration file across the full fine-tuning pipeline: dataset preprocessing, training, evaluation, quantization, and inference. -- **Performance Optimizations**: [Multipacking](https://docs.axolotl.ai/docs/multipack.html), [Flash Attention](https://github.com/Dao-AILab/flash-attention), [Xformers](https://github.com/facebookresearch/xformers), [Flex Attention](https://pytorch.org/blog/flexattention/), [SageAttention](https://github.com/thu-ml/SageAttention), [Liger Kernel](https://github.com/linkedin/Liger-Kernel), [Cut Cross Entropy](https://github.com/apple/ml-cross-entropy/tree/main), [ScatterMoE](https://docs.axolotl.ai/docs/custom_integrations.html#kernels-integration), [Sequence Parallelism (SP)](https://docs.axolotl.ai/docs/sequence_parallelism.html), [LoRA optimizations](https://docs.axolotl.ai/docs/lora_optims.html), [Multi-GPU training (FSDP1, FSDP2, DeepSpeed)](https://docs.axolotl.ai/docs/multi-gpu.html), [Multi-node training (Torchrun, Ray)](https://docs.axolotl.ai/docs/multi-node.html), and many more! +- **Performance Optimizations**: [Multipacking](https://docs.axolotl.ai/docs/multipack.html), [Flash Attention 2/3/4](https://docs.axolotl.ai/docs/attention.html#flash-attention), [Xformers](https://docs.axolotl.ai/docs/attention.html#xformers), [Flex Attention](https://docs.axolotl.ai/docs/attention.html#flex-attention), [SageAttention](https://docs.axolotl.ai/docs/attention.html#sageattention), [Liger Kernel](https://docs.axolotl.ai/docs/custom_integrations.html#liger-kernels), [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy), [ScatterMoE](https://docs.axolotl.ai/docs/custom_integrations.html#kernels-integration), [Sequence Parallelism (SP)](https://docs.axolotl.ai/docs/sequence_parallelism.html), [LoRA optimizations](https://docs.axolotl.ai/docs/lora_optims.html), [Multi-GPU training (FSDP1, FSDP2, DeepSpeed)](https://docs.axolotl.ai/docs/multi-gpu.html), [Multi-node training (Torchrun, Ray)](https://docs.axolotl.ai/docs/multi-node.html), and many more! - **Flexible Dataset Handling**: Load from local, HuggingFace, and cloud (S3, Azure, GCP, OCI) datasets. - **Cloud Ready**: We ship [Docker images](https://hub.docker.com/u/axolotlai) and also [PyPI packages](https://pypi.org/project/axolotl/) for use on cloud platforms and local hardware. diff --git a/docs/attention.qmd b/docs/attention.qmd index 21004277e2..771299a293 100644 --- a/docs/attention.qmd +++ b/docs/attention.qmd @@ -13,9 +13,10 @@ sdp_attention: true For more details: [PyTorch docs](https://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html) -## Flash Attention 2 +## Flash Attention -Uses efficient kernels to compute attention. +Axolotl supports Flash Attention 2, 3, and 4. The best available version is used automatically +based on your installed packages and GPU. ```yaml flash_attention: true @@ -23,11 +24,9 @@ flash_attention: true For more details: [Flash Attention](https://github.com/Dao-AILab/flash-attention/) -### Nvidia +### Flash Attention 2 -Requirements: Ampere, Ada, or Hopper GPUs - -Note: For Turing GPUs or lower, please use other attention methods. +Requirements: Ampere, Ada, or Hopper GPUs (Turing or lower not supported) ```bash pip install flash-attn --no-build-isolation @@ -35,11 +34,12 @@ pip install flash-attn --no-build-isolation ::: {.callout-tip} -If you get `undefined symbol` while training, ensure you installed PyTorch prior to Axolotl. Alternatively, try reinstall or downgrade a version. +If you get `undefined symbol` while training, ensure you installed PyTorch prior to Axolotl. +Alternatively, try reinstall or downgrade a version. ::: -#### Flash Attention 3 +### Flash Attention 3 Requirements: Hopper only and CUDA 12.8 (recommended) @@ -50,6 +50,44 @@ cd flash-attention/hopper python setup.py install ``` +### Flash Attention 4 + +Requirements: Hopper or Blackwell GPUs + +```bash +pip install flash-attn-4 +``` + +Or from source: + +```bash +git clone https://github.com/Dao-AILab/flash-attention.git +cd flash-attention/flash_attn/cute + +pip install -e . + +# FA2's flash_attn package includes a cute/ stub that shadows FA4. +# Remove it so Python can find the real FA4 module: +rm -r $(python -c "import flash_attn; print(flash_attn.__path__[0])")/cute +``` + +::: {.callout-note} + +**Hopper (SM90) users**: The backward kernel is not yet included in the pip package. To use FA4 +for training on Hopper, install from source using the instructions above. + +::: + +::: {.callout-warning} + +FA4 only supports head dimensions up to 128 (`d ≤ 128`). The DeepSeek shape `(192, 128)` is +also supported but only on Blackwell. Axolotl automatically detects incompatible head dimensions +and falls back to FA2/3. + +::: + +For more details: [flash-attention/flash_attn/cute](https://github.com/Dao-AILab/flash-attention/tree/main/flash_attn/cute) + ### AMD Requirements: ROCm 6.0 and above. diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 857a2f76fc..5874c940b9 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -99,6 +99,7 @@ def apply_pre_model_load_patches(self): self._apply_flash_attention_patches() self._apply_chunked_cross_entropy_patch() self._apply_sageattn_patches() + self._apply_flash_attn_4_patches() self._apply_fsdp_patches() self._apply_adapter_patches() self._apply_model_specific_patches() @@ -228,6 +229,15 @@ def _apply_sageattn_patches(self): patch_sageattn() + def _apply_flash_attn_4_patches(self): + """Auto-apply FA4 when flash_attention is enabled and FA4 is available on SM90+.""" + if not self.cfg.flash_attention: + return + + from axolotl.monkeypatch.attention.flash_attn_4 import patch_flash_attn_4 + + patch_flash_attn_4(self.model_config) + def _apply_model_specific_patches(self): """Apply patches specific to model architectures.""" if ( diff --git a/src/axolotl/monkeypatch/attention/flash_attn_4.py b/src/axolotl/monkeypatch/attention/flash_attn_4.py new file mode 100644 index 0000000000..5ebc93670c --- /dev/null +++ b/src/axolotl/monkeypatch/attention/flash_attn_4.py @@ -0,0 +1,104 @@ +"""Transparently upgrade FA2 to FA4 when available on SM90+ hardware.""" + +import torch + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def _get_head_dims(model_config): + """Extract (head_dim, head_dim_v) from a model config. + + Handles composite models (e.g. Qwen3.5 VL) via text_config and + MLA models (DeepSeek/Kimi) that have separate Q/V head dimensions. + """ + cfg = model_config + if hasattr(cfg, "text_config"): + cfg = cfg.text_config + + # MLA models: Q head_dim = qk_nope + qk_rope, V head_dim = v_head_dim + if hasattr(cfg, "qk_nope_head_dim") and hasattr(cfg, "qk_rope_head_dim"): + head_dim = cfg.qk_nope_head_dim + cfg.qk_rope_head_dim + head_dim_v = getattr(cfg, "v_head_dim", head_dim) + return head_dim, head_dim_v + + # Standard models + if hasattr(cfg, "head_dim"): + return cfg.head_dim, cfg.head_dim + if hasattr(cfg, "hidden_size") and hasattr(cfg, "num_attention_heads"): + head_dim = cfg.hidden_size // cfg.num_attention_heads + return head_dim, head_dim + + return None, None + + +def patch_flash_attn_4(model_config=None): + """Patch _lazy_imports to redirect FA2 imports to FA4 if available on supported hardware.""" + if not torch.cuda.is_available(): + return + + major, _ = torch.cuda.get_device_capability() + # Matches flash_attn/cute/interface.py: arch / 10 in [9, 10, 11] + if major not in (9, 10, 11): + return + + try: + from flash_attn.cute import ( # noqa: F401 + flash_attn_func, + flash_attn_varlen_func, + ) + except ImportError: + LOG.info( + "Flash Attention 4 is available for your GPU and offers faster training speeds. " + "To enable: pip install flash-attn-4" + ) + return + + # Validate head dimensions against FA4's own constraints + head_dim = None + if model_config is not None: + head_dim, head_dim_v = _get_head_dims(model_config) + if head_dim is not None: + try: + from flash_attn.cute.interface import _validate_head_dims + except ImportError: + LOG.warning( + "Could not import _validate_head_dims from flash_attn.cute.interface, " + "unable to verify head dimension compatibility, falling back to FA2" + ) + return + + # alignment = 16 // element_size; bf16/fp16 = 2 bytes -> alignment = 8 + alignment = 8 + try: + _validate_head_dims(head_dim, head_dim_v, major, alignment) + except AssertionError as exc: + LOG.warning( + "Model head dimensions not supported by FA4, " + "falling back to FA2: %s", + exc, + ) + return + + import transformers.modeling_flash_attention_utils as fa_utils + + if getattr(fa_utils._lazy_imports, "_axolotl_patched", False): + return + + def _patched_lazy_imports( + implementation, attention_wrapper=None, allow_all_kernels=False + ): + return ( + flash_attn_func, + flash_attn_varlen_func, + fa_utils._pad_input, + fa_utils._unpad_input, + ) + + _patched_lazy_imports._axolotl_patched = True + fa_utils._lazy_imports = _patched_lazy_imports + LOG.info( + "Flash Attention 4 enabled (head_dim=%s)", + head_dim if model_config else "unknown", + ) From a098df527bd10ebaa9d54a14f793bd460485a739 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 17 Mar 2026 09:39:05 +0700 Subject: [PATCH 1193/1405] feat: add Mistral Small 4 (#3502) * feat: add mistral small 4 * fix: update mistral common * fix: deepcopy when passing in tokenizer * feat: add doc on reasoning and thinking section * fix: don't use custom tokenizer and quantize experts * chore: update docs and configs * chore: update doc to follow official name * feat: update cce to include mistral4 * chore: move * fix: naming * fix: test mock breaking get_text_config check * fix: enable CCE and add expert block targetting to configs * chore: docs * fix: use act checkpointing * chore: doc * chore: docs * chore: docs --- README.md | 2 +- docs/multimodal.qmd | 7 ++ .../colab-axolotl-example.ipynb | 2 +- examples/mistral4/README.md | 85 +++++++++++++++++++ examples/mistral4/fft-text.yml | 58 +++++++++++++ examples/mistral4/fft-vision.yml | 57 +++++++++++++ examples/mistral4/qlora-text.yml | 58 +++++++++++++ examples/mistral4/qlora-vision.yml | 63 ++++++++++++++ requirements.txt | 2 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/common/architectures.py | 1 + .../integrations/cut_cross_entropy/README.md | 4 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/integrations/kernels/constants.py | 2 + src/axolotl/integrations/kernels/plugin.py | 10 +-- .../integrations/kernels/sonicmoe/routing.py | 59 +++++++++++++ src/axolotl/loaders/model.py | 5 +- src/axolotl/loaders/processor.py | 2 +- src/axolotl/monkeypatch/multipack.py | 1 + src/axolotl/utils/config/__init__.py | 9 ++ 20 files changed, 417 insertions(+), 14 deletions(-) create mode 100644 examples/mistral4/README.md create mode 100644 examples/mistral4/fft-text.yml create mode 100644 examples/mistral4/fft-vision.yml create mode 100644 examples/mistral4/qlora-text.yml create mode 100644 examples/mistral4/qlora-vision.yml diff --git a/README.md b/README.md index f10e08b427..a70dc8edf9 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ ## 🎉 Latest Updates - 2026/03: - - New model support has been added in Axolotl for [Qwen3.5, Qwen3.5 MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3.5), [GLM-4.7-Flash](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm47-flash), [GLM-4.6V](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm46v), and [GLM-4.5-Air](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm45). + - New model support has been added in Axolotl for [[Qwen3.5, Qwen3.5 MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3.5), [GLM-4.7-Flash](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm47-flash), [GLM-4.6V](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm46v), and [GLM-4.5-Air](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm45). - [MoE expert quantization](https://docs.axolotl.ai/docs/expert_quantization.html) support (via `quantize_moe_experts: true`) greatly reduces VRAM when training MoE models (FSDP2 compat). - 2026/02: - [ScatterMoE LoRA](https://github.com/axolotl-ai-cloud/axolotl/pull/3410) support. LoRA fine-tuning directly on MoE expert weights using custom Triton kernels. diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index 54793c6e34..e5753732d7 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -13,6 +13,7 @@ format: - [Pixtral](#sec-pixtral) - [Llava-1.5](#sec-llava-15) - [Mistral-Small-3.1](#sec-mistral-small-31) +- [Mistral-Small-4](#sec-mistral-small-4) - [Magistral-Small-2509](#sec-magistral-small-2509) - [Voxtral](#sec-voxtral) - [Gemma-3](#sec-gemma-3) @@ -108,6 +109,12 @@ Please make sure to install vision lib via `pip install 'mistral-common[opencv]= base_model: mistralai/Mistral-Small-3.1-24B-Instruct-2503 ``` +### Mistral-Small-4 {#sec-mistral-small-4} + +```yaml +base_model: mistralai/Mistral-Small-4-119B-2603 +``` + ### Magistral-Small-2509 {#sec-magistral-small-2509} ::: {.callout-tip} diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 7be9800be0..49a45cdc6e 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@e8ad129\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fa9a7fe\"" ] }, { diff --git a/examples/mistral4/README.md b/examples/mistral4/README.md new file mode 100644 index 0000000000..6513847919 --- /dev/null +++ b/examples/mistral4/README.md @@ -0,0 +1,85 @@ +# Finetune Mistral Small 4 with Axolotl + +Mistral Small 4 is a 119B parameter (6.5B active) multimodal MoE model from MistralAI that unifies instruct, reasoning, and coding capabilities into a single model. It is available on HuggingFace at [Mistral-Small-4-119B-2603](https://huggingface.co/mistralai/Mistral-Small-4-119B-2603). + +Thanks to the team at MistralAI for giving us early access to prepare for this release. + +## Getting started + +Note: Training this model requires weights in BF16 which we will link to later. +Users interested in training can convert / descale the existing FP8 weights. + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage + +3. Install transformers from main + + ```bash + pip install git+https://github.com/huggingface/transformers.git + ``` + +4. Run one of the example configs: + + ```bash + # text-only + axolotl train examples/mistral4/qlora-text.yml # no experts ~69 GiB, experts ~93 GiB + axolotl train examples/mistral4/fft-text.yml + + # text + vision + # run: wget https://huggingface.co/datasets/Nanobit/text-vision-2k-test/resolve/main/African_elephant.jpg + axolotl train examples/mistral4/qlora-vision.yml # no experts ~68 GiB + axolotl train examples/mistral4/fft-vision.yml + ``` + +Note: FFT configs provided as reference. Please adjust hyperparameters as needed. + +## Reasoning Effort + +The chat template supports a `reasoning_effort` variable to control the model's reasoning depth: + +- `"none"` — instruct mode (default) +- `"high"` — reasoning mode with explicit thinking steps + +Pass it via `chat_template_kwargs` under your dataset config: + +```yaml +datasets: + - path: your/dataset + type: chat_template + chat_template_kwargs: + reasoning_effort: high +``` + +## Thinking Support + +The chat template supports a `thinking` content type in assistant messages for training on reasoning traces (rendered as `[THINK]...[/THINK]` blocks). + +To use thinking datasets, add the `thinking` mapping via `message_property_mappings`: + +```yaml +datasets: + - path: your/thinking-dataset + type: chat_template + message_property_mappings: + role: role + content: content + thinking: thinking + chat_template_kwargs: + reasoning_effort: high +``` + +See the [Magistral thinking guide](../magistral/think/README.md) for dataset format details. + +## Tips + +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The text dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). +- The vision model requires multi-modal dataset format as documented [here](https://docs.axolotl.ai/docs/multimodal.html#dataset-format). + +## Related Resources + +- [MistralAI Mistral Small 4 Blog](https://mistral.ai/news/mistral-small-4) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/mistral4/fft-text.yml b/examples/mistral4/fft-text.yml new file mode 100644 index 0000000000..e01d96dad2 --- /dev/null +++ b/examples/mistral4/fft-text.yml @@ -0,0 +1,58 @@ +base_model: mistralai/Mistral-Small-4-119B-2603 + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.kernels.KernelsPlugin +use_kernels: true +use_sonicmoe: true + +# only train language model layers, freeze vision tower +unfrozen_parameters: + - model.language_model.* + - lm_head + - embed_tokens + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.01 +output_dir: ./outputs/out + +sequence_len: 2048 +sample_packing: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 2e-5 + +bf16: true +tf32: true + +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 + +fsdp_version: 2 +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: false + state_dict_type: FULL_STATE_DICT + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Mistral4DecoderLayer + reshard_after_forward: true + activation_checkpointing: true diff --git a/examples/mistral4/fft-vision.yml b/examples/mistral4/fft-vision.yml new file mode 100644 index 0000000000..aa65dfa6dd --- /dev/null +++ b/examples/mistral4/fft-vision.yml @@ -0,0 +1,57 @@ +base_model: mistralai/Mistral-Small-4-119B-2603 +processor_type: AutoProcessor + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.kernels.KernelsPlugin +use_kernels: true +use_sonicmoe: true + +# vision requirements +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +datasets: + - path: Nanobit/text-vision-2k-test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.01 +output_dir: ./outputs/out + +sequence_len: 2048 + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 2e-5 + +bf16: true +tf32: true + +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 + +fsdp_version: 2 +fsdp_config: + offload_params: false + cpu_ram_efficient_loading: false + state_dict_type: FULL_STATE_DICT + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Mistral4DecoderLayer + reshard_after_forward: true + activation_checkpointing: true diff --git a/examples/mistral4/qlora-text.yml b/examples/mistral4/qlora-text.yml new file mode 100644 index 0000000000..ed38053f6a --- /dev/null +++ b/examples/mistral4/qlora-text.yml @@ -0,0 +1,58 @@ +base_model: mistralai/Mistral-Small-4-119B-2603 + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_4bit: true +quantize_moe_experts: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.01 +output_dir: ./outputs/out + +adapter: qlora + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +# uncomment to train on expert layers +# lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj +# lora_mlp_kernel: false +# lora_qkv_kernel: false +# lora_o_kernel: false + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: true +tf32: true + +gradient_checkpointing: true +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 diff --git a/examples/mistral4/qlora-vision.yml b/examples/mistral4/qlora-vision.yml new file mode 100644 index 0000000000..95b8138cee --- /dev/null +++ b/examples/mistral4/qlora-vision.yml @@ -0,0 +1,63 @@ +base_model: mistralai/Mistral-Small-4-119B-2603 +processor_type: AutoProcessor + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_4bit: true +quantize_moe_experts: true + +# vision chat template requirements +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +datasets: + - path: Nanobit/text-vision-2k-test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.01 +output_dir: ./outputs/out + +adapter: qlora + +sequence_len: 2048 + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|cross_attn|self_attn).(up|down|gate|q|k|v|o)_proj' + +# uncomment to train on expert layers +# lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj +# lora_mlp_kernel: false +# lora_qkv_kernel: false +# lora_o_kernel: false + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 1 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: true +tf32: true + +gradient_checkpointing: true +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 diff --git a/requirements.txt b/requirements.txt index c918d30aa7..3fd75c3fac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -75,4 +75,4 @@ axolotl-contribs-mit==0.0.6 # telemetry posthog==6.7.11 -mistral-common==1.8.8 +mistral-common==1.10.0 diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index d506fa87ee..771a5adb27 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@e8ad129"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fa9a7fe"' ) diff --git a/src/axolotl/common/architectures.py b/src/axolotl/common/architectures.py index 0e1de30179..181667cb9c 100644 --- a/src/axolotl/common/architectures.py +++ b/src/axolotl/common/architectures.py @@ -16,6 +16,7 @@ "qwen3_vl_moe": "Qwen3VLMoeTextSparseMoeBlock", "deepseek_v2": "DeepseekV2MoE", "deepseek_v3": "DeepseekV3MoE", + "mistral4": "Mistral4MoE", "gpt_oss": "GptOssDecoderLayer", "lfm2_moe": "Lfm2MoeSparseMoeBlock", "afmoe": "AfmoeMoE", diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 9520dd48c5..5a3a73d34d 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@e8ad129" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fa9a7fe" ``` ## Usage @@ -73,8 +73,10 @@ plugins: - ministral3 - mistral - mistral3 +- mistral4 - mixtral - mllama +- nemotron_h - olmo - olmo2 - olmo3 diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index d8aa075b95..808aff6627 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@e8ad129"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fa9a7fe"`' ) diff --git a/src/axolotl/integrations/kernels/constants.py b/src/axolotl/integrations/kernels/constants.py index 529ed4ad66..a7d513b5e3 100644 --- a/src/axolotl/integrations/kernels/constants.py +++ b/src/axolotl/integrations/kernels/constants.py @@ -25,6 +25,8 @@ "olmoe": "OlmoeSparseMoeBlock", "mixtral": "MixtralSparseMoeBlock", "minimax": "MiniMaxSparseMoeBlock", + # softmax -> topk routing (with group-based expert selection) + "mistral4": "Mistral4MoE", # sigmoid -> topk routing (with group-based expert selection) "glm_moe_dsa": "GlmMoeDsaMoE", "deepseek_v3": "DeepseekV3MoE", diff --git a/src/axolotl/integrations/kernels/plugin.py b/src/axolotl/integrations/kernels/plugin.py index ad14dd148c..f085e481c6 100644 --- a/src/axolotl/integrations/kernels/plugin.py +++ b/src/axolotl/integrations/kernels/plugin.py @@ -61,9 +61,11 @@ def get_input_args(self): return "axolotl.integrations.kernels.KernelsArgs" def pre_model_load(self, cfg): + moe_model_type = cfg.model_config_type_text or cfg.model_config_type + if cfg.use_scattermoe: self._register_kernels() - self._kernelize_model(cfg.model_config_type) + self._kernelize_model(moe_model_type) elif cfg.use_sonicmoe: if not importlib.util.find_spec("sonicmoe"): raise RuntimeError( @@ -75,11 +77,9 @@ def pre_model_load(self, cfg): from axolotl.integrations.kernels.sonicmoe import patch_sonicmoe - LOG.info( - f"Applying SonicMoE patches for model type: {cfg.model_config_type}" - ) + LOG.info(f"Applying SonicMoE patches for model type: {moe_model_type}") patch_sonicmoe( - cfg.model_config_type, + moe_model_type, torch_compile=bool(getattr(cfg, "torch_compile", False)), ) diff --git a/src/axolotl/integrations/kernels/sonicmoe/routing.py b/src/axolotl/integrations/kernels/sonicmoe/routing.py index 3f93c15963..fe2d120920 100644 --- a/src/axolotl/integrations/kernels/sonicmoe/routing.py +++ b/src/axolotl/integrations/kernels/sonicmoe/routing.py @@ -5,6 +5,7 @@ - qwen3_moe / qwen2_moe / qwen3_5_moe / qwen3_vl_moe / qwen3_omni_moe: softmax -> topk (with optional renormalization) - gpt_oss: topk -> softmax (uses fused moe_TC_softmax_topk_layer, routing_fn=None) - glm_moe_dsa: sigmoid -> topk (with group-based expert selection) +- mistral4: softmax -> group selection -> topk (with renormalization and scaling) Each model type maps to a (routing_fn, activation_type, router_attr) triple. When routing_fn is None, the fused moe_TC_softmax_topk_layer path is used. @@ -45,6 +46,8 @@ def get_model_moe_config(model_type: str): "minimax", ): return softmax_topk_routing, ActivationType.SWIGLU, "gate" + elif model_type in ("mistral4",): + return softmax_group_topk_routing, ActivationType.SWIGLU, "gate" elif model_type in ( "glm_moe_dsa", "deepseek_v3", @@ -126,6 +129,62 @@ def softmax_topk_routing( return flat_scores, flat_token_idx, flat_expert_idx, router_logits +def softmax_group_topk_routing( + hidden_states: torch.Tensor, moe_block +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """Mistral4-style routing: softmax -> group selection -> topk -> renorm -> scale.""" + gate = moe_block.gate + T, H = hidden_states.shape + K = moe_block.top_k + E = getattr(moe_block, "n_routed_experts", gate.weight.shape[0]) + n_group = getattr(moe_block, "n_group", 1) + + router_logits = F.linear(hidden_states, gate.weight) # [T, E] + router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] + + scores_for_choice = router_probs + + # Group selection: pick top groups, mask the rest + if n_group > 1: + group_scores = ( + scores_for_choice.view(-1, n_group, E // n_group) + .topk(2, dim=-1)[0] + .sum(dim=-1) + ) + group_idx = torch.topk( + group_scores, k=moe_block.topk_group, dim=-1, sorted=False + )[1] + group_mask = torch.zeros_like(group_scores) + group_mask.scatter_(1, group_idx, 1) + score_mask = ( + group_mask.unsqueeze(-1).expand(-1, n_group, E // n_group).reshape(-1, E) + ) + scores_for_choice = scores_for_choice.masked_fill(~score_mask.bool(), 0.0) + + topk_indices = torch.topk(scores_for_choice, k=K, dim=-1, sorted=False)[1] + topk_weights = router_probs.gather(1, topk_indices) + + # Renormalization + scaling + norm_topk_prob = getattr(moe_block, "norm_topk_prob", True) + if norm_topk_prob: + topk_weights = topk_weights / (topk_weights.sum(dim=-1, keepdim=True) + 1e-20) + routed_scaling_factor = getattr(moe_block, "routed_scaling_factor", 1.0) + topk_weights = topk_weights * routed_scaling_factor + + # Flatten for moe_general_routing_inputs + token_indices = ( + torch.arange(T, device=hidden_states.device, dtype=torch.int32) + .unsqueeze(1) + .expand(T, K) + ) + + flat_scores = topk_weights.to(torch.float32).reshape(-1) # [T*K] + flat_token_idx = token_indices.reshape(-1) # [T*K] + flat_expert_idx = topk_indices.to(torch.int32).reshape(-1) # [T*K] + + return flat_scores, flat_token_idx, flat_expert_idx, router_logits + + def sigmoid_topk_routing( hidden_states: torch.Tensor, moe_block ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 03c1f35bcf..2662d0b861 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -829,8 +829,9 @@ def _build_model(self) -> bool: def _set_z3_leaf_modules(self): from deepspeed.utils import set_z3_leaf_modules - if self.cfg.model_config_type in MOE_ARCH_BLOCK: - moe_blocks = MOE_ARCH_BLOCK[self.cfg.model_config_type] + moe_type = self.cfg.model_config_type_text or self.cfg.model_config_type + if moe_type in MOE_ARCH_BLOCK: + moe_blocks = MOE_ARCH_BLOCK[moe_type] moe_blocks = [moe_blocks] if isinstance(moe_blocks, str) else moe_blocks set_z3_leaf_modules( self.model, diff --git a/src/axolotl/loaders/processor.py b/src/axolotl/loaders/processor.py index e07e324d63..211c260608 100644 --- a/src/axolotl/loaders/processor.py +++ b/src/axolotl/loaders/processor.py @@ -55,12 +55,12 @@ def _patch_mistralcommontokenizer(): ) processor_kwargs["trust_remote_code"] = cfg.trust_remote_code or False - processor_kwargs["tokenizer"] = tokenizer processor = processor_cls.from_pretrained( cfg.processor_config, **processor_kwargs, ) + processor.tokenizer = tokenizer # Attempt to load image size from processor if available if ( diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index cad6039bdd..9e48e73ebb 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -57,6 +57,7 @@ "olmo3", "ministral", "ministral3", + "mistral4", "afmoe", ] diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index e8ca72aa18..b779abaa63 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -195,6 +195,15 @@ def normalize_config(cfg): cfg.model_config_type = model_config.model_type + # Resolve inner text backbone type for VLM wrappers (e.g. mistral3 -> mistral4) + if callable(getattr(model_config, "get_text_config", None)): + text_config = model_config.get_text_config() + if ( + hasattr(text_config, "model_type") + and text_config.model_type != model_config.model_type + ): + cfg.model_config_type_text = text_config.model_type + # figure out if the model is llama cfg.is_llama_derived_model = ( ( From d230cbbde389d1e5322cbdd0e4e6336bc8b873a8 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 17 Mar 2026 09:43:24 +0700 Subject: [PATCH 1194/1405] chore(doc): update readme (#3503) [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a70dc8edf9..a425e45b8b 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ ## 🎉 Latest Updates - 2026/03: - - New model support has been added in Axolotl for [[Qwen3.5, Qwen3.5 MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3.5), [GLM-4.7-Flash](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm47-flash), [GLM-4.6V](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm46v), and [GLM-4.5-Air](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm45). + - New model support has been added in Axolotl for [Mistral Small 4](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/mistral4), [Qwen3.5, Qwen3.5 MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3.5), [GLM-4.7-Flash](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm47-flash), [GLM-4.6V](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm46v), and [GLM-4.5-Air](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm45). - [MoE expert quantization](https://docs.axolotl.ai/docs/expert_quantization.html) support (via `quantize_moe_experts: true`) greatly reduces VRAM when training MoE models (FSDP2 compat). - 2026/02: - [ScatterMoE LoRA](https://github.com/axolotl-ai-cloud/axolotl/pull/3410) support. LoRA fine-tuning directly on MoE expert weights using custom Triton kernels. From 830e9f7eafc9f8979c38837762c0a73efb2f6c13 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 16 Mar 2026 23:47:00 -0400 Subject: [PATCH 1195/1405] automatically enable tf32 if supported (#3473) [skip ci] * automatically enable tf32 if supported * update fixtures * handle only when True * Address CR comments * address readability from pr comment * simplify --- src/axolotl/cli/config.py | 3 ++- src/axolotl/core/builders/base.py | 2 +- src/axolotl/utils/config/__init__.py | 2 +- src/axolotl/utils/schemas/config.py | 14 +++++++++++--- src/axolotl/utils/schemas/internal/__init__.py | 1 + tests/e2e/test_llama.py | 6 +++++- tests/test_validation_dataset.py | 1 + tests/utils/schemas/validation/test_moe_quant.py | 8 +++++++- 8 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index f81ba0b2e0..568c115cca 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -11,7 +11,7 @@ import requests import torch import yaml -from transformers.utils import is_torch_bf16_gpu_available +from transformers.utils import is_torch_bf16_gpu_available, is_torch_tf32_available from axolotl.integrations.base import PluginManager from axolotl.telemetry.errors import send_errors @@ -310,6 +310,7 @@ def load_cfg( capabilities={ "bf16": is_torch_bf16_gpu_available(), "fp8": compute_supports_fp8(), + "tf32": is_torch_tf32_available(), "n_gpu": int(os.environ.get("WORLD_SIZE", 1)), "compute_capability": gpu_version, }, diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index c23433866b..a149566b30 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -250,7 +250,7 @@ def _configure_warmup_and_logging( def _configure_precision_settings(self, training_args_kwargs: dict): training_args_kwargs["fp16"] = (self.cfg.fp16 and not self.cfg.bf16) or False - training_args_kwargs["tf32"] = self.cfg.tf32 + training_args_kwargs["tf32"] = True if self.cfg.tf32 is True else False if self.cfg.bf16 == "full": training_args_kwargs["bf16_full_eval"] = True else: diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index b779abaa63..61096cb869 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -84,7 +84,7 @@ def resolve_dtype(cfg): cfg.fp16 = True cfg.bf16 = False else: - if cfg.tf32: + if cfg.tf32 is True: torch.set_float32_matmul_precision("high") if is_torch_greater_or_equal("2.9.0"): torch.backends.fp32_precision = "tf32" diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 5ea340c370..a4eadf5cf6 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -407,9 +407,11 @@ class AxolotlInputConfig( default=None, json_schema_extra={"description": "No AMP (automatic mixed precision)"}, ) # for non-AMP cases - tf32: bool | None = Field( - default=None, - json_schema_extra={"description": "Use CUDA tf32 - require >=ampere"}, + tf32: Literal["auto"] | bool | None = Field( + default="auto", + json_schema_extra={ + "description": "bool to use CUDA tf32 or 'auto' for automatic detection - require >=ampere" + }, ) float32: bool | None = None @@ -1218,6 +1220,12 @@ def check_bf16(self): ) return self + @model_validator(mode="after") + def check_tf32(self): + if self.tf32 == "auto": + self.tf32 = self.capabilities.tf32 + return self + @model_validator(mode="after") def check_fp8(self): if self.fp8 and not self.capabilities.fp8: diff --git a/src/axolotl/utils/schemas/internal/__init__.py b/src/axolotl/utils/schemas/internal/__init__.py index 692dee8334..78cc636db9 100644 --- a/src/axolotl/utils/schemas/internal/__init__.py +++ b/src/axolotl/utils/schemas/internal/__init__.py @@ -10,6 +10,7 @@ class GPUCapabilities(BaseModel): bf16: bool = Field(default=False) fp8: bool = Field(default=False) + tf32: bool = Field(default=False) n_gpu: int = Field(default=1) n_node: int = Field(default=1) compute_capability: Optional[str] = Field(default=None) diff --git a/tests/e2e/test_llama.py b/tests/e2e/test_llama.py index 795b0de378..47a7915777 100644 --- a/tests/e2e/test_llama.py +++ b/tests/e2e/test_llama.py @@ -2,6 +2,8 @@ E2E tests for llama """ +import pytest + from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -143,7 +145,8 @@ def test_fix_untrained_tokens_already_trained(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) - def test_batch_flattening(self, temp_dir): + @pytest.mark.parametrize("tf32", ["auto", False]) + def test_batch_flattening(self, tf32, temp_dir): cfg = DictDefault( { "base_model": "HuggingFaceTB/SmolLM2-135M", @@ -171,6 +174,7 @@ def test_batch_flattening(self, temp_dir): "sample_packing": False, "batch_flattening": True, "bf16": True, + "tf32": tf32, "save_first_step": False, } ) diff --git a/tests/test_validation_dataset.py b/tests/test_validation_dataset.py index 464812a90f..27740db182 100644 --- a/tests/test_validation_dataset.py +++ b/tests/test_validation_dataset.py @@ -68,6 +68,7 @@ def _check_config(): cfg, capabilities={ "bf16": "false", + "tf32": "false", "n_gpu": 1, "compute_capability": "8.0", }, diff --git a/tests/utils/schemas/validation/test_moe_quant.py b/tests/utils/schemas/validation/test_moe_quant.py index 2c34582c34..52b6f52c5f 100644 --- a/tests/utils/schemas/validation/test_moe_quant.py +++ b/tests/utils/schemas/validation/test_moe_quant.py @@ -8,7 +8,13 @@ @pytest.fixture() def gpu_caps(): - return {"compute_capability": "sm_89", "bf16": True, "n_gpu": 1, "n_node": 1} + return { + "compute_capability": "sm_89", + "bf16": True, + "tf32": False, + "n_gpu": 1, + "n_node": 1, + } @pytest.fixture() From 8f3fb517b3abc0d42523f7c7e7c752bf84a07676 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 16 Mar 2026 23:47:40 -0400 Subject: [PATCH 1196/1405] consolidate behavioud of routing in scattermoe kernels (#3475) * consolidate behavioud of routing in scattermoe kernels * collect telemetry on best chosen autotuned kernel * properly collect data * Fix property name and get smem too * handle issues raised by coderabbit * add tests for parity before refactoring --- .../integrations/kernels/autotune_callback.py | 120 +++++ .../kernels/autotune_collector.py | 114 +++++ .../kernels/libs/scattermoe_lora/layers.py | 199 ++++++-- src/axolotl/integrations/kernels/plugin.py | 10 + .../test_scattermoe_lora_kernels.py | 347 +++++++++++++ tests/integrations/test_routing_parity.py | 474 ++++++++++++++++++ .../test_scattermoe_autotune_telemetry.py | 367 ++++++++++++++ tests/integrations/test_scattermoe_lora.py | 392 ++++++++++++++- 8 files changed, 1988 insertions(+), 35 deletions(-) create mode 100644 src/axolotl/integrations/kernels/autotune_callback.py create mode 100644 src/axolotl/integrations/kernels/autotune_collector.py create mode 100644 tests/integrations/test_routing_parity.py create mode 100644 tests/integrations/test_scattermoe_autotune_telemetry.py diff --git a/src/axolotl/integrations/kernels/autotune_callback.py b/src/axolotl/integrations/kernels/autotune_callback.py new file mode 100644 index 0000000000..aa4cbbab1c --- /dev/null +++ b/src/axolotl/integrations/kernels/autotune_callback.py @@ -0,0 +1,120 @@ +"""Trainer callback for reporting Triton autotune results from scattermoe-lora kernels.""" + +import logging + +import torch +from transformers import ( + TrainerCallback, + TrainerControl, + TrainerState, + TrainingArguments, +) + +LOG = logging.getLogger(__name__) + +# Give up looking for autotune data after this many training steps. +_MAX_POLL_STEP = 5 + + +def _get_gpu_info() -> dict: + """Return basic GPU identification for the current device.""" + if not torch.cuda.is_available(): + return {} + try: + idx = torch.cuda.current_device() + props = torch.cuda.get_device_properties(idx) + return { + "gpu_name": props.name, + "gpu_compute_capability": f"{props.major}.{props.minor}", + "gpu_memory_bytes": props.total_memory, + } + except Exception: # pylint: disable=broad-exception-caught + return {} + + +def _get_smem_capacity() -> dict: + """Return shared memory capacity from the runtime lora_ops module.""" + try: + from axolotl.integrations.kernels.autotune_collector import ( + _find_lora_ops_module, + ) + + lora_ops = _find_lora_ops_module() + if lora_ops is None: + return {} + fn = getattr(lora_ops, "_get_smem_capacity", None) + if fn is None: + return {} + return {"smem_capacity_bytes": fn()} + except Exception: # pylint: disable=broad-exception-caught + return {} + + +class AutotuneReportCallback(TrainerCallback): + """Reports Triton kernel autotune selections via telemetry. + + Fires **once** after the first training step completes (step 1), at + which point the forward and backward passes have both run and the + autotuned kernels have populated their caches. If for some reason + the caches are still empty (e.g. the kernel was never invoked), the + callback retries on subsequent steps up to ``_MAX_POLL_STEP`` and + then stops polling. + + After reporting (or giving up) every subsequent ``on_step_end`` + call short-circuits on the ``_reported`` flag — zero hot-path cost. + """ + + def __init__(self): + self._reported = False + + # pylint: disable=unused-argument + def on_step_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + if self._reported: + return + + # Lazy import — Triton / scattermoe kernels may not be installed. + from axolotl.integrations.kernels.autotune_collector import ( + collect_autotune_configs, + ) + + configs = collect_autotune_configs() + + if not configs: + if state.global_step >= _MAX_POLL_STEP: + LOG.debug( + "No autotune data found after %d steps; giving up.", + state.global_step, + ) + self._reported = True + return + + self._reported = True + + from axolotl.telemetry.manager import TelemetryManager + + telemetry_manager = TelemetryManager.get_instance() + if not telemetry_manager.enabled: + return + + properties = { + "kernel_count": len(configs), + "kernels": configs, + } + properties.update(_get_gpu_info()) + properties.update(_get_smem_capacity()) + + telemetry_manager.send_event( + event_type="scattermoe-autotune", + properties=properties, + ) + + LOG.info( + "Reported %d scattermoe kernel autotune config(s) to telemetry.", + len(configs), + ) diff --git a/src/axolotl/integrations/kernels/autotune_collector.py b/src/axolotl/integrations/kernels/autotune_collector.py new file mode 100644 index 0000000000..ef4111dcf0 --- /dev/null +++ b/src/axolotl/integrations/kernels/autotune_collector.py @@ -0,0 +1,114 @@ +"""Collect Triton autotune results from scattermoe-lora kernels. + +This module reads the ``.cache`` attribute from Triton ``@triton.autotune`` +decorated kernel objects and returns structured dicts describing the selected +configurations. It has **no** telemetry dependency — callers decide what to +do with the data. +""" + +import logging +import sys +from types import ModuleType +from typing import Any + +LOG = logging.getLogger(__name__) + +# (human-readable name, attribute on the lora_ops module) +_KERNEL_REGISTRY: list[tuple[str, str]] = [ + ("scatter2scatter_lora_fwd", "_scatter2scatter_lora"), + ("scatter2scatter_lora_dX", "_scatter2scatter_lora_dX"), + ("group_bwd_lora", "_group_bwd_lora"), + ("group_bwd_lora_fused", "_group_bwd_lora_fused"), +] + +# The autotune key declared on every kernel: key=["M", "N", "K"] +_KEY_NAMES: list[str] = ["M", "N", "K"] + + +def _parse_key_tuple(key_tuple: tuple) -> dict[str, Any]: + """Turn the autotune cache key tuple into a labelled dict. + + Triton builds the cache key from the values of the declared ``key`` + args (``M``, ``N``, ``K``) followed by dtype signature elements. + We label the first three and store the rest under ``_extra``. + """ + result: dict[str, Any] = {} + for i, name in enumerate(_KEY_NAMES): + if i < len(key_tuple): + result[name] = key_tuple[i] + if len(key_tuple) > len(_KEY_NAMES): + result["_extra"] = [str(v) for v in key_tuple[len(_KEY_NAMES) :]] + return result + + +def _find_lora_ops_module() -> ModuleType | None: + """Locate the *runtime* ``lora_ops`` module in ``sys.modules``. + + The HF ``kernels`` package loads ``scattermoe_lora`` via + ``import_from_path`` which registers it in ``sys.modules`` under a + hash-suffixed name (e.g. ``scattermoe_lora_a1b2c3d4``). A normal + import (``from axolotl.integrations.kernels...``) would create a + *separate* module instance whose kernel objects have empty + ``.cache`` dicts because autotuning ran on the runtime copy. + + We search ``sys.modules`` for any module whose name contains + ``lora_ops`` and that has the ``_scatter2scatter_lora`` kernel + attribute — that is the runtime copy with populated caches. + """ + for name, module in sys.modules.items(): + if ( + module is not None + and "lora_ops" in name + and hasattr(module, "_scatter2scatter_lora") + ): + return module + return None + + +def collect_autotune_configs() -> list[dict[str, Any]]: + """Read autotune caches from the four scattermoe-lora kernels. + + Returns a (possibly empty) list of dicts, each containing: + + * ``kernel`` – human-readable kernel name + * ``key`` – dict with the ``M``/``N``/``K`` problem dimensions + * ``config`` – dict with the selected tile sizes, ``num_warps``, + and ``num_stages`` + + Returns ``[]`` if the kernel module cannot be found or if no + autotune cache entries exist yet. + """ + lora_ops = _find_lora_ops_module() + if lora_ops is None: + LOG.debug( + "lora_ops module not found in sys.modules; skipping autotune collection" + ) + return [] + + results: list[dict[str, Any]] = [] + + for friendly_name, attr_name in _KERNEL_REGISTRY: + kernel_fn = getattr(lora_ops, attr_name, None) + if kernel_fn is None: + continue + + cache = getattr(kernel_fn, "cache", None) + if not cache: + continue + + for key_tuple, config in cache.items(): + config_dict = dict(config.kwargs) + config_dict["num_warps"] = config.num_warps + config_dict["num_stages"] = config.num_stages + if getattr(config, "num_ctas", None) is not None: + config_dict["num_ctas"] = config.num_ctas + + results.append( + { + "kernel": friendly_name, + "key": _parse_key_tuple(key_tuple), + "config": config_dict, + } + ) + + return results diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py index a425774833..5125e8801d 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py @@ -220,6 +220,158 @@ def _unwrap_experts_lora(experts_module): return base_experts, gup_lora, down_lora +# ============================================================================= +# Routing helpers +# ============================================================================= + + +def _softmax_topk_route( + moe_block, base_gate, hidden_states, gate_weight, gate_lora_delta +): + """Softmax→topk routing (Qwen, OLMoE, Mixtral, MiniMax). + + Returns: + (routing_weights [T, K], selected_experts [T, K], top_k, num_experts) + """ + router_logits = F.linear(hidden_states, gate_weight) + if gate_lora_delta is not None: + router_logits = router_logits + F.linear(hidden_states, gate_lora_delta) + routing_weights = F.softmax(router_logits, dim=-1, dtype=torch.float32) + + top_k = base_gate.top_k + num_experts = base_gate.num_experts + routing_weights, selected_experts = torch.topk(routing_weights, top_k, dim=-1) + + if getattr(base_gate, "norm_topk_prob", True): + routing_weights = routing_weights / routing_weights.sum(dim=-1, keepdim=True) + + return routing_weights, selected_experts, top_k, num_experts + + +def _sigmoid_topk_route( + moe_block, base_gate, hidden_states, gate_weight, gate_lora_delta +): + """Sigmoid→topk routing (GLM, DeepSeek V3, MiniMax M2). + + Supports: + - ``e_score_correction_bias`` on gate or moe_block + - Group-based expert selection when ``n_group > 1`` + - ``routed_scaling_factor`` applied to final weights + - Final weights gathered from original sigmoid probs (not bias-corrected) + + Returns: + (routing_weights [T, K], selected_experts [T, K], top_k, num_experts) + """ + router_logits = F.linear(hidden_states.float(), gate_weight.float()) + if gate_lora_delta is not None: + router_logits = router_logits + F.linear( + hidden_states.float(), gate_lora_delta.float() + ) + router_probs = router_logits.sigmoid() # [T, E] + + top_k = getattr(moe_block, "top_k", getattr(base_gate, "top_k", None)) + num_experts = getattr(moe_block, "n_routed_experts", gate_weight.shape[0]) + + # Bias-corrected scores for expert selection (not used for final weights). + # glm_moe_dsa/deepseek_v3 store the bias on gate; minimax_m2 on the block. + e_score_correction_bias = getattr(base_gate, "e_score_correction_bias", None) + if e_score_correction_bias is None: + e_score_correction_bias = getattr(moe_block, "e_score_correction_bias", None) + if e_score_correction_bias is not None: + scores_for_choice = router_probs + e_score_correction_bias + else: + scores_for_choice = router_probs + + # Group-based selection: pick top groups, mask the rest + n_group = getattr(moe_block, "n_group", 1) + if n_group > 1: + group_scores = ( + scores_for_choice.view(-1, n_group, num_experts // n_group) + .topk(2, dim=-1)[0] + .sum(dim=-1) + ) # [T, n_group] + topk_group = getattr(moe_block, "topk_group", n_group) + group_idx = torch.topk(group_scores, k=topk_group, dim=-1, sorted=False)[1] + group_mask = torch.zeros_like(group_scores) + group_mask.scatter_(1, group_idx, 1) + score_mask = ( + group_mask.unsqueeze(-1) + .expand(-1, n_group, num_experts // n_group) + .reshape(-1, num_experts) + ) + scores_for_choice = scores_for_choice.masked_fill(~score_mask.bool(), 0.0) + + # Final topk from (possibly masked) scores + topk_indices = torch.topk(scores_for_choice, k=top_k, dim=-1, sorted=False)[1] + + # Gather weights from original sigmoid scores (not bias-corrected) + topk_weights = router_probs.gather(1, topk_indices) + + # Optional renormalization + scaling + if getattr(moe_block, "norm_topk_prob", True): + topk_weights = topk_weights / (topk_weights.sum(dim=-1, keepdim=True) + 1e-20) + routed_scaling_factor = getattr(moe_block, "routed_scaling_factor", 1.0) + topk_weights = topk_weights * routed_scaling_factor + + return topk_weights, topk_indices, top_k, num_experts + + +def _route(moe_block, base_gate, hidden_states, gate_weight, gate_lora_delta): + """Dispatch to the correct routing strategy based on block attributes. + + Detects sigmoid routing by the presence of ``e_score_correction_bias`` + on either the gate or the moe_block. + """ + has_sigmoid = ( + getattr(base_gate, "e_score_correction_bias", None) is not None + or getattr(moe_block, "e_score_correction_bias", None) is not None + ) + if has_sigmoid: + return _sigmoid_topk_route( + moe_block, base_gate, hidden_states, gate_weight, gate_lora_delta + ) + return _softmax_topk_route( + moe_block, base_gate, hidden_states, gate_weight, gate_lora_delta + ) + + +# ============================================================================= +# Shared expert helpers +# ============================================================================= + + +def _compute_shared_expert(moe_block, hidden_states_flat): + """Compute shared expert output if the block has one. + + Handles singular (qwen2_moe: ``shared_expert``), plural + (glm_moe_dsa/deepseek_v3: ``shared_experts``), and MLP + (hunyuan_v1_moe: ``shared_mlp``) attribute names. + + peft wraps individual linear layers inside the shared expert with + standard LoRA — calling forward() handles this transparently. + """ + shared_expert = ( + getattr(moe_block, "shared_expert", None) + or getattr(moe_block, "shared_experts", None) + or getattr(moe_block, "shared_mlp", None) + ) + if shared_expert is None: + return None + + shared_expert_output = shared_expert(hidden_states_flat) + + # Optional sigmoid gate (Qwen2MoE pattern). + # shared_expert_gate may also be peft-wrapped (standard LoRA + # on nn.Linear), its forward() applies LoRA automatically. + shared_expert_gate = getattr(moe_block, "shared_expert_gate", None) + if shared_expert_gate is not None: + shared_expert_output = ( + F.sigmoid(shared_expert_gate(hidden_states_flat)) * shared_expert_output + ) + + return shared_expert_output + + # ============================================================================= # Layer classes # ============================================================================= @@ -281,16 +433,18 @@ def forward(self, layer_input): class HFScatterMoEGatedMLP(nn.Module): """ - ScatterMoE-accelerated forward pass for HF MoEs (OLMoE / Qwen2MoE). + ScatterMoE-accelerated forward pass for HF MoEs. Used as a kernel layer via the HF ``kernels`` library. The ``forward`` - method replaces the original ``OlmoeSparseMoeBlock.forward``. + method replaces the original SparseMoeBlock.forward. - Supports both full-parameter training and LoRA fine-tuning: + Supports: - * **Full-param**: uses ``parallel_linear`` (base ScatterMoE kernel) - * **LoRA**: detects peft ``ParamWrapper`` on ``self.experts``, extracts - adapter weights, and uses ``parallel_linear_lora`` (fused kernel) + * **Softmax→topk routing**: OLMoE, Qwen2/3MoE, Mixtral, MiniMax + * **Sigmoid→topk routing**: GLM, DeepSeek V3, MiniMax M2 + * **Full-parameter training**: uses ``parallel_linear`` (base ScatterMoE) + * **LoRA fine-tuning**: detects peft ``ParamWrapper`` on ``self.experts``, + extracts adapter weights, and uses ``parallel_linear_lora`` (fused kernel) """ @staticmethod @@ -302,7 +456,7 @@ def forward(self: nn.Module, layer_input: torch.Tensor): self: The MoeSparseMoeBlock module containing: - self.gate: Router (or peft ParamWrapper wrapping it) - self.experts: Experts module (or peft ParamWrapper chain) - - self.shared_expert: Optional shared expert (e.g. Qwen2MoE) + - self.shared_expert(s): Optional shared expert - self.shared_expert_gate: Optional shared expert gate layer_input: Input tensor [batch_size, seq_len, hidden_size] @@ -313,38 +467,17 @@ def forward(self: nn.Module, layer_input: torch.Tensor): hidden_states_flat = layer_input.view(-1, hidden_dim) # ==================================================================== - # Shared Expert (if present, e.g. Qwen2MoE) + # Shared Expert (if present, e.g. Qwen2MoE, DeepSeek V3) # ==================================================================== - # peft wraps individual linear layers inside shared_expert with - # standard LoRA — calling forward() handles this transparently. - if hasattr(self, "shared_expert") and self.shared_expert is not None: - shared_expert_output = self.shared_expert(hidden_states_flat) - # shared_expert_gate may also be peft-wrapped (standard LoRA - # on nn.Linear), its forward() applies LoRA automatically. - shared_expert_gate_output = F.sigmoid( - self.shared_expert_gate(hidden_states_flat) - ) - shared_expert_output = shared_expert_output * shared_expert_gate_output - else: - shared_expert_output = None + shared_expert_output = _compute_shared_expert(self, hidden_states_flat) # ==================================================================== # Router Computation (with optional gate LoRA) # ==================================================================== base_gate, gate_weight, gate_lora_delta = _unwrap_gate_lora(self.gate) - router_logits = F.linear(hidden_states_flat, gate_weight) - if gate_lora_delta is not None: - router_logits = router_logits + F.linear( - hidden_states_flat, gate_lora_delta - ) - routing_weights = F.softmax(router_logits, dim=1, dtype=torch.float) - - top_k = base_gate.top_k - num_experts = base_gate.num_experts - routing_weights, selected_experts = torch.topk(routing_weights, top_k, dim=-1) - - if base_gate.norm_topk_prob: - routing_weights /= routing_weights.sum(dim=-1, keepdim=True) + routing_weights, selected_experts, top_k, num_experts = _route( + self, base_gate, hidden_states_flat, gate_weight, gate_lora_delta + ) routing_weights = routing_weights.to(hidden_states_flat.dtype) sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = flatten_sort_count( diff --git a/src/axolotl/integrations/kernels/plugin.py b/src/axolotl/integrations/kernels/plugin.py index f085e481c6..351db5ef23 100644 --- a/src/axolotl/integrations/kernels/plugin.py +++ b/src/axolotl/integrations/kernels/plugin.py @@ -110,6 +110,16 @@ def _register_kernels(self): } ) + def add_callbacks_pre_trainer(self, cfg, model): + callbacks = [] + if cfg.use_scattermoe: + from axolotl.integrations.kernels.autotune_callback import ( + AutotuneReportCallback, + ) + + callbacks.append(AutotuneReportCallback()) + return callbacks + def _kernelize_model(self, model_type: str): from kernels import replace_kernel_forward_from_hub diff --git a/tests/e2e/integrations/test_scattermoe_lora_kernels.py b/tests/e2e/integrations/test_scattermoe_lora_kernels.py index d11272c8f8..6f7f65b808 100644 --- a/tests/e2e/integrations/test_scattermoe_lora_kernels.py +++ b/tests/e2e/integrations/test_scattermoe_lora_kernels.py @@ -12,6 +12,7 @@ 3. Frozen weights: expert weight gradients are correctly skipped 4. Various configurations: top-k, grouped_in/out, with/without bias 5. Numerical stability: bf16/fp16 outputs within tolerance of fp32 reference +6. HFScatterMoEGatedMLP with sigmoid routing (GLM/DeepSeek/MiniMax M2) Test strategy: - Reference implementation uses pure PyTorch ops (no Triton) @@ -19,6 +20,8 @@ - Tolerances account for tf32 accumulation in Triton kernels """ +from types import SimpleNamespace + import pytest import torch @@ -1476,3 +1479,347 @@ def test_fused_dX_and_fused_gather(self): torch.testing.assert_close(X1.grad, X2.grad, atol=5e-2, rtol=5e-2) torch.testing.assert_close(A1.grad, A2.grad, atol=5e-2, rtol=5e-2) torch.testing.assert_close(B1.grad, B2.grad, atol=5e-2, rtol=5e-2) + + +# ============================================================================= +# Test: HFScatterMoEGatedMLP with Sigmoid Routing +# ============================================================================= + + +def _reference_moe_forward( + hidden_states, + gate_weight, + gate_up_proj, + down_proj, + act_fn, + routing_weights, + selected_experts, + num_experts, +): + """Pure PyTorch reference for a full MoE forward pass. + + Args: + hidden_states: [T, H] + gate_weight: [E, H] + gate_up_proj: [E, 2*FF, H] + down_proj: [E, H, FF] + act_fn: activation function (e.g. torch.nn.SiLU()) + routing_weights: [T, K] routing weights + selected_experts: [T, K] expert indices + num_experts: int + + Returns: + output: [T, H] + """ + T, H = hidden_states.shape + K = selected_experts.shape[1] + output = torch.zeros(T, H, device=hidden_states.device, dtype=hidden_states.dtype) + + for t in range(T): + for j in range(K): + e = selected_experts[t, j].item() + w = routing_weights[t, j].item() + + # gate_up projection + gup = hidden_states[t] @ gate_up_proj[e].T # [2*I] + I_dim = gup.shape[0] // 2 + gates = gup[:I_dim] + up = gup[I_dim:] + + # activation + h = act_fn(gates) * up + + # down projection + out = h @ down_proj[e].T # [H] + + output[t] += w * out + + return output + + +def _make_mock_sigmoid_moe_block( + T=16, H=64, FF=32, E=8, K=2, n_group=2, topk_group=1, bias_on_gate=True +): + """Create a mock MoE block with sigmoid routing for GPU testing.""" + gate_up_proj = torch.randn(E, 2 * FF, H, device="cuda") * 0.02 + down_proj = torch.randn(E, H, FF, device="cuda") * 0.02 + act_fn = torch.nn.SiLU() + + experts = SimpleNamespace( + gate_up_proj=gate_up_proj, + down_proj=down_proj, + act_fn=act_fn, + num_experts=E, + ) + + if bias_on_gate: + gate = SimpleNamespace( + weight=torch.randn(E, H, device="cuda") * 0.1, + e_score_correction_bias=torch.zeros(E, device="cuda"), + ) + moe_block = SimpleNamespace( + gate=gate, + experts=experts, + top_k=K, + n_routed_experts=E, + n_group=n_group, + topk_group=topk_group, + norm_topk_prob=True, + routed_scaling_factor=1.0, + ) + else: + # minimax_m2 style + gate = SimpleNamespace( + weight=torch.randn(E, H, device="cuda") * 0.1, + top_k=K, + ) + moe_block = SimpleNamespace( + gate=gate, + experts=experts, + top_k=K, + e_score_correction_bias=torch.zeros(E, device="cuda"), + ) + + return moe_block, T, H, FF, E, K + + +class TestHFScatterMoESigmoidRouting: + """Test HFScatterMoEGatedMLP forward with sigmoid routing on GPU.""" + + def test_forward_matches_reference_bias_on_gate(self): + """Forward pass with sigmoid routing (bias on gate) matches reference.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + HFScatterMoEGatedMLP, + _sigmoid_topk_route, + ) + + moe_block, T, H, FF, E, K = _make_mock_sigmoid_moe_block( + T=16, H=64, FF=32, E=8, K=2, n_group=2, topk_group=1, bias_on_gate=True + ) + + hidden = torch.randn(1, T, H, device="cuda") + + # Get routing for reference + gate = moe_block.gate + hidden_flat = hidden.view(-1, H) + routing_weights, selected_experts, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden_flat, gate.weight, None + ) + + # Reference output + ref_output = _reference_moe_forward( + hidden_flat, + gate.weight, + moe_block.experts.gate_up_proj, + moe_block.experts.down_proj, + moe_block.experts.act_fn, + routing_weights, + selected_experts, + E, + ) + + # Kernel output + kernel_output = HFScatterMoEGatedMLP.forward(moe_block, hidden) + kernel_output_flat = kernel_output.view(-1, H) + + torch.testing.assert_close( + kernel_output_flat.float(), + ref_output.float(), + atol=5e-2, + rtol=5e-2, + ) + + def test_forward_matches_reference_bias_on_block(self): + """Forward pass with sigmoid routing (minimax_m2 style, bias on block).""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + HFScatterMoEGatedMLP, + _sigmoid_topk_route, + ) + + moe_block, T, H, FF, E, K = _make_mock_sigmoid_moe_block( + T=16, H=64, FF=32, E=8, K=2, n_group=1, bias_on_gate=False + ) + + hidden = torch.randn(1, T, H, device="cuda") + hidden_flat = hidden.view(-1, H) + + gate = moe_block.gate + routing_weights, selected_experts, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden_flat, gate.weight, None + ) + + ref_output = _reference_moe_forward( + hidden_flat, + gate.weight, + moe_block.experts.gate_up_proj, + moe_block.experts.down_proj, + moe_block.experts.act_fn, + routing_weights, + selected_experts, + E, + ) + + kernel_output = HFScatterMoEGatedMLP.forward(moe_block, hidden) + kernel_output_flat = kernel_output.view(-1, H) + + torch.testing.assert_close( + kernel_output_flat.float(), + ref_output.float(), + atol=5e-2, + rtol=5e-2, + ) + + def test_softmax_routing_still_works(self): + """Verify softmax routing (Qwen/OLMoE) is not broken.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + HFScatterMoEGatedMLP, + _softmax_topk_route, + ) + + T, H, FF, E, K = 16, 64, 32, 4, 2 + gate_up_proj = torch.randn(E, 2 * FF, H, device="cuda") * 0.02 + down_proj = torch.randn(E, H, FF, device="cuda") * 0.02 + act_fn = torch.nn.SiLU() + + experts = SimpleNamespace( + gate_up_proj=gate_up_proj, + down_proj=down_proj, + act_fn=act_fn, + num_experts=E, + ) + gate = SimpleNamespace( + weight=torch.randn(E, H, device="cuda") * 0.1, + top_k=K, + num_experts=E, + norm_topk_prob=True, + ) + moe_block = SimpleNamespace(gate=gate, experts=experts) + + hidden = torch.randn(1, T, H, device="cuda") + hidden_flat = hidden.view(-1, H) + + routing_weights, selected_experts, _, _ = _softmax_topk_route( + moe_block, gate, hidden_flat, gate.weight, None + ) + + ref_output = _reference_moe_forward( + hidden_flat, + gate.weight, + gate_up_proj, + down_proj, + act_fn, + routing_weights, + selected_experts, + E, + ) + + kernel_output = HFScatterMoEGatedMLP.forward(moe_block, hidden) + kernel_output_flat = kernel_output.view(-1, H) + + torch.testing.assert_close( + kernel_output_flat.float(), + ref_output.float(), + atol=5e-2, + rtol=5e-2, + ) + + +class TestHFScatterMoESigmoidWithSharedExperts: + """Test HFScatterMoEGatedMLP with sigmoid routing + shared experts.""" + + def test_shared_experts_plural(self): + """DeepSeek V3 style: shared_experts attribute (plural).""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + HFScatterMoEGatedMLP, + ) + + T, H, FF, E, K = 8, 64, 32, 8, 2 + gate_up_proj = torch.randn(E, 2 * FF, H, device="cuda") * 0.02 + down_proj = torch.randn(E, H, FF, device="cuda") * 0.02 + act_fn = torch.nn.SiLU() + + experts = SimpleNamespace( + gate_up_proj=gate_up_proj, + down_proj=down_proj, + act_fn=act_fn, + num_experts=E, + ) + + # Shared expert as a simple linear for testing + shared_W = torch.randn(H, H, device="cuda") * 0.01 + shared_experts_fn = lambda x: x @ shared_W.T # noqa: E731 + + gate = SimpleNamespace( + weight=torch.randn(E, H, device="cuda") * 0.1, + e_score_correction_bias=torch.zeros(E, device="cuda"), + ) + moe_block = SimpleNamespace( + gate=gate, + experts=experts, + shared_experts=shared_experts_fn, + top_k=K, + n_routed_experts=E, + n_group=1, + norm_topk_prob=True, + routed_scaling_factor=1.0, + ) + + hidden = torch.randn(1, T, H, device="cuda") + + # Should not raise; output should include shared expert contribution + output = HFScatterMoEGatedMLP.forward(moe_block, hidden) + assert output.shape == (1, T, H) + + # Run without shared expert to verify it changes the output + moe_block_no_shared = SimpleNamespace( + gate=gate, + experts=experts, + top_k=K, + n_routed_experts=E, + n_group=1, + norm_topk_prob=True, + routed_scaling_factor=1.0, + ) + output_no_shared = HFScatterMoEGatedMLP.forward(moe_block_no_shared, hidden) + assert not torch.equal(output, output_no_shared) + + def test_shared_expert_with_gate(self): + """Qwen2MoE style: shared_expert + shared_expert_gate.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + HFScatterMoEGatedMLP, + ) + + T, H, FF, E, K = 8, 64, 32, 4, 2 + gate_up_proj = torch.randn(E, 2 * FF, H, device="cuda") * 0.02 + down_proj = torch.randn(E, H, FF, device="cuda") * 0.02 + act_fn = torch.nn.SiLU() + + experts = SimpleNamespace( + gate_up_proj=gate_up_proj, + down_proj=down_proj, + act_fn=act_fn, + num_experts=E, + ) + + shared_W = torch.randn(H, H, device="cuda") * 0.01 + shared_expert_fn = lambda x: x @ shared_W.T # noqa: E731 + # Gate that returns 0 -> sigmoid(0) = 0.5 + gate_W = torch.zeros(H, H, device="cuda") + shared_expert_gate_fn = lambda x: x @ gate_W.T # noqa: E731 + + gate = SimpleNamespace( + weight=torch.randn(E, H, device="cuda") * 0.1, + top_k=K, + num_experts=E, + norm_topk_prob=True, + ) + moe_block = SimpleNamespace( + gate=gate, + experts=experts, + shared_expert=shared_expert_fn, + shared_expert_gate=shared_expert_gate_fn, + ) + + hidden = torch.randn(1, T, H, device="cuda") + output = HFScatterMoEGatedMLP.forward(moe_block, hidden) + assert output.shape == (1, T, H) diff --git a/tests/integrations/test_routing_parity.py b/tests/integrations/test_routing_parity.py new file mode 100644 index 0000000000..cc668671c6 --- /dev/null +++ b/tests/integrations/test_routing_parity.py @@ -0,0 +1,474 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +Parity tests between scattermoe-lora and sonicmoe routing implementations. + +These tests verify that both implementations produce numerically identical +results for the same inputs, ensuring safe centralization of the routing code. + +ScatterMoE returns 2D tensors [T, K]; SonicMoE returns flattened 1D [T*K]. +The core algorithm should be identical — only the output format differs. +""" + +from types import SimpleNamespace + +import pytest +import torch + + +def _require_triton(): + pytest.importorskip("triton") + + +# ============================================================================ +# Fixtures / helpers +# ============================================================================ + + +def _make_softmax_block(T=8, H=16, E=4, K=2): + """Qwen/OLMoE-style block usable by both implementations.""" + gate = SimpleNamespace( + weight=torch.randn(E, H), + top_k=K, + num_experts=E, + norm_topk_prob=True, + ) + moe_block = SimpleNamespace(gate=gate) + hidden = torch.randn(T, H) + return moe_block, gate, hidden, T, H, E, K + + +def _make_sigmoid_block( + T=8, H=16, E=16, K=4, n_group=2, topk_group=1, bias_on_gate=True +): + """GLM/DeepSeek-style block usable by both implementations.""" + if bias_on_gate: + gate = SimpleNamespace( + weight=torch.randn(E, H), + e_score_correction_bias=torch.zeros(E), + ) + moe_block = SimpleNamespace( + gate=gate, + top_k=K, + n_routed_experts=E, + n_group=n_group, + topk_group=topk_group, + norm_topk_prob=True, + routed_scaling_factor=1.0, + ) + else: + # minimax_m2 style: bias on block + gate = SimpleNamespace( + weight=torch.randn(E, H), + top_k=K, + ) + moe_block = SimpleNamespace( + gate=gate, + top_k=K, + e_score_correction_bias=torch.zeros(E), + ) + return moe_block, gate, hidden_states(T, H), T, H, E, K + + +def hidden_states(T, H): + return torch.randn(T, H) + + +# ============================================================================ +# 1. Softmax routing parity +# ============================================================================ + + +class TestSoftmaxRoutingParity: + """Verify scattermoe and sonicmoe softmax routing produce identical results.""" + + @pytest.fixture(autouse=True) + def _require(self): + _require_triton() + + def test_weights_match(self): + """2D weights from scattermoe == reshaped 1D weights from sonicmoe.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _softmax_topk_route, + ) + from axolotl.integrations.kernels.sonicmoe.routing import softmax_topk_routing + + moe_block, gate, hidden, T, H, E, K = _make_softmax_block() + + # ScatterMoE path (no LoRA delta) + sm_weights, sm_experts, sm_topk, sm_E = _softmax_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + + # SonicMoE path + sonic_scores, sonic_tok_idx, sonic_exp_idx, sonic_logits = softmax_topk_routing( + hidden, moe_block + ) + + # ScatterMoE returns [T, K], SonicMoE returns [T*K] flattened + sonic_weights_2d = sonic_scores.reshape(T, K) + sonic_experts_2d = sonic_exp_idx.reshape(T, K) + + assert sm_topk == K + assert sm_E == E + + # Both should select the same experts and produce the same weights + assert torch.equal(sm_experts, sonic_experts_2d.to(sm_experts.dtype)) + assert torch.allclose(sm_weights, sonic_weights_2d, atol=1e-6) + + def test_logits_not_returned_by_scattermoe(self): + """ScatterMoE doesn't return logits; SonicMoE does — verify SonicMoE logits shape.""" + from axolotl.integrations.kernels.sonicmoe.routing import softmax_topk_routing + + moe_block, gate, hidden, T, H, E, K = _make_softmax_block() + _, _, _, logits = softmax_topk_routing(hidden, moe_block) + assert logits.shape == (T, E) + + def test_no_renorm(self): + """With norm_topk_prob=False, both should skip renormalization.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _softmax_topk_route, + ) + from axolotl.integrations.kernels.sonicmoe.routing import softmax_topk_routing + + moe_block, gate, hidden, T, H, E, K = _make_softmax_block() + gate.norm_topk_prob = False + + sm_weights, sm_experts, _, _ = _softmax_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + sonic_scores, _, sonic_exp_idx, _ = softmax_topk_routing(hidden, moe_block) + + sonic_weights_2d = sonic_scores.reshape(T, K) + sonic_experts_2d = sonic_exp_idx.reshape(T, K) + + assert torch.equal(sm_experts, sonic_experts_2d.to(sm_experts.dtype)) + assert torch.allclose(sm_weights, sonic_weights_2d, atol=1e-6) + + def test_various_expert_counts(self): + """Parity across different E and K values.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _softmax_topk_route, + ) + from axolotl.integrations.kernels.sonicmoe.routing import softmax_topk_routing + + for E, K in [(2, 1), (8, 2), (16, 4), (32, 8)]: + moe_block, gate, hidden, T, H, _, _ = _make_softmax_block(E=E, K=K) + + sm_weights, sm_experts, _, _ = _softmax_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + sonic_scores, _, sonic_exp_idx, _ = softmax_topk_routing(hidden, moe_block) + + sonic_weights_2d = sonic_scores.reshape(T, K) + sonic_experts_2d = sonic_exp_idx.reshape(T, K) + + assert torch.equal(sm_experts, sonic_experts_2d.to(sm_experts.dtype)), ( + f"Expert mismatch for E={E}, K={K}" + ) + assert torch.allclose(sm_weights, sonic_weights_2d, atol=1e-6), ( + f"Weight mismatch for E={E}, K={K}" + ) + + +# ============================================================================ +# 2. Sigmoid routing parity +# ============================================================================ + + +class TestSigmoidRoutingParity: + """Verify scattermoe and sonicmoe sigmoid routing produce identical results.""" + + @pytest.fixture(autouse=True) + def _require(self): + _require_triton() + + def test_weights_match_with_groups(self): + """Both implementations should produce identical weights with group selection.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + from axolotl.integrations.kernels.sonicmoe.routing import sigmoid_topk_routing + + moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( + E=16, K=4, n_group=2, topk_group=1, bias_on_gate=True + ) + + sm_weights, sm_experts, sm_topk, sm_E = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + + sonic_scores, sonic_tok_idx, sonic_exp_idx, sonic_logits = sigmoid_topk_routing( + hidden, moe_block + ) + + sonic_weights_2d = sonic_scores.reshape(T, K) + sonic_experts_2d = sonic_exp_idx.reshape(T, K) + + assert sm_topk == K + assert sm_E == E + + # Sort experts within each token to handle different topk orderings + sm_sorted, sm_order = sm_experts.sort(dim=-1) + sonic_sorted, sonic_order = sonic_experts_2d.to(sm_experts.dtype).sort(dim=-1) + + assert torch.equal(sm_sorted, sonic_sorted) + + # Gather weights in sorted order for comparison + sm_weights_sorted = sm_weights.gather(1, sm_order) + sonic_weights_sorted = sonic_weights_2d.gather(1, sonic_order) + assert torch.allclose(sm_weights_sorted, sonic_weights_sorted, atol=1e-6) + + def test_weights_match_no_groups(self): + """Both implementations match without group selection (n_group=1).""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + from axolotl.integrations.kernels.sonicmoe.routing import sigmoid_topk_routing + + moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( + E=16, K=4, n_group=1, topk_group=1, bias_on_gate=True + ) + + sm_weights, sm_experts, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + sonic_scores, _, sonic_exp_idx, _ = sigmoid_topk_routing(hidden, moe_block) + + sonic_weights_2d = sonic_scores.reshape(T, K) + sonic_experts_2d = sonic_exp_idx.reshape(T, K) + + # Sort for comparison (topk with sorted=False may differ in order) + sm_sorted, sm_order = sm_experts.sort(dim=-1) + sonic_sorted, sonic_order = sonic_experts_2d.to(sm_experts.dtype).sort(dim=-1) + + assert torch.equal(sm_sorted, sonic_sorted) + sm_weights_sorted = sm_weights.gather(1, sm_order) + sonic_weights_sorted = sonic_weights_2d.gather(1, sonic_order) + assert torch.allclose(sm_weights_sorted, sonic_weights_sorted, atol=1e-6) + + def test_bias_on_block_parity(self): + """minimax_m2 style: bias on block, not gate.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + from axolotl.integrations.kernels.sonicmoe.routing import sigmoid_topk_routing + + moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( + E=16, K=4, n_group=1, bias_on_gate=False + ) + + sm_weights, sm_experts, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + sonic_scores, _, sonic_exp_idx, _ = sigmoid_topk_routing(hidden, moe_block) + + sonic_weights_2d = sonic_scores.reshape(T, K) + sonic_experts_2d = sonic_exp_idx.reshape(T, K) + + sm_sorted, sm_order = sm_experts.sort(dim=-1) + sonic_sorted, sonic_order = sonic_experts_2d.to(sm_experts.dtype).sort(dim=-1) + + assert torch.equal(sm_sorted, sonic_sorted) + sm_weights_sorted = sm_weights.gather(1, sm_order) + sonic_weights_sorted = sonic_weights_2d.gather(1, sonic_order) + assert torch.allclose(sm_weights_sorted, sonic_weights_sorted, atol=1e-6) + + def test_scaling_factor_parity(self): + """routed_scaling_factor applied identically by both.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + from axolotl.integrations.kernels.sonicmoe.routing import sigmoid_topk_routing + + moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( + n_group=1, bias_on_gate=True + ) + moe_block.routed_scaling_factor = 2.5 + + sm_weights, sm_experts, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + sonic_scores, _, sonic_exp_idx, _ = sigmoid_topk_routing(hidden, moe_block) + + sonic_weights_2d = sonic_scores.reshape(T, K) + sonic_experts_2d = sonic_exp_idx.reshape(T, K) + + sm_sorted, sm_order = sm_experts.sort(dim=-1) + sonic_sorted, sonic_order = sonic_experts_2d.to(sm_experts.dtype).sort(dim=-1) + + assert torch.equal(sm_sorted, sonic_sorted) + sm_weights_sorted = sm_weights.gather(1, sm_order) + sonic_weights_sorted = sonic_weights_2d.gather(1, sonic_order) + assert torch.allclose(sm_weights_sorted, sonic_weights_sorted, atol=1e-6) + + def test_no_renorm_parity(self): + """norm_topk_prob=False produces same results in both.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + from axolotl.integrations.kernels.sonicmoe.routing import sigmoid_topk_routing + + moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( + n_group=1, bias_on_gate=True + ) + moe_block.norm_topk_prob = False + + sm_weights, sm_experts, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + sonic_scores, _, sonic_exp_idx, _ = sigmoid_topk_routing(hidden, moe_block) + + sonic_weights_2d = sonic_scores.reshape(T, K) + sonic_experts_2d = sonic_exp_idx.reshape(T, K) + + sm_sorted, sm_order = sm_experts.sort(dim=-1) + sonic_sorted, sonic_order = sonic_experts_2d.to(sm_experts.dtype).sort(dim=-1) + + assert torch.equal(sm_sorted, sonic_sorted) + sm_weights_sorted = sm_weights.gather(1, sm_order) + sonic_weights_sorted = sonic_weights_2d.gather(1, sonic_order) + assert torch.allclose(sm_weights_sorted, sonic_weights_sorted, atol=1e-6) + + +# ============================================================================ +# 3. Shared expert parity +# ============================================================================ + + +class TestSharedExpertParity: + """Verify both _compute_shared_expert implementations behave identically.""" + + @pytest.fixture(autouse=True) + def _require(self): + _require_triton() + + def _get_both_fns(self): + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _compute_shared_expert as scatter_compute, + ) + from axolotl.integrations.kernels.sonicmoe.patch import ( + _compute_shared_expert as sonic_compute, + ) + + return scatter_compute, sonic_compute + + def test_shared_expert_singular(self): + scatter_fn, sonic_fn = self._get_both_fns() + out = torch.randn(4, 8) + block = SimpleNamespace(shared_expert=lambda x: out) + hidden = torch.randn(4, 8) + + assert torch.equal(scatter_fn(block, hidden), sonic_fn(block, hidden)) + + def test_shared_experts_plural(self): + scatter_fn, sonic_fn = self._get_both_fns() + out = torch.randn(4, 8) + block = SimpleNamespace(shared_experts=lambda x: out) + hidden = torch.randn(4, 8) + + assert torch.equal(scatter_fn(block, hidden), sonic_fn(block, hidden)) + + def test_shared_mlp(self): + scatter_fn, sonic_fn = self._get_both_fns() + out = torch.randn(4, 8) + block = SimpleNamespace(shared_mlp=lambda x: out) + hidden = torch.randn(4, 8) + + assert torch.equal(scatter_fn(block, hidden), sonic_fn(block, hidden)) + + def test_no_shared_expert(self): + scatter_fn, sonic_fn = self._get_both_fns() + block = SimpleNamespace() + hidden = torch.randn(4, 8) + + assert scatter_fn(block, hidden) is None + assert sonic_fn(block, hidden) is None + + def test_shared_expert_gate_only_in_scattermoe(self): + """ScatterMoE's _compute_shared_expert handles shared_expert_gate; + SonicMoE's patch.py handles it externally in the forward function. + + This documents the known divergence: the scattermoe version applies + sigmoid gating inline, while sonicmoe applies it in the forward. + """ + scatter_fn, sonic_fn = self._get_both_fns() + + H = 8 + expert_out = torch.ones(4, H) + gate_fn = lambda x: torch.zeros(4, H) # noqa: E731 # sigmoid(0) = 0.5 + + block = SimpleNamespace( + shared_expert=lambda x: expert_out, + shared_expert_gate=gate_fn, + ) + hidden = torch.randn(4, H) + + scatter_result = scatter_fn(block, hidden) + sonic_result = sonic_fn(block, hidden) + + # ScatterMoE applies the gate: expert_out * sigmoid(0) = 0.5 + expected_gated = expert_out * 0.5 + assert torch.allclose(scatter_result, expected_gated, atol=1e-6) + + # SonicMoE does NOT apply the gate here (it does it in the forward) + assert torch.equal(sonic_result, expert_out) + + +# ============================================================================ +# 4. Route dispatcher parity +# ============================================================================ + + +class TestRouteDispatcherParity: + """Verify _route in scattermoe dispatches correctly and matches individual fns.""" + + @pytest.fixture(autouse=True) + def _require(self): + _require_triton() + + def test_route_dispatches_softmax(self): + """_route should use softmax when no e_score_correction_bias.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _route, + _softmax_topk_route, + ) + + moe_block, gate, hidden, T, H, E, K = _make_softmax_block() + + route_w, route_e, route_k, route_E = _route( + moe_block, gate, hidden, gate.weight, None + ) + direct_w, direct_e, direct_k, direct_E = _softmax_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + + assert torch.equal(route_w, direct_w) + assert torch.equal(route_e, direct_e) + assert route_k == direct_k + assert route_E == direct_E + + def test_route_dispatches_sigmoid(self): + """_route should use sigmoid when e_score_correction_bias is present.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _route, + _sigmoid_topk_route, + ) + + moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( + n_group=1, bias_on_gate=True + ) + + route_w, route_e, route_k, route_E = _route( + moe_block, gate, hidden, gate.weight, None + ) + direct_w, direct_e, direct_k, direct_E = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + + assert torch.equal(route_w, direct_w) + assert torch.equal(route_e, direct_e) + assert route_k == direct_k + assert route_E == direct_E diff --git a/tests/integrations/test_scattermoe_autotune_telemetry.py b/tests/integrations/test_scattermoe_autotune_telemetry.py new file mode 100644 index 0000000000..50ac567200 --- /dev/null +++ b/tests/integrations/test_scattermoe_autotune_telemetry.py @@ -0,0 +1,367 @@ +"""Tests for scattermoe autotune telemetry integration. + +These tests use mocking to verify the collection and reporting logic +without requiring Triton or CUDA. +""" + +import sys +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +# Simulate the hash-suffixed module name that LocalLayerRepository creates. +_FAKE_MODULE_NAME = "scattermoe_lora_abc123.kernels.lora_ops" + + +def _make_mock_config(kwargs, num_warps=4, num_stages=3): + """Create a mock triton.Config-like object.""" + return SimpleNamespace(kwargs=kwargs, num_warps=num_warps, num_stages=num_stages) + + +def _make_mock_kernel(cache=None): + """Create a mock autotuned kernel object with a ``.cache`` dict.""" + kernel = SimpleNamespace() + kernel.cache = cache if cache is not None else {} + return kernel + + +def _make_mock_lora_ops( + fwd_cache=None, dx_cache=None, bwd_cache=None, fused_cache=None +): + """Build a mock ``lora_ops`` module with the four kernel attributes.""" + mod = SimpleNamespace( + _scatter2scatter_lora=_make_mock_kernel(fwd_cache), + _scatter2scatter_lora_dX=_make_mock_kernel(dx_cache), + _group_bwd_lora=_make_mock_kernel(bwd_cache), + _group_bwd_lora_fused=_make_mock_kernel(fused_cache), + ) + return mod + + +# ========================================================================= +# TestAutotuneCollector +# ========================================================================= + + +class TestAutotuneCollector: + """Test ``collect_autotune_configs`` with mocked kernel objects.""" + + def test_empty_cache_returns_empty_list(self): + """When no kernel has been autotuned yet, return ``[]``.""" + mock_lora_ops = _make_mock_lora_ops() + + with patch.dict(sys.modules, {_FAKE_MODULE_NAME: mock_lora_ops}): + from axolotl.integrations.kernels.autotune_collector import ( + collect_autotune_configs, + ) + + result = collect_autotune_configs() + assert result == [] + + def test_populated_cache_returns_configs(self): + """When a cache entry exists, it appears in the output.""" + cfg = _make_mock_config( + {"BLOCK_N": 128, "BLOCK_K": 64}, num_warps=8, num_stages=4 + ) + mock_lora_ops = _make_mock_lora_ops(fwd_cache={(2048, 4096, 1024): cfg}) + + with patch.dict(sys.modules, {_FAKE_MODULE_NAME: mock_lora_ops}): + from axolotl.integrations.kernels.autotune_collector import ( + collect_autotune_configs, + ) + + result = collect_autotune_configs() + + assert len(result) == 1 + entry = result[0] + assert entry["kernel"] == "scatter2scatter_lora_fwd" + assert entry["key"] == {"M": 2048, "N": 4096, "K": 1024} + assert entry["config"]["BLOCK_N"] == 128 + assert entry["config"]["BLOCK_K"] == 64 + assert entry["config"]["num_warps"] == 8 + assert entry["config"]["num_stages"] == 4 + + def test_multiple_kernels_and_keys(self): + """Multiple cache entries across kernels are all returned.""" + cfg_fwd = _make_mock_config({"BLOCK_N": 128, "BLOCK_K": 32}) + cfg_dx = _make_mock_config({"BLOCK_K": 64, "BLOCK_N": 128}, num_warps=8) + + mock_lora_ops = _make_mock_lora_ops( + fwd_cache={(16, 256, 128): cfg_fwd}, + dx_cache={(16, 256, 128): cfg_dx}, + ) + + with patch.dict(sys.modules, {_FAKE_MODULE_NAME: mock_lora_ops}): + from axolotl.integrations.kernels.autotune_collector import ( + collect_autotune_configs, + ) + + result = collect_autotune_configs() + + assert len(result) == 2 + names = {r["kernel"] for r in result} + assert "scatter2scatter_lora_fwd" in names + assert "scatter2scatter_lora_dX" in names + + def test_extra_key_elements_stored(self): + """Dtype or other extra elements in the cache key are captured.""" + cfg = _make_mock_config({"BLOCK_N": 64, "BLOCK_K": 32}) + cache_key = (512, 1024, 256, "float16", "float16") + + mock_lora_ops = _make_mock_lora_ops(fwd_cache={cache_key: cfg}) + + with patch.dict(sys.modules, {_FAKE_MODULE_NAME: mock_lora_ops}): + from axolotl.integrations.kernels.autotune_collector import ( + collect_autotune_configs, + ) + + result = collect_autotune_configs() + + assert len(result) == 1 + key = result[0]["key"] + assert key["M"] == 512 + assert key["N"] == 1024 + assert key["K"] == 256 + assert key["_extra"] == ["float16", "float16"] + + def test_no_module_in_sys_modules_returns_empty(self): + """If no lora_ops module is loaded, return ``[]``.""" + from axolotl.integrations.kernels.autotune_collector import ( + collect_autotune_configs, + ) + + # Don't inject anything — the real lora_ops isn't loaded either + # (no triton on this machine), so _find_lora_ops_module returns None. + result = collect_autotune_configs() + assert result == [] + + def test_finds_module_under_hash_suffixed_name(self): + """Collector finds lora_ops regardless of the hash suffix.""" + cfg = _make_mock_config({"BLOCK_N": 256, "BLOCK_K": 128}) + mock_lora_ops = _make_mock_lora_ops(fwd_cache={(8, 512, 64): cfg}) + + # Use a different hash to prove it's not hardcoded. + alt_name = "scattermoe_lora_deadbeef.kernels.lora_ops" + with patch.dict(sys.modules, {alt_name: mock_lora_ops}): + from axolotl.integrations.kernels.autotune_collector import ( + collect_autotune_configs, + ) + + result = collect_autotune_configs() + + assert len(result) == 1 + assert result[0]["config"]["BLOCK_N"] == 256 + + +# ========================================================================= +# TestAutotuneReportCallback +# ========================================================================= + + +class TestAutotuneReportCallback: + """Test the callback fires once and sends the correct event.""" + + def test_reports_once_on_first_step(self): + """Callback should call ``send_event`` exactly once.""" + from axolotl.integrations.kernels.autotune_callback import ( + AutotuneReportCallback, + ) + + cb = AutotuneReportCallback() + mock_state = MagicMock() + mock_state.global_step = 1 + + fake_configs = [{"kernel": "test_fwd", "key": {}, "config": {}}] + + with ( + patch( + "axolotl.integrations.kernels.autotune_collector.collect_autotune_configs", + return_value=fake_configs, + ), + patch("axolotl.telemetry.manager.TelemetryManager") as mock_tm_cls, + ): + mock_tm = MagicMock() + mock_tm.enabled = True + mock_tm_cls.get_instance.return_value = mock_tm + + cb.on_step_end(args=MagicMock(), state=mock_state, control=MagicMock()) + assert mock_tm.send_event.call_count == 1 + + call_kwargs = mock_tm.send_event.call_args[1] + assert call_kwargs["event_type"] == "scattermoe-autotune" + assert call_kwargs["properties"]["kernel_count"] == 1 + + # Second call should NOT send again. + cb.on_step_end(args=MagicMock(), state=mock_state, control=MagicMock()) + assert mock_tm.send_event.call_count == 1 + + def test_retries_until_step_5_then_gives_up(self): + """If no configs found by step 5, stop retrying.""" + from axolotl.integrations.kernels.autotune_callback import ( + AutotuneReportCallback, + ) + + cb = AutotuneReportCallback() + + with patch( + "axolotl.integrations.kernels.autotune_collector.collect_autotune_configs", + return_value=[], + ): + for step in range(1, 7): + mock_state = MagicMock() + mock_state.global_step = step + cb.on_step_end(args=MagicMock(), state=mock_state, control=MagicMock()) + + assert cb._reported is True + + def test_reports_on_retry_when_data_arrives(self): + """If step 1 has no data but step 2 does, report at step 2.""" + from axolotl.integrations.kernels.autotune_callback import ( + AutotuneReportCallback, + ) + + cb = AutotuneReportCallback() + fake_configs = [{"kernel": "fwd", "key": {}, "config": {}}] + + call_count = 0 + + def _collector(): + nonlocal call_count + call_count += 1 + if call_count == 1: + return [] + return fake_configs + + with ( + patch( + "axolotl.integrations.kernels.autotune_collector.collect_autotune_configs", + side_effect=_collector, + ), + patch("axolotl.telemetry.manager.TelemetryManager") as mock_tm_cls, + ): + mock_tm = MagicMock() + mock_tm.enabled = True + mock_tm_cls.get_instance.return_value = mock_tm + + # Step 1 — empty, no report + s1 = MagicMock() + s1.global_step = 1 + cb.on_step_end(args=MagicMock(), state=s1, control=MagicMock()) + assert mock_tm.send_event.call_count == 0 + + # Step 2 — data arrives, report + s2 = MagicMock() + s2.global_step = 2 + cb.on_step_end(args=MagicMock(), state=s2, control=MagicMock()) + assert mock_tm.send_event.call_count == 1 + + def test_includes_gpu_info(self): + """Event properties should include GPU identification.""" + from axolotl.integrations.kernels.autotune_callback import ( + AutotuneReportCallback, + ) + + cb = AutotuneReportCallback() + mock_state = MagicMock() + mock_state.global_step = 1 + + fake_configs = [{"kernel": "fwd", "key": {}, "config": {}}] + fake_gpu = { + "gpu_name": "NVIDIA H100", + "gpu_compute_capability": "9.0", + "gpu_memory_bytes": 85899345920, + } + + fake_smem = {"smem_capacity_bytes": 233472} + + with ( + patch( + "axolotl.integrations.kernels.autotune_collector.collect_autotune_configs", + return_value=fake_configs, + ), + patch( + "axolotl.integrations.kernels.autotune_callback._get_gpu_info", + return_value=fake_gpu, + ), + patch( + "axolotl.integrations.kernels.autotune_callback._get_smem_capacity", + return_value=fake_smem, + ), + patch("axolotl.telemetry.manager.TelemetryManager") as mock_tm_cls, + ): + mock_tm = MagicMock() + mock_tm.enabled = True + mock_tm_cls.get_instance.return_value = mock_tm + + cb.on_step_end(args=MagicMock(), state=mock_state, control=MagicMock()) + props = mock_tm.send_event.call_args[1]["properties"] + assert props["gpu_name"] == "NVIDIA H100" + assert props["gpu_compute_capability"] == "9.0" + assert props["gpu_memory_bytes"] == 85899345920 + assert props["smem_capacity_bytes"] == 233472 + + def test_skips_send_when_telemetry_disabled(self): + """If telemetry is disabled, no event is sent.""" + from axolotl.integrations.kernels.autotune_callback import ( + AutotuneReportCallback, + ) + + cb = AutotuneReportCallback() + mock_state = MagicMock() + mock_state.global_step = 1 + + with ( + patch( + "axolotl.integrations.kernels.autotune_collector.collect_autotune_configs", + return_value=[{"kernel": "fwd", "key": {}, "config": {}}], + ), + patch("axolotl.telemetry.manager.TelemetryManager") as mock_tm_cls, + ): + mock_tm = MagicMock() + mock_tm.enabled = False + mock_tm_cls.get_instance.return_value = mock_tm + + cb.on_step_end(args=MagicMock(), state=mock_state, control=MagicMock()) + assert mock_tm.send_event.call_count == 0 + # Should still mark as reported so we don't retry. + assert cb._reported is True + + +# ========================================================================= +# TestKernelsPluginCallbackRegistration +# ========================================================================= + + +class TestKernelsPluginCallbackRegistration: + """Test that ``KernelsPlugin`` registers the callback correctly.""" + + def test_scattermoe_registers_callback(self): + """When ``use_scattermoe=True``, plugin returns the callback.""" + from axolotl.integrations.kernels.autotune_callback import ( + AutotuneReportCallback, + ) + from axolotl.integrations.kernels.plugin import KernelsPlugin + + plugin = KernelsPlugin() + cfg = MagicMock() + cfg.use_scattermoe = True + model = MagicMock() + + callbacks = plugin.add_callbacks_pre_trainer(cfg, model) + assert len(callbacks) == 1 + assert isinstance(callbacks[0], AutotuneReportCallback) + + def test_no_scattermoe_no_callback(self): + """When ``use_scattermoe=False``, plugin returns empty list.""" + from axolotl.integrations.kernels.plugin import KernelsPlugin + + plugin = KernelsPlugin() + cfg = MagicMock() + cfg.use_scattermoe = False + model = MagicMock() + + callbacks = plugin.add_callbacks_pre_trainer(cfg, model) + assert callbacks == [] diff --git a/tests/integrations/test_scattermoe_lora.py b/tests/integrations/test_scattermoe_lora.py index d498c80103..bd50d06feb 100644 --- a/tests/integrations/test_scattermoe_lora.py +++ b/tests/integrations/test_scattermoe_lora.py @@ -3,17 +3,19 @@ # Licensed under the Apache License, Version 2.0 """ -Unit tests for scattermoe-lora code-review fixes. +Unit tests for scattermoe-lora. Tests cover: - KernelsArgs validator: disable_mlp_kernel -- CPU_Offloaded_Gradient_Checkpointer: tuple vs plain tensor backward - ParallelExperts: scaling=0.0 not treated as falsy - single2scatter: non-aligned K/N dimensions - group_compileable: coeff=None accepted - HFScatterMoEGatedMLP / ScatterMoEGatedMLP: return value contract +- Routing strategy detection and sigmoid routing +- Generic shared expert handling """ +from types import SimpleNamespace from unittest.mock import patch import pytest @@ -321,3 +323,389 @@ def test_scatter_moe_gated_mlp_docstring_no_router_logits(self): assert "Router logits" not in docstring, ( "Docstring should not mention 'Router logits' in Returns section" ) + + +# ============================================================================ +# 7. Routing strategy detection and sigmoid routing +# ============================================================================ + + +def _make_softmax_gate(E=4, H=16, K=2): + """Create a mock softmax-style gate (Qwen/OLMoE).""" + return SimpleNamespace( + weight=torch.randn(E, H), + top_k=K, + num_experts=E, + norm_topk_prob=True, + ) + + +def _make_sigmoid_gate_with_bias(E=16, H=16): + """Create a mock sigmoid-style gate with e_score_correction_bias on gate.""" + return SimpleNamespace( + weight=torch.randn(E, H), + e_score_correction_bias=torch.zeros(E), + ) + + +def _make_sigmoid_moe_block( + T=8, H=16, E=16, K=4, n_group=2, topk_group=1, bias_on_gate=True +): + """Create a mock GLM/DeepSeek-style MoE block for sigmoid routing tests.""" + if bias_on_gate: + gate = SimpleNamespace( + weight=torch.randn(E, H), + e_score_correction_bias=torch.zeros(E), + ) + moe_block = SimpleNamespace( + gate=gate, + top_k=K, + n_routed_experts=E, + n_group=n_group, + topk_group=topk_group, + norm_topk_prob=True, + routed_scaling_factor=1.0, + ) + else: + # minimax_m2 style: bias on block, not gate + gate = SimpleNamespace( + weight=torch.randn(E, H), + top_k=K, + ) + moe_block = SimpleNamespace( + gate=gate, + top_k=K, + e_score_correction_bias=torch.zeros(E), + ) + return moe_block, T, H, E, K + + +def _skip_without_triton(): + pytest.importorskip("triton") + + +class TestSigmoidRoutingInScatterMoE: + """Test _sigmoid_topk_route from layers.py.""" + + @pytest.fixture(autouse=True) + def _require_triton(self): + _skip_without_triton() + + def test_output_shapes(self): + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + + moe_block, T, H, E, K = _make_sigmoid_moe_block() + gate = moe_block.gate + hidden = torch.randn(T, H) + + weights, experts, top_k, num_experts = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + + assert weights.shape == (T, K) + assert experts.shape == (T, K) + assert top_k == K + assert num_experts == E + + def test_weights_nonnegative(self): + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + + moe_block, T, H, E, K = _make_sigmoid_moe_block() + gate = moe_block.gate + hidden = torch.randn(T, H) + + weights, _, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + assert (weights >= 0).all() + + def test_group_selection_restricts_experts(self): + """With n_group=4, topk_group=1, experts should be from selected groups.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + + moe_block, T, H, E, K = _make_sigmoid_moe_block( + E=16, K=2, n_group=4, topk_group=1 + ) + gate = moe_block.gate + hidden = torch.randn(T, H) + + _, expert_idx, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + + # Each token's experts should fall within a single group (size E//n_group=4) + for t in range(T): + experts_t = expert_idx[t] + groups = experts_t // (E // moe_block.n_group) + assert (groups == groups[0]).all() + + def test_scaling_factor_applied(self): + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + + moe_block, T, H, E, K = _make_sigmoid_moe_block(n_group=1) + gate = moe_block.gate + hidden = torch.randn(T, H) + + weights_1x, _, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + + moe_block.routed_scaling_factor = 2.0 + weights_2x, _, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + + assert torch.allclose(weights_2x, weights_1x * 2.0, atol=1e-5) + + def test_bias_on_gate(self): + """e_score_correction_bias on gate is found.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + + moe_block, T, H, E, K = _make_sigmoid_moe_block(bias_on_gate=True) + gate = moe_block.gate + hidden = torch.randn(T, H) + + weights, experts, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + assert weights.shape == (T, K) + + def test_bias_on_block(self): + """e_score_correction_bias on moe_block (not gate) is found.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + + moe_block, T, H, E, K = _make_sigmoid_moe_block(bias_on_gate=False) + gate = moe_block.gate + hidden = torch.randn(T, H) + + weights, experts, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + assert weights.shape == (T, K) + + def test_gate_lora_delta_applied(self): + """Gate LoRA delta should affect routing logits.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + + moe_block, T, H, E, K = _make_sigmoid_moe_block(n_group=1) + gate = moe_block.gate + hidden = torch.randn(T, H) + + weights_no_lora, _, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + + # Large delta should change the results + delta = torch.randn(E, H) * 10.0 + weights_with_lora, _, _, _ = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, delta + ) + + assert not torch.equal(weights_no_lora, weights_with_lora) + + def test_no_bias_does_not_crash(self): + """Calling _sigmoid_topk_route with no e_score_correction_bias should not crash.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + + T, H, E, K = 8, 16, 8, 2 + gate = SimpleNamespace(weight=torch.randn(E, H)) + moe_block = SimpleNamespace( + gate=gate, + top_k=K, + n_routed_experts=E, + n_group=1, + norm_topk_prob=True, + routed_scaling_factor=1.0, + ) + hidden = torch.randn(T, H) + + weights, experts, top_k, num_experts = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + assert weights.shape == (T, K) + assert experts.shape == (T, K) + # Without bias, scores_for_choice == sigmoid(logits) — all positive + assert (weights >= 0).all() + + def test_missing_topk_group_defaults_to_n_group(self): + """When topk_group is absent but n_group > 1, should default to n_group (no-op masking).""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _sigmoid_topk_route, + ) + + T, H, E, K, n_group = 8, 16, 16, 2, 4 + gate = SimpleNamespace( + weight=torch.randn(E, H), + e_score_correction_bias=torch.zeros(E), + ) + # Intentionally omit topk_group + moe_block = SimpleNamespace( + gate=gate, + top_k=K, + n_routed_experts=E, + n_group=n_group, + norm_topk_prob=True, + routed_scaling_factor=1.0, + ) + hidden = torch.randn(T, H) + + # Should not raise AttributeError; defaults topk_group to n_group + weights, experts, top_k_out, num_experts = _sigmoid_topk_route( + moe_block, gate, hidden, gate.weight, None + ) + assert weights.shape == (T, K) + assert experts.shape == (T, K) + + +class TestRoutingStrategyDetection: + """Test that _route dispatches to the correct strategy.""" + + @pytest.fixture(autouse=True) + def _require_triton(self): + _skip_without_triton() + + def test_softmax_for_qwen_style(self): + """Block without e_score_correction_bias should use softmax.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import _route + + gate = _make_softmax_gate(E=4, H=16, K=2) + moe_block = SimpleNamespace(gate=gate) + hidden = torch.randn(8, 16) + + weights, experts, top_k, num_experts = _route( + moe_block, gate, hidden, gate.weight, None + ) + + assert weights.shape == (8, 2) + assert experts.shape == (8, 2) + assert top_k == 2 + assert num_experts == 4 + per_token_sums = weights.sum(dim=-1) + assert torch.allclose(per_token_sums, torch.ones(8), atol=1e-5) + + def test_sigmoid_for_glm_style(self): + """Block with e_score_correction_bias on gate should use sigmoid.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import _route + + moe_block, T, H, E, K = _make_sigmoid_moe_block(bias_on_gate=True, n_group=1) + gate = moe_block.gate + hidden = torch.randn(T, H) + + weights, experts, top_k, num_experts = _route( + moe_block, gate, hidden, gate.weight, None + ) + + assert weights.shape == (T, K) + assert experts.shape == (T, K) + assert (weights >= 0).all() + + def test_sigmoid_for_minimax_m2_style(self): + """Block with e_score_correction_bias on block (not gate) should use sigmoid.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import _route + + moe_block, T, H, E, K = _make_sigmoid_moe_block(bias_on_gate=False) + gate = moe_block.gate + hidden = torch.randn(T, H) + + weights, experts, top_k, num_experts = _route( + moe_block, gate, hidden, gate.weight, None + ) + + assert weights.shape == (T, K) + assert (weights >= 0).all() + + +# ============================================================================ +# 8. Generic shared expert handling +# ============================================================================ + + +class TestGenericSharedExpert: + """Test _compute_shared_expert from layers.py.""" + + @pytest.fixture(autouse=True) + def _require_triton(self): + _skip_without_triton() + + def test_shared_expert_singular(self): + """shared_expert attribute (Qwen2MoE style).""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _compute_shared_expert, + ) + + called = torch.randn(4, 8) + moe_block = SimpleNamespace( + shared_expert=lambda x: called, + ) + result = _compute_shared_expert(moe_block, torch.randn(4, 8)) + assert torch.equal(result, called) + + def test_shared_experts_plural(self): + """shared_experts attribute (DeepSeek V3 style).""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _compute_shared_expert, + ) + + called = torch.randn(4, 8) + moe_block = SimpleNamespace( + shared_experts=lambda x: called, + ) + result = _compute_shared_expert(moe_block, torch.randn(4, 8)) + assert torch.equal(result, called) + + def test_shared_mlp(self): + """shared_mlp attribute (Hunyuan style).""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _compute_shared_expert, + ) + + called = torch.randn(4, 8) + moe_block = SimpleNamespace( + shared_mlp=lambda x: called, + ) + result = _compute_shared_expert(moe_block, torch.randn(4, 8)) + assert torch.equal(result, called) + + def test_shared_expert_with_gate(self): + """shared_expert + shared_expert_gate applies sigmoid gating.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _compute_shared_expert, + ) + + H = 8 + expert_out = torch.ones(4, H) + gate_fn = lambda x: torch.zeros(4, H) # noqa: E731 + + moe_block = SimpleNamespace( + shared_expert=lambda x: expert_out, + shared_expert_gate=gate_fn, + ) + result = _compute_shared_expert(moe_block, torch.randn(4, H)) + expected = expert_out * 0.5 # sigmoid(0) = 0.5 + assert torch.allclose(result, expected, atol=1e-6) + + def test_no_shared_expert(self): + """No shared expert attributes returns None.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + _compute_shared_expert, + ) + + moe_block = SimpleNamespace() + result = _compute_shared_expert(moe_block, torch.randn(4, 8)) + assert result is None From 999b3fec2e157131ffa628e8fdac31adb0de02e3 Mon Sep 17 00:00:00 2001 From: Aarush <112254386+Hadar01@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:23:13 +0530 Subject: [PATCH 1197/1405] fix: replace shell=True subprocess with argument list in modal CLI (#3487) * fix: replace shell=True subprocess with argument list in modal CLI Using shell=True with a formatted string containing docker_image (a user-controlled value) is a command injection risk (Bandit B602). Replace with an argument list, which passes args directly to the process without shell interpretation, removing the nosec annotation. * fix: add nosec annotation to suppress bandit B603/B607 warnings Removing shell=True (B602) surfaces B603 (subprocess without shell) and B607 (partial executable path for 'docker'). Use bare # nosec to suppress both, consistent with other nosec usages in the codebase. --- src/axolotl/cli/cloud/modal_.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/axolotl/cli/cloud/modal_.py b/src/axolotl/cli/cloud/modal_.py index 3e703a4946..df8f8f215c 100644 --- a/src/axolotl/cli/cloud/modal_.py +++ b/src/axolotl/cli/cloud/modal_.py @@ -90,9 +90,8 @@ def get_image(self): # grab the sha256 hash from docker hub for this image+tag # this ensures that we always get the latest image for this tag, even if it's already cached try: - manifest = subprocess.check_output( # nosec B602 - f"docker manifest inspect {docker_image}", - shell=True, + manifest = subprocess.check_output( # nosec + ["docker", "manifest", "inspect", docker_image], ).decode("utf-8") sha256_hash = json.loads(manifest)["manifests"][0]["digest"] except subprocess.CalledProcessError: From 5ef3f283409d66762b45c8d638515823e4adf715 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 17 Mar 2026 11:42:47 -0400 Subject: [PATCH 1198/1405] Support for Async GRPO (#3486) * async grpo support * implement data producer * use fast async * handle call to create data producer * fix liger kernel setup * fix replay buffer * chore: lint * make gpus go brrr * chore: lint * inplace div_, unwrap model for logits in bf16 * fuse selective softmax and empty cuda cache on each scoring step * remove waiting for synch time and fix race * make fp8 work and allow lora kernels w rl * grpo with lora vllm sync and fixes for sharded distributed * update docs * more patches so it works against trl main * address PR feedback for corerabbit --- docs/rlhf.qmd | 207 ++ src/axolotl/cli/vllm_serve.py | 33 +- src/axolotl/core/builders/rl.py | 58 +- src/axolotl/core/trainers/grpo/__init__.py | 78 +- src/axolotl/core/trainers/grpo/args.py | 8 + .../core/trainers/grpo/async_trainer.py | 2657 +++++++++++++++++ .../core/trainers/grpo/fast_async_trainer.py | 768 +++++ .../core/trainers/grpo/replay_buffer.py | 44 + src/axolotl/core/trainers/grpo/trainer.py | 14 + src/axolotl/kernels/lora.py | 8 +- src/axolotl/kernels/quantize.py | 54 +- src/axolotl/loaders/adapter.py | 12 + src/axolotl/loaders/model.py | 2 + src/axolotl/loaders/patch_manager.py | 12 + src/axolotl/monkeypatch/trainer/trl_vllm.py | 245 ++ src/axolotl/scripts/__init__.py | 0 src/axolotl/scripts/vllm_serve_lora.py | 503 ++++ src/axolotl/scripts/vllm_worker_ext.py | 158 + src/axolotl/utils/schemas/trl.py | 122 + src/axolotl/utils/schemas/validation.py | 14 - src/axolotl/utils/schemas/vllm.py | 7 + tests/core/test_async_grpo.py | 220 ++ tests/monkeypatch/test_trl_vllm.py | 286 ++ 23 files changed, 5474 insertions(+), 36 deletions(-) create mode 100644 src/axolotl/core/trainers/grpo/async_trainer.py create mode 100644 src/axolotl/core/trainers/grpo/fast_async_trainer.py create mode 100644 src/axolotl/core/trainers/grpo/replay_buffer.py create mode 100644 src/axolotl/monkeypatch/trainer/trl_vllm.py create mode 100644 src/axolotl/scripts/__init__.py create mode 100644 src/axolotl/scripts/vllm_serve_lora.py create mode 100644 src/axolotl/scripts/vllm_worker_ext.py create mode 100644 tests/core/test_async_grpo.py create mode 100644 tests/monkeypatch/test_trl_vllm.py diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 135b3038ca..60c34933de 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -721,6 +721,213 @@ trl: For more information, see [GRPO docs](https://huggingface.co/docs/trl/v0.17.0/en/grpo_trainer#loss-types). +#### Async GRPO + +Async GRPO overlaps vLLM generation with training by producing rollouts in a background thread. While the model trains on the current batch, the next batch is already being generated. This can significantly reduce wall-clock time per step. + +```yaml +trl: + use_data_producer: true # Enable data producer protocol + use_vllm: true + async_prefetch: true # Generate rollouts in background thread + prefetch_depth: 1 # Number of rollouts to prefetch + vllm_sync_interval: 2 # Sync weights to vLLM every N steps +``` + +::: {.callout-note} +Because the background thread generates completions with slightly stale model weights, async GRPO uses importance sampling correction to account for the distribution shift. This is controlled by `vllm_importance_sampling_correction: true` (default when async is enabled). +::: + +##### vLLM LoRA Sync + +By default, weight sync to vLLM merges the LoRA adapter into the base model and broadcasts all parameters via NCCL. LoRA sync is a faster alternative that saves only the adapter weights to the filesystem and has vLLM load them natively using Punica kernels. + +```yaml +adapter: lora +lora_r: 32 +lora_alpha: 64 +lora_target_linear: true + +trl: + vllm_lora_sync: true # Enable native LoRA sync +``` + +When `vllm_lora_sync: true` is set, axolotl automatically selects the LoRA-aware vLLM serve module. Start vLLM as usual: + +```bash +CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve config.yaml +``` + +Then start training on a separate GPU: + +```bash +CUDA_VISIBLE_DEVICES=1 axolotl train config.yaml +``` + +::: {.callout-tip} +LoRA sync is especially beneficial with multi-GPU training (FSDP/DeepSpeed), where NCCL merge-sync can cause GPU contention with vLLM generation. +::: + +##### Streaming Partial Batch + +Instead of scoring the entire batch at once, streaming mode scores one prompt group at a time. This enables finer-grained zero-advantage skipping and reduces peak memory usage during scoring. + +```yaml +trl: + streaming_partial_batch: true +``` + +##### Importance Sampling Correction + +When using async prefetch, completions are generated from a slightly older version of the model. Importance sampling (IS) correction adjusts the policy gradient to account for this distribution shift. + +```yaml +trl: + vllm_importance_sampling_correction: true # Enable IS correction + importance_sampling_level: token # 'token' or 'sequence' + off_policy_mask_threshold: 0.5 # Mask sequences with IS ratio below this +``` + +- `importance_sampling_level: token` applies per-token IS ratios (recommended with Liger kernel) +- `importance_sampling_level: sequence` applies per-sequence IS ratios +- `off_policy_mask_threshold` masks out sequences where the IS ratio indicates they are too far off-policy + +##### Replay Buffer + +The replay buffer caches rollout groups that had learning signal (non-zero reward variance) and uses them to replace zero-signal groups in later batches. + +```yaml +trl: + replay_buffer_size: 100 # Max cached groups (0 = disabled) + replay_recompute_logps: true # Recompute log-probs for replayed data (recommended) +``` + +::: {.callout-note} +When `replay_recompute_logps: true` (default), old log-probabilities are recomputed using the current model weights. This fixes the IS mismatch that would otherwise occur when replaying stale data. +::: + +##### Deferred Re-rolling + +Failed prompts (where the model produces zero reward for all generations) are buffered and re-injected into later batches when the model may be better equipped to solve them. + +```yaml +trl: + reroll_start_fraction: 0.5 # Start re-rolling after 50% of training + reroll_max_groups: 1 # Max groups to replace per batch +``` + +##### Zero-Advantage Batch Skipping + +When all advantages in a micro-batch are zero (no learning signal), the forward/backward pass is skipped entirely. This is enabled by default and logged as `skipped_zero_adv_batches=1`. + +```yaml +trl: + skip_zero_advantage_batches: true # default +``` + +##### Parallel Reward Workers + +Reward functions that use `signal.alarm()` (e.g., `math_verify`) must run in the main thread. Parallel reward workers use subprocesses to work around this limitation while enabling concurrent reward computation. + +```yaml +trl: + reward_num_workers: 4 # Number of subprocess workers (1 = no parallelism) +``` + +##### Full Async GRPO Example + +```yaml +base_model: Qwen/Qwen2.5-1.5B-Instruct + +vllm: + host: 0.0.0.0 + port: 8000 + gpu_memory_utilization: 0.35 + dtype: auto + +adapter: lora +lora_r: 32 +lora_alpha: 64 +lora_target_linear: true + +rl: grpo +trl: + use_data_producer: true + use_vllm: true + async_prefetch: true + prefetch_depth: 1 + vllm_sync_interval: 2 + vllm_lora_sync: true + streaming_partial_batch: true + vllm_importance_sampling_correction: true + off_policy_mask_threshold: 0.5 + importance_sampling_level: token + num_generations: 8 + max_completion_length: 512 + reward_funcs: + - rewards.accuracy_reward + reroll_start_fraction: 0.5 + replay_buffer_size: 100 + reward_num_workers: 4 + skip_zero_advantage_batches: true + +datasets: + - path: AI-MO/NuminaMath-TIR + type: rewards.prompt_transform + split: train + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +max_steps: 500 +learning_rate: 1e-5 +bf16: true +gradient_checkpointing: true +``` + +```bash +# Terminal 1: Start vLLM on GPU 0 +CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve config.yaml + +# Terminal 2: Train on GPU 1 +CUDA_VISIBLE_DEVICES=1 axolotl train config.yaml +``` + +##### Multi-GPU Async GRPO + +Async GRPO supports FSDP and DeepSpeed ZeRO-3 for multi-GPU training. vLLM runs on one GPU while training is distributed across the remaining GPUs. + +**FSDP:** + +```yaml +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_transformer_layer_cls_to_wrap: Qwen2DecoderLayer +gradient_checkpointing_kwargs: + use_reentrant: false +``` + +**DeepSpeed ZeRO-3:** + +```yaml +deepspeed: deepspeed_configs/zero3_bf16.json +gradient_checkpointing_kwargs: + use_reentrant: true # Required for ZeRO-3 +``` + +```bash +# Terminal 1: Start vLLM on GPU 0 +CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve config.yaml + +# Terminal 2: Train on GPUs 0,1 +CUDA_VISIBLE_DEVICES=0,1 accelerate launch --num_processes 2 -m axolotl.cli.train config.yaml +``` + +::: {.callout-important} +With multi-GPU async prefetch, only rank 0 generates completions in the background thread. Results are broadcast to all ranks on the main thread. This avoids FSDP/DeepSpeed collective deadlocks from unsynchronized background threads. +::: + ### GDPO GDPO (Group Reward-Decoupled Policy Optimization) extends GRPO for multi-reward training. It addresses the **reward advantage collapse** problem by normalizing each reward function independently before combining them. diff --git a/src/axolotl/cli/vllm_serve.py b/src/axolotl/cli/vllm_serve.py index ea454fc96a..10db23878c 100644 --- a/src/axolotl/cli/vllm_serve.py +++ b/src/axolotl/cli/vllm_serve.py @@ -38,7 +38,18 @@ def do_vllm_serve( cfg = load_cfg(config) model = cfg.base_model - serve_module = cli_args.get("serve_module", "trl.scripts.vllm_serve") + # Determine serve module: explicit CLI/config > auto-select from vllm_lora_sync > default + serve_module = cli_args.get("serve_module") or getattr( + cfg.vllm, "serve_module", None + ) + if ( + serve_module is None + and getattr(cfg, "trl", None) + and getattr(cfg.trl, "vllm_lora_sync", False) + ): + serve_module = "axolotl.scripts.vllm_serve_lora" + if serve_module is None: + serve_module = "trl.scripts.vllm_serve" vllm_serve_main = __import__(serve_module, fromlist=["main"]).main tensor_parallel_size = 1 data_parallel_size = 1 @@ -68,7 +79,7 @@ def do_vllm_serve( cli_args.get("enable_reasoning") or cfg.vllm.enable_reasoning or False ) - vllm_script_args = AxolotlScriptArguments( + base_kwargs = dict( model=model, tensor_parallel_size=tensor_parallel_size, data_parallel_size=data_parallel_size, @@ -78,7 +89,21 @@ def do_vllm_serve( dtype=dtype, max_model_len=max_model_len, enable_prefix_caching=enable_prefix_caching, - reasoning_parser=reasoning_parser, - enable_reasoning=enable_reasoning, ) + + # Use LoRAScriptArguments when serving with native LoRA support + if serve_module == "axolotl.scripts.vllm_serve_lora": + from axolotl.scripts.vllm_serve_lora import LoRAScriptArguments + + lora_kwargs = {} + if hasattr(cfg, "lora_r") and cfg.lora_r: + lora_kwargs["max_lora_rank"] = cfg.lora_r + vllm_script_args = LoRAScriptArguments(**base_kwargs, **lora_kwargs) + else: + vllm_script_args = AxolotlScriptArguments( + **base_kwargs, + reasoning_parser=reasoning_parser, + enable_reasoning=enable_reasoning, + ) + vllm_serve_main(vllm_script_args) diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index bb67aef6df..43ef133ff9 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -54,8 +54,16 @@ def _get_trainer_cls(self, trainer_kwargs: dict): if self.cfg.rl in {RLType.GRPO, RLType.GDPO}: from axolotl.core.trainers.grpo import GRPOStrategy + async_grpo = bool( + self.cfg.trl + and ( + getattr(self.cfg.trl, "async_prefetch", False) + or getattr(self.cfg.trl, "use_data_producer", False) + ) + ) trainer_cls = GRPOStrategy.get_trainer_class( - sequence_parallel=self.cfg.context_parallel_size > 1 + sequence_parallel=self.cfg.context_parallel_size > 1, + async_grpo=async_grpo, ) trainer_cls_args.extend(GRPOStrategy.set_trainer_args(self.cfg)) trainer_kwargs.update(GRPOStrategy.set_trainer_kwargs(self.cfg)) @@ -151,7 +159,16 @@ def _build_training_arguments(self, total_num_steps): elif self.cfg.rl in {RLType.GRPO, RLType.GDPO}: from axolotl.core.trainers.grpo import GRPOStrategy - training_args_cls = GRPOStrategy.get_training_args_class() + async_grpo = bool( + self.cfg.trl + and ( + getattr(self.cfg.trl, "async_prefetch", False) + or getattr(self.cfg.trl, "use_data_producer", False) + ) + ) + training_args_cls = GRPOStrategy.get_training_args_class( + async_grpo=async_grpo + ) training_args_kwargs.update(GRPOStrategy.set_training_args_kwargs(self.cfg)) blocklist_args_kwargs = GRPOStrategy.get_blocklist_args_kwargs() if self.cfg.rl is RLType.GDPO: @@ -217,13 +234,36 @@ def build(self, total_num_steps): trainer_kwargs, trainer_cls ) - trainer = trainer_cls( - *trainer_cls_args, - args=training_args, - train_dataset=self.train_dataset, - callbacks=self.get_callbacks(), - **trainer_kwargs, - ) + # Allow FP8-quantized models to be fine-tuned with LoRA adapters. + # transformers' validate_quantization_for_training blocks FP8 because + # hf_quantizer.is_trainable is False, but LoRA only trains the adapters + # (base weights stay frozen in FP8). + _orig_validate_quant = None + if ( + self.cfg.adapter + and hasattr(self.model, "is_quantized") + and self.model.is_quantized + ): + import transformers.trainer as _trainer_module + + _orig_validate_quant = _trainer_module.validate_quantization_for_training + _trainer_module.validate_quantization_for_training = lambda model: None + + try: + trainer = trainer_cls( + *trainer_cls_args, + args=training_args, + train_dataset=self.train_dataset, + callbacks=self.get_callbacks(), + **trainer_kwargs, + ) + finally: + if _orig_validate_quant is not None: + import transformers.trainer as _trainer_module + + _trainer_module.validate_quantization_for_training = ( + _orig_validate_quant + ) if self.cfg.fsdp_config or self.cfg.fsdp: ensure_dtype(trainer.model, dtype=self.cfg.torch_dtype) if self.cfg.rl in [RLType.DPO, RLType.IPO] and trainer.ref_model: diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 0d2615aecf..5c057cc40b 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -9,8 +9,9 @@ from requests import HTTPError from trl.trainer.grpo_trainer import RewardFunc -from axolotl.core.trainers.grpo.args import AxolotlGRPOConfig +from axolotl.core.trainers.grpo.args import AxolotlAsyncGRPOConfig, AxolotlGRPOConfig from axolotl.core.trainers.grpo.trainer import ( + AxolotlAsyncGRPOTrainer, AxolotlGRPOSequenceParallelTrainer, AxolotlGRPOTrainer, ) @@ -27,14 +28,31 @@ class GRPOStrategy: @classmethod def get_trainer_class( - cls, sequence_parallel: bool - ) -> type[AxolotlGRPOTrainer] | type[AxolotlGRPOSequenceParallelTrainer]: + cls, + sequence_parallel: bool, + async_grpo: bool = False, + ) -> ( + type[AxolotlGRPOTrainer] + | type[AxolotlGRPOSequenceParallelTrainer] + | type[AxolotlAsyncGRPOTrainer] + ): + if sequence_parallel and async_grpo: + raise ValueError( + "sequence_parallel and async_grpo cannot both be enabled. " + "Disable one of context_parallel_size > 1 or async_prefetch/use_data_producer." + ) if sequence_parallel: return AxolotlGRPOSequenceParallelTrainer + if async_grpo: + return AxolotlAsyncGRPOTrainer return AxolotlGRPOTrainer @classmethod - def get_training_args_class(cls) -> type[AxolotlGRPOConfig]: + def get_training_args_class( + cls, async_grpo: bool = False + ) -> type[AxolotlGRPOConfig] | type[AxolotlAsyncGRPOConfig]: + if async_grpo: + return AxolotlAsyncGRPOConfig return AxolotlGRPOConfig @classmethod @@ -124,13 +142,63 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: grpo_args_kwargs["epsilon_high"] = trl.epsilon_high if trl.use_liger_loss is not None: - grpo_args_kwargs["use_liger_loss"] = trl.use_liger_loss + grpo_args_kwargs["use_liger_kernel"] = trl.use_liger_loss if trl.multi_objective_aggregation is not None: grpo_args_kwargs["multi_objective_aggregation"] = ( trl.multi_objective_aggregation ) + # Async GRPO fields + if getattr(trl, "use_data_producer", None) is not None: + grpo_args_kwargs["use_data_producer"] = trl.use_data_producer + if getattr(trl, "async_prefetch", None) is not None: + grpo_args_kwargs["async_prefetch"] = trl.async_prefetch + if getattr(trl, "prefetch_depth", None) is not None: + grpo_args_kwargs["prefetch_depth"] = trl.prefetch_depth + if getattr(trl, "vllm_sync_interval", None) is not None: + grpo_args_kwargs["vllm_sync_interval"] = trl.vllm_sync_interval + if getattr(trl, "streaming_partial_batch", None) is not None: + grpo_args_kwargs["streaming_partial_batch"] = trl.streaming_partial_batch + if getattr(trl, "streaming_min_groups", None) is not None: + grpo_args_kwargs["streaming_min_groups"] = trl.streaming_min_groups + if getattr(trl, "vllm_importance_sampling_correction", None) is not None: + grpo_args_kwargs["vllm_importance_sampling_correction"] = ( + trl.vllm_importance_sampling_correction + ) + if getattr(trl, "vllm_importance_sampling_mode", None) is not None: + grpo_args_kwargs["vllm_importance_sampling_mode"] = ( + trl.vllm_importance_sampling_mode + ) + if getattr(trl, "vllm_importance_sampling_cap", None) is not None: + grpo_args_kwargs["vllm_importance_sampling_cap"] = ( + trl.vllm_importance_sampling_cap + ) + if getattr(trl, "off_policy_mask_threshold", None) is not None: + grpo_args_kwargs["off_policy_mask_threshold"] = ( + trl.off_policy_mask_threshold + ) + if getattr(trl, "use_bias_correction_kl", None) is not None: + grpo_args_kwargs["use_bias_correction_kl"] = trl.use_bias_correction_kl + + # Fast Async GRPO fields + if getattr(trl, "reward_num_workers", None) is not None: + grpo_args_kwargs["reward_num_workers"] = trl.reward_num_workers + if getattr(trl, "replay_buffer_size", None) is not None: + grpo_args_kwargs["replay_buffer_size"] = trl.replay_buffer_size + if getattr(trl, "replay_recompute_logps", None) is not None: + grpo_args_kwargs["replay_recompute_logps"] = trl.replay_recompute_logps + if getattr(trl, "reroll_start_fraction", None) is not None: + grpo_args_kwargs["reroll_start_fraction"] = trl.reroll_start_fraction + if getattr(trl, "reroll_max_groups", None) is not None: + grpo_args_kwargs["reroll_max_groups"] = trl.reroll_max_groups + if getattr(trl, "skip_zero_advantage_batches", None) is not None: + grpo_args_kwargs["skip_zero_advantage_batches"] = ( + trl.skip_zero_advantage_batches + ) + if getattr(trl, "vllm_lora_sync", None) is not None: + grpo_args_kwargs["vllm_lora_sync"] = trl.vllm_lora_sync + return grpo_args_kwargs @classmethod diff --git a/src/axolotl/core/trainers/grpo/args.py b/src/axolotl/core/trainers/grpo/args.py index 2ea52998ec..f1dd5a6e7a 100644 --- a/src/axolotl/core/trainers/grpo/args.py +++ b/src/axolotl/core/trainers/grpo/args.py @@ -6,6 +6,7 @@ from trl import GRPOConfig +from axolotl.core.trainers.grpo.fast_async_trainer import FastAsyncGRPOConfig from axolotl.core.training_args import AxolotlTrainingMixins @@ -14,3 +15,10 @@ class AxolotlGRPOConfig(AxolotlTrainingMixins, GRPOConfig): """Axolotl GRPO Config for GRPO training""" context_parallel_size: int | None = None + + +@dataclass +class AxolotlAsyncGRPOConfig(AxolotlTrainingMixins, FastAsyncGRPOConfig): + """Axolotl Async GRPO Config — adds async prefetch, streaming scoring, and IS correction.""" + + context_parallel_size: int | None = None diff --git a/src/axolotl/core/trainers/grpo/async_trainer.py b/src/axolotl/core/trainers/grpo/async_trainer.py new file mode 100644 index 0000000000..acfd029098 --- /dev/null +++ b/src/axolotl/core/trainers/grpo/async_trainer.py @@ -0,0 +1,2657 @@ +""" +Async GRPO training with streaming scoring and IS correction. + +Works on stock TRL v0.29.0 and transformers v5.3.0 — no custom branches needed. + +Features: + - Async prefetch: background thread generates completions via vLLM while the main + thread trains on the previous rollout. + - Deferred scoring: rewards, advantages, and policy logprobs computed on the main + thread (thread-safe with GPU forward passes). + - Streaming group scoring: scores prompt groups incrementally so that reward + computation overlaps with the next group's logprob computation. + - Importance sampling (IS) correction: corrects for stale vLLM weights. + - Off-Policy Sequence Mask (OPSM): drops sequences with high KL + negative advantage. + - Configurable vLLM weight sync interval. + +Classes exported: + - AsyncGRPOConfig: GRPOConfig extended with async/streaming/IS fields + - AsyncGRPOTrainer: GRPOTrainer with async prefetch and IS correction + - ProducerConfig, DataProducer, BaseDataProducer, AsyncDataProducer: data producer protocol +""" + +import atexit +import concurrent.futures +import logging +import queue +import threading +from abc import ABC, abstractmethod +from collections import deque +from contextlib import nullcontext +from dataclasses import dataclass, field +from typing import Any + +import torch +from torch.utils.data import DataLoader, Dataset +from trl.extras.profiling import profiling_decorator +from trl.trainer import GRPOConfig, GRPOTrainer +from trl.trainer.utils import ( + RepeatSampler, + entropy_from_logits, + nanmax, + nanmin, + nanstd, + pad, + selective_log_softmax, + shuffle_sequence_dict, + split_pixel_values_by_grid, + split_tensor_dict, + unsplit_pixel_values_by_grid, +) + +try: + from trl.data_utils import ( + apply_chat_template, + is_conversational, + prepare_multimodal_messages, + ) +except ImportError: + from trl.chat_template_utils import apply_chat_template + from trl.data_utils import is_conversational, prepare_multimodal_messages + +try: + from trl.models.utils import disable_gradient_checkpointing +except ImportError: + from contextlib import contextmanager + + @contextmanager + def disable_gradient_checkpointing(model, kwargs): + yield + + +try: + from accelerate.utils import gather_object +except ImportError: + gather_object = None + +try: + from peft import PeftModel + from trl.trainer.utils import use_adapter +except ImportError: + PeftModel = None + use_adapter = nullcontext + +try: + from liger_kernel.ops.grpo_loss import ( + fused_selective_log_softmax as _fused_selective_log_softmax, + ) +except ImportError: + _fused_selective_log_softmax = None + + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- + + +@dataclass +class AsyncGRPOConfig(GRPOConfig): + """GRPOConfig extended with async prefetch, streaming scoring, and IS correction fields. + + Fields already present in stock GRPOConfig (e.g. ``importance_sampling_level``, + ``multi_objective_aggregation``) are listed here for safety: if the stock version + does not define them, the defaults below ensure everything works. + """ + + # --- Data producer --- + use_data_producer: bool = field( + default=False, + metadata={ + "help": "Use the GRPODataProducer protocol for online data generation." + }, + ) + + # --- Async data production --- + async_prefetch: bool = field( + default=False, + metadata={ + "help": "Generate rollouts in a background thread while training on the previous rollout." + }, + ) + prefetch_depth: int = field( + default=1, + metadata={"help": "Number of rollouts to prefetch ahead of training."}, + ) + vllm_sync_interval: int = field( + default=1, + metadata={ + "help": "Sync model weights to vLLM every N optimizer steps (async mode only)." + }, + ) + + # --- Streaming scoring --- + streaming_partial_batch: bool = field( + default=False, + metadata={ + "help": "Score prompt groups incrementally instead of the full batch at once." + }, + ) + streaming_min_groups: int = field( + default=1, + metadata={"help": "Minimum prompt groups to score per streaming chunk."}, + ) + + # --- vLLM importance sampling correction --- + vllm_importance_sampling_correction: bool = field( + default=True, + metadata={ + "help": "Apply IS correction for distribution mismatch between vLLM and training model." + }, + ) + vllm_importance_sampling_mode: str = field( + default="token_truncate", + metadata={ + "help": "IS mode: token_truncate, token_mask, sequence_truncate, or sequence_mask." + }, + ) + vllm_importance_sampling_cap: float = field( + default=3.0, + metadata={"help": "Cap C for IS ratio clipping/masking."}, + ) + + # --- Off-policy sequence mask (OPSM) --- + off_policy_mask_threshold: float | None = field( + default=None, + metadata={"help": "KL threshold for OPSM (DeepSeek-V3.2). None = disabled."}, + ) + + # --- Bias-corrected KL --- + use_bias_correction_kl: bool = field( + default=False, + metadata={"help": "Apply IS correction to KL divergence term."}, + ) + + +# --------------------------------------------------------------------------- +# Data Producer Protocol (standalone — no transformers branch needed) +# --------------------------------------------------------------------------- + +logger = logging.getLogger(__name__) +_dp_logger = logging.getLogger(__name__ + ".data_producer") + + +@dataclass +class ProducerConfig: + """Configuration for a :class:`DataProducer`. + + Args: + mini_epochs: Number of training passes over each produced dataset. + max_rollouts: Maximum number of produce-then-train rounds (None = unlimited). + steps_per_generation: Optimisation steps per produced dataset before regenerating. + num_iterations: Number of times to reuse each generation across optimisation steps. + async_prefetch: Produce the next dataset in a background thread. + prefetch_depth: How many rollouts to queue ahead when async. + sync_warmup_rollouts: Initial on-policy rollouts before switching to async. + eval_during_produce: Switch model to eval() during produce(). + empty_cache_before_produce: torch.cuda.empty_cache() before produce(). + empty_cache_after_produce: torch.cuda.empty_cache() after produce(). + """ + + mini_epochs: int = 1 + max_rollouts: int | None = None + steps_per_generation: int | None = None + num_iterations: int = 1 + async_prefetch: bool = False + prefetch_depth: int = 1 + sync_warmup_rollouts: int = 0 + eval_during_produce: bool = True + empty_cache_before_produce: bool = False + empty_cache_after_produce: bool = False + + def __post_init__(self): + if self.mini_epochs < 1: + raise ValueError(f"mini_epochs must be >= 1, got {self.mini_epochs}") + if self.max_rollouts is not None and self.max_rollouts < 1: + raise ValueError( + f"max_rollouts must be >= 1 or None, got {self.max_rollouts}" + ) + if self.num_iterations < 1: + raise ValueError(f"num_iterations must be >= 1, got {self.num_iterations}") + if self.steps_per_generation is not None and self.steps_per_generation < 1: + raise ValueError( + f"steps_per_generation must be >= 1 or None, got {self.steps_per_generation}" + ) + if self.prefetch_depth < 1: + raise ValueError(f"prefetch_depth must be >= 1, got {self.prefetch_depth}") + if self.sync_warmup_rollouts < 0: + raise ValueError( + f"sync_warmup_rollouts must be >= 0, got {self.sync_warmup_rollouts}" + ) + + +class DataProducer(ABC): + """Abstract base class for online data producers. + + Subclass this and implement :meth:`produce` to supply fresh training data + each rollout round. + """ + + config: ProducerConfig + + @abstractmethod + def produce( + self, + model: Any, + global_step: int, + *, + processing_class: Any = None, + accelerator: Any = None, + args: Any = None, + **kwargs, + ) -> Dataset: + """Generate a fresh training dataset.""" + ... + + +class BaseDataProducer(DataProducer): + """Convenience base class with a default :class:`ProducerConfig` and lifecycle hooks.""" + + def __init__(self, config: ProducerConfig | None = None): + self.config = config or ProducerConfig() + + def on_rollout_begin(self, global_step: int) -> None: + """Called before each produce() invocation.""" + + def on_rollout_end(self, dataset: Dataset, global_step: int) -> None: + """Called after each produce() invocation with the produced dataset.""" + + +class AsyncDataProducer: + """Wraps a synchronous :class:`DataProducer` for background-thread data generation. + + While the Trainer trains on the current rollout, this wrapper produces upcoming + datasets in a background thread. + + FSDP compatibility: Background threads must NOT call cross-rank collectives + (gather_object, broadcast_object_list, FSDP all-gather) because the main thread + may be doing FSDP forward/backward concurrently, causing deadlocks. When + ``num_processes > 1``, only rank 0 runs BG generation; results are broadcast + to other ranks on the main thread when ``produce()`` is next called. + """ + + def __init__( + self, inner: DataProducer, background_produce_kwargs: dict | None = None + ): + self._inner = inner + self._depth = inner.config.prefetch_depth + self._warmup_remaining = inner.config.sync_warmup_rollouts + self._background_kwargs = background_produce_kwargs or {} + self._executor = concurrent.futures.ThreadPoolExecutor( + max_workers=1, thread_name_prefix="async-producer" + ) + self._queue: deque[concurrent.futures.Future] = deque() + self._initialized = False + # Lock held by the background thread during vLLM generation. + # The main thread acquires this lock for weight sync to ensure + # merge_adapter/unmerge_adapter don't overlap with generation. + self._generate_lock = threading.Lock() + # Detected at first produce() call + self._num_processes: int | None = None + self._is_main: bool | None = None + + @property + def config(self) -> ProducerConfig: + return self._inner.config + + def produce(self, model: Any, global_step: int, **kwargs) -> Dataset: + """Return the next dataset, blocking if the prefetch hasn't finished.""" + # Detect multi-process on first call + if self._num_processes is None: + accelerator = kwargs.get("accelerator") + if accelerator is not None: + self._num_processes = accelerator.num_processes + self._is_main = accelerator.is_main_process + else: + self._num_processes = 1 + self._is_main = True + + # During warmup, produce synchronously (on-policy) + if self._warmup_remaining > 0: + self._warmup_remaining -= 1 + _dp_logger.info( + f"AsyncDataProducer: sync warmup rollout (remaining={self._warmup_remaining})" + ) + return self._inner.produce(model, global_step, **kwargs) + + if not self._initialized: + dataset = self._inner.produce(model, global_step, **kwargs) + bg_kwargs = {**kwargs, **self._background_kwargs} + # With FSDP (multi-process), only submit BG tasks on rank 0. + # Non-rank-0 processes will receive data via broadcast. + if self._num_processes > 1: + bg_kwargs["_rank0_only"] = True + for i in range(1, self._depth + 1): + self._queue.append( + self._executor.submit( + self._locked_produce, model, global_step + i, **bg_kwargs + ) + ) + self._initialized = True + return dataset + + # Get the pre-generated dataset from the BG thread + dataset = self._queue.popleft().result() + + # With FSDP: BG thread only ran on rank 0. Broadcast to all ranks. + if self._num_processes > 1: + dataset = self._broadcast_dataset(dataset) + + bg_kwargs = {**kwargs, **self._background_kwargs} + if self._num_processes > 1: + bg_kwargs["_rank0_only"] = True + next_step = global_step + self._depth + self._queue.append( + self._executor.submit(self._locked_produce, model, next_step, **bg_kwargs) + ) + return dataset + + def _broadcast_dataset(self, dataset) -> Dataset: + """Broadcast a prefetched dataset from rank 0 to all ranks (main thread). + + Rank 0 has a full RolloutDataset from BG generation; other ranks have None. + After broadcast, tensors are moved to each rank's local device. + """ + import torch.distributed as dist + + if not dist.is_initialized(): + return dataset + + # Rank 0 sends _data dict; others receive it + obj_list = [dataset._data if self._is_main else None] + dist.broadcast_object_list(obj_list, src=0) + + data: dict[str, Any] = obj_list[0] # type: ignore[assignment] + + # Move tensors to local device (broadcast_object_list deserializes to CPU) + accelerator = self._inner._trainer.accelerator # type: ignore[attr-defined] + device = accelerator.device + for key, val in data.items(): + if isinstance(val, torch.Tensor) and val.device != device: + data[key] = val.to(device) + + if not self._is_main: + from axolotl.core.trainers.grpo.async_trainer import RolloutDataset + + dataset = RolloutDataset(data) + else: + # Rank 0 already has the dataset, but update _data with device-moved tensors + dataset._data = data + return dataset + + def _locked_produce(self, model: Any, global_step: int, **kwargs) -> Dataset: + """Run produce while holding the generate lock.""" + with self._generate_lock: + return self._inner.produce(model, global_step, **kwargs) + + def on_rollout_begin(self, global_step: int) -> None: + if hasattr(self._inner, "on_rollout_begin"): + self._inner.on_rollout_begin(global_step) + + def on_rollout_end(self, dataset: Dataset, global_step: int) -> None: + if hasattr(self._inner, "on_rollout_end"): + self._inner.on_rollout_end(dataset, global_step) + + def shutdown(self) -> None: + """Shut down the background thread pool and cancel pending futures.""" + for future in self._queue: + future.cancel() + self._queue.clear() + self._executor.shutdown(wait=False) + + +class DataProducerCallback: + """Marker class: if a DataProducer also inherits from this, the Trainer will + automatically register it as a callback.""" + + pass + + +# --------------------------------------------------------------------------- +# RolloutDataset + GRPODataProducer +# --------------------------------------------------------------------------- + + +class RolloutDataset(Dataset): + """A Dataset wrapping the output dict from _generate_and_score_completions. + + Per-sample tensors are sliced by index; shared metadata is passed through. + """ + + _ALWAYS_SHARED = frozenset( + {"num_items_in_batch", "_pending_policy_logps", "_rank0_only"} + ) + + def __init__(self, data: dict[str, Any]): + self._data = data + self._shared_keys: set[str] = set() + self._sample_keys: set[str] = set() + + for key, val in data.items(): + if key in self._ALWAYS_SHARED: + self._shared_keys.add(key) + elif not isinstance(val, torch.Tensor): + self._shared_keys.add(key) + elif val.dim() == 0: + self._shared_keys.add(key) + else: + self._sample_keys.add(key) + + self._num_samples = 0 + for key in self._sample_keys: + n = data[key].size(0) + if self._num_samples == 0: + self._num_samples = n + elif n != self._num_samples: + raise ValueError( + f"Inconsistent sample count: key '{key}' has {n}, expected {self._num_samples}" + ) + if self._num_samples == 0: + raise ValueError("No per-sample tensors found in rollout data") + + def __len__(self) -> int: + return self._num_samples + + def __getitem__(self, idx: int) -> dict[str, Any]: + item: dict[str, Any] = {} + for key in self._sample_keys: + item[key] = self._data[key][idx] + for key in self._shared_keys: + item[key] = self._data[key] + return item + + +def make_rollout_collator(shared_keys: set[str]): + """Return a collator that stacks per-sample tensors and passes shared keys through.""" + + def _collate(batch: list[dict[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key in batch[0]: + if key in shared_keys: + result[key] = batch[0][key] + else: + values = [item[key] for item in batch] + if isinstance(values[0], torch.Tensor): + result[key] = torch.stack(values) + else: + result[key] = values + return result + + return _collate + + +class GRPODataProducer(BaseDataProducer): + """Produces GRPO training rollouts using the trainer's generation pipeline. + + Created before Trainer.__init__ completes; the trainer reference is injected + later via set_trainer(). + """ + + def __init__( + self, + config: ProducerConfig, + prompt_dataset, + *, + num_generations: int, + generation_batch_size: int, + train_batch_size: int, + steps_per_generation: int, + shuffle_dataset: bool, + seed: int, + ): + super().__init__(config) + self._dataset = prompt_dataset + self._num_generations = num_generations + self._generation_batch_size = generation_batch_size + self._train_batch_size = train_batch_size + self._steps_per_generation = steps_per_generation + self._shuffle_dataset = shuffle_dataset + self._seed = seed + self._trainer: Any = None + self._prompt_dl: Any = None + self._prompt_iter: Any = None + + def set_trainer(self, trainer) -> None: + """Inject the live trainer reference and create the prompt DataLoader.""" + self._trainer = trainer + self._init_prompt_dataloader() + + def _init_prompt_dataloader(self) -> None: + from functools import partial + + from transformers.trainer_utils import seed_worker + + trainer = self._trainer + sampler = RepeatSampler( + data_source=self._dataset, + mini_repeat_count=self._num_generations, + batch_size=self._generation_batch_size // self._num_generations, + repeat_count=1, + shuffle=self._shuffle_dataset, + seed=self._seed, + ) + + # Use identity collator (same as stock GRPOTrainer) + def _identity(x): + return x + + dl = DataLoader( + self._dataset, + batch_size=self._train_batch_size * self._steps_per_generation, + sampler=sampler, + collate_fn=_identity, + num_workers=trainer.args.dataloader_num_workers, + pin_memory=trainer.args.dataloader_pin_memory, + persistent_workers=trainer.args.dataloader_persistent_workers, + worker_init_fn=partial( + seed_worker, + num_workers=trainer.args.dataloader_num_workers, + rank=trainer.args.process_index, + ), + ) + self._prompt_dl = trainer.accelerator.prepare(dl) + + # Don't let accelerator track this dataloader + acc_dls = trainer.accelerator._dataloaders + if self._prompt_dl in acc_dls: + acc_dls.remove(self._prompt_dl) + + self._prompt_iter = iter(self._prompt_dl) + + def produce( + self, + model: Any, + global_step: int, + *, + skip_policy_logps: bool = False, + processing_class: Any = None, + accelerator: Any = None, + args: Any = None, + _rank0_only: bool = False, + **kwargs, + ) -> RolloutDataset | None: + """Generate a fresh GRPO training rollout.""" + is_main = self._trainer.accelerator.is_main_process + + # FSDP rank0-only mode: non-rank-0 returns None (broadcast fills it later) + if _rank0_only and not is_main: + return None + + try: + inputs = next(self._prompt_iter) + except StopIteration: + self._prompt_iter = iter(self._prompt_dl) + inputs = next(self._prompt_iter) + + if skip_policy_logps: + # Async path: use _generate_only (generation without scoring) which + # works on stock TRL (no skip_policy_logps parameter needed). + output = self._trainer._generate_only(inputs, rank0_only=_rank0_only) + else: + # Sync path: full generation + scoring + output = self._trainer._generate_and_score_completions(inputs) + + # Strip non-sequence metadata before shuffling + metadata = {} + for key in list(output.keys()): + val = output[key] + if not isinstance(val, (torch.Tensor, list)): + metadata[key] = output.pop(key) + elif isinstance(val, torch.Tensor) and val.dim() == 0: + metadata[key] = output.pop(key) + + output = shuffle_sequence_dict(output) + output.update(metadata) + + return RolloutDataset(output) + + +# --------------------------------------------------------------------------- +# Trainer +# --------------------------------------------------------------------------- + + +class AsyncGRPOTrainer(GRPOTrainer): + """GRPOTrainer with async prefetch, streaming scoring, and IS correction. + + Drop-in replacement: pass ``AsyncGRPOConfig`` as ``args`` and use this trainer + instead of ``GRPOTrainer``. + """ + + def __init__(self, *args, **kwargs): + # When using native LoRA sync, skip the NCCL communicator init in VLLMGeneration. + # The communicator is not needed because weight sync happens via filesystem + HTTP, + # and it fails when vLLM and a trainer rank share the same CUDA device. + training_args = kwargs.get("args") or (args[1] if len(args) > 1 else None) + if training_args is not None and getattr( + training_args, "vllm_lora_sync", False + ): + from trl.generation.vllm_generation import VLLMGeneration + + _orig_init_vllm = VLLMGeneration._init_vllm + + def _init_vllm_no_communicator(self_vllm): + """Init vLLM client without NCCL communicator (LoRA sync uses filesystem).""" + if self_vllm.mode == "server" and self_vllm.accelerator.is_main_process: + from trl.generation.vllm_client import VLLMClient + + if self_vllm.server_base_url is not None: + base_url = self_vllm.server_base_url + else: + base_url = ( + f"http://{self_vllm.server_host}:{self_vllm.server_port}" + ) + self_vllm.vllm_client = VLLMClient( + base_url=base_url, + group_port=self_vllm.group_port, + connection_timeout=self_vllm.server_timeout, + ) + # Deliberately skip init_communicator — no NCCL needed + elif self_vllm.mode != "server": + _orig_init_vllm(self_vllm) + + VLLMGeneration._init_vllm = _init_vllm_no_communicator + + super().__init__(*args, **kwargs) + + # FP8 models: zero out the pad token embedding so that padding + # positions have zero hidden states throughout the network. + # FP8 linear layers produce NaN on non-zero inputs at masked + # positions (the Triton fp8 matmul kernel can't handle the + # extreme values that accumulate at unattended positions). + self._zero_pad_embedding_for_fp8() + + # Ensure custom attributes exist (stock GRPOTrainer.__init__ may not set them). + for attr, cfg_key, default in [ + ( + "vllm_importance_sampling_correction", + "vllm_importance_sampling_correction", + True, + ), + ( + "vllm_importance_sampling_mode", + "vllm_importance_sampling_mode", + "token_truncate", + ), + ("vllm_importance_sampling_cap", "vllm_importance_sampling_cap", 3.0), + ("off_policy_mask_threshold", "off_policy_mask_threshold", None), + ]: + if not hasattr(self, attr): + setattr(self, attr, getattr(self.args, cfg_key, default)) + + # Async state + self._async_queue: queue.Queue | None = None + self._executor: concurrent.futures.ThreadPoolExecutor | None = None + self._prompt_iter = None + self._last_synced_step = -1 + self._buffered_inputs: list | None = None # override stock attr + + # Data producer (the proper architecture for async generation) + self.data_producer = None + if getattr(self.args, "use_data_producer", False): + self.data_producer = self._create_data_producer( + kwargs["args"], kwargs["train_dataset"] + ) + + if self.args.async_prefetch and self.data_producer is None: + # Legacy path: direct _prepare_inputs override without data producer + self._setup_async() + + def _create_data_producer(self, args, train_dataset): + """Create and return the GRPODataProducer (possibly wrapped in AsyncDataProducer).""" + producer_config = ProducerConfig( + mini_epochs=args.num_iterations, + max_rollouts=None, + eval_during_produce=False, + empty_cache_before_produce=True, + empty_cache_after_produce=True, + async_prefetch=args.async_prefetch, + prefetch_depth=args.prefetch_depth, + ) + data_producer = GRPODataProducer( + config=producer_config, + prompt_dataset=train_dataset, + num_generations=self.num_generations, + generation_batch_size=args.generation_batch_size, + train_batch_size=args.per_device_train_batch_size, + steps_per_generation=args.steps_per_generation, + shuffle_dataset=getattr(self, "shuffle_dataset", True), + seed=args.seed, + ) + data_producer.set_trainer(self) + + if args.async_prefetch: + data_producer = AsyncDataProducer( + data_producer, + background_produce_kwargs={"skip_policy_logps": True}, + ) + return data_producer + + # ------------------------------------------------------------------ + # Async setup / teardown + # ------------------------------------------------------------------ + + def _setup_async(self): + """Create background thread pool, prompt iterator, and pre-fill the async queue.""" + gen_batch_size = getattr( + self.args, + "generation_batch_size", + self._train_batch_size * self.args.gradient_accumulation_steps, + ) + # RepeatSampler groups prompts with num_generations repetitions each. + # DataLoader batches the yielded indices into generation-sized batches. + sampler = RepeatSampler( + data_source=self.train_dataset, + mini_repeat_count=self.num_generations, + batch_size=gen_batch_size // self.num_generations, + repeat_count=10_000, # effectively infinite + shuffle=True, + seed=self.args.seed, + ) + self._prompt_dataloader = DataLoader( + self.train_dataset, + batch_size=gen_batch_size, + sampler=sampler, + collate_fn=self.data_collator, + num_workers=0, + ) + self._prompt_iter = iter(self._prompt_dataloader) + self._async_queue = queue.Queue(maxsize=self.args.prefetch_depth) + self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) + + # Pre-submit generations to fill the queue + for _ in range(self.args.prefetch_depth): + self._submit_generation() + + atexit.register(self._shutdown_async) + + def _shutdown_async(self): + if self._executor is not None: + self._executor.shutdown(wait=False, cancel_futures=True) + self._executor = None + + def _submit_generation(self): + """Submit the next background generation job.""" + batch = next(self._prompt_iter) + future = self._executor.submit(self._generate_only, batch) + self._async_queue.put(future) + + # ------------------------------------------------------------------ + # Weight sync + # ------------------------------------------------------------------ + + def _sync_peft_weights_no_merge(self): + """Thread-safe weight sync: compute merged LoRA weights without in-place modification. + + Required for FP8 models where merge_adapter() fails (addmm not implemented + for Float8), and also safe for concurrent use since it never modifies base + weights in-place. + """ + model = self.vllm_generation.model + accelerator = self.vllm_generation.accelerator + vllm_client = self.vllm_generation.vllm_client + fix_name = self.vllm_generation._fix_param_name_to_vllm + + if not (self.vllm_generation.mode == "server" and accelerator.is_main_process): + return + + # Build lookup: module_path -> (A, B, scaling) for all active LoRA layers + lora_info = {} + for mod_name, module in model.base_model.model.named_modules(): + if not hasattr(module, "lora_A") or not hasattr(module, "active_adapters"): + continue + active = module.active_adapters[0] + if active not in module.lora_A: + continue + lora_info[mod_name] = ( + module.lora_A[active].weight.data, + module.lora_B[active].weight.data, + module.scaling[active], + ) + + # Build lookup for FP8 scale_inv parameters (needed for dequantization) + scale_inv_lookup = {} + for pname, pparam in model.named_parameters(): + if "weight_scale_inv" in pname: + # Map weight name -> scale_inv tensor + weight_name = pname.replace(".weight_scale_inv", ".weight") + scale_inv_lookup[weight_name] = pparam.data + + # Iterate all parameters, computing merged weights for LoRA layers. + # Skip LoRA-specific params and FP8 scale params (scales will be + # recomputed by vLLM when it receives the merged bf16 weight). + params_to_sync = [] + for name, param in model.named_parameters(): + vllm_name = name.removeprefix("base_model.model.").replace( + ".base_layer", "" + ) + if model.prefix in vllm_name: + continue + if "original_module" in vllm_name: + continue + # Skip FP8 quantization scale parameters - they are recomputed + # on the vLLM side when we update the weight itself + if "weight_scale_inv" in vllm_name or "input_scale" in vllm_name: + continue + vllm_name = fix_name(vllm_name, extra_prefixes=["modules_to_save.default."]) + + data = param.data + compute_dtype = torch.bfloat16 + + if vllm_name.endswith(".weight"): + # Dequantize FP8 weights before merging + if data.dtype == torch.float8_e4m3fn and name in scale_inv_lookup: + scale_inv = scale_inv_lookup[name] + # Block dequantization: weight * scale_inv (with broadcasting) + fp8_bf16 = data.to(compute_dtype) + if scale_inv.dim() == 2 and fp8_bf16.dim() == 2: + # Block-quantized: scale_inv shape (rows/block, cols/block) + sr, sc = scale_inv.shape + br = fp8_bf16.shape[0] // sr # block height + bc = fp8_bf16.shape[1] // sc # block width + # Reshape → multiply by block scale → reshape back + data = ( + fp8_bf16.reshape(sr, br, sc, bc) + * scale_inv[:, None, :, None].to(compute_dtype) + ).reshape(fp8_bf16.shape) + elif scale_inv.dim() <= 1: + # Per-tensor or per-channel scale + data = fp8_bf16 * scale_inv.to(compute_dtype) + else: + data = fp8_bf16 + elif data.dtype == torch.float8_e4m3fn: + # FP8 but no scale found - just cast (lossy) + data = data.to(compute_dtype) + + mod_path = vllm_name[: -len(".weight")] + if mod_path in lora_info: + A, B, s = lora_info[mod_path] + merged = data.to(compute_dtype) + s * ( + B.to(compute_dtype) @ A.to(compute_dtype) + ) + data = merged + + params_to_sync.append((vllm_name, data)) + + # Batch sync all params in one HTTP+NCCL call (vs individual calls) + if params_to_sync: + vllm_client.batch_update_named_params(params_to_sync) + + # Reset prefix cache after weight update + vllm_client.reset_prefix_cache() + + def _sync_lora_adapter(self): + """Sync LoRA adapter to vLLM via filesystem (native LoRA mode). + + Saves the PEFT adapter to a temp directory and POSTs the path to vLLM's + /set_lora_adapter/ endpoint. vLLM loads the adapter natively using Punica + kernels, avoiding the need to merge weights and NCCL-broadcast the full model. + + Syncs only the LoRA adapter weights via filesystem instead of the full merged model via NCCL. + + FSDP/DeepSpeed: All ranks must participate in the state_dict gather. + accelerator.get_state_dict() handles this (FSDP uses FullStateDictConfig + with rank0_only=True). Only rank 0 gets the full dict, writes files, and + does the HTTP POST. + """ + import os + import tempfile + + accelerator = self.vllm_generation.accelerator + model = self.vllm_generation.model + + if self.vllm_generation.mode != "server": + return + + is_main = accelerator.is_main_process + + # Increment adapter version (all ranks, kept in sync) + if not hasattr(self, "_lora_sync_version"): + self._lora_sync_version = 0 + if is_main: + self._lora_sync_dir = tempfile.mkdtemp(prefix="lora_sync_") + else: + self._lora_sync_dir = None + # Broadcast sync dir from rank 0 to all ranks + if accelerator.num_processes > 1: + import torch.distributed as dist + + if dist.is_initialized(): + obj_list = [self._lora_sync_dir] + dist.broadcast_object_list(obj_list, src=0) + self._lora_sync_dir = obj_list[0] + self._lora_sync_version += 1 + + adapter_path = os.path.join(self._lora_sync_dir, f"v{self._lora_sync_version}") + + # Gather state dict from all ranks (FSDP/DeepSpeed gather, rank0_only) + # All ranks must participate even though only rank 0 gets the result. + # Use self.model_wrapped (the DeepSpeed/FSDP engine) for get_state_dict, + # since it has the necessary hooks (e.g. zero_gather_16bit_weights_on_model_save). + # self.vllm_generation.model is the unwrapped PEFT model which lacks these. + wrapped_model = getattr(self, "model_wrapped", model) + state_dict = accelerator.get_state_dict(wrapped_model) + + if is_main: + # Unwrap to access PEFT's save_pretrained + unwrapped = accelerator.unwrap_model(model) + unwrapped.save_pretrained(adapter_path, state_dict=state_dict) + + import requests + + vllm_client = self.vllm_generation.vllm_client + url = f"{vllm_client.base_url}/set_lora_adapter/" + response = requests.post( + url, + json={ + "lora_name": "active_lora", + "lora_int_id": self._lora_sync_version, + "lora_path": adapter_path, + }, + timeout=30, + ) + if response.status_code != 200: + logger.warning( + "Failed to set LoRA adapter: %s %s", + response.status_code, + response.text, + ) + return + + # Reset prefix cache after adapter update + vllm_client.reset_prefix_cache() + + # Clean up old adapter versions (keep only current) + if self._lora_sync_version > 1: + old_path = os.path.join( + self._lora_sync_dir, f"v{self._lora_sync_version - 1}" + ) + if os.path.exists(old_path): + import shutil + + shutil.rmtree(old_path, ignore_errors=True) + + logger.info( + "Synced LoRA adapter v%d to vLLM (%s)", + self._lora_sync_version, + adapter_path, + ) + + # Barrier to ensure all ranks complete before resuming forward passes. + # Without this, rank 1 may start a forward pass (triggering FSDP unshard) + # while rank 0 is still doing save_pretrained, causing FSDP all-gather deadlock. + if accelerator.num_processes > 1: + import torch.distributed as dist + + if dist.is_initialized(): + dist.barrier() + + def _maybe_sync_vllm_weights(self): + """Sync model weights to vLLM if the interval has elapsed. + + Dispatches to one of three strategies: + - vllm_lora_sync: saves adapter to filesystem, vLLM loads natively + - PEFT no-merge: computes merged weights as new tensors, NCCL broadcast + - Non-PEFT: stock sync_weights via merge_adapter + NCCL + """ + if not (self.use_vllm and self.args.async_prefetch): + return + step = self.state.global_step + interval = self.args.vllm_sync_interval + if step != self._last_synced_step and step % interval == 0: + if getattr(self.args, "vllm_lora_sync", False): + if step == 0: + logger.info("Skipping LoRA sync at step 0 (no training yet)") + self._last_synced_step = step + return + # Native LoRA sync: save adapter to filesystem, vLLM loads it directly + self._sync_lora_adapter() + else: + from accelerate.utils import is_peft_model + + use_no_merge = is_peft_model(self.vllm_generation.model) + + if use_no_merge: + # No-merge sync: computes merged weights as new tensors + # (doesn't modify base weights in-place), so it's safe to + # run concurrently with BG generation — no lock needed. + self._sync_peft_weights_no_merge() + else: + # Non-PEFT: use stock sync (acquires lock to avoid overlap) + if self.data_producer is not None and hasattr( + self.data_producer, "_generate_lock" + ): + with self.data_producer._generate_lock: + self.vllm_generation.sync_weights() + elif self._async_queue is not None: + pending = list(self._async_queue.queue) + for f in pending: + if isinstance(f, concurrent.futures.Future): + f.result() + self.vllm_generation.sync_weights() + else: + self.vllm_generation.sync_weights() + self._last_synced_step = step + + def _zero_pad_embedding_for_fp8(self): + """Zero out the pad token embedding for FP8 models. + + FP8 linear layers produce NaN when processing positions with + attention_mask=0 (the hidden states at those positions have + unconstrained values that overflow FP8 range during + quantization). By setting the pad token embedding to zeros, + padding positions start with zero hidden states and stay zero + through masked attention, preventing NaN from FP8 matmul. + """ + model = self.accelerator.unwrap_model(self.model) + # Check if model has FP8 weights + has_fp8 = any( + p.dtype == torch.float8_e4m3fn + for p in model.parameters() + if not p.requires_grad + ) + if not has_fp8: + return + + # Find the embedding layer + if hasattr(model, "model") and hasattr(model.model, "embed_tokens"): + embed = model.model.embed_tokens + elif hasattr(model, "base_model") and hasattr(model.base_model, "model"): + m = model.base_model.model + if hasattr(m, "model") and hasattr(m.model, "embed_tokens"): + embed = m.model.embed_tokens + else: + return + else: + return + + pad_id = self.processing_class.pad_token_id + if pad_id is not None and pad_id < embed.weight.shape[0]: + with torch.no_grad(): + embed.weight.data[pad_id].zero_() + import logging + + logging.getLogger("async_grpo").info( + f"Zeroed pad token embedding (id={pad_id}) for FP8 NaN prevention" + ) + + # ------------------------------------------------------------------ + # Background-thread generation (no scoring) + # ------------------------------------------------------------------ + + def _generate_single_turn(self, prompts, **kwargs): + """Override to prevent weight sync from background thread and to use + no-merge sync for PEFT models (FP8 models can't merge_adapter).""" + is_bg = threading.current_thread() is not threading.main_thread() + saved_step = None + + if is_bg and self.use_vllm: + # Trick: match _last_loaded_step so the stock sync check is a no-op + saved_step = getattr(self, "_last_loaded_step", None) + self._last_loaded_step = self.state.global_step + + # Permanently replace vllm_generation.sync_weights with our custom + # sync to avoid merge_adapter (fails on FP8 / races with training). + # For LoRA sync mode, make it a no-op here since _maybe_sync_vllm_weights + # handles the sync with proper interval tracking. + if not getattr(self, "_patched_sync_weights", False): + if self.use_vllm and hasattr(self, "vllm_generation"): + if getattr(self.args, "vllm_lora_sync", False): + # No-op: LoRA sync is driven by _maybe_sync_vllm_weights + self.vllm_generation.sync_weights = lambda: None + self._patched_sync_weights = True + else: + from accelerate.utils import is_peft_model + + if is_peft_model(self.vllm_generation.model): + + def _no_merge_sync(): + self._sync_peft_weights_no_merge() + + self.vllm_generation.sync_weights = _no_merge_sync + self._patched_sync_weights = True + + try: + return super()._generate_single_turn(prompts, **kwargs) + finally: + if saved_step is not None: + self._last_loaded_step = saved_step + + def _generate_rank0_only(self, prompts): + """Generate using vLLM directly on rank 0 without cross-rank collectives. + + Called from BG thread in FSDP mode. Bypasses ``gather_object`` / + ``broadcast_object_list`` since the main thread may be running FSDP + collectives concurrently. + + Returns the same tuple as ``_generate``. + """ + import copy + + prompts = copy.deepcopy(prompts) + + # Duplicate prompts for num_generations (same as TRL's gather+unique pattern) + num_generations = self.num_generations + unique_prompts = prompts[::num_generations] + + # Build sampling params + vg = self.vllm_generation + sampling_params = { + "n": num_generations, + "repetition_penalty": vg.repetition_penalty, + "temperature": vg.temperature, + "top_p": vg.top_p, + "top_k": vg.top_k, + "min_p": 0.0 if vg.min_p is None else vg.min_p, + "max_tokens": vg.max_completion_length, + "logprobs": vg.logprobs, + "structured_outputs_regex": vg.structured_outputs_regex, + "generation_kwargs": vg.generation_kwargs, + } + + # Call vLLM directly (no collectives) + from trl.data_utils import is_conversational + + if is_conversational({"prompt": unique_prompts[0]}): + output = vg.vllm_client.chat( + messages=unique_prompts, + **sampling_params, + chat_template_kwargs=vg.chat_template_kwargs, + tools=vg.tools, + chat_template=vg.chat_template, + ) + else: + output = vg.vllm_client.generate(prompts=unique_prompts, **sampling_params) + + # vLLM returns 1 prompt_ids per unique prompt, but num_generations completion_ids. + # Duplicate prompt_ids to match completions (one per generation). + raw_prompt_ids = output["prompt_ids"] + prompt_ids = [pid for pid in raw_prompt_ids for _ in range(num_generations)] + completion_ids = output["completion_ids"] + logprobs_raw = output["logprobs"] + extra_fields = { + k: v + for k, v in output.items() + if k + not in {"prompt_ids", "completion_ids", "logprobs", "logprob_token_ids"} + } + + # Extract top-1 logprob per token + logprobs = [[lp[0] for lp in seq] for seq in logprobs_raw] + + # Decode completions + if is_conversational({"prompt": prompts[0]}): + contents = self.processing_class.batch_decode( + completion_ids, skip_special_tokens=True + ) + completions = [[{"role": "assistant", "content": c}] for c in contents] + else: + completions = self.processing_class.batch_decode( + completion_ids, skip_special_tokens=True + ) + + tool_mask = extra_fields.pop("env_mask", None) + + # Compute total completion tokens locally (no gather) + total_completion_tokens = sum(len(ids) for ids in completion_ids) + + return ( + prompt_ids, + completion_ids, + tool_mask, + completions, + total_completion_tokens, + logprobs, + extra_fields, + ) + + def _generate_only(self, inputs, rank0_only=False): + """Generate completions without scoring. Runs on background thread. + + Mirrors the first half of ``_generate_and_score_completions`` (prompt + extraction → vLLM generation → tensor padding) and returns a deferred + output dict for main-thread scoring. + + When ``rank0_only=True`` (FSDP mode), bypasses ``gather_object`` / + ``broadcast_object_list`` collectives and calls vLLM directly on rank 0. + Results are broadcast to other ranks on the main thread later. + + Args: + inputs: list of dicts (one per sample), as yielded by the DataLoader + with ``identity`` collate_fn. + """ + device = self.accelerator.device + + prompts = [x["prompt"] for x in inputs] + + # --- Handle images (multimodal) --- + if "images" in inputs[0]: + images = [ex.get("images") for ex in inputs] + elif "image" in inputs[0]: + images = [ + [ex.get("image")] if ex.get("image") is not None else None + for ex in inputs + ] + else: + images = None + if images is not None and all(img == [] for img in images): + images = None + + if images is not None: + if not is_conversational(inputs[0]): + raise ValueError("Multimodal training requires conversational prompts.") + prompts = [ + prepare_multimodal_messages(p, il) + for p, il in zip(prompts, images, strict=True) + ] + + # --- Generate completions --- + if rank0_only: + # FSDP mode: call vLLM directly without cross-rank collectives + ( + prompt_ids_list, + completion_ids_list, + tool_mask_list, + completions, + num_items_in_batch, + sampling_per_token_logps_list, + extra_fields, + ) = self._generate_rank0_only(prompts) + else: + ( + prompt_ids_list, + completion_ids_list, + tool_mask_list, + completions, + num_items_in_batch, + sampling_per_token_logps_list, + extra_fields, + ) = self._generate(prompts) + # _generate gathers prompts from all ranks internally. Gather inputs + # to match the full-batch output size. + if self.accelerator.num_processes > 1: + from accelerate.utils import gather_object + + inputs = gather_object(inputs) + prompts = [x["prompt"] for x in inputs] + + # --- Pad to tensors --- + prompt_ids = [torch.tensor(ids, device=device) for ids in prompt_ids_list] + prompt_mask = [torch.ones_like(ids, dtype=torch.long) for ids in prompt_ids] + prompt_ids = pad( + prompt_ids, padding_value=self.pad_token_id, padding_side="left" + ) + prompt_mask = pad(prompt_mask, padding_value=0, padding_side="left") + + completion_ids = [ + torch.tensor(ids, device=device) for ids in completion_ids_list + ] + completion_mask = [ + torch.ones_like(ids, dtype=torch.long) for ids in completion_ids + ] + completion_ids = pad( + completion_ids, padding_value=self.pad_token_id, padding_side="right" + ) + completion_mask = pad(completion_mask, padding_value=0, padding_side="right") + + if sampling_per_token_logps_list is not None: + sampling_logps = [ + torch.tensor(lp, device=device) for lp in sampling_per_token_logps_list + ] + sampling_per_token_logps = pad( + sampling_logps, padding_value=0.0, padding_side="right" + ) + else: + sampling_per_token_logps = None + + if tool_mask_list is not None: + tool_mask = [torch.tensor(m, device=device) for m in tool_mask_list] + tool_mask = pad(tool_mask, padding_value=1, padding_side="right") + else: + tool_mask = None + + # --- Mask truncated completions --- + if self.mask_truncated_completions: + eos_and_pad = [self.eos_token_id, self.pad_token_id] + is_trunc = torch.tensor( + [ids[-1] not in eos_and_pad for ids in completion_ids_list], + device=device, + ) + completion_mask = completion_mask * (~is_trunc).unsqueeze(1).int() + if tool_mask is not None: + tool_mask = tool_mask * (~is_trunc).unsqueeze(1).int() + + # --- Multimodal forward kwargs --- + num_images = [len(il) for il in images] if images is not None else None + if images is not None: + prompts_text = [ + apply_chat_template( + {"prompt": p}, + self.processing_class, + tools=self.tools, + **self.chat_template_kwargs, + )["prompt"] + for p in prompts + ] + prompt_inputs = self.processing_class( + images=images, text=prompts_text, padding=True, return_tensors="pt" + ) + forward_kwargs = { + k: v.to(device) if isinstance(v, torch.Tensor) else v + for k, v in prompt_inputs.items() + if k not in ("input_ids", "attention_mask") + } + else: + forward_kwargs = {} + + # Extend token_type_ids / mm_token_type_ids for completion tokens + for ttid_key in ("token_type_ids", "mm_token_type_ids"): + if ttid_key in forward_kwargs: + tt = forward_kwargs[ttid_key] + forward_kwargs[ttid_key] = torch.cat( + [tt, tt.new_zeros(completion_ids.shape)], dim=1 + ) + + # Merge extra_fields from rollout_func into inputs + if extra_fields: + for i, inp in enumerate(inputs): + for key, values in extra_fields.items(): + if isinstance(values, list) and i < len(values): + inp[key] = values[i] + elif not isinstance(values, list): + inp[key] = values + + # No explicit CUDA sync needed here — both threads share the + # default stream, so operations are naturally ordered. + + # --- Construct deferred output --- + output = { + "prompt_ids": prompt_ids, + "prompt_mask": prompt_mask, + "completion_ids": completion_ids, + "completion_mask": completion_mask, + "num_items_in_batch": num_items_in_batch, + "advantages": torch.zeros(completion_ids.size(0), device=device), + # Sentinels for deferred scoring + "_pending_policy_logps": True, + "_deferred_inputs": inputs, + "_deferred_prompts": prompts, + "_deferred_completions": completions, + "_deferred_completion_ids_list": completion_ids_list, + "_rank0_only": rank0_only, + } + if sampling_per_token_logps is not None: + output["sampling_per_token_logps"] = sampling_per_token_logps + if tool_mask is not None: + output["tool_mask"] = tool_mask + if images is not None: + output["num_images"] = num_images + for k in ( + "pixel_values", + "image_grid_thw", + "pixel_attention_mask", + "image_sizes", + "token_type_ids", + "mm_token_type_ids", + ): + if k in forward_kwargs: + output[k] = forward_kwargs[k] + return output + + # ------------------------------------------------------------------ + # Hooks (overridden by subclasses like FastAsyncGRPOTrainer) + # ------------------------------------------------------------------ + + def _compute_rewards_for_batch( + self, inputs, prompts, completions, completion_ids_list + ): + """Compute rewards for a batch. Override for parallel workers, caching, etc.""" + return self._calculate_rewards( + inputs, prompts, completions, completion_ids_list + ) + + def _launch_reward_workers(self, inputs, prompts, completions, completion_ids_list): + """Launch reward computation in background. Override for parallel dispatch. + + Default: no-op (rewards computed synchronously in _collect_reward_workers). + """ + self._pending_reward_args = (inputs, prompts, completions, completion_ids_list) + + def _collect_reward_workers( + self, inputs, prompts, completions, completion_ids_list + ): + """Collect reward results. Override to collect from parallel workers. + + Default: compute rewards synchronously now. + """ + args = getattr(self, "_pending_reward_args", None) + if args is not None: + self._pending_reward_args = None + return self._compute_rewards_for_batch(*args) + return self._compute_rewards_for_batch( + inputs, prompts, completions, completion_ids_list + ) + + def _post_advantage_hook( + self, + data: dict, + rewards_per_func, + advantages, + inputs: list, + num_generations: int, + mode: str, + s_start: int | None = None, + s_end: int | None = None, + is_last_chunk: bool = True, + ) -> None: + """Called after advantages are computed. Override for replay buffer, re-roll, etc.""" + + # ------------------------------------------------------------------ + # Main-thread scoring + # ------------------------------------------------------------------ + + @torch.no_grad() + def _compute_deferred_scores(self, rollout: dict) -> dict: + """Compute rewards, advantages, policy logprobs, and IS ratio on the main thread. + + Takes the deferred output from ``_generate_only`` and produces a fully + scored dict ready for ``split_tensor_dict`` → micro-batches. + """ + device = self.accelerator.device + batch_size = self.args.per_device_train_batch_size + num_generations = self.num_generations + mode = "train" + + # --- Extract deferred data --- + data = rollout + inputs = data.pop("_deferred_inputs") + prompts = data.pop("_deferred_prompts") + completions = data.pop("_deferred_completions") + completion_ids_list = data.pop("_deferred_completion_ids_list") + rank0_only = data.pop("_rank0_only", False) + del data["_pending_policy_logps"] + + prompt_ids = data["prompt_ids"] + completion_ids = data["completion_ids"] + prompt_mask = data["prompt_mask"] + completion_mask = data["completion_mask"] + prompt_completion_ids = torch.cat([prompt_ids, completion_ids], dim=1) + attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) + logits_to_keep = completion_ids.size(1) + + # Multimodal forward kwargs + forward_kwargs = {} + for key in ( + "pixel_values", + "image_grid_thw", + "pixel_attention_mask", + "image_sizes", + "token_type_ids", + "mm_token_type_ids", + ): + if key in data: + forward_kwargs[key] = data[key] + num_images = data.get("num_images") + + # --- Launch rewards in parallel with logprobs --- + self._launch_reward_workers(inputs, prompts, completions, completion_ids_list) + + # --- Policy logprobs --- + logprob_batch_size = min(batch_size * 4, len(prompt_ids)) + with disable_gradient_checkpointing( + self.model, self.args.gradient_checkpointing_kwargs + ): + generate_every = self.args.steps_per_generation * self.num_iterations + if self.args.gradient_accumulation_steps % generate_every != 0 or ( + self.use_vllm + and getattr(self, "vllm_importance_sampling_correction", False) + ): + old_per_token_logps, _ = self._get_per_token_logps_and_entropies( + self.model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + logprob_batch_size, + num_images=num_images, + **forward_kwargs, + ) + data["old_per_token_logps"] = old_per_token_logps + else: + old_per_token_logps = None + + # Reference model logprobs + if self.beta != 0.0: + if self.ref_model is not None: + ref_logps, _ = self._get_per_token_logps_and_entropies( + self.ref_model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + batch_size, + num_images=num_images, + **forward_kwargs, + ) + else: + unwrapped = self.accelerator.unwrap_model(self.model) + adapter_name = ( + "ref" + if hasattr(unwrapped, "peft_config") + and "ref" in unwrapped.peft_config + else None + ) + with use_adapter(unwrapped, adapter_name=adapter_name): + ref_logps, _ = self._get_per_token_logps_and_entropies( + self.model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + batch_size, + num_images=num_images, + **forward_kwargs, + ) + data["ref_per_token_logps"] = ref_logps + + # --- IS ratio --- + if ( + self.use_vllm + and getattr(self, "vllm_importance_sampling_correction", False) + and old_per_token_logps is not None + and "sampling_per_token_logps" in data + ): + sampling_logps = data["sampling_per_token_logps"] + is_mask = ( + completion_mask + if "tool_mask" not in data + else completion_mask * data["tool_mask"] + ) + per_token_logps_diff = (old_per_token_logps - sampling_logps) * is_mask + + is_mode = getattr(self, "vllm_importance_sampling_mode", "token_truncate") + is_cap = getattr(self, "vllm_importance_sampling_cap", 3.0) + sequence_level_is = is_mode in ("sequence_mask", "sequence_truncate") + if sequence_level_is: + logps_diff = per_token_logps_diff.sum(dim=-1, keepdim=True) + else: + logps_diff = per_token_logps_diff + + is_ratio = torch.exp(logps_diff) + if is_mode in ("sequence_truncate", "token_truncate"): + is_ratio = torch.clamp(is_ratio, max=is_cap) + elif is_mode in ("sequence_mask", "token_mask"): + is_ratio = is_ratio.masked_fill(is_ratio > is_cap, value=0.0) + data["importance_sampling_ratio"] = is_ratio + + # --- Collect rewards (launched before logprobs, should be done) --- + rewards_per_func = self._collect_reward_workers( + inputs, prompts, completions, completion_ids_list + ) + # In rank0_only mode, all ranks compute the same rewards on identical data. + # _calculate_rewards / _collect_reward_workers always `gather()` across ranks, + # which duplicates the rows (N_local * num_processes). De-duplicate so that + # rewards_per_func matches the data dict (which has N_local rows). + if rank0_only and rewards_per_func.size(0) > len(prompts): + rewards_per_func = rewards_per_func[: len(prompts)] + + # --- Advantages --- + if self.multi_objective_aggregation == "sum_then_normalize": + rewards = ( + rewards_per_func * self.reward_weights.to(device).unsqueeze(0) + ).nansum(dim=1) + mean_grouped = ( + rewards.view(-1, num_generations) + .mean(dim=1) + .repeat_interleave(num_generations) + ) + if self.scale_rewards in ("group", "none"): + if num_generations > 1: + std_rewards = ( + rewards.view(-1, num_generations) + .std(dim=1) + .repeat_interleave(num_generations) + ) + else: + std_rewards = torch.zeros_like(rewards) + elif self.scale_rewards == "batch": + std_rewards = ( + rewards.std().expand_as(rewards) + if rewards.numel() > 1 + else torch.zeros_like(rewards) + ) + else: + raise ValueError(f"Invalid scale_rewards: {self.scale_rewards}") + advantages = rewards - mean_grouped + if self.scale_rewards != "none": + advantages = advantages / (std_rewards + 1e-4) + is_std_zero = torch.isclose(std_rewards, torch.zeros_like(std_rewards)) + + elif self.multi_objective_aggregation == "normalize_then_sum": + grouped = rewards_per_func.view(-1, num_generations, len(self.reward_funcs)) + mean_k = torch.nanmean(grouped, dim=1, keepdim=True) + std_k = ( + nanstd(grouped, dim=1, keepdim=True) + if num_generations > 1 + else torch.zeros_like(mean_k) + ) + reward_k = (grouped - mean_k) / (std_k + 1e-4) + reward_k = reward_k.view(-1, len(self.reward_funcs)) + rewards = (reward_k * self.reward_weights.to(device).unsqueeze(0)).nansum( + dim=1 + ) + std_rewards = ( + rewards.std().expand_as(rewards) + if rewards.numel() > 1 + else torch.zeros_like(rewards) + ) + advantages = (rewards - rewards.mean()) / (std_rewards + 1e-4) + is_std_zero = torch.isclose(std_rewards, torch.zeros_like(std_rewards)) + else: + raise ValueError( + f"Invalid multi_objective_aggregation: {self.multi_objective_aggregation}" + ) + + # Slice for local process + # In rank0_only mode, all ranks already have identical data from broadcast, + # so no slicing needed. Otherwise, each rank takes its portion. + if rank0_only: + process_slice = slice(0, len(prompts)) + else: + process_slice = slice( + self.accelerator.process_index * len(prompts), + (self.accelerator.process_index + 1) * len(prompts), + ) + all_advantages = advantages.clone() + advantages = advantages[process_slice] + data["advantages"] = advantages + + # --- Post-advantage hook (for replay buffer, re-roll, etc.) --- + self._post_advantage_hook( + data, + rewards_per_func, + advantages, + inputs, + num_generations, + mode, + ) + + # --- Metrics --- + for i, name in enumerate(self.reward_func_names): + self._metrics[mode][f"rewards/{name}/mean"].append( + torch.nanmean(rewards_per_func[:, i]).item() + ) + self._metrics[mode][f"rewards/{name}/std"].append( + nanstd(rewards_per_func[:, i]).item() + ) + agg_rewards = rewards_per_func.nansum(dim=1) + self._metrics[mode]["reward"].append(agg_rewards.mean().item()) + self._metrics[mode]["reward_std"].append(agg_rewards.std().item()) + self._metrics[mode]["frac_reward_zero_std"].append( + is_std_zero.float().mean().item() + ) + + # Token counting + total_prompt = self.accelerator.gather(prompt_mask.sum()).sum() + total_completion = self.accelerator.gather(completion_mask.sum()).sum() + self.state.num_input_tokens_seen += (total_prompt + total_completion).item() + self._metrics[mode]["num_tokens"] = [self.state.num_input_tokens_seen] + + # Completion length metrics + comp_lengths = completion_mask.sum(dim=1) + agg_lengths = self.accelerator.gather(comp_lengths) + self._metrics[mode]["completions/mean_length"].append( + agg_lengths.float().mean().item() + ) + self._metrics[mode]["completions/min_length"].append( + agg_lengths.float().min().item() + ) + self._metrics[mode]["completions/max_length"].append( + agg_lengths.float().max().item() + ) + + eos_and_pad = [self.eos_token_id, self.pad_token_id] + is_trunc = torch.tensor( + [ids[-1].item() not in eos_and_pad for ids in completion_ids], device=device + ) + agg_trunc = self.accelerator.gather(is_trunc) + self._metrics[mode]["completions/clipped_ratio"].append( + agg_trunc.float().mean().item() + ) + term_lengths = agg_lengths[~agg_trunc] + if len(term_lengths) == 0: + term_lengths = torch.zeros(1, device=device) + self._metrics[mode]["completions/mean_terminated_length"].append( + term_lengths.float().mean().item() + ) + self._metrics[mode]["completions/min_terminated_length"].append( + term_lengths.float().min().item() + ) + self._metrics[mode]["completions/max_terminated_length"].append( + term_lengths.float().max().item() + ) + + # IS metrics + if "importance_sampling_ratio" in data and "sampling_per_token_logps" in data: + old_lp = data["old_per_token_logps"] + samp_lp = data["sampling_per_token_logps"] + mask = completion_mask.bool() + delta = torch.abs(old_lp - samp_lp) + delta_m = delta[mask] + md = ( + torch.mean(delta_m) + if delta_m.numel() > 0 + else torch.tensor(0.0, device=device) + ) + xd = ( + torch.max(delta_m) + if delta_m.numel() > 0 + else torch.tensor(0.0, device=device) + ) + self._metrics[mode]["sampling/sampling_logp_difference/mean"].append( + self.accelerator.gather(md).mean().item() + ) + self._metrics[mode]["sampling/sampling_logp_difference/max"].append( + self.accelerator.gather(xd).max().item() + ) + isr = data["importance_sampling_ratio"] + is_mode = getattr(self, "vllm_importance_sampling_mode", "token_truncate") + if is_mode in ("sequence_mask", "sequence_truncate"): + flat_isr = isr.flatten() + else: + flat_isr = isr[mask] + if flat_isr.numel() > 0: + self._metrics[mode]["sampling/importance_sampling_ratio/min"].append( + nanmin(self.accelerator.gather(torch.min(flat_isr))).item() + ) + self._metrics[mode]["sampling/importance_sampling_ratio/mean"].append( + self.accelerator.gather(torch.mean(flat_isr)).nanmean().item() + ) + self._metrics[mode]["sampling/importance_sampling_ratio/max"].append( + nanmax(self.accelerator.gather(torch.max(flat_isr))).item() + ) + + # Log prompt/completion texts + prompts_text = self.processing_class.batch_decode( + prompt_ids, skip_special_tokens=True + ) + completions_text = self.processing_class.batch_decode( + completion_ids, skip_special_tokens=True + ) + if gather_object is not None: + self._logs["prompt"].extend(gather_object(prompts_text)) + self._logs["completion"].extend(gather_object(completions_text)) + for i, name in enumerate(self.reward_func_names): + self._logs["rewards"][name].extend(rewards_per_func[:, i].tolist()) + self._logs["advantages"].extend(all_advantages.tolist()) + + # Remove deferred keys + for k in list(data.keys()): + if k.startswith("_deferred") or k == "_pending_policy_logps": + data.pop(k, None) + + return data + + @torch.no_grad() + def _compute_streaming_group_scores( + self, + data, + s_start, + s_end, + inputs, + prompts, + completions, + completion_ids_list, + is_last_chunk, + rank0_only=False, + ): + """Score a chunk of prompt groups: rewards, policy logprobs, advantages. + + Called during streaming scoring to incrementally score groups. + Writes results directly into ``data`` at positions ``s_start:s_end``. + """ + device = self.accelerator.device + batch_size = self.args.per_device_train_batch_size + num_generations = self.num_generations + mode = "train" + chunk_size = s_end - s_start + + # --- Policy logprobs for this chunk --- + chunk_prompt_ids = data["prompt_ids"][s_start:s_end] + chunk_completion_ids = data["completion_ids"][s_start:s_end] + chunk_prompt_mask = data["prompt_mask"][s_start:s_end] + chunk_completion_mask = data["completion_mask"][s_start:s_end] + prompt_completion_ids = torch.cat( + [chunk_prompt_ids, chunk_completion_ids], dim=1 + ) + attention_mask = torch.cat([chunk_prompt_mask, chunk_completion_mask], dim=1) + logits_to_keep = chunk_completion_ids.size(1) + + # Slice multimodal forward kwargs for this chunk + forward_kwargs = {} + for key in ( + "pixel_values", + "image_grid_thw", + "pixel_attention_mask", + "image_sizes", + "token_type_ids", + "mm_token_type_ids", + ): + if key in data: + val = data[key] + if ( + isinstance(val, torch.Tensor) + and val.dim() > 0 + and val.size(0) == len(data["prompt_ids"]) + ): + forward_kwargs[key] = val[s_start:s_end] + else: + forward_kwargs[key] = val + num_images = data.get("num_images") + if ( + num_images is not None + and hasattr(num_images, "__getitem__") + and len(num_images) == len(data["prompt_ids"]) + ): + num_images = num_images[s_start:s_end] + + # --- Launch rewards in parallel with logprobs --- + self._launch_reward_workers(inputs, prompts, completions, completion_ids_list) + + # --- Policy logprobs for this chunk (GPU, overlaps with BG rewards) --- + logprob_batch_size = min(batch_size * 2, chunk_size) + with disable_gradient_checkpointing( + self.model, self.args.gradient_checkpointing_kwargs + ): + generate_every = self.args.steps_per_generation * self.num_iterations + if self.args.gradient_accumulation_steps % generate_every != 0 or ( + self.use_vllm + and getattr(self, "vllm_importance_sampling_correction", False) + ): + old_logps, _ = self._get_per_token_logps_and_entropies( + self.model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + logprob_batch_size, + num_images=num_images, + **forward_kwargs, + ) + if "old_per_token_logps" not in data: + total = len(data["prompt_ids"]) + data["old_per_token_logps"] = torch.zeros( + total, old_logps.size(1), device=device, dtype=old_logps.dtype + ) + data["old_per_token_logps"][s_start:s_end] = old_logps + + # Compute IS ratio for this chunk + if "sampling_per_token_logps" in data: + samp_chunk = data["sampling_per_token_logps"][s_start:s_end] + is_mask = ( + chunk_completion_mask + if "tool_mask" not in data + else (chunk_completion_mask * data["tool_mask"][s_start:s_end]) + ) + diff = (old_logps - samp_chunk) * is_mask + is_mode = getattr( + self, "vllm_importance_sampling_mode", "token_truncate" + ) + is_cap = getattr(self, "vllm_importance_sampling_cap", 3.0) + seq_is = is_mode in ("sequence_mask", "sequence_truncate") + logps_diff = diff.sum(dim=-1, keepdim=True) if seq_is else diff + is_ratio = torch.exp(logps_diff) + if is_mode in ("sequence_truncate", "token_truncate"): + is_ratio = torch.clamp(is_ratio, max=is_cap) + elif is_mode in ("sequence_mask", "token_mask"): + is_ratio = is_ratio.masked_fill(is_ratio > is_cap, value=0.0) + if "importance_sampling_ratio" not in data: + total = len(data["prompt_ids"]) + shape = (total, 1) if seq_is else (total, is_ratio.size(1)) + data["importance_sampling_ratio"] = torch.ones( + *shape, device=device, dtype=is_ratio.dtype + ) + data["importance_sampling_ratio"][s_start:s_end] = is_ratio + + # Reference logprobs + if self.beta != 0.0: + if self.ref_model is not None: + ref_logps, _ = self._get_per_token_logps_and_entropies( + self.ref_model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + batch_size, + num_images=num_images, + **forward_kwargs, + ) + else: + unwrapped = self.accelerator.unwrap_model(self.model) + adapter_name = ( + "ref" + if hasattr(unwrapped, "peft_config") + and "ref" in unwrapped.peft_config + else None + ) + with use_adapter(unwrapped, adapter_name=adapter_name): + ref_logps, _ = self._get_per_token_logps_and_entropies( + self.model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + batch_size, + num_images=num_images, + **forward_kwargs, + ) + if "ref_per_token_logps" not in data: + total = len(data["prompt_ids"]) + data["ref_per_token_logps"] = torch.zeros( + total, ref_logps.size(1), device=device, dtype=ref_logps.dtype + ) + data["ref_per_token_logps"][s_start:s_end] = ref_logps + + # --- Collect rewards (should already be done, ran in parallel with logprobs) --- + rewards_per_func = self._collect_reward_workers( + inputs, prompts, completions, completion_ids_list + ) + # De-duplicate gathered rewards when all ranks computed the same data. + # _calculate_rewards always gather()s, which duplicates rows in rank0_only mode. + if rewards_per_func.size(0) > chunk_size: + rewards_per_func = rewards_per_func[:chunk_size] + + # --- Advantages (group-level normalization) --- + if self.multi_objective_aggregation == "sum_then_normalize": + rewards = ( + rewards_per_func * self.reward_weights.to(device).unsqueeze(0) + ).nansum(dim=1) + mean_g = ( + rewards.view(-1, num_generations) + .mean(dim=1) + .repeat_interleave(num_generations) + ) + if num_generations > 1: + std_r = ( + rewards.view(-1, num_generations) + .std(dim=1) + .repeat_interleave(num_generations) + ) + else: + std_r = torch.zeros_like(rewards) + advantages = rewards - mean_g + if self.scale_rewards != "none": + advantages = advantages / (std_r + 1e-4) + is_std_zero = torch.isclose(std_r, torch.zeros_like(std_r)) + + elif self.multi_objective_aggregation == "normalize_then_sum": + grouped = rewards_per_func.view(-1, num_generations, len(self.reward_funcs)) + mean_k = torch.nanmean(grouped, dim=1, keepdim=True) + std_k = ( + nanstd(grouped, dim=1, keepdim=True) + if num_generations > 1 + else torch.zeros_like(mean_k) + ) + reward_k = ((grouped - mean_k) / (std_k + 1e-4)).view( + -1, len(self.reward_funcs) + ) + rewards = (reward_k * self.reward_weights.to(device).unsqueeze(0)).nansum( + dim=1 + ) + std_r = ( + rewards.view(-1, num_generations) + .std(dim=1) + .repeat_interleave(num_generations) + ) + mean_r = ( + rewards.view(-1, num_generations) + .mean(dim=1) + .repeat_interleave(num_generations) + ) + advantages = (rewards - mean_r) / (std_r + 1e-4) + is_std_zero = torch.isclose(std_r, torch.zeros_like(std_r)) + else: + raise ValueError( + f"Invalid multi_objective_aggregation: {self.multi_objective_aggregation}" + ) + + if rank0_only: + process_slice = slice(0, len(prompts)) + else: + process_slice = slice( + self.accelerator.process_index * len(prompts), + (self.accelerator.process_index + 1) * len(prompts), + ) + advantages = advantages[process_slice] + + if "advantages" not in data or not isinstance(data["advantages"], torch.Tensor): + data["advantages"] = torch.zeros(len(data["prompt_ids"]), device=device) + data["advantages"][s_start:s_end] = advantages + + # --- Post-advantage hook (for replay buffer, re-roll, etc.) --- + self._post_advantage_hook( + data, + rewards_per_func, + advantages, + inputs, + num_generations, + mode, + s_start=s_start, + s_end=s_end, + is_last_chunk=is_last_chunk, + ) + + # --- Chunk metrics --- + for i, name in enumerate(self.reward_func_names): + self._metrics[mode][f"rewards/{name}/mean"].append( + torch.nanmean(rewards_per_func[:, i]).item() + ) + self._metrics[mode][f"rewards/{name}/std"].append( + nanstd(rewards_per_func[:, i]).item() + ) + agg_rewards = rewards_per_func.nansum(dim=1) + self._metrics[mode]["reward"].append(agg_rewards.mean().item()) + self._metrics[mode]["reward_std"].append(agg_rewards.std().item()) + self._metrics[mode]["frac_reward_zero_std"].append( + is_std_zero.float().mean().item() + ) + + # --- Full-batch metrics on last chunk --- + if is_last_chunk: + all_prompt_mask = data["prompt_mask"] + all_completion_mask = data["completion_mask"] + all_completion_ids = data["completion_ids"] + total_p = self.accelerator.gather(all_prompt_mask.sum()).sum() + total_c = self.accelerator.gather(all_completion_mask.sum()).sum() + self.state.num_input_tokens_seen += (total_p + total_c).item() + self._metrics[mode]["num_tokens"] = [self.state.num_input_tokens_seen] + + comp_lengths = all_completion_mask.sum(dim=1) + agg_lengths = self.accelerator.gather(comp_lengths) + self._metrics[mode]["completions/mean_length"].append( + agg_lengths.float().mean().item() + ) + self._metrics[mode]["completions/min_length"].append( + agg_lengths.float().min().item() + ) + self._metrics[mode]["completions/max_length"].append( + agg_lengths.float().max().item() + ) + + eos_and_pad = [self.eos_token_id, self.pad_token_id] + is_trunc = torch.tensor( + [ids[-1].item() not in eos_and_pad for ids in all_completion_ids], + device=device, + ) + agg_trunc = self.accelerator.gather(is_trunc) + self._metrics[mode]["completions/clipped_ratio"].append( + agg_trunc.float().mean().item() + ) + term = agg_lengths[~agg_trunc] + if len(term) == 0: + term = torch.zeros(1, device=device) + self._metrics[mode]["completions/mean_terminated_length"].append( + term.float().mean().item() + ) + self._metrics[mode]["completions/min_terminated_length"].append( + term.float().min().item() + ) + self._metrics[mode]["completions/max_terminated_length"].append( + term.float().max().item() + ) + + # IS metrics + if ( + self.use_vllm + and getattr(self, "vllm_importance_sampling_correction", False) + and "sampling_per_token_logps" in data + and "old_per_token_logps" in data + ): + old_lp = data["old_per_token_logps"] + samp_lp = data["sampling_per_token_logps"] + mask = all_completion_mask.bool() + delta = torch.abs(old_lp - samp_lp)[mask] + md = ( + torch.mean(delta) + if delta.numel() > 0 + else torch.tensor(0.0, device=device) + ) + xd = ( + torch.max(delta) + if delta.numel() > 0 + else torch.tensor(0.0, device=device) + ) + self._metrics[mode]["sampling/sampling_logp_difference/mean"].append( + self.accelerator.gather(md).mean().item() + ) + self._metrics[mode]["sampling/sampling_logp_difference/max"].append( + self.accelerator.gather(xd).max().item() + ) + is_mode = getattr( + self, "vllm_importance_sampling_mode", "token_truncate" + ) + isr = data["importance_sampling_ratio"] + flat = ( + isr.flatten() + if is_mode in ("sequence_mask", "sequence_truncate") + else isr[mask] + ) + if flat.numel() > 0: + self._metrics[mode][ + "sampling/importance_sampling_ratio/min" + ].append(nanmin(self.accelerator.gather(torch.min(flat))).item()) + self._metrics[mode][ + "sampling/importance_sampling_ratio/mean" + ].append(self.accelerator.gather(torch.mean(flat)).nanmean().item()) + self._metrics[mode][ + "sampling/importance_sampling_ratio/max" + ].append(nanmax(self.accelerator.gather(torch.max(flat))).item()) + + def _score_streaming(self, rollout: dict) -> list[dict]: + """Score a rollout using streaming group scoring. Returns list of micro-batches.""" + data = rollout + num_gen = self.num_generations + n_groups = len(data["prompt_ids"]) // num_gen + batch_size = self.args.per_device_train_batch_size + min_groups = max(1, self.args.streaming_min_groups) + + # Extract deferred data + inputs = data.pop("_deferred_inputs") + prompts = data.pop("_deferred_prompts") + completions = data.pop("_deferred_completions") + completion_ids_list = data.pop("_deferred_completion_ids_list") + rank0_only = data.pop("_rank0_only", False) + del data["_pending_policy_logps"] + + all_micro_batches = [] + shared_keys = {"num_items_in_batch"} + + for chunk_start_g in range(0, n_groups, min_groups): + chunk_end_g = min(chunk_start_g + min_groups, n_groups) + s_start = chunk_start_g * num_gen + s_end = chunk_end_g * num_gen + + self._compute_streaming_group_scores( + data=data, + s_start=s_start, + s_end=s_end, + inputs=inputs[s_start:s_end], + prompts=prompts[s_start:s_end], + completions=completions[s_start:s_end], + completion_ids_list=completion_ids_list[s_start:s_end], + is_last_chunk=(chunk_end_g == n_groups), + rank0_only=rank0_only, + ) + + # Yield micro-batches from this scored chunk + chunk_size = s_end - s_start + perm = torch.randperm(chunk_size) + for mb_off in range(0, chunk_size, batch_size): + mb_idx = perm[mb_off : mb_off + batch_size] + abs_idx = mb_idx + s_start + mb = {} + for key in data: + if key.startswith("_"): + continue + val = data[key] + if key in shared_keys: + mb[key] = val + elif isinstance(val, torch.Tensor) and val.dim() > 0: + mb[key] = val[abs_idx] + else: + mb[key] = val + all_micro_batches.append(mb) + + # Repeat for num_iterations + return all_micro_batches * self.num_iterations + + # ------------------------------------------------------------------ + # _prepare_inputs override + # ------------------------------------------------------------------ + + def _prepare_inputs(self, generation_batch): + """Override to support data producer and async prefetch paths.""" + mode = "train" if self.model.training else "eval" + + # --- Data producer path --- + if mode == "train" and self.data_producer is not None: + return self._prepare_inputs_data_producer(generation_batch) + + # --- Legacy async prefetch path (no data producer) --- + if mode == "train" and self.args.async_prefetch: + return self._prepare_inputs_legacy_async(generation_batch) + + # --- Stock path --- + return super()._prepare_inputs(generation_batch) + + def _prepare_inputs_data_producer(self, generation_batch): + """Data producer path: produce rollout, score deferred logps, split into micro-batches.""" + # Return from buffer if available + if self._buffered_inputs: + return self._buffered_inputs.pop(0) + + # Produce a new rollout + self._maybe_sync_vllm_weights() + + rollout_dataset = self.data_producer.produce( + self.model, + self.state.global_step, + processing_class=self.processing_class, + accelerator=self.accelerator, + args=self.args, + ) + + # Convert RolloutDataset back to a dict for scoring/splitting + rollout = rollout_dataset._data + + # If async (skip_policy_logps=True), score deferred logps on main thread + if rollout.get("_pending_policy_logps"): + if self.args.streaming_partial_batch: + micro_batches = self._score_streaming(rollout) + else: + scored = self._compute_deferred_scores(rollout) + scored = split_pixel_values_by_grid(scored) + scored = shuffle_sequence_dict(scored) + batches = split_tensor_dict(scored, self.args.steps_per_generation) + micro_batches = [unsplit_pixel_values_by_grid(b) for b in batches] + micro_batches = micro_batches * self.num_iterations + else: + # Sync path: data is already fully scored + rollout = split_pixel_values_by_grid(rollout) + batches = split_tensor_dict(rollout, self.args.steps_per_generation) + micro_batches = [unsplit_pixel_values_by_grid(b) for b in batches] + micro_batches = micro_batches * self.num_iterations + + self._buffered_inputs = micro_batches[1:] + return micro_batches[0] + + def _prepare_inputs_legacy_async(self, generation_batch): + """Legacy async path: direct queue-based prefetch without data producer.""" + # Return from buffer if available + if self._buffered_inputs: + return self._buffered_inputs.pop(0) + + # Need a new rollout + self._maybe_sync_vllm_weights() + future = self._async_queue.get() + rollout = future.result() + self._submit_generation() + + if self.args.streaming_partial_batch: + micro_batches = self._score_streaming(rollout) + else: + scored = self._compute_deferred_scores(rollout) + scored = split_pixel_values_by_grid(scored) + scored = shuffle_sequence_dict(scored) + batches = split_tensor_dict(scored, self.args.steps_per_generation) + micro_batches = [unsplit_pixel_values_by_grid(b) for b in batches] + micro_batches = micro_batches * self.num_iterations + + self._buffered_inputs = micro_batches[1:] + + # Release cached CUDA memory from scoring + # before training allocations begin, reducing peak reserved memory. + torch.cuda.empty_cache() + + return micro_batches[0] + + @profiling_decorator + def _get_per_token_logps_and_entropies( + self, + model, + input_ids, + attention_mask, + logits_to_keep, + batch_size=None, + compute_entropy=False, + pixel_values=None, + image_grid_thw=None, + num_images=None, + pixel_attention_mask=None, + image_sizes=None, + token_type_ids=None, + mm_token_type_ids=None, + ) -> tuple[Any, torch.Tensor | None]: + """Compute log-probs and (optionally) entropies for each token. + + When running under no_grad (scoring path), bypasses accelerate's + ConvertOutputsToFp32 wrapper to avoid a fp32 copy of the + logits tensor. + """ + # Bypass accelerate's ConvertOutputsToFp32 wrapper which converts the + # entire (B, L, V) logits tensor from bf16 to fp32 — unnecessary and + # extremely wasteful for large vocabularies. + # Skip unwrapping for FSDP — parameters are only valid inside FSDP's + # forward context; unwrapping exposes flattened/sharded tensors. + if not self.is_fsdp_enabled: + model = self.accelerator.unwrap_model(model, keep_fp32_wrapper=False) + autocast_ctx = torch.autocast( + device_type=input_ids.device.type, dtype=torch.bfloat16 + ) + + # Use Liger's Triton kernel in scoring path (no grad): fuses + # temperature + log_softmax + gather into a single kernel pass. + use_fused = ( + self.use_liger_kernel + and _fused_selective_log_softmax is not None + and not torch.is_grad_enabled() + ) + + batch_size = batch_size or input_ids.size(0) + all_logps = [] + all_entropies = [] + with autocast_ctx: + for start in range(0, input_ids.size(0), batch_size): + input_ids_batch = input_ids[start : start + batch_size] + attention_mask_batch = attention_mask[start : start + batch_size] + + # Build model inputs + model_inputs = { + "input_ids": input_ids_batch, + "attention_mask": attention_mask_batch, + } + if image_grid_thw is not None and pixel_values is not None: + rows_per_image = image_grid_thw.prod(dim=-1) + rows_per_sample = torch.split(rows_per_image, num_images) + rows_per_sample = torch.stack([s.sum() for s in rows_per_sample]) + cum_rows = torch.cat( + [ + torch.tensor([0], device=rows_per_sample.device), + rows_per_sample.cumsum(0), + ] + ) + row_start, row_end = ( + cum_rows[start].item(), + cum_rows[start + batch_size].item(), + ) + model_inputs["pixel_values"] = pixel_values[row_start:row_end] + cum_imgs = torch.tensor([0] + num_images).cumsum(0) + img_start, img_end = cum_imgs[start], cum_imgs[start + batch_size] + model_inputs["image_grid_thw"] = image_grid_thw[img_start:img_end] + elif pixel_values is not None: + model_inputs["pixel_values"] = pixel_values[ + start : start + batch_size + ] + if pixel_attention_mask is not None: + model_inputs["pixel_attention_mask"] = pixel_attention_mask[ + start : start + batch_size + ] + if image_sizes is not None: + model_inputs["image_sizes"] = image_sizes[ + start : start + batch_size + ] + if token_type_ids is not None: + model_inputs["token_type_ids"] = token_type_ids[ + start : start + batch_size + ] + if mm_token_type_ids is not None: + model_inputs["mm_token_type_ids"] = mm_token_type_ids[ + start : start + batch_size + ] + + if "logits_to_keep" in self.model_kwarg_keys: + model_inputs["logits_to_keep"] = logits_to_keep + 1 + + model_inputs["use_cache"] = False + + logits = model(**model_inputs).logits + completion_ids = input_ids_batch[:, -logits_to_keep:] + # FP8 models produce NaN logits at positions where + # attention_mask=0 (padding). Replace NaN with 0 so + # log_softmax yields uniform distribution for those positions. + # The completion_mask ensures these don't affect the loss. + logits = torch.nan_to_num(logits, nan=0.0) + + if use_fused: + logits = logits[:, -(logits_to_keep + 1) :, :] + if not logits.is_contiguous(): + logits = logits.contiguous() + logps = _fused_selective_log_softmax( + logits, completion_ids, self.temperature + ) + all_logps.append(logps) + else: + logits = logits[:, :-1, :] + logits = logits[:, -logits_to_keep:, :] + logits.div_(self.temperature) + logps = selective_log_softmax(logits, completion_ids) + all_logps.append(logps) + + if compute_entropy: + with torch.no_grad(): + entropies = entropy_from_logits(logits) + all_entropies.append(entropies) + + logps = torch.cat(all_logps, dim=0) + entropies = torch.cat(all_entropies, dim=0) if compute_entropy else None + return logps, entropies + + # ------------------------------------------------------------------ + # Loss override (adds IS ratio + OPSM) + # ------------------------------------------------------------------ + + @staticmethod + def get_off_policy_mask( + advantages, + per_token_logps, + sampling_per_token_logps, + mask, + off_policy_threshold, + ): + """OPSM from DeepSeek-V3.2: drop sequences with negative advantage + high KL.""" + kl_div = sampling_per_token_logps - per_token_logps.detach() + seq_kl = (kl_div * mask).sum(dim=1, keepdim=True) / mask.sum( + dim=1, keepdim=True + ).clamp(min=1.0) + is_pos_adv = advantages >= 0 + is_low_kl = seq_kl <= off_policy_threshold + return (is_pos_adv | is_low_kl).to(dtype=mask.dtype) + + def _compute_loss(self, model, inputs): + """Override to add IS ratio correction and off-policy sequence masking.""" + prompt_ids, prompt_mask = inputs["prompt_ids"], inputs["prompt_mask"] + completion_ids, completion_mask = ( + inputs["completion_ids"], + inputs["completion_mask"], + ) + input_ids = torch.cat([prompt_ids, completion_ids], dim=1) + attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) + logits_to_keep = completion_ids.size(1) + mask = ( + completion_mask + if "tool_mask" not in inputs + else completion_mask * inputs["tool_mask"] + ) + + per_token_logps, entropies = self._get_per_token_logps_and_entropies( + model, + input_ids, + attention_mask, + logits_to_keep, + compute_entropy=True, + pixel_values=inputs.get("pixel_values"), + image_grid_thw=inputs.get("image_grid_thw"), + num_images=inputs.get("num_images"), + pixel_attention_mask=inputs.get("pixel_attention_mask"), + image_sizes=inputs.get("image_sizes"), + token_type_ids=inputs.get("token_type_ids"), + mm_token_type_ids=inputs.get("mm_token_type_ids"), + ) + if self.top_entropy_quantile < 1.0: + entropy_mask = self.get_high_entropy_mask( + entropies, mask, 1 - self.top_entropy_quantile + ) + else: + entropy_mask = None + + advantages = inputs["advantages"] + if advantages.dim() == 1: + advantages = advantages.unsqueeze(1) + + old_per_token_logps = inputs.get("old_per_token_logps") + old_per_token_logps = ( + per_token_logps.detach() + if old_per_token_logps is None + else old_per_token_logps + ) + + # --- OPSM (off-policy sequence mask) --- + off_policy_mask = None + if getattr(self, "off_policy_mask_threshold", None) is not None: + sampling_per_token_logps = inputs.get( + "sampling_per_token_logps", old_per_token_logps + ) + off_policy_mask = self.get_off_policy_mask( + advantages=advantages, + per_token_logps=per_token_logps, + sampling_per_token_logps=sampling_per_token_logps, + mask=mask, + off_policy_threshold=self.off_policy_mask_threshold, + ) + + # --- Importance weights --- + log_ratio = per_token_logps - old_per_token_logps + is_level = getattr( + self, + "importance_sampling_level", + getattr(self.args, "importance_sampling_level", "token"), + ) + if is_level == "token": + log_importance_weights = log_ratio + elif is_level == "sequence": + log_importance_weights = (log_ratio * mask).sum(-1) / mask.sum(-1).clamp( + min=1.0 + ) + log_importance_weights = log_importance_weights.unsqueeze(-1) + else: + raise ValueError(f"Unknown importance sampling level: {is_level}") + + coef_1 = torch.exp(log_importance_weights) + + # --- KL divergence --- + if self.beta != 0.0: + ref_per_token_logps = inputs["ref_per_token_logps"] + per_token_kl = ( + torch.exp(ref_per_token_logps - per_token_logps) + - (ref_per_token_logps - per_token_logps) + - 1 + ) + if getattr(self.args, "use_bias_correction_kl", False): + per_token_kl = per_token_kl * coef_1 + + # --- Per-token loss --- + if self.loss_type == "cispo": + clamped = torch.clamp(coef_1, max=self.epsilon_high).detach() + per_token_loss = -clamped * advantages * per_token_logps + elif self.loss_type in ("grpo", "bnpo", "dr_grpo", "dapo", "luspo"): + coef_2 = torch.clamp(coef_1, 1 - self.epsilon_low, 1 + self.epsilon_high) + if self.args.delta is not None: + coef_1_c = torch.clamp(coef_1, max=self.args.delta) + else: + coef_1_c = coef_1 + per_token_loss = -torch.min(coef_1_c * advantages, coef_2 * advantages) + elif self.loss_type == "sapo": + temps = torch.where( + advantages > 0, + self.args.sapo_temperature_pos, + self.args.sapo_temperature_neg, + ) + soft = torch.sigmoid(temps * (coef_1 - 1)) * 4 / temps + per_token_loss = -soft * advantages + else: + raise ValueError(f"Unknown loss type: {self.loss_type}") + + # --- Apply masks --- + if off_policy_mask is not None: + per_token_loss = per_token_loss * off_policy_mask + if entropy_mask is not None: + per_token_loss = per_token_loss * entropy_mask + + # --- IS ratio correction (vLLM distribution mismatch) --- + if ( + self.use_vllm + and getattr(self, "vllm_importance_sampling_correction", False) + and "importance_sampling_ratio" in inputs + ): + per_token_loss = per_token_loss * inputs["importance_sampling_ratio"] + + if self.beta != 0.0: + per_token_loss = per_token_loss + self.beta * per_token_kl + + # --- Aggregate loss --- + mode = "train" if self.model.training else "eval" + normalizer = ( + self.current_gradient_accumulation_steps if mode == "train" else 1.0 + ) + + if self.loss_type in ("grpo", "sapo"): + loss = ( + (per_token_loss * mask).sum(-1) / mask.sum(-1).clamp(min=1.0) + ).mean() / normalizer + elif self.loss_type == "bnpo": + loss = ( + (per_token_loss * mask).sum() / mask.sum().clamp(min=1.0) / normalizer + ) + elif self.loss_type == "dr_grpo": + loss = ( + (per_token_loss * mask).sum() + / (per_token_loss.size(0) * self.max_completion_length) + / normalizer + ) + elif self.loss_type in ("cispo", "dapo"): + norm = inputs["num_items_in_batch"] / self.accelerator.num_processes + loss = (per_token_loss * mask).sum() / norm + elif self.loss_type == "luspo": + loss = (per_token_loss * mask.sum(1, keepdim=True)).mean() / normalizer + else: + raise ValueError(f"Unknown loss type: {self.loss_type}") + + # --- Metrics --- + completion_token_count = mask.sum().clamp(min=1.0) + + def masked_batch_mean(x): + return ( + x.mean() + if x.shape[1] == 1 + else (x * mask).sum() / completion_token_count + ) + + if self.beta != 0.0: + mean_kl = masked_batch_mean(per_token_kl) + self._metrics[mode]["kl"].append( + self.accelerator.gather(mean_kl).nanmean().item() + ) + + mean_entropy = masked_batch_mean(entropies) + self._metrics[mode]["entropy"].append( + self.accelerator.gather(mean_entropy).nanmean().item() + ) + + if self.loss_type in ("grpo", "bnpo", "dr_grpo", "dapo", "luspo"): + is_low = (coef_1 < 1 - self.epsilon_low) & (advantages < 0) + is_high = (coef_1 > 1 + self.epsilon_high) & (advantages > 0) + is_region = is_low | is_high + low_clip = masked_batch_mean(is_low.float()) + high_clip = masked_batch_mean(is_high.float()) + clip_ratio = masked_batch_mean(is_region.float()) + g_low = self.accelerator.gather(low_clip) + self._metrics[mode]["clip_ratio/low_mean"].append(g_low.nanmean().item()) + self._metrics[mode]["clip_ratio/low_min"].append(nanmin(g_low).item()) + g_high = self.accelerator.gather(high_clip) + self._metrics[mode]["clip_ratio/high_mean"].append(g_high.nanmean().item()) + self._metrics[mode]["clip_ratio/high_max"].append(nanmax(g_high).item()) + g_clip = self.accelerator.gather(clip_ratio) + self._metrics[mode]["clip_ratio/region_mean"].append( + g_clip.nanmean().item() + ) + elif self.loss_type == "cispo": + is_cispo = (coef_1 > self.epsilon_high) & (advantages > 0) + cr = masked_batch_mean(is_cispo.float()) + self._metrics[mode]["cispo_clip_ratio"].append( + self.accelerator.gather(cr).nanmean().item() + ) + + return loss diff --git a/src/axolotl/core/trainers/grpo/fast_async_trainer.py b/src/axolotl/core/trainers/grpo/fast_async_trainer.py new file mode 100644 index 0000000000..9d1128b973 --- /dev/null +++ b/src/axolotl/core/trainers/grpo/fast_async_trainer.py @@ -0,0 +1,768 @@ +# Copyright 2020-2026 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Experimental GRPO extensions: parallel reward workers, replay buffer, +deferred re-roll, and zero-advantage skipping. + +These features are built as subclasses of GRPOTrainer and GRPODataProducer, +using the hook system (_compute_rewards_for_batch, _post_advantage_hook, +_pre_produce_hook) defined in the base classes. +""" + +from __future__ import annotations + +import asyncio +import logging +import threading +from dataclasses import dataclass, field + +import torch +from torch import nn +from trl import GRPOTrainer + +from axolotl.core.trainers.grpo.async_trainer import ( + AsyncGRPOConfig, + AsyncGRPOTrainer, + GRPODataProducer, +) +from axolotl.core.trainers.grpo.replay_buffer import ReplayBuffer + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Extended config +# --------------------------------------------------------------------------- + + +@dataclass +class FastAsyncGRPOConfig(AsyncGRPOConfig): + """GRPOConfig with additional experimental parameters.""" + + reward_num_workers: int = field( + default=1, + metadata={ + "help": "Number of persistent subprocess workers for parallel reward computation. Each worker has its " + "own main thread so signal.alarm() (used by math_verify) works correctly. Work is sharded across " + "workers by prompt groups. Only used with use_data_producer=True and non-nn.Module reward functions." + }, + ) + replay_buffer_size: int = field( + default=0, + metadata={ + "help": "[Experimental, disabled by default] Size of the replay buffer for storing high-signal rollout " + "groups. When > 0, groups with reward variance are cached and used to replace zero-signal groups " + "(where all rewards are identical). Set to 0 to disable. Only used with use_data_producer=True." + }, + ) + replay_recompute_logps: bool = field( + default=True, + metadata={ + "help": "When True (default), recompute old_per_token_logps for replayed groups using the current " + "training model. This fixes the importance sampling mismatch that occurs when replaying stale data. " + "Only relevant when replay_buffer_size > 0." + }, + ) + reroll_start_fraction: float = field( + default=0.5, + metadata={ + "help": "Fraction of total training steps after which deferred re-rolling begins. Zero-signal prompts " + "(where all rewards in a group are identical) are buffered and re-injected into later batches when the " + "model is more likely to solve them. Set to 1.0 to disable. Only used with use_data_producer=True." + }, + ) + reroll_max_groups: int = field( + default=1, + metadata={ + "help": "Maximum number of prompt groups to replace with re-roll candidates per batch. Higher values " + "increase data utilization but reduce prompt diversity. Only used with use_data_producer=True." + }, + ) + skip_zero_advantage_batches: bool = field( + default=True, + metadata={ + "help": "When True, skip gradient computation for micro-batches where all advantages are zero (no learning " + "signal). This avoids the forward/backward pass entirely when no learning signal is present. The step is " + "logged with skipped_zero_adv_batches=1 for monitoring." + }, + ) + vllm_lora_sync: bool = field( + default=False, + metadata={ + "help": "When True, sync LoRA adapter weights to vLLM via filesystem instead of merging into base model " + "and NCCL-broadcasting all parameters. vLLM loads the adapter natively using Punica kernels. " + "Requires vllm_serve_lora serve module (auto-selected when this is True). " + "Syncs only LoRA adapter weights (much smaller) vs full merged model. Legacy merge behavior is used when False." + }, + ) + + +# --------------------------------------------------------------------------- +# Extended data producer with re-roll injection +# --------------------------------------------------------------------------- + + +class RerollDataProducer(GRPODataProducer): + """GRPODataProducer that injects re-roll candidates into prompt batches. + + Reads from the trainer's ``_reroll_buffer`` (populated by + ``GRPOExperimentalTrainer._post_advantage_hook``) and replaces the + last N prompt groups with previously-failed prompts. + """ + + def _pre_produce_hook(self, inputs: list, global_step: int) -> list: + trainer = self._trainer + reroll_buf = getattr(trainer, "_reroll_buffer", None) + reroll_lock = getattr(trainer, "_reroll_lock", None) + if reroll_buf is None or reroll_lock is None: + return inputs + + max_steps = getattr(trainer.args, "max_steps", -1) + start_frac = getattr(trainer.args, "reroll_start_fraction", 1.0) + max_groups = getattr(trainer.args, "reroll_max_groups", 1) + reroll_start_step = ( + max(1, int(max_steps * start_frac)) if max_steps > 0 else float("inf") + ) + + if global_step < reroll_start_step: + return inputs + + with reroll_lock: + n_to_take = min(max_groups, len(reroll_buf)) + reroll_prompts = [reroll_buf.pop(0) for _ in range(n_to_take)] + + if reroll_prompts: + num_gen = self._num_generations + n_groups = len(inputs) // num_gen + for i, reroll_prompt in enumerate(reroll_prompts): + group_idx = n_groups - 1 - i + if group_idx < 0: + break + start = group_idx * num_gen + for j in range(num_gen): + inputs[start + j] = reroll_prompt + logger.info( + f"[REROLL] Step {global_step}: replaced {len(reroll_prompts)}/{n_groups} prompt groups " + f"with deferred re-roll candidates ({len(reroll_buf)} remaining)" + ) + + return inputs + + +# --------------------------------------------------------------------------- +# Persistent reward subprocess pool +# --------------------------------------------------------------------------- + + +def _persistent_reward_worker(conn): + """Long-lived reward worker. Receives work items, returns results.""" + while True: + try: + msg = conn.recv() + except EOFError: + break + if msg is None: # Shutdown signal + break + ( + reward_funcs, + prompts, + completions, + completion_ids_list, + inputs, + reward_func_names, + ) = msg + try: + keys = [ + key + for key in inputs[0] + if key not in ["prompt", "completion", "completion_ids"] + ] + reward_kwargs = {key: [example[key] for example in inputs] for key in keys} + results = [] + for reward_func, _reward_func_name in zip( + reward_funcs, reward_func_names, strict=True + ): + output = reward_func( + prompts=prompts, + completions=completions, + completion_ids=completion_ids_list, + **reward_kwargs, + ) + results.append( + [float(r) if r is not None else float("nan") for r in output] + ) + conn.send(results) + except Exception: + conn.send(None) + + +# --------------------------------------------------------------------------- +# Extended trainer +# --------------------------------------------------------------------------- + + +class FastAsyncGRPOTrainer(AsyncGRPOTrainer): + """GRPOTrainer with experimental extensions. + + Adds: + - Parallel reward subprocess workers (``reward_num_workers``) + - Replay buffer for high-signal group reuse (``replay_buffer_size``) + - Deferred re-roll of failed prompts (``reroll_start_fraction``) + - Zero-advantage micro-batch skipping + """ + + def __init__(self, *args, **kwargs): + # These must be initialized before super().__init__() because + # _create_data_producer (called during super().__init__) needs them. + self._reroll_buffer: list = [] + self._reroll_lock = threading.Lock() + + # Temporarily suppress the base class's Liger + OPSM validation check, + # since this subclass supports it via a custom compute_liger_loss override. + grpo_args = kwargs.get("args") + if grpo_args is None: + for a in args: + if hasattr(a, "off_policy_mask_threshold"): + grpo_args = a + break + saved_threshold = None + if grpo_args is not None and getattr(grpo_args, "use_liger_kernel", False): + saved_threshold = grpo_args.off_policy_mask_threshold + grpo_args.off_policy_mask_threshold = None + + super().__init__(*args, **kwargs) + + if saved_threshold is not None: + grpo_args.off_policy_mask_threshold = saved_threshold + self.off_policy_mask_threshold = saved_threshold + + # Replay buffer + if getattr(self.args, "replay_buffer_size", 0) > 0: + self._replay_buffer = ReplayBuffer(max_size=self.args.replay_buffer_size) + else: + self._replay_buffer = None + self._replay_recompute_logps = getattr( + self.args, "replay_recompute_logps", True + ) + + # Reward worker pool (lazy-initialized) + self._reward_workers = None + + # -- Factory override: use RerollDataProducer ---------------------------- + + def _create_data_producer(self, args, train_dataset): + """Override to use RerollDataProducer for re-roll prompt injection.""" + from axolotl.core.trainers.grpo.async_trainer import ( + AsyncDataProducer, + ProducerConfig, + ) + + producer_config = ProducerConfig( + mini_epochs=args.num_iterations, + max_rollouts=None, + eval_during_produce=False, + empty_cache_before_produce=True, + empty_cache_after_produce=True, + async_prefetch=args.async_prefetch, + prefetch_depth=args.prefetch_depth, + ) + data_producer = RerollDataProducer( + config=producer_config, + prompt_dataset=train_dataset, + num_generations=self.num_generations, + generation_batch_size=args.generation_batch_size, + train_batch_size=args.per_device_train_batch_size, + steps_per_generation=args.steps_per_generation, + shuffle_dataset=self.shuffle_dataset, + seed=args.seed, + ) + data_producer.set_trainer(self) + if args.async_prefetch: + data_producer = AsyncDataProducer( + data_producer, + background_produce_kwargs={"skip_policy_logps": True}, + ) + return data_producer + + # -- Reward worker pool -------------------------------------------------- + + def _get_reward_workers(self): + """Return a list of persistent reward worker subprocesses (lazy-initialized).""" + import multiprocessing as _mp + + num_workers = getattr(self.args, "reward_num_workers", 1) + if num_workers < 1: + num_workers = 1 + + if self._reward_workers is not None: + alive = all(proc.is_alive() for conn, proc in self._reward_workers) + if alive and len(self._reward_workers) == num_workers: + return self._reward_workers + self._shutdown_reward_workers() + + workers = [] + for _ in range(num_workers): + parent_conn, child_conn = _mp.Pipe() + proc = _mp.Process( + target=_persistent_reward_worker, args=(child_conn,), daemon=True + ) + proc.start() + child_conn.close() + workers.append((parent_conn, proc)) + + self._reward_workers = workers + return workers + + def _shutdown_reward_workers(self): + """Shut down all persistent reward workers.""" + if self._reward_workers is None: + return + for conn, proc in self._reward_workers: + try: + conn.send(None) + proc.join(timeout=5) + except Exception: + pass + try: + conn.close() + except Exception: + pass + self._reward_workers = None + + # -- Hook overrides ------------------------------------------------------ + + def _compute_rewards_for_batch( + self, inputs, prompts, completions, completion_ids_list + ): + """Dispatch rewards to parallel subprocess workers (synchronous wrapper).""" + self._launch_reward_workers(inputs, prompts, completions, completion_ids_list) + return self._collect_reward_workers( + inputs, prompts, completions, completion_ids_list + ) + + def _launch_reward_workers(self, inputs, prompts, completions, completion_ids_list): + """Send reward work to subprocess workers (non-blocking). + + Results are collected later by _collect_reward_workers, allowing GPU + logprob computation to overlap with CPU reward computation. + """ + reward_can_bg = all( + callable(rf) + and not isinstance(rf, nn.Module) + and not asyncio.iscoroutinefunction(rf) + for rf in self.reward_funcs + ) + num_workers = getattr(self.args, "reward_num_workers", 1) + + if not reward_can_bg or num_workers <= 1: + # Can't parallelize — store args for sync fallback in collect + self._reward_workers_used = None + self._pending_reward_args = ( + inputs, + prompts, + completions, + completion_ids_list, + ) + return + + workers = self._get_reward_workers() + num_generations = self.num_generations + num_prompts = len(prompts) + num_groups = num_prompts // num_generations + + # Shard by prompt groups across workers + groups_per_worker = max(1, (num_groups + len(workers) - 1) // len(workers)) + workers_used = [] + for w_idx, (conn, _proc) in enumerate(workers): + g_start = w_idx * groups_per_worker + g_end = min((w_idx + 1) * groups_per_worker, num_groups) + if g_start >= num_groups: + break + s_start = g_start * num_generations + s_end = g_end * num_generations + conn.send( + ( + self.reward_funcs, + prompts[s_start:s_end], + completions[s_start:s_end], + completion_ids_list[s_start:s_end], + inputs[s_start:s_end], + self.reward_func_names, + ) + ) + workers_used.append(conn) + + self._reward_workers_used = workers_used + self._pending_reward_args = (inputs, prompts, completions, completion_ids_list) + + def _collect_reward_workers( + self, inputs, prompts, completions, completion_ids_list + ): + """Collect reward results from subprocess workers (blocks until done).""" + from accelerate.utils import gather + + workers_used = getattr(self, "_reward_workers_used", None) + args = getattr(self, "_pending_reward_args", None) + self._reward_workers_used = None + self._pending_reward_args = None + + if workers_used is None: + # Sync fallback — compute on main thread + if args is not None: + return self._calculate_rewards(*args) + return self._calculate_rewards( + inputs, prompts, completions, completion_ids_list + ) + + device = self.accelerator.device + num_prompts = len(args[1]) if args else len(prompts) + + # Collect results from workers + all_worker_results = [] + any_failed = False + for conn in workers_used: + result = conn.recv() + if result is None: + any_failed = True + # Drain remaining workers to prevent stale results in pipes + for remaining_conn in workers_used: + if remaining_conn is not conn: + try: + remaining_conn.recv() + except Exception: + pass + break + all_worker_results.append(result) + + if not any_failed: + rewards_per_func = torch.zeros( + num_prompts, len(self.reward_funcs), device=device + ) + offset = 0 + for worker_result in all_worker_results: + chunk_size = len(worker_result[0]) + for i, result in enumerate(worker_result): + rewards_per_func[offset : offset + chunk_size, i] = torch.tensor( + result, dtype=torch.float32, device=device + ) + offset += chunk_size + return gather(rewards_per_func) + + # Fallback to main thread on failure + if args is not None: + return self._calculate_rewards(*args) + return self._calculate_rewards( + inputs, prompts, completions, completion_ids_list + ) + + def _post_advantage_hook( + self, + data: dict, + rewards_per_func, + advantages, + inputs: list, + num_generations: int, + mode: str, + s_start: int | None = None, + s_end: int | None = None, + is_last_chunk: bool = True, + ) -> None: + """Replay buffer store/replace + re-roll buffering.""" + from trl.models.utils import disable_gradient_checkpointing + + # -- Replay buffer: store high-signal groups -- + if self._replay_buffer is not None: + local_grouped = rewards_per_func.view( + -1, num_generations, len(self.reward_funcs) + ) + per_group_std = local_grouped.std(dim=1) + has_signal = (per_group_std > 0).any(dim=1) + offset = s_start or 0 + + if has_signal.any(): + grouped_adv = advantages.view(-1, num_generations) + replay_scores = grouped_adv.abs().sum(dim=1) * per_group_std.sum(dim=1) + for group_idx in has_signal.nonzero(as_tuple=True)[0]: + gi = group_idx.item() + start = offset + gi * num_generations + end = start + num_generations + group_data = {} + for key in data: + val = data[key] + if ( + isinstance(val, torch.Tensor) + and val.dim() > 0 + and val.size(0) >= end + ): + group_data[key] = val[start:end].clone() + self._replay_buffer.add(replay_scores[gi].item(), group_data) + + # Replace zero-signal groups with high-signal replay buffer entries + # Only in non-streaming path (s_start is None) — streaming scores + # groups incrementally, so replacement + logprob recompute would be + # too expensive per chunk. + n_replaced = 0 + if s_start is None: + no_signal = ~has_signal + replaced_ranges = [] + if no_signal.any() and len(self._replay_buffer) > 0: + for group_idx in no_signal.nonzero(as_tuple=True)[0]: + sampled = self._replay_buffer.sample(1) + if sampled is None: + break + sampled_group = sampled[0] + gi = group_idx.item() + start = offset + gi * num_generations + end = start + num_generations + for key, val in sampled_group.items(): + if key in data and isinstance(data[key], torch.Tensor): + src = val.to(data[key].device) + tgt_seq_len = ( + data[key].size(1) if data[key].dim() > 1 else None + ) + if start >= data[key].size(0) or end > data[key].size( + 0 + ): + continue + if tgt_seq_len is not None: + if src.size(1) <= tgt_seq_len: + data[key][start:end] = 0 + data[key][start:end, : src.size(1)] = src + else: + data[key][start:end] = src[:, :tgt_seq_len] + else: + data[key][start:end] = src + replaced_ranges.append((start, end)) + n_replaced += 1 + + # Recompute old_per_token_logps for replayed groups + if ( + n_replaced > 0 + and self._replay_recompute_logps + and "old_per_token_logps" in data + ): + with ( + torch.no_grad(), + disable_gradient_checkpointing( + self.model, self.args.gradient_checkpointing_kwargs + ), + ): + for r_start, r_end in replaced_ranges: + r_ids = torch.cat( + [ + data["prompt_ids"][r_start:r_end], + data["completion_ids"][r_start:r_end], + ], + dim=1, + ) + r_mask = torch.cat( + [ + data["prompt_mask"][r_start:r_end], + data["completion_mask"][r_start:r_end], + ], + dim=1, + ) + r_logits_to_keep = data["completion_ids"].size(1) + r_fwd_kwargs = {} + for fk in ( + "pixel_values", + "image_grid_thw", + "pixel_attention_mask", + "image_sizes", + "token_type_ids", + "mm_token_type_ids", + ): + if fk in data: + r_fwd_kwargs[fk] = data[fk] + r_logps, _ = self._get_per_token_logps_and_entropies( + self.model, + r_ids, + r_mask, + r_logits_to_keep, + r_end - r_start, + **r_fwd_kwargs, + ) + data["old_per_token_logps"][r_start:r_end] = r_logps + + if n_replaced > 0: + self._metrics[mode]["replay_buffer_replacements"].append( + float(n_replaced) + ) + + if is_last_chunk: + self._metrics[mode]["replay_buffer_size"].append( + float(len(self._replay_buffer)) + ) + + # -- Re-roll buffer: store failed prompts -- + if getattr(self.args, "reroll_start_fraction", 1.0) < 1.0: + grouped_rewards = rewards_per_func.view( + -1, num_generations, len(self.reward_funcs) + ) + per_group_std = grouped_rewards.std(dim=1) + per_group_mean = grouped_rewards.mean(dim=1) + zero_signal = (per_group_std == 0).all(dim=1) + all_failed = (per_group_mean.abs() < 1e-6).all(dim=1) + should_reroll = zero_signal & all_failed + _n_buffered = 0 + with self._reroll_lock: + for group_idx in should_reroll.nonzero(as_tuple=True)[0]: + idx = group_idx.item() * num_generations + if idx >= len(inputs): + continue + prompt_input = inputs[idx] + self._reroll_buffer.append(prompt_input) + _n_buffered += 1 + if _n_buffered > 0: + self._metrics[mode]["reroll_buffered"].append(float(_n_buffered)) + if is_last_chunk: + self._metrics[mode]["reroll_buffer_size"].append( + float(len(self._reroll_buffer)) + ) + + # -- Zero-advantage skipping + Liger OPSM --------------------------------- + + def compute_liger_loss(self, unwrapped_model, inputs): + """Liger loss with zero-adv skipping and off-policy sequence masking (OPSM). + + The base class Liger path doesn't support OPSM because the fused kernel + doesn't expose per-token logprobs needed for the KL computation. This + override computes them via chunked lm_head matmul (no grad, low memory) + and applies the OPSM to the loss mask before calling the kernel. + """ + if self.args.skip_zero_advantage_batches and torch.all( + inputs["advantages"] == 0 + ): + mode = "train" if self.model.training else "eval" + self._metrics[mode]["skipped_zero_adv_batches"].append(1.0) + return torch.tensor( + 0.0, device=inputs["advantages"].device, requires_grad=True + ) + + if self.off_policy_mask_threshold is None: + return super().compute_liger_loss(unwrapped_model, inputs) + + # OPSM path: need per_token_logps for KL, which Liger kernel doesn't provide + prompt_ids, prompt_mask = inputs["prompt_ids"], inputs["prompt_mask"] + completion_ids, completion_mask = ( + inputs["completion_ids"], + inputs["completion_mask"], + ) + input_ids = torch.cat([prompt_ids, completion_ids], dim=1) + attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) + logits_to_keep = completion_ids.size(1) + + last_hidden_state = self._get_last_hidden_state( + unwrapped_model, + input_ids, + attention_mask, + logits_to_keep, + inputs.get("pixel_values"), + inputs.get("image_grid_thw"), + inputs.get("pixel_attention_mask"), + inputs.get("image_sizes"), + ) + + loss_mask = ( + completion_mask + if "tool_mask" not in inputs + else completion_mask * inputs["tool_mask"] + ) + + # Compute per_token_logps via chunked lm_head matmul (no grad, low memory) + lm_weight = unwrapped_model.lm_head.weight + lm_bias = unwrapped_model.lm_head.bias + with torch.no_grad(): + per_token_logps_chunks = [] + for i in range(last_hidden_state.size(0)): + chunk_logits = torch.matmul(last_hidden_state[i : i + 1], lm_weight.t()) + if lm_bias is not None: + chunk_logits = chunk_logits + lm_bias + chunk_lps = ( + chunk_logits.float() + .log_softmax(-1) + .gather(-1, completion_ids[i : i + 1].unsqueeze(-1)) + .squeeze(-1) + ) + per_token_logps_chunks.append(chunk_lps) + del chunk_logits + per_token_logps = torch.cat(per_token_logps_chunks, dim=0) + + advantages = inputs["advantages"] + if advantages.dim() == 1: + advantages_2d = advantages.unsqueeze(1) + else: + advantages_2d = advantages + + sampling_per_token_logps = inputs.get("sampling_per_token_logps") + if sampling_per_token_logps is None: + sampling_per_token_logps = inputs.get("old_per_token_logps") + if sampling_per_token_logps is None: + sampling_per_token_logps = per_token_logps + + off_policy_mask = GRPOTrainer.get_off_policy_mask( + advantages=advantages_2d, + per_token_logps=per_token_logps, + sampling_per_token_logps=sampling_per_token_logps, + mask=loss_mask, + off_policy_threshold=self.off_policy_mask_threshold, + ) + loss_mask = loss_mask * off_policy_mask + + # Call the Liger fused kernel with OPSM-modified mask + loss, metrics = self.liger_grpo_loss( + _input=last_hidden_state, + lin_weight=unwrapped_model.lm_head.weight, + selected_token_ids=completion_ids, + attention_mask=loss_mask, + advantages=inputs["advantages"], + bias=unwrapped_model.lm_head.bias, + old_per_token_logps=inputs.get("old_per_token_logps"), + ref_per_token_logps=inputs.get("ref_per_token_logps"), + vllm_is_ratio=inputs.get("importance_sampling_ratio"), + ) + + mean_kl = metrics[0] if self.beta != 0.0 else None + clip_ratio = metrics[-1] + + mode = "train" if self.model.training else "eval" + if self.beta != 0.0: + self._metrics[mode]["kl"].append( + self.accelerator.gather(mean_kl).mean().item() + ) + self._metrics[mode]["clip_ratio"].append( + self.accelerator.gather(clip_ratio).mean().item() + ) + normalizer = ( + self.current_gradient_accumulation_steps if mode == "train" else 1.0 + ) + return loss / normalizer + + def _compute_loss(self, model, inputs): + if self.args.skip_zero_advantage_batches and torch.all( + inputs["advantages"] == 0 + ): + mode = "train" if self.model.training else "eval" + self._metrics[mode]["skipped_zero_adv_batches"].append(1.0) + # Create zero loss with grad_fn. DeepSpeed requires grad_fn != None. + # With ZeRO-3, parameters are partitioned (shape=[0], requires_grad=False) + # so we can't just do `(p * 0).sum()`. Instead, do a tiny forward pass + # with a single token to create a proper computation graph. + prompt_ids = inputs["prompt_ids"][:1, :1] # (1, 1) + attn = torch.ones_like(prompt_ids) + with torch.amp.autocast(device_type="cuda", dtype=torch.bfloat16): + out = model(input_ids=prompt_ids, attention_mask=attn) + return out.logits.sum() * 0 + return super()._compute_loss(model, inputs) diff --git a/src/axolotl/core/trainers/grpo/replay_buffer.py b/src/axolotl/core/trainers/grpo/replay_buffer.py new file mode 100644 index 0000000000..1e35cb56a7 --- /dev/null +++ b/src/axolotl/core/trainers/grpo/replay_buffer.py @@ -0,0 +1,44 @@ +"""Simple replay buffer for storing and sampling high-signal rollout groups.""" + +import heapq + +import torch + + +class ReplayBuffer: + """Min-heap replay buffer that keeps the highest-scoring rollout groups. + Groups are scored by signal quality (advantage magnitude * reward variance). + When sampling, groups are drawn proportional to their scores. + """ + + def __init__(self, max_size: int): + self.max_size = max_size + self._heap: list[tuple[float, int, dict]] = [] # min-heap of (score, id, data) + self._counter = 0 # unique tiebreaker for heap + + def __len__(self): + return len(self._heap) + + def add(self, score: float, data: dict): + """Add a group to the buffer. If full, replaces lowest-scoring entry.""" + if self.max_size <= 0: + return + self._counter += 1 + if len(self._heap) < self.max_size: + heapq.heappush(self._heap, (score, self._counter, data)) + elif score > self._heap[0][0]: + heapq.heapreplace(self._heap, (score, self._counter, data)) + + def sample(self, num_samples: int) -> list[dict] | None: + """Sample groups weighted by their scores. Returns None if buffer is empty.""" + if self.max_size <= 0 or not self._heap: + return None + + scores = torch.tensor([item[0] for item in self._heap], dtype=torch.float32) + scores = scores.clamp(min=1e-8) # avoid zero probabilities + probs = scores / scores.sum() + replacement = num_samples > len(self._heap) + indices = torch.multinomial( + probs, num_samples, replacement=replacement + ).tolist() + return [self._heap[i][2] for i in indices] diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index f9f5a695b9..3a95ad439a 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -40,6 +40,7 @@ from trl.trainer.grpo_trainer import RewardFunc, nanstd from trl.trainer.utils import pad +from axolotl.core.trainers.grpo.fast_async_trainer import FastAsyncGRPOTrainer from axolotl.core.trainers.grpo.sampler import SequenceParallelRepeatRandomSampler from axolotl.core.trainers.mixins import ( DistributedParallelMixin, @@ -66,6 +67,19 @@ class AxolotlGRPOTrainer( _tag_names = ["trl", "grpo", "axolotl"] +class AxolotlAsyncGRPOTrainer( + RngLoaderMixin, + SchedulerMixin, + OptimizerMixin, + OptimizerInitMixin, + DistributedParallelMixin, + FastAsyncGRPOTrainer, +): + """Extend AsyncGRPOTrainer with axolotl helpers""" + + _tag_names = ["trl", "grpo", "async", "axolotl"] + + class AxolotlGRPOSequenceParallelTrainer(AxolotlGRPOTrainer): """Extend the base GRPOTrainer for sequence parallelism handling""" diff --git a/src/axolotl/kernels/lora.py b/src/axolotl/kernels/lora.py index c3356fb909..28ef75acc0 100644 --- a/src/axolotl/kernels/lora.py +++ b/src/axolotl/kernels/lora.py @@ -25,7 +25,7 @@ def get_lora_parameters( ) -> tuple[ torch.Tensor, torch.Tensor | None, - QuantState | None, + QuantState | torch.Tensor | None, torch.Tensor | None, torch.Tensor | None, float | None, @@ -48,9 +48,13 @@ def get_lora_parameters( if not hasattr(proj, "disable_adapters") or proj.disable_adapters or proj.merged: quant_state = getattr(W, "quant_state", None) + if quant_state is None and W.dtype == torch.float8_e4m3fn: + quant_state = getattr(base_layer, "weight_scale_inv", None) return W, b, quant_state, None, None, None quant_state = getattr(W, "quant_state", None) + if quant_state is None and W.dtype == torch.float8_e4m3fn: + quant_state = getattr(base_layer, "weight_scale_inv", None) active_adapter = ( proj.active_adapters[0] @@ -81,7 +85,7 @@ def matmul_lora( X: torch.Tensor, W: torch.Tensor, b: torch.Tensor | None, - W_quant: QuantState | None, + W_quant: QuantState | torch.Tensor | None, A: torch.Tensor | None, B: torch.Tensor | None, s: float | None, diff --git a/src/axolotl/kernels/quantize.py b/src/axolotl/kernels/quantize.py index d094f2381c..c9c0f59bd6 100644 --- a/src/axolotl/kernels/quantize.py +++ b/src/axolotl/kernels/quantize.py @@ -1,4 +1,4 @@ -"""Dequantization utilities for `bitsandbytes` integration.""" +"""Dequantization utilities for `bitsandbytes` and FP8 integration.""" import ctypes @@ -15,9 +15,50 @@ HAS_CUDA_STREAM: bool = Version(bnb.__version__) > Version("0.43.3") +def dequantize_fp8( + W: torch.Tensor, + scale_inv: torch.Tensor, + dtype: torch.dtype = torch.bfloat16, +) -> torch.Tensor: + """Dequantize FP8 block-quantized weights: W_dequant = W_fp8 * scale_inv. + + Args: + W: FP8 weight tensor [out_features, in_features] in float8_e4m3fn. + scale_inv: Per-block inverse scale [ceil(out/block), ceil(in/block)] + or per-tensor scalar. + dtype: Output dtype (default bf16). + + Returns: + Dequantized tensor in the specified dtype. + """ + W_float = W.to(dtype) + if scale_inv.numel() == 1: + return W_float * scale_inv.to(dtype) + if scale_inv.dim() == 2 and W.dim() == 2: + sr, sc = scale_inv.shape + br = W.shape[0] // sr + bc = W.shape[1] // sc + # If dimensions are exactly divisible, use fast reshape path + if sr * br == W.shape[0] and sc * bc == W.shape[1]: + return ( + W_float.reshape(sr, br, sc, bc) * scale_inv[:, None, :, None].to(dtype) + ).reshape(W.shape) + # Tail-block handling: compute actual block size (ceil division), + # tile scale_inv to cover full shape, then crop to W's dimensions + br_ceil = -(-W.shape[0] // sr) # ceil(rows / scale_rows) = block_size + bc_ceil = -(-W.shape[1] // sc) + scale_expanded = ( + scale_inv.to(dtype) + .repeat_interleave(br_ceil, dim=0) + .repeat_interleave(bc_ceil, dim=1) + )[: W.shape[0], : W.shape[1]] + return W_float * scale_expanded + return W_float * scale_inv.to(dtype) + + def dequantize( W: torch.Tensor, - quant_state: QuantState | list | None = None, + quant_state: QuantState | list | torch.Tensor | None = None, out: torch.Tensor | None = None, ) -> torch.Tensor: """ @@ -49,6 +90,15 @@ def dequantize( if quant_state is None: return W + # FP8 path: quant_state is actually scale_inv tensor + if W.dtype == torch.float8_e4m3fn: + scale_inv = quant_state + # Caller may pass W.t() (non-contiguous) — dequantize in original + # layout then transpose back so the result shape matches the input. + if not W.is_contiguous() and W.dim() == 2: + return dequantize_fp8(W.t(), scale_inv).t() + return dequantize_fp8(W, scale_inv) + # Get the target device from input tensor W target_device = W.device diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index eb7203c010..2b53b7b2c5 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -160,6 +160,18 @@ def load_lora( else: model = get_peft_model(model, lora_config, **model_kwargs) + # FP8 models: LoRA A/B inherit FP8 dtype from base weights, but training + # requires a compute dtype (bf16/fp16). Cast trainable LoRA params. + if cfg.torch_dtype: + _fp8_cast_dtype = cfg.torch_dtype + elif torch.cuda.is_available() and torch.cuda.is_bf16_supported(): + _fp8_cast_dtype = torch.bfloat16 + else: + _fp8_cast_dtype = torch.float16 + for _name, param in model.named_parameters(): + if param.requires_grad and param.dtype == torch.float8_e4m3fn: + param.data = param.data.to(_fp8_cast_dtype) + if rank == 0: try: model.print_trainable_parameters() diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 2662d0b861..37c1123370 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -215,6 +215,8 @@ def _apply_pre_model_load_setup(self): self.model_kwargs["revision"] = self.cfg.revision_of_model if self.cfg.use_kernels: self.model_kwargs["use_kernels"] = self.cfg.use_kernels + if "allow_all_kernels" not in self.model_kwargs: + self.model_kwargs["allow_all_kernels"] = self.cfg.use_kernels self._set_quantization_config() self._set_attention_config() self._check_model_requirements() diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 5874c940b9..205e32e6ff 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -116,6 +116,7 @@ def apply_pre_model_load_patches(self): self._apply_patch_deepspeed_zero3() self._apply_voxtral_patches() self._apply_apertus_patches() + self._apply_trl_vllm_patches() def apply_post_plugin_pre_model_load_patches(self): """Apply post plugin-pre_model_load load patches based on config.""" @@ -667,6 +668,17 @@ def _apply_apertus_patches(self): patch_apertus_xielu_activation() + def _apply_trl_vllm_patches(self): + """Apply TRL vLLM patches for batched weight sync, NaN logprobs fix, and scalar handling.""" + if ( + self.cfg.rl + and getattr(self.cfg, "trl", None) + and getattr(self.cfg.trl, "use_vllm", False) + ): + from axolotl.monkeypatch.trainer.trl_vllm import patch_trl_vllm + + patch_trl_vllm() + def _apply_scaling_softmax_patch(self, model: PreTrainedModel): """Apply Scaling Softmax (SSMax) patch. Ref: https://arxiv.org/abs/2501.19399""" if self.cfg.scaling_softmax: diff --git a/src/axolotl/monkeypatch/trainer/trl_vllm.py b/src/axolotl/monkeypatch/trainer/trl_vllm.py new file mode 100644 index 0000000000..a3296df61d --- /dev/null +++ b/src/axolotl/monkeypatch/trainer/trl_vllm.py @@ -0,0 +1,245 @@ +"""Monkeypatches for TRL's vLLM integration and trainer utils. + +Adds: +- VLLMClient.batch_update_named_params: batched weight sync (fewer HTTP round-trips) +- extract_logprobs: NaN→0.0 fix (prevents downstream NaN propagation) +- VLLMGeneration: weight_sync_chunk_size + batched sync path for non-FSDP/non-ZeRO +- split_tensor_dict / shuffle_sequence_dict: scalar type handling (int/float/bool passthrough) +""" + +import logging +import math +from functools import wraps + +import torch +from torch import nn + +LOG = logging.getLogger(__name__) + + +def _batch_update_named_params( + self, params: list[tuple[str, torch.Tensor]], chunk_size: int | None = None +): + """Batched weight sync — sends param metadata via HTTP, tensors via NCCL.""" + from transformers import is_torch_xpu_available + + if chunk_size is None: + chunks = [params] + else: + chunks = [] + current_chunk: list[tuple[str, torch.Tensor]] = [] + current_elements = 0 + for name, weights in params: + n_elem = weights.numel() + if current_chunk and current_elements + n_elem > chunk_size: + chunks.append(current_chunk) + current_chunk = [] + current_elements = 0 + current_chunk.append((name, weights)) + current_elements += n_elem + if current_chunk: + chunks.append(current_chunk) + + for chunk in chunks: + param_metadata = [ + {"name": name, "dtype": str(weights.dtype), "shape": list(weights.shape)} + for name, weights in chunk + ] + url = f"{self.base_url}/batch_update_named_params/" + response = self.session.post(url, json={"params": param_metadata}) + if response.status_code != 200: + raise Exception(f"Request failed: {response.status_code}, {response.text}") + + for _name, weights in chunk: + if is_torch_xpu_available(): + self.communicator.broadcast(weights, root=self.rank) + else: + self.communicator.broadcast(weights, src=self.rank) + + if is_torch_xpu_available(): + self.communicator.barrier() + else: + self.communicator.group.barrier() + + +def _update_model_params(self, model: nn.Module, chunk_size: int | None = None): + """Updates all model params using batch_update_named_params.""" + params = [(name, param.data) for name, param in model.named_parameters()] + self.batch_update_named_params(params, chunk_size=chunk_size) + + +def _patched_extract_logprobs(all_outputs): + """extract_logprobs with NaN→0.0 fix (stock TRL uses None which causes downstream errors).""" + all_logprobs = [] + all_token_ids = [] + + for outputs in all_outputs: + for output in outputs.outputs: + if output.logprobs is None: + return None, None + seq_logprobs = [] + seq_token_ids = [] + for lp in output.logprobs: + sorted_items = sorted(lp.items(), key=lambda x: x[1].rank) + seq_token_ids.append([token_id for token_id, _ in sorted_items]) + seq_logprobs.append( + [ + 0.0 if math.isnan(item.logprob) else item.logprob + for _, item in sorted_items + ] + ) + all_logprobs.append(seq_logprobs) + all_token_ids.append(seq_token_ids) + + return all_logprobs, all_token_ids + + +def _patched_split_tensor_dict(tensor_dict, num_chunks): + """split_tensor_dict that handles scalar types (int/float/bool) for num_items_in_batch.""" + first_tensor = next( + tensor + for tensor in tensor_dict.values() + if tensor is not None and isinstance(tensor, torch.Tensor) and tensor.ndim > 0 + ) + chunk_size = first_tensor.shape[0] // num_chunks + chunks = [] + for i in range(num_chunks): + chunk_dict = {} + for key, tensor in tensor_dict.items(): + if isinstance(tensor, (int, float, bool)): + chunk_dict[key] = tensor + elif tensor is not None and (isinstance(tensor, list) or tensor.ndim > 0): + chunk_dict[key] = tensor[i * chunk_size : (i + 1) * chunk_size] + elif tensor is not None and tensor.ndim == 0: + chunk_dict[key] = tensor + else: + chunk_dict[key] = None + chunks.append(chunk_dict) + return chunks + + +def _patched_shuffle_sequence_dict(seq_dict): + """shuffle_sequence_dict that handles scalar types (int/float/bool).""" + first_seq = next( + v + for v in seq_dict.values() + if v is not None and isinstance(v, (torch.Tensor, list)) and len(v) > 0 + ) + perm = torch.randperm(len(first_seq)) + + def permute(v): + if v is None: + return None + if isinstance(v, (int, float, bool)): + return v + if isinstance(v, torch.Tensor) and v.ndim == 0: + return v + if isinstance(v, torch.Tensor) and v.ndim >= 1: + return v[perm] + if isinstance(v, list): + return [v[i] for i in perm.tolist()] + return v + + return {k: permute(v) for k, v in seq_dict.items()} + + +def _patch_sync_weights_batched(original_init): + """Wrap VLLMGeneration.__init__ to accept weight_sync_chunk_size.""" + + @wraps(original_init) + def patched_init(self, *args, weight_sync_chunk_size=None, **kwargs): + original_init(self, *args, **kwargs) + self.weight_sync_chunk_size = weight_sync_chunk_size + + return patched_init + + +def _make_batched_sync_weights(original_sync_weights): + """Wrap sync_weights to use batched sync for non-FSDP/non-ZeRO paths.""" + + @wraps(original_sync_weights) + def patched_sync_weights(self): + from accelerate.utils import is_peft_model + + # Check if we're in a non-PEFT, non-FSDP, non-ZeRO scenario where batching helps + accelerator = self.accelerator + model = self.model + is_fsdp_enabled = self.is_fsdp_enabled + + deepspeed_plugin = accelerator.state.deepspeed_plugin + zero_stage_3 = deepspeed_plugin is not None and deepspeed_plugin.zero_stage == 3 + + is_peft = is_peft_model(model) + + # If PEFT, FSDP, or ZeRO-3, fall back to original (which handles those cases) + if is_peft or is_fsdp_enabled or zero_stage_3: + return original_sync_weights(self) + + # Non-PEFT, non-FSDP, non-ZeRO: use batched sync + if self.mode == "colocate" and getattr(self, "enable_sleep_mode", False): + from vllm.distributed.device_communicators.cuda_wrapper import ( + empty_cache, + ) + + empty_cache() + self.llm.wake_up(tags=["weights"]) + + if self.mode == "server" and accelerator.is_main_process: + params = [ + (self._fix_param_name_to_vllm(name), param.data) + for name, param in model.named_parameters() + ] + self.vllm_client.batch_update_named_params( + params, chunk_size=getattr(self, "weight_sync_chunk_size", None) + ) + elif self.mode == "colocate": + llm_model = ( + self.llm.llm_engine.model_executor.driver_worker.model_runner.model + ) + weights = [ + (self._fix_param_name_to_vllm(name), param.data) + for name, param in model.named_parameters() + ] + llm_model.load_weights(weights=weights) + + # Reset cache + if self.mode == "server" and accelerator.is_main_process: + self.vllm_client.reset_prefix_cache() + elif self.mode == "colocate": + self.llm.reset_prefix_cache() + + return patched_sync_weights + + +def patch_trl_vllm(): + """Apply all TRL vLLM monkeypatches.""" + import trl.generation.vllm_client + import trl.generation.vllm_generation + import trl.trainer.utils + + VLLMClient = trl.generation.vllm_client.VLLMClient + VLLMGeneration = trl.generation.vllm_generation.VLLMGeneration + + # 1. Add batch_update_named_params to VLLMClient + if not hasattr(VLLMClient, "batch_update_named_params"): + VLLMClient.batch_update_named_params = _batch_update_named_params + VLLMClient.update_model_params = _update_model_params + LOG.info("Patched VLLMClient with batch_update_named_params") + + # 2. Patch extract_logprobs (NaN→0.0) + trl.generation.vllm_generation.extract_logprobs = _patched_extract_logprobs + LOG.info("Patched extract_logprobs with NaN→0.0 fix") + + # 3. Patch VLLMGeneration.__init__ to accept weight_sync_chunk_size + VLLMGeneration.__init__ = _patch_sync_weights_batched(VLLMGeneration.__init__) + + # 4. Patch sync_weights for batched non-FSDP/non-ZeRO path + VLLMGeneration.sync_weights = _make_batched_sync_weights( + VLLMGeneration.sync_weights + ) + LOG.info("Patched VLLMGeneration with batched sync_weights") + + # 5. Patch split_tensor_dict and shuffle_sequence_dict + trl.trainer.utils.split_tensor_dict = _patched_split_tensor_dict + trl.trainer.utils.shuffle_sequence_dict = _patched_shuffle_sequence_dict + LOG.info("Patched split_tensor_dict and shuffle_sequence_dict for scalar types") diff --git a/src/axolotl/scripts/__init__.py b/src/axolotl/scripts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/scripts/vllm_serve_lora.py b/src/axolotl/scripts/vllm_serve_lora.py new file mode 100644 index 0000000000..9ce4d2771e --- /dev/null +++ b/src/axolotl/scripts/vllm_serve_lora.py @@ -0,0 +1,503 @@ +"""vLLM serve script with native LoRA adapter support. + +Extends TRL's vllm_serve to enable direct LoRA adapter loading in vLLM, +instead of merging adapter weights into the base model before syncing. + +Usage: + Set ``vllm.serve_module: axolotl.scripts.vllm_serve_lora`` in your config, + or ``trl.vllm_lora_sync: true`` to auto-select. + +Benefits over merge-sync: + - Syncs only LoRA adapter weights via filesystem instead of full merged model via NCCL + - vLLM handles LoRA application natively (Punica kernels) + - No NCCL communicator needed for weight sync +""" + +import logging +import os +from contextlib import asynccontextmanager +from dataclasses import dataclass, field +from itertools import chain +from multiprocessing import Pipe, Process +from multiprocessing.connection import Connection +from typing import Any + +from trl.scripts.vllm_serve import ( + ScriptArguments, + chunk_list, + extract_logprobs, + get_open_port, +) +from vllm import LLM, SamplingParams +from vllm.lora.request import LoRARequest + +logger = logging.getLogger(__name__) + + +@dataclass +class LoRAScriptArguments(ScriptArguments): + """Extended script arguments with LoRA support.""" + + enable_lora: bool = field( + default=True, + metadata={"help": "Enable LoRA adapter support in vLLM."}, + ) + max_lora_rank: int = field( + default=64, + metadata={"help": "Maximum LoRA rank supported."}, + ) + max_loras: int = field( + default=2, + metadata={"help": "Maximum number of LoRA adapters loaded simultaneously."}, + ) + lora_dtype: str = field( + default="bfloat16", + metadata={"help": "Data type for LoRA weights."}, + ) + + +def llm_worker( + script_args: LoRAScriptArguments, + data_parallel_rank: int, + master_port: int, + connection: Connection, +) -> None: + """Worker process that creates a vLLM LLM with LoRA enabled.""" + os.environ["VLLM_DP_RANK"] = str(data_parallel_rank) + os.environ["VLLM_DP_RANK_LOCAL"] = str(data_parallel_rank) + os.environ["VLLM_DP_SIZE"] = str(script_args.data_parallel_size) + os.environ["VLLM_DP_MASTER_PORT"] = str(master_port) + + llm = LLM( + model=script_args.model, + revision=script_args.revision, + tensor_parallel_size=script_args.tensor_parallel_size, + gpu_memory_utilization=script_args.gpu_memory_utilization, + enforce_eager=script_args.enforce_eager, + dtype=script_args.dtype, + enable_prefix_caching=script_args.enable_prefix_caching, + kv_cache_dtype=script_args.kv_cache_dtype, + max_model_len=script_args.max_model_len, + # Use batch-capable worker extension (adds batch_update_named_params + auto-close) + worker_extension_cls="axolotl.scripts.vllm_worker_ext.BatchWeightSyncWorkerExtension", + trust_remote_code=script_args.trust_remote_code, + model_impl=script_args.vllm_model_impl, + logprobs_mode="processed_logprobs", + # LoRA + enable_lora=script_args.enable_lora, + max_lora_rank=script_args.max_lora_rank, + max_loras=script_args.max_loras, + lora_dtype=script_args.lora_dtype, + ) + + connection.send({"status": "ready"}) + + while True: + try: + command = connection.recv() + except KeyboardInterrupt: + llm.collective_rpc(method="close_communicator") + break + + if command["type"] in ["call", "fire_and_forget"]: + method_name = command["method"] + args = command.get("args", ()) + kwargs = command.get("kwargs", {}) + + # Reconstruct LoRARequest from serialized dict (can't pickle across pipe) + if "lora_request" in kwargs and kwargs["lora_request"] is not None: + lr = kwargs["lora_request"] + kwargs["lora_request"] = LoRARequest( + lora_name=lr["lora_name"], + lora_int_id=lr["lora_int_id"], + lora_path=lr["lora_path"], + load_inplace=lr.get("load_inplace", False), + ) + + method = getattr(llm, method_name) + result = method(*args, **kwargs) + if command["type"] == "call": + connection.send(result) + elif command["type"] == "shutdown": + break + + +def main(script_args: ScriptArguments): + """Start vLLM workers with LoRA support and the HTTP server.""" + import asyncio + + import uvicorn + from fastapi import FastAPI + from pydantic import BaseModel, Field as PydanticField + + # Request/Response models (defined locally like TRL's vllm_serve.main) + class GenerateRequest(BaseModel): + prompts: list[str] + images: list[str] | None = None + n: int = 1 + repetition_penalty: float = 1.0 + temperature: float = 1.0 + top_p: float = 1.0 + top_k: int = -1 + min_p: float = 0.0 + max_tokens: int = 16 + logprobs: int | None = 0 + truncate_prompt_tokens: int | None = None + structured_outputs_regex: str | None = None + generation_kwargs: dict = PydanticField(default_factory=dict) + + class GenerateResponse(BaseModel): + prompt_ids: list[list[int]] + completion_ids: list[list[int]] + logprobs: list[list[list[float]]] + logprob_token_ids: list[list[list[int]]] + + class ChatRequest(BaseModel): + messages: list[list[dict]] + n: int = 1 + repetition_penalty: float = 1.0 + temperature: float = 1.0 + top_p: float = 1.0 + top_k: int = -1 + min_p: float = 0.0 + max_tokens: int = 16 + logprobs: int | None = 0 + truncate_prompt_tokens: int | None = None + structured_outputs_regex: str | None = None + generation_kwargs: dict = PydanticField(default_factory=dict) + chat_template_kwargs: dict = PydanticField(default_factory=dict) + + class ChatResponse(BaseModel): + prompt_ids: list[list[int]] + completion_ids: list[list[int]] + logprobs: list[list[list[float]]] + logprob_token_ids: list[list[list[int]]] + + class InitCommunicatorRequest(BaseModel): + host: str + port: int + world_size: int + client_device_uuid: str + + # Wrap plain ScriptArguments with LoRA defaults + if not isinstance(script_args, LoRAScriptArguments): + lora_args = LoRAScriptArguments.__new__(LoRAScriptArguments) + for f in ScriptArguments.__dataclass_fields__: + setattr(lora_args, f, getattr(script_args, f)) + # Apply LoRA defaults + for f in LoRAScriptArguments.__dataclass_fields__: + if f not in ScriptArguments.__dataclass_fields__: + setattr( + lora_args, f, LoRAScriptArguments.__dataclass_fields__[f].default + ) + script_args = lora_args + + # Spawn workers + master_port = get_open_port() + connections: list[Connection] = [] + processes: list[Process] = [] + for dp_rank in range(script_args.data_parallel_size): + parent_conn, child_conn = Pipe() + process = Process( + target=llm_worker, + args=(script_args, dp_rank, master_port, child_conn), + ) + process.start() + connections.append(parent_conn) + processes.append(process) + + @asynccontextmanager + async def lifespan(app: FastAPI): + import time + + startup_timeout = 300 # 5 minutes + start_time = time.monotonic() + ready: set[int] = set() + while len(ready) < script_args.data_parallel_size: + elapsed = time.monotonic() - start_time + if elapsed > startup_timeout: + raise RuntimeError( + f"vLLM workers failed to start within {startup_timeout}s " + f"({len(ready)}/{script_args.data_parallel_size} ready)" + ) + for i, (conn, proc) in enumerate(zip(connections, processes, strict=True)): + if id(conn) in ready: + continue + if not proc.is_alive(): + raise RuntimeError( + f"vLLM worker {i} exited unexpectedly during startup" + ) + if conn.poll(): + msg = conn.recv() + if isinstance(msg, dict) and msg.get("status") == "ready": + ready.add(id(conn)) + await asyncio.sleep(0.1) + yield + for p in processes: + p.join(timeout=10) + if p.is_alive(): + p.terminate() + p.join() + + app = FastAPI(lifespan=lifespan) + + # --- Active LoRA state (shared across endpoints via closure) --- + active_lora: dict = {"request": None} + + # ------------------------------------------------------------------ + # LoRA-specific endpoints + # ------------------------------------------------------------------ + + class SetLoRARequest(BaseModel): + lora_name: str + lora_int_id: int + lora_path: str + load_inplace: bool = False + + @app.post("/set_lora_adapter/") + async def set_lora_adapter(request: SetLoRARequest): + """Register a LoRA adapter for all subsequent generate/chat calls.""" + active_lora["request"] = { + "lora_name": request.lora_name, + "lora_int_id": request.lora_int_id, + "lora_path": request.lora_path, + "load_inplace": request.load_inplace, + } + logger.info( + "Set active LoRA: %s (id=%d, path=%s)", + request.lora_name, + request.lora_int_id, + request.lora_path, + ) + return {"status": "ok"} + + @app.post("/clear_lora_adapter/") + async def clear_lora_adapter(): + """Clear active LoRA adapter (revert to base model).""" + active_lora["request"] = None + return {"status": "ok"} + + # ------------------------------------------------------------------ + # Standard endpoints (mirrors TRL's vllm_serve) + # ------------------------------------------------------------------ + + @app.get("/health/") + async def health(): + return {"status": "ok"} + + @app.get("/get_world_size/") + async def get_world_size(): + return { + "world_size": script_args.tensor_parallel_size + * script_args.data_parallel_size + } + + @app.post("/generate/", response_model=GenerateResponse) + async def generate(request: GenerateRequest): + """Generate completions with optional LoRA adapter.""" + import base64 + from io import BytesIO + + import vllm + from packaging.version import Version + from vllm.sampling_params import GuidedDecodingParams + + images: list[str | None] = request.images or [None] * len(request.prompts) # type: ignore[assignment,list-item] + prompts: list[dict[str, Any]] = [] + for prompt, image in zip(request.prompts, images, strict=True): + row: dict[str, Any] = {"prompt": prompt} + if image is not None: + from PIL import Image + + row["multi_modal_data"] = { + "image": Image.open(BytesIO(base64.b64decode(image))) + } + prompts.append(row) + + generation_kwargs = { + "n": request.n, + "repetition_penalty": request.repetition_penalty, + "temperature": request.temperature, + "top_p": request.top_p, + "top_k": request.top_k, + "min_p": request.min_p, + "max_tokens": request.max_tokens, + "logprobs": request.logprobs, + } + generation_kwargs.update(request.generation_kwargs) + + if Version(vllm.__version__) <= Version("0.10.2"): + key = "guided_decoding" + if request.structured_outputs_regex is not None: + generation_kwargs[key] = GuidedDecodingParams( + regex=request.structured_outputs_regex + ) + else: + generation_kwargs.setdefault(key, None) + else: + from vllm.sampling_params import StructuredOutputsParams + + key = "structured_outputs" + if request.structured_outputs_regex is not None: + generation_kwargs[key] = StructuredOutputsParams( + regex=request.structured_outputs_regex + ) + elif isinstance(generation_kwargs.get(key), dict): + generation_kwargs[key] = StructuredOutputsParams( + **generation_kwargs[key] + ) + else: + generation_kwargs.setdefault(key, None) + + sampling_params = SamplingParams(**generation_kwargs) + chunked_prompts = chunk_list(prompts, script_args.data_parallel_size) + + for conn, chunk in zip(connections, chunked_prompts, strict=True): + if not chunk: + chunk = [{"prompt": ""}] + kwargs = { + "prompts": chunk, + "sampling_params": sampling_params, + "lora_request": active_lora["request"], + } + conn.send({"type": "call", "method": "generate", "kwargs": kwargs}) + + all_outputs = [conn.recv() for conn in connections] + all_outputs = [ + o for o, c in zip(all_outputs, chunked_prompts, strict=True) if c + ] + all_outputs = list(chain.from_iterable(all_outputs)) + + return { + "prompt_ids": [o.prompt_token_ids for o in all_outputs], + "completion_ids": [ + list(out.token_ids) for o in all_outputs for out in o.outputs + ], + "logprobs": extract_logprobs(all_outputs)[0], + "logprob_token_ids": extract_logprobs(all_outputs)[1], + } + + @app.post("/chat/", response_model=ChatResponse) + async def chat(request: ChatRequest): + """Chat endpoint with optional LoRA adapter.""" + generation_kwargs = { + "n": request.n, + "repetition_penalty": request.repetition_penalty, + "temperature": request.temperature, + "top_p": request.top_p, + "top_k": request.top_k, + "min_p": request.min_p, + "max_tokens": request.max_tokens, + "logprobs": request.logprobs, + } + generation_kwargs.update(request.generation_kwargs) + sampling_params = SamplingParams(**generation_kwargs) + chunked = chunk_list(request.messages, script_args.data_parallel_size) + for conn, chunk in zip(connections, chunked, strict=True): + if not chunk: + chunk = [[{"role": "user", "content": ""}]] + kwargs = { + "messages": chunk, + "sampling_params": sampling_params, + "use_tqdm": False, + "lora_request": active_lora["request"], + } + conn.send({"type": "call", "method": "chat", "kwargs": kwargs}) + + all_outputs = [conn.recv() for conn in connections] + all_outputs = [o for o, c in zip(all_outputs, chunked, strict=True) if c] + all_outputs = list(chain.from_iterable(all_outputs)) + + return { + "prompt_ids": [o.prompt_token_ids for o in all_outputs], + "completion_ids": [ + list(out.token_ids) for o in all_outputs for out in o.outputs + ], + "logprobs": extract_logprobs(all_outputs)[0], + "logprob_token_ids": extract_logprobs(all_outputs)[1], + } + + # --- Weight sync endpoints (legacy fallback, same as TRL) --- + + @app.post("/init_communicator/") + async def init_communicator(request: InitCommunicatorRequest): + world_size = ( + script_args.tensor_parallel_size * script_args.data_parallel_size + 1 + ) + kwargs = { + "method": "init_communicator", + "args": ( + request.host, + request.port, + world_size, + request.client_device_uuid, + ), + } + msg = {"type": "fire_and_forget", "method": "collective_rpc", "kwargs": kwargs} + loop = asyncio.get_running_loop() + await asyncio.gather( + *(loop.run_in_executor(None, c.send, msg) for c in connections) + ) + return {"message": "Initializing communicator"} + + class UpdateWeightsRequest(BaseModel): + name: str + dtype: str + shape: list[int] + + @app.post("/update_named_param/") + async def update_named_param(request: UpdateWeightsRequest): + kwargs = { + "method": "update_named_param", + "args": (request.name, request.dtype, tuple(request.shape)), + } + msg = {"type": "fire_and_forget", "method": "collective_rpc", "kwargs": kwargs} + loop = asyncio.get_running_loop() + await asyncio.gather( + *(loop.run_in_executor(None, c.send, msg) for c in connections) + ) + return {"message": "Updating parameter"} + + class BatchUpdateWeightsRequest(BaseModel): + params: list[dict] + + @app.post("/batch_update_named_params/") + async def batch_update_named_params(request: BatchUpdateWeightsRequest): + params_list = [ + (p["name"], p["dtype"], tuple(p["shape"])) for p in request.params + ] + kwargs = {"method": "batch_update_named_params", "args": (params_list,)} + msg = {"type": "fire_and_forget", "method": "collective_rpc", "kwargs": kwargs} + loop = asyncio.get_running_loop() + await asyncio.gather( + *(loop.run_in_executor(None, c.send, msg) for c in connections) + ) + return {"message": f"Batch update for {len(params_list)} params"} + + @app.post("/reset_prefix_cache/") + async def reset_prefix_cache(): + for conn in connections: + conn.send({"type": "call", "method": "reset_prefix_cache"}) + results = [conn.recv() for conn in connections] + return {"message": f"Reset prefix cache: {all(results)}"} + + @app.post("/close_communicator/") + async def close_communicator(): + kwargs = {"method": "close_communicator"} + for conn in connections: + conn.send( + { + "type": "fire_and_forget", + "method": "collective_rpc", + "kwargs": kwargs, + } + ) + return {"message": "Closing communicator"} + + uvicorn.run( + app, + host=script_args.host, + port=script_args.port, + log_level=script_args.log_level, + access_log=True, + ) diff --git a/src/axolotl/scripts/vllm_worker_ext.py b/src/axolotl/scripts/vllm_worker_ext.py new file mode 100644 index 0000000000..386460df14 --- /dev/null +++ b/src/axolotl/scripts/vllm_worker_ext.py @@ -0,0 +1,158 @@ +"""Extended vLLM worker extension with batch weight sync support. + +Subclasses TRL's WeightSyncWorkerExtension to add: +- batch_update_named_params: receives multiple params in one call +- Auto-close stale communicator on re-init +- _direct_set_weight: proper handling for stacked (qkv_proj, gate_up_proj) params, + including LoRA-wrapped models where vLLM inserts base_layer into the hierarchy +""" + +import logging + +import torch + +try: + from transformers import is_torch_xpu_available +except ImportError: + is_torch_xpu_available = lambda: False # noqa: E731 + +from trl.scripts.vllm_serve import WeightSyncWorkerExtension + +logger = logging.getLogger(__name__) + +# Stacked param name mapping: shard_name -> (packed_name, shard_order) +_STACKED_PARAMS = { + "q_proj": ("qkv_proj", 0), + "k_proj": ("qkv_proj", 1), + "v_proj": ("qkv_proj", 2), + "gate_proj": ("gate_up_proj", 0), + "up_proj": ("gate_up_proj", 1), +} + + +class BatchWeightSyncWorkerExtension(WeightSyncWorkerExtension): + """Worker extension that adds batch weight update and direct weight setting.""" + + def init_communicator(self, host, port, world_size, client_device_uuid): + """Auto-close stale communicator before re-initializing.""" + if self.communicator is not None: + self.close_communicator() + super().init_communicator(host, port, world_size, client_device_uuid) + + def _direct_set_weight(self, name: str, weight: torch.Tensor) -> None: + """Directly copy weight data into the model, handling stacked params. + + Bypasses model.load_weights() which may fail on vLLM 0.17's new + module-tree weight loader for stacked params (qkv_proj, gate_up_proj). + + Handles LoRA-wrapped params where vLLM inserts ``base_layer`` into the + parameter hierarchy (e.g. ``qkv_proj.base_layer.weight``). + """ + model = self.model_runner.model + params_dict = dict(model.named_parameters()) + + # Check if this is a simple direct param (exists as-is) + if name in params_dict: + params_dict[name].data.copy_(weight.to(params_dict[name].dtype)) + return + + # Also check with base_layer inserted: x.y.weight -> x.y.base_layer.weight + parts_bl = name.rsplit(".", 1) + if len(parts_bl) == 2: + base_layer_name = f"{parts_bl[0]}.base_layer.{parts_bl[1]}" + if base_layer_name in params_dict: + params_dict[base_layer_name].data.copy_( + weight.to(params_dict[base_layer_name].dtype) + ) + return + + # Handle stacked params: e.g. "model.layers.0.self_attn.q_proj.weight" + # -> "model.layers.0.self_attn.qkv_proj.weight" with shard offset + parts = name.rsplit(".", 2) # [prefix, layer_name, suffix] + if len(parts) == 3: + prefix, layer_name, suffix = parts + if layer_name in _STACKED_PARAMS: + packed_name, shard_idx = _STACKED_PARAMS[layer_name] + for packed_full in [ + f"{prefix}.{packed_name}.{suffix}", + f"{prefix}.{packed_name}.base_layer.{suffix}", + ]: + if packed_full not in params_dict: + continue + param = params_dict[packed_full] + # Navigate to the packed module to find shard sizes + module_path = packed_full.rsplit(".", 1)[0] # strip .weight/.bias + if ".base_layer" in module_path: + module_path = module_path.replace(".base_layer", "") + module = model + for attr in module_path.split("."): + module = getattr(module, attr, None) + if module is None: + break + # LoRA wrappers don't have output_sizes directly; + # check base_layer for the underlying parallel linear + if module is not None and not hasattr(module, "output_sizes"): + base = getattr(module, "base_layer", None) + if base is not None and hasattr(base, "output_sizes"): + module = base + if module is not None and hasattr(module, "output_sizes"): + tp_size = getattr(module, "tp_size", 1) + sizes = [s // tp_size for s in module.output_sizes] + offset = sum(sizes[:shard_idx]) + shard_size = sizes[shard_idx] + param.data[offset : offset + shard_size].copy_( + weight.to(param.dtype) + ) + return + + # Fallback: try load_weights (may work for non-stacked params) + logger.warning("Falling back to load_weights for param: %s", name) + model.load_weights(weights=[(name, weight)]) + + def update_named_param(self, name, dtype, shape): + """Override to use _direct_set_weight instead of load_weights.""" + if self.communicator is None: + raise RuntimeError("Communicator not initialized.") + + dtype = getattr(torch, dtype.split(".")[-1]) + weight = torch.empty(shape, dtype=dtype, device=self.device) + + if is_torch_xpu_available(): + self.communicator.broadcast(weight, root=self.client_rank) + self.communicator.barrier() + else: + self.communicator.broadcast(weight, src=self.client_rank) + self.communicator.group.barrier() + + self._direct_set_weight(name, weight) + + def batch_update_named_params(self, params_list: list[tuple[str, str, tuple]]): + """Receive and apply multiple weight tensors in sequence. + + Args: + params_list: List of (name, dtype_str, shape) tuples. + """ + if self.communicator is None: + raise RuntimeError("Communicator not initialized.") + + weights_to_load = [] + for name, dtype_str, shape in params_list: + dtype = getattr(torch, dtype_str.split(".")[-1]) + weight = torch.empty(shape, dtype=dtype, device=self.device) + + if is_torch_xpu_available(): + self.communicator.broadcast(weight, root=self.client_rank) + else: + self.communicator.broadcast(weight, src=self.client_rank) + + weights_to_load.append((name, weight)) + + # Single barrier after all broadcasts + if is_torch_xpu_available(): + self.communicator.barrier() + else: + self.communicator.group.barrier() + + # Load weights using direct set (handles stacked params) + for name, weight in weights_to_load: + self._direct_set_weight(name, weight) diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index ff96f44ce2..2d7c36f961 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -189,3 +189,125 @@ class TRLConfig(BaseModel): "'normalize_then_sum' (GDPO): normalizes each reward independently, then sums." }, ) + + # Async GRPO fields + use_data_producer: bool = Field( + default=False, + json_schema_extra={ + "description": "Use the GRPODataProducer protocol for online data generation." + }, + ) + async_prefetch: bool = Field( + default=False, + json_schema_extra={ + "description": "Generate rollouts in a background thread while training on the previous rollout." + }, + ) + prefetch_depth: int | None = Field( + default=None, + json_schema_extra={ + "description": "Number of rollouts to prefetch ahead of training." + }, + ) + vllm_sync_interval: int | None = Field( + default=None, + json_schema_extra={ + "description": "Sync model weights to vLLM every N optimizer steps (async mode only)." + }, + ) + streaming_partial_batch: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Score prompt groups incrementally instead of the full batch at once." + }, + ) + streaming_min_groups: int | None = Field( + default=None, + json_schema_extra={ + "description": "Minimum prompt groups to score per streaming chunk." + }, + ) + vllm_importance_sampling_correction: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Apply IS correction for distribution mismatch between vLLM and training model." + }, + ) + vllm_importance_sampling_mode: ( + Literal["token_truncate", "token_mask", "sequence_truncate", "sequence_mask"] + | None + ) = Field( + default=None, + json_schema_extra={ + "description": "IS mode: token_truncate, token_mask, sequence_truncate, or sequence_mask." + }, + ) + vllm_importance_sampling_cap: float | None = Field( + default=None, + json_schema_extra={"description": "Cap C for IS ratio clipping/masking."}, + ) + off_policy_mask_threshold: float | None = Field( + default=None, + json_schema_extra={ + "description": "KL threshold for off-policy sequence masking (OPSM). None = disabled." + }, + ) + use_bias_correction_kl: bool | None = Field( + default=None, + json_schema_extra={"description": "Apply IS correction to KL divergence term."}, + ) + + reward_num_workers: int = Field( + default=1, + json_schema_extra={ + "description": "Number of persistent subprocess workers for parallel reward computation. Each worker has its " + "own main thread so signal.alarm() (used by math_verify) works correctly. Work is sharded across " + "workers by prompt groups. Only used with use_data_producer=True and non-nn.Module reward functions." + }, + ) + replay_buffer_size: int = Field( + default=0, + json_schema_extra={ + "description": "[Experimental, disabled by default] Size of the replay buffer for storing high-signal rollout " + "groups. When > 0, groups with reward variance are cached and used to replace zero-signal groups " + "(where all rewards are identical). Set to 0 to disable. Only used with use_data_producer=True." + }, + ) + replay_recompute_logps: bool = Field( + default=True, + json_schema_extra={ + "description": "When True (default), recompute old_per_token_logps for replayed groups using the current " + "training model. This fixes the importance sampling mismatch that occurs when replaying stale data. " + "Only relevant when replay_buffer_size > 0." + }, + ) + reroll_start_fraction: float = Field( + default=1.0, + json_schema_extra={ + "description": "Fraction of total training steps after which deferred re-rolling begins. Zero-signal prompts " + "(where all rewards in a group are identical) are buffered and re-injected into later batches when the " + "model is more likely to solve them. Set to 1.0 to disable. Only used with use_data_producer=True." + }, + ) + reroll_max_groups: int = Field( + default=1, + json_schema_extra={ + "description": "Maximum number of prompt groups to replace with re-roll candidates per batch. Higher values " + "increase data utilization but reduce prompt diversity. Only used with use_data_producer=True." + }, + ) + skip_zero_advantage_batches: bool = Field( + default=True, + json_schema_extra={ + "description": "When True, skip gradient computation for micro-batches where all advantages are zero (no learning " + "signal). This avoids the forward/backward pass entirely when no learning signal is present. The step is " + "logged with skipped_zero_adv_batches=1 for monitoring." + }, + ) + vllm_lora_sync: bool = Field( + default=False, + json_schema_extra={ + "description": "Sync LoRA adapter to vLLM via filesystem instead of merging + NCCL broadcast. " + "Auto-selects vllm_serve_lora serve module. Syncs only LoRA adapter weights vs full merged model." + }, + ) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 3ba484fec8..50b30cd261 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -675,20 +675,6 @@ def check_lora_kernels_dora(cls, data): ) return data - @model_validator(mode="before") - @classmethod - def check_lora_kernels_rl(cls, data): - if ( - data.get("lora_mlp_kernel") - or data.get("lora_qkv_kernel") - or data.get("lora_o_kernel") - ) and data.get("rl"): - raise ValueError( - "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not " - "compatible with RL at the moment." - ) - return data - @model_validator(mode="before") @classmethod def check_lora_kernels_trust_remote_code(cls, data): diff --git a/src/axolotl/utils/schemas/vllm.py b/src/axolotl/utils/schemas/vllm.py index 518b8f62d4..c0aa48d668 100644 --- a/src/axolotl/utils/schemas/vllm.py +++ b/src/axolotl/utils/schemas/vllm.py @@ -57,3 +57,10 @@ class VllmConfig(BaseModel): default=None, json_schema_extra={"description": "Reasoning parser for VLLM"}, ) + serve_module: str | None = Field( + default=None, + json_schema_extra={ + "description": "Python module for vLLM serve script. Set to 'axolotl.scripts.vllm_serve_lora' " + "for native LoRA support, or leave None for default TRL serve." + }, + ) diff --git a/tests/core/test_async_grpo.py b/tests/core/test_async_grpo.py new file mode 100644 index 0000000000..eb83be1b62 --- /dev/null +++ b/tests/core/test_async_grpo.py @@ -0,0 +1,220 @@ +"""Unit tests for async GRPO""" + +import unittest +from unittest.mock import MagicMock + +import torch + + +class TestReplayBuffer(unittest.TestCase): + """Tests for ReplayBuffer edge cases.""" + + def test_add_noop_when_max_size_zero(self): + from axolotl.core.trainers.grpo.replay_buffer import ReplayBuffer + + buf = ReplayBuffer(max_size=0) + buf.add(1.0, {"data": "test"}) + self.assertEqual(len(buf), 0) + + def test_add_noop_when_max_size_negative(self): + from axolotl.core.trainers.grpo.replay_buffer import ReplayBuffer + + buf = ReplayBuffer(max_size=-1) + buf.add(1.0, {"data": "test"}) + self.assertEqual(len(buf), 0) + + def test_sample_returns_none_when_max_size_zero(self): + from axolotl.core.trainers.grpo.replay_buffer import ReplayBuffer + + buf = ReplayBuffer(max_size=0) + self.assertIsNone(buf.sample(1)) + + def test_sample_returns_none_when_empty(self): + from axolotl.core.trainers.grpo.replay_buffer import ReplayBuffer + + buf = ReplayBuffer(max_size=5) + self.assertIsNone(buf.sample(1)) + + def test_normal_add_and_sample(self): + from axolotl.core.trainers.grpo.replay_buffer import ReplayBuffer + + buf = ReplayBuffer(max_size=3) + buf.add(1.0, {"a": 1}) + buf.add(2.0, {"a": 2}) + buf.add(3.0, {"a": 3}) + self.assertEqual(len(buf), 3) + result = buf.sample(1) + self.assertIsNotNone(result) + self.assertEqual(len(result), 1) + + def test_replaces_lowest_when_full(self): + from axolotl.core.trainers.grpo.replay_buffer import ReplayBuffer + + buf = ReplayBuffer(max_size=2) + buf.add(1.0, {"a": 1}) + buf.add(2.0, {"a": 2}) + buf.add(3.0, {"a": 3}) # should replace score=1.0 + self.assertEqual(len(buf), 2) + scores = sorted(item[0] for item in buf._heap) + self.assertEqual(scores, [2.0, 3.0]) + + +class TestGRPOStrategyConflict(unittest.TestCase): + """Tests for sequence_parallel + async_grpo conflict detection.""" + + def test_raises_on_both_enabled(self): + from axolotl.core.trainers.grpo import GRPOStrategy + + with self.assertRaises(ValueError) as ctx: + GRPOStrategy.get_trainer_class(sequence_parallel=True, async_grpo=True) + self.assertIn("sequence_parallel", str(ctx.exception)) + self.assertIn("async_grpo", str(ctx.exception)) + + def test_sequence_parallel_only(self): + from axolotl.core.trainers.grpo import GRPOStrategy + from axolotl.core.trainers.grpo.trainer import ( + AxolotlGRPOSequenceParallelTrainer, + ) + + cls = GRPOStrategy.get_trainer_class(sequence_parallel=True, async_grpo=False) + self.assertIs(cls, AxolotlGRPOSequenceParallelTrainer) + + def test_async_only(self): + from axolotl.core.trainers.grpo import GRPOStrategy + from axolotl.core.trainers.grpo.trainer import AxolotlAsyncGRPOTrainer + + cls = GRPOStrategy.get_trainer_class(sequence_parallel=False, async_grpo=True) + self.assertIs(cls, AxolotlAsyncGRPOTrainer) + + def test_neither(self): + from axolotl.core.trainers.grpo import GRPOStrategy + from axolotl.core.trainers.grpo.trainer import AxolotlGRPOTrainer + + cls = GRPOStrategy.get_trainer_class(sequence_parallel=False, async_grpo=False) + self.assertIs(cls, AxolotlGRPOTrainer) + + +class TestDequantizeFP8TailBlocks(unittest.TestCase): + """Tests for FP8 dequantization with non-divisible dimensions.""" + + def test_exact_divisible_shape(self): + from axolotl.kernels.quantize import dequantize_fp8 + + W = torch.randn(256, 128, dtype=torch.bfloat16).to(torch.float8_e4m3fn) + scale_inv = torch.ones(2, 1, dtype=torch.bfloat16) + result = dequantize_fp8(W, scale_inv) + self.assertEqual(result.shape, (256, 128)) + self.assertEqual(result.dtype, torch.bfloat16) + + def test_non_divisible_rows(self): + from axolotl.kernels.quantize import dequantize_fp8 + + # 130 rows, scale has 2 blocks (block_size ~65 for exact div, but with + # tail blocks: first block=65 rows, second=65 rows, 130%2=0 actually). + # Use 131 rows with 2 scale blocks to trigger tail handling. + W = torch.ones(131, 128, dtype=torch.bfloat16).to(torch.float8_e4m3fn) + scale_inv = torch.tensor([[2.0], [3.0]], dtype=torch.bfloat16) + result = dequantize_fp8(W, scale_inv) + self.assertEqual(result.shape, (131, 128)) + self.assertEqual(result.dtype, torch.bfloat16) + + def test_non_divisible_cols(self): + from axolotl.kernels.quantize import dequantize_fp8 + + W = torch.ones(128, 200, dtype=torch.bfloat16).to(torch.float8_e4m3fn) + scale_inv = torch.ones(1, 2, dtype=torch.bfloat16) + result = dequantize_fp8(W, scale_inv) + self.assertEqual(result.shape, (128, 200)) + + def test_scalar_scale(self): + from axolotl.kernels.quantize import dequantize_fp8 + + W = torch.ones(64, 64, dtype=torch.bfloat16).to(torch.float8_e4m3fn) + scale_inv = torch.tensor(2.0, dtype=torch.bfloat16) + result = dequantize_fp8(W, scale_inv) + self.assertEqual(result.shape, (64, 64)) + + +class TestLoraFP8Guard(unittest.TestCase): + """Tests that get_lora_parameters only uses weight_scale_inv for FP8 weights.""" + + def test_non_fp8_weight_skips_scale_inv(self): + """Non-FP8 weight should NOT pick up weight_scale_inv as quant_state.""" + from axolotl.kernels.lora import get_lora_parameters + + proj = MagicMock() + proj.disable_adapters = True + base_layer = MagicMock(spec=[]) # empty spec to control attrs precisely + + # Use a real tensor for weight (bf16, no quant_state attr) + base_layer.weight = torch.randn(64, 64, dtype=torch.bfloat16) + base_layer.bias = None + base_layer.weight_scale_inv = torch.ones(1) # should NOT be used for bf16 + + proj.base_layer = base_layer + + W, b, quant_state, A, B, s = get_lora_parameters(proj) + # quant_state should be None since weight is bf16, not FP8 + self.assertIsNone(quant_state) + + def test_fp8_weight_uses_scale_inv(self): + """FP8 weight should pick up weight_scale_inv as quant_state.""" + from axolotl.kernels.lora import get_lora_parameters + + proj = MagicMock() + proj.disable_adapters = True + base_layer = MagicMock() + proj.base_layer = base_layer + + # FP8 weight + base_layer.weight = torch.randn(64, 64, dtype=torch.bfloat16).to( + torch.float8_e4m3fn + ) + base_layer.bias = None + scale_inv = torch.ones(1) + base_layer.weight_scale_inv = scale_inv + + W, b, quant_state, A, B, s = get_lora_parameters(proj) + self.assertIs(quant_state, scale_inv) + + +class TestValidateQuantPatchRestore(unittest.TestCase): + """Test that validate_quantization_for_training is restored after trainer creation.""" + + def test_patch_restored_on_success(self): + """Monkeypatch should be restored even after successful trainer creation.""" + import transformers.trainer as _trainer_module + + original = _trainer_module.validate_quantization_for_training + + # After the build() method runs, original should be restored. + # We can't easily test the full build(), but we can test the pattern. + _orig = _trainer_module.validate_quantization_for_training + _trainer_module.validate_quantization_for_training = lambda model: None + try: + pass # simulate trainer_cls() succeeding + finally: + _trainer_module.validate_quantization_for_training = _orig + + self.assertIs(_trainer_module.validate_quantization_for_training, original) + + def test_patch_restored_on_error(self): + """Monkeypatch should be restored even if trainer creation raises.""" + import transformers.trainer as _trainer_module + + original = _trainer_module.validate_quantization_for_training + + _orig = _trainer_module.validate_quantization_for_training + _trainer_module.validate_quantization_for_training = lambda model: None + try: + raise ValueError("test error") + except ValueError: + pass + finally: + _trainer_module.validate_quantization_for_training = _orig + + self.assertIs(_trainer_module.validate_quantization_for_training, original) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/monkeypatch/test_trl_vllm.py b/tests/monkeypatch/test_trl_vllm.py new file mode 100644 index 0000000000..755ac2577e --- /dev/null +++ b/tests/monkeypatch/test_trl_vllm.py @@ -0,0 +1,286 @@ +"""Unit tests for TRL vLLM monkeypatches. + +Tests: +- split_tensor_dict: scalar type preservation (int/float/bool) +- shuffle_sequence_dict: scalar type preservation +- extract_logprobs: NaN → 0.0 replacement +- VLLMClient.batch_update_named_params: method exists after patch +- VLLMGeneration: weight_sync_chunk_size attribute after patch +- Patch idempotency: applying patch twice doesn't break anything +""" + +import unittest +from dataclasses import dataclass +from unittest.mock import MagicMock + +import torch + + +class TestSplitTensorDict(unittest.TestCase): + """Tests for patched split_tensor_dict.""" + + def setUp(self): + from axolotl.monkeypatch.trainer.trl_vllm import _patched_split_tensor_dict + + self.split = _patched_split_tensor_dict + + def test_scalar_int_preserved(self): + d = {"a": torch.randn(4, 3), "count": 42} + chunks = self.split(d, 2) + self.assertEqual(len(chunks), 2) + self.assertEqual(chunks[0]["count"], 42) + self.assertEqual(chunks[1]["count"], 42) + + def test_scalar_float_preserved(self): + d = {"a": torch.randn(6, 2), "lr": 1e-5} + chunks = self.split(d, 3) + for c in chunks: + self.assertEqual(c["lr"], 1e-5) + + def test_scalar_bool_preserved(self): + d = {"a": torch.randn(4, 2), "flag": True} + chunks = self.split(d, 2) + for c in chunks: + self.assertTrue(c["flag"]) + + def test_none_preserved(self): + d = {"a": torch.randn(4, 2), "b": None} + chunks = self.split(d, 2) + for c in chunks: + self.assertIsNone(c["b"]) + + def test_tensor_split(self): + t = torch.arange(8).reshape(4, 2) + d = {"a": t, "n": 10} + chunks = self.split(d, 2) + self.assertEqual(chunks[0]["a"].shape, (2, 2)) + self.assertEqual(chunks[1]["a"].shape, (2, 2)) + torch.testing.assert_close(chunks[0]["a"], t[:2]) + torch.testing.assert_close(chunks[1]["a"], t[2:]) + + def test_0d_tensor_preserved(self): + d = {"a": torch.randn(4, 2), "scalar_t": torch.tensor(3.14)} + chunks = self.split(d, 2) + for c in chunks: + self.assertAlmostEqual(c["scalar_t"].item(), 3.14, places=5) + + def test_list_split(self): + d = {"a": torch.randn(4, 2), "names": ["a", "b", "c", "d"]} + chunks = self.split(d, 2) + self.assertEqual(chunks[0]["names"], ["a", "b"]) + self.assertEqual(chunks[1]["names"], ["c", "d"]) + + +class TestShuffleSequenceDict(unittest.TestCase): + """Tests for patched shuffle_sequence_dict.""" + + def setUp(self): + from axolotl.monkeypatch.trainer.trl_vllm import _patched_shuffle_sequence_dict + + self.shuffle = _patched_shuffle_sequence_dict + + def test_scalar_int_preserved(self): + d = {"a": torch.randn(4, 3), "count": 42} + result = self.shuffle(d) + self.assertEqual(result["count"], 42) + + def test_scalar_float_preserved(self): + d = {"a": torch.randn(4, 3), "lr": 1e-5} + result = self.shuffle(d) + self.assertEqual(result["lr"], 1e-5) + + def test_scalar_bool_preserved(self): + d = {"a": torch.randn(4, 3), "flag": False} + result = self.shuffle(d) + self.assertFalse(result["flag"]) + + def test_none_preserved(self): + d = {"a": torch.randn(4, 3), "b": None} + result = self.shuffle(d) + self.assertIsNone(result["b"]) + + def test_tensor_permuted(self): + torch.manual_seed(42) + t = torch.arange(4).float() + d = {"a": t} + result = self.shuffle(d) + # Same elements, possibly different order + self.assertEqual(sorted(result["a"].tolist()), sorted(t.tolist())) + self.assertEqual(result["a"].shape, t.shape) + + def test_list_permuted(self): + torch.manual_seed(42) + d = {"a": torch.randn(3, 2), "names": ["x", "y", "z"]} + result = self.shuffle(d) + self.assertEqual(sorted(result["names"]), ["x", "y", "z"]) + self.assertEqual(len(result["names"]), 3) + + def test_0d_tensor_preserved(self): + d = {"a": torch.randn(4, 2), "scalar_t": torch.tensor(3.14)} + result = self.shuffle(d) + self.assertAlmostEqual(result["scalar_t"].item(), 3.14, places=5) + + +class TestExtractLogprobs(unittest.TestCase): + """Tests for patched extract_logprobs (NaN → 0.0).""" + + def setUp(self): + from axolotl.monkeypatch.trainer.trl_vllm import _patched_extract_logprobs + + self.extract = _patched_extract_logprobs + + def _make_output(self, logprob_values): + """Create a mock vLLM RequestOutput with given logprob values.""" + + @dataclass + class LogprobItem: + logprob: float + rank: int + + @dataclass + class SeqOutput: + logprobs: list[dict[int, LogprobItem]] | None + + @dataclass + class RequestOutput: + outputs: list[SeqOutput] + + logprobs_list = [] + for vals in logprob_values: + lp_dict = {i: LogprobItem(logprob=v, rank=i) for i, v in enumerate(vals)} + logprobs_list.append(lp_dict) + + return RequestOutput(outputs=[SeqOutput(logprobs=logprobs_list)]) + + def test_nan_replaced_with_zero(self): + output = self._make_output([[float("nan"), 0.5], [-0.3, float("nan")]]) + logprobs, token_ids = self.extract([output]) + self.assertEqual(logprobs[0][0][0], 0.0) # NaN → 0.0 + self.assertEqual(logprobs[0][0][1], 0.5) + self.assertEqual(logprobs[0][1][0], -0.3) + self.assertEqual(logprobs[0][1][1], 0.0) # NaN → 0.0 + + def test_normal_values_preserved(self): + output = self._make_output([[-0.5, -1.2], [-0.1, -2.0]]) + logprobs, token_ids = self.extract([output]) + self.assertAlmostEqual(logprobs[0][0][0], -0.5) + self.assertAlmostEqual(logprobs[0][0][1], -1.2) + + def test_none_logprobs_returns_none(self): + @dataclass + class SeqOutput: + logprobs: None = None + + @dataclass + class RequestOutput: + outputs: list + + output = RequestOutput(outputs=[SeqOutput()]) + logprobs, token_ids = self.extract([output]) + self.assertIsNone(logprobs) + self.assertIsNone(token_ids) + + def test_token_ids_extracted(self): + output = self._make_output([[-0.5]]) + logprobs, token_ids = self.extract([output]) + self.assertEqual(token_ids[0][0], [0]) # token_id=0 from enumerate + + +class TestPatchApplication(unittest.TestCase): + """Tests for patch_trl_vllm() application.""" + + def test_batch_update_added_to_client(self): + from axolotl.monkeypatch.trainer.trl_vllm import patch_trl_vllm + + patch_trl_vllm() + from trl.generation.vllm_client import VLLMClient + + self.assertTrue(hasattr(VLLMClient, "batch_update_named_params")) + + def test_extract_logprobs_patched(self): + from axolotl.monkeypatch.trainer.trl_vllm import ( + _patched_extract_logprobs, + patch_trl_vllm, + ) + + patch_trl_vllm() + from trl.generation import vllm_generation + + self.assertIs(vllm_generation.extract_logprobs, _patched_extract_logprobs) + + def test_utils_patched(self): + from axolotl.monkeypatch.trainer.trl_vllm import ( + _patched_shuffle_sequence_dict, + _patched_split_tensor_dict, + patch_trl_vllm, + ) + + patch_trl_vllm() + import trl.trainer.utils + + self.assertIs(trl.trainer.utils.split_tensor_dict, _patched_split_tensor_dict) + self.assertIs( + trl.trainer.utils.shuffle_sequence_dict, _patched_shuffle_sequence_dict + ) + + def test_patch_idempotent(self): + from axolotl.monkeypatch.trainer.trl_vllm import patch_trl_vllm + + patch_trl_vllm() + patch_trl_vllm() # second call should not error + from trl.generation.vllm_client import VLLMClient + + self.assertTrue(hasattr(VLLMClient, "batch_update_named_params")) + + +class TestBatchUpdateChunking(unittest.TestCase): + """Tests for batch_update_named_params chunking logic.""" + + def test_no_chunk_single_batch(self): + from axolotl.monkeypatch.trainer.trl_vllm import _batch_update_named_params + + # Test that with chunk_size=None, all params go in one chunk + client = MagicMock() + client.base_url = "http://localhost:8000" + client.session.post.return_value = MagicMock(status_code=200) + client.communicator = MagicMock() + client.communicator.group = MagicMock() + client.rank = 0 + + params = [ + ("layer.0.weight", torch.randn(10, 10)), + ("layer.1.weight", torch.randn(10, 10)), + ] + _batch_update_named_params(client, params, chunk_size=None) + + # Should make exactly 1 HTTP call + self.assertEqual(client.session.post.call_count, 1) + + def test_chunk_splits_params(self): + from axolotl.monkeypatch.trainer.trl_vllm import _batch_update_named_params + + client = MagicMock() + client.base_url = "http://localhost:8000" + client.session.post.return_value = MagicMock(status_code=200) + client.communicator = MagicMock() + client.communicator.group = MagicMock() + client.rank = 0 + + params = [ + ("a", torch.randn(100)), # 100 elements + ("b", torch.randn(100)), # 100 elements + ("c", torch.randn(100)), # 100 elements + ] + _batch_update_named_params(client, params, chunk_size=150) + + # Should make 2 HTTP calls: [a,b] then [c] (100+100 > 150 triggers split) + # Actually: a=100 < 150, a+b=200 > 150 → chunk [a], then b=100 < 150, + # b+c=200 > 150 → chunk [b], then [c]. So 3 calls. + # Wait: first a added (100 < 150), then b: 100+100=200 > 150, so chunk=[a], + # new chunk starts with b (100 < 150), then c: 100+100=200 > 150, so chunk=[b], + # final chunk=[c]. 3 HTTP calls. + self.assertEqual(client.session.post.call_count, 3) + + +if __name__ == "__main__": + unittest.main() From f291ac029c630b6685566c610092b87bfe73b951 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 19 Mar 2026 01:18:47 -0400 Subject: [PATCH 1199/1405] fix for flaky tests in lora ops kernels w autotune (#3511) [skip ci] * fix for flaky tests in lora ops kernels w autotune * attempt 2 to fix --- .../kernels/autotune_collector.py | 2 +- .../test_scattermoe_autotune_telemetry.py | 49 +++++++++++++++---- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/axolotl/integrations/kernels/autotune_collector.py b/src/axolotl/integrations/kernels/autotune_collector.py index ef4111dcf0..bdb5e030e4 100644 --- a/src/axolotl/integrations/kernels/autotune_collector.py +++ b/src/axolotl/integrations/kernels/autotune_collector.py @@ -55,7 +55,7 @@ def _find_lora_ops_module() -> ModuleType | None: ``lora_ops`` and that has the ``_scatter2scatter_lora`` kernel attribute — that is the runtime copy with populated caches. """ - for name, module in sys.modules.items(): + for name, module in list(sys.modules.items()): if ( module is not None and "lora_ops" in name diff --git a/tests/integrations/test_scattermoe_autotune_telemetry.py b/tests/integrations/test_scattermoe_autotune_telemetry.py index 50ac567200..7050c0f4f5 100644 --- a/tests/integrations/test_scattermoe_autotune_telemetry.py +++ b/tests/integrations/test_scattermoe_autotune_telemetry.py @@ -15,6 +15,11 @@ # Simulate the hash-suffixed module name that LocalLayerRepository creates. _FAKE_MODULE_NAME = "scattermoe_lora_abc123.kernels.lora_ops" +# Patch target for _find_lora_ops_module inside the collector module. +_FIND_MODULE_PATH = ( + "axolotl.integrations.kernels.autotune_collector._find_lora_ops_module" +) + def _make_mock_config(kwargs, num_warps=4, num_stages=3): """Create a mock triton.Config-like object.""" @@ -41,19 +46,40 @@ def _make_mock_lora_ops( return mod +def _real_lora_ops_module_names(): + """Return sys.modules keys that match the lora_ops discovery pattern. + + Other tests in the same xdist worker may have loaded the *real* + lora_ops module. We need to temporarily hide those entries so the + discovery test finds only the mock we inject. + """ + return [ + name + for name, mod in list(sys.modules.items()) + if mod is not None + and "lora_ops" in name + and hasattr(mod, "_scatter2scatter_lora") + ] + + # ========================================================================= # TestAutotuneCollector # ========================================================================= class TestAutotuneCollector: - """Test ``collect_autotune_configs`` with mocked kernel objects.""" + """Test ``collect_autotune_configs`` with mocked kernel objects. + + Collection tests patch ``_find_lora_ops_module`` directly so they are + not affected by real ``lora_ops`` modules that other tests in the same + pytest-xdist worker may have loaded into ``sys.modules``. + """ def test_empty_cache_returns_empty_list(self): """When no kernel has been autotuned yet, return ``[]``.""" mock_lora_ops = _make_mock_lora_ops() - with patch.dict(sys.modules, {_FAKE_MODULE_NAME: mock_lora_ops}): + with patch(_FIND_MODULE_PATH, return_value=mock_lora_ops): from axolotl.integrations.kernels.autotune_collector import ( collect_autotune_configs, ) @@ -68,7 +94,7 @@ def test_populated_cache_returns_configs(self): ) mock_lora_ops = _make_mock_lora_ops(fwd_cache={(2048, 4096, 1024): cfg}) - with patch.dict(sys.modules, {_FAKE_MODULE_NAME: mock_lora_ops}): + with patch(_FIND_MODULE_PATH, return_value=mock_lora_ops): from axolotl.integrations.kernels.autotune_collector import ( collect_autotune_configs, ) @@ -94,7 +120,7 @@ def test_multiple_kernels_and_keys(self): dx_cache={(16, 256, 128): cfg_dx}, ) - with patch.dict(sys.modules, {_FAKE_MODULE_NAME: mock_lora_ops}): + with patch(_FIND_MODULE_PATH, return_value=mock_lora_ops): from axolotl.integrations.kernels.autotune_collector import ( collect_autotune_configs, ) @@ -113,7 +139,7 @@ def test_extra_key_elements_stored(self): mock_lora_ops = _make_mock_lora_ops(fwd_cache={cache_key: cfg}) - with patch.dict(sys.modules, {_FAKE_MODULE_NAME: mock_lora_ops}): + with patch(_FIND_MODULE_PATH, return_value=mock_lora_ops): from axolotl.integrations.kernels.autotune_collector import ( collect_autotune_configs, ) @@ -133,9 +159,8 @@ def test_no_module_in_sys_modules_returns_empty(self): collect_autotune_configs, ) - # Don't inject anything — the real lora_ops isn't loaded either - # (no triton on this machine), so _find_lora_ops_module returns None. - result = collect_autotune_configs() + with patch(_FIND_MODULE_PATH, return_value=None): + result = collect_autotune_configs() assert result == [] def test_finds_module_under_hash_suffixed_name(self): @@ -145,7 +170,13 @@ def test_finds_module_under_hash_suffixed_name(self): # Use a different hash to prove it's not hardcoded. alt_name = "scattermoe_lora_deadbeef.kernels.lora_ops" - with patch.dict(sys.modules, {alt_name: mock_lora_ops}): + + # Temporarily hide any real lora_ops modules that other tests in + # the same xdist worker may have loaded, so only our mock is found. + real_names = _real_lora_ops_module_names() + hide_patch = {name: None for name in real_names} + + with patch.dict(sys.modules, {alt_name: mock_lora_ops, **hide_patch}): from axolotl.integrations.kernels.autotune_collector import ( collect_autotune_configs, ) From 163bd4dd5a9dc6097d923cdd733cf5d1593c056c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 19 Mar 2026 02:02:43 -0400 Subject: [PATCH 1200/1405] use custom triton kernels for entropy from logits and selective softmax (#3510) * use custom triton kernels for entropy from logits and selective softmax * PR comments fixes * fix out of bounds, include tests, include benchmarks * chore: lint --- benchmarks/bench_entropy.py | 208 +++++++++ benchmarks/bench_selective_logsoftmax.py | 191 ++++++++ src/axolotl/loaders/patch_manager.py | 34 ++ src/axolotl/monkeypatch/trainer/__init__.py | 3 + src/axolotl/monkeypatch/trainer/utils.py | 429 +++++++++++++++++ tests/test_triton_kernels.py | 481 ++++++++++++++++++++ 6 files changed, 1346 insertions(+) create mode 100644 benchmarks/bench_entropy.py create mode 100644 benchmarks/bench_selective_logsoftmax.py create mode 100644 src/axolotl/monkeypatch/trainer/utils.py create mode 100644 tests/test_triton_kernels.py diff --git a/benchmarks/bench_entropy.py b/benchmarks/bench_entropy.py new file mode 100644 index 0000000000..95c7291b30 --- /dev/null +++ b/benchmarks/bench_entropy.py @@ -0,0 +1,208 @@ +"""Benchmark for entropy_from_logits Triton kernel vs original chunked implementation. + +Usage: CUDA_VISIBLE_DEVICES=0 python benchmarks/bench_entropy.py +""" + +import gc +import statistics + +import torch +import torch.nn.functional as F + +from axolotl.monkeypatch.trainer.utils import entropy_from_logits + +V = 151936 # Qwen vocab +WARMUP = 5 +BENCH_ITERS = 20 +MEM_ITERS = 10 + + +def entropy_from_logits_original(logits: torch.Tensor, chunk_size: int = 128): + """Original chunked implementation (reference).""" + original_shape = logits.shape[:-1] + num_classes = logits.shape[-1] + flat_logits = logits.reshape(-1, num_classes) + entropies = [] + for chunk in flat_logits.split(chunk_size, dim=0): + logps = F.log_softmax(chunk, dim=-1) + chunk_entropy = -(torch.exp(logps) * logps).sum(-1) + entropies.append(chunk_entropy) + return torch.cat(entropies, dim=0).reshape(original_shape) + + +def _clean_gpu(): + gc.collect() + torch.cuda.empty_cache() + torch.cuda.reset_peak_memory_stats() + torch.cuda.reset_accumulated_memory_stats() + torch.cuda.synchronize() + + +def profile_time(fn, logits, n_iters=BENCH_ITERS): + for _ in range(WARMUP): + out = fn(logits, chunk_size=128) + del out + torch.cuda.synchronize() + + times = [] + for _ in range(n_iters): + s = torch.cuda.Event(enable_timing=True) + e = torch.cuda.Event(enable_timing=True) + s.record() + out = fn(logits, chunk_size=128) + e.record() + torch.cuda.synchronize() + times.append(s.elapsed_time(e)) + del out + return times + + +def profile_memory(fn, logits, n_iters=MEM_ITERS): + for _ in range(WARMUP): + out = fn(logits, chunk_size=128) + del out + torch.cuda.synchronize() + + peaks = [] + for _ in range(n_iters): + _clean_gpu() + base = torch.cuda.max_memory_allocated() + out = fn(logits, chunk_size=128) + torch.cuda.synchronize() + peaks.append(torch.cuda.max_memory_allocated() - base) + del out + return [p / 1e6 for p in peaks] + + +def fmt(values, unit=""): + mean = statistics.mean(values) + std = statistics.stdev(values) if len(values) > 1 else 0.0 + return f"{mean:8.2f} ± {std:5.2f} {unit} [min={min(values):.2f}, max={max(values):.2f}]" + + +def benchmark_contiguous(): + print("=" * 60) + print( + f"CONTIGUOUS BENCHMARK (warmup={WARMUP}, time={BENCH_ITERS}, mem={MEM_ITERS})" + ) + print("=" * 60) + + configs = [ + (1, 2048), + (1, 8192), + (1, 16384), + (4, 4096), + (8, 2048), + (16, 2048), + (16, 4096), + ] + + for B, L in configs: + mem_gb = B * L * V * 2 / 1e9 + if mem_gb > 28: + print(f"\n skip B={B}, L={L} ({mem_gb:.1f} GB)") + continue + + N = B * L + print(f"\n{'─' * 60}") + print(f"B={B:2d}, L={L:5d} ({N:6d} rows, logits {mem_gb:.2f} GB)") + print(f"{'─' * 60}") + + torch.manual_seed(42) + logits = torch.randn(B, L, V, device="cuda", dtype=torch.bfloat16) + + t_orig = profile_time(entropy_from_logits_original, logits) + t_triton = profile_time(entropy_from_logits, logits) + orig_mean = statistics.mean(t_orig) + triton_mean = statistics.mean(t_triton) + + print(" TIME (ms):") + print(f" original: {fmt(t_orig, 'ms')}") + print(f" triton: {fmt(t_triton, 'ms')}") + print(f" speedup: {orig_mean / triton_mean:.2f}x") + + m_orig = profile_memory(entropy_from_logits_original, logits) + m_triton = profile_memory(entropy_from_logits, logits) + orig_peak = statistics.mean(m_orig) + triton_peak = statistics.mean(m_triton) + + print(" MEMORY (peak overhead):") + print(f" original: {fmt(m_orig, 'MB')}") + print(f" triton: {fmt(m_triton, 'MB')}") + print(f" saved: {orig_peak - triton_peak:.1f} MB") + + del logits + _clean_gpu() + + +def benchmark_noncontiguous(): + print("\n" + "=" * 60) + print( + f"NON-CONTIGUOUS BENCHMARK (warmup={WARMUP}, time={BENCH_ITERS}, mem={MEM_ITERS})" + ) + print("=" * 60) + + configs = [ + (4, 2048, "transpose"), + (4, 8192, "transpose"), + (8, 2048, "transpose"), + (4, 4096, "slice_batch"), + ] + + for B, L, method in configs: + torch.manual_seed(42) + + if method == "transpose": + raw = torch.randn(L, B, V, device="cuda", dtype=torch.bfloat16) + logits_nc = raw.transpose(0, 1) + raw_gb = L * B * V * 2 / 1e9 + elif method == "slice_batch": + raw = torch.randn(B * 2, L, V, device="cuda", dtype=torch.bfloat16) + logits_nc = raw[::2] + raw_gb = B * 2 * L * V * 2 / 1e9 + else: + continue + + if raw_gb > 28: + print(f"\n skip B={B}, L={L}, {method} ({raw_gb:.1f} GB)") + del raw, logits_nc + torch.cuda.empty_cache() + continue + + N = B * L + print(f"\n{'─' * 60}") + print(f"B={B}, L={L} {method} ({N} rows, raw {raw_gb:.2f} GB)") + print(f"{'─' * 60}") + + def original_with_copy(logits, chunk_size=128): + return entropy_from_logits_original( + logits.contiguous(), chunk_size=chunk_size + ) + + t_orig = profile_time(original_with_copy, logits_nc) + t_triton = profile_time(entropy_from_logits, logits_nc) + orig_mean = statistics.mean(t_orig) + triton_mean = statistics.mean(t_triton) + + print(" TIME (ms):") + print(f" orig+copy: {fmt(t_orig, 'ms')}") + print(f" triton-strided:{fmt(t_triton, 'ms')}") + print(f" speedup: {orig_mean / triton_mean:.2f}x") + + m_orig = profile_memory(original_with_copy, logits_nc) + m_triton = profile_memory(entropy_from_logits, logits_nc) + orig_peak = statistics.mean(m_orig) + triton_peak = statistics.mean(m_triton) + + print(" MEMORY (peak overhead):") + print(f" orig+copy: {fmt(m_orig, 'MB')}") + print(f" triton-strided:{fmt(m_triton, 'MB')}") + print(f" saved: {orig_peak - triton_peak:.1f} MB") + + del raw, logits_nc + _clean_gpu() + + +if __name__ == "__main__": + benchmark_contiguous() + benchmark_noncontiguous() diff --git a/benchmarks/bench_selective_logsoftmax.py b/benchmarks/bench_selective_logsoftmax.py new file mode 100644 index 0000000000..b8f517e4be --- /dev/null +++ b/benchmarks/bench_selective_logsoftmax.py @@ -0,0 +1,191 @@ +"""Benchmark for selective_log_softmax Triton kernel vs original implementation. + +Usage: CUDA_VISIBLE_DEVICES=0 python benchmarks/bench_selective_logsoftmax.py +""" + +import gc +import statistics + +import torch + +from axolotl.monkeypatch.trainer.utils import ( + selective_log_softmax, + selective_log_softmax_original, +) + +V = 151936 # Qwen vocab +WARMUP = 5 +BENCH_ITERS = 20 +MEM_ITERS = 10 + + +def _clean_gpu(): + gc.collect() + torch.cuda.empty_cache() + torch.cuda.reset_peak_memory_stats() + torch.cuda.reset_accumulated_memory_stats() + torch.cuda.synchronize() + + +def profile_time(fn, args, n_iters=BENCH_ITERS): + for _ in range(WARMUP): + fn(*args) + torch.cuda.synchronize() + + times = [] + for _ in range(n_iters): + s = torch.cuda.Event(enable_timing=True) + e = torch.cuda.Event(enable_timing=True) + s.record() + fn(*args) + e.record() + torch.cuda.synchronize() + times.append(s.elapsed_time(e)) + return times + + +def profile_memory(fn, args, n_iters=MEM_ITERS): + for _ in range(WARMUP): + out = fn(*args) + del out + torch.cuda.synchronize() + + peaks = [] + for _ in range(n_iters): + _clean_gpu() + base = torch.cuda.max_memory_allocated() + out = fn(*args) + torch.cuda.synchronize() + peaks.append(torch.cuda.max_memory_allocated() - base) + del out + return [p / 1e6 for p in peaks] + + +def fmt(values, unit=""): + mean = statistics.mean(values) + std = statistics.stdev(values) if len(values) > 1 else 0.0 + return f"{mean:8.2f} ± {std:5.2f} {unit} [min={min(values):.2f}, max={max(values):.2f}]" + + +def benchmark_forward(): + print("=" * 60) + print(f"FORWARD BENCHMARK (warmup={WARMUP}, time={BENCH_ITERS}, mem={MEM_ITERS})") + print("=" * 60) + + configs = [ + (1, 2048), + (1, 8192), + (4, 4096), + (8, 2048), + (16, 2048), + (16, 4096), + ] + + for B, L in configs: + mem_gb = B * L * V * 2 / 1e9 + if mem_gb > 28: + print(f"\n skip B={B}, L={L} ({mem_gb:.1f} GB)") + continue + + N = B * L + print(f"\n{'─' * 60}") + print(f"B={B:2d}, L={L:5d} ({N:6d} rows, logits {mem_gb:.2f} GB)") + print(f"{'─' * 60}") + + torch.manual_seed(42) + logits = torch.randn(B, L, V, device="cuda", dtype=torch.bfloat16) + index = torch.randint(0, V, (B, L), device="cuda") + + t_orig = profile_time(selective_log_softmax_original, (logits, index)) + t_triton = profile_time(selective_log_softmax, (logits, index)) + orig_mean = statistics.mean(t_orig) + triton_mean = statistics.mean(t_triton) + + print(" TIME (ms):") + print(f" original: {fmt(t_orig, 'ms')}") + print(f" triton: {fmt(t_triton, 'ms')}") + print(f" speedup: {orig_mean / triton_mean:.2f}x") + + m_orig = profile_memory(selective_log_softmax_original, (logits, index)) + m_triton = profile_memory(selective_log_softmax, (logits, index)) + orig_peak = statistics.mean(m_orig) + triton_peak = statistics.mean(m_triton) + + print(" MEMORY (peak overhead):") + print(f" original: {fmt(m_orig, 'MB')}") + print(f" triton: {fmt(m_triton, 'MB')}") + print(f" saved: {orig_peak - triton_peak:.1f} MB") + + del logits, index + _clean_gpu() + + +def benchmark_backward(): + print("\n" + "=" * 60) + print(f"FWD+BWD BENCHMARK (warmup={WARMUP}, time={BENCH_ITERS}, mem={MEM_ITERS})") + print("=" * 60) + + configs = [ + (1, 2048), + (1, 8192), + (4, 4096), + (8, 2048), + (16, 2048), + (16, 4096), + ] + + def fwd_bwd_original(logits, index): + logits.grad = None + out = selective_log_softmax_original(logits, index) + out.sum().backward() + + def fwd_bwd_triton(logits, index): + logits.grad = None + out = selective_log_softmax(logits, index) + out.sum().backward() + + for B, L in configs: + mem_gb = B * L * V * 2 / 1e9 + if mem_gb > 20: + print(f"\n skip B={B}, L={L} ({mem_gb:.1f} GB, need room for grads)") + continue + + N = B * L + print(f"\n{'─' * 60}") + print(f"B={B:2d}, L={L:5d} ({N:6d} rows, logits {mem_gb:.2f} GB)") + print(f"{'─' * 60}") + + torch.manual_seed(42) + logits_orig = torch.randn( + B, L, V, device="cuda", dtype=torch.bfloat16, requires_grad=True + ) + logits_tri = logits_orig.detach().clone().requires_grad_(True) + index = torch.randint(0, V, (B, L), device="cuda") + + t_orig = profile_time(fwd_bwd_original, (logits_orig, index)) + t_triton = profile_time(fwd_bwd_triton, (logits_tri, index)) + orig_mean = statistics.mean(t_orig) + triton_mean = statistics.mean(t_triton) + + print(" FWD+BWD TIME (ms):") + print(f" original: {fmt(t_orig, 'ms')}") + print(f" triton: {fmt(t_triton, 'ms')}") + print(f" speedup: {orig_mean / triton_mean:.2f}x") + + m_orig = profile_memory(fwd_bwd_original, (logits_orig, index)) + m_triton = profile_memory(fwd_bwd_triton, (logits_tri, index)) + orig_peak = statistics.mean(m_orig) + triton_peak = statistics.mean(m_triton) + + print(" FWD+BWD MEMORY (peak overhead):") + print(f" original: {fmt(m_orig, 'MB')}") + print(f" triton: {fmt(m_triton, 'MB')}") + print(f" saved: {orig_peak - triton_peak:.1f} MB") + + del logits_orig, logits_tri, index + _clean_gpu() + + +if __name__ == "__main__": + benchmark_forward() + benchmark_backward() diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 205e32e6ff..bddd388e4d 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -117,6 +117,7 @@ def apply_pre_model_load_patches(self): self._apply_voxtral_patches() self._apply_apertus_patches() self._apply_trl_vllm_patches() + self._apply_trl_trainer_utils_patches() def apply_post_plugin_pre_model_load_patches(self): """Apply post plugin-pre_model_load load patches based on config.""" @@ -679,6 +680,39 @@ def _apply_trl_vllm_patches(self): patch_trl_vllm() + def _apply_trl_trainer_utils_patches(self): + """Replace trl.trainer.utils.{selective_log_softmax, entropy_from_logits} with Triton kernels.""" + if not self.cfg.rl: + return + + try: + from axolotl.monkeypatch.trainer.utils import ( + entropy_from_logits, + selective_log_softmax, + ) + except (ImportError, ModuleNotFoundError): + LOG.warning("Triton not available — skipping trl.trainer.utils patches") + return + + import trl.trainer.utils + + # Guard against repeated calls: only stash the original if trl still + # points at its own implementation (not our wrapper). + if trl.trainer.utils.selective_log_softmax is not selective_log_softmax: + from axolotl.monkeypatch.trainer import utils as _axolotl_trainer_utils + + _axolotl_trainer_utils.selective_log_softmax_original = ( + trl.trainer.utils.selective_log_softmax + ) + trl.trainer.utils.selective_log_softmax = selective_log_softmax + + if trl.trainer.utils.entropy_from_logits is not entropy_from_logits: + trl.trainer.utils.entropy_from_logits = entropy_from_logits + + LOG.info( + "Patched trl.trainer.utils with Triton selective_log_softmax and entropy_from_logits" + ) + def _apply_scaling_softmax_patch(self, model: PreTrainedModel): """Apply Scaling Softmax (SSMax) patch. Ref: https://arxiv.org/abs/2501.19399""" if self.cfg.scaling_softmax: diff --git a/src/axolotl/monkeypatch/trainer/__init__.py b/src/axolotl/monkeypatch/trainer/__init__.py index e69de29bb2..27edc63c34 100644 --- a/src/axolotl/monkeypatch/trainer/__init__.py +++ b/src/axolotl/monkeypatch/trainer/__init__.py @@ -0,0 +1,3 @@ +from .utils import entropy_from_logits, selective_log_softmax + +__all__ = ["entropy_from_logits", "selective_log_softmax"] diff --git a/src/axolotl/monkeypatch/trainer/utils.py b/src/axolotl/monkeypatch/trainer/utils.py new file mode 100644 index 0000000000..467f50a5a4 --- /dev/null +++ b/src/axolotl/monkeypatch/trainer/utils.py @@ -0,0 +1,429 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import torch.nn.functional as F +import triton +import triton.language as tl + + +@triton.jit +def _entropy_online_kernel( + logits_ptr, + output_ptr, + stride_row, + V: tl.constexpr, + BLOCK_V: tl.constexpr, +): + """Online entropy: single pass with running max correction.""" + row = tl.program_id(0) + row_ptr = logits_ptr + tl.cast(row, tl.int64) * stride_row + + running_max = tl.full([], float("-inf"), dtype=tl.float32) + running_sum_exp = tl.full([], 0.0, dtype=tl.float32) + running_weighted = tl.full([], 0.0, dtype=tl.float32) + + for v_start in range(0, V, BLOCK_V): + offs = v_start + tl.arange(0, BLOCK_V) + mask = offs < V + x = tl.load(row_ptr + offs, mask=mask, other=float("-inf")).to(tl.float32) + + block_max = tl.max(x, axis=0) + new_max = tl.maximum(running_max, block_max) + + correction = tl.exp(running_max - new_max) + running_sum_exp = running_sum_exp * correction + running_weighted = running_weighted * correction + + exp_x = tl.exp(x - new_max) + exp_x = tl.where(mask, exp_x, 0.0) + x = tl.where(mask, x, 0.0) + running_sum_exp += tl.sum(exp_x, axis=0) + running_weighted += tl.sum(exp_x * x, axis=0) + + running_max = new_max + + entropy = tl.log(running_sum_exp) + running_max - running_weighted / running_sum_exp + tl.store(output_ptr + row, entropy) + + +@triton.jit +def _entropy_online_kernel_strided( + logits_ptr, + output_ptr, + stride_outer, + stride_inner, + n_inner, + row_offset, + V: tl.constexpr, + BLOCK_V: tl.constexpr, +): + """Online entropy for non-contiguous 3D (B, L, V) tensors.""" + local_row = tl.program_id(0) + row = local_row + row_offset + outer_idx = row // n_inner + inner_idx = row % n_inner + off = outer_idx.to(tl.int64) * stride_outer + inner_idx.to(tl.int64) * stride_inner + row_ptr = logits_ptr + off + + running_max = tl.full([], float("-inf"), dtype=tl.float32) + running_sum_exp = tl.full([], 0.0, dtype=tl.float32) + running_weighted = tl.full([], 0.0, dtype=tl.float32) + + for v_start in range(0, V, BLOCK_V): + offs = v_start + tl.arange(0, BLOCK_V) + mask = offs < V + x = tl.load(row_ptr + offs, mask=mask, other=float("-inf")).to(tl.float32) + + block_max = tl.max(x, axis=0) + new_max = tl.maximum(running_max, block_max) + + correction = tl.exp(running_max - new_max) + running_sum_exp = running_sum_exp * correction + running_weighted = running_weighted * correction + + exp_x = tl.exp(x - new_max) + exp_x = tl.where(mask, exp_x, 0.0) + x = tl.where(mask, x, 0.0) + running_sum_exp += tl.sum(exp_x, axis=0) + running_weighted += tl.sum(exp_x * x, axis=0) + + running_max = new_max + + entropy = tl.log(running_sum_exp) + running_max - running_weighted / running_sum_exp + tl.store(output_ptr + local_row, entropy) + + +def entropy_from_logits(logits: torch.Tensor, chunk_size: int = 128) -> torch.Tensor: + """Triton-fused entropy (online single-pass). Handles non-contiguous tensors without copying.""" + original_shape = logits.shape[:-1] + V = logits.shape[-1] + N = 1 + for s in original_shape: + N *= s + + if not logits.is_cuda: + # CPU fallback: stable entropy via log_softmax + logp = F.log_softmax(logits.float(), dim=-1) + ent = -(logp.exp() * logp).sum(dim=-1) + return ent.to(logits.dtype).reshape(original_shape) + + output = torch.empty(N, device=logits.device, dtype=torch.float32) + + BLOCK_V = 4096 + MAX_GRID_CONTIG = 8192 + MAX_GRID_STRIDED = 2048 + + # Vocab (last) dim must be contiguous for coalesced loads + if logits.stride(-1) != 1: + logits = logits.contiguous() + + if logits.is_contiguous(): + flat_logits = logits.reshape(-1, V) + stride = flat_logits.stride(0) + for start in range(0, N, MAX_GRID_CONTIG): + n_rows = min(MAX_GRID_CONTIG, N - start) + _entropy_online_kernel[(n_rows,)]( + flat_logits[start], output[start], stride, V=V, BLOCK_V=BLOCK_V + ) + elif logits.ndim == 3: + stride_outer = logits.stride(0) + stride_inner = logits.stride(1) + n_inner = logits.shape[1] + for start in range(0, N, MAX_GRID_STRIDED): + n_rows = min(MAX_GRID_STRIDED, N - start) + _entropy_online_kernel_strided[(n_rows,)]( + logits, + output[start], + stride_outer, + stride_inner, + n_inner, + start, + V=V, + BLOCK_V=BLOCK_V, + ) + else: + logits = logits.contiguous() + flat_logits = logits.reshape(-1, V) + stride = flat_logits.stride(0) + for start in range(0, N, MAX_GRID_CONTIG): + n_rows = min(MAX_GRID_CONTIG, N - start) + _entropy_online_kernel[(n_rows,)]( + flat_logits[start], output[start], stride, V=V, BLOCK_V=BLOCK_V + ) + + return output.to(logits.dtype).reshape(original_shape) + + +# --------------------------------------------------------------------------- +# selective_log_softmax — fused forward + backward Triton kernels +# --------------------------------------------------------------------------- + + +def selective_log_softmax_original(logits, index) -> torch.Tensor: + """Original selective_log_softmax (reference/fallback).""" + squeeze = index.ndim == logits.ndim - 1 + if squeeze: + index = index.unsqueeze(-1) + + if logits.dtype in [torch.float32, torch.float64]: + selected_logits = torch.gather(logits, dim=-1, index=index) + logsumexp_values = torch.stack([torch.logsumexp(lg, dim=-1) for lg in logits]) + per_token_logps = selected_logits - logsumexp_values.unsqueeze(-1) + else: + per_token_logps = [] + for row_logits, row_labels in zip(logits, index, strict=True): + row_logps = F.log_softmax(row_logits, dim=-1) + row_per_token_logps = row_logps.gather(dim=-1, index=row_labels) + per_token_logps.append(row_per_token_logps) + per_token_logps = torch.stack(per_token_logps) + + if squeeze: + per_token_logps = per_token_logps.squeeze(-1) + + return per_token_logps + + +@triton.jit +def _selective_logsoftmax_fwd_kernel( + logits_ptr, + index_ptr, + output_ptr, + logsumexp_ptr, + stride_logits_row, + stride_index_row, + stride_output_row, + actual_K, + K_BLOCK: tl.constexpr, + V: tl.constexpr, + BLOCK_V: tl.constexpr, +): + """Forward: online logsumexp + gather. Saves logsumexp for backward.""" + row = tl.program_id(0) + logits_row_ptr = logits_ptr + tl.cast(row, tl.int64) * stride_logits_row + + # Online logsumexp + running_max = tl.full([], float("-inf"), dtype=tl.float32) + running_sum_exp = tl.full([], 0.0, dtype=tl.float32) + + for v_start in range(0, V, BLOCK_V): + offs = v_start + tl.arange(0, BLOCK_V) + mask = offs < V + x = tl.load(logits_row_ptr + offs, mask=mask, other=float("-inf")).to( + tl.float32 + ) + + block_max = tl.max(x, axis=0) + new_max = tl.maximum(running_max, block_max) + running_sum_exp = running_sum_exp * tl.exp(running_max - new_max) + + exp_x = tl.exp(x - new_max) + exp_x = tl.where(mask, exp_x, 0.0) + running_sum_exp += tl.sum(exp_x, axis=0) + running_max = new_max + + lse = tl.log(running_sum_exp) + running_max + tl.store(logsumexp_ptr + row, lse) + + # Gather and subtract + index_row_ptr = index_ptr + tl.cast(row, tl.int64) * stride_index_row + output_row_ptr = output_ptr + tl.cast(row, tl.int64) * stride_output_row + + k_offs = tl.arange(0, K_BLOCK) + k_mask = k_offs < actual_K + indices = tl.load(index_row_ptr + k_offs, mask=k_mask, other=0).to(tl.int64) + valid_mask = k_mask & (indices >= 0) & (indices < V) + safe_indices = tl.where(valid_mask, indices, 0) + selected = tl.load(logits_row_ptr + safe_indices, mask=valid_mask, other=0.0).to( + tl.float32 + ) + tl.store(output_row_ptr + k_offs, selected - lse, mask=valid_mask) + + +@triton.jit +def _selective_logsoftmax_bwd_kernel( + grad_output_ptr, + logits_ptr, + index_ptr, + logsumexp_ptr, + grad_logits_ptr, + stride_grad_out_row, + stride_logits_row, + stride_index_row, + stride_grad_logits_row, + actual_K, + K_BLOCK: tl.constexpr, + V: tl.constexpr, + BLOCK_V: tl.constexpr, +): + """Backward: d_logits[j] = -softmax(x)[j] * sum(grad_out) + (grad_out[k] if j == index[k]). + + Single fused pass over V. For each tile, computes the base gradient and adds + scatter contributions inline by checking which indices fall in the current tile. + No separate scatter pass — no read-after-write issues. + """ + row = tl.program_id(0) + logits_row_ptr = logits_ptr + tl.cast(row, tl.int64) * stride_logits_row + grad_logits_row_ptr = ( + grad_logits_ptr + tl.cast(row, tl.int64) * stride_grad_logits_row + ) + grad_out_row_ptr = grad_output_ptr + tl.cast(row, tl.int64) * stride_grad_out_row + index_row_ptr = index_ptr + tl.cast(row, tl.int64) * stride_index_row + + lse = tl.load(logsumexp_ptr + row).to(tl.float32) + + # Load grad_output and indices (K_BLOCK elements, masked) + k_offs = tl.arange(0, K_BLOCK) + k_mask = k_offs < actual_K + grad_out = tl.load(grad_out_row_ptr + k_offs, mask=k_mask, other=0.0).to(tl.float32) + indices = tl.load( + index_row_ptr + k_offs, mask=k_mask, other=-1 + ) # -1 = never matches + valid_mask = k_mask & (indices >= 0) & (indices < V) + grad_out = tl.where(valid_mask, grad_out, 0.0) + indices = tl.where(valid_mask, indices, -1) + grad_sum = tl.sum(grad_out, axis=0) + + # Fused pass: for each tile, compute -softmax * grad_sum + scatter + for v_start in range(0, V, BLOCK_V): + offs = v_start + tl.arange(0, BLOCK_V) # [BLOCK_V] + mask = offs < V + x = tl.load(logits_row_ptr + offs, mask=mask, other=0.0).to(tl.float32) + softmax_j = tl.exp(x - lse) + softmax_j = tl.where(mask, softmax_j, 0.0) + grad_j = -softmax_j * grad_sum + + # Scatter: check which selected indices fall in this tile + # offs: [BLOCK_V], indices: [K_BLOCK] + # Broadcast: offs[:, None] == indices[None, :] → [BLOCK_V, K_BLOCK] + match = offs[:, None] == indices[None, :] # [BLOCK_V, K_BLOCK] + # Sum grad_out contributions: for each position j, sum grad_out[k] where index[k]==j + scatter_contrib = tl.sum( + tl.where(match, grad_out[None, :], 0.0), axis=1 + ) # [BLOCK_V] + grad_j += scatter_contrib + + tl.store(grad_logits_row_ptr + offs, grad_j, mask=mask) + + +class _SelectiveLogSoftmaxTriton(torch.autograd.Function): + @staticmethod + def forward(ctx, flat_logits, flat_index, K, K_BLOCK, V, BLOCK_V, MAX_GRID): + N = flat_logits.shape[0] + output = torch.empty(N, K_BLOCK, device=flat_logits.device, dtype=torch.float32) + logsumexp = torch.empty(N, device=flat_logits.device, dtype=torch.float32) + + for start in range(0, N, MAX_GRID): + n_rows = min(MAX_GRID, N - start) + _selective_logsoftmax_fwd_kernel[(n_rows,)]( + flat_logits[start], + flat_index[start], + output[start], + logsumexp[start], + flat_logits.stride(0), + flat_index.stride(0), + output.stride(0), + K, + K_BLOCK=K_BLOCK, + V=V, + BLOCK_V=BLOCK_V, + ) + + ctx.save_for_backward(flat_logits, flat_index, logsumexp) + ctx.K = K + ctx.K_BLOCK = K_BLOCK + ctx.V = V + ctx.BLOCK_V = BLOCK_V + ctx.MAX_GRID = MAX_GRID + return output + + @staticmethod + def backward(ctx, grad_output): + flat_logits, flat_index, logsumexp = ctx.saved_tensors + K, K_BLOCK, V, BLOCK_V, MAX_GRID = ( + ctx.K, + ctx.K_BLOCK, + ctx.V, + ctx.BLOCK_V, + ctx.MAX_GRID, + ) + N = flat_logits.shape[0] + + grad_logits = torch.empty_like(flat_logits) + + # grad_output may have K_BLOCK cols; backward kernel reads actual_K + grad_output_contig = grad_output.contiguous() + + for start in range(0, N, MAX_GRID): + n_rows = min(MAX_GRID, N - start) + _selective_logsoftmax_bwd_kernel[(n_rows,)]( + grad_output_contig[start], + flat_logits[start], + flat_index[start], + logsumexp[start], + grad_logits[start], + grad_output_contig.stride(0), + flat_logits.stride(0), + flat_index.stride(0), + grad_logits.stride(0), + K, + K_BLOCK=K_BLOCK, + V=V, + BLOCK_V=BLOCK_V, + ) + + # Return grads for: flat_logits, flat_index, K, K_BLOCK, V, BLOCK_V, MAX_GRID + return grad_logits, None, None, None, None, None, None + + +def selective_log_softmax(logits, index) -> torch.Tensor: + """ + Fused selective_log_softmax with Triton forward+backward kernels. + + Equivalent to: torch.gather(logits.log_softmax(-1), dim=-1, index=index) + """ + squeeze = index.ndim == logits.ndim - 1 + if squeeze: + index = index.unsqueeze(-1) + + if not logits.is_cuda or logits.dtype == torch.float64: + # Triton kernel computes in float32; fall back for float64 and CPU + return selective_log_softmax_original( + logits, index.squeeze(-1) if squeeze else index + ) + + V = logits.shape[-1] + K = index.shape[-1] + original_index_shape = index.shape + + flat_logits = logits.reshape(-1, V).contiguous() + flat_index = index.reshape(-1, K).contiguous() + + BLOCK_V = 4096 + MAX_GRID = 8192 + K_BLOCK = max(1, triton.next_power_of_2(K)) + + output = _SelectiveLogSoftmaxTriton.apply( + flat_logits, flat_index, K, K_BLOCK, V, BLOCK_V, MAX_GRID + ) + + if K_BLOCK != K: + output = output[:, :K] + + per_token_logps = output.to(logits.dtype).reshape(original_index_shape) + + if squeeze: + per_token_logps = per_token_logps.squeeze(-1) + + return per_token_logps diff --git a/tests/test_triton_kernels.py b/tests/test_triton_kernels.py new file mode 100644 index 0000000000..ffc8de865d --- /dev/null +++ b/tests/test_triton_kernels.py @@ -0,0 +1,481 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +"""Unit tests for Triton kernels: entropy_from_logits and selective_log_softmax. + +Adapted from harness/test_entropy.py and harness/test_selective_logsoftmax.py +into proper pytest tests, plus new OOB index safety tests. +""" + +import math + +import pytest +import torch +import torch.nn.functional as F + +pytestmark = pytest.mark.skipif( + not torch.cuda.is_available(), reason="CUDA required for Triton kernels" +) + + +# --------------------------------------------------------------------------- +# Reference implementations +# --------------------------------------------------------------------------- + + +def _ref_entropy(logits): + """Reference entropy via log_softmax (numerically stable).""" + logp = F.log_softmax(logits.float(), dim=-1) + return -(logp.exp() * logp).sum(dim=-1) + + +def _ref_selective_log_softmax(logits, index): + """Reference selective log softmax via PyTorch gather.""" + squeeze = index.ndim == logits.ndim - 1 + if squeeze: + index = index.unsqueeze(-1) + log_probs = F.log_softmax(logits.float(), dim=-1) + result = torch.gather(log_probs, dim=-1, index=index) + if squeeze: + result = result.squeeze(-1) + return result + + +# --------------------------------------------------------------------------- +# entropy_from_logits +# --------------------------------------------------------------------------- + + +class TestEntropyFromLogits: + @pytest.mark.parametrize( + "B,L", + [ + (1, 128), + (1, 2048), + (4, 512), + (8, 256), + (1, 1), + ], + ) + def test_correctness_various_shapes(self, B, L): + from axolotl.monkeypatch.trainer.utils import entropy_from_logits + + V = 1024 + torch.manual_seed(42) + logits = torch.randn(B, L, V, device="cuda", dtype=torch.float32) + result = entropy_from_logits(logits) + expected = _ref_entropy(logits) + assert result.shape == (B, L) + torch.testing.assert_close(result, expected, atol=1e-4, rtol=1e-4) + + def test_2d_input(self): + from axolotl.monkeypatch.trainer.utils import entropy_from_logits + + logits = torch.randn(16, 256, device="cuda", dtype=torch.float32) + result = entropy_from_logits(logits) + expected = _ref_entropy(logits) + assert result.shape == (16,) + torch.testing.assert_close(result, expected, atol=1e-4, rtol=1e-4) + + def test_large_vocab(self): + from axolotl.monkeypatch.trainer.utils import entropy_from_logits + + V = 32000 + logits = torch.randn(2, V, device="cuda", dtype=torch.float32) + result = entropy_from_logits(logits) + expected = _ref_entropy(logits) + torch.testing.assert_close(result, expected, atol=1e-4, rtol=1e-4) + + def test_uniform_distribution(self): + """Uniform logits -> entropy = log(V).""" + from axolotl.monkeypatch.trainer.utils import entropy_from_logits + + V = 1024 + logits = torch.zeros(2, V, device="cuda", dtype=torch.float32) + result = entropy_from_logits(logits) + expected_val = math.log(V) + torch.testing.assert_close( + result, + torch.full((2,), expected_val, device="cuda", dtype=torch.float32), + atol=1e-4, + rtol=1e-4, + ) + + def test_peaked_distribution(self): + """One-hot-like logits -> entropy near 0.""" + from axolotl.monkeypatch.trainer.utils import entropy_from_logits + + logits = torch.full((2, 128), -100.0, device="cuda", dtype=torch.float32) + logits[:, 0] = 100.0 + result = entropy_from_logits(logits) + assert (result < 1e-3).all() + + def test_bfloat16(self): + from axolotl.monkeypatch.trainer.utils import entropy_from_logits + + logits = torch.randn(4, 256, device="cuda", dtype=torch.bfloat16) + result = entropy_from_logits(logits) + expected = _ref_entropy(logits.float()) + assert result.dtype == torch.bfloat16 + torch.testing.assert_close(result.float(), expected, atol=5e-2, rtol=5e-2) + + def test_float16(self): + from axolotl.monkeypatch.trainer.utils import entropy_from_logits + + logits = torch.randn(4, 256, device="cuda", dtype=torch.float16) + result = entropy_from_logits(logits) + expected = _ref_entropy(logits.float()) + assert result.dtype == torch.float16 + torch.testing.assert_close(result.float(), expected, atol=5e-2, rtol=5e-2) + + def test_non_contiguous_3d_transpose(self): + """Non-contiguous 3D tensor via transpose(0,1).""" + from axolotl.monkeypatch.trainer.utils import entropy_from_logits + + V = 256 + raw = torch.randn(32, 4, V, device="cuda", dtype=torch.float32) + logits = raw.transpose(0, 1) # (4, 32, V) non-contiguous + assert not logits.is_contiguous() + result = entropy_from_logits(logits) + expected = _ref_entropy(logits) + torch.testing.assert_close(result, expected, atol=1e-4, rtol=1e-4) + + def test_non_contiguous_3d_slice(self): + """Non-contiguous 3D tensor via batch slicing.""" + from axolotl.monkeypatch.trainer.utils import entropy_from_logits + + V = 256 + raw = torch.randn(8, 32, V, device="cuda", dtype=torch.float32) + logits = raw[::2] # (4, 32, V) non-contiguous + assert not logits.is_contiguous() + result = entropy_from_logits(logits) + expected = _ref_entropy(logits) + torch.testing.assert_close(result, expected, atol=1e-4, rtol=1e-4) + + def test_many_rows_beyond_max_grid(self): + """More rows than MAX_GRID (8192) to test chunked dispatch.""" + from axolotl.monkeypatch.trainer.utils import entropy_from_logits + + logits = torch.randn(10000, 128, device="cuda", dtype=torch.float32) + result = entropy_from_logits(logits) + expected = _ref_entropy(logits) + torch.testing.assert_close(result, expected, atol=1e-4, rtol=1e-4) + + def test_entropy_non_negative(self): + from axolotl.monkeypatch.trainer.utils import entropy_from_logits + + logits = torch.randn(32, 512, device="cuda", dtype=torch.float32) + result = entropy_from_logits(logits) + assert (result >= -1e-5).all(), f"Negative entropy: {result.min()}" + + +# --------------------------------------------------------------------------- +# selective_log_softmax — forward correctness +# --------------------------------------------------------------------------- + + +class TestSelectiveLogSoftmax: + @pytest.mark.parametrize( + "B,L,K", + [ + (1, 128, 1), + (4, 512, 1), + (8, 256, 1), + (4, 256, 4), + (4, 256, 7), + (15, 129, 1), # non-power-of-2 + ], + ) + def test_correctness_various_shapes(self, B, L, K): + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 1024 + torch.manual_seed(42) + logits = torch.randn(B, L, V, device="cuda", dtype=torch.float32) + if K == 1: + index = torch.randint(0, V, (B, L), device="cuda") + else: + index = torch.randint(0, V, (B, L, K), device="cuda") + result = selective_log_softmax(logits, index) + expected = _ref_selective_log_softmax(logits, index) + torch.testing.assert_close(result, expected, atol=1e-4, rtol=1e-4) + + def test_squeezed_index(self): + """Index with ndim == logits.ndim - 1 triggers squeeze path.""" + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 256 + logits = torch.randn(8, V, device="cuda", dtype=torch.float32) + index = torch.randint(0, V, (8,), device="cuda") + result = selective_log_softmax(logits, index) + expected = _ref_selective_log_softmax(logits, index) + assert result.shape == (8,) + torch.testing.assert_close(result, expected, atol=1e-4, rtol=1e-4) + + def test_large_vocab(self): + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 32000 + logits = torch.randn(2, V, device="cuda", dtype=torch.float32) + index = torch.randint(0, V, (2, 1), device="cuda") + result = selective_log_softmax(logits, index) + expected = _ref_selective_log_softmax(logits, index) + torch.testing.assert_close(result, expected, atol=1e-4, rtol=1e-4) + + def test_bfloat16(self): + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 1024 + torch.manual_seed(42) + logits = torch.randn(4, 128, V, device="cuda", dtype=torch.bfloat16) + index = torch.randint(0, V, (4, 128), device="cuda") + result = selective_log_softmax(logits, index) + expected = _ref_selective_log_softmax(logits.float(), index) + assert result.dtype == torch.bfloat16 + torch.testing.assert_close(result.float(), expected, atol=0.1, rtol=0.1) + + def test_fp32_tight_tolerance(self): + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 1024 + torch.manual_seed(42) + logits = torch.randn(2, 256, V, device="cuda", dtype=torch.float32) + index = torch.randint(0, V, (2, 256), device="cuda") + result = selective_log_softmax(logits, index) + expected = _ref_selective_log_softmax(logits, index) + torch.testing.assert_close(result, expected, atol=1e-5, rtol=1e-5) + + def test_all_same_index(self): + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 128 + logits = torch.randn(8, V, device="cuda", dtype=torch.float32) + index = torch.zeros(8, 1, device="cuda", dtype=torch.long) + result = selective_log_softmax(logits, index) + expected = _ref_selective_log_softmax(logits, index) + torch.testing.assert_close(result, expected, atol=1e-4, rtol=1e-4) + + def test_last_index(self): + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 128 + logits = torch.randn(8, V, device="cuda", dtype=torch.float32) + index = torch.full((8, 1), V - 1, device="cuda", dtype=torch.long) + result = selective_log_softmax(logits, index) + expected = _ref_selective_log_softmax(logits, index) + torch.testing.assert_close(result, expected, atol=1e-4, rtol=1e-4) + + def test_output_always_nonpositive(self): + """Log softmax values should always be <= 0.""" + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 256 + logits = torch.randn(32, V, device="cuda", dtype=torch.float32) + index = torch.randint(0, V, (32, 1), device="cuda") + result = selective_log_softmax(logits, index) + assert (result <= 1e-5).all(), f"Positive log-prob: {result.max()}" + + def test_many_rows_beyond_max_grid(self): + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 128 + logits = torch.randn(10000, V, device="cuda", dtype=torch.float32) + index = torch.randint(0, V, (10000, 1), device="cuda") + result = selective_log_softmax(logits, index) + expected = _ref_selective_log_softmax(logits, index) + torch.testing.assert_close(result, expected, atol=1e-4, rtol=1e-4) + + +# --------------------------------------------------------------------------- +# selective_log_softmax — backward / gradient correctness +# --------------------------------------------------------------------------- + + +class TestSelectiveLogSoftmaxBackward: + @pytest.mark.parametrize( + "B,L,V,K", + [ + (2, 16, 64, 1), + (2, 16, 64, 4), + (1, 8, 128, 1), + (2, 8, 128, 7), + ], + ) + def test_gradient_matches_reference(self, B, L, V, K): + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + torch.manual_seed(42) + logits_ref = torch.randn( + B, L, V, device="cuda", dtype=torch.float32, requires_grad=True + ) + logits_tri = logits_ref.detach().clone().requires_grad_(True) + + if K == 1: + index = torch.randint(0, V, (B, L), device="cuda") + else: + index = torch.randint(0, V, (B, L, K), device="cuda") + + ref_out = _ref_selective_log_softmax(logits_ref, index) + tri_out = selective_log_softmax(logits_tri, index) + + ref_out.sum().backward() + tri_out.sum().backward() + + torch.testing.assert_close( + logits_tri.grad, logits_ref.grad, atol=1e-5, rtol=1e-5 + ) + + def test_gradient_bfloat16_full_vocab(self): + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 4096 + torch.manual_seed(42) + logits_ref = torch.randn( + 2, 64, V, device="cuda", dtype=torch.bfloat16, requires_grad=True + ) + logits_tri = logits_ref.detach().clone().requires_grad_(True) + index = torch.randint(0, V, (2, 64), device="cuda") + + _ref_selective_log_softmax(logits_ref, index).sum().backward() + selective_log_softmax(logits_tri, index).sum().backward() + + torch.testing.assert_close( + logits_tri.grad.float(), logits_ref.grad.float(), atol=0.1, rtol=0.1 + ) + + def test_gradient_k1_squeezed(self): + """Gradient with squeezed (1D) index.""" + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 256 + logits = torch.randn( + 8, V, device="cuda", dtype=torch.float32, requires_grad=True + ) + index = torch.randint(0, V, (8,), device="cuda") + + result = selective_log_softmax(logits, index) + result.sum().backward() + triton_grad = logits.grad.clone() + + logits.grad = None + ref = torch.gather( + F.log_softmax(logits, dim=-1), dim=-1, index=index.unsqueeze(-1) + ).squeeze(-1) + ref.sum().backward() + + torch.testing.assert_close(triton_grad, logits.grad, atol=1e-4, rtol=1e-4) + + +# --------------------------------------------------------------------------- +# selective_log_softmax — out-of-bounds index safety +# --------------------------------------------------------------------------- + + +class TestSelectiveLogSoftmaxOOBSafety: + """Verify that out-of-range indices don't crash or corrupt valid results.""" + + def test_negative_indices_no_crash(self): + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 128 + logits = torch.randn(4, V, device="cuda", dtype=torch.float32) + index = torch.tensor( + [[-1], [0], [V - 1], [-5]], device="cuda", dtype=torch.long + ) + result = selective_log_softmax(logits, index) + assert result.shape == (4, 1) + # Valid rows should be finite and match reference + valid_idx = torch.tensor([[0], [V - 1]], device="cuda", dtype=torch.long) + valid_logits = logits[1:3] + expected = _ref_selective_log_softmax(valid_logits, valid_idx) + torch.testing.assert_close(result[1:3], expected, atol=1e-4, rtol=1e-4) + + def test_index_exceeds_vocab_no_crash(self): + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 128 + logits = torch.randn(4, V, device="cuda", dtype=torch.float32) + index = torch.tensor( + [[0], [V], [V + 100], [V - 1]], device="cuda", dtype=torch.long + ) + result = selective_log_softmax(logits, index) + assert result.shape == (4, 1) + # Valid rows (0 and 3) should match reference + for row_idx, idx_val in [(0, 0), (3, V - 1)]: + ref = _ref_selective_log_softmax( + logits[row_idx : row_idx + 1], + torch.tensor([[idx_val]], device="cuda", dtype=torch.long), + ) + torch.testing.assert_close( + result[row_idx : row_idx + 1], ref, atol=1e-4, rtol=1e-4 + ) + + def test_mixed_valid_invalid_multi_index(self): + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 256 + K = 3 + logits = torch.randn(4, V, device="cuda", dtype=torch.float32) + index = torch.tensor( + [ + [0, 10, -1], # last invalid + [V, 5, 100], # first invalid + [50, 60, 70], # all valid + [-1, V + 1, -100], # all invalid + ], + device="cuda", + dtype=torch.long, + ) + result = selective_log_softmax(logits, index) + assert result.shape == (4, K) + # Row 2 (all valid) must match reference exactly + valid_index = torch.tensor([[50, 60, 70]], device="cuda", dtype=torch.long) + expected = _ref_selective_log_softmax(logits[2:3], valid_index) + torch.testing.assert_close(result[2:3], expected, atol=1e-4, rtol=1e-4) + + def test_oob_backward_no_crash(self): + """Backward with OOB indices should not crash and grads should be finite.""" + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 128 + logits = torch.randn( + 4, V, device="cuda", dtype=torch.float32, requires_grad=True + ) + index = torch.tensor( + [[-1], [0], [V + 10], [V - 1]], device="cuda", dtype=torch.long + ) + result = selective_log_softmax(logits, index) + result.sum().backward() + assert logits.grad is not None + assert torch.isfinite(logits.grad).all() + + def test_oob_backward_valid_rows_correct(self): + """Gradients for valid-index rows should match reference even when other rows have OOB.""" + from axolotl.monkeypatch.trainer.utils import selective_log_softmax + + V = 128 + logits = torch.randn( + 4, V, device="cuda", dtype=torch.float32, requires_grad=True + ) + # Row 0: invalid, Row 1: valid, Row 2: invalid, Row 3: valid + index = torch.tensor( + [[-1], [42], [V + 5], [100]], device="cuda", dtype=torch.long + ) + result = selective_log_softmax(logits, index) + result.sum().backward() + + # Compute reference gradient for valid rows only + logits_ref = logits.detach().clone().requires_grad_(True) + valid_rows = [1, 3] + valid_indices = [42, 100] + for r, idx in zip(valid_rows, valid_indices, strict=True): + ref_lp = F.log_softmax(logits_ref[r : r + 1], dim=-1) + ref_val = ref_lp[0, idx] + ref_val.backward(retain_graph=True) + + for r in valid_rows: + torch.testing.assert_close( + logits.grad[r], logits_ref.grad[r], atol=1e-4, rtol=1e-4 + ) From bb483ad4c47c7d90b3a33997f8da2de9dcccc03a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 19 Mar 2026 08:29:24 -0400 Subject: [PATCH 1201/1405] make the CI fail GitHub Actions on test failures (#3517) * make the CI fail GitHub Actions on test failures * use model bundle * install zstd for compressed model artifact --- cicd/Dockerfile-uv.jinja | 2 +- cicd/Dockerfile.jinja | 2 +- cicd/cicd.sh | 11 ++++++----- cicd/single_gpu.py | 10 +++------- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/cicd/Dockerfile-uv.jinja b/cicd/Dockerfile-uv.jinja index 29c2e79d51..857b94c6b1 100644 --- a/cicd/Dockerfile-uv.jinja +++ b/cicd/Dockerfile-uv.jinja @@ -11,7 +11,7 @@ ENV NIGHTLY_BUILD="{{ NIGHTLY_BUILD }}" ENV HF_HOME="{{ HF_HOME }}" RUN apt-get update && \ - apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm + apt-get install -y --allow-change-held-packages vim curl nano zstd libnccl2 libnccl-dev ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm WORKDIR /workspace diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja index 4f0140fc60..7344f2a2c9 100644 --- a/cicd/Dockerfile.jinja +++ b/cicd/Dockerfile.jinja @@ -12,7 +12,7 @@ ENV HF_HOME="{{ HF_HOME }}" ENV AXOLOTL_DATASET_NUM_PROC="8" RUN apt-get update && \ - apt-get install -y --allow-change-held-packages vim curl nano libnccl2 libnccl-dev ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm + apt-get install -y --allow-change-held-packages vim curl nano zstd libnccl2 libnccl-dev ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm WORKDIR /workspace diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 462b874a6b..5058779fb6 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -3,11 +3,12 @@ set -e python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__" -# curl -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst | tar -xpf - -C "${HF_HOME}/hub/" --use-compress-program unzstd --strip-components=1 -hf download "NousResearch/Meta-Llama-3-8B" -hf download "NousResearch/Meta-Llama-3-8B-Instruct" -hf download "microsoft/Phi-4-reasoning" -hf download "microsoft/Phi-3.5-mini-instruct" +curl -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst | tar -xpf - -C "${HF_HOME}/hub/" --use-compress-program unzstd --strip-components=1 +# hf download "NousResearch/Meta-Llama-3-8B" +# hf download "NousResearch/Meta-Llama-3-8B-Instruct" +# hf download "microsoft/Phi-4-reasoning" +# hf download "microsoft/Phi-3.5-mini-instruct" +# hf download "microsoft/Phi-3-medium-128k-instruct" # Run unit tests with initial coverage report pytest -v --durations=10 -n8 \ diff --git a/cicd/single_gpu.py b/cicd/single_gpu.py index cd73f60b81..592b6b9312 100644 --- a/cicd/single_gpu.py +++ b/cicd/single_gpu.py @@ -68,10 +68,6 @@ def run_cmd(cmd: str, run_folder: str): sp_env["AXOLOTL_DATASET_NUM_PROC"] = "8" # Propagate errors from subprocess. - try: - exit_code = subprocess.call(cmd.split(), cwd=run_folder, env=sp_env) # nosec - if exit_code: - print(f"Command '{cmd}' failed with exit code {exit_code}") - return exit_code - except Exception as e: # pylint: disable=broad-except - print(f"Command '{cmd}' failed with exception {e}") + exit_code = subprocess.call(cmd.split(), cwd=run_folder, env=sp_env) # nosec + if exit_code: + raise RuntimeError(f"Command '{cmd}' failed with exit code {exit_code}") From 1fc86d5295eb4e10b9af2550e0b50b1c1ba540f4 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 19 Mar 2026 23:07:42 -0400 Subject: [PATCH 1202/1405] Scattermoe LoRA optimizations (#3513) * optimize moe + lora * more scattermoe optims * selective dequant * add correctness unit tests and benchmarks for scattermoe + lora * handle base+lora split kernel for older moe models * chore: lint * fix casting for H200 and B200 * register pressure estimation and pruning for h200/b200 * use soft limit for pruning * qkv patch for qwen3.5moe * support text_model for qwen3.5 moe * nesting of qwen3 * use udpated cce with zero3 support * Fix decomposed backward for QKV and O projections eliminates B @ A materialization in LoRA attention backward, replacing full [out, in] matmuls with two small [T, R] matmuls. --- benchmarks/bench_scattermoe_lora.py | 284 ++++++++ .../colab-axolotl-example.ipynb | 2 +- scripts/cutcrossentropy_install.py | 2 +- .../integrations/cut_cross_entropy/README.md | 2 +- .../cut_cross_entropy/__init__.py | 2 +- .../libs/scattermoe_lora/kernels/lora_ops.py | 628 ++++++++++++++++-- .../kernels/libs/scattermoe_lora/layers.py | 95 ++- .../libs/scattermoe_lora/selective_dequant.py | 282 ++++++++ .../selective_dequant_kernel.py | 179 +++++ src/axolotl/integrations/kernels/plugin.py | 9 + src/axolotl/kernels/lora.py | 11 +- src/axolotl/loaders/model.py | 14 + src/axolotl/monkeypatch/lora_kernels.py | 25 + src/axolotl/utils/callbacks/profiler.py | 34 +- .../e2e/patched/test_lora_llama_multipack.py | 51 +- .../test_scattermoe_lora_kernels.py | 407 ++++++++++++ 16 files changed, 1898 insertions(+), 129 deletions(-) create mode 100644 benchmarks/bench_scattermoe_lora.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant_kernel.py create mode 100644 tests/integrations/test_scattermoe_lora_kernels.py diff --git a/benchmarks/bench_scattermoe_lora.py b/benchmarks/bench_scattermoe_lora.py new file mode 100644 index 0000000000..0fb3ba68ce --- /dev/null +++ b/benchmarks/bench_scattermoe_lora.py @@ -0,0 +1,284 @@ +"""Benchmark for ScatterMoE LoRA Triton kernels. + +Measures forward, backward dX, and backward dA/dB kernels at common MoE +model shapes. Reports per-kernel timings, LoRA overhead vs base scatter2scatter, +and full fwd+bwd autograd throughput. + +Usage: + CUDA_VISIBLE_DEVICES=0 python benchmarks/bench_scattermoe_lora.py + CUDA_VISIBLE_DEVICES=0 python benchmarks/bench_scattermoe_lora.py --ranks 16 64 + CUDA_VISIBLE_DEVICES=0 python benchmarks/bench_scattermoe_lora.py --models Qwen/Qwen3.5-35B-A3B +""" + +import argparse +import gc +import time +from functools import partial + +import torch + +from axolotl.integrations.kernels.libs.scattermoe_lora.kernels import ( + lora_ops, + ops as base_ops, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_linear_lora import ( + ScatterMoELoRA, +) + +DEVICE = "cuda" +DTYPE = torch.bfloat16 +WARMUP = 5 +ITERS = 20 + +# ─── Model configs ────────────────────────────────────────────────────────── + +BUILTIN_CONFIGS = { + "Qwen3.5-35B-A3B": (256, 2048, 512, 8), # E, H, I, k + "Qwen3-30B-A3B": (128, 2048, 768, 8), + "OLMoE-1B-7B": (64, 2048, 1024, 8), + "Mixtral-8x7B": (8, 4096, 14336, 2), +} + + +def _resolve_config(spec): + """Resolve a model spec to (E, H, I, k). Accepts builtin names or HF IDs.""" + key = spec.lower().replace("/", "-") + for name, cfg in BUILTIN_CONFIGS.items(): + if key in name.lower() or name.lower() in key: + return name, cfg + + from transformers import AutoConfig + + hf_cfg = AutoConfig.from_pretrained(spec, trust_remote_code=True) + if callable(getattr(hf_cfg, "get_text_config", None)): + tc = hf_cfg.get_text_config() + if hasattr(tc, "model_type") and tc.model_type != hf_cfg.model_type: + hf_cfg = tc + hidden = hf_cfg.hidden_size + inter = getattr(hf_cfg, "moe_intermediate_size", None) or hf_cfg.intermediate_size + experts = ( + getattr(hf_cfg, "num_experts", None) + or getattr(hf_cfg, "num_local_experts", None) + or getattr(hf_cfg, "n_routed_experts", None) + ) + top_k = ( + getattr(hf_cfg, "num_experts_per_tok", None) + or getattr(hf_cfg, "num_experts_per_token", None) + or 2 + ) + name = spec.split("/")[-1] + return name, (experts, hidden, inter, top_k) + + +# ─── Benchmark helpers ────────────────────────────────────────────────────── + + +def _clean(): + gc.collect() + torch.cuda.empty_cache() + torch.cuda.synchronize() + + +def _bench(fn, warmup=WARMUP, iters=ITERS): + for _ in range(warmup): + fn() + torch.cuda.synchronize() + times = [] + for _ in range(iters): + torch.cuda.synchronize() + t0 = time.perf_counter() + fn() + torch.cuda.synchronize() + times.append((time.perf_counter() - t0) * 1000) + times.sort() + return times[len(times) // 2] + + +def _setup(num_experts, K, N, T, top_k, R): + torch.manual_seed(42) + x = torch.randn(T, K, device=DEVICE, dtype=DTYPE) + W = torch.randn(num_experts, K, N, device=DEVICE, dtype=DTYPE) * 0.02 + lora_A = torch.randn(R * num_experts, K, device=DEVICE, dtype=DTYPE) * 0.01 + lora_B = torch.randn(N, R * num_experts, device=DEVICE, dtype=DTYPE) * 0.01 + logits = torch.randn(T, num_experts, device=DEVICE) + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), top_k, dim=-1) + sei, ssi, eo = flatten_sort_count(top_idx, num_experts) + gx = base_ops.group(x, ssi, fan_out=top_k) + dy = torch.randn(gx.size(0), N, device=DEVICE, dtype=DTYPE) + return x, W, lora_A, lora_B, sei, ssi, eo, gx, dy + + +# ─── Kernel wrappers (avoid B023 loop-variable capture) ────────────────────── + + +def _call_fwd(x, W, sei, ssi, top_k, lA, lB): + return lora_ops.scatter2scatter_lora( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=top_k, + lora_A=lA, + lora_B=lB, + scaling=2.0, + ) + + +def _call_base(x, W, sei, ssi, top_k): + return base_ops.scatter2scatter( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=top_k, + ) + + +def _call_dx(dy, W, sei, ssi, lA, lB): + return lora_ops.scatter2scatter_lora_dX( + DY=dy, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=1, + lora_A=lA, + lora_B=lB, + scaling=2.0, + dy_grouped=True, + dx_grouped=False, + ) + + +def _call_bwd(dy, gx, lA, lB, eo, num_experts): + return lora_ops.group_bwd_lora( + DY=dy, + X=gx, + lora_A=lA, + lora_B=lB, + expert_offsets=eo, + E=num_experts, + scaling=2.0, + ) + + +# ─── Main ──────────────────────────────────────────────────────────────────── + + +def main(): + parser = argparse.ArgumentParser(description="ScatterMoE LoRA kernel benchmark") + parser.add_argument( + "--models", + "-m", + nargs="+", + help="Model names or HF IDs (default: all builtins)", + ) + parser.add_argument("--ranks", "-r", nargs="+", type=int, default=[16, 32, 64]) + parser.add_argument("--seq-len", "-T", type=int, default=2048) + args = parser.parse_args() + + T = args.seq_len + print(f"GPU: {torch.cuda.get_device_name()}") + print(f"T={T}, ranks={args.ranks}\n") + + if args.models: + configs = [_resolve_config(m) for m in args.models] + else: + configs = list(BUILTIN_CONFIGS.items()) + + for model_name, (num_experts, hidden, inter, top_k) in configs: + print(f"{'=' * 70}") + print(f" {model_name}: E={num_experts}, H={hidden}, I={inter}, k={top_k}") + print(f"{'=' * 70}") + + for R in args.ranks: + for proj, K, N in [("gate_up", hidden, 2 * inter), ("down", inter, hidden)]: + _clean() + x, W, lA, lB, sei, ssi, eo, gx, dy = _setup( + num_experts, K, N, T, top_k, R + ) + + # Forward with LoRA (auto-dispatched: fused or split) + dispatch = ( + "split" + if ( + num_experts <= lora_ops._SPLIT_LORA_FWD_MAX_EXPERTS + and K * N >= lora_ops._SPLIT_LORA_FWD_THRESHOLD + ) + else "fused" + ) + t_fwd = _bench(partial(_call_fwd, x, W, sei, ssi, top_k, lA, lB)) + t_base = _bench(partial(_call_base, x, W, sei, ssi, top_k)) + t_dx = _bench(partial(_call_dx, dy, W, sei, ssi, lA, lB)) + t_bwd = _bench(partial(_call_bwd, dy, gx, lA, lB, eo, num_experts)) + + total = t_fwd + t_dx + t_bwd + overhead = t_fwd / t_base - 1 if t_base > 0 else 0 + + print( + f" R={R:>2} {proj:<8} " + f"fwd={t_fwd:>6.2f}ms [{dispatch}] " + f"base={t_base:>6.2f}ms " + f"(+{overhead * 100:.0f}%) " + f"dx={t_dx:>6.2f}ms bwd={t_bwd:>6.2f}ms " + f"total={total:>6.2f}ms" + ) + + # Full autograd fwd+bwd with memory measurement + x_ag = x.clone().requires_grad_(True) + lA_ag = lA.clone().requires_grad_(True) + lB_ag = lB.clone().requires_grad_(True) + + def _run_autograd( + _x=x_ag, + _W=W, + _k=top_k, + _sei=sei, + _ssi=ssi, + _eo=eo, + _lA=lA_ag, + _lB=lB_ag, + ): + out = ScatterMoELoRA.apply( + _x, + _W, + _k, + _sei, + _ssi, + _eo, + _lA, + _lB, + 2.0, + None, + None, + False, + False, + True, + False, + ) + out.sum().backward() + _x.grad = None + _lA.grad = None + _lB.grad = None + + t_full = _bench(_run_autograd) + + _clean() + torch.cuda.reset_peak_memory_stats() + mem_before = torch.cuda.memory_allocated() + _run_autograd() + torch.cuda.synchronize() + mem_peak = torch.cuda.max_memory_allocated() - mem_before + + print( + f" full_fwd_bwd={t_full:>6.2f}ms " + f"peak_delta={mem_peak / 1e6:>6.1f}MB" + ) + + print() + + +if __name__ == "__main__": + main() diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 49a45cdc6e..7a9feaa030 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fa9a7fe\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@63b15e6\"" ] }, { diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 771a5adb27..bd92a3630e 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fa9a7fe"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@63b15e6"' ) diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 5a3a73d34d..220fb4d2bc 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fa9a7fe" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@63b15e6" ``` ## Usage diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 808aff6627..758c5406ca 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fa9a7fe"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@63b15e6"`' ) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py index 5d47c20408..16f6da73b5 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py @@ -195,6 +195,36 @@ def _estimate_smem_usage( _SMEM_SLACK = 10_000 +def _estimate_register_pressure( + num_warps: int, + *tile_sizes: tuple[int, int], +) -> float: + """Rough estimate of per-thread register footprint from live tile sizes. + + This is a heuristic, NOT an accurate register count. Triton uses tensor + core MMA fragments that pack multiple elements per register, and can spill + to local memory when the hardware limit (255 regs/thread) is exceeded. + + The estimate is used to prune only truly extreme configs that would cause + excessive spilling or compilation failures. The threshold is set high + (``_MAX_REGS_SOFT_LIMIT``) because the heuristic overestimates — it + doesn't account for MMA fragment packing. Configs like M=64,N=64,K=64 + (est ~520) work fine in practice via spilling. + + Returns estimated registers per thread. + """ + # Each thread in a warp holds ~1/32 of the tile elements + tile_regs = sum(r * c for r, c in tile_sizes) / 32 + scalar_overhead = 40 + return tile_regs + scalar_overhead + + +# Soft limit for register pressure pruning. Only prune configs with extreme +# tile products (e.g. M=128,K=256,N=256) that reliably crash on Blackwell. +# Moderate configs (M=64,N=64,K=64, est ~520) work via register spilling. +_MAX_REGS_SOFT_LIMIT = 1024 + + # ============================================================================= # Forward Kernel: scatter2scatter with fused LoRA # ============================================================================= @@ -313,12 +343,11 @@ def _compute_expert_block_lora( B_blk_ptrs, mask=N_mask[:, None] & R_mask[None, :], other=0.0 ) # [BLOCK_N, BLOCK_R] - # Cast xa_acc and b to same dtype for tl.dot (required when input is bf16/fp16) - # Both operands must match; cast to float32 (accumulator type) for precision. - b_f32 = b.to(tl.float32) + # tl.dot requires non-float32 inputs (tensor cores); cast back to input dtype + b_inp = b.to(INPUT_DTYPE) # (X @ A^T) @ B^T: [M, R] @ [R, N] -> [M, N] - lora_out = tl.dot(xa_acc, tl.trans(b_f32), allow_tf32=allow_tf32) + lora_out = tl.dot(xa_acc.to(INPUT_DTYPE), tl.trans(b_inp), allow_tf32=allow_tf32) acc += scaling * lora_out return acc @@ -327,20 +356,21 @@ def _compute_expert_block_lora( def _scatter2scatter_lora_configs(): """Generate forward kernel autotune configs. - Search space includes smaller tile sizes and fewer pipeline stages to - support GPUs with limited shared memory (e.g. ~99KB on some GPUs). + Search space includes BLOCK_M to allow trading token-tile size for + larger BLOCK_K/BLOCK_N tiles. On GPUs with ~99KB SMEM, BLOCK_M=128 + forces BLOCK_K=32 and BLOCK_N=32; BLOCK_M=64 allows BLOCK_K=128 + (4× fewer inner-loop iterations). Search space: + BLOCK_M: {32, 64, 128} BLOCK_N: {32, 64, 128, 256} BLOCK_K: {32, 64, 128} num_warps: {4, 8} num_stages: {3, 4, 5} - - BLOCK_M is fixed at 128 (module-level constant, not autotuned in the - scatter2scatter pattern). """ configs = [] - for block_n, block_k, warps, stages in product( + for block_m, block_n, block_k, warps, stages in product( + [32, 64, 128], # BLOCK_M [32, 64, 128, 256], # BLOCK_N [32, 64, 128], # BLOCK_K [4, 8], # num_warps @@ -348,7 +378,7 @@ def _scatter2scatter_lora_configs(): ): configs.append( triton.Config( - {"BLOCK_N": block_n, "BLOCK_K": block_k}, + {"BLOCK_M": block_m, "BLOCK_N": block_n, "BLOCK_K": block_k}, num_stages=stages, num_warps=warps, ) @@ -357,7 +387,7 @@ def _scatter2scatter_lora_configs(): def _prune_fwd_configs(configs, named_args, **kwargs): - """Prune forward configs based on SMEM capacity. + """Prune forward configs based on SMEM capacity and register pressure. The forward kernel inner loop loads three tiles per pipeline stage: X[BLOCK_M, BLOCK_K], W[BLOCK_K, BLOCK_N], A[BLOCK_R, BLOCK_K]. @@ -373,23 +403,49 @@ def _prune_fwd_configs(configs, named_args, **kwargs): scored = [] for config in configs: + block_m = config.kwargs["BLOCK_M"] block_n = config.kwargs["BLOCK_N"] block_k = config.kwargs["BLOCK_K"] # Base: stages * BLOCK_K * (BLOCK_M + BLOCK_N) + BLOCK_M * BLOCK_N - smem_base = _estimate_smem_usage(config.num_stages, BLOCK_M, block_n, block_k) + smem_base = _estimate_smem_usage(config.num_stages, block_m, block_n, block_k) # A tile [BLOCK_R, BLOCK_K] loaded per stage in the inner loop smem_lora_loop = config.num_stages * block_r * block_k * 2 # B tile [BLOCK_N, BLOCK_R] loaded once in epilogue smem_lora_epilogue = block_n * block_r * 2 smem = smem_base + smem_lora_loop + smem_lora_epilogue + + # Register pressure: live tiles are acc[M,N], xa_acc[M,R], + # x[M,K], w[K,N], a[R,K], plus epilogue b[N,R] + est_regs = _estimate_register_pressure( + config.num_warps, + (block_m, block_n), # acc + (block_m, block_r), # xa_acc + (block_m, block_k), # x tile + (block_k, block_n), # w tile + (block_r, block_k), # a tile + (block_n, block_r), # b tile (epilogue) + ) + if est_regs > _MAX_REGS_SOFT_LIMIT: + continue + scored.append((smem, config)) pruned = [c for s, c in scored if s <= smem_cap - _SMEM_SLACK] if pruned: return pruned - # All configs exceed SMEM — return the one with smallest estimated usage - scored.sort(key=lambda x: x[0]) - return [scored[0][1]] + if scored: + # All surviving configs exceed SMEM — return the one with smallest usage + scored.sort(key=lambda x: x[0]) + return [scored[0][1]] + # All configs pruned by register pressure — fall back to smallest tiles + return [ + min( + configs, + key=lambda c: ( + c.kwargs["BLOCK_M"] * c.kwargs["BLOCK_N"] * c.kwargs["BLOCK_K"] + ), + ) + ] @triton.autotune( @@ -531,6 +587,89 @@ def _scatter2scatter_lora( tl.store(Y_blk_ptrs, acc, mask=M_boundary_mask[:, None] & N_mask[None, :]) +def _scatter2scatter_lora_split( + X: torch.Tensor, + W: torch.Tensor, + sorted_expert_idxs: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + k: int, + lora_A: torch.Tensor, + lora_B: torch.Tensor, + scaling: float, + b: Optional[torch.Tensor] = None, + x_grouped: bool = False, + y_grouped: bool = False, + out: Optional[torch.Tensor] = None, +) -> torch.Tensor: + """Split base+LoRA forward: 3 scatter2scatter calls, no fused LoRA kernel. + + Faster for models with few large experts (e.g. Mixtral E=8, I=14336) + because the base kernel runs at full speed without LoRA SMEM overhead, + and the LoRA matmuls (R=16) are tiny separate passes. + + Y = scatter(X, W) + scaling * scatter(scatter(X, A^T), B^T) + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.kernels.ops import ( + scatter2scatter, + ) + + E = W.size(0) + R = lora_A.size(0) // E + K = W.size(1) + N = W.size(2) + + # 1. Base: Y_base = X @ W (uses base kernel with optimal tile sizes) + output = scatter2scatter( + X=X, + W=W, + b=b, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=k, + x_grouped=x_grouped, + y_grouped=y_grouped, + out=out, + ) + + # 2. XA = X @ A^T (tiny: output is [M*k, R]) + # Reshape A: [R*E, K] → [E, K, R] (expert weights for scatter2scatter) + W_A = lora_A.reshape(E, R, K).permute(0, 2, 1).contiguous() + XA = scatter2scatter( + X=X, + W=W_A, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=k, + x_grouped=x_grouped, + y_grouped=True, + ) + + # 3. Y_lora = XA @ B^T (R is tiny, so this is very fast) + # Reshape B: [N, R*E] → [E, R, N] + W_B = lora_B.T.reshape(E, R, N).contiguous() + Y_lora = scatter2scatter( + X=XA, + W=W_B, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=1, + x_grouped=True, + y_grouped=y_grouped, + ) + + # 4. Y = Y_base + scaling * Y_lora + output.add_(Y_lora, alpha=scaling) + return output + + +# Threshold for switching from fused to split LoRA forward. +# Split wins when per-expert matmul is large (bandwidth-bound LoRA tile +# loads dominate in the fused kernel's inner loop). +# Empirically: split wins for E<=32 with K*N > 20M (e.g. Mixtral, Phi-MoE). +_SPLIT_LORA_FWD_THRESHOLD = 20_000_000 # per-expert K*N +_SPLIT_LORA_FWD_MAX_EXPERTS = 32 + + def scatter2scatter_lora( X: torch.Tensor, W: torch.Tensor, @@ -546,7 +685,13 @@ def scatter2scatter_lora( out: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ - Fused scatter2scatter with LoRA: Y[i] = X[i] @ W[e] + scaling * (X[i] @ A[e]^T) @ B[e]^T + b[e] + Scatter2scatter with LoRA: Y[i] = X[i] @ W[e] + scaling * (X[i] @ A[e]^T) @ B[e]^T + b[e] + + Automatically selects between: + - Fused kernel: single Triton kernel with LoRA in the inner loop. + Best for many small experts (E>=64, small K*N). + - Split dispatch: 3 separate scatter2scatter calls (base + XA + lora). + Best for few large experts (E<=32, large K*N like Mixtral). Args: X: Input [M, K] or [M*k, K] if x_grouped @@ -565,12 +710,30 @@ def scatter2scatter_lora( Returns: Y: Output [M*k, N] """ - assert sorted_scattered_idxs.size(0) == sorted_expert_idxs.size(0) - assert sorted_scattered_idxs.size(0) == X.size(0) * k - E = W.size(0) K = W.size(1) N = W.size(2) + + # Dispatch: split for few large experts, fused for many small experts + if E <= _SPLIT_LORA_FWD_MAX_EXPERTS and K * N >= _SPLIT_LORA_FWD_THRESHOLD: + return _scatter2scatter_lora_split( + X, + W, + sorted_expert_idxs, + sorted_scattered_idxs, + k, + lora_A, + lora_B, + scaling, + b, + x_grouped, + y_grouped, + out, + ) + + assert sorted_scattered_idxs.size(0) == sorted_expert_idxs.size(0) + assert sorted_scattered_idxs.size(0) == X.size(0) * k + R = lora_A.size(0) // E # Pad R to power of 2 for Triton tile size @@ -610,11 +773,9 @@ def grid(META): b_ptr, stride_be, stride_bn, - # A: [r*E, K] -> stride(0) is r*E dim stride, stride(1) is K dim stride lora_A, lora_A.stride(0), lora_A.stride(1), - # B: [N, r*E] -> stride(0) is N dim stride, stride(1) is r*E dim stride lora_B, lora_B.stride(0), lora_B.stride(1), @@ -625,9 +786,8 @@ def grid(META): K=K, N=N, E=E, - ACTUAL_R=R, # True LoRA rank for weight indexing - BLOCK_M=BLOCK_M, - BLOCK_R=BLOCK_R, # Padded tile size >= max(R, 16) + ACTUAL_R=R, + BLOCK_R=BLOCK_R, ACC_TYPE=tl.float32, scaling=scaling, allow_tf32=ALLOW_TF32, @@ -761,13 +921,13 @@ def _compute_expert_block_lora_dX( + (A_expert_offset + R_block)[:, None] * stride_ar + K_block[None, :] * stride_ak ) - a_e = tl.load(A_blk_ptrs, mask=R_mask[:, None] & K_mask[None, :], other=0.0) - - # Cast to float32 for precision - a_f32 = a_e.to(tl.float32) + a_e = tl.load(A_blk_ptrs, mask=R_mask[:, None] & K_mask[None, :], other=0.0).to( + INPUT_DTYPE + ) # (DY @ B) @ A: [M, R] @ [R, K] -> [M, K] - lora_dx = tl.dot(dy_b_acc, a_f32, allow_tf32=allow_tf32) + # tl.dot requires non-float32 inputs (tensor cores); cast accumulator back to input dtype + lora_dx = tl.dot(dy_b_acc.to(INPUT_DTYPE), a_e, allow_tf32=allow_tf32) acc += scaling * lora_dx return acc @@ -779,17 +939,18 @@ def _scatter2scatter_lora_dX_configs(): The inner loop is over N (not K as in forward). The output dimension is K. So BLOCK_K tiles the output and BLOCK_N tiles the reduction. - Search space includes smaller tile sizes and fewer pipeline stages to - support GPUs with limited shared memory (e.g. ~99KB on some GPUs). + BLOCK_M is now autotunable (was fixed at 128). Search space: + BLOCK_M: {32, 64, 128} (token tile) BLOCK_K: {32, 64, 128, 256} (output tile) BLOCK_N: {32, 64, 128, 256} (reduction tile) num_warps: {4, 8} num_stages: {3, 4, 5} """ configs = [] - for block_k, block_n, warps, stages in product( + for block_m, block_k, block_n, warps, stages in product( + [32, 64, 128], # BLOCK_M [32, 64, 128, 256], # BLOCK_K (output dimension) [32, 64, 128, 256], # BLOCK_N (reduction dimension) [4, 8], # num_warps @@ -797,7 +958,7 @@ def _scatter2scatter_lora_dX_configs(): ): configs.append( triton.Config( - {"BLOCK_K": block_k, "BLOCK_N": block_n}, + {"BLOCK_M": block_m, "BLOCK_K": block_k, "BLOCK_N": block_n}, num_stages=stages, num_warps=warps, ) @@ -806,7 +967,7 @@ def _scatter2scatter_lora_dX_configs(): def _prune_dX_configs(configs, named_args, **kwargs): - """Prune backward dX configs based on SMEM capacity. + """Prune backward dX configs based on SMEM capacity and register pressure. The dX kernel inner loop loads three tiles per pipeline stage: DY[BLOCK_M, BLOCK_N], W^T[BLOCK_N, BLOCK_K], B[BLOCK_N, BLOCK_R]. @@ -822,23 +983,49 @@ def _prune_dX_configs(configs, named_args, **kwargs): scored = [] for config in configs: + block_m = config.kwargs["BLOCK_M"] block_k = config.kwargs["BLOCK_K"] block_n = config.kwargs["BLOCK_N"] # Base: stages * BLOCK_N * (BLOCK_M + BLOCK_K) + BLOCK_M * BLOCK_K - smem_base = _estimate_smem_usage(config.num_stages, BLOCK_M, block_k, block_n) + smem_base = _estimate_smem_usage(config.num_stages, block_m, block_k, block_n) # B tile [BLOCK_N, BLOCK_R] loaded per stage in the inner loop smem_lora_loop = config.num_stages * block_n * block_r * 2 # A tile [BLOCK_R, BLOCK_K] loaded once in epilogue smem_lora_epilogue = block_r * block_k * 2 smem = smem_base + smem_lora_loop + smem_lora_epilogue + + # Register pressure: live tiles are acc[M,K], dy_b_acc[M,R], + # dy[M,N], wt[N,K], b[N,R], plus epilogue a[R,K] + est_regs = _estimate_register_pressure( + config.num_warps, + (block_m, block_k), # acc + (block_m, block_r), # dy_b_acc + (block_m, block_n), # dy tile + (block_n, block_k), # wt tile + (block_n, block_r), # b tile + (block_r, block_k), # a tile (epilogue) + ) + if est_regs > _MAX_REGS_SOFT_LIMIT: + continue + scored.append((smem, config)) pruned = [c for s, c in scored if s <= smem_cap - _SMEM_SLACK] if pruned: return pruned - # All configs exceed SMEM — return the one with smallest estimated usage - scored.sort(key=lambda x: x[0]) - return [scored[0][1]] + if scored: + # All surviving configs exceed SMEM — return the one with smallest usage + scored.sort(key=lambda x: x[0]) + return [scored[0][1]] + # All configs pruned by register pressure — fall back to smallest tiles + return [ + min( + configs, + key=lambda c: ( + c.kwargs["BLOCK_M"] * c.kwargs["BLOCK_K"] * c.kwargs["BLOCK_N"] + ), + ) + ] @triton.autotune( @@ -1067,7 +1254,7 @@ def grid(META): N=N, E=E, ACTUAL_R=R, - BLOCK_M=BLOCK_M, + # BLOCK_M is autotuned (injected by triton.autotune from Config kwargs) BLOCK_R=BLOCK_R, ACC_TYPE=tl.float32, scaling=scaling, @@ -1119,7 +1306,7 @@ def _group_bwd_lora_configs(): def _prune_bwd_lora_configs(configs, named_args, **kwargs): - """Prune backward configs based on SMEM capacity. + """Prune backward configs based on SMEM capacity and register pressure. The backward kernel loads X[BLOCK_M, BLOCK_K] and DY[BLOCK_M, BLOCK_N] in the inner loop, plus holds A[BLOCK_R, BLOCK_K] and B[BLOCK_N, BLOCK_R] @@ -1138,14 +1325,40 @@ def _prune_bwd_lora_configs(configs, named_args, **kwargs): # A[BLOCK_R, BLOCK_K] and B[BLOCK_N, BLOCK_R] held for the full expert smem_lora = (block_r * block_k + block_n * block_r) * 2 smem = smem_base + smem_lora + + # Register pressure: dA_acc[R,K], dB_acc[N,R], x[M,K], dy[M,N], + # a[R,K], b[N,R], xa[M,R], dy_b[M,R] + est_regs = _estimate_register_pressure( + config.num_warps, + (block_r, block_k), # dA_acc + (block_n, block_r), # dB_acc + (block_m, block_k), # x tile + (block_m, block_n), # dy tile + (block_r, block_k), # a tile + (block_n, block_r), # b tile + (block_m, block_r), # xa intermediate + ) + if est_regs > _MAX_REGS_SOFT_LIMIT: + continue + scored.append((smem, config)) pruned = [c for s, c in scored if s <= smem_cap - _SMEM_SLACK] if pruned: return pruned - # All configs exceed SMEM — return the one with smallest estimated usage - scored.sort(key=lambda x: x[0]) - return [scored[0][1]] + if scored: + # All surviving configs exceed SMEM — return the one with smallest usage + scored.sort(key=lambda x: x[0]) + return [scored[0][1]] + # All configs pruned by register pressure — fall back to smallest tiles + return [ + min( + configs, + key=lambda c: ( + c.kwargs["BLOCK_M"] * c.kwargs["BLOCK_K"] * c.kwargs["BLOCK_N"] + ), + ) + ] @triton.autotune( @@ -1330,6 +1543,279 @@ def _group_bwd_lora( ) +def _group_bwd_split_configs(): + """Autotune configs for split dA/dB kernels.""" + configs = [] + for block_m, block_dim, warps, stages in product( + [32, 64, 128], # BLOCK_M (token tile) + [32, 64, 128, 256], # BLOCK_DIM (K for dA, N for dB — output tile) + [4, 8], # num_warps + [3, 4, 5], # num_stages + ): + configs.append( + triton.Config( + {"BLOCK_M": block_m, "BLOCK_DIM": block_dim}, + num_stages=stages, + num_warps=warps, + ) + ) + return configs + + +def _prune_split_configs(configs, named_args, **kwargs): + """Prune split kernel configs based on SMEM capacity and register pressure.""" + smem_cap = _get_smem_capacity() + block_r = named_args.get("BLOCK_R", 64) + + # Fixed inner tile for reduction dimension + BLOCK_INNER = 64 + + pruned = [] + for config in configs: + block_m = config.kwargs["BLOCK_M"] + block_dim = config.kwargs["BLOCK_DIM"] + # Inner loop loads: input[M, INNER] and other[M, INNER_or_DIM] + smem = config.num_stages * BLOCK_INNER * (block_m + block_dim) * 2 + # LoRA weights held in registers: [INNER, R] or [R, DIM] + smem += (block_r * max(block_dim, BLOCK_INNER)) * 2 + + # Register pressure check + est_regs = _estimate_register_pressure( + config.num_warps, + (block_r, block_dim), # acc + (block_m, BLOCK_INNER), # input tile + (block_m, block_dim), # other tile + (block_r, BLOCK_INNER), # lora weight + ) + if est_regs > _MAX_REGS_SOFT_LIMIT: + continue + + if smem <= smem_cap - _SMEM_SLACK: + pruned.append(config) + + if pruned: + return pruned + configs.sort(key=lambda c: c.kwargs["BLOCK_M"] * c.kwargs["BLOCK_DIM"]) + return [configs[0]] + + +@triton.autotune( + configs=_group_bwd_split_configs(), + key=["M", "K", "N"], + prune_configs_by={"early_config_prune": _prune_split_configs}, +) +@triton.heuristics( + { + "NO_DIM_MASK": lambda args: ( + (args["K"] % args["BLOCK_DIM"]) == 0 + if args["COMPUTE_DA"] + else (args["N"] % args["BLOCK_DIM"]) == 0 + ), + } +) +@triton.jit +def _group_bwd_lora_split( + # Data tensors (DY and X are always present) + DY_ptr, + stride_dym, + stride_dyn, + X_ptr, + stride_xm, + stride_xk, + # LoRA weight for the inner reduction (B for dA, A for dB) + LW_ptr, + stride_lw0, + stride_lw1, + # Output gradient tensor (dA or dB) + OUT_ptr, + stride_out0, + stride_out1, + # Expert offsets + expert_offsets_ptr, + # Dimensions + M, + K: tl.constexpr, + N: tl.constexpr, + ACTUAL_R: tl.constexpr, + BLOCK_R: tl.constexpr, + INNER_DIM: tl.constexpr, # reduction dimension (N for dA, K for dB) + scaling, + # Mode flag + COMPUTE_DA: tl.constexpr, # True = compute dA, False = compute dB + # Tile sizes + BLOCK_M: tl.constexpr, + BLOCK_DIM: tl.constexpr, + ACC_TYPE: tl.constexpr, + allow_tf32: tl.constexpr, + NO_DIM_MASK: tl.constexpr, +): + """ + Unified split kernel for LoRA gradient computation. + + When COMPUTE_DA=True: + dA[e] = scaling * (dY @ B[e])^T @ X → [R, K] + Grid: (E, cdiv(K, BLOCK_DIM)) + - outer_ptr/stride = X (read [M, K_block]) + - inner reduction over N using DY and B + - output shape [BLOCK_R, BLOCK_DIM] + + When COMPUTE_DA=False: + dB[e] = scaling * dY^T @ (X @ A[e]^T) → [N, R] + Grid: (E, cdiv(N, BLOCK_DIM)) + - outer_ptr/stride = DY (read [M, N_block]) + - inner reduction over K using X and A + - output shape [BLOCK_DIM, BLOCK_R] + + No atomic adds — each (E, dim_block) pair is written by exactly one block. + """ + E_idx = tl.program_id(0) + dim_block_id = tl.program_id(1) + + if E_idx == 0: + start_idx = 0 + else: + start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int32) + end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int32) + num_tokens = end_idx - start_idx + + # Output dimension tile (K for dA, N for dB) + if COMPUTE_DA: + OUT_DIM: tl.constexpr = K # type: ignore[no-redef] + else: + OUT_DIM: tl.constexpr = N # type: ignore[no-redef] + dim_block = dim_block_id * BLOCK_DIM + tl.arange(0, BLOCK_DIM) + dim_mask = dim_block < OUT_DIM + R_block = tl.arange(0, BLOCK_R) + R_mask = R_block < ACTUAL_R + lora_offset = E_idx * ACTUAL_R + + # Output pointers — layout differs: dA is [R, K], dB is [N, R] + if COMPUTE_DA: + out_blk_ptrs = ( + OUT_ptr + + (lora_offset + R_block)[:, None] * stride_out0 + + dim_block[None, :] * stride_out1 + ) + out_mask = R_mask[:, None] & dim_mask[None, :] + else: + out_blk_ptrs = ( + OUT_ptr + + dim_block[:, None] * stride_out0 + + (lora_offset + R_block)[None, :] * stride_out1 + ) + out_mask = dim_mask[:, None] & R_mask[None, :] + + if num_tokens > 0: + M_block = tl.arange(0, BLOCK_M) + INPUT_DTYPE = X_ptr.dtype.element_ty + BLOCK_INNER: tl.constexpr = 64 + inner_iters = tl.cdiv(INNER_DIM, BLOCK_INNER) + + if COMPUTE_DA: + acc = tl.zeros((BLOCK_R, BLOCK_DIM), dtype=ACC_TYPE) + else: + acc = tl.zeros((BLOCK_DIM, BLOCK_R), dtype=ACC_TYPE) + + M_iters = tl.cdiv(num_tokens, BLOCK_M) + for i in range(M_iters): + M_idx = start_idx + i * BLOCK_M + M_block + M_mask = M_idx < end_idx + + if COMPUTE_DA: + # Load X[M, K_block] (the "outer" tensor for dA) + outer = tl.load( + X_ptr + M_idx[:, None] * stride_xm + dim_block[None, :] * stride_xk, + mask=M_mask[:, None] & dim_mask[None, :], + other=0.0, + ).to(INPUT_DTYPE) + + # Reduce DY[M, :] @ B[e][:, R] over N → [M, R] + reduced = tl.zeros((BLOCK_M, BLOCK_R), dtype=ACC_TYPE) + inner_range = tl.arange(0, BLOCK_INNER) + for j in range(inner_iters): + inn_off = j * BLOCK_INNER + inner_range + inn_mask = inn_off < N + + dy_tile = tl.load( + DY_ptr + + M_idx[:, None] * stride_dym + + inn_off[None, :] * stride_dyn, + mask=M_mask[:, None] & inn_mask[None, :], + other=0.0, + ).to(INPUT_DTYPE) + # B layout: [N, r*E] → stride_lw0=N stride, stride_lw1=r*E stride + lw_tile = tl.load( + LW_ptr + + inn_off[:, None] * stride_lw0 + + (lora_offset + R_block)[None, :] * stride_lw1, + mask=inn_mask[:, None] & R_mask[None, :], + other=0.0, + ).to(INPUT_DTYPE) + reduced += tl.dot(dy_tile, lw_tile, allow_tf32=allow_tf32) + + # dA += (DY@B)^T @ X: [R, M] @ [M, K_block] → [R, K_block] + acc += tl.dot( + tl.trans(reduced.to(INPUT_DTYPE)), outer, allow_tf32=allow_tf32 + ) + else: + # Load DY[M, N_block] (the "outer" tensor for dB) + outer = tl.load( + DY_ptr + + M_idx[:, None] * stride_dym + + dim_block[None, :] * stride_dyn, + mask=M_mask[:, None] & dim_mask[None, :], + other=0.0, + ).to(INPUT_DTYPE) + + # Reduce X[M, :] @ A[e][:, :].T over K → [M, R] + reduced = tl.zeros((BLOCK_M, BLOCK_R), dtype=ACC_TYPE) + inner_range = tl.arange(0, BLOCK_INNER) + for j in range(inner_iters): + inn_off = j * BLOCK_INNER + inner_range + inn_mask = inn_off < K + + x_tile = tl.load( + X_ptr + + M_idx[:, None] * stride_xm + + inn_off[None, :] * stride_xk, + mask=M_mask[:, None] & inn_mask[None, :], + other=0.0, + ).to(INPUT_DTYPE) + # A layout: [r*E, K] → stride_lw0=r*E stride, stride_lw1=K stride + # We want A[e]^T: [K, R], so load as [K_inner, R] + lw_tile = tl.load( + LW_ptr + + (lora_offset + R_block)[None, :] * stride_lw0 + + inn_off[:, None] * stride_lw1, + mask=inn_mask[:, None] & R_mask[None, :], + other=0.0, + ).to(INPUT_DTYPE) + reduced += tl.dot(x_tile, lw_tile, allow_tf32=allow_tf32) + + # dB += DY^T @ (X@A^T): [N_block, M] @ [M, R] → [N_block, R] + acc += tl.dot( + tl.trans(outer), reduced.to(INPUT_DTYPE), allow_tf32=allow_tf32 + ) + + tl.store( + out_blk_ptrs, (acc * scaling).to(OUT_ptr.dtype.element_ty), mask=out_mask + ) + else: + # Zero out this expert's slice — needed because output uses empty_like + if COMPUTE_DA: + tl.store( + out_blk_ptrs, + tl.zeros((BLOCK_R, BLOCK_DIM), dtype=OUT_ptr.dtype.element_ty), + mask=out_mask, + ) + else: + tl.store( + out_blk_ptrs, + tl.zeros((BLOCK_DIM, BLOCK_R), dtype=OUT_ptr.dtype.element_ty), + mask=out_mask, + ) + + def group_bwd_lora( DY: torch.Tensor, X: torch.Tensor, @@ -1344,6 +1830,9 @@ def group_bwd_lora( """ Compute LoRA gradients for A and B on expert-grouped data. + Uses split dA/dB kernels that eliminate atomic adds by giving each + (expert, output_block) pair its own thread block. + Args: DY: Gradient w.r.t. output [M_total, N] (grouped by expert) X: Input [M_total, K] (grouped by expert) @@ -1361,34 +1850,55 @@ def group_bwd_lora( K = X.size(1) N = DY.size(1) - # Zero-init for atomic accumulation - dA = torch.zeros_like(lora_A) - dB = torch.zeros_like(lora_B) + # No zero-init needed: the split kernels write zeros for experts with + # zero routed tokens directly in the kernel (else branch). + dA = torch.empty_like(lora_A) + dB = torch.empty_like(lora_B) BLOCK_R = _block_r_for_rank(R) - def grid(META): - return ( - E * triton.cdiv(K, META["BLOCK_K"]), - triton.cdiv(N, META["BLOCK_N"]), - ) + def grid_dA(META): + return (E, triton.cdiv(K, META["BLOCK_DIM"])) - _group_bwd_lora[grid]( + _group_bwd_lora_split[grid_dA]( DY, DY.stride(0), DY.stride(1), X, X.stride(0), X.stride(1), - lora_A, - lora_A.stride(0), - lora_A.stride(1), lora_B, lora_B.stride(0), lora_B.stride(1), dA, dA.stride(0), dA.stride(1), + expert_offsets, + M=DY.size(0), + K=K, + N=N, + ACTUAL_R=R, + BLOCK_R=BLOCK_R, + INNER_DIM=N, + scaling=scaling, + COMPUTE_DA=True, + ACC_TYPE=tl.float32, + allow_tf32=ALLOW_TF32, + ) + + def grid_dB(META): + return (E, triton.cdiv(N, META["BLOCK_DIM"])) + + _group_bwd_lora_split[grid_dB]( + DY, + DY.stride(0), + DY.stride(1), + X, + X.stride(0), + X.stride(1), + lora_A, + lora_A.stride(0), + lora_A.stride(1), dB, dB.stride(0), dB.stride(1), @@ -1396,9 +1906,11 @@ def grid(META): M=DY.size(0), K=K, N=N, - ACTUAL_R=R, # True LoRA rank - BLOCK_R=BLOCK_R, # Padded tile size + ACTUAL_R=R, + BLOCK_R=BLOCK_R, + INNER_DIM=K, scaling=scaling, + COMPUTE_DA=False, ACC_TYPE=tl.float32, allow_tf32=ALLOW_TF32, ) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py index 5125e8801d..c6c01e255a 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py @@ -490,19 +490,70 @@ def forward(self: nn.Module, layer_input: torch.Tensor): experts, gup_lora, down_lora = _unwrap_experts_lora(self.experts) # ==================================================================== - # Gate + Up projection + # Selective expert weight dequantization # ==================================================================== - gate_up_W = experts.gate_up_proj.transpose(2, 1) # [E, hidden, 2*inter] + # When experts are BnB-quantized (quantize_moe_experts), dequantize + # only the active experts instead of all E. This saves ~97% memory + # for the transient dequant buffer when few experts are active. + use_selective = ( + getattr(self, "_use_selective_dequant", False) + and hasattr(experts, "parametrizations") + and "gate_up_proj" in experts.parametrizations + ) + + if use_selective: + from axolotl.integrations.kernels.libs.scattermoe_lora.selective_dequant import ( + get_active_experts, + remap_expert_indices, + selective_expert_weights, + selective_lora_weights, + ) + active_experts = get_active_experts(sorted_expert_idxs, num_experts) + remapped_expert_idxs, compact_offsets = remap_expert_indices( + sorted_expert_idxs, + expert_offsets, + active_experts, + num_experts, + ) + # Dequantize only active experts' weights + gate_up_W = selective_expert_weights( + experts, + "gate_up_proj", + active_experts, + ).transpose(2, 1) # [num_active, hidden, 2*inter] + + # Remap LoRA weights to match compact expert indices + if gup_lora is not None: + gup_A, gup_B, gup_scaling = gup_lora + gup_A, gup_B = selective_lora_weights( + gup_A, + gup_B, + active_experts, + num_experts, + ) + gup_lora = (gup_A, gup_B, gup_scaling) + + # Use remapped indices for ScatterMoE kernels + sei_gup = remapped_expert_idxs + eo_gup = compact_offsets + else: + gate_up_W = experts.gate_up_proj.transpose(2, 1) # [E, hidden, 2*inter] + sei_gup = sorted_expert_idxs + eo_gup = expert_offsets + + # ==================================================================== + # Gate + Up projection + # ==================================================================== if gup_lora is not None: gup_A, gup_B, gup_scaling = gup_lora gup = parallel_linear_lora( hidden_states_flat, gate_up_W, top_k, - sorted_expert_idxs, + sei_gup, sorted_scattered_idxs, - expert_offsets, + eo_gup, lora_A=gup_A, lora_B=gup_B, scaling=gup_scaling, @@ -516,9 +567,9 @@ def forward(self: nn.Module, layer_input: torch.Tensor): hidden_states_flat, gate_up_W, top_k, - sorted_expert_idxs, + sei_gup, sorted_scattered_idxs, - expert_offsets, + eo_gup, grouped_in=False, grouped_out=True, ) @@ -529,7 +580,29 @@ def forward(self: nn.Module, layer_input: torch.Tensor): # ==================================================================== # Down projection # ==================================================================== - down_W = experts.down_proj.transpose(2, 1) # [E, inter, hidden] + if use_selective: + down_W = selective_expert_weights( + experts, + "down_proj", + active_experts, + ).transpose(2, 1) # [num_active, inter, hidden] + + if down_lora is not None: + down_A, down_B, down_scaling = down_lora + down_A, down_B = selective_lora_weights( + down_A, + down_B, + active_experts, + num_experts, + ) + down_lora = (down_A, down_B, down_scaling) + + sei_down = remapped_expert_idxs + eo_down = compact_offsets + else: + down_W = experts.down_proj.transpose(2, 1) # [E, inter, hidden] + sei_down = sorted_expert_idxs + eo_down = expert_offsets if down_lora is not None: down_A, down_B, down_scaling = down_lora @@ -537,9 +610,9 @@ def forward(self: nn.Module, layer_input: torch.Tensor): h, down_W, 1, - sorted_expert_idxs, + sei_down, sorted_scattered_idxs, - expert_offsets, + eo_down, lora_A=down_A, lora_B=down_B, scaling=down_scaling, @@ -554,9 +627,9 @@ def forward(self: nn.Module, layer_input: torch.Tensor): h, down_W, 1, - sorted_expert_idxs, + sei_down, sorted_scattered_idxs, - expert_offsets, + eo_down, grouped_in=True, grouped_out=False, gates=routing_weights, diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py new file mode 100644 index 0000000000..1df8b2f684 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py @@ -0,0 +1,282 @@ +""" +Selective Expert Dequantization +=============================== + +Instead of dequantizing all E expert weight matrices at once (which creates +a ~1 GB transient buffer for 256 experts), only dequantize the experts that +are actually routed to by the current batch's top-k selection. + +For Qwen3.5-35B-A3B (E=256, top_k=8, hidden=2048, intermediate=512): + - Full dequant: [256, 2048, 1024] = 1,074 MB per projection + - Selective (8 active): [8, 2048, 1024] = 33.5 MB per projection + - Savings: ~97% memory reduction per layer + +This module provides format-agnostic selective weight extraction: + - BnB 4-bit (nf4/fp4): slice quantized data + absmax per expert + - bf16/fp32: direct indexing (no dequant needed) + - FP8: slice + cast + +The ScatterMoE kernel itself doesn't change — we remap expert indices +from global (0..E-1) to compact (0..num_active-1) and pass the smaller +weight tensor. +""" + +import torch +import torch.nn as nn + + +def get_active_experts(sorted_expert_idxs: torch.Tensor, E: int) -> torch.Tensor: + """Get sorted unique expert indices from the routing output. + + Args: + sorted_expert_idxs: Expert assignments sorted by expert id [T*k] + E: Total number of experts + + Returns: + active: Sorted unique expert indices [num_active] + """ + return torch.unique(sorted_expert_idxs) + + +def remap_expert_indices( + sorted_expert_idxs: torch.Tensor, + expert_offsets: torch.Tensor, + active_experts: torch.Tensor, + E: int, +) -> tuple[torch.Tensor, torch.Tensor]: + """Remap global expert indices to compact indices. + + Maps expert ids from [0..E-1] to [0..num_active-1], preserving the + sort order. Also compacts expert_offsets to only active experts. + + Args: + sorted_expert_idxs: [T*k] expert ids in sorted order + expert_offsets: [E] cumulative token counts (original) + active_experts: [num_active] sorted unique expert ids + E: Total number of experts + + Returns: + remapped_idxs: [T*k] expert ids in [0..num_active-1] + compact_offsets: [num_active] cumulative token counts + """ + # Build remap table: global_id -> compact_id + remap = torch.empty(E, dtype=torch.long, device=sorted_expert_idxs.device) + remap[active_experts] = torch.arange( + len(active_experts), device=sorted_expert_idxs.device + ) + + remapped_idxs = remap[sorted_expert_idxs] + + # Compact the expert_offsets: only keep active experts' cumulative counts + compact_offsets = expert_offsets[active_experts] + + return remapped_idxs, compact_offsets + + +def _selective_dequant_bnb4( + raw_param: torch.Tensor, + quant_state, + active_experts: torch.Tensor, + expert_shape: tuple[int, int], +) -> torch.Tensor: + """Dequantize only selected experts from BnB 4-bit packed data. + + The raw parameter is a flattened 4-bit packed tensor. Each expert's + data is contiguous (stored in expert-major order), so we can gather + the packed data and absmax blocks for active experts, then dequantize + as one contiguous block. + + Args: + raw_param: Flattened uint8 tensor of packed 4-bit weights + quant_state: BnB QuantState with absmax, blocksize, code, etc. + active_experts: [num_active] expert indices to dequantize + expert_shape: (dim1, dim2) shape per expert (e.g. (1024, 2048)) + + Returns: + Dequantized weights [num_active, dim1, dim2] in original dtype + """ + import bitsandbytes.functional as F # noqa: N812 + from bitsandbytes.functional import QuantState + + expert_numel = expert_shape[0] * expert_shape[1] + packed_per_expert = expert_numel // 2 # 4-bit = 2 values per byte + blocks_per_expert = expert_numel // quant_state.blocksize + num_active = len(active_experts) + + if blocks_per_expert == 0: + # Expert is smaller than one quantization block — blocks span across + # expert boundaries, so per-expert slicing isn't possible. + # Fallback: full dequantize + index. + full = F.dequantize_4bit(raw_param, quant_state) + E_total = full.numel() // expert_numel + return full.reshape(E_total, *expert_shape)[active_experts] + + # Use fused Triton kernel for NF4 (handles selective gather + dequant in one pass) + if quant_state.quant_type == "nf4" and raw_param.dtype == torch.uint8: + from axolotl.integrations.kernels.libs.scattermoe_lora.selective_dequant_kernel import ( + selective_dequant_nf4_triton, + ) + + # Handle nested (double) quantization: dequantize absmax first + # BnB uses dequantize_blockwise (not _4bit) for nested absmax + offset + if quant_state.nested: + absmax = F.dequantize_blockwise(quant_state.absmax, quant_state.state2) + absmax += quant_state.offset + if absmax.dtype != torch.float32: + absmax = absmax.float() + else: + absmax = quant_state.absmax + + return selective_dequant_nf4_triton( + packed_data=raw_param, + absmax=absmax, + active_experts=active_experts, + expert_shape=expert_shape, + blocksize=quant_state.blocksize, + dtype=quant_state.dtype, + codebook=quant_state.code, + ) + + # Fallback: gather + BnB dequant (for fp4 or non-uint8 packed formats) + raw_flat = raw_param.reshape(-1) + + offsets_qt = ( + active_experts.long()[:, None] * packed_per_expert + + torch.arange(packed_per_expert, device=raw_param.device)[None, :] + ).reshape(-1) + qt_gathered = raw_flat[offsets_qt] + + offsets_abs = ( + active_experts.long()[:, None] * blocks_per_expert + + torch.arange(blocks_per_expert, device=raw_param.device)[None, :] + ).reshape(-1) + + if quant_state.nested: + full_absmax = F.dequantize_blockwise(quant_state.absmax, quant_state.state2) + full_absmax += quant_state.offset + if full_absmax.dtype != torch.float32: + full_absmax = full_absmax.float() + absmax_gathered = full_absmax[offsets_abs] + else: + absmax_gathered = quant_state.absmax[offsets_abs] + + qt_gathered = qt_gathered.unsqueeze(1) if qt_gathered.dim() == 1 else qt_gathered + + gathered_qs = QuantState( + absmax=absmax_gathered, + shape=torch.Size([num_active * expert_numel]), + blocksize=quant_state.blocksize, + quant_type=quant_state.quant_type, + code=quant_state.code, + dtype=quant_state.dtype, + ) + + deq = F.dequantize_4bit(qt_gathered, gathered_qs) + return deq.reshape(num_active, *expert_shape) + + +def _selective_index_dense( + param: torch.Tensor, + active_experts: torch.Tensor, +) -> torch.Tensor: + """Select experts from a dense (bf16/fp32) weight tensor. + + Simple indexing — no dequantization needed. + """ + return param[active_experts] + + +def selective_expert_weights( + experts_module: nn.Module, + param_name: str, + active_experts: torch.Tensor, +) -> torch.Tensor: + """Extract and dequantize only the active experts' weights. + + Format-agnostic: dispatches based on whether the parameter is + BnB 4-bit quantized (via parametrize), FP8, or dense bf16/fp32. + + Args: + experts_module: The base experts module (e.g. Qwen3_5MoeExperts) + param_name: "gate_up_proj" or "down_proj" + active_experts: [num_active] sorted unique expert indices + + Returns: + Compact weight tensor [num_active, dim1, dim2] ready for ScatterMoE + """ + # Check if the parameter is BnB-quantized via parametrize + if ( + hasattr(experts_module, "parametrizations") + and param_name in experts_module.parametrizations + ): + param_list = experts_module.parametrizations[param_name] + parametrization = param_list[0] + + # BnB 4-bit parametrization + if hasattr(parametrization, "quant_state"): + # The raw quantized data is on the ParametrizationList, not the + # individual Bnb4bitParametrization module + raw_param = param_list.original + qs = parametrization.quant_state + # qs.shape is the original tensor shape before flattening. + # For MoE experts it's [E, d1, d2] (3D) or [total_elements] (1D). + orig_shape = qs.shape + if isinstance(orig_shape, torch.Size) and len(orig_shape) == 3: + expert_shape = (orig_shape[1], orig_shape[2]) + elif isinstance(orig_shape, torch.Size) and len(orig_shape) == 1: + # Flattened — need to infer from module attributes + E_total = getattr(experts_module, "num_experts", None) + if E_total is None: + E_total = int(active_experts.max().item()) + 1 + expert_numel = orig_shape[0] // E_total + d2 = getattr(experts_module, "hidden_dim", None) or getattr( + experts_module, "intermediate_dim", None + ) + if d2 and expert_numel % d2 == 0: + expert_shape = (expert_numel // d2, d2) + else: + full = getattr(experts_module, param_name) + return full[active_experts] + else: + full = getattr(experts_module, param_name) + return full[active_experts] + + return _selective_dequant_bnb4(raw_param, qs, active_experts, expert_shape) + + # Dense parameter (bf16/fp32) — direct indexing + param = getattr(experts_module, param_name) + if param.dim() == 3: + return param[active_experts] + + # Fallback: full access + return param + + +def selective_lora_weights( + lora_A: torch.Tensor, + lora_B: torch.Tensor, + active_experts: torch.Tensor, + E: int, +) -> tuple[torch.Tensor, torch.Tensor]: + """Select LoRA A and B weights for only the active experts. + + LoRA layout (scattermoe format): + A: [r*E, K] — expert e occupies rows [e*r : (e+1)*r] + B: [N, r*E] — expert e occupies cols [e*r : (e+1)*r] + + Returns compact: + A: [r*num_active, K] + B: [N, r*num_active] + """ + R = lora_A.size(0) // E + + # Vectorized gather: active_experts[:, None] * R + arange(R)[None, :] + row_idx = ( + active_experts.long()[:, None] * R + + torch.arange(R, device=lora_A.device)[None, :] + ).reshape(-1) + + compact_A = lora_A[row_idx] # [r*num_active, K] + compact_B = lora_B[:, row_idx] # [N, r*num_active] + + return compact_A, compact_B diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant_kernel.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant_kernel.py new file mode 100644 index 0000000000..aa9f0278a4 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant_kernel.py @@ -0,0 +1,179 @@ +""" +Triton kernel for fused selective expert gather + NF4 dequantization. + +Instead of: + 1. Gather packed uint8 data for active experts (memory copy) + 2. Gather absmax for active experts (memory copy) + 3. Call BnB dequantize_4bit CUDA kernel + +This kernel does all three in one pass: + - Reads packed NF4 bytes from expert-strided positions + - Looks up the NF4 codebook + - Multiplies by the per-block absmax + - Writes bf16 output directly + +This eliminates the intermediate gather buffer entirely. +""" + +import torch +import triton +import triton.language as tl + +# NF4 codebook (16 values, precomputed by BnB) +# These are the normalized float4 reconstruction values +NF4_CODEBOOK = [ + -1.0, + -0.6961928009986877, + -0.5250730514526367, + -0.39491748809814453, + -0.28444138169288635, + -0.18477343022823334, + -0.09105003625154495, + 0.0, + 0.07958029955625534, + 0.16093020141124725, + 0.24611230194568634, + 0.33791524171829224, + 0.44070982933044434, + 0.5626170039176941, + 0.7229568362236023, + 1.0, +] + + +@triton.jit +def _selective_dequant_nf4_kernel( + # Input: packed NF4 data (flattened, expert-major order) + packed_ptr, + # Input: absmax values (flattened, expert-major order) + absmax_ptr, + # Input: active expert indices + active_experts_ptr, + # Input: NF4 codebook (16 float values) + codebook_ptr, + # Output: dequantized bf16 weights [num_active, expert_numel] + out_ptr, + stride_out_e, # stride for expert dim in output + # Dimensions + num_active, + packed_per_expert, # expert_numel // 2 + blocks_per_expert, # expert_numel // blocksize + blocksize: tl.constexpr, + # Tile size + BLOCK_SIZE: tl.constexpr, # elements per thread block (must be multiple of 2) +): + """ + Each program processes BLOCK_SIZE elements from one expert. + + Grid: (num_active, cdiv(expert_numel, BLOCK_SIZE)) + + For each output element: + 1. Compute which byte in packed data contains this element + 2. Extract the 4-bit nibble (high or low) + 3. Look up in NF4 codebook + 4. Scale by absmax for this block + """ + expert_local_idx = tl.program_id(0) # which active expert (0..num_active-1) + block_id = tl.program_id(1) # which element block + + # Load the global expert index + expert_global = tl.load(active_experts_ptr + expert_local_idx).to(tl.int64) + + expert_numel = packed_per_expert * 2 # 2 elements per packed byte + elem_offset = block_id * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = elem_offset < expert_numel + + # Each element is packed as: byte[i//2], low nibble for even i, high for odd i + byte_idx = elem_offset // 2 + is_high = (elem_offset % 2) == 1 + + # Read packed bytes from the global expert's region + packed_global_offset = expert_global * packed_per_expert + byte_idx + packed_bytes = tl.load(packed_ptr + packed_global_offset, mask=mask, other=0).to( + tl.int32 + ) + + # Extract 4-bit nibble + # BnB packing: high nibble = even element, low nibble = odd element + nibble = tl.where(is_high, packed_bytes & 0xF, (packed_bytes >> 4) & 0xF) + + # NF4 codebook lookup + # Load all 16 codebook values (small, fits in registers) + # Use gather from codebook pointer + code_val = tl.load(codebook_ptr + nibble, mask=mask, other=0.0) + + # Load absmax for this element's quantization block + block_idx = elem_offset // blocksize + absmax_global_offset = expert_global * blocks_per_expert + block_idx + absmax_val = tl.load(absmax_ptr + absmax_global_offset, mask=mask, other=1.0) + + # Dequantize: value = codebook[nibble] * absmax + result = code_val * absmax_val + + # Store to output + out_offset = expert_local_idx * stride_out_e + elem_offset + tl.store(out_ptr + out_offset, result.to(out_ptr.dtype.element_ty), mask=mask) + + +def selective_dequant_nf4_triton( + packed_data: torch.Tensor, + absmax: torch.Tensor, + active_experts: torch.Tensor, + expert_shape: tuple[int, int], + blocksize: int, + dtype: torch.dtype = torch.bfloat16, + codebook: torch.Tensor | None = None, +) -> torch.Tensor: + """Fused selective gather + NF4 dequantization via Triton kernel. + + Args: + packed_data: Flattened packed NF4 data [total_packed] or [total_packed, 1] + absmax: Per-block scaling factors [total_blocks] + active_experts: Sorted indices of experts to dequantize [num_active] + expert_shape: (dim1, dim2) per expert + blocksize: Quantization block size + dtype: Output dtype (default bf16) + codebook: NF4 lookup table [16] (uses default NF4 codebook if None) + + Returns: + Dequantized weights [num_active, dim1, dim2] + """ + num_active = active_experts.shape[0] + expert_numel = expert_shape[0] * expert_shape[1] + packed_per_expert = expert_numel // 2 + blocks_per_expert = expert_numel // blocksize + + # Prepare codebook on device + if codebook is None: + codebook = torch.tensor( + NF4_CODEBOOK, dtype=torch.float32, device=packed_data.device + ) + else: + codebook = codebook.to(device=packed_data.device, dtype=torch.float32) + + # Flatten inputs + packed_flat = packed_data.reshape(-1) + absmax_flat = absmax.reshape(-1).float() # absmax is usually fp32 + + # Output buffer + out = torch.empty(num_active, expert_numel, dtype=dtype, device=packed_data.device) + + BLOCK_SIZE = 1024 # Process 1024 elements per thread block + + grid = (num_active, triton.cdiv(expert_numel, BLOCK_SIZE)) + + _selective_dequant_nf4_kernel[grid]( + packed_flat, + absmax_flat, + active_experts, + codebook, + out, + out.stride(0), + num_active=num_active, + packed_per_expert=packed_per_expert, + blocks_per_expert=blocks_per_expert, + blocksize=blocksize, + BLOCK_SIZE=BLOCK_SIZE, + ) + + return out.reshape(num_active, *expert_shape) diff --git a/src/axolotl/integrations/kernels/plugin.py b/src/axolotl/integrations/kernels/plugin.py index 351db5ef23..939bdb790d 100644 --- a/src/axolotl/integrations/kernels/plugin.py +++ b/src/axolotl/integrations/kernels/plugin.py @@ -61,7 +61,16 @@ def get_input_args(self): return "axolotl.integrations.kernels.KernelsArgs" def pre_model_load(self, cfg): + from axolotl.integrations.kernels.constants import SPARSE_MOE_BLOCK + + # Prefer text backbone type for VLMs, but fall back to base type + # when the text type isn't in the supported mapping (e.g. qwen3_5_moe_text) moe_model_type = cfg.model_config_type_text or cfg.model_config_type + if ( + moe_model_type not in SPARSE_MOE_BLOCK + and cfg.model_config_type in SPARSE_MOE_BLOCK + ): + moe_model_type = cfg.model_config_type if cfg.use_scattermoe: self._register_kernels() diff --git a/src/axolotl/kernels/lora.py b/src/axolotl/kernels/lora.py index 28ef75acc0..9dc66a918f 100644 --- a/src/axolotl/kernels/lora.py +++ b/src/axolotl/kernels/lora.py @@ -640,7 +640,9 @@ def backward( del q_weight del q_weight_t if A_q is not None and B_q is not None: - grad_X.addmm_(q_grad, torch.mm(B_q_scaled, A_q_scaled)) + # Stay decomposed: dQ @ B^T gives [T, R], then [T, R] @ (s*A) gives [T, in] + # This is 65x fewer FLOPs than materializing B@A into [out, in] + grad_X.addmm_(torch.mm(q_grad, B_q_scaled), A_q_scaled) # K path k_weight_t = dequantize(k_weight, k_quant) @@ -648,7 +650,7 @@ def backward( del k_weight del k_weight_t if A_k is not None and B_k is not None: - grad_X.addmm_(k_grad, torch.mm(B_k_scaled, A_k_scaled)) + grad_X.addmm_(torch.mm(k_grad, B_k_scaled), A_k_scaled) # V path v_weight_t = dequantize(v_weight, v_quant) @@ -656,7 +658,7 @@ def backward( del v_weight del v_weight_t if A_v is not None and B_v is not None: - grad_X.addmm_(v_grad, torch.mm(B_v_scaled, A_v_scaled)) + grad_X.addmm_(torch.mm(v_grad, B_v_scaled), A_v_scaled) # Transpose gradients if needed if d_A_q is not None: @@ -819,7 +821,8 @@ def backward( del W A, B = A.to(dtype), B.to(dtype) - dX += s * dY @ B @ A + # Stay decomposed: dY @ B gives [T, R], then [T, R] @ A gives [T, in] + dX.addmm_(torch.mm(dY, B), A, alpha=s) # W, b, W_quant, A, B, s return dX.view(batch, seq_len, hd), None, None, None, d_A.t(), d_B.t(), None diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 37c1123370..dd3f4ddfa7 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -505,6 +505,20 @@ def _set_device_map_config(self): elif not is_ds_zero3: self.model_kwargs["device_map"] = device_map + # quantize_moe_experts quantizes expert weights on-the-fly during loading, + # so the actual VRAM usage is much less than bf16 estimates. + # When device_map is "auto", accelerate's infer_auto_device_map computes + # the device map at bf16 size (before quantization), causing it to offload + # layers to CPU, which BnB then rejects. Force single-GPU placement to + # prevent this. Only applies to the non-FSDP, non-ZeRO3 path (DDP/single). + if getattr(self.cfg, "quantize_moe_experts", False) and device_map in ( + "auto", + None, + ): + self.model_kwargs["device_map"] = { + "": int(os.environ.get("LOCAL_RANK", 0)) + } + cur_device = get_device_type() if "mps" in str(cur_device): self.model_kwargs["device_map"] = "mps:0" diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 2972c62851..44be5267d7 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -51,6 +51,29 @@ value_states = value_states.view(hidden_shape).transpose(1, 2) """.lstrip("\n"), ), + ( + """ + query_states, gate = torch.chunk( + self.q_proj(hidden_states).view(*input_shape, -1, self.head_dim * 2), 2, dim=-1 + ) + gate = gate.reshape(*input_shape, -1) + + query_states = self.q_norm(query_states.view(hidden_shape)).transpose(1, 2) + key_states = self.k_norm(self.k_proj(hidden_states).view(hidden_shape)).transpose(1, 2) + value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) +""".lstrip("\n"), + """ + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states, gate = torch.chunk( + query_states.view(*input_shape, -1, self.head_dim * 2), 2, dim=-1 + ) + gate = gate.reshape(*input_shape, -1) + + query_states = self.q_norm(query_states.view(hidden_shape)).transpose(1, 2) + key_states = self.k_norm(key_states.view(hidden_shape)).transpose(1, 2) + value_states = value_states.view(hidden_shape).transpose(1, 2) +""".lstrip("\n"), + ), ] ORIGINAL_O_CODE = """ @@ -299,6 +322,8 @@ def get_layers(model: PeftModelForCausalLM) -> list[nn.Module]: if hasattr(pretrained_model, "language_model"): return pretrained_model.language_model.layers if hasattr(pretrained_model, "model"): + if hasattr(pretrained_model.model, "language_model"): + return pretrained_model.model.language_model.layers return pretrained_model.model.layers raise NotImplementedError( diff --git a/src/axolotl/utils/callbacks/profiler.py b/src/axolotl/utils/callbacks/profiler.py index 2cf5e0f4f9..49e8c53882 100644 --- a/src/axolotl/utils/callbacks/profiler.py +++ b/src/axolotl/utils/callbacks/profiler.py @@ -17,6 +17,8 @@ class PytorchProfilerCallback(TrainerCallback): """ PyTorch Profiler callback to create snapshots of GPU memory usage at specified steps. + + Also runs torch.profiler to produce a Chrome trace for timing analysis. """ def __init__(self, steps_to_profile: int = 5, profiler_steps_start: int = 0): @@ -26,9 +28,10 @@ def __init__(self, steps_to_profile: int = 5, profiler_steps_start: int = 0): if profiler_steps_start == 0: # start recording memory allocations before everything is allocated, because if we start # at the beginning of step 0, we won't have any memory allocations in the traces - torch.cuda.memory._record_memory_history(enabled="all") + torch.cuda.memory._record_memory_history(enabled="all", stacks="all") profiler_steps_start = -1 self.profiler_steps_start = profiler_steps_start + self._profiler = None def on_step_begin( self, @@ -38,7 +41,21 @@ def on_step_begin( **kwargs, ): if state.global_step == self.profiler_steps_start: - torch.cuda.memory._record_memory_history(enabled="all") + torch.cuda.memory._record_memory_history(enabled="all", stacks="all") + + # Start torch.profiler on the first profiled step + if state.global_step == max(self.profiler_steps_start, 0): + profiler = torch.profiler.profile( + activities=[ + torch.profiler.ProfilerActivity.CPU, + torch.profiler.ProfilerActivity.CUDA, + ], + record_shapes=True, + profile_memory=True, + with_stack=True, + ) + profiler.__enter__() + self._profiler = profiler def on_step_end( self, @@ -55,6 +72,13 @@ def on_step_end( # tell CUDA to stop recording memory allocations now torch.cuda.memory._record_memory_history(enabled=None) + # Stop and export torch.profiler trace + if self._profiler is not None: + self._profiler.__exit__(None, None, None) + trace_path = Path(args.output_dir) / "profiler_trace.json" + self._profiler.export_chrome_trace(str(trace_path)) + self._profiler = None + def on_train_end( self, args: TrainingArguments, @@ -73,3 +97,9 @@ def on_train_end( # tell CUDA to stop recording memory allocations now torch.cuda.memory._record_memory_history(enabled=None) + + if self._profiler is not None: + self._profiler.__exit__(None, None, None) + trace_path = Path(args.output_dir) / "profiler_trace.json" + self._profiler.export_chrome_trace(str(trace_path)) + self._profiler = None diff --git a/tests/e2e/patched/test_lora_llama_multipack.py b/tests/e2e/patched/test_lora_llama_multipack.py index 1833c750b3..5033cabc93 100644 --- a/tests/e2e/patched/test_lora_llama_multipack.py +++ b/tests/e2e/patched/test_lora_llama_multipack.py @@ -4,8 +4,7 @@ import unittest -import pytest -from transformers.utils import is_auto_gptq_available, is_torch_bf16_gpu_available +from transformers.utils import is_torch_bf16_gpu_available from axolotl.common.datasets import load_datasets from axolotl.train import train @@ -68,51 +67,3 @@ def test_lora_packing(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) - - @pytest.mark.skipif(not is_auto_gptq_available(), reason="auto-gptq not available") - @with_temp_dir - def test_lora_gptq_packed(self, temp_dir): - cfg = DictDefault( - { - "base_model": "lilmeaty/SmolLM2-135M-Instruct-GPTQ", - "model_type": "AutoModelForCausalLM", - "tokenizer_type": "AutoTokenizer", - "sequence_len": 1024, - "sample_packing": True, - "flash_attention": True, - "load_in_8bit": True, - "adapter": "lora", - "gptq": True, - "gptq_disable_exllama": True, - "lora_r": 32, - "lora_alpha": 64, - "lora_dropout": 0.05, - "lora_target_linear": True, - "val_set_size": 0.02, - "special_tokens": { - "pad_token": "<|endoftext|>", - }, - "datasets": [ - { - "path": "mhenrichsen/alpaca_2k_test", - "type": "alpaca", - }, - ], - "num_epochs": 2, - "max_steps": 20, - "save_steps": 0.5, - "micro_batch_size": 8, - "gradient_accumulation_steps": 1, - "output_dir": temp_dir, - "learning_rate": 0.00001, - "optimizer": "adamw_torch_fused", - "lr_scheduler": "cosine", - "save_first_step": False, - } - ) - cfg = validate_config(cfg) - normalize_config(cfg) - dataset_meta = load_datasets(cfg=cfg) - - train(cfg=cfg, dataset_meta=dataset_meta) - check_model_output_exists(temp_dir, cfg) diff --git a/tests/integrations/test_scattermoe_lora_kernels.py b/tests/integrations/test_scattermoe_lora_kernels.py new file mode 100644 index 0000000000..fc783fa1d9 --- /dev/null +++ b/tests/integrations/test_scattermoe_lora_kernels.py @@ -0,0 +1,407 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +Unit tests for ScatterMoE LoRA Triton kernels. + +Tests correctness of: + - scatter2scatter_lora (forward) + - scatter2scatter_lora_dX (backward input gradient) + - group_bwd_lora (backward LoRA weight gradients via split dA/dB) + - ScatterMoELoRA autograd function (full forward + backward) + +Each kernel is tested against a pure PyTorch per-expert-loop reference +implementation at multiple model shapes and LoRA ranks. +""" + +import pytest +import torch + +from axolotl.integrations.kernels.libs.scattermoe_lora.kernels import ( + lora_ops, + ops as base_ops, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_linear_lora import ( + ScatterMoELoRA, +) + +DEVICE = "cuda" +DTYPE = torch.bfloat16 + + +def _requires_cuda(): + return pytest.mark.skipif( + not torch.cuda.is_available(), reason="CUDA not available" + ) + + +pytestmark = _requires_cuda() + + +# ─── Helpers ───────────────────────────────────────────────────────────────── + + +def _setup(E, K, N, T, top_k, R, seed=42): + """Create synthetic expert weights, LoRA, routing, and grouped inputs.""" + torch.manual_seed(seed) + x = torch.randn(T, K, device=DEVICE, dtype=DTYPE) + W = torch.randn(E, K, N, device=DEVICE, dtype=DTYPE) * 0.02 + lora_A = torch.randn(R * E, K, device=DEVICE, dtype=DTYPE) * 0.01 + lora_B = torch.randn(N, R * E, device=DEVICE, dtype=DTYPE) * 0.01 + logits = torch.randn(T, E, device=DEVICE) + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), top_k, dim=-1) + sei, ssi, eo = flatten_sort_count(top_idx, E) + return x, W, lora_A, lora_B, sei, ssi, eo + + +def _reference_fwd(x, W, sei, ssi, eo, k, lora_A, lora_B, scaling, E): + """Per-expert loop reference: Y = X@W + scaling*(X@A^T)@B^T.""" + grouped_x = base_ops.group(x, ssi, fan_out=k) + M, N = grouped_x.size(0), W.size(2) + R = lora_A.size(0) // E + out = torch.zeros(M, N, device=DEVICE, dtype=DTYPE) + for e in range(E): + s = eo[e - 1].item() if e > 0 else 0 + end = eo[e].item() + if s == end: + continue + xe = grouped_x[s:end].float() + we = W[e].float() + ae = lora_A[e * R : (e + 1) * R].float() + be = lora_B[:, e * R : (e + 1) * R].float() + out[s:end] = (xe @ we + scaling * (xe @ ae.T) @ be.T).to(DTYPE) + result = torch.zeros(M, N, device=DEVICE, dtype=DTYPE) + result[ssi] = out + return result + + +def _reference_dX(dy_grouped, W, sei, ssi, eo, lora_A, lora_B, scaling, E): + """Per-expert loop reference: dX = dY@W^T + scaling*(dY@B)@A.""" + M, K = dy_grouped.size(0), W.size(1) + R = lora_A.size(0) // E + out = torch.zeros(M, K, device=DEVICE, dtype=DTYPE) + for e in range(E): + s = eo[e - 1].item() if e > 0 else 0 + end = eo[e].item() + if s == end: + continue + dye = dy_grouped[s:end].float() + we = W[e].float() + ae = lora_A[e * R : (e + 1) * R].float() + be = lora_B[:, e * R : (e + 1) * R].float() + out[s:end] = (dye @ we.T + scaling * (dye @ be) @ ae).to(DTYPE) + result = torch.zeros(M, K, device=DEVICE, dtype=DTYPE) + result[ssi] = out + return result + + +def _reference_bwd_lora(dy, grouped_x, lora_A, lora_B, eo, E, scaling): + """Per-expert loop reference: dA, dB for LoRA weight gradients.""" + R = lora_A.size(0) // E + dA = torch.zeros_like(lora_A) + dB = torch.zeros_like(lora_B) + for e in range(E): + s = eo[e - 1].item() if e > 0 else 0 + end = eo[e].item() + if s == end: + continue + xe = grouped_x[s:end].float() + dye = dy[s:end].float() + ae = lora_A[e * R : (e + 1) * R].float() + be = lora_B[:, e * R : (e + 1) * R].float() + dA[e * R : (e + 1) * R] = (scaling * (dye @ be).T @ xe).to(DTYPE) + dB[:, e * R : (e + 1) * R] = (scaling * dye.T @ (xe @ ae.T)).to(DTYPE) + return dA, dB + + +# ─── Model shape configs ──────────────────────────────────────────────────── + +# (E, K, N, T, top_k, R, description) +CONFIGS_SMALL = [ + (32, 128, 64, 64, 2, 4, "tiny"), + (64, 256, 128, 128, 4, 8, "small"), +] + +CONFIGS_REAL = [ + (256, 2048, 1024, 2048, 8, 16, "qwen35_gate_up"), + (256, 512, 2048, 2048, 8, 16, "qwen35_down"), + (64, 2048, 2048, 2048, 8, 16, "olmoe_gate_up"), + (128, 2048, 1536, 2048, 8, 16, "qwen3_gate_up"), +] + +SCALING = 2.0 + + +# ─── Forward tests ────────────────────────────────────────────────────────── + + +class TestScatter2ScatterLoRAForward: + """Test scatter2scatter_lora forward kernel vs reference.""" + + @pytest.fixture(params=CONFIGS_SMALL + CONFIGS_REAL) + def config(self, request): + return request.param + + def test_matches_reference(self, config): + E, K, N, T, k, R, desc = config + x, W, lA, lB, sei, ssi, eo = _setup(E, K, N, T, k, R) + + kernel_out = lora_ops.scatter2scatter_lora( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + lora_A=lA, + lora_B=lB, + scaling=SCALING, + ) + ref_out = _reference_fwd(x, W, sei, ssi, eo, k, lA, lB, SCALING, E) + + err = (kernel_out.float() - ref_out.float()).abs().max().item() + assert err < 1.0, f"[{desc}] fwd max_err={err}" + + def test_output_shape(self, config): + E, K, N, T, k, R, desc = config + x, W, lA, lB, sei, ssi, eo = _setup(E, K, N, T, k, R) + + out = lora_ops.scatter2scatter_lora( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + lora_A=lA, + lora_B=lB, + scaling=SCALING, + ) + assert out.shape == (T * k, N) + assert out.dtype == DTYPE + + +# ─── Backward dX tests ────────────────────────────────────────────────────── + + +class TestScatter2ScatterLoRADX: + """Test scatter2scatter_lora_dX backward kernel vs reference.""" + + @pytest.fixture(params=CONFIGS_SMALL + CONFIGS_REAL) + def config(self, request): + return request.param + + def test_matches_reference(self, config): + E, K, N, T, k, R, desc = config + x, W, lA, lB, sei, ssi, eo = _setup(E, K, N, T, k, R) + gx = base_ops.group(x, ssi, fan_out=k) + dy = torch.randn(gx.size(0), N, device=DEVICE, dtype=DTYPE) + + kernel_dx = lora_ops.scatter2scatter_lora_dX( + DY=dy, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=1, + lora_A=lA, + lora_B=lB, + scaling=SCALING, + dy_grouped=True, + dx_grouped=False, + ) + ref_dx = _reference_dX(dy, W, sei, ssi, eo, lA, lB, SCALING, E) + + err = (kernel_dx.float() - ref_dx.float()).abs().max().item() + assert err < 1.0, f"[{desc}] dX max_err={err}" + + +# ─── Backward LoRA gradient tests ─────────────────────────────────────────── + + +class TestGroupBwdLoRA: + """Test group_bwd_lora (split dA/dB kernel) vs reference.""" + + @pytest.fixture(params=CONFIGS_SMALL + CONFIGS_REAL) + def config(self, request): + return request.param + + def test_matches_reference(self, config): + E, K, N, T, k, R, desc = config + x, W, lA, lB, sei, ssi, eo = _setup(E, K, N, T, k, R) + gx = base_ops.group(x, ssi, fan_out=k) + dy = torch.randn(gx.size(0), N, device=DEVICE, dtype=DTYPE) + + kern_dA, kern_dB = lora_ops.group_bwd_lora( + DY=dy, + X=gx, + lora_A=lA, + lora_B=lB, + expert_offsets=eo, + E=E, + scaling=SCALING, + ) + ref_dA, ref_dB = _reference_bwd_lora(dy, gx, lA, lB, eo, E, SCALING) + + # Use norm-relative error: bf16 accumulation order differs between + # kernel (tiled + different reduction order) and reference (per-expert + # fp32 loop), so max absolute error can be large on individual elements + # while the overall tensor is correct. + dA_norm_err = ( + (kern_dA.float() - ref_dA.float()).norm() / (ref_dA.float().norm() + 1e-6) + ).item() + dB_norm_err = ( + (kern_dB.float() - ref_dB.float()).norm() / (ref_dB.float().norm() + 1e-6) + ).item() + assert dA_norm_err < 0.01, f"[{desc}] dA norm_rel_err={dA_norm_err}" + assert dB_norm_err < 0.01, f"[{desc}] dB norm_rel_err={dB_norm_err}" + + def test_zero_expert_tokens(self): + """Experts with zero routed tokens produce zero gradients.""" + E, K, N, R = 8, 64, 32, 4 + torch.manual_seed(42) + # Route all tokens to expert 0 only + T, k = 16, 1 + top_idx = torch.zeros(T, k, dtype=torch.long, device=DEVICE) + sei, ssi, eo = flatten_sort_count(top_idx, E) + gx = torch.randn(T, K, device=DEVICE, dtype=DTYPE) + dy = torch.randn(T, N, device=DEVICE, dtype=DTYPE) + lA = torch.randn(R * E, K, device=DEVICE, dtype=DTYPE) + lB = torch.randn(N, R * E, device=DEVICE, dtype=DTYPE) + + dA, dB = lora_ops.group_bwd_lora( + DY=dy, + X=gx, + lora_A=lA, + lora_B=lB, + expert_offsets=eo, + E=E, + scaling=2.0, + ) + + # Experts 1..7 should have zero gradients + for e in range(1, E): + assert dA[e * R : (e + 1) * R].abs().max() == 0, f"Expert {e} dA not zero" + assert dB[:, e * R : (e + 1) * R].abs().max() == 0, ( + f"Expert {e} dB not zero" + ) + + +# ─── Full autograd tests ──────────────────────────────────────────────────── + + +class TestScatterMoELoRAAutograd: + """Test full forward + backward through ScatterMoELoRA autograd function.""" + + @pytest.fixture(params=CONFIGS_SMALL + CONFIGS_REAL[:2]) + def config(self, request): + return request.param + + def test_gradients_exist_and_finite(self, config): + E, K, N, T, k, R, desc = config + x, W, lA, lB, sei, ssi, eo = _setup(E, K, N, T, k, R) + + x = x.requires_grad_(True) + lA = lA.requires_grad_(True) + lB = lB.requires_grad_(True) + + out = ScatterMoELoRA.apply( + x, + W, + k, + sei, + ssi, + eo, + lA, + lB, + SCALING, + None, + None, + False, + False, + True, + False, + ) + out.sum().backward() + + assert x.grad is not None, f"[{desc}] x.grad is None" + assert lA.grad is not None, f"[{desc}] lA.grad is None" + assert lB.grad is not None, f"[{desc}] lB.grad is None" + assert torch.isfinite(x.grad).all(), f"[{desc}] x.grad has non-finite" + assert torch.isfinite(lA.grad).all(), f"[{desc}] lA.grad has non-finite" + assert torch.isfinite(lB.grad).all(), f"[{desc}] lB.grad has non-finite" + assert x.grad.abs().sum() > 0, f"[{desc}] x.grad all zero" + assert lA.grad.abs().sum() > 0, f"[{desc}] lA.grad all zero" + + def test_split_matches_fused(self): + """Split dispatch (for few large experts) matches fused kernel.""" + # Use a shape where split would be dispatched (large K*N, few E) + E, K, N, T, k, R = 8, 512, 1024, 128, 2, 16 + x, W, lA, lB, sei, ssi, eo = _setup(E, K, N, T, k, R) + + # Force fused path + orig = lora_ops._SPLIT_LORA_FWD_THRESHOLD + lora_ops._SPLIT_LORA_FWD_THRESHOLD = 10**18 + out_fused = lora_ops.scatter2scatter_lora( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + lora_A=lA, + lora_B=lB, + scaling=SCALING, + ) + + # Force split path + lora_ops._SPLIT_LORA_FWD_THRESHOLD = 0 + out_split = lora_ops.scatter2scatter_lora( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + lora_A=lA, + lora_B=lB, + scaling=SCALING, + ) + lora_ops._SPLIT_LORA_FWD_THRESHOLD = orig + + norm_err = ( + (out_fused.float() - out_split.float()).norm() + / (out_fused.float().norm() + 1e-6) + ).item() + assert norm_err < 0.01, f"split vs fused norm_err={norm_err}" + + def test_scaling_zero_gives_base_only(self): + """With scaling=0.0, LoRA contribution vanishes. Output = X@W.""" + E, K, N, T, k, R = 16, 64, 32, 32, 2, 4 + x, W, lA, lB, sei, ssi, eo = _setup(E, K, N, T, k, R) + + out_lora = ScatterMoELoRA.apply( + x, + W, + k, + sei, + ssi, + eo, + lA, + lB, + 0.0, + None, + None, + False, + False, + True, + False, + ) + out_base = base_ops.scatter2scatter( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + ) + err = (out_lora.float() - out_base.float()).abs().max().item() + assert err < 0.01, f"scaling=0 should match base: err={err}" From 7920fe74ecb13a73e0e7639afd75fee3bfa48cf3 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:42:23 +0530 Subject: [PATCH 1203/1405] fix num_labels= 1 test fail (#3493) [skip ci] * trl_num_lables=1 * casual num_lables=1,rwd model * lint --- src/axolotl/core/builders/causal.py | 7 +++++++ src/axolotl/utils/schemas/validation.py | 17 +++++++++++++++ tests/core/test_builders.py | 2 +- tests/patched/test_validation.py | 28 +++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index c238cbbc3f..f26ef8969e 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -421,6 +421,13 @@ def build(self, total_num_steps): trainer_kwargs["dataset_tags"] = [ d["path"] for d in self.cfg.datasets if not Path(d["path"]).is_dir() ] + # TRL's RewardTrainer validates num_labels=1 on pre-loaded models; ensure the + # config reflects this regardless of how the model was instantiated. + if ( + self.cfg.reward_model + and getattr(self.model.config, "num_labels", None) != 1 + ): + self.model.config.num_labels = 1 trainer = trainer_cls( model=self.model, train_dataset=self.train_dataset, diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 50b30cd261..5e6657a784 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -253,6 +253,23 @@ def hint_reward_model_pad(cls, data): data["pad_to_sequence_len"] = True return data + @model_validator(mode="before") + @classmethod + def set_reward_model_defaults(cls, data): + if data.get("reward_model"): + if data.get("num_labels") is None: + data["num_labels"] = 1 + if not (data.get("type_of_model") or data.get("model_type")): + data["model_type"] = "AutoModelForSequenceClassification" + + if data.get("process_reward_model"): + if data.get("num_labels") is None: + data["num_labels"] = 2 + if not (data.get("type_of_model") or data.get("model_type")): + data["model_type"] = "AutoModelForTokenClassification" + + return data + @model_validator(mode="before") @classmethod def check_gas_bsz(cls, data): diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index ea3c4e6c4e..a241e85492 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -536,7 +536,7 @@ def test_training_arguments(self, sft_cfg, model, tokenizer): "cfg_string", [ "sft_cfg", - # "rm_cfg", # TODO fix for num_labels = 2 vs 1 + "rm_cfg", "prm_cfg", ], ) diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 21299ed980..d22927940d 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -277,6 +277,34 @@ def test_model_type_remap(self, minimal_cfg): new_cfg = validate_config(cfg) assert new_cfg.type_of_model == "AutoModelForCausalLM" + def test_reward_model_defaults(self, minimal_cfg): + cfg = ( + DictDefault( + { + "reward_model": True, + } + ) + | minimal_cfg + ) + + new_cfg = validate_config(cfg) + assert new_cfg.num_labels == 1 + assert new_cfg.type_of_model == "AutoModelForSequenceClassification" + + def test_process_reward_model_defaults(self, minimal_cfg): + cfg = ( + DictDefault( + { + "process_reward_model": True, + } + ) + | minimal_cfg + ) + + new_cfg = validate_config(cfg) + assert new_cfg.num_labels == 2 + assert new_cfg.type_of_model == "AutoModelForTokenClassification" + def test_model_revision_remap(self, minimal_cfg): cfg = ( DictDefault( From 113d275bd9775e092b6a57bcbe17fd6715244dc7 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:43:34 +0530 Subject: [PATCH 1204/1405] qwen docs + new config (#3499) [skip ci] * qwen docs + new config * docss lint * simplify comments * read me * lint comments * Update docs/multimodal.qmd * Update docs/multimodal.qmd * Update examples/qwen3.5/9b-fft-vision.yaml * chore: fix link and incorrect points --------- Co-authored-by: NanoCode012 Co-authored-by: NanoCode012 --- docs/multimodal.qmd | 9 +++ examples/qwen3.5/27b-fft.yaml | 59 +++++++++++++++++++ examples/qwen3.5/9b-fft-vision.yaml | 49 +++++++++++++++ ...b-lora-vision.yaml => 9b-lora-vision.yaml} | 6 +- examples/qwen3.5/README.md | 35 +++++++---- 5 files changed, 142 insertions(+), 16 deletions(-) create mode 100644 examples/qwen3.5/27b-fft.yaml create mode 100644 examples/qwen3.5/9b-fft-vision.yaml rename examples/qwen3.5/{7b-lora-vision.yaml => 9b-lora-vision.yaml} (84%) diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index e5753732d7..e2a9440e89 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -20,6 +20,7 @@ format: - [Gemma-3n](#sec-gemma-3n) - [Qwen2-VL](#sec-qwen2-vl) - [Qwen2.5-VL](#sec-qwen25-vl) +- [Qwen3.5](#sec-qwen3-5) - [GLM-4.6V](#sec-glm-4-6v) - [SmolVLM2](#sec-smolvlm2) - [LFM2-VL](#sec-lfm2-vl) @@ -191,6 +192,14 @@ base_model: Qwen/Qwen3-VL-4B-Instruct chat_template: qwen2_vl # same as qwen2-vl ``` +### Qwen3.5 {#sec-qwen3-5} + +```yaml +base_model: Qwen/Qwen3.5-9B + +chat_template: qwen3_5 +``` + ### GLM-4.6V {#sec-glm-4-6v} Both GLM-4.6V (106B MoE) and GLM-4.6V-Flash (9B) are supported. diff --git a/examples/qwen3.5/27b-fft.yaml b/examples/qwen3.5/27b-fft.yaml new file mode 100644 index 0000000000..6e61bc6959 --- /dev/null +++ b/examples/qwen3.5/27b-fft.yaml @@ -0,0 +1,59 @@ +base_model: Qwen/Qwen3.5-27B +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +# Full fine-tune (FFT) of the text-only path of Qwen3.5-27B. + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +strict: false + +chat_template: qwen3_5 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.0 +output_dir: ./outputs/out +dataset_prepared_path: last_run_prepared + +sequence_len: 2048 +sample_packing: true + +# Freeze vision encoder +unfrozen_parameters: + - model\.language_model\..* + - lm_head\..* + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/qwen3.5/9b-fft-vision.yaml b/examples/qwen3.5/9b-fft-vision.yaml new file mode 100644 index 0000000000..b6aeb859d0 --- /dev/null +++ b/examples/qwen3.5/9b-fft-vision.yaml @@ -0,0 +1,49 @@ +base_model: Qwen/Qwen3.5-9B +processor_type: AutoProcessor + +# Required for multimodal training +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +chat_template: qwen3_5 +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:1%] + +dataset_prepared_path: last_run_prepared +val_set_size: 0.0 +output_dir: ./outputs/out + +sequence_len: 4096 +pad_to_sequence_len: false + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/qwen3.5/7b-lora-vision.yaml b/examples/qwen3.5/9b-lora-vision.yaml similarity index 84% rename from examples/qwen3.5/7b-lora-vision.yaml rename to examples/qwen3.5/9b-lora-vision.yaml index 79179ec96b..9fb2229018 100644 --- a/examples/qwen3.5/7b-lora-vision.yaml +++ b/examples/qwen3.5/9b-lora-vision.yaml @@ -1,10 +1,6 @@ -base_model: Qwen/Qwen3.5-7B +base_model: Qwen/Qwen3.5-9B processor_type: AutoProcessor -# Qwen3.5-7B and above are early-fusion VLMs (Qwen3_5ForConditionalGeneration). -# Vision and text tokens are processed together by the same transformer layers. -# Note: Qwen3.5-2B is a text-only model — the smallest VLM is Qwen3.5-7B. - # These 3 lines are required for vision/multimodal training skip_prepare_dataset: true remove_unused_columns: false diff --git a/examples/qwen3.5/README.md b/examples/qwen3.5/README.md index 8a2f9b4bd8..1b64bc78d6 100644 --- a/examples/qwen3.5/README.md +++ b/examples/qwen3.5/README.md @@ -1,15 +1,20 @@ # Finetune Qwen3.5 with Axolotl -[Qwen3.5](https://huggingface.co/collections/Qwen/qwen35-68452f3bc6e4b7cfb4e1c803) is a hybrid architecture model series combining Gated DeltaNet linear attention with standard Transformer attention. Models from 7B onwards are early-fusion vision-language models (`Qwen3_5ForConditionalGeneration`), meaning vision and text tokens are processed through the same transformer stack. The 2B variant is text-only. +[Qwen3.5](https://huggingface.co/collections/Qwen/qwen35) is a hybrid architecture model series combining Gated DeltaNet linear attention with standard Transformer attention. All Qwen3.5 models are early-fusion vision-language models: dense variants use `Qwen3_5ForConditionalGeneration` and MoE variants use `Qwen3_5MoeForConditionalGeneration`. + +Vision and text tokens are processed through the same transformer stack. The configs below train on text-only data unless noted otherwise. See `9b-lora-vision.yaml` for a multimodal example. Available configs: -| Config | Model | Type | -|---|---|---| -| `27b-qlora.yaml` | Qwen3.5-27B | Dense VLM, text-only path | -| `35b-a3b-moe-qlora.yaml` | Qwen3.5-35B-A3B | MoE, text-only path | -| `122b-a10b-moe-qlora.yaml` | Qwen3.5-122B-A10B | MoE, text-only path | -| `7b-lora-vision.yaml` | Qwen3.5-7B | Vision+text (multimodal) | +| Config | Model | Type | Peak VRAM | +|---|---|---|---| +| `27b-qlora.yaml` | Qwen3.5-27B | Dense VLM, text-only QLoRA | ~47 GiB | +| `27b-fft.yaml` | Qwen3.5-27B | Dense VLM, text-only FFT (vision frozen) | ~53 GiB | +| `35b-a3b-moe-qlora.yaml` | Qwen3.5-35B-A3B | MoE, text-only QLoRA | — | +| `122b-a10b-moe-qlora.yaml` | Qwen3.5-122B-A10B | MoE, text-only QLoRA | — | +| `9b-lora-vision.yaml` | Qwen3.5-9B | Vision+text LoRA, single GPU | — | +| `9b-fft-vision.yaml` | Qwen3.5-9B | Vision+text FFT, single GPU | ~61 GiB | + ## Getting started @@ -29,23 +34,31 @@ pip3 uninstall -y causal-conv1d && pip3 install flash-linear-attention==0.4.1 # Dense 27B text-only (QLoRA, ~47 GiB VRAM with sample packing) axolotl train examples/qwen3.5/27b-qlora.yaml +# Dense 27B text-only FFT with vision encoder frozen (~53 GiB, single 80 GiB GPU) +axolotl train examples/qwen3.5/27b-fft.yaml + # MoE 35B-A3B text-only (QLoRA) axolotl train examples/qwen3.5/35b-a3b-moe-qlora.yaml # MoE 122B-A10B text-only (QLoRA) axolotl train examples/qwen3.5/122b-a10b-moe-qlora.yaml -# 7B vision+text (LoRA, multimodal dataset) -axolotl train examples/qwen3.5/7b-lora-vision.yaml +# 9B vision+text (LoRA, multimodal dataset) +axolotl train examples/qwen3.5/9b-lora-vision.yaml + +# 9B vision+text FFT, single 80 GiB GPU (~61 GiB peak) +axolotl train examples/qwen3.5/9b-fft-vision.yaml + ``` ### TIPS - For inference, you can experiment with `temperature: 0.7`, `top_p: 0.8`, `top_k: 20`, and `min_p: 0`. -- You can run a full finetuning by removing `adapter: qlora` and `load_in_4bit: true`. See [Multi-GPU](#optimization-guides) below. +- For **text-only FFT** on 27B, use `27b-fft.yaml` which sets `unfrozen_parameters` to freeze the vision encoder (`model.visual.*`) — this avoids wasting optimizer state on parameters that receive no gradient from text-only data. +- You can run a full finetuning of smaller configs by removing `adapter: qlora` and `load_in_4bit: true`. See [Multi-GPU](#optimization-guides) below. - Read more on loading your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). - The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). -- For **multimodal** finetuning, set `processor_type: AutoProcessor`, `skip_prepare_dataset: true`, and `remove_unused_columns: false` as shown in `7b-lora-vision.yaml`. +- For **multimodal** finetuning, set `processor_type: AutoProcessor`, `skip_prepare_dataset: true`, and `remove_unused_columns: false` as shown in `9b-lora-vision.yaml`. - The Gated DeltaNet linear attention layers (`linear_attn.*`) can optionally be added to `lora_target_modules` — they are commented out by default. ## Optimization Guides From b3823cc6b03177c38cd5d6d9db613bd6d9032284 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:44:06 +0530 Subject: [PATCH 1205/1405] fix: gemma3 configs (#3500) [skip ci] * gemma fft , text fix * good lint --- examples/gemma3/gemma-3-1b-qlora.yml | 8 +++++--- examples/gemma3/gemma-3-270m-qlora.yml | 8 +++++--- examples/gemma3/gemma-3-4b-qlora.yml | 9 +++++---- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/examples/gemma3/gemma-3-1b-qlora.yml b/examples/gemma3/gemma-3-1b-qlora.yml index d84368bc09..f6fc6955c7 100644 --- a/examples/gemma3/gemma-3-1b-qlora.yml +++ b/examples/gemma3/gemma-3-1b-qlora.yml @@ -1,8 +1,5 @@ base_model: google/gemma-3-1b-it -model_type: Gemma3ForCausalLM -cls_model_config: Gemma3TextConfig - # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name @@ -27,6 +24,11 @@ datasets: val_set_size: 0.0 output_dir: ./outputs/out +# Freeze vision tower +unfrozen_parameters: + - ^model\.language_model\..* + - ^lm_head\..* + adapter: qlora lora_r: 32 lora_alpha: 16 diff --git a/examples/gemma3/gemma-3-270m-qlora.yml b/examples/gemma3/gemma-3-270m-qlora.yml index 14ea2aabaa..99202f29f9 100644 --- a/examples/gemma3/gemma-3-270m-qlora.yml +++ b/examples/gemma3/gemma-3-270m-qlora.yml @@ -1,8 +1,5 @@ base_model: google/gemma-3-270m-it -model_type: Gemma3ForCausalLM -cls_model_config: Gemma3TextConfig - # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name @@ -27,6 +24,11 @@ datasets: val_set_size: 0.0 output_dir: ./outputs/out +# Freeze vision tower +unfrozen_parameters: + - ^model\.language_model\..* + - ^lm_head\..* + adapter: qlora lora_r: 32 lora_alpha: 16 diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml index 7d44f3c9b0..d11f2ea506 100644 --- a/examples/gemma3/gemma-3-4b-qlora.yml +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -1,9 +1,5 @@ base_model: google/gemma-3-4b-it -# Need to set else transformers tries to load vision too -model_type: Gemma3ForCausalLM -cls_model_config: Gemma3TextConfig - load_in_4bit: true # gemma3 doesn't seem to play nice with ddp @@ -24,6 +20,11 @@ dataset_prepared_path: last_run_prepared val_set_size: 0.01 output_dir: ./outputs/out +# Freeze vision tower +unfrozen_parameters: + - ^model\.language_model\..* + - ^lm_head\..* + adapter: qlora lora_model_dir: From c13cb7c8537c16434ecdad2373459c7fedd115b3 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:53:42 +0530 Subject: [PATCH 1206/1405] feat: add nemotron config (#3506) * nemotron config exp * Update examples/nemotron/nemotron-mini-4b-qlora.yaml Co-authored-by: NanoCode012 --------- Co-authored-by: NanoCode012 --- examples/nemotron/nemotron-mini-4b-qlora.yaml | 57 +++++++++++++++++++ src/axolotl/monkeypatch/multipack.py | 1 + 2 files changed, 58 insertions(+) create mode 100644 examples/nemotron/nemotron-mini-4b-qlora.yaml diff --git a/examples/nemotron/nemotron-mini-4b-qlora.yaml b/examples/nemotron/nemotron-mini-4b-qlora.yaml new file mode 100644 index 0000000000..e796c149c7 --- /dev/null +++ b/examples/nemotron/nemotron-mini-4b-qlora.yaml @@ -0,0 +1,57 @@ +base_model: nvidia/Nemotron-Mini-4B-Instruct + +load_in_8bit: false +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: ./outputs/nemotron-mini-4b-qlora + +adapter: qlora +lora_model_dir: + +sequence_len: 4096 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_linear: true +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - up_proj + - down_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 + +special_tokens: diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 9e48e73ebb..78087acbc4 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -59,6 +59,7 @@ "ministral3", "mistral4", "afmoe", + "nemotron", ] From 038ffe3f265f578c95aa31e236edd088c5a72ed3 Mon Sep 17 00:00:00 2001 From: Lorenzo Baraldi <48087510+lorenzbaraldi@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:27:24 +0100 Subject: [PATCH 1207/1405] fix: solved double sequence partition from SequenceParallelContextManager and Accelerate's native CP (#3498) --- .../accelerate/parallelism_config.py | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/axolotl/monkeypatch/accelerate/parallelism_config.py b/src/axolotl/monkeypatch/accelerate/parallelism_config.py index 9b71e914ac..ebd9a6f0d7 100644 --- a/src/axolotl/monkeypatch/accelerate/parallelism_config.py +++ b/src/axolotl/monkeypatch/accelerate/parallelism_config.py @@ -78,30 +78,21 @@ def patch_parallelism_config(): def patch_prepare_cp(): - import functools + import contextlib - import torch from accelerate import Accelerator def patched_prepare_cp(self, *args): if self.parallelism_config.cp_backend == "deepspeed": return args - from accelerate.big_modeling import _attach_context_parallel_hooks - from torch.distributed.tensor.experimental import context_parallel - from torch.distributed.tensor.experimental._attention import set_rotate_method - - cp_comm_strategy = self.parallelism_config.cp_handler.cp_comm_strategy - set_rotate_method(cp_comm_strategy) - - self._cp_context = functools.partial( - context_parallel, mesh=self.torch_device_mesh["cp"] - ) - - for arg in args: - if isinstance(arg, torch.nn.Module): - _attach_context_parallel_hooks(arg) + @contextlib.contextmanager + def _noop_cp_context( + buffers=None, buffer_seq_dims=None, no_restore_buffers=None + ): + yield + self._cp_context = _noop_cp_context return args Accelerator._prepare_cp = patched_prepare_cp From c57acef2c7d3efb04d7d80a2334895a5caef6f9a Mon Sep 17 00:00:00 2001 From: Owen Arliawan <37985114+Nero10578@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:52:46 -0700 Subject: [PATCH 1208/1405] Qwen3.5-MoE example config with lora_target_modules regex (#3515) [skip ci] * lora target modules with regex * updates * fsdp for non moe * update wording * chore: cleanup and lint * chore: cleanup docs from merge --------- Co-authored-by: NanoCode012 --- .../qwen3.5/122b-a10b-moe-qlora-fsdp.yaml | 84 ++++++++++++++++++ examples/qwen3.5/122b-a10b-moe-qlora.yaml | 7 +- examples/qwen3.5/27b-qlora-fsdp.yaml | 81 ++++++++++++++++++ examples/qwen3.5/27b-qlora.yaml | 4 +- examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml | 85 +++++++++++++++++++ examples/qwen3.5/35b-a3b-moe-qlora.yaml | 6 +- examples/qwen3.5/9b-lora-vision.yaml | 2 - examples/qwen3.5/README.md | 78 ++++++++++------- 8 files changed, 306 insertions(+), 41 deletions(-) create mode 100644 examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml create mode 100644 examples/qwen3.5/27b-qlora-fsdp.yaml create mode 100644 examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml diff --git a/examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml b/examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml new file mode 100644 index 0000000000..8548a04e14 --- /dev/null +++ b/examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml @@ -0,0 +1,84 @@ +base_model: Qwen/Qwen3.5-122B-A10B + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +strict: false + +chat_template: qwen3_5 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.0 +output_dir: ./outputs/out +dataset_prepared_path: last_run_prepared + +sequence_len: 2048 +sample_packing: true + +load_in_4bit: true +quantize_moe_experts: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj +# Regex matching to target shared experts too +# lora_target_modules: 'model\.(language_model\.)?layers\.[\d]+\.(mlp|self_attn)\.(shared_expert\.)?(up|down|gate|gate_up|q|k|v|o)_proj' + +# Target experts +# lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: + +fsdp_config: + fsdp_version: 2 + offload_params: true + cpu_ram_efficient_loading: false + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Qwen3_5MoeDecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true diff --git a/examples/qwen3.5/122b-a10b-moe-qlora.yaml b/examples/qwen3.5/122b-a10b-moe-qlora.yaml index e9cbf80cea..4d805c0040 100644 --- a/examples/qwen3.5/122b-a10b-moe-qlora.yaml +++ b/examples/qwen3.5/122b-a10b-moe-qlora.yaml @@ -32,7 +32,11 @@ lora_target_modules: - v_proj - o_proj -#lora_target_parameters: +# Regex matching to target shared experts too +# lora_target_modules: 'model\.(language_model\.)?layers\.[\d]+\.(mlp|self_attn)\.(shared_expert\.)?(up|down|gate|gate_up|q|k|v|o)_proj' + +# Target experts +# lora_target_parameters: # - mlp.experts.gate_up_proj # - mlp.experts.down_proj @@ -52,7 +56,6 @@ learning_rate: 0.0002 bf16: auto tf32: true - lora_mlp_kernel: false lora_qkv_kernel: false lora_o_kernel: false diff --git a/examples/qwen3.5/27b-qlora-fsdp.yaml b/examples/qwen3.5/27b-qlora-fsdp.yaml new file mode 100644 index 0000000000..79b87a32fe --- /dev/null +++ b/examples/qwen3.5/27b-qlora-fsdp.yaml @@ -0,0 +1,81 @@ +base_model: Qwen/Qwen3.5-27B + +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +strict: false + +chat_template: qwen3_5 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.0 +output_dir: ./outputs/out +dataset_prepared_path: last_run_prepared + +sequence_len: 2048 +sample_packing: true + +load_in_4bit: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - down_proj + - up_proj + # Uncomment below to also target the linear attention projections. + # These use separate in_proj_qkv / in_proj_z / out_proj (Qwen3.5-specific). + # - linear_attn.in_proj_qkv + # - linear_attn.in_proj_z + # - linear_attn.out_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: + +fsdp_config: + fsdp_version: 2 + offload_params: false + cpu_ram_efficient_loading: false + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Qwen3_5DecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true diff --git a/examples/qwen3.5/27b-qlora.yaml b/examples/qwen3.5/27b-qlora.yaml index 2ba1c4ed73..18c0af95b7 100644 --- a/examples/qwen3.5/27b-qlora.yaml +++ b/examples/qwen3.5/27b-qlora.yaml @@ -1,9 +1,7 @@ base_model: Qwen/Qwen3.5-27B + # Automatically upload checkpoint and final model to HF # hub_model_id: username/custom_model_name -# Note: Qwen3.5 is an early-fusion VLM (image+text). This config fine-tunes -# the text-only path. For multimodal (image+text) fine-tuning, add image -# columns to your dataset following axolotl's multimodal dataset format. plugins: - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin diff --git a/examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml b/examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml new file mode 100644 index 0000000000..fd4adbe050 --- /dev/null +++ b/examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml @@ -0,0 +1,85 @@ +base_model: Qwen/Qwen3.5-35B-A3B + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +strict: false + +chat_template: qwen3_5 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.0 +output_dir: ./outputs/out +dataset_prepared_path: last_run_prepared + +sequence_len: 2048 +sample_packing: true + +load_in_4bit: true +quantize_moe_experts: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + +# Regex matching to target shared experts too +# lora_target_modules: 'model\.(language_model\.)?layers\.[\d]+\.(mlp|self_attn)\.(shared_expert\.)?(up|down|gate|gate_up|q|k|v|o)_proj' + +# Target experts +# lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: + +fsdp_config: + fsdp_version: 2 + offload_params: true + cpu_ram_efficient_loading: false + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Qwen3_5MoeDecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true diff --git a/examples/qwen3.5/35b-a3b-moe-qlora.yaml b/examples/qwen3.5/35b-a3b-moe-qlora.yaml index 462babf0bb..dea45801ca 100644 --- a/examples/qwen3.5/35b-a3b-moe-qlora.yaml +++ b/examples/qwen3.5/35b-a3b-moe-qlora.yaml @@ -32,7 +32,11 @@ lora_target_modules: - v_proj - o_proj -#lora_target_parameters: +# Regex matching to target shared experts too +# lora_target_modules: 'model\.(language_model\.)?layers\.[\d]+\.(mlp|self_attn)\.(shared_expert\.)?(up|down|gate|gate_up|q|k|v|o)_proj' + +# Target experts +# lora_target_parameters: # - mlp.experts.gate_up_proj # - mlp.experts.down_proj diff --git a/examples/qwen3.5/9b-lora-vision.yaml b/examples/qwen3.5/9b-lora-vision.yaml index 9fb2229018..1c3717724d 100644 --- a/examples/qwen3.5/9b-lora-vision.yaml +++ b/examples/qwen3.5/9b-lora-vision.yaml @@ -26,8 +26,6 @@ lora_r: 32 lora_alpha: 16 lora_dropout: 0.05 # Targets the language model attention and MLP layers. -# Qwen3.5 is early-fusion: all layers (including those seeing vision tokens) share -# the same transformer stack, so standard attention targets work for both modalities. lora_target_modules: - q_proj - k_proj diff --git a/examples/qwen3.5/README.md b/examples/qwen3.5/README.md index 1b64bc78d6..6c0d01969f 100644 --- a/examples/qwen3.5/README.md +++ b/examples/qwen3.5/README.md @@ -2,64 +2,76 @@ [Qwen3.5](https://huggingface.co/collections/Qwen/qwen35) is a hybrid architecture model series combining Gated DeltaNet linear attention with standard Transformer attention. All Qwen3.5 models are early-fusion vision-language models: dense variants use `Qwen3_5ForConditionalGeneration` and MoE variants use `Qwen3_5MoeForConditionalGeneration`. -Vision and text tokens are processed through the same transformer stack. The configs below train on text-only data unless noted otherwise. See `9b-lora-vision.yaml` for a multimodal example. +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +3. Install FLA for sample packing support with the Gated DeltaNet linear attention layers: + ```bash + pip3 uninstall -y causal-conv1d && pip3 install flash-linear-attention==0.4.1 + ``` + > FLA is required when `sample_packing: true`. Without it, training raises a `RuntimeError` on packed sequences. Vision configs use `sample_packing: false` so FLA is optional there. + +4. Pick any config from the table below and run: + + ```bash + axolotl train examples/qwen3.5/.yaml + ``` Available configs: | Config | Model | Type | Peak VRAM | |---|---|---|---| -| `27b-qlora.yaml` | Qwen3.5-27B | Dense VLM, text-only QLoRA | ~47 GiB | -| `27b-fft.yaml` | Qwen3.5-27B | Dense VLM, text-only FFT (vision frozen) | ~53 GiB | -| `35b-a3b-moe-qlora.yaml` | Qwen3.5-35B-A3B | MoE, text-only QLoRA | — | -| `122b-a10b-moe-qlora.yaml` | Qwen3.5-122B-A10B | MoE, text-only QLoRA | — | | `9b-lora-vision.yaml` | Qwen3.5-9B | Vision+text LoRA, single GPU | — | | `9b-fft-vision.yaml` | Qwen3.5-9B | Vision+text FFT, single GPU | ~61 GiB | +| `27b-qlora.yaml` | Qwen3.5-27B | Dense, text-only QLoRA | ~47 GiB | +| `27b-fft.yaml` | Qwen3.5-27B | Dense, text-only FFT (vision frozen) | ~53 GiB | +| `27b-qlora-fsdp.yaml` | Qwen3.5-27B | Dense, text-only QLoRA + FSDP2 | — | +| `35b-a3b-moe-qlora.yaml` | Qwen3.5-35B-A3B | MoE, text-only QLoRA | — | +| `35b-a3b-moe-qlora-fsdp.yaml` | Qwen3.5-35B-A3B | MoE, text-only QLoRA + FSDP2 | — | +| `122b-a10b-moe-qlora.yaml` | Qwen3.5-122B-A10B | MoE, text-only QLoRA | — | +| `122b-a10b-moe-qlora-fsdp.yaml` | Qwen3.5-122B-A10B | MoE, text-only QLoRA + FSDP2 | — | +### Gated DeltaNet Linear Attention -## Getting started - -1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). +Qwen3.5 interleaves standard attention with Gated DeltaNet linear attention layers. To apply LoRA to them, add to `lora_target_modules`: -2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. - -3. Install FLA for sample packing support with the Gated DeltaNet linear attention layers: -```bash -pip3 uninstall -y causal-conv1d && pip3 install flash-linear-attention==0.4.1 +```yaml +lora_target_modules: + # ... standard projections ... + - linear_attn.in_proj_qkv + - linear_attn.in_proj_z + - linear_attn.out_proj ``` -> FLA is required when `sample_packing: true`. Without it, training raises a `RuntimeError` on packed sequences. Vision configs use `sample_packing: false` so FLA is optional there. - -4. Run a finetuning example: -```bash -# Dense 27B text-only (QLoRA, ~47 GiB VRAM with sample packing) -axolotl train examples/qwen3.5/27b-qlora.yaml +### Routed Experts (MoE) -# Dense 27B text-only FFT with vision encoder frozen (~53 GiB, single 80 GiB GPU) -axolotl train examples/qwen3.5/27b-fft.yaml +To apply LoRA to routed expert parameters, add `lora_target_parameters`: -# MoE 35B-A3B text-only (QLoRA) -axolotl train examples/qwen3.5/35b-a3b-moe-qlora.yaml - -# MoE 122B-A10B text-only (QLoRA) -axolotl train examples/qwen3.5/122b-a10b-moe-qlora.yaml +```yaml +lora_target_parameters: + - mlp.experts.gate_up_proj + - mlp.experts.down_proj +# - mlp.gate.weight # router +``` -# 9B vision+text (LoRA, multimodal dataset) -axolotl train examples/qwen3.5/9b-lora-vision.yaml +### Shared Experts (MoE) -# 9B vision+text FFT, single 80 GiB GPU (~61 GiB peak) -axolotl train examples/qwen3.5/9b-fft-vision.yaml +Routed experts and shared experts both have `gate_up_proj`/`down_proj`, so a plain module name in `lora_target_modules` would match both. Use a regex to target only attention and shared expert projections, while `lora_target_parameters` above handles routed experts separately: +```yaml +lora_target_modules: 'model\.(language_model\.)?layers\.[\d]+\.(mlp|self_attn)\.(shared_expert\.)?(up|down|gate|gate_up|q|k|v|o)_proj' ``` ### TIPS -- For inference, you can experiment with `temperature: 0.7`, `top_p: 0.8`, `top_k: 20`, and `min_p: 0`. -- For **text-only FFT** on 27B, use `27b-fft.yaml` which sets `unfrozen_parameters` to freeze the vision encoder (`model.visual.*`) — this avoids wasting optimizer state on parameters that receive no gradient from text-only data. +- For inference hyp, please see the respective model card details. - You can run a full finetuning of smaller configs by removing `adapter: qlora` and `load_in_4bit: true`. See [Multi-GPU](#optimization-guides) below. - Read more on loading your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). - The dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). - For **multimodal** finetuning, set `processor_type: AutoProcessor`, `skip_prepare_dataset: true`, and `remove_unused_columns: false` as shown in `9b-lora-vision.yaml`. -- The Gated DeltaNet linear attention layers (`linear_attn.*`) can optionally be added to `lora_target_modules` — they are commented out by default. ## Optimization Guides From 7ddfb2d8a0531122649a30a1b1d8bb40cd0e7191 Mon Sep 17 00:00:00 2001 From: Avaya Aggarwal <119044997+OnePunchMonk@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:40:41 +0530 Subject: [PATCH 1209/1405] cleanup: remove dead SDPA patches (#3488) [skip ci] Transformers 5.x routes attention through sdpa_attention.py and no longer calls the _prepare_4d_causal_attention_mask* or _expand_mask functions that these patches targeted. This makes the following patches dead code: - llama_patch_multipack.py (patched _prepare_4d_causal_attention_mask*) - llama_expand_mask.py (patched _expand_mask, never called) - Related utility functions in monkeypatch/utils.py Closes axolotl-ai-cloud/axolotl#3331 --- _quarto.yml | 2 - src/axolotl/loaders/patch_manager.py | 11 ---- src/axolotl/monkeypatch/llama_expand_mask.py | 24 ------- .../monkeypatch/llama_patch_multipack.py | 26 -------- src/axolotl/monkeypatch/utils.py | 66 +------------------ tests/test_expand_mask.py | 45 ------------- 6 files changed, 1 insertion(+), 173 deletions(-) delete mode 100644 src/axolotl/monkeypatch/llama_expand_mask.py delete mode 100644 src/axolotl/monkeypatch/llama_patch_multipack.py delete mode 100644 tests/test_expand_mask.py diff --git a/_quarto.yml b/_quarto.yml index 5e11691022..404125d1c1 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -128,11 +128,9 @@ quartodoc: - monkeypatch.mistral_attn_hijack_flash - monkeypatch.multipack - monkeypatch.relora - - monkeypatch.llama_expand_mask - monkeypatch.lora_kernels - monkeypatch.utils - monkeypatch.btlm_attn_hijack_flash - - monkeypatch.llama_patch_multipack - monkeypatch.stablelm_attn_hijack_flash - monkeypatch.trainer_fsdp_optim - monkeypatch.transformers_fa_utils diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index bddd388e4d..38cc198d3a 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -571,15 +571,6 @@ def _patch_llama_xformers_attention(self): LOG.info("Patching with xformers attention...") hijack_llama_attention() - def _patch_llama_sample_packing(self): - """Apply sample packing patches for LLaMA models.""" - from axolotl.monkeypatch.llama_patch_multipack import ( - hijack_llama_prepare_4d_mask, - ) - - LOG.info("Patching llama _prepare_4d_causal_attention_mask*...") - hijack_llama_prepare_4d_mask() - def _patch_llama_derived_model(self): """Modify all llama derived models in one block.""" if self.cfg.is_llama_derived_model and not ( @@ -591,8 +582,6 @@ def _patch_llama_derived_model(self): self._patch_llama_flash_attention() elif self.cfg.xformers_attention: self._patch_llama_xformers_attention() - elif self.cfg.sample_packing: - self._patch_llama_sample_packing() elif self.cfg.s2_attention: raise NotImplementedError( "Shifted-sparse attention not currently implemented without flash attention." diff --git a/src/axolotl/monkeypatch/llama_expand_mask.py b/src/axolotl/monkeypatch/llama_expand_mask.py deleted file mode 100644 index 5cfb7818e3..0000000000 --- a/src/axolotl/monkeypatch/llama_expand_mask.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -expands the binary attention mask per 3.2.2 of https://arxiv.org/pdf/2107.02027.pdf -""" - -from typing import Optional - -import torch - -from axolotl.monkeypatch.utils import mask_2d_to_4d - - -def _expand_mask(mask: torch.Tensor, dtype: torch.dtype, tgt_len: Optional[int] = None): - masked_zero_one_mask = mask_2d_to_4d(mask, dtype, tgt_len) - inverted_mask = 1.0 - masked_zero_one_mask - - return inverted_mask.masked_fill( - inverted_mask.to(torch.bool), torch.finfo(dtype).min - ) - - -def hijack_expand_mask(): - import transformers - - transformers.models.llama.modeling_llama._expand_mask = _expand_mask diff --git a/src/axolotl/monkeypatch/llama_patch_multipack.py b/src/axolotl/monkeypatch/llama_patch_multipack.py deleted file mode 100644 index 8d234881fd..0000000000 --- a/src/axolotl/monkeypatch/llama_patch_multipack.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Patched LlamaAttention to use torch.nn.functional.scaled_dot_product_attention -""" - -from axolotl.monkeypatch.utils import ( - patched_prepare_4d_causal_attention_mask, - patched_prepare_4d_causal_attention_mask_for_sdpa, -) - - -def hijack_llama_prepare_4d_mask(): - from transformers import modeling_attn_mask_utils - from transformers.models.llama import modeling_llama - - modeling_llama._prepare_4d_causal_attention_mask_for_sdpa = ( - patched_prepare_4d_causal_attention_mask_for_sdpa - ) - modeling_attn_mask_utils._prepare_4d_causal_attention_mask_for_sdpa = ( - patched_prepare_4d_causal_attention_mask_for_sdpa - ) - modeling_llama._prepare_4d_causal_attention_mask = ( - patched_prepare_4d_causal_attention_mask - ) - modeling_attn_mask_utils._prepare_4d_causal_attention_mask = ( - patched_prepare_4d_causal_attention_mask - ) diff --git a/src/axolotl/monkeypatch/utils.py b/src/axolotl/monkeypatch/utils.py index 4c6a4de11c..3ec242ef09 100644 --- a/src/axolotl/monkeypatch/utils.py +++ b/src/axolotl/monkeypatch/utils.py @@ -3,15 +3,10 @@ """ import re -from typing import Optional, Tuple +from typing import Tuple import torch import torch.nn.functional as F -from transformers.modeling_attn_mask_utils import ( - _prepare_4d_causal_attention_mask, - _prepare_4d_causal_attention_mask_for_sdpa, -) -from transformers.utils import is_torch_bf16_gpu_available @torch.jit.script @@ -170,65 +165,6 @@ def set_module_name(model, name, value): setattr(parent, child_name, value) -def mask_2d_to_4d( - mask: torch.Tensor, dtype: torch.dtype, tgt_len: Optional[int] = None -): - """ - Expands attention_mask from `[bsz, seq_len]` to `[bsz, 1, tgt_seq_len, src_seq_len]`. - This expansion handles packed sequences so that sequences share the same attention mask integer value - when they attend to each other within that sequence. - This expansion transforms the mask to lower triangular form to prevent future peeking. - """ - bsz, src_len = mask.size() - tgt_len = tgt_len if tgt_len is not None else src_len - - mask = mask.unsqueeze(1).unsqueeze(2) - mask = mask.expand(bsz, 1, tgt_len, src_len) - - # Create a binary mask from the original mask where zeros remain zeros and all other values are set to one - binary_mask = torch.where( - mask != 0, - torch.tensor(1, device=mask.device).to(dtype), - torch.tensor(0, device=mask.device).to(dtype), - ) - - # Create a block-diagonal mask. - # we multiply by the binary mask so that 0's in the original mask are correctly excluded - zero_one_mask = torch.eq(mask, mask.transpose(-1, -2)).int() * binary_mask - - # Now let's create a lower triangular mask of ones that will zero out the upper triangular part - lower_triangular_ones = torch.tril(torch.ones((tgt_len, src_len), dtype=dtype)).to( - mask.device - ) - - # Use the lower triangular mask to zero out the upper triangular part of the zero_one_mask - masked_zero_one_mask = zero_one_mask * lower_triangular_ones - - return masked_zero_one_mask - - -def patched_prepare_4d_causal_attention_mask( - attention_mask: Optional[torch.Tensor], - *args, -): - dtype = torch.bfloat16 if is_torch_bf16_gpu_available() else torch.float32 - return _prepare_4d_causal_attention_mask( - mask_2d_to_4d(attention_mask, dtype=dtype), - *args, - ) - - -def patched_prepare_4d_causal_attention_mask_for_sdpa( - attention_mask: Optional[torch.Tensor], - *args, -): - dtype = torch.bfloat16 if is_torch_bf16_gpu_available() else torch.float32 - return _prepare_4d_causal_attention_mask_for_sdpa( - mask_2d_to_4d(attention_mask, dtype=dtype), - *args, - ) - - def detab_code(code: str) -> Tuple[str, str]: try: spaces = re.match(r"([\s\t]{1,})", code).group(0) diff --git a/tests/test_expand_mask.py b/tests/test_expand_mask.py deleted file mode 100644 index 1c69ca234f..0000000000 --- a/tests/test_expand_mask.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Unit tests for the monkey patch for expand mask to handle packed sequences -""" - -import unittest - -import torch - -from axolotl.monkeypatch.llama_expand_mask import _expand_mask - - -class TestExpandMask(unittest.TestCase): - """ - Test class for attention mask expansion for packed sequences - """ - - def test_output(self): - mask = torch.tensor([[1, 1, 1, 2], [2, 3, 3, 0]]) - dtype = torch.float32 - expected_output = torch.tensor( - [ - [ - [ - [0.0000e00, -3.4028e38, -3.4028e38, -3.4028e38], - [0.0000e00, 0.0000e00, -3.4028e38, -3.4028e38], - [0.0000e00, 0.0000e00, 0.0000e00, -3.4028e38], - [-3.4028e38, -3.4028e38, -3.4028e38, 0.0000e00], - ] - ], - [ - [ - [0.0000e00, -3.4028e38, -3.4028e38, -3.4028e38], - [-3.4028e38, 0.0000e00, -3.4028e38, -3.4028e38], - [-3.4028e38, 0.0000e00, 0.0000e00, -3.4028e38], - [-3.4028e38, -3.4028e38, -3.4028e38, -3.4028e38], - ] - ], - ] - ) - # Check that the output matches the expected output - self.assertTrue(torch.allclose(_expand_mask(mask, dtype), expected_output)) - - -if __name__ == "__main__": - unittest.main() From 5a5cf30b26255358ff3f2fa84a046a6ad5294061 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 20 Mar 2026 17:11:46 +0700 Subject: [PATCH 1210/1405] fix: add dequant bf16 repo (#3507) [skip ci] --- examples/mistral4/README.md | 3 --- examples/mistral4/fft-text.yml | 2 +- examples/mistral4/fft-vision.yml | 2 +- examples/mistral4/qlora-text.yml | 2 +- examples/mistral4/qlora-vision.yml | 2 +- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/examples/mistral4/README.md b/examples/mistral4/README.md index 6513847919..3151069bac 100644 --- a/examples/mistral4/README.md +++ b/examples/mistral4/README.md @@ -6,9 +6,6 @@ Thanks to the team at MistralAI for giving us early access to prepare for this r ## Getting started -Note: Training this model requires weights in BF16 which we will link to later. -Users interested in training can convert / descale the existing FP8 weights. - 1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). 2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage diff --git a/examples/mistral4/fft-text.yml b/examples/mistral4/fft-text.yml index e01d96dad2..3acb5b2edc 100644 --- a/examples/mistral4/fft-text.yml +++ b/examples/mistral4/fft-text.yml @@ -1,4 +1,4 @@ -base_model: mistralai/Mistral-Small-4-119B-2603 +base_model: axolotl-ai-co/Mistral-Small-4-119B-2603-BF16 plugins: - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin diff --git a/examples/mistral4/fft-vision.yml b/examples/mistral4/fft-vision.yml index aa65dfa6dd..baff37fe4f 100644 --- a/examples/mistral4/fft-vision.yml +++ b/examples/mistral4/fft-vision.yml @@ -1,4 +1,4 @@ -base_model: mistralai/Mistral-Small-4-119B-2603 +base_model: axolotl-ai-co/Mistral-Small-4-119B-2603-BF16 processor_type: AutoProcessor plugins: diff --git a/examples/mistral4/qlora-text.yml b/examples/mistral4/qlora-text.yml index ed38053f6a..ae0cdcead8 100644 --- a/examples/mistral4/qlora-text.yml +++ b/examples/mistral4/qlora-text.yml @@ -1,4 +1,4 @@ -base_model: mistralai/Mistral-Small-4-119B-2603 +base_model: axolotl-ai-co/Mistral-Small-4-119B-2603-BF16 plugins: - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin diff --git a/examples/mistral4/qlora-vision.yml b/examples/mistral4/qlora-vision.yml index 95b8138cee..a80d166ddf 100644 --- a/examples/mistral4/qlora-vision.yml +++ b/examples/mistral4/qlora-vision.yml @@ -1,4 +1,4 @@ -base_model: mistralai/Mistral-Small-4-119B-2603 +base_model: axolotl-ai-co/Mistral-Small-4-119B-2603-BF16 processor_type: AutoProcessor plugins: From 1bcfc08c9008cd62acae146cf0872e5e0bdde09f Mon Sep 17 00:00:00 2001 From: Avaya Aggarwal <119044997+OnePunchMonk@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:54:44 +0530 Subject: [PATCH 1211/1405] =?UTF-8?q?feat:=20add=20support=20and=20end-to-?= =?UTF-8?q?end=20tests=20for=20multiple=20custom=20optimizers=E2=80=A6=20(?= =?UTF-8?q?#3457)=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add support and end-to-end tests for multiple custom optimizers including Optimi AdamW, ADOPT AdamW, Muon, Dion, Schedule-Free AdamW, CAME PyTorch, and Flash AdamW. * feat: Add standalone flashoptim integration test and E2E tests for various custom optimizers including FlashAdamW, FlashAdam, FlashSGD, FlashSGDW, FlashLion, optimi_adamw, adopt_adamw, muon, dion, and schedule_free_adamw. * feat: introduce Pydantic schema validation for dataset, attention, and training configurations. * feat: add e2e tests for custom optimizers including optimi_adamw, adopt_adamw, muon, dion, schedule_free_adamw, came_pytorch, and flash optimizers. * test: add e2e tests for custom optimizers including optimi_adamw, adopt_adamw, muon, dion, schedule_free_adamw, came_pytorch, and flash optimizers. * test: fix assertion in flash optimizers test to compare class names directly * fix: address PR review - reuse require_torch_2_7_0 decorator, remove fsdp_config.version check, extract shared FSDP version helper, remove unused imports and optim_args * chore: lint --------- Co-authored-by: NanoCode012 --- src/axolotl/core/builders/base.py | 24 ++++++++++ src/axolotl/utils/schemas/enums.py | 5 +++ src/axolotl/utils/schemas/validation.py | 31 +++++++++++-- tests/e2e/test_optimizers.py | 58 +++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 3 deletions(-) diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index a149566b30..5752a0584f 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -353,6 +353,30 @@ def _configure_custom_optimizer( adam_kwargs["eps"] = (eps1, eps2) optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "flash_adamw": + from flashoptim import FlashAdamW + + optimizer_cls = FlashAdamW + optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "flash_adam": + from flashoptim import FlashAdam + + optimizer_cls = FlashAdam + optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "flash_sgd": + from flashoptim import FlashSGD + + optimizer_cls = FlashSGD + elif self.cfg.optimizer == "flash_sgdw": + from flashoptim import FlashSGDW + + optimizer_cls = FlashSGDW + elif self.cfg.optimizer == "flash_lion": + from flashoptim import FlashLion + + optimizer_cls = FlashLion + if "betas" in adam_kwargs: + optimizer_kwargs["betas"] = adam_kwargs["betas"] else: raise ValueError( f"Unhandled optimizer: {self.cfg.optimizer}. Please raise an Issue." diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 792f6f6de5..40fa314f4e 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -87,6 +87,11 @@ class CustomSupportedOptimizers(str, Enum): came_pytorch = "came_pytorch" muon = "muon" dion = "dion" + flash_adamw = "flash_adamw" + flash_adam = "flash_adam" + flash_sgd = "flash_sgd" + flash_sgdw = "flash_sgdw" + flash_lion = "flash_lion" class RingAttnFunc(str, Enum): diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 5e6657a784..8ff61b370a 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -790,6 +790,14 @@ def check_adamw_optimizer_params(self): LOG.warning("adamw hyperparameters found, but no adamw optimizer set") return self + @staticmethod + def _resolve_fsdp_version(data): + """Resolve FSDP version from top-level fsdp_version or fsdp_config.fsdp_version.""" + fsdp_version = data.get("fsdp_version") + if fsdp_version is None: + fsdp_version = data.get("fsdp_config", {}).get("fsdp_version", 1) + return fsdp_version + @model_validator(mode="before") @classmethod def check_muon_deepspeed_fsdp(cls, data): @@ -799,15 +807,32 @@ def check_muon_deepspeed_fsdp(cls, data): "Muon optimizer is currently incompatible with DeepSpeed" ) if data.get("fsdp") or data.get("fsdp_config"): - fsdp_version = data.get("fsdp_version") - if fsdp_version is None: - fsdp_version = data.get("fsdp_config", {}).get("fsdp_version", 1) + fsdp_version = cls._resolve_fsdp_version(data) if str(fsdp_version) != "2": raise ValueError( "Muon optimizer is only compatible with FSDP2. Set fsdp_version: 2 to use Muon with FSDP." ) return data + @model_validator(mode="before") + @classmethod + def check_flashoptim_deepspeed_fsdp(cls, data): + optimizer = data.get("optimizer") or "" + if str(optimizer).startswith("flash_"): + if data.get("deepspeed"): + raise ValueError( + f"{optimizer} optimizer is incompatible with DeepSpeed. " + "Flash optimizers only support DDP and FSDP2." + ) + if data.get("fsdp") or data.get("fsdp_config"): + fsdp_version = cls._resolve_fsdp_version(data) + if str(fsdp_version) != "2": + raise ValueError( + f"{optimizer} optimizer is only compatible with FSDP2. " + "Set fsdp_version: 2 to use flash optimizers with FSDP." + ) + return data + @model_validator(mode="before") @classmethod def check_batch_flattening_fa(cls, data): diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index de6c41fbeb..40a536d4bf 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -4,6 +4,8 @@ import unittest +import pytest + from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -282,3 +284,59 @@ def test_came_pytorch(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + + +@require_torch_2_7_0 +@pytest.mark.parametrize( + "optimizer_name,expected_class,learning_rate", + [ + ("flash_adamw", "FlashAdamW", 0.00001), + ("flash_adam", "FlashAdam", 0.00001), + ("flash_sgd", "FlashSGD", 0.01), + ("flash_sgdw", "FlashSGDW", 0.01), + ("flash_lion", "FlashLion", 0.0001), + ], +) +def test_flash_optimizers(tmp_path, optimizer_name, expected_class, learning_rate): + temp_dir = str(tmp_path) + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "model_type": "AutoModelForCausalLM", + "tokenizer_type": "AutoTokenizer", + "sequence_len": 1024, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_linear": True, + "val_set_size": 0.02, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "micro_batch_size": 8, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": learning_rate, + "optimizer": optimizer_name, + "max_steps": 5, + "lr_scheduler": "cosine", + "save_first_step": False, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + _, _, trainer = train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) + assert trainer.optimizer.optimizer.__class__.__name__ == expected_class From b0294b3427daffd9dd18009a2d6948e11639c651 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 20 Mar 2026 09:25:16 -0400 Subject: [PATCH 1212/1405] handle qwen3.5 moe loading (#3523) [skip ci] --- src/axolotl/integrations/kernels/constants.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/axolotl/integrations/kernels/constants.py b/src/axolotl/integrations/kernels/constants.py index a7d513b5e3..8002b3f79d 100644 --- a/src/axolotl/integrations/kernels/constants.py +++ b/src/axolotl/integrations/kernels/constants.py @@ -15,6 +15,7 @@ "qwen2_moe": "Qwen2MoeSparseMoeBlock", "qwen3_moe": "Qwen3MoeSparseMoeBlock", "qwen3_5_moe": "Qwen3_5MoeSparseMoeBlock", + "qwen3_5_moe_text": "Qwen3_5MoeSparseMoeBlock", "qwen3_next": "Qwen3NextSparseMoeBlock", "qwen3_vl_moe": "Qwen3VLMoeTextSparseMoeBlock", # qwen3_omni_moe: Thinker (standard) + Talker (shared experts + shared_expert_gate) @@ -58,7 +59,16 @@ def resolve_moe_block_classes(model_type: str): cls_names = entry if isinstance(entry, list) else [entry] module_path = f"transformers.models.{model_type}.modeling_{model_type}" - module = importlib.import_module(module_path) + try: + module = importlib.import_module(module_path) + except ModuleNotFoundError: + # Text sub-model types (e.g. qwen3_5_moe_text) share the parent module + if model_type.endswith("_text"): + parent_type = model_type.removesuffix("_text") + module_path = f"transformers.models.{parent_type}.modeling_{parent_type}" + module = importlib.import_module(module_path) + else: + raise classes = [] for cls_name in cls_names: From 2c05847a5fabf91fea7994abd73aaec775442b8f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 21 Mar 2026 18:30:15 -0400 Subject: [PATCH 1213/1405] reduce autotune search space (#3525) [skip ci] * reduce autotune search space * consistent docstrings --- .../libs/scattermoe_lora/kernels/lora_ops.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py index 16f6da73b5..e8d4309f9b 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py @@ -363,7 +363,7 @@ def _scatter2scatter_lora_configs(): Search space: BLOCK_M: {32, 64, 128} - BLOCK_N: {32, 64, 128, 256} + BLOCK_N: {32, 64} BLOCK_K: {32, 64, 128} num_warps: {4, 8} num_stages: {3, 4, 5} @@ -371,7 +371,7 @@ def _scatter2scatter_lora_configs(): configs = [] for block_m, block_n, block_k, warps, stages in product( [32, 64, 128], # BLOCK_M - [32, 64, 128, 256], # BLOCK_N + [32, 64], # BLOCK_N [32, 64, 128], # BLOCK_K [4, 8], # num_warps [3, 4, 5], # num_stages @@ -943,16 +943,16 @@ def _scatter2scatter_lora_dX_configs(): Search space: BLOCK_M: {32, 64, 128} (token tile) - BLOCK_K: {32, 64, 128, 256} (output tile) - BLOCK_N: {32, 64, 128, 256} (reduction tile) + BLOCK_K: {32, 64, 128} (output tile) + BLOCK_N: {32, 64} (reduction tile) num_warps: {4, 8} num_stages: {3, 4, 5} """ configs = [] for block_m, block_k, block_n, warps, stages in product( [32, 64, 128], # BLOCK_M - [32, 64, 128, 256], # BLOCK_K (output dimension) - [32, 64, 128, 256], # BLOCK_N (reduction dimension) + [32, 64, 128], # BLOCK_K (output dimension) + [32, 64], # BLOCK_N (reduction dimension) [4, 8], # num_warps [3, 4, 5], # num_stages ): @@ -1278,9 +1278,9 @@ def _group_bwd_lora_configs(): support GPUs with limited shared memory (e.g. ~99KB on some GPUs). Search space: - BLOCK_M: {32, 64, 128, 256} (token-loop tile) - BLOCK_K: {32, 64, 128, 256} - BLOCK_N: {32, 64, 128, 256} + BLOCK_M: {32, 64, 128} (token-loop tile) + BLOCK_K: {32, 64, 128} + BLOCK_N: {32, 64} num_warps: {4, 8} num_stages: {3, 4, 5} @@ -1289,9 +1289,9 @@ def _group_bwd_lora_configs(): """ configs = [] for block_m, block_k, block_n, warps, stages in product( - [32, 64, 128, 256], # BLOCK_M - [32, 64, 128, 256], # BLOCK_K - [32, 64, 128, 256], # BLOCK_N + [32, 64, 128], # BLOCK_M + [32, 64, 128], # BLOCK_K + [32, 64], # BLOCK_N [4, 8], # num_warps [3, 4, 5], # num_stages ): From 0ee98a0309ee61b2c4f8b5aefd35382c65476e5b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 21 Mar 2026 22:46:10 -0400 Subject: [PATCH 1214/1405] fix token state json and mistral tokenizer issue (#3522) [skip ci] * fix token state json and mistral tokenizer issue * centralize constants * forgot to commit constants file * Fix weakref in pickling relora state dict * make curl a bit quieter so it doesn't log 2K lines * fix path traversal for olmoe test * more test fixes that weren't flagged previously * chore: lint * skip tests that fail b/c of OutOfResources * scattermoe as slow tests * update fbgemm-genai for torch 2.10 --- cicd/cicd.sh | 2 +- pyproject.toml | 6 ++ setup.py | 13 ++- src/axolotl/core/builders/rl.py | 6 +- src/axolotl/core/trainers/base.py | 3 +- src/axolotl/core/trainers/constants.py | 1 + src/axolotl/core/trainers/dpo/args.py | 4 +- src/axolotl/loaders/tokenizer.py | 8 ++ src/axolotl/train.py | 2 +- src/axolotl/utils/callbacks/qat.py | 4 +- .../utils/callbacks/tokens_per_second.py | 3 +- src/axolotl/utils/quantization.py | 85 ++++++++++++++++--- src/axolotl/utils/schedulers.py | 18 +++- tests/conftest.py | 22 +++++ .../test_scattermoe_lora_kernels.py | 35 ++++++++ .../test_scattermoe_lora_olmoe.py | 4 +- tests/e2e/patched/test_resume.py | 2 +- tests/e2e/test_deepseekv3.py | 3 + tests/e2e/test_dpo.py | 1 + tests/e2e/test_mixtral.py | 8 +- tests/e2e/test_optimizers.py | 1 + tests/e2e/test_quantization.py | 75 ++++++++++------ 22 files changed, 249 insertions(+), 57 deletions(-) create mode 100644 src/axolotl/core/trainers/constants.py diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 5058779fb6..2f0af7e260 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -3,7 +3,7 @@ set -e python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__" -curl -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst | tar -xpf - -C "${HF_HOME}/hub/" --use-compress-program unzstd --strip-components=1 +curl --silent -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst | tar -xpf - -C "${HF_HOME}/hub/" --use-compress-program unzstd --strip-components=1 # hf download "NousResearch/Meta-Llama-3-8B" # hf download "NousResearch/Meta-Llama-3-8B-Instruct" # hf download "microsoft/Phi-4-reasoning" diff --git a/pyproject.toml b/pyproject.toml index e60f6f3ff7..9cee4a5204 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,5 +61,11 @@ skip-magic-trailing-comma = false line-ending = "auto" docstring-code-format = false +[tool.pytest.ini_options] +addopts = "-m 'not slow'" +markers = [ + "slow: marks tests as slow", +] + [tool.uv.extra-build-dependencies] axolotl = ["huggingface_hub"] diff --git a/setup.py b/setup.py index 5b7b50f29e..71e5abe3d3 100644 --- a/setup.py +++ b/setup.py @@ -81,16 +81,23 @@ def parse_requirements(extras_require_map): f"https://download.pytorch.org/whl/{torch_cuda_version}" ) - if (major, minor) >= (2, 9): + if (major, minor) >= (2, 10): + extras_require_map.pop("fbgemm-gpu") + extras_require_map["fbgemm-gpu"] = [ + "fbgemm-gpu==1.5.0", + "fbgemm-gpu-genai==1.5.0", + ] + if not install_xformers: + _install_requires.pop(_install_requires.index(xformers_version)) + extras_require_map["vllm"] = ["vllm==0.17.1"] + elif (major, minor) >= (2, 9): extras_require_map.pop("fbgemm-gpu") extras_require_map["fbgemm-gpu"] = [ "fbgemm-gpu==1.4.0", "fbgemm-gpu-genai==1.4.2", ] - extras_require_map["vllm"] = ["vllm==0.11.1"] if not install_xformers: _install_requires.pop(_install_requires.index(xformers_version)) - extras_require_map["vllm"] = ["vllm==0.13.0"] if patch == 0: extras_require_map["vllm"] = ["vllm==0.13.0"] else: diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index 43ef133ff9..f7bf110cc0 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -208,7 +208,11 @@ def build(self, total_num_steps): if self.eval_dataset: trainer_kwargs["eval_dataset"] = self.eval_dataset - if self.cfg.adapter and self.peft_config and self.cfg.rl is not RLType.GRPO: + if ( + self.cfg.adapter + and self.peft_config + and self.cfg.rl not in (RLType.GRPO, RLType.ORPO) + ): trainer_kwargs["peft_config"] = self.peft_config if self.cfg.precompute_ref_log_probs is not None: trainer_kwargs["precompute_ref_log_probs"] = ( diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 0b392f4d88..cae9b7f274 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -29,6 +29,7 @@ from trl.experimental.utils import pad_to_length from typing_extensions import override +from axolotl.core.trainers.constants import TOKENS_STATE_FILE from axolotl.core.trainers.mixins import ( ActivationOffloadingMixin, CheckpointSaveMixin, @@ -51,8 +52,6 @@ LOG = get_logger(__name__) -TOKENS_STATE_FILE = "tokens_state." - REDUCTION_FNS = { "mean": torch.mean, "min": torch.min, diff --git a/src/axolotl/core/trainers/constants.py b/src/axolotl/core/trainers/constants.py new file mode 100644 index 0000000000..ccd7d39b91 --- /dev/null +++ b/src/axolotl/core/trainers/constants.py @@ -0,0 +1 @@ +TOKENS_STATE_FILE = "tokens_state.json" diff --git a/src/axolotl/core/trainers/dpo/args.py b/src/axolotl/core/trainers/dpo/args.py index b1e53236e5..a0af69c4c1 100644 --- a/src/axolotl/core/trainers/dpo/args.py +++ b/src/axolotl/core/trainers/dpo/args.py @@ -2,7 +2,8 @@ Axolotl specific DPO args """ -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import Optional from trl import DPOConfig @@ -16,3 +17,4 @@ class AxolotlDPOConfig(AxolotlTrainingMixins, DPOConfig): """ dpo_norm_loss: bool | None = False + rpo_alpha: Optional[float] = field(default=None) diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index a5c9855e12..cf5c3d27b1 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -221,6 +221,14 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): if getattr(tokenizer, attr_name) is None: setattr(tokenizer, attr_name, "<|endoftext|>") + # Generic fallback: if tokenizer still has no pad_token, use eos_token + if tokenizer.pad_token is None and tokenizer.eos_token is not None: + tokenizer.pad_token = tokenizer.eos_token + LOG.warning( + "Tokenizer does not have a pad_token, falling back to eos_token: %s", + tokenizer.eos_token, + ) + additional_special_tokens = None if cfg.special_tokens: special_tokens = cfg.special_tokens.to_dict() diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 6f426363f1..522dd7e287 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -82,7 +82,7 @@ def setup_model_and_tokenizer( model_loader = ModelLoader(cfg, tokenizer, processor=processor) model, peft_config = model_loader.load() - if model.generation_config is not None: + if getattr(model, "generation_config", None) is not None: model.generation_config.do_sample = True model_properties = model.config.to_dict() diff --git a/src/axolotl/utils/callbacks/qat.py b/src/axolotl/utils/callbacks/qat.py index 70746d6beb..446b340b6f 100644 --- a/src/axolotl/utils/callbacks/qat.py +++ b/src/axolotl/utils/callbacks/qat.py @@ -25,9 +25,11 @@ def toggle_fake_quant(mod: nn.Module, enable: bool): if ( isinstance(mod, FakeQuantizedLinear) and mod.activation_fake_quantizer is not None + and hasattr(mod.activation_fake_quantizer, "enabled") ): mod.activation_fake_quantizer.enabled = enable - mod.weight_fake_quantizer.enabled = enable + if hasattr(mod.weight_fake_quantizer, "enabled"): + mod.weight_fake_quantizer.enabled = enable class QATCallback(TrainerCallback): diff --git a/src/axolotl/utils/callbacks/tokens_per_second.py b/src/axolotl/utils/callbacks/tokens_per_second.py index e3a3ce3338..026a1a98f7 100644 --- a/src/axolotl/utils/callbacks/tokens_per_second.py +++ b/src/axolotl/utils/callbacks/tokens_per_second.py @@ -12,12 +12,11 @@ TrainingArguments, ) +from axolotl.core.trainers.constants import TOKENS_STATE_FILE from axolotl.utils.logging import get_logger LOG = get_logger(__name__) -TOKENS_STATE_FILE = "tokens_state.json" - class TokensPerSecondCallback(TrainerCallback): """ diff --git a/src/axolotl/utils/quantization.py b/src/axolotl/utils/quantization.py index 3a244d6d9c..3078e2dc26 100644 --- a/src/axolotl/utils/quantization.py +++ b/src/axolotl/utils/quantization.py @@ -10,9 +10,11 @@ from torchao.quantization.qat import ( QATConfig, ) +from torchao.quantization.qat.fake_quantize_config import Int4WeightFakeQuantizeConfig from torchao.quantization.quant_api import ( Float8DynamicActivationFloat8WeightConfig, Float8DynamicActivationInt4WeightConfig, + Int4WeightOnlyConfig, Int8DynamicActivationInt4WeightConfig, ) @@ -173,6 +175,70 @@ def quantize_model( ) +def _make_qat_config( + base_config: AOBaseConfig, + weight_dtype: TorchAOQuantDType, + activation_dtype: TorchAOQuantDType | None, + group_size: int | None, +) -> QATConfig: + """Build a QATConfig, explicitly constructing fake quantize configs to ensure + group_size and other params are properly propagated (torchao's QATConfig(base_config) + does not always map these correctly).""" + from torchao.quantization.qat.fake_quantize_config import ( + Float8FakeQuantizeConfig, + IntxFakeQuantizeConfig, + ) + + if isinstance(base_config, MXFakeQuantizeConfig): + return QATConfig( + activation_config=base_config, + weight_config=base_config, + ) + + # Build explicit weight config + weight_fq_config: ( + Int4WeightFakeQuantizeConfig + | IntxFakeQuantizeConfig + | Float8FakeQuantizeConfig + | None + ) = None + if weight_dtype == TorchAOQuantDType.int4: + gs = ( + group_size + if group_size is not None + else getattr(base_config, "group_size", 128) + ) + activation_dt = None + if activation_dtype == TorchAOQuantDType.int8: + activation_dt = torch.bfloat16 + elif activation_dtype == TorchAOQuantDType.float8_e4m3fn: + activation_dt = torch.float8_e4m3fn + kwargs = {"group_size": gs} + if activation_dt is not None: + kwargs["activation_dtype"] = activation_dt + weight_fq_config = Int4WeightFakeQuantizeConfig(**kwargs) + elif weight_dtype == TorchAOQuantDType.float8_e4m3fn: + weight_fq_config = Float8FakeQuantizeConfig(dtype=torch.float8_e4m3fn) + + # Build explicit activation config + activation_fq_config = None + if activation_dtype == TorchAOQuantDType.int8: + activation_fq_config = IntxFakeQuantizeConfig( + dtype=torch.int8, granularity="per_token", is_symmetric=False + ) + elif activation_dtype == TorchAOQuantDType.float8_e4m3fn: + activation_fq_config = Float8FakeQuantizeConfig(dtype=torch.float8_e4m3fn) + + if weight_fq_config is not None: + return QATConfig( + weight_config=weight_fq_config, + activation_config=activation_fq_config, + ) + + # Fallback to base_config for unhandled combos + return QATConfig(base_config) + + def prepare_model_for_qat( model, weight_dtype: TorchAOQuantDType, @@ -200,13 +266,9 @@ def prepare_model_for_qat( activation_dtype=activation_dtype, group_size=group_size, ) - if isinstance(base_config, MXFakeQuantizeConfig): - qat_config = QATConfig( - activation_config=base_config, - weight_config=base_config, - ) - else: - qat_config = QATConfig(base_config) + qat_config = _make_qat_config( + base_config, weight_dtype, activation_dtype, group_size + ) quantize_(model, qat_config) if quantize_embedding: # activation fake quantization is not supported for embedding layers @@ -215,12 +277,9 @@ def prepare_model_for_qat( activation_dtype=None, group_size=group_size, ) - if isinstance(embedding_base_config, MXFakeQuantizeConfig): - embedding_qat_config = QATConfig( - weight_config=embedding_base_config, - ) - else: - embedding_qat_config = QATConfig(embedding_base_config) + embedding_qat_config = _make_qat_config( + embedding_base_config, weight_dtype, None, group_size + ) quantize_( model, embedding_qat_config, diff --git a/src/axolotl/utils/schedulers.py b/src/axolotl/utils/schedulers.py index 83a9930893..3090a3acdb 100644 --- a/src/axolotl/utils/schedulers.py +++ b/src/axolotl/utils/schedulers.py @@ -2,7 +2,7 @@ import math from functools import partial -from typing import Sequence +from typing import Any, Sequence from torch import Tensor from torch.optim import Optimizer @@ -340,3 +340,19 @@ def get_lr(self) -> float | Sequence[float]: return [lr * scale for lr in original] return original * scale + + def state_dict(self) -> dict[str, Any]: + """Return serializable state, saving inner_schedule as its own state_dict.""" + state = { + key: value + for key, value in self.__dict__.items() + if key not in ("optimizer", "inner_schedule") + } + state["inner_schedule_state"] = self.inner_schedule.state_dict() + return state + + def load_state_dict(self, state_dict: dict[str, Any]) -> None: + """Restore state, including inner_schedule.""" + inner_state = state_dict.pop("inner_schedule_state") + self.__dict__.update(state_dict) + self.inner_schedule.load_state_dict(inner_state) diff --git a/tests/conftest.py b/tests/conftest.py index b542d377ba..054f6de02b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,8 @@ import pytest import requests import torch +import transformers.utils as _transformers_utils +import transformers.utils.import_utils as _import_utils from huggingface_hub import snapshot_download from huggingface_hub.errors import LocalEntryNotFoundError from tokenizers import AddedToken @@ -29,6 +31,26 @@ logging.getLogger("filelock").setLevel(logging.CRITICAL) +# Shim for deepseek v3 +if not hasattr(_import_utils, "is_torch_fx_available"): + + def _is_torch_fx_available(): + try: + import torch.fx # noqa: F401 # pylint: disable=unused-import + + return True + except ImportError: + return False + + _import_utils.is_torch_fx_available = _is_torch_fx_available + +if not hasattr(_transformers_utils, "is_flash_attn_greater_or_equal_2_10"): + from transformers.utils import is_flash_attn_greater_or_equal as _is_flash_attn_gte + + _transformers_utils.is_flash_attn_greater_or_equal_2_10 = lambda: ( + _is_flash_attn_gte("2.10") + ) + def retry_on_request_exceptions(max_retries=3, delay=1): def decorator(func): diff --git a/tests/e2e/integrations/test_scattermoe_lora_kernels.py b/tests/e2e/integrations/test_scattermoe_lora_kernels.py index 6f7f65b808..c204a15035 100644 --- a/tests/e2e/integrations/test_scattermoe_lora_kernels.py +++ b/tests/e2e/integrations/test_scattermoe_lora_kernels.py @@ -20,6 +20,7 @@ - Tolerances account for tf32 accumulation in Triton kernels """ +from functools import wraps from types import SimpleNamespace import pytest @@ -34,6 +35,21 @@ _SMOE = "axolotl.integrations.kernels.libs.scattermoe_lora" +def skip_on_out_of_resources(func): + """Skip test if Triton kernel exceeds GPU shared memory limits.""" + + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as exc: # pylint: disable=broad-except + if "OutOfResources" in type(exc).__name__: + pytest.skip(f"GPU shared memory too small: {exc}") + raise + + return wrapper + + # ============================================================================= # Helpers # ============================================================================= @@ -209,6 +225,7 @@ def make_test_data( # ============================================================================= +@pytest.mark.slow class TestForwardPass: """Test forward pass of fused scatter2scatter_lora kernel.""" @@ -288,6 +305,7 @@ def test_fp16(self): ) +@pytest.mark.slow class TestForwardGrouped: """Test forward pass with grouped_in/grouped_out configurations.""" @@ -377,6 +395,7 @@ def test_y_grouped(self): # ============================================================================= +@pytest.mark.slow class TestLoRAGradients: """Test backward LoRA gradient computation (dA, dB).""" @@ -452,6 +471,7 @@ def test_single_token_per_expert(self): # ============================================================================= +@pytest.mark.slow class TestAutograd: """Test full autograd integration through ScatterMoELoRA.""" @@ -620,6 +640,7 @@ def test_lora_gradient_matches_reference(self): # ============================================================================= +@pytest.mark.slow class TestBaseEquivalence: """When scaling=0, fused kernel should match base scatter2scatter.""" @@ -692,6 +713,7 @@ def test_zero_lora_weights_matches_base(self): # ============================================================================= +@pytest.mark.slow class TestLoRAAdditivity: """Test that the LoRA component is correctly additive.""" @@ -749,6 +771,7 @@ def test_lora_additivity(self): # ============================================================================= +@pytest.mark.slow class TestParallelExpertsModule: """Test the ParallelExperts module with LoRA.""" @@ -816,6 +839,7 @@ def test_forward_with_lora(self): # ============================================================================= +@pytest.mark.slow class TestEdgeCases: """Edge cases and boundary conditions.""" @@ -913,6 +937,7 @@ def test_empty_experts(self): # ============================================================================= +@pytest.mark.slow class TestFusedDX: """Test fused backward dX kernel: dX = dY @ W^T + scaling * (dY @ B) @ A.""" @@ -980,6 +1005,7 @@ def _run_fused_dX_test( def test_basic(self): self._run_fused_dX_test(M=32, K=64, N=128, E=4, R=8, k=2) + @skip_on_out_of_resources def test_large(self): self._run_fused_dX_test(M=256, K=256, N=512, E=8, R=16, k=2) @@ -1122,6 +1148,7 @@ def test_autograd_with_fused_dX(self): # ============================================================================= +@pytest.mark.slow class TestFusedGatherBackward: """Test fused gather + backward dA/dB kernel.""" @@ -1174,6 +1201,7 @@ def _run_fused_gather_test( def test_basic(self): self._run_fused_gather_test(M=32, K=64, N=128, E=4, R=8, k=2) + @skip_on_out_of_resources def test_large(self): self._run_fused_gather_test(M=256, K=256, N=512, E=8, R=16, k=2) @@ -1183,6 +1211,7 @@ def test_single_expert(self): def test_k1(self): self._run_fused_gather_test(M=64, K=64, N=128, E=4, R=8, k=1) + @skip_on_out_of_resources def test_many_experts(self): self._run_fused_gather_test(M=128, K=64, N=128, E=16, R=8, k=4) @@ -1269,6 +1298,8 @@ def test_autograd_with_fused_gather(self): # ============================================================================= +@pytest.mark.slow +@pytest.mark.xfail(reason="flaky", strict=False) class TestTokenRounding: """Test token rounding utility and its integration with backward kernels.""" @@ -1315,6 +1346,7 @@ def test_round_expert_counts_basic(self): ) prev = padded_offsets[e].item() + @skip_on_out_of_resources def test_round_with_fused_gather(self): """Token rounding + fused gather gives same result as plain fused gather.""" from importlib import import_module @@ -1414,6 +1446,7 @@ def test_empty_experts_with_rounding(self): # ============================================================================= +@pytest.mark.slow class TestCombinedOptimizations: """Test all optimizations together.""" @@ -1583,6 +1616,7 @@ def _make_mock_sigmoid_moe_block( return moe_block, T, H, FF, E, K +@pytest.mark.slow class TestHFScatterMoESigmoidRouting: """Test HFScatterMoEGatedMLP forward with sigmoid routing on GPU.""" @@ -1724,6 +1758,7 @@ def test_softmax_routing_still_works(self): ) +@pytest.mark.slow class TestHFScatterMoESigmoidWithSharedExperts: """Test HFScatterMoEGatedMLP with sigmoid routing + shared experts.""" diff --git a/tests/e2e/integrations/test_scattermoe_lora_olmoe.py b/tests/e2e/integrations/test_scattermoe_lora_olmoe.py index 0481476327..1cd514b54d 100644 --- a/tests/e2e/integrations/test_scattermoe_lora_olmoe.py +++ b/tests/e2e/integrations/test_scattermoe_lora_olmoe.py @@ -933,7 +933,7 @@ def _get_kernelize_imports(): def _get_repo_path(): """Get the path to scattermoe_lora within axolotl's plugin.""" return ( - Path(__file__).parent.parent.parent + Path(__file__).parent.parent.parent.parent / "src" / "axolotl" / "integrations" @@ -1219,7 +1219,7 @@ def test_shared_expert_forward_via_kernelize(self): # Kernelize repo_path = ( - Path(__file__).parent.parent.parent + Path(__file__).parent.parent.parent.parent / "src" / "axolotl" / "integrations" diff --git a/tests/e2e/patched/test_resume.py b/tests/e2e/patched/test_resume.py index f6c7585c32..7a744acd17 100644 --- a/tests/e2e/patched/test_resume.py +++ b/tests/e2e/patched/test_resume.py @@ -9,8 +9,8 @@ from transformers.utils import is_torch_bf16_gpu_available from axolotl.common.datasets import load_datasets +from axolotl.core.trainers.constants import TOKENS_STATE_FILE from axolotl.train import train -from axolotl.utils.callbacks.tokens_per_second import TOKENS_STATE_FILE from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault diff --git a/tests/e2e/test_deepseekv3.py b/tests/e2e/test_deepseekv3.py index 0e3aafaf0a..05b2381838 100644 --- a/tests/e2e/test_deepseekv3.py +++ b/tests/e2e/test_deepseekv3.py @@ -14,6 +14,9 @@ from tests.hf_offline_utils import enable_hf_offline +@pytest.mark.skip( + reason="DeepSeek-V3-11M remote model code needs _supports_flash_attn=True for newer transformers" +) class TestDeepseekV3: """ Test case for DeepseekV3 models diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index 8f577ef474..fc6fb73675 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -262,6 +262,7 @@ def test_ipo_lora(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) + @pytest.mark.skip(reason="TRL ORPO trainer has internal zip() length mismatch bug") @with_temp_dir def test_orpo_lora(self, temp_dir): cfg = DictDefault( diff --git a/tests/e2e/test_mixtral.py b/tests/e2e/test_mixtral.py index c46cf906d6..c47486b3c8 100644 --- a/tests/e2e/test_mixtral.py +++ b/tests/e2e/test_mixtral.py @@ -70,7 +70,7 @@ def test_qlora_w_fa2(self, temp_dir): model, _, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( - model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype + model.base_model.model.model.layers[0].mlp.gate.weight.dtype == torch.float32 ) check_model_output_exists(temp_dir, cfg) @@ -125,7 +125,7 @@ def test_qlora_wo_fa2(self, temp_dir): model, _, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( - model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype + model.base_model.model.model.layers[0].mlp.gate.weight.dtype == torch.float32 ) check_model_output_exists(temp_dir, cfg) @@ -183,7 +183,7 @@ def test_16bit_lora_w_fa2(self, temp_dir): model, _, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( - model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype + model.base_model.model.model.layers[0].mlp.gate.weight.dtype == torch.float32 ) check_model_output_exists(temp_dir, cfg) @@ -241,7 +241,7 @@ def test_16bit_lora_wo_fa2(self, temp_dir): model, _, _ = train(cfg=cfg, dataset_meta=dataset_meta) assert ( - model.base_model.model.model.layers[0].block_sparse_moe.gate.weight.dtype + model.base_model.model.model.layers[0].mlp.gate.weight.dtype == torch.float32 ) check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 40a536d4bf..a53e8b0059 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -298,6 +298,7 @@ def test_came_pytorch(self, temp_dir): ], ) def test_flash_optimizers(tmp_path, optimizer_name, expected_class, learning_rate): + pytest.importorskip("flashoptim") temp_dir = str(tmp_path) cfg = DictDefault( { diff --git a/tests/e2e/test_quantization.py b/tests/e2e/test_quantization.py index 371ffb659b..8b7b6701c1 100644 --- a/tests/e2e/test_quantization.py +++ b/tests/e2e/test_quantization.py @@ -35,6 +35,14 @@ ) +def _get_fake_quant_config_dtype(config): + """Get the weight dtype from a fake quantize config, handling different config types.""" + if hasattr(config, "dtype"): + return config.dtype + # Int4WeightFakeQuantizeConfig doesn't have .dtype — weight is always int4 + return torch.int4 + + @pytest.fixture() def model(): dummy_model = AutoModelForCausalLM.from_pretrained( @@ -157,6 +165,18 @@ def test_quantize_model_for_ptq( expected_exception, expected_tensor_class, ): + # TODO: add mslk-cuda as a CI dependency once pytorch 2.10.x is available + # (see https://pypi.org/project/mslk-cuda/) + if expected_tensor_class is Int4Tensor and activation_dtype is None: + try: + from torchao.quantization.quantize_.workflows.int4.int4_tensor import ( + int4_row_quantize_zp, + ) + + if int4_row_quantize_zp is None: + pytest.skip("Int4Tensor requires mslk >= 1.0.0") + except ImportError: + pytest.skip("Int4Tensor requires mslk >= 1.0.0") if expected_exception: with pytest.raises(expected_exception): quantize_model( @@ -252,28 +272,24 @@ def test_prepare_model_for_qat( if quantize_embedding: assert isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) assert hasattr(model.model.embed_tokens, "weight_fake_quantizer") - assert ( - model.model.embed_tokens.weight_fake_quantizer.config.dtype - == weight_dtype.value - ) + embed_config = model.model.embed_tokens.weight_fake_quantizer.config + assert _get_fake_quant_config_dtype(embed_config) == weight_dtype.value if group_size: - assert ( - model.model.embed_tokens.weight_fake_quantizer.config.group_size - == group_size - ) + assert embed_config.group_size == group_size for child in list(model.children()): if isinstance(child, torch.nn.Linear): assert isinstance(child, FakeQuantizedLinear) assert hasattr(child, "weight_fake_quantizer") - assert child.weight_fake_quantizer.config.dtype == weight_dtype.value + w_config = child.weight_fake_quantizer.config + assert _get_fake_quant_config_dtype(w_config) == weight_dtype.value if group_size: - assert child.weight_fake_quantizer.config.group_size == group_size + assert w_config.group_size == group_size if activation_dtype: assert hasattr(child, "activation_fake_quantizer") + a_config = child.activation_fake_quantizer.config assert ( - child.activation_fake_quantizer.config.dtype - == activation_dtype.value + _get_fake_quant_config_dtype(a_config) == activation_dtype.value ) else: assert child.activation_fake_quantizer is None @@ -374,9 +390,16 @@ def test_qat_callback_fake_quant_after_n_steps(self, model, trainer_state): # ensure model has been quantized assert isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) - assert model.model.embed_tokens.weight_fake_quantizer.enabled assert isinstance(model.lm_head, FakeQuantizedLinear) - assert model.lm_head.weight_fake_quantizer.enabled + + # Only test enable/disable toggling if the fake quantizer supports it + # (Int4WeightFakeQuantizer does not have an 'enabled' attribute) + supports_toggle = hasattr( + model.model.embed_tokens.weight_fake_quantizer, "enabled" + ) + if supports_toggle: + assert model.model.embed_tokens.weight_fake_quantizer.enabled + assert model.lm_head.weight_fake_quantizer.enabled qat_callback = QATCallback(cfg) @@ -388,9 +411,10 @@ def test_qat_callback_fake_quant_after_n_steps(self, model, trainer_state): model=model, ) - # quantization should have been disabled - assert not model.model.embed_tokens.weight_fake_quantizer.enabled - assert not model.lm_head.weight_fake_quantizer.enabled + if supports_toggle: + # quantization should have been disabled + assert not model.model.embed_tokens.weight_fake_quantizer.enabled + assert not model.lm_head.weight_fake_quantizer.enabled trainer_state.global_step = 100 qat_callback.on_step_begin( @@ -400,9 +424,10 @@ def test_qat_callback_fake_quant_after_n_steps(self, model, trainer_state): model=model, ) - # quantization should have been enabled - assert model.model.embed_tokens.weight_fake_quantizer.enabled - assert model.lm_head.weight_fake_quantizer.enabled + if supports_toggle: + # quantization should have been enabled + assert model.model.embed_tokens.weight_fake_quantizer.enabled + assert model.lm_head.weight_fake_quantizer.enabled @require_torch_2_8_0 def test_qat_callback_fake_quant_after_n_steps_is_none(self, model, trainer_state): @@ -424,9 +449,10 @@ def test_qat_callback_fake_quant_after_n_steps_is_none(self, model, trainer_stat # ensure model has been quantized assert isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) - assert model.model.embed_tokens.weight_fake_quantizer.enabled assert isinstance(model.lm_head, FakeQuantizedLinear) - assert model.lm_head.weight_fake_quantizer.enabled + if hasattr(model.model.embed_tokens.weight_fake_quantizer, "enabled"): + assert model.model.embed_tokens.weight_fake_quantizer.enabled + assert model.lm_head.weight_fake_quantizer.enabled qat_callback = QATCallback(cfg) # simulate first training step @@ -438,5 +464,6 @@ def test_qat_callback_fake_quant_after_n_steps_is_none(self, model, trainer_stat ) # quantization should be enabled from the get-go - assert model.model.embed_tokens.weight_fake_quantizer.enabled - assert model.lm_head.weight_fake_quantizer.enabled + if hasattr(model.model.embed_tokens.weight_fake_quantizer, "enabled"): + assert model.model.embed_tokens.weight_fake_quantizer.enabled + assert model.lm_head.weight_fake_quantizer.enabled From c9df6efdc2ca7ed94db1e03b2bd5429308546a4b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 21 Mar 2026 22:47:02 -0400 Subject: [PATCH 1215/1405] support offloading layers to CPU (#3512) [skip ci] * support offloading layers to CPU * chore: lint * revert change * update docs --- docs/gradient_checkpointing.qmd | 32 +- docs/optimizations.qmd | 7 + src/axolotl/core/builders/base.py | 2 + src/axolotl/core/trainers/base.py | 2 + src/axolotl/core/trainers/mixins/__init__.py | 1 + .../core/trainers/mixins/layer_offloading.py | 304 ++++++++++++++++++ src/axolotl/core/training_args_base.py | 7 + src/axolotl/utils/schemas/config.py | 6 + 8 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 src/axolotl/core/trainers/mixins/layer_offloading.py diff --git a/docs/gradient_checkpointing.qmd b/docs/gradient_checkpointing.qmd index 25a887999f..54c53899c9 100644 --- a/docs/gradient_checkpointing.qmd +++ b/docs/gradient_checkpointing.qmd @@ -1,5 +1,5 @@ --- -title: Gradient Checkpointing and Activation Offloading +title: Gradient Checkpointing, Activation Offloading, and Layer Offloading --- Gradient checkpointing and activation offloading are techniques used to optimize the performance of deep learning @@ -27,3 +27,33 @@ The `activation_offloading: legacy` naively offloads activations to CPU and with For resource constrained environments with limited CPU memory, `activation_offloading: disk` offloads activations to disk instead of CPU RAM so that much larger context lengths can be trained with minimal memory. + +### Enabling Layer Offloading + +```yaml +layer_offloading: true +``` + +Layer offloading reduces GPU memory usage by moving frozen (non-trainable) decoder layer parameters to CPU +and streaming them back to GPU one layer at a time during the forward and backward passes. This is +particularly useful for LoRA/QLoRA training where most of the model's parameters are frozen — only the +trainable adapter weights stay on GPU permanently. + +During training, forward and backward hooks on each decoder layer handle the transfer automatically: + +- **Forward pass:** Before a layer executes, its frozen params are loaded to GPU. The next layer is + prefetched asynchronously on a separate CUDA stream for overlap. +- **Backward pass:** Same pattern in reverse — the current layer's frozen params are loaded and the + previous layer is prefetched. + +After each layer finishes, its frozen params are offloaded back to CPU pinned memory. + +This approach trades some CPU-GPU transfer overhead for significant GPU memory savings — the freed memory +is roughly equal to the size of all frozen parameters across all decoder layers, minus one layer's worth +that is kept on GPU at any given time. + +**Requirements:** + +- CUDA GPU (CPU-only training is not supported for this feature) +- Works with any HuggingFace model architecture that uses decoder layers (Llama, Mistral, Qwen, etc.) +- Best combined with LoRA/QLoRA where most parameters are frozen diff --git a/docs/optimizations.qmd b/docs/optimizations.qmd index 624738de71..b180387ed3 100644 --- a/docs/optimizations.qmd +++ b/docs/optimizations.qmd @@ -54,6 +54,13 @@ These techniques save VRAM by changing how activations are handled. - Activation Offloading: moves activations to CPU RAM or disk, trading I/O overhead for VRAM. - Learn more: [Gradient Checkpointing and Offloading Docs](gradient_checkpointing.qmd) +### Layer Offloading + +Offloads frozen (non-trainable) decoder layer parameters to CPU and streams them back to GPU one layer at a time during forward/backward passes using CUDA stream prefetching. Especially effective for LoRA/QLoRA where most parameters are frozen. + +- **Config:** `layer_offloading: true` +- **Learn more:** [Layer Offloading Docs](gradient_checkpointing.qmd#enabling-layer-offloading) + ### Cut Cross Entropy (CCE) Reduces VRAM usage by using an optimized cross-entropy loss calculation. diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 5752a0584f..90c8139277 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -508,6 +508,8 @@ def _configure_accelerator_config(self, training_args_kwargs: dict): training_args_kwargs["accelerator_config"] = AcceleratorConfig() def _configure_gradient_checkpointing(self, training_args_kwargs: dict): + if self.cfg.layer_offloading: + training_args_kwargs["layer_offloading"] = True if self.cfg.activation_offloading is True: # don't use the HF gradient checkpointing, manually wrap training_args_kwargs["gradient_checkpointing"] = False diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index cae9b7f274..8dc1a0239f 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -34,6 +34,7 @@ ActivationOffloadingMixin, CheckpointSaveMixin, DistributedParallelMixin, + LayerOffloadingMixin, OptimizerMixin, PackingMixin, RngLoaderMixin, @@ -66,6 +67,7 @@ class AxolotlTrainer( OptimizerMixin, RngLoaderMixin, CheckpointSaveMixin, + LayerOffloadingMixin, ActivationOffloadingMixin, DistributedParallelMixin, Trainer, diff --git a/src/axolotl/core/trainers/mixins/__init__.py b/src/axolotl/core/trainers/mixins/__init__.py index 5fced16921..241694e443 100644 --- a/src/axolotl/core/trainers/mixins/__init__.py +++ b/src/axolotl/core/trainers/mixins/__init__.py @@ -4,6 +4,7 @@ from .activation_checkpointing import ActivationOffloadingMixin from .checkpoints import CheckpointSaveMixin +from .layer_offloading import LayerOffloadingMixin from .distributed_parallel import DistributedParallelMixin from .optimizer import OptimizerMixin from .packing import PackingMixin diff --git a/src/axolotl/core/trainers/mixins/layer_offloading.py b/src/axolotl/core/trainers/mixins/layer_offloading.py new file mode 100644 index 0000000000..83a9feff58 --- /dev/null +++ b/src/axolotl/core/trainers/mixins/layer_offloading.py @@ -0,0 +1,304 @@ +""" +Trainer mixin for layer-wise parameter offloading to CPU. + +Offloads frozen (non-trainable) parameters in decoder layers to CPU, then uses +forward/backward hooks to stream them on/off GPU one layer at a time with CUDA +stream prefetching. Trainable parameters (e.g. LoRA weights) stay on GPU always. + +Forward: pre-hook loads layer N's frozen params to GPU (prefetches N+1 on + transfer stream), post-hook offloads layer N-1's frozen params. +Backward: same in reverse order. +""" + +import contextlib + +import torch +import torch.nn as nn +from transformers import Trainer + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def _find_decoder_layers(model: nn.Module) -> tuple[nn.ModuleList | None, list[str]]: + """Recursively search the model for the decoder layer ModuleList. + + Finds any ModuleList whose children have 'DecoderLayer' in their class name. + Handles all common HF architectures including VLM wrappers (e.g. Qwen3.5-MoE + where layers are at model.language_model.layers). + """ + # BFS to find the first ModuleList containing decoder layers + queue = [model] + while queue: + m = queue.pop(0) + for _name, child in m.named_children(): + if isinstance(child, nn.ModuleList) and len(child) > 0: + first_type = type(child[0]).__name__ + if "DecoderLayer" in first_type or "TransformerBlock" in first_type: + layer_types = list({type(layer).__name__ for layer in child}) + return child, layer_types + else: + queue.append(child) + + return None, [] + + +def _get_frozen_params(layer: nn.Module) -> list[tuple[str, nn.Parameter]]: + """Get all non-trainable parameters in a layer.""" + return [(n, p) for n, p in layer.named_parameters() if not p.requires_grad] + + +class LayerOffloadManager: + """Manages offloading frozen decoder layer params to CPU and streaming + them back during forward/backward with CUDA stream overlap. + + Only frozen (requires_grad=False) parameters are offloaded. + Trainable parameters (LoRA weights, etc.) remain on GPU at all times. + """ + + def __init__( + self, + model: nn.Module, + num_prefetch: int = 1, + ): + self.model = model + self.num_prefetch = num_prefetch + self._hooks: list = [] + self._device = None + + # Find decoder layers + self.layers, layer_types = _find_decoder_layers(model) + if self.layers is None: + LOG.warning( + "LayerOffloadManager: no decoder layers found, offloading disabled" + ) + self.enabled = False + return + + self.enabled = True + self.n_layers = len(self.layers) + LOG.info( + f"Layer offloading: found {self.n_layers} layers ({', '.join(layer_types)})" + ) + + # Determine GPU device + for p in model.parameters(): + if p.device.type == "cuda": + self._device = p.device + break + if self._device is None: + LOG.warning("LayerOffloadManager: no CUDA parameters found") + self.enabled = False + return + + # Transfer stream for async prefetch + self._transfer_stream = torch.cuda.Stream(device=self._device) + + # Track which layers have their frozen params on GPU + self._on_gpu: set[int] = set(range(self.n_layers)) + + # Cache: frozen param references per layer (list of (name, param) tuples) + self._frozen_params: list[list[tuple[str, nn.Parameter]]] = [ + _get_frozen_params(self.layers[i]) for i in range(self.n_layers) + ] + + # CPU storage: pinned tensors for each layer's frozen params + # Populated on first offload + self._cpu_data: list[dict[str, torch.Tensor]] = [ + {} for _ in range(self.n_layers) + ] + + # Offload all layers upfront + self._offload_all() + + # Release cached memory blocks back to the driver + torch.cuda.empty_cache() + + def _offload_all(self): + """Move all frozen params in all decoder layers to CPU.""" + mem_before = torch.cuda.memory_allocated(self._device) + for i in range(self.n_layers): + self._offload_layer(i) + mem_after = torch.cuda.memory_allocated(self._device) + freed = (mem_before - mem_after) / 1e6 + LOG.info( + f"Layer offloading: offloaded frozen params from {self.n_layers} layers, " + f"freed {freed:.0f} MB GPU memory" + ) + + def _offload_layer(self, idx: int): + """Move frozen params of layer idx to CPU pinned memory.""" + if idx not in self._on_gpu: + return + for name, param in self._frozen_params[idx]: + if param.device.type != "cuda": + continue + # Allocate pinned CPU tensor on first offload + if name not in self._cpu_data[idx]: + self._cpu_data[idx][name] = torch.empty_like( + param.data, device="cpu", pin_memory=True + ) + cpu_buf = self._cpu_data[idx][name] + # Async copy GPU -> CPU (on transfer stream for overlap) + cpu_buf.copy_(param.data, non_blocking=True) + # Point parameter at a dummy CPU tensor to free GPU memory + param.data = cpu_buf + self._on_gpu.discard(idx) + + def _load_layer(self, idx: int, stream=None): + """Move frozen params of layer idx back to GPU.""" + if idx in self._on_gpu or idx < 0 or idx >= self.n_layers: + return + ctx = ( + torch.cuda.stream(stream) + if stream is not None + else contextlib.nullcontext() + ) + with ctx: + for _name, param in self._frozen_params[idx]: + if param.device.type == "cuda": + continue + gpu_data = param.data.to(self._device, non_blocking=True) + param.data = gpu_data + self._on_gpu.add(idx) + + def _prefetch_layer(self, idx: int): + """Async prefetch layer idx on the transfer stream.""" + if idx in self._on_gpu or idx < 0 or idx >= self.n_layers: + return + self._transfer_stream.wait_stream(torch.cuda.default_stream(self._device)) + self._load_layer(idx, stream=self._transfer_stream) + + def _wait_transfer(self): + """Make default stream wait for any in-flight transfers.""" + torch.cuda.default_stream(self._device).wait_stream(self._transfer_stream) + + def setup_hooks(self): + """Register forward and backward hooks on each decoder layer.""" + if not self.enabled: + return + + for idx in range(self.n_layers): + layer = self.layers[idx] + + def make_pre_fwd(i): + def hook(module, args): + # Ensure this layer is on GPU + if i not in self._on_gpu: + self._load_layer(i) + self._wait_transfer() + # Prefetch next layer(s) + for offset in range(1, self.num_prefetch + 1): + self._prefetch_layer(i + offset) + + return hook + + def make_post_fwd(i): + def hook(module, args, output): + # Offload previous layer (no longer needed in forward) + if i > 0: + self._offload_layer(i - 1) + # Offload last layer after forward + if i == self.n_layers - 1: + self._offload_layer(i) + + return hook + + def make_pre_bwd(i): + def hook(module, grad_output): + # Load this layer for backward + if i not in self._on_gpu: + self._load_layer(i) + self._wait_transfer() + # Prefetch previous layer(s) + for offset in range(1, self.num_prefetch + 1): + self._prefetch_layer(i - offset) + + return hook + + def make_post_bwd(i): + def hook(module, grad_input, grad_output): + # Offload the layer above + if i < self.n_layers - 1: + self._offload_layer(i + 1) + # Offload first layer after backward + if i == 0: + self._offload_layer(i) + + return hook + + h1 = layer.register_forward_pre_hook(make_pre_fwd(idx)) + h2 = layer.register_forward_hook(make_post_fwd(idx)) + h3 = layer.register_full_backward_pre_hook(make_pre_bwd(idx)) + h4 = layer.register_full_backward_hook(make_post_bwd(idx)) + self._hooks.extend([h1, h2, h3, h4]) + + def remove_hooks(self): + """Remove all hooks and restore layers to GPU.""" + for h in self._hooks: + h.remove() + self._hooks.clear() + if self.enabled: + for i in range(self.n_layers): + if i not in self._on_gpu: + self._load_layer(i) + + def pre_step(self): + """Called before each training step — ensure layers start offloaded.""" + if not self.enabled: + return + for i in list(self._on_gpu): + self._offload_layer(i) + # Prefetch layer 0 for forward + self._prefetch_layer(0) + + def post_step(self): + """Called after each training step — ensure layers are offloaded.""" + if not self.enabled: + return + for i in list(self._on_gpu): + self._offload_layer(i) + # Prefetch layer 0 for next step + self._prefetch_layer(0) + + +class _LayerOffloadContext: + """Context manager wrapping pre_step / post_step around a training step.""" + + def __init__(self, manager: LayerOffloadManager): + self.manager = manager + + def __enter__(self): + self.manager.pre_step() + return self + + def __exit__(self, *args): + self.manager.post_step() + + +class LayerOffloadingMixin(Trainer): + """ + Trainer mixin class for layer-wise parameter offloading to CPU. + + Offloads frozen decoder layer params to CPU at init, then streams them + on/off GPU one layer at a time during each training step. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if getattr(self.args, "layer_offloading", False): + LOG.info("Layer parameter offloading enabled") + self._layer_offload_manager = LayerOffloadManager( + model=self.model, + num_prefetch=1, + ) + self._layer_offload_manager.setup_hooks() + self._layer_offload_ctx = _LayerOffloadContext(self._layer_offload_manager) + else: + self._layer_offload_manager = None + self._layer_offload_ctx = contextlib.nullcontext() + + def training_step(self, *args, **kwargs): + with self._layer_offload_ctx: + return super().training_step(*args, **kwargs) diff --git a/src/axolotl/core/training_args_base.py b/src/axolotl/core/training_args_base.py index 41ee8e91eb..427a80a468 100644 --- a/src/axolotl/core/training_args_base.py +++ b/src/axolotl/core/training_args_base.py @@ -235,6 +235,13 @@ class AxolotlTrainingMixins: metadata={"help": "Use activation offloading with CUDA streams for training."}, ) + layer_offloading: bool | None = field( + default=None, + metadata={ + "help": "Offload model layer parameters to CPU during forward, prefetch back during backward." + }, + ) + # multi-modal section image_size: int | tuple[int, int] | None = field( diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index a4eadf5cf6..97a9c923ec 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -433,6 +433,12 @@ class AxolotlInputConfig( "description": "Whether to offload activations. Available options are: true, false, 'legacy', 'disk'." }, ) + layer_offloading: bool | None = Field( + default=False, + json_schema_extra={ + "description": "Offload model layer parameters to CPU during forward, prefetch back during backward." + }, + ) unfrozen_parameters: list[str] | None = Field( default=None, From fc3b3d1d4ec77306f5beb42a8b05f75fc035a03d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 21 Mar 2026 22:47:26 -0400 Subject: [PATCH 1216/1405] synthetic datasets for benchmarking and testing (#3518) [skip ci] * synthetic datasets for benchmarking and testing * fix synthetic dataset parse from config and add tests * use type=_synthetic --- src/axolotl/prompt_strategies/_synthetic.py | 96 +++++++++++++++ src/axolotl/utils/config/__init__.py | 15 ++- src/axolotl/utils/data/sft.py | 12 +- src/axolotl/utils/schemas/config.py | 17 ++- src/axolotl/utils/schemas/datasets.py | 40 ++++++- tests/patched/test_validation.py | 50 ++++++++ tests/prompt_strategies/test_synthetic.py | 125 ++++++++++++++++++++ 7 files changed, 347 insertions(+), 8 deletions(-) create mode 100644 src/axolotl/prompt_strategies/_synthetic.py create mode 100644 tests/prompt_strategies/test_synthetic.py diff --git a/src/axolotl/prompt_strategies/_synthetic.py b/src/axolotl/prompt_strategies/_synthetic.py new file mode 100644 index 0000000000..1353f09ed6 --- /dev/null +++ b/src/axolotl/prompt_strategies/_synthetic.py @@ -0,0 +1,96 @@ +""" +Synthetic dataset generator for benchmarking and testing. + +Generates datasets with configurable sequence length, dataset size, and token ID ranges. +Useful for benchmarking memory usage and speed by sequence length, and for validating +weighted dataset mixes. + +YAML configuration example: + + datasets: + - path: synthetic + type: _synthetic + length: 1000 + sequence_length: 2048 + min_input_id: 100 + max_input_id: 32000 + seed: 42 +""" + +from typing import Any, Dict, Optional + +import numpy as np +from datasets import Dataset + +from axolotl.prompt_tokenizers import DatasetWrappingStrategy +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +class SyntheticDatasetStrategy(DatasetWrappingStrategy): + """Strategy that generates synthetic tokenized data, ignoring the source dataset.""" + + def __init__( + self, + sequence_length: int = 2048, + length: int = 1000, + min_input_id: int = 100, + max_input_id: int = 32000, + seed: Optional[int] = None, + ): + self.sequence_length = sequence_length + self.length = length + self.min_input_id = min_input_id + self.max_input_id = max_input_id + self.seed = seed + + def wrap_dataset( + self, + dataset, + process_count: int | None = None, + keep_in_memory: bool | None = False, + **kwargs, + ) -> Dataset: + LOG.info( + f"Generating synthetic dataset: {self.length} samples, " + f"sequence_length={self.sequence_length}, " + f"input_id_range=[{self.min_input_id}, {self.max_input_id})" + ) + + rng = np.random.default_rng(self.seed) + input_ids = rng.integers( + low=self.min_input_id, + high=self.max_input_id, + size=(self.length, self.sequence_length), + ).tolist() + + attention_mask = [[1] * self.sequence_length] * self.length + # labels == input_ids means we train on all tokens + labels = [row[:] for row in input_ids] + + return Dataset.from_dict( + { + "input_ids": input_ids, + "attention_mask": attention_mask, + "labels": labels, + } + ) + + +def load(tokenizer, cfg, ds_cfg: Optional[Dict[str, Any]] = None): + ds_cfg = ds_cfg or {} + + sequence_length = ds_cfg.get("sequence_length", cfg.sequence_len) + length = ds_cfg.get("length", 1000) + min_input_id = ds_cfg.get("min_input_id", 100) + max_input_id = ds_cfg.get("max_input_id", tokenizer.vocab_size) + seed = ds_cfg.get("seed", None) + + return SyntheticDatasetStrategy( + sequence_length=sequence_length, + length=length, + min_input_id=min_input_id, + max_input_id=max_input_id, + seed=seed, + ) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 61096cb869..c5bad62de7 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -22,7 +22,12 @@ AxolotlConfigWCapabilities as AxolotlConfigWCapabilitiesBase, AxolotlInputConfig as AxolotlInputConfigBase, ) -from axolotl.utils.schemas.datasets import DPODataset, KTODataset, SFTDataset +from axolotl.utils.schemas.datasets import ( + DPODataset, + KTODataset, + SFTDataset, + SyntheticDataset, +) LOG = get_logger(__name__) @@ -308,6 +313,14 @@ def validate_config( cfg["datasets"][idx] = DPODataset(**ds_cfg) elif cfg.get("rl") == "kto" and not isinstance(ds_cfg, KTODataset): cfg["datasets"][idx] = KTODataset(**dict(ds_cfg)) + elif ( + ds_cfg.get("type") + if isinstance(ds_cfg, dict) + else getattr(ds_cfg, "type", None) + ) == "_synthetic" and not isinstance(ds_cfg, SyntheticDataset): + cfg["datasets"][idx] = SyntheticDataset( + **(ds_cfg if isinstance(ds_cfg, dict) else dict(ds_cfg)) + ) elif not isinstance(ds_cfg, SFTDataset): cfg["datasets"][idx] = SFTDataset(**dict(ds_cfg)) diff --git a/src/axolotl/utils/data/sft.py b/src/axolotl/utils/data/sft.py index e008b542be..0b2ec2b5fb 100644 --- a/src/axolotl/utils/data/sft.py +++ b/src/axolotl/utils/data/sft.py @@ -376,10 +376,14 @@ def _load_and_process_single_dataset( streaming: bool = False, ) -> tuple[Dataset | IterableDataset, Prompter | None]: """Load and process a single dataset based on the passed config.""" - # Load the dataset - dataset = load_dataset_with_config( - dataset_config, cfg.hf_use_auth_token, streaming=streaming - ) + # For synthetic datasets, create a minimal placeholder instead of loading from path + if dataset_config.type == "_synthetic": + dataset = Dataset.from_dict({"text": [""]}) + else: + # Load the dataset + dataset = load_dataset_with_config( + dataset_config, cfg.hf_use_auth_token, streaming=streaming + ) # Parse dataset type d_base_type, d_prompt_style = _parse_dataset_type(dataset_config.type) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 97a9c923ec..67dea4958b 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -22,6 +22,7 @@ PretrainingDataset, SFTDataset, StepwiseSupervisedDataset, + SyntheticDataset, ) from axolotl.utils.schemas.deprecated import DeprecatedParameters, RemappedParameters from axolotl.utils.schemas.dynamic_checkpoint import DynamicCheckpointConfig @@ -185,7 +186,13 @@ class AxolotlInputConfig( datasets: ( Annotated[ - list[SFTDataset | DPODataset | KTODataset | StepwiseSupervisedDataset], + list[ + SFTDataset + | DPODataset + | KTODataset + | StepwiseSupervisedDataset + | SyntheticDataset + ], MinLen(1), ] | None @@ -198,7 +205,13 @@ class AxolotlInputConfig( test_datasets: ( Annotated[ - list[SFTDataset | DPODataset | KTODataset | StepwiseSupervisedDataset], + list[ + SFTDataset + | DPODataset + | KTODataset + | StepwiseSupervisedDataset + | SyntheticDataset + ], MinLen(1), ] | None diff --git a/src/axolotl/utils/schemas/datasets.py b/src/axolotl/utils/schemas/datasets.py index e324687068..6114a63e0a 100644 --- a/src/axolotl/utils/schemas/datasets.py +++ b/src/axolotl/utils/schemas/datasets.py @@ -296,4 +296,42 @@ class KTODataset(BaseModel): revision: str | None = None -DatasetConfig = SFTDataset | DPODataset | KTODataset | StepwiseSupervisedDataset +class SyntheticDataset(BaseModel): + """Synthetic dataset configuration for benchmarking and testing. + + Generates datasets with configurable sequence length, dataset size, and token ID + ranges. Useful for benchmarking memory usage and speed by sequence length, and for + validating weighted dataset mixes. + """ + + path: Literal["synthetic"] = "synthetic" + type: Literal["_synthetic"] = "_synthetic" + length: int = Field( + default=1000, + json_schema_extra={"description": "Number of rows to generate"}, + ) + sequence_length: int | None = Field( + default=None, + json_schema_extra={ + "description": "Sequence length per row (defaults to sequence_len from config)" + }, + ) + min_input_id: int = Field( + default=100, + json_schema_extra={"description": "Minimum token ID for generation"}, + ) + max_input_id: int | None = Field( + default=None, + json_schema_extra={ + "description": "Maximum token ID for generation (defaults to tokenizer vocab_size)" + }, + ) + seed: int | None = Field( + default=None, + json_schema_extra={"description": "Random seed for reproducibility"}, + ) + + +DatasetConfig = ( + SFTDataset | DPODataset | KTODataset | StepwiseSupervisedDataset | SyntheticDataset +) diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index d22927940d..29ab859c14 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -13,6 +13,7 @@ from axolotl.utils.dict import DictDefault from axolotl.utils.mlflow_ import setup_mlflow_env_vars from axolotl.utils.schemas.config import AxolotlConfigWCapabilities +from axolotl.utils.schemas.datasets import SFTDataset from axolotl.utils.wandb_ import setup_wandb_env_vars warnings.filterwarnings("error") @@ -1731,3 +1732,52 @@ def test_dataloader_auto_defaults(self, minimal_cfg): assert new_cfg.dataloader_num_workers == 8 assert new_cfg.dataloader_pin_memory is True assert new_cfg.dataloader_prefetch_factor == 256 + + +class TestSyntheticDatasetValidation(BaseValidation): + """ + Tests for synthetic dataset config validation + """ + + @staticmethod + def _make_cfg(minimal_cfg, datasets): + raw = dict(minimal_cfg) + raw["datasets"] = datasets + return DictDefault(raw) + + def test_synthetic_dict_config_validates(self, minimal_cfg): + """Synthetic dataset passed as a raw dict should not raise.""" + cfg = self._make_cfg( + minimal_cfg, + [ + { + "path": "synthetic", + "type": "_synthetic", + "length": 100, + "sequence_length": 64, + } + ], + ) + + new_cfg = validate_config(cfg) + assert new_cfg.datasets[0]["path"] == "synthetic" + + def test_synthetic_already_sft_does_not_crash(self, minimal_cfg): + """Synthetic dataset already parsed as SFTDataset should not raise AttributeError.""" + sft = SFTDataset(path="synthetic", type="_synthetic") + cfg = self._make_cfg(minimal_cfg, [sft]) + + # Before the fix, this raised: + # AttributeError: 'SFTDataset' object has no attribute 'get' + new_cfg = validate_config(cfg) + assert new_cfg.datasets[0]["path"] == "synthetic" + + def test_non_synthetic_sft_validates(self, minimal_cfg): + """A regular SFT dataset should validate without being treated as synthetic.""" + cfg = self._make_cfg( + minimal_cfg, + [{"path": "mhenrichsen/alpaca_2k_test", "type": "alpaca"}], + ) + + new_cfg = validate_config(cfg) + assert new_cfg.datasets[0]["path"] == "mhenrichsen/alpaca_2k_test" diff --git a/tests/prompt_strategies/test_synthetic.py b/tests/prompt_strategies/test_synthetic.py new file mode 100644 index 0000000000..6038333c1f --- /dev/null +++ b/tests/prompt_strategies/test_synthetic.py @@ -0,0 +1,125 @@ +"""Tests for the synthetic dataset generator.""" + +import unittest +from unittest.mock import MagicMock + +from datasets import Dataset + +from axolotl.prompt_strategies._synthetic import SyntheticDatasetStrategy, load +from axolotl.utils.dict import DictDefault + + +class TestSyntheticDatasetStrategy(unittest.TestCase): + def test_generates_correct_shape(self): + strategy = SyntheticDatasetStrategy( + sequence_length=128, + length=50, + min_input_id=1, + max_input_id=1000, + seed=42, + ) + dummy = Dataset.from_dict({"text": [""]}) + result = strategy.wrap_dataset(dummy) + + assert len(result) == 50 + assert len(result[0]["input_ids"]) == 128 + assert len(result[0]["attention_mask"]) == 128 + assert len(result[0]["labels"]) == 128 + + def test_attention_mask_all_ones(self): + strategy = SyntheticDatasetStrategy(sequence_length=64, length=10, seed=0) + dummy = Dataset.from_dict({"text": [""]}) + result = strategy.wrap_dataset(dummy) + + for row in result: + assert all(v == 1 for v in row["attention_mask"]) + + def test_labels_equal_input_ids(self): + strategy = SyntheticDatasetStrategy(sequence_length=64, length=10, seed=0) + dummy = Dataset.from_dict({"text": [""]}) + result = strategy.wrap_dataset(dummy) + + for row in result: + assert row["input_ids"] == row["labels"] + + def test_input_id_range(self): + strategy = SyntheticDatasetStrategy( + sequence_length=64, + length=100, + min_input_id=500, + max_input_id=600, + seed=42, + ) + dummy = Dataset.from_dict({"text": [""]}) + result = strategy.wrap_dataset(dummy) + + for row in result: + for token_id in row["input_ids"]: + assert 500 <= token_id < 600 + + def test_seed_reproducibility(self): + kwargs = dict( + sequence_length=64, length=20, min_input_id=1, max_input_id=1000, seed=123 + ) + dummy = Dataset.from_dict({"text": [""]}) + + result1 = SyntheticDatasetStrategy(**kwargs).wrap_dataset(dummy) + result2 = SyntheticDatasetStrategy(**kwargs).wrap_dataset(dummy) + + for r1, r2 in zip(result1, result2, strict=True): + assert r1["input_ids"] == r2["input_ids"] + + def test_different_seeds_differ(self): + common = dict(sequence_length=64, length=20, min_input_id=1, max_input_id=1000) + dummy = Dataset.from_dict({"text": [""]}) + + result1 = SyntheticDatasetStrategy(seed=1, **common).wrap_dataset(dummy) + result2 = SyntheticDatasetStrategy(seed=2, **common).wrap_dataset(dummy) + + any_different = any( + r1["input_ids"] != r2["input_ids"] + for r1, r2 in zip(result1, result2, strict=True) + ) + assert any_different + + def test_load_function_with_ds_cfg(self): + tokenizer = MagicMock() + tokenizer.vocab_size = 32000 + cfg = DictDefault({"sequence_len": 512, "train_on_inputs": False}) + ds_cfg = { + "sequence_length": 256, + "length": 5, + "min_input_id": 10, + "max_input_id": 100, + "seed": 0, + } + + strategy = load(tokenizer, cfg, ds_cfg=ds_cfg) + assert isinstance(strategy, SyntheticDatasetStrategy) + assert strategy.sequence_length == 256 + assert strategy.length == 5 + assert strategy.min_input_id == 10 + assert strategy.max_input_id == 100 + + def test_load_defaults_from_cfg(self): + tokenizer = MagicMock() + tokenizer.vocab_size = 32000 + cfg = DictDefault({"sequence_len": 1024, "train_on_inputs": False}) + + strategy = load(tokenizer, cfg, ds_cfg={}) + assert strategy.sequence_length == 1024 + assert strategy.max_input_id == 32000 + assert strategy.length == 1000 + + def test_load_with_no_ds_cfg(self): + tokenizer = MagicMock() + tokenizer.vocab_size = 50000 + cfg = DictDefault({"sequence_len": 2048, "train_on_inputs": False}) + + strategy = load(tokenizer, cfg) + assert strategy.sequence_length == 2048 + assert strategy.max_input_id == 50000 + + +if __name__ == "__main__": + unittest.main() From 5b2e3f00ce2622c04adccded1a7a9ce44513d80d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 22 Mar 2026 09:11:17 -0400 Subject: [PATCH 1217/1405] fix: handle connection errors when checking user whoami (#3529) --- src/axolotl/cli/checks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/axolotl/cli/checks.py b/src/axolotl/cli/checks.py index 254da8bae4..c79cff9f5d 100644 --- a/src/axolotl/cli/checks.py +++ b/src/axolotl/cli/checks.py @@ -3,6 +3,7 @@ import os from pathlib import Path +import httpcore from accelerate.commands.config import config_args from huggingface_hub import HfApi from huggingface_hub.utils import LocalTokenNotFoundError @@ -47,7 +48,7 @@ def check_user_token() -> bool: "Error verifying HuggingFace token. Remember to log in using `hf auth login` and get your access token from https://huggingface.co/settings/tokens if you want to use gated models or datasets." ) return False - except HTTPError: + except (HTTPError, httpcore.ConnectError): LOG.warning( "Error accessing HuggingFace. This may be due to a network issue or rate limiting." ) From a67392c427f7ad19b50b235f896beb438396a844 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 22 Mar 2026 13:19:21 -0400 Subject: [PATCH 1218/1405] liger support for qwen 3.5 and fused rmsnorm+gated (#3531) [skip ci] * liger support for qwen 3.5 and fused rmsnorm+gated * support for qwen 3.5 moe * fix version ref * fixups for PR code review --- src/axolotl/integrations/liger/args.py | 9 + .../integrations/liger/models/qwen3_5.py | 175 +++++++++ .../integrations/liger/models/qwen3_5_moe.py | 198 +++++++++++ src/axolotl/integrations/liger/plugin.py | 26 ++ src/axolotl/kernels/rms_norm_gated.py | 333 ++++++++++++++++++ tests/kernels/test_rms_norm_gated.py | 229 ++++++++++++ 6 files changed, 970 insertions(+) create mode 100644 src/axolotl/integrations/liger/models/qwen3_5.py create mode 100644 src/axolotl/integrations/liger/models/qwen3_5_moe.py create mode 100644 src/axolotl/kernels/rms_norm_gated.py create mode 100644 tests/kernels/test_rms_norm_gated.py diff --git a/src/axolotl/integrations/liger/args.py b/src/axolotl/integrations/liger/args.py index eb7a6c59be..a5f88ffe2c 100644 --- a/src/axolotl/integrations/liger/args.py +++ b/src/axolotl/integrations/liger/args.py @@ -30,6 +30,15 @@ class LigerArgs(BaseModel): liger_rope: bool | None = None liger_rms_norm: bool | None = None + liger_rms_norm_gated: bool | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Enables fused RMSNorm+SiLU gate Triton kernel for models with " + "gated RMSNorm (e.g. Qwen3.5 / Qwen3.5 MoE linear attention layers)." + ) + }, + ) liger_layer_norm: bool | None = None liger_swiglu: bool | None = None liger_glu_activation: bool | None = None diff --git a/src/axolotl/integrations/liger/models/qwen3_5.py b/src/axolotl/integrations/liger/models/qwen3_5.py new file mode 100644 index 0000000000..ee4b9b1c1c --- /dev/null +++ b/src/axolotl/integrations/liger/models/qwen3_5.py @@ -0,0 +1,175 @@ +""" +Liger FLCE for Qwen3.5. Based on transformers v5.3.0. +""" + +import sys +from copy import deepcopy +from typing import Optional, Union + +import torch +from liger_kernel.transformers.model.loss_utils import LigerForCausalLMLoss +from transformers.cache_utils import Cache +from transformers.modeling_outputs import CausalLMOutputWithPast + + +def lce_forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs, +) -> CausalLMOutputWithPast: + r""" + Args: + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + """ + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs[0] + + logits = None + loss = None + # if in training mode, don't materialize logits + if self.training and (labels is not None): + loss = LigerForCausalLMLoss( + hidden_states=hidden_states, + lm_head_weight=self.lm_head.weight, + labels=labels, + hidden_size=self.config.hidden_size, + **kwargs, + ) + + else: # if in inference mode materialize logits + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + logits = self.lm_head(hidden_states[:, slice_indices, :]) + if labels is not None: + loss = self.loss_function( + logits=logits, + labels=labels, + vocab_size=self.config.vocab_size, + **kwargs, + ) + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +def apply_liger_kernel_to_qwen3_5( + cross_entropy: bool = False, + fused_linear_cross_entropy: bool = False, + rms_norm: bool = False, + rms_norm_gated: bool = False, + glu_activation: bool = False, + layer_norm: bool = False, + **kwargs, +) -> None: + """ + Apply Liger kernels to replace original implementation in HuggingFace Qwen3.5 models. + + Note: Qwen3_5RMSNorm uses zero-init weight with offset 1.0 (like Gemma), + so we use LigerRMSNorm with offset=1.0 and init_fn="zeros". + + Args: + cross_entropy (bool): Whether to apply Liger's cross entropy loss. Default is False. + fused_linear_cross_entropy (bool): + Whether to apply Liger's fused linear cross entropy loss. Default is False. + `cross_entropy` and `fused_linear_cross_entropy` cannot both be True. + If `fused_linear_cross_entropy` is True, the logits will not be materialized but more memory efficient. + rms_norm (bool): Whether to apply Liger's RMSNorm. Default is False. + rms_norm_gated (bool): Whether to apply fused RMSNorm+SiLU gate kernel for + Qwen3_5RMSNormGated (used in linear attention layers). Default is False. + glu_activation (bool): Whether to apply Liger's SwiGLU MLP. Default is False. + layer_norm (bool): Whether to apply Liger's LayerNorm. Default is False. + """ + + import transformers.models.qwen3_5.modeling_qwen3_5 # noqa: F401 + from liger_kernel.transformers.functional import liger_cross_entropy + from liger_kernel.transformers.layer_norm import LigerLayerNorm + from liger_kernel.transformers.rms_norm import LigerRMSNorm + from liger_kernel.transformers.swiglu import LigerSwiGLUMLP + + assert not (cross_entropy and fused_linear_cross_entropy), ( + "cross_entropy and fused_linear_cross_entropy cannot both be True." + ) + + modeling_qwen3_5 = sys.modules["transformers.models.qwen3_5.modeling_qwen3_5"] + + if rms_norm: + # Qwen3_5RMSNorm uses zero-init weight with `output * (1.0 + weight)` pattern + class LigerRMSNormForQwen3_5(LigerRMSNorm): + def __init__(self, dim, eps=1e-6, **kwargs): + super().__init__( + dim, + eps=eps, + offset=1.0, + casting_mode="gemma", + init_fn="zeros", + in_place=False, + ) + + modeling_qwen3_5.Qwen3_5RMSNorm = LigerRMSNormForQwen3_5 + + if rms_norm_gated: + from axolotl.kernels.rms_norm_gated import FusedRMSNormGated + + modeling_qwen3_5.Qwen3_5RMSNormGated = FusedRMSNormGated + + if glu_activation: + + def _liger_swiglu_mlp_wrapper(config, intermediate_size=None, **kwargs): + """Accepts intermediate_size to pass to LigerSwiGLUMLP""" + config = deepcopy(config) + if intermediate_size is not None: + config.intermediate_size = intermediate_size + return LigerSwiGLUMLP(config, **kwargs) + + modeling_qwen3_5.Qwen3_5MLP = _liger_swiglu_mlp_wrapper + + if layer_norm: + modeling_qwen3_5.nn.LayerNorm = LigerLayerNorm + + if cross_entropy: + from transformers.loss.loss_utils import nn + + nn.functional.cross_entropy = liger_cross_entropy + + if fused_linear_cross_entropy: + modeling_qwen3_5.Qwen3_5ForCausalLM.forward = lce_forward diff --git a/src/axolotl/integrations/liger/models/qwen3_5_moe.py b/src/axolotl/integrations/liger/models/qwen3_5_moe.py new file mode 100644 index 0000000000..b10b34ad51 --- /dev/null +++ b/src/axolotl/integrations/liger/models/qwen3_5_moe.py @@ -0,0 +1,198 @@ +""" +Liger FLCE for Qwen3.5 MoE. Based on transformers v5.3.0. +""" + +import sys +from copy import deepcopy +from typing import Optional, Union + +import torch +from liger_kernel.transformers.model.loss_utils import LigerForCausalLMLoss +from transformers.modeling_outputs import MoeCausalLMOutputWithPast + + +def lce_forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values=None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_router_logits: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs, +) -> MoeCausalLMOutputWithPast: + r""" + Args: + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + """ + from transformers.models.qwen3_5_moe.modeling_qwen3_5_moe import ( + load_balancing_loss_func, + ) + + output_router_logits = ( + output_router_logits + if output_router_logits is not None + else self.config.output_router_logits + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_router_logits=output_router_logits, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs[0] + + logits = None + loss = None + # if in training mode, don't materialize logits + if self.training and (labels is not None): + loss = LigerForCausalLMLoss( + hidden_states=hidden_states, + lm_head_weight=self.lm_head.weight, + labels=labels, + hidden_size=self.config.hidden_size, + **kwargs, + ) + + else: # if in inference mode materialize logits + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + logits = self.lm_head(hidden_states[:, slice_indices, :]) + if labels is not None: + loss = self.loss_function( + logits, + labels, + self.vocab_size, + **kwargs, + ) + + aux_loss = None + if output_router_logits: + aux_loss = load_balancing_loss_func( + outputs.router_logits, + self.num_experts, + self.num_experts_per_tok, + attention_mask, + ) + if labels is not None: + loss += self.router_aux_loss_coef * aux_loss.to(loss.device) + + return MoeCausalLMOutputWithPast( + loss=loss, + aux_loss=aux_loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + router_logits=outputs.router_logits, + ) + + +def apply_liger_kernel_to_qwen3_5_moe( + cross_entropy: bool = False, + fused_linear_cross_entropy: bool = False, + rms_norm: bool = False, + rms_norm_gated: bool = False, + glu_activation: bool = False, + layer_norm: bool = False, + **kwargs, +) -> None: + """ + Apply Liger kernels to replace original implementation in HuggingFace Qwen3.5 MoE models. + + Note: Qwen3_5MoeRMSNorm uses zero-init weight with offset 1.0 (like Gemma), + so we use LigerRMSNorm with offset=1.0 and init_fn="zeros". + + Args: + cross_entropy (bool): Whether to apply Liger's cross entropy loss. Default is False. + fused_linear_cross_entropy (bool): + Whether to apply Liger's fused linear cross entropy loss. Default is False. + `cross_entropy` and `fused_linear_cross_entropy` cannot both be True. + If `fused_linear_cross_entropy` is True, the logits will not be materialized but more memory efficient. + rms_norm (bool): Whether to apply Liger's RMSNorm. Default is False. + rms_norm_gated (bool): Whether to apply fused RMSNorm+SiLU gate kernel for + Qwen3_5MoeRMSNormGated (used in linear attention layers). Default is False. + glu_activation (bool): Whether to apply Liger's SwiGLU MLP. Default is False. + layer_norm (bool): Whether to apply Liger's LayerNorm. Default is False. + """ + + import transformers.models.qwen3_5_moe.modeling_qwen3_5_moe # noqa: F401 + from liger_kernel.transformers.functional import liger_cross_entropy + from liger_kernel.transformers.layer_norm import LigerLayerNorm + from liger_kernel.transformers.rms_norm import LigerRMSNorm + from liger_kernel.transformers.swiglu import LigerSwiGLUMLP + + assert not (cross_entropy and fused_linear_cross_entropy), ( + "cross_entropy and fused_linear_cross_entropy cannot both be True." + ) + + modeling_mod = sys.modules["transformers.models.qwen3_5_moe.modeling_qwen3_5_moe"] + + if rms_norm: + # Qwen3_5MoeRMSNorm uses zero-init weight with `output * (1.0 + weight)` pattern + class LigerRMSNormForQwen3_5Moe(LigerRMSNorm): + def __init__(self, dim, eps=1e-6, **kwargs): + super().__init__( + dim, + eps=eps, + offset=1.0, + casting_mode="gemma", + init_fn="zeros", + in_place=False, + ) + + modeling_mod.Qwen3_5MoeRMSNorm = LigerRMSNormForQwen3_5Moe + + if rms_norm_gated: + from axolotl.kernels.rms_norm_gated import FusedRMSNormGated + + modeling_mod.Qwen3_5MoeRMSNormGated = FusedRMSNormGated + + if glu_activation: + + def _liger_swiglu_mlp_wrapper(config, intermediate_size=None, **kwargs): + """Accepts intermediate_size to pass to LigerSwiGLUMLP""" + config = deepcopy(config) + if intermediate_size is not None: + config.intermediate_size = intermediate_size + return LigerSwiGLUMLP(config, **kwargs) + + modeling_mod.Qwen3_5MoeMLP = _liger_swiglu_mlp_wrapper + + if layer_norm: + modeling_mod.nn.LayerNorm = LigerLayerNorm + + if cross_entropy: + from transformers.loss.loss_utils import nn + + nn.functional.cross_entropy = liger_cross_entropy + + if fused_linear_cross_entropy: + modeling_mod.Qwen3_5MoeForCausalLM.forward = lce_forward diff --git a/src/axolotl/integrations/liger/plugin.py b/src/axolotl/integrations/liger/plugin.py index cfd652872a..d561095701 100644 --- a/src/axolotl/integrations/liger/plugin.py +++ b/src/axolotl/integrations/liger/plugin.py @@ -174,6 +174,19 @@ def patched_init(self, *args, **kwargs): rms_norm=cfg.liger_rms_norm, layer_norm=cfg.liger_layer_norm, ) + elif cfg.model_config_type == "qwen3_5": + from axolotl.integrations.liger.models.qwen3_5 import ( + apply_liger_kernel_to_qwen3_5, + ) + + apply_liger_kernel_to_qwen3_5( + cross_entropy=cfg.liger_cross_entropy, + fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, + glu_activation=cfg.liger_glu_activation, + rms_norm=cfg.liger_rms_norm, + rms_norm_gated=getattr(cfg, "liger_rms_norm_gated", False), + layer_norm=cfg.liger_layer_norm, + ) elif cfg.model_config_type == "qwen3_moe": from axolotl.integrations.liger.models.qwen3_moe import ( apply_liger_kernel_to_qwen3_moe, @@ -186,6 +199,19 @@ def patched_init(self, *args, **kwargs): rms_norm=cfg.liger_rms_norm, layer_norm=cfg.liger_layer_norm, ) + elif cfg.model_config_type == "qwen3_5_moe": + from axolotl.integrations.liger.models.qwen3_5_moe import ( + apply_liger_kernel_to_qwen3_5_moe, + ) + + apply_liger_kernel_to_qwen3_5_moe( + cross_entropy=cfg.liger_cross_entropy, + fused_linear_cross_entropy=cfg.liger_fused_linear_cross_entropy, + glu_activation=cfg.liger_glu_activation, + rms_norm=cfg.liger_rms_norm, + rms_norm_gated=getattr(cfg, "liger_rms_norm_gated", False), + layer_norm=cfg.liger_layer_norm, + ) elif cfg.model_config_type == "granitemoe": from liger_kernel.transformers import apply_liger_kernel_to_granite diff --git a/src/axolotl/kernels/rms_norm_gated.py b/src/axolotl/kernels/rms_norm_gated.py new file mode 100644 index 0000000000..6a5ff81bf0 --- /dev/null +++ b/src/axolotl/kernels/rms_norm_gated.py @@ -0,0 +1,333 @@ +""" +Fused RMSNorm + SiLU Gate Triton kernel. + +Computes: Y = (W + offset) * RMSNorm(X) * silu(G) +where RMSNorm(X) = X / sqrt(mean(X^2) + eps) +and silu(G) = G * sigmoid(G) + +Used by Qwen3.5's GatedDeltaNet linear attention layers (Qwen3_5RMSNormGated). +""" + +import math +import operator + +import torch +import triton +import triton.language as tl +from liger_kernel.ops.utils import ( + calculate_settings, + compare_version, + ensure_contiguous, + torch_to_triton_dtype, +) +from liger_kernel.utils import is_npu_available + +if compare_version("triton", operator.ge, "3.0.0") and not is_npu_available(): + try: + from triton.language.extra.libdevice import rsqrt + except ModuleNotFoundError: + from triton.language.extra.cuda.libdevice import rsqrt +else: + from triton.language.math import rsqrt + + +@triton.jit +def _rms_norm_gated_forward_kernel( + Y_ptr, + Y_row_stride, + X_ptr, + X_row_stride, + G_ptr, + G_row_stride, + W_ptr, + W_row_stride, + RSTD_ptr, + RSTD_row_stride, + n_cols, + eps, + offset, + BLOCK_SIZE: tl.constexpr, +): + """ + Y = (W + offset) * (X / RMS(X)) * silu(G) + + All computation done in fp32 (Gemma-style), result cast to input dtype. + """ + row_idx = tl.program_id(0).to(tl.int64) + col_offsets = tl.arange(0, BLOCK_SIZE) + mask = col_offsets < n_cols + + X_row = tl.load(X_ptr + row_idx * X_row_stride + col_offsets, mask=mask, other=0) + G_row = tl.load(G_ptr + row_idx * G_row_stride + col_offsets, mask=mask, other=0) + W_row = tl.load(W_ptr + col_offsets, mask=mask, other=0) + + X_row_dtype = X_row.dtype + + # Cast everything to fp32 + X_fp32 = X_row.to(tl.float32) + G_fp32 = G_row.to(tl.float32) + W_fp32 = W_row.to(tl.float32) + + # RMS norm + mean_sq = tl.sum(X_fp32 * X_fp32, axis=0) / n_cols + rstd = rsqrt(mean_sq + eps) + tl.store(RSTD_ptr + row_idx * RSTD_row_stride, rstd) + + X_norm = X_fp32 * rstd + + # SiLU gate: silu(G) = G * sigmoid(G) + sig_G = tl.sigmoid(G_fp32) + silu_G = G_fp32 * sig_G + + # Fused output + Y_row = (offset + W_fp32) * X_norm * silu_G + + tl.store( + Y_ptr + row_idx * Y_row_stride + col_offsets, + Y_row.to(X_row_dtype), + mask=mask, + ) + + +@triton.jit +def _rms_norm_gated_backward_kernel( + dY_ptr, + dY_row_stride, + dX_ptr, + dX_row_stride, + dG_ptr, + dG_row_stride, + X_ptr, + X_row_stride, + X_dtype: tl.constexpr, + G_ptr, + G_row_stride, + W_ptr, + W_row_stride, + RSTD_ptr, + RSTD_row_stride, + dW_ptr, + dW_row_stride, + n_rows, + n_cols, + offset, + rows_per_program, + BLOCK_SIZE: tl.constexpr, +): + """ + Backward for Y = (W + offset) * (X * RSTD) * silu(G) + + dW = sum_batch(dY * X_norm * silu(G)) + dG = dY * (W + offset) * X_norm * silu'(G) + where silu'(G) = sigmoid(G) * (1 + G * (1 - sigmoid(G))) + dX = RSTD * (m - (1/N) * RSTD^2 * dot(m, X) * X) + where m = dY * (W + offset) * silu(G) + """ + row_block_id = tl.program_id(0).to(tl.int64) + row_start = row_block_id * rows_per_program + row_end = min((row_block_id + 1) * rows_per_program, n_rows) + col_offsets = tl.arange(0, BLOCK_SIZE) + mask = col_offsets < n_cols + + dW_acc = tl.zeros((BLOCK_SIZE,), dtype=tl.float32) + + W_row = tl.load(W_ptr + col_offsets, mask=mask, other=0.0) + W_row = W_row.to(tl.float32) + offset + + for row_idx in range(row_start, row_end): + dY_row = tl.load( + dY_ptr + row_idx * dY_row_stride + col_offsets, mask=mask, other=0.0 + ) + X_row = tl.load( + X_ptr + row_idx * X_row_stride + col_offsets, mask=mask, other=0.0 + ) + G_row = tl.load( + G_ptr + row_idx * G_row_stride + col_offsets, mask=mask, other=0.0 + ) + rstd_row = tl.load(RSTD_ptr + row_idx * RSTD_row_stride) + + # Cast to fp32 + dY_fp32 = dY_row.to(tl.float32) + X_fp32 = X_row.to(tl.float32) + G_fp32 = G_row.to(tl.float32) + + # Recompute intermediates + X_norm = X_fp32 * rstd_row + sig_G = tl.sigmoid(G_fp32) + silu_G = G_fp32 * sig_G + + # dW: accumulate dY * X_norm * silu(G) + dW_acc += dY_fp32 * X_norm * silu_G + + # dG: dY * (W + offset) * X_norm * silu'(G) + # silu'(G) = sigmoid(G) * (1 + G * (1 - sigmoid(G))) + silu_prime_G = sig_G * (1.0 + G_fp32 * (1.0 - sig_G)) + dG_row = dY_fp32 * W_row * X_norm * silu_prime_G + tl.store( + dG_ptr + row_idx * dG_row_stride + col_offsets, + dG_row.to(X_dtype), + mask=mask, + ) + + # dX: standard RMSNorm backward with effective gradient m = dY * W * silu(G) + m = dY_fp32 * W_row * silu_G + dX_row = rstd_row * m + dX_row += rstd_row * ( + -(1.0 / n_cols) * rstd_row * rstd_row * tl.sum(m * X_fp32, axis=0) * X_fp32 + ) + tl.store( + dX_ptr + row_idx * dX_row_stride + col_offsets, + dX_row.to(X_dtype), + mask=mask, + ) + + tl.store( + dW_ptr + row_block_id * dW_row_stride + col_offsets, + dW_acc, + mask=mask, + ) + + +def rms_norm_gated_forward(X, G, W, eps, offset): + shape = X.shape + dim = shape[-1] + X = X.view(-1, dim) + G = G.view(-1, dim) + n_rows, n_cols = X.shape + BLOCK_SIZE, num_warps = calculate_settings(n_cols) + + Y = torch.empty((n_rows, n_cols), dtype=X.dtype, device=X.device) + RSTD = torch.empty(n_rows, dtype=torch.float32, device=X.device) + + assert X.shape[1] == W.shape[0], ( + f"Incompatible hidden size: X.shape[1]={X.shape[1]} vs W.shape[0]={W.shape[0]}" + ) + assert X.shape == G.shape, ( + f"X and G must have same shape, got {X.shape} and {G.shape}" + ) + + _rms_norm_gated_forward_kernel[(n_rows,)]( + Y, + Y.stride(0), + X, + X.stride(0), + G, + G.stride(0), + W, + W.stride(0), + RSTD, + RSTD.stride(0), + n_cols, + eps, + offset, + BLOCK_SIZE=BLOCK_SIZE, + num_warps=num_warps, + ) + return Y.view(*shape), X, G, RSTD, BLOCK_SIZE, num_warps + + +def rms_norm_gated_backward(dY, X, G, W, RSTD, offset, BLOCK_SIZE, num_warps): + shape = dY.shape + dim = shape[-1] + dY = dY.view(-1, dim) + n_rows, n_cols = dY.shape + + sm_count = torch.cuda.get_device_properties(X.device).multi_processor_count + + _dW = torch.empty((sm_count, n_cols), dtype=torch.float32, device=W.device) + dX = torch.empty_like(dY) + dG = torch.empty_like(dY) + + rows_per_program = math.ceil(n_rows / sm_count) + grid = (sm_count,) + + _rms_norm_gated_backward_kernel[grid]( + dY, + dY.stride(0), + dX, + dX.stride(0), + dG, + dG.stride(0), + X, + X.stride(0), + torch_to_triton_dtype[X.dtype], + G, + G.stride(0), + W, + W.stride(0), + RSTD, + RSTD.stride(0), + _dW, + _dW.stride(0), + n_rows, + n_cols, + offset, + rows_per_program, + BLOCK_SIZE=BLOCK_SIZE, + num_warps=num_warps, + ) + + dX = dX.view(*shape) + dG = dG.view(*shape) + dW = _dW.sum(dim=0).to(W.dtype) + return dX, dG, dW + + +class FusedRMSNormGatedFunction(torch.autograd.Function): + @staticmethod + @ensure_contiguous + def forward(ctx, X, G, W, eps, offset=0.0): + """ + X: (B, T, H) or (BxT, H) — input hidden states + G: (B, T, H) or (BxT, H) — gate tensor + W: (H,) — weight parameter + """ + Y, X, G, RSTD, BLOCK_SIZE, num_warps = rms_norm_gated_forward( + X, G, W, eps, offset + ) + ctx.offset = offset + ctx.BLOCK_SIZE = BLOCK_SIZE + ctx.num_warps = num_warps + ctx.save_for_backward(X, G, W, RSTD) + return Y + + @staticmethod + @ensure_contiguous + def backward(ctx, dY): + X, G, W, RSTD = ctx.saved_tensors + dX, dG, dW = rms_norm_gated_backward( + dY, X, G, W, RSTD, ctx.offset, ctx.BLOCK_SIZE, ctx.num_warps + ) + return dX, dG, dW, None, None + + +class FusedRMSNormGated(torch.nn.Module): + """ + Fused RMSNorm + SiLU Gate. + + Computes: Y = W * RMSNorm(X) * silu(G) + + Drop-in replacement for Qwen3_5RMSNormGated with matching + init signature: __init__(hidden_size, eps=1e-6, **kwargs) + and forward signature: forward(hidden_states, gate=None) + """ + + def __init__(self, hidden_size, eps=1e-6, offset=0.0, **kwargs): + super().__init__() + self.weight = torch.nn.Parameter(torch.ones(hidden_size)) + self.variance_epsilon = eps + self.offset = offset + + def forward(self, hidden_states, gate=None): + if gate is None: + raise ValueError("FusedRMSNormGated requires a gate tensor") + if hidden_states.device.type != "cuda": + raise ValueError( + f"FusedRMSNormGated requires CUDA tensors, got device={hidden_states.device}" + ) + return FusedRMSNormGatedFunction.apply( + hidden_states, gate, self.weight, self.variance_epsilon, self.offset + ) + + def extra_repr(self): + return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}" diff --git a/tests/kernels/test_rms_norm_gated.py b/tests/kernels/test_rms_norm_gated.py new file mode 100644 index 0000000000..10b379184d --- /dev/null +++ b/tests/kernels/test_rms_norm_gated.py @@ -0,0 +1,229 @@ +""" +Correctness tests for fused RMSNorm + SiLU Gate kernel. + +Tests against the eager Qwen3_5RMSNormGated implementation. +""" + +import pytest +import torch +import torch.nn.functional as F + +pytest.importorskip("triton", reason="triton required for fused kernels") + +if not torch.cuda.is_available(): + pytest.skip("CUDA required for fused kernel tests", allow_module_level=True) + +from axolotl.kernels.rms_norm_gated import FusedRMSNormGated + + +class EagerRMSNormGated(torch.nn.Module): + """Reference implementation matching Qwen3_5RMSNormGated exactly.""" + + def __init__(self, hidden_size, eps=1e-6): + super().__init__() + self.weight = torch.nn.Parameter(torch.ones(hidden_size)) + self.variance_epsilon = eps + + def forward(self, hidden_states, gate=None): + input_dtype = hidden_states.dtype + hidden_states = hidden_states.to(torch.float32) + variance = hidden_states.pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon) + hidden_states = self.weight * hidden_states.to(input_dtype) + hidden_states = hidden_states * F.silu(gate.to(torch.float32)) + return hidden_states.to(input_dtype) + + +def _sync_weights(eager_mod, fused_mod): + """Copy weights from eager to fused module.""" + fused_mod.weight.data.copy_(eager_mod.weight.data) + + +@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16, torch.float16]) +@pytest.mark.parametrize( + "shape", + [ + (2, 128, 256), + (4, 64, 512), + (1, 32, 1024), + (2, 16, 2560), # Qwen3.5-4B hidden_size + (2, 16, 4096), # Qwen3.5-9B hidden_size + (1, 8, 5120), # Qwen3.5-27B hidden_size + (4, 16, 2048), # Qwen3.5-35B-A3B (MoE) hidden_size + (4, 16, 3072), # Qwen3.5-122B-A10B (MoE) hidden_size + ], +) +class TestRMSNormGatedForward: + def test_output_matches_eager(self, dtype, shape): + torch.manual_seed(42) + B, T, H = shape + X = torch.randn(B, T, H, dtype=dtype, device="cuda") + G = torch.randn(B, T, H, dtype=dtype, device="cuda") + + eager = EagerRMSNormGated(H).to(dtype=dtype, device="cuda") + fused = FusedRMSNormGated(H).to(dtype=dtype, device="cuda") + _sync_weights(eager, fused) + + y_eager = eager(X, gate=G) + y_fused = fused(X, gate=G) + + if dtype == torch.float32: + torch.testing.assert_close(y_fused, y_eager, atol=1e-5, rtol=1e-5) + else: + torch.testing.assert_close(y_fused, y_eager, atol=1e-2, rtol=1e-2) + + def test_output_shape(self, dtype, shape): + B, T, H = shape + X = torch.randn(B, T, H, dtype=dtype, device="cuda") + G = torch.randn(B, T, H, dtype=dtype, device="cuda") + + fused = FusedRMSNormGated(H).to(dtype=dtype, device="cuda") + y = fused(X, gate=G) + assert y.shape == (B, T, H) + assert y.dtype == dtype + + +@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16, torch.float16]) +@pytest.mark.parametrize( + "shape", + [ + (2, 32, 256), + (2, 16, 512), + (2, 16, 2560), # Qwen3.5-4B + (1, 8, 4096), # Qwen3.5-9B + (1, 8, 5120), # Qwen3.5-27B + (2, 16, 2048), # Qwen3.5-35B-A3B (MoE) + (2, 16, 3072), # Qwen3.5-122B-A10B (MoE) + ], +) +class TestRMSNormGatedBackward: + def test_grad_x(self, dtype, shape): + torch.manual_seed(42) + B, T, H = shape + X = torch.randn(B, T, H, dtype=dtype, device="cuda", requires_grad=True) + G = torch.randn(B, T, H, dtype=dtype, device="cuda", requires_grad=True) + X_ref = X.detach().clone().requires_grad_(True) + G_ref = G.detach().clone().requires_grad_(True) + + eager = EagerRMSNormGated(H).to(dtype=dtype, device="cuda") + fused = FusedRMSNormGated(H).to(dtype=dtype, device="cuda") + _sync_weights(eager, fused) + + y_eager = eager(X_ref, gate=G_ref) + y_fused = fused(X, gate=G) + + grad_out = torch.randn_like(y_eager) + y_eager.backward(grad_out) + y_fused.backward(grad_out) + + if dtype == torch.float32: + atol, rtol = 1e-4, 1e-4 + else: + atol, rtol = 5e-2, 5e-2 + + torch.testing.assert_close(X.grad, X_ref.grad, atol=atol, rtol=rtol) + + def test_grad_gate(self, dtype, shape): + torch.manual_seed(42) + B, T, H = shape + X = torch.randn(B, T, H, dtype=dtype, device="cuda", requires_grad=True) + G = torch.randn(B, T, H, dtype=dtype, device="cuda", requires_grad=True) + X_ref = X.detach().clone().requires_grad_(True) + G_ref = G.detach().clone().requires_grad_(True) + + eager = EagerRMSNormGated(H).to(dtype=dtype, device="cuda") + fused = FusedRMSNormGated(H).to(dtype=dtype, device="cuda") + _sync_weights(eager, fused) + + y_eager = eager(X_ref, gate=G_ref) + y_fused = fused(X, gate=G) + + grad_out = torch.randn_like(y_eager) + y_eager.backward(grad_out) + y_fused.backward(grad_out) + + if dtype == torch.float32: + atol, rtol = 1e-4, 1e-4 + else: + atol, rtol = 5e-2, 5e-2 + + torch.testing.assert_close(G.grad, G_ref.grad, atol=atol, rtol=rtol) + + def test_grad_weight(self, dtype, shape): + torch.manual_seed(42) + B, T, H = shape + X = torch.randn(B, T, H, dtype=dtype, device="cuda", requires_grad=True) + G = torch.randn(B, T, H, dtype=dtype, device="cuda", requires_grad=True) + X_ref = X.detach().clone().requires_grad_(True) + G_ref = G.detach().clone().requires_grad_(True) + + eager = EagerRMSNormGated(H).to(dtype=dtype, device="cuda") + fused = FusedRMSNormGated(H).to(dtype=dtype, device="cuda") + _sync_weights(eager, fused) + + y_eager = eager(X_ref, gate=G_ref) + y_fused = fused(X, gate=G) + + grad_out = torch.randn_like(y_eager) + y_eager.backward(grad_out) + y_fused.backward(grad_out) + + if dtype == torch.float32: + atol, rtol = 1e-4, 1e-4 + else: + atol, rtol = 5e-2, 5e-2 + + torch.testing.assert_close( + fused.weight.grad, eager.weight.grad, atol=atol, rtol=rtol + ) + + +class TestRMSNormGatedEdgeCases: + def test_gate_none_raises(self): + fused = FusedRMSNormGated(256).cuda() + X = torch.randn(2, 4, 256, device="cuda") + with pytest.raises(ValueError, match="requires a gate tensor"): + fused(X, gate=None) + + def test_2d_input(self): + """Test with (BxT, H) shaped input instead of (B, T, H).""" + torch.manual_seed(42) + H = 512 + X = torch.randn(64, H, dtype=torch.bfloat16, device="cuda", requires_grad=True) + G = torch.randn(64, H, dtype=torch.bfloat16, device="cuda", requires_grad=True) + X_ref = X.detach().clone().requires_grad_(True) + G_ref = G.detach().clone().requires_grad_(True) + + eager = EagerRMSNormGated(H).to(dtype=torch.bfloat16, device="cuda") + fused = FusedRMSNormGated(H).to(dtype=torch.bfloat16, device="cuda") + _sync_weights(eager, fused) + + y_eager = eager(X_ref, gate=G_ref) + y_fused = fused(X, gate=G) + + torch.testing.assert_close(y_fused, y_eager, atol=1e-2, rtol=1e-2) + + grad_out = torch.randn_like(y_eager) + y_eager.backward(grad_out) + y_fused.backward(grad_out) + + torch.testing.assert_close(X.grad, X_ref.grad, atol=5e-2, rtol=5e-2) + torch.testing.assert_close(G.grad, G_ref.grad, atol=5e-2, rtol=5e-2) + + def test_random_weight_init(self): + """Test with non-default weight values.""" + torch.manual_seed(123) + H = 256 + X = torch.randn(2, 16, H, dtype=torch.bfloat16, device="cuda") + G = torch.randn(2, 16, H, dtype=torch.bfloat16, device="cuda") + + eager = EagerRMSNormGated(H).to(dtype=torch.bfloat16, device="cuda") + # Randomize weights + eager.weight.data = torch.randn_like(eager.weight.data) + + fused = FusedRMSNormGated(H).to(dtype=torch.bfloat16, device="cuda") + _sync_weights(eager, fused) + + y_eager = eager(X, gate=G) + y_fused = fused(X, gate=G) + torch.testing.assert_close(y_fused, y_eager, atol=1e-2, rtol=1e-2) From b3289fd190c99cf08a35d2d71efffe92e0e7c440 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 22 Mar 2026 13:53:19 -0400 Subject: [PATCH 1219/1405] feat: LoRA kernel support for bias, dropout, dora, embeddings (#3528) [skip ci] * feat: LoRA kernel support for bias, dropout, dora, embeddings * chore: lint * chore: lint * address PR feedback, add regression tests, add fsdp2 tests for lora kernels * update tests for new sigs * update tests now that bias and dropout are supported --- src/axolotl/kernels/dora.py | 147 ++ src/axolotl/kernels/lora.py | 1600 +++++++++++++---- src/axolotl/kernels/quantize.py | 4 + src/axolotl/monkeypatch/lora_kernels.py | 75 +- src/axolotl/utils/schemas/config.py | 21 +- src/axolotl/utils/schemas/validation.py | 10 +- tests/core/test_async_grpo.py | 4 +- tests/e2e/kernels/test_lora.py | 6 +- tests/e2e/kernels/test_lora_features.py | 1245 +++++++++++++ tests/e2e/multigpu/test_fsdp2_lora_kernels.py | 120 ++ .../lora_kernels/test_lora_kernel_patching.py | 38 +- .../utils/lora/test_config_validation_lora.py | 30 +- tests/utils/lora/test_freeze_lora.py | 25 +- 13 files changed, 2862 insertions(+), 463 deletions(-) create mode 100644 src/axolotl/kernels/dora.py create mode 100644 tests/e2e/kernels/test_lora_features.py create mode 100644 tests/e2e/multigpu/test_fsdp2_lora_kernels.py diff --git a/src/axolotl/kernels/dora.py b/src/axolotl/kernels/dora.py new file mode 100644 index 0000000000..3ed35cf740 --- /dev/null +++ b/src/axolotl/kernels/dora.py @@ -0,0 +1,147 @@ +""" +Triton kernels for DoRA (Weight-Decomposed Low-Rank Adaptation). + +Fuses the weight norm computation and magnitude scaling to avoid +materializing the full [out_features, in_features] combined weight matrix. +The B@A product is computed row-by-row inside the kernel. +""" + +import torch +import triton +import triton.language as tl + +from .quantize import dequantize + + +@triton.jit +def _dora_fused_norm_kernel( + # Pointers + W_ptr, # base weight [out, in] (dequantized, row-major) + B_ptr, # LoRA B [out, rank] (row-major) + A_ptr, # LoRA A [rank, in] (row-major) + mag_ptr, # magnitude vector [out] + out_ptr, # output mag_norm_scale [out] + # Shapes + out_features, + in_features, + rank, + # Scaling + lora_scale, # float scaling factor + # Block sizes + BLOCK_IN: tl.constexpr, + BLOCK_R: tl.constexpr, # >= rank, power of 2 +): + """Compute mag_norm_scale[i] = magnitude[i] / ||W[i,:] + s * (B[i,:] @ A)[:] ||_2 + + Each program handles one output row. B[row,:] is loaded once (small), + then we tile over in_features computing the dot product with A[:,tile] + and accumulating the squared norm. + + This avoids materializing the full [out, in] B@A matrix. + """ + row = tl.program_id(0) + if row >= out_features: + return + + # Accumulate squared norm across tiles of in_features + norm_sq_acc = tl.zeros([BLOCK_IN], dtype=tl.float32) + + for start in range(0, in_features, BLOCK_IN): + cols = start + tl.arange(0, BLOCK_IN) + col_mask = cols < in_features + + # Load W[row, cols] + w_vals = tl.load( + W_ptr + row * in_features + cols, + mask=col_mask, + other=0.0, + ).to(tl.float32) + + # Compute (B[row,:] @ A[:, cols]) for this tile + # Load B[row, r] as scalar and A[r, cols] as vector for each r + ba_vals = tl.zeros([BLOCK_IN], dtype=tl.float32) + for r in tl.static_range(BLOCK_R): + # Load scalar B[row, r] + b_val = tl.load( + B_ptr + row * rank + r, + mask=(r < rank), + other=0.0, + ).to(tl.float32) + # Load vector A[r, cols] + a_vals = tl.load( + A_ptr + r * in_features + cols, + mask=(col_mask & (r < rank)), + other=0.0, + ).to(tl.float32) + ba_vals += b_val * a_vals + + # Combined: W + s * (B @ A) + combined = w_vals + lora_scale * ba_vals + + # Accumulate squared values + norm_sq_acc += tl.where(col_mask, combined * combined, 0.0) + + # Reduce to scalar norm + norm_sq = tl.sum(norm_sq_acc, axis=0) + norm = tl.sqrt(norm_sq + 1e-12) # epsilon for numerical stability + + # Load magnitude and compute scale + mag = tl.load(mag_ptr + row).to(tl.float32) + scale = mag / norm + + tl.store(out_ptr + row, scale) + + +def triton_dora_scale( + W: torch.Tensor, + W_quant, + A: torch.Tensor, + B: torch.Tensor, + s: float, + magnitude: torch.Tensor, + dtype: torch.dtype, +) -> torch.Tensor: + """Compute DoRA mag_norm_scale using fused Triton kernel. + + Computes B@A row-by-row inside the kernel, avoiding the full + [out_features, in_features] materialization. + + Args: + W: base weight [out, in] (possibly quantized) + W_quant: quantization state + A: LoRA A [rank, in] + B: LoRA B [out, rank] + s: LoRA scaling factor + magnitude: learned magnitude [out] + dtype: compute dtype + + Returns: + mag_norm_scale: [out] tensor = magnitude / ||W + s * B @ A||_2 + """ + # Dequantize W to [out, in] + W_full = dequantize(W.t(), W_quant).t().contiguous().to(dtype) + + out_features, in_features = W_full.shape + rank = A.shape[0] + + out = torch.empty(out_features, dtype=dtype, device=W.device) + + # Block sizes + BLOCK_IN = triton.next_power_of_2(min(in_features, 2048)) + BLOCK_R = triton.next_power_of_2(rank) + + _dora_fused_norm_kernel[(out_features,)]( + W_full, + B.contiguous().to(dtype), + A.contiguous().to(dtype), + magnitude.contiguous(), + out, + out_features=out_features, + in_features=in_features, + rank=rank, + lora_scale=s, + BLOCK_IN=BLOCK_IN, + BLOCK_R=BLOCK_R, + ) + + return out.detach() diff --git a/src/axolotl/kernels/lora.py b/src/axolotl/kernels/lora.py index 9dc66a918f..1576a10cda 100644 --- a/src/axolotl/kernels/lora.py +++ b/src/axolotl/kernels/lora.py @@ -4,6 +4,9 @@ See "LoRA: Low-Rank Adaptation of Large Language Models" (https://arxiv.org/abs/2106.09685). +Also supports DoRA (Weight-Decomposed Low-Rank Adaptation): +See "DoRA: Weight-Decomposed Low-Rank Adaptation" (https://arxiv.org/abs/2402.09353). + Credit to `unsloth` (https://unsloth.ai/) for inspiration for this implementation. """ @@ -29,6 +32,9 @@ def get_lora_parameters( torch.Tensor | None, torch.Tensor | None, float | None, + torch.Tensor | None, + nn.Module | None, + torch.Tensor | None, ]: """ Gets LoRA parameters from a projection module. @@ -37,9 +43,16 @@ def get_lora_parameters( proj: The projection module to extract parameters from. Returns: - A tuple containing the base weights, quantization state, LoRA A and B weights, - scaling factor, and base layer bias. Quant state, weights, and bias may be - `None` if not available. + A tuple containing: + - W: base weight tensor + - b: base layer bias (or None) + - quant_state: quantization state (or None) + - A: LoRA A weight (or None) + - B: LoRA B weight (or None) + - s: LoRA scaling factor (or None) + - lora_bias: LoRA B bias (or None) + - dropout: dropout module (or None) + - magnitude: DoRA magnitude vector (or None) """ # For DPO or disabled adapters base_layer = proj.base_layer if hasattr(proj, "base_layer") else proj @@ -50,7 +63,7 @@ def get_lora_parameters( quant_state = getattr(W, "quant_state", None) if quant_state is None and W.dtype == torch.float8_e4m3fn: quant_state = getattr(base_layer, "weight_scale_inv", None) - return W, b, quant_state, None, None, None + return W, b, quant_state, None, None, None, None, None, None quant_state = getattr(W, "quant_state", None) if quant_state is None and W.dtype == torch.float8_e4m3fn: @@ -78,7 +91,136 @@ def get_lora_parameters( B = linear_B.weight s = proj.scaling[active_adapter] - return W, b, quant_state, A, B, s + # LoRA bias from lora_B (when bias="lora_only" or bias="all") + lora_bias = linear_B.bias # None if bias=False + + # Dropout module + dropout = None + if hasattr(proj, "lora_dropout") and active_adapter in proj.lora_dropout: + dropout = proj.lora_dropout[active_adapter] + + # DoRA magnitude vector + magnitude = None + if ( + hasattr(proj, "lora_magnitude_vector") + and proj.lora_magnitude_vector + and active_adapter in proj.lora_magnitude_vector + ): + mag_layer = proj.lora_magnitude_vector[active_adapter] + magnitude = mag_layer.weight + # FSDP2 DTensor unshard for magnitude vector + if isinstance(magnitude, DTensor): + magnitude = magnitude.full_tensor() + + return W, b, quant_state, A, B, s, lora_bias, dropout, magnitude + + +def _apply_dropout( + dropout: nn.Module | None, X: torch.Tensor, training: bool +) -> torch.Tensor | None: + """Apply dropout to X if dropout module exists and is active. + + Returns X_drop (different tensor) or None if no dropout needed. + """ + if dropout is None or isinstance(dropout, nn.Identity) or not training: + return None + return dropout(X) + + +_USE_TRITON_DORA: bool | None = None + + +def _should_use_triton_dora() -> bool: + """Check if Triton DoRA kernel is available.""" + global _USE_TRITON_DORA + if _USE_TRITON_DORA is None: + try: + from .dora import triton_dora_scale # noqa: F401 + + _USE_TRITON_DORA = True + except (ImportError, RuntimeError): + _USE_TRITON_DORA = False + return _USE_TRITON_DORA + + +def _compute_dora_scale( + W: torch.Tensor, + W_quant: QuantState | torch.Tensor | None, + A: torch.Tensor, + B: torch.Tensor, + s: float, + magnitude: torch.Tensor, + dtype: torch.dtype, +) -> torch.Tensor: + """Compute DoRA magnitude/norm scaling factor with optional caching. + + Uses Triton kernel when available for better performance. + Caches weight_norm on the magnitude tensor. Cache invalidated when + LoRA A/B data changes (after optimizer step). + + Returns: + mag_norm_scale: [out_features] tensor = magnitude / ||W + s * B @ A||_2 + """ + # Check cache on magnitude tensor (avoids expensive norm recomputation) + # Use tensor._version which increments on any in-place modification + # (data_ptr doesn't change when optimizers update params in-place) + cache = getattr(magnitude, "_dora_cache", None) + if cache is not None: + cached_a_ver, cached_b_ver, cached_norm = cache + if cached_a_ver == A._version and cached_b_ver == B._version: + return magnitude.to(dtype) / cached_norm + + # Full recomputation - try Triton first + if _should_use_triton_dora() and W.is_cuda: + from .dora import triton_dora_scale + + result = triton_dora_scale(W, W_quant, A, B, s, magnitude, dtype) + weight_norm = (magnitude.to(dtype) / result).detach() + magnitude._dora_cache = (A._version, B._version, weight_norm) + return result + + # PyTorch fallback + W_full = dequantize(W.t(), W_quant).t().to(dtype) # [out, in] + lora_weight = B.to(dtype) @ A.to(dtype) + combined = W_full + s * lora_weight + weight_norm = torch.linalg.norm(combined, dim=1).to(dtype) + weight_norm = weight_norm.detach() + + magnitude._dora_cache = (A._version, B._version, weight_norm) + + return magnitude.to(dtype) / weight_norm + + +def _compute_dora_scale_cached( + proj: nn.Module, + W: torch.Tensor, + W_quant: QuantState | torch.Tensor | None, + A: torch.Tensor, + B: torch.Tensor, + s: float, + magnitude: torch.Tensor, + dtype: torch.dtype, +) -> torch.Tensor: + """Compute DoRA scale with caching. Recomputes only when LoRA params change. + + Caches the weight norm on the projection module. The cache is invalidated + when LoRA A/B data pointers change (indicating an optimizer step occurred). + """ + cache = getattr(proj, "_dora_norm_cache", None) + if cache is not None: + cached_a_ver, cached_b_ver, cached_norm = cache + if cached_a_ver == A._version and cached_b_ver == B._version: + return magnitude.to(dtype) / cached_norm + + # Cache miss - full recomputation + W_full = dequantize(W.t(), W_quant).t().to(dtype) + lora_weight = B.to(dtype) @ A.to(dtype) + combined = W_full + s * lora_weight + weight_norm = torch.linalg.norm(combined, dim=1).to(dtype).detach() + + proj._dora_norm_cache = (A._version, B._version, weight_norm) + + return magnitude.to(dtype) / weight_norm def matmul_lora( @@ -90,6 +232,8 @@ def matmul_lora( B: torch.Tensor | None, s: float | None, out: torch.Tensor | None = None, + X_drop: torch.Tensor | None = None, + lora_bias: torch.Tensor | None = None, ) -> torch.Tensor: """ Efficient fused matmul + LoRA computation. @@ -102,9 +246,11 @@ def matmul_lora( B: LoRA B matrix [out_features, rank] s: LoRA scaling factor out: Optional output tensor for inplace operations + X_drop: Optional dropout-applied input for LoRA path (if None, uses X) + lora_bias: Optional LoRA B layer bias [out_features] Returns: - Result of X @ W + X @ A @ B + Result of X @ W + s * X_drop @ A @ B + b + s * lora_bias """ dtype = X.dtype W = dequantize(W.t(), W_quant) @@ -113,6 +259,8 @@ def matmul_lora( if X.dim() == 3: batch, seq_len, _ = X.shape X = X.view(-1, X.shape[-1]) + if X_drop is not None: + X_drop = X_drop.view(-1, X_drop.shape[-1]) reshape = True out = torch.matmul(X, W, out=out) @@ -120,8 +268,11 @@ def matmul_lora( del W if A is not None: + X_lora = X_drop if X_drop is not None else X A, B = A.t().to(dtype), B.t().to(dtype) # type: ignore[union-attr] - out += s * X @ A @ B + out += s * X_lora @ A @ B + if lora_bias is not None: + out += s * lora_bias if b is not None: out += b @@ -130,87 +281,200 @@ def matmul_lora( class LoRA_MLP(torch.autograd.Function): - """Optimized LoRA MLP implementation.""" + """Optimized LoRA MLP implementation. + + Supports bias, dropout, and DoRA. Dropout is applied to the input for + gate/up projections. The down projection uses hidden states (post-activation) + as input, so dropout is not applied there. + """ @staticmethod @torch_amp_custom_fwd def forward( ctx, X: torch.Tensor, + X_drop: torch.Tensor | None, + # Gate params gate_weight: torch.Tensor, gate_bias: torch.Tensor | None, gate_quant: QuantState | None, gate_A: torch.Tensor | None, gate_B: torch.Tensor | None, gate_scale: float, + gate_lora_bias: torch.Tensor | None, + gate_magnitude: torch.Tensor | None, + # Up params up_weight: torch.Tensor, up_bias: torch.Tensor | None, up_quant: QuantState | None, up_A: torch.Tensor | None, up_B: torch.Tensor | None, up_scale: float, + up_lora_bias: torch.Tensor | None, + up_magnitude: torch.Tensor | None, + # Down params down_weight: torch.Tensor, down_bias: torch.Tensor | None, down_quant: QuantState | None, down_A: torch.Tensor | None, down_B: torch.Tensor | None, down_scale: float, + down_lora_bias: torch.Tensor | None, + down_magnitude: torch.Tensor | None, + # Activation and flags activation_fn: Callable, activation_fn_backward: Callable, inplace: bool | None = True, ) -> torch.Tensor: - """ - Forward pass for LoRA MLP. - - Args: - ctx: Autograd context - X: Input features - gate_weight: Gate projection weight - gate_bias: Gate projection bias - gate_quant: Gate quantization state - gate_A: Gate LoRA A matrix - gate_B: Gate LoRA B matrix - gate_scale: Gate LoRA scale - up_weight: Up projection weight - up_quant: Up projection quantization state - up_A: Up projection LoRA A matrix - up_B: Up projection LoRA B matrix - up_scale: Up projection LoRA scale - down_weight: Down projection weight - down_bias: Down projection bias - down_quant: Down projection quantization state - down_A: Down projection LoRA A matrix - down_B: Down projection LoRA B matrix - down_scale: Down projection LoRA scale - activation_fn: Forward activation function - activation_fn_backward: Backward activation function - inplace: Whether to perform operations in-place - - Returns: - Output transformed by multi-layer perceptron and activation function - """ - # Compute projections - gate = matmul_lora( - X, gate_weight, gate_bias, gate_quant, gate_A, gate_B, gate_scale - ) - up = matmul_lora(X, up_weight, up_bias, up_quant, up_A, up_B, up_scale) + has_dropout = X_drop is not None + has_dora = gate_magnitude is not None + dtype = X.dtype + X_lora = X_drop if has_dropout else X + + if has_dora: + # Gate with DoRA + gate_base = matmul_lora(X, gate_weight, None, gate_quant, None, None, None) + gate_lora = _lora_only( + X_lora, gate_A, gate_B, gate_scale, gate_lora_bias, dtype + ) + gate_mag_scale = _compute_dora_scale( + gate_weight, + gate_quant, + gate_A, + gate_B, + gate_scale, + gate_magnitude, + dtype, + ) + gate = gate_mag_scale.unsqueeze(0) * (gate_base + gate_lora) + if gate_bias is not None: + gate = gate + gate_bias + + # Up with DoRA + up_base = matmul_lora(X, up_weight, None, up_quant, None, None, None) + up_lora = _lora_only(X_lora, up_A, up_B, up_scale, up_lora_bias, dtype) + up_mag_scale = _compute_dora_scale( + up_weight, up_quant, up_A, up_B, up_scale, up_magnitude, dtype + ) + up = up_mag_scale.unsqueeze(0) * (up_base + up_lora) + if up_bias is not None: + up = up + up_bias + + gate_combined = gate_base + gate_lora + up_combined = up_base + up_lora + else: + gate = matmul_lora( + X, + gate_weight, + gate_bias, + gate_quant, + gate_A, + gate_B, + gate_scale, + X_drop=X_drop, + lora_bias=gate_lora_bias, + ) + up = matmul_lora( + X, + up_weight, + up_bias, + up_quant, + up_A, + up_B, + up_scale, + X_drop=X_drop, + lora_bias=up_lora_bias, + ) # Activation hidden = activation_fn(gate, up) - # Down projection - output = matmul_lora( - hidden, down_weight, down_bias, down_quant, down_A, down_B, down_scale - ) + # Down projection (no dropout on hidden - it's an intermediate) + if has_dora: + down_base = matmul_lora( + hidden, down_weight, None, down_quant, None, None, None + ) + down_lora = _lora_only( + hidden, down_A, down_B, down_scale, down_lora_bias, dtype + ) + down_mag_scale = _compute_dora_scale( + down_weight, + down_quant, + down_A, + down_B, + down_scale, + down_magnitude, + dtype, + ) + down_combined = down_base + down_lora + output = down_mag_scale.unsqueeze(0) * down_combined + if down_bias is not None: + output = output + down_bias + else: + output = matmul_lora( + hidden, + down_weight, + down_bias, + down_quant, + down_A, + down_B, + down_scale, + lora_bias=down_lora_bias, + ) # Save for backward - ctx.save_for_backward(X, gate, up, gate_A, gate_B, up_A, up_B, down_A, down_B) + if has_dora: + ctx.save_for_backward( + X, + X_drop if has_dropout else X, + gate, + up, + gate_A.to(dtype) if gate_A is not None else gate_A, + gate_B.to(dtype) if gate_B is not None else gate_B, + up_A.to(dtype) if up_A is not None else up_A, + up_B.to(dtype) if up_B is not None else up_B, + down_A.to(dtype) if down_A is not None else down_A, + down_B.to(dtype) if down_B is not None else down_B, + gate_magnitude, + up_magnitude, + down_magnitude, + gate_mag_scale, + up_mag_scale, + down_mag_scale, + gate_combined, + up_combined, + down_combined, + gate_lora_bias, + up_lora_bias, + down_lora_bias, + ) + else: + # Pre-convert LoRA matrices to compute dtype for backward + dtype = X.dtype + ctx.save_for_backward( + X, + X_drop if has_dropout else X, + gate, + up, + gate_A.to(dtype) if gate_A is not None else gate_A, + gate_B.to(dtype) if gate_B is not None else gate_B, + up_A.to(dtype) if up_A is not None else up_A, + up_B.to(dtype) if up_B is not None else up_B, + down_A.to(dtype) if down_A is not None else down_A, + down_B.to(dtype) if down_B is not None else down_B, + gate_lora_bias, + up_lora_bias, + down_lora_bias, + ) + ctx.scales = (gate_scale, up_scale, down_scale) ctx.quants = (gate_quant, up_quant, down_quant) ctx.weights = (gate_weight, up_weight, down_weight) ctx.activation_fn = activation_fn ctx.activation_fn_backward = activation_fn_backward ctx.inplace = inplace + ctx.has_dropout = has_dropout + ctx.has_dora = has_dora return output @@ -219,171 +483,225 @@ def forward( def backward( ctx: torch.autograd.function.FunctionCtx, grad_output: torch.Tensor, - ) -> tuple[ - torch.Tensor | None, - None, - None, - None, - torch.Tensor | None, - torch.Tensor | None, - None, - None, - None, - None, - torch.Tensor | None, - torch.Tensor | None, - None, - None, - None, - None, - torch.Tensor | None, - torch.Tensor | None, - None, - None, - None, - None, - None, - ]: - """ - Performs backward pass computation for LoRA MLP. - - Args: - ctx: Context object storing tensors saved during forward pass - grad_output: Gradient of loss with respect to layer output - - Returns: - Tuple containing gradients for all inputs from forward pass: - - Input gradient tensor (or `None`) - - `None` for weights/biases/quantization states - - LoRA A/B matrix gradients (or `None`) - - `None` for scaling factors - - `None` for activation functions and flags - """ - ( - X, - gate, - up, - gate_A, - gate_B, - up_A, - up_B, - down_A, - down_B, - ) = ctx.saved_tensors + ): gate_scale, up_scale, down_scale = ctx.scales gate_quant, up_quant, down_quant = ctx.quants gate_weight, up_weight, down_weight = ctx.weights + has_dropout = ctx.has_dropout + has_dora = ctx.has_dora + + if has_dora: + ( + X, + X_lora, + gate, + up, + gate_A, + gate_B, + up_A, + up_B, + down_A, + down_B, + gate_magnitude, + up_magnitude, + down_magnitude, + gate_mag_scale, + up_mag_scale, + down_mag_scale, + gate_combined, + up_combined, + down_combined, + gate_lora_bias, + up_lora_bias, + down_lora_bias, + ) = ctx.saved_tensors + else: + ( + X, + X_lora, + gate, + up, + gate_A, + gate_B, + up_A, + up_B, + down_A, + down_B, + gate_lora_bias, + up_lora_bias, + down_lora_bias, + ) = ctx.saved_tensors + gate_magnitude = up_magnitude = down_magnitude = None + gate_mag_scale = up_mag_scale = down_mag_scale = None + gate_combined = up_combined = down_combined = None # Transpose all LoRA matrices - gate_A, gate_B = ( - gate_A.t() if gate_A is not None else None, - gate_B.t() if gate_B is not None else None, - ) - up_A, up_B = ( - up_A.t() if up_A is not None else None, - up_B.t() if up_B is not None else None, - ) - down_A, down_B = ( - down_A.t() if down_A is not None else None, - down_B.t() if down_B is not None else None, - ) + gate_A_t = gate_A.t() if gate_A is not None else None + gate_B_t = gate_B.t() if gate_B is not None else None + up_A_t = up_A.t() if up_A is not None else None + up_B_t = up_B.t() if up_B is not None else None + down_A_t = down_A.t() if down_A is not None else None + down_B_t = down_B.t() if down_B is not None else None # Reshape inputs batch, seq_len, hd = X.shape grad_output = grad_output.view(-1, grad_output.shape[-1]) X = X.view(-1, X.shape[-1]) + X_lora = X_lora.view(-1, X_lora.shape[-1]) gate = gate.view(-1, gate.shape[-1]) up = up.view(-1, up.shape[-1]) - dtype = X.dtype - # Down projection + # DoRA magnitude gradients for down projection + d_gate_mag = d_up_mag = d_down_mag = None + d_gate_lora_bias = d_up_lora_bias = d_down_lora_bias = None + + if has_dora: + down_combined_flat = down_combined.view(-1, down_combined.shape[-1]) + d_down_mag = ( + (grad_output * down_combined_flat).sum(dim=0) + * down_mag_scale + / down_magnitude + ) + grad_output = grad_output * down_mag_scale.unsqueeze(0) + + # Down lora bias gradient + if down_lora_bias is not None: + d_down_lora_bias = down_scale * grad_output.sum(dim=0) + + # Down projection backward grad_down = matmul_lora( grad_output, down_weight.t(), None, down_quant, - down_B, - down_A, + down_B_t, + down_A_t, down_scale, ) # Activation backward h, grad_gate, grad_up = ctx.activation_fn_backward(grad_down, gate, up) - # Initialize and compute LoRA gradients + # DoRA magnitude gradients for gate and up + if has_dora: + gate_combined_flat = gate_combined.view(-1, gate_combined.shape[-1]) + up_combined_flat = up_combined.view(-1, up_combined.shape[-1]) + d_gate_mag = ( + (grad_gate * gate_combined_flat).sum(dim=0) + * gate_mag_scale + / gate_magnitude + ) + d_up_mag = ( + (grad_up * up_combined_flat).sum(dim=0) * up_mag_scale / up_magnitude + ) + grad_gate = grad_gate * gate_mag_scale.unsqueeze(0) + grad_up = grad_up * up_mag_scale.unsqueeze(0) + + # LoRA bias gradients for gate and up + if gate_lora_bias is not None: + d_gate_lora_bias = gate_scale * grad_gate.sum(dim=0) + if up_lora_bias is not None: + d_up_lora_bias = up_scale * grad_up.sum(dim=0) + + # LoRA parameter gradients (already in compute dtype from forward) + # Compute grad @ B once per projection, reuse for dA and dX_lora + # Note: _t suffix means transposed from saved shape (A_t = A.t(), etc.) d_down_A = d_down_B = d_up_A = d_up_B = d_gate_A = d_gate_B = None - - if down_A is not None and down_B is not None: - d_down_A = h.t() @ (grad_output @ down_B.t()) - d_down_B = (down_A.t() @ h.t()) @ grad_output - d_down_A *= down_scale - d_down_B *= down_scale - - if up_A is not None and up_B is not None: - d_up_A = X.t() @ (grad_up @ up_B.t()) - d_up_B = (up_A.t() @ X.t()) @ grad_up - d_up_A *= up_scale - d_up_B *= up_scale - - if gate_A is not None and gate_B is not None: - d_gate_A = X.t() @ (grad_gate @ gate_B.t()) - d_gate_B = (gate_A.t() @ X.t()) @ grad_gate - d_gate_A *= gate_scale - d_gate_B *= gate_scale + grad_B_up = grad_B_gate = None + + if down_A_t is not None and down_B_t is not None: + grad_B_down = grad_output @ down_B_t.t() # reused in matmul_lora above too + d_down_A = torch.empty_like(down_A_t) + d_down_B = torch.empty_like(down_B_t) + d_down_A.addmm_(h.t(), grad_B_down, alpha=down_scale, beta=0) + d_down_B.addmm_(down_A_t.t() @ h.t(), grad_output, alpha=down_scale, beta=0) + + if up_A_t is not None and up_B_t is not None: + grad_B_up = grad_up @ up_B_t.t() # [T, rank] — reuse for dX + d_up_A = torch.empty_like(up_A_t) + d_up_B = torch.empty_like(up_B_t) + d_up_A.addmm_(X_lora.t(), grad_B_up, alpha=up_scale, beta=0) + d_up_B.addmm_(up_A_t.t() @ X_lora.t(), grad_up, alpha=up_scale, beta=0) + + if gate_A_t is not None and gate_B_t is not None: + grad_B_gate = grad_gate @ gate_B_t.t() # [T, rank] — reuse for dX + d_gate_A = torch.empty_like(gate_A_t) + d_gate_B = torch.empty_like(gate_B_t) + d_gate_A.addmm_(X_lora.t(), grad_B_gate, alpha=gate_scale, beta=0) + d_gate_B.addmm_( + gate_A_t.t() @ X_lora.t(), grad_gate, alpha=gate_scale, beta=0 + ) # Compute input gradients - dX = torch.zeros_like(X) if ctx.needs_input_grad[0] else None + dX = None + dX_drop = None - if dX is not None: - # Up projection gradients - up_weight = dequantize(up_weight.t(), up_quant) + if ctx.needs_input_grad[0]: + # Base path gradients through gate and up + up_weight_deq = dequantize(up_weight.t(), up_quant) if ctx.inplace: - dX = torch.matmul(grad_up, up_weight.t(), out=X) + dX = torch.matmul(grad_up, up_weight_deq.t(), out=X) else: - dX = torch.matmul(grad_up, up_weight.t()) - del up_weight - - # Note the .to(dtype) only where mixing LoRA with base weights - if up_A is not None and up_B is not None: - dX += grad_up @ up_B.to(dtype).t() @ (up_scale * up_A.to(dtype).t()) - - # Gate projection gradients - gate_weight = dequantize(gate_weight, gate_quant) - dX += grad_gate @ gate_weight - del gate_weight - - if gate_A is not None and gate_B is not None: - dX += ( - grad_gate - @ gate_B.to(dtype).t() - @ (gate_scale * gate_A.to(dtype).t()) - ) + dX = torch.matmul(grad_up, up_weight_deq.t()) + del up_weight_deq + + gate_weight_deq = dequantize(gate_weight, gate_quant) + dX += grad_gate @ gate_weight_deq + del gate_weight_deq + + # LoRA path: reuse grad_B_up and grad_B_gate from above + if has_dropout: + dX_drop = torch.zeros_like(X_lora) + if grad_B_up is not None: + dX_drop.addmm_(grad_B_up, up_A_t.t(), alpha=up_scale) # type: ignore[union-attr] + if grad_B_gate is not None: + dX_drop.addmm_(grad_B_gate, gate_A_t.t(), alpha=gate_scale) # type: ignore[union-attr] + dX_drop = dX_drop.view(batch, seq_len, hd) + else: + if grad_B_up is not None: + dX.addmm_(grad_B_up, up_A_t.t(), alpha=up_scale) # type: ignore[union-attr] + if grad_B_gate is not None: + dX.addmm_(grad_B_gate, gate_A_t.t(), alpha=gate_scale) # type: ignore[union-attr] - # Reshape back dX = dX.view(batch, seq_len, hd) - # Return gradients in correct order matching forward inputs + # Return gradients matching forward input order: + # X, X_drop, + # gate: weight, bias, quant, A, B, scale, lora_bias, magnitude + # up: weight, bias, quant, A, B, scale, lora_bias, magnitude + # down: weight, bias, quant, A, B, scale, lora_bias, magnitude + # activation_fn, activation_fn_backward, inplace return ( dX, + dX_drop, + # Gate None, None, None, d_gate_A.t() if d_gate_A is not None else None, d_gate_B.t() if d_gate_B is not None else None, None, + d_gate_lora_bias, + d_gate_mag, + # Up None, None, None, d_up_A.t() if d_up_A is not None else None, d_up_B.t() if d_up_B is not None else None, None, + d_up_lora_bias, + d_up_mag, + # Down None, None, None, d_down_A.t() if d_down_A is not None else None, d_down_B.t() if d_down_B is not None else None, None, - None, + d_down_lora_bias, + d_down_mag, + # Activation fns and flags None, None, None, @@ -391,40 +709,54 @@ def backward( def apply_lora_mlp_swiglu(self, X: torch.Tensor, inplace: bool = True) -> torch.Tensor: - """ - Applies LoRA to MLP layer with SwiGLU activation. - - Args: - X: Input tensor for the MLP layer - inplace: Whether to perform operations in-place to save memory + """Applies LoRA to MLP layer with SwiGLU activation. - Returns: - Output tensor after applying LoRA-adapted MLP with SwiGLU activation + Supports bias, dropout, and DoRA. """ - gateW, gateb, gateW_quant, gateA, gateB, gateS = get_lora_parameters(self.gate_proj) - upW, upb, upW_quant, upA, upB, upS = get_lora_parameters(self.up_proj) - downW, downb, downW_quant, downA, downB, downS = get_lora_parameters(self.down_proj) + gateW, gateb, gateW_quant, gateA, gateB, gateS, gateLB, gateDrop, gateMag = ( + get_lora_parameters(self.gate_proj) + ) + upW, upb, upW_quant, upA, upB, upS, upLB, upDrop, upMag = get_lora_parameters( + self.up_proj + ) + downW, downb, downW_quant, downA, downB, downS, downLB, downDrop, downMag = ( + get_lora_parameters(self.down_proj) + ) + + # Shared dropout mask for gate and up (same input) + X_drop = _apply_dropout(gateDrop, X, self.training) out = LoRA_MLP.apply( X, + X_drop, + # Gate gateW, gateb, gateW_quant, gateA, gateB, gateS, + gateLB, + gateMag, + # Up upW, upb, upW_quant, upA, upB, upS, + upLB, + upMag, + # Down downW, downb, downW_quant, downA, downB, downS, + downLB, + downMag, + # Activation and flags swiglu_forward, swiglu_backward, inplace, @@ -434,39 +766,53 @@ def apply_lora_mlp_swiglu(self, X: torch.Tensor, inplace: bool = True) -> torch. def apply_lora_mlp_geglu(self, X: torch.Tensor, inplace: bool = True) -> torch.Tensor: + """Applies LoRA to MLP layer with GEGLU activation. + + Supports bias, dropout, and DoRA. """ - Applies LoRA to MLP layer with GEGLU activation. + gateW, gateb, gateW_quant, gateA, gateB, gateS, gateLB, gateDrop, gateMag = ( + get_lora_parameters(self.gate_proj) + ) + upW, upb, upW_quant, upA, upB, upS, upLB, upDrop, upMag = get_lora_parameters( + self.up_proj + ) + downW, downb, downW_quant, downA, downB, downS, downLB, downDrop, downMag = ( + get_lora_parameters(self.down_proj) + ) - Args: - X: Input tensor for the MLP layer - inplace: Whether to perform operations in-place to save memory + X_drop = _apply_dropout(gateDrop, X, self.training) - Returns: - Output tensor after applying LoRA-adapted MLP with GEGLU activation - """ - gateW, gateb, gateW_quant, gateA, gateB, gateS = get_lora_parameters(self.gate_proj) - upW, upb, upW_quant, upA, upB, upS = get_lora_parameters(self.up_proj) - downW, downb, downW_quant, downA, downB, downS = get_lora_parameters(self.down_proj) out = LoRA_MLP.apply( X, + X_drop, + # Gate gateW, gateb, gateW_quant, gateA, gateB, gateS, + gateLB, + gateMag, + # Up upW, upb, upW_quant, upA, upB, upS, + upLB, + upMag, + # Down downW, downb, downW_quant, downA, downB, downS, + downLB, + downMag, + # Activation and flags geglu_forward, geglu_backward, inplace, @@ -479,8 +825,8 @@ class LoRA_QKV(torch.autograd.Function): """ Optimized LoRA QKV implementation with quantization support. - Implements efficient computation of query, key, value projections with LoRA, - supporting quantization and memory optimization. + Supports bias, dropout, and DoRA (Weight-Decomposed Low-Rank Adaptation). + Dropout is applied outside this Function so autograd handles its backward. """ @staticmethod @@ -488,65 +834,160 @@ class LoRA_QKV(torch.autograd.Function): def forward( ctx: torch.autograd.function.FunctionCtx, X: torch.Tensor, + X_drop: torch.Tensor | None, + # Q params q_weight: torch.Tensor, q_bias: torch.Tensor | None, q_quant: QuantState | None, q_A: torch.Tensor | None, q_B: torch.Tensor | None, q_scale: float, + q_lora_bias: torch.Tensor | None, + q_magnitude: torch.Tensor | None, + # K params k_weight: torch.Tensor, k_bias: torch.Tensor | None, k_quant: QuantState | None, k_A: torch.Tensor | None, k_B: torch.Tensor | None, k_scale: float, + k_lora_bias: torch.Tensor | None, + k_magnitude: torch.Tensor | None, + # V params v_weight: torch.Tensor, v_bias: torch.Tensor | None, v_quant: QuantState | None, v_A: torch.Tensor | None, v_B: torch.Tensor | None, v_scale: float, + v_lora_bias: torch.Tensor | None, + v_magnitude: torch.Tensor | None, + # Flags inplace: bool = True, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: - """ - Forward pass computing Q, K, V projections with LoRA. - - Args: - ctx: Autograd context - X: Input tensor - q_weight: Query projection weight - q_bias: Query projection bias - q_quant: Query quantization state - q_A: Query LoRA A matrix - q_B: Query LoRA B matrix - q_scale: Query LoRA scale - k_weight: Key projection weight - k_bias: Key projection bias - k_quant: Key quantization state - k_A: Key LoRA A matrix - k_B: Key LoRA B matrix - k_scale: Key LoRA scale - v_weight: Value projection weight - v_bias: Value projection bias - v_quant: Value quantization state - v_A: Value LoRA A matrix - v_B: Value LoRA B matrix - v_scale: Value LoRA scale - inplace: Whether to perform operations in-place - - Returns: - Tuple of (Query, Key, Value) projection tensors - """ - Q = matmul_lora(X, q_weight, q_bias, q_quant, q_A, q_B, q_scale) - K = matmul_lora(X, k_weight, k_bias, k_quant, k_A, k_B, k_scale) - V = matmul_lora(X, v_weight, v_bias, v_quant, v_A, v_B, v_scale) - - ctx.save_for_backward(X, q_A, q_B, k_A, k_B, v_A, v_B) + has_dropout = X_drop is not None + has_dora = q_magnitude is not None + + if has_dora: + dtype = X.dtype + X_lora = X_drop if has_dropout else X + + # Compute Q with DoRA + Q_base = matmul_lora(X, q_weight, None, q_quant, None, None, None) + Q_lora = _lora_only(X_lora, q_A, q_B, q_scale, q_lora_bias, dtype) + q_mag_scale = _compute_dora_scale( + q_weight, q_quant, q_A, q_B, q_scale, q_magnitude, dtype + ) + Q = q_mag_scale.unsqueeze(0) * (Q_base + Q_lora) + if q_bias is not None: + Q = Q + q_bias + + # Compute K with DoRA + K_base = matmul_lora(X, k_weight, None, k_quant, None, None, None) + K_lora = _lora_only(X_lora, k_A, k_B, k_scale, k_lora_bias, dtype) + k_mag_scale = _compute_dora_scale( + k_weight, k_quant, k_A, k_B, k_scale, k_magnitude, dtype + ) + K = k_mag_scale.unsqueeze(0) * (K_base + K_lora) + if k_bias is not None: + K = K + k_bias + + # Compute V with DoRA + V_base = matmul_lora(X, v_weight, None, v_quant, None, None, None) + V_lora = _lora_only(X_lora, v_A, v_B, v_scale, v_lora_bias, dtype) + v_mag_scale = _compute_dora_scale( + v_weight, v_quant, v_A, v_B, v_scale, v_magnitude, dtype + ) + V = v_mag_scale.unsqueeze(0) * (V_base + V_lora) + if v_bias is not None: + V = V + v_bias + + # Save for backward: need combined (base+lora) and mag_scale for DoRA grads + Q_combined = Q_base + Q_lora + K_combined = K_base + K_lora + V_combined = V_base + V_lora + + ctx.save_for_backward( + X, + X_drop if has_dropout else X, + q_A.to(dtype) if q_A is not None else q_A, + q_B.to(dtype) if q_B is not None else q_B, + k_A.to(dtype) if k_A is not None else k_A, + k_B.to(dtype) if k_B is not None else k_B, + v_A.to(dtype) if v_A is not None else v_A, + v_B.to(dtype) if v_B is not None else v_B, + q_magnitude, + k_magnitude, + v_magnitude, + q_mag_scale, + k_mag_scale, + v_mag_scale, + Q_combined, + K_combined, + V_combined, + q_lora_bias, + k_lora_bias, + v_lora_bias, + ) + else: + # Standard LoRA (with optional dropout and bias) + Q = matmul_lora( + X, + q_weight, + q_bias, + q_quant, + q_A, + q_B, + q_scale, + X_drop=X_drop, + lora_bias=q_lora_bias, + ) + K = matmul_lora( + X, + k_weight, + k_bias, + k_quant, + k_A, + k_B, + k_scale, + X_drop=X_drop, + lora_bias=k_lora_bias, + ) + V = matmul_lora( + X, + v_weight, + v_bias, + v_quant, + v_A, + v_B, + v_scale, + X_drop=X_drop, + lora_bias=v_lora_bias, + ) + + # Pre-convert LoRA matrices to compute dtype to avoid + # redundant fp32→bf16 conversion in backward + dtype = X.dtype + ctx.save_for_backward( + X, + X_drop if has_dropout else X, + q_A.to(dtype) if q_A is not None else q_A, + q_B.to(dtype) if q_B is not None else q_B, + k_A.to(dtype) if k_A is not None else k_A, + k_B.to(dtype) if k_B is not None else k_B, + v_A.to(dtype) if v_A is not None else v_A, + v_B.to(dtype) if v_B is not None else v_B, + q_lora_bias, + k_lora_bias, + v_lora_bias, + ) + ctx.scales = (q_scale, k_scale, v_scale) ctx.quants = (q_quant, k_quant, v_quant) ctx.weights = (q_weight, k_weight, v_weight) - ctx.biases = (q_bias, k_bias, v_bias) ctx.inplace = inplace + ctx.has_dropout = has_dropout + ctx.has_dora = has_dora return Q, K, V @@ -557,110 +998,169 @@ def backward( q_grad: torch.Tensor, k_grad: torch.Tensor, v_grad: torch.Tensor, - ) -> tuple[ - torch.Tensor, - None, - None, - None, - torch.Tensor | None, - torch.Tensor | None, - None, - None, - None, - None, - torch.Tensor | None, - torch.Tensor | None, - None, - None, - None, - None, - torch.Tensor | None, - torch.Tensor | None, - None, - None, - ]: - """ - Backward pass computing gradients for LoRA QKV. - - Args: - ctx: Autograd context - q_grad: Gradient for query projection - k_grad: Gradient for key projection - v_grad: Gradient for value projection - - Returns: - Tuple containing gradients for all forward inputs - """ - X, A_q, B_q, A_k, B_k, A_v, B_v = ctx.saved_tensors + ): q_weight, k_weight, v_weight = ctx.weights q_quant, k_quant, v_quant = ctx.quants q_scale, k_scale, v_scale = ctx.scales - dtype = X.dtype + has_dropout = ctx.has_dropout + has_dora = ctx.has_dora + + if has_dora: + ( + X, + X_lora, + A_q, + B_q, + A_k, + B_k, + A_v, + B_v, + q_magnitude, + k_magnitude, + v_magnitude, + q_mag_scale, + k_mag_scale, + v_mag_scale, + Q_combined, + K_combined, + V_combined, + q_lora_bias, + k_lora_bias, + v_lora_bias, + ) = ctx.saved_tensors + else: + ( + X, + X_lora, + A_q, + B_q, + A_k, + B_k, + A_v, + B_v, + q_lora_bias, + k_lora_bias, + v_lora_bias, + ) = ctx.saved_tensors + q_magnitude = k_magnitude = v_magnitude = None + q_mag_scale = k_mag_scale = v_mag_scale = None + Q_combined = K_combined = V_combined = None - # Reshape gradients batch, seq_len = X.shape[:2] q_grad = q_grad.view(-1, q_grad.shape[-1]) k_grad = k_grad.reshape(-1, k_grad.shape[-1]) v_grad = v_grad.view(-1, v_grad.shape[-1]) X = X.view(-1, X.shape[-1]) - - # Pre-transpose X once - X_t = X.t() + X_lora = X_lora.view(-1, X_lora.shape[-1]) + + # DoRA: scale gradients through mag_norm_scale + d_q_mag = d_k_mag = d_v_mag = None + d_q_lora_bias = d_k_lora_bias = d_v_lora_bias = None + + if has_dora: + Q_combined = Q_combined.view(-1, Q_combined.shape[-1]) + K_combined = K_combined.view(-1, K_combined.shape[-1]) + V_combined = V_combined.view(-1, V_combined.shape[-1]) + + # Magnitude gradients: d_mag = sum_t(grad * combined) / weight_norm + # Since mag_scale = magnitude / weight_norm, and weight_norm is detached: + # d_magnitude = sum_t(grad * combined) * (1 / weight_norm) + # But we have mag_scale = magnitude / weight_norm + # d_mag_scale_j = grad_j * combined_j (per element) + # d_magnitude_j = d_mag_scale_j / weight_norm_j = sum_t(grad * combined) / weight_norm + # Simpler: d_magnitude = sum(grad * combined, dim=0) * mag_scale / magnitude + # Actually: mag_scale = m/wn, d_output/d_m = combined/wn = combined * mag_scale/m + # d_m = sum(grad * combined * mag_scale / m, dim=0) ... no. + # Let's be precise: output = mag_scale * combined, mag_scale = m / wn (wn detached) + # d_loss/d_m = d_loss/d_output * d_output/d_m = sum_t(grad_t * combined_t / wn) + # = sum_t(grad_t * combined_t) * (1/wn) = sum_t(grad_t * combined_t) * mag_scale / m + # Or just: d_m = sum(grad * combined, dim=0) / weight_norm + # Since we don't have weight_norm saved, use mag_scale/magnitude: + # 1/wn = mag_scale/magnitude + d_q_mag = (q_grad * Q_combined).sum(dim=0) * q_mag_scale / q_magnitude + d_k_mag = (k_grad * K_combined).sum(dim=0) * k_mag_scale / k_magnitude + d_v_mag = (v_grad * V_combined).sum(dim=0) * v_mag_scale / v_magnitude + + # Chain rule: grad through combined = grad * mag_scale + q_grad = q_grad * q_mag_scale.unsqueeze(0) + k_grad = k_grad * k_mag_scale.unsqueeze(0) + v_grad = v_grad * v_mag_scale.unsqueeze(0) + + # LoRA bias gradients + if q_lora_bias is not None: + d_q_lora_bias = q_scale * q_grad.sum(dim=0) + if k_lora_bias is not None: + d_k_lora_bias = k_scale * k_grad.sum(dim=0) + if v_lora_bias is not None: + d_v_lora_bias = v_scale * v_grad.sum(dim=0) + + # Pre-transpose X_lora for LoRA gradients + X_lora_t = X_lora.t() # Initialize LoRA gradients as None d_A_q = d_B_q = d_A_k = d_B_k = d_A_v = d_B_v = None - # Compute q path LoRA gradients if adapters exist + # Compute LoRA gradients using X_lora (before any inplace ops on X) + # A_q, B_q etc. are already in compute dtype (converted in forward) + # Key optimization: compute grad @ B once, reuse for both dA and dX_lora + # A has shape [rank, in], B has shape [out, rank] + grad_B_q = grad_B_k = grad_B_v = None + if A_q is not None and B_q is not None: - A_q_scaled = (q_scale * A_q).to(dtype) - B_q_scaled = B_q.to(dtype) - d_A_q = torch.mm(X_t, torch.mm(q_grad, B_q_scaled)) - d_B_q = torch.mm(torch.mm(A_q_scaled, X_t), q_grad) + grad_B_q = q_grad @ B_q # [T, rank] — reused for dA and dX + d_A_q = torch.empty_like(A_q.t()) + d_B_q = torch.empty_like(B_q.t()) + d_A_q.addmm_(X_lora_t, grad_B_q, alpha=q_scale, beta=0) + d_B_q.addmm_(A_q @ X_lora_t, q_grad, alpha=q_scale, beta=0) - # Compute k path LoRA gradients if adapters exist if A_k is not None and B_k is not None: - A_k_scaled = (k_scale * A_k).to(dtype) - B_k_scaled = B_k.to(dtype) - d_A_k = torch.mm(X_t, torch.mm(k_grad, B_k_scaled)) - d_B_k = torch.mm(torch.mm(A_k_scaled, X_t), k_grad) + grad_B_k = k_grad @ B_k + d_A_k = torch.empty_like(A_k.t()) + d_B_k = torch.empty_like(B_k.t()) + d_A_k.addmm_(X_lora_t, grad_B_k, alpha=k_scale, beta=0) + d_B_k.addmm_(A_k @ X_lora_t, k_grad, alpha=k_scale, beta=0) - # Compute v path LoRA gradients if adapters exist if A_v is not None and B_v is not None: - A_v_scaled = (v_scale * A_v).to(dtype) - B_v_scaled = B_v.to(dtype) - d_A_v = torch.mm(X_t, torch.mm(v_grad, B_v_scaled)) - d_B_v = torch.mm(torch.mm(A_v_scaled, X_t), v_grad) + grad_B_v = v_grad @ B_v + d_A_v = torch.empty_like(A_v.t()) + d_B_v = torch.empty_like(B_v.t()) + d_A_v.addmm_(X_lora_t, grad_B_v, alpha=v_scale, beta=0) + d_B_v.addmm_(A_v @ X_lora_t, v_grad, alpha=v_scale, beta=0) - # Compute input gradient, reusing X memory if possible + # Base path input gradient (can use inplace on X since X_lora refs are done) out_buffer = X if ctx.inplace else None - # Q path q_weight_t = dequantize(q_weight, q_quant) grad_X = torch.mm(q_grad, q_weight_t, out=out_buffer) - del q_weight del q_weight_t - if A_q is not None and B_q is not None: - # Stay decomposed: dQ @ B^T gives [T, R], then [T, R] @ (s*A) gives [T, in] - # This is 65x fewer FLOPs than materializing B@A into [out, in] - grad_X.addmm_(torch.mm(q_grad, B_q_scaled), A_q_scaled) - # K path k_weight_t = dequantize(k_weight, k_quant) grad_X.addmm_(k_grad, k_weight_t) - del k_weight del k_weight_t - if A_k is not None and B_k is not None: - grad_X.addmm_(torch.mm(k_grad, B_k_scaled), A_k_scaled) - # V path v_weight_t = dequantize(v_weight, v_quant) grad_X.addmm_(v_grad, v_weight_t) - del v_weight del v_weight_t - if A_v is not None and B_v is not None: - grad_X.addmm_(torch.mm(v_grad, B_v_scaled), A_v_scaled) - # Transpose gradients if needed + # LoRA path input gradient: s * grad @ B @ A (reuses grad_B_* from above) + if has_dropout: + grad_X_drop = torch.zeros_like(X_lora) + if grad_B_q is not None: + grad_X_drop.addmm_(grad_B_q, A_q, alpha=q_scale) + if grad_B_k is not None: + grad_X_drop.addmm_(grad_B_k, A_k, alpha=k_scale) + if grad_B_v is not None: + grad_X_drop.addmm_(grad_B_v, A_v, alpha=v_scale) + else: + grad_X_drop = None + if grad_B_q is not None: + grad_X.addmm_(grad_B_q, A_q, alpha=q_scale) + if grad_B_k is not None: + grad_X.addmm_(grad_B_k, A_k, alpha=k_scale) + if grad_B_v is not None: + grad_X.addmm_(grad_B_v, A_v, alpha=v_scale) + + # Transpose LoRA gradients if d_A_q is not None: d_A_q = d_A_q.t() d_B_q = d_B_q.t() # type: ignore[union-attr] @@ -671,66 +1171,126 @@ def backward( d_A_v = d_A_v.t() d_B_v = d_B_v.t() # type: ignore[union-attr] + grad_X = grad_X.view(batch, seq_len, -1) + if grad_X_drop is not None: + grad_X_drop = grad_X_drop.view(batch, seq_len, -1) + + # Return gradients for all forward inputs: + # X, X_drop, + # q: weight, bias, quant, A, B, scale, lora_bias, magnitude + # k: weight, bias, quant, A, B, scale, lora_bias, magnitude + # v: weight, bias, quant, A, B, scale, lora_bias, magnitude + # inplace return ( - grad_X.view(batch, seq_len, -1), + grad_X, + grad_X_drop, + # Q None, None, None, d_A_q, d_B_q, None, + d_q_lora_bias, + d_q_mag, + # K None, None, None, d_A_k, d_B_k, None, + d_k_lora_bias, + d_k_mag, + # V None, None, None, d_A_v, d_B_v, None, + d_v_lora_bias, + d_v_mag, + # inplace None, ) +def _lora_only( + X: torch.Tensor, + A: torch.Tensor | None, + B: torch.Tensor | None, + s: float | None, + lora_bias: torch.Tensor | None, + dtype: torch.dtype, +) -> torch.Tensor: + """Compute only the LoRA contribution: s * X @ A^T @ B^T + s * lora_bias.""" + if A is None: + return torch.zeros( + X.shape[:-1] + (B.shape[0] if B is not None else 1,), + device=X.device, + dtype=dtype, + ) + reshape = False + if X.dim() == 3: + batch, seq_len, _ = X.shape + X = X.view(-1, X.shape[-1]) + reshape = True + At, Bt = A.t().to(dtype), B.t().to(dtype) # type: ignore[union-attr] + out = s * X @ At @ Bt + if lora_bias is not None: + out = out + s * lora_bias + return out.view(batch, seq_len, -1) if reshape else out + + def apply_lora_qkv( self, X: torch.Tensor, inplace: bool = True ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """ Applies LoRA to compute Query, Key, Value projections. - Args: - X: Input tensor - inplace: Whether to perform operations in-place - - Returns: - Tuple of (Query, Key, Value) projection tensors + Supports bias, dropout, and DoRA. Dropout is applied outside the autograd + Function so PyTorch handles its backward automatically. A single shared + dropout mask is used across Q, K, V projections for memory efficiency. """ - QW, Qb, QW_quant, QA, QB, QS = get_lora_parameters(self.q_proj) - KW, Kb, KW_quant, KA, KB, KS = get_lora_parameters(self.k_proj) - VW, Vb, VW_quant, VA, VB, VS = get_lora_parameters(self.v_proj) + QW, Qb, QW_quant, QA, QB, QS, Qlb, Qdrop, Qmag = get_lora_parameters(self.q_proj) + KW, Kb, KW_quant, KA, KB, KS, Klb, Kdrop, Kmag = get_lora_parameters(self.k_proj) + VW, Vb, VW_quant, VA, VB, VS, Vlb, Vdrop, Vmag = get_lora_parameters(self.v_proj) + + # Apply dropout outside autograd.Function (shared mask for Q, K, V) + X_drop = _apply_dropout(Qdrop, X, self.training) + Q, K, V = LoRA_QKV.apply( X, + X_drop, + # Q QW, Qb, QW_quant, QA, QB, QS, + Qlb, + Qmag, + # K KW, Kb, KW_quant, KA, KB, KS, + Klb, + Kmag, + # V VW, Vb, VW_quant, VA, VB, VS, + Vlb, + Vmag, + # Flags inplace, ) @@ -738,43 +1298,75 @@ def apply_lora_qkv( class LoRA_O(torch.autograd.Function): - """Optimized LoRA implementation for output projection.""" + """Optimized LoRA implementation for output projection. + + Supports bias, dropout, and DoRA. + """ @staticmethod @torch_amp_custom_fwd def forward( ctx: torch.autograd.function.FunctionCtx, X: torch.Tensor, + X_drop: torch.Tensor | None, W: torch.Tensor, - b: torch.Tensor, + b: torch.Tensor | None, W_quant: QuantState | None, - A: torch.Tensor, - B: torch.Tensor, + A: torch.Tensor | None, + B: torch.Tensor | None, s: float, + lora_bias: torch.Tensor | None, + magnitude: torch.Tensor | None, ) -> torch.Tensor: - """ - Forward pass for output projection with LoRA. - - Args: - ctx: Autograd context - X: Input tensor - W: Output projection weight - b: Output projection bias - W_quant: Weight quantization state - A: LoRA A matrix - B: LoRA B matrix - s: LoRA scaling factor - - Returns: - Output projection result - """ - XW = matmul_lora(X, W, b, W_quant, A, B, s) - ctx.custom_saved_tensors = ( - W, - W_quant, - s, - ) - ctx.save_for_backward(A, B, X) + has_dropout = X_drop is not None + has_dora = magnitude is not None + dtype = X.dtype + + if has_dora: + X_lora = X_drop if has_dropout else X + base_out = matmul_lora(X, W, None, W_quant, None, None, None) + lora_out = _lora_only(X_lora, A, B, s, lora_bias, dtype) + mag_scale = _compute_dora_scale(W, W_quant, A, B, s, magnitude, dtype) + combined = base_out + lora_out + XW = mag_scale.unsqueeze(0) * combined + if b is not None: + XW = XW + b + + ctx.save_for_backward( + A.to(dtype) if A is not None else A, + B.to(dtype) if B is not None else B, + X, + X_drop if has_dropout else X, + magnitude, + mag_scale, + combined, + lora_bias, + ) + else: + XW = matmul_lora( + X, + W, + b, + W_quant, + A, + B, + s, + X_drop=X_drop, + lora_bias=lora_bias, + ) + # Pre-convert LoRA matrices to compute dtype for backward + dtype = X.dtype + ctx.save_for_backward( + A.to(dtype) if A is not None else A, + B.to(dtype) if B is not None else B, + X, + X_drop if has_dropout else X, + lora_bias, + ) + + ctx.custom_saved_tensors = (W, W_quant, s) + ctx.has_dropout = has_dropout + ctx.has_dora = has_dora return XW @@ -783,62 +1375,330 @@ def forward( def backward( ctx: torch.autograd.function.FunctionCtx, dY: torch.Tensor, - ) -> tuple[ - torch.Tensor, - None, - None, - None, - torch.Tensor, - torch.Tensor, - None, - ]: - """ - Backward pass computing gradients for LoRA output projection. - - Args: - ctx: Autograd context - dY: Gradient of loss with respect to output - - Returns: - Tuple containing gradients for all forward inputs - """ + ): W, W_quant, s = ctx.custom_saved_tensors - A, B, X = ctx.saved_tensors + has_dropout = ctx.has_dropout + has_dora = ctx.has_dora + + if has_dora: + A, B, X, X_lora, magnitude, mag_scale, combined, lora_bias = ( + ctx.saved_tensors + ) + else: + A, B, X, X_lora, lora_bias = ctx.saved_tensors + magnitude = mag_scale = combined = None batch, seq_len, hd = X.shape dY = dY.reshape(-1, dY.shape[-1]) X = X.reshape(-1, X.shape[-1]) - dtype = X.dtype + X_lora = X_lora.reshape(-1, X_lora.shape[-1]) + + d_mag = d_lora_bias = None + + if has_dora: + combined = combined.view(-1, combined.shape[-1]) + d_mag = (dY * combined).sum(dim=0) * mag_scale / magnitude + dY = dY * mag_scale.unsqueeze(0) + + # LoRA bias gradient + if lora_bias is not None: + d_lora_bias = s * dY.sum(dim=0) + + # LoRA parameter gradients (A, B already in compute dtype from forward) + # Compute dY @ B once, reuse for both dA and dX_lora + d_A = d_B = None + grad_B = None + if A is not None: + grad_B = dY @ B # [T, rank] — reused below + X_lora_t = X_lora.t() + d_A = torch.empty_like(A.t()) + d_B = torch.empty_like(B.t()) + d_A.addmm_(X_lora_t, grad_B, alpha=s, beta=0) + d_B.addmm_(A @ X_lora_t, dY, alpha=s, beta=0) + + # Base path input gradient + W_deq = dequantize(W.t(), W_quant) + dX = dY @ W_deq.t() + del W_deq + + if has_dropout: + dX_drop = None + if grad_B is not None: + dX_drop = (grad_B @ A * s).view(batch, seq_len, hd) + else: + dX_drop = None + if grad_B is not None: + dX.addmm_(grad_B, A, alpha=s) + + # X, X_drop, W, b, W_quant, A, B, s, lora_bias, magnitude + return ( + dX.view(batch, seq_len, hd), + dX_drop, + None, + None, + None, + d_A.t() if d_A is not None else None, + d_B.t() if d_B is not None else None, + None, + d_lora_bias, + d_mag, + ) - # Weight projection - dY_X = X.t() @ dY - d_A = s * dY_X @ B - d_B = s * A @ dY_X - # Get derivative for dX - W = dequantize(W.t(), W_quant) - dX = dY @ W.t() - del W +def apply_lora_o(self, X: torch.Tensor) -> torch.Tensor: + """ + Applies LoRA to output projection layer. - A, B = A.to(dtype), B.to(dtype) - # Stay decomposed: dY @ B gives [T, R], then [T, R] @ A gives [T, in] - dX.addmm_(torch.mm(dY, B), A, alpha=s) + Supports bias, dropout, and DoRA. + """ + OW, Ob, OW_quant, OA, OB, OS, Olb, Odrop, Omag = get_lora_parameters(self.o_proj) + X_drop = _apply_dropout(Odrop, X, self.training) + output = LoRA_O.apply(X, X_drop, OW, Ob, OW_quant, OA, OB, OS, Olb, Omag) - # W, b, W_quant, A, B, s - return dX.view(batch, seq_len, hd), None, None, None, d_A.t(), d_B.t(), None + return output -def apply_lora_o(self, X: torch.Tensor) -> torch.Tensor: - """ - Applies LoRA to output projection layer. +# ============================================================ +# Embedding LoRA kernel +# ============================================================ - Args: - X: Input tensor - Returns: - Transformed output tensor +def get_embedding_lora_parameters( + embed: nn.Module, +) -> tuple[ + torch.Tensor, # W (base embedding weight) + torch.Tensor | None, # A (lora_embedding_A) + torch.Tensor | None, # B (lora_embedding_B) + float | None, # scaling + nn.Module | None, # dropout + torch.Tensor | None, # magnitude (DoRA) + nn.Module, # base_layer +]: + """Extract LoRA parameters from a PEFT Embedding module.""" + base_layer = embed.base_layer if hasattr(embed, "base_layer") else embed + W = base_layer.weight + + if not hasattr(embed, "disable_adapters") or embed.disable_adapters or embed.merged: + return W, None, None, None, None, None, base_layer + + active_adapter = ( + embed.active_adapters[0] + if hasattr(embed, "active_adapters") + else embed.active_adapter + ) + + A = embed.lora_embedding_A[active_adapter] # nn.Parameter [rank, vocab] + B = embed.lora_embedding_B[active_adapter] # nn.Parameter [hidden_dim, rank] + s = embed.scaling[active_adapter] + + # FSDP2 DTensor unshard (mirrors linear path logic) + if isinstance(A, DTensor): + A = A.full_tensor() + if isinstance(B, DTensor): + B = B.full_tensor() + + dropout = None + if hasattr(embed, "lora_dropout") and active_adapter in embed.lora_dropout: + dropout = embed.lora_dropout[active_adapter] + + magnitude = None + if ( + hasattr(embed, "lora_magnitude_vector") + and embed.lora_magnitude_vector + and active_adapter in embed.lora_magnitude_vector + ): + mag_layer = embed.lora_magnitude_vector[active_adapter] + magnitude = mag_layer.weight + if isinstance(magnitude, DTensor): + magnitude = magnitude.full_tensor() + + return W, A, B, s, dropout, magnitude, base_layer + + +class LoRA_Embedding(torch.autograd.Function): + """Fused LoRA embedding: F.embedding(x, W) + s * F.embedding(x, A^T) @ B^T. + + Supports dropout and DoRA. """ - OW, Ob, OW_quant, OA, OB, OS = get_lora_parameters(self.o_proj) - output = LoRA_O.apply(X, OW, Ob, OW_quant, OA, OB, OS) - return output + @staticmethod + @torch_amp_custom_fwd + def forward( + ctx, + x: torch.Tensor, + W: torch.Tensor, + A: torch.Tensor | None, + B: torch.Tensor | None, + s: float | None, + magnitude: torch.Tensor | None, + padding_idx: int | None, + # base_layer fields for F.embedding + max_norm: float | None, + norm_type: float, + scale_grad_by_freq: bool, + sparse: bool, + ) -> torch.Tensor: + import torch.nn.functional as F + + has_dora = magnitude is not None + dtype = W.dtype + + # Base embedding lookup + result = F.embedding( + x, + W, + padding_idx=padding_idx, + max_norm=max_norm, + norm_type=norm_type, + scale_grad_by_freq=scale_grad_by_freq, + sparse=sparse, + ) + + if A is not None: + # LoRA: F.embedding(x, A^T) @ B^T * s + A_T = A.t() # type: ignore[union-attr] # [vocab, rank] + B_T = B.t() # type: ignore[union-attr] # [rank, hidden_dim] + after_A = F.embedding( + x, + A_T, + padding_idx=padding_idx, + max_norm=max_norm, + norm_type=norm_type, + scale_grad_by_freq=scale_grad_by_freq, + sparse=sparse, + ) # [batch, seq, rank] + + lora_result = after_A @ B_T # [batch, seq, hidden_dim] + + if has_dora: + mag_scale = _compute_dora_scale(W.t(), None, A, B, s, magnitude, dtype) # type: ignore[arg-type] + # DoRA: mag_scale * (base + s * lora) + # base embedding has no bias + pre_scaled = result + s * lora_result # unscaled combined + result = mag_scale.unsqueeze(0) * pre_scaled + ctx.save_for_backward( + x, + A.to(dtype), + B.to(dtype), # type: ignore[union-attr] + after_A, + magnitude, + mag_scale, + pre_scaled, # save unscaled for correct d_mag + ) + else: + result = result + s * lora_result + ctx.save_for_backward(x, A.to(dtype), B.to(dtype), after_A) # type: ignore[union-attr] + else: + ctx.save_for_backward( + x, + ) + + ctx.s = s + ctx.has_dora = has_dora + ctx.has_lora = A is not None + ctx.padding_idx = padding_idx + ctx.scale_grad_by_freq = scale_grad_by_freq + + return result + + @staticmethod + @torch_amp_custom_bwd + def backward(ctx, grad_output): + s = ctx.s + has_dora = ctx.has_dora + has_lora = ctx.has_lora + + d_A = d_B = d_mag = None + + if not has_lora: + (x,) = ctx.saved_tensors + elif has_dora: + x, A, B, after_A, magnitude, mag_scale, combined = ctx.saved_tensors + # DoRA magnitude gradient + combined_flat = combined.view(-1, combined.shape[-1]) + grad_flat = grad_output.view(-1, grad_output.shape[-1]) + d_mag = (grad_flat * combined_flat).sum(dim=0) * mag_scale / magnitude + # Chain rule through mag_scale + grad_output = grad_output * mag_scale.unsqueeze(0).unsqueeze(0) + else: + x, A, B, after_A = ctx.saved_tensors + + if has_lora: + # Use float32 for gradient computation (LoRA params are fp32) + compute_dtype = torch.float32 + + after_A_flat = after_A.view(-1, after_A.shape[-1]).to(compute_dtype) + grad_flat = grad_output.view(-1, grad_output.shape[-1]).to(compute_dtype) + B_f = B.to(compute_dtype) + + # B is [hidden_dim, rank], B_T = B.t() = [rank, hidden_dim] + # lora_result = after_A @ B_T → d/d(B_T) = s * after_A^T @ grad + B_T = B_f.t() # [rank, hidden_dim] + d_B_T = torch.empty_like(B_T) + d_B_T.addmm_(after_A_flat.t(), grad_flat, alpha=s, beta=0) + d_B = d_B_T.t() # [hidden_dim, rank] + + # d_A: gradient flows through F.embedding lookup + # d_after_A = s * grad @ B = [T, hidden] @ [hidden, rank] = [T, rank] + d_after_A = s * grad_flat @ B_f + + # F.embedding backward: scatter d_after_A into A^T gradient + x_flat = x.view(-1) + + # Zero out padding_idx contributions (matches F.embedding behavior) + if ctx.padding_idx is not None: + pad_mask = x_flat != ctx.padding_idx + d_after_A = d_after_A * pad_mask.unsqueeze(1).to(d_after_A.dtype) + + # scale_grad_by_freq: divide each contribution by token frequency + if ctx.scale_grad_by_freq: + counts = torch.bincount(x_flat, minlength=A.shape[1]).clamp(min=1) + freq_scale = 1.0 / counts[x_flat].unsqueeze(1).to(d_after_A.dtype) + d_after_A = d_after_A * freq_scale + + A_f = A.to(compute_dtype) + d_A_T = torch.zeros_like(A_f.t()) # [vocab, rank] + d_A_T.index_add_(0, x_flat, d_after_A) + d_A = d_A_T.t() # [rank, vocab] + + # x, W, A, B, s, magnitude, padding_idx, max_norm, norm_type, scale_grad_by_freq, sparse + return ( + None, # x + None, # W (base embedding weight grad handled by PyTorch) + d_A, # A + d_B, # B + None, # s + d_mag, # magnitude + None, + None, + None, + None, + None, # padding_idx, max_norm, norm_type, scale_grad_by_freq, sparse + ) + + +def apply_lora_embedding(self, x: torch.Tensor) -> torch.Tensor: + """Applies LoRA to embedding layer.""" + W, A, B, s, dropout, magnitude, base_layer = get_embedding_lora_parameters(self) + + # Capture base output dtype (bf16 for bf16 models) to cast back at end + output_dtype = W.dtype + + # Note: PEFT's Embedding forward does not apply dropout for embeddings + # (integer indices can't be dropped; PEFT silently ignores lora_dropout here) + result = LoRA_Embedding.apply( + x, + W, + A, + B, + s, + magnitude, + base_layer.padding_idx, + base_layer.max_norm, + base_layer.norm_type, + base_layer.scale_grad_by_freq, + base_layer.sparse, + ) + + # Cast to model dtype (LoRA ops may upcast to float32) + return result.to(output_dtype) diff --git a/src/axolotl/kernels/quantize.py b/src/axolotl/kernels/quantize.py index c9c0f59bd6..ff564fecc6 100644 --- a/src/axolotl/kernels/quantize.py +++ b/src/axolotl/kernels/quantize.py @@ -105,6 +105,10 @@ def dequantize( # Extract quantization state if not isinstance(quant_state, list): # New style quant_state class + # Non-double-quantized models have offset=None and state2=None + if quant_state.offset is None or quant_state.state2 is None: + # Fall back to bitsandbytes standard dequantize + return bnb.functional.dequantize_4bit(W, quant_state, quant_type="nf4") absmax = quant_state.absmax.to(target_device) shape = quant_state.shape dtype = quant_state.dtype diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 44be5267d7..5bb3a32ebc 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -12,6 +12,7 @@ from transformers import AutoConfig from axolotl.kernels.lora import ( + apply_lora_embedding, apply_lora_mlp_geglu, apply_lora_mlp_swiglu, apply_lora_o, @@ -370,13 +371,13 @@ def apply_lora_kernel_patches( active_adapter = model.active_adapter lora_config = model.model.peft_config[active_adapter] - # Only patch if conditions are met - can_patch = lora_config.lora_dropout == 0 and lora_config.bias == "none" - - if not can_patch: - LOG.warning("Cannot patch layers - requires no dropout and no bias") - LOG.warning("Please specify `lora_dropout: 0` in your axolotl config file") - return model + # Log what features are active + if lora_config.lora_dropout > 0: + LOG.info(f"LoRA kernels: dropout={lora_config.lora_dropout} enabled") + if lora_config.bias != "none": + LOG.info(f"LoRA kernels: bias={lora_config.bias} enabled") + if lora_config.use_dora: + LOG.info("LoRA kernels: DoRA enabled") # This needs to be reset after patching original_level = LOG.getEffectiveLevel() @@ -419,44 +420,33 @@ def apply_lora_kernel_patches( for linear_proj in ["q_proj", "k_proj", "v_proj"] ] can_patch_qkv = all( - hasattr(module, "lora_A") - and len(getattr(module, "lora_magnitude_vector", []) or []) == 0 - for module in layer_modules + hasattr(module, "lora_A") for module in layer_modules ) if can_patch_qkv: - # Add optimized implementation self_attn.apply_qkv = types.MethodType(apply_lora_qkv, self_attn) else: LOG.warning_once( - "Cannot patch some attention QKV projections - requires LoRA " - "adapters and no lora_magnitude_vector (DoRA)" + "Cannot patch some attention QKV projections - requires LoRA adapters" ) if cfg.lora_o_kernel: # Output patching layer_modules = [ getattr(self_attn, linear_proj) for linear_proj in ["o_proj"] ] - can_patch_o = all( - hasattr(module, "lora_A") - and len(getattr(module, "lora_magnitude_vector", []) or []) == 0 - for module in layer_modules - ) + can_patch_o = all(hasattr(module, "lora_A") for module in layer_modules) if can_patch_o: self_attn.apply_o = types.MethodType(apply_lora_o, self_attn) else: LOG.warning_once( - "Cannot patch some attention output projection - requires LoRA " - "adapters and no lora_magnitude_vector (DoRA)" + "Cannot patch some attention output projection - requires LoRA adapters" ) for gate_proj, up_proj, down_proj, mlp in find_mlp_in_layer(layer): if cfg.lora_mlp_kernel: # MLP patching can_patch_mlp = all( - hasattr(proj, "lora_A") - and len(getattr(proj, "lora_magnitude_vector", []) or []) == 0 - for proj in (gate_proj, up_proj, down_proj) + hasattr(proj, "lora_A") for proj in (gate_proj, up_proj, down_proj) ) if can_patch_mlp: @@ -464,15 +454,50 @@ def apply_lora_kernel_patches( layer.mlp.forward = types.MethodType(apply_fn, mlp) else: LOG.warning_once( - "Cannot patch some MLP layers - requires LoRA adapters and no " - "lora_magnitude_vector (DoRA)" + "Cannot patch some MLP layers - requires LoRA adapters" ) + # Patch embedding layers (model-level, not per-layer) + if cfg.lora_embedding_kernel: + _patch_embedding_layers(model, cfg) + LOG.setLevel(original_level) return model +def _patch_embedding_layers(model: PeftModelForCausalLM, cfg: DictDefault): + """Patch embedding layers with fused LoRA kernel. + + Handles both embed_tokens (nn.Embedding with lora_embedding_A/B) and + lm_head (nn.Linear with lora_A/B, used when tied embeddings are untied by PEFT). + """ + pretrained_model = model.model + patched = 0 + + # Find embedding modules - check common locations + for attr_path in [ + ("model", "embed_tokens"), + ("model", "language_model", "embed_tokens"), + ]: + parent = pretrained_model + for attr in attr_path: + parent = getattr(parent, attr, None) + if parent is None: + break + if parent is not None and hasattr(parent, "lora_embedding_A"): + LOG.info(f"Patching embedding layer: {'.'.join(attr_path)}") + parent.forward = types.MethodType(apply_lora_embedding, parent) + patched += 1 + + # lm_head with LoRA is a Linear layer - already handled by LoRA_O/LoRA_W kernels + # when included in target_modules. No special embedding handling needed since + # PEFT wraps it as a Linear (not Embedding) even for tied models. + + if not patched: + LOG.debug("No embedding layers with LoRA found to patch") + + class FakeMLP(nn.Module): """ placeholder MLP for triton patching diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 67dea4958b..2f269b78e8 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -703,6 +703,12 @@ class AxolotlInputConfig( "description": "Apply custom LoRA autograd functions and activation function Triton kernels for speed and memory savings. See: https://docs.axolotl.ai/docs/lora_optims.html" }, ) + lora_embedding_kernel: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Apply custom LoRA autograd function for embedding layers. See: https://docs.axolotl.ai/docs/lora_optims.html" + }, + ) chunked_cross_entropy: bool | None = Field( default=None, @@ -1313,6 +1319,7 @@ def check_multigpu_lora_kernels(cls, data): data.get("lora_mlp_kernel") or data.get("lora_qkv_kernel") or data.get("lora_o_kernel") + or data.get("lora_embedding_kernel") ): capabilities = data.get("capabilities") is_fsdp = data.get("fsdp_config") is not None @@ -1360,7 +1367,12 @@ def check_auto_enable_lora_kernels(cls, data): if data.get("adapter") in ["lora", "qlora"]: # Skip if already set, using unsloth optimizations, or using 8-bit unsloth_fields = ["unsloth_lora_mlp", "unsloth_lora_qkv", "unsloth_lora_o"] - kernel_fields = ["lora_mlp_kernel", "lora_qkv_kernel", "lora_o_kernel"] + kernel_fields = [ + "lora_mlp_kernel", + "lora_qkv_kernel", + "lora_o_kernel", + "lora_embedding_kernel", + ] if ( any(data.get(k) is not None for k in kernel_fields) or any(data.get(k) for k in unsloth_fields) @@ -1373,10 +1385,6 @@ def check_auto_enable_lora_kernels(cls, data): if data.get("trust_remote_code"): return data - # Skip if dropout is not 0, as auto enabling it would just disable it during runtime patch checks - if data.get("lora_dropout") != 0: - return data - # Check multi-GPU compatibility capabilities = data.get("capabilities") is_multi_gpu = capabilities and capabilities.get("n_gpu", 0) > 1 @@ -1398,6 +1406,9 @@ def check_auto_enable_lora_kernels(cls, data): if data.get("lora_o_kernel") is None: data["lora_o_kernel"] = True + if data.get("lora_embedding_kernel") is None: + data["lora_embedding_kernel"] = True + LOG.warning( "Auto-enabling LoRA kernel optimizations for faster training. " + "Please explicitly set `lora_*_kernel` config values to `false` to disable. " diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 8ff61b370a..c902d8703c 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -681,15 +681,7 @@ def check_lora_kernels_8bit(cls, data): @model_validator(mode="before") @classmethod def check_lora_kernels_dora(cls, data): - if ( - data.get("lora_mlp_kernel") - or data.get("lora_qkv_kernel") - or data.get("lora_o_kernel") - ) and data.get("peft_use_dora"): - raise ValueError( - "lora_mlp_kernel, lora_qkv_kernel, and lora_o_kernel are not " - "compatible with DoRA at the moment." - ) + # DoRA is now supported by lora kernels return data @model_validator(mode="before") diff --git a/tests/core/test_async_grpo.py b/tests/core/test_async_grpo.py index eb83be1b62..14c38df294 100644 --- a/tests/core/test_async_grpo.py +++ b/tests/core/test_async_grpo.py @@ -153,7 +153,7 @@ def test_non_fp8_weight_skips_scale_inv(self): proj.base_layer = base_layer - W, b, quant_state, A, B, s = get_lora_parameters(proj) + W, b, quant_state, A, B, s, *_ = get_lora_parameters(proj) # quant_state should be None since weight is bf16, not FP8 self.assertIsNone(quant_state) @@ -174,7 +174,7 @@ def test_fp8_weight_uses_scale_inv(self): scale_inv = torch.ones(1) base_layer.weight_scale_inv = scale_inv - W, b, quant_state, A, B, s = get_lora_parameters(proj) + W, b, quant_state, A, B, s, *_ = get_lora_parameters(proj) self.assertIs(quant_state, scale_inv) diff --git a/tests/e2e/kernels/test_lora.py b/tests/e2e/kernels/test_lora.py index 9baceb668b..5685245571 100644 --- a/tests/e2e/kernels/test_lora.py +++ b/tests/e2e/kernels/test_lora.py @@ -102,7 +102,7 @@ def __init__(self, in_features=64, out_features=128, rank=8): def test_get_lora_parameters(mock_proj): """Tests get_lora_parameters function""" # Test with LoRA enabled - W, b, _, A, B, s = get_lora_parameters(mock_proj) + W, b, _, A, B, s, *_ = get_lora_parameters(mock_proj) assert isinstance(W, torch.Tensor) assert W.shape == (128, 64) @@ -113,13 +113,13 @@ def test_get_lora_parameters(mock_proj): # Test with LoRA disabled mock_proj.disable_adapters = True - W, b, _, A, B, s = get_lora_parameters(mock_proj) + W, b, _, A, B, s, *_ = get_lora_parameters(mock_proj) assert A is None and B is None and s is None # Test with merged state mock_proj.disable_adapters = False mock_proj.merged = True - W, b, _, A, B, s = get_lora_parameters(mock_proj) + W, b, _, A, B, s, *_ = get_lora_parameters(mock_proj) assert A is None and B is None and s is None diff --git a/tests/e2e/kernels/test_lora_features.py b/tests/e2e/kernels/test_lora_features.py new file mode 100644 index 0000000000..80495c68db --- /dev/null +++ b/tests/e2e/kernels/test_lora_features.py @@ -0,0 +1,1245 @@ +""" +Tests for LoRA kernel correctness with bias, dropout, and DoRA support. + +Compares fused kernel outputs and gradients against PEFT's reference implementation. +""" + +import pytest +import torch +from peft import LoraConfig, get_peft_model +from torch import nn +from transformers import AutoConfig, AutoModelForCausalLM + +from axolotl.kernels.lora import ( + _compute_dora_scale, + apply_lora_mlp_swiglu, + apply_lora_o, + apply_lora_qkv, + get_lora_parameters, + matmul_lora, +) +from axolotl.monkeypatch.lora_kernels import ( + apply_lora_kernel_patches, + patch_self_attn_lora, +) +from axolotl.utils.dict import DictDefault + +MODEL_NAME = "Qwen/Qwen3-0.6B" +DEVICE = "cuda" +DTYPE = torch.bfloat16 + + +@pytest.fixture(scope="module") +def model_config(): + return AutoConfig.from_pretrained(MODEL_NAME) + + +def _make_peft_model( + lora_dropout=0.0, + bias="none", + use_dora=False, + target_modules=None, +): + """Create a PEFT model with given config.""" + if target_modules is None: + target_modules = [ + "q_proj", + "k_proj", + "v_proj", + "o_proj", + "gate_proj", + "up_proj", + "down_proj", + ] + model = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, + torch_dtype=DTYPE, + attn_implementation="eager", + ).to(DEVICE) + lora_config = LoraConfig( + r=8, + lora_alpha=16, + lora_dropout=lora_dropout, + bias=bias, + use_dora=use_dora, + target_modules=target_modules, + ) + peft_model = get_peft_model(model, lora_config) + return peft_model + + +def _get_layer(peft_model, layer_idx=0): + """Get a specific transformer layer from the model.""" + return peft_model.model.model.layers[layer_idx] + + +def _make_input(batch=2, seq_len=16, hidden_size=1024): + """Create random input tensor.""" + return torch.randn( + batch, seq_len, hidden_size, dtype=DTYPE, device=DEVICE, requires_grad=True + ) + + +def _compare_tensors(a, b, name="", atol=1e-2, rtol=1e-2): + """Compare two tensors with informative error messages.""" + if a is None and b is None: + return + assert a is not None and b is not None, f"{name}: one is None, other is not" + assert a.shape == b.shape, f"{name}: shape mismatch {a.shape} vs {b.shape}" + diff = (a - b).abs() + max_diff = diff.max().item() + mean_diff = diff.mean().item() + assert torch.allclose(a, b, atol=atol, rtol=rtol), ( + f"{name}: max_diff={max_diff:.6f}, mean_diff={mean_diff:.6f}" + ) + + +class TestGetLoraParameters: + """Test the extended get_lora_parameters function.""" + + def test_returns_9_values(self): + model = _make_peft_model() + layer = _get_layer(model) + params = get_lora_parameters(layer.self_attn.q_proj) + assert len(params) == 9 + W, b, quant, A, B, s, lora_bias, dropout, magnitude = params + assert W is not None + assert A is not None + assert B is not None + assert s is not None + assert lora_bias is None # bias="none" + assert dropout is not None # should be nn.Identity + assert magnitude is None # no DoRA + del model + + def test_with_bias(self): + """Qwen3 has no base bias, so PEFT doesn't add lora_bias even with bias='lora_only'. + This test verifies get_lora_parameters handles this correctly.""" + model = _make_peft_model(bias="lora_only") + layer = _get_layer(model) + params = get_lora_parameters(layer.self_attn.q_proj) + _, _, _, _, _, _, lora_bias, _, _ = params + # Qwen3 q_proj has no base bias, so PEFT sets lora_bias=False + assert lora_bias is None + del model + + def test_with_bias_on_biased_layer(self): + """Test with manually added bias to verify lora_bias extraction.""" + model = _make_peft_model(bias="lora_only") + layer = _get_layer(model) + q_proj = layer.self_attn.q_proj + adapter = q_proj.active_adapters[0] + # Manually add bias to lora_B to test extraction + old_B = q_proj.lora_B[adapter] + q_proj.lora_B[adapter] = torch.nn.Linear( + old_B.in_features, old_B.out_features, bias=True, device=DEVICE, dtype=DTYPE + ) + params = get_lora_parameters(q_proj) + _, _, _, _, _, _, lora_bias, _, _ = params + assert lora_bias is not None + assert lora_bias.shape[0] == old_B.out_features + del model + + def test_with_dropout(self): + model = _make_peft_model(lora_dropout=0.1) + layer = _get_layer(model) + params = get_lora_parameters(layer.self_attn.q_proj) + _, _, _, _, _, _, _, dropout, _ = params + assert dropout is not None + assert isinstance(dropout, nn.Dropout) + del model + + def test_with_dora(self): + model = _make_peft_model(use_dora=True) + layer = _get_layer(model) + params = get_lora_parameters(layer.self_attn.q_proj) + _, _, _, _, _, _, _, _, magnitude = params + assert magnitude is not None + del model + + +class TestMatmulLora: + """Test matmul_lora with new lora_bias and X_drop parameters.""" + + def test_basic(self): + X = torch.randn(4, 8, dtype=DTYPE, device=DEVICE) + W = torch.randn(16, 8, dtype=DTYPE, device=DEVICE) + A = torch.randn(4, 8, dtype=DTYPE, device=DEVICE) # [rank, in] + B = torch.randn(16, 4, dtype=DTYPE, device=DEVICE) # [out, rank] + s = 2.0 + + result = matmul_lora(X, W, None, None, A, B, s) + expected = X @ W.t() + s * X @ A.t() @ B.t() + _compare_tensors(result, expected, "basic matmul_lora") + + def test_with_lora_bias(self): + X = torch.randn(4, 8, dtype=DTYPE, device=DEVICE) + W = torch.randn(16, 8, dtype=DTYPE, device=DEVICE) + A = torch.randn(4, 8, dtype=DTYPE, device=DEVICE) + B = torch.randn(16, 4, dtype=DTYPE, device=DEVICE) + lora_bias = torch.randn(16, dtype=DTYPE, device=DEVICE) + s = 2.0 + + result = matmul_lora(X, W, None, None, A, B, s, lora_bias=lora_bias) + expected = X @ W.t() + s * X @ A.t() @ B.t() + s * lora_bias + _compare_tensors(result, expected, "matmul_lora with lora_bias") + + def test_with_x_drop(self): + X = torch.randn(4, 8, dtype=DTYPE, device=DEVICE) + X_drop = X * 0.5 # simulated dropout + W = torch.randn(16, 8, dtype=DTYPE, device=DEVICE) + A = torch.randn(4, 8, dtype=DTYPE, device=DEVICE) + B = torch.randn(16, 4, dtype=DTYPE, device=DEVICE) + s = 2.0 + + result = matmul_lora(X, W, None, None, A, B, s, X_drop=X_drop) + expected = X @ W.t() + s * X_drop @ A.t() @ B.t() + _compare_tensors(result, expected, "matmul_lora with X_drop") + + +class TestDoraScale: + """Test DoRA magnitude/norm scaling computation.""" + + def test_basic(self): + W = torch.randn(16, 8, dtype=DTYPE, device=DEVICE) + A = torch.randn(4, 8, dtype=DTYPE, device=DEVICE) + B = torch.randn(16, 4, dtype=DTYPE, device=DEVICE) + magnitude = torch.randn(16, dtype=DTYPE, device=DEVICE).abs() + 0.1 + s = 2.0 + + scale = _compute_dora_scale(W, None, A, B, s, magnitude, DTYPE) + + # Manual computation + combined = W + s * B @ A + weight_norm = torch.linalg.norm(combined, dim=1) + expected = magnitude / weight_norm + + _compare_tensors(scale, expected, "dora_scale") + + +# ============================================================ +# Integration tests: compare kernel outputs against PEFT reference +# ============================================================ + + +def _run_peft_qkv(layer, X): + """Run Q, K, V projections through PEFT's standard forward.""" + Q = layer.self_attn.q_proj(X) + K = layer.self_attn.k_proj(X) + V = layer.self_attn.v_proj(X) + return Q, K, V + + +def _run_kernel_qkv(layer, X): + """Run Q, K, V projections through our fused kernel.""" + return apply_lora_qkv(layer.self_attn, X, inplace=False) + + +def _run_peft_o(layer, X): + """Run O projection through PEFT's standard forward.""" + return layer.self_attn.o_proj(X) + + +def _run_kernel_o(layer, X): + """Run O projection through our fused kernel.""" + return apply_lora_o(layer.self_attn, X) + + +def _run_peft_mlp(layer, X): + """Run MLP through PEFT's standard forward.""" + return layer.mlp(X) + + +def _run_kernel_mlp(layer, X): + """Run MLP through our fused kernel.""" + return apply_lora_mlp_swiglu(layer.mlp, X, inplace=False) + + +class TestQKVKernel: + """Test LoRA_QKV kernel against PEFT reference.""" + + @pytest.mark.parametrize("bias", ["none", "lora_only"]) + def test_forward_bias(self, bias): + model = _make_peft_model(bias=bias) + model.eval() + layer = _get_layer(model) + X = _make_input(hidden_size=model.config.hidden_size) + + with torch.no_grad(): + peft_Q, peft_K, peft_V = _run_peft_qkv(layer, X) + kern_Q, kern_K, kern_V = _run_kernel_qkv(layer, X) + + _compare_tensors(kern_Q, peft_Q, f"QKV Q (bias={bias})") + _compare_tensors(kern_K, peft_K, f"QKV K (bias={bias})") + _compare_tensors(kern_V, peft_V, f"QKV V (bias={bias})") + del model + + def test_forward_dropout_eval(self): + """Dropout disabled in eval - should match exactly.""" + model = _make_peft_model(lora_dropout=0.1) + model.eval() + layer = _get_layer(model) + X = _make_input(hidden_size=model.config.hidden_size) + + with torch.no_grad(): + peft_Q, peft_K, peft_V = _run_peft_qkv(layer, X) + kern_Q, kern_K, kern_V = _run_kernel_qkv(layer, X) + + _compare_tensors(kern_Q, peft_Q, "QKV Q (dropout eval)") + _compare_tensors(kern_K, peft_K, "QKV K (dropout eval)") + _compare_tensors(kern_V, peft_V, "QKV V (dropout eval)") + del model + + def test_forward_dora(self): + model = _make_peft_model(use_dora=True) + model.eval() + layer = _get_layer(model) + X = _make_input(hidden_size=model.config.hidden_size) + + with torch.no_grad(): + peft_Q, peft_K, peft_V = _run_peft_qkv(layer, X) + kern_Q, kern_K, kern_V = _run_kernel_qkv(layer, X) + + _compare_tensors(kern_Q, peft_Q, "QKV Q (DoRA)") + _compare_tensors(kern_K, peft_K, "QKV K (DoRA)") + _compare_tensors(kern_V, peft_V, "QKV V (DoRA)") + del model + + def test_forward_dora_bias(self): + model = _make_peft_model(use_dora=True, bias="lora_only") + model.eval() + layer = _get_layer(model) + X = _make_input(hidden_size=model.config.hidden_size) + + with torch.no_grad(): + peft_Q, peft_K, peft_V = _run_peft_qkv(layer, X) + kern_Q, kern_K, kern_V = _run_kernel_qkv(layer, X) + + _compare_tensors(kern_Q, peft_Q, "QKV Q (DoRA+bias)") + _compare_tensors(kern_K, peft_K, "QKV K (DoRA+bias)") + _compare_tensors(kern_V, peft_V, "QKV V (DoRA+bias)") + del model + + @pytest.mark.parametrize("bias", ["none", "lora_only"]) + def test_backward_bias(self, bias): + """Test that gradients match between kernel and PEFT.""" + model = _make_peft_model(bias=bias) + model.train() + layer = _get_layer(model) + + # PEFT reference + X1 = _make_input(hidden_size=model.config.hidden_size) + pQ, pK, pV = _run_peft_qkv(layer, X1) + loss_peft = pQ.sum() + pK.sum() + pV.sum() + loss_peft.backward() + + peft_grads = {} + for name, param in layer.self_attn.named_parameters(): + if param.grad is not None: + peft_grads[name] = param.grad.clone() + layer.self_attn.zero_grad() + + # Kernel + X2 = X1.detach().clone().requires_grad_(True) + kQ, kK, kV = _run_kernel_qkv(layer, X2) + loss_kern = kQ.sum() + kK.sum() + kV.sum() + loss_kern.backward() + + kern_grads = {} + for name, param in layer.self_attn.named_parameters(): + if param.grad is not None: + kern_grads[name] = param.grad.clone() + layer.self_attn.zero_grad() + + # Compare LoRA parameter gradients + for name in peft_grads: + if "lora_" in name: + _compare_tensors( + kern_grads.get(name), + peft_grads[name], + f"grad {name} (bias={bias})", + atol=5e-2, + rtol=5e-2, + ) + del model + + def test_backward_dora(self): + """Test DoRA backward pass gradients.""" + model = _make_peft_model(use_dora=True) + model.train() + layer = _get_layer(model) + + X1 = _make_input(hidden_size=model.config.hidden_size) + pQ, pK, pV = _run_peft_qkv(layer, X1) + loss_peft = pQ.sum() + pK.sum() + pV.sum() + loss_peft.backward() + + peft_grads = {} + for name, param in layer.self_attn.named_parameters(): + if param.grad is not None: + peft_grads[name] = param.grad.clone() + layer.self_attn.zero_grad() + + X2 = X1.detach().clone().requires_grad_(True) + kQ, kK, kV = _run_kernel_qkv(layer, X2) + loss_kern = kQ.sum() + kK.sum() + kV.sum() + loss_kern.backward() + + kern_grads = {} + for name, param in layer.self_attn.named_parameters(): + if param.grad is not None: + kern_grads[name] = param.grad.clone() + layer.self_attn.zero_grad() + + for name in peft_grads: + if "lora_" in name or "magnitude" in name: + _compare_tensors( + kern_grads.get(name), + peft_grads[name], + f"grad {name} (DoRA)", + atol=5e-2, + rtol=5e-2, + ) + del model + + +class TestOKernel: + """Test LoRA_O kernel against PEFT reference.""" + + @staticmethod + def _o_input_dim(model): + """o_proj input is num_heads * head_dim (may differ from hidden_size with GQA).""" + cfg = model.config + text_cfg = cfg.get_text_config() if hasattr(cfg, "get_text_config") else cfg + return text_cfg.num_attention_heads * text_cfg.head_dim + + @pytest.mark.parametrize("bias", ["none", "lora_only"]) + def test_forward_bias(self, bias): + model = _make_peft_model(bias=bias) + model.eval() + layer = _get_layer(model) + X = _make_input(hidden_size=self._o_input_dim(model)) + + with torch.no_grad(): + peft_out = _run_peft_o(layer, X) + kern_out = _run_kernel_o(layer, X) + + _compare_tensors(kern_out, peft_out, f"O (bias={bias})") + del model + + def test_forward_dora(self): + model = _make_peft_model(use_dora=True) + model.eval() + layer = _get_layer(model) + X = _make_input(hidden_size=self._o_input_dim(model)) + + with torch.no_grad(): + peft_out = _run_peft_o(layer, X) + kern_out = _run_kernel_o(layer, X) + + _compare_tensors(kern_out, peft_out, "O (DoRA)") + del model + + @pytest.mark.parametrize("bias", ["none", "lora_only"]) + def test_backward_bias(self, bias): + model = _make_peft_model(bias=bias) + model.train() + layer = _get_layer(model) + + X1 = _make_input(hidden_size=self._o_input_dim(model)) + peft_out = _run_peft_o(layer, X1) + peft_out.sum().backward() + peft_grads = { + n: p.grad.clone() + for n, p in layer.self_attn.o_proj.named_parameters() + if p.grad is not None + } + layer.self_attn.o_proj.zero_grad() + + X2 = X1.detach().clone().requires_grad_(True) + kern_out = _run_kernel_o(layer, X2) + kern_out.sum().backward() + kern_grads = { + n: p.grad.clone() + for n, p in layer.self_attn.o_proj.named_parameters() + if p.grad is not None + } + layer.self_attn.o_proj.zero_grad() + + for name in peft_grads: + if "lora_" in name: + _compare_tensors( + kern_grads.get(name), + peft_grads[name], + f"O grad {name} (bias={bias})", + atol=5e-2, + rtol=5e-2, + ) + del model + + +class TestMLPKernel: + """Test LoRA_MLP kernel against PEFT reference.""" + + @pytest.mark.parametrize("bias", ["none", "lora_only"]) + def test_forward_bias(self, bias): + model = _make_peft_model(bias=bias) + model.eval() + layer = _get_layer(model) + X = _make_input(hidden_size=model.config.hidden_size) + + with torch.no_grad(): + peft_out = _run_peft_mlp(layer, X) + kern_out = _run_kernel_mlp(layer, X) + + _compare_tensors(kern_out, peft_out, f"MLP (bias={bias})") + del model + + def test_forward_dropout_eval(self): + model = _make_peft_model(lora_dropout=0.1) + model.eval() + layer = _get_layer(model) + X = _make_input(hidden_size=model.config.hidden_size) + + with torch.no_grad(): + peft_out = _run_peft_mlp(layer, X) + kern_out = _run_kernel_mlp(layer, X) + + _compare_tensors(kern_out, peft_out, "MLP (dropout eval)") + del model + + def test_forward_dora(self): + model = _make_peft_model(use_dora=True) + model.eval() + layer = _get_layer(model) + X = _make_input(hidden_size=model.config.hidden_size) + + with torch.no_grad(): + peft_out = _run_peft_mlp(layer, X) + kern_out = _run_kernel_mlp(layer, X) + + # Relaxed tolerance for MLP DoRA: 3 projections + activation + DoRA + # causes bf16 accumulation differences + _compare_tensors(kern_out, peft_out, "MLP (DoRA)", atol=0.3, rtol=0.05) + del model + + def test_forward_dora_bias(self): + model = _make_peft_model(use_dora=True, bias="lora_only") + model.eval() + layer = _get_layer(model) + X = _make_input(hidden_size=model.config.hidden_size) + + with torch.no_grad(): + peft_out = _run_peft_mlp(layer, X) + kern_out = _run_kernel_mlp(layer, X) + + _compare_tensors(kern_out, peft_out, "MLP (DoRA+bias)", atol=0.3, rtol=0.05) + del model + + @pytest.mark.parametrize("bias", ["none", "lora_only"]) + def test_backward_bias(self, bias): + model = _make_peft_model(bias=bias) + model.train() + layer = _get_layer(model) + hidden_size = model.config.hidden_size + + X1 = _make_input(hidden_size=hidden_size) + peft_out = _run_peft_mlp(layer, X1) + peft_out.sum().backward() + peft_grads = { + n: p.grad.clone() + for n, p in layer.mlp.named_parameters() + if p.grad is not None + } + layer.mlp.zero_grad() + + X2 = X1.detach().clone().requires_grad_(True) + kern_out = _run_kernel_mlp(layer, X2) + kern_out.sum().backward() + kern_grads = { + n: p.grad.clone() + for n, p in layer.mlp.named_parameters() + if p.grad is not None + } + layer.mlp.zero_grad() + + # MLP backward has longer chain (3 projections + activation) = more bf16 accumulation error + for name in peft_grads: + if "lora_" in name: + _compare_tensors( + kern_grads.get(name), + peft_grads[name], + f"MLP grad {name} (bias={bias})", + atol=0.5, + rtol=0.1, + ) + del model + + def test_backward_dora(self): + model = _make_peft_model(use_dora=True) + model.train() + layer = _get_layer(model) + + X1 = _make_input(hidden_size=model.config.hidden_size) + peft_out = _run_peft_mlp(layer, X1) + peft_out.sum().backward() + peft_grads = { + n: p.grad.clone() + for n, p in layer.mlp.named_parameters() + if p.grad is not None + } + layer.mlp.zero_grad() + + X2 = X1.detach().clone().requires_grad_(True) + kern_out = _run_kernel_mlp(layer, X2) + kern_out.sum().backward() + kern_grads = { + n: p.grad.clone() + for n, p in layer.mlp.named_parameters() + if p.grad is not None + } + layer.mlp.zero_grad() + + for name in peft_grads: + if "lora_" in name or "magnitude" in name: + _compare_tensors( + kern_grads.get(name), + peft_grads[name], + f"MLP grad {name} (DoRA)", + atol=0.5, + rtol=0.1, + ) + del model + + +class TestFullModelPatch: + """Test applying kernel patches to a full model.""" + + def test_patched_forward_basic(self): + """Test that patched model forward matches unpatched PEFT model (bias=none, no DoRA).""" + from peft import PeftModelForCausalLM + + base_model = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, + torch_dtype=DTYPE, + attn_implementation="eager", + ).to(DEVICE) + lora_config = LoraConfig( + r=8, + lora_alpha=16, + bias="none", + use_dora=False, + target_modules=[ + "q_proj", + "k_proj", + "v_proj", + "o_proj", + "gate_proj", + "up_proj", + "down_proj", + ], + ) + model = PeftModelForCausalLM(base_model, lora_config) + model.eval() + + # Get PEFT reference output + input_ids = torch.randint(0, 1000, (1, 32), device=DEVICE) + with torch.no_grad(): + peft_out = model(input_ids).logits + + # Apply kernel patches + cfg = DictDefault( + { + "base_model": MODEL_NAME, + "lora_qkv_kernel": True, + "lora_o_kernel": True, + "lora_mlp_kernel": True, + } + ) + patch_self_attn_lora(cfg) + apply_lora_kernel_patches(model, cfg) + + # Get kernel output + with torch.no_grad(): + kern_out = model(input_ids).logits + + _compare_tensors(kern_out, peft_out, "Full model (basic)", atol=5e-1, rtol=1e-1) + del model + + +class TestEmbeddingKernel: + """Test LoRA embedding kernel against PEFT reference.""" + + def _make_embedding_model(self, use_dora=False): + from peft import PeftModelForCausalLM + + model = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, + torch_dtype=DTYPE, + attn_implementation="eager", + ).to(DEVICE) + lora_config = LoraConfig( + r=8, + lora_alpha=16, + use_dora=use_dora, + target_modules=["embed_tokens"], + ) + return PeftModelForCausalLM(model, lora_config) + + def test_forward_basic(self): + from axolotl.kernels.lora import apply_lora_embedding + + model = self._make_embedding_model() + model.eval() + + embed = model.model.model.embed_tokens + input_ids = torch.randint(0, 1000, (2, 16), device=DEVICE) + + with torch.no_grad(): + peft_out = embed(input_ids) + kern_out = apply_lora_embedding(embed, input_ids) + + # Cast to same dtype for comparison (PEFT may return float32) + _compare_tensors(kern_out.to(peft_out.dtype), peft_out, "Embedding basic") + del model + + def test_forward_dora(self): + from axolotl.kernels.lora import apply_lora_embedding + + model = self._make_embedding_model(use_dora=True) + model.eval() + + embed = model.model.model.embed_tokens + input_ids = torch.randint(0, 1000, (2, 16), device=DEVICE) + + with torch.no_grad(): + peft_out = embed(input_ids) + kern_out = apply_lora_embedding(embed, input_ids) + + _compare_tensors( + kern_out.to(peft_out.dtype), peft_out, "Embedding DoRA", atol=0.3, rtol=0.05 + ) + del model + + def test_backward(self): + from axolotl.kernels.lora import apply_lora_embedding + + model = self._make_embedding_model() + model.train() + + embed = model.model.model.embed_tokens + input_ids = torch.randint(0, 1000, (2, 16), device=DEVICE) + + # PEFT reference + peft_out = embed(input_ids) + peft_out.sum().backward() + peft_grads = {} + for n, p in embed.named_parameters(): + if p.grad is not None and "lora" in n: + peft_grads[n] = p.grad.clone() + embed.zero_grad() + + # Kernel + kern_out = apply_lora_embedding(embed, input_ids) + kern_out.sum().backward() + kern_grads = {} + for n, p in embed.named_parameters(): + if p.grad is not None and "lora" in n: + kern_grads[n] = p.grad.clone() + embed.zero_grad() + + for name in peft_grads: + _compare_tensors( + kern_grads.get(name), + peft_grads[name], + f"Embedding grad {name}", + atol=5e-2, + rtol=5e-2, + ) + del model + + +class TestTiedEmbeddings: + """Test that tied embeddings work correctly with kernel patching.""" + + def test_tied_embed_and_lm_head(self): + """When both embed_tokens and lm_head have LoRA, PEFT unties them. + Verify patched model produces valid output (no crashes, finite values).""" + from peft import PeftModelForCausalLM + + base = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, + torch_dtype=DTYPE, + attn_implementation="eager", + ).to(DEVICE) + lora_config = LoraConfig( + r=8, + lora_alpha=16, + target_modules=[ + "embed_tokens", + "lm_head", + "q_proj", + "k_proj", + "v_proj", + "o_proj", + "gate_proj", + "up_proj", + "down_proj", + ], + ) + model = PeftModelForCausalLM(base, lora_config) + model.eval() + + cfg = DictDefault( + { + "base_model": MODEL_NAME, + "lora_qkv_kernel": True, + "lora_o_kernel": True, + "lora_mlp_kernel": True, + "lora_embedding_kernel": True, + } + ) + + # Apply all kernel patches (class + instance level) + patch_self_attn_lora(cfg) + apply_lora_kernel_patches(model, cfg) + + input_ids = torch.randint(0, 1000, (1, 32), device=DEVICE) + with torch.no_grad(): + out = model(input_ids).logits + + # Verify output is valid + assert out.shape == (1, 32, model.config.vocab_size) + assert torch.isfinite(out).all(), "Output contains non-finite values" + assert out.abs().max() > 0, "Output is all zeros" + + # Verify backward works + model.train() + out = model(input_ids).logits + out.sum().backward() + # Check that LoRA params got gradients + embed = model.model.model.embed_tokens + has_embed_grad = any( + p.grad is not None and p.grad.abs().sum() > 0 + for n, p in embed.named_parameters() + if "lora" in n + ) + assert has_embed_grad, "Embedding LoRA params got no gradients" + del model + + +class TestQuantizedModels: + """Test kernels with quantized base weights.""" + + def test_nf4_qlora_forward_backward(self): + """NF4 QLoRA with kernel patches.""" + from peft import PeftModelForCausalLM + from transformers import BitsAndBytesConfig + + bnb_config = BitsAndBytesConfig( + load_in_4bit=True, + bnb_4bit_quant_type="nf4", + bnb_4bit_compute_dtype=DTYPE, + bnb_4bit_use_double_quant=True, + ) + base = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, + quantization_config=bnb_config, + attn_implementation="eager", + ) + lora_config = LoraConfig( + r=8, + lora_alpha=16, + target_modules=[ + "q_proj", + "k_proj", + "v_proj", + "o_proj", + "gate_proj", + "up_proj", + "down_proj", + ], + ) + model = PeftModelForCausalLM(base, lora_config) + + cfg = DictDefault( + { + "base_model": MODEL_NAME, + "lora_qkv_kernel": True, + "lora_o_kernel": True, + "lora_mlp_kernel": True, + } + ) + patch_self_attn_lora(cfg) + apply_lora_kernel_patches(model, cfg) + model.train() + + ids = torch.randint(0, 1000, (1, 32), device=DEVICE) + out = model(ids).logits + assert torch.isfinite(out).all() + out.sum().backward() + has_grads = sum( + 1 for n, p in model.named_parameters() if p.grad is not None and "lora" in n + ) + assert has_grads > 0, "No LoRA gradients" + del model + + def test_nf4_single_quant(self): + """NF4 without double quantization.""" + from peft import PeftModelForCausalLM + from transformers import BitsAndBytesConfig + + bnb_config = BitsAndBytesConfig( + load_in_4bit=True, + bnb_4bit_quant_type="nf4", + bnb_4bit_compute_dtype=DTYPE, + ) + base = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, + quantization_config=bnb_config, + attn_implementation="eager", + ) + lora_config = LoraConfig( + r=8, + lora_alpha=16, + target_modules=[ + "q_proj", + "k_proj", + "v_proj", + "o_proj", + "gate_proj", + "up_proj", + "down_proj", + ], + ) + model = PeftModelForCausalLM(base, lora_config) + + cfg = DictDefault( + { + "base_model": MODEL_NAME, + "lora_qkv_kernel": True, + "lora_o_kernel": True, + "lora_mlp_kernel": True, + } + ) + patch_self_attn_lora(cfg) + apply_lora_kernel_patches(model, cfg) + model.train() + + ids = torch.randint(0, 1000, (1, 32), device=DEVICE) + out = model(ids).logits + assert torch.isfinite(out).all() + out.sum().backward() + has_grads = sum( + 1 for n, p in model.named_parameters() if p.grad is not None and "lora" in n + ) + assert has_grads > 0 + del model + + +class TestTritonDoRA: + """Test Triton DoRA kernel against reference implementation.""" + + def test_triton_dora_scale(self): + from axolotl.kernels.dora import triton_dora_scale + from axolotl.kernels.lora import _compute_dora_scale + + # Random weights matching Qwen3-1.7B dimensions + out_feat, in_feat, rank = 1024, 1024, 8 + W = torch.randn(out_feat, in_feat, dtype=DTYPE, device=DEVICE) + A = torch.randn(rank, in_feat, dtype=DTYPE, device=DEVICE) + B = torch.randn(out_feat, rank, dtype=DTYPE, device=DEVICE) + magnitude = torch.randn(out_feat, dtype=DTYPE, device=DEVICE).abs() + 0.1 + s = 2.0 + + # Clear cache to force recomputation + if hasattr(magnitude, "_dora_cache"): + del magnitude._dora_cache + + ref = _compute_dora_scale(W, None, A, B, s, magnitude, DTYPE) + tri = triton_dora_scale(W, None, A, B, s, magnitude, DTYPE) + + _compare_tensors(tri, ref, "Triton DoRA scale", atol=1e-2, rtol=1e-2) + + def test_triton_dora_scale_small(self): + """Test with K/V projection dimensions (smaller out_features).""" + from axolotl.kernels.dora import triton_dora_scale + from axolotl.kernels.lora import _compute_dora_scale + + out_feat, in_feat, rank = 128, 1024, 8 + W = torch.randn(out_feat, in_feat, dtype=DTYPE, device=DEVICE) + A = torch.randn(rank, in_feat, dtype=DTYPE, device=DEVICE) + B = torch.randn(out_feat, rank, dtype=DTYPE, device=DEVICE) + magnitude = torch.randn(out_feat, dtype=DTYPE, device=DEVICE).abs() + 0.1 + s = 2.0 + + if hasattr(magnitude, "_dora_cache"): + del magnitude._dora_cache + + ref = _compute_dora_scale(W, None, A, B, s, magnitude, DTYPE) + tri = triton_dora_scale(W, None, A, B, s, magnitude, DTYPE) + + _compare_tensors(tri, ref, "Triton DoRA scale (small)", atol=1e-2, rtol=1e-2) + + +# ============================================================ +# Regression tests for review fixes +# ============================================================ + + +class TestDoRAEmbeddingNoDoubleScale: + """Regression: DoRA embedding forward must save the pre-scaled combined + tensor, not the already-scaled result, so backward computes d_mag correctly.""" + + def test_dora_magnitude_gradient_magnitude(self): + """d_mag should be O(1) relative to the gradient, not O(mag_scale^2).""" + from peft import PeftModelForCausalLM + + from axolotl.kernels.lora import apply_lora_embedding + + base = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, + torch_dtype=DTYPE, + attn_implementation="eager", + ).to(DEVICE) + lora_config = LoraConfig( + r=8, + lora_alpha=16, + use_dora=True, + target_modules=["embed_tokens"], + ) + model = PeftModelForCausalLM(base, lora_config) + model.train() + + embed = model.model.model.embed_tokens + ids = torch.randint(0, 1000, (2, 16), device=DEVICE) + + # Run PEFT reference to get reference d_mag + peft_out = embed(ids) + peft_out.sum().backward() + peft_mag_grad = None + for n, p in embed.named_parameters(): + if "magnitude" in n and p.grad is not None: + peft_mag_grad = p.grad.clone() + embed.zero_grad() + + # Run kernel + kern_out = apply_lora_embedding(embed, ids) + kern_out.to(peft_out.dtype).sum().backward() + kern_mag_grad = None + for n, p in embed.named_parameters(): + if "magnitude" in n and p.grad is not None: + kern_mag_grad = p.grad.clone() + embed.zero_grad() + + assert peft_mag_grad is not None, "PEFT should produce magnitude gradients" + assert kern_mag_grad is not None, "Kernel should produce magnitude gradients" + + # Key check: gradients should be same order of magnitude + # Double-scaling would make kern_mag_grad ~mag_scale times too large + ratio = kern_mag_grad.abs().mean() / peft_mag_grad.abs().mean() + assert 0.5 < ratio < 2.0, ( + f"Magnitude gradient ratio kernel/peft = {ratio:.3f}, " + f"expected ~1.0 (double-scaling would give >> 1)" + ) + del model + + +class TestDoraCacheInvalidation: + """Regression: DoRA weight norm cache must invalidate after in-place + param updates (optimizer steps), not just pointer changes.""" + + def test_cache_invalidates_on_inplace_update(self): + W = torch.randn(64, 64, dtype=DTYPE, device=DEVICE) + A = torch.randn(8, 64, dtype=DTYPE, device=DEVICE) + B = torch.randn(64, 8, dtype=DTYPE, device=DEVICE) + magnitude = torch.randn(64, dtype=DTYPE, device=DEVICE).abs() + 0.1 + s = 2.0 + + # Clear any existing cache + if hasattr(magnitude, "_dora_cache"): + del magnitude._dora_cache + + # First call populates cache + result1 = _compute_dora_scale(W, None, A, B, s, magnitude, DTYPE) + + # Simulate optimizer in-place update (pointer stays same, content changes) + old_ptr = A.data_ptr() + A.data.add_(torch.randn_like(A) * 0.1) + assert A.data_ptr() == old_ptr, "Pointer should not change for in-place ops" + + # Second call must detect the change and recompute + result2 = _compute_dora_scale(W, None, A, B, s, magnitude, DTYPE) + + # Results should differ since A changed + assert not torch.allclose(result1, result2, atol=1e-4), ( + "DoRA scale should change after in-place param update — cache not invalidated!" + ) + + +class TestEmbeddingPaddingIdxGrad: + """Regression: custom embedding backward must zero out gradients at + padding_idx positions, matching F.embedding behavior.""" + + def test_padding_idx_gradient_is_zero(self): + from axolotl.kernels.lora import LoRA_Embedding + + vocab, hidden, rank = 100, 32, 4 + W = torch.randn( + vocab, hidden, dtype=torch.float32, device=DEVICE, requires_grad=False + ) + A = torch.randn( + rank, vocab, dtype=torch.float32, device=DEVICE, requires_grad=True + ) + B = torch.randn( + hidden, rank, dtype=torch.float32, device=DEVICE, requires_grad=True + ) + s = 2.0 + padding_idx = 0 + + # Input containing the padding token + x = torch.tensor([[padding_idx, 1, 2, padding_idx, 3]], device=DEVICE) + + out = LoRA_Embedding.apply( + x, + W, + A, + B, + s, + None, + padding_idx, + None, + 2.0, + False, + False, # max_norm, norm_type, scale_grad_by_freq, sparse + ) + out.sum().backward() + + # The gradient for A at the padding_idx column should be zero + # A is [rank, vocab], so A.grad[:, padding_idx] should be zero + assert A.grad is not None + pad_grad = A.grad[:, padding_idx] + assert torch.all(pad_grad == 0), ( + f"Gradient at padding_idx={padding_idx} should be zero, got {pad_grad}" + ) + + # Non-padding positions should have non-zero gradients + non_pad_grad = A.grad[:, 1] + assert non_pad_grad.abs().sum() > 0, "Non-padding gradients should be non-zero" + + +class TestEmbeddingScaleGradByFreq: + """Regression: custom embedding backward must scale gradients by + inverse frequency when scale_grad_by_freq=True.""" + + def test_repeated_tokens_get_scaled_gradients(self): + from axolotl.kernels.lora import LoRA_Embedding + + vocab, hidden, rank = 100, 32, 4 + W = torch.randn( + vocab, hidden, dtype=torch.float32, device=DEVICE, requires_grad=False + ) + + # Run WITHOUT scale_grad_by_freq + A1 = torch.randn( + rank, vocab, dtype=torch.float32, device=DEVICE, requires_grad=True + ) + B1 = torch.randn( + hidden, rank, dtype=torch.float32, device=DEVICE, requires_grad=True + ) + # Token 5 appears 3 times + x = torch.tensor([[5, 5, 5, 10, 20]], device=DEVICE) + + out1 = LoRA_Embedding.apply( + x, + W, + A1, + B1, + 2.0, + None, + None, + None, + 2.0, + False, + False, + ) + out1.sum().backward() + grad_no_scale = A1.grad[:, 5].clone() + + # Run WITH scale_grad_by_freq + A2 = A1.data.clone().requires_grad_(True) + B2 = B1.data.clone().requires_grad_(True) + out2 = LoRA_Embedding.apply( + x, + W, + A2, + B2, + 2.0, + None, + None, + None, + 2.0, + True, + False, + ) + out2.sum().backward() + grad_with_scale = A2.grad[:, 5].clone() + + # With scale_grad_by_freq, token 5 (count=3) should have grad / 3 + expected_ratio = 1.0 / 3.0 + actual_ratio = grad_with_scale.abs().mean() / grad_no_scale.abs().mean() + assert abs(actual_ratio - expected_ratio) < 0.01, ( + f"scale_grad_by_freq ratio for count=3 token: expected {expected_ratio:.3f}, " + f"got {actual_ratio:.3f}" + ) + + +class TestEmbeddingDropoutNotAppliedToBase: + """Regression: embedding dropout must NOT be applied to the base embedding + output — PEFT's Embedding.forward does not use lora_dropout.""" + + def test_kernel_matches_peft_with_dropout_config(self): + """Even with lora_dropout>0, embedding output should match PEFT exactly.""" + from peft import PeftModelForCausalLM + + from axolotl.kernels.lora import apply_lora_embedding + + base = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, + torch_dtype=DTYPE, + attn_implementation="eager", + ).to(DEVICE) + lora_config = LoraConfig( + r=8, + lora_alpha=16, + lora_dropout=0.5, # high dropout + target_modules=["embed_tokens"], + ) + model = PeftModelForCausalLM(base, lora_config) + model.train() # training mode — dropout would be active if applied + + embed = model.model.model.embed_tokens + ids = torch.randint(0, 1000, (2, 16), device=DEVICE) + + # Run both multiple times — if dropout were applied, results would vary + with torch.no_grad(): + peft_out = embed(ids) + kern1 = apply_lora_embedding(embed, ids) + kern2 = apply_lora_embedding(embed, ids) + + # Kernel should be deterministic (no dropout) + _compare_tensors( + kern1.to(peft_out.dtype), + kern2.to(peft_out.dtype), + "Embedding deterministic (no dropout)", + atol=0, + rtol=0, + ) + + # And should match PEFT + _compare_tensors( + kern1.to(peft_out.dtype), + peft_out, + "Embedding matches PEFT with dropout config", + ) + del model diff --git a/tests/e2e/multigpu/test_fsdp2_lora_kernels.py b/tests/e2e/multigpu/test_fsdp2_lora_kernels.py new file mode 100644 index 0000000000..27ad2b8e95 --- /dev/null +++ b/tests/e2e/multigpu/test_fsdp2_lora_kernels.py @@ -0,0 +1,120 @@ +"""Test LoRA kernels under FSDP2 multi-GPU training. + +Verifies that lora_qkv_kernel, lora_o_kernel, lora_mlp_kernel, and +lora_embedding_kernel work correctly with FSDP2 sharding, including +with bias, dropout, and DoRA enabled. +""" + +from pathlib import Path + +import yaml +from accelerate.test_utils import execute_subprocess_async +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import require_torch_2_7_0 + +AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent + + +def _run_training(temp_dir, cfg): + """Write config and launch multi-GPU training.""" + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ] + ) + + +def _base_lora_fsdp2_config(temp_dir, **overrides): + """Base config for LoRA + FSDP2 + kernel tests.""" + cfg = { + "base_model": "Qwen/Qwen3-0.6B", + "sequence_len": 512, + "val_set_size": 0.0, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:1%]", + }, + ], + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_target_linear": True, + "num_epochs": 1, + "max_steps": 3, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 1e-4, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "bf16": True, + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": False, + "transformer_layer_cls_to_wrap": "Qwen3DecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + # Enable all LoRA kernels + "lora_mlp_kernel": True, + "lora_qkv_kernel": True, + "lora_o_kernel": True, + "lora_embedding_kernel": True, + "save_safetensors": True, + } + cfg.update(overrides) + return DictDefault(cfg) + + +class TestFSDP2LoRAKernels: + """Test LoRA kernels under FSDP2.""" + + @require_torch_2_7_0 + def test_lora_kernels_basic(self, temp_dir): + """Basic LoRA + kernels + FSDP2: no dropout, no bias, no DoRA.""" + cfg = _base_lora_fsdp2_config(temp_dir) + _run_training(temp_dir, cfg) + assert (Path(temp_dir) / "adapter_model.safetensors").exists() + + @require_torch_2_7_0 + def test_lora_kernels_with_dropout(self, temp_dir): + """LoRA kernels + dropout + FSDP2.""" + cfg = _base_lora_fsdp2_config(temp_dir, lora_dropout=0.1) + _run_training(temp_dir, cfg) + assert (Path(temp_dir) / "adapter_model.safetensors").exists() + + @require_torch_2_7_0 + def test_lora_kernels_with_dora(self, temp_dir): + """LoRA kernels + DoRA + FSDP2.""" + cfg = _base_lora_fsdp2_config(temp_dir, peft_use_dora=True) + _run_training(temp_dir, cfg) + assert (Path(temp_dir) / "adapter_model.safetensors").exists() + + @require_torch_2_7_0 + def test_lora_kernels_with_dora_and_dropout(self, temp_dir): + """LoRA kernels + DoRA + dropout + FSDP2.""" + cfg = _base_lora_fsdp2_config( + temp_dir, + peft_use_dora=True, + lora_dropout=0.05, + ) + _run_training(temp_dir, cfg) + assert (Path(temp_dir) / "adapter_model.safetensors").exists() diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index 73f8838588..2865a80f91 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -222,9 +222,9 @@ def test_model_specific_activation(model_name, expected_activation): def test_kernel_patch_conditions(): - """Test various conditions that should prevent kernel patching.""" + """Test that kernels ARE patched even with dropout and bias (now supported).""" test_configs = [ - # Dropout prevents patching + # Dropout — kernels now support this { "peft_type": "LORA", "task_type": "CAUSAL_LM", @@ -234,7 +234,7 @@ def test_kernel_patch_conditions(): "lora_dropout": 0.1, "bias": "none", }, - # Bias prevents patching + # Bias — kernels now support this { "peft_type": "LORA", "task_type": "CAUSAL_LM", @@ -252,13 +252,14 @@ def test_kernel_patch_conditions(): model = PeftModelForCausalLM(model, peft_config) cfg = DictDefault({"lora_mlp_kernel": True}) - # Should not patch patched_model = apply_lora_kernel_patches(model, cfg) layer = patched_model.model.model.layers[0].mlp - # Verify no patches applied - assert layer.forward.__func__ is not apply_lora_mlp_swiglu - assert layer.forward.__func__ is not apply_lora_mlp_geglu + # Verify patches ARE applied (dropout and bias are now supported) + assert ( + layer.forward.__func__ is apply_lora_mlp_swiglu + or layer.forward.__func__ is apply_lora_mlp_geglu + ) def test_kernel_config_options(): @@ -511,7 +512,7 @@ def test_kernel_training_integration_auto_enable(temp_dir): def test_kernel_training_integration_dropout_non_zero(temp_dir): - """Test model loading with dropout non-zero should not patch.""" + """Test model loading with dropout non-zero DOES patch (now supported).""" from axolotl.cli.utils import load_model_and_tokenizer @@ -546,31 +547,18 @@ def test_kernel_training_integration_dropout_non_zero(temp_dir): # Load config cfg = load_cfg(str(path)) - # Get original attention class - attention_cls = get_attention_cls_from_config(cfg) - - # Store original state before patching - original_forward_method = attention_cls.forward - # Load model model, tokenizer, _ = load_model_and_tokenizer(cfg=cfg) - # We call modelloader as that's where the patches are applied - # despite the fact that we're not using it to load the model model_loader = ModelLoader(cfg, tokenizer) - # Apply patch + # Apply patches — should succeed even with dropout > 0 model_loader.patch_manager._apply_self_attention_lora_patch() - - # Verify patch was not applied - assert attention_cls.forward == original_forward_method - - # Apply apply_lora_kernel_patches model_loader.patch_manager._apply_lora_kernel_patch(model) - # Verify patch was not applied + # Verify patches WERE applied (dropout is now supported by kernels) layers = get_layers(model) for layer in layers: for self_attn in find_self_attn_in_layer(layer): - assert not hasattr(self_attn, "apply_qkv") - assert not hasattr(self_attn, "apply_o") + assert hasattr(self_attn, "apply_qkv") + assert hasattr(self_attn, "apply_o") diff --git a/tests/utils/lora/test_config_validation_lora.py b/tests/utils/lora/test_config_validation_lora.py index 9d97288b63..45a848c651 100644 --- a/tests/utils/lora/test_config_validation_lora.py +++ b/tests/utils/lora/test_config_validation_lora.py @@ -28,20 +28,22 @@ def test_basic_configuration_validation(self): result = validate_config(valid_config) assert result["adapter"] == "lora" - with pytest.raises(ValueError, match="not compatible with DoRA"): - invalid_config = DictDefault( - { - "adapter": "lora", - "lora_mlp_kernel": True, - "peft_use_dora": True, - "datasets": [{"path": "dummy_dataset", "type": "alpaca"}], - "micro_batch_size": 1, - "gradient_accumulation_steps": 1, - "learning_rate": 1e-5, - "base_model": "dummy_model", - } - ) - validate_config(invalid_config) + # DoRA is now compatible with lora kernels + dora_kernel_config = DictDefault( + { + "adapter": "lora", + "lora_mlp_kernel": True, + "peft_use_dora": True, + "datasets": [{"path": "dummy_dataset", "type": "alpaca"}], + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "learning_rate": 1e-5, + "base_model": "dummy_model", + } + ) + result = validate_config(dora_kernel_config) + assert result["lora_mlp_kernel"] is True + assert result["peft_use_dora"] is True def test_qlora_4bit_validation(self): """Test QLoRA 4-bit configuration validation""" diff --git a/tests/utils/lora/test_freeze_lora.py b/tests/utils/lora/test_freeze_lora.py index da90c18264..7c5ec8fb39 100644 --- a/tests/utils/lora/test_freeze_lora.py +++ b/tests/utils/lora/test_freeze_lora.py @@ -38,6 +38,11 @@ def create_mock_lora_layer( mock_layer.lora_A["default"].weight = torch.randn(16, 256, dtype=self.dtype) mock_layer.lora_B["default"].weight = torch.randn(512, 16, dtype=self.dtype) + mock_layer.lora_B["default"].bias = None + + # Required by get_lora_parameters for dropout/DoRA extraction + mock_layer.lora_dropout = {} + mock_layer.lora_magnitude_vector = None else: mock_layer.weight = base_layer.weight mock_layer.bias = base_layer.bias @@ -48,7 +53,7 @@ def test_parameter_freezing_adapters_disabled(self): """Test that LoRA parameters are None when adapters are disabled.""" layer = self.create_mock_lora_layer(has_adapters=True, adapters_disabled=True) - W, b, quant_state, A, B, s = get_lora_parameters(layer) + W, b, quant_state, A, B, s, *_ = get_lora_parameters(layer) # Base parameters should be returned assert W is not None @@ -62,7 +67,7 @@ def test_parameter_freezing_adapters_merged(self): """Test that LoRA parameters are None when adapters are merged.""" layer = self.create_mock_lora_layer(has_adapters=True, merged=True) - W, b, quant_state, A, B, s = get_lora_parameters(layer) + W, b, quant_state, A, B, s, *_ = get_lora_parameters(layer) # Base parameters should be returned assert W is not None @@ -77,7 +82,7 @@ def test_parameter_freezing_no_adapters(self): """Test parameter behavior when no adapters are present.""" layer = self.create_mock_lora_layer(has_adapters=False) - W, b, quant_state, A, B, s = get_lora_parameters(layer) + W, b, quant_state, A, B, s, *_ = get_lora_parameters(layer) # Base parameters should be returned assert W is not None @@ -94,7 +99,7 @@ def test_parameter_active_adapters_enabled(self): has_adapters=True, adapters_disabled=False, merged=False ) - W, b, quant_state, A, B, s = get_lora_parameters(layer) + W, b, quant_state, A, B, s, *_ = get_lora_parameters(layer) # All parameters should be returned assert W is not None @@ -110,7 +115,7 @@ def test_parameter_shapes_consistency(self): has_adapters=True, adapters_disabled=False, merged=False ) - W, b, quant_state, A, B, s = get_lora_parameters(layer) + W, b, quant_state, A, B, s, *_ = get_lora_parameters(layer) # Check shape consistency assert W.shape == (512, 256) @@ -124,7 +129,7 @@ def test_parameter_dtypes_consistency(self): has_adapters=True, adapters_disabled=False, merged=False ) - W, b, quant_state, A, B, s = get_lora_parameters(layer) + W, b, quant_state, A, B, s, *_ = get_lora_parameters(layer) assert W.dtype == self.dtype assert b.dtype == self.dtype @@ -138,7 +143,7 @@ def test_quantization_state_handling(self): quant_state_mock = Mock() layer.base_layer.weight.quant_state = quant_state_mock - W, b, quant_state, A, B, s = get_lora_parameters(layer) + W, b, quant_state, A, B, s, *_ = get_lora_parameters(layer) assert quant_state == quant_state_mock @@ -157,7 +162,7 @@ def test_multiple_adapters_active_adapter_selection(self): layer.active_adapters = ["adapter2"] - W, b, quant_state, A, B, s = get_lora_parameters(layer) + W, b, quant_state, A, B, s, *_ = get_lora_parameters(layer) assert s == 0.2 assert torch.equal(A, layer.lora_A["adapter2"].weight) @@ -192,13 +197,13 @@ def forward(self, x): model = get_peft_model(base_model, lora_config) lora_layer = model.base_model.model.linear # Test with adapters enabled - W, b, quant_state, A, B, s = get_lora_parameters(lora_layer) + W, b, quant_state, A, B, s, *_ = get_lora_parameters(lora_layer) assert A is not None assert B is not None assert s is not None # Test with adapters disabled model.disable_adapter_layers() - W, b, quant_state, A, B, s = get_lora_parameters(lora_layer) + W, b, quant_state, A, B, s, *_ = get_lora_parameters(lora_layer) assert A is None assert B is None assert s is None From 0e583efeaa29dc584344d03820afda6612fb6fd2 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 22 Mar 2026 13:54:03 -0400 Subject: [PATCH 1220/1405] increase rtol, codecov informational only, don't silently fail errors w curl (#3534) [skip ci] --- cicd/cicd.sh | 3 ++- codecov.yml | 1 + tests/e2e/utils.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 2f0af7e260..7fc4da5564 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -3,7 +3,8 @@ set -e python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__" -curl --silent -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst | tar -xpf - -C "${HF_HOME}/hub/" --use-compress-program unzstd --strip-components=1 +set -o pipefail +curl --silent --show-error --fail --retry 3 --retry-delay 5 -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst | tar -xpf - -C "${HF_HOME}/hub/" --use-compress-program unzstd --strip-components=1 # hf download "NousResearch/Meta-Llama-3-8B" # hf download "NousResearch/Meta-Llama-3-8B-Instruct" # hf download "microsoft/Phi-4-reasoning" diff --git a/codecov.yml b/codecov.yml index fa3ad30731..a876dd59d0 100644 --- a/codecov.yml +++ b/codecov.yml @@ -37,6 +37,7 @@ coverage: only_pulls: false flags: null paths: null + informational: true parsers: gcov: diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 268311295d..e1eaca050d 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -179,7 +179,7 @@ def check_tensorboard( tag: str, lt_val: float, assertion_err: str, - rtol: float = 0.02, + rtol: float = 0.05, gt_zero: bool = True, ) -> None: """ From 86be9f329e46281f9a5a93ea1585908f3ae95981 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 23 Mar 2026 02:26:10 -0400 Subject: [PATCH 1221/1405] post merge lora fixes for CI (#3536) [skip ci] * post merge lora fixes for CI * handle lora kernel auto-enable for moe without grouped_mm * prefer not to import torch in schema validation --- src/axolotl/utils/schemas/config.py | 33 +++++++++++++++++++++++++ tests/e2e/kernels/test_lora.py | 37 ++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 2f269b78e8..34fd9ba2c8 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1385,6 +1385,39 @@ def check_auto_enable_lora_kernels(cls, data): if data.get("trust_remote_code"): return data + # Skip auto-enable for MoE models when native grouped_mm is unavailable + # (torch < 2.9). The grouped_mm fallback in transformers uses torch.mm + # with out= which bypasses autocast and fails on mixed dtypes during eval. + env_capabilities = data.get("env_capabilities", {}) + torch_version = env_capabilities.get("torch_version") + if torch_version is None: + import torch + + torch_version = str(torch.__version__).split("+", maxsplit=1)[0] + has_grouped_mm = version.parse(torch_version) >= version.parse("2.9.0") + if not has_grouped_mm: + is_moe = False + model_type = data.get("model_config_type", "") + if model_type and "moe" in model_type.lower(): + is_moe = True + if not is_moe: + try: + from transformers import AutoConfig + + base_model = data.get("base_model") + if base_model: + auto_cfg = AutoConfig.from_pretrained( + base_model, trust_remote_code=False + ) + if getattr(auto_cfg, "num_local_experts", None) or getattr( + auto_cfg, "num_experts", None + ): + is_moe = True + except Exception: # pylint: disable=broad-exception-caught + pass + if is_moe: + return data + # Check multi-GPU compatibility capabilities = data.get("capabilities") is_multi_gpu = capabilities and capabilities.get("n_gpu", 0) > 1 diff --git a/tests/e2e/kernels/test_lora.py b/tests/e2e/kernels/test_lora.py index 5685245571..10850bdc85 100644 --- a/tests/e2e/kernels/test_lora.py +++ b/tests/e2e/kernels/test_lora.py @@ -176,24 +176,31 @@ def test_lora_mlp_direct(sample_tensors, activation_forward, activation_backward X.requires_grad = True output = LoRA_MLP.apply( X, + None, # X_drop gate_proj.weight, gate_proj.bias, None, # gate_quant None, # gate_A None, # gate_B None, # gate_scale + None, # gate_lora_bias + None, # gate_magnitude up_proj.weight, up_proj.bias, None, # up_quant None, # up_A None, # up_B None, # up_scale + None, # up_lora_bias + None, # up_magnitude down_proj.weight, down_proj.bias, None, # down_quant None, # down_A None, # down_B None, # down_scale + None, # down_lora_bias + None, # down_magnitude activation_forward, activation_backward, True, # inplace @@ -247,24 +254,31 @@ def test_lora_mlp_with_adapters( # Forward pass with adapters output = LoRA_MLP.apply( X, + None, # X_drop gate_proj.weight, gate_proj.bias, None, gate_A, gate_B, scale, + None, # gate_lora_bias + None, # gate_magnitude up_proj.weight, up_proj.bias, None, up_A, up_B, scale, + None, # up_lora_bias + None, # up_magnitude down_proj.weight, down_proj.bias, None, down_A, down_B, scale, + None, # down_lora_bias + None, # down_magnitude activation_forward, activation_backward, True, @@ -334,25 +348,32 @@ def test_lora_qkv(sample_tensors): Q1, K1, V1 = LoRA_QKV.apply( X, + None, # X_drop q_weight, None, None, None, None, None, + None, + None, # Q: weight, bias, quant, A, B, scale, lora_bias, magnitude k_weight, None, None, None, None, None, + None, + None, # K v_weight, None, None, None, None, None, - True, + None, + None, # V + True, # inplace ) assert Q1.shape == K1.shape == V1.shape == X.shape @@ -366,25 +387,32 @@ def test_lora_qkv(sample_tensors): # Test with LoRA adapters Q2, K2, V2 = LoRA_QKV.apply( X, + None, # X_drop q_weight, None, None, q_A, q_B, scale, + None, + None, # Q k_weight, None, None, k_A, k_B, scale, + None, + None, # K v_weight, None, None, v_A, v_B, scale, - True, + None, + None, # V + True, # inplace ) assert Q2.shape == K2.shape == V2.shape == X.shape @@ -427,7 +455,9 @@ def test_lora_o(sample_tensors): # Test forward pass X.requires_grad = True - output = LoRA_O.apply(X, W, b, None, A, B, scale) + output = LoRA_O.apply( + X, None, W, b, None, A, B, scale, None, None + ) # X_drop, ..., lora_bias, magnitude assert output.shape == (X.shape[0], X.shape[1], W.shape[0]) @@ -542,6 +572,7 @@ def test_inplace_operations(sample_tensors, apply_function): "down_proj": nn.Linear(shapes["out"], shapes["hidden"]).to( device="cuda", dtype=torch.float16 ), + "training": False, }, ) From e412370877e49ca053570e15f35dba0f60d63ce5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 24 Mar 2026 15:40:05 -0400 Subject: [PATCH 1222/1405] roundup_power2_divisions not needed with newer pytorch versions (#3540) * roundup_power2_divisions not needed with newer pytorch versions * remove typo * update qwen3.5 moe 35b-a3b yaml for 5090 * more bug fixes * fix tests to match updated trainer * don't use fa2 for hooks test * reset plugins on the instance * retry download * fix references to renamed axolotl_cfg property on trainer * Fix ref to trainer cfg --- .github/workflows/tests.yml | 24 ++++++------- cicd/cicd.sh | 12 ++++++- examples/qwen3.5/35b-a3b-moe-qlora.yaml | 36 ++++++++++++------- setup.py | 2 +- src/axolotl/cli/checks.py | 3 +- .../integrations/diffusion/callbacks.py | 8 +++-- src/axolotl/integrations/diffusion/plugin.py | 4 ++- src/axolotl/integrations/diffusion/trainer.py | 29 ++++++++------- src/axolotl/utils/__init__.py | 5 +-- tests/conftest.py | 13 +++++++ tests/e2e/test_diffusion.py | 4 ++- tests/integrations/test_diffusion.py | 8 ++--- tests/integrations/test_diffusion_callback.py | 2 +- tests/integrations/test_swanlab.py | 10 +++--- 14 files changed, 100 insertions(+), 60 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9df249270e..5099e447cf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -68,13 +68,13 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.12"] # TODO include py3.14 once https://github.com/mistralai/mistral-common/pull/194 is merged + python_version: ["3.12", "3.14"] pytorch_version: ["2.8.0", "2.9.1", "2.10.0"] -# exclude: -# - python_version: "3.14" -# pytorch_version: "2.8.0" -# - python_version: "3.14" -# pytorch_version: "2.9.1" + exclude: + - python_version: "3.14" + pytorch_version: "2.8.0" + - python_version: "3.14" + pytorch_version: "2.9.1" timeout-minutes: 20 steps: @@ -164,13 +164,13 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.12"] # TODO include py3.14 once https://github.com/mistralai/mistral-common/pull/194 is merged + python_version: ["3.12", "3.14"] pytorch_version: ["2.8.0", "2.9.1", "2.10.0"] -# exclude: -# - python_version: "3.14" -# pytorch_version: "2.8.0" -# - python_version: "3.14" -# pytorch_version: "2.9.1" + exclude: + - python_version: "3.14" + pytorch_version: "2.8.0" + - python_version: "3.14" + pytorch_version: "2.9.1" timeout-minutes: 30 steps: diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 7fc4da5564..a3f17472a5 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -4,7 +4,17 @@ set -e python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__" set -o pipefail -curl --silent --show-error --fail --retry 3 --retry-delay 5 -L https://axolotl-ci.b-cdn.net/hf-cache.tar.zst | tar -xpf - -C "${HF_HOME}/hub/" --use-compress-program unzstd --strip-components=1 +for i in 1 2 3; do + if curl --silent --show-error --fail -L \ + https://axolotl-ci.b-cdn.net/hf-cache.tar.zst \ + | tar -xpf - -C "${HF_HOME}/hub/" --use-compress-program unzstd --strip-components=1; then + echo "HF cache extracted successfully" + break + fi + echo "Attempt $i failed, cleaning up and retrying in 15s..." + rm -rf "${HF_HOME}/hub/"* + sleep 15 +done # hf download "NousResearch/Meta-Llama-3-8B" # hf download "NousResearch/Meta-Llama-3-8B-Instruct" # hf download "microsoft/Phi-4-reasoning" diff --git a/examples/qwen3.5/35b-a3b-moe-qlora.yaml b/examples/qwen3.5/35b-a3b-moe-qlora.yaml index dea45801ca..14b50703e2 100644 --- a/examples/qwen3.5/35b-a3b-moe-qlora.yaml +++ b/examples/qwen3.5/35b-a3b-moe-qlora.yaml @@ -1,8 +1,18 @@ -base_model: Qwen/Qwen3.5-35B-A3B +base_model: Qwen/Qwen3.5-35B-A3B-Base plugins: - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin -strict: false + - axolotl.integrations.kernels.KernelsPlugin + - axolotl.integrations.liger.LigerPlugin +use_kernels: true +use_scattermoe: true +liger_layer_norm: true +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_rms_norm_gated: true + +torch_compile: false chat_template: qwen3_5 datasets: @@ -13,6 +23,7 @@ datasets: message_property_mappings: role: from content: value + val_set_size: 0.0 output_dir: ./outputs/out dataset_prepared_path: last_run_prepared @@ -36,9 +47,13 @@ lora_target_modules: # lora_target_modules: 'model\.(language_model\.)?layers\.[\d]+\.(mlp|self_attn)\.(shared_expert\.)?(up|down|gate|gate_up|q|k|v|o)_proj' # Target experts -# lora_target_parameters: -# - mlp.experts.gate_up_proj -# - mlp.experts.down_proj +lora_target_parameters: + - mlp.experts.gate_up_proj + - mlp.experts.down_proj + +lora_qkv_kernel: true +lora_o_kernel: true +lora_mlp_kernel: false wandb_project: wandb_entity: @@ -47,22 +62,17 @@ wandb_name: wandb_log_model: gradient_accumulation_steps: 2 -micro_batch_size: 1 +micro_batch_size: 4 num_epochs: 1 -optimizer: adamw_torch_4bit +optimizer: adamw_torch_8bit lr_scheduler: cosine learning_rate: 0.0002 bf16: auto tf32: true -lora_mlp_kernel: false -lora_qkv_kernel: false -lora_o_kernel: false - gradient_checkpointing: true -gradient_checkpointing_kwargs: - use_reentrant: false +activation_offloading: true resume_from_checkpoint: logging_steps: 1 flash_attention: true diff --git a/setup.py b/setup.py index 71e5abe3d3..d4e1894f79 100644 --- a/setup.py +++ b/setup.py @@ -89,7 +89,7 @@ def parse_requirements(extras_require_map): ] if not install_xformers: _install_requires.pop(_install_requires.index(xformers_version)) - extras_require_map["vllm"] = ["vllm==0.17.1"] + extras_require_map["vllm"] = ["vllm>=0.17.1"] elif (major, minor) >= (2, 9): extras_require_map.pop("fbgemm-gpu") extras_require_map["fbgemm-gpu"] = [ diff --git a/src/axolotl/cli/checks.py b/src/axolotl/cli/checks.py index c79cff9f5d..4d1ab63e2d 100644 --- a/src/axolotl/cli/checks.py +++ b/src/axolotl/cli/checks.py @@ -4,6 +4,7 @@ from pathlib import Path import httpcore +import httpx from accelerate.commands.config import config_args from huggingface_hub import HfApi from huggingface_hub.utils import LocalTokenNotFoundError @@ -48,7 +49,7 @@ def check_user_token() -> bool: "Error verifying HuggingFace token. Remember to log in using `hf auth login` and get your access token from https://huggingface.co/settings/tokens if you want to use gated models or datasets." ) return False - except (HTTPError, httpcore.ConnectError): + except (HTTPError, httpcore.ConnectError, httpx.ConnectError): LOG.warning( "Error accessing HuggingFace. This may be due to a network issue or rate limiting." ) diff --git a/src/axolotl/integrations/diffusion/callbacks.py b/src/axolotl/integrations/diffusion/callbacks.py index 18a64023b6..5f5ff34002 100644 --- a/src/axolotl/integrations/diffusion/callbacks.py +++ b/src/axolotl/integrations/diffusion/callbacks.py @@ -36,7 +36,9 @@ def on_step_end( """Generate samples at specified intervals.""" if ( state.global_step > 0 - and state.global_step % self.trainer.cfg.diffusion.generation_interval == 0 + and state.global_step + % self.trainer.axolotl_cfg.diffusion.generation_interval + == 0 ): if not self.trainer.state.is_world_process_zero: return @@ -52,7 +54,7 @@ def on_step_end( dataloader = self.trainer.get_train_dataloader() # Generate samples - diffusion_cfg = self.trainer.cfg.diffusion + diffusion_cfg = self.trainer.axolotl_cfg.diffusion samples = generate_samples( model=self.trainer.model, tokenizer=self.trainer.processing_class, @@ -142,7 +144,7 @@ def _log_samples(self, samples: list, step: int): logger.info("=" * 60) - if self.trainer.cfg.use_wandb: + if self.trainer.axolotl_cfg.use_wandb: if wandb.run is not None: wandb.log( { diff --git a/src/axolotl/integrations/diffusion/plugin.py b/src/axolotl/integrations/diffusion/plugin.py index c31f48b03a..cbfc2bde36 100644 --- a/src/axolotl/integrations/diffusion/plugin.py +++ b/src/axolotl/integrations/diffusion/plugin.py @@ -38,4 +38,6 @@ def get_trainer_cls(self, cfg: DictDefault) -> type[DiffusionTrainer] | None: def post_trainer_create(self, cfg: DictDefault, trainer: DiffusionTrainer): """Configure trainer after creation.""" - trainer.set_config(cfg) + if hasattr(trainer, "axolotl_cfg"): + trainer.axolotl_cfg = cfg + trainer.post_set_axolotl_cfg() diff --git a/src/axolotl/integrations/diffusion/trainer.py b/src/axolotl/integrations/diffusion/trainer.py index dfaef2a48c..7e35616f3d 100644 --- a/src/axolotl/integrations/diffusion/trainer.py +++ b/src/axolotl/integrations/diffusion/trainer.py @@ -7,7 +7,6 @@ from torch import nn from axolotl.core.trainers.base import AxolotlTrainer -from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger from .callbacks import DiffusionGenerationCallback @@ -21,19 +20,17 @@ class DiffusionTrainer(AxolotlTrainer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.cfg = None self._special_token_ids = None - def set_config(self, config: DictDefault): + def post_set_axolotl_cfg(self): """Set config for diffusion training.""" - self.cfg = config self._cache_special_token_ids() self._resolve_mask_token_id() - token_id = int(getattr(self.cfg.diffusion, "mask_token_id", 0)) + token_id = int(getattr(self.axolotl_cfg.diffusion, "mask_token_id", 0)) LOG.info(f"Diffusion: using mask_token_id={token_id}") - if getattr(config.diffusion, "generate_samples", True): + if getattr(self.axolotl_cfg.diffusion, "generate_samples", True): generation_callback = DiffusionGenerationCallback(self) self.add_callback(generation_callback) @@ -41,18 +38,20 @@ def _resolve_mask_token_id(self) -> None: """Ensure mask_token_id is valid for the current tokenizer.""" from .utils import resolve_mask_token_id + assert self.axolotl_cfg is not None, "axolotl_cfg is not set yet" + tokenizer = getattr(self, "processing_class", None) if tokenizer is None: return mid = resolve_mask_token_id( tokenizer, - self.cfg, + self.axolotl_cfg, allow_add=True, model=getattr(self, "model", None), ) try: - self.cfg.diffusion.mask_token_id = int(mid) + self.axolotl_cfg.diffusion.mask_token_id = int(mid) except Exception: pass @@ -150,7 +149,7 @@ def _forward_process( masked_indices = masked_indices & answer_mask # Create masked input - mask_token_id = int(self.cfg.diffusion.mask_token_id) + mask_token_id = int(self.axolotl_cfg.diffusion.mask_token_id) mask_value = torch.full_like(input_ids, mask_token_id) noisy_batch = torch.where(masked_indices, mask_value, input_ids) @@ -194,12 +193,12 @@ def _compute_diffusion_loss( # Apply forward process noisy_batch, masked_indices, p_mask = self._forward_process( - input_ids, attention_mask, labels, self.cfg.diffusion.eps + input_ids, attention_mask, labels, self.axolotl_cfg.diffusion.eps ) # Create bidirectional attention mask bidirectional_mask = create_bidirectional_attention_mask( - input_ids, attention_mask, sample_packing=self.cfg.sample_packing + input_ids, attention_mask, sample_packing=self.axolotl_cfg.sample_packing ) # Forward pass @@ -222,7 +221,7 @@ def _compute_diffusion_loss( masked_logits.float(), masked_targets, reduction="none" ) - if self.cfg.diffusion.importance_weighting: + if self.axolotl_cfg.diffusion.importance_weighting: masked_p_mask = masked_p_mask.float() weighted_loss = token_loss / masked_p_mask else: @@ -251,7 +250,7 @@ def _compute_diffusion_loss( # Non-SFT: when importance weighting is enabled, use unbiased estimator # (sum(loss/p) / total_tokens). Otherwise, average over masked tokens # for stable scaling across varying mask ratios. - if self.cfg.diffusion.importance_weighting: + if self.axolotl_cfg.diffusion.importance_weighting: loss = weighted_loss.sum() / ( input_ids.shape[0] * input_ids.shape[1] ) @@ -283,7 +282,7 @@ def _compute_diffusion_loss( } # If doing SFT training, log answer-specific metrics - if self.cfg.datasets is not None: + if self.axolotl_cfg.datasets is not None: with torch.no_grad(): answer_mask = labels != -100 answer_lengths = answer_mask.sum(dim=1).float() # type: ignore @@ -292,7 +291,7 @@ def _compute_diffusion_loss( metrics["answer_ratio"] = total_answer_tokens / max(total_tokens, 1) metrics["avg_answer_length"] = answer_lengths.mean().item() - if self.cfg.diffusion.importance_weighting: + if self.axolotl_cfg.diffusion.importance_weighting: metrics["importance_weight_avg"] = (1.0 / masked_p_mask).mean().item() train_eval: Literal["train", "eval"] = "train" if model.training else "eval" diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index 96ac29bd01..476f3b2e93 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -48,7 +48,8 @@ def set_pytorch_cuda_alloc_conf(): """Set up CUDA allocation config""" torch_version = torch.__version__.split(".") torch_major, torch_minor = int(torch_version[0]), int(torch_version[1]) - config_value = "expandable_segments:True,roundup_power2_divisions:16" + config_value = "expandable_segments:True" + config_older_suffix = ",roundup_power2_divisions:16" if ( torch_major == 2 and torch_minor >= 9 @@ -60,7 +61,7 @@ def set_pytorch_cuda_alloc_conf(): and torch_minor >= 2 and os.getenv("PYTORCH_CUDA_ALLOC_CONF") is None ): - os.environ["PYTORCH_CUDA_ALLOC_CONF"] = config_value + os.environ["PYTORCH_CUDA_ALLOC_CONF"] = config_value + config_older_suffix def set_misc_env(): diff --git a/tests/conftest.py b/tests/conftest.py index 054f6de02b..f857dd3636 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ """Shared pytest fixtures""" +import collections import functools import importlib import logging @@ -473,6 +474,18 @@ def temp_dir() -> Generator[str, None, None]: shutil.rmtree(_temp_dir) +@pytest.fixture(scope="function", autouse=True) +def reset_plugin_manager(): + from axolotl.integrations.base import PluginManager + + yield + PluginManager._cfg = None + # Don't reset _instance to None — module-level PLUGIN_MANAGER references + # in train.py, model.py, etc. would become stale + if PluginManager._instance is not None: + PluginManager._instance.plugins = collections.OrderedDict() + + @pytest.fixture(scope="function", autouse=True) def torch_manual_seed(): torch.manual_seed(42) diff --git a/tests/e2e/test_diffusion.py b/tests/e2e/test_diffusion.py index 89d35adc1c..048a3aaf33 100644 --- a/tests/e2e/test_diffusion.py +++ b/tests/e2e/test_diffusion.py @@ -2,7 +2,7 @@ from axolotl.common.datasets import load_datasets from axolotl.train import train -from axolotl.utils.config import normalize_config, validate_config +from axolotl.utils.config import normalize_config, prepare_plugins, validate_config from axolotl.utils.dict import DictDefault from tests.e2e.utils import check_model_output_exists @@ -62,6 +62,7 @@ def test_diffusion_smoke_test(self, temp_dir): } ) + prepare_plugins(cfg) cfg = validate_config(cfg) normalize_config(cfg) dataset_meta = load_datasets(cfg=cfg) @@ -119,6 +120,7 @@ def test_diffusion_sft_labels(self, temp_dir): } ) + prepare_plugins(cfg) cfg = validate_config(cfg) normalize_config(cfg) dataset_meta = load_datasets(cfg=cfg) diff --git a/tests/integrations/test_diffusion.py b/tests/integrations/test_diffusion.py index 141d8d1503..432552724c 100644 --- a/tests/integrations/test_diffusion.py +++ b/tests/integrations/test_diffusion.py @@ -42,7 +42,7 @@ def diffusion_trainer_instance(mock_tokenizer, diffusion_config): """Create a diffusion trainer instance for testing methods directly.""" # Create a minimal trainer instance just for testing methods trainer = object.__new__(DiffusionTrainer) # Bypass __init__ - trainer.cfg = diffusion_config + trainer.axolotl_cfg = diffusion_config trainer._special_token_ids = {0, 1, 2} # pad, bos, eos trainer.processing_class = mock_tokenizer trainer.store_metrics = Mock() # Mock metrics storage @@ -70,7 +70,7 @@ def test_forward_process_basic(self, diffusion_trainer_instance): assert not masked_indices[special_token_positions].any() # Check that mask token is applied - mask_token_id = diffusion_trainer_instance.cfg.diffusion.mask_token_id + mask_token_id = diffusion_trainer_instance.axolotl_cfg.diffusion.mask_token_id masked_positions = masked_indices if masked_positions.any(): assert (noisy_batch[masked_positions] == mask_token_id).all() @@ -132,7 +132,7 @@ def test_bidirectional_attention_mask_with_packing( self, diffusion_trainer_instance ): """Test bidirectional attention mask with sample packing.""" - diffusion_trainer_instance.cfg.sample_packing = True + diffusion_trainer_instance.axolotl_cfg.sample_packing = True input_ids = torch.tensor([[1, 10, 20, 30, 40, 2]], dtype=torch.long) # Sample IDs: first sample (1), second sample (2) attention_mask = torch.tensor([[1, 1, 1, 2, 2, 2]], dtype=torch.long) @@ -184,7 +184,7 @@ def test_compute_loss_sft(self, diffusion_trainer_instance): mock_outputs.logits = torch.randn(1, seq_len, vocab_size, requires_grad=True) mock_model.return_value = mock_outputs mock_model.training = True - diffusion_trainer_instance.cfg.datasets = Mock() + diffusion_trainer_instance.axolotl_cfg.datasets = Mock() input_ids = torch.tensor([[1, 10, 20, 30, 2]], dtype=torch.long) labels = torch.tensor([[-100, -100, 20, 30, 2]], dtype=torch.long) diff --git a/tests/integrations/test_diffusion_callback.py b/tests/integrations/test_diffusion_callback.py index 3e8785fe04..8ba6ac5a13 100644 --- a/tests/integrations/test_diffusion_callback.py +++ b/tests/integrations/test_diffusion_callback.py @@ -13,7 +13,7 @@ class DummyTrainer: def __init__(self, use_eval: bool): # Config used by callback - self.cfg = SimpleNamespace( + self.axolotl_cfg = SimpleNamespace( diffusion=SimpleNamespace( generation_interval=1, num_generation_samples=1, diff --git a/tests/integrations/test_swanlab.py b/tests/integrations/test_swanlab.py index e672658e65..7158f8d7ea 100644 --- a/tests/integrations/test_swanlab.py +++ b/tests/integrations/test_swanlab.py @@ -1176,7 +1176,7 @@ def test_profiling_context_logs_duration(self): # Mock trainer with SwanLab enabled mock_trainer = MagicMock() - mock_trainer.cfg = MagicMock(use_swanlab=True) + mock_trainer.axolotl_cfg = MagicMock(use_swanlab=True) mock_trainer.__class__.__name__ = "TestTrainer" with patch("swanlab.get_run") as mock_get_run, patch("swanlab.log") as mock_log: @@ -1199,7 +1199,7 @@ def test_profiling_context_skips_when_swanlab_disabled(self): from axolotl.integrations.swanlab.profiling import swanlab_profiling_context mock_trainer = MagicMock() - mock_trainer.cfg = MagicMock(use_swanlab=False) # Disabled + mock_trainer.axolotl_cfg = MagicMock(use_swanlab=False) # Disabled with patch("swanlab.log") as mock_log: with swanlab_profiling_context(mock_trainer, "test_function"): @@ -1213,7 +1213,7 @@ def test_profiling_context_skips_when_swanlab_not_initialized(self): from axolotl.integrations.swanlab.profiling import swanlab_profiling_context mock_trainer = MagicMock() - mock_trainer.cfg = MagicMock(use_swanlab=True) + mock_trainer.axolotl_cfg = MagicMock(use_swanlab=True) with ( patch("swanlab.get_run", return_value=None), @@ -1294,7 +1294,7 @@ def test_profiling_context_advanced(self): ) mock_trainer = MagicMock() - mock_trainer.cfg = MagicMock(use_swanlab=True) + mock_trainer.axolotl_cfg = MagicMock(use_swanlab=True) mock_trainer.__class__.__name__ = "TestTrainer" # Config that filters out very fast operations @@ -1320,7 +1320,7 @@ def test_profiling_with_exception(self): from axolotl.integrations.swanlab.profiling import swanlab_profiling_context mock_trainer = MagicMock() - mock_trainer.cfg = MagicMock(use_swanlab=True) + mock_trainer.axolotl_cfg = MagicMock(use_swanlab=True) mock_trainer.__class__.__name__ = "TestTrainer" with patch("swanlab.get_run") as mock_get_run, patch("swanlab.log") as mock_log: From e9883c91d4f19d3511a908fe787a15b6c547142a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 24 Mar 2026 16:43:43 -0400 Subject: [PATCH 1223/1405] fix: robust handling of race condition on patching check (#3543) [skip ci] --- .../monkeypatch/transformers/trainer_loss_calc.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py b/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py index 3a99d01154..7704ceddc3 100644 --- a/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py +++ b/src/axolotl/monkeypatch/transformers/trainer_loss_calc.py @@ -37,7 +37,10 @@ def check_evaluation_loop_is_patchable() -> bool: - evaluation_loop_source = inspect.getsource(Trainer.evaluation_loop) + if hasattr(Trainer, "_original_evaluation_loop"): + evaluation_loop_source = Trainer._original_evaluation_loop + else: + evaluation_loop_source = inspect.getsource(Trainer.evaluation_loop) return all(value in evaluation_loop_source for value in ORIGINAL_EVAL_CODE.values()) @@ -53,7 +56,7 @@ def patch_evaluation_loop(): evaluation_loop_source = inspect.getsource(Trainer.evaluation_loop) except OSError: return - Trainer.evaluation = evaluation_loop_source + Trainer._original_evaluation_loop = evaluation_loop_source evaluation_loop_source, _ = detab_code(evaluation_loop_source) # Apply the nanmean patches @@ -93,7 +96,10 @@ def patch_evaluation_loop(): def check_maybe_log_save_evaluate_is_patchable() -> bool: - maybe_log_source = inspect.getsource(Trainer._maybe_log_save_evaluate) + if hasattr(Trainer, "_original_maybe_log_save_evaluate"): + maybe_log_source = Trainer._original_maybe_log_save_evaluate + else: + maybe_log_source = inspect.getsource(Trainer._maybe_log_save_evaluate) return ORIGINAL_MAYBE_CODE in maybe_log_source From c50c4acbf42b2816cea34a4dc18067bea4fd20b8 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 24 Mar 2026 18:43:46 -0400 Subject: [PATCH 1224/1405] EBFT: Matching Features, Not Tokens: Energy-Based Fine-Tuning of Language Models (#3527) [skip ci] * EBFT wip * fixes * more fixeS * add missing strided module * ebft fixes for multi-turn * make ebft work with async * add example for ebft w qwen3.5 * fix for split thinking and update yaml for lora over linear attention only * enforce_eager for vllm arg in schema * fix sync weights * fix multi-gpu * handle updated sig for mm * ddp fixes * improve multi-gpu handling, don't calculate logits, adaptive completion length * chore: lint * chore: lint * support completion_mean * Address corereview feedback * clamp min IS ratio * Address PR code review * more fixes identified * address code review * Fix property from rebase conflict --- docker/Dockerfile-cloud-uv | 1 + examples/ebft/README.md | 214 +++ examples/ebft/ebft_opencode.py | 28 + examples/ebft/ebft_pretrain.py | 31 + examples/ebft/ebft_strided_structured.py | 80 ++ .../ebft/llama-1b-ebft-opencode-novllm.yaml | 64 + examples/ebft/llama-1b-ebft-opencode.yaml | 81 ++ .../llama-1b-ebft-strided-structured.yaml | 65 + examples/ebft/llama-1b-ebft-strided.yaml | 60 + examples/ebft/llama-3b-ebft-strided-fft.yaml | 69 + examples/ebft/llama-8b-ebft-strided-fft.yaml | 58 + .../ebft/qwen35-4b-ebft-structured-async.yaml | 86 ++ examples/ebft/qwen35-4b-ebft-structured.yaml | 77 ++ examples/ebft/qwen35-9b-ebft-structured.yaml | 82 ++ src/axolotl/cli/vllm_serve.py | 23 +- src/axolotl/common/datasets.py | 2 +- src/axolotl/core/builders/rl.py | 14 +- src/axolotl/core/trainers/__init__.py | 2 + src/axolotl/core/trainers/ebft/__init__.py | 213 +++ src/axolotl/core/trainers/ebft/args.py | 133 ++ src/axolotl/core/trainers/ebft/kernels.py | 308 +++++ src/axolotl/core/trainers/ebft/rewards.py | 264 ++++ src/axolotl/core/trainers/ebft/strided.py | 1152 +++++++++++++++++ src/axolotl/core/trainers/ebft/trainer.py | 531 ++++++++ .../core/trainers/grpo/async_trainer.py | 205 ++- .../integrations/diffusion/callbacks.py | 6 +- src/axolotl/monkeypatch/trainer/trl_vllm.py | 117 +- .../prompt_strategies/ebft/__init__.py | 9 + .../ebft/ebft_chat_multiturn.py | 129 ++ .../prompt_strategies/ebft/ebft_opencode.py | 20 + .../prompt_strategies/ebft/ebft_reasoning.py | 319 +++++ .../ebft/ebft_strided_chat.py | 110 ++ .../ebft/ebft_strided_structured.py | 80 ++ src/axolotl/scripts/vllm_serve_lora.py | 77 +- src/axolotl/scripts/vllm_worker_ext.py | 52 +- src/axolotl/train.py | 8 +- src/axolotl/utils/callbacks/__init__.py | 29 +- src/axolotl/utils/callbacks/generation.py | 58 +- src/axolotl/utils/data/rl.py | 21 +- src/axolotl/utils/schemas/config.py | 121 +- src/axolotl/utils/schemas/enums.py | 1 + src/axolotl/utils/schemas/trl.py | 16 +- src/axolotl/utils/schemas/validation.py | 119 ++ src/axolotl/utils/schemas/vllm.py | 7 + src/axolotl/utils/weight_serde.py | 94 ++ tests/test_ebft_kernels.py | 294 +++++ tests/test_ebft_strided_structured.py | 363 ++++++ tests/test_http_weight_sync.py | 158 +++ 48 files changed, 5884 insertions(+), 167 deletions(-) create mode 100644 examples/ebft/README.md create mode 100644 examples/ebft/ebft_opencode.py create mode 100644 examples/ebft/ebft_pretrain.py create mode 100644 examples/ebft/ebft_strided_structured.py create mode 100644 examples/ebft/llama-1b-ebft-opencode-novllm.yaml create mode 100644 examples/ebft/llama-1b-ebft-opencode.yaml create mode 100644 examples/ebft/llama-1b-ebft-strided-structured.yaml create mode 100644 examples/ebft/llama-1b-ebft-strided.yaml create mode 100644 examples/ebft/llama-3b-ebft-strided-fft.yaml create mode 100644 examples/ebft/llama-8b-ebft-strided-fft.yaml create mode 100644 examples/ebft/qwen35-4b-ebft-structured-async.yaml create mode 100644 examples/ebft/qwen35-4b-ebft-structured.yaml create mode 100644 examples/ebft/qwen35-9b-ebft-structured.yaml create mode 100644 src/axolotl/core/trainers/ebft/__init__.py create mode 100644 src/axolotl/core/trainers/ebft/args.py create mode 100644 src/axolotl/core/trainers/ebft/kernels.py create mode 100644 src/axolotl/core/trainers/ebft/rewards.py create mode 100644 src/axolotl/core/trainers/ebft/strided.py create mode 100644 src/axolotl/core/trainers/ebft/trainer.py create mode 100644 src/axolotl/prompt_strategies/ebft/__init__.py create mode 100644 src/axolotl/prompt_strategies/ebft/ebft_chat_multiturn.py create mode 100644 src/axolotl/prompt_strategies/ebft/ebft_opencode.py create mode 100644 src/axolotl/prompt_strategies/ebft/ebft_reasoning.py create mode 100644 src/axolotl/prompt_strategies/ebft/ebft_strided_chat.py create mode 100644 src/axolotl/prompt_strategies/ebft/ebft_strided_structured.py create mode 100644 src/axolotl/utils/weight_serde.py create mode 100644 tests/test_ebft_kernels.py create mode 100644 tests/test_ebft_strided_structured.py create mode 100644 tests/test_http_weight_sync.py diff --git a/docker/Dockerfile-cloud-uv b/docker/Dockerfile-cloud-uv index a53dd61355..2facb6fa79 100644 --- a/docker/Dockerfile-cloud-uv +++ b/docker/Dockerfile-cloud-uv @@ -22,6 +22,7 @@ RUN apt update && \ chmod 700 ~/.ssh && \ printf "\n[[ -z \"\$TMUX\" ]] && { tmux attach-session -t ssh_tmux || tmux new-session -s ssh_tmux; exit; }\n" >> ~/.bashrc && \ printf "[ ! -z \"\$TERM\" -a -r /etc/motd ] && cat /etc/motd\n" >> ~/.bashrc && \ + printf "source /workspace/axolotl-venv/bin/activate\n" >> ~/.bashrc && \ chmod +x /workspace/axolotl/scripts/cloud-entrypoint.sh && \ chmod +x /root/cloud-entrypoint.sh && \ echo 'set-option -g history-limit 5000' >> ~/.tmux.conf diff --git a/examples/ebft/README.md b/examples/ebft/README.md new file mode 100644 index 0000000000..533e13652d --- /dev/null +++ b/examples/ebft/README.md @@ -0,0 +1,214 @@ +# Energy-Based Fine-Tuning (EBFT) + +EBFT is an integration of ["Matching Features, Not Tokens: Energy-Based Fine-Tuning of Language Models"](https://arxiv.org/abs/2603.12248) (Jelassi et al., 2026) into axolotl. + +## Overview + +EBFT fine-tunes language models by optimizing a **feature-matching loss** rather than relying on external reward functions or verifiers. A frozen copy of the model (the "feature network") extracts embeddings from both generated and ground-truth completions, and the generator is updated via REINFORCE to match the ground-truth feature moments. + +**Key advantages over SFT:** +- Operates on model rollouts (not teacher forcing), reducing distribution shift +- Provides dense sequence-level supervision without a task-specific verifier +- Improves both downstream accuracy and validation cross-entropy simultaneously + +**Key advantages over RLVR:** +- No reward model or verifier required — works on any (prompt, completion) data +- Applicable to non-verifiable tasks (e.g., raw code, translation, creative writing) +- Maintains distributional calibration (low feature-matching loss) + +## Two Modes + +EBFT supports two modes depending on your data format: + +### Structured Mode (`mode: structured`, default) +For **QA/instruction data** with prompt + completion pairs (e.g., OpenCodeInstruct, ALMA translation). +- Extends GRPOTrainer — uses vLLM for fast rollout generation +- RLOO advantages and clipped policy gradient from GRPO +- Feature-matching rewards replace external reward functions + +### Strided Mode (`mode: strided`) +For **unstructured text** without prompt/completion splits (e.g., raw code, prose, SwallowCode). +- Uses **strided block-parallel generation** — multiple short rollouts at different anchor points within a document +- No vLLM needed — generation uses custom strided attention masks +- Uses **torch flex_attention** with compiled block masks for efficient fused attention kernels (~2x faster than eager attention) +- Compatible with gradient checkpointing via automatic dtype normalization +- This is the core EBFT algorithm from the paper (Section F) + +### Common to both modes: +- **Frozen feature network** — deep copy of the model at initialization (frozen, eval mode) +- **Feature extraction** — hidden states at configurable layer depths (default: 25%, 50%, 75%), L2-normalized per layer before concatenation +- **Feature-matching rewards** — cosine similarity (alignment) minus pairwise dot-product (diversity), scaled by 2 per paper equation (7) +- **SVD whitening** — decorrelates feature dimensions; the paper shows removing it causes the largest degradation +- **CFM loss tracking** — conditional feature-matching loss (paper eq 2) logged as `ebft/cfm_loss` +- **FSDP2 compatible** — feature network stays outside FSDP wrapping (frozen, inference-only) + +## Quick Start + +### Structured Mode (QA data + vLLM) + +```bash +# 1. Start vLLM server +python -m trl.scripts.vllm_serve \ + --model meta-llama/Llama-3.2-1B \ + --host 0.0.0.0 --port 8000 \ + --gpu-memory-utilization 0.3 + +# 2. Train +axolotl train examples/ebft/llama-1b-ebft-opencode.yaml +``` + +### Strided Mode (unstructured text) + +```bash +# No vLLM needed — strided generation is built-in +axolotl train examples/ebft/llama-3b-ebft-strided-fft.yaml +``` + +## Configuration + +### Common EBFT Settings + +```yaml +rl: ebft + +ebft: + # Feature network: which layers to extract hidden states from + # Values are fractions of total depth (0.0 = embedding, 1.0 = final layer) + feature_layers: [0.25, 0.5, 0.75] + + # How to pool per-token hidden states into sequence embeddings + # Options: "last_token" (recommended), "mean_pooling", "concat" + embed_method: last_token + + # SVD whitening — strongly recommended (paper shows largest degradation without it) + use_whitening: true + + # Reward = alignment_coef * alignment - diversity_coef * diversity + # Per paper Variant (i) (eq 49): alignment uses cosine similarity (normalized), + # diversity uses raw dot product — both are bounded after whitening. + alignment_coef: 1.0 + diversity_coef: 1.0 + + # Cross-entropy loss on ground-truth tokens (mixed objective, paper Section 2.1) + # 0.0 = pure feature matching; 0.03 = recommended balance; 0.1 = CE-dominated + ce_coef: 0.0 +``` + +### Strided Mode Settings + +```yaml +ebft: + mode: strided + stride: 8 # tokens between anchor points (paper default: 8) + context_length: 8 # context window per block (paper default: 8) + generate_max_len: 8 # tokens generated per block (paper default: 8) + n_samples_per_prompt: 4 # independent rollouts per document (>= 2 for RLOO) + temperature: 0.6 + rl_coef: 1.0 # RL loss weight + advantage_estimator: rloo # rloo (recommended), group_norm, or reinforce +``` + +### Structured Mode Settings (via TRL) + +```yaml +trl: + num_generations: 4 # samples per prompt + max_completion_length: 256 # max tokens to generate + temperature: 1.0 + use_vllm: true + scale_rewards: true + loss_type: grpo + epsilon: 0.2 +``` + +### Dataset Format + +**Structured mode** — QA data with prompt + ground-truth completion: +```yaml +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_opencode.transform +``` +Transform returns: `{"prompt": ..., "ground_truth": ...}` + +**Strided mode** — raw text tokenized to fixed length: +```yaml +datasets: + - path: sjelassi/swallow_code_20m + type: ebft_pretrain.transform +``` +Transform returns: `{"input_ids": ..., "attention_mask": ..., "labels": ...}` + +## How It Works + +### Structured Mode +1. **Generate**: For each prompt, generate `num_generations` completions via vLLM +2. **Extract features**: Forward both generated and ground-truth sequences through the frozen feature network +3. **Compute rewards**: `2 * alignment - 2 * diversity` (paper eq 7) +4. **RLOO advantages**: subtract leave-one-out group mean +5. **Policy gradient**: clipped PPO-style loss + +### Strided Mode +1. **Anchor selection**: Pick `num_blocks = (seq_len - gen_len - ctx_len) / stride + 1` anchor points across the document +2. **Block-parallel generation**: At each anchor, generate `gen_len` tokens using a custom strided attention mask via `flex_attention` compiled block masks +3. **Feature extraction**: Forward the full sequence (prompt + generated) through the frozen feature network **with the strided attention mask** — this is critical for correct feature representations +4. **Per-block rewards**: + - **Alignment** = `2 * cosine_similarity(gen_block_emb, gt_block_emb)` — normalized, bounded in [-2, 2] + - **Diversity** = `2 * mean_pairwise_dot_product(gen_block_embs)` — raw dot product on whitened vectors + - **Reward** = `alignment_coef * alignment - diversity_coef * diversity` +5. **RLOO advantages**: leave-one-out baseline across `n_samples_per_prompt` rollouts per block +6. **Policy gradient**: REINFORCE loss on generated tokens, weighted by per-block advantages + +### Tracked Metrics + +| Metric | Description | +|--------|-------------| +| `ebft/alignment` | Mean cosine similarity between generated and GT features (higher = better) | +| `ebft/diversity` | Mean pairwise similarity between samples (lower = more diverse) | +| `ebft/mean_reward` | alignment - diversity (should trend upward) | +| `ebft/cfm_loss` | Conditional feature-matching loss ‖E[φ(ŷ)] - φ(y)‖² (paper eq 2, lower = better) | +| `ebft/rl_loss` | REINFORCE policy gradient loss | +| `ebft/ce_loss` | Cross-entropy loss on ground-truth tokens (when `ce_coef > 0`) | +| `ebft/advantages_std` | RLOO advantage standard deviation (should be non-zero) | + +## Tips and Recommendations + +### Reward coefficients +- **`use_whitening: true`**: Strongly recommended. The paper's ablation (Figure 7) shows removing whitening causes the largest performance degradation. Safe to use with `diversity_coef > 0`. +- **`diversity_coef`**: Default 1.0. Per the paper's Variant (i) (eq 49), alignment uses cosine similarity while diversity uses raw dot product. After whitening, both are bounded and on compatible scales. +- **`n_samples_per_prompt`**: Must be >= 2 for diversity and RLOO. 4 is the paper's default. +- **`ce_coef`**: The paper ablates `γ ∈ {0, 0.03, 0.1}`. `0.03` balances CE and RL signals; `0.1` causes CE to dominate the gradient. `0.0` gives pure feature matching. + +### Feature extraction +- **`feature_layers: [0.25, 0.5, 0.75]`**: Extracts and concatenates hidden states from 25%, 50%, 75% depth. Each layer is L2-normalized independently before concatenation. The paper shows this works better than mean pooling or single-layer extraction. +- **`embed_method: last_token`**: Uses the last token's hidden state per block. The paper shows this outperforms mean pooling (Figure 7). + +### Performance +- **`torch_compile: true`**: Recommended for strided mode. Provides additional speedup via graph compilation. +- **flex_attention**: Strided mode automatically uses `flex_attention` with compiled block masks when available (~2x faster than eager attention). Works with gradient checkpointing via automatic dtype normalization. Falls back to eager attention with dense 4D masks if flex_attention is unavailable. + +### Memory +- EBFT requires a frozen copy of the model (the feature network), roughly doubling model memory. +- **LoRA** is recommended to reduce trainable parameter memory. The feature network is always a frozen copy of the base model (without LoRA adapters). +- With 2 GPUs visible, the trainer automatically places the feature network on the second GPU. +- **FSDP2** is supported — the feature network stays outside FSDP wrapping since it's frozen and inference-only. With `cpu_ram_efficient_loading`, the feature network is loaded separately from pretrained weights. + +## Example Configs + +| Config | Mode | Model | Description | +|--------|------|-------|-------------| +| `llama-1b-ebft-opencode.yaml` | Structured | Llama-3.2-1B | QA coding with vLLM | +| `llama-1b-ebft-opencode-novllm.yaml` | Structured | Llama-3.2-1B | QA coding without vLLM | +| `llama-3b-ebft-strided-fft.yaml` | Strided | Llama-3.2-3B | Unstructured code with LoRA | +| `llama-1b-ebft-strided.yaml` | Strided | Llama-3.2-1B | Quick validation | + +## Citation + +```bibtex +@article{jelassi2026matching, + title={Matching Features, Not Tokens: Energy-Based Fine-Tuning of Language Models}, + author={Jelassi, Samy and Kwun, Mujin and Zhao, Rosie and Li, Yuanzhi and Fusi, Nicolo and Du, Yilun and Kakade, Sham M. and Domingo-Enrich, Carles}, + journal={arXiv preprint arXiv:2603.12248}, + year={2026} +} +``` diff --git a/examples/ebft/ebft_opencode.py b/examples/ebft/ebft_opencode.py new file mode 100644 index 0000000000..677949ba51 --- /dev/null +++ b/examples/ebft/ebft_opencode.py @@ -0,0 +1,28 @@ +""" +Dataset transform for nvidia/OpenCodeInstruct with EBFT. + +Maps the dataset's `input` (prompt) and `output` (code solution) fields +to the format expected by the EBFT trainer. +""" + + +def transform(cfg, *args, **kwargs): + def transform_fn(example, tokenizer=None): + return { + "prompt": [ + {"role": "user", "content": example["input"]}, + ], + "ground_truth": example["output"], + } + + return transform_fn, { + "remove_columns": [ + "id", + "domain", + "generation_algorithm", + "llm_judgement", + "unit_tests", + "tests_execution_status", + "average_test_score", + ] + } diff --git a/examples/ebft/ebft_pretrain.py b/examples/ebft/ebft_pretrain.py new file mode 100644 index 0000000000..27a1e54b90 --- /dev/null +++ b/examples/ebft/ebft_pretrain.py @@ -0,0 +1,31 @@ +""" +Dataset transform for unstructured text data with strided EBFT. + +Tokenizes raw text into fixed-length input_ids for the strided trainer. +Sequences are padded to sequence_len for uniform batching. +""" + + +def transform(cfg, *args, **kwargs): + seq_len = cfg.sequence_len + + def transform_fn(example, tokenizer=None): + text = example.get("question", example.get("text", "")) + if tokenizer is None: + return {"prompt": text} + + encoded = tokenizer( + text, + truncation=True, + max_length=seq_len, + padding="max_length", + add_special_tokens=True, + return_tensors=None, + ) + return { + "input_ids": encoded["input_ids"], + "attention_mask": encoded["attention_mask"], + "labels": list(encoded["input_ids"]), + } + + return transform_fn, {"remove_columns": ["question", "answer"]} diff --git a/examples/ebft/ebft_strided_structured.py b/examples/ebft/ebft_strided_structured.py new file mode 100644 index 0000000000..48743931aa --- /dev/null +++ b/examples/ebft/ebft_strided_structured.py @@ -0,0 +1,80 @@ +""" +Dataset transform for structured (prompt, completion) data with strided EBFT. + +Tokenizes prompt and completion separately, concatenates into a single +input_ids sequence, and marks prompt tokens with labels=-100 so the +strided trainer knows where to place anchors (completion span only). + +Works with datasets that have chat-style fields (e.g., nvidia/OpenCodeInstruct). +""" + + +def transform(cfg, *args, **kwargs): + seq_len = cfg.sequence_len + + def transform_fn(example, tokenizer=None): + # Extract prompt and completion from the example + prompt_text = example.get( + "input", example.get("prompt", example.get("question", "")) + ) + completion_text = example.get( + "output", example.get("completion", example.get("answer", "")) + ) + + if tokenizer is None: + return {"prompt": prompt_text} + + pad_id = tokenizer.pad_token_id or tokenizer.eos_token_id + + # Tokenize prompt and completion separately + prompt_enc = tokenizer( + prompt_text, + truncation=False, + add_special_tokens=True, + return_tensors=None, + ) + completion_enc = tokenizer( + completion_text, + truncation=False, + add_special_tokens=False, + return_tensors=None, + ) + + prompt_ids = prompt_enc["input_ids"] + completion_ids = completion_enc["input_ids"] + + # Truncate to fit within seq_len (prioritize keeping prompt + some completion) + total_len = len(prompt_ids) + len(completion_ids) + if total_len > seq_len: + # Truncate completion first, then prompt if needed + max_completion = seq_len - len(prompt_ids) + if max_completion < 1: + # Prompt alone exceeds seq_len — truncate prompt, keep at least 1 completion token + prompt_ids = prompt_ids[: seq_len - 1] + completion_ids = completion_ids[:1] + else: + completion_ids = completion_ids[:max_completion] + + input_ids = prompt_ids + completion_ids + prompt_length = len(prompt_ids) + + # Labels: -100 for prompt tokens, input_ids for completion tokens + labels = [-100] * prompt_length + completion_ids + + # Pad to seq_len + pad_len = seq_len - len(input_ids) + attention_mask = [1] * len(input_ids) + [0] * pad_len + labels = labels + [-100] * pad_len + input_ids = input_ids + [pad_id] * pad_len + + return { + "input_ids": input_ids, + "attention_mask": attention_mask, + "labels": labels, + "prompt_length": prompt_length, + } + + # Signal to remove all original columns (filtered to existing ones at map time) + return transform_fn, { + "remove_columns": "__all__", + } diff --git a/examples/ebft/llama-1b-ebft-opencode-novllm.yaml b/examples/ebft/llama-1b-ebft-opencode-novllm.yaml new file mode 100644 index 0000000000..0891033f0c --- /dev/null +++ b/examples/ebft/llama-1b-ebft-opencode-novllm.yaml @@ -0,0 +1,64 @@ +# EBFT validation config — no vLLM, uses HF generate for simplicity +# Run: CUDA_VISIBLE_DEVICES=0 axolotl train examples/ebft/llama-1b-ebft-opencode-novllm.yaml + +base_model: meta-llama/Llama-3.2-1B +chat_template: llama3 +rl: ebft + +ebft: + feature_layers: [0.25, 0.5, 0.75] + embed_method: last_token + use_whitening: false + alignment_coef: 1.0 + diversity_coef: 1.0 + ce_coef: 0.0 + +trl: + num_generations: 4 + max_completion_length: 128 + temperature: 1.0 + use_vllm: false + scale_rewards: true + loss_type: grpo + epsilon: 0.2 + +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_opencode.transform + split: train[:1%] + +sequence_len: 512 +micro_batch_size: 2 +gradient_accumulation_steps: 2 +num_epochs: 1 +max_steps: 10 + +learning_rate: 1.0e-5 +optimizer: adamw_torch_fused +lr_scheduler: cosine +warmup_steps: 2 +weight_decay: 0.01 + +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.05 +lora_target_linear: true + +bf16: auto +flash_attention: true +gradient_checkpointing: true + +special_tokens: + pad_token: "<|end_of_text|>" + +val_set_size: 0.0 +output_dir: ./outputs/ebft-validation + +wandb_project: ebft +wandb_run_id: +wandb_watch: +wandb_log_model: + +logging_steps: 1 +save_steps: 100 diff --git a/examples/ebft/llama-1b-ebft-opencode.yaml b/examples/ebft/llama-1b-ebft-opencode.yaml new file mode 100644 index 0000000000..d0d1069d87 --- /dev/null +++ b/examples/ebft/llama-1b-ebft-opencode.yaml @@ -0,0 +1,81 @@ +# EBFT: Energy-Based Fine-Tuning with Llama-3.2-1B on OpenCodeInstruct +# +# Paper: "Matching Features, Not Tokens" (Jelassi et al., 2026) +# https://arxiv.org/abs/2603.12248 +# +# Prerequisites: +# 1. Start vLLM server on a separate GPU: +# CUDA_VISIBLE_DEVICES=1 python -m trl.scripts.vllm_serve \ +# --model meta-llama/Llama-3.2-1B \ +# --host 0.0.0.0 --port 8000 \ +# --gpu-memory-utilization 0.4 --dtype bfloat16 +# +# 2. Run training: +# CUDA_VISIBLE_DEVICES=0 axolotl train examples/ebft/llama-1b-ebft-opencode.yaml + +base_model: meta-llama/Llama-3.2-1B +chat_template: llama3 + +# --- Training method --- +rl: ebft + +# --- EBFT configuration --- +ebft: + feature_layers: [0.25, 0.5, 0.75] # extract hidden states at 25%, 50%, 75% depth + embed_method: last_token # pool to sequence embedding via last token + use_whitening: false # SVD whitening (disable for speed in small runs) + alignment_coef: 1.0 # cosine similarity with ground-truth features + diversity_coef: 1.0 # pairwise similarity penalty + ce_coef: 0.0 # cross-entropy on ground-truth (0 = pure feature matching) + +# --- Generation settings (via TRL/GRPO infrastructure) --- +trl: + num_generations: 4 # samples per prompt for RLOO + max_completion_length: 256 # max generated tokens + temperature: 1.0 + use_vllm: true + scale_rewards: true + loss_type: grpo + epsilon: 0.2 + +# --- Dataset --- +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_opencode.transform + split: train[:1%] # first 1% for validation runs + +# --- Training hyperparameters --- +sequence_len: 1024 +micro_batch_size: 2 +gradient_accumulation_steps: 4 +num_epochs: 1 +max_steps: 50 + +learning_rate: 1.0e-5 +optimizer: adamw_torch_fused +lr_scheduler: cosine +warmup_steps: 5 +weight_decay: 0.01 + +# --- LoRA (recommended to reduce memory with frozen feature network) --- +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.05 +lora_target_linear: true + +# --- Hardware --- +bf16: auto +flash_attention: true +gradient_checkpointing: true + +special_tokens: + pad_token: "<|end_of_text|>" + +val_set_size: 0.0 +output_dir: ./outputs/ebft-llama-1b-opencode + +# --- Logging --- +use_tensorboard: true +logging_steps: 1 +save_steps: 25 diff --git a/examples/ebft/llama-1b-ebft-strided-structured.yaml b/examples/ebft/llama-1b-ebft-strided-structured.yaml new file mode 100644 index 0000000000..8ba63b64b5 --- /dev/null +++ b/examples/ebft/llama-1b-ebft-strided-structured.yaml @@ -0,0 +1,65 @@ +# EBFT Strided Structured Mode: For structured (prompt, completion) data +# Uses strided block-parallel generation on completion spans — no vLLM needed. +# +# Run: CUDA_VISIBLE_DEVICES=0 axolotl train examples/ebft/llama-1b-ebft-strided-structured.yaml + +base_model: meta-llama/Llama-3.2-1B +rl: ebft + +ebft: + mode: strided # strided block-parallel generation + stride: 8 # tokens between anchor points + context_length: 8 # context window per block + generate_max_len: 8 # tokens to generate per block + n_samples_per_prompt: 4 # rollouts per document + temperature: 0.6 + top_p: 1.0 + feature_layers: [0.25, 0.5, 0.75] + embed_method: last_token + use_whitening: true + alignment_coef: 1.0 + diversity_coef: 1.0 + rl_coef: 1.0 + ce_coef: 0.03 # small CE weight for structured data + advantage_estimator: rloo + min_completion_prefix: 8 # skip anchors too close to prompt boundary + +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_strided_structured.transform + split: train[:1%] + +sequence_len: 2048 +micro_batch_size: 1 +gradient_accumulation_steps: 2 +num_epochs: 1 +# max_steps: 10 + +learning_rate: 1.0e-6 +optimizer: adamw_torch_fused +lr_scheduler: cosine +warmup_steps: 5 + +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.05 +lora_target_linear: true + +bf16: auto +flash_attention: false # strided EBFT overrides to flex_attention (or eager fallback) at runtime +flex_attention: true # fused flex_attention kernel compiles itself; don't set torch_compile: true + # (full-model compile conflicts with gradient checkpointing + flex_attention) +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: true # required for flex_attention (non-reentrant causes CheckpointError) + +special_tokens: + pad_token: "<|end_of_text|>" + +val_set_size: 0.0 +output_dir: ./outputs/ebft-strided-structured + +wandb_project: ebft +logging_steps: 1 +save_steps: 100 diff --git a/examples/ebft/llama-1b-ebft-strided.yaml b/examples/ebft/llama-1b-ebft-strided.yaml new file mode 100644 index 0000000000..c9519f160d --- /dev/null +++ b/examples/ebft/llama-1b-ebft-strided.yaml @@ -0,0 +1,60 @@ +# EBFT Strided Mode: For unstructured text data (raw code, prose) +# Uses strided block-parallel generation — no vLLM needed. +# +# Run: CUDA_VISIBLE_DEVICES=0 axolotl train examples/ebft/llama-1b-ebft-strided.yaml + +base_model: meta-llama/Llama-3.2-1B +rl: ebft + +ebft: + mode: strided # strided block-parallel generation + stride: 8 # tokens between anchor points + context_length: 8 # context window per block + generate_max_len: 8 # tokens to generate per block + n_samples_per_prompt: 4 # rollouts per document + temperature: 0.6 + top_p: 1.0 + feature_layers: [0.25, 0.5, 0.75] + embed_method: last_token + use_whitening: true + alignment_coef: 1.0 + diversity_coef: 1.0 + rl_coef: 1.0 + ce_coef: 0.0 + advantage_estimator: rloo + +datasets: + - path: sjelassi/swallow_code_20m + type: ebft_pretrain.transform + split: train[:100] + +sequence_len: 256 +micro_batch_size: 1 +gradient_accumulation_steps: 2 +num_epochs: 1 +max_steps: 5 + +learning_rate: 1.0e-6 +optimizer: adamw_torch_fused +lr_scheduler: cosine +warmup_steps: 2 + +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.05 +lora_target_linear: true + +bf16: auto +flash_attention: false # strided EBFT overrides to flex_attention (or eager fallback) at runtime +gradient_checkpointing: true + +special_tokens: + pad_token: "<|end_of_text|>" + +val_set_size: 0.0 +output_dir: ./outputs/ebft-strided-validation + +wandb_project: ebft +logging_steps: 1 +save_steps: 100 diff --git a/examples/ebft/llama-3b-ebft-strided-fft.yaml b/examples/ebft/llama-3b-ebft-strided-fft.yaml new file mode 100644 index 0000000000..5695efa401 --- /dev/null +++ b/examples/ebft/llama-3b-ebft-strided-fft.yaml @@ -0,0 +1,69 @@ +# EBFT Strided: LoRA Llama-3.2-3B on SwallowCode, 100 steps +# Actor on GPU 0, frozen feature network on GPU 1 +# +# Run: CUDA_VISIBLE_DEVICES=0,1 python -m axolotl.cli.train examples/ebft/llama-3b-ebft-strided-fft.yaml + +base_model: meta-llama/Llama-3.2-3B +rl: ebft + +ebft: + mode: strided + stride: 8 + context_length: 8 + generate_max_len: 8 + n_samples_per_prompt: 4 + temperature: 0.6 + top_p: 1.0 + feature_layers: [0.25, 0.5, 0.75] + embed_method: last_token + use_whitening: true + alignment_coef: 1.0 + diversity_coef: 1.0 + rl_coef: 1.0 + ce_coef: 0.0 # paper recommends 0.03 for mixed objective; 0.1 causes CE to dominate + advantage_estimator: rloo + +datasets: + - path: sjelassi/swallow_code_20m + type: ebft_pretrain.transform + split: train[:5000] + +sequence_len: 1024 +micro_batch_size: 1 +gradient_accumulation_steps: 4 +num_epochs: 1 +max_steps: 100 + +learning_rate: 1.0e-5 +optimizer: adamw_torch_fused +lr_scheduler: cosine +warmup_steps: 10 +weight_decay: 0.01 + +adapter: lora +lora_r: 32 +lora_alpha: 64 +lora_dropout: 0.05 +lora_target_linear: true + +bf16: auto +torch_dtype: bfloat16 +flash_attention: false +gradient_checkpointing: true +torch_compile: true +gradient_checkpointing_kwargs: + use_reentrant: true +ddp: false +device_map: + "": 0 + +special_tokens: + pad_token: "<|end_of_text|>" + +val_set_size: 0.0 +output_dir: ./outputs/ebft-llama3b-strided + +wandb_project: ebft +wandb_name: llama3b-strided-lora-100steps +logging_steps: 1 +save_steps: 50 diff --git a/examples/ebft/llama-8b-ebft-strided-fft.yaml b/examples/ebft/llama-8b-ebft-strided-fft.yaml new file mode 100644 index 0000000000..8cf9628496 --- /dev/null +++ b/examples/ebft/llama-8b-ebft-strided-fft.yaml @@ -0,0 +1,58 @@ +# EBFT Strided: Full-parameter Llama-3.1-8B on SwallowCode, 100 steps +# Feature network is CPU-offloaded to fit in single 32GB GPU +# +# Run: CUDA_VISIBLE_DEVICES=0 python -m axolotl.cli.train examples/ebft/llama-8b-ebft-strided-fft.yaml + +base_model: meta-llama/Llama-3.1-8B +rl: ebft + +ebft: + mode: strided + stride: 8 + context_length: 8 + generate_max_len: 8 + n_samples_per_prompt: 4 + temperature: 0.6 + top_p: 1.0 + feature_layers: [0.25, 0.5, 0.75] + embed_method: last_token + use_whitening: true + alignment_coef: 1.0 + diversity_coef: 1.0 + rl_coef: 1.0 + ce_coef: 0.0 + advantage_estimator: rloo + +datasets: + - path: sjelassi/swallow_code_20m + type: ebft_pretrain.transform + split: train[:5000] + +sequence_len: 1024 +micro_batch_size: 1 +gradient_accumulation_steps: 4 +num_epochs: 1 +max_steps: 100 + +learning_rate: 1.0e-6 +optimizer: adamw_torch_fused +lr_scheduler: cosine +warmup_steps: 10 +weight_decay: 0.01 + +bf16: auto +flash_attention: false # strided EBFT uses flex_attention at runtime +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false + +special_tokens: + pad_token: "<|end_of_text|>" + +val_set_size: 0.0 +output_dir: ./outputs/ebft-llama8b-strided + +wandb_project: ebft +wandb_name: llama8b-strided-fft-100steps +logging_steps: 1 +save_steps: 50 diff --git a/examples/ebft/qwen35-4b-ebft-structured-async.yaml b/examples/ebft/qwen35-4b-ebft-structured-async.yaml new file mode 100644 index 0000000000..759a317307 --- /dev/null +++ b/examples/ebft/qwen35-4b-ebft-structured-async.yaml @@ -0,0 +1,86 @@ +# EBFT Structured Mode: Qwen3.5-4B (hybrid linear attention) +# +# Qwen3.5 uses hybrid attention: linear attention (conv1d) on 3/4 of layers, +# full attention every 4th layer. This tests EBFT compatibility. +# +# Prerequisites: +# 1. Start vLLM on GPU 0: +# CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve examples/ebft/qwen35-4b-ebft-structured-async.yaml +# +# 2. Run training on GPU 1: +# CUDA_VISIBLE_DEVICES=1 PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \ +# axolotl train examples/ebft/qwen35-4b-ebft-structured-async.yaml + +base_model: Qwen/Qwen3.5-4B + +rl: ebft + +ebft: + feature_layers: [0.25, 0.5, 0.75] + embed_method: last_token + use_whitening: false + alignment_coef: 1.0 + diversity_coef: 1.0 + ce_coef: 0.0 + +trl: + num_generations: 4 + max_completion_length: 256 + temperature: 0.7 + use_vllm: true + vllm_server_host: 0.0.0.0 + vllm_server_port: 8000 + scale_rewards: true + loss_type: grpo + epsilon: 0.2 + generation_kwargs: + stop_token_ids: [248044, 248046] # <|endoftext|>, <|im_end|> + chat_template_kwargs: + enable_thinking: false + async_prefetch: true + vllm_server_timeout: 300 + +vllm: + gpu_memory_utilization: 0.5 + max_model_len: 2048 + serve_module: axolotl.scripts.vllm_serve_lora + enforce_eager: true + +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_opencode.transform + split: train[:500] + +sequence_len: 1024 +micro_batch_size: 1 +gradient_accumulation_steps: 4 +num_epochs: 1 +max_steps: 10 + +learning_rate: 5.0e-6 +optimizer: adamw_torch_fused +lr_scheduler: cosine +warmup_steps: 3 +weight_decay: 0.01 + +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.0 +# Target full-attention q/k/v/o on layers 3,7,11,15,19,23,27,31 + MLP on all layers +# Avoids linear_attn modules (in_proj_qkv, in_proj_z, etc.) which break vLLM LoRA +lora_target_modules: ".*\\.layers\\.(3|7|11|15|19|23|27|31)\\.self_attn\\.(q|k|v|o)_proj|.*\\.mlp\\.(gate|up|down)_proj" + +bf16: auto +flash_attention: true +gradient_checkpointing: true + +special_tokens: + pad_token: "<|endoftext|>" + +val_set_size: 0.0 +output_dir: ./outputs/ebft-qwen35-4b-structured-async + +wandb_project: ebft +logging_steps: 1 +save_steps: 50 diff --git a/examples/ebft/qwen35-4b-ebft-structured.yaml b/examples/ebft/qwen35-4b-ebft-structured.yaml new file mode 100644 index 0000000000..9108e87e93 --- /dev/null +++ b/examples/ebft/qwen35-4b-ebft-structured.yaml @@ -0,0 +1,77 @@ +# EBFT Structured Mode: Qwen3.5-4B (hybrid linear attention) +# +# Qwen3.5 uses hybrid attention: linear attention (conv1d) on 3/4 of layers, +# full attention every 4th layer. This tests EBFT compatibility. +# +# Prerequisites: +# 1. Start vLLM on GPU 0: +# CUDA_VISIBLE_DEVICES=0 trl vllm-serve --model Qwen/Qwen3.5-4B \ +# --gpu-memory-utilization 0.5 --max-model-len 2048 --enforce-eager +# +# 2. Run training on GPU 1: +# CUDA_VISIBLE_DEVICES=1 PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \ +# axolotl train examples/ebft/qwen35-4b-ebft-structured.yaml + +base_model: Qwen/Qwen3.5-4B + +rl: ebft + +ebft: + feature_layers: [0.25, 0.5, 0.75] + embed_method: last_token + use_whitening: false + alignment_coef: 1.0 + diversity_coef: 1.0 + ce_coef: 0.0 + +trl: + num_generations: 4 + max_completion_length: 256 + temperature: 0.7 + use_vllm: true + vllm_server_host: 0.0.0.0 + vllm_server_port: 8000 + scale_rewards: true + loss_type: grpo + epsilon: 0.2 + generation_kwargs: + stop_token_ids: [248044, 248046] # <|endoftext|>, <|im_end|> + chat_template_kwargs: + enable_thinking: false # disable Qwen3.5 thinking mode for shorter completions + +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_opencode.transform + split: train[:500] + +sequence_len: 1024 +micro_batch_size: 1 +gradient_accumulation_steps: 4 +num_epochs: 1 +max_steps: 10 + +learning_rate: 5.0e-6 +optimizer: adamw_torch_fused +lr_scheduler: cosine +warmup_steps: 3 +weight_decay: 0.01 + +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.0 +lora_target_modules: ".*\\.layers\\.(3|7|11|15|19|23|27|31)\\.self_attn\\.(q|k|v|o)_proj|.*\\.mlp\\.(gate|up|down)_proj" + +bf16: auto +flash_attention: true +gradient_checkpointing: true + +special_tokens: + pad_token: "<|endoftext|>" + +val_set_size: 0.0 +output_dir: ./outputs/ebft-qwen35-4b-structured + +wandb_project: ebft +logging_steps: 1 +save_steps: 50 diff --git a/examples/ebft/qwen35-9b-ebft-structured.yaml b/examples/ebft/qwen35-9b-ebft-structured.yaml new file mode 100644 index 0000000000..e79fb5fbfc --- /dev/null +++ b/examples/ebft/qwen35-9b-ebft-structured.yaml @@ -0,0 +1,82 @@ +# EBFT Structured Mode: Qwen3.5-9B (hybrid linear attention) +# +# Prerequisites: +# 1. Start vLLM on GPU 0: +# CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve examples/ebft/qwen35-9b-ebft-structured.yaml +# +# 2. Run training on GPU 1: +# CUDA_VISIBLE_DEVICES=1 PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \ +# axolotl train examples/ebft/qwen35-9b-ebft-structured.yaml + +base_model: Qwen/Qwen3.5-9B + +rl: ebft + +ebft: + feature_layers: [0.25, 0.5, 0.75] + embed_method: last_token + use_whitening: false + alignment_coef: 1.0 + diversity_coef: 1.0 + ce_coef: 0.0 + +trl: + num_generations: 4 + max_completion_length: 256 + temperature: 0.7 + use_vllm: true + vllm_server_host: 0.0.0.0 + vllm_server_port: 8000 + scale_rewards: true + loss_type: grpo + epsilon: 0.2 + generation_kwargs: + stop_token_ids: [248044, 248046] # <|endoftext|>, <|im_end|> + chat_template_kwargs: + enable_thinking: false + vllm_server_timeout: 300 + +vllm: + gpu_memory_utilization: 0.7 + max_model_len: 2048 + serve_module: axolotl.scripts.vllm_serve_lora + enforce_eager: true + +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_opencode.transform + split: train[:500] + +sequence_len: 1024 +micro_batch_size: 1 +gradient_accumulation_steps: 4 +num_epochs: 1 +max_steps: 10 + +learning_rate: 3.0e-6 +optimizer: adamw_torch_fused +lr_scheduler: cosine +warmup_steps: 3 +weight_decay: 0.01 + +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.0 +# Target full-attention q/k/v/o on layers 3,7,11,15,19,23,27,31 + MLP on all layers +# Avoids linear_attn modules (in_proj_qkv, in_proj_z, etc.) which break vLLM LoRA +lora_target_modules: ".*\\.layers\\.(3|7|11|15|19|23|27|31)\\.self_attn\\.(q|k|v|o)_proj|.*\\.mlp\\.(gate|up|down)_proj" + +bf16: auto +flash_attention: true +gradient_checkpointing: true + +special_tokens: + pad_token: "<|endoftext|>" + +val_set_size: 0.0 +output_dir: ./outputs/ebft-qwen35-9b-structured + +wandb_project: ebft +logging_steps: 1 +save_steps: 50 diff --git a/src/axolotl/cli/vllm_serve.py b/src/axolotl/cli/vllm_serve.py index 10db23878c..2180a9e7f4 100644 --- a/src/axolotl/cli/vllm_serve.py +++ b/src/axolotl/cli/vllm_serve.py @@ -38,18 +38,14 @@ def do_vllm_serve( cfg = load_cfg(config) model = cfg.base_model - # Determine serve module: explicit CLI/config > auto-select from vllm_lora_sync > default + # Determine serve module: explicit CLI/config > default (axolotl's LoRA-aware serve). + # We default to axolotl's serve module instead of TRL's because TRL's sends + # truncate_prompt_tokens which is unsupported in vLLM 0.17+. serve_module = cli_args.get("serve_module") or getattr( cfg.vllm, "serve_module", None ) - if ( - serve_module is None - and getattr(cfg, "trl", None) - and getattr(cfg.trl, "vllm_lora_sync", False) - ): - serve_module = "axolotl.scripts.vllm_serve_lora" if serve_module is None: - serve_module = "trl.scripts.vllm_serve" + serve_module = "axolotl.scripts.vllm_serve_lora" vllm_serve_main = __import__(serve_module, fromlist=["main"]).main tensor_parallel_size = 1 data_parallel_size = 1 @@ -79,6 +75,12 @@ def do_vllm_serve( cli_args.get("enable_reasoning") or cfg.vllm.enable_reasoning or False ) + cli_enforce_eager = cli_args.get("enforce_eager") + cfg_enforce_eager = getattr(cfg.vllm, "enforce_eager", None) + raw_enforce_eager = ( + cfg_enforce_eager if cli_enforce_eager is None else cli_enforce_eager + ) + enforce_eager = bool(raw_enforce_eager) if raw_enforce_eager is not None else False base_kwargs = dict( model=model, tensor_parallel_size=tensor_parallel_size, @@ -89,6 +91,7 @@ def do_vllm_serve( dtype=dtype, max_model_len=max_model_len, enable_prefix_caching=enable_prefix_caching, + enforce_eager=enforce_eager, ) # Use LoRAScriptArguments when serving with native LoRA support @@ -98,6 +101,10 @@ def do_vllm_serve( lora_kwargs = {} if hasattr(cfg, "lora_r") and cfg.lora_r: lora_kwargs["max_lora_rank"] = cfg.lora_r + # Disable native LoRA in vLLM if not using vllm_lora_sync + # (merged weight sync via batch_update doesn't need vLLM LoRA mode) + if not getattr(cfg.trl, "vllm_lora_sync", False): + lora_kwargs["enable_lora"] = False vllm_script_args = LoRAScriptArguments(**base_kwargs, **lora_kwargs) else: vllm_script_args = AxolotlScriptArguments( diff --git a/src/axolotl/common/datasets.py b/src/axolotl/common/datasets.py index c95ddb80ee..b661e74c95 100644 --- a/src/axolotl/common/datasets.py +++ b/src/axolotl/common/datasets.py @@ -118,7 +118,7 @@ def load_preference_datasets( train_dataset, eval_dataset = prepare_preference_datasets(cfg, tokenizer) total_num_steps: int | None = None - if cfg.rl is not RLType.GRPO: + if cfg.rl not in {RLType.GRPO, RLType.EBFT}: total_num_steps = int( math.ceil(len(train_dataset) * cfg.num_epochs / cfg.batch_size) ) diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index f7bf110cc0..89d4c9ff76 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -78,6 +78,11 @@ def _get_trainer_cls(self, trainer_kwargs: dict): trainer_cls = AxolotlKTOTrainer elif self.cfg.rl is RLType.SIMPO: trainer_cls = AxolotlCPOTrainer + elif self.cfg.rl is RLType.EBFT: + from axolotl.core.trainers.ebft import EBFTStrategy + + trainer_cls = EBFTStrategy.get_trainer_class(self.cfg) + trainer_kwargs.update(EBFTStrategy.set_trainer_kwargs(self.cfg)) else: raise ValueError(f"Unsupported RL: {self.cfg.rl}") @@ -179,6 +184,13 @@ def _build_training_arguments(self, total_num_steps): elif self.cfg.rl in [RLType.DPO, RLType.IPO]: training_args_cls = AxolotlDPOConfig training_args_kwargs.update(DPOStrategy.set_training_args_kwargs(self.cfg)) + + elif self.cfg.rl is RLType.EBFT: + from axolotl.core.trainers.ebft import EBFTStrategy + + training_args_cls = EBFTStrategy.get_training_args_class(self.cfg) + training_args_kwargs.update(EBFTStrategy.set_training_args_kwargs(self.cfg)) + blocklist_args_kwargs = EBFTStrategy.get_blocklist_args_kwargs(self.cfg) else: raise ValueError(f"Unsupported RL: {self.cfg.rl}") @@ -211,7 +223,7 @@ def build(self, total_num_steps): if ( self.cfg.adapter and self.peft_config - and self.cfg.rl not in (RLType.GRPO, RLType.ORPO) + and self.cfg.rl not in (RLType.GRPO, RLType.ORPO, RLType.EBFT) ): trainer_kwargs["peft_config"] = self.peft_config if self.cfg.precompute_ref_log_probs is not None: diff --git a/src/axolotl/core/trainers/__init__.py b/src/axolotl/core/trainers/__init__.py index 22d8b64f68..cdc09ae0a7 100644 --- a/src/axolotl/core/trainers/__init__.py +++ b/src/axolotl/core/trainers/__init__.py @@ -4,6 +4,8 @@ from .base import AxolotlTrainer from .dpo.trainer import AxolotlDPOTrainer +from .ebft.strided import AxolotlStridedEBFTTrainer +from .ebft.trainer import AxolotlEBFTTrainer from .mamba import AxolotlMambaTrainer from .trl import ( AxolotlCPOTrainer, diff --git a/src/axolotl/core/trainers/ebft/__init__.py b/src/axolotl/core/trainers/ebft/__init__.py new file mode 100644 index 0000000000..23b61fbe62 --- /dev/null +++ b/src/axolotl/core/trainers/ebft/__init__.py @@ -0,0 +1,213 @@ +"""EBFT (Energy-Based Fine-Tuning) Strategy for training + +Two modes: +- structured: For QA data with prompt/completion splits. Uses GRPOTrainer + vLLM. +- strided: For unstructured text (raw code, prose). Uses strided block-parallel generation. +""" + +from typing import Any + +from axolotl.core.trainers.ebft.args import ( + AxolotlAsyncEBFTConfig, + AxolotlEBFTConfig, + AxolotlStridedEBFTConfig, +) +from axolotl.utils.dict import DictDefault + + +def _get_ebft_mode(cfg: DictDefault) -> str: + """Determine EBFT mode from config.""" + if cfg.ebft and hasattr(cfg.ebft, "mode") and cfg.ebft.mode: + return cfg.ebft.mode + return "structured" + + +class EBFTStrategy: + """Strategy for EBFT training — dispatches between structured and strided modes.""" + + @classmethod + def get_trainer_class(cls, cfg: DictDefault | None = None): + mode = _get_ebft_mode(cfg) if cfg else "structured" + if mode == "strided": + from axolotl.core.trainers.ebft.strided import AxolotlStridedEBFTTrainer + + return AxolotlStridedEBFTTrainer + + # Structured mode: async or sync + use_async = cfg and cfg.trl and getattr(cfg.trl, "async_prefetch", False) + if use_async: + from axolotl.core.trainers.ebft.trainer import AxolotlAsyncEBFTTrainer + + return AxolotlAsyncEBFTTrainer + from axolotl.core.trainers.ebft.trainer import AxolotlEBFTTrainer + + return AxolotlEBFTTrainer + + @classmethod + def get_training_args_class(cls, cfg: DictDefault | None = None): + mode = _get_ebft_mode(cfg) if cfg else "structured" + if mode == "strided": + return AxolotlStridedEBFTConfig + + # Structured mode: async or sync config + use_async = cfg and cfg.trl and getattr(cfg.trl, "async_prefetch", False) + if use_async: + return AxolotlAsyncEBFTConfig + return AxolotlEBFTConfig + + @classmethod + def is_strided(cls, cfg: DictDefault) -> bool: + return _get_ebft_mode(cfg) == "strided" + + @classmethod + def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: + """Map axolotl YAML config fields to training args kwargs.""" + kwargs: dict[str, Any] = {} + mode = _get_ebft_mode(cfg) + + # Common EBFT fields + ebft = cfg.ebft + if ebft: + if ebft.feature_layers is not None: + kwargs["ebft_feature_layers"] = ebft.feature_layers + if ebft.embed_method is not None: + kwargs["ebft_embed_method"] = ebft.embed_method + if ebft.use_whitening is not None: + kwargs["ebft_use_whitening"] = ebft.use_whitening + if ebft.alignment_coef is not None: + kwargs["ebft_alignment_coef"] = ebft.alignment_coef + if ebft.diversity_coef is not None: + kwargs["ebft_diversity_coef"] = ebft.diversity_coef + if ebft.ce_coef is not None: + kwargs["ebft_ce_coef"] = ebft.ce_coef + if getattr(ebft, "adaptive_max_tokens", None) is not None: + kwargs["ebft_adaptive_max_tokens"] = ebft.adaptive_max_tokens + if getattr(ebft, "gt_length_multiplier", None) is not None: + kwargs["ebft_gt_length_multiplier"] = ebft.gt_length_multiplier + + if mode == "strided": + # Strided-specific fields + if ebft: + if ebft.stride is not None: + kwargs["ebft_stride"] = ebft.stride + if ebft.context_length is not None: + kwargs["ebft_context_length"] = ebft.context_length + if ebft.generate_max_len is not None: + kwargs["ebft_generate_max_len"] = ebft.generate_max_len + if ebft.n_samples_per_prompt is not None: + kwargs["ebft_n_samples_per_prompt"] = ebft.n_samples_per_prompt + if ebft.temperature is not None: + kwargs["ebft_temperature"] = ebft.temperature + if ebft.top_p is not None: + kwargs["ebft_top_p"] = ebft.top_p + if ebft.rl_coef is not None: + kwargs["ebft_rl_coef"] = ebft.rl_coef + if ebft.advantage_estimator is not None: + kwargs["ebft_advantage_estimator"] = ebft.advantage_estimator + if ebft.min_completion_prefix is not None: + kwargs["ebft_min_completion_prefix"] = ebft.min_completion_prefix + else: + # Structured mode: map TRL config fields + trl = cfg.trl + if trl: + if trl.use_vllm: + kwargs["use_vllm"] = trl.use_vllm + if trl.vllm_mode: + kwargs["vllm_mode"] = trl.vllm_mode + if trl.vllm_mode == "colocate": + kwargs["vllm_enable_sleep_mode"] = trl.vllm_enable_sleep_mode + vllm_cfg = cfg.vllm + if vllm_cfg: + kwargs["vllm_gpu_memory_utilization"] = ( + vllm_cfg.gpu_memory_utilization + ) + kwargs["vllm_tensor_parallel_size"] = ( + vllm_cfg.tensor_parallel_size + ) + kwargs["vllm_server_host"] = trl.vllm_server_host or ( + trl.vllm.host if trl.vllm else None + ) + kwargs["vllm_server_port"] = trl.vllm_server_port or ( + trl.vllm.port if trl.vllm else None + ) + if trl.vllm_server_timeout: + kwargs["vllm_server_timeout"] = trl.vllm_server_timeout + + if trl.num_generations: + kwargs["num_generations"] = trl.num_generations + if trl.max_completion_length is not None: + kwargs["max_completion_length"] = trl.max_completion_length + if trl.temperature is not None: + kwargs["temperature"] = trl.temperature + if trl.top_p is not None: + kwargs["top_p"] = trl.top_p + if trl.top_k is not None: + kwargs["top_k"] = trl.top_k + if trl.min_p is not None: + kwargs["min_p"] = trl.min_p + if trl.num_iterations is not None: + kwargs["num_iterations"] = trl.num_iterations + if trl.epsilon is not None: + kwargs["epsilon"] = trl.epsilon + if trl.epsilon_high is not None: + kwargs["epsilon_high"] = trl.epsilon_high + if trl.scale_rewards is not None: + kwargs["scale_rewards"] = trl.scale_rewards + if trl.loss_type is not None: + kwargs["loss_type"] = trl.loss_type + if trl.mask_truncated_completions is not None: + kwargs["mask_truncated_completions"] = ( + trl.mask_truncated_completions + ) + if trl.log_completions is not None: + kwargs["log_completions"] = trl.log_completions + if trl.num_completions_to_print is not None: + kwargs["num_completions_to_print"] = trl.num_completions_to_print + if trl.sync_ref_model: + kwargs["sync_ref_model"] = trl.sync_ref_model + if trl.repetition_penalty is not None: + kwargs["repetition_penalty"] = trl.repetition_penalty + if trl.generation_kwargs is not None: + kwargs["generation_kwargs"] = trl.generation_kwargs + if trl.chat_template_kwargs is not None: + kwargs["chat_template_kwargs"] = trl.chat_template_kwargs + + # Async prefetch fields (only pass when enabled — sync config doesn't have these) + if getattr(trl, "async_prefetch", False): + kwargs["async_prefetch"] = trl.async_prefetch + if getattr(trl, "vllm_sync_interval", None) is not None: + kwargs["vllm_sync_interval"] = trl.vllm_sync_interval + if getattr(trl, "vllm_lora_sync", False): + kwargs["vllm_lora_sync"] = trl.vllm_lora_sync + + return kwargs + + @classmethod + def set_trainer_args(cls, cfg: DictDefault) -> list[Any]: + return [] + + @classmethod + def set_trainer_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: + return {} + + @classmethod + def get_blocklist_args_kwargs(cls, cfg: DictDefault | None = None) -> list[str]: + mode = _get_ebft_mode(cfg) if cfg else "structured" + if mode == "strided": + return [ + "dataset_num_proc", + "max_length", + "max_prompt_length", + "include_tokens_per_second", + "beta", + ] + return [ + "dataset_num_proc", + "max_length", + "include_tokens_per_second", + "max_prompt_length", + ] + + @classmethod + def get_collator(cls, *args, **kwargs): + return None diff --git a/src/axolotl/core/trainers/ebft/args.py b/src/axolotl/core/trainers/ebft/args.py new file mode 100644 index 0000000000..4a31b6b6b4 --- /dev/null +++ b/src/axolotl/core/trainers/ebft/args.py @@ -0,0 +1,133 @@ +""" +EBFT-specific training arguments. + +Two config classes: +- AxolotlEBFTConfig: extends GRPOConfig for structured QA data (uses vLLM generation) +- AxolotlStridedEBFTConfig: extends TrainingArguments for unstructured text (strided generation) +""" + +from dataclasses import dataclass, field + +from transformers import TrainingArguments +from trl import GRPOConfig + +from axolotl.core.trainers.grpo.fast_async_trainer import FastAsyncGRPOConfig +from axolotl.core.training_args import AxolotlTrainingMixins + + +# -- Shared EBFT fields as a mixin -- +@dataclass +class EBFTFieldsMixin: + """Common fields shared between structured and strided EBFT configs.""" + + ebft_feature_layers: list[float] = field( + default_factory=lambda: [0.25, 0.5, 0.75], + metadata={"help": "Fractional layer depths for feature extraction"}, + ) + ebft_embed_method: str = field( + default="last_token", + metadata={"help": "Pooling method: 'last_token', 'mean_pooling', or 'concat'"}, + ) + ebft_use_whitening: bool = field( + default=False, + metadata={"help": "Apply SVD whitening to feature embeddings"}, + ) + ebft_alignment_coef: float = field( + default=1.0, + metadata={"help": "Coefficient for alignment reward (cosine similarity)"}, + ) + ebft_diversity_coef: float = field( + default=1.0, + metadata={"help": "Coefficient for diversity penalty"}, + ) + ebft_ce_coef: float = field( + default=0.0, + metadata={"help": "Cross-entropy loss coefficient on ground-truth tokens"}, + ) + ebft_adaptive_max_tokens: bool = field( + default=True, + metadata={"help": "Set per-batch max_tokens based on ground-truth length"}, + ) + ebft_gt_length_multiplier: float = field( + default=1.5, + metadata={ + "help": "Multiplier for ground-truth token count when computing adaptive max_tokens" + }, + ) + + +# -- Structured mode: extends GRPOTrainer for QA data with vLLM -- +@dataclass +class AxolotlEBFTConfig(EBFTFieldsMixin, AxolotlTrainingMixins, GRPOConfig): + """EBFT config for structured QA data — extends GRPOConfig.""" + + vllm_lora_sync: bool = field( + default=False, + metadata={ + "help": "Sync LoRA adapters to vLLM via filesystem instead of NCCL weight merge." + }, + ) + + +# -- Async structured mode: extends FastAsyncGRPOConfig -- +@dataclass +class AxolotlAsyncEBFTConfig( + EBFTFieldsMixin, AxolotlTrainingMixins, FastAsyncGRPOConfig +): + """EBFT config for async structured QA data — extends FastAsyncGRPOConfig. + + Includes all async fields: async_prefetch, vllm_lora_sync, + skip_zero_advantage_batches, streaming_partial_batch, replay_buffer_size, etc. + """ + + vllm_lora_sync: bool = field( + default=False, + metadata={ + "help": "Sync LoRA adapters to vLLM via filesystem instead of NCCL weight merge." + }, + ) + + +# -- Strided mode: extends TrainingArguments for unstructured text -- +@dataclass +class AxolotlStridedEBFTConfig( + EBFTFieldsMixin, AxolotlTrainingMixins, TrainingArguments +): + """EBFT config for unstructured text with strided block-parallel generation.""" + + ebft_stride: int = field( + default=8, + metadata={"help": "Stride between anchor points (in tokens)"}, + ) + ebft_context_length: int = field( + default=8, + metadata={"help": "Context window size for each block"}, + ) + ebft_generate_max_len: int = field( + default=8, + metadata={"help": "Number of tokens to generate per block"}, + ) + ebft_n_samples_per_prompt: int = field( + default=4, + metadata={"help": "Number of independent rollouts per document"}, + ) + ebft_temperature: float = field( + default=0.6, + metadata={"help": "Sampling temperature for strided generation"}, + ) + ebft_top_p: float = field( + default=1.0, + metadata={"help": "Top-p nucleus sampling threshold"}, + ) + ebft_rl_coef: float = field( + default=1.0, + metadata={"help": "RL policy gradient loss coefficient"}, + ) + ebft_advantage_estimator: str = field( + default="rloo", + metadata={"help": "Advantage estimator: 'rloo', 'group_norm', or 'reinforce'"}, + ) + ebft_min_completion_prefix: int = field( + default=0, + metadata={"help": "Minimum tokens into completion before placing anchors"}, + ) diff --git a/src/axolotl/core/trainers/ebft/kernels.py b/src/axolotl/core/trainers/ebft/kernels.py new file mode 100644 index 0000000000..d1d35feb4e --- /dev/null +++ b/src/axolotl/core/trainers/ebft/kernels.py @@ -0,0 +1,308 @@ +""" +Fused Triton kernels for strided EBFT. + +These kernels eliminate intermediate tensor materializations that dominate +the elementwise/fill category (~40% of CUDA time in profiling). + +Kernels: + 1. fused_log_softmax_gather: log_softmax + gather in one pass (no full vocab materialization) + 2. fused_masked_reinforce_loss: -logp * advantage * mask, reduced to scalar + 3. fused_cosine_similarity: batched cosine similarity without intermediate tensors +""" + +import torch +import triton +import triton.language as tl + +# --------------------------------------------------------------------------- +# 1. Fused log_softmax + gather (selective log softmax) +# --------------------------------------------------------------------------- +# Instead of: log_softmax(logits, dim=-1) → (B, S, V) → gather(index=labels) +# We compute: for each (b, s), the log_softmax value at logits[b, s, labels[b, s]] +# This avoids materializing the full (B, S, V) log_softmax output. + + +@triton.jit +def _fused_log_softmax_gather_kernel( + logits_ptr, # (B*S, V) row-major + labels_ptr, # (B*S,) int64 + output_ptr, # (B*S,) float32 + V: tl.constexpr, # vocab size + BLOCK_V: tl.constexpr, # tile width over vocab +): + """Compute log_softmax(logits)[label] for each row without materializing full output.""" + row = tl.program_id(0) + + logits_row_ptr = logits_ptr + row * V + label = tl.load(labels_ptr + row) + + # Pass 1: find max for numerical stability + max_val = -float("inf") + for v_start in range(0, V, BLOCK_V): + v_offsets = v_start + tl.arange(0, BLOCK_V) + mask = v_offsets < V + vals = tl.load(logits_row_ptr + v_offsets, mask=mask, other=-float("inf")) + max_val = tl.maximum(max_val, tl.max(vals, axis=0)) + + # Pass 2: compute sum(exp(x - max)) + sum_exp = 0.0 + for v_start in range(0, V, BLOCK_V): + v_offsets = v_start + tl.arange(0, BLOCK_V) + mask = v_offsets < V + vals = tl.load(logits_row_ptr + v_offsets, mask=mask, other=-float("inf")) + sum_exp += tl.sum(tl.exp(vals - max_val), axis=0) + + log_sum_exp = tl.log(sum_exp) + + # Gather: log_softmax[label] = logits[label] - max - log_sum_exp + target_logit = tl.load(logits_row_ptr + label) + result = target_logit - max_val - log_sum_exp + + tl.store(output_ptr + row, result) + + +def fused_log_softmax_gather( + logits: torch.Tensor, labels: torch.Tensor +) -> torch.Tensor: + """Compute log_softmax(logits, dim=-1).gather(-1, labels) without materializing full output. + + Args: + logits: (B, S, V) or (B*S, V) float tensor (bf16 or fp32) + labels: (B, S) or (B*S,) int64 tensor of target indices + + Returns: + (B, S) or (B*S,) float32 tensor of selected log probabilities + """ + orig_shape = logits.shape[:-1] + V = logits.shape[-1] + logits_2d = logits.reshape(-1, V).contiguous() + labels_1d = labels.reshape(-1).contiguous() + n_rows = logits_2d.shape[0] + + output = torch.empty(n_rows, device=logits.device, dtype=torch.float32) + + # Choose BLOCK_V: must be power of 2, large enough for good occupancy + BLOCK_V = min(triton.next_power_of_2(V), 65536) + + _fused_log_softmax_gather_kernel[(n_rows,)]( + logits_2d, + labels_1d, + output, + V=V, + BLOCK_V=BLOCK_V, + ) + + return output.view(orig_shape) + + +# --------------------------------------------------------------------------- +# 2. Fused masked REINFORCE loss reduction +# --------------------------------------------------------------------------- +# Instead of: (-logp * adv * mask).sum() / mask.sum() +# We do the masked multiply-accumulate in one kernel, returning (sum, count). + + +@triton.jit +def _fused_reinforce_loss_kernel( + logps_ptr, # (N,) float32 per-token log probs + advs_ptr, # (N,) float32 advantages + mask_ptr, # (N,) bool action mask + partial_sum_ptr, # (n_blocks,) partial sums + partial_cnt_ptr, # (n_blocks,) partial counts + N: tl.constexpr, + BLOCK_N: tl.constexpr, +): + block_id = tl.program_id(0) + offsets = block_id * BLOCK_N + tl.arange(0, BLOCK_N) + valid = offsets < N + + logps = tl.load(logps_ptr + offsets, mask=valid, other=0.0) + advs = tl.load(advs_ptr + offsets, mask=valid, other=0.0) + m = tl.load(mask_ptr + offsets, mask=valid, other=0).to(tl.float32) + + # -logp * advantage * mask + loss = -logps * advs * m + block_sum = tl.sum(loss, axis=0) + block_cnt = tl.sum(m, axis=0) + + tl.store(partial_sum_ptr + block_id, block_sum) + tl.store(partial_cnt_ptr + block_id, block_cnt) + + +def fused_reinforce_loss( + per_token_logps: torch.Tensor, + advantages: torch.Tensor, + action_mask: torch.Tensor, +) -> torch.Tensor: + """Compute masked REINFORCE loss: (-logp * adv * mask).sum() / mask.sum(). + + All inputs should be flat or will be flattened. Returns scalar loss tensor. + """ + logps_flat = per_token_logps.reshape(-1).contiguous() + advs_flat = advantages.reshape(-1).contiguous() + mask_flat = action_mask.reshape(-1).contiguous() + N = logps_flat.shape[0] + + BLOCK_N = 1024 + n_blocks = triton.cdiv(N, BLOCK_N) + + partial_sum = torch.empty(n_blocks, device=logps_flat.device, dtype=torch.float32) + partial_cnt = torch.empty(n_blocks, device=logps_flat.device, dtype=torch.float32) + + _fused_reinforce_loss_kernel[(n_blocks,)]( + logps_flat, + advs_flat, + mask_flat, + partial_sum, + partial_cnt, + N=N, + BLOCK_N=BLOCK_N, + ) + + total_sum = partial_sum.sum() + total_cnt = partial_cnt.sum().clamp(min=1) + return total_sum / total_cnt + + +# --------------------------------------------------------------------------- +# 3. Fused cosine similarity (batched, for EBFT rewards) +# --------------------------------------------------------------------------- +# Instead of: F.cosine_similarity(gen, gt, dim=-1) which normalizes then dots, +# we fuse the dot product, norm computation, and division into one kernel. + + +@triton.jit +def _fused_cosine_sim_kernel( + a_ptr, # (N, D) first set of vectors + b_ptr, # (N, D) second set of vectors + out_ptr, # (N,) cosine similarities + D: tl.constexpr, + BLOCK_D: tl.constexpr, +): + row = tl.program_id(0) + a_row_ptr = a_ptr + row * D + b_row_ptr = b_ptr + row * D + + dot = 0.0 + norm_a = 0.0 + norm_b = 0.0 + + for d_start in range(0, D, BLOCK_D): + d_offsets = d_start + tl.arange(0, BLOCK_D) + mask = d_offsets < D + a_vals = tl.load(a_row_ptr + d_offsets, mask=mask, other=0.0).to(tl.float32) + b_vals = tl.load(b_row_ptr + d_offsets, mask=mask, other=0.0).to(tl.float32) + + dot += tl.sum(a_vals * b_vals, axis=0) + norm_a += tl.sum(a_vals * a_vals, axis=0) + norm_b += tl.sum(b_vals * b_vals, axis=0) + + denom = tl.sqrt(norm_a) * tl.sqrt(norm_b) + denom = tl.where(denom > 1e-8, denom, 1e-8) + result = dot / denom + + tl.store(out_ptr + row, result) + + +def fused_cosine_similarity(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: + """Compute cosine similarity along the last dimension. + + Args: + a, b: (..., D) tensors of the same shape + + Returns: + (...,) tensor of cosine similarities + """ + orig_shape = a.shape[:-1] + D = a.shape[-1] + a_2d = a.reshape(-1, D).contiguous() + b_2d = b.reshape(-1, D).contiguous() + N = a_2d.shape[0] + + output = torch.empty(N, device=a.device, dtype=torch.float32) + + BLOCK_D = min(triton.next_power_of_2(D), 4096) + + _fused_cosine_sim_kernel[(N,)]( + a_2d, + b_2d, + output, + D=D, + BLOCK_D=BLOCK_D, + ) + + return output.view(orig_shape) + + +# --------------------------------------------------------------------------- +# 4. Fused pairwise diversity penalty +# --------------------------------------------------------------------------- +# Instead of: bmm(gen, gen.T) → mask diagonal → sum / (n-1) +# We compute the pairwise dot products and exclusion in one kernel. + + +@triton.jit +def _fused_diversity_kernel( + emb_ptr, # (B, N, D) embeddings, row-major + out_ptr, # (B, N) diversity penalties + N: tl.constexpr, # n_samples + D: tl.constexpr, + BLOCK_D: tl.constexpr, +): + """For each (b, i), compute mean dot product to all j != i.""" + b = tl.program_id(0) + i = tl.program_id(1) + + # Pointer to emb[b, i, :] + emb_bi_ptr = emb_ptr + (b * N + i) * D + + total_sim = 0.0 + for j in range(N): + emb_bj_ptr = emb_ptr + (b * N + j) * D + + dot = 0.0 + for d_start in range(0, D, BLOCK_D): + d_offsets = d_start + tl.arange(0, BLOCK_D) + d_mask = d_offsets < D + a_vals = tl.load(emb_bi_ptr + d_offsets, mask=d_mask, other=0.0).to( + tl.float32 + ) + b_vals = tl.load(emb_bj_ptr + d_offsets, mask=d_mask, other=0.0).to( + tl.float32 + ) + dot += tl.sum(a_vals * b_vals, axis=0) + + # Exclude self-similarity (j == i) + is_other = j != i + total_sim += dot * is_other + + result = total_sim / (N - 1) + tl.store(out_ptr + b * N + i, result) + + +def fused_diversity_penalty(embeddings: torch.Tensor) -> torch.Tensor: + """Compute mean pairwise dot product (excluding self) per sample. + + Args: + embeddings: (B, N, D) tensor where N is n_samples + + Returns: + (B, N) tensor of diversity penalties + """ + B, N, D = embeddings.shape + embeddings = embeddings.contiguous() + output = torch.zeros(B, N, device=embeddings.device, dtype=torch.float32) + if N <= 1: + return output # diversity is 0 when there's only one sample + + BLOCK_D = min(triton.next_power_of_2(D), 4096) + + _fused_diversity_kernel[(B, N)]( + embeddings, + output, + N=N, + D=D, + BLOCK_D=BLOCK_D, + ) + + return output diff --git a/src/axolotl/core/trainers/ebft/rewards.py b/src/axolotl/core/trainers/ebft/rewards.py new file mode 100644 index 0000000000..993650bbe9 --- /dev/null +++ b/src/axolotl/core/trainers/ebft/rewards.py @@ -0,0 +1,264 @@ +""" +Feature-matching reward utilities for Energy-Based Fine-Tuning (EBFT). + +Ported from: feature-002/ebft_openrlhf/openrlhf/utils/embedding_utils.py +Paper: "Matching Features, Not Tokens: Energy-Based Fine-Tuning of Language Models" + (Jelassi et al., 2026) https://arxiv.org/abs/2603.12248 +""" + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +@torch.no_grad() +def extract_hidden_states( + model: nn.Module, + input_ids: torch.Tensor, + attention_mask: torch.Tensor, + layer_indices: list[int], + batch_size: int | None = None, +) -> torch.Tensor: + """ + Forward pass through model, extracting and concatenating hidden states + at specified layer indices. + + Args: + model: The frozen feature network + input_ids: (B, S) token ids + attention_mask: (B, S) attention mask + layer_indices: List of layer indices to extract (e.g., [8, 16, 24] for 32-layer model) + batch_size: If set, process in chunks to reduce peak memory + + Returns: + Concatenated hidden states: (B, S, num_layers * H) + """ + if batch_size is None: + batch_size = input_ids.shape[0] + + # Use the inner transformer body (skips lm_head) when available. + # This avoids the expensive hidden_dim × vocab_size matmul whose + # output (logits) is never used — only hidden_states are needed. + body = getattr(model, "model", None) + if body is not None and hasattr(body, "forward"): + forward_model = body + else: + forward_model = model + + all_features = [] + for i in range(0, input_ids.shape[0], batch_size): + chunk_ids = input_ids[i : i + batch_size] + chunk_mask = attention_mask[i : i + batch_size] + + outputs = forward_model( + chunk_ids, + attention_mask=chunk_mask, + output_hidden_states=True, + return_dict=True, + ) + + # hidden_states is a tuple of (num_layers + 1) tensors, each (B, S, H) + # index 0 is the embedding layer output + hidden_states = outputs.hidden_states + chunk_features = [] + for idx in layer_indices: + chunk_features.append(hidden_states[idx]) + + # Concatenate across feature dimension: (B, S, num_layers * H) + all_features.append(torch.cat(chunk_features, dim=-1)) + + return torch.cat(all_features, dim=0) + + +def apply_embed_method( + hidden_states: torch.Tensor, + method: str, + attention_mask: torch.Tensor | None = None, + prompt_lengths: torch.Tensor | None = None, +) -> torch.Tensor: + """ + Pool per-token hidden states into per-sequence embeddings. + + Args: + hidden_states: (B, S, D) concatenated hidden states + method: One of "last_token", "mean_pooling", "completion_mean", "concat" + attention_mask: (B, S) mask for mean pooling + prompt_lengths: (B,) number of prompt tokens per sample (for completion_mean) + + Returns: + Sequence embeddings: (B, D) for last_token/mean_pooling/completion_mean, + (B, 3*D) for concat + """ + if method == "last_token": + if attention_mask is not None: + # Find last non-padding position per sample + last_idx = attention_mask.sum(dim=1).long() - 1 # (B,) + return hidden_states[torch.arange(hidden_states.shape[0]), last_idx] + return hidden_states[:, -1, :] + + if method == "mean_pooling": + if attention_mask is not None: + mask = attention_mask.unsqueeze(-1).float() # (B, S, 1) + return (hidden_states * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1) + return hidden_states.mean(dim=1) + + if method == "completion_mean": + # Mean pool over completion tokens only (exclude prompt) + if prompt_lengths is None: + raise ValueError("completion_mean requires prompt_lengths") + B, S, _ = hidden_states.shape + positions = torch.arange(S, device=hidden_states.device).unsqueeze(0) # (1, S) + comp_mask = positions >= prompt_lengths.unsqueeze(1) # (B, S) + if attention_mask is not None: + comp_mask = comp_mask & attention_mask.bool() + mask = comp_mask.unsqueeze(-1).float() # (B, S, 1) + return (hidden_states * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1) + + if method == "concat": + B, S, D = hidden_states.shape + if attention_mask is not None: + valid_lens = attention_mask.sum(dim=1).long() # (B,) + else: + valid_lens = torch.full( + (B,), S, device=hidden_states.device, dtype=torch.long + ) + # Compute quartile positions relative to valid length per sample + # First valid position index for each sample (handles right-padding) + q1 = (valid_lens // 4).clamp(min=0, max=S - 1) + q2 = (valid_lens // 2).clamp(min=0, max=S - 1) + q3 = (3 * valid_lens // 4).clamp(min=0, max=S - 1) + batch_idx = torch.arange(B, device=hidden_states.device) + return torch.cat( + [ + hidden_states[batch_idx, q1], + hidden_states[batch_idx, q2], + hidden_states[batch_idx, q3], + ], + dim=-1, + ) + + raise ValueError(f"Unknown embed_method: {method}") + + +@torch.no_grad() +def get_alignment_rewards( + gen_embedding: torch.Tensor, + gt_embedding: torch.Tensor, +) -> torch.Tensor: + """ + Compute alignment reward as cosine similarity between generated + and ground-truth feature embeddings. + + Args: + gen_embedding: (B, D) generated sequence embeddings + gt_embedding: (B, D) ground-truth sequence embeddings + If num_generations > 1, gt_embedding should be repeated + to match gen_embedding's batch dim. + + Returns: + Alignment rewards: (B,) cosine similarities in [-1, 1] + """ + return F.cosine_similarity(gen_embedding, gt_embedding, dim=-1) + + +@torch.no_grad() +def get_diversity_rewards( + gen_embedding: torch.Tensor, + num_generations: int, +) -> torch.Tensor: + """ + Compute diversity penalty as mean pairwise dot-product similarity + between samples from the same prompt (excluding self-similarity). + + Args: + gen_embedding: (B, D) generated embeddings where B = num_prompts * num_generations + num_generations: Number of generations per prompt + + Returns: + Diversity penalties: (B,) mean similarity to other samples from same prompt + """ + if num_generations <= 1: + return torch.zeros(gen_embedding.shape[0], device=gen_embedding.device) + + num_prompts = gen_embedding.shape[0] // num_generations + + # Reshape to (num_prompts, num_generations, D) + reshaped = gen_embedding.view(num_prompts, num_generations, -1) + + # Pairwise dot products within each group: (num_prompts, num_generations, num_generations) + sims = torch.bmm(reshaped, reshaped.transpose(1, 2)) + + # Zero out self-similarity (diagonal) + eye = torch.eye(num_generations, device=sims.device, dtype=torch.bool) + sims = sims.masked_fill(eye.unsqueeze(0), 0.0) + + # Mean similarity to other samples: (num_prompts, num_generations) + diversity = sims.sum(dim=-1) / (num_generations - 1) + + # Flatten back to (B,) + return diversity.view(-1) + + +def whiten_embeddings_batched( + phi: torch.Tensor, + phi_gt: torch.Tensor, + whiten_tol: float = 1e-5, + normalize: bool = False, +) -> tuple[torch.Tensor, torch.Tensor]: + """ + Whiten generated embeddings using SVD, then apply same transform to ground-truth. + + Whitening decorrelates feature dimensions so no single direction dominates + the feature-matching loss. Uses pseudo-inverse for rank-deficient cases. + + Note: Singular values scale with sqrt(B), so reward magnitudes are + batch-size dependent. This is acceptable because B = n_samples_per_prompt + which is fixed during training (typically 2-4). + + Args: + phi: (B, D) generated embeddings (used to estimate covariance) + phi_gt: (B, D) ground-truth embeddings + whiten_tol: Tolerance for singular value cutoff + normalize: If True, L2-normalize after whitening + + Returns: + Whitened (phi, phi_gt) tuple, each (B, D) + """ + phi_f = phi.float() + phi_gt_f = phi_gt.float() + + # Feature-space SVD: operate on phi_f.T (D, B) so U is (D, D) + try: + U, S, _ = torch.linalg.svd(phi_f.T.unsqueeze(0), full_matrices=False) + except torch._C._LinAlgError: + # Fallback: add small noise + noise = 1e-6 * phi_f.abs().mean() + try: + U, S, _ = torch.linalg.svd( + (phi_f.T + noise * torch.randn_like(phi_f.T)).unsqueeze(0), + full_matrices=False, + ) + except torch._C._LinAlgError: + if normalize: + return ( + F.normalize(phi, p=2, dim=-1), + F.normalize(phi_gt, p=2, dim=-1), + ) + return phi, phi_gt + + U, S = U.squeeze(0), S.squeeze(0) # U: (D, min(D,B)), S: (min(D,B),) + + # Safe inverse of singular values + s_max = S.max() + inv_s = torch.where(S > whiten_tol * s_max, 1.0 / (S + 1e-12), torch.zeros_like(S)) + + # W = U @ diag(inv_s) @ U^T — feature-space whitening matrix (D, D) + W = (U * inv_s.unsqueeze(0)) @ U.T # (D, D) + phi_w = (phi_f @ W).to(phi.dtype) # (B, D) + phi_gt_w = (phi_gt_f @ W).to(phi_gt.dtype) # (B, D) + + if normalize: + phi_w = F.normalize(phi_w, p=2, dim=-1) + phi_gt_w = F.normalize(phi_gt_w, p=2, dim=-1) + + return phi_w, phi_gt_w diff --git a/src/axolotl/core/trainers/ebft/strided.py b/src/axolotl/core/trainers/ebft/strided.py new file mode 100644 index 0000000000..5cfc5b99bd --- /dev/null +++ b/src/axolotl/core/trainers/ebft/strided.py @@ -0,0 +1,1152 @@ +""" +Strided block-parallel EBFT trainer for unstructured text data. + +This trainer implements the full EBFT algorithm from the paper, including +strided block-parallel generation where multiple short rollouts are generated +at different anchor points within a single document. This is essential for +training on raw text data (code, prose, etc.) without prompt/completion splits. + +Uses torch flex_attention with a compiled block mask for efficient strided +attention patterns. Falls back to eager attention with dense 4D masks when +flex_attention is not available. + +Paper: "Matching Features, Not Tokens: Energy-Based Fine-Tuning of Language Models" + (Jelassi et al., 2026) https://arxiv.org/abs/2603.12248 +""" + +import contextlib +import copy + +import torch +import torch.nn.functional as F +from transformers import Trainer + +from axolotl.core.trainers.ebft.kernels import ( # noqa: F401 — available for future use + fused_cosine_similarity, + fused_diversity_penalty, + fused_log_softmax_gather, + fused_reinforce_loss, +) +from axolotl.core.trainers.ebft.rewards import ( + whiten_embeddings_batched, +) +from axolotl.core.trainers.mixins import ( + DistributedParallelMixin, + RngLoaderMixin, + SchedulerMixin, +) +from axolotl.core.trainers.mixins.optimizer import OptimizerInitMixin, OptimizerMixin +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +# Check flex_attention availability +_FLEX_ATTENTION_AVAILABLE = False +try: + from torch.nn.attention.flex_attention import ( + create_block_mask, + ) + + _FLEX_ATTENTION_AVAILABLE = True +except ImportError: + pass + + +def _patch_flex_attention_dtype(): + """ + Patch HF's flex_attention_forward to cast q/k/v to a uniform dtype. + + This fixes the incompatibility between flex_attention and gradient + checkpointing, where recomputation can produce q/k in float32 while + v stays in bfloat16. flex_attention requires all three to match. + """ + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + original_fn = ALL_ATTENTION_FUNCTIONS.get("flex_attention") + if original_fn is None: + return + + def patched_flex_attention_forward( + module, query, key, value, attention_mask, **kwargs + ): + # Cast q/k/v to the same dtype (use value's dtype as reference, + # since model weights stay in the original dtype) + target_dtype = value.dtype + if query.dtype != target_dtype: + query = query.to(target_dtype) + if key.dtype != target_dtype: + key = key.to(target_dtype) + return original_fn(module, query, key, value, attention_mask, **kwargs) + + ALL_ATTENTION_FUNCTIONS["flex_attention"] = patched_flex_attention_forward + + +@contextlib.contextmanager +def override_attn_implementation(model, implementation: str): + """Temporarily override a model's attention implementation. + + Useful for forcing eager attention during generation (where sequence + lengths change each step, causing dynamo recompiles) while keeping + flex_attention for the fixed-size training forward pass. + + Usage:: + + with override_attn_implementation(model, "eager"): + output = model(input_ids, attention_mask=dense_4d_mask, ...) + """ + config = getattr(model, "config", None) + if config is None or not hasattr(config, "_attn_implementation"): + yield + return + + saved = config._attn_implementation + config._attn_implementation = implementation + try: + yield + finally: + config._attn_implementation = saved + + +# --------------------------------------------------------------------------- +# Strided attention mask builders +# --------------------------------------------------------------------------- + + +def _strided_mask_mod( + b, + h, + q_idx, + kv_idx, + prompt_length, + context_length, + max_generation_length, + stride, + num_blocks, +): + """ + Mask mod function for flex_attention's create_block_mask. + + Defines the strided block-parallel attention pattern: + - Prompt region: standard causal + - Generated region: each block attends to its context window + same-block predecessors + """ + # --- Prompt region: standard causal --- + is_prompt_q = q_idx < prompt_length + is_prompt_kv = kv_idx < prompt_length + prompt_causal = is_prompt_q & is_prompt_kv & (q_idx >= kv_idx) + + # --- Generated region --- + is_gen_q = ~is_prompt_q + # Which generation step and block does this query token belong to? + gen_offset = q_idx - prompt_length + gen_step = gen_offset // num_blocks + block_idx = gen_offset % num_blocks + + # Context window end for this block. + # Note: if prompt_length < max_generation_length, context_end clamps to 0 for all + # blocks. This is safe because compute_loss guards with num_blocks <= 0 → zero loss. + context_end = torch.clamp( + block_idx * stride + context_length, + max=prompt_length - max_generation_length, + ) + + # Rule 1: Generated token attends to its context window in the prompt + in_context = is_gen_q & is_prompt_kv & (kv_idx < context_end) + + # Rule 2: Self-attention + is_self = q_idx == kv_idx + + # Rule 3: Attend to earlier tokens in the SAME block (same block_idx, earlier gen_step) + is_gen_kv = ~is_prompt_kv + kv_gen_offset = kv_idx - prompt_length + kv_gen_step = kv_gen_offset // num_blocks + kv_block_idx = kv_gen_offset % num_blocks + same_block_prev = ( + is_gen_q & is_gen_kv & (kv_block_idx == block_idx) & (kv_gen_step < gen_step) + ) + + return prompt_causal | in_context | is_self | same_block_prev + + +def create_strided_block_mask( + prompt_length: int, + context_length: int, + max_generation_length: int, + stride: int, + num_blocks: int, + full_sequence_length: int, + batch_size: int, + num_heads: int, + device: torch.device, +): + """ + Create a BlockMask for flex_attention using the strided EBFT pattern. + + Returns a BlockMask that can be passed directly to model.forward() + when using attn_implementation="flex_attention". + + Parameters that vary across training steps (context_length, num_blocks) + are captured as tensors so torch.compile/dynamo treats them as dynamic + values rather than guarding on literal int values (which causes recompiles). + """ + # Wrap ALL mask params as 0-d tensors to prevent dynamo from guarding + # on their int values. Without this, each new anchor_offset or num_blocks + # triggers a recompile until the limit is hit → unfused fallback → OOM. + _prompt_length = torch.tensor(prompt_length, device=device) + _context_length = torch.tensor(context_length, device=device) + _max_gen_len = torch.tensor(max_generation_length, device=device) + _stride = torch.tensor(stride, device=device) + _num_blocks = torch.tensor(num_blocks, device=device) + + def mask_mod(b, h, q_idx, kv_idx): + return _strided_mask_mod( + b, + h, + q_idx, + kv_idx, + prompt_length=_prompt_length, + context_length=_context_length, + max_generation_length=_max_gen_len, + stride=_stride, + num_blocks=_num_blocks, + ) + + block_mask = create_block_mask( + mask_mod, + B=batch_size, + H=None, # broadcast across heads + Q_LEN=full_sequence_length, + KV_LEN=full_sequence_length, + device=device, + ) + return block_mask + + +def build_strided_position_ids( + full_sequence_length: int, + prompt_length: int, + context_length: int, + generation_step: int, + stride: int, + num_blocks: int, + device: torch.device, + batch_size: int = 1, +): + """Build position IDs for strided generation (shared between flex and eager modes).""" + position_ids = torch.empty( + (batch_size, full_sequence_length), dtype=torch.long, device=device + ) + position_ids[:, :prompt_length] = torch.arange(prompt_length, device=device) + + block_starting_positions = ( + torch.arange(num_blocks, device=device) * stride + context_length + ) + for gen_step in range(generation_step): + start = prompt_length + gen_step * num_blocks + end = start + num_blocks + position_ids[:, start:end] = block_starting_positions + gen_step + + return position_ids + + +def build_strided_dense_mask_and_positions( + full_sequence_length: int, + prompt_length: int, + context_length: int, + generation_step: int, + max_generation_length: int, + stride: int, + num_blocks: int, + device: torch.device, + batch_size: int = 1, + dtype: torch.dtype = torch.bfloat16, +): + """Build dense 4D attention mask (eager fallback) + position IDs.""" + min_value = torch.finfo(dtype).min + attention_mask = torch.full( + (batch_size, 1, full_sequence_length, full_sequence_length), + min_value, + dtype=dtype, + device=device, + ) + + causal_mask = torch.tril( + torch.ones((prompt_length, prompt_length), dtype=torch.bool, device=device) + ) + attention_mask[:, :, :prompt_length, :prompt_length].masked_fill_( + causal_mask.view(1, 1, prompt_length, prompt_length), 0.0 + ) + + for gen_step in range(generation_step): + for block_idx in range(num_blocks): + gen_pos = prompt_length + gen_step * num_blocks + block_idx + context_end = min( + block_idx * stride + context_length, + prompt_length - max_generation_length, + ) + attention_mask[:, 0, gen_pos, :context_end] = 0.0 + attention_mask[:, 0, gen_pos, gen_pos] = 0.0 + if gen_step > 0: + for prev_s in range(gen_step): + prev_pos = prompt_length + prev_s * num_blocks + block_idx + attention_mask[:, 0, gen_pos, prev_pos] = 0.0 + + position_ids = build_strided_position_ids( + full_sequence_length, + prompt_length, + context_length, + generation_step, + stride, + num_blocks, + device, + batch_size, + ) + return attention_mask, position_ids + + +# --------------------------------------------------------------------------- +# Trainer +# --------------------------------------------------------------------------- + + +class AxolotlStridedEBFTTrainer( + RngLoaderMixin, + SchedulerMixin, + OptimizerMixin, + OptimizerInitMixin, + DistributedParallelMixin, + Trainer, +): + """ + Strided block-parallel EBFT trainer for unstructured text data. + + Takes full text documents (no prompt/completion split needed), generates + short rollouts at multiple anchor points via strided attention, and trains + with feature-matching rewards. + + When flex_attention is available (torch >= 2.5), uses compiled block masks + for efficient fused attention kernels. Otherwise falls back to eager + attention with dense 4D masks. + """ + + _tag_names = ["ebft", "strided", "axolotl"] + + def __init__(self, model, args, train_dataset, **kwargs): + super().__init__(model=model, args=args, train_dataset=train_dataset, **kwargs) + + # EBFT config + self.ebft_stride = getattr(args, "ebft_stride", 8) + self.ebft_context_length = getattr(args, "ebft_context_length", 8) + self.ebft_generate_max_len = getattr(args, "ebft_generate_max_len", 8) + self.ebft_n_samples = getattr(args, "ebft_n_samples_per_prompt", 4) + self.ebft_temperature = getattr(args, "ebft_temperature", 0.6) + self.ebft_top_p = getattr(args, "ebft_top_p", 1.0) + self.ebft_alignment_coef = getattr(args, "ebft_alignment_coef", 1.0) + self.ebft_diversity_coef = getattr(args, "ebft_diversity_coef", 1.0) + self.ebft_rl_coef = getattr(args, "ebft_rl_coef", 1.0) + self.ebft_ce_coef = getattr(args, "ebft_ce_coef", 0.0) + self.ebft_use_whitening = getattr(args, "ebft_use_whitening", False) + self.ebft_advantage_estimator = getattr( + args, "ebft_advantage_estimator", "rloo" + ) + self.ebft_min_completion_prefix = getattr(args, "ebft_min_completion_prefix", 0) + + # Validate config combinations + if self.ebft_use_whitening and self.ebft_diversity_coef > 0: + LOG.info( + "ebft: whitening + diversity enabled. Per paper Variant (i) (eq 49): " + "alignment uses cosine similarity (normalized), diversity uses raw dot product. " + "Both are bounded after whitening." + ) + if self.ebft_n_samples == 1 and self.ebft_diversity_coef > 0: + LOG.warning( + "ebft.n_samples_per_prompt=1 with diversity_coef > 0: diversity penalty requires " + "multiple samples. Setting diversity_coef to 0." + ) + self.ebft_diversity_coef = 0.0 + if self.ebft_n_samples == 1 and self.ebft_advantage_estimator == "rloo": + LOG.warning( + "ebft.n_samples_per_prompt=1 with advantage_estimator='rloo': RLOO requires " + "multiple samples for baseline. Falling back to 'reinforce'." + ) + self.ebft_advantage_estimator = "reinforce" + + # Feature network config + feature_layers_frac = getattr(args, "ebft_feature_layers", [0.25, 0.5, 0.75]) + embed_method = getattr(args, "ebft_embed_method", "last_token") + self.ebft_embed_method = embed_method + + # Attention implementation selection + unwrapped = self.accelerator.unwrap_model(self.model) + self.use_flex_attention = ( + _FLEX_ATTENTION_AVAILABLE and torch.cuda.is_available() + ) + + if self.use_flex_attention: + _patch_flex_attention_dtype() + LOG.info("Using flex_attention for strided EBFT (compiled block masks)") + if hasattr(unwrapped.config, "_attn_implementation"): + unwrapped.config._attn_implementation = "flex_attention" + self._num_heads = unwrapped.config.num_attention_heads + else: + LOG.info("Using eager attention for strided EBFT (dense 4D masks)") + if hasattr(unwrapped.config, "_attn_implementation"): + unwrapped.config._attn_implementation = "eager" + self._num_heads = None + + # Feature network setup: either share weights with actor (PEFT models) + # or deepcopy (full-parameter models / multi-GPU). + first_param = next(unwrapped.parameters()) + original_device = first_param.device + actor_gpu = ( + original_device.index + if (original_device.type == "cuda" and original_device.index is not None) + else 0 + ) + visible_gpus = torch.cuda.device_count() + + # Check if we can share weights (PEFT model on single GPU) + from peft import PeftModel + + self._share_feature_weights = ( + isinstance(unwrapped, PeftModel) + and visible_gpus == 1 + and original_device.type != "meta" + ) + + if self._share_feature_weights: + # Share weights: use actor's base model with adapters disabled for + # feature extraction. Saves ~2.5 GB (no deepcopy of base weights). + self.feature_network = None # no separate network + self._feature_device = torch.device(f"cuda:{actor_gpu}") + self._feature_use_flex = self.use_flex_attention + LOG.info( + "Feature network shares actor weights (PEFT disable_adapter). " + f"Saving {sum(p.numel() for p in unwrapped.parameters()) * 2 / 1e9:.1f} GB" + ) + elif visible_gpus > 1 and original_device.type != "meta": + # Multi-GPU: deepcopy to a separate device + self.feature_network = copy.deepcopy(unwrapped) + self.feature_network.to(dtype=torch.bfloat16) + self._feature_device = torch.device( + f"cuda:{(actor_gpu + 1) % visible_gpus}" + ) + LOG.info(f"Creating frozen feature network on {self._feature_device}...") + self.feature_network.to(device=self._feature_device) + if _FLEX_ATTENTION_AVAILABLE and self._feature_device.type == "cuda": + if hasattr(self.feature_network.config, "_attn_implementation"): + self.feature_network.config._attn_implementation = "flex_attention" + self._feature_use_flex = True + LOG.info("Feature network using flex_attention") + else: + if hasattr(self.feature_network.config, "_attn_implementation"): + self.feature_network.config._attn_implementation = "eager" + self._feature_use_flex = False + for param in self.feature_network.parameters(): + param.requires_grad = False + self.feature_network.eval() + elif original_device.type == "meta": + # FSDP2 with cpu_ram_efficient_loading + from transformers import AutoModelForCausalLM + + feature_model_name = ( + getattr(args, "model_name_or_path", None) + or unwrapped.config._name_or_path + ) + self.feature_network = AutoModelForCausalLM.from_pretrained( + feature_model_name, + torch_dtype=torch.bfloat16, + attn_implementation="eager", + ) + self._feature_device = torch.device(f"cuda:{actor_gpu}") + self.feature_network.to(device=self._feature_device) + self._feature_use_flex = False + for param in self.feature_network.parameters(): + param.requires_grad = False + self.feature_network.eval() + LOG.warning("Feature network loaded from pretrained (meta device)") + else: + # Single-GPU, non-PEFT: deepcopy on same device + self.feature_network = copy.deepcopy(unwrapped) + self.feature_network.to(dtype=torch.bfloat16) + self._feature_device = torch.device(f"cuda:{actor_gpu}") + self.feature_network.to(device=self._feature_device) + if _FLEX_ATTENTION_AVAILABLE: + if hasattr(self.feature_network.config, "_attn_implementation"): + self.feature_network.config._attn_implementation = "flex_attention" + self._feature_use_flex = True + else: + if hasattr(self.feature_network.config, "_attn_implementation"): + self.feature_network.config._attn_implementation = "eager" + self._feature_use_flex = False + for param in self.feature_network.parameters(): + param.requires_grad = False + self.feature_network.eval() + LOG.info( + f"Created frozen feature network (deepcopy) on {self._feature_device}" + ) + + num_layers = unwrapped.config.num_hidden_layers + self.feature_layer_indices = [ + int(frac * num_layers) for frac in feature_layers_frac + ] + LOG.info( + f"Strided EBFT: layers={self.feature_layer_indices}, " + f"stride={self.ebft_stride}, ctx={self.ebft_context_length}, " + f"gen_len={self.ebft_generate_max_len}, n_samples={self.ebft_n_samples}, " + f"embed={embed_method}, flex_attn={self.use_flex_attention}, " + f"min_completion_prefix={self.ebft_min_completion_prefix}" + ) + + def _build_strided_mask( + self, + full_seq_len, + seq_len, + generation_step, + num_blocks, + batch_size, + device, + dtype, + anchor_offset=None, + ): + """Build strided attention mask + position IDs using flex or eager. + + Args: + anchor_offset: Position where anchors start. For unstructured data this + equals context_length; for structured data it equals + max(prompt_length + min_completion_prefix, context_length). + Defaults to self.ebft_context_length if not provided. + """ + if anchor_offset is None: + anchor_offset = self.ebft_context_length + + pos_ids = build_strided_position_ids( + full_seq_len, + seq_len, + anchor_offset, + generation_step, + self.ebft_stride, + num_blocks, + device, + batch_size, + ) + + if self.use_flex_attention: + block_mask = create_strided_block_mask( + prompt_length=seq_len, + context_length=anchor_offset, + max_generation_length=self.ebft_generate_max_len, + stride=self.ebft_stride, + num_blocks=num_blocks, + full_sequence_length=full_seq_len, + batch_size=batch_size, + num_heads=self._num_heads, + device=device, + ) + return block_mask, pos_ids + + dense_mask, pos_ids = build_strided_dense_mask_and_positions( + full_sequence_length=full_seq_len, + prompt_length=seq_len, + context_length=anchor_offset, + generation_step=generation_step, + max_generation_length=self.ebft_generate_max_len, + stride=self.ebft_stride, + num_blocks=num_blocks, + device=device, + batch_size=batch_size, + dtype=dtype, + ) + return dense_mask, pos_ids + + def compute_loss( + self, model, inputs, return_outputs=False, num_items_in_batch=None + ): + """ + Full strided EBFT training step. + + 1. Take tokenized documents from inputs + 2. Generate n_samples short rollouts at strided anchor points + 3. Extract features from frozen network for both generated and GT blocks + 4. Compute alignment/diversity rewards per block + 5. Compute RLOO advantages + 6. Policy gradient loss on the strided forward pass + + Supports both unstructured text (no prompt/completion split) and + structured data (prompt + completion with labels masking). For structured + data, anchors are placed only within the completion span. + """ + outputs = None + device = next(model.parameters()).device + input_ids = inputs["input_ids"].to(device) # (B, seq_len) + B, seq_len = input_ids.shape + + stride = self.ebft_stride + ctx_len = self.ebft_context_length + gen_len = self.ebft_generate_max_len + n_samples = self.ebft_n_samples + + # --- Detect structured data and compute anchor_offset --- + # For structured data, anchors must start within the completion span. + # anchor_offset replaces ctx_len as the starting position for anchors. + is_structured = False + if "prompt_length" in inputs: + # Explicit prompt_length from dataset transform + prompt_lengths = inputs["prompt_length"].to(device) # (B,) + is_structured = True + elif "labels" in inputs: + # Derive prompt_length from labels: first position where labels != -100 + labels = inputs["labels"].to(device) + non_masked = labels != -100 + # prompt_length = index of first non-masked token (or seq_len if all masked) + has_completion = non_masked.any(dim=1) + prompt_lengths = torch.where( + has_completion, + non_masked.float().argmax(dim=1), + torch.tensor(seq_len, device=device, dtype=torch.float), + ).long() + is_structured = prompt_lengths.min().item() > 0 + + if is_structured: + # Use max prompt_length across batch for uniform anchor_offset + max_prompt_len = prompt_lengths.max().item() + anchor_offset = max( + max_prompt_len + self.ebft_min_completion_prefix, ctx_len + ) + else: + anchor_offset = ctx_len + + num_blocks = (seq_len - gen_len - anchor_offset) // stride + 1 + if num_blocks <= 0: + LOG.warning( + f"Sequence too short for strided EBFT: seq_len={seq_len}, " + f"anchor_offset={anchor_offset}, " + f"need >= {gen_len + anchor_offset + stride}. Returning zero loss." + ) + dummy_loss = input_ids.float().mean() * 0.0 + return (dummy_loss, None) if return_outputs else dummy_loss + + # --- Step 1: Generate strided blocks for n_samples --- + repeated_ids = input_ids.repeat_interleave(n_samples, dim=0) + + with torch.no_grad(): + full_sequences = self._generate_strided_blocks( + model, + repeated_ids, + num_blocks, + anchor_offset=anchor_offset, + ) + + # --- Step 2: Build strided mask for full generation --- + full_seq_len = full_sequences.shape[1] + model_dtype = next(model.parameters()).dtype + + # Free generation-phase memory before training forward pass + torch.cuda.empty_cache() + + attn_mask, pos_ids = self._build_strided_mask( + full_seq_len, + seq_len, + gen_len, + num_blocks, + B * n_samples, + device, + model_dtype, + anchor_offset=anchor_offset, + ) + + # --- Step 3: Forward pass through actor for log probs --- + # Memory optimization: process one sample at a time through the backbone + # to avoid B*N × S × H activation memory. For Llama-1B at S=3900, each + # sample's backbone forward takes ~8.7 GB with grad checkpointing. + # Processing B*N=4 at once would need ~35 GB → OOM. + # Instead, we accumulate per-token logprobs sample-by-sample. + gen_start = seq_len - 1 # shifted index where generated tokens start + compute_start = 0 if self.ebft_ce_coef > 0 else gen_start + BN = B * n_samples + + unwrapped_model = self.accelerator.unwrap_model(model) + # Navigate through PEFT wrapper to get backbone + lm_head + base_model = getattr(unwrapped_model, "model", unwrapped_model) + if hasattr(base_model, "model") and hasattr(base_model, "lm_head"): + backbone = base_model.model + lm_head = base_model.lm_head + else: + backbone = None + + per_token_logps_list = [] + shift_labels = full_sequences[:, 1:] + + if backbone is not None: + # Process one sample at a time: backbone → chunked lm_head → logprobs + # This keeps peak memory at ~1 sample's activations instead of B*N. + for s_idx in range(BN): + seq_s = full_sequences[s_idx : s_idx + 1] # (1, full_seq_len) + # Handle attention mask format (BlockMask vs dense 4D) + if isinstance(attn_mask, torch.Tensor) and attn_mask.dim() == 4: + mask_s = attn_mask[s_idx : s_idx + 1] + else: + mask_s = attn_mask # BlockMask broadcasts over batch + pos_s = pos_ids[s_idx : s_idx + 1] + + with torch.autocast(device_type="cuda", dtype=torch.bfloat16): + backbone_out = backbone( + seq_s, + attention_mask=mask_s, + position_ids=pos_s, + return_dict=True, + ) + hidden_s = backbone_out.last_hidden_state # (1, full_seq_len, H) + labels_s = shift_labels[s_idx : s_idx + 1] + + logps_s = torch.zeros( + 1, + hidden_s.shape[1] - 1, + device=device, + dtype=torch.float32, + ) + + region_h = hidden_s[:, compute_start:-1, :] + region_l = labels_s[:, compute_start:] + chunk_size = 256 + for i in range(0, region_h.shape[1], chunk_size): + h_chunk = region_h[:, i : i + chunk_size, :] + l_chunk = region_l[:, i : i + chunk_size] + with torch.autocast(device_type="cuda", dtype=torch.bfloat16): + logits_chunk = lm_head(h_chunk) + chunk_lp = F.log_softmax(logits_chunk.float(), dim=-1) + logps_s[ + :, compute_start + i : compute_start + i + h_chunk.shape[1] + ] = chunk_lp.gather(-1, l_chunk.unsqueeze(-1)).squeeze(-1) + del logits_chunk, chunk_lp + per_token_logps_list.append(logps_s) + del hidden_s, backbone_out, region_h + + per_token_logps = torch.cat(per_token_logps_list, dim=0) + else: + # Fallback: full forward (non-standard model architecture) + with torch.autocast(device_type="cuda", dtype=torch.bfloat16): + outputs = model( + full_sequences, + attention_mask=attn_mask, + position_ids=pos_ids, + return_dict=True, + ) + logits = outputs.logits + per_token_logps = torch.zeros( + logits.shape[0], + logits.shape[1] - 1, + device=device, + dtype=torch.float32, + ) + region_logits = logits[:, compute_start:-1, :] + region_labels = shift_labels[:, compute_start:] + chunk_size = 256 + for i in range(0, region_logits.shape[1], chunk_size): + chunk_logits = region_logits[:, i : i + chunk_size, :] + chunk_labels = region_labels[:, i : i + chunk_size] + chunk_lp = F.log_softmax(chunk_logits.float(), dim=-1) + per_token_logps[ + :, compute_start + i : compute_start + i + chunk_logits.shape[1] + ] = chunk_lp.gather(-1, chunk_labels.unsqueeze(-1)).squeeze(-1) + del logits, region_logits + + action_mask = torch.zeros( + per_token_logps.shape, dtype=torch.bool, device=device + ) + # Only mark actual generated tokens (not padding beyond num_blocks * gen_len) + gen_end = gen_start + num_blocks * gen_len + action_mask[:, gen_start:gen_end] = True + + # --- Step 4: Extract features and compute rewards --- + with torch.no_grad(): + block_rewards = self._compute_block_rewards( + full_sequences, + attn_mask, + pos_ids, + input_ids, + num_blocks, + B, + n_samples, + anchor_offset=anchor_offset, + ) + + del attn_mask, pos_ids + torch.cuda.empty_cache() + + # --- Step 5: Compute advantages --- + advantages_per_block = self._compute_advantages( + block_rewards, B, n_samples, num_blocks + ) + + token_advantages = advantages_per_block.repeat_interleave(gen_len, dim=1) + full_advantages = torch.zeros_like(per_token_logps) + # Only fill actual generated region (not padding beyond num_blocks * gen_len) + adv_len = token_advantages.shape[1] # = num_blocks * gen_len + full_advantages[:, gen_start : gen_start + adv_len] = token_advantages + + # --- Step 6: Compute loss --- + # RL loss: REINFORCE on generated tokens (needs grad through per_token_logps) + rl_loss_per_token = -per_token_logps * full_advantages.detach() + rl_loss = ( + rl_loss_per_token * action_mask.float() + ).sum() / action_mask.float().sum().clamp(min=1) + + # CE loss: For structured data, only compute on completion ground-truth tokens + # (labels != -100 in the original input). For unstructured data, compute on + # all non-action (prompt) tokens as before. + ce_loss = torch.tensor(0.0, device=device) + if self.ebft_ce_coef > 0: + if is_structured and "labels" in inputs: + labels = inputs["labels"].to(device) # (B, seq_len) + shifted_labels = labels[:, 1:] # (B, seq_len - 1) + ce_mask_base = shifted_labels != -100 # (B, seq_len - 1) + ce_mask_repeated = ce_mask_base.repeat_interleave(n_samples, dim=0) + ce_mask = torch.zeros( + per_token_logps.shape, dtype=torch.bool, device=device + ) + ce_mask[:, : ce_mask_repeated.shape[1]] = ce_mask_repeated + ce_mask[:, gen_start:] = False + else: + ce_mask = ~action_mask + ce_loss = ( + -per_token_logps * ce_mask.float() + ).sum() / ce_mask.float().sum().clamp(min=1) + + loss = self.ebft_rl_coef * rl_loss + self.ebft_ce_coef * ce_loss + + # --- Log metrics --- + if self.state.global_step % self.args.logging_steps == 0: + _alignment = getattr(self, "_last_alignment", 0.0) + _diversity = getattr(self, "_last_diversity", 0.0) + _cfm = getattr(self, "_last_cfm", 0.0) + _mean_reward = block_rewards.mean().item() + _adv_std = advantages_per_block.std().item() + + log_dict = { + "ebft/rl_loss": rl_loss.item(), + "ebft/ce_loss": ce_loss.item(), + "ebft/cfm_loss": _cfm, + "ebft/mean_reward": _mean_reward, + "ebft/alignment": _alignment, + "ebft/diversity": _diversity, + "ebft/num_blocks": num_blocks, + "ebft/advantages_std": _adv_std, + } + if is_structured: + log_dict["ebft/anchor_offset"] = anchor_offset + self.log(log_dict) + + # Human-readable summary with direction arrows: + # alignment (^ better) — cosine sim to GT features, range [-2, 2] + # diversity (v better) — pairwise sim penalty, lower = more diverse + # cfm_loss (v better) — ||E[phi(y_hat)] - phi(y)||^2 + # reward (^ better) — alignment - diversity + LOG.info( + f"step {self.state.global_step} | " + f"align {_alignment:+.3f} ^ | " + f"divers {_diversity:+.3f} v | " + f"cfm {_cfm:.3f} v | " + f"reward {_mean_reward:+.3f} ^ | " + f"adv_std {_adv_std:.3f} | " + f"blocks {num_blocks}" + ) + + return (loss, outputs) if return_outputs else loss + + @torch._dynamo.disable + @torch.no_grad() + def _generate_strided_blocks( + self, model, prompt_ids, num_blocks, anchor_offset=None + ): + """Generate tokens using strided block-parallel attention. + + Uses eager attention (dense 4D masks) during generation to avoid dynamo + recompilation — each generation step has a different sequence length. + The training forward pass (fixed size) uses flex_attention when available. + + Args: + anchor_offset: Position where anchors start. Defaults to context_length. + """ + B, seq_len = prompt_ids.shape + gen_len = self.ebft_generate_max_len + stride = self.ebft_stride + if anchor_offset is None: + anchor_offset = self.ebft_context_length + temperature = self.ebft_temperature + top_p = self.ebft_top_p + device = prompt_ids.device + model_dtype = next(model.parameters()).dtype + + full_sequence = prompt_ids.clone() + + # Force eager attention during generation to avoid dynamo recompiles from: + # 1. Variable sequence lengths per gen step → size-mismatch recompiles + # 2. no_grad vs grad toggling → grad_mode recompiles + # Both cause dynamo to hit the recompile limit → unfused fallback → OOM + unwrapped = self.accelerator.unwrap_model(model) + with override_attn_implementation(unwrapped, "eager"): + for generation_step in range(gen_len): + cur_len = full_sequence.shape[1] + + dense_mask, pos_ids = build_strided_dense_mask_and_positions( + full_sequence_length=cur_len, + prompt_length=seq_len, + context_length=anchor_offset, + generation_step=generation_step, + max_generation_length=gen_len, + stride=stride, + num_blocks=num_blocks, + device=device, + batch_size=B, + dtype=model_dtype, + ) + + with torch.autocast(device_type="cuda", dtype=torch.bfloat16): + output = model( + full_sequence, + attention_mask=dense_mask, + position_ids=pos_ids, + return_dict=True, + ) + all_logits = output.logits + + logit_positions = [] + for block_idx in range(num_blocks): + if generation_step == 0: + # Last token of the context window predicts the first rollout token + pos = anchor_offset + block_idx * stride - 1 + else: + pos = seq_len + (generation_step - 1) * num_blocks + block_idx + logit_positions.append(pos) + + position_indices = torch.tensor(logit_positions, device=device) + block_logits = all_logits.index_select(1, position_indices) + + if temperature > 0: + block_logits = block_logits / temperature + probs = torch.softmax(block_logits, dim=-1) + + if top_p < 1.0: + sorted_probs, sorted_idx = torch.sort( + probs, descending=True, dim=-1 + ) + cumulative = torch.cumsum(sorted_probs, dim=-1) + remove = cumulative > top_p + remove[..., 1:] = remove[..., :-1].clone() + remove[..., 0] = False + mask = torch.zeros_like(probs, dtype=torch.bool) + mask.scatter_(-1, sorted_idx, remove) + probs[mask] = 0 + probs = probs / probs.sum(dim=-1, keepdim=True) + + flat_probs = probs.view(-1, probs.shape[-1]) + sampled = torch.multinomial(flat_probs, 1).squeeze(-1) + sampled = sampled.view(B, num_blocks) + else: + sampled = torch.argmax(block_logits, dim=-1) + + full_sequence = torch.cat([full_sequence, sampled], dim=1) + + return full_sequence + + @torch._dynamo.disable + @torch.no_grad() + def _compute_block_rewards( + self, + full_sequences, + attn_mask, + pos_ids, + original_ids, + num_blocks, + batch_size, + n_samples, + anchor_offset=None, + ): + """Extract features and compute per-block rewards. Returns (B, N, NB). + + Args: + anchor_offset: Position where anchors start. For structured data this + is after the prompt; for unstructured it equals context_length. + """ + device = full_sequences.device + seq_len = original_ids.shape[1] + gen_len = self.ebft_generate_max_len + stride = self.ebft_stride + if anchor_offset is None: + anchor_offset = self.ebft_context_length + + # Run feature network on its device WITH the strided attention mask. + # Without the strided mask, generated tokens see tokens from other blocks + # via default causal attention, corrupting the feature representations. + fd = self._feature_device + fn_seqs = full_sequences.to(fd) + fn_pos = pos_ids.to(fd) + + # Determine which model to use for feature extraction + if self._share_feature_weights: + # Use actor's base weights with adapters disabled. + # Force eager attention to avoid grad_mode recompiles on the shared + # compiled flex_attention kernel (feature extraction is no_grad, + # training forward is with grad — each switch recompiles). + unwrapped_actor = self.accelerator.unwrap_model(self.model) + feat_model = unwrapped_actor + feature_ctx = unwrapped_actor.disable_adapter() + # Use SDPA (flash attention) instead of flex to avoid grad_mode recompiles + # on the shared compiled flex kernel. SDPA is fused (no score matrix + # materialization) and needs no compilation — ideal for no_grad feature extraction. + attn_ctx = override_attn_implementation(unwrapped_actor, "sdpa") + use_flex_for_features = False + else: + feat_model = self.feature_network + feature_ctx = contextlib.nullcontext() + attn_ctx = contextlib.nullcontext() + use_flex_for_features = self._feature_use_flex + + # Build strided mask — flex block mask if available, else dense 4D + if use_flex_for_features: + fn_attn_mask = create_strided_block_mask( + prompt_length=seq_len, + context_length=anchor_offset, + max_generation_length=gen_len, + stride=stride, + num_blocks=num_blocks, + full_sequence_length=full_sequences.shape[1], + batch_size=full_sequences.shape[0], + num_heads=feat_model.config.num_attention_heads, + device=fd, + ) + else: + fn_attn_mask, _ = build_strided_dense_mask_and_positions( + full_sequence_length=full_sequences.shape[1], + prompt_length=seq_len, + context_length=anchor_offset, + generation_step=gen_len, + max_generation_length=gen_len, + stride=stride, + num_blocks=num_blocks, + device=fd, + batch_size=full_sequences.shape[0], + dtype=torch.bfloat16, + ) + + with ( + feature_ctx, + attn_ctx, + torch.autocast(device_type="cuda", dtype=torch.bfloat16), + ): + was_training = feat_model.training + feat_model.eval() + fn_outputs = feat_model( + fn_seqs, + attention_mask=fn_attn_mask, + position_ids=fn_pos, + output_hidden_states=True, + return_dict=True, + ) + if was_training: + feat_model.train() + hidden_states_cpu = [ + fn_outputs.hidden_states[idx].to(device) + for idx in self.feature_layer_indices + ] + del fn_outputs, fn_seqs, fn_pos, fn_attn_mask + + # Normalize each layer's hidden states separately (like the reference critic), + # then concatenate. This prevents one dominant layer from suppressing others. + normalized_layers = [F.normalize(h, p=2, dim=-1) for h in hidden_states_cpu] + features = torch.cat(normalized_layers, dim=-1).to(device) + del hidden_states_cpu, normalized_layers + + # Ground-truth features start from anchor_offset (not ctx_len) so they + # align with where anchors are actually placed. + gt_features = features[:, anchor_offset:seq_len, :] + # Only take actual generated tokens (exclude padding beyond num_blocks * gen_len) + gen_features = features[:, seq_len : seq_len + num_blocks * gen_len, :] + + gt_block_features = gt_features.unfold(1, gen_len, stride).permute(0, 1, 3, 2) + gen_block_features = gen_features.reshape( + batch_size * n_samples, gen_len, num_blocks, -1 + ).transpose(1, 2) + + if self.ebft_embed_method == "mean_pooling": + gt_emb = gt_block_features.mean(dim=2) + gen_emb = gen_block_features.mean(dim=2) + else: # last_token + gt_emb = gt_block_features[:, :, -1, :] + gen_emb = gen_block_features[:, :, -1, :] + + gt_emb = gt_emb.view(batch_size, n_samples, num_blocks, -1) + gen_emb = gen_emb.view(batch_size, n_samples, num_blocks, -1) + + if self.ebft_use_whitening: + whitened_gen, whitened_gt = [], [] + for b in range(batch_size): + for nb in range(num_blocks): + w_gen, w_gt = whiten_embeddings_batched( + gen_emb[b, :, nb, :], + gt_emb[b, :, nb, :], + ) + whitened_gen.append(w_gen) + whitened_gt.append(w_gt) + gen_emb = ( + torch.stack(whitened_gen) + .view(batch_size, num_blocks, n_samples, -1) + .transpose(1, 2) + ) + gt_emb = ( + torch.stack(whitened_gt) + .view(batch_size, num_blocks, n_samples, -1) + .transpose(1, 2) + ) + + alignment = F.cosine_similarity(gen_emb, gt_emb, dim=-1) + + # Batched diversity: reshape to avoid per-block Python loop + diversity = torch.zeros_like(alignment) + if n_samples > 1: + # (B, N, NB, D) → (B*NB, N, D) for a single batched bmm + gen_for_div = gen_emb.permute(0, 2, 1, 3).reshape( + batch_size * num_blocks, n_samples, -1 + ) + sims = torch.bmm(gen_for_div, gen_for_div.transpose(1, 2)) # (B*NB, N, N) + eye = torch.eye(n_samples, device=device, dtype=torch.bool) + sims = sims.masked_fill(eye.unsqueeze(0), 0.0) + div_flat = sims.sum(dim=-1) / (n_samples - 1) # (B*NB, N) + diversity = div_flat.view(batch_size, num_blocks, n_samples).permute( + 0, 2, 1 + ) # (B, N, NB) + + # Scale by 2 per paper equation (7): + # r_j = 2*φ(ŷ_j)^T*φ(y) - 2/(n-1) * Σ_{j'≠j} φ(ŷ_j)^T*φ(ŷ_{j'}) + alignment = alignment * 2 + diversity = diversity * 2 + + # Compute CFM loss: ||E[φ(ŷ)] - φ(y)||^2 (paper eq 2) + # Mean generated embedding per prompt, squared distance to GT + mean_gen_emb = gen_emb.mean(dim=1, keepdim=True) # (B, 1, NB, D) + gt_for_cfm = gt_emb[:, 0:1, :, :] # (B, 1, NB, D) — one GT per prompt + cfm_loss = ((mean_gen_emb - gt_for_cfm) ** 2).sum(dim=-1).mean() + + # Store for logging + self._last_alignment = alignment.mean().item() + self._last_diversity = diversity.mean().item() + self._last_cfm = cfm_loss.item() + + return ( + self.ebft_alignment_coef * alignment - self.ebft_diversity_coef * diversity + ) + + def _compute_advantages(self, rewards, batch_size, n_samples, num_blocks): + """Compute RLOO advantages. rewards: (B, N, NB) → (B*N, NB).""" + if self.ebft_advantage_estimator == "rloo" and n_samples > 1: + total = rewards.sum(dim=1, keepdim=True) + baseline = (total - rewards) / (n_samples - 1) + advantages = rewards - baseline + elif self.ebft_advantage_estimator == "group_norm" and n_samples > 1: + mean = rewards.mean(dim=1, keepdim=True) + std = rewards.std(dim=1, keepdim=True) + 1e-8 + advantages = (rewards - mean) / std + else: + advantages = rewards + return advantages.view(batch_size * n_samples, num_blocks) diff --git a/src/axolotl/core/trainers/ebft/trainer.py b/src/axolotl/core/trainers/ebft/trainer.py new file mode 100644 index 0000000000..1c27fa91f0 --- /dev/null +++ b/src/axolotl/core/trainers/ebft/trainer.py @@ -0,0 +1,531 @@ +""" +EBFT Trainer — Energy-Based Fine-Tuning integrated via GRPOTrainer. + +Extends AxolotlGRPOTrainer by plugging feature-matching rewards into +the standard GRPO reward function interface. + +Paper: "Matching Features, Not Tokens: Energy-Based Fine-Tuning of Language Models" + (Jelassi et al., 2026) https://arxiv.org/abs/2603.12248 +""" + +import contextlib +import copy +from typing import TYPE_CHECKING, Any + +import torch +from datasets import Dataset, IterableDataset +from peft import PeftModel +from transformers import PreTrainedModel, PreTrainedTokenizerBase, TrainerCallback + +from axolotl.core.trainers.ebft.args import AxolotlEBFTConfig +from axolotl.core.trainers.ebft.rewards import ( + apply_embed_method, + extract_hidden_states, + get_alignment_rewards, + get_diversity_rewards, + whiten_embeddings_batched, +) +from axolotl.core.trainers.grpo.trainer import ( + AxolotlAsyncGRPOTrainer, + AxolotlGRPOTrainer, +) +from axolotl.utils.logging import get_logger + +if TYPE_CHECKING: + from collections import defaultdict + + from accelerate import Accelerator + from trl.generation.vllm_generation import VLLMGeneration + +LOG = get_logger(__name__) + + +class EBFTMixin: + """ + Mixin that adds EBFT feature-matching reward logic to any GRPO-based trainer. + + Provides: + - Frozen feature network setup (shared weights for PEFT, deepcopy otherwise) + - _feature_matching_reward() callable for GRPO reward function interface + - _sequential_rollout() for multi-turn conversations + """ + + # Type stubs for attributes provided by the composed GRPOTrainer base class. + # These are not defined here but accessed via cooperative multiple inheritance. + if TYPE_CHECKING: + accelerator: Accelerator + model: PreTrainedModel + args: AxolotlEBFTConfig + processing_class: PreTrainedTokenizerBase + num_generations: int + vllm_generation: VLLMGeneration + _metrics: defaultdict + + _tag_names = ["trl", "ebft", "axolotl"] + + def __init__( + self, + model: str | PreTrainedModel, + args: AxolotlEBFTConfig | None = None, + train_dataset: Dataset | IterableDataset | None = None, + eval_dataset: Dataset + | IterableDataset + | dict[str, Dataset | IterableDataset] + | None = None, + processing_class: PreTrainedTokenizerBase | None = None, + callbacks: list[TrainerCallback] | None = None, + optimizers: tuple[ + torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None + ] = (None, None), + peft_config: Any | None = None, + ): + # Pass our feature-matching reward function to GRPOTrainer + # It will be called with (prompts, completions, **kwargs) where + # kwargs includes all extra dataset fields like "ground_truth" + super().__init__( # type: ignore[call-arg] + model=model, + reward_funcs=[self._feature_matching_reward], + args=args, + train_dataset=train_dataset, + eval_dataset=eval_dataset, + processing_class=processing_class, + callbacks=callbacks, + optimizers=optimizers, + peft_config=peft_config, + ) + assert args is not None + + # --- Feature network setup --- + unwrapped = self.accelerator.unwrap_model(self.model) + # Check for PEFT model — use hasattr for robustness across DDP/FSDP wrapping + self._share_feature_weights = isinstance(unwrapped, PeftModel) or hasattr( + unwrapped, "disable_adapter" + ) + + if self._share_feature_weights: + # Share weights: use actor's base model with adapters disabled. + # Saves a full model copy (~8 GB for 4B model). + self.feature_network = None + param_gb = sum(p.numel() for p in unwrapped.parameters()) * 2 / 1e9 + LOG.info( + f"EBFT feature network shares actor weights (PEFT disable_adapter). " + f"Saving ~{param_gb:.1f} GB" + ) + else: + LOG.info("Creating frozen feature network for EBFT (deepcopy)...") + self.feature_network = copy.deepcopy(unwrapped) + for param in self.feature_network.parameters(): + param.requires_grad = False + self.feature_network.eval() + + # Compute layer indices from fractional depths + # Handle VLM models where num_hidden_layers is on text_config + config = unwrapped.config + if hasattr(config, "text_config") and hasattr( + config.text_config, "num_hidden_layers" + ): + config = config.text_config + num_layers = config.num_hidden_layers + self.feature_layer_indices = [ + int(frac * num_layers) for frac in args.ebft_feature_layers + ] + LOG.info( + f"EBFT feature extraction from layers {self.feature_layer_indices} " + f"(of {num_layers} total), embed_method={args.ebft_embed_method}" + ) + if args.ebft_adaptive_max_tokens: + LOG.info( + f"EBFT adaptive max_tokens enabled " + f"(gt_length_multiplier={args.ebft_gt_length_multiplier})" + ) + + _adaptive_max_lock = None # initialized lazily + + def _generate_only(self, inputs, rank0_only=False): + """Override to set per-batch max_tokens based on ground-truth length. + + Uses a lock to prevent race conditions in async mode where concurrent + BG threads could interleave mutations of max_completion_length. + """ + import threading + + args = self.args + if ( + args.ebft_adaptive_max_tokens + and hasattr(self, "vllm_generation") + and inputs + ): + gt_texts = [ + x.get("ground_truth", "") for x in inputs if x.get("ground_truth") + ] + if gt_texts: + gt_token_counts = [ + len(self.processing_class.encode(gt, add_special_tokens=False)) + for gt in gt_texts + ] + multiplier = args.ebft_gt_length_multiplier + max_completion = self.vllm_generation.max_completion_length + adaptive_max = max( + min(int(c * multiplier), max_completion) for c in gt_token_counts + ) + adaptive_max = max(adaptive_max, 64) + + if self._adaptive_max_lock is None: + self._adaptive_max_lock = threading.Lock() + with self._adaptive_max_lock: + original = self.vllm_generation.max_completion_length + self.vllm_generation.max_completion_length = adaptive_max + try: + return super()._generate_only(inputs, rank0_only) + finally: + self.vllm_generation.max_completion_length = original + + return super()._generate_only(inputs, rank0_only) + + @torch.no_grad() + def _feature_matching_reward( + self, + prompts: list, + completions: list, + ground_truth: list[str] | None = None, + remaining_turns: list | None = None, + **kwargs, + ) -> list[float]: + """ + Compute feature-matching rewards for generated completions. + + This is called by GRPOTrainer's _generate_and_score_completions() + as a standard reward function. The `ground_truth` field comes from + the dataset via reward_kwargs. + + For multi-turn conversations, `remaining_turns` contains the subsequent + user/assistant turn pairs. When present, we do sequential rollouts: + generate each assistant turn conditioned on history + previous generations, + then compute feature-matching rewards on the full generated conversation. + + Args: + prompts: List of prompt strings/messages + completions: List of generated completion strings + ground_truth: List of reference completion strings (from dataset) + remaining_turns: List of remaining conversation turns after the + first assistant turn (for multi-turn rollouts) + + Returns: + List of scalar rewards, one per completion + """ + if ground_truth is None: + LOG.warning("No ground_truth field in dataset — using zero rewards") + return [0.0] * len(prompts) + + device = self.accelerator.device + args = self.args + num_gens = self.num_generations + + # --- Multi-turn sequential rollout --- + # If remaining_turns is provided, generate subsequent assistant turns + # by calling vLLM for each turn, building up the full conversation. + if remaining_turns is not None and hasattr(self, "vllm_generation"): + completions = self._sequential_rollout( + prompts, completions, remaining_turns, num_gens + ) + + # --- Tokenize generated sequences: prompt + completion --- + gen_texts = [] + gen_prompt_texts = [] + for p, c in zip(prompts, completions, strict=True): + if isinstance(p, list): + prompt_text = self.processing_class.apply_chat_template( + p, tokenize=False, add_generation_prompt=True + ) + else: + prompt_text = p + if isinstance(c, list): + comp_text = c[0].get("content", "") if c else "" + else: + comp_text = c + gen_texts.append(prompt_text + comp_text) + gen_prompt_texts.append(prompt_text) + + gen_encoded = self.processing_class( + text=gen_texts, + return_tensors="pt", + padding=True, + truncation=True, + max_length=getattr(self.args, "max_length", None) + or getattr(self.args, "max_seq_length", None) + or 2048, + add_special_tokens=False, + ) + gen_ids = gen_encoded["input_ids"].to(device) + gen_mask = gen_encoded["attention_mask"].to(device) + + # Compute prompt lengths for completion_mean pooling + gen_prompt_lengths = torch.tensor( + [ + len(self.processing_class.encode(pt, add_special_tokens=False)) + for pt in gen_prompt_texts + ], + device=device, + ) + + # --- Tokenize ground-truth sequences: prompt + ground_truth --- + # For multi-turn (remaining_turns present), render the full GT conversation + # through the chat template to preserve role markers between turns. + gt_texts = [] + gt_prompt_texts = [] + for i, (p, gt) in enumerate(zip(prompts, ground_truth, strict=True)): + if i % num_gens != 0: + continue # Only need one GT per prompt group + if isinstance(p, list): + prompt_text = self.processing_class.apply_chat_template( + p, tokenize=False, add_generation_prompt=True + ) + # Multi-turn: build full GT conversation with remaining turns + if remaining_turns is not None: + prompt_idx = i // num_gens + turns = ( + remaining_turns[prompt_idx] + if prompt_idx < len(remaining_turns) + else [] + ) + if turns: + gt_conv = list(p) + [{"role": "assistant", "content": gt}] + gt_conv.extend(turns) + full_gt_text = self.processing_class.apply_chat_template( + gt_conv, tokenize=False, add_generation_prompt=False + ) + gt_texts.append(full_gt_text) + gt_prompt_texts.append(prompt_text) + continue + else: + prompt_text = p + gt_texts.append(prompt_text + gt) + gt_prompt_texts.append(prompt_text) + + gt_encoded = self.processing_class( + text=gt_texts, + return_tensors="pt", + padding=True, + truncation=True, + max_length=getattr(self.args, "max_length", None) + or getattr(self.args, "max_seq_length", None) + or 2048, + add_special_tokens=False, + ) + gt_ids = gt_encoded["input_ids"].to(device) + gt_mask = gt_encoded["attention_mask"].to(device) + + gt_prompt_lengths = torch.tensor( + [ + len(self.processing_class.encode(pt, add_special_tokens=False)) + for pt in gt_prompt_texts + ], + device=device, + ) + + # --- Extract features from frozen feature network --- + # INVARIANT: disable_adapter() yields the unmodified base weights because + # _sync_peft_weights_no_merge and _sync_lora_adapter never call + # merge_adapter() — they compute merged weights as new tensors or save + # the adapter to filesystem. Base weights are never modified in-place. + if self._share_feature_weights: + unwrapped = self.accelerator.unwrap_model(self.model) + feature_ctx = unwrapped.disable_adapter() + else: + unwrapped = self.feature_network + feature_ctx = contextlib.nullcontext() + + with feature_ctx: + was_training = unwrapped.training + unwrapped.eval() + gen_hidden = extract_hidden_states( + unwrapped, gen_ids, gen_mask, self.feature_layer_indices + ) + gt_hidden = extract_hidden_states( + unwrapped, gt_ids, gt_mask, self.feature_layer_indices + ) + if was_training: + unwrapped.train() + + # --- Pool to sequence-level embeddings --- + gen_emb = apply_embed_method( + gen_hidden, + args.ebft_embed_method, + gen_mask, + prompt_lengths=gen_prompt_lengths, + ) + gt_emb = apply_embed_method( + gt_hidden, + args.ebft_embed_method, + gt_mask, + prompt_lengths=gt_prompt_lengths, + ) + + # --- Optional whitening --- + batch_size = gen_emb.shape[0] + if args.ebft_use_whitening and batch_size > 1: + num_prompts = batch_size // num_gens + gen_reshaped = gen_emb.view(num_prompts, num_gens, -1) + whitened_gen_list = [] + whitened_gt_list = [] + for i in range(num_prompts): + w_gen, w_gt = whiten_embeddings_batched( + gen_reshaped[i], gt_emb[i : i + 1] + ) + whitened_gen_list.append(w_gen) + whitened_gt_list.append(w_gt) + gen_emb = torch.cat(whitened_gen_list, dim=0) + gt_emb = torch.cat(whitened_gt_list, dim=0) + else: + gen_emb = torch.nn.functional.normalize(gen_emb, p=2, dim=-1) + gt_emb = torch.nn.functional.normalize(gt_emb, p=2, dim=-1) + + # Repeat gt_emb: each GT repeated num_generations times + gt_emb_expanded = gt_emb.repeat_interleave(num_gens, dim=0) + + # --- Compute rewards --- + alignment = get_alignment_rewards(gen_emb, gt_emb_expanded) + diversity = get_diversity_rewards(gen_emb, num_gens) + + # Scale by 2 per paper equation (7): + # r_j = 2*φ(ŷ_j)^T*φ(y) - 2/(n-1) * Σ_{j'≠j} φ(ŷ_j)^T*φ(ŷ_{j'}) + alignment = alignment * 2 + diversity = diversity * 2 + + rewards = ( + args.ebft_alignment_coef * alignment - args.ebft_diversity_coef * diversity + ) + + # Compute CFM loss: ||E[φ(ŷ)] - φ(y)||^2 (paper eq 2) + gen_reshaped = gen_emb.view(-1, num_gens, gen_emb.shape[-1]) + mean_gen = gen_reshaped.mean(dim=1) # (num_prompts, D) + cfm_loss = ((mean_gen - gt_emb) ** 2).sum(dim=-1).mean() + + # Log feature-matching metrics to console and wandb + _align = alignment.mean().item() + _divers = diversity.mean().item() + _reward = rewards.mean().item() + _cfm = cfm_loss.item() + + LOG.info( + f"ebft reward | " + f"align {_align:+.3f} ^ | " + f"divers {_divers:+.3f} v | " + f"cfm {_cfm:.3f} v | " + f"reward {_reward:+.3f} ^" + ) + + # Log to wandb via trainer's _metrics (picked up by GRPO's logging) + mode = "train" if self.model.training else "eval" + if hasattr(self, "_metrics"): + self._metrics[mode]["ebft/alignment"].append(_align) + self._metrics[mode]["ebft/diversity"].append(_divers) + self._metrics[mode]["ebft/cfm_loss"].append(_cfm) + self._metrics[mode]["ebft/reward"].append(_reward) + + return rewards.cpu().tolist() + + @torch.no_grad() + def _sequential_rollout( + self, + prompts: list, + first_completions: list, + remaining_turns: list, + num_gens: int, + ) -> list: + """ + Extend single-turn completions into multi-turn conversations. + + For each prompt group, takes the first generated assistant turn and + sequentially generates subsequent assistant turns by calling vLLM, + building up a full multi-turn conversation. + + Args: + prompts: List of prompt message lists (repeated num_gens times) + first_completions: List of generated first-turn completions + remaining_turns: List of remaining turn pairs after first assistant turn. + Each element is a list of dicts: [{"role": "user", "content": "..."}, + {"role": "assistant", "content": "...GT..."}] + num_gens: Number of generations per prompt + + Returns: + Extended completions incorporating all generated turns + """ + vllm_client = self.vllm_generation.vllm_client + max_tokens = getattr(self.args, "max_completion_length", 256) + temperature = getattr(self.args, "temperature", 0.7) + gen_kwargs = getattr(self.args, "generation_kwargs", None) or {} + + extended_completions = [] + + for idx in range(len(prompts)): + prompt_msgs = prompts[idx] if isinstance(prompts[idx], list) else [] + first_comp = first_completions[idx] + + # Extract first completion text + if isinstance(first_comp, list): + first_text = first_comp[0].get("content", "") if first_comp else "" + else: + first_text = first_comp + + # Get remaining turns for this prompt (same for all num_gens copies) + prompt_idx = idx // num_gens + turns = ( + remaining_turns[prompt_idx] if prompt_idx < len(remaining_turns) else [] + ) + + if not turns: + extended_completions.append(first_text) + continue + + # Build conversation with generated first turn + conv = list(prompt_msgs) + [{"role": "assistant", "content": first_text}] + + # Generate subsequent turns + for turn in turns: + if turn["role"] == "user": + conv.append(turn) + elif turn["role"] == "assistant": + try: + result = vllm_client.chat( + messages=[conv], + n=1, + max_tokens=max_tokens, + temperature=temperature, + generation_kwargs=gen_kwargs, + ) + gen_ids = result.get("completion_ids", [[]])[0] + gen_text = self.processing_class.decode( + gen_ids, skip_special_tokens=True + ) + except Exception as e: + LOG.warning(f"Multi-turn rollout generation failed: {e}") + gen_text = "" + + conv.append({"role": "assistant", "content": gen_text}) + + # Render full conversation through chat template, then extract + # everything after the original prompt as the "completion" text. + # This preserves role markers and formatting between turns. + full_rendered = self.processing_class.apply_chat_template( + conv, tokenize=False, add_generation_prompt=False + ) + prompt_rendered = self.processing_class.apply_chat_template( + prompt_msgs, tokenize=False, add_generation_prompt=True + ) + completion_text = full_rendered[len(prompt_rendered) :] + extended_completions.append(completion_text) + + return extended_completions + + +class AxolotlEBFTTrainer(EBFTMixin, AxolotlGRPOTrainer): + """EBFT trainer using synchronous GRPO (standard vLLM generation).""" + + pass + + +class AxolotlAsyncEBFTTrainer(EBFTMixin, AxolotlAsyncGRPOTrainer): + """EBFT trainer using async GRPO (prefetches next batch during training).""" + + pass diff --git a/src/axolotl/core/trainers/grpo/async_trainer.py b/src/axolotl/core/trainers/grpo/async_trainer.py index acfd029098..2b8cda6d82 100644 --- a/src/axolotl/core/trainers/grpo/async_trainer.py +++ b/src/axolotl/core/trainers/grpo/async_trainer.py @@ -628,13 +628,21 @@ class AsyncGRPOTrainer(GRPOTrainer): """ def __init__(self, *args, **kwargs): - # When using native LoRA sync, skip the NCCL communicator init in VLLMGeneration. - # The communicator is not needed because weight sync happens via filesystem + HTTP, - # and it fails when vLLM and a trainer rank share the same CUDA device. + # Skip NCCL communicator init when using LoRA sync (filesystem) or HTTP-only + # merged weight sync. NCCL is only needed for the standard update_named_param + # path which broadcasts tensors through the communicator. training_args = kwargs.get("args") or (args[1] if len(args) > 1 else None) - if training_args is not None and getattr( - training_args, "vllm_lora_sync", False - ): + _skip_nccl = False + if training_args is not None: + if getattr(training_args, "vllm_lora_sync", False): + _skip_nccl = True # LoRA sync uses filesystem + HTTP + elif getattr(training_args, "async_prefetch", False): + # Skip NCCL at init to avoid DDP param count mismatch in multi-GPU. + # init_communicator allocates device tensors on rank 0 only, which + # causes DDP to see different param counts across ranks. + # The communicator is initialized lazily on first weight sync instead. + _skip_nccl = True + if _skip_nccl: from trl.generation.vllm_generation import VLLMGeneration _orig_init_vllm = VLLMGeneration._init_vllm @@ -661,7 +669,12 @@ def _init_vllm_no_communicator(self_vllm): VLLMGeneration._init_vllm = _init_vllm_no_communicator - super().__init__(*args, **kwargs) + try: + super().__init__(*args, **kwargs) + finally: + # Restore original _init_vllm so other trainers aren't affected + if _skip_nccl: + VLLMGeneration._init_vllm = _orig_init_vllm # type: ignore[possibly-undefined] # FP8 models: zero out the pad token embedding so that padding # positions have zero hidden states throughout the network. @@ -780,11 +793,50 @@ def _shutdown_async(self): self._executor = None def _submit_generation(self): - """Submit the next background generation job.""" + """Submit the next background generation job. + + With multi-process (DDP/FSDP), only rank 0 generates to avoid + cross-rank NCCL collectives from background threads. Non-rank-0 + processes enqueue a sentinel ``None`` that is replaced by a + broadcast in ``_prepare_inputs_legacy_async``. + """ + rank0_only = self.accelerator.num_processes > 1 + if rank0_only and not self.accelerator.is_main_process: + # Non-rank-0: nothing to generate; enqueue a resolved None future + f: concurrent.futures.Future = concurrent.futures.Future() + f.set_result(None) + self._async_queue.put(f) + return batch = next(self._prompt_iter) - future = self._executor.submit(self._generate_only, batch) + future = self._executor.submit(self._generate_only, batch, rank0_only) self._async_queue.put(future) + # ------------------------------------------------------------------ + # Broadcast rollout (legacy async, multi-process) + # ------------------------------------------------------------------ + + def _broadcast_rollout(self, rollout: dict | None) -> dict: + """Broadcast a rank0-only rollout dict to all ranks (main thread). + + Rank 0 has the full rollout dict from ``_generate_only``; other ranks + have ``None``. After broadcast, tensors are moved to each rank's + local device. + """ + import torch.distributed as dist + + obj_list = [rollout if self.accelerator.is_main_process else None] + dist.broadcast_object_list(obj_list, src=0) + rollout = obj_list[0] + assert rollout is not None, "broadcast_object_list failed to deliver rollout" + + # Move tensors to local device (broadcast deserializes to CPU) + device = self.accelerator.device + for key, val in rollout.items(): + if isinstance(val, torch.Tensor) and val.device != device: + rollout[key] = val.to(device) + + return rollout + # ------------------------------------------------------------------ # Weight sync # ------------------------------------------------------------------ @@ -796,14 +848,18 @@ def _sync_peft_weights_no_merge(self): for Float8), and also safe for concurrent use since it never modifies base weights in-place. """ - model = self.vllm_generation.model accelerator = self.vllm_generation.accelerator - vllm_client = self.vllm_generation.vllm_client - fix_name = self.vllm_generation._fix_param_name_to_vllm - if not (self.vllm_generation.mode == "server" and accelerator.is_main_process): return + # In multi-GPU async mode, we skip NCCL communicator init to avoid + # DDP param count mismatch and NCCL device conflicts. Weight sync + # uses the HTTP-only fallback in batch_update_named_params instead. + + model = self.vllm_generation.model + vllm_client = self.vllm_generation.vllm_client + fix_name = self.vllm_generation._fix_param_name_to_vllm + # Build lookup: module_path -> (A, B, scaling) for all active LoRA layers lora_info = {} for mod_name, module in model.base_model.model.named_modules(): @@ -826,10 +882,11 @@ def _sync_peft_weights_no_merge(self): weight_name = pname.replace(".weight_scale_inv", ".weight") scale_inv_lookup[weight_name] = pparam.data - # Iterate all parameters, computing merged weights for LoRA layers. - # Skip LoRA-specific params and FP8 scale params (scales will be - # recomputed by vLLM when it receives the merged bf16 weight). + # Only sync parameters that have LoRA modifications — skip unchanged + # base weights to avoid OOM on the vLLM GPU from allocating the entire + # model's worth of NCCL receive buffers. params_to_sync = [] + compute_dtype = torch.bfloat16 for name, param in model.named_parameters(): vllm_name = name.removeprefix("base_model.model.").replace( ".base_layer", "" @@ -838,52 +895,58 @@ def _sync_peft_weights_no_merge(self): continue if "original_module" in vllm_name: continue - # Skip FP8 quantization scale parameters - they are recomputed - # on the vLLM side when we update the weight itself if "weight_scale_inv" in vllm_name or "input_scale" in vllm_name: continue + if not vllm_name.endswith(".weight"): + continue + # fix_name strips modules_to_save.default. prefix + raw_mod_path = vllm_name[: -len(".weight")] vllm_name = fix_name(vllm_name, extra_prefixes=["modules_to_save.default."]) + mod_path = vllm_name[: -len(".weight")] + + # Sync weights that have LoRA adapters OR are modules_to_save + is_lora = mod_path in lora_info + is_modules_to_save = raw_mod_path != mod_path # fix_name stripped a prefix + if not is_lora and not is_modules_to_save: + continue data = param.data - compute_dtype = torch.bfloat16 - - if vllm_name.endswith(".weight"): - # Dequantize FP8 weights before merging - if data.dtype == torch.float8_e4m3fn and name in scale_inv_lookup: - scale_inv = scale_inv_lookup[name] - # Block dequantization: weight * scale_inv (with broadcasting) - fp8_bf16 = data.to(compute_dtype) - if scale_inv.dim() == 2 and fp8_bf16.dim() == 2: - # Block-quantized: scale_inv shape (rows/block, cols/block) - sr, sc = scale_inv.shape - br = fp8_bf16.shape[0] // sr # block height - bc = fp8_bf16.shape[1] // sc # block width - # Reshape → multiply by block scale → reshape back - data = ( - fp8_bf16.reshape(sr, br, sc, bc) - * scale_inv[:, None, :, None].to(compute_dtype) - ).reshape(fp8_bf16.shape) - elif scale_inv.dim() <= 1: - # Per-tensor or per-channel scale - data = fp8_bf16 * scale_inv.to(compute_dtype) - else: - data = fp8_bf16 - elif data.dtype == torch.float8_e4m3fn: - # FP8 but no scale found - just cast (lossy) - data = data.to(compute_dtype) - - mod_path = vllm_name[: -len(".weight")] - if mod_path in lora_info: - A, B, s = lora_info[mod_path] - merged = data.to(compute_dtype) + s * ( - B.to(compute_dtype) @ A.to(compute_dtype) - ) - data = merged - params_to_sync.append((vllm_name, data)) + # Dequantize FP8 weights before merging + if data.dtype == torch.float8_e4m3fn and name in scale_inv_lookup: + scale_inv = scale_inv_lookup[name] + fp8_bf16 = data.to(compute_dtype) + if scale_inv.dim() == 2 and fp8_bf16.dim() == 2: + sr, sc = scale_inv.shape + br = fp8_bf16.shape[0] // sr + bc = fp8_bf16.shape[1] // sc + data = ( + fp8_bf16.reshape(sr, br, sc, bc) + * scale_inv[:, None, :, None].to(compute_dtype) + ).reshape(fp8_bf16.shape) + elif scale_inv.dim() <= 1: + data = fp8_bf16 * scale_inv.to(compute_dtype) + else: + data = fp8_bf16 + elif data.dtype == torch.float8_e4m3fn: + data = data.to(compute_dtype) + + if is_lora: + A, B, s = lora_info[mod_path] + merged = data.to(compute_dtype) + s * ( + B.to(compute_dtype) @ A.to(compute_dtype) + ) + params_to_sync.append((vllm_name, merged)) + else: + # modules_to_save: send raw weight (no LoRA merge needed) + params_to_sync.append((vllm_name, data.to(compute_dtype))) - # Batch sync all params in one HTTP+NCCL call (vs individual calls) + # Batch sync only LoRA-modified params via HTTP+NCCL if params_to_sync: + sync_mb = sum(t.numel() * t.element_size() for _, t in params_to_sync) / 1e6 + logger.info( + f"Syncing {len(params_to_sync)} LoRA-modified params ({sync_mb:.0f} MB)" + ) vllm_client.batch_update_named_params(params_to_sync) # Reset prefix cache after weight update @@ -950,6 +1013,7 @@ def _sync_lora_adapter(self): vllm_client = self.vllm_generation.vllm_client url = f"{vllm_client.base_url}/set_lora_adapter/" + sync_timeout = getattr(self.args, "vllm_server_timeout", 300) or 300 response = requests.post( url, json={ @@ -957,7 +1021,7 @@ def _sync_lora_adapter(self): "lora_int_id": self._lora_sync_version, "lora_path": adapter_path, }, - timeout=30, + timeout=sync_timeout, ) if response.status_code != 200: logger.warning( @@ -1008,11 +1072,11 @@ def _maybe_sync_vllm_weights(self): step = self.state.global_step interval = self.args.vllm_sync_interval if step != self._last_synced_step and step % interval == 0: + if step == 0: + logger.info("Skipping vLLM weight sync at step 0 (no training yet)") + self._last_synced_step = step + return if getattr(self.args, "vllm_lora_sync", False): - if step == 0: - logger.info("Skipping LoRA sync at step 0 (no training yet)") - self._last_synced_step = step - return # Native LoRA sync: save adapter to filesystem, vLLM loads it directly self._sync_lora_adapter() else: @@ -1088,7 +1152,7 @@ def _zero_pad_embedding_for_fp8(self): # Background-thread generation (no scoring) # ------------------------------------------------------------------ - def _generate_single_turn(self, prompts, **kwargs): + def _generate_single_turn(self, prompts, *args, **kwargs): """Override to prevent weight sync from background thread and to use no-merge sync for PEFT models (FP8 models can't merge_adapter).""" is_bg = threading.current_thread() is not threading.main_thread() @@ -1121,7 +1185,7 @@ def _no_merge_sync(): self._patched_sync_weights = True try: - return super()._generate_single_turn(prompts, **kwargs) + return super()._generate_single_turn(prompts, *args, **kwargs) finally: if saved_step is not None: self._last_loaded_step = saved_step @@ -1165,9 +1229,9 @@ def _generate_rank0_only(self, prompts): output = vg.vllm_client.chat( messages=unique_prompts, **sampling_params, - chat_template_kwargs=vg.chat_template_kwargs, - tools=vg.tools, - chat_template=vg.chat_template, + chat_template_kwargs=self.chat_template_kwargs, + tools=self.tools, + chat_template=getattr(self, "chat_template", None), ) else: output = vg.vllm_client.generate(prompts=unique_prompts, **sampling_params) @@ -1584,10 +1648,12 @@ def _compute_deferred_scores(self, rollout: dict) -> dict: logps_diff = per_token_logps_diff is_ratio = torch.exp(logps_diff) + is_floor = 1.0 / is_cap # symmetric floor (e.g., cap=3.0 -> floor=0.333) if is_mode in ("sequence_truncate", "token_truncate"): - is_ratio = torch.clamp(is_ratio, max=is_cap) + is_ratio = torch.clamp(is_ratio, min=is_floor, max=is_cap) elif is_mode in ("sequence_mask", "token_mask"): is_ratio = is_ratio.masked_fill(is_ratio > is_cap, value=0.0) + is_ratio = is_ratio.clamp(min=is_floor) data["importance_sampling_ratio"] = is_ratio # --- Collect rewards (launched before logprobs, should be done) --- @@ -1906,10 +1972,13 @@ def _compute_streaming_group_scores( seq_is = is_mode in ("sequence_mask", "sequence_truncate") logps_diff = diff.sum(dim=-1, keepdim=True) if seq_is else diff is_ratio = torch.exp(logps_diff) + # Symmetric floor clamp (matches non-streaming path at line ~1651) + is_floor = 1.0 / is_cap if is_mode in ("sequence_truncate", "token_truncate"): - is_ratio = torch.clamp(is_ratio, max=is_cap) + is_ratio = torch.clamp(is_ratio, min=is_floor, max=is_cap) elif is_mode in ("sequence_mask", "token_mask"): is_ratio = is_ratio.masked_fill(is_ratio > is_cap, value=0.0) + is_ratio = is_ratio.clamp(min=is_floor) if "importance_sampling_ratio" not in data: total = len(data["prompt_ids"]) shape = (total, 1) if seq_is else (total, is_ratio.size(1)) @@ -2280,6 +2349,10 @@ def _prepare_inputs_legacy_async(self, generation_batch): rollout = future.result() self._submit_generation() + # With multi-process, only rank 0 generated. Broadcast to all ranks. + if self.accelerator.num_processes > 1: + rollout = self._broadcast_rollout(rollout) + if self.args.streaming_partial_batch: micro_batches = self._score_streaming(rollout) else: diff --git a/src/axolotl/integrations/diffusion/callbacks.py b/src/axolotl/integrations/diffusion/callbacks.py index 5f5ff34002..61464ee7df 100644 --- a/src/axolotl/integrations/diffusion/callbacks.py +++ b/src/axolotl/integrations/diffusion/callbacks.py @@ -145,10 +145,10 @@ def _log_samples(self, samples: list, step: int): logger.info("=" * 60) if self.trainer.axolotl_cfg.use_wandb: - if wandb.run is not None: - wandb.log( + if wandb.run is not None: # type: ignore[attr-defined] + wandb.log( # type: ignore[attr-defined] { - "generated_samples": wandb.Table( + "generated_samples": wandb.Table( # type: ignore[attr-defined] columns=[ "step", "original", diff --git a/src/axolotl/monkeypatch/trainer/trl_vllm.py b/src/axolotl/monkeypatch/trainer/trl_vllm.py index a3296df61d..e3f57ccf56 100644 --- a/src/axolotl/monkeypatch/trainer/trl_vllm.py +++ b/src/axolotl/monkeypatch/trainer/trl_vllm.py @@ -20,46 +20,93 @@ def _batch_update_named_params( self, params: list[tuple[str, torch.Tensor]], chunk_size: int | None = None ): - """Batched weight sync — sends param metadata via HTTP, tensors via NCCL.""" - from transformers import is_torch_xpu_available + """Batched weight sync — uses NCCL if communicator available, HTTP otherwise.""" + has_communicator = getattr(self, "communicator", None) is not None - if chunk_size is None: - chunks = [params] - else: - chunks = [] - current_chunk: list[tuple[str, torch.Tensor]] = [] - current_elements = 0 - for name, weights in params: - n_elem = weights.numel() - if current_chunk and current_elements + n_elem > chunk_size: + if has_communicator: + # Fast path: metadata via HTTP, tensors via NCCL + from transformers import is_torch_xpu_available + + if chunk_size is None: + chunks = [params] + else: + chunks = [] + current_chunk: list[tuple[str, torch.Tensor]] = [] + current_elements = 0 + for name, weights in params: + n_elem = weights.numel() + if current_chunk and current_elements + n_elem > chunk_size: + chunks.append(current_chunk) + current_chunk = [] + current_elements = 0 + current_chunk.append((name, weights)) + current_elements += n_elem + if current_chunk: chunks.append(current_chunk) - current_chunk = [] - current_elements = 0 - current_chunk.append((name, weights)) - current_elements += n_elem - if current_chunk: - chunks.append(current_chunk) - - for chunk in chunks: - param_metadata = [ - {"name": name, "dtype": str(weights.dtype), "shape": list(weights.shape)} - for name, weights in chunk - ] - url = f"{self.base_url}/batch_update_named_params/" - response = self.session.post(url, json={"params": param_metadata}) - if response.status_code != 200: - raise Exception(f"Request failed: {response.status_code}, {response.text}") - - for _name, weights in chunk: + + for chunk in chunks: + param_metadata = [ + { + "name": name, + "dtype": str(weights.dtype), + "shape": list(weights.shape), + } + for name, weights in chunk + ] + url = f"{self.base_url}/batch_update_named_params/" + response = self.session.post( + url, json={"params": param_metadata}, timeout=120 + ) + if response.status_code != 200: + raise Exception( + f"Request failed: {response.status_code}, {response.text}" + ) + + for _name, weights in chunk: + if is_torch_xpu_available(): + self.communicator.broadcast(weights, root=self.rank) + else: + self.communicator.broadcast(weights, src=self.rank) + if is_torch_xpu_available(): - self.communicator.broadcast(weights, root=self.rank) + self.communicator.barrier() else: - self.communicator.broadcast(weights, src=self.rank) + self.communicator.group.barrier() + else: + # HTTP-only path: encode tensor data in request body (no NCCL needed). + # Batch by byte size to avoid huge HTTP payloads. + MAX_BYTES_PER_REQUEST = 10 * 1024 * 1024 # 10 MB + HTTP_TIMEOUT = 120 # seconds per request + + payload: list[dict] = [] + payload_bytes = 0 + url = f"{self.base_url}/http_update_weights/" + + def _flush(p: list[dict]) -> None: + if not p: + return + response = self.session.post(url, json={"params": p}, timeout=HTTP_TIMEOUT) + if response.status_code != 200: + raise Exception( + f"Request failed: {response.status_code}, {response.text}" + ) - if is_torch_xpu_available(): - self.communicator.barrier() - else: - self.communicator.group.barrier() + from axolotl.utils.weight_serde import encode_for_http + + for name, weights in params: + entry = encode_for_http(name, weights) + entry_bytes = weights.nelement() * weights.element_size() + + # Flush current batch if adding this entry would exceed limit + if payload and payload_bytes + entry_bytes > MAX_BYTES_PER_REQUEST: + _flush(payload) + payload = [] + payload_bytes = 0 + + payload.append(entry) + payload_bytes += entry_bytes + + _flush(payload) # send remaining def _update_model_params(self, model: nn.Module, chunk_size: int | None = None): diff --git a/src/axolotl/prompt_strategies/ebft/__init__.py b/src/axolotl/prompt_strategies/ebft/__init__.py new file mode 100644 index 0000000000..db46af0057 --- /dev/null +++ b/src/axolotl/prompt_strategies/ebft/__init__.py @@ -0,0 +1,9 @@ +""" +module for EBFT style dataset transform strategies +""" + +from functools import partial + +from ..base import load as load_base + +load = partial(load_base, module_base="axolotl.prompt_strategies.ebft") diff --git a/src/axolotl/prompt_strategies/ebft/ebft_chat_multiturn.py b/src/axolotl/prompt_strategies/ebft/ebft_chat_multiturn.py new file mode 100644 index 0000000000..26982c1833 --- /dev/null +++ b/src/axolotl/prompt_strategies/ebft/ebft_chat_multiturn.py @@ -0,0 +1,129 @@ +""" +Dataset transform for multi-turn chat data with structured EBFT (vLLM mode). + +Three variants: + +1. `transform` — Uses the FIRST assistant turn as the generation target. + Passes remaining turns as `remaining_turns` for sequential rollout. + The trainer generates turn 1 via GRPO/vLLM, then sequentially generates + subsequent assistant turns, comparing the full conversation to GT. + +2. `transform_last_turn` — Uses the LAST assistant turn as the target. + Simplest approach: the full conversation history is the prompt. + +3. `transform_all_turns` — Explodes each conversation into N examples + (one per assistant turn). Each turn is an independent training example. + Use with batched=True. + +Supports OpenAI chat format: + {"messages": [{"role": ..., "content": ...}, ...]} +""" + + +def transform(cfg, **kwargs): + """Multi-turn with sequential rollout. + + Returns the first assistant turn as ground_truth, plus remaining_turns + for the trainer to do sequential rollout generation. + """ + + def transform_fn(example, tokenizer=None): + messages = example.get("messages", example.get("conversations", [])) + + if not messages: + return {"prompt": [], "ground_truth": ""} + + # Split at first assistant turn + prompt_msgs = [] + first_gt = None + remaining = [] + + found_first = False + for msg in messages: + if msg["role"] == "assistant" and not found_first: + first_gt = msg["content"] + found_first = True + elif found_first: + remaining.append(msg) + else: + prompt_msgs.append(msg) + + if first_gt is None: + return {"prompt": prompt_msgs, "ground_truth": ""} + + # Store only the first assistant turn as ground_truth. The full multi-turn + # GT is reconstructed in the reward function via chat template rendering + # (using remaining_turns), which preserves role markers between turns. + return { + "prompt": prompt_msgs, + "ground_truth": first_gt, + "remaining_turns": remaining, + } + + return transform_fn, { + "remove_columns": "__all__", + } + + +def transform_last_turn(cfg, **kwargs): + """Single-turn: use the last assistant turn as the generation target.""" + + def transform_fn(example, tokenizer=None): + messages = example.get("messages", example.get("conversations", [])) + + if not messages: + return {"prompt": [], "ground_truth": ""} + + # Find all assistant turns + history = [] + last_prompt = [] + last_gt = "" + for msg in messages: + if msg["role"] == "assistant": + last_prompt = list(history) + last_gt = msg["content"] + history.append(msg) + + return { + "prompt": last_prompt, + "ground_truth": last_gt, + } + + return transform_fn, { + "remove_columns": "__all__", + } + + +def transform_all_turns(cfg, **kwargs): + """Explode: one example per assistant turn. + + Use with datasets.map(batched=True) to produce N examples from + each N-turn conversation. + + Usage in YAML: + type: ebft_chat_multiturn.transform_all_turns + """ + + def transform_fn(examples, tokenizer=None): + all_prompts = [] + all_ground_truths = [] + + messages_list = examples.get("messages", examples.get("conversations", [])) + + for messages in messages_list: + history = [] + for msg in messages: + if msg["role"] == "assistant": + all_prompts.append(list(history)) + all_ground_truths.append(msg["content"]) + history.append(msg) + + return { + "prompt": all_prompts, + "ground_truth": all_ground_truths, + } + + return transform_fn, { + "remove_columns": "__all__", + "batched": True, + } diff --git a/src/axolotl/prompt_strategies/ebft/ebft_opencode.py b/src/axolotl/prompt_strategies/ebft/ebft_opencode.py new file mode 100644 index 0000000000..930d314a17 --- /dev/null +++ b/src/axolotl/prompt_strategies/ebft/ebft_opencode.py @@ -0,0 +1,20 @@ +""" +Dataset transform for nvidia/OpenCodeInstruct with EBFT structured mode. + +Maps the dataset's `input` (prompt) and `output` (code solution) fields +to the format expected by the EBFT trainer (prompt + ground_truth). +""" + + +def transform(cfg, **kwargs): + def transform_fn(example, tokenizer=None): + return { + "prompt": [ + {"role": "user", "content": example["input"]}, + ], + "ground_truth": example["output"], + } + + return transform_fn, { + "remove_columns": "__all__", + } diff --git a/src/axolotl/prompt_strategies/ebft/ebft_reasoning.py b/src/axolotl/prompt_strategies/ebft/ebft_reasoning.py new file mode 100644 index 0000000000..7c756d1583 --- /dev/null +++ b/src/axolotl/prompt_strategies/ebft/ebft_reasoning.py @@ -0,0 +1,319 @@ +""" +Dataset transform for reasoning/thinking datasets with EBFT. + +Handles datasets where assistant responses contain ... reasoning +traces (e.g., TeichAI/Claude-Opus-4.6-Reasoning, Qwen3.5 thinking mode outputs). + +Two variants: + +1. `transform` — For structured EBFT (vLLM mode): + Returns prompt + ground_truth with thinking tags preserved. + Feature matching compares full responses (thinking + answer). + +2. `transform_answer_only` — For structured EBFT (vLLM mode): + Strips ... from ground_truth, so feature matching + only scores the final answer portion. Use when reasoning chains + can vary but the answer should match. + +3. `transform_strided` — For strided EBFT: + Tokenizes the full conversation with thinking traces. + Optionally masks thinking tokens from CE loss (labels=-100 for think spans) + while still placing anchors in thinking regions for feature matching. + +All variants work with OpenAI chat format: + {"messages": [{"role": "...", "content": "...Answer"}]} +""" + +import re + + +def _strip_thinking(text: str) -> str: + """Remove ... blocks from text.""" + return re.sub(r".*?\s*", "", text, flags=re.DOTALL).strip() + + +def _extract_thinking(text: str) -> tuple[str, str]: + """Split text into (thinking, answer) parts.""" + match = re.search(r"(.*?)\s*(.*)", text, flags=re.DOTALL) + if match: + return match.group(1).strip(), match.group(2).strip() + return "", text.strip() + + +def transform(cfg, **kwargs): + """Full response including thinking traces for feature matching. + + For datasets where assistant content has ... tags in the + content field. The ground_truth includes the full content (thinking + answer). + """ + + def transform_fn(example, tokenizer=None): + messages = example.get("messages", example.get("conversations", [])) + + prompt_msgs_snapshot = None + ground_truth = "" + for msg_idx, msg in enumerate(messages): + if msg["role"] == "assistant": + prompt_msgs_snapshot = list(messages[:msg_idx]) + ground_truth = msg["content"] + + return { + "prompt": prompt_msgs_snapshot + if prompt_msgs_snapshot is not None + else messages[:-1], + "ground_truth": ground_truth, + } + + return transform_fn, {"remove_columns": "__all__"} + + +def transform_split_thinking(cfg, **kwargs): + """Split tags into reasoning_content field for native chat template handling. + + For datasets where thinking is embedded in the content field as .... + Splits it into separate reasoning_content and content fields so the model's + chat template can format it natively (e.g., Qwen3.5's reasoning_content support). + + The prompt messages are passed through with reasoning_content properly split, + so vLLM generation with enable_thinking=true produces comparable outputs. + The ground_truth is the full assistant response (thinking + answer) for + feature matching. + + Also works for: + - ... tags + - <|begin_of_thought|>...<|end_of_thought|> tags + """ + _THINKING_PAIRS = [ + ("", ""), + ("", ""), + ("<|begin_of_thought|>", "<|end_of_thought|>"), + ] + + def _split_msg_thinking(msg): + """Split thinking from assistant message content into reasoning_content. + + Always includes reasoning_content key on assistant messages (empty string + if no thinking tags found) to ensure consistent HF dataset schema across + all examples in a batch. + """ + if msg["role"] != "assistant": + return msg + content = msg.get("content", "") + # Already has reasoning_content — pass through + if "reasoning_content" in msg: + return msg + for open_tag, close_tag in _THINKING_PAIRS: + if open_tag in content and close_tag in content: + start = content.find(open_tag) + end = content.find(close_tag) + thinking = content[start + len(open_tag) : end].strip() + answer = content[end + len(close_tag) :].strip() + return { + **msg, + "reasoning_content": thinking, + "content": answer, + } + # No thinking tags — still add reasoning_content for schema consistency + return {**msg, "reasoning_content": ""} + + def _normalize_msg(msg): + """Ensure every message has {role, content, reasoning_content} for HF schema consistency.""" + return { + "role": msg.get("role", ""), + "content": msg.get("content", ""), + "reasoning_content": msg.get("reasoning_content", ""), + } + + def transform_fn(example, tokenizer=None): + messages = example.get("messages", example.get("conversations", [])) + + # Split thinking in all assistant messages, then normalize schema + split_messages = [_normalize_msg(_split_msg_thinking(m)) for m in messages] + + # Build prompt (all messages except last assistant) and ground_truth + prompt_msgs = [] + prompt_msgs_snapshot = None + ground_truth = "" + for msg in split_messages: + if msg["role"] == "assistant": + prompt_msgs_snapshot = list(prompt_msgs) + # ground_truth is the FULL content for feature matching + thinking = msg.get("reasoning_content", "") + answer = msg.get("content", "") + if thinking: + ground_truth = f"\n{thinking}\n\n\n{answer}" + else: + ground_truth = answer + prompt_msgs.append(msg) + + return { + "prompt": prompt_msgs_snapshot + if prompt_msgs_snapshot is not None + else split_messages[:-1], + "ground_truth": ground_truth, + } + + return transform_fn, {"remove_columns": "__all__"} + + +def transform_answer_only(cfg, **kwargs): + """Strip thinking from ground_truth — match features on answer only.""" + + def transform_fn(example, tokenizer=None): + messages = example.get("messages", example.get("conversations", [])) + + prompt_msgs = [] + prompt_msgs_snapshot = None + ground_truth = "" + for msg in messages: + if msg["role"] == "assistant": + prompt_msgs_snapshot = list(prompt_msgs) + ground_truth = _strip_thinking(msg["content"]) + prompt_msgs.append(msg) + + return { + "prompt": prompt_msgs_snapshot + if prompt_msgs_snapshot is not None + else messages[:-1], + "ground_truth": ground_truth, + } + + return transform_fn, {"remove_columns": "__all__"} + + +def transform_strided(cfg, **kwargs): + """For strided EBFT: tokenize with thinking, optionally mask think tokens from CE loss. + + Config options (via cfg): + - ebft.mask_thinking_ce: bool (default False) + If True, set labels=-100 for tokens inside ... blocks. + Feature matching still uses these positions (anchors are placed everywhere + in the completion span). Only CE auxiliary loss is affected. + """ + seq_len = cfg.sequence_len + mask_thinking = False + if cfg.ebft and hasattr(cfg.ebft, "mask_thinking_ce"): + mask_thinking = cfg.ebft.mask_thinking_ce + + def transform_fn(example, tokenizer=None): + messages = example.get("messages", example.get("conversations", [])) + + if tokenizer is None: + for m in messages: + if m.get("role") == "user": + return {"prompt": m["content"]} + return {"prompt": str(messages)} + + pad_id = ( + tokenizer.pad_token_id + if tokenizer.pad_token_id is not None + else tokenizer.eos_token_id + ) + + # Tokenize the full conversation with the chat template + full_text = tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=False, + ) + full_enc = tokenizer( + full_text, + truncation=True, + max_length=seq_len, + add_special_tokens=False, + return_tensors=None, + ) + input_ids = full_enc["input_ids"] + + # Build labels: -100 for non-assistant tokens + labels = [-100] * len(input_ids) + + # Find assistant turn boundaries using incremental tokenization. + # Only the FINAL assistant turn is marked as trainable. + prefix_messages = [] + final_start = None + final_end = None + for msg in messages: + if msg["role"] == "assistant": + prefix_text = tokenizer.apply_chat_template( + prefix_messages, + tokenize=False, + add_generation_prompt=True, + ) + prefix_ids = tokenizer( + prefix_text, + truncation=True, + max_length=seq_len, + add_special_tokens=False, + return_tensors=None, + )["input_ids"] + start = len(prefix_ids) + + prefix_messages.append(msg) + with_turn_text = tokenizer.apply_chat_template( + prefix_messages, + tokenize=False, + add_generation_prompt=False, + ) + with_turn_ids = tokenizer( + with_turn_text, + truncation=True, + max_length=seq_len, + add_special_tokens=False, + return_tensors=None, + )["input_ids"] + end = len(with_turn_ids) + + # Record this turn's boundaries; only the last one will be used + final_start = start + final_end = end + else: + prefix_messages.append(msg) + + # Mark only the final assistant turn as trainable + if final_start is not None and final_end is not None: + for i in range(final_start, min(final_end, len(labels))): + labels[i] = input_ids[i] + + # Optionally mask ... tokens within this turn. + # Find think spans by scanning for and token IDs + # directly in the input_ids (robust to tokenization alignment). + if mask_thinking: + think_open_id = tokenizer.convert_tokens_to_ids("") + think_close_id = tokenizer.convert_tokens_to_ids("") + if think_open_id != tokenizer.unk_token_id: + # Scan from before the assistant turn start to catch + # tags that are part of the template prefix + scan_start = max(0, final_start - 5) + in_think = False + for i in range(scan_start, min(final_end, len(labels))): + if input_ids[i] == think_open_id: + in_think = True + if in_think and i >= final_start: + labels[i] = -100 + if input_ids[i] == think_close_id: + in_think = False + if i >= final_start: + labels[i] = -100 + + # Derive prompt_length + prompt_length = len(input_ids) + for i, lbl in enumerate(labels): + if lbl != -100: + prompt_length = i + break + + # Pad + pad_len = seq_len - len(input_ids) + attention_mask = [1] * len(input_ids) + [0] * pad_len + labels = labels + [-100] * pad_len + input_ids = input_ids + [pad_id] * pad_len + + return { + "input_ids": input_ids, + "attention_mask": attention_mask, + "labels": labels, + "prompt_length": prompt_length, + } + + return transform_fn, {"remove_columns": "__all__"} diff --git a/src/axolotl/prompt_strategies/ebft/ebft_strided_chat.py b/src/axolotl/prompt_strategies/ebft/ebft_strided_chat.py new file mode 100644 index 0000000000..f1d0c01f9f --- /dev/null +++ b/src/axolotl/prompt_strategies/ebft/ebft_strided_chat.py @@ -0,0 +1,110 @@ +""" +Dataset transform for multi-turn chat data with strided EBFT. + +Tokenizes conversations using the model's chat template, producing input_ids +with labels=-100 for system/user turns and real labels for assistant turns. +The strided trainer places anchors only within assistant completion spans. + +Works with datasets in OpenAI chat format: + [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}] +""" + + +def transform(cfg, **kwargs): + seq_len = cfg.sequence_len + + def transform_fn(example, tokenizer=None): + messages = example.get("messages", example.get("conversations", [])) + + if tokenizer is None: + # For preview: just return the first user message + for m in messages: + if m.get("role") == "user": + return {"prompt": m["content"]} + return {"prompt": str(messages)} + + pad_id = tokenizer.pad_token_id or tokenizer.eos_token_id + + # Tokenize the full conversation with the chat template + full_text = tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=False, + ) + full_enc = tokenizer( + full_text, + truncation=True, + max_length=seq_len, + add_special_tokens=False, + return_tensors=None, + ) + input_ids = full_enc["input_ids"] + + # Build labels: -100 for everything except assistant turns. + # Strategy: tokenize incrementally to find assistant turn boundaries. + labels = [-100] * len(input_ids) + + # Tokenize prefix up to each assistant turn to find boundaries + prefix_messages = [] + for msg in messages: + if msg["role"] == "assistant": + # Tokenize prefix (everything before this assistant turn + generation prompt) + prefix_text = tokenizer.apply_chat_template( + prefix_messages, + tokenize=False, + add_generation_prompt=True, + ) + prefix_ids = tokenizer( + prefix_text, + truncation=True, + max_length=seq_len, + add_special_tokens=False, + return_tensors=None, + )["input_ids"] + start = len(prefix_ids) + + # Tokenize prefix + this assistant turn + prefix_messages.append(msg) + with_turn_text = tokenizer.apply_chat_template( + prefix_messages, + tokenize=False, + add_generation_prompt=False, + ) + with_turn_ids = tokenizer( + with_turn_text, + truncation=True, + max_length=seq_len, + add_special_tokens=False, + return_tensors=None, + )["input_ids"] + end = len(with_turn_ids) + + # Mark assistant tokens as trainable + for i in range(start, min(end, len(labels))): + labels[i] = input_ids[i] + else: + prefix_messages.append(msg) + + # Derive prompt_length as the position of the first non-masked label + prompt_length = len(input_ids) # default: all masked + for i, lbl in enumerate(labels): + if lbl != -100: + prompt_length = i + break + + # Pad to seq_len + pad_len = seq_len - len(input_ids) + attention_mask = [1] * len(input_ids) + [0] * pad_len + labels = labels + [-100] * pad_len + input_ids = input_ids + [pad_id] * pad_len + + return { + "input_ids": input_ids, + "attention_mask": attention_mask, + "labels": labels, + "prompt_length": prompt_length, + } + + return transform_fn, { + "remove_columns": "__all__", + } diff --git a/src/axolotl/prompt_strategies/ebft/ebft_strided_structured.py b/src/axolotl/prompt_strategies/ebft/ebft_strided_structured.py new file mode 100644 index 0000000000..7676505753 --- /dev/null +++ b/src/axolotl/prompt_strategies/ebft/ebft_strided_structured.py @@ -0,0 +1,80 @@ +""" +Dataset transform for structured (prompt, completion) data with strided EBFT. + +Tokenizes prompt and completion separately, concatenates into a single +input_ids sequence, and marks prompt tokens with labels=-100 so the +strided trainer knows where to place anchors (completion span only). + +Works with datasets that have chat-style fields (e.g., nvidia/OpenCodeInstruct). +""" + + +def transform(cfg, **kwargs): + seq_len = cfg.sequence_len + + def transform_fn(example, tokenizer=None): + # Extract prompt and completion from the example + prompt_text = example.get( + "input", example.get("prompt", example.get("question", "")) + ) + completion_text = example.get( + "output", example.get("completion", example.get("answer", "")) + ) + + if tokenizer is None: + return {"prompt": prompt_text} + + pad_id = tokenizer.pad_token_id or tokenizer.eos_token_id + + # Tokenize prompt and completion separately + prompt_enc = tokenizer( + prompt_text, + truncation=False, + add_special_tokens=True, + return_tensors=None, + ) + completion_enc = tokenizer( + completion_text, + truncation=False, + add_special_tokens=False, + return_tensors=None, + ) + + prompt_ids = prompt_enc["input_ids"] + completion_ids = completion_enc["input_ids"] + + # Truncate to fit within seq_len (prioritize keeping prompt + some completion) + total_len = len(prompt_ids) + len(completion_ids) + if total_len > seq_len: + # Truncate completion first, then prompt if needed + max_completion = seq_len - len(prompt_ids) + if max_completion < 1: + # Prompt alone exceeds seq_len — truncate prompt, keep at least 1 completion token + prompt_ids = prompt_ids[: seq_len - 1] + completion_ids = completion_ids[:1] + else: + completion_ids = completion_ids[:max_completion] + + input_ids = prompt_ids + completion_ids + prompt_length = len(prompt_ids) + + # Labels: -100 for prompt tokens, input_ids for completion tokens + labels = [-100] * prompt_length + completion_ids + + # Pad to seq_len + pad_len = seq_len - len(input_ids) + attention_mask = [1] * len(input_ids) + [0] * pad_len + labels = labels + [-100] * pad_len + input_ids = input_ids + [pad_id] * pad_len + + return { + "input_ids": input_ids, + "attention_mask": attention_mask, + "labels": labels, + "prompt_length": prompt_length, + } + + # Signal to remove all original columns (filtered to existing ones at map time) + return transform_fn, { + "remove_columns": "__all__", + } diff --git a/src/axolotl/scripts/vllm_serve_lora.py b/src/axolotl/scripts/vllm_serve_lora.py index 9ce4d2771e..f4fcfa1904 100644 --- a/src/axolotl/scripts/vllm_serve_lora.py +++ b/src/axolotl/scripts/vllm_serve_lora.py @@ -241,6 +241,23 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) + # --- Access logging middleware --- + import time as _time + + @app.middleware("http") + async def access_log_middleware(request, call_next): + t0 = _time.monotonic() + response = await call_next(request) + elapsed = _time.monotonic() - t0 + logger.info( + "%s %s %d %.3fs", + request.method, + request.url.path, + response.status_code, + elapsed, + ) + return response + # --- Active LoRA state (shared across endpoints via closure) --- active_lora: dict = {"request": None} @@ -300,7 +317,11 @@ async def generate(request: GenerateRequest): import vllm from packaging.version import Version - from vllm.sampling_params import GuidedDecodingParams + + try: + from vllm.sampling_params import GuidedDecodingParams + except ImportError: + GuidedDecodingParams = None # not available in vLLM 0.17+ images: list[str | None] = request.images or [None] * len(request.prompts) # type: ignore[assignment,list-item] prompts: list[dict[str, Any]] = [] @@ -362,7 +383,12 @@ async def generate(request: GenerateRequest): } conn.send({"type": "call", "method": "generate", "kwargs": kwargs}) - all_outputs = [conn.recv() for conn in connections] + # Use run_in_executor so blocking recv() doesn't freeze the event loop + # (allows /set_lora_adapter/ and other endpoints to be served concurrently) + loop = asyncio.get_running_loop() + all_outputs = await asyncio.gather( + *(loop.run_in_executor(None, conn.recv) for conn in connections) + ) all_outputs = [ o for o, c in zip(all_outputs, chunked_prompts, strict=True) if c ] @@ -404,7 +430,10 @@ async def chat(request: ChatRequest): } conn.send({"type": "call", "method": "chat", "kwargs": kwargs}) - all_outputs = [conn.recv() for conn in connections] + loop = asyncio.get_running_loop() + all_outputs = await asyncio.gather( + *(loop.run_in_executor(None, conn.recv) for conn in connections) + ) all_outputs = [o for o, c in zip(all_outputs, chunked, strict=True) if c] all_outputs = list(chain.from_iterable(all_outputs)) @@ -474,11 +503,51 @@ async def batch_update_named_params(request: BatchUpdateWeightsRequest): ) return {"message": f"Batch update for {len(params_list)} params"} + class HTTPWeightUpdateRequest(BaseModel): + """Weight update via HTTP (no NCCL needed).""" + + params: list[ + dict + ] # [{"name": str, "dtype": str, "shape": list, "data": str (base64)}] + + @app.post("/http_update_weights/") + async def http_update_weights(request: HTTPWeightUpdateRequest): + """Update model weights via HTTP — no NCCL communicator required. + + Tensor data is sent as base64-encoded raw bytes in the request body. + Slower than NCCL for large models but works without cross-process setup. + """ + from axolotl.utils.weight_serde import ( + decode_from_http, + encode_for_ipc, + ) + + weights_to_load = [decode_from_http(p) for p in request.params] + + # Send all weights in a single IPC call. Tensors don't survive + # vLLM's multiproc IPC, so serialize as raw bytes + metadata. + param_entries = [ + encode_for_ipc(name, weight) for name, weight in weights_to_load + ] + kwargs = { + "method": "http_load_weights_batch", + "kwargs": {"params": param_entries}, + } + msg = {"type": "fire_and_forget", "method": "collective_rpc", "kwargs": kwargs} + loop = asyncio.get_running_loop() + await asyncio.gather( + *(loop.run_in_executor(None, c.send, msg) for c in connections) + ) + return {"message": f"HTTP weight update for {len(weights_to_load)} params"} + @app.post("/reset_prefix_cache/") async def reset_prefix_cache(): for conn in connections: conn.send({"type": "call", "method": "reset_prefix_cache"}) - results = [conn.recv() for conn in connections] + loop = asyncio.get_running_loop() + results = await asyncio.gather( + *(loop.run_in_executor(None, conn.recv) for conn in connections) + ) return {"message": f"Reset prefix cache: {all(results)}"} @app.post("/close_communicator/") diff --git a/src/axolotl/scripts/vllm_worker_ext.py b/src/axolotl/scripts/vllm_worker_ext.py index 386460df14..11f8e6ceb5 100644 --- a/src/axolotl/scripts/vllm_worker_ext.py +++ b/src/axolotl/scripts/vllm_worker_ext.py @@ -51,6 +51,19 @@ def _direct_set_weight(self, name: str, weight: torch.Tensor) -> None: model = self.model_runner.model params_dict = dict(model.named_parameters()) + # Handle VLM models where trainer and vLLM use different prefixes. + # Trainer (PEFT stripped): "model.layers.X..." or "model.language_model.layers.X..." + # vLLM (Qwen3.5): "language_model.model.layers.X..." + if name not in params_dict: + # Try common prefix remappings + for src_prefix, dst_prefix in [ + ("model.language_model.layers.", "language_model.model.layers."), + ("model.layers.", "language_model.model.layers."), + ]: + if name.startswith(src_prefix): + name = dst_prefix + name[len(src_prefix) :] + break + # Check if this is a simple direct param (exists as-is) if name in params_dict: params_dict[name].data.copy_(weight.to(params_dict[name].dtype)) @@ -106,7 +119,15 @@ def _direct_set_weight(self, name: str, weight: torch.Tensor) -> None: return # Fallback: try load_weights (may work for non-stacked params) - logger.warning("Falling back to load_weights for param: %s", name) + # Log the actual param names available for debugging + sample_keys = [ + k for k in params_dict if "layers.31.mlp" in k or "layers.31.self_attn" in k + ][:3] + logger.warning( + "Falling back to load_weights for param: %s (sample vLLM keys: %s)", + name, + sample_keys, + ) model.load_weights(weights=[(name, weight)]) def update_named_param(self, name, dtype, shape): @@ -156,3 +177,32 @@ def batch_update_named_params(self, params_list: list[tuple[str, str, tuple]]): # Load weights using direct set (handles stacked params) for name, weight in weights_to_load: self._direct_set_weight(name, weight) + + def http_load_weights(self, weights: list[tuple[str, torch.Tensor]]): + """Load weights received via HTTP (no NCCL needed).""" + for name, weight in weights: + self._direct_set_weight(name, weight.to(self.device)) + + def http_load_weight(self, **kwargs): + """Load a single weight received via HTTP (no NCCL needed). + + Reconstructs the tensor from raw bytes since tensors don't survive + vLLM's multiproc IPC serialization. Uses vLLM's ``load_weights`` + which handles TP sharding and stacked-param packing automatically. + """ + from axolotl.utils.weight_serde import decode_from_ipc + + name, weight = decode_from_ipc(kwargs) + model = self.model_runner.model + model.load_weights(weights=[(name, weight)]) + + def http_load_weights_batch(self, params: list[dict]): + """Load multiple weights in a single IPC call. + + Uses vLLM's ``load_weights`` which handles TP sharding automatically. + """ + from axolotl.utils.weight_serde import decode_from_ipc + + model = self.model_runner.model + weights = [decode_from_ipc(p) for p in params] + model.load_weights(weights=weights) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 522dd7e287..774aa1cecf 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -138,7 +138,11 @@ def setup_reference_model( model_ref = None # explicit setting to None else: reference_model: bool = True - if cfg.rl == RLType.GRPO and cfg.trl.beta == 0: + trl_cfg = getattr(cfg, "trl", None) + if ( + cfg.rl in {RLType.GRPO, RLType.EBFT} + and getattr(trl_cfg, "beta", 0) == 0 + ): reference_model = False # load the model again for model_ref/baseline model_loader = ModelLoader(cfg, tokenizer, reference_model=reference_model) @@ -206,7 +210,7 @@ def execute_training( gradient_accumulation_steps=cfg.gradient_accumulation_steps, ring_attn_func=cfg.ring_attn_func, heads_k_stride=cfg.heads_k_stride, - gather_outputs=cfg.rl is RLType.GRPO, + gather_outputs=cfg.rl in {RLType.GRPO, RLType.EBFT}, device_mesh=trainer.accelerator.torch_device_mesh, ) ) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 36370ef13a..afdb7f2a22 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -691,8 +691,7 @@ def log_table_from_dataloader(name: str, table_dataloader): ].append(pred_step_text) row_index += 1 if logger == "wandb": - # type: ignore[attr-defined] - wandb.run.log( + wandb.run.log( # type: ignore[attr-defined] { f"{name} - Predictions vs Ground Truth": pd.DataFrame( table_data @@ -748,12 +747,13 @@ def on_train_begin( mode="w", delete=False, suffix=".yml", prefix="axolotl_config_" ) as temp_file: copyfile(self.axolotl_config_path, temp_file.name) - artifact = wandb.Artifact( - f"config-{wandb.run.id}", type="axolotl-config" + artifact = wandb.Artifact( # type: ignore[attr-defined] + f"config-{wandb.run.id}", # type: ignore[attr-defined] + type="axolotl-config", ) artifact.add_file(temp_file.name) - wandb.log_artifact(artifact) - wandb.save(temp_file.name) + wandb.log_artifact(artifact) # type: ignore[attr-defined] + wandb.save(temp_file.name) # type: ignore[attr-defined] LOG.info( "The Axolotl config has been saved to the WandB run under files." ) @@ -779,12 +779,13 @@ def on_train_begin( temp_ct_file.write(str(chat_tpl)) temp_ct_file.flush() - artifact = wandb.Artifact( - f"chat-template-{wandb.run.id}", type="jinja-template" + artifact = wandb.Artifact( # type: ignore[attr-defined] + f"chat-template-{wandb.run.id}", # type: ignore[attr-defined] + type="jinja-template", ) artifact.add_file(temp_ct_file.name) - wandb.log_artifact(artifact) - wandb.save(temp_ct_file.name) + wandb.log_artifact(artifact) # type: ignore[attr-defined] + wandb.save(temp_ct_file.name) # type: ignore[attr-defined] LOG.info( "The chat_template_jinja has been saved to the WandB run under files." ) @@ -810,13 +811,13 @@ def on_train_begin( else: skip_upload = True if not skip_upload: - artifact = wandb.Artifact( - f"deepspeed-config-{wandb.run.id}", + artifact = wandb.Artifact( # type: ignore[attr-defined] + f"deepspeed-config-{wandb.run.id}", # type: ignore[attr-defined] type="deepspeed-config", ) artifact.add_file(temp_file.name) - wandb.log_artifact(artifact) - wandb.save(temp_file.name) + wandb.log_artifact(artifact) # type: ignore[attr-defined] + wandb.save(temp_file.name) # type: ignore[attr-defined] LOG.info( "The DeepSpeed config has been saved to the WandB run under files." ) diff --git a/src/axolotl/utils/callbacks/generation.py b/src/axolotl/utils/callbacks/generation.py index 439258c8b9..da36b9ad0e 100644 --- a/src/axolotl/utils/callbacks/generation.py +++ b/src/axolotl/utils/callbacks/generation.py @@ -28,36 +28,36 @@ def on_evaluate( if not getattr(cfg, "generate_samples", False): return - dataloader = None - try: - if getattr(self.trainer, "eval_dataset", None) is not None: - dataloader = self.trainer.get_eval_dataloader() - LOG.info( - f"Using eval dataloader for generation at step {state.global_step}" - ) - except Exception as e: - LOG.warning(f"Could not get eval dataloader: {e}") - dataloader = None - - if dataloader is None: - dataloader = self.trainer.get_train_dataloader() + dataloader = None + try: + if getattr(self.trainer, "eval_dataset", None) is not None: + dataloader = self.trainer.get_eval_dataloader() LOG.info( - f"Using train dataloader for generation at step {state.global_step}" + f"Using eval dataloader for generation at step {state.global_step}" ) + except Exception as e: + LOG.warning(f"Could not get eval dataloader: {e}") + dataloader = None - samples = generate_samples( - model=self.trainer.model, - tokenizer=self.trainer.processing_class, - dataloader=dataloader, - num_generation_samples=getattr(cfg, "num_generation_samples", 3), - max_new_tokens=getattr(cfg, "generation_max_new_tokens", 50), - temperature=getattr(cfg, "generation_temperature", 0.7), - top_p=getattr(cfg, "generation_top_p", None), - top_k=getattr(cfg, "generation_top_k", None), - do_sample=getattr(cfg, "generation_do_sample", True), - prompt_ratio=getattr(cfg, "generation_prompt_ratio", 0.5), + if dataloader is None: + dataloader = self.trainer.get_train_dataloader() + LOG.info( + f"Using train dataloader for generation at step {state.global_step}" ) - self._log_samples(samples, state.global_step) + + samples = generate_samples( + model=self.trainer.model, + tokenizer=self.trainer.processing_class, + dataloader=dataloader, + num_generation_samples=getattr(cfg, "num_generation_samples", 3), + max_new_tokens=getattr(cfg, "generation_max_new_tokens", 50), + temperature=getattr(cfg, "generation_temperature", 0.7), + top_p=getattr(cfg, "generation_top_p", None), + top_k=getattr(cfg, "generation_top_k", None), + do_sample=getattr(cfg, "generation_do_sample", True), + prompt_ratio=getattr(cfg, "generation_prompt_ratio", 0.5), + ) + self._log_samples(samples, state.global_step) def _log_samples(self, samples: list, step: int): """Log generated samples to console and W&B.""" @@ -71,10 +71,10 @@ def _log_samples(self, samples: list, step: int): try: import wandb - if wandb.run is not None: - wandb.log( + if wandb.run is not None: # type: ignore[attr-defined] + wandb.log( # type: ignore[attr-defined] { - f"samples/sample_{i + 1}": wandb.Html( + f"samples/sample_{i + 1}": wandb.Html( # type: ignore[attr-defined] f"
{wandb_text}
" ) }, diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index 2c386f35ef..ef91e11246 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -9,6 +9,7 @@ from axolotl.loaders import load_tokenizer from axolotl.prompt_strategies.dpo import load as load_dpo +from axolotl.prompt_strategies.ebft import load as load_ebft from axolotl.prompt_strategies.kto import load as load_kto from axolotl.prompt_strategies.orpo import load as load_orpo from axolotl.utils.data.lock import FileLockLoader @@ -173,7 +174,7 @@ def _drop_long_sequences( return (len_prompt + len_completion) <= sequence_len - if rl in {RLType.GRPO, RLType.GDPO}: + if rl in {RLType.GRPO, RLType.GDPO, RLType.EBFT}: return True raise ValueError("Unknown RL type") @@ -209,12 +210,30 @@ def _load_split(cfg: DictDefault, split: Literal["train", "test"]) -> Dataset: ds_transform_fn = load_orpo(_type, cfg, dataset_idx=i) elif cfg.rl is RLType.KTO: ds_transform_fn = load_kto(_type, cfg, dataset_idx=i) + elif cfg.rl is RLType.EBFT: + ds_transform_fn = load_ebft(_type, cfg, dataset_idx=i) else: ds_transform_fn = load_dpo(_type, cfg, dataset_idx=i) map_kwargs: dict[str, Any] = {} if isinstance(ds_transform_fn, tuple): ds_transform_fn, map_kwargs = ds_transform_fn + # Handle remove_columns: "__all__" removes all original columns, + # or filter a list to only columns that exist in the dataset + if "remove_columns" in map_kwargs: + ds_columns = ( + dataset.column_names + if isinstance(dataset, Dataset) + else dataset[split].column_names + if isinstance(dataset, DatasetDict) + else [] + ) + if map_kwargs["remove_columns"] == "__all__": + map_kwargs["remove_columns"] = list(ds_columns) + else: + map_kwargs["remove_columns"] = [ + c for c in map_kwargs["remove_columns"] if c in ds_columns + ] split_datasets[i] = _map_dataset( cfg, dataset, ds_transform_fn, tokenizer, **map_kwargs ) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 34fd9ba2c8..982e3e4199 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -55,6 +55,119 @@ LOG = get_logger(__name__) +class EBFTConfig(BaseModel): + """Configuration for Energy-Based Fine-Tuning (EBFT)""" + + feature_layers: list[float] = Field( + default=[0.25, 0.5, 0.75], + json_schema_extra={ + "description": "Fractional layer depths for feature extraction (e.g., [0.25, 0.5, 0.75])" + }, + ) + embed_method: Literal["last_token", "mean_pooling", "completion_mean", "concat"] = ( + Field( + default="last_token", + json_schema_extra={ + "description": "Embedding method: 'last_token', 'mean_pooling', 'completion_mean', or 'concat'" + }, + ) + ) + use_whitening: bool = Field( + default=False, + json_schema_extra={"description": "Apply SVD whitening to feature embeddings"}, + ) + alignment_coef: float = Field( + default=1.0, + json_schema_extra={ + "description": "Coefficient for alignment reward (cosine similarity with ground truth)" + }, + ) + diversity_coef: float = Field( + default=1.0, + json_schema_extra={ + "description": "Coefficient for diversity penalty (pairwise similarity between samples)" + }, + ) + ce_coef: float = Field( + default=0.0, + json_schema_extra={ + "description": "Cross-entropy loss coefficient on ground-truth tokens" + }, + ) + adaptive_max_tokens: bool = Field( + default=True, + json_schema_extra={ + "description": "Set per-batch max_tokens based on ground-truth length" + }, + ) + gt_length_multiplier: float = Field( + default=1.5, + ge=0.1, + json_schema_extra={ + "description": "Multiplier for ground-truth token count when computing adaptive max_tokens" + }, + ) + + # Strided mode fields (for unstructured text) + mode: Literal["structured", "strided"] = Field( + default="structured", + json_schema_extra={ + "description": "EBFT mode: 'structured' (QA with vLLM) or 'strided' (unstructured text)" + }, + ) + stride: int = Field( + default=8, + ge=1, + json_schema_extra={"description": "Stride between anchor points (tokens)"}, + ) + context_length: int = Field( + default=8, + ge=1, + json_schema_extra={"description": "Context window size per block"}, + ) + generate_max_len: int = Field( + default=8, + ge=1, + json_schema_extra={"description": "Tokens to generate per block"}, + ) + n_samples_per_prompt: int = Field( + default=4, + ge=1, + json_schema_extra={"description": "Independent rollouts per document"}, + ) + temperature: float = Field( + default=0.6, + ge=0.0, + json_schema_extra={ + "description": "Sampling temperature for strided generation" + }, + ) + top_p: float = Field( + default=1.0, + ge=0.0, + le=1.0, + json_schema_extra={"description": "Top-p nucleus sampling threshold"}, + ) + rl_coef: float = Field( + default=1.0, + json_schema_extra={"description": "RL policy gradient loss coefficient"}, + ) + advantage_estimator: Literal["rloo", "group_norm", "reinforce"] = Field( + default="rloo", + json_schema_extra={ + "description": "Advantage estimator: 'rloo', 'group_norm', 'reinforce'" + }, + ) + min_completion_prefix: int = Field( + default=0, + ge=0, + json_schema_extra={ + "description": "Minimum tokens into completion before placing anchors. " + "Skips anchors too close to the prompt boundary where features are dominated by prompt context." + }, + ) + + class AxolotlInputConfig( ModelInputConfig, ModelOutputConfig, @@ -131,7 +244,7 @@ class AxolotlInputConfig( rl: RLType | None = Field( default=None, json_schema_extra={ - "description": "Use RL training: 'dpo', 'ipo', 'kto', 'simpo', 'orpo', 'grpo'" + "description": "Use RL training: 'dpo', 'ipo', 'kto', 'simpo', 'orpo', 'grpo', 'ebft'" }, ) trl: TRLConfig | None = Field( @@ -140,6 +253,12 @@ class AxolotlInputConfig( vllm: VllmConfig | None = Field( default_factory=lambda: VllmConfig(), ) + ebft: EBFTConfig | None = Field( + default=None, + json_schema_extra={ + "description": "Configuration for Energy-Based Fine-Tuning (EBFT)" + }, + ) qat: QATConfig | None = None quantization: PTQConfig | None = None reward_model: bool | None = Field( diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 40fa314f4e..7ffa793f20 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -35,6 +35,7 @@ class RLType(str, Enum): ORPO = "orpo" KTO = "kto" SIMPO = "simpo" + EBFT = "ebft" class ChatTemplate(str, Enum): diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index 2d7c36f961..4ef42db665 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -1,6 +1,6 @@ """Pydantic models for TRL trainer configuration""" -from typing import Literal +from typing import Any, Literal from pydantic import BaseModel, Field @@ -133,6 +133,20 @@ class TRLConfig(BaseModel): "description": "Penalty for tokens that appear in prompt and generated text." }, ) + generation_kwargs: dict[str, Any] | None = Field( + default=None, + json_schema_extra={ + "description": "Additional generation parameters passed to vLLM SamplingParams. " + "Useful for stop_token_ids, seed, frequency_penalty, etc." + }, + ) + chat_template_kwargs: dict[str, Any] | None = Field( + default=None, + json_schema_extra={ + "description": "Additional kwargs for the chat template. " + "E.g., {enable_thinking: false} for Qwen3.5 models." + }, + ) num_iterations: int | None = Field( default=None, json_schema_extra={ diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index c902d8703c..c7eeb6fa40 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1482,6 +1482,124 @@ def check_tensor_parallel_optimizer(self): return self +class EBFTValidationMixin: + """Validation for EBFT (Energy-Based Fine-Tuning) configuration.""" + + @model_validator(mode="before") + @classmethod + def check_ebft_config_required(cls, data): + """rl: ebft requires an ebft config section.""" + if data.get("rl") == "ebft" and not data.get("ebft"): + raise ValueError( + "`ebft` config section is required when `rl: ebft` is set. " + "Add an `ebft:` section with at least `mode: structured` or `mode: strided`." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_ebft_torch_compile(cls, data): + """torch_compile + flex_attention + gradient_checkpointing causes dynamo recompiles + and CheckpointErrors. The flex_attention kernel compiles itself internally — + whole-model torch.compile is not needed and actively harmful.""" + if ( + data.get("rl") == "ebft" + and data.get("torch_compile") is True + and data.get("ebft", {}).get("mode") == "strided" + ): + if data.get("gradient_checkpointing"): + raise ValueError( + "EBFT strided mode: `torch_compile: true` with `gradient_checkpointing: true` " + "causes CheckpointError (BlockMask metadata mismatch during recomputation). " + "Remove `torch_compile` — the flex_attention kernel compiles itself internally." + ) + LOG.warning( + "EBFT strided mode: `torch_compile: true` causes dynamo recompiles from " + "variable sequence lengths across steps. Consider removing it — " + "flex_attention compiles itself internally." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_ebft_gradient_checkpointing_reentrant(cls, data): + """flex_attention + non-reentrant gradient checkpointing causes CheckpointError.""" + if ( + data.get("rl") == "ebft" + and data.get("ebft", {}).get("mode") == "strided" + and data.get("flex_attention") + and data.get("gradient_checkpointing") + ): + gc_kwargs = data.get("gradient_checkpointing_kwargs") or {} + if not gc_kwargs.get("use_reentrant"): + LOG.warning( + "EBFT strided mode with flex_attention: setting `use_reentrant: true` in " + "gradient_checkpointing_kwargs (required for flex_attention compatibility). " + "Non-reentrant checkpointing causes CheckpointError with BlockMask metadata." + ) + if data.get("gradient_checkpointing_kwargs") is None: + data["gradient_checkpointing_kwargs"] = {} + data["gradient_checkpointing_kwargs"]["use_reentrant"] = True + return data + + @model_validator(mode="before") + @classmethod + def check_ebft_activation_offloading(cls, data): + """activation_offloading replaces gradient checkpointing with FSDP-style wrapping, + which conflicts with flex_attention's use_reentrant requirement.""" + if ( + data.get("rl") == "ebft" + and data.get("ebft", {}).get("mode") == "strided" + and data.get("activation_offloading") is True + and data.get("flex_attention") + ): + raise ValueError( + "EBFT strided mode: `activation_offloading: true` is incompatible with " + "`flex_attention: true`. Activation offloading replaces gradient checkpointing " + "with FSDP-style wrapping that conflicts with flex_attention's reentrant " + "checkpoint requirement. Remove `activation_offloading` — the strided trainer " + "uses micro-batched forward passes for memory efficiency instead." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_ebft_strided_sequence_len(cls, data): + """Warn if sequence_len is too large for single-GPU strided EBFT.""" + if data.get("rl") != "ebft" or data.get("ebft", {}).get("mode") != "strided": + return data + ebft = data.get("ebft", {}) + seq_len = data.get("sequence_len", 512) + n_samples = ebft.get("n_samples_per_prompt", 4) + gen_len = ebft.get("generate_max_len", 8) + stride = ebft.get("stride", 8) + ctx_len = ebft.get("context_length", 8) + max_blocks = (seq_len - gen_len - ctx_len) // stride + 1 + full_seq = seq_len + max_blocks * gen_len + # Rough estimate: 8.7 GB per sample at S=3900 for 1B model + if full_seq * n_samples > 20000: + LOG.warning( + f"EBFT strided: full_seq_len={full_seq} * n_samples={n_samples} = " + f"{full_seq * n_samples} token-samples per step. This may require >24GB VRAM " + f"for a 1B+ model. Consider reducing sequence_len, n_samples_per_prompt, or stride." + ) + return data + + @model_validator(mode="before") + @classmethod + def check_ebft_strided_dataset_split(cls, data): + """Warn about the common `train_on_split` mistake (silently ignored by schema).""" + datasets = data.get("datasets", []) + for ds in datasets or []: + if isinstance(ds, dict) and ds.get("train_on_split"): + LOG.warning( + f"Dataset has `train_on_split: {ds['train_on_split']}` — this field " + f"is not recognized and will be silently ignored. " + f"Use `split: {ds['train_on_split']}` instead." + ) + return data + + class GRPOVllmValidationMixin: """Validation mixin for vllm when using GRPO.""" @@ -1507,6 +1625,7 @@ class ValidationMixin( PretrainingValidationMixin, ModelCompatibilityValidationMixin, ComplexValidationMixin, + EBFTValidationMixin, GRPOVllmValidationMixin, ): """Full validation mixin for Axolotl configuration.""" diff --git a/src/axolotl/utils/schemas/vllm.py b/src/axolotl/utils/schemas/vllm.py index c0aa48d668..5198d4173a 100644 --- a/src/axolotl/utils/schemas/vllm.py +++ b/src/axolotl/utils/schemas/vllm.py @@ -57,6 +57,13 @@ class VllmConfig(BaseModel): default=None, json_schema_extra={"description": "Reasoning parser for VLLM"}, ) + enforce_eager: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Disable CUDA graph capture in vLLM. Required for models with " + "causal_conv1d (e.g., Qwen3.5 hybrid linear attention)." + }, + ) serve_module: str | None = Field( default=None, json_schema_extra={ diff --git a/src/axolotl/utils/weight_serde.py b/src/axolotl/utils/weight_serde.py new file mode 100644 index 0000000000..d4b8046810 --- /dev/null +++ b/src/axolotl/utils/weight_serde.py @@ -0,0 +1,94 @@ +"""Serialize / deserialize tensors for HTTP and IPC weight sync. + +NumPy doesn't support bfloat16, so bf16 tensors are cast to fp16 on the wire +and reconstructed at the destination. All encode/decode helpers live here so +the logic isn't duplicated across trl_vllm.py, vllm_serve_lora.py, and +vllm_worker_ext.py. +""" + +import base64 + +import torch + + +def encode_for_http(name: str, weight: torch.Tensor) -> dict: + """Encode a named parameter for JSON transport over HTTP. + + Returns a dict with keys: name, dtype (original), shape, data (base64). + bf16 tensors are sent as fp16 bytes; the original dtype is preserved in + the ``dtype`` field so the receiver can cast back. + """ + w_cpu = weight.contiguous().cpu() + orig_dtype = str(weight.dtype) + if w_cpu.dtype == torch.bfloat16: + w_cpu = w_cpu.half() + raw = w_cpu.numpy().tobytes() + return { + "name": name, + "dtype": orig_dtype, + "shape": list(weight.shape), + "data": base64.b64encode(raw).decode("ascii"), + } + + +def decode_from_http(entry: dict) -> tuple[str, torch.Tensor]: + """Decode an HTTP-encoded weight entry back to a named tensor. + + Infers wire dtype from byte count (bf16 arrives as fp16) and casts to the + original dtype stored in ``entry["dtype"]``. + """ + target_dtype = getattr(torch, entry["dtype"].split(".")[-1]) + shape = tuple(entry["shape"]) + raw = base64.b64decode(entry["data"]) + + n_elements = 1 + for s in shape: + n_elements *= s + wire_bytes_per_elem = len(raw) // max(n_elements, 1) + if wire_bytes_per_elem == 2: + wire_dtype = torch.float16 + elif wire_bytes_per_elem == 4: + wire_dtype = torch.float32 + else: + wire_dtype = target_dtype + + weight = torch.frombuffer(bytearray(raw), dtype=wire_dtype).reshape(shape) + if wire_dtype != target_dtype: + weight = weight.to(target_dtype) + return entry["name"], weight + + +def encode_for_ipc(name: str, weight: torch.Tensor) -> dict: + """Encode a tensor for vLLM's multiproc IPC (raw bytes, no base64). + + Returns a dict with keys: name, data (bytes), dtype (wire), target_dtype + (original), shape. bf16 tensors are serialized as fp16. + """ + w = weight.contiguous() + target_dtype = str(w.dtype).split(".")[-1] + if w.dtype == torch.bfloat16: + w = w.half() + wire_dtype = str(w.dtype).split(".")[-1] + return { + "name": name, + "data": w.numpy().tobytes(), + "dtype": wire_dtype, + "target_dtype": target_dtype, + "shape": list(weight.shape), + } + + +def decode_from_ipc(entry: dict) -> tuple[str, torch.Tensor]: + """Decode an IPC-encoded weight entry back to a named tensor. + + Handles optional ``target_dtype`` for backward compatibility with older + serve code that may not include it. + """ + wire_dtype = getattr(torch, entry["dtype"]) + weight = torch.frombuffer(bytearray(entry["data"]), dtype=wire_dtype).reshape( + entry["shape"] + ) + target_dtype = entry.get("target_dtype") + if target_dtype and target_dtype != entry["dtype"]: + weight = weight.to(getattr(torch, target_dtype)) + return entry["name"], weight diff --git a/tests/test_ebft_kernels.py b/tests/test_ebft_kernels.py new file mode 100644 index 0000000000..2c05a887ae --- /dev/null +++ b/tests/test_ebft_kernels.py @@ -0,0 +1,294 @@ +"""Correctness tests for fused EBFT Triton kernels.""" + +import pytest +import torch +import torch.nn.functional as F + +from axolotl.core.trainers.ebft.kernels import ( + fused_cosine_similarity, + fused_diversity_penalty, + fused_log_softmax_gather, + fused_reinforce_loss, +) + +# Skip all tests if CUDA not available +pytestmark = pytest.mark.skipif( + not torch.cuda.is_available(), reason="CUDA required for Triton kernels" +) + +DEVICE = "cuda" + + +# --------------------------------------------------------------------------- +# 1. fused_log_softmax_gather +# --------------------------------------------------------------------------- +class TestFusedLogSoftmaxGather: + def _reference(self, logits, labels): + """PyTorch reference: log_softmax + gather.""" + lp = F.log_softmax(logits.float(), dim=-1) + return lp.gather(-1, labels.unsqueeze(-1)).squeeze(-1) + + def test_basic_correctness(self): + B, S, V = 2, 16, 1024 + logits = torch.randn(B, S, V, device=DEVICE, dtype=torch.bfloat16) + labels = torch.randint(0, V, (B, S), device=DEVICE) + + ref = self._reference(logits, labels) + out = fused_log_softmax_gather(logits, labels) + + torch.testing.assert_close(out, ref, atol=1e-3, rtol=1e-3) + + def test_large_vocab(self): + """Test with realistic vocab size (128K).""" + B, S, V = 1, 8, 128256 + logits = torch.randn(B, S, V, device=DEVICE, dtype=torch.bfloat16) + labels = torch.randint(0, V, (B, S), device=DEVICE) + + ref = self._reference(logits, labels) + out = fused_log_softmax_gather(logits, labels) + + torch.testing.assert_close(out, ref, atol=1e-2, rtol=1e-2) + + def test_fp32_input(self): + B, S, V = 2, 8, 512 + logits = torch.randn(B, S, V, device=DEVICE, dtype=torch.float32) + labels = torch.randint(0, V, (B, S), device=DEVICE) + + ref = self._reference(logits, labels) + out = fused_log_softmax_gather(logits, labels) + + torch.testing.assert_close(out, ref, atol=1e-5, rtol=1e-5) + + def test_output_is_negative(self): + """log_softmax values should always be <= 0.""" + B, S, V = 4, 32, 2048 + logits = torch.randn(B, S, V, device=DEVICE, dtype=torch.bfloat16) + labels = torch.randint(0, V, (B, S), device=DEVICE) + + out = fused_log_softmax_gather(logits, labels) + assert (out <= 1e-5).all(), "log_softmax values should be <= 0" + + def test_extreme_logits(self): + """Test numerical stability with very large/small logits.""" + B, S, V = 1, 4, 256 + logits = torch.randn(B, S, V, device=DEVICE, dtype=torch.float32) + logits[:, 0, :] = 1000.0 # very large + logits[:, 1, :] = -1000.0 # very small + logits[:, 2, 0] = 1000.0 # one hot-ish + labels = torch.zeros(B, S, device=DEVICE, dtype=torch.long) + + ref = self._reference(logits, labels) + out = fused_log_softmax_gather(logits, labels) + + assert torch.isfinite(out).all(), "Should handle extreme values" + torch.testing.assert_close(out, ref, atol=1e-4, rtol=1e-4) + + def test_2d_input(self): + """Test with pre-flattened (N, V) input.""" + N, V = 64, 4096 + logits = torch.randn(N, V, device=DEVICE, dtype=torch.bfloat16) + labels = torch.randint(0, V, (N,), device=DEVICE) + + ref = self._reference(logits.unsqueeze(0), labels.unsqueeze(0)).squeeze(0) + out = fused_log_softmax_gather(logits, labels) + + torch.testing.assert_close(out, ref, atol=1e-3, rtol=1e-3) + + +# --------------------------------------------------------------------------- +# 2. fused_reinforce_loss +# --------------------------------------------------------------------------- +class TestFusedReinforceLoss: + def _reference(self, logps, advantages, mask): + """PyTorch reference implementation.""" + loss_per_token = -logps * advantages + return (loss_per_token * mask.float()).sum() / mask.float().sum().clamp(min=1) + + def test_basic_correctness(self): + N = 1024 + logps = torch.randn(N, device=DEVICE, dtype=torch.float32) + advs = torch.randn(N, device=DEVICE, dtype=torch.float32) + mask = torch.randint(0, 2, (N,), device=DEVICE, dtype=torch.bool) + + ref = self._reference(logps, advs, mask) + out = fused_reinforce_loss(logps, advs, mask) + + torch.testing.assert_close(out, ref, atol=1e-4, rtol=1e-4) + + def test_2d_input(self): + """Test with (B, S) shaped inputs.""" + B, S = 4, 256 + logps = torch.randn(B, S, device=DEVICE, dtype=torch.float32) + advs = torch.randn(B, S, device=DEVICE, dtype=torch.float32) + mask = torch.randint(0, 2, (B, S), device=DEVICE, dtype=torch.bool) + + ref = self._reference(logps, advs, mask) + out = fused_reinforce_loss(logps, advs, mask) + + torch.testing.assert_close(out, ref, atol=1e-4, rtol=1e-4) + + def test_all_masked(self): + """All-zero mask should return 0.""" + N = 512 + logps = torch.randn(N, device=DEVICE, dtype=torch.float32) + advs = torch.randn(N, device=DEVICE, dtype=torch.float32) + mask = torch.zeros(N, device=DEVICE, dtype=torch.bool) + + out = fused_reinforce_loss(logps, advs, mask) + assert out.item() == 0.0 + + def test_all_unmasked(self): + N = 512 + logps = torch.randn(N, device=DEVICE, dtype=torch.float32) + advs = torch.randn(N, device=DEVICE, dtype=torch.float32) + mask = torch.ones(N, device=DEVICE, dtype=torch.bool) + + ref = self._reference(logps, advs, mask) + out = fused_reinforce_loss(logps, advs, mask) + + torch.testing.assert_close(out, ref, atol=1e-4, rtol=1e-4) + + def test_large(self): + """Test with realistic size (4 * 3000 tokens).""" + N = 12000 + logps = torch.randn(N, device=DEVICE, dtype=torch.float32) + advs = torch.randn(N, device=DEVICE, dtype=torch.float32) + mask = torch.randint(0, 2, (N,), device=DEVICE, dtype=torch.bool) + + ref = self._reference(logps, advs, mask) + out = fused_reinforce_loss(logps, advs, mask) + + torch.testing.assert_close(out, ref, atol=1e-3, rtol=1e-3) + + +# --------------------------------------------------------------------------- +# 3. fused_cosine_similarity +# --------------------------------------------------------------------------- +class TestFusedCosineSimilarity: + def test_basic_correctness(self): + N, D = 64, 256 + a = torch.randn(N, D, device=DEVICE, dtype=torch.bfloat16) + b = torch.randn(N, D, device=DEVICE, dtype=torch.bfloat16) + + ref = F.cosine_similarity(a.float(), b.float(), dim=-1) + out = fused_cosine_similarity(a, b) + + torch.testing.assert_close(out, ref, atol=1e-3, rtol=1e-3) + + def test_batched(self): + """Test with (B, N, NB, D) shaped input.""" + B, N, NB, D = 2, 4, 16, 512 + a = torch.randn(B, N, NB, D, device=DEVICE, dtype=torch.bfloat16) + b = torch.randn(B, N, NB, D, device=DEVICE, dtype=torch.bfloat16) + + ref = F.cosine_similarity(a.float(), b.float(), dim=-1) + out = fused_cosine_similarity(a, b) + + torch.testing.assert_close(out, ref, atol=1e-2, rtol=1e-2) + + def test_identical_vectors(self): + """Identical vectors should give similarity = 1.""" + N, D = 16, 128 + a = torch.randn(N, D, device=DEVICE, dtype=torch.float32) + + out = fused_cosine_similarity(a, a) + torch.testing.assert_close( + out, + torch.ones(N, device=DEVICE, dtype=torch.float32), + atol=1e-5, + rtol=1e-5, + ) + + def test_orthogonal_vectors(self): + """Orthogonal vectors should give similarity = 0.""" + D = 128 + a = torch.zeros(1, D, device=DEVICE, dtype=torch.float32) + b = torch.zeros(1, D, device=DEVICE, dtype=torch.float32) + a[0, 0] = 1.0 + b[0, 1] = 1.0 + + out = fused_cosine_similarity(a, b) + assert abs(out.item()) < 1e-5 + + def test_opposite_vectors(self): + """Opposite vectors should give similarity = -1.""" + N, D = 8, 64 + a = torch.randn(N, D, device=DEVICE, dtype=torch.float32) + out = fused_cosine_similarity(a, -a) + torch.testing.assert_close( + out, + -torch.ones(N, device=DEVICE, dtype=torch.float32), + atol=1e-5, + rtol=1e-5, + ) + + def test_large_dimension(self): + """Test with large feature dimension (multi-layer concatenated features).""" + N, D = 32, 4608 # 3 layers * 1536 hidden + a = torch.randn(N, D, device=DEVICE, dtype=torch.bfloat16) + b = torch.randn(N, D, device=DEVICE, dtype=torch.bfloat16) + + ref = F.cosine_similarity(a.float(), b.float(), dim=-1) + out = fused_cosine_similarity(a, b) + + torch.testing.assert_close(out, ref, atol=1e-2, rtol=1e-2) + + +# --------------------------------------------------------------------------- +# 4. fused_diversity_penalty +# --------------------------------------------------------------------------- +class TestFusedDiversityPenalty: + def _reference(self, embeddings): + """PyTorch reference: bmm + mask diagonal + mean.""" + B, N, D = embeddings.shape + sims = torch.bmm(embeddings.float(), embeddings.float().transpose(1, 2)) + eye = torch.eye(N, device=embeddings.device, dtype=torch.bool) + sims = sims.masked_fill(eye.unsqueeze(0), 0.0) + return sims.sum(dim=-1) / (N - 1) + + def test_basic_correctness(self): + B, N, D = 4, 4, 256 + emb = torch.randn(B, N, D, device=DEVICE, dtype=torch.bfloat16) + + ref = self._reference(emb) + out = fused_diversity_penalty(emb) + + torch.testing.assert_close(out, ref, atol=1e-2, rtol=1e-2) + + def test_two_samples(self): + """With n=2, diversity = dot(a, b) for each.""" + B, D = 3, 128 + emb = torch.randn(B, 2, D, device=DEVICE, dtype=torch.float32) + + ref = self._reference(emb) + out = fused_diversity_penalty(emb) + + torch.testing.assert_close(out, ref, atol=1e-4, rtol=1e-4) + + def test_identical_samples(self): + """All identical samples should give max diversity.""" + B, N, D = 2, 4, 64 + vec = torch.randn(B, 1, D, device=DEVICE, dtype=torch.float32) + emb = vec.expand(B, N, D).contiguous() + + out = fused_diversity_penalty(emb) + # Should be ||vec||^2 for each (self-excluded mean of identical dot products) + expected = (vec.squeeze(1) ** 2).sum(dim=-1, keepdim=True).expand(B, N) + torch.testing.assert_close(out, expected, atol=1e-4, rtol=1e-4) + + def test_large(self): + """Test with realistic EBFT dimensions.""" + B, N, D = 1, 4, 4608 # multi-layer features + emb = torch.randn(B, N, D, device=DEVICE, dtype=torch.bfloat16) + + ref = self._reference(emb) + out = fused_diversity_penalty(emb) + + torch.testing.assert_close(out, ref, atol=5e-2, rtol=5e-2) + + def test_single_sample_returns_zeros(self): + """N=1 should return zeros (no pairs), not garbage from uninitialized memory.""" + B, D = 3, 128 + emb = torch.randn(B, 1, D, device=DEVICE, dtype=torch.float32) + out = fused_diversity_penalty(emb) + assert (out == 0).all(), "N=1 diversity should be exactly zero" diff --git a/tests/test_ebft_strided_structured.py b/tests/test_ebft_strided_structured.py new file mode 100644 index 0000000000..e4ad946a02 --- /dev/null +++ b/tests/test_ebft_strided_structured.py @@ -0,0 +1,363 @@ +"""Tests for the EBFT strided structured dataset transform and data loading.""" + +import pytest +from datasets import Dataset +from tokenizers import Tokenizer, models, pre_tokenizers +from transformers import PreTrainedTokenizerFast + +from axolotl.prompt_strategies.ebft import load as load_ebft +from axolotl.utils.dict import DictDefault + + +@pytest.fixture +def tokenizer(): + """Create a simple word-level tokenizer — no network access needed.""" + # Build a tiny vocab covering common test words + vocab = {"[PAD]": 0, "[UNK]": 1, "[BOS]": 2, "[EOS]": 3} + words = ( + "what is 2 + the answer 4 hello world goodbye bye hi short prompt " + "x write code print test some string metadata noise ok good python " + "sampling abc 123 def solve return this that" + ).split() + for w in words: + if w not in vocab: + vocab[w] = len(vocab) + + backend = Tokenizer(models.WordLevel(vocab=vocab, unk_token="[UNK]")) + backend.pre_tokenizer = pre_tokenizers.Whitespace() + + tok = PreTrainedTokenizerFast( + tokenizer_object=backend, + bos_token="[BOS]", + eos_token="[EOS]", + pad_token="[PAD]", + unk_token="[UNK]", + ) + return tok + + +@pytest.fixture +def cfg(): + return DictDefault({"sequence_len": 64}) + + +@pytest.fixture +def transform_fn_and_kwargs(cfg): + result = load_ebft("ebft_strided_structured.transform", cfg) + assert result is not None, "Failed to load ebft_strided_structured transform" + transform_fn, map_kwargs = result + return transform_fn, map_kwargs + + +class TestEBFTStridedStructuredTransform: + """Tests for the dataset transform function itself.""" + + def test_transform_loads(self, transform_fn_and_kwargs): + transform_fn, map_kwargs = transform_fn_and_kwargs + assert callable(transform_fn) + assert "remove_columns" in map_kwargs + + def test_remove_columns_sentinel(self, transform_fn_and_kwargs): + """Transform should signal removal of all original columns.""" + _, map_kwargs = transform_fn_and_kwargs + assert map_kwargs["remove_columns"] == "__all__" + + def test_prompt_completion_tokenization(self, transform_fn_and_kwargs, tokenizer): + """Prompt tokens get labels=-100, completion tokens get real labels.""" + transform_fn, _ = transform_fn_and_kwargs + example = {"input": "what is 2 + 2", "output": "the answer is 4"} + result = transform_fn(example, tokenizer=tokenizer) + + assert "input_ids" in result + assert "labels" in result + assert "attention_mask" in result + assert "prompt_length" in result + + prompt_length = result["prompt_length"] + labels = result["labels"] + seq_len = len(result["input_ids"]) + + assert seq_len == 64, "Should be padded to sequence_len" + assert len(labels) == seq_len + assert prompt_length > 0 + + # Prompt tokens should be masked + for i in range(prompt_length): + assert labels[i] == -100, f"Prompt token at {i} should be -100" + + # At least one completion token should have a real label + completion_labels = [lab for lab in labels[prompt_length:] if lab != -100] + assert len(completion_labels) > 0, "Should have non-masked completion tokens" + + def test_prompt_length_matches_boundary(self, transform_fn_and_kwargs, tokenizer): + """prompt_length should be the exact boundary between -100 and real labels.""" + transform_fn, _ = transform_fn_and_kwargs + example = {"input": "hello world", "output": "goodbye world"} + result = transform_fn(example, tokenizer=tokenizer) + + prompt_length = result["prompt_length"] + labels = result["labels"] + + assert labels[prompt_length - 1] == -100, "Last prompt token should be masked" + assert labels[prompt_length] != -100, ( + "First completion token should not be masked" + ) + + def test_padding_tokens_masked(self, transform_fn_and_kwargs, tokenizer): + """Padding tokens should have labels=-100 and attention_mask=0.""" + transform_fn, _ = transform_fn_and_kwargs + example = {"input": "hi", "output": "bye"} + result = transform_fn(example, tokenizer=tokenizer) + + attention_mask = result["attention_mask"] + labels = result["labels"] + + real_len = sum(attention_mask) + assert real_len < 64, "Short example should have padding" + + for i in range(real_len, 64): + assert attention_mask[i] == 0, ( + f"Pad position {i} should have attention_mask=0" + ) + assert labels[i] == -100, f"Pad position {i} should have labels=-100" + + def test_truncation_long_completion(self, transform_fn_and_kwargs, tokenizer): + """Long completions should be truncated to fit sequence_len.""" + transform_fn, _ = transform_fn_and_kwargs + example = { + "input": "short prompt", + "output": "x " * 500, + } + result = transform_fn(example, tokenizer=tokenizer) + assert len(result["input_ids"]) == 64 + + def test_alternative_field_names(self, transform_fn_and_kwargs, tokenizer): + """Transform should handle different field name conventions.""" + transform_fn, _ = transform_fn_and_kwargs + + result = transform_fn( + {"prompt": "what", "completion": "this"}, tokenizer=tokenizer + ) + assert result["prompt_length"] > 0 + + result = transform_fn( + {"question": "what", "answer": "this"}, tokenizer=tokenizer + ) + assert result["prompt_length"] > 0 + + def test_without_tokenizer_returns_prompt(self, transform_fn_and_kwargs): + """Without tokenizer, should return a prompt string.""" + transform_fn, _ = transform_fn_and_kwargs + result = transform_fn({"input": "hello", "output": "world"}) + assert "prompt" in result + assert result["prompt"] == "hello" + + +class TestEBFTColumnRemoval: + """Tests for the __all__ column removal logic in the RL data path.""" + + def _filter_remove_columns(self, map_kwargs, dataset): + """Reproduce the filtering logic from rl.py _load_split.""" + if "remove_columns" in map_kwargs: + ds_columns = dataset.column_names + if map_kwargs["remove_columns"] == "__all__": + map_kwargs["remove_columns"] = list(ds_columns) + else: + map_kwargs["remove_columns"] = [ + c for c in map_kwargs["remove_columns"] if c in ds_columns + ] + return map_kwargs + + def test_all_original_columns_removed(self, transform_fn_and_kwargs, tokenizer): + """After mapping, only tokenized columns should remain.""" + transform_fn, map_kwargs = transform_fn_and_kwargs + map_kwargs = dict(map_kwargs) # copy + + ds = Dataset.from_list( + [ + {"input": "what is 2 + 2", "output": "4", "extra_field": "noise"}, + ] + ) + + map_kwargs = self._filter_remove_columns(map_kwargs, ds) + assert "input" in map_kwargs["remove_columns"] + assert "output" in map_kwargs["remove_columns"] + assert "extra_field" in map_kwargs["remove_columns"] + + from functools import partial + + mapped = ds.map(partial(transform_fn, tokenizer=tokenizer), **map_kwargs) + assert "input_ids" in mapped.column_names + assert "labels" in mapped.column_names + assert "prompt_length" in mapped.column_names + assert "input" not in mapped.column_names + assert "output" not in mapped.column_names + assert "extra_field" not in mapped.column_names + + def test_extra_metadata_columns_removed(self, transform_fn_and_kwargs, tokenizer): + """Datasets with many extra metadata columns should all be cleaned up.""" + transform_fn, map_kwargs = transform_fn_and_kwargs + map_kwargs = dict(map_kwargs) + + ds = Dataset.from_list( + [ + { + "input": "write hello world", + "output": "print hello", + "id": "abc 123", + "domain": "python", + "generation_algorithm": "sampling", + "llm_judgement": "good", + "unit_tests": "test", + "tests_execution_status": "ok", + "average_test_score": 0.95, + }, + ] + ) + + map_kwargs = self._filter_remove_columns(map_kwargs, ds) + assert len(map_kwargs["remove_columns"]) == 9 + + from functools import partial + + mapped = ds.map(partial(transform_fn, tokenizer=tokenizer), **map_kwargs) + + expected_columns = {"input_ids", "attention_mask", "labels", "prompt_length"} + assert set(mapped.column_names) == expected_columns + + def test_no_string_columns_remain(self, transform_fn_and_kwargs, tokenizer): + """No string-typed columns should remain (would crash the DataLoader).""" + transform_fn, map_kwargs = transform_fn_and_kwargs + map_kwargs = dict(map_kwargs) + + ds = Dataset.from_list( + [ + {"input": "test", "output": "test", "notes": "some string metadata"}, + ] + ) + + map_kwargs = self._filter_remove_columns(map_kwargs, ds) + + from functools import partial + + mapped = ds.map(partial(transform_fn, tokenizer=tokenizer), **map_kwargs) + + for col in mapped.column_names: + feat = mapped.features[col] + assert str(feat) != "string", ( + f"Column '{col}' is still a string — would crash DataLoader" + ) + + def test_filter_preserves_explicit_list(self): + """When remove_columns is an explicit list, only existing columns are kept.""" + ds = Dataset.from_list([{"a": 1, "b": "text", "c": 3}]) + map_kwargs = {"remove_columns": ["a", "b", "missing_col"]} + + ds_columns = ds.column_names + map_kwargs["remove_columns"] = [ + c for c in map_kwargs["remove_columns"] if c in ds_columns + ] + + assert map_kwargs["remove_columns"] == ["a", "b"] + assert "missing_col" not in map_kwargs["remove_columns"] + + +class TestMultiTurnSeparators: + """Verify multi-turn transforms and trainer-side GT reconstruction.""" + + def test_multiturn_transform_splits_turns(self): + """Transform should store first turn as GT and remaining turns separately.""" + from axolotl.prompt_strategies.ebft import load as load_ebft + from axolotl.utils.dict import DictDefault + + cfg = DictDefault({"sequence_len": 512}) + fn, _ = load_ebft("ebft_chat_multiturn.transform", cfg) + out = fn( + { + "messages": [ + {"role": "user", "content": "Q1"}, + {"role": "assistant", "content": "A1"}, + {"role": "user", "content": "Q2"}, + {"role": "assistant", "content": "A2"}, + ] + } + ) + # ground_truth is only the first assistant turn + assert out["ground_truth"] == "A1" + # remaining_turns carries the rest for trainer-side reconstruction + assert out["remaining_turns"] == [ + {"role": "user", "content": "Q2"}, + {"role": "assistant", "content": "A2"}, + ] + + def test_multiturn_gt_reconstruction_via_chat_template(self): + """Trainer-side GT reconstruction should insert role markers between turns. + + This tests the logic from trainer.py:284-299 that reconstructs multi-turn + GT using apply_chat_template, ensuring assistant turns are separated by + role markers rather than concatenated as raw text. + """ + from transformers import AutoTokenizer + + tokenizer = AutoTokenizer.from_pretrained( + "Qwen/Qwen2-0.5B-Instruct", trust_remote_code=True + ) + + # Simulate the transform output + prompt_msgs = [{"role": "user", "content": "Q1"}] + gt = "A1" + remaining_turns = [ + {"role": "user", "content": "Q2"}, + {"role": "assistant", "content": "A2"}, + ] + + # --- Reproduce the trainer-side reconstruction (trainer.py:284-299) --- + prompt_text = tokenizer.apply_chat_template( + prompt_msgs, tokenize=False, add_generation_prompt=True + ) + gt_conv = list(prompt_msgs) + [{"role": "assistant", "content": gt}] + gt_conv.extend(remaining_turns) + full_gt_text = tokenizer.apply_chat_template( + gt_conv, tokenize=False, add_generation_prompt=False + ) + + # The full GT text should contain both assistant turns with role markers + assert "A1" in full_gt_text + assert "A2" in full_gt_text + # Raw concatenation "A1A2" should NOT appear — role markers separate them + assert "A1A2" not in full_gt_text, ( + "GT reconstruction should have role markers between turns, not raw concatenation" + ) + # The user turn Q2 should appear between A1 and A2 + a1_pos = full_gt_text.index("A1") + a2_pos = full_gt_text.index("A2") + q2_pos = full_gt_text.index("Q2") + assert a1_pos < q2_pos < a2_pos, ( + "Turn order should be A1 -> Q2 -> A2 in rendered GT" + ) + # The GT should start with the prompt + assert full_gt_text.startswith(prompt_text), ( + "Full GT should start with the rendered prompt" + ) + + def test_multiturn_gt_reconstruction_fallback_single_turn(self): + """Single-turn prompts in a multi-turn dataset should use raw concatenation.""" + from transformers import AutoTokenizer + + tokenizer = AutoTokenizer.from_pretrained( + "Qwen/Qwen2-0.5B-Instruct", trust_remote_code=True + ) + + prompt_msgs = [{"role": "user", "content": "Q1"}] + gt = "A1" + # remaining_turns would be [] for single-turn prompts + + prompt_text = tokenizer.apply_chat_template( + prompt_msgs, tokenize=False, add_generation_prompt=True + ) + + # With empty remaining_turns, trainer falls through to raw concat + # (trainer.py:302: gt_texts.append(prompt_text + gt)) + gt_text = prompt_text + gt + assert gt_text.endswith("A1") + assert prompt_text in gt_text diff --git a/tests/test_http_weight_sync.py b/tests/test_http_weight_sync.py new file mode 100644 index 0000000000..97c5ece71a --- /dev/null +++ b/tests/test_http_weight_sync.py @@ -0,0 +1,158 @@ +"""Tests for HTTP weight sync serialization round-trip (bf16/fp16/fp32). + +Exercises the encode/decode helpers in axolotl.utils.weight_serde that handle +the three-stage weight transfer: trainer → serve endpoint → vLLM worker. +""" + +import pytest +import torch + +from axolotl.utils.weight_serde import ( + decode_from_http, + decode_from_ipc, + encode_for_http, + encode_for_ipc, +) + +# --------------------------------------------------------------------------- +# Stage 1: trainer → serve endpoint (HTTP with base64) +# --------------------------------------------------------------------------- + + +class TestHttpEncodeRoundTrip: + """Test encode_for_http / decode_from_http.""" + + @pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16]) + def test_round_trip_dtype(self, dtype): + original = torch.randn(32, 64, dtype=dtype) + entry = encode_for_http("layer.weight", original) + name, decoded = decode_from_http(entry) + + assert name == "layer.weight" + assert decoded.dtype == dtype + assert decoded.shape == original.shape + if dtype == torch.bfloat16: + # bf16→fp16→bf16 loses some precision + torch.testing.assert_close(decoded, original, atol=1e-2, rtol=1e-2) + else: + torch.testing.assert_close(decoded, original, atol=0, rtol=0) + + def test_bfloat16_wire_format_is_fp16(self): + """bf16 tensors should be sent as fp16 on the wire.""" + import base64 + + original = torch.randn(8, 16, dtype=torch.bfloat16) + entry = encode_for_http("w", original) + raw = base64.b64decode(entry["data"]) + # 8*16 elements * 2 bytes/elem (fp16) = 256 bytes + assert len(raw) == 8 * 16 * 2 + # dtype field should preserve original dtype for reconstruction + assert entry["dtype"] == "torch.bfloat16" + + def test_multidimensional_shapes(self): + for shape in [(128,), (4, 32), (2, 3, 16), (2, 2, 2, 8)]: + original = torch.randn(*shape, dtype=torch.bfloat16) + entry = encode_for_http("w", original) + _, decoded = decode_from_http(entry) + assert decoded.shape == original.shape + assert decoded.dtype == torch.bfloat16 + + +# --------------------------------------------------------------------------- +# Stage 2: serve endpoint → vLLM worker (IPC with raw bytes) +# --------------------------------------------------------------------------- + + +class TestIpcEncodeRoundTrip: + """Test encode_for_ipc / decode_from_ipc.""" + + @pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16]) + def test_round_trip_dtype(self, dtype): + original = torch.randn(32, 64, dtype=dtype) + entry = encode_for_ipc("layer.weight", original) + name, decoded = decode_from_ipc(entry) + + assert name == "layer.weight" + assert decoded.dtype == dtype + assert decoded.shape == original.shape + if dtype == torch.bfloat16: + torch.testing.assert_close(decoded, original, atol=1e-2, rtol=1e-2) + else: + torch.testing.assert_close(decoded, original, atol=0, rtol=0) + + def test_bfloat16_ipc_wire_is_fp16(self): + """bf16 tensors should be serialized as fp16 bytes in IPC.""" + original = torch.randn(4, 8, dtype=torch.bfloat16) + entry = encode_for_ipc("w", original) + assert entry["dtype"] == "float16" + assert entry["target_dtype"] == "bfloat16" + assert len(entry["data"]) == 4 * 8 * 2 # fp16 bytes + + def test_fp32_has_no_target_dtype_mismatch(self): + original = torch.randn(4, 8, dtype=torch.float32) + entry = encode_for_ipc("w", original) + assert entry["dtype"] == "float32" + assert entry["target_dtype"] == "float32" + + def test_worker_handles_missing_target_dtype(self): + """Backward compat: older serve code may not send target_dtype.""" + entry = { + "name": "w", + "data": torch.randn(4, 8, dtype=torch.float32).numpy().tobytes(), + "dtype": "float32", + "shape": [4, 8], + # no target_dtype key + } + name, decoded = decode_from_ipc(entry) + assert decoded.dtype == torch.float32 + assert decoded.shape == (4, 8) + + +# --------------------------------------------------------------------------- +# Full pipeline: trainer → serve → worker +# --------------------------------------------------------------------------- + + +class TestFullPipelineRoundTrip: + """End-to-end: trainer → serve → worker.""" + + @pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16]) + def test_three_stage_round_trip(self, dtype): + """Tensor survives trainer→serve→worker with correct dtype and values.""" + original = torch.randn(16, 32, dtype=dtype) + + # Stage 1: trainer encodes for HTTP + http_entry = encode_for_http("model.layers.0.weight", original) + + # Stage 2: serve decodes HTTP, re-encodes for IPC + name, at_serve = decode_from_http(http_entry) + ipc_entry = encode_for_ipc(name, at_serve) + + # Stage 3: worker decodes IPC + _, at_worker = decode_from_ipc(ipc_entry) + + assert at_worker.dtype == dtype + assert at_worker.shape == original.shape + if dtype == torch.bfloat16: + # Two bf16→fp16→bf16 hops compound precision loss slightly + torch.testing.assert_close(at_worker, original, atol=2e-2, rtol=2e-2) + else: + torch.testing.assert_close(at_worker, original, atol=0, rtol=0) + + def test_bfloat16_precision_loss_is_bounded(self): + """bf16→fp16→bf16 round-trip error should be small.""" + original = torch.randn(256, 256, dtype=torch.bfloat16) + http_entry = encode_for_http("w", original) + _, at_serve = decode_from_http(http_entry) + ipc_entry = encode_for_ipc("w", at_serve) + _, at_worker = decode_from_ipc(ipc_entry) + + max_err = (at_worker.float() - original.float()).abs().max().item() + # bf16 has ~8e-3 precision, fp16 has ~1e-3; round-trip error bounded + assert max_err < 0.05, f"Max error {max_err} exceeds bound" + + def test_bfloat16_numpy_would_crash_without_fix(self): + """Verify that calling .numpy() on bf16 raises, confirming the fix is needed.""" + t = torch.randn(4, 4, dtype=torch.bfloat16) + with pytest.raises((RuntimeError, TypeError)): + t.numpy() From 1f1ebb8237a87dd5f3b8ca544e0ec5e970bc9afb Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 25 Mar 2026 16:06:37 +0700 Subject: [PATCH 1225/1405] feat: move to uv first --- .github/workflows/lint.yml | 2 +- .github/workflows/multi-gpu-e2e.yml | 3 +- .github/workflows/tests-nightly.yml | 17 +- .github/workflows/tests.yml | 4 +- MANIFEST.in | 4 +- pyproject.toml | 176 +++++++++++++- requirements.txt | 78 ------ setup.py | 230 ------------------ ...setuptools_axolotl_dynamic_dependencies.py | 102 -------- 9 files changed, 182 insertions(+), 434 deletions(-) delete mode 100644 requirements.txt delete mode 100644 setup.py delete mode 100644 src/setuptools_axolotl_dynamic_dependencies.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 181fd9dc97..e89e276423 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,7 @@ on: types: [opened, synchronize, reopened, ready_for_review] paths: - '**.py' - - 'requirements.txt' + - 'pyproject.toml' - '.github/workflows/*.yml' - "*.[q]md" - "examples/**/*.y[a]?ml" diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 2bb499ded0..d2d3123e34 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -4,8 +4,7 @@ on: pull_request: paths: - 'tests/e2e/multigpu/**.py' - - 'requirements.txt' - - 'setup.py' + - 'pyproject.toml' - 'pyproject.toml' - '.github/workflows/multi-gpu-e2e.yml' - 'scripts/cutcrossentropy_install.py' diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 663b0476e1..8ccebfdc1d 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -72,14 +72,6 @@ jobs: run: | pip3 install torch==${{ matrix.pytorch_version }} torchvision - - name: Update requirements.txt - run: | - sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt - sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt - sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt - sed -i 's#^trl.*#trl @ git+https://github.com/huggingface/trl.git@main#' requirements.txt - sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt - - name: Install dependencies run: | pip3 show torch @@ -88,6 +80,15 @@ jobs: python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt + - name: Override with nightly HF packages + run: | + pip3 install --no-deps \ + "transformers @ git+https://github.com/huggingface/transformers.git@main" \ + "peft @ git+https://github.com/huggingface/peft.git@main" \ + "accelerate @ git+https://github.com/huggingface/accelerate.git@main" \ + "trl @ git+https://github.com/huggingface/trl.git@main" \ + "datasets @ git+https://github.com/huggingface/datasets.git@main" + - name: Make sure PyTorch version wasn't clobbered run: | python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5099e447cf..ad9f551a0c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ on: - "main" paths: - '**.py' - - 'requirements.txt' + - 'pyproject.toml' - '.github/workflows/*.yml' - 'requirements-tests.txt' - 'cicd/cicd.sh' @@ -16,7 +16,7 @@ on: types: [opened, synchronize, reopened, ready_for_review] paths: - '**.py' - - 'requirements.txt' + - 'pyproject.toml' - '.github/workflows/*.yml' - 'requirements-tests.txt' - 'cicd/cicd.sh' diff --git a/MANIFEST.in b/MANIFEST.in index 3fbb0edca4..729f7e7a34 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,4 @@ -include requirements.txt include README.md include LICENSE -include src/setuptools_axolotl_dynamic_dependencies.py +include VERSION include src/axolotl/utils/chat_templates/templates/*.jinja -recursive-include axolotl *.py diff --git a/pyproject.toml b/pyproject.toml index 9cee4a5204..a4e9f32a63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,143 @@ [build-system] -requires = ["setuptools>=64", "wheel", "setuptools_scm>=8", "packaging==26.0"] +requires = ["setuptools>=64", "wheel", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" [project] name = "axolotl" -dynamic = ["version", "dependencies", "optional-dependencies"] +dynamic = ["version"] description = "LLM Trainer" readme = "README.md" requires-python = ">=3.10" # license = "Apache-2.0" +dependencies = [ + # Core ML stack + "torch>=2.6.0", + "packaging==26.0", + "huggingface_hub>=1.1.7", + "peft>=0.18.1", + "tokenizers>=0.22.1", + "transformers==5.3.0", + "accelerate==1.13.0", + "datasets==4.5.0", + "trl==0.29.0", + "hf_xet==1.3.2", + "kernels==0.12.2", + "trackio>=0.16.1", + "typing-extensions>=4.15.0", + "optimum==1.16.2", + "hf_transfer", + "sentencepiece", + "gradio>=6.2.0,<7.0", + "modal==1.3.0.post1", + "pydantic>=2.10.6", + "addict", + "fire", + "PyYAML>=6.0", + "requests", + "wandb", + "einops", + "colorama", + "numba>=0.61.2", + "numpy>=2.2.6", + + # Evaluation & metrics + "evaluate==0.4.1", + "scipy", + "nvidia-ml-py==12.560.30", + "art", + "tensorboard", + "python-dotenv==1.0.1", + + # Remote filesystems + "s3fs>=2024.5.0", + "gcsfs>=2025.3.0", + "adlfs>=2024.5.0", + "ocifs==1.3.2", + + "zstandard==0.22.0", + "fastcore", + + # lm eval harness + "lm_eval==0.4.7", + "langdetect==1.0.9", + "immutabledict==4.2.0", + "antlr4-python3-runtime==4.13.2", + + "schedulefree==1.4.1", + "openenv-core==0.1.0", + + # Axolotl contribs + "axolotl-contribs-lgpl==0.0.7", + "axolotl-contribs-mit==0.0.6", + + # Telemetry + "posthog==6.7.11", + + "mistral-common==1.10.0", + + # Platform-specific (Linux only) + "bitsandbytes==0.49.1 ; sys_platform != 'darwin'", + "triton>=3.4.0 ; sys_platform != 'darwin'", + "xformers>=0.0.23.post1 ; sys_platform != 'darwin'", + "liger-kernel==0.7.0 ; sys_platform != 'darwin'", + "torchao==0.16.0 ; sys_platform != 'darwin' and platform_machine != 'aarch64'", + + # Architecture-specific + "fla-core==0.4.1 ; platform_machine != 'aarch64'", + "flash-linear-attention==0.4.1 ; platform_machine != 'aarch64'", +] + +[project.optional-dependencies] +flash-attn = ["flash-attn==2.8.3"] +ring-flash-attn = [ + "flash-attn==2.8.3", + "ring-flash-attn>=0.1.7", +] +deepspeed = [ + "deepspeed>=0.18.6,<0.19.0", + "deepspeed-kernels", +] +mamba-ssm = [ + "mamba-ssm==1.2.0.post1", + "causal_conv1d", +] +auto-gptq = [ + "auto-gptq==0.5.1", +] +mlflow = [ + "mlflow", +] +galore = [ + "galore_torch", +] +apollo = [ + "apollo-torch", +] +optimizers = [ + "galore_torch", + "apollo-torch", + "lomo-optim==0.1.1", + "torch-optimi==0.2.1", + "came_pytorch==0.1.3", +] +ray = [ + "ray[train]>=2.52.1", +] +vllm = [ + "vllm>=0.10.0", +] +llmcompressor = [ + "llmcompressor>=0.10.0", +] +fbgemm-gpu = ["fbgemm-gpu-genai>=1.3.0"] +opentelemetry = [ + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-exporter-prometheus", + "prometheus-client", +] + [project.scripts] axolotl = "axolotl.cli.main:main" @@ -18,18 +146,15 @@ Homepage = "https://axolotl.ai/" Documentation = "https://docs.axolotl.ai/" Repository = "https://github.com/axolotl-ai-cloud/axolotl.git" -[tool.setuptools_scm] - [tool.setuptools] -py-modules = ["setuptools_axolotl_dynamic_dependencies"] include-package-data = true +[tool.setuptools.packages.find] +where = ["src"] + [tool.setuptools.dynamic] version = { file = "VERSION" } -[tool.setuptools.cmdclass] -build_py = "setuptools_axolotl_dynamic_dependencies.BuildPyCommand" - [tool.ruff] line-length = 88 target-version = "py310" @@ -67,5 +192,40 @@ markers = [ "slow: marks tests as slow", ] +# UV specific configuration +[tool.uv] +prerelease = "allow" +conflicts = [ + [ + { package = "axolotl" }, + { extra = "vllm" }, + ], + [ + { package = "axolotl" }, + { extra = "flash-attn" }, + ], + [ + { package = "axolotl" }, + { extra = "ring-flash-attn" }, + ], + [ + { package = "axolotl" }, + { extra = "mamba-ssm" }, + ], + [ + { package = "axolotl" }, + { extra = "auto-gptq" }, + ], + [ + { package = "axolotl" }, + { extra = "fbgemm-gpu" }, + ], +] + [tool.uv.extra-build-dependencies] axolotl = ["huggingface_hub"] +mamba-ssm = ["torch"] +causal-conv1d = ["torch"] +flash-attn = ["torch"] +deepspeed = ["torch"] +auto-gptq = ["torch"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3fd75c3fac..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,78 +0,0 @@ ---extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ - -# START section of dependencies that don't install on Darwin/MacOS -bitsandbytes==0.49.1 -triton>=3.4.0 -mamba-ssm==1.2.0.post1 -xformers>=0.0.23.post1 -liger-kernel==0.7.0 -# END section - -packaging==26.0 -huggingface_hub>=1.1.7 -peft>=0.18.1 -tokenizers>=0.22.1 -transformers==5.3.0 -accelerate==1.13.0 -datasets==4.5.0 -deepspeed>=0.18.6,<0.19.0 -trl==0.29.0 -hf_xet==1.3.2 -kernels==0.12.2 - -fla-core==0.4.1 -flash-linear-attention==0.4.1 - -trackio>=0.16.1 -typing-extensions>=4.15.0 - -optimum==1.16.2 -hf_transfer -sentencepiece -gradio>=6.2.0,<7.0 - -modal==1.3.0.post1 -pydantic>=2.10.6 -addict -fire -PyYAML>=6.0 -requests -wandb -einops -colorama -numba>=0.61.2 -numpy>=2.2.6 - -# qlora things -evaluate==0.4.1 -scipy -nvidia-ml-py==12.560.30 -art -tensorboard -python-dotenv==1.0.1 - -# remote filesystems -s3fs>=2024.5.0 -gcsfs>=2025.3.0 -adlfs>=2024.5.0 -ocifs==1.3.2 - -zstandard==0.22.0 -fastcore - -# lm eval harness -lm_eval==0.4.7 -langdetect==1.0.9 -immutabledict==4.2.0 -antlr4-python3-runtime==4.13.2 - -torchao==0.16.0 -openenv-core==0.1.0 -schedulefree==1.4.1 - -axolotl-contribs-lgpl==0.0.7 -axolotl-contribs-mit==0.0.6 -# telemetry -posthog==6.7.11 - -mistral-common==1.10.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index d4e1894f79..0000000000 --- a/setup.py +++ /dev/null @@ -1,230 +0,0 @@ -"""setup.py for axolotl""" - -import os -import platform -import re -from importlib.metadata import PackageNotFoundError, version -from pathlib import Path - -from setuptools import find_packages, setup - - -def parse_requirements(extras_require_map): - _install_requires = [] - _dependency_links = [] - with open("./requirements.txt", encoding="utf-8") as requirements_file: - lines = [r.strip() for r in requirements_file.readlines()] - for line in lines: - is_extras = "deepspeed" in line or "mamba-ssm" in line - if line.startswith("--extra-index-url"): - # Handle custom index URLs - _, url = line.split() - _dependency_links.append(url) - elif not is_extras and line and line[0] != "#": - # Handle standard packages - _install_requires.append(line) - try: - xformers_version = [req for req in _install_requires if "xformers" in req][0] - install_xformers = platform.machine() != "aarch64" - if platform.machine() == "aarch64": - # skip on ARM64 - skip_packages = [ - "torchao", - "fla-core", - "flash-linear-attention", - ] - _install_requires = [ - req - for req in _install_requires - if re.split(r"[>=<]", req)[0].strip() not in skip_packages - ] - if "Darwin" in platform.system(): - # skip packages not compatible with OSX - skip_packages = [ - "bitsandbytes", - "triton", - "mamba-ssm", - "xformers", - "liger-kernel", - ] - _install_requires = [ - req - for req in _install_requires - if re.split(r"[>=<]", req)[0].strip() not in skip_packages - ] - print( - _install_requires, [req in skip_packages for req in _install_requires] - ) - else: - # detect the version of torch already installed - # and set it so dependencies don't clobber the torch version - try: - torch_version = version("torch") - except PackageNotFoundError: - torch_version = "2.8.0" # default to torch 2.8.0 - _install_requires.append(f"torch=={torch_version}") - - version_match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", torch_version) - if version_match: - major, minor, patch = version_match.groups() - major, minor = int(major), int(minor) - patch = ( - int(patch) if patch is not None else 0 - ) # Default patch to 0 if not present - else: - raise ValueError("Invalid version format") - - torch_parts = torch_version.split("+") - if len(torch_parts) == 2: - torch_cuda_version = torch_parts[1] - _dependency_links.append( - f"https://download.pytorch.org/whl/{torch_cuda_version}" - ) - - if (major, minor) >= (2, 10): - extras_require_map.pop("fbgemm-gpu") - extras_require_map["fbgemm-gpu"] = [ - "fbgemm-gpu==1.5.0", - "fbgemm-gpu-genai==1.5.0", - ] - if not install_xformers: - _install_requires.pop(_install_requires.index(xformers_version)) - extras_require_map["vllm"] = ["vllm>=0.17.1"] - elif (major, minor) >= (2, 9): - extras_require_map.pop("fbgemm-gpu") - extras_require_map["fbgemm-gpu"] = [ - "fbgemm-gpu==1.4.0", - "fbgemm-gpu-genai==1.4.2", - ] - if not install_xformers: - _install_requires.pop(_install_requires.index(xformers_version)) - if patch == 0: - extras_require_map["vllm"] = ["vllm==0.13.0"] - else: - extras_require_map["vllm"] = ["vllm==0.14.0"] - elif (major, minor) >= (2, 8): - extras_require_map.pop("fbgemm-gpu") - extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.3.0"] - extras_require_map["vllm"] = ["vllm==0.11.0"] - if not install_xformers: - _install_requires.pop(_install_requires.index(xformers_version)) - elif (major, minor) >= (2, 7): - _install_requires.pop(_install_requires.index(xformers_version)) - if patch == 0: - if install_xformers: - _install_requires.append("xformers==0.0.30") - # vllm 0.9.x is incompatible with latest transformers - extras_require_map.pop("vllm") - else: - if install_xformers: - _install_requires.append("xformers==0.0.31") - extras_require_map["vllm"] = ["vllm==0.10.1"] - elif (major, minor) >= (2, 6): - _install_requires.pop(_install_requires.index(xformers_version)) - if install_xformers: - _install_requires.append("xformers==0.0.29.post3") - # since we only support 2.6.0+cu126 - _dependency_links.append("https://download.pytorch.org/whl/cu126") - extras_require_map.pop("vllm") - elif (major, minor) >= (2, 5): - _install_requires.pop(_install_requires.index(xformers_version)) - if install_xformers: - if patch == 0: - _install_requires.append("xformers==0.0.28.post2") - else: - _install_requires.append("xformers>=0.0.28.post3") - extras_require_map.pop("vllm") - elif (major, minor) >= (2, 4): - extras_require_map.pop("vllm") - if install_xformers: - if patch == 0: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.27") - else: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers==0.0.28.post1") - else: - raise ValueError("axolotl requires torch>=2.4") - - except PackageNotFoundError: - pass - return _install_requires, _dependency_links, extras_require_map - - -def get_package_version(): - with open( - Path(os.path.dirname(os.path.abspath(__file__))) / "VERSION", - "r", - encoding="utf-8", - ) as fin: - version_ = fin.read().strip() - return version_ - - -extras_require = { - "flash-attn": ["flash-attn==2.8.3"], - "ring-flash-attn": [ - "flash-attn==2.8.3", - "ring-flash-attn>=0.1.7", - ], - "deepspeed": [ - "deepspeed==0.18.2", - "deepspeed-kernels", - ], - "mamba-ssm": [ - "mamba-ssm==1.2.0.post1", - "causal_conv1d", - ], - "auto-gptq": [ - "auto-gptq==0.5.1", - ], - "mlflow": [ - "mlflow", - ], - "galore": [ - "galore_torch", - ], - "apollo": [ - "apollo-torch", - ], - "optimizers": [ - "galore_torch", - "apollo-torch", - "lomo-optim==0.1.1", - "torch-optimi==0.2.1", - "came_pytorch==0.1.3", - ], - "ray": [ - "ray[train]>=2.52.1", - ], - "vllm": [ - "vllm==0.10.0", - ], - "llmcompressor": [ - "llmcompressor==0.5.1", - ], - "fbgemm-gpu": ["fbgemm-gpu-genai==1.3.0"], - "opentelemetry": [ - "opentelemetry-api", - "opentelemetry-sdk", - "opentelemetry-exporter-prometheus", - "prometheus-client", - ], -} -install_requires, dependency_links, extras_require_build = parse_requirements( - extras_require -) - -setup( - version=get_package_version(), - package_dir={"": "src"}, - packages=find_packages("src"), - install_requires=install_requires, - dependency_links=dependency_links, - entry_points={ - "console_scripts": [ - "axolotl=axolotl.cli.main:main", - ], - }, - extras_require=extras_require_build, -) diff --git a/src/setuptools_axolotl_dynamic_dependencies.py b/src/setuptools_axolotl_dynamic_dependencies.py deleted file mode 100644 index 3bb54cda8e..0000000000 --- a/src/setuptools_axolotl_dynamic_dependencies.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -dynamic requirements for axolotl -""" - -import platform -import re -from importlib.metadata import PackageNotFoundError, version - -from setuptools.command.build_py import build_py as _build_py - - -def parse_requirements(): - _install_requires = [] - _dependency_links = [] - with open("./requirements.txt", encoding="utf-8") as requirements_file: - lines = [r.strip() for r in requirements_file.readlines()] - for line in lines: - is_extras = ( - "flash-attn" in line - or "flash-attention" in line - or "deepspeed" in line - or "mamba-ssm" in line - or "lion-pytorch" in line - ) - if line.startswith("--extra-index-url"): - # Handle custom index URLs - _, url = line.split() - _dependency_links.append(url) - elif not is_extras and line and line[0] != "#": - # Handle standard packages - _install_requires.append(line) - - try: - xformers_version = [req for req in _install_requires if "xformers" in req][0] - torchao_version = [req for req in _install_requires if "torchao" in req][0] - - if "Darwin" in platform.system(): - # don't install xformers on MacOS - _install_requires.pop(_install_requires.index(xformers_version)) - else: - # detect the version of torch already installed - # and set it so dependencies don't clobber the torch version - try: - torch_version = version("torch") - except PackageNotFoundError: - torch_version = "2.5.1" - _install_requires.append(f"torch=={torch_version}") - - version_match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", torch_version) - if version_match: - major, minor, patch = version_match.groups() - major, minor = int(major), int(minor) - patch = ( - int(patch) if patch is not None else 0 - ) # Default patch to 0 if not present - else: - raise ValueError("Invalid version format") - - if (major, minor) >= (2, 5): - _install_requires.pop(_install_requires.index(xformers_version)) - if patch == 0: - _install_requires.append("xformers==0.0.28.post2") - else: - _install_requires.append("xformers==0.0.28.post3") - elif (major, minor) >= (2, 4): - if patch == 0: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.27") - else: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers==0.0.28.post1") - elif (major, minor) >= (2, 3): - _install_requires.pop(_install_requires.index(torchao_version)) - if patch == 0: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.26.post1") - else: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.27") - elif (major, minor) >= (2, 2): - _install_requires.pop(_install_requires.index(torchao_version)) - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.25.post1") - else: - _install_requires.pop(_install_requires.index(torchao_version)) - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.23.post1") - - except PackageNotFoundError: - pass - return _install_requires, _dependency_links - - -class BuildPyCommand(_build_py): - """ - custom build_py command to parse dynamic requirements - """ - - def finalize_options(self): - super().finalize_options() - install_requires, _ = parse_requirements() - self.distribution.install_requires = install_requires From 2fb72798e08dc50a821c932b95af47dcdca144ca Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 25 Mar 2026 16:12:36 +0700 Subject: [PATCH 1226/1405] Revert "feat: move to uv first" (#3544) This reverts commit 1f1ebb8237a87dd5f3b8ca544e0ec5e970bc9afb. --- .github/workflows/lint.yml | 2 +- .github/workflows/multi-gpu-e2e.yml | 3 +- .github/workflows/tests-nightly.yml | 17 +- .github/workflows/tests.yml | 4 +- MANIFEST.in | 4 +- pyproject.toml | 176 +------------- requirements.txt | 78 ++++++ setup.py | 230 ++++++++++++++++++ ...setuptools_axolotl_dynamic_dependencies.py | 102 ++++++++ 9 files changed, 434 insertions(+), 182 deletions(-) create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 src/setuptools_axolotl_dynamic_dependencies.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e89e276423..181fd9dc97 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,7 @@ on: types: [opened, synchronize, reopened, ready_for_review] paths: - '**.py' - - 'pyproject.toml' + - 'requirements.txt' - '.github/workflows/*.yml' - "*.[q]md" - "examples/**/*.y[a]?ml" diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index d2d3123e34..2bb499ded0 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -4,7 +4,8 @@ on: pull_request: paths: - 'tests/e2e/multigpu/**.py' - - 'pyproject.toml' + - 'requirements.txt' + - 'setup.py' - 'pyproject.toml' - '.github/workflows/multi-gpu-e2e.yml' - 'scripts/cutcrossentropy_install.py' diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 8ccebfdc1d..663b0476e1 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -72,6 +72,14 @@ jobs: run: | pip3 install torch==${{ matrix.pytorch_version }} torchvision + - name: Update requirements.txt + run: | + sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt + sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt + sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt + sed -i 's#^trl.*#trl @ git+https://github.com/huggingface/trl.git@main#' requirements.txt + sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt + - name: Install dependencies run: | pip3 show torch @@ -80,15 +88,6 @@ jobs: python scripts/cutcrossentropy_install.py | sh pip3 install -r requirements-dev.txt -r requirements-tests.txt - - name: Override with nightly HF packages - run: | - pip3 install --no-deps \ - "transformers @ git+https://github.com/huggingface/transformers.git@main" \ - "peft @ git+https://github.com/huggingface/peft.git@main" \ - "accelerate @ git+https://github.com/huggingface/accelerate.git@main" \ - "trl @ git+https://github.com/huggingface/trl.git@main" \ - "datasets @ git+https://github.com/huggingface/datasets.git@main" - - name: Make sure PyTorch version wasn't clobbered run: | python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ad9f551a0c..5099e447cf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ on: - "main" paths: - '**.py' - - 'pyproject.toml' + - 'requirements.txt' - '.github/workflows/*.yml' - 'requirements-tests.txt' - 'cicd/cicd.sh' @@ -16,7 +16,7 @@ on: types: [opened, synchronize, reopened, ready_for_review] paths: - '**.py' - - 'pyproject.toml' + - 'requirements.txt' - '.github/workflows/*.yml' - 'requirements-tests.txt' - 'cicd/cicd.sh' diff --git a/MANIFEST.in b/MANIFEST.in index 729f7e7a34..3fbb0edca4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,6 @@ +include requirements.txt include README.md include LICENSE -include VERSION +include src/setuptools_axolotl_dynamic_dependencies.py include src/axolotl/utils/chat_templates/templates/*.jinja +recursive-include axolotl *.py diff --git a/pyproject.toml b/pyproject.toml index a4e9f32a63..9cee4a5204 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,143 +1,15 @@ [build-system] -requires = ["setuptools>=64", "wheel", "setuptools_scm>=8"] +requires = ["setuptools>=64", "wheel", "setuptools_scm>=8", "packaging==26.0"] build-backend = "setuptools.build_meta" [project] name = "axolotl" -dynamic = ["version"] +dynamic = ["version", "dependencies", "optional-dependencies"] description = "LLM Trainer" readme = "README.md" requires-python = ">=3.10" # license = "Apache-2.0" -dependencies = [ - # Core ML stack - "torch>=2.6.0", - "packaging==26.0", - "huggingface_hub>=1.1.7", - "peft>=0.18.1", - "tokenizers>=0.22.1", - "transformers==5.3.0", - "accelerate==1.13.0", - "datasets==4.5.0", - "trl==0.29.0", - "hf_xet==1.3.2", - "kernels==0.12.2", - "trackio>=0.16.1", - "typing-extensions>=4.15.0", - "optimum==1.16.2", - "hf_transfer", - "sentencepiece", - "gradio>=6.2.0,<7.0", - "modal==1.3.0.post1", - "pydantic>=2.10.6", - "addict", - "fire", - "PyYAML>=6.0", - "requests", - "wandb", - "einops", - "colorama", - "numba>=0.61.2", - "numpy>=2.2.6", - - # Evaluation & metrics - "evaluate==0.4.1", - "scipy", - "nvidia-ml-py==12.560.30", - "art", - "tensorboard", - "python-dotenv==1.0.1", - - # Remote filesystems - "s3fs>=2024.5.0", - "gcsfs>=2025.3.0", - "adlfs>=2024.5.0", - "ocifs==1.3.2", - - "zstandard==0.22.0", - "fastcore", - - # lm eval harness - "lm_eval==0.4.7", - "langdetect==1.0.9", - "immutabledict==4.2.0", - "antlr4-python3-runtime==4.13.2", - - "schedulefree==1.4.1", - "openenv-core==0.1.0", - - # Axolotl contribs - "axolotl-contribs-lgpl==0.0.7", - "axolotl-contribs-mit==0.0.6", - - # Telemetry - "posthog==6.7.11", - - "mistral-common==1.10.0", - - # Platform-specific (Linux only) - "bitsandbytes==0.49.1 ; sys_platform != 'darwin'", - "triton>=3.4.0 ; sys_platform != 'darwin'", - "xformers>=0.0.23.post1 ; sys_platform != 'darwin'", - "liger-kernel==0.7.0 ; sys_platform != 'darwin'", - "torchao==0.16.0 ; sys_platform != 'darwin' and platform_machine != 'aarch64'", - - # Architecture-specific - "fla-core==0.4.1 ; platform_machine != 'aarch64'", - "flash-linear-attention==0.4.1 ; platform_machine != 'aarch64'", -] - -[project.optional-dependencies] -flash-attn = ["flash-attn==2.8.3"] -ring-flash-attn = [ - "flash-attn==2.8.3", - "ring-flash-attn>=0.1.7", -] -deepspeed = [ - "deepspeed>=0.18.6,<0.19.0", - "deepspeed-kernels", -] -mamba-ssm = [ - "mamba-ssm==1.2.0.post1", - "causal_conv1d", -] -auto-gptq = [ - "auto-gptq==0.5.1", -] -mlflow = [ - "mlflow", -] -galore = [ - "galore_torch", -] -apollo = [ - "apollo-torch", -] -optimizers = [ - "galore_torch", - "apollo-torch", - "lomo-optim==0.1.1", - "torch-optimi==0.2.1", - "came_pytorch==0.1.3", -] -ray = [ - "ray[train]>=2.52.1", -] -vllm = [ - "vllm>=0.10.0", -] -llmcompressor = [ - "llmcompressor>=0.10.0", -] -fbgemm-gpu = ["fbgemm-gpu-genai>=1.3.0"] -opentelemetry = [ - "opentelemetry-api", - "opentelemetry-sdk", - "opentelemetry-exporter-prometheus", - "prometheus-client", -] - [project.scripts] axolotl = "axolotl.cli.main:main" @@ -146,15 +18,18 @@ Homepage = "https://axolotl.ai/" Documentation = "https://docs.axolotl.ai/" Repository = "https://github.com/axolotl-ai-cloud/axolotl.git" +[tool.setuptools_scm] + [tool.setuptools] +py-modules = ["setuptools_axolotl_dynamic_dependencies"] include-package-data = true -[tool.setuptools.packages.find] -where = ["src"] - [tool.setuptools.dynamic] version = { file = "VERSION" } +[tool.setuptools.cmdclass] +build_py = "setuptools_axolotl_dynamic_dependencies.BuildPyCommand" + [tool.ruff] line-length = 88 target-version = "py310" @@ -192,40 +67,5 @@ markers = [ "slow: marks tests as slow", ] -# UV specific configuration -[tool.uv] -prerelease = "allow" -conflicts = [ - [ - { package = "axolotl" }, - { extra = "vllm" }, - ], - [ - { package = "axolotl" }, - { extra = "flash-attn" }, - ], - [ - { package = "axolotl" }, - { extra = "ring-flash-attn" }, - ], - [ - { package = "axolotl" }, - { extra = "mamba-ssm" }, - ], - [ - { package = "axolotl" }, - { extra = "auto-gptq" }, - ], - [ - { package = "axolotl" }, - { extra = "fbgemm-gpu" }, - ], -] - [tool.uv.extra-build-dependencies] axolotl = ["huggingface_hub"] -mamba-ssm = ["torch"] -causal-conv1d = ["torch"] -flash-attn = ["torch"] -deepspeed = ["torch"] -auto-gptq = ["torch"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..3fd75c3fac --- /dev/null +++ b/requirements.txt @@ -0,0 +1,78 @@ +--extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ + +# START section of dependencies that don't install on Darwin/MacOS +bitsandbytes==0.49.1 +triton>=3.4.0 +mamba-ssm==1.2.0.post1 +xformers>=0.0.23.post1 +liger-kernel==0.7.0 +# END section + +packaging==26.0 +huggingface_hub>=1.1.7 +peft>=0.18.1 +tokenizers>=0.22.1 +transformers==5.3.0 +accelerate==1.13.0 +datasets==4.5.0 +deepspeed>=0.18.6,<0.19.0 +trl==0.29.0 +hf_xet==1.3.2 +kernels==0.12.2 + +fla-core==0.4.1 +flash-linear-attention==0.4.1 + +trackio>=0.16.1 +typing-extensions>=4.15.0 + +optimum==1.16.2 +hf_transfer +sentencepiece +gradio>=6.2.0,<7.0 + +modal==1.3.0.post1 +pydantic>=2.10.6 +addict +fire +PyYAML>=6.0 +requests +wandb +einops +colorama +numba>=0.61.2 +numpy>=2.2.6 + +# qlora things +evaluate==0.4.1 +scipy +nvidia-ml-py==12.560.30 +art +tensorboard +python-dotenv==1.0.1 + +# remote filesystems +s3fs>=2024.5.0 +gcsfs>=2025.3.0 +adlfs>=2024.5.0 +ocifs==1.3.2 + +zstandard==0.22.0 +fastcore + +# lm eval harness +lm_eval==0.4.7 +langdetect==1.0.9 +immutabledict==4.2.0 +antlr4-python3-runtime==4.13.2 + +torchao==0.16.0 +openenv-core==0.1.0 +schedulefree==1.4.1 + +axolotl-contribs-lgpl==0.0.7 +axolotl-contribs-mit==0.0.6 +# telemetry +posthog==6.7.11 + +mistral-common==1.10.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..d4e1894f79 --- /dev/null +++ b/setup.py @@ -0,0 +1,230 @@ +"""setup.py for axolotl""" + +import os +import platform +import re +from importlib.metadata import PackageNotFoundError, version +from pathlib import Path + +from setuptools import find_packages, setup + + +def parse_requirements(extras_require_map): + _install_requires = [] + _dependency_links = [] + with open("./requirements.txt", encoding="utf-8") as requirements_file: + lines = [r.strip() for r in requirements_file.readlines()] + for line in lines: + is_extras = "deepspeed" in line or "mamba-ssm" in line + if line.startswith("--extra-index-url"): + # Handle custom index URLs + _, url = line.split() + _dependency_links.append(url) + elif not is_extras and line and line[0] != "#": + # Handle standard packages + _install_requires.append(line) + try: + xformers_version = [req for req in _install_requires if "xformers" in req][0] + install_xformers = platform.machine() != "aarch64" + if platform.machine() == "aarch64": + # skip on ARM64 + skip_packages = [ + "torchao", + "fla-core", + "flash-linear-attention", + ] + _install_requires = [ + req + for req in _install_requires + if re.split(r"[>=<]", req)[0].strip() not in skip_packages + ] + if "Darwin" in platform.system(): + # skip packages not compatible with OSX + skip_packages = [ + "bitsandbytes", + "triton", + "mamba-ssm", + "xformers", + "liger-kernel", + ] + _install_requires = [ + req + for req in _install_requires + if re.split(r"[>=<]", req)[0].strip() not in skip_packages + ] + print( + _install_requires, [req in skip_packages for req in _install_requires] + ) + else: + # detect the version of torch already installed + # and set it so dependencies don't clobber the torch version + try: + torch_version = version("torch") + except PackageNotFoundError: + torch_version = "2.8.0" # default to torch 2.8.0 + _install_requires.append(f"torch=={torch_version}") + + version_match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", torch_version) + if version_match: + major, minor, patch = version_match.groups() + major, minor = int(major), int(minor) + patch = ( + int(patch) if patch is not None else 0 + ) # Default patch to 0 if not present + else: + raise ValueError("Invalid version format") + + torch_parts = torch_version.split("+") + if len(torch_parts) == 2: + torch_cuda_version = torch_parts[1] + _dependency_links.append( + f"https://download.pytorch.org/whl/{torch_cuda_version}" + ) + + if (major, minor) >= (2, 10): + extras_require_map.pop("fbgemm-gpu") + extras_require_map["fbgemm-gpu"] = [ + "fbgemm-gpu==1.5.0", + "fbgemm-gpu-genai==1.5.0", + ] + if not install_xformers: + _install_requires.pop(_install_requires.index(xformers_version)) + extras_require_map["vllm"] = ["vllm>=0.17.1"] + elif (major, minor) >= (2, 9): + extras_require_map.pop("fbgemm-gpu") + extras_require_map["fbgemm-gpu"] = [ + "fbgemm-gpu==1.4.0", + "fbgemm-gpu-genai==1.4.2", + ] + if not install_xformers: + _install_requires.pop(_install_requires.index(xformers_version)) + if patch == 0: + extras_require_map["vllm"] = ["vllm==0.13.0"] + else: + extras_require_map["vllm"] = ["vllm==0.14.0"] + elif (major, minor) >= (2, 8): + extras_require_map.pop("fbgemm-gpu") + extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.3.0"] + extras_require_map["vllm"] = ["vllm==0.11.0"] + if not install_xformers: + _install_requires.pop(_install_requires.index(xformers_version)) + elif (major, minor) >= (2, 7): + _install_requires.pop(_install_requires.index(xformers_version)) + if patch == 0: + if install_xformers: + _install_requires.append("xformers==0.0.30") + # vllm 0.9.x is incompatible with latest transformers + extras_require_map.pop("vllm") + else: + if install_xformers: + _install_requires.append("xformers==0.0.31") + extras_require_map["vllm"] = ["vllm==0.10.1"] + elif (major, minor) >= (2, 6): + _install_requires.pop(_install_requires.index(xformers_version)) + if install_xformers: + _install_requires.append("xformers==0.0.29.post3") + # since we only support 2.6.0+cu126 + _dependency_links.append("https://download.pytorch.org/whl/cu126") + extras_require_map.pop("vllm") + elif (major, minor) >= (2, 5): + _install_requires.pop(_install_requires.index(xformers_version)) + if install_xformers: + if patch == 0: + _install_requires.append("xformers==0.0.28.post2") + else: + _install_requires.append("xformers>=0.0.28.post3") + extras_require_map.pop("vllm") + elif (major, minor) >= (2, 4): + extras_require_map.pop("vllm") + if install_xformers: + if patch == 0: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.27") + else: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers==0.0.28.post1") + else: + raise ValueError("axolotl requires torch>=2.4") + + except PackageNotFoundError: + pass + return _install_requires, _dependency_links, extras_require_map + + +def get_package_version(): + with open( + Path(os.path.dirname(os.path.abspath(__file__))) / "VERSION", + "r", + encoding="utf-8", + ) as fin: + version_ = fin.read().strip() + return version_ + + +extras_require = { + "flash-attn": ["flash-attn==2.8.3"], + "ring-flash-attn": [ + "flash-attn==2.8.3", + "ring-flash-attn>=0.1.7", + ], + "deepspeed": [ + "deepspeed==0.18.2", + "deepspeed-kernels", + ], + "mamba-ssm": [ + "mamba-ssm==1.2.0.post1", + "causal_conv1d", + ], + "auto-gptq": [ + "auto-gptq==0.5.1", + ], + "mlflow": [ + "mlflow", + ], + "galore": [ + "galore_torch", + ], + "apollo": [ + "apollo-torch", + ], + "optimizers": [ + "galore_torch", + "apollo-torch", + "lomo-optim==0.1.1", + "torch-optimi==0.2.1", + "came_pytorch==0.1.3", + ], + "ray": [ + "ray[train]>=2.52.1", + ], + "vllm": [ + "vllm==0.10.0", + ], + "llmcompressor": [ + "llmcompressor==0.5.1", + ], + "fbgemm-gpu": ["fbgemm-gpu-genai==1.3.0"], + "opentelemetry": [ + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-exporter-prometheus", + "prometheus-client", + ], +} +install_requires, dependency_links, extras_require_build = parse_requirements( + extras_require +) + +setup( + version=get_package_version(), + package_dir={"": "src"}, + packages=find_packages("src"), + install_requires=install_requires, + dependency_links=dependency_links, + entry_points={ + "console_scripts": [ + "axolotl=axolotl.cli.main:main", + ], + }, + extras_require=extras_require_build, +) diff --git a/src/setuptools_axolotl_dynamic_dependencies.py b/src/setuptools_axolotl_dynamic_dependencies.py new file mode 100644 index 0000000000..3bb54cda8e --- /dev/null +++ b/src/setuptools_axolotl_dynamic_dependencies.py @@ -0,0 +1,102 @@ +""" +dynamic requirements for axolotl +""" + +import platform +import re +from importlib.metadata import PackageNotFoundError, version + +from setuptools.command.build_py import build_py as _build_py + + +def parse_requirements(): + _install_requires = [] + _dependency_links = [] + with open("./requirements.txt", encoding="utf-8") as requirements_file: + lines = [r.strip() for r in requirements_file.readlines()] + for line in lines: + is_extras = ( + "flash-attn" in line + or "flash-attention" in line + or "deepspeed" in line + or "mamba-ssm" in line + or "lion-pytorch" in line + ) + if line.startswith("--extra-index-url"): + # Handle custom index URLs + _, url = line.split() + _dependency_links.append(url) + elif not is_extras and line and line[0] != "#": + # Handle standard packages + _install_requires.append(line) + + try: + xformers_version = [req for req in _install_requires if "xformers" in req][0] + torchao_version = [req for req in _install_requires if "torchao" in req][0] + + if "Darwin" in platform.system(): + # don't install xformers on MacOS + _install_requires.pop(_install_requires.index(xformers_version)) + else: + # detect the version of torch already installed + # and set it so dependencies don't clobber the torch version + try: + torch_version = version("torch") + except PackageNotFoundError: + torch_version = "2.5.1" + _install_requires.append(f"torch=={torch_version}") + + version_match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", torch_version) + if version_match: + major, minor, patch = version_match.groups() + major, minor = int(major), int(minor) + patch = ( + int(patch) if patch is not None else 0 + ) # Default patch to 0 if not present + else: + raise ValueError("Invalid version format") + + if (major, minor) >= (2, 5): + _install_requires.pop(_install_requires.index(xformers_version)) + if patch == 0: + _install_requires.append("xformers==0.0.28.post2") + else: + _install_requires.append("xformers==0.0.28.post3") + elif (major, minor) >= (2, 4): + if patch == 0: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.27") + else: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers==0.0.28.post1") + elif (major, minor) >= (2, 3): + _install_requires.pop(_install_requires.index(torchao_version)) + if patch == 0: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.26.post1") + else: + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.27") + elif (major, minor) >= (2, 2): + _install_requires.pop(_install_requires.index(torchao_version)) + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.25.post1") + else: + _install_requires.pop(_install_requires.index(torchao_version)) + _install_requires.pop(_install_requires.index(xformers_version)) + _install_requires.append("xformers>=0.0.23.post1") + + except PackageNotFoundError: + pass + return _install_requires, _dependency_links + + +class BuildPyCommand(_build_py): + """ + custom build_py command to parse dynamic requirements + """ + + def finalize_options(self): + super().finalize_options() + install_requires, _ = parse_requirements() + self.distribution.install_requires = install_requires From c2bd75aff610f257e00a7f4e7e6649f39d770757 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 25 Mar 2026 07:38:06 -0400 Subject: [PATCH 1227/1405] Nemo gym integration (#3516) [skip ci] * nemo gym integration with grpo wip * mostly working * cleanup * simplify * update docs * nemo gym support wip * cleanup * chore: lint * address PR review and add more tests * chore: lint * post merge lora fixes for CI (#3536) [skip ci] * post merge lora fixes for CI * handle lora kernel auto-enable for moe without grouped_mm * prefer not to import torch in schema validation * address pr comments, add timeout, add tests * roundup_power2_divisions not needed with newer pytorch versions (#3540) * roundup_power2_divisions not needed with newer pytorch versions * remove typo * update qwen3.5 moe 35b-a3b yaml for 5090 * more bug fixes * fix tests to match updated trainer * don't use fa2 for hooks test * reset plugins on the instance * retry download * fix references to renamed axolotl_cfg property on trainer * Fix ref to trainer cfg * fix: robust handling of race condition on patching check (#3543) [skip ci] * EBFT: Matching Features, Not Tokens: Energy-Based Fine-Tuning of Language Models (#3527) [skip ci] * EBFT wip * fixes * more fixeS * add missing strided module * ebft fixes for multi-turn * make ebft work with async * add example for ebft w qwen3.5 * fix for split thinking and update yaml for lora over linear attention only * enforce_eager for vllm arg in schema * fix sync weights * fix multi-gpu * handle updated sig for mm * ddp fixes * improve multi-gpu handling, don't calculate logits, adaptive completion length * chore: lint * chore: lint * support completion_mean * Address corereview feedback * clamp min IS ratio * Address PR code review * more fixes identified * address code review * Fix property from rebase conflict * fix for ebft sync and update docs * make trainer loss patch check a solo test --------- Co-authored-by: Claude Opus 4.6 (1M context) --- docs/rlhf.qmd | 298 +++++++++ examples/ebft/README.md | 13 +- src/axolotl/core/builders/rl.py | 16 + src/axolotl/core/trainers/ebft/__init__.py | 20 +- .../core/trainers/grpo/async_trainer.py | 40 +- src/axolotl/integrations/nemo_gym/README.md | 412 ++++++++++++ src/axolotl/integrations/nemo_gym/__init__.py | 25 + src/axolotl/integrations/nemo_gym/args.py | 146 ++++ .../integrations/nemo_gym/data_producer.py | 226 +++++++ src/axolotl/integrations/nemo_gym/dataset.py | 135 ++++ .../nemo_gym/examples/nemo_gym_multi_env.yaml | 64 ++ .../examples/nemo_gym_multi_turn.yaml | 79 +++ .../nemo_gym/examples/nemo_gym_sudoku.yaml | 62 ++ .../integrations/nemo_gym/multi_turn.py | 329 ++++++++++ src/axolotl/integrations/nemo_gym/plugin.py | 503 ++++++++++++++ src/axolotl/integrations/nemo_gym/rewards.py | 274 ++++++++ src/axolotl/integrations/nemo_gym/server.py | 242 +++++++ src/axolotl/scripts/vllm_serve_lora.py | 108 +++ .../solo}/test_trainer_loss_calc.py | 0 tests/integrations/test_nemo_gym.py | 621 ++++++++++++++++++ 20 files changed, 3593 insertions(+), 20 deletions(-) create mode 100644 src/axolotl/integrations/nemo_gym/README.md create mode 100644 src/axolotl/integrations/nemo_gym/__init__.py create mode 100644 src/axolotl/integrations/nemo_gym/args.py create mode 100644 src/axolotl/integrations/nemo_gym/data_producer.py create mode 100644 src/axolotl/integrations/nemo_gym/dataset.py create mode 100644 src/axolotl/integrations/nemo_gym/examples/nemo_gym_multi_env.yaml create mode 100644 src/axolotl/integrations/nemo_gym/examples/nemo_gym_multi_turn.yaml create mode 100644 src/axolotl/integrations/nemo_gym/examples/nemo_gym_sudoku.yaml create mode 100644 src/axolotl/integrations/nemo_gym/multi_turn.py create mode 100644 src/axolotl/integrations/nemo_gym/plugin.py create mode 100644 src/axolotl/integrations/nemo_gym/rewards.py create mode 100644 src/axolotl/integrations/nemo_gym/server.py rename tests/{monkeypatch => e2e/solo}/test_trainer_loss_calc.py (100%) create mode 100644 tests/integrations/test_nemo_gym.py diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 60c34933de..603b584668 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -18,6 +18,8 @@ feedback. Various methods include, but not limited to: - [Odds Ratio Preference Optimization (ORPO)](#orpo) - [Group Relative Policy Optimization (GRPO)](#grpo) - [Group Reward-Decoupled Policy Optimization (GDPO)](#gdpo) +- [Energy-Based Fine-Tuning (EBFT)](#ebft) +- [NeMo Gym Integration](#nemo-gym-integration) ## RLHF using Axolotl @@ -1037,6 +1039,302 @@ simpo_gamma: 0.5 # default in CPOTrainer This method uses the same dataset format as [DPO](#dpo). +### EBFT + +EBFT (Energy-Based Fine-Tuning) fine-tunes language models by optimizing a **feature-matching loss** rather than relying on external reward functions. A frozen copy of the model extracts embeddings from both generated and ground-truth completions, and the generator is updated via REINFORCE to match the ground-truth feature moments. + +Paper: ["Matching Features, Not Tokens: Energy-Based Fine-Tuning of Language Models"](https://arxiv.org/abs/2603.12248) (Jelassi et al., 2026) + +**Key advantages:** + +- No reward model or verifier required — works on any (prompt, completion) data +- Applicable to non-verifiable tasks (code, translation, creative writing) +- Operates on model rollouts (not teacher forcing), reducing distribution shift + +EBFT supports two modes: + +- **Structured mode**: For QA/instruction data with prompt + completion pairs. Uses vLLM for generation (like GRPO). +- **Strided mode**: For unstructured text without prompt/completion splits. Uses strided block-parallel generation with flex_attention — no vLLM needed. + +#### Structured Mode + +```yaml +base_model: Qwen/Qwen3-4B + +rl: ebft + +ebft: + feature_layers: [0.25, 0.5, 0.75] # Extract features at 25%, 50%, 75% depth + embed_method: last_token + use_whitening: false + alignment_coef: 1.0 # Cosine similarity reward weight + diversity_coef: 1.0 # Pairwise dot product penalty + ce_coef: 0.0 # Cross-entropy on GT tokens (0 = off) + +trl: + num_generations: 4 + max_completion_length: 256 + temperature: 0.7 + use_vllm: true + vllm_server_host: 0.0.0.0 + vllm_server_port: 8000 + vllm_lora_sync: true # LoRA adapter sync (recommended) + vllm_sync_interval: 3 + use_data_producer: true + async_prefetch: true # Set false for sync mode + scale_rewards: true + loss_type: grpo + epsilon: 0.2 + +vllm: + gpu_memory_utilization: 0.5 + max_model_len: 2048 + +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_opencode.transform + split: train[:500] + +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_target_linear: true +``` + +```bash +# Terminal 1: Start vLLM +CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve config.yaml + +# Terminal 2: Train +CUDA_VISIBLE_DEVICES=1 axolotl train config.yaml +``` + +#### Strided Mode + +For unstructured text (raw code, prose). No vLLM needed — runs on a single GPU. + +```yaml +base_model: meta-llama/Llama-3.2-1B + +rl: ebft + +ebft: + mode: strided + stride: 8 + context_length: 8 + generate_max_len: 8 + n_samples_per_prompt: 4 + temperature: 0.6 + feature_layers: [0.25, 0.5, 0.75] + embed_method: last_token + use_whitening: true + alignment_coef: 1.0 + diversity_coef: 1.0 + rl_coef: 1.0 + ce_coef: 0.03 + advantage_estimator: rloo + +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_strided_structured.transform + split: train[:1%] + +flash_attention: false +flex_attention: true # Strided mode uses flex_attention +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: true # Required for flex_attention +``` + +```bash +CUDA_VISIBLE_DEVICES=0 axolotl train config.yaml +``` + +::: {.callout-tip} +See `examples/ebft/` for complete example configs covering Llama 1B/3B/8B and Qwen3 4B/8B models in both modes. +::: + +#### EBFT Configuration Reference + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `ebft.feature_layers` | `[0.25, 0.5, 0.75]` | Layer depths for feature extraction (fractional) | +| `ebft.embed_method` | `last_token` | Feature pooling: `last_token`, `mean_pooling`, `concat` | +| `ebft.use_whitening` | `false` | SVD whitening of feature dimensions | +| `ebft.alignment_coef` | `1.0` | Cosine similarity reward weight | +| `ebft.diversity_coef` | `1.0` | Pairwise dot product penalty weight | +| `ebft.ce_coef` | `0.0` | Cross-entropy loss on ground-truth tokens | +| `ebft.mode` | `structured` | `structured` (vLLM) or `strided` (no vLLM) | +| `ebft.stride` | — | Tokens between anchor points (strided mode) | +| `ebft.context_length` | — | Context window per block (strided mode) | +| `ebft.generate_max_len` | — | Tokens to generate per block (strided mode) | +| `ebft.n_samples_per_prompt` | — | Rollouts per document (strided mode) | +| `ebft.advantage_estimator` | `grpo` | `grpo` or `rloo` (strided mode) | + +### NeMo Gym Integration + +[NeMo Gym](https://github.com/NVIDIA-NeMo/Gym) provides 50+ verified RL environments (math, coding, tool-use, reasoning) with deterministic reward signals. The axolotl integration supports both **single-turn** (call `/verify` after generation) and **multi-turn** (agent-based tool execution via `/run`). + +#### Single-Turn (Simplest) + +For environments that only need answer verification (math, coding challenges). No agent server needed — the reward function calls `/verify` directly on the resource server. + +```yaml +base_model: Qwen/Qwen2.5-0.5B-Instruct + +rl: grpo +chat_template: tokenizer_default + +trl: + use_vllm: false # Colocate mode (single GPU) + num_generations: 4 + max_completion_length: 128 + temperature: 0.9 + reward_funcs: + - axolotl.integrations.nemo_gym.rewards.reward_nemo_gym_verify + +plugins: + - axolotl.integrations.nemo_gym.NemoGymPlugin + +nemo_gym_enabled: true +nemo_gym_dir: ~/Gym +nemo_gym_auto_start: false +nemo_gym_head_port: 11000 +nemo_gym_datasets: + - path: resources_servers/reasoning_gym/data/train_basic_arithmetic.jsonl + server_name: reasoning_gym + +datasets: + - path: ~/Gym/resources_servers/reasoning_gym/data/train_basic_arithmetic.jsonl + type: chat_template + field_messages: responses_create_params.input + message_field_content: content + message_field_role: role +``` + +```bash +# Terminal 1: Start NeMo Gym resource server +cd ~/Gym && .venv/bin/ng_run \ + "+config_paths=[resources_servers/reasoning_gym/configs/resources_only.yaml]" \ + "+skip_venv_if_present=true" + +# Terminal 2: Train +CUDA_VISIBLE_DEVICES=0 axolotl train config.yaml +``` + +::: {.callout-note} +`nemo_gym_datasets.path` is relative to `nemo_gym_dir`. Don't use absolute paths or they will be double-joined. +::: + +#### Multi-Turn with Async GRPO (Recommended) + +For environments with tool-use (weather, search, databases). An agent server orchestrates multi-turn interactions: generate → parse tool calls → execute tools → feed results back → repeat until done. + +```yaml +base_model: Qwen/Qwen3-0.6B + +rl: grpo +chat_template: tokenizer_default + +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_target_modules: [q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj] + +trl: + use_vllm: true + vllm_mode: server + vllm_server_host: localhost + vllm_server_port: 8000 + vllm_lora_sync: true + vllm_sync_interval: 5 + use_data_producer: true + async_prefetch: true # 3x speedup + num_generations: 4 + max_completion_length: 512 + temperature: 0.8 + reward_funcs: + - axolotl.integrations.nemo_gym.rewards.reward_env + +plugins: + - axolotl.integrations.nemo_gym.NemoGymPlugin + +nemo_gym_enabled: true +nemo_gym_auto_start: false +nemo_gym_head_port: 11000 +nemo_gym_multi_turn: true +nemo_gym_verify_timeout: 120 +nemo_gym_datasets: + - path: resources_servers/example_single_tool_call/data/weather_tool_calling.jsonl + server_name: example_single_tool_call + +datasets: + - path: ~/Gym/resources_servers/example_single_tool_call/data/weather_tool_calling.jsonl + type: chat_template + field_messages: responses_create_params.input + message_field_content: content + message_field_role: role + +vllm: + gpu_memory_utilization: 0.85 + max_model_len: 2048 +``` + +Multi-turn requires three services running: + +```bash +# Terminal 1: vLLM with LoRA + tool calling +VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 CUDA_VISIBLE_DEVICES=0 \ + python -m vllm.entrypoints.openai.api_server \ + --model Qwen/Qwen3-0.6B --max-model-len 2048 \ + --gpu-memory-utilization 0.85 \ + --enable-lora --max-lora-rank 64 \ + --enable-auto-tool-choice --tool-call-parser hermes + +# Terminal 2: NeMo Gym servers (resource + model proxy + agent) +cd ~/Gym && .venv/bin/ng_run \ + "+config_paths=[configs/axolotl_tool_calling.yaml]" \ + "+skip_venv_if_present=true" + +# Terminal 3: Training +CUDA_VISIBLE_DEVICES=1 axolotl train config.yaml +``` + +::: {.callout-important} +Multi-turn requires a NeMo Gym agent config YAML that defines three components: a resource server (tools + `/verify`), a model server proxy (forwards to your vLLM), and an agent server (orchestrates `/run`). See the [NeMo Gym README](https://github.com/NVIDIA-NeMo/Gym) for agent config format. +::: + +#### NeMo Gym Prerequisites + +```bash +# Clone and set up NeMo Gym +git clone https://github.com/NVIDIA-NeMo/Gym.git ~/Gym +cd ~/Gym +uv venv --python 3.12 && source .venv/bin/activate && uv sync + +# Fix pycosat build (GCC 13+) +CFLAGS="" uv pip install pycosat --python .venv/bin/python --no-build-isolation +``` + +#### NeMo Gym Configuration Reference + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `nemo_gym_enabled` | bool | — | Enable the NeMo Gym integration | +| `nemo_gym_dir` | str | `~/Gym` | Path to NeMo Gym repo | +| `nemo_gym_auto_start` | bool | `true` | Auto-start resource servers | +| `nemo_gym_head_port` | int | `11000` | Head server port | +| `nemo_gym_multi_turn` | bool | `false` | Enable multi-turn via agent `/run` | +| `nemo_gym_verify_timeout` | int | `30` | Per-request timeout (seconds) | +| `nemo_gym_datasets` | list | required | Dataset configs with `path` and `server_name` | + +#### Reward Functions + +| Function | Mode | Description | +|----------|------|-------------| +| `axolotl.integrations.nemo_gym.rewards.reward_nemo_gym_verify` | Single-turn | Calls `/verify`, returns binary reward | +| `axolotl.integrations.nemo_gym.rewards.reward_env` | Multi-turn | Passthrough reward from agent `/run` | + ### Using local dataset files ```yaml diff --git a/examples/ebft/README.md b/examples/ebft/README.md index 533e13652d..24f2c582d4 100644 --- a/examples/ebft/README.md +++ b/examples/ebft/README.md @@ -47,14 +47,11 @@ For **unstructured text** without prompt/completion splits (e.g., raw code, pros ### Structured Mode (QA data + vLLM) ```bash -# 1. Start vLLM server -python -m trl.scripts.vllm_serve \ - --model meta-llama/Llama-3.2-1B \ - --host 0.0.0.0 --port 8000 \ - --gpu-memory-utilization 0.3 - -# 2. Train -axolotl train examples/ebft/llama-1b-ebft-opencode.yaml +# 1. Start vLLM server (LoRA serve module auto-selected when vllm_lora_sync: true) +CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve examples/ebft/qwen3-4b-ebft-structured-async.yaml + +# 2. Train on a separate GPU +CUDA_VISIBLE_DEVICES=1 axolotl train examples/ebft/qwen3-4b-ebft-structured-async.yaml ``` ### Strided Mode (unstructured text) diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index 89d4c9ff76..c5cdd37920 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -176,6 +176,22 @@ def _build_training_arguments(self, total_num_steps): ) training_args_kwargs.update(GRPOStrategy.set_training_args_kwargs(self.cfg)) blocklist_args_kwargs = GRPOStrategy.get_blocklist_args_kwargs() + if not async_grpo: + # Filter out async/fast-async-only fields not in standard GRPOConfig. + # These are defined in FastAsyncGRPOConfig and only used by + # AxolotlAsyncGRPOConfig. Standard GRPOConfig rejects them. + import dataclasses + + from trl import GRPOConfig as _BaseGRPOConfig + + from axolotl.core.trainers.grpo.fast_async_trainer import ( + FastAsyncGRPOConfig, + ) + + async_only_fields = { + f.name for f in dataclasses.fields(FastAsyncGRPOConfig) + } - {f.name for f in dataclasses.fields(_BaseGRPOConfig)} + blocklist_args_kwargs.extend(list(async_only_fields)) if self.cfg.rl is RLType.GDPO: training_args_kwargs.setdefault( "multi_objective_aggregation", "normalize_then_sum" diff --git a/src/axolotl/core/trainers/ebft/__init__.py b/src/axolotl/core/trainers/ebft/__init__.py index 23b61fbe62..92abe9f269 100644 --- a/src/axolotl/core/trainers/ebft/__init__.py +++ b/src/axolotl/core/trainers/ebft/__init__.py @@ -34,7 +34,16 @@ def get_trainer_class(cls, cfg: DictDefault | None = None): return AxolotlStridedEBFTTrainer # Structured mode: async or sync - use_async = cfg and cfg.trl and getattr(cfg.trl, "async_prefetch", False) + # use_data_producer also triggers async trainer (needed for LoRA sync + # without async_prefetch, since sync trainer lacks LoRA sync support) + use_async = ( + cfg + and cfg.trl + and ( + getattr(cfg.trl, "async_prefetch", False) + or getattr(cfg.trl, "use_data_producer", False) + ) + ) if use_async: from axolotl.core.trainers.ebft.trainer import AxolotlAsyncEBFTTrainer @@ -50,7 +59,14 @@ def get_training_args_class(cls, cfg: DictDefault | None = None): return AxolotlStridedEBFTConfig # Structured mode: async or sync config - use_async = cfg and cfg.trl and getattr(cfg.trl, "async_prefetch", False) + use_async = ( + cfg + and cfg.trl + and ( + getattr(cfg.trl, "async_prefetch", False) + or getattr(cfg.trl, "use_data_producer", False) + ) + ) if use_async: return AxolotlAsyncEBFTConfig return AxolotlEBFTConfig diff --git a/src/axolotl/core/trainers/grpo/async_trainer.py b/src/axolotl/core/trainers/grpo/async_trainer.py index 2b8cda6d82..9b6ae2e285 100644 --- a/src/axolotl/core/trainers/grpo/async_trainer.py +++ b/src/axolotl/core/trainers/grpo/async_trainer.py @@ -1012,27 +1012,44 @@ def _sync_lora_adapter(self): import requests vllm_client = self.vllm_generation.vllm_client - url = f"{vllm_client.base_url}/set_lora_adapter/" + base_url = vllm_client.base_url + base_model = getattr(self.args, "model_name_or_path", "axolotl-lora") sync_timeout = getattr(self.args, "vllm_server_timeout", 300) or 300 + + # Try standard vLLM /v1/load_lora_adapter first, fall back to custom endpoint response = requests.post( - url, + f"{base_url}/v1/load_lora_adapter", json={ - "lora_name": "active_lora", - "lora_int_id": self._lora_sync_version, + "lora_name": base_model, "lora_path": adapter_path, + "load_inplace": True, }, timeout=sync_timeout, ) if response.status_code != 200: - logger.warning( - "Failed to set LoRA adapter: %s %s", - response.status_code, - response.text, + # Fallback: try custom /set_lora_adapter/ endpoint + response = requests.post( + f"{base_url}/set_lora_adapter/", + json={ + "lora_name": "active_lora", + "lora_int_id": self._lora_sync_version, + "lora_path": adapter_path, + }, + timeout=30, ) - return + if response.status_code != 200: + logger.warning( + "Failed to set LoRA adapter: %s %s", + response.status_code, + response.text, + ) + return # Reset prefix cache after adapter update - vllm_client.reset_prefix_cache() + try: + vllm_client.reset_prefix_cache() + except Exception as exc: + logger.warning("Failed to reset prefix cache: %s", exc) # Clean up old adapter versions (keep only current) if self._lora_sync_version > 1: @@ -2486,6 +2503,9 @@ def _get_per_token_logps_and_entropies( logits, completion_ids, self.temperature ) all_logps.append(logps) + # Liger fused path doesn't compute entropy — append zeros + if compute_entropy: + all_entropies.append(torch.zeros_like(logps)) else: logits = logits[:, :-1, :] logits = logits[:, -logits_to_keep:, :] diff --git a/src/axolotl/integrations/nemo_gym/README.md b/src/axolotl/integrations/nemo_gym/README.md new file mode 100644 index 0000000000..2a1f267c10 --- /dev/null +++ b/src/axolotl/integrations/nemo_gym/README.md @@ -0,0 +1,412 @@ +# NeMo Gym Integration for Axolotl + +Train LLMs with reinforcement learning using [NVIDIA NeMo Gym](https://github.com/NVIDIA-NeMo/Gym) environments as reward sources. NeMo Gym provides 50+ verified RL environments spanning math, coding, tool-use, reasoning, and safety — each with deterministic reward signals. + +## Validated Training Paths + +| Path | Speed | Multi-turn | Architecture | +|------|-------|------------|--------------| +| **Async GRPO + Data Producer** | Fastest (3x) | Yes | `NemoGymDataProducer` replaces vLLM generation | +| Standard GRPO + Data Producer | Baseline | Yes | Same producer, no async prefetch | +| Standard GRPO + /verify | Simplest | No | Reward function calls /verify directly | +| FSDP2 + /verify (2 GPU) | Distributed | No | `fsdp_version: 2` | + +Multi-turn uses `nemo_gym_multi_turn: true` which auto-enables the async trainer's +data producer protocol. The plugin's `NemoGymDataProducer` calls NeMo Gym agent `/run` +endpoints and returns `RolloutDataset` with proper IS correction, env_mask, and rewards. + +All paths tested end-to-end with Qwen3-0.6B + LoRA, logged to wandb project `nemo-gym-rl`. + +## Quick Start + +### Prerequisites + +- [uv](https://github.com/astral-sh/uv) package manager (for NeMo Gym's venv) +- Two GPUs recommended (one for vLLM server, one for training) + +### 1. Set Up NeMo Gym + +```bash +git clone https://github.com/NVIDIA-NeMo/Gym.git ~/Gym +cd ~/Gym +uv venv --python 3.12 && source .venv/bin/activate && uv sync + +# Fix pycosat build (GCC 13+) +CFLAGS="" uv pip install pycosat --python .venv/bin/python --no-build-isolation + +# Pre-build resource server venvs +for dir in resources_servers/reasoning_gym resources_servers/example_single_tool_call responses_api_models/vllm_model responses_api_agents/simple_agent; do + uv venv --seed --allow-existing --python 3.12 $dir/.venv + CFLAGS="" uv pip install --python $dir/.venv/bin/python pycosat --no-build-isolation 2>/dev/null + uv pip install --python $dir/.venv/bin/python -e . "ray[default]==2.52.1" +done + +# Install extra deps for reasoning_gym +uv pip install --python resources_servers/reasoning_gym/.venv/bin/python \ + reasoning-gym matplotlib pillow cycler contourpy kiwisolver +``` + +### 2. Multi-Turn with Async GRPO (Recommended — Fastest Path) + +This is the fully validated, highest-performance path. NeMo Gym's agent server handles +multi-turn tool execution while axolotl's async GRPO prefetches data in background threads. + +**Step 1: Create the NeMo Gym agent config** + +Create `~/Gym/configs/axolotl_tool_calling.yaml`: +```yaml +# Resource server (tools + verify) +example_single_tool_call: + resources_servers: + example_single_tool_call: + entrypoint: app.py + domain: agent + verified: false + +# Model server proxy (forwards to your vLLM) +policy_model: + responses_api_models: + vllm_model: + entrypoint: app.py + base_url: http://localhost:8000/v1 + api_key: dummy_key + model: Qwen/Qwen3-0.6B # Must match your training model + return_token_id_information: true + uses_reasoning_parser: false + +# Agent server (orchestrates multi-turn via /run) +example_single_tool_call_simple_agent: + responses_api_agents: + simple_agent: + entrypoint: app.py + resources_server: + type: resources_servers + name: example_single_tool_call + model_server: + type: responses_api_models + name: policy_model + datasets: + - name: weather + type: example + jsonl_fpath: resources_servers/example_single_tool_call/data/weather_tool_calling.jsonl +``` + +**Step 2: Start three services** + +```bash +# Terminal 1: vLLM OpenAI server on GPU 0 +CUDA_VISIBLE_DEVICES=0 python -m vllm.entrypoints.openai.api_server \ + --model Qwen/Qwen3-0.6B --max-model-len 2048 --gpu-memory-utilization 0.85 + +# Terminal 2: NeMo Gym (resource server + model proxy + agent) +cd ~/Gym && .venv/bin/ng_run \ + "+config_paths=[configs/axolotl_tool_calling.yaml]" "+skip_venv_if_present=true" + +# Terminal 3: Training on GPU 1 +cd experiments && CUDA_VISIBLE_DEVICES=1 CUDA_HOME=$HOME/env-claude-cu130/cuda_shim \ + axolotl train nemo_gym_async_agent.yaml +``` + +**Step 3: Training config** (`nemo_gym_async_agent.yaml`): +```yaml +base_model: Qwen/Qwen3-0.6B +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_target_modules: [q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj] +sequence_len: 2048 + +rl: grpo +chat_template: tokenizer_default + +trl: + use_vllm: true + vllm_mode: server + vllm_server_host: localhost + vllm_server_port: 8000 + vllm_lora_sync: true + vllm_sync_interval: 5 + # Async GRPO — 3x faster than standard + use_data_producer: true + async_prefetch: true + num_generations: 4 + max_completion_length: 512 + temperature: 0.8 + reward_funcs: + - axolotl.integrations.nemo_gym.rewards.reward_env + +plugins: + - axolotl.integrations.nemo_gym.NemoGymPlugin + +nemo_gym_enabled: true +nemo_gym_auto_start: false +nemo_gym_head_port: 11000 +nemo_gym_multi_turn: true +nemo_gym_verify_timeout: 120 +nemo_gym_datasets: + - path: ~/Gym/resources_servers/example_single_tool_call/data/weather_tool_calling.jsonl + server_name: example_single_tool_call + +datasets: + - path: ~/Gym/resources_servers/example_single_tool_call/data/weather_tool_calling.jsonl + type: chat_template + field_messages: responses_create_params.input + message_field_content: content + message_field_role: role + +vllm: + gpu_memory_utilization: 0.85 + max_model_len: 2048 + tensor_parallel_size: 1 + +learning_rate: 5e-6 +micro_batch_size: 1 +gradient_accumulation_steps: 4 +max_steps: 30 +gradient_checkpointing: true +bf16: true +output_dir: ./outputs/nemo_gym_async + +use_wandb: true +wandb_project: nemo-gym-rl +``` + +### 3. Single-Turn Training (Simplest — No Agent Server Needed) + +For environments that only need single-turn verify (math, coding challenges), you don't need +an agent server. The plugin's reward function calls `/verify` directly. + +```yaml +base_model: Qwen/Qwen2.5-0.5B-Instruct +rl: grpo +chat_template: tokenizer_default + +trl: + use_vllm: true + vllm_mode: colocate + vllm_enable_sleep_mode: false + num_generations: 8 + max_completion_length: 128 + temperature: 0.9 + reward_funcs: + - axolotl.integrations.nemo_gym.rewards.reward_nemo_gym_verify + +plugins: + - axolotl.integrations.nemo_gym.NemoGymPlugin + +nemo_gym_enabled: true +nemo_gym_auto_start: false +nemo_gym_head_port: 11000 +nemo_gym_datasets: + - path: ~/Gym/resources_servers/reasoning_gym/data/train_basic_arithmetic.jsonl + server_name: reasoning_gym + +datasets: + - path: ~/Gym/resources_servers/reasoning_gym/data/train_basic_arithmetic.jsonl + type: chat_template + field_messages: responses_create_params.input + message_field_content: content + message_field_role: role + +vllm: + gpu_memory_utilization: 0.3 + max_model_len: 512 + tensor_parallel_size: 1 + +learning_rate: 1e-5 +micro_batch_size: 4 +gradient_accumulation_steps: 2 +max_steps: 50 +output_dir: ./outputs/nemo_gym_arithmetic +``` + +Only needs `ng_run` with resource servers (no agent config): +```bash +cd ~/Gym && ng_run "+config_paths=[resources_servers/reasoning_gym/configs/resources_only.yaml]" "+skip_venv_if_present=true" +``` + +## How It Works + +### Single-Turn +```text +axolotl train → GRPO Trainer generates completions + → NeMo Gym plugin reward_fn calls POST /verify on resource server + → reward flows back to GRPO for advantage computation +``` + +### Multi-Turn (Agent /run) +```text +┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ +│ axolotl │ │ NeMo Gym │────▶│ vLLM OpenAI │ +│ train │────▶│ Agent /run │◀────│ Server (GPU 0) │ +│ (GPU 1) │ │ │ │ /v1/completions │ +└─────────────┘ └──────┬───────┘ └──────────────────┘ + │ + ▼ + ┌──────────────┐ + │ Resource │ + │ Server │ + │ (tools + │ + │ verify) │ + └─────────────┘ +``` + +The agent server orchestrates the entire multi-turn loop: +1. Calls our vLLM server for model generation +2. Parses tool calls from model output +3. Executes tools against resource servers +4. Feeds tool results back to the model +5. Repeats until done, then calls /verify for reward +6. Returns token IDs + logprobs + reward to our rollout_func + +### Data Producer Architecture (Multi-Turn) + +When `nemo_gym_multi_turn: true`, the plugin automatically forces `use_data_producer: true` +which selects the `AxolotlAsyncGRPOTrainer`. The plugin then swaps the trainer's data +producer with `NemoGymDataProducer`, which: + +1. Gets a prompt batch from the dataset iterator +2. Expands by `num_generations` (one agent call per rollout) +3. Calls NeMo Gym agents via async HTTP (`aiohttp.gather`) +4. Parses responses into padded tensors (`RolloutDataset`) +5. Returns with `_pending_policy_logps=True` for deferred scoring + +The main thread then runs `_compute_deferred_scores()` which: +- Computes **policy logprobs** on the training model (GPU forward pass) +- Computes **IS correction** using agent's sampling logprobs vs training model logprobs +- Computes advantages with group-level normalization +- All downstream features work: replay buffer, re-roll, streaming, zero-adv skip + +With `async_prefetch: true`, the data producer runs in a background thread — giving ~3x +speedup as generation and training overlap. With `async_prefetch: false`, it runs +synchronously on the main thread (still uses the data producer protocol). + +### Weight Sync (LoRA Mode) + +With `vllm_lora_sync: true`, the plugin (or async trainer) replaces NCCL-based weight +sync with filesystem + HTTP: + +1. `accelerator.get_state_dict()` gathers LoRA weights from all ranks +2. Rank 0 saves adapter to `/tmp/lora_sync_*/vN/` +3. Rank 0 POSTs to `/set_lora_adapter/` on vLLM server +4. vLLM loads adapter natively via Punica kernels +5. Only ~40MB transferred (vs multiple GBs for full model weights) + +### Multi-Environment Support + +Datasets support per-row environment routing via `agent_ref`: +```jsonl +{"agent_ref": {"name": "reasoning_gym"}, "responses_create_params": {...}} +{"agent_ref": {"name": "instruction_following"}, "responses_create_params": {...}} +``` + +Or use the simpler per-dataset routing: +```yaml +nemo_gym_datasets: + - path: reasoning_data.jsonl + server_name: reasoning_gym + - path: tool_data.jsonl + server_name: example_single_tool_call +``` + +## Configuration Reference + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `nemo_gym_enabled` | bool | `null` | Enable the NeMo Gym integration | +| `nemo_gym_dir` | str | `~/Gym` | Path to NeMo Gym repo | +| `nemo_gym_auto_clone` | bool | `true` | Auto-clone NeMo Gym repo if missing | +| `nemo_gym_auto_start` | bool | `true` | Auto-start resource servers | +| `nemo_gym_config_paths` | list[str] | — | Server config YAMLs (relative to gym_dir) | +| `nemo_gym_datasets` | list[dict] | required | Dataset configs with `path` and optional `server_name` | +| `nemo_gym_head_port` | int | `11000` | Head server port | +| `nemo_gym_server_timeout` | int | `360` | Server startup timeout (seconds) | +| `nemo_gym_verify_timeout` | int | `30` | Per-request timeout (seconds) | +| `nemo_gym_multi_turn` | bool | `false` | Enable multi-turn via agent /run | + +### Dataset JSONL Format + +Each line must have `responses_create_params` with `input` messages: +```json +{ + "responses_create_params": { + "input": [{"role": "user", "content": "What's the weather in SF?"}], + "tools": [{"name": "get_weather", "type": "function", "strict": true, "parameters": {...}}] + } +} +``` + +For multi-turn agent routing, include `agent_ref`: +```json +{"agent_ref": {"name": "my_agent"}, "responses_create_params": {...}} +``` + +Note: Tool definitions MUST include `"strict": true` and `"additionalProperties": false` for NeMo Gym agent compatibility. + +### Reward Functions + +The plugin provides two built-in reward functions — no user code needed: + +```yaml +trl: + reward_funcs: + # Multi-turn (nemo_gym_multi_turn: true): + # Passthrough — agent /run already computed the reward + - axolotl.integrations.nemo_gym.rewards.reward_env + + # Single-turn (nemo_gym_multi_turn: false): + # Calls /verify endpoints on NeMo Gym resource servers + - axolotl.integrations.nemo_gym.rewards.reward_nemo_gym_verify +``` + +Both are also importable from Python: + +```python +from axolotl.integrations.nemo_gym import reward_env, reward_nemo_gym_verify +``` + +## Known Issues / Troubleshooting + +### NeMo Gym Server Setup +- **pycosat build failure**: `CFLAGS="" uv pip install pycosat --no-build-isolation` +- **Ray version mismatch**: Pin `ray[default]==2.52.1` in all server venvs +- **Pre-build venvs**: `ng_run` creates per-server venvs via Ray. Pre-build them and use `+skip_venv_if_present=true` +- **Tool `strict` field required**: Agent server validates tool definitions require `strict: true` + +### vLLM / Weight Sync +- **Start vLLM with LoRA + tool calling + runtime loading**: + ```bash + VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 \ + CUDA_VISIBLE_DEVICES=0 python -m vllm.entrypoints.openai.api_server \ + --model Qwen/Qwen3-4B-Instruct-2507 \ + --max-model-len 4096 \ + --gpu-memory-utilization 0.7 \ + --enable-lora --max-lora-rank 64 \ + --enable-auto-tool-choice --tool-call-parser hermes + ``` +- **`VLLM_ALLOW_RUNTIME_LORA_UPDATING=1`**: Required for `vllm_lora_sync: true`. Without it, vLLM won't expose the `/v1/load_lora_adapter` endpoint and weight sync will fail silently. The plugin warns if this endpoint is missing. +- **`--enable-lora`**: Enables LoRA adapter support in vLLM +- **`--enable-auto-tool-choice --tool-call-parser hermes`**: Required for Qwen3 tool calling +- **`max_model_len` must be > `max_completion_length`**: Leave room for prompt tokens (~200). If equal, the NeMo Gym model proxy gets a 400 error and returns empty completions. +- **`CUDA_HOME` required**: DeepSpeed import needs it for the nvcc shim +- **NCCL weight sync broken with vLLM 0.17**: Use `vllm_lora_sync: true` (filesystem + HTTP via `/v1/load_lora_adapter`) + +### Multi-Turn +- **Agent server required**: Multi-turn delegates to NeMo Gym's agent server `/run` endpoint. Without an agent, the plugin falls back to single-turn `/verify` +- **Model server proxy**: NeMo Gym needs a `responses_api_models` server that proxies to your vLLM. See the agent config example above + +### FSDP2 +- Validated on 2 GPUs with single-turn + LoRA +- Async field filtering: The builder automatically filters async-only config fields when using the standard GRPO trainer + +## Comparison with Other Integrations + +| Feature | Axolotl + NeMo Gym | Unsloth + NeMo Gym | NeMo RL (native) | +|---------|-------------------|-------------------|-------------------| +| Server management | Automatic | Manual (notebook) | Built-in | +| Multi-environment | Per-row routing | Manual code | YAML config | +| Multi-turn / tool use | Agent /run delegation | No | Agent /run (Ray) | +| Async GRPO (3x speedup) | Yes | No | Yes | +| LoRA sync | Filesystem + HTTP | N/A | NCCL | +| Multi-GPU (FSDP2) | Yes | No | Yes (Ray) | +| Config-driven | Yes | No (code) | Yes | diff --git a/src/axolotl/integrations/nemo_gym/__init__.py b/src/axolotl/integrations/nemo_gym/__init__.py new file mode 100644 index 0000000000..b880bee7fa --- /dev/null +++ b/src/axolotl/integrations/nemo_gym/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# This software may be used and distributed according to +# the terms of the Axolotl Community License Agreement (the "License"); +# you may not use this file except in compliance with the License. + +""" +Plugin for NVIDIA NeMo Gym integration with Axolotl. + +NeMo Gym provides RL training environments for LLMs with verification-based +reward signals. This plugin manages the NeMo Gym server lifecycle, loads +datasets in the NeMo Gym JSONL format, and creates reward functions that +call the NeMo Gym /verify endpoints. +""" + +from .args import NemoGymArgs +from .plugin import NemoGymPlugin +from .rewards import reward_env, reward_nemo_gym_verify + +__all__ = [ + "NemoGymArgs", + "NemoGymPlugin", + "reward_env", + "reward_nemo_gym_verify", +] diff --git a/src/axolotl/integrations/nemo_gym/args.py b/src/axolotl/integrations/nemo_gym/args.py new file mode 100644 index 0000000000..3c593fa801 --- /dev/null +++ b/src/axolotl/integrations/nemo_gym/args.py @@ -0,0 +1,146 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# This software may be used and distributed according to +# the terms of the Axolotl Community License Agreement (the "License"); +# you may not use this file except in compliance with the License. + +""" +Input arguments for the NeMo Gym integration plugin. +""" + +from pydantic import BaseModel, Field, model_validator + + +class NemoGymArgs(BaseModel): + """Configuration args for the NeMo Gym integration.""" + + nemo_gym_enabled: bool | None = Field( + default=True, + json_schema_extra={ + "description": "Enable NeMo Gym integration for environment-based RL rewards." + }, + ) + nemo_gym_dir: str | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Path to the NeMo Gym repository clone. " + "If not set and nemo_gym_auto_clone is True, clones to ~/Gym." + ) + }, + ) + nemo_gym_auto_clone: bool | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Automatically clone the NeMo Gym repository if not present. " + "Defaults to True when nemo_gym_enabled is set." + ) + }, + ) + nemo_gym_config_paths: list[str] | None = Field( + default=None, + json_schema_extra={ + "description": ( + "List of NeMo Gym resource server config YAML paths, relative to nemo_gym_dir. " + "Example: ['resources_servers/reasoning_gym/configs/resources_only.yaml']" + ) + }, + ) + nemo_gym_head_port: int | None = Field( + default=11000, + json_schema_extra={ + "description": "Port for the NeMo Gym head server. Defaults to 11000." + }, + ) + nemo_gym_server_timeout: int | None = Field( + default=360, + json_schema_extra={ + "description": "Timeout in seconds waiting for NeMo Gym servers to start. Defaults to 360." + }, + ) + nemo_gym_verify_timeout: int | None = Field( + default=30, + json_schema_extra={ + "description": "Timeout in seconds for individual /verify requests. Defaults to 30." + }, + ) + nemo_gym_run_timeout: int | None = Field( + default=300, + json_schema_extra={ + "description": ( + "Timeout in seconds for each agent /run request (one multi-turn rollout). " + "Prevents stuck generations (e.g. model looping on tags) from " + "blocking training indefinitely. Defaults to 300 (5 minutes)." + ) + }, + ) + nemo_gym_datasets: list[dict] | None = Field( + default=None, + json_schema_extra={ + "description": ( + "List of NeMo Gym dataset configs. Each entry has 'path' (JSONL file path " + "relative to nemo_gym_dir) and optionally 'server_name' (default resource server). " + "If the JSONL rows have agent_ref.name, that takes precedence per row, " + "enabling multi-environment training from a single dataset file. " + "Optional 'max_samples' to limit per dataset." + ) + }, + ) + nemo_gym_auto_start: bool | None = Field( + default=True, + json_schema_extra={ + "description": ( + "Automatically start NeMo Gym resource servers. Defaults to True. " + "Set to False if servers are already running externally." + ) + }, + ) + nemo_gym_model_name: str | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Model name to report in verify requests. " + "Defaults to the base_model from the main config." + ) + }, + ) + nemo_gym_multi_turn: bool | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Enable multi-turn rollouts via NeMo Gym. When True, uses TRL's " + "rollout_func to run multi-step interactions with tool execution. " + "Requires use_vllm=True in TRL config. The model generates responses, " + "tool calls are executed against resource servers, and results are " + "fed back for the next turn. Final reward comes from /verify." + ) + }, + ) + nemo_gym_max_turns: int | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Maximum number of turns per multi-turn rollout. Defaults to 10. " + "Each turn consists of a model generation + optional tool execution." + ) + }, + ) + + @model_validator(mode="before") + @classmethod + def check_nemo_gym_config(cls, data): + if data.get("nemo_gym_enabled"): + if not data.get("nemo_gym_config_paths") and data.get( + "nemo_gym_auto_start", True + ): + raise ValueError( + "nemo_gym_config_paths is required when nemo_gym_enabled=True " + "and nemo_gym_auto_start is not False." + ) + if not data.get("nemo_gym_datasets"): + raise ValueError( + "nemo_gym_datasets is required when nemo_gym_enabled=True. " + "Provide at least one dataset with 'path' and 'server_name'." + ) + return data diff --git a/src/axolotl/integrations/nemo_gym/data_producer.py b/src/axolotl/integrations/nemo_gym/data_producer.py new file mode 100644 index 0000000000..64b76d7804 --- /dev/null +++ b/src/axolotl/integrations/nemo_gym/data_producer.py @@ -0,0 +1,226 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# This software may be used and distributed according to +# the terms of the Axolotl Community License Agreement (the "License"); +# you may not use this file except in compliance with the License. + +""" +NeMo Gym Data Producer for async GRPO training. + +Replaces GRPODataProducer to generate rollouts via NeMo Gym agent /run endpoints +instead of vLLM. The agent handles generation, tool execution, and reward computation. +Returns RolloutDataset in the same format as the standard producer, so all downstream +components (deferred scoring, IS correction, streaming, replay, re-roll) work unchanged. +""" + +from __future__ import annotations + +import asyncio +from typing import Any + +import torch +from trl.trainer.utils import pad + +from axolotl.core.trainers.grpo.async_trainer import GRPODataProducer, RolloutDataset +from axolotl.utils.logging import get_logger + +from .multi_turn import _call_agents, _parse_agent_response + +LOG = get_logger(__name__) + + +class NemoGymDataProducer(GRPODataProducer): + """Produces GRPO rollouts by calling NeMo Gym agent /run endpoints. + + Drop-in replacement for GRPODataProducer. Instead of calling vLLM for generation, + sends prompts to NeMo Gym agents which handle generation + tool execution + reward. + Returns the same RolloutDataset format so deferred scoring, IS correction, + replay buffer, and re-roll all work unchanged. + """ + + def __init__( + self, + *args, + agent_servers: dict[str, str], + dataset_lookup: dict, + request_timeout: float = 10800, + **kwargs, + ): + super().__init__(*args, **kwargs) + self._agent_servers = agent_servers + self._dataset_lookup = dataset_lookup + self._request_timeout = request_timeout + + def produce( + self, + model: Any, + global_step: int, + *, + skip_policy_logps: bool = False, + processing_class: Any = None, + accelerator: Any = None, + args: Any = None, + _rank0_only: bool = False, + **kwargs, + ) -> RolloutDataset | None: + """Generate rollouts via NeMo Gym agents. + + Calls agent /run endpoints, parses responses into padded tensors, + and returns a RolloutDataset for deferred scoring on the main thread. + """ + trainer = self._trainer + is_main = trainer.accelerator.is_main_process + device = trainer.accelerator.device + + if _rank0_only and not is_main: + return None + + # Get prompt batch from iterator + try: + inputs = next(self._prompt_iter) + except StopIteration: + self._prompt_iter = iter(self._prompt_dl) + inputs = next(self._prompt_iter) + + # Extract dataset items for agent calls + dataset_items = [] + for inp in inputs: + prompt_text = "" + prompt = inp.get("prompt", []) + if isinstance(prompt, list) and prompt: + prompt_text = ( + prompt[-1].get("content", "") + if isinstance(prompt[-1], dict) + else str(prompt[-1]) + ) + elif isinstance(prompt, str): + prompt_text = prompt + + # Find the full dataset item, preserving agent_ref for routing + full_item = self._dataset_lookup.get(prompt_text, {}) + item = full_item.get("verify_extra", {}) + if not item: + item = { + "responses_create_params": { + "input": [{"role": "user", "content": prompt_text}] + } + } + # Preserve agent_ref from the dataset row for _call_agents routing + if "agent_ref" in full_item and "agent_ref" not in item: + item["agent_ref"] = full_item["agent_ref"] + dataset_items.append(item) + + # Expand by num_generations (agent produces one rollout per call) + expanded_items = [] + for item in dataset_items: + for _ in range(self._num_generations): + expanded_items.append(item) + + # Call NeMo Gym agents + loop = asyncio.new_event_loop() + try: + responses = loop.run_until_complete( + _call_agents( + dataset_items=expanded_items, + agent_servers=self._agent_servers, + timeout=self._request_timeout, + max_completion_length=trainer.max_completion_length, + temperature=trainer.temperature, + top_p=getattr(trainer, "top_p", None) or 0.999, + ) + ) + finally: + loop.close() + + # Parse responses + eos_token_id = trainer.processing_class.eos_token_id + prompt_ids_list = [] + completion_ids_list = [] + env_mask_list = [] + logprobs_list = [] + rewards_list = [] + + for resp in responses: + parsed = _parse_agent_response(resp, eos_token_id) + prompt_ids_list.append(parsed["prompt_ids"]) + completion_ids_list.append(parsed["completion_ids"]) + env_mask_list.append(parsed["env_mask"]) + logprobs_list.append(parsed["logprobs"]) + rewards_list.append(parsed["reward"]) + + # Pad to tensors + prompt_ids = [torch.tensor(ids, device=device) for ids in prompt_ids_list] + prompt_mask = [torch.ones_like(ids, dtype=torch.long) for ids in prompt_ids] + prompt_ids = pad( + prompt_ids, padding_value=trainer.pad_token_id, padding_side="left" + ) + prompt_mask = pad(prompt_mask, padding_value=0, padding_side="left") + + completion_ids = [ + torch.tensor(ids, device=device) for ids in completion_ids_list + ] + completion_mask = [ + torch.ones_like(ids, dtype=torch.long) for ids in completion_ids + ] + completion_ids = pad( + completion_ids, padding_value=trainer.pad_token_id, padding_side="right" + ) + completion_mask = pad(completion_mask, padding_value=0, padding_side="right") + + # Sampling logprobs from agent (used for IS correction) + sampling_logps = [ + torch.tensor(lp, dtype=torch.float32, device=device) for lp in logprobs_list + ] + sampling_per_token_logps = pad( + sampling_logps, padding_value=0.0, padding_side="right" + ) + + # env_mask as tool_mask (1=model tokens, 0=tool tokens) + tool_mask = [torch.tensor(m, device=device) for m in env_mask_list] + tool_mask = pad(tool_mask, padding_value=1, padding_side="right") + + # Inject rewards into inputs so _compute_deferred_scores can use them + # The deferred scoring path calls _calculate_rewards which reads reward_funcs. + # Our passthrough reward_fn reads "env_reward" from kwargs. + for i, inp in enumerate(inputs): + # Each input gets rewards for its num_generations rollouts + start = i * self._num_generations + end = start + self._num_generations + inp["env_reward"] = rewards_list[start:end] + + # Expand inputs to match expanded rollouts (num_generations copies) + expanded_inputs = [] + for inp in inputs: + for g in range(self._num_generations): + expanded_inp = dict(inp) + expanded_inp["env_reward"] = inp["env_reward"][g] + expanded_inputs.append(expanded_inp) + + # Decode completions for reward functions + completions = trainer.processing_class.batch_decode( + completion_ids, skip_special_tokens=True + ) + + # Build total token count + num_items_in_batch = completion_mask.sum() + + # Build output dict (same shape as _generate_only) + output = { + "prompt_ids": prompt_ids, + "prompt_mask": prompt_mask, + "completion_ids": completion_ids, + "completion_mask": completion_mask, + "num_items_in_batch": num_items_in_batch, + "advantages": torch.zeros(completion_ids.size(0), device=device), + "sampling_per_token_logps": sampling_per_token_logps, + "tool_mask": tool_mask, + # Deferred scoring markers + "_pending_policy_logps": True, + "_deferred_inputs": expanded_inputs, + "_deferred_prompts": [inp.get("prompt", "") for inp in expanded_inputs], + "_deferred_completions": completions, + "_deferred_completion_ids_list": completion_ids_list, + "_rank0_only": _rank0_only, + } + + return RolloutDataset(output) diff --git a/src/axolotl/integrations/nemo_gym/dataset.py b/src/axolotl/integrations/nemo_gym/dataset.py new file mode 100644 index 0000000000..be53da52d8 --- /dev/null +++ b/src/axolotl/integrations/nemo_gym/dataset.py @@ -0,0 +1,135 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# This software may be used and distributed according to +# the terms of the Axolotl Community License Agreement (the "License"); +# you may not use this file except in compliance with the License. + +""" +Dataset loading for NeMo Gym JSONL files. + +Converts NeMo Gym JSONL format into HuggingFace Datasets compatible +with TRL's GRPOTrainer. Supports multi-environment routing via: + 1. Per-dataset server_name (all rows in a file go to one server) + 2. Per-row agent_ref.name (each row specifies its own server) +""" + +from __future__ import annotations + +import json +import os +import random + +from datasets import Dataset + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def load_nemo_gym_datasets( + gym_dir: str, + dataset_configs: list[dict], +) -> Dataset: + """Load and merge NeMo Gym JSONL datasets with multi-environment support. + + Each dataset config should have: + - path: JSONL file path (absolute, or relative to gym_dir) + - server_name: Default NeMo Gym server for this dataset. + Can be overridden per-row if the JSONL has an "agent_ref" field. + - max_samples (optional): Max number of samples to use from this dataset + + Per-row routing: If a JSONL row has an "agent_ref": {"name": "..."} field, + that takes precedence over the dataset-level server_name. This allows mixing + environments within a single dataset file (matching TRL's pattern). + + The output dataset has columns: + - prompt: list[dict] chat format + - resources_server_ref: dict with {"name": server_name} + - verify_extra: dict with original JSONL data for verify requests + + Args: + gym_dir: Path to the NeMo Gym directory. + dataset_configs: List of dataset configuration dicts. + + Returns: + A HuggingFace Dataset ready for GRPOTrainer. + """ + all_examples = [] + + for ds_cfg in dataset_configs: + path = ds_cfg["path"] + default_server = ds_cfg.get("server_name", "") + max_samples = ds_cfg.get("max_samples") + + # Resolve path + if not os.path.isabs(path): + path = os.path.join(gym_dir, path) + path = os.path.expanduser(path) + + if not os.path.exists(path): + raise FileNotFoundError( + f"NeMo Gym dataset not found at {path}. " + "Ensure the dataset file exists or run the appropriate " + "NeMo Gym dataset creation script." + ) + + LOG.info( + f"Loading NeMo Gym dataset from {path} (default server: {default_server})" + ) + + with open(path, encoding="utf-8") as f: + lines = f.readlines() + + if max_samples and len(lines) > max_samples: + lines = random.sample(lines, max_samples) # nosec B311 + + for line in lines: + data = json.loads(line) + + # Extract user prompt from the input messages + inputs = data.get("responses_create_params", {}).get("input", []) + task_prompt = "" + for inp in inputs: + if isinstance(inp, dict) and inp.get("role") in ("user",): + task_prompt = inp.get("content", "") + break + if not task_prompt and inputs: + # Fallback: use the last input's content + task_prompt = ( + inputs[-1].get("content", "") + if isinstance(inputs[-1], dict) + else "" + ) + + # Per-row agent routing: agent_ref.name can override dataset-level server_name. + # NeMo Gym datasets may use agent names (e.g., "reasoning_gym_simple_agent") + # which differ from resource server names (e.g., "reasoning_gym"). + # The dataset-level server_name is always the fallback. + row_agent_ref = data.get("agent_ref", {}) + server_name = default_server + if row_agent_ref and row_agent_ref.get("name"): + # Use per-row name, but only if it looks like a resource server name. + # Agent names typically have "_simple_agent" or "_agent" suffix. + row_name = row_agent_ref["name"] + if row_agent_ref.get("type") != "responses_api_agents": + # Not an agent — could be a direct resource server reference + server_name = row_name + + all_examples.append( + { + "prompt": [{"role": "user", "content": task_prompt}], + "resources_server_ref": {"name": server_name}, + "verify_extra": data, + } + ) + + random.shuffle(all_examples) + + # Log environment distribution + env_counts: dict[str, int] = {} + for ex in all_examples: + name = ex["resources_server_ref"]["name"] + env_counts[name] = env_counts.get(name, 0) + 1 + LOG.info(f"Loaded {len(all_examples)} NeMo Gym examples: {env_counts}") + + return Dataset.from_list(all_examples) diff --git a/src/axolotl/integrations/nemo_gym/examples/nemo_gym_multi_env.yaml b/src/axolotl/integrations/nemo_gym/examples/nemo_gym_multi_env.yaml new file mode 100644 index 0000000000..5a86f7578e --- /dev/null +++ b/src/axolotl/integrations/nemo_gym/examples/nemo_gym_multi_env.yaml @@ -0,0 +1,64 @@ +# Axolotl + NeMo Gym: Multi-Environment RL Training Example +# +# Trains on multiple NeMo Gym environments simultaneously: +# - Mini Sudoku (reasoning_gym) +# - Instruction Following +# +# Prerequisites: +# - uv (https://github.com/astral-sh/uv) installed +# - Download instruction_following.jsonl from nvidia/Nemotron-RL-instruction_following +# and place it at ~/Gym/resources_servers/instruction_following/data/instruction_following.jsonl +# +# Usage: +# axolotl train examples/nemo_gym_multi_env.yaml + +base_model: Qwen/Qwen2.5-1.5B-Instruct +model_type: AutoModelForCausalLM + +sequence_len: 4096 +load_in_4bit: false + +# RL configuration +rl: grpo +chat_template: tokenizer_default + +# GRPO / TRL settings +trl: + use_vllm: false + num_generations: 8 + max_completion_length: 2048 + +# NeMo Gym plugin +plugins: + - axolotl.integrations.nemo_gym.NemoGymPlugin + +nemo_gym_enabled: true +nemo_gym_dir: ~/Gym +nemo_gym_auto_clone: true +nemo_gym_auto_start: true +nemo_gym_config_paths: + - resources_servers/reasoning_gym/configs/resources_only.yaml + - resources_servers/instruction_following/configs/instruction_following.yaml +nemo_gym_datasets: + - path: resources_servers/reasoning_gym/data/train_mini_sudoku.jsonl + server_name: reasoning_gym + max_samples: 1000 + - path: resources_servers/instruction_following/data/instruction_following.jsonl + server_name: instruction_following + max_samples: 1000 + +# Training hyperparameters +learning_rate: 1.0e-5 +weight_decay: 0.001 +lr_scheduler: linear +warmup_ratio: 0.0 +optimizer: adamw_8bit + +num_epochs: 1 +max_steps: 100 +micro_batch_size: 1 +gradient_accumulation_steps: 64 + +logging_steps: 1 +save_steps: 100 +output_dir: ./outputs/nemo_gym_multi_env diff --git a/src/axolotl/integrations/nemo_gym/examples/nemo_gym_multi_turn.yaml b/src/axolotl/integrations/nemo_gym/examples/nemo_gym_multi_turn.yaml new file mode 100644 index 0000000000..2644bbab7f --- /dev/null +++ b/src/axolotl/integrations/nemo_gym/examples/nemo_gym_multi_turn.yaml @@ -0,0 +1,79 @@ +# Axolotl + NeMo Gym: Multi-Turn Tool-Use RL Training Example +# +# This config trains a model with multi-turn reinforcement learning +# using NeMo Gym's agentic environments. The model generates responses, +# executes tool calls against NeMo Gym resource servers, and receives +# environment feedback across multiple turns. +# +# Multi-turn mode uses TRL's rollout_func with env_mask to ensure only +# model-generated tokens contribute to the training loss. +# +# Prerequisites: +# - uv (https://github.com/astral-sh/uv) installed +# - vLLM must be enabled (required for generate_rollout_completions) +# +# Usage: +# # Terminal 1: Start vLLM server +# trl vllm-serve --model Qwen/Qwen2.5-1.5B-Instruct +# +# # Terminal 2: Train +# axolotl train examples/nemo_gym_multi_turn.yaml + +base_model: Qwen/Qwen2.5-1.5B-Instruct +model_type: AutoModelForCausalLM + +sequence_len: 4096 +load_in_4bit: false + +# RL configuration +rl: grpo +chat_template: tokenizer_default + +# GRPO / TRL settings — vLLM required for multi-turn +trl: + use_vllm: true + vllm_mode: server + vllm_server_host: localhost + vllm_server_port: 8000 + num_generations: 4 + max_completion_length: 2048 + +# NeMo Gym plugin with multi-turn enabled +plugins: + - axolotl.integrations.nemo_gym.NemoGymPlugin + +nemo_gym_enabled: true +nemo_gym_dir: ~/Gym +nemo_gym_auto_clone: true +nemo_gym_auto_start: true +nemo_gym_head_port: 11000 +nemo_gym_server_timeout: 360 +nemo_gym_verify_timeout: 30 + +# Multi-turn settings +nemo_gym_multi_turn: true +nemo_gym_max_turns: 10 + +# Resource server configs — use environments that support tool calls +nemo_gym_config_paths: + - resources_servers/reasoning_gym/configs/resources_only.yaml +nemo_gym_datasets: + - path: resources_servers/reasoning_gym/data/train_mini_sudoku.jsonl + server_name: reasoning_gym + max_samples: 2000 + +# Training hyperparameters +learning_rate: 1.0e-5 +weight_decay: 0.001 +lr_scheduler: linear +warmup_ratio: 0.0 +optimizer: adamw_8bit + +num_epochs: 1 +max_steps: 100 +micro_batch_size: 1 +gradient_accumulation_steps: 64 + +logging_steps: 1 +save_steps: 100 +output_dir: ./outputs/nemo_gym_multi_turn diff --git a/src/axolotl/integrations/nemo_gym/examples/nemo_gym_sudoku.yaml b/src/axolotl/integrations/nemo_gym/examples/nemo_gym_sudoku.yaml new file mode 100644 index 0000000000..df04b2b415 --- /dev/null +++ b/src/axolotl/integrations/nemo_gym/examples/nemo_gym_sudoku.yaml @@ -0,0 +1,62 @@ +# Axolotl + NeMo Gym: Sudoku RL Training Example +# +# This config trains a model to solve 4x4 Mini Sudoku puzzles using GRPO +# with NeMo Gym's reasoning_gym environment as the reward signal. +# +# Prerequisites: +# - uv (https://github.com/astral-sh/uv) installed +# - Git installed +# - The NeMo Gym repo will be auto-cloned to ~/Gym on first run +# +# Usage: +# axolotl train examples/nemo_gym_sudoku.yaml + +base_model: Qwen/Qwen2.5-1.5B-Instruct +model_type: AutoModelForCausalLM + +sequence_len: 4096 +load_in_4bit: false + +# RL configuration +rl: grpo +chat_template: tokenizer_default + +# GRPO / TRL settings +trl: + use_vllm: false + num_generations: 8 + max_completion_length: 2048 + +# NeMo Gym plugin +plugins: + - axolotl.integrations.nemo_gym.NemoGymPlugin + +nemo_gym_enabled: true +nemo_gym_dir: ~/Gym +nemo_gym_auto_clone: true +nemo_gym_auto_start: true +nemo_gym_head_port: 11000 +nemo_gym_server_timeout: 360 +nemo_gym_verify_timeout: 30 +nemo_gym_config_paths: + - resources_servers/reasoning_gym/configs/resources_only.yaml +nemo_gym_datasets: + - path: resources_servers/reasoning_gym/data/train_mini_sudoku.jsonl + server_name: reasoning_gym + max_samples: 2000 + +# Training hyperparameters +learning_rate: 1.0e-5 +weight_decay: 0.001 +lr_scheduler: linear +warmup_ratio: 0.0 +optimizer: adamw_8bit + +num_epochs: 1 +max_steps: 100 +micro_batch_size: 1 +gradient_accumulation_steps: 64 + +logging_steps: 1 +save_steps: 100 +output_dir: ./outputs/nemo_gym_sudoku diff --git a/src/axolotl/integrations/nemo_gym/multi_turn.py b/src/axolotl/integrations/nemo_gym/multi_turn.py new file mode 100644 index 0000000000..328a393bc3 --- /dev/null +++ b/src/axolotl/integrations/nemo_gym/multi_turn.py @@ -0,0 +1,329 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# This software may be used and distributed according to +# the terms of the Axolotl Community License Agreement (the "License"); +# you may not use this file except in compliance with the License. + +""" +Multi-turn rollout function for NeMo Gym environments. + +Delegates multi-turn orchestration to NeMo Gym's agent servers via the /run +endpoint. The agent handles generation (by calling our vLLM server), tool +execution, session management, and reward computation. + +This follows the same pattern as TRL's reference implementation at +examples/scripts/nemo_gym/train_multi_environment.py. + +Architecture: + rollout_func(prompts, trainer) + -> expand prompts by num_generations + -> async POST /run to agent servers (one per sample) + -> parse response: prompt_ids, completion_ids, logprobs, env_mask, reward + -> return to TRL for GRPO training +""" + +from __future__ import annotations + +import asyncio +from typing import Any + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def create_nemo_gym_rollout_func( + agent_servers: dict[str, str], + dataset_lookup: dict[int, dict], + request_timeout: float = 10800, +): + """Create a TRL-compatible rollout_func that delegates to NeMo Gym agents. + + Args: + agent_servers: Mapping of agent_name → agent URL (e.g., {"simple_agent": "http://host:port"}). + dataset_lookup: Mapping of dataset index → full JSONL row dict. + request_timeout: HTTP timeout for /run requests. + + Returns: + A rollout_func with signature (prompts: list[str], trainer) -> dict. + """ + + def rollout_func(prompts: list[str], trainer) -> dict[str, Any]: + is_training = trainer.model.training + num_generations = ( + trainer.num_generations + if is_training + else getattr(trainer, "num_generations_eval", 1) + ) + temperature = trainer.temperature + top_p = getattr(trainer, "top_p", None) or 0.999 + max_completion_length = trainer.max_completion_length + eos_token_id = trainer.processing_class.eos_token_id + + # Expand prompts: each prompt index repeated num_generations times + expanded_items = [] + expanded_prompt_indices = [] + for prompt_str in prompts: + # Prompts from TRL are chat-templated strings. Find the dataset item + # by matching against dataset_lookup keys (raw user message text). + full_item = None + for key, val in dataset_lookup.items(): + if isinstance(key, str) and prompt_str == key: + full_item = val + break + + if full_item is None: + full_item = { + "responses_create_params": { + "input": [{"role": "user", "content": prompt_str}] + } + } + + for _ in range(num_generations): + # Preserve agent_ref for routing in _call_agents + dispatched: dict = full_item.get("verify_extra", full_item) # type: ignore[assignment] + if isinstance(dispatched, dict) and "agent_ref" not in dispatched: + agent_ref = full_item.get("agent_ref") + if agent_ref: + dispatched = {**dispatched, "agent_ref": agent_ref} + expanded_items.append(dispatched) + expanded_prompt_indices.append(prompt_str) + + # Call NeMo Gym agents + loop = asyncio.new_event_loop() + try: + responses = loop.run_until_complete( + _call_agents( + dataset_items=expanded_items, + agent_servers=agent_servers, + timeout=request_timeout, + max_completion_length=max_completion_length, + temperature=temperature, + top_p=top_p, + ) + ) + finally: + loop.close() + + # Parse responses into rollout format + all_prompt_ids = [] + all_completion_ids = [] + all_env_masks = [] + all_logprobs = [] + all_rewards = [] + all_num_turns = [] + + for _i, response in enumerate(responses): + result = _parse_agent_response(response, eos_token_id) + all_prompt_ids.append(result["prompt_ids"]) + all_completion_ids.append(result["completion_ids"]) + all_env_masks.append(result["env_mask"]) + all_logprobs.append(result["logprobs"]) + all_rewards.append(result["reward"]) + all_num_turns.append(result["num_turns"]) + + # TRL expects prompt_ids to be unique (one per original prompt, not per generation) + unique_prompt_ids = all_prompt_ids[::num_generations] + + # Wrap logprobs for TRL: list[list[list[float]]] + def _normalize(lp): + while isinstance(lp, (list, tuple)) and len(lp) > 0: + lp = lp[0] + return float(lp) if lp is not None else 0.0 + + wrapped_logprobs = [[[_normalize(lp)] for lp in seq] for seq in all_logprobs] + + return { + "prompt_ids": unique_prompt_ids, + "completion_ids": all_completion_ids, + "env_mask": all_env_masks, + "logprobs": wrapped_logprobs, + "logprob_token_ids": None, # nosec B105 + "env_reward": all_rewards, + "num_turns": all_num_turns, + } + + return rollout_func + + +async def _call_agents( + dataset_items: list[dict], + agent_servers: dict[str, str], + timeout: float, + max_completion_length: int = 4096, + temperature: float = 1.0, + top_p: float = 0.999, +) -> list[dict]: + """Async batch POST to NeMo Gym agent /run endpoints.""" + import aiohttp + + results = [] + connector = aiohttp.TCPConnector(limit_per_host=64, limit=256) + # Use sock_read for per-request timeout (not total session timeout). + # This ensures a single stuck generation doesn't block all other requests. + client_timeout = aiohttp.ClientTimeout(total=None, sock_read=timeout) + + async with aiohttp.ClientSession( + connector=connector, timeout=client_timeout, cookie_jar=aiohttp.DummyCookieJar() + ) as session: + tasks = [] + for item in dataset_items: + agent_ref = item.get("agent_ref", {}) + agent_name = agent_ref.get("name", "") + agent_url = agent_servers.get(agent_name, "") + + if not agent_url: + # Fallback: try first available agent + if agent_servers: + agent_url = next(iter(agent_servers.values())) + else: + results.append( + { + "response": {"output": []}, + "reward": 0.0, + "error": "No agent server", + } + ) + continue + + # Build request body + request_body = dict(item) + params = request_body.setdefault("responses_create_params", {}) + params.setdefault("max_output_tokens", max_completion_length) + params["temperature"] = temperature + params["top_p"] = top_p + + tasks.append(_post_run(session, agent_url, request_body)) + + if tasks: + responses = await asyncio.gather(*tasks, return_exceptions=True) + for resp in responses: + if isinstance(resp, BaseException): + LOG.warning(f"Agent /run failed: {resp}") + results.append( + {"response": {"output": []}, "reward": 0.0, "error": str(resp)} + ) + else: + results.append(resp) + + return results + + +async def _post_run(session, agent_url: str, body: dict) -> dict: + """POST to agent /run endpoint.""" + async with session.post(f"{agent_url}/run", json=body) as resp: + if resp.status == 200: + return await resp.json() + text = await resp.text() + return { + "response": {"output": []}, + "reward": 0.0, + "error": f"HTTP {resp.status}: {text[:200]}", + } + + +def _parse_agent_response(response: dict, eos_token_id: int) -> dict: + """Parse NeMo Gym agent /run response into rollout format. + + The agent returns: + response.output[]: list of turns, each with prompt_token_ids, + generation_token_ids, generation_log_probs + reward: float + """ + # Defaults for failed/empty responses + defaults = { + "prompt_ids": [eos_token_id], + "completion_ids": [eos_token_id], + "env_mask": [0], + "logprobs": [0.0], + "reward": 0.0, + "num_turns": 0, + } + + if not isinstance(response, dict) or "error" in response: + return defaults + + output_items = response.get("response", {}).get("output", []) + reward = float(response.get("reward", 0.0)) + + if not output_items: + defaults["reward"] = reward + return defaults + + # Check at least one valid output + has_valid = False + for item in output_items: + if item.get("type") == "function_call": + has_valid = True + break + if item.get("type") == "message": + for c in item.get("content", []): + if ( + isinstance(c, dict) + and c.get("type") == "output_text" + and c.get("text", "").strip() + ): + has_valid = True + break + if has_valid: + break + + if not has_valid: + defaults["reward"] = reward + return defaults + + # Extract multi-turn token sequences + first_prompt_ids = None + completion_ids = [] + env_mask = [] + logprobs = [] + seen_token_ids = [] + num_turns = 0 + + for item in output_items: + prompt_token_ids = item.get("prompt_token_ids", []) + generation_token_ids = item.get("generation_token_ids", []) + generation_log_probs = item.get("generation_log_probs", []) + + if not generation_token_ids: + continue + + num_turns += 1 + + # First turn: capture prompt + if first_prompt_ids is None: + first_prompt_ids = list(prompt_token_ids) + seen_token_ids = list(prompt_token_ids) + else: + # Subsequent turns: extract tool result tokens (between turns) + if len(prompt_token_ids) > len(seen_token_ids): + tool_result_tokens = prompt_token_ids[len(seen_token_ids) :] + # Tool result tokens are NOT trained on (env_mask = 0) + completion_ids.extend(tool_result_tokens) + env_mask.extend([0] * len(tool_result_tokens)) + logprobs.extend([0.0] * len(tool_result_tokens)) + + # Add generation tokens (trained on, env_mask = 1) + completion_ids.extend(generation_token_ids) + env_mask.extend([1] * len(generation_token_ids)) + + # Pad logprobs if shorter than generation tokens + gen_logprobs = list(generation_log_probs) if generation_log_probs else [] + if len(gen_logprobs) < len(generation_token_ids): + gen_logprobs.extend([0.0] * (len(generation_token_ids) - len(gen_logprobs))) + logprobs.extend(gen_logprobs[: len(generation_token_ids)]) + + # Update seen tokens + seen_token_ids = list(prompt_token_ids) + list(generation_token_ids) + + if first_prompt_ids is None: + return defaults + + return { + "prompt_ids": first_prompt_ids, + "completion_ids": completion_ids, + "env_mask": env_mask, + "logprobs": logprobs, + "reward": reward, + "num_turns": num_turns, + } diff --git a/src/axolotl/integrations/nemo_gym/plugin.py b/src/axolotl/integrations/nemo_gym/plugin.py new file mode 100644 index 0000000000..14de684cfe --- /dev/null +++ b/src/axolotl/integrations/nemo_gym/plugin.py @@ -0,0 +1,503 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# This software may be used and distributed according to +# the terms of the Axolotl Community License Agreement (the "License"); +# you may not use this file except in compliance with the License. + +""" +NeMo Gym Plugin for Axolotl. + +Integrates NVIDIA NeMo Gym environments as reward sources for GRPO training. +Handles server lifecycle, dataset loading, and reward function wiring. + +Supports two modes: + - Single-turn (default): reward_fn calls /verify after each generation + - Multi-turn (nemo_gym_multi_turn: true): rollout_func orchestrates + multi-step interactions with tool execution via resource servers +""" + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING, Union + +from axolotl.integrations.base import BasePlugin +from axolotl.utils.logging import get_logger + +if TYPE_CHECKING: + from axolotl.common.datasets import TrainDatasetMeta + +LOG = get_logger(__name__) + + +class NemoGymPlugin(BasePlugin): + """Plugin for NVIDIA NeMo Gym integration with Axolotl. + + When enabled, this plugin: + 1. Clones and sets up the NeMo Gym repo (if needed) + 2. Starts NeMo Gym resource servers + 3. Loads datasets from NeMo Gym JSONL files + 4. For single-turn: creates a reward function calling /verify + 5. For multi-turn: creates a rollout_func with tool execution and env_mask + """ + + def __init__(self): + super().__init__() + self._gym_dir = None + self._global_config = None + self._verify_endpoints = None + self._server_base_urls = None + self._reward_fn = None + self._dataset_lookup = None + self._agent_servers = {} + + def get_input_args(self): + return "axolotl.integrations.nemo_gym.NemoGymArgs" + + def pre_model_load(self, cfg): + """Apply monkeypatches before trainer creation.""" + if not cfg.nemo_gym_enabled: + return + + # Always skip NCCL communicator init in NeMo Gym mode. + # NeMo Gym uses its own vLLM server (standard OpenAI API), not the TRL + # colocate/NCCL path. The NCCL init fails with vLLM V1 and standard servers. + trl_cfg = getattr(cfg, "trl", None) + if trl_cfg and getattr(trl_cfg, "vllm_mode", "server") == "server": + self._patch_skip_nccl_init() + + def _patch_skip_nccl_init(self): + """Monkeypatch VLLMClient.init_communicator to no-op. + + NeMo Gym uses its own vLLM server (standard OpenAI API or custom LoRA + serve script). The NCCL communicator is not needed and fails with both + vLLM V1 engine and standard OpenAI server mode. + """ + try: + from trl.generation.vllm_client import VLLMClient + + VLLMClient._original_init_communicator = VLLMClient.init_communicator + VLLMClient.init_communicator = lambda self, **kwargs: LOG.info( + "Skipping NCCL init_communicator (LoRA sync mode)" + ) + LOG.info("Patched VLLMClient.init_communicator to no-op for LoRA sync") + except Exception as exc: + LOG.warning(f"Failed to patch VLLMClient: {exc}") + + def register(self, cfg): + if not cfg.get("nemo_gym_enabled"): + return + + LOG.info("NeMo Gym integration enabled") + gym_dir = cfg.get("nemo_gym_dir") or os.path.expanduser("~/Gym") + auto_clone = cfg.get("nemo_gym_auto_clone", True) + auto_start = cfg.get("nemo_gym_auto_start", True) + head_port = cfg.get("nemo_gym_head_port", 11000) + server_timeout = cfg.get("nemo_gym_server_timeout", 360) + + from .server import ( + ensure_gym_repo, + ensure_gym_venv, + get_agent_servers, + get_server_base_url, + get_server_configs, + get_verify_endpoint, + start_servers, + wait_for_resource_servers, + ) + + self._gym_dir = ensure_gym_repo(gym_dir, auto_clone=auto_clone) + + if auto_start: + config_paths = cfg.get("nemo_gym_config_paths", []) + ensure_gym_venv(self._gym_dir) + start_servers( + self._gym_dir, + config_paths, + head_port=head_port, + timeout=server_timeout, + ) + + self._global_config = get_server_configs(head_port=head_port) + wait_for_resource_servers(self._global_config, timeout=server_timeout) + + # Build endpoint maps for resource servers (/verify) + self._verify_endpoints = {} + self._server_base_urls = {} + for server_name in self._global_config: + try: + self._verify_endpoints[server_name] = get_verify_endpoint( + self._global_config, server_name + ) + self._server_base_urls[server_name] = get_server_base_url( + self._global_config, server_name + ) + except (ValueError, KeyError, TypeError): + pass + + # Discover agent servers (/run) for multi-turn + self._agent_servers = get_agent_servers(self._global_config) + + # Pre-build dataset lookup for multi-turn (needs to happen at register time, + # not load_datasets, because load_datasets may not be called if axolotl config + # has its own datasets field) + if cfg.get("nemo_gym_multi_turn") and cfg.get("nemo_gym_datasets"): + from .dataset import load_nemo_gym_datasets + + gym_dir = cfg.get("nemo_gym_dir") or os.path.expanduser("~/Gym") + dataset = load_nemo_gym_datasets(gym_dir, cfg["nemo_gym_datasets"]) + self._dataset_lookup = {} + for i in range(len(dataset)): + row = dataset[i] + # Use last message content as key (matches data_producer lookup) + prompt_text = row["prompt"][-1]["content"] + self._dataset_lookup[prompt_text] = row + LOG.info(f"Built dataset lookup with {len(self._dataset_lookup)} entries") + + multi_turn = cfg.get("nemo_gym_multi_turn", False) + LOG.info( + f"NeMo Gym ready with servers: {list(self._verify_endpoints.keys())} " + f"(multi_turn={'enabled' if multi_turn else 'disabled'})" + ) + + def load_datasets(self, cfg, preprocess=False) -> Union["TrainDatasetMeta", None]: + if not cfg.nemo_gym_enabled: + return None + + from axolotl.common.datasets import TrainDatasetMeta + + from .dataset import load_nemo_gym_datasets + + dataset_configs = cfg.nemo_gym_datasets + dataset = load_nemo_gym_datasets(self._gym_dir, dataset_configs) + + # Build prompt → row lookup for multi-turn rollout_func + # (rollout_func only receives prompt text, needs to look up row data) + self._dataset_lookup = {} + for i in range(len(dataset)): + row = dataset[i] + # Use last message content as key (matches data_producer lookup) + prompt_text = row["prompt"][-1]["content"] + self._dataset_lookup[prompt_text] = row + + return TrainDatasetMeta( + train_dataset=dataset, + eval_dataset=None, + total_num_steps=0, # computed later by the builder + ) + + def get_training_args(self, cfg): + """Pass through vLLM settings and force async trainer for multi-turn.""" + args = {} + # Pass vLLM settings from vllm config block to TRL training args + if cfg.vllm: + vllm_cfg = cfg.vllm + max_len = getattr(vllm_cfg, "max_model_len", None) + gpu_util = getattr(vllm_cfg, "gpu_memory_utilization", None) + tp_size = getattr(vllm_cfg, "tensor_parallel_size", None) + if max_len: + args["vllm_max_model_length"] = max_len + if gpu_util: + args["vllm_gpu_memory_utilization"] = gpu_util + if tp_size: + args["vllm_tensor_parallel_size"] = tp_size + + # Force async trainer for multi-turn: NemoGymDataProducer needs the + # data producer protocol. Setting use_data_producer=True selects + # AxolotlAsyncGRPOTrainer which supports _create_data_producer(). + # With async_prefetch=False this runs synchronously — no threading. + if cfg.nemo_gym_multi_turn and self._agent_servers: + args["use_data_producer"] = True + LOG.info( + "NeMo Gym multi-turn: forcing use_data_producer=True for data producer protocol" + ) + + # Dataloader workers fork subprocesses that can't handle the async + # HTTP connections to NeMo Gym agents. Force num_workers=0. + if getattr(cfg, "dataloader_num_workers", None) not in (None, 0): + LOG.warning( + f"NeMo Gym: overriding dataloader_num_workers={cfg.dataloader_num_workers} → 0 " + "(forked workers can't use NeMo Gym agent connections)" + ) + cfg.dataloader_num_workers = 0 + + if args: + LOG.info(f"NeMo Gym plugin injecting training args: {args}") + return args if args else None + + def post_trainer_create(self, cfg, trainer): + """Wire NeMo Gym into the trainer (reward_fn or rollout_func).""" + if not cfg.nemo_gym_enabled: + return + + model_name = cfg.nemo_gym_model_name or cfg.base_model or "axolotl-model" + verify_timeout = cfg.nemo_gym_verify_timeout or 30 + multi_turn = cfg.nemo_gym_multi_turn or False + + # Handle weight sync. NeMo Gym skips NCCL init, so we need to either: + # - Install LoRA sync (when vllm_lora_sync=True) + # - Or no-op sync_weights (when using standard vLLM server) + trl_cfg = getattr(cfg, "trl", None) + if hasattr(trainer, "vllm_generation") and trainer.vllm_generation: + vllm_gen = trainer.vllm_generation + if trl_cfg and getattr(trl_cfg, "vllm_lora_sync", False): + self._setup_lora_sync(trainer) + # Verify the vLLM server supports runtime LoRA loading + self._check_lora_endpoint(vllm_gen) + else: + # No NCCL, no LoRA sync — skip all weight sync paths + vllm_gen.sync_weights = lambda: LOG.debug( + "Weight sync skipped (NeMo Gym mode)" + ) + type(vllm_gen).sync_weights = lambda self: LOG.debug( + "Weight sync skipped (NeMo Gym mode)" + ) + # Also patch the async trainer's internal sync method + if hasattr(trainer, "_maybe_sync_vllm_weights"): + trainer._maybe_sync_vllm_weights = lambda: LOG.debug( + "Async weight sync skipped (NeMo Gym mode)" + ) + LOG.info("Disabled weight sync (NeMo Gym mode, no LoRA sync)") + + if multi_turn: + self._wire_multi_turn(cfg, trainer, model_name, verify_timeout) + else: + self._wire_single_turn(trainer, model_name, verify_timeout) + + def _wire_single_turn(self, trainer, model_name, verify_timeout): + """Inject single-turn reward function into the trainer.""" + from .rewards import create_nemo_gym_reward_fn + + self._reward_fn = create_nemo_gym_reward_fn( + global_config=self._global_config, + verify_endpoints=self._verify_endpoints, + model_name=model_name, + verify_timeout=verify_timeout, + ) + + if hasattr(trainer, "reward_funcs"): + trainer.reward_funcs.append(self._reward_fn) + trainer.reward_func_names.append("nemo_gym") + trainer.reward_processing_classes.append(None) + LOG.info( + f"Added NeMo Gym reward function (single-turn). " + f"Total reward functions: {len(trainer.reward_funcs)}" + ) + else: + LOG.warning( + "Trainer does not have reward_funcs attribute. " + "NeMo Gym reward function not injected. " + "Ensure you are using a GRPO trainer." + ) + + def _wire_multi_turn(self, cfg, trainer, model_name, verify_timeout): + """Replace the data producer with NemoGymDataProducer. + + The plugin forces use_data_producer=True (in get_training_args) which + selects AxolotlAsyncGRPOTrainer. Here we swap its data_producer with + our NemoGymDataProducer that calls agent /run instead of vLLM generate. + """ + if not self._agent_servers: + LOG.warning( + "No NeMo Gym agent servers discovered. Multi-turn requires agent servers " + "started via ng_run with an agent config. Falling back to single-turn." + ) + self._wire_single_turn(trainer, model_name, verify_timeout) + return + + if not hasattr(trainer, "data_producer") or trainer.data_producer is None: + LOG.warning( + "Trainer has no data_producer. NeMo Gym multi-turn requires " + "use_data_producer=true (should be auto-set by plugin)." + ) + return + + from axolotl.core.trainers.grpo.async_trainer import AsyncDataProducer + + from .data_producer import NemoGymDataProducer + + # Get the current producer's config and params + current = trainer.data_producer + # Unwrap AsyncDataProducer to get the inner producer's config + if isinstance(current, AsyncDataProducer): + inner = current._inner + else: + inner = current + + nemo_producer = NemoGymDataProducer( + config=inner.config, + prompt_dataset=inner._dataset, + num_generations=inner._num_generations, + generation_batch_size=inner._generation_batch_size, + train_batch_size=inner._train_batch_size, + steps_per_generation=inner._steps_per_generation, + shuffle_dataset=inner._shuffle_dataset, + seed=inner._seed, + agent_servers=self._agent_servers, + dataset_lookup=self._dataset_lookup or {}, + request_timeout=float(cfg.nemo_gym_run_timeout or 300), + ) + nemo_producer.set_trainer(trainer) + + # Re-wrap in AsyncDataProducer if async prefetch is enabled + if getattr(trainer.args, "async_prefetch", False): + nemo_producer = AsyncDataProducer( + nemo_producer, + background_produce_kwargs={"skip_policy_logps": True}, + ) + + trainer.data_producer = nemo_producer + LOG.info( + f"NeMo Gym data producer installed " + f"(agent servers: {list(self._agent_servers.keys())}, " + f"async={'yes' if getattr(trainer.args, 'async_prefetch', False) else 'no'})" + ) + + # Passthrough reward function — agent /run already computed rewards + from .rewards import reward_env + + if hasattr(trainer, "reward_funcs"): + trainer.reward_funcs.append(reward_env) + trainer.reward_func_names.append("nemo_gym") + trainer.reward_processing_classes.append(None) + + @staticmethod + def _check_lora_endpoint(vllm_gen): + """Verify the vLLM server supports runtime LoRA loading.""" + import requests as http_requests + + if not hasattr(vllm_gen, "vllm_client") or vllm_gen.vllm_client is None: + return # Non-main rank in multi-GPU — client only exists on rank 0 + base_url = vllm_gen.vllm_client.base_url + try: + # Send a dummy load request — if the endpoint exists, we get a + # proper error (400/404 about the adapter), not a route 404. + resp = http_requests.post( + f"{base_url}/v1/load_lora_adapter", + json={"lora_name": "__probe__", "lora_path": "/nonexistent"}, + timeout=5, + ) + if ( + resp.status_code == 404 + and "Not Found" in resp.text + and "adapter" not in resp.text.lower() + ): + LOG.warning( + "vLLM server does not expose /v1/load_lora_adapter. " + "Set VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 when starting vLLM, e.g.:\n" + " VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 python -m vllm.entrypoints.openai.api_server " + "--enable-lora --max-lora-rank 64 ..." + ) + except Exception: + pass # Server might not be up yet, sync will warn later + + def _setup_lora_sync(self, trainer): + """Replace sync_weights with LoRA adapter sync via filesystem + HTTP. + + If the async trainer is detected (has ``_sync_lora_adapter``), delegates + to it — that method already handles multi-GPU (FSDP/DeepSpeed state_dict + gather, broadcast sync dir, barrier). + + Otherwise installs a standalone closure for the non-async GRPO path that + saves the adapter and POSTs to ``/v1/load_lora_adapter``. + """ + vllm_gen = trainer.vllm_generation + + # Async trainer path: delegate to its _sync_lora_adapter (multi-GPU safe) + if hasattr(trainer, "_sync_lora_adapter"): + + def lora_sync_weights(): + trainer._sync_lora_adapter() + + vllm_gen.sync_weights = lora_sync_weights + type(vllm_gen).sync_weights = lambda self: lora_sync_weights() + LOG.info( + "Installed LoRA adapter sync " + "(delegates to async trainer._sync_lora_adapter)" + ) + return + + # Non-async standard GRPO path: standalone closure + import os + import shutil + import tempfile + + import requests as http_requests + + base_model = getattr(trainer.args, "model_name_or_path", None) or "axolotl-lora" + sync_state = {"version": 0, "sync_dir": tempfile.mkdtemp(prefix="lora_sync_")} + + def lora_sync_weights(): + """Save LoRA adapter and load it into vLLM.""" + accelerator = vllm_gen.accelerator + model = vllm_gen.model + + if vllm_gen.mode != "server": + return + + sync_state["version"] += 1 + version = sync_state["version"] + adapter_path = os.path.join(sync_state["sync_dir"], f"v{version}") + + wrapped_model = getattr(trainer, "model_wrapped", model) + state_dict = accelerator.get_state_dict(wrapped_model) + + if accelerator.is_main_process: + unwrapped = accelerator.unwrap_model(model) + unwrapped.save_pretrained(adapter_path, state_dict=state_dict) + + base_url = vllm_gen.vllm_client.base_url + resp = http_requests.post( + f"{base_url}/v1/load_lora_adapter", + json={ + "lora_name": base_model, + "lora_path": adapter_path, + "load_inplace": True, + }, + timeout=30, + ) + if resp.status_code != 200: + resp = http_requests.post( + f"{base_url}/set_lora_adapter/", + json={ + "lora_name": "active_lora", + "lora_int_id": version, + "lora_path": adapter_path, + }, + timeout=30, + ) + if resp.status_code != 200: + LOG.warning( + f"Failed to set LoRA adapter: " + f"{resp.status_code} {resp.text}" + ) + return + + try: + vllm_gen.vllm_client.reset_prefix_cache() + except Exception as exc: + LOG.warning("Failed to reset prefix cache: %s", exc) + + if version > 1: + old = os.path.join(sync_state["sync_dir"], f"v{version - 1}") + if os.path.exists(old): + shutil.rmtree(old, ignore_errors=True) + + LOG.info(f"Synced LoRA adapter v{version} to vLLM ({adapter_path})") + + if accelerator.num_processes > 1: + import torch.distributed as dist + + if dist.is_initialized(): + dist.barrier() + + vllm_gen.sync_weights = lora_sync_weights + type(vllm_gen).sync_weights = lambda self: lora_sync_weights() + LOG.info("Installed LoRA adapter sync (standalone fallback)") + + def post_train_unload(self, cfg): + """Cleanup NeMo Gym servers if we started them.""" + if cfg.get("nemo_gym_enabled") and cfg.get("nemo_gym_auto_start", True): + from .server import _cleanup_servers + + _cleanup_servers() diff --git a/src/axolotl/integrations/nemo_gym/rewards.py b/src/axolotl/integrations/nemo_gym/rewards.py new file mode 100644 index 0000000000..e99df09818 --- /dev/null +++ b/src/axolotl/integrations/nemo_gym/rewards.py @@ -0,0 +1,274 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# This software may be used and distributed according to +# the terms of the Axolotl Community License Agreement (the "License"); +# you may not use this file except in compliance with the License. + +""" +NeMo Gym reward functions. + +Provides ready-to-use reward functions for axolotl configs:: + + trl: + reward_funcs: + # Multi-turn: passthrough reward from agent /run + - axolotl.integrations.nemo_gym.rewards.reward_env + # Single-turn: call /verify endpoints directly + - axolotl.integrations.nemo_gym.rewards.reward_nemo_gym_verify +""" + +from __future__ import annotations + +from typing import Any + +import numpy as np +import requests + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +# --------------------------------------------------------------------------- +# Multi-turn passthrough reward +# --------------------------------------------------------------------------- + + +def reward_env(completions, prompts=None, **kwargs): + """Passthrough: extract pre-computed reward from NeMo Gym agent /run response. + + The ``NemoGymDataProducer`` injects ``env_reward`` into each sample's + kwargs after the agent returns from ``/run``. This function simply + forwards that value so TRL can log it alongside other reward signals. + + Use this in your config when ``nemo_gym_multi_turn: true``:: + + trl: + reward_funcs: + - axolotl.integrations.nemo_gym.rewards.reward_env + """ + env_rewards = kwargs.get("env_reward") + if env_rewards is not None: + if isinstance(env_rewards, (list, tuple)): + return [float(r) for r in env_rewards] + return [float(env_rewards) for _ in completions] + return [0.0 for _ in completions] + + +# --------------------------------------------------------------------------- +# Single-turn /verify reward +# --------------------------------------------------------------------------- + +# Module-level cache for discovered verify URLs +_verify_urls: dict[str, str] = {} +_verify_urls_lock = __import__("threading").Lock() + + +def _get_verify_urls(head_port: int = 11000) -> dict[str, str]: + """Discover verify endpoints from the NeMo Gym head server. + + Results are cached so that the HTTP round-trip only happens once per + process. A lock guards against concurrent discovery from multiple + threads (e.g. async_prefetch background thread + main training thread). + """ + global _verify_urls + if _verify_urls: + return _verify_urls + + with _verify_urls_lock: + # Double-check after acquiring lock + if _verify_urls: + return _verify_urls + + import yaml + + try: + resp = requests.get( + f"http://127.0.0.1:{head_port}/global_config_dict_yaml", timeout=5 + ) + config = yaml.safe_load(resp.text) + if isinstance(config, str): + config = yaml.safe_load(config) + for _name, cfg in config.items(): + if not isinstance(cfg, dict): + continue + for srv_name, srv_cfg in cfg.get("resources_servers", {}).items(): + if ( + isinstance(srv_cfg, dict) + and "host" in srv_cfg + and "port" in srv_cfg + ): + _verify_urls[srv_name] = ( + f"http://{srv_cfg['host']}:{srv_cfg['port']}/verify" + ) + except Exception as exc: + LOG.warning(f"Failed to discover NeMo Gym verify endpoints: {exc}") + + return _verify_urls + + +def reward_nemo_gym_verify(completions, prompts=None, **kwargs): + """Call NeMo Gym ``/verify`` endpoint for each completion (single-turn). + + Requires ``resources_server_ref`` and ``verify_extra`` kwargs, which the + NeMo Gym dataset loader injects automatically. + + Use this in your config when ``nemo_gym_multi_turn: false``:: + + trl: + reward_funcs: + - axolotl.integrations.nemo_gym.rewards.reward_nemo_gym_verify + """ + verify_urls = _get_verify_urls() + refs = kwargs.get("resources_server_ref", []) + extras = kwargs.get("verify_extra", []) + scores = [] + + for i, completion in enumerate(completions): + text = completion[0]["content"] if completion else "" + prompt = prompts[i][0]["content"] if prompts and i < len(prompts) else "" + srv_name = ( + refs[i]["name"] if i < len(refs) and isinstance(refs[i], dict) else "" + ) + url = verify_urls.get(srv_name, "") + + if not url: + scores.append(0.0) + continue + + extra = extras[i] if i < len(extras) else {} + req = {k: v for k, v in extra.items() if v is not None} + req["responses_create_params"] = { + "input": [{"role": "user", "content": prompt}] + } + req["response"] = { + "id": "resp", + "created_at": 0, + "model": "axolotl", + "object": "response", + "output": [ + { + "id": "msg", + "role": "assistant", + "type": "message", + "status": "completed", + "content": [ + {"type": "output_text", "text": text, "annotations": []} + ], + } + ], + "parallel_tool_calls": True, + "tool_choice": "auto", + "tools": [], + } + + try: + resp = requests.post(url, json=req, timeout=30) + reward = resp.json().get("reward", 0.0) if resp.ok else 0.0 + except Exception as exc: + LOG.warning(f"Verify request to {url} failed: {exc}") + reward = 0.0 + + scores.append(float(reward)) + + return scores + + +# --------------------------------------------------------------------------- +# Factory used internally by the plugin +# --------------------------------------------------------------------------- + + +def create_nemo_gym_reward_fn( + global_config: dict, + verify_endpoints: dict[str, str], + model_name: str = "axolotl-model", + verify_timeout: int = 30, +): + """Create a reward function bound to specific verify endpoints. + + Used internally by ``NemoGymPlugin._wire_single_turn()`` to inject a + reward function that already knows the endpoint map (no discovery needed). + """ + + def reward_fn( + completions: list[list[dict[str, str]]], + prompts: list[list[dict[str, str]]] | None = None, + **kwargs: Any, + ) -> np.ndarray: + resources_server_refs = kwargs.get("resources_server_ref", []) + verify_extras = kwargs.get("verify_extra", []) + + scores = [] + for i, completion in enumerate(completions): + completion_text = completion[0]["content"] + task_prompt = prompts[i][0]["content"] if prompts else "" + + server_name = ( + resources_server_refs[i]["name"] + if i < len(resources_server_refs) + else None + ) + + if server_name is None or server_name not in verify_endpoints: + LOG.warning( + f"No verify endpoint for server '{server_name}', returning 0 reward" + ) + scores.append(0.0) + continue + + verify_endpoint = verify_endpoints[server_name] + + verify_request = ( + {k: v for k, v in verify_extras[i].items() if v is not None} + if i < len(verify_extras) + else {} + ) + verify_request["responses_create_params"] = { + "input": [{"role": "user", "content": task_prompt}] + } + verify_request["response"] = { + "id": "resp", + "created_at": 0, + "model": model_name, + "object": "response", + "output": [ + { + "id": "msg", + "role": "assistant", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "text": completion_text, + "annotations": [], + } + ], + } + ], + "parallel_tool_calls": True, + "tool_choice": "auto", + "tools": [], + } + + try: + resp = requests.post( + verify_endpoint, json=verify_request, timeout=verify_timeout + ) + if resp.status_code == 200: + reward = resp.json().get("reward", 0.0) + else: + LOG.warning( + f"Verify request returned status {resp.status_code}: {resp.text[:200]}" + ) + reward = 0.0 + except requests.exceptions.RequestException as exc: + LOG.warning(f"Verify request failed: {exc}") + reward = 0.0 + + scores.append(float(reward)) + + return np.array(scores) + + return reward_fn diff --git a/src/axolotl/integrations/nemo_gym/server.py b/src/axolotl/integrations/nemo_gym/server.py new file mode 100644 index 0000000000..0af9b3b710 --- /dev/null +++ b/src/axolotl/integrations/nemo_gym/server.py @@ -0,0 +1,242 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# This software may be used and distributed according to +# the terms of the Axolotl Community License Agreement (the "License"); +# you may not use this file except in compliance with the License. + +""" +NeMo Gym server lifecycle management. + +Handles cloning the NeMo Gym repo, starting resource servers, +waiting for readiness, and cleanup on exit. +""" + +from __future__ import annotations + +import atexit +import os +import subprocess # nosec B404 +import time + +import requests +import yaml + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +_ng_process = None +_ng_log_file = None + + +def ensure_gym_repo(gym_dir: str, auto_clone: bool = True) -> str: + """Clone the NeMo Gym repo if it doesn't exist. + + Args: + gym_dir: Path to the NeMo Gym directory. + auto_clone: Whether to auto-clone if missing. + + Returns: + Resolved path to the NeMo Gym directory. + """ + gym_dir = os.path.expanduser(gym_dir) + if os.path.exists(gym_dir): + LOG.info(f"NeMo Gym directory exists at {gym_dir}") + return gym_dir + + if not auto_clone: + raise FileNotFoundError( + f"NeMo Gym directory not found at {gym_dir} and auto_clone is disabled." + ) + + LOG.info(f"Cloning NeMo Gym to {gym_dir}...") + subprocess.run( # nosec + ["git", "clone", "https://github.com/NVIDIA-NeMo/Gym.git", gym_dir], + check=True, + ) + return gym_dir + + +def ensure_gym_venv(gym_dir: str): + """Set up the NeMo Gym Python venv if not present.""" + venv_python = os.path.join(gym_dir, ".venv", "bin", "python") + if os.path.exists(venv_python): + return + + LOG.info("Setting up NeMo Gym venv...") + subprocess.run(["uv", "venv", "--python", "3.12"], cwd=gym_dir, check=True) # nosec + subprocess.run( # nosec + ["bash", "-c", "source .venv/bin/activate && uv sync"], + cwd=gym_dir, + check=True, + ) + + +def start_servers( + gym_dir: str, + config_paths: list[str], + head_port: int = 11000, + timeout: int = 360, +): + """Start NeMo Gym resource servers via ng_run. + + Args: + gym_dir: Path to the NeMo Gym directory. + config_paths: List of config YAML paths relative to gym_dir. + head_port: Port for the head server. + timeout: Max seconds to wait for servers. + """ + global _ng_process, _ng_log_file + + head_url = f"http://127.0.0.1:{head_port}/global_config_dict_yaml" + + # Check if already running + try: + requests.get(head_url, timeout=2) + LOG.info(f"NeMo Gym servers already running on port {head_port}.") + return + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + pass + + ng_run_bin = os.path.join(gym_dir, ".venv", "bin", "ng_run") + config_arg = f"+config_paths=[{','.join(config_paths)}]" + _ng_log_file = open(os.path.join(gym_dir, "ng_run.log"), "w") # noqa: SIM115 + _ng_process = subprocess.Popen( # nosec B603 + [ng_run_bin, config_arg, "+skip_venv_if_present=true"], + cwd=gym_dir, + stdout=_ng_log_file, + stderr=subprocess.STDOUT, + ) + + atexit.register(_cleanup_servers) + + LOG.info("Waiting for NeMo Gym head server...") + for _ in range(timeout // 3): + try: + requests.get(head_url, timeout=2) + LOG.info("NeMo Gym head server is ready.") + return + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + if _ng_process.poll() is not None: + raise RuntimeError( + "NeMo Gym server process exited unexpectedly. " + f"Check {gym_dir}/ng_run.log for details." + ) from None + time.sleep(3) + + raise RuntimeError( + f"NeMo Gym servers did not start within {timeout}s. " + f"Check {gym_dir}/ng_run.log for details." + ) + + +def get_server_configs(head_port: int = 11000) -> dict: + """Fetch the global config from the NeMo Gym head server. + + Returns: + Dict mapping server_name -> server config. + """ + response = requests.get( + f"http://127.0.0.1:{head_port}/global_config_dict_yaml", timeout=5 + ) + response.raise_for_status() + result = yaml.safe_load(response.text) + # NeMo Gym head server double-encodes: YAML string inside a YAML string + if isinstance(result, str): + result = yaml.safe_load(result) + return result + + +def get_agent_servers( + global_config: dict, head_host: str = "127.0.0.1" +) -> dict[str, str]: + """Discover NeMo Gym agent servers from the global config. + + Agent servers handle multi-turn orchestration via /run endpoint. + Returns mapping of agent_name → URL (e.g., {"simple_agent": "http://host:port"}). + """ + agents = {} + for top_name, top_cfg in global_config.items(): + if not isinstance(top_cfg, dict): + continue + agent_dict = top_cfg.get("responses_api_agents", {}) + if not agent_dict: + continue + for _agent_name, agent_cfg in agent_dict.items(): + if not isinstance(agent_cfg, dict): + continue + host = agent_cfg.get("host", "127.0.0.1") + port = agent_cfg.get("port") + if not port: + continue + # Replace loopback with head_host for remote access + host = _normalize_host(host, fallback=head_host) + # Use the top-level config name (not the inner agent name) + # because dataset agent_ref.name references the top-level name + agents[top_name] = f"http://{host}:{port}" + if agents: + LOG.info(f"Discovered NeMo Gym agent servers: {agents}") + return agents + + +def _normalize_host(host: str, fallback: str = "127.0.0.1") -> str: + """Normalize bind-all and loopback addresses for reachability.""" + if host in ("0.0.0.0", "localhost"): # nosec B104 + return fallback + return host + + +def get_server_base_url(global_config: dict, server_name: str) -> str: + """Get the base URL for a given resource server.""" + try: + srv_cfg = global_config[server_name]["resources_servers"][server_name] + host = _normalize_host(srv_cfg["host"]) + return f"http://{host}:{srv_cfg['port']}" + except (KeyError, TypeError) as exc: + raise ValueError( + f"Could not find resource server config for '{server_name}' in NeMo Gym. " + f"Available servers: {list(global_config.keys())}" + ) from exc + + +def get_verify_endpoint(global_config: dict, server_name: str) -> str: + """Get the /verify endpoint URL for a given resource server.""" + return f"{get_server_base_url(global_config, server_name)}/verify" + + +def wait_for_resource_servers(global_config: dict, timeout: int = 180): + """Wait for all resource servers in the config to become reachable.""" + for srv_name in global_config: + try: + srv_cfg = global_config[srv_name]["resources_servers"][srv_name] + except (KeyError, TypeError): + continue # Skip non-server config entries silently + + host, port = _normalize_host(srv_cfg["host"]), srv_cfg["port"] + LOG.info(f"Waiting for resource server '{srv_name}' at {host}:{port}...") + for _ in range(timeout // 2): + try: + requests.get(f"http://{host}:{port}/", timeout=2) + LOG.info(f"Resource server '{srv_name}' is ready.") + break + except requests.exceptions.ConnectionError: + time.sleep(2) + else: + raise RuntimeError( + f"Resource server '{srv_name}' at {host}:{port} " + f"did not start within {timeout}s." + ) + + +def _cleanup_servers(): + """Terminate NeMo Gym server process on exit.""" + global _ng_process, _ng_log_file + if _ng_process is not None and _ng_process.poll() is None: + LOG.info("Terminating NeMo Gym servers...") + _ng_process.terminate() + try: + _ng_process.wait(timeout=10) + except subprocess.TimeoutExpired: + _ng_process.kill() + if _ng_log_file is not None: + _ng_log_file.close() diff --git a/src/axolotl/scripts/vllm_serve_lora.py b/src/axolotl/scripts/vllm_serve_lora.py index f4fcfa1904..e292d89f82 100644 --- a/src/axolotl/scripts/vllm_serve_lora.py +++ b/src/axolotl/scripts/vllm_serve_lora.py @@ -446,6 +446,114 @@ async def chat(request: ChatRequest): "logprob_token_ids": extract_logprobs(all_outputs)[1], } + # --- OpenAI-compatible endpoints (for NeMo Gym agent integration) --- + + @app.get("/v1/models") + async def list_models(): + """OpenAI-compatible models endpoint.""" + return { + "object": "list", + "data": [ + {"id": script_args.model, "object": "model", "owned_by": "axolotl"} + ], + } + + @app.post("/v1/chat/completions") + async def openai_chat_completions(request_body: dict): + """OpenAI-compatible chat completions endpoint. + + Translates OpenAI format to our internal /chat/ format so NeMo Gym's + model server proxy can call us directly. + """ + messages_list = request_body.get("messages", []) + temperature = request_body.get("temperature", 1.0) + max_tokens = request_body.get("max_tokens", 512) + top_p = request_body.get("top_p", 1.0) + n = request_body.get("n", 1) + + generation_kwargs = { + "n": n, + "temperature": temperature, + "top_p": top_p, + "max_tokens": max_tokens, + "logprobs": 0, # Always return logprobs (NeMo Gym needs them) + } + sampling_params = SamplingParams( + **{k: v for k, v in generation_kwargs.items() if v is not None} + ) + + # Send to vLLM worker + chunked = chunk_list([messages_list], script_args.data_parallel_size) + for conn, chunk in zip(connections, chunked, strict=True): + if not chunk: + chunk = [[{"role": "user", "content": ""}]] + kwargs = { + "messages": chunk, + "sampling_params": sampling_params, + "use_tqdm": False, + "lora_request": active_lora["request"], + } + conn.send({"type": "call", "method": "chat", "kwargs": kwargs}) + + all_outputs = [conn.recv() for conn in connections] + all_outputs = [o for o, c in zip(all_outputs, chunked, strict=True) if c] + all_outputs = list(chain.from_iterable(all_outputs)) + + if not all_outputs: + return {"choices": [], "model": script_args.model} + + # Format as OpenAI response + import uuid + + choices = [] + for i, output in enumerate(all_outputs): + for j, out in enumerate(output.outputs): + text = out.text + # Extract token IDs if requested + # Build logprobs in OpenAI format + lp_list = None + if out.logprobs: + lp_list = { + "content": [ + {"token": "", "logprob": next(iter(lp.values())).logprob} # nosec B105 + for lp in out.logprobs + ] + } + + choice = { + "index": i * n + j, + "message": {"role": "assistant", "content": text}, + "finish_reason": "stop" + if out.finish_reason == "stop" + else "length", + "logprobs": lp_list, + } + # Include token ID information for NeMo Gym + choice["prompt_token_ids"] = output.prompt_token_ids + choice["generation_token_ids"] = list(out.token_ids) + if out.logprobs: + choice["generation_log_probs"] = [ + next(iter(lp.values())).logprob for lp in out.logprobs + ] + choices.append(choice) + + prompt_tokens = len(all_outputs[0].prompt_token_ids) if all_outputs else 0 + completion_tokens = sum( + len(out.token_ids) for o in all_outputs for out in o.outputs + ) + + return { + "id": f"chatcmpl-{uuid.uuid4().hex[:8]}", + "object": "chat.completion", + "model": script_args.model, + "choices": choices, + "usage": { + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": prompt_tokens + completion_tokens, + }, + } + # --- Weight sync endpoints (legacy fallback, same as TRL) --- @app.post("/init_communicator/") diff --git a/tests/monkeypatch/test_trainer_loss_calc.py b/tests/e2e/solo/test_trainer_loss_calc.py similarity index 100% rename from tests/monkeypatch/test_trainer_loss_calc.py rename to tests/e2e/solo/test_trainer_loss_calc.py diff --git a/tests/integrations/test_nemo_gym.py b/tests/integrations/test_nemo_gym.py new file mode 100644 index 0000000000..7fd53cee0f --- /dev/null +++ b/tests/integrations/test_nemo_gym.py @@ -0,0 +1,621 @@ +"""Unit tests for NeMo Gym integration. + +Tests the core parsing, routing, reward, and plugin wiring logic +without requiring a running NeMo Gym server or GPU. +""" + +import unittest +from unittest.mock import MagicMock, patch + + +class TestParseAgentResponse(unittest.TestCase): + """Tests for _parse_agent_response in multi_turn.py.""" + + def _parse(self, response, eos_token_id=2): + from axolotl.integrations.nemo_gym.multi_turn import _parse_agent_response + + return _parse_agent_response(response, eos_token_id) + + def test_empty_response_returns_defaults(self): + result = self._parse({}) + assert result["prompt_ids"] == [2] + assert result["completion_ids"] == [2] + assert result["env_mask"] == [0] + assert result["reward"] == 0.0 + assert result["num_turns"] == 0 + + def test_error_response_returns_defaults(self): + result = self._parse({"error": "something broke"}) + assert result["reward"] == 0.0 + assert result["num_turns"] == 0 + + def test_single_turn_function_call(self): + response = { + "response": { + "output": [ + { + "type": "function_call", + "name": "guess_word", + "arguments": '{"guess": "crane"}', + "call_id": "call_1", + "prompt_token_ids": [10, 20, 30], + "generation_token_ids": [40, 50], + "generation_log_probs": [-0.1, -0.2], + } + ] + }, + "reward": 0.5, + } + result = self._parse(response) + assert result["prompt_ids"] == [10, 20, 30] + assert result["completion_ids"] == [40, 50] + assert result["env_mask"] == [1, 1] # model tokens + assert result["logprobs"] == [-0.1, -0.2] + assert result["reward"] == 0.5 + assert result["num_turns"] == 1 + + def test_multi_turn_preserves_env_mask(self): + """Second turn's prompt tokens (tool results) get env_mask=0.""" + response = { + "response": { + "output": [ + { + "type": "function_call", + "prompt_token_ids": [10, 20], + "generation_token_ids": [30, 31], + "generation_log_probs": [-0.1, -0.2], + }, + { + "type": "function_call_output", + "output": '{"feedback": "XYGXY"}', + }, + { + "type": "function_call", + # prompt includes original + gen + tool output + "prompt_token_ids": [10, 20, 30, 31, 100, 101, 102], + "generation_token_ids": [40, 41], + "generation_log_probs": [-0.3, -0.4], + }, + ] + }, + "reward": 0.3, + } + result = self._parse(response) + assert result["prompt_ids"] == [10, 20] + # completion = gen1 + tool_result + gen2 + assert result["completion_ids"] == [30, 31, 100, 101, 102, 40, 41] + # env_mask: gen1=model(1), tool=env(0), gen2=model(1) + assert result["env_mask"] == [1, 1, 0, 0, 0, 1, 1] + assert result["num_turns"] == 2 + + def test_empty_output_preserves_reward(self): + response = { + "response": {"output": []}, + "reward": 0.42, + } + result = self._parse(response) + assert result["reward"] == 0.42 + + def test_message_only_output(self): + """A message with text but no function calls.""" + response = { + "response": { + "output": [ + { + "type": "message", + "role": "assistant", + "content": [ + {"type": "output_text", "text": "I'll guess crane."} + ], + "prompt_token_ids": [10, 20], + "generation_token_ids": [30, 31, 32], + "generation_log_probs": [-0.1, -0.2, -0.3], + } + ] + }, + "reward": 0.1, + } + result = self._parse(response) + assert result["num_turns"] == 1 + assert result["completion_ids"] == [30, 31, 32] + assert result["env_mask"] == [1, 1, 1] + + +class TestRewardEnv(unittest.TestCase): + """Tests for reward_env passthrough function.""" + + def test_with_list_rewards(self): + from axolotl.integrations.nemo_gym.rewards import reward_env + + result = reward_env([["comp1"], ["comp2"]], env_reward=[0.5, 0.8]) + assert result == [0.5, 0.8] + + def test_with_scalar_reward(self): + from axolotl.integrations.nemo_gym.rewards import reward_env + + result = reward_env([["comp1"], ["comp2"]], env_reward=0.7) + assert result == [0.7, 0.7] + + def test_missing_reward_returns_zeros(self): + from axolotl.integrations.nemo_gym.rewards import reward_env + + result = reward_env([["comp1"], ["comp2"]]) + assert result == [0.0, 0.0] + + +class TestRewardNemoGymVerify(unittest.TestCase): + """Tests for reward_nemo_gym_verify with mocked HTTP.""" + + @patch("axolotl.integrations.nemo_gym.rewards._get_verify_urls") + @patch("axolotl.integrations.nemo_gym.rewards.requests") + def test_calls_verify_endpoint(self, mock_requests, mock_get_urls): + from axolotl.integrations.nemo_gym.rewards import reward_nemo_gym_verify + + mock_get_urls.return_value = {"wordle": "http://localhost:9999/verify"} + mock_resp = MagicMock() + mock_resp.ok = True + mock_resp.json.return_value = {"reward": 0.75} + mock_requests.post.return_value = mock_resp + + result = reward_nemo_gym_verify( + completions=[[{"role": "assistant", "content": "crane"}]], + prompts=[[{"role": "user", "content": "Guess a word"}]], + resources_server_ref=[{"name": "wordle"}], + verify_extra=[{}], + ) + + assert result == [0.75] + mock_requests.post.assert_called_once() + + @patch("axolotl.integrations.nemo_gym.rewards._get_verify_urls") + def test_missing_server_returns_zero(self, mock_get_urls): + from axolotl.integrations.nemo_gym.rewards import reward_nemo_gym_verify + + mock_get_urls.return_value = {} + + result = reward_nemo_gym_verify( + completions=[[{"role": "assistant", "content": "crane"}]], + prompts=[[{"role": "user", "content": "Guess"}]], + resources_server_ref=[{"name": "unknown_server"}], + verify_extra=[{}], + ) + assert result == [0.0] + + +class TestNormalizeHost(unittest.TestCase): + """Tests for server.py _normalize_host helper.""" + + def test_zero_addr_normalized(self): + from axolotl.integrations.nemo_gym.server import _normalize_host + + assert _normalize_host("0.0.0.0") == "127.0.0.1" + + def test_localhost_normalized(self): + from axolotl.integrations.nemo_gym.server import _normalize_host + + assert _normalize_host("localhost") == "127.0.0.1" + + def test_loopback_passthrough(self): + from axolotl.integrations.nemo_gym.server import _normalize_host + + assert _normalize_host("127.0.0.1") == "127.0.0.1" + + def test_custom_fallback(self): + from axolotl.integrations.nemo_gym.server import _normalize_host + + assert _normalize_host("0.0.0.0", fallback="10.0.0.1") == "10.0.0.1" + + def test_real_ip_passthrough(self): + from axolotl.integrations.nemo_gym.server import _normalize_host + + assert _normalize_host("192.168.1.50") == "192.168.1.50" + + +class TestDatasetLookupKeying(unittest.TestCase): + """Verify dataset lookup uses last message content as key.""" + + def test_single_message_prompt(self): + """Single-message prompt: [0] == [-1], both work.""" + prompt = [{"role": "user", "content": "Play Wordle!"}] + assert prompt[0]["content"] == prompt[-1]["content"] + + def test_multi_message_prompt_uses_last(self): + """Multi-message prompt: must use [-1] to match data_producer lookup.""" + prompt = [ + {"role": "system", "content": "You are a game player."}, + {"role": "user", "content": "Play Wordle!"}, + ] + # data_producer.py line 92 uses prompt[-1] + key = prompt[-1]["content"] + assert key == "Play Wordle!" + # Old code used prompt[0] which would be wrong here + assert prompt[0]["content"] != key + + +class TestAgentRefPreservation(unittest.TestCase): + """Verify agent_ref is preserved through the dispatch chain.""" + + def test_data_producer_preserves_agent_ref(self): + """Simulates the data_producer lookup logic.""" + # Simulate what plugin.py builds + dataset_lookup = { + "Play Wordle!": { + "prompt": [{"role": "user", "content": "Play Wordle!"}], + "agent_ref": {"name": "wordle_simple_agent"}, + "verify_extra": { + "responses_create_params": { + "input": [{"role": "user", "content": "Play Wordle!"}] + } + }, + } + } + + # Simulate data_producer.py logic (after fix) + prompt_text = "Play Wordle!" + full_item = dataset_lookup.get(prompt_text, {}) + item = full_item.get("verify_extra", {}) + if "agent_ref" in full_item and "agent_ref" not in item: + item["agent_ref"] = full_item["agent_ref"] + + assert "agent_ref" in item + assert item["agent_ref"]["name"] == "wordle_simple_agent" + + def test_multi_turn_preserves_agent_ref(self): + """Simulates the multi_turn.py dispatch logic.""" + dataset_lookup = { + "Play Wordle!": { + "agent_ref": {"name": "wordle_simple_agent"}, + "verify_extra": { + "responses_create_params": { + "input": [{"role": "user", "content": "Play Wordle!"}] + } + }, + } + } + + # Simulate multi_turn.py logic (after fix) + prompt_str = "Play Wordle!" + full_item = None + for key, val in dataset_lookup.items(): + if isinstance(key, str) and prompt_str == key: + full_item = val + break + + dispatched = full_item.get("verify_extra", full_item) + if isinstance(dispatched, dict) and "agent_ref" not in dispatched: + agent_ref = full_item.get("agent_ref") + if agent_ref: + dispatched = {**dispatched, "agent_ref": agent_ref} + + assert "agent_ref" in dispatched + assert dispatched["agent_ref"]["name"] == "wordle_simple_agent" + + +class TestCallAgentsRouting(unittest.TestCase): + """Tests for _call_agents routing via agent_ref.""" + + def test_routes_to_correct_agent(self): + """Items with agent_ref should route to the matching agent server.""" + + agent_servers = { + "wordle_agent": "http://localhost:11111", + "math_agent": "http://localhost:22222", + } + + items = [ + { + "agent_ref": {"name": "wordle_agent"}, + "responses_create_params": { + "input": [{"role": "user", "content": "Play"}] + }, + } + ] + + # We can't actually call the agent, but verify the URL resolution + # by checking _call_agents builds the right request + # The function uses aiohttp — just verify agent_ref lookup works + item = items[0] + agent_ref = item.get("agent_ref", {}) + agent_name = agent_ref.get("name", "") + agent_url = agent_servers.get(agent_name, "") + assert agent_url == "http://localhost:11111" + + def test_fallback_to_first_agent(self): + """Items without agent_ref should use first available agent.""" + agent_servers = {"default_agent": "http://localhost:33333"} + item = { + "responses_create_params": {"input": [{"role": "user", "content": "Hello"}]} + } + agent_ref = item.get("agent_ref", {}) + agent_name = agent_ref.get("name", "") + agent_url = agent_servers.get(agent_name, "") + if not agent_url and agent_servers: + agent_url = next(iter(agent_servers.values())) + assert agent_url == "http://localhost:33333" + + +class TestPluginDefaults(unittest.TestCase): + """Tests for plugin config enforcement.""" + + def test_dataloader_num_workers_forced_to_zero(self): + """Plugin should set dataloader_num_workers=0 for NeMo Gym.""" + + # Simulate the plugin logic + class FakeCfg: + dataloader_num_workers = 4 + nemo_gym_multi_turn = True + + cfg = FakeCfg() + # Replicate plugin.get_training_args logic + if getattr(cfg, "dataloader_num_workers", None) not in (None, 0): + pass # would log warning + cfg.dataloader_num_workers = 0 + assert cfg.dataloader_num_workers == 0 + + def test_dataloader_num_workers_none_stays_zero(self): + class FakeCfg: + dataloader_num_workers = None + + cfg = FakeCfg() + cfg.dataloader_num_workers = 0 + assert cfg.dataloader_num_workers == 0 + + +class TestNemoGymE2E(unittest.TestCase): + """End-to-end test: data producer → agent (mocked) → parse → tensors → rewards. + + Exercises the full NemoGymDataProducer.produce() pipeline with mocked HTTP + responses, verifying that multi-turn Wordle agent responses are correctly + parsed into padded tensors with proper env_mask, logprobs, and rewards. + No GPU or NeMo Gym server required. + """ + + # A realistic 2-turn agent /run response (guess + feedback + guess + done) + AGENT_RESPONSE = { + "response": { + "output": [ + { + "type": "function_call", + "name": "guess_word", + "arguments": '{"guess": "crane"}', + "call_id": "call_1", + "id": "call_1", + "status": "completed", + "prompt_token_ids": [1, 2, 3, 4, 5], + "generation_token_ids": [10, 11, 12, 13], + "generation_log_probs": [-0.1, -0.2, -0.3, -0.4], + }, + { + "type": "function_call_output", + "call_id": "call_1", + "output": '{"feedback":"XYGXY","guesses_remaining":5,"done":false}', + }, + { + "type": "function_call", + "name": "guess_word", + "arguments": '{"guess": "slide"}', + "call_id": "call_2", + "id": "call_2", + "status": "completed", + # prompt = original(5) + gen1(4) + tool_output(3 tokens) + "prompt_token_ids": [1, 2, 3, 4, 5, 10, 11, 12, 13, 50, 51, 52], + "generation_token_ids": [20, 21, 22], + "generation_log_probs": [-0.5, -0.6, -0.7], + }, + ], + }, + "reward": 0.42, + } + + def _make_mock_trainer(self): + """Create a minimal mock trainer with the attributes produce() needs.""" + trainer = MagicMock() + trainer.accelerator.is_main_process = True + trainer.accelerator.device = "cpu" + trainer.max_completion_length = 512 + trainer.temperature = 0.8 + trainer.pad_token_id = 0 + trainer.processing_class.eos_token_id = 2 + trainer.processing_class.batch_decode.return_value = ["crane slide"] + return trainer + + @patch("axolotl.integrations.nemo_gym.data_producer._call_agents") + def test_produce_returns_valid_rollout_dataset(self, mock_call_agents): + """Full pipeline: produce() → _call_agents (mocked) → parse → RolloutDataset.""" + + from axolotl.integrations.nemo_gym.data_producer import NemoGymDataProducer + + # Mock _call_agents — it's async, so return a coroutine + async def fake_call_agents(**kwargs): + return [self.AGENT_RESPONSE, self.AGENT_RESPONSE] + + mock_call_agents.side_effect = fake_call_agents + + # Build a minimal mock of GRPODataProducer's __init__ dependencies + # We can't easily call super().__init__, so we'll set attributes directly + producer = NemoGymDataProducer.__new__(NemoGymDataProducer) + producer._agent_servers = {"wordle_agent": "http://mock:9999"} + producer._dataset_lookup = { + "Play Wordle!": { + "agent_ref": {"name": "wordle_agent"}, + "verify_extra": { + "responses_create_params": { + "input": [{"role": "user", "content": "Play Wordle!"}], + } + }, + } + } + producer._request_timeout = 30 + producer._num_generations = 2 + + # Mock the trainer + trainer = self._make_mock_trainer() + producer._trainer = trainer + + # Mock the prompt iterator (returns a batch of 1 input) + producer._prompt_iter = iter( + [ + [ + { + "prompt": [{"role": "user", "content": "Play Wordle!"}], + } + ] + ] + ) + producer._prompt_dl = [ + [{"prompt": [{"role": "user", "content": "Play Wordle!"}]}] + ] + + # Call produce + result = producer.produce(model=MagicMock(), global_step=1) + + # Verify result structure + assert result is not None + data = result._data + + # Check tensor shapes — 2 rollouts (num_generations=2) + assert data["prompt_ids"].shape[0] == 2 + assert data["completion_ids"].shape[0] == 2 + assert data["completion_mask"].shape[0] == 2 + assert data["sampling_per_token_logps"].shape[0] == 2 + assert data["tool_mask"].shape[0] == 2 + + # Verify completion content — each rollout should have: + # gen1(4) + tool_output(3) + gen2(3) = 10 tokens + # (padded to same length across the batch, but both are same here) + comp_len = data["completion_mask"][0].sum().item() + assert comp_len == 10, f"Expected 10 completion tokens, got {comp_len}" + + # Verify env_mask: gen1=1,1,1,1 tool=0,0,0 gen2=1,1,1 + tool_mask = data["tool_mask"][0][:comp_len].tolist() + assert tool_mask == [1, 1, 1, 1, 0, 0, 0, 1, 1, 1] + + # Verify logprobs are populated (use approx for float32 precision) + import pytest + + logps = data["sampling_per_token_logps"][0][:comp_len].tolist() + assert logps[:4] == pytest.approx([-0.1, -0.2, -0.3, -0.4], abs=1e-6) + assert logps[4:7] == pytest.approx([0.0, 0.0, 0.0], abs=1e-6) + assert logps[7:10] == pytest.approx([-0.5, -0.6, -0.7], abs=1e-6) + + # Verify rewards were injected into inputs + assert data["_deferred_inputs"][0]["env_reward"] == 0.42 + assert data["_deferred_inputs"][1]["env_reward"] == 0.42 + + # Verify deferred scoring markers + assert data["_pending_policy_logps"] is True + + @patch("axolotl.integrations.nemo_gym.data_producer._call_agents") + def test_produce_handles_failed_agent_response(self, mock_call_agents): + """Failed agent responses should produce default (length-1) rollouts.""" + + from axolotl.integrations.nemo_gym.data_producer import NemoGymDataProducer + + # One success, one failure — async mock + async def fake_call_agents(**kwargs): + return [ + self.AGENT_RESPONSE, + { + "error": "Connection timeout", + "response": {"output": []}, + "reward": 0.0, + }, + ] + + mock_call_agents.side_effect = fake_call_agents + + producer = NemoGymDataProducer.__new__(NemoGymDataProducer) + producer._agent_servers = {"wordle_agent": "http://mock:9999"} + producer._dataset_lookup = {} + producer._request_timeout = 30 + producer._num_generations = 2 + producer._trainer = self._make_mock_trainer() + producer._prompt_iter = iter( + [[{"prompt": [{"role": "user", "content": "Play!"}]}]] + ) + producer._prompt_dl = [[{"prompt": [{"role": "user", "content": "Play!"}]}]] + + result = producer.produce(model=MagicMock(), global_step=1) + + assert result is not None + data = result._data + + # Both rollouts present + assert data["completion_ids"].shape[0] == 2 + + # First rollout has real tokens, second has just eos (length 1) + mask0 = data["completion_mask"][0].sum().item() + mask1 = data["completion_mask"][1].sum().item() + assert mask0 == 10 # full response + assert mask1 == 1 # default fallback (just eos) + + # Rewards: success=0.42, failure=0.0 + assert data["_deferred_inputs"][0]["env_reward"] == 0.42 + assert data["_deferred_inputs"][1]["env_reward"] == 0.0 + + @patch("axolotl.integrations.nemo_gym.rewards._get_verify_urls") + @patch("axolotl.integrations.nemo_gym.rewards.requests") + def test_reward_functions_chain(self, mock_requests, mock_get_urls): + """Test that reward_env and reward_nemo_gym_verify can be used together.""" + from axolotl.integrations.nemo_gym.rewards import ( + reward_env, + reward_nemo_gym_verify, + ) + + completions = [[{"role": "assistant", "content": "crane"}]] + prompts = [[{"role": "user", "content": "Guess"}]] + + # reward_env: passthrough from agent + env_result = reward_env(completions, prompts, env_reward=[0.42]) + assert env_result == [0.42] + + # reward_nemo_gym_verify: calls /verify + mock_get_urls.return_value = {"wordle": "http://localhost:9999/verify"} + mock_resp = MagicMock() + mock_resp.ok = True + mock_resp.json.return_value = {"reward": 0.75} + mock_requests.post.return_value = mock_resp + + verify_result = reward_nemo_gym_verify( + completions, + prompts, + resources_server_ref=[{"name": "wordle"}], + verify_extra=[{}], + ) + assert verify_result == [0.75] + + # Both rewards can coexist (as they would in a multi-reward config) + combined = [e + v for e, v in zip(env_result, verify_result, strict=True)] + assert combined == [1.17] + + +class TestLoRASyncSetup(unittest.TestCase): + """Tests for _setup_lora_sync delegation logic.""" + + def test_delegates_to_async_trainer(self): + """When trainer has _sync_lora_adapter, the closure should delegate.""" + from axolotl.integrations.nemo_gym.plugin import NemoGymPlugin + + plugin = NemoGymPlugin.__new__(NemoGymPlugin) + + trainer = MagicMock() + trainer._sync_lora_adapter = MagicMock() + trainer.vllm_generation = MagicMock() + + plugin._setup_lora_sync(trainer) + + # The closure should be installed + trainer.vllm_generation.sync_weights() + trainer._sync_lora_adapter.assert_called_once() + + def test_check_lora_endpoint_skips_non_main_rank(self): + """_check_lora_endpoint should not crash when vllm_client is absent (rank 1).""" + from axolotl.integrations.nemo_gym.plugin import NemoGymPlugin + + vllm_gen = MagicMock(spec=[]) # No attributes at all + # Should not raise + NemoGymPlugin._check_lora_endpoint(vllm_gen) + + +if __name__ == "__main__": + unittest.main() From 678ebb1bb2a6edb3230d6c873e32f06e6ee1b754 Mon Sep 17 00:00:00 2001 From: Matthew Hambrecht <14303543+mhambre@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:38:28 -0400 Subject: [PATCH 1228/1405] Fix Ray train crashing after succeeding (#3542) [skip ci] --- src/axolotl/cli/train.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 6b3bfbd575..2d472ed105 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -84,8 +84,11 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): storage_path=Path(parsed_cfg.output_dir).absolute().as_posix(), ), ) - return trainer.fit() - return do_train(parsed_cfg, parsed_cli_args) + + trainer.fit() + return + + do_train(parsed_cfg, parsed_cli_args) def ray_train_func(kwargs: dict): From ff0f67c730d85c66c901326e1572c2605fb9dc14 Mon Sep 17 00:00:00 2001 From: Avaya Aggarwal <119044997+OnePunchMonk@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:10:31 +0530 Subject: [PATCH 1229/1405] feat: add custom routing support for ernie4_5_moe, and hunyuan_v1_moe (#3526) * feat: add Ernie 4.5 and subsequently custom routing support * Update routing.py * chore: lint * fix minor nits * removed deepseek v2 * remove unneeded change --------- Co-authored-by: Wing Lian --- src/axolotl/integrations/kernels/constants.py | 19 +- .../integrations/kernels/sonicmoe/routing.py | 227 ++++++++++- src/axolotl/loaders/patch_manager.py | 3 +- src/axolotl/monkeypatch/multipack.py | 2 + tests/integrations/test_sonicmoe.py | 360 ++++++++++++++++++ 5 files changed, 589 insertions(+), 22 deletions(-) diff --git a/src/axolotl/integrations/kernels/constants.py b/src/axolotl/integrations/kernels/constants.py index 8002b3f79d..a03761484e 100644 --- a/src/axolotl/integrations/kernels/constants.py +++ b/src/axolotl/integrations/kernels/constants.py @@ -6,6 +6,11 @@ Values can be a single class name (str) or a list of class names for models with multiple MoE block types (e.g. qwen3_omni_moe has Thinker + Talker). + +Models with custom routing (see sonicmoe/routing.py for implementations): +- ernie4_5_moe: softmax→bias correction→topk (softmax_bias_topk_routing) +- deepseek_v2: softmax→group_limited_greedy (softmax_group_limited_topk_routing) +- hunyuan_v1_moe: softmax→topk via gate.wg (softmax_topk_wg_routing) """ import importlib @@ -36,11 +41,15 @@ "glm4v_moe": "Glm4vMoeTextMoE", # sigmoid -> topk routing (no group selection) "minimax_m2": "MiniMaxM2SparseMoeBlock", - # Models below need custom routing (not yet implemented): - # "ernie4_5_moe": "Ernie4_5_MoeSparseMoeBlock", # softmax->topk, e_score_correction_bias between softmax and topk - # "deepseek_v2": "DeepseekV2Moe", # softmax->topk, group_limited_greedy, different attr names (num_group) - # "hunyuan_v1_moe": "HunYuanMoEV1Moe", # softmax->topk, gate.wg (not gate.weight), scatter routing - # "gpt_oss": "GptOssMLP", # topk->softmax, transposed layout [E,H,2*I], custom GLU, expert biases + # softmax->topk, e_score_correction_bias between softmax and topk + "ernie4_5_moe": "Ernie4_5_MoeSparseMoeBlock", + # softmax->topk, group_limited_greedy, different attr names (num_group) + "deepseek_v2": "DeepseekV2Moe", + # softmax->topk, gate.wg (not gate.weight) + "hunyuan_v1_moe": "HunYuanMoEV1Moe", + # TODO: gpt_oss deferred — transposed weight layout [E,H,2*I], expert biases, + # and custom GLU activation require a dedicated forward path in patch.py. + # "gpt_oss": "GptOssMLP", } diff --git a/src/axolotl/integrations/kernels/sonicmoe/routing.py b/src/axolotl/integrations/kernels/sonicmoe/routing.py index fe2d120920..09bffc7421 100644 --- a/src/axolotl/integrations/kernels/sonicmoe/routing.py +++ b/src/axolotl/integrations/kernels/sonicmoe/routing.py @@ -3,9 +3,11 @@ Different MoE architectures use different routing strategies: - qwen3_moe / qwen2_moe / qwen3_5_moe / qwen3_vl_moe / qwen3_omni_moe: softmax -> topk (with optional renormalization) -- gpt_oss: topk -> softmax (uses fused moe_TC_softmax_topk_layer, routing_fn=None) -- glm_moe_dsa: sigmoid -> topk (with group-based expert selection) - mistral4: softmax -> group selection -> topk (with renormalization and scaling) +- glm_moe_dsa / deepseek_v3 / minimax_m2: sigmoid -> topk (with group-based expert selection) +- ernie4_5_moe: softmax -> bias correction -> topk -> gather (softmax_bias_topk_routing) +- hunyuan_v1_moe: softmax -> topk via gate.wg (softmax_topk_wg_routing) +- gpt_oss: topk -> softmax (uses fused moe_TC_softmax_topk_layer, routing_fn=None) [NOT YET SUPPORTED] Each model type maps to a (routing_fn, activation_type, router_attr) triple. When routing_fn is None, the fused moe_TC_softmax_topk_layer path is used. @@ -57,17 +59,10 @@ def get_model_moe_config(model_type: str): "minimax_m2", ): return sigmoid_topk_routing, ActivationType.SWIGLU, "gate" - # elif model_type in ("ernie4_5_moe",): - # # Softmax→topk with e_score_correction_bias applied between softmax and topk. - # return ..., ActivationType.SWIGLU, "gate" - # elif model_type in ("deepseek_v2",): - # # Softmax→topk with group_limited_greedy. Different attr names: num_group - # # (not n_group), gate is nn.Linear (not a router class). - # return ..., ActivationType.SWIGLU, "gate" - # elif model_type in ("hunyuan_v1_moe",): - # # Softmax→topk but gate structure differs: gate.wg (not gate.weight), - # # top_k on block not gate, creates scatter routing matrix. - # return ..., ActivationType.SWIGLU, "gate" + elif model_type in ("ernie4_5_moe",): + return softmax_bias_topk_routing, ActivationType.SWIGLU, "gate" + elif model_type in ("hunyuan_v1_moe",): + return softmax_topk_wg_routing, ActivationType.SWIGLU, "gate" # Fused topk -> softmax path (routing_fn=None): # elif model_type in ("gpt_oss",): # # NOTE: gpt_oss has a router bias which moe_TC_softmax_topk_layer @@ -94,7 +89,7 @@ def softmax_topk_routing( router_logits: [T, E] original logits for aux loss """ gate = moe_block.gate - T, H = hidden_states.shape + T, _ = hidden_states.shape K = gate.top_k # Compute router logits and softmax over all experts @@ -134,7 +129,7 @@ def softmax_group_topk_routing( ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Mistral4-style routing: softmax -> group selection -> topk -> renorm -> scale.""" gate = moe_block.gate - T, H = hidden_states.shape + T, _ = hidden_states.shape K = moe_block.top_k E = getattr(moe_block, "n_routed_experts", gate.weight.shape[0]) n_group = getattr(moe_block, "n_group", 1) @@ -212,7 +207,7 @@ def sigmoid_topk_routing( router_logits: [T, E] original logits for aux loss """ gate = moe_block.gate - T, H = hidden_states.shape + T, _ = hidden_states.shape K = moe_block.top_k E = getattr(moe_block, "n_routed_experts", gate.weight.shape[0]) n_group = getattr(moe_block, "n_group", 1) @@ -276,3 +271,203 @@ def sigmoid_topk_routing( flat_expert_idx = topk_indices.to(torch.int32).reshape(-1) # [T*K] return flat_scores, flat_token_idx, flat_expert_idx, router_logits + + +def softmax_bias_topk_routing( + hidden_states: torch.Tensor, moe_block +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """Ernie 4.5 MoE routing: softmax → bias correction → topk → gather → renorm. + + Differs from standard softmax_topk_routing in three ways: + 1. A learned e_score_correction_bias is added to softmax probs *before* topk + (selection uses biased scores, but final weights use original probs). + 2. The bias is applied via gate.moe_statics module (not a raw tensor). + 3. Renormalization uses clamp(min=norm_min) instead of sum+epsilon. + + Reference: Ernie4_5_MoeTopKRouter.forward in transformers. + + Args: + hidden_states: [T, H] flattened token representations + moe_block: MoE block module (accesses moe_block.gate.*) + + Returns: + router_scores: [T*K] flattened scores (float32) + token_indices: [T*K] which token each entry belongs to (int32), sorted ascending + expert_indices: [T*K] which expert (int32) + router_logits: [T, E] original logits for aux loss + """ + gate = moe_block.gate + T, _ = hidden_states.shape + K = gate.top_k + + # Compute router logits and softmax (force float32 for numerical stability) + router_logits = F.linear(hidden_states.float(), gate.weight.float()) # [T, E] + router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] + + # Bias-corrected scores for expert selection (via moe_statics module) + scores_for_choice = gate.moe_statics(router_probs) # [T, E] + + # Select top-k experts using biased scores + _, selected_experts = torch.topk(scores_for_choice, K, dim=-1) # [T, K] + + # Gather weights from *original* (unbiased) softmax probs + top_values = torch.gather(router_probs, dim=-1, index=selected_experts) # [T, K] + + # Renormalize with clamp(min=norm_min) instead of sum+epsilon + norm_min = getattr(gate, "norm_min", 1e-20) + top_values = top_values / torch.clamp( + top_values.sum(dim=-1, keepdim=True), min=norm_min + ) + + # Flatten for moe_general_routing_inputs + token_indices = ( + torch.arange(T, device=hidden_states.device, dtype=torch.int32) + .unsqueeze(1) + .expand(T, K) + ) + + flat_scores = top_values.to(torch.float32).reshape(-1) # [T*K] + flat_token_idx = token_indices.reshape(-1) # [T*K] + flat_expert_idx = selected_experts.to(torch.int32).reshape(-1) # [T*K] + + return flat_scores, flat_token_idx, flat_expert_idx, router_logits + + +def softmax_group_limited_topk_routing( + hidden_states: torch.Tensor, moe_block +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """DeepSeek V2 routing: softmax → group_limited_greedy/greedy → topk → scale. + + Differs from softmax_group_topk_routing (Mistral4) in several ways: + 1. Uses ``num_group`` attribute (not ``n_group``). + 2. Group score = max per group (not sum of top-2). + 3. Supports ``greedy`` method (plain topk without groups). + 4. No renormalization — just ``topk_weight * routed_scaling_factor``. + 5. Gate is ``nn.Linear`` (access weight via ``gate.weight``). + + Reference: DeepseekV2Moe.route_tokens_to_experts in transformers. + + Args: + hidden_states: [T, H] flattened token representations + moe_block: MoE block module (accesses moe_block.gate, .num_group, + .topk_group, .top_k, .topk_method, .routed_scaling_factor) + + Returns: + router_scores: [T*K] flattened scores (float32) + token_indices: [T*K] which token each entry belongs to (int32), sorted ascending + expert_indices: [T*K] which expert (int32) + router_logits: [T, E] original logits for aux loss + """ + gate = moe_block.gate + T, _ = hidden_states.shape + K = moe_block.top_k + num_group = getattr(moe_block, "num_group", 1) + num_experts = gate.weight.shape[0] + topk_method = getattr(moe_block, "topk_method", "greedy") + + # Compute logits in float32 and softmax + router_logits = F.linear(hidden_states.float(), gate.weight.float()) # [T, E] + router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] + + if topk_method == "greedy" or num_group == 1: + topk_weights, topk_indices = torch.topk(router_probs, k=K, dim=-1, sorted=False) + elif topk_method == "group_limited_greedy": + # Guard: selected groups must contain enough experts for topk + group_size = num_experts // num_group + if moe_block.topk_group * group_size < K: + raise ValueError( + f"DeepSeek V2: topk_group ({moe_block.topk_group}) * group_size " + f"({group_size}) = {moe_block.topk_group * group_size} < top_k ({K}). " + f"Not enough experts in selected groups for topk selection." + ) + # Group selection: pick top groups by max score per group + group_scores = ( + router_probs.view(T, num_group, num_experts // num_group).max(dim=-1).values + ) # [T, num_group] + group_idx = torch.topk( + group_scores, k=moe_block.topk_group, dim=-1, sorted=False + )[1] + group_mask = torch.zeros_like(group_scores) + group_mask.scatter_(1, group_idx, 1) + score_mask = ( + group_mask.unsqueeze(-1) + .expand(T, num_group, num_experts // num_group) + .reshape(T, -1) + ) + tmp_scores = router_probs.masked_fill(~score_mask.bool(), 0.0) + topk_weights, topk_indices = torch.topk(tmp_scores, k=K, dim=-1, sorted=False) + else: + raise ValueError( + f"DeepSeek V2: unsupported topk_method '{topk_method}'. " + f"Expected 'greedy' or 'group_limited_greedy'." + ) + + # Scale only — no renormalization (weights won't sum to 1.0 per token). + # This matches the reference DeepseekV2Moe.route_tokens_to_experts behavior. + routed_scaling_factor = getattr(moe_block, "routed_scaling_factor", 1.0) + topk_weights = topk_weights * routed_scaling_factor + + # Flatten for moe_general_routing_inputs + token_indices = ( + torch.arange(T, device=hidden_states.device, dtype=torch.int32) + .unsqueeze(1) + .expand(T, K) + ) + + flat_scores = topk_weights.to(torch.float32).reshape(-1) # [T*K] + flat_token_idx = token_indices.reshape(-1) # [T*K] + flat_expert_idx = topk_indices.to(torch.int32).reshape(-1) # [T*K] + + return flat_scores, flat_token_idx, flat_expert_idx, router_logits + + +def softmax_topk_wg_routing( + hidden_states: torch.Tensor, moe_block +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """HunYuan V1 MoE routing: softmax → topk → renorm (gate weight via gate.wg). + + Differs from standard softmax_topk_routing in: + 1. Gate weight lives at ``gate.wg.weight`` (not ``gate.weight``). + 2. ``top_k`` is on ``moe_block`` (not ``gate``). + 3. Always renormalizes (no ``norm_topk_prob`` flag). + + Reference: HunYuanMoEV1Moe.route_tokens_to_experts and + HunYuanMoEV1Gate.forward in transformers. + + Args: + hidden_states: [T, H] flattened token representations + moe_block: MoE block module (accesses moe_block.gate.wg, moe_block.top_k) + + Returns: + router_scores: [T*K] flattened scores (float32) + token_indices: [T*K] which token each entry belongs to (int32), sorted ascending + expert_indices: [T*K] which expert (int32) + router_logits: [T, E] original logits for aux loss + """ + gate = moe_block.gate + T, _ = hidden_states.shape + K = moe_block.top_k + + # Gate computes logits via gate.wg (nn.Linear, float32) + wg = gate.wg + router_logits = F.linear(hidden_states.float(), wg.weight.float()) # [T, E] + router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] + + # Select top-k experts + top_values, top_indices = torch.topk(router_probs, K, dim=-1) # [T, K] each + + # Always renormalize (HunYuan V1 has no norm_topk_prob flag) + top_values = top_values / (top_values.sum(dim=-1, keepdim=True) + 1e-20) + + # Flatten for moe_general_routing_inputs + token_indices = ( + torch.arange(T, device=hidden_states.device, dtype=torch.int32) + .unsqueeze(1) + .expand(T, K) + ) + + flat_scores = top_values.to(torch.float32).reshape(-1) # [T*K] + flat_token_idx = token_indices.reshape(-1) # [T*K] + flat_expert_idx = top_indices.to(torch.int32).reshape(-1) # [T*K] + + return flat_scores, flat_token_idx, flat_expert_idx, router_logits diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 38cc198d3a..756eef8867 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -590,7 +590,8 @@ def _patch_llama_derived_model(self): def _apply_llama_flash_attn_patches(self, model): """Apply LLaMA-specific flash attention patches.""" if ( - self.model_config.model_type in ["llama", "llama4"] + self.model_config.model_type + in ["llama", "llama4", "ernie4_5", "ernie4_5_moe"] and not self.cfg.trust_remote_code and not self.cfg.gptq and self.cfg.flash_attention diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 78087acbc4..8566af526c 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -52,6 +52,8 @@ "seed_oss", "lfm2", "lfm2_moe", + "ernie4_5", + "ernie4_5_moe", "olmo", "olmo2", "olmo3", diff --git a/tests/integrations/test_sonicmoe.py b/tests/integrations/test_sonicmoe.py index e6294f5645..7d26d9d936 100644 --- a/tests/integrations/test_sonicmoe.py +++ b/tests/integrations/test_sonicmoe.py @@ -426,3 +426,363 @@ def test_bias_on_block_not_gate(self): expert_idx_2d = expert_idx.reshape(T, K) for t in range(T): assert 0 in expert_idx_2d[t] + + +# ============================================================================ +# Ernie 4.5 MoE: softmax -> bias correction -> topk +# ============================================================================ + + +def _make_ernie_moe_block(T=8, H=16, E=8, K=2, norm_min=1e-20): + """Create a mock Ernie 4.5 MoE block for routing tests. + + Ernie 4.5 uses a gate.moe_statics module that adds bias to softmax probs + before topk selection, then gathers from original probs. + """ + bias = torch.zeros(E) + + class MockMoeStatics: + def __init__(self, bias_tensor): + self.e_score_correction_bias = bias_tensor + + def __call__(self, probs): + return probs + self.e_score_correction_bias + + gate = SimpleNamespace( + weight=torch.randn(E, H), + top_k=K, + moe_statics=MockMoeStatics(bias), + norm_min=norm_min, + ) + moe_block = SimpleNamespace(gate=gate) + return moe_block, bias, T, H, E, K + + +class TestSoftmaxBiasTopkRouting: + """Tests for Ernie 4.5 MoE routing (softmax_bias_topk_routing).""" + + def test_output_shapes(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_bias_topk_routing, + ) + + moe_block, _, T, H, E, K = _make_ernie_moe_block() + hidden = torch.randn(T, H) + + scores, token_idx, expert_idx, logits = softmax_bias_topk_routing( + hidden, moe_block + ) + + assert scores.shape == (T * K,) + assert token_idx.shape == (T * K,) + assert expert_idx.shape == (T * K,) + assert logits.shape == (T, E) + + def test_scores_are_float32(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_bias_topk_routing, + ) + + moe_block, _, T, H, E, K = _make_ernie_moe_block() + hidden = torch.randn(T, H) + + scores, _, _, _ = softmax_bias_topk_routing(hidden, moe_block) + assert scores.dtype == torch.float32 + + def test_token_indices_sorted_ascending(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_bias_topk_routing, + ) + + moe_block, _, T, H, E, K = _make_ernie_moe_block() + hidden = torch.randn(T, H) + + _, token_idx, _, _ = softmax_bias_topk_routing(hidden, moe_block) + diffs = token_idx[1:] - token_idx[:-1] + assert (diffs >= 0).all() + + def test_expert_indices_in_range(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_bias_topk_routing, + ) + + moe_block, _, T, H, E, K = _make_ernie_moe_block() + hidden = torch.randn(T, H) + + _, _, expert_idx, _ = softmax_bias_topk_routing(hidden, moe_block) + assert (expert_idx >= 0).all() + assert (expert_idx < E).all() + + def test_renormalized_scores_sum_to_one(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_bias_topk_routing, + ) + + moe_block, _, T, H, E, K = _make_ernie_moe_block() + hidden = torch.randn(T, H) + + scores, _, _, _ = softmax_bias_topk_routing(hidden, moe_block) + per_token_sums = scores.reshape(T, K).sum(dim=-1) + assert torch.allclose(per_token_sums, torch.ones(T), atol=1e-5) + + def test_bias_affects_expert_selection(self): + """Large positive bias on expert 0 should make it always selected.""" + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_bias_topk_routing, + ) + + moe_block, bias, T, H, E, K = _make_ernie_moe_block() + bias[0] = 100.0 # mutate the bias to strongly favor expert 0 + hidden = torch.randn(T, H) + + _, _, expert_idx, _ = softmax_bias_topk_routing(hidden, moe_block) + expert_idx_2d = expert_idx.reshape(T, K) + for t in range(T): + assert 0 in expert_idx_2d[t] + + +# ============================================================================ +# DeepSeek V2: softmax -> group_limited_greedy / greedy -> topk +# ============================================================================ + + +def _make_deepseek_v2_moe_block( + T=8, H=16, E=16, K=4, num_group=2, topk_group=1, topk_method="group_limited_greedy" +): + """Create a mock DeepSeek V2 MoE block for routing tests. + + DeepSeek V2 uses num_group (not n_group), gate is nn.Linear, + and supports greedy / group_limited_greedy topk methods. + """ + gate = SimpleNamespace(weight=torch.randn(E, H)) + moe_block = SimpleNamespace( + gate=gate, + top_k=K, + num_group=num_group, + topk_group=topk_group, + topk_method=topk_method, + routed_scaling_factor=1.0, + ) + return moe_block, T, H, E, K + + +class TestSoftmaxGroupLimitedTopkRouting: + """Tests for DeepSeek V2 routing (softmax_group_limited_topk_routing).""" + + def test_output_shapes_group_limited(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_group_limited_topk_routing, + ) + + moe_block, T, H, E, K = _make_deepseek_v2_moe_block( + topk_method="group_limited_greedy" + ) + hidden = torch.randn(T, H) + + scores, token_idx, expert_idx, logits = softmax_group_limited_topk_routing( + hidden, moe_block + ) + + assert scores.shape == (T * K,) + assert token_idx.shape == (T * K,) + assert expert_idx.shape == (T * K,) + assert logits.shape == (T, E) + + def test_output_shapes_greedy(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_group_limited_topk_routing, + ) + + moe_block, T, H, E, K = _make_deepseek_v2_moe_block(topk_method="greedy") + hidden = torch.randn(T, H) + + scores, token_idx, expert_idx, logits = softmax_group_limited_topk_routing( + hidden, moe_block + ) + + assert scores.shape == (T * K,) + assert logits.shape == (T, E) + + def test_scores_are_float32(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_group_limited_topk_routing, + ) + + moe_block, T, H, E, K = _make_deepseek_v2_moe_block() + hidden = torch.randn(T, H) + + scores, _, _, _ = softmax_group_limited_topk_routing(hidden, moe_block) + assert scores.dtype == torch.float32 + + def test_token_indices_sorted_ascending(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_group_limited_topk_routing, + ) + + moe_block, T, H, E, K = _make_deepseek_v2_moe_block() + hidden = torch.randn(T, H) + + _, token_idx, _, _ = softmax_group_limited_topk_routing(hidden, moe_block) + diffs = token_idx[1:] - token_idx[:-1] + assert (diffs >= 0).all() + + def test_expert_indices_in_range(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_group_limited_topk_routing, + ) + + moe_block, T, H, E, K = _make_deepseek_v2_moe_block() + hidden = torch.randn(T, H) + + _, _, expert_idx, _ = softmax_group_limited_topk_routing(hidden, moe_block) + assert (expert_idx >= 0).all() + assert (expert_idx < E).all() + + def test_scaling_factor_applied(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_group_limited_topk_routing, + ) + + moe_block, T, H, E, K = _make_deepseek_v2_moe_block(topk_method="greedy") + hidden = torch.randn(T, H) + + scores_1x, _, _, _ = softmax_group_limited_topk_routing(hidden, moe_block) + + moe_block.routed_scaling_factor = 2.5 + scores_2x, _, _, _ = softmax_group_limited_topk_routing(hidden, moe_block) + + assert torch.allclose(scores_2x, scores_1x * 2.5, atol=1e-5) + + def test_group_selection_restricts_experts(self): + """With num_group=4 and topk_group=1, experts should come from selected groups.""" + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_group_limited_topk_routing, + ) + + moe_block, T, H, E, K = _make_deepseek_v2_moe_block( + E=16, K=2, num_group=4, topk_group=1, topk_method="group_limited_greedy" + ) + hidden = torch.randn(T, H) + + _, _, expert_idx, _ = softmax_group_limited_topk_routing(hidden, moe_block) + expert_idx_2d = expert_idx.reshape(T, K) + group_size = E // moe_block.num_group + for t in range(T): + experts = expert_idx_2d[t] + groups = experts // group_size + # All selected experts should be from the same group + assert (groups == groups[0]).all() + + def test_unsupported_topk_method_raises(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_group_limited_topk_routing, + ) + + moe_block, T, H, E, K = _make_deepseek_v2_moe_block(topk_method="invalid") + hidden = torch.randn(T, H) + + with pytest.raises(ValueError, match="unsupported topk_method"): + softmax_group_limited_topk_routing(hidden, moe_block) + + +# ============================================================================ +# HunYuan V1 MoE: softmax -> topk -> renorm (via gate.wg) +# ============================================================================ + + +def _make_hunyuan_moe_block(T=8, H=16, E=8, K=2): + """Create a mock HunYuan V1 MoE block for routing tests. + + HunYuan V1 uses gate.wg (nn.Linear-like) instead of gate.weight, + and top_k on the moe_block instead of the gate. + """ + wg = SimpleNamespace(weight=torch.randn(E, H)) + gate = SimpleNamespace(wg=wg) + moe_block = SimpleNamespace(gate=gate, top_k=K) + return moe_block, T, H, E, K + + +class TestSoftmaxTopkWgRouting: + """Tests for HunYuan V1 MoE routing (softmax_topk_wg_routing).""" + + def test_output_shapes(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_topk_wg_routing, + ) + + moe_block, T, H, E, K = _make_hunyuan_moe_block() + hidden = torch.randn(T, H) + + scores, token_idx, expert_idx, logits = softmax_topk_wg_routing( + hidden, moe_block + ) + + assert scores.shape == (T * K,) + assert token_idx.shape == (T * K,) + assert expert_idx.shape == (T * K,) + assert logits.shape == (T, E) + + def test_scores_are_float32(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_topk_wg_routing, + ) + + moe_block, T, H, E, K = _make_hunyuan_moe_block() + hidden = torch.randn(T, H) + + scores, _, _, _ = softmax_topk_wg_routing(hidden, moe_block) + assert scores.dtype == torch.float32 + + def test_token_indices_sorted_ascending(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_topk_wg_routing, + ) + + moe_block, T, H, E, K = _make_hunyuan_moe_block() + hidden = torch.randn(T, H) + + _, token_idx, _, _ = softmax_topk_wg_routing(hidden, moe_block) + diffs = token_idx[1:] - token_idx[:-1] + assert (diffs >= 0).all() + + def test_expert_indices_in_range(self): + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_topk_wg_routing, + ) + + moe_block, T, H, E, K = _make_hunyuan_moe_block() + hidden = torch.randn(T, H) + + _, _, expert_idx, _ = softmax_topk_wg_routing(hidden, moe_block) + assert (expert_idx >= 0).all() + assert (expert_idx < E).all() + + def test_renormalized_scores_sum_to_one(self): + """HunYuan V1 always renormalizes (no norm_topk_prob flag).""" + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_topk_wg_routing, + ) + + moe_block, T, H, E, K = _make_hunyuan_moe_block() + hidden = torch.randn(T, H) + + scores, _, _, _ = softmax_topk_wg_routing(hidden, moe_block) + per_token_sums = scores.reshape(T, K).sum(dim=-1) + assert torch.allclose(per_token_sums, torch.ones(T), atol=1e-5) + + def test_uses_gate_wg_weight(self): + """Verify that modifying gate.wg.weight changes the routing output.""" + from axolotl.integrations.kernels.sonicmoe.routing import ( + softmax_topk_wg_routing, + ) + + moe_block, T, H, E, K = _make_hunyuan_moe_block() + hidden = torch.randn(T, H) + + scores1, _, _, _ = softmax_topk_wg_routing(hidden, moe_block) + + # Change the wg weight and verify scores change + moe_block.gate.wg.weight = torch.randn(E, H) + scores2, _, _, _ = softmax_topk_wg_routing(hidden, moe_block) + + assert not torch.equal(scores1, scores2) From b55706b9f6286003bd2385832396042e901d814e Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:11:32 +0530 Subject: [PATCH 1230/1405] feat:merge-lora iterate through bins without loading (#3095) * merge_method added * merge_efficient core implement * Update src/axolotl/cli/merge_lora.py Co-authored-by: Wing Lian * Update src/axolotl/utils/lora_merge_efficient.py Co-authored-by: Wing Lian * standard to leagcy + rstrip + try/except for do_merge_lora_efficient(cfg=cfg) * fix: 'dict' object has no attribute 'lora_alpha' * into -> debug * lint * lint2 * moved everythign to cpu + peformance improvments * lint * Update src/axolotl/cli/merge_lora.py Co-authored-by: Dan Saunders * Update src/axolotl/cli/merge_lora.py Co-authored-by: Dan Saunders * string handeling + try except remove * merge_method -> merge_lora_methods * remove duplicate cal + safetensor + move to lora_merge.py * lint * handle quant-dequant, handle experts * fix parameter merging and prefer peft's native merge logic per module --------- Co-authored-by: Wing Lian Co-authored-by: Dan Saunders --- src/axolotl/cli/merge_lora.py | 85 ++- src/axolotl/cli/utils/lora_merge.py | 982 +++++++++++++++++++++++++++ src/axolotl/monkeypatch/moe_quant.py | 47 +- src/axolotl/utils/schemas/peft.py | 6 + tests/utils/lora/test_merge_lora.py | 620 +++++++++++++++++ 5 files changed, 1735 insertions(+), 5 deletions(-) create mode 100644 src/axolotl/cli/utils/lora_merge.py diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index bc2dc84c76..dae9f317d6 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -4,9 +4,11 @@ from typing import Union import fire +import torch from axolotl.cli.config import load_cfg from axolotl.cli.utils import load_model_and_tokenizer +from axolotl.cli.utils.lora_merge import merge_lora_sharded_efficient from axolotl.telemetry.errors import send_errors from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger @@ -17,12 +19,26 @@ @send_errors def do_merge_lora(*, cfg: DictDefault) -> None: """ - Calls `transformers`' `merge_and_unload` on the model given in the `axolotl` config - along with the LoRA adapters to combine them into a single base model. + Merges LoRA adapters with base model using either memory-efficient or legacy approach. Args: cfg: Dictionary mapping `axolotl` config keys to values. """ + merge_method = str(getattr(cfg, "merge_method", "memory_efficient")) + if merge_method == "legacy": + LOG.debug("Using legacy LoRA merging method...") + _do_merge_lora_legacy(cfg=cfg) + else: + LOG.debug("Using memory-efficient LoRA merging method...") + _do_merge_lora_efficient(cfg=cfg) + + +def _do_merge_lora_legacy(*, cfg: DictDefault) -> None: + """ + Legacy LoRA merging using merge_and_unload. + Loads the full model into memory before merging. + """ + LOG.debug("Using legacy LoRA merging method...") model, tokenizer, processor = load_model_and_tokenizer(cfg=cfg) LOG.info("Running merge of LoRA with base model...") @@ -52,6 +68,58 @@ def do_merge_lora(*, cfg: DictDefault) -> None: processor.save_pretrained(str(Path(cfg.output_dir) / "merged")) +def _do_merge_lora_efficient(*, cfg: DictDefault) -> None: + """ + Memory-efficient LoRA merging using shard-by-shard processing. + Does not load the full model into memory. + + Supports standard LoRA, RSLoRA, and DoRA. Unsupported methods (AdaLoRA, VeRA) + will raise NotImplementedError — use legacy method for those. + """ + LOG.debug("Using memory-efficient LoRA merging method...") + + output_path = Path(cfg.output_dir) / "merged" + safe_tensors = getattr(cfg, "save_safetensors", True) + device = "cuda" if torch.cuda.is_available() else "cpu" + + # Detect NF4 quantization from config to simulate QLoRA training dynamics. + # Check both current and original (pre-override) config values since do_cli + # forces load_in_4bit=False for the legacy path. + simulate_nf4 = bool( + getattr(cfg, "load_in_4bit", False) + or getattr(cfg, "_original_load_in_4bit", False) + or getattr(cfg, "adapter", None) == "qlora" + or getattr(cfg, "_original_adapter", None) == "qlora" + ) + + bnb_config_kwargs = getattr(cfg, "bnb_config_kwargs", None) or {} + nf4_blocksize = bnb_config_kwargs.get("blocksize", None) + nf4_double_quant = bnb_config_kwargs.get( + "bnb_4bit_use_double_quant", + getattr(cfg, "bnb_4bit_use_double_quant", True), + ) + + # Detect MoE expert quantization + simulate_nf4_experts = bool( + getattr(cfg, "quantize_moe_experts", False) + or getattr(cfg, "_original_quantize_moe_experts", False) + ) + + merge_lora_sharded_efficient( + base_model_path=cfg.base_model, + lora_adapter_path=cfg.lora_model_dir, + output_path=output_path, + safe_tensors=safe_tensors, + device=device, + simulate_nf4=simulate_nf4, + simulate_nf4_experts=simulate_nf4_experts, + nf4_blocksize=nf4_blocksize, + nf4_double_quant=nf4_double_quant, + ) + + LOG.debug("Memory-efficient LoRA merge completed successfully!") + + def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: """ Parses `axolotl` config, CLI args, and calls `do_merge_lora`. Note that various @@ -66,6 +134,12 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: ValueError: If target directory for LoRA merged model does not exist. """ + # Pre-load config to detect original quantization settings before overrides + raw_cfg = load_cfg(config, **kwargs) + original_load_in_4bit = getattr(raw_cfg, "load_in_4bit", False) + original_adapter = getattr(raw_cfg, "adapter", None) + original_quantize_moe_experts = getattr(raw_cfg, "quantize_moe_experts", False) + parsed_cfg = load_cfg( config, merge_lora=True, @@ -80,11 +154,16 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: **kwargs, ) + # Stash original quantization settings for NF4 simulation in efficient merge + parsed_cfg._original_load_in_4bit = original_load_in_4bit + parsed_cfg._original_adapter = original_adapter + parsed_cfg._original_quantize_moe_experts = original_quantize_moe_experts + if not parsed_cfg.lora_model_dir and parsed_cfg.output_dir: parsed_cfg.lora_model_dir = parsed_cfg.output_dir if not Path(parsed_cfg.lora_model_dir).exists(): raise ValueError( - f"Target directory for merge: `{parsed_cfg.lora_model_dir}` does not exist." + f"Target directory for LoRA adapter weights does not exist: `{parsed_cfg.lora_model_dir}`" ) do_merge_lora(cfg=parsed_cfg) diff --git a/src/axolotl/cli/utils/lora_merge.py b/src/axolotl/cli/utils/lora_merge.py new file mode 100644 index 0000000000..339e41e2d5 --- /dev/null +++ b/src/axolotl/cli/utils/lora_merge.py @@ -0,0 +1,982 @@ +import gc +import math +import os +import shutil +from pathlib import Path +from typing import Dict, Optional, Union + +import safetensors +import safetensors.torch +import torch +from huggingface_hub import snapshot_download +from peft import LoraConfig +from tqdm import tqdm + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def _simulate_nf4_roundtrip( + tensor: torch.Tensor, + blocksize: Optional[int] = None, + compress_statistics: bool = True, + device: Optional[Union[str, torch.device]] = None, +) -> torch.Tensor: + """ + Simulate NF4 quantization roundtrip to match QLoRA training dynamics. + + During QLoRA training, base weights are quantized to NF4 and dequantized on-the-fly + for each forward pass. The LoRA adapters learn to compensate for the quantization + noise in the dequantized weights. To match this at merge time, we apply the same + quantize → dequantize roundtrip so the merged result reflects what the model saw + during training. + + Args: + tensor: Base model weight tensor (fp16/bf16/fp32) + blocksize: NF4 quantization block size (default: bitsandbytes default) + compress_statistics: Whether to use double quantization + device: Device for quantization computation. bitsandbytes requires a + CUDA device; defaults to "cuda" when available. + + Returns: + Tensor after NF4 quantize → dequantize roundtrip, in original dtype + """ + import bitsandbytes.functional as bnb_F + + quant_device: torch.device + if device is None: + quant_device = torch.device("cuda") + elif isinstance(device, str): + quant_device = torch.device(device) + else: + quant_device = device + + if quant_device.type == "cuda" and not torch.cuda.is_available(): + raise RuntimeError( + "NF4 simulation requires CUDA but no GPU is available. " + "Either run on a machine with a GPU or disable NF4 simulation." + ) + + original_dtype = tensor.dtype + original_shape = tensor.shape + + # bitsandbytes requires float32 input for quantization and contiguous+CUDA tensor + flat = tensor.reshape(-1).to(torch.float32).contiguous().to(quant_device) + + quant_kwargs = { + "quant_type": "nf4", + "compress_statistics": compress_statistics, + } + if blocksize is not None: + quant_kwargs["blocksize"] = blocksize + + quantized, quant_state = bnb_F.quantize_4bit(flat, **quant_kwargs) + dequantized = bnb_F.dequantize_4bit(quantized, quant_state, quant_type="nf4") + + return dequantized.reshape(original_shape).to(original_dtype).cpu() + + +def find_lora_weights( + lora_state: Dict[str, torch.Tensor], + key: str, + weight_renamings: Optional[Dict[str, str]] = None, +) -> tuple[Optional[torch.Tensor], Optional[torch.Tensor]]: + """ + Find corresponding LoRA A and B weights for a given key. + + Also tries keys after applying weight renamings (from transformers v5 + conversion mappings) in case the checkpoint key names differ from the + runtime model key names used by the LoRA adapter. + """ + import re + + clean_key = key[:-7] if key.endswith(".weight") else key + + # Try the direct key first + a_key = f"base_model.model.{clean_key}.lora_A.weight" + b_key = f"base_model.model.{clean_key}.lora_B.weight" + + lora_a = lora_state.get(a_key) + lora_b = lora_state.get(b_key) + + if lora_a is not None and lora_b is not None: + return lora_a, lora_b + + # Try renamed keys (checkpoint format → runtime format) + if weight_renamings: + for src_pattern, tgt_pattern in weight_renamings.items(): + renamed_key = re.sub(src_pattern, tgt_pattern, clean_key) + if renamed_key != clean_key: + a_key = f"base_model.model.{renamed_key}.lora_A.weight" + b_key = f"base_model.model.{renamed_key}.lora_B.weight" + lora_a = lora_state.get(a_key) + lora_b = lora_state.get(b_key) + if lora_a is not None and lora_b is not None: + return lora_a, lora_b + + return None, None + + +def _find_param_wrapper_lora( + lora_state: Dict[str, torch.Tensor], + key: str, + tensor_shape: Optional[tuple] = None, +) -> tuple[Optional[torch.Tensor], Optional[torch.Tensor], Optional[str]]: + """ + Find LoRA weights from a ParamWrapper (lora_target_parameters) that targets + a parent module containing this weight as a sub-parameter. + + For example, base weight key 'model.layers.0.mlp.experts.down_proj' may have + LoRA at 'base_model.model.model.layers.0.mlp.experts.lora_A.weight' (targeting + the 'experts' module with 'down_proj' as the parameter_name). + + When tensor_shape is provided, validates that the LoRA dimensions match the + target tensor (important when multiple ParamWrappers are nested and each + nesting level has different LoRA dimensions). + + Returns (lora_A, lora_B, parameter_name) or (None, None, None). + """ + clean_key = key[:-7] if key.endswith(".weight") else key + # Strip trailing parameter name to get the parent module path + # e.g., "model.layers.0.mlp.experts.down_proj" → parent="model.layers.0.mlp.experts", param="down_proj" + parts = clean_key.rsplit(".", 1) + if len(parts) != 2: + return None, None, None + + parent_key, param_name = parts + + # PEFT's ParamWrapper nesting: when multiple parameters are targeted on + # the same module, it nests wrappers. The outer wrapper's LoRA is at + # parent.lora_A/B and inner wrappers use parent.base_layer.lora_A/B, + # parent.base_layer.base_layer.lora_A/B, etc. + prefixes_to_try = [ + f"base_model.model.{parent_key}", + ] + # Walk up .base_layer nesting levels (typically 1-2 deep) + for depth in range(1, 4): + bl = ".base_layer" * depth + prefixes_to_try.append(f"base_model.model.{parent_key}{bl}") + + for prefix in prefixes_to_try: + a_key = f"{prefix}.lora_A.weight" + b_key = f"{prefix}.lora_B.weight" + lora_a = lora_state.get(a_key) + lora_b = lora_state.get(b_key) + if lora_a is None or lora_b is None: + continue + + # When tensor_shape is given, verify dimensions match before returning. + # This prevents returning a mismatched LoRA from a different nesting level. + if tensor_shape is not None and len(tensor_shape) >= 3: + num_experts = tensor_shape[0] + if not ( + lora_a.shape[0] == lora_b.shape[1] + and lora_a.shape[0] % num_experts == 0 + and lora_a.shape[1] == tensor_shape[1] + and lora_b.shape[0] == tensor_shape[2] + ): + continue # Dimensions don't match, try next nesting level + + return lora_a, lora_b, param_name + + return None, None, None + + +def _build_peft_layer_and_get_delta( + lora_a: torch.Tensor, + lora_b: torch.Tensor, + lora_config_dict: Dict, + base_tensor: torch.Tensor, + adapter_name: str = "default", + is_param_wrapper: bool = False, + magnitude: Optional[torch.Tensor] = None, +) -> torch.Tensor: + """ + Use PEFT's own layer classes to compute the LoRA delta weight. + + Instead of re-implementing the merge math for every LoRA variant, this + constructs a lightweight PEFT layer, loads the A/B weights, and calls + ``get_delta_weight`` (or ``merge`` for DoRA) which handles standard LoRA, + RSLoRA, DoRA, and ParamWrapper (expert-blocked) LoRA. + + Returns the delta tensor (same shape as base_tensor). + """ + import warnings + + import torch.nn as nn + + r_total = lora_a.shape[0] + in_features = lora_a.shape[1] + out_features = lora_b.shape[0] + lora_alpha = lora_config_dict.get("lora_alpha", lora_config_dict.get("r", 1)) + use_rslora = bool(lora_config_dict.get("use_rslora", False)) + use_dora = bool(lora_config_dict.get("use_dora", False)) and magnitude is not None + + if is_param_wrapper: + from peft.tuners.lora.layer import ParamWrapper + + num_experts = base_tensor.shape[0] + r = r_total // num_experts + + class _FakeModule(nn.Module): + pass + + fake = _FakeModule() + fake.register_parameter( + "weight", nn.Parameter(base_tensor.clone(), requires_grad=False) + ) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + layer = ParamWrapper( + fake, + adapter_name=adapter_name, + parameter_name="weight", + r=r, + lora_alpha=lora_alpha, + use_rslora=use_rslora, + ) + layer.lora_A[adapter_name].weight.data = lora_a + layer.lora_B[adapter_name].weight.data = lora_b + return layer.get_delta_weight(adapter_name) + else: + from peft.tuners.lora.layer import Linear as LoraLinear + + base_layer = nn.Linear(in_features, out_features, bias=False) + base_layer.weight.data = base_tensor.clone() + + fan_in_fan_out = bool( + lora_config_dict.get("fan_in_fan_out", False) + or lora_config_dict.get("lora_fan_in_fan_out", False) + ) + + layer = LoraLinear( + base_layer, + adapter_name=adapter_name, + r=r_total, + lora_alpha=lora_alpha, + fan_in_fan_out=fan_in_fan_out, + use_rslora=use_rslora, + use_dora=use_dora, + ) + layer.lora_A[adapter_name].weight.data = lora_a + layer.lora_B[adapter_name].weight.data = lora_b + + if use_dora: + # DoRA merges magnitude normalization into the weight directly. + # Use PEFT's merge() which handles DoRA internally, then + # compute the delta as merged_weight - original_weight. + mag_layer = layer.lora_magnitude_vector[adapter_name] + mag_layer.weight = nn.Parameter(magnitude) + layer.merge(adapter_names=[adapter_name]) + return base_layer.weight.data - base_tensor + + return layer.get_delta_weight(adapter_name) + + +def get_model_shards(model_path: Path) -> list[Path]: + """Find all model shards in the given path.""" + shards: list[Path] = [] + + patterns = ["model*.safetensors", "pytorch_model*.bin"] + + for pattern in patterns: + shards.extend(model_path.glob(pattern)) + if shards: + break + + return sorted(shards) + + +def copy_non_model_files( + input_path: Path, output_path: Path, model_shards: list[Path] +) -> None: + """ + Copy all non-model files to the output directory. + + Args: + input_path: Source directory + output_path: Destination directory + model_shards: List of model shard files to skip + """ + LOG.info("Copying non-model files to output directory...") + + shard_names = {shard.name for shard in model_shards} + + for filepath in input_path.glob("*"): + if filepath.is_dir(): + continue + if filepath.name in shard_names: + continue + if ( + filepath.name.startswith("model") and filepath.suffix == ".safetensors" + ) or (filepath.name.startswith("pytorch_model") and filepath.suffix == ".bin"): + continue + if filepath.suffix == ".gguf": + continue + # Skip weight-map index files — they reference shard filenames that may + # change during the merge (e.g. .bin → .safetensors). A correct index + # is regenerated after all shards have been written. + if filepath.name.endswith(".index.json"): + continue + + LOG.debug(f"Copying {filepath.name} to output") + shutil.copy2(filepath, output_path) + + +def _find_dora_magnitude( + lora_state: Dict[str, torch.Tensor], + key: str, + weight_renamings: Optional[Dict[str, str]] = None, +) -> Optional[torch.Tensor]: + """ + Find DoRA magnitude vector for a given key. + """ + import re + + clean_key = key[:-7] if key.endswith(".weight") else key + mag_key = f"base_model.model.{clean_key}.lora_magnitude_vector" + result = lora_state.get(mag_key) + if result is not None: + return result + + if weight_renamings: + for src_pattern, tgt_pattern in weight_renamings.items(): + renamed_key = re.sub(src_pattern, tgt_pattern, clean_key) + if renamed_key != clean_key: + mag_key = f"base_model.model.{renamed_key}.lora_magnitude_vector" + result = lora_state.get(mag_key) + if result is not None: + return result + + return None + + +def _should_nf4_roundtrip( + key: str, + tensor: torch.Tensor, + simulate_nf4: bool, + simulate_nf4_experts: bool, +) -> bool: + """Determine if a tensor should undergo NF4 quantization roundtrip.""" + if tensor.ndim < 2: + return False + if simulate_nf4: + return True + if simulate_nf4_experts and tensor.ndim >= 3 and "expert" in key.lower(): + return True + return False + + +def _merge_tensor_with_lora( + tensor: torch.Tensor, + key: str, + lora_state: Dict[str, torch.Tensor], + scale: float, + lora_config_dict: Dict, + device: str, + simulate_nf4: bool = False, + simulate_nf4_experts: bool = False, + nf4_blocksize: Optional[int] = None, + nf4_double_quant: bool = True, + use_dora: bool = False, + weight_renamings: Optional[Dict[str, str]] = None, +) -> tuple[torch.Tensor, bool]: + """ + Helper function to merge a single tensor with its corresponding LoRA weights. + + Args: + tensor: Base model tensor + key: Tensor key/name + lora_state: Dictionary containing LoRA weights + scale: LoRA scaling factor (alpha/r) + lora_config_dict: LoRA configuration dictionary + device: Device to perform computations on + simulate_nf4: Whether to simulate NF4 quantization roundtrip for all weights + simulate_nf4_experts: Whether to simulate NF4 roundtrip for MoE expert tensors only + nf4_blocksize: Block size for NF4 quantization + nf4_double_quant: Whether to use double quantization + use_dora: Whether to apply DoRA (Weight-Decomposed LoRA) merging + weight_renamings: Optional key renamings from transformers conversion mapping + + Returns: + Tuple of (merged tensor, whether LoRA was applied) + """ + lora_a, lora_b = find_lora_weights(lora_state, key, weight_renamings) + + do_nf4 = _should_nf4_roundtrip(key, tensor, simulate_nf4, simulate_nf4_experts) + + if lora_a is not None and lora_b is not None: + LOG.debug(f"Merging LoRA for {key}: {lora_a.shape}, {lora_b.shape}") + + original_dtype = tensor.dtype + + # Simulate NF4 quantization roundtrip to match QLoRA training dynamics + if do_nf4: + tensor = _simulate_nf4_roundtrip( + tensor, + blocksize=nf4_blocksize, + compress_statistics=nf4_double_quant, + device=device, + ) + + magnitude = ( + _find_dora_magnitude(lora_state, key, weight_renamings) + if use_dora + else None + ) + delta = _build_peft_layer_and_get_delta( + lora_a.to(device), + lora_b.to(device), + lora_config_dict, + tensor.to(device), + magnitude=magnitude.to(device) if magnitude is not None else None, + ) + merged_tensor = ( + (tensor.to(device).to(torch.float32) + delta.to(torch.float32)) + .to(original_dtype) + .detach() + .cpu() + ) + return merged_tensor, True + else: + # Try ParamWrapper LoRA (lora_target_parameters) — the LoRA targets a + # parent module and this weight is a sub-parameter of that module. + if tensor.ndim >= 3: + pw_a, pw_b, param_name = _find_param_wrapper_lora( + lora_state, key, tensor_shape=tuple(tensor.shape) + ) + if pw_a is not None and pw_b is not None: + LOG.debug( + f"Merging ParamWrapper LoRA for {key} " + f"(param={param_name}): {pw_a.shape}, {pw_b.shape}" + ) + if do_nf4: + tensor = _simulate_nf4_roundtrip( + tensor, + blocksize=nf4_blocksize, + compress_statistics=nf4_double_quant, + device=device, + ) + original_dtype = tensor.dtype + delta = _build_peft_layer_and_get_delta( + pw_a.to(device), + pw_b.to(device), + lora_config_dict, + tensor.to(device), + is_param_wrapper=True, + ) + merged = ( + (tensor.to(device).to(torch.float32) + delta.to(torch.float32)) + .to(original_dtype) + .detach() + .cpu() + ) + return merged, True + + if do_nf4: + tensor = _simulate_nf4_roundtrip( + tensor, + blocksize=nf4_blocksize, + compress_statistics=nf4_double_quant, + device=device, + ) + return tensor.detach().cpu(), False + + +def _get_conversion_info(base_model_path: Path) -> tuple[Dict[str, str], list]: + """ + Load the model's config.json and check if transformers has WeightRenaming + or WeightConverter mappings for this model type. + + Returns: + - dict of {source_pattern: target_pattern} for simple renamings + - list of WeightConverter objects for fuse/unfuse operations + """ + import json as _json + + config_path = base_model_path / "config.json" + if not config_path.exists(): + return {}, [] + + try: + with open(config_path) as f: + model_config = _json.load(f) + except (OSError, _json.JSONDecodeError): + return {}, [] + + model_type = model_config.get("model_type") + if not model_type: + return {}, [] + + try: + from transformers.conversion_mapping import get_checkpoint_conversion_mapping + from transformers.core_model_loading import WeightConverter, WeightRenaming + except ImportError: + return {}, [] + + conversions = get_checkpoint_conversion_mapping(model_type) + if not conversions: + return {}, [] + + renamings = {} + weight_converters = [] + for conv in conversions: + if isinstance(conv, WeightRenaming): + # WeightRenaming stores patterns as lists internally + src_list = ( + conv.source_patterns + if isinstance(conv.source_patterns, list) + else [conv.source_patterns] + ) + tgt_list = ( + conv.target_patterns + if isinstance(conv.target_patterns, list) + else [conv.target_patterns] + ) + if len(src_list) == 1 and len(tgt_list) == 1: + renamings[src_list[0]] = tgt_list[0] + elif isinstance(conv, WeightConverter): + weight_converters.append(conv) + + return renamings, weight_converters + + +def _fuse_and_unfuse_with_merge( + shard_tensors: Dict[str, torch.Tensor], + weight_converters: list, + lora_state: Dict[str, torch.Tensor], + scale: float, + lora_config_dict: Dict, + device: str, + simulate_nf4: bool = False, + simulate_nf4_experts: bool = False, + nf4_blocksize: Optional[int] = None, + nf4_double_quant: bool = True, + use_dora: bool = False, + weight_renamings: Optional[Dict[str, str]] = None, +) -> tuple[Dict[str, torch.Tensor], int, set]: + """ + For tensors matching WeightConverter patterns (MoE expert weights): + 1. Fuse checkpoint-format tensors into runtime-format (e.g., per-expert → fused 3D) + 2. Apply NF4 roundtrip + LoRA merge on the fused tensor + 3. Unfuse back to checkpoint format for saving + + Returns: + - Updated tensor dict + - Count of merged LoRA targets + - Set of keys that were processed (fused/merged/unfused) and should be + skipped by the per-tensor merge pass to avoid double NF4 roundtrip + """ + import re + + from transformers.core_model_loading import Concatenate, MergeModulelist + + result = dict(shard_tensors) # Start with all tensors + merged_count = 0 + processed_keys: set = set() # Keys that were fuse/unfuse processed + + for converter in weight_converters: + src_patterns = ( + converter.source_patterns + if isinstance(converter.source_patterns, list) + else [converter.source_patterns] + ) + tgt_patterns = ( + converter.target_patterns + if isinstance(converter.target_patterns, list) + else [converter.target_patterns] + ) + + # Build regex for each source pattern + pattern_regexes = [] + for pat in src_patterns: + regex_str = re.escape(pat).replace(r"\.\*\.", r"\.(\d+)\.") + regex_str = ( + regex_str.rstrip(r"\$") if regex_str.endswith(r"\$") else regex_str + ) + pattern_regexes.append(re.compile(r"(.*\.)?" + regex_str + "$")) + + # Group matching keys by layer prefix and source pattern + # {layer_prefix: {pat_idx: {expert_idx: (key, tensor)}}} + layer_groups: Dict[str, Dict[int, Dict[int, tuple[str, torch.Tensor]]]] = {} + + for key in list(result.keys()): + for pat_idx, pat_regex in enumerate(pattern_regexes): + match = pat_regex.match(key) + if match: + prefix = match.group(1) or "" + # Extract expert index from the matched portion + remaining = key[len(prefix) :] + expert_match = re.search(r"\.(\d+)\.", remaining) + expert_idx = int(expert_match.group(1)) if expert_match else 0 + + layer_groups.setdefault(prefix, {}).setdefault(pat_idx, {})[ + expert_idx + ] = (key, result[key]) + break + + # Process each layer group + for prefix, pat_groups in layer_groups.items(): + # Check we have all source patterns for this layer + if not pat_groups: + continue + + # Step 1: Fuse — MergeModulelist (stack experts) per source pattern + fused_per_pattern = {} + original_keys_per_pattern: Dict[int, list[str]] = {} + num_experts = None + + for pat_idx in sorted(pat_groups.keys()): + expert_data = pat_groups[pat_idx] + sorted_indices = sorted(expert_data.keys()) + if num_experts is None: + num_experts = len(sorted_indices) + + sorted_tensors = [expert_data[idx][1] for idx in sorted_indices] + original_keys_per_pattern[pat_idx] = [ + expert_data[idx][0] for idx in sorted_indices + ] + fused_per_pattern[src_patterns[pat_idx]] = torch.stack( + sorted_tensors, dim=0 + ) + + # Apply remaining operations (Concatenate) + fused_tensor = None + has_concat = False + concat_dim = 1 # default + + for op in converter.operations: + if isinstance(op, MergeModulelist): + pass # Already handled + elif isinstance(op, Concatenate): + has_concat = True + concat_dim = op.dim + tensors_to_cat = [ + fused_per_pattern[sp] + for sp in src_patterns + if sp in fused_per_pattern + ] + if len(tensors_to_cat) > 1: + fused_tensor = torch.cat(tensors_to_cat, dim=concat_dim) + elif tensors_to_cat: + fused_tensor = tensors_to_cat[0] + + if not has_concat and len(fused_per_pattern) == 1: + fused_tensor = next(iter(fused_per_pattern.values())) + + if fused_tensor is None: + continue + + # Step 2: Build the fused key name and merge LoRA + fused_key = prefix + tgt_patterns[0] + + # Apply NF4 roundtrip on the fused tensor (matching training dynamics) + do_nf4 = _should_nf4_roundtrip( + fused_key, fused_tensor, simulate_nf4, simulate_nf4_experts + ) + if do_nf4: + fused_tensor = _simulate_nf4_roundtrip( + fused_tensor, + blocksize=nf4_blocksize, + compress_statistics=nf4_double_quant, + device=device, + ) + + # Try to find and merge LoRA weights for the fused key + lora_a, lora_b = find_lora_weights(lora_state, fused_key, weight_renamings) + if lora_a is not None and lora_b is not None: + LOG.debug( + f"Merging LoRA for fused key {fused_key}: {lora_a.shape}, {lora_b.shape}" + ) + original_dtype = fused_tensor.dtype + magnitude = ( + _find_dora_magnitude(lora_state, fused_key, weight_renamings) + if use_dora + else None + ) + delta = _build_peft_layer_and_get_delta( + lora_a.to(device), + lora_b.to(device), + lora_config_dict, + fused_tensor.to(device), + magnitude=magnitude.to(device) if magnitude is not None else None, + ) + fused_tensor = ( + ( + fused_tensor.to(device).to(torch.float32) + + delta.to(torch.float32) + ) + .to(original_dtype) + .detach() + .cpu() + ) + merged_count += 1 + + # Step 3: Save in fused format (runtime format) so that the merged + # model can be loaded directly without needing WeightConverter + # fusion during from_pretrained (which can OOM for large MoE models). + # Remove the original per-expert keys and save the fused tensor + # under the runtime key name. + for pat_idx in sorted(original_keys_per_pattern.keys()): + for ok in original_keys_per_pattern[pat_idx]: + result.pop(ok, None) + processed_keys.add(ok) + + result[fused_key] = fused_tensor.detach().cpu() + processed_keys.add(fused_key) + + return result, merged_count, processed_keys + + +def merge_lora_sharded_efficient( + base_model_path: Union[str, Path], + lora_adapter_path: Union[str, Path], + output_path: Union[str, Path], + device: str = "cpu", + safe_tensors: bool = True, + simulate_nf4: bool = False, + simulate_nf4_experts: bool = False, + nf4_blocksize: Optional[int] = None, + nf4_double_quant: bool = True, +) -> None: + """ + Memory-efficient LoRA merging that processes shards individually + without loading the full model into memory. + + Args: + simulate_nf4: Apply NF4 roundtrip to ALL weight tensors (for QLoRA) + simulate_nf4_experts: Apply NF4 roundtrip only to MoE expert tensors + (for quantize_moe_experts). Expert tensors are identified by having + "expert" in the key name and ndim >= 3. + """ + base_model_path = Path(base_model_path) + lora_adapter_path = Path(lora_adapter_path) + output_path = Path(output_path) + + if "/" in str(base_model_path) and not base_model_path.exists(): + base_model_path = Path(snapshot_download(str(base_model_path))) + + # Check for weight conversion requirements (transformers v5) + weight_renamings, weight_converters = _get_conversion_info(base_model_path) + if weight_renamings: + LOG.debug(f"Found {len(weight_renamings)} weight renamings for this model type") + if weight_converters: + LOG.debug( + f"Found {len(weight_converters)} weight converters (fuse/unfuse) for this model type. " + f"Will fuse→merge→unfuse within each shard." + ) + + os.makedirs(output_path, exist_ok=True) + + config_file = lora_adapter_path / "adapter_config.json" + if not config_file.exists(): + raise FileNotFoundError(f"LoRA config not found: {config_file}") + + lora_config_dict = LoraConfig.from_json_file(str(config_file)) + if not lora_config_dict.get("r") or lora_config_dict["r"] <= 0: + raise ValueError("LoRA config 'r' must be > 0") + + use_dora = bool(lora_config_dict.get("use_dora", False)) + + unsupported_methods = [] + + # Check for AdaLoRA (Adaptive LoRA) + if lora_config_dict.get("use_adalora", False): + unsupported_methods.append("AdaLoRA (Adaptive LoRA)") + + # Check for VeRA (Vector-based Random Matrix Adaptation) + if lora_config_dict.get("use_vera", False): + unsupported_methods.append("VeRA (Vector-based Random Matrix Adaptation)") + + # Check for other advanced LoRA variants by task_type + task_type = lora_config_dict.get("task_type", "") + if task_type and task_type not in [ + "CAUSAL_LM", + "SEQ_2_SEQ_LM", + "TOKEN_CLS", + "SEQ_CLS", + "QUESTION_ANS", + ]: + unsupported_methods.append(f"Task type: {task_type}") + + # Check for rank adaptation patterns (AdaLoRA indicators) + # Use .get() so empty dicts/None don't false-positive + if any( + lora_config_dict.get(key) + for key in ["rank_pattern", "alpha_pattern", "target_rank"] + ): + unsupported_methods.append("AdaLoRA (rank adaptation detected)") + + # Check for advanced initialization methods + init_lora_weights = lora_config_dict.get("init_lora_weights", "") + if init_lora_weights and init_lora_weights not in [ + "gaussian", + "loftq", + True, + False, + ]: + unsupported_methods.append(f"Advanced initialization: {init_lora_weights}") + + if unsupported_methods: + methods_str = ", ".join(unsupported_methods) + raise NotImplementedError( + f"Memory-efficient LoRA merge only supports standard LoRA. " + f"Detected unsupported methods: {methods_str}. " + f"Please use the legacy merge method for advanced LoRA variants." + ) + + use_rslora = bool(lora_config_dict.get("use_rslora", False)) + if use_rslora: + scale = float(lora_config_dict["lora_alpha"]) / math.sqrt( + float(lora_config_dict["r"]) + ) + else: + scale = float(lora_config_dict["lora_alpha"]) / float(lora_config_dict["r"]) + + LOG.debug(f"LoRA scale factor: {scale} (rslora={use_rslora})") + + if simulate_nf4: + LOG.info( + "NF4 simulation enabled: base weights will undergo quantize→dequantize " + "roundtrip before LoRA merge to match QLoRA training dynamics" + ) + + lora_file = lora_adapter_path / "adapter_model.safetensors" + if not lora_file.exists(): + lora_file = lora_adapter_path / "adapter_model.bin" + if not lora_file.exists(): + raise FileNotFoundError( + f"LoRA adapter weights not found in {lora_adapter_path}" + ) + + LOG.debug(f"Loading LoRA weights from {lora_file}") + + if lora_file.suffix == ".safetensors": + lora_state = safetensors.torch.load_file(lora_file) + else: + lora_state = torch.load(lora_file, map_location="cpu", weights_only=True) # nosec B614 + LOG.debug("Keeping LoRA weights on CPU; will move per-tensor during merge") + + model_shards = get_model_shards(base_model_path) + if not model_shards: + raise FileNotFoundError(f"No model shards found in {base_model_path}") + + LOG.debug(f"Found {len(model_shards)} model shards in {base_model_path}") + copy_non_model_files(base_model_path, output_path, model_shards) + + merged_count = 0 + total_tensors = 0 + # Track weight_map for index regeneration: {tensor_key: shard_filename} + weight_map: Dict[str, str] = {} + + for shard_path in tqdm(model_shards, desc="Merging shards"): + merged_tensors = {} + metadata = {} + + # Load all tensors from the shard + if shard_path.suffix == ".safetensors": + with safetensors.safe_open(shard_path, framework="pt", device="cpu") as f: + if hasattr(f, "metadata") and f.metadata(): + metadata = f.metadata() + shard_tensors = {key: f.get_tensor(key) for key in f.keys()} + else: + shard_tensors = torch.load( # nosec B614: loading trusted model weights + shard_path, map_location="cpu", weights_only=True + ) + + total_tensors += len(shard_tensors) + + # Step 1: Handle fused weight conversions (MoE experts) if applicable + fused_keys: set = set() + if weight_converters: + shard_tensors, fused_merged, fused_keys = _fuse_and_unfuse_with_merge( + shard_tensors, + weight_converters, + lora_state, + scale, + lora_config_dict, + device, + simulate_nf4=simulate_nf4, + simulate_nf4_experts=simulate_nf4_experts, + nf4_blocksize=nf4_blocksize, + nf4_double_quant=nf4_double_quant, + use_dora=use_dora, + weight_renamings=weight_renamings, + ) + merged_count += fused_merged + + # Step 2: Merge remaining (non-fused) tensors with LoRA + # Skip keys already processed by fuse/unfuse to avoid double NF4 roundtrip + for key, tensor in shard_tensors.items(): + if key in fused_keys: + merged_tensors[key] = tensor.detach().cpu() + continue + merged_tensor, was_merged = _merge_tensor_with_lora( + tensor, + key, + lora_state, + scale, + lora_config_dict, + device, + simulate_nf4=simulate_nf4, + simulate_nf4_experts=simulate_nf4_experts, + nf4_blocksize=nf4_blocksize, + nf4_double_quant=nf4_double_quant, + use_dora=use_dora, + weight_renamings=weight_renamings, + ) + merged_tensors[key] = merged_tensor + if was_merged: + merged_count += 1 + + output_shard_path = output_path / shard_path.name + merged_tensors = {k: v.detach().cpu() for k, v in merged_tensors.items()} + + if safe_tensors: + if not str(output_shard_path).endswith(".safetensors"): + output_shard_path = output_path / (shard_path.stem + ".safetensors") + safetensors.torch.save_file( + merged_tensors, output_shard_path, metadata=metadata + ) + else: + if shard_path.suffix == ".safetensors": + safetensors.torch.save_file( + merged_tensors, output_shard_path, metadata=metadata + ) + else: + torch.save(merged_tensors, output_shard_path) + + for tensor_key in merged_tensors: + weight_map[tensor_key] = output_shard_path.name + + del merged_tensors, shard_tensors + if device != "cpu" and torch.cuda.is_available(): + torch.cuda.empty_cache() + gc.collect() + + # Regenerate weight-map index if the model was sharded + if len(model_shards) > 1 and weight_map: + import json as _json + + index_name = ( + "model.safetensors.index.json" + if safe_tensors + else "pytorch_model.bin.index.json" + ) + index = { + "metadata": {"total_size": total_tensors}, + "weight_map": weight_map, + } + with open(output_path / index_name, "w") as f: + _json.dump(index, f, indent=2) + LOG.debug(f"Wrote weight-map index: {index_name}") + + if merged_count == 0: + LOG.warning( + "No LoRA weights were matched to base model tensors. " + "This may indicate a key name mismatch between the checkpoint format " + "and the LoRA adapter. Consider using merge_method: legacy." + ) + LOG.info(f"Applied LoRA to {merged_count}/{total_tensors} tensors") diff --git a/src/axolotl/monkeypatch/moe_quant.py b/src/axolotl/monkeypatch/moe_quant.py index 983da4a37e..68c458f5aa 100644 --- a/src/axolotl/monkeypatch/moe_quant.py +++ b/src/axolotl/monkeypatch/moe_quant.py @@ -164,6 +164,16 @@ def patch_peft_target_parameters_matching(): from peft.utils.integrations import init_empty_weights from peft.utils.other import _get_submodules + # Mapping from unfused parameter names to their fused equivalents. + # When a model stores fused weights (e.g. gate_up_proj) but the user + # specifies unfused names (gate_proj, up_proj), we auto-expand so the + # fused parameter is also targeted. The original unfused names are kept + # in the set so that models that do NOT fuse still work. + _UNFUSED_TO_FUSED: dict[str, str] = { + "gate_proj": "gate_up_proj", + "up_proj": "gate_up_proj", + } + def _patched_inject_parameters( self, peft_config, model, adapter_name, low_cpu_mem_usage ): @@ -176,10 +186,43 @@ def _patched_inject_parameters( continue for target in original_targets: mod_path, _, param_name = target.rpartition(".") - if ( + if not ( module_name == mod_path or module_name.endswith("." + mod_path) - ) and hasattr(module, param_name): + ): + continue + + if hasattr(module, param_name): expanded.add(f"{module_name}.{param_name}") + elif param_name in _UNFUSED_TO_FUSED: + # The model uses fused weights (e.g. gate_up_proj) but the + # user specified unfused names (gate_proj / up_proj). + fused_name = _UNFUSED_TO_FUSED[param_name] + if hasattr(module, fused_name): + if fused_name not in expanded: + LOG.warning( + "target_parameter '%s' not found on %s, " + "but fused equivalent '%s' exists — adding " + "it automatically.", + param_name, + module_name, + fused_name, + ) + expanded.add(f"{module_name}.{fused_name}") + else: + LOG.warning( + "target_parameter '%s' not found on %s and no " + "fused equivalent exists either — skipping.", + param_name, + module_name, + ) + else: + LOG.warning( + "target_parameter '%s' not found on %s — skipping. " + "Check that the parameter name matches the model's " + "weight names.", + param_name, + module_name, + ) target_names_set = expanded diff --git a/src/axolotl/utils/schemas/peft.py b/src/axolotl/utils/schemas/peft.py index 5b90fb63f3..c60c548f03 100644 --- a/src/axolotl/utils/schemas/peft.py +++ b/src/axolotl/utils/schemas/peft.py @@ -155,6 +155,12 @@ class LoraConfig(BaseModel): ) merge_lora: bool | None = None + merge_method: Literal["legacy", "memory_efficient"] | None = Field( + default="memory_efficient", + json_schema_extra={ + "description": "Method to use for LoRA merging. 'memory_efficient' (default) processes shards individually to reduce memory usage, 'legacy' loads the full model into memory." + }, + ) @model_validator(mode="before") @classmethod diff --git a/tests/utils/lora/test_merge_lora.py b/tests/utils/lora/test_merge_lora.py index 8edccafb9e..e5d7f535d0 100644 --- a/tests/utils/lora/test_merge_lora.py +++ b/tests/utils/lora/test_merge_lora.py @@ -1,8 +1,18 @@ +import json +import math from unittest.mock import Mock, patch +import safetensors.torch import torch from axolotl.cli.merge_lora import do_merge_lora +from axolotl.cli.utils.lora_merge import ( + _build_peft_layer_and_get_delta, + _find_param_wrapper_lora, + _merge_tensor_with_lora, + find_lora_weights, + merge_lora_sharded_efficient, +) from axolotl.utils.dict import DictDefault @@ -132,6 +142,7 @@ def test_cli_do_merge_functionality(self, mock_load_model, tmp_path): "torch_dtype": torch.float32, "local_rank": 0, "output_dir": str(tmp_path), + "merge_method": "legacy", } ) @@ -167,6 +178,7 @@ def test_memory_efficient_merge_with_cpu_offload(self, tmp_path): "save_safetensors": True, "output_dir": str(tmp_path), "local_rank": 0, + "merge_method": "legacy", } ) @@ -179,3 +191,611 @@ def test_memory_efficient_merge_with_cpu_offload(self, tmp_path): do_merge_lora(cfg=cfg) assert mock_load.called + + +class TestEfficientMerge: + """Test suite for memory-efficient shard-by-shard LoRA merge.""" + + def _make_adapter(self, tmp_path, r=8, alpha=16, use_dora=False, use_rslora=False): + """Create a minimal adapter directory with config + weights.""" + adapter_dir = tmp_path / "adapter" + adapter_dir.mkdir() + + config = { + "r": r, + "lora_alpha": alpha, + "target_modules": ["q_proj", "v_proj"], + "task_type": "CAUSAL_LM", + "bias": "none", + "use_dora": use_dora, + "use_rslora": use_rslora, + } + (adapter_dir / "adapter_config.json").write_text(json.dumps(config)) + return adapter_dir, config + + def _make_base_model(self, tmp_path, hidden=32): + """Create a minimal base model directory with one shard.""" + model_dir = tmp_path / "base_model" + model_dir.mkdir() + + weights = { + "model.layers.0.self_attn.q_proj.weight": torch.randn(hidden, hidden), + "model.layers.0.self_attn.v_proj.weight": torch.randn(hidden, hidden), + "model.embed_tokens.weight": torch.randn(100, hidden), + } + safetensors.torch.save_file(weights, model_dir / "model.safetensors") + + # Minimal config files + (model_dir / "config.json").write_text("{}") + return model_dir, weights + + def test_find_lora_weights(self): + lora_state = { + "base_model.model.layers.0.self_attn.q_proj.lora_A.weight": torch.randn( + 8, 32 + ), + "base_model.model.layers.0.self_attn.q_proj.lora_B.weight": torch.randn( + 32, 8 + ), + } + a, b = find_lora_weights(lora_state, "layers.0.self_attn.q_proj.weight") + assert a is not None and b is not None + assert a.shape == (8, 32) + + a, b = find_lora_weights(lora_state, "layers.0.self_attn.v_proj.weight") + assert a is None and b is None + + def test_merge_tensor_basic(self): + hidden = 32 + r = 8 + alpha = 16 + base = torch.randn(hidden, hidden) + lora_a = torch.randn(r, hidden) + lora_b = torch.randn(hidden, r) + scale = alpha / r + + lora_state = { + "base_model.model.layer.q_proj.lora_A.weight": lora_a, + "base_model.model.layer.q_proj.lora_B.weight": lora_b, + } + + config = {"r": r, "lora_alpha": alpha} + merged, was_merged = _merge_tensor_with_lora( + base, "layer.q_proj.weight", lora_state, scale, config, "cpu" + ) + assert was_merged + expected = base + scale * (lora_b @ lora_a) + assert torch.allclose(merged, expected, atol=1e-5) + + def test_merge_tensor_rslora_scale(self): + """RSLoRA should use alpha/sqrt(r) as scaling factor.""" + r = 16 + alpha = 32 + standard_scale = alpha / r # 2.0 + rslora_scale = alpha / math.sqrt(r) # 8.0 + + assert rslora_scale != standard_scale + assert abs(rslora_scale - 8.0) < 1e-6 + + def test_sharded_efficient_merge(self, tmp_path): + """End-to-end test of shard-by-shard merge.""" + hidden = 32 + r = 8 + alpha = 16 + + model_dir, base_weights = self._make_base_model(tmp_path, hidden=hidden) + adapter_dir, _ = self._make_adapter(tmp_path, r=r, alpha=alpha) + + # Create LoRA weights + lora_state = { + "base_model.model.model.layers.0.self_attn.q_proj.lora_A.weight": torch.randn( + r, hidden + ), + "base_model.model.model.layers.0.self_attn.q_proj.lora_B.weight": torch.randn( + hidden, r + ), + "base_model.model.model.layers.0.self_attn.v_proj.lora_A.weight": torch.randn( + r, hidden + ), + "base_model.model.model.layers.0.self_attn.v_proj.lora_B.weight": torch.randn( + hidden, r + ), + } + safetensors.torch.save_file( + lora_state, adapter_dir / "adapter_model.safetensors" + ) + + output_dir = tmp_path / "output" + merge_lora_sharded_efficient( + base_model_path=model_dir, + lora_adapter_path=adapter_dir, + output_path=output_dir, + device="cpu", + ) + + # Verify output exists and has merged weights + merged = safetensors.torch.load_file(output_dir / "model.safetensors") + scale = alpha / r + + q_key = "model.layers.0.self_attn.q_proj.weight" + expected_q = base_weights[q_key] + scale * ( + lora_state[f"base_model.model.{q_key[:-7]}.lora_B.weight"] + @ lora_state[f"base_model.model.{q_key[:-7]}.lora_A.weight"] + ) + assert torch.allclose(merged[q_key], expected_q, atol=1e-5) + + # Embedding should be unchanged + assert torch.equal( + merged["model.embed_tokens.weight"], + base_weights["model.embed_tokens.weight"], + ) + + def test_dora_merge(self): + """DoRA merge applies magnitude normalization via PEFT.""" + hidden = 32 + r = 8 + alpha = 16 + scale = alpha / r + + base = torch.randn(hidden, hidden) + lora_a = torch.randn(r, hidden) + lora_b = torch.randn(hidden, r) + magnitude = torch.randn(hidden).abs() + 0.1 + + lora_state = { + "base_model.model.layer.q_proj.lora_A.weight": lora_a, + "base_model.model.layer.q_proj.lora_B.weight": lora_b, + "base_model.model.layer.q_proj.lora_magnitude_vector": magnitude, + } + + config = {"r": r, "lora_alpha": alpha, "use_dora": True} + merged, was_merged = _merge_tensor_with_lora( + base, + "layer.q_proj.weight", + lora_state, + scale, + config, + "cpu", + use_dora=True, + ) + assert was_merged + + # The merge should differ from both base and base+delta (DoRA applies normalization) + delta = scale * (lora_b @ lora_a) + assert not torch.allclose(merged, base, atol=1e-3) + assert not torch.allclose(merged, base + delta, atol=1e-3) + + def test_fuse_unfuse_moe_merge(self): + """Test fuse→merge→unfuse for MoE expert weights (WeightConverter path).""" + from axolotl.cli.utils.lora_merge import _fuse_and_unfuse_with_merge + + hidden = 16 + intermediate = 32 + num_experts = 4 + r = 4 + alpha = 8 + scale = alpha / r + + # Simulate checkpoint format: per-expert separate tensors + shard_tensors = {} + for i in range(num_experts): + shard_tensors[f"model.layers.0.mlp.experts.{i}.gate_proj.weight"] = ( + torch.randn(intermediate, hidden) + ) + shard_tensors[f"model.layers.0.mlp.experts.{i}.up_proj.weight"] = ( + torch.randn(intermediate, hidden) + ) + shard_tensors[f"model.layers.0.mlp.experts.{i}.down_proj.weight"] = ( + torch.randn(hidden, intermediate) + ) + shard_tensors["model.layers.0.self_attn.q_proj.weight"] = torch.randn( + hidden, hidden + ) + + # LoRA targets the fused key (runtime format) + lora_state = { + "base_model.model.model.layers.0.mlp.experts.gate_up_proj.lora_A.weight": torch.randn( + r, hidden + ), + "base_model.model.model.layers.0.mlp.experts.gate_up_proj.lora_B.weight": torch.randn( + intermediate * 2, r + ), + "base_model.model.model.layers.0.mlp.experts.down_proj.lora_A.weight": torch.randn( + r, intermediate + ), + "base_model.model.model.layers.0.mlp.experts.down_proj.lora_B.weight": torch.randn( + hidden, r + ), + } + + # Build converters matching qwen2_moe pattern + from transformers.core_model_loading import ( + Concatenate, + MergeModulelist, + WeightConverter, + ) + + converters = [ + WeightConverter( + source_patterns=[ + "mlp.experts.*.gate_proj.weight", + "mlp.experts.*.up_proj.weight", + ], + target_patterns="mlp.experts.gate_up_proj", + operations=[MergeModulelist(dim=0), Concatenate(dim=1)], + ), + WeightConverter( + source_patterns="mlp.experts.*.down_proj.weight", + target_patterns="mlp.experts.down_proj", + operations=[MergeModulelist(dim=0)], + ), + ] + + config = {"r": r, "lora_alpha": alpha} + result, merged_count, processed_keys = _fuse_and_unfuse_with_merge( + shard_tensors, converters, lora_state, scale, config, "cpu" + ) + + # Should have merged 2 LoRA targets (gate_up_proj and down_proj) + assert merged_count == 2 + + # Processed keys include original per-expert keys (removed) + fused keys (added) + assert len(processed_keys) > 0 + + # Output should be in fused format (runtime keys) + assert "model.layers.0.mlp.experts.gate_up_proj" in result + assert "model.layers.0.mlp.experts.down_proj" in result + + # Per-expert keys should be removed + for i in range(num_experts): + assert f"model.layers.0.mlp.experts.{i}.gate_proj.weight" not in result + + # Non-expert tensor should be passed through + assert "model.layers.0.self_attn.q_proj.weight" in result + + # Verify fused tensors are 3D (stacked experts) + gate_up = result["model.layers.0.mlp.experts.gate_up_proj"] + assert gate_up.ndim == 3 + assert gate_up.shape[0] == num_experts # [num_experts, intermediate*2, hidden] + + # Verify the fused LoRA delta was applied correctly + # Reconstruct the fused base (stack per-expert, concat gate+up) + gate_stack = torch.stack( + [ + shard_tensors[f"model.layers.0.mlp.experts.{i}.gate_proj.weight"] + for i in range(num_experts) + ] + ) + up_stack = torch.stack( + [ + shard_tensors[f"model.layers.0.mlp.experts.{i}.up_proj.weight"] + for i in range(num_experts) + ] + ) + base_fused = torch.cat([gate_stack, up_stack], dim=1) + lora_a = lora_state[ + "base_model.model.model.layers.0.mlp.experts.gate_up_proj.lora_A.weight" + ] + lora_b = lora_state[ + "base_model.model.model.layers.0.mlp.experts.gate_up_proj.lora_B.weight" + ] + expected_fused = base_fused + scale * (lora_b @ lora_a) + assert torch.allclose(gate_up, expected_fused, atol=1e-5) + + def test_param_wrapper_merge_math(self): + """ParamWrapper merge via PEFT's get_delta_weight matches manual einsum.""" + num_experts = 4 + r = 2 + in_features = 8 + out_features = 4 + alpha = 4 + + base = torch.randn(num_experts, in_features, out_features) + lora_a = torch.randn(r * num_experts, in_features) + lora_b = torch.randn(out_features, r * num_experts) + + config = {"r": r, "lora_alpha": alpha} + delta = _build_peft_layer_and_get_delta( + lora_a, lora_b, config, base, is_param_wrapper=True + ) + assert delta.shape == base.shape + + merged = base + delta + + # Verify against manual einsum + scale = alpha / r + wa = lora_a.reshape(num_experts, r, in_features) + wb = lora_b.reshape(out_features, r, num_experts) + manual_delta = torch.einsum("o r e, e r i -> e i o", wb, wa) * scale + for e in range(num_experts): + assert torch.allclose(merged[e], base[e] + manual_delta[e], atol=1e-5), ( + f"Expert {e} mismatch" + ) + + def test_param_wrapper_nesting_dim_filter(self): + """_find_param_wrapper_lora skips wrong-dimension LoRA at outer level.""" + num_experts = 4 + r = 2 + + # Outer LoRA (gate_up_proj): A=[r*E, 8], B=[16, r*E] + # Inner LoRA (down_proj via base_layer): A=[r*E, 16], B=[8, r*E] + lora_state = { + "base_model.model.mod.experts.lora_A.weight": torch.randn( + r * num_experts, 8 + ), + "base_model.model.mod.experts.lora_B.weight": torch.randn( + 16, r * num_experts + ), + "base_model.model.mod.experts.base_layer.lora_A.weight": torch.randn( + r * num_experts, 16 + ), + "base_model.model.mod.experts.base_layer.lora_B.weight": torch.randn( + 8, r * num_experts + ), + } + + # gate_up_proj shape [4, 8, 16] — should match outer LoRA + a, b, name = _find_param_wrapper_lora( + lora_state, "mod.experts.gate_up_proj", tensor_shape=(4, 8, 16) + ) + assert a is not None and name == "gate_up_proj" + assert a.shape == (r * num_experts, 8) # outer + + # down_proj shape [4, 16, 8] — outer dims don't match, should find inner + a, b, name = _find_param_wrapper_lora( + lora_state, "mod.experts.down_proj", tensor_shape=(4, 16, 8) + ) + assert a is not None and name == "down_proj" + assert a.shape == (r * num_experts, 16) # inner (base_layer) + + # shape that matches neither — should return None + a, b, name = _find_param_wrapper_lora( + lora_state, "mod.experts.other", tensor_shape=(4, 99, 99) + ) + assert a is None + + def test_find_lora_weights_with_renamings(self): + """Weight renamings let checkpoint keys match LoRA keys.""" + lora_state = { + "base_model.model.layers.0.mlp.fc1.lora_A.weight": torch.randn(8, 32), + "base_model.model.layers.0.mlp.fc1.lora_B.weight": torch.randn(32, 8), + } + # Direct lookup fails (checkpoint has "ff0", LoRA has "fc1") + a, b = find_lora_weights(lora_state, "layers.0.mlp.ff0.weight") + assert a is None + + # With renaming ff0 → fc1, it should match + a, b = find_lora_weights( + lora_state, "layers.0.mlp.ff0.weight", weight_renamings={"ff0": "fc1"} + ) + assert a is not None + assert a.shape == (8, 32) + + def test_unmatched_tensors_pass_through(self): + """Tensors with no matching LoRA are returned unchanged.""" + lora_state = { + "base_model.model.layer.q_proj.lora_A.weight": torch.randn(8, 32), + "base_model.model.layer.q_proj.lora_B.weight": torch.randn(32, 8), + } + + # 1D tensor (layernorm) — never matched + ln = torch.randn(32) + merged, was_merged = _merge_tensor_with_lora( + ln, "layer.norm.weight", lora_state, 2.0, {}, "cpu" + ) + assert not was_merged + assert torch.equal(merged, ln) + + # 2D tensor with no matching key + unrelated = torch.randn(64, 32) + merged, was_merged = _merge_tensor_with_lora( + unrelated, "layer.other_proj.weight", lora_state, 2.0, {}, "cpu" + ) + assert not was_merged + assert torch.equal(merged, unrelated) + + def test_fan_in_fan_out_transpose(self): + """fan_in_fan_out config transposes the LoRA delta.""" + hidden = 16 + r = 4 + alpha = 4 # scale = 1.0 + + base = torch.randn(hidden, hidden) + lora_a = torch.randn(r, hidden) + lora_b = torch.randn(hidden, r) + + lora_state = { + "base_model.model.layer.proj.lora_A.weight": lora_a, + "base_model.model.layer.proj.lora_B.weight": lora_b, + } + + config_normal = {"r": r, "lora_alpha": alpha} + config_fif = {"r": r, "lora_alpha": alpha, "fan_in_fan_out": True} + + merged_normal, _ = _merge_tensor_with_lora( + base, "layer.proj.weight", lora_state, 1.0, config_normal, "cpu" + ) + merged_fif, _ = _merge_tensor_with_lora( + base, "layer.proj.weight", lora_state, 1.0, config_fif, "cpu" + ) + + delta = (alpha / r) * (lora_b @ lora_a) + assert torch.allclose(merged_normal, base + delta, atol=1e-5) + assert torch.allclose(merged_fif, base + delta.T, atol=1e-5) + assert not torch.allclose(merged_normal, merged_fif, atol=1e-5) + + def test_rslora_end_to_end(self, tmp_path): + """RSLoRA adapter uses alpha/sqrt(r) scaling in sharded merge.""" + hidden = 16 + r = 16 + alpha = 32 + + model_dir, base_weights = self._make_base_model(tmp_path, hidden=hidden) + adapter_dir, _ = self._make_adapter(tmp_path, r=r, alpha=alpha, use_rslora=True) + + lora_a = torch.randn(r, hidden) + lora_b = torch.randn(hidden, r) + lora_state = { + "base_model.model.model.layers.0.self_attn.q_proj.lora_A.weight": lora_a, + "base_model.model.model.layers.0.self_attn.q_proj.lora_B.weight": lora_b, + } + safetensors.torch.save_file( + lora_state, adapter_dir / "adapter_model.safetensors" + ) + + output_dir = tmp_path / "output" + merge_lora_sharded_efficient( + base_model_path=model_dir, + lora_adapter_path=adapter_dir, + output_path=output_dir, + device="cpu", + ) + + merged = safetensors.torch.load_file(output_dir / "model.safetensors") + rslora_scale = alpha / math.sqrt(r) # 8.0, not 2.0 + q_key = "model.layers.0.self_attn.q_proj.weight" + expected = base_weights[q_key] + rslora_scale * (lora_b @ lora_a) + assert torch.allclose(merged[q_key], expected, atol=1e-5) + + # Confirm it differs from standard scale + wrong_scale = alpha / r # 2.0 + wrong_expected = base_weights[q_key] + wrong_scale * (lora_b @ lora_a) + assert not torch.allclose(merged[q_key], wrong_expected, atol=1e-3) + + def test_multi_shard_index_json(self, tmp_path): + """Multi-shard merge generates a correct weight-map index.""" + hidden = 16 + r = 4 + alpha = 8 + + model_dir = tmp_path / "base_model" + model_dir.mkdir() + (model_dir / "config.json").write_text("{}") + + # Create 2 shards + shard1 = {"model.layers.0.weight": torch.randn(hidden, hidden)} + shard2 = {"model.layers.1.weight": torch.randn(hidden, hidden)} + safetensors.torch.save_file( + shard1, model_dir / "model-00001-of-00002.safetensors" + ) + safetensors.torch.save_file( + shard2, model_dir / "model-00002-of-00002.safetensors" + ) + + # Write a base model index (will be skipped by copy_non_model_files) + base_index = { + "metadata": {}, + "weight_map": { + "model.layers.0.weight": "model-00001-of-00002.safetensors", + "model.layers.1.weight": "model-00002-of-00002.safetensors", + }, + } + (model_dir / "model.safetensors.index.json").write_text(json.dumps(base_index)) + + adapter_dir, _ = self._make_adapter(tmp_path, r=r, alpha=alpha) + safetensors.torch.save_file({}, adapter_dir / "adapter_model.safetensors") + + output_dir = tmp_path / "output" + merge_lora_sharded_efficient( + base_model_path=model_dir, + lora_adapter_path=adapter_dir, + output_path=output_dir, + device="cpu", + ) + + # Verify index was generated + index_path = output_dir / "model.safetensors.index.json" + assert index_path.exists() + with open(index_path) as f: + idx = json.load(f) + + assert "weight_map" in idx + assert len(idx["weight_map"]) == 2 + # Each key should map to a shard that exists + for _key, shard_name in idx["weight_map"].items(): + assert (output_dir / shard_name).exists(), f"Missing shard: {shard_name}" + + def test_dora_end_to_end(self, tmp_path): + """DoRA merge through the full sharded merge pipeline.""" + hidden = 16 + r = 4 + alpha = 8 + + model_dir, base_weights = self._make_base_model(tmp_path, hidden=hidden) + adapter_dir, _ = self._make_adapter(tmp_path, r=r, alpha=alpha, use_dora=True) + + lora_a = torch.randn(r, hidden) + lora_b = torch.randn(hidden, r) + magnitude = torch.randn(hidden).abs() + 0.1 + lora_state = { + "base_model.model.model.layers.0.self_attn.q_proj.lora_A.weight": lora_a, + "base_model.model.model.layers.0.self_attn.q_proj.lora_B.weight": lora_b, + "base_model.model.model.layers.0.self_attn.q_proj.lora_magnitude_vector": magnitude, + } + safetensors.torch.save_file( + lora_state, adapter_dir / "adapter_model.safetensors" + ) + + output_dir = tmp_path / "output" + merge_lora_sharded_efficient( + base_model_path=model_dir, + lora_adapter_path=adapter_dir, + output_path=output_dir, + device="cpu", + ) + + merged = safetensors.torch.load_file(output_dir / "model.safetensors") + q_key = "model.layers.0.self_attn.q_proj.weight" + + # Use PEFT's own get_delta_weight as the reference + delta = _build_peft_layer_and_get_delta( + lora_a, + lora_b, + {"r": r, "lora_alpha": alpha, "use_dora": True}, + base_weights[q_key], + magnitude=magnitude, + ) + expected = base_weights[q_key] + delta + assert torch.allclose(merged[q_key], expected, atol=1e-5) + + # Verify it differs from standard (non-DoRA) merge + standard_delta = _build_peft_layer_and_get_delta( + lora_a, + lora_b, + {"r": r, "lora_alpha": alpha}, + base_weights[q_key], + ) + assert not torch.allclose(delta, standard_delta, atol=1e-3) + + # v_proj has no LoRA weights — should be unchanged + v_key = "model.layers.0.self_attn.v_proj.weight" + assert torch.equal(merged[v_key], base_weights[v_key]), ( + "v_proj should be unchanged (no LoRA weights for it)" + ) + + def test_dora_missing_magnitude_falls_back(self): + """DoRA without magnitude vector falls back to standard LoRA merge.""" + hidden = 16 + r = 4 + alpha = 8 + scale = alpha / r + + base = torch.randn(hidden, hidden) + lora_a = torch.randn(r, hidden) + lora_b = torch.randn(hidden, r) + + # No magnitude vector in lora_state + lora_state = { + "base_model.model.layer.proj.lora_A.weight": lora_a, + "base_model.model.layer.proj.lora_B.weight": lora_b, + } + + config = {"r": r, "lora_alpha": alpha, "use_dora": True} + merged, was_merged = _merge_tensor_with_lora( + base, "layer.proj.weight", lora_state, scale, config, "cpu", use_dora=True + ) + assert was_merged + # No magnitude vector → PEFT creates DoRA layer but with default magnitude, + # which produces a result different from plain W + scale * B @ A. + # Just verify it was merged (not unchanged). + assert not torch.equal(merged, base) From 74b959e035df1e77e02ddb4b3990292bdf583cec Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 25 Mar 2026 11:19:15 -0400 Subject: [PATCH 1231/1405] dispatch scored rollouts to plugins, extend path for external plugins, better handle errors with vllm /reset_prefix_cache (#3549) * dispatch scored rollouts to plugins, extend path for external plugins, better handle errors with vllm /reset_prefix_cache * address PR comments, lint --- .../core/trainers/grpo/async_trainer.py | 50 +++++++++++++++-- src/axolotl/integrations/__init__.py | 3 ++ src/axolotl/integrations/base.py | 54 +++++++++++++++++++ src/axolotl/scripts/vllm_serve_lora.py | 22 +++++--- 4 files changed, 116 insertions(+), 13 deletions(-) diff --git a/src/axolotl/core/trainers/grpo/async_trainer.py b/src/axolotl/core/trainers/grpo/async_trainer.py index 9b6ae2e285..3e541c16dc 100644 --- a/src/axolotl/core/trainers/grpo/async_trainer.py +++ b/src/axolotl/core/trainers/grpo/async_trainer.py @@ -1536,6 +1536,29 @@ def _post_advantage_hook( ) -> None: """Called after advantages are computed. Override for replay buffer, re-roll, etc.""" + def _notify_rollouts_scored( + self, + prompts: list[str], + completions: list[str], + rewards: dict[str, list[float]], + advantages: list[float], + ): + """Dispatch on_rollouts_scored to all registered plugins (rank 0 only).""" + if not self.accelerator.is_main_process: + return + + from axolotl.integrations.base import PluginManager + + pm = PluginManager.get_instance() + if pm and pm.plugins: + # Try _axolotl_cfg first (set by causal builder), fall back to + # PluginManager's stored cfg (set during register phase). + cfg = getattr(self, "_axolotl_cfg", None) or getattr(pm, "_cfg", None) + if cfg is not None: + pm.on_rollouts_scored( + cfg, self, prompts, completions, rewards, advantages + ) + # ------------------------------------------------------------------ # Main-thread scoring # ------------------------------------------------------------------ @@ -1860,7 +1883,10 @@ def _compute_deferred_scores(self, rollout: dict) -> dict: nanmax(self.accelerator.gather(torch.max(flat_isr))).item() ) - # Log prompt/completion texts + # Log prompt/completion texts. + # NB: gather_object merges per-rank local texts into a full-batch list + # matching rewards_per_func and all_advantages which are already full-batch + # tensors (gathered/computed earlier in this method). Lengths stay aligned. prompts_text = self.processing_class.batch_decode( prompt_ids, skip_special_tokens=True ) @@ -1868,11 +1894,25 @@ def _compute_deferred_scores(self, rollout: dict) -> dict: completion_ids, skip_special_tokens=True ) if gather_object is not None: - self._logs["prompt"].extend(gather_object(prompts_text)) - self._logs["completion"].extend(gather_object(completions_text)) + gathered_prompts = gather_object(prompts_text) + gathered_completions = gather_object(completions_text) + self._logs["prompt"].extend(gathered_prompts) + self._logs["completion"].extend(gathered_completions) + else: + gathered_prompts = prompts_text + gathered_completions = completions_text + rewards_dict = {} for i, name in enumerate(self.reward_func_names): - self._logs["rewards"][name].extend(rewards_per_func[:, i].tolist()) - self._logs["advantages"].extend(all_advantages.tolist()) + reward_list = rewards_per_func[:, i].tolist() # already full-batch + self._logs["rewards"][name].extend(reward_list) + rewards_dict[name] = reward_list + adv_list = all_advantages.tolist() # already full-batch + self._logs["advantages"].extend(adv_list) + + # Notify plugins of scored rollouts + self._notify_rollouts_scored( + gathered_prompts, gathered_completions, rewards_dict, adv_list + ) # Remove deferred keys for k in list(data.keys()): diff --git a/src/axolotl/integrations/__init__.py b/src/axolotl/integrations/__init__.py index e69de29bb2..f77af49c2f 100644 --- a/src/axolotl/integrations/__init__.py +++ b/src/axolotl/integrations/__init__.py @@ -0,0 +1,3 @@ +import pkgutil + +__path__ = pkgutil.extend_path(__path__, __name__) diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index c66bc01c63..48260cb4fc 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -242,6 +242,30 @@ def add_callbacks_post_trainer( """ return [] + def on_rollouts_scored( + self, + cfg: DictDefault, + trainer, + prompts: list[str], + completions: list[str], + rewards: dict[str, list[float]], + advantages: list[float], + ): + """Called after rollouts are scored during online RL (GRPO/PPO). + + Provides access to the full scored rollout data for logging, trace + storage, or analysis. Called once per scoring step with all samples + from that step. + + Args: + cfg: The axolotl configuration. + trainer: The trainer instance. + prompts: List of prompt texts (one per sample). + completions: List of completion texts (one per sample). + rewards: Dict mapping reward function name to list of reward values. + advantages: List of advantage values (one per sample). + """ + def post_train(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): """Performs actions after training is complete. @@ -613,6 +637,36 @@ def post_train(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): for plugin in self.plugins.values(): plugin.post_train(cfg, model) + def on_rollouts_scored( + self, + cfg: DictDefault, + trainer, + prompts: list[str], + completions: list[str], + rewards: dict[str, list[float]], + advantages: list[float], + ): + """Calls the on_rollouts_scored method of all registered plugins. + + Args: + cfg: The configuration for the plugins. + trainer: The trainer instance. + prompts: List of prompt texts. + completions: List of completion texts. + rewards: Dict mapping reward function name to list of rewards. + advantages: List of advantage values. + """ + for plugin in self.plugins.values(): + try: + plugin.on_rollouts_scored( + cfg, trainer, prompts, completions, rewards, advantages + ) + except Exception: + LOG.warning( + f"Plugin {plugin.__class__.__name__}.on_rollouts_scored failed", + exc_info=True, + ) + def post_train_unload(self, cfg: DictDefault): """Calls the post_train_unload method of all registered plugins. diff --git a/src/axolotl/scripts/vllm_serve_lora.py b/src/axolotl/scripts/vllm_serve_lora.py index e292d89f82..2dda0f9bfe 100644 --- a/src/axolotl/scripts/vllm_serve_lora.py +++ b/src/axolotl/scripts/vllm_serve_lora.py @@ -114,8 +114,14 @@ def llm_worker( load_inplace=lr.get("load_inplace", False), ) - method = getattr(llm, method_name) - result = method(*args, **kwargs) + try: + method = getattr(llm, method_name) + result = method(*args, **kwargs) + except Exception as exc: + logger.warning("Worker method %s failed: %s", method_name, exc) + if command["type"] == "call": + connection.send({"error": str(exc), "kind": "worker_error"}) + continue if command["type"] == "call": connection.send(result) elif command["type"] == "shutdown": @@ -650,13 +656,13 @@ async def http_update_weights(request: HTTPWeightUpdateRequest): @app.post("/reset_prefix_cache/") async def reset_prefix_cache(): + # Fire-and-forget: send reset without expecting a reply. + # Using "fire_and_forget" type so workers don't send back a response + # that would sit in the pipe and corrupt the next recv() for + # generate/chat calls. for conn in connections: - conn.send({"type": "call", "method": "reset_prefix_cache"}) - loop = asyncio.get_running_loop() - results = await asyncio.gather( - *(loop.run_in_executor(None, conn.recv) for conn in connections) - ) - return {"message": f"Reset prefix cache: {all(results)}"} + conn.send({"type": "fire_and_forget", "method": "reset_prefix_cache"}) + return {"message": "Reset prefix cache received"} @app.post("/close_communicator/") async def close_communicator(): From 5191e4eb534be2d80f621be9f5fb1391df02ab4d Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 25 Mar 2026 18:17:49 -0400 Subject: [PATCH 1232/1405] More minor RL fixes (#3551) * fix: handle get_open_port import across TRL versions TRL 0.29+ removed get_open_port from exports; fall back to importing directly from vllm.utils or vllm.utils.network_utils. * support DP with vllm and make generation_batch_size confifurable --- src/axolotl/core/trainers/grpo/__init__.py | 3 +++ src/axolotl/scripts/vllm_serve_lora.py | 28 ++++++++++++++++++---- src/axolotl/utils/schemas/trl.py | 8 +++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 5c057cc40b..4a8c0b81d6 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -89,6 +89,9 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: if trl.num_generations: grpo_args_kwargs["num_generations"] = trl.num_generations + if trl.generation_batch_size is not None: + grpo_args_kwargs["generation_batch_size"] = trl.generation_batch_size + if trl.sync_ref_model: grpo_args_kwargs["sync_ref_model"] = trl.sync_ref_model diff --git a/src/axolotl/scripts/vllm_serve_lora.py b/src/axolotl/scripts/vllm_serve_lora.py index 2dda0f9bfe..9ca8a91348 100644 --- a/src/axolotl/scripts/vllm_serve_lora.py +++ b/src/axolotl/scripts/vllm_serve_lora.py @@ -26,8 +26,15 @@ ScriptArguments, chunk_list, extract_logprobs, - get_open_port, ) + +try: + from trl.scripts.vllm_serve import get_open_port +except ImportError: + try: + from vllm.utils import get_open_port + except ImportError: + from vllm.utils.network_utils import get_open_port from vllm import LLM, SamplingParams from vllm.lora.request import LoRARequest @@ -63,10 +70,21 @@ def llm_worker( connection: Connection, ) -> None: """Worker process that creates a vLLM LLM with LoRA enabled.""" - os.environ["VLLM_DP_RANK"] = str(data_parallel_rank) - os.environ["VLLM_DP_RANK_LOCAL"] = str(data_parallel_rank) - os.environ["VLLM_DP_SIZE"] = str(script_args.data_parallel_size) - os.environ["VLLM_DP_MASTER_PORT"] = str(master_port) + # For DP with TP=1: pin each worker to its own GPU via CUDA_VISIBLE_DEVICES. + # vLLM's LLM() offline mode doesn't support DP env vars natively, so we + # isolate each worker to a single GPU and let vLLM think it's the only one. + if script_args.data_parallel_size > 1 and script_args.tensor_parallel_size == 1: + visible = os.environ.get("CUDA_VISIBLE_DEVICES", "") + if visible: + gpu_ids = visible.split(",") + os.environ["CUDA_VISIBLE_DEVICES"] = gpu_ids[data_parallel_rank] + else: + os.environ["CUDA_VISIBLE_DEVICES"] = str(data_parallel_rank) + else: + os.environ["VLLM_DP_RANK"] = str(data_parallel_rank) + os.environ["VLLM_DP_RANK_LOCAL"] = str(data_parallel_rank) + os.environ["VLLM_DP_SIZE"] = str(script_args.data_parallel_size) + os.environ["VLLM_DP_MASTER_PORT"] = str(master_port) llm = LLM( model=script_args.model, diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index 4ef42db665..cd6a9c57a1 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -66,6 +66,14 @@ class TRLConfig(BaseModel): "description": "List of reward weights for the reward functions." }, ) + generation_batch_size: int | None = Field( + default=None, + json_schema_extra={ + "description": "Batch size for generation. Controls how many unique " + "prompts are generated per step. For full DP utilization, set to " + "num_generations * data_parallel_size (or a multiple thereof)." + }, + ) num_generations: int | None = Field( default=None, json_schema_extra={"description": "Number of generations to sample."}, From 99bde0124ce675af08ba2aca46165e314b5b2047 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 25 Mar 2026 18:22:47 -0400 Subject: [PATCH 1233/1405] deprecate torch 2.8.0 support (#3550) * deprecate torch 2.8.0 support * shell lint * odd naming of manylinux wheels for x86 --- .github/workflows/base.yml | 16 ---------------- .github/workflows/main.yml | 12 ------------ .github/workflows/multi-gpu-e2e.yml | 6 ------ .github/workflows/nightlies.yml | 10 ---------- .github/workflows/tests-nightly.yml | 2 +- .github/workflows/tests.yml | 15 ++------------- README.md | 2 +- docker/Dockerfile-uv-base | 10 +++++----- 8 files changed, 9 insertions(+), 64 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 0fe0d2b253..521d26201f 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -30,14 +30,6 @@ jobs: fail-fast: false matrix: include: - - cuda: "128" - cuda_version: 12.8.1 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.8.0 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-base" - platforms: "linux/amd64" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" @@ -160,14 +152,6 @@ jobs: fail-fast: false matrix: include: - - cuda: "128" - cuda_version: 12.8.1 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.8.0 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-uv-base" - platforms: "linux/amd64" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a3a24537c4..1fb6290d9a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,12 +18,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.8.0 - axolotl_extras: - platforms: "linux/amd64" - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" @@ -186,12 +180,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.8.0 - axolotl_extras: - platforms: "linux/amd64" - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 2bb499ded0..2c5d76e4cf 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -33,12 +33,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.8.0 - axolotl_extras: fbgemm-gpu - num_gpus: 2 # - cuda: 129 # cuda_version: 12.9.1 # python_version: "3.12" diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 0372f5c7ab..19643bea51 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -15,11 +15,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.8.0 - axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" @@ -67,11 +62,6 @@ jobs: strategy: matrix: include: - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.8.0 - axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 663b0476e1..235aebcfaf 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -44,7 +44,7 @@ jobs: fail-fast: false matrix: python_version: ["3.12"] # TODO include py3.14 once https://github.com/mistralai/mistral-common/pull/194 is merged - pytorch_version: ["2.8.0", "2.9.1", "2.10.0"] + pytorch_version: ["2.9.1", "2.10.0"] timeout-minutes: 20 steps: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5099e447cf..d753afe015 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -69,10 +69,8 @@ jobs: fail-fast: false matrix: python_version: ["3.12", "3.14"] - pytorch_version: ["2.8.0", "2.9.1", "2.10.0"] + pytorch_version: ["2.9.1", "2.10.0"] exclude: - - python_version: "3.14" - pytorch_version: "2.8.0" - python_version: "3.14" pytorch_version: "2.9.1" timeout-minutes: 20 @@ -165,10 +163,8 @@ jobs: fail-fast: false matrix: python_version: ["3.12", "3.14"] - pytorch_version: ["2.8.0", "2.9.1", "2.10.0"] + pytorch_version: ["2.9.1", "2.10.0"] exclude: - - python_version: "3.14" - pytorch_version: "2.8.0" - python_version: "3.14" pytorch_version: "2.9.1" timeout-minutes: 30 @@ -329,13 +325,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.8.0 - num_gpus: 1 - gpu_type: "B200" - axolotl_extras: fbgemm-gpu - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" diff --git a/README.md b/README.md index a425e45b8b..e353d20ad8 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Features: - NVIDIA GPU (Ampere or newer for `bf16` and Flash Attention) or AMD GPU - Python 3.11 -- PyTorch ≥2.8.0 +- PyTorch ≥2.9.1 ### Google Colab diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index 0e7acbe29a..f16777378b 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -36,22 +36,22 @@ RUN uv pip install packaging setuptools wheel psutil \ && uv pip install awscli pydantic RUN if [ "$TARGETARCH" = "amd64" ]; then \ - uv pip install --no-build-isolation "causal_conv1d @ git+https://github.com/Dao-AILab/causal-conv1d.git@main"; \ - uv pip install "mamba_ssm @ git+https://github.com/state-spaces/mamba.git@main"; \ + MAMBA_SKIP_CUDA_BUILD=TRUE CAUSAL_CONV1D_SKIP_CUDA_BUILD=TRUE uv pip install --no-build-isolation mamba_ssm causal_conv1d; \ fi # Map Python version (e.g., 3.12 -> cp312) RUN PYTHON_CP="cp$(echo $PYTHON_VERSION | tr -d '.')" && \ # Map PyTorch version (e.g., 2.9.1 -> torch2.9, 2.10.0 -> torch2.10) TORCH_TAG="torch$(echo $PYTORCH_VERSION | grep -oP '^\d+\.\d+')" && \ + LINUX_TAG="manylinux_" && \ # Map architecture case "$TARGETARCH" in \ - amd64) ARCH_TAG="x86_64" ;; \ - arm64) ARCH_TAG="aarch64" ;; \ + amd64) ARCH_TAG="2_24_x86_64.manylinux_2_28_x86_64" ;; \ + arm64) ARCH_TAG="2_34_aarch64" ;; \ *) echo "Unsupported architecture: $TARGETARCH"; exit 1 ;; \ esac && \ WHL_VERSION="v0.7.16" && \ - WHL_FILE="flash_attn-2.8.3+cu${CUDA}${TORCH_TAG}-${PYTHON_CP}-${PYTHON_CP}-linux_${ARCH_TAG}.whl" && \ + WHL_FILE="flash_attn-2.8.3+cu${CUDA}${TORCH_TAG}-${PYTHON_CP}-${PYTHON_CP}-${LINUX_TAG}${ARCH_TAG}.whl" && \ wget -nv "https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/${WHL_VERSION}/${WHL_FILE}" && \ uv pip install --no-cache-dir "${WHL_FILE}" && \ rm "${WHL_FILE}" From 00dee05fc6bd8fdff3929ba7d953bd968c8bbe30 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 28 Mar 2026 13:15:54 -0400 Subject: [PATCH 1234/1405] support flattening/packing for GRPO (#3552) * support flattening/packing for GRPO * more flattening * fix tests * improve dead vllm handling * refactor out process handling for vllm serve and move bench flattening tests to gpu tests * add validation for flattening with liger * isolate batch flattening test * flaky test --- src/axolotl/cli/vllm_serve.py | 2 + src/axolotl/core/trainers/grpo/__init__.py | 7 +- .../core/trainers/grpo/async_trainer.py | 395 +++++++++-- src/axolotl/monkeypatch/trainer/trl_vllm.py | 11 +- src/axolotl/scripts/process_cleanup.py | 232 +++++++ src/axolotl/scripts/vllm_serve_lora.py | 79 ++- src/axolotl/utils/schemas/validation.py | 11 + src/axolotl/utils/schemas/vllm.py | 7 + tests/e2e/solo/test_batch_flattening.py | 612 ++++++++++++++++++ tests/e2e/solo/test_trainer_loss_calc.py | 3 + 10 files changed, 1307 insertions(+), 52 deletions(-) create mode 100644 src/axolotl/scripts/process_cleanup.py create mode 100644 tests/e2e/solo/test_batch_flattening.py diff --git a/src/axolotl/cli/vllm_serve.py b/src/axolotl/cli/vllm_serve.py index 2180a9e7f4..06822cd78b 100644 --- a/src/axolotl/cli/vllm_serve.py +++ b/src/axolotl/cli/vllm_serve.py @@ -105,6 +105,8 @@ def do_vllm_serve( # (merged weight sync via batch_update doesn't need vLLM LoRA mode) if not getattr(cfg.trl, "vllm_lora_sync", False): lora_kwargs["enable_lora"] = False + if getattr(cfg.vllm, "worker_extension_cls", None): + lora_kwargs["worker_extension_cls"] = cfg.vllm.worker_extension_cls vllm_script_args = LoRAScriptArguments(**base_kwargs, **lora_kwargs) else: vllm_script_args = AxolotlScriptArguments( diff --git a/src/axolotl/core/trainers/grpo/__init__.py b/src/axolotl/core/trainers/grpo/__init__.py index 4a8c0b81d6..bb0046e57b 100644 --- a/src/axolotl/core/trainers/grpo/__init__.py +++ b/src/axolotl/core/trainers/grpo/__init__.py @@ -29,7 +29,7 @@ class GRPOStrategy: @classmethod def get_trainer_class( cls, - sequence_parallel: bool, + sequence_parallel: bool = False, async_grpo: bool = False, ) -> ( type[AxolotlGRPOTrainer] @@ -88,7 +88,6 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: if trl.num_generations: grpo_args_kwargs["num_generations"] = trl.num_generations - if trl.generation_batch_size is not None: grpo_args_kwargs["generation_batch_size"] = trl.generation_batch_size @@ -202,6 +201,10 @@ def set_training_args_kwargs(cls, cfg: DictDefault) -> dict[str, Any]: if getattr(trl, "vllm_lora_sync", None) is not None: grpo_args_kwargs["vllm_lora_sync"] = trl.vllm_lora_sync + # Batch flattening (top-level config, not under trl) + if getattr(cfg, "batch_flattening", None): + grpo_args_kwargs["batch_flattening"] = cfg.batch_flattening + return grpo_args_kwargs @classmethod diff --git a/src/axolotl/core/trainers/grpo/async_trainer.py b/src/axolotl/core/trainers/grpo/async_trainer.py index 3e541c16dc..3388687ad6 100644 --- a/src/axolotl/core/trainers/grpo/async_trainer.py +++ b/src/axolotl/core/trainers/grpo/async_trainer.py @@ -32,6 +32,7 @@ from typing import Any import torch +import torch.nn.functional as F from torch.utils.data import DataLoader, Dataset from trl.extras.profiling import profiling_decorator from trl.trainer import GRPOConfig, GRPOTrainer @@ -129,6 +130,18 @@ class AsyncGRPOConfig(GRPOConfig): }, ) + # --- Batch flattening --- + batch_flattening: bool = field( + default=False, + metadata={ + "help": "Use batch flattening for the scoring forward pass. Removes padding tokens " + "before the forward pass, reducing attention FLOPs proportional to the padding ratio. " + "Requires flash_attention_2 attention implementation. Incompatible with FSDP and " + "multimodal models. The per-token logprob results differ by bf16 precision (~0.03 mean) " + "but produce equivalent loss and gradients." + }, + ) + # --- Streaming scoring --- streaming_partial_batch: bool = field( default=False, @@ -523,7 +536,10 @@ def __init__( def set_trainer(self, trainer) -> None: """Inject the live trainer reference and create the prompt DataLoader.""" self._trainer = trainer - self._init_prompt_dataloader() + # Defer _init_prompt_dataloader if trainer.args is not yet set + # (happens when set_trainer is called from _create_data_producer during __init__) + if getattr(trainer, "args", None) is not None: + self._init_prompt_dataloader() def _init_prompt_dataloader(self) -> None: from functools import partial @@ -580,6 +596,10 @@ def produce( **kwargs, ) -> RolloutDataset | None: """Generate a fresh GRPO training rollout.""" + # Lazy init: create prompt DataLoader if deferred from set_trainer + if self._prompt_dl is None and self._trainer is not None: + self._init_prompt_dataloader() + is_main = self._trainer.accelerator.is_main_process # FSDP rank0-only mode: non-rank-0 returns None (broadcast fills it later) @@ -1610,6 +1630,16 @@ def _compute_deferred_scores(self, rollout: dict) -> dict: self._launch_reward_workers(inputs, prompts, completions, completion_ids_list) # --- Policy logprobs --- + # When batch_flattening is enabled, use the flattened (padding-free) forward + # pass for the scoring path. This removes padding tokens before the forward + # pass, reducing attention FLOPs proportional to the padding ratio (20-34% + # faster in benchmarks). Requires flash_attention_2 and no multimodal inputs. + can_flatten = ( + getattr(self.args, "batch_flattening", False) + and not forward_kwargs # no multimodal inputs + and not self.is_fsdp_enabled # FSDP needs wrapped model + ) + logprob_batch_size = min(batch_size * 4, len(prompt_ids)) with disable_gradient_checkpointing( self.model, self.args.gradient_checkpointing_kwargs @@ -1619,15 +1649,25 @@ def _compute_deferred_scores(self, rollout: dict) -> dict: self.use_vllm and getattr(self, "vllm_importance_sampling_correction", False) ): - old_per_token_logps, _ = self._get_per_token_logps_and_entropies( - self.model, - prompt_completion_ids, - attention_mask, - logits_to_keep, - logprob_batch_size, - num_images=num_images, - **forward_kwargs, - ) + if can_flatten: + old_per_token_logps = self._get_per_token_logps_flattened( + self.model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + batch_size=logprob_batch_size, + prompt_mask=prompt_mask, + ) + else: + old_per_token_logps, _ = self._get_per_token_logps_and_entropies( + self.model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + logprob_batch_size, + num_images=num_images, + **forward_kwargs, + ) data["old_per_token_logps"] = old_per_token_logps else: old_per_token_logps = None @@ -1988,6 +2028,11 @@ def _compute_streaming_group_scores( self._launch_reward_workers(inputs, prompts, completions, completion_ids_list) # --- Policy logprobs for this chunk (GPU, overlaps with BG rewards) --- + can_flatten = ( + getattr(self.args, "batch_flattening", False) + and not forward_kwargs + and not self.is_fsdp_enabled + ) logprob_batch_size = min(batch_size * 2, chunk_size) with disable_gradient_checkpointing( self.model, self.args.gradient_checkpointing_kwargs @@ -1997,15 +2042,25 @@ def _compute_streaming_group_scores( self.use_vllm and getattr(self, "vllm_importance_sampling_correction", False) ): - old_logps, _ = self._get_per_token_logps_and_entropies( - self.model, - prompt_completion_ids, - attention_mask, - logits_to_keep, - logprob_batch_size, - num_images=num_images, - **forward_kwargs, - ) + if can_flatten: + old_logps = self._get_per_token_logps_flattened( + self.model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + batch_size=logprob_batch_size, + prompt_mask=chunk_prompt_mask, + ) + else: + old_logps, _ = self._get_per_token_logps_and_entropies( + self.model, + prompt_completion_ids, + attention_mask, + logits_to_keep, + logprob_batch_size, + num_images=num_images, + **forward_kwargs, + ) if "old_per_token_logps" not in data: total = len(data["prompt_ids"]) data["old_per_token_logps"] = torch.zeros( @@ -2354,7 +2409,38 @@ def _prepare_inputs(self, generation_batch): return super()._prepare_inputs(generation_batch) def _prepare_inputs_data_producer(self, generation_batch): - """Data producer path: produce rollout, score deferred logps, split into micro-batches.""" + """Data producer path: produce rollout, score deferred logps, split into micro-batches. + + Architecture (with async_prefetch=True): + BG thread: produce(skip_policy_logps=True) → vLLM generation + reward computation + Main thread: deferred scoring (policy logprobs via GPU forward pass) → training + + Why deferred scoring is necessary for stable training: + The policy logprobs (old_per_token_logps) must come from the CURRENT + training model, not the vLLM model (which is N steps behind). Using + stale vLLM logprobs as old_logps causes the importance sampling ratio + to start far from 1.0, leading to: + - Immediate PPO clipping → wasted samples + - High-variance gradients from IS correction + - Compounding per-token ratio errors on long sequences + - In extreme cases, complete training failure (exp-003: accuracy=0) + + Deferred scoring computes old_logps with the latest model weights, so + the IS ratio starts at exactly 1.0 and drifts gradually — giving + maximum useful gradient signal before clipping activates. + + Cost: one additional forward pass per scoring round (GPU-bound, cannot + overlap with training on the same GPU). Use ``batch_flattening: true`` + to reduce this cost by eliminating padding tokens from the forward pass. + + Pipeline: + [produce(BG)] → [deferred_scores(GPU)] → [train×GA(GPU)] → [weight_sync] + ↑ can't overlap with train (same GPU) + + Bottleneck: the produce() wait (generation-limited) dominates when + generation is slower than training + scoring. Async prefetch hides + part of this by generating in the BG thread while training runs. + """ # Return from buffer if available if self._buffered_inputs: return self._buffered_inputs.pop(0) @@ -2370,10 +2456,8 @@ def _prepare_inputs_data_producer(self, generation_batch): args=self.args, ) - # Convert RolloutDataset back to a dict for scoring/splitting rollout = rollout_dataset._data - # If async (skip_policy_logps=True), score deferred logps on main thread if rollout.get("_pending_policy_logps"): if self.args.streaming_partial_batch: micro_batches = self._score_streaming(rollout) @@ -2385,7 +2469,6 @@ def _prepare_inputs_data_producer(self, generation_batch): micro_batches = [unsplit_pixel_values_by_grid(b) for b in batches] micro_batches = micro_batches * self.num_iterations else: - # Sync path: data is already fully scored rollout = split_pixel_values_by_grid(rollout) batches = split_tensor_dict(rollout, self.args.steps_per_generation) micro_batches = [unsplit_pixel_values_by_grid(b) for b in batches] @@ -2428,6 +2511,219 @@ def _prepare_inputs_legacy_async(self, generation_batch): return micro_batches[0] + def _get_per_token_logps_flattened( + self, + model, + input_ids, + attention_mask, + logits_to_keep, + batch_size=None, + prompt_mask=None, + ) -> torch.Tensor: + """Compute per-token log-probs using batch flattening (padding-free). + + Instead of processing padded batches where attention wastes compute on + padding tokens, this method: + 1. Chunks the batch into sub-batches of ``batch_size`` sequences + 2. For each chunk, flattens non-padding tokens into [1, chunk_tokens] + 3. Uses FlashAttentionKwargs (cu_seq_lens) for varlen attention + 4. Computes selective_log_softmax on the flat logits + 5. Gathers completion logprobs back to (B, logits_to_keep) padded format + + Args: + prompt_mask: (B, L) mask where 1 = prompt token, 0 = completion/padding. + Used to determine the exact prompt length per sequence for correct + logprob gathering. If None, inferred as seq_len - logits_to_keep. + + Chunking prevents OOM when the total flattened sequence is too long + (e.g., 32 sequences × 2048 tokens = 65K tokens → 20GB logits tensor). + + Requires flash_attention_2 attention implementation. + """ + if not self.is_fsdp_enabled: + model = self.accelerator.unwrap_model(model, keep_fp32_wrapper=False) + + device = input_ids.device + B, L = input_ids.shape + if batch_size is None: + batch_size = max(1, B) + + autocast_ctx = torch.autocast(device_type=device.type, dtype=torch.bfloat16) + all_logps = torch.zeros(B, logits_to_keep, device=device) + + for chunk_start in range(0, B, batch_size): + chunk_end = min(chunk_start + batch_size, B) + chunk_ids = input_ids[chunk_start:chunk_end] + chunk_mask = attention_mask[chunk_start:chunk_end] + n = chunk_end - chunk_start + + seq_lens = chunk_mask.sum(dim=1).to(torch.int32) + total_tokens = seq_lens.sum().item() + cu_seqlens = torch.zeros(n + 1, dtype=torch.int32, device=device) + cu_seqlens[1:] = seq_lens.cumsum(0) + + valid = chunk_mask.bool() + flat_ids = chunk_ids[valid].unsqueeze(0) + positions = torch.arange(L, device=device).unsqueeze(0).expand(n, L) + flat_pos = positions[valid].unsqueeze(0) + + with autocast_ctx: + logits = model( + input_ids=flat_ids, + position_ids=flat_pos, + use_cache=False, + cu_seq_lens_q=cu_seqlens, + cu_seq_lens_k=cu_seqlens, + max_length_q=seq_lens.max().item(), + max_length_k=seq_lens.max().item(), + ).logits + logits = torch.nan_to_num(logits, nan=0.0) + + # Compute logprobs on the flat shifted tensor + flat_logits = logits[0, :-1, :] / self.temperature + flat_targets = flat_ids[0, 1:] + flat_logps = selective_log_softmax( + flat_logits.unsqueeze(0), flat_targets.unsqueeze(0) + )[0] + + # Mask out cross-sequence boundary positions. In the shifted + # tensor, position cu_seqlens[i]-1 (for i>0) is where sequence + # i-1's last token "predicts" sequence i's first token — garbage. + for boundary in cu_seqlens[1:-1]: + idx = boundary.item() - 1 + if 0 <= idx < flat_logps.size(0): + flat_logps[idx] = 0.0 + + # Gather completion logprobs per sequence. + # Use prompt_mask to determine exact prompt length (not logits_to_keep, + # which is the padded completion dimension and may exceed the actual + # completion length for shorter sequences). + for i in range(n): + slen = seq_lens[i].item() + abs_i = chunk_start + i # absolute index in the full batch + if prompt_mask is not None: + plen = int(prompt_mask[abs_i].sum().item()) + else: + plen = max(1, slen - logits_to_keep) + n_compl = slen - plen + start = cu_seqlens[i].item() + plen - 1 + start = max(0, start) + actual = min(n_compl, total_tokens - 1 - start) + if actual > 0: + all_logps[chunk_start + i, :actual] = flat_logps[ + start : start + actual + ] + + del logits, flat_logits, flat_logps, flat_ids + torch.cuda.empty_cache() + + return all_logps + + def _get_per_token_logps_and_entropies_flattened( + self, + model, + input_ids, + attention_mask, + logits_to_keep, + batch_size=None, + prompt_mask=None, + compute_entropy=True, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + """Flattened forward pass for training (with gradients). + + Same padding removal as the scoring path, but: + - Gradients flow through for backward pass + - Computes entropy alongside logprobs + - Per-sequence logprob/entropy extraction preserves grad graph + """ + device = input_ids.device + B, L = input_ids.shape + if batch_size is None: + batch_size = max(1, B) + + autocast_ctx = torch.autocast(device_type=device.type, dtype=torch.bfloat16) + + # Pre-allocate output containers (will be filled with grad-carrying slices) + all_logps_list: list[torch.Tensor] = [] + all_entropy_list: list[torch.Tensor] = [] + + for chunk_start in range(0, B, batch_size): + chunk_end = min(chunk_start + batch_size, B) + chunk_ids = input_ids[chunk_start:chunk_end] + chunk_mask = attention_mask[chunk_start:chunk_end] + n = chunk_end - chunk_start + + seq_lens = chunk_mask.sum(dim=1).to(torch.int32) + cu_seqlens = torch.zeros(n + 1, dtype=torch.int32, device=device) + cu_seqlens[1:] = seq_lens.cumsum(0) + + valid = chunk_mask.bool() + flat_ids = chunk_ids[valid].unsqueeze(0) + positions = torch.arange(L, device=device).unsqueeze(0).expand(n, L) + flat_pos = positions[valid].unsqueeze(0) + + with autocast_ctx: + logits = model( + input_ids=flat_ids, + position_ids=flat_pos, + use_cache=False, + cu_seq_lens_q=cu_seqlens, + cu_seq_lens_k=cu_seqlens, + max_length_q=seq_lens.max().item(), + max_length_k=seq_lens.max().item(), + ).logits + logits = torch.nan_to_num(logits, nan=0.0) + + # Extract logprobs and entropy per-sequence (avoids cross-sequence targets, + # preserves gradient graph through selective_log_softmax → logits → model) + for i in range(n): + slen = seq_lens[i].item() + abs_i = chunk_start + i + if prompt_mask is not None: + plen = int(prompt_mask[abs_i].sum().item()) + else: + plen = max(1, slen - logits_to_keep) + n_compl = slen - plen + s = cu_seqlens[i].item() + + if n_compl <= 0: + # No completion tokens — append zeros + all_logps_list.append(torch.zeros(logits_to_keep, device=device)) + if compute_entropy: + all_entropy_list.append( + torch.zeros(logits_to_keep, device=device) + ) + continue + + with autocast_ctx: + # Shifted logits and targets for this sequence only + seq_logits = logits[0, s + plen - 1 : s + slen - 1, :] + seq_logits = seq_logits / self.temperature + seq_targets = flat_ids[0, s + plen : s + slen] + + # Log probs (differentiable) + lps = selective_log_softmax( + seq_logits.unsqueeze(0), seq_targets.unsqueeze(0) + )[0] # (n_compl,) + + # Pad to logits_to_keep + if n_compl < logits_to_keep: + lps = F.pad(lps, (0, logits_to_keep - n_compl)) + all_logps_list.append(lps[:logits_to_keep]) + + if compute_entropy: + ent = entropy_from_logits(seq_logits) # (n_compl,) + if n_compl < logits_to_keep: + ent = F.pad(ent, (0, logits_to_keep - n_compl)) + all_entropy_list.append(ent[:logits_to_keep]) + + # Stack per-sequence results into (B, logits_to_keep) tensors + all_logps = torch.stack(all_logps_list, dim=0) + all_entropies = ( + torch.stack(all_entropy_list, dim=0) if compute_entropy else None + ) + return all_logps, all_entropies + @profiling_decorator def _get_per_token_logps_and_entropies( self, @@ -2599,20 +2895,47 @@ def _compute_loss(self, model, inputs): else completion_mask * inputs["tool_mask"] ) - per_token_logps, entropies = self._get_per_token_logps_and_entropies( - model, - input_ids, - attention_mask, - logits_to_keep, - compute_entropy=True, - pixel_values=inputs.get("pixel_values"), - image_grid_thw=inputs.get("image_grid_thw"), - num_images=inputs.get("num_images"), - pixel_attention_mask=inputs.get("pixel_attention_mask"), - image_sizes=inputs.get("image_sizes"), - token_type_ids=inputs.get("token_type_ids"), - mm_token_type_ids=inputs.get("mm_token_type_ids"), + # Check for multimodal inputs + forward_kwargs = { + k: inputs[k] + for k in ( + "pixel_values", + "image_grid_thw", + "num_images", + "pixel_attention_mask", + "image_sizes", + "token_type_ids", + "mm_token_type_ids", + ) + if k in inputs and inputs[k] is not None + } + + can_flatten = ( + getattr(self.args, "batch_flattening", False) + and not forward_kwargs + and not self.is_fsdp_enabled ) + + if can_flatten: + per_token_logps, entropies = ( + self._get_per_token_logps_and_entropies_flattened( + model, + input_ids, + attention_mask, + logits_to_keep, + prompt_mask=prompt_mask, + compute_entropy=True, + ) + ) + else: + per_token_logps, entropies = self._get_per_token_logps_and_entropies( + model, + input_ids, + attention_mask, + logits_to_keep, + compute_entropy=True, + **forward_kwargs, + ) if self.top_entropy_quantile < 1.0: entropy_mask = self.get_high_entropy_mask( entropies, mask, 1 - self.top_entropy_quantile diff --git a/src/axolotl/monkeypatch/trainer/trl_vllm.py b/src/axolotl/monkeypatch/trainer/trl_vllm.py index e3f57ccf56..a234bbf3c5 100644 --- a/src/axolotl/monkeypatch/trainer/trl_vllm.py +++ b/src/axolotl/monkeypatch/trainer/trl_vllm.py @@ -57,7 +57,16 @@ def _batch_update_named_params( response = self.session.post( url, json={"params": param_metadata}, timeout=120 ) - if response.status_code != 200: + if response.status_code == 404: + # Server doesn't support batch endpoint — fall back to individual updates + for meta in param_metadata: + ind_url = f"{self.base_url}/update_named_param/" + ind_response = self.session.post(ind_url, json=meta, timeout=120) + if ind_response.status_code != 200: + raise Exception( + f"Individual update failed: {ind_response.status_code}, {ind_response.text}" + ) + elif response.status_code != 200: raise Exception( f"Request failed: {response.status_code}, {response.text}" ) diff --git a/src/axolotl/scripts/process_cleanup.py b/src/axolotl/scripts/process_cleanup.py new file mode 100644 index 0000000000..56f2943753 --- /dev/null +++ b/src/axolotl/scripts/process_cleanup.py @@ -0,0 +1,232 @@ +"""Reusable process lifecycle management for vLLM serve scripts. + +Handles graceful shutdown, orphan cleanup, and health monitoring for +multiprocessing-based server architectures where a main process +dispatches work to worker subprocesses that spawn GPU-heavy children +(e.g., vLLM EngineCore). + +Usage: + + from axolotl.scripts.process_cleanup import ProcessManager + + manager = ProcessManager(processes, connections) + manager.register_signal_handlers() + + # In FastAPI lifespan: + async with manager.lifespan_context(): + yield # server runs here + + # In endpoints: + manager.check_workers_alive() # raises if dead + + # In worker command loop: + if manager.is_fatal_error(exc): + break # exit worker +""" + +import asyncio +import atexit +import logging +import os +from multiprocessing import Process +from multiprocessing.connection import Connection + +logger = logging.getLogger(__name__) + + +def kill_process_tree(pid: int) -> None: + """Kill a process and all its descendants (depth-first).""" + import subprocess # nosec B404 + + try: + result = subprocess.run( # nosec B603 B607 + ["pgrep", "-P", str(pid)], + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + for child_pid in result.stdout.strip().split("\n"): + child_pid = child_pid.strip() + if child_pid: + kill_process_tree(int(child_pid)) + except (FileNotFoundError, ValueError): + pass + + try: + os.kill(pid, 9) + except (ProcessLookupError, PermissionError): + pass + + +def cleanup_orphan_processes(*patterns: str) -> None: + """Kill orphan processes matching any of the given patterns. + + Uses ``pgrep -f`` to find processes. Skips the current process. + Intended for cleaning up GPU-holding subprocesses (EngineCore) + that survive their parent's death. + """ + import subprocess # nosec B404 + + my_pid = os.getpid() + for pattern in patterns: + try: + result = subprocess.run( # nosec B603 B607 + ["pgrep", "-f", pattern], + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + for pid in result.stdout.strip().split("\n"): + pid = pid.strip() + if pid and int(pid) != my_pid: + try: + os.kill(int(pid), 9) + logger.info("Killed orphan process %s (%s)", pid, pattern) + except (ProcessLookupError, ValueError): + pass + except FileNotFoundError: + pass + + +def is_fatal_worker_error(exc: Exception) -> bool: + """Check if an exception indicates the worker should exit. + + Returns True for errors from which the worker cannot recover, + such as the vLLM EngineCore dying. + """ + exc_str = str(exc) + exc_type = type(exc).__name__ + return ( + "EngineCore" in exc_str + or "EngineDeadError" in exc_type + or "engine" in exc_str.lower() + and "died" in exc_str.lower() + ) + + +def safe_recv(conn: Connection): + """Receive from a pipe, returning an error dict if the pipe is broken.""" + try: + return conn.recv() + except EOFError: + return {"error": "Worker process died (pipe closed)", "kind": "worker_dead"} + + +class ProcessManager: + """Manages worker process lifecycle for a FastAPI-based serve script. + + Handles: + - Signal-based shutdown (SIGTERM) + - Background health monitoring (detects dead workers) + - Process tree cleanup on exit + - Orphan EngineCore cleanup + + Args: + processes: List of worker Process objects. + connections: List of parent-side Pipe connections to workers. + orphan_patterns: Process name patterns to search for orphans on cleanup. + Defaults to ``["VLLM::EngineCore"]``. + monitor_interval: Seconds between worker health checks. + shutdown_timeout: Seconds to wait for graceful worker exit before SIGTERM. + kill_timeout: Seconds to wait after SIGTERM before SIGKILL. + """ + + def __init__( + self, + processes: list[Process], + connections: list[Connection], + orphan_patterns: list[str] | None = None, + monitor_interval: float = 5.0, + shutdown_timeout: float = 30.0, + kill_timeout: float = 15.0, + ): + self.processes = processes + self.connections = connections + self.orphan_patterns = orphan_patterns or ["VLLM::EngineCore"] + self.monitor_interval = monitor_interval + self.shutdown_timeout = shutdown_timeout + self.kill_timeout = kill_timeout + + def register_cleanup(self) -> None: + """Register atexit cleanup for orphan processes. + + Does NOT override SIGTERM — let uvicorn handle it naturally, + which triggers the lifespan shutdown where ``_shutdown_workers`` + runs. The atexit handler is a safety net for abnormal exits. + """ + atexit.register(self._cleanup_orphans) + + def check_workers_alive(self) -> None: + """Raise RuntimeError if any worker process has died. + + Call this at the start of request handlers to fail fast + instead of hanging on a broken pipe. + """ + dead = [i for i, p in enumerate(self.processes) if not p.is_alive()] + if dead: + raise RuntimeError( + f"vLLM worker(s) {dead} died. Restart the server to recover." + ) + + def get_health_status(self) -> dict: + """Return health status dict. Use as the /health endpoint response.""" + dead = [i for i, p in enumerate(self.processes) if not p.is_alive()] + if dead: + return { + "status": "unhealthy", + "dead_workers": dead, + "message": "Worker(s) died. Restart the server.", + } + return {"status": "ok"} + + async def monitor_workers(self) -> None: + """Background coroutine that detects dead workers and exits. + + When all workers are dead, cleans up their process trees and + orphan subprocesses, then force-exits the server. + """ + while True: + await asyncio.sleep(self.monitor_interval) + alive = [p.is_alive() for p in self.processes] + if not any(alive): + logger.error( + "All vLLM workers died. Shutting down server. " + "Check logs for EngineCore errors and restart." + ) + # Kill process trees for any workers that left orphans + for p in self.processes: + if p.pid is not None: + kill_process_tree(p.pid) + self._cleanup_orphans() + os._exit(1) + + def _shutdown_workers(self) -> None: + """Send shutdown commands and escalate to kill if needed.""" + for conn in self.connections: + try: + conn.send({"type": "shutdown"}) + except Exception: + pass + for i, p in enumerate(self.processes): + if not p.is_alive(): + continue + p.join(timeout=self.shutdown_timeout) + if p.is_alive(): + logger.warning( + "Worker %d didn't exit in %.0fs, sending SIGTERM", + i, + self.shutdown_timeout, + ) + p.terminate() + p.join(timeout=self.kill_timeout) + if p.is_alive(): + logger.warning("Worker %d didn't respond to SIGTERM, force killing", i) + p.kill() + p.join(timeout=5) + self._cleanup_orphans() + logger.info("Worker shutdown complete") + + def _cleanup_orphans(self) -> None: + cleanup_orphan_processes(*self.orphan_patterns) diff --git a/src/axolotl/scripts/vllm_serve_lora.py b/src/axolotl/scripts/vllm_serve_lora.py index 9ca8a91348..344c4327f0 100644 --- a/src/axolotl/scripts/vllm_serve_lora.py +++ b/src/axolotl/scripts/vllm_serve_lora.py @@ -38,6 +38,12 @@ from vllm import LLM, SamplingParams from vllm.lora.request import LoRARequest +from axolotl.scripts.process_cleanup import ( + ProcessManager, + is_fatal_worker_error, + safe_recv, +) + logger = logging.getLogger(__name__) @@ -61,6 +67,10 @@ class LoRAScriptArguments(ScriptArguments): default="bfloat16", metadata={"help": "Data type for LoRA weights."}, ) + worker_extension_cls: str = field( + default="trl.scripts.vllm_serve.WeightSyncWorkerExtension", + metadata={"help": "vLLM worker extension class for weight synchronization."}, + ) def llm_worker( @@ -96,8 +106,7 @@ def llm_worker( enable_prefix_caching=script_args.enable_prefix_caching, kv_cache_dtype=script_args.kv_cache_dtype, max_model_len=script_args.max_model_len, - # Use batch-capable worker extension (adds batch_update_named_params + auto-close) - worker_extension_cls="axolotl.scripts.vllm_worker_ext.BatchWeightSyncWorkerExtension", + worker_extension_cls=script_args.worker_extension_cls, trust_remote_code=script_args.trust_remote_code, model_impl=script_args.vllm_model_impl, logprobs_mode="processed_logprobs", @@ -110,11 +119,28 @@ def llm_worker( connection.send({"status": "ready"}) + def _worker_cleanup(): + """Clean up the LLM and its EngineCore subprocess on worker exit.""" + from axolotl.scripts.process_cleanup import cleanup_orphan_processes + + try: + llm.collective_rpc(method="close_communicator") + except Exception: + pass + # Kill EngineCore children of this worker + cleanup_orphan_processes("VLLM::EngineCore") + + import atexit as _atexit + + _atexit.register(_worker_cleanup) + while True: try: command = connection.recv() - except KeyboardInterrupt: - llm.collective_rpc(method="close_communicator") + except (KeyboardInterrupt, EOFError): + break + + if command.get("type") == "shutdown": break if command["type"] in ["call", "fire_and_forget"]: @@ -139,6 +165,12 @@ def llm_worker( logger.warning("Worker method %s failed: %s", method_name, exc) if command["type"] == "call": connection.send({"error": str(exc), "kind": "worker_error"}) + if is_fatal_worker_error(exc): + logger.error( + "Fatal worker error (EngineCore died), exiting. " + "Restart the vLLM server to recover." + ) + break continue if command["type"] == "call": connection.send(result) @@ -156,7 +188,7 @@ def main(script_args: ScriptArguments): # Request/Response models (defined locally like TRL's vllm_serve.main) class GenerateRequest(BaseModel): - prompts: list[str] + prompts: list[str] | list[list[int]] images: list[str] | None = None n: int = 1 repetition_penalty: float = 1.0 @@ -230,6 +262,10 @@ class InitCommunicatorRequest(BaseModel): connections.append(parent_conn) processes.append(process) + # Process lifecycle management + manager = ProcessManager(processes, connections) + manager.register_cleanup() + @asynccontextmanager async def lifespan(app: FastAPI): import time @@ -256,12 +292,11 @@ async def lifespan(app: FastAPI): if isinstance(msg, dict) and msg.get("status") == "ready": ready.add(id(conn)) await asyncio.sleep(0.1) + + monitor_task = asyncio.create_task(manager.monitor_workers()) yield - for p in processes: - p.join(timeout=10) - if p.is_alive(): - p.terminate() - p.join() + monitor_task.cancel() + manager._shutdown_workers() app = FastAPI(lifespan=lifespan) @@ -324,7 +359,12 @@ async def clear_lora_adapter(): @app.get("/health/") async def health(): - return {"status": "ok"} + status = manager.get_health_status() + if status["status"] != "ok": + from fastapi.responses import JSONResponse + + return JSONResponse(status_code=503, content=status) + return status @app.get("/get_world_size/") async def get_world_size(): @@ -336,6 +376,8 @@ async def get_world_size(): @app.post("/generate/", response_model=GenerateResponse) async def generate(request: GenerateRequest): """Generate completions with optional LoRA adapter.""" + manager.check_workers_alive() + import base64 from io import BytesIO @@ -350,7 +392,12 @@ async def generate(request: GenerateRequest): images: list[str | None] = request.images or [None] * len(request.prompts) # type: ignore[assignment,list-item] prompts: list[dict[str, Any]] = [] for prompt, image in zip(request.prompts, images, strict=True): - row: dict[str, Any] = {"prompt": prompt} + # Support both string prompts and token ID lists + row: dict[str, Any] + if isinstance(prompt, list): + row = {"prompt_token_ids": prompt} + else: + row = {"prompt": prompt} if image is not None: from PIL import Image @@ -410,12 +457,17 @@ async def generate(request: GenerateRequest): # Use run_in_executor so blocking recv() doesn't freeze the event loop # (allows /set_lora_adapter/ and other endpoints to be served concurrently) loop = asyncio.get_running_loop() + all_outputs = await asyncio.gather( - *(loop.run_in_executor(None, conn.recv) for conn in connections) + *(loop.run_in_executor(None, safe_recv, conn) for conn in connections) ) all_outputs = [ o for o, c in zip(all_outputs, chunked_prompts, strict=True) if c ] + # Check for worker errors before flattening + for o in all_outputs: + if isinstance(o, dict) and "error" in o: + raise RuntimeError(f"vLLM worker error: {o['error']}") all_outputs = list(chain.from_iterable(all_outputs)) return { @@ -430,6 +482,7 @@ async def generate(request: GenerateRequest): @app.post("/chat/", response_model=ChatResponse) async def chat(request: ChatRequest): """Chat endpoint with optional LoRA adapter.""" + manager.check_workers_alive() generation_kwargs = { "n": request.n, "repetition_penalty": request.repetition_penalty, diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index c7eeb6fa40..f665a99ff9 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -837,6 +837,17 @@ def check_batch_flattening_fa(cls, data): if data.get("micro_batch_size") == 1 and not batch_flattening_auto: LOG.warning("batch_flattening has no effect with micro_batch_size == 1") + # Liger loss takes a separate code path (compute_liger_loss) that + # bypasses the flattened training forward pass. Batch flattening + # still applies to the scoring/deferred logprobs path. + trl_cfg = data.get("trl") or {} + if isinstance(trl_cfg, dict) and trl_cfg.get("use_liger_loss"): + LOG.warning( + "batch_flattening with use_liger_loss: flattening will only " + "apply to the scoring path (deferred logprobs). The training " + "forward pass uses Liger's fused lm_head+loss kernel instead." + ) + if ( batch_flattening_auto and data.get("flash_attention") diff --git a/src/axolotl/utils/schemas/vllm.py b/src/axolotl/utils/schemas/vllm.py index 5198d4173a..c1f010b02c 100644 --- a/src/axolotl/utils/schemas/vllm.py +++ b/src/axolotl/utils/schemas/vllm.py @@ -71,3 +71,10 @@ class VllmConfig(BaseModel): "for native LoRA support, or leave None for default TRL serve." }, ) + worker_extension_cls: str | None = Field( + default=None, + json_schema_extra={ + "description": "vLLM worker extension class for weight synchronization. " + "Defaults to 'trl.scripts.vllm_serve.WeightSyncWorkerExtension'." + }, + ) diff --git a/tests/e2e/solo/test_batch_flattening.py b/tests/e2e/solo/test_batch_flattening.py new file mode 100644 index 0000000000..80b7b02590 --- /dev/null +++ b/tests/e2e/solo/test_batch_flattening.py @@ -0,0 +1,612 @@ +""" +Unit tests for batch flattening correctness in GRPO. + +Validates that flattened (padding-free) forward passes produce identical +results to padded forward passes by calling the ACTUAL AsyncGRPOTrainer methods: + 1. Deferred scoring: _get_per_token_logps_flattened vs _get_per_token_logps_and_entropies + 2. Training loss: _get_per_token_logps_and_entropies_flattened vs _get_per_token_logps_and_entropies + +Run: CUDA_VISIBLE_DEVICES=1 python test_batch_flattening.py +""" + +import types +from unittest.mock import MagicMock + +import torch +from transformers import AutoModelForCausalLM + +# Import the actual trainer methods we want to test +from axolotl.core.trainers.grpo.async_trainer import AsyncGRPOTrainer + +MODEL_NAME = "Qwen/Qwen3-0.6B" + + +def _fix_patched_attention(model): + """Bind apply_qkv on attention modules if LoRA kernel monkeypatch is active. + + The LoRA kernel tests replace ``Qwen3Attention.forward`` at the class level + with ``axolotl_attn_forward``, which expects a per-instance ``apply_qkv`` + method. Models created *after* that patch but *without* the per-instance + setup will crash. We fix this by binding the original (non-LoRA) apply_qkv. + """ + from axolotl.monkeypatch.lora_kernels import original_apply_o, original_apply_qkv + + for module in model.modules(): + fwd_name = getattr(type(module).forward, "__name__", "") + if "axolotl" in fwd_name and not hasattr(module, "apply_qkv"): + module.apply_qkv = types.MethodType(original_apply_qkv, module) + module.apply_o = types.MethodType(original_apply_o, module) + + +def setup_model(eval_mode=True): + model = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, dtype=torch.bfloat16, attn_implementation="flash_attention_2" + ).cuda() + _fix_patched_attention(model) + if eval_mode: + model.eval() + else: + model.train() + return model + + +def make_mock_trainer(model): + """Create a minimal mock that has the attributes needed by the trainer methods. + + The three methods we test (_get_per_token_logps_flattened, + _get_per_token_logps_and_entropies_flattened, _get_per_token_logps_and_entropies) + access self.temperature, self.use_liger_kernel, self.is_fsdp_enabled, + self.accelerator, and self.model_kwarg_keys. + """ + trainer = MagicMock(spec=[]) + + trainer.temperature = 1.0 + trainer.use_liger_kernel = False + trainer.is_fsdp_enabled = False + trainer.model_kwarg_keys = set() + + # accelerator.unwrap_model should return the model unchanged + accelerator = MagicMock() + accelerator.unwrap_model = lambda m, keep_fp32_wrapper=True: m + trainer.accelerator = accelerator + + # Bind the real unbound methods to our mock + trainer._get_per_token_logps_flattened = types.MethodType( + AsyncGRPOTrainer._get_per_token_logps_flattened, trainer + ) + trainer._get_per_token_logps_and_entropies_flattened = types.MethodType( + AsyncGRPOTrainer._get_per_token_logps_and_entropies_flattened, trainer + ) + trainer._get_per_token_logps_and_entropies = types.MethodType( + AsyncGRPOTrainer._get_per_token_logps_and_entropies, trainer + ) + + return trainer + + +def make_grpo_batch(B=4, max_compl=64, vocab_range=(100, 5000)): + """Create a GRPO-style batch matching the real data layout. + + In real GRPO, input_ids = cat([prompt_ids, completion_ids], dim=1). + prompt_ids is padded to max_prompt_len, completion_ids to max_compl. + So input_ids has shape (B, max_prompt_len + max_compl), and the last + max_compl positions are ALWAYS the completion dimension. + """ + torch.manual_seed(42) + + # Fixed prompt length: avoids prompt padding which causes position-0 + # divergence between padded and flattened paths (the padded path's shifted + # window at position 0 uses a padding-position logit when prompt_len < max_prompt). + fixed_prompt = 20 + prompt_lens = [fixed_prompt] * B + compl_lens = [max_compl] * B + max_prompt = fixed_prompt + logits_to_keep = max_compl + + # Build like real GRPO: prompt_ids (B, max_prompt) + completion_ids (B, max_compl) + prompt_ids = torch.zeros(B, max_prompt, dtype=torch.long, device="cuda") + completion_ids = torch.randint(*vocab_range, (B, max_compl), device="cuda") + prompt_mask_raw = torch.zeros(B, max_prompt, dtype=torch.long, device="cuda") + + for i in range(B): + prompt_ids[i, : prompt_lens[i]] = torch.randint( + *vocab_range, (prompt_lens[i],), device="cuda" + ) + prompt_mask_raw[i, : prompt_lens[i]] = 1 + + # Concatenate like _compute_loss does + input_ids = torch.cat([prompt_ids, completion_ids], dim=1) + completion_mask_raw = torch.ones(B, max_compl, dtype=torch.long, device="cuda") + attention_mask = torch.cat([prompt_mask_raw, completion_mask_raw], dim=1) + # Full prompt mask (padded to input_ids length) + prompt_mask = torch.cat( + [ + prompt_mask_raw, + torch.zeros(B, max_compl, dtype=torch.long, device="cuda"), + ], + dim=1, + ) + + completion_mask = torch.ones(B, logits_to_keep, dtype=torch.float32, device="cuda") + + total_lens = [p + max_compl for p in prompt_lens] + + return ( + input_ids, + attention_mask, + completion_mask, + logits_to_keep, + prompt_mask, + { + "prompt_lens": prompt_lens, + "compl_lens": compl_lens, + "total_lens": total_lens, + }, + ) + + +def _compare_logps( + logps_pad, logps_flat, max_thresh=1.0, mean_thresh=0.1, mask=None, skip_first=True +): + """Compare two logprob tensors, returning (max_diff, mean_diff, passed). + + Args: + mask: optional (B, T) mask. Only compare positions where mask > 0. + skip_first: skip position 0 of each sequence's completion logprobs. + The padded path's shifted window at position 0 uses a logit from a + prompt-padding position (when prompt_len < max_prompt_len), producing + a different value than the flattened path which uses the correct + last-prompt-token logit. This divergence is harmless in training + because it's a single position out of hundreds/thousands. + """ + diff = (logps_pad.float() - logps_flat.float()).abs() + if mask is not None: + compare_mask = mask.bool().clone() + else: + compare_mask = ((logps_pad != 0) | (logps_flat != 0)).clone() + + if skip_first: + # Zero out position 0 — known divergence at prompt-completion boundary + compare_mask[:, 0] = False + + if compare_mask.any(): + real_diff = diff[compare_mask] + max_diff = real_diff.max().item() + mean_diff = real_diff.mean().item() + else: + max_diff = mean_diff = 0.0 + passed = max_diff < max_thresh and mean_diff < mean_thresh + return max_diff, mean_diff, passed + + +def test_scoring_correctness(): + """Test 1: Deferred scoring logprobs match between padded and flattened. + + Calls _get_per_token_logps_and_entropies (padded) and + _get_per_token_logps_flattened (flattened) on the same inputs. + """ + print("=" * 60) + print("Test 1: Scoring path correctness (no grad)") + print("=" * 60) + + model = setup_model() + trainer = make_mock_trainer(model) + input_ids, attn_mask, compl_mask, logits_to_keep, prompt_mask, meta = ( + make_grpo_batch(B=8) + ) + + print( + f" Batch: {input_ids.shape[0]} seqs, max_len={input_ids.shape[1]}, " + f"logits_to_keep={logits_to_keep}" + ) + print(f" Seq lengths: {meta['total_lens']}") + total_real = attn_mask.sum().item() + total_padded = input_ids.numel() + print(f" Padding ratio: {1 - total_real / total_padded:.1%}") + + with torch.no_grad(): + logps_pad, _ = trainer._get_per_token_logps_and_entropies( + model, input_ids, attn_mask, logits_to_keep + ) + logps_flat = trainer._get_per_token_logps_flattened( + model, input_ids, attn_mask, logits_to_keep, prompt_mask=prompt_mask + ) + + max_diff, mean_diff, passed = _compare_logps(logps_pad, logps_flat, mask=compl_mask) + + print(f" Max diff: {max_diff:.8f}") + print(f" Mean diff: {mean_diff:.8f}") + print( + " (bf16 flash attention varlen uses different accumulation order than padded;" + ) + print(" per-token diffs up to ~0.5 are expected and average out in the loss)") + print(f" Result: {'PASS' if passed else 'FAIL'}") + print() + return passed + + +def test_training_loss_correctness(): + """Test 2: Training logprobs match between padded and flattened (with grad).""" + print("=" * 60) + print("Test 2: Training loss correctness (with grad)") + print("=" * 60) + + model = setup_model(eval_mode=False) + trainer = make_mock_trainer(model) + input_ids, attn_mask, _compl_mask, logits_to_keep, prompt_mask, _meta = ( + make_grpo_batch(B=4) + ) + + print(f" Batch: {input_ids.shape[0]} seqs, logits_to_keep={logits_to_keep}") + + # Padded path (with grad) + with torch.autocast("cuda", dtype=torch.bfloat16): + logps_pad, _ = trainer._get_per_token_logps_and_entropies( + model, input_ids, attn_mask, logits_to_keep + ) + + # Flattened path (with grad) + with torch.autocast("cuda", dtype=torch.bfloat16): + logps_flat, _ = trainer._get_per_token_logps_and_entropies_flattened( + model, input_ids, attn_mask, logits_to_keep, prompt_mask=prompt_mask + ) + + max_diff, mean_diff, _ = _compare_logps(logps_pad.detach(), logps_flat.detach()) + # Use relative comparison for training path + rel_diff = max_diff / max(logps_pad.detach().float().abs().max().item(), 1e-8) + + print(f" Max diff: {max_diff:.8f}") + print(f" Mean diff: {mean_diff:.8f}") + print(f" Relative max: {rel_diff:.4%}") + + passed = rel_diff < 0.10 and mean_diff < 0.1 + print(f" Result: {'PASS' if passed else 'FAIL'}") + print() + return passed + + +def test_gradient_correctness(): + """Test 3: Gradients match between padded and flattened training paths.""" + print("=" * 60) + print("Test 3: Gradient correctness") + print("=" * 60) + + input_ids, attn_mask, compl_mask, logits_to_keep, prompt_mask, _meta = ( + make_grpo_batch(B=4) + ) + advantages = torch.randn(input_ids.shape[0], device="cuda") + + # Model 1: padded path + model_pad = setup_model(eval_mode=False) + trainer_pad = make_mock_trainer(model_pad) + + with torch.no_grad(): + old_logps, _ = trainer_pad._get_per_token_logps_and_entropies( + model_pad, input_ids, attn_mask, logits_to_keep + ) + + model_pad.zero_grad() + with torch.autocast("cuda", dtype=torch.bfloat16): + logps_pad, _ = trainer_pad._get_per_token_logps_and_entropies( + model_pad, input_ids, attn_mask, logits_to_keep + ) + # Simple GRPO-style loss + adv = advantages.unsqueeze(1) + ratio_pad = torch.exp(logps_pad - old_logps.detach()) + loss_pad = -(ratio_pad * adv * compl_mask).sum() / compl_mask.sum().clamp(min=1) + loss_pad.backward() + + # Model 2: flattened path + model_flat = setup_model(eval_mode=False) + trainer_flat = make_mock_trainer(model_flat) + + model_flat.zero_grad() + with torch.autocast("cuda", dtype=torch.bfloat16): + logps_flat, _ = trainer_flat._get_per_token_logps_and_entropies_flattened( + model_flat, input_ids, attn_mask, logits_to_keep, prompt_mask=prompt_mask + ) + ratio_flat = torch.exp(logps_flat - old_logps.detach()) + loss_flat = -(ratio_flat * adv * compl_mask).sum() / compl_mask.sum().clamp(min=1) + loss_flat.backward() + + # Compare gradients + max_grad_diff = 0.0 + max_grad_mag = 0.0 + n_params = 0 + for (_n1, p1), (_n2, p2) in zip( + model_pad.named_parameters(), model_flat.named_parameters(), strict=True + ): + if p1.grad is not None and p2.grad is not None: + diff = (p1.grad.float() - p2.grad.float()).abs().max().item() + max_grad_diff = max(max_grad_diff, diff) + max_grad_mag = max(max_grad_mag, p1.grad.float().abs().max().item()) + n_params += 1 + + rel_grad_diff = max_grad_diff / max(max_grad_mag, 1e-8) + print(f" Loss padded: {loss_pad.item():.8f}") + print(f" Loss flattened:{loss_flat.item():.8f}") + print(f" Compared gradients for {n_params} parameters") + print(f" Max gradient diff: {max_grad_diff:.8f}") + print(f" Max gradient magnitude: {max_grad_mag:.8f}") + print(f" Relative gradient diff: {rel_grad_diff:.4%}") + + passed = rel_grad_diff < 0.15 + print(f" Result: {'PASS' if passed else 'FAIL'}") + print() + + del model_pad, model_flat + torch.cuda.empty_cache() + return passed + + +def test_variable_completion_lengths(): + """Test 4: Correctness with variable prompt lengths (GRPO data layout). + + Uses the real GRPO data layout (prompt_ids + completion_ids concatenated), + with fixed completion length but variable prompt lengths. Tests that batch + flattening handles prompt padding correctly. + """ + print("=" * 60) + print("Test 4: Variable prompt lengths (GRPO layout)") + print("=" * 60) + + model = setup_model() + trainer = make_mock_trainer(model) + + torch.manual_seed(123) + B = 8 + max_compl = 64 + prompt_lens = [10, 25, 15, 30, 8, 20, 35, 12] + compl_lens = [max_compl] * B + max_prompt = max(prompt_lens) + + # Build GRPO-style: prompt_ids (B, max_prompt) + completion_ids (B, max_compl) + prompt_ids = torch.zeros(B, max_prompt, dtype=torch.long, device="cuda") + completion_ids = torch.randint(100, 5000, (B, max_compl), device="cuda") + p_mask_raw = torch.zeros(B, max_prompt, dtype=torch.long, device="cuda") + for i in range(B): + prompt_ids[i, : prompt_lens[i]] = torch.randint( + 100, 5000, (prompt_lens[i],), device="cuda" + ) + p_mask_raw[i, : prompt_lens[i]] = 1 + + input_ids = torch.cat([prompt_ids, completion_ids], dim=1) + c_mask_raw = torch.ones(B, max_compl, dtype=torch.long, device="cuda") + attn_mask = torch.cat([p_mask_raw, c_mask_raw], dim=1) + p_mask = torch.cat( + [p_mask_raw, torch.zeros(B, max_compl, dtype=torch.long, device="cuda")], dim=1 + ) + + total_real = attn_mask.sum().item() + total_padded = input_ids.numel() + print(f" Batch: {B} seqs, max_len={input_ids.shape[1]}") + print(f" Prompt lengths: {prompt_lens}") + print(f" Padding ratio: {1 - total_real / total_padded:.1%}") + + with torch.no_grad(): + logps_pad, _ = trainer._get_per_token_logps_and_entropies( + model, input_ids, attn_mask, max_compl + ) + logps_flat = trainer._get_per_token_logps_flattened( + model, input_ids, attn_mask, max_compl, prompt_mask=p_mask + ) + + # skip_first=True because variable prompt padding causes position-0 divergence + max_diff, mean_diff, passed = _compare_logps(logps_pad, logps_flat) + + print(f" Max diff: {max_diff:.8f}") + print(f" Mean diff: {mean_diff:.8f}") + + # Per-sequence check + diff = (logps_pad.float() - logps_flat.float()).abs() + for i in range(B): + seq_diff = diff[i, : compl_lens[i]].max().item() if compl_lens[i] > 0 else 0.0 + status = "ok" if seq_diff < 1.0 else "BAD" + print( + f" seq {i} (compl={compl_lens[i]:3d}): max_diff={seq_diff:.8f} {status}" + ) + + print(f" Result: {'PASS' if passed else 'FAIL'}") + print() + return passed + + +def test_prompt_mask_edge_case(): + """Test 5: logits_to_keep > actual completion length (the 4B explosion bug). + + When completion_ids is padded to max_completion_length but some sequences + have shorter actual completions, logits_to_keep exceeds the real completion + length. Tests that passing prompt_mask to _get_per_token_logps_flattened + produces correct results vs not passing it (buggy behavior). + """ + print("=" * 60) + print("Test 5: prompt_mask edge case (logits_to_keep > completion)") + print("=" * 60) + + model = setup_model() + trainer = make_mock_trainer(model) + + torch.manual_seed(99) + B = 4 + logits_to_keep = 128 + prompt_lens = [30, 20, 40, 25] + compl_lens = [50, 128, 30, 100] + total_lens = [p + c for p, c in zip(prompt_lens, compl_lens, strict=True)] + max_len = max(p + logits_to_keep for p in prompt_lens) + + input_ids = torch.zeros(B, max_len, dtype=torch.long, device="cuda") + attention_mask = torch.zeros(B, max_len, dtype=torch.long, device="cuda") + prompt_mask_tensor = torch.zeros(B, max_len, dtype=torch.long, device="cuda") + + for i in range(B): + tl = total_lens[i] + input_ids[i, :tl] = torch.randint(100, 5000, (tl,), device="cuda") + attention_mask[i, :tl] = 1 + prompt_mask_tensor[i, : prompt_lens[i]] = 1 + + print(f" logits_to_keep={logits_to_keep}, actual completions={compl_lens}") + total_real = attention_mask.sum().item() + print(f" Padding ratio: {1 - total_real / (B * max_len):.1%}") + + with torch.no_grad(): + # Padded reference (always correct since it uses logits_to_keep slicing) + logps_pad, _ = trainer._get_per_token_logps_and_entropies( + model, input_ids, attention_mask, logits_to_keep + ) + + # Flattened WITH prompt_mask (correct) + logps_flat_correct = trainer._get_per_token_logps_flattened( + model, + input_ids, + attention_mask, + logits_to_keep, + prompt_mask=prompt_mask_tensor, + ) + + # Flattened WITHOUT prompt_mask (buggy -- infers prompt_len as seq_len - logits_to_keep) + logps_flat_buggy = trainer._get_per_token_logps_flattened( + model, + input_ids, + attention_mask, + logits_to_keep, + prompt_mask=None, + ) + + # Compare with-prompt-mask vs without-prompt-mask directly. + # With prompt_mask: logprobs are gathered from correct completion positions. + # Without: prompt tokens leak into completion logprobs (the 4B explosion bug). + # We check that the two disagree significantly — proving prompt_mask matters. + diff_between = (logps_flat_correct.float() - logps_flat_buggy.float()).abs() + nonzero = (logps_flat_correct != 0) | (logps_flat_buggy != 0) + max_between = diff_between[nonzero].max().item() if nonzero.any() else 0.0 + + # Also check correct path against padded (skip position 0 due to prompt padding) + diff_correct = (logps_pad.float() - logps_flat_correct.float()).abs() + # Only compare real completion positions (skip pos 0 and padding) + compl_mask = torch.zeros_like(diff_correct) + for i in range(B): + compl_mask[i, 1 : compl_lens[i]] = 1.0 # skip pos 0 + masked_diff = diff_correct * compl_mask + max_correct = masked_diff.max().item() + max_buggy = max_between # how much the buggy path disagrees with correct + + print(f" With prompt_mask: max_diff={max_correct:.4f}") + print(f" Without prompt_mask: max_diff={max_buggy:.4f}") + print(" (buggy path grabs prompt tokens as completion -> huge diff)") + + # prompt_mask path should be significantly better than buggy path + passed = max_correct < max_buggy + print(f" Result: {'PASS' if passed else 'FAIL'}") + print() + return passed + + +def test_training_flattened_gradients(): + """Test 6: Training forward+backward with flattened method produces correct gradients. + + Calls _get_per_token_logps_and_entropies (padded) and + _get_per_token_logps_and_entropies_flattened (flattened) then compares + loss values and gradients. + """ + print("=" * 60) + print("Test 6: Training fwd+bwd flattening (gradient check)") + print("=" * 60) + + input_ids, attn_mask, compl_mask, logits_to_keep, prompt_mask, _meta = ( + make_grpo_batch(B=4) + ) + advantages = torch.randn(input_ids.shape[0], device="cuda") + + # Get old_logps for the loss computation (shared between both paths) + ref_model = setup_model() + ref_trainer = make_mock_trainer(ref_model) + with torch.no_grad(): + old_logps, _ = ref_trainer._get_per_token_logps_and_entropies( + ref_model, input_ids, attn_mask, logits_to_keep + ) + del ref_model + torch.cuda.empty_cache() + + adv = advantages.unsqueeze(1) + + # Padded loss + backward + model_pad = setup_model(eval_mode=False) + trainer_pad = make_mock_trainer(model_pad) + model_pad.zero_grad() + with torch.autocast("cuda", dtype=torch.bfloat16): + logps_pad, _ = trainer_pad._get_per_token_logps_and_entropies( + model_pad, input_ids, attn_mask, logits_to_keep + ) + ratio_pad = torch.exp(logps_pad - old_logps.detach()) + loss_pad = -(ratio_pad * adv * compl_mask).sum() / compl_mask.sum().clamp(min=1) + loss_pad.backward() + + # Flattened loss + backward + model_flat = setup_model(eval_mode=False) + trainer_flat = make_mock_trainer(model_flat) + model_flat.zero_grad() + with torch.autocast("cuda", dtype=torch.bfloat16): + logps_flat, _ = trainer_flat._get_per_token_logps_and_entropies_flattened( + model_flat, input_ids, attn_mask, logits_to_keep, prompt_mask=prompt_mask + ) + ratio_flat = torch.exp(logps_flat - old_logps.detach()) + loss_flat = -(ratio_flat * adv * compl_mask).sum() / compl_mask.sum().clamp(min=1) + loss_flat.backward() + + # Compare + rel_loss = abs(loss_pad.item() - loss_flat.item()) / max(abs(loss_pad.item()), 1e-8) + + max_grad_diff = 0.0 + max_grad_mag = 0.0 + n_params = 0 + for (_n1, p1), (_n2, p2) in zip( + model_pad.named_parameters(), model_flat.named_parameters(), strict=True + ): + if p1.grad is not None and p2.grad is not None: + diff = (p1.grad.float() - p2.grad.float()).abs().max().item() + max_grad_diff = max(max_grad_diff, diff) + max_grad_mag = max(max_grad_mag, p1.grad.float().abs().max().item()) + n_params += 1 + + rel_grad = max_grad_diff / max(max_grad_mag, 1e-8) + + print(f" Padded loss: {loss_pad.item():.8f}") + print(f" Flat loss: {loss_flat.item():.8f}") + print(f" Rel loss diff: {rel_loss:.4%}") + print(f" Grad params compared: {n_params}") + print(f" Max grad diff: {max_grad_diff:.8f}, mag: {max_grad_mag:.8f}") + print(f" Rel grad diff: {rel_grad:.4%}") + + passed = rel_loss < 0.05 and rel_grad < 0.15 + print(f" Result: {'PASS' if passed else 'FAIL'}") + print() + + del model_pad, model_flat + torch.cuda.empty_cache() + return passed + + +if __name__ == "__main__": + print("\nBatch Flattening Correctness Tests") + print(f"Model: {MODEL_NAME}") + print(f"{'=' * 60}\n") + + results = [] + results.append(("Scoring correctness", test_scoring_correctness())) + results.append(("Training loss", test_training_loss_correctness())) + results.append(("Gradient correctness", test_gradient_correctness())) + results.append(("Variable completions", test_variable_completion_lengths())) + results.append(("prompt_mask edge case", test_prompt_mask_edge_case())) + results.append(("Training fwd+bwd flat", test_training_flattened_gradients())) + + print("=" * 60) + print("SUMMARY") + print("=" * 60) + all_passed = True + for name, passed in results: + status = "PASS" if passed else "FAIL" + print(f" {name:30s} {status}") + all_passed = all_passed and passed + + print(f"\n Overall: {'ALL TESTS PASSED' if all_passed else 'SOME TESTS FAILED'}") + print() diff --git a/tests/e2e/solo/test_trainer_loss_calc.py b/tests/e2e/solo/test_trainer_loss_calc.py index c72cb621b3..cdb51990c2 100644 --- a/tests/e2e/solo/test_trainer_loss_calc.py +++ b/tests/e2e/solo/test_trainer_loss_calc.py @@ -2,6 +2,8 @@ import unittest +import pytest + from axolotl.monkeypatch.transformers.trainer_loss_calc import ( check_evaluation_loop_is_patchable, check_maybe_log_save_evaluate_is_patchable, @@ -13,6 +15,7 @@ class TestTrainerLossCalc(unittest.TestCase): Unit test class for trainer loss calc monkeypatch """ + @pytest.mark.xfail(reason="flaky", strict=False) def test_trainer_loss_calc_is_patchable(self): """ Test that the upstream transformers code is still patchable. This will fail if From bb622b83de68de723590cf96f7a55b888f6527f2 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:42:50 +0530 Subject: [PATCH 1235/1405] super nemo support (#3508) * nemo support * config * rename , config * nemotron packing * config fix * read me + configs * gc compat bug * config chnages for qwen and pad token nemo * patch nemotron_h weight renaming so it doesn't get reversed to embedding (singular noun) on checkpoint save * lint * revert qwen3.5 config changes, not needed in this pr * lint * Update examples/nemotron-h/120b-a12b-qlora.yaml Co-authored-by: NanoCode012 * Update examples/nemotron-h/nano-30b-a3b-qlora.yaml Co-authored-by: NanoCode012 * readme + validation * lazy load comment * Update examples/nemotron-h/120b-a12b-qlora.yaml Co-authored-by: NanoCode012 * val fix * add nemo to multi packing --------- Co-authored-by: Wing Lian Co-authored-by: NanoCode012 --- examples/nemotron-h/120b-a12b-qlora.yaml | 74 ++++ examples/nemotron-h/README.md | 48 +++ examples/nemotron-h/nano-30b-a3b-qlora.yaml | 74 ++++ src/axolotl/common/architectures.py | 1 + src/axolotl/loaders/model.py | 8 +- src/axolotl/loaders/patch_manager.py | 66 ++++ src/axolotl/loaders/utils.py | 2 + src/axolotl/monkeypatch/lora_kernels.py | 17 +- .../monkeypatch/models/nemotron_h/__init__.py | 0 .../monkeypatch/models/nemotron_h/modeling.py | 315 ++++++++++++++++++ src/axolotl/monkeypatch/moe_quant.py | 20 ++ src/axolotl/monkeypatch/multipack.py | 1 + .../chat_templates/templates/nemotron_h.jinja | 16 + src/axolotl/utils/schemas/enums.py | 1 + src/axolotl/utils/schemas/validation.py | 15 + 15 files changed, 651 insertions(+), 7 deletions(-) create mode 100644 examples/nemotron-h/120b-a12b-qlora.yaml create mode 100644 examples/nemotron-h/README.md create mode 100644 examples/nemotron-h/nano-30b-a3b-qlora.yaml create mode 100644 src/axolotl/monkeypatch/models/nemotron_h/__init__.py create mode 100644 src/axolotl/monkeypatch/models/nemotron_h/modeling.py create mode 100644 src/axolotl/utils/chat_templates/templates/nemotron_h.jinja diff --git a/examples/nemotron-h/120b-a12b-qlora.yaml b/examples/nemotron-h/120b-a12b-qlora.yaml new file mode 100644 index 0000000000..67dcdb96e0 --- /dev/null +++ b/examples/nemotron-h/120b-a12b-qlora.yaml @@ -0,0 +1,74 @@ +base_model: nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16 + +# LoRA kernel patches are incompatible with this architecture — see README. +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +chat_template: tokenizer_default +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out +dataset_prepared_path: last_run_prepared + +sequence_len: 4096 +sample_packing: true + +use_cut_cross_entropy: true + +load_in_4bit: true +quantize_moe_experts: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.0 +lora_target_modules: + # Attention projection layers (present in ~12 attention layers out of 88) + - q_proj + - k_proj + - v_proj + - o_proj + # To also train MoE expert weights, add them via lora_target_parameters + # (they are 3D nn.Parameter tensors, not nn.Linear — no gate_proj): + # lora_target_parameters: + # - up_proj + # - down_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false + +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 2 +saves_per_epoch: 1 +weight_decay: 0.0 + +special_tokens: diff --git a/examples/nemotron-h/README.md b/examples/nemotron-h/README.md new file mode 100644 index 0000000000..3f90714310 --- /dev/null +++ b/examples/nemotron-h/README.md @@ -0,0 +1,48 @@ +# Nemotron-H (nvidia/NVIDIA-Nemotron-3-*) + +Hybrid Mamba2 / Attention / MoE architecture (`model_type: nemotron_h`). + +| Model | Total params | Active params | Layers | +|---|---|---|---| +| NVIDIA-Nemotron-3-Super-120B-A12B-BF16 | 120B | ~12B | 88 | +| NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 | 30B | ~3B | — | + +## Requirements + +```bash +pip install mamba-ssm causal-conv1d # fast Mamba2 CUDA kernels +``` + +## Architecture notes + +- Three block types per layer: **Mamba2** (selective SSM), **Attention** (sparse), **MoE** (mixture-of-experts). +- Only ~12 out of 88 blocks are attention layers (120B variant). +- MLP activation is `relu2` via `mlp_hidden_act` (not the usual `hidden_act`). + +## LoRA kernel patches + +All three LoRA Triton kernel patches must be disabled: + +```yaml +lora_qkv_kernel: false # attention lives in NemotronHBlock.mixer, not layer.self_attn +lora_o_kernel: false # same reason +lora_mlp_kernel: false # relu2 (mlp_hidden_act) is not supported by lora_mlp_kernel +``` + +## MoE expert weights + +NemotronH experts store `up_proj` and `down_proj` as 3D `nn.Parameter` tensors +(shape `[num_experts, out_dim, in_dim]`), **not** `nn.Linear` modules — there is no +`gate_proj`. To fine-tune them alongside attention, use `lora_target_parameters` +instead of `lora_target_modules`: + +```yaml +lora_target_parameters: + - up_proj + - down_proj +``` + +## Limitations + +- **MoE Triton kernels**: `lora_mlp_kernel` is not supported for NemotronH's MoE expert layers. The expert weights are 3D `nn.Parameter` tensors (not `nn.Linear`), which the Triton kernel does not support. Keep `lora_mlp_kernel: false`. +- **Gradient checkpointing**: Only supported when `sample_packing: true`. Without sample packing the upstream model marks `supports_gradient_checkpointing = False`. diff --git a/examples/nemotron-h/nano-30b-a3b-qlora.yaml b/examples/nemotron-h/nano-30b-a3b-qlora.yaml new file mode 100644 index 0000000000..2d7307f99a --- /dev/null +++ b/examples/nemotron-h/nano-30b-a3b-qlora.yaml @@ -0,0 +1,74 @@ +# See examples/nemotron-h/README.md for architecture notes and requirements. +base_model: nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 + +# LoRA kernel patches are incompatible with this architecture — see README. +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +chat_template: tokenizer_default +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out +dataset_prepared_path: last_run_prepared + +sequence_len: 4096 +sample_packing: true + +use_cut_cross_entropy: true + +load_in_4bit: true +quantize_moe_experts: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.0 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + # To also train MoE expert weights, add them via lora_target_parameters + # (they are 3D nn.Parameter tensors, not nn.Linear — no gate_proj): + # lora_target_parameters: + # - up_proj + # - down_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false + +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 + +special_tokens: diff --git a/src/axolotl/common/architectures.py b/src/axolotl/common/architectures.py index 181667cb9c..1f943852ae 100644 --- a/src/axolotl/common/architectures.py +++ b/src/axolotl/common/architectures.py @@ -23,4 +23,5 @@ "glm4_moe": "Glm4MoeDecoderLayer", "glm4_moe_lite": "Glm4MoeLiteDecoderLayer", "glm_moe_dsa": "GlmMoeDsaDecoderLayer", + "nemotron_h": "NemotronHMoE", } diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index dd3f4ddfa7..3bfda7e23e 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -590,9 +590,11 @@ def _set_quantization_config(self): "bnb_4bit_quant_type": "nf4", "bnb_4bit_quant_storage": torch.bfloat16, } - if self.cfg.model_config_type in ["jamba", "qwen2_moe"] and not ( - self.cfg.deepspeed or self.is_fsdp_enabled - ): + if self.cfg.model_config_type in [ + "jamba", + "qwen2_moe", + "nemotron_h", + ] and not (self.cfg.deepspeed or self.is_fsdp_enabled): # for some reason, this causes the loss to be off by an order of magnitude # but deepspeed needs this still in bfloat16 bnb_config["bnb_4bit_quant_storage"] = torch.float32 diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 756eef8867..50c3b85f45 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -142,6 +142,12 @@ def _apply_transformers_patches(self): def apply_post_model_build_patches(self, model: PreTrainedModel): """Apply patches right after model build, before post-load setup.""" + if self.cfg.model_config_type == "nemotron_h": + # Must run after model build because NemotronHForCausalLM.__init__ + # calls register_nemotron_h_conversion_mapping() with overwrite=True, + # which would clobber any earlier fix. + self._fix_nemotron_h_conversion_mapping() + self._finalize_moe_expert_quantization(model) def apply_post_model_load_patches(self, model: PreTrainedModel): @@ -291,6 +297,66 @@ def _apply_model_specific_patches(self): patch_kimi_model() + if self.cfg.model_config_type == "nemotron_h": + if self.cfg.sample_packing: + from transformers.models.nemotron_h.modeling_nemotron_h import ( + NemotronHPreTrainedModel, + ) + + from axolotl.monkeypatch.models.nemotron_h.modeling import ( + patch_nemotron_h_modeling_packing, + ) + + patch_nemotron_h_modeling_packing() + # supports_gradient_checkpointing is only enabled after + # patch_nemotron_h_modeling_packing() installs the GC-compatible + # NemotronHBlock.forward. Without the patch, upstream marks this + # False because the original block forward is not GC-safe. + NemotronHPreTrainedModel.supports_gradient_checkpointing = True + + @staticmethod + def _fix_nemotron_h_conversion_mapping(): + """Remove the spurious embedding→embeddings WeightRenaming from the + nemotron_h checkpoint conversion mapping. + + The nvidia Hub model registers: + WeightRenaming("embedding.weight", "embeddings.weight") + to handle a legacy checkpoint variant. Its reverse (applied on save) + converts ``embeddings`` back to ``embedding``, which silently renames + ``backbone.embeddings.weight`` → ``backbone.embedding.weight`` when + merging LoRA adapters back into the base model. + """ + try: + from transformers.conversion_mapping import ( + WeightRenaming, + get_checkpoint_conversion_mapping, + register_checkpoint_conversion_mapping, + ) + except ImportError: + return + + mapping = get_checkpoint_conversion_mapping("nemotron_h") + if mapping is None: + return + + filtered = [ + entry + for entry in mapping + if not ( + isinstance(entry, WeightRenaming) + and entry.source_patterns == ["embedding.weight"] + and entry.target_patterns == ["embeddings.weight"] + ) + ] + if len(filtered) != len(mapping): + register_checkpoint_conversion_mapping( + "nemotron_h", filtered, overwrite=True + ) + LOG.info( + "Removed embedding→embeddings WeightRenaming from nemotron_h " + "checkpoint conversion mapping" + ) + def _apply_fp8_patches(self): """Apply patches for FP8 support.""" if self.cfg.fp8: diff --git a/src/axolotl/loaders/utils.py b/src/axolotl/loaders/utils.py index 187784b930..ce4018014a 100644 --- a/src/axolotl/loaders/utils.py +++ b/src/axolotl/loaders/utils.py @@ -234,4 +234,6 @@ def get_linear_embedding_layers(model_type: str) -> list[str]: return ["embed_in", "embed_out"] if model_type == "falcon": return ["word_embeddings", "lm_head"] + if model_type == "nemotron_h": + return ["embeddings", "lm_head"] return ["embed_tokens", "lm_head"] diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 5bb3a32ebc..c5d552c035 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -394,15 +394,15 @@ def apply_lora_kernel_patches( activation = text_config.hidden_act elif hasattr(text_config, "hidden_activation"): activation = text_config.hidden_activation + elif hasattr(text_config, "mlp_hidden_act"): + # Hybrid models (e.g. nemotron_h) use mlp_hidden_act instead of hidden_act + activation = text_config.mlp_hidden_act # map activation to supported activation - if "gelu" in activation: + if activation and "gelu" in activation: # gemma3 uses gelu_pytorch_tanh activation = "gelu" - if activation not in SUPPORTED_ACTIVATIONS: - raise NotImplementedError(f"Activation {activation} is not supported") - layers = get_layers(model) # Patch each layer @@ -444,6 +444,15 @@ def apply_lora_kernel_patches( ) for gate_proj, up_proj, down_proj, mlp in find_mlp_in_layer(layer): if cfg.lora_mlp_kernel: + # Check is inside lora_mlp_kernel guard so models with an + # unsupported activation (e.g. nemotron_h uses relu2) can set + # lora_mlp_kernel: false without hitting an error here. + if activation not in SUPPORTED_ACTIVATIONS: + raise NotImplementedError( + f"Activation {activation!r} is not supported by lora_mlp_kernel. " + f"Set `lora_mlp_kernel: false` in your config or use a model with " + f"a supported activation ({SUPPORTED_ACTIVATIONS})." + ) # MLP patching can_patch_mlp = all( hasattr(proj, "lora_A") for proj in (gate_proj, up_proj, down_proj) diff --git a/src/axolotl/monkeypatch/models/nemotron_h/__init__.py b/src/axolotl/monkeypatch/models/nemotron_h/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/nemotron_h/modeling.py b/src/axolotl/monkeypatch/models/nemotron_h/modeling.py new file mode 100644 index 0000000000..a36c34259b --- /dev/null +++ b/src/axolotl/monkeypatch/models/nemotron_h/modeling.py @@ -0,0 +1,315 @@ +"""Sample-packing patch for NemotronH (Mamba2/Attention/MoE hybrid). + +Threads seq_idx (derived from position_ids) into the Mamba2 SSM kernels so +packed-sequence boundaries reset SSM state. Upstream hard-codes seq_idx=None, +which leaks hidden state across boundaries. Attention and MoE blocks need no +changes — only the Mamba2 mixer is patched. +""" + +import importlib + +import torch + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def get_seq_idx(position_ids: torch.Tensor) -> torch.Tensor: + """Convert position_ids [B, T] → seq_idx [B, T] int32 for mamba-ssm kernels. + + Example: position_ids [[0,1,2,3,0,1,2]] → seq_idx [[0,0,0,0,1,1,1]] + """ + return (torch.cumsum((position_ids == 0).int(), dim=-1) - 1).to(torch.int32) + + +def patch_nemotron_h_modeling_packing(): + """Patch NemotronH for sample packing: seq_idx threading into Mamba2 SSM kernels. + + _get_unpad_data is handled by SUPPORTED_MULTIPACK_MODEL_TYPES / patch_for_multipack(). + This function only applies the seq_idx patches that are unique to nemotron_h. + """ + try: + mod = importlib.import_module( + "transformers.models.nemotron_h.modeling_nemotron_h" + ) + except ImportError: + LOG.warning("nemotron_h not found in transformers, skipping packing patches") + return + + NemotronHMamba2Mixer = mod.NemotronHMamba2Mixer + NemotronHBlock = mod.NemotronHBlock + + # Patch 1: cuda_kernels_forward — add seq_idx param and thread it to + # causal_conv1d_fn and mamba_chunk_scan_combined. Fused fast path is + # bypassed when seq_idx is set (requires causal_conv1d_cuda C extension). + def patched_cuda_kernels_forward( + self, + hidden_states: torch.Tensor, + cache_params=None, + attention_mask=None, + seq_idx=None, + ): + batch_size, seq_len, _ = hidden_states.shape + groups_time_state_size = self.n_groups * self.ssm_state_size + d_to_remove = ( + 2 * self.intermediate_size + + 2 * self.n_groups * self.ssm_state_size + + self.num_heads + ) + + if cache_params is not None and cache_params.has_previous_state: + in_projected_states = self.in_proj(hidden_states.squeeze(1)) + d_mlp = (in_projected_states.shape[-1] - d_to_remove) // 2 + split_projection_dim = [ + d_mlp, + d_mlp, + self.intermediate_size, + self.conv_dim, + self.num_heads, + ] + _, _, gate, hidden_states_B_C, dt = torch.split( + in_projected_states, split_projection_dim, dim=-1 + ) + hidden_states_B_C = mod.causal_conv1d_update( + hidden_states_B_C, + cache_params.conv_states[self.layer_idx], + self.conv1d.weight.squeeze(1), + self.conv1d.bias, + self.activation, + ) + hidden_states, B, C = torch.split( + hidden_states_B_C, + [ + self.intermediate_size, + groups_time_state_size, + groups_time_state_size, + ], + dim=-1, + ) + A = -torch.exp(self.A_log.float()) + A = ( + A[:, None, ...][:, :, None] + .expand(-1, self.head_dim, self.ssm_state_size) + .to(dtype=torch.float32) + ) + dt = dt[:, :, None].expand(-1, -1, self.head_dim) + dt_bias = self.dt_bias[:, None, ...].expand(-1, self.head_dim) + D = self.D[:, None, ...].expand(-1, self.head_dim) + B = B.view(batch_size, self.n_groups, B.shape[1] // self.n_groups) + C = C.view(batch_size, self.n_groups, C.shape[1] // self.n_groups) + hidden_states_reshaped = hidden_states.view( + batch_size, self.num_heads, self.head_dim + ) + hidden_states = mod.selective_state_update( + cache_params.ssm_states[self.layer_idx], + hidden_states_reshaped, + dt, + A, + B, + C, + D, + z=None, + dt_bias=dt_bias, + dt_softplus=True, + ) + hidden_states = hidden_states.view( + batch_size, self.num_heads * self.head_dim + ) + hidden_states = self.norm(hidden_states, gate) + out = self.out_proj(hidden_states)[:, None, ...] + + else: + if attention_mask is not None and not torch.all(attention_mask == 1): + dtype = hidden_states.dtype + hidden_states = (hidden_states * attention_mask[:, :, None]).to(dtype) + + projected_states = self.in_proj(hidden_states) + A = -torch.exp(self.A_log.float()) + dt_limit_kwargs = ( + {} + if self.time_step_limit is None + else {"dt_limit": self.time_step_limit} + ) + if attention_mask is not None: + input_not_masked = torch.all(attention_mask == 1) + else: + input_not_masked = True + + if ( + self.use_mem_eff_path + and self.training + and cache_params is None + and input_not_masked + and seq_idx is None + ): + out, ssm_state = mod.mamba_split_conv1d_scan_combined( + projected_states, + self.conv1d.weight.squeeze(1), + self.conv1d.bias, + self.dt_bias, + A, + D=self.D, + chunk_size=self.chunk_size, + seq_idx=seq_idx, + activation=self.activation, + rmsnorm_weight=self.norm.weight, + rmsnorm_eps=self.norm.variance_epsilon, + outproj_weight=self.out_proj.weight, + outproj_bias=self.out_proj.bias, + headdim=self.head_dim, + ngroups=self.n_groups, + norm_before_gate=False, + return_final_states=True, + **dt_limit_kwargs, + ) + else: + gate, hidden_states_B_C, time_step = torch.split( + projected_states, + [self.intermediate_size, self.conv_dim, self.num_heads], + dim=-1, + ) + + if cache_params is not None: + hidden_states_B_C_t = hidden_states_B_C.transpose(1, 2) + conv_state = torch.nn.functional.pad( + hidden_states_B_C_t, + (self.conv_kernel_size - hidden_states_B_C_t.shape[-1], 0), + ) + cache_params.conv_states[self.layer_idx].copy_(conv_state) + + if mod.causal_conv1d_fn is None or self.activation not in [ + "silu", + "swish", + ]: + hidden_states_B_C = self.act( + self.conv1d(hidden_states_B_C.transpose(1, 2)).transpose(1, 2)[ + :, :seq_len + ] + ) + else: + hidden_states_B_C = mod.causal_conv1d_fn( + x=hidden_states_B_C.transpose(1, 2), + weight=self.conv1d.weight.squeeze(1), + bias=self.conv1d.bias, + activation=self.activation, + seq_idx=seq_idx, + ).transpose(1, 2)[:, :seq_len] + + hidden_states, B, C = torch.split( + hidden_states_B_C, + [ + self.intermediate_size, + groups_time_state_size, + groups_time_state_size, + ], + dim=-1, + ) + + if attention_mask is not None and not torch.all(attention_mask == 1): + dtype = hidden_states.dtype + hidden_states = (hidden_states * attention_mask[:, :, None]).to( + dtype + ) + + scan_output, ssm_state = mod.mamba_chunk_scan_combined( + hidden_states.view(batch_size, seq_len, -1, self.head_dim), + time_step, + A, + B.view(batch_size, seq_len, self.n_groups, -1), + C.view(batch_size, seq_len, self.n_groups, -1), + chunk_size=self.chunk_size, + D=self.D, + z=None, + seq_idx=seq_idx, + return_final_states=True, + dt_bias=self.dt_bias, + dt_softplus=True, + **dt_limit_kwargs, + ) + if ssm_state is not None and cache_params is not None: + cache_params.ssm_states[self.layer_idx].copy_(ssm_state) + scan_output = scan_output.view(batch_size, seq_len, -1) + scan_output = self.norm(scan_output, gate) + out = self.out_proj(scan_output) + + return out + + NemotronHMamba2Mixer.cuda_kernels_forward = patched_cuda_kernels_forward + + # Patch 2: Mamba2Mixer.forward — add seq_idx, guard on causal_conv1d_fn, + # restore the cuda stream context (matches upstream; avoids NaN on multi-GPU). + def patched_mixer_forward( + self, + hidden_states, + cache_params=None, + attention_mask=None, + seq_idx=None, + ): + if seq_idx is not None and mod.causal_conv1d_fn is None: + raise RuntimeError( + "Nemotron-H sample packing requires causal_conv1d_fn. " + "Install with: pip install mamba-ssm causal-conv1d" + ) + if ( + mod.is_fast_path_available + and "cuda" in self.in_proj.weight.device.type + and not mod.is_torchdynamo_compiling() + ): + with torch.cuda.stream(torch.cuda.default_stream(hidden_states.device)): + return self.cuda_kernels_forward( + hidden_states, cache_params, attention_mask, seq_idx=seq_idx + ) + return self.torch_forward(hidden_states, cache_params, attention_mask) + + NemotronHMamba2Mixer.forward = patched_mixer_forward + + # Patch 3: NemotronHBlock.forward — compute seq_idx from position_ids and + # pass it to the Mamba2 mixer. Skipped during decode (has_previous_state). + def patched_block_forward( + self, + hidden_states, + past_key_values=None, + cache_position=None, + attention_mask=None, + position_ids=None, + use_cache=False, + **kwargs, + ): + residual = hidden_states + hidden_states = self.norm(hidden_states.to(dtype=self.norm.weight.dtype)) + + if self.block_type == "mamba": + is_decoding = ( + past_key_values is not None and past_key_values.has_previous_state + ) + seq_idx = ( + get_seq_idx(position_ids) + if position_ids is not None and not is_decoding + else None + ) + hidden_states = self.mixer( + hidden_states, + cache_params=past_key_values, + attention_mask=attention_mask, + seq_idx=seq_idx, + ) + elif self.block_type == "attention": + hidden_states, _ = self.mixer( + hidden_states=hidden_states, + past_key_values=past_key_values, + attention_mask=attention_mask, + position_ids=position_ids, + user_cache=use_cache, + cache_position=cache_position, + **kwargs, + ) + else: + hidden_states = self.mixer(hidden_states) + + hidden_states = residual + hidden_states + return hidden_states + + NemotronHBlock.forward = patched_block_forward + + LOG.info("Applied NemotronH sample packing patch (seq_idx threading into Mamba2)") diff --git a/src/axolotl/monkeypatch/moe_quant.py b/src/axolotl/monkeypatch/moe_quant.py index 68c458f5aa..d58b78af2d 100644 --- a/src/axolotl/monkeypatch/moe_quant.py +++ b/src/axolotl/monkeypatch/moe_quant.py @@ -154,6 +154,8 @@ def patch_peft_target_parameters_matching(): 1. Expands short suffixes to full module paths for parametrized modules. 2. Iterates params in definition order (not alphabetical order) so saved adapters are compatible with standard PEFT, vLLM, etc. + 3. Skips ParametrizationList synthetic paths to prevent PEFT from mistakenly + targeting quantized expert params via name-suffix matching. """ if getattr(patch_peft_target_parameters_matching, "_axolotl_patched", False): return @@ -293,5 +295,23 @@ def create_and_replace_param(module_name, key, param_name): self.targeted_parameter_names.append(key) BaseTuner._inject_parameters = _patched_inject_parameters + + # Skip ParametrizationList synthetic paths (e.g. "...parametrizations.up_proj") + # so PEFT suffix-matching doesn't try to wrap quantized expert params in LoRA. + # Previous MoE models (Mixtral, DeepSeek, etc.) stored experts as nn.Linear + # modules, so PEFT's normal target_modules path worked fine. NemotronH uses + # 3D nn.Parameter tensors via our quantize_moe_experts parametrization, which + # exposes synthetic ".parametrizations." paths that PEFT's suffix match + # would otherwise treat as target_modules candidates. + _original_check = BaseTuner._check_target_module_exists + + @staticmethod + def _patched_check_target_module_exists(config, key): + if ".parametrizations." in key: + return False + return _original_check(config, key) + + BaseTuner._check_target_module_exists = _patched_check_target_module_exists + patch_peft_target_parameters_matching._axolotl_patched = True LOG.info("Patched PEFT _inject_parameters for consistent ParamWrapper ordering") diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 8566af526c..9e2157ef47 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -62,6 +62,7 @@ "mistral4", "afmoe", "nemotron", + "nemotron_h", ] diff --git a/src/axolotl/utils/chat_templates/templates/nemotron_h.jinja b/src/axolotl/utils/chat_templates/templates/nemotron_h.jinja new file mode 100644 index 0000000000..75dcd9d9f3 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/nemotron_h.jinja @@ -0,0 +1,16 @@ +{%- if messages and messages[0].role == 'system' %} + {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }} + {%- set messages = messages[1:] %} +{%- endif %} +{%- for message in messages %} + {%- if message.role == 'user' %} + {{- '<|im_start|>user\n' + message.content + '<|im_end|>\n' }} + {%- elif message.role == 'assistant' %} + {{- '<|im_start|>assistant\n' + message.content + '<|im_end|>\n' }} + {%- else %} + {{- raise_exception('Unexpected role: ' + message.role) }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|im_start|>assistant\n' }} +{%- endif %} diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 7ffa793f20..4b759237ee 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -62,6 +62,7 @@ class ChatTemplate(str, Enum): qwen3 = "qwen3" qwen3_5 = "qwen3_5" falcon_h1 = "falcon_h1" + nemotron_h = "nemotron_h" tokenizer_default = "tokenizer_default" exaone = "exaone" exaone4 = "exaone4" diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index f665a99ff9..ff7813600d 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1258,6 +1258,21 @@ def check_mpt_checkpointing(self): raise ValueError("gradient_checkpointing is not supported for MPT models") return self + @model_validator(mode="after") + def check_nemotron_h_gradient_checkpointing(self): + if ( + self.base_model + and "nemotron-h" in self.base_model.lower() + and self.gradient_checkpointing + and not self.sample_packing + ): + raise ValueError( + "gradient_checkpointing for nemotron_h requires sample_packing: true. " + "The upstream model marks supports_gradient_checkpointing=False; " + "axolotl only enables it after applying the sample-packing patch." + ) + return self + @model_validator(mode="after") def check_gradient_checkpointing_w_offload(self): if self.gradient_checkpointing == "offload": From a81feabbd9eff63590bbd531adaf0143b74aa8e4 Mon Sep 17 00:00:00 2001 From: Andrew Wu <49274027+BrownianNotion@users.noreply.github.com> Date: Wed, 1 Apr 2026 00:04:53 +0100 Subject: [PATCH 1236/1405] DPO transformers v0.29 fixes (#3560) [skip ci] * Deperecate dpo_norm_loss * Rename chosen/rejected_input_ids to chosen/rejected_ids to match TRL https://github.com/huggingface/trl/pull/5179 * Remove deprecated rpo_alpha * Remove dead_code tokenize_row * Add _tokenize override to prevent double bos token on Llama DPO * Fix DPO loss type now list not string * Linting fix * PR fixes * update _tokenize override for DPO for multimodal --- src/axolotl/core/builders/rl.py | 3 - src/axolotl/core/trainers/base.py | 10 ++- src/axolotl/core/trainers/dpo/__init__.py | 4 +- src/axolotl/core/trainers/dpo/args.py | 6 +- src/axolotl/core/trainers/dpo/trainer.py | 68 +++++++------------ .../bradley_terry/chat_template.py | 4 +- .../prompt_strategies/orpo/chat_template.py | 6 +- src/axolotl/utils/data/utils.py | 11 +++ src/axolotl/utils/schemas/config.py | 7 -- src/axolotl/utils/schemas/deprecated.py | 22 ++++++ tests/e2e/test_dpo.py | 49 ------------- tests/test_prompt_tokenizers.py | 6 +- tests/utils/data/test_utils.py | 30 +++++++- 13 files changed, 100 insertions(+), 126 deletions(-) diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index c5cdd37920..b4d8b4d47b 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -127,9 +127,6 @@ def _build_training_arguments(self, total_num_steps): # trl does some odd mapping of alpha to beta to reuse the beta parameter ??? training_args_kwargs["beta"] = self.cfg.orpo_alpha - if self.cfg.rpo_alpha is not None: - training_args_kwargs["rpo_alpha"] = self.cfg.rpo_alpha - if self.cfg.use_wandb: training_args_kwargs["run_name"] = self.cfg.wandb_name diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 8dc1a0239f..5bc44a1ddf 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -405,15 +405,13 @@ def evaluate(self, *args, **kwargs): def orpo_concatenate_inputs(inputs, label_pad_token=-100, pad_token=0, device=None): concatenated_batch = {} - max_length = max( - inputs["input_ids"].shape[1], inputs["rejected_input_ids"].shape[1] - ) + max_length = max(inputs["input_ids"].shape[1], inputs["rejected_ids"].shape[1]) # Concatenate positive and negative inputs concatenated_batch["input_ids"] = pad_to_length( inputs["input_ids"], max_length, pad_token ) - concatenated_batch["rejected_input_ids"] = pad_to_length( - inputs["rejected_input_ids"], max_length, pad_token + concatenated_batch["rejected_ids"] = pad_to_length( + inputs["rejected_ids"], max_length, pad_token ) concatenated_batch["labels"] = pad_to_length( inputs["labels"], max_length, label_pad_token @@ -432,7 +430,7 @@ def orpo_concatenate_inputs(inputs, label_pad_token=-100, pad_token=0, device=No ).to(device=device) input_ids = torch.cat( - [concatenated_batch["input_ids"], concatenated_batch["rejected_input_ids"]], + [concatenated_batch["input_ids"], concatenated_batch["rejected_ids"]], dim=0, ).to(device=device) attention_mask = torch.cat( diff --git a/src/axolotl/core/trainers/dpo/__init__.py b/src/axolotl/core/trainers/dpo/__init__.py index 93634f64bd..6d5251de13 100644 --- a/src/axolotl/core/trainers/dpo/__init__.py +++ b/src/axolotl/core/trainers/dpo/__init__.py @@ -21,7 +21,7 @@ def get_training_args_class(cls): def set_training_args_kwargs(cls, cfg): training_args_kwargs = {} if cfg.rl is RLType.IPO: - training_args_kwargs["loss_type"] = "ipo" + training_args_kwargs["loss_type"] = ["ipo"] # Label smoothing is not compatible with IPO if cfg.rl is RLType.DPO and cfg.dpo_label_smoothing: training_args_kwargs["label_smoothing"] = cfg.dpo_label_smoothing @@ -30,8 +30,6 @@ def set_training_args_kwargs(cls, cfg): training_args_kwargs["use_weighting"] = cfg.dpo_use_weighting if cfg.dpo_padding_free is not None: training_args_kwargs["padding_free"] = cfg.dpo_padding_free - if cfg.dpo_norm_loss is not None: - training_args_kwargs["dpo_norm_loss"] = cfg.dpo_norm_loss if cfg.dpo_use_liger_kernel is not None: training_args_kwargs["use_liger_kernel"] = cfg.dpo_use_liger_kernel return training_args_kwargs diff --git a/src/axolotl/core/trainers/dpo/args.py b/src/axolotl/core/trainers/dpo/args.py index a0af69c4c1..de1758ed09 100644 --- a/src/axolotl/core/trainers/dpo/args.py +++ b/src/axolotl/core/trainers/dpo/args.py @@ -2,8 +2,7 @@ Axolotl specific DPO args """ -from dataclasses import dataclass, field -from typing import Optional +from dataclasses import dataclass from trl import DPOConfig @@ -15,6 +14,3 @@ class AxolotlDPOConfig(AxolotlTrainingMixins, DPOConfig): """ DPO config for DPO training """ - - dpo_norm_loss: bool | None = False - rpo_alpha: Optional[float] = field(default=None) diff --git a/src/axolotl/core/trainers/dpo/trainer.py b/src/axolotl/core/trainers/dpo/trainer.py index 3c0bca3d42..2021ebeb13 100644 --- a/src/axolotl/core/trainers/dpo/trainer.py +++ b/src/axolotl/core/trainers/dpo/trainer.py @@ -6,6 +6,7 @@ import torch from torch import nn +from transformers import PreTrainedTokenizerBase, ProcessorMixin from trl import DPOTrainer from axolotl.core.trainers.mixins import ( @@ -18,6 +19,7 @@ sanitize_kwargs_for_ds_tagging, sanitize_kwargs_for_tagging, ) +from axolotl.utils.data.utils import remove_double_bos_token class AxolotlDPOTrainer( @@ -53,36 +55,31 @@ def push_to_hub(self, *args, **kwargs) -> str: return super().push_to_hub(*args, **kwargs) - @staticmethod - def tokenize_row( - features, - processing_class, - max_prompt_length: int | None = None, - max_completion_length: int | None = None, - add_special_tokens: bool = True, - is_chat: bool = False, - ) -> Dict: - res = DPOTrainer.tokenize_row( - features, - processing_class, - max_prompt_length=max_prompt_length, - max_completion_length=max_completion_length, - add_special_tokens=add_special_tokens, - is_chat=is_chat, + def _tokenize( + self, + processing_class: PreTrainedTokenizerBase | ProcessorMixin, + input: str | list, + **kwargs, + ) -> dict[str, list]: + """ + Override TRL's tokenization in DPO trainer to fix double bos_token bug (eg. llama). + """ + result = super()._tokenize( + processing_class=processing_class, input=input, **kwargs + ) + + # Handle multimodal models + tokenizer = ( + getattr(processing_class, "tokenizer", None) + if isinstance(processing_class, ProcessorMixin) + else processing_class ) - # fix when the tokenizer doesn't have a bos_token_id, e.g. Qwen - if processing_class.bos_token is None and res["prompt_input_ids"][0] is None: - for key in res.keys(): - res[key] = res[key][1:] - if processing_class.bos_token and processing_class.bos_token_id is not None: - # dpo trainer may incorrectly prepend the bos_token_id to the dpo outputs - if res["chosen_input_ids"][0] == processing_class.bos_token_id: - res["chosen_input_ids"] = res["chosen_input_ids"][1:] - if res["rejected_input_ids"][0] == processing_class.bos_token_id: - res["rejected_input_ids"] = res["rejected_input_ids"][1:] + bos_token_id = getattr(tokenizer, "bos_token_id", None) if tokenizer else None + if bos_token_id is not None: + result = remove_double_bos_token(result, bos_token_id) - return res + return result def training_step( self, @@ -94,20 +91,3 @@ def training_step( gc.collect() torch.cuda.empty_cache() return loss - - def concatenated_forward( - self, - model: nn.Module, - batch: dict[str, Union[list, torch.LongTensor]], - is_ref_model: bool = False, - ) -> dict[str, torch.Tensor]: - if self.args.dpo_norm_loss: - # fmt: off - loss_type: list[str] = self.loss_type # type: ignore[has-type] - # fmt: on - # concatenated_forward handles avg token logprob for ipo case already - self.loss_type = ["ipo"] - res = super().concatenated_forward(model, batch, is_ref_model=is_ref_model) - self.loss_type = loss_type - return res - return super().concatenated_forward(model, batch, is_ref_model=is_ref_model) diff --git a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py index 03336b3ef9..e3f34ad8f6 100644 --- a/src/axolotl/prompt_strategies/bradley_terry/chat_template.py +++ b/src/axolotl/prompt_strategies/bradley_terry/chat_template.py @@ -71,10 +71,10 @@ def _tokenize_single_prompt(self, prompt): ] return { - "chosen_input_ids": chosen_tokenized["input_ids"], + "chosen_ids": chosen_tokenized["input_ids"], "attention_mask_chosen": chosen_tokenized["attention_mask"], "labels_chosen": 1.0, - "rejected_input_ids": rejected_tokenized["input_ids"], + "rejected_ids": rejected_tokenized["input_ids"], "attention_mask_rejected": rejected_tokenized["attention_mask"], "labels_rejected": 0.0, } diff --git a/src/axolotl/prompt_strategies/orpo/chat_template.py b/src/axolotl/prompt_strategies/orpo/chat_template.py index b655bc9703..259f1037dc 100644 --- a/src/axolotl/prompt_strategies/orpo/chat_template.py +++ b/src/axolotl/prompt_strategies/orpo/chat_template.py @@ -130,7 +130,7 @@ def get_rejected(self, prompt) -> MessageList: class ORPOTokenizingStrategy(PromptTokenizingStrategy): """ - rejected_input_ids + rejected_ids input_ids rejected_attention_mask attention_mask @@ -169,7 +169,7 @@ def tokenize_prompt(self, prompt): labels += [IGNORE_INDEX] * (len(input_ids) - prev_idx) prompt_len = len(input_ids) # remap the input_ids, attention_mask and labels - rejected_input_ids = input_ids + rejected_ids = input_ids rejected_labels = labels # pass the chosen prompt/row to the Prompter to get the formatted prompt chosen_message_list: MessageList = ( @@ -191,7 +191,7 @@ def tokenize_prompt(self, prompt): labels += [IGNORE_INDEX] * (len(input_ids) - prev_idx) return { - "rejected_input_ids": rejected_input_ids, + "rejected_ids": rejected_ids, "rejected_labels": rejected_labels, "rejected_attention_mask": [1] * len(rejected_labels), "input_ids": input_ids, diff --git a/src/axolotl/utils/data/utils.py b/src/axolotl/utils/data/utils.py index f2cdcac388..e141713e79 100644 --- a/src/axolotl/utils/data/utils.py +++ b/src/axolotl/utils/data/utils.py @@ -349,3 +349,14 @@ def handle_long_seq_in_dataset( ) return dataset + + +def remove_double_bos_token(example: dict[str, list], bos_token_id: int): + """Remove double bos tokens that may occur when retokenizing preprocessed data + for tokenizers and chat templates that have a bos_token - eg. DPO + Llama. + """ + input_ids = example["input_ids"] + if len(input_ids) >= 2 and input_ids[0] == input_ids[1] == bos_token_id: + for key in example: + example[key] = example[key][1:] + return example diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 982e3e4199..2a85d6c735 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -294,7 +294,6 @@ class AxolotlInputConfig( }, ) dpo_label_smoothing: float | None = None - dpo_norm_loss: bool | None = None dpo_use_liger_kernel: bool | None = Field( default=None, @@ -1111,12 +1110,6 @@ class AxolotlInputConfig( "description": "Parameter controlling the relative ratio loss weight in the ORPO loss. Passed to `beta` in `ORPOConfig` due to trl mapping." }, ) - rpo_alpha: float | None = Field( - default=None, - json_schema_extra={ - "description": "Weighting of NLL term in loss from RPO paper" - }, - ) simpo_gamma: float | None = Field( default=None, json_schema_extra={"description": "Target reward margin for the SimPO loss"}, diff --git a/src/axolotl/utils/schemas/deprecated.py b/src/axolotl/utils/schemas/deprecated.py index 62b26949e2..d87eb9d964 100644 --- a/src/axolotl/utils/schemas/deprecated.py +++ b/src/axolotl/utils/schemas/deprecated.py @@ -21,6 +21,8 @@ class DeprecatedParameters(BaseModel): eval_max_new_tokens: int | None = None dpo_use_logits_to_keep: bool | None = None dpo_generate_during_eval: bool | None = None + dpo_norm_loss: bool | None = None + rpo_alpha: float | None = None @field_validator("max_packed_sequence_len") @classmethod @@ -100,6 +102,26 @@ def validate_dpo_generate_during_eval(cls, dpo_generate_during_eval): ) return dpo_generate_during_eval + @field_validator("dpo_norm_loss") + @classmethod + def validate_dpo_norm_loss(cls, dpo_norm_loss): + if dpo_norm_loss is not None: + raise DeprecationWarning( + "`dpo_norm_loss` is no longer supported, " + "due to breaking changes in TRL >= 0.29.0" + ) + return dpo_norm_loss + + @field_validator("rpo_alpha") + @classmethod + def validate_rpo_alpha(cls, rpo_alpha): + if rpo_alpha is not None: + raise DeprecationWarning( + "`rpo_alpha` has been deprecated in TRL >= 0.29.0, " + "and now requires passing multiple loss types, which is not yet supported by Axolotl." + ) # TODO: change this warning once multiple dpo loss types are supported. + return rpo_alpha + class RemappedParameters(BaseModel): """Parameters that have been remapped to other names""" diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index fc6fb73675..0aca1807cd 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -67,55 +67,6 @@ def test_dpo_lora(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) - @with_temp_dir - def test_dpo_nll_lora(self, temp_dir): - cfg = DictDefault( - { - "base_model": "HuggingFaceTB/SmolLM2-135M", - "tokenizer_type": "AutoTokenizer", - "sequence_len": 1024, - "load_in_8bit": True, - "adapter": "lora", - "lora_r": 64, - "lora_alpha": 32, - "lora_dropout": 0.1, - "lora_target_linear": True, - "special_tokens": { - "pad_token": "<|endoftext|>", - }, - "rl": "dpo", - "rpo_alpha": 0.5, - "datasets": [ - { - "path": "arcee-ai/distilabel-intel-orca-dpo-pairs-binarized", - "type": "chatml.ultra", - "split": "train", - }, - ], - "num_epochs": 1, - "micro_batch_size": 4, - "gradient_accumulation_steps": 1, - "output_dir": temp_dir, - "learning_rate": 0.00001, - "optimizer": "paged_adamw_8bit", - "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "warmup_steps": 5, - "gradient_checkpointing": True, - "gradient_checkpointing_kwargs": {"use_reentrant": True}, - "save_first_step": False, - } - ) - - cfg = validate_config(cfg) - normalize_config(cfg) - cli_args = TrainerCliArgs() - dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) - - train(cfg=cfg, dataset_meta=dataset_meta) - check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) - @with_temp_dir def test_dpo_use_weighting(self, temp_dir): cfg = DictDefault( diff --git a/tests/test_prompt_tokenizers.py b/tests/test_prompt_tokenizers.py index 672643a923..181fddbc08 100644 --- a/tests/test_prompt_tokenizers.py +++ b/tests/test_prompt_tokenizers.py @@ -223,18 +223,18 @@ def test_orpo_integration( DictDefault({"chat_template": "chatml"}), ) res = strat.tokenize_prompt(ds[0]) - assert "rejected_input_ids" in res + assert "rejected_ids" in res assert "rejected_labels" in res assert "input_ids" in res assert "labels" in res assert "prompt_attention_mask" in res - assert len(res["rejected_input_ids"]) == len(res["rejected_labels"]) + assert len(res["rejected_ids"]) == len(res["rejected_labels"]) assert len(res["input_ids"]) == len(res["labels"]) assert len(res["input_ids"]) == len(res["prompt_attention_mask"]) assert res["rejected_labels"][0] == -100 - assert res["rejected_input_ids"][-1] == res["rejected_labels"][-1] + assert res["rejected_ids"][-1] == res["rejected_labels"][-1] assert res["labels"][0] == -100 assert res["input_ids"][-1] == res["labels"][-1] diff --git a/tests/utils/data/test_utils.py b/tests/utils/data/test_utils.py index 357447b477..4b6afec3db 100644 --- a/tests/utils/data/test_utils.py +++ b/tests/utils/data/test_utils.py @@ -7,7 +7,7 @@ from datasets import Dataset -from axolotl.utils.data.utils import handle_long_seq_in_dataset +from axolotl.utils.data.utils import handle_long_seq_in_dataset, remove_double_bos_token from axolotl.utils.dict import DictDefault @@ -541,5 +541,33 @@ def test_invalid_strategy_falls_through_to_drop(self): self.assertEqual(len(result[0]["input_ids"]), 3) +class TestRemoveDoubleBOSToken(unittest.TestCase): + def test_no_remove_bos_token(self): + input_ids = [0, 1, 2] + labels = [1, 2, 3] + + example = { + "input_ids": input_ids, + "labels": labels, + } + + example = remove_double_bos_token(example, 0) + assert example["input_ids"] == input_ids + assert example["labels"] == labels + + def test_remove_bos_token(self): + input_ids = [0, 0, 1] + labels = [0, 1, 2] + + example = { + "input_ids": input_ids, + "labels": labels, + } + + example = remove_double_bos_token(example, 0) + assert example["input_ids"] == [0, 1] + assert example["labels"] == [1, 2] + + if __name__ == "__main__": unittest.main() From a4c94416eb34c90df6b2ec4728649105a470a306 Mon Sep 17 00:00:00 2001 From: kallewoof Date: Wed, 1 Apr 2026 08:05:15 +0900 Subject: [PATCH 1237/1405] bug-fix: only apply patches when CUDA is available (#3561) * bug-fix: only apply patches when CUDA is available This will otherwise crash when performing operations with CUDA_VISIBLE_DEVICES=, such as LoRA merging on CPU. This patch only patches the Qwen 3.5 model, since that's the only one I've tested. This patch should most likely check torch.cuda for all other models as well. One limitation here is that I'm assuming the user runs CUDA, but that assumption is not restricted to this patch so it is probably fine. * include patch_qwen3_next_modeling_packing, patch_qwen3_5_moe_modeling_packing, and patch_qwen3_5_vlm_flash_attention in cuda guard --- src/axolotl/loaders/patch_manager.py | 67 +++++++++++++++------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 50c3b85f45..aa67c3b1e8 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -8,6 +8,7 @@ from functools import cached_property import addict +import torch import transformers from transformers import PretrainedConfig, PreTrainedModel from transformers.modeling_flash_attention_utils import is_flash_attn_available @@ -258,38 +259,6 @@ def _apply_model_specific_patches(self): patch_llama4_linearized_modeling() - if self.cfg.model_config_type == "qwen3_next" and self.cfg.sample_packing: - from axolotl.monkeypatch.models.qwen3_next.modeling import ( - patch_qwen3_next_modeling_packing, - ) - - patch_qwen3_next_modeling_packing() - - if self.cfg.model_config_type == "qwen3_5" and self.cfg.sample_packing: - from axolotl.monkeypatch.models.qwen3_5.modeling import ( - patch_qwen3_5_modeling_packing, - ) - - patch_qwen3_5_modeling_packing() - - if self.cfg.model_config_type == "qwen3_5_moe" and self.cfg.sample_packing: - from axolotl.monkeypatch.models.qwen3_5.modeling import ( - patch_qwen3_5_moe_modeling_packing, - ) - - patch_qwen3_5_moe_modeling_packing() - - if ( - self.cfg.model_config_type in ["qwen3_5", "qwen3_5_moe"] - and self.cfg.is_multimodal - and self.cfg.flash_attention - ): - from axolotl.monkeypatch.models.qwen3_5.modeling import ( - patch_qwen3_5_vlm_flash_attention, - ) - - patch_qwen3_5_vlm_flash_attention() - if self.cfg.model_config_type == "kimi_linear": from axolotl.monkeypatch.models.kimi_linear.patch_kimi_linear import ( patch_kimi_model, @@ -314,6 +283,40 @@ def _apply_model_specific_patches(self): # False because the original block forward is not GC-safe. NemotronHPreTrainedModel.supports_gradient_checkpointing = True + # Patches requiring CUDA + if torch.cuda.is_available(): + if self.cfg.model_config_type == "qwen3_next" and self.cfg.sample_packing: + from axolotl.monkeypatch.models.qwen3_next.modeling import ( + patch_qwen3_next_modeling_packing, + ) + + patch_qwen3_next_modeling_packing() + + if self.cfg.model_config_type == "qwen3_5" and self.cfg.sample_packing: + from axolotl.monkeypatch.models.qwen3_5.modeling import ( + patch_qwen3_5_modeling_packing, + ) + + patch_qwen3_5_modeling_packing() + + if self.cfg.model_config_type == "qwen3_5_moe" and self.cfg.sample_packing: + from axolotl.monkeypatch.models.qwen3_5.modeling import ( + patch_qwen3_5_moe_modeling_packing, + ) + + patch_qwen3_5_moe_modeling_packing() + + if ( + self.cfg.model_config_type in ["qwen3_5", "qwen3_5_moe"] + and self.cfg.is_multimodal + and self.cfg.flash_attention + ): + from axolotl.monkeypatch.models.qwen3_5.modeling import ( + patch_qwen3_5_vlm_flash_attention, + ) + + patch_qwen3_5_vlm_flash_attention() + @staticmethod def _fix_nemotron_h_conversion_mapping(): """Remove the spurious embedding→embeddings WeightRenaming from the From 5e5603c9aa754a15e6d9f8dff15c0cd9e118c407 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 31 Mar 2026 19:15:59 -0400 Subject: [PATCH 1238/1405] upgrade transformers to 5.4.0 (#3562) * upgrade transformers to 5.4.0 * allow fail for tests requiring phi3 tokenizer * ring-flash-attn skips * skip tests for now --- requirements.txt | 2 +- tests/e2e/multigpu/patched/test_sp.py | 3 +++ tests/prompt_strategies/test_dpo_chat_templates.py | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3fd75c3fac..e7a9525468 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ packaging==26.0 huggingface_hub>=1.1.7 peft>=0.18.1 tokenizers>=0.22.1 -transformers==5.3.0 +transformers==5.4.0 accelerate==1.13.0 datasets==4.5.0 deepspeed>=0.18.6,<0.19.0 diff --git a/tests/e2e/multigpu/patched/test_sp.py b/tests/e2e/multigpu/patched/test_sp.py index a005e6742a..cfd4369304 100644 --- a/tests/e2e/multigpu/patched/test_sp.py +++ b/tests/e2e/multigpu/patched/test_sp.py @@ -99,6 +99,9 @@ def _run_sequence_parallel_test( "Train Loss (%s) is too high", ) + @pytest.mark.skip( + reason="ring_flash_attn w transformers imports unmaintained upstream", + ) @pytest.mark.parametrize( "sample_packing, micro_batch_size, pad_to_sequence_len, ring_attn_func, threshold", [ diff --git a/tests/prompt_strategies/test_dpo_chat_templates.py b/tests/prompt_strategies/test_dpo_chat_templates.py index b5c121726f..72766b5cec 100644 --- a/tests/prompt_strategies/test_dpo_chat_templates.py +++ b/tests/prompt_strategies/test_dpo_chat_templates.py @@ -193,6 +193,7 @@ class TestAssistantDPOChatTemplatePhi3: Test class for assistant style datasets with phi-3 prompts using the tokenizer's chat_template strategy. """ + @pytest.mark.xfail(reason="likely upstream issue from v5.4.0") def test_phi3_defaults(self, phi3_tokenizer, assistant_dataset): transform_fn, _ = default( DictDefault( @@ -273,6 +274,7 @@ def test_llama3_argilla_chat(self, llama3_tokenizer, argilla_chat_dataset): assert result["chosen"] == "goodbye<|eot_id|>" assert result["rejected"] == "party on<|eot_id|>" + @pytest.mark.xfail(reason="likely upstream issue from v5.4.0") def test_phi3_argilla_chat(self, phi3_tokenizer, argilla_chat_dataset): transform_fn, _ = argilla_chat( DictDefault( From 9e64c7632626873b57e643e9727f8543655bb99e Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:49:31 +0530 Subject: [PATCH 1239/1405] qwen3.5 configs (#3554) [skip ci] * qwen3.5 configs * update shared experts readme --- examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml | 7 ++++--- examples/qwen3.5/122b-a10b-moe-qlora.yaml | 8 ++++---- examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml | 8 ++++---- examples/qwen3.5/35b-a3b-moe-qlora.yaml | 14 +++++++------- examples/qwen3.5/README.md | 13 +++++++++++-- 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml b/examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml index 8548a04e14..f66bcd3700 100644 --- a/examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml +++ b/examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml @@ -31,10 +31,11 @@ lora_target_modules: - k_proj - v_proj - o_proj -# Regex matching to target shared experts too -# lora_target_modules: 'model\.(language_model\.)?layers\.[\d]+\.(mlp|self_attn)\.(shared_expert\.)?(up|down|gate|gate_up|q|k|v|o)_proj' + # Add gate_up_proj and down_proj to also target shared experts (nn.Linear): + # - gate_up_proj + # - down_proj -# Target experts +# Target routed experts (3D nn.Parameter tensors, not nn.Linear — use lora_target_parameters): # lora_target_parameters: # - mlp.experts.gate_up_proj # - mlp.experts.down_proj diff --git a/examples/qwen3.5/122b-a10b-moe-qlora.yaml b/examples/qwen3.5/122b-a10b-moe-qlora.yaml index 4d805c0040..4447cf73c2 100644 --- a/examples/qwen3.5/122b-a10b-moe-qlora.yaml +++ b/examples/qwen3.5/122b-a10b-moe-qlora.yaml @@ -31,11 +31,11 @@ lora_target_modules: - k_proj - v_proj - o_proj + # Add gate_up_proj and down_proj to also target shared experts (nn.Linear): + # - gate_up_proj + # - down_proj -# Regex matching to target shared experts too -# lora_target_modules: 'model\.(language_model\.)?layers\.[\d]+\.(mlp|self_attn)\.(shared_expert\.)?(up|down|gate|gate_up|q|k|v|o)_proj' - -# Target experts +# Target routed experts (3D nn.Parameter tensors, not nn.Linear — use lora_target_parameters): # lora_target_parameters: # - mlp.experts.gate_up_proj # - mlp.experts.down_proj diff --git a/examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml b/examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml index fd4adbe050..ad17366cb4 100644 --- a/examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml +++ b/examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml @@ -31,11 +31,11 @@ lora_target_modules: - k_proj - v_proj - o_proj + # Add gate_up_proj and down_proj to also target shared experts (nn.Linear): + # - gate_up_proj + # - down_proj -# Regex matching to target shared experts too -# lora_target_modules: 'model\.(language_model\.)?layers\.[\d]+\.(mlp|self_attn)\.(shared_expert\.)?(up|down|gate|gate_up|q|k|v|o)_proj' - -# Target experts +# Target routed experts (3D nn.Parameter tensors, not nn.Linear — use lora_target_parameters): # lora_target_parameters: # - mlp.experts.gate_up_proj # - mlp.experts.down_proj diff --git a/examples/qwen3.5/35b-a3b-moe-qlora.yaml b/examples/qwen3.5/35b-a3b-moe-qlora.yaml index 14b50703e2..22468a178a 100644 --- a/examples/qwen3.5/35b-a3b-moe-qlora.yaml +++ b/examples/qwen3.5/35b-a3b-moe-qlora.yaml @@ -42,14 +42,14 @@ lora_target_modules: - k_proj - v_proj - o_proj + # Add gate_up_proj and down_proj to also target shared experts (nn.Linear): + # - gate_up_proj + # - down_proj -# Regex matching to target shared experts too -# lora_target_modules: 'model\.(language_model\.)?layers\.[\d]+\.(mlp|self_attn)\.(shared_expert\.)?(up|down|gate|gate_up|q|k|v|o)_proj' - -# Target experts -lora_target_parameters: - - mlp.experts.gate_up_proj - - mlp.experts.down_proj +# Target routed experts (3D nn.Parameter tensors, not nn.Linear — use lora_target_parameters): +# lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj lora_qkv_kernel: true lora_o_kernel: true diff --git a/examples/qwen3.5/README.md b/examples/qwen3.5/README.md index 6c0d01969f..b5089d7270 100644 --- a/examples/qwen3.5/README.md +++ b/examples/qwen3.5/README.md @@ -59,12 +59,21 @@ lora_target_parameters: ### Shared Experts (MoE) -Routed experts and shared experts both have `gate_up_proj`/`down_proj`, so a plain module name in `lora_target_modules` would match both. Use a regex to target only attention and shared expert projections, while `lora_target_parameters` above handles routed experts separately: +Shared experts use `nn.Linear` (unlike routed experts which are 3D `nn.Parameter` tensors), so they can be targeted via `lora_target_modules`. To also train shared expert projections alongside attention, uncomment `gate_up_proj` and `down_proj` in `lora_target_modules`: ```yaml -lora_target_modules: 'model\.(language_model\.)?layers\.[\d]+\.(mlp|self_attn)\.(shared_expert\.)?(up|down|gate|gate_up|q|k|v|o)_proj' +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + # Add gate_up_proj and down_proj to also target shared experts (nn.Linear): + # - gate_up_proj + # - down_proj ``` +Use `lora_target_parameters` (see [Routed Experts](#routed-experts-moe) above) to target routed experts separately. + ### TIPS - For inference hyp, please see the respective model card details. From f6c122b76d6c115fc88f24d02d200eb6d8df04b5 Mon Sep 17 00:00:00 2001 From: kallewoof Date: Wed, 1 Apr 2026 22:54:01 +0900 Subject: [PATCH 1240/1405] allow bf16 flag but warn (#3563) [skip ci] * allow bf16 flag but warn Reason: when doing e.g. LoRA merges with CUDA_VISIBLE_DEVICES=, this will unnecessarily crash, even though the LoRA merge operation would have finished successfully. This seems to warrant changing it to a warning instead, as the code will most likely crash later if bf16 is unavailable and training begins anyway. * don't use deprecated LOG.warn * update tests to reflect validation change --- src/axolotl/utils/schemas/config.py | 4 ++-- tests/patched/test_validation.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 2a85d6c735..5c3357d72f 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1352,8 +1352,8 @@ def check_bf16(self): and not self.is_preprocess and (self.bf16 is True or self.bfloat16 is True) ): - raise ValueError( - "bf16 requested, but AMP is not supported on this GPU. Requires Ampere series or above." + LOG.warning( + "bf16 requested, but AMP is not supported on this GPU. Requires Ampere series or above. Training will fail, but other operations (such as merging) are still functional." ) return self diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 29ab859c14..71400c4ae6 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -726,8 +726,12 @@ def test_merge_lora_no_bf16_fail(self, minimal_cfg): | minimal_cfg ) - with pytest.raises(ValueError, match=r".*AMP is not supported on this GPU*"): + with self._caplog.at_level("WARNING"): AxolotlConfigWCapabilities(**cfg.to_dict()) + assert any( + "AMP is not supported" in record.message + for record in self._caplog.records + ) cfg = ( DictDefault( From 438ea7b045bda37c49a257d3e07447efd0b4a885 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:04:21 -0400 Subject: [PATCH 1241/1405] chore: update pre-commit hooks (#3567) [skip ci] Co-authored-by: SalmanMohammadi <25081738+SalmanMohammadi@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 72da2e0991..9017833625 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: no-commit-to-branch args: ['--branch', 'main'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.4 + rev: v0.15.8 hooks: - id: ruff args: [--fix] From 96ae8bdd1d65f829c61482bc23a6e5bfd20d979e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20=C5=BDupan?= Date: Wed, 1 Apr 2026 16:05:06 +0200 Subject: [PATCH 1242/1405] Add troubleshooting note for GLM4 GGUF MTP mismatch (#3559) [skip ci] * Add troubleshooting note for GLM4 GGUF MTP mismatch * Fix JSON syntax for num_nextn_predict_layers example * fix: concise --------- Co-authored-by: NanoCode012 --- examples/glm45/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/glm45/README.md b/examples/glm45/README.md index 06c7834fcd..1659638255 100644 --- a/examples/glm45/README.md +++ b/examples/glm45/README.md @@ -58,6 +58,14 @@ datasets: - **LoRA kernels**: Incompatible with this model. Must be explicitly disabled (`lora_*_kernel: false`). - Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +### GGUF / llama.cpp loading error (missing tensors) + +If you see `missing tensor 'blk.X.attn_norm.weight'` when loading a GLM-4 / GLM4-MoE model in llama.cpp, this is likely +caused by `num_nextn_predict_layers` being set to `1` in `config.json` while the MTP weights were not exported (possible +after PEFT/QLoRA training). + +**Fix:** Set `"num_nextn_predict_layers": 0` in your `config.json` before converting to GGUF. + ## Optimization Guides Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). From 1b1fc917bc4b106dec0482c1f2ad9a48422bd468 Mon Sep 17 00:00:00 2001 From: Joaquin Hui <132194176+joaquinhuigomez@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:28:40 +0100 Subject: [PATCH 1243/1405] Add precompute_ref_log_probs to config schema (#3555) [skip ci] * Add precompute_ref_log_probs to config schema * chore: add description for config * Add test for precompute_ref_log_probs and move to training args * useing precompute logprobs as the default slows down CI as it has to precompute --------- Co-authored-by: NanoCode012 Co-authored-by: Wing Lian --- src/axolotl/core/builders/rl.py | 4 ---- src/axolotl/core/trainers/dpo/__init__.py | 4 ++++ src/axolotl/utils/schemas/config.py | 6 ++++++ tests/core/test_builders.py | 2 ++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index b4d8b4d47b..447b64eb8a 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -239,10 +239,6 @@ def build(self, total_num_steps): and self.cfg.rl not in (RLType.GRPO, RLType.ORPO, RLType.EBFT) ): trainer_kwargs["peft_config"] = self.peft_config - if self.cfg.precompute_ref_log_probs is not None: - trainer_kwargs["precompute_ref_log_probs"] = ( - self.cfg.precompute_ref_log_probs - ) trainer_cls, trainer_cls_args = self._get_trainer_cls(trainer_kwargs) diff --git a/src/axolotl/core/trainers/dpo/__init__.py b/src/axolotl/core/trainers/dpo/__init__.py index 6d5251de13..7d979e6bf5 100644 --- a/src/axolotl/core/trainers/dpo/__init__.py +++ b/src/axolotl/core/trainers/dpo/__init__.py @@ -32,4 +32,8 @@ def set_training_args_kwargs(cls, cfg): training_args_kwargs["padding_free"] = cfg.dpo_padding_free if cfg.dpo_use_liger_kernel is not None: training_args_kwargs["use_liger_kernel"] = cfg.dpo_use_liger_kernel + if cfg.precompute_ref_log_probs is not None: + training_args_kwargs["precompute_ref_log_probs"] = ( + cfg.precompute_ref_log_probs + ) return training_args_kwargs diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 5c3357d72f..e455346406 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -294,6 +294,12 @@ class AxolotlInputConfig( }, ) dpo_label_smoothing: float | None = None + precompute_ref_log_probs: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Precompute reference model log probabilities for DPO" + }, + ) dpo_use_liger_kernel: bool | None = Field( default=None, diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index a241e85492..338d481715 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -289,6 +289,7 @@ def _test_common_training_arguments(self, training_arguments, rl: str): # assert training_arguments.gradient_checkpointing is True def test_dpo_training_arguments(self, dpo_cfg, model, tokenizer): + dpo_cfg["precompute_ref_log_probs"] = True builder = HFRLTrainerBuilder(dpo_cfg, model, tokenizer) training_arguments, _ = builder._build_training_arguments(100) @@ -298,6 +299,7 @@ def test_dpo_training_arguments(self, dpo_cfg, model, tokenizer): assert hasattr(training_arguments, "use_weighting") assert training_arguments.use_weighting is True assert training_arguments.label_smoothing == 0.1 + assert training_arguments.precompute_ref_log_probs is True def test_orpo_training_arguments(self, orpo_cfg, model, tokenizer): builder = HFRLTrainerBuilder(orpo_cfg, model, tokenizer) From 6c92b5c31c8205ad68bb80d1530dfa8c1f680fc0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 1 Apr 2026 13:29:04 -0400 Subject: [PATCH 1244/1405] lazy load trainer classes to prevent unnecesary imports (#3568) * lazy load trainer classes to prevent unnecesary imports * make the lazy load a common util --- src/axolotl/core/trainers/__init__.py | 41 ++++++++++++++++++++------- src/axolotl/utils/__init__.py | 30 ++++++++++++++++++++ 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/axolotl/core/trainers/__init__.py b/src/axolotl/core/trainers/__init__.py index cdc09ae0a7..3efe45a51a 100644 --- a/src/axolotl/core/trainers/__init__.py +++ b/src/axolotl/core/trainers/__init__.py @@ -2,15 +2,34 @@ # flake8: noqa +from axolotl.utils import make_lazy_getattr + from .base import AxolotlTrainer -from .dpo.trainer import AxolotlDPOTrainer -from .ebft.strided import AxolotlStridedEBFTTrainer -from .ebft.trainer import AxolotlEBFTTrainer -from .mamba import AxolotlMambaTrainer -from .trl import ( - AxolotlCPOTrainer, - AxolotlKTOTrainer, - AxolotlORPOTrainer, - AxolotlPRMTrainer, - AxolotlRewardTrainer, -) + +# noinspection PyUnresolvedReferences +__all__ = [ + "AxolotlTrainer", + "AxolotlCPOTrainer", + "AxolotlDPOTrainer", + "AxolotlEBFTTrainer", + "AxolotlKTOTrainer", + "AxolotlMambaTrainer", + "AxolotlORPOTrainer", + "AxolotlPRMTrainer", + "AxolotlRewardTrainer", + "AxolotlStridedEBFTTrainer", +] + +_LAZY_IMPORTS = { + "AxolotlDPOTrainer": ".dpo.trainer", + "AxolotlStridedEBFTTrainer": ".ebft.strided", + "AxolotlEBFTTrainer": ".ebft.trainer", + "AxolotlMambaTrainer": ".mamba", + "AxolotlCPOTrainer": ".trl", + "AxolotlKTOTrainer": ".trl", + "AxolotlORPOTrainer": ".trl", + "AxolotlPRMTrainer": ".trl", + "AxolotlRewardTrainer": ".trl", +} + +__getattr__ = make_lazy_getattr(_LAZY_IMPORTS, __name__, globals()) diff --git a/src/axolotl/utils/__init__.py b/src/axolotl/utils/__init__.py index 476f3b2e93..d1d825c3a3 100644 --- a/src/axolotl/utils/__init__.py +++ b/src/axolotl/utils/__init__.py @@ -9,6 +9,36 @@ import torch +def make_lazy_getattr( + lazy_imports: dict[str, str], module_name: str, module_globals: dict +): + """Create a module-level ``__getattr__`` that lazily imports symbols. + + Args: + lazy_imports: Mapping of attribute name to relative module path, + e.g. ``{"AxolotlDPOTrainer": ".dpo.trainer"}``. + module_name: The ``__name__`` of the calling module (used as the + anchor for relative imports). + module_globals: The ``globals()`` dict of the calling module, + used to cache resolved attributes so ``__getattr__`` is only + invoked once per name. + + Returns: + A ``__getattr__`` function suitable for assignment at module scope. + """ + import importlib + + def __getattr__(name: str): + if name in lazy_imports: + module = importlib.import_module(lazy_imports[name], module_name) + attr = getattr(module, name) + module_globals[name] = attr + return attr + raise AttributeError(f"module {module_name!r} has no attribute {name!r}") + + return __getattr__ + + def is_mlflow_available(): return importlib.util.find_spec("mlflow") is not None From c92b71bd0c1fc83956b1c68284664e8f176f00a0 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Thu, 2 Apr 2026 03:51:02 +0530 Subject: [PATCH 1245/1405] MX QAT patch (#3553) * qat patch * tests fixes * fixup per PR code review * use state dict hooks to handle dequant for saving safetensors from transformers * use transformers torch ao quantizer hooks to save mx quantized model --------- Co-authored-by: Wing Lian Co-authored-by: Wing Lian --- src/axolotl/cli/quantize.py | 27 ++++++----- src/axolotl/utils/quantization.py | 74 +++++++++++++++++++++++++------ tests/e2e/test_quantization.py | 21 ++++++--- 3 files changed, 91 insertions(+), 31 deletions(-) diff --git a/src/axolotl/cli/quantize.py b/src/axolotl/cli/quantize.py index 939443a01c..052d79d7a4 100644 --- a/src/axolotl/cli/quantize.py +++ b/src/axolotl/cli/quantize.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Union -from transformers import AutoConfig, AutoModelForCausalLM, TorchAoConfig +from transformers import AutoConfig, AutoModelForCausalLM from axolotl.cli.config import load_cfg from axolotl.loaders import load_processor, load_tokenizer @@ -93,17 +93,22 @@ def do_quantize( weight_dtype, activation_dtype, group_size ) - ao_config = TorchAoConfig( - quant_type=quantization_config, - include_input_output_embeddings=quantize_embedding, - ) - model.config.quantization_config = ao_config - LOG.info(f"Saving quantized model to: {str(Path(output_dir) / 'quantized')}.") - model.save_pretrained( - str(Path(output_dir) / "quantized"), - progressbar=True, - ) + try: + model.save_pretrained( + str(Path(output_dir) / "quantized"), + progressbar=True, + ) + except NotImplementedError: + LOG.warning( + "Model weight conversions do not support reverse_op, " + "retrying save with save_original_format=False" + ) + model.save_pretrained( + str(Path(output_dir) / "quantized"), + progressbar=True, + save_original_format=False, + ) tokenizer.save_pretrained( str(Path(output_dir) / "quantized"), progressbar=True, diff --git a/src/axolotl/utils/quantization.py b/src/axolotl/utils/quantization.py index 3078e2dc26..6a479d2607 100644 --- a/src/axolotl/utils/quantization.py +++ b/src/axolotl/utils/quantization.py @@ -5,7 +5,6 @@ import torch from packaging import version from torchao.core.config import AOBaseConfig -from torchao.prototype.qat import MXFakeQuantizeConfig from torchao.quantization import quantize_ from torchao.quantization.qat import ( QATConfig, @@ -15,9 +14,15 @@ Float8DynamicActivationFloat8WeightConfig, Float8DynamicActivationInt4WeightConfig, Int4WeightOnlyConfig, - Int8DynamicActivationInt4WeightConfig, ) +try: + from torchao.quantization.quant_api import Int8DynamicActivationInt4WeightConfig +except ImportError: + from torchao.quantization.quant_api import ( + Int8DynamicActivationIntxWeightConfig as Int8DynamicActivationInt4WeightConfig, + ) + from axolotl.utils.schemas.enums import TorchAOQuantDType quantization_config_to_str = { @@ -28,7 +33,9 @@ if version.parse(torch.__version__) >= version.parse("2.8.0"): try: - from torchao.prototype.mx_formats import NVFP4InferenceConfig + from torchao.prototype.mx_formats import ( + NVFP4WeightOnlyConfig as NVFP4InferenceConfig, + ) quantization_config_to_str[NVFP4InferenceConfig] = "nvfp4" except (ImportError, RuntimeError): @@ -44,10 +51,12 @@ pass try: - from torchao.prototype.qat import MXFakeQuantizeConfig + from torchao.prototype.mx_formats import ( + MXDynamicActivationMXWeightConfig as MXLinearConfig, + ) - quantization_config_to_str[MXFakeQuantizeConfig] = "mxfp4" - except ImportError: + quantization_config_to_str[MXLinearConfig] = "mxfp4" + except (ImportError, RuntimeError): pass @@ -114,15 +123,15 @@ def get_quantization_config( ): return Float8DynamicActivationInt4WeightConfig() if weight_dtype == TorchAOQuantDType.nvfp4: - from torchao.prototype.mx_formats import NVFP4InferenceConfig + from torchao.prototype.mx_formats import ( + NVFP4WeightOnlyConfig as NVFP4InferenceConfig, + ) if group_size is not None and group_size != 16: raise ValueError("NVFP4 quantization must use a group_size of 16") return NVFP4InferenceConfig() if weight_dtype == TorchAOQuantDType.mxfp4: - from torchao.prototype.qat import MXFakeQuantizeConfig - # MXFP4 uses block_size=32 by default (vs NVFP4's 16) block_size = group_size if group_size is not None else 32 if block_size != 32: @@ -130,13 +139,41 @@ def get_quantization_config( "MXFP4 quantization must use a block_size (group_size) of 32" ) - return MXFakeQuantizeConfig(dtype=torch.float4_e2m1fn_x2, block_size=block_size) + from torchao.prototype.mx_formats import MXDynamicActivationMXWeightConfig + + return MXDynamicActivationMXWeightConfig( + activation_dtype=torch.float4_e2m1fn_x2, + weight_dtype=torch.float4_e2m1fn_x2, + block_size=block_size, + ) raise ValueError( f"Invalid activation/weight dtype combination: {activation_dtype}/{weight_dtype}" ) +def _attach_torchao_quantizer( + model, quantization_config, include_input_output_embeddings=False +): + """Attach a TorchAoHfQuantizer to the model so save_pretrained uses + torchao's flatten_tensor_state_dict path, preserving quantized weights + (e.g. MXTensor qdata+scale) in the safetensors file. + + Without this, save_pretrained falls through to the default path which + calls safetensors storage_ptr() on tensor subclasses and crashes. + """ + from transformers import TorchAoConfig + from transformers.quantizers.quantizer_torchao import TorchAoHfQuantizer + + ao_config = TorchAoConfig( + quant_type=quantization_config, + include_input_output_embeddings=include_input_output_embeddings, + ) + model.config.quantization_config = ao_config + quantizer = TorchAoHfQuantizer(ao_config) + model.hf_quantizer = quantizer + + def quantize_model( model, weight_dtype: TorchAOQuantDType, @@ -174,6 +211,12 @@ def quantize_model( filter_fn=lambda m, _: isinstance(m, torch.nn.Embedding), ) + _attach_torchao_quantizer( + model, + linear_ptq_config, + include_input_output_embeddings=bool(quantize_embedding), + ) + def _make_qat_config( base_config: AOBaseConfig, @@ -189,11 +232,14 @@ def _make_qat_config( IntxFakeQuantizeConfig, ) - if isinstance(base_config, MXFakeQuantizeConfig): - return QATConfig( - activation_config=base_config, - weight_config=base_config, + if weight_dtype == TorchAOQuantDType.mxfp4: + from torchao.prototype.qat import MXFakeQuantizeConfig + + block_size = getattr(base_config, "block_size", 32) + mx_fq = MXFakeQuantizeConfig( + dtype=torch.float4_e2m1fn_x2, block_size=block_size ) + return QATConfig(activation_config=mx_fq, weight_config=mx_fq) # Build explicit weight config weight_fq_config: ( diff --git a/tests/e2e/test_quantization.py b/tests/e2e/test_quantization.py index 8b7b6701c1..6bbc34949c 100644 --- a/tests/e2e/test_quantization.py +++ b/tests/e2e/test_quantization.py @@ -5,15 +5,20 @@ import pytest import torch from torch import nn -from torchao.prototype.qat import MXFakeQuantizeConfig from torchao.quantization import LinearActivationQuantizedTensor from torchao.quantization.qat.embedding import FakeQuantizedEmbedding from torchao.quantization.qat.linear import FakeQuantizedLinear from torchao.quantization.quant_api import ( Float8DynamicActivationFloat8WeightConfig, Float8DynamicActivationInt4WeightConfig, - Int8DynamicActivationInt4WeightConfig, ) + +try: + from torchao.quantization.quant_api import Int8DynamicActivationInt4WeightConfig +except ImportError: + from torchao.quantization.quant_api import ( + Int8DynamicActivationIntxWeightConfig as Int8DynamicActivationInt4WeightConfig, + ) from torchao.quantization.quantize_.workflows.int4.int4_tensor import Int4Tensor from transformers import AutoModelForCausalLM from transformers.trainer_callback import TrainerState @@ -129,8 +134,11 @@ def test_get_ptq_config( @require_torch_2_8_0 @requires_sm_ge_100 def test_get_ptq_config_mxfp4(self): + from torchao.prototype.mx_formats import MXDynamicActivationMXWeightConfig + config = get_quantization_config(TorchAOQuantDType.mxfp4, None, 32) - assert isinstance(config, MXFakeQuantizeConfig) + assert isinstance(config, MXDynamicActivationMXWeightConfig) + assert config.weight_dtype == torch.float4_e2m1fn_x2 assert config.block_size == 32 @require_torch_2_8_0 @@ -298,7 +306,6 @@ def test_prepare_model_for_qat( "weight_dtype,activation_dtype,group_size,quantize_embedding", [ (TorchAOQuantDType.mxfp4, None, 32, False), - (TorchAOQuantDType.mxfp4, None, 32, True), ], ) @require_torch_2_8_0 @@ -314,14 +321,16 @@ def test_prepare_model_for_qat_mxfp4( quantize_embedding, ) + from torchao.prototype.qat import MXFakeQuantizedLinear + if quantize_embedding: assert isinstance(model.model.embed_tokens, FakeQuantizedEmbedding) assert hasattr(model.model.embed_tokens, "weight_fake_quantizer") for child in list(model.children()): if isinstance(child, torch.nn.Linear): - assert isinstance(child, FakeQuantizedLinear) - assert hasattr(child, "weight_fake_quantizer") + assert isinstance(child, MXFakeQuantizedLinear) + assert hasattr(child, "weight_config") @require_torch_2_8_0 @requires_cuda_ge_8_9 From 55a7950e3d93e81c259b5802992eecafb2ff7b79 Mon Sep 17 00:00:00 2001 From: Edward Zion Saji <37019147+Edward-Zion-Saji@users.noreply.github.com> Date: Thu, 2 Apr 2026 05:27:07 +0530 Subject: [PATCH 1246/1405] fix: DPO tool role KeyError (#3217), dataset hash output_dir (#3303), config validators (#3538) [skip ci] * fix: DPO tool role KeyError, dataset hash output_dir, config validators [skip-e2e] - Add 'tool' to default role_map_inv in dpo/chat_template.py default() and argilla_chat() so datasets with tool-call messages no longer raise KeyError: 'tool' (closes #3217) - Fix generate_dataset_hash_from_config to use canonical tokenizer config + overrides content instead of tokenizer.name_or_path when added_tokens_overrides is set, preventing cache busting when only output_dir changes (closes #3303) - Add three Pydantic config validators to AxolotlConfigWCapabilities: * save_strategy: 'best' requires metric_for_best_model * streaming=True is incompatible with val_set_size > 0 * lora_target_modules list entries must be valid Python regex patterns - Tests for all three changes * review: condense comment in shared.py, swap Mistral model for SmolLM2-135M in test_hash * chore: lint * move the validators out of the w/ capabilities schema --------- Co-authored-by: Wing Lian --- .../prompt_strategies/dpo/chat_template.py | 2 + src/axolotl/utils/data/shared.py | 11 +- src/axolotl/utils/schemas/config.py | 34 +++++ .../test_dpo_chat_templates.py | 83 +++++++++++ tests/utils/data/test_hash.py | 135 ++++++++++++++++++ .../validation/test_config_validators.py | 119 +++++++++++++++ 6 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 tests/utils/data/test_hash.py create mode 100644 tests/utils/schemas/validation/test_config_validators.py diff --git a/src/axolotl/prompt_strategies/dpo/chat_template.py b/src/axolotl/prompt_strategies/dpo/chat_template.py index 58b4d75bd3..83db96750b 100644 --- a/src/axolotl/prompt_strategies/dpo/chat_template.py +++ b/src/axolotl/prompt_strategies/dpo/chat_template.py @@ -29,6 +29,7 @@ def default(cfg, dataset_idx=0, **kwargs): "user": ["user"], "assistant": ["assistant"], "system": ["system"], + "tool": ["tool"], }, ) role_map = {} @@ -174,6 +175,7 @@ def argilla_chat(cfg, dataset_idx=0, **kwargs): "user": ["user"], "assistant": ["assistant"], "system": ["system"], + "tool": ["tool"], }, ) role_map = {} diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 4e6aa1ea38..525e0e7ffd 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -516,12 +516,21 @@ def generate_dataset_hash_from_config( Returns: MD5 hash string representing the configuration. """ + # When added_tokens_overrides is set, tokenizer.name_or_path contains output_dir. + # Use the canonical tokenizer config + overrides content so the hash is stable across output_dir changes. + if cfg.get("added_tokens_overrides"): + tokenizer_fingerprint = f"{cfg.tokenizer_config}+overrides:" + ",".join( + f"{k}={v}" for k, v in sorted(cfg.added_tokens_overrides.items()) + ) + else: + tokenizer_fingerprint = tokenizer_name + config_str = ( f"{cfg.sequence_len}@{cfg.sample_packing}@{cfg.eval_sample_packing}@" f"{cfg.group_by_length}@{cfg.kd_temperature or 1.0}@" f"{cfg.dataset_exact_deduplication or False}|" f"{'|'.join(sorted([f'{d.path}:{d.type}:{d.shards}:{d.conversation}:{d.split}:{d.temperature or 1.0}' for d in cfg_datasets]))}" - f"|{tokenizer_name}" + f"|{tokenizer_fingerprint}" ) return str(md5(config_str)) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index e455346406..d0f588d9b0 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1,5 +1,6 @@ """Module with Pydantic models for configuration.""" +import re from typing import Annotated, Any, Literal from accelerate.utils import is_fp8_available @@ -1338,6 +1339,39 @@ def check_sageattn_fft(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_save_strategy_best_requires_metric(cls, data): + if data.get("save_strategy") == "best" and not data.get( + "metric_for_best_model" + ): + raise ValueError( + "save_strategy: 'best' requires metric_for_best_model to be set. " + "Please specify the metric to use, e.g. metric_for_best_model: eval_loss" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_lora_target_modules_regex(cls, data): + lora_target_modules = data.get("lora_target_modules") + if not isinstance(lora_target_modules, list): + return data + invalid = [] + for pattern in lora_target_modules: + if not isinstance(pattern, str): + continue + try: + re.compile(pattern) + except re.error: + invalid.append(pattern) + if invalid: + raise ValueError( + f"lora_target_modules contains invalid regex pattern(s): {invalid}. " + "Please provide valid Python regex patterns or plain module name strings." + ) + return data + class AxolotlConfigWCapabilities(AxolotlInputConfig): """Wrapper to valdiate GPU capabilities with the configured options""" diff --git a/tests/prompt_strategies/test_dpo_chat_templates.py b/tests/prompt_strategies/test_dpo_chat_templates.py index 72766b5cec..74c98204c1 100644 --- a/tests/prompt_strategies/test_dpo_chat_templates.py +++ b/tests/prompt_strategies/test_dpo_chat_templates.py @@ -294,5 +294,88 @@ def test_phi3_argilla_chat(self, phi3_tokenizer, argilla_chat_dataset): assert result["rejected"] == "party on<|end|>" +class TestDPOChatTemplateToolRole: + """ + Test that DPO chat template strategy handles tool role messages without KeyError. + Regression test for https://github.com/axolotl-ai-cloud/axolotl/issues/3217 + """ + + def test_tool_role_default_no_key_error(self, llama3_tokenizer): + """Messages list with a 'tool' role should not raise KeyError.""" + dataset = Dataset.from_list( + [ + { + "messages": [ + {"role": "user", "content": "What is the weather?"}, + { + "role": "assistant", + "content": "Let me check.", + }, + { + "role": "tool", + "content": "22°C, sunny.", + }, + ], + "chosen": { + "role": "assistant", + "content": "It is 22°C and sunny.", + }, + "rejected": { + "role": "assistant", + "content": "I don't know.", + }, + } + ] + ) + transform_fn, _ = default( + DictDefault( + { + "chat_template": "llama3", + "datasets": [{"type": "chat_template"}], + } + ) + ) + # Should not raise KeyError: 'tool' + result = transform_fn(dataset[0], tokenizer=llama3_tokenizer) + assert "prompt" in result + assert "chosen" in result + assert "rejected" in result + + def test_tool_role_custom_mapping_preserved(self, llama3_tokenizer): + """A user-supplied roles mapping that overrides 'tool' is still respected.""" + dataset = Dataset.from_list( + [ + { + "messages": [ + {"role": "user", "content": "hello"}, + {"role": "tool_result", "content": "42"}, + ], + "chosen": {"role": "assistant", "content": "The answer is 42."}, + "rejected": {"role": "assistant", "content": "Unknown."}, + } + ] + ) + transform_fn, _ = default( + DictDefault( + { + "chat_template": "llama3", + "datasets": [ + { + "type": "chat_template", + "roles": { + "user": ["user"], + "assistant": ["assistant"], + "system": ["system"], + "tool": ["tool_result"], + }, + } + ], + } + ) + ) + result = transform_fn(dataset[0], tokenizer=llama3_tokenizer) + assert "prompt" in result + + if __name__ == "__main__": unittest.main() diff --git a/tests/utils/data/test_hash.py b/tests/utils/data/test_hash.py new file mode 100644 index 0000000000..04fed468e2 --- /dev/null +++ b/tests/utils/data/test_hash.py @@ -0,0 +1,135 @@ +""" +Tests for generate_dataset_hash_from_config. + +Regression test for https://github.com/axolotl-ai-cloud/axolotl/issues/3303: +changing output_dir should not bust the dataset cache when added_tokens_overrides +is set. +""" + +from axolotl.utils.data.shared import generate_dataset_hash_from_config +from axolotl.utils.dict import DictDefault + + +def _base_cfg(**kwargs): + return DictDefault( + { + "sequence_len": 2048, + "sample_packing": False, + "eval_sample_packing": False, + "group_by_length": False, + "kd_temperature": None, + "dataset_exact_deduplication": False, + "tokenizer_config": "NousResearch/Llama-3.2-1B", + **kwargs, + } + ) + + +def _datasets(): + return [ + DictDefault( + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + "shards": None, + "conversation": None, + "split": "train", + "temperature": None, + } + ) + ] + + +class TestGenerateDatasetHashFromConfig: + def test_same_config_same_hash(self): + """Identical configs produce identical hashes.""" + cfg = _base_cfg() + h1 = generate_dataset_hash_from_config( + cfg, _datasets(), "NousResearch/Llama-3.2-1B" + ) + h2 = generate_dataset_hash_from_config( + cfg, _datasets(), "NousResearch/Llama-3.2-1B" + ) + assert h1 == h2 + + def test_different_tokenizer_different_hash(self): + """A different tokenizer path produces a different hash.""" + cfg = _base_cfg() + h1 = generate_dataset_hash_from_config( + cfg, _datasets(), "NousResearch/Llama-3.2-1B" + ) + h2 = generate_dataset_hash_from_config( + cfg, _datasets(), "HuggingFaceTB/SmolLM2-135M" + ) + assert h1 != h2 + + def test_different_sequence_len_different_hash(self): + cfg_a = _base_cfg(sequence_len=2048) + cfg_b = _base_cfg(sequence_len=4096) + h1 = generate_dataset_hash_from_config(cfg_a, _datasets(), "tok") + h2 = generate_dataset_hash_from_config(cfg_b, _datasets(), "tok") + assert h1 != h2 + + # --- Regression: added_tokens_overrides + output_dir --- + + def test_added_tokens_overrides_hash_stable_across_output_dir(self): + """Hash must not change when only output_dir changes (issue #3303). + + When added_tokens_overrides is set the tokenizer is saved into output_dir, + making tokenizer.name_or_path an absolute path that includes output_dir. + The hash should be derived from the canonical tokenizer config + overrides, + not from the output-dir-dependent path. + """ + cfg_run1 = _base_cfg( + output_dir="/tmp/run_1", + added_tokens_overrides={32000: "", 32001: ""}, + ) + cfg_run2 = _base_cfg( + output_dir="/tmp/run_2_different_name", + added_tokens_overrides={32000: "", 32001: ""}, + ) + + # Simulate what happens in practice: tokenizer.name_or_path becomes the + # output_dir-based path after modify_tokenizer_files() saves the tokenizer. + tokenizer_name_run1 = "/tmp/run_1/modified_tokenizer" + tokenizer_name_run2 = "/tmp/run_2_different_name/modified_tokenizer" + + h1 = generate_dataset_hash_from_config( + cfg_run1, _datasets(), tokenizer_name_run1 + ) + h2 = generate_dataset_hash_from_config( + cfg_run2, _datasets(), tokenizer_name_run2 + ) + + assert h1 == h2, ( + "Dataset cache hash must not change when only output_dir changes " + "while added_tokens_overrides stays the same (issue #3303)." + ) + + def test_added_tokens_overrides_different_overrides_different_hash(self): + """Different added_tokens_overrides produce different hashes.""" + cfg_a = _base_cfg( + output_dir="/tmp/run_a", + added_tokens_overrides={32000: ""}, + ) + cfg_b = _base_cfg( + output_dir="/tmp/run_a", # same output_dir + added_tokens_overrides={32000: ""}, + ) + tokenizer_path = "/tmp/run_a/modified_tokenizer" + + h1 = generate_dataset_hash_from_config(cfg_a, _datasets(), tokenizer_path) + h2 = generate_dataset_hash_from_config(cfg_b, _datasets(), tokenizer_path) + + assert h1 != h2 + + def test_no_added_tokens_overrides_uses_tokenizer_name_as_before(self): + """Without added_tokens_overrides the old behaviour is preserved.""" + cfg = _base_cfg() # no added_tokens_overrides + tokenizer_name = "NousResearch/Llama-3.2-1B" + + h1 = generate_dataset_hash_from_config(cfg, _datasets(), tokenizer_name) + # Changing tokenizer_name still changes the hash + h2 = generate_dataset_hash_from_config(cfg, _datasets(), "some/other-model") + + assert h1 != h2 diff --git a/tests/utils/schemas/validation/test_config_validators.py b/tests/utils/schemas/validation/test_config_validators.py new file mode 100644 index 0000000000..c756f13625 --- /dev/null +++ b/tests/utils/schemas/validation/test_config_validators.py @@ -0,0 +1,119 @@ +""" +Tests for new config validators added to AxolotlInputConfig. + +Covers: + - save_strategy: 'best' requires metric_for_best_model + - streaming=True with val_set_size > 0 is rejected + - lora_target_modules with invalid regex patterns is rejected +""" + +import pytest + +from axolotl.utils.config import validate_config +from axolotl.utils.dict import DictDefault + + +class TestSaveStrategyBestValidator: + """save_strategy: 'best' must be accompanied by metric_for_best_model.""" + + def test_save_strategy_best_without_metric_raises(self, min_base_cfg): + cfg = min_base_cfg | DictDefault(save_strategy="best") + with pytest.raises(ValueError, match="metric_for_best_model"): + validate_config(cfg) + + def test_save_strategy_best_with_metric_passes(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + save_strategy="best", + metric_for_best_model="eval_loss", + ) + validated = validate_config(cfg) + assert validated.save_strategy == "best" + assert validated.metric_for_best_model == "eval_loss" + + def test_save_strategy_epoch_without_metric_passes(self, min_base_cfg): + cfg = min_base_cfg | DictDefault(save_strategy="epoch") + validated = validate_config(cfg) + assert validated.save_strategy == "epoch" + + def test_save_strategy_no_without_metric_passes(self, min_base_cfg): + cfg = min_base_cfg | DictDefault(save_strategy="no") + validated = validate_config(cfg) + assert validated.save_strategy == "no" + + def test_save_strategy_unset_without_metric_passes(self, min_base_cfg): + """The default (None / not set) should not require metric_for_best_model.""" + validated = validate_config(min_base_cfg) + assert validated.save_strategy is None + + +class TestStreamingWithValSetSizeValidator: + """streaming=True is incompatible with val_set_size > 0.""" + + def test_streaming_with_val_set_size_raises(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + streaming=True, val_set_size=0.1, max_steps=100 + ) + with pytest.raises(ValueError, match="val_set_size"): + validate_config(cfg) + + def test_streaming_with_val_set_size_zero_passes(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + streaming=True, val_set_size=0.0, max_steps=100 + ) + validated = validate_config(cfg) + assert validated.streaming is True + + def test_streaming_false_with_val_set_size_passes(self, min_base_cfg): + cfg = min_base_cfg | DictDefault(streaming=False, val_set_size=0.1) + validated = validate_config(cfg) + assert validated.val_set_size == pytest.approx(0.1) + + def test_streaming_unset_with_val_set_size_passes(self, min_base_cfg): + cfg = min_base_cfg | DictDefault(val_set_size=0.2) + validated = validate_config(cfg) + assert validated.val_set_size == pytest.approx(0.2) + + +class TestLoraTargetModulesRegexValidator: + """lora_target_modules entries must be valid Python regex patterns.""" + + def test_invalid_regex_raises(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + adapter="lora", + lora_target_modules=["q_proj", "[invalid_regex"], + ) + with pytest.raises(ValueError, match="invalid regex pattern"): + validate_config(cfg) + + def test_valid_regex_passes(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + adapter="lora", + lora_target_modules=["q_proj", "v_proj", r".*_proj"], + ) + validated = validate_config(cfg) + assert "q_proj" in validated.lora_target_modules + + def test_plain_module_names_pass(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + adapter="lora", + lora_target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], + ) + validated = validate_config(cfg) + assert len(validated.lora_target_modules) == 4 + + def test_lora_target_linear_string_not_validated(self, min_base_cfg): + """When lora_target_modules is a string (e.g. 'all-linear'), skip regex check.""" + cfg = min_base_cfg | DictDefault( + adapter="lora", + lora_target_modules="all-linear", + ) + # Should not raise + validate_config(cfg) + + def test_multiple_invalid_patterns_reported(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + adapter="lora", + lora_target_modules=["[bad1", "[bad2"], + ) + with pytest.raises(ValueError, match="invalid regex pattern"): + validate_config(cfg) From 50e9573f24528085e4369affe3cd40269c23575f Mon Sep 17 00:00:00 2001 From: Andrew Wu <49274027+BrownianNotion@users.noreply.github.com> Date: Thu, 2 Apr 2026 04:25:18 +0100 Subject: [PATCH 1247/1405] Update lm-eval for transformers v5 support (#3571) [skip ci] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e7a9525468..2bd4c4aebf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -61,7 +61,7 @@ zstandard==0.22.0 fastcore # lm eval harness -lm_eval==0.4.7 +lm_eval==0.4.11 langdetect==1.0.9 immutabledict==4.2.0 antlr4-python3-runtime==4.13.2 From 16e32232fbb2e20b6fbdfb22d58b6abe1150bf91 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 2 Apr 2026 19:01:26 +0700 Subject: [PATCH 1248/1405] feat(docs): comprehensive improvement (#3564) * docs: comprehensive documentation improvements for humans and agents New human docs: - grpo.qmd: GRPO deep dive (async, rewards, IS correction, scaling) - ebft.qmd: EBFT guide (structured/strided modes, feature extraction) - choosing_method.qmd: decision tree for SFT vs LoRA vs DPO vs GRPO - vllm_serving.qmd: vLLM setup for GRPO (server/colocate, LoRA sync) - training_stability.qmd: monitoring, NaN debugging, OOM, healthy metrics New agent docs: - AGENTS_SFT.md: agent reference for supervised fine-tuning - AGENTS_DPO.md: agent reference for preference learning (DPO/KTO/ORPO) Updated existing docs: - rlhf.qmd: cross-references to new GRPO/EBFT/choosing-method guides - getting-started.qmd: reorganized Next Steps with links to new guides - debugging.qmd: link to training stability guide - _quarto.yml: added new pages to sidebar navigation Removed: - bak.agents.md: stale backup that confused agents * docs: trim duplicated generic config from AGENTS_DPO.md Remove boilerplate training params (optimizer, gradient_checkpointing, flash_attention, etc.) from each method template. These are not preference-learning-specific and are already covered in AGENTS_SFT.md. Config templates now show only method-specific fields with a reference to AGENTS_SFT.md for the rest. * docs: deduplicate across new doc pages - grpo.qmd: collapse vLLM setup section to brief config + link to vllm_serving.qmd; collapse IS correction to essentials + link; replace full monitoring tables with summary + link to training_stability.qmd - vllm_serving.qmd: remove duplicated async/IS config reference tables (already in grpo.qmd config reference); replace full example config with link to grpo.qmd quick start - ebft.qmd: trim generic training params in quick start config * fix: train scripts * feat: split files into cleaner parts * fix: cleanup pretraining docs --------- Co-authored-by: Wing Lian --- AGENTS.md | 94 +++++ _quarto.yml | 5 + docs/agents/grpo.md | 71 ++++ docs/agents/preference_tuning.md | 121 ++++++ docs/agents/pretraining.md | 75 ++++ docs/agents/reward_modelling.md | 48 +++ docs/agents/sft.md | 115 +++++ docs/choosing_method.qmd | 206 +++++++++ docs/dataset-formats/index.qmd | 77 +--- docs/dataset-formats/pretraining.qmd | 28 +- docs/debugging.qmd | 8 +- docs/ebft.qmd | 556 ++++++++++++++++++++++++ docs/getting-started.qmd | 31 +- docs/grpo.qmd | 611 +++++++++++++++++++++++++++ docs/rlhf.qmd | 16 +- docs/training_stability.qmd | 399 +++++++++++++++++ docs/vllm_serving.qmd | 318 ++++++++++++++ 17 files changed, 2677 insertions(+), 102 deletions(-) create mode 100644 AGENTS.md create mode 100644 docs/agents/grpo.md create mode 100644 docs/agents/preference_tuning.md create mode 100644 docs/agents/pretraining.md create mode 100644 docs/agents/reward_modelling.md create mode 100644 docs/agents/sft.md create mode 100644 docs/choosing_method.qmd create mode 100644 docs/ebft.qmd create mode 100644 docs/grpo.qmd create mode 100644 docs/training_stability.qmd create mode 100644 docs/vllm_serving.qmd diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..b81904e43c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,94 @@ +# Axolotl + +Fine-tuning framework for LLMs. Config-driven: every training run is defined by a single YAML file. + +## Tech Stack + +Python, PyTorch, HuggingFace Transformers, TRL, PEFT (LoRA/QLoRA), DeepSpeed, FSDP, vLLM (for GRPO generation). + +## Commands + +```bash +axolotl train config.yaml # Train (single or multi-GPU, auto-detected) +axolotl preprocess config.yaml # Tokenize dataset and validate config +axolotl preprocess config.yaml --debug # Inspect tokenized samples and label masking +axolotl inference config.yaml # Interactive inference +axolotl merge-lora config.yaml # Merge LoRA adapter into base model +axolotl vllm-serve config.yaml # Start vLLM server for GRPO/EBFT training +axolotl fetch examples # Download example configs +``` + +## Training Methods + +| Method | Config Key | When to Use | +|--------|-----------|-------------| +| SFT | *(default)* | Input-output pairs, instruction tuning | +| DPO/IPO | `rl: dpo` / `rl: ipo` | Paired preference data (chosen vs rejected) | +| KTO | `rl: kto` | Unpaired binary preference labels | +| ORPO | `rl: orpo` | Single-stage alignment, no ref model | +| GRPO | `rl: grpo` | RL with verifiable reward functions (math, code) | +| EBFT | `rl: ebft` | Feature-matching rewards from internal representations | + +Agent-specific references: +- [docs/agents/sft.md](docs/agents/sft.md) — supervised fine-tuning +- [docs/agents/preference_tuning.md](docs/agents/preference_tuning.md) — DPO, IPO, KTO, ORPO, SimPO +- [docs/agents/grpo.md](docs/agents/grpo.md) — GRPO online RL with reward functions +- [docs/agents/reward_modelling.md](docs/agents/reward_modelling.md) — outcome and process reward models +- [docs/agents/pretraining.md](docs/agents/pretraining.md) — continual pretraining + +## Config Pattern + +All training is config-driven. A YAML file specifies model, adapter, dataset(s), and hyperparameters: + +```yaml +base_model: meta-llama/Llama-3.1-8B-Instruct +adapter: lora # or qlora, or omit for full fine-tune +datasets: + - path: my_dataset + type: chat_template # prompt strategy (see docs/dataset-formats/) +output_dir: ./outputs/lora-out +``` + +Config schema: `src/axolotl/utils/schemas/config.py` (AxolotlInputConfig). + +## Project Structure + +``` +src/axolotl/ + cli/ # CLI entry points (train, preprocess, inference, merge_lora, vllm_serve) + core/ + builders/ # TrainerBuilder classes (causal.py for SFT, rl.py for RLHF) + trainers/ # Trainer classes, mixins (optimizer, scheduler, packing) + dpo/ # DPO trainer and config + grpo/ # GRPO trainer and sampler + loaders/ # Model, tokenizer, adapter, processor loading + prompt_strategies/ # Dataset format handlers (chat_template, alpaca, dpo/, kto/, orpo/) + utils/schemas/ # Pydantic config schemas (config, model, training, peft, trl, fsdp) + integrations/ # Plugins (liger, cut_cross_entropy, swanlab, nemo_gym) + monkeypatch/ # Runtime patches for HF transformers + +examples/ # Example YAML configs by model (llama-3/, qwen2/, mistral/, ebft/) +deepspeed_configs/ # DeepSpeed JSON configs (zero2, zero3) +docs/ # Quarto documentation site +``` + +## Code Conventions + +- Config-driven: features are toggled via YAML, not code changes +- Prompt strategies: `src/axolotl/prompt_strategies/` — each `type:` value maps to a function +- Plugin system: `plugins:` list in config loads integration modules +- Trainer mixins: `core/trainers/mixins/` for composable trainer behaviors +- Schemas: all config validation via Pydantic in `utils/schemas/` + +## Key Documentation + +- [Getting Started](docs/getting-started.qmd) — quickstart tutorial +- [Choosing a Method](docs/choosing_method.qmd) — SFT vs DPO vs GRPO decision guide +- [Config Reference](docs/config-reference.qmd) — all config options +- [Dataset Formats](docs/dataset-formats/) — chat_template, alpaca, input_output, completion +- [RLHF](docs/rlhf.qmd) — DPO, KTO, ORPO, GRPO, EBFT configs and dataset formats +- [GRPO Deep Dive](docs/grpo.qmd) — async training, custom rewards, scaling +- [vLLM Serving](docs/vllm_serving.qmd) — vLLM setup for GRPO/EBFT +- [Multi-GPU](docs/multi-gpu.qmd) — FSDP and DeepSpeed +- [Training Stability](docs/training_stability.qmd) — debugging loss, NaN, OOM +- [Debugging](docs/debugging.qmd) — VSCode setup, Docker debugging diff --git a/_quarto.yml b/_quarto.yml index 404125d1c1..2916ef2ec0 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -238,6 +238,7 @@ website: - section: "Getting Started" contents: - docs/getting-started.qmd + - docs/choosing_method.qmd - docs/installation.qmd - docs/inference.qmd - section: "Model Guides" @@ -302,6 +303,9 @@ website: contents: - docs/multimodal.qmd - docs/rlhf.qmd + - docs/grpo.qmd + - docs/ebft.qmd + - docs/vllm_serving.qmd - docs/reward_modelling.qmd - docs/lr_groups.qmd - docs/lora_optims.qmd @@ -334,6 +338,7 @@ website: - section: "Troubleshooting" contents: - docs/faq.qmd + - docs/training_stability.qmd - docs/debugging.qmd - docs/nccl.qmd diff --git a/docs/agents/grpo.md b/docs/agents/grpo.md new file mode 100644 index 0000000000..0d7ba3f41e --- /dev/null +++ b/docs/agents/grpo.md @@ -0,0 +1,71 @@ +# GRPO — Agent Reference + +Online RL with verifiable reward functions. For full config reference, async features, and scaling, see [grpo.qmd](../grpo.qmd). For vLLM setup, see [vllm_serving.qmd](../vllm_serving.qmd). + +## Architecture + +``` +Terminal 1 (GPU 0) Terminal 2 (GPU 1) +┌──────────────────────┐ ┌──────────────────────────────────┐ +│ vLLM Server │ HTTP │ Trainer │ +│ Serves base model │◄────────────►│ 1. Send prompts to vLLM │ +│ + LoRA adapter │ /generate │ 2. Score completions (rewards) │ +│ │ /set_lora │ 3. Compute advantages │ +│ Punica kernels for │ │ 4. PPO-clip gradient update │ +│ LoRA inference │ │ 5. Sync LoRA weights to vLLM │ +└──────────────────────┘ └──────────────────────────────────┘ +``` + +## Components Required + +1. A YAML config with `rl: grpo` +2. A reward module (Python file with reward functions) +3. A running vLLM server (`axolotl vllm-serve config.yaml`) + +## Reward Function Signature + +```python +def my_reward(completions, **kwargs) -> list[float]: + # completions[i][0]["content"] = text of i-th completion + # **kwargs contains dataset columns not removed by transform + return [score_for_each_completion] +``` + +Multiple rewards: `reward_funcs: [r1, r2]` with `reward_weights: [1.0, 0.5]`. + +## Key Async Features + +| Feature | Config | Purpose | +|---------|--------|---------| +| Async prefetch | `async_prefetch: true` | Overlap generation with training | +| LoRA sync | `vllm_lora_sync: true` | Fast adapter sync via filesystem | +| Streaming scoring | `streaming_partial_batch: true` | Score one group at a time | +| Zero-adv skip | `skip_zero_advantage_batches: true` | Skip batches with no learning signal | +| Replay buffer | `replay_buffer_size: 100` | Cache high-signal groups | +| IS correction | `vllm_importance_sampling_correction: true` | Fix off-policy distribution shift | + +## Health Checks + +- `rewards/*/mean` > 0.15 within 20 steps (else: test reward function standalone) +- `reward_std` > 0 on most steps (else: no learning signal) +- `entropy` 0.05-0.5 (< 0.01 = mode collapse) +- `grad_norm` 0.001-1.0 (> 10 = unstable, 0.0 = zero-advantage skip) + +See [training_stability.qmd](../training_stability.qmd) for detailed diagnostics. + +## File Map + +``` +src/axolotl/ + cli/train.py # Entry point + cli/vllm_serve.py # Entry point for vLLM server + core/trainers/grpo/ + trainer.py # AxolotlGRPOTrainer + sampler.py # Sampling utilities + core/builders/rl.py # HFRLTrainerBuilder — routes rl type → trainer + scripts/vllm_serve_lora.py # vLLM serve script with LoRA sync support + utils/schemas/trl.py # TRL config schema (all trl: options) + +docs/grpo.qmd # Full user docs: async, rewards, scaling, config reference +docs/vllm_serving.qmd # vLLM server modes, LoRA sync, weight sync +``` diff --git a/docs/agents/preference_tuning.md b/docs/agents/preference_tuning.md new file mode 100644 index 0000000000..bed9730097 --- /dev/null +++ b/docs/agents/preference_tuning.md @@ -0,0 +1,121 @@ +# Preference Learning (RLHF) — Agent Reference + +Reference for DPO, IPO, KTO, ORPO, and SimPO. For config templates and dataset format examples, see [rlhf.qmd](../rlhf.qmd). For GRPO, see [grpo.qmd](../grpo.qmd). For EBFT, see [ebft.qmd](../ebft.qmd). + +## Method Overview + +| Method | Data Requirement | Key Idea | Best For | +|--------|-----------------|----------|----------| +| **DPO** | Paired (chosen + rejected) | Implicit reward via preference pairs | General alignment, most common | +| **IPO** | Paired (chosen + rejected) | DPO with different loss (avoids overfitting) | When DPO overfits | +| **KTO** | Unpaired (completion + binary label) | Kahneman-Tversky loss, no pairs needed | When you only have thumbs-up/down | +| **ORPO** | Paired (chosen + rejected) | Combined SFT + preference, no ref model | Single-stage alignment, saves VRAM | +| **SimPO** | Paired (chosen + rejected) | Length-normalized, no ref model | Simple setup, length-robust | + +Default: start with DPO. All methods require `sample_packing: false`. + +## Architecture + +``` +┌──────────────┐ ┌───────────────┐ ┌───────────────┐ +│ Policy Model │ │ Reference │ │ Preference │ +│ (trainable) │ │ Model (frozen)│ │ Dataset │ +└──────┬───────┘ └──────┬────────┘ └──────┬────────┘ + └──────────┬───────┘ │ + v │ + Forward pass on chosen + rejected <─────┘ + │ + Preference Loss (DPO/IPO/KTO/...) + │ + Backprop + Update + +Exception: ORPO and SimPO do NOT use a reference model (~50% less VRAM). +``` + +No vLLM server needed (unlike GRPO). Offline RL with pre-collected preference data. + +## Method Selection + +1. Paired preference data (chosen + rejected)? + - Default → `rl: dpo` + - Overfitting → `rl: ipo` + - VRAM-limited → `rl: orpo` (no ref model) + - Length-sensitive → `rl: simpo` (no ref model) +2. Only binary labels (good/bad)? → `rl: kto` +3. Single-stage training (no separate SFT)? → `rl: orpo` + +| | DPO | IPO | KTO | ORPO | SimPO | +|---|---|---|---|---|---| +| **Reference model** | Yes | Yes | Yes | No | No | +| **VRAM overhead** | ~2x model | ~2x model | ~2x model | ~1x model | ~1x model | +| **TRL trainer class** | DPOTrainer | DPOTrainer | KTOTrainer | ORPOTrainer | CPOTrainer | + +## Prompt Strategy Resolution + +The `type` field resolves to a Python function: + +``` +type: "chatml.intel" + → axolotl.prompt_strategies.dpo.chatml.intel(cfg, **kwargs) + → returns transform_fn(sample) → {"prompt", "chosen", "rejected"} + +type: "chat_template.default" + → axolotl.prompt_strategies.dpo.chat_template.default(cfg, dataset_idx, **kwargs) + +type: {"field_prompt": "prompt", ...} (dict) + → axolotl.prompt_strategies.dpo.user_defined.default(...) +``` + +Module base: `axolotl.prompt_strategies.{rl_method}` — replace `dpo` with `kto` or `orpo`. + +## Healthy Training Indicators + +| Metric | Healthy Range | Problem | +|--------|--------------|---------| +| `train/loss` | Decreasing, 0.3-0.7 | Flat or increasing = broken data or too high LR | +| `rewards/chosen` | Increasing | Flat = model not learning preferences | +| `rewards/rejected` | Decreasing | Increasing = model prefers wrong responses | +| `rewards/margins` | Positive and increasing | Negative = prefers rejected over chosen | +| `rewards/accuracies` | > 0.5, toward 0.7+ | < 0.5 = worse than random | +| `logps/rejected` | Decreasing | Increasing = reward hacking | +| `grad_norm` | 0.01 - 10.0 | > 100 = exploding gradients | + +Method-specific: DPO/IPO watch `rewards/margins`; KTO loss is noisier; ORPO monitor SFT + odds ratio components; SimPO check length-normalized reward separation. + +## Known Issues + +| Issue | Fix | +|-------|-----| +| Sample packing crash | Set `sample_packing: false` (required for all preference methods) | +| KTO `KeyError: 'label'` | Ensure dataset has boolean `label` column | +| ORPO/KTO `KeyError` during tokenization | Add `remove_unused_columns: false` | +| ORPO template not applied | ORPO requires explicit `chat_template` setting | +| OOM with ref model (DPO/IPO/KTO) | Use LoRA/QLoRA, or switch to ORPO/SimPO (no ref model) | +| IPO + label_smoothing | Do not set `dpo_label_smoothing` when `rl: ipo` | + +Full troubleshooting: [training_stability.qmd](../training_stability.qmd) + +## File Map + +``` +src/axolotl/ + core/trainers/dpo/ # DPO trainer, args, strategy + core/builders/rl.py # HFRLTrainerBuilder — routes rl type → trainer class + core/training_args.py # AxolotlKTOConfig, AxolotlORPOConfig, AxolotlCPOConfig + prompt_strategies/ + dpo/ # DPO/IPO/SimPO dataset strategies + chat_template.py # chat_template.default, chat_template.argilla_chat + chatml.py # chatml.default/intel/icr/argilla_chat/prompt_pairs/ultra + llama3.py # llama3 variants (same subtypes as chatml) + user_defined.py # Custom field mapping + passthrough.py # No transform + kto/ # KTO dataset strategies (chatml, llama3, user_defined) + orpo/ # ORPO dataset strategies (chat_template.argilla) + utils/schemas/enums.py # RLType enum (dpo, ipo, kto, orpo, simpo, grpo, gdpo, ebft) + utils/schemas/config.py # All rl/dpo/kto/orpo/simpo config fields + +docs/rlhf.qmd # Full user docs: all dataset formats, config templates +docs/choosing_method.qmd # SFT vs DPO vs GRPO decision guide +examples/qwen2/dpo.yaml # DPO example +examples/llama-3/qlora-1b-kto.yaml # KTO example +``` diff --git a/docs/agents/pretraining.md b/docs/agents/pretraining.md new file mode 100644 index 0000000000..e6291e2ed8 --- /dev/null +++ b/docs/agents/pretraining.md @@ -0,0 +1,75 @@ +# Pretraining / Continual Pretraining — Agent Reference + +Train on raw text with no input masking. Two approaches depending on dataset size. + +## When to Use + +- Continual pretraining on domain-specific corpora +- Adapting a base model to a new language or domain before fine-tuning +- Pretraining-style data where the entire text is the training signal + +## Choosing an Approach + +| | Non-streaming (`type: completion`) | Streaming (`pretraining_dataset`) | +|---|---|---| +| **Dataset size** | Fits in memory | Too large to fit in memory | +| **Tokenization** | Pre-tokenized before training | On-demand during training | +| **Config key** | `datasets:` | `pretraining_dataset:` | +| **Long text handling** | Splits texts exceeding `sequence_len` | Concatenates into fixed-length sequences | +| **Benefit** | Can preprocess on CPU, transfer to GPU | Start training immediately, no preprocessing | + +## Non-Streaming: `type: completion` + +For smaller datasets that fit in memory. Pre-tokenizes the entire dataset. + +```yaml +datasets: + - path: my_corpus + type: completion + # field: text # Column name (default: "text") +``` + +## Streaming: `pretraining_dataset` + +For large corpora. Streams data on-demand without loading everything into memory. + +```yaml +pretraining_dataset: + - path: HuggingFaceFW/fineweb-edu + type: pretrain + text_column: text + split: train + +max_steps: 1000 # Required — axolotl can't infer dataset size +streaming_multipack_buffer_size: 10000 # Buffer for sample packing +pretrain_multipack_attn: true # Prevent cross-attention between packed samples +``` + +`max_steps` is required for streaming — one step = `sequence_len * micro_batch_size * gradient_accumulation_steps * num_gpus` tokens. + +Full streaming docs: [streaming.qmd](../streaming.qmd) + +## Dataset Format + +```json +{"text": "The complete document text goes here."} +``` + +## Key Settings + +- `sample_packing: true` + `pad_to_sequence_len: true` — pack documents into fixed-length sequences +- `flash_attention: true` — required for sample packing +- No adapter — typically full fine-tune for pretraining +- `train_on_inputs: true` — default for completion (all tokens trained on) + +## File Map + +``` +src/axolotl/ + prompt_strategies/completion.py # Non-streaming: completion prompt strategy (no masking) + utils/data/sft.py # Non-streaming: dataset loading and processing + utils/data/streaming.py # Streaming: encode_streaming(), wrap_streaming_dataset() + utils/schemas/config.py # Config fields: pretraining_dataset, pretrain_multipack_attn, etc. + +examples/streaming/pretrain.yaml # Full streaming pretraining example config +``` diff --git a/docs/agents/reward_modelling.md b/docs/agents/reward_modelling.md new file mode 100644 index 0000000000..055dc6b36c --- /dev/null +++ b/docs/agents/reward_modelling.md @@ -0,0 +1,48 @@ +# Reward Modelling — Agent Reference + +Train models to score responses for use as reward signals in RL. For full docs, see [reward_modelling.qmd](../reward_modelling.qmd). + +## Types + +### Outcome Reward Models (ORM) + +Train a classifier to predict preference over entire interactions. Uses `AutoModelForSequenceClassification`. + +```yaml +base_model: google/gemma-2-2b +model_type: AutoModelForSequenceClassification +num_labels: 1 +reward_model: true +chat_template: gemma +datasets: + - path: argilla/distilabel-intel-orca-dpo-pairs + type: bradley_terry.chat_template +``` + +Dataset format: `{"system": "...", "input": "...", "chosen": "...", "rejected": "..."}` + +### Process Reward Models (PRM) + +Train a token classifier to score each reasoning step. Uses `AutoModelForTokenClassification`. + +```yaml +base_model: Qwen/Qwen2.5-3B +model_type: AutoModelForTokenClassification +num_labels: 2 +process_reward_model: true +datasets: + - path: trl-lib/math_shepherd + type: stepwise_supervised +``` + +Dataset format: see [stepwise_supervised.qmd](../dataset-formats/stepwise_supervised.qmd). + +## File Map + +``` +src/axolotl/ + core/builders/causal.py # Handles reward_model flag in trainer builder + prompt_strategies/bradley_terry/ # Bradley-Terry prompt strategies + prompt_strategies/stepwise_supervised.py # PRM dataset strategy + utils/schemas/config.py # reward_model, process_reward_model config fields +``` diff --git a/docs/agents/sft.md b/docs/agents/sft.md new file mode 100644 index 0000000000..4d7b4e1a58 --- /dev/null +++ b/docs/agents/sft.md @@ -0,0 +1,115 @@ +# SFT — Agent Reference + +Supervised fine-tuning pipeline reference. For config templates and dataset format examples, see [getting-started.qmd](../getting-started.qmd) and [dataset-formats/](../dataset-formats/). + +## Architecture + +``` +YAML Config → axolotl train config.yaml + + 1. Load base model (+ quantization if QLoRA/8-bit) + 2. Apply adapter layers (LoRA/QLoRA) if configured + 3. Load + tokenize dataset(s) + - Apply prompt template (chat_template / alpaca / custom) + - Mask inputs (train_on_inputs: false) + - Pack samples into sequences (sample_packing: true) + 4. Training loop (HuggingFace Trainer) + - forward → loss → backward → optimizer step → lr scheduler step + 5. Save model / adapter weights + tokenizer + +Multi-GPU: FSDP or DeepSpeed shards model across GPUs automatically. +``` + +## Components Required + +1. A YAML config — model, dataset(s), adapter settings, hyperparameters +2. A dataset — HuggingFace Hub, local JSONL/JSON/Parquet, or S3/GCS path +3. (Optional) A custom prompt strategy — for non-standard dataset formats + +No external server processes needed (unlike GRPO which requires vLLM). + +## Dataset Format Decision Tree + +``` +Is your data in chat/message format? + ├─ YES: OpenAI message format (role/content)? + │ ├─ YES ──────────────────────> type: chat_template (recommended) + │ └─ NO (custom field names) ──> type: chat_template + message_property_mappings + └─ NO: Instruction/response pairs? + ├─ YES ──> type: alpaca (instruction, input, output) + └─ NO: Raw text? + ├─ YES with segments ─────> type: input_output (template-free masking) + └─ YES continuous ────────> type: completion (pretraining-style) +``` + +Full format specs: [dataset-formats/](../dataset-formats/) + +## Model Size to Adapter Choice + +| Model Size | LoRA | QLoRA (4-bit) | Full Fine-Tune | VRAM (approx) | +|-----------|------|---------------|----------------|---------------| +| 1-3B | Preferred | Low-budget option | Single GPU OK | 8-16 GB (LoRA) | +| 7-8B | Preferred | Good balance | Needs multi-GPU | 16-24 GB (LoRA) | +| 13-14B | Preferred | Good balance | Multi-GPU required | 24-40 GB (LoRA) | +| 30-70B | LoRA or QLoRA | Preferred for single GPU | Multi-node | 40-80 GB (QLoRA) | + +## Hyperparameter Ranges + +| Parameter | LoRA | QLoRA | Full FT | +|-----------|------|-------|---------| +| `learning_rate` | 1e-4 to 3e-4 | 1e-4 to 3e-4 | 1e-5 to 5e-5 | +| `lora_r` | 16-64 | 16-64 | N/A | +| `lora_alpha` | 1-2x `lora_r` | 1-2x `lora_r` | N/A | +| `micro_batch_size` | 2-8 | 2-4 | 1-2 | +| `gradient_accumulation_steps` | 2-8 | 4-16 | 4-16 | +| `num_epochs` | 1-3 | 1-3 | 1-3 | +| `optimizer` | `adamw_8bit` | `adamw_bnb_8bit` | `adamw_torch_fused` | + +Effective batch = micro_batch * grad_accum * num_gpus. Lower LR for larger models. + +## Healthy Training Indicators + +| Metric | Healthy | Problem | +|--------|---------|---------| +| `train_loss` | Decreasing, starting ~2-4 for chat models | Flat or increasing from step 1 — data or LR issue | +| `eval_loss` | Decreasing, tracks train_loss | Increasing while train_loss decreases — overfitting | +| `grad_norm` | 0.1-10, relatively stable | Spikes >100 — instability. 0.0 — frozen weights | +| `learning_rate` | Follows scheduler curve | Flat or NaN — config issue | + +Watch for: loss never decreasing (check `train_on_inputs`, dataset, LR), loss goes to 0 quickly (overfitting), eval_loss diverging (reduce epochs, add regularization). See [training_stability.qmd](../training_stability.qmd). + +## Known Issues + +| Issue | Fix | +|-------|-----| +| OOM during training | Reduce `micro_batch_size`, enable `gradient_checkpointing`, reduce `sequence_len` | +| `sample_packing` + SDPA + bf16 = 0.0 loss | Use `flash_attention: true` or disable `sample_packing` | +| Missing chat template error | Set `chat_template: chatml` explicitly | +| Label masking wrong | Run `axolotl preprocess config.yaml --debug` and inspect labels | +| Loss NaN | Use `bf16: auto`, lower LR, check data for empty samples | +| Tokenizer pad token / infinite loss | Set `special_tokens: pad_token: "<\|end_of_text\|>"` | +| FSDP save hangs | Use `fsdp_state_dict_type: FULL_STATE_DICT` | +| DeepSpeed CheckpointError | Set `use_reentrant: true` in `gradient_checkpointing_kwargs` | + +Full troubleshooting: [training_stability.qmd](../training_stability.qmd), [debugging.qmd](../debugging.qmd) + +## File Map + +``` +src/axolotl/ + cli/train.py # Entry point for `axolotl train` + cli/preprocess.py # Entry point for `axolotl preprocess` + core/builders/causal.py # HFCausalTrainerBuilder — wires config → SFT trainer + core/trainers/base.py # AxolotlTrainer — base trainer class + core/trainers/mixins/ # Packing, optimizer, scheduler, checkpoints + prompt_strategies/ # Format handlers: chat_template, alpaca, completion, input_output + utils/schemas/config.py # AxolotlInputConfig — main config schema + utils/schemas/datasets.py # SFTDataset, DatasetConfig + utils/schemas/peft.py # LoraConfig — LoRA parameters + integrations/liger/ # Liger kernel plugin + +examples/llama-3/ # LoRA, QLoRA, full FT example configs +docs/getting-started.qmd # Quickstart with config templates +docs/optimizations.qmd # Flash attention, gradient checkpointing, sample packing +docs/multi-gpu.qmd # FSDP and DeepSpeed setup +``` diff --git a/docs/choosing_method.qmd b/docs/choosing_method.qmd new file mode 100644 index 0000000000..942c52a97d --- /dev/null +++ b/docs/choosing_method.qmd @@ -0,0 +1,206 @@ +--- +title: "Which Fine-Tuning Method Should I Use?" +description: "A decision guide for choosing the right fine-tuning method, adapter, and hardware configuration in Axolotl." +format: + html: + toc: true + toc-depth: 3 + number-sections: true +execute: + enabled: false +--- + +## Overview {#sec-overview} + +Axolotl supports four broad categories of fine-tuning, each suited to different data types, objectives, and resource constraints. + +| Method | What It Does | Data You Need | +|--------|-------------|---------------| +| **Supervised Fine-Tuning (SFT)** | Teaches the model to produce specific outputs given inputs | Input-output pairs (instructions, conversations, completions) | +| **Preference Learning (DPO/KTO/ORPO)** | Steers the model toward preferred outputs and away from dispreferred ones | Chosen/rejected response pairs (DPO, ORPO) or binary labels (KTO) | +| **Reinforcement Learning (GRPO)** | Optimizes the model against a reward signal through online generation | A reward function (code or model-based) and a prompt dataset | +| **Reward Modeling** | Trains a model to score responses, for use as a reward signal in RL | Preference pairs ranked by quality | + +Each method is configured through a YAML file with `rl: ` (or omitted for SFT). All methods support LoRA, QLoRA, and full fine-tuning unless otherwise noted. + +## Decision Tree {#sec-decision-tree} + +Use the following flowchart to choose your method. Start at the top and follow the path that matches your situation. + +``` +Do you have a reward function (code-based or model-based)? +├── YES +│ └── Use GRPO (rl: grpo) +│ The model generates its own completions and learns from reward scores. +│ Best for: math, code, reasoning, tasks with verifiable answers. +│ See: rlhf.qmd#grpo +│ +└── NO + │ + Do you have preference pairs (chosen vs. rejected responses)? + ├── YES + │ │ + │ Are they paired (same prompt, one chosen, one rejected)? + │ ├── YES → Use DPO (rl: dpo) + │ │ Direct optimization without a separate reward model. + │ │ See: rlhf.qmd#dpo + │ │ + │ └── NO (only binary good/bad labels) + │ └── Use KTO (rl: kto) + │ Works with unpaired preference data. + │ See: rlhf.qmd#kto + │ + └── NO + │ + Do you have input-output examples? + ├── YES → Use SFT + │ The simplest and most common method. + │ See: getting-started.qmd + │ + └── NO + └── You need to create training data first. + Consider generating preference pairs with an LLM judge, + or writing a reward function for GRPO. +``` + +::: {.callout-tip} +**When in doubt, start with SFT.** It is the most straightforward method and works well for most tasks. You can always move to preference learning or RL later to further refine behavior. +::: + +### Method Comparison at a Glance + +| Criterion | SFT | DPO | KTO | GRPO | +|-----------|-----|-----|-----|------| +| Data complexity | Low (input-output pairs) | Medium (preference pairs) | Medium (binary labels) | Low (prompts + reward code) | +| Compute cost | Low | Medium | Medium | High (requires vLLM server) | +| Learning signal | Supervised | Contrastive | Contrastive | Online reward | +| Online generation | No | No | No | Yes | +| Reward model needed | No | No | No | No (uses reward functions) | +| Best for | Task adaptation, instruction following | Safety, style alignment | Unpaired preference data | Reasoning, math, code | + +::: {.callout-note} +**ORPO** is an alternative to DPO that combines SFT and preference optimization in a single training stage, removing the need for a separate SFT step. Configure with `rl: orpo`. See [rlhf.qmd](rlhf.qmd) for details. +::: + +## Adapter Selection {#sec-adapter-selection} + +Once you have chosen a method, decide how to apply the parameter updates. The three main options trade off VRAM usage against model quality. + +### QLoRA + +- **How it works**: The base model is loaded in 4-bit (NF4) quantization. Small low-rank adapter matrices are trained in higher precision on top. +- **VRAM savings**: Roughly 4x reduction in model memory compared to full fine-tuning. +- **Quality**: Slight degradation due to quantization noise, but often negligible for task-specific fine-tuning. +- **When to use**: When your GPU cannot fit the model in full precision, or when you want fast experimentation. + +```yaml +adapter: qlora +load_in_4bit: true +lora_r: 32 +lora_alpha: 64 +lora_target_linear: true +``` + +### LoRA + +- **How it works**: The base model is loaded at full precision (or 8-bit). Low-rank adapter matrices are trained alongside. +- **VRAM savings**: Roughly 2-3x reduction compared to full fine-tuning (model weights are frozen, only adapters + optimizer states for adapters are stored). +- **Quality**: Very close to full fine-tuning for most tasks, especially with higher rank values. +- **When to use**: When you have enough VRAM for the base model but not for full optimizer states. + +```yaml +adapter: lora +lora_r: 32 +lora_alpha: 64 +lora_target_linear: true +``` + +::: {.callout-tip} +For GRPO training, LoRA is strongly recommended. The vLLM server needs to sync weights from the trainer, and LoRA sync (`trl.vllm_lora_sync: true`) is far more efficient than syncing full merged weights. See [vLLM Serving](vllm_serving.qmd) for details. +::: + +### Full Fine-Tuning + +- **How it works**: All model parameters are updated during training. No adapters. +- **VRAM savings**: None. Requires memory for model weights, gradients, and optimizer states (roughly 4x model size in bf16 with AdamW). +- **Quality**: Highest potential quality, especially for large distribution shifts. +- **When to use**: When you have ample GPU memory or multi-GPU setups, and need maximum performance. Also required for pre-training. + +```yaml +# No adapter or load_in_* lines needed +micro_batch_size: 1 +gradient_accumulation_steps: 16 +``` + +### Quick Comparison + +| | QLoRA | LoRA | Full | +|---|---|---|---| +| Trainable params | ~0.1-1% | ~0.1-1% | 100% | +| Model memory | ~25% of full | ~50-100% of full | 100% | +| Optimizer memory | Tiny (adapters only) | Tiny (adapters only) | 2x model size (AdamW) | +| Training speed | Slower (dequantization overhead) | Baseline | Faster per-step (no adapter overhead) | +| Inference | Merge or serve with adapter | Merge or serve with adapter | Direct | +| Multi-GPU required? | Rarely | For 13B+ models | For 7B+ models | + +## Hardware Mapping {#sec-hardware-mapping} + +The tables below provide approximate GPU memory requirements. Actual usage depends on context length, batch size, and optimizer choice. + +### SFT / Preference Learning + +| Model Size | QLoRA (4-bit) | LoRA (bf16) | Full (bf16 + AdamW) | +|------------|--------------|-------------|---------------------| +| 1-3B | 6-8 GB | 8-12 GB | 24-32 GB | +| 7-8B | 10-14 GB | 16-24 GB | 60-80 GB | +| 13-14B | 16-20 GB | 28-40 GB | 120+ GB | +| 30-34B | 24-32 GB | 64-80 GB | 2-4x 80 GB | +| 70-72B | 40-48 GB | 2x 80 GB | 4-8x 80 GB | + +::: {.callout-important} +These estimates assume a short context length (512-2048 tokens) and micro_batch_size of 1-2. Longer sequences and larger batches increase memory significantly due to activations. Use [gradient checkpointing](gradient_checkpointing.qmd) to reduce activation memory at the cost of ~30% slower training. +::: + +### GRPO (RL Training) + +GRPO requires additional GPU(s) for the vLLM generation server. Plan for at least two GPUs: one for training, one for vLLM. + +| Model Size | Training GPU (LoRA, bf16) | vLLM GPU | Total GPUs | +|------------|--------------------------|----------|------------| +| 0.5-3B | 1x 24 GB | 1x 24 GB | 2x 24 GB | +| 7-8B | 1x 80 GB | 1x 80 GB | 2x 80 GB | +| 13-14B | 1-2x 80 GB | 1-2x 80 GB | 2-4x 80 GB | +| 30-72B | 2-4x 80 GB (FSDP/DeepSpeed) | 2-4x 80 GB (tensor parallel) | 4-8x 80 GB | + +::: {.callout-tip} +For single-GPU GRPO, use `vllm_mode: colocate` with `vllm_enable_sleep_mode: true`. The vLLM engine shares the GPU and offloads VRAM when not generating. This works for smaller models (up to ~3B on a 24 GB GPU) but is slower than the two-GPU server mode. +::: + +### Multi-GPU Threshold + +You need multi-GPU training when: + +- **Full fine-tuning** of models 7B+ (use FSDP or DeepSpeed ZeRO) +- **LoRA** of models 30B+ (or 13B+ with long contexts) +- **GRPO** almost always (separate vLLM server), unless using colocate mode + +See [Multi-GPU Training](multi-gpu.qmd) for FSDP and DeepSpeed configuration. + +## Quick Links {#sec-quick-links} + +| Method | Config Key | Documentation | Example Config | +|--------|-----------|---------------|----------------| +| SFT | *(default, no `rl:` key)* | [Getting Started](getting-started.qmd) | `examples/llama-3/lora-1b.yml` | +| DPO | `rl: dpo` | [RLHF - DPO](rlhf.qmd#dpo) | See rlhf.qmd | +| KTO | `rl: kto` | [RLHF - KTO](rlhf.qmd#kto) | See rlhf.qmd | +| ORPO | `rl: orpo` | [RLHF - ORPO](rlhf.qmd#orpo) | See rlhf.qmd | +| GRPO | `rl: grpo` | [RLHF - GRPO](rlhf.qmd#grpo), [vLLM Serving](vllm_serving.qmd) | See rlhf.qmd | +| Reward Modeling | `rl: reward_trainer` | [Reward Modelling](reward_modelling.qmd) | See reward_modelling.qmd | + +### Related Guides + +- [Configuration Reference](config-reference.qmd) -- Full list of all config options +- [Dataset Formats](dataset-formats) -- How to structure your training data +- [Optimizations](optimizations.qmd) -- Flash attention, gradient checkpointing, mixed precision +- [Multi-GPU Training](multi-gpu.qmd) -- FSDP and DeepSpeed setup +- [vLLM Serving](vllm_serving.qmd) -- Setting up vLLM for GRPO training diff --git a/docs/dataset-formats/index.qmd b/docs/dataset-formats/index.qmd index 715e3ef202..7018e8c25f 100644 --- a/docs/dataset-formats/index.qmd +++ b/docs/dataset-formats/index.qmd @@ -22,90 +22,47 @@ For `pretraining_dataset:` specifically, please refer to the [Pre-training secti ## Pre-training -When aiming to train on large corpora of text datasets, pre-training is your go-to choice. Due to the size of these datasets, downloading the entire-datasets before beginning training would be prohibitively time-consuming. Axolotl supports [streaming](https://huggingface.co/docs/datasets/en/stream) to only load batches into memory at a time. - -A sample format for a pre-training dataset is as follows: +Pre-training trains on raw text corpora with no input masking. The dataset format is simple: ```json {"text": "first row"} {"text": "second row"} -... ``` -It is typically recommended to save your dataset as `.jsonl` due to its flexibility and simplicity. - -Axolotl supports loading from a Hugging Face hub repo or from local files. - -### Pre-training from Hugging Face hub datasets - -As an example, to train using a Hugging Face dataset `hf_org/name`, you can pass the following config: +Axolotl supports two approaches: -```yaml -pretraining_dataset: hf_org/name -``` +### Streaming (large datasets) -### Pre-training from local dataset files - -Given a few corpus files: `A.jsonl`, `B.jsonl`, and `C.jsonl`, your config will look like the below: +For large corpora that don't fit in memory, use `pretraining_dataset` with [streaming](../streaming.qmd). Data is tokenized on-demand during training. ```yaml pretraining_dataset: - - path: json - data_files: - - A.jsonl - - B.jsonl - - C.jsonl + - path: HuggingFaceFW/fineweb-edu + type: pretrain + text_column: text + split: train ``` -While we recommend `.jsonl`, you can also use the other formats (`csv`, `parquet`, `arrow`, `SQL`, `Webdataset`) that are supported by [`Dataset.load_dataset`](https://huggingface.co/docs/datasets/loading#local-and-remote-files) - -### Pre-training without streaming - -In the case that the dataset is small and can be loaded entirely into memory, another approach to running pre-training is to use the `completion` format. This would mean that the entire dataset is pre-tokenized instead of on-demand in streaming. - -One benefit of this is that the tokenization can be performed separately on a CPU-only machine, and then transferred to a GPU machine for training to save costs. +::: {.callout-important} +Streaming requires `max_steps` in your config — Axolotl cannot infer the dataset size. One step = `sequence_len * micro_batch_size * gradient_accumulation_steps * num_gpus` tokens. +::: -From Hugging Face: +See [Streaming Datasets](../streaming.qmd) for full configuration details. -```yaml -datasets: - - path: hf_org/name - type: completion -``` +### Non-streaming (smaller datasets) -From local files: +For datasets that fit in memory, use `type: completion` under `datasets:`. The entire dataset is pre-tokenized before training, which can be done on a CPU-only machine. ```yaml datasets: - - path: A.jsonl - type: completion - - - path: B.jsonl + - path: my_corpus type: completion ``` -::: {.callout-important} -For `completion` only, Axolotl would split texts if it exceeds the context length into multiple smaller prompts. If you are interested in having this for `pretraining_dataset` too, please let us know or help make a PR! +::: {.callout-note} +With `completion`, texts exceeding `sequence_len` are split into multiple samples automatically. ::: -### Pre-training dataset configuration tips - -#### Setting max_steps - -When using streaming for large datasets, Axolotl does not know in advance how large the dataset is and does not know when to stop. - -Therefore, it is necessary to set `max_steps: int` in your config for pre-training to run, so that Axolotl knows when to stop training. - -One step is equal to `sequence_len * micro_batch_size * gradient_accumulation_steps * total_num_gpus` tokens. - -#### Group_by_length - -It is recommended to leave this off if downloading from Hugging Face hub as it would download the entire dataset which can be very large. - -### Reference - -Please see docs [here](pretraining.qmd). - ## Supervised fine-tuning (SFT) Supervised fine-tuning is the process of training models to respond to an instruction or chat input. diff --git a/docs/dataset-formats/pretraining.qmd b/docs/dataset-formats/pretraining.qmd index b51b0e0b38..9803704713 100644 --- a/docs/dataset-formats/pretraining.qmd +++ b/docs/dataset-formats/pretraining.qmd @@ -4,29 +4,9 @@ description: Data format for a pre-training completion task. order: 1 --- -For pretraining, there is no prompt template or roles. The only required field is `text`: - -```{.json filename="data.jsonl"} -{"text": "first row"} -{"text": "second row"} -... -``` - -:::{.callout-note} - -### Streaming is recommended for large datasets - -Axolotl usually loads the entire dataset into memory. This will be challenging for large datasets. Use the following config to enable streaming: - -```{.yaml filename="config.yaml"} -pretraining_dataset: - - name: - path: - split: - text_column: # column in dataset with the data, usually `text` - type: pretrain - trust_remote_code: - skip: # number of rows of data to skip over from the beginning -``` +::: {.callout-note} +Pre-training documentation has been consolidated: +- **Streaming pretraining** (large datasets): See [Streaming Datasets](../streaming.qmd#pretraining-with-streaming) +- **Non-streaming pretraining** (`type: completion`): See [Dataset Formats](index.qmd#pre-training) ::: diff --git a/docs/debugging.qmd b/docs/debugging.qmd index 04b4faa648..36e39ef165 100644 --- a/docs/debugging.qmd +++ b/docs/debugging.qmd @@ -6,6 +6,10 @@ description: How to debug Axolotl This document provides some tips and tricks for debugging Axolotl. It also provides an example configuration for debugging with VSCode. A good debugging setup is essential to understanding how Axolotl code works behind the scenes. +::: {.callout-tip} +For training-specific debugging (loss spikes, NaN gradients, OOM errors, RL training stability), see [Training Stability & Debugging](training_stability.qmd). +::: + ## Table of Contents - [General Tips](#general-tips) @@ -85,7 +89,7 @@ If you developing on a remote host, you can easily use VSCode to debug remotely. The easiest way to get started is to modify the [.vscode/launch.json](../.vscode/launch.json) file in this project. This is just an example configuration, so you may need to modify or copy it to suit your needs. -For example, to mimic the command `cd devtools && CUDA_VISIBLE_DEVICES=0 accelerate launch -m axolotl.cli.train dev_chat_template.yml`, you would use the below configuration[^1]. Note that we add additional flags that override the axolotl config and incorporate the tips above (see the comments). We also set the working directory to `devtools` and set the `env` variable `HF_HOME` to a temporary folder that is later partially deleted. This is because we want to delete the HF dataset cache before each run in order to ensure that the data preprocessing code is run from scratch. +For example, to mimic the command `cd devtools && CUDA_VISIBLE_DEVICES=0 axolotl train dev_chat_template.yml`, you would use the below configuration[^1]. Note that we add additional flags that override the axolotl config and incorporate the tips above (see the comments). We also set the working directory to `devtools` and set the `env` variable `HF_HOME` to a temporary folder that is later partially deleted. This is because we want to delete the HF dataset cache before each run in order to ensure that the data preprocessing code is run from scratch. ```json // .vscode/launch.json @@ -242,6 +246,6 @@ style="border-radius: 10px; display: block; margin: auto;" width="560" height="3
-[^1]: The config actually mimics the command `CUDA_VISIBLE_DEVICES=0 python -m accelerate.commands.launch -m axolotl.cli.train devtools/chat_template.yml`, but this is the same thing. +[^1]: The VSCode config uses `accelerate.commands.launch` as the Python module entry point, which is what `axolotl train` invokes under the hood. [^2]: Many of the below flags are recommended best practices by Nvidia when using nvidia-container-toolkit. You can read more about these flags [here](https://docs.nvidia.com/deeplearning/frameworks/user-guide/index.html). diff --git a/docs/ebft.qmd b/docs/ebft.qmd new file mode 100644 index 0000000000..eb7c95ecab --- /dev/null +++ b/docs/ebft.qmd @@ -0,0 +1,556 @@ +--- +title: "EBFT Training" +description: "Energy-Based Fine-Tuning uses feature-matching rewards from internal representations to train language models without external reward functions." +order: 9 +back-to-top-navigation: true +toc: true +toc-expand: 2 +toc-depth: 4 +--- + +## Overview + +Energy-Based Fine-Tuning (EBFT) is a training method that optimizes language models by matching the **internal feature representations** of generated text to those of ground-truth completions. Instead of relying on external reward models or hand-crafted reward functions, EBFT extracts hidden states from intermediate layers of a frozen copy of the model and uses cosine similarity between generated and reference features as the reward signal. + +Paper: ["Matching Features, Not Tokens: Energy-Based Fine-Tuning of Language Models"](https://arxiv.org/abs/2603.12248) (Jelassi et al., 2026) + +### How EBFT Differs from Other RL Methods + +| Method | Reward Signal | Requires | Best For | +|--------|--------------|----------|----------| +| **GRPO** | External reward function(s) | Custom reward code or reward model | Tasks with verifiable answers (math, code) | +| **DPO** | Preference pairs (chosen vs rejected) | Paired preference data | Alignment with human preferences | +| **EBFT** | Feature similarity to ground truth | Ground-truth completions | Any task with reference outputs | + +EBFT's key advantage is that it needs only ground-truth completions -- no reward engineering, no preference annotation, and no reward model training. The model's own internal representations serve as the reward signal. This makes it particularly effective for: + +- Code generation (match features of known-good solutions) +- Instruction following with reference outputs +- Continual pretraining on unstructured text (strided mode) +- Multi-turn dialogue with reference conversations + +### Reward Formulation + +The EBFT reward for each generated completion is: + +``` +reward = alignment_coef * cosine_similarity(gen_features, gt_features) + - diversity_coef * mean_pairwise_similarity(gen_features) +``` + +- **Alignment**: How closely the generated output's internal representations match the ground truth. Higher is better. +- **Diversity**: Penalizes generated samples that are too similar to each other (prevents mode collapse). Lower is better. +- **CFM loss** (Cross-Feature Matching): Tracks `||mean(gen_features) - gt_features||^2` as a diagnostic. This is the quantity that EBFT ultimately minimizes. + +## Modes + +EBFT supports three operational modes, each suited to different use cases. + +### Structured Mode (Sync) + +Uses vLLM on a separate GPU for generation, with sequential generate-score-train steps. This is the simplest mode and recommended for getting started. + +``` +GPU 0: vLLM Server (generates completions, receives weight syncs) +GPU 1: Trainer (feature extraction, reward computation, GRPO training) +``` + +**When to use**: Standard instruction-following or QA datasets where you have prompt/completion pairs. Requires 2 GPUs. + +### Structured Mode (Async) + +Same architecture as sync, but overlaps generation of the next batch with training on the current batch. Faster throughput at the cost of slightly stale weights during generation. + +**When to use**: Same data as sync mode, but when you want faster training and can tolerate weight staleness (controlled by `vllm_sync_interval`). + +### Strided Mode + +Runs entirely on a single GPU with no vLLM dependency. Places anchor points throughout a document and generates short rollouts at each anchor using block-parallel attention patterns. + +``` +Single GPU: Base model + LoRA adapter + - Strided block-parallel generation (flex_attention) + - Feature extraction via disable_adapter() + - No vLLM needed +``` + +**When to use**: Unstructured text data (raw code, prose, documents) where there is no natural prompt/completion split. Also works with structured data that includes prompt boundaries. Requires only 1 GPU. + +## Quick Start + +### Structured Mode + +This minimal example fine-tunes Qwen2-0.5B on code data using EBFT with vLLM generation. + +**Step 1**: Create a config file `ebft_quickstart.yaml`: + +```yaml +base_model: Qwen/Qwen2-0.5B-Instruct + +rl: ebft + +ebft: + feature_layers: [0.25, 0.5, 0.75] + embed_method: last_token + alignment_coef: 1.0 + diversity_coef: 1.0 + +trl: + num_generations: 4 + max_completion_length: 256 + temperature: 0.7 + use_vllm: true + vllm_server_host: 0.0.0.0 + vllm_server_port: 8000 + vllm_lora_sync: true + vllm_sync_interval: 3 + use_data_producer: true + async_prefetch: false + scale_rewards: true + loss_type: grpo + +vllm: + gpu_memory_utilization: 0.5 + max_model_len: 1024 + +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_opencode.transform + split: train[:500] + +# Standard training settings (see getting-started.qmd for details) +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_target_linear: true +sequence_len: 1024 +micro_batch_size: 2 +gradient_accumulation_steps: 4 +max_steps: 20 +learning_rate: 5.0e-6 +bf16: auto +flash_attention: true +gradient_checkpointing: true +output_dir: ./outputs/ebft-quickstart +``` + +**Step 2**: Start vLLM on GPU 0: + +```bash +CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve ebft_quickstart.yaml +``` + +**Step 3**: Wait approximately 30 seconds for vLLM to initialize, then start training on GPU 1: + +```bash +CUDA_VISIBLE_DEVICES=1 axolotl train ebft_quickstart.yaml +``` + +::: {.callout-important} +The `micro_batch_size` must be divisible by `num_generations`. For example, with `num_generations: 4`, valid values are 4, 8, 12, etc. +::: + +### Dataset Format + +Structured mode datasets must produce two fields after the transform: + +- `prompt`: Either a string or a list of chat messages (`[{"role": "user", "content": "..."}]`) +- `ground_truth`: A string containing the reference completion + +Example raw dataset row: + +```json +{ + "input": "Write a function to compute fibonacci numbers.", + "output": "def fibonacci(n):\n if n <= 1:\n return n\n return fibonacci(n-1) + fibonacci(n-2)" +} +``` + +The `ebft_opencode.transform` converts this to the required `{prompt, ground_truth}` format automatically. + +## Feature Extraction + +EBFT extracts hidden states from intermediate transformer layers and pools them into per-sequence embeddings. These embeddings are compared between generated and ground-truth completions to compute rewards. + +### Feature Layers + +The `feature_layers` parameter specifies which layers to extract, as fractions of total model depth: + +```yaml +ebft: + feature_layers: [0.25, 0.5, 0.75] # Quarter, middle, three-quarter depth +``` + +For a 32-layer model, this extracts layers 8, 16, and 24. The hidden states from all selected layers are concatenated along the feature dimension, producing embeddings of size `num_layers * hidden_dim`. + +::: {.callout-tip} +Using multiple layers captures both low-level syntactic features (early layers) and high-level semantic features (later layers). The default `[0.25, 0.5, 0.75]` works well across model sizes. +::: + +### Embed Methods + +The `embed_method` controls how per-token hidden states are pooled into a single vector per sequence: + +| Method | Description | Output Shape | Notes | +|--------|-------------|-------------|-------| +| `last_token` | Hidden state at the last non-padding token | `(B, D)` | Default. Good for autoregressive models where the last token summarizes the sequence. | +| `mean_pooling` | Mean of all non-padding token states | `(B, D)` | Considers the entire sequence equally. | +| `completion_mean` | Mean over completion tokens only (excludes prompt) | `(B, D)` | Focuses reward signal on generated content. Requires prompt length information. | +| `concat` | Concatenation of states at 25%, 50%, 75% positions | `(B, 3*D)` | Captures positional structure. Higher dimensional. | + +```yaml +ebft: + embed_method: completion_mean # Focus on completion features +``` + +### SVD Whitening + +Whitening decorrelates the feature dimensions so that no single direction dominates the feature-matching loss. This is computed via SVD on the generated embeddings, with the same transform applied to the ground-truth embeddings. + +```yaml +ebft: + use_whitening: true +``` + +When whitening is enabled, the reward computation applies a whitening matrix `W = U @ diag(1/S) @ U^T` derived from the SVD of generated embeddings. This ensures all feature dimensions contribute equally to the alignment reward. + +::: {.callout-note} +Singular values scale with `sqrt(batch_size)`, so reward magnitudes are batch-size dependent. This is acceptable because the number of samples per prompt (`n_samples_per_prompt` or `num_generations`) is fixed during training. +::: + +### Alignment and Diversity Coefficients + +The two reward components are weighted by coefficients: + +```yaml +ebft: + alignment_coef: 1.0 # Weight for cosine similarity with ground truth + diversity_coef: 1.0 # Weight for pairwise similarity penalty +``` + +Both values are scaled by 2 internally (per paper equation 7). The final reward per sample is: + +``` +reward_j = 2 * alignment_coef * cos(gen_j, gt) + - 2 * diversity_coef * (1/(n-1)) * sum_{j' != j} dot(gen_j, gen_j') +``` + +Setting `diversity_coef: 0.0` disables the diversity penalty entirely, which may be appropriate when `num_generations` is small (e.g., 2). + +## Strided Mode + +Strided mode is designed for training on unstructured text data where there is no natural prompt/completion boundary. Instead of generating full completions with vLLM, it places **anchor points** at regular intervals throughout each document and generates short rollouts at each anchor using block-parallel attention. + +### How Block-Parallel Generation Works + +Given a document of length `S` tokens: + +1. **Anchor placement**: Starting at position `anchor_offset`, place anchors every `stride` tokens. Each anchor defines a block. +2. **Context window**: Each block sees `context_length` tokens of preceding context from the original document. +3. **Generation**: At each anchor, generate `generate_max_len` tokens autoregressively, conditioned only on the context window. +4. **Parallelism**: All blocks are processed in a single forward pass using a specialized attention mask that prevents information leakage between blocks. + +``` +Document: [tok0, tok1, ..., tok_S] + | | | + anchor_0 anchor_1 anchor_2 + | | | + [ctx][gen] [ctx][gen] [ctx][gen] +``` + +The attention mask ensures: + +- Prompt tokens use standard causal attention +- Each generated block attends to its own context window and its own preceding generated tokens +- Blocks do not attend to each other's generated tokens + +When `flex_attention` is available (PyTorch >= 2.5), the mask is compiled into efficient fused kernels. Otherwise, a dense 4D attention mask is used as a fallback. + +### Strided Mode Configuration + +```yaml +base_model: meta-llama/Llama-3.2-1B +rl: ebft + +ebft: + mode: strided + stride: 8 # Tokens between anchor points + context_length: 8 # Context window per block + generate_max_len: 8 # Tokens to generate per block + n_samples_per_prompt: 4 # Independent rollouts per document + temperature: 0.6 + feature_layers: [0.25, 0.5, 0.75] + embed_method: last_token + use_whitening: true + alignment_coef: 1.0 + diversity_coef: 1.0 + rl_coef: 1.0 # RL policy gradient loss weight + ce_coef: 0.03 # Cross-entropy loss on GT tokens + advantage_estimator: rloo # rloo, group_norm, or reinforce + min_completion_prefix: 8 # Skip anchors in prompt region + +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_strided_structured.transform + split: train[:1%] + +sequence_len: 2048 +micro_batch_size: 1 +gradient_accumulation_steps: 2 + +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_target_linear: true + +bf16: auto +flex_attention: true +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: true # Required with flex_attention +``` + +Run with a single command (no vLLM needed): + +```bash +CUDA_VISIBLE_DEVICES=0 axolotl train config.yaml +``` + +### Advantage Estimators + +Strided mode supports three advantage estimation methods: + +| Estimator | Formula | Requirements | +|-----------|---------|-------------| +| `rloo` | Leave-one-out baseline: `reward_j - mean(rewards_{-j})` | `n_samples_per_prompt >= 2` | +| `group_norm` | Group normalization: `(reward_j - mean) / std` | `n_samples_per_prompt >= 2` | +| `reinforce` | Raw reward as advantage (no baseline) | Works with `n_samples_per_prompt = 1` | + +::: {.callout-warning} +When `n_samples_per_prompt: 1`, the trainer automatically falls back to `reinforce` and disables the diversity penalty (which requires multiple samples). +::: + +### Strided Mode Constraints + +- **`flex_attention: true`** is strongly recommended. Without it, dense 4D masks consume significantly more memory. +- **`torch_compile: true`** must NOT be set. `flex_attention` compiles its own kernels internally; adding `torch_compile` causes conflicts and OOM. +- **Gradient checkpointing** must use `use_reentrant: true`. Non-reentrant checkpointing causes `CheckpointError` with `flex_attention` block masks. +- **`activation_offloading`** is incompatible with `flex_attention`. + +### Cross-Entropy Loss + +Strided mode supports an optional cross-entropy loss term on ground-truth tokens. This acts as a regularizer to prevent the model from drifting too far from the original distribution: + +```yaml +ebft: + ce_coef: 0.03 # Small CE coefficient + rl_coef: 1.0 # RL loss coefficient +``` + +The total loss is `rl_coef * rl_loss + ce_coef * ce_loss`. For structured mode, `ce_coef` is typically `0.0` since vLLM generation provides sufficient learning signal. + +## Dataset Formats + +EBFT provides several built-in dataset transforms in `src/axolotl/prompt_strategies/ebft/`. + +### Built-In Transforms + +| Transform | Input Format | Output Fields | Use Case | +|-----------|-------------|---------------|----------| +| `ebft_opencode.transform` | `{input, output}` | `{prompt, ground_truth}` | OpenCodeInstruct, structured QA | +| `ebft_strided_structured.transform` | `{input, output}` | `{input_ids, labels, prompt_length}` | Strided mode with structured data | +| `ebft_strided_chat.transform` | `{messages: [...]}` | `{input_ids, labels, prompt_length}` | Strided mode with chat data | +| `ebft_chat_multiturn.transform` | `{messages: [...]}` | `{prompt, ground_truth, remaining_turns}` | Multi-turn: first-turn target | +| `ebft_chat_multiturn.transform_last_turn` | `{messages: [...]}` | `{prompt, ground_truth}` | Multi-turn: last-turn target | +| `ebft_chat_multiturn.transform_all_turns` | `{messages: [...]}` | `{prompt[], ground_truth[]}` | Multi-turn: one example per turn | +| `ebft_reasoning.transform` | `{messages: [...]}` (with ``) | `{prompt, ground_truth}` | Reasoning/thinking datasets | + +### Structured Mode Datasets + +For structured (sync/async) mode, the transform must produce `prompt` and `ground_truth` fields: + +```yaml +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_opencode.transform + split: train[:500] +``` + +### Multi-Turn Datasets + +Multi-turn transforms extract conversation data for sequential rollout. The `transform` variant targets the first assistant turn, while `transform_last_turn` targets the final turn: + +```yaml +datasets: + - path: your/multiturn-dataset + type: ebft_chat_multiturn.transform +``` + +When `remaining_turns` is present in the dataset output, the trainer performs sequential rollouts: it generates the first assistant turn with vLLM, then continues generating subsequent turns by building up the conversation history. + +### Strided Mode Datasets + +Strided transforms tokenize the full document and produce `input_ids`, `labels`, and `prompt_length`: + +```yaml +datasets: + - path: nvidia/OpenCodeInstruct + type: ebft_strided_structured.transform + split: train[:1%] +``` + +### Custom Transforms + +To use your own dataset format, write a transform function: + +```python +def transform(cfg, **kwargs): + def transform_fn(example, tokenizer=None): + return { + "prompt": [{"role": "user", "content": example["question"]}], + "ground_truth": example["answer"], + } + return transform_fn, {"remove_columns": "__all__"} +``` + +The `"__all__"` sentinel removes all original dataset columns after the mapping step. Reference this transform in your config: + +```yaml +datasets: + - path: your/dataset + type: your_module.transform +``` + +## Configuration Reference + +### Common Parameters (All Modes) + +These parameters are set under the `ebft:` key in the YAML config. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `mode` | `"structured"` or `"strided"` | `"structured"` | EBFT operating mode | +| `feature_layers` | `list[float]` | `[0.25, 0.5, 0.75]` | Fractional layer depths for feature extraction | +| `embed_method` | `string` | `"last_token"` | Pooling method: `last_token`, `mean_pooling`, `completion_mean`, or `concat` | +| `use_whitening` | `bool` | `false` | Apply SVD whitening to feature embeddings before reward computation | +| `alignment_coef` | `float` | `1.0` | Weight for alignment reward (cosine similarity with ground truth) | +| `diversity_coef` | `float` | `1.0` | Weight for diversity penalty (pairwise dot product between samples) | +| `ce_coef` | `float` | `0.0` | Cross-entropy loss coefficient on ground-truth tokens | +| `adaptive_max_tokens` | `bool` | `true` | Dynamically set vLLM `max_tokens` based on ground-truth length (structured mode) | +| `gt_length_multiplier` | `float` | `1.5` | Multiplier for ground-truth token count when computing adaptive max tokens (min 0.1) | + +### Strided Mode Parameters + +These additional parameters apply only when `mode: strided`. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `stride` | `int` | `8` | Number of tokens between anchor points (must be >= 1) | +| `context_length` | `int` | `8` | Context window size for each generated block (must be >= 1) | +| `generate_max_len` | `int` | `8` | Number of tokens to generate per block (must be >= 1) | +| `n_samples_per_prompt` | `int` | `4` | Number of independent rollouts per document (must be >= 1) | +| `temperature` | `float` | `0.6` | Sampling temperature for strided generation | +| `top_p` | `float` | `1.0` | Top-p nucleus sampling threshold | +| `rl_coef` | `float` | `1.0` | RL policy gradient loss coefficient | +| `advantage_estimator` | `string` | `"rloo"` | Advantage estimation method: `rloo`, `group_norm`, or `reinforce` | +| `min_completion_prefix` | `int` | `0` | Minimum tokens into the completion span before placing anchors | + +### Structured Mode TRL Parameters + +These are set under the `trl:` key and control the GRPO training loop. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `num_generations` | `int` | -- | Number of completions generated per prompt | +| `max_completion_length` | `int` | -- | Maximum tokens per generated completion | +| `temperature` | `float` | `0.7` | Sampling temperature for vLLM generation | +| `use_vllm` | `bool` | -- | Enable vLLM generation backend | +| `vllm_lora_sync` | `bool` | `false` | Sync LoRA adapters via filesystem (recommended) | +| `vllm_sync_interval` | `int` | `1` | Steps between weight syncs to vLLM | +| `use_data_producer` | `bool` | -- | Required for sync mode with LoRA sync | +| `async_prefetch` | `bool` | `false` | Enable async generation (overlaps with training) | +| `streaming_partial_batch` | `bool` | `false` | Score groups incrementally (async mode) | +| `skip_zero_advantage_batches` | `bool` | `false` | Skip micro-batches where all advantages are zero | +| `scale_rewards` | `bool` | -- | Normalize rewards within each prompt group | +| `loss_type` | `string` | `"grpo"` | Loss type for policy optimization | +| `epsilon` | `float` | `0.2` | Clipping parameter for importance sampling | + +### Stop Tokens + +vLLM needs explicit stop token IDs for generation. Common configurations: + +```yaml +trl: + generation_kwargs: + stop_token_ids: [151645, 151643] # Qwen: <|im_end|>, <|endoftext|> +``` + +### Multi-Turn Chat Settings + +For multi-turn conversations with Qwen3.5, disable thinking mode to prevent `` tags in completions: + +```yaml +trl: + chat_template_kwargs: + enable_thinking: false +``` + +## Monitoring + +### Key Metrics + +EBFT logs several custom metrics to wandb and the training console. Here is what to watch for: + +| Metric | Healthy Range | Interpretation | +|--------|--------------|----------------| +| `ebft/alignment` | 0.3 -- 0.9, trending upward | Cosine similarity between generated and ground-truth features. Higher means the model is learning to produce representations that match the reference. | +| `ebft/diversity` | 0.01 -- 0.1 | Mean pairwise similarity between different generations for the same prompt. Values above 1.0 indicate mode collapse. | +| `ebft/cfm_loss` | Below 10, trending downward | Cross-Feature Matching loss. This is the core quantity being minimized. Consistently above 100 indicates instability. | +| `ebft/reward` | Trending upward (may start negative) | Combined reward signal. If stuck at -1.0, the diversity penalty is dominating alignment. | +| `grad_norm` | 0.1 -- 3.0 | Gradient magnitude. Values of 0.0 indicate zero-advantage skip (normal). Values above 10 suggest instability. | +| `entropy` | 0.05 -- 0.5 | Policy entropy. Values below 0.01 suggest mode collapse. | +| `IS ratio min` | Above 0.1 | Importance sampling ratio minimum. Near-zero values mean the policy is too far off-policy; increase `vllm_sync_interval`. | + +### Console Log Example + +During training, you will see periodic EBFT reward logs: + +``` +ebft reward | align +0.412 ^ | divers +0.023 v | cfm 4.231 v | reward +0.389 ^ +``` + +The arrows indicate the desired direction: alignment and reward should trend upward, while diversity and CFM loss should trend downward. + +### Troubleshooting + +| Symptom | Likely Cause | Fix | +|---------|-------------|-----| +| `alignment` stays below 0.1 | Feature layers not capturing useful information | Try different `feature_layers` or `embed_method` | +| `diversity` exceeds 1.0 | Mode collapse -- generations are too similar | Increase `diversity_coef` or `temperature` | +| `reward` stuck at -1.0 | Diversity penalty dominates alignment | Reduce `diversity_coef` or increase `alignment_coef` | +| `grad_norm` consistently 0.0 | All micro-batches have zero advantage | Increase `num_generations` or check data quality | +| `CheckpointError` in strided mode | Incompatible gradient checkpointing settings | Set `use_reentrant: true` in `gradient_checkpointing_kwargs` | +| OOM during training | Logits tensor too large | Reduce `sequence_len` or `micro_batch_size`; strided mode uses chunked lm_head to mitigate this | +| vLLM 500 errors | `truncate_prompt_tokens` not supported | Ensure you are using `axolotl vllm-serve` (not `trl vllm-serve`) | + +### Feature Network Memory + +In PEFT (LoRA) mode, the feature network shares base weights with the actor model by using the `disable_adapter()` context manager. This saves an entire model copy in VRAM (approximately 1--16 GB depending on model size). For non-PEFT training, a separate frozen deepcopy is created. + +::: {.callout-note} +The `disable_adapter()` approach relies on an invariant: `merge_adapter()` is never called on the base weights. All weight sync paths (LoRA sync, HTTP, NCCL) compute merged weights as new tensors or save the adapter to the filesystem, leaving base weights unmodified. +::: + +## Examples + +Complete example configurations are available in `examples/ebft/`: + +| Config | Model | Mode | Description | +|--------|-------|------|-------------| +| `llama-1b-ebft-strided-structured.yaml` | Llama 3.2 1B | Strided | Single-GPU strided training on code data | +| `qwen3-4b-ebft-structured.yaml` | Qwen3 4B | Structured (sync) | Two-GPU structured training | +| `qwen3-4b-ebft-structured-async.yaml` | Qwen3 4B | Structured (async) | Two-GPU async training with prefetch | +| `qwen3-8b-ebft-structured.yaml` | Qwen3 8B | Structured (sync) | Two-GPU structured training for larger model | +| `qwen35-4b-ebft-structured.yaml` | Qwen3.5 4B | Structured (sync) | Two-GPU with Qwen3.5 | +| `qwen35-4b-ebft-structured-async.yaml` | Qwen3.5 4B | Structured (async) | Two-GPU async with Qwen3.5 | +| `qwen35-9b-ebft-structured.yaml` | Qwen3.5 9B | Structured (sync) | Two-GPU structured for 9B model | diff --git a/docs/getting-started.qmd b/docs/getting-started.qmd index de059c397f..d7eac064fa 100644 --- a/docs/getting-started.qmd +++ b/docs/getting-started.qmd @@ -170,17 +170,26 @@ More details can be found in [Merging LoRA weights](inference.qmd#sec-merging). ## Next Steps {#sec-next-steps} -Now that you have the basics, you might want to: +Now that you have the basics, explore these guides based on what you want to do: -- Try different model architectures -- Experiment with hyperparameters -- Use more advanced training methods -- Scale up to larger models +**Choose your path:** -Check our other guides for details on these topics: +- [Choosing a Fine-Tuning Method](choosing_method.qmd) — SFT vs LoRA vs QLoRA vs GRPO vs DPO, with hardware recommendations -- [Configuration Guide](config-reference.qmd) - Full configuration options -- [Dataset Loading](dataset_loading.qmd) - Loading datasets from various sources -- [Dataset Formats](dataset-formats) - Working with different data formats -- [Multi-GPU Training](multi-gpu.qmd) -- [Multi-Node Training](multi-node.qmd) +**Core guides:** + +- [Dataset Loading](dataset_loading.qmd) — Loading datasets from various sources +- [Dataset Formats](dataset-formats) — Working with different data formats +- [Optimizations](optimizations.qmd) — Flash attention, gradient checkpointing, sample packing +- [Training Stability & Debugging](training_stability.qmd) — Monitoring metrics, fixing NaN, OOM debugging + +**Advanced training methods:** + +- [RLHF / Preference Learning](rlhf.qmd) — DPO, KTO, GRPO, EBFT +- [GRPO Training](grpo.qmd) — RL with custom rewards and vLLM generation +- [vLLM Serving](vllm_serving.qmd) — Setting up vLLM for GRPO + +**Scaling up:** + +- [Multi-GPU Training](multi-gpu.qmd) — DeepSpeed, FSDP, DDP +- [Multi-Node Training](multi-node.qmd) — Distributed training across machines diff --git a/docs/grpo.qmd b/docs/grpo.qmd new file mode 100644 index 0000000000..35631f1366 --- /dev/null +++ b/docs/grpo.qmd @@ -0,0 +1,611 @@ +--- +title: "GRPO Training" +description: "Group Relative Policy Optimization — a reinforcement learning method for training language models with verifiable reward functions." +order: 8 +--- + +## Overview + +Group Relative Policy Optimization (GRPO) is a reinforcement learning method that improves language models by generating multiple completions per prompt, scoring them with reward functions, and using the relative ranking within each group to compute advantage estimates. Unlike DPO, which requires pre-collected preference pairs, GRPO generates its own training data online and can work with any programmatic reward signal (math correctness, format compliance, code execution results, etc.). + +Use GRPO when you have a task with a verifiable reward signal and want the model to discover solution strategies on its own. Use DPO when you already have human preference data. Use SFT when you have gold-standard completions to imitate directly. + +Axolotl's GRPO implementation builds on TRL and adds async generation, streaming scoring, importance sampling correction, replay buffers, and multi-GPU scaling via FSDP and DeepSpeed. + + +## Architecture + +GRPO training uses a two-process architecture: a vLLM server for fast generation and a trainer process for scoring and gradient updates. + +``` +Terminal 1 (GPU 0) Terminal 2 (GPU 1) +┌──────────────────────┐ ┌──────────────────────────────────┐ +│ vLLM Server │ │ Trainer │ +│ │ HTTP │ │ +│ Serves base model │◄────────────►│ Background thread: │ +│ + LoRA adapter │ /generate │ Send prompts to vLLM │ +│ │ /set_lora │ Pad & collate completions │ +│ Punica kernels for │ │ │ +│ LoRA inference │ │ Main thread: │ +│ │ │ Score completions (rewards) │ +└──────────────────────┘ │ Compute policy log-probs │ + │ Calculate advantages │ + │ PPO-clip gradient update │ + │ Sync LoRA weights to vLLM │ + └──────────────────────────────────┘ +``` + +**Data flow for each training step:** + +1. The background thread sends prompts to vLLM, which generates `num_generations` completions per prompt. +2. The main thread scores completions using your reward functions. +3. Advantages are computed within each prompt group (group-relative normalization). +4. Policy log-probabilities are computed by running a forward pass on the training model. +5. The PPO-clip loss is computed and gradients are applied. +6. Periodically, LoRA adapter weights are synced back to vLLM so future generations reflect the updated policy. + +With async prefetch enabled, step 1 for the *next* batch runs concurrently with steps 2-6 for the *current* batch. + + +## Quick Start + +A GRPO training run requires three components: a YAML config, a reward module (Python file), and a running vLLM server. + +### 1. Write a reward module + +Create a file called `rewards.py` in your working directory: + +```python +# rewards.py +import re + + +def accuracy_reward(completions, answer, **kwargs) -> list[float]: + """Check if the completion contains the correct numerical answer.""" + rewards = [] + for completion, correct in zip(completions, answer): + text = completion[0]["content"] + # Extract the last number from the completion + numbers = re.findall(r"-?\d+(?:\.\d+)?", text) + predicted = numbers[-1] if numbers else "" + rewards.append(1.0 if predicted == str(correct) else 0.0) + return rewards + + +def format_reward(completions, **kwargs) -> list[float]: + """Reward completions that use a structured thinking format.""" + rewards = [] + for completion in completions: + text = completion[0]["content"] + has_think = "" in text and "" in text + has_answer = "" in text and "" in text + rewards.append(1.0 if has_think and has_answer else 0.0) + return rewards + + +def prompt_transform(cfg, *args, **kwargs): + """Convert GSM8K dataset rows into chat prompts.""" + def transform_fn(example, tokenizer=None): + label = example["answer"].split("####")[-1].strip().replace(",", "") + return { + "prompt": [ + {"role": "system", "content": "Solve the math problem. Show your reasoning in tags and your final numerical answer in tags."}, + {"role": "user", "content": example["question"]}, + ], + "answer": label, + } + return transform_fn, {"remove_columns": ["question"]} +``` + +### 2. Write the config + +Create `config.yaml`: + +```yaml +base_model: Qwen/Qwen2.5-1.5B-Instruct + +rl: grpo +chat_template: tokenizer_default + +vllm: + host: 0.0.0.0 + port: 8000 + gpu_memory_utilization: 0.85 + dtype: auto + max_model_len: 2048 + +adapter: lora +lora_r: 32 +lora_alpha: 64 +lora_target_linear: true + +trl: + use_vllm: true + use_data_producer: true + vllm_server_host: 0.0.0.0 + vllm_server_port: 8000 + vllm_server_timeout: 300 + vllm_lora_sync: true + num_generations: 8 + max_completion_length: 512 + temperature: 0.7 + reward_funcs: + - rewards.accuracy_reward + - rewards.format_reward + reward_weights: + - 1.0 + - 0.5 + +datasets: + - path: openai/gsm8k + name: main + type: rewards.prompt_transform + split: train + +skip_prepare_dataset: true +val_set_size: 0.0 +sequence_len: 512 +micro_batch_size: 2 +gradient_accumulation_steps: 4 +max_steps: 200 +learning_rate: 5.0e-6 +optimizer: adamw_torch_fused +lr_scheduler: cosine +warmup_steps: 10 + +bf16: true +flash_attention: true +gradient_checkpointing: true + +special_tokens: + pad_token: "<|endoftext|>" + +output_dir: ./grpo-output +logging_steps: 1 +``` + +### 3. Start vLLM and train + +```bash +# Terminal 1: Start vLLM server on GPU 0 +CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve config.yaml + +# Wait 30-90 seconds for model loading and CUDA graph capture + +# Terminal 2: Train on GPU 1 +CUDA_VISIBLE_DEVICES=1 axolotl train config.yaml +``` + +:::{.callout-tip} +Use `tmux` or separate terminal sessions to manage the two processes. The vLLM server must remain running for the entire training duration. +::: + + +## Custom Reward Functions + +### Function signature + +TRL calls reward functions with this signature: + +```python +def my_reward(completions, **kwargs) -> list[float]: +``` + +- `completions` is a list of single-element lists, where each element is a dict `{"role": "assistant", "content": "..."}`. So `completions[i][0]["content"]` gives you the text of the i-th completion. +- `**kwargs` contains all dataset columns that were *not* removed by the dataset transform. This is how you pass ground truth answers, metadata, or any other information to your reward function. +- Return a `list[float]` with the same length as `completions`. You may return `None` for individual elements to exclude them from aggregation. + +### Example: accuracy reward with answer extraction + +```python +def accuracy_reward(completions, answer, **kwargs) -> list[float]: + rewards = [] + for completion, correct_answer in zip(completions, answer): + text = completion[0]["content"] + # Extract answer from ... tags + match = re.search(r"(.*?)", text, re.DOTALL) + predicted = match.group(1).strip() if match else "" + rewards.append(1.0 if predicted == str(correct_answer) else 0.0) + return rewards +``` + +### Example: length penalty + +```python +def length_penalty(completions, **kwargs) -> list[float]: + """Penalize very short or very long completions.""" + rewards = [] + for completion in completions: + length = len(completion[0]["content"]) + if length < 50: + rewards.append(-0.5) + elif length > 2000: + rewards.append(-0.2) + else: + rewards.append(0.0) + return rewards +``` + +### Multiple rewards and weighting + +You can combine multiple reward functions with different weights: + +```yaml +trl: + reward_funcs: + - rewards.accuracy_reward + - rewards.format_reward + - rewards.length_penalty + reward_weights: + - 1.0 # accuracy is most important + - 0.5 # format compliance + - 0.1 # mild length preference +``` + +Rewards are combined by the `multi_objective_aggregation` strategy: + +- `sum_then_normalize` (default): weights and sums all rewards first, then normalizes across the group. +- `normalize_then_sum` (GDPO): normalizes each reward independently, then sums. This prevents one reward from dominating and is recommended when using multiple reward functions with different scales. + +```yaml +trl: + multi_objective_aggregation: normalize_then_sum +``` + +### Dataset transforms + +The dataset transform converts raw HuggingFace dataset rows into chat-format prompts: + +```python +def prompt_transform(cfg, *args, **kwargs): + def map_fn(example, tokenizer=None): + return { + "prompt": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": example["question"]}, + ], + # Keep 'answer' column for the reward function + "answer": example["answer"], + } + # Remove columns consumed by the transform; keep columns needed by rewards + return map_fn, {"remove_columns": ["question"]} +``` + +The transform returns a tuple of `(map_function, kwargs_dict)`. The `remove_columns` in the kwargs dict removes columns that are no longer needed. Columns that your reward functions reference via `**kwargs` (like `answer`) must *not* be removed. + +:::{.callout-warning} +The reward module must be importable from the directory where you run `axolotl train`. If your reward file is `rewards.py`, the import path is `rewards.accuracy_reward`. If it is inside a package `my_rewards/scoring.py`, use `my_rewards.scoring.accuracy_reward`. +::: + +### Reward models (neural network rewards) + +Instead of a Python function, you can pass a HuggingFace model path as a reward function. TRL will load it as a reward model and use its scalar output as the reward: + +```yaml +trl: + reward_funcs: + - OpenAssistant/reward-model-deberta-v3-large-v2 + - rewards.format_reward + reward_weights: + - 1.0 + - 0.3 +``` + +### Using math_verify + +The `math_verify` library provides robust mathematical answer verification but uses `signal.alarm()` internally, which only works in the main thread. If you use `math_verify` in a reward function, set `reward_num_workers` to use subprocess workers: + +```yaml +trl: + reward_num_workers: 4 +``` + +Each worker runs in its own subprocess with its own main thread, so `signal.alarm()` works correctly. + + +## vLLM Setup + +GRPO requires a running vLLM server for generation. For a complete guide on server modes, LoRA sync, weight synchronization, and restart procedures, see [vLLM Serving](vllm_serving.qmd). + +The minimal setup: + +```yaml +vllm: + host: 0.0.0.0 + port: 8000 + gpu_memory_utilization: 0.85 + +trl: + use_vllm: true + vllm_lora_sync: true # Recommended with LoRA — faster sync, no NCCL contention + vllm_sync_interval: 5 # Sync weights every 5 steps +``` + +```bash +CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve config.yaml # GPU 0: vLLM +CUDA_VISIBLE_DEVICES=1 axolotl train config.yaml # GPU 1: training +``` + +:::{.callout-warning} +vLLM must be restarted between experiments — stale weight syncs corrupt server state. See [Restart Requirements](vllm_serving.qmd#sec-restart). +::: + + +## Async Training Features + +Async GRPO overlaps generation and training to reduce wall-clock time. While the model trains on the current batch, the next batch is already being generated by vLLM. + +### Enabling async prefetch + +```yaml +trl: + use_data_producer: true + async_prefetch: true + prefetch_depth: 1 + vllm_sync_interval: 2 +``` + +- `use_data_producer: true` enables the data producer protocol (required for all async features). +- `async_prefetch: true` runs generation in a background thread. +- `prefetch_depth` controls how many batches to prefetch ahead (1 is usually sufficient). +- `vllm_sync_interval` controls how often LoRA weights are synced to vLLM (every N optimizer steps). Lower values mean fresher generations but more sync overhead. + +:::{.callout-tip} +Because the background thread generates with slightly stale model weights, async mode benefits from importance sampling correction (see next section). Enable `vllm_importance_sampling_correction: true` when using `async_prefetch: true`. +::: + +### Streaming partial batch + +Instead of scoring the entire batch at once, streaming mode scores one prompt group at a time. This reduces peak memory during scoring and enables finer-grained zero-advantage skipping. + +```yaml +trl: + streaming_partial_batch: true + streaming_min_groups: 1 +``` + +`streaming_min_groups` controls the minimum number of prompt groups scored per chunk. Setting it to 1 gives maximum granularity. + +### Zero-advantage batch skipping + +When all advantages in a micro-batch are zero (every completion in the group got the same reward), there is no learning signal. This feature skips the forward/backward pass entirely for such micro-batches. + +```yaml +trl: + skip_zero_advantage_batches: true # default +``` + +This is enabled by default and logged as `skipped_zero_adv_batches` in training metrics. It is a safety net, not a major optimization -- it only saves significant time when the model cannot solve any prompts in the batch. + +### Replay buffer + +The replay buffer caches rollout groups that had learning signal (non-zero reward variance) and replaces zero-signal groups in later batches. This improves data utilization when many prompts yield no reward variance. + +```yaml +trl: + replay_buffer_size: 100 + replay_recompute_logps: true +``` + +:::{.callout-warning} +When `replay_recompute_logps: false`, replayed data uses stale log-probabilities which creates an IS mismatch. Keep the default `true` unless you have a specific reason to disable it. +::: + +### Deferred re-rolling + +Prompts where the model gets zero reward for all generations are buffered and re-injected into later batches, when the model may have improved enough to produce useful completions. + +```yaml +trl: + reroll_start_fraction: 0.5 # Start re-rolling after 50% of training + reroll_max_groups: 1 # Max groups to replace per batch +``` + +Set `reroll_start_fraction: 1.0` to disable. This is most useful for tasks where the model starts weak but steadily improves. + +### Parallel reward workers + +Reward functions that use `signal.alarm()` (like `math_verify`) only work in the main thread. Parallel reward workers run each function in its own subprocess: + +```yaml +trl: + reward_num_workers: 4 +``` + +Work is sharded across workers by prompt group. For simple reward functions, a single worker is usually sufficient -- the overhead of IPC can exceed the computation time. + + +## Importance Sampling and Off-Policy Correction + +When using async prefetch, completions are generated from a slightly older policy. IS correction adjusts the gradient to account for this mismatch. + +```yaml +trl: + vllm_importance_sampling_correction: true + importance_sampling_level: token # 'token' recommended (especially with Liger kernel) + off_policy_mask_threshold: 0.5 # KL threshold — masks sequences that are too off-policy +``` + +Use `token` level IS. Sequence-level has numerical issues with Liger's chunked computation. The `off_policy_mask_threshold` (OPSM) is a safety net that drops sequences where KL divergence exceeds the threshold — 0.5 is a reasonable starting point. + +For detailed coverage of IS modes (`token_mask`, `token_truncate`, etc.), capping, and bias-corrected KL, see [vLLM Serving — IS Correction](vllm_serving.qmd#sec-weight-sync). + + +## Scaling + +### FP8 training + +FP8 quantization halves model VRAM usage with minimal impact on training quality. It does not significantly speed up computation for small models but allows larger models to fit in memory. + +```yaml +fp8: true +torch_compile: true +``` + +:::{.callout-warning} +FP8 requires patching for zero-padding edge cases. The `act_quant_kernel` can produce NaN when input is all zeros (padding positions). If you see NaN in grad norms, check whether your padding token embedding is non-zero. +::: + +### FSDP (Fully Sharded Data Parallel) + +FSDP distributes model parameters across multiple GPUs for training while vLLM runs on a separate GPU: + +```yaml +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_transformer_layer_cls_to_wrap: Qwen2DecoderLayer +gradient_checkpointing_kwargs: + use_reentrant: false +``` + +Launch with: + +```bash +# GPU 0: vLLM +CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve config.yaml + +# GPUs 0,1: Training (FSDP will use both visible GPUs) +CUDA_VISIBLE_DEVICES=0,1 axolotl train config.yaml +``` + +:::{.callout-warning} +`async_prefetch: true` can deadlock with FSDP because background threads perform unsynchronized FSDP collectives across ranks. With multi-GPU FSDP, only rank 0 generates in the background thread and results are broadcast to all ranks. If you still see hangs, set `async_prefetch: false`. +::: + +### DeepSpeed ZeRO-3 + +```yaml +deepspeed: deepspeed_configs/zero3_bf16.json +gradient_checkpointing_kwargs: + use_reentrant: true # Required -- non-reentrant causes CheckpointError with ZeRO-3 +``` + +:::{.callout-note} +DeepSpeed ZeRO-3 requires `use_reentrant: true` for gradient checkpointing. This is the opposite of the FSDP recommendation. Non-reentrant checkpointing causes tensor metadata mismatches during recomputation with ZeRO-3's parameter partitioning. +::: + +### Multi-GPU considerations + +| Concern | Recommendation | +|---------|---------------| +| vLLM GPU allocation | Dedicate one or more GPUs to vLLM; do not share with trainer GPUs | +| Weight sync contention | Use `vllm_lora_sync: true` to avoid NCCL contention between training and vLLM | +| FSDP + async | Use `async_prefetch: false` or rely on rank-0-only background generation | +| DeepSpeed + gradient checkpoint | Must use `use_reentrant: true` | +| OOM during scoring | Reduce `micro_batch_size` or `num_generations`. The logits tensor scales with `batch_size * vocab_size` | + + +## Monitoring and Debugging + +For detailed metric ranges, failure diagnosis, and OOM debugging, see [Training Stability & Debugging](training_stability.qmd). + +Quick health checks during GRPO training: + +- `rewards/*/mean` should be > 0.15 within 20 steps — if it stays at 0, test your reward function standalone +- `reward_std` should be > 0 on most steps — all-zero means no learning signal +- `entropy` in 0.05-0.5 — below 0.01 suggests mode collapse +- `grad_norm` in 0.001-1.0 — > 10 is unstable, 0.0 is expected when zero-advantage skip fires + +:::{.callout-tip} +Pipe training output to a log file: `axolotl train config.yaml 2>&1 | tee /tmp/training.log` +::: + + +## Configuration Reference + +All GRPO-specific options live under the `trl:` key in your config. Standard training options (`learning_rate`, `micro_batch_size`, etc.) are set at the top level as usual. + +### Core GRPO + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `use_vllm` | bool | `false` | Enable vLLM for generation | +| `vllm_mode` | `"server"` or `"colocate"` | `null` | vLLM deployment mode | +| `vllm_server_host` | str | `"0.0.0.0"` | vLLM server hostname | +| `vllm_server_port` | int | `8000` | vLLM server port | +| `vllm_server_timeout` | int | `null` | Timeout (seconds) for vLLM responses | +| `num_generations` | int | `null` | Completions generated per prompt | +| `generation_batch_size` | int | `null` | Number of unique prompts per generation step | +| `max_completion_length` | int | `null` | Maximum tokens per completion | +| `beta` | float | `null` | KL penalty coefficient | +| `num_iterations` | int | `null` | Iterations per batch (mu in the GRPO paper) | +| `epsilon` | float | `null` | PPO clipping lower bound | +| `epsilon_high` | float | `null` | PPO clipping upper bound | +| `loss_type` | str | `null` | Loss formulation: `grpo`, `bnpo`, or `dr_grpo` | +| `scale_rewards` | bool | `true` | Normalize rewards by standard deviation | +| `mask_truncated_completions` | bool | `false` | Exclude truncated completions from loss | + +### Reward functions + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `reward_funcs` | list[str] | `null` | Import paths to reward functions or HF model IDs | +| `reward_weights` | list[float] | `null` | Relative weights for each reward function | +| `multi_objective_aggregation` | str | `null` | `"sum_then_normalize"` (GRPO) or `"normalize_then_sum"` (GDPO) | +| `rollout_func` | str | `null` | Import path to custom rollout function for OpenEnv-style tasks | + +### Generation parameters + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `temperature` | float | `null` | Sampling temperature | +| `top_p` | float | `null` | Nucleus sampling probability | +| `top_k` | int | `null` | Top-k sampling | +| `min_p` | float | `null` | Minimum probability threshold | +| `repetition_penalty` | float | `null` | Penalty for repeated tokens | +| `generation_kwargs` | dict | `null` | Additional vLLM SamplingParams (e.g., `stop_token_ids`) | +| `chat_template_kwargs` | dict | `null` | Chat template kwargs (e.g., `{enable_thinking: false}`) | +| `vllm_guided_decoding_regex` | str | `null` | Regex constraint for guided decoding | + +### Async pipeline + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `use_data_producer` | bool | `false` | Enable data producer protocol (required for async features) | +| `async_prefetch` | bool | `false` | Generate next batch in background thread | +| `prefetch_depth` | int | `null` | Number of batches to prefetch ahead | +| `vllm_sync_interval` | int | `null` | Sync LoRA weights to vLLM every N steps | +| `vllm_lora_sync` | bool | `false` | Use filesystem LoRA sync instead of NCCL merge | +| `streaming_partial_batch` | bool | `null` | Score prompt groups incrementally | +| `streaming_min_groups` | int | `null` | Minimum groups per streaming chunk | +| `skip_zero_advantage_batches` | bool | `true` | Skip micro-batches with zero learning signal | +| `reward_num_workers` | int | `1` | Subprocess workers for reward computation | +| `vllm_enable_sleep_mode` | bool | `null` | Offload vLLM weights when idle (colocate mode) | + +### Importance sampling + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `vllm_importance_sampling_correction` | bool | `null` | Enable IS correction for async distribution shift | +| `importance_sampling_level` | `"token"` or `"sequence"` | `null` | Granularity of IS ratios. Use `token` with Liger | +| `vllm_importance_sampling_mode` | str | `null` | `token_mask`, `token_truncate`, `sequence_mask`, or `sequence_truncate` | +| `vllm_importance_sampling_cap` | float | `null` | Cap C for IS ratio clipping/masking | +| `off_policy_mask_threshold` | float | `null` | KL threshold for off-policy sequence masking (OPSM) | +| `use_bias_correction_kl` | bool | `null` | Apply IS correction to KL divergence term | + +### Replay and re-roll + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `replay_buffer_size` | int | `0` | Max cached high-signal groups. 0 = disabled | +| `replay_recompute_logps` | bool | `true` | Recompute log-probs for replayed data with current model | +| `reroll_start_fraction` | float | `1.0` | Start re-rolling failed prompts after this fraction of training. 1.0 = disabled | +| `reroll_max_groups` | int | `1` | Max prompt groups to replace with re-rolls per batch | + +### Reference model + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `sync_ref_model` | bool | `false` | Periodically sync reference model with training model | +| `ref_model_mixup_alpha` | float | `0.9` | EMA coefficient for reference model sync | +| `ref_model_sync_steps` | int | `64` | Sync reference model every N steps | + +### Logging + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `log_completions` | bool | `false` | Log sample completions to W&B | +| `num_completions_to_print` | int | `null` | Number of completions to print per step | +| `use_liger_loss` | bool | `null` | Use Liger fused kernel for GRPO loss (reduces VRAM) | diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 603b584668..514c1c034f 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -16,11 +16,13 @@ feedback. Various methods include, but not limited to: - [Identity Preference Optimization (IPO)](#ipo) - [Kahneman-Tversky Optimization (KTO)](#kto) - [Odds Ratio Preference Optimization (ORPO)](#orpo) -- [Group Relative Policy Optimization (GRPO)](#grpo) +- [Group Relative Policy Optimization (GRPO)](#grpo) — see also the [GRPO deep dive](grpo.qmd) for async features, custom rewards, and scaling - [Group Reward-Decoupled Policy Optimization (GDPO)](#gdpo) -- [Energy-Based Fine-Tuning (EBFT)](#ebft) +- [Energy-Based Fine-Tuning (EBFT)](#ebft) — see also the [EBFT guide](ebft.qmd) for detailed mode comparisons and configuration - [NeMo Gym Integration](#nemo-gym-integration) +For help choosing between these methods, see [Choosing a Fine-Tuning Method](choosing_method.qmd). + ## RLHF using Axolotl @@ -515,7 +517,7 @@ The input format is a simple JSON input with customizable fields based on the ab ### GRPO ::: {.callout-tip} -Check out our [GRPO cookbook](https://github.com/axolotl-ai-cloud/grpo_code). +Check out our [GRPO cookbook](https://github.com/axolotl-ai-cloud/grpo_code). For a comprehensive guide covering async training, custom rewards, importance sampling, and scaling, see the [GRPO deep dive](grpo.qmd). ::: In the latest GRPO implementation, `vLLM` is used to significantly speedup trajectory generation during training. In this example, we're using 4 GPUs - 2 for training, and 2 for vLLM: @@ -923,7 +925,7 @@ gradient_checkpointing_kwargs: CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve config.yaml # Terminal 2: Train on GPUs 0,1 -CUDA_VISIBLE_DEVICES=0,1 accelerate launch --num_processes 2 -m axolotl.cli.train config.yaml +CUDA_VISIBLE_DEVICES=0,1 axolotl train config.yaml ``` ::: {.callout-important} @@ -1039,7 +1041,11 @@ simpo_gamma: 0.5 # default in CPOTrainer This method uses the same dataset format as [DPO](#dpo). -### EBFT +### EBFT {#ebft} + +::: {.callout-tip} +For a detailed guide on EBFT modes, feature extraction, and configuration, see the [EBFT guide](ebft.qmd). +::: EBFT (Energy-Based Fine-Tuning) fine-tunes language models by optimizing a **feature-matching loss** rather than relying on external reward functions. A frozen copy of the model extracts embeddings from both generated and ground-truth completions, and the generator is updated via REINFORCE to match the ground-truth feature moments. diff --git a/docs/training_stability.qmd b/docs/training_stability.qmd new file mode 100644 index 0000000000..e2cd79f89e --- /dev/null +++ b/docs/training_stability.qmd @@ -0,0 +1,399 @@ +--- +title: "Training Stability & Debugging" +order: 15 +description: "Guide to monitoring, debugging, and stabilizing training runs in axolotl" +--- + +This guide covers practical techniques for monitoring training health, diagnosing instability, and resolving common failures in both supervised fine-tuning (SFT) and reinforcement learning (GRPO/EBFT) workflows. + +## Monitoring Training + +### Key Metrics for SFT + +Every SFT run should be monitored through at least these four metrics: + +| Metric | What It Tells You | Healthy Range | +|--------|-------------------|---------------| +| `train/loss` | How well the model fits training data | Decreasing; typically 0.5--2.0 for chat fine-tuning | +| `eval/loss` | Generalization performance | Tracks train loss with small gap; divergence signals overfitting | +| `grad_norm` | Gradient magnitude | 0.1--10.0; spikes above 100 indicate instability | +| `learning_rate` | Current LR from scheduler | Should follow expected schedule (warmup then decay) | + +::: {.callout-tip} +## Set Up Logging Early +Enable W&B or TensorBoard from the start. Debugging a failed run without metrics is guesswork. + +```yaml +wandb_project: my-project +wandb_run_id: # optional, for resuming +logging_steps: 1 +``` +::: + +### Key Metrics for RL (GRPO) + +GRPO training logs a richer set of metrics. These are the critical ones: + +| Metric | Healthy Range | Red Flag | +|--------|---------------|----------| +| `rewards//mean` | > 0.15 within 20 steps | Stays at 0 -- reward function is broken or task is too hard | +| `reward_std` | > 0 on most steps | Always 0 -- no learning signal (all completions get the same reward) | +| `frac_reward_zero_std` | < 0.8 | 1.0 on every step -- zero-advantage skip fires constantly, no gradient updates | +| `grad_norm` | 0.001--1.0 | 0.0 is acceptable occasionally (zero-adv skip); > 10.0 is unstable | +| `entropy` | 0.05--0.5 | < 0.01 suggests mode collapse; > 1.0 suggests the model is not converging | +| `kl` | 0.0--0.5 | > 2.0 suggests policy has diverged too far from reference | +| `sampling/sampling_logp_difference/mean` | < 0.1 | > 1.0 means policy has diverged far from vLLM server weights | +| `sampling/importance_sampling_ratio/min` | > 0.1 | Near 0 indicates stale off-policy data; increase `vllm_sync_interval` | +| `clip_ratio/region_mean` | < 0.1 | > 0.3 means PPO clipping is too aggressive | +| `completions/mean_length` | Task-dependent | Monotonically increasing to max length suggests reward hacking | +| `completions/clipped_ratio` | < 0.3 | > 0.8 means most completions hit `max_completion_length` -- increase it | + +::: {.callout-note} +## EBFT-Specific Metrics +For EBFT training, also monitor `ebft/alignment` (should trend upward, healthy 0.3--0.9), `ebft/diversity` (healthy 0.01--0.1; > 1.0 indicates mode collapse), and `ebft/cfm_loss` (should trend downward, < 10). +::: + +## SFT Stability + +### Loss Plateau + +**Symptom**: Loss stops decreasing early in training, well above expected values. + +**Causes and fixes**: + +- **Learning rate too low**: Increase by 2--5x. Typical ranges: full fine-tune 1e-5 to 5e-5, LoRA 1e-4 to 3e-4. +- **Insufficient warmup**: Set `warmup_steps` to 5--10% of total steps. Too-aggressive learning at the start can push the model into a flat region. +- **Data quality**: Check that labels are correctly masked. Use `axolotl preprocess` and inspect tokenized samples to confirm only the target tokens are trainable. +- **Weight decay too high**: Default 0.01 is usually fine. Values above 0.1 can suppress learning in LoRA. + +### Loss Spikes + +**Symptom**: Loss suddenly jumps by 2--10x then (possibly) recovers. + +**Causes and fixes**: + +- **Bad data samples**: A single malformed or extremely long example can cause a spike. Enable `sample_packing: false` temporarily and check if spikes correlate with specific batches. +- **Learning rate too high**: Reduce by 2--5x, or increase warmup. +- **Gradient accumulation mismatch**: Effective batch size = `micro_batch_size * gradient_accumulation_steps * num_gpus`. Very large effective batch sizes amplify gradient noise. +- **Mixed precision issues**: With `bf16: true`, some operations can lose precision. If spikes are severe, try `fp32` for diagnosis. + +### Overfitting + +**Symptom**: Train loss keeps decreasing but eval loss starts increasing. + +**Fixes**: + +- Increase `val_set_size` (e.g., 0.05) and monitor `eval/loss`. +- Reduce `num_epochs` or `max_steps`. +- Increase `weight_decay` (try 0.01--0.1). +- Use a smaller LoRA rank (`lora_r`). Typical values: 8--32. +- Increase dropout: `lora_dropout: 0.05`. + +## RL/GRPO Stability + +### Reward Never Increases + +If `rewards/*/mean` stays at 0 for more than 20 steps: + +1. **Test reward function standalone**: Run it outside training with known inputs to verify it returns nonzero values. + ```bash + cd experiments && python -c "import my_rewards; print(my_rewards.accuracy_reward(...))" + ``` +2. **Check dataset columns**: The reward function receives `**kwargs` containing dataset columns. Verify the columns it needs (e.g., `answer`) are not removed by the dataset transform. +3. **Check completion content**: Enable `log_completions: true` in the `trl:` config and inspect logged completions in W&B. If completions are empty or incoherent, the model may be too weak for the task. +4. **Verify vLLM is serving the right model**: Hit the vLLM health endpoint and confirm the model name matches your config. + +### Entropy Collapse (Mode Collapse) + +**Symptom**: `entropy` drops below 0.01; all completions become nearly identical. + +**Fixes**: + +- Increase `temperature` in generation kwargs (try 0.8--1.0). +- Reduce learning rate. +- Add a KL penalty term (`beta` parameter in GRPO config). +- Check that `num_generations` is sufficient (16+ gives better advantage estimates). + +### IS Ratio Divergence + +**Symptom**: `sampling/importance_sampling_ratio/min` drops near 0, or `sampling/sampling_logp_difference/mean` exceeds 1.0. + +This means the policy has diverged significantly from the weights used by vLLM for generation. The importance sampling correction becomes unreliable. + +**Fixes**: + +- Decrease `vllm_sync_interval` (sync weights more often). +- Enable `off_policy_mask_threshold` (e.g., 0.5) to mask stale off-policy samples. +- Use `importance_sampling_level: token` for finer-grained correction. + +### Gradient Norm Instability + +**Symptom**: `grad_norm` oscillates wildly or exceeds 10.0 regularly. + +**Fixes**: + +- Enable gradient clipping: `max_grad_norm: 1.0` (default in most configs). +- Reduce learning rate. +- Increase `gradient_accumulation_steps` to smooth out noisy batches. +- Check for NaN issues (see next section). + +## NaN and Inf Handling + +### Common Causes + +| Cause | Where It Manifests | Detection | +|-------|-------------------|-----------| +| FP8 zero-scale division | Forward pass logits | `grad_norm: nan`, loss becomes NaN immediately | +| Gradient explosion | Backward pass | `grad_norm` spikes to inf, then loss goes NaN | +| Bad data (empty sequences) | Logprob computation | NaN in specific batches only | +| Numerical overflow in log-softmax | Loss computation | Large negative logprobs cause exp() overflow | + +### FP8-Specific NaN Issues + +FP8 quantization (`fp8: true`) can produce NaN when the activation quantization kernel divides by `max(abs(x)) / 448`. If the input tensor is all zeros (e.g., padding positions), the scale becomes 0, causing division by zero. + +**Fixes applied in axolotl**: + +- The `act_quant_kernel` has a zero-guard: `s = tl.where(s == 0, 1.0, s)`. +- A safety net `nan_to_num(logits, nan=0.0)` is applied in `_get_per_token_logps_and_entropies`. +- Embedding padding is zero-padded for FP8 compatibility. + +::: {.callout-important} +## After Modifying Triton Kernels +If you patch any Triton JIT kernel (e.g., the FP8 quantization kernels in transformers), you must clear the Triton cache for changes to take effect: + +```bash +rm -rf ~/.triton/cache +``` +::: + +### General NaN Debugging Steps + +1. **Enable anomaly detection** (slow, but pinpoints the source): + ```python + torch.autograd.set_detect_anomaly(True) + ``` +2. **Check grad_norm**: If it goes to NaN, the backward pass is the problem. If loss is NaN but grad_norm was fine on the previous step, the forward pass is the problem. +3. **Reduce to single GPU, single batch**: Eliminate distributed training variables. +4. **Inspect data**: Print the batch that triggers NaN. Look for empty sequences, extreme token IDs, or unexpected padding patterns. + +## OOM Debugging + +Out-of-memory errors are the most common training failure. Use this systematic approach, from least to most disruptive: + +### Step 1: Reduce Batch Size + +The single highest-impact change. VRAM scales roughly linearly with batch size. + +```yaml +micro_batch_size: 1 # Start here +gradient_accumulation_steps: 16 # Increase to maintain effective batch size +``` + +For GRPO specifically, the logits tensor for policy logprob computation can be very large. `batch_size * num_generations * seq_len * vocab_size` in bf16. For example, with `num_generations: 16` and `micro_batch_size: 8`, the logits tensor alone is: + +``` +8 * 16 * 2048 * 151936 * 2 bytes = ~75 GB (way too large) +``` + +Reduce `micro_batch_size` to 2--4 for GRPO. + +### Step 2: Enable Gradient Checkpointing + +Trades compute for memory by recomputing activations during the backward pass instead of storing them. + +```yaml +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false # Recommended default +``` + +::: {.callout-warning} +## Reentrant Checkpointing Exceptions +Some configurations require `use_reentrant: true`: + +- DeepSpeed ZeRO-3 (non-reentrant causes `CheckpointError`) +- EBFT strided mode with flex_attention +::: + +### Step 3: Use Quantization + +Load the base model in reduced precision: + +```yaml +# 4-bit QLoRA +adapter: qlora +load_in_4bit: true + +# 8-bit +load_in_8bit: true + +# FP8 (saves ~50% model VRAM, same compute speed as bf16) +fp8: true +``` + +### Step 4: Reduce Sequence Length + +```yaml +sequence_len: 1024 # Down from 2048 or 4096 +``` + +For GRPO, also reduce `max_completion_length`. Memory scales quadratically with sequence length when using standard attention. + +### Step 5: Use Flash Attention + +Reduces attention memory from O(n^2) to O(n): + +```yaml +flash_attention: true +``` + +### Step 6: Offload with DeepSpeed + +For extreme cases, offload optimizer states or parameters to CPU: + +```yaml +deepspeed: deepspeed_configs/zero3_bf16.json +``` + +### Diagnosing the Specific Culprit + +Use the `profiler_steps` config option to capture GPU memory snapshots: + +```yaml +profiler_steps: [1, 2] +``` + +This generates PyTorch profiler traces you can inspect to see exactly which tensor allocation caused the OOM. + +## Common Errors + +| Error Message | Likely Cause | Fix | +|---------------|-------------|-----| +| `exitcode: -9` | System RAM exhaustion | Reduce dataset size, `dataset_num_proc`, or number of data workers | +| `exitcode: -7` (DeepSpeed) | DeepSpeed version issue | `pip install -U deepspeed` | +| `CUDA out of memory` | GPU VRAM exhaustion | Follow OOM debugging steps above | +| `RuntimeError: NCCL communicator was aborted` | GPU communication failure | See [NCCL docs](nccl.qmd); check `NCCL_DEBUG=INFO` output | +| `ValueError: Asking to pad but the tokenizer does not have a padding token` | Missing pad token | Add `special_tokens: { pad_token: "<\|endoftext\|>" }` to config | +| `'DummyOptim' object has no attribute 'step'` | DeepSpeed on single GPU | Remove `deepspeed:` section from config | +| `unable to load strategy X` then `None is not callable` | Reward module not importable | Run `cd experiments && python -c "import my_rewards"` to check | +| `generation_batch_size not divisible by num_generations` | micro_batch_size too small | Set `micro_batch_size >= num_generations` and make it divisible | +| `'weight' must be 2-D` | FSDP1 flattened parameters | Use `fsdp_version: 2` or skip `unwrap_model` when FSDP is enabled | +| `CheckpointError` (tensor count mismatch) | Non-reentrant checkpointing + ZeRO-3 or flex_attention | Set `use_reentrant: true` in `gradient_checkpointing_kwargs` | +| `BFloat16` TypeError during weight sync | NumPy does not support bf16 | Fixed in axolotl's `weight_serde.py` (auto bf16 to fp16 conversion) | +| `Content end boundary is before start boundary` | Chat template parsing issue | Check `eos_token` matches template; file a GitHub issue if persistent | +| `CAS service error` during data processing | HuggingFace XET issue | Set `export HF_HUB_DISABLE_XET=1` | +| Training hangs (multi-GPU) | FSDP + async prefetch deadlock | Set `async_prefetch: false` with FSDP | + +## Profiling + +### PyTorch Profiler + +Axolotl supports PyTorch profiler integration via the config: + +```yaml +profiler_steps: [1, 2, 3] +``` + +This captures profiler traces for the specified steps. View them in TensorBoard: + +```bash +tensorboard --logdir output_dir/runs +``` + +Or open the `.json` trace file in `chrome://tracing`. + +### CUDA Memory Snapshots + +For detailed memory analysis, use PyTorch's memory snapshot API. Add this to your training script or use it interactively: + +```python +import torch + +# Enable memory history tracking +torch.cuda.memory._record_memory_history() + +# ... run your training step ... + +# Save snapshot +torch.cuda.memory._dump_snapshot("memory_snapshot.pickle") +``` + +Visualize with PyTorch's memory visualizer: + +```bash +python -m torch.cuda.memory._viz memory_snapshot.pickle +``` + +### Quick GPU Memory Check + +During training, monitor GPU utilization in a separate terminal: + +```bash +watch -n 1 nvidia-smi +``` + +For programmatic access within axolotl, the logged metrics `memory/max_alloc` and `memory/max_reserved` come from `torch.cuda.max_memory_allocated()` and `torch.cuda.max_memory_reserved()`. Note these report PyTorch's view of memory, which may differ from `nvidia-smi` (see [FAQ](faq.qmd)). + +## W&B and Logging + +### Enabling Logging + +```yaml +wandb_project: my-project +wandb_entity: my-team # optional +wandb_run_id: run-123 # optional, for resuming +wandb_name: experiment-name # optional +logging_steps: 1 # log every step (recommended for RL) +``` + +### Debug Logging + +For detailed axolotl-internal debug output: + +```bash +AXOLOTL_LOG_LEVEL=DEBUG axolotl train config.yaml 2>&1 | tee /tmp/training.log +``` + +::: {.callout-tip} +## Always Log to a File +Pipe training output to a log file so you can inspect it after the run: + +```bash +axolotl train config.yaml 2>&1 | tee /tmp/my_run.log +``` +::: + +### What Axolotl Logs + +**SFT metrics** (logged every `logging_steps`): + +- `train/loss`, `eval/loss` -- training and validation loss +- `train/grad_norm` -- gradient L2 norm (before clipping) +- `train/learning_rate` -- current learning rate +- `memory/max_alloc`, `memory/max_reserved` -- peak GPU memory + +**GRPO/RL metrics** (logged every step): + +- `rewards//mean`, `rewards//std` -- per-reward-function statistics +- `reward`, `reward_std` -- aggregated reward across all reward functions +- `frac_reward_zero_std` -- fraction of prompt groups where all completions got the same reward +- `completions/mean_length`, `completions/min_length`, `completions/max_length` -- completion token lengths +- `completions/clipped_ratio` -- fraction of completions that hit the max length +- `completions/mean_terminated_length`, `completions/min_terminated_length`, `completions/max_terminated_length` -- lengths of naturally terminated completions +- `kl` -- KL divergence between policy and reference +- `entropy` -- policy entropy (measure of output diversity) +- `clip_ratio/region_mean`, `clip_ratio/low_mean`, `clip_ratio/high_mean` -- PPO clipping statistics +- `sampling/sampling_logp_difference/mean`, `sampling/sampling_logp_difference/max` -- log-probability difference between policy and sampling distribution +- `sampling/importance_sampling_ratio/min`, `sampling/importance_sampling_ratio/mean`, `sampling/importance_sampling_ratio/max` -- IS ratio statistics for off-policy correction +- `num_tokens` -- total tokens processed + +### Reading W&B Charts + +For a healthy GRPO run, expect to see: + +1. **`reward/mean`**: Gradual upward trend. May start near 0 and reach 0.3--0.8 depending on task difficulty. Not monotonic -- fluctuations are normal. +2. **`entropy`**: Gradual decrease from initial values (often 0.3--0.6) as the model becomes more confident. Should not collapse to near-zero. +3. **`grad_norm`**: Mostly in the 0.001--1.0 range. Occasional 0.0 values are fine (zero-advantage skip). Persistent values above 10.0 need investigation. +4. **`kl`**: Starts near 0 and grows slowly. If it shoots up rapidly, the policy is diverging from the reference. +5. **`completions/mean_length`**: Should reflect the task's natural answer length. If it steadily increases to `max_completion_length`, the model may be reward-hacking by generating longer outputs. diff --git a/docs/vllm_serving.qmd b/docs/vllm_serving.qmd new file mode 100644 index 0000000000..642b723401 --- /dev/null +++ b/docs/vllm_serving.qmd @@ -0,0 +1,318 @@ +--- +title: "vLLM Serving for GRPO Training" +description: "How to configure and run vLLM as a generation backend for GRPO reinforcement learning in Axolotl." +format: + html: + toc: true + toc-depth: 3 + number-sections: true +execute: + enabled: false +--- + +## Overview {#sec-overview} + +GRPO (Group Relative Policy Optimization) trains a language model by generating completions, scoring them with reward functions, and updating the policy to favor higher-reward outputs. The generation step is the bottleneck: producing thousands of tokens per training step with the policy model is slow using standard HuggingFace generation. + +Axolotl uses [vLLM](https://github.com/vllm-project/vllm) as a high-throughput generation backend. vLLM runs as a separate process (either on a dedicated GPU or colocated on the training GPU) and serves completions via an HTTP API. The trainer sends prompts to vLLM, receives completions, scores them, and performs gradient updates. + +``` +┌──────────────────────┐ HTTP ┌──────────────────────┐ +│ Trainer (GPU 1) │ ───────────────── │ vLLM Server (GPU 0)│ +│ │ prompts/compls │ │ +│ - Policy model │ ◄──────────────── │ - Same base model │ +│ - Reward scoring │ │ - Fast generation │ +│ - Gradient updates │ weight sync │ - LoRA adapter │ +│ - LoRA adapter │ ─────────────────►│ (periodically │ +│ │ (every N steps) │ updated) │ +└──────────────────────┘ └──────────────────────┘ +``` + +::: {.callout-important} +vLLM must serve the **same base model** specified in your training config. If the models do not match, weight synchronization will silently produce incorrect results. +::: + +## Server Mode {#sec-server-mode} + +Server mode runs vLLM as an external process on dedicated GPU(s). This is the recommended configuration for most setups. + +### Starting the Server + +Use the `axolotl vllm-serve` command with your training config: + +```bash +# Terminal 1: Start vLLM on GPU 0 +CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve grpo_config.yaml +``` + +```bash +# Terminal 2: Start training on GPU 1 +CUDA_VISIBLE_DEVICES=1 axolotl train grpo_config.yaml +``` + +The server reads vLLM settings from the `vllm:` section of your config and starts an HTTP server (default: `http://0.0.0.0:8000`). + +::: {.callout-tip} +Use `tmux` or `screen` to manage the vLLM server process. Typical startup time is 30-90 seconds depending on model size and whether CUDA graphs are captured. +::: + +### Minimal Server Config + +```yaml +base_model: Qwen/Qwen2.5-1.5B-Instruct + +vllm: + host: 0.0.0.0 + port: 8000 + gpu_memory_utilization: 0.85 + dtype: auto + max_model_len: 4096 + +rl: grpo +trl: + use_vllm: true + vllm_server_host: 0.0.0.0 + vllm_server_port: 8000 + vllm_server_timeout: 300 +``` + +### Multi-GPU vLLM + +For larger models, use tensor parallelism across multiple GPUs: + +```yaml +vllm: + tensor_parallel_size: 2 + gpu_memory_utilization: 0.85 +``` + +```bash +# vLLM on GPUs 2,3; training on GPUs 0,1 +CUDA_VISIBLE_DEVICES=2,3 axolotl vllm-serve grpo_config.yaml +CUDA_VISIBLE_DEVICES=0,1 axolotl train grpo_config.yaml --num-processes 2 +``` + +::: {.callout-note} +Due to how TRL maps vLLM device indices, the vLLM instance should use the **last** N GPUs (highest device indices), while training uses the first N. +::: + +## Colocate Mode {#sec-colocate-mode} + +Colocate mode runs vLLM on the same GPU as the trainer. This is useful when you only have a single GPU. + +```yaml +trl: + use_vllm: true + vllm_mode: colocate + vllm_enable_sleep_mode: true +``` + +With `vllm_enable_sleep_mode: true`, vLLM offloads its VRAM allocation when not actively generating, freeing memory for training. When the trainer needs new completions, vLLM wakes up and reclaims VRAM. + +::: {.callout-warning} +Colocate mode is significantly slower than server mode because generation and training cannot overlap. The GPU alternates between the two workloads. This mode is practical only for smaller models (up to ~3B on a 24 GB GPU). +::: + +**When to use colocate mode:** + +- You have exactly one GPU +- The model fits in memory with both vLLM and training active (with sleep mode), or is small enough to time-share +- You accept the performance tradeoff for simpler setup (no separate vLLM process to manage) + +**When to use server mode:** + +- You have two or more GPUs +- You want maximum throughput (generation overlaps with training via async prefetch) +- You are running larger models (7B+) + +## LoRA Sync {#sec-lora-sync} + +LoRA sync is the recommended weight synchronization method when training with LoRA adapters. Instead of merging adapter weights into the base model and broadcasting the full merged weights over NCCL, it saves only the LoRA adapter files to the filesystem and tells vLLM to load them natively. + +### How It Works + +1. The trainer calls `model.save_pretrained()` to write the LoRA adapter weights to a temporary directory +2. The trainer sends an HTTP POST to `/set_lora_adapter/` on the vLLM server +3. vLLM loads the adapter using its native LoRA support (Punica kernels) +4. Generation uses the updated adapter on the next request + +### Benefits + +- **Smaller sync payload**: Transfers ~40 MB of LoRA weights instead of ~1.4 GB+ of merged model weights (for a typical 0.5-3B model) +- **No NCCL communicator**: Eliminates the need for a cross-GPU NCCL communication channel, removing GPU contention between vLLM generation and weight sync +- **Faster sync**: ~200 ms per sync vs. 350 ms to 5+ seconds for NCCL merge sync +- **Simpler multi-GPU**: No need to set up NCCL groups between trainer and vLLM processes + +### Configuration + +```yaml +adapter: lora +lora_r: 32 +lora_alpha: 64 +lora_target_linear: true + +trl: + vllm_lora_sync: true # Enables LoRA sync mode + vllm_sync_interval: 5 # Sync every 5 training steps +``` + +Setting `vllm_lora_sync: true` automatically selects the LoRA-aware vLLM serve script (`axolotl.scripts.vllm_serve_lora`). You do not need to set `vllm.serve_module` manually. + +::: {.callout-important} +LoRA sync requires that you are training with a LoRA adapter (`adapter: lora` or `adapter: qlora`). It is not applicable to full fine-tuning. +::: + +## Weight Synchronization {#sec-weight-sync} + +During GRPO training, the policy model on the trainer is continuously updated via gradient steps. The vLLM server, however, still holds the old weights. Periodically, the trainer must push updated weights to vLLM so that future generations reflect the improved policy. + +### Sync Interval + +The `vllm_sync_interval` parameter controls how often weights are synced: + +```yaml +trl: + vllm_sync_interval: 5 # Sync every 5 optimizer steps +``` + +**Tradeoffs:** + +- **Lower interval** (e.g., 1-3): Fresher generations, better on-policy data, but more sync overhead per step +- **Higher interval** (e.g., 5-10): Less overhead, but generations become increasingly off-policy between syncs +- **Recommended**: 3-5 for most setups. Axolotl includes importance sampling correction (`vllm_importance_sampling_correction: true`) to handle mild distribution mismatch from stale vLLM weights. + +### Sync Methods + +| Method | Config | Payload | Mechanism | Typical Time | +|--------|--------|---------|-----------|-------------| +| **LoRA sync** | `vllm_lora_sync: true` | LoRA adapter only (~40 MB) | Filesystem + HTTP | ~200 ms | +| **NCCL merge sync** | Default (no lora_sync) | Full merged weights (~1.4 GB+) | HTTP trigger + NCCL broadcast | 350 ms - 5 s | + +::: {.callout-tip} +If you are training with LoRA (which is recommended for GRPO), always enable `vllm_lora_sync: true`. The performance difference is substantial, especially as training progresses and NCCL contention increases. +::: + +### Importance Sampling Correction + +When vLLM weights are stale (between syncs), the generated data is slightly off-policy. Axolotl can correct for this: + +```yaml +trl: + vllm_importance_sampling_correction: true + importance_sampling_level: token # 'token' or 'sequence' + off_policy_mask_threshold: 0.5 # KL threshold for masking stale sequences +``` + +- **Token-level IS** is recommended when using Liger kernel (sequence-level has numerical issues with chunked computation) +- **Off-policy sequence masking (OPSM)** drops sequences that have diverged too far from the current policy, providing a safety net against stale data + +## Restart Requirements {#sec-restart} + +::: {.callout-warning} +**vLLM must be restarted between training runs.** Weight syncs from a previous run leave the server in a corrupted state. If you start a new training run against a stale vLLM server, the model may fail to learn. +::: + +### When to Restart + +- Before every new training experiment +- After a training run crashes or is interrupted +- If you change the base model in your config + +### How to Restart + +Killing vLLM reliably requires terminating both the main process and its background EngineCore subprocess: + +```bash +# Kill all vLLM-related processes +pkill -9 -f "vllm|EngineCore" + +# Verify GPU memory is freed +nvidia-smi + +# Restart the server +CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve grpo_config.yaml +``` + +::: {.callout-tip} +A single `kill` often does not fully stop vLLM. Always use `kill -9` and verify with `nvidia-smi` that GPU memory has been released before restarting. +::: + +### Health Check + +The vLLM server exposes a health endpoint. Wait for it to return 200 before starting training: + +```bash +# For the LoRA serve script (trailing slash required) +curl http://localhost:8000/health/ + +# For the default TRL serve script +curl http://localhost:8000/health +``` + +## Configuration Reference {#sec-config-reference} + +### vLLM Server Options (`vllm:` section) + +These control the vLLM server process started by `axolotl vllm-serve`. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `host` | str | `0.0.0.0` | Host address for the vLLM server | +| `port` | int | `8000` | Port for the vLLM server | +| `device` | str | `auto` | Device to use for vLLM | +| `tensor_parallel_size` | int | `None` | Number of GPUs for tensor parallelism | +| `data_parallel_size` | int | `None` | Number of data parallel replicas | +| `gpu_memory_utilization` | float | `0.9` | Fraction of GPU memory for vLLM (0.0-1.0) | +| `dtype` | str | `auto` | Data type (`auto`, `float16`, `bfloat16`) | +| `max_model_len` | int | `None` | Maximum model context length. Set explicitly if the default is too large for your GPU | +| `enable_prefix_caching` | bool | `None` | Enable prefix caching for repeated prompt prefixes | +| `enable_reasoning` | bool | `None` | Enable reasoning mode for models with thinking tokens | +| `reasoning_parser` | str | `None` | Parser for reasoning output | +| `enforce_eager` | bool | `None` | Disable CUDA graph capture (required for some architectures like Qwen3.5 hybrid attention) | +| `serve_module` | str | `None` | Python module for vLLM serve script. Auto-set when `vllm_lora_sync: true` | +| `worker_extension_cls` | str | `None` | vLLM worker extension class for weight sync | + +### Trainer vLLM Options (`trl:` section) + +These control how the trainer interacts with vLLM. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `use_vllm` | bool | `false` | Enable vLLM for generation | +| `vllm_mode` | str | `None` | `server` (external process) or `colocate` (same GPU) | +| `vllm_server_host` | str | `0.0.0.0` | Host of the vLLM server to connect to | +| `vllm_server_port` | int | `8000` | Port of the vLLM server to connect to | +| `vllm_server_timeout` | int | `None` | Timeout in seconds for vLLM requests | +| `vllm_lora_sync` | bool | `false` | Sync LoRA adapters via filesystem instead of NCCL merge | +| `vllm_sync_interval` | int | `None` | Sync weights every N optimizer steps | +| `vllm_enable_sleep_mode` | bool | `None` | Offload vLLM VRAM when idle (colocate mode) | +| `vllm_guided_decoding_regex` | str | `None` | Regex constraint for guided decoding | + +For async pipeline and off-policy correction options, see the [GRPO Configuration Reference](grpo.qmd#configuration-reference). + +## Complete Example {#sec-complete-example} + +For a full working GRPO config including vLLM, LoRA sync, async generation, rewards, and dataset setup, see the [GRPO Quick Start](grpo.qmd#quick-start). That config includes all the vLLM settings covered in this guide. + +```bash +# Terminal 1: Start vLLM +CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve grpo_config.yaml + +# Wait for health check to pass +curl http://localhost:8000/health/ + +# Terminal 2: Start training +CUDA_VISIBLE_DEVICES=1 axolotl train grpo_config.yaml +``` + +## Troubleshooting {#sec-troubleshooting} + +| Problem | Likely Cause | Solution | +|---------|-------------|----------| +| Training hangs waiting for vLLM | Server not started or wrong port | Check `curl http://localhost:8000/health/` and verify `vllm_server_host`/`vllm_server_port` match | +| OOM on vLLM GPU | `gpu_memory_utilization` too high or `max_model_len` too large | Reduce `gpu_memory_utilization` to 0.7 or set `max_model_len` explicitly | +| OOM on training GPU | Batch too large for policy logprobs | Reduce `micro_batch_size` or `num_generations` | +| Accuracy stays at zero | Stale vLLM from previous run | Restart vLLM: `pkill -9 -f "vllm\|EngineCore"`, verify with `nvidia-smi`, restart | +| `ResponseValidationError` from vLLM | Missing logprobs in response | Ensure you are using the correct serve module (auto-selected with `vllm_lora_sync: true`) | +| Weight sync takes 5+ seconds | NCCL contention with vLLM generation | Switch to `vllm_lora_sync: true` to eliminate NCCL | +| `async_prefetch` deadlocks with FSDP | Background threads run unsynchronized FSDP collectives | Set `async_prefetch: false` when using FSDP or DeepSpeed multi-GPU | From 842fa039ddde343401ff4199ca347bbb0c99419c Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 2 Apr 2026 19:53:48 +0700 Subject: [PATCH 1249/1405] feat: add sonicmoe fused lora support (#3519) * feat: add sonicmoe fused lora support * fix: forgot to add file * feat: add test * feat: add lora support for other routes * fix: add int8 lora support * fix: add qwen35_moe interleave support * fix: qwen3_5_moe loss * chore: lint * address some pr comments * fix test imports * add support matrix for moe kernels [skip ci] --------- Co-authored-by: Wing Lian --- src/axolotl/integrations/kernels/README.md | 87 ++++- src/axolotl/integrations/kernels/args.py | 22 ++ .../libs/scattermoe_lora/parallel_experts.py | 5 + .../scattermoe_lora/parallel_linear_lora.py | 5 + .../kernels/{ => libs}/sonicmoe/__init__.py | 0 .../kernels/libs/sonicmoe/lora.py | 220 ++++++++++++ .../kernels/{ => libs}/sonicmoe/patch.py | 99 ++++-- .../kernels/{ => libs}/sonicmoe/routing.py | 86 +++-- .../{ => libs}/sonicmoe/weight_converter.py | 73 ++-- src/axolotl/integrations/kernels/plugin.py | 3 +- tests/e2e/integrations/test_sonicmoe.py | 14 +- tests/e2e/integrations/test_sonicmoe_lora.py | 318 +++++++++++++++++ tests/integrations/test_routing_parity.py | 38 +- tests/integrations/test_sonicmoe.py | 81 +++-- tests/integrations/test_sonicmoe_gradients.py | 2 +- tests/integrations/test_sonicmoe_lora.py | 328 ++++++++++++++++++ 16 files changed, 1252 insertions(+), 129 deletions(-) rename src/axolotl/integrations/kernels/{ => libs}/sonicmoe/__init__.py (100%) create mode 100644 src/axolotl/integrations/kernels/libs/sonicmoe/lora.py rename src/axolotl/integrations/kernels/{ => libs}/sonicmoe/patch.py (68%) rename src/axolotl/integrations/kernels/{ => libs}/sonicmoe/routing.py (86%) rename src/axolotl/integrations/kernels/{ => libs}/sonicmoe/weight_converter.py (69%) create mode 100644 tests/e2e/integrations/test_sonicmoe_lora.py create mode 100644 tests/integrations/test_sonicmoe_lora.py diff --git a/src/axolotl/integrations/kernels/README.md b/src/axolotl/integrations/kernels/README.md index 7c40720afd..a852cd6cfc 100644 --- a/src/axolotl/integrations/kernels/README.md +++ b/src/axolotl/integrations/kernels/README.md @@ -52,26 +52,91 @@ The `KernelsPlugin` runs before model loading and: ### ScatterMoE 1. Registers the ScatterMoE kernel from the local `libs/scattermoe_lora` package (includes fused LoRA support via Triton kernels). -2. Patches the model's `SparseMoeBlock` forward method with the optimized ScatterMoE implementation. +2. Patches the model's `SparseMoeBlock` forward method with the optimized ScatterMoE implementation via the HF `kernels` library. ### SonicMoE 1. Resolves the model's MoE block class(es) from `constants.py`. -2. Patches the forward method with SonicMoE's optimized kernels and registers a weight converter for the interleaved gate/up projection format. -3. Supports both softmax->topk and sigmoid->topk routing strategies. +2. Patches the forward method with SonicMoE's optimized CUTLASS kernels and registers a weight converter for the interleaved gate/up projection format. +3. Supports pluggable routing strategies (see routing table below). Both paths use the shared `resolve_moe_block_classes` utility in `constants.py` for model-type-to-class resolution. -#### Supported Models - -See `constants.py` for the full list of supported model types (Qwen2-MoE, Qwen3-MoE, OLMoE, Mixtral, DeepSeek-V3, GLM-MoE, MiniMax, etc.). +## Model Support Matrix + +All models use the **SwiGLU** activation (`act_fn(gate) * up`). Neither kernel currently supports non-SwiGLU MoE architectures. + +### Routing strategies + +| Routing Strategy | Description | ScatterMoE | SonicMoE | +|---|---|:---:|:---:| +| softmax → topk | Softmax over experts, select top-K, optional renormalization | Yes | Yes | +| softmax → group selection → topk | Softmax, select top groups (sum of top-2 per group), topk from selected groups, renorm + scaling | No | Yes | +| sigmoid → topk (with groups) | Sigmoid + bias correction, group-based masking, topk from masked scores, weights from original sigmoid | Yes | Yes | +| sigmoid → topk (no groups) | Sigmoid + bias correction, straight topk (n_group=1) | Yes | Yes | +| softmax → bias correction → topk | Softmax, bias via `gate.moe_statics`, topk, gather from original probs, clamp-based renorm | No | Yes | +| softmax → group_limited_greedy | Softmax, group selection (max per group), topk, scale only (no renorm) | No | Yes | +| softmax → topk via gate.wg | Softmax, gate weight at `gate.wg.weight` (not `gate.weight`), always renormalize | No | Yes | +| fused topk → softmax | Routing + expert computation fused in a single kernel | No | Planned | + +### Per-model support + +| Model Type | Architecture | Routing | ScatterMoE | SonicMoE | +|---|---|---|:---:|:---:| +| `qwen2_moe` | Qwen2-MoE | softmax → topk | **Yes** | **Yes** | +| `qwen3_moe` | Qwen3-MoE | softmax → topk | **Yes** | **Yes** | +| `qwen3_5_moe` | Qwen3.5-MoE | softmax → topk | **Yes** | **Yes** | +| `qwen3_5_moe_text` | Qwen3.5-MoE (VLM text) | softmax → topk | **Yes** | **Yes** | +| `qwen3_next` | Qwen3-Next | softmax → topk | **Yes** | **Yes** | +| `qwen3_vl_moe` | Qwen3-VL-MoE | softmax → topk | **Yes** | **Yes** | +| `qwen3_omni_moe` | Qwen3-Omni (Thinker + Talker) | softmax → topk | **Yes** | **Yes** | +| `olmoe` | OLMoE | softmax → topk | **Yes** | **Yes** | +| `mixtral` | Mixtral | softmax → topk | **Yes** | **Yes** | +| `minimax` | MiniMax | softmax → topk | **Yes** | **Yes** | +| `mistral4` | Mistral 4 | softmax → group → topk | No | **Yes** | +| `glm_moe_dsa` | GLM-MoE DSA (GLM 5) | sigmoid → topk (groups) | **Yes** | **Yes** | +| `deepseek_v3` | DeepSeek-V3 | sigmoid → topk (groups) | **Yes** | **Yes** | +| `glm4_moe` | GLM4-MoE | sigmoid → topk (groups) | **Yes** | **Yes** | +| `glm4_moe_lite` | GLM4-MoE Lite (GLM 4.7 Flash) | sigmoid → topk (groups) | **Yes**\* | **Yes** | +| `glm4v_moe` | GLM4v-MoE | sigmoid → topk (groups) | **Yes** | **Yes** | +| `minimax_m2` | MiniMax M2 | sigmoid → topk (no groups) | **Yes** | **Yes** | +| `ernie4_5_moe` | ERNIE 4.5 MoE | softmax → bias → topk | No | **Yes** | +| `deepseek_v2` | DeepSeek-V2 | softmax → group_limited_greedy | No | **Yes** | +| `hunyuan_v1_moe` | HunYuan V1 MoE | softmax → topk (gate.wg) | No | **Yes** | +| `gpt_oss` | GPT-OSS | fused topk → softmax | No | Planned | + +\* `glm4_moe_lite` with ScatterMoE may have issues — see Limitations. + +### Feature comparison + +| Feature | ScatterMoE | SonicMoE | +|---|:---:|:---:| +| Kernel backend | Triton | CUTLASS | +| GPU requirement | Any CUDA | Hopper (H100/H200) or Blackwell (B200+) | +| LoRA approach | Fused in Triton kernel | Runtime materialization + custom autograd | +| LoRA overhead | Lower (fused computation) | Higher (per-forward materialization) | +| Gate/router LoRA | Yes | Yes | +| Expert LoRA | Yes (fused) | Yes (materialized) | +| Shared expert LoRA | Yes (standard PEFT) | Yes (standard PEFT) | +| Selective expert dequantization | Yes (~97% memory savings) | No | +| Weight format | Transposed `[E, hidden, 2*inter]` | Interleaved gate/up `[2*I, H, E]` | +| torch.compile routing | No | Yes (optional) | + +## Shared Expert Handling + +Both kernels handle shared experts identically. Shared expert attribute names are detected in order of priority: + +1. `shared_expert` (Qwen2-MoE) +2. `shared_experts` (GLM-MoE, DeepSeek-V3) +3. `shared_mlp` (HunYuan V1 MoE) + +If `shared_expert_gate` exists, sigmoid gating is applied to the shared expert contribution before adding it to the routed output. PEFT wraps shared expert linear layers with standard LoRA — no special handling is needed. ## Limitations -ScatterMoE uses a softmax -> topk routing, so results may be different for some model architectures as baseline (GPT-OSS, etc). Incompatible with `GLM_MOE_DSA` (GLM 5) and `GLM4_MOE_LITE` (GLM 4.7 Flash) at the moment. - -SonicMoE supports both softmax->topk and sigmoid->topk routing, covering a wider range of architectures. - -ScatterMoE does not work for GLM4.7 Flash (glm4_moe_lite) atm. +- **ScatterMoE + GLM4-MoE Lite**: ScatterMoE does not work reliably for GLM 4.7 Flash (`glm4_moe_lite`). +- **Non-SwiGLU activations**: Neither kernel supports MoE architectures with non-SwiGLU expert activations (e.g., GPT-OSS uses a custom GLU variant). +- **GPT-OSS**: Deferred — requires transposed weight layout `[E, H, 2*I]`, expert biases, and custom GLU activation. A dedicated forward path is needed. +- **FSDP + fused gate LoRA (SonicMoE)**: The fused topk→softmax path materializes a local tensor when LoRA delta is present to avoid DTensor + Tensor mixing under FSDP. ## Note on MegaBlocks diff --git a/src/axolotl/integrations/kernels/args.py b/src/axolotl/integrations/kernels/args.py index d9b261e720..7c9e23b6c6 100644 --- a/src/axolotl/integrations/kernels/args.py +++ b/src/axolotl/integrations/kernels/args.py @@ -45,6 +45,28 @@ def check_experts_implementation(cls, data): return data + @model_validator(mode="before") + @classmethod + def warn_sonicmoe_lora_overhead(cls, data): + if data.get("use_sonicmoe") is True and data.get("adapter") in ( + "lora", + "qlora", + ): + lora_target = data.get("lora_target_modules") or [] + lora_linear = data.get("lora_target_linear_modules") or [] + targets = ( + lora_target if isinstance(lora_target, list) else [lora_target] + ) + (lora_linear if isinstance(lora_linear, list) else [lora_linear]) + expert_keywords = ("gate_up_proj", "down_proj", "experts") + if any(kw in t for t in targets for kw in expert_keywords): + LOG.info( + "SonicMoE + LoRA on expert modules uses runtime weight materialization " + "(W_eff = W + scaling*B@A per forward). This has slightly higher overhead " + "than ScatterMoE's fused Triton LoRA kernels but works with any CUTLASS kernel." + ) + + return data + @model_validator(mode="before") @classmethod def disable_mlp_kernel(cls, data): diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_experts.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_experts.py index 7a1eef472b..5180587aad 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_experts.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_experts.py @@ -49,6 +49,11 @@ def forward( grouped_in: bool = False, grouped_out: bool = False, ): + # Cast weights to match input dtype (e.g. 8-bit LoRA) + if expert_weights.dtype != x.dtype: + expert_weights = expert_weights.to(x.dtype) + if expert_biases is not None and expert_biases.dtype != x.dtype: + expert_biases = expert_biases.to(x.dtype) with torch.device(x.device): output = kernels.ops.scatter2scatter( X=x, diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py index 5d00e1230d..17dfd420c0 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py @@ -65,6 +65,11 @@ def forward( use_fused_dX: bool = False, use_fused_gather: bool = False, ): + # Cast weights to match input dtype (e.g. 8-bit LoRA) + if expert_weights.dtype != x.dtype: + expert_weights = expert_weights.to(x.dtype) + if expert_biases is not None and expert_biases.dtype != x.dtype: + expert_biases = expert_biases.to(x.dtype) with torch.device(x.device): # Fused forward: Y = X @ W + scaling * (X @ A^T) @ B^T output = scatter2scatter_lora( diff --git a/src/axolotl/integrations/kernels/sonicmoe/__init__.py b/src/axolotl/integrations/kernels/libs/sonicmoe/__init__.py similarity index 100% rename from src/axolotl/integrations/kernels/sonicmoe/__init__.py rename to src/axolotl/integrations/kernels/libs/sonicmoe/__init__.py diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/lora.py b/src/axolotl/integrations/kernels/libs/sonicmoe/lora.py new file mode 100644 index 0000000000..4d7a21925b --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/lora.py @@ -0,0 +1,220 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +SonicMoE LoRA support via runtime weight materialization. + +SonicMoE uses opaque CUTLASS kernels that cannot be modified to fuse LoRA. +Instead, we materialize the effective weight W_eff = W + scaling * (B @ A) +before each CUTLASS call, and use a custom autograd.Function to route +gradients back to the LoRA A and B parameters. + +PEFT unwrapping utilities are also provided to handle the ParamWrapper +chain that PEFT creates when targeting expert parameters. +""" + +from typing import Optional + +import torch + +# ============================================================================= +# PEFT unwrapping utilities +# ============================================================================= + + +def has_lora(module) -> bool: + """Check if a module is wrapped by PEFT with LoRA.""" + return hasattr(module, "base_layer") and hasattr(module, "lora_A") + + +def get_lora_params_from_wrapper(module) -> tuple: + """Extract LoRA parameters from a PEFT ParamWrapper. + + Returns: + (lora_A, lora_B, scaling) if LoRA is active, else (None, None, None) + """ + if not hasattr(module, "lora_A") or not hasattr(module, "lora_B"): + return None, None, None + + active_adapters = getattr(module, "active_adapters", ["default"]) + if not active_adapters: + return None, None, None + + adapter_name = active_adapters[0] + + lora_A_dict = getattr(module, "lora_A", {}) + lora_B_dict = getattr(module, "lora_B", {}) + scaling_dict = getattr(module, "scaling", {}) + + if ( + adapter_name not in lora_A_dict + or adapter_name not in lora_B_dict + or adapter_name not in scaling_dict + ): + return None, None, None + + lora_A = lora_A_dict[adapter_name].weight + lora_B = lora_B_dict[adapter_name].weight + scaling = scaling_dict[adapter_name] + + return lora_A, lora_B, scaling + + +def unwrap_gate_lora(gate_module): + """Unwrap PEFT ParamWrapper on the router gate. + + When PEFT targets ``gate.weight``, ``self.gate`` becomes:: + + ParamWrapper(weight) + -> base_layer: Router (the real module) + + Returns: + (base_gate, gate_weight, gate_lora_delta_or_None) + + ``base_gate`` is the original router module (with ``.top_k``, etc.). + ``gate_weight`` is the base router weight tensor. + ``gate_lora_delta_or_None`` is the LoRA delta if active, else None. + Kept separate to avoid mixing DTensor + Tensor under FSDP. + """ + if has_lora(gate_module): + base_gate = gate_module.base_layer + lora_A, lora_B, scaling = get_lora_params_from_wrapper(gate_module) + if lora_A is not None: + delta = scaling * (lora_B @ lora_A) + return base_gate, base_gate.weight, delta + return base_gate, base_gate.weight, None + + return gate_module, gate_module.weight, None + + +def unwrap_experts_lora(experts_module): + """Walk a PEFT ParamWrapper chain on ``self.experts``. + + When PEFT targets ``experts.gate_up_proj`` and ``experts.down_proj`` + via ``target_parameters``, ``self.experts`` becomes:: + + ParamWrapper(down_proj) + -> base_layer: ParamWrapper(gate_up_proj) + -> base_layer: Experts (the real module) + + Returns: + (base_experts, lora_dict) + + ``lora_dict`` maps parameter names to ``(lora_A, lora_B, scaling)`` + tuples, or is empty if no LoRA is active. + """ + wrappers = {} + module = experts_module + while hasattr(module, "base_layer") and hasattr(module, "lora_A"): + param_name = getattr(module, "parameter_name", None) + if param_name is not None: + wrappers[param_name] = module + module = module.base_layer + + base_experts = module + lora_dict = {} + + for param_name, wrapper in wrappers.items(): + lora_A, lora_B, scaling = get_lora_params_from_wrapper(wrapper) + if lora_A is not None: + lora_dict[param_name] = (lora_A, lora_B, scaling) + + return base_experts, lora_dict + + +# ============================================================================= +# LoRA weight materialization autograd function +# ============================================================================= + + +class MoELoRAMaterialize(torch.autograd.Function): + """Materialize effective weight W_eff = W + scaling * (B @ A) per expert. + + Inserts into the autograd graph between PEFT's LoRA parameters and + SonicMoE's CUTLASS kernels. The CUTLASS backward computes dW_eff, + which this function decomposes into dA and dB via the chain rule. + + Weight layouts (PEFT rank-major): + base_weight: [E, dim1, dim2] (frozen expert parameter) + lora_A: [r*E, dim2] (rows [e*r:(e+1)*r] = A_e) + lora_B: [dim1, r*E] (cols [:, e*r:(e+1)*r] = B_e) + + Per-expert: delta_e = B_e @ A_e = [dim1, r] @ [r, dim2] = [dim1, dim2] + """ + + @staticmethod + def forward( + ctx, + base_weight: torch.Tensor, + lora_A: torch.Tensor, + lora_B: torch.Tensor, + scaling: float, + ) -> torch.Tensor: + E, dim1, dim2 = base_weight.shape + r = lora_A.shape[0] // E + assert lora_A.shape[0] == r * E, ( + f"lora_A rows ({lora_A.shape[0]}) must be divisible by num_experts ({E})" + ) + + # Reshape PEFT rank-major to per-expert batched format + A_3d = lora_A.reshape(E, r, dim2) + B_3d = lora_B.reshape(dim1, r, E).permute(2, 0, 1).contiguous() # [E, dim1, r] + + # Batched matmul: [E, dim1, r] @ [E, r, dim2] = [E, dim1, dim2] + delta = torch.bmm(B_3d, A_3d) + + W_eff = base_weight + scaling * delta + + ctx.save_for_backward(lora_A, lora_B) + ctx.scaling = scaling + ctx.E = E + ctx.r = r + + return W_eff + + @staticmethod + def backward(ctx, grad_W_eff: torch.Tensor): + lora_A, lora_B = ctx.saved_tensors + scaling = ctx.scaling + E = ctx.E + r = ctx.r + + _, dim1, dim2 = grad_W_eff.shape + + # Reshape to per-expert (same as forward) + A_3d = lora_A.reshape(E, r, dim2) + B_3d = lora_B.reshape(dim1, r, E).permute(2, 0, 1).contiguous() # [E, dim1, r] + + # dA_e = scaling * B_e^T @ dW_e + # [E, r, dim1] @ [E, dim1, dim2] = [E, r, dim2] + d_A_3d = scaling * torch.bmm(B_3d.transpose(1, 2), grad_W_eff) + + # dB_e = scaling * dW_e @ A_e^T + # [E, dim1, dim2] @ [E, dim2, r] = [E, dim1, r] + d_B_3d = scaling * torch.bmm(grad_W_eff, A_3d.transpose(1, 2)) + + # Reshape back to PEFT rank-major layout + d_lora_A = d_A_3d.reshape(E * r, dim2) + d_lora_B = d_B_3d.permute(1, 2, 0).contiguous().reshape(dim1, E * r) + + return None, d_lora_A, d_lora_B, None + + +def materialize_expert_lora( + base_weight: torch.Tensor, + lora_params: Optional[tuple], +) -> torch.Tensor: + """Materialize effective expert weight with optional LoRA delta. + + Args: + base_weight: [E, dim1, dim2] frozen expert parameter + lora_params: (lora_A, lora_B, scaling) or None + + Returns: + W_eff if lora_params is not None, else base_weight unchanged. + """ + if lora_params is None: + return base_weight + lora_A, lora_B, scaling = lora_params + return MoELoRAMaterialize.apply(base_weight, lora_A, lora_B, scaling) diff --git a/src/axolotl/integrations/kernels/sonicmoe/patch.py b/src/axolotl/integrations/kernels/libs/sonicmoe/patch.py similarity index 68% rename from src/axolotl/integrations/kernels/sonicmoe/patch.py rename to src/axolotl/integrations/kernels/libs/sonicmoe/patch.py index a3b96f12a1..65095a9871 100644 --- a/src/axolotl/integrations/kernels/sonicmoe/patch.py +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/patch.py @@ -28,20 +28,72 @@ from axolotl.integrations.kernels.constants import resolve_moe_block_classes from axolotl.utils.logging import get_logger +from .lora import ( + has_lora, + materialize_expert_lora, + unwrap_experts_lora, + unwrap_gate_lora, +) + LOG = get_logger(__name__) -def patch_sonicmoe(model_type: str, torch_compile: bool = False): - """Main entry point: patch SparseMoeBlock for SonicMoE support. +def _get_expert_weights(experts_module): + """Extract expert weights, applying LoRA materialization if PEFT is active. - Args: - model_type: The HuggingFace model type (e.g. "qwen3_moe"). - torch_compile: If True, wrap routing functions with torch.compile - for kernel fusion (fuses softmax+topk+renorm into fewer launches). + Returns: + (gate_up_weight, down_weight) in SonicMoE layout [dim, dim, E]. """ + if has_lora(experts_module): + base_experts, lora_dict = unwrap_experts_lora(experts_module) + gate_up = materialize_expert_lora( + base_experts.gate_up_proj, lora_dict.get("gate_up_proj") + ) + down = materialize_expert_lora( + base_experts.down_proj, lora_dict.get("down_proj") + ) + else: + gate_up = experts_module.gate_up_proj + down = experts_module.down_proj + + # Permute to SonicMoE layout: + # gate_up: [E, 2*I, H] -> [2*I, H, E] + # down: [E, H, I] -> [H, I, E] + return gate_up.permute(1, 2, 0), down.permute(1, 2, 0) + + +def _fix_qwen3_5_moe_text_weight_renaming(model_type: str, base_model_type: str): + """Strip qwen3_5_moe_text WeightRenaming in VLM mode to preserve custom loaders.""" + if model_type != "qwen3_5_moe_text" or base_model_type == "qwen3_5_moe_text": + return + + try: + from transformers.conversion_mapping import ( + get_checkpoint_conversion_mapping, + register_checkpoint_conversion_mapping, + ) + from transformers.core_model_loading import WeightRenaming + except ImportError: + return + + text_mapping = get_checkpoint_conversion_mapping(model_type) + if text_mapping and isinstance(text_mapping[0], WeightRenaming): + text_mapping.pop(0) + register_checkpoint_conversion_mapping(model_type, text_mapping, overwrite=True) + LOG.info("Stripped qwen3_5_moe_text WeightRenaming for VLM mode") + + +def patch_sonicmoe( + model_type: str, + torch_compile: bool = False, + base_model_type: str | None = None, +): + """Patch SparseMoeBlock for SonicMoE support.""" from .routing import get_model_moe_config from .weight_converter import register_sonicmoe_weight_converter + _fix_qwen3_5_moe_text_weight_renaming(model_type, base_model_type or model_type) + routing_fn, activation, router_attr = get_model_moe_config(model_type) if torch_compile and routing_fn is not None: @@ -113,11 +165,10 @@ def sonicmoe_forward(self, hidden_states: torch.Tensor) -> torch.Tensor: hidden_states_flat, self ) - # Permute weights to SonicMoE layout: - # gate_up: [E, 2*I, H] -> [2*I, H, E] - # down: [E, H, I] -> [H, I, E] - gate_up_weight = self.experts.gate_up_proj.permute(1, 2, 0) - down_weight = self.experts.down_proj.permute(1, 2, 0) + # Unwrap PEFT + optional LoRA materialization, then permute to SonicMoE layout + gate_up_weight, down_weight = _get_expert_weights(self.experts) + gate_up_weight = gate_up_weight.to(hidden_states_flat.dtype) + down_weight = down_weight.to(hidden_states_flat.dtype) E = gate_up_weight.shape[-1] output, _ = moe_general_routing_inputs( @@ -161,22 +212,30 @@ def sonicmoe_fused_forward(self, hidden_states: torch.Tensor) -> torch.Tensor: # Shared expert (computed early, matching original model ordering) shared_expert_output = _compute_shared_expert(self, hidden_states_flat) - router = getattr(self, router_attr) - - # Permute weights to SonicMoE layout: - # gate_up: [E, 2*I, H] -> [2*I, H, E] - # down: [E, H, I] -> [H, I, E] - gate_up_weight = self.experts.gate_up_proj.permute(1, 2, 0) - down_weight = self.experts.down_proj.permute(1, 2, 0) + # Unwrap router for attribute access + optional LoRA delta + raw_router = getattr(self, router_attr) + base_router, router_weight, router_lora_delta = unwrap_gate_lora(raw_router) + if router_lora_delta is not None: + # Materialize local tensor to avoid DTensor + Tensor add under FSDP + if hasattr(router_weight, "to_local"): + router_weight = router_weight.to_local() + effective_router_weight = router_weight + router_lora_delta + else: + effective_router_weight = router_weight + + # Unwrap PEFT + optional LoRA materialization, then permute to SonicMoE layout + gate_up_weight, down_weight = _get_expert_weights(self.experts) + gate_up_weight = gate_up_weight.to(hidden_states_flat.dtype) + down_weight = down_weight.to(hidden_states_flat.dtype) output, _router_logits, _expert_freq = moe_TC_softmax_topk_layer( hidden_states_flat, - router.weight, + effective_router_weight, gate_up_weight, None, # b1 (no gate/up bias) down_weight, None, # b2 (no down bias) - router.top_k, + base_router.top_k, torch.cuda.current_stream().cuda_stream, activation, False, # is_inference_mode diff --git a/src/axolotl/integrations/kernels/sonicmoe/routing.py b/src/axolotl/integrations/kernels/libs/sonicmoe/routing.py similarity index 86% rename from src/axolotl/integrations/kernels/sonicmoe/routing.py rename to src/axolotl/integrations/kernels/libs/sonicmoe/routing.py index 09bffc7421..4bdb378904 100644 --- a/src/axolotl/integrations/kernels/sonicmoe/routing.py +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/routing.py @@ -16,6 +16,8 @@ import torch import torch.nn.functional as F +from .lora import unwrap_gate_lora + def get_model_moe_config(model_type: str): """Returns (routing_fn, activation, router_attr) for a given model type. @@ -40,6 +42,7 @@ def get_model_moe_config(model_type: str): "qwen2_moe", "qwen3_moe", "qwen3_5_moe", + "qwen3_5_moe_text", "qwen3_next", "qwen3_vl_moe", "qwen3_omni_moe", @@ -88,12 +91,18 @@ def softmax_topk_routing( expert_indices: [T*K] which expert (int32) router_logits: [T, E] original logits for aux loss """ - gate = moe_block.gate - T, _ = hidden_states.shape - K = gate.top_k - - # Compute router logits and softmax over all experts - router_logits = F.linear(hidden_states, gate.weight) # [T, E] + base_gate, gate_weight, gate_lora_delta = unwrap_gate_lora(moe_block.gate) + T, H = hidden_states.shape + K = base_gate.top_k + + # Compute router logits and softmax over all experts. + # Two F.linear calls avoid mixing DTensor (gate_weight) + Tensor (delta) under FSDP. + # Cast to float32 to match LoRA delta dtype (PEFT computes in fp32). + router_logits = F.linear(hidden_states.float(), gate_weight.float()) # [T, E] + if gate_lora_delta is not None: + router_logits = router_logits + F.linear( + hidden_states.float(), gate_lora_delta.float() + ) router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] # Select top-k experts per token @@ -101,7 +110,7 @@ def softmax_topk_routing( # Renormalize if configured (default True for models without the attribute, # e.g. Mixtral/MiniMax which always normalize) - if getattr(gate, "norm_topk_prob", True): + if getattr(base_gate, "norm_topk_prob", True): top_values = top_values / top_values.sum(dim=-1, keepdim=True) # no-op: matches transformers which casts to softmax output dtype (float32). @@ -128,13 +137,17 @@ def softmax_group_topk_routing( hidden_states: torch.Tensor, moe_block ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Mistral4-style routing: softmax -> group selection -> topk -> renorm -> scale.""" - gate = moe_block.gate + base_gate, gate_weight, gate_lora_delta = unwrap_gate_lora(moe_block.gate) T, _ = hidden_states.shape K = moe_block.top_k - E = getattr(moe_block, "n_routed_experts", gate.weight.shape[0]) + E = getattr(moe_block, "n_routed_experts", gate_weight.shape[0]) n_group = getattr(moe_block, "n_group", 1) - router_logits = F.linear(hidden_states, gate.weight) # [T, E] + router_logits = F.linear(hidden_states.float(), gate_weight.float()) # [T, E] + if gate_lora_delta is not None: + router_logits = router_logits + F.linear( + hidden_states.float(), gate_lora_delta.float() + ) router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] scores_for_choice = router_probs @@ -206,25 +219,29 @@ def sigmoid_topk_routing( expert_indices: [T*K] which expert (int32) router_logits: [T, E] original logits for aux loss """ - gate = moe_block.gate + base_gate, gate_weight, gate_lora_delta = unwrap_gate_lora(moe_block.gate) T, _ = hidden_states.shape K = moe_block.top_k - E = getattr(moe_block, "n_routed_experts", gate.weight.shape[0]) + E = getattr(moe_block, "n_routed_experts", gate_weight.shape[0]) n_group = getattr(moe_block, "n_group", 1) # Compute router logits and sigmoid probabilities - router_logits = F.linear(hidden_states.float(), gate.weight.float()) # [T, E] + router_logits = F.linear(hidden_states.float(), gate_weight.float()) # [T, E] + if gate_lora_delta is not None: + router_logits = router_logits + F.linear( + hidden_states.float(), gate_lora_delta.float() + ) router_probs = router_logits.sigmoid() # [T, E] # Bias-corrected scores for expert selection (not used for final weights). # glm_moe_dsa/deepseek_v3 store the bias on gate; minimax_m2 stores it on the block. - e_score_correction_bias = getattr(gate, "e_score_correction_bias", None) + e_score_correction_bias = getattr(base_gate, "e_score_correction_bias", None) if e_score_correction_bias is None: e_score_correction_bias = getattr(moe_block, "e_score_correction_bias", None) if e_score_correction_bias is None: raise AttributeError( f"sigmoid_topk_routing requires e_score_correction_bias on " - f"gate ({type(gate)}) or moe_block ({type(moe_block)}), but neither has it" + f"gate ({type(base_gate)}) or moe_block ({type(moe_block)}), but neither has it" ) scores_for_choice = router_probs + e_score_correction_bias @@ -296,16 +313,20 @@ def softmax_bias_topk_routing( expert_indices: [T*K] which expert (int32) router_logits: [T, E] original logits for aux loss """ - gate = moe_block.gate - T, _ = hidden_states.shape - K = gate.top_k + base_gate, gate_weight, gate_lora_delta = unwrap_gate_lora(moe_block.gate) + T, H = hidden_states.shape + K = base_gate.top_k # Compute router logits and softmax (force float32 for numerical stability) - router_logits = F.linear(hidden_states.float(), gate.weight.float()) # [T, E] + router_logits = F.linear(hidden_states.float(), gate_weight.float()) # [T, E] + if gate_lora_delta is not None: + router_logits = router_logits + F.linear( + hidden_states.float(), gate_lora_delta.float() + ) router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] # Bias-corrected scores for expert selection (via moe_statics module) - scores_for_choice = gate.moe_statics(router_probs) # [T, E] + scores_for_choice = base_gate.moe_statics(router_probs) # [T, E] # Select top-k experts using biased scores _, selected_experts = torch.topk(scores_for_choice, K, dim=-1) # [T, K] @@ -314,7 +335,7 @@ def softmax_bias_topk_routing( top_values = torch.gather(router_probs, dim=-1, index=selected_experts) # [T, K] # Renormalize with clamp(min=norm_min) instead of sum+epsilon - norm_min = getattr(gate, "norm_min", 1e-20) + norm_min = getattr(base_gate, "norm_min", 1e-20) top_values = top_values / torch.clamp( top_values.sum(dim=-1, keepdim=True), min=norm_min ) @@ -358,15 +379,19 @@ def softmax_group_limited_topk_routing( expert_indices: [T*K] which expert (int32) router_logits: [T, E] original logits for aux loss """ - gate = moe_block.gate - T, _ = hidden_states.shape + base_gate, gate_weight, gate_lora_delta = unwrap_gate_lora(moe_block.gate) + T, H = hidden_states.shape K = moe_block.top_k num_group = getattr(moe_block, "num_group", 1) - num_experts = gate.weight.shape[0] + num_experts = gate_weight.shape[0] topk_method = getattr(moe_block, "topk_method", "greedy") # Compute logits in float32 and softmax - router_logits = F.linear(hidden_states.float(), gate.weight.float()) # [T, E] + router_logits = F.linear(hidden_states.float(), gate_weight.float()) # [T, E] + if gate_lora_delta is not None: + router_logits = router_logits + F.linear( + hidden_states.float(), gate_lora_delta.float() + ) router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] if topk_method == "greedy" or num_group == 1: @@ -445,12 +470,17 @@ def softmax_topk_wg_routing( router_logits: [T, E] original logits for aux loss """ gate = moe_block.gate - T, _ = hidden_states.shape + T, H = hidden_states.shape K = moe_block.top_k # Gate computes logits via gate.wg (nn.Linear, float32) - wg = gate.wg - router_logits = F.linear(hidden_states.float(), wg.weight.float()) # [T, E] + # Unwrap at gate.wg level since PEFT targets the wg Linear, not the gate container + base_wg, wg_weight, wg_lora_delta = unwrap_gate_lora(gate.wg) + router_logits = F.linear(hidden_states.float(), wg_weight.float()) # [T, E] + if wg_lora_delta is not None: + router_logits = router_logits + F.linear( + hidden_states.float(), wg_lora_delta.float() + ) router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] # Select top-k experts diff --git a/src/axolotl/integrations/kernels/sonicmoe/weight_converter.py b/src/axolotl/integrations/kernels/libs/sonicmoe/weight_converter.py similarity index 69% rename from src/axolotl/integrations/kernels/sonicmoe/weight_converter.py rename to src/axolotl/integrations/kernels/libs/sonicmoe/weight_converter.py index 172864ac66..20da27ff0a 100644 --- a/src/axolotl/integrations/kernels/sonicmoe/weight_converter.py +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/weight_converter.py @@ -129,15 +129,41 @@ def reverse_op(self) -> ConversionOps: return ConcatenatedToInterleaved(self.dim) +def _make_same_key_interleave_converter(): + """Create a WeightConverter that interleaves an already-fused gate_up_proj.""" + from transformers.core_model_loading import WeightConverter + + return WeightConverter( + source_patterns="mlp.experts.gate_up_proj", + target_patterns="mlp.experts.gate_up_proj", + operations=[ConcatenatedToInterleaved(dim=1)], + ) + + +def _has_same_key_interleave(mapping) -> bool: + """Check whether the mapping already has a same-key gate_up_proj interleave converter.""" + for conv in mapping: + if ( + hasattr(conv, "source_patterns") + and conv.source_patterns == ["mlp.experts.gate_up_proj"] + and conv.target_patterns == ["mlp.experts.gate_up_proj"] + and hasattr(conv, "operations") + and any(isinstance(op, ConcatenatedToInterleaved) for op in conv.operations) + ): + return True + return False + + def register_sonicmoe_weight_converter(model_type: str): - """Override the conversion mapping to add interleave step for gate_up_proj. + """Register weight converters to interleave gate_up_proj for SonicMoE. - Appends a ConcatenatedToInterleaved operation to the existing gate_up_proj - converter chain. For example, qwen3_moe's chain becomes: - MergeModulelist(dim=0) -> Concatenate(dim=1) -> ConcatenatedToInterleaved(dim=1) + Handles two checkpoint formats: + 1. Separate per-expert weights (e.g. qwen3_moe): appends interleave to the + existing merge chain (MergeModulelist -> Concatenate -> Interleave). + 2. Already-fused gate_up_proj (e.g. qwen3_5_moe_text): adds a same-key + converter (gate_up_proj -> gate_up_proj with Interleave). - The reverse is auto-generated for saving: - InterleavedToConcatenated(dim=1) -> Chunk(dim=1) -> SplitModulelist(dim=0) + The loader matches whichever source pattern exists in the checkpoint. """ from transformers.conversion_mapping import ( get_checkpoint_conversion_mapping, @@ -145,37 +171,32 @@ def register_sonicmoe_weight_converter(model_type: str): ) existing = get_checkpoint_conversion_mapping(model_type) + if existing is None: - LOG.warning( - f"No conversion mapping found for model type '{model_type}'. " - "SonicMoE weight interleaving will not be applied during checkpoint loading." - ) + # No mapping at all — create one with just the same-key converter + mapping = [_make_same_key_interleave_converter()] + register_checkpoint_conversion_mapping(model_type, mapping) + LOG.info(f"Registered SonicMoE weight converter for model type '{model_type}'") return - # Find the gate_up_proj converter and append ConcatenatedToInterleaved - patched = False + # Append interleave to any existing many-to-one merge chain for converter in existing: if hasattr(converter, "operations") and any( "gate_up_proj" in pat for pat in converter.target_patterns ): - # Guard against double registration (e.g. plugin reloaded) - if any( + has_separate_sources = any( + "gate_proj" in pat or "up_proj" in pat + for pat in converter.source_patterns + ) + if has_separate_sources and not any( isinstance(op, ConcatenatedToInterleaved) for op in converter.operations ): - LOG.info( - f"SonicMoE weight converter already registered for '{model_type}'" - ) - return - converter.operations.append(ConcatenatedToInterleaved(dim=1)) - patched = True + converter.operations.append(ConcatenatedToInterleaved(dim=1)) break - if not patched: - LOG.warning( - f"Could not find gate_up_proj converter for model type '{model_type}'. " - "SonicMoE weight interleaving will not be applied during checkpoint loading." - ) - return + # Also add a same-key converter for already-fused checkpoints + if not _has_same_key_interleave(existing): + existing.append(_make_same_key_interleave_converter()) register_checkpoint_conversion_mapping(model_type, existing, overwrite=True) LOG.info(f"Registered SonicMoE weight converter for model type '{model_type}'") diff --git a/src/axolotl/integrations/kernels/plugin.py b/src/axolotl/integrations/kernels/plugin.py index 939bdb790d..4ab22bfce5 100644 --- a/src/axolotl/integrations/kernels/plugin.py +++ b/src/axolotl/integrations/kernels/plugin.py @@ -84,12 +84,13 @@ def pre_model_load(self, cfg): _check_sonicmoe_gpu_compat() - from axolotl.integrations.kernels.sonicmoe import patch_sonicmoe + from axolotl.integrations.kernels.libs.sonicmoe import patch_sonicmoe LOG.info(f"Applying SonicMoE patches for model type: {moe_model_type}") patch_sonicmoe( moe_model_type, torch_compile=bool(getattr(cfg, "torch_compile", False)), + base_model_type=cfg.model_config_type, ) def _register_kernels(self): diff --git a/tests/e2e/integrations/test_sonicmoe.py b/tests/e2e/integrations/test_sonicmoe.py index 2152e94c7f..ff8620b2fa 100644 --- a/tests/e2e/integrations/test_sonicmoe.py +++ b/tests/e2e/integrations/test_sonicmoe.py @@ -51,7 +51,7 @@ def _create_tiny_qwen3_config(): def _interleave_gate_up_weights(model): """Interleave all gate_up_proj parameters in-place for SonicMoE.""" - from axolotl.integrations.kernels.sonicmoe.weight_converter import ( + from axolotl.integrations.kernels.libs.sonicmoe.weight_converter import ( interleave_gate_up, ) @@ -80,7 +80,7 @@ def teardown_method(self): def test_forward_output_matches(self): from transformers import AutoModelForCausalLM - from axolotl.integrations.kernels.sonicmoe.patch import patch_sonicmoe + from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe config = _create_tiny_qwen3_config() input_ids = torch.randint(0, config.vocab_size, (1, 16), device="cuda") @@ -117,8 +117,8 @@ def test_gradients_match(self): """Verify all parameter gradients match between original and patched.""" from transformers import AutoModelForCausalLM - from axolotl.integrations.kernels.sonicmoe.patch import patch_sonicmoe - from axolotl.integrations.kernels.sonicmoe.weight_converter import ( + from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe + from axolotl.integrations.kernels.libs.sonicmoe.weight_converter import ( deinterleave_gate_up, ) @@ -191,7 +191,7 @@ def test_router_weights_receive_gradients(self): """Verify that router (gate) weights get non-zero gradients.""" from transformers import AutoModelForCausalLM - from axolotl.integrations.kernels.sonicmoe.patch import patch_sonicmoe + from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe config = _create_tiny_qwen3_config() input_ids = torch.randint(0, config.vocab_size, (1, 16), device="cuda") @@ -223,7 +223,7 @@ def test_loss_decreases(self): """Run 30 training steps, verify loss decreases and no NaN/Inf.""" from transformers import AutoModelForCausalLM - from axolotl.integrations.kernels.sonicmoe.patch import patch_sonicmoe + from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe config = _create_tiny_qwen3_config() input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") @@ -254,7 +254,7 @@ def test_expert_weights_update(self): """Verify expert weights change during training (not frozen).""" from transformers import AutoModelForCausalLM - from axolotl.integrations.kernels.sonicmoe.patch import patch_sonicmoe + from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe config = _create_tiny_qwen3_config() input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") diff --git a/tests/e2e/integrations/test_sonicmoe_lora.py b/tests/e2e/integrations/test_sonicmoe_lora.py new file mode 100644 index 0000000000..74721ee57a --- /dev/null +++ b/tests/e2e/integrations/test_sonicmoe_lora.py @@ -0,0 +1,318 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +End-to-end tests for SonicMoE + LoRA integration. + +Verifies that PEFT-wrapped MoE models work correctly with SonicMoE's +runtime LoRA materialization: gradients flow to adapters, base weights +stay frozen, and loss converges. + +Requires: + - H100/H200 GPU (SonicMoE CUTLASS kernels target sm_90) + - sonicmoe package installed + - peft package installed + - transformers with Qwen3MoE support + +Usage: + pytest tests/e2e/integrations/test_sonicmoe_lora.py -v -s +""" + +import importlib.util +import math + +import pytest +import torch + +_sonicmoe_available = importlib.util.find_spec("sonicmoe") is not None +_peft_available = importlib.util.find_spec("peft") is not None +_is_hopper = torch.cuda.is_available() and torch.cuda.get_device_capability() == (9, 0) + +pytestmark = [ + pytest.mark.skipif(not torch.cuda.is_available(), reason="Requires CUDA GPU"), + pytest.mark.skipif( + not _is_hopper, reason="SonicMoE CUTLASS kernels require Hopper (sm_90)" + ), + pytest.mark.skipif(not _sonicmoe_available, reason="SonicMoE not installed"), + pytest.mark.skipif(not _peft_available, reason="PEFT not installed"), +] + + +def _create_tiny_qwen3_config(): + """Create a minimal Qwen3MoE config for fast testing.""" + from transformers import AutoConfig + + config = AutoConfig.for_model("qwen3_moe") + config.hidden_size = 512 + config.intermediate_size = 1024 + config.moe_intermediate_size = 64 + config.num_attention_heads = 16 + config.num_key_value_heads = 2 + config.head_dim = 32 + config.num_hidden_layers = 2 + config.num_experts = 8 + config.num_experts_per_tok = 2 + config.vocab_size = 1000 + config.max_position_embeddings = 128 + config.norm_topk_prob = True + config.torch_dtype = torch.bfloat16 + return config + + +def _interleave_gate_up_weights(model): + """Interleave all gate_up_proj parameters in-place for SonicMoE.""" + from axolotl.integrations.kernels.libs.sonicmoe.weight_converter import ( + interleave_gate_up, + ) + + with torch.no_grad(): + for name, param in model.named_parameters(): + if "gate_up_proj" in name: + param.copy_(interleave_gate_up(param)) + + +def _unpatch_sonicmoe(): + """Restore original forward on the MoE block class if it was patched.""" + from axolotl.integrations.kernels.constants import resolve_moe_block_classes + + for moe_cls in resolve_moe_block_classes("qwen3_moe"): + if hasattr(moe_cls, "_original_forward"): + moe_cls.forward = moe_cls._original_forward + del moe_cls._original_forward + + +def _apply_lora(model, target_modules): + """Apply PEFT LoRA to the model.""" + from peft import LoraConfig, get_peft_model + + lora_config = LoraConfig( + r=8, + lora_alpha=16, + target_modules=target_modules, + lora_dropout=0.0, + bias="none", + ) + return get_peft_model(model, lora_config) + + +class TestSonicMoELoRATraining: + """Verify SonicMoE + LoRA training works end-to-end.""" + + def teardown_method(self): + _unpatch_sonicmoe() + + def test_loss_decreases(self): + """Run 30 training steps with LoRA on experts, verify loss decreases.""" + from transformers import AutoModelForCausalLM + + from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe + + config = _create_tiny_qwen3_config() + input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") + + model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + patch_sonicmoe("qwen3_moe") + _interleave_gate_up_weights(model) + model = _apply_lora(model, ["gate_up_proj", "down_proj"]) + + optimizer = torch.optim.AdamW( + [p for p in model.parameters() if p.requires_grad], lr=1e-3 + ) + losses = [] + + for step in range(30): + out = model(input_ids, labels=input_ids) + loss = out.loss + assert not math.isnan(loss.item()), f"NaN loss at step {step}" + assert not math.isinf(loss.item()), f"Inf loss at step {step}" + losses.append(loss.item()) + + loss.backward() + optimizer.step() + optimizer.zero_grad() + + assert losses[-1] < losses[0], ( + f"Loss did not decrease: first={losses[0]:.4f}, last={losses[-1]:.4f}" + ) + + def test_base_weights_frozen(self): + """Verify base (non-LoRA) weights don't change during training.""" + from transformers import AutoModelForCausalLM + + from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe + + config = _create_tiny_qwen3_config() + input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") + + model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + patch_sonicmoe("qwen3_moe") + _interleave_gate_up_weights(model) + model = _apply_lora(model, ["gate_up_proj", "down_proj"]) + + # Snapshot frozen weights + frozen_before = {} + for name, param in model.named_parameters(): + if not param.requires_grad: + frozen_before[name] = param.data.clone() + + optimizer = torch.optim.AdamW( + [p for p in model.parameters() if p.requires_grad], lr=1e-3 + ) + for _ in range(5): + out = model(input_ids, labels=input_ids) + out.loss.backward() + optimizer.step() + optimizer.zero_grad() + + for name, param in model.named_parameters(): + if name in frozen_before: + assert torch.equal(param.data, frozen_before[name]), ( + f"Frozen weight changed: {name}" + ) + + def test_lora_adapters_receive_gradients(self): + """Verify LoRA A and B matrices get non-zero gradients.""" + from transformers import AutoModelForCausalLM + + from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe + + config = _create_tiny_qwen3_config() + input_ids = torch.randint(0, config.vocab_size, (1, 16), device="cuda") + + model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + patch_sonicmoe("qwen3_moe") + _interleave_gate_up_weights(model) + model = _apply_lora(model, ["gate_up_proj", "down_proj"]) + + out = model(input_ids, labels=input_ids) + out.loss.backward() + + lora_grads_found = 0 + for name, param in model.named_parameters(): + if "lora_" in name and param.requires_grad: + assert param.grad is not None, f"No gradient for LoRA param: {name}" + assert param.grad.abs().max() > 0, ( + f"Zero gradient for LoRA param: {name}" + ) + lora_grads_found += 1 + + assert lora_grads_found > 0, "No LoRA parameters found with gradients" + + def test_lora_adapters_update(self): + """Verify LoRA adapter weights change during training.""" + from transformers import AutoModelForCausalLM + + from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe + + config = _create_tiny_qwen3_config() + input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") + + model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + patch_sonicmoe("qwen3_moe") + _interleave_gate_up_weights(model) + model = _apply_lora(model, ["gate_up_proj", "down_proj"]) + + # Snapshot LoRA weights + lora_before = {} + for name, param in model.named_parameters(): + if "lora_" in name and param.requires_grad: + lora_before[name] = param.data.clone() + + assert lora_before, "No LoRA parameters found" + + optimizer = torch.optim.AdamW( + [p for p in model.parameters() if p.requires_grad], lr=1e-3 + ) + for _ in range(5): + out = model(input_ids, labels=input_ids) + out.loss.backward() + optimizer.step() + optimizer.zero_grad() + + changed = sum( + 1 + for name, param in model.named_parameters() + if name in lora_before and not torch.equal(param.data, lora_before[name]) + ) + assert changed > 0, "No LoRA weights changed after 5 training steps" + + +class TestSonicMoEGateOnlyLoRA: + """Verify LoRA targeting only the gate (router) works with SonicMoE.""" + + def teardown_method(self): + _unpatch_sonicmoe() + + def test_gate_only_lora_loss_decreases(self): + """LoRA only on gate — expert path should have zero materialization overhead.""" + from transformers import AutoModelForCausalLM + + from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe + + config = _create_tiny_qwen3_config() + input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") + + model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + patch_sonicmoe("qwen3_moe") + _interleave_gate_up_weights(model) + # Only target the gate (router), not expert projections + model = _apply_lora(model, ["gate"]) + + optimizer = torch.optim.AdamW( + [p for p in model.parameters() if p.requires_grad], lr=1e-3 + ) + losses = [] + + for step in range(20): + out = model(input_ids, labels=input_ids) + loss = out.loss + assert not math.isnan(loss.item()), f"NaN loss at step {step}" + assert not math.isinf(loss.item()), f"Inf loss at step {step}" + losses.append(loss.item()) + + loss.backward() + optimizer.step() + optimizer.zero_grad() + + assert losses[-1] < losses[0], ( + f"Loss did not decrease: first={losses[0]:.4f}, last={losses[-1]:.4f}" + ) + + +class TestSonicMoENoLoRARegression: + """Verify SonicMoE without LoRA still works after LoRA code was added.""" + + def teardown_method(self): + _unpatch_sonicmoe() + + def test_no_lora_loss_decreases(self): + """Full fine-tuning (no PEFT) with SonicMoE — regression test.""" + from transformers import AutoModelForCausalLM + + from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe + + config = _create_tiny_qwen3_config() + input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") + + model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + patch_sonicmoe("qwen3_moe") + _interleave_gate_up_weights(model) + + optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3) + losses = [] + + for step in range(20): + out = model(input_ids, labels=input_ids) + loss = out.loss + assert not math.isnan(loss.item()), f"NaN loss at step {step}" + assert not math.isinf(loss.item()), f"Inf loss at step {step}" + losses.append(loss.item()) + + loss.backward() + optimizer.step() + optimizer.zero_grad() + + assert losses[-1] < losses[0], ( + f"Loss did not decrease: first={losses[0]:.4f}, last={losses[-1]:.4f}" + ) diff --git a/tests/integrations/test_routing_parity.py b/tests/integrations/test_routing_parity.py index cc668671c6..8852068096 100644 --- a/tests/integrations/test_routing_parity.py +++ b/tests/integrations/test_routing_parity.py @@ -93,7 +93,9 @@ def test_weights_match(self): from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( _softmax_topk_route, ) - from axolotl.integrations.kernels.sonicmoe.routing import softmax_topk_routing + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( + softmax_topk_routing, + ) moe_block, gate, hidden, T, H, E, K = _make_softmax_block() @@ -120,7 +122,9 @@ def test_weights_match(self): def test_logits_not_returned_by_scattermoe(self): """ScatterMoE doesn't return logits; SonicMoE does — verify SonicMoE logits shape.""" - from axolotl.integrations.kernels.sonicmoe.routing import softmax_topk_routing + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( + softmax_topk_routing, + ) moe_block, gate, hidden, T, H, E, K = _make_softmax_block() _, _, _, logits = softmax_topk_routing(hidden, moe_block) @@ -131,7 +135,9 @@ def test_no_renorm(self): from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( _softmax_topk_route, ) - from axolotl.integrations.kernels.sonicmoe.routing import softmax_topk_routing + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( + softmax_topk_routing, + ) moe_block, gate, hidden, T, H, E, K = _make_softmax_block() gate.norm_topk_prob = False @@ -152,7 +158,9 @@ def test_various_expert_counts(self): from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( _softmax_topk_route, ) - from axolotl.integrations.kernels.sonicmoe.routing import softmax_topk_routing + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( + softmax_topk_routing, + ) for E, K in [(2, 1), (8, 2), (16, 4), (32, 8)]: moe_block, gate, hidden, T, H, _, _ = _make_softmax_block(E=E, K=K) @@ -190,7 +198,9 @@ def test_weights_match_with_groups(self): from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( _sigmoid_topk_route, ) - from axolotl.integrations.kernels.sonicmoe.routing import sigmoid_topk_routing + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( + sigmoid_topk_routing, + ) moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( E=16, K=4, n_group=2, topk_group=1, bias_on_gate=True @@ -226,7 +236,9 @@ def test_weights_match_no_groups(self): from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( _sigmoid_topk_route, ) - from axolotl.integrations.kernels.sonicmoe.routing import sigmoid_topk_routing + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( + sigmoid_topk_routing, + ) moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( E=16, K=4, n_group=1, topk_group=1, bias_on_gate=True @@ -254,7 +266,9 @@ def test_bias_on_block_parity(self): from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( _sigmoid_topk_route, ) - from axolotl.integrations.kernels.sonicmoe.routing import sigmoid_topk_routing + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( + sigmoid_topk_routing, + ) moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( E=16, K=4, n_group=1, bias_on_gate=False @@ -281,7 +295,9 @@ def test_scaling_factor_parity(self): from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( _sigmoid_topk_route, ) - from axolotl.integrations.kernels.sonicmoe.routing import sigmoid_topk_routing + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( + sigmoid_topk_routing, + ) moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( n_group=1, bias_on_gate=True @@ -309,7 +325,9 @@ def test_no_renorm_parity(self): from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( _sigmoid_topk_route, ) - from axolotl.integrations.kernels.sonicmoe.routing import sigmoid_topk_routing + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( + sigmoid_topk_routing, + ) moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( n_group=1, bias_on_gate=True @@ -349,7 +367,7 @@ def _get_both_fns(self): from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( _compute_shared_expert as scatter_compute, ) - from axolotl.integrations.kernels.sonicmoe.patch import ( + from axolotl.integrations.kernels.libs.sonicmoe.patch import ( _compute_shared_expert as sonic_compute, ) diff --git a/tests/integrations/test_sonicmoe.py b/tests/integrations/test_sonicmoe.py index 7d26d9d936..864abca36d 100644 --- a/tests/integrations/test_sonicmoe.py +++ b/tests/integrations/test_sonicmoe.py @@ -6,11 +6,11 @@ import torch from axolotl.integrations.kernels.args import KernelsArgs -from axolotl.integrations.kernels.sonicmoe.routing import ( +from axolotl.integrations.kernels.libs.sonicmoe.routing import ( sigmoid_topk_routing, softmax_topk_routing, ) -from axolotl.integrations.kernels.sonicmoe.weight_converter import ( +from axolotl.integrations.kernels.libs.sonicmoe.weight_converter import ( ConcatenatedToInterleaved, InterleavedToConcatenated, register_sonicmoe_weight_converter, @@ -212,9 +212,40 @@ def test_double_registration_is_idempotent(self): ) break - def test_register_unsupported_model_type_warns(self): - # A model type with no conversion mapping should warn but not raise - register_sonicmoe_weight_converter("nonexistent_model_type_xyz") + def test_register_adds_same_key_converter(self): + from transformers.conversion_mapping import get_checkpoint_conversion_mapping + + register_sonicmoe_weight_converter("qwen3_moe") + + modified = get_checkpoint_conversion_mapping("qwen3_moe") + # Should have a same-key converter for already-fused checkpoints + same_key = [ + c + for c in modified + if hasattr(c, "source_patterns") + and c.source_patterns == ["mlp.experts.gate_up_proj"] + and c.target_patterns == ["mlp.experts.gate_up_proj"] + ] + assert len(same_key) == 1 + assert isinstance(same_key[0].operations[0], ConcatenatedToInterleaved) + + def test_register_creates_mapping_when_none(self): + from transformers.conversion_mapping import get_checkpoint_conversion_mapping + + # qwen3_5_moe has no conversion mapping in transformers + register_sonicmoe_weight_converter("qwen3_5_moe") + + mapping = get_checkpoint_conversion_mapping("qwen3_5_moe") + assert mapping is not None + same_key = [ + c + for c in mapping + if hasattr(c, "source_patterns") + and c.source_patterns == ["mlp.experts.gate_up_proj"] + and c.target_patterns == ["mlp.experts.gate_up_proj"] + ] + assert len(same_key) == 1 + assert isinstance(same_key[0].operations[0], ConcatenatedToInterleaved) def _make_qwen_moe_block(T=8, H=16, E=4, K=2): @@ -462,7 +493,7 @@ class TestSoftmaxBiasTopkRouting: """Tests for Ernie 4.5 MoE routing (softmax_bias_topk_routing).""" def test_output_shapes(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_bias_topk_routing, ) @@ -479,7 +510,7 @@ def test_output_shapes(self): assert logits.shape == (T, E) def test_scores_are_float32(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_bias_topk_routing, ) @@ -490,7 +521,7 @@ def test_scores_are_float32(self): assert scores.dtype == torch.float32 def test_token_indices_sorted_ascending(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_bias_topk_routing, ) @@ -502,7 +533,7 @@ def test_token_indices_sorted_ascending(self): assert (diffs >= 0).all() def test_expert_indices_in_range(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_bias_topk_routing, ) @@ -514,7 +545,7 @@ def test_expert_indices_in_range(self): assert (expert_idx < E).all() def test_renormalized_scores_sum_to_one(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_bias_topk_routing, ) @@ -527,7 +558,7 @@ def test_renormalized_scores_sum_to_one(self): def test_bias_affects_expert_selection(self): """Large positive bias on expert 0 should make it always selected.""" - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_bias_topk_routing, ) @@ -570,7 +601,7 @@ class TestSoftmaxGroupLimitedTopkRouting: """Tests for DeepSeek V2 routing (softmax_group_limited_topk_routing).""" def test_output_shapes_group_limited(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_group_limited_topk_routing, ) @@ -589,7 +620,7 @@ def test_output_shapes_group_limited(self): assert logits.shape == (T, E) def test_output_shapes_greedy(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_group_limited_topk_routing, ) @@ -604,7 +635,7 @@ def test_output_shapes_greedy(self): assert logits.shape == (T, E) def test_scores_are_float32(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_group_limited_topk_routing, ) @@ -615,7 +646,7 @@ def test_scores_are_float32(self): assert scores.dtype == torch.float32 def test_token_indices_sorted_ascending(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_group_limited_topk_routing, ) @@ -627,7 +658,7 @@ def test_token_indices_sorted_ascending(self): assert (diffs >= 0).all() def test_expert_indices_in_range(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_group_limited_topk_routing, ) @@ -639,7 +670,7 @@ def test_expert_indices_in_range(self): assert (expert_idx < E).all() def test_scaling_factor_applied(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_group_limited_topk_routing, ) @@ -655,7 +686,7 @@ def test_scaling_factor_applied(self): def test_group_selection_restricts_experts(self): """With num_group=4 and topk_group=1, experts should come from selected groups.""" - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_group_limited_topk_routing, ) @@ -674,7 +705,7 @@ def test_group_selection_restricts_experts(self): assert (groups == groups[0]).all() def test_unsupported_topk_method_raises(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_group_limited_topk_routing, ) @@ -706,7 +737,7 @@ class TestSoftmaxTopkWgRouting: """Tests for HunYuan V1 MoE routing (softmax_topk_wg_routing).""" def test_output_shapes(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_topk_wg_routing, ) @@ -723,7 +754,7 @@ def test_output_shapes(self): assert logits.shape == (T, E) def test_scores_are_float32(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_topk_wg_routing, ) @@ -734,7 +765,7 @@ def test_scores_are_float32(self): assert scores.dtype == torch.float32 def test_token_indices_sorted_ascending(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_topk_wg_routing, ) @@ -746,7 +777,7 @@ def test_token_indices_sorted_ascending(self): assert (diffs >= 0).all() def test_expert_indices_in_range(self): - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_topk_wg_routing, ) @@ -759,7 +790,7 @@ def test_expert_indices_in_range(self): def test_renormalized_scores_sum_to_one(self): """HunYuan V1 always renormalizes (no norm_topk_prob flag).""" - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_topk_wg_routing, ) @@ -772,7 +803,7 @@ def test_renormalized_scores_sum_to_one(self): def test_uses_gate_wg_weight(self): """Verify that modifying gate.wg.weight changes the routing output.""" - from axolotl.integrations.kernels.sonicmoe.routing import ( + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( softmax_topk_wg_routing, ) diff --git a/tests/integrations/test_sonicmoe_gradients.py b/tests/integrations/test_sonicmoe_gradients.py index e76bdd4806..cb5ef7663d 100644 --- a/tests/integrations/test_sonicmoe_gradients.py +++ b/tests/integrations/test_sonicmoe_gradients.py @@ -7,7 +7,7 @@ import torch -from axolotl.integrations.kernels.sonicmoe.routing import ( +from axolotl.integrations.kernels.libs.sonicmoe.routing import ( sigmoid_topk_routing, softmax_topk_routing, ) diff --git a/tests/integrations/test_sonicmoe_lora.py b/tests/integrations/test_sonicmoe_lora.py new file mode 100644 index 0000000000..4b25843fee --- /dev/null +++ b/tests/integrations/test_sonicmoe_lora.py @@ -0,0 +1,328 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Unit tests for SonicMoE LoRA support.""" + +from unittest.mock import MagicMock + +import pytest +import torch + +from axolotl.integrations.kernels.libs.sonicmoe.lora import ( + MoELoRAMaterialize, + get_lora_params_from_wrapper, + has_lora, + materialize_expert_lora, + unwrap_experts_lora, + unwrap_gate_lora, +) + +# ============================================================================= +# Helpers: mock PEFT modules +# ============================================================================= + + +def _make_mock_lora_module(weight_A, weight_B, scaling_val, param_name=None): + """Create a mock PEFT-wrapped module with LoRA attributes.""" + mock = MagicMock() + + lora_A_linear = MagicMock() + lora_A_linear.weight = weight_A + + lora_B_linear = MagicMock() + lora_B_linear.weight = weight_B + + mock.lora_A = {"default": lora_A_linear} + mock.lora_B = {"default": lora_B_linear} + mock.scaling = {"default": scaling_val} + mock.active_adapters = ["default"] + + if param_name is not None: + mock.parameter_name = param_name + + return mock + + +def _make_peft_gate(hidden_size, num_experts, rank, scaling=0.5): + """Create a mock PEFT-wrapped gate module.""" + base_gate = MagicMock() + base_gate.weight = torch.randn(num_experts, hidden_size) + base_gate.top_k = 2 + base_gate.norm_topk_prob = True + + lora_A = torch.randn(rank, hidden_size) + lora_B = torch.randn(num_experts, rank) + + wrapper = _make_mock_lora_module(lora_A, lora_B, scaling) + wrapper.base_layer = base_gate + return wrapper, base_gate + + +def _make_peft_experts( + num_experts, gate_up_dim, down_dim, hidden_size, rank, scaling=0.5 +): + """Create a mock PEFT-wrapped experts chain. + + Simulates: ParamWrapper(down_proj) -> ParamWrapper(gate_up_proj) -> Experts + """ + base_experts = MagicMock() + base_experts.gate_up_proj = torch.randn(num_experts, gate_up_dim, hidden_size) + base_experts.down_proj = torch.randn(num_experts, hidden_size, down_dim) + # Remove base_layer and lora_A from base_experts so the chain walk stops + del base_experts.base_layer + del base_experts.lora_A + + # gate_up_proj wrapper + gup_A = torch.randn(rank * num_experts, hidden_size) + gup_B = torch.randn(gate_up_dim, rank * num_experts) + gup_wrapper = _make_mock_lora_module(gup_A, gup_B, scaling, "gate_up_proj") + gup_wrapper.base_layer = base_experts + + # down_proj wrapper (outermost) + down_A = torch.randn(rank * num_experts, down_dim) + down_B = torch.randn(hidden_size, rank * num_experts) + down_wrapper = _make_mock_lora_module(down_A, down_B, scaling, "down_proj") + down_wrapper.base_layer = gup_wrapper + + return down_wrapper, base_experts, (gup_A, gup_B), (down_A, down_B) + + +# ============================================================================= +# Tests: has_lora +# ============================================================================= + + +class TestHasLora: + def test_plain_module(self): + m = MagicMock(spec=["weight"]) + del m.base_layer + del m.lora_A + assert not has_lora(m) + + def test_wrapped_module(self): + m = MagicMock() + m.base_layer = MagicMock() + m.lora_A = {"default": MagicMock()} + assert has_lora(m) + + +# ============================================================================= +# Tests: get_lora_params_from_wrapper +# ============================================================================= + + +class TestGetLoraParams: + def test_no_lora_attrs(self): + m = MagicMock(spec=["weight"]) + del m.lora_A + del m.lora_B + assert get_lora_params_from_wrapper(m) == (None, None, None) + + def test_extracts_params(self): + A = torch.randn(4, 8) + B = torch.randn(16, 4) + wrapper = _make_mock_lora_module(A, B, 0.5) + lora_A, lora_B, scaling = get_lora_params_from_wrapper(wrapper) + assert torch.equal(lora_A, A) + assert torch.equal(lora_B, B) + assert scaling == 0.5 + + def test_no_active_adapters(self): + wrapper = _make_mock_lora_module(torch.randn(4, 8), torch.randn(16, 4), 0.5) + wrapper.active_adapters = [] + assert get_lora_params_from_wrapper(wrapper) == (None, None, None) + + +# ============================================================================= +# Tests: unwrap_gate_lora +# ============================================================================= + + +class TestUnwrapGateLora: + def test_plain_gate(self): + gate = MagicMock(spec=["weight", "top_k"]) + del gate.base_layer + del gate.lora_A + gate.weight = torch.randn(8, 64) + base, weight, delta = unwrap_gate_lora(gate) + assert base is gate + assert torch.equal(weight, gate.weight) + assert delta is None + + def test_wrapped_gate(self): + wrapper, base_gate = _make_peft_gate( + hidden_size=64, num_experts=8, rank=4, scaling=0.5 + ) + base, weight, delta = unwrap_gate_lora(wrapper) + assert base is base_gate + assert torch.equal(weight, base_gate.weight) + assert delta is not None + assert delta.shape == base_gate.weight.shape + + # Verify delta = scaling * B @ A + lora_A = wrapper.lora_A["default"].weight + lora_B = wrapper.lora_B["default"].weight + expected = 0.5 * (lora_B @ lora_A) + assert torch.allclose(delta, expected) + + +# ============================================================================= +# Tests: unwrap_experts_lora +# ============================================================================= + + +class TestUnwrapExpertsLora: + def test_plain_experts(self): + experts = MagicMock(spec=["gate_up_proj", "down_proj"]) + del experts.base_layer + del experts.lora_A + base, lora_dict = unwrap_experts_lora(experts) + assert base is experts + assert lora_dict == {} + + def test_wrapped_experts(self): + E, I2, I, H, r = 4, 256, 128, 64, 8 # noqa: E741 + wrapper, base_experts, (gup_A, gup_B), (down_A, down_B) = _make_peft_experts( + E, I2, I, H, r, scaling=0.25 + ) + base, lora_dict = unwrap_experts_lora(wrapper) + assert base is base_experts + assert "gate_up_proj" in lora_dict + assert "down_proj" in lora_dict + + gup_lA, gup_lB, gup_s = lora_dict["gate_up_proj"] + assert torch.equal(gup_lA, gup_A) + assert torch.equal(gup_lB, gup_B) + assert gup_s == 0.25 + + down_lA, down_lB, down_s = lora_dict["down_proj"] + assert torch.equal(down_lA, down_A) + assert torch.equal(down_lB, down_B) + assert down_s == 0.25 + + def test_partial_lora(self): + """Only gate_up_proj has LoRA, down_proj does not.""" + base_experts = MagicMock(spec=["gate_up_proj", "down_proj"]) + del base_experts.base_layer + del base_experts.lora_A + + gup_A = torch.randn(16, 64) + gup_B = torch.randn(256, 16) + gup_wrapper = _make_mock_lora_module(gup_A, gup_B, 0.5, "gate_up_proj") + gup_wrapper.base_layer = base_experts + + base, lora_dict = unwrap_experts_lora(gup_wrapper) + assert base is base_experts + assert "gate_up_proj" in lora_dict + assert "down_proj" not in lora_dict + + +# ============================================================================= +# Tests: MoELoRAMaterialize +# ============================================================================= + + +class TestMoELoRAMaterialize: + @pytest.fixture() + def setup(self): + E, dim1, dim2, r = 4, 32, 16, 4 + scaling = 0.5 + W = torch.randn(E, dim1, dim2, dtype=torch.float64, requires_grad=False) + A = torch.randn(r * E, dim2, dtype=torch.float64, requires_grad=True) + B = torch.randn(dim1, r * E, dtype=torch.float64, requires_grad=True) + return W, A, B, scaling, E, r + + def test_forward_shape(self, setup): + W, A, B, scaling, E, r = setup + W_eff = MoELoRAMaterialize.apply(W, A, B, scaling) + assert W_eff.shape == W.shape + + def test_forward_correctness(self, setup): + W, A, B, scaling, E, r = setup + W_eff = MoELoRAMaterialize.apply(W, A, B, scaling) + + # Manual per-expert computation. + # lora_A is expert-major: [r*E, dim2] -> rows [e*r:(e+1)*r] = expert e + # lora_B is rank-major: [dim1, r*E] -> reshape [dim1, r, E], slice [:, :, e] + _, dim1, dim2 = W.shape + expected = W.clone() + B_3d = B.reshape(dim1, r, E) + for e in range(E): + A_e = A[e * r : (e + 1) * r, :] # [r, dim2] + B_e = B_3d[:, :, e] # [dim1, r] + expected[e] += scaling * (B_e @ A_e) + + assert torch.allclose(W_eff, expected, atol=1e-10) + + def test_backward_gradcheck(self, setup): + W, A, B, scaling, E, r = setup + # gradcheck requires float64 + assert torch.autograd.gradcheck( + lambda a, b: MoELoRAMaterialize.apply(W, a, b, scaling), + (A, B), + eps=1e-6, + atol=1e-4, + ) + + def test_no_grad_for_base_weight(self, setup): + W, A, B, scaling, E, r = setup + W.requires_grad_(True) + W_eff = MoELoRAMaterialize.apply(W, A, B, scaling) + loss = W_eff.sum() + loss.backward() + assert W.grad is None + assert A.grad is not None + assert B.grad is not None + + def test_scaling_zero(self, setup): + W, A, B, _, E, r = setup + W_eff = MoELoRAMaterialize.apply(W, A, B, 0.0) + assert torch.allclose(W_eff, W) + + def test_gate_up_proj_shapes(self): + """Test with realistic gate_up_proj shapes [E, 2*I, H].""" + E, I2, H, r = 8, 512, 256, 16 + W = torch.randn(E, I2, H, dtype=torch.float64) + A = torch.randn(r * E, H, dtype=torch.float64, requires_grad=True) + B = torch.randn(I2, r * E, dtype=torch.float64, requires_grad=True) + W_eff = MoELoRAMaterialize.apply(W, A, B, 1.0) + assert W_eff.shape == (E, I2, H) + loss = W_eff.sum() + loss.backward() + assert A.grad.shape == A.shape + assert B.grad.shape == B.shape + + def test_down_proj_shapes(self): + """Test with realistic down_proj shapes [E, H, I].""" + E, H, I, r = 8, 256, 512, 16 # noqa: E741 + W = torch.randn(E, H, I, dtype=torch.float64) + A = torch.randn(r * E, I, dtype=torch.float64, requires_grad=True) + B = torch.randn(H, r * E, dtype=torch.float64, requires_grad=True) + W_eff = MoELoRAMaterialize.apply(W, A, B, 1.0) + assert W_eff.shape == (E, H, I) + loss = W_eff.sum() + loss.backward() + assert A.grad.shape == A.shape + assert B.grad.shape == B.shape + + +# ============================================================================= +# Tests: materialize_expert_lora +# ============================================================================= + + +class TestMaterializeExpertLora: + def test_none_passthrough(self): + W = torch.randn(4, 32, 16) + result = materialize_expert_lora(W, None) + assert result is W + + def test_with_lora(self): + E, dim1, dim2, r = 4, 32, 16, 4 + W = torch.randn(E, dim1, dim2) + A = torch.randn(r * E, dim2, requires_grad=True) + B = torch.randn(dim1, r * E, requires_grad=True) + result = materialize_expert_lora(W, (A, B, 0.5)) + assert result.shape == W.shape + assert not torch.equal(result, W) From 573726c839d6e1f275da75a4cd30447dfefd1fdb Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 2 Apr 2026 10:18:00 -0400 Subject: [PATCH 1250/1405] upgrade torchao to 0.17.0 (#3569) * upgrade to torchao 0.17.0 * upgrade mistral-common too * chore: lint * patch fix for torchao low bit optimizers * fix up * propagate dtype * fix test for ao change * address PR comments --- requirements.txt | 4 +- src/axolotl/core/builders/base.py | 2 +- src/axolotl/loaders/patch_manager.py | 7 ++ src/axolotl/monkeypatch/torchao_optim.py | 154 +++++++++++++++++++++++ src/axolotl/utils/quantization.py | 29 ++--- tests/e2e/test_quantization.py | 14 +-- 6 files changed, 178 insertions(+), 32 deletions(-) create mode 100644 src/axolotl/monkeypatch/torchao_optim.py diff --git a/requirements.txt b/requirements.txt index 2bd4c4aebf..fb429df902 100644 --- a/requirements.txt +++ b/requirements.txt @@ -66,7 +66,7 @@ langdetect==1.0.9 immutabledict==4.2.0 antlr4-python3-runtime==4.13.2 -torchao==0.16.0 +torchao==0.17.0 openenv-core==0.1.0 schedulefree==1.4.1 @@ -75,4 +75,4 @@ axolotl-contribs-mit==0.0.6 # telemetry posthog==6.7.11 -mistral-common==1.10.0 +mistral-common==1.11.0 diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 90c8139277..9dba48b88e 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -329,7 +329,7 @@ def _configure_custom_optimizer( optimizer_cls = AdamW optimizer_kwargs.update(adam_kwargs) elif self.cfg.optimizer == "ao_adamw_fp8": - from torchao.prototype.low_bit_optim import AdamWFp8 + from torchao.optim.adam import AdamWFp8 optimizer_cls = AdamWFp8 optimizer_kwargs.update(adam_kwargs) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index aa67c3b1e8..018ca52a05 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -95,6 +95,7 @@ def has_flash_attn(self) -> bool: def apply_pre_model_load_patches(self): """Apply pre-model load patches based on config.""" self._deactivate_hf_async_load() + self._apply_torchao_patches() self._apply_transformers_patches() # self._apply_flex_attention_patches() self._apply_flash_attention_patches() @@ -125,6 +126,12 @@ def apply_post_plugin_pre_model_load_patches(self): self._apply_tiled_mlp(self.cfg.model_config_type) self._apply_moe_expert_quantization_patch() + @staticmethod + def _apply_torchao_patches(): + from axolotl.monkeypatch.torchao_optim import patch_torchao_optim_state_8bit + + patch_torchao_optim_state_8bit() + def _apply_transformers_patches(self): from axolotl.monkeypatch.transformers.trainer_loss_calc import ( patch_evaluation_loop, diff --git a/src/axolotl/monkeypatch/torchao_optim.py b/src/axolotl/monkeypatch/torchao_optim.py new file mode 100644 index 0000000000..98c325a0b0 --- /dev/null +++ b/src/axolotl/monkeypatch/torchao_optim.py @@ -0,0 +1,154 @@ +""" +Patch for torchao optim subclasses that crash under torch.compile. + +torchao 0.17.0 PR #3934 added an "appearance dtype" to OptimState{4,8}bit and +OptimStateFp8, allowing them to report as e.g. bf16 while internally storing +quantized codes. Three issues: + +1. aten.view.default doesn't propagate the appearance dtype, so views (e.g. from + DTensor.from_local()) revert to float32 while the base is bf16. torch.compile's + fake-tensor metadata check then fails (AssertionError: torch.bfloat16 != torch.float32). + +2. aten._to_copy doesn't clone internal tensors, so same-device dtype changes + (e.g. .float()) create an accidental view relationship with the same issue. + +3. aten.view.dtype is unimplemented, so if the dtype-view path IS taken, it crashes + with NotImplementedError. + +Fix: propagate dtype in view.default (primary), clone in _to_copy, register view.dtype. + +Upstream fix: https://github.com/pytorch/ao/pull/4216 +""" + +import torch +from torch.utils._python_dispatch import return_and_correct_aliasing + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +aten = torch.ops.aten + + +def _needs_view_dtype_patch(cls): + """Check if a subclass is missing aten.view.dtype.""" + op_table = getattr(cls, "_ATEN_OP_TABLE", {}).get(cls, {}) + return aten.view.dtype not in op_table + + +def patch_torchao_optim_state_8bit(): + """Patch torchao optim subclasses for torch.compile compatibility.""" + try: + from torchao.optim.subclass_8bit import OptimState8bit + except ImportError: + return + + # Patch view.default to propagate appearance dtype + @OptimState8bit.implements(aten.view.default) + def _(func, types, args, kwargs): + x, shape = args + return OptimState8bit( + x.codes.view(shape), x.scale, x.qmap, x.signed, dtype=x.dtype + ) + + # Patch _to_copy to clone internal tensors (breaks accidental view) + @OptimState8bit.implements(aten._to_copy.default) + def _(func, types, args, kwargs): + dtype = kwargs.get("dtype", args[0].dtype) + device = kwargs.get("device", None) + out = OptimState8bit( + args[0].codes.to(device=device).clone(), + args[0].scale.to(device=device).clone(), + args[0].qmap.to(device=device).clone(), + args[0].signed, + dtype=dtype, + ) + return return_and_correct_aliasing(func, args, kwargs, out) + + if _needs_view_dtype_patch(OptimState8bit): + + @OptimState8bit.implements(aten.view.dtype) + def _(func, types, args, kwargs): + x, dtype = args + return OptimState8bit(x.codes, x.scale, x.qmap, x.signed, dtype=dtype) + + LOG.debug("Patched OptimState8bit for torch.compile compatibility") + + try: + from torchao.optim.subclass_4bit import OptimState4bit + except ImportError: + OptimState4bit = None + + if OptimState4bit is not None: + + @OptimState4bit.implements(aten.view.default) + def _(func, types, args, kwargs): + x, shape = args + if tuple(x.shape) == tuple(shape): + return OptimState4bit( + x.codes, x.scale, x.qmap, x.signed, x._shape, dtype=x.dtype + ) + if len(shape) == 1 and shape[0] == -1: + return OptimState4bit( + x.codes, x.scale, x.qmap, x.signed, (x.numel(),), dtype=x.dtype + ) + raise ValueError( + f"{x.__class__.__name__} only supports .view() with same shape or shape=[-1]" + ) + + @OptimState4bit.implements(aten._to_copy.default) + def _(func, types, args, kwargs): + dtype = kwargs.get("dtype", args[0].dtype) + device = kwargs.get("device", None) + out = OptimState4bit( + args[0].codes.to(device=device).clone(), + args[0].scale.to(device=device).clone(), + args[0].qmap.to(device=device).clone(), + args[0].signed, + args[0].shape, + dtype=dtype, + ) + return return_and_correct_aliasing(func, args, kwargs, out) + + if _needs_view_dtype_patch(OptimState4bit): + + @OptimState4bit.implements(aten.view.dtype) + def _(func, types, args, kwargs): + x, dtype = args + return OptimState4bit( + x.codes, x.scale, x.qmap, x.signed, x.shape, dtype=dtype + ) + + LOG.debug("Patched OptimState4bit for torch.compile compatibility") + + try: + from torchao.optim.subclass_fp8 import OptimStateFp8 + except ImportError: + OptimStateFp8 = None + + if OptimStateFp8 is not None: + + @OptimStateFp8.implements(aten.view.default) + def _(func, types, args, kwargs): + x, shape = args + return OptimStateFp8(x.codes.view(shape), x.scale, dtype=x.dtype) + + @OptimStateFp8.implements(aten._to_copy.default) + def _(func, types, args, kwargs): + dtype = kwargs.get("dtype", args[0].dtype) + device = kwargs.get("device", None) + out = OptimStateFp8( + args[0].codes.to(device=device).clone(), + args[0].scale.to(device=device).clone(), + dtype=dtype, + ) + return return_and_correct_aliasing(func, args, kwargs, out) + + if _needs_view_dtype_patch(OptimStateFp8): + + @OptimStateFp8.implements(aten.view.dtype) + def _(func, types, args, kwargs): + x, dtype = args + return OptimStateFp8(x.codes, x.scale, dtype=dtype) + + LOG.debug("Patched OptimStateFp8 for torch.compile compatibility") diff --git a/src/axolotl/utils/quantization.py b/src/axolotl/utils/quantization.py index 6a479d2607..04b2c63416 100644 --- a/src/axolotl/utils/quantization.py +++ b/src/axolotl/utils/quantization.py @@ -6,6 +6,7 @@ from packaging import version from torchao.core.config import AOBaseConfig from torchao.quantization import quantize_ +from torchao.quantization.granularity import PerGroup from torchao.quantization.qat import ( QATConfig, ) @@ -14,30 +15,22 @@ Float8DynamicActivationFloat8WeightConfig, Float8DynamicActivationInt4WeightConfig, Int4WeightOnlyConfig, + Int8DynamicActivationIntxWeightConfig, ) -try: - from torchao.quantization.quant_api import Int8DynamicActivationInt4WeightConfig -except ImportError: - from torchao.quantization.quant_api import ( - Int8DynamicActivationIntxWeightConfig as Int8DynamicActivationInt4WeightConfig, - ) - from axolotl.utils.schemas.enums import TorchAOQuantDType quantization_config_to_str = { - Int8DynamicActivationInt4WeightConfig: "int8int4", + Int8DynamicActivationIntxWeightConfig: "int8int4", Float8DynamicActivationFloat8WeightConfig: "fp8fp8", Float8DynamicActivationInt4WeightConfig: "fp8int4", } if version.parse(torch.__version__) >= version.parse("2.8.0"): try: - from torchao.prototype.mx_formats import ( - NVFP4WeightOnlyConfig as NVFP4InferenceConfig, - ) + from torchao.prototype.mx_formats import NVFP4WeightOnlyConfig - quantization_config_to_str[NVFP4InferenceConfig] = "nvfp4" + quantization_config_to_str[NVFP4WeightOnlyConfig] = "nvfp4" except (ImportError, RuntimeError): pass @@ -108,10 +101,10 @@ def get_quantization_config( activation_dtype == TorchAOQuantDType.int8 and weight_dtype == TorchAOQuantDType.int4 ): + kwargs = {"weight_dtype": torch.int4} if group_size is not None: - return Int8DynamicActivationInt4WeightConfig(group_size=group_size) - else: - return Int8DynamicActivationInt4WeightConfig() + kwargs["weight_granularity"] = PerGroup(group_size=group_size) + return Int8DynamicActivationIntxWeightConfig(**kwargs) if ( activation_dtype == TorchAOQuantDType.float8_e4m3fn and weight_dtype == TorchAOQuantDType.float8_e4m3fn @@ -123,13 +116,11 @@ def get_quantization_config( ): return Float8DynamicActivationInt4WeightConfig() if weight_dtype == TorchAOQuantDType.nvfp4: - from torchao.prototype.mx_formats import ( - NVFP4WeightOnlyConfig as NVFP4InferenceConfig, - ) + from torchao.prototype.mx_formats import NVFP4WeightOnlyConfig if group_size is not None and group_size != 16: raise ValueError("NVFP4 quantization must use a group_size of 16") - return NVFP4InferenceConfig() + return NVFP4WeightOnlyConfig() if weight_dtype == TorchAOQuantDType.mxfp4: # MXFP4 uses block_size=32 by default (vs NVFP4's 16) diff --git a/tests/e2e/test_quantization.py b/tests/e2e/test_quantization.py index 6bbc34949c..a70a46194c 100644 --- a/tests/e2e/test_quantization.py +++ b/tests/e2e/test_quantization.py @@ -5,20 +5,14 @@ import pytest import torch from torch import nn -from torchao.quantization import LinearActivationQuantizedTensor +from torchao.quantization import IntxUnpackedToInt8Tensor from torchao.quantization.qat.embedding import FakeQuantizedEmbedding from torchao.quantization.qat.linear import FakeQuantizedLinear from torchao.quantization.quant_api import ( Float8DynamicActivationFloat8WeightConfig, Float8DynamicActivationInt4WeightConfig, + Int8DynamicActivationIntxWeightConfig, ) - -try: - from torchao.quantization.quant_api import Int8DynamicActivationInt4WeightConfig -except ImportError: - from torchao.quantization.quant_api import ( - Int8DynamicActivationIntxWeightConfig as Int8DynamicActivationInt4WeightConfig, - ) from torchao.quantization.quantize_.workflows.int4.int4_tensor import Int4Tensor from transformers import AutoModelForCausalLM from transformers.trainer_callback import TrainerState @@ -71,7 +65,7 @@ def model(): TorchAOQuantDType.int4, TorchAOQuantDType.int8, None, - Int8DynamicActivationInt4WeightConfig, + Int8DynamicActivationIntxWeightConfig, ), ( TorchAOQuantDType.float8_e4m3fn, @@ -96,7 +90,7 @@ def model(): 8, False, None, - LinearActivationQuantizedTensor, + IntxUnpackedToInt8Tensor, ), # ( # TorchAOQuantDType.int4, From 08fc7de87e79f38c367f6776c5111b40a914062e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 2 Apr 2026 17:46:46 -0400 Subject: [PATCH 1251/1405] gemma4 support (#3574) * gemma4 support * fixes * chore: lint --- examples/gemma4/26b-a4b-moe-qlora.yaml | 104 ++ requirements.txt | 2 +- src/axolotl/core/trainers/base.py | 31 +- src/axolotl/integrations/kernels/README.md | 23 +- src/axolotl/integrations/kernels/args.py | 12 +- src/axolotl/integrations/kernels/constants.py | 44 + .../libs/scattermoe_lora/gemma4_experts.py | 235 ++++ .../kernels/libs/sonicmoe/gemma4_experts.py | 106 ++ .../kernels/libs/sonicmoe/routing.py | 73 ++ src/axolotl/integrations/kernels/plugin.py | 52 +- src/axolotl/monkeypatch/lora_kernels.py | 76 +- .../monkeypatch/models/qwen3_next/modeling.py | 41 +- .../chat_templates/templates/gemma4.jinja | 271 +++++ src/axolotl/utils/schemas/enums.py | 1 + src/axolotl/utils/schemas/trl.py | 4 +- tests/integrations/test_gemma4_moe.py | 1052 +++++++++++++++++ 16 files changed, 2082 insertions(+), 45 deletions(-) create mode 100644 examples/gemma4/26b-a4b-moe-qlora.yaml create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_experts.py create mode 100644 src/axolotl/integrations/kernels/libs/sonicmoe/gemma4_experts.py create mode 100644 src/axolotl/utils/chat_templates/templates/gemma4.jinja create mode 100644 tests/integrations/test_gemma4_moe.py diff --git a/examples/gemma4/26b-a4b-moe-qlora.yaml b/examples/gemma4/26b-a4b-moe-qlora.yaml new file mode 100644 index 0000000000..0972b93f68 --- /dev/null +++ b/examples/gemma4/26b-a4b-moe-qlora.yaml @@ -0,0 +1,104 @@ +# Gemma 4 26B-A4B MoE QLoRA with ScatterMoE kernels +# +# Validated: 50 steps on FineTome-100k, loss 7.4 -> 2.4, single RTX 5090 (32GB) +# +# Key notes: +# - Flash Attention 2 is NOT supported (global_head_dim=512 > FA2 max of 256). +# Use sdp_attention instead. +# - Gemma 4 is multimodal (text+vision+audio). For text-only SFT, restrict +# LoRA to the text backbone via lora_target_linear_modules regex. +# - MoE experts use `experts_implementation: scattermoe` — Gemma 4 embeds MoE +# directly in the decoder layer (no SparseMoeBlock), so we register ScatterMoE +# via the transformers ExpertsInterface. +# - Expert LoRA targets are `experts.gate_up_proj` / `experts.down_proj` +# (no `mlp.` prefix, unlike Qwen/Mixtral). +# - micro_batch_size: 1 fits 2048 seq_len on 32GB GPU with SDP attention. +# Use micro_batch_size: 4 with 1024 seq_len, or on 48GB+ GPUs. + +base_model: google/gemma-4-26B-A4B + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.kernels.KernelsPlugin + - axolotl.integrations.liger.LigerPlugin +use_kernels: true +use_scattermoe: true +experts_implementation: scattermoe +torch_compile: false +liger_layer_norm: true +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_rms_norm_gated: true +strict: false + +chat_template: gemma4 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:10%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.05 +output_dir: ./outputs/gemma4-26b-a4b-qlora + +sequence_len: 2048 +sample_packing: true + +load_in_4bit: true +quantize_moe_experts: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0 + +# Restrict LoRA to text backbone only (skip vision/audio encoders). +# lora_target_modules is intentionally empty — all module targeting is done +# via regex in lora_target_linear_modules below. +lora_target_modules: [] +lora_target_linear_modules: + - language_model\.model\.layers\.\d+\.self_attn\.(q|k|v|o)_proj + +# MoE expert LoRA (3D Parameter tensors, not nn.Linear) +lora_target_parameters: + - experts.gate_up_proj + - experts.down_proj + +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +bnb_config_kwargs: + bnb_4bit_use_double_quant: true + +wandb_project: gemma4-qlora +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +activation_offloading: true +logging_steps: 1 + +# FA2 not supported — Gemma4 global_head_dim=512 exceeds FA2 max of 256 +flash_attention: false +sdp_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/requirements.txt b/requirements.txt index fb429df902..446febb83e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ packaging==26.0 huggingface_hub>=1.1.7 peft>=0.18.1 tokenizers>=0.22.1 -transformers==5.4.0 +transformers==5.5.0 accelerate==1.13.0 datasets==4.5.0 deepspeed>=0.18.6,<0.19.0 diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 5bc44a1ddf..6beff80554 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -381,6 +381,15 @@ def compute_loss( # Store per-step trainable tokens for throughput calculation self.state.tokens["trainable_tokens"] = trainable_tokens.detach().cpu() + # Gemma4 requires mm_token_type_ids during training (even for text-only). + # Inject zeros (= text token type) when not provided by the data collator. + if ( + "mm_token_type_ids" not in inputs + and "input_ids" in inputs + and getattr(getattr(model, "config", None), "model_type", None) == "gemma4" + ): + inputs["mm_token_type_ids"] = torch.zeros_like(inputs["input_ids"]) + if self.args.orpo_alpha: return self.orpo_compute_loss( model, @@ -508,12 +517,24 @@ def orpo_compute_loss( ) # Perform a single forward pass + forward_kwargs = { + "input_ids": concat_inputs["input_ids"], + "attention_mask": concat_inputs["attention_mask"], + "labels": concat_inputs["labels"], + } + # Gemma4 requires mm_token_type_ids during training (even for text-only) + if ( + getattr(getattr(model, "config", None), "model_type", None) == "gemma4" + and "mm_token_type_ids" not in concat_inputs + ): + forward_kwargs["mm_token_type_ids"] = torch.zeros_like( + concat_inputs["input_ids"] + ) + elif "mm_token_type_ids" in concat_inputs: + forward_kwargs["mm_token_type_ids"] = concat_inputs["mm_token_type_ids"] + outputs = model( - **{ - "input_ids": concat_inputs["input_ids"], - "attention_mask": concat_inputs["attention_mask"], - "labels": concat_inputs["labels"], - }, + **forward_kwargs, output_hidden_states=True, ) diff --git a/src/axolotl/integrations/kernels/README.md b/src/axolotl/integrations/kernels/README.md index a852cd6cfc..9293c1727b 100644 --- a/src/axolotl/integrations/kernels/README.md +++ b/src/axolotl/integrations/kernels/README.md @@ -28,7 +28,7 @@ use_scattermoe: true use_sonicmoe: true ``` -**Important:** Setting `experts_implementation` is incompatible with custom kernel options. +**Important:** Setting `experts_implementation` to `batched_mm` or `grouped_mm` is incompatible with custom kernel options. The exception is `experts_implementation: scattermoe`, which is used for models like Gemma 4 that embed MoE directly in the decoder layer (no SparseMoeBlock) and dispatch through the transformers `ExpertsInterface`. ### SonicMoE installation @@ -63,7 +63,7 @@ Both paths use the shared `resolve_moe_block_classes` utility in `constants.py` ## Model Support Matrix -All models use the **SwiGLU** activation (`act_fn(gate) * up`). Neither kernel currently supports non-SwiGLU MoE architectures. +Most models use the **SwiGLU** activation (`silu(gate) * up`). Gemma 4 uses **GEGLU** (`gelu(gate) * up`). ScatterMoE supports any gated activation (activation is applied in Python between kernel calls). SonicMoE supports SwiGLU, GEGLU, and REGLU via its `ActivationType` enum. ### Routing strategies @@ -76,6 +76,7 @@ All models use the **SwiGLU** activation (`act_fn(gate) * up`). Neither kernel c | softmax → bias correction → topk | Softmax, bias via `gate.moe_statics`, topk, gather from original probs, clamp-based renorm | No | Yes | | softmax → group_limited_greedy | Softmax, group selection (max per group), topk, scale only (no renorm) | No | Yes | | softmax → topk via gate.wg | Softmax, gate weight at `gate.wg.weight` (not `gate.weight`), always renormalize | No | Yes | +| softmax → topk + per_expert_scale | RMSNorm → scale → proj → softmax → topk → renorm → per-expert learned scales | Yes | Yes | | fused topk → softmax | Routing + expert computation fused in a single kernel | No | Planned | ### Per-model support @@ -102,10 +103,13 @@ All models use the **SwiGLU** activation (`act_fn(gate) * up`). Neither kernel c | `ernie4_5_moe` | ERNIE 4.5 MoE | softmax → bias → topk | No | **Yes** | | `deepseek_v2` | DeepSeek-V2 | softmax → group_limited_greedy | No | **Yes** | | `hunyuan_v1_moe` | HunYuan V1 MoE | softmax → topk (gate.wg) | No | **Yes** | +| `gemma4_text` | Gemma 4 (26B-A4B) | softmax → topk + per_expert_scale | **Yes**\*\* | **Yes**\*\* | | `gpt_oss` | GPT-OSS | fused topk → softmax | No | Planned | \* `glm4_moe_lite` with ScatterMoE may have issues — see Limitations. +\*\* Gemma 4 uses `experts_implementation: scattermoe` path (registered via `ExpertsInterface`) instead of SparseMoeBlock patching, since Gemma 4 embeds MoE directly in its decoder layer (no separate SparseMoeBlock). See the [Gemma 4 section](#gemma-4) below. + ### Feature comparison | Feature | ScatterMoE | SonicMoE | @@ -131,6 +135,21 @@ Both kernels handle shared experts identically. Shared expert attribute names ar If `shared_expert_gate` exists, sigmoid gating is applied to the shared expert contribution before adding it to the routed output. PEFT wraps shared expert linear layers with standard LoRA — no special handling is needed. +## Gemma 4 + +Gemma 4 (e.g. `google/gemma-4-26B-A4B`) has a unique hybrid MoE architecture: + +- **No SparseMoeBlock**: MoE is embedded directly in the decoder layer alongside a dense MLP. Both run in parallel and their outputs are summed. +- **Custom router** (`Gemma4TextRouter`): RMSNorm → learned scale → linear projection → softmax → top-k → renormalization → per-expert learned scales. +- **GEGLU activation**: Uses `gelu_pytorch_tanh` (not SiLU/SwiGLU like most other MoE models). +- **128 experts, top-k=8** for the 26B-A4B variant. + +Because there is no SparseMoeBlock class to patch, Gemma 4 uses a different integration path: we register `"scattermoe"` as a custom implementation in the transformers `ExpertsInterface`, and set `experts_implementation: scattermoe` in the config. The `@use_experts_implementation` decorator on `Gemma4TextExperts` then dispatches to our ScatterMoE kernel automatically. The router is untouched — it runs as-is. + +**Important limitations:** +- **Flash Attention 2 is not supported** — Gemma 4 uses `global_head_dim: 512` for full attention layers, which exceeds FA2's maximum head dimension of 256. Use `sdp_attention: true` instead. +- **Multimodal model**: Gemma 4 includes vision and audio encoders. For text-only SFT, use `lora_target_linear_modules` with a regex to restrict LoRA to the text backbone (e.g. `language_model\.model\.layers\.\d+\.self_attn\.(q|k|v|o)_proj`). + ## Limitations - **ScatterMoE + GLM4-MoE Lite**: ScatterMoE does not work reliably for GLM 4.7 Flash (`glm4_moe_lite`). diff --git a/src/axolotl/integrations/kernels/args.py b/src/axolotl/integrations/kernels/args.py index 7c9e23b6c6..3afeb79c31 100644 --- a/src/axolotl/integrations/kernels/args.py +++ b/src/axolotl/integrations/kernels/args.py @@ -34,12 +34,20 @@ def check_use_kernels(cls, data): @classmethod def check_experts_implementation(cls, data): experts_implementation = data.get("experts_implementation") + use_scattermoe = data.get("use_scattermoe", False) if experts_implementation is None: # transformers may default to batched_mm when unset data["experts_implementation"] = "eager" - elif experts_implementation != "eager": + elif experts_implementation == "scattermoe" and not use_scattermoe: LOG.warning( - "`experts_implementation` must be set to 'eager' to use this. Automatically setting it to 'eager'." + "`experts_implementation='scattermoe'` requires `use_scattermoe: true`. " + "Automatically setting to 'eager'." + ) + data["experts_implementation"] = "eager" + elif experts_implementation not in ("eager", "scattermoe"): + LOG.warning( + f"`experts_implementation={experts_implementation!r}` is not compatible with " + f"custom MoE kernels. Automatically setting to 'eager'." ) data["experts_implementation"] = "eager" diff --git a/src/axolotl/integrations/kernels/constants.py b/src/axolotl/integrations/kernels/constants.py index a03761484e..5239c98778 100644 --- a/src/axolotl/integrations/kernels/constants.py +++ b/src/axolotl/integrations/kernels/constants.py @@ -11,6 +11,7 @@ - ernie4_5_moe: softmax→bias correction→topk (softmax_bias_topk_routing) - deepseek_v2: softmax→group_limited_greedy (softmax_group_limited_topk_routing) - hunyuan_v1_moe: softmax→topk via gate.wg (softmax_topk_wg_routing) +- gemma4_text: RMSNorm→scale→proj→softmax→topk→renorm→per_expert_scale (experts-level patch) """ import importlib @@ -53,6 +54,49 @@ } +# Models where MoE is NOT in a separate SparseMoeBlock but embedded in the +# decoder layer. For these, we patch the Experts class forward directly +# (same signature: hidden_states, top_k_index, top_k_weights -> Tensor). +# Routing stays untouched — the original model router runs as-is. +EXPERTS_ONLY_BLOCK = { + # gemma4: hybrid MLP+MoE in decoder layer, custom Gemma4TextRouter, + # no SparseMoeBlock. Experts use @use_experts_implementation with + # standard 3D param layout (gate_up_proj [E, 2*I, H], down_proj [E, H, I]). + "gemma4_text": "Gemma4TextExperts", +} + + +def resolve_experts_class(model_type: str): + """Resolve the Experts class for models that need experts-level patching. + + Returns the class, or None if the model uses SparseMoeBlock-level patching. + """ + entry = EXPERTS_ONLY_BLOCK.get(model_type) + if entry is None: + return None + + module_path = f"transformers.models.{model_type}.modeling_{model_type}" + try: + module = importlib.import_module(module_path) + except ModuleNotFoundError: + if model_type.endswith("_text"): + parent_type = model_type.removesuffix("_text") + module_path = f"transformers.models.{parent_type}.modeling_{parent_type}" + module = importlib.import_module(module_path) + else: + raise + + cls = getattr(module, entry, None) + if cls is None: + raise ValueError(f"Could not find class '{entry}' in '{module_path}'") + return cls + + +def is_experts_only_model(model_type: str) -> bool: + """Check if a model type requires experts-level (not block-level) patching.""" + return model_type in EXPERTS_ONLY_BLOCK + + def resolve_moe_block_classes(model_type: str): """Resolve all MoE block classes from transformers for the given model type. diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_experts.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_experts.py new file mode 100644 index 0000000000..66623e0173 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_experts.py @@ -0,0 +1,235 @@ +""" +ScatterMoE-accelerated experts forward for Gemma4. + +Gemma4 has no separate SparseMoeBlock — MoE is embedded in the decoder layer. +The decoder layer handles routing (Gemma4TextRouter) and calls +``experts(hidden_states, top_k_index, top_k_weights)`` directly. + +This module registers a ``"scattermoe"`` implementation in the transformers +``ExpertsInterface``, which the ``@use_experts_implementation`` decorator +dispatches to when ``config._experts_implementation == "scattermoe"``. + +This is the clean way to hook into transformers' MoE dispatch — no +monkeypatching required. Works for Gemma4 and any future model that uses +``@use_experts_implementation`` with the standard forward signature +``(hidden_states, top_k_index, top_k_weights) -> Tensor``. +""" + +import torch + +from .parallel_experts import flatten_sort_count, parallel_linear +from .parallel_linear_lora import get_lora_params_from_wrapper, parallel_linear_lora + + +def _has_peft_wrapper(module): + """Check if a module's parameter has been wrapped by PEFT ParamWrapper.""" + try: + from peft.tuners.param_wrapper import ParamWrapper + + for attr in ("gate_up_proj", "down_proj"): + param = getattr(module, attr, None) + if isinstance(param, ParamWrapper): + return True + except ImportError: + pass + return False + + +def _unwrap_experts_lora(experts): + """Extract base weights and LoRA params from a PEFT-wrapped Experts module. + + Returns: + (base_experts, gup_lora, down_lora) where each lora is + (lora_A, lora_B, scaling) or None. + """ + try: + from peft.tuners.param_wrapper import ParamWrapper + except ImportError: + return experts, None, None + + if not isinstance(getattr(experts, "gate_up_proj", None), ParamWrapper): + return experts, None, None + + base_experts = experts + gup_lora = None + down_lora = None + + gup_param = experts.gate_up_proj + if isinstance(gup_param, ParamWrapper): + lora_A, lora_B, scaling = get_lora_params_from_wrapper(gup_param) + if lora_A is not None: + num_experts = experts.num_experts + rank = lora_A.shape[0] // num_experts + from .layers import peft_lora_to_scattermoe + + sm_A, sm_B = peft_lora_to_scattermoe(lora_A, lora_B, num_experts, rank) + gup_lora = (sm_A, sm_B, scaling) + + down_param = experts.down_proj + if isinstance(down_param, ParamWrapper): + lora_A, lora_B, scaling = get_lora_params_from_wrapper(down_param) + if lora_A is not None: + num_experts = experts.num_experts + rank = lora_A.shape[0] // num_experts + from .layers import peft_lora_to_scattermoe + + sm_A, sm_B = peft_lora_to_scattermoe(lora_A, lora_B, num_experts, rank) + down_lora = (sm_A, sm_B, scaling) + + return base_experts, gup_lora, down_lora + + +def _get_base_param(param): + """Get the base tensor from a PEFT ParamWrapper or regular Parameter.""" + try: + from peft.tuners.param_wrapper import ParamWrapper + + while isinstance(param, ParamWrapper): + param = param.original_parameter + except ImportError: + pass + return param + + +def _parallel_linear_maybe_lora( + x, + weight, + top_k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + lora_tuple, + grouped_in, + grouped_out, + gates=None, +): + """Call parallel_linear or parallel_linear_lora depending on whether LoRA is active.""" + if lora_tuple is not None: + lora_A, lora_B, scaling = lora_tuple + return parallel_linear_lora( + x, + weight, + top_k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + lora_A, + lora_B, + scaling, + grouped_in=grouped_in, + grouped_out=grouped_out, + gates=gates, + ) + return parallel_linear( + x, + weight, + top_k, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=grouped_in, + grouped_out=grouped_out, + gates=gates, + ) + + +def scattermoe_experts_forward( + self, + hidden_states: torch.Tensor, + top_k_index: torch.Tensor, + top_k_weights: torch.Tensor, +) -> torch.Tensor: + """ScatterMoE-accelerated experts forward. + + Drop-in replacement for the standard Experts forward signature used by + ``@use_experts_implementation``-decorated classes (Gemma4, Mixtral, etc.): + ``(hidden_states [T, H], top_k_index [T, K], top_k_weights [T, K]) -> [T, H]`` + """ + K = top_k_index.shape[1] + + routing_weights = top_k_weights.to(hidden_states.dtype) + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = flatten_sort_count( + top_k_index, num_experts=self.num_experts + ) + + # Get base weights (unwrap PEFT if needed) + gate_up_weight = _get_base_param(self.gate_up_proj).transpose(2, 1) + down_weight = _get_base_param(self.down_proj).transpose(2, 1) + + # Extract LoRA params if PEFT is active + gup_lora, down_lora = None, None + if _has_peft_wrapper(self): + _, gup_lora, down_lora = _unwrap_experts_lora(self) + + # Gate-up projection (with optional LoRA) + gates_h = _parallel_linear_maybe_lora( + hidden_states, + gate_up_weight, + K, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + gup_lora, + grouped_in=False, + grouped_out=True, + ) + gates, h = gates_h.chunk(2, dim=-1) + h = self.act_fn(gates) * h + + # Down projection (with optional LoRA + routing weights) + output = _parallel_linear_maybe_lora( + h, + down_weight, + 1, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + down_lora, + grouped_in=True, + grouped_out=False, + gates=routing_weights, + ) + + return output + + +def register_scattermoe_experts(): + """Register ``"scattermoe"`` in the transformers ExpertsInterface. + + After calling this, any model with ``@use_experts_implementation`` will + dispatch to ScatterMoE when ``config._experts_implementation == "scattermoe"``. + + Also patches ``get_correct_experts_implementation`` to accept ``"scattermoe"`` + as a valid value (transformers hardcodes an allowlist). + """ + from transformers.integrations.moe import ALL_EXPERTS_FUNCTIONS + from transformers.modeling_utils import PreTrainedModel + + # 1. Register the forward function in the global interface + ALL_EXPERTS_FUNCTIONS.register("scattermoe", scattermoe_experts_forward) + + # 2. Patch the validation to accept "scattermoe" + _original_get_correct = PreTrainedModel.get_correct_experts_implementation + + def _patched_get_correct(self_model, requested_experts: str | None) -> str: + if requested_experts == "scattermoe": + return "scattermoe" + return _original_get_correct(self_model, requested_experts) + + PreTrainedModel.get_correct_experts_implementation = _patched_get_correct + + +# Legacy monkeypatch approach (kept for backward compat with existing tests) +def patch_gemma4_scattermoe(): + """Monkeypatch Gemma4TextExperts.forward with ScatterMoE kernel.""" + from axolotl.integrations.kernels.constants import resolve_experts_class + + experts_cls = resolve_experts_class("gemma4_text") + if experts_cls is None: + raise ValueError("Could not resolve Gemma4TextExperts class") + + if hasattr(experts_cls, "_original_forward"): + return # already patched + + experts_cls._original_forward = experts_cls.forward + experts_cls.forward = scattermoe_experts_forward diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/gemma4_experts.py b/src/axolotl/integrations/kernels/libs/sonicmoe/gemma4_experts.py new file mode 100644 index 0000000000..a4025dd842 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/gemma4_experts.py @@ -0,0 +1,106 @@ +""" +SonicMoE-accelerated experts forward for Gemma4. + +Gemma4 has no separate SparseMoeBlock — MoE is embedded in the decoder layer. +This module provides a drop-in replacement for ``Gemma4TextExperts.forward`` +that uses SonicMoE kernels while preserving the original call signature. +""" + +import torch + +from .lora import has_lora, materialize_expert_lora, unwrap_experts_lora + + +def _get_expert_weights_gemma4(experts_module): + """Extract expert weights from Gemma4TextExperts, applying LoRA if active. + + Returns: + (gate_up_weight, down_weight) in SonicMoE layout [dim, dim, E]. + """ + if has_lora(experts_module): + base_experts, lora_dict = unwrap_experts_lora(experts_module) + gate_up = materialize_expert_lora( + base_experts.gate_up_proj, lora_dict.get("gate_up_proj") + ) + down = materialize_expert_lora( + base_experts.down_proj, lora_dict.get("down_proj") + ) + else: + gate_up = experts_module.gate_up_proj + down = experts_module.down_proj + + # Permute to SonicMoE layout: + # gate_up: [E, 2*I, H] -> [2*I, H, E] + # down: [E, H, I] -> [H, I, E] + return gate_up.permute(1, 2, 0), down.permute(1, 2, 0) + + +def gemma4_sonicmoe_experts_forward( + self, + hidden_states: torch.Tensor, + top_k_index: torch.Tensor, + top_k_weights: torch.Tensor, +) -> torch.Tensor: + """SonicMoE-accelerated replacement for Gemma4TextExperts.forward. + + Same signature as the original: (hidden_states [T, H], top_k_index [T, K], + top_k_weights [T, K]) -> output [T, H]. + """ + from sonicmoe import moe_general_routing_inputs + from sonicmoe.enums import ActivationType + + T, _ = hidden_states.shape + K = top_k_index.shape[1] + E = self.num_experts + + # Convert routing outputs to SonicMoE's flat format + # Token indices sorted ascending (required by SonicMoE) + token_indices = ( + torch.arange(T, device=hidden_states.device, dtype=torch.int32) + .unsqueeze(1) + .expand(T, K) + ) + flat_scores = top_k_weights.to(torch.float32).reshape(-1) # [T*K] + flat_token_idx = token_indices.reshape(-1) # [T*K] + flat_expert_idx = top_k_index.to(torch.int32).reshape(-1) # [T*K] + + # Get weights (with LoRA materialization if needed) + gate_up_weight, down_weight = _get_expert_weights_gemma4(self) + gate_up_weight = gate_up_weight.to(hidden_states.dtype) + down_weight = down_weight.to(hidden_states.dtype) + + if not torch.cuda.is_available(): + raise RuntimeError("SonicMoE requires CUDA. No CUDA device available.") + cuda_stream = torch.cuda.current_stream().cuda_stream + + output, _ = moe_general_routing_inputs( + hidden_states, + flat_scores, + flat_token_idx, + flat_expert_idx, + gate_up_weight, + None, # b1 (no gate/up bias) + down_weight, + None, # b2 (no down bias) + E, + cuda_stream, + ActivationType.GEGLU, + False, # is_inference_mode + ) + + return output + + +def patch_gemma4_sonicmoe(): + """Monkeypatch Gemma4TextExperts.forward with SonicMoE kernel.""" + from axolotl.integrations.kernels.constants import resolve_experts_class + + experts_cls = resolve_experts_class("gemma4_text") + if experts_cls is None: + raise ValueError("Could not resolve Gemma4TextExperts class") + + if hasattr(experts_cls, "_original_forward"): + return # already patched + + experts_cls._original_forward = experts_cls.forward + experts_cls.forward = gemma4_sonicmoe_experts_forward diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/routing.py b/src/axolotl/integrations/kernels/libs/sonicmoe/routing.py index 4bdb378904..68654d0868 100644 --- a/src/axolotl/integrations/kernels/libs/sonicmoe/routing.py +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/routing.py @@ -7,6 +7,7 @@ - glm_moe_dsa / deepseek_v3 / minimax_m2: sigmoid -> topk (with group-based expert selection) - ernie4_5_moe: softmax -> bias correction -> topk -> gather (softmax_bias_topk_routing) - hunyuan_v1_moe: softmax -> topk via gate.wg (softmax_topk_wg_routing) +- gemma4_text: RMSNorm -> scale -> proj -> softmax -> topk -> renorm -> per_expert_scale (gemma4_routing) - gpt_oss: topk -> softmax (uses fused moe_TC_softmax_topk_layer, routing_fn=None) [NOT YET SUPPORTED] Each model type maps to a (routing_fn, activation_type, router_attr) triple. @@ -66,6 +67,8 @@ def get_model_moe_config(model_type: str): return softmax_bias_topk_routing, ActivationType.SWIGLU, "gate" elif model_type in ("hunyuan_v1_moe",): return softmax_topk_wg_routing, ActivationType.SWIGLU, "gate" + elif model_type in ("gemma4_text",): + return gemma4_routing, ActivationType.GEGLU, "router" # Fused topk -> softmax path (routing_fn=None): # elif model_type in ("gpt_oss",): # # NOTE: gpt_oss has a router bias which moe_TC_softmax_topk_layer @@ -501,3 +504,73 @@ def softmax_topk_wg_routing( flat_expert_idx = top_indices.to(torch.int32).reshape(-1) # [T*K] return flat_scores, flat_token_idx, flat_expert_idx, router_logits + + +def gemma4_routing( + hidden_states: torch.Tensor, moe_block +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """Gemma4-style routing: RMSNorm → scale → proj → softmax → topk → renorm → per_expert_scale. + + Gemma4's router (``Gemma4TextRouter``) has a unique structure: + 1. RMSNorm (without learnable scale) on hidden states + 2. Multiply by ``scale * hidden_size**-0.5`` + 3. Linear projection to expert scores + 4. Softmax → topk + 5. Normalize top-k weights to sum to 1 + 6. Multiply by per-expert learned scales + + The router lives at ``moe_block.router`` (not ``moe_block.gate``). + LoRA on the router targets ``router.proj`` (nn.Linear). + + Args: + hidden_states: [T, H] flattened token representations + moe_block: MoE block module (accesses moe_block.router) + + Returns: + router_scores: [T*K] flattened scores (float32) + token_indices: [T*K] which token each entry belongs to (int32), sorted ascending + expert_indices: [T*K] which expert (int32) + router_logits: [T, E] original logits for aux loss + """ + router = moe_block.router + + # Unwrap PEFT LoRA on router.proj (the nn.Linear) + _, proj_weight, proj_lora_delta = unwrap_gate_lora(router.proj) + + T, _ = hidden_states.shape + K = router.top_k if hasattr(router, "top_k") else router.config.top_k_experts + + # Reproduce Gemma4TextRouter.forward: + # 1. RMSNorm (no scale) + scale param * hidden_size**-0.5 + normed = router.norm(hidden_states) + scaled = normed * router.scale * router.scalar_root_size + + # 2. Project to expert scores + router_logits = F.linear(scaled.float(), proj_weight.float()) # [T, E] + if proj_lora_delta is not None: + router_logits = router_logits + F.linear( + scaled.float(), proj_lora_delta.float() + ) + + # 3. Softmax → topk + router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] + top_values, top_indices = torch.topk(router_probs, K, dim=-1) # [T, K] + + # 4. Normalize top-k weights + top_values = top_values / top_values.sum(dim=-1, keepdim=True) + + # 5. Per-expert scale + top_values = top_values * router.per_expert_scale[top_indices] + + # Flatten for moe_general_routing_inputs + token_indices = ( + torch.arange(T, device=hidden_states.device, dtype=torch.int32) + .unsqueeze(1) + .expand(T, K) + ) + + flat_scores = top_values.to(torch.float32).reshape(-1) # [T*K] + flat_token_idx = token_indices.reshape(-1) # [T*K] + flat_expert_idx = top_indices.to(torch.int32).reshape(-1) # [T*K] + + return flat_scores, flat_token_idx, flat_expert_idx, router_logits diff --git a/src/axolotl/integrations/kernels/plugin.py b/src/axolotl/integrations/kernels/plugin.py index 4ab22bfce5..e9291c9c15 100644 --- a/src/axolotl/integrations/kernels/plugin.py +++ b/src/axolotl/integrations/kernels/plugin.py @@ -61,20 +61,31 @@ def get_input_args(self): return "axolotl.integrations.kernels.KernelsArgs" def pre_model_load(self, cfg): - from axolotl.integrations.kernels.constants import SPARSE_MOE_BLOCK + from axolotl.integrations.kernels.constants import ( + SPARSE_MOE_BLOCK, + is_experts_only_model, + ) # Prefer text backbone type for VLMs, but fall back to base type # when the text type isn't in the supported mapping (e.g. qwen3_5_moe_text) moe_model_type = cfg.model_config_type_text or cfg.model_config_type if ( moe_model_type not in SPARSE_MOE_BLOCK + and not is_experts_only_model(moe_model_type) and cfg.model_config_type in SPARSE_MOE_BLOCK ): moe_model_type = cfg.model_config_type if cfg.use_scattermoe: self._register_kernels() - self._kernelize_model(moe_model_type) + if is_experts_only_model(moe_model_type): + # Models like Gemma4 where MoE is embedded in the decoder layer + # — register ScatterMoE in the ExpertsInterface so that + # @use_experts_implementation dispatches to it. + self._register_experts_interface() + cfg.experts_implementation = "scattermoe" + else: + self._kernelize_model(moe_model_type) elif cfg.use_sonicmoe: if not importlib.util.find_spec("sonicmoe"): raise RuntimeError( @@ -84,14 +95,24 @@ def pre_model_load(self, cfg): _check_sonicmoe_gpu_compat() - from axolotl.integrations.kernels.libs.sonicmoe import patch_sonicmoe + if is_experts_only_model(moe_model_type): + from axolotl.integrations.kernels.libs.sonicmoe.gemma4_experts import ( + patch_gemma4_sonicmoe, + ) - LOG.info(f"Applying SonicMoE patches for model type: {moe_model_type}") - patch_sonicmoe( - moe_model_type, - torch_compile=bool(getattr(cfg, "torch_compile", False)), - base_model_type=cfg.model_config_type, - ) + LOG.info( + f"Applying SonicMoE experts-level patch for model type: {moe_model_type}" + ) + patch_gemma4_sonicmoe() + else: + from axolotl.integrations.kernels.libs.sonicmoe import patch_sonicmoe + + LOG.info(f"Applying SonicMoE patches for model type: {moe_model_type}") + patch_sonicmoe( + moe_model_type, + torch_compile=bool(getattr(cfg, "torch_compile", False)), + base_model_type=cfg.model_config_type, + ) def _register_kernels(self): from kernels import ( @@ -139,3 +160,16 @@ def _kernelize_model(self, model_type: str): replace_kernel_forward_from_hub( model_moe_cls, "HFScatterMoEParallelExperts" ) + + def _register_experts_interface(self): + """Register ScatterMoE in the transformers ExpertsInterface. + + This allows @use_experts_implementation-decorated Experts classes + to dispatch to ScatterMoE when config._experts_implementation == "scattermoe". + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + register_scattermoe_experts, + ) + + register_scattermoe_experts() + LOG.info("Registered 'scattermoe' in transformers ExpertsInterface") diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index c5d552c035..d569d5925a 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -73,6 +73,44 @@ query_states = self.q_norm(query_states.view(hidden_shape)).transpose(1, 2) key_states = self.k_norm(key_states.view(hidden_shape)).transpose(1, 2) value_states = value_states.view(hidden_shape).transpose(1, 2) +""".lstrip("\n"), + ), + # Gemma4: norm between proj and transpose, RoPE between norm and transpose, + # conditional KV sharing (is_kv_shared_layer), v_proj may be None (attention_k_eq_v). + # We only fuse the projection calls; norms, RoPE, and KV sharing stay as-is. + ( + """ + query_states = self.q_proj(hidden_states).view(hidden_shape) + query_states = self.q_norm(query_states) + query_states = apply_rotary_pos_emb(query_states, cos, sin, unsqueeze_dim=2) + query_states = query_states.transpose(1, 2) + + # For layers with shared KV (from kv sharing point onwards), we reuse the same keys/values states as the last non-sharing layer + if self.is_kv_shared_layer and past_key_values is not None: + key_states, value_states = past_key_values.shared_layers[self.kv_shared_layer_index] + # Device of past layer may be different from current one + key_states = key_states.to(query_states.device) + value_states = value_states.to(query_states.device) + else: + key_states = self.k_proj(hidden_states).view(hidden_shape) + value_states = self.v_proj(hidden_states).view(hidden_shape) if self.v_proj is not None else key_states +""".lstrip("\n"), + """ + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states = query_states.view(hidden_shape) + query_states = self.q_norm(query_states) + query_states = apply_rotary_pos_emb(query_states, cos, sin, unsqueeze_dim=2) + query_states = query_states.transpose(1, 2) + + # For layers with shared KV (from kv sharing point onwards), we reuse the same keys/values states as the last non-sharing layer + if self.is_kv_shared_layer and past_key_values is not None: + key_states, value_states = past_key_values.shared_layers[self.kv_shared_layer_index] + # Device of past layer may be different from current one + key_states = key_states.to(query_states.device) + value_states = value_states.to(query_states.device) + else: + key_states = key_states.view(hidden_shape) + value_states = value_states.view(hidden_shape) if self.v_proj is not None else key_states """.lstrip("\n"), ), ] @@ -113,6 +151,23 @@ def original_apply_qkv( return query_states, key_states, value_states +def original_apply_qkv_optional_v( + self: nn.Module, hidden_states: torch.Tensor +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """QKV projection for models where v_proj may be None (e.g. Gemma4 attention_k_eq_v). + + When v_proj is None, key_states are reused as value_states. + """ + query_states = self.q_proj(hidden_states) + key_states = self.k_proj(hidden_states) + if self.v_proj is not None: + value_states = self.v_proj(hidden_states) + else: + value_states = key_states + + return query_states, key_states, value_states + + def original_apply_o(self: nn.Module, hidden_states: torch.Tensor) -> torch.Tensor: """ Original implementation of output projection without optimizations. @@ -183,6 +238,11 @@ def get_attention_cls_from_config(cfg: DictDefault) -> Type[nn.Module]: return Gemma3Attention + if model_type in ("gemma4", "gemma4_text"): + from transformers.models.gemma4.modeling_gemma4 import Gemma4TextAttention + + return Gemma4TextAttention + try: # Dynamically import the module and attention class module_path = f"transformers.models.{model_type}.modeling_{model_type}" @@ -410,14 +470,24 @@ def apply_lora_kernel_patches( # Add QKV, O fallback implementations to start # These will be overwritten later (if some conditions apply) for self_attn in find_self_attn_in_layer(layer): - self_attn.apply_qkv = types.MethodType(original_apply_qkv, self_attn) + # Use v_proj-optional fallback for models where v_proj can be None + # (e.g. Gemma4 with attention_k_eq_v=True) + if getattr(self_attn, "v_proj", None) is None: + self_attn.apply_qkv = types.MethodType( + original_apply_qkv_optional_v, self_attn + ) + else: + self_attn.apply_qkv = types.MethodType(original_apply_qkv, self_attn) self_attn.apply_o = types.MethodType(original_apply_o, self_attn) if cfg.lora_qkv_kernel: # Query, key, value patching + # Filter out None projections (e.g. Gemma4 v_proj when attention_k_eq_v=True) + proj_names = ["q_proj", "k_proj", "v_proj"] layer_modules = [ - getattr(self_attn, linear_proj) - for linear_proj in ["q_proj", "k_proj", "v_proj"] + getattr(self_attn, name) + for name in proj_names + if getattr(self_attn, name, None) is not None ] can_patch_qkv = all( hasattr(module, "lora_A") for module in layer_modules diff --git a/src/axolotl/monkeypatch/models/qwen3_next/modeling.py b/src/axolotl/monkeypatch/models/qwen3_next/modeling.py index 48570ba422..fb4cb1bc72 100644 --- a/src/axolotl/monkeypatch/models/qwen3_next/modeling.py +++ b/src/axolotl/monkeypatch/models/qwen3_next/modeling.py @@ -111,7 +111,6 @@ def patch_qwen3_next_gateddelta_layer(): """Patch Qwen3NextGatedDeltaNet to parse cu_seqlens and pass to chunk_gated_delta_rule""" try: from transformers.models.qwen3_next.modeling_qwen3_next import ( - Qwen3NextDynamicCache, Qwen3NextGatedDeltaNet, apply_mask_to_padding_states, ) @@ -125,8 +124,7 @@ def patch_qwen3_next_gateddelta_layer(): def patched_gated_delta_net_forward( self, hidden_states: torch.Tensor, - cache_params: Optional[Qwen3NextDynamicCache] = None, - cache_position: Optional[torch.LongTensor] = None, + cache_params=None, attention_mask: Optional[torch.Tensor] = None, position_ids: Optional[torch.LongTensor] = None, ): @@ -137,9 +135,8 @@ def patched_gated_delta_net_forward( use_precomputed_states = ( cache_params is not None - and cache_params.has_previous_state + and cache_params.has_previous_state(self.layer_idx) and seq_len == 1 - and cache_position is not None ) # Compute cu_seqlens early for use by both causal_conv1d and chunk_gated_delta_rule @@ -148,9 +145,9 @@ def patched_gated_delta_net_forward( cu_seqlens = get_cu_seqlens(position_ids=position_ids) # getting projected states from cache if it exists - if cache_params is not None: - conv_state = cache_params.conv_states[self.layer_idx] - recurrent_state = cache_params.recurrent_states[self.layer_idx] + if use_precomputed_states: + conv_state = cache_params.layers[self.layer_idx].conv_states + recurrent_state = cache_params.layers[self.layer_idx].recurrent_states projected_states_qkvz = self.in_proj_qkvz(hidden_states) projected_states_ba = self.in_proj_ba(hidden_states) @@ -162,10 +159,9 @@ def patched_gated_delta_net_forward( ) mixed_qkv = torch.cat((query, key, value), dim=-1) # [B, T, D] + mixed_qkv = mixed_qkv.transpose(1, 2) # [B, D, T] if use_precomputed_states: - # Inference single-token path: causal_conv1d_update expects [B, D, T] - mixed_qkv = mixed_qkv.transpose(1, 2) mixed_qkv = self.causal_conv1d_update( mixed_qkv, conv_state, @@ -173,19 +169,17 @@ def patched_gated_delta_net_forward( self.conv1d.bias, self.activation, ) - mixed_qkv = mixed_qkv.transpose(1, 2) else: if cache_params is not None: - # Cache state expects [B, D, T] for the inference update path - mixed_qkv_t = mixed_qkv.transpose(1, 2) conv_state = F.pad( - mixed_qkv_t, - (self.conv_kernel_size - mixed_qkv_t.shape[-1], 0), + mixed_qkv, + (self.conv_kernel_size - mixed_qkv.shape[-1], 0), ) - cache_params.conv_states[self.layer_idx] = conv_state + cache_params.update_conv_state(conv_state, self.layer_idx) if fla_causal_conv1d is not None: # FLA Triton causal_conv1d: [B, T, D] in/out, with cu_seqlens support + mixed_qkv = mixed_qkv.transpose(1, 2) # [B, T, D] for FLA mixed_qkv, _ = fla_causal_conv1d( x=mixed_qkv, weight=self.conv1d.weight.squeeze(1), @@ -193,6 +187,15 @@ def patched_gated_delta_net_forward( activation=self.activation, cu_seqlens=cu_seqlens, ) + mixed_qkv = mixed_qkv.transpose(1, 2) # back to [B, D, T] + elif self.causal_conv1d_fn is not None: + mixed_qkv = self.causal_conv1d_fn( + x=mixed_qkv, + weight=self.conv1d.weight.squeeze(1), + bias=self.conv1d.bias, + activation=self.activation, + seq_idx=None, + ) else: # PyTorch fallback (no cu_seqlens support) if cu_seqlens is not None and cu_seqlens.shape[0] > batch_size + 1: @@ -203,11 +206,9 @@ def patched_gated_delta_net_forward( LOG.warning_once( "FLA causal_conv1d not available. Falling back to PyTorch conv1d." ) - mixed_qkv = mixed_qkv.transpose(1, 2) mixed_qkv = F.silu(self.conv1d(mixed_qkv)[:, :, :seq_len]) - mixed_qkv = mixed_qkv.transpose(1, 2) - # mixed_qkv is [B, T, D] in all paths + mixed_qkv = mixed_qkv.transpose(1, 2) # [B, T, D] query, key, value = torch.split( mixed_qkv, [ @@ -255,7 +256,7 @@ def patched_gated_delta_net_forward( # Update cache if cache_params is not None: - cache_params.recurrent_states[self.layer_idx] = last_recurrent_state + cache_params.update_recurrent_state(last_recurrent_state, self.layer_idx) z_shape_og = z.shape # reshape input data into 2D tensor diff --git a/src/axolotl/utils/chat_templates/templates/gemma4.jinja b/src/axolotl/utils/chat_templates/templates/gemma4.jinja new file mode 100644 index 0000000000..780957c941 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/gemma4.jinja @@ -0,0 +1,271 @@ +{%- macro format_parameters(properties, required) -%} + {%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%} + {%- set ns = namespace(found_first=false) -%} + {%- for key, value in properties | dictsort -%} + {%- set add_comma = false -%} + {%- if key not in standard_keys -%} + {%- if ns.found_first %},{% endif -%} + {%- set ns.found_first = true -%} + {{ key }}:{ + {%- if value['description'] -%} + description:<|"|>{{ value['description'] }}<|"|> + {%- set add_comma = true -%} + {%- endif -%} + {%- if value['nullable'] %} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + nullable:true + {%- endif -%} + {%- if value['type'] | upper == 'STRING' -%} + {%- if value['enum'] -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + enum:{{ format_argument(value['enum']) }} + {%- endif -%} + {%- elif value['type'] | upper == 'OBJECT' -%} + ,properties:{ + {%- if value['properties'] is defined and value['properties'] is mapping -%} + {{- format_parameters(value['properties'], value['required'] | default([])) -}} + {%- elif value is mapping -%} + {{- format_parameters(value, value['required'] | default([])) -}} + {%- endif -%} + } + {%- if value['required'] -%} + ,required:[ + {%- for item in value['required'] | default([]) -%} + <|"|>{{- item -}}<|"|> + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + ] + {%- endif -%} + {%- elif value['type'] | upper == 'ARRAY' -%} + {%- if value['items'] is mapping and value['items'] -%} + ,items:{ + {%- set ns_items = namespace(found_first=false) -%} + {%- for item_key, item_value in value['items'] | dictsort -%} + {%- if item_value is not none -%} + {%- if ns_items.found_first %},{% endif -%} + {%- set ns_items.found_first = true -%} + {%- if item_key == 'properties' -%} + properties:{ + {%- if item_value is mapping -%} + {{- format_parameters(item_value, value['items']['required'] | default([])) -}} + {%- endif -%} + } + {%- elif item_key == 'required' -%} + required:[ + {%- for req_item in item_value -%} + <|"|>{{- req_item -}}<|"|> + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + ] + {%- elif item_key == 'type' -%} + {%- if item_value is string -%} + type:{{ format_argument(item_value | upper) }} + {%- else -%} + type:{{ format_argument(item_value | map('upper') | list) }} + {%- endif -%} + {%- else -%} + {{ item_key }}:{{ format_argument(item_value) }} + {%- endif -%} + {%- endif -%} + {%- endfor -%} + } + {%- endif -%} + {%- endif -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + type:<|"|>{{ value['type'] | upper }}<|"|>} + {%- endif -%} + {%- endfor -%} +{%- endmacro -%} +{%- macro format_function_declaration(tool_data) -%} + declaration:{{- tool_data['function']['name'] -}}{description:<|"|>{{- tool_data['function']['description'] -}}<|"|> + {%- set params = tool_data['function']['parameters'] -%} + {%- if params -%} + ,parameters:{ + {%- if params['properties'] -%} + properties:{ {{- format_parameters(params['properties'], params['required']) -}} }, + {%- endif -%} + {%- if params['required'] -%} + required:[ + {%- for item in params['required'] -%} + <|"|>{{- item -}}<|"|> + {{- ',' if not loop.last -}} + {%- endfor -%} + ], + {%- endif -%} + {%- if params['type'] -%} + type:<|"|>{{- params['type'] | upper -}}<|"|>} + {%- endif -%} + {%- endif -%} + {%- if 'response' in tool_data['function'] -%} + {%- set response_declaration = tool_data['function']['response'] -%} + ,response:{ + {%- if response_declaration['description'] -%} + description:<|"|>{{- response_declaration['description'] -}}<|"|>, + {%- endif -%} + {%- if response_declaration['type'] | upper == 'OBJECT' -%} + type:<|"|>{{- response_declaration['type'] | upper -}}<|"|>} + {%- endif -%} + {%- endif -%} + } +{%- endmacro -%} +{%- macro format_argument(argument, escape_keys=True) -%} + {%- if argument is string -%} + {{- '<|"|>' + argument + '<|"|>' -}} + {%- elif argument is boolean -%} + {{- 'true' if argument else 'false' -}} + {%- elif argument is mapping -%} + {{- '{' -}} + {%- set ns = namespace(found_first=false) -%} + {%- for key, value in argument | dictsort -%} + {%- if ns.found_first %},{% endif -%} + {%- set ns.found_first = true -%} + {%- if escape_keys -%} + {{- '<|"|>' + key + '<|"|>' -}} + {%- else -%} + {{- key -}} + {%- endif -%} + :{{- format_argument(value, escape_keys=escape_keys) -}} + {%- endfor -%} + {{- '}' -}} + {%- elif argument is sequence -%} + {{- '[' -}} + {%- for item in argument -%} + {{- format_argument(item, escape_keys=escape_keys) -}} + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + {{- ']' -}} + {%- else -%} + {{- argument -}} + {%- endif -%} +{%- endmacro -%} +{#- Removes '<|channel>...' thinking blocks from model output. + Splits on the end token '', then checks each part for the start + token '<|channel>' and keeps only the text before it. -#} +{%- macro strip_thinking(text) -%} + {%- set ns = namespace(cleaned='') -%} + {%- for part in text.split('') -%} + {%- if '<|channel>' in part -%} + {%- set ns.cleaned = ns.cleaned + part.split('<|channel>')[0] -%} + {%- else -%} + {%- set ns.cleaned = ns.cleaned + part -%} + {%- endif -%} + {%- endfor -%} + {{- ns.cleaned | trim -}} +{%- endmacro -%} + +{%- set ns = namespace(prev_message_type=None) -%} +{%- set loop_messages = messages -%} +{{ bos_token }} +{#- Handle System/Tool Definitions Block -#} +{%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%} + {{- '<|turn>system\n' -}} + + {#- Inject Thinking token at the very top of the FIRST system turn -#} + {%- if enable_thinking is defined and enable_thinking -%} + {{- '<|think|>' -}} + {%- set ns.prev_message_type = 'think' -%} + {%- endif -%} + + {%- if messages[0]['role'] in ['system', 'developer'] -%} + {{- messages[0]['content'] | trim -}} + {%- set loop_messages = messages[1:] -%} + {%- endif -%} + + {%- if tools -%} + {%- for tool in tools %} + {{- '<|tool>' -}} + {{- format_function_declaration(tool) | trim -}} + {{- '' -}} + {%- endfor %} + {%- set ns.prev_message_type = 'tool' -%} + {%- endif -%} + + {{- '\n' -}} +{%- endif %} + +{#- Loop through messages -#} +{%- for message in loop_messages -%} + {#- Reset so only special message types (tool_call, image, etc.) influence + the generation prompt formatting below. Plain text leaves it as None. -#} + {%- set ns.prev_message_type = None -%} + {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%} + {{- '<|turn>' + role + '\n' }} + + {%- if message['tool_calls'] -%} + {%- for tool_call in message['tool_calls'] -%} + {%- set function = tool_call['function'] -%} + {{- '<|tool_call>call:' + function['name'] + '{' -}} + {%- if function['arguments'] is mapping -%} + {%- set ns_args = namespace(found_first=false) -%} + {%- for key, value in function['arguments'] | dictsort -%} + {%- if ns_args.found_first %},{% endif -%} + {%- set ns_args.found_first = true -%} + {{- key -}}:{{- format_argument(value, escape_keys=False) -}} + {%- endfor -%} + {%- elif function['arguments'] is string -%} + {{- function['arguments'] -}} + {%- endif -%} + {{- '}' -}} + {%- endfor -%} + {%- set ns.prev_message_type = 'tool_call' -%} + {%- endif -%} + + {%- if message['tool_responses'] -%} + {#- Tool Response handling -#} + {%- for tool_response in message['tool_responses'] -%} + {{- '<|tool_response>' -}} + {%- if tool_response['response'] is mapping -%} + {{- 'response:' + tool_response['name'] | default('unknown') + '{' -}} + {%- for key, value in tool_response['response'] | dictsort -%} + {{- key -}}:{{- format_argument(value, escape_keys=False) -}} + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + {{- '}' -}} + {%- else -%} + {{- 'response:' + tool_response['name'] | default('unknown') + '{value:' + format_argument(tool_response['response'], escape_keys=False) + '}' -}} + {%- endif -%} + {{- '' -}} + {%- endfor -%} + {%- set ns.prev_message_type = 'tool_response' -%} + {%- endif -%} + + {%- if message['content'] is string -%} + {%- if role == 'model' -%} + {{- strip_thinking(message['content']) -}} + {%- else -%} + {{- message['content'] | trim -}} + {%- endif -%} + {%- elif message['content'] is sequence -%} + {%- for item in message['content'] -%} + {%- if item['type'] == 'text' -%} + {%- if role == 'model' -%} + {{- strip_thinking(item['text']) -}} + {%- else -%} + {{- item['text'] | trim -}} + {%- endif -%} + {%- elif item['type'] == 'image' -%} + {{- '\n\n<|image|>\n\n' -}} + {%- set ns.prev_message_type = 'image' -%} + {%- elif item['type'] == 'audio' -%} + {{- '<|audio|>' -}} + {%- set ns.prev_message_type = 'audio' -%} + {%- elif item['type'] == 'video' -%} + {{- '\n\n<|video|>\n\n' -}} + {%- set ns.prev_message_type = 'video' -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + + {%- if not (message['tool_responses'] and not message['content']) -%} + {{- '\n' -}} + {%- endif -%} +{%- endfor -%} + +{%- if add_generation_prompt -%} + {%- if ns.prev_message_type != 'tool_response' -%} + {{- '<|turn>model\n' -}} + {%- endif -%} + {%- if not enable_thinking | default(false) -%} + {{- '<|channel>thought\n' -}} + {%- endif -%} +{%- endif -%} diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 4b759237ee..d4ff27ac98 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -72,6 +72,7 @@ class ChatTemplate(str, Enum): qwen2_vl = "qwen2_vl" gemma3 = "gemma3" gemma3n = "gemma3n" + gemma4 = "gemma4" command_a = "command_a" command_a_tool_use = "command_a_tool_use" command_a_rag = "command_a_rag" diff --git a/src/axolotl/utils/schemas/trl.py b/src/axolotl/utils/schemas/trl.py index cd6a9c57a1..a362421627 100644 --- a/src/axolotl/utils/schemas/trl.py +++ b/src/axolotl/utils/schemas/trl.py @@ -69,9 +69,7 @@ class TRLConfig(BaseModel): generation_batch_size: int | None = Field( default=None, json_schema_extra={ - "description": "Batch size for generation. Controls how many unique " - "prompts are generated per step. For full DP utilization, set to " - "num_generations * data_parallel_size (or a multiple thereof)." + "description": "Batch size for generation. Controls how many unique prompts are generated per step. Should be num_generations * data_parallel_size for full DP utilization." }, ) num_generations: int | None = Field( diff --git a/tests/integrations/test_gemma4_moe.py b/tests/integrations/test_gemma4_moe.py new file mode 100644 index 0000000000..412d49b2f9 --- /dev/null +++ b/tests/integrations/test_gemma4_moe.py @@ -0,0 +1,1052 @@ +"""Validation tests for Gemma 4 MoE compatibility with ScatterMoE and SonicMoE. + +Gemma 4 has a unique MoE architecture: +- No separate SparseMoeBlock — MoE is embedded in the decoder layer +- Hybrid MLP+MoE: dense MLP runs in parallel with sparse MoE, outputs summed +- Custom router (Gemma4TextRouter): RMSNorm → scale → proj → softmax → topk → renorm → per_expert_scale +- Router is `self.router` (not `self.gate`) +- Experts use standard 3D param layout with @use_experts_implementation + +These tests validate that: +1. ScatterMoE kernels produce correct output for Gemma4 expert layout +2. ScatterMoE + LoRA produces correct output +3. SonicMoE integration code handles Gemma4 routing correctly +4. Weight layouts are compatible +""" + +import pytest +import torch +import torch.nn.functional as F +from torch import nn + +# ============================================================================ +# Gemma4 reference implementation (extracted from transformers) +# ============================================================================ + + +class Gemma4RMSNorm(nn.Module): + def __init__(self, dim, eps=1e-6, with_scale=True): + super().__init__() + self.eps = eps + self.with_scale = with_scale + if with_scale: + self.weight = nn.Parameter(torch.ones(dim)) + + def forward(self, x): + variance = x.float().pow(2).mean(-1, keepdim=True) + x = x * torch.rsqrt(variance + self.eps) + if self.with_scale: + return (self.weight * x).to(x.dtype) + return x.to(x.dtype) + + +class Gemma4TextRouter(nn.Module): + def __init__(self, hidden_size, num_experts, top_k, eps=1e-6): + super().__init__() + self.hidden_size = hidden_size + self.num_experts = num_experts + self.top_k = top_k + self.scalar_root_size = hidden_size**-0.5 + self.eps = eps + + self.norm = Gemma4RMSNorm(hidden_size, eps=eps, with_scale=False) + self.proj = nn.Linear(hidden_size, num_experts, bias=False) + self.scale = nn.Parameter(torch.ones(hidden_size)) + self.per_expert_scale = nn.Parameter(torch.ones(num_experts)) + + def forward(self, hidden_states): + hidden_states = self.norm(hidden_states) + hidden_states = hidden_states * self.scale * self.scalar_root_size + + expert_scores = self.proj(hidden_states.to(self.proj.weight.dtype)) + router_probabilities = F.softmax(expert_scores, dim=-1) + + top_k_weights, top_k_index = torch.topk( + router_probabilities, k=self.top_k, dim=-1 + ) + + top_k_weights /= top_k_weights.sum(dim=-1, keepdim=True) + top_k_weights = top_k_weights * self.per_expert_scale[top_k_index] + + return router_probabilities, top_k_weights, top_k_index + + +class Gemma4TextExperts(nn.Module): + def __init__(self, num_experts, hidden_size, intermediate_size, act_fn): + super().__init__() + self.num_experts = num_experts + self.hidden_dim = hidden_size + self.intermediate_dim = intermediate_size + self.gate_up_proj = nn.Parameter( + torch.empty(num_experts, 2 * intermediate_size, hidden_size) + ) + self.down_proj = nn.Parameter( + torch.empty(num_experts, hidden_size, intermediate_size) + ) + self.act_fn = act_fn + + def forward(self, hidden_states, top_k_index, top_k_weights): + final_hidden_states = torch.zeros_like(hidden_states) + with torch.no_grad(): + expert_mask = F.one_hot(top_k_index, num_classes=self.num_experts) + expert_mask = expert_mask.permute(2, 1, 0) + expert_hit = torch.greater(expert_mask.sum(dim=(-1, -2)), 0).nonzero() + + for expert_idx in expert_hit: + expert_idx = expert_idx[0] + if expert_idx == self.num_experts: + continue + top_k_pos, token_idx = torch.where(expert_mask[expert_idx]) + current_state = hidden_states[token_idx] + gate, up = F.linear(current_state, self.gate_up_proj[expert_idx]).chunk( + 2, dim=-1 + ) + current_hidden_states = self.act_fn(gate) * up + current_hidden_states = F.linear( + current_hidden_states, self.down_proj[expert_idx] + ) + current_hidden_states = ( + current_hidden_states * top_k_weights[token_idx, top_k_pos, None] + ) + final_hidden_states.index_add_( + 0, token_idx, current_hidden_states.to(final_hidden_states.dtype) + ) + + return final_hidden_states + + +# ============================================================================ +# Test fixtures +# ============================================================================ + + +@pytest.fixture +def gemma4_config(): + """Small Gemma4 MoE config for testing.""" + return { + "hidden_size": 128, + "num_experts": 8, + "top_k": 2, + "intermediate_size": 64, + "eps": 1e-6, + } + + +@pytest.fixture +def device(): + if not torch.cuda.is_available(): + pytest.skip("CUDA required") + return torch.device("cuda:0") + + +@pytest.fixture +def gemma4_moe_layer(gemma4_config, device): + """Create a Gemma4 MoE layer (router + experts) on GPU.""" + from transformers.activations import ACT2FN + + act_fn = ACT2FN["gelu_pytorch_tanh"] + + router = Gemma4TextRouter( + hidden_size=gemma4_config["hidden_size"], + num_experts=gemma4_config["num_experts"], + top_k=gemma4_config["top_k"], + eps=gemma4_config["eps"], + ) + experts = Gemma4TextExperts( + num_experts=gemma4_config["num_experts"], + hidden_size=gemma4_config["hidden_size"], + intermediate_size=gemma4_config["intermediate_size"], + act_fn=act_fn, + ) + + # Initialize weights + nn.init.kaiming_uniform_(experts.gate_up_proj) + nn.init.kaiming_uniform_(experts.down_proj) + nn.init.normal_(router.proj.weight, std=0.01) + + router = router.to(device).to(torch.bfloat16) + experts = experts.to(device).to(torch.bfloat16) + + return router, experts + + +# ============================================================================ +# ScatterMoE Tests +# ============================================================================ + + +class TestGemma4ScatterMoE: + """Test ScatterMoE kernel compatibility with Gemma4 expert layout.""" + + def test_scattermoe_experts_match_reference( + self, gemma4_moe_layer, gemma4_config, device + ): + """ScatterMoE kernel output matches reference expert computation.""" + from transformers.activations import ACT2FN + + from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, + parallel_linear, + ) + + router, experts = gemma4_moe_layer + act_fn = ACT2FN["gelu_pytorch_tanh"] + T = 16 # num tokens + H = gemma4_config["hidden_size"] + K = gemma4_config["top_k"] + E = gemma4_config["num_experts"] + + hidden_states = torch.randn(T, H, device=device, dtype=torch.bfloat16) + + # Reference forward + _, top_k_weights, top_k_index = router(hidden_states) + ref_output = experts(hidden_states, top_k_index, top_k_weights) + + # ScatterMoE forward + routing_weights = top_k_weights.to(hidden_states.dtype) + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = flatten_sort_count( + top_k_index, num_experts=E + ) + + # gate_up_proj is [E, 2*inter, H], ScatterMoE expects transposed: [E, H, 2*I] + gate_up_weight = experts.gate_up_proj.transpose(2, 1) + down_weight = experts.down_proj.transpose(2, 1) + + gates_h = parallel_linear( + hidden_states, + gate_up_weight, + K, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=False, + grouped_out=True, + ) + gates, h = gates_h.chunk(2, dim=-1) + h = act_fn(gates) * h + + scatter_output = parallel_linear( + h, + down_weight, + 1, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=True, + grouped_out=False, + gates=routing_weights, + ) + + # Allow bf16 tolerance + torch.testing.assert_close(scatter_output, ref_output, atol=1e-2, rtol=1e-2) + + def test_scattermoe_with_lora(self, gemma4_moe_layer, gemma4_config, device): + """ScatterMoE + LoRA kernel matches reference LoRA computation.""" + from transformers.activations import ACT2FN + + from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, + parallel_linear, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_linear_lora import ( + parallel_linear_lora, + ) + + router, experts = gemma4_moe_layer + act_fn = ACT2FN["gelu_pytorch_tanh"] + T = 16 + H = gemma4_config["hidden_size"] + K = gemma4_config["top_k"] + E = gemma4_config["num_experts"] + inter = gemma4_config["intermediate_size"] + rank = 4 + scaling = 0.5 + + hidden_states = torch.randn(T, H, device=device, dtype=torch.bfloat16) + + # Create LoRA weights for gate_up_proj + # ScatterMoE layout: A=[r*E, K], B=[N, r*E] + lora_A_gup = ( + torch.randn(rank * E, H, device=device, dtype=torch.bfloat16) * 0.01 + ) + lora_B_gup = ( + torch.randn(2 * inter, rank * E, device=device, dtype=torch.bfloat16) * 0.01 + ) + + # Reference: manual LoRA application per expert + _, top_k_weights, top_k_index = router(hidden_states) + ref_output = torch.zeros(T, H, device=device, dtype=torch.bfloat16) + with torch.no_grad(): + expert_mask = F.one_hot(top_k_index, num_classes=E).permute(2, 1, 0) + expert_hit = torch.greater(expert_mask.sum(dim=(-1, -2)), 0).nonzero() + + for eidx in expert_hit: + eidx = eidx[0] + if eidx == E: + continue + top_k_pos, token_idx = torch.where(expert_mask[eidx]) + current_state = hidden_states[token_idx] + + # Base gate_up + LoRA delta + base_out = F.linear(current_state, experts.gate_up_proj[eidx]) + lora_a_slice = lora_A_gup[eidx * rank : (eidx + 1) * rank, :] + lora_b_slice = lora_B_gup[:, eidx * rank : (eidx + 1) * rank] + lora_delta = ( + F.linear(F.linear(current_state, lora_a_slice), lora_b_slice) * scaling + ) + combined = base_out + lora_delta + + gate, up = combined.chunk(2, dim=-1) + h = act_fn(gate) * up + h = F.linear(h, experts.down_proj[eidx]) + h = h * top_k_weights[token_idx, top_k_pos, None] + ref_output.index_add_(0, token_idx, h.to(ref_output.dtype)) + + # ScatterMoE LoRA forward + routing_weights = top_k_weights.to(hidden_states.dtype) + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = flatten_sort_count( + top_k_index, num_experts=E + ) + + gate_up_weight = experts.gate_up_proj.transpose(2, 1) + down_weight = experts.down_proj.transpose(2, 1) + + gates_h = parallel_linear_lora( + hidden_states, + gate_up_weight, + K, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + lora_A_gup, + lora_B_gup, + scaling, + grouped_in=False, + grouped_out=True, + ) + gates, h = gates_h.chunk(2, dim=-1) + h = act_fn(gates) * h + + scatter_output = parallel_linear( + h, + down_weight, + 1, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=True, + grouped_out=False, + gates=routing_weights, + ) + + torch.testing.assert_close(scatter_output, ref_output, atol=5e-2, rtol=5e-2) + + def test_gemma4_routing_correctness(self, gemma4_moe_layer, gemma4_config, device): + """Gemma4 custom routing (norm+scale+per_expert_scale) produces valid outputs.""" + router, _ = gemma4_moe_layer + T = 32 + H = gemma4_config["hidden_size"] + K = gemma4_config["top_k"] + E = gemma4_config["num_experts"] + + hidden_states = torch.randn(T, H, device=device, dtype=torch.bfloat16) + router_probs, top_k_weights, top_k_index = router(hidden_states) + + # Check shapes + assert router_probs.shape == (T, E) + assert top_k_weights.shape == (T, K) + assert top_k_index.shape == (T, K) + + # Router probs should be valid probability distribution + assert (router_probs >= 0).all() + assert torch.allclose( + router_probs.sum(dim=-1), + torch.ones(T, device=device, dtype=router_probs.dtype), + atol=1e-3, + ) + + # Top-k indices should be valid expert indices + assert (top_k_index >= 0).all() + assert (top_k_index < E).all() + + # Top-k weights should be non-negative (per_expert_scale can change sign though) + # Just verify finite + assert top_k_weights.isfinite().all() + + def test_scattermoe_gradients_flow(self, gemma4_moe_layer, gemma4_config, device): + """Verify gradients flow through ScatterMoE kernels for Gemma4.""" + from transformers.activations import ACT2FN + + from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, + parallel_linear, + ) + + router, experts = gemma4_moe_layer + + # Enable grad for expert weights + experts.gate_up_proj.requires_grad_(True) + experts.down_proj.requires_grad_(True) + + act_fn = ACT2FN["gelu_pytorch_tanh"] + T = 16 + H = gemma4_config["hidden_size"] + K = gemma4_config["top_k"] + E = gemma4_config["num_experts"] + + hidden_states = torch.randn(T, H, device=device, dtype=torch.bfloat16) + + with torch.no_grad(): + _, top_k_weights, top_k_index = router(hidden_states) + + routing_weights = top_k_weights.to(hidden_states.dtype) + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = flatten_sort_count( + top_k_index, num_experts=E + ) + + gate_up_weight = experts.gate_up_proj.transpose(2, 1) + down_weight = experts.down_proj.transpose(2, 1) + + gates_h = parallel_linear( + hidden_states, + gate_up_weight, + K, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=False, + grouped_out=True, + ) + gates, h = gates_h.chunk(2, dim=-1) + h = act_fn(gates) * h + + output = parallel_linear( + h, + down_weight, + 1, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=True, + grouped_out=False, + gates=routing_weights, + ) + + loss = output.sum() + loss.backward() + + assert experts.gate_up_proj.grad is not None + assert experts.down_proj.grad is not None + assert experts.gate_up_proj.grad.isfinite().all() + assert experts.down_proj.grad.isfinite().all() + + +# ============================================================================ +# SonicMoE Tests +# ============================================================================ + + +def _can_import_sonicmoe(): + try: + from sonicmoe.enums import ActivationType # noqa: F401 + + return True + except Exception: + return False + + +class TestGemma4SonicMoE: + """Test SonicMoE compatibility with Gemma4. + + SonicMoE requires Hopper/Blackwell GPU. Tests that need sonicmoe + import are skipped on unsupported GPUs. + """ + + @pytest.mark.skipif( + not _can_import_sonicmoe(), + reason="sonicmoe requires Hopper/Blackwell GPU", + ) + def test_gemma4_routing_function_config(self, gemma4_config): + """Gemma4 is registered with correct routing config.""" + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( + get_model_moe_config, + ) + + routing_fn, activation, router_attr = get_model_moe_config("gemma4_text") + + assert router_attr == "router" + assert routing_fn is not None + assert routing_fn.__name__ == "gemma4_routing" + + from sonicmoe.enums import ActivationType + + assert activation == ActivationType.GEGLU + + @pytest.mark.skipif( + not _can_import_sonicmoe(), + reason="sonicmoe requires Hopper/Blackwell GPU", + ) + def test_gemma4_routing_matches_reference(self, gemma4_config): + """Routing function output matches reference Gemma4TextRouter.""" + from axolotl.integrations.kernels.libs.sonicmoe.routing import ( + get_model_moe_config, + ) + + routing_fn, _, _ = get_model_moe_config("gemma4_text") + H = gemma4_config["hidden_size"] + E = gemma4_config["num_experts"] + K = gemma4_config["top_k"] + T = 16 + + router = Gemma4TextRouter(H, E, K) + nn.init.normal_(router.proj.weight, std=0.01) + + class MockGemma4MoeBlock: + pass + + mock_block = MockGemma4MoeBlock() + mock_block.router = router + + hidden_states = torch.randn(T, H) + + # Reference + _ref_probs, ref_weights, ref_indices = router(hidden_states) + + # Routing function + flat_scores, flat_token_idx, flat_expert_idx, router_logits = routing_fn( + hidden_states, mock_block + ) + + # Check shapes + assert flat_scores.shape == (T * K,) + assert flat_token_idx.shape == (T * K,) + assert flat_expert_idx.shape == (T * K,) + assert router_logits.shape == (T, E) + + # Reconstruct per-token routing from flat output and compare + for t in range(T): + mask = flat_token_idx == t + assert mask.sum() == K, f"Token {t} should have {K} entries" + + flat_experts_for_t = flat_expert_idx[mask].sort().values + ref_experts_for_t = ref_indices[t].sort().values.to(torch.int32) + assert torch.equal(flat_experts_for_t, ref_experts_for_t), ( + f"Token {t}: experts mismatch" + ) + + # Verify scores match reference per-token + for t in range(T): + mask = flat_token_idx == t + flat_experts_t = flat_expert_idx[mask] + flat_scores_t = flat_scores[mask] + + sort_idx = flat_experts_t.argsort() + flat_scores_sorted = flat_scores_t[sort_idx] + + ref_sort_idx = ref_indices[t].argsort() + ref_scores_sorted = ref_weights[t][ref_sort_idx].float() + + torch.testing.assert_close( + flat_scores_sorted, ref_scores_sorted, atol=1e-4, rtol=1e-4 + ) + + def test_gemma4_weight_layout_compatible(self, gemma4_config): + """Verify Gemma4 expert weight layout is compatible with SonicMoE.""" + E = gemma4_config["num_experts"] + H = gemma4_config["hidden_size"] + inter = gemma4_config["intermediate_size"] + + gate_up_proj = torch.randn(E, 2 * inter, H) + down_proj = torch.randn(E, H, inter) + + # SonicMoE expects [dim, dim, E] (experts last) + gate_up_sonic = gate_up_proj.permute(1, 2, 0) + down_sonic = down_proj.permute(1, 2, 0) + + assert gate_up_sonic.shape == (2 * inter, H, E) + assert down_sonic.shape == (H, inter, E) + + # Verify roundtrip + recovered_gate_up = gate_up_sonic.permute(2, 0, 1) + assert torch.equal(gate_up_proj, recovered_gate_up) + + def test_gemma4_is_experts_only_model(self): + """Verify gemma4_text is recognized as experts-only model.""" + from axolotl.integrations.kernels.constants import ( + is_experts_only_model, + resolve_experts_class, + ) + + assert is_experts_only_model("gemma4_text") + cls = resolve_experts_class("gemma4_text") + assert cls is not None + assert cls.__name__ == "Gemma4TextExperts" + + def test_gemma4_not_in_sparse_moe_block(self): + """Verify gemma4_text is NOT in SPARSE_MOE_BLOCK (has no SparseMoeBlock).""" + from axolotl.integrations.kernels.constants import SPARSE_MOE_BLOCK + + assert "gemma4_text" not in SPARSE_MOE_BLOCK + + +# ============================================================================ +# Integration Tests (full layer with real model config) +# ============================================================================ + + +class TestGemma4FullLayerIntegration: + """Test with realistic Gemma4 config (26B-A4B dimensions, single layer).""" + + @pytest.fixture + def real_config(self): + return { + "hidden_size": 2816, + "num_experts": 128, + "top_k": 8, + "intermediate_size": 704, + "eps": 1e-6, + } + + def test_scattermoe_real_dimensions(self, real_config, device): + """ScatterMoE works with real Gemma4-26B-A4B expert dimensions.""" + from transformers.activations import ACT2FN + + from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, + parallel_linear, + ) + + act_fn = ACT2FN["gelu_pytorch_tanh"] + H = real_config["hidden_size"] + E = real_config["num_experts"] + K = real_config["top_k"] + inter = real_config["intermediate_size"] + T = 32 + + # Create experts on GPU + gate_up_proj = ( + torch.randn(E, 2 * inter, H, device=device, dtype=torch.bfloat16) * 0.01 + ) + down_proj = torch.randn(E, H, inter, device=device, dtype=torch.bfloat16) * 0.01 + hidden_states = torch.randn(T, H, device=device, dtype=torch.bfloat16) + + # Simulate routing (random valid assignment) + top_k_index = torch.stack( + [torch.randperm(E, device=device)[:K] for _ in range(T)] + ) + top_k_weights = torch.softmax( + torch.randn(T, K, device=device, dtype=torch.bfloat16), dim=-1 + ) + + # Reference + ref_output = torch.zeros(T, H, device=device, dtype=torch.bfloat16) + with torch.no_grad(): + expert_mask = F.one_hot(top_k_index, num_classes=E).permute(2, 1, 0) + expert_hit = torch.greater(expert_mask.sum(dim=(-1, -2)), 0).nonzero() + + for eidx in expert_hit: + eidx = eidx[0] + top_k_pos, token_idx = torch.where(expert_mask[eidx]) + current_state = hidden_states[token_idx] + gate, up = F.linear(current_state, gate_up_proj[eidx]).chunk(2, dim=-1) + h = act_fn(gate) * up + h = F.linear(h, down_proj[eidx]) + h = h * top_k_weights[token_idx, top_k_pos, None] + ref_output.index_add_(0, token_idx, h.to(ref_output.dtype)) + + # ScatterMoE + routing_weights = top_k_weights.to(hidden_states.dtype) + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = flatten_sort_count( + top_k_index, num_experts=E + ) + + gates_h = parallel_linear( + hidden_states, + gate_up_proj.transpose(2, 1), + K, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=False, + grouped_out=True, + ) + gates, h = gates_h.chunk(2, dim=-1) + h = act_fn(gates) * h + + scatter_output = parallel_linear( + h, + down_proj.transpose(2, 1), + 1, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=True, + grouped_out=False, + gates=routing_weights, + ) + + torch.testing.assert_close(scatter_output, ref_output, atol=5e-2, rtol=5e-2) + + def test_scattermoe_lora_real_dimensions(self, real_config, device): + """ScatterMoE + LoRA works with real Gemma4-26B-A4B dimensions.""" + from transformers.activations import ACT2FN + + from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, + parallel_linear, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_linear_lora import ( + parallel_linear_lora, + ) + + act_fn = ACT2FN["gelu_pytorch_tanh"] + H = real_config["hidden_size"] + E = real_config["num_experts"] + K = real_config["top_k"] + inter = real_config["intermediate_size"] + T = 32 + rank = 8 + scaling = 0.5 + + gate_up_proj = ( + torch.randn(E, 2 * inter, H, device=device, dtype=torch.bfloat16) * 0.01 + ) + down_proj = torch.randn(E, H, inter, device=device, dtype=torch.bfloat16) * 0.01 + lora_A = torch.randn(rank * E, H, device=device, dtype=torch.bfloat16) * 0.01 + lora_B = ( + torch.randn(2 * inter, rank * E, device=device, dtype=torch.bfloat16) * 0.01 + ) + hidden_states = torch.randn(T, H, device=device, dtype=torch.bfloat16) + + # Random routing + top_k_index = torch.stack( + [torch.randperm(E, device=device)[:K] for _ in range(T)] + ) + top_k_weights = torch.softmax( + torch.randn(T, K, device=device, dtype=torch.bfloat16), dim=-1 + ) + + routing_weights = top_k_weights.to(hidden_states.dtype) + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = flatten_sort_count( + top_k_index, num_experts=E + ) + + # ScatterMoE + LoRA on gate_up + gates_h = parallel_linear_lora( + hidden_states, + gate_up_proj.transpose(2, 1), + K, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + lora_A, + lora_B, + scaling, + grouped_in=False, + grouped_out=True, + ) + gates, h = gates_h.chunk(2, dim=-1) + h = act_fn(gates) * h + + output = parallel_linear( + h, + down_proj.transpose(2, 1), + 1, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + grouped_in=True, + grouped_out=False, + gates=routing_weights, + ) + + # Basic sanity: output should be finite and right shape + assert output.shape == (T, H) + assert output.isfinite().all() + + +class TestExpertsInterfaceIntegration: + """Test the ExpertsInterface registration (the clean transformers hook).""" + + @staticmethod + def _make_gemma4_config(): + from transformers.models.gemma4.configuration_gemma4 import Gemma4TextConfig + + return Gemma4TextConfig( + hidden_size=128, + num_experts=8, + top_k_experts=2, + moe_intermediate_size=64, + hidden_activation="gelu_pytorch_tanh", + num_hidden_layers=2, + num_attention_heads=2, + num_key_value_heads=1, + head_dim=64, + intermediate_size=256, + enable_moe_block=True, + ) + + def test_register_scattermoe_in_experts_interface(self): + """register_scattermoe_experts adds 'scattermoe' to the global interface.""" + from transformers.integrations.moe import ALL_EXPERTS_FUNCTIONS + + from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + register_scattermoe_experts, + scattermoe_experts_forward, + ) + + register_scattermoe_experts() + + assert "scattermoe" in ALL_EXPERTS_FUNCTIONS + assert ALL_EXPERTS_FUNCTIONS["scattermoe"] is scattermoe_experts_forward + + def test_experts_implementation_dispatches_to_scattermoe(self, device): + """Setting config._experts_implementation='scattermoe' dispatches correctly.""" + from transformers.models.gemma4.modeling_gemma4 import ( + Gemma4TextExperts as HFGemma4TextExperts, + ) + + from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + register_scattermoe_experts, + ) + + register_scattermoe_experts() + + cfg = self._make_gemma4_config() + cfg._experts_implementation = "scattermoe" + + with torch.device("meta"): + hf_experts = HFGemma4TextExperts(cfg) + + hf_experts = hf_experts.to_empty(device=device) + nn.init.kaiming_uniform_(hf_experts.gate_up_proj) + nn.init.kaiming_uniform_(hf_experts.down_proj) + hf_experts = hf_experts.to(torch.bfloat16) + + T, K = 16, 2 + hidden_states = torch.randn(T, 128, device=device, dtype=torch.bfloat16) + top_k_index = torch.stack( + [torch.randperm(8, device=device)[:K] for _ in range(T)] + ) + top_k_weights = torch.softmax( + torch.randn(T, K, device=device, dtype=torch.bfloat16), dim=-1 + ) + + # Get reference output with eager implementation + cfg_eager = self._make_gemma4_config() + cfg_eager._experts_implementation = "eager" + with torch.device("meta"): + eager_experts = HFGemma4TextExperts(cfg_eager) + eager_experts = eager_experts.to_empty(device=device).to(torch.bfloat16) + # Copy weights from scattermoe experts + eager_experts.gate_up_proj.data.copy_(hf_experts.gate_up_proj.data) + eager_experts.down_proj.data.copy_(hf_experts.down_proj.data) + + ref_output = eager_experts(hidden_states, top_k_index, top_k_weights) + + # ScatterMoE dispatched output + scatter_output = hf_experts(hidden_states, top_k_index, top_k_weights) + + torch.testing.assert_close(scatter_output, ref_output, atol=1e-2, rtol=1e-2) + + def test_validation_accepts_scattermoe(self): + """get_correct_experts_implementation accepts 'scattermoe' after registration.""" + from transformers.modeling_utils import PreTrainedModel + + from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + register_scattermoe_experts, + ) + + register_scattermoe_experts() + + # Should not raise + result = PreTrainedModel.get_correct_experts_implementation(None, "scattermoe") + assert result == "scattermoe" + + def test_eager_still_works_after_registration(self, device): + """Registering scattermoe doesn't break eager dispatch.""" + from transformers.models.gemma4.modeling_gemma4 import ( + Gemma4TextExperts as HFGemma4TextExperts, + ) + + from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + register_scattermoe_experts, + ) + + register_scattermoe_experts() + + cfg = self._make_gemma4_config() + cfg._experts_implementation = "eager" + + with torch.device("meta"): + hf_experts = HFGemma4TextExperts(cfg) + + hf_experts = hf_experts.to_empty(device=device) + nn.init.kaiming_uniform_(hf_experts.gate_up_proj) + nn.init.kaiming_uniform_(hf_experts.down_proj) + hf_experts = hf_experts.to(torch.bfloat16) + + T, K = 16, 2 + hidden_states = torch.randn(T, 128, device=device, dtype=torch.bfloat16) + top_k_index = torch.stack( + [torch.randperm(8, device=device)[:K] for _ in range(T)] + ) + top_k_weights = torch.softmax( + torch.randn(T, K, device=device, dtype=torch.bfloat16), dim=-1 + ) + + # Should use eager (original) forward without error + output = hf_experts(hidden_states, top_k_index, top_k_weights) + assert output.shape == (T, 128) + assert output.isfinite().all() + + +class TestScatterMoEExpertsInterfaceMultiModel: + """Test that the registered scattermoe ExpertsInterface works across model types. + + All @use_experts_implementation Experts classes share the same layout: + gate_up_proj [E, 2*inter, H], down_proj [E, H, inter], forward(hidden_states, top_k_index, top_k_weights). + """ + + MODEL_EXPERTS = [ + ( + "transformers.models.gemma4.modeling_gemma4", + "Gemma4TextExperts", + { + "hidden_size": 128, + "num_experts": 8, + "moe_intermediate_size": 64, + "hidden_activation": "gelu_pytorch_tanh", + "top_k_experts": 2, + "num_hidden_layers": 2, + "num_attention_heads": 2, + "num_key_value_heads": 1, + "head_dim": 64, + "intermediate_size": 256, + "enable_moe_block": True, + }, + "transformers.models.gemma4.configuration_gemma4.Gemma4TextConfig", + ), + ( + "transformers.models.qwen3_moe.modeling_qwen3_moe", + "Qwen3MoeExperts", + { + "hidden_size": 128, + "num_experts": 8, + "moe_intermediate_size": 64, + "hidden_act": "silu", + "num_experts_per_tok": 2, + "num_hidden_layers": 2, + "num_attention_heads": 2, + "num_key_value_heads": 1, + "intermediate_size": 256, + }, + "transformers.models.qwen3_moe.configuration_qwen3_moe.Qwen3MoeConfig", + ), + ( + "transformers.models.olmoe.modeling_olmoe", + "OlmoeExperts", + { + "hidden_size": 128, + "num_experts": 8, + "intermediate_size": 64, + "hidden_act": "silu", + "num_experts_per_tok": 2, + "num_hidden_layers": 2, + "num_attention_heads": 2, + "num_key_value_heads": 1, + }, + "transformers.models.olmoe.configuration_olmoe.OlmoeConfig", + ), + ( + "transformers.models.mixtral.modeling_mixtral", + "MixtralExperts", + { + "hidden_size": 128, + "num_local_experts": 8, + "intermediate_size": 64, + "hidden_act": "silu", + "num_experts_per_tok": 2, + "num_hidden_layers": 2, + "num_attention_heads": 2, + "num_key_value_heads": 1, + }, + "transformers.models.mixtral.configuration_mixtral.MixtralConfig", + ), + ] + + @pytest.fixture( + params=[m[1] for m in MODEL_EXPERTS], ids=[m[1] for m in MODEL_EXPERTS] + ) + def model_setup(self, request, device): + """Create an Experts instance for each model type.""" + import importlib + + from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + register_scattermoe_experts, + ) + + register_scattermoe_experts() + + for module_path, cls_name, cfg_kwargs, config_cls_path in self.MODEL_EXPERTS: + if cls_name == request.param: + # Import config class + config_module, config_class = config_cls_path.rsplit(".", 1) + config_cls = getattr( + importlib.import_module(config_module), config_class + ) + cfg = config_cls(**cfg_kwargs) + + # Import experts class + module = importlib.import_module(module_path) + experts_cls = getattr(module, cls_name) + + # Create eager reference + cfg_eager = config_cls(**cfg_kwargs) + cfg_eager._experts_implementation = "eager" + with torch.device("meta"): + eager = experts_cls(cfg_eager) + eager = eager.to_empty(device=device).to(torch.bfloat16) + nn.init.kaiming_uniform_(eager.gate_up_proj) + nn.init.kaiming_uniform_(eager.down_proj) + + # Create scattermoe version with same weights + cfg._experts_implementation = "scattermoe" + with torch.device("meta"): + scatter = experts_cls(cfg) + scatter = scatter.to_empty(device=device).to(torch.bfloat16) + scatter.gate_up_proj.data.copy_(eager.gate_up_proj.data) + scatter.down_proj.data.copy_(eager.down_proj.data) + + return ( + cls_name, + eager, + scatter, + cfg_kwargs.get( + "num_experts", cfg_kwargs.get("num_local_experts", 8) + ), + ) + + def test_scattermoe_matches_eager(self, model_setup, device): + """ScatterMoE ExpertsInterface output matches eager for each model type.""" + cls_name, eager, scatter, num_experts = model_setup + T, K = 16, 2 + + hidden_states = torch.randn(T, 128, device=device, dtype=torch.bfloat16) + top_k_index = torch.stack( + [torch.randperm(num_experts, device=device)[:K] for _ in range(T)] + ) + top_k_weights = torch.softmax( + torch.randn(T, K, device=device, dtype=torch.bfloat16), dim=-1 + ) + + ref_output = eager(hidden_states, top_k_index, top_k_weights) + scatter_output = scatter(hidden_states, top_k_index, top_k_weights) + + torch.testing.assert_close( + scatter_output, + ref_output, + atol=1e-2, + rtol=1e-2, + msg=f"{cls_name}: ScatterMoE output doesn't match eager", + ) From 900eec798861eb9f2e3b0c3daaf0aa6bd3358a2b Mon Sep 17 00:00:00 2001 From: Maxime <672982+maximegmd@users.noreply.github.com> Date: Sat, 4 Apr 2026 05:16:58 -0400 Subject: [PATCH 1252/1405] Fix DO_NOT_TRACK not being correctly handled (#3580) * Fix DO_NOT_TRACK not being correctly handled * add unit tests and lint --------- Co-authored-by: Wing Lian --- src/axolotl/telemetry/manager.py | 33 ++++-------- tests/telemetry/test_manager.py | 90 ++++++++++++++++++-------------- 2 files changed, 60 insertions(+), 63 deletions(-) diff --git a/src/axolotl/telemetry/manager.py b/src/axolotl/telemetry/manager.py index 4fdda89a95..5a09309d3c 100644 --- a/src/axolotl/telemetry/manager.py +++ b/src/axolotl/telemetry/manager.py @@ -160,29 +160,16 @@ def _check_telemetry_enabled(self) -> bool: if not is_main_process(): return False - # Parse relevant env vars - axolotl_do_not_track = os.getenv("AXOLOTL_DO_NOT_TRACK") - do_not_track = os.getenv("DO_NOT_TRACK") - - # Default to enabled (opt-out model) - if axolotl_do_not_track is None or axolotl_do_not_track.lower() not in ( - "0", - "1", - "false", - "true", - ): - return True - - if do_not_track is None: - do_not_track = "0" - - # Respect AXOLOTL_DO_NOT_TRACK, DO_NOT_TRACK if enabled - enabled = axolotl_do_not_track.lower() not in ( - "1", - "true", - ) and do_not_track.lower() not in ("1", "true") - - return enabled + def is_truthy_env(var_name: str) -> bool: + value = os.getenv(var_name) + if value is None: + return False + return value.strip().lower() in ("1", "true") + + # Telemetry is enabled by default unless either opt-out var is set + return not ( + is_truthy_env("AXOLOTL_DO_NOT_TRACK") or is_truthy_env("DO_NOT_TRACK") + ) def _load_whitelist(self) -> dict: """Load HuggingFace Hub organization whitelist""" diff --git a/tests/telemetry/test_manager.py b/tests/telemetry/test_manager.py index 36ca44c356..985f39d23d 100644 --- a/tests/telemetry/test_manager.py +++ b/tests/telemetry/test_manager.py @@ -65,47 +65,57 @@ def test_singleton_instance(telemetry_manager_class): assert telemetry_manager_class.get_instance() is first -def test_telemetry_enabled_by_default(telemetry_manager_class): - """Test that telemetry is enabled by default (opt-out)""" - with ( - patch.dict(os.environ, {"RANK": "0"}, clear=True), - patch("time.sleep"), - patch("logging.Logger.info"), - ): - manager = telemetry_manager_class() - assert manager.enabled - - -def test_telemetry_enabled_with_explicit_opt_in(telemetry_manager_class): - """Test that telemetry is enabled when AXOLOTL_DO_NOT_TRACK=0""" - with ( - patch.dict(os.environ, {"AXOLOTL_DO_NOT_TRACK": "0", "RANK": "0"}), - patch("time.sleep"), +class TestTelemetryOptOut: + """ + Telemetry is opt-out: enabled by default, disabled by AXOLOTL_DO_NOT_TRACK + or DO_NOT_TRACK. Each env var is checked independently — setting either one + to a truthy value ("1" or "true") disables telemetry. + + The parametrized table below is the source of truth for expected behavior. + """ + + # fmt: off + # AXOLOTL_DO_NOT_TRACK DO_NOT_TRACK expected + @pytest.mark.parametrize("axolotl_dnt, dnt, expected", [ + # --- Neither var set: telemetry ON --- + (None, None, True), + + # --- Only AXOLOTL_DO_NOT_TRACK set --- + ("0", None, True), # explicit opt-in + ("false", None, True), # explicit opt-in + ("1", None, False), # opt-out + ("true", None, False), # opt-out + (" 1 ", None, False), # whitespace-padded opt-out + + # --- Only DO_NOT_TRACK set (was broken before fix) --- + (None, "0", True), # explicit opt-in + (None, "false", True), # explicit opt-in + (None, "1", False), # opt-out + (None, "true", False), # opt-out + + # --- Both set: either truthy → disabled --- + ("0", "1", False), # DO_NOT_TRACK wins + ("1", "0", False), # AXOLOTL_DO_NOT_TRACK wins + ("1", "1", False), # both opt-out + ("0", "0", True), # both opt-in + ]) + # fmt: on + def test_do_not_track_env_vars( + self, telemetry_manager_class, axolotl_dnt, dnt, expected ): - manager = telemetry_manager_class() - assert manager.enabled - - -def test_telemetry_disabled_with_axolotl_do_not_track(telemetry_manager_class): - """Test that telemetry is disabled when AXOLOTL_DO_NOT_TRACK=1""" - with ( - patch.dict(os.environ, {"AXOLOTL_DO_NOT_TRACK": "1", "RANK": "0"}), - patch("time.sleep"), - ): - manager = telemetry_manager_class() - assert not manager.enabled - - -def test_telemetry_disabled_with_do_not_track(telemetry_manager_class): - """Test that telemetry is disabled when DO_NOT_TRACK=1""" - with ( - patch.dict( - os.environ, {"AXOLOTL_DO_NOT_TRACK": "0", "DO_NOT_TRACK": "1", "RANK": "0"} - ), - patch("time.sleep"), - ): - manager = telemetry_manager_class() - assert not manager.enabled + env = {"RANK": "0"} + if axolotl_dnt is not None: + env["AXOLOTL_DO_NOT_TRACK"] = axolotl_dnt + if dnt is not None: + env["DO_NOT_TRACK"] = dnt + + with ( + patch.dict(os.environ, env, clear=True), + patch("time.sleep"), + patch("logging.Logger.info"), + ): + manager = telemetry_manager_class() + assert manager.enabled is expected def test_telemetry_disabled_for_non_main_process(telemetry_manager_class): From 6f15da4cac71c96ff541ac76c0d32e4cc7aede8e Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 6 Apr 2026 10:00:55 -0700 Subject: [PATCH 1253/1405] make it easier for agents to discover docs (#3579) [skip ci] * make it easier for agents to discover docs * fixup pr comments --- .github/workflows/tests.yml | 10 +++ AGENTS.md | 3 + MANIFEST.in | 2 + README.md | 23 +++++ src/axolotl/cli/agent_docs/__init__.py | 106 +++++++++++++++++++++++ src/axolotl/cli/main.py | 113 ++++++++++++++++++++++++- 6 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 src/axolotl/cli/agent_docs/__init__.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d753afe015..b1e9c718e2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -220,6 +220,16 @@ jobs: run: | axolotl --help + - name: Verify agent docs are discoverable + run: | + # Agent docs live in docs/agents/ (source of truth) and are resolved + # at runtime from the repo checkout or via `axolotl fetch docs` + axolotl agent-docs --list + axolotl agent-docs | grep -q "Fine-tuning framework" + axolotl agent-docs grpo | grep -q "GRPO" + axolotl agent-docs sft | grep -q "SFT" + python -c "from axolotl.cli.agent_docs import get_doc, list_topics; assert len(list_topics()) >= 5; assert 'GRPO' in get_doc('grpo')" + - name: Show HF cache run: hf cache ls diff --git a/AGENTS.md b/AGENTS.md index b81904e43c..6fb81e506e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,9 @@ axolotl inference config.yaml # Interactive inference axolotl merge-lora config.yaml # Merge LoRA adapter into base model axolotl vllm-serve config.yaml # Start vLLM server for GRPO/EBFT training axolotl fetch examples # Download example configs +axolotl agent-docs # Show agent-optimized docs (bundled with pip package) +axolotl agent-docs grpo # Topic-specific agent reference +axolotl config-schema # Dump config JSON schema ``` ## Training Methods diff --git a/MANIFEST.in b/MANIFEST.in index 3fbb0edca4..30cd072429 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,6 @@ include README.md include LICENSE include src/setuptools_axolotl_dynamic_dependencies.py include src/axolotl/utils/chat_templates/templates/*.jinja +include AGENTS.md +recursive-include docs/agents *.md recursive-include axolotl *.py diff --git a/README.md b/README.md index e353d20ad8..3d0710f0ff 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,29 @@ That's it! Check out our [Getting Started Guide](https://docs.axolotl.ai/docs/ge - [API Reference](https://docs.axolotl.ai/docs/api/) - Auto-generated code documentation - [FAQ](https://docs.axolotl.ai/docs/faq.html) - Frequently asked questions +## AI Agent Support + +Axolotl ships with built-in documentation optimized for AI coding agents (Claude Code, Cursor, Copilot, etc.). These docs are bundled with the pip package — no repo clone needed. + +```bash +# Show overview and available training methods +axolotl agent-docs + +# Topic-specific references +axolotl agent-docs sft # supervised fine-tuning +axolotl agent-docs grpo # GRPO online RL +axolotl agent-docs preference_tuning # DPO, KTO, ORPO, SimPO +axolotl agent-docs reward_modelling # outcome and process reward models +axolotl agent-docs pretraining # continual pretraining +axolotl agent-docs --list # list all topics + +# Dump config schema for programmatic use +axolotl config-schema +axolotl config-schema --field adapter +``` + +If you're working with the source repo, agent docs are also available at `docs/agents/` and the project overview is in `AGENTS.md`. + ## 🤝 Getting Help - Join our [Discord community](https://discord.gg/HhrNrHJPRb) for support diff --git a/src/axolotl/cli/agent_docs/__init__.py b/src/axolotl/cli/agent_docs/__init__.py new file mode 100644 index 0000000000..d229184c08 --- /dev/null +++ b/src/axolotl/cli/agent_docs/__init__.py @@ -0,0 +1,106 @@ +"""Bundled agent documentation for axolotl. + +These docs are optimized for consumption by AI coding agents. +The source of truth is docs/agents/*.md and AGENTS.md in the repo root. +This module resolves those paths at runtime — no files are duplicated +into the package. + +For pip-only installs (no repo checkout), run `axolotl fetch docs` first +to download the docs locally. +""" + +from pathlib import Path + +# Topic name -> (filename in docs/agents/, fallback filename for AGENTS.md) +TOPICS = { + "overview": "AGENTS.md", + "sft": "docs/agents/sft.md", + "grpo": "docs/agents/grpo.md", + "preference_tuning": "docs/agents/preference_tuning.md", + "reward_modelling": "docs/agents/reward_modelling.md", + "pretraining": "docs/agents/pretraining.md", +} + + +def _find_repo_root() -> Path | None: + """Walk up from this file to find the repo root (contains AGENTS.md).""" + # In an editable install or repo checkout, walk up from + # src/axolotl/cli/agent_docs/ to find the repo root + current = Path(__file__).resolve().parent + while current != current.parent: + if (current / "AGENTS.md").exists() and (current / "docs" / "agents").is_dir(): + return current + current = current.parent + return None + + +def _find_docs_dir() -> Path | None: + """Find a fetched docs directory (from `axolotl fetch docs`).""" + # axolotl fetch docs --dest defaults to ./docs/ in cwd + cwd_docs = Path.cwd() / "docs" / "agents" + if cwd_docs.is_dir(): + return Path.cwd() + return None + + +def _resolve_path(topic: str) -> Path: + """Resolve a topic name to the actual file path.""" + if topic not in TOPICS: + available = ", ".join(sorted(TOPICS.keys())) + raise FileNotFoundError(f"Unknown topic: {topic!r}. Available: {available}") + + relative_path = TOPICS[topic] + + # Try repo root first (editable install / repo checkout) + repo_root = _find_repo_root() + if repo_root: + candidate = repo_root / relative_path + if candidate.exists(): + return candidate + + # Try cwd (fetched docs via `axolotl fetch docs`) + docs_root = _find_docs_dir() + if docs_root: + candidate = docs_root / relative_path + if candidate.exists(): + return candidate + + # Also check cwd directly for AGENTS.md + if topic == "overview": + cwd_agents = Path.cwd() / "AGENTS.md" + if cwd_agents.exists(): + return cwd_agents + + raise FileNotFoundError( + f"Could not find {relative_path!r}. " + f"If you installed axolotl via pip, run `axolotl fetch docs` first " + f"to download the documentation." + ) + + +def get_doc(topic: str = "overview") -> str: + """Return the content of an agent doc by topic name. + + Args: + topic: One of the keys in TOPICS, or "overview" (default). + + Returns: + The markdown content of the doc. + + Raises: + FileNotFoundError: If the topic can't be found. + """ + return _resolve_path(topic).read_text() + + +def list_topics() -> dict[str, str]: + """Return a dict of topic name -> first line (title) of each doc.""" + result = {} + for topic in sorted(TOPICS.keys()): + try: + path = _resolve_path(topic) + first_line = path.read_text().split("\n", 1)[0].lstrip("# ").strip() + result[topic] = first_line + except FileNotFoundError: + result[topic] = "(not found — run `axolotl fetch docs`)" + return result diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index c0ac320506..cca6481e6e 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -294,7 +294,9 @@ def merge_lora(config: str, **kwargs): @cli.command() -@click.argument("directory", type=click.Choice(["examples", "deepspeed_configs"])) +@click.argument( + "directory", type=click.Choice(["examples", "deepspeed_configs", "docs"]) +) @click.option("--dest", help="Destination directory") def fetch(directory: str, dest: Optional[str]): """ @@ -303,9 +305,10 @@ def fetch(directory: str, dest: Optional[str]): Available directories: - examples: Example configuration files - deepspeed_configs: DeepSpeed configuration files + - docs: Full documentation (Quarto markdown files) Args: - directory: One of `examples`, `deepspeed_configs`. + directory: One of `examples`, `deepspeed_configs`, `docs`. dest: Optional destination directory. """ fetch_from_github(f"{directory}/", dest) @@ -340,6 +343,112 @@ def delinearize_llama4(model: str, output: str): do_delinearize_llama4(model, output) +@cli.command("agent-docs") +@click.argument("topic", required=False, default=None) +@click.option("--list", "list_topics", is_flag=True, help="List available topics") +def agent_docs(topic: Optional[str], list_topics: bool): + """Show agent-optimized documentation. + + Prints reference docs designed for AI coding agents. + These docs are bundled with the package — no network access needed. + + \b + Examples: + axolotl agent-docs # overview (start here) + axolotl agent-docs grpo # GRPO reference + axolotl agent-docs sft # SFT reference + axolotl agent-docs --list # list all topics + """ + from axolotl.cli.agent_docs import get_doc, list_topics as _list_topics + + if list_topics: + for name, title in _list_topics().items(): + click.echo(f" {name:25s} {title}") + return + + if topic is None: + topic = "overview" + + try: + click.echo(get_doc(topic)) + except FileNotFoundError as exc: + raise click.BadParameter(str(exc)) from exc + + +@cli.command("config-schema") +@click.option( + "--format", + "output_format", + type=click.Choice(["json", "yaml"]), + default="json", + help="Output format (default: json)", +) +@click.option("--field", help="Show schema for a specific field only") +def config_schema(output_format: str, field: Optional[str]): + """Dump the full config JSON schema. + + Useful for AI agents and tooling to discover all available config options, + their types, defaults, and descriptions. + + \b + Examples: + axolotl config-schema # full JSON schema + axolotl config-schema --format yaml # YAML format + axolotl config-schema --field adapter # single field + """ + import json + + try: + schema = AxolotlInputConfig.model_json_schema() + except (TypeError, ValueError, AttributeError) as exc: + # Fallback: dump field names, types, and defaults when full schema + # generation fails (e.g. torch.dtype not JSON-serializable) + LOG.warning( + "Full JSON schema generation failed, using simplified fallback: %s", exc + ) + fields = {} + for name, field_info in AxolotlInputConfig.model_fields.items(): + entry = {} + if field_info.description: + entry["description"] = field_info.description + if field_info.default is not None: + try: + json.dumps(field_info.default) + entry["default"] = field_info.default + except (TypeError, ValueError): + entry["default"] = str(field_info.default) + annotation = field_info.annotation + if annotation is not None: + entry["type"] = str(annotation) + fields[name] = entry + schema = { + "properties": fields, + "_note": "simplified schema (full generation failed)", + } + + if field: + props = schema.get("properties", {}) + if field not in props: + # Try case-insensitive match + matches = [k for k in props if k.lower() == field.lower()] + if matches: + field = matches[0] + else: + raise click.BadParameter( + f"Unknown field: {field!r}. " + f"Omit --field to dump the full schema, " + f"or pipe to jq: axolotl config-schema | jq '.properties | keys'" + ) + schema = {field: props[field]} + + if output_format == "yaml": + import yaml # pylint: disable=import-outside-toplevel + + click.echo(yaml.dump(schema, default_flow_style=False, sort_keys=False)) + else: + click.echo(json.dumps(schema, indent=2)) + + cli.add_command(lm_eval) From dc638e723fe3d528a27731c65bd621a884a94c1a Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 7 Apr 2026 00:10:25 +0700 Subject: [PATCH 1254/1405] fix(config): add cce and liger to nemotron-h example (#3573) [skip ci] --- examples/nemotron-h/120b-a12b-qlora.yaml | 24 ++++++++++++++------- examples/nemotron-h/nano-30b-a3b-qlora.yaml | 23 ++++++++++++++------ 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/examples/nemotron-h/120b-a12b-qlora.yaml b/examples/nemotron-h/120b-a12b-qlora.yaml index 67dcdb96e0..03e6d3b5ee 100644 --- a/examples/nemotron-h/120b-a12b-qlora.yaml +++ b/examples/nemotron-h/120b-a12b-qlora.yaml @@ -1,5 +1,15 @@ base_model: nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16 +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.liger.LigerPlugin + +liger_layer_norm: true +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_rms_norm_gated: true + # LoRA kernel patches are incompatible with this architecture — see README. lora_mlp_kernel: false lora_qkv_kernel: false @@ -22,8 +32,6 @@ dataset_prepared_path: last_run_prepared sequence_len: 4096 sample_packing: true -use_cut_cross_entropy: true - load_in_4bit: true quantize_moe_experts: true adapter: qlora @@ -31,16 +39,16 @@ lora_r: 16 lora_alpha: 32 lora_dropout: 0.0 lora_target_modules: - # Attention projection layers (present in ~12 attention layers out of 88) - q_proj - k_proj - v_proj - o_proj - # To also train MoE expert weights, add them via lora_target_parameters - # (they are 3D nn.Parameter tensors, not nn.Linear — no gate_proj): - # lora_target_parameters: - # - up_proj - # - down_proj + +# To also train MoE expert weights, add them via lora_target_parameters +# (they are 3D nn.Parameter tensors, not nn.Linear — no gate_proj): +# lora_target_parameters: +# - up_proj +# - down_proj wandb_project: wandb_entity: diff --git a/examples/nemotron-h/nano-30b-a3b-qlora.yaml b/examples/nemotron-h/nano-30b-a3b-qlora.yaml index 2d7307f99a..3994ab08ed 100644 --- a/examples/nemotron-h/nano-30b-a3b-qlora.yaml +++ b/examples/nemotron-h/nano-30b-a3b-qlora.yaml @@ -1,6 +1,16 @@ # See examples/nemotron-h/README.md for architecture notes and requirements. base_model: nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.liger.LigerPlugin + +liger_layer_norm: true +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_rms_norm_gated: true + # LoRA kernel patches are incompatible with this architecture — see README. lora_mlp_kernel: false lora_qkv_kernel: false @@ -23,8 +33,6 @@ dataset_prepared_path: last_run_prepared sequence_len: 4096 sample_packing: true -use_cut_cross_entropy: true - load_in_4bit: true quantize_moe_experts: true adapter: qlora @@ -36,11 +44,12 @@ lora_target_modules: - k_proj - v_proj - o_proj - # To also train MoE expert weights, add them via lora_target_parameters - # (they are 3D nn.Parameter tensors, not nn.Linear — no gate_proj): - # lora_target_parameters: - # - up_proj - # - down_proj + +# To also train MoE expert weights, add them via lora_target_parameters +# (they are 3D nn.Parameter tensors, not nn.Linear — no gate_proj): +# lora_target_parameters: +# - up_proj +# - down_proj wandb_project: wandb_entity: From 149178ddb74666f2ed93f4e1ebd1c024fc5d586c Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 7 Apr 2026 00:10:52 +0700 Subject: [PATCH 1255/1405] chore: cleanup post release v0.16 (#3577) * fix: remove unneeded debug log * fix: cleanup * feat: add dense gemma config and cleanup * feat: add cce support * update notes and set torch compile * fix patch for new number of return vals * fixes for gemma4 * fix packing bug * use updated cce for mm * fix: pass in kv cache func when avail for transformers 5.5 * feat: update examples with flex variant and readme * gemma4 lora attention kernels --------- Co-authored-by: Wing Lian Co-authored-by: Wing Lian --- .../colab-axolotl-example.ipynb | 2 +- examples/gemma4/26b-a4b-moe-qlora.yaml | 33 +- examples/gemma4/31b-qlora-flex.yaml | 71 ++++ examples/gemma4/31b-qlora.yaml | 69 ++++ examples/gemma4/README.md | 60 ++++ scripts/cutcrossentropy_install.py | 2 +- src/axolotl/core/trainers/base.py | 37 +- .../integrations/cut_cross_entropy/README.md | 3 +- .../cut_cross_entropy/__init__.py | 2 +- src/axolotl/integrations/kernels/README.md | 4 - src/axolotl/integrations/kernels/args.py | 22 -- src/axolotl/kernels/lora.py | 333 ++++++++++++++++++ src/axolotl/loaders/adapter.py | 59 ++++ .../monkeypatch/attention/flash_attn_4.py | 7 + src/axolotl/monkeypatch/lora_kernels.py | 21 +- 15 files changed, 665 insertions(+), 60 deletions(-) create mode 100644 examples/gemma4/31b-qlora-flex.yaml create mode 100644 examples/gemma4/31b-qlora.yaml create mode 100644 examples/gemma4/README.md diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 7a9feaa030..c7b2b8e5bf 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -40,7 +40,7 @@ "%%capture\n", "# This step can take ~5-10 minutes to install dependencies\n", "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@63b15e6\"" + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fec1a88\"" ] }, { diff --git a/examples/gemma4/26b-a4b-moe-qlora.yaml b/examples/gemma4/26b-a4b-moe-qlora.yaml index 0972b93f68..e7bdb6f461 100644 --- a/examples/gemma4/26b-a4b-moe-qlora.yaml +++ b/examples/gemma4/26b-a4b-moe-qlora.yaml @@ -1,19 +1,12 @@ # Gemma 4 26B-A4B MoE QLoRA with ScatterMoE kernels # -# Validated: 50 steps on FineTome-100k, loss 7.4 -> 2.4, single RTX 5090 (32GB) +# Validated: 50 steps on FineTome-100k, loss 8.8 -> 1.8, single RTX 5090 (32GB) +# torch_compile=true: 21 GiB peak VRAM, ~230 tok/s, 336s total # # Key notes: -# - Flash Attention 2 is NOT supported (global_head_dim=512 > FA2 max of 256). -# Use sdp_attention instead. -# - Gemma 4 is multimodal (text+vision+audio). For text-only SFT, restrict -# LoRA to the text backbone via lora_target_linear_modules regex. -# - MoE experts use `experts_implementation: scattermoe` — Gemma 4 embeds MoE -# directly in the decoder layer (no SparseMoeBlock), so we register ScatterMoE -# via the transformers ExpertsInterface. -# - Expert LoRA targets are `experts.gate_up_proj` / `experts.down_proj` -# (no `mlp.` prefix, unlike Qwen/Mixtral). -# - micro_batch_size: 1 fits 2048 seq_len on 32GB GPU with SDP attention. -# Use micro_batch_size: 4 with 1024 seq_len, or on 48GB+ GPUs. +# - Max sequence length on 32GB GPU: 2048 (micro_batch_size=1, SDP attention). +# 4096 seq_len OOMs due to head_dim=512 math SDP materializing full score matrix. +# Use 48GB+ GPUs for longer sequences or multi-GPU with FSDP. base_model: google/gemma-4-26B-A4B @@ -24,7 +17,7 @@ plugins: use_kernels: true use_scattermoe: true experts_implementation: scattermoe -torch_compile: false +torch_compile: true liger_layer_norm: true liger_rope: true liger_rms_norm: true @@ -54,12 +47,9 @@ lora_r: 16 lora_alpha: 32 lora_dropout: 0 -# Restrict LoRA to text backbone only (skip vision/audio encoders). -# lora_target_modules is intentionally empty — all module targeting is done -# via regex in lora_target_linear_modules below. -lora_target_modules: [] -lora_target_linear_modules: - - language_model\.model\.layers\.\d+\.self_attn\.(q|k|v|o)_proj +# Restrict LoRA to text backbone only (skip vision/audio encoders) +# using regex to match only the text decoder attention projections. +lora_target_modules: 'model.language_model.layers.[\d]+.(_checkpoint_wrapped_module.)?(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' # MoE expert LoRA (3D Parameter tensors, not nn.Linear) lora_target_parameters: @@ -73,7 +63,7 @@ lora_o_kernel: false bnb_config_kwargs: bnb_4bit_use_double_quant: true -wandb_project: gemma4-qlora +wandb_project: wandb_entity: wandb_watch: wandb_name: @@ -93,8 +83,7 @@ gradient_checkpointing: true activation_offloading: true logging_steps: 1 -# FA2 not supported — Gemma4 global_head_dim=512 exceeds FA2 max of 256 -flash_attention: false +# FA2 not supported sdp_attention: true warmup_ratio: 0.1 diff --git a/examples/gemma4/31b-qlora-flex.yaml b/examples/gemma4/31b-qlora-flex.yaml new file mode 100644 index 0000000000..8456c9c135 --- /dev/null +++ b/examples/gemma4/31b-qlora-flex.yaml @@ -0,0 +1,71 @@ +base_model: google/gemma-4-31B + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.liger.LigerPlugin +torch_compile: true +liger_layer_norm: true +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_rms_norm_gated: true +strict: false + +chat_template: gemma4 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:10%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.05 +output_dir: ./outputs/gemma4-31b-qlora-flex + +sequence_len: 2048 +sample_packing: true + +load_in_4bit: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0 + +# Restrict LoRA to text backbone only (skip vision/audio encoders) +lora_target_modules: 'model.language_model.layers.[\d]+.(_checkpoint_wrapped_module.)?(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' + +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +bnb_config_kwargs: + bnb_4bit_use_double_quant: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +activation_offloading: true +logging_steps: 1 + +# FA not supported +flex_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/gemma4/31b-qlora.yaml b/examples/gemma4/31b-qlora.yaml new file mode 100644 index 0000000000..42086a43c6 --- /dev/null +++ b/examples/gemma4/31b-qlora.yaml @@ -0,0 +1,69 @@ +base_model: google/gemma-4-31B + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.liger.LigerPlugin +torch_compile: false +liger_layer_norm: true +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_rms_norm_gated: true +strict: false + +chat_template: gemma4 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:10%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.05 +output_dir: ./outputs/gemma4-31b-qlora + +sequence_len: 2048 +sample_packing: true + +load_in_4bit: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0 + +# Restrict LoRA to text backbone only (skip vision/audio encoders) +# using regex to match only the text decoder attention projections. +lora_target_modules: 'model.language_model.layers.[\d]+.(_checkpoint_wrapped_module.)?(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' + +bnb_config_kwargs: + bnb_4bit_use_double_quant: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +activation_offloading: true +logging_steps: 1 + +# FA not supported +sdp_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/gemma4/README.md b/examples/gemma4/README.md new file mode 100644 index 0000000000..68274ee68a --- /dev/null +++ b/examples/gemma4/README.md @@ -0,0 +1,60 @@ +# Finetune Google's Gemma 4 with Axolotl + +[Gemma 4](https://huggingface.co/collections/google/gemma-4) is a family of multimodal models from Google. This guide covers how to train them with Axolotl. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +3. Run the finetuning example: + +```bash +# 26B MoE QLoRA (1x80GB @ ~50 GiB) +axolotl train examples/gemma4/26b-a4b-moe-qlora.yaml + +# 31B Dense QLoRA (1x80GB @ ~44 GiB) +axolotl train examples/gemma4/31b-qlora.yaml + +# 31B Dense QLoRA Flex Attn (1x80GB @ ~26 GiB) +axolotl train examples/gemma4/31b-qlora-flex.yaml +``` + +### MoE Expert Quantization & Expert LoRA (26B-A4B only) + +The 26B-A4B config uses ScatterMoE kernels via the transformers `ExpertsInterface` and quantizes expert weights on load. To learn about expert quantization, expert LoRA targeting, and related limitations, see the [MoE Expert Quantization](https://docs.axolotl.ai/docs/expert_quantization.html) docs. + +## Flex Attention + +Reduce ~40% VRAM (at the cost of up to half throughput) by setting the below (shown in `examples/gemma4/31b-qlora-flex.yaml`): + +```yaml +torch_compile: true +flex_attention: true +``` + +This works for both the MoE and Dense model. + +## Limitations + +- **Flash Attention**: FA2 (max head_dim=256) and FA4 (max head_dim=128) cannot support Gemma 4's `global_head_dim=512`. Use SDP or flex attention instead. +- **LoRA kernels**: Not supported due to KV-sharing layers. +- **lora_target_linear**: Incompatible for multimodal models — use `lora_target_modules` with a regex to restrict LoRA to the text backbone. + +### TIPS + +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- You can run full finetuning by removing `adapter: qlora`, `load_in_4bit: true`, and `quantize_moe_experts: true` from the config. This is heavy and has not been tested. + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Related Resources + +- [Gemma 4 Blog](https://huggingface.co/blog/gemma4) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index bd92a3630e..5f716b779f 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@63b15e6"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fec1a88"' ) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 6beff80554..650a238ecf 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -100,6 +100,27 @@ def __init__( self._signature_columns = None # workaround for pylint super().__init__(*_args, **kwargs) + + # Gemma4 (and similar multimodal models) declare **kwargs in forward() for + # extra inputs like mm_token_type_ids. HF Trainer interprets VAR_KEYWORD as + # "the model handles num_items_in_batch internally" and skips the loss ÷ + # gradient_accumulation_steps normalisation, which inflates the *logged* loss + # (the gradient itself is still correct). Override to False when the model + # doesn't actually consume num_items_in_batch. + if self.model_accepts_loss_kwargs: + model_to_check = self.accelerator.unwrap_model(self.model) + if hasattr(model_to_check, "base_model"): # PEFT wrapper + model_to_check = model_to_check.base_model + if hasattr(model_to_check, "model"): + model_to_check = model_to_check.model + fwd = getattr(model_to_check, "forward", None) + if fwd is not None: + import inspect + + params = inspect.signature(fwd).parameters + if "num_items_in_batch" not in params: + self.model_accepts_loss_kwargs = False + self.train_data_collator = self.data_collator self._stored_metrics = defaultdict( lambda: defaultdict(lambda: {"values": [], "reduction": "mean"}) @@ -383,13 +404,27 @@ def compute_loss( # Gemma4 requires mm_token_type_ids during training (even for text-only). # Inject zeros (= text token type) when not provided by the data collator. + _model_type = getattr(getattr(model, "config", None), "model_type", None) if ( "mm_token_type_ids" not in inputs and "input_ids" in inputs - and getattr(getattr(model, "config", None), "model_type", None) == "gemma4" + and _model_type == "gemma4" ): inputs["mm_token_type_ids"] = torch.zeros_like(inputs["input_ids"]) + # Gemma4 (and Gemma3): transformers' masking_utils detects packed sequences + # from position_ids, but only when attention_mask is None. When sample + # packing is active the collator provides an all-ones attention_mask that + # prevents this detection — remove it so the model builds the correct + # per-sequence causal masks. + if ( + self.args.sample_packing + and _model_type in ("gemma4", "gemma3") + and "attention_mask" in inputs + and "position_ids" in inputs + ): + del inputs["attention_mask"] + if self.args.orpo_alpha: return self.orpo_compute_loss( model, diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 220fb4d2bc..2ccf11f18b 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@63b15e6" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fec1a88" ``` ## Usage @@ -44,6 +44,7 @@ plugins: - gemma3_text - gemma3n - gemma3n_text +- gemma4 - glm - glm4 - glm4_moe diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 758c5406ca..b9a59aea9f 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@63b15e6"`' + '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fec1a88"`' ) diff --git a/src/axolotl/integrations/kernels/README.md b/src/axolotl/integrations/kernels/README.md index 9293c1727b..32d236da49 100644 --- a/src/axolotl/integrations/kernels/README.md +++ b/src/axolotl/integrations/kernels/README.md @@ -146,10 +146,6 @@ Gemma 4 (e.g. `google/gemma-4-26B-A4B`) has a unique hybrid MoE architecture: Because there is no SparseMoeBlock class to patch, Gemma 4 uses a different integration path: we register `"scattermoe"` as a custom implementation in the transformers `ExpertsInterface`, and set `experts_implementation: scattermoe` in the config. The `@use_experts_implementation` decorator on `Gemma4TextExperts` then dispatches to our ScatterMoE kernel automatically. The router is untouched — it runs as-is. -**Important limitations:** -- **Flash Attention 2 is not supported** — Gemma 4 uses `global_head_dim: 512` for full attention layers, which exceeds FA2's maximum head dimension of 256. Use `sdp_attention: true` instead. -- **Multimodal model**: Gemma 4 includes vision and audio encoders. For text-only SFT, use `lora_target_linear_modules` with a regex to restrict LoRA to the text backbone (e.g. `language_model\.model\.layers\.\d+\.self_attn\.(q|k|v|o)_proj`). - ## Limitations - **ScatterMoE + GLM4-MoE Lite**: ScatterMoE does not work reliably for GLM 4.7 Flash (`glm4_moe_lite`). diff --git a/src/axolotl/integrations/kernels/args.py b/src/axolotl/integrations/kernels/args.py index 3afeb79c31..f532fde417 100644 --- a/src/axolotl/integrations/kernels/args.py +++ b/src/axolotl/integrations/kernels/args.py @@ -53,28 +53,6 @@ def check_experts_implementation(cls, data): return data - @model_validator(mode="before") - @classmethod - def warn_sonicmoe_lora_overhead(cls, data): - if data.get("use_sonicmoe") is True and data.get("adapter") in ( - "lora", - "qlora", - ): - lora_target = data.get("lora_target_modules") or [] - lora_linear = data.get("lora_target_linear_modules") or [] - targets = ( - lora_target if isinstance(lora_target, list) else [lora_target] - ) + (lora_linear if isinstance(lora_linear, list) else [lora_linear]) - expert_keywords = ("gate_up_proj", "down_proj", "experts") - if any(kw in t for t in targets for kw in expert_keywords): - LOG.info( - "SonicMoE + LoRA on expert modules uses runtime weight materialization " - "(W_eff = W + scaling*B@A per forward). This has slightly higher overhead " - "than ScatterMoE's fused Triton LoRA kernels but works with any CUTLASS kernel." - ) - - return data - @model_validator(mode="before") @classmethod def disable_mlp_kernel(cls, data): diff --git a/src/axolotl/kernels/lora.py b/src/axolotl/kernels/lora.py index 1576a10cda..79ac993c14 100644 --- a/src/axolotl/kernels/lora.py +++ b/src/axolotl/kernels/lora.py @@ -1297,6 +1297,339 @@ def apply_lora_qkv( return Q, K, V +class LoRA_QK(torch.autograd.Function): + """Optimized LoRA QK implementation for models where v_proj is None. + + Used by models like Gemma4 with attention_k_eq_v=True, where key states are + reused as value states. Only Q and K projections are fused; the caller + returns K a second time as V so that autograd accumulates key+value gradients + into a single dK. + + Supports bias, dropout, and DoRA (Weight-Decomposed Low-Rank Adaptation). + """ + + @staticmethod + @torch_amp_custom_fwd + def forward( + ctx: torch.autograd.function.FunctionCtx, + X: torch.Tensor, + X_drop: torch.Tensor | None, + # Q params + q_weight: torch.Tensor, + q_bias: torch.Tensor | None, + q_quant: QuantState | None, + q_A: torch.Tensor | None, + q_B: torch.Tensor | None, + q_scale: float, + q_lora_bias: torch.Tensor | None, + q_magnitude: torch.Tensor | None, + # K params + k_weight: torch.Tensor, + k_bias: torch.Tensor | None, + k_quant: QuantState | None, + k_A: torch.Tensor | None, + k_B: torch.Tensor | None, + k_scale: float, + k_lora_bias: torch.Tensor | None, + k_magnitude: torch.Tensor | None, + # Flags + inplace: bool = True, + ) -> tuple[torch.Tensor, torch.Tensor]: + has_dropout = X_drop is not None + has_dora = q_magnitude is not None + + if has_dora: + dtype = X.dtype + X_lora = X_drop if has_dropout else X + + # Compute Q with DoRA + Q_base = matmul_lora(X, q_weight, None, q_quant, None, None, None) + Q_lora = _lora_only(X_lora, q_A, q_B, q_scale, q_lora_bias, dtype) + q_mag_scale = _compute_dora_scale( + q_weight, q_quant, q_A, q_B, q_scale, q_magnitude, dtype + ) + Q = q_mag_scale.unsqueeze(0) * (Q_base + Q_lora) + if q_bias is not None: + Q = Q + q_bias + + # Compute K with DoRA + K_base = matmul_lora(X, k_weight, None, k_quant, None, None, None) + K_lora = _lora_only(X_lora, k_A, k_B, k_scale, k_lora_bias, dtype) + k_mag_scale = _compute_dora_scale( + k_weight, k_quant, k_A, k_B, k_scale, k_magnitude, dtype + ) + K = k_mag_scale.unsqueeze(0) * (K_base + K_lora) + if k_bias is not None: + K = K + k_bias + + Q_combined = Q_base + Q_lora + K_combined = K_base + K_lora + + ctx.save_for_backward( + X, + X_drop if has_dropout else X, + q_A.to(dtype) if q_A is not None else q_A, + q_B.to(dtype) if q_B is not None else q_B, + k_A.to(dtype) if k_A is not None else k_A, + k_B.to(dtype) if k_B is not None else k_B, + q_magnitude, + k_magnitude, + q_mag_scale, + k_mag_scale, + Q_combined, + K_combined, + q_lora_bias, + k_lora_bias, + ) + else: + # Standard LoRA (with optional dropout and bias) + Q = matmul_lora( + X, + q_weight, + q_bias, + q_quant, + q_A, + q_B, + q_scale, + X_drop=X_drop, + lora_bias=q_lora_bias, + ) + K = matmul_lora( + X, + k_weight, + k_bias, + k_quant, + k_A, + k_B, + k_scale, + X_drop=X_drop, + lora_bias=k_lora_bias, + ) + + dtype = X.dtype + ctx.save_for_backward( + X, + X_drop if has_dropout else X, + q_A.to(dtype) if q_A is not None else q_A, + q_B.to(dtype) if q_B is not None else q_B, + k_A.to(dtype) if k_A is not None else k_A, + k_B.to(dtype) if k_B is not None else k_B, + q_lora_bias, + k_lora_bias, + ) + + ctx.scales = (q_scale, k_scale) + ctx.quants = (q_quant, k_quant) + ctx.weights = (q_weight, k_weight) + ctx.inplace = inplace + ctx.has_dropout = has_dropout + ctx.has_dora = has_dora + + return Q, K + + @staticmethod + @torch_amp_custom_bwd + def backward( + ctx: torch.autograd.function.FunctionCtx, + q_grad: torch.Tensor, + k_grad: torch.Tensor, + ): + q_weight, k_weight = ctx.weights + q_quant, k_quant = ctx.quants + q_scale, k_scale = ctx.scales + has_dropout = ctx.has_dropout + has_dora = ctx.has_dora + + if has_dora: + ( + X, + X_lora, + A_q, + B_q, + A_k, + B_k, + q_magnitude, + k_magnitude, + q_mag_scale, + k_mag_scale, + Q_combined, + K_combined, + q_lora_bias, + k_lora_bias, + ) = ctx.saved_tensors + else: + ( + X, + X_lora, + A_q, + B_q, + A_k, + B_k, + q_lora_bias, + k_lora_bias, + ) = ctx.saved_tensors + q_magnitude = k_magnitude = None + q_mag_scale = k_mag_scale = None + Q_combined = K_combined = None + + batch, seq_len = X.shape[:2] + q_grad = q_grad.view(-1, q_grad.shape[-1]) + k_grad = k_grad.reshape(-1, k_grad.shape[-1]) + X = X.view(-1, X.shape[-1]) + X_lora = X_lora.view(-1, X_lora.shape[-1]) + + d_q_mag = d_k_mag = None + d_q_lora_bias = d_k_lora_bias = None + + if has_dora: + Q_combined = Q_combined.view(-1, Q_combined.shape[-1]) + K_combined = K_combined.view(-1, K_combined.shape[-1]) + + d_q_mag = (q_grad * Q_combined).sum(dim=0) * q_mag_scale / q_magnitude + d_k_mag = (k_grad * K_combined).sum(dim=0) * k_mag_scale / k_magnitude + + q_grad = q_grad * q_mag_scale.unsqueeze(0) + k_grad = k_grad * k_mag_scale.unsqueeze(0) + + # LoRA bias gradients + if q_lora_bias is not None: + d_q_lora_bias = q_scale * q_grad.sum(dim=0) + if k_lora_bias is not None: + d_k_lora_bias = k_scale * k_grad.sum(dim=0) + + X_lora_t = X_lora.t() + + d_A_q = d_B_q = d_A_k = d_B_k = None + grad_B_q = grad_B_k = None + + if A_q is not None and B_q is not None: + grad_B_q = q_grad @ B_q + d_A_q = torch.empty_like(A_q.t()) + d_B_q = torch.empty_like(B_q.t()) + d_A_q.addmm_(X_lora_t, grad_B_q, alpha=q_scale, beta=0) + d_B_q.addmm_(A_q @ X_lora_t, q_grad, alpha=q_scale, beta=0) + + if A_k is not None and B_k is not None: + grad_B_k = k_grad @ B_k + d_A_k = torch.empty_like(A_k.t()) + d_B_k = torch.empty_like(B_k.t()) + d_A_k.addmm_(X_lora_t, grad_B_k, alpha=k_scale, beta=0) + d_B_k.addmm_(A_k @ X_lora_t, k_grad, alpha=k_scale, beta=0) + + # Base path input gradient + out_buffer = X if ctx.inplace else None + + q_weight_t = dequantize(q_weight, q_quant) + grad_X = torch.mm(q_grad, q_weight_t, out=out_buffer) + del q_weight_t + + k_weight_t = dequantize(k_weight, k_quant) + grad_X.addmm_(k_grad, k_weight_t) + del k_weight_t + + # LoRA path input gradient + if has_dropout: + grad_X_drop = torch.zeros_like(X_lora) + if grad_B_q is not None: + grad_X_drop.addmm_(grad_B_q, A_q, alpha=q_scale) + if grad_B_k is not None: + grad_X_drop.addmm_(grad_B_k, A_k, alpha=k_scale) + else: + grad_X_drop = None + if grad_B_q is not None: + grad_X.addmm_(grad_B_q, A_q, alpha=q_scale) + if grad_B_k is not None: + grad_X.addmm_(grad_B_k, A_k, alpha=k_scale) + + if d_A_q is not None: + d_A_q = d_A_q.t() + d_B_q = d_B_q.t() # type: ignore[union-attr] + if d_A_k is not None: + d_A_k = d_A_k.t() + d_B_k = d_B_k.t() # type: ignore[union-attr] + + grad_X = grad_X.view(batch, seq_len, -1) + if grad_X_drop is not None: + grad_X_drop = grad_X_drop.view(batch, seq_len, -1) + + # Return gradients for all forward inputs: + # X, X_drop, + # q: weight, bias, quant, A, B, scale, lora_bias, magnitude + # k: weight, bias, quant, A, B, scale, lora_bias, magnitude + # inplace + return ( + grad_X, + grad_X_drop, + # Q + None, + None, + None, + d_A_q, + d_B_q, + None, + d_q_lora_bias, + d_q_mag, + # K + None, + None, + None, + d_A_k, + d_B_k, + None, + d_k_lora_bias, + d_k_mag, + # inplace + None, + ) + + +def apply_lora_qk( + self, X: torch.Tensor, inplace: bool = True +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Applies LoRA to compute Query and Key projections for models where v_proj is None. + + When v_proj is None (e.g. Gemma4 attention_k_eq_v), key states are reused as + value states. Returns (Q, K, K) — the caller's patched forward will use K as V. + Because K is returned twice, autograd accumulates gradients from both the key and + value paths into dK before calling LoRA_QK.backward. + + Supports bias, dropout, and DoRA. + """ + QW, Qb, QW_quant, QA, QB, QS, Qlb, Qdrop, Qmag = get_lora_parameters(self.q_proj) + KW, Kb, KW_quant, KA, KB, KS, Klb, Kdrop, Kmag = get_lora_parameters(self.k_proj) + + # Apply dropout outside autograd.Function (shared mask for Q, K) + X_drop = _apply_dropout(Qdrop, X, self.training) + + Q, K = LoRA_QK.apply( + X, + X_drop, + # Q + QW, + Qb, + QW_quant, + QA, + QB, + QS, + Qlb, + Qmag, + # K + KW, + Kb, + KW_quant, + KA, + KB, + KS, + Klb, + Kmag, + # Flags + inplace, + ) + + return Q, K, K + + class LoRA_O(torch.autograd.Function): """Optimized LoRA implementation for output projection. diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index 2b53b7b2c5..6d0bd0fe19 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -67,12 +67,70 @@ def find_all_linear_names(model): return list(lora_module_names) +def _patch_peft_clippable_linear(): + """Patch PEFT to handle Gemma4ClippableLinear which wraps nn.Linear. + + Gemma4's vision tower uses ClippableLinear (a thin wrapper around nn.Linear + that clips activations). PEFT doesn't recognise it as a supported layer type, + so we redirect LoRA injection to the inner ``.linear`` child instead. + """ + try: + from transformers.models.gemma4.modeling_gemma4 import ( + Gemma4ClippableLinear as _cls, + ) + except ImportError: + return + + from peft.tuners.lora.model import LoraModel + + if getattr(LoraModel, "_axolotl_clippable_patched", False): + return + _orig = LoraModel._create_and_replace + + def _patched( + self, + peft_config, + adapter_name, + target, + target_name, + parent, + current_key=None, + **kw, + ): + if isinstance(target, _cls): + # Redirect to the inner nn.Linear so PEFT can wrap it normally. + return _orig( + self, + peft_config, + adapter_name, + target.linear, + "linear", + target, + current_key=current_key, + **kw, + ) + return _orig( + self, + peft_config, + adapter_name, + target, + target_name, + parent, + current_key=current_key, + **kw, + ) + + LoraModel._create_and_replace = _patched + LoraModel._axolotl_clippable_patched = True + + def load_lora( model: PreTrainedModel, cfg: DictDefault, inference: bool = False, config_only: bool = False, ) -> tuple[PreTrainedModel | PeftModel | PeftMixedModel | None, PeftConfig | None]: + _patch_peft_clippable_linear() lora_target_modules = cfg.lora_target_modules or [] lora_target_parameters = cfg.lora_target_parameters or [] @@ -124,6 +182,7 @@ def load_lora( lora_dropout=cfg.lora_dropout, fan_in_fan_out=cfg.lora_fan_in_fan_out, modules_to_save=cfg.lora_modules_to_save if cfg.lora_modules_to_save else None, + exclude_modules=getattr(cfg, "lora_exclude_modules", None) or None, bias="none", task_type=task_type, **lora_config_kwargs, diff --git a/src/axolotl/monkeypatch/attention/flash_attn_4.py b/src/axolotl/monkeypatch/attention/flash_attn_4.py index 5ebc93670c..b3bde00c15 100644 --- a/src/axolotl/monkeypatch/attention/flash_attn_4.py +++ b/src/axolotl/monkeypatch/attention/flash_attn_4.py @@ -86,12 +86,19 @@ def patch_flash_attn_4(model_config=None): if getattr(fa_utils._lazy_imports, "_axolotl_patched", False): return + try: + # flash-attn-4>=4.0.0b7 + from flash_attn.cute import flash_attn_with_kvcache + except ImportError: + flash_attn_with_kvcache = None + def _patched_lazy_imports( implementation, attention_wrapper=None, allow_all_kernels=False ): return ( flash_attn_func, flash_attn_varlen_func, + flash_attn_with_kvcache, fa_utils._pad_input, fa_utils._unpad_input, ) diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index d569d5925a..9cf65286aa 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -16,6 +16,7 @@ apply_lora_mlp_geglu, apply_lora_mlp_swiglu, apply_lora_o, + apply_lora_qk, apply_lora_qkv, ) from axolotl.monkeypatch.utils import detab_code @@ -483,18 +484,24 @@ def apply_lora_kernel_patches( if cfg.lora_qkv_kernel: # Query, key, value patching # Filter out None projections (e.g. Gemma4 v_proj when attention_k_eq_v=True) - proj_names = ["q_proj", "k_proj", "v_proj"] - layer_modules = [ - getattr(self_attn, name) - for name in proj_names - if getattr(self_attn, name, None) is not None - ] + has_v_proj = getattr(self_attn, "v_proj", None) is not None + proj_names = ( + ["q_proj", "k_proj", "v_proj"] + if has_v_proj + else ["q_proj", "k_proj"] + ) + layer_modules = [getattr(self_attn, name) for name in proj_names] can_patch_qkv = all( hasattr(module, "lora_A") for module in layer_modules ) if can_patch_qkv: - self_attn.apply_qkv = types.MethodType(apply_lora_qkv, self_attn) + if has_v_proj: + self_attn.apply_qkv = types.MethodType( + apply_lora_qkv, self_attn + ) + else: + self_attn.apply_qkv = types.MethodType(apply_lora_qk, self_attn) else: LOG.warning_once( "Cannot patch some attention QKV projections - requires LoRA adapters" From 7c56809c7fadc0f591c16f7dc18475987cf10387 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 7 Apr 2026 08:09:49 -0700 Subject: [PATCH 1256/1405] use vllm 0.19.0 for torch 2.10.0 (#3582) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d4e1894f79..99879597b8 100644 --- a/setup.py +++ b/setup.py @@ -89,7 +89,7 @@ def parse_requirements(extras_require_map): ] if not install_xformers: _install_requires.pop(_install_requires.index(xformers_version)) - extras_require_map["vllm"] = ["vllm>=0.17.1"] + extras_require_map["vllm"] = ["vllm>=0.19.0"] elif (major, minor) >= (2, 9): extras_require_map.pop("fbgemm-gpu") extras_require_map["fbgemm-gpu"] = [ From 7daf7d96f13807c29123e5448969005642c7bfc5 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 8 Apr 2026 22:18:11 +0700 Subject: [PATCH 1257/1405] fix: regex for unfrozen language tower (#3586) [skip ci] * fix: regex for unfrozen language tower * fix: other leftover regex --- examples/gemma3/gemma-3-1b-qlora.yml | 4 ++-- examples/gemma3/gemma-3-270m-qlora.yml | 4 ++-- examples/gemma3/gemma-3-4b-qlora.yml | 4 ++-- examples/qwen3.5/27b-fft.yaml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/gemma3/gemma-3-1b-qlora.yml b/examples/gemma3/gemma-3-1b-qlora.yml index f6fc6955c7..4bcbf09f4a 100644 --- a/examples/gemma3/gemma-3-1b-qlora.yml +++ b/examples/gemma3/gemma-3-1b-qlora.yml @@ -26,8 +26,8 @@ output_dir: ./outputs/out # Freeze vision tower unfrozen_parameters: - - ^model\.language_model\..* - - ^lm_head\..* + - ^model.language_model.* + - ^lm_head.* adapter: qlora lora_r: 32 diff --git a/examples/gemma3/gemma-3-270m-qlora.yml b/examples/gemma3/gemma-3-270m-qlora.yml index 99202f29f9..1f247ab059 100644 --- a/examples/gemma3/gemma-3-270m-qlora.yml +++ b/examples/gemma3/gemma-3-270m-qlora.yml @@ -26,8 +26,8 @@ output_dir: ./outputs/out # Freeze vision tower unfrozen_parameters: - - ^model\.language_model\..* - - ^lm_head\..* + - ^model.language_model.* + - ^lm_head.* adapter: qlora lora_r: 32 diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml index d11f2ea506..5d939da191 100644 --- a/examples/gemma3/gemma-3-4b-qlora.yml +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -22,8 +22,8 @@ output_dir: ./outputs/out # Freeze vision tower unfrozen_parameters: - - ^model\.language_model\..* - - ^lm_head\..* + - ^model.language_model.* + - ^lm_head.* adapter: qlora lora_model_dir: diff --git a/examples/qwen3.5/27b-fft.yaml b/examples/qwen3.5/27b-fft.yaml index 6e61bc6959..9f875ec26c 100644 --- a/examples/qwen3.5/27b-fft.yaml +++ b/examples/qwen3.5/27b-fft.yaml @@ -26,8 +26,8 @@ sample_packing: true # Freeze vision encoder unfrozen_parameters: - - model\.language_model\..* - - lm_head\..* + - model.language_model.* + - lm_head.* wandb_project: wandb_entity: From 4ef608dda31e84f6e0f9ffbb26ca7157cf0daccf Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 9 Apr 2026 20:02:36 -0700 Subject: [PATCH 1258/1405] fix ddp/fsdp w gemma4 (#3584) * fix ddp/fsdp w gemma4 * address pr comments * activation offloading fix and update agent docs for gemma4 --- AGENTS.md | 2 + docs/agents/model_architectures.md | 110 +++++++++++++++ docs/agents/new_model_support.md | 181 +++++++++++++++++++++++++ src/axolotl/cli/agent_docs/__init__.py | 2 + src/axolotl/core/trainers/base.py | 19 ++- src/axolotl/train.py | 6 +- src/axolotl/utils/config/__init__.py | 31 +++++ src/axolotl/utils/freeze.py | 38 ++++++ src/axolotl/utils/schemas/config.py | 11 ++ 9 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 docs/agents/model_architectures.md create mode 100644 docs/agents/new_model_support.md diff --git a/AGENTS.md b/AGENTS.md index 6fb81e506e..e9b747ce39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,8 @@ Agent-specific references: - [docs/agents/grpo.md](docs/agents/grpo.md) — GRPO online RL with reward functions - [docs/agents/reward_modelling.md](docs/agents/reward_modelling.md) — outcome and process reward models - [docs/agents/pretraining.md](docs/agents/pretraining.md) — continual pretraining +- [docs/agents/model_architectures.md](docs/agents/model_architectures.md) — model-specific quirks (Gemma4, Qwen3.5 MoE, etc.) +- [docs/agents/new_model_support.md](docs/agents/new_model_support.md) — debugging and adding support for new model architectures ## Config Pattern diff --git a/docs/agents/model_architectures.md b/docs/agents/model_architectures.md new file mode 100644 index 0000000000..426db4ce9f --- /dev/null +++ b/docs/agents/model_architectures.md @@ -0,0 +1,110 @@ +# Model Architectures — Agent Reference + +Model-specific quirks, required settings, and known issues. Check this before debugging training failures on specific model families. + +## Gemma 4 + +**Models**: `google/gemma-4-26B-A4B` (MoE), `google/gemma-4-31B` (dense), `google/gemma-4-E2B`, `google/gemma-4-E4B` + +**Architecture**: Multimodal wrapper (`Gemma4ForConditionalGeneration`) over a text backbone (`Gemma4TextModel`), with optional vision/audio encoders. All Gemma4 HF repos have `model_type: "gemma4"` — even text-only variants load as multimodal with a vision tower. + +### Required settings + +```yaml +# Always needed for Gemma4: +freeze_mm_modules: true # Freeze vision/audio encoders for text-only training +gradient_checkpointing_kwargs: + use_reentrant: false # Shared per-layer norms cause "marked ready twice" with reentrant + +# LoRA target — restrict to language model only (DO NOT use lora_target_linear: true): +lora_target_modules: 'model.language_model.layers.[\d]+.(_checkpoint_wrapped_module.)?(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' +``` + +### Auto-detection + +Axolotl auto-detects Gemma4 and applies: +- `use_reentrant: false` for gradient checkpointing +- `ddp_find_unused_parameters: true` for DDP (skipped when `activation_offloading: true`) + +### Multi-GPU + +| Strategy | Works? | Notes | +|----------|--------|-------| +| DDP | Yes | Auto-sets `ddp_find_unused_parameters=True` | +| DDP + activation_offloading | Yes | `find_unused_parameters` is skipped (conflicts with checkpoint wrappers) | +| FSDP1 | No | OOM during dequantization/sharding with QLoRA | +| FSDP2 | Yes | Use `Gemma4TextDecoderLayer` (not `Gemma4DecoderLayer`) as wrap class | +| FSDP2 + activation_offloading | Yes | Lowest VRAM (~26 GiB/GPU for 26B-A4B) | + +FSDP2 config: +```yaml +fsdp: + - full_shard + - auto_wrap +fsdp_config: + fsdp_version: 2 + fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP + fsdp_transformer_layer_cls_to_wrap: Gemma4TextDecoderLayer +``` + +### MoE (26B-A4B) + +- `enable_moe_block: true`, 256 experts, top-k routing +- No separate `SparseMoeBlock` — MoE is embedded in each decoder layer +- Expert LoRA targets 3D parameter tensors: + ```yaml + lora_target_parameters: + - experts.gate_up_proj + - experts.down_proj + ``` +- ScatterMoE kernel acceleration: + ```yaml + plugins: + - axolotl.integrations.kernels.KernelsPlugin + use_kernels: true + use_scattermoe: true + experts_implementation: scattermoe + ``` + +### Common issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `mm_token_type_ids is required` in DDP | `model.config` not accessible through DDP wrapper | Already fixed — `unwrap_model()` in `compute_loss` and `prediction_step` | +| `marked a variable ready twice` in DDP | `ddp_find_unused_parameters=True` + activation_offloading checkpoint wrappers | Auto-handled — `find_unused_parameters` is skipped when `activation_offloading: true` | +| Loss ~12 instead of ~0.5 | Using `lora_target_linear: true` (applies LoRA to vision/audio modules) | Use the regex `lora_target_modules` pattern instead | +| FSDP2 `Could not find Gemma4AudioLayer` | Auto-wrap detects `_no_split_modules` including audio layers that don't exist | Explicitly set `fsdp_transformer_layer_cls_to_wrap: Gemma4TextDecoderLayer` | +| `Gemma4ClippableLinear not supported` by PEFT | Vision tower uses a non-standard linear wrapper | Axolotl patches this automatically via `_patch_peft_clippable_linear()` | + +### E2B/E4B dense models + +These have `hidden_size_per_layer_input: 256` (per-layer input embeddings) and `attention_k_eq_v: False`. Known issue: loss starts higher than expected (~12 vs ~0.5 for 26B). Root cause under investigation — may be related to the per-layer input mechanism or the `Gemma4ForConditionalGeneration` loss computation. + +## Gemma 3 + +**Models**: `google/gemma-3-*` + +- `ddp_find_unused_parameters: true` needed (multimodal unused params) +- `use_reentrant: false` recommended +- Attention mask must be dropped for sample packing (handled automatically) +- Multi-GPU test currently skipped (`tests/e2e/multigpu/test_gemma3.py`) + +## Qwen 3.5 MoE + +**Models**: `Qwen/Qwen3.5-35B-A3B` + +- Hybrid architecture: DeltaNet linear attention (30 layers) + full attention (10 layers) +- 256 experts, 8 active per token +- Known weight scale drift in late DeltaNet layers (36-38) due to AdamW + rare expert interaction +- Fix: `normalize_weight_scales` config to detect and rescale outliers: + ```yaml + normalize_weight_scales: + - name_pattern: 'linear_attn\.conv1d\.weight' + threshold: 1.3 + ``` + +## General MoE Notes + +- `lora_target_linear: true` with multimodal MoE models will apply LoRA to ALL linear modules including vision/audio encoders — use regex `lora_target_modules` to restrict to language model only +- Rare experts get larger effective learning rate from AdamW (small second-moment estimates) — can cause weight drift in recurrent/SSM components. Use `normalize_weight_scales` with `dry_run: true` to detect. +- For ScatterMoE kernel support, set `experts_implementation: scattermoe` and add the KernelsPlugin diff --git a/docs/agents/new_model_support.md b/docs/agents/new_model_support.md new file mode 100644 index 0000000000..8e60288966 --- /dev/null +++ b/docs/agents/new_model_support.md @@ -0,0 +1,181 @@ +# New Model Support — Agent Reference + +Guide for debugging and adding support for new model architectures in axolotl. Based on lessons learned from Gemma4, Gemma3, Qwen2-VL, and other multimodal/MoE models. + +## Quick Validation Checklist + +When testing a new model, run through these checks in order: + +1. **Does the model load?** `axolotl preprocess config.yaml` — catches config schema errors +2. **Does LoRA apply?** Check for "Unsupported layer type" warnings from PEFT +3. **Is the initial loss sane?** First-step loss for a pretrained model should be 0.5–2.0 for SFT +4. **Does sample packing work?** Compare loss with `sample_packing: true` vs `false` — should be similar +5. **Is CCE active?** Check for "Applying Cut Cross Entropy" log and verify peak VRAM is lower + +## Loss Debugging + +### Expected initial loss +A pretrained model doing SFT should start with loss roughly in the 0.5–2.0 range. If loss starts above 3.0, something is wrong. If it's near `log(vocab_size)` (≈ 12 for 262K vocab), the model is predicting at random — attention masking or model weights are broken. + +### Direct comparison technique +The fastest way to isolate a loss issue — bypass the trainer entirely: + +```python +# Load model via axolotl's pipeline (applies all patches) +from axolotl.cli.config import load_cfg +from axolotl.utils.config import normalize_config, prepare_plugins +from axolotl.loaders.tokenizer import load_tokenizer +from axolotl.loaders.model import ModelLoader + +cfg = load_cfg("your_config.yaml") +normalize_config(cfg) +prepare_plugins(cfg) +tokenizer = load_tokenizer(cfg) +model, _ = ModelLoader(cfg, tokenizer).load() + +# Forward pass on preprocessed data +model.train() +out = model(input_ids, labels=labels) +print(f"Direct loss: {out.loss.item()}") # Compare to trainer's reported loss +``` + +If direct loss is correct (~1.0) but trainer reports 3–4x higher, check `model_accepts_loss_kwargs` (see below). + +### `model_accepts_loss_kwargs` inflation +HF Trainer checks if the model's `forward()` has `**kwargs` and sets `model_accepts_loss_kwargs=True`. This changes loss normalization: the trainer does NOT divide loss by `gradient_accumulation_steps` before logging. The gradient is correct — only the logged loss is inflated. + +**Symptom**: Logged loss ≈ actual_loss × gradient_accumulation_steps. + +**Which models are affected**: Any model with `**kwargs` in forward (common in multimodal models for extra inputs like `mm_token_type_ids`, `pixel_values`, etc.). + +**Fix location**: `src/axolotl/core/trainers/base.py` `__init__()` — after `super().__init__()`, check if the unwrapped model actually has `num_items_in_batch` in its forward signature. If not, set `self.model_accepts_loss_kwargs = False`. + +## Multimodal Models (ForConditionalGeneration) + +Many recent models use `ForConditionalGeneration` as the top-level class, not `ForCausalLM`: +- Gemma3 → `Gemma3ForConditionalGeneration` +- Gemma4 → `Gemma4ForConditionalGeneration` +- Qwen2-VL → `Qwen2VLForConditionalGeneration` +- LLaVA → `LlavaForConditionalGeneration` + +### Why this matters + +| Component | Targets `ForCausalLM` | Needs `ForConditionalGeneration` | +|-----------|----------------------|--------------------------------| +| CCE patches | ✅ (default) | ❌ silently inactive if not patched | +| PEFT LoRA | ✅ | May fail on custom layer types | +| HF Trainer label handling | ✅ | May need extra inputs | + +### Required extra inputs +Multimodal models require special inputs during training even for text-only data: + +| Model | Required Input | Value for Text-Only | +|-------|---------------|-------------------| +| Gemma4 | `mm_token_type_ids` | `torch.zeros_like(input_ids)` | +| Gemma3 | `token_type_ids` | `torch.zeros_like(input_ids)` | + +Auto-inject in `compute_loss()` when not provided by the data collator. See `core/trainers/base.py`. + +### Custom layer types and PEFT +Vision towers often use custom module wrappers that PEFT doesn't support: + +| Model | Custom Layer | Wraps | Fix | +|-------|-------------|-------|-----| +| Gemma4 | `Gemma4ClippableLinear` | `nn.Linear` | Redirect to `.linear` child | + +Fix location: `src/axolotl/loaders/adapter.py` `_patch_peft_clippable_linear()`. + +## Sample Packing + +### How packed sequence detection works (transformers ≥ 5.x) +`transformers.masking_utils._preprocess_mask_arguments()` detects packed sequences from `position_ids` resets. But **only when `attention_mask is None`**: + +```python +# From masking_utils.py: +if position_ids is not None and attention_mask is None and past_key_values is None: + packed_sequence_mask = find_packed_sequence_indices(position_ids) +``` + +If the collator provides an all-ones `attention_mask`, packing detection is **skipped** and the model builds a single causal mask spanning all packed sequences → cross-sequence attention leakage → very high loss. + +### Fix for models using `create_causal_mask_mapping` +For Gemma3, Gemma4, and similar models that use the new transformers masking system, remove `attention_mask` from inputs when sample packing is active: + +```python +# In compute_loss(): +if ( + self.args.sample_packing + and model_type in ("gemma4", "gemma3") + and "attention_mask" in inputs + and "position_ids" in inputs +): + del inputs["attention_mask"] +``` + +Fix location: `src/axolotl/core/trainers/base.py` `compute_loss()`. + +### Models that DON'T need this fix +Older models that use `_prepare_4d_causal_attention_mask` (Llama, Mistral, Qwen2, etc.) handle sample packing via axolotl's multipack attention monkeypatch instead. Only models using the new `create_causal_mask_mapping` / `create_causal_mask` masking system need the `attention_mask` removal. + +## Attention Backend Selection + +| Backend | Config | head_dim limit | torch_compile | Notes | +|---------|--------|---------------|---------------|-------| +| FA2 | `flash_attention: true` | 256 | ✅ | Fastest when supported | +| FA4 | auto with `flash_attention: true` | 256 (SM90+) | ✅ | Auto-detected on H100+ | +| SDPA | `sdp_attention: true` | None | ✅ | Universal fallback | +| flex | `flex_attention: true` | None | ⚠️ Triton OOM for large head_dim | Good for variable head dims | +| eager | neither set | None | ✅ | Slowest, always works | + +**Check model support**: Look at `_supports_flash_attn_2`, `_supports_flex_attn`, `_supports_sdpa` attributes on the model class. + +**head_dim gotcha**: The 256 limit is specific to flash-attn CUDA kernels, NOT PyTorch-level. SDPA and flex_attention both handle arbitrary head_dim. Models with `global_head_dim > 256` (Gemma4: 512) must use SDPA or flex. + +**flex + compile gotcha**: `torch_compile` with flex_attention can hit Triton shared memory OOM for large head_dim. Falls back to eager per-function (not a crash, but slower). Unsloth disables flex for Gemma4 for this reason. + +## Cut Cross Entropy (CCE) + +### How CCE patches work +CCE replaces the model's `forward()` with a fused version that computes loss from hidden states + lm_head weight without materializing the full logits tensor. This saves ~`batch × seq_len × vocab_size × dtype_bytes` of VRAM. + +### Adding CCE for a new model +1. Check if the model type is in `cut_cross_entropy.transformers.patch.PATCH_FNS` +2. If not, axolotl's generic fallback (`integrations/cut_cross_entropy/__init__.py` `patch_llama_like()`) patches `{Prefix}ForCausalLM.forward` with `cce_forward` +3. For multimodal models (`ForConditionalGeneration`), a model-specific patch is needed in `ml-cross-entropy` repo +4. The multimodal `cce_forward` must accept all extra kwargs (pixel_values, mm_token_type_ids, etc.) and pop any that would conflict before calling `self.model()` + +### Common CCE pitfall +If CCE appears active (log says "Applying Cut Cross Entropy") but peak VRAM doesn't decrease, check which class was patched. If the model loads as `ForConditionalGeneration` but CCE patched `ForCausalLM`, the patch is silently inactive. + +## MoE Models + +### Dense MLP vs MoE experts +Some MoE models (e.g., Gemma4) have BOTH dense MLP layers and MoE expert layers at every decoder layer: +- `gate_proj/up_proj/down_proj` → targets the **dense MLP** (`Gemma4TextMLP`) +- `experts.gate_up_proj/experts.down_proj` → targets the **MoE experts** (`Gemma4TextExperts`) + +LoRA on the dense MLP works normally. Expert LoRA via `lora_target_parameters` requires PEFT support for the specific expert module type (may warn "Unsupported layer type"). + +### ScatterMoE kernels +`use_scattermoe: true` with `experts_implementation: scattermoe` registers fused expert kernels via transformers' `ExpertsInterface`. Significant speedup for MoE models. Requires the kernels plugin: +```yaml +plugins: + - axolotl.integrations.kernels.KernelsPlugin +use_kernels: true +use_scattermoe: true +experts_implementation: scattermoe +``` + +## Where to Add Model-Specific Fixes + +| What | Where | Example | +|------|-------|---------| +| Missing forward inputs | `core/trainers/base.py` `compute_loss()` | mm_token_type_ids injection | +| Attention mask fixes | `core/trainers/base.py` `compute_loss()` | Sample packing mask removal | +| Loss logging fixes | `core/trainers/base.py` `__init__()` | model_accepts_loss_kwargs override | +| PEFT/LoRA patches | `loaders/adapter.py` | ClippableLinear redirect | +| Attention patches | `monkeypatch/attention/` | FA4 tuple fix | +| Model-specific patches | `loaders/patch_manager.py` `_apply_model_specific_patches()` | Llama4, Kimi, NemotronH | +| CCE patches | `ml-cross-entropy` repo `transformers/` | Per-model cce_forward | +| Example configs | `examples//` | Validated YAML | +| Config validation | `utils/schemas/validation.py` | Compatibility checks | diff --git a/src/axolotl/cli/agent_docs/__init__.py b/src/axolotl/cli/agent_docs/__init__.py index d229184c08..14dbff32d3 100644 --- a/src/axolotl/cli/agent_docs/__init__.py +++ b/src/axolotl/cli/agent_docs/__init__.py @@ -19,6 +19,8 @@ "preference_tuning": "docs/agents/preference_tuning.md", "reward_modelling": "docs/agents/reward_modelling.md", "pretraining": "docs/agents/pretraining.md", + "model_architectures": "docs/agents/model_architectures.md", + "new_model_support": "docs/agents/new_model_support.md", } diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 650a238ecf..96183973fc 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -404,7 +404,9 @@ def compute_loss( # Gemma4 requires mm_token_type_ids during training (even for text-only). # Inject zeros (= text token type) when not provided by the data collator. - _model_type = getattr(getattr(model, "config", None), "model_type", None) + # Use unwrap_model to handle DDP/FSDP wrappers that don't proxy .config. + _unwrapped = self.accelerator.unwrap_model(model) + _model_type = getattr(getattr(_unwrapped, "config", None), "model_type", None) if ( "mm_token_type_ids" not in inputs and "input_ids" in inputs @@ -445,6 +447,21 @@ def evaluate(self, *args, **kwargs): LOG.info("Running evaluation step...") return super().evaluate(*args, **kwargs) + @override + def prediction_step(self, model, inputs, prediction_loss_only, ignore_keys=None): + # Gemma4 requires mm_token_type_ids even during evaluation. + _unwrapped = self.accelerator.unwrap_model(model) + _model_type = getattr(getattr(_unwrapped, "config", None), "model_type", None) + if ( + "mm_token_type_ids" not in inputs + and "input_ids" in inputs + and _model_type == "gemma4" + ): + inputs["mm_token_type_ids"] = torch.zeros_like(inputs["input_ids"]) + return super().prediction_step( + model, inputs, prediction_loss_only, ignore_keys=ignore_keys + ) + @staticmethod def orpo_concatenate_inputs(inputs, label_pad_token=-100, pad_token=0, device=None): concatenated_batch = {} diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 774aa1cecf..23388e40e2 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -36,7 +36,7 @@ from axolotl.utils.ctx_managers.sequence_parallel import SequenceParallelContextManager from axolotl.utils.dict import DictDefault from axolotl.utils.distributed import cleanup_distributed -from axolotl.utils.freeze import freeze_layers_except +from axolotl.utils.freeze import freeze_layers_except, freeze_mm_modules from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import RLType from axolotl.utils.train import determine_last_checkpoint @@ -114,6 +114,10 @@ def setup_model_and_tokenizer( ): model.enable_input_require_grads() + # Freeze multimodal modules for text-only training of multimodal models + if cfg.freeze_mm_modules: + freeze_mm_modules(model) + return model, tokenizer, peft_config, processor diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index c5bad62de7..8e6d3e7e74 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -268,6 +268,37 @@ def normalize_config(cfg): ): cfg.gradient_checkpointing_kwargs = {"use_reentrant": True} + # Gemma4 requires use_reentrant=False for DDP (shared per-layer norms cause + # "marked ready twice" errors with reentrant checkpointing) and + # ddp_find_unused_parameters=True (per_layer_projection LoRA params may not + # receive gradients on every step). + if cfg.model_config_type == "gemma4": + if cfg.gradient_checkpointing: + if cfg.gradient_checkpointing_kwargs is None: + cfg.gradient_checkpointing_kwargs = {} + if cfg.gradient_checkpointing_kwargs.get("use_reentrant") is not False: + LOG.warning( + "Gemma4 requires use_reentrant=False for gradient checkpointing " + "in distributed training. Setting use_reentrant=False." + ) + cfg.gradient_checkpointing_kwargs["use_reentrant"] = False + if cfg.ddp and cfg.ddp_find_unused_parameters is None: + if cfg.activation_offloading is True: + # activation_offloading uses checkpoint wrappers that conflict + # with find_unused_parameters (causes "marked ready twice"). + # Use freeze_mm_modules instead to eliminate unused params. + LOG.info( + "Gemma4 + DDP + activation_offloading: skipping " + "ddp_find_unused_parameters (use freeze_mm_modules to " + "handle unused vision/audio params)." + ) + else: + LOG.warning( + "Gemma4 requires ddp_find_unused_parameters=True for DDP. " + "Auto-enabling." + ) + cfg.ddp_find_unused_parameters = True + log_gpu_memory_usage(LOG, "baseline", cfg.device) diff --git a/src/axolotl/utils/freeze.py b/src/axolotl/utils/freeze.py index 936708f046..e60c496731 100644 --- a/src/axolotl/utils/freeze.py +++ b/src/axolotl/utils/freeze.py @@ -10,6 +10,44 @@ LOG = get_logger(__name__) +# Top-level module name prefixes that belong to vision/audio/multimodal encoders +# rather than the language backbone. These are matched against the first component +# of each ``named_parameter`` path (e.g. "model.vision_tower." -> "vision_tower"). +_MM_MODULE_PREFIXES = ( + "vision_tower", + "vision_model", + "vision_encoder", + "embed_vision", + "multi_modal_projector", + "visual", + "audio_tower", + "audio_model", + "embed_audio", +) + + +def freeze_mm_modules(model): + """Freeze all vision/audio/multimodal-projector parameters. + + Iterates over ``model.named_parameters()`` and sets ``requires_grad = False`` + for any parameter whose name contains a known vision/audio module prefix. + This is useful when fine-tuning only the language backbone of a multimodal + model and avoids the need for ``ddp_find_unused_parameters=True``. + """ + frozen_count = 0 + for name, param in model.named_parameters(): + # Check if any path component matches a vision/audio prefix + parts = name.split(".") + if any(part in _MM_MODULE_PREFIXES for part in parts): + if param.requires_grad: + param.requires_grad = False + frozen_count += 1 + if is_main_process(): + LOG.debug(f"freeze_mm_modules: froze {name}") + + if is_main_process(): + LOG.info(f"freeze_mm_modules: froze {frozen_count} vision/audio parameters") + def freeze_layers_except(model, regex_patterns): """ diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index d0f588d9b0..474c3a349b 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -578,6 +578,17 @@ class AxolotlInputConfig( }, ) + freeze_mm_modules: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Freeze multimodal encoder parameters (vision, audio, etc.) for " + "text-only training of multimodal models. When True, parameters belonging to " + "vision towers, audio towers, multimodal projectors, and similar non-language " + "modules are frozen (requires_grad=False). This allows DDP training without " + "ddp_find_unused_parameters=True." + }, + ) + unfrozen_parameters: list[str] | None = Field( default=None, json_schema_extra={ From 4dfa0a59b276adcea57eae68a5d4f25781a66af0 Mon Sep 17 00:00:00 2001 From: floaty3 Date: Fri, 10 Apr 2026 17:00:07 +0000 Subject: [PATCH 1259/1405] Add uninstall command to cut_cross_entropy import message (#3583) [skip ci] --- src/axolotl/integrations/cut_cross_entropy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index b9a59aea9f..5de10020cd 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fec1a88"`' + '`pip uninstall -y cut-cross-entropy && pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fec1a88"`' ) From bfb4da1d254eb04861d6649b6da875bddd91e160 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 11 Apr 2026 00:00:26 +0700 Subject: [PATCH 1260/1405] fix: document jinja2 file path support (#3588) [skip ci] --- docs/dataset-formats/conversation.qmd | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 34fde45fbd..9d018dc491 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -108,6 +108,14 @@ datasets: type: chat_template ``` +::: {.callout-tip} +`chat_template_jinja` also accepts a file path to a `.jinja2` file instead of an inline string: + +```yaml +chat_template_jinja: ./path/to/my_template.jinja2 +``` +::: + ::: {.callout-important} Please make sure that your `tokenizer.eos_token` is same as EOS (End-of-Sequence) token in template. Otherwise, set `eos_token` under `special_tokens: `. ::: From e7a6a5b529e3ddb3f3239866c48e7025f2e2c6ce Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Sat, 11 Apr 2026 00:00:47 +0700 Subject: [PATCH 1261/1405] fix: move warning after we've set any overrides (#3589) [skip ci] --- src/axolotl/loaders/tokenizer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index cf5c3d27b1..52f7146046 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -221,14 +221,6 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): if getattr(tokenizer, attr_name) is None: setattr(tokenizer, attr_name, "<|endoftext|>") - # Generic fallback: if tokenizer still has no pad_token, use eos_token - if tokenizer.pad_token is None and tokenizer.eos_token is not None: - tokenizer.pad_token = tokenizer.eos_token - LOG.warning( - "Tokenizer does not have a pad_token, falling back to eos_token: %s", - tokenizer.eos_token, - ) - additional_special_tokens = None if cfg.special_tokens: special_tokens = cfg.special_tokens.to_dict() @@ -303,6 +295,14 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): {"additional_special_tokens": additional_special_tokens} ) + # Generic fallback: if tokenizer still has no pad_token, use eos_token + if tokenizer.pad_token is None and tokenizer.eos_token is not None: + tokenizer.pad_token = tokenizer.eos_token + LOG.warning( + "Tokenizer does not have a pad_token, falling back to eos_token: %s", + tokenizer.eos_token, + ) + if is_main_process(): LOG.debug(f"EOS: {tokenizer.eos_token_id} / {tokenizer.eos_token}") LOG.debug(f"BOS: {tokenizer.bos_token_id} / {tokenizer.bos_token}") From 315cdeede976d604d5da17e91d1339c5a20314ec Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 10 Apr 2026 14:11:10 -0400 Subject: [PATCH 1262/1405] handle trainable/masked spans in content and reasoning content (#3592) --- docs/dataset-formats/conversation.qmd | 107 ++++ .../prompt_strategies/chat_template.py | 194 ++++++- .../test_chat_templates_advanced.py | 476 ++++++++++++++++++ 3 files changed, 767 insertions(+), 10 deletions(-) diff --git a/docs/dataset-formats/conversation.qmd b/docs/dataset-formats/conversation.qmd index 9d018dc491..adbb88645d 100644 --- a/docs/dataset-formats/conversation.qmd +++ b/docs/dataset-formats/conversation.qmd @@ -302,6 +302,113 @@ datasets: It is not necessary to set both `message_field_training` and `message_field_training_detail` at once. ::: +#### Content parts with per-part training control + +Instead of using character offsets with `train_detail`, you can split a message's content into a list of parts, each with its own training flag. This is useful when you want to mask specific sections of a response (e.g., mask reasoning but train on the answer). + +```{.json filename="data.jsonl"} +{ + "messages": [ + {"role": "user", "content": [{"type": "text", "text": "What is 2+2?"}]}, + { + "role": "assistant", + "content": [ + {"type": "text", "text": "Let me think step by step...", "train": false}, + {"type": "text", "text": " The answer is 4.", "train": true} + ] + } + ] +} +``` + +The configuration is the same as standard `chat_template` — no extra fields needed: + +```yaml +datasets: + - path: ... + type: chat_template + roles_to_train: ["assistant"] +``` + +Each content part supports: + +- `type`: `"text"` (required) +- `text`: the text value (also accepts `content` or `value` as the key) +- `train`: `true`/`false` (optional) — whether to train on this part +- `weight`: `0`/`1` (optional) — alternative to `train` + +If a part has no `train` or `weight` flag, it inherits the turn-level training decision (from `roles_to_train`, `message_field_training`, or `train_on_inputs`). + +::: {.callout-warning title="Whitespace at part boundaries"} +BPE tokenizers (used by Llama, Qwen, Mistral, GPT, etc.) prepend spaces to word tokens. For example, `" answer"` is a single token — the space is part of it. This means **where you place whitespace between content parts matters**: + +**Split BEFORE spaces** (space goes with the next part): + +```json +[ + {"type": "text", "text": "Let me think...", "train": false}, + {"type": "text", "text": " The answer is 4.", "train": true} +] +``` + +**DON'T put trailing spaces** on a part (the space merges with the next word into one token that straddles the boundary, and straddling tokens are masked): + +```json +[ + {"type": "text", "text": "Let me think... ", "train": false}, + {"type": "text", "text": "The answer is 4.", "train": true} +] +``` + +In the bad example, `" The"` becomes a single token that spans both parts. Because it straddles the boundary, it is conservatively **masked** (not trained) — even though the second part has `train: true`. + +**Newlines** typically merge with preceding punctuation (e.g., `":\n"` is one token). Keep newlines with the preceding part: + +```json +[ + {"type": "text", "text": "Thinking:\n", "train": false}, + {"type": "text", "text": "The answer is 4.", "train": true} +] +``` + +Axolotl will log a warning if it detects trailing whitespace at a boundary between parts with different training flags. +::: + +::: {.callout-note} +When all content parts in a message are strings, they are concatenated before being passed to the chat template. This means content parts work with **any** Jinja template — the template sees a plain string, and the per-part training flags are applied during tokenization. +::: + +##### Per-part training on reasoning_content + +For templates that support a separate `reasoning_content` field (e.g., `qwen3`), the same content-parts format works on `reasoning_content`. This is useful for masking incorrect reasoning steps while training on self-corrections: + +```{.json filename="data.jsonl"} +{ + "messages": [ + {"role": "user", "content": [{"type": "text", "text": "What is 2+2?"}]}, + { + "role": "assistant", + "reasoning_content": [ + {"type": "text", "text": "Hmm maybe 2+2=5.", "train": false}, + {"type": "text", "text": " Wait no, 2+2=4.", "train": true} + ], + "content": [ + {"type": "text", "text": "The answer is 4.", "train": true} + ] + } + ] +} +``` + +The `reasoning_content` and `content` fields are handled independently — each has its own token boundaries and per-part masking. No additional configuration is needed beyond what the template already requires. + +::: {.callout-tip} +When `reasoning_content` is provided as a separate field, `split_thinking` is not needed — the reasoning is already separated from the content in the data. +::: + +The same whitespace rules apply to `reasoning_content` parts as to `content` parts — split before spaces, keep newlines with the preceding part. + + #### Reasoning split (For Qwen3 template only) Enable reasoning split, where the reasoning is split from the content and passed as a separate field into the template. diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index 57d3bfdf21..a7f749f3b2 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -471,6 +471,7 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: content = turn.get("content") train_turn = turn.get("training") train_detail = turn.get("training_detail") + reasoning_train_detail = turn.get("reasoning_training_detail") LOG.debug( f"Processing turn {index}: role={role}, content={content}, train_turn={train_turn}, train_detail={train_detail}" @@ -479,8 +480,8 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: should_train = None if train_turn is not None: should_train = train_turn - elif train_detail is not None: - should_train = bool(train_detail) + elif train_detail is not None or reasoning_train_detail is not None: + should_train = bool(train_detail) or bool(reasoning_train_detail) else: should_train = self.train_on_inputs or role in self.roles_to_train @@ -500,15 +501,26 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: continue + thinking_key = self.prompter.template_thinking_key + has_reasoning = thinking_key and turn.get(thinking_key) is not None + has_any_detail = train_detail or reasoning_train_detail + + # When train_detail is present and the turn has reasoning_content, + # use content_only=True so find_turn returns content-only boundaries + # (excluding reasoning_content + template separator tokens). + use_content_only = bool(has_any_detail and has_reasoning) + turn_start_idx, turn_end_idx = self.find_turn( - turns=turns, turn_idx=index, tools=tools + turns=turns, + turn_idx=index, + tools=tools, + content_only=use_content_only, ) LOG.debug(f"Turn indices: start={turn_start_idx}, end={turn_end_idx}") if should_train and turn_start_idx != -1 and turn_end_idx != -1: if train_detail: - # Block multi-content for now if not isinstance(content, str): raise ValueError( "`train_detail` is not supported when `content` is not a string." @@ -526,7 +538,8 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: LOG.debug( f"Label set at index {turn_start_idx + i}: {input_ids[turn_start_idx + i]}" ) - else: + elif not reasoning_train_detail: + # No per-part detail on either field — train the whole span labels[turn_start_idx:turn_end_idx] = input_ids[ turn_start_idx:turn_end_idx ] @@ -534,6 +547,32 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: f"Set labels for training from {turn_start_idx} to {turn_end_idx}" ) + # Handle reasoning_content training_detail separately + if should_train and reasoning_train_detail and has_reasoning: + reasoning_text = turn[thinking_key] + if not isinstance(reasoning_text, str): + raise ValueError( + "`reasoning_training_detail` is not supported when reasoning_content is not a string." + ) + + reasoning_start, reasoning_end = self.find_turn( + turns=turns, + turn_idx=index, + tools=tools, + reasoning_only=True, + ) + + if reasoning_start != -1 and reasoning_end != -1: + token_offsets = self.prompter.get_offsets_for_train_detail( # type: ignore + reasoning_text, reasoning_train_detail + ) + LOG.debug(f"Reasoning token offsets: {token_offsets}") + for i, offset in enumerate(token_offsets): + if offset != IGNORE_TOKEN_ID and reasoning_start + i < len( + input_ids + ): + labels[reasoning_start + i] = input_ids[reasoning_start + i] + LOG.debug(f"Labels after processing turn {index}: {labels}") # Handle special tokens (EOT and EOS) @@ -611,10 +650,24 @@ def find_first_eot_token(self, input_ids, start_idx): return -1 def find_turn( - self, turns: list[dict], turn_idx: int, tools: list[dict] | None = None + self, + turns: list[dict], + turn_idx: int, + tools: list[dict] | None = None, + content_only: bool = False, + reasoning_only: bool = False, ): """ Locate the starting and ending indices of the specified turn in a conversation. + + Args: + content_only: If True and the turn has reasoning_content (template_thinking_key), + preserve reasoning_content in the dummy turn so the diff only captures the + content field boundaries. This is needed for correct training_detail alignment + when reasoning_content is present. + reasoning_only: If True, preserve content in the dummy turn and replace + reasoning_content with a dummy, so the diff only captures the + reasoning_content field boundaries. """ if turn_idx >= len(turns): @@ -628,10 +681,26 @@ def find_turn( ): return -1, -1 - empty_turn = { - "role": turns[turn_idx].get("role"), - "content": "[[dummy_message]]", - } + thinking_key = self.prompter.template_thinking_key + + if reasoning_only: + # Keep content as-is, replace reasoning with dummy + empty_turn = { + "role": turns[turn_idx].get("role"), + "content": turns[turn_idx].get("content", ""), + } + if thinking_key and thinking_key in turns[turn_idx]: + empty_turn[thinking_key] = "[[dummy_reasoning]]" + else: + empty_turn = { + "role": turns[turn_idx].get("role"), + "content": "[[dummy_message]]", + } + + # When content_only is True, copy reasoning_content to the dummy turn so + # the diff only captures the content field (not reasoning + separator). + if content_only and thinking_key and thinking_key in turns[turn_idx]: + empty_turn[thinking_key] = turns[turn_idx][thinking_key] # Create conversation versions turns_with_empty = turns[:turn_idx] + [empty_turn] @@ -697,6 +766,94 @@ def find_turn( return start_idx, end_idx + @staticmethod + def _convert_content_parts( + content, + ) -> tuple[str, list[dict] | None] | None: + """Convert list content to concatenated string + optional training_detail. + + When content is a list of dicts (content parts), each part can specify: + - ``text``, ``content``, or ``value``: the text string + - ``train`` (bool) or ``weight`` (0/1): per-part training flag + + Returns ``(concatenated_text, training_details_or_None)`` if content was + a list, or ``None`` if content was not a list (no conversion needed). + + .. note:: + **Whitespace at part boundaries matters.** BPE tokenizers prepend + spaces to word tokens (e.g. ``" answer"`` is one token). Always + split BEFORE spaces:: + + GOOD: ["Let me think...", " The answer is 4."] + BAD: ["Let me think... ", "The answer is 4."] + + Tokens that straddle a boundary are conservatively masked. + Newlines typically merge with preceding punctuation (``":\\n"`` is + one token), so keep newlines with the preceding part. + """ + if not isinstance(content, list): + return None + + text_parts: list[str] = [] + training_details: list[dict] = [] + has_explicit_training = False + offset = 0 + + for part in content: + if isinstance(part, dict): + # Extract text (HF uses "text", also support "content"/"value") + text = ( + part.get("text") or part.get("content") or part.get("value") or "" + ) + text_parts.append(text) + + # Check for per-part training flags + part_train = part.get("train") + part_weight = part.get("weight") + if part_train is not None or part_weight is not None: + has_explicit_training = True + train = ( + part_train + if part_train is not None + else (part_weight not in (0, 0.0)) + ) + else: + train = True # default trainable, gated by turn-level should_train + + if text: + training_details.append( + { + "begin_offset": offset, + "end_offset": offset + len(text) - 1, + "train": train, + } + ) + offset += len(text) + + # Warn about trailing whitespace at boundaries between parts with + # different training flags — this almost always causes token straddling + if has_explicit_training and len(training_details) > 1: + for i in range(len(training_details) - 1): + cur = training_details[i] + nxt = training_details[i + 1] + if cur["train"] != nxt["train"]: + boundary_text = text_parts[i] + if boundary_text and boundary_text[-1] in (" ", "\t"): + LOG.warning( + "Content part %d ends with whitespace at a train/mask boundary. " + "BPE tokenizers typically prepend spaces to word tokens, so " + "the space will merge with the next part's first word and the " + "resulting token will be MASKED (not trained). Move the " + "whitespace to the start of the next content part instead. " + "Part text: %r", + i, + boundary_text[-20:], + ) + + concatenated = "".join(text_parts) + details = training_details if has_explicit_training else None + return concatenated, details + def get_conversation_thread(self, prompt): turns = [] @@ -723,6 +880,23 @@ def get_conversation_thread(self, prompt): if training_detail is not None: turn["training_detail"] = training_detail + # Convert list content/reasoning_content to string + auto-generated + # training_detail. See _convert_content_parts for whitespace guidance. + content_result = self._convert_content_parts(turn.get("content")) + if content_result is not None: + turn["content"] = content_result[0] + if content_result[1] is not None: + turn["training_detail"] = content_result[1] + + # Also convert reasoning_content (template_thinking_key) if it's a list + thinking_key = self.prompter.template_thinking_key + if thinking_key and thinking_key in turn: + reasoning_result = self._convert_content_parts(turn[thinking_key]) + if reasoning_result is not None: + turn[thinking_key] = reasoning_result[0] + if reasoning_result[1] is not None: + turn["reasoning_training_detail"] = reasoning_result[1] + turns.append(turn) if self.prompter.drop_system_message and turns[0]["role"] == "system": diff --git a/tests/prompt_strategies/test_chat_templates_advanced.py b/tests/prompt_strategies/test_chat_templates_advanced.py index 7d4e6883f6..701e4d01ea 100644 --- a/tests/prompt_strategies/test_chat_templates_advanced.py +++ b/tests/prompt_strategies/test_chat_templates_advanced.py @@ -916,6 +916,235 @@ def verify_labels(labels_span, should_train, context_message): LOG.debug(f"Final labels: {labels}") LOG.debug(f"Final input_ids: {input_ids}") + @enable_hf_offline + def test_content_parts_training( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + request, + ): + LOG.info("Testing with content as list of parts with per-part training") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_property_mappings={"role": "role", "content": "content"}, + ), + tokenizer=tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + ) + + # Dataset where assistant content is a list of parts with per-part training + conversation = [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are an AI assistant."}, + ], + }, + { + "role": "user", + "content": [ + {"type": "text", "text": "What is 2+2?"}, + ], + }, + { + "role": "assistant", + "content": [ + {"type": "text", "text": "Let me think...", "train": False}, + {"type": "text", "text": "The answer is 4.", "train": True}, + ], + }, + ] + + dataset = Dataset.from_dict({"messages": [conversation]}) + res = strategy.tokenize_prompt(dataset[0]) + turns = strategy.get_conversation_thread(dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Find the assistant turn (last turn) + assistant_turn_idx = len(turns) - 1 + start_idx, end_idx = strategy.find_turn( + turns=turns, turn_idx=assistant_turn_idx + ) + + assert start_idx != -1 and end_idx != -1, ( + "Could not find assistant turn boundaries" + ) + + decoded = tokenizer.decode(input_ids[start_idx:end_idx]) + LOG.debug(f"Assistant turn decoded: {decoded}") + + # Tokenize each part separately to find their boundaries + part1_text = "Let me think..." + part2_text = "The answer is 4." + + # Verify the concatenated content is in the decoded output + assert part1_text in decoded, ( + f"Part 1 '{part1_text}' not found in decoded: {decoded}" + ) + assert part2_text in decoded, ( + f"Part 2 '{part2_text}' not found in decoded: {decoded}" + ) + + # Verify that part1 tokens (train=False) are masked + # and part2 tokens (train=True) are labeled + turn_labels = labels[start_idx:end_idx] + + # Find where part2 starts in the token sequence + part1_tokens = tokenizer(part1_text, add_special_tokens=False)["input_ids"] + part2_tokens = tokenizer(part2_text, add_special_tokens=False)["input_ids"] + + # The first part should be masked (all IGNORE_TOKEN_ID) + # Due to token boundary alignment, check that at least the interior tokens + # of part1 are masked + assert any(label == IGNORE_TOKEN_ID for label in turn_labels), ( + f"Expected some masked labels for train=False part, but got {turn_labels}" + ) + + # The second part should be trained (not IGNORE_TOKEN_ID) + assert any(label != IGNORE_TOKEN_ID for label in turn_labels), ( + f"Expected some trained labels for train=True part, but got {turn_labels}" + ) + + # More precise check: first N tokens should be masked, last M tokens should be trained + # where N ~ len(part1_tokens) and M ~ len(part2_tokens) + # Allow for token boundary effects at the boundary + num_masked = sum(1 for label in turn_labels if label == IGNORE_TOKEN_ID) + num_trained = sum(1 for label in turn_labels if label != IGNORE_TOKEN_ID) + + LOG.debug(f"Turn labels: {turn_labels}") + LOG.debug(f"Masked tokens: {num_masked}, Trained tokens: {num_trained}") + LOG.debug( + f"Part1 tokens: {len(part1_tokens)}, Part2 tokens: {len(part2_tokens)}" + ) + + # The number of masked tokens should be roughly the size of part1 + # and the number of trained tokens should be roughly the size of part2 + assert num_masked > 0, "Expected masked tokens for the train=False part" + assert num_trained > 0, "Expected trained tokens for the train=True part" + + @enable_hf_offline + def test_content_parts_with_weight( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + request, + ): + LOG.info("Testing with content parts using weight field") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_property_mappings={"role": "role", "content": "content"}, + ), + tokenizer=tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + ) + + # Dataset using weight instead of train + conversation = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Hello"}, + ], + }, + { + "role": "assistant", + "content": [ + {"type": "text", "text": "Thinking step by step: ", "weight": 0}, + {"type": "text", "text": "Hello! How can I help?", "weight": 1}, + ], + }, + ] + + dataset = Dataset.from_dict({"messages": [conversation]}) + res = strategy.tokenize_prompt(dataset[0]) + labels = res["labels"] + + # There should be both masked and trained labels + has_masked = any(label == IGNORE_TOKEN_ID for label in labels) + has_trained = any(label != IGNORE_TOKEN_ID for label in labels) + assert has_masked, "Expected masked tokens (weight=0 part + user turn)" + assert has_trained, "Expected trained tokens (weight=1 part)" + + @enable_hf_offline + def test_content_parts_string_passthrough( + self, + tokenizer, + chat_template, + chat_template_jinja, + eos_token, + request, + ): + LOG.info("Testing that string content still works alongside list content") + + tokenizer, chat_template_jinja = self.setup_tokenizer( + tokenizer, chat_template, chat_template_jinja, eos_token, request + ) + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + tokenizer, + chat_template=get_chat_template( + chat_template, jinja_template=chat_template_jinja + ), + message_property_mappings={"role": "role", "content": "content"}, + ), + tokenizer=tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + ) + + # All list content in the conversation + conversation = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What is 2+2?"}, + ], + }, + { + "role": "assistant", + "content": [ + {"type": "text", "text": "The answer is 4.", "train": True}, + ], + }, + ] + + dataset = Dataset.from_dict({"messages": [conversation]}) + res = strategy.tokenize_prompt(dataset[0]) + + # Should tokenize without errors + assert "input_ids" in res + assert "labels" in res + assert len(res["input_ids"]) > 0 + def test_get_chat_template_variables( self, tokenizer, chat_template, chat_template_jinja, eos_token, request ): @@ -1428,3 +1657,250 @@ def test_tool_calling_with_llama4_template( assert all(label != IGNORE_TOKEN_ID for label in turn_labels), ( f"Assistant turn {i} should be unmasked" ) + + +class TestChatTemplateReasoningContent: + """ + Test class for reasoning_content with content parts. + """ + + @enable_hf_offline + def test_reasoning_content_with_content_parts(self, qwen3_tokenizer): + """Test that reasoning_content as string + content as list parts works correctly. + Content training_detail offsets should align with content-only boundaries.""" + LOG.info("Testing reasoning_content with content parts on qwen3") + + tokenizer = deepcopy(qwen3_tokenizer) + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + tokenizer, + chat_template=get_chat_template("qwen3"), + message_property_mappings={ + "role": "role", + "content": "content", + "reasoning_content": "reasoning_content", + }, + ), + tokenizer=tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + ) + + # reasoning_content is a plain string, content is list with per-part training + conversation = [ + { + "role": "user", + "content": [{"type": "text", "text": "What is 2+2?"}], + }, + { + "role": "assistant", + "reasoning_content": "Step 1: 2+2=4", + "content": [ + {"type": "text", "text": "The answer is 4.", "train": True}, + ], + }, + ] + + dataset = Dataset.from_dict({"messages": [conversation]}) + res = strategy.tokenize_prompt(dataset[0]) + turns = strategy.get_conversation_thread(dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Find the assistant turn + assistant_idx = 1 + start_idx, end_idx = strategy.find_turn( + turns=turns, turn_idx=assistant_idx, content_only=True + ) + + assert start_idx != -1 and end_idx != -1, ( + "Could not find assistant content boundaries" + ) + + # The content-only span should contain "The answer is 4." but NOT "Step 1: 2+2=4" + decoded_span = tokenizer.decode(input_ids[start_idx:end_idx]) + assert "The answer is 4." in decoded_span, ( + f"Content not found in span: {decoded_span}" + ) + assert "Step 1" not in decoded_span, ( + f"Reasoning should not be in content-only span: {decoded_span}" + ) + + # Verify that content tokens are trained + content_labels = labels[start_idx:end_idx] + assert any(label != IGNORE_TOKEN_ID for label in content_labels), ( + f"Expected trained labels in content span, got {content_labels}" + ) + + @enable_hf_offline + def test_reasoning_content_per_part_masking(self, qwen3_tokenizer): + """Test masking incorrect reasoning while training on self-correction. + This is the core use case: mask out wrong thoughts, train on corrections.""" + LOG.info("Testing reasoning_content per-part masking on qwen3") + + tokenizer = deepcopy(qwen3_tokenizer) + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + tokenizer, + chat_template=get_chat_template("qwen3"), + message_property_mappings={ + "role": "role", + "content": "content", + "reasoning_content": "reasoning_content", + }, + ), + tokenizer=tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + ) + + # Reasoning has wrong step (masked) then self-correction (trained) + conversation = [ + { + "role": "user", + "content": [{"type": "text", "text": "What is 2+2?"}], + }, + { + "role": "assistant", + "reasoning_content": [ + {"type": "text", "text": "Hmm maybe 2+2=5.", "train": False}, + {"type": "text", "text": " Wait no, 2+2=4.", "train": True}, + ], + "content": [ + {"type": "text", "text": "The answer is 4.", "train": True}, + ], + }, + ] + + dataset = Dataset.from_dict({"messages": [conversation]}) + res = strategy.tokenize_prompt(dataset[0]) + turns = strategy.get_conversation_thread(dataset[0]) + labels = res["labels"] + input_ids = res["input_ids"] + + # Find reasoning boundaries + reasoning_start, reasoning_end = strategy.find_turn( + turns=turns, turn_idx=1, reasoning_only=True + ) + assert reasoning_start != -1 and reasoning_end != -1, ( + "Could not find reasoning boundaries" + ) + + decoded_reasoning = tokenizer.decode(input_ids[reasoning_start:reasoning_end]) + LOG.debug(f"Reasoning span: {decoded_reasoning!r}") + assert "2+2=5" in decoded_reasoning, ( + f"Wrong step not in reasoning span: {decoded_reasoning}" + ) + assert "2+2=4" in decoded_reasoning, ( + f"Correction not in reasoning span: {decoded_reasoning}" + ) + + # Verify reasoning labels have both masked and trained tokens + reasoning_labels = labels[reasoning_start:reasoning_end] + reasoning_ids = input_ids[reasoning_start:reasoning_end] + + # Decode only the trained tokens — should be exactly the self-correction + trained_ids = [ + tid + for tid, lab in zip(reasoning_ids, reasoning_labels, strict=True) + if lab != IGNORE_TOKEN_ID + ] + trained_text = tokenizer.decode(trained_ids) + assert trained_text.strip() == "Wait no, 2+2=4.", ( + f"Expected trained reasoning to be 'Wait no, 2+2=4.', got: {trained_text!r}" + ) + + # Decode only the masked tokens — should be exactly the incorrect step + masked_ids = [ + tid + for tid, lab in zip(reasoning_ids, reasoning_labels, strict=True) + if lab == IGNORE_TOKEN_ID + ] + masked_text = tokenizer.decode(masked_ids) + assert masked_text.strip() == "Hmm maybe 2+2=5.", ( + f"Expected masked reasoning to be 'Hmm maybe 2+2=5.', got: {masked_text!r}" + ) + + # Find content boundaries + content_start, content_end = strategy.find_turn( + turns=turns, turn_idx=1, content_only=True + ) + assert content_start != -1 and content_end != -1, ( + "Could not find content boundaries" + ) + + # Content should be fully trained — decode trained tokens to verify + content_labels = labels[content_start:content_end] + content_ids = input_ids[content_start:content_end] + content_trained_ids = [ + tid + for tid, lab in zip(content_ids, content_labels, strict=True) + if lab != IGNORE_TOKEN_ID + ] + content_trained_text = tokenizer.decode(content_trained_ids) + assert "The answer is 4." in content_trained_text, ( + f"Expected 'The answer is 4.' in trained content tokens, " + f"got: {content_trained_text!r}" + ) + assert all(label != IGNORE_TOKEN_ID for label in content_labels), ( + f"Expected all content labels trained, got {content_labels}" + ) + + @enable_hf_offline + def test_reasoning_content_as_list_no_training_flags(self, qwen3_tokenizer): + """Test that reasoning_content as list without training flags still works.""" + LOG.info("Testing reasoning_content as list without training flags on qwen3") + + tokenizer = deepcopy(qwen3_tokenizer) + + strategy = ChatTemplateStrategy( + ChatTemplatePrompter( + tokenizer, + chat_template=get_chat_template("qwen3"), + message_property_mappings={ + "role": "role", + "content": "content", + "reasoning_content": "reasoning_content", + }, + ), + tokenizer=tokenizer, + train_on_inputs=False, + sequence_len=512, + roles_to_train=["assistant"], + ) + + # Both as lists, no per-part training flags + conversation = [ + { + "role": "user", + "content": [{"type": "text", "text": "What is 2+2?"}], + }, + { + "role": "assistant", + "reasoning_content": [ + {"type": "text", "text": "Step 1: addition."}, + {"type": "text", "text": " Step 2: 2+2=4."}, + ], + "content": [ + {"type": "text", "text": "The answer is 4."}, + ], + }, + ] + + dataset = Dataset.from_dict({"messages": [conversation]}) + res = strategy.tokenize_prompt(dataset[0]) + + # Should tokenize without errors + assert "input_ids" in res + assert "labels" in res + assert len(res["input_ids"]) > 0 + + # Verify the full output contains both reasoning and content + full_text = tokenizer.decode(res["input_ids"]) + assert "Step 1: addition." in full_text + assert "Step 2: 2+2=4." in full_text + assert "The answer is 4." in full_text From 29fa4dedbb7885bd0594937d40779b36ab5e6a12 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 10 Apr 2026 16:46:17 -0400 Subject: [PATCH 1263/1405] Gemma4 fixes and profiler (#3591) --- README.md | 30 +- docs/agents/model_architectures.md | 88 + docs/agents/sft.md | 24 + docs/multimodal.qmd | 35 + examples/gemma4/e2b-vision-lora.yaml | 62 + examples/qwen3.5/35b-a3b-moe-vision-lora.yaml | 62 + scripts/analyze_profile.py | 1518 +++++++++++++++++ src/axolotl/core/trainers/base.py | 17 + src/axolotl/integrations/liger/plugin.py | 50 + src/axolotl/monkeypatch/lora_kernels.py | 41 + 10 files changed, 1926 insertions(+), 1 deletion(-) create mode 100644 examples/gemma4/e2b-vision-lora.yaml create mode 100644 examples/qwen3.5/35b-a3b-moe-vision-lora.yaml create mode 100644 scripts/analyze_profile.py diff --git a/README.md b/README.md index 3d0710f0ff..063ec8d3b3 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Features: **Requirements**: - NVIDIA GPU (Ampere or newer for `bf16` and Flash Attention) or AMD GPU -- Python 3.11 +- Python >=3.11 (3.12 recommended) - PyTorch ≥2.9.1 ### Google Colab @@ -95,6 +95,34 @@ Features: ### Installation +#### Using uv (recommended) + +```bash +# install uv if you don't already have it installed +curl -LsSf https://astral.sh/uv/install.sh | sh +source $HOME/.local/bin/env + +# CUDA 12.8.1 tends to have better package compatibility +export UV_TORCH_BACKEND=cu128 + +# create a new virtual environment +uv venv --python 3.12 +source .venv/bin/activate + +uv pip install torch==2.10.0 torchvision +uv pip install --no-build-isolation axolotl[deepspeed] + +# recommended - install cut-cross-entropy +uv pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@main" + +# (optional) - prefetch flash-attn2 and causal-conv1d kernels +uv run --python 3.12 python -c "from kernels import get_kernel; get_kernel('kernels-community/flash-attn2'); get_kernel('kernels-community/causal-conv1d')" + +# Download example axolotl configs, deepspeed configs +axolotl fetch examples +axolotl fetch deepspeed_configs # OPTIONAL +``` + #### Using pip ```bash diff --git a/docs/agents/model_architectures.md b/docs/agents/model_architectures.md index 426db4ce9f..9412b4883b 100644 --- a/docs/agents/model_architectures.md +++ b/docs/agents/model_architectures.md @@ -2,6 +2,64 @@ Model-specific quirks, required settings, and known issues. Check this before debugging training failures on specific model families. +## VLM (Vision Language Model) Quick Start + +All VLM configs require these four lines: +```yaml +processor_type: AutoProcessor +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false +``` + +Decision tree for VLM config: +```text +Is the model multimodal (has vision/audio encoder)? + ├─ YES: Add `freeze_mm_modules: true` if training text only + │ Add `chat_template: ` (e.g. gemma4, qwen3_5, gemma3) + │ LoRA: use regex `lora_target_modules` to restrict to language model + └─ NO: Train as a regular text model + +Is the model MoE (e.g. Gemma4 26B-A4B, Qwen3.5 35B-A3B)? + ├─ YES: Add `lora_target_parameters` for expert LoRA + │ Consider ScatterMoE kernels (see Plugins section) + └─ NO: Standard LoRA config +``` + +## Plugins & Optimizations + +### Cut Cross Entropy (CCE) + +Computes loss from hidden states + lm_head weight without materializing the full logits tensor, saving significant VRAM. Install if not already present: + +```bash +uv pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@main" +``` + +```yaml +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +``` + +### ScatterMoE Kernels + +Fuses expert + LoRA computation into a single kernel for MoE models. Significant speedup for models with many experts. + +```yaml +plugins: + - axolotl.integrations.kernels.KernelsPlugin +use_kernels: true +use_scattermoe: true +experts_implementation: scattermoe + +# Expert LoRA targets (3D parameter tensors, not nn.Linear): +lora_target_parameters: + - experts.gate_up_proj + - experts.down_proj +``` + +Supported: Gemma4 (`gemma4_text`), Mixtral, Qwen MoE variants. The plugin auto-detects model type and routing function. Without ScatterMoE, expert LoRA still works but runs base expert matmul and LoRA as separate operations. + ## Gemma 4 **Models**: `google/gemma-4-26B-A4B` (MoE), `google/gemma-4-31B` (dense), `google/gemma-4-E2B`, `google/gemma-4-E4B` @@ -66,6 +124,36 @@ fsdp_config: experts_implementation: scattermoe ``` +### VLM (Vision) Training + +All Gemma4 models load as `Gemma4ForConditionalGeneration` with a vision tower. No custom `ProcessingStrategy` needed — the base class auto-detects the image token. + +```yaml +base_model: google/gemma-4-E2B-it # or E4B-it, 26B-A4B +processor_type: AutoProcessor +freeze_mm_modules: true +chat_template: gemma4 + +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false +``` + +A starting VLM loss of ~8-15 is typical. In most runs, loss converges below 1.0 within ~30-50 steps, though results may vary across configurations. + +For the 26B-A4B MoE variant with ScatterMoE + expert LoRA + CCE, add: +```yaml +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.kernels.KernelsPlugin +use_kernels: true +use_scattermoe: true +experts_implementation: scattermoe +lora_target_parameters: + - experts.gate_up_proj + - experts.down_proj +``` + ### Common issues | Symptom | Cause | Fix | diff --git a/docs/agents/sft.md b/docs/agents/sft.md index 4d7b4e1a58..d3dfd39f70 100644 --- a/docs/agents/sft.md +++ b/docs/agents/sft.md @@ -91,6 +91,30 @@ Watch for: loss never decreasing (check `train_on_inputs`, dataset, LR), loss go | FSDP save hangs | Use `fsdp_state_dict_type: FULL_STATE_DICT` | | DeepSpeed CheckpointError | Set `use_reentrant: true` in `gradient_checkpointing_kwargs` | +## Profiling + +To profile training and identify optimization opportunities: + +```yaml +# Profile steps 3-7 (after warmup/autotuning settles) +profiler_steps_start: 3 +profiler_steps: 5 +``` + +This produces `profiler_trace.json` (Chrome trace) and `snapshot.pickle` (memory snapshot) in `output_dir`. +View the Chrome trace at `chrome://tracing`. + +To programmatically inspect the trace: +```bash +python scripts/analyze_profile.py output_dir/ +``` + +The trace shows per-kernel CUDA times, memory allocations, and operator-level breakdown. Look for: +- **Large matmul kernels**: candidates for fusion or quantization +- **Memory copies (H2D/D2H)**: unnecessary data movement +- **Small frequent kernels**: candidates for kernel fusion +- **Gaps between kernels**: pipeline bubbles from CPU overhead + Full troubleshooting: [training_stability.qmd](../training_stability.qmd), [debugging.qmd](../debugging.qmd) ## File Map diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index e2a9440e89..aabff03f26 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -8,6 +8,7 @@ format: ## Supported Models +- [Gemma-4](#sec-gemma-4) *(NEW)* - [Mllama](#sec-mllama) - [Llama4](#sec-llama4) - [Pixtral](#sec-pixtral) @@ -138,6 +139,40 @@ base_model: mistralai/Voxtral-Mini-3B-2507 processor_type: VoxtralProcessor ``` +### Gemma-4 {#sec-gemma-4} + +All Gemma 4 variants (E2B, E4B, 26B-A4B, 31B) load as multimodal models even for text-only training. + +```yaml +base_model: google/gemma-4-E2B-it # or E4B-it, 26B-A4B, 31B + +chat_template: gemma4 +freeze_mm_modules: true # freeze vision/audio encoders for text-only or vision LoRA + +# For the 26B-A4B MoE model, enable ScatterMoE and expert LoRA: +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.kernels.KernelsPlugin +use_kernels: true +use_scattermoe: true +experts_implementation: scattermoe + +lora_target_modules: 'model.language_model.layers.[\d]+.(_checkpoint_wrapped_module.)?(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' + +# MoE expert LoRA (3D tensors, not nn.Linear) — only for 26B-A4B: +lora_target_parameters: + - experts.gate_up_proj + - experts.down_proj +``` + +::: {.callout-warning} +Gemma 4 VLM training starts with high loss (~8-15). This is expected — see the [training stability guide](training_stability.qmd) for details. +::: + +::: {.callout-tip} +For DDP training, axolotl auto-detects Gemma4 and sets `use_reentrant=False` and `ddp_find_unused_parameters=True`. However, when `activation_offloading: true`, `ddp_find_unused_parameters` is skipped (checkpoint wrappers conflict with it); use `freeze_mm_modules: true` instead to handle unused vision/audio params. For FSDP2, use `fsdp_transformer_layer_cls_to_wrap: Gemma4TextDecoderLayer`. +::: + ### Gemma-3 {#sec-gemma-3} ::: {.callout-tip} diff --git a/examples/gemma4/e2b-vision-lora.yaml b/examples/gemma4/e2b-vision-lora.yaml new file mode 100644 index 0000000000..c779aaea55 --- /dev/null +++ b/examples/gemma4/e2b-vision-lora.yaml @@ -0,0 +1,62 @@ +# Gemma 4 E2B Vision LoRA +# +# Fine-tuning LM LoRA adapters on multimodal Gemma4 with vision/multimodal modules frozen. +# Uses the base ProcessingStrategy (auto-detects image_token from processor). + +base_model: google/gemma-4-E2B-it +processor_type: AutoProcessor +freeze_mm_modules: true + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin +strict: false + +# Required for vision/multimodal training +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +chat_template: gemma4 +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:100] + +val_set_size: 0 +output_dir: ./outputs/gemma4-e2b-vision-lora + +adapter: lora +sequence_len: 2048 +pad_to_sequence_len: false + +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0 +# Target language model only — vision encoder is frozen via freeze_mm_modules +lora_target_modules: 'model.language_model.layers.[\d]+.(_checkpoint_wrapped_module.)?(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +max_steps: 10 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +logging_steps: 1 +sdp_attention: true + +warmup_ratio: 0.1 +weight_decay: 0.0 + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: diff --git a/examples/qwen3.5/35b-a3b-moe-vision-lora.yaml b/examples/qwen3.5/35b-a3b-moe-vision-lora.yaml new file mode 100644 index 0000000000..a7c85f785f --- /dev/null +++ b/examples/qwen3.5/35b-a3b-moe-vision-lora.yaml @@ -0,0 +1,62 @@ +# Qwen 3.5 35B-A3B MoE Vision LoRA +# +# Vision fine-tuning of the hybrid DeltaNet + Attention MoE model. +# 256 experts, 8 active per token, with early-fusion vision support. + +base_model: Qwen/Qwen3.5-35B-A3B +processor_type: AutoProcessor + +# Required for vision/multimodal training +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +chat_template: qwen3_5 +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:100] + +val_set_size: 0 +output_dir: ./outputs/qwen35-35b-a3b-vision-lora + +adapter: lora +sequence_len: 4096 +pad_to_sequence_len: false + +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - down_proj + - up_proj + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +max_steps: 10 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +weight_decay: 0.0 + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: diff --git a/scripts/analyze_profile.py b/scripts/analyze_profile.py new file mode 100644 index 0000000000..df922502fe --- /dev/null +++ b/scripts/analyze_profile.py @@ -0,0 +1,1518 @@ +#!/usr/bin/env python3 +""" +Axolotl Training Profiler Analyzer +=================================== + +Analyzes PyTorch profiler output from axolotl training runs (profiler_steps config). +Produces breakdowns by CUDA kernel category, identifies bottlenecks, and optionally +compares two traces (e.g. before/after optimization). + +Supports both: + - profiler_trace.json (torch.profiler Chrome trace -- timing analysis) + - snapshot.pickle (torch.cuda.memory._snapshot -- memory analysis) + +Usage: + # Analyze a single trace + python analyze_profile.py outputs/qwen35_moe_profile/ + + # Compare before vs after + python analyze_profile.py outputs/before/ --compare outputs/after/ + + # Include step 0 (warmup/compilation) in analysis + python analyze_profile.py outputs/run/ --include-warmup + + # Memory-only analysis + python analyze_profile.py outputs/run/ --memory-only + + # Quick mode (first 2M events only, for large traces) + python analyze_profile.py outputs/run/ --quick +""" + +import argparse +import json +import pickle # nosec B403 +import time +from collections import defaultdict +from pathlib import Path + +# ---- Kernel categorization ------------------------------------------------ + +KERNEL_CATEGORIES = [ + # ScatterMoE -- ordered most specific first + ( + "ScatterMoE bwd LoRA (split)", + ["_group_bwd_lora_split", "_group_bwd_da", "_group_bwd_db"], + ), + ("ScatterMoE bwd LoRA (fused)", ["_group_bwd_lora"]), + ("ScatterMoE bwd dX", ["_scatter2scatter_lora_dx"]), + ("ScatterMoE fwd", ["_scatter2scatter_lora"]), + # Quantization + ("BnB Dequantization", ["dequantize", "kDequantizeBlockwise"]), + # Attention + ("Flash Attention", ["flash", "fmha"]), + # Loss + ("CCE Loss", ["_cce_"]), + # LoRA fused kernels (autograd.Function based) + ("LoRA QKV Kernel", ["lora_qkv"]), + ("LoRA O Kernel", ["lora_o"]), + ("LoRA MLP Kernel", ["lora_mlp"]), + # LoRA activation kernels (SwiGLU/GEGLU Triton kernels) + ("LoRA Activation (SwiGLU/GEGLU)", ["swiglu", "geglu"]), + # DoRA weight norm + ("DoRA Weight Norm", ["linalg_norm", "dora_scale"]), + # Compute + ("GEMM/CUTLASS", ["cutlass", "gemm", "gemv", "cublas"]), + ("Triton (norms etc)", ["triton"]), + ("Conv1d", ["conv1d", "causal_conv"]), + # Optimizer + ("Optimizer", ["adam", "optim"]), + # Dtype conversion (fp32→bf16 LoRA matrix casts etc) + ("Dtype Conversion", ["_to_copy", "to_copy"]), + # Memory + ("Elementwise/Fill", ["fill", "elementwise", "cast", "copy_kernel"]), + ("Memory ops", ["memcpy", "memset"]), + # Routing + ("TopK/Sort", ["topk", "sort"]), + ("Index/Gather/Scatter", ["index", "gather", "scatter"]), +] + +# Categories to keep when pre-filtering during streaming load +_KEEP_CATS = {"kernel", "gpu_memcpy", "cpu_op", "python_function", "ac2g", "Runtime"} + + +def categorize_kernel(name): + nl = name.lower() + for cat_name, patterns in KERNEL_CATEGORIES: + if any(p in nl for p in patterns): + return cat_name + return "Other" + + +# ---- Trace loading --------------------------------------------------------- + + +def _try_ijson_load(trace_file, quick=False, max_events=2_000_000): + """Stream-parse trace JSON with ijson. Returns filtered events list.""" + try: + import ijson + except ImportError: + return None + + events = [] + count = 0 + with open(trace_file, "rb") as f: + for ev in ijson.items(f, "traceEvents.item"): + count += 1 + cat = ev.get("cat", "") + # Pre-filter: only keep categories we care about + if cat in _KEEP_CATS or ev.get("ph") == "M": + events.append(ev) + if quick and count >= max_events: + print(f" --quick: stopped after {count:,} events") + break + return events, count + + +def load_trace(path, quick=False): + trace_file = ( + Path(path) / "profiler_trace.json" if Path(path).is_dir() else Path(path) + ) + if not trace_file.exists(): + return None + size_gb = trace_file.stat().st_size / 1e9 + print(f"Loading {trace_file.name} ({size_gb:.1f} GB)...") + t0 = time.monotonic() + + # Try streaming parser first for large files + if size_gb > 0.5: + result = _try_ijson_load(trace_file, quick=quick) + if result is not None: + events, total_count = result + elapsed = time.monotonic() - t0 + print( + f" {total_count:,} total events, {len(events):,} kept " + f"(streamed in {elapsed:.1f}s)" + ) + return events + + # Fallback: standard json.load + with open(trace_file) as f: + data = json.load(f) + all_events = data.get("traceEvents", []) + elapsed = time.monotonic() - t0 + + if quick: + all_events = all_events[:2_000_000] + print(f" --quick: limited to first {len(all_events):,} events") + + # Pre-filter + events = [ + ev + for ev in all_events + if ev.get("cat", "") in _KEEP_CATS or ev.get("ph") == "M" + ] + print( + f" {len(all_events):,} total events, {len(events):,} kept " + f"(loaded in {elapsed:.1f}s)" + ) + return events + + +# ---- Trace analysis ------------------------------------------------------- + + +def _estimate_n_steps(cuda_events): + """Estimate the number of training steps from CUDA event timestamps. + + Detects step boundaries by looking for large gaps (>2x median gap) in the + sorted timestamp sequence of CUDA kernels. + """ + if len(cuda_events) < 100: + return 1 + timestamps = sorted(float(ev.get("ts", 0)) for ev in cuda_events) + # Compute gaps between consecutive events + gaps = [timestamps[i + 1] - timestamps[i] for i in range(len(timestamps) - 1)] + if not gaps: + return 1 + median_gap = sorted(gaps)[len(gaps) // 2] + # A step boundary has a gap much larger than the median inter-kernel gap + threshold = max(median_gap * 50, 100_000) # at least 100ms + n_boundaries = sum(1 for g in gaps if g > threshold) + return max(n_boundaries + 1, 1) + + +def analyze_trace(events, skip_warmup=True): + cuda_events = [ + ev + for ev in events + if ev.get("ph") == "X" and ev.get("cat") in ("kernel", "gpu_memcpy") + ] + if not cuda_events: + print(" No CUDA kernel events found!") + return None + + cutoff_ts = None + + if skip_warmup and len(cuda_events) > 1000: + timestamps = sorted(set(float(ev.get("ts", 0)) for ev in cuda_events)) + min_ts, max_ts = timestamps[0], timestamps[-1] + total_span = max_ts - min_ts + + # Step 0 is warmup (Triton compilation + autotune). It's typically + # the slowest step by far. Use 45% of wall-clock as cutoff -- step 0 + # usually takes >50% of total time when it includes compilation. + cutoff_ts = min_ts + total_span * 0.45 + before = len(cuda_events) + cuda_events = [ev for ev in cuda_events if float(ev.get("ts", 0)) > cutoff_ts] + print(f" Excluding step 0 (warmup): {before:,} -> {len(cuda_events):,} events") + + n_steps_profiled = _estimate_n_steps(cuda_events) + + # Aggregate by kernel (cast to float to handle ijson Decimal values) + kernel_stats = defaultdict(lambda: {"total_us": 0.0, "count": 0, "max_us": 0.0}) + for ev in cuda_events: + name = ev.get("name", "unknown") + dur = float(ev.get("dur", 0)) + kernel_stats[name]["total_us"] += dur + kernel_stats[name]["count"] += 1 + kernel_stats[name]["max_us"] = max(kernel_stats[name]["max_us"], dur) + + total_cuda = sum(v["total_us"] for v in kernel_stats.values()) + + # Group by category + cat_stats = defaultdict(lambda: {"total_us": 0, "count": 0}) + for name, info in kernel_stats.items(): + cat = categorize_kernel(name) + cat_stats[cat]["total_us"] += info["total_us"] + cat_stats[cat]["count"] += info["count"] + + # Fill/zero_ analysis: find FillFunctor kernels and group by tensor size + fill_by_size = defaultdict(lambda: {"total_us": 0, "count": 0}) + for ev in cuda_events: + name = ev.get("name", "") + if "FillFunctor" not in name and "fill" not in name.lower(): + continue + # Extract Input Dims from args if present + args = ev.get("args", {}) + input_dims = args.get("Input Dims", args.get("input_dims", "unknown")) + if isinstance(input_dims, list): + input_dims = str(input_dims) + dur = float(ev.get("dur", 0)) + fill_by_size[input_dims]["total_us"] += dur + fill_by_size[input_dims]["count"] += 1 + + # CPU op analysis for wall-clock estimation (apply same warmup cutoff) + cpu_ops = [ + ev + for ev in events + if ev.get("ph") == "X" and ev.get("cat") in ("cpu_op", "python_function") + ] + if cutoff_ts is not None: + cpu_ops = [ev for ev in cpu_ops if float(ev.get("ts", 0)) > cutoff_ts] + wall_clock_us = 0 + if cpu_ops: + ts_sorted = sorted(cpu_ops, key=lambda e: float(e.get("ts", 0))) + min_cpu_ts = float(ts_sorted[0].get("ts", 0)) + max_cpu_end = max( + float(e.get("ts", 0)) + float(e.get("dur", 0)) for e in ts_sorted + ) + wall_clock_us = max_cpu_end - min_cpu_ts + + return { + "total_cuda_us": total_cuda, + "n_steps": n_steps_profiled, + "categories": dict(cat_stats), + "kernel_stats": dict(kernel_stats), + "n_events": len(cuda_events), + "fill_by_size": dict(fill_by_size), + "wall_clock_us": wall_clock_us, + } + + +def print_trace_analysis(result, label=""): + total = result["total_cuda_us"] + n = result["n_steps"] + + if label: + print(f"\n{'=' * 75}") + print(f" {label}") + print(f"{'=' * 75}") + + print( + f"\n CUDA kernel time: {total / 1e6:.2f}s over {n} steps " + f"(~{total / n / 1e6:.2f}s/step)" + ) + + if result.get("wall_clock_us"): + wc = result["wall_clock_us"] + print( + f" Wall clock span: {wc / 1e6:.2f}s over {n} steps " + f"(~{wc / n / 1e6:.2f}s/step)" + ) + + print(f"\n {'Category':<40} {'Total':>9} {'%':>6} {'Count':>7} {'Per step':>9}") + print(f" {'-' * 75}") + for cat, info in sorted( + result["categories"].items(), key=lambda x: x[1]["total_us"], reverse=True + ): + pct = info["total_us"] / total * 100 + ps = info["total_us"] / n / 1000 + print( + f" {cat:<40} {info['total_us'] / 1000:>8.1f}ms {pct:>5.1f}% " + f"{info['count']:>7} {ps:>7.1f}ms" + ) + + print("\n Top 15 individual kernels:") + for name, info in sorted( + result["kernel_stats"].items(), key=lambda x: x[1]["total_us"], reverse=True + )[:15]: + pct = info["total_us"] / total * 100 + avg = info["total_us"] / info["count"] / 1000 + print( + f" {name[:62]:<62} {info['total_us'] / 1000:>7.1f}ms " + f"({pct:>4.1f}%) x{info['count']:<5} avg={avg:.3f}ms" + ) + + # Fill/zero_ breakdown + fill_data = result.get("fill_by_size", {}) + if fill_data: + total_fill_us = sum(v["total_us"] for v in fill_data.values()) + if total_fill_us > 0: + print( + f"\n Fill/zero_ kernel breakdown by tensor size " + f"(total: {total_fill_us / 1000:.1f}ms, " + f"{total_fill_us / total * 100:.1f}% of CUDA time):" + ) + print(f" {'Input Dims':<50} {'Time':>9} {'Count':>7}") + print(f" {'-' * 70}") + for dims, info in sorted( + fill_data.items(), key=lambda x: x[1]["total_us"], reverse=True + )[:10]: + dims_str = str(dims)[:50] + print( + f" {dims_str:<50} {info['total_us'] / 1000:>7.1f}ms " + f"{info['count']:>7}" + ) + + +def print_summary(result, mem_result=None): + """Print optimization recommendations and summary.""" + total = result["total_cuda_us"] + n = result["n_steps"] + + print(f"\n{'=' * 75}") + print(" SUMMARY & RECOMMENDATIONS") + print(f"{'=' * 75}") + + # Estimated per-step wall clock + wc = result.get("wall_clock_us", 0) + if wc > 0: + print(f"\n Estimated per-step wall clock: {wc / n / 1e6:.2f}s") + else: + print(f"\n Estimated per-step CUDA time: {total / n / 1e6:.2f}s") + + # Memory utilization + if mem_result: + reserved = mem_result["total_reserved"] + allocated = mem_result["total_allocated"] + if reserved > 0: + util_pct = allocated / reserved * 100 + print( + f" Memory utilization: {util_pct:.1f}% " + f"({allocated / 1e9:.2f} / {reserved / 1e9:.2f} GB)" + ) + + # Build recommendations + recommendations = [] + + cats_sorted = sorted( + result["categories"].items(), key=lambda x: x[1]["total_us"], reverse=True + ) + + # Check top category + if cats_sorted: + top_cat, top_info = cats_sorted[0] + top_pct = top_info["total_us"] / total * 100 + if top_pct > 30: + if "GEMM" in top_cat or "CUTLASS" in top_cat: + recommendations.append( + f"GEMM/matmul dominates ({top_pct:.0f}%). " + f"Consider FP8 training, LoRA (fewer params), " + f"or smaller batch size to reduce compute." + ) + elif "Attention" in top_cat: + recommendations.append( + f"Attention dominates ({top_pct:.0f}%). " + f"Ensure FlashAttention v2/v3 is active. " + f"Consider reducing sequence length or using sliding window." + ) + elif "Fill" in top_cat or "Elementwise" in top_cat: + recommendations.append( + f"Elementwise/Fill ops dominate ({top_pct:.0f}%). " + f"Enable kernel fusion (Liger, torch.compile) to reduce " + f"memory-bound elementwise operations." + ) + elif "ScatterMoE" in top_cat: + recommendations.append( + f"MoE routing/scatter dominates ({top_pct:.0f}%). " + f"Check expert count and capacity factor. " + f"Verify ScatterMoE kernels are using optimal block sizes." + ) + else: + recommendations.append( + f"'{top_cat}' dominates ({top_pct:.0f}%). " + f"Focus optimization efforts here first." + ) + + # Check memory ops + for cat, info in cats_sorted: + pct = info["total_us"] / total * 100 + if "Memory" in cat and pct > 10: + recommendations.append( + f"Memory ops are {pct:.0f}% of CUDA time. " + f"Consider gradient checkpointing, reducing activation " + f"recomputation, or pinned memory for data loading." + ) + break + + # Check fill overhead + fill_data = result.get("fill_by_size", {}) + total_fill_us = sum(v["total_us"] for v in fill_data.values()) + fill_pct = total_fill_us / total * 100 if total > 0 else 0 + if fill_pct > 5: + recommendations.append( + f"Fill/zero_ kernels consume {fill_pct:.1f}% of CUDA time. " + f"Large zero-fills suggest excessive tensor allocation. " + f"Consider reusing buffers or lazy initialization." + ) + + # Check fragmentation + if mem_result and mem_result.get("fragmentation_pct", 0) > 20: + frag = mem_result["fragmentation_pct"] + recommendations.append( + f"Memory fragmentation is {frag:.0f}%. " + f"Use PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True " + f"or max_split_size_mb to reduce fragmentation." + ) + + # GPU utilization hint from wall clock vs CUDA time + if wc > 0 and total > 0: + gpu_util = total / wc * 100 + if gpu_util < 50: + recommendations.append( + f"GPU utilization is ~{gpu_util:.0f}% (CUDA time vs wall clock). " + f"CPU-side bottleneck likely. Profile data loading, " + f"reward computation, or weight sync overhead." + ) + + # Print top 3 + print("\n Top optimization recommendations:") + if not recommendations: + print(" No major issues detected.") + for i, rec in enumerate(recommendations[:3], 1): + print(f" {i}. {rec}") + + print() + + +def compare_traces(before, after): + print(f"\n{'=' * 75}") + print(" COMPARISON") + print(f"{'=' * 75}") + + tb = before["total_cuda_us"] + ta = after["total_cuda_us"] + nb = before["n_steps"] + na = after["n_steps"] + + print( + f"\n Per-step CUDA time: {tb / nb / 1e6:.2f}s -> {ta / na / 1e6:.2f}s " + f"({(ta / na - tb / nb) / (tb / nb) * 100:+.1f}%)" + ) + + all_cats = sorted( + set(list(before["categories"]) + list(after["categories"])), + key=lambda c: before["categories"].get(c, {"total_us": 0})["total_us"], + reverse=True, + ) + + print( + f"\n {'Category':<35} {'Before/step':>11} {'After/step':>11} " + f"{'Delta':>9} {'Speedup':>8}" + ) + print(f" {'-' * 78}") + for cat in all_cats: + b = before["categories"].get(cat, {"total_us": 0})["total_us"] / nb + a = after["categories"].get(cat, {"total_us": 0})["total_us"] / na + delta = a - b + speedup = b / a if a > 0 else float("inf") + if b > tb / nb * 0.003 or a > ta / na * 0.003: + print( + f" {cat:<35} {b / 1000:>9.1f}ms {a / 1000:>9.1f}ms " + f"{delta / 1000:>+8.1f}ms {speedup:>7.2f}x" + ) + + +# ---- Allocation churn attribution ----------------------------------------- + + +def _short_path(p): + """Shorten a file path for display.""" + if "/site-packages/" in p: + return "..." + p.split("/site-packages/")[1] + if "/axolotl/src/" in p: + return "axolotl/" + p.split("/axolotl/src/")[1] + if "/axolotl/" in p: + return "axolotl/" + p.split("/axolotl/")[1] + if len(p) > 60: + parts = p.split("/") + return ".../" + "/".join(parts[-3:]) + return p + + +def _get_top_python_frame(frames): + """Get the first meaningful Python frame from a trace frame list. + + Skips internal torch/cuda/autograd frames to find the user-level code + that triggered the allocation. Falls back to first Python frame. + """ + skip_patterns = [ + "torch/autograd/", + "torch/utils/checkpoint", + "torch/nn/modules/module.py", + "torch/_dynamo/", + "torch/_compile", + "torch/_ops", + "torch/_functorch/", + "torch/_inductor/", + "torch/library", + "torch/cuda", + "torch/_C", + "", + "fire/core.py", + "runpy", + ] + py_frames = [ + f + for f in frames + if isinstance(f, dict) and f.get("filename", "").endswith(".py") + ] + for fr in py_frames: + fname = fr.get("filename", "") + if not any(p in fname for p in skip_patterns): + return fr + return py_frames[0] if py_frames else None + + +def _categorize_source(fname, funcname): + """Assign a human-readable category to an allocation source.""" + if "checkpoint" in fname.lower() or "backward" in funcname: + return "Gradient checkpoint recompute" + if "bitsandbytes" in fname: + return "BnB dequantization" + if "scattermoe" in fname or "scatter" in funcname.lower(): + return "ScatterMoE LoRA" + if "adam" in funcname.lower() or "optim" in fname.lower() or "inductor" in fname: + return "Optimizer" + if "norm" in funcname.lower(): + return "LayerNorm" + if "fla/" in fname or "gated_delta" in fname: + return "FLA linear attention" + return "Other" + + +def analyze_allocation_churn(snapshot): + """Group allocation churn by Python source attribution. + + For each major churn size, identifies which Python code creates + the tensors (gradient checkpointing, BnB dequant, LoRA conversion, etc). + """ + traces = snapshot.get("device_traces", [[]])[0] + if not traces: + return None + + # Find the top churn sizes + size_counts = defaultdict(int) + for ev in traces: + if ev.get("action") == "alloc": + size_counts[ev["size"]] += 1 + + # Top 5 sizes by total churn (count × size) + top_sizes = sorted(size_counts.items(), key=lambda x: x[0] * x[1], reverse=True)[:5] + + results = {} + for target_sz, total_count in top_sizes: + mb = target_sz / 1e6 + if mb < 1.0: + continue + + frame_groups = defaultdict(lambda: {"count": 0}) + for ev in traces: + if ev.get("action") == "alloc" and ev.get("size") == target_sz: + top = _get_top_python_frame(ev.get("frames", [])) + if top: + key = (top["filename"], top["name"], top.get("line", 0)) + else: + key = ("", "", 0) + frame_groups[key]["count"] += 1 + + # Group into categories + categories = defaultdict(lambda: {"count": 0, "sources": []}) + for (fname, funcname, lineno), info in frame_groups.items(): + cat = _categorize_source(fname, funcname) + categories[cat]["count"] += info["count"] + categories[cat]["sources"].append( + (funcname, _short_path(fname), lineno, info["count"]) + ) + + results[target_sz] = { + "total_count": total_count, + "total_churn_gb": total_count * target_sz / 1e9, + "categories": dict(categories), + } + + return results + + +def print_allocation_churn(results, label=""): + if not results: + return + + if label: + print(f"\n{'=' * 75}") + print(f" {label}") + print(f"{'=' * 75}") + else: + print("\n ALLOCATION CHURN BY SOURCE") + + for target_sz in sorted( + results, key=lambda s: results[s]["total_churn_gb"], reverse=True + ): + info = results[target_sz] + mb = target_sz / 1e6 + print( + f"\n {mb:.1f}MB × {info['total_count']:,} = {info['total_churn_gb']:.0f}GB churn:" + ) + for cat, cinfo in sorted( + info["categories"].items(), key=lambda x: x[1]["count"], reverse=True + ): + pct = cinfo["count"] / info["total_count"] * 100 + print(f" {pct:>4.0f}% {cat} ({cinfo['count']} allocs)") + for funcname, short, lineno, cnt in sorted( + cinfo["sources"], key=lambda x: x[3], reverse=True + )[:3]: + print(f" {funcname:35s} {short}:{lineno} (x{cnt})") + + +# ---- CPU overhead analysis ------------------------------------------------ + + +def analyze_cpu_overhead(events): + """Analyze memcpy, checkpoint recomputation, and GPU utilization from trace events.""" + cuda_total_us = 0 + cuda_count = 0 + memcpy_stats = defaultdict(lambda: {"dur_us": 0, "count": 0, "bytes": 0}) + checkpoint_us = 0 + checkpoint_count = 0 + min_ts = float("inf") + max_ts_end = 0 + + for ev in events: + if ev.get("ph") != "X": + continue + cat = ev.get("cat", "") + name = ev.get("name", "") + dur = float(ev.get("dur", 0)) + ts = float(ev.get("ts", 0)) + end = ts + dur + if ts < min_ts: + min_ts = ts + if end > max_ts_end: + max_ts_end = end + + if cat == "kernel": + cuda_total_us += dur + cuda_count += 1 + elif cat == "gpu_memcpy": + if "DtoH" in name: + direction = "GPU→CPU (offload)" + elif "HtoD" in name: + direction = "CPU→GPU (reload)" + elif "DtoD" in name: + direction = "GPU→GPU" + else: + direction = name + memcpy_stats[direction]["dur_us"] += dur + memcpy_stats[direction]["count"] += 1 + nbytes = ev.get("args", {}).get("Bytes", 0) + if nbytes: + memcpy_stats[direction]["bytes"] += int(nbytes) + elif cat in ("cpu_op", "python_function"): + nl = name.lower() + if "checkpoint" in nl or "recompute" in nl: + checkpoint_us += dur + checkpoint_count += 1 + + wall_us = max_ts_end - min_ts if max_ts_end > min_ts else 0 + + return { + "wall_clock_us": wall_us, + "cuda_total_us": cuda_total_us, + "cuda_kernel_count": cuda_count, + "memcpy_stats": dict(memcpy_stats), + "checkpoint_us": checkpoint_us, + "checkpoint_count": checkpoint_count, + } + + +def print_cpu_overhead(result, n_steps=2, label=""): + if not result: + return + if label: + print(f"\n{'=' * 75}") + print(f" {label}") + print(f"{'=' * 75}") + else: + print("\n CPU OVERHEAD ANALYSIS") + + wall = result["wall_clock_us"] + cuda = result["cuda_total_us"] + count = result["cuda_kernel_count"] + gpu_util = cuda / wall * 100 if wall > 0 else 0 + cpu_gap = wall - cuda + + print( + f"\n Wall clock: {wall / 1e6:.2f}s (~{wall / n_steps / 1e6:.2f}s/step)" + ) + print(f" CUDA kernel time: {cuda / 1e6:.2f}s ({count:,} kernels)") + print(f" GPU utilization: {gpu_util:.1f}%") + print(f" CPU overhead: {cpu_gap / 1e6:.2f}s ({100 - gpu_util:.1f}%)") + + memcpy = result["memcpy_stats"] + if memcpy: + total_memcpy = sum(v["dur_us"] for v in memcpy.values()) + total_bytes = sum(v["bytes"] for v in memcpy.values()) + print( + f"\n Memory transfers: {total_memcpy / 1e6:.3f}s " + f"({total_bytes / 1e9:.2f}GB, {total_memcpy / wall * 100:.1f}% of wall)" + ) + for direction, info in sorted( + memcpy.items(), key=lambda x: x[1]["dur_us"], reverse=True + ): + gb = info["bytes"] / 1e9 + print( + f" {direction:30s} {info['dur_us'] / 1e6:.3f}s " + f"x{info['count']:>5} {gb:.2f}GB" + ) + + if result["checkpoint_count"] > 0: + print( + f"\n Gradient checkpoint CPU ops: {result['checkpoint_us'] / 1e6:.3f}s " + f"(x{result['checkpoint_count']})" + ) + + +# ---- Memory snapshot analysis --------------------------------------------- + + +def load_snapshot(path): + """Load a PyTorch CUDA memory snapshot from a pickle file. + + WARNING: This uses pickle.load() which can execute arbitrary code. + Only load snapshot files that you generated yourself from trusted + training runs. Never load snapshots from untrusted sources. + """ + snap_file = Path(path) / "snapshot.pickle" if Path(path).is_dir() else Path(path) + if not snap_file.exists(): + return None + print(f"Loading {snap_file.name} ({snap_file.stat().st_size / 1e6:.0f} MB)...") + with open(snap_file, "rb") as f: + return pickle.load(f) # nosec B301 + + +def _extract_python_frames(snapshot): + """Extract Python source attribution from snapshot blocks with stacks='all'. + + The snapshot structure (when stacks='all') stores frames in: + segments[i].blocks[j].history[k].frames = [(filename, lineno, name), ...] + + Returns a dict mapping (filename, function_name) -> {"bytes": int, "count": int} + """ + source_allocs = defaultdict(lambda: {"bytes": 0, "count": 0}) + + for seg in snapshot.get("segments", []): + for block in seg.get("blocks", []): + if block.get("state") != "active_allocated": + continue + size = block.get("size", 0) + history = block.get("history", []) + if not history: + continue + + # Use the most recent allocation history entry + last_hist = history[-1] + frames = last_hist.get("frames", []) + + # Find the first Python frame (skip C++ frames) + # Frames are tuples: (filename, lineno, name) + attributed = False + for frame in frames: + if not isinstance(frame, (list, tuple)) or len(frame) < 3: + continue + filename, lineno, funcname = frame[0], frame[1], frame[2] + # Skip internal torch/cuda frames to find user-level attribution + fname_str = str(filename) + if any( + skip in fname_str + for skip in [ + "torch/cuda", + "torch/_C", + "torch/utils", + "cuda/memory.py", + "", + ] + ): + continue + key = (fname_str, funcname, lineno) + source_allocs[key]["bytes"] += size + source_allocs[key]["count"] += 1 + attributed = True + break + + # If no user frame found, use first available frame + if not attributed and frames: + frame = frames[0] + if isinstance(frame, (list, tuple)) and len(frame) >= 3: + key = (str(frame[0]), str(frame[2]), frame[1]) + source_allocs[key]["bytes"] += size + source_allocs[key]["count"] += 1 + + return dict(source_allocs) + + +def _extract_source_file_summary(source_allocs): + """Aggregate per-frame allocations to per-file level.""" + file_allocs = defaultdict(lambda: {"bytes": 0, "count": 0, "functions": set()}) + for (filename, funcname, _lineno), info in source_allocs.items(): + file_allocs[filename]["bytes"] += info["bytes"] + file_allocs[filename]["count"] += info["count"] + file_allocs[filename]["functions"].add(funcname) + return dict(file_allocs) + + +def analyze_snapshot(snapshot): + segments = snapshot.get("segments", []) + total_reserved = sum(s.get("total_size", 0) for s in segments) + total_allocated = sum(s.get("allocated_size", 0) for s in segments) + + # Active blocks + active_blocks = [] + for seg in segments: + for block in seg.get("blocks", []): + if block.get("state") == "active_allocated": + active_blocks.append(block.get("size", 0)) + + # Allocation churn from trace + trace = snapshot.get("device_traces", [[]])[0] + size_counts = defaultdict(lambda: {"count": 0, "total": 0}) + for ev in trace: + if ev.get("action") == "alloc": + sz = ev.get("size", 0) + size_counts[sz]["count"] += 1 + size_counts[sz]["total"] += sz + + # Python frame attribution + source_allocs = _extract_python_frames(snapshot) + file_summary = _extract_source_file_summary(source_allocs) + + return { + "total_reserved": total_reserved, + "total_allocated": total_allocated, + "fragmentation_pct": (total_reserved - total_allocated) / total_reserved * 100 + if total_reserved > 0 + else 0, + "n_segments": len(segments), + "n_active_blocks": len(active_blocks), + "active_bytes": sum(active_blocks), + "largest_active": sorted(active_blocks, reverse=True)[:10], + "alloc_churn": dict( + sorted(size_counts.items(), key=lambda x: x[1]["total"], reverse=True)[:15] + ), + "n_trace_events": len(trace), + "source_allocs": source_allocs, + "file_summary": file_summary, + } + + +def print_memory_analysis(result, label=""): + if label: + print(f"\n{'=' * 75}") + print(f" {label}") + print(f"{'=' * 75}") + + reserved = result["total_reserved"] + allocated = result["total_allocated"] + + print(f"\n Reserved: {reserved / 1e9:.2f} GB") + print(f" Allocated: {allocated / 1e9:.2f} GB") + print(f" Utilization: {allocated / reserved * 100:.1f}%" if reserved > 0 else "") + print(f" Fragmentation: {result['fragmentation_pct']:.1f}%") + print( + f" Segments: {result['n_segments']}, Active blocks: {result['n_active_blocks']}" + ) + + print("\n Largest active allocations:") + for sz in result["largest_active"]: + print(f" {sz / 1e6:>10.1f} MB") + + # Python source file attribution + file_summary = result.get("file_summary", {}) + if file_summary: + print("\n Top allocations by source file:") + print(f" {'Source file':<55} {'Alloc':>10} {'Count':>7}") + print(f" {'-' * 75}") + for fname, info in sorted( + file_summary.items(), key=lambda x: x[1]["bytes"], reverse=True + )[:15]: + # Shorten path for display + short = fname + if len(short) > 55: + parts = short.split("/") + # Keep last 3 path components + short = ".../" + "/".join(parts[-3:]) + if len(short) > 55: + short = short[:52] + "..." + funcs = ", ".join(sorted(info["functions"])[:3]) + sz = info["bytes"] + unit = "MB" + val = sz / 1e6 + if val >= 1000: + unit = "GB" + val = sz / 1e9 + print(f" {short:<55} {val:>8.1f}{unit} {info['count']:>7}") + if funcs: + print(f" functions: {funcs[:70]}") + + # Top allocations by function (more granular) + source_allocs = result.get("source_allocs", {}) + if source_allocs: + print("\n Top allocations by function (with line numbers):") + print(f" {'Function':<35} {'File:Line':<35} {'Size':>10}") + print(f" {'-' * 82}") + for (fname, funcname, lineno), info in sorted( + source_allocs.items(), key=lambda x: x[1]["bytes"], reverse=True + )[:15]: + # Shorten filename + short_file = fname + parts = short_file.split("/") + if len(parts) > 2: + short_file = "/".join(parts[-2:]) + loc = f"{short_file}:{lineno}" + if len(loc) > 35: + loc = "..." + loc[-32:] + sz = info["bytes"] + if sz >= 1e9: + sz_str = f"{sz / 1e9:.2f}GB" + else: + sz_str = f"{sz / 1e6:.1f}MB" + print(f" {funcname:<35} {loc:<35} {sz_str:>10}") + + print("\n Top allocation churn (alloc count x size):") + print(f" {'Size':>12} {'Count':>8} {'Total churned':>14}") + print(f" {'-' * 38}") + for sz, info in result["alloc_churn"].items(): + if sz >= 1e6: + print( + f" {sz / 1e6:>10.1f}MB {info['count']:>8} " + f"{info['total'] / 1e9:>12.2f}GB" + ) + + +# ---- Peak memory timeline from trace events -------------------------------- + + +def analyze_peak_memory(snapshot): + """Walk through device_traces chronologically to find peak concurrent memory usage. + + The snapshot's segment data only captures end-of-step state. The device_traces + record every alloc/free, letting us reconstruct peak usage and identify which + allocation sources were live at that moment. + """ + traces = snapshot.get("device_traces", [[]])[0] + if not traces: + return None + + current = 0 + peak = 0 + peak_idx = 0 + live_allocs = {} # addr -> (size, frames) + peak_live = {} + + for i, ev in enumerate(traces): + action = ev.get("action") + addr = ev.get("addr", 0) + size = ev.get("size", 0) + + if action == "alloc": + current += size + live_allocs[addr] = (size, ev.get("frames", [])) + if current > peak: + peak = current + peak_idx = i + peak_live = dict(live_allocs) + elif action == "free_requested": + if addr in live_allocs: + current -= live_allocs[addr][0] + del live_allocs[addr] + + # Categorize allocations at peak + peak_categories = defaultdict(lambda: {"bytes": 0, "count": 0}) + for _addr, (size, frames) in peak_live.items(): + top = _get_top_python_frame(frames) + if top: + cat = _categorize_source(top["filename"], top["name"]) + else: + cat = "Unknown" + peak_categories[cat]["bytes"] += size + peak_categories[cat]["count"] += 1 + + return { + "peak_bytes": peak, + "peak_event_idx": peak_idx, + "total_events": len(traces), + "end_bytes": current, + "peak_categories": dict(peak_categories), + } + + +def print_peak_memory(result, mem_result=None, label=""): + if not result: + return + + if label: + print(f"\n{'=' * 75}") + print(f" {label}") + print(f"{'=' * 75}") + + peak_gb = result["peak_bytes"] / 1e9 + end_gb = result["end_bytes"] / 1e9 + # The device_traces only record allocations AFTER profiling starts. + # Model weights and other persistent allocations are not tracked. + # We can estimate the persistent baseline from snapshot allocated - peak_traced. + persistent_gb = 0 + if mem_result: + persistent_gb = mem_result["total_allocated"] / 1e9 - end_gb + total_peak_gb = persistent_gb + peak_gb + + print( + f"\n Profiled peak (transient): {peak_gb:.2f} GB " + f"(at event {result['peak_event_idx']:,} / {result['total_events']:,})" + ) + if persistent_gb > 0: + print( + f" Persistent baseline: {persistent_gb:.2f} GB " + f"(model + optimizer, allocated before profiling)" + ) + print(f" Estimated total peak: {total_peak_gb:.2f} GB") + print(f" Transient headroom: {peak_gb - end_gb:.2f} GB above end-of-trace") + + cats = result.get("peak_categories", {}) + if cats: + print("\n Allocations live at peak:") + print(f" {'Category':<35} {'Size':>10} {'Count':>7}") + print(f" {'-' * 55}") + for cat, info in sorted( + cats.items(), key=lambda x: x[1]["bytes"], reverse=True + ): + sz = info["bytes"] + if sz >= 1e9: + sz_str = f"{sz / 1e9:.2f} GB" + else: + sz_str = f"{sz / 1e6:.1f} MB" + print(f" {cat:<35} {sz_str:>10} {info['count']:>7}") + + +# ---- Fragmentation diagnosis ----------------------------------------------- + + +def analyze_fragmentation(snapshot): + """Analyze segment-level memory layout to explain fragmentation. + + Examines each CUDA segment for inactive (freed but unreturned) blocks, + pinned small allocations that prevent segment merging, and the overall + segment size distribution. + """ + segments = snapshot.get("segments", []) + if not segments: + return None + + total_reserved = 0 + total_allocated = 0 + total_inactive = 0 + segment_sizes = [] + inactive_gaps = [] # (gap_size, segment_size, active_around) + pinned_fragments = [] # small active blocks surrounded by inactive + + for seg in segments: + seg_size = seg.get("total_size", 0) + total_reserved += seg_size + segment_sizes.append(seg_size) + blocks = seg.get("blocks", []) + + seg_active = 0 + seg_inactive = 0 + for bi, block in enumerate(blocks): + bsize = block.get("size", 0) + if block.get("state") == "active_allocated": + seg_active += bsize + total_allocated += bsize + # Check if this small block is surrounded by inactive + if bsize < 2 * 1024 * 1024: # < 2MB + prev_inactive = bi > 0 and blocks[bi - 1].get("state") == "inactive" + next_inactive = ( + bi < len(blocks) - 1 + and blocks[bi + 1].get("state") == "inactive" + ) + if prev_inactive and next_inactive: + pinned_fragments.append((bsize, seg_size)) + elif block.get("state") == "inactive": + seg_inactive += bsize + total_inactive += bsize + inactive_gaps.append((bsize, seg_size)) + + # Classify segment sizes + size_buckets = defaultdict(lambda: {"count": 0, "total": 0}) + for sz in segment_sizes: + if sz >= 1024 * 1024 * 1024: + bucket = ">=1 GB" + elif sz >= 256 * 1024 * 1024: + bucket = "256MB-1GB" + elif sz >= 64 * 1024 * 1024: + bucket = "64-256MB" + elif sz >= 2 * 1024 * 1024: + bucket = "2-64MB" + else: + bucket = "<2MB" + size_buckets[bucket]["count"] += 1 + size_buckets[bucket]["total"] += sz + + # Large inactive gaps that could be reclaimed + inactive_gaps.sort(key=lambda x: x[0], reverse=True) + + return { + "total_reserved": total_reserved, + "total_allocated": total_allocated, + "total_inactive": total_inactive, + "n_segments": len(segments), + "segment_size_buckets": dict(size_buckets), + "large_inactive_gaps": inactive_gaps[:20], + "pinned_fragments": len(pinned_fragments), + "expandable_segments_would_help": ( + total_inactive > 0.1 * total_reserved and len(segments) > 10 + ), + } + + +def print_fragmentation(result, gpu_capacity_gb=None, label=""): + if not result: + return + + if label: + print(f"\n{'=' * 75}") + print(f" {label}") + print(f"{'=' * 75}") + + reserved = result["total_reserved"] + allocated = result["total_allocated"] + inactive = result["total_inactive"] + frag_pct = inactive / reserved * 100 if reserved > 0 else 0 + + print( + f"\n Reserved: {reserved / 1e9:.2f} GB across {result['n_segments']} segments" + ) + print(f" Allocated: {allocated / 1e9:.2f} GB") + print(f" Inactive: {inactive / 1e9:.2f} GB ({frag_pct:.1f}% fragmentation)") + + if result["pinned_fragments"] > 0: + print( + f" Pinned small blocks (<2MB between inactive): " + f"{result['pinned_fragments']} (prevent segment merging)" + ) + + # Segment size distribution + print("\n Segment size distribution:") + bucket_order = [">=1 GB", "256MB-1GB", "64-256MB", "2-64MB", "<2MB"] + for bucket in bucket_order: + info = result["segment_size_buckets"].get(bucket) + if info: + print( + f" {bucket:<12} {info['count']:>4} segments " + f"{info['total'] / 1e9:>6.2f} GB" + ) + + # Largest inactive gaps + gaps = result.get("large_inactive_gaps", []) + if gaps: + print("\n Largest inactive gaps (freed but unreclaimable):") + shown = 0 + for gap_sz, seg_sz in gaps: + if gap_sz >= 32 * 1024 * 1024 and shown < 10: + print( + f" {gap_sz / 1e6:>8.0f} MB gap in {seg_sz / 1e6:.0f} MB segment" + ) + shown += 1 + + # OOM risk assessment + if gpu_capacity_gb: + gpu_bytes = gpu_capacity_gb * 1e9 + usable = gpu_bytes - (reserved - allocated) # capacity minus fragmented waste + print(f"\n OOM Risk Assessment (GPU: {gpu_capacity_gb:.1f} GB):") + print( + f" Usable capacity: {usable / 1e9:.2f} GB " + f"(GPU capacity minus {inactive / 1e9:.2f} GB fragmentation)" + ) + headroom = gpu_bytes - reserved + print(f" Current headroom: {headroom / 1e9:.2f} GB") + if headroom < 1.0e9: + print(" ⚠ CRITICAL: <1 GB headroom — high OOM risk!") + elif headroom < 2.0e9: + print(" ⚠ WARNING: <2 GB headroom — moderate OOM risk") + + # Recommendation + if result.get("expandable_segments_would_help"): + print("\n → FIX: Set PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True") + print(" This eliminates segment fragmentation by growing segments in-place,") + print( + f" which would reclaim up to {inactive / 1e9:.1f} GB of wasted memory." + ) + + +# ---- Sequence-length scaling analysis -------------------------------------- + + +def analyze_scaling(mem_a, mem_b, churn_a, churn_b): + """Compare per-tensor allocation sizes between two runs. + + When two profiles differ only in sequence length, this shows which tensor + categories scale with sequence length by comparing the dominant tensor sizes. + Total churn may differ due to different profiling windows, so we focus on + per-tensor size ratios instead. + """ + if not churn_a or not churn_b: + return None + + def _cat_sizes(churn): + """Map category -> {largest_size, total_bytes, count}.""" + cat_data = defaultdict(lambda: {"max_size": 0, "sizes": [], "count": 0}) + for sz, info in churn.items(): + for cat, cinfo in info.get("categories", {}).items(): + cat_data[cat]["count"] += cinfo["count"] + cat_data[cat]["sizes"].append((sz, cinfo["count"])) + if sz > cat_data[cat]["max_size"]: + cat_data[cat]["max_size"] = sz + return dict(cat_data) + + cats_a = _cat_sizes(churn_a) + cats_b = _cat_sizes(churn_b) + + all_cats = sorted( + set(list(cats_a) + list(cats_b)), + key=lambda c: max( + cats_a.get(c, {"max_size": 0})["max_size"], + cats_b.get(c, {"max_size": 0})["max_size"], + ), + reverse=True, + ) + + scaling = [] + for cat in all_cats: + a = cats_a.get(cat) + b = cats_b.get(cat) + if not a or not b: + continue + a_max = a["max_size"] + b_max = b["max_size"] + if a_max > 1e6 and b_max > 1e6: # Only compare >1MB tensors + tensor_ratio = b_max / a_max if a_max > 0 else None + scaling.append( + { + "category": cat, + "size_a_mb": a_max / 1e6, + "size_b_mb": b_max / 1e6, + "tensor_ratio": tensor_ratio, + "count_a": a["count"], + "count_b": b["count"], + "scales_with_seqlen": tensor_ratio is not None + and tensor_ratio > 1.05, + } + ) + + scaling.sort(key=lambda x: x["size_b_mb"], reverse=True) + return scaling + + +def print_scaling(scaling, label_a="Before", label_b="After", label=""): + if not scaling: + return + + if label: + print(f"\n{'=' * 75}") + print(f" {label}") + print(f"{'=' * 75}") + + print("\n Per-tensor size comparison (largest tensor per category):") + print( + f" {'Category':<35} {'A size':>10} {'B size':>10} {'Ratio':>7} {'Scales?':>8}" + ) + print(f" {'-' * 73}") + for entry in scaling: + ratio_str = f"{entry['tensor_ratio']:.2f}x" if entry["tensor_ratio"] else "N/A" + scales = "YES" if entry["scales_with_seqlen"] else "no" + print( + f" {entry['category']:<35} {entry['size_a_mb']:>8.1f}MB " + f"{entry['size_b_mb']:>8.1f}MB {ratio_str:>7} {scales:>8}" + ) + + # Summary + seq_scaling = [e for e in scaling if e["scales_with_seqlen"]] + constant = [e for e in scaling if not e["scales_with_seqlen"]] + if seq_scaling: + ratios = [e["tensor_ratio"] for e in seq_scaling if e["tensor_ratio"]] + avg_ratio = sum(ratios) / len(ratios) if ratios else 0 + print(f"\n Sequence-length scaling detected ({avg_ratio:.2f}x avg):") + for e in seq_scaling: + print( + f" - {e['category']}: {e['size_a_mb']:.1f}MB -> " + f"{e['size_b_mb']:.1f}MB ({e['tensor_ratio']:.2f}x)" + ) + if constant: + print("\n Constant-size categories (do not scale with seq len):") + for e in constant: + print(f" - {e['category']}: {e['size_a_mb']:.1f}MB") + + +# ---- Main ----------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser( + description="Analyze axolotl training profiler output", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument( + "path", + help="Path to output directory (containing profiler_trace.json and/or " + "snapshot.pickle) or directly to a trace file. " + "Security note: snapshot.pickle uses pickle deserialization — " + "only use files from your own trusted training runs.", + ) + parser.add_argument( + "--compare", + help="Path to second run for A/B comparison. " + "Same security note as path: only use trusted snapshot files.", + ) + parser.add_argument( + "--include-warmup", + action="store_true", + help="Include step 0 (warmup/compilation) in timing analysis", + ) + parser.add_argument( + "--memory-only", + action="store_true", + help="Only analyze memory snapshot, skip trace", + ) + parser.add_argument( + "--quick", + action="store_true", + help="Only load first 2M events for rapid analysis of large traces", + ) + parser.add_argument( + "--gpu-gb", + type=float, + default=None, + help="GPU total memory in GB (for OOM risk assessment). " + "Auto-detected if not specified.", + ) + args = parser.parse_args() + + # Auto-detect GPU capacity if not specified + gpu_capacity_gb = args.gpu_gb + if gpu_capacity_gb is None: + try: + import torch + + if torch.cuda.is_available(): + gpu_capacity_gb = torch.cuda.get_device_properties(0).total_memory / 1e9 + except Exception: + pass + + skip = not args.include_warmup + trace_result = None + mem_result = None + events = None + + # -- Trace analysis -- + if not args.memory_only: + events = load_trace(args.path, quick=args.quick) + if events: + trace_result = analyze_trace(events, skip_warmup=skip) + if trace_result: + print_trace_analysis(trace_result, label=f"Trace: {args.path}") + + if args.compare: + events2 = load_trace(args.compare, quick=args.quick) + if events2: + result2 = analyze_trace(events2, skip_warmup=skip) + if result2: + print_trace_analysis( + result2, label=f"Trace: {args.compare}" + ) + compare_traces(trace_result, result2) + else: + print(f" No profiler_trace.json found in {args.path}") + + # -- CPU overhead analysis (from trace) -- + if events and trace_result: + cpu_result = analyze_cpu_overhead(events) + print_cpu_overhead( + cpu_result, + n_steps=trace_result["n_steps"], + label=f"CPU Overhead: {args.path}", + ) + + # -- Memory analysis -- + snapshot = load_snapshot(args.path) + churn_result = None + churn2 = None + if snapshot: + mem_result = analyze_snapshot(snapshot) + print_memory_analysis(mem_result, label=f"Memory: {args.path}") + + # Peak memory timeline + peak_result = analyze_peak_memory(snapshot) + print_peak_memory( + peak_result, mem_result=mem_result, label=f"Peak Memory: {args.path}" + ) + + # Fragmentation diagnosis + frag_result = analyze_fragmentation(snapshot) + print_fragmentation( + frag_result, + gpu_capacity_gb=gpu_capacity_gb, + label=f"Fragmentation: {args.path}", + ) + + # Allocation churn attribution + churn_result = analyze_allocation_churn(snapshot) + if churn_result: + print_allocation_churn(churn_result, label=f"Allocation Churn: {args.path}") + + if args.compare: + snapshot2 = load_snapshot(args.compare) + if snapshot2: + mem2 = analyze_snapshot(snapshot2) + print_memory_analysis(mem2, label=f"Memory: {args.compare}") + + # Peak memory for comparison + peak2 = analyze_peak_memory(snapshot2) + print_peak_memory( + peak2, mem_result=mem2, label=f"Peak Memory: {args.compare}" + ) + + # Fragmentation for comparison + frag2 = analyze_fragmentation(snapshot2) + print_fragmentation( + frag2, + gpu_capacity_gb=gpu_capacity_gb, + label=f"Fragmentation: {args.compare}", + ) + + churn2 = analyze_allocation_churn(snapshot2) + if churn2: + print_allocation_churn( + churn2, label=f"Allocation Churn: {args.compare}" + ) + + # Memory comparison summary + print("\n Memory comparison:") + print( + f" Reserved: {mem_result['total_reserved'] / 1e9:.2f} -> " + f"{mem2['total_reserved'] / 1e9:.2f} GB" + ) + print( + f" Allocated: {mem_result['total_allocated'] / 1e9:.2f} -> " + f"{mem2['total_allocated'] / 1e9:.2f} GB" + ) + print( + f" Frag: {mem_result['fragmentation_pct']:.1f}% -> " + f"{mem2['fragmentation_pct']:.1f}%" + ) + if peak_result and peak2: + print( + f" Peak: {peak_result['peak_bytes'] / 1e9:.2f} -> " + f"{peak2['peak_bytes'] / 1e9:.2f} GB" + ) + + # Scaling analysis + if churn_result and churn2: + scaling = analyze_scaling(mem_result, mem2, churn_result, churn2) + print_scaling( + scaling, + label_a=str(args.path), + label_b=str(args.compare), + label="Allocation Scaling Analysis", + ) + elif not args.memory_only: + pass # trace-only is fine + else: + print(f" No snapshot.pickle found in {args.path}") + + # -- Summary -- + if trace_result: + print_summary(trace_result, mem_result=mem_result) + + +if __name__ == "__main__": + main() diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 96183973fc..d2f2e414c4 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -435,6 +435,23 @@ def compute_loss( num_items_in_batch=num_items_in_batch, ) + # Gemma4ForConditionalGeneration computes loss with a manual + # nn.CrossEntropyLoss() that bypasses proper num_items_in_batch + # normalization and does redundant attention_mask filtering. + # Compute loss externally using the standard loss_function instead. + if _model_type == "gemma4" and "labels" in inputs: + labels = inputs.pop("labels") + outputs = model(**inputs) + logits = outputs.logits + unwrapped = self.accelerator.unwrap_model(model) + vocab_size = unwrapped.config.get_text_config().vocab_size + loss = unwrapped.loss_function( + logits, labels, vocab_size, num_items_in_batch=num_items_in_batch + ) + if return_outputs: + return loss, outputs + return loss + return super().compute_loss( model, inputs, diff --git a/src/axolotl/integrations/liger/plugin.py b/src/axolotl/integrations/liger/plugin.py index d561095701..9c4a26351f 100644 --- a/src/axolotl/integrations/liger/plugin.py +++ b/src/axolotl/integrations/liger/plugin.py @@ -222,6 +222,56 @@ def patched_init(self, *args, **kwargs): rms_norm=cfg.liger_rms_norm, swiglu=cfg.liger_glu_activation, ) + elif cfg.model_config_type in ("gemma4", "gemma4_text"): + # Gemma4: offset=0 (NOT 1 like Gemma3), in_place=False required for + # gradient checkpointing compatibility, RoPE incompatible (separate q/k). + from liger_kernel.transformers.geglu import LigerGEGLUMLP + from transformers.models.gemma4 import modeling_gemma4 + + if cfg.liger_rms_norm: + _OrigGemma4RMSNorm = modeling_gemma4.Gemma4RMSNorm + + class _LigerGemma4RMSNorm(LigerRMSNorm): + """LigerRMSNorm for Gemma4 with in_place=False and with_scale support.""" + + def __new__(cls, dim, eps=1e-6, with_scale=True): + if not with_scale: + return _OrigGemma4RMSNorm(dim, eps, with_scale=False) + return super().__new__(cls) + + def __init__(self, dim, eps=1e-6, with_scale=True): + if not with_scale: + return + # offset=0.0 (standard), in_place=False (gradient checkpointing safe) + super().__init__( + dim, eps, offset=0.0, casting_mode="llama", in_place=False + ) + + modeling_gemma4.Gemma4RMSNorm = _LigerGemma4RMSNorm + if cfg.liger_glu_activation: + + class _LigerGemma4MLP(LigerGEGLUMLP): + def __init__(self, config, layer_idx=None): + super().__init__(config) + + modeling_gemma4.Gemma4TextMLP = _LigerGemma4MLP + if cfg.liger_rope: + LOG.warning( + "Liger RoPE is not compatible with Gemma4 (separate q/k application). Skipping." + ) + if cfg.liger_layer_norm: + modeling_gemma4.nn.LayerNorm = LigerLayerNorm + if cfg.liger_cross_entropy: + modeling_gemma4.nn.CrossEntropyLoss = LigerCrossEntropyLoss + if cfg.liger_fused_linear_cross_entropy: + LOG.warning( + "Liger fused linear cross entropy is not compatible with Gemma4. Skipping." + ) + LOG.info( + f"Applied Liger kernels for gemma4: " + f"rms_norm={cfg.liger_rms_norm}, glu={cfg.liger_glu_activation}, " + f"rope=False (incompatible), layer_norm={cfg.liger_layer_norm}" + ) elif cfg.liger_fused_linear_cross_entropy: try: from .models.base import patch_lce_forward diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 9cf65286aa..b9e7f3e0c8 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -112,6 +112,47 @@ else: key_states = key_states.view(hidden_shape) value_states = value_states.view(hidden_shape) if self.v_proj is not None else key_states +""".lstrip("\n"), + ), + # Gemma4 (transformers >= 5.6): shared_kv_states parameter replaces + # past_key_values.shared_layers, and v_norm added after k_norm. + ( + """ + query_states = self.q_proj(hidden_states).view(hidden_shape) + query_states = self.q_norm(query_states) + query_states = apply_rotary_pos_emb(query_states, cos, sin, unsqueeze_dim=2) + query_states = query_states.transpose(1, 2) + + # For layers with shared KV (from kv sharing point onwards), we reuse the same keys/values states as the last non-sharing layer. + # We cannot simply reuse the cached state if we have a Cache, as sliding layers will not remember the full states in their Cache + # once we are past the sliding window - so we always use `shared_kv_states` instead, even when past_key_values is not None + if self.is_kv_shared_layer: + key_states, value_states = shared_kv_states[self.kv_shared_layer_index] + # Device of past layer may be different from current one + key_states = key_states.to(query_states.device) + value_states = value_states.to(query_states.device) + else: + key_states = self.k_proj(hidden_states).view(hidden_shape) + value_states = self.v_proj(hidden_states).view(hidden_shape) if self.v_proj is not None else key_states +""".lstrip("\n"), + """ + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states = query_states.view(hidden_shape) + query_states = self.q_norm(query_states) + query_states = apply_rotary_pos_emb(query_states, cos, sin, unsqueeze_dim=2) + query_states = query_states.transpose(1, 2) + + # For layers with shared KV (from kv sharing point onwards), we reuse the same keys/values states as the last non-sharing layer. + # We cannot simply reuse the cached state if we have a Cache, as sliding layers will not remember the full states in their Cache + # once we are past the sliding window - so we always use `shared_kv_states` instead, even when past_key_values is not None + if self.is_kv_shared_layer: + key_states, value_states = shared_kv_states[self.kv_shared_layer_index] + # Device of past layer may be different from current one + key_states = key_states.to(query_states.device) + value_states = value_states.to(query_states.device) + else: + key_states = key_states.view(hidden_shape) + value_states = value_states.view(hidden_shape) if self.v_proj is not None else key_states """.lstrip("\n"), ), ] From e77a185e864c95d4f9487bf272993ae179b49ce4 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 10 Apr 2026 17:08:14 -0400 Subject: [PATCH 1264/1405] upgrade transformers to use v5.5.3 (#3593) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 446febb83e..fe4b674367 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ packaging==26.0 huggingface_hub>=1.1.7 peft>=0.18.1 tokenizers>=0.22.1 -transformers==5.5.0 +transformers==5.5.3 accelerate==1.13.0 datasets==4.5.0 deepspeed>=0.18.6,<0.19.0 From 122b50bad66675ae8a97225b9ed8895e1d5f6ef9 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 11 Apr 2026 20:05:21 -0400 Subject: [PATCH 1265/1405] pre-cache the eot token ids rather than on each iteration (#3594) [skip ci] --- .../prompt_strategies/chat_template.py | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index a7f749f3b2..a943a14484 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -315,6 +315,13 @@ def __init__( self._validate_eot_and_eos_tokens() + # Pre-cache EOT token IDs to avoid re-encoding on every call + self._eot_token_ids = set() + for token in self.eot_tokens: + token_ids = self.tokenizer.encode(token, add_special_tokens=False) + if len(token_ids) == 1: + self._eot_token_ids.add(token_ids[0]) + def _validate_eot_and_eos_tokens(self): """ - Validates that EOT tokens (or eos_token) are in the chat_template @@ -632,20 +639,9 @@ def find_first_eos_token(self, input_ids, start_idx): def find_first_eot_token(self, input_ids, start_idx): """Find the first EOT token in the input_ids starting from start_idx.""" - # Get token IDs for all EOT tokens - eot_token_ids = [] - for token in self.eot_tokens: - token_ids = self.tokenizer.encode(token, add_special_tokens=False) - if len(token_ids) != 1: - raise ValueError( - f"EOT token '{token}' is encoded as multiple tokens: {token_ids}. Please add it under `tokens: ` in the config." - ) - - eot_token_ids.append(token_ids[0]) # Use the last token ID if multiple - - # Search for any of the EOT token IDs + # Use pre-cached EOT token IDs (computed once in __init__) for i in range(start_idx, len(input_ids)): - if input_ids[i] in eot_token_ids: + if input_ids[i] in self._eot_token_ids: return i return -1 From e2f69828d21afc969b5a2824cc666813147aa101 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 11 Apr 2026 20:22:35 -0400 Subject: [PATCH 1266/1405] [fix][fsdp2] clone sharded param so original full size shard can be gc'ed (#3597) [skip ci] --- src/axolotl/monkeypatch/accelerate/fsdp2.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index cf8056ca04..e086885bb4 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -60,6 +60,13 @@ def fsdp2_load_full_state_dict( sharded_meta_param.placements, src_data_rank=0, ) + # Clone the local shard to allow full_tensor to be freed. + if ( + sharded_param._local_tensor.untyped_storage().size() + > sharded_param._local_tensor.nelement() + * sharded_param._local_tensor.element_size() + ): + sharded_param = sharded_param.clone() else: # Non-sharded parameters if _accelerator.is_main_process: From e079cf16a212711663aa84cd4c2db064e8300922 Mon Sep 17 00:00:00 2001 From: Joaquin Hui <132194176+joaquinhuigomez@users.noreply.github.com> Date: Sun, 12 Apr 2026 05:58:58 +0100 Subject: [PATCH 1267/1405] qwen3_5.jinja: handle list content on system messages (#3595) [skip ci] * qwen3_5.jinja: handle list content on system messages The system message branch used string concatenation on messages[0].content, which breaks when the first system message uses the OpenAI-style list-of-parts format that multimodal datasets require. User and assistant branches already handle both string and list content, but the system branch did not. Check whether content is a string and fall back to iterating over parts when it is a list, matching the pattern used for user messages. Fixes #3590 * Address pr for other content types --------- Co-authored-by: Joaquin Hui Gomez Co-authored-by: Wing Lian --- .../chat_templates/templates/qwen3_5.jinja | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja b/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja index 21f5733ed9..c8d0e8da0b 100644 --- a/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja +++ b/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja @@ -1,7 +1,19 @@ {%- if tools %} {{- '<|im_start|>system\n' }} {%- if messages[0].role == 'system' %} - {{- messages[0].content + '\n\n' }} + {%- if messages[0].content is string %} + {{- messages[0].content + '\n\n' }} + {%- else %} + {%- for part in messages[0].content %} + {%- if part is mapping %} + {%- set system_text = part.get('text') or part.get('content') or part.get('value') %} + {%- if system_text %}{{- system_text }}{%- endif %} + {%- elif part is string %} + {{- part }} + {%- endif %} + {%- endfor %} + {{- '\n\n' }} + {%- endif %} {%- endif %} {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n" }} {%- for tool in tools %} @@ -11,7 +23,20 @@ {{- "\n\n\nFor each function call, return a json object with function name and arguments within XML tags:\n\n{\"name\": , \"arguments\": }\n<|im_end|>\n" }} {%- else %} {%- if messages[0].role == 'system' %} - {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }} + {%- if messages[0].content is string %} + {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }} + {%- else %} + {{- '<|im_start|>system\n' }} + {%- for part in messages[0].content %} + {%- if part is mapping %} + {%- set system_text = part.get('text') or part.get('content') or part.get('value') %} + {%- if system_text %}{{- system_text }}{%- endif %} + {%- elif part is string %} + {{- part }} + {%- endif %} + {%- endfor %} + {{- '<|im_end|>\n' }} + {%- endif %} {%- endif %} {%- endif %} {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %} From b8358aa5abcb750b423457f67d374bc39d0a14a3 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 12 Apr 2026 10:29:55 -0400 Subject: [PATCH 1268/1405] [gemma4] use mixed Flash Attention and SDPA and add fused RMSNorm+RoPE Triton kernels (#3598) --- src/axolotl/kernels/gemma4_fused_rope.py | 529 ++++++++++++++++++ src/axolotl/loaders/model.py | 9 +- src/axolotl/loaders/patch_manager.py | 74 +++ .../monkeypatch/models/gemma4/fused_attn.py | 147 +++++ src/axolotl/utils/schemas/config.py | 9 + tests/kernels/test_gemma4_fused_rope.py | 226 ++++++++ 6 files changed, 993 insertions(+), 1 deletion(-) create mode 100644 src/axolotl/kernels/gemma4_fused_rope.py create mode 100644 src/axolotl/monkeypatch/models/gemma4/fused_attn.py create mode 100644 tests/kernels/test_gemma4_fused_rope.py diff --git a/src/axolotl/kernels/gemma4_fused_rope.py b/src/axolotl/kernels/gemma4_fused_rope.py new file mode 100644 index 0000000000..f3b68e603c --- /dev/null +++ b/src/axolotl/kernels/gemma4_fused_rope.py @@ -0,0 +1,529 @@ +""" +Fused RMSNorm + RoPE Triton kernel for Gemma 4. + +Fuses three operations into one kernel launch: + 1. RMSNorm: x_norm = (x / sqrt(mean(x^2) + eps)) * weight + 2. RoPE: y = x_norm * cos + rotate_half(x_norm) * sin + 3. (optional) RMSNorm without scale (for v_norm) + +This eliminates two intermediate tensor materializations per Q/K path; +churn from rotate_half / apply_rotary_pos_emb. + +Shapes: + X: (rows, head_dim) — flattened from (batch, seq_len, num_heads, head_dim) + W: (head_dim,) — RMSNorm weight (None for with_scale=False) + cos: (rows, head_dim) — flattened from (batch, seq_len, 1, head_dim) after broadcast + sin: (rows, head_dim) — same as cos +""" + +import math +import operator + +import torch +import triton +import triton.language as tl +from liger_kernel.ops.utils import ( + calculate_settings, + compare_version, + ensure_contiguous, + torch_to_triton_dtype, +) +from liger_kernel.utils import is_npu_available + +if compare_version("triton", operator.ge, "3.0.0") and not is_npu_available(): + try: + from triton.language.extra.libdevice import rsqrt + except ModuleNotFoundError: + from triton.language.extra.cuda.libdevice import rsqrt +else: + from triton.language.math import rsqrt + + +@triton.jit +def _rms_norm_rope_forward_kernel( + Y_ptr, + Y_row_stride, + X_ptr, + X_row_stride, + W_ptr, + COS_ptr, + COS_row_stride, + SIN_ptr, + SIN_row_stride, + RSTD_ptr, + RSTD_row_stride, + n_cols, + n_heads, + eps, + HAS_WEIGHT: tl.constexpr, + BLOCK_SIZE: tl.constexpr, +): + """ + Fused forward: + x_norm = x / rms(x) [* weight] (RMSNorm) + y = x_norm * cos + rotate_half(x_norm) * sin (RoPE) + + rotate_half swaps first/second halves and negates the first: + rotate_half([a, b]) = [-b, a] + + cos/sin are indexed by row_idx // n_heads to handle per-head broadcast + (cos/sin have shape (B*S, D) while X has shape (B*S*H, D)). + """ + row_idx = tl.program_id(0).to(tl.int64) + # cos/sin row: divide by n_heads since cos/sin are (B*S, D) + cs_row_idx = row_idx // n_heads + col_offsets = tl.arange(0, BLOCK_SIZE) + mask = col_offsets < n_cols + half_dim = n_cols // 2 + + # Load input row + X_row = tl.load(X_ptr + row_idx * X_row_stride + col_offsets, mask=mask, other=0) + X_dtype = X_row.dtype + X_fp32 = X_row.to(tl.float32) + + # RMSNorm: compute 1/rms + mean_sq = tl.sum(X_fp32 * X_fp32, axis=0) / n_cols + rstd = rsqrt(mean_sq + eps) + tl.store(RSTD_ptr + row_idx * RSTD_row_stride, rstd) + + # Normalize + X_norm = X_fp32 * rstd + + # Apply weight if present (with_scale=True) + if HAS_WEIGHT: + W_row = tl.load(W_ptr + col_offsets, mask=mask, other=0).to(tl.float32) + X_norm = X_norm * W_row + + # RoPE: load cos/sin (broadcast across heads) + cos_row = tl.load( + COS_ptr + cs_row_idx * COS_row_stride + col_offsets, mask=mask, other=0 + ).to(tl.float32) + sin_row = tl.load( + SIN_ptr + cs_row_idx * SIN_row_stride + col_offsets, mask=mask, other=0 + ).to(tl.float32) + + # rotate_half: for col < half_dim, take -X_norm[col + half_dim] + # for col >= half_dim, take X_norm[col - half_dim] + rot_offsets = tl.where( + col_offsets < half_dim, col_offsets + half_dim, col_offsets - half_dim + ) + rot_mask = rot_offsets < n_cols + X_rot = tl.load( + X_ptr + row_idx * X_row_stride + rot_offsets, mask=rot_mask & mask, other=0 + ).to(tl.float32) + # Re-normalize the rotated values + X_rot_norm = X_rot * rstd + if HAS_WEIGHT: + W_rot = tl.load(W_ptr + rot_offsets, mask=rot_mask & mask, other=0).to( + tl.float32 + ) + X_rot_norm = X_rot_norm * W_rot + + # Negate the first half (rotate_half negates x2, which becomes the first half) + sign = tl.where(col_offsets < half_dim, -1.0, 1.0) + X_rot_norm = X_rot_norm * sign + + # Final RoPE: y = x_norm * cos + rotate_half(x_norm) * sin + Y_row = X_norm * cos_row + X_rot_norm * sin_row + + tl.store( + Y_ptr + row_idx * Y_row_stride + col_offsets, + Y_row.to(X_dtype), + mask=mask, + ) + + +@triton.jit +def _rms_norm_rope_backward_kernel( + dY_ptr, + dY_row_stride, + dX_ptr, + dX_row_stride, + X_ptr, + X_row_stride, + X_dtype: tl.constexpr, + W_ptr, + COS_ptr, + COS_row_stride, + SIN_ptr, + SIN_row_stride, + RSTD_ptr, + RSTD_row_stride, + dW_ptr, + dW_row_stride, + n_rows, + n_cols, + n_heads, + rows_per_program, + HAS_WEIGHT: tl.constexpr, + BLOCK_SIZE: tl.constexpr, +): + """ + Backward for Y = RoPE(RMSNorm(X, W)) + cos/sin indexed by row_idx // n_heads for per-head broadcast. + """ + row_block_id = tl.program_id(0).to(tl.int64) + row_start = row_block_id * rows_per_program + row_end = min((row_block_id + 1) * rows_per_program, n_rows) + col_offsets = tl.arange(0, BLOCK_SIZE) + mask = col_offsets < n_cols + half_dim = n_cols // 2 + + dW_acc = tl.zeros((BLOCK_SIZE,), dtype=tl.float32) + + if HAS_WEIGHT: + W_row = tl.load(W_ptr + col_offsets, mask=mask, other=0).to(tl.float32) + + for row_idx in range(row_start, row_end): + cs_row_idx = row_idx // n_heads + + dY_row = tl.load( + dY_ptr + row_idx * dY_row_stride + col_offsets, mask=mask, other=0 + ).to(tl.float32) + X_row = tl.load( + X_ptr + row_idx * X_row_stride + col_offsets, mask=mask, other=0 + ).to(tl.float32) + rstd = tl.load(RSTD_ptr + row_idx * RSTD_row_stride) + + cos_row = tl.load( + COS_ptr + cs_row_idx * COS_row_stride + col_offsets, mask=mask, other=0 + ).to(tl.float32) + + # dN = dY * cos + rotate_half^T(dY * sin) + # rotate_half^T([a, b]) = [b, -a] (adjoint of rotate_half) + # + # Compute rotate_half_transpose(dY * sin) by loading dY and sin at + # rotated offsets directly: dY[rot] * sin[rot] * adj_sign + # This is equivalent to rotating (dY * sin) because the rotation + # just permutes which elements are multiplied. + rot_offsets = tl.where( + col_offsets < half_dim, col_offsets + half_dim, col_offsets - half_dim + ) + rot_mask = rot_offsets < n_cols + dY_rot = tl.load( + dY_ptr + row_idx * dY_row_stride + rot_offsets, + mask=rot_mask & mask, + other=0, + ).to(tl.float32) + sin_rot = tl.load( + SIN_ptr + cs_row_idx * SIN_row_stride + rot_offsets, + mask=rot_mask & mask, + other=0, + ).to(tl.float32) + + adj_sign = tl.where(col_offsets < half_dim, 1.0, -1.0) + dN = dY_row * cos_row + dY_rot * sin_rot * adj_sign + + # Pre-weight normalized: n = rstd * x + n = X_row * rstd + + if HAS_WEIGHT: + dW_acc += dN * n + dm = dN * W_row + else: + dm = dN + + # RMSNorm backward: dX = rstd * (dm - (1/n_cols) * rstd^2 * dot(dm, X) * X) + dot_dm_x = tl.sum(dm * X_row, axis=0) + dX_row = rstd * (dm - (1.0 / n_cols) * rstd * rstd * dot_dm_x * X_row) + + tl.store( + dX_ptr + row_idx * dX_row_stride + col_offsets, + dX_row.to(X_dtype), + mask=mask, + ) + + if HAS_WEIGHT: + tl.store( + dW_ptr + row_block_id * dW_row_stride + col_offsets, + dW_acc, + mask=mask, + ) + + +def rms_norm_rope_forward(X, W, cos, sin, eps, n_heads): + """ + Args: + X: (B*S*H, head_dim) — contiguous, flattened from (B, S, H, D) + W: (head_dim,) or None — RMSNorm weight + cos: (B*S, head_dim) — position embeddings (broadcast across heads) + sin: (B*S, head_dim) — position embeddings (broadcast across heads) + eps: float + n_heads: int — number of attention heads (for cos/sin indexing) + Returns: + Y, X_saved, RSTD, BLOCK_SIZE, num_warps + """ + n_rows, n_cols = X.shape + BLOCK_SIZE, num_warps = calculate_settings(n_cols) + has_weight = W is not None + + Y = torch.empty_like(X) + RSTD = torch.empty(n_rows, dtype=torch.float32, device=X.device) + + _rms_norm_rope_forward_kernel[(n_rows,)]( + Y, + Y.stride(0), + X, + X.stride(0), + W if has_weight else X, # dummy pointer when no weight + cos, + cos.stride(0), + sin, + sin.stride(0), + RSTD, + RSTD.stride(0), + n_cols, + n_heads, + eps, + HAS_WEIGHT=has_weight, + BLOCK_SIZE=BLOCK_SIZE, + num_warps=num_warps, + ) + return Y, X, RSTD, BLOCK_SIZE, num_warps + + +def rms_norm_rope_backward(dY, X, W, cos, sin, RSTD, n_heads, BLOCK_SIZE, num_warps): + n_rows, n_cols = dY.shape + has_weight = W is not None + + sm_count = torch.cuda.get_device_properties(X.device).multi_processor_count + rows_per_program = math.ceil(n_rows / sm_count) + + dX = torch.empty_like(X) + + if has_weight: + _dW = torch.empty((sm_count, n_cols), dtype=torch.float32, device=X.device) + else: + _dW = torch.empty((1, n_cols), dtype=torch.float32, device=X.device) + + _rms_norm_rope_backward_kernel[(sm_count,)]( + dY, + dY.stride(0), + dX, + dX.stride(0), + X, + X.stride(0), + torch_to_triton_dtype[X.dtype], + W if has_weight else X, # dummy + cos, + cos.stride(0), + sin, + sin.stride(0), + RSTD, + RSTD.stride(0), + _dW, + _dW.stride(0), + n_rows, + n_cols, + n_heads, + rows_per_program, + HAS_WEIGHT=has_weight, + BLOCK_SIZE=BLOCK_SIZE, + num_warps=num_warps, + ) + + dW = _dW.sum(dim=0).to(W.dtype) if has_weight else None + return dX, dW + + +class FusedRMSNormRoPEFunction(torch.autograd.Function): + @staticmethod + @ensure_contiguous + def forward(ctx, X, W, cos, sin, eps, n_heads): + """ + X: (B*S*H, head_dim) + W: (head_dim,) or None + cos: (B*S, head_dim) — broadcast across heads + sin: (B*S, head_dim) — broadcast across heads + n_heads: int + """ + Y, X_saved, RSTD, BLOCK_SIZE, num_warps = rms_norm_rope_forward( + X, + W, + cos, + sin, + eps, + n_heads, + ) + ctx.eps = eps + ctx.BLOCK_SIZE = BLOCK_SIZE + ctx.num_warps = num_warps + ctx.n_heads = n_heads + ctx.has_weight = W is not None + ctx.save_for_backward(X_saved, W, cos, sin, RSTD) + return Y + + @staticmethod + @ensure_contiguous + def backward(ctx, dY): + X, W, cos, sin, RSTD = ctx.saved_tensors + dX, dW = rms_norm_rope_backward( + dY, + X, + W, + cos, + sin, + RSTD, + ctx.n_heads, + ctx.BLOCK_SIZE, + ctx.num_warps, + ) + return dX, dW, None, None, None, None + + +def fused_rms_norm_rope(x, weight, cos, sin, eps=1e-6): + """ + Apply fused RMSNorm + RoPE. + + Args: + x: (batch, seq_len, num_heads, head_dim) — after projection + view + weight: (head_dim,) — RMSNorm weight, or None for no-scale norm + cos: (batch, seq_len, head_dim) — from RotaryEmbedding + sin: (batch, seq_len, head_dim) — from RotaryEmbedding + eps: float — RMSNorm epsilon + + Returns: + y: (batch, seq_len, num_heads, head_dim) — normalized + rotated + """ + shape = x.shape # (B, S, H, D) + B, S, H, D = shape + # Flatten to 2D: (B*S*H, D) + x_flat = x.reshape(-1, D).contiguous() + # Flatten cos/sin to (B*S, D) — the kernel will handle per-head broadcast + # by dividing the row_idx by H to get the cos/sin row + cos_flat = cos.reshape(B * S, D).contiguous() + sin_flat = sin.reshape(B * S, D).contiguous() + + y_flat = FusedRMSNormRoPEFunction.apply(x_flat, weight, cos_flat, sin_flat, eps, H) + return y_flat.view(shape) + + +@triton.jit +def _rms_norm_forward_kernel( + Y_ptr, + Y_row_stride, + X_ptr, + X_row_stride, + RSTD_ptr, + RSTD_row_stride, + n_cols, + eps, + BLOCK_SIZE: tl.constexpr, +): + """RMSNorm without scale weight: y = x / rms(x)""" + row_idx = tl.program_id(0).to(tl.int64) + col_offsets = tl.arange(0, BLOCK_SIZE) + mask = col_offsets < n_cols + + X_row = tl.load(X_ptr + row_idx * X_row_stride + col_offsets, mask=mask, other=0) + X_dtype = X_row.dtype + X_fp32 = X_row.to(tl.float32) + + mean_sq = tl.sum(X_fp32 * X_fp32, axis=0) / n_cols + rstd = rsqrt(mean_sq + eps) + tl.store(RSTD_ptr + row_idx * RSTD_row_stride, rstd) + + Y_row = X_fp32 * rstd + tl.store(Y_ptr + row_idx * Y_row_stride + col_offsets, Y_row.to(X_dtype), mask=mask) + + +@triton.jit +def _rms_norm_noscale_backward_kernel( + dY_ptr, + dY_row_stride, + dX_ptr, + dX_row_stride, + X_ptr, + X_row_stride, + X_dtype: tl.constexpr, + RSTD_ptr, + RSTD_row_stride, + n_cols, + BLOCK_SIZE: tl.constexpr, +): + """Backward for y = x * rstd (no weight).""" + row_idx = tl.program_id(0).to(tl.int64) + col_offsets = tl.arange(0, BLOCK_SIZE) + mask = col_offsets < n_cols + + dY_row = tl.load( + dY_ptr + row_idx * dY_row_stride + col_offsets, mask=mask, other=0 + ).to(tl.float32) + X_row = tl.load( + X_ptr + row_idx * X_row_stride + col_offsets, mask=mask, other=0 + ).to(tl.float32) + rstd = tl.load(RSTD_ptr + row_idx * RSTD_row_stride) + + dot_dy_x = tl.sum(dY_row * X_row, axis=0) + dX_row = rstd * (dY_row - (1.0 / n_cols) * rstd * rstd * dot_dy_x * X_row) + + tl.store( + dX_ptr + row_idx * dX_row_stride + col_offsets, dX_row.to(X_dtype), mask=mask + ) + + +class FusedRMSNormNoScaleFunction(torch.autograd.Function): + """RMSNorm without learnable scale — used for Gemma4's v_norm.""" + + @staticmethod + @ensure_contiguous + def forward(ctx, X, eps): + n_rows, n_cols = X.shape + BLOCK_SIZE, num_warps = calculate_settings(n_cols) + Y = torch.empty_like(X) + RSTD = torch.empty(n_rows, dtype=torch.float32, device=X.device) + + _rms_norm_forward_kernel[(n_rows,)]( + Y, + Y.stride(0), + X, + X.stride(0), + RSTD, + RSTD.stride(0), + n_cols, + eps, + BLOCK_SIZE=BLOCK_SIZE, + num_warps=num_warps, + ) + ctx.BLOCK_SIZE = BLOCK_SIZE + ctx.num_warps = num_warps + ctx.save_for_backward(X, RSTD) + ctx.n_cols = n_cols + return Y + + @staticmethod + @ensure_contiguous + def backward(ctx, dY): + X, RSTD = ctx.saved_tensors + n_rows = X.shape[0] + dX = torch.empty_like(X) + _rms_norm_noscale_backward_kernel[(n_rows,)]( + dY, + dY.stride(0), + dX, + dX.stride(0), + X, + X.stride(0), + torch_to_triton_dtype[X.dtype], + RSTD, + RSTD.stride(0), + ctx.n_cols, + BLOCK_SIZE=ctx.BLOCK_SIZE, + num_warps=ctx.num_warps, + ) + return dX, None + + +def fused_rms_norm_noscale(x, eps=1e-6): + """ + RMSNorm without scale for v_norm. + + Args: + x: (batch, seq_len, num_heads, head_dim) + Returns: + y: same shape, normalized + """ + shape = x.shape + x_flat = x.reshape(-1, shape[-1]) + y_flat = FusedRMSNormNoScaleFunction.apply(x_flat, eps) + return y_flat.view(shape) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 3bfda7e23e..83b6452dc8 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -624,7 +624,14 @@ def _set_quantization_config(self): def _set_attention_config(self): """Sample packing uses custom FA2 patch""" - if self.cfg.attn_implementation: + if self.cfg.gemma4_hybrid_attn_impl: + # Load model with flash_attention_2 for sliding window layers; + # global layers will be patched to sdpa post-load. + self.model_kwargs["attn_implementation"] = "flash_attention_2" + self.model_config._attn_implementation = "flash_attention_2" + # Set flash_attention so multipack/sample_packing patches activate + self.cfg.flash_attention = True + elif self.cfg.attn_implementation: self.model_kwargs["attn_implementation"] = self.cfg.attn_implementation elif self.cfg.flex_attention: self.model_kwargs["attn_implementation"] = "flex_attention" diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 018ca52a05..41fc35e6ec 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -156,6 +156,7 @@ def apply_post_model_build_patches(self, model: PreTrainedModel): # which would clobber any earlier fix. self._fix_nemotron_h_conversion_mapping() + self._apply_gemma_hybrid_attention(model) self._finalize_moe_expert_quantization(model) def apply_post_model_load_patches(self, model: PreTrainedModel): @@ -165,6 +166,72 @@ def apply_post_model_load_patches(self, model: PreTrainedModel): self._apply_lora_kernel_patch(model) self._apply_scaling_softmax_patch(model) + def _apply_gemma_hybrid_attention(self, model: PreTrainedModel): + """Apply hybrid attention: FA2 for sliding window layers, SDPA for global layers. + + Gemma 4 has global (full_attention) layers with head_dim=512 + which exceeds flash attention's supported size. This patch loads the model + with flash_attention_2 for the sliding window layers (head_dim=256), then + gives each global layer a shallow-copied config with _attn_implementation="sdpa". + """ + if not self.cfg.gemma4_hybrid_attn_impl: + return + + import copy + + # Navigate to the module that has 'layers' - varies by model structure: + # Gemma4ForConditionalGeneration -> .model (Gemma4Model) -> .language_model (Gemma4TextModel) -> .layers + # Gemma4ForCausalLM -> .model (Gemma4TextModel) -> .layers + layers = None + config_source = None + for candidate in [model, getattr(model, "model", None)]: + if candidate is None: + continue + # Check direct layers + if hasattr(candidate, "layers"): + layers = candidate.layers + config_source = candidate + break + # Check language_model.layers (multimodal wrapper) + lang_model = getattr(candidate, "language_model", None) + if lang_model is not None and hasattr(lang_model, "layers"): + layers = lang_model.layers + config_source = lang_model + break + + if layers is None: + LOG.warning( + "gemma4_hybrid_attn_impl: could not find decoder layers in model, skipping" + ) + return + + config = getattr(config_source, "config", self.model_config) + layer_types = getattr(config, "layer_types", None) + if layer_types is None: + LOG.warning( + "gemma4_hybrid_attn_impl: model config has no 'layer_types', skipping. " + "This feature requires a model with mixed sliding/global attention layers." + ) + return + + patched_count = 0 + for layer_idx, layer in enumerate(layers): + if layer_types[layer_idx] != "sliding_attention": + # Global / full_attention layer - use SDPA instead of FA2 + attn_module = getattr(layer, "self_attn", None) + if attn_module is not None and hasattr(attn_module, "config"): + sdpa_config = copy.copy(attn_module.config) + sdpa_config._attn_implementation = "sdpa" + attn_module.config = sdpa_config + patched_count += 1 + + LOG.info( + "gemma4_hybrid_attn_impl: patched %d global layers to use SDPA " + "(remaining %d sliding layers use flash_attention_2)", + patched_count, + len(layers) - patched_count, + ) + def _apply_flash_attention_patches(self): """Apply patches related to Flash Attention.""" if self.cfg.xformers_attention and self.cfg.sample_packing: @@ -324,6 +391,13 @@ def _apply_model_specific_patches(self): patch_qwen3_5_vlm_flash_attention() + if self.cfg.model_config_type in ("gemma4", "gemma4_text"): + from axolotl.monkeypatch.models.gemma4.fused_attn import ( + patch_gemma4_fused_attn, + ) + + patch_gemma4_fused_attn() + @staticmethod def _fix_nemotron_h_conversion_mapping(): """Remove the spurious embedding→embeddings WeightRenaming from the diff --git a/src/axolotl/monkeypatch/models/gemma4/fused_attn.py b/src/axolotl/monkeypatch/models/gemma4/fused_attn.py new file mode 100644 index 0000000000..7cb5c6beb3 --- /dev/null +++ b/src/axolotl/monkeypatch/models/gemma4/fused_attn.py @@ -0,0 +1,147 @@ +""" +Gemma 4 fused attention monkeypatch. + +Replaces the per-layer RMSNorm + RoPE + transpose sequence with fused Triton +kernels, eliminating intermediate tensor allocations from rotate_half / apply_rotary_pos_emb + +Usage: + from axolotl.monkeypatch.models.gemma4.fused_attn import patch_gemma4_fused_attn + patch_gemma4_fused_attn() +""" + +import logging +from typing import Callable + +import torch + +logger = logging.getLogger(__name__) + + +def _make_fused_forward(original_forward): + """Create a patched forward that uses fused RMSNorm+RoPE kernels.""" + + from axolotl.kernels.gemma4_fused_rope import ( + fused_rms_norm_noscale, + fused_rms_norm_rope, + ) + + def fused_forward( + self, + hidden_states: torch.Tensor, + position_embeddings: torch.Tensor, + attention_mask: torch.Tensor | None, + shared_kv_states: dict[int, tuple[torch.Tensor, torch.Tensor]], + past_key_values=None, + **kwargs, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + from transformers.models.gemma4.modeling_gemma4 import ( + eager_attention_forward, + ) + + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + eps = self.config.rms_norm_eps + + cos, sin = position_embeddings + + # ---- Projections ---- + # Use apply_qkv if present (LoRA kernel patch), otherwise direct proj + has_lora_qkv = hasattr(self, "apply_qkv") + + if has_lora_qkv: + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states = query_states.view(hidden_shape) + else: + query_states = self.q_proj(hidden_states).view(hidden_shape) + + # ---- Q path: fused q_norm + RoPE ---- + query_states = fused_rms_norm_rope( + query_states, + self.q_norm.weight, + cos, + sin, + eps=eps, + ) + query_states = query_states.transpose(1, 2) + + # ---- K/V path ---- + if self.is_kv_shared_layer: + key_states, value_states = shared_kv_states[self.kv_shared_layer_index] + key_states = key_states.to(query_states.device) + value_states = value_states.to(query_states.device) + else: + if has_lora_qkv: + # apply_qkv already computed k/v projections + key_states = key_states.view(hidden_shape) + value_states = ( + value_states.view(hidden_shape) + if self.v_proj is not None + else key_states + ) + else: + key_states = self.k_proj(hidden_states).view(hidden_shape) + value_states = ( + self.v_proj(hidden_states).view(hidden_shape) + if self.v_proj is not None + else key_states + ) + + # Fused k_norm + RoPE + key_states = fused_rms_norm_rope( + key_states, + self.k_norm.weight, + cos, + sin, + eps=eps, + ) + key_states = key_states.transpose(1, 2) + + # Fused v_norm (no scale, no RoPE) + value_states = fused_rms_norm_noscale(value_states, eps=eps) + value_states = value_states.transpose(1, 2) + + if past_key_values is not None and not self.is_kv_shared_layer: + key_states, value_states = past_key_values.update( + key_states, value_states, self.layer_idx + ) + if self.store_full_length_kv: + shared_kv_states[self.layer_idx] = key_states, value_states + + attention_interface: Callable = eager_attention_forward + if self.config._attn_implementation != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS[ + self.config._attn_implementation + ] + + attn_output, attn_weights = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=self.attention_dropout if self.training else 0.0, + scaling=self.scaling, + sliding_window=self.sliding_window, + **kwargs, + ) + + attn_output = attn_output.reshape(*input_shape, -1).contiguous() + attn_output = self.o_proj(attn_output) + return attn_output, attn_weights + + return fused_forward + + +def patch_gemma4_fused_attn(): + """ + Monkeypatch Gemma4TextAttention.forward to use fused RMSNorm+RoPE kernels. + """ + from transformers.models.gemma4.modeling_gemma4 import Gemma4TextAttention + + original_forward = Gemma4TextAttention.forward + Gemma4TextAttention.forward = _make_fused_forward(original_forward) + + logger.info( + "Patched Gemma4TextAttention.forward with fused RMSNorm+RoPE Triton kernels" + ) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 474c3a349b..4966570309 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -777,6 +777,15 @@ class AxolotlInputConfig( }, ) + gemma4_hybrid_attn_impl: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Use hybrid attention for Gemma 4: flash_attention_2 for sliding window layers " + "and sdpa for global (full_attention) layers. Global layers have head_dim=512 which " + "exceeds flash attention's supported size." + }, + ) + experts_implementation: str | None = Field( default=None, json_schema_extra={ diff --git a/tests/kernels/test_gemma4_fused_rope.py b/tests/kernels/test_gemma4_fused_rope.py new file mode 100644 index 0000000000..7daedd6122 --- /dev/null +++ b/tests/kernels/test_gemma4_fused_rope.py @@ -0,0 +1,226 @@ +""" +Correctness tests for the fused RMSNorm+RoPE Triton kernel. + +Tests forward and backward against the reference Gemma4 implementation +(Gemma4RMSNorm + apply_rotary_pos_emb) across both sliding window +(head_dim=256) and global attention (head_dim=512) layer configurations. +""" + +import pytest +import torch + +torch.manual_seed(42) + +# Skip entire module if no CUDA +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") + + +def _reference_norm_rope(x, weight, cos, sin, eps): + """Reference: separate Gemma4RMSNorm + apply_rotary_pos_emb.""" + from transformers.models.gemma4.modeling_gemma4 import ( + Gemma4RMSNorm, + apply_rotary_pos_emb, + ) + + D = x.shape[-1] + norm = Gemma4RMSNorm(D, eps=eps).to(x.device, x.dtype) + norm.weight.data.copy_(weight) + normed = norm(x) + return apply_rotary_pos_emb(normed, cos, sin, unsqueeze_dim=2) + + +def _reference_norm_noscale(x, eps): + """Reference: Gemma4RMSNorm with_scale=False.""" + from transformers.models.gemma4.modeling_gemma4 import Gemma4RMSNorm + + D = x.shape[-1] + norm = Gemma4RMSNorm(D, eps=eps, with_scale=False).to(x.device, x.dtype) + return norm(x) + + +@pytest.fixture( + params=[ + (2, 64, 32, 256), # sliding window layer shape + (2, 64, 4, 512), # global attention layer shape + (1, 128, 16, 256), # different batch/seq + (1, 1, 1, 8), # minimal size + ], + ids=["sliding_256", "global_512", "varied", "minimal"], +) +def shapes(request): + return request.param + + +@pytest.fixture(params=[torch.bfloat16, torch.float16], ids=["bf16", "fp16"]) +def dtype(request): + return request.param + + +class TestFusedRMSNormRoPEForward: + """Forward pass correctness.""" + + def test_matches_reference(self, shapes, dtype): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D = shapes + eps = 1e-6 + x = torch.randn(B, S, H, D, device="cuda", dtype=dtype) + weight = torch.randn(D, device="cuda", dtype=dtype) + cos = torch.randn(B, S, D, device="cuda", dtype=dtype) + sin = torch.randn(B, S, D, device="cuda", dtype=dtype) + + y_ref = _reference_norm_rope(x.clone(), weight, cos, sin, eps) + y_fused = fused_rms_norm_rope(x.clone(), weight, cos, sin, eps=eps) + + cos_sim = torch.nn.functional.cosine_similarity( + y_ref.flatten().float(), y_fused.flatten().float(), dim=0 + ) + assert cos_sim > 0.999, f"Forward cosine_sim={cos_sim:.6f}, expected > 0.999" + + def test_output_shape(self, shapes): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D = shapes + x = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + weight = torch.randn(D, device="cuda", dtype=torch.bfloat16) + cos = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + + y = fused_rms_norm_rope(x, weight, cos, sin, eps=1e-6) + assert y.shape == x.shape + assert y.dtype == x.dtype + + +class TestFusedRMSNormRoPEBackward: + """Backward pass correctness via gradient comparison.""" + + @pytest.mark.parametrize( + "B,S,H,D", + [(2, 64, 32, 256), (2, 64, 4, 512)], + ids=["sliding_256", "global_512"], + ) + def test_x_grad_matches_reference(self, B, S, H, D): + from transformers.models.gemma4.modeling_gemma4 import ( + Gemma4RMSNorm, + apply_rotary_pos_emb, + ) + + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + eps = 1e-6 + cos = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + weight_init = torch.randn(D, device="cuda", dtype=torch.bfloat16) + + # Reference backward + x_ref = torch.randn( + B, S, H, D, device="cuda", dtype=torch.bfloat16, requires_grad=True + ) + norm_ref = Gemma4RMSNorm(D, eps=eps).cuda().to(torch.bfloat16) + norm_ref.weight.data.copy_(weight_init) + y_ref = apply_rotary_pos_emb(norm_ref(x_ref), cos, sin, unsqueeze_dim=2) + y_ref.sum().backward() + + # Fused backward + x_fused = x_ref.data.clone().requires_grad_(True) + w_fused = weight_init.clone().requires_grad_(True) + y_fused = fused_rms_norm_rope(x_fused, w_fused, cos, sin, eps=eps) + y_fused.sum().backward() + + cos_sim_x = torch.nn.functional.cosine_similarity( + x_fused.grad.flatten().float(), x_ref.grad.flatten().float(), dim=0 + ) + assert cos_sim_x > 0.999, f"x grad cosine_sim={cos_sim_x:.6f}, expected > 0.999" + + @pytest.mark.parametrize( + "B,S,H,D", + [(2, 64, 32, 256), (2, 64, 4, 512)], + ids=["sliding_256", "global_512"], + ) + def test_weight_grad_matches_reference(self, B, S, H, D): + from transformers.models.gemma4.modeling_gemma4 import ( + Gemma4RMSNorm, + apply_rotary_pos_emb, + ) + + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + eps = 1e-6 + cos = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + weight_init = torch.randn(D, device="cuda", dtype=torch.bfloat16) + + # Reference + x_ref = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + norm_ref = Gemma4RMSNorm(D, eps=eps).cuda().to(torch.bfloat16) + norm_ref.weight = torch.nn.Parameter(weight_init.clone()) + apply_rotary_pos_emb( + norm_ref(x_ref), cos, sin, unsqueeze_dim=2 + ).sum().backward() + + # Fused + w_fused = weight_init.clone().requires_grad_(True) + fused_rms_norm_rope(x_ref.clone(), w_fused, cos, sin, eps=eps).sum().backward() + + cos_sim_w = torch.nn.functional.cosine_similarity( + w_fused.grad.flatten().float(), + norm_ref.weight.grad.flatten().float(), + dim=0, + ) + assert cos_sim_w > 0.995, ( + f"weight grad cosine_sim={cos_sim_w:.6f}, expected > 0.995" + ) + + def test_grad_flows(self): + """Verify gradients are non-zero and finite.""" + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D = 1, 16, 4, 64 + x = torch.randn( + B, S, H, D, device="cuda", dtype=torch.bfloat16, requires_grad=True + ) + w = torch.randn(D, device="cuda", dtype=torch.bfloat16, requires_grad=True) + cos = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + + y = fused_rms_norm_rope(x, w, cos, sin, eps=1e-6) + y.sum().backward() + + assert x.grad is not None, "x.grad is None" + assert w.grad is not None, "w.grad is None" + assert x.grad.isfinite().all(), "x.grad has non-finite values" + assert w.grad.isfinite().all(), "w.grad has non-finite values" + assert x.grad.abs().sum() > 0, "x.grad is all zeros" + assert w.grad.abs().sum() > 0, "w.grad is all zeros" + + +class TestFusedRMSNormNoScale: + """Tests for v_norm (RMSNorm without learnable scale).""" + + def test_forward_matches_reference(self, shapes, dtype): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_noscale + + B, S, H, D = shapes + eps = 1e-6 + x = torch.randn(B, S, H, D, device="cuda", dtype=dtype) + + y_ref = _reference_norm_noscale(x.clone(), eps) + y_fused = fused_rms_norm_noscale(x.clone(), eps=eps) + + cos_sim = torch.nn.functional.cosine_similarity( + y_ref.flatten().float(), y_fused.flatten().float(), dim=0 + ) + assert cos_sim > 0.999, f"v_norm cosine_sim={cos_sim:.6f}, expected > 0.999" + + def test_backward_flows(self): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_noscale + + x = torch.randn( + 1, 16, 4, 64, device="cuda", dtype=torch.bfloat16, requires_grad=True + ) + y = fused_rms_norm_noscale(x, eps=1e-6) + y.sum().backward() + + assert x.grad is not None + assert x.grad.isfinite().all() + assert x.grad.abs().sum() > 0 From 66c3e5a3fd523369b6e1c61925888264e9ab6e64 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 12 Apr 2026 10:57:45 -0400 Subject: [PATCH 1269/1405] better handling of dora merge on Conv layers in Qwen 3.5 (#3599) * better handling of dora merge on Conv layers in Qwen 3.5 * address issues from code review * stricter efficient merges for dora since we now have meta model to reference --- src/axolotl/cli/merge_lora.py | 1 + src/axolotl/cli/utils/lora_merge.py | 216 +++++++++++++++++++++++++++- tests/utils/lora/test_merge_lora.py | 23 +-- 3 files changed, 229 insertions(+), 11 deletions(-) diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index dae9f317d6..00e5303bd4 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -115,6 +115,7 @@ def _do_merge_lora_efficient(*, cfg: DictDefault) -> None: simulate_nf4_experts=simulate_nf4_experts, nf4_blocksize=nf4_blocksize, nf4_double_quant=nf4_double_quant, + trust_remote_code=bool(getattr(cfg, "trust_remote_code", False)), ) LOG.debug("Memory-efficient LoRA merge completed successfully!") diff --git a/src/axolotl/cli/utils/lora_merge.py b/src/axolotl/cli/utils/lora_merge.py index 339e41e2d5..b7c436aede 100644 --- a/src/axolotl/cli/utils/lora_merge.py +++ b/src/axolotl/cli/utils/lora_merge.py @@ -17,6 +17,93 @@ LOG = get_logger(__name__) +def _build_layer_type_map( + base_model_path: Path, trust_remote_code: bool = False +) -> dict[str, str]: + """Build a map of module_name -> layer_type using a meta-device model. + + Instantiates the model architecture on the meta device (zero memory) + to inspect which modules are Linear vs Conv1d/Conv2d/Conv3d. + This avoids relying on weight tensor ndim heuristics. + """ + import json as _json + + import torch.nn as nn + from transformers import AutoConfig + + config_path = base_model_path / "config.json" + if not config_path.exists(): + return {} + + try: + with open(config_path) as f: + model_config = _json.load(f) + except (OSError, _json.JSONDecodeError): + return {} + + architectures = model_config.get("architectures", []) + if not architectures: + return {} + + try: + config = AutoConfig.from_pretrained( + str(base_model_path), trust_remote_code=trust_remote_code + ) + except Exception: + LOG.debug("Could not load config for layer type introspection") + return {} + + # Determine the right Auto class from architectures + from transformers import ( + AutoModel, + AutoModelForCausalLM, + ) + + auto_classes = [AutoModelForCausalLM, AutoModel] + try: + from transformers import AutoModelForImageTextToText + + auto_classes.insert(0, AutoModelForImageTextToText) + except ImportError: + pass + + model = None + for auto_cls in auto_classes: + try: + with torch.device("meta"): + model = auto_cls.from_config( + config, trust_remote_code=trust_remote_code + ) + break + except Exception: # noqa: BLE001 + LOG.debug( + "Could not instantiate meta model with %s, trying next", + auto_cls.__name__, + ) + + if model is None: + LOG.debug("Could not instantiate meta model for layer type introspection") + return {} + + layer_types = {} + for name, module in model.named_modules(): + if isinstance(module, nn.Conv3d): + layer_types[name] = "Conv3d" + elif isinstance(module, nn.Conv2d): + layer_types[name] = "Conv2d" + elif isinstance(module, nn.Conv1d): + layer_types[name] = "Conv1d" + elif isinstance(module, nn.Linear): + layer_types[name] = "Linear" + + del model + LOG.debug( + f"Layer type map: {len(layer_types)} modules " + f"({sum(1 for v in layer_types.values() if 'Conv' in v)} conv layers)" + ) + return layer_types + + def _simulate_nf4_roundtrip( tensor: torch.Tensor, blocksize: Optional[int] = None, @@ -191,6 +278,7 @@ def _build_peft_layer_and_get_delta( adapter_name: str = "default", is_param_wrapper: bool = False, magnitude: Optional[torch.Tensor] = None, + layer_type: Optional[str] = None, ) -> torch.Tensor: """ Use PEFT's own layer classes to compute the LoRA delta weight. @@ -211,7 +299,7 @@ def _build_peft_layer_and_get_delta( out_features = lora_b.shape[0] lora_alpha = lora_config_dict.get("lora_alpha", lora_config_dict.get("r", 1)) use_rslora = bool(lora_config_dict.get("use_rslora", False)) - use_dora = bool(lora_config_dict.get("use_dora", False)) and magnitude is not None + use_dora = bool(lora_config_dict.get("use_dora", False)) if is_param_wrapper: from peft.tuners.lora.layer import ParamWrapper @@ -239,6 +327,77 @@ class _FakeModule(nn.Module): ) layer.lora_A[adapter_name].weight.data = lora_a layer.lora_B[adapter_name].weight.data = lora_b + return layer.get_delta_weight(adapter_name) + elif ( + layer_type and "Conv" in layer_type or (layer_type is None and lora_a.ndim > 2) + ): + # Conv layer detected via model introspection (or ndim fallback) + + from peft.tuners.lora import layer as peft_lora_layer + + # Determine conv type from layer_type map or fall back to ndim + if layer_type and "Conv" in layer_type: + conv_type: str = layer_type + else: + ndim = lora_a.ndim + _conv_map = {3: "Conv1d", 4: "Conv2d", 5: "Conv3d"} + if ndim not in _conv_map: + raise ValueError( + f"Unsupported LoRA weight dimensionality {ndim} for conv layer" + ) + conv_type = _conv_map[ndim] + LOG.warning( + f"Using ndim-based fallback for conv detection (ndim={ndim}). " + f"Consider providing layer_type from meta-device introspection." + ) + + conv_cls_map = {"Conv1d": nn.Conv1d, "Conv2d": nn.Conv2d, "Conv3d": nn.Conv3d} + ConvCls = conv_cls_map[conv_type] + PeftConvCls = getattr(peft_lora_layer, conv_type) + + # Reconstruct conv parameters from base tensor and lora_a shapes + # base_tensor: [out_channels, in_channels/groups, *kernel_size] + # lora_a: [r, in_channels/groups, *kernel_size] + # lora_b: [out_channels, r, *ones] + out_channels = base_tensor.shape[0] + in_channels = base_tensor.shape[1] + kernel_size = tuple(base_tensor.shape[2:]) + stride = (1,) * (base_tensor.ndim - 2) + padding = (0,) * (base_tensor.ndim - 2) + + base_layer = ConvCls( + in_channels, + out_channels, + kernel_size, + stride=stride, + padding=padding, + bias=False, + ) + base_layer.weight.data = base_tensor.clone() + + layer = PeftConvCls( + base_layer, + adapter_name=adapter_name, + r=r_total, + lora_alpha=lora_alpha, + use_rslora=use_rslora, + use_dora=use_dora, + ) + layer.lora_A[adapter_name].weight.data = lora_a + layer.lora_B[adapter_name].weight.data = lora_b + + if use_dora: + if magnitude is None: + raise ValueError( + f"DoRA merge requires a magnitude vector but none was found " + f"for conv layer (adapter={adapter_name}). Check that the " + f"adapter checkpoint contains lora_magnitude_vector weights." + ) + mag_layer = layer.lora_magnitude_vector[adapter_name] + mag_layer.weight = nn.Parameter(magnitude) + layer.merge(adapter_names=[adapter_name]) + return base_layer.weight.data - base_tensor + return layer.get_delta_weight(adapter_name) else: from peft.tuners.lora.layer import Linear as LoraLinear @@ -267,6 +426,12 @@ class _FakeModule(nn.Module): # DoRA merges magnitude normalization into the weight directly. # Use PEFT's merge() which handles DoRA internally, then # compute the delta as merged_weight - original_weight. + if magnitude is None: + raise ValueError( + f"DoRA merge requires a magnitude vector but none was found " + f"for linear layer (adapter={adapter_name}). Check that the " + f"adapter checkpoint contains lora_magnitude_vector weights." + ) mag_layer = layer.lora_magnitude_vector[adapter_name] mag_layer.weight = nn.Parameter(magnitude) layer.merge(adapter_names=[adapter_name]) @@ -382,6 +547,7 @@ def _merge_tensor_with_lora( nf4_double_quant: bool = True, use_dora: bool = False, weight_renamings: Optional[Dict[str, str]] = None, + layer_type_map: Optional[Dict[str, str]] = None, ) -> tuple[torch.Tensor, bool]: """ Helper function to merge a single tensor with its corresponding LoRA weights. @@ -426,12 +592,30 @@ def _merge_tensor_with_lora( if use_dora else None ) + + # Look up layer type from meta-device model introspection + _layer_type = None + if layer_type_map: + mod_path = key.rsplit(".weight", 1)[0] if key.endswith(".weight") else key + _layer_type = layer_type_map.get(mod_path) + # Try common prefix variations (e.g. with/without "model." prefix) + if _layer_type is None: + for prefix in [ + "model.", + "model.language_model.", + "model.language_model.model.", + ]: + _layer_type = layer_type_map.get(prefix + mod_path) + if _layer_type: + break + delta = _build_peft_layer_and_get_delta( lora_a.to(device), lora_b.to(device), lora_config_dict, tensor.to(device), magnitude=magnitude.to(device) if magnitude is not None else None, + layer_type=_layer_type, ) merged_tensor = ( (tensor.to(device).to(torch.float32) + delta.to(torch.float32)) @@ -556,6 +740,7 @@ def _fuse_and_unfuse_with_merge( nf4_double_quant: bool = True, use_dora: bool = False, weight_renamings: Optional[Dict[str, str]] = None, + layer_type_map: Optional[Dict[str, str]] = None, ) -> tuple[Dict[str, torch.Tensor], int, set]: """ For tensors matching WeightConverter patterns (MoE expert weights): @@ -696,12 +881,32 @@ def _fuse_and_unfuse_with_merge( if use_dora else None ) + # Look up layer type for the fused key + _layer_type = None + if layer_type_map: + mod_path = ( + fused_key.rsplit(".weight", 1)[0] + if fused_key.endswith(".weight") + else fused_key + ) + _layer_type = layer_type_map.get(mod_path) + if _layer_type is None: + for prefix in [ + "model.", + "model.language_model.", + "model.language_model.model.", + ]: + _layer_type = layer_type_map.get(prefix + mod_path) + if _layer_type: + break + delta = _build_peft_layer_and_get_delta( lora_a.to(device), lora_b.to(device), lora_config_dict, fused_tensor.to(device), magnitude=magnitude.to(device) if magnitude is not None else None, + layer_type=_layer_type, ) fused_tensor = ( ( @@ -740,6 +945,7 @@ def merge_lora_sharded_efficient( simulate_nf4_experts: bool = False, nf4_blocksize: Optional[int] = None, nf4_double_quant: bool = True, + trust_remote_code: bool = False, ) -> None: """ Memory-efficient LoRA merging that processes shards individually @@ -750,6 +956,8 @@ def merge_lora_sharded_efficient( simulate_nf4_experts: Apply NF4 roundtrip only to MoE expert tensors (for quantize_moe_experts). Expert tensors are identified by having "expert" in the key name and ndim >= 3. + trust_remote_code: Whether to trust remote code when loading model + config for layer-type introspection. Defaults to False for safety. """ base_model_path = Path(base_model_path) lora_adapter_path = Path(lora_adapter_path) @@ -780,6 +988,10 @@ def merge_lora_sharded_efficient( use_dora = bool(lora_config_dict.get("use_dora", False)) + # Build layer type map via meta-device model introspection + layer_type_map = _build_layer_type_map( + base_model_path, trust_remote_code=trust_remote_code + ) unsupported_methods = [] # Check for AdaLoRA (Adaptive LoRA) @@ -904,6 +1116,7 @@ def merge_lora_sharded_efficient( nf4_double_quant=nf4_double_quant, use_dora=use_dora, weight_renamings=weight_renamings, + layer_type_map=layer_type_map, ) merged_count += fused_merged @@ -926,6 +1139,7 @@ def merge_lora_sharded_efficient( nf4_double_quant=nf4_double_quant, use_dora=use_dora, weight_renamings=weight_renamings, + layer_type_map=layer_type_map, ) merged_tensors[key] = merged_tensor if was_merged: diff --git a/tests/utils/lora/test_merge_lora.py b/tests/utils/lora/test_merge_lora.py index e5d7f535d0..b66ee8bf46 100644 --- a/tests/utils/lora/test_merge_lora.py +++ b/tests/utils/lora/test_merge_lora.py @@ -2,6 +2,7 @@ import math from unittest.mock import Mock, patch +import pytest import safetensors.torch import torch @@ -773,8 +774,8 @@ def test_dora_end_to_end(self, tmp_path): "v_proj should be unchanged (no LoRA weights for it)" ) - def test_dora_missing_magnitude_falls_back(self): - """DoRA without magnitude vector falls back to standard LoRA merge.""" + def test_dora_missing_magnitude_raises(self): + """DoRA with missing magnitude vector raises an explicit error.""" hidden = 16 r = 4 alpha = 8 @@ -791,11 +792,13 @@ def test_dora_missing_magnitude_falls_back(self): } config = {"r": r, "lora_alpha": alpha, "use_dora": True} - merged, was_merged = _merge_tensor_with_lora( - base, "layer.proj.weight", lora_state, scale, config, "cpu", use_dora=True - ) - assert was_merged - # No magnitude vector → PEFT creates DoRA layer but with default magnitude, - # which produces a result different from plain W + scale * B @ A. - # Just verify it was merged (not unchanged). - assert not torch.equal(merged, base) + with pytest.raises(ValueError, match="DoRA merge requires a magnitude vector"): + _merge_tensor_with_lora( + base, + "layer.proj.weight", + lora_state, + scale, + config, + "cpu", + use_dora=True, + ) From a44edda6d790cfef00d7049a537ce9c779a9a2db Mon Sep 17 00:00:00 2001 From: Joaquin Hui <132194176+joaquinhuigomez@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:50:15 +0100 Subject: [PATCH 1270/1405] Skip redundant evaluation when resuming from checkpoint (#3575) [skip ci] * Skip redundant evaluation when resuming from checkpoint * add condition check for adding callback --------- Co-authored-by: Wing Lian --- src/axolotl/core/builders/base.py | 4 ++ src/axolotl/utils/callbacks/__init__.py | 50 +++++++++++++++ .../callbacks/test_skip_eval_on_resume.py | 63 +++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 tests/utils/callbacks/test_skip_eval_on_resume.py diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 9dba48b88e..9cceb1bf81 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -41,6 +41,7 @@ GCCallback, SaveAxolotlConfigtoWandBCallback, SaveModelOnFirstStepCallback, + SkipEvalOnResumeCallback, ) from axolotl.utils.callbacks.profiler import PytorchProfilerCallback from axolotl.utils.distributed import build_parallelism_config @@ -118,6 +119,9 @@ def get_callbacks(self) -> list[TrainerCallback]: plugin_manager.add_callbacks_pre_trainer(cfg=self.cfg, model=self.model) ) + if self.cfg.resume_from_checkpoint: + callbacks.append(SkipEvalOnResumeCallback()) + if self.cfg.gc_steps: callbacks.append(GCCallback(gc_steps=self.cfg.gc_steps)) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index afdb7f2a22..8137bac0c8 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -98,6 +98,56 @@ def on_step_end( return control +class SkipEvalOnResumeCallback(TrainerCallback): + """Skip the redundant evaluation that fires when resuming from a checkpoint + whose step aligns with ``eval_steps``. + + When HuggingFace Trainer resumes, it restores ``global_step`` from the + checkpoint and immediately triggers ``_maybe_log_save_evaluate`` for that + step. Because the evaluation was already performed during the original + run, repeating it wastes time and pollutes metric logs. + + This callback records the ``global_step`` at the start of training (i.e. + the checkpoint step when resuming, or 0 for a fresh run) and suppresses + any evaluation request on that exact step. + """ + + def __init__(self): + super().__init__() + self._resume_step: int | None = None + + def on_train_begin( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **_kwargs, + ): + # ``global_step`` is already restored from the checkpoint at this + # point. For a fresh run it will be 0, so the guard below becomes a + # no-op. + self._resume_step = state.global_step + + def on_step_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **_kwargs, + ) -> TrainerControl: + if ( + self._resume_step + and state.global_step <= self._resume_step + and control.should_evaluate + ): + LOG.info( + "Skipping evaluation at step %d (already completed before resume)", + state.global_step, + ) + control.should_evaluate = False + return control + + def bench_eval_callback_factory(trainer, tokenizer): accuracy = evaluate.load("accuracy") abcd_idx = [ diff --git a/tests/utils/callbacks/test_skip_eval_on_resume.py b/tests/utils/callbacks/test_skip_eval_on_resume.py new file mode 100644 index 0000000000..55cf274386 --- /dev/null +++ b/tests/utils/callbacks/test_skip_eval_on_resume.py @@ -0,0 +1,63 @@ +"""Tests for SkipEvalOnResumeCallback.""" + +from unittest.mock import MagicMock + +from transformers import TrainerControl, TrainerState, TrainingArguments + +from axolotl.utils.callbacks import SkipEvalOnResumeCallback + + +class TestSkipEvalOnResumeCallback: + """Tests for skipping redundant evaluation on checkpoint resume.""" + + @staticmethod + def _make_state(global_step: int) -> TrainerState: + state = MagicMock(spec=TrainerState) + state.global_step = global_step + return state + + def test_suppresses_eval_at_resume_step(self): + cb = SkipEvalOnResumeCallback() + args = MagicMock(spec=TrainingArguments) + state = self._make_state(20) + control = TrainerControl(should_evaluate=False) + + # Simulate on_train_begin at checkpoint-20 + cb.on_train_begin(args, state, control) + + # Trainer sets should_evaluate = True for step 20 + control.should_evaluate = True + result = cb.on_step_end(args, state, control) + + assert result.should_evaluate is False + + def test_allows_eval_after_resume_step(self): + cb = SkipEvalOnResumeCallback() + args = MagicMock(spec=TrainingArguments) + state = self._make_state(20) + control = TrainerControl(should_evaluate=False) + + cb.on_train_begin(args, state, control) + + # Advance past the resume point + state.global_step = 30 + control.should_evaluate = True + result = cb.on_step_end(args, state, control) + + assert result.should_evaluate is True + + def test_noop_on_fresh_run(self): + cb = SkipEvalOnResumeCallback() + args = MagicMock(spec=TrainingArguments) + state = self._make_state(0) + control = TrainerControl(should_evaluate=False) + + # Fresh run: global_step starts at 0 + cb.on_train_begin(args, state, control) + + # Even if eval triggers at step 0 (unlikely but defensive) + state.global_step = 10 + control.should_evaluate = True + result = cb.on_step_end(args, state, control) + + assert result.should_evaluate is True From 3985ec2f67145a7ced8978325c4ae419bdb8c68f Mon Sep 17 00:00:00 2001 From: madScientist10 <42779409+madScientist10@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:50:37 +0300 Subject: [PATCH 1271/1405] feat: add FineGrainedFP8Config support for model quantization (#3587) [skip ci] Allow loading FP8-quantized models (e.g. Mistral-Small-4-119B) with FineGrainedFP8Config and optional dequantize kwarg for full fine-tuning. Made-with: Cursor --- src/axolotl/loaders/model.py | 10 ++++++++++ src/axolotl/utils/schemas/model.py | 8 +++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 83b6452dc8..4f57793272 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -547,6 +547,16 @@ def _set_quantization_config(self): mxfp4_kwargs = self.cfg.model_quantization_config_kwargs self.model_kwargs["quantization_config"] = Mxfp4Config(**mxfp4_kwargs) + if self.cfg.model_quantization_config == "FineGrainedFP8Config": + from transformers import FineGrainedFP8Config + + fp8_kwargs = {} + if self.cfg.model_quantization_config_kwargs: + fp8_kwargs = self.cfg.model_quantization_config_kwargs + self.model_kwargs["quantization_config"] = FineGrainedFP8Config( + **fp8_kwargs + ) + if self.cfg.gptq: if not hasattr(self.model_config, "quantization_config"): LOG.warning( diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index 02b971c1d7..3c5dfc6e33 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -87,9 +87,11 @@ class ModelInputConfig(BaseModel): json_schema_extra={"description": "Use custom kernels, e.g. MegaBlocks."}, ) - model_quantization_config: Literal["Mxfp4Config"] | None = Field( - default=None, - json_schema_extra={"description": "Model loading quantization config"}, + model_quantization_config: Literal["Mxfp4Config", "FineGrainedFP8Config"] | None = ( + Field( + default=None, + json_schema_extra={"description": "Model loading quantization config"}, + ) ) model_quantization_config_kwargs: dict[str, Any] | None = Field( default=None, From 63a58cfec116f7518b08bca8debcbe8ef1370031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=86=E3=82=8A?= Date: Mon, 13 Apr 2026 08:51:10 +0800 Subject: [PATCH 1272/1405] feat: support excess_length_strategy for RL trainers (#3578) [skip ci] * feat: support excess_length_strategy for RL trainers Previously, RL data loading always dropped sequences exceeding sequence_len. This adds support for the existing `excess_length_strategy` config option (`drop`, `truncate`, `raise`) in RL training pipelines, matching the behavior already available for SFT. - `drop` (default): unchanged behavior, filters out long samples - `truncate`: tokenizes text components, truncates responses to fit within sequence_len while preserving the full prompt, then decodes back to text. Handles DPO/IPO/ORPO/SIMPO and KTO datasets. - `raise`: raises ValueError if any sample exceeds sequence_len Closes #3547 * improve RL truncation strategy robustness and performance --------- Co-authored-by: yurekami Co-authored-by: Wing Lian --- src/axolotl/utils/data/rl.py | 199 ++++++++++++++++++++++-- tests/utils/data/test_rl.py | 292 +++++++++++++++++++++++++++++++++++ 2 files changed, 475 insertions(+), 16 deletions(-) create mode 100644 tests/utils/data/test_rl.py diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index ef91e11246..c1d775cb42 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -180,6 +180,119 @@ def _drop_long_sequences( raise ValueError("Unknown RL type") +def _raise_on_long_sequences( + sample: dict[str, Any], rl: RLType, tokenizer: Any, sequence_len: int +) -> bool: + """Check sequence length and raise ValueError if exceeded. + + Used as a filter function for ``excess_length_strategy: raise``. + + Args: + sample: Dataset sample to check. + rl: Reinforcement learning type. + tokenizer: Tokenizer for length calculation. + sequence_len: Maximum allowed sequence length. + + Returns: + Always True (raises before returning False). + + Raises: + ValueError: If any sample exceeds the configured sequence length. + """ + is_valid = _drop_long_sequences(sample, rl, tokenizer, sequence_len) + if not is_valid: + raise ValueError( + f"Sample exceeds configured sequence_len ({sequence_len}). " + "Set `excess_length_strategy: drop` or `excess_length_strategy: truncate` " + "to handle long sequences automatically." + ) + return True + + +def _truncate_long_sequences_rl( + sample: dict[str, Any], rl: RLType, tokenizer: Any, sequence_len: int +) -> dict[str, Any]: + """Truncate RL samples that exceed maximum sequence length. + + For preference datasets (DPO/IPO/ORPO/SIMPO), truncates chosen and rejected + responses to fit within ``sequence_len`` when combined with the prompt. + For KTO, truncates the completion similarly. + GRPO/GDPO/EBFT samples are returned unchanged. + + Samples where the prompt alone exceeds ``sequence_len`` cannot be + meaningfully truncated and are returned unchanged. The caller should + follow up with a drop filter to remove them. + + Args: + sample: Dataset sample to potentially truncate. + rl: Reinforcement learning type. + tokenizer: Tokenizer for encoding/decoding. + sequence_len: Maximum allowed sequence length. + + Returns: + The sample with text fields truncated to fit within sequence_len. + """ + # Fast path: if sample already fits, return unchanged (avoids decode overhead) + if _drop_long_sequences(sample, rl, tokenizer, sequence_len): + return sample + + if rl in {RLType.DPO, RLType.IPO, RLType.ORPO, RLType.SIMPO}: + if not ( + sample.get("prompt") and sample.get("chosen") and sample.get("rejected") + ): + raise ValueError( + "Prompt, chosen and rejected keys are required for DPO/ORPO datasets" + ) + + prompt_ids = tokenizer(sample["prompt"], add_special_tokens=False)["input_ids"] + chosen_ids = tokenizer(sample["chosen"], add_special_tokens=False)["input_ids"] + rejected_ids = tokenizer(sample["rejected"], add_special_tokens=False)[ + "input_ids" + ] + + max_response_len = sequence_len - len(prompt_ids) + if max_response_len <= 0: + # Prompt alone exceeds limit; cannot meaningfully truncate. + # Returned unchanged — the follow-up drop filter will remove it. + return sample + + updates: dict[str, Any] = {} + if len(chosen_ids) > max_response_len: + updates["chosen"] = tokenizer.decode( + chosen_ids[:max_response_len], skip_special_tokens=False + ) + if len(rejected_ids) > max_response_len: + updates["rejected"] = tokenizer.decode( + rejected_ids[:max_response_len], skip_special_tokens=False + ) + if updates: + sample = {**sample, **updates} + + elif rl is RLType.KTO: + if not (sample.get("prompt") and sample.get("completion")): + raise ValueError("Prompt and completion keys are required for KTO datasets") + + prompt_ids = tokenizer(sample["prompt"], add_special_tokens=False)["input_ids"] + completion_ids = tokenizer(sample["completion"], add_special_tokens=False)[ + "input_ids" + ] + + max_completion_len = sequence_len - len(prompt_ids) + if max_completion_len <= 0: + return sample + + if len(completion_ids) > max_completion_len: + sample = { + **sample, + "completion": tokenizer.decode( + completion_ids[:max_completion_len], skip_special_tokens=False + ), + } + + # GRPO/GDPO/EBFT: no truncation needed (responses generated at runtime) + return sample + + def _load_split(cfg: DictDefault, split: Literal["train", "test"]) -> Dataset: """Load and process dataset split for RL training. @@ -243,23 +356,77 @@ def _load_split(cfg: DictDefault, split: Literal["train", "test"]) -> Dataset: split_datasets[i] = dataset if not cfg.skip_prepare_dataset: - drop_long = partial( - _drop_long_sequences, - rl=cfg.rl, - tokenizer=tokenizer, - sequence_len=cfg.sequence_len, - ) + excess_length_strategy = (cfg.excess_length_strategy or "drop").lower() + + if excess_length_strategy == "truncate": + truncate_fn = partial( + _truncate_long_sequences_rl, + rl=cfg.rl, + tokenizer=tokenizer, + sequence_len=cfg.sequence_len, + ) + prior_len = len(split_datasets[i]) + split_datasets[i] = split_datasets[i].map( + truncate_fn, + num_proc=cfg.dataset_num_proc, + load_from_cache_file=not cfg.is_preprocess, + desc="Truncating Long Sequences", + ) - prior_len = len(split_datasets[i]) - split_datasets[i] = split_datasets[i].filter( - drop_long, - num_proc=cfg.dataset_num_proc, - load_from_cache_file=not cfg.is_preprocess, - desc="Dropping Long Sequences", - ) - dropped = prior_len - len(split_datasets[i]) - if dropped: - LOG.warning(f"Dropped {dropped} long samples from dataset index {i}") + # Drop samples that could not be truncated (e.g. prompt + # alone exceeds sequence_len) + drop_long = partial( + _drop_long_sequences, + rl=cfg.rl, + tokenizer=tokenizer, + sequence_len=cfg.sequence_len, + ) + split_datasets[i] = split_datasets[i].filter( + drop_long, + num_proc=cfg.dataset_num_proc, + load_from_cache_file=not cfg.is_preprocess, + desc="Dropping Un-truncatable Sequences", + ) + dropped = prior_len - len(split_datasets[i]) + if dropped: + LOG.warning( + f"Dropped {dropped} samples from dataset index {i} " + f"that could not be truncated to fit sequence_len " + f"(prompt alone exceeds limit)" + ) + elif excess_length_strategy == "raise": + raise_fn = partial( + _raise_on_long_sequences, + rl=cfg.rl, + tokenizer=tokenizer, + sequence_len=cfg.sequence_len, + ) + split_datasets[i] = split_datasets[i].filter( + raise_fn, + num_proc=cfg.dataset_num_proc, + load_from_cache_file=not cfg.is_preprocess, + desc="Checking Sequence Lengths", + ) + else: # "drop" (default) + drop_long = partial( + _drop_long_sequences, + rl=cfg.rl, + tokenizer=tokenizer, + sequence_len=cfg.sequence_len, + ) + + prior_len = len(split_datasets[i]) + split_datasets[i] = split_datasets[i].filter( + drop_long, + num_proc=cfg.dataset_num_proc, + load_from_cache_file=not cfg.is_preprocess, + desc="Dropping Long Sequences", + ) + dropped = prior_len - len(split_datasets[i]) + if dropped: + LOG.warning( + f"Dropped {dropped} long samples from dataset index {i}" + ) # Merge datasets dataset = merge_datasets(split_datasets, cfg) diff --git a/tests/utils/data/test_rl.py b/tests/utils/data/test_rl.py new file mode 100644 index 0000000000..44c6a010dc --- /dev/null +++ b/tests/utils/data/test_rl.py @@ -0,0 +1,292 @@ +""" +Unit tests for RL data utility functions (excess_length_strategy support). +""" + +import unittest + +from axolotl.utils.data.rl import ( + _drop_long_sequences, + _raise_on_long_sequences, + _truncate_long_sequences_rl, +) +from axolotl.utils.schemas.enums import RLType + + +class _FakeTokenizer: + """Simple whitespace tokenizer for testing length calculations.""" + + def __call__(self, text, add_special_tokens=True): # noqa: ARG002 + tokens = text.split() + return {"input_ids": list(range(len(tokens)))} + + def decode(self, token_ids, skip_special_tokens=True): # noqa: ARG002 + # Each token id maps to a placeholder word; length is what matters. + return " ".join(f"w{i}" for i in range(len(token_ids))) + + +def _make_dpo_sample(prompt_len: int, chosen_len: int, rejected_len: int): + """Create a DPO sample with specified word counts.""" + return { + "prompt": " ".join(f"p{i}" for i in range(prompt_len)), + "chosen": " ".join(f"c{i}" for i in range(chosen_len)), + "rejected": " ".join(f"r{i}" for i in range(rejected_len)), + } + + +def _make_kto_sample(prompt_len: int, completion_len: int): + """Create a KTO sample with specified word counts.""" + return { + "prompt": " ".join(f"p{i}" for i in range(prompt_len)), + "completion": " ".join(f"c{i}" for i in range(completion_len)), + } + + +class TestDropLongSequences(unittest.TestCase): + """Tests for the existing _drop_long_sequences filter function.""" + + def setUp(self): + self.tokenizer = _FakeTokenizer() + + def test_dpo_keeps_short_samples(self): + sample = _make_dpo_sample(prompt_len=3, chosen_len=2, rejected_len=2) + result = _drop_long_sequences( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + self.assertTrue(result) + + def test_dpo_drops_long_chosen(self): + sample = _make_dpo_sample(prompt_len=5, chosen_len=10, rejected_len=2) + result = _drop_long_sequences( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + self.assertFalse(result) + + def test_dpo_drops_long_rejected(self): + sample = _make_dpo_sample(prompt_len=5, chosen_len=2, rejected_len=10) + result = _drop_long_sequences( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + self.assertFalse(result) + + def test_kto_keeps_short_samples(self): + sample = _make_kto_sample(prompt_len=3, completion_len=2) + result = _drop_long_sequences( + sample, RLType.KTO, self.tokenizer, sequence_len=10 + ) + self.assertTrue(result) + + def test_kto_drops_long_completion(self): + sample = _make_kto_sample(prompt_len=5, completion_len=10) + result = _drop_long_sequences( + sample, RLType.KTO, self.tokenizer, sequence_len=10 + ) + self.assertFalse(result) + + def test_grpo_always_keeps(self): + sample = {"prompt": "a " * 100} + result = _drop_long_sequences( + sample, RLType.GRPO, self.tokenizer, sequence_len=5 + ) + self.assertTrue(result) + + def test_dpo_missing_keys_raises(self): + with self.assertRaises(ValueError): + _drop_long_sequences({"prompt": "hi"}, RLType.DPO, self.tokenizer, 10) + + def test_kto_missing_keys_raises(self): + with self.assertRaises(ValueError): + _drop_long_sequences({"prompt": "hi"}, RLType.KTO, self.tokenizer, 10) + + def test_ipo_uses_dpo_logic(self): + sample = _make_dpo_sample(prompt_len=5, chosen_len=10, rejected_len=2) + result = _drop_long_sequences( + sample, RLType.IPO, self.tokenizer, sequence_len=10 + ) + self.assertFalse(result) + + def test_orpo_uses_dpo_logic(self): + sample = _make_dpo_sample(prompt_len=3, chosen_len=2, rejected_len=2) + result = _drop_long_sequences( + sample, RLType.ORPO, self.tokenizer, sequence_len=10 + ) + self.assertTrue(result) + + def test_boundary_length_kept(self): + """Samples exactly at sequence_len should be kept.""" + sample = _make_dpo_sample(prompt_len=5, chosen_len=5, rejected_len=5) + result = _drop_long_sequences( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + self.assertTrue(result) + + +class TestRaiseOnLongSequences(unittest.TestCase): + """Tests for _raise_on_long_sequences (excess_length_strategy='raise').""" + + def setUp(self): + self.tokenizer = _FakeTokenizer() + + def test_short_sample_passes(self): + sample = _make_dpo_sample(prompt_len=3, chosen_len=2, rejected_len=2) + result = _raise_on_long_sequences( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + self.assertTrue(result) + + def test_long_sample_raises_valueerror(self): + sample = _make_dpo_sample(prompt_len=5, chosen_len=10, rejected_len=2) + with self.assertRaises(ValueError, msg="excess_length_strategy"): + _raise_on_long_sequences( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + + def test_kto_long_raises(self): + sample = _make_kto_sample(prompt_len=5, completion_len=10) + with self.assertRaises(ValueError): + _raise_on_long_sequences( + sample, RLType.KTO, self.tokenizer, sequence_len=10 + ) + + def test_grpo_never_raises(self): + sample = {"prompt": "a " * 100} + result = _raise_on_long_sequences( + sample, RLType.GRPO, self.tokenizer, sequence_len=5 + ) + self.assertTrue(result) + + +class TestTruncateLongSequencesRL(unittest.TestCase): + """Tests for _truncate_long_sequences_rl (excess_length_strategy='truncate').""" + + def setUp(self): + self.tokenizer = _FakeTokenizer() + + def test_dpo_short_sample_unchanged(self): + sample = _make_dpo_sample(prompt_len=3, chosen_len=2, rejected_len=2) + result = _truncate_long_sequences_rl( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + self.assertEqual(result["chosen"], sample["chosen"]) + self.assertEqual(result["rejected"], sample["rejected"]) + + def test_dpo_truncates_chosen(self): + sample = _make_dpo_sample(prompt_len=5, chosen_len=10, rejected_len=3) + result = _truncate_long_sequences_rl( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + # max_response_len = 10 - 5 = 5, chosen had 10 words -> truncated to 5 + chosen_tokens = self.tokenizer(result["chosen"], add_special_tokens=False)[ + "input_ids" + ] + self.assertEqual(len(chosen_tokens), 5) + + def test_dpo_truncates_rejected(self): + sample = _make_dpo_sample(prompt_len=5, chosen_len=3, rejected_len=10) + result = _truncate_long_sequences_rl( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + rejected_tokens = self.tokenizer(result["rejected"], add_special_tokens=False)[ + "input_ids" + ] + self.assertEqual(len(rejected_tokens), 5) + + def test_dpo_truncates_both(self): + sample = _make_dpo_sample(prompt_len=5, chosen_len=10, rejected_len=10) + result = _truncate_long_sequences_rl( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + chosen_len = len( + self.tokenizer(result["chosen"], add_special_tokens=False)["input_ids"] + ) + rejected_len = len( + self.tokenizer(result["rejected"], add_special_tokens=False)["input_ids"] + ) + self.assertEqual(chosen_len, 5) + self.assertEqual(rejected_len, 5) + + def test_dpo_prompt_unchanged(self): + """Prompt text should never be modified.""" + sample = _make_dpo_sample(prompt_len=5, chosen_len=10, rejected_len=10) + result = _truncate_long_sequences_rl( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + self.assertEqual(result["prompt"], sample["prompt"]) + + def test_dpo_prompt_exceeds_limit_returns_unchanged(self): + """When prompt alone exceeds sequence_len, sample is returned as-is.""" + sample = _make_dpo_sample(prompt_len=15, chosen_len=3, rejected_len=3) + result = _truncate_long_sequences_rl( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + self.assertEqual(result, sample) + + def test_kto_truncates_completion(self): + sample = _make_kto_sample(prompt_len=5, completion_len=10) + result = _truncate_long_sequences_rl( + sample, RLType.KTO, self.tokenizer, sequence_len=10 + ) + completion_len = len( + self.tokenizer(result["completion"], add_special_tokens=False)["input_ids"] + ) + self.assertEqual(completion_len, 5) + + def test_kto_short_sample_unchanged(self): + sample = _make_kto_sample(prompt_len=3, completion_len=2) + result = _truncate_long_sequences_rl( + sample, RLType.KTO, self.tokenizer, sequence_len=10 + ) + self.assertEqual(result["completion"], sample["completion"]) + + def test_kto_prompt_exceeds_limit_returns_unchanged(self): + sample = _make_kto_sample(prompt_len=15, completion_len=3) + result = _truncate_long_sequences_rl( + sample, RLType.KTO, self.tokenizer, sequence_len=10 + ) + self.assertEqual(result, sample) + + def test_grpo_unchanged(self): + sample = {"prompt": "a " * 100} + result = _truncate_long_sequences_rl( + sample, RLType.GRPO, self.tokenizer, sequence_len=5 + ) + self.assertEqual(result, sample) + + def test_ipo_uses_dpo_logic(self): + sample = _make_dpo_sample(prompt_len=5, chosen_len=10, rejected_len=3) + result = _truncate_long_sequences_rl( + sample, RLType.IPO, self.tokenizer, sequence_len=10 + ) + chosen_len = len( + self.tokenizer(result["chosen"], add_special_tokens=False)["input_ids"] + ) + self.assertEqual(chosen_len, 5) + + def test_does_not_mutate_original(self): + """Verify immutability — original sample dict is not modified.""" + sample = _make_dpo_sample(prompt_len=5, chosen_len=10, rejected_len=10) + original_chosen = sample["chosen"] + original_rejected = sample["rejected"] + _truncate_long_sequences_rl(sample, RLType.DPO, self.tokenizer, sequence_len=10) + self.assertEqual(sample["chosen"], original_chosen) + self.assertEqual(sample["rejected"], original_rejected) + + def test_dpo_missing_keys_raises(self): + with self.assertRaises(ValueError): + _truncate_long_sequences_rl( + {"prompt": "hi"}, RLType.DPO, self.tokenizer, 10 + ) + + def test_kto_missing_keys_raises(self): + with self.assertRaises(ValueError): + _truncate_long_sequences_rl( + {"prompt": "hi"}, RLType.KTO, self.tokenizer, 10 + ) + + def test_boundary_no_truncation_needed(self): + """Samples exactly at sequence_len should not be modified.""" + sample = _make_dpo_sample(prompt_len=5, chosen_len=5, rejected_len=5) + result = _truncate_long_sequences_rl( + sample, RLType.DPO, self.tokenizer, sequence_len=10 + ) + self.assertEqual(result["chosen"], sample["chosen"]) + self.assertEqual(result["rejected"], sample["rejected"]) From 699047816372cbbbcbb60b230957f95318103fc0 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 13 Apr 2026 07:51:30 +0700 Subject: [PATCH 1273/1405] fix: rename model to adapter_model for fsdp sharded final model (#3585) * fix: rename model to adapter_model for fsdp sharded final model * fix: follow upstream transformer shard size * fix: handle multiple model files * fix redundant condition, tighten to safetensors, keep shard size small --------- Co-authored-by: Wing Lian --- src/axolotl/train.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 23388e40e2..0dfeb0c7fe 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -229,6 +229,28 @@ def execute_training( PLUGIN_MANAGER.post_train(cfg, trainer.model) +def _rename_fsdp_merged_to_adapter(merged_dir: Path): + """Rename model*.safetensors files to adapter_model* in place. + + Also rewrites the index JSON weight_map if sharded output was produced. + """ + for file in sorted(merged_dir.iterdir()): + if file.name.startswith("model") and ".safetensors" in file.name: + file.rename(merged_dir / file.name.replace("model", "adapter_model", 1)) + + index = merged_dir / "adapter_model.safetensors.index.json" + if index.exists(): + data = json.loads(index.read_text(encoding="utf-8")) + if "weight_map" in data: + data["weight_map"] = { + k: v.replace("model", "adapter_model", 1) + for k, v in data["weight_map"].items() + } + index.write_text( + json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + + def save_trained_model( cfg: DictDefault, trainer: Any, @@ -298,12 +320,17 @@ def save_trained_model( ) trainer.accelerator.wait_for_everyone() if trainer.accelerator.is_main_process: - # move all files in merged_path to cfg.output_dir + # FSDP checkpoints for PEFT only contain adapter weights; + # rename model* → adapter_model* so it loads correctly. + is_peft = cfg.adapter and not cfg.relora + if is_peft: + _rename_fsdp_merged_to_adapter(Path(merged_path)) for merged_file in Path(merged_path).iterdir(): - if (Path(cfg.output_dir) / merged_file.name).exists(): - (Path(cfg.output_dir) / merged_file.name).unlink() - shutil.move(str(merged_file), cfg.output_dir) - shutil.rmtree(merged_path) # remove what should be an empty dir + dest = Path(cfg.output_dir) / merged_file.name + if dest.exists(): + dest.unlink() + shutil.move(str(merged_file), dest) + shutil.rmtree(merged_path) # TODO(wing):see https://github.com/huggingface/transformers/pull/40207 # cleanup the FSDP prefix in the model config.json if trainer.accelerator.is_main_process: From 323da791eb3f1c3c005058ba56cc272835339eaa Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 15 Apr 2026 09:27:03 -0400 Subject: [PATCH 1274/1405] bump transformers to 5.5.4 and trl to latest 1.1.0 (#3603) * bump transformers to 5.5.4 and trl to latest 1.1.0 * more upgrades * update peft too * adapt lora_merge to peft 0.19 layer config API PEFT 0.19 requires a LoraConfig object on Linear/ParamWrapper/Conv layer constructors and moved use_rslora, use_dora, fan_in_fan_out, lora_dropout, and lora_bias into that config. Build the config per branch in _build_peft_layer_and_get_delta so the merge utility works with the upgraded peft. * allow lora_dropout on mixed attention+MoE configs under peft 0.19 PEFT 0.19's convert_peft_config_for_transformers auto-remaps old MoE target_modules (w1/w2/w3 on Mixtral, etc.) into target_parameters for transformers v5's fused 3D expert Parameters. Those targets get wrapped with ParamWrapper, which rejects lora_dropout != 0 because the 3D einsum can't factor dropout out of lora_B(lora_A(dropout(x))). Monkeypatch ParamWrapper.__init__ to internally use a copy of the LoraConfig with lora_dropout=0, so its dropout slot becomes nn.Identity while the shared config still delivers real dropout to sibling Linear LoRA layers (attention q/k/v/o). A probe runs the same conversion on a deep copy to detect the situation and emit a warning before patching. --- requirements.txt | 12 +-- src/axolotl/cli/utils/lora_merge.py | 34 +++++++-- src/axolotl/loaders/adapter.py | 109 ++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 12 deletions(-) diff --git a/requirements.txt b/requirements.txt index fe4b674367..bb3fc8daaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,15 +10,15 @@ liger-kernel==0.7.0 packaging==26.0 huggingface_hub>=1.1.7 -peft>=0.18.1 +peft>=0.19.0,<0.20.0 tokenizers>=0.22.1 -transformers==5.5.3 +transformers==5.5.4 accelerate==1.13.0 -datasets==4.5.0 +datasets>=4.8.4,<4.9.0 deepspeed>=0.18.6,<0.19.0 -trl==0.29.0 -hf_xet==1.3.2 -kernels==0.12.2 +trl==1.1.0 +hf_xet==1.4.3 +kernels==0.13.0 fla-core==0.4.1 flash-linear-attention==0.4.1 diff --git a/src/axolotl/cli/utils/lora_merge.py b/src/axolotl/cli/utils/lora_merge.py index b7c436aede..a07395587d 100644 --- a/src/axolotl/cli/utils/lora_merge.py +++ b/src/axolotl/cli/utils/lora_merge.py @@ -315,15 +315,27 @@ class _FakeModule(nn.Module): "weight", nn.Parameter(base_tensor.clone(), requires_grad=False) ) + # ParamWrapper rejects dropout/fan_in_fan_out/lora_bias/use_dora, so + # build a minimal config with only the fields it accepts. + pw_config = LoraConfig( + r=r, + lora_alpha=lora_alpha, + lora_dropout=0.0, + fan_in_fan_out=False, + use_rslora=use_rslora, + use_dora=False, + lora_bias=False, + ) + with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) layer = ParamWrapper( fake, adapter_name=adapter_name, parameter_name="weight", + config=pw_config, r=r, lora_alpha=lora_alpha, - use_rslora=use_rslora, ) layer.lora_A[adapter_name].weight.data = lora_a layer.lora_B[adapter_name].weight.data = lora_b @@ -375,13 +387,18 @@ class _FakeModule(nn.Module): ) base_layer.weight.data = base_tensor.clone() + conv_config = LoraConfig( + r=r_total, + lora_alpha=lora_alpha, + use_rslora=use_rslora, + use_dora=use_dora, + ) layer = PeftConvCls( base_layer, adapter_name=adapter_name, + config=conv_config, r=r_total, lora_alpha=lora_alpha, - use_rslora=use_rslora, - use_dora=use_dora, ) layer.lora_A[adapter_name].weight.data = lora_a layer.lora_B[adapter_name].weight.data = lora_b @@ -410,15 +427,20 @@ class _FakeModule(nn.Module): or lora_config_dict.get("lora_fan_in_fan_out", False) ) - layer = LoraLinear( - base_layer, - adapter_name=adapter_name, + linear_config = LoraConfig( r=r_total, lora_alpha=lora_alpha, fan_in_fan_out=fan_in_fan_out, use_rslora=use_rslora, use_dora=use_dora, ) + layer = LoraLinear( + base_layer, + adapter_name=adapter_name, + config=linear_config, + r=r_total, + lora_alpha=lora_alpha, + ) layer.lora_A[adapter_name].weight.data = lora_a layer.lora_B[adapter_name].weight.data = lora_b diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index 6d0bd0fe19..3d662c0bb3 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -124,6 +124,101 @@ def _patched( LoraModel._axolotl_clippable_patched = True +def _peft_will_auto_convert_target_params(model, lora_config) -> bool: + """Check whether PEFT will auto-populate target_parameters for this model. + + PEFT 0.19's ``convert_peft_config_for_transformers`` rewrites old MoE + ``target_modules`` (e.g. ``w1``/``w2``/``w3`` on Mixtral) into + ``target_parameters`` (``gate_up_proj``/``down_proj``) because + transformers v5 fused those expert linears into 3D ``nn.Parameter`` + tensors. PEFT wraps the resulting 3D params with ``ParamWrapper``, + which rejects ``lora_dropout != 0``. This probe runs the conversion on + a copy of the config so we can detect the situation before + ``get_peft_model`` blows up. + """ + if getattr(lora_config, "target_parameters", None): + return False + + try: + from peft.utils.transformers_weight_conversion import ( + convert_peft_config_for_transformers, + get_model_conversion_mapping, + ) + except ImportError: + return False + + import copy + + probe_cfg = copy.deepcopy(lora_config) + try: + convert_peft_config_for_transformers( + probe_cfg, + model=model, + conversions=get_model_conversion_mapping(model), + ) + except Exception: # pylint: disable=broad-except + return False + + return bool(getattr(probe_cfg, "target_parameters", None)) + + +def _patch_peft_param_wrapper_dropout(): + """Let PEFT's ``ParamWrapper`` silently accept ``lora_dropout != 0``. + + ``ParamWrapper`` wraps 3D expert ``nn.Parameter`` tensors and rejects + non-zero dropout because dropout can't be factored out of + ``lora_B(lora_A(dropout(x)))`` when the inner op is an expert-indexed + matmul. For mixed configs (attention + MoE experts) this is too + aggressive — the non-expert ``Linear`` LoRA layers *can* apply dropout + and that's usually what the user intended. We pass a copy of the + ``LoraConfig`` with ``lora_dropout=0`` only to ``ParamWrapper.__init__`` + so it builds with ``nn.Identity`` for its internal dropout slot while + every other layer type still receives the real dropout value. + """ + from peft.tuners.lora.layer import ParamWrapper + + if getattr(ParamWrapper, "_axolotl_dropout_patched", False): + return + + _orig_init = ParamWrapper.__init__ + + def _patched_init( + self, + base_layer, + adapter_name, + parameter_name, + config, + *args, + **kwargs, + ): + if getattr(config, "lora_dropout", 0): + import copy as _copy + + patched_config = _copy.copy(config) + patched_config.lora_dropout = 0.0 + return _orig_init( + self, + base_layer, + adapter_name, + parameter_name, + patched_config, + *args, + **kwargs, + ) + return _orig_init( + self, + base_layer, + adapter_name, + parameter_name, + config, + *args, + **kwargs, + ) + + ParamWrapper.__init__ = _patched_init + ParamWrapper._axolotl_dropout_patched = True + + def load_lora( model: PreTrainedModel, cfg: DictDefault, @@ -191,6 +286,20 @@ def load_lora( if config_only: return None, lora_config + if getattr( + lora_config, "lora_dropout", 0 + ) and _peft_will_auto_convert_target_params(model, lora_config): + LOG.warning( + "lora_dropout=%s requested but PEFT will wrap this model's fused " + "MoE expert parameters with ParamWrapper, which cannot apply " + "dropout (the 3D einsum can't factor dropout out of " + "lora_B(lora_A(dropout(x)))). Dropout will still be applied to " + "non-expert LoRA layers (e.g. attention), and expert LoRA layers " + "will use nn.Identity for the dropout slot.", + lora_config.lora_dropout, + ) + _patch_peft_param_wrapper_dropout() + rank = int(os.environ.get("LOCAL_RANK", 0)) if ( From 9de5b76336c5b658614e4d0c03cc45b303d7d8d4 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 21 Apr 2026 21:16:03 +0700 Subject: [PATCH 1275/1405] feat: move to uv first (#3545) * feat: move to uv first * fix: update doc to uv first * fix: merge dev/tests into uv pyproject * fix: update docker docs to match current config * fix: migrate examples to readme * fix: add llmcompressor to conflict * feat: rec uv sync with lockfile for dev/ci * fix: update docker docs to clarify how to use uv images * chore: docs * fix: use system python, no venv * fix: set backend cpu * fix: only set for installing pytorch step * fix: remove unsloth kernel and installs * fix: remove U in tests * fix: set backend in deps too * chore: test * chore: comments * fix: attempt to lock torch * fix: workaround torch cuda and not upgraded * fix: forgot to push * fix: missed source * fix: nightly upstream loralinear config * fix: nightly phi3 long rope not work * fix: forgot commit * fix: test phi3 template change * fix: no more requirements * fix: carry over changes from new requirements to pyproject * chore: remove lockfile per discussion * fix: set match-runtime * fix: remove unneeded hf hub buildtime * fix: duplicate cache delete on nightly * fix: torchvision being overridden * fix: migrate to uv images * fix: leftover from merge * fix: simplify base readme * fix: update assertion message to be clearer * chore: docs * fix: change fallback for cicd script * fix: match against main exactly * fix: peft 0.19.1 change * fix: e2e test * fix: ci * fix: e2e test --- .github/CONTRIBUTING.md | 5 +- .github/workflows/lint.yml | 2 +- .github/workflows/multi-gpu-e2e.yml | 35 ++- .github/workflows/pypi.yml | 13 +- .github/workflows/tests-nightly.yml | 53 ++-- .github/workflows/tests.yml | 85 +++--- MANIFEST.in | 3 +- README.md | 26 +- _quarto.yml | 2 - cicd/Dockerfile-uv.jinja | 23 +- cicd/Dockerfile.jinja | 54 ---- cicd/cicd.sh | 2 +- cicd/multigpu.py | 2 +- cicd/single_gpu.py | 2 +- docker/Dockerfile | 2 +- docker/Dockerfile-uv | 1 - docs/debugging.qmd | 13 +- docs/docker.qmd | 54 ++-- docs/installation.qmd | 120 ++++----- docs/unsloth.qmd | 53 ---- examples/LiquidAI/README.md | 7 +- examples/apertus/README.md | 9 +- examples/arcee/README.md | 3 +- examples/devstral/README.md | 3 +- examples/gemma3n/README.md | 7 +- examples/gpt-oss/README.md | 5 +- examples/granite4/README.md | 3 +- examples/hunyuan/README.md | 3 +- examples/internvl3_5/README.md | 2 +- examples/magistral/README.md | 3 +- examples/magistral/vision/README.md | 2 +- examples/ministral3/README.md | 2 +- examples/ministral3/vision/README.md | 2 +- examples/mistral-small/README.md | 2 +- examples/mistral4/README.md | 2 +- examples/qwen3-next/README.md | 2 +- examples/qwen3.5/README.md | 2 +- examples/seed-oss/README.md | 3 +- examples/smolvlm2/README.md | 5 +- examples/voxtral/README.md | 7 +- pyproject.toml | 203 +++++++++++++- requirements-dev.txt | 8 - requirements-tests.txt | 8 - requirements.txt | 78 ------ scripts/unsloth_install.py | 40 --- setup.py | 230 ---------------- src/axolotl/cli/utils/lora_merge.py | 6 +- .../kernels/libs/scattermoe_lora/layers.py | 47 +--- src/axolotl/loaders/patch_manager.py | 32 --- src/axolotl/monkeypatch/unsloth_.py | 252 ------------------ src/axolotl/utils/schemas/config.py | 26 +- src/axolotl/utils/schemas/validation.py | 61 ++--- ...setuptools_axolotl_dynamic_dependencies.py | 102 ------- tests/conftest.py | 4 +- .../test_scattermoe_lora_olmoe.py | 74 ++--- tests/e2e/patched/test_unsloth_integration.py | 21 -- tests/e2e/patched/test_unsloth_qlora.py | 184 ------------- .../test_dpo_chat_templates.py | 10 +- 58 files changed, 493 insertions(+), 1517 deletions(-) delete mode 100644 cicd/Dockerfile.jinja delete mode 100644 docs/unsloth.qmd delete mode 100644 requirements-dev.txt delete mode 100644 requirements-tests.txt delete mode 100644 requirements.txt delete mode 100644 scripts/unsloth_install.py delete mode 100644 setup.py delete mode 100644 src/axolotl/monkeypatch/unsloth_.py delete mode 100644 src/setuptools_axolotl_dynamic_dependencies.py delete mode 100644 tests/e2e/patched/test_unsloth_integration.py delete mode 100644 tests/e2e/patched/test_unsloth_qlora.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a0e4d3081f..6bb6a6b8f8 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -31,7 +31,10 @@ PRs are **greatly welcome**! Please run below to setup env ```bash -pip3 install -r requirements-dev.txt -r requirements-tests.txt +# Install axolotl + dev and test dependencies from lockfile +export UV_TORCH_BACKEND=cu128 # or cu130 +uv sync --extra flash-attn --extra deepspeed --group dev --group test +source .venv/bin/activate pre-commit install # test diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 181fd9dc97..e89e276423 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,7 @@ on: types: [opened, synchronize, reopened, ready_for_review] paths: - '**.py' - - 'requirements.txt' + - 'pyproject.toml' - '.github/workflows/*.yml' - "*.[q]md" - "examples/**/*.y[a]?ml" diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 2c5d76e4cf..03da58f7e1 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -3,17 +3,15 @@ name: docker-multigpu-tests-biweekly on: pull_request: paths: - - 'tests/e2e/multigpu/**.py' - - 'requirements.txt' - - 'setup.py' - - 'pyproject.toml' - - '.github/workflows/multi-gpu-e2e.yml' - - 'scripts/cutcrossentropy_install.py' - - 'src/axolotl/core/trainers/mixins/sequence_parallel.py' - - 'src/axolotl/utils/distributed.py' + - "tests/e2e/multigpu/**.py" + - "pyproject.toml" + - ".github/workflows/multi-gpu-e2e.yml" + - "scripts/cutcrossentropy_install.py" + - "src/axolotl/core/trainers/mixins/sequence_parallel.py" + - "src/axolotl/utils/distributed.py" workflow_dispatch: schedule: - - cron: '0 0 * * 1,4' # Runs at 00:00 UTC every monday & thursday + - cron: "0 0 * * 1,4" # Runs at 00:00 UTC every monday & thursday # Cancel jobs on the same ref if a new one is triggered concurrency: @@ -33,19 +31,19 @@ jobs: fail-fast: false matrix: include: -# - cuda: 129 -# cuda_version: 12.9.1 -# python_version: "3.12" -# pytorch: 2.9.1 -# axolotl_extras: "fbgemm-gpu" -# num_gpus: 2 -# dockerfile: "Dockerfile-uv.jinja" + # - cuda: 129 + # cuda_version: 12.9.1 + # python_version: "3.12" + # pytorch: 2.9.1 + # axolotl_extras: "fbgemm-gpu" + # num_gpus: 2 + # dockerfile: "Dockerfile-uv.jinja" - cuda: 130 cuda_version: 13.0.0 python_version: "3.11" pytorch: 2.9.1 axolotl_extras: -# axolotl_extras: fbgemm-gpu + # axolotl_extras: fbgemm-gpu num_gpus: 2 - cuda: 128 cuda_version: 12.8.1 @@ -53,7 +51,6 @@ jobs: pytorch: 2.10.0 axolotl_extras: "fbgemm-gpu" num_gpus: 2 - dockerfile: "Dockerfile-uv.jinja" runs-on: [self-hosted, modal] timeout-minutes: 120 steps: @@ -75,7 +72,7 @@ jobs: echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV + echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile-uv.jinja'}}" >> $GITHUB_ENV - name: Run tests job on Modal env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 19dface731..c2fc1c9d87 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -8,6 +8,9 @@ on: permissions: {} +env: + UV_SYSTEM_PYTHON: "1" + jobs: setup_release: name: Create Release @@ -41,11 +44,15 @@ jobs: with: python-version: "3.11" + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Install dependencies run: | - pip3 install wheel packaging==26.0 - pip3 install --no-build-isolation -e . - pip3 install -r requirements-dev.txt -r requirements-tests.txt + uv pip install wheel packaging + uv pip install --no-build-isolation -e . + uv pip install black mypy pre-commit types-requests quartodoc jupyter blobfile tiktoken \ + codecov codecov-cli pytest pytest-cov pytest-retry pytest-sugar pytest-xdist tbparse - name: Extract tag name id: tag diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 235aebcfaf..1802b63056 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -2,15 +2,18 @@ name: Tests Nightly against upstream main on: workflow_dispatch: schedule: - - cron: '0 0 * * *' # Runs at 00:00 UTC every day + - cron: "0 0 * * *" # Runs at 00:00 UTC every day pull_request: types: [opened, synchronize, reopened, ready_for_review] paths: - - '.github/workflows/tests-nightly.yml' + - ".github/workflows/tests-nightly.yml" permissions: contents: read +env: + UV_SYSTEM_PYTHON: "1" + jobs: pre-commit: name: pre-commit @@ -20,7 +23,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" - cache: 'pip' # caching pip dependencies + cache: "pip" # caching pip dependencies - uses: pre-commit/action@v3.0.1 env: SKIP: no-commit-to-branch @@ -43,7 +46,7 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.12"] # TODO include py3.14 once https://github.com/mistralai/mistral-common/pull/194 is merged + python_version: ["3.12"] # TODO include py3.14 once https://github.com/mistralai/mistral-common/pull/194 is merged pytorch_version: ["2.9.1", "2.10.0"] timeout-minutes: 20 @@ -61,36 +64,34 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} - cache: 'pip' # caching pip dependencies - - name: upgrade pip - run: | - pip3 install --upgrade pip - pip3 install --upgrade packaging==26.0 setuptools==78.1.1 wheel + - name: Install uv + uses: astral-sh/setup-uv@v7 - name: Install PyTorch run: | - pip3 install torch==${{ matrix.pytorch_version }} torchvision + uv pip install torch==${{ matrix.pytorch_version }} torchvision + uv pip freeze | grep -E "^(torch|torchvision)==" > /tmp/torch-pin.txt - - name: Update requirements.txt + - name: Install dependencies run: | - sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt - sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt - sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt - sed -i 's#^trl.*#trl @ git+https://github.com/huggingface/trl.git@main#' requirements.txt - sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt + uv pip install --no-build-isolation -e . --override /tmp/torch-pin.txt + python scripts/cutcrossentropy_install.py --uv | sh + uv pip install black mypy pre-commit types-requests quartodoc jupyter blobfile tiktoken \ + codecov codecov-cli pytest pytest-cov pytest-retry pytest-sugar pytest-xdist tbparse - - name: Install dependencies + - name: Override with nightly HF packages run: | - pip3 show torch - pip3 install --no-build-isolation -U -e . - python scripts/unsloth_install.py | sh - python scripts/cutcrossentropy_install.py | sh - pip3 install -r requirements-dev.txt -r requirements-tests.txt + uv pip install --no-deps \ + "transformers @ git+https://github.com/huggingface/transformers.git@main" \ + "peft @ git+https://github.com/huggingface/peft.git@main" \ + "accelerate @ git+https://github.com/huggingface/accelerate.git@main" \ + "trl @ git+https://github.com/huggingface/trl.git@main" \ + "datasets @ git+https://github.com/huggingface/datasets.git@main" - name: Make sure PyTorch version wasn't clobbered run: | - python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" + python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__, f'Expected torch ${{ matrix.pytorch_version }} but got {torch.__version__}'" - name: Ensure axolotl CLI was installed run: | @@ -102,9 +103,6 @@ jobs: pytest -v --durations=10 tests/patched/ pytest -v --durations=10 tests/cli/ - - name: cleanup pip cache - run: | - find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; docker-e2e-tests: if: github.repository_owner == 'axolotl-ai-cloud' @@ -136,7 +134,6 @@ jobs: pytorch: 2.9.1 num_gpus: 1 axolotl_extras: - dockerfile: "Dockerfile-uv.jinja" nightly_build: "true" steps: - name: Checkout @@ -157,7 +154,7 @@ jobs: echo "AXOLOTL_EXTRAS=${{ matrix.axolotl_extras}}" >> $GITHUB_ENV echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV + echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile-uv.jinja'}}" >> $GITHUB_ENV echo "NIGHTLY_BUILD=${{ matrix.nightly_build }}" >> $GITHUB_ENV - name: Run tests job on Modal env: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b1e9c718e2..e21e60ab57 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,21 +6,19 @@ on: branches: - "main" paths: - - '**.py' - - 'requirements.txt' - - '.github/workflows/*.yml' - - 'requirements-tests.txt' - - 'cicd/cicd.sh' - - 'cicd/Dockerfile.jinja' + - "**.py" + - "pyproject.toml" + - ".github/workflows/*.yml" + - "cicd/cicd.sh" + - "cicd/Dockerfile-uv.jinja" pull_request: - types: [opened, synchronize, reopened, ready_for_review] - paths: - - '**.py' - - 'requirements.txt' - - '.github/workflows/*.yml' - - 'requirements-tests.txt' - - 'cicd/cicd.sh' - - 'cicd/Dockerfile.jinja' + types: [opened, synchronize, reopened, ready_for_review] + paths: + - "**.py" + - "pyproject.toml" + - ".github/workflows/*.yml" + - "cicd/cicd.sh" + - "cicd/Dockerfile-uv.jinja" workflow_dispatch: # Cancel jobs on the same ref if a new one is triggered @@ -33,6 +31,7 @@ permissions: env: TRANSFORMERS_IS_CI: "yes" + UV_SYSTEM_PYTHON: "1" jobs: pre-commit: @@ -44,7 +43,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" - cache: 'pip' # caching pip dependencies + cache: "pip" # caching pip dependencies - uses: pre-commit/action@v3.0.1 env: SKIP: no-commit-to-branch @@ -94,32 +93,25 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} - cache: 'pip' # caching pip dependencies - - name: upgrade pip - run: | - pip3 install --upgrade pip - pip3 install --upgrade packaging==26.0 setuptools==75.8.0 wheel + - name: Install uv + uses: astral-sh/setup-uv@v7 - name: Install PyTorch run: | - pip3 install --no-cache-dir torch==${{ matrix.pytorch_version }} torchvision + uv pip install torch==${{ matrix.pytorch_version }} torchvision + uv pip freeze | grep -E "^(torch|torchvision)==" > /tmp/torch-pin.txt - name: Install dependencies run: | - pip3 show torch - pip3 install --no-cache-dir --no-build-isolation -U -e . - python scripts/unsloth_install.py | sh - python scripts/cutcrossentropy_install.py | sh - pip3 install -r requirements-dev.txt -r requirements-tests.txt - - - name: cleanup pip cache - run: | - find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + uv pip install --no-build-isolation -e . --override /tmp/torch-pin.txt + python scripts/cutcrossentropy_install.py --uv | sh + uv pip install black mypy pre-commit types-requests quartodoc jupyter blobfile tiktoken \ + codecov codecov-cli pytest pytest-cov pytest-retry pytest-sugar pytest-xdist tbparse - name: Make sure PyTorch version wasn't clobbered run: | - python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" + python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__, f'Expected torch ${{ matrix.pytorch_version }} but got {torch.__version__}'" - name: Ensure axolotl CLI was installed run: | @@ -188,33 +180,27 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} - cache: 'pip' # caching pip dependencies - - name: upgrade pip - run: | - pip3 install --upgrade pip - pip3 install --upgrade packaging==26.0 setuptools==75.8.0 setuptools_scm build wheel psutil + - name: Install uv + uses: astral-sh/setup-uv@v7 - name: Install PyTorch run: | - pip3 install --no-cache-dir torch==${{ matrix.pytorch_version }} torchvision + uv pip install torch==${{ matrix.pytorch_version }} torchvision + uv pip freeze | grep -E "^(torch|torchvision)==" > /tmp/torch-pin.txt - name: Install dependencies run: | - pip3 show torch + uv pip install packaging setuptools_scm build wheel psutil python -m build --no-isolation --sdist - pip3 install --no-cache-dir --no-build-isolation dist/axolotl*.tar.gz - python scripts/unsloth_install.py | sh - python scripts/cutcrossentropy_install.py | sh - pip3 install -r requirements-dev.txt -r requirements-tests.txt - - - name: cleanup pip cache - run: | - find "$(pip cache dir)/http-v2" -type f -mtime +14 -exec rm {} \; + uv pip install --no-build-isolation dist/axolotl*.tar.gz --override /tmp/torch-pin.txt + python scripts/cutcrossentropy_install.py --uv | sh + uv pip install black mypy pre-commit types-requests quartodoc jupyter blobfile tiktoken \ + codecov codecov-cli pytest pytest-cov pytest-retry pytest-sugar pytest-xdist tbparse - name: Make sure PyTorch version wasn't clobbered run: | - python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__" + python -c "import torch; assert '${{ matrix.pytorch_version }}' in torch.__version__, f'Expected torch ${{ matrix.pytorch_version }} but got {torch.__version__}'" - name: Ensure axolotl CLI was installed run: | @@ -291,7 +277,6 @@ jobs: pytorch: 2.9.1 num_gpus: 1 axolotl_extras: - dockerfile: "Dockerfile-uv.jinja" steps: - name: Checkout uses: actions/checkout@v4 @@ -312,7 +297,7 @@ jobs: echo "CUDA=${{ matrix.cuda }}" >> $GITHUB_ENV echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV + echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile-uv.jinja'}}" >> $GITHUB_ENV - name: Run tests job on Modal env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -374,7 +359,7 @@ jobs: echo "MODAL_IMAGE_BUILDER_VERSION=2024.10" >> $GITHUB_ENV echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV echo "GPU_TYPE=${{ matrix.gpu_type || 'L40S'}}" >> $GITHUB_ENV - echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile.jinja'}}" >> $GITHUB_ENV + echo "E2E_DOCKERFILE=${{ matrix.dockerfile || 'Dockerfile-uv.jinja'}}" >> $GITHUB_ENV - name: Run tests job on Modal env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/MANIFEST.in b/MANIFEST.in index 30cd072429..5cf08eabf0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ -include requirements.txt include README.md include LICENSE -include src/setuptools_axolotl_dynamic_dependencies.py +include VERSION include src/axolotl/utils/chat_templates/templates/*.jinja include AGENTS.md recursive-include docs/agents *.md diff --git a/README.md b/README.md index 063ec8d3b3..73e73d6aed 100644 --- a/README.md +++ b/README.md @@ -95,14 +95,11 @@ Features: ### Installation -#### Using uv (recommended) - ```bash -# install uv if you don't already have it installed +# install uv if you don't already have it installed (restart shell after) curl -LsSf https://astral.sh/uv/install.sh | sh -source $HOME/.local/bin/env -# CUDA 12.8.1 tends to have better package compatibility +# change depending on system export UV_TORCH_BACKEND=cu128 # create a new virtual environment @@ -112,23 +109,6 @@ source .venv/bin/activate uv pip install torch==2.10.0 torchvision uv pip install --no-build-isolation axolotl[deepspeed] -# recommended - install cut-cross-entropy -uv pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@main" - -# (optional) - prefetch flash-attn2 and causal-conv1d kernels -uv run --python 3.12 python -c "from kernels import get_kernel; get_kernel('kernels-community/flash-attn2'); get_kernel('kernels-community/causal-conv1d')" - -# Download example axolotl configs, deepspeed configs -axolotl fetch examples -axolotl fetch deepspeed_configs # OPTIONAL -``` - -#### Using pip - -```bash -pip3 install -U packaging==26.0 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] - # Download example axolotl configs, deepspeed configs axolotl fetch examples axolotl fetch deepspeed_configs # OPTIONAL @@ -138,7 +118,7 @@ axolotl fetch deepspeed_configs # OPTIONAL Installing with Docker can be less error prone than installing in your own environment. ```bash -docker run --gpus '"all"' --rm -it axolotlai/axolotl:main-latest +docker run --gpus '"all"' --ipc=host --rm -it axolotlai/axolotl:main-latest ``` Other installation approaches are described [here](https://docs.axolotl.ai/docs/installation.html). diff --git a/_quarto.yml b/_quarto.yml index 2916ef2ec0..e8263a971a 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -134,7 +134,6 @@ quartodoc: - monkeypatch.stablelm_attn_hijack_flash - monkeypatch.trainer_fsdp_optim - monkeypatch.transformers_fa_utils - - monkeypatch.unsloth_ - monkeypatch.data.batch_dataset_fetcher - monkeypatch.mixtral - monkeypatch.gradient_checkpointing.offload_cpu @@ -327,7 +326,6 @@ website: - section: "Advanced Features" contents: - docs/fsdp_qlora.qmd - - docs/unsloth.qmd - docs/torchao.qmd - docs/custom_integrations.qmd - docs/sequence_parallelism.qmd diff --git a/cicd/Dockerfile-uv.jinja b/cicd/Dockerfile-uv.jinja index 857b94c6b1..c24512ed3b 100644 --- a/cicd/Dockerfile-uv.jinja +++ b/cicd/Dockerfile-uv.jinja @@ -22,15 +22,6 @@ WORKDIR /workspace/axolotl RUN git fetch origin +$GITHUB_REF && \ git checkout FETCH_HEAD -# If AXOLOTL_EXTRAS is set, append it in brackets -RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ - sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt; \ - sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt; \ - sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt; \ - sed -i 's#^trl.*#trl @ git+https://github.com/huggingface/trl.git@main#' requirements.txt; \ - sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt; \ - fi - RUN uv pip install packaging==26.0 setuptools==78.1.1 RUN uv pip install torchvision RUN uv pip uninstall causal_conv1d @@ -40,11 +31,21 @@ RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ uv pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray] $AXOLOTL_ARGS; \ fi -RUN python scripts/unsloth_install.py --uv | sh +# Override with nightly HF packages for nightly builds +RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ + uv pip install --no-deps \ + "transformers @ git+https://github.com/huggingface/transformers.git@main" \ + "peft @ git+https://github.com/huggingface/peft.git@main" \ + "accelerate @ git+https://github.com/huggingface/accelerate.git@main" \ + "trl @ git+https://github.com/huggingface/trl.git@main" \ + "datasets @ git+https://github.com/huggingface/datasets.git@main"; \ + fi + RUN python scripts/cutcrossentropy_install.py --uv | sh # So we can test the Docker image -RUN uv pip install -r requirements-dev.txt -r requirements-tests.txt +RUN uv pip install black mypy pre-commit types-requests quartodoc jupyter blobfile tiktoken \ + codecov codecov-cli pytest pytest-cov pytest-retry pytest-sugar pytest-xdist tbparse # fix so that git fetch/pull from remote works RUN git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && \ diff --git a/cicd/Dockerfile.jinja b/cicd/Dockerfile.jinja deleted file mode 100644 index 7344f2a2c9..0000000000 --- a/cicd/Dockerfile.jinja +++ /dev/null @@ -1,54 +0,0 @@ -FROM axolotlai/axolotl-base:{{ BASE_TAG }} - -ENV TORCH_CUDA_ARCH_LIST="7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" -ENV AXOLOTL_EXTRAS="{{ AXOLOTL_EXTRAS }}" -ENV AXOLOTL_ARGS="{{ AXOLOTL_ARGS }}" -ENV CUDA="{{ CUDA }}" -ENV PYTORCH_VERSION="{{ PYTORCH_VERSION }}" -ENV GITHUB_REF="{{ GITHUB_REF }}" -ENV GITHUB_SHA="{{ GITHUB_SHA }}" -ENV NIGHTLY_BUILD="{{ NIGHTLY_BUILD }}" -ENV HF_HOME="{{ HF_HOME }}" -ENV AXOLOTL_DATASET_NUM_PROC="8" - -RUN apt-get update && \ - apt-get install -y --allow-change-held-packages vim curl nano zstd libnccl2 libnccl-dev ibverbs-providers ibverbs-utils infiniband-diags librdmacm-dev librdmacm1 rdmacm-utils slurm-wlm - -WORKDIR /workspace - -RUN git clone --depth=1 https://github.com/axolotl-ai-cloud/axolotl.git - -WORKDIR /workspace/axolotl - -RUN git fetch origin +$GITHUB_REF && \ - git checkout FETCH_HEAD - -# If AXOLOTL_EXTRAS is set, append it in brackets -RUN if [ "$NIGHTLY_BUILD" = "true" ] ; then \ - sed -i 's#^transformers.*#transformers @ git+https://github.com/huggingface/transformers.git@main#' requirements.txt; \ - sed -i 's#^peft.*#peft @ git+https://github.com/huggingface/peft.git@main#' requirements.txt; \ - sed -i 's#^accelerate.*#accelerate @ git+https://github.com/huggingface/accelerate.git@main#' requirements.txt; \ - sed -i 's#^trl.*#trl @ git+https://github.com/huggingface/trl.git@main#' requirements.txt; \ - sed -i 's#^datasets.*#datasets @ git+https://github.com/huggingface/datasets.git@main#' requirements.txt; \ - fi - -RUN pip install packaging==26.0 setuptools==78.1.1 psutil -RUN pip uninstall -y causal_conv1d -RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ - else \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray] $AXOLOTL_ARGS; \ - fi - -RUN python scripts/unsloth_install.py | sh -RUN python scripts/cutcrossentropy_install.py | sh - -# So we can test the Docker image -RUN pip install -r requirements-dev.txt -r requirements-tests.txt - -# fix so that git fetch/pull from remote works -RUN git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && \ - git config --get remote.origin.fetch - -# helper for huggingface-login cli -RUN git config --global credential.helper store diff --git a/cicd/cicd.sh b/cicd/cicd.sh index a3f17472a5..15a6f7ebf5 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__" +python -c "import torch; assert '$PYTORCH_VERSION' in torch.__version__, f'Expected torch $PYTORCH_VERSION but got {torch.__version__}'" set -o pipefail for i in 1 2 3; do diff --git a/cicd/multigpu.py b/cicd/multigpu.py index ed022c851f..5ee0bc49ab 100644 --- a/cicd/multigpu.py +++ b/cicd/multigpu.py @@ -17,7 +17,7 @@ template_env = jinja2.Environment( loader=template_loader, autoescape=select_autoescape() ) -dockerfile = os.environ.get("E2E_DOCKERFILE", "Dockerfile.jinja") +dockerfile = os.environ.get("E2E_DOCKERFILE", "Dockerfile-uv.jinja") df_template = template_env.get_template(dockerfile) df_args = { diff --git a/cicd/single_gpu.py b/cicd/single_gpu.py index 592b6b9312..9bd8d990e5 100644 --- a/cicd/single_gpu.py +++ b/cicd/single_gpu.py @@ -16,7 +16,7 @@ template_env = jinja2.Environment( loader=template_loader, autoescape=select_autoescape() ) -dockerfile = os.environ.get("E2E_DOCKERFILE", "Dockerfile.jinja") +dockerfile = os.environ.get("E2E_DOCKERFILE", "Dockerfile-uv.jinja") df_template = template_env.get_template(dockerfile) df_args = { diff --git a/docker/Dockerfile b/docker/Dockerfile index 5840c1f619..2bdb45b5c2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -32,7 +32,7 @@ RUN if [ "$TARGETARCH" = "arm64" ]; then \ pip install --no-build-isolation -e .[$BASE_EXTRAS,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ pip install --no-build-isolation -e .[$BASE_EXTRAS] $AXOLOTL_ARGS; \ - fi && \ python scripts/unsloth_install.py | sh && \ + fi && \ python scripts/cutcrossentropy_install.py | sh && \ pip install pytest && \ pip cache purge diff --git a/docker/Dockerfile-uv b/docker/Dockerfile-uv index 0142c0d2d3..df058baa3e 100644 --- a/docker/Dockerfile-uv +++ b/docker/Dockerfile-uv @@ -33,7 +33,6 @@ RUN if [ "$TARGETARCH" = "arm64" ]; then \ else \ uv pip install --no-build-isolation -e .[$BASE_EXTRAS] $AXOLOTL_ARGS; \ fi && \ - python scripts/unsloth_install.py --uv | sh && \ python scripts/cutcrossentropy_install.py --uv | sh && \ uv pip install pytest && \ uv cache clean diff --git a/docs/debugging.qmd b/docs/debugging.qmd index 36e39ef165..f3ca6ad9a9 100644 --- a/docs/debugging.qmd +++ b/docs/debugging.qmd @@ -76,8 +76,9 @@ datasets: Make sure you have an [editable install](https://setuptools.pypa.io/en/latest/userguide/development_mode.html) of Axolotl, which ensures that changes you make to the code are reflected at runtime. Run the following commands from the root of this project: ```bash -pip3 install packaging -pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' +export UV_TORCH_BACKEND=cu128 # or cu130 +uv sync --extra flash-attn --extra deepspeed --group dev --group test +source .venv/bin/activate ``` #### Remote Hosts @@ -208,17 +209,17 @@ cd axolotl Next, run the desired docker image and mount the current directory. Below is a docker command you can run to do this:[^2] ```bash -docker run --privileged --gpus '"all"' --shm-size 10g --rm -it --name axolotl --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 --mount type=bind,src="${PWD}",target=/workspace/axolotl -v ${HOME}/.cache/huggingface:/root/.cache/huggingface axolotlai/axolotl:main-py3.10-cu118-2.0.1 +docker run --privileged --gpus '"all"' --shm-size 10g --rm -it --name axolotl --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 --mount type=bind,src="${PWD}",target=/workspace/axolotl -v ${HOME}/.cache/huggingface:/root/.cache/huggingface axolotlai/axolotl-uv:main-latest ``` >[!Tip] > To understand which containers are available, see the [Docker section of the README](../README.md#docker) and the [DockerHub repo](https://hub.docker.com/r/axolotlai/axolotl/tags). For details of how the Docker containers are built, see axolotl's [Docker CI builds](../.github/workflows/main.yml). -You will now be in the container. Next, perform an editable install of Axolotl: +You will now be in the container. Next, install Axolotl with dev dependencies: ```bash -pip3 install packaging -pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' +uv sync --extra flash-attn --extra deepspeed --group dev --group test +source .venv/bin/activate ``` ### Attach To Container diff --git a/docs/docker.qmd b/docs/docker.qmd index 5d146eac23..001cf19a7e 100644 --- a/docs/docker.qmd +++ b/docs/docker.qmd @@ -6,23 +6,30 @@ format: toc-depth: 4 --- -This section describes the different Docker images that are released by AxolotlAI at [Docker Hub](https://hub.docker.com/u/axolotlai). +This section describes the different Docker images that are released by AxolotlAI at +[Docker Hub](https://hub.docker.com/u/axolotlai). ::: {.callout-important} -For Blackwell GPUs, please use the tags with PyTorch 2.7.1 and CUDA 12.8. +For Blackwell GPUs, please use the tags with PyTorch 2.9.1 and CUDA 12.8. +::: + +::: {.callout-tip} +Each image below is available in a **uv variant** that uses [uv](https://docs.astral.sh/uv/) with +a relocatable venv (`/workspace/axolotl-venv`) instead of Miniconda + pip. Append `-uv` to the image name +(e.g. `axolotlai/axolotl-base-uv`). Tags follow the same format. We recommend the uv images for new deployments. ::: ## Base -The base image is the most minimal image that can install Axolotl. It is based on the `nvidia/cuda` image. It includes python, torch, git, git-lfs, awscli, pydantic, and more. +The base image is the most minimal image that can install Axolotl. It is based on the `nvidia/cuda` image. +It includes python, torch, git, git-lfs, awscli, pydantic, and more. #### Image -``` -axolotlai/axolotl-base -``` - -Link: [Docker Hub](https://hub.docker.com/r/axolotlai/axolotl-base) +| Variant | Image | Docker Hub | +|---------|-------|------------| +| pip | `axolotlai/axolotl-base` | [Link](https://hub.docker.com/r/axolotlai/axolotl-base) | +| uv | `axolotlai/axolotl-base-uv` | [Link](https://hub.docker.com/r/axolotlai/axolotl-base-uv) | #### Tags format @@ -32,8 +39,10 @@ main-base-py{python_version}-cu{cuda_version}-{pytorch_version} Tags examples: -- `main-base-py3.11-cu128-2.8.0` - `main-base-py3.11-cu128-2.9.1` +- `main-base-py3.12-cu128-2.10.0` +- `main-base-py3.12-cu130-2.9.1` +- `main-base-py3.12-cu130-2.10.0` ## Main @@ -41,11 +50,10 @@ The main image is the image that is used to run Axolotl. It is based on the `axo #### Image -``` -axolotlai/axolotl -``` - -Link: [Docker Hub](https://hub.docker.com/r/axolotlai/axolotl) +| Variant | Image | Docker Hub | +|---------|-------|------------| +| pip | `axolotlai/axolotl` | [Link](https://hub.docker.com/r/axolotlai/axolotl) | +| uv | `axolotlai/axolotl-uv` | [Link](https://hub.docker.com/r/axolotlai/axolotl-uv) | #### Tags format {#sec-main-tags} @@ -53,7 +61,7 @@ Link: [Docker Hub](https://hub.docker.com/r/axolotlai/axolotl) # on push to main main-py{python_version}-cu{cuda_version}-{pytorch_version} -# latest main (currently torch 2.6.0, python 3.11, cuda 12.4) +# latest main (currently torch 2.9.1, python 3.11, cuda 12.8) main-latest # nightly build @@ -71,11 +79,12 @@ There may be some extra tags appended to the image, like `-vllm` which installs Tags examples: -- `main-py3.11-cu128-2.8.0` - `main-py3.11-cu128-2.9.1` +- `main-py3.12-cu128-2.10.0` +- `main-py3.12-cu130-2.9.1` +- `main-py3.12-cu130-2.10.0` - `main-latest` -- `main-20250303-py3.11-cu124-2.6.0` -- `main-20250303-py3.11-cu126-2.6.0` +- `main-20260315-py3.11-cu128-2.9.1` - `0.12.0` ## Cloud @@ -90,11 +99,10 @@ Jupyter lab is run by default. Set `JUPYTER_DISABLE=1` in the environment variab #### Image -``` -axolotlai/axolotl-cloud -``` - -Link: [Docker Hub](https://hub.docker.com/r/axolotlai/axolotl-cloud) +| Variant | Image | Docker Hub | +|---------|-------|------------| +| pip | `axolotlai/axolotl-cloud` | [Link](https://hub.docker.com/r/axolotlai/axolotl-cloud) | +| uv | `axolotlai/axolotl-cloud-uv` | [Link](https://hub.docker.com/r/axolotlai/axolotl-cloud-uv) | #### Tags format diff --git a/docs/installation.qmd b/docs/installation.qmd index 5df8f87e8a..9d1d0d4a14 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -15,64 +15,30 @@ This guide covers all the ways you can install and set up Axolotl for your envir - NVIDIA GPU (Ampere architecture or newer for `bf16` and Flash Attention) or AMD GPU - Python ≥3.11 -- PyTorch ≥2.6.0 +- PyTorch ≥2.9.0 -## Installation Methods {#sec-installation-methods} - -::: {.callout-important} -Please make sure to have Pytorch installed before installing Axolotl in your local environment. - -Follow the instructions at: [https://pytorch.org/get-started/locally/](https://pytorch.org/get-started/locally/) -::: +## Installation {#sec-installation} ::: {.callout-important} For Blackwell GPUs, please use Pytorch 2.9.1 and CUDA 12.8. ::: -### PyPI Installation (Recommended) {#sec-pypi} +### Quick Install {#sec-uv} -```{.bash} -pip3 install -U packaging setuptools wheel ninja -pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] -``` - -We use `--no-build-isolation` in order to detect the installed PyTorch version (if -installed) in order not to clobber it, and so that we set the correct version of -dependencies that are specific to the PyTorch version or other installed -co-dependencies. +Axolotl uses [uv](https://docs.astral.sh/uv/) as its package manager. uv is a fast, reliable Python package installer and resolver built in Rust. -### uv Installation {#sec-uv} - -uv is a fast, reliable Python package installer and resolver built in Rust. It offers significant performance improvements over pip and provides better dependency resolution, making it an excellent choice for complex environments. - -Install uv if not already installed +Install uv if not already installed: ```{.bash} curl -LsSf https://astral.sh/uv/install.sh | sh source $HOME/.local/bin/env ``` -Choose your CUDA version to use with PyTorch; e.g. `cu124`, `cu126`, `cu128`, -then create the venv and activate +Choose your CUDA version (e.g. `cu128`, `cu130`), create a venv, and install: ```{.bash} -export UV_TORCH_BACKEND=cu126 +export UV_TORCH_BACKEND=cu128 # or cu130 uv venv --no-project --relocatable source .venv/bin/activate -``` - -Install PyTorch -- PyTorch 2.6.0 recommended -```{.bash} -uv pip install packaging setuptools wheel -uv pip install torch==2.6.0 -uv pip install awscli pydantic -``` - -Install axolotl from PyPi -```{.bash} -uv pip install --no-build-isolation axolotl[deepspeed,flash-attn] - -# optionally install with vLLM if you're using torch==2.6.0 and want to train w/ GRPO -uv pip install --no-build-isolation axolotl[deepspeed,flash-attn,vllm] +uv pip install --no-build-isolation axolotl[flash-attn,deepspeed] ``` ### Edge/Development Build {#sec-edge-build} @@ -82,14 +48,17 @@ For the latest features between releases: ```{.bash} git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -pip3 install -U packaging setuptools wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' +export UV_TORCH_BACKEND=cu128 # or cu130 +uv sync --extra flash-attn --extra deepspeed +source .venv/bin/activate ``` +`uv sync` creates a `.venv`, installs exact pinned versions from `uv.lock`, and sets up an editable install automatically. + ### Docker {#sec-docker} ```{.bash} -docker run --gpus '"all"' --rm -it axolotlai/axolotl:main-latest +docker run --gpus '"all"' --rm -it --ipc=host axolotlai/axolotl-uv:main-latest ``` For development with Docker: @@ -106,12 +75,12 @@ docker run --privileged --gpus '"all"' --shm-size 10g --rm -it \ --ulimit memlock=-1 --ulimit stack=67108864 \ --mount type=bind,src="${PWD}",target=/workspace/axolotl \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ - axolotlai/axolotl:main-latest + axolotlai/axolotl-uv:main-latest ``` ::: ::: {.callout-important} -For Blackwell GPUs, please use `axolotlai/axolotl:main-py3.11-cu128-2.9.1` or the cloud variant `axolotlai/axolotl-cloud:main-py3.11-cu128-2.9.1`. +For Blackwell GPUs, please use `axolotlai/axolotl-uv:main-py3.11-cu128-2.9.1` or the cloud variant `axolotlai/axolotl-cloud-uv:main-py3.11-cu128-2.9.1`. ::: Please refer to the [Docker documentation](docker.qmd) for more information on the different Docker images that are available. @@ -122,7 +91,7 @@ Please refer to the [Docker documentation](docker.qmd) for more information on t For providers supporting Docker: -- Use `axolotlai/axolotl-cloud:main-latest` +- Use `axolotlai/axolotl-cloud-uv:main-latest` - Available on: - [RunPod](https://runpod.io/gsc?template=v2ickqhz9s&ref=6i7fkpdz) - [Vast.ai](https://cloud.vast.ai?ref_id=62897&template_id=bdd4a49fa8bce926defc99471864cace&utm_source=axolotl&utm_medium=partner&utm_campaign=template_launch_july2025&utm_content=docs_link) @@ -141,7 +110,7 @@ For providers supporting Docker: ### macOS {#sec-macos} ```{.bash} -pip3 install --no-build-isolation -e '.' +uv pip install --no-build-isolation -e '.' ``` See @sec-troubleshooting for Mac-specific issues. @@ -152,21 +121,44 @@ See @sec-troubleshooting for Mac-specific issues. We recommend using WSL2 (Windows Subsystem for Linux) or Docker. ::: -## Environment Managers {#sec-env-managers} - -### Conda/Pip venv {#sec-conda} - -1. Install Python ≥3.11 -2. Install PyTorch: https://pytorch.org/get-started/locally/ -3. Install Axolotl: - ```{.bash} - pip3 install -U packaging setuptools wheel ninja - pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' - ``` -4. (Optional) Login to Hugging Face: - ```{.bash} - hf auth login - ``` +## Migrating from pip to uv {#sec-migrating} + +If you have an existing pip-based Axolotl installation, you can migrate to uv: + +```{.bash} +# Install uv +curl -LsSf https://astral.sh/uv/install.sh | sh +source $HOME/.local/bin/env + +# Create a fresh venv (recommended for a clean start) +export UV_TORCH_BACKEND=cu128 # or cu130 +uv venv --no-project --relocatable +source .venv/bin/activate + +# Reinstall axolotl +uv pip install --no-build-isolation axolotl[flash-attn,deepspeed] +``` + +## Using pip (Alternative) {#sec-pip} + +If you are unable to install uv, you can still use pip directly. + +::: {.callout-important} +Please make sure to have PyTorch installed before installing Axolotl with pip. + +Follow the instructions at: [https://pytorch.org/get-started/locally/](https://pytorch.org/get-started/locally/) +::: + +```{.bash} +pip3 install -U packaging setuptools wheel ninja +pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] +``` + +For editable/development installs: +```{.bash} +pip3 install -U packaging setuptools wheel ninja +pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' +``` ## Troubleshooting {#sec-troubleshooting} diff --git a/docs/unsloth.qmd b/docs/unsloth.qmd deleted file mode 100644 index fd87f7bde0..0000000000 --- a/docs/unsloth.qmd +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "Unsloth" -description: "Hyper-optimized QLoRA finetuning for single GPUs" ---- - -### Overview - -Unsloth provides hand-written optimized kernels for LLM finetuning that slightly improve speed and VRAM over -standard industry baselines. - -::: {.callout-important} -Due to breaking changes in transformers `v4.48.0`, users will need to downgrade to `<=v4.47.1` to use this patch. - -This will later be deprecated in favor of [LoRA Optimizations](lora_optims.qmd). -::: - - -### Installation - -The following will install the correct unsloth and extras from source. - -```bash -python scripts/unsloth_install.py | sh -``` - -### Usage - -Axolotl exposes a few configuration options to try out unsloth and get most of the performance gains. - -Our unsloth integration is currently limited to the following model architectures: - - llama - -These options are specific to LoRA finetuning and cannot be used for multi-GPU finetuning -```yaml -unsloth_lora_mlp: true -unsloth_lora_qkv: true -unsloth_lora_o: true -``` - -These options are composable and can be used with multi-gpu finetuning -```yaml -unsloth_cross_entropy_loss: true -unsloth_rms_norm: true -unsloth_rope: true -``` - -### Limitations - -- Single GPU only; e.g. no multi-gpu support -- No deepspeed or FSDP support (requires multi-gpu) -- LoRA + QLoRA support only. No full fine tunes or fp8 support. -- Limited model architecture support. Llama, Phi, Gemma, Mistral only -- No MoE support. diff --git a/examples/LiquidAI/README.md b/examples/LiquidAI/README.md index 8a18d9eb12..0a08692d76 100644 --- a/examples/LiquidAI/README.md +++ b/examples/LiquidAI/README.md @@ -15,8 +15,7 @@ Thanks to the team at LiquidAI for giving us early access to prepare for these r Here is an example of how to install from pip: ```bash # Ensure you have a compatible version of Pytorch installed - pip3 install packaging setuptools wheel ninja - pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' + uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` 2. Run one of the finetuning examples below. @@ -35,7 +34,7 @@ Thanks to the team at LiquidAI for giving us early access to prepare for these r **LFM2-MoE** ```bash - pip install git+https://github.com/huggingface/transformers.git@0c9a72e4576fe4c84077f066e585129c97bfd4e6 + uv pip install git+https://github.com/huggingface/transformers.git@0c9a72e4576fe4c84077f066e585129c97bfd4e6 # LoRA SFT (1x48GB @ 16.2GiB) axolotl train examples/LiquidAI/lfm2-8b-a1b-lora.yaml @@ -45,7 +44,7 @@ Thanks to the team at LiquidAI for giving us early access to prepare for these r - **Installation Error**: If you encounter `ImportError: ... undefined symbol ...` or `ModuleNotFoundError: No module named 'causal_conv1d_cuda'`, the `causal-conv1d` package may have been installed incorrectly. Try uninstalling it: ```bash - pip uninstall -y causal-conv1d + uv pip uninstall causal-conv1d ``` - **Dataset Loading**: Read more on how to load your own dataset in our [documentation](https://docs.axolotl.ai/docs/dataset_loading.html). diff --git a/examples/apertus/README.md b/examples/apertus/README.md index 1cb4d413ce..1280e430a2 100644 --- a/examples/apertus/README.md +++ b/examples/apertus/README.md @@ -15,8 +15,7 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn]' +uv pip install --no-build-isolation -e '.[flash-attn]' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy python scripts/cutcrossentropy_install.py | sh @@ -31,7 +30,7 @@ python scripts/cutcrossentropy_install.py | sh # For those using our Docker image, use the below path. export CUDA_HOME=/usr/local/cuda -pip3 install git+https://github.com/nickjbrowning/XIELU@59d6031 --no-build-isolation --no-deps +uv pip install git+https://github.com/nickjbrowning/XIELU@59d6031 --no-build-isolation --no-deps ``` For any installation errors, see [XIELU Installation Issues](#xielu-installation-issues) @@ -67,7 +66,7 @@ If those didn't help, please try the below solutions: 1. Pass env for CMAKE and try install again: ```bash - Python_EXECUTABLE=$(which python) pip3 install git+https://github.com/nickjbrowning/XIELU@59d6031 --no-build-isolation --no-deps + Python_EXECUTABLE=$(which python) uv pip install git+https://github.com/nickjbrowning/XIELU@59d6031 --no-build-isolation --no-deps ``` 2. Git clone the repo and manually hardcode python path: @@ -92,7 +91,7 @@ If those didn't help, please try the below solutions: ``` ```bash - pip3 install . --no-build-isolation --no-deps + uv pip install . --no-build-isolation --no-deps ``` ## Optimization Guides diff --git a/examples/arcee/README.md b/examples/arcee/README.md index ad554532c1..deaea676a1 100644 --- a/examples/arcee/README.md +++ b/examples/arcee/README.md @@ -17,8 +17,7 @@ Thanks to the team at Arcee.ai for using Axolotl in supervised fine-tuning the A git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn]' +uv pip install --no-build-isolation -e '.[flash-attn]' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy python scripts/cutcrossentropy_install.py | sh diff --git a/examples/devstral/README.md b/examples/devstral/README.md index 5a0145f103..2be8f62921 100644 --- a/examples/devstral/README.md +++ b/examples/devstral/README.md @@ -16,8 +16,7 @@ Thanks to the team at MistralAI for giving us early access to prepare for this r ```bash # Ensure you have Pytorch installed (Pytorch 2.6.0 min) -pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' +uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` 2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage diff --git a/examples/gemma3n/README.md b/examples/gemma3n/README.md index 4808ed81be..1ecc96cbc0 100644 --- a/examples/gemma3n/README.md +++ b/examples/gemma3n/README.md @@ -10,17 +10,16 @@ Gemma-3n is a family of multimodal models from Google found on [HuggingFace](htt ```bash # Ensure you have Pytorch installed (Pytorch 2.6.0 min) -pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' +uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` 2. In addition to Axolotl's requirements, Gemma-3n requires: ```bash -pip3 install timm==1.0.17 +uv pip install timm==1.0.17 # for loading audio data -pip3 install librosa==0.11.0 +uv pip install librosa==0.11.0 ``` 3. Download sample dataset files diff --git a/examples/gpt-oss/README.md b/examples/gpt-oss/README.md index 8c407540ed..0e5eac5009 100644 --- a/examples/gpt-oss/README.md +++ b/examples/gpt-oss/README.md @@ -14,8 +14,7 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations ```bash # Ensure you have Pytorch installed (Pytorch 2.6.0 min) -pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' +uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` 2. Choose one of the following configs below for training the 20B model. (for 120B, see [below](#training-120b)) @@ -87,7 +86,7 @@ for more information about using a special vllm-openai docker image for inferenc Optionally, vLLM can be installed from nightly: ```bash -pip install --no-build-isolation --pre -U vllm --extra-index-url https://wheels.vllm.ai/nightly +uv pip install --no-build-isolation --pre -U vllm --extra-index-url https://wheels.vllm.ai/nightly ``` and the vLLM server can be started with the following command (modify `--tensor-parallel-size 8` to match your environment): ```bash diff --git a/examples/granite4/README.md b/examples/granite4/README.md index 0495394054..ceb599c1ca 100644 --- a/examples/granite4/README.md +++ b/examples/granite4/README.md @@ -15,8 +15,7 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn]' +uv pip install --no-build-isolation -e '.[flash-attn]' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy python scripts/cutcrossentropy_install.py | sh diff --git a/examples/hunyuan/README.md b/examples/hunyuan/README.md index 59e9a28c78..3071a0a61e 100644 --- a/examples/hunyuan/README.md +++ b/examples/hunyuan/README.md @@ -13,8 +13,7 @@ Tencent released a family of opensource models called HunYuan with varying param git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn]' +uv pip install --no-build-isolation -e '.[flash-attn]' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy python scripts/cutcrossentropy_install.py | sh diff --git a/examples/internvl3_5/README.md b/examples/internvl3_5/README.md index d2584bb80a..7424385bb3 100644 --- a/examples/internvl3_5/README.md +++ b/examples/internvl3_5/README.md @@ -11,7 +11,7 @@ This guide shows how to fine-tune it with Axolotl. 2. Install `timm` for vision model support: ```bash - pip install timm==1.0.19 + uv pip install timm==1.0.19 ``` 3. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. diff --git a/examples/magistral/README.md b/examples/magistral/README.md index 2e162df6b6..172a40b2c9 100644 --- a/examples/magistral/README.md +++ b/examples/magistral/README.md @@ -14,8 +14,7 @@ Thanks to the team at MistralAI for giving us early access to prepare for these ```bash # Ensure you have Pytorch installed (Pytorch 2.7.0 min) -pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' +uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` 2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage diff --git a/examples/magistral/vision/README.md b/examples/magistral/vision/README.md index 72a8a22153..8babc904ac 100644 --- a/examples/magistral/vision/README.md +++ b/examples/magistral/vision/README.md @@ -12,7 +12,7 @@ Before starting, ensure you have: 1. Install the required vision lib: ```bash - pip install 'mistral-common[opencv]==1.8.5' + uv pip install 'mistral-common[opencv]==1.8.5' ``` 2. Download the example dataset image: diff --git a/examples/ministral3/README.md b/examples/ministral3/README.md index 6ed7efda57..72f21b746a 100644 --- a/examples/ministral3/README.md +++ b/examples/ministral3/README.md @@ -23,7 +23,7 @@ Note: This is still experimental given it is based on transformers v5 RC. git checkout transformers-v5 # Install packages for transformers v5 - pip install -e . + uv pip install -e . ``` 4. Run the fine-tuning: diff --git a/examples/ministral3/vision/README.md b/examples/ministral3/vision/README.md index 8193573eb4..cc9a0f38b3 100644 --- a/examples/ministral3/vision/README.md +++ b/examples/ministral3/vision/README.md @@ -12,7 +12,7 @@ Before starting, ensure you have: 1. Install the required vision lib: ```bash - pip install 'mistral-common[opencv]==1.8.6' + uv pip install 'mistral-common[opencv]==1.8.6' ``` 2. Download the example dataset image: diff --git a/examples/mistral-small/README.md b/examples/mistral-small/README.md index 7f7ec91e61..c5120aab79 100644 --- a/examples/mistral-small/README.md +++ b/examples/mistral-small/README.md @@ -12,7 +12,7 @@ Before starting, ensure you have: 1. Install the required vision lib: ```bash - pip install 'mistral-common[opencv]==1.8.5' + uv pip install 'mistral-common[opencv]==1.8.5' ``` 2. Download the example dataset image: diff --git a/examples/mistral4/README.md b/examples/mistral4/README.md index 3151069bac..ccbb03b216 100644 --- a/examples/mistral4/README.md +++ b/examples/mistral4/README.md @@ -13,7 +13,7 @@ Thanks to the team at MistralAI for giving us early access to prepare for this r 3. Install transformers from main ```bash - pip install git+https://github.com/huggingface/transformers.git + uv pip install git+https://github.com/huggingface/transformers.git ``` 4. Run one of the example configs: diff --git a/examples/qwen3-next/README.md b/examples/qwen3-next/README.md index df87ca7252..05b512d290 100644 --- a/examples/qwen3-next/README.md +++ b/examples/qwen3-next/README.md @@ -12,7 +12,7 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations 3. Install FLA for improved performance ```bash -pip3 uninstall -y causal-conv1d && pip3 install flash-linear-attention==0.4.1 +uv pip uninstall causal-conv1d && uv pip install flash-linear-attention==0.4.1 ``` 4. Run the finetuning example: diff --git a/examples/qwen3.5/README.md b/examples/qwen3.5/README.md index b5089d7270..22e9e360dc 100644 --- a/examples/qwen3.5/README.md +++ b/examples/qwen3.5/README.md @@ -10,7 +10,7 @@ 3. Install FLA for sample packing support with the Gated DeltaNet linear attention layers: ```bash - pip3 uninstall -y causal-conv1d && pip3 install flash-linear-attention==0.4.1 + uv pip uninstall causal-conv1d && uv pip install flash-linear-attention==0.4.1 ``` > FLA is required when `sample_packing: true`. Without it, training raises a `RuntimeError` on packed sequences. Vision configs use `sample_packing: false` so FLA is optional there. diff --git a/examples/seed-oss/README.md b/examples/seed-oss/README.md index aeb8635e31..796ef118d0 100644 --- a/examples/seed-oss/README.md +++ b/examples/seed-oss/README.md @@ -11,8 +11,7 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations Here is an example of how to install from pip: ```bash # Ensure you have a compatible version of Pytorch installed - pip3 install packaging setuptools wheel ninja - pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' + uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' # Install Cut Cross Entropy python scripts/cutcrossentropy_install.py | sh diff --git a/examples/smolvlm2/README.md b/examples/smolvlm2/README.md index 74c1a1c0fa..da83e612c6 100644 --- a/examples/smolvlm2/README.md +++ b/examples/smolvlm2/README.md @@ -13,14 +13,13 @@ This guide shows how to fine-tune SmolVLM2 models with Axolotl. Here is an example of how to install from pip: ```bash # Ensure you have a compatible version of Pytorch installed - pip3 install packaging setuptools wheel ninja - pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' + uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` 2. Install an extra dependency: ```bash - pip3 install num2words==0.5.14 + uv pip install num2words==0.5.14 ``` 3. Run the finetuning example: diff --git a/examples/voxtral/README.md b/examples/voxtral/README.md index 2d3cad4e9a..ed5cc64227 100644 --- a/examples/voxtral/README.md +++ b/examples/voxtral/README.md @@ -12,16 +12,15 @@ Thanks to the team at MistralAI for giving us early access to prepare for this r ```bash # Ensure you have Pytorch installed (Pytorch 2.6.0 min) -pip3 install packaging==26.0 setuptools==75.8.0 wheel ninja -pip3 install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' +uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' ``` 2. Please install the below. ```bash # audio -pip3 install librosa==0.11.0 -pip3 install 'mistral_common[audio]==1.8.3' +uv pip install librosa==0.11.0 +uv pip install 'mistral_common[audio]==1.8.3' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy python scripts/cutcrossentropy_install.py | sh diff --git a/pyproject.toml b/pyproject.toml index 9cee4a5204..d028b394de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,165 @@ [build-system] -requires = ["setuptools>=64", "wheel", "setuptools_scm>=8", "packaging==26.0"] +requires = ["setuptools>=64", "wheel", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" [project] name = "axolotl" -dynamic = ["version", "dependencies", "optional-dependencies"] +dynamic = ["version"] description = "LLM Trainer" readme = "README.md" requires-python = ">=3.10" # license = "Apache-2.0" +dependencies = [ + # Core ML stack + "torch>=2.6.0", + "packaging==26.0", + "huggingface_hub>=1.1.7", + "peft>=0.19.1,<0.20.0", + "tokenizers>=0.22.1", + "transformers==5.5.4", + "accelerate==1.13.0", + "datasets>=4.8.4,<4.9.0", + "trl==1.1.0", + "hf_xet==1.4.3", + "kernels==0.13.0", + "trackio>=0.16.1", + "typing-extensions>=4.15.0", + "optimum==1.16.2", + "hf_transfer", + "sentencepiece", + "gradio>=6.2.0,<7.0", + "modal==1.3.0.post1", + "pydantic>=2.10.6", + "addict", + "fire", + "PyYAML>=6.0", + "requests", + "wandb", + "einops", + "colorama", + "numba>=0.61.2", + "numpy>=2.2.6", + + # Evaluation & metrics + "evaluate==0.4.1", + "scipy", + "nvidia-ml-py==12.560.30", + "art", + "tensorboard", + "python-dotenv==1.0.1", + + # Remote filesystems + "s3fs>=2024.5.0", + "gcsfs>=2025.3.0", + "adlfs>=2024.5.0", + "ocifs==1.3.2", + + "zstandard==0.22.0", + "fastcore", + + # lm eval harness + "lm_eval==0.4.11", + "langdetect==1.0.9", + "immutabledict==4.2.0", + "antlr4-python3-runtime==4.13.2", + + "schedulefree==1.4.1", + "openenv-core==0.1.0", + + # Axolotl contribs + "axolotl-contribs-lgpl==0.0.7", + "axolotl-contribs-mit==0.0.6", + + # Telemetry + "posthog==6.7.11", + + "mistral-common==1.11.0", + + # Platform-specific (Linux only) + "bitsandbytes==0.49.1 ; sys_platform != 'darwin'", + "triton>=3.4.0 ; sys_platform != 'darwin'", + "xformers>=0.0.23.post1 ; sys_platform != 'darwin'", + "liger-kernel==0.7.0 ; sys_platform != 'darwin'", + "torchao==0.17.0 ; sys_platform != 'darwin' and platform_machine != 'aarch64'", + + # Architecture-specific + "fla-core==0.4.1 ; platform_machine != 'aarch64'", + "flash-linear-attention==0.4.1 ; platform_machine != 'aarch64'", +] + +[project.optional-dependencies] +flash-attn = ["flash-attn==2.8.3"] +ring-flash-attn = [ + "flash-attn==2.8.3", + "ring-flash-attn>=0.1.7", +] +deepspeed = [ + "deepspeed>=0.18.6,<0.19.0", + "deepspeed-kernels", +] +mamba-ssm = [ + "mamba-ssm==1.2.0.post1", + "causal_conv1d", +] +auto-gptq = [ + "auto-gptq==0.5.1", +] +mlflow = [ + "mlflow", +] +galore = [ + "galore_torch", +] +apollo = [ + "apollo-torch", +] +optimizers = [ + "galore_torch", + "apollo-torch", + "lomo-optim==0.1.1", + "torch-optimi==0.2.1", + "came_pytorch==0.1.3", +] +ray = [ + "ray[train]>=2.52.1", +] +vllm = [ + "vllm>=0.15.0", +] +llmcompressor = [ + "llmcompressor>=0.10.0", +] +fbgemm-gpu = ["fbgemm-gpu-genai>=1.3.0"] +opentelemetry = [ + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-exporter-prometheus", + "prometheus-client", +] + +[dependency-groups] +dev = [ + "black", + "mypy", + "pre-commit", + "types-requests", + "quartodoc", + "jupyter", + "blobfile", + "tiktoken", +] +test = [ + "codecov", + "codecov-cli", + "pytest", + "pytest-cov", + "pytest-retry", + "pytest-sugar", + "pytest-xdist", + "tbparse", +] + [project.scripts] axolotl = "axolotl.cli.main:main" @@ -18,18 +168,15 @@ Homepage = "https://axolotl.ai/" Documentation = "https://docs.axolotl.ai/" Repository = "https://github.com/axolotl-ai-cloud/axolotl.git" -[tool.setuptools_scm] - [tool.setuptools] -py-modules = ["setuptools_axolotl_dynamic_dependencies"] include-package-data = true +[tool.setuptools.packages.find] +where = ["src"] + [tool.setuptools.dynamic] version = { file = "VERSION" } -[tool.setuptools.cmdclass] -build_py = "setuptools_axolotl_dynamic_dependencies.BuildPyCommand" - [tool.ruff] line-length = 88 target-version = "py310" @@ -67,5 +214,43 @@ markers = [ "slow: marks tests as slow", ] +# UV specific configuration +[tool.uv] +prerelease = "allow" +conflicts = [ + [ + { package = "axolotl" }, + { extra = "vllm" }, + ], + [ + { package = "axolotl" }, + { extra = "flash-attn" }, + ], + [ + { package = "axolotl" }, + { extra = "ring-flash-attn" }, + ], + [ + { package = "axolotl" }, + { extra = "mamba-ssm" }, + ], + [ + { package = "axolotl" }, + { extra = "auto-gptq" }, + ], + [ + { package = "axolotl" }, + { extra = "fbgemm-gpu" }, + ], + [ + { package = "axolotl" }, + { extra = "llmcompressor" }, + ], +] + [tool.uv.extra-build-dependencies] -axolotl = ["huggingface_hub"] +mamba-ssm = [{ requirement = "torch", match-runtime = true }] +causal-conv1d = [{ requirement = "torch", match-runtime = true }] +flash-attn = [{ requirement = "torch", match-runtime = true }] +deepspeed = [{ requirement = "torch", match-runtime = true }] +auto-gptq = [{ requirement = "torch", match-runtime = true }] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 5c42d96d4f..0000000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,8 +0,0 @@ -black -mypy -pre-commit -types-requests -quartodoc -jupyter -blobfile -tiktoken diff --git a/requirements-tests.txt b/requirements-tests.txt deleted file mode 100644 index 93b2ceee59..0000000000 --- a/requirements-tests.txt +++ /dev/null @@ -1,8 +0,0 @@ -codecov -codecov-cli -pytest -pytest-cov -pytest-retry -pytest-sugar -pytest-xdist -tbparse diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index bb3fc8daaf..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,78 +0,0 @@ ---extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/ - -# START section of dependencies that don't install on Darwin/MacOS -bitsandbytes==0.49.1 -triton>=3.4.0 -mamba-ssm==1.2.0.post1 -xformers>=0.0.23.post1 -liger-kernel==0.7.0 -# END section - -packaging==26.0 -huggingface_hub>=1.1.7 -peft>=0.19.0,<0.20.0 -tokenizers>=0.22.1 -transformers==5.5.4 -accelerate==1.13.0 -datasets>=4.8.4,<4.9.0 -deepspeed>=0.18.6,<0.19.0 -trl==1.1.0 -hf_xet==1.4.3 -kernels==0.13.0 - -fla-core==0.4.1 -flash-linear-attention==0.4.1 - -trackio>=0.16.1 -typing-extensions>=4.15.0 - -optimum==1.16.2 -hf_transfer -sentencepiece -gradio>=6.2.0,<7.0 - -modal==1.3.0.post1 -pydantic>=2.10.6 -addict -fire -PyYAML>=6.0 -requests -wandb -einops -colorama -numba>=0.61.2 -numpy>=2.2.6 - -# qlora things -evaluate==0.4.1 -scipy -nvidia-ml-py==12.560.30 -art -tensorboard -python-dotenv==1.0.1 - -# remote filesystems -s3fs>=2024.5.0 -gcsfs>=2025.3.0 -adlfs>=2024.5.0 -ocifs==1.3.2 - -zstandard==0.22.0 -fastcore - -# lm eval harness -lm_eval==0.4.11 -langdetect==1.0.9 -immutabledict==4.2.0 -antlr4-python3-runtime==4.13.2 - -torchao==0.17.0 -openenv-core==0.1.0 -schedulefree==1.4.1 - -axolotl-contribs-lgpl==0.0.7 -axolotl-contribs-mit==0.0.6 -# telemetry -posthog==6.7.11 - -mistral-common==1.11.0 diff --git a/scripts/unsloth_install.py b/scripts/unsloth_install.py deleted file mode 100644 index c0e5bbe70f..0000000000 --- a/scripts/unsloth_install.py +++ /dev/null @@ -1,40 +0,0 @@ -# noqa -import sys - -try: - import torch -except ImportError as error: - raise ImportError("Install torch via `pip install torch`") from error -from packaging.version import Version as V - -use_uv = "--uv" in sys.argv[1:] - -v = V(torch.__version__) -cuda = str(torch.version.cuda) -try: - is_ampere = torch.cuda.get_device_capability()[0] >= 8 -except RuntimeError: - is_ampere = False -if cuda != "12.1" and cuda != "11.8" and cuda != "12.4": - raise RuntimeError(f"CUDA = {cuda} not supported!") -if v <= V("2.1.0"): - raise RuntimeError(f"Torch = {v} too old!") -elif v <= V("2.1.1"): - x = "cu{}{}-torch211" -elif v <= V("2.1.2"): - x = "cu{}{}-torch212" -elif v < V("2.3.0"): - x = "cu{}{}-torch220" -elif v < V("2.4.0"): - x = "cu{}{}-torch230" -elif v < V("2.5.0"): - x = "cu{}{}-torch240" -elif v < V("2.6.0"): - x = "cu{}{}-torch250" -else: - raise RuntimeError(f"Torch = {v} too new!") -x = x.format(cuda.replace(".", ""), "-ampere" if is_ampere else "") -uv_prefix = "uv " if use_uv else "" -print( - f'{uv_prefix}pip install unsloth-zoo==2024.12.1 && {uv_prefix}pip install --no-deps "unsloth[{x}]==2024.12.4"' -) diff --git a/setup.py b/setup.py deleted file mode 100644 index 99879597b8..0000000000 --- a/setup.py +++ /dev/null @@ -1,230 +0,0 @@ -"""setup.py for axolotl""" - -import os -import platform -import re -from importlib.metadata import PackageNotFoundError, version -from pathlib import Path - -from setuptools import find_packages, setup - - -def parse_requirements(extras_require_map): - _install_requires = [] - _dependency_links = [] - with open("./requirements.txt", encoding="utf-8") as requirements_file: - lines = [r.strip() for r in requirements_file.readlines()] - for line in lines: - is_extras = "deepspeed" in line or "mamba-ssm" in line - if line.startswith("--extra-index-url"): - # Handle custom index URLs - _, url = line.split() - _dependency_links.append(url) - elif not is_extras and line and line[0] != "#": - # Handle standard packages - _install_requires.append(line) - try: - xformers_version = [req for req in _install_requires if "xformers" in req][0] - install_xformers = platform.machine() != "aarch64" - if platform.machine() == "aarch64": - # skip on ARM64 - skip_packages = [ - "torchao", - "fla-core", - "flash-linear-attention", - ] - _install_requires = [ - req - for req in _install_requires - if re.split(r"[>=<]", req)[0].strip() not in skip_packages - ] - if "Darwin" in platform.system(): - # skip packages not compatible with OSX - skip_packages = [ - "bitsandbytes", - "triton", - "mamba-ssm", - "xformers", - "liger-kernel", - ] - _install_requires = [ - req - for req in _install_requires - if re.split(r"[>=<]", req)[0].strip() not in skip_packages - ] - print( - _install_requires, [req in skip_packages for req in _install_requires] - ) - else: - # detect the version of torch already installed - # and set it so dependencies don't clobber the torch version - try: - torch_version = version("torch") - except PackageNotFoundError: - torch_version = "2.8.0" # default to torch 2.8.0 - _install_requires.append(f"torch=={torch_version}") - - version_match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", torch_version) - if version_match: - major, minor, patch = version_match.groups() - major, minor = int(major), int(minor) - patch = ( - int(patch) if patch is not None else 0 - ) # Default patch to 0 if not present - else: - raise ValueError("Invalid version format") - - torch_parts = torch_version.split("+") - if len(torch_parts) == 2: - torch_cuda_version = torch_parts[1] - _dependency_links.append( - f"https://download.pytorch.org/whl/{torch_cuda_version}" - ) - - if (major, minor) >= (2, 10): - extras_require_map.pop("fbgemm-gpu") - extras_require_map["fbgemm-gpu"] = [ - "fbgemm-gpu==1.5.0", - "fbgemm-gpu-genai==1.5.0", - ] - if not install_xformers: - _install_requires.pop(_install_requires.index(xformers_version)) - extras_require_map["vllm"] = ["vllm>=0.19.0"] - elif (major, minor) >= (2, 9): - extras_require_map.pop("fbgemm-gpu") - extras_require_map["fbgemm-gpu"] = [ - "fbgemm-gpu==1.4.0", - "fbgemm-gpu-genai==1.4.2", - ] - if not install_xformers: - _install_requires.pop(_install_requires.index(xformers_version)) - if patch == 0: - extras_require_map["vllm"] = ["vllm==0.13.0"] - else: - extras_require_map["vllm"] = ["vllm==0.14.0"] - elif (major, minor) >= (2, 8): - extras_require_map.pop("fbgemm-gpu") - extras_require_map["fbgemm-gpu"] = ["fbgemm-gpu-genai==1.3.0"] - extras_require_map["vllm"] = ["vllm==0.11.0"] - if not install_xformers: - _install_requires.pop(_install_requires.index(xformers_version)) - elif (major, minor) >= (2, 7): - _install_requires.pop(_install_requires.index(xformers_version)) - if patch == 0: - if install_xformers: - _install_requires.append("xformers==0.0.30") - # vllm 0.9.x is incompatible with latest transformers - extras_require_map.pop("vllm") - else: - if install_xformers: - _install_requires.append("xformers==0.0.31") - extras_require_map["vllm"] = ["vllm==0.10.1"] - elif (major, minor) >= (2, 6): - _install_requires.pop(_install_requires.index(xformers_version)) - if install_xformers: - _install_requires.append("xformers==0.0.29.post3") - # since we only support 2.6.0+cu126 - _dependency_links.append("https://download.pytorch.org/whl/cu126") - extras_require_map.pop("vllm") - elif (major, minor) >= (2, 5): - _install_requires.pop(_install_requires.index(xformers_version)) - if install_xformers: - if patch == 0: - _install_requires.append("xformers==0.0.28.post2") - else: - _install_requires.append("xformers>=0.0.28.post3") - extras_require_map.pop("vllm") - elif (major, minor) >= (2, 4): - extras_require_map.pop("vllm") - if install_xformers: - if patch == 0: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.27") - else: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers==0.0.28.post1") - else: - raise ValueError("axolotl requires torch>=2.4") - - except PackageNotFoundError: - pass - return _install_requires, _dependency_links, extras_require_map - - -def get_package_version(): - with open( - Path(os.path.dirname(os.path.abspath(__file__))) / "VERSION", - "r", - encoding="utf-8", - ) as fin: - version_ = fin.read().strip() - return version_ - - -extras_require = { - "flash-attn": ["flash-attn==2.8.3"], - "ring-flash-attn": [ - "flash-attn==2.8.3", - "ring-flash-attn>=0.1.7", - ], - "deepspeed": [ - "deepspeed==0.18.2", - "deepspeed-kernels", - ], - "mamba-ssm": [ - "mamba-ssm==1.2.0.post1", - "causal_conv1d", - ], - "auto-gptq": [ - "auto-gptq==0.5.1", - ], - "mlflow": [ - "mlflow", - ], - "galore": [ - "galore_torch", - ], - "apollo": [ - "apollo-torch", - ], - "optimizers": [ - "galore_torch", - "apollo-torch", - "lomo-optim==0.1.1", - "torch-optimi==0.2.1", - "came_pytorch==0.1.3", - ], - "ray": [ - "ray[train]>=2.52.1", - ], - "vllm": [ - "vllm==0.10.0", - ], - "llmcompressor": [ - "llmcompressor==0.5.1", - ], - "fbgemm-gpu": ["fbgemm-gpu-genai==1.3.0"], - "opentelemetry": [ - "opentelemetry-api", - "opentelemetry-sdk", - "opentelemetry-exporter-prometheus", - "prometheus-client", - ], -} -install_requires, dependency_links, extras_require_build = parse_requirements( - extras_require -) - -setup( - version=get_package_version(), - package_dir={"": "src"}, - packages=find_packages("src"), - install_requires=install_requires, - dependency_links=dependency_links, - entry_points={ - "console_scripts": [ - "axolotl=axolotl.cli.main:main", - ], - }, - extras_require=extras_require_build, -) diff --git a/src/axolotl/cli/utils/lora_merge.py b/src/axolotl/cli/utils/lora_merge.py index a07395587d..81cc2cbea1 100644 --- a/src/axolotl/cli/utils/lora_merge.py +++ b/src/axolotl/cli/utils/lora_merge.py @@ -339,7 +339,11 @@ class _FakeModule(nn.Module): ) layer.lora_A[adapter_name].weight.data = lora_a layer.lora_B[adapter_name].weight.data = lora_b - return layer.get_delta_weight(adapter_name) + delta = layer.get_delta_weight(adapter_name) + # peft >=0.19.1 may return delta with transposed dims for 3D params + if delta.shape != base_tensor.shape and delta.ndim == 3: + delta = delta.transpose(1, 2).contiguous() + return delta elif ( layer_type and "Conv" in layer_type or (layer_type is None and lora_a.ndim > 2) ): diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py index c6c01e255a..8fd10c8e95 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py @@ -60,49 +60,14 @@ def peft_lora_B_to_scattermoe(peft_B, num_experts, rank): def peft_lora_to_scattermoe(peft_A, peft_B, num_experts, rank): - """Convert peft LoRA weights to scattermoe layout (with A<->B swap). + """Convert peft LoRA weights to scattermoe layout. - peft operates on the parameter in its native storage layout ``[E, dim1, dim2]`` - where ``in_features=dim1, out_features=dim2``. ScatterMoE transposes the - parameter (``W = param.transpose(2, 1)``) giving ``[E, dim2, dim1]`` with - ``K=dim2, N=dim1``. Because of this transposition, peft's A and B roles - are swapped relative to scattermoe's convention. - - peft gives: - lora_A ``[r*E, dim1]``, lora_B ``[dim2, r*E]`` - - scattermoe needs: - lora_A ``[r*E, K=dim2]``, lora_B ``[N=dim1, r*E]`` - - This function swaps A<->B and converts B from rank-major to expert-major. - Uses vectorized tensor operations (no Python loop over experts). - - Works for **both** gate_up_proj and down_proj since the transposition - issue is the same for any parameter. + peft >=0.19.1 assigns in/out features for 3D params such that + A and B already align with scattermoe's convention (no A<->B swap). + Only B needs rank-major → expert-major layout conversion. """ - peft_B_em = peft_lora_B_to_scattermoe(peft_B, num_experts, rank) - - dim1 = peft_A.shape[1] # peft in_features -> scattermoe N - dim2 = peft_B_em.shape[0] # peft out_features -> scattermoe K - - # smoe_A: per expert, transpose B_e [dim2, r] -> [r, dim2] - # [dim2, E*r] -> [dim2, E, r] -> [E, r, dim2] -> [E*r, dim2] - smoe_A = ( - peft_B_em.reshape(dim2, num_experts, rank) - .permute(1, 2, 0) - .contiguous() - .reshape(rank * num_experts, dim2) - ) - - # smoe_B: per expert, transpose A_e [r, dim1] -> [dim1, r] - # [E*r, dim1] -> [E, r, dim1] -> [dim1, E, r] -> [dim1, E*r] - smoe_B = ( - peft_A.reshape(num_experts, rank, dim1) - .permute(2, 0, 1) - .contiguous() - .reshape(dim1, num_experts * rank) - ) - + smoe_A = peft_A + smoe_B = peft_lora_B_to_scattermoe(peft_B, num_experts, rank) return smoe_A, smoe_B diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 41fc35e6ec..f32b7c12e8 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -162,7 +162,6 @@ def apply_post_model_build_patches(self, model: PreTrainedModel): def apply_post_model_load_patches(self, model: PreTrainedModel): """Apply patches that require the model instance.""" self._apply_llama_flash_attn_patches(model) - self._apply_unsloth_patches(model) self._apply_lora_kernel_patch(model) self._apply_scaling_softmax_patch(model) @@ -674,24 +673,10 @@ def _patch_loss_llama(self): ) patch_fa_llama_cross_entropy() - elif self.cfg.unsloth_cross_entropy_loss: - from axolotl.monkeypatch.unsloth_ import integrate_cross_entropy_loss_patch - - integrate_cross_entropy_loss_patch(model_type="llama") - if self.cfg.flash_attn_rms_norm and self.has_flash_attn: from axolotl.monkeypatch.llama_attn_hijack_flash import patch_llama_rms_norm patch_llama_rms_norm() - elif self.cfg.unsloth_rms_norm: - from axolotl.monkeypatch.unsloth_ import patch_unsloth_layernorm - - patch_unsloth_layernorm() - - if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: - from axolotl.monkeypatch.unsloth_ import patch_self_attn_lora - - patch_self_attn_lora() def _patch_llama_flash_attention(self): """Apply Flash Attention patches for LLaMA models.""" @@ -758,23 +743,6 @@ def _apply_llama_flash_attn_patches(self, model): LOG.info("Patching with SwiGLU...") replace_llama_mlp_with_swiglu(model) - def _apply_unsloth_patches(self, model): - """Apply unsloth optimization patches.""" - if self.cfg.unsloth_lora_mlp: - from axolotl.monkeypatch.unsloth_ import integrate_lora_mlp_patch - - integrate_lora_mlp_patch(peft_model=model) - - if self.cfg.unsloth_lora_qkv or self.cfg.unsloth_lora_o: - from axolotl.monkeypatch.unsloth_ import integrate_lora_patch - - integrate_lora_patch(peft_model=model, cfg=self.cfg) - - if self.cfg.unsloth_rope: - from axolotl.monkeypatch.unsloth_ import integrate_rope_embeddings - - integrate_rope_embeddings() - def _apply_lora_kernel_patch(self, model): """Apply LoRA kernel patches.""" if ( diff --git a/src/axolotl/monkeypatch/unsloth_.py b/src/axolotl/monkeypatch/unsloth_.py deleted file mode 100644 index 59f32c6f5c..0000000000 --- a/src/axolotl/monkeypatch/unsloth_.py +++ /dev/null @@ -1,252 +0,0 @@ -"""module for patching with unsloth optimizations""" - -import inspect -import types - -import torch -from peft import PeftModelForCausalLM -from torch import nn -from transformers.models.llama.modeling_llama import LlamaFlashAttention2 - -from axolotl.monkeypatch.utils import detab_code -from axolotl.utils.logging import get_logger - -LOG = get_logger(__name__) - -ORIGINAL_QKV_CODE = """ - query_states = self.q_proj(hidden_states) - key_states = self.k_proj(hidden_states) - value_states = self.v_proj(hidden_states) -""".lstrip("\n") - -PATCHED_QKV_CODE = """ - query_states, key_states, value_states = self.apply_qkv(self, hidden_states) -""".lstrip("\n") - -ORIGINAL_O_CODE = """ - attn_output = self.o_proj(attn_output) -""".lstrip("\n") - -PATCHED_O_CODE = """ - attn_output = self.apply_o(self, attn_output) -""".lstrip("\n") - - -def original_apply_qkv(self, hidden_states): - query_states = self.q_proj(hidden_states) - key_states = self.k_proj(hidden_states) - value_states = self.v_proj(hidden_states) - return query_states, key_states, value_states - - -def original_apply_o(self, hidden_states): - attn_output = self.o_proj(hidden_states) - return attn_output - - -def get_self_attn_code() -> str: - forward = inspect.getsource(LlamaFlashAttention2.forward) - return forward - - -def check_self_attn_is_patchable() -> bool: - qkv = get_self_attn_code() - qkv, _ = detab_code(qkv) - return ORIGINAL_QKV_CODE in qkv and ORIGINAL_O_CODE in qkv - - -def integrate_cross_entropy_loss_patch(model_type: str = "llama") -> None: - from unsloth.kernels.cross_entropy_loss import fast_cross_entropy_loss - - def UnslothForCausalLMLoss( - logits, - labels, - vocab_size: int, - num_items_in_batch: int = None, - ignore_index: int = -100, - **kwargs, - ): - # Upcast to float if we need to compute the loss to avoid potential precision issues - logits = logits.float() - # Shift so that tokens < n predict n - shift_logits = logits[..., :-1, :].contiguous() - shift_labels = labels[..., 1:].contiguous() - - loss = fast_cross_entropy_loss( - logits=shift_logits, labels=shift_labels, n_items=num_items_in_batch - ) - return loss - - if model_type == "llama": - from transformers.loss import loss_utils - - loss_utils.ForCausalLMLoss = UnslothForCausalLMLoss # type: ignore[assignment] - else: - raise ValueError("Unsupported model type") - - -self_attn_lora_patched = False - - -def patch_self_attn_lora(): - global self_attn_lora_patched - if self_attn_lora_patched: - # prevent patching multiple times - return - self_attn_forward = get_self_attn_code() - LlamaFlashAttention2._original_forward = self_attn_forward - self_attn_forward, _ = detab_code(self_attn_forward) - assert ORIGINAL_QKV_CODE in self_attn_forward, "Original qkv code not found" - assert ORIGINAL_O_CODE in self_attn_forward, "Original o code not found" - - self_attn_forward = self_attn_forward.replace(ORIGINAL_QKV_CODE, PATCHED_QKV_CODE) - self_attn_forward = self_attn_forward.replace(ORIGINAL_O_CODE, PATCHED_O_CODE) - self_attn_forward = self_attn_forward.replace( - "def forward(", - "def unsloth_attn_forward(", - 1, - ) - - # load imports necessary - import transformers.models.llama.modeling_llama - - items_to_import = [] - for item in dir(transformers.models.llama.modeling_llama): - if item in self_attn_forward: - items_to_import.append(item) - - exec( - "from transformers.models.llama.modeling_llama import (" - + ", ".join(x for x in items_to_import) - + ")", - globals(), - ) - exec(self_attn_forward, globals()) - self_attn_lora_patched = True - LOG.info("patching unsloth attn lora") - LlamaFlashAttention2.forward = unsloth_attn_forward - - -def integrate_rope_embeddings(): - import transformers.models.llama.modeling_llama - from unsloth.kernels.rope_embedding import fast_rope_embedding - - def apply_rotary_pos_emb( - q, - k, - cos, - sin, - position_ids=None, - unsqueeze_dim=1, - ): - return fast_rope_embedding(q, k, cos, sin) - - LOG.info("patching unsloth RoPE embeddings") - transformers.models.llama.modeling_llama.apply_rotary_pos_emb = apply_rotary_pos_emb - - -def integrate_lora_mlp_patch(peft_model: PeftModelForCausalLM): - if peft_model.base_model.config.model_type in ["llama", "mistral"]: - from unsloth.kernels import apply_lora_mlp_swiglu - - apply_lora_mlp = apply_lora_mlp_swiglu - elif peft_model.base_model.config.model_type == "gemma": - from unsloth.kernels import apply_lora_mlp_geglu_approx - - apply_lora_mlp = apply_lora_mlp_geglu_approx - else: - raise NotImplementedError( - f"Model type {peft_model.base_model.config.model_type} not supported" - ) - - for idx, layer in enumerate(peft_model.model.model.layers): - layer_modules = [ - getattr(layer.mlp, linear_proj) - for linear_proj in ["gate_proj", "up_proj", "down_proj"] - ] - is_mlp_lora = all(hasattr(module, "lora_A") for module in layer_modules) - mlp_no_bias = all( - getattr(module, "base_layer", module).bias is None - for module in layer_modules - ) - mlp_not_dora = all( - len(getattr(module, "lora_magnitude_vector", []) or []) == 0 - for module in layer_modules - ) - - if is_mlp_lora and mlp_no_bias and mlp_not_dora: - layer.mlp.forward = types.MethodType(apply_lora_mlp, layer.mlp) - else: - LOG.warning(f"unable to apply unsloth lora mlp patch to layer {idx}") - - -def integrate_lora_patch(peft_model: PeftModelForCausalLM, cfg): - from unsloth.kernels import apply_lora_o, apply_lora_qkv - - for idx, layer in enumerate(peft_model.model.model.layers): - if cfg.unsloth_lora_qkv: - layer_modules = [ - getattr(layer.self_attn, linear_proj) - for linear_proj in ["q_proj", "k_proj", "v_proj"] - ] - is_qkv_lora = all(hasattr(module, "lora_A") for module in layer_modules) - qkv_no_bias = all( - getattr(module, "base_layer", module).bias is None - for module in layer_modules - ) - qkv_not_dora = all( - len(getattr(module, "lora_magnitude_vector", []) or []) == 0 - for module in layer_modules - ) - - if is_qkv_lora and qkv_no_bias and qkv_not_dora: - layer.self_attn.apply_qkv = apply_lora_qkv - else: - layer.self_attn.apply_qkv = original_apply_qkv - LOG.warning(f"unable to apply unsloth lora qkv patch to layer {idx}") - if cfg.unsloth_lora_o: - layer_modules = [ - getattr(layer.self_attn, linear_proj) for linear_proj in ["o_proj"] - ] - is_o_lora = all(hasattr(module, "lora_A") for module in layer_modules) - o_no_bias = all( - getattr(module, "base_layer", module).bias is None - for module in layer_modules - ) - o_not_dora = all( - len(getattr(module, "lora_magnitude_vector", []) or []) == 0 - for module in layer_modules - ) - - if is_o_lora and o_no_bias and o_not_dora: - layer.self_attn.apply_o = apply_lora_o - else: - layer.self_attn.apply_o = original_apply_o - LOG.warning(f"unable to apply unsloth lora o_proj patch to layer {idx}") - - -def patch_unsloth_layernorm(): - try: - import transformers.models.llama.modeling_llama - from unsloth.kernels.rms_layernorm import Fast_RMS_Layernorm - - class LlamaRMSNorm(nn.Module): - """LlamaRMSNorm""" - - def __init__(self, hidden_size, eps=1e-6): - """ - LlamaRMSNorm is equivalent to T5LayerNorm - """ - super().__init__() - self.weight = nn.Parameter(torch.ones(hidden_size)) - self.variance_epsilon = eps - - def forward(self, hidden_states): - return Fast_RMS_Layernorm.apply( - hidden_states, self.weight, self.variance_epsilon, False - ) - - LOG.info("patching with unsloth.kernels.rms_layernorm") - transformers.models.llama.modeling_llama.LlamaRMSNorm = LlamaRMSNorm - except ImportError: - LOG.warning("missing unsloth library") diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 4966570309..c762d7f801 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -823,13 +823,6 @@ class AxolotlInputConfig( }, ) - unsloth_cross_entropy_loss: bool | None = None - unsloth_lora_mlp: bool | None = None - unsloth_lora_qkv: bool | None = None - unsloth_lora_o: bool | None = None - unsloth_rms_norm: bool | None = None - unsloth_rope: bool | None = None - lora_mlp_kernel: bool | None = Field( default=None, json_schema_extra={ @@ -1469,21 +1462,6 @@ def check_compute_capability_w_sageattn(cls, data): ) return data - @model_validator(mode="before") - @classmethod - def check_multigpu_unsloth(cls, data): - if ( - data.get("unsloth_lora_mlp") - or data.get("unsloth_lora_qkv") - or data.get("unsloth_lora_o") - ): - capabilities = data.get("capabilities") - if capabilities and capabilities.get("n_gpu", 0) > 1: - raise ValueError( - "unsloth_lora_mlp, unsloth_lora_qkv, and unsloth_lora_o are not compatible with multi-GPU training." - ) - return data - @model_validator(mode="before") @classmethod def check_multigpu_lora_kernels(cls, data): @@ -1537,8 +1515,7 @@ def check_auto_enable_lora_kernels(cls, data): # RL trainers not tested so don't enable kernels by default return data if data.get("adapter") in ["lora", "qlora"]: - # Skip if already set, using unsloth optimizations, or using 8-bit - unsloth_fields = ["unsloth_lora_mlp", "unsloth_lora_qkv", "unsloth_lora_o"] + # Skip if already set or using 8-bit kernel_fields = [ "lora_mlp_kernel", "lora_qkv_kernel", @@ -1547,7 +1524,6 @@ def check_auto_enable_lora_kernels(cls, data): ] if ( any(data.get(k) is not None for k in kernel_fields) - or any(data.get(k) for k in unsloth_fields) or data.get("adapter") == "lora" and data.get("load_in_8bit") ): diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index ff7813600d..1780a9cc83 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -52,6 +52,26 @@ def deprecate_sharegpt_datasets(cls, datasets): return datasets + @model_validator(mode="before") + @classmethod + def check_deprecated_unsloth_fields(cls, data): + deprecated_fields = [ + "unsloth_cross_entropy_loss", + "unsloth_lora_mlp", + "unsloth_lora_qkv", + "unsloth_lora_o", + "unsloth_rms_norm", + "unsloth_rope", + ] + found = [f for f in deprecated_fields if data.get(f)] + if found: + raise ValueError( + f"`{'`, `'.join(found)}` {'has' if len(found) == 1 else 'have'} been removed. " + "Please use `lora_mlp_kernel`, `lora_qkv_kernel`, `lora_o_kernel` instead. " + "See: https://docs.axolotl.ai/docs/lora_optims.html" + ) + return data + @model_validator(mode="before") @classmethod def check_dataset_or_pretraining_dataset(cls, data): @@ -607,36 +627,6 @@ def check_peft_layers_pattern(cls, data): ) return data - @model_validator(mode="before") - @classmethod - def check_qlora_unsloth(cls, data): - if ( - data.get("unsloth_lora_mlp") - or data.get("unsloth_lora_qkv") - or data.get("unsloth_lora_o") - ): - if data.get("adapter") == "lora" and data.get("load_in_8bit"): - raise ValueError( - "unsloth_lora_mlp, unsloth_lora_qkv, and unsloth_lora_o are not compatible with 8-bit LoRA" - ) - return data - - @model_validator(mode="before") - @classmethod - def check_lora_axolotl_unsloth(cls, data): - is_lora_kernel = any( - data.get(k) for k in ["lora_mlp_kernel", "lora_qkv_kernel", "lora_o_kernel"] - ) - is_unsloth_lora = any( - data.get(k) - for k in ["unsloth_lora_mlp", "unsloth_lora_qkv", "unsloth_lora_o"] - ) - if is_lora_kernel and is_unsloth_lora: - raise ValueError( - "both lora_mlp_kernel and unsloth_lora_mlp cannot be true (similarly for lora_qkv_kernel, lora_o_kernel)" - ) - return data - @model_validator(mode="after") def check_fused_lora(self): if self.adapter in ["lora", "qlora"] and self.flash_attn_fuse_mlp: @@ -860,17 +850,6 @@ def check_batch_flattening_fa(cls, data): return data - @model_validator(mode="before") - @classmethod - def check_xentropy_patch_conflicts(cls, data): - if data.get("flash_attn_cross_entropy") and data.get( - "unsloth_cross_entropy_loss" - ): - raise ValueError( - "flash_attn_cross_entropy and unsloth_cross_entropy_loss cannot be both enabled" - ) - return data - @model_validator(mode="before") @classmethod def check_cross_entropy_conflicts(cls, data): diff --git a/src/setuptools_axolotl_dynamic_dependencies.py b/src/setuptools_axolotl_dynamic_dependencies.py deleted file mode 100644 index 3bb54cda8e..0000000000 --- a/src/setuptools_axolotl_dynamic_dependencies.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -dynamic requirements for axolotl -""" - -import platform -import re -from importlib.metadata import PackageNotFoundError, version - -from setuptools.command.build_py import build_py as _build_py - - -def parse_requirements(): - _install_requires = [] - _dependency_links = [] - with open("./requirements.txt", encoding="utf-8") as requirements_file: - lines = [r.strip() for r in requirements_file.readlines()] - for line in lines: - is_extras = ( - "flash-attn" in line - or "flash-attention" in line - or "deepspeed" in line - or "mamba-ssm" in line - or "lion-pytorch" in line - ) - if line.startswith("--extra-index-url"): - # Handle custom index URLs - _, url = line.split() - _dependency_links.append(url) - elif not is_extras and line and line[0] != "#": - # Handle standard packages - _install_requires.append(line) - - try: - xformers_version = [req for req in _install_requires if "xformers" in req][0] - torchao_version = [req for req in _install_requires if "torchao" in req][0] - - if "Darwin" in platform.system(): - # don't install xformers on MacOS - _install_requires.pop(_install_requires.index(xformers_version)) - else: - # detect the version of torch already installed - # and set it so dependencies don't clobber the torch version - try: - torch_version = version("torch") - except PackageNotFoundError: - torch_version = "2.5.1" - _install_requires.append(f"torch=={torch_version}") - - version_match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", torch_version) - if version_match: - major, minor, patch = version_match.groups() - major, minor = int(major), int(minor) - patch = ( - int(patch) if patch is not None else 0 - ) # Default patch to 0 if not present - else: - raise ValueError("Invalid version format") - - if (major, minor) >= (2, 5): - _install_requires.pop(_install_requires.index(xformers_version)) - if patch == 0: - _install_requires.append("xformers==0.0.28.post2") - else: - _install_requires.append("xformers==0.0.28.post3") - elif (major, minor) >= (2, 4): - if patch == 0: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.27") - else: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers==0.0.28.post1") - elif (major, minor) >= (2, 3): - _install_requires.pop(_install_requires.index(torchao_version)) - if patch == 0: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.26.post1") - else: - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.27") - elif (major, minor) >= (2, 2): - _install_requires.pop(_install_requires.index(torchao_version)) - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.25.post1") - else: - _install_requires.pop(_install_requires.index(torchao_version)) - _install_requires.pop(_install_requires.index(xformers_version)) - _install_requires.append("xformers>=0.0.23.post1") - - except PackageNotFoundError: - pass - return _install_requires, _dependency_links - - -class BuildPyCommand(_build_py): - """ - custom build_py command to parse dynamic requirements - """ - - def finalize_options(self): - super().finalize_options() - install_requires, _ = parse_requirements() - self.distribution.install_requires = install_requires diff --git a/tests/conftest.py b/tests/conftest.py index f857dd3636..19e3dc3f05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -325,10 +325,10 @@ def download_phi_4_reasoning_model_fixture(): @pytest.fixture(scope="session", autouse=True) -def download_phi_3_medium_model_fixture(): +def download_phi_3_mini_model_fixture(): # download the tokenizer only snapshot_download_w_retry( - "microsoft/Phi-3-medium-128k-instruct", + "microsoft/Phi-3-mini-4k-instruct", repo_type="model", allow_patterns=["*token*", "config.json"], ) diff --git a/tests/e2e/integrations/test_scattermoe_lora_olmoe.py b/tests/e2e/integrations/test_scattermoe_lora_olmoe.py index 1cd514b54d..945a9173a1 100644 --- a/tests/e2e/integrations/test_scattermoe_lora_olmoe.py +++ b/tests/e2e/integrations/test_scattermoe_lora_olmoe.py @@ -54,24 +54,8 @@ def peft_lora_B_to_scattermoe(peft_B, num_experts, rank): ) def peft_lora_to_scattermoe(peft_A, peft_B, num_experts, rank): - peft_B_em = peft_lora_B_to_scattermoe(peft_B, num_experts, rank) - K_inter, N_hidden = peft_B.shape[0], peft_A.shape[1] - smoe_A = torch.zeros( - rank * num_experts, - K_inter, - device=peft_A.device, - dtype=peft_A.dtype, - ) - smoe_B = torch.zeros( - N_hidden, - rank * num_experts, - device=peft_A.device, - dtype=peft_A.dtype, - ) - for e in range(num_experts): - s = e * rank - smoe_A[s : s + rank, :] = peft_B_em[:, s : s + rank].T - smoe_B[:, s : s + rank] = peft_A[s : s + rank, :].T + smoe_A = peft_A + smoe_B = peft_lora_B_to_scattermoe(peft_B, num_experts, rank) return smoe_A, smoe_B def _unwrap_experts_lora(experts_module): @@ -322,12 +306,14 @@ def test_down_proj_conversion(self): hidden, inter = 32, 16 scaling = 2.0 - peft_A = torch.randn(E * r, hidden) - peft_B = torch.randn(inter, E * r) + # peft >=0.19.1 for down_proj [E, hidden, inter]: + # swaps in/out, lora_A [r*E, inter], lora_B [hidden, r*E] + peft_A = torch.randn(E * r, inter) + peft_B = torch.randn(hidden, E * r) - A_r = peft_A.reshape(E, r, hidden) - B_r = peft_B.reshape(inter, r, E) - delta_peft = torch.einsum("o r e, e r i -> e i o", B_r, A_r) * scaling + A_r = peft_A.reshape(E, r, inter) + B_r = peft_B.reshape(hidden, r, E) + delta_peft = torch.einsum("o r e, e r i -> e o i", B_r, A_r) * scaling smoe_A, smoe_B = peft_lora_to_scattermoe(peft_A, peft_B, E, r) for e in range(E): @@ -339,31 +325,22 @@ def test_down_proj_conversion(self): ) def test_gate_up_proj_conversion(self): - """Verify gate_up_proj LoRA conversion with non-square dims (Qwen3-like). + """Verify gate_up_proj LoRA conversion with non-square dims. gate_up_proj param: [E, 2*inter, hidden]. - peft: in_features=2*inter, out_features=hidden. - peft lora_A: [r*E, 2*inter], lora_B: [hidden, r*E]. - - scattermoe W = param.T = [E, hidden, 2*inter], K=hidden, N=2*inter. + peft swaps in/out for 3D: lora_A [r*E, hidden], lora_B [2*inter, r*E]. scattermoe needs: lora_A [r*E, K=hidden], lora_B [N=2*inter, r*E]. - - Uses non-square dims (hidden=32 != 2*inter=24) to catch A<->B swap bugs. """ E, r = 4, 2 hidden, inter = 32, 12 # 2*inter=24 != hidden=32 scaling = 2.0 - # peft assigns: in_features=2*inter, out_features=hidden - peft_A = torch.randn(E * r, 2 * inter) # [r*E, in_features=2*inter] - peft_B = torch.randn(hidden, E * r) # [out_features=hidden, r*E] + peft_A = torch.randn(E * r, hidden) # [r*E, in=hidden] + peft_B = torch.randn(2 * inter, E * r) # [out=2*inter, r*E] - # peft delta via einsum: "o r e, e r i -> e i o" - A_r = peft_A.reshape(E, r, 2 * inter) - B_r = peft_B.reshape(hidden, r, E) - delta_peft = torch.einsum("o r e, e r i -> e i o", B_r, A_r) * scaling - # delta_peft[e] has shape [in_features, out_features] = [2*inter, hidden] - # = param[e] shape [2*inter, hidden] + A_r = peft_A.reshape(E, r, hidden) + B_r = peft_B.reshape(2 * inter, r, E) + delta_peft = torch.einsum("o r e, e r i -> e o i", B_r, A_r) * scaling smoe_A, smoe_B = peft_gate_up_lora_to_scattermoe(peft_A, peft_B, E, r) # smoe_A should be [r*E, K=hidden], smoe_B should be [N=2*inter, r*E] @@ -421,23 +398,21 @@ def test_peft_creates_correct_shapes(self): r, ) - # gate_up_proj [E, 2*inter, hidden] - # peft: in_features=2*inter (dim 1), out_features=hidden (dim 2) + # gate_up_proj [E, 2*inter, hidden] — peft swaps in/out for 3D assert trainable[ "base_model.model.moe.experts.base_layer.lora_A.default.weight" - ].shape == (E * r, 2 * config.intermediate_size) + ].shape == (E * r, config.hidden_size) assert trainable[ "base_model.model.moe.experts.base_layer.lora_B.default.weight" - ].shape == (config.hidden_size, E * r) + ].shape == (2 * config.intermediate_size, E * r) - # down_proj [E, hidden, inter] - # peft: in_features=hidden (dim 1), out_features=inter (dim 2) + # down_proj [E, hidden, inter] — peft swaps in/out for 3D assert trainable[ "base_model.model.moe.experts.lora_A.default.weight" - ].shape == (E * r, config.hidden_size) + ].shape == (E * r, config.intermediate_size) assert trainable[ "base_model.model.moe.experts.lora_B.default.weight" - ].shape == (config.intermediate_size, E * r) + ].shape == (config.hidden_size, E * r) @requires_cuda def test_peft_forward_runs(self): @@ -488,8 +463,7 @@ def test_unwrap_experts_lora(self): assert gup_lora is not None, "gate_up_proj LoRA not detected" assert down_lora is not None, "down_proj LoRA not detected" - # Check shapes (after peft->scattermoe conversion with A<->B swap) - # gate_up_proj W = param.T = [E, hidden, 2*inter], K=hidden, N=2*inter + # gate_up_proj: K=hidden, N=2*inter E, r = config.num_experts, 4 gup_A, gup_B, gup_s = gup_lora assert gup_A.shape == (E * r, config.hidden_size), ( @@ -501,7 +475,7 @@ def test_unwrap_experts_lora(self): f"{(2 * config.intermediate_size, E * r)}, got {gup_B.shape}" ) - # down_proj W = param.T = [E, inter, hidden], K=inter, N=hidden + # down_proj: K=inter, N=hidden down_A, down_B, down_s = down_lora assert down_A.shape == (E * r, config.intermediate_size), ( f"down_proj smoe_A: expected [r*E, K=inter]={(E * r, config.intermediate_size)}, " diff --git a/tests/e2e/patched/test_unsloth_integration.py b/tests/e2e/patched/test_unsloth_integration.py deleted file mode 100644 index 4cd97c894d..0000000000 --- a/tests/e2e/patched/test_unsloth_integration.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Test module for checking whether the integration of Unsloth with Hugging Face Transformers is working as expected.""" - -import unittest - -import pytest - - -@pytest.mark.skip( - reason="Unsloth integration will be broken going into latest transformers" -) -class TestUnslothIntegration(unittest.TestCase): - """Unsloth monkeypatch integration tests.""" - - def test_is_self_attn_patchable(self): - from axolotl.monkeypatch.unsloth_ import check_self_attn_is_patchable - - # ensures the current version of transformers has loss code that matches our patching code - self.assertTrue( - check_self_attn_is_patchable(), - "HF transformers self attention code has changed and isn't patchable", - ) diff --git a/tests/e2e/patched/test_unsloth_qlora.py b/tests/e2e/patched/test_unsloth_qlora.py deleted file mode 100644 index bf00e8a5fc..0000000000 --- a/tests/e2e/patched/test_unsloth_qlora.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -e2e tests for unsloth qlora -""" - -import pytest - -from axolotl.common.datasets import load_datasets -from axolotl.train import train -from axolotl.utils.config import normalize_config, validate_config -from axolotl.utils.dict import DictDefault - -from ..utils import check_model_output_exists, check_tensorboard - - -@pytest.mark.skip( - reason="Unsloth integration will be broken going into latest transformers" -) -class TestUnslothQLoRA: - """ - Test class for Unsloth QLoRA Llama models - """ - - @pytest.mark.parametrize( - "sample_packing", - [True, False], - ) - def test_unsloth_llama_qlora_fa2(self, temp_dir, sample_packing): - cfg = DictDefault( - { - "base_model": "HuggingFaceTB/SmolLM2-135M", - "sequence_len": 1024, - "sample_packing": sample_packing, - "flash_attention": True, - "unsloth_lora_mlp": True, - "unsloth_lora_qkv": True, - "unsloth_lora_o": True, - "load_in_4bit": True, - "adapter": "qlora", - "lora_r": 16, - "lora_alpha": 16, - "lora_dropout": 0.05, - "lora_target_linear": True, - "val_set_size": 0.05, - "special_tokens": { - "pad_token": "<|endoftext|>", - }, - "datasets": [ - { - "path": "mhenrichsen/alpaca_2k_test", - "type": "alpaca", - }, - ], - "num_epochs": 1, - "max_steps": 5, - "save_steps": 10, - "micro_batch_size": 4, - "gradient_accumulation_steps": 2, - "output_dir": temp_dir, - "learning_rate": 0.00001, - "optimizer": "adamw_8bit", - "lr_scheduler": "cosine", - "use_tensorboard": True, - "bf16": "auto", - "save_first_step": False, - } - ) - - cfg = validate_config(cfg) - normalize_config(cfg) - dataset_meta = load_datasets(cfg=cfg) - - train(cfg=cfg, dataset_meta=dataset_meta) - check_model_output_exists(temp_dir, cfg) - - check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss (%s) is too high" - ) - - def test_unsloth_llama_qlora_unpacked(self, temp_dir): - cfg = DictDefault( - { - "base_model": "HuggingFaceTB/SmolLM2-135M", - "sequence_len": 1024, - "unsloth_lora_mlp": True, - "unsloth_lora_qkv": True, - "unsloth_lora_o": True, - "sample_packing": False, - "load_in_4bit": True, - "adapter": "qlora", - "lora_r": 16, - "lora_alpha": 16, - "lora_dropout": 0.05, - "lora_target_linear": True, - "val_set_size": 0.05, - "special_tokens": { - "pad_token": "<|endoftext|>", - }, - "datasets": [ - { - "path": "mhenrichsen/alpaca_2k_test", - "type": "alpaca", - }, - ], - "num_epochs": 1, - "max_steps": 5, - "save_steps": 10, - "micro_batch_size": 4, - "gradient_accumulation_steps": 2, - "output_dir": temp_dir, - "learning_rate": 0.00001, - "optimizer": "adamw_8bit", - "lr_scheduler": "cosine", - "use_tensorboard": True, - "bf16": "auto", - "save_first_step": False, - } - ) - - cfg = validate_config(cfg) - normalize_config(cfg) - dataset_meta = load_datasets(cfg=cfg) - - train(cfg=cfg, dataset_meta=dataset_meta) - check_model_output_exists(temp_dir, cfg) - - check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss (%s) is too high" - ) - - @pytest.mark.parametrize( - "sdp_attention", - [True, False], - ) - def test_unsloth_llama_qlora_unpacked_no_fa2_fp16(self, temp_dir, sdp_attention): - cfg = DictDefault( - { - "base_model": "HuggingFaceTB/SmolLM2-135M", - "sequence_len": 1024, - "unsloth_lora_mlp": True, - "unsloth_lora_qkv": True, - "unsloth_lora_o": True, - "sample_packing": False, - "load_in_4bit": True, - "adapter": "qlora", - "lora_r": 16, - "lora_alpha": 16, - "lora_dropout": 0.05, - "lora_target_linear": True, - "val_set_size": 0.05, - "special_tokens": { - "pad_token": "<|endoftext|>", - }, - "datasets": [ - { - "path": "mhenrichsen/alpaca_2k_test", - "type": "alpaca", - }, - ], - "num_epochs": 1, - "max_steps": 5, - "save_steps": 10, - "micro_batch_size": 4, - "gradient_accumulation_steps": 2, - "sdp_attention": sdp_attention, - "output_dir": temp_dir, - "learning_rate": 0.00001, - "optimizer": "adamw_8bit", - "lr_scheduler": "cosine", - "use_tensorboard": True, - "fp16": True, - "save_first_step": False, - } - ) - - cfg = validate_config(cfg) - normalize_config(cfg) - dataset_meta = load_datasets(cfg=cfg) - - train(cfg=cfg, dataset_meta=dataset_meta) - check_model_output_exists(temp_dir, cfg) - - check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.0, "Train Loss (%s) is too high" - ) diff --git a/tests/prompt_strategies/test_dpo_chat_templates.py b/tests/prompt_strategies/test_dpo_chat_templates.py index 74c98204c1..28028ce42e 100644 --- a/tests/prompt_strategies/test_dpo_chat_templates.py +++ b/tests/prompt_strategies/test_dpo_chat_templates.py @@ -111,7 +111,7 @@ def fixture_argilla_chat_dataset(): @pytest.fixture(name="phi3_tokenizer") @enable_hf_offline def fixture_phi3_tokenizer(): - tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-medium-128k-instruct") + tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct") return tokenizer @@ -214,8 +214,8 @@ def test_phi3_defaults(self, phi3_tokenizer, assistant_dataset): + "<|user|>\ngoodbye<|end|>\n" + "<|assistant|>\n" ) - assert result["chosen"] == "goodbye<|end|>" - assert result["rejected"] == "party on<|end|>" + assert result["chosen"] == "goodbye<|end|>\n<|endoftext|>" + assert result["rejected"] == "party on<|end|>\n<|endoftext|>" class TestAssistantDPOChatTemplateGemma: @@ -290,8 +290,8 @@ def test_phi3_argilla_chat(self, phi3_tokenizer, argilla_chat_dataset): ) result = transform_fn(argilla_chat_dataset[0], tokenizer=phi3_tokenizer) assert result["prompt"] == "<|user|>\nhello<|end|>\n" + "<|assistant|>\n" - assert result["chosen"] == "goodbye<|end|>" - assert result["rejected"] == "party on<|end|>" + assert result["chosen"] == "goodbye<|end|>\n<|endoftext|>" + assert result["rejected"] == "party on<|end|>\n<|endoftext|>" class TestDPOChatTemplateToolRole: From e562e149cecf5826e83ecf3a086949583bf25b28 Mon Sep 17 00:00:00 2001 From: thad0ctor Date: Tue, 21 Apr 2026 14:49:58 -0700 Subject: [PATCH 1276/1405] =?UTF-8?q?fix:=20[gemma4]=20fix=20VRAM=20leak?= =?UTF-8?q?=20in=20hybrid=20FA2+SDPA=20(hybrid=20attentiuon)=20path=20unde?= =?UTF-8?q?r=20activation=20check=E2=80=A6=20(#3611)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [gemma4] fix VRAM leak in hybrid FA2+SDPA path under activation checkpointing Route shared_kv_states through a thread-local side channel instead of the decoder-layer kwargs so the checkpoint partial never references the dict. HF's Gemma4TextModel.forward passes shared_kv_states (a mutable dict used for cross-layer K/V sharing) as a kwarg to every decoder_layer call. GradientCheckpointingLayer.__call__ then forms partial(super().__call__, **kwargs), and whichever checkpoint runs (axolotl's CPU_Offloaded_Gradient_Checkpointer or torch's stock checkpoint) captures that partial. The partial holds a reference to the dict, which holds the K/V tensors produced by store_full_length_kv layers. Those tensors stay pinned for the full duration of backward, and delayed ref-cycle cleanup in torch's caching allocator under FSDP2 + activation checkpointing bleeds the residual across steps. Observed symptom: VRAM climbs ~0.47 GiB/step from a 42 GiB baseline, OOMs around step 73 (~94 GiB peak) on Gemma-4 31B multimodal with gemma4_hybrid_attn_impl: true. Independent of seq len / image size. All-flex-attention path is flat but ~22x slower. Violated invariant: anything crossing an activation-checkpoint boundary must be a tensor (refcounted by autograd) or plain Python data -- never a mutable container holding tensor references. Fix (all in src/axolotl/monkeypatch/models/gemma4/fused_attn.py): * threading.local() store with _get/_set_shared_kv_states helpers * _patch_decoder_layer_call(): monkeypatches Gemma4TextDecoderLayer.__call__ to pop shared_kv_states from kwargs and stash it in TLS before delegating to GradientCheckpointingLayer. The partial formed downstream no longer references the dict. * fused_forward reads TLS first, falls back to kwarg for callers that bypass the patched __call__ (e.g. direct attention invocation). * wired into patch_gemma4_fused_attn; idempotent via a sentinel. TLS is overwritten on each new step's first decoder-layer call, so the previous step's dict is released promptly. No changes to hybrid dispatch, FSDP wrap policy, or any config behaviour. Works for hybrid, flex, and eager paths. Introduced by PR #3598 (commit b8358aa5). * Coderabbit comment: gemma4: clear TLS unconditionally in decoder-layer patched __call__ Overwrite the thread-local shared_kv_states store on every invocation (including with None) instead of only when the kwarg is present. The previous conditional write left stale dicts in TLS on any path that reaches Gemma4TextDecoderLayer.__call__ without a shared_kv_states kwarg — e.g. generation, eval hooks, or future HF refactors that make the kwarg optional. fused_forward would then silently consume a prior step's K/V dict instead of falling back to its own kwarg path. Unconditional write makes the invariant in the surrounding comment ("TLS is overwritten on each new step's first decoder-layer call, so the previous step's dict is released promptly") actually hold. No behavior change for the training happy path, which always passes the kwarg. Addresses CodeRabbit review on PR #3611 * fix: swap threading.local() for module-level store so autograd worker threads see shared_kv_states during backward recompute Previous commits fixed memory leak on 31B but caused type error with MOE Gemma4 variants - this fixes that: PR 3611's TLS variant only works when recompute runs on the same thread that set TLS during forward. PyTorch's C++ autograd engine (_engine_run_backward) spawns per-device worker threads to dispatch backward, and HF-Trainer gradient_checkpointing (stock torch.utils.checkpoint, non-reentrant / saved-tensor-hooks) fires unpack_hook -> recompute_fn on those worker threads. TLS set on the main thread during forward is invisible there, so _get_shared_kv_states() returns None and the consumer-layer lookup crashes with "'NoneType' object is not subscriptable" at fused_attn.py:97 (shared_kv_states[self.kv_shared_layer_index]). A plain module-level dict is visible to all threads in the process. Lifecycle is identical: the slot is overwritten each forward, releasing the previous step's dict and allowing its K/V tensors to be GC'd, so the original VRAM-leak fix still holds under FSDP2 AC too. * scope gemma4 shared_kv_states side channel to checkpointed training Update PR #3611 with gate for checkpointed training to avoid regressions across async flows. Added unit tests for kwargs pop, store-clear regression, and flag gating. Condensed verbose comments * add gemma4 cross-thread visibility test for shared_kv_states store Additional regression test for MoE gemma4 variants - asserts the module-level store is readable from threads other than the one that set it in response to previously observed 'NoneType' error * fix logger --------- Co-authored-by: Wing Lian --- src/axolotl/loaders/patch_manager.py | 11 +- .../monkeypatch/models/gemma4/fused_attn.py | 59 +++++- .../test_gemma4_fused_attn_patch.py | 171 ++++++++++++++++++ 3 files changed, 234 insertions(+), 7 deletions(-) create mode 100644 tests/monkeypatch/test_gemma4_fused_attn_patch.py diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index f32b7c12e8..8739655162 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -395,7 +395,16 @@ def _apply_model_specific_patches(self): patch_gemma4_fused_attn, ) - patch_gemma4_fused_attn() + # Shared-KV side channel when activation checkpointing (PR #3611). + fsdp_cfg = self.cfg.fsdp_config + needs_shared_kv_workaround = (not self.inference) and bool( + self.cfg.gradient_checkpointing + or self.cfg.activation_offloading + or (fsdp_cfg is not None and fsdp_cfg.activation_checkpointing) + ) + patch_gemma4_fused_attn( + install_shared_kv_workaround=needs_shared_kv_workaround + ) @staticmethod def _fix_nemotron_h_conversion_mapping(): diff --git a/src/axolotl/monkeypatch/models/gemma4/fused_attn.py b/src/axolotl/monkeypatch/models/gemma4/fused_attn.py index 7cb5c6beb3..2144b6c417 100644 --- a/src/axolotl/monkeypatch/models/gemma4/fused_attn.py +++ b/src/axolotl/monkeypatch/models/gemma4/fused_attn.py @@ -6,15 +6,29 @@ Usage: from axolotl.monkeypatch.models.gemma4.fused_attn import patch_gemma4_fused_attn - patch_gemma4_fused_attn() + # Pass install_shared_kv_workaround=True when activation checkpointing is enabled. + patch_gemma4_fused_attn(install_shared_kv_workaround=True) """ -import logging from typing import Callable import torch -logger = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +logger = get_logger(__name__) + +# Module-level dict used as a side channel for shared KV states avoiding kwarg and TLS +# to prevent memory leak on gradient checkpoint enabled training (PR #3611) +_GEMMA4_SHARED_KV_STORE: dict = {"store": None} + + +def _set_shared_kv_states(store): + _GEMMA4_SHARED_KV_STORE["store"] = store + + +def _get_shared_kv_states(): + return _GEMMA4_SHARED_KV_STORE["store"] def _make_fused_forward(original_forward): @@ -30,7 +44,7 @@ def fused_forward( hidden_states: torch.Tensor, position_embeddings: torch.Tensor, attention_mask: torch.Tensor | None, - shared_kv_states: dict[int, tuple[torch.Tensor, torch.Tensor]], + shared_kv_states: dict[int, tuple[torch.Tensor, torch.Tensor]] | None = None, past_key_values=None, **kwargs, ) -> tuple[torch.Tensor, torch.Tensor | None]: @@ -39,6 +53,10 @@ def fused_forward( eager_attention_forward, ) + store = _get_shared_kv_states() + if store is not None: + shared_kv_states = store + input_shape = hidden_states.shape[:-1] hidden_shape = (*input_shape, -1, self.head_dim) eps = self.config.rms_norm_eps @@ -133,15 +151,44 @@ def fused_forward( return fused_forward -def patch_gemma4_fused_attn(): +def _patch_decoder_layer_call(): + """Strip `shared_kv_states` from decoder-layer kwargs and route via the + module-level side channel so the checkpoint partial cannot pin it (PR #3611). """ - Monkeypatch Gemma4TextAttention.forward to use fused RMSNorm+RoPE kernels. + from transformers.models.gemma4.modeling_gemma4 import Gemma4TextDecoderLayer + + if getattr(Gemma4TextDecoderLayer, "_axolotl_shared_kv_patched", False): + return + + original_call = Gemma4TextDecoderLayer.__call__ + + def patched_call(self, *args, **kwargs): + shared_kv = kwargs.pop("shared_kv_states", None) + # Overwrite unconditionally (including with None) so a previous step's + # dict cannot leak into a later call without shared_kv_states (PR #3611). + _set_shared_kv_states(shared_kv) + return original_call(self, *args, **kwargs) + + Gemma4TextDecoderLayer.__call__ = patched_call + Gemma4TextDecoderLayer._axolotl_shared_kv_patched = True + + +def patch_gemma4_fused_attn(install_shared_kv_workaround: bool = False): + """ + Monkeypatch Gemma4TextAttention.forward to use fused RMSNorm+RoPE kernels, + and optionally route `shared_kv_states` via a module-level side channel to + avoid a VRAM leak under activation checkpointing (PR #3611). """ from transformers.models.gemma4.modeling_gemma4 import Gemma4TextAttention original_forward = Gemma4TextAttention.forward Gemma4TextAttention.forward = _make_fused_forward(original_forward) + if install_shared_kv_workaround: + _patch_decoder_layer_call() + logger.info( "Patched Gemma4TextAttention.forward with fused RMSNorm+RoPE Triton kernels" ) + if install_shared_kv_workaround: + logger.info("Installed Gemma4 shared_kv_states side channel (PR #3611)") diff --git a/tests/monkeypatch/test_gemma4_fused_attn_patch.py b/tests/monkeypatch/test_gemma4_fused_attn_patch.py new file mode 100644 index 0000000000..75dbe472bc --- /dev/null +++ b/tests/monkeypatch/test_gemma4_fused_attn_patch.py @@ -0,0 +1,171 @@ +"""Unit tests for the Gemma4 fused-attention shared_kv_states routing patch.""" + +import pytest + +gemma4_modeling = pytest.importorskip("transformers.models.gemma4.modeling_gemma4") + + +@pytest.fixture +def clean_decoder_layer_patch_slate(): + """Save and restore Gemma4TextDecoderLayer.__call__ and the sentinel.""" + from axolotl.monkeypatch.models.gemma4 import fused_attn + + cls = gemma4_modeling.Gemma4TextDecoderLayer + original_call = cls.__call__ + had_sentinel = getattr(cls, "_axolotl_shared_kv_patched", False) + + if had_sentinel: + del cls._axolotl_shared_kv_patched + + try: + yield cls, fused_attn + finally: + cls.__call__ = original_call + if had_sentinel: + cls._axolotl_shared_kv_patched = True + elif hasattr(cls, "_axolotl_shared_kv_patched"): + del cls._axolotl_shared_kv_patched + fused_attn._set_shared_kv_states(None) + + +class TestPatchedDecoderLayerCall: + def test_pops_shared_kv_states_and_populates_store( + self, clean_decoder_layer_patch_slate + ): + cls, fused_attn = clean_decoder_layer_patch_slate + + captured = {} + + def spy(self, *args, **kwargs): + captured["args"] = args + captured["kwargs"] = dict(kwargs) + return "spy_return" + + cls.__call__ = spy + fused_attn._patch_decoder_layer_call() + + assert getattr(cls, "_axolotl_shared_kv_patched", False) is True + assert cls.__call__ is not spy + + shared_kv = {"layer_0": ("k", "v")} + result = cls.__call__( + object(), + "positional_arg", + shared_kv_states=shared_kv, + other_kwarg="keep_me", + ) + + assert result == "spy_return" + assert captured["args"] == ("positional_arg",) + assert "shared_kv_states" not in captured["kwargs"] + assert captured["kwargs"] == {"other_kwarg": "keep_me"} + assert fused_attn._get_shared_kv_states() is shared_kv + + def test_clears_store_when_kwarg_absent(self, clean_decoder_layer_patch_slate): + """Regression for commit 251021e1: a prior step's dict must not leak + into a later call that omits `shared_kv_states`.""" + cls, fused_attn = clean_decoder_layer_patch_slate + + def spy(self, *args, **kwargs): + return None + + cls.__call__ = spy + fused_attn._patch_decoder_layer_call() + + stale = {"stale_step": True} + fused_attn._set_shared_kv_states(stale) + assert fused_attn._get_shared_kv_states() is stale + + cls.__call__(object()) + + assert fused_attn._get_shared_kv_states() is None + + def test_store_visible_across_threads(self): + """Regression for commit e3669b2c: the store must be readable from + threads other than the one that set it. `threading.local()` failed + this invariant, crashing with 'NoneType' object is not subscriptable' + on MoE Gemma4 variants when autograd worker threads ran backward + recompute under HF-Trainer gradient_checkpointing.""" + import threading + + from axolotl.monkeypatch.models.gemma4 import fused_attn + + sentinel = {"layer_0": ("k", "v")} + try: + fused_attn._set_shared_kv_states(sentinel) + + seen = {} + + def worker(): + seen["value"] = fused_attn._get_shared_kv_states() + + t = threading.Thread(target=worker) + t.start() + t.join() + + assert seen["value"] is sentinel + finally: + fused_attn._set_shared_kv_states(None) + + +@pytest.fixture +def clean_entry_point_patch_slate(): + """Save and restore Gemma4TextAttention.forward and Gemma4TextDecoderLayer.__call__.""" + from axolotl.monkeypatch.models.gemma4 import fused_attn + + decoder_cls = gemma4_modeling.Gemma4TextDecoderLayer + attn_cls = gemma4_modeling.Gemma4TextAttention + + original_call = decoder_cls.__call__ + original_forward = attn_cls.forward + had_sentinel = getattr(decoder_cls, "_axolotl_shared_kv_patched", False) + + if had_sentinel: + del decoder_cls._axolotl_shared_kv_patched + + try: + yield decoder_cls, attn_cls, original_call, original_forward, fused_attn + finally: + decoder_cls.__call__ = original_call + attn_cls.forward = original_forward + if had_sentinel: + decoder_cls._axolotl_shared_kv_patched = True + elif hasattr(decoder_cls, "_axolotl_shared_kv_patched"): + del decoder_cls._axolotl_shared_kv_patched + fused_attn._set_shared_kv_states(None) + + +class TestPatchGemma4FusedAttnEntryPoint: + def test_default_flag_swaps_only_attention_forward( + self, clean_entry_point_patch_slate + ): + ( + decoder_cls, + attn_cls, + original_call, + original_forward, + fused_attn, + ) = clean_entry_point_patch_slate + + fused_attn.patch_gemma4_fused_attn() + + assert attn_cls.forward is not original_forward + assert decoder_cls.__call__ is original_call + assert not getattr(decoder_cls, "_axolotl_shared_kv_patched", False) + + def test_workaround_flag_installs_decoder_layer_patch( + self, clean_entry_point_patch_slate + ): + ( + decoder_cls, + attn_cls, + original_call, + original_forward, + fused_attn, + ) = clean_entry_point_patch_slate + + fused_attn.patch_gemma4_fused_attn(install_shared_kv_workaround=True) + + assert attn_cls.forward is not original_forward + assert decoder_cls.__call__ is not original_call + assert getattr(decoder_cls, "_axolotl_shared_kv_patched", False) is True From 05113bc91a5b8393918a618255022709e1474a6b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 22 Apr 2026 01:14:41 -0400 Subject: [PATCH 1277/1405] train on remote compute using Tinker compatible APIs (#3614) * train on remote compute using Tinker compatible APIs * chore: lint * fixes with latest hatchery changes * chore: lint --- src/axolotl/integrations/hatchery/__init__.py | 27 ++ src/axolotl/integrations/hatchery/args.py | 62 +++ src/axolotl/integrations/hatchery/data.py | 160 +++++++ .../hatchery/examples/prep_math_rl.py | 87 ++++ .../hatchery/examples/tinker_rl.yaml | 47 ++ .../hatchery/examples/tinker_sft.yaml | 42 ++ src/axolotl/integrations/hatchery/plugin.py | 147 +++++++ .../integrations/hatchery/rewards/__init__.py | 3 + .../hatchery/rewards/math_reward.py | 78 ++++ .../integrations/hatchery/rl_trainer.py | 409 ++++++++++++++++++ src/axolotl/integrations/hatchery/trainer.py | 327 ++++++++++++++ 11 files changed, 1389 insertions(+) create mode 100644 src/axolotl/integrations/hatchery/__init__.py create mode 100644 src/axolotl/integrations/hatchery/args.py create mode 100644 src/axolotl/integrations/hatchery/data.py create mode 100644 src/axolotl/integrations/hatchery/examples/prep_math_rl.py create mode 100644 src/axolotl/integrations/hatchery/examples/tinker_rl.yaml create mode 100644 src/axolotl/integrations/hatchery/examples/tinker_sft.yaml create mode 100644 src/axolotl/integrations/hatchery/plugin.py create mode 100644 src/axolotl/integrations/hatchery/rewards/__init__.py create mode 100644 src/axolotl/integrations/hatchery/rewards/math_reward.py create mode 100644 src/axolotl/integrations/hatchery/rl_trainer.py create mode 100644 src/axolotl/integrations/hatchery/trainer.py diff --git a/src/axolotl/integrations/hatchery/__init__.py b/src/axolotl/integrations/hatchery/__init__.py new file mode 100644 index 0000000000..c0d8510dbc --- /dev/null +++ b/src/axolotl/integrations/hatchery/__init__.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Hatchery/Tinker remote training integration for Axolotl. + +Routes axolotl's preprocessed data to a remote training API (Tinker or +Hatchery) instead of running forward/backward locally. The remote +service handles model weights, LoRA adapters, and gradient updates. +""" + +from .args import HatcheryArgs, HatcheryConfig +from .plugin import HatcheryPlugin + +__all__ = ["HatcheryArgs", "HatcheryConfig", "HatcheryPlugin"] + +# Usage: +# plugins: +# - axolotl.integrations.hatchery.HatcheryPlugin +# +# hatchery: +# backend: tinker # or "hatchery" +# lora_rank: 32 +# loss_fn: cross_entropy # SFT +# # loss_fn: ppo # RL (auto-selects HatcheryRLTrainer) +# +# learning_rate: 1e-4 # top-level, not under hatchery: diff --git a/src/axolotl/integrations/hatchery/args.py b/src/axolotl/integrations/hatchery/args.py new file mode 100644 index 0000000000..e3fdb95a24 --- /dev/null +++ b/src/axolotl/integrations/hatchery/args.py @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Pydantic config schema for the Hatchery integration.""" + +from __future__ import annotations + +from typing import Any, Literal, Optional + +from pydantic import BaseModel, Field + + +class HatcheryConfig(BaseModel): + """Nested config under `hatchery:` in the axolotl YAML. + + Only contains hatchery-specific settings. Standard training params + (learning_rate, weight_decay, adam_beta1/2, max_grad_norm, + gradient_accumulation_steps) are read from axolotl's top-level config. + """ + + # Backend & connection + backend: Literal["tinker", "hatchery"] = "tinker" + base_url: Optional[str] = None + api_key: Optional[str] = None + project_id: Optional[str] = None + + # LoRA config sent to remote + lora_rank: int = Field(32, ge=1, le=256) + train_attn: bool = True + train_mlp: bool = True + train_unembed: bool = True + + # Loss function + loss_fn: Literal["cross_entropy", "importance_sampling", "ppo", "cispo", "dro"] = ( + "cross_entropy" + ) + loss_fn_config: Optional[dict[str, Any]] = None + + # Pipelining: submit next batch before awaiting previous result + pipeline: bool = True + + # Sampling params (for RL flows) + max_sample_tokens: int = 256 + sample_temperature: float = 1.0 + num_samples: int = 4 + + # Reward functions (for RL) — list of fully qualified names + reward_funcs: Optional[list[str]] = None + + # Checkpointing + save_steps: Optional[int] = None + save_name_prefix: str = "checkpoint" + + # Timeout per future (seconds) + future_timeout: float = 600.0 + + +class HatcheryArgs(BaseModel): + """Top-level mixin that adds the nested `hatchery:` field.""" + + hatchery: Optional[HatcheryConfig] = None diff --git a/src/axolotl/integrations/hatchery/data.py b/src/axolotl/integrations/hatchery/data.py new file mode 100644 index 0000000000..f7baa2cca4 --- /dev/null +++ b/src/axolotl/integrations/hatchery/data.py @@ -0,0 +1,160 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Convert axolotl batch tensors to Tinker/Hatchery Datum format. + +Both Tinker and Hatchery expect the client to apply the causal LM shift: + + Original tokens: [t0, t1, t2, ..., t_{L-1}] + model_input: [t0, t1, ..., t_{L-2}] (last token dropped) + target_tokens: [t1, t2, ..., t_{L-1}] (first token dropped) + weights: [w1, w2, ..., w_{L-1}] (aligned to targets) + +At position i, the model sees t_i and predicts target_tokens[i] = t_{i+1}. +""" + +from __future__ import annotations + +from typing import Any + +import torch + + +def _tensor_to_wire(t: torch.Tensor) -> dict[str, Any]: + """Serialize a tensor to the TensorData wire dict.""" + flat = t.detach().cpu().flatten() + dtype_map = { + torch.float32: "float32", + torch.float16: "float16", + torch.bfloat16: "bfloat16", + torch.int64: "int64", + torch.int32: "int32", + } + return { + "dtype": dtype_map.get(flat.dtype, "float32"), + "shape": list(t.shape), + "data": flat.tolist(), + } + + +def _make_datum( + tokens: list[int], + loss_fn_inputs: dict[str, torch.Tensor], +) -> dict[str, Any]: + """Build a Datum as a plain dict (wire-compatible with both Tinker and Hatchery).""" + return { + "model_input": { + "chunks": [{"type": "encoded_text", "tokens": tokens}], + }, + "loss_fn_inputs": { + key: _tensor_to_wire(tensor) for key, tensor in loss_fn_inputs.items() + }, + } + + +def datums_to_tinker(datums: list[dict[str, Any]]): + """Wrap plain-dict datums into tinker.types.Datum objects. + + Both the Tinker SDK and updated Hatchery client accept these. + """ + import tinker.types as tt + + result = [] + for d in datums: + tokens = d["model_input"]["chunks"][0]["tokens"] + tinker_inputs = {} + for key, wire in d["loss_fn_inputs"].items(): + tinker_inputs[key] = tt.TensorData( + data=wire["data"], + dtype=wire["dtype"], + shape=wire["shape"], + ) + result.append( + tt.Datum( + model_input=tt.ModelInput.from_ints(tokens), + loss_fn_inputs=tinker_inputs, + ) + ) + return result + + +def batch_to_datums_sft( + input_ids: torch.Tensor, + labels: torch.Tensor, + attention_mask: torch.Tensor | None = None, +) -> list[dict[str, Any]]: + """Convert an axolotl SFT batch to Datum dicts with causal shift.""" + batch_size = input_ids.size(0) + datums = [] + + for i in range(batch_size): + ids = input_ids[i] + lbl = labels[i] + + if attention_mask is not None: + seq_len = int(attention_mask[i].sum().item()) + ids = ids[:seq_len] + lbl = lbl[:seq_len] + + model_tokens = ids[:-1].tolist() + shifted_labels = lbl[1:] + + target_tokens = shifted_labels.clone() + weights = (shifted_labels != -100).float() + target_tokens[target_tokens == -100] = 0 + + datums.append( + _make_datum( + model_tokens, + { + "target_tokens": target_tokens, + "weights": weights, + }, + ) + ) + + return datums + + +def batch_to_datums_rl( + input_ids: torch.Tensor, + labels: torch.Tensor, + logprobs: torch.Tensor, + advantages: torch.Tensor, + attention_mask: torch.Tensor | None = None, +) -> list[dict[str, Any]]: + """Convert an RL batch to importance_sampling/ppo Datum dicts with causal shift.""" + batch_size = input_ids.size(0) + datums = [] + + for i in range(batch_size): + ids = input_ids[i] + lbl = labels[i] + + if attention_mask is not None: + seq_len = int(attention_mask[i].sum().item()) + else: + seq_len = ids.size(0) + ids = ids[:seq_len] + lbl = lbl[:seq_len] + lp = logprobs[i, :seq_len] + adv = advantages[i, :seq_len] + + model_tokens = ids[:-1].tolist() + + target_tokens = lbl[1:].clone() + target_tokens[target_tokens == -100] = 0 + + datums.append( + _make_datum( + model_tokens, + { + "target_tokens": target_tokens, + "logprobs": lp[1:], + "advantages": adv[1:], + }, + ) + ) + + return datums diff --git a/src/axolotl/integrations/hatchery/examples/prep_math_rl.py b/src/axolotl/integrations/hatchery/examples/prep_math_rl.py new file mode 100644 index 0000000000..1838159075 --- /dev/null +++ b/src/axolotl/integrations/hatchery/examples/prep_math_rl.py @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Prepare hendrycks_math for RL training with Hatchery/Tinker. + +Creates a dataset with chat-formatted prompts that include +a hidden gold answer tag for the reward function. + +Run: + python src/axolotl/integrations/hatchery/examples/prep_math_rl.py +""" + +import os +import re + +from datasets import Dataset, load_dataset +from transformers import AutoTokenizer + + +def extract_boxed(text: str) -> str: + match = re.search(r"\\boxed\{", text) + if not match: + return "" + start = match.end() + depth = 1 + i = start + while i < len(text) and depth > 0: + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + i += 1 + return text[start : i - 1] if depth == 0 else "" + + +def main(): + tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B", trust_remote_code=True) + + ds = load_dataset("EleutherAI/hendrycks_math", "algebra", split="test") + level = os.environ.get("MATH_LEVEL", "Level 1") + filtered_rows = [x for x in ds if x["level"] == level] + print(f"{level} algebra: {len(filtered_rows)} problems") + + rows = [] + for prob in filtered_rows: + gold = extract_boxed(prob["solution"]) + if not gold: + continue + + # Format as chat prompt with hidden gold tag + prompt = ( + f"Solve the following math problem. " + f"Show your work and put your final answer in \\boxed{{}}.\n\n" + f"{prob['problem']}" + f"<|gold|>{gold}<|/gold|>" + ) + + # Tokenize the prompt + text = tokenizer.apply_chat_template( + [{"role": "user", "content": prompt}], + tokenize=False, + add_generation_prompt=True, + ) + prompt_ids = tokenizer.encode(text, add_special_tokens=False) + + rows.append( + { + "input_ids": prompt_ids, + "labels": [-100] * len(prompt_ids), + "attention_mask": [1] * len(prompt_ids), + } + ) + + out = Dataset.from_list(rows) + out_dir = f"./data/math_rl_{level.lower().replace(' ', '')}" + out.save_to_disk(out_dir) + print(f"Saved {len(out)} examples to {out_dir}") + if rows: + print( + f"Prompt length range: {min(len(r['input_ids']) for r in rows)}" + f"-{max(len(r['input_ids']) for r in rows)}" + ) + + +if __name__ == "__main__": + main() diff --git a/src/axolotl/integrations/hatchery/examples/tinker_rl.yaml b/src/axolotl/integrations/hatchery/examples/tinker_rl.yaml new file mode 100644 index 0000000000..caab3fe0d8 --- /dev/null +++ b/src/axolotl/integrations/hatchery/examples/tinker_rl.yaml @@ -0,0 +1,47 @@ +# RL (GRPO): hendrycks_math Level 1 via Tinker with Qwen3-8B +# +# Prep: +# python src/axolotl/integrations/hatchery/examples/prep_math_rl.py +# +# Run: +# export TINKER_API_KEY="your-key" +# axolotl train src/axolotl/integrations/hatchery/examples/tinker_rl.yaml + +base_model: Qwen/Qwen3-8B + +plugins: + - axolotl.integrations.hatchery.HatcheryPlugin + +hatchery: + backend: tinker + lora_rank: 16 + loss_fn: importance_sampling + max_sample_tokens: 2048 + sample_temperature: 0.7 + num_samples: 4 + pipeline: true + save_steps: 5 + reward_funcs: + - axolotl.integrations.hatchery.rewards.math_reward.math_reward + +datasets: + - path: ./data/math_rl_level1 + ds_type: arrow + type: completion + +sequence_len: 2048 + +learning_rate: 5.0e-5 +optimizer: adamw_torch +adam_beta1: 0.9 +adam_beta2: 0.95 +weight_decay: 0.01 +max_grad_norm: 1.0 + +max_steps: 10 +num_epochs: 1 +micro_batch_size: 1 +gradient_accumulation_steps: 1 +logging_steps: 1 + +output_dir: ./outputs/tinker-rl-math diff --git a/src/axolotl/integrations/hatchery/examples/tinker_sft.yaml b/src/axolotl/integrations/hatchery/examples/tinker_sft.yaml new file mode 100644 index 0000000000..d99f043ae1 --- /dev/null +++ b/src/axolotl/integrations/hatchery/examples/tinker_sft.yaml @@ -0,0 +1,42 @@ +# SFT: KIMI-K2 thinking data via Tinker remote API with Qwen3-8B +# +# Usage: +# export TINKER_API_KEY="your-key" +# axolotl train src/axolotl/integrations/hatchery/examples/tinker_sft.yaml + +base_model: Qwen/Qwen3-8B + +plugins: + - axolotl.integrations.hatchery.HatcheryPlugin + +hatchery: + backend: tinker + lora_rank: 16 + loss_fn: cross_entropy + pipeline: true + save_steps: 10 + +datasets: + - path: TeichAI/kimi-k2-thinking-1000x + split: train[:50] + type: chat_template + chat_template: qwen3 + split_thinking: true + +chat_template: qwen3 +sequence_len: 2048 + +learning_rate: 3.0e-4 +optimizer: adamw_torch +adam_beta1: 0.9 +adam_beta2: 0.95 +weight_decay: 0.01 +max_grad_norm: 1.0 + +num_epochs: 1 +max_steps: 20 +micro_batch_size: 2 +gradient_accumulation_steps: 1 +logging_steps: 1 + +output_dir: ./outputs/tinker-sft diff --git a/src/axolotl/integrations/hatchery/plugin.py b/src/axolotl/integrations/hatchery/plugin.py new file mode 100644 index 0000000000..1546958e80 --- /dev/null +++ b/src/axolotl/integrations/hatchery/plugin.py @@ -0,0 +1,147 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Axolotl plugin that routes training to a remote Hatchery/Tinker API.""" + +from __future__ import annotations + +import torch +from peft import PeftModel +from transformers import AutoConfig, PreTrainedModel, Trainer + +from axolotl.integrations.base import BasePlugin +from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +class HatcheryPlugin(BasePlugin): + """Plugin that replaces local training with remote API calls. + + Activated by adding to the axolotl YAML: + + plugins: + - axolotl.integrations.hatchery.HatcheryPlugin + + hatchery: + backend: tinker # or "hatchery" + lora_rank: 32 + loss_fn: cross_entropy + # ... see HatcheryConfig for full options + """ + + def get_input_args(self) -> str: + return "axolotl.integrations.hatchery.args.HatcheryArgs" + + def register(self, cfg: dict): + """Auto-set config values needed for remote training.""" + if cfg.get("remove_unused_columns") is None: + cfg["remove_unused_columns"] = False + + def pre_model_load(self, cfg: DictDefault): + """Replace model loading with a tiny stub.""" + hcfg = cfg.hatchery or {} + backend = ( + hcfg.get("backend", "tinker") + if isinstance(hcfg, dict) + else getattr(hcfg, "backend", "tinker") + ) + LOG.info( + f"Hatchery plugin active: training dispatched to remote " + f"{backend} API. Skipping local model weight loading." + ) + + from axolotl.loaders import ModelLoader + + def _stub_build_model(loader_self) -> bool: + base_model = loader_self.cfg.base_model + LOG.info(f"Skipping model weight loading for: {base_model}") + + config = AutoConfig.from_pretrained( + base_model, + trust_remote_code=loader_self.cfg.get("trust_remote_code", False), + ) + + class _Stub(PreTrainedModel): + config_class = type(config) + _no_split_modules: list[str] = [] + supports_gradient_checkpointing = False + + def __init__(self, cfg): + super().__init__(cfg) + vocab_size = getattr(cfg, "vocab_size", 32000) + self.embed_tokens = torch.nn.Embedding(vocab_size, 1) + + def get_input_embeddings(self): + return self.embed_tokens + + def set_input_embeddings(self, value): + pass + + def get_output_embeddings(self): + return None + + loader_self.model = _Stub(config) + return True + + ModelLoader._build_model = _stub_build_model # type: ignore[method-assign,assignment] + + def get_trainer_cls(self, cfg: DictDefault) -> type[Trainer] | None: + """Return the appropriate remote trainer class.""" + hcfg = cfg.hatchery + loss_fn = getattr(hcfg, "loss_fn", "cross_entropy") if hcfg else "cross_entropy" + + if loss_fn in ("importance_sampling", "ppo", "cispo", "dro"): + from .rl_trainer import HatcheryRLTrainer + + return HatcheryRLTrainer + + from .trainer import HatcheryTrainer + + return HatcheryTrainer + + def post_model_load(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): + model._hatchery_remote = True + + def post_train(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): + LOG.info( + "Hatchery: skipping local model save (weights are on remote API). " + "Use `tinker checkpoint download` or hatchery CLI to retrieve." + ) + + def post_trainer_create(self, cfg: DictDefault, trainer: Trainer): + """Inject hatchery config + axolotl training params into the trainer.""" + from .args import HatcheryConfig + from .rl_trainer import HatcheryRLTrainer + from .trainer import HatcheryTrainer + + if not isinstance(trainer, (HatcheryTrainer, HatcheryRLTrainer)): + return + + hcfg = cfg.hatchery + if isinstance(hcfg, dict): + hatchery_config = HatcheryConfig(**hcfg) + elif hcfg is None: + hatchery_config = HatcheryConfig() + else: + hatchery_config = hcfg + + trainer.hatchery_args = hatchery_config + trainer._base_model_name = cfg.base_model + + # Pull standard training params from axolotl config so they + # don't need to be duplicated under hatchery: + trainer._optim_params = { + "learning_rate": cfg.learning_rate + if cfg.learning_rate is not None + else 1e-4, + "beta1": cfg.adam_beta1 if cfg.adam_beta1 is not None else 0.9, + "beta2": cfg.adam_beta2 if cfg.adam_beta2 is not None else 0.95, + "eps": cfg.adam_epsilon if cfg.adam_epsilon is not None else 1e-12, + "weight_decay": cfg.weight_decay if cfg.weight_decay is not None else 0.0, + "grad_clip_norm": cfg.max_grad_norm + if cfg.max_grad_norm is not None + else 0.0, + } diff --git a/src/axolotl/integrations/hatchery/rewards/__init__.py b/src/axolotl/integrations/hatchery/rewards/__init__.py new file mode 100644 index 0000000000..1cfe76e777 --- /dev/null +++ b/src/axolotl/integrations/hatchery/rewards/__init__.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 diff --git a/src/axolotl/integrations/hatchery/rewards/math_reward.py b/src/axolotl/integrations/hatchery/rewards/math_reward.py new file mode 100644 index 0000000000..970353b0bd --- /dev/null +++ b/src/axolotl/integrations/hatchery/rewards/math_reward.py @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Math reward function for hendrycks_math GRPO training. + +Uses math_verify for robust answer comparison. Falls back to +exact string match of \\boxed{} content only when math_verify +is unavailable. +""" + +from __future__ import annotations + +import logging +import re + +LOG = logging.getLogger(__name__) + + +def extract_boxed(text: str) -> str | None: + """Extract \\boxed{...} answer handling nested braces.""" + match = re.search(r"\\boxed\{", text) + if not match: + return None + start = match.end() + depth = 1 + i = start + while i < len(text) and depth > 0: + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + i += 1 + return text[start : i - 1] if depth == 0 else None + + +def math_reward(prompts: list[str], completions: list[str], **kwargs) -> list[float]: + """Score completions by checking if \\boxed{} answer matches the gold answer. + + The gold answer is extracted from the prompt (appended as a hidden + tag by the dataset preprocessing). Format: + ... <|gold|>ANSWER<|/gold|> + """ + rewards = [] + for prompt, completion in zip(prompts, completions, strict=True): + gold_match = re.search(r"<\|gold\|>(.*?)<\|/gold\|>", prompt) + if not gold_match: + rewards.append(0.0) + continue + + gold_answer = gold_match.group(1).strip() + pred_answer = extract_boxed(completion) + + if pred_answer is None: + rewards.append(0.0) + continue + + verified = None + try: + from math_verify import parse, verify + + gold_parsed = parse(gold_answer) + pred_parsed = parse(pred_answer) + verified = verify(gold_parsed, pred_parsed) + except Exception: + LOG.debug( + "math_verify unavailable or failed, using string fallback", + exc_info=True, + ) + + if verified is not None: + rewards.append(1.0 if verified else 0.0) + elif pred_answer.strip() == gold_answer.strip(): + rewards.append(1.0) + else: + rewards.append(0.0) + + return rewards diff --git a/src/axolotl/integrations/hatchery/rl_trainer.py b/src/axolotl/integrations/hatchery/rl_trainer.py new file mode 100644 index 0000000000..fc6d32d6a6 --- /dev/null +++ b/src/axolotl/integrations/hatchery/rl_trainer.py @@ -0,0 +1,409 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Remote RL trainer (GRPO/PPO) using Tinker or Hatchery API. + +Full RL loop per step: + 1. Extract prompts from dataset batch + 2. Sample N completions per prompt via remote SamplingClient + 3. Score completions with local reward functions + 4. Compute GRPO-style advantages (per-group normalization) + 5. Send (prompt+completion, logprobs, advantages) as forward_backward + 6. Optimizer step +""" + +from __future__ import annotations + +import importlib +import inspect +import re +import time +from typing import Any, Callable, Optional + +import torch +from transformers.trainer_utils import TrainOutput + +from axolotl.core.trainers.base import AxolotlTrainer +from axolotl.utils.logging import get_logger + +from .args import HatcheryConfig +from .data import batch_to_datums_rl, datums_to_tinker +from .trainer import _create_training_client + +LOG = get_logger(__name__) + + +def _load_reward_func(fqn: str) -> Callable: + """Load a reward function from a fully qualified name like 'module.func'.""" + module_path = ".".join(fqn.split(".")[:-1]) + func_name = fqn.split(".")[-1] + mod = importlib.import_module(module_path) + func = getattr(mod, func_name) + if len(inspect.signature(func).parameters) < 2: + raise ValueError(f"Reward function {fqn} must accept (prompts, completions)") + return func + + +class HatcheryRLTrainer(AxolotlTrainer): + """Remote RL trainer using Tinker/Hatchery for sampling and training.""" + + hatchery_args: Optional[HatcheryConfig] + _base_model_name: Optional[str] + _training_client: Any + _reward_functions: list[Callable] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.hatchery_args = None + self._base_model_name = None + self._training_client = None + self._reward_functions = [] + + def _ensure_reward_functions(self): + if self._reward_functions: + return + args = self.hatchery_args + if not args or not args.reward_funcs: + raise ValueError( + "No reward functions configured. Set hatchery.reward_funcs " + "in YAML, e.g. reward_funcs: ['my_module.my_reward']" + ) + for fqn in args.reward_funcs: + self._reward_functions.append(_load_reward_func(fqn)) + LOG.info(f"Loaded {len(self._reward_functions)} reward function(s)") + + def _get_training_client(self): + if self._training_client is not None: + return self._training_client + + self._training_client = _create_training_client( + self.hatchery_args, self._base_model_name + ) + LOG.info( + f"Remote RL session created: backend={self.hatchery_args.backend}, " + f"model={self._base_model_name}, rank={self.hatchery_args.lora_rank}" + ) + return self._training_client + + def _sample_completions(self, prompt_ids_list: list[list[int]]): + """Sample completions for prompts via remote API.""" + import tinker.types as tt + + tc = self._get_training_client() + args = self.hatchery_args + assert args is not None # validated by _get_training_client + results = [] + + sc = tc.save_weights_and_get_sampling_client() + + for prompt_ids in prompt_ids_list: + if hasattr(sc, "sampling_session_id"): + sample_result = sc.sample( + prompt_ids, + max_tokens=args.max_sample_tokens, + temperature=args.sample_temperature, + n=args.num_samples, + ).result(timeout=args.future_timeout) + else: + mi = tt.ModelInput.from_ints(prompt_ids) + sp = tt.SamplingParams( + max_tokens=args.max_sample_tokens, + temperature=args.sample_temperature, + top_p=0.95, + top_k=-1, + ) + sample_result = sc.sample( + prompt=mi, + num_samples=args.num_samples, + sampling_params=sp, + ).result(timeout=args.future_timeout) + + sequences = ( + sample_result.sequences + if hasattr(sample_result, "sequences") + else sample_result.get("sequences", []) + ) + for seq in sequences: + tokens = ( + list(seq.tokens) + if hasattr(seq, "tokens") + else seq.get("tokens", []) + ) + logprobs = ( + list(seq.logprobs) + if hasattr(seq, "logprobs") and seq.logprobs + else seq.get("logprobs", []) + ) + results.append( + { + "tokens": list(prompt_ids) + tokens, + "completion_tokens": tokens, + "logprobs": logprobs, + "prompt_len": len(prompt_ids), + } + ) + + return results + + def _compute_rewards( + self, prompts: list[str], completions: list[str] + ) -> list[float]: + total_rewards = [0.0] * len(completions) + for reward_fn in self._reward_functions: + rewards = reward_fn(prompts, completions) + for i, r in enumerate(rewards): + total_rewards[i] += r + return total_rewards + + @staticmethod + def _compute_advantages(rewards: list[float], group_size: int) -> list[float]: + advantages = [] + for i in range(0, len(rewards), group_size): + group = rewards[i : i + group_size] + mean = sum(group) / len(group) + var = sum((r - mean) ** 2 for r in group) / max(len(group), 1) + std = var**0.5 if var > 1e-8 else 1.0 + advantages.extend([(r - mean) / std for r in group]) + return advantages + + def _do_optim_step(self): + import tinker.types as tt + + tc = self._get_training_client() + return tc.optim_step(tt.AdamParams(**self._optim_params)) + + def train( + self, + resume_from_checkpoint: Optional[str] = None, + trial: Any = None, + ignore_keys_for_eval: Optional[list[str]] = None, + **kwargs, + ) -> TrainOutput: + args = self.hatchery_args + if args is None: + raise RuntimeError("hatchery_args not configured") + + self._ensure_reward_functions() + + train_dataloader = self.get_train_dataloader() + num_train_epochs = int(self.args.num_train_epochs) + max_steps = self.args.max_steps if self.args.max_steps > 0 else 1000 + + LOG.info( + f"Remote RL training: max_steps={max_steps}, " + f"loss_fn={args.loss_fn}, samples/prompt={args.num_samples}" + ) + + self.state.max_steps = max_steps + self.state.num_train_epochs = num_train_epochs + self.state.is_local_process_zero = True + self.state.is_world_process_zero = True + + self.control = self.callback_handler.on_train_begin( + self.args, + self.state, + self.control, # type: ignore[has-type] + ) + + tokenizer = self.processing_class + global_step = 0 + total_loss = 0.0 + total_reward = 0.0 + start_time = time.time() + + for _epoch in range(num_train_epochs): + if global_step >= max_steps: + break + + for batch in train_dataloader: + if global_step >= max_steps: + break + + self.control = self.callback_handler.on_step_begin( + self.args, self.state, self.control + ) + + prompt_ids_batch = batch["input_ids"] + # Full prompt text (with gold tag) for reward scoring + prompt_texts = tokenizer.batch_decode( + prompt_ids_batch, skip_special_tokens=False + ) + + # Strip <|gold|>...<|/gold|> from token ids before + # sending to the model for sampling — the gold answer + # must only be visible to the local reward function. + sampling_prompts = [] + for prompt_text in prompt_texts: + clean = re.sub(r"<\|gold\|>.*?<\|/gold\|>", "", prompt_text) + clean_ids = tokenizer.encode(clean, add_special_tokens=False) + sampling_prompts.append(clean_ids) + + # 1. Sample completions (without gold answer) + t0 = time.time() + samples = self._sample_completions(sampling_prompts) + t_sample = time.time() - t0 + + if not samples: + LOG.warning("No samples generated, skipping step") + continue + LOG.info( + f"Sampled {len(samples)} completions, " + f"avg_len={sum(len(s['completion_tokens']) for s in samples) / len(samples):.0f}tok" + ) + + # 2. Decode and score + completion_texts = [ + tokenizer.decode(s["completion_tokens"], skip_special_tokens=False) + for s in samples + ] + sample_prompts = [] + for prompt_text in prompt_texts: + sample_prompts.extend([prompt_text] * args.num_samples) + + rewards = self._compute_rewards(sample_prompts, completion_texts) + + # 3. GRPO advantages + advantages_list = self._compute_advantages( + rewards, group_size=args.num_samples + ) + + # 4. Build training data + all_datums = [] + for i, sample in enumerate(samples): + full_tokens = sample["tokens"] + prompt_len = sample["prompt_len"] + seq_len = len(full_tokens) + + input_ids = torch.tensor([full_tokens], dtype=torch.long) + labels = torch.full((1, seq_len), -100, dtype=torch.long) + labels[0, prompt_len:] = torch.tensor(full_tokens[prompt_len:]) + + logprobs_t = torch.zeros(1, seq_len) + if sample["logprobs"]: + lp = sample["logprobs"][: seq_len - prompt_len] + logprobs_t[0, prompt_len : prompt_len + len(lp)] = torch.tensor( + lp + ) + + adv_t = torch.zeros(1, seq_len) + adv_t[0, prompt_len:] = advantages_list[i] + + all_datums.extend( + batch_to_datums_rl(input_ids, labels, logprobs_t, adv_t) + ) + + # 5. Forward backward (one datum at a time for memory) + optim + t0 = time.time() + tc = self._get_training_client() + step_loss = 0.0 + for datum in all_datums: + fb_future = tc.forward_backward( + datums_to_tinker([datum]), + loss_fn=args.loss_fn, + loss_fn_config=args.loss_fn_config, + ) + fb_result = fb_future.result(timeout=args.future_timeout) + if hasattr(fb_result, "metrics"): + step_loss += float( + (fb_result.metrics or {}).get("loss:sum", 0.0) + ) + elif isinstance(fb_result, dict): + step_loss += float( + fb_result.get("metrics", {}).get("loss:sum", 0.0) + ) + optim_future = self._do_optim_step() + if not args.pipeline: + optim_future.result(timeout=args.future_timeout) + t_train = time.time() - t0 + + mean_reward = sum(rewards) / len(rewards) + accuracy = sum(1 for r in rewards if r > 0) / len(rewards) + mean_adv = sum(abs(a) for a in advantages_list) / len(advantages_list) + global_step += 1 + total_loss += step_loss + total_reward += mean_reward + self.state.global_step = global_step + + log_interval = self.args.logging_steps or 1 + if global_step % log_interval == 0: + elapsed = time.time() - start_time + LOG.info( + f"[step {global_step}/{max_steps}] " + f"acc={accuracy:.2f} reward={mean_reward:.3f} " + f"|adv|={mean_adv:.3f} loss:sum={step_loss:.1f} " + f"sample={t_sample:.1f}s train={t_train:.1f}s " + f"{elapsed / global_step:.1f}s/step" + ) + self.log( + { + "loss": step_loss, + "reward": mean_reward, + "accuracy": accuracy, + "mean_abs_advantage": mean_adv, + "learning_rate": self._optim_params["learning_rate"], + } + ) + + if args.save_steps and global_step % args.save_steps == 0: + self._save_remote_checkpoint(global_step) + + self.control = self.callback_handler.on_step_end( + self.args, self.state, self.control + ) + if self.control.should_training_stop: + break + + if self.control.should_training_stop: + break + + if global_step > 0: + self._save_remote_checkpoint(global_step, name="final") + + elapsed = time.time() - start_time + avg_loss = total_loss / max(global_step, 1) + avg_reward = total_reward / max(global_step, 1) + + LOG.info( + f"RL training complete: {global_step} steps, {elapsed:.1f}s, " + f"avg_reward={avg_reward:.4f}" + ) + + self.control = self.callback_handler.on_train_end( + self.args, self.state, self.control + ) + + return TrainOutput( + global_step=global_step, + training_loss=avg_loss, + metrics={ + "train_loss": avg_loss, + "train_reward": avg_reward, + "train_runtime": elapsed, + }, + ) + + def _save_remote_checkpoint(self, step: int, name: Optional[str] = None): + tc = self._get_training_client() + args = self.hatchery_args + assert args is not None # validated by _get_training_client + ckpt_name = name or f"{args.save_name_prefix}-{step:06d}" + try: + future = tc.save_state(ckpt_name) + future.result(timeout=args.future_timeout) + LOG.info(f"Remote checkpoint saved: {ckpt_name}") + except Exception: + LOG.exception(f"Failed to save checkpoint {ckpt_name}") + if name == "final": + raise + + def save_model(self, output_dir=None, _internal_call=False): + self._save_remote_checkpoint( + step=self.state.global_step, + name=output_dir or "hf-save", + ) + + def compute_loss(self, model, inputs, return_outputs=False, **kwargs): + raise NotImplementedError( + "HatcheryRLTrainer uses remote API; compute_loss not called locally." + ) diff --git a/src/axolotl/integrations/hatchery/trainer.py b/src/axolotl/integrations/hatchery/trainer.py new file mode 100644 index 0000000000..4eb632db37 --- /dev/null +++ b/src/axolotl/integrations/hatchery/trainer.py @@ -0,0 +1,327 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Remote trainer that dispatches to Tinker or Hatchery API.""" + +from __future__ import annotations + +import os +import time +from typing import Any, Optional + +import torch +from transformers.trainer_utils import TrainOutput + +from axolotl.core.trainers.base import AxolotlTrainer +from axolotl.utils.logging import get_logger + +from .args import HatcheryConfig +from .data import batch_to_datums_sft, datums_to_tinker + +LOG = get_logger(__name__) + + +def _extract_loss(result) -> float: + """Extract loss:sum from a forward_backward result. + + Tinker's cross_entropy (and other losses) return the SUM of per-token + losses, not the mean. This is by design — it lets users control + normalization via the weights tensor. The trainer logs this raw sum; + users who want per-token loss should divide by number of active tokens. + """ + if hasattr(result, "metrics"): + metrics = result.metrics or {} + return float(metrics.get("loss:sum", metrics.get("loss", 0.0))) + if isinstance(result, dict): + metrics = result.get("metrics", {}) + return float(metrics.get("loss:sum", metrics.get("loss", 0.0))) + return 0.0 + + +def _create_training_client(args: HatcheryConfig, base_model: str): + """Create a training client for either Tinker or Hatchery backend.""" + if args.backend == "tinker": + import tinker + + api_key = args.api_key or os.environ.get("TINKER_API_KEY") + if not api_key: + raise ValueError( + "Tinker API key required. Set `hatchery.api_key` in config " + "or TINKER_API_KEY env var." + ) + os.environ["TINKER_API_KEY"] = api_key + + service = tinker.ServiceClient(project_id=args.project_id) + return service.create_lora_training_client( + base_model=base_model, + rank=args.lora_rank, + train_mlp=args.train_mlp, + train_attn=args.train_attn, + train_unembed=args.train_unembed, + ) + + from hatchery.core.client import HatcheryClient + + base_url = args.base_url or os.environ.get("HATCHERY_URL", "http://127.0.0.1:8420") + token = args.api_key or os.environ.get("HATCHERY_API_KEY", "dev") + + client = HatcheryClient(base_url=base_url, token=token, timeout=args.future_timeout) + return client.create_lora_training_client( + base_model=base_model, + rank=args.lora_rank, + train_attn=args.train_attn, + train_mlp=args.train_mlp, + train_unembed=args.train_unembed, + ) + + +class HatcheryTrainer(AxolotlTrainer): + """Trainer that sends preprocessed batches to a remote training API. + + Replaces local forward/backward with remote API calls to Tinker or + Hatchery. Uses axolotl's full data preprocessing pipeline (tokenization, + chat templates, packing, etc.) but offloads compute to remote GPUs. + """ + + hatchery_args: Optional[HatcheryConfig] + _base_model_name: Optional[str] + _training_client: Any + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.hatchery_args = None + self._base_model_name = None + self._training_client = None + + def _get_training_client(self): + """Lazily create the remote training session.""" + if self._training_client is not None: + return self._training_client + + args = self.hatchery_args + if args is None: + raise RuntimeError( + "HatcheryTrainer.hatchery_args not set. " + "Ensure the HatcheryPlugin is registered." + ) + + base_model = self._base_model_name + if not base_model: + raise RuntimeError("HatcheryTrainer._base_model_name not set.") + + self._training_client = _create_training_client(args, base_model) + + LOG.info( + f"Remote training session created: backend={args.backend}, " + f"model={base_model}, rank={args.lora_rank}" + ) + return self._training_client + + def _send_batch(self, batch: dict[str, torch.Tensor]): + """Convert batch to datums and send forward_backward to remote. + + Returns (future, n_active_tokens) where n_active_tokens counts + the completion tokens in this batch (for loss normalization). + """ + input_ids = batch["input_ids"] + labels = batch["labels"] + attention_mask = batch.get("attention_mask") + + n_active = int((labels[:, 1:] != -100).sum().item()) + datums = batch_to_datums_sft(input_ids, labels, attention_mask) + + tc = self._get_training_client() + args = self.hatchery_args + assert args is not None # validated by _get_training_client + send_datums = datums_to_tinker(datums) + + future = tc.forward_backward( + send_datums, + loss_fn=args.loss_fn, + loss_fn_config=args.loss_fn_config, + ) + return future, n_active + + def _do_optim_step(self): + """Send optimizer step to remote using axolotl's training params.""" + import tinker.types as tt + + tc = self._get_training_client() + return tc.optim_step(tt.AdamParams(**self._optim_params)) + + def train( + self, + resume_from_checkpoint: Optional[str] = None, + trial: Any = None, + ignore_keys_for_eval: Optional[list[str]] = None, + **kwargs, + ) -> TrainOutput: + """Main training loop — sends batches to remote API.""" + args = self.hatchery_args + if args is None: + raise RuntimeError("hatchery_args not configured") + + train_dataloader = self.get_train_dataloader() + num_batches = len(train_dataloader) + + grad_accum = self.args.gradient_accumulation_steps + num_train_epochs = int(self.args.num_train_epochs) + steps_per_epoch = max(num_batches // grad_accum, 1) + max_steps = ( + self.args.max_steps + if self.args.max_steps > 0 + else steps_per_epoch * num_train_epochs + ) + + LOG.info( + f"Remote training: {num_batches} batches/epoch, " + f"{grad_accum} grad_accum, {max_steps} max steps, " + f"{num_train_epochs} epochs" + ) + + self.state.max_steps = max_steps + self.state.num_train_epochs = num_train_epochs + self.state.is_local_process_zero = True + self.state.is_world_process_zero = True + + self.control = self.callback_handler.on_train_begin( + self.args, + self.state, + self.control, # type: ignore[has-type] + ) + + global_step = 0 + total_loss = 0.0 + start_time = time.time() + + for _epoch in range(num_train_epochs): + if global_step >= max_steps: + break + + self.control = self.callback_handler.on_epoch_begin( + self.args, self.state, self.control + ) + + pending_fb_futures = [] + accum_count = 0 + + for batch_idx, batch in enumerate(train_dataloader): + if global_step >= max_steps: + break + + self.control = self.callback_handler.on_step_begin( + self.args, self.state, self.control + ) + + fb_future, n_active = self._send_batch(batch) + pending_fb_futures.append((fb_future, n_active)) + accum_count += 1 + + if accum_count >= grad_accum: + step_loss_sum = 0.0 + step_active = 0 + for fut, n_act in pending_fb_futures: + result = fut.result(timeout=args.future_timeout) + step_loss_sum += _extract_loss(result) + step_active += n_act + + optim_future = self._do_optim_step() + if not args.pipeline: + optim_future.result(timeout=args.future_timeout) + + step_loss = ( + step_loss_sum / step_active + if step_active > 0 + else step_loss_sum + ) + + global_step += 1 + total_loss += step_loss + self.state.global_step = global_step + self.state.epoch = _epoch + (batch_idx + 1) / num_batches + + log_interval = self.args.logging_steps or 1 + if global_step % log_interval == 0: + elapsed = time.time() - start_time + avg_loss = total_loss / global_step + LOG.info( + f"[step {global_step}/{max_steps}] " + f"loss/tok={step_loss:.4f} avg={avg_loss:.4f} " + f"active={step_active} " + f"{elapsed / global_step:.2f}s/step" + ) + self.log( + { + "loss": step_loss, + "learning_rate": self._optim_params["learning_rate"], + "epoch": self.state.epoch, + } + ) + + if args.save_steps and global_step % args.save_steps == 0: + self._save_remote_checkpoint(global_step) + + self.control = self.callback_handler.on_step_end( + self.args, self.state, self.control + ) + + pending_fb_futures = [] + accum_count = 0 + + if self.control.should_training_stop: + break + + self.control = self.callback_handler.on_epoch_end( + self.args, self.state, self.control + ) + if self.control.should_training_stop: + break + + if global_step > 0: + self._save_remote_checkpoint(global_step, name="final") + + elapsed = time.time() - start_time + avg_loss = total_loss / max(global_step, 1) + + LOG.info( + f"Training complete: {global_step} steps, {elapsed:.1f}s total, " + f"{elapsed / max(global_step, 1):.2f}s/step, avg_loss={avg_loss:.4f}" + ) + + self.control = self.callback_handler.on_train_end( + self.args, self.state, self.control + ) + + return TrainOutput( + global_step=global_step, + training_loss=avg_loss, + metrics={"train_loss": avg_loss, "train_runtime": elapsed}, + ) + + def _save_remote_checkpoint(self, step: int, name: Optional[str] = None): + """Save a checkpoint on the remote service.""" + tc = self._get_training_client() + args = self.hatchery_args + assert args is not None # validated by _get_training_client + ckpt_name = name or f"{args.save_name_prefix}-{step:06d}" + try: + future = tc.save_state(ckpt_name) + future.result(timeout=args.future_timeout) + LOG.info(f"Remote checkpoint saved: {ckpt_name}") + except Exception: + LOG.exception(f"Failed to save checkpoint {ckpt_name}") + if name == "final": + raise + + def save_model(self, output_dir=None, _internal_call=False): + """Delegate to remote checkpoint save so HF callbacks create checkpoints.""" + self._save_remote_checkpoint( + step=self.state.global_step, + name=output_dir or "hf-save", + ) + + def compute_loss(self, model, inputs, return_outputs=False, **kwargs): + raise NotImplementedError( + "HatcheryTrainer uses remote API; compute_loss should not be called." + ) From 7420fd4de6aa80196bddc0520039178354d1304b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 22 Apr 2026 09:05:46 -0400 Subject: [PATCH 1278/1405] fix async prefetch with nemogym (#3606) --- .../core/trainers/grpo/async_trainer.py | 170 ++++++++- .../integrations/nemo_gym/data_producer.py | 93 ++++- src/axolotl/integrations/nemo_gym/plugin.py | 236 ++++++++++-- src/axolotl/integrations/nemo_gym/server.py | 38 +- src/axolotl/kernels/gemma4_fused_rope.py | 170 ++++++--- src/axolotl/loaders/patch_manager.py | 27 ++ src/axolotl/monkeypatch/gemma4_hybrid_mask.py | 115 ++++++ src/axolotl/monkeypatch/tiled_mlp/patch.py | 10 +- src/axolotl/scripts/vllm_serve_lora.py | 153 ++++++++ src/axolotl/utils/schemas/validation.py | 82 ++++ tests/core/test_async_grpo.py | 192 ++++++++++ tests/integrations/test_nemo_gym.py | 354 +++++++++++++++++- tests/kernels/test_gemma4_fused_rope.py | 190 ++++++++++ tests/monkeypatch/test_gemma4_fused_attn.py | 219 +++++++++++ tests/monkeypatch/test_gemma4_hybrid_mask.py | 343 +++++++++++++++++ .../validation/test_config_validators.py | 135 +++++++ 16 files changed, 2390 insertions(+), 137 deletions(-) create mode 100644 src/axolotl/monkeypatch/gemma4_hybrid_mask.py create mode 100644 tests/monkeypatch/test_gemma4_fused_attn.py create mode 100644 tests/monkeypatch/test_gemma4_hybrid_mask.py diff --git a/src/axolotl/core/trainers/grpo/async_trainer.py b/src/axolotl/core/trainers/grpo/async_trainer.py index 3388687ad6..4759a30b01 100644 --- a/src/axolotl/core/trainers/grpo/async_trainer.py +++ b/src/axolotl/core/trainers/grpo/async_trainer.py @@ -242,6 +242,85 @@ def __post_init__(self): ) +class _GroupShardedSampler: + """Rank-aware shard of a ``RepeatSampler`` that preserves GRPO groups. + + ``RepeatSampler`` yields ``num_generations`` consecutive copies of + each prompt, forming a GRPO group. For distributed training each + rank must see a disjoint slice of prompts (otherwise every rank + dogpiles on the first 1/world_size of the batch) while keeping each + group intact on a single rank so advantage normalization sees all + peer generations. + + ``accelerator.prepare(DataLoader)`` does not handle this correctly + for custom samplers with ``split_batches=False`` (the default): it + leaves the sampler alone and every rank replays identical indices. + This wrapper fixes that by consuming the inner sampler's full + output, chunking it into ``num_generations``-sized groups, and + round-robining whole groups across ranks. + + Intended to be used ONLY when distributed training is active + (``num_replicas > 1``); for single-rank it is a no-op but still + correct. + """ + + def __init__( + self, + inner: Any, + num_generations: int, + rank: int, + num_replicas: int, + ): + if num_generations < 1: + raise ValueError(f"num_generations must be >= 1, got {num_generations}") + if num_replicas < 1: + raise ValueError(f"num_replicas must be >= 1, got {num_replicas}") + if not (0 <= rank < num_replicas): + raise ValueError(f"rank must be in [0, {num_replicas}), got {rank}") + self.inner = inner + self.num_generations = num_generations + self.rank = rank + self.num_replicas = num_replicas + + def __iter__(self): + all_indices = list(self.inner) + if len(all_indices) % self.num_generations != 0: + raise ValueError( + f"inner sampler yielded {len(all_indices)} indices, " + f"not a multiple of num_generations={self.num_generations}" + ) + # Chunk the flat index sequence into groups of num_generations + # consecutive indices. ``RepeatSampler`` guarantees that each + # group contains num_generations copies of the same prompt id. + groups = [ + all_indices[i : i + self.num_generations] + for i in range(0, len(all_indices), self.num_generations) + ] + # Round-robin whole groups across ranks. Round-robin (vs. + # contiguous chunking) preserves approximate shuffled order on + # each rank even when the group count is small relative to the + # world size. + for group in groups[self.rank :: self.num_replicas]: + yield from group + + def __len__(self): + try: + inner_len = len(self.inner) + except TypeError: + # Non-sized inner sampler — we can't know the per-rank + # length without materializing. Return 0 as a hint that the + # DataLoader should fall back to iteration. + return 0 + total_groups = inner_len // self.num_generations + # Ceiling division for the trailing groups that don't divide + # evenly — extra groups go to the first ``total_groups % + # num_replicas`` ranks, matching the round-robin above. + my_groups = ( + total_groups + self.num_replicas - self.rank - 1 + ) // self.num_replicas + return my_groups * self.num_generations + + class DataProducer(ABC): """Abstract base class for online data producers. @@ -556,6 +635,34 @@ def _init_prompt_dataloader(self) -> None: seed=self._seed, ) + # Shard the sampler across distributed ranks so each rank sees + # a disjoint slice of prompts. ``RepeatSampler`` groups each + # prompt with ``num_generations`` consecutive copies — our + # wrapper round-robins WHOLE groups across ranks so all + # generations of a given prompt stay on the same rank (needed + # for GRPO advantage normalization within a group). + # + # Without this, ``accelerator.prepare(dl)`` with the default + # ``split_batches=False`` leaves the custom sampler alone, so + # every rank iterates the identical index sequence and the + # cluster dogpiles on the first 1/world_size of the prompts. + num_replicas = max(1, trainer.accelerator.num_processes) + if num_replicas > 1: + sampler = _GroupShardedSampler( + inner=sampler, + num_generations=self._num_generations, + rank=trainer.accelerator.process_index, + num_replicas=num_replicas, + ) + logger.info( + "[RANK:%d] _GroupShardedSampler active " + "(num_replicas=%d, num_generations=%d, gen_batch=%d)", + trainer.accelerator.process_index, + num_replicas, + self._num_generations, + self._generation_batch_size, + ) + # Use identity collator (same as stock GRPOTrainer) def _identity(x): return x @@ -574,12 +681,11 @@ def _identity(x): rank=trainer.args.process_index, ), ) - self._prompt_dl = trainer.accelerator.prepare(dl) - - # Don't let accelerator track this dataloader - acc_dls = trainer.accelerator._dataloaders - if self._prompt_dl in acc_dls: - acc_dls.remove(self._prompt_dl) + # Skip accelerator.prepare — we're handling per-rank sharding + # ourselves via ``_GroupShardedSampler``. ``prepare()`` would + # otherwise try to wrap the DataLoader with its own sharding + # logic which does not understand our group structure. + self._prompt_dl = dl self._prompt_iter = iter(self._prompt_dl) @@ -1103,11 +1209,22 @@ def _maybe_sync_vllm_weights(self): - vllm_lora_sync: saves adapter to filesystem, vLLM loads natively - PEFT no-merge: computes merged weights as new tensors, NCCL broadcast - Non-PEFT: stock sync_weights via merge_adapter + NCCL + + This is the canonical sync trigger and runs in BOTH async and + synchronous modes from ``_prepare_inputs_with_data_producer`` / + ``_prepare_inputs_legacy_async``. The ``_generate_single_turn`` + patch is a parallel backup for non-data-producer paths (vanilla + GRPO without NeMo Gym), where the data producer is bypassed + entirely and TRL's stock generate-then-sync flow is used instead. """ - if not (self.use_vllm and self.args.async_prefetch): + if not self.use_vllm: return step = self.state.global_step - interval = self.args.vllm_sync_interval + # Default to syncing every step when no interval is configured — + # otherwise ``step % None`` would TypeError, and the previous + # behavior of crashing on the first sync was strictly worse than + # the standard "sync every optimizer step". + interval = self.args.vllm_sync_interval or 1 if step != self._last_synced_step and step % interval == 0: if step == 0: logger.info("Skipping vLLM weight sync at step 0 (no training yet)") @@ -1202,13 +1319,42 @@ def _generate_single_turn(self, prompts, *args, **kwargs): # Permanently replace vllm_generation.sync_weights with our custom # sync to avoid merge_adapter (fails on FP8 / races with training). - # For LoRA sync mode, make it a no-op here since _maybe_sync_vllm_weights - # handles the sync with proper interval tracking. + # + # The design has two modes that have to be threaded carefully: + # + # - Async prefetch ON: BG generation thread can't safely call + # sync_weights mid-rollout (it races with the trainer's optimizer + # step and can corrupt weights). We no-op the stock sync hook and + # drive sync ourselves from ``_maybe_sync_vllm_weights`` after the + # optimizer step on the main thread. + # + # - Async prefetch OFF (synchronous mode): TRL's stock + # ``_generate_single_turn`` calls ``sync_weights`` once per step + # boundary. There's no BG thread to race with, and + # ``_maybe_sync_vllm_weights`` short-circuits with + # ``if not async_prefetch: return``, so we MUST wire the stock + # hook directly to our LoRA sync helper — otherwise nothing ever + # pushes weights to vLLM and the trainer becomes a no-op (vLLM + # keeps serving the base model, every rollout in every group + # produces identical outputs, advantages are zero, optimizer + # step gets skipped, repeat). if not getattr(self, "_patched_sync_weights", False): if self.use_vllm and hasattr(self, "vllm_generation"): if getattr(self.args, "vllm_lora_sync", False): - # No-op: LoRA sync is driven by _maybe_sync_vllm_weights - self.vllm_generation.sync_weights = lambda: None + if getattr(self.args, "async_prefetch", False): + # Async: drive sync from main thread via + # _maybe_sync_vllm_weights instead. + self.vllm_generation.sync_weights = lambda: None + else: + # Sync mode: TRL's _generate_single_turn already + # calls sync_weights once per step boundary. Wire + # it directly to our LoRA filesystem sync helper. + sync_helper = self._sync_lora_adapter + + def _lora_filesystem_sync(): + sync_helper() + + self.vllm_generation.sync_weights = _lora_filesystem_sync self._patched_sync_weights = True else: from accelerate.utils import is_peft_model diff --git a/src/axolotl/integrations/nemo_gym/data_producer.py b/src/axolotl/integrations/nemo_gym/data_producer.py index 64b76d7804..3a9635d154 100644 --- a/src/axolotl/integrations/nemo_gym/data_producer.py +++ b/src/axolotl/integrations/nemo_gym/data_producer.py @@ -110,11 +110,36 @@ def produce( item["agent_ref"] = full_item["agent_ref"] dataset_items.append(item) - # Expand by num_generations (agent produces one rollout per call) - expanded_items = [] - for item in dataset_items: - for _ in range(self._num_generations): - expanded_items.append(item) + # NOTE: do NOT re-expand by num_generations here. + # ``RepeatSampler(mini_repeat_count=num_generations)`` already + # yields ``num_generations`` consecutive copies of each unique + # prompt, so ``inputs`` is a list of ``(unique_prompts_per_rank * + # num_generations)`` items — one entry per rollout. Expanding + # again here would fire ``num_generations^2`` rollouts per + # prompt per rank and make every step dogpile on a handful of + # tasks. + expanded_items = dataset_items + + # Diagnostic: log what this rank is about to fire. + try: + import collections + + iid_counts: collections.Counter[str | None] = collections.Counter() + for it in dataset_items: + iid_counts[ + (it.get("responses_create_params", {}).get("metadata") or {}).get( + "instance_id" + ) + ] += 1 + LOG.info( + "[RANK:%d] produce(): firing %d agent /run calls covering %d unique prompts: %s", + trainer.accelerator.process_index, + len(dataset_items), + len(iid_counts), + list(iid_counts.most_common(5)), + ) + except Exception: + pass # Call NeMo Gym agents loop = asyncio.new_event_loop() @@ -140,6 +165,7 @@ def produce( logprobs_list = [] rewards_list = [] + num_turns_list: list[int] = [] for resp in responses: parsed = _parse_agent_response(resp, eos_token_id) prompt_ids_list.append(parsed["prompt_ids"]) @@ -147,6 +173,7 @@ def produce( env_mask_list.append(parsed["env_mask"]) logprobs_list.append(parsed["logprobs"]) rewards_list.append(parsed["reward"]) + num_turns_list.append(parsed.get("num_turns", 0)) # Pad to tensors prompt_ids = [torch.tensor(ids, device=device) for ids in prompt_ids_list] @@ -179,22 +206,48 @@ def produce( tool_mask = [torch.tensor(m, device=device) for m in env_mask_list] tool_mask = pad(tool_mask, padding_value=1, padding_side="right") - # Inject rewards into inputs so _compute_deferred_scores can use them - # The deferred scoring path calls _calculate_rewards which reads reward_funcs. - # Our passthrough reward_fn reads "env_reward" from kwargs. + # Inject per-rollout reward + num_turns into each input. Since + # ``RepeatSampler`` already yields ``num_generations`` copies of + # each prompt, ``inputs`` has ONE entry per rollout (matching + # ``rewards_list`` 1:1). No per-prompt grouping happens here — + # GRPO advantage normalization is the trainer's job downstream. + assert len(inputs) == len(rewards_list), ( + f"rewards/inputs length mismatch: " + f"{len(rewards_list)} rewards vs {len(inputs)} inputs" + ) for i, inp in enumerate(inputs): - # Each input gets rewards for its num_generations rollouts - start = i * self._num_generations - end = start + self._num_generations - inp["env_reward"] = rewards_list[start:end] - - # Expand inputs to match expanded rollouts (num_generations copies) - expanded_inputs = [] - for inp in inputs: - for g in range(self._num_generations): - expanded_inp = dict(inp) - expanded_inp["env_reward"] = inp["env_reward"][g] - expanded_inputs.append(expanded_inp) + inp["env_reward"] = rewards_list[i] + inp["num_turns"] = num_turns_list[i] + + # One expanded_input per rollout (already correct count because + # inputs has num_generations copies baked in by the sampler). + expanded_inputs = [dict(inp) for inp in inputs] + + # Log rollout-level stats to wandb from rank 0. These are the + # true agent-side metrics (not the tokenized TRL view) — so + # num_turns reflects how many /run iterations each rollout + # actually took before finishing or hitting max_turns. + if is_main and num_turns_list: + try: + import wandb + + if wandb.run is not None: + import statistics as _stats + + nonzero = sum(1 for r in rewards_list if r > 0) + log_payload = { + "rollout/num_turns/mean": float(_stats.mean(num_turns_list)), + "rollout/num_turns/min": float(min(num_turns_list)), + "rollout/num_turns/max": float(max(num_turns_list)), + "rollout/reward/mean": float(_stats.mean(rewards_list)), + "rollout/reward/nonzero_frac": ( + nonzero / len(rewards_list) if rewards_list else 0.0 + ), + "rollout/n_samples": float(len(rewards_list)), + } + wandb.log(log_payload, commit=False) + except Exception as exc: # never let metric logging break training + LOG.warning("rollout wandb log failed: %s", exc) # Decode completions for reward functions completions = trainer.processing_class.batch_decode( diff --git a/src/axolotl/integrations/nemo_gym/plugin.py b/src/axolotl/integrations/nemo_gym/plugin.py index 14de684cfe..b85e344db2 100644 --- a/src/axolotl/integrations/nemo_gym/plugin.py +++ b/src/axolotl/integrations/nemo_gym/plugin.py @@ -19,6 +19,7 @@ from __future__ import annotations import os +from dataclasses import dataclass, field from typing import TYPE_CHECKING, Union from axolotl.integrations.base import BasePlugin @@ -30,6 +31,107 @@ LOG = get_logger(__name__) +# ---- vLLM weight-sync transport probe ------------------------------------ + + +@dataclass +class VLLMWeightSyncCapabilities: + """What weight-sync routes a vLLM server actually exposes. + + Discovered once at ``pre_model_load`` time by fetching the server's + ``/openapi.json``. Drives the transport-selection table below. + """ + + nccl: bool = False # /init_communicator/ + /update_named_param/ + lora_filesystem: bool = False # /v1/load_lora_adapter (vLLM native) + lora_axolotl: bool = False # /set_lora_adapter/ (axolotl serve_lora extension) + http_full: bool = False # /http_update_weights/ (axolotl serve_lora extension) + probed: bool = False + probe_error: str | None = None + routes: list[str] = field(default_factory=list) + + @property + def any_full_param_sync(self) -> bool: + """True if at least one transport can push full-model weights.""" + return self.nccl or self.http_full + + @property + def any_lora_sync(self) -> bool: + """True if at least one transport can push LoRA adapters.""" + return self.lora_filesystem or self.lora_axolotl or self.nccl + + +def probe_vllm_weight_sync( + base_url: str, timeout: float = 5.0 +) -> VLLMWeightSyncCapabilities: + """Detect which weight-sync routes the configured vLLM server exposes. + + Uses the server's FastAPI ``/openapi.json`` — every weight-sync transport + we care about is mounted as a POST route there. Falls back to all-False + on any error so the caller can still decide what to do (typically: raise + a clear error rather than silently no-op). + """ + import requests + + caps = VLLMWeightSyncCapabilities() + try: + r = requests.get(f"{base_url.rstrip('/')}/openapi.json", timeout=timeout) + r.raise_for_status() + spec = r.json() + routes = sorted((spec.get("paths") or {}).keys()) + caps.routes = routes + caps.nccl = "/init_communicator/" in routes and "/update_named_param/" in routes + caps.lora_filesystem = "/v1/load_lora_adapter" in routes + caps.lora_axolotl = "/set_lora_adapter/" in routes + caps.http_full = "/http_update_weights/" in routes + caps.probed = True + except Exception as exc: + caps.probe_error = f"{type(exc).__name__}: {exc}" + LOG.warning( + "NeMo Gym: failed to probe vLLM /openapi.json at %s — %s. " + "Will fall back to LoRA-only behavior.", + base_url, + caps.probe_error, + ) + return caps + + +def select_weight_sync_transport( + caps: VLLMWeightSyncCapabilities, + *, + has_lora: bool, + vllm_lora_sync_pref: bool, +) -> str: + """Pick the right transport for a (server caps, model type) combo. + + Returns one of: ``"lora_filesystem"``, ``"nccl"``, ``"http_full"``, or + ``"none"``. The caller decides what to do with ``"none"`` (typically: + raise an error explaining the misconfiguration). + + Selection table: + LoRA model + lora endpoint + lora-sync pref → lora_filesystem + LoRA model + lora endpoint → lora_filesystem + LoRA model + nccl endpoint → nccl (broadcast merged adapter) + Full model + nccl endpoint → nccl + Full model + http endpoint → http_full + anything else → none + """ + if has_lora: + if (caps.lora_filesystem or caps.lora_axolotl) and vllm_lora_sync_pref: + return "lora_filesystem" + if caps.lora_filesystem or caps.lora_axolotl: + return "lora_filesystem" + if caps.nccl: + return "nccl" + return "none" + # Full-parameter model + if caps.nccl: + return "nccl" + if caps.http_full: + return "http_full" + return "none" + + class NemoGymPlugin(BasePlugin): """Plugin for NVIDIA NeMo Gym integration with Axolotl. @@ -50,37 +152,69 @@ def __init__(self): self._reward_fn = None self._dataset_lookup = None self._agent_servers = {} + self._vllm_caps: VLLMWeightSyncCapabilities | None = None def get_input_args(self): return "axolotl.integrations.nemo_gym.NemoGymArgs" def pre_model_load(self, cfg): - """Apply monkeypatches before trainer creation.""" + """Probe vLLM weight-sync routes and conditionally bypass NCCL init. + + Replaces the previous unconditional ``init_communicator`` monkey-patch + with a probe of the configured vLLM server's ``/openapi.json``. We only + bypass NCCL init when the server we're talking to actually lacks the + ``/init_communicator/`` route (i.e. stock ``vllm serve``); against + TRL/axolotl serve modules that DO expose NCCL routes, we leave the + standard TRL flow alone so full-finetune training can sync weights. + """ if not cfg.nemo_gym_enabled: return - # Always skip NCCL communicator init in NeMo Gym mode. - # NeMo Gym uses its own vLLM server (standard OpenAI API), not the TRL - # colocate/NCCL path. The NCCL init fails with vLLM V1 and standard servers. trl_cfg = getattr(cfg, "trl", None) - if trl_cfg and getattr(trl_cfg, "vllm_mode", "server") == "server": + if not (trl_cfg and getattr(trl_cfg, "vllm_mode", "server") == "server"): + return + + host = getattr(trl_cfg, "vllm_server_host", None) or "127.0.0.1" + port = getattr(trl_cfg, "vllm_server_port", None) or 8000 + base_url = f"http://{host}:{port}" + self._vllm_caps = probe_vllm_weight_sync(base_url) + + if self._vllm_caps.probed: + LOG.info( + "NeMo Gym: vLLM weight-sync probe @ %s — nccl=%s lora_native=%s " + "lora_axolotl=%s http_full=%s", + base_url, + self._vllm_caps.nccl, + self._vllm_caps.lora_filesystem, + self._vllm_caps.lora_axolotl, + self._vllm_caps.http_full, + ) + + # Only bypass NCCL init when the server doesn't speak it. If NCCL is + # available we leave VLLMClient.init_communicator alone so the + # standard TRL sync flow can run for full-parameter training. + if not self._vllm_caps.nccl: self._patch_skip_nccl_init() def _patch_skip_nccl_init(self): """Monkeypatch VLLMClient.init_communicator to no-op. - NeMo Gym uses its own vLLM server (standard OpenAI API or custom LoRA - serve script). The NCCL communicator is not needed and fails with both - vLLM V1 engine and standard OpenAI server mode. + Only called when the configured vLLM server doesn't expose + ``/init_communicator/`` (e.g. stock ``vllm serve``). In that case + TRL's standard ``init_communicator`` would 404 inside trainer + construction; we no-op it so the LoRA filesystem path can install + its own sync in ``post_trainer_create``. """ try: from trl.generation.vllm_client import VLLMClient VLLMClient._original_init_communicator = VLLMClient.init_communicator VLLMClient.init_communicator = lambda self, **kwargs: LOG.info( - "Skipping NCCL init_communicator (LoRA sync mode)" + "Skipping NCCL init_communicator (server has no /init_communicator/)" + ) + LOG.info( + "Patched VLLMClient.init_communicator to no-op (server has no NCCL routes)" ) - LOG.info("Patched VLLMClient.init_communicator to no-op for LoRA sync") except Exception as exc: LOG.warning(f"Failed to patch VLLMClient: {exc}") @@ -234,30 +368,80 @@ def post_trainer_create(self, cfg, trainer): verify_timeout = cfg.nemo_gym_verify_timeout or 30 multi_turn = cfg.nemo_gym_multi_turn or False - # Handle weight sync. NeMo Gym skips NCCL init, so we need to either: - # - Install LoRA sync (when vllm_lora_sync=True) - # - Or no-op sync_weights (when using standard vLLM server) + # Pick a weight-sync transport based on what the configured vLLM + # server actually exposes (see ``pre_model_load`` probe) and what + # kind of model we're training. The selection table is documented + # in ``select_weight_sync_transport``. trl_cfg = getattr(cfg, "trl", None) if hasattr(trainer, "vllm_generation") and trainer.vllm_generation: vllm_gen = trainer.vllm_generation - if trl_cfg and getattr(trl_cfg, "vllm_lora_sync", False): + adapter = getattr(cfg, "adapter", None) + has_lora = adapter in ("lora", "qlora") + vllm_lora_sync_pref = bool( + trl_cfg and getattr(trl_cfg, "vllm_lora_sync", False) + ) + caps = self._vllm_caps or VLLMWeightSyncCapabilities() + transport = select_weight_sync_transport( + caps, + has_lora=has_lora, + vllm_lora_sync_pref=vllm_lora_sync_pref, + ) + + if transport == "lora_filesystem": self._setup_lora_sync(trainer) - # Verify the vLLM server supports runtime LoRA loading self._check_lora_endpoint(vllm_gen) - else: - # No NCCL, no LoRA sync — skip all weight sync paths - vllm_gen.sync_weights = lambda: LOG.debug( - "Weight sync skipped (NeMo Gym mode)" + LOG.info("NeMo Gym weight sync: LoRA filesystem") + elif transport == "nccl": + # Standard TRL NCCL path. We leave ``VLLMClient.init_communicator`` + # alone (pre_model_load only patched it when the probe found no + # NCCL route) so the trainer's normal weight-sync flow runs. + LOG.info( + "NeMo Gym weight sync: NCCL (server exposes /init_communicator/)" ) - type(vllm_gen).sync_weights = lambda self: LOG.debug( - "Weight sync skipped (NeMo Gym mode)" + elif transport == "http_full": + # Full-parameter HTTP sync — implementation lands in step 3. + # For now, fail loudly so users know the path is detected but + # not yet wired up, instead of silently no-oping like before. + raise NotImplementedError( + "NeMo Gym + full fine-tune + HTTP weight sync is detected " + "but the client-side sync helper is not yet implemented " + "(planned). Use `adapter: lora|qlora` for now, or use a " + "vLLM serve module that exposes /init_communicator/ for " + "NCCL sync." ) - # Also patch the async trainer's internal sync method - if hasattr(trainer, "_maybe_sync_vllm_weights"): - trainer._maybe_sync_vllm_weights = lambda: LOG.debug( - "Async weight sync skipped (NeMo Gym mode)" + else: # transport == "none" + # No viable sync path. Build a precise error so the user knows + # exactly what's missing and how to fix it. + if not caps.probed: + msg = ( + "could not probe the vLLM server's " + f"/openapi.json: {caps.probe_error}. " + "Verify that vLLM is reachable at " + f"{getattr(trl_cfg, 'vllm_server_host', '?')}:" + f"{getattr(trl_cfg, 'vllm_server_port', '?')}." ) - LOG.info("Disabled weight sync (NeMo Gym mode, no LoRA sync)") + elif has_lora: + msg = ( + "the vLLM server has neither NCCL routes " + "(/init_communicator/) nor a LoRA-loading route " + "(/v1/load_lora_adapter or /set_lora_adapter/). " + "Restart vLLM with `--enable-lora --max-lora-rank N " + "VLLM_ALLOW_RUNTIME_LORA_UPDATING=1` for the stock " + "server, or use `axolotl vllm-serve` for the " + "NCCL-capable serve module." + ) + else: + msg = ( + "the vLLM server exposes no full-parameter sync route " + "(/init_communicator/ for NCCL or /http_update_weights/ " + "for HTTP). Use `axolotl vllm-serve` (which has both) " + "or set `adapter: lora|qlora`." + ) + raise ValueError( + f"NeMo Gym: no usable weight-sync transport — {msg} Without " + "weight sync the trainer's gradient updates never reach the " + "rollout policy (functionally a no-op trainer)." + ) if multi_turn: self._wire_multi_turn(cfg, trainer, model_name, verify_timeout) diff --git a/src/axolotl/integrations/nemo_gym/server.py b/src/axolotl/integrations/nemo_gym/server.py index 0af9b3b710..bd619569ef 100644 --- a/src/axolotl/integrations/nemo_gym/server.py +++ b/src/axolotl/integrations/nemo_gym/server.py @@ -130,21 +130,41 @@ def start_servers( ) -def get_server_configs(head_port: int = 11000) -> dict: +def get_server_configs(head_port: int = 11000, timeout: float = 30.0) -> dict: """Fetch the global config from the NeMo Gym head server. + Retries up to 3 times with exponential backoff. The default per-attempt + timeout is 30s (raised from the original 5s) because head servers can + be slow to respond when they're concurrently serving rollouts from a + prior training run. A 5s timeout was empirically too tight to survive + a kill-and-relaunch cycle. + Returns: Dict mapping server_name -> server config. """ - response = requests.get( - f"http://127.0.0.1:{head_port}/global_config_dict_yaml", timeout=5 + url = f"http://127.0.0.1:{head_port}/global_config_dict_yaml" + last_exc: Exception | None = None + for attempt in (1, 2, 3): + try: + response = requests.get(url, timeout=timeout) + response.raise_for_status() + result = yaml.safe_load(response.text) + # NeMo Gym head server double-encodes: YAML string inside a YAML string + if isinstance(result, str): + result = yaml.safe_load(result) + return result + except (requests.exceptions.RequestException, OSError) as exc: + last_exc = exc + LOG.warning( + "NeMo Gym head probe attempt %d/3 failed: %s. Retrying...", + attempt, + type(exc).__name__, + ) + if attempt < 3: + time.sleep(2.0 * attempt) + raise RuntimeError( + f"NeMo Gym head server at {url} did not respond after 3 attempts: {last_exc}" ) - response.raise_for_status() - result = yaml.safe_load(response.text) - # NeMo Gym head server double-encodes: YAML string inside a YAML string - if isinstance(result, str): - result = yaml.safe_load(result) - return result def get_agent_servers( diff --git a/src/axolotl/kernels/gemma4_fused_rope.py b/src/axolotl/kernels/gemma4_fused_rope.py index f3b68e603c..f98e9a3de6 100644 --- a/src/axolotl/kernels/gemma4_fused_rope.py +++ b/src/axolotl/kernels/gemma4_fused_rope.py @@ -53,6 +53,7 @@ def _rms_norm_rope_forward_kernel( RSTD_ptr, RSTD_row_stride, n_cols, + n_rot, n_heads, eps, HAS_WEIGHT: tl.constexpr, @@ -60,28 +61,35 @@ def _rms_norm_rope_forward_kernel( ): """ Fused forward: - x_norm = x / rms(x) [* weight] (RMSNorm) - y = x_norm * cos + rotate_half(x_norm) * sin (RoPE) + x_norm = x / rms(x) [* weight] (RMSNorm, full n_cols) + y[..., :n_rot] = rope(x_norm[..., :n_rot]) + y[..., n_rot:] = x_norm[..., n_rot:] (pass-through for partial rotary) - rotate_half swaps first/second halves and negates the first: - rotate_half([a, b]) = [-b, a] + rotate_half swaps first/second halves and negates the first, restricted + to the rotary span [0, n_rot): + rotate_half([a, b]) = [-b, a] where len(a) = len(b) = n_rot/2 + + For the partial-rotary pass-through region we load cos with default 1.0 + and sin with default 0.0 outside [0, n_rot), so the same formula + `Y = X_norm * cos + X_rot_norm * sin` collapses to `Y = X_norm`. cos/sin are indexed by row_idx // n_heads to handle per-head broadcast - (cos/sin have shape (B*S, D) while X has shape (B*S*H, D)). + (cos/sin have shape (B*S, n_rot) while X has shape (B*S*H, n_cols)). """ row_idx = tl.program_id(0).to(tl.int64) - # cos/sin row: divide by n_heads since cos/sin are (B*S, D) + # cos/sin row: divide by n_heads since cos/sin are (B*S, n_rot) cs_row_idx = row_idx // n_heads col_offsets = tl.arange(0, BLOCK_SIZE) mask = col_offsets < n_cols - half_dim = n_cols // 2 + rot_mask_col = col_offsets < n_rot + half_rot = n_rot // 2 # Load input row X_row = tl.load(X_ptr + row_idx * X_row_stride + col_offsets, mask=mask, other=0) X_dtype = X_row.dtype X_fp32 = X_row.to(tl.float32) - # RMSNorm: compute 1/rms + # RMSNorm: compute 1/rms over the full row (rotary + pass-through) mean_sq = tl.sum(X_fp32 * X_fp32, axis=0) / n_cols rstd = rsqrt(mean_sq + eps) tl.store(RSTD_ptr + row_idx * RSTD_row_stride, rstd) @@ -94,33 +102,38 @@ def _rms_norm_rope_forward_kernel( W_row = tl.load(W_ptr + col_offsets, mask=mask, other=0).to(tl.float32) X_norm = X_norm * W_row - # RoPE: load cos/sin (broadcast across heads) + # RoPE: load cos/sin (broadcast across heads). For col >= n_rot we get + # cos=1, sin=0 so the formula leaves X_norm untouched. cos_row = tl.load( - COS_ptr + cs_row_idx * COS_row_stride + col_offsets, mask=mask, other=0 + COS_ptr + cs_row_idx * COS_row_stride + col_offsets, + mask=rot_mask_col, + other=1.0, ).to(tl.float32) sin_row = tl.load( - SIN_ptr + cs_row_idx * SIN_row_stride + col_offsets, mask=mask, other=0 + SIN_ptr + cs_row_idx * SIN_row_stride + col_offsets, + mask=rot_mask_col, + other=0.0, ).to(tl.float32) - # rotate_half: for col < half_dim, take -X_norm[col + half_dim] - # for col >= half_dim, take X_norm[col - half_dim] + # rotate_half within [0, n_rot): + # for col < half_rot: take -X_norm[col + half_rot] + # for col in [half_rot, n_rot): take X_norm[col - half_rot] + # For col >= n_rot the rotation is irrelevant (sin = 0 zeros it out). rot_offsets = tl.where( - col_offsets < half_dim, col_offsets + half_dim, col_offsets - half_dim + col_offsets < half_rot, col_offsets + half_rot, col_offsets - half_rot ) - rot_mask = rot_offsets < n_cols + rot_load_mask = (rot_offsets < n_cols) & rot_mask_col X_rot = tl.load( - X_ptr + row_idx * X_row_stride + rot_offsets, mask=rot_mask & mask, other=0 + X_ptr + row_idx * X_row_stride + rot_offsets, mask=rot_load_mask, other=0 ).to(tl.float32) # Re-normalize the rotated values X_rot_norm = X_rot * rstd if HAS_WEIGHT: - W_rot = tl.load(W_ptr + rot_offsets, mask=rot_mask & mask, other=0).to( - tl.float32 - ) + W_rot = tl.load(W_ptr + rot_offsets, mask=rot_load_mask, other=0).to(tl.float32) X_rot_norm = X_rot_norm * W_rot # Negate the first half (rotate_half negates x2, which becomes the first half) - sign = tl.where(col_offsets < half_dim, -1.0, 1.0) + sign = tl.where(col_offsets < half_rot, -1.0, 1.0) X_rot_norm = X_rot_norm * sign # Final RoPE: y = x_norm * cos + rotate_half(x_norm) * sin @@ -153,13 +166,21 @@ def _rms_norm_rope_backward_kernel( dW_row_stride, n_rows, n_cols, + n_rot, n_heads, rows_per_program, HAS_WEIGHT: tl.constexpr, BLOCK_SIZE: tl.constexpr, ): """ - Backward for Y = RoPE(RMSNorm(X, W)) + Backward for Y = RoPE(RMSNorm(X, W)) with optional partial rotary + (`n_rot <= n_cols`). + + For col < n_rot the standard RoPE adjoint applies. For col >= n_rot the + output is just the normalized row, so dN[col] = dY[col] (achieved by + loading cos with default 1.0 and forcing the rotate-half contribution + to zero outside the rotary span). + cos/sin indexed by row_idx // n_heads for per-head broadcast. """ row_block_id = tl.program_id(0).to(tl.int64) @@ -167,7 +188,8 @@ def _rms_norm_rope_backward_kernel( row_end = min((row_block_id + 1) * rows_per_program, n_rows) col_offsets = tl.arange(0, BLOCK_SIZE) mask = col_offsets < n_cols - half_dim = n_cols // 2 + rot_mask_col = col_offsets < n_rot + half_rot = n_rot // 2 dW_acc = tl.zeros((BLOCK_SIZE,), dtype=tl.float32) @@ -186,33 +208,37 @@ def _rms_norm_rope_backward_kernel( rstd = tl.load(RSTD_ptr + row_idx * RSTD_row_stride) cos_row = tl.load( - COS_ptr + cs_row_idx * COS_row_stride + col_offsets, mask=mask, other=0 + COS_ptr + cs_row_idx * COS_row_stride + col_offsets, + mask=rot_mask_col, + other=1.0, ).to(tl.float32) - # dN = dY * cos + rotate_half^T(dY * sin) + # dN = dY * cos + rotate_half^T(dY * sin) (within the rotary span) # rotate_half^T([a, b]) = [b, -a] (adjoint of rotate_half) # - # Compute rotate_half_transpose(dY * sin) by loading dY and sin at - # rotated offsets directly: dY[rot] * sin[rot] * adj_sign - # This is equivalent to rotating (dY * sin) because the rotation - # just permutes which elements are multiplied. + # For col >= n_rot the formula must collapse to dN = dY (since the + # forward is just a pass-through). cos defaults to 1.0 above; the + # rotate-half contribution is masked to zero below. rot_offsets = tl.where( - col_offsets < half_dim, col_offsets + half_dim, col_offsets - half_dim + col_offsets < half_rot, col_offsets + half_rot, col_offsets - half_rot ) - rot_mask = rot_offsets < n_cols + rot_load_mask = (rot_offsets < n_cols) & rot_mask_col dY_rot = tl.load( dY_ptr + row_idx * dY_row_stride + rot_offsets, - mask=rot_mask & mask, + mask=rot_load_mask, other=0, ).to(tl.float32) sin_rot = tl.load( SIN_ptr + cs_row_idx * SIN_row_stride + rot_offsets, - mask=rot_mask & mask, + mask=rot_load_mask, other=0, ).to(tl.float32) - adj_sign = tl.where(col_offsets < half_dim, 1.0, -1.0) - dN = dY_row * cos_row + dY_rot * sin_rot * adj_sign + adj_sign = tl.where(col_offsets < half_rot, 1.0, -1.0) + rotate_term = dY_rot * sin_rot * adj_sign + # Zero out rotate-half contribution outside the rotary span. + rotate_term = tl.where(rot_mask_col, rotate_term, 0.0) + dN = dY_row * cos_row + rotate_term # Pre-weight normalized: n = rstd * x n = X_row * rstd @@ -241,15 +267,17 @@ def _rms_norm_rope_backward_kernel( ) -def rms_norm_rope_forward(X, W, cos, sin, eps, n_heads): +def rms_norm_rope_forward(X, W, cos, sin, eps, n_heads, n_rot): """ Args: X: (B*S*H, head_dim) — contiguous, flattened from (B, S, H, D) W: (head_dim,) or None — RMSNorm weight - cos: (B*S, head_dim) — position embeddings (broadcast across heads) - sin: (B*S, head_dim) — position embeddings (broadcast across heads) + cos: (B*S, n_rot) — position embeddings (broadcast across heads) + sin: (B*S, n_rot) — position embeddings (broadcast across heads) eps: float n_heads: int — number of attention heads (for cos/sin indexing) + n_rot: int — rotary dim (== head_dim for full rotary, < head_dim for + partial rotary). Must be even and ``<= head_dim``. Returns: Y, X_saved, RSTD, BLOCK_SIZE, num_warps """ @@ -273,6 +301,7 @@ def rms_norm_rope_forward(X, W, cos, sin, eps, n_heads): RSTD, RSTD.stride(0), n_cols, + n_rot, n_heads, eps, HAS_WEIGHT=has_weight, @@ -282,7 +311,9 @@ def rms_norm_rope_forward(X, W, cos, sin, eps, n_heads): return Y, X, RSTD, BLOCK_SIZE, num_warps -def rms_norm_rope_backward(dY, X, W, cos, sin, RSTD, n_heads, BLOCK_SIZE, num_warps): +def rms_norm_rope_backward( + dY, X, W, cos, sin, RSTD, n_heads, n_rot, BLOCK_SIZE, num_warps +): n_rows, n_cols = dY.shape has_weight = W is not None @@ -315,6 +346,7 @@ def rms_norm_rope_backward(dY, X, W, cos, sin, RSTD, n_heads, BLOCK_SIZE, num_wa _dW.stride(0), n_rows, n_cols, + n_rot, n_heads, rows_per_program, HAS_WEIGHT=has_weight, @@ -329,13 +361,14 @@ def rms_norm_rope_backward(dY, X, W, cos, sin, RSTD, n_heads, BLOCK_SIZE, num_wa class FusedRMSNormRoPEFunction(torch.autograd.Function): @staticmethod @ensure_contiguous - def forward(ctx, X, W, cos, sin, eps, n_heads): + def forward(ctx, X, W, cos, sin, eps, n_heads, n_rot): """ - X: (B*S*H, head_dim) - W: (head_dim,) or None - cos: (B*S, head_dim) — broadcast across heads - sin: (B*S, head_dim) — broadcast across heads + X: (B*S*H, head_dim) + W: (head_dim,) or None + cos: (B*S, n_rot) — broadcast across heads + sin: (B*S, n_rot) — broadcast across heads n_heads: int + n_rot: int — rotary dim (<= head_dim) """ Y, X_saved, RSTD, BLOCK_SIZE, num_warps = rms_norm_rope_forward( X, @@ -344,11 +377,13 @@ def forward(ctx, X, W, cos, sin, eps, n_heads): sin, eps, n_heads, + n_rot, ) ctx.eps = eps ctx.BLOCK_SIZE = BLOCK_SIZE ctx.num_warps = num_warps ctx.n_heads = n_heads + ctx.n_rot = n_rot ctx.has_weight = W is not None ctx.save_for_backward(X_saved, W, cos, sin, RSTD) return Y @@ -365,21 +400,26 @@ def backward(ctx, dY): sin, RSTD, ctx.n_heads, + ctx.n_rot, ctx.BLOCK_SIZE, ctx.num_warps, ) - return dX, dW, None, None, None, None + return dX, dW, None, None, None, None, None def fused_rms_norm_rope(x, weight, cos, sin, eps=1e-6): """ - Apply fused RMSNorm + RoPE. + Apply fused RMSNorm + (partial) RoPE. Args: x: (batch, seq_len, num_heads, head_dim) — after projection + view weight: (head_dim,) — RMSNorm weight, or None for no-scale norm - cos: (batch, seq_len, head_dim) — from RotaryEmbedding - sin: (batch, seq_len, head_dim) — from RotaryEmbedding + cos: (batch, seq_len, n_rot) — from RotaryEmbedding. ``n_rot`` + must be even and ``<= head_dim``. When ``n_rot < head_dim`` + the trailing ``head_dim - n_rot`` columns are RMSNorm-only + (partial-rotary pass-through), matching stock Gemma 4 with + ``partial_rotary_factor < 1.0``. + sin: (batch, seq_len, n_rot) — same shape as ``cos`` eps: float — RMSNorm epsilon Returns: @@ -387,14 +427,38 @@ def fused_rms_norm_rope(x, weight, cos, sin, eps=1e-6): """ shape = x.shape # (B, S, H, D) B, S, H, D = shape + n_rot = cos.shape[-1] + if sin.shape[-1] != n_rot: + raise ValueError( + f"cos and sin must have the same last dim, got cos={cos.shape[-1]} " + f"sin={sin.shape[-1]}" + ) + if n_rot > D: + raise ValueError(f"rotary dim ({n_rot}) cannot exceed head_dim ({D})") + if n_rot % 2 != 0: + raise ValueError(f"rotary dim must be even, got {n_rot}") + # Flatten to 2D: (B*S*H, D) x_flat = x.reshape(-1, D).contiguous() - # Flatten cos/sin to (B*S, D) — the kernel will handle per-head broadcast - # by dividing the row_idx by H to get the cos/sin row - cos_flat = cos.reshape(B * S, D).contiguous() - sin_flat = sin.reshape(B * S, D).contiguous() - - y_flat = FusedRMSNormRoPEFunction.apply(x_flat, weight, cos_flat, sin_flat, eps, H) + # cos/sin may broadcast over the batch dim (e.g. (1, S, n_rot) when + # all sequences share the same rotary positions). The kernel needs a + # dense (B*S, n_rot) buffer so that row_idx // n_heads maps cleanly + # onto a single (b, s) pair, so expand-then-contiguous to materialize + # the per-batch broadcast. Expand is a no-op when B == cos.shape[0]. + if cos.shape[0] != B: + if cos.shape[0] != 1: + raise ValueError( + f"cos/sin batch dim ({cos.shape[0]}) must be 1 or equal " + f"to x batch dim ({B})" + ) + cos = cos.expand(B, S, n_rot) + sin = sin.expand(B, S, n_rot) + cos_flat = cos.reshape(B * S, n_rot).contiguous() + sin_flat = sin.reshape(B * S, n_rot).contiguous() + + y_flat = FusedRMSNormRoPEFunction.apply( + x_flat, weight, cos_flat, sin_flat, eps, H, n_rot + ) return y_flat.view(shape) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 8739655162..01d9997d7d 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -156,6 +156,14 @@ def apply_post_model_build_patches(self, model: PreTrainedModel): # which would clobber any earlier fix. self._fix_nemotron_h_conversion_mapping() + # Gemma 4 hybrid attention runs here in post-build (NOT post-load): + # the per-layer ``self_attn.config._attn_implementation="sdpa"`` + # override needs to walk the raw model tree, which is broken by + # the post-load PEFT wrapping. The accompanying + # ``patch_gemma4_hybrid_mask`` monkey-patch is module-level and + # installation-time-independent, so both halves of the fix live + # cleanly in the same call even though one is instance-scoped + # and the other is module-scoped. self._apply_gemma_hybrid_attention(model) self._finalize_moe_expert_quantization(model) @@ -172,12 +180,23 @@ def _apply_gemma_hybrid_attention(self, model: PreTrainedModel): which exceeds flash attention's supported size. This patch loads the model with flash_attention_2 for the sliding window layers (head_dim=256), then gives each global layer a shallow-copied config with _attn_implementation="sdpa". + + We also install :func:`axolotl.monkeypatch.gemma4_hybrid_mask.patch_gemma4_hybrid_mask` + which fixes the corresponding mask construction inside + ``Gemma4TextModel.forward``. Without it, the per-layer SDPA config + override is not enough — the forward still builds a 2D FA2-format mask + at the model level and the SDPA layers crash at long context lengths + with ``RuntimeError: The expanded size of the tensor ... must match``. """ if not self.cfg.gemma4_hybrid_attn_impl: return import copy + from axolotl.monkeypatch.gemma4_hybrid_mask import patch_gemma4_hybrid_mask + + patch_gemma4_hybrid_mask() + # Navigate to the module that has 'layers' - varies by model structure: # Gemma4ForConditionalGeneration -> .model (Gemma4Model) -> .language_model (Gemma4TextModel) -> .layers # Gemma4ForCausalLM -> .model (Gemma4TextModel) -> .layers @@ -391,6 +410,14 @@ def _apply_model_specific_patches(self): patch_qwen3_5_vlm_flash_attention() if self.cfg.model_config_type in ("gemma4", "gemma4_text"): + # The fused attn path is now compatible with + # ``gemma4_hybrid_attn_impl``: the kernel handles partial + # rotary (cos.shape[-1] < head_dim) and the fused forward + # mirrors the current ``Gemma4TextAttention.forward`` API + # for shared kv (read from / write to + # ``past_key_values.shared_layers``). See + # ``src/axolotl/kernels/GEMMA4_FUSED_ROPE_HYBRID_ATTN_BUG.md`` + # for the history. from axolotl.monkeypatch.models.gemma4.fused_attn import ( patch_gemma4_fused_attn, ) diff --git a/src/axolotl/monkeypatch/gemma4_hybrid_mask.py b/src/axolotl/monkeypatch/gemma4_hybrid_mask.py new file mode 100644 index 0000000000..17b8cf053d --- /dev/null +++ b/src/axolotl/monkeypatch/gemma4_hybrid_mask.py @@ -0,0 +1,115 @@ +"""Hybrid attention mask fix for Gemma 4. + +Gemma 4 has full-attention (global) layers with ``head_dim=512`` which +exceeds flash-attention-2's supported size. Axolotl's hybrid-attention +patch in ``patch_manager._apply_gemma_hybrid_attention`` works around +this by forcing ``_attn_implementation="sdpa"`` on each global layer's +``self_attn.config``, leaving sliding-window layers on FA2. + +The per-layer config override alone is insufficient, however: +``Gemma4TextModel.forward`` builds a single ``causal_mask_mapping`` dict +using the **model-level** config and passes the mapped mask to each +decoder layer. With FA2 still set at the model level, the ``full_attention`` +entry in that mapping is a 2D mask (FA2 format), but SDPA needs a 4D mask. +The global layers then fail with:: + + RuntimeError: The expanded size of the tensor (S) must match the existing + size (B) at non-singleton dimension 2. Target sizes: [B, H, S, S]. Tensor + sizes: [B, S] + +...when the sequence length grows past roughly 7k tokens. + +This module fixes the symptom by monkey-patching ``create_causal_mask`` in +``transformers.models.gemma4.modeling_gemma4``'s module namespace — NOT +the original in ``masking_utils``. The wrapper forces +``_attn_implementation="sdpa"`` on a shallow-copied config before calling +through, so the ``full_attention`` mask built inside ``Gemma4TextModel.forward`` +is always 4D/SDPA-compatible. ``create_sliding_window_causal_mask`` is left +alone, so sliding-window layers continue to receive FA2-format masks. + +The patch is idempotent. Install once per process, before any Gemma 4 +forward pass runs. +""" + +from __future__ import annotations + +import copy +from typing import Any + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +_PATCH_APPLIED = False + + +def patch_gemma4_hybrid_mask() -> bool: + """Install the Gemma 4 hybrid-attention mask fix. + + Returns ``True`` if the patch was installed (or was already installed), + ``False`` if the target module could not be imported (e.g. transformers + version predates Gemma 4) — in which case nothing is done and the + caller can continue unaffected. + """ + global _PATCH_APPLIED + if _PATCH_APPLIED: + return True + + try: + from transformers.models.gemma4 import modeling_gemma4 + except ImportError: + LOG.debug( + "gemma4_hybrid_mask: transformers.models.gemma4 not importable, " + "skipping. This is fine for non-Gemma4 training." + ) + return False + + if not hasattr(modeling_gemma4, "create_causal_mask"): + LOG.warning( + "gemma4_hybrid_mask: modeling_gemma4 has no 'create_causal_mask' " + "binding, skipping. Transformers API may have changed." + ) + return False + + original = modeling_gemma4.create_causal_mask + + def hybrid_create_causal_mask(config: Any, *args: Any, **kwargs: Any): + """Wrapper that forces SDPA format for the full-attention mask. + + The global layers were patched to SDPA by + ``_apply_gemma_hybrid_attention``, so their mask must be 4D. The + original ``create_causal_mask`` dispatches on + ``config._attn_implementation``; we shadow that with a local + override. + """ + sdpa_config = copy.copy(config) + sdpa_config._attn_implementation = "sdpa" + return original(sdpa_config, *args, **kwargs) + + # Preserve the original reference on the wrapper for tests / teardown. + hybrid_create_causal_mask._axolotl_original = original # type: ignore[attr-defined] + + modeling_gemma4.create_causal_mask = hybrid_create_causal_mask + _PATCH_APPLIED = True + LOG.info( + "gemma4_hybrid_mask: patched modeling_gemma4.create_causal_mask to " + "force SDPA-format masks for full-attention layers" + ) + return True + + +def unpatch_gemma4_hybrid_mask() -> None: + """Restore the original ``create_causal_mask``. Useful for tests.""" + global _PATCH_APPLIED + if not _PATCH_APPLIED: + return + try: + from transformers.models.gemma4 import modeling_gemma4 + except ImportError: + _PATCH_APPLIED = False + return + current = modeling_gemma4.create_causal_mask + original = getattr(current, "_axolotl_original", None) + if original is not None: + modeling_gemma4.create_causal_mask = original + _PATCH_APPLIED = False diff --git a/src/axolotl/monkeypatch/tiled_mlp/patch.py b/src/axolotl/monkeypatch/tiled_mlp/patch.py index 65885396bc..23f48a1016 100644 --- a/src/axolotl/monkeypatch/tiled_mlp/patch.py +++ b/src/axolotl/monkeypatch/tiled_mlp/patch.py @@ -24,7 +24,15 @@ def patch_tiled_mlp(model_type, use_original_mlp=True, cfg_num_shards=None): module_path = f"transformers.models.{model_type}.modeling_{model_type}" model_cls_prefix, _ = get_causal_lm_model_cls_prefix(model_type) module = __import__(module_path, fromlist=[f"{model_cls_prefix}MLP"]) - mlp_cls = getattr(module, f"{model_cls_prefix}MLP") + # Some multimodal wrappers (e.g. Gemma 4) name the MLP class + # ``{prefix}TextMLP`` rather than ``{prefix}MLP`` because the + # language-side module is separated from the vision tower. Try + # both names before giving up. + mlp_cls = getattr( + module, + f"{model_cls_prefix}MLP", + None, + ) or getattr(module, f"{model_cls_prefix}TextMLP") if use_original_mlp: mlp_forward = mlp_cls.forward diff --git a/src/axolotl/scripts/vllm_serve_lora.py b/src/axolotl/scripts/vllm_serve_lora.py index 344c4327f0..ca2f743fcf 100644 --- a/src/axolotl/scripts/vllm_serve_lora.py +++ b/src/axolotl/scripts/vllm_serve_lora.py @@ -320,6 +320,15 @@ async def access_log_middleware(request, call_next): # --- Active LoRA state (shared across endpoints via closure) --- active_lora: dict = {"request": None} + # Serializes access to the worker pipe. The underlying + # multiprocessing.Connection is a single full-duplex stream shared + # across all HTTP handlers; concurrent requests interleave bytes on + # the wire and corrupt the pickle framing (seen as + # ``UnpicklingError: pickle data was truncated``). Any endpoint that + # does ``conn.send(...); conn.recv()`` MUST hold this lock across + # the round-trip so only one inflight call at a time per pipe. + worker_pipe_lock = asyncio.Lock() + # ------------------------------------------------------------------ # LoRA-specific endpoints # ------------------------------------------------------------------ @@ -631,6 +640,150 @@ async def openai_chat_completions(request_body: dict): }, } + @app.post("/v1/completions") + async def openai_completions(request_body: dict): + """OpenAI-compatible text-completions endpoint. + + Accepts either a string ``prompt`` or a list-of-int + ``prompt_token_ids`` (as the text-completions spec allows). Routes + to the internal vLLM generate method with the active LoRA adapter + and returns an OpenAI /v1/completions-shaped response including + per-choice ``prompt_token_ids``, ``generation_token_ids``, and + ``generation_log_probs`` for NeMo Gym agents that need raw + tokens + logprobs. + """ + import uuid + + prompt_raw = request_body.get("prompt") + temperature = request_body.get("temperature", 1.0) + max_tokens = request_body.get("max_tokens", 512) + top_p = request_body.get("top_p", 1.0) + n = request_body.get("n", 1) + logprobs = request_body.get("logprobs") or 0 + stop_token_ids = request_body.get("stop_token_ids") or None + + # Accept either a string or a list[int] token id prompt. Lists + # must contain ints only (raise on lists of strings so callers get + # a clear error). Also accept [[int, int, ...]] nesting for the + # rare case callers pass a single-prompt batch. + if ( + isinstance(prompt_raw, list) + and prompt_raw + and isinstance(prompt_raw[0], list) + ): + prompt_raw = prompt_raw[0] + + prompt_dict: dict[str, Any] = {} + if isinstance(prompt_raw, list): + prompt_dict = {"prompt_token_ids": prompt_raw} + elif isinstance(prompt_raw, str): + prompt_dict = {"prompt": prompt_raw} + else: + return { + "error": { + "message": ("prompt must be a string or a list of token ids"), + "type": "invalid_request", + } + } + + generation_kwargs: dict[str, Any] = { + "n": n, + "temperature": temperature, + "top_p": top_p, + "max_tokens": max_tokens, + "logprobs": logprobs, + } + if stop_token_ids: + generation_kwargs["stop_token_ids"] = stop_token_ids + sampling_params = SamplingParams( + **{k: v for k, v in generation_kwargs.items() if v is not None} + ) + + chunked = chunk_list([prompt_dict], script_args.data_parallel_size) + + # Hold the pipe lock across send+recv — concurrent requests would + # otherwise interleave pickle frames on the worker connection. + async with worker_pipe_lock: + for conn, chunk in zip(connections, chunked, strict=True): + if not chunk: + chunk = [{"prompt": ""}] + kwargs = { + "prompts": chunk, + "sampling_params": sampling_params, + "lora_request": active_lora["request"], + } + conn.send({"type": "call", "method": "generate", "kwargs": kwargs}) + + loop = asyncio.get_running_loop() + all_outputs = await asyncio.gather( + *(loop.run_in_executor(None, safe_recv, conn) for conn in connections) + ) + + all_outputs = [o for o, c in zip(all_outputs, chunked, strict=True) if c] + for o in all_outputs: + if isinstance(o, dict) and "error" in o: + raise RuntimeError(f"vLLM worker error: {o['error']}") + all_outputs = list(chain.from_iterable(all_outputs)) + + if not all_outputs: + return {"choices": [], "model": script_args.model} + + choices = [] + for i, output in enumerate(all_outputs): + for j, out in enumerate(output.outputs): + text = out.text + # OpenAI-style `logprobs` block for text-completions: + # { "tokens": [...], "token_logprobs": [...] } + lp_block = None + if out.logprobs: + tokens_str: list[str] = [] + token_lps: list[float] = [] + for step in out.logprobs: + chosen = next(iter(step.values())) + tokens_str.append(getattr(chosen, "decoded_token", "") or "") + token_lps.append(float(chosen.logprob)) + lp_block = { + "tokens": tokens_str, + "token_logprobs": token_lps, + } + + choice = { + "index": i * n + j, + "text": text, + "finish_reason": "stop" + if out.finish_reason == "stop" + else "length", + "logprobs": lp_block, + # NeMo-Gym / retrace agent extras — preserved on the + # choice so callers with raw-token pipelines don't + # have to re-tokenize. + "prompt_token_ids": output.prompt_token_ids, + "generation_token_ids": list(out.token_ids), + "generation_log_probs": ( + [float(next(iter(lp.values())).logprob) for lp in out.logprobs] + if out.logprobs + else [] + ), + } + choices.append(choice) + + prompt_tokens = len(all_outputs[0].prompt_token_ids) if all_outputs else 0 + completion_tokens = sum( + len(out.token_ids) for o in all_outputs for out in o.outputs + ) + + return { + "id": f"cmpl-{uuid.uuid4().hex[:8]}", + "object": "text_completion", + "model": script_args.model, + "choices": choices, + "usage": { + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": prompt_tokens + completion_tokens, + }, + } + # --- Weight sync endpoints (legacy fallback, same as TRL) --- @app.post("/init_communicator/") diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 1780a9cc83..484a1fb47d 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -760,6 +760,88 @@ def check_gdpo(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_grpo_batch_size_divisibility(cls, data): + """Surface GRPO batch-shape mismatches at config-parse time. + + TRL's GRPOTrainer requires that the per-step generation batch size be + evenly divisible by ``num_generations`` so that every prompt can be + replicated exactly ``num_generations`` times. The runtime check inside + ``GRPOTrainer.__init__`` only fires after the model has been loaded — + too late and too cryptic for the user. We replicate the check here so + the failure is immediate and actionable. + + Also enforces: + - ``num_generations >= 2`` (group-relative advantage needs variance) + - ``effective_gbs >= num_generations * world_size`` when capabilities + indicate multiple ranks (each rank needs at least one full group) + """ + if data.get("rl") != "grpo": + return data + + trl_cfg = data.get("trl") or {} + num_gen = trl_cfg.get("num_generations") + if num_gen is None: + # TRL's own default is 8 — but if the user didn't set it, we + # don't have enough info to validate anything. Let TRL's own + # init handle the default-vs-batch interaction. + return data + if num_gen < 2: + raise ValueError( + f"GRPO requires `trl.num_generations >= 2` (got {num_gen}). " + "With num_generations=1, every group has zero advantage and " + "the policy never updates." + ) + + explicit_gbs = trl_cfg.get("generation_batch_size") + if explicit_gbs is not None: + effective_gbs = int(explicit_gbs) + gbs_source = "trl.generation_batch_size" + else: + mb = data.get("micro_batch_size") or 1 + ga = data.get("gradient_accumulation_steps") or 1 + effective_gbs = int(mb) * int(ga) + gbs_source = f"micro_batch_size ({mb}) * gradient_accumulation_steps ({ga})" + + if effective_gbs % num_gen != 0: + # Suggest the smallest GA bump that fixes it for the common case + # where the user hasn't set generation_batch_size explicitly. + hint = "" + if explicit_gbs is None: + from math import gcd + + mb_val = int(data.get("micro_batch_size") or 1) + # smallest GA such that mb*GA is a multiple of num_gen + lcm = num_gen * mb_val // gcd(num_gen, mb_val) + suggested_ga = lcm // mb_val + hint = ( + f" Smallest fix: set `gradient_accumulation_steps: " + f"{suggested_ga}` (so micro_batch_size * GA = " + f"{mb_val * suggested_ga} is a multiple of {num_gen})." + ) + raise ValueError( + f"GRPO: generation batch size must be divisible by " + f"`trl.num_generations`. Got effective_gbs={effective_gbs} " + f"(from {gbs_source}) and num_generations={num_gen}.{hint}" + ) + + # Multi-rank check: each rank must receive at least one full group + # per step. Without `capabilities` populated yet (mode='before'), we + # fall back to user-set distributed fields. + world_size = ( + (data.get("capabilities") or {}).get("n_gpu") or data.get("world_size") or 1 + ) + if world_size and world_size > 1 and effective_gbs < num_gen * world_size: + raise ValueError( + f"GRPO with world_size={world_size} requires effective_gbs " + f">= num_generations * world_size = {num_gen * world_size}, " + f"got {effective_gbs}. Increase gradient_accumulation_steps " + f"or micro_batch_size." + ) + + return data + class OptimizationValidationMixin: """Validation methods related to optimization and performance.""" diff --git a/tests/core/test_async_grpo.py b/tests/core/test_async_grpo.py index 14c38df294..3a4c188bcc 100644 --- a/tests/core/test_async_grpo.py +++ b/tests/core/test_async_grpo.py @@ -216,5 +216,197 @@ def test_patch_restored_on_error(self): self.assertIs(_trainer_module.validate_quantization_for_training, original) +class TestVllmLoraSyncPatch(unittest.TestCase): + """The ``_generate_single_turn`` patch wires sync_weights to the right place. + + These tests exercise the patch-installation branch in isolation. They build + a stub trainer with just enough attributes to look like + ``AsyncGRPOTrainer`` for the duration of the relevant code path. + + Background — there are two correct behaviors and we historically had a bug + where both modes used the same one: + + - Async prefetch ON: the BG generation thread can't safely call + sync_weights mid-rollout. We no-op the stock hook and drive sync from + the main thread via ``_maybe_sync_vllm_weights``. + - Async prefetch OFF: TRL's stock ``_generate_single_turn`` already + calls ``sync_weights`` once per step boundary on the main thread. We + wire that hook directly to ``_sync_lora_adapter`` because + ``_maybe_sync_vllm_weights`` short-circuits when async is off. + + Before the fix, both modes installed ``lambda: None``, so sync mode never + pushed any LoRA adapter to vLLM and the trainer was a no-op. + """ + + @staticmethod + def _make_stub_trainer(*, vllm_lora_sync, async_prefetch): + from axolotl.core.trainers.grpo.async_trainer import ( + AsyncGRPOTrainer, + ) + + class FakeArgs: + pass + + args = FakeArgs() + args.vllm_lora_sync = vllm_lora_sync + args.async_prefetch = async_prefetch + + class FakeVllmGen: + sync_weights = staticmethod(lambda: None) + model = MagicMock() + + # Use object.__new__ so we don't run __init__ (which needs a real + # model, dataset, etc.). We only need the `_generate_single_turn` + # method's patch branch to run, so we set up the minimum state. + trainer = object.__new__(AsyncGRPOTrainer) + trainer.args = args + trainer.use_vllm = True + trainer.vllm_generation = FakeVllmGen() + trainer._patched_sync_weights = False + # Spy on _sync_lora_adapter so we can assert it's the function the + # hook delegates to in sync mode. + trainer._sync_lora_adapter = MagicMock(name="_sync_lora_adapter_spy") + trainer._sync_peft_weights_no_merge = MagicMock( + name="_sync_peft_weights_no_merge_spy" + ) + return trainer + + @staticmethod + def _run_patch_branch(trainer): + """Execute just the sync_weights-patching branch in isolation. + + We can't easily call the real ``_generate_single_turn`` because it + does a full vLLM generate. Instead we copy the exact branch out of + the source so the test verifies the same logic the trainer runs. + """ + if not getattr(trainer, "_patched_sync_weights", False): + if trainer.use_vllm and hasattr(trainer, "vllm_generation"): + if getattr(trainer.args, "vllm_lora_sync", False): + if getattr(trainer.args, "async_prefetch", False): + trainer.vllm_generation.sync_weights = lambda: None + else: + sync_helper = trainer._sync_lora_adapter + + def _lora_filesystem_sync(): + sync_helper() + + trainer.vllm_generation.sync_weights = _lora_filesystem_sync + trainer._patched_sync_weights = True + + def test_sync_mode_with_lora_sync_wires_to_sync_lora_adapter(self): + trainer = self._make_stub_trainer(vllm_lora_sync=True, async_prefetch=False) + self._run_patch_branch(trainer) + + assert trainer._patched_sync_weights is True + # Trigger the patched hook — it must call _sync_lora_adapter. + trainer.vllm_generation.sync_weights() + trainer._sync_lora_adapter.assert_called_once() + + def test_async_mode_with_lora_sync_installs_noop_hook(self): + trainer = self._make_stub_trainer(vllm_lora_sync=True, async_prefetch=True) + self._run_patch_branch(trainer) + + assert trainer._patched_sync_weights is True + # Hook must be a no-op so BG-thread generation doesn't fight the + # main-thread optimizer step over the model weights. + trainer.vllm_generation.sync_weights() + trainer._sync_lora_adapter.assert_not_called() + + def test_sync_mode_with_lora_sync_does_not_call_during_install(self): + """Installing the patch should not pre-emptively sync.""" + trainer = self._make_stub_trainer(vllm_lora_sync=True, async_prefetch=False) + self._run_patch_branch(trainer) + # _sync_lora_adapter should only be called when the patched hook + # itself is invoked (e.g., from TRL's _generate_single_turn). + trainer._sync_lora_adapter.assert_not_called() + + def test_patch_is_idempotent(self): + trainer = self._make_stub_trainer(vllm_lora_sync=True, async_prefetch=False) + self._run_patch_branch(trainer) + first_hook = trainer.vllm_generation.sync_weights + # Second call must not re-patch (otherwise we'd lose the original). + self._run_patch_branch(trainer) + assert trainer.vllm_generation.sync_weights is first_hook + + +class TestMaybeSyncVllmWeightsIntervalDefault(unittest.TestCase): + """``_maybe_sync_vllm_weights`` must not crash when interval is unset. + + Before the fix, ``step % self.args.vllm_sync_interval`` would TypeError + on the very first call when ``vllm_sync_interval`` was ``None`` (which + is the default for any config that doesn't explicitly set it). We now + fall back to interval=1 so unset means "sync every step", matching the + behavior of TRL's own ``_generate_single_turn``. + """ + + @staticmethod + def _make_stub_trainer(interval, async_prefetch): + from axolotl.core.trainers.grpo.async_trainer import ( + AsyncGRPOTrainer, + ) + + class FakeArgs: + pass + + args = FakeArgs() + args.async_prefetch = async_prefetch + args.vllm_sync_interval = interval + args.vllm_lora_sync = True + + class FakeState: + global_step = 1 + + trainer = object.__new__(AsyncGRPOTrainer) + trainer.args = args + trainer.use_vllm = True + trainer.state = FakeState() + trainer._last_synced_step = 0 + trainer._sync_lora_adapter = MagicMock(name="sync_spy") + return trainer + + def test_interval_none_in_async_mode_does_not_crash(self): + trainer = self._make_stub_trainer(interval=None, async_prefetch=True) + from axolotl.core.trainers.grpo.async_trainer import ( + AsyncGRPOTrainer, + ) + + # Should not raise TypeError — defaults to every-step sync + AsyncGRPOTrainer._maybe_sync_vllm_weights(trainer) + trainer._sync_lora_adapter.assert_called_once() + + def test_sync_mode_drives_sync(self): + """Sync mode must fire ``_sync_lora_adapter`` from ``_maybe_sync_vllm_weights``. + + The previous behavior (early return when ``not async_prefetch``) + assumed TRL's stock ``_generate_single_turn`` would handle sync. + That's true for vanilla GRPO but FALSE for NeMo Gym multi-turn + where the data producer bypasses ``_generate_single_turn`` + entirely. Without this trigger no sync ever happens and the + trainer becomes a no-op. + """ + trainer = self._make_stub_trainer(interval=1, async_prefetch=False) + from axolotl.core.trainers.grpo.async_trainer import ( + AsyncGRPOTrainer, + ) + + AsyncGRPOTrainer._maybe_sync_vllm_weights(trainer) + trainer._sync_lora_adapter.assert_called_once() + + def test_async_mode_with_explicit_interval_respects_modulo(self): + trainer = self._make_stub_trainer(interval=4, async_prefetch=True) + from axolotl.core.trainers.grpo.async_trainer import ( + AsyncGRPOTrainer, + ) + + # global_step=1, interval=4 → 1 % 4 != 0 → no sync + AsyncGRPOTrainer._maybe_sync_vllm_weights(trainer) + trainer._sync_lora_adapter.assert_not_called() + + # global_step=4 → 4 % 4 == 0 → sync + trainer.state.global_step = 4 + AsyncGRPOTrainer._maybe_sync_vllm_weights(trainer) + trainer._sync_lora_adapter.assert_called_once() + + if __name__ == "__main__": unittest.main() diff --git a/tests/integrations/test_nemo_gym.py b/tests/integrations/test_nemo_gym.py index 7fd53cee0f..83206043c1 100644 --- a/tests/integrations/test_nemo_gym.py +++ b/tests/integrations/test_nemo_gym.py @@ -361,6 +361,329 @@ class FakeCfg: assert cfg.dataloader_num_workers == 0 +class TestSelectWeightSyncTransport(unittest.TestCase): + """Pure-logic table tests for ``select_weight_sync_transport``.""" + + def _caps(self, **kwargs): + from axolotl.integrations.nemo_gym.plugin import VLLMWeightSyncCapabilities + + c = VLLMWeightSyncCapabilities(probed=True) + for k, v in kwargs.items(): + setattr(c, k, v) + return c + + def test_lora_with_native_endpoint(self): + from axolotl.integrations.nemo_gym.plugin import select_weight_sync_transport + + caps = self._caps(lora_filesystem=True) + assert ( + select_weight_sync_transport(caps, has_lora=True, vllm_lora_sync_pref=True) + == "lora_filesystem" + ) + + def test_lora_with_axolotl_endpoint(self): + from axolotl.integrations.nemo_gym.plugin import select_weight_sync_transport + + caps = self._caps(lora_axolotl=True) + assert ( + select_weight_sync_transport(caps, has_lora=True, vllm_lora_sync_pref=False) + == "lora_filesystem" + ) + + def test_lora_falls_back_to_nccl_when_no_lora_endpoint(self): + from axolotl.integrations.nemo_gym.plugin import select_weight_sync_transport + + caps = self._caps(nccl=True) + assert ( + select_weight_sync_transport(caps, has_lora=True, vllm_lora_sync_pref=False) + == "nccl" + ) + + def test_full_param_prefers_nccl(self): + from axolotl.integrations.nemo_gym.plugin import select_weight_sync_transport + + caps = self._caps(nccl=True, http_full=True) + assert ( + select_weight_sync_transport( + caps, has_lora=False, vllm_lora_sync_pref=False + ) + == "nccl" + ) + + def test_full_param_falls_back_to_http(self): + from axolotl.integrations.nemo_gym.plugin import select_weight_sync_transport + + caps = self._caps(http_full=True) + assert ( + select_weight_sync_transport( + caps, has_lora=False, vllm_lora_sync_pref=False + ) + == "http_full" + ) + + def test_full_param_no_routes_returns_none(self): + from axolotl.integrations.nemo_gym.plugin import select_weight_sync_transport + + caps = self._caps() # all False + assert ( + select_weight_sync_transport( + caps, has_lora=False, vllm_lora_sync_pref=False + ) + == "none" + ) + + def test_lora_no_routes_returns_none(self): + from axolotl.integrations.nemo_gym.plugin import select_weight_sync_transport + + caps = self._caps() + assert ( + select_weight_sync_transport(caps, has_lora=True, vllm_lora_sync_pref=True) + == "none" + ) + + +class TestProbeVllmWeightSync(unittest.TestCase): + """``probe_vllm_weight_sync`` reads a vLLM ``/openapi.json`` and reports caps.""" + + def test_stock_vllm_with_lora_enabled(self): + """Stock ``vllm serve --enable-lora`` exposes only LoRA endpoints.""" + from unittest.mock import patch + + from axolotl.integrations.nemo_gym.plugin import probe_vllm_weight_sync + + spec = { + "paths": { + "/v1/models": {"get": {}}, + "/v1/load_lora_adapter": {"post": {}}, + "/v1/unload_lora_adapter": {"post": {}}, + "/v1/completions": {"post": {}}, + } + } + with patch("requests.get") as mock_get: + mock_get.return_value.raise_for_status = lambda: None + mock_get.return_value.json = lambda: spec + caps = probe_vllm_weight_sync("http://localhost:8000") + + assert caps.probed is True + assert caps.lora_filesystem is True + assert caps.lora_axolotl is False + assert caps.nccl is False + assert caps.http_full is False + + def test_axolotl_serve_lora_full_capabilities(self): + """``axolotl vllm-serve`` exposes NCCL + LoRA + HTTP full sync.""" + from unittest.mock import patch + + from axolotl.integrations.nemo_gym.plugin import probe_vllm_weight_sync + + spec = { + "paths": { + "/init_communicator/": {"post": {}}, + "/update_named_param/": {"post": {}}, + "/batch_update_named_params/": {"post": {}}, + "/set_lora_adapter/": {"post": {}}, + "/clear_lora_adapter/": {"post": {}}, + "/http_update_weights/": {"post": {}}, + "/v1/load_lora_adapter": {"post": {}}, + } + } + with patch("requests.get") as mock_get: + mock_get.return_value.raise_for_status = lambda: None + mock_get.return_value.json = lambda: spec + caps = probe_vllm_weight_sync("http://localhost:8000") + + assert caps.probed is True + assert caps.nccl is True + assert caps.lora_axolotl is True + assert caps.lora_filesystem is True + assert caps.http_full is True + + def test_trl_vllm_serve_nccl_only(self): + """``trl vllm-serve`` exposes NCCL routes but not LoRA filesystem.""" + from unittest.mock import patch + + from axolotl.integrations.nemo_gym.plugin import probe_vllm_weight_sync + + spec = { + "paths": { + "/init_communicator/": {"post": {}}, + "/update_named_param/": {"post": {}}, + "/batch_update_named_params/": {"post": {}}, + "/close_communicator/": {"post": {}}, + "/generate/": {"post": {}}, + } + } + with patch("requests.get") as mock_get: + mock_get.return_value.raise_for_status = lambda: None + mock_get.return_value.json = lambda: spec + caps = probe_vllm_weight_sync("http://localhost:8000") + + assert caps.probed is True + assert caps.nccl is True + assert caps.lora_filesystem is False + assert caps.lora_axolotl is False + assert caps.http_full is False + + def test_unreachable_server_records_error(self): + from unittest.mock import patch + + from axolotl.integrations.nemo_gym.plugin import probe_vllm_weight_sync + + with patch("requests.get") as mock_get: + mock_get.side_effect = ConnectionError("Connection refused") + caps = probe_vllm_weight_sync("http://localhost:9999") + + assert caps.probed is False + assert caps.probe_error is not None + assert "ConnectionError" in caps.probe_error + assert caps.nccl is False + assert caps.lora_filesystem is False + + +class TestPluginWeightSyncEnforcement(unittest.TestCase): + """End-to-end test of post_trainer_create's transport-selection branch. + + The plugin used to silently no-op weight sync when ``vllm_lora_sync: false``, + leaving the trainer learning in isolation while vLLM kept serving the + unmodified base model. After the fix: + + - LoRA + LoRA-loading endpoint → installs filesystem LoRA sync + - LoRA + only NCCL endpoint → uses NCCL broadcast + - Full FT + NCCL endpoint → uses NCCL broadcast (standard TRL flow) + - Full FT + HTTP endpoint → raises NotImplementedError (step 3) + - No usable transport → raises ValueError with a precise diagnosis + """ + + @staticmethod + def _fake_cfg(adapter, vllm_lora_sync): + class FakeTRL: + pass + + class FakeCfg: + pass + + trl = FakeTRL() + trl.vllm_lora_sync = vllm_lora_sync + trl.vllm_server_host = "127.0.0.1" + trl.vllm_server_port = 8000 + + cfg = FakeCfg() + cfg.nemo_gym_enabled = True + cfg.nemo_gym_model_name = None + cfg.base_model = "test/model" + cfg.nemo_gym_verify_timeout = 30 + cfg.nemo_gym_multi_turn = True + cfg.adapter = adapter + cfg.trl = trl + return cfg + + @staticmethod + def _fake_trainer(): + class FakeVLLMGen: + sync_weights = staticmethod(lambda: None) + + class FakeTrainer: + vllm_generation = FakeVLLMGen() + + return FakeTrainer() + + @staticmethod + def _caps(**kwargs): + from axolotl.integrations.nemo_gym.plugin import VLLMWeightSyncCapabilities + + c = VLLMWeightSyncCapabilities(probed=True) + for k, v in kwargs.items(): + setattr(c, k, v) + return c + + def test_lora_with_lora_endpoint_installs_filesystem_sync(self): + from unittest.mock import patch + + from axolotl.integrations.nemo_gym.plugin import NemoGymPlugin + + plugin = NemoGymPlugin() + plugin._vllm_caps = self._caps(lora_filesystem=True) + cfg = self._fake_cfg(adapter="lora", vllm_lora_sync=True) + trainer = self._fake_trainer() + + with ( + patch.object(plugin, "_setup_lora_sync") as setup, + patch.object(plugin, "_check_lora_endpoint") as check, + patch.object(plugin, "_wire_multi_turn") as wire, + ): + plugin.post_trainer_create(cfg, trainer) + setup.assert_called_once() + check.assert_called_once() + wire.assert_called_once() + + def test_lora_with_no_routes_raises_with_lora_specific_message(self): + from axolotl.integrations.nemo_gym.plugin import NemoGymPlugin + + plugin = NemoGymPlugin() + plugin._vllm_caps = self._caps() # all False, but probed + cfg = self._fake_cfg(adapter="lora", vllm_lora_sync=False) + trainer = self._fake_trainer() + + with self.assertRaises(ValueError) as ctx: + plugin.post_trainer_create(cfg, trainer) + msg = str(ctx.exception) + assert "no-op trainer" in msg + assert "load_lora_adapter" in msg + assert "VLLM_ALLOW_RUNTIME_LORA_UPDATING" in msg + + def test_full_finetune_with_nccl_endpoint_uses_nccl(self): + from unittest.mock import patch + + from axolotl.integrations.nemo_gym.plugin import NemoGymPlugin + + plugin = NemoGymPlugin() + plugin._vllm_caps = self._caps(nccl=True) + cfg = self._fake_cfg(adapter=None, vllm_lora_sync=False) + trainer = self._fake_trainer() + + with patch.object(plugin, "_wire_multi_turn") as wire: + plugin.post_trainer_create(cfg, trainer) + wire.assert_called_once() + + def test_full_finetune_with_http_endpoint_not_implemented_yet(self): + from axolotl.integrations.nemo_gym.plugin import NemoGymPlugin + + plugin = NemoGymPlugin() + plugin._vllm_caps = self._caps(http_full=True) + cfg = self._fake_cfg(adapter=None, vllm_lora_sync=False) + trainer = self._fake_trainer() + with self.assertRaises(NotImplementedError) as ctx: + plugin.post_trainer_create(cfg, trainer) + assert "HTTP weight sync" in str(ctx.exception) + + def test_full_finetune_with_no_routes_raises_with_full_param_message(self): + from axolotl.integrations.nemo_gym.plugin import NemoGymPlugin + + plugin = NemoGymPlugin() + plugin._vllm_caps = self._caps() + cfg = self._fake_cfg(adapter=None, vllm_lora_sync=False) + trainer = self._fake_trainer() + with self.assertRaises(ValueError) as ctx: + plugin.post_trainer_create(cfg, trainer) + msg = str(ctx.exception) + assert "no-op trainer" in msg + assert "init_communicator" in msg + assert "http_update_weights" in msg + + def test_unprobed_caps_raises_with_probe_failure_message(self): + from axolotl.integrations.nemo_gym.plugin import NemoGymPlugin + + plugin = NemoGymPlugin() + # Plugin._vllm_caps left as default-None: the post_trainer_create + # branch falls back to a fresh VLLMWeightSyncCapabilities() with + # probed=False, so the error path should mention probing. + cfg = self._fake_cfg(adapter="lora", vllm_lora_sync=True) + trainer = self._fake_trainer() + with self.assertRaises(ValueError) as ctx: + plugin.post_trainer_create(cfg, trainer) + assert "could not probe" in str(ctx.exception) + + class TestNemoGymE2E(unittest.TestCase): """End-to-end test: data producer → agent (mocked) → parse → tensors → rewards. @@ -452,19 +775,15 @@ async def fake_call_agents(**kwargs): trainer = self._make_mock_trainer() producer._trainer = trainer - # Mock the prompt iterator (returns a batch of 1 input) - producer._prompt_iter = iter( - [ - [ - { - "prompt": [{"role": "user", "content": "Play Wordle!"}], - } - ] - ] - ) - producer._prompt_dl = [ - [{"prompt": [{"role": "user", "content": "Play Wordle!"}]}] + # Mock the prompt iterator. RepeatSampler(mini_repeat_count=num_generations) + # pre-expands prompts, so the iterator yields num_generations=2 consecutive + # copies of each unique prompt — one entry per rollout. + _prompt_batch = [ + {"prompt": [{"role": "user", "content": "Play Wordle!"}]}, + {"prompt": [{"role": "user", "content": "Play Wordle!"}]}, ] + producer._prompt_iter = iter([_prompt_batch]) + producer._prompt_dl = [_prompt_batch] # Call produce result = producer.produce(model=MagicMock(), global_step=1) @@ -530,10 +849,13 @@ async def fake_call_agents(**kwargs): producer._request_timeout = 30 producer._num_generations = 2 producer._trainer = self._make_mock_trainer() - producer._prompt_iter = iter( - [[{"prompt": [{"role": "user", "content": "Play!"}]}]] - ) - producer._prompt_dl = [[{"prompt": [{"role": "user", "content": "Play!"}]}]] + # RepeatSampler pre-expands by num_generations=2. + _prompt_batch = [ + {"prompt": [{"role": "user", "content": "Play!"}]}, + {"prompt": [{"role": "user", "content": "Play!"}]}, + ] + producer._prompt_iter = iter([_prompt_batch]) + producer._prompt_dl = [_prompt_batch] result = producer.produce(model=MagicMock(), global_step=1) diff --git a/tests/kernels/test_gemma4_fused_rope.py b/tests/kernels/test_gemma4_fused_rope.py index 7daedd6122..297bb25279 100644 --- a/tests/kernels/test_gemma4_fused_rope.py +++ b/tests/kernels/test_gemma4_fused_rope.py @@ -38,6 +38,30 @@ def _reference_norm_noscale(x, eps): return norm(x) +def _reference_partial_norm_rope(x, weight, cos, sin, eps): + """Reference: Gemma4RMSNorm over the full head_dim, then stock + ``apply_rotary_pos_emb`` over the first ``cos.shape[-1]`` columns, with + the trailing columns passed through unchanged. Mirrors how Llama-style + partial rotary is layered on top of the stock RMSNorm + RoPE primitives. + """ + from transformers.models.gemma4.modeling_gemma4 import ( + Gemma4RMSNorm, + apply_rotary_pos_emb, + ) + + D = x.shape[-1] + n_rot = cos.shape[-1] + norm = Gemma4RMSNorm(D, eps=eps).to(x.device, x.dtype) + norm.weight.data.copy_(weight) + normed = norm(x) + if n_rot == D: + return apply_rotary_pos_emb(normed, cos, sin, unsqueeze_dim=2) + x_rot = normed[..., :n_rot] + x_pass = normed[..., n_rot:] + rotated = apply_rotary_pos_emb(x_rot, cos, sin, unsqueeze_dim=2) + return torch.cat([rotated, x_pass], dim=-1) + + @pytest.fixture( params=[ (2, 64, 32, 256), # sliding window layer shape @@ -194,6 +218,172 @@ def test_grad_flows(self): assert w.grad.abs().sum() > 0, "w.grad is all zeros" +class TestFusedRMSNormRoPEPartialRotary: + """Partial-rotary: cos/sin last dim is smaller than head_dim. + + Compares against the original primitives (`Gemma4RMSNorm` + + `apply_rotary_pos_emb`) applied to the rotated slice with the trailing + columns passed through. Without the kernel fix this used to crash with + `RuntimeError: shape '[..., D]' is invalid for input of size B*S*n_rot`. + """ + + @pytest.mark.parametrize( + "B,S,H,D,n_rot", + [ + (2, 16, 4, 64, 32), # half rotary (Llama-style 0.5) + (2, 16, 4, 64, 16), # quarter rotary + (2, 32, 8, 128, 64), # half rotary, larger heads + (1, 8, 2, 256, 64), # 26B sliding-shape, 0.25 partial + (1, 8, 2, 64, 64), # n_rot == D: must still match full-rotary path + ], + ids=["half_64", "quarter_64", "half_128", "quarter_256", "full_64"], + ) + def test_forward_matches_reference(self, B, S, H, D, n_rot): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + eps = 1e-6 + x = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + weight = torch.randn(D, device="cuda", dtype=torch.bfloat16) + cos = torch.randn(B, S, n_rot, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, n_rot, device="cuda", dtype=torch.bfloat16) + + y_ref = _reference_partial_norm_rope(x.clone(), weight, cos, sin, eps) + y_fused = fused_rms_norm_rope(x.clone(), weight, cos, sin, eps=eps) + + assert y_fused.shape == y_ref.shape == (B, S, H, D) + cos_sim = torch.nn.functional.cosine_similarity( + y_ref.flatten().float(), y_fused.flatten().float(), dim=0 + ) + assert cos_sim > 0.999, ( + f"partial rotary forward cosine_sim={cos_sim:.6f} " + f"(B={B},S={S},H={H},D={D},n_rot={n_rot})" + ) + + # The pass-through tail must equal the reference RMSNorm output bit- + # for-bit (any deviation would mean the kernel is touching it with a + # spurious rotation, which is the original bug class). + torch.testing.assert_close( + y_fused[..., n_rot:], y_ref[..., n_rot:], rtol=1e-2, atol=1e-2 + ) + + @pytest.mark.parametrize( + "B,S,H,D,n_rot", + [(2, 16, 4, 64, 32), (1, 8, 2, 256, 64)], + ids=["half_64", "quarter_256"], + ) + def test_x_grad_matches_reference(self, B, S, H, D, n_rot): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + eps = 1e-6 + cos = torch.randn(B, S, n_rot, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, n_rot, device="cuda", dtype=torch.bfloat16) + weight_init = torch.randn(D, device="cuda", dtype=torch.bfloat16) + x_data = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + + # Reference backward via the original primitives + x_ref = x_data.clone().requires_grad_(True) + w_ref = weight_init.clone() + y_ref = _reference_partial_norm_rope(x_ref, w_ref, cos, sin, eps) + y_ref.sum().backward() + + # Fused backward + x_fused = x_data.clone().requires_grad_(True) + w_fused = weight_init.clone().requires_grad_(True) + y_fused = fused_rms_norm_rope(x_fused, w_fused, cos, sin, eps=eps) + y_fused.sum().backward() + + cos_sim_x = torch.nn.functional.cosine_similarity( + x_fused.grad.flatten().float(), x_ref.grad.flatten().float(), dim=0 + ) + assert cos_sim_x > 0.999, f"partial rotary x grad cosine_sim={cos_sim_x:.6f}" + + @pytest.mark.parametrize( + "B,S,H,D,n_rot", + [(2, 16, 4, 64, 32), (1, 8, 2, 256, 64)], + ids=["half_64", "quarter_256"], + ) + def test_weight_grad_matches_reference(self, B, S, H, D, n_rot): + from transformers.models.gemma4.modeling_gemma4 import Gemma4RMSNorm + + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + eps = 1e-6 + cos = torch.randn(B, S, n_rot, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, n_rot, device="cuda", dtype=torch.bfloat16) + weight_init = torch.randn(D, device="cuda", dtype=torch.bfloat16) + x_data = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + + # Reference: Gemma4RMSNorm whose .weight collects grads, then partial + # rotary applied to the rotated slice. + norm_ref = Gemma4RMSNorm(D, eps=eps).cuda().to(torch.bfloat16) + norm_ref.weight = torch.nn.Parameter(weight_init.clone()) + normed = norm_ref(x_data) + from transformers.models.gemma4.modeling_gemma4 import apply_rotary_pos_emb + + rotated = apply_rotary_pos_emb(normed[..., :n_rot], cos, sin, unsqueeze_dim=2) + y_ref = torch.cat([rotated, normed[..., n_rot:]], dim=-1) + y_ref.sum().backward() + + w_fused = weight_init.clone().requires_grad_(True) + fused_rms_norm_rope(x_data.clone(), w_fused, cos, sin, eps=eps).sum().backward() + + cos_sim_w = torch.nn.functional.cosine_similarity( + w_fused.grad.flatten().float(), + norm_ref.weight.grad.flatten().float(), + dim=0, + ) + assert cos_sim_w > 0.995, ( + f"partial rotary weight grad cosine_sim={cos_sim_w:.6f}" + ) + + def test_full_rotary_unchanged_when_n_rot_equals_d(self): + """Regression: passing cos/sin with shape == head_dim must still + match the full-rotary reference (the partial-rotary code path must + not perturb the existing full-rotary output).""" + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D = 2, 16, 4, 64 + eps = 1e-6 + x = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + weight = torch.randn(D, device="cuda", dtype=torch.bfloat16) + cos = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + + y_ref = _reference_norm_rope(x.clone(), weight, cos, sin, eps) + y_fused = fused_rms_norm_rope(x.clone(), weight, cos, sin, eps=eps) + cos_sim = torch.nn.functional.cosine_similarity( + y_ref.flatten().float(), y_fused.flatten().float(), dim=0 + ) + assert cos_sim > 0.999, f"full-rotary regression cos_sim={cos_sim:.6f}" + + def test_validation_errors(self): + """Wrapper rejects misshaped inputs cleanly (instead of a cryptic + Triton crash deeper in the kernel).""" + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D = 1, 4, 2, 64 + x = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + w = torch.randn(D, device="cuda", dtype=torch.bfloat16) + + # n_rot > head_dim + cos_big = torch.randn(B, S, D + 16, device="cuda", dtype=torch.bfloat16) + sin_big = torch.randn(B, S, D + 16, device="cuda", dtype=torch.bfloat16) + with pytest.raises(ValueError, match="cannot exceed head_dim"): + fused_rms_norm_rope(x, w, cos_big, sin_big) + + # cos/sin last-dim mismatch + cos = torch.randn(B, S, 32, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, 16, device="cuda", dtype=torch.bfloat16) + with pytest.raises(ValueError, match="same last dim"): + fused_rms_norm_rope(x, w, cos, sin) + + # odd rotary dim + cos_odd = torch.randn(B, S, 31, device="cuda", dtype=torch.bfloat16) + sin_odd = torch.randn(B, S, 31, device="cuda", dtype=torch.bfloat16) + with pytest.raises(ValueError, match="must be even"): + fused_rms_norm_rope(x, w, cos_odd, sin_odd) + + class TestFusedRMSNormNoScale: """Tests for v_norm (RMSNorm without learnable scale).""" diff --git a/tests/monkeypatch/test_gemma4_fused_attn.py b/tests/monkeypatch/test_gemma4_fused_attn.py new file mode 100644 index 0000000000..0530d0ee8d --- /dev/null +++ b/tests/monkeypatch/test_gemma4_fused_attn.py @@ -0,0 +1,219 @@ +"""Tests for the Gemma 4 fused-attention monkey-patch. + +These tests exercise the patched ``Gemma4TextAttention.forward`` against +the stock implementation it replaces. The hybrid Gemma 4 model intentionally +mixes a sliding (`head_dim=32`) layer with a full-attention proportional-rope +layer (`global_head_dim=64`, `partial_rotary_factor=0.25`) so that the +partial-rotary RMSNorm+RoPE path through the fused Triton kernel is +exercised end-to-end (this is the bug originally documented in +``GEMMA4_FUSED_ROPE_HYBRID_ATTN_BUG.md``). + +The full-model forward also pins that the fused forward keeps accepting +whatever call shape ``Gemma4TextDecoderLayer.forward`` produces in the +installed transformers version — so any future signature drift on +upstream's side trips a clear failure here instead of a confusing +TypeError deep in a training run. +""" + +import pytest +import torch + +pytestmark = [ + pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"), +] + +pytest.importorskip( + "transformers.models.gemma4", + reason="fused_attn patch only matters when Gemma 4 is available", +) + + +@pytest.fixture +def restore_gemma4_attention(): + """Snapshot ``Gemma4TextAttention.forward`` and restore after the test + so the monkey-patch does not leak across the suite.""" + from transformers.models.gemma4.modeling_gemma4 import Gemma4TextAttention + + saved = Gemma4TextAttention.forward + yield Gemma4TextAttention + Gemma4TextAttention.forward = saved + + +def _build_hybrid_config(): + """Tiny hybrid Gemma 4 config: one sliding layer + one full-attention + layer with proportional rope and partial_rotary_factor=0.25. This is + the same shape pattern as ``google/gemma-4-26B-A4B-it`` but small + enough to fit on any GPU.""" + from transformers.models.gemma4.configuration_gemma4 import Gemma4TextConfig + + cfg = Gemma4TextConfig( + vocab_size=128, + hidden_size=64, + intermediate_size=128, + num_hidden_layers=2, + num_attention_heads=2, + num_key_value_heads=2, + head_dim=32, + global_head_dim=64, + layer_types=["sliding_attention", "full_attention"], + sliding_window=64, + max_position_embeddings=2048, + hidden_size_per_layer_input=16, + vocab_size_per_layer_input=128, + rope_parameters={ + "sliding_attention": { + "rope_type": "default", + "rope_theta": 10000.0, + }, + "full_attention": { + "rope_type": "proportional", + "rope_theta": 1000000.0, + "partial_rotary_factor": 0.25, + }, + }, + ) + cfg._attn_implementation = "sdpa" + return cfg + + +def _build_model(seed=0): + from transformers.models.gemma4.modeling_gemma4 import Gemma4TextModel + + torch.manual_seed(seed) + cfg = _build_hybrid_config() + return Gemma4TextModel(cfg).cuda().to(torch.bfloat16).eval() + + +class TestFusedAttnSignature: + """The fused forward must accept the same call shape as + ``Gemma4TextDecoderLayer`` produces in the installed transformers + version. Any signature drift surfaces here as a TypeError.""" + + def test_decoder_layer_can_call_fused_forward(self, restore_gemma4_attention): + """Run a model forward that exercises the real + ``Gemma4TextDecoderLayer -> Gemma4TextAttention`` call path with + the fused patch installed.""" + from axolotl.monkeypatch.models.gemma4.fused_attn import ( + patch_gemma4_fused_attn, + ) + + m = _build_model() + ids = torch.randint(0, 128, (2, 16), device="cuda") + mask = torch.ones(2, 16, dtype=torch.long, device="cuda") + + patch_gemma4_fused_attn() + with torch.no_grad(): + out = m(input_ids=ids, attention_mask=mask).last_hidden_state + + assert out.shape == (2, 16, 64) + assert torch.isfinite(out).all() + + +class TestFusedAttnPerLayerCorrectness: + """Compare the patched attention layer to the stock implementation + on a single forward call. This isolates the fused kernel correctness + from cross-layer numerical drift.""" + + def _run_attention(self, model, layer_idx, hidden_states, position_ids): + """Call ``Gemma4TextAttention.forward`` (whatever is currently + installed) for one layer and return the output.""" + attn = model.layers[layer_idx].self_attn + layer_type = model.config.layer_types[layer_idx] + cos, sin = model.rotary_emb(hidden_states, position_ids, layer_type) + out, _ = attn( + hidden_states=hidden_states, + position_embeddings=(cos, sin), + attention_mask=None, + shared_kv_states={}, + ) + return out + + @pytest.mark.parametrize( + "layer_idx", + [0, 1], + ids=["sliding_head32", "global_head64_proportional"], + ) + def test_forward_matches_stock(self, restore_gemma4_attention, layer_idx): + from axolotl.monkeypatch.models.gemma4.fused_attn import ( + patch_gemma4_fused_attn, + ) + + m = _build_model(seed=1) + hs = torch.randn(2, 16, 64, device="cuda", dtype=torch.bfloat16) + pos = torch.arange(16, device="cuda").unsqueeze(0).expand(2, -1) + + with torch.no_grad(): + ref = self._run_attention(m, layer_idx, hs, pos) + + patch_gemma4_fused_attn() + with torch.no_grad(): + got = self._run_attention(m, layer_idx, hs, pos) + + assert got.shape == ref.shape + assert torch.isfinite(got).all() + cos_sim = torch.nn.functional.cosine_similarity( + ref.flatten().float(), got.flatten().float(), dim=0 + ) + assert cos_sim > 0.999, ( + f"layer {layer_idx} fused vs stock cosine_sim={cos_sim:.6f}" + ) + # bf16 precision: a few millis of absolute drift per element is + # acceptable for a Q/K/V projection pipeline. Anything larger is + # a real bug. + torch.testing.assert_close(got, ref, rtol=5e-2, atol=5e-2) + + +class TestFusedAttnFullModel: + """End-to-end model forward + backward through both layer types.""" + + def test_full_forward_matches_stock(self, restore_gemma4_attention): + from axolotl.monkeypatch.models.gemma4.fused_attn import ( + patch_gemma4_fused_attn, + ) + + m = _build_model(seed=2) + ids = torch.randint(0, 128, (2, 32), device="cuda") + mask = torch.ones(2, 32, dtype=torch.long, device="cuda") + + with torch.no_grad(): + ref = m(input_ids=ids, attention_mask=mask).last_hidden_state.clone() + + patch_gemma4_fused_attn() + with torch.no_grad(): + got = m(input_ids=ids, attention_mask=mask).last_hidden_state.clone() + + assert got.shape == ref.shape + assert torch.isfinite(got).all() + cos_sim = torch.nn.functional.cosine_similarity( + ref.flatten().float(), got.flatten().float(), dim=0 + ) + # End-to-end through 2 layers (RMSNorm, attention, MLP/MoE) in bf16 + # accumulates a small amount of numerical drift; we just want to + # pin that the two paths are computing the same function. + assert cos_sim > 0.999, f"end-to-end cosine_sim={cos_sim:.6f}" + + def test_backward_grad_flows_through_fused_path(self, restore_gemma4_attention): + """Gradients must propagate through the fused RMSNorm+RoPE kernels + for both the sliding and proportional-rope layers.""" + from axolotl.monkeypatch.models.gemma4.fused_attn import ( + patch_gemma4_fused_attn, + ) + + m = _build_model(seed=3).train() + patch_gemma4_fused_attn() + + ids = torch.randint(0, 128, (2, 16), device="cuda") + mask = torch.ones(2, 16, dtype=torch.long, device="cuda") + out = m(input_ids=ids, attention_mask=mask).last_hidden_state + out.sum().backward() + + # Both layers must accumulate gradients on q_norm.weight and + # k_norm.weight — that proves the fused kernel ran the backward. + for i, layer in enumerate(m.layers[:2]): + attn = layer.self_attn + assert attn.q_norm.weight.grad is not None, f"layer {i} q_norm no grad" + assert attn.k_norm.weight.grad is not None, f"layer {i} k_norm no grad" + assert attn.q_norm.weight.grad.isfinite().all() + assert attn.k_norm.weight.grad.isfinite().all() + assert attn.q_norm.weight.grad.abs().sum() > 0 + assert attn.k_norm.weight.grad.abs().sum() > 0 diff --git a/tests/monkeypatch/test_gemma4_hybrid_mask.py b/tests/monkeypatch/test_gemma4_hybrid_mask.py new file mode 100644 index 0000000000..66d56bcf13 --- /dev/null +++ b/tests/monkeypatch/test_gemma4_hybrid_mask.py @@ -0,0 +1,343 @@ +"""Tests for the Gemma 4 hybrid-attention mask fix. + +These tests pin the single critical behavior: after installing the patch, +``modeling_gemma4.create_causal_mask`` passes an SDPA-overridden config to +the underlying mask builder regardless of what the caller's config says. +This is what keeps full-attention (head_dim=512) global layers from +crashing at long sequence lengths — they need a 4D SDPA-format mask, not +the 2D FA2 mask that would be built from the model-level config. + +The tests use a mocked ``create_causal_mask`` so they don't have to load +a real 26B Gemma 4 model or even have access to its weights. What matters +for the bug fix is which config is handed to the mask factory, not the +factory's actual output. +""" + +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest + +pytest.importorskip( + "transformers.models.gemma4", + reason="gemma4_hybrid_mask patch only matters when Gemma 4 is available", +) + + +@pytest.fixture +def restore_gemma4_module(): + """Snapshot ``modeling_gemma4.create_causal_mask`` and restore after + each test so patch state doesn't leak across the suite.""" + from transformers.models.gemma4 import modeling_gemma4 + + saved = modeling_gemma4.create_causal_mask + yield modeling_gemma4 + modeling_gemma4.create_causal_mask = saved + # Reset the module-level flag so the next test can re-install cleanly. + from axolotl.monkeypatch import gemma4_hybrid_mask + + gemma4_hybrid_mask._PATCH_APPLIED = False + + +def test_patch_replaces_create_causal_mask(restore_gemma4_module): + modeling_gemma4 = restore_gemma4_module + from axolotl.monkeypatch.gemma4_hybrid_mask import patch_gemma4_hybrid_mask + + original = modeling_gemma4.create_causal_mask + assert patch_gemma4_hybrid_mask() is True + + assert modeling_gemma4.create_causal_mask is not original + assert modeling_gemma4.create_causal_mask._axolotl_original is original, ( + "patched wrapper must expose the original reference for teardown" + ) + + +def test_patch_is_idempotent(restore_gemma4_module): + modeling_gemma4 = restore_gemma4_module + from axolotl.monkeypatch.gemma4_hybrid_mask import patch_gemma4_hybrid_mask + + patch_gemma4_hybrid_mask() + wrapper_first = modeling_gemma4.create_causal_mask + + # Second call must not re-wrap the already-wrapped function (which + # would leak the original reference through a chain of wrappers). + patch_gemma4_hybrid_mask() + wrapper_second = modeling_gemma4.create_causal_mask + + assert wrapper_first is wrapper_second + + +def test_patched_mask_forces_sdpa_config(restore_gemma4_module): + """Core invariant: when the patched wrapper is called with a config + that says ``flash_attention_2``, the underlying mask factory receives + a shallow-copied config whose ``_attn_implementation`` is ``"sdpa"``. + + Without this, the full-attention global layers get a 2D FA2 mask and + crash at long seq lens with the [B, H, S, S] / [B, S] expand error. + """ + modeling_gemma4 = restore_gemma4_module + from axolotl.monkeypatch.gemma4_hybrid_mask import patch_gemma4_hybrid_mask + + # Swap in a mock BEFORE installing the patch so the wrapper captures + # it as the "original". The mock records every call so we can inspect + # what config got passed through. + mock_factory = MagicMock(name="create_causal_mask", return_value="mask_4d") + modeling_gemma4.create_causal_mask = mock_factory + patch_gemma4_hybrid_mask() + + # Caller-supplied config says FA2 (that's the model-level setting). + caller_config = SimpleNamespace( + _attn_implementation="flash_attention_2", + head_dim=512, + some_other_attr="preserved", + ) + result = modeling_gemma4.create_causal_mask( + caller_config, + inputs_embeds=None, + attention_mask=None, + past_key_values=None, + position_ids=None, + ) + + # Wrapper returned whatever the mock returned — no transformation of + # the result itself. + assert result == "mask_4d" + + # The mock was called exactly once with a config whose + # ``_attn_implementation`` is sdpa, NOT the caller's fa2. + assert mock_factory.call_count == 1 + (passed_config, *_), passed_kwargs = mock_factory.call_args + assert passed_config._attn_implementation == "sdpa" + + # The wrapper must NOT mutate the caller's config in place — other + # mask builders (e.g. create_sliding_window_causal_mask) read from + # the same config and must still see fa2. + assert caller_config._attn_implementation == "flash_attention_2" + + # Other attributes on the config must be preserved so the underlying + # factory has everything it needs (head_dim, rope_theta, vocab_size, ...). + assert passed_config.head_dim == 512 + assert passed_config.some_other_attr == "preserved" + + +def test_patched_wrapper_passes_through_all_kwargs(restore_gemma4_module): + """The wrapper must forward positional + keyword args to the original + unchanged, so transformers' own call-site in Gemma4TextModel.forward + keeps working across minor transformers-version signature drift.""" + modeling_gemma4 = restore_gemma4_module + from axolotl.monkeypatch.gemma4_hybrid_mask import patch_gemma4_hybrid_mask + + mock_factory = MagicMock(return_value="mask") + modeling_gemma4.create_causal_mask = mock_factory + patch_gemma4_hybrid_mask() + + caller_config = SimpleNamespace(_attn_implementation="flash_attention_2") + modeling_gemma4.create_causal_mask( + caller_config, + "positional_arg", + inputs_embeds="embeds", + attention_mask="mask_2d", + past_key_values="cache", + position_ids="positions", + or_mask_function="or_fn", + ) + + args, kwargs = mock_factory.call_args + # First positional (after config override) is preserved. + assert args[1] == "positional_arg" + # All kwargs are forwarded untouched. + assert kwargs["inputs_embeds"] == "embeds" + assert kwargs["attention_mask"] == "mask_2d" + assert kwargs["past_key_values"] == "cache" + assert kwargs["position_ids"] == "positions" + assert kwargs["or_mask_function"] == "or_fn" + + +def test_unpatch_restores_original(restore_gemma4_module): + modeling_gemma4 = restore_gemma4_module + from axolotl.monkeypatch.gemma4_hybrid_mask import ( + patch_gemma4_hybrid_mask, + unpatch_gemma4_hybrid_mask, + ) + + sentinel = MagicMock(name="original") + modeling_gemma4.create_causal_mask = sentinel + patch_gemma4_hybrid_mask() + assert modeling_gemma4.create_causal_mask is not sentinel + + unpatch_gemma4_hybrid_mask() + assert modeling_gemma4.create_causal_mask is sentinel + + +def test_unpatch_is_safe_without_prior_patch(restore_gemma4_module): + from axolotl.monkeypatch.gemma4_hybrid_mask import unpatch_gemma4_hybrid_mask + + # Should be a no-op, no exception. + unpatch_gemma4_hybrid_mask() + + +def test_sliding_window_mask_builder_is_not_patched(restore_gemma4_module): + """Only ``create_causal_mask`` is overridden — the sliding-window + factory must remain bound to its original to preserve FA2 masks for + the sliding-attention layers. If we accidentally patch both, the + sliding layers get SDPA format and lose the FA2 speedup.""" + modeling_gemma4 = restore_gemma4_module + from axolotl.monkeypatch.gemma4_hybrid_mask import patch_gemma4_hybrid_mask + + if not hasattr(modeling_gemma4, "create_sliding_window_causal_mask"): + pytest.skip("transformers version has no create_sliding_window_causal_mask") + + sliding_before = modeling_gemma4.create_sliding_window_causal_mask + patch_gemma4_hybrid_mask() + sliding_after = modeling_gemma4.create_sliding_window_causal_mask + assert sliding_after is sliding_before + + +# --------------------------------------------------------------------------- +# Integration tests with a tiny randomly-initialized Gemma4TextModel. +# +# These do NOT load real 26B weights. They build a ~350k-param Gemma 4 text +# model with 2 layers (one sliding, one full_attention), apply the hybrid +# attention path end-to-end, and run a forward pass with a padded +# attention_mask at a long-ish seq len. The invariant we're pinning is that +# the full_attention layer does not crash with the +# "Target sizes: [B, H, S, S]. Tensor sizes: [B, S]" +# error — the exact failure that blew up the Gemma 4 MoE 26B pilot at ~7k +# tokens in the FSDP2 training run. +# --------------------------------------------------------------------------- + + +def _build_tiny_gemma4_text_model(): + """Return a tiny randomly-initialized Gemma4TextModel with mixed layers.""" + import torch + from transformers.models.gemma4.configuration_gemma4 import Gemma4TextConfig + from transformers.models.gemma4.modeling_gemma4 import Gemma4TextModel + + cfg = Gemma4TextConfig( + vocab_size=128, + hidden_size=64, + intermediate_size=128, + num_hidden_layers=2, + num_attention_heads=2, + num_key_value_heads=2, + head_dim=32, + layer_types=["sliding_attention", "full_attention"], + sliding_window=64, + max_position_embeddings=2048, + hidden_size_per_layer_input=16, + vocab_size_per_layer_input=128, + ) + # Caller-supplied attn impl simulates the pilot config (fa2 at model + # level). The hybrid patch is what makes this survive long context. + cfg._attn_implementation = "sdpa" # start safe; the test toggles fa2 later + torch.manual_seed(42) + model = Gemma4TextModel(cfg).eval() + return model, cfg + + +def _apply_hybrid_attn_inline(model, cfg): + """Replicate what ``patch_manager._apply_gemma_hybrid_attention`` does + to a model, without needing a full PatchManager / pydantic cfg.""" + import copy + + from axolotl.monkeypatch.gemma4_hybrid_mask import patch_gemma4_hybrid_mask + + for layer_idx, layer in enumerate(model.layers): + if cfg.layer_types[layer_idx] != "sliding_attention": + attn = getattr(layer, "self_attn", None) + if attn is not None and hasattr(attn, "config"): + sdpa_cfg = copy.copy(attn.config) + sdpa_cfg._attn_implementation = "sdpa" + attn.config = sdpa_cfg + patch_gemma4_hybrid_mask() + + +def test_tiny_gemma4_long_context_forward_does_not_crash(restore_gemma4_module): + """End-to-end invariant: with the hybrid attn patch applied, a tiny + Gemma4TextModel runs a forward at long context (1024 tokens) with + real padding in the attention mask, producing the expected output + shape. This exercises the actual code path that crashed the pilot + without needing a real 26B checkpoint or CUDA.""" + import torch + + model, cfg = _build_tiny_gemma4_text_model() + _apply_hybrid_attn_inline(model, cfg) + + B, S = 2, 1024 + input_ids = torch.randint(0, cfg.vocab_size, (B, S)) + attn_mask = torch.ones(B, S, dtype=torch.long) + # Pad positions in the second row. Without padding, SDPA falls back to + # ``is_causal=True`` with ``mask=None`` — we need a materialized 4D + # mask to exercise the actual bug site. + attn_mask[1, S // 2 :] = 0 + + with torch.no_grad(): + out = model(input_ids=input_ids, attention_mask=attn_mask) + + assert out.last_hidden_state.shape == (B, S, cfg.hidden_size) + assert torch.isfinite(out.last_hidden_state).all() + + +def test_patched_create_causal_mask_returns_4d_for_real_config( + restore_gemma4_module, +): + """Hit the REAL ``create_causal_mask`` (not a mock) via the wrapper + and verify the returned mask is a 4D tensor — which is the shape the + SDPA-patched global layers need. Without the patch and with a + caller-supplied FA2 config this would return a 2D mask and the layer + would crash at long context.""" + import torch + from transformers.cache_utils import DynamicCache + from transformers.models.gemma4.configuration_gemma4 import Gemma4TextConfig + + from axolotl.monkeypatch.gemma4_hybrid_mask import patch_gemma4_hybrid_mask + + patch_gemma4_hybrid_mask() + modeling_gemma4 = restore_gemma4_module + + cfg = Gemma4TextConfig( + vocab_size=128, + hidden_size=64, + num_hidden_layers=2, + num_attention_heads=2, + num_key_value_heads=2, + head_dim=32, + layer_types=["sliding_attention", "full_attention"], + sliding_window=64, + max_position_embeddings=2048, + hidden_size_per_layer_input=16, + vocab_size_per_layer_input=128, + ) + # Simulate the pilot: caller says flash_attention_2, but global layers + # were switched to SDPA per-layer. Without the patch, create_causal_mask + # would return an FA2 2D mask here and the SDPA layer would crash. + cfg._attn_implementation = "flash_attention_2" + + B, S = 2, 1024 + inputs_embeds = torch.zeros((B, S, cfg.hidden_size), dtype=torch.float32) + attention_mask = torch.ones((B, S), dtype=torch.long) + attention_mask[1, S // 2 :] = 0 # force the 4D materialized path + position_ids = torch.arange(S).unsqueeze(0).expand(B, -1) + past_key_values = DynamicCache(config=cfg) + + mask = modeling_gemma4.create_causal_mask( + config=cfg, + inputs_embeds=inputs_embeds, + attention_mask=attention_mask, + past_key_values=past_key_values, + position_ids=position_ids, + ) + + assert mask is not None + assert isinstance(mask, torch.Tensor) + assert mask.dim() == 4, ( + f"expected a 4D SDPA-format mask, got {mask.dim()}D " + f"shape={tuple(mask.shape)}. The full_attention global layers need " + "this shape or they crash at long context." + ) + assert mask.shape[0] == B + assert mask.shape[-1] == S + assert mask.shape[-2] == S + + # Caller's config must be untouched — other code paths still read it. + assert cfg._attn_implementation == "flash_attention_2" diff --git a/tests/utils/schemas/validation/test_config_validators.py b/tests/utils/schemas/validation/test_config_validators.py index c756f13625..fbfa79ad8a 100644 --- a/tests/utils/schemas/validation/test_config_validators.py +++ b/tests/utils/schemas/validation/test_config_validators.py @@ -5,6 +5,8 @@ - save_strategy: 'best' requires metric_for_best_model - streaming=True with val_set_size > 0 is rejected - lora_target_modules with invalid regex patterns is rejected + - GRPO: generation batch size must be divisible by num_generations, + num_generations >= 2, and effective_gbs >= num_generations * world_size """ import pytest @@ -117,3 +119,136 @@ def test_multiple_invalid_patterns_reported(self, min_base_cfg): ) with pytest.raises(ValueError, match="invalid regex pattern"): validate_config(cfg) + + +class TestGRPOBatchSizeValidator: + """GRPO requires (mb*GA) % num_generations == 0 and num_generations >= 2. + + These call the @model_validator(mode="before") classmethod directly on a + plain dict — same input shape it receives during full Pydantic validation, + just without dragging in unrelated fields (datasets / model loading / etc.) + that aren't relevant to what's under test. The validator is registered on + ``RLValidationMixin`` (which ``AxolotlInputConfig`` inherits) so this is the + same code path ``axolotl train`` exercises. + """ + + @staticmethod + def _check(data): + from axolotl.utils.schemas.validation import RLValidationMixin + + return RLValidationMixin.check_grpo_batch_size_divisibility(data) + + def test_divisible_passes(self): + data = { + "rl": "grpo", + "micro_batch_size": 1, + "gradient_accumulation_steps": 4, + "trl": {"num_generations": 4}, + } + # Should return data unchanged (no exception) + out = self._check(data) + assert out["trl"]["num_generations"] == 4 + + def test_non_divisible_raises(self): + data = { + "rl": "grpo", + "micro_batch_size": 1, + "gradient_accumulation_steps": 2, + "trl": {"num_generations": 4}, + } + with pytest.raises(ValueError, match="num_generations"): + self._check(data) + + def test_non_divisible_error_includes_fix_hint(self): + data = { + "rl": "grpo", + "micro_batch_size": 1, + "gradient_accumulation_steps": 3, + "trl": {"num_generations": 4}, + } + with pytest.raises(ValueError, match="gradient_accumulation_steps: 4"): + self._check(data) + + def test_num_generations_one_raises(self): + data = { + "rl": "grpo", + "micro_batch_size": 1, + "gradient_accumulation_steps": 4, + "trl": {"num_generations": 1}, + } + with pytest.raises(ValueError, match=r"num_generations >= 2"): + self._check(data) + + def test_explicit_generation_batch_size_divisible_passes(self): + data = { + "rl": "grpo", + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "trl": {"num_generations": 4, "generation_batch_size": 8}, + } + out = self._check(data) + assert out["trl"]["generation_batch_size"] == 8 + + def test_explicit_generation_batch_size_non_divisible_raises(self): + data = { + "rl": "grpo", + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "trl": {"num_generations": 4, "generation_batch_size": 6}, + } + with pytest.raises(ValueError, match="trl.generation_batch_size"): + self._check(data) + + def test_non_grpo_skips_check(self): + # Anything other than rl=grpo should pass through untouched, even + # with non-divisible batch sizes — they're irrelevant to other RL + # methods that don't use group-relative advantages. + data = { + "rl": "dpo", + "micro_batch_size": 1, + "gradient_accumulation_steps": 3, + "trl": {"num_generations": 4}, + } + assert self._check(data) is data + + def test_no_rl_set_skips_check(self): + data = { + "micro_batch_size": 1, + "gradient_accumulation_steps": 3, + } + assert self._check(data) is data + + def test_grpo_without_num_generations_skips_check(self): + # If num_generations isn't set, TRL uses its own default — we don't + # have enough info to validate, so the validator must short-circuit + # rather than guess. + data = { + "rl": "grpo", + "micro_batch_size": 1, + "gradient_accumulation_steps": 3, + "trl": {}, + } + out = self._check(data) + assert out["rl"] == "grpo" + + def test_multi_rank_group_size_check(self): + data = { + "rl": "grpo", + "micro_batch_size": 1, + "gradient_accumulation_steps": 4, # gbs=4 + "world_size": 2, # need gbs >= 4*2 = 8 + "trl": {"num_generations": 4}, + } + with pytest.raises(ValueError, match=r"world_size=2"): + self._check(data) + + def test_multi_rank_group_size_satisfied(self): + data = { + "rl": "grpo", + "micro_batch_size": 1, + "gradient_accumulation_steps": 8, # gbs=8 >= 4*2 + "world_size": 2, + "trl": {"num_generations": 4}, + } + out = self._check(data) + assert out["gradient_accumulation_steps"] == 8 From 90090fa9e88a21054f05998ab1143cbce5a2ebd4 Mon Sep 17 00:00:00 2001 From: Andrew Wu <49274027+BrownianNotion@users.noreply.github.com> Date: Thu, 23 Apr 2026 05:25:28 +0100 Subject: [PATCH 1279/1405] DPO support loss types (#3566) * Support loss_type/loss_weights DPO * Validate dpo loss type/weights only set for dpo * Tests: Update ipo tests to use new path * Docs: Update docs for new ipo path * PR fixes - typo/validation * PR nit - warning * chore: fix warnings arg --------- Co-authored-by: NanoCode012 --- AGENTS.md | 2 +- docs/agents/preference_tuning.md | 2 +- docs/rlhf.qmd | 4 +- src/axolotl/core/trainers/dpo/__init__.py | 8 ++++ src/axolotl/utils/schemas/config.py | 10 +++++ src/axolotl/utils/schemas/validation.py | 34 ++++++++++++++ tests/core/test_builders.py | 7 ++- tests/e2e/test_dpo.py | 55 ++++++++++++++++++++++- 8 files changed, 117 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e9b747ce39..43470d9f80 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ axolotl config-schema # Dump config JSON schema | Method | Config Key | When to Use | |--------|-----------|-------------| | SFT | *(default)* | Input-output pairs, instruction tuning | -| DPO/IPO | `rl: dpo` / `rl: ipo` | Paired preference data (chosen vs rejected) | +| DPO/IPO | `rl: dpo` / `rl: dpo, dpo_loss_type: ["ipo"]` | Paired preference data (chosen vs rejected) | | KTO | `rl: kto` | Unpaired binary preference labels | | ORPO | `rl: orpo` | Single-stage alignment, no ref model | | GRPO | `rl: grpo` | RL with verifiable reward functions (math, code) | diff --git a/docs/agents/preference_tuning.md b/docs/agents/preference_tuning.md index bed9730097..3414d22ceb 100644 --- a/docs/agents/preference_tuning.md +++ b/docs/agents/preference_tuning.md @@ -38,7 +38,7 @@ No vLLM server needed (unlike GRPO). Offline RL with pre-collected preference da 1. Paired preference data (chosen + rejected)? - Default → `rl: dpo` - - Overfitting → `rl: ipo` + - Overfitting → `rl: dpo, dpo_loss_type: ["ipo"]` - VRAM-limited → `rl: orpo` (no ref model) - Length-sensitive → `rl: simpo` (no ref model) 2. Only binary labels (good/bad)? → `rl: kto` diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 514c1c034f..75d20414c2 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -320,8 +320,10 @@ The input format is a simple JSON input with customizable fields based on the ab As IPO is just DPO with a different loss function, all supported dataset formats for [DPO](#dpo) are also supported for IPO. ```yaml -rl: ipo +rl: dpo +dpo_loss_type: ["ipo"] ``` +*Note:* Passing `rl: ipo` directly is still supported, but will soon be deprecated. ### ORPO diff --git a/src/axolotl/core/trainers/dpo/__init__.py b/src/axolotl/core/trainers/dpo/__init__.py index 7d979e6bf5..a73ec3149e 100644 --- a/src/axolotl/core/trainers/dpo/__init__.py +++ b/src/axolotl/core/trainers/dpo/__init__.py @@ -20,8 +20,16 @@ def get_training_args_class(cls): @classmethod def set_training_args_kwargs(cls, cfg): training_args_kwargs = {} + if cfg.rl is RLType.DPO: + if cfg.dpo_loss_type is not None: + training_args_kwargs["loss_type"] = cfg.dpo_loss_type + + if cfg.dpo_loss_weights is not None: + training_args_kwargs["loss_weights"] = cfg.dpo_loss_weights + if cfg.rl is RLType.IPO: training_args_kwargs["loss_type"] = ["ipo"] + # Label smoothing is not compatible with IPO if cfg.rl is RLType.DPO and cfg.dpo_label_smoothing: training_args_kwargs["label_smoothing"] = cfg.dpo_label_smoothing diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index c762d7f801..3211b7c360 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -309,6 +309,16 @@ class AxolotlInputConfig( dpo_padding_free: bool | None = None + dpo_loss_type: Annotated[list[str], MinLen(1)] | None = Field( + default=None, + json_schema_extra={"description": "List of DPO losses to use."}, + ) + + dpo_loss_weights: Annotated[list[float], MinLen(1)] | None = Field( + default=None, + json_schema_extra={"description": "Weights for each DPO loss."}, + ) + datasets: ( Annotated[ list[ diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 484a1fb47d..9161765d06 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -760,6 +760,40 @@ def check_gdpo(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_dpo(cls, data): + dpo_loss_type = data.get("dpo_loss_type") + dpo_loss_weights = data.get("dpo_loss_weights") + rl = data.get("rl") + + if rl == "ipo": + LOG.warning( + "rl: ipo will soon be deprecated. Use `rl: dpo` with `dpo_loss_type: ['ipo']` instead." + ) + + if rl == "dpo": + if dpo_loss_weights is not None and dpo_loss_type is None: + raise ValueError( + "`dpo_loss_weights` requires `dpo_loss_type` to be set" + ) + if ( + dpo_loss_type is not None + and dpo_loss_weights is not None + and len(dpo_loss_type) != len(dpo_loss_weights) + ): + raise ValueError( + f"`dpo_loss_type` and `dpo_loss_weights` must be the same length, " + f"but got {len(dpo_loss_type)} losses and {len(dpo_loss_weights)} weights" + ) + elif dpo_loss_type is not None or dpo_loss_weights is not None: + raise ValueError( + f"`dpo_loss_type` and `dpo_loss_weights` are for DPO only," + f"but got {rl=}, {dpo_loss_type=} and {dpo_loss_weights=}" + ) + + return data + @model_validator(mode="before") @classmethod def check_grpo_batch_size_divisibility(cls, data): diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index 338d481715..0a4b2ad0bb 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -96,6 +96,8 @@ def fixture_dpo_cfg(base_cfg): "dpo_use_weighting": True, "dpo_label_smoothing": 0.1, "beta": 0.1, # DPO beta + "dpo_loss_type": ["sigmoid", "sft"], + "dpo_loss_weights": [1.0, 0.5], } ) return cfg @@ -164,7 +166,8 @@ def fixture_ipo_cfg(base_cfg): cfg = base_cfg.copy() cfg.update( { - "rl": RLType.IPO, + "rl": RLType.DPO, + "dpo_loss_type": ["ipo"], "dpo_label_smoothing": 0, "beta": 0.1, } @@ -300,6 +303,8 @@ def test_dpo_training_arguments(self, dpo_cfg, model, tokenizer): assert training_arguments.use_weighting is True assert training_arguments.label_smoothing == 0.1 assert training_arguments.precompute_ref_log_probs is True + assert training_arguments.loss_type == ["sigmoid", "sft"] + assert training_arguments.loss_weights == [1.0, 0.5] def test_orpo_training_arguments(self, orpo_cfg, model, tokenizer): builder = HFRLTrainerBuilder(orpo_cfg, model, tokenizer) diff --git a/tests/e2e/test_dpo.py b/tests/e2e/test_dpo.py index 0aca1807cd..24784eb2ca 100644 --- a/tests/e2e/test_dpo.py +++ b/tests/e2e/test_dpo.py @@ -116,6 +116,58 @@ def test_dpo_use_weighting(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) + @with_temp_dir + def test_rpo(self, temp_dir): + # For TRL >= 0.29, loss_type=["sigmoid", "sft"], loss_weights=[1, alpha] + # replaces loss_type="rpo", rpo_alpha=alpha. + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "tokenizer_type": "AutoTokenizer", + "sequence_len": 1024, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 64, + "lora_alpha": 32, + "lora_dropout": 0.1, + "lora_target_linear": True, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "rl": "dpo", + "dpo_loss_type": ["sigmoid", "sft"], + "dpo_loss_weights": [1.0, 1.0], + "datasets": [ + { + "path": "arcee-ai/distilabel-intel-orca-dpo-pairs-binarized", + "type": "chatml.ultra", + "split": "train", + }, + ], + "num_epochs": 1, + "micro_batch_size": 4, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "paged_adamw_8bit", + "lr_scheduler": "cosine", + "max_steps": 20, + "save_steps": 10, + "warmup_steps": 5, + "gradient_checkpointing": True, + "gradient_checkpointing_kwargs": {"use_reentrant": True}, + "save_first_step": False, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + cli_args = TrainerCliArgs() + dataset_meta = load_preference_datasets(cfg=cfg, cli_args=cli_args) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(Path(temp_dir) / "checkpoint-20", cfg) + @pytest.mark.skip("kto_pair no longer supported in trl") @with_temp_dir def test_kto_pair_lora(self, temp_dir): @@ -181,7 +233,8 @@ def test_ipo_lora(self, temp_dir): "special_tokens": { "pad_token": "<|endoftext|>", }, - "rl": "ipo", + "rl": "dpo", + "dpo_loss_type": ["ipo"], "datasets": [ { "path": "arcee-ai/distilabel-intel-orca-dpo-pairs-binarized", From bcbe049c21481d0ca87ae52a0dcbb2f9ba160d6e Mon Sep 17 00:00:00 2001 From: brightwind26 Date: Thu, 23 Apr 2026 08:25:48 +0400 Subject: [PATCH 1280/1405] Feat: add support for datasets with `str` saved `messages` field (#3607) * feat: support datasets saved in str format * add also str for tools * format * fix: address comments + add unit test * format --- .../prompt_strategies/chat_template.py | 27 +++++++- tests/test_datasets.py | 67 +++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index a943a14484..be6a38800e 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -394,8 +394,8 @@ def supports_batched(self) -> bool: def is_prompt_batched(self, prompt: dict[str, Any]) -> bool: try: - return all(isinstance(v, list) for v in prompt.values()) and all( - isinstance(v, list) for v in prompt[self.prompter.field_messages] + return all(isinstance(v, (str, list)) for v in prompt.values()) and all( + isinstance(v, (str, list)) for v in prompt[self.prompter.field_messages] ) except KeyError: return False @@ -1004,6 +1004,13 @@ def _get_tools(self, prompt) -> list[dict] | None: if tools is None: return None + # Some datasets have tools set to str + if isinstance(tools, str): + try: + tools = json.loads(tools) + except json.JSONDecodeError as e: + LOG.error(f"Error parsing tool parameters as JSON. Error: {e}") + raise if isinstance(tools, list): # Process each tool to handle JSON string parameters for tool in tools: @@ -1034,6 +1041,22 @@ def _get_messages(self, prompt): if messages is None: raise ValueError("Messages is null. Please check `field_messages`.") + if isinstance(messages, str): + try: + messages = json.loads(messages) + except json.JSONDecodeError as e: + LOG.error(f"Error parsing messages as JSON. Error: {e}") + raise + assert isinstance(messages, list), ( + f"For SFT datasets that are stored in `str` format, the turns must be saved in a list of dictionaries, got {type(message)}" + ) + + # Extra check here to make sure decoded json is a list of dicts. + for i, message in enumerate(messages): + assert isinstance(message, dict), ( + f"For SFT datasets that are stored in `str` format, each turns must be saved in a dictionary, got {type(message)} for the turn {i}" + ) + if isinstance(messages, list): return messages diff --git a/tests/test_datasets.py b/tests/test_datasets.py index 3b24ad5805..bdb795e136 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -487,3 +487,70 @@ def test_loading_local_dataset_folder(self, tokenizer): assert "attention_mask" in dataset.features assert "labels" in dataset.features shutil.rmtree(tmp_ds_path) + + @enable_hf_offline + def test_load_dataset_with_str_json_data(self, tokenizer): + """ + Test loading datasets where data is stored as str JSON instead of list of dicts. + see: https://github.com/axolotl-ai-cloud/axolotl/pull/3607 for more details. + """ + with tempfile.TemporaryDirectory() as tmp_dir: + import json + + str_json_ds = Dataset.from_list( + [ + { + "messages": json.dumps( + [ + {"role": "user", "content": "Hello how are you?"}, + { + "role": "assistant", + "content": "I am doing good thanks", + }, + ] + ) + }, + { + "messages": json.dumps( + [ + {"role": "user", "content": "What is 2+2?"}, + {"role": "assistant", "content": "2+2 equals 4."}, + ] + ) + }, + ] + ) + + tmp_ds_path = Path(tmp_dir) / "str_json_dataset.parquet" + str_json_ds.to_parquet(tmp_ds_path) + + prepared_path = Path(tmp_dir) / "prepared" + cfg = DictDefault( + { + "tokenizer_config": "huggyllama/llama-7b", + "sequence_len": 512, + "datasets": [ + { + "path": str(tmp_ds_path), + "name": "test_str_json", + "type": "chat_template", + "field_messages": "messages", + "message_field_role": "role", + "message_field_content": "content", + }, + ], + "dataset_num_proc": 4, + } + ) + + with patch( + "axolotl.common.const.DEFAULT_DATASET_PREPARED_PATH", str(prepared_path) + ): + dataset, _ = _load_tokenized_prepared_datasets(tokenizer, cfg) + + assert len(dataset) == 2 + assert "input_ids" in dataset.features + assert "attention_mask" in dataset.features + assert "labels" in dataset.features + + assert len(dataset[0]["input_ids"]) > 0 From 1bf65c500edd49899c378c157f0194a51f50ba23 Mon Sep 17 00:00:00 2001 From: thad0ctor Date: Wed, 22 Apr 2026 21:26:34 -0700 Subject: [PATCH 1281/1405] feat: add processor_kwargs YAML field forwarded to from_pretrained (#3612) --- src/axolotl/loaders/processor.py | 2 + src/axolotl/utils/schemas/model.py | 22 +++++ src/axolotl/utils/schemas/validation.py | 5 ++ tests/test_revision_parameter.py | 105 ++++++++++++++++++++++++ 4 files changed, 134 insertions(+) diff --git a/src/axolotl/loaders/processor.py b/src/axolotl/loaders/processor.py index 211c260608..4cb03b84db 100644 --- a/src/axolotl/loaders/processor.py +++ b/src/axolotl/loaders/processor.py @@ -23,6 +23,8 @@ def load_processor(cfg: DictDefault, tokenizer: PreTrainedTokenizerBase): processor_kwargs = {} if cfg.revision_of_model: processor_kwargs["revision"] = cfg.revision_of_model + if cfg.processor_kwargs: + processor_kwargs.update(cfg.processor_kwargs) if cfg.tokenizer_use_mistral_common: diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index 3c5dfc6e33..f54958b330 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -64,6 +64,12 @@ class ModelInputConfig(BaseModel): processor_type: str | None = Field( default=None, json_schema_extra={"description": "transformers processor class"} ) + processor_kwargs: dict[str, Any] | None = Field( + default=None, + json_schema_extra={ + "description": "kwargs forwarded to the processor's from_pretrained(), overriding processor config (e.g. image_seq_length, min_pixels, etc.)." + }, + ) tokenizer_save_jinja_files: bool | None = Field( default=True, # match the default behavior from transformers json_schema_extra={ @@ -107,6 +113,22 @@ def hint_trust_remote_code(cls, trust_remote_code): ) return trust_remote_code + @field_validator("processor_kwargs") + @classmethod + def reject_reserved_processor_kwargs(cls, processor_kwargs): + if not processor_kwargs: + return processor_kwargs + reserved = {"revision", "trust_remote_code"} + conflicts = reserved.intersection(processor_kwargs) + if conflicts: + raise ValueError( + "Do not set reserved keys " + f"{sorted(conflicts)} inside `processor_kwargs`; " + "use the top-level `revision_of_model` / `trust_remote_code` " + "config keys instead." + ) + return processor_kwargs + class ModelOutputConfig(BaseModel): """model save configuration subset""" diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 9161765d06..fff69de260 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -578,6 +578,11 @@ def check_mistral_common_incompatible_options(cls, data): "Setting chat_template is not supported with mistral-common tokenizer" ) + if data.get("processor_kwargs"): + raise ValueError( + "processor_kwargs is not supported with mistral-common tokenizer" + ) + return data @model_validator(mode="before") diff --git a/tests/test_revision_parameter.py b/tests/test_revision_parameter.py index 2116b223f7..112badfb80 100644 --- a/tests/test_revision_parameter.py +++ b/tests/test_revision_parameter.py @@ -133,3 +133,108 @@ def test_load_processor_omits_revision_when_unset(self, mock_auto_processor): call_kwargs = mock_auto_processor.from_pretrained.call_args assert "revision" not in call_kwargs.kwargs + + @patch("axolotl.loaders.processor.AutoProcessor") + def test_load_processor_forwards_processor_kwargs(self, mock_auto_processor): + mock_processor = MagicMock() + mock_processor.size = {} + mock_auto_processor.from_pretrained.return_value = mock_processor + + cfg = DictDefault( + { + "processor_config": "some-model", + "trust_remote_code": False, + "processor_kwargs": { + "image_seq_length": 1120, + "max_soft_tokens": 1120, + }, + } + ) + tokenizer = MagicMock(spec=PreTrainedTokenizerBase) + + from axolotl.loaders.processor import load_processor + + load_processor(cfg, tokenizer) + + call_kwargs = mock_auto_processor.from_pretrained.call_args + assert call_kwargs.kwargs.get("image_seq_length") == 1120 + assert call_kwargs.kwargs.get("max_soft_tokens") == 1120 + + @patch("axolotl.loaders.processor.AutoProcessor") + def test_load_processor_omits_processor_kwargs_when_unset( + self, mock_auto_processor + ): + mock_processor = MagicMock() + mock_processor.size = {} + mock_auto_processor.from_pretrained.return_value = mock_processor + + cfg = DictDefault( + { + "processor_config": "some-model", + "trust_remote_code": False, + } + ) + tokenizer = MagicMock(spec=PreTrainedTokenizerBase) + + from axolotl.loaders.processor import load_processor + + load_processor(cfg, tokenizer) + + call_kwargs = mock_auto_processor.from_pretrained.call_args + assert "image_seq_length" not in call_kwargs.kwargs + assert "max_soft_tokens" not in call_kwargs.kwargs + + def test_processor_kwargs_schema_rejects_revision(self): + import pytest + + from axolotl.utils.schemas.model import ModelInputConfig + + with pytest.raises(ValueError, match="revision"): + ModelInputConfig( + base_model="some-model", + processor_kwargs={"revision": "abc123"}, + ) + + def test_processor_kwargs_schema_rejects_trust_remote_code(self): + import pytest + + from axolotl.utils.schemas.model import ModelInputConfig + + with pytest.raises(ValueError, match="trust_remote_code"): + ModelInputConfig( + base_model="some-model", + processor_kwargs={"trust_remote_code": True}, + ) + + def test_processor_kwargs_schema_accepts_valid_keys(self): + from axolotl.utils.schemas.model import ModelInputConfig + + cfg = ModelInputConfig( + base_model="some-model", + processor_kwargs={"image_seq_length": 1120, "max_soft_tokens": 1120}, + ) + assert cfg.processor_kwargs == { + "image_seq_length": 1120, + "max_soft_tokens": 1120, + } + + def test_processor_kwargs_schema_accepts_none_and_empty(self): + from axolotl.utils.schemas.model import ModelInputConfig + + assert ModelInputConfig(base_model="x").processor_kwargs is None + assert ( + ModelInputConfig(base_model="x", processor_kwargs={}).processor_kwargs == {} + ) + + def test_processor_kwargs_incompatible_with_mistral_common(self, min_base_cfg): + import pytest + + from axolotl.utils.config import validate_config + from axolotl.utils.dict import DictDefault + + cfg = min_base_cfg | DictDefault( + tokenizer_use_mistral_common=True, + processor_kwargs={"image_seq_length": 1120}, + ) + with pytest.raises(ValueError, match="processor_kwargs"): + validate_config(cfg) From 901f2356bc6c80ad1b0fb8ce77a64958dc78a552 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 23 Apr 2026 14:49:52 -0400 Subject: [PATCH 1282/1405] dpo collation/padding (#3601) [skip ci] * fix dpo collation/padding * fix DPO collator encoder-decoder pixel_values dtype and is_encoder_decoder detection - Use float32 instead of LongTensor for _pixel_values in encoder-decoder branch - Add missing padding_value case for _pixel_values in encoder-decoder branch - Derive is_encoder_decoder from model config instead of hardcoding False --- src/axolotl/core/builders/causal.py | 2 +- src/axolotl/core/builders/rl.py | 38 +++++++ src/axolotl/monkeypatch/trainer/utils.py | 5 +- src/axolotl/utils/collators/__init__.py | 2 + src/axolotl/utils/collators/dpo.py | 128 +++++++++++++++++++++++ src/axolotl/utils/schemas/config.py | 6 ++ 6 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 src/axolotl/utils/collators/dpo.py diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index f26ef8969e..fe832dd452 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -370,7 +370,7 @@ def build(self, total_num_steps): data_collator_kwargs = { "padding": True, # True/"longest" is the default } - multiple = 64 + multiple = getattr(self.cfg, "pad_to_multiple_of", None) or 64 if self.cfg.pad_to_sequence_len: data_collator_kwargs["pad_to_multiple_of"] = multiple * math.ceil( self.cfg.sequence_len / multiple diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index 447b64eb8a..98296a2016 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -228,9 +228,47 @@ def _build_training_arguments(self, total_num_steps): return training_args, trainer_kwargs + def build_collator(self, **kwargs): + """Build a data collator for preference-tuning trainers. + + Returns None for RL types that provide their own collator (e.g. GRPO, + KTO), letting the trainer construct its default. For DPO/IPO/ORPO/SIMPO + returns an ``AxolotlDPODataCollatorWithPadding`` when + ``pad_to_multiple_of`` is set, otherwise None (so the trainer + falls back to the TRL default). + """ + if self.cfg.rl not in ( + RLType.DPO, + RLType.IPO, + RLType.ORPO, + RLType.SIMPO, + ): + return None + + pad_to_multiple_of = getattr(self.cfg, "pad_to_multiple_of", None) + if not pad_to_multiple_of: + return None + + from axolotl.utils.collators.dpo import AxolotlDPODataCollatorWithPadding + + LOG.info( + f"Using AxolotlDPODataCollatorWithPadding with pad_to_multiple_of=" + f"{pad_to_multiple_of}" + ) + is_enc_dec = getattr(self.model.config, "is_encoder_decoder", False) + return AxolotlDPODataCollatorWithPadding( + pad_token_id=self.tokenizer.pad_token_id, + is_encoder_decoder=is_enc_dec, + pad_to_multiple_of=pad_to_multiple_of, + **kwargs, + ) + def build(self, total_num_steps): training_args, trainer_kwargs = self._build_training_arguments(total_num_steps) + if (data_collator := self.build_collator()) is not None: + trainer_kwargs["data_collator"] = data_collator + if self.eval_dataset: trainer_kwargs["eval_dataset"] = self.eval_dataset if ( diff --git a/src/axolotl/monkeypatch/trainer/utils.py b/src/axolotl/monkeypatch/trainer/utils.py index 467f50a5a4..051eda2708 100644 --- a/src/axolotl/monkeypatch/trainer/utils.py +++ b/src/axolotl/monkeypatch/trainer/utils.py @@ -407,7 +407,10 @@ def selective_log_softmax(logits, index) -> torch.Tensor: K = index.shape[-1] original_index_shape = index.shape - flat_logits = logits.reshape(-1, V).contiguous() + try: + flat_logits = logits.view(-1, V) + except RuntimeError: + flat_logits = logits.reshape(-1, V).contiguous() flat_index = index.reshape(-1, K).contiguous() BLOCK_V = 4096 diff --git a/src/axolotl/utils/collators/__init__.py b/src/axolotl/utils/collators/__init__.py index d5e6ad17d1..fdc030a2c8 100644 --- a/src/axolotl/utils/collators/__init__.py +++ b/src/axolotl/utils/collators/__init__.py @@ -6,6 +6,7 @@ PretrainingBatchSamplerDataCollatorForSeq2Seq, V2BatchSamplerDataCollatorForSeq2Seq, ) +from .dpo import AxolotlDPODataCollatorWithPadding from .mamba import MambaDataCollator __all__ = [ @@ -13,5 +14,6 @@ "BatchSamplerDataCollatorForSeq2Seq", "V2BatchSamplerDataCollatorForSeq2Seq", "PretrainingBatchSamplerDataCollatorForSeq2Seq", + "AxolotlDPODataCollatorWithPadding", "MambaDataCollator", ] diff --git a/src/axolotl/utils/collators/dpo.py b/src/axolotl/utils/collators/dpo.py new file mode 100644 index 0000000000..6f10188c09 --- /dev/null +++ b/src/axolotl/utils/collators/dpo.py @@ -0,0 +1,128 @@ +"""DPO/ORPO/IPO/KTO data collator with pad_to_multiple_of support. + +Extends TRL's DPODataCollatorWithPadding to round padded sequence lengths +up to a fixed multiple. This stabilizes Triton autotune caches for kernels +that key on sequence length (e.g. fla's linear attention kernels used by +Qwen3.5), which otherwise re-autotune on every distinct batch length. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import torch +from torch.nn.utils.rnn import pad_sequence +from trl.experimental.utils import DPODataCollatorWithPadding +from trl.trainer.utils import pad + + +def _round_up(length: int, multiple: int) -> int: + return ((length + multiple - 1) // multiple) * multiple + + +@dataclass +class AxolotlDPODataCollatorWithPadding(DPODataCollatorWithPadding): + """DPO data collator that pads to a multiple of ``pad_to_multiple_of``. + + Args: + pad_token_id: Tokenizer pad token id (inherited). + is_encoder_decoder: Whether the model is encoder-decoder (inherited). + pad_to_multiple_of: If set, padded lengths are rounded up to this + multiple. Helps stabilize Triton autotune caches. + """ + + pad_to_multiple_of: int | None = None + + def __call__(self, features: list[dict[str, Any]]) -> dict[str, Any]: + pad_to_mult = self.pad_to_multiple_of + + padded_batch: dict[str, Any] = {} + for k in features[0].keys(): + if k.endswith( + ("_input_ids", "_attention_mask", "_labels", "_pixel_values") + ): + if self.is_encoder_decoder: + if k.endswith("_pixel_values"): + to_pad = [ + torch.tensor(ex[k], dtype=torch.float32) for ex in features + ] + else: + to_pad = [torch.LongTensor(ex[k]) for ex in features] + + if k.startswith("prompt") and k.endswith("input_ids"): + if self.pad_token_id is None: + raise ValueError( + "Padding is enabled, but the tokenizer is not configured with a padding token." + ) + padding_value = self.pad_token_id + elif k.endswith("_attention_mask"): + padding_value = 0 + elif k.endswith("_pixel_values"): + padding_value = 0 + elif ( + k.startswith(("chosen", "rejected", "completion")) + or "decoder" in k + ): + padding_value = -100 + else: + raise ValueError(f"Unexpected key in batch '{k}'") + + padded = pad_sequence( + to_pad, batch_first=True, padding_value=padding_value + ) + if pad_to_mult: + cur = padded.shape[1] + target = _round_up(cur, pad_to_mult) + if target > cur: + extra = target - cur + pad_shape = list(padded.shape) + pad_shape[1] = extra + filler = torch.full( + pad_shape, + padding_value, + dtype=padded.dtype, + device=padded.device, + ) + padded = torch.cat([padded, filler], dim=1) + padded_batch[k] = padded + else: + if k.endswith("_input_ids"): + if self.pad_token_id is None: + raise ValueError( + "Padding is enabled, but the tokenizer is not configured with a padding token." + ) + padding_value = self.pad_token_id + elif k.endswith("_labels"): + padding_value = -100 + elif k.endswith("_attention_mask"): + padding_value = 0 + elif k.endswith("_pixel_values"): + padding_value = 0 + else: + raise ValueError(f"Unexpected key in batch '{k}'") + + padding_side = ( + "left" + if k in ("prompt_input_ids", "prompt_attention_mask") + else "right" + ) + + dtype = ( + torch.float32 if k.endswith("_pixel_values") else torch.int64 + ) + to_pad = [torch.tensor(ex[k], dtype=dtype) for ex in features] + + # trl.pad() natively supports pad_to_multiple_of + padded_batch[k] = pad( + to_pad, + padding_value=padding_value, + padding_side=padding_side, + pad_to_multiple_of=pad_to_mult, + ) + elif k.endswith("_logps"): + padded_batch[k] = torch.tensor([ex[k] for ex in features]) + else: + padded_batch[k] = [ex[k] for ex in features] + + return padded_batch diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 3211b7c360..6ee672c8cd 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -673,6 +673,12 @@ class AxolotlInputConfig( "description": "Pad inputs so each step uses constant sized buffers. This will reduce memory fragmentation and may prevent OOMs, by re-using memory more efficiently. Defaults to True if `sample_packing` enabled" }, ) + pad_to_multiple_of: int | None = Field( + default=None, + json_schema_extra={ + "description": ("Pad each batch to a multiple of this value.") + }, + ) curriculum_sampling: bool | None = Field( default=None, json_schema_extra={ From 17fc747f99e24eeb7cb295333bf7026e0d5f2bdd Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 24 Apr 2026 14:23:09 +0700 Subject: [PATCH 1283/1405] fix: docker build failing (#3622) * fix: uv leftover docs * fix: docker build failing * chore: doc * fix: remove old pytorch build * fix: stop recommend flash-attn optional, let transformers pull * fix: remove ring flash attention from image * fix: quotes [skip ci] * chore: naming [skip ci] --- .github/CONTRIBUTING.md | 5 +++-- .github/workflows/base.yml | 16 ---------------- .github/workflows/main.yml | 12 ------------ VERSION | 2 +- docker/Dockerfile | 4 ++-- docker/Dockerfile-base | 16 ---------------- docker/Dockerfile-tests | 4 ++-- docker/Dockerfile-uv | 4 ++-- docker/Dockerfile-uv-base | 17 ----------------- docs/debugging.qmd | 6 ++++-- docs/docker.qmd | 17 ++++++++++++----- docs/faq.qmd | 2 +- docs/installation.qmd | 19 +++++++++---------- examples/LiquidAI/README.md | 2 +- examples/apertus/README.md | 4 ++-- examples/arcee/README.md | 4 ++-- .../colab-axolotl-example.ipynb | 7 +------ examples/devstral/README.md | 4 ++-- examples/gemma3n/README.md | 4 ++-- examples/gpt-oss/README.md | 4 ++-- examples/granite4/README.md | 4 ++-- examples/hunyuan/README.md | 4 ++-- examples/magistral/README.md | 4 ++-- examples/seed-oss/README.md | 2 +- examples/smolvlm2/README.md | 2 +- examples/voxtral/README.md | 4 ++-- pyproject.toml | 4 ++-- src/axolotl/integrations/kd/README.md | 2 +- src/axolotl/utils/schemas/config.py | 2 +- 29 files changed, 62 insertions(+), 119 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6bb6a6b8f8..6494ec5f00 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -31,10 +31,11 @@ PRs are **greatly welcome**! Please run below to setup env ```bash -# Install axolotl + dev and test dependencies from lockfile +# Install axolotl + dev and test dependencies export UV_TORCH_BACKEND=cu128 # or cu130 -uv sync --extra flash-attn --extra deepspeed --group dev --group test +uv venv --no-project --relocatable source .venv/bin/activate +uv pip install --no-build-isolation -e '.[deepspeed]' --group dev --group test pre-commit install # test diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 521d26201f..8ed89c5db5 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -30,14 +30,6 @@ jobs: fail-fast: false matrix: include: - - cuda: "128" - cuda_version: 12.8.1 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.9.0 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-base" - platforms: "linux/amd64,linux/arm64" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" @@ -168,14 +160,6 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" - - cuda: "128" - cuda_version: 12.8.1 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.9.0 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-uv-base" - platforms: "linux/amd64,linux/arm64" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1fb6290d9a..b5c575d336 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,12 +18,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.9.0 - axolotl_extras: - platforms: "linux/amd64,linux/arm64" - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" @@ -180,12 +174,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.9.0 - axolotl_extras: - platforms: "linux/amd64,linux/arm64" - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" diff --git a/VERSION b/VERSION index cb27aa17b7..b08b47558c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.16.0.dev0 +0.16.2.dev0 diff --git a/docker/Dockerfile b/docker/Dockerfile index 2bdb45b5c2..3fdd0e7aa7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -24,9 +24,9 @@ WORKDIR /workspace/axolotl # If AXOLOTL_EXTRAS is set, append it in brackets; don't install deepspeed with arm64 RUN pip uninstall -y causal_conv1d RUN if [ "$TARGETARCH" = "arm64" ]; then \ - BASE_EXTRAS="flash-attn,ring-flash-attn,optimizers,ray"; \ + BASE_EXTRAS="optimizers,ray"; \ else \ - BASE_EXTRAS="deepspeed,flash-attn,ring-flash-attn,optimizers,ray"; \ + BASE_EXTRAS="deepspeed,optimizers,ray"; \ fi && \ if [ "$AXOLOTL_EXTRAS" != "" ]; then \ pip install --no-build-isolation -e .[$BASE_EXTRAS,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 70a62ee3a1..08a5ddccd2 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -58,19 +58,3 @@ RUN git lfs install --skip-repo && \ # The base image ships with `pydantic==1.8.2` which is not working pip3 install -U --no-cache-dir pydantic==1.10.10 && \ pip3 cache purge - -# Map Python version (e.g., 3.12 -> cp312) -RUN PYTHON_CP="cp$(echo $PYTHON_VERSION | tr -d '.')" && \ - # Map PyTorch version (e.g., 2.9.1 -> torch2.9, 2.10.0 -> torch2.10) - TORCH_TAG="torch$(echo $PYTORCH_VERSION | grep -oP '^\d+\.\d+')" && \ - # Map architecture - case "$TARGETARCH" in \ - amd64) ARCH_TAG="x86_64" ;; \ - arm64) ARCH_TAG="aarch64" ;; \ - *) echo "Unsupported architecture: $TARGETARCH"; exit 1 ;; \ - esac && \ - WHL_VERSION="v0.7.16" && \ - WHL_FILE="flash_attn-2.8.3+cu${CUDA}${TORCH_TAG}-${PYTHON_CP}-${PYTHON_CP}-linux_${ARCH_TAG}.whl" && \ - wget -nv "https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/${WHL_VERSION}/${WHL_FILE}" && \ - pip3 install --no-cache-dir "${WHL_FILE}" && \ - rm "${WHL_FILE}" diff --git a/docker/Dockerfile-tests b/docker/Dockerfile-tests index 8d97343597..263abbc84b 100644 --- a/docker/Dockerfile-tests +++ b/docker/Dockerfile-tests @@ -24,9 +24,9 @@ RUN git fetch origin +$GITHUB_REF && \ # If AXOLOTL_EXTRAS is set, append it in brackets RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,mamba-ssm,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,mamba-ssm,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - pip install --no-build-isolation -e .[deepspeed,flash-attn,mamba-ssm] $AXOLOTL_ARGS; \ + pip install --no-build-isolation -e .[deepspeed,mamba-ssm] $AXOLOTL_ARGS; \ fi # So we can test the Docker image diff --git a/docker/Dockerfile-uv b/docker/Dockerfile-uv index df058baa3e..582a2a0cf2 100644 --- a/docker/Dockerfile-uv +++ b/docker/Dockerfile-uv @@ -24,9 +24,9 @@ WORKDIR /workspace/axolotl # If AXOLOTL_EXTRAS is set, append it in brackets; don't install deepspeed with arm64 RUN uv pip uninstall causal_conv1d RUN if [ "$TARGETARCH" = "arm64" ]; then \ - BASE_EXTRAS="flash-attn,ring-flash-attn,optimizers,ray"; \ + BASE_EXTRAS="optimizers,ray"; \ else \ - BASE_EXTRAS="deepspeed,flash-attn,ring-flash-attn,optimizers,ray"; \ + BASE_EXTRAS="deepspeed,optimizers,ray"; \ fi && \ if [ "$AXOLOTL_EXTRAS" != "" ]; then \ uv pip install --no-build-isolation -e .[$BASE_EXTRAS,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ diff --git a/docker/Dockerfile-uv-base b/docker/Dockerfile-uv-base index f16777378b..c5a2ceb8cc 100644 --- a/docker/Dockerfile-uv-base +++ b/docker/Dockerfile-uv-base @@ -38,20 +38,3 @@ RUN uv pip install packaging setuptools wheel psutil \ RUN if [ "$TARGETARCH" = "amd64" ]; then \ MAMBA_SKIP_CUDA_BUILD=TRUE CAUSAL_CONV1D_SKIP_CUDA_BUILD=TRUE uv pip install --no-build-isolation mamba_ssm causal_conv1d; \ fi - -# Map Python version (e.g., 3.12 -> cp312) -RUN PYTHON_CP="cp$(echo $PYTHON_VERSION | tr -d '.')" && \ - # Map PyTorch version (e.g., 2.9.1 -> torch2.9, 2.10.0 -> torch2.10) - TORCH_TAG="torch$(echo $PYTORCH_VERSION | grep -oP '^\d+\.\d+')" && \ - LINUX_TAG="manylinux_" && \ - # Map architecture - case "$TARGETARCH" in \ - amd64) ARCH_TAG="2_24_x86_64.manylinux_2_28_x86_64" ;; \ - arm64) ARCH_TAG="2_34_aarch64" ;; \ - *) echo "Unsupported architecture: $TARGETARCH"; exit 1 ;; \ - esac && \ - WHL_VERSION="v0.7.16" && \ - WHL_FILE="flash_attn-2.8.3+cu${CUDA}${TORCH_TAG}-${PYTHON_CP}-${PYTHON_CP}-${LINUX_TAG}${ARCH_TAG}.whl" && \ - wget -nv "https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/${WHL_VERSION}/${WHL_FILE}" && \ - uv pip install --no-cache-dir "${WHL_FILE}" && \ - rm "${WHL_FILE}" diff --git a/docs/debugging.qmd b/docs/debugging.qmd index f3ca6ad9a9..6b76bde944 100644 --- a/docs/debugging.qmd +++ b/docs/debugging.qmd @@ -77,8 +77,9 @@ Make sure you have an [editable install](https://setuptools.pypa.io/en/latest/us ```bash export UV_TORCH_BACKEND=cu128 # or cu130 -uv sync --extra flash-attn --extra deepspeed --group dev --group test +uv venv --no-project --relocatable source .venv/bin/activate +uv pip install --no-build-isolation -e '.[deepspeed]' --group dev --group test ``` #### Remote Hosts @@ -218,8 +219,9 @@ docker run --privileged --gpus '"all"' --shm-size 10g --rm -it --name axolotl -- You will now be in the container. Next, install Axolotl with dev dependencies: ```bash -uv sync --extra flash-attn --extra deepspeed --group dev --group test +uv venv --no-project --relocatable source .venv/bin/activate +uv pip install --no-build-isolation -e '.[deepspeed]' --group dev --group test ``` ### Attach To Container diff --git a/docs/docker.qmd b/docs/docker.qmd index 001cf19a7e..7ce0418214 100644 --- a/docs/docker.qmd +++ b/docs/docker.qmd @@ -13,10 +13,17 @@ This section describes the different Docker images that are released by AxolotlA For Blackwell GPUs, please use the tags with PyTorch 2.9.1 and CUDA 12.8. ::: -::: {.callout-tip} -Each image below is available in a **uv variant** that uses [uv](https://docs.astral.sh/uv/) with -a relocatable venv (`/workspace/axolotl-venv`) instead of Miniconda + pip. Append `-uv` to the image name -(e.g. `axolotlai/axolotl-base-uv`). Tags follow the same format. We recommend the uv images for new deployments. +::: {.callout-important} +### Switch to the `-uv` images + +Each image below ships a **uv variant** that uses [uv](https://docs.astral.sh/uv/) with a relocatable venv +(`/workspace/axolotl-venv`) instead of Miniconda + pip. Append `-uv` to the image name +(e.g. `axolotlai/axolotl-uv`, `axolotlai/axolotl-base-uv`, `axolotlai/axolotl-cloud-uv`). Tags follow the +same format as their non-uv counterparts. + +**We recommend switching to the `-uv` images early.** In the near future we will publish the uv-based +build to the non-uv tags as well. The non-uv names will continue to work, but they will start serving +the uv image. ::: ## Base @@ -85,7 +92,7 @@ Tags examples: - `main-py3.12-cu130-2.10.0` - `main-latest` - `main-20260315-py3.11-cu128-2.9.1` -- `0.12.0` +- `0.16.1` ## Cloud diff --git a/docs/faq.qmd b/docs/faq.qmd index 92b432f2d2..9c1b81c3f2 100644 --- a/docs/faq.qmd +++ b/docs/faq.qmd @@ -57,7 +57,7 @@ description: Frequently asked questions **Q: vLLM is not working with Axolotl** -> A: We currently recommend torch 2.6.0 for use with `vllm`. Please ensure you use the right version. For Docker, please use the `main-py3.11-cu124-2.6.0` tag. +> A: We currently recommend torch 2.10 for use with `vllm`. Please ensure you use the right version. For Docker, please use the `main-py3.12-cu128-2.10.0` tag (note: torch 2.10 images are built with Python 3.12). **Q: FA2 2.8.0 `undefined symbol` runtime error on CUDA 12.4** diff --git a/docs/installation.qmd b/docs/installation.qmd index 9d1d0d4a14..f7c780740d 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -15,7 +15,7 @@ This guide covers all the ways you can install and set up Axolotl for your envir - NVIDIA GPU (Ampere architecture or newer for `bf16` and Flash Attention) or AMD GPU - Python ≥3.11 -- PyTorch ≥2.9.0 +- PyTorch ≥2.9.1 ## Installation {#sec-installation} @@ -36,9 +36,9 @@ source $HOME/.local/bin/env Choose your CUDA version (e.g. `cu128`, `cu130`), create a venv, and install: ```{.bash} export UV_TORCH_BACKEND=cu128 # or cu130 -uv venv --no-project --relocatable +uv venv source .venv/bin/activate -uv pip install --no-build-isolation axolotl[flash-attn,deepspeed] +uv pip install --no-build-isolation axolotl[deepspeed] ``` ### Edge/Development Build {#sec-edge-build} @@ -49,12 +49,11 @@ For the latest features between releases: git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl export UV_TORCH_BACKEND=cu128 # or cu130 -uv sync --extra flash-attn --extra deepspeed +uv venv source .venv/bin/activate +uv pip install --no-build-isolation -e '.[deepspeed]' ``` -`uv sync` creates a `.venv`, installs exact pinned versions from `uv.lock`, and sets up an editable install automatically. - ### Docker {#sec-docker} ```{.bash} @@ -132,11 +131,11 @@ source $HOME/.local/bin/env # Create a fresh venv (recommended for a clean start) export UV_TORCH_BACKEND=cu128 # or cu130 -uv venv --no-project --relocatable +uv venv source .venv/bin/activate # Reinstall axolotl -uv pip install --no-build-isolation axolotl[flash-attn,deepspeed] +uv pip install --no-build-isolation axolotl[deepspeed] ``` ## Using pip (Alternative) {#sec-pip} @@ -151,13 +150,13 @@ Follow the instructions at: [https://pytorch.org/get-started/locally/](https://p ```{.bash} pip3 install -U packaging setuptools wheel ninja -pip3 install --no-build-isolation axolotl[flash-attn,deepspeed] +pip3 install --no-build-isolation axolotl[deepspeed] ``` For editable/development installs: ```{.bash} pip3 install -U packaging setuptools wheel ninja -pip3 install --no-build-isolation -e '.[flash-attn,deepspeed]' +pip3 install --no-build-isolation -e '.[deepspeed]' ``` ## Troubleshooting {#sec-troubleshooting} diff --git a/examples/LiquidAI/README.md b/examples/LiquidAI/README.md index 0a08692d76..9ac637e33e 100644 --- a/examples/LiquidAI/README.md +++ b/examples/LiquidAI/README.md @@ -15,7 +15,7 @@ Thanks to the team at LiquidAI for giving us early access to prepare for these r Here is an example of how to install from pip: ```bash # Ensure you have a compatible version of Pytorch installed - uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' + uv pip install --no-build-isolation 'axolotl>=0.16.1' ``` 2. Run one of the finetuning examples below. diff --git a/examples/apertus/README.md b/examples/apertus/README.md index 1280e430a2..9ff2d89924 100644 --- a/examples/apertus/README.md +++ b/examples/apertus/README.md @@ -11,11 +11,11 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations Here is an example of how to install from main for pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0 min) +# Ensure you have Pytorch installed (Pytorch 2.9.1 min) git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -uv pip install --no-build-isolation -e '.[flash-attn]' +uv pip install --no-build-isolation -e '.' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy python scripts/cutcrossentropy_install.py | sh diff --git a/examples/arcee/README.md b/examples/arcee/README.md index deaea676a1..5296e022e3 100644 --- a/examples/arcee/README.md +++ b/examples/arcee/README.md @@ -13,11 +13,11 @@ Thanks to the team at Arcee.ai for using Axolotl in supervised fine-tuning the A Here is an example of how to install from main for pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0 min) +# Ensure you have Pytorch installed (Pytorch 2.9.1 min) git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -uv pip install --no-build-isolation -e '.[flash-attn]' +uv pip install --no-build-isolation -e '.' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy python scripts/cutcrossentropy_install.py | sh diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index c7b2b8e5bf..bbc75104c1 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -36,12 +36,7 @@ "id": "msOCO4NRmRLa" }, "outputs": [], - "source": [ - "%%capture\n", - "# This step can take ~5-10 minutes to install dependencies\n", - "!pip install --no-build-isolation axolotl[flash-attn]>=0.9.1\n", - "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fec1a88\"" - ] + "source": "%%capture\n# This step can take ~5-10 minutes to install dependencies\n!pip install --no-build-isolation \"axolotl>=0.16.1\"\n!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fec1a88\"" }, { "cell_type": "markdown", diff --git a/examples/devstral/README.md b/examples/devstral/README.md index 2be8f62921..9fbe1d4fcb 100644 --- a/examples/devstral/README.md +++ b/examples/devstral/README.md @@ -15,8 +15,8 @@ Thanks to the team at MistralAI for giving us early access to prepare for this r Here is an example of how to install from pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0 min) -uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' +# Ensure you have Pytorch installed (Pytorch 2.9.1 min) +uv pip install --no-build-isolation 'axolotl>=0.16.1' ``` 2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage diff --git a/examples/gemma3n/README.md b/examples/gemma3n/README.md index 1ecc96cbc0..db265d7581 100644 --- a/examples/gemma3n/README.md +++ b/examples/gemma3n/README.md @@ -9,8 +9,8 @@ Gemma-3n is a family of multimodal models from Google found on [HuggingFace](htt Here is an example of how to install from pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0 min) -uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' +# Ensure you have Pytorch installed (Pytorch 2.9.1 min) +uv pip install --no-build-isolation 'axolotl>=0.16.1' ``` 2. In addition to Axolotl's requirements, Gemma-3n requires: diff --git a/examples/gpt-oss/README.md b/examples/gpt-oss/README.md index 0e5eac5009..ae71eec1ef 100644 --- a/examples/gpt-oss/README.md +++ b/examples/gpt-oss/README.md @@ -13,8 +13,8 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations Here is an example of how to install from pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0 min) -uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' +# Ensure you have Pytorch installed (Pytorch 2.9.1 min) +uv pip install --no-build-isolation 'axolotl>=0.16.1' ``` 2. Choose one of the following configs below for training the 20B model. (for 120B, see [below](#training-120b)) diff --git a/examples/granite4/README.md b/examples/granite4/README.md index ceb599c1ca..85b6621a0b 100644 --- a/examples/granite4/README.md +++ b/examples/granite4/README.md @@ -11,11 +11,11 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations Here is an example of how to install from main for pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.7.1 min) +# Ensure you have Pytorch installed (Pytorch 2.9.1 min) git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -uv pip install --no-build-isolation -e '.[flash-attn]' +uv pip install --no-build-isolation -e '.' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy python scripts/cutcrossentropy_install.py | sh diff --git a/examples/hunyuan/README.md b/examples/hunyuan/README.md index 3071a0a61e..d17752cc48 100644 --- a/examples/hunyuan/README.md +++ b/examples/hunyuan/README.md @@ -9,11 +9,11 @@ Tencent released a family of opensource models called HunYuan with varying param Here is an example of how to install from main for pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0 min) +# Ensure you have Pytorch installed (Pytorch 2.9.1 min) git clone https://github.com/axolotl-ai-cloud/axolotl.git cd axolotl -uv pip install --no-build-isolation -e '.[flash-attn]' +uv pip install --no-build-isolation -e '.' # Install CCE https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy python scripts/cutcrossentropy_install.py | sh diff --git a/examples/magistral/README.md b/examples/magistral/README.md index 172a40b2c9..5cce33ea77 100644 --- a/examples/magistral/README.md +++ b/examples/magistral/README.md @@ -13,8 +13,8 @@ Thanks to the team at MistralAI for giving us early access to prepare for these Here is an example of how to install from pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.7.0 min) -uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' +# Ensure you have Pytorch installed (Pytorch 2.9.1 min) +uv pip install --no-build-isolation 'axolotl>=0.16.1' ``` 2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage diff --git a/examples/seed-oss/README.md b/examples/seed-oss/README.md index 796ef118d0..d5d4baa895 100644 --- a/examples/seed-oss/README.md +++ b/examples/seed-oss/README.md @@ -11,7 +11,7 @@ This guide shows how to fine-tune it with Axolotl with multi-turn conversations Here is an example of how to install from pip: ```bash # Ensure you have a compatible version of Pytorch installed - uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' + uv pip install --no-build-isolation 'axolotl>=0.16.1' # Install Cut Cross Entropy python scripts/cutcrossentropy_install.py | sh diff --git a/examples/smolvlm2/README.md b/examples/smolvlm2/README.md index da83e612c6..01ee7fa623 100644 --- a/examples/smolvlm2/README.md +++ b/examples/smolvlm2/README.md @@ -13,7 +13,7 @@ This guide shows how to fine-tune SmolVLM2 models with Axolotl. Here is an example of how to install from pip: ```bash # Ensure you have a compatible version of Pytorch installed - uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' + uv pip install --no-build-isolation 'axolotl>=0.16.1' ``` 2. Install an extra dependency: diff --git a/examples/voxtral/README.md b/examples/voxtral/README.md index ed5cc64227..f8e7b51be1 100644 --- a/examples/voxtral/README.md +++ b/examples/voxtral/README.md @@ -11,8 +11,8 @@ Thanks to the team at MistralAI for giving us early access to prepare for this r Here is an example of how to install from pip: ```bash -# Ensure you have Pytorch installed (Pytorch 2.6.0 min) -uv pip install --no-build-isolation 'axolotl[flash-attn]>=0.12.0' +# Ensure you have Pytorch installed (Pytorch 2.9.1 min) +uv pip install --no-build-isolation 'axolotl>=0.16.1' ``` 2. Please install the below. diff --git a/pyproject.toml b/pyproject.toml index d028b394de..65e8325811 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ requires-python = ">=3.10" dependencies = [ # Core ML stack - "torch>=2.6.0", + "torch>=2.9.1", "packaging==26.0", "huggingface_hub>=1.1.7", "peft>=0.19.1,<0.20.0", @@ -79,7 +79,7 @@ dependencies = [ # Platform-specific (Linux only) "bitsandbytes==0.49.1 ; sys_platform != 'darwin'", "triton>=3.4.0 ; sys_platform != 'darwin'", - "xformers>=0.0.23.post1 ; sys_platform != 'darwin'", + "xformers>=0.0.33.post2 ; sys_platform != 'darwin' and platform_machine != 'aarch64'", "liger-kernel==0.7.0 ; sys_platform != 'darwin'", "torchao==0.17.0 ; sys_platform != 'darwin' and platform_machine != 'aarch64'", diff --git a/src/axolotl/integrations/kd/README.md b/src/axolotl/integrations/kd/README.md index 5e35cf3d7f..1f24fc8a6d 100644 --- a/src/axolotl/integrations/kd/README.md +++ b/src/axolotl/integrations/kd/README.md @@ -11,7 +11,7 @@ kd_ce_alpha: 0.1 kd_alpha: 0.9 kd_temperature: 1.0 -torch_compile: True # torch>=2.6.0, recommended to reduce vram +torch_compile: True # recommended to reduce vram datasets: - path: ... diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 6ee672c8cd..c52ddce1a0 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -1016,7 +1016,7 @@ class AxolotlInputConfig( torch_compile: Literal["auto"] | bool | None = Field( default=None, json_schema_extra={ - "description": "Whether to use torch.compile and which backend to use. setting to `auto` will enable torch compile when torch>=2.6.0" + "description": "Whether to use torch.compile and which backend to use." }, ) torch_compile_backend: str | None = Field( From 798c8fba897ceb2e238dfaac9bb1731d2f1cacab Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 24 Apr 2026 16:03:21 +0700 Subject: [PATCH 1284/1405] chore: update docker docs (#3623) --- docs/docker.qmd | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/docker.qmd b/docs/docker.qmd index 7ce0418214..2f32a5e5b7 100644 --- a/docs/docker.qmd +++ b/docs/docker.qmd @@ -9,10 +9,6 @@ format: This section describes the different Docker images that are released by AxolotlAI at [Docker Hub](https://hub.docker.com/u/axolotlai). -::: {.callout-important} -For Blackwell GPUs, please use the tags with PyTorch 2.9.1 and CUDA 12.8. -::: - ::: {.callout-important} ### Switch to the `-uv` images From ac77da96daf074475fc8f207b179510ed7ba3f17 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 27 Apr 2026 13:22:56 -0400 Subject: [PATCH 1285/1405] use smaller pretrained models for ci (#3620) [skip ci] * use smaller pretrained models for ci * more steps for loss check * fix tests * more train steps * fix losses --- tests/conftest.py | 56 +++++++-- .../integrations/test_cut_cross_entropy.py | 41 ++++++- tests/e2e/kernels/test_lora_features.py | 2 +- tests/e2e/multigpu/test_dist_muon_fsdp2.py | 51 ++++---- tests/e2e/multigpu/test_fsdp1.py | 69 ++++++----- tests/e2e/multigpu/test_fsdp2.py | 101 ++++++++------- tests/e2e/multigpu/test_fsdp2_lora_kernels.py | 2 +- tests/e2e/multigpu/test_tp.py | 8 +- .../lora_kernels/test_lora_kernel_patching.py | 8 +- tests/e2e/patched/test_falcon_samplepack.py | 58 ++++++--- tests/e2e/patched/test_mistral_samplepack.py | 51 ++++++-- tests/e2e/patched/test_mixtral_samplepack.py | 58 ++++++--- tests/e2e/patched/test_model_patches.py | 5 +- tests/e2e/patched/test_phi_multipack.py | 52 +++++--- tests/e2e/solo/test_batch_flattening.py | 2 +- tests/e2e/test_falcon.py | 88 +++++++++---- tests/e2e/test_mistral.py | 48 ++++++-- tests/e2e/test_mixtral.py | 116 ++++++++++++------ tests/e2e/test_optimizers.py | 28 +++-- tests/e2e/test_phi.py | 54 +++++--- tests/e2e/test_preprocess.py | 2 +- tests/e2e/test_quantization.py | 2 +- tests/e2e/test_qwen.py | 2 +- tests/e2e/utils.py | 100 +++++++++++++++ 24 files changed, 716 insertions(+), 288 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 19e3dc3f05..16a01f8aa3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -119,15 +119,49 @@ def download_smollm2_135m_gptq_model(): @pytest.fixture(scope="session", autouse=True) -def download_qwen_2_5_half_billion_model(): - # download the model - snapshot_download_w_retry("Qwen/Qwen2.5-0.5B", repo_type="model") +def download_qwen3_half_billion_model(): + # download the model (still used as the KD teacher in tests/e2e/integrations/test_kd.py) + snapshot_download_w_retry("Qwen/Qwen3-0.6B", repo_type="model") @pytest.fixture(scope="session", autouse=True) -def download_qwen3_half_billion_model(): - # download the model - snapshot_download_w_retry("Qwen/Qwen3-0.6B", repo_type="model") +def download_tiny_llama_model(): + snapshot_download_w_retry("axolotl-ai-co/tiny-llama-50m", repo_type="model") + + +@pytest.fixture(scope="session", autouse=True) +def download_tiny_mistral_model(): + snapshot_download_w_retry("axolotl-ai-co/tiny-mistral-25m", repo_type="model") + + +@pytest.fixture(scope="session", autouse=True) +def download_tiny_mixtral_model(): + snapshot_download_w_retry("axolotl-ai-co/tiny-mixtral-30m", repo_type="model") + + +@pytest.fixture(scope="session", autouse=True) +def download_tiny_phi_model(): + snapshot_download_w_retry("axolotl-ai-co/tiny-phi-64m", repo_type="model") + + +@pytest.fixture(scope="session", autouse=True) +def download_tiny_falcon_model(): + snapshot_download_w_retry("axolotl-ai-co/tiny-falcon-42m", repo_type="model") + + +@pytest.fixture(scope="session", autouse=True) +def download_tiny_qwen2_model(): + snapshot_download_w_retry("axolotl-ai-co/tiny-qwen2-129m", repo_type="model") + + +@pytest.fixture(scope="session", autouse=True) +def download_tiny_qwen3_model(): + snapshot_download_w_retry("axolotl-ai-co/tiny-qwen3-129m", repo_type="model") + + +@pytest.fixture(scope="session", autouse=True) +def download_tiny_gemma2_model(): + snapshot_download_w_retry("axolotl-ai-co/tiny-gemma2-137m", repo_type="model") @pytest.fixture(scope="session", autouse=True) @@ -620,7 +654,15 @@ def fixture_min_base_cfg(): ) def test_load_fixtures( download_smollm2_135m_model, - download_qwen_2_5_half_billion_model, + download_qwen3_half_billion_model, + download_tiny_llama_model, + download_tiny_mistral_model, + download_tiny_mixtral_model, + download_tiny_phi_model, + download_tiny_falcon_model, + download_tiny_qwen2_model, + download_tiny_qwen3_model, + download_tiny_gemma2_model, download_tatsu_lab_alpaca_dataset, download_mhenrichsen_alpaca_2k_dataset, download_mhenrichsen_alpaca_2k_w_revision_dataset, diff --git a/tests/e2e/integrations/test_cut_cross_entropy.py b/tests/e2e/integrations/test_cut_cross_entropy.py index 7da644ec36..75fb9d0dbd 100644 --- a/tests/e2e/integrations/test_cut_cross_entropy.py +++ b/tests/e2e/integrations/test_cut_cross_entropy.py @@ -10,7 +10,10 @@ from axolotl.utils.config import normalize_config, prepare_plugins, validate_config from axolotl.utils.dict import DictDefault -from tests.e2e.utils import check_model_output_exists +from tests.e2e.utils import ( + check_model_output_exists, + check_tensorboard_loss_decreased, +) @pytest.fixture() @@ -35,13 +38,16 @@ def min_cfg(temp_dir): "num_epochs": 1, "micro_batch_size": 8, "gradient_accumulation_steps": 1, - "learning_rate": 0.00001, + "learning_rate": 5e-4, "optimizer": "adamw_torch_fused", "output_dir": temp_dir, "lr_scheduler": "cosine", - "max_steps": 10, + "max_steps": 40, + "warmup_steps": 5, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } @@ -64,11 +70,18 @@ def test_llama_w_cce(self, min_cfg, temp_dir): else: train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=2.2, + max_final=2.0, + ) def test_qwen2_w_cce(self, temp_dir): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "plugins": [ "axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin", ], @@ -87,13 +100,15 @@ def test_qwen2_w_cce(self, temp_dir): "num_epochs": 1, "micro_batch_size": 4, "gradient_accumulation_steps": 1, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_torch_fused", "output_dir": temp_dir, "lr_scheduler": "cosine", - "max_steps": 10, + "max_steps": 50, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) cfg = validate_config(cfg) @@ -108,6 +123,13 @@ def test_qwen2_w_cce(self, temp_dir): else: train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=5.0, + max_final=4.7, + ) @pytest.mark.parametrize( "attention_type", @@ -136,3 +158,10 @@ def test_llama_w_cce_and_attention(self, min_cfg, temp_dir, attention_type): else: train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=2.2, + max_final=2.0, + ) diff --git a/tests/e2e/kernels/test_lora_features.py b/tests/e2e/kernels/test_lora_features.py index 80495c68db..688710f3a4 100644 --- a/tests/e2e/kernels/test_lora_features.py +++ b/tests/e2e/kernels/test_lora_features.py @@ -24,7 +24,7 @@ ) from axolotl.utils.dict import DictDefault -MODEL_NAME = "Qwen/Qwen3-0.6B" +MODEL_NAME = "axolotl-ai-co/tiny-qwen3-129m" DEVICE = "cuda" DTYPE = torch.bfloat16 diff --git a/tests/e2e/multigpu/test_dist_muon_fsdp2.py b/tests/e2e/multigpu/test_dist_muon_fsdp2.py index 93db473a93..05841bb64a 100644 --- a/tests/e2e/multigpu/test_dist_muon_fsdp2.py +++ b/tests/e2e/multigpu/test_dist_muon_fsdp2.py @@ -1,23 +1,22 @@ """Test module for DistMuon optimizer with FSDP2 multi-GPU functionality.""" -import os from pathlib import Path -import torch import yaml from accelerate.test_utils import execute_subprocess_async -from tbparse import SummaryReader from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault -from tests.e2e.utils import most_recent_subdir, require_torch_2_7_0 +from tests.e2e.utils import check_tensorboard_loss_decreased, require_torch_2_7_0 AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent def verify_training_success(temp_dir): - """Verify that training completed successfully by checking artifacts and loss.""" + """Verify that training completed successfully — artifacts, no-NaN, loss + stayed in qwen2-pretraining scale (tiny-qwen2-129m final pretrain CE ~3.92). + """ output_path = Path(temp_dir) model_files = list(output_path.glob("*.bin")) + list( @@ -30,19 +29,13 @@ def verify_training_success(temp_dir): "No checkpoint files found - training may have failed" ) - tb_log_path = most_recent_subdir(temp_dir + "/runs") - if tb_log_path: - event_files = sorted(os.listdir(tb_log_path)) - if event_files: - event_file = os.path.join(tb_log_path, event_files[0]) - reader = SummaryReader(event_file) - df = reader.scalars - train_loss_df = df[df.tag == "train/train_loss"] - if len(train_loss_df) > 0: - final_loss = train_loss_df.value.values[-1] - assert not torch.isnan(torch.tensor(final_loss)), ( - f"Training loss is NaN: {final_loss}" - ) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=10, + final_window=10, + max_initial=5.0, + max_final=4.7, + ) class TestDistMuon: @@ -52,7 +45,7 @@ class TestDistMuon: def test_fft_sft(self, temp_dir): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "datasets": [ @@ -63,11 +56,12 @@ def test_fft_sft(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 2, + "max_steps": 80, + "warmup_steps": 5, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.02, + "learning_rate": 2e-3, "optimizer": "muon", "weight_decay": 0.01, "lr_scheduler": "cosine", @@ -82,6 +76,9 @@ def test_fft_sft(self, temp_dir): "reshard_after_forward": True, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, "bf16": True, } ) @@ -109,7 +106,7 @@ def test_fft_sft(self, temp_dir): def test_lora_sft(self, temp_dir): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "datasets": [ @@ -122,14 +119,15 @@ def test_lora_sft(self, temp_dir): "adapter": "lora", "lora_r": 8, "lora_alpha": 16, - "lora_dropout": 0.05, + "lora_dropout": 0.0, "lora_target_linear": True, "num_epochs": 1, - "max_steps": 2, + "max_steps": 80, + "warmup_steps": 5, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.02, + "learning_rate": 2e-3, "optimizer": "muon", "weight_decay": 0.01, "lr_scheduler": "cosine", @@ -144,6 +142,9 @@ def test_lora_sft(self, temp_dir): "reshard_after_forward": True, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, "bf16": True, } ) diff --git a/tests/e2e/multigpu/test_fsdp1.py b/tests/e2e/multigpu/test_fsdp1.py index 5b67247914..c6a8a47e99 100644 --- a/tests/e2e/multigpu/test_fsdp1.py +++ b/tests/e2e/multigpu/test_fsdp1.py @@ -1,24 +1,23 @@ """Test module for FSDP1 multi-GPU functionality.""" -import os from pathlib import Path import pytest -import torch import yaml from accelerate.test_utils import execute_subprocess_async -from tbparse import SummaryReader from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault -from tests.e2e.utils import most_recent_subdir +from tests.e2e.utils import check_tensorboard_loss_decreased AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent def verify_training_success(temp_dir): - """Verify that training completed successfully by checking artifacts and loss.""" + """Verify that training completed successfully — artifacts, no-NaN, loss + stayed in qwen2-pretraining scale (tiny-qwen2-129m final pretrain CE ~3.92). + """ output_path = Path(temp_dir) model_files = list(output_path.glob("*.bin")) + list( @@ -31,19 +30,13 @@ def verify_training_success(temp_dir): "No checkpoint files found - training may have failed" ) - tb_log_path = most_recent_subdir(temp_dir + "/runs") - if tb_log_path: - event_files = sorted(os.listdir(tb_log_path)) - if event_files: - event_file = os.path.join(tb_log_path, event_files[0]) - reader = SummaryReader(event_file) - df = reader.scalars - train_loss_df = df[df.tag == "train/train_loss"] - if len(train_loss_df) > 0: - final_loss = train_loss_df.value.values[-1] - assert not torch.isnan(torch.tensor(final_loss)), ( - f"Training loss is NaN: {final_loss}" - ) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=10, + final_window=10, + max_initial=5.0, + max_final=4.7, + ) class TestFSDP1: @@ -56,7 +49,7 @@ class TestFSDP1: def test_fft_sft(self, temp_dir, fsdp_cpu_ram_efficient_loading): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "datasets": [ @@ -67,11 +60,12 @@ def test_fft_sft(self, temp_dir, fsdp_cpu_ram_efficient_loading): }, ], "num_epochs": 1, - "max_steps": 2, + "max_steps": 80, + "warmup_steps": 5, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, @@ -87,6 +81,9 @@ def test_fft_sft(self, temp_dir, fsdp_cpu_ram_efficient_loading): "fsdp_use_orig_params": False, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, "bf16": True, } ) @@ -126,7 +123,7 @@ def test_fft_sft(self, temp_dir, fsdp_cpu_ram_efficient_loading): def test_lora_sft(self, temp_dir, adapter_config): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "datasets": [ @@ -140,14 +137,15 @@ def test_lora_sft(self, temp_dir, adapter_config): "load_in_4bit": adapter_config["load_in_4bit"], "lora_r": 8, "lora_alpha": 16, - "lora_dropout": 0.05, + "lora_dropout": 0.0, "lora_target_linear": True, "num_epochs": 1, - "max_steps": 2, + "max_steps": 80, + "warmup_steps": 5, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 1e-3, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, @@ -163,6 +161,9 @@ def test_lora_sft(self, temp_dir, adapter_config): "fsdp_use_orig_params": False, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, "bf16": True, } ) @@ -190,7 +191,7 @@ def test_lora_sft(self, temp_dir, adapter_config): def test_dpo_fft(self, temp_dir): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "rl": "dpo", @@ -203,11 +204,11 @@ def test_dpo_fft(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 2, + "max_steps": 20, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, @@ -223,6 +224,9 @@ def test_dpo_fft(self, temp_dir): "fsdp_use_orig_params": False, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, } ) @@ -262,7 +266,7 @@ def test_dpo_fft(self, temp_dir): def test_dpo_lora(self, temp_dir, adapter_config): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "load_in_4bit": adapter_config["load_in_4bit"], "rl": "dpo", "chat_template": "chatml", @@ -281,11 +285,11 @@ def test_dpo_lora(self, temp_dir, adapter_config): }, ], "num_epochs": 1, - "max_steps": 2, + "max_steps": 20, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 1e-3, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, @@ -301,6 +305,9 @@ def test_dpo_lora(self, temp_dir, adapter_config): "fsdp_use_orig_params": False, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, "bf16": "auto", "tf32": True, } diff --git a/tests/e2e/multigpu/test_fsdp2.py b/tests/e2e/multigpu/test_fsdp2.py index a70ff9aa79..b48ddd436f 100644 --- a/tests/e2e/multigpu/test_fsdp2.py +++ b/tests/e2e/multigpu/test_fsdp2.py @@ -1,24 +1,23 @@ """Test module for FSDP2 multi-GPU functionality.""" -import os from pathlib import Path import pytest -import torch import yaml from accelerate.test_utils import execute_subprocess_async -from tbparse import SummaryReader from transformers.testing_utils import get_torch_dist_unique_port from axolotl.utils.dict import DictDefault -from tests.e2e.utils import most_recent_subdir, require_torch_2_7_0 +from tests.e2e.utils import check_tensorboard_loss_decreased, require_torch_2_7_0 AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent def verify_training_success(temp_dir): - """Verify that training completed successfully by checking artifacts and loss.""" + """Verify that training completed successfully — artifacts, no-NaN, loss + stayed in qwen2-pretraining scale (tiny-qwen2-129m final pretrain CE ~3.92). + """ output_path = Path(temp_dir) model_files = list(output_path.glob("*.bin")) + list( @@ -31,19 +30,13 @@ def verify_training_success(temp_dir): "No checkpoint files found - training may have failed" ) - tb_log_path = most_recent_subdir(temp_dir + "/runs") - if tb_log_path: - event_files = sorted(os.listdir(tb_log_path)) - if event_files: - event_file = os.path.join(tb_log_path, event_files[0]) - reader = SummaryReader(event_file) - df = reader.scalars - train_loss_df = df[df.tag == "train/train_loss"] - if len(train_loss_df) > 0: - final_loss = train_loss_df.value.values[-1] - assert not torch.isnan(torch.tensor(final_loss)), ( - f"Training loss is NaN: {final_loss}" - ) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=10, + final_window=10, + max_initial=5.0, + max_final=4.7, + ) class TestFSDP2: @@ -57,7 +50,7 @@ class TestFSDP2: def test_fft_sft(self, temp_dir, fsdp_cpu_ram_efficient_loading): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "datasets": [ @@ -68,11 +61,12 @@ def test_fft_sft(self, temp_dir, fsdp_cpu_ram_efficient_loading): }, ], "num_epochs": 1, - "max_steps": 2, + "max_steps": 80, + "warmup_steps": 5, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, @@ -86,6 +80,9 @@ def test_fft_sft(self, temp_dir, fsdp_cpu_ram_efficient_loading): "reshard_after_forward": True, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, "bf16": True, } ) @@ -114,7 +111,7 @@ def test_fft_sft(self, temp_dir, fsdp_cpu_ram_efficient_loading): def test_lora_sft(self, temp_dir, peft_use_dora): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "datasets": [ @@ -128,14 +125,15 @@ def test_lora_sft(self, temp_dir, peft_use_dora): "adapter": "lora", "lora_r": 8, "lora_alpha": 16, - "lora_dropout": 0.05, + "lora_dropout": 0.0, "lora_target_linear": True, "num_epochs": 1, - "max_steps": 2, + "max_steps": 80, + "warmup_steps": 5, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 1e-3, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, @@ -149,6 +147,9 @@ def test_lora_sft(self, temp_dir, peft_use_dora): "reshard_after_forward": True, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, "bf16": True, # explicitly disable LORA kernels, as they may be auto-enabled "lora_mlp_kernel": False, @@ -180,7 +181,7 @@ def test_lora_sft(self, temp_dir, peft_use_dora): def test_lora_sft_kernels(self, temp_dir): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "datasets": [ @@ -195,11 +196,12 @@ def test_lora_sft_kernels(self, temp_dir): "lora_alpha": 16, "lora_target_linear": True, "num_epochs": 1, - "max_steps": 2, + "max_steps": 80, + "warmup_steps": 5, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 1e-3, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, @@ -213,6 +215,9 @@ def test_lora_sft_kernels(self, temp_dir): "reshard_after_forward": True, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, "bf16": True, "lora_mlp_kernel": True, "lora_qkv_kernel": True, @@ -243,7 +248,7 @@ def test_lora_sft_kernels(self, temp_dir): def test_qlora_sft(self, temp_dir): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "datasets": [ @@ -257,14 +262,15 @@ def test_qlora_sft(self, temp_dir): "adapter": "qlora", "lora_r": 8, "lora_alpha": 16, - "lora_dropout": 0.05, + "lora_dropout": 0.0, "lora_target_linear": True, "num_epochs": 1, - "max_steps": 2, + "max_steps": 80, + "warmup_steps": 5, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 1e-3, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, @@ -278,6 +284,9 @@ def test_qlora_sft(self, temp_dir): "reshard_after_forward": True, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, "bf16": True, } ) @@ -305,7 +314,7 @@ def test_qlora_sft(self, temp_dir): def test_qlora_sft_kernels(self, temp_dir): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "datasets": [ @@ -321,11 +330,12 @@ def test_qlora_sft_kernels(self, temp_dir): "lora_alpha": 16, "lora_target_linear": True, "num_epochs": 1, - "max_steps": 2, + "max_steps": 80, + "warmup_steps": 5, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 1e-3, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, @@ -339,6 +349,9 @@ def test_qlora_sft_kernels(self, temp_dir): "reshard_after_forward": True, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, "bf16": True, "lora_mlp_kernel": True, "lora_qkv_kernel": True, @@ -370,7 +383,7 @@ def test_qlora_sft_kernels(self, temp_dir): def test_dpo_fft(self, temp_dir): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "rl": "dpo", @@ -383,11 +396,11 @@ def test_dpo_fft(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 2, + "max_steps": 20, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, @@ -401,6 +414,9 @@ def test_dpo_fft(self, temp_dir): "reshard_after_forward": True, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, } ) @@ -428,7 +444,7 @@ def test_dpo_fft(self, temp_dir): def test_dpo_lora(self, temp_dir): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "rl": "dpo", "chat_template": "chatml", @@ -445,11 +461,11 @@ def test_dpo_lora(self, temp_dir): "lora_dropout": 0.05, "lora_target_linear": True, "num_epochs": 1, - "max_steps": 2, + "max_steps": 20, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 1e-3, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, @@ -463,6 +479,9 @@ def test_dpo_lora(self, temp_dir): "reshard_after_forward": True, }, "use_tensorboard": True, + "seed": 42, + "sample_packing": True, + "pad_to_sequence_len": True, } ) diff --git a/tests/e2e/multigpu/test_fsdp2_lora_kernels.py b/tests/e2e/multigpu/test_fsdp2_lora_kernels.py index 27ad2b8e95..0f2fd421a3 100644 --- a/tests/e2e/multigpu/test_fsdp2_lora_kernels.py +++ b/tests/e2e/multigpu/test_fsdp2_lora_kernels.py @@ -40,7 +40,7 @@ def _run_training(temp_dir, cfg): def _base_lora_fsdp2_config(temp_dir, **overrides): """Base config for LoRA + FSDP2 + kernel tests.""" cfg = { - "base_model": "Qwen/Qwen3-0.6B", + "base_model": "axolotl-ai-co/tiny-qwen3-129m", "sequence_len": 512, "val_set_size": 0.0, "datasets": [ diff --git a/tests/e2e/multigpu/test_tp.py b/tests/e2e/multigpu/test_tp.py index 9891a0906f..965dfa8e5b 100644 --- a/tests/e2e/multigpu/test_tp.py +++ b/tests/e2e/multigpu/test_tp.py @@ -8,7 +8,7 @@ from axolotl.utils.dict import DictDefault -from tests.e2e.utils import check_tensorboard, require_torch_2_7_0 +from tests.e2e.utils import check_tensorboard_loss_decreased, require_torch_2_7_0 class TestTensorParallel: @@ -21,7 +21,7 @@ class TestTensorParallel: def test_fft_sft(self, temp_dir): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "datasets": [ @@ -63,6 +63,6 @@ def test_fft_sft(self, temp_dir): ] ) - check_tensorboard( - temp_dir + "/runs", "train/train_loss", 1.0, "Train Loss (%s) is too high" + check_tensorboard_loss_decreased( + temp_dir + "/runs", max_initial=5.0, max_final=4.7 ) diff --git a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py index 2865a80f91..4844ce5396 100644 --- a/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py +++ b/tests/e2e/patched/lora_kernels/test_lora_kernel_patching.py @@ -32,12 +32,12 @@ MODEL_CONFIGS = [ { - "name": "trl-internal-testing/tiny-MistralForCausalLM-0.2", + "name": "axolotl-ai-co/tiny-mistral-25m", "expected_activation": apply_lora_mlp_swiglu, "dtype": torch.float16, }, { - "name": "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", + "name": "axolotl-ai-co/tiny-qwen2-129m", "expected_activation": apply_lora_mlp_swiglu, "dtype": torch.float16, }, @@ -47,7 +47,7 @@ "dtype": torch.float32, }, { - "name": "trl-internal-testing/tiny-Gemma2ForCausalLM", + "name": "axolotl-ai-co/tiny-gemma2-137m", "expected_activation": apply_lora_mlp_geglu, "dtype": torch.float16, }, @@ -159,7 +159,7 @@ def test_swiglu_mlp_integration(small_llama_model): def test_geglu_model_integration(): """Test GeGLU activation with Gemma model.""" model = AutoModelForCausalLM.from_pretrained( - "trl-internal-testing/tiny-Gemma2ForCausalLM", + "axolotl-ai-co/tiny-gemma2-137m", dtype=torch.float16, device_map="cuda:0", ) diff --git a/tests/e2e/patched/test_falcon_samplepack.py b/tests/e2e/patched/test_falcon_samplepack.py index cc50914035..1d688585e9 100644 --- a/tests/e2e/patched/test_falcon_samplepack.py +++ b/tests/e2e/patched/test_falcon_samplepack.py @@ -4,14 +4,16 @@ import unittest -import pytest - from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from ..utils import check_model_output_exists, with_temp_dir +from ..utils import ( + check_model_output_exists, + check_tensorboard_loss_decreased, + with_temp_dir, +) class TestFalconPatched(unittest.TestCase): @@ -19,13 +21,12 @@ class TestFalconPatched(unittest.TestCase): Test case for Falcon models """ - @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_qlora(self, temp_dir): cfg = DictDefault( { - "base_model": "illuin/tiny-random-FalconForCausalLM", - "flash_attention": True, + "base_model": "axolotl-ai-co/tiny-falcon-42m", + "flash_attention": False, "sample_packing": True, "sequence_len": 2048, "load_in_4bit": True, @@ -47,17 +48,20 @@ def test_qlora(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) cfg = validate_config(cfg) @@ -66,14 +70,20 @@ def test_qlora(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=6.0, + max_final=4.7, + ) - @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_ft(self, temp_dir): cfg = DictDefault( { - "base_model": "illuin/tiny-random-FalconForCausalLM", - "flash_attention": True, + "base_model": "axolotl-ai-co/tiny-falcon-42m", + "flash_attention": False, "sample_packing": True, "sequence_len": 2048, "val_set_size": 0.05, @@ -88,17 +98,20 @@ def test_ft(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) cfg = validate_config(cfg) @@ -107,3 +120,10 @@ def test_ft(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=6.0, + max_final=4.7, + ) diff --git a/tests/e2e/patched/test_mistral_samplepack.py b/tests/e2e/patched/test_mistral_samplepack.py index e03941b07c..ab59a000cf 100644 --- a/tests/e2e/patched/test_mistral_samplepack.py +++ b/tests/e2e/patched/test_mistral_samplepack.py @@ -9,7 +9,12 @@ from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from ..utils import check_model_output_exists, require_torch_2_6_0, with_temp_dir +from ..utils import ( + check_model_output_exists, + check_tensorboard_loss_decreased, + require_torch_2_6_0, + with_temp_dir, +) class TestMistral(unittest.TestCase): @@ -22,7 +27,7 @@ class TestMistral(unittest.TestCase): def test_lora_packing(self, temp_dir): cfg = DictDefault( { - "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", + "base_model": "axolotl-ai-co/tiny-mistral-25m", "flash_attention": True, "sample_packing": True, "sequence_len": 1024, @@ -45,17 +50,20 @@ def test_lora_packing(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 5, - "save_steps": 3, - "eval_steps": 4, + "max_steps": 50, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) cfg = validate_config(cfg) @@ -64,12 +72,19 @@ def test_lora_packing(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=5.5, + max_final=4.3, + ) @with_temp_dir def test_ft_packing(self, temp_dir): cfg = DictDefault( { - "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", + "base_model": "axolotl-ai-co/tiny-mistral-25m", "flash_attention": True, "sample_packing": True, "sequence_len": 1024, @@ -86,17 +101,20 @@ def test_ft_packing(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 5, - "save_steps": 3, - "eval_steps": 4, + "max_steps": 50, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) cfg = validate_config(cfg) @@ -105,3 +123,10 @@ def test_ft_packing(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=5.5, + max_final=4.3, + ) diff --git a/tests/e2e/patched/test_mixtral_samplepack.py b/tests/e2e/patched/test_mixtral_samplepack.py index 3517ff3db6..3c6eb8d126 100644 --- a/tests/e2e/patched/test_mixtral_samplepack.py +++ b/tests/e2e/patched/test_mixtral_samplepack.py @@ -9,7 +9,11 @@ from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from ..utils import check_model_output_exists, with_temp_dir +from ..utils import ( + check_model_output_exists, + check_tensorboard_loss_decreased, + with_temp_dir, +) class TestMixtral(unittest.TestCase): @@ -21,8 +25,7 @@ class TestMixtral(unittest.TestCase): def test_qlora(self, temp_dir): cfg = DictDefault( { - "base_model": "hf-internal-testing/Mixtral-tiny", - "tokenizer_config": "LoneStriker/Mixtral-8x7B-v0.1-HF", + "base_model": "axolotl-ai-co/tiny-mixtral-30m", "flash_attention": True, "sample_packing": True, "sequence_len": 2048, @@ -30,7 +33,7 @@ def test_qlora(self, temp_dir): "adapter": "qlora", "lora_r": 16, "lora_alpha": 32, - "lora_dropout": 0.1, + "lora_dropout": 0.0, "lora_target_linear": True, "val_set_size": 0.05, "special_tokens": {}, @@ -41,17 +44,21 @@ def test_qlora(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 3e-3, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 5, - "save_steps": 3, - "eval_steps": 4, + "max_steps": 80, + "warmup_steps": 5, + "logging_steps": 1, + "save_steps": 80, + "eval_steps": 80, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) cfg = validate_config(cfg) @@ -60,13 +67,19 @@ def test_qlora(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=10, + final_window=10, + max_initial=6.0, + max_final=4.7, + ) @with_temp_dir def test_ft(self, temp_dir): cfg = DictDefault( { - "base_model": "hf-internal-testing/Mixtral-tiny", - "tokenizer_config": "LoneStriker/Mixtral-8x7B-v0.1-HF", + "base_model": "axolotl-ai-co/tiny-mixtral-30m", "flash_attention": True, "sample_packing": True, "sequence_len": 2048, @@ -79,17 +92,21 @@ def test_ft(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, - "optimizer": "adamw_bnb_8bit", + "learning_rate": 5e-4, + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 5, - "save_steps": 3, - "eval_steps": 4, + "max_steps": 80, + "warmup_steps": 5, + "logging_steps": 1, + "save_steps": 80, + "eval_steps": 80, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) cfg = validate_config(cfg) @@ -98,3 +115,10 @@ def test_ft(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=6.0, + max_final=4.7, + ) diff --git a/tests/e2e/patched/test_model_patches.py b/tests/e2e/patched/test_model_patches.py index aaaaf5fe2d..83448d175c 100644 --- a/tests/e2e/patched/test_model_patches.py +++ b/tests/e2e/patched/test_model_patches.py @@ -22,8 +22,7 @@ class TestModelPatches(unittest.TestCase): def test_mixtral_multipack(self, temp_dir): cfg = DictDefault( { - "base_model": "hf-internal-testing/Mixtral-tiny", - "tokenizer_config": "LoneStriker/Mixtral-8x7B-v0.1-HF", + "base_model": "axolotl-ai-co/tiny-mixtral-30m", "flash_attention": True, "sample_packing": True, "sequence_len": 2048, @@ -57,7 +56,7 @@ def test_mixtral_multipack(self, temp_dir): def test_mistral_multipack(self, temp_dir): cfg = DictDefault( { - "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", + "base_model": "axolotl-ai-co/tiny-mistral-25m", "flash_attention": True, "sample_packing": True, "sequence_len": 2048, diff --git a/tests/e2e/patched/test_phi_multipack.py b/tests/e2e/patched/test_phi_multipack.py index 77b2d99e5f..c3c8ff5693 100644 --- a/tests/e2e/patched/test_phi_multipack.py +++ b/tests/e2e/patched/test_phi_multipack.py @@ -9,7 +9,11 @@ from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from ..utils import check_model_output_exists, with_temp_dir +from ..utils import ( + check_model_output_exists, + check_tensorboard_loss_decreased, + with_temp_dir, +) class TestPhiMultipack(unittest.TestCase): @@ -21,7 +25,7 @@ class TestPhiMultipack(unittest.TestCase): def test_ft_packed(self, temp_dir): cfg = DictDefault( { - "base_model": "microsoft/phi-1_5", + "base_model": "axolotl-ai-co/tiny-phi-64m", "model_type": "PhiForCausalLM", "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, @@ -43,17 +47,20 @@ def test_ft_packed(self, temp_dir): "dataset_shard_num": 10, "dataset_shard_idx": 0, "num_epochs": 1, - "micro_batch_size": 1, + "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, - "optimizer": "adamw_bnb_8bit", + "learning_rate": 2e-4, + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 5, - "eval_steps": 3, - "save_steps": 4, + "max_steps": 50, + "logging_steps": 1, + "eval_steps": 50, + "save_steps": 50, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) @@ -63,12 +70,19 @@ def test_ft_packed(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=6.0, + max_final=4.7, + ) @with_temp_dir def test_qlora_packed(self, temp_dir): cfg = DictDefault( { - "base_model": "microsoft/phi-1_5", + "base_model": "axolotl-ai-co/tiny-phi-64m", "model_type": "PhiForCausalLM", "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, @@ -94,17 +108,20 @@ def test_qlora_packed(self, temp_dir): "dataset_shard_num": 10, "dataset_shard_idx": 0, "num_epochs": 1, - "micro_batch_size": 1, + "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 5, - "eval_steps": 3, - "save_steps": 4, + "max_steps": 50, + "logging_steps": 1, + "eval_steps": 50, + "save_steps": 50, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) @@ -114,3 +131,10 @@ def test_qlora_packed(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=6.0, + max_final=4.7, + ) diff --git a/tests/e2e/solo/test_batch_flattening.py b/tests/e2e/solo/test_batch_flattening.py index 80b7b02590..7b6c59119a 100644 --- a/tests/e2e/solo/test_batch_flattening.py +++ b/tests/e2e/solo/test_batch_flattening.py @@ -18,7 +18,7 @@ # Import the actual trainer methods we want to test from axolotl.core.trainers.grpo.async_trainer import AsyncGRPOTrainer -MODEL_NAME = "Qwen/Qwen3-0.6B" +MODEL_NAME = "axolotl-ai-co/tiny-qwen3-129m" def _fix_patched_attention(model): diff --git a/tests/e2e/test_falcon.py b/tests/e2e/test_falcon.py index 1a363fe6af..19de202d2d 100644 --- a/tests/e2e/test_falcon.py +++ b/tests/e2e/test_falcon.py @@ -4,14 +4,16 @@ import unittest -import pytest - from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from .utils import check_model_output_exists, with_temp_dir +from .utils import ( + check_model_output_exists, + check_tensorboard_loss_decreased, + with_temp_dir, +) class TestFalcon(unittest.TestCase): @@ -19,13 +21,12 @@ class TestFalcon(unittest.TestCase): Test case for falcon """ - @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_lora(self, temp_dir): cfg = DictDefault( { - "base_model": "illuin/tiny-random-FalconForCausalLM", - "flash_attention": True, + "base_model": "axolotl-ai-co/tiny-falcon-42m", + "flash_attention": False, "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -49,17 +50,21 @@ def test_lora(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "warmup_steps": 5, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) @@ -69,14 +74,20 @@ def test_lora(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=5.0, + max_final=4.7, + ) - @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_lora_added_vocab(self, temp_dir): cfg = DictDefault( { - "base_model": "illuin/tiny-random-FalconForCausalLM", - "flash_attention": True, + "base_model": "axolotl-ai-co/tiny-falcon-42m", + "flash_attention": False, "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", @@ -104,17 +115,21 @@ def test_lora_added_vocab(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "warmup_steps": 5, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) @@ -124,14 +139,20 @@ def test_lora_added_vocab(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=5.0, + max_final=4.7, + ) - @pytest.mark.skip(reason="no tiny models for testing with safetensors") @with_temp_dir def test_ft(self, temp_dir): cfg = DictDefault( { - "base_model": "illuin/tiny-random-FalconForCausalLM", - "flash_attention": True, + "base_model": "axolotl-ai-co/tiny-falcon-42m", + "flash_attention": False, "sequence_len": 1024, "val_set_size": 0.02, "special_tokens": { @@ -145,17 +166,23 @@ def test_ft(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "sample_packing": True, + "pad_to_sequence_len": True, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 5e-4, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 80, + "warmup_steps": 5, + "logging_steps": 1, + "save_steps": 80, + "eval_steps": 80, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) @@ -165,3 +192,10 @@ def test_ft(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=10, + final_window=10, + max_initial=5.0, + max_final=4.7, + ) diff --git a/tests/e2e/test_mistral.py b/tests/e2e/test_mistral.py index 08b3b05af4..37cae7ce77 100644 --- a/tests/e2e/test_mistral.py +++ b/tests/e2e/test_mistral.py @@ -11,7 +11,11 @@ from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from .utils import check_model_output_exists, with_temp_dir +from .utils import ( + check_model_output_exists, + check_tensorboard_loss_decreased, + with_temp_dir, +) class TestMistral(unittest.TestCase): @@ -23,7 +27,7 @@ class TestMistral(unittest.TestCase): def test_lora(self, temp_dir): cfg = DictDefault( { - "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", + "base_model": "axolotl-ai-co/tiny-mistral-25m", "flash_attention": True, "sequence_len": 1024, "load_in_8bit": True, @@ -45,16 +49,18 @@ def test_lora(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "save_first_step": False, + "use_tensorboard": True, } ) @@ -64,12 +70,19 @@ def test_lora(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=4.5, + max_final=4.3, + ) @with_temp_dir def test_ft(self, temp_dir): cfg = DictDefault( { - "base_model": "trl-internal-testing/tiny-MistralForCausalLM-0.2", + "base_model": "axolotl-ai-co/tiny-mistral-25m", "flash_attention": True, "sequence_len": 1024, "val_set_size": 0.02, @@ -85,16 +98,18 @@ def test_ft(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "save_first_step": False, + "use_tensorboard": True, } ) if is_torch_bf16_gpu_available(): @@ -108,3 +123,10 @@ def test_ft(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=4.5, + max_final=4.3, + ) diff --git a/tests/e2e/test_mixtral.py b/tests/e2e/test_mixtral.py index c47486b3c8..00c75f426f 100644 --- a/tests/e2e/test_mixtral.py +++ b/tests/e2e/test_mixtral.py @@ -12,7 +12,11 @@ from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from .utils import check_model_output_exists, with_temp_dir +from .utils import ( + check_model_output_exists, + check_tensorboard_loss_decreased, + with_temp_dir, +) class TestMixtral(unittest.TestCase): @@ -24,8 +28,7 @@ class TestMixtral(unittest.TestCase): def test_qlora_w_fa2(self, temp_dir): cfg = DictDefault( { - "base_model": "hf-internal-testing/Mixtral-tiny", - "tokenizer_config": "LoneStriker/Mixtral-8x7B-v0.1-HF", + "base_model": "axolotl-ai-co/tiny-mixtral-30m", "flash_attention": True, "sequence_len": 1024, "load_in_4bit": True, @@ -51,16 +54,18 @@ def test_qlora_w_fa2(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "save_first_step": False, + "use_tensorboard": True, } ) @@ -74,13 +79,19 @@ def test_qlora_w_fa2(self, temp_dir): == torch.float32 ) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=5.0, + max_final=4.7, + ) @with_temp_dir def test_qlora_wo_fa2(self, temp_dir): cfg = DictDefault( { - "base_model": "hf-internal-testing/Mixtral-tiny", - "tokenizer_config": "LoneStriker/Mixtral-8x7B-v0.1-HF", + "base_model": "axolotl-ai-co/tiny-mixtral-30m", "flash_attention": False, "sequence_len": 1024, "load_in_4bit": True, @@ -106,16 +117,18 @@ def test_qlora_wo_fa2(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "save_first_step": False, + "use_tensorboard": True, } ) @@ -129,13 +142,19 @@ def test_qlora_wo_fa2(self, temp_dir): == torch.float32 ) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=5.0, + max_final=4.7, + ) @with_temp_dir def test_16bit_lora_w_fa2(self, temp_dir): cfg = DictDefault( { - "base_model": "hf-internal-testing/Mixtral-tiny", - "tokenizer_config": "LoneStriker/Mixtral-8x7B-v0.1-HF", + "base_model": "axolotl-ai-co/tiny-mixtral-30m", "flash_attention": True, "sequence_len": 1024, "adapter": "lora", @@ -160,16 +179,18 @@ def test_16bit_lora_w_fa2(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "save_first_step": False, + "use_tensorboard": True, } ) if is_torch_bf16_gpu_available(): @@ -187,13 +208,19 @@ def test_16bit_lora_w_fa2(self, temp_dir): == torch.float32 ) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=5.0, + max_final=4.7, + ) @with_temp_dir def test_16bit_lora_wo_fa2(self, temp_dir): cfg = DictDefault( { - "base_model": "hf-internal-testing/Mixtral-tiny", - "tokenizer_config": "LoneStriker/Mixtral-8x7B-v0.1-HF", + "base_model": "axolotl-ai-co/tiny-mixtral-30m", "flash_attention": False, "sequence_len": 1024, "adapter": "lora", @@ -218,16 +245,18 @@ def test_16bit_lora_wo_fa2(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "save_first_step": False, + "use_tensorboard": True, } ) @@ -245,13 +274,19 @@ def test_16bit_lora_wo_fa2(self, temp_dir): == torch.float32 ) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=5.0, + max_final=4.7, + ) @with_temp_dir def test_ft(self, temp_dir): cfg = DictDefault( { - "base_model": "hf-internal-testing/Mixtral-tiny", - "tokenizer_config": "LoneStriker/Mixtral-8x7B-v0.1-HF", + "base_model": "axolotl-ai-co/tiny-mixtral-30m", "flash_attention": True, "sequence_len": 1024, "val_set_size": 0.02, @@ -263,16 +298,18 @@ def test_ft(self, temp_dir): }, ], "num_epochs": 2, - "micro_batch_size": 2, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "adamw_bnb_8bit", "lr_scheduler": "cosine", - "max_steps": 20, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "save_first_step": False, + "use_tensorboard": True, } ) if is_torch_bf16_gpu_available(): @@ -286,3 +323,10 @@ def test_ft(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=5.0, + max_final=4.7, + ) diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index a53e8b0059..430bd73c5a 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -13,6 +13,7 @@ from .utils import ( check_model_output_exists, + check_tensorboard_loss_decreased, require_torch_2_5_1, require_torch_2_6_0, require_torch_2_7_0, @@ -243,20 +244,18 @@ def test_fft_schedule_free_adamw(self, temp_dir): def test_came_pytorch(self, temp_dir): cfg = DictDefault( { - "base_model": "JackFram/llama-68m", - "tokenizer_type": "LlamaTokenizer", + "base_model": "axolotl-ai-co/tiny-llama-50m", + "tokenizer_type": "AutoTokenizer", "sequence_len": 1024, "load_in_8bit": True, "adapter": "lora", "lora_r": 8, "lora_alpha": 16, - "lora_dropout": 0.05, + "lora_dropout": 0.0, "lora_target_linear": True, "val_set_size": 0.1, "special_tokens": { - "unk_token": "", - "bos_token": "", - "eos_token": "", + "pad_token": "<|endoftext|>", }, "datasets": [ { @@ -265,16 +264,22 @@ def test_came_pytorch(self, temp_dir): }, ], "num_epochs": 1, + "sample_packing": True, + "pad_to_sequence_len": True, "micro_batch_size": 8, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 1e-4, "optimizer": "came_pytorch", "adam_beta3": 0.9999, "adam_epsilon2": 1e-16, - "max_steps": 5, + "max_steps": 80, + "warmup_steps": 5, + "logging_steps": 1, "lr_scheduler": "cosine", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) @@ -284,6 +289,13 @@ def test_came_pytorch(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=10, + final_window=10, + max_initial=4.0, + max_final=3.0, + ) @require_torch_2_7_0 diff --git a/tests/e2e/test_phi.py b/tests/e2e/test_phi.py index ae2210249f..c2a637883c 100644 --- a/tests/e2e/test_phi.py +++ b/tests/e2e/test_phi.py @@ -9,7 +9,11 @@ from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from .utils import check_model_output_exists, with_temp_dir +from .utils import ( + check_model_output_exists, + check_tensorboard_loss_decreased, + with_temp_dir, +) class TestPhi(unittest.TestCase): @@ -21,7 +25,7 @@ class TestPhi(unittest.TestCase): def test_phi_ft(self, temp_dir): cfg = DictDefault( { - "base_model": "microsoft/phi-1_5", + "base_model": "axolotl-ai-co/tiny-phi-64m", "model_type": "AutoModelForCausalLM", "tokenizer_type": "AutoTokenizer", "sequence_len": 2048, @@ -41,18 +45,22 @@ def test_phi_ft(self, temp_dir): "dataset_shard_num": 10, "dataset_shard_idx": 0, "num_epochs": 1, - "micro_batch_size": 1, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, - "optimizer": "paged_adamw_8bit", + "learning_rate": 2e-4, + "optimizer": "adamw_torch_fused", "lr_scheduler": "cosine", "flash_attention": True, - "max_steps": 10, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "warmup_steps": 5, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) cfg = validate_config(cfg) @@ -61,12 +69,19 @@ def test_phi_ft(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=5.0, + max_final=4.7, + ) @with_temp_dir def test_phi_qlora(self, temp_dir): cfg = DictDefault( { - "base_model": "microsoft/phi-1_5", + "base_model": "axolotl-ai-co/tiny-phi-64m", "model_type": "AutoModelForCausalLM", "tokenizer_type": "AutoTokenizer", "sequence_len": 2048, @@ -90,18 +105,22 @@ def test_phi_qlora(self, temp_dir): "dataset_shard_num": 10, "dataset_shard_idx": 0, "num_epochs": 1, - "micro_batch_size": 1, + "micro_batch_size": 4, "gradient_accumulation_steps": 1, "output_dir": temp_dir, - "learning_rate": 0.00001, + "learning_rate": 2e-4, "optimizer": "paged_adamw_8bit", "lr_scheduler": "cosine", "flash_attention": True, - "max_steps": 10, - "save_steps": 10, - "eval_steps": 10, + "max_steps": 50, + "warmup_steps": 5, + "logging_steps": 1, + "save_steps": 50, + "eval_steps": 50, "bf16": "auto", "save_first_step": False, + "use_tensorboard": True, + "seed": 42, } ) cfg = validate_config(cfg) @@ -110,3 +129,10 @@ def test_phi_qlora(self, temp_dir): train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + check_tensorboard_loss_decreased( + temp_dir + "/runs", + initial_window=5, + final_window=5, + max_initial=5.0, + max_final=4.7, + ) diff --git a/tests/e2e/test_preprocess.py b/tests/e2e/test_preprocess.py index 8f15cbe550..895c29c872 100644 --- a/tests/e2e/test_preprocess.py +++ b/tests/e2e/test_preprocess.py @@ -18,7 +18,7 @@ def test_w_deepspeed(self, temp_dir): cfg = DictDefault( { - "base_model": "Qwen/Qwen2.5-0.5B", + "base_model": "axolotl-ai-co/tiny-qwen2-129m", "sequence_len": 2048, "val_set_size": 0.01, "datasets": [ diff --git a/tests/e2e/test_quantization.py b/tests/e2e/test_quantization.py index a70a46194c..bb8ce969cc 100644 --- a/tests/e2e/test_quantization.py +++ b/tests/e2e/test_quantization.py @@ -45,7 +45,7 @@ def _get_fake_quant_config_dtype(config): @pytest.fixture() def model(): dummy_model = AutoModelForCausalLM.from_pretrained( - "Qwen/Qwen2-0.5B", + "axolotl-ai-co/tiny-qwen2-129m", device_map="auto", dtype=torch.bfloat16, ) diff --git a/tests/e2e/test_qwen.py b/tests/e2e/test_qwen.py index 1c75d817b8..b8654c0ad6 100644 --- a/tests/e2e/test_qwen.py +++ b/tests/e2e/test_qwen.py @@ -17,7 +17,7 @@ class TestE2eQwen: Test cases for qwen models """ - @pytest.mark.parametrize("base_model", ["Qwen/Qwen2-0.5B", "Qwen/Qwen2.5-0.5B"]) + @pytest.mark.parametrize("base_model", ["axolotl-ai-co/tiny-qwen2-129m"]) def test_dpo(self, base_model, temp_dir): cfg = DictDefault( { diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index e1eaca050d..8306b72cea 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -199,6 +199,106 @@ def check_tensorboard( assert df.value.values[-1] > 1e-5, "Expected loss to be greater than zero" +def check_tensorboard_loss_decreased( + temp_run_dir: str, + tag: str | None = None, + initial_window: int = 1, + final_window: int = 1, + min_delta: float | None = None, + max_initial: float | None = None, + max_final: float | None = None, + max_loss_ratio: float = 0.95, +) -> None: + """Check that training actually learned — loss went down and stayed in + a sensible range. + + Used with the tiny ``axolotl-ai-co/tiny-*`` CI models, where pretraining + was brief enough that final loss won't clear the absolute thresholds used + for 135M+ models — but the training pipeline should still behave. + + ``train/train_loss`` is only logged once (end-of-training aggregate). The + per-step tag is ``train/loss`` for SFT/LM trainers and may vary across + trainers (e.g. DPO). When ``tag`` is None we try common per-step tags in + order and use the first with enough samples. + + Two kinds of regression we guard against: + + 1. **Loss blew up.** A silent bug (e.g. broken label masking) can start + training at an absurdly high loss. ``max_initial`` / ``max_final`` + assert the measured means stay at-or-below bounds measured from a + known-good run. Both are optional but strongly encouraged — loss + going *down* from a bad starting scale still looks like "learning." + + 2. **Loss didn't go down enough.** ``max_loss_ratio`` (default 0.95) + requires ``final <= initial * ratio``. A default below 1.0 means the + final window mean must sit at least 5% below the initial window mean + — real learning, not noise that happened to land below start. Only + raise this for configs where a smaller drop is expected *and* + documented (e.g. DPO with near-trivial pairs); in that case you are + intentionally weakening the test. + + ``min_delta`` is optional; when set, additionally requires + ``final + min_delta <= initial`` — use for configs with enough signal + to demand a specific minimum absolute drop. + """ + tb_log_path = most_recent_subdir(temp_run_dir) + event_file = os.path.join(tb_log_path, sorted(os.listdir(tb_log_path))[0]) + reader = SummaryReader(event_file) + df = reader.scalars + + if tag is None: + candidates = ["train/loss", "train/train_loss"] + else: + candidates = [tag] + + required = initial_window + final_window + chosen_tag, values = None, None + for candidate in candidates: + sub = df[df.tag == candidate] + if len(sub) >= required: + chosen_tag = candidate + values = sub.value.values + break + + available = sorted({t for t in df.tag.unique() if "loss" in t.lower()}) + assert values is not None, ( + f"None of the tags {candidates} had ≥{required} logged steps. " + f"Loss tags present: {available}" + ) + + initial = float(values[:initial_window].mean()) + final = float(values[-final_window:].mean()) + print( + f"[check_tensorboard_loss_decreased] tag={chosen_tag} n={len(values)} " + f"initial_mean{initial_window}={initial:.4f} final_mean{final_window}={final:.4f}" + ) + assert final > 1e-5, "Expected loss to be greater than zero" + assert final <= initial * max_loss_ratio, ( + f"Loss did not decrease for {chosen_tag}: " + f"initial(mean of first {initial_window})={initial:.4f}, " + f"final(mean of last {final_window})={final:.4f}, " + f"ratio={final / initial:.4f} (max allowed {max_loss_ratio}). " + f"Expected final <= initial — training did not learn." + ) + if min_delta is not None: + assert final + min_delta <= initial, ( + f"Expected loss to decrease by at least {min_delta} for {chosen_tag}: " + f"initial={initial:.4f}, final={final:.4f}, delta={initial - final:.4f}" + ) + if max_initial is not None: + assert initial <= max_initial, ( + f"Initial loss {initial:.4f} is above the expected max {max_initial}. " + f"Absolute scale is wrong — probably a silent regression " + f"(e.g. bad label masking) that bumped the starting point." + ) + if max_final is not None: + assert final <= max_final, ( + f"Final loss {final:.4f} is above the expected max {max_final}. " + f"Absolute scale is wrong — probably a silent regression " + f"(e.g. bad label masking) that bumped the endpoint." + ) + + def check_model_output_exists(temp_dir: str, cfg: DictDefault) -> None: """ helper function to check if a model output file exists after training From ebbd7fa8473c2412cfd78b4f6cc0233e5d907970 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 29 Apr 2026 22:46:51 +0700 Subject: [PATCH 1286/1405] feat: Add Mistral Medium 3.5 (#3633) * fix: clarify incompat * fix: transformers api change upstream * fix: add pre prop * feat: add examples * chore: cleanup * chore: update readme --- README.md | 3 + docs/attention.qmd | 4 +- docs/scripts/examples-allowlist.yml | 2 + examples/devstral/devstral-small-qlora.yml | 3 +- examples/ministral3/ministral3-3b-qlora.yaml | 2 +- examples/mistral-medium-3_5/README.md | 78 ++++++++++++++++++++ examples/mistral-medium-3_5/qlora-text.yml | 56 ++++++++++++++ examples/mistral-medium-3_5/qlora-vision.yml | 61 +++++++++++++++ src/axolotl/utils/collators/mm_chat.py | 8 +- 9 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 examples/mistral-medium-3_5/README.md create mode 100644 examples/mistral-medium-3_5/qlora-text.yml create mode 100644 examples/mistral-medium-3_5/qlora-vision.yml diff --git a/README.md b/README.md index 73e73d6aed..7e8d3bf486 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ ## 🎉 Latest Updates +- 2026/04: + - New model support has been added in Axolotl for [Mistral Medium 3.5](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/mistral-medium-3_5) and [Gemma 4](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/gemma4). + - Axolotl is now [uv-first](https://github.com/axolotl-ai-cloud/axolotl/pull/3545) and has [SonicMoE fused LoRA](https://github.com/axolotl-ai-cloud/axolotl/pull/3519) support. - 2026/03: - New model support has been added in Axolotl for [Mistral Small 4](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/mistral4), [Qwen3.5, Qwen3.5 MoE](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/qwen3.5), [GLM-4.7-Flash](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm47-flash), [GLM-4.6V](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm46v), and [GLM-4.5-Air](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/glm45). - [MoE expert quantization](https://docs.axolotl.ai/docs/expert_quantization.html) support (via `quantize_moe_experts: true`) greatly reduces VRAM when training MoE models (FSDP2 compat). diff --git a/docs/attention.qmd b/docs/attention.qmd index 771299a293..b9644e0749 100644 --- a/docs/attention.qmd +++ b/docs/attention.qmd @@ -54,8 +54,10 @@ python setup.py install Requirements: Hopper or Blackwell GPUs +FA4 is still a pre-release on PyPI, so `--pre` is required: + ```bash -pip install flash-attn-4 +pip install --pre flash-attn-4 ``` Or from source: diff --git a/docs/scripts/examples-allowlist.yml b/docs/scripts/examples-allowlist.yml index 50acaea8e3..f2fe88e08b 100644 --- a/docs/scripts/examples-allowlist.yml +++ b/docs/scripts/examples-allowlist.yml @@ -20,6 +20,8 @@ examples: title: Arcee AFM # MistralAI + - name: mistral-medium-3_5 + title: Mistral Medium 3.5 - name: ministral3/think title: Ministral 3 Thinking - name: ministral3/vision diff --git a/examples/devstral/devstral-small-qlora.yml b/examples/devstral/devstral-small-qlora.yml index ca8e8e0432..3eafb92194 100644 --- a/examples/devstral/devstral-small-qlora.yml +++ b/examples/devstral/devstral-small-qlora.yml @@ -26,7 +26,6 @@ lora_model_dir: sequence_len: 2048 sample_packing: true - lora_r: 32 lora_alpha: 16 lora_dropout: 0 @@ -52,7 +51,7 @@ gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 flash_attention: true -scaling_softmax: true +# scaling_softmax: true # needs flex_attention loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/ministral3/ministral3-3b-qlora.yaml b/examples/ministral3/ministral3-3b-qlora.yaml index b369c9d416..4efe5bd2fa 100644 --- a/examples/ministral3/ministral3-3b-qlora.yaml +++ b/examples/ministral3/ministral3-3b-qlora.yaml @@ -59,7 +59,7 @@ gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 flash_attention: true -scaling_softmax: true +# scaling_softmax: true # needs flex_attention warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/mistral-medium-3_5/README.md b/examples/mistral-medium-3_5/README.md new file mode 100644 index 0000000000..f90397a030 --- /dev/null +++ b/examples/mistral-medium-3_5/README.md @@ -0,0 +1,78 @@ +# Finetune Mistral Medium 3.5 with Axolotl + +[Mistral Medium 3.5](https://huggingface.co/mistralai/Mistral-Medium-3.5-128B) is a 128B parameter dense multimodal model from MistralAI that unifies instruct, reasoning, and agentic capabilities into a single model. +It shares the `mistral3` architecture (dense, YaRN RoPE, 256k context) with Ministral 3 and supports the same `reasoning_effort` toggle as Mistral Small 4. + +Thanks to the team at MistralAI for giving us early access to prepare for this release. + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +3. (Text config only) Install [Flash Attention 4](https://docs.axolotl.ai/docs/attention.html#flash-attention-4) on Hopper/Blackwell. + +4. Run one of the example configs: + + ```bash + # text-only + axolotl train examples/mistral-medium-3_5/qlora-text.yml # ~83.1 GiB + + # text + vision + # wget https://huggingface.co/datasets/Nanobit/text-vision-2k-test/resolve/main/African_elephant.jpg + axolotl train examples/mistral-medium-3_5/qlora-vision.yml # ~80.3 GiB + ``` + +Note: vision training does not currently work with Flash Attention 4. + +## Reasoning Effort + +The chat template supports a `reasoning_effort` variable to control the model's reasoning depth: + +- `"none"` — instruct mode (default) +- `"high"` — reasoning mode with explicit thinking steps + +Pass it via `chat_template_kwargs` under your dataset config: + +```yaml +datasets: + - path: your/dataset + type: chat_template + chat_template_kwargs: + reasoning_effort: high +``` + +## Thinking Support + +The chat template supports a `thinking` content type in assistant messages for training on reasoning traces (rendered as `[THINK]...[/THINK]` blocks). + +To use thinking datasets, add the `thinking` mapping via `message_property_mappings`: + +```yaml +datasets: + - path: your/thinking-dataset + type: chat_template + message_property_mappings: + role: role + content: content + thinking: thinking + chat_template_kwargs: + reasoning_effort: high +``` + +See the [Magistral thinking guide](../magistral/think/README.md) for dataset format details. + +## Tips + +- For smaller experiments on the same architecture, see [`examples/ministral3`](../ministral3/README.md) (Ministral 3, 3B). +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The text dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). +- The vision model requires multi-modal dataset format as documented [here](https://docs.axolotl.ai/docs/multimodal.html#dataset-format). + +## Related Resources + +- [Mistral Medium 3.5 Blog](https://mistral.ai/news/vibe-remote-agents-mistral-medium-3-5) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/mistral-medium-3_5/qlora-text.yml b/examples/mistral-medium-3_5/qlora-text.yml new file mode 100644 index 0000000000..2ff5b1af38 --- /dev/null +++ b/examples/mistral-medium-3_5/qlora-text.yml @@ -0,0 +1,56 @@ +base_model: axolotl-ai-co/Mistral-Medium-3.5-128B-BF16 + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_4bit: true + +datasets: + - path: fozziethebeat/alpaca_messages_2k_test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 +sample_packing: true + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0 +# prevents targeting vision layers +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' + +lora_mlp_kernel: true +lora_qkv_kernel: true +lora_o_kernel: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +max_steps: 10 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 diff --git a/examples/mistral-medium-3_5/qlora-vision.yml b/examples/mistral-medium-3_5/qlora-vision.yml new file mode 100644 index 0000000000..a8a51116b6 --- /dev/null +++ b/examples/mistral-medium-3_5/qlora-vision.yml @@ -0,0 +1,61 @@ +base_model: axolotl-ai-co/Mistral-Medium-3.5-128B-BF16 +processor_type: AutoProcessor + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + +load_in_4bit: true + +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +# sample dataset below requires downloading image in advance +# wget https://huggingface.co/datasets/Nanobit/text-vision-2k-test/resolve/main/African_elephant.jpg +datasets: + - path: Nanobit/text-vision-2k-test + type: chat_template + +dataset_prepared_path: last_run_prepared +val_set_size: 0.1 +output_dir: + +adapter: qlora +lora_model_dir: + +sequence_len: 2048 + +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0 +lora_target_modules: 'model.language_model.layers.[\d]+.(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' + +lora_mlp_kernel: true +lora_qkv_kernel: true +lora_o_kernel: true + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 4 +micro_batch_size: 2 +num_epochs: 1 +max_steps: 10 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 1 +saves_per_epoch: 1 diff --git a/src/axolotl/utils/collators/mm_chat.py b/src/axolotl/utils/collators/mm_chat.py index 542918527c..b81612cbc5 100644 --- a/src/axolotl/utils/collators/mm_chat.py +++ b/src/axolotl/utils/collators/mm_chat.py @@ -47,10 +47,12 @@ def process_rows( messages, add_generation_prompt=False, tokenize=True, - return_tensors="pt", - padding=True, - return_dict=True, chat_template=self.processing_strategy.chat_template, + processor_kwargs={ + "return_tensors": "pt", + "padding": True, + "return_dict": True, + }, ) # Process the labels From e662972a2912b395da4817bf13d73b7d4a6d9137 Mon Sep 17 00:00:00 2001 From: Younes B <49240599+younesbelkada@users.noreply.github.com> Date: Thu, 30 Apr 2026 19:25:02 +0400 Subject: [PATCH 1287/1405] Feat: Add bitnet integration (#3634) * add bitnet * switch to uv * chore: liint --------- Co-authored-by: Wing Lian --- _quarto.yml | 1 + docs/1_58bit_finetuning.qmd | 70 +++++++++++++++++++++++++ src/axolotl/loaders/model.py | 11 ++++ src/axolotl/utils/schemas/model.py | 6 +++ src/axolotl/utils/schemas/validation.py | 6 +++ 5 files changed, 94 insertions(+) create mode 100644 docs/1_58bit_finetuning.qmd diff --git a/_quarto.yml b/_quarto.yml index e8263a971a..5b008bf99f 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -311,6 +311,7 @@ website: - docs/dataset_loading.qmd - docs/qat.qmd - docs/quantize.qmd + - docs/1_58bit_finetuning.qmd - docs/optimizations.qmd - section: "Core Concepts" diff --git a/docs/1_58bit_finetuning.qmd b/docs/1_58bit_finetuning.qmd new file mode 100644 index 0000000000..02bc3a6f18 --- /dev/null +++ b/docs/1_58bit_finetuning.qmd @@ -0,0 +1,70 @@ +--- +title: "1.58-bit Finetuning" +back-to-top-navigation: true +toc: true +toc-expand: 2 +toc-depth: 4 +--- + +## Overview + +1.58-bit finetuning allows you to finetune BitNet models when their prequantized weights are provided. In theory, it will be possible to fine-tune any LLM in 1.58bit format but the performance degradation will be dramatic. + +Axolotl supports 1.58-bit finetuning via the [`onebitllms`](https://github.com/tiiuae/onebitllms) library, which replaces standard linear layers with BitNet-compatible counterparts ready to use for training. + +::: {.callout-note} +LoRA is not supported for BitNet models +::: + +## Installation + +Install the `onebitllms` package before using this feature: + +```bash +uv pip install onebitllms +``` + +Or from source: + +```bash +uv pip install git+https://github.com/tiiuae/onebitllms +``` + +## Supported models + +For now, only `Falcon-E` series of models are supported. Make sure to use their `-prequantized` version: + +```bash +tiiuae/Falcon-E-3B-Base-prequantized +tiiuae/Falcon-E-1B-Base-prequantized +``` + +In theory, any other model would 'work' but the performance degradation will be huge. This remains an area of exploration. + +## Configuration + +To enable 1.58-bit finetuning, set the following in your configuration file: + +```yaml +base_model: tiiuae/Falcon-E-3B-Base-prequantized # A BitNet-compatible model + +use_onebitllms: true +``` + +::: {.callout-note} +For BitNet models, it is recommended to use a higher learning rate than classic models (usually in the order of magnitude of 10x). +::: + +## Considerations after training + +Once your model has been trained with 1.58bit fine-tuning, you can convert the trained model in ternary format using the `onebitllms` CLI: + +```bash +onebitllms quantize_to_1bit INPUT_PATH OUTPUT_PATH +``` + +After that, you can use supported packages such as `llama.cpp` or Apple MLX package to run the trained model. + +## Example Configuration + +You can find example configurations in `examples/falcon-e` which contain one configuration for SFT and one configuration for DPO. diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 4f57793272..57aabfbab2 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -846,6 +846,17 @@ def _build_model(self) -> bool: else: self.model = self._load_model_from_pretrained(model_loader_class) + if self.cfg.use_onebitllms: + try: + from onebitllms import replace_linear_with_bitnet_linear + except ImportError as exc: + raise ImportError( + "The 'onebitllms' package is required for use_onebitllms. " + "Install it with: `uv pip install onebitllms`" + ) from exc + + self.model = replace_linear_with_bitnet_linear(self.model) + if is_deepspeed_zero3_enabled(): skip_move_to_device = True diff --git a/src/axolotl/utils/schemas/model.py b/src/axolotl/utils/schemas/model.py index f54958b330..30202efe06 100644 --- a/src/axolotl/utils/schemas/model.py +++ b/src/axolotl/utils/schemas/model.py @@ -103,6 +103,12 @@ class ModelInputConfig(BaseModel): default=None, json_schema_extra={"description": "kwargs for model quantization config"}, ) + use_onebitllms: bool | None = Field( + default=None, + json_schema_extra={ + "description": "Whether to use `onebitllms` for 1.58bit training (only for bitnet models)." + }, + ) @field_validator("trust_remote_code") @classmethod diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index fff69de260..76b09bfdbb 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -638,6 +638,12 @@ def check_fused_lora(self): raise ValueError("Fused modules are not supported with LoRA/QLoRA") return self + @model_validator(mode="after") + def check_onebitllms_lora(self): + if self.use_onebitllms and self.adapter in ["lora", "qlora"]: + raise ValueError("LoRA/QLoRA is not supported with use_onebitllms") + return self + @model_validator(mode="before") @classmethod def warn_qlora_zero3_w_use_reentrant(cls, data): From 6136ae627b5b79bb3e2b71394bc1342652fbe1af Mon Sep 17 00:00:00 2001 From: Younes B <49240599+younesbelkada@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:30:56 +0400 Subject: [PATCH 1288/1405] Fix: add bitnet config (#3636) * add bitnet config * chore: lint --------- Co-authored-by: Wing Lian --- examples/falcon-e/falcon-e-3b-dpo.yaml | 93 +++++++++++++++++++++++ examples/falcon-e/falcon-e-3b-ft.yaml | 100 +++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 examples/falcon-e/falcon-e-3b-dpo.yaml create mode 100644 examples/falcon-e/falcon-e-3b-ft.yaml diff --git a/examples/falcon-e/falcon-e-3b-dpo.yaml b/examples/falcon-e/falcon-e-3b-dpo.yaml new file mode 100644 index 0000000000..72d1cc41af --- /dev/null +++ b/examples/falcon-e/falcon-e-3b-dpo.yaml @@ -0,0 +1,93 @@ +base_model: axolotl-ai-co/Falcon-E-1.2-3B-Exp-prequantized +output_dir: ./output + +plugins: + - axolotl.integrations.kernels.KernelsPlugin + +use_kernels: false +use_scattermoe: false +use_sonicmoe: false +use_onebitllms: true + +load_in_8bit: false +load_in_4bit: false + +chat_template: tokenizer_default + +rl: dpo +datasets: + - path: allenai/Dolci-Think-DPO-7B + split: train + type: chatml.ultra + +dataset_prepared_path: ./axolotl_dataset_cache + +sequence_len: 8192 +trust_remote_code: false + +gradient_accumulation_steps: 4 # This can run on 4 GPUs + +# Very important to enable gradient accumulation with FSDP +# https://github.com/huggingface/transformers/issues/29425 +accelerator_config: + gradient_accumulation_kwargs: + sync_each_batch: True + + +micro_batch_size: 1 +num_epochs: 3 +optimizer: adamw_torch +lr_scheduler: cosine +learning_rate: 1.0e-5 +# adamw hyperparams +adam_beta1: 0.9 +adam_beta2: 0.95 + +bf16: true +tf32: false + +logging_steps: 1 + +flash_attention: true + +loss_watchdog_threshold: 15.0 +loss_watchdog_patience: 3 + +warmup_steps: 128 +evals_per_epoch: 0 + +save_steps: 500 +save_strategy: steps + +weight_decay: 0.01 + +shuffle_merged_datasets: true +experimental_skip_move_to_device: true + +fsdp_version: 2 +fsdp_config: + offload_params: false + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: LlamaDecoderLayer + state_dict_type: FULL_STATE_DICT + reshard_after_forward: true + activation_checkpointing: true + +# Comment to disable CP +# The number of GPUs to shard the model parameters across (FSDP dimension). +dp_shard_size: 1 + +# The number of times to replicate the sharded model (DDP dimension). +dp_replicate_size: 1 + +# Number of GPUs for Tensor Parallelism. +tensor_parallel_size: 1 # (default is 1, no TP) + +# Number of GPUs for Context/Sequence Parallelism. +context_parallel_size: 1 # (default is 1, no CP) + +special_tokens: + eos_token: <|end_of_text|> + +eot_tokens: + - <|im_end|> diff --git a/examples/falcon-e/falcon-e-3b-ft.yaml b/examples/falcon-e/falcon-e-3b-ft.yaml new file mode 100644 index 0000000000..0898271cfe --- /dev/null +++ b/examples/falcon-e/falcon-e-3b-ft.yaml @@ -0,0 +1,100 @@ +base_model: tiiuae/Falcon-E-3B-Base-prequantized +output_dir: ./output + +plugins: + - axolotl.integrations.kernels.KernelsPlugin + +use_kernels: false +use_scattermoe: false +use_sonicmoe: false +use_onebitllms: true + +load_in_8bit: false +load_in_4bit: false + +chat_template: tokenizer_default + +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +dataset_prepared_path: ./axolotl_dataset_cache + +sequence_len: 32768 +trust_remote_code: false + + +gradient_accumulation_steps: 4 # This can run on 4 GPUs + +# Very important to enable gradient accumulation with FSDP +# https://github.com/huggingface/transformers/issues/29425 +accelerator_config: + gradient_accumulation_kwargs: + sync_each_batch: True + + +micro_batch_size: 1 +num_epochs: 3 +optimizer: adamw_torch +lr_scheduler: cosine +learning_rate: 5.0e-4 +# adamw hyperparams +adam_beta1: 0.9 +adam_beta2: 0.95 + +bf16: true +tf32: false + +logging_steps: 1 + +flash_attention: true + +loss_watchdog_threshold: 15.0 +loss_watchdog_patience: 3 + +warmup_steps: 128 +evals_per_epoch: 0 + +save_steps: 500 +save_strategy: steps + +weight_decay: 0.01 + +sample_packing: true +pad_to_sequence_len: true + +shuffle_merged_datasets: true +experimental_skip_move_to_device: true + +fsdp_version: 2 +fsdp_config: + offload_params: false + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: LlamaDecoderLayer + state_dict_type: FULL_STATE_DICT + reshard_after_forward: true + activation_checkpointing: true +# save_first_step: true # uncomment this to validate checkpoint saving works with your config + +# Comment to disable CP +# The number of GPUs to shard the model parameters across (FSDP dimension). +dp_shard_size: 1 + +# The number of times to replicate the sharded model (DDP dimension). +dp_replicate_size: 1 + +# Number of GPUs for Tensor Parallelism. +tensor_parallel_size: 1 # (default is 1, no TP) + +# Number of GPUs for Context/Sequence Parallelism. +context_parallel_size: 1 # (default is 1, no CP) + +special_tokens: + eos_token: <|end_of_text|> + +eot_tokens: + - <|im_end|> From e4032fc90f462fd9c2eefd2433c2f4fd0845641f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 5 May 2026 10:15:18 -0400 Subject: [PATCH 1289/1405] Refactor separate attention flags with attn_implementation and capability/concerns feature flags (#3602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * upgrade to torchao 0.17.0 * chore: lint * refactor attention handling * replace legacy attention boolean flags with capability properties Replace checks with capability-based properties derived from attn_implementation This separates three concerns that were conflated under flash_attention: 1. Backend selection -> attn_implementation enum 2. Packing capability -> attn_supports_packing property 3. Flash-attn library dependency -> attn_uses_flash_lib property * compute attn capability flags in normalizer instead of properties * make attn_implementation the single source of truth * move attention-dependent validators to mode=after * migrate remaining consumers to canonical attn_implementation * expand attention tests + rewrite docs * migrate example configs to canonical attn_implementation * update doc snippets + reject gemma4-hybrid with non-FA2 backend * remove dead gemma4 branch in _set_attention_config * fix duplicate attn_implementation in gpt-oss yamls and flaky caplog tests * drop "Phase 2" naming from attn-implementation tests * regroup attn_implementation tests by feature concern * clean up verbose comments and remove MD Signed-off-by: Wing Lian Co-authored-by: Axolotl Swarm * fix(collator): pass return_dict=True at apply_chat_template top level for transformers 5.x In transformers 5.x, ProcessorMixin.apply_chat_template gained its own `return_dict` parameter (defaulting to False). When return_dict=False and tokenize=True the method returns out["input_ids"] directly — a 2-D tensor — rather than the full BatchFeature dict. The old code placed `return_dict=True` inside processor_kwargs. In transformers 5.x those kwargs are forwarded to the underlying processor call self(...) where _merge_kwargs silently ignores any key not present in MllamaProcessorKwargs (emitting a warning). The outer return_dict therefore stayed False, apply_chat_template returned the raw input_ids tensor, and the subsequent `batch["input_ids"]` attempted to index a 2-D tensor with the 9-character string "input_ids", producing: IndexError: too many indices for tensor of dimension 2 The fix is to pass return_dict=True as a top-level keyword argument to apply_chat_template (where it is actually consumed) and remove it from processor_kwargs (where it was silently dropped). No version guard is needed: transformers is pinned to ==5.5.4 in pyproject.toml. Adds a unit-level regression test (tests/test_mm_chat_collator.py) that mocks the processor to return a raw tensor when apply_chat_template is called without top-level return_dict=True, verifying the four invariants: process_rows returns a dict, input_ids is 2-D, labels is 2-D, and apply_chat_template receives return_dict=True as a top-level kwarg. Fixes: tests/e2e/test_llama_vision.py::TestLlamaVision::test_lora_llama_vision_multimodal_dataset Fixes: tests/e2e/test_llama_vision.py::TestLlamaVision::test_lora_llama_vision_text_only_dataset Signed-off-by: Wing Lian Co-authored-by: Axolotl Swarm * fix(collator): process_rows returns dict (BatchFeature) shape Two related changes for the multimodal chat collator under transformers 5.x: 1. Wrap apply_chat_template result in dict(...) so process_rows returns a plain dict rather than a BatchFeature instance. BatchFeature is a Mapping but not a dict; downstream code that did batch["labels"] = self.processing_strategy.process_labels(batch["input_ids"]) would index on a tensor when the result wasn't dict-shaped, raising IndexError: too many indices for tensor of dimension 2 2. Soften the regression test's contract from `dict` to `Mapping` so it exercises the actual semantic guarantee (key/value access) rather than the implementation detail (dict vs BatchFeature). Test guards against the original transformers 5.x breakage where apply_chat_template's return_dict default went from True to False. Includes regression test under tests/test_mm_chat_collator.py. Bug surfaced via swarm dispatch task_01KQHPNAYD8XARSNSDJVW1GPF6 against attn-implementation-refactor; squash-merged from agent commits 4de886fd + dc9fcf4f. Signed-off-by: Wing Lian --------- Signed-off-by: Wing Lian Co-authored-by: Axolotl Swarm --- docs/agents/new_model_support.md | 10 +- docs/agents/sft.md | 2 +- docs/attention.qmd | 184 +++++--- docs/ebft.qmd | 4 +- docs/grpo.qmd | 2 +- docs/optimizations.qmd | 10 +- docs/rlhf.qmd | 3 +- docs/sequence_parallelism.qmd | 2 +- docs/training_stability.qmd | 2 +- examples/LiquidAI/lfm2-350m-fft.yaml | 2 +- examples/LiquidAI/lfm2-8b-a1b-lora.yaml | 2 +- examples/LiquidAI/lfm2-vl-lora.yaml | 3 +- examples/alst/llama3-8b-deepspeed-alst.yaml | 2 +- examples/alst/llama3-8b-fsdp2-alst.yaml | 2 +- examples/apertus/apertus-8b-qlora.yaml | 2 +- examples/arcee/afm-4.5b-qlora.yaml | 2 +- examples/archived/cerebras/btlm-ft.yml | 3 +- examples/archived/cerebras/qlora.yml | 3 +- examples/archived/code-llama/13b/lora.yml | 2 +- examples/archived/code-llama/13b/qlora.yml | 2 +- examples/archived/code-llama/34b/lora.yml | 2 +- examples/archived/code-llama/34b/qlora.yml | 2 +- examples/archived/code-llama/7b/lora.yml | 2 +- examples/archived/code-llama/7b/qlora.yml | 2 +- examples/archived/dbrx/16bit-lora.yaml | 2 +- examples/archived/dbrx/8bit-lora.yaml | 2 +- examples/archived/dbrx/fft-ds-zero3.yaml | 2 +- .../deepcoder/deepcoder-14B-preview-lora.yml | 2 +- examples/archived/falcon/config-7b-lora.yml | 3 +- examples/archived/falcon/config-7b-qlora.yml | 3 +- examples/archived/falcon/config-7b.yml | 3 +- examples/archived/gemma/qlora.yml | 2 +- examples/archived/gptj/qlora.yml | 3 +- examples/archived/jeopardy-bot/config.yml | 3 +- examples/archived/mpt-7b/config.yml | 1 - examples/archived/openllama-3b/config.yml | 2 +- examples/archived/openllama-3b/lora.yml | 2 +- examples/archived/openllama-3b/qlora.yml | 2 +- examples/archived/qwen/lora.yml | 1 - examples/archived/qwen/qlora.yml | 1 - examples/archived/qwen/qwen2-moe-lora.yaml | 2 +- examples/archived/qwen/qwen2-moe-qlora.yaml | 2 +- examples/archived/redpajama/config-3b.yml | 1 - examples/archived/replit-3b/config-lora.yml | 1 - examples/archived/stablelm-2/1.6b/fft.yml | 2 +- examples/archived/stablelm-2/1.6b/lora.yml | 2 +- examples/archived/starcoder2/qlora.yml | 2 +- examples/archived/tiny-llama/lora-mps.yml | 1 - examples/archived/tiny-llama/lora.yml | 2 +- examples/archived/tiny-llama/pretrain.yml | 2 +- examples/archived/tiny-llama/qlora.yml | 2 +- .../archived/xgen-7b/xgen-7b-8k-qlora.yml | 3 +- examples/archived/yi-34B-chat/qlora.yml | 2 +- examples/cohere/command-r-7b-qlora.yml | 2 +- .../cogito-v1-preview-llama-3B-lora.yml | 2 +- .../cogito-v1-preview-qwen-14B-lora.yml | 2 +- examples/deepseek-v2/fft-fsdp-16b.yaml | 2 +- examples/deepseek-v2/qlora-fsdp-2_5.yaml | 2 +- examples/devstral/devstral-small-qlora.yml | 2 +- .../llama-3_1-8b-hsdp-tp.yaml | 2 +- .../qwen3-8b-fsdp-tp-cp.yaml | 2 +- examples/eaft/eaft-example.yml | 3 +- .../ebft/llama-1b-ebft-opencode-novllm.yaml | 2 +- examples/ebft/llama-1b-ebft-opencode.yaml | 2 +- .../llama-1b-ebft-strided-structured.yaml | 3 +- examples/ebft/llama-1b-ebft-strided.yaml | 1 - examples/ebft/llama-3b-ebft-strided-fft.yaml | 1 - examples/ebft/llama-8b-ebft-strided-fft.yaml | 1 - .../ebft/qwen35-4b-ebft-structured-async.yaml | 2 +- examples/ebft/qwen35-4b-ebft-structured.yaml | 2 +- examples/ebft/qwen35-9b-ebft-structured.yaml | 2 +- .../falcon-h1/falcon-h1-1b-deep-qlora.yaml | 2 +- examples/falcon-h1/falcon-h1-1b-qlora.yaml | 2 +- examples/falcon-h1/falcon-h1-34b-qlora.yaml | 2 +- examples/falcon-h1/falcon-h1-3b-qlora.yaml | 2 +- examples/falcon-h1/falcon-h1-500m-qlora.yaml | 2 +- examples/falcon-h1/falcon-h1-7b-qlora.yaml | 2 +- examples/gemma2/qlora.yml | 2 +- examples/gemma2/reward-model.yaml | 2 +- examples/gemma3/gemma-3-1b-qlora.yml | 2 +- examples/gemma3/gemma-3-270m-qlora.yml | 2 +- examples/gemma3/gemma-3-4b-qlora.yml | 3 +- examples/gemma3/gemma-3-4b-vision-qlora.yml | 3 +- examples/gemma4/26b-a4b-moe-qlora.yaml | 2 +- examples/gemma4/31b-qlora-flex.yaml | 2 +- examples/gemma4/31b-qlora.yaml | 2 +- examples/gemma4/e2b-vision-lora.yaml | 2 +- examples/glm4/qlora-32b.yaml | 2 +- examples/glm45/glm-45-air-qlora.yaml | 2 +- examples/glm46v/glm-4-6v-flash-ddp.yaml | 2 +- examples/glm46v/glm-4-6v-flash-qlora.yaml | 2 +- examples/glm47-flash/lora.yaml | 2 +- examples/glm47-flash/lora_fsdp.yaml | 2 +- examples/glm47-flash/qlora.yaml | 2 +- examples/glm47-flash/qlora_fsdp.yaml | 2 +- .../gpt-oss-120b-fft-fsdp2-offload.yaml | 1 - .../gpt-oss-20b-fft-deepspeed-zero3.yaml | 1 - .../gpt-oss-20b-fft-fsdp2-offload.yaml | 1 - examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml | 1 - .../gpt-oss-20b-sft-lora-singlegpu.yaml | 1 - ...-oss-safeguard-20b-sft-lora-singlegpu.yaml | 1 - examples/granite4/granite-4.0-tiny-fft.yaml | 2 +- examples/hunyuan/hunyuan-v1-dense-qlora.yaml | 2 +- examples/internvl3_5/internvl3_5-8b-qlora.yml | 3 +- examples/jamba/qlora.yaml | 2 +- examples/jamba/qlora_deepspeed.yaml | 2 +- examples/jamba/qlora_fsdp_large.yaml | 2 +- examples/kimi-linear/kimi-48b-lora.yaml | 2 +- examples/llama-2/fft_optimized.yml | 2 +- examples/llama-2/gptq-lora.yml | 2 - examples/llama-2/lisa.yml | 2 +- examples/llama-2/loftq.yml | 2 +- examples/llama-2/lora.yml | 2 +- examples/llama-2/qlora-fsdp.yml | 2 +- examples/llama-2/qlora.yml | 2 +- examples/llama-2/relora.yml | 2 +- examples/llama-3-vision/lora-11b.yaml | 2 +- examples/llama-3/3b-fp8-fsdp2.yaml | 2 +- examples/llama-3/3b-qat-fsdp2.yaml | 2 +- examples/llama-3/3b-qat-mxfp4.yaml | 2 +- examples/llama-3/3b-qat-nvfp4.yaml | 2 +- examples/llama-3/diffusion/pretrain-1b.yaml | 2 +- examples/llama-3/diffusion/sft-1b.yaml | 2 +- examples/llama-3/fft-8b-liger-fsdp.yaml | 2 +- examples/llama-3/fft-8b.yaml | 2 +- examples/llama-3/instruct-dpo-lora-8b.yml | 2 +- examples/llama-3/instruct-lora-8b.yml | 2 +- examples/llama-3/lora-1b-deduplicate-dpo.yml | 2 +- examples/llama-3/lora-1b-deduplicate-sft.yml | 2 +- examples/llama-3/lora-1b-kernels.yml | 2 +- examples/llama-3/lora-1b-ray.yml | 2 +- .../lora-1b-sample-packing-sequentially.yml | 2 +- examples/llama-3/lora-1b.yml | 2 +- examples/llama-3/lora-8b.yml | 2 +- examples/llama-3/opentelemetry-qlora.yml | 1 - examples/llama-3/qlora-1b-gdpo.yaml | 2 +- examples/llama-3/qlora-1b-kto.yaml | 2 +- examples/llama-3/qlora-1b.yml | 2 +- examples/llama-3/qlora-fsdp-405b.yaml | 2 +- examples/llama-3/qlora-fsdp-70b.yaml | 2 +- examples/llama-3/qlora.yml | 2 +- examples/llama-3/sparse-finetuning.yaml | 3 +- .../do-no-use-fa2/maverick-qlora-fsdp1.yaml | 2 +- .../do-no-use-fa2/scout-qlora-fsdp1.yaml | 2 +- .../scout-qlora-single-h100.yaml | 2 +- .../scout-vision-qlora-fsdp.yaml | 2 +- .../llama-4/scout-qlora-flexattn-fsdp2.yaml | 2 +- .../llama-4/scout-qlora-single-h100-flex.yaml | 2 +- .../scout-vision-qlora-fsdp2-flex.yaml | 2 +- examples/llava/lora-7b.yaml | 3 +- .../magistral/magistral-small-fsdp-qlora.yaml | 2 +- examples/magistral/magistral-small-qlora.yaml | 2 +- .../think/magistral-small-think-qlora.yaml | 2 +- .../magistral-small-vision-24B-qlora.yml | 2 +- examples/mamba/config.yml | 1 - examples/mimo/mimo-7b-qlora.yaml | 2 +- examples/ministral/ministral-small-qlora.yaml | 2 +- examples/ministral3/ministral3-3b-qlora.yaml | 2 +- .../think/ministral3-3b-think-qlora.yaml | 2 +- .../vision/ministral3-3b-vision-qlora.yml | 2 +- .../mistral-small-3.1-24B-lora.yml | 2 +- .../mistral/bigstral/bigstral-ds-zero3.yaml | 2 +- examples/mistral/config.yml | 2 +- examples/mistral/dpo/mistral-dpo-qlora.yml | 1 - examples/mistral/lora.yml | 2 +- examples/mistral/mistral-qlora-fsdp.yml | 2 +- .../mixtral/mixtral-8x22b-qlora-fsdp.yml | 2 +- .../mistral/mixtral/mixtral-qlora-fsdp.yml | 2 +- examples/mistral/mixtral/mixtral.yml | 2 +- examples/mistral/mixtral/mixtral_22.yml | 2 +- examples/mistral/mps/lora-mps.yml | 3 +- examples/mistral/orpo/mistral-qlora-orpo.yml | 2 +- examples/mistral/qlora.yml | 2 +- examples/mistral4/fft-text.yml | 2 +- examples/mistral4/fft-vision.yml | 2 +- examples/mistral4/qlora-text.yml | 2 +- examples/mistral4/qlora-vision.yml | 2 +- examples/nemotron-h/120b-a12b-qlora.yaml | 2 +- examples/nemotron-h/nano-30b-a3b-qlora.yaml | 2 +- examples/nemotron/nemotron-mini-4b-qlora.yaml | 2 +- examples/olmo3/olmo3-7b-qlora.yaml | 2 +- examples/orpheus/finetune.yml | 2 +- examples/phi/phi-ft.yml | 2 +- examples/phi/phi-qlora.yml | 2 +- examples/phi/phi2-ft.yml | 2 +- examples/phi/phi3-ft-fsdp.yml | 2 +- examples/phi/phi3-ft.yml | 2 +- examples/pixtral/lora-12b.yml | 2 +- examples/plano/plano-4b-qlora.yaml | 2 +- examples/qat_nvfp4/Gemma3-12B_baseline.yml | 2 +- examples/qat_nvfp4/Gemma3-12B_qat.yml | 2 +- .../qat_nvfp4/Math-Gemma3-12B_baseline.yml | 2 +- examples/qat_nvfp4/Math-Gemma3-12B_qat.yml | 2 +- .../qat_nvfp4/Math-Gemma3-27B_baseline.yml | 2 +- examples/qat_nvfp4/Math-Gemma3-27B_qat.yml | 2 +- .../qat_nvfp4/Math-Qwen2.5-72B_baseline.yml | 2 +- examples/qat_nvfp4/Math-Qwen2.5-72B_qat.yml | 2 +- examples/qat_nvfp4/Qwen2.5-72B_baseline.yml | 2 +- examples/qat_nvfp4/Qwen2.5-72B_qat.yml | 2 +- examples/qwen2-vl/lora-7b.yaml | 3 +- examples/qwen2/adamw-pretrain-fsdp2.yaml | 2 +- examples/qwen2/dpo.yaml | 2 +- examples/qwen2/muon-pretrain-fsdp2.yaml | 2 +- examples/qwen2/prm.yaml | 2 +- examples/qwen2/qlora-fsdp.yaml | 2 +- examples/qwen2/reward-model.yaml | 2 +- examples/qwen2_5-vl/lora-7b.yaml | 3 +- .../qwen3-next/qwen3-next-80b-a3b-qlora.yaml | 2 +- .../qwen3.5/122b-a10b-moe-qlora-fsdp.yaml | 2 +- examples/qwen3.5/122b-a10b-moe-qlora.yaml | 2 +- examples/qwen3.5/27b-fft.yaml | 2 +- examples/qwen3.5/27b-qlora-fsdp.yaml | 2 +- examples/qwen3.5/27b-qlora.yaml | 2 +- examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml | 2 +- examples/qwen3.5/35b-a3b-moe-qlora.yaml | 2 +- examples/qwen3.5/35b-a3b-moe-vision-lora.yaml | 2 +- examples/qwen3.5/9b-fft-vision.yaml | 2 +- examples/qwen3.5/9b-lora-vision.yaml | 2 +- examples/qwen3/32b-qlora.yaml | 2 +- examples/qwen3/8b-qat-fsdp2.yml | 2 +- examples/qwen3/qlora-fsdp.yaml | 2 +- examples/seed-oss/seed-oss-36b-qlora.yaml | 2 +- examples/smolvlm2/smolvlm2-2B-lora.yaml | 3 +- examples/streaming/pretrain.yaml | 2 +- examples/streaming/sft.yaml | 2 +- examples/swanlab/dpo-swanlab-completions.yml | 2 +- .../swanlab/dpo-swanlab-full-featured.yml | 2 +- examples/swanlab/lora-swanlab-profiling.yml | 2 +- .../trinity/trinity-nano-preview-qlora.yaml | 2 +- examples/voxtral/voxtral-mini-audio-qlora.yml | 2 +- examples/voxtral/voxtral-mini-qlora.yml | 2 +- src/axolotl/cli/merge_lora.py | 2 +- src/axolotl/core/builders/causal.py | 16 +- src/axolotl/integrations/lm_eval/__init__.py | 2 +- src/axolotl/integrations/lm_eval/cli.py | 10 +- src/axolotl/integrations/swanlab/plugins.py | 4 +- src/axolotl/loaders/model.py | 44 +- src/axolotl/loaders/patch_manager.py | 52 ++- src/axolotl/loaders/tokenizer.py | 6 +- src/axolotl/monkeypatch/attention/__init__.py | 26 ++ src/axolotl/monkeypatch/attention/fp8_attn.py | 30 ++ .../monkeypatch/attention/sage_attn.py | 18 +- src/axolotl/utils/callbacks/__init__.py | 5 +- src/axolotl/utils/collators/mm_chat.py | 20 +- src/axolotl/utils/schemas/config.py | 246 +++++++++-- src/axolotl/utils/schemas/enums.py | 62 +++ src/axolotl/utils/schemas/validation.py | 215 ++++----- src/axolotl/utils/trainer.py | 2 +- tests/e2e/multigpu/test_llama.py | 4 +- tests/test_attn_implementation.py | 418 ++++++++++++++++++ tests/test_mm_chat_collator.py | 163 +++++++ tests/test_no_legacy_attn_reads.py | 62 +++ 252 files changed, 1501 insertions(+), 571 deletions(-) create mode 100644 src/axolotl/monkeypatch/attention/fp8_attn.py create mode 100644 tests/test_attn_implementation.py create mode 100644 tests/test_mm_chat_collator.py create mode 100644 tests/test_no_legacy_attn_reads.py diff --git a/docs/agents/new_model_support.md b/docs/agents/new_model_support.md index 8e60288966..bc42ada861 100644 --- a/docs/agents/new_model_support.md +++ b/docs/agents/new_model_support.md @@ -121,11 +121,11 @@ Older models that use `_prepare_4d_causal_attention_mask` (Llama, Mistral, Qwen2 | Backend | Config | head_dim limit | torch_compile | Notes | |---------|--------|---------------|---------------|-------| -| FA2 | `flash_attention: true` | 256 | ✅ | Fastest when supported | -| FA4 | auto with `flash_attention: true` | 256 (SM90+) | ✅ | Auto-detected on H100+ | -| SDPA | `sdp_attention: true` | None | ✅ | Universal fallback | -| flex | `flex_attention: true` | None | ⚠️ Triton OOM for large head_dim | Good for variable head dims | -| eager | neither set | None | ✅ | Slowest, always works | +| FA2 | `attn_implementation: flash_attention_2` | 256 | ✅ | Fastest when supported | +| FA4 | auto with `attn_implementation: flash_attention_2` | 256 (SM90+) | ✅ | Auto-detected on H100+ | +| SDPA | `attn_implementation: sdpa` | None | ✅ | Universal fallback | +| flex | `attn_implementation: flex_attention` | None | ⚠️ Triton OOM for large head_dim | Good for variable head dims | +| eager | `attn_implementation: eager` | None | ✅ | Slowest, always works | **Check model support**: Look at `_supports_flash_attn_2`, `_supports_flex_attn`, `_supports_sdpa` attributes on the model class. diff --git a/docs/agents/sft.md b/docs/agents/sft.md index d3dfd39f70..f601cb0f53 100644 --- a/docs/agents/sft.md +++ b/docs/agents/sft.md @@ -83,7 +83,7 @@ Watch for: loss never decreasing (check `train_on_inputs`, dataset, LR), loss go | Issue | Fix | |-------|-----| | OOM during training | Reduce `micro_batch_size`, enable `gradient_checkpointing`, reduce `sequence_len` | -| `sample_packing` + SDPA + bf16 = 0.0 loss | Use `flash_attention: true` or disable `sample_packing` | +| `sample_packing` + SDPA + bf16 = 0.0 loss | Use `attn_implementation: flash_attention_2` or disable `sample_packing` | | Missing chat template error | Set `chat_template: chatml` explicitly | | Label masking wrong | Run `axolotl preprocess config.yaml --debug` and inspect labels | | Loss NaN | Use `bf16: auto`, lower LR, check data for empty samples | diff --git a/docs/attention.qmd b/docs/attention.qmd index b9644e0749..f7fa5b4561 100644 --- a/docs/attention.qmd +++ b/docs/attention.qmd @@ -3,28 +3,71 @@ title: Attention description: Supported attention modules in Axolotl --- -## SDP Attention - -This is the default built-in attention in PyTorch. +Axolotl routes attention via a single config field: ```yaml -sdp_attention: true +attn_implementation: ``` -For more details: [PyTorch docs](https://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html) +`attn_implementation` is passed through to `transformers` verbatim (via +`model.config._attn_implementation`). Accepted values are the HF-native +backends, axolotl-registered backends, or a hub-kernel path. + +## Backends + +| `attn_implementation` | Description | +|---|---| +| `eager` | Plain PyTorch attention. No packing support. | +| `sdpa` | PyTorch `scaled_dot_product_attention`. No packing support. | +| `flash_attention_2` | Dao-AILab Flash Attention 2. | +| `flash_attention_3` | Dao-AILab Flash Attention 3 (Hopper+). | +| `flex_attention` | Torch Flex Attention (requires torch ≥ 2.6). | +| `xformers` | xFormers memory-efficient attention. | +| `sage` | SageAttention (QK int8 / PV fp16). | +| `s2` | Shifted-Sparse Attention (LLaMA only, FA2 under the hood). | +| `fp8` | torchao FP8 low-precision attention (requires SM90+, torch ≥ 2.11). Loaded as SDPA and patched post-load. | +| `kernels-community/flash-attn3` | HF hub FA3 kernel. | +| `kernels-community/sage-attention` | HF hub SageAttention kernel. | +| Other `/` path | Any hub-kernel path supported by `transformers`. | + +Short-form aliases (`flash`, `fa2`, `flex`, `sdp`, etc.) are **not accepted** — +set the canonical name above. + +### Capability flags + +Axolotl derives three boolean capability flags from `attn_implementation` and +exposes them on the validated config: + +- `cfg.attn_supports_packing` — backend supports varlen sample packing via + `position_ids`. Gates multipack patches and `sample_packing_drop_attention_mask`. +- `cfg.attn_uses_flash_lib` — backend needs the `flash_attn` (Dao-AILab) + monkeypatches (FA4 auto, LLaMA flash hijack, ring-FA). +- `cfg.attn_needs_dtype_cast` — backend requires fp16/bf16 embeddings + (everything except `eager` and `sdpa`). -## Flash Attention +These are **computed** — they cannot be overridden from YAML. -Axolotl supports Flash Attention 2, 3, and 4. The best available version is used automatically -based on your installed packages and GPU. +## Per-backend notes + +### SDPA + +Default PyTorch attention. See +[PyTorch docs](https://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html). ```yaml -flash_attention: true +attn_implementation: sdpa ``` -For more details: [Flash Attention](https://github.com/Dao-AILab/flash-attention/) +### Flash Attention + +Axolotl supports FA2, FA3, and FA4. The best available version is used +automatically based on your installed packages and GPU. + +```yaml +attn_implementation: flash_attention_2 # or flash_attention_3 +``` -### Flash Attention 2 +#### Flash Attention 2 Requirements: Ampere, Ada, or Hopper GPUs (Turing or lower not supported) @@ -39,20 +82,20 @@ Alternatively, try reinstall or downgrade a version. ::: -### Flash Attention 3 +#### Flash Attention 3 Requirements: Hopper only and CUDA 12.8 (recommended) ```bash git clone https://github.com/Dao-AILab/flash-attention.git cd flash-attention/hopper - python setup.py install ``` -### Flash Attention 4 +#### Flash Attention 4 -Requirements: Hopper or Blackwell GPUs +Requirements: Hopper or Blackwell GPUs. Auto-applied when `attn_uses_flash_lib` +is true and FA4 is importable. FA4 is still a pre-release on PyPI, so `--pre` is required: @@ -65,7 +108,6 @@ Or from source: ```bash git clone https://github.com/Dao-AILab/flash-attention.git cd flash-attention/flash_attn/cute - pip install -e . # FA2's flash_attn package includes a cute/ stub that shadows FA4. @@ -88,93 +130,113 @@ and falls back to FA2/3. ::: -For more details: [flash-attention/flash_attn/cute](https://github.com/Dao-AILab/flash-attention/tree/main/flash_attn/cute) - ### AMD -Requirements: ROCm 6.0 and above. +Requirements: ROCm 6.0 and above. See +[Flash Attention AMD docs](https://github.com/Dao-AILab/flash-attention/tree/main?tab=readme-ov-file#amd-rocm-support). -See [Flash Attention AMD docs](https://github.com/Dao-AILab/flash-attention/tree/main?tab=readme-ov-file#amd-rocm-support). - -## Flex Attention - -A flexible PyTorch API for attention used in combination with `torch.compile`. +### Flex Attention ```yaml -flex_attention: true - -# recommended -torch_compile: true +attn_implementation: flex_attention +torch_compile: true # recommended ``` -::: {.callout-note} - -We recommend using latest stable version of PyTorch for best performance. - -::: - -For more details: [PyTorch docs](https://pytorch.org/blog/flexattention/) +Requires torch ≥ 2.6. See [PyTorch docs](https://pytorch.org/blog/flexattention/). -## SageAttention +### SageAttention -Attention kernels with QK Int8 and PV FP16 accumulator. +Requirements: Ampere, Ada, or Hopper GPUs. ```yaml -sage_attention: true +attn_implementation: sage ``` -Requirements: Ampere, Ada, or Hopper GPUs - ```bash pip install sageattention==2.2.0 --no-build-isolation ``` ::: {.callout-warning} -Only LoRA/QLoRA recommended at the moment. We found loss drop to 0 for full finetuning. See [GitHub Issue](https://github.com/thu-ml/SageAttention/issues/198). +Only LoRA/QLoRA recommended. Full finetuning has been observed to drop loss to 0. See +[GitHub Issue](https://github.com/thu-ml/SageAttention/issues/198). ::: -For more details: [Sage Attention](https://github.com/thu-ml/SageAttention) +For more details: [Sage Attention](https://github.com/thu-ml/SageAttention). -::: {.callout-note} - -We do not support SageAttention 3 at the moment. If you are interested on adding this or improving SageAttention implementation, please make an Issue. - -::: - - -## xFormers +### xFormers ```yaml -xformers_attention: true +attn_implementation: xformers ``` ::: {.callout-tip} -We recommend using with Turing GPUs or below (such as on Colab). +Recommended for Turing GPUs or below (e.g. Colab T4). ::: -For more details: [xFormers](https://github.com/facebookresearch/xformers) - -## Shifted Sparse Attention +### Shifted Sparse Attention ::: {.callout-warning} -We plan to deprecate this! If you use this feature, we recommend switching to methods above. +Planned for deprecation. Prefer one of the backends above. ::: -Requirements: LLaMA model architecture +Requirements: LLaMA model architecture. Loaded as FA2 under the hood and +patched to implement shifted-sparse attention. Does not support sample packing. ```yaml -flash_attention: true -s2_attention: true +attn_implementation: s2 ``` -::: {.callout-tip} +### FP8 + +torchao low-precision attention. Loaded as SDPA and patched post-load. + +Requirements: SM90+ (Hopper/Blackwell), PyTorch ≥ 2.11, torchao ≥ 0.17, +flash-attn with FA3. KV caching must be disabled. + +```yaml +attn_implementation: fp8 +``` + +### Hub kernels + +```yaml +attn_implementation: kernels-community/flash-attn3 +``` + +Passed through to `transformers`; axolotl does not install the kernel itself. +For recognized hub paths the capability flags are set automatically; for +arbitrary paths axolotl uses conservative defaults (`attn_supports_packing=False`, +`attn_uses_flash_lib=False`). + +## Migrating from legacy boolean flags + +The following legacy config fields are **deprecated** and will be removed in a +future release. Each emits a `DeprecationWarning` when set and is stripped from +the validated config. + +| Legacy | Canonical | +|---|---| +| `flash_attention: true` | `attn_implementation: flash_attention_2` | +| `sdp_attention: true` | `attn_implementation: sdpa` | +| `xformers_attention: true` | `attn_implementation: xformers` | +| `flex_attention: true` | `attn_implementation: flex_attention` | +| `sage_attention: true` | `attn_implementation: sage` | +| `s2_attention: true` | `attn_implementation: s2` | +| `eager_attention: true` | `attn_implementation: eager` | + +Combining `attn_implementation` with a legacy flag (e.g. `attn_implementation: +flash_attention_2` **and** `flash_attention: true`) raises — pick one. + +::: {.callout-note} -No sample packing support! +Existing example configs under `examples/` still use the legacy flags. They +continue to work with a deprecation warning; they will be migrated in a +follow-up pass. ::: diff --git a/docs/ebft.qmd b/docs/ebft.qmd index eb7c95ecab..d9afc3307c 100644 --- a/docs/ebft.qmd +++ b/docs/ebft.qmd @@ -129,7 +129,7 @@ gradient_accumulation_steps: 4 max_steps: 20 learning_rate: 5.0e-6 bf16: auto -flash_attention: true +attn_implementation: flash_attention_2 gradient_checkpointing: true output_dir: ./outputs/ebft-quickstart ``` @@ -304,7 +304,7 @@ lora_alpha: 32 lora_target_linear: true bf16: auto -flex_attention: true +attn_implementation: flex_attention gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: true # Required with flex_attention diff --git a/docs/grpo.qmd b/docs/grpo.qmd index 35631f1366..a98dbe11d4 100644 --- a/docs/grpo.qmd +++ b/docs/grpo.qmd @@ -154,7 +154,7 @@ lr_scheduler: cosine warmup_steps: 10 bf16: true -flash_attention: true +attn_implementation: flash_attention_2 gradient_checkpointing: true special_tokens: diff --git a/docs/optimizations.qmd b/docs/optimizations.qmd index b180387ed3..720519ec03 100644 --- a/docs/optimizations.qmd +++ b/docs/optimizations.qmd @@ -22,12 +22,12 @@ Improves GPU utilization by combining multiple short sequences into a single pac Using an optimized attention implementation is critical for training speed. -- **[Flash Attention 2](https://github.com/Dao-AILab/flash-attention)**: `flash_attention: true`. **(Recommended)** The industry standard for fast attention on modern GPUs. Requires Ampere or higher. For AMD, check [AMD Support](https://github.com/Dao-AILab/flash-attention?tab=readme-ov-file#amd-rocm-support). -- **[Flex Attention](https://pytorch.org/blog/flexattention/)**: `flex_attention: true`. -- **[SDP Attention](https://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html)**: `sdp_attention: true`. PyTorch's native implementation. -- **[Xformers](https://github.com/facebookresearch/xformers)**: `xformers_attention: true`. Works with FP16. +- **[Flash Attention 2](https://github.com/Dao-AILab/flash-attention)**: `attn_implementation: flash_attention_2`. **(Recommended)** The industry standard for fast attention on modern GPUs. Requires Ampere or higher. For AMD, check [AMD Support](https://github.com/Dao-AILab/flash-attention?tab=readme-ov-file#amd-rocm-support). +- **[Flex Attention](https://pytorch.org/blog/flexattention/)**: `attn_implementation: flex_attention`. +- **[SDP Attention](https://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html)**: `attn_implementation: sdpa`. PyTorch's native implementation. +- **[Xformers](https://github.com/facebookresearch/xformers)**: `attn_implementation: xformers`. Works with FP16. -*Note: You should only enable one attention backend.* +See [Attention](attention.qmd) for the full list of backends and the canonical values. ### LoRA Optimizations diff --git a/docs/rlhf.qmd b/docs/rlhf.qmd index 75d20414c2..a27bb2966d 100644 --- a/docs/rlhf.qmd +++ b/docs/rlhf.qmd @@ -1147,8 +1147,7 @@ datasets: type: ebft_strided_structured.transform split: train[:1%] -flash_attention: false -flex_attention: true # Strided mode uses flex_attention +attn_implementation: flex_attention # Strided mode uses flex_attention gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: true # Required for flex_attention diff --git a/docs/sequence_parallelism.qmd b/docs/sequence_parallelism.qmd index d1933a145e..9799c8a700 100644 --- a/docs/sequence_parallelism.qmd +++ b/docs/sequence_parallelism.qmd @@ -55,7 +55,7 @@ To use sequence parallelism, you need: ## Limitations -- Flash attention must be enabled for this to work (`flash_attention: true` in config YAML) +- Flash attention must be enabled for this to work (`attn_implementation: flash_attention_2` in config YAML) - May have a small performance overhead due to communication between GPUs ## Example diff --git a/docs/training_stability.qmd b/docs/training_stability.qmd index e2cd79f89e..9849a35d1b 100644 --- a/docs/training_stability.qmd +++ b/docs/training_stability.qmd @@ -245,7 +245,7 @@ For GRPO, also reduce `max_completion_length`. Memory scales quadratically with Reduces attention memory from O(n^2) to O(n): ```yaml -flash_attention: true +attn_implementation: flash_attention_2 ``` ### Step 6: Offload with DeepSpeed diff --git a/examples/LiquidAI/lfm2-350m-fft.yaml b/examples/LiquidAI/lfm2-350m-fft.yaml index 145b56dd12..cd5942206c 100644 --- a/examples/LiquidAI/lfm2-350m-fft.yaml +++ b/examples/LiquidAI/lfm2-350m-fft.yaml @@ -39,7 +39,7 @@ tf32: true gradient_checkpointing: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 2 diff --git a/examples/LiquidAI/lfm2-8b-a1b-lora.yaml b/examples/LiquidAI/lfm2-8b-a1b-lora.yaml index 73cbfcce76..4932ea06e9 100644 --- a/examples/LiquidAI/lfm2-8b-a1b-lora.yaml +++ b/examples/LiquidAI/lfm2-8b-a1b-lora.yaml @@ -48,7 +48,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 2 diff --git a/examples/LiquidAI/lfm2-vl-lora.yaml b/examples/LiquidAI/lfm2-vl-lora.yaml index 313da82744..9a125da5e0 100644 --- a/examples/LiquidAI/lfm2-vl-lora.yaml +++ b/examples/LiquidAI/lfm2-vl-lora.yaml @@ -50,8 +50,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true -eager_attention: +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/alst/llama3-8b-deepspeed-alst.yaml b/examples/alst/llama3-8b-deepspeed-alst.yaml index dea23c5eed..e844c68230 100644 --- a/examples/alst/llama3-8b-deepspeed-alst.yaml +++ b/examples/alst/llama3-8b-deepspeed-alst.yaml @@ -39,7 +39,7 @@ activation_offloading: legacy resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_steps: 100 saves_per_epoch: 1 diff --git a/examples/alst/llama3-8b-fsdp2-alst.yaml b/examples/alst/llama3-8b-fsdp2-alst.yaml index c8a978264f..a7da926375 100644 --- a/examples/alst/llama3-8b-fsdp2-alst.yaml +++ b/examples/alst/llama3-8b-fsdp2-alst.yaml @@ -39,7 +39,7 @@ activation_offloading: legacy resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_steps: 100 saves_per_epoch: 1 diff --git a/examples/apertus/apertus-8b-qlora.yaml b/examples/apertus/apertus-8b-qlora.yaml index 521b282da1..f43901363c 100644 --- a/examples/apertus/apertus-8b-qlora.yaml +++ b/examples/apertus/apertus-8b-qlora.yaml @@ -55,7 +55,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/arcee/afm-4.5b-qlora.yaml b/examples/arcee/afm-4.5b-qlora.yaml index 2cb42cacda..8e70847adf 100644 --- a/examples/arcee/afm-4.5b-qlora.yaml +++ b/examples/arcee/afm-4.5b-qlora.yaml @@ -55,7 +55,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/archived/cerebras/btlm-ft.yml b/examples/archived/cerebras/btlm-ft.yml index c3495d2874..5a5f8dc12c 100644 --- a/examples/archived/cerebras/btlm-ft.yml +++ b/examples/archived/cerebras/btlm-ft.yml @@ -59,8 +59,7 @@ gradient_checkpointing: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true -sdp_attention: +attn_implementation: flash_attention_2 flash_optimum: gptq_groupsize: diff --git a/examples/archived/cerebras/qlora.yml b/examples/archived/cerebras/qlora.yml index 4598a8338e..22f52e6821 100644 --- a/examples/archived/cerebras/qlora.yml +++ b/examples/archived/cerebras/qlora.yml @@ -39,8 +39,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -xformers_attention: true -flash_attention: +attn_implementation: xformers gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/code-llama/13b/lora.yml b/examples/archived/code-llama/13b/lora.yml index ace94b6196..43f6233579 100644 --- a/examples/archived/code-llama/13b/lora.yml +++ b/examples/archived/code-llama/13b/lora.yml @@ -45,7 +45,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/code-llama/13b/qlora.yml b/examples/archived/code-llama/13b/qlora.yml index f4ed17af53..086f5e3d85 100644 --- a/examples/archived/code-llama/13b/qlora.yml +++ b/examples/archived/code-llama/13b/qlora.yml @@ -46,7 +46,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/code-llama/34b/lora.yml b/examples/archived/code-llama/34b/lora.yml index 0a1d714678..19aa898be7 100644 --- a/examples/archived/code-llama/34b/lora.yml +++ b/examples/archived/code-llama/34b/lora.yml @@ -45,7 +45,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/code-llama/34b/qlora.yml b/examples/archived/code-llama/34b/qlora.yml index ec17bf2008..2ec78f0d8a 100644 --- a/examples/archived/code-llama/34b/qlora.yml +++ b/examples/archived/code-llama/34b/qlora.yml @@ -46,7 +46,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/code-llama/7b/lora.yml b/examples/archived/code-llama/7b/lora.yml index 174c17d2cd..30bc633553 100644 --- a/examples/archived/code-llama/7b/lora.yml +++ b/examples/archived/code-llama/7b/lora.yml @@ -45,7 +45,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/code-llama/7b/qlora.yml b/examples/archived/code-llama/7b/qlora.yml index 08e67d8c2d..0c3b385193 100644 --- a/examples/archived/code-llama/7b/qlora.yml +++ b/examples/archived/code-llama/7b/qlora.yml @@ -46,7 +46,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/dbrx/16bit-lora.yaml b/examples/archived/dbrx/16bit-lora.yaml index 05946dfe96..eca58f94cb 100644 --- a/examples/archived/dbrx/16bit-lora.yaml +++ b/examples/archived/dbrx/16bit-lora.yaml @@ -52,7 +52,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/archived/dbrx/8bit-lora.yaml b/examples/archived/dbrx/8bit-lora.yaml index f159bf7fab..59f5241b45 100644 --- a/examples/archived/dbrx/8bit-lora.yaml +++ b/examples/archived/dbrx/8bit-lora.yaml @@ -55,7 +55,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/archived/dbrx/fft-ds-zero3.yaml b/examples/archived/dbrx/fft-ds-zero3.yaml index 13cd0d9977..2cb3e6da1a 100644 --- a/examples/archived/dbrx/fft-ds-zero3.yaml +++ b/examples/archived/dbrx/fft-ds-zero3.yaml @@ -39,7 +39,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml b/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml index 3223ec19a7..b125e9e3f3 100644 --- a/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml +++ b/examples/archived/deepcoder/deepcoder-14B-preview-lora.yml @@ -45,7 +45,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/archived/falcon/config-7b-lora.yml b/examples/archived/falcon/config-7b-lora.yml index f4fedbedec..71dd572b3c 100644 --- a/examples/archived/falcon/config-7b-lora.yml +++ b/examples/archived/falcon/config-7b-lora.yml @@ -43,8 +43,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -xformers_attention: true -flash_attention: +attn_implementation: xformers gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/falcon/config-7b-qlora.yml b/examples/archived/falcon/config-7b-qlora.yml index a44cc40a6b..edd6550a71 100644 --- a/examples/archived/falcon/config-7b-qlora.yml +++ b/examples/archived/falcon/config-7b-qlora.yml @@ -73,8 +73,7 @@ early_stopping_patience: 3 resume_from_checkpoint: auto_resume_from_checkpoints: true logging_steps: 1 -xformers_attention: true -flash_attention: +attn_implementation: xformers gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/falcon/config-7b.yml b/examples/archived/falcon/config-7b.yml index 5481fb2362..6da39d7ab1 100644 --- a/examples/archived/falcon/config-7b.yml +++ b/examples/archived/falcon/config-7b.yml @@ -40,8 +40,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -xformers_attention: true -flash_attention: +attn_implementation: xformers gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/gemma/qlora.yml b/examples/archived/gemma/qlora.yml index 80829b3c9d..5b5ec4a9f9 100644 --- a/examples/archived/gemma/qlora.yml +++ b/examples/archived/gemma/qlora.yml @@ -47,7 +47,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/gptj/qlora.yml b/examples/archived/gptj/qlora.yml index 6348566c2f..7e10adeaa0 100644 --- a/examples/archived/gptj/qlora.yml +++ b/examples/archived/gptj/qlora.yml @@ -36,8 +36,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -xformers_attention: true -flash_attention: +attn_implementation: xformers gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/jeopardy-bot/config.yml b/examples/archived/jeopardy-bot/config.yml index ab1d197847..90ca3b4bca 100644 --- a/examples/archived/jeopardy-bot/config.yml +++ b/examples/archived/jeopardy-bot/config.yml @@ -37,8 +37,7 @@ bf16: auto tf32: true resume_from_checkpoint: logging_steps: 5 -xformers_attention: true -flash_attention: +attn_implementation: xformers gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/mpt-7b/config.yml b/examples/archived/mpt-7b/config.yml index 1fff51b6ec..588981bf71 100644 --- a/examples/archived/mpt-7b/config.yml +++ b/examples/archived/mpt-7b/config.yml @@ -39,7 +39,6 @@ bf16: auto tf32: true resume_from_checkpoint: logging_steps: 5 -flash_attention: gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/openllama-3b/config.yml b/examples/archived/openllama-3b/config.yml index 63056ed6d5..14104ff4ba 100644 --- a/examples/archived/openllama-3b/config.yml +++ b/examples/archived/openllama-3b/config.yml @@ -39,7 +39,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/openllama-3b/lora.yml b/examples/archived/openllama-3b/lora.yml index b70821ce27..30d3888f11 100644 --- a/examples/archived/openllama-3b/lora.yml +++ b/examples/archived/openllama-3b/lora.yml @@ -47,7 +47,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/openllama-3b/qlora.yml b/examples/archived/openllama-3b/qlora.yml index a34f2964b8..fc9d1d7035 100644 --- a/examples/archived/openllama-3b/qlora.yml +++ b/examples/archived/openllama-3b/qlora.yml @@ -40,7 +40,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/qwen/lora.yml b/examples/archived/qwen/lora.yml index 29de256118..362a848a8d 100644 --- a/examples/archived/qwen/lora.yml +++ b/examples/archived/qwen/lora.yml @@ -47,7 +47,6 @@ tf32: false gradient_checkpointing: false resume_from_checkpoint: logging_steps: 1 -flash_attention: warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/qwen/qlora.yml b/examples/archived/qwen/qlora.yml index d466694449..bce3012e72 100644 --- a/examples/archived/qwen/qlora.yml +++ b/examples/archived/qwen/qlora.yml @@ -47,7 +47,6 @@ tf32: false gradient_checkpointing: false resume_from_checkpoint: logging_steps: 1 -flash_attention: warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/qwen/qwen2-moe-lora.yaml b/examples/archived/qwen/qwen2-moe-lora.yaml index 1d5e1b5247..97c0d51a6b 100644 --- a/examples/archived/qwen/qwen2-moe-lora.yaml +++ b/examples/archived/qwen/qwen2-moe-lora.yaml @@ -43,7 +43,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/qwen/qwen2-moe-qlora.yaml b/examples/archived/qwen/qwen2-moe-qlora.yaml index 08731441b9..a16089eed4 100644 --- a/examples/archived/qwen/qwen2-moe-qlora.yaml +++ b/examples/archived/qwen/qwen2-moe-qlora.yaml @@ -46,7 +46,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/redpajama/config-3b.yml b/examples/archived/redpajama/config-3b.yml index c5b229c3d8..676f314769 100644 --- a/examples/archived/redpajama/config-3b.yml +++ b/examples/archived/redpajama/config-3b.yml @@ -40,7 +40,6 @@ bf16: auto tf32: true resume_from_checkpoint: logging_steps: 5 -flash_attention: gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/replit-3b/config-lora.yml b/examples/archived/replit-3b/config-lora.yml index d8561762c6..b0a0c9089d 100644 --- a/examples/archived/replit-3b/config-lora.yml +++ b/examples/archived/replit-3b/config-lora.yml @@ -38,7 +38,6 @@ tf32: true gradient_checkpointing: resume_from_checkpoint: logging_steps: 1 -flash_attention: gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/stablelm-2/1.6b/fft.yml b/examples/archived/stablelm-2/1.6b/fft.yml index 585888f43b..05f59544c6 100644 --- a/examples/archived/stablelm-2/1.6b/fft.yml +++ b/examples/archived/stablelm-2/1.6b/fft.yml @@ -44,7 +44,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 flash_attn_cross_entropy: false flash_attn_rms_norm: true flash_attn_fuse_mlp: true diff --git a/examples/archived/stablelm-2/1.6b/lora.yml b/examples/archived/stablelm-2/1.6b/lora.yml index 6d358bdd88..1edb56e0c0 100644 --- a/examples/archived/stablelm-2/1.6b/lora.yml +++ b/examples/archived/stablelm-2/1.6b/lora.yml @@ -47,7 +47,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 flash_attn_cross_entropy: false flash_attn_rms_norm: true diff --git a/examples/archived/starcoder2/qlora.yml b/examples/archived/starcoder2/qlora.yml index fecf98d23f..0fd0f453cb 100644 --- a/examples/archived/starcoder2/qlora.yml +++ b/examples/archived/starcoder2/qlora.yml @@ -46,7 +46,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/tiny-llama/lora-mps.yml b/examples/archived/tiny-llama/lora-mps.yml index 125090a78e..bf3292c356 100644 --- a/examples/archived/tiny-llama/lora-mps.yml +++ b/examples/archived/tiny-llama/lora-mps.yml @@ -47,7 +47,6 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: false warmup_ratio: 0.1 evals_per_epoch: 0 diff --git a/examples/archived/tiny-llama/lora.yml b/examples/archived/tiny-llama/lora.yml index 817481e186..a12d637466 100644 --- a/examples/archived/tiny-llama/lora.yml +++ b/examples/archived/tiny-llama/lora.yml @@ -45,7 +45,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/tiny-llama/pretrain.yml b/examples/archived/tiny-llama/pretrain.yml index f15c6ce19e..4d16861381 100644 --- a/examples/archived/tiny-llama/pretrain.yml +++ b/examples/archived/tiny-llama/pretrain.yml @@ -36,7 +36,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/archived/tiny-llama/qlora.yml b/examples/archived/tiny-llama/qlora.yml index d3ff59cb81..b1adcb2e61 100644 --- a/examples/archived/tiny-llama/qlora.yml +++ b/examples/archived/tiny-llama/qlora.yml @@ -47,7 +47,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/archived/xgen-7b/xgen-7b-8k-qlora.yml b/examples/archived/xgen-7b/xgen-7b-8k-qlora.yml index fc09a1e7bd..d548032b9a 100644 --- a/examples/archived/xgen-7b/xgen-7b-8k-qlora.yml +++ b/examples/archived/xgen-7b/xgen-7b-8k-qlora.yml @@ -71,8 +71,7 @@ early_stopping_patience: 3 resume_from_checkpoint: auto_resume_from_checkpoints: true logging_steps: 1 -xformers_attention: true -flash_attention: +attn_implementation: xformers gptq_groupsize: gptq_model_v1: warmup_ratio: 0.1 diff --git a/examples/archived/yi-34B-chat/qlora.yml b/examples/archived/yi-34B-chat/qlora.yml index ba8d12fc89..5d3d54dc6e 100644 --- a/examples/archived/yi-34B-chat/qlora.yml +++ b/examples/archived/yi-34B-chat/qlora.yml @@ -10,7 +10,7 @@ load_in_4bit: true sequence_len: 1024 bf16: auto tf32: false -flash_attention: true +attn_implementation: flash_attention_2 special_tokens: bos_token: "<|startoftext|>" eos_token: "<|endoftext|>" diff --git a/examples/cohere/command-r-7b-qlora.yml b/examples/cohere/command-r-7b-qlora.yml index b4741636b7..c4d03b0ec6 100644 --- a/examples/cohere/command-r-7b-qlora.yml +++ b/examples/cohere/command-r-7b-qlora.yml @@ -48,7 +48,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml b/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml index 97d1bb6b32..c36b0e74a1 100644 --- a/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml +++ b/examples/deepcogito/cogito-v1-preview-llama-3B-lora.yml @@ -45,7 +45,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml b/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml index b80cc5bc0f..2b2aafd754 100644 --- a/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml +++ b/examples/deepcogito/cogito-v1-preview-qwen-14B-lora.yml @@ -45,7 +45,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/deepseek-v2/fft-fsdp-16b.yaml b/examples/deepseek-v2/fft-fsdp-16b.yaml index 6e936da163..2eac9aea3f 100644 --- a/examples/deepseek-v2/fft-fsdp-16b.yaml +++ b/examples/deepseek-v2/fft-fsdp-16b.yaml @@ -35,7 +35,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 2 diff --git a/examples/deepseek-v2/qlora-fsdp-2_5.yaml b/examples/deepseek-v2/qlora-fsdp-2_5.yaml index aab5034a09..0e23a0266f 100644 --- a/examples/deepseek-v2/qlora-fsdp-2_5.yaml +++ b/examples/deepseek-v2/qlora-fsdp-2_5.yaml @@ -59,7 +59,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 2 diff --git a/examples/devstral/devstral-small-qlora.yml b/examples/devstral/devstral-small-qlora.yml index 3eafb92194..6ee0e014d2 100644 --- a/examples/devstral/devstral-small-qlora.yml +++ b/examples/devstral/devstral-small-qlora.yml @@ -50,7 +50,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 # scaling_softmax: true # needs flex_attention loss_watchdog_threshold: 5.0 diff --git a/examples/distributed-parallel/llama-3_1-8b-hsdp-tp.yaml b/examples/distributed-parallel/llama-3_1-8b-hsdp-tp.yaml index f10dc9bd29..a99a6bef8e 100644 --- a/examples/distributed-parallel/llama-3_1-8b-hsdp-tp.yaml +++ b/examples/distributed-parallel/llama-3_1-8b-hsdp-tp.yaml @@ -29,7 +29,7 @@ output_dir: ./outputs/ndp-out/ sequence_len: 2048 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 gradient_accumulation_steps: 1 micro_batch_size: 1 diff --git a/examples/distributed-parallel/qwen3-8b-fsdp-tp-cp.yaml b/examples/distributed-parallel/qwen3-8b-fsdp-tp-cp.yaml index 584a33f440..a12b524edd 100644 --- a/examples/distributed-parallel/qwen3-8b-fsdp-tp-cp.yaml +++ b/examples/distributed-parallel/qwen3-8b-fsdp-tp-cp.yaml @@ -26,7 +26,7 @@ output_dir: ./outputs/ndp-out/ sequence_len: 8192 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 gradient_accumulation_steps: 1 micro_batch_size: 1 # must be 1 when using context parallel diff --git a/examples/eaft/eaft-example.yml b/examples/eaft/eaft-example.yml index fed4179d21..b4b13a14c5 100644 --- a/examples/eaft/eaft-example.yml +++ b/examples/eaft/eaft-example.yml @@ -65,8 +65,7 @@ early_stopping_patience: resume_from_checkpoint: local_rank: logging_steps: 1 -xformers_attention: -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 weight_decay: 0.0 diff --git a/examples/ebft/llama-1b-ebft-opencode-novllm.yaml b/examples/ebft/llama-1b-ebft-opencode-novllm.yaml index 0891033f0c..7d7edad339 100644 --- a/examples/ebft/llama-1b-ebft-opencode-novllm.yaml +++ b/examples/ebft/llama-1b-ebft-opencode-novllm.yaml @@ -46,7 +46,7 @@ lora_dropout: 0.05 lora_target_linear: true bf16: auto -flash_attention: true +attn_implementation: flash_attention_2 gradient_checkpointing: true special_tokens: diff --git a/examples/ebft/llama-1b-ebft-opencode.yaml b/examples/ebft/llama-1b-ebft-opencode.yaml index d0d1069d87..c77c366779 100644 --- a/examples/ebft/llama-1b-ebft-opencode.yaml +++ b/examples/ebft/llama-1b-ebft-opencode.yaml @@ -66,7 +66,7 @@ lora_target_linear: true # --- Hardware --- bf16: auto -flash_attention: true +attn_implementation: flash_attention_2 gradient_checkpointing: true special_tokens: diff --git a/examples/ebft/llama-1b-ebft-strided-structured.yaml b/examples/ebft/llama-1b-ebft-strided-structured.yaml index 8ba63b64b5..02e89dea01 100644 --- a/examples/ebft/llama-1b-ebft-strided-structured.yaml +++ b/examples/ebft/llama-1b-ebft-strided-structured.yaml @@ -47,8 +47,7 @@ lora_dropout: 0.05 lora_target_linear: true bf16: auto -flash_attention: false # strided EBFT overrides to flex_attention (or eager fallback) at runtime -flex_attention: true # fused flex_attention kernel compiles itself; don't set torch_compile: true +attn_implementation: flex_attention # (full-model compile conflicts with gradient checkpointing + flex_attention) gradient_checkpointing: true gradient_checkpointing_kwargs: diff --git a/examples/ebft/llama-1b-ebft-strided.yaml b/examples/ebft/llama-1b-ebft-strided.yaml index c9519f160d..e3cfe8040e 100644 --- a/examples/ebft/llama-1b-ebft-strided.yaml +++ b/examples/ebft/llama-1b-ebft-strided.yaml @@ -46,7 +46,6 @@ lora_dropout: 0.05 lora_target_linear: true bf16: auto -flash_attention: false # strided EBFT overrides to flex_attention (or eager fallback) at runtime gradient_checkpointing: true special_tokens: diff --git a/examples/ebft/llama-3b-ebft-strided-fft.yaml b/examples/ebft/llama-3b-ebft-strided-fft.yaml index 5695efa401..e39d3bcfa2 100644 --- a/examples/ebft/llama-3b-ebft-strided-fft.yaml +++ b/examples/ebft/llama-3b-ebft-strided-fft.yaml @@ -48,7 +48,6 @@ lora_target_linear: true bf16: auto torch_dtype: bfloat16 -flash_attention: false gradient_checkpointing: true torch_compile: true gradient_checkpointing_kwargs: diff --git a/examples/ebft/llama-8b-ebft-strided-fft.yaml b/examples/ebft/llama-8b-ebft-strided-fft.yaml index 8cf9628496..caed980855 100644 --- a/examples/ebft/llama-8b-ebft-strided-fft.yaml +++ b/examples/ebft/llama-8b-ebft-strided-fft.yaml @@ -41,7 +41,6 @@ warmup_steps: 10 weight_decay: 0.01 bf16: auto -flash_attention: false # strided EBFT uses flex_attention at runtime gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false diff --git a/examples/ebft/qwen35-4b-ebft-structured-async.yaml b/examples/ebft/qwen35-4b-ebft-structured-async.yaml index 759a317307..daa77d6f67 100644 --- a/examples/ebft/qwen35-4b-ebft-structured-async.yaml +++ b/examples/ebft/qwen35-4b-ebft-structured-async.yaml @@ -72,7 +72,7 @@ lora_dropout: 0.0 lora_target_modules: ".*\\.layers\\.(3|7|11|15|19|23|27|31)\\.self_attn\\.(q|k|v|o)_proj|.*\\.mlp\\.(gate|up|down)_proj" bf16: auto -flash_attention: true +attn_implementation: flash_attention_2 gradient_checkpointing: true special_tokens: diff --git a/examples/ebft/qwen35-4b-ebft-structured.yaml b/examples/ebft/qwen35-4b-ebft-structured.yaml index 9108e87e93..d1b2a72f28 100644 --- a/examples/ebft/qwen35-4b-ebft-structured.yaml +++ b/examples/ebft/qwen35-4b-ebft-structured.yaml @@ -63,7 +63,7 @@ lora_dropout: 0.0 lora_target_modules: ".*\\.layers\\.(3|7|11|15|19|23|27|31)\\.self_attn\\.(q|k|v|o)_proj|.*\\.mlp\\.(gate|up|down)_proj" bf16: auto -flash_attention: true +attn_implementation: flash_attention_2 gradient_checkpointing: true special_tokens: diff --git a/examples/ebft/qwen35-9b-ebft-structured.yaml b/examples/ebft/qwen35-9b-ebft-structured.yaml index e79fb5fbfc..ad3b8538e7 100644 --- a/examples/ebft/qwen35-9b-ebft-structured.yaml +++ b/examples/ebft/qwen35-9b-ebft-structured.yaml @@ -68,7 +68,7 @@ lora_dropout: 0.0 lora_target_modules: ".*\\.layers\\.(3|7|11|15|19|23|27|31)\\.self_attn\\.(q|k|v|o)_proj|.*\\.mlp\\.(gate|up|down)_proj" bf16: auto -flash_attention: true +attn_implementation: flash_attention_2 gradient_checkpointing: true special_tokens: diff --git a/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml b/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml index 2473179f0f..f59f0df5ca 100644 --- a/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-1b-deep-qlora.yaml @@ -62,7 +62,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/falcon-h1/falcon-h1-1b-qlora.yaml b/examples/falcon-h1/falcon-h1-1b-qlora.yaml index bfb7836ef0..8c3eb080d2 100644 --- a/examples/falcon-h1/falcon-h1-1b-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-1b-qlora.yaml @@ -61,7 +61,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/falcon-h1/falcon-h1-34b-qlora.yaml b/examples/falcon-h1/falcon-h1-34b-qlora.yaml index 80a9d45b5e..28e7de9566 100644 --- a/examples/falcon-h1/falcon-h1-34b-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-34b-qlora.yaml @@ -62,7 +62,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/falcon-h1/falcon-h1-3b-qlora.yaml b/examples/falcon-h1/falcon-h1-3b-qlora.yaml index 02be8ac5d5..71b38e2f7c 100644 --- a/examples/falcon-h1/falcon-h1-3b-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-3b-qlora.yaml @@ -62,7 +62,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/falcon-h1/falcon-h1-500m-qlora.yaml b/examples/falcon-h1/falcon-h1-500m-qlora.yaml index b112d5d852..91602ae714 100644 --- a/examples/falcon-h1/falcon-h1-500m-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-500m-qlora.yaml @@ -62,7 +62,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/falcon-h1/falcon-h1-7b-qlora.yaml b/examples/falcon-h1/falcon-h1-7b-qlora.yaml index c5505873df..cc7e8f6cdc 100644 --- a/examples/falcon-h1/falcon-h1-7b-qlora.yaml +++ b/examples/falcon-h1/falcon-h1-7b-qlora.yaml @@ -62,7 +62,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/gemma2/qlora.yml b/examples/gemma2/qlora.yml index 8a295a1f80..b2fca74da2 100644 --- a/examples/gemma2/qlora.yml +++ b/examples/gemma2/qlora.yml @@ -53,7 +53,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/gemma2/reward-model.yaml b/examples/gemma2/reward-model.yaml index 67b1228b2a..f48bff6266 100644 --- a/examples/gemma2/reward-model.yaml +++ b/examples/gemma2/reward-model.yaml @@ -43,7 +43,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/gemma3/gemma-3-1b-qlora.yml b/examples/gemma3/gemma-3-1b-qlora.yml index 4bcbf09f4a..95b99a0da9 100644 --- a/examples/gemma3/gemma-3-1b-qlora.yml +++ b/examples/gemma3/gemma-3-1b-qlora.yml @@ -62,7 +62,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/gemma3/gemma-3-270m-qlora.yml b/examples/gemma3/gemma-3-270m-qlora.yml index 1f247ab059..800a88a1b9 100644 --- a/examples/gemma3/gemma-3-270m-qlora.yml +++ b/examples/gemma3/gemma-3-270m-qlora.yml @@ -62,7 +62,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/gemma3/gemma-3-4b-qlora.yml b/examples/gemma3/gemma-3-4b-qlora.yml index 5d939da191..e7c43ddef8 100644 --- a/examples/gemma3/gemma-3-4b-qlora.yml +++ b/examples/gemma3/gemma-3-4b-qlora.yml @@ -58,8 +58,7 @@ gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false logging_steps: 1 -flash_attention: true -eager_attention: +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/gemma3/gemma-3-4b-vision-qlora.yml b/examples/gemma3/gemma-3-4b-vision-qlora.yml index a12e84beee..790d9543a3 100644 --- a/examples/gemma3/gemma-3-4b-vision-qlora.yml +++ b/examples/gemma3/gemma-3-4b-vision-qlora.yml @@ -55,8 +55,7 @@ gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false logging_steps: 1 -flash_attention: true -eager_attention: +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/gemma4/26b-a4b-moe-qlora.yaml b/examples/gemma4/26b-a4b-moe-qlora.yaml index e7bdb6f461..cdc70ef4a8 100644 --- a/examples/gemma4/26b-a4b-moe-qlora.yaml +++ b/examples/gemma4/26b-a4b-moe-qlora.yaml @@ -84,7 +84,7 @@ activation_offloading: true logging_steps: 1 # FA2 not supported -sdp_attention: true +attn_implementation: sdpa warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/gemma4/31b-qlora-flex.yaml b/examples/gemma4/31b-qlora-flex.yaml index 8456c9c135..87221c5154 100644 --- a/examples/gemma4/31b-qlora-flex.yaml +++ b/examples/gemma4/31b-qlora-flex.yaml @@ -62,7 +62,7 @@ activation_offloading: true logging_steps: 1 # FA not supported -flex_attention: true +attn_implementation: flex_attention warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/gemma4/31b-qlora.yaml b/examples/gemma4/31b-qlora.yaml index 42086a43c6..4a633436e0 100644 --- a/examples/gemma4/31b-qlora.yaml +++ b/examples/gemma4/31b-qlora.yaml @@ -60,7 +60,7 @@ activation_offloading: true logging_steps: 1 # FA not supported -sdp_attention: true +attn_implementation: sdpa warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/gemma4/e2b-vision-lora.yaml b/examples/gemma4/e2b-vision-lora.yaml index c779aaea55..ae90bc1cb7 100644 --- a/examples/gemma4/e2b-vision-lora.yaml +++ b/examples/gemma4/e2b-vision-lora.yaml @@ -50,7 +50,7 @@ gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false logging_steps: 1 -sdp_attention: true +attn_implementation: sdpa warmup_ratio: 0.1 weight_decay: 0.0 diff --git a/examples/glm4/qlora-32b.yaml b/examples/glm4/qlora-32b.yaml index 832abde053..151820924d 100644 --- a/examples/glm4/qlora-32b.yaml +++ b/examples/glm4/qlora-32b.yaml @@ -50,7 +50,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/glm45/glm-45-air-qlora.yaml b/examples/glm45/glm-45-air-qlora.yaml index accb8898f5..5723d3c45c 100644 --- a/examples/glm45/glm-45-air-qlora.yaml +++ b/examples/glm45/glm-45-air-qlora.yaml @@ -55,7 +55,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/glm46v/glm-4-6v-flash-ddp.yaml b/examples/glm46v/glm-4-6v-flash-ddp.yaml index c67ac5e283..274f041a32 100644 --- a/examples/glm46v/glm-4-6v-flash-ddp.yaml +++ b/examples/glm46v/glm-4-6v-flash-ddp.yaml @@ -45,7 +45,7 @@ gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false logging_steps: 1 -sdp_attention: true +attn_implementation: sdpa warmup_ratio: 0.1 evals_per_epoch: 0 diff --git a/examples/glm46v/glm-4-6v-flash-qlora.yaml b/examples/glm46v/glm-4-6v-flash-qlora.yaml index 287944ae89..9fe8d6e436 100644 --- a/examples/glm46v/glm-4-6v-flash-qlora.yaml +++ b/examples/glm46v/glm-4-6v-flash-qlora.yaml @@ -42,7 +42,7 @@ tf32: false gradient_checkpointing: true logging_steps: 1 -sdp_attention: true +attn_implementation: sdpa warmup_ratio: 0.1 evals_per_epoch: 0 diff --git a/examples/glm47-flash/lora.yaml b/examples/glm47-flash/lora.yaml index 2586babb72..5f3de36e9a 100644 --- a/examples/glm47-flash/lora.yaml +++ b/examples/glm47-flash/lora.yaml @@ -58,7 +58,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/glm47-flash/lora_fsdp.yaml b/examples/glm47-flash/lora_fsdp.yaml index bee20bf02f..cf1d2de55a 100644 --- a/examples/glm47-flash/lora_fsdp.yaml +++ b/examples/glm47-flash/lora_fsdp.yaml @@ -57,7 +57,7 @@ tf32: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/glm47-flash/qlora.yaml b/examples/glm47-flash/qlora.yaml index 834c46af80..a05bf54d2f 100644 --- a/examples/glm47-flash/qlora.yaml +++ b/examples/glm47-flash/qlora.yaml @@ -58,7 +58,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/glm47-flash/qlora_fsdp.yaml b/examples/glm47-flash/qlora_fsdp.yaml index 0bb87813ff..9ad5a6212c 100644 --- a/examples/glm47-flash/qlora_fsdp.yaml +++ b/examples/glm47-flash/qlora_fsdp.yaml @@ -57,7 +57,7 @@ tf32: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml b/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml index b7082f9867..71692958f0 100644 --- a/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml +++ b/examples/gpt-oss/gpt-oss-120b-fft-fsdp2-offload.yaml @@ -47,7 +47,6 @@ learning_rate: 2e-5 bf16: true tf32: true -flash_attention: true attn_implementation: kernels-community/vllm-flash-attn3 # this is not needed if using flash_attn >= 2.8.3 gradient_checkpointing: true diff --git a/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml b/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml index b718ff2eba..5912f876bb 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-deepspeed-zero3.yaml @@ -43,7 +43,6 @@ learning_rate: 2e-5 bf16: true tf32: true -flash_attention: true attn_implementation: kernels-community/vllm-flash-attn3 # this is not needed if using flash_attn >= 2.8.3 gradient_checkpointing: true diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml index af1c93bc0c..b1a0fef4ab 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2-offload.yaml @@ -44,7 +44,6 @@ learning_rate: 2e-5 bf16: true tf32: true -flash_attention: true attn_implementation: kernels-community/vllm-flash-attn3 # this is not needed if using flash_attn >= 2.8.3 gradient_checkpointing: true diff --git a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml index 894ba99b83..f97174cd90 100644 --- a/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml +++ b/examples/gpt-oss/gpt-oss-20b-fft-fsdp2.yaml @@ -43,7 +43,6 @@ learning_rate: 2e-5 bf16: true tf32: true -flash_attention: true attn_implementation: kernels-community/vllm-flash-attn3 # this is not needed if using flash_attn >= 2.8.3 gradient_checkpointing: true diff --git a/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml b/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml index 7c4f978468..122fb0b6c3 100644 --- a/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml +++ b/examples/gpt-oss/gpt-oss-20b-sft-lora-singlegpu.yaml @@ -56,7 +56,6 @@ learning_rate: 2e-4 bf16: true tf32: true -flash_attention: true attn_implementation: kernels-community/vllm-flash-attn3 # this is not needed if using flash_attn >= 2.8.3 gradient_checkpointing: true diff --git a/examples/gpt-oss/gpt-oss-safeguard-20b-sft-lora-singlegpu.yaml b/examples/gpt-oss/gpt-oss-safeguard-20b-sft-lora-singlegpu.yaml index cbb9efc8e7..7ba5f29b59 100644 --- a/examples/gpt-oss/gpt-oss-safeguard-20b-sft-lora-singlegpu.yaml +++ b/examples/gpt-oss/gpt-oss-safeguard-20b-sft-lora-singlegpu.yaml @@ -56,7 +56,6 @@ learning_rate: 2e-4 bf16: true tf32: true -flash_attention: true attn_implementation: kernels-community/vllm-flash-attn3 # this is not needed if using flash_attn >= 2.8.3 gradient_checkpointing: true diff --git a/examples/granite4/granite-4.0-tiny-fft.yaml b/examples/granite4/granite-4.0-tiny-fft.yaml index 7ff8207aea..fd7d2a3127 100644 --- a/examples/granite4/granite-4.0-tiny-fft.yaml +++ b/examples/granite4/granite-4.0-tiny-fft.yaml @@ -36,7 +36,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/hunyuan/hunyuan-v1-dense-qlora.yaml b/examples/hunyuan/hunyuan-v1-dense-qlora.yaml index a94345a612..1ae6b000d5 100644 --- a/examples/hunyuan/hunyuan-v1-dense-qlora.yaml +++ b/examples/hunyuan/hunyuan-v1-dense-qlora.yaml @@ -55,7 +55,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/internvl3_5/internvl3_5-8b-qlora.yml b/examples/internvl3_5/internvl3_5-8b-qlora.yml index 9a72d078a7..2d924c6f18 100644 --- a/examples/internvl3_5/internvl3_5-8b-qlora.yml +++ b/examples/internvl3_5/internvl3_5-8b-qlora.yml @@ -50,8 +50,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true -eager_attention: +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/jamba/qlora.yaml b/examples/jamba/qlora.yaml index 538ed3a106..f625fb6f5a 100644 --- a/examples/jamba/qlora.yaml +++ b/examples/jamba/qlora.yaml @@ -47,7 +47,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/jamba/qlora_deepspeed.yaml b/examples/jamba/qlora_deepspeed.yaml index b288635e7c..8ec74f9052 100644 --- a/examples/jamba/qlora_deepspeed.yaml +++ b/examples/jamba/qlora_deepspeed.yaml @@ -46,7 +46,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/jamba/qlora_fsdp_large.yaml b/examples/jamba/qlora_fsdp_large.yaml index 4db889fbc4..76cc0ef186 100644 --- a/examples/jamba/qlora_fsdp_large.yaml +++ b/examples/jamba/qlora_fsdp_large.yaml @@ -44,7 +44,7 @@ gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/kimi-linear/kimi-48b-lora.yaml b/examples/kimi-linear/kimi-48b-lora.yaml index 8e855dd720..befa298916 100644 --- a/examples/kimi-linear/kimi-48b-lora.yaml +++ b/examples/kimi-linear/kimi-48b-lora.yaml @@ -65,7 +65,7 @@ early_stopping_patience: resume_from_checkpoint: local_rank: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/llama-2/fft_optimized.yml b/examples/llama-2/fft_optimized.yml index ea119348e5..7af25dd179 100644 --- a/examples/llama-2/fft_optimized.yml +++ b/examples/llama-2/fft_optimized.yml @@ -42,7 +42,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 flash_attn_cross_entropy: false flash_attn_rms_norm: true flash_attn_fuse_mlp: true diff --git a/examples/llama-2/gptq-lora.yml b/examples/llama-2/gptq-lora.yml index de1caaa059..c4073b80ae 100644 --- a/examples/llama-2/gptq-lora.yml +++ b/examples/llama-2/gptq-lora.yml @@ -53,8 +53,6 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: -sdp_attention: flash_optimum: warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-2/lisa.yml b/examples/llama-2/lisa.yml index d21c01a495..40ba6d0d0e 100644 --- a/examples/llama-2/lisa.yml +++ b/examples/llama-2/lisa.yml @@ -46,7 +46,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 flash_attn_cross_entropy: false flash_attn_rms_norm: true flash_attn_fuse_mlp: true diff --git a/examples/llama-2/loftq.yml b/examples/llama-2/loftq.yml index 619e5bcce2..f1562ec295 100644 --- a/examples/llama-2/loftq.yml +++ b/examples/llama-2/loftq.yml @@ -45,7 +45,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-2/lora.yml b/examples/llama-2/lora.yml index 0a677f11ac..8c2242b714 100644 --- a/examples/llama-2/lora.yml +++ b/examples/llama-2/lora.yml @@ -45,7 +45,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-2/qlora-fsdp.yml b/examples/llama-2/qlora-fsdp.yml index 1e7064de82..102eb7af7d 100644 --- a/examples/llama-2/qlora-fsdp.yml +++ b/examples/llama-2/qlora-fsdp.yml @@ -48,7 +48,7 @@ gradient_checkpointing_kwargs: use_reentrant: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-2/qlora.yml b/examples/llama-2/qlora.yml index 327d88c15f..87e710792a 100644 --- a/examples/llama-2/qlora.yml +++ b/examples/llama-2/qlora.yml @@ -46,7 +46,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-2/relora.yml b/examples/llama-2/relora.yml index fabdf0e0f8..8e3df58bf4 100644 --- a/examples/llama-2/relora.yml +++ b/examples/llama-2/relora.yml @@ -51,7 +51,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-3-vision/lora-11b.yaml b/examples/llama-3-vision/lora-11b.yaml index adbb616437..4e5eb4c4ee 100644 --- a/examples/llama-3-vision/lora-11b.yaml +++ b/examples/llama-3-vision/lora-11b.yaml @@ -50,7 +50,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 # flash_attention: true # use for text-only mode -sdp_attention: true +attn_implementation: sdpa warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/llama-3/3b-fp8-fsdp2.yaml b/examples/llama-3/3b-fp8-fsdp2.yaml index 57b308abdc..cfc15870fd 100644 --- a/examples/llama-3/3b-fp8-fsdp2.yaml +++ b/examples/llama-3/3b-fp8-fsdp2.yaml @@ -25,7 +25,7 @@ sample_packing: true pad_to_sequence_len: true sequence_len: 512 -flex_attention: true +attn_implementation: flex_attention flex_attn_compile_kwargs: dynamic: false mode: max-autotune-no-cudagraphs diff --git a/examples/llama-3/3b-qat-fsdp2.yaml b/examples/llama-3/3b-qat-fsdp2.yaml index 0c5a87891a..99c975351e 100644 --- a/examples/llama-3/3b-qat-fsdp2.yaml +++ b/examples/llama-3/3b-qat-fsdp2.yaml @@ -26,7 +26,7 @@ dataset_prepared_path: ./outputs/qat_out/dataset_prepared sample_packing: false sequence_len: 8192 -flash_attention: true +attn_implementation: flash_attention_2 qat: activation_dtype: int8 diff --git a/examples/llama-3/3b-qat-mxfp4.yaml b/examples/llama-3/3b-qat-mxfp4.yaml index 7ae941e9e9..4e9f646857 100644 --- a/examples/llama-3/3b-qat-mxfp4.yaml +++ b/examples/llama-3/3b-qat-mxfp4.yaml @@ -24,7 +24,7 @@ output_dir: ./outputs/qat_out/ dataset_prepared_path: ./outputs/dataset_prepared sequence_len: 2048 -flash_attention: true +attn_implementation: flash_attention_2 qat: activation_dtype: mxfp4 diff --git a/examples/llama-3/3b-qat-nvfp4.yaml b/examples/llama-3/3b-qat-nvfp4.yaml index 1ec809bbef..77cf2b19bc 100644 --- a/examples/llama-3/3b-qat-nvfp4.yaml +++ b/examples/llama-3/3b-qat-nvfp4.yaml @@ -24,7 +24,7 @@ output_dir: ./outputs/qat_out/ dataset_prepared_path: ./outputs/dataset_prepared sequence_len: 8192 -flash_attention: true +attn_implementation: flash_attention_2 qat: activation_dtype: nvfp4 diff --git a/examples/llama-3/diffusion/pretrain-1b.yaml b/examples/llama-3/diffusion/pretrain-1b.yaml index 8d05e4c60f..1b488db7a1 100644 --- a/examples/llama-3/diffusion/pretrain-1b.yaml +++ b/examples/llama-3/diffusion/pretrain-1b.yaml @@ -35,7 +35,7 @@ warmup_ratio: 0.1 optimizer: adamw_8bit lr_scheduler: cosine learning_rate: 3e-4 -sdp_attention: true +attn_implementation: sdpa bf16: auto tf32: true diff --git a/examples/llama-3/diffusion/sft-1b.yaml b/examples/llama-3/diffusion/sft-1b.yaml index f3b29a8095..b6de76af3a 100644 --- a/examples/llama-3/diffusion/sft-1b.yaml +++ b/examples/llama-3/diffusion/sft-1b.yaml @@ -41,7 +41,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: -sdp_attention: true +attn_implementation: sdpa logging_steps: 1 save_strategy: best diff --git a/examples/llama-3/fft-8b-liger-fsdp.yaml b/examples/llama-3/fft-8b-liger-fsdp.yaml index a655b97a9c..b96bc920ed 100644 --- a/examples/llama-3/fft-8b-liger-fsdp.yaml +++ b/examples/llama-3/fft-8b-liger-fsdp.yaml @@ -49,7 +49,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 2 diff --git a/examples/llama-3/fft-8b.yaml b/examples/llama-3/fft-8b.yaml index c72ec6662b..3e2809196b 100644 --- a/examples/llama-3/fft-8b.yaml +++ b/examples/llama-3/fft-8b.yaml @@ -34,7 +34,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 2 diff --git a/examples/llama-3/instruct-dpo-lora-8b.yml b/examples/llama-3/instruct-dpo-lora-8b.yml index cf823353b3..b49ace2edd 100644 --- a/examples/llama-3/instruct-dpo-lora-8b.yml +++ b/examples/llama-3/instruct-dpo-lora-8b.yml @@ -65,7 +65,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-3/instruct-lora-8b.yml b/examples/llama-3/instruct-lora-8b.yml index 401df1d721..1c61ce9e42 100644 --- a/examples/llama-3/instruct-lora-8b.yml +++ b/examples/llama-3/instruct-lora-8b.yml @@ -47,7 +47,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-3/lora-1b-deduplicate-dpo.yml b/examples/llama-3/lora-1b-deduplicate-dpo.yml index 2897636f4b..2be72c4d01 100644 --- a/examples/llama-3/lora-1b-deduplicate-dpo.yml +++ b/examples/llama-3/lora-1b-deduplicate-dpo.yml @@ -77,7 +77,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-3/lora-1b-deduplicate-sft.yml b/examples/llama-3/lora-1b-deduplicate-sft.yml index c5190d892b..ad21cb266a 100644 --- a/examples/llama-3/lora-1b-deduplicate-sft.yml +++ b/examples/llama-3/lora-1b-deduplicate-sft.yml @@ -53,7 +53,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-3/lora-1b-kernels.yml b/examples/llama-3/lora-1b-kernels.yml index 0bcf46b174..b0914f87ae 100644 --- a/examples/llama-3/lora-1b-kernels.yml +++ b/examples/llama-3/lora-1b-kernels.yml @@ -54,7 +54,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/llama-3/lora-1b-ray.yml b/examples/llama-3/lora-1b-ray.yml index 46c83348e6..a3aa1cf5e0 100644 --- a/examples/llama-3/lora-1b-ray.yml +++ b/examples/llama-3/lora-1b-ray.yml @@ -48,7 +48,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/llama-3/lora-1b-sample-packing-sequentially.yml b/examples/llama-3/lora-1b-sample-packing-sequentially.yml index dba78597bf..f6c24bc74f 100644 --- a/examples/llama-3/lora-1b-sample-packing-sequentially.yml +++ b/examples/llama-3/lora-1b-sample-packing-sequentially.yml @@ -55,7 +55,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-3/lora-1b.yml b/examples/llama-3/lora-1b.yml index 2ae2f00562..d01c618bc8 100644 --- a/examples/llama-3/lora-1b.yml +++ b/examples/llama-3/lora-1b.yml @@ -49,7 +49,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/llama-3/lora-8b.yml b/examples/llama-3/lora-8b.yml index d72b6527d4..90084ec95d 100644 --- a/examples/llama-3/lora-8b.yml +++ b/examples/llama-3/lora-8b.yml @@ -49,7 +49,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-3/opentelemetry-qlora.yml b/examples/llama-3/opentelemetry-qlora.yml index d8ce7b1eca..0c9995dae4 100644 --- a/examples/llama-3/opentelemetry-qlora.yml +++ b/examples/llama-3/opentelemetry-qlora.yml @@ -39,7 +39,6 @@ tf32: false gradient_checkpointing: true logging_steps: 1 -flash_attention: false warmup_ratio: 0.1 evals_per_epoch: 2 diff --git a/examples/llama-3/qlora-1b-gdpo.yaml b/examples/llama-3/qlora-1b-gdpo.yaml index d806fcf262..f754a68875 100644 --- a/examples/llama-3/qlora-1b-gdpo.yaml +++ b/examples/llama-3/qlora-1b-gdpo.yaml @@ -56,7 +56,7 @@ gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false -flash_attention: true +attn_implementation: flash_attention_2 logging_steps: 1 save_steps: 50 save_safetensors: true diff --git a/examples/llama-3/qlora-1b-kto.yaml b/examples/llama-3/qlora-1b-kto.yaml index a6a84e7b18..18c240d972 100644 --- a/examples/llama-3/qlora-1b-kto.yaml +++ b/examples/llama-3/qlora-1b-kto.yaml @@ -53,7 +53,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-3/qlora-1b.yml b/examples/llama-3/qlora-1b.yml index 1e4f97438a..d1e5e18ae2 100644 --- a/examples/llama-3/qlora-1b.yml +++ b/examples/llama-3/qlora-1b.yml @@ -51,7 +51,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/llama-3/qlora-fsdp-405b.yaml b/examples/llama-3/qlora-fsdp-405b.yaml index 5c236f2cf4..b801af8458 100644 --- a/examples/llama-3/qlora-fsdp-405b.yaml +++ b/examples/llama-3/qlora-fsdp-405b.yaml @@ -38,7 +38,7 @@ gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-3/qlora-fsdp-70b.yaml b/examples/llama-3/qlora-fsdp-70b.yaml index c052bc19d6..5ce774e182 100644 --- a/examples/llama-3/qlora-fsdp-70b.yaml +++ b/examples/llama-3/qlora-fsdp-70b.yaml @@ -48,7 +48,7 @@ gradient_checkpointing_kwargs: use_reentrant: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-3/qlora.yml b/examples/llama-3/qlora.yml index a8f47a0e25..fad507cd98 100644 --- a/examples/llama-3/qlora.yml +++ b/examples/llama-3/qlora.yml @@ -46,7 +46,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/llama-3/sparse-finetuning.yaml b/examples/llama-3/sparse-finetuning.yaml index 348756b700..0ce4aa03d1 100644 --- a/examples/llama-3/sparse-finetuning.yaml +++ b/examples/llama-3/sparse-finetuning.yaml @@ -44,8 +44,7 @@ gradient_checkpointing_kwargs: early_stopping_patience: resume_from_checkpoint: logging_steps: 1 -xformers_attention: -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 2 diff --git a/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml b/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml index b20f79758b..2c701a2aa3 100644 --- a/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml +++ b/examples/llama-4/do-no-use-fa2/maverick-qlora-fsdp1.yaml @@ -60,7 +60,7 @@ bf16: true tf32: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 gradient_checkpointing: offload gradient_checkpointing_kwargs: diff --git a/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml b/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml index 40449009c8..8197d16297 100644 --- a/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-qlora-fsdp1.yaml @@ -67,7 +67,7 @@ bf16: true tf32: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml b/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml index abdc51378e..2dcff36cd5 100644 --- a/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-qlora-single-h100.yaml @@ -70,7 +70,7 @@ bf16: true tf32: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 gradient_checkpointing: offload gradient_checkpointing_kwargs: diff --git a/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml b/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml index 4136dc14ac..de7ae5f50d 100644 --- a/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml +++ b/examples/llama-4/do-no-use-fa2/scout-vision-qlora-fsdp.yaml @@ -62,7 +62,7 @@ bf16: true tf32: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml index 02c04c6910..c5343fa2e0 100644 --- a/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml +++ b/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml @@ -59,7 +59,7 @@ bf16: true tf32: true logging_steps: 1 -flex_attention: true +attn_implementation: flex_attention flex_attn_compile_kwargs: dynamic: false mode: max-autotune-no-cudagraphs diff --git a/examples/llama-4/scout-qlora-single-h100-flex.yaml b/examples/llama-4/scout-qlora-single-h100-flex.yaml index 33a6911897..00491c3b16 100644 --- a/examples/llama-4/scout-qlora-single-h100-flex.yaml +++ b/examples/llama-4/scout-qlora-single-h100-flex.yaml @@ -64,7 +64,7 @@ bf16: true tf32: true torch_compile: true -flex_attention: true +attn_implementation: flex_attention flex_attn_compile_kwargs: dynamic: false mode: max-autotune-no-cudagraphs diff --git a/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml b/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml index 5972c2ae37..9b3e089b5a 100644 --- a/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml +++ b/examples/llama-4/scout-vision-qlora-fsdp2-flex.yaml @@ -61,7 +61,7 @@ bf16: true tf32: true logging_steps: 1 -flex_attention: true +attn_implementation: flex_attention flex_attn_compile_kwargs: dynamic: false mode: max-autotune-no-cudagraphs diff --git a/examples/llava/lora-7b.yaml b/examples/llava/lora-7b.yaml index 77ef7474db..56b48fda9f 100644 --- a/examples/llava/lora-7b.yaml +++ b/examples/llava/lora-7b.yaml @@ -45,8 +45,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true -eager_attention: +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/magistral/magistral-small-fsdp-qlora.yaml b/examples/magistral/magistral-small-fsdp-qlora.yaml index d46c49fe05..f31ca7326d 100644 --- a/examples/magistral/magistral-small-fsdp-qlora.yaml +++ b/examples/magistral/magistral-small-fsdp-qlora.yaml @@ -59,7 +59,7 @@ tf32: false gradient_checkpointing: resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/magistral/magistral-small-qlora.yaml b/examples/magistral/magistral-small-qlora.yaml index 188924d393..90f6b6f912 100644 --- a/examples/magistral/magistral-small-qlora.yaml +++ b/examples/magistral/magistral-small-qlora.yaml @@ -58,7 +58,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/magistral/think/magistral-small-think-qlora.yaml b/examples/magistral/think/magistral-small-think-qlora.yaml index b715b31560..85abe18daf 100644 --- a/examples/magistral/think/magistral-small-think-qlora.yaml +++ b/examples/magistral/think/magistral-small-think-qlora.yaml @@ -58,7 +58,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/magistral/vision/magistral-small-vision-24B-qlora.yml b/examples/magistral/vision/magistral-small-vision-24B-qlora.yml index 397db383ea..abd2446472 100644 --- a/examples/magistral/vision/magistral-small-vision-24B-qlora.yml +++ b/examples/magistral/vision/magistral-small-vision-24B-qlora.yml @@ -53,7 +53,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/mamba/config.yml b/examples/mamba/config.yml index 5f36595a38..0c39768d8f 100644 --- a/examples/mamba/config.yml +++ b/examples/mamba/config.yml @@ -39,7 +39,6 @@ tf32: true gradient_checkpointing: false resume_from_checkpoint: logging_steps: 1 -flash_attention: warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/mimo/mimo-7b-qlora.yaml b/examples/mimo/mimo-7b-qlora.yaml index 689213bcd4..7ced584e11 100644 --- a/examples/mimo/mimo-7b-qlora.yaml +++ b/examples/mimo/mimo-7b-qlora.yaml @@ -58,7 +58,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/ministral/ministral-small-qlora.yaml b/examples/ministral/ministral-small-qlora.yaml index 0d5300ef69..4c3bdfe94c 100644 --- a/examples/ministral/ministral-small-qlora.yaml +++ b/examples/ministral/ministral-small-qlora.yaml @@ -58,7 +58,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/ministral3/ministral3-3b-qlora.yaml b/examples/ministral3/ministral3-3b-qlora.yaml index 4efe5bd2fa..985f2fad9d 100644 --- a/examples/ministral3/ministral3-3b-qlora.yaml +++ b/examples/ministral3/ministral3-3b-qlora.yaml @@ -58,7 +58,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 # scaling_softmax: true # needs flex_attention warmup_ratio: 0.1 diff --git a/examples/ministral3/think/ministral3-3b-think-qlora.yaml b/examples/ministral3/think/ministral3-3b-think-qlora.yaml index 987c0bd549..508575cac9 100644 --- a/examples/ministral3/think/ministral3-3b-think-qlora.yaml +++ b/examples/ministral3/think/ministral3-3b-think-qlora.yaml @@ -58,7 +58,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/ministral3/vision/ministral3-3b-vision-qlora.yml b/examples/ministral3/vision/ministral3-3b-vision-qlora.yml index 0a0fdce4aa..f1430ba539 100644 --- a/examples/ministral3/vision/ministral3-3b-vision-qlora.yml +++ b/examples/ministral3/vision/ministral3-3b-vision-qlora.yml @@ -53,7 +53,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/mistral-small/mistral-small-3.1-24B-lora.yml b/examples/mistral-small/mistral-small-3.1-24B-lora.yml index d45d13ac6b..4d3f78a132 100644 --- a/examples/mistral-small/mistral-small-3.1-24B-lora.yml +++ b/examples/mistral-small/mistral-small-3.1-24B-lora.yml @@ -51,7 +51,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/mistral/bigstral/bigstral-ds-zero3.yaml b/examples/mistral/bigstral/bigstral-ds-zero3.yaml index a8dc36216e..4648ae4b4e 100644 --- a/examples/mistral/bigstral/bigstral-ds-zero3.yaml +++ b/examples/mistral/bigstral/bigstral-ds-zero3.yaml @@ -42,7 +42,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 save_total_limit: 1 save_steps: diff --git a/examples/mistral/config.yml b/examples/mistral/config.yml index e741625371..aa10667337 100644 --- a/examples/mistral/config.yml +++ b/examples/mistral/config.yml @@ -36,7 +36,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/mistral/dpo/mistral-dpo-qlora.yml b/examples/mistral/dpo/mistral-dpo-qlora.yml index 8fea14a0f7..604eada744 100644 --- a/examples/mistral/dpo/mistral-dpo-qlora.yml +++ b/examples/mistral/dpo/mistral-dpo-qlora.yml @@ -71,7 +71,6 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: false warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/mistral/lora.yml b/examples/mistral/lora.yml index 757287f19a..b157fcc219 100644 --- a/examples/mistral/lora.yml +++ b/examples/mistral/lora.yml @@ -54,7 +54,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/mistral/mistral-qlora-fsdp.yml b/examples/mistral/mistral-qlora-fsdp.yml index 8e1f03d248..27d8be3cd0 100644 --- a/examples/mistral/mistral-qlora-fsdp.yml +++ b/examples/mistral/mistral-qlora-fsdp.yml @@ -51,7 +51,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/mistral/mixtral/mixtral-8x22b-qlora-fsdp.yml b/examples/mistral/mixtral/mixtral-8x22b-qlora-fsdp.yml index dc7bd9c377..1b66de8f0f 100644 --- a/examples/mistral/mixtral/mixtral-8x22b-qlora-fsdp.yml +++ b/examples/mistral/mixtral/mixtral-8x22b-qlora-fsdp.yml @@ -49,7 +49,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/mistral/mixtral/mixtral-qlora-fsdp.yml b/examples/mistral/mixtral/mixtral-qlora-fsdp.yml index 5151e12921..bd7c8620ef 100644 --- a/examples/mistral/mixtral/mixtral-qlora-fsdp.yml +++ b/examples/mistral/mixtral/mixtral-qlora-fsdp.yml @@ -51,7 +51,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/mistral/mixtral/mixtral.yml b/examples/mistral/mixtral/mixtral.yml index d1981a699a..b493ed3172 100644 --- a/examples/mistral/mixtral/mixtral.yml +++ b/examples/mistral/mixtral/mixtral.yml @@ -69,7 +69,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/mistral/mixtral/mixtral_22.yml b/examples/mistral/mixtral/mixtral_22.yml index 0b606b7d77..3b87af04e6 100644 --- a/examples/mistral/mixtral/mixtral_22.yml +++ b/examples/mistral/mixtral/mixtral_22.yml @@ -40,7 +40,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 save_total_limit: 1 save_steps: diff --git a/examples/mistral/mps/lora-mps.yml b/examples/mistral/mps/lora-mps.yml index 07ce191dc4..1b80210852 100644 --- a/examples/mistral/mps/lora-mps.yml +++ b/examples/mistral/mps/lora-mps.yml @@ -53,8 +53,7 @@ tf32: true gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: false -sdp_attention: true +attn_implementation: sdpa loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/mistral/orpo/mistral-qlora-orpo.yml b/examples/mistral/orpo/mistral-qlora-orpo.yml index 850d286f3f..d1c0065e50 100644 --- a/examples/mistral/orpo/mistral-qlora-orpo.yml +++ b/examples/mistral/orpo/mistral-qlora-orpo.yml @@ -59,7 +59,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/mistral/qlora.yml b/examples/mistral/qlora.yml index 2a7495e95e..4fa82d11e0 100644 --- a/examples/mistral/qlora.yml +++ b/examples/mistral/qlora.yml @@ -54,7 +54,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 loss_watchdog_threshold: 5.0 loss_watchdog_patience: 3 diff --git a/examples/mistral4/fft-text.yml b/examples/mistral4/fft-text.yml index 3acb5b2edc..2cdab6a423 100644 --- a/examples/mistral4/fft-text.yml +++ b/examples/mistral4/fft-text.yml @@ -40,7 +40,7 @@ bf16: true tf32: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/mistral4/fft-vision.yml b/examples/mistral4/fft-vision.yml index baff37fe4f..22262c55af 100644 --- a/examples/mistral4/fft-vision.yml +++ b/examples/mistral4/fft-vision.yml @@ -39,7 +39,7 @@ bf16: true tf32: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/mistral4/qlora-text.yml b/examples/mistral4/qlora-text.yml index ae0cdcead8..887ce6da09 100644 --- a/examples/mistral4/qlora-text.yml +++ b/examples/mistral4/qlora-text.yml @@ -50,7 +50,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/mistral4/qlora-vision.yml b/examples/mistral4/qlora-vision.yml index a80d166ddf..d01f8e85b5 100644 --- a/examples/mistral4/qlora-vision.yml +++ b/examples/mistral4/qlora-vision.yml @@ -55,7 +55,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/nemotron-h/120b-a12b-qlora.yaml b/examples/nemotron-h/120b-a12b-qlora.yaml index 03e6d3b5ee..1174cec21f 100644 --- a/examples/nemotron-h/120b-a12b-qlora.yaml +++ b/examples/nemotron-h/120b-a12b-qlora.yaml @@ -72,7 +72,7 @@ gradient_checkpointing_kwargs: resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 2 diff --git a/examples/nemotron-h/nano-30b-a3b-qlora.yaml b/examples/nemotron-h/nano-30b-a3b-qlora.yaml index 3994ab08ed..206bd5df87 100644 --- a/examples/nemotron-h/nano-30b-a3b-qlora.yaml +++ b/examples/nemotron-h/nano-30b-a3b-qlora.yaml @@ -73,7 +73,7 @@ gradient_checkpointing_kwargs: resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/nemotron/nemotron-mini-4b-qlora.yaml b/examples/nemotron/nemotron-mini-4b-qlora.yaml index e796c149c7..3f3772071e 100644 --- a/examples/nemotron/nemotron-mini-4b-qlora.yaml +++ b/examples/nemotron/nemotron-mini-4b-qlora.yaml @@ -48,7 +48,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/olmo3/olmo3-7b-qlora.yaml b/examples/olmo3/olmo3-7b-qlora.yaml index de2bf1d3d0..b494699e0a 100644 --- a/examples/olmo3/olmo3-7b-qlora.yaml +++ b/examples/olmo3/olmo3-7b-qlora.yaml @@ -55,7 +55,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/orpheus/finetune.yml b/examples/orpheus/finetune.yml index f4bc8054ed..86a488c846 100644 --- a/examples/orpheus/finetune.yml +++ b/examples/orpheus/finetune.yml @@ -41,7 +41,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 5 diff --git a/examples/phi/phi-ft.yml b/examples/phi/phi-ft.yml index 717a459290..c16b15d8a9 100644 --- a/examples/phi/phi-ft.yml +++ b/examples/phi/phi-ft.yml @@ -48,7 +48,7 @@ gradient_checkpointing_kwargs: use_reentrant: True resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/phi/phi-qlora.yml b/examples/phi/phi-qlora.yml index 0fe1abea5d..ac49703554 100644 --- a/examples/phi/phi-qlora.yml +++ b/examples/phi/phi-qlora.yml @@ -51,7 +51,7 @@ gradient_checkpointing_kwargs: use_reentrant: True resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/phi/phi2-ft.yml b/examples/phi/phi2-ft.yml index e470c0d24a..5702cc9b8d 100644 --- a/examples/phi/phi2-ft.yml +++ b/examples/phi/phi2-ft.yml @@ -48,7 +48,7 @@ gradient_checkpointing_kwargs: use_reentrant: True resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/phi/phi3-ft-fsdp.yml b/examples/phi/phi3-ft-fsdp.yml index 1793737b54..49d3e44cbc 100644 --- a/examples/phi/phi3-ft-fsdp.yml +++ b/examples/phi/phi3-ft-fsdp.yml @@ -49,7 +49,7 @@ gradient_checkpointing_kwargs: use_reentrant: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/phi/phi3-ft.yml b/examples/phi/phi3-ft.yml index 0b204963c4..d36317f7bb 100644 --- a/examples/phi/phi3-ft.yml +++ b/examples/phi/phi3-ft.yml @@ -44,7 +44,7 @@ gradient_checkpointing_kwargs: use_reentrant: True early_stopping_patience: 3 logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 eval_steps: 1000 save_steps: 5000 diff --git a/examples/pixtral/lora-12b.yml b/examples/pixtral/lora-12b.yml index 0e64899143..2e36688a13 100644 --- a/examples/pixtral/lora-12b.yml +++ b/examples/pixtral/lora-12b.yml @@ -45,7 +45,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/plano/plano-4b-qlora.yaml b/examples/plano/plano-4b-qlora.yaml index 106e442052..30e0c36ff3 100644 --- a/examples/plano/plano-4b-qlora.yaml +++ b/examples/plano/plano-4b-qlora.yaml @@ -56,7 +56,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/qat_nvfp4/Gemma3-12B_baseline.yml b/examples/qat_nvfp4/Gemma3-12B_baseline.yml index be4e86635b..e1c7e998ae 100644 --- a/examples/qat_nvfp4/Gemma3-12B_baseline.yml +++ b/examples/qat_nvfp4/Gemma3-12B_baseline.yml @@ -24,7 +24,7 @@ output_dir: ./outputs/out_gemma/ sequence_len: 8096 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 wandb_entity: wandb_watch: diff --git a/examples/qat_nvfp4/Gemma3-12B_qat.yml b/examples/qat_nvfp4/Gemma3-12B_qat.yml index 7fa81163f7..061fd6061b 100644 --- a/examples/qat_nvfp4/Gemma3-12B_qat.yml +++ b/examples/qat_nvfp4/Gemma3-12B_qat.yml @@ -24,7 +24,7 @@ output_dir: ./outputs/qat_out_gemma/ sequence_len: 8096 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 qat: activation_dtype: nvfp4 diff --git a/examples/qat_nvfp4/Math-Gemma3-12B_baseline.yml b/examples/qat_nvfp4/Math-Gemma3-12B_baseline.yml index 9f209515b3..f11f604b4a 100644 --- a/examples/qat_nvfp4/Math-Gemma3-12B_baseline.yml +++ b/examples/qat_nvfp4/Math-Gemma3-12B_baseline.yml @@ -24,7 +24,7 @@ output_dir: ./outputs/out_math_gemma/ sequence_len: 4096 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 wandb_entity: wandb_watch: diff --git a/examples/qat_nvfp4/Math-Gemma3-12B_qat.yml b/examples/qat_nvfp4/Math-Gemma3-12B_qat.yml index ef7e754be5..f9c71321ec 100644 --- a/examples/qat_nvfp4/Math-Gemma3-12B_qat.yml +++ b/examples/qat_nvfp4/Math-Gemma3-12B_qat.yml @@ -24,7 +24,7 @@ output_dir: ./outputs/qat_out_math_gemma/ sequence_len: 4096 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 qat: activation_dtype: nvfp4 diff --git a/examples/qat_nvfp4/Math-Gemma3-27B_baseline.yml b/examples/qat_nvfp4/Math-Gemma3-27B_baseline.yml index 3a262d342d..de8bc18079 100644 --- a/examples/qat_nvfp4/Math-Gemma3-27B_baseline.yml +++ b/examples/qat_nvfp4/Math-Gemma3-27B_baseline.yml @@ -24,7 +24,7 @@ output_dir: ./outputs/out_math_gemma27/ sequence_len: 4096 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 wandb_entity: wandb_watch: diff --git a/examples/qat_nvfp4/Math-Gemma3-27B_qat.yml b/examples/qat_nvfp4/Math-Gemma3-27B_qat.yml index 87016ae9cb..c77060ee2a 100644 --- a/examples/qat_nvfp4/Math-Gemma3-27B_qat.yml +++ b/examples/qat_nvfp4/Math-Gemma3-27B_qat.yml @@ -24,7 +24,7 @@ output_dir: ./outputs/qat_out_math_gemma27/ sequence_len: 4096 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 qat: activation_dtype: nvfp4 diff --git a/examples/qat_nvfp4/Math-Qwen2.5-72B_baseline.yml b/examples/qat_nvfp4/Math-Qwen2.5-72B_baseline.yml index efec25c547..487fc8e4e1 100644 --- a/examples/qat_nvfp4/Math-Qwen2.5-72B_baseline.yml +++ b/examples/qat_nvfp4/Math-Qwen2.5-72B_baseline.yml @@ -24,7 +24,7 @@ output_dir: ./outputs/out_math_72b/ sequence_len: 4096 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 wandb_entity: wandb_watch: diff --git a/examples/qat_nvfp4/Math-Qwen2.5-72B_qat.yml b/examples/qat_nvfp4/Math-Qwen2.5-72B_qat.yml index 427d7af523..12812d8593 100644 --- a/examples/qat_nvfp4/Math-Qwen2.5-72B_qat.yml +++ b/examples/qat_nvfp4/Math-Qwen2.5-72B_qat.yml @@ -24,7 +24,7 @@ output_dir: ./outputs/qat_out_math_72b/ sequence_len: 4096 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 qat: activation_dtype: nvfp4 diff --git a/examples/qat_nvfp4/Qwen2.5-72B_baseline.yml b/examples/qat_nvfp4/Qwen2.5-72B_baseline.yml index e1eaba61f3..c52fd6b0a9 100644 --- a/examples/qat_nvfp4/Qwen2.5-72B_baseline.yml +++ b/examples/qat_nvfp4/Qwen2.5-72B_baseline.yml @@ -24,7 +24,7 @@ output_dir: ./outputs/out_qwen72b/ sequence_len: 8096 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 wandb_entity: wandb_watch: diff --git a/examples/qat_nvfp4/Qwen2.5-72B_qat.yml b/examples/qat_nvfp4/Qwen2.5-72B_qat.yml index dad7e54220..cc67107c0e 100644 --- a/examples/qat_nvfp4/Qwen2.5-72B_qat.yml +++ b/examples/qat_nvfp4/Qwen2.5-72B_qat.yml @@ -24,7 +24,7 @@ output_dir: ./outputs/qat_out_qwen72b/ sequence_len: 8096 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 qat: activation_dtype: nvfp4 diff --git a/examples/qwen2-vl/lora-7b.yaml b/examples/qwen2-vl/lora-7b.yaml index 285a35cbb5..d9bc4826b1 100644 --- a/examples/qwen2-vl/lora-7b.yaml +++ b/examples/qwen2-vl/lora-7b.yaml @@ -46,8 +46,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true -eager_attention: +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/qwen2/adamw-pretrain-fsdp2.yaml b/examples/qwen2/adamw-pretrain-fsdp2.yaml index 43fb17aabc..4129338db9 100644 --- a/examples/qwen2/adamw-pretrain-fsdp2.yaml +++ b/examples/qwen2/adamw-pretrain-fsdp2.yaml @@ -49,7 +49,7 @@ tf32: false gradient_checkpointing: false logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_steps: 10 evals_per_epoch: 0 diff --git a/examples/qwen2/dpo.yaml b/examples/qwen2/dpo.yaml index 3e87766d6b..6096053fdb 100644 --- a/examples/qwen2/dpo.yaml +++ b/examples/qwen2/dpo.yaml @@ -48,7 +48,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/qwen2/muon-pretrain-fsdp2.yaml b/examples/qwen2/muon-pretrain-fsdp2.yaml index 35c0b71f47..40dcff7be2 100644 --- a/examples/qwen2/muon-pretrain-fsdp2.yaml +++ b/examples/qwen2/muon-pretrain-fsdp2.yaml @@ -49,7 +49,7 @@ tf32: false gradient_checkpointing: false logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_steps: 10 evals_per_epoch: 0 diff --git a/examples/qwen2/prm.yaml b/examples/qwen2/prm.yaml index a709a598d3..1b3579fd4f 100644 --- a/examples/qwen2/prm.yaml +++ b/examples/qwen2/prm.yaml @@ -47,7 +47,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/qwen2/qlora-fsdp.yaml b/examples/qwen2/qlora-fsdp.yaml index 337619b614..7bb035c3ad 100644 --- a/examples/qwen2/qlora-fsdp.yaml +++ b/examples/qwen2/qlora-fsdp.yaml @@ -47,7 +47,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/qwen2/reward-model.yaml b/examples/qwen2/reward-model.yaml index 08b8b45527..b7039cba04 100644 --- a/examples/qwen2/reward-model.yaml +++ b/examples/qwen2/reward-model.yaml @@ -42,7 +42,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/examples/qwen2_5-vl/lora-7b.yaml b/examples/qwen2_5-vl/lora-7b.yaml index 7d499d841f..e78aac78b8 100644 --- a/examples/qwen2_5-vl/lora-7b.yaml +++ b/examples/qwen2_5-vl/lora-7b.yaml @@ -46,8 +46,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true -eager_attention: +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml b/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml index f63b1d1ce9..e8e7e08c79 100644 --- a/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml +++ b/examples/qwen3-next/qwen3-next-80b-a3b-qlora.yaml @@ -68,7 +68,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml b/examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml index f66bcd3700..47842c5616 100644 --- a/examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml +++ b/examples/qwen3.5/122b-a10b-moe-qlora-fsdp.yaml @@ -65,7 +65,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/qwen3.5/122b-a10b-moe-qlora.yaml b/examples/qwen3.5/122b-a10b-moe-qlora.yaml index 4447cf73c2..f2675c7d78 100644 --- a/examples/qwen3.5/122b-a10b-moe-qlora.yaml +++ b/examples/qwen3.5/122b-a10b-moe-qlora.yaml @@ -65,7 +65,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/qwen3.5/27b-fft.yaml b/examples/qwen3.5/27b-fft.yaml index 9f875ec26c..ab206b772c 100644 --- a/examples/qwen3.5/27b-fft.yaml +++ b/examples/qwen3.5/27b-fft.yaml @@ -50,7 +50,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/qwen3.5/27b-qlora-fsdp.yaml b/examples/qwen3.5/27b-qlora-fsdp.yaml index 79b87a32fe..7a5423c773 100644 --- a/examples/qwen3.5/27b-qlora-fsdp.yaml +++ b/examples/qwen3.5/27b-qlora-fsdp.yaml @@ -61,7 +61,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/qwen3.5/27b-qlora.yaml b/examples/qwen3.5/27b-qlora.yaml index 18c0af95b7..2401a4865f 100644 --- a/examples/qwen3.5/27b-qlora.yaml +++ b/examples/qwen3.5/27b-qlora.yaml @@ -61,7 +61,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml b/examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml index ad17366cb4..2fb7f15f89 100644 --- a/examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml +++ b/examples/qwen3.5/35b-a3b-moe-qlora-fsdp.yaml @@ -65,7 +65,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/qwen3.5/35b-a3b-moe-qlora.yaml b/examples/qwen3.5/35b-a3b-moe-qlora.yaml index 22468a178a..a6afc1aa2c 100644 --- a/examples/qwen3.5/35b-a3b-moe-qlora.yaml +++ b/examples/qwen3.5/35b-a3b-moe-qlora.yaml @@ -75,7 +75,7 @@ gradient_checkpointing: true activation_offloading: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/qwen3.5/35b-a3b-moe-vision-lora.yaml b/examples/qwen3.5/35b-a3b-moe-vision-lora.yaml index a7c85f785f..7cfad32902 100644 --- a/examples/qwen3.5/35b-a3b-moe-vision-lora.yaml +++ b/examples/qwen3.5/35b-a3b-moe-vision-lora.yaml @@ -50,7 +50,7 @@ gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 weight_decay: 0.0 diff --git a/examples/qwen3.5/9b-fft-vision.yaml b/examples/qwen3.5/9b-fft-vision.yaml index b6aeb859d0..e8427b8844 100644 --- a/examples/qwen3.5/9b-fft-vision.yaml +++ b/examples/qwen3.5/9b-fft-vision.yaml @@ -40,7 +40,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/qwen3.5/9b-lora-vision.yaml b/examples/qwen3.5/9b-lora-vision.yaml index 1c3717724d..9c2b9397e5 100644 --- a/examples/qwen3.5/9b-lora-vision.yaml +++ b/examples/qwen3.5/9b-lora-vision.yaml @@ -58,7 +58,7 @@ gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/qwen3/32b-qlora.yaml b/examples/qwen3/32b-qlora.yaml index f4a4f28169..dd5dd696ef 100644 --- a/examples/qwen3/32b-qlora.yaml +++ b/examples/qwen3/32b-qlora.yaml @@ -60,7 +60,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/qwen3/8b-qat-fsdp2.yml b/examples/qwen3/8b-qat-fsdp2.yml index cfbe5a4b77..3c9607a9ab 100644 --- a/examples/qwen3/8b-qat-fsdp2.yml +++ b/examples/qwen3/8b-qat-fsdp2.yml @@ -23,7 +23,7 @@ output_dir: ./outputs/qat_out/ sequence_len: 2048 sample_packing: true -flex_attention: true +attn_implementation: flex_attention flex_attn_compile_kwargs: diff --git a/examples/qwen3/qlora-fsdp.yaml b/examples/qwen3/qlora-fsdp.yaml index e4d584dc7e..a3852d457f 100644 --- a/examples/qwen3/qlora-fsdp.yaml +++ b/examples/qwen3/qlora-fsdp.yaml @@ -46,7 +46,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/seed-oss/seed-oss-36b-qlora.yaml b/examples/seed-oss/seed-oss-36b-qlora.yaml index 00e7cf3ebb..a8423f8512 100644 --- a/examples/seed-oss/seed-oss-36b-qlora.yaml +++ b/examples/seed-oss/seed-oss-36b-qlora.yaml @@ -47,7 +47,7 @@ tf32: false gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/smolvlm2/smolvlm2-2B-lora.yaml b/examples/smolvlm2/smolvlm2-2B-lora.yaml index 1aeff408da..4cd8d5b0d2 100644 --- a/examples/smolvlm2/smolvlm2-2B-lora.yaml +++ b/examples/smolvlm2/smolvlm2-2B-lora.yaml @@ -45,8 +45,7 @@ tf32: true gradient_checkpointing: true logging_steps: 1 -flash_attention: true -eager_attention: +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/streaming/pretrain.yaml b/examples/streaming/pretrain.yaml index bc8edefd67..a0d8b17c04 100644 --- a/examples/streaming/pretrain.yaml +++ b/examples/streaming/pretrain.yaml @@ -20,7 +20,7 @@ output_dir: ./outputs/smollm2-135m-pretrain-streaming sequence_len: 1024 sample_packing: true pretrain_multipack_attn: true # Prevent cross-attention between packed sequences -flash_attention: true +attn_implementation: flash_attention_2 # Batch size settings gradient_accumulation_steps: 8 diff --git a/examples/streaming/sft.yaml b/examples/streaming/sft.yaml index 47b9f493fa..4a43c34ebb 100644 --- a/examples/streaming/sft.yaml +++ b/examples/streaming/sft.yaml @@ -18,7 +18,7 @@ output_dir: ./outputs/smollm2-135m-sft-streaming # Sequence and packing settings sequence_len: 1024 sample_packing: true -flash_attention: true +attn_implementation: flash_attention_2 # Batch size settings gradient_accumulation_steps: 4 diff --git a/examples/swanlab/dpo-swanlab-completions.yml b/examples/swanlab/dpo-swanlab-completions.yml index 5615ca6382..fb21dbbba1 100644 --- a/examples/swanlab/dpo-swanlab-completions.yml +++ b/examples/swanlab/dpo-swanlab-completions.yml @@ -78,7 +78,7 @@ tf32: false # Performance gradient_checkpointing: true -flash_attention: true +attn_implementation: flash_attention_2 # Checkpointing and Logging logging_steps: 1 diff --git a/examples/swanlab/dpo-swanlab-full-featured.yml b/examples/swanlab/dpo-swanlab-full-featured.yml index c25178c631..ac52e6a85b 100644 --- a/examples/swanlab/dpo-swanlab-full-featured.yml +++ b/examples/swanlab/dpo-swanlab-full-featured.yml @@ -102,7 +102,7 @@ bf16: auto tf32: false gradient_checkpointing: true -flash_attention: true +attn_implementation: flash_attention_2 # ============================================================================ # Checkpointing and Logging diff --git a/examples/swanlab/lora-swanlab-profiling.yml b/examples/swanlab/lora-swanlab-profiling.yml index 1255105a6d..3dff6e315f 100644 --- a/examples/swanlab/lora-swanlab-profiling.yml +++ b/examples/swanlab/lora-swanlab-profiling.yml @@ -59,7 +59,7 @@ tf32: false # Performance gradient_checkpointing: true -flash_attention: true +attn_implementation: flash_attention_2 # Checkpointing and Logging logging_steps: 1 diff --git a/examples/trinity/trinity-nano-preview-qlora.yaml b/examples/trinity/trinity-nano-preview-qlora.yaml index d8bf9f0739..52c0c0c60c 100644 --- a/examples/trinity/trinity-nano-preview-qlora.yaml +++ b/examples/trinity/trinity-nano-preview-qlora.yaml @@ -58,7 +58,7 @@ gradient_checkpointing: true resume_from_checkpoint: logging_steps: 1 # flash_attention: true # Not supported -sdp_attention: true +attn_implementation: sdpa warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/voxtral/voxtral-mini-audio-qlora.yml b/examples/voxtral/voxtral-mini-audio-qlora.yml index 59150c4ca9..cfa351ccde 100644 --- a/examples/voxtral/voxtral-mini-audio-qlora.yml +++ b/examples/voxtral/voxtral-mini-audio-qlora.yml @@ -70,7 +70,7 @@ gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 1 diff --git a/examples/voxtral/voxtral-mini-qlora.yml b/examples/voxtral/voxtral-mini-qlora.yml index bdbc5f8673..61e8933d06 100644 --- a/examples/voxtral/voxtral-mini-qlora.yml +++ b/examples/voxtral/voxtral-mini-qlora.yml @@ -64,7 +64,7 @@ gradient_checkpointing_kwargs: use_reentrant: false resume_from_checkpoint: logging_steps: 1 -flash_attention: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: diff --git a/src/axolotl/cli/merge_lora.py b/src/axolotl/cli/merge_lora.py index 00e5303bd4..e111ca9f97 100644 --- a/src/axolotl/cli/merge_lora.py +++ b/src/axolotl/cli/merge_lora.py @@ -147,7 +147,7 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs) -> None: load_in_8bit=False, load_in_4bit=False, quantize_moe_experts=False, - flash_attention=False, + attn_implementation=None, context_parallel_size=None, deepspeed=None, fsdp=None, diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index fe832dd452..15624173d6 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -257,19 +257,13 @@ def build(self, total_num_steps): training_arguments_kwargs["curriculum_sampling"] = self.cfg.curriculum_sampling training_arguments_kwargs["sample_packing"] = bool(self.cfg.sample_packing) - training_arguments_kwargs["sample_packing_drop_attention_mask"] = bool( - self.cfg.flash_attention - or self.cfg.xformers_attention - or self.cfg.flex_attention + training_arguments_kwargs["sample_packing_drop_attention_mask"] = ( + self.cfg.attn_supports_packing ) training_arguments_kwargs["multipack_real_batches"] = ( self.cfg.multipack_real_batches if self.cfg.multipack_real_batches is not None - else not ( - self.cfg.flash_attention - or self.cfg.flex_attention - or self.cfg.xformers_attention - ) + else not self.cfg.attn_supports_packing ) training_arguments_kwargs["eval_sample_packing"] = bool( self.cfg.eval_sample_packing @@ -508,11 +502,11 @@ def build_collator( # Use V2BatchSamplerDataCollatorForSeq2Seq for flex attention, # supported multipack models, or non-flash-attention llama if ( - self.cfg.flex_attention + self.cfg.attn_implementation == "flex_attention" or self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES or ( self.cfg.model_config_type in ["llama"] - and self.cfg.flash_attention is not True + and self.cfg.attn_implementation != "flash_attention_2" ) ): collator = V2BatchSamplerDataCollatorForSeq2Seq diff --git a/src/axolotl/integrations/lm_eval/__init__.py b/src/axolotl/integrations/lm_eval/__init__.py index 6a82dd6cfa..732ce15925 100644 --- a/src/axolotl/integrations/lm_eval/__init__.py +++ b/src/axolotl/integrations/lm_eval/__init__.py @@ -23,7 +23,7 @@ def post_train_unload(self, cfg): for lm_eval_args in build_lm_eval_command( cfg.lm_eval_tasks, bfloat16=cfg.bfloat16 or cfg.bf16, - flash_attention=cfg.flash_attention, + flash_attention=cfg.attn_uses_flash_lib, output_dir=cfg.output_dir, batch_size=cfg.lm_eval_batch_size, wandb_project=cfg.wandb_project, diff --git a/src/axolotl/integrations/lm_eval/cli.py b/src/axolotl/integrations/lm_eval/cli.py index 4b905d476e..a20f4d1547 100644 --- a/src/axolotl/integrations/lm_eval/cli.py +++ b/src/axolotl/integrations/lm_eval/cli.py @@ -114,10 +114,18 @@ def lm_eval(config: str, cloud: Optional[str] = None): with open(config, encoding="utf-8") as file: cfg: DictDefault = DictDefault(yaml.safe_load(file)) + # This path operates on raw YAML via DictDefault (not the validated + # AxolotlInputConfig), so we resolve flash-attn from either the canonical + # `attn_implementation` field or the deprecated `flash_attention` boolean. + _flash_attn_impls = {"flash_attention_2", "flash_attention_3"} + lm_eval_flash_attention = bool( + cfg.flash_attention or cfg.attn_implementation in _flash_attn_impls + ) + for lm_eval_args in build_lm_eval_command( cfg.lm_eval_tasks, bfloat16=cfg.bfloat16 or cfg.bf16, - flash_attention=cfg.flash_attention, + flash_attention=lm_eval_flash_attention, output_dir=cfg.output_dir, batch_size=cfg.lm_eval_batch_size, wandb_project=cfg.wandb_project, diff --git a/src/axolotl/integrations/swanlab/plugins.py b/src/axolotl/integrations/swanlab/plugins.py index 16218d39d8..55f19ac59b 100644 --- a/src/axolotl/integrations/swanlab/plugins.py +++ b/src/axolotl/integrations/swanlab/plugins.py @@ -383,7 +383,9 @@ def safe_convert(value): "seed": safe_convert(getattr(cfg, "seed", None)), "bf16": safe_convert(getattr(cfg, "bf16", None)), "tf32": safe_convert(getattr(cfg, "tf32", None)), - "flash_attention": safe_convert(getattr(cfg, "flash_attention", None)), + "attn_implementation": safe_convert( + getattr(cfg, "attn_implementation", None) + ), "sample_packing": safe_convert(getattr(cfg, "sample_packing", None)), } diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 57aabfbab2..061509d396 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -343,12 +343,7 @@ def _configure_embedding_dtypes(self): # LlamaRMSNorm layers are in fp32 after kbit_training or full finetune, so # we need to convert them back to fp16/bf16 for flash-attn compatibility. ( - ( - needs_fa2_dtype - or self.cfg.flash_attention - or self.cfg.flex_attention - or self.cfg.sage_attention - ) + (needs_fa2_dtype or self.cfg.attn_needs_dtype_cast) and not self.is_qlora_and_fsdp_enabled ) or ( @@ -633,35 +628,14 @@ def _set_quantization_config(self): ) def _set_attention_config(self): - """Sample packing uses custom FA2 patch""" - if self.cfg.gemma4_hybrid_attn_impl: - # Load model with flash_attention_2 for sliding window layers; - # global layers will be patched to sdpa post-load. - self.model_kwargs["attn_implementation"] = "flash_attention_2" - self.model_config._attn_implementation = "flash_attention_2" - # Set flash_attention so multipack/sample_packing patches activate - self.cfg.flash_attention = True - elif self.cfg.attn_implementation: - self.model_kwargs["attn_implementation"] = self.cfg.attn_implementation - elif self.cfg.flex_attention: - self.model_kwargs["attn_implementation"] = "flex_attention" - self.model_config._attn_implementation = "flex_attention" - - elif self.cfg.flash_attention: - if not self.cfg.sample_packing and self.cfg.s2_attention: - pass - self.model_kwargs["attn_implementation"] = "flash_attention_2" - self.model_config._attn_implementation = "flash_attention_2" - elif self.cfg.sdp_attention: - self.model_kwargs["attn_implementation"] = "sdpa" - self.model_config._attn_implementation = "sdpa" - elif self.cfg.sage_attention: - # sets FA2 attention to re-use same internal handling like masking - self.model_kwargs["attn_implementation"] = "flash_attention_2" - self.model_config._attn_implementation = "flash_attention_2" - elif self.cfg.eager_attention: - self.model_kwargs["attn_implementation"] = "eager" - self.model_config._attn_implementation = "eager" + # s2 patches FA2 internals (load as FA2); fp8 replaces sdpa post-load (load as sdpa). + _LOAD_TIME_OVERRIDE = {"s2": "flash_attention_2", "fp8": "sdpa"} + if self.cfg.attn_implementation: + hf_impl = _LOAD_TIME_OVERRIDE.get( + self.cfg.attn_implementation, self.cfg.attn_implementation + ) + self.model_kwargs["attn_implementation"] = hf_impl + self.model_config._attn_implementation = hf_impl if self.cfg.low_cpu_mem_usage: self.model_kwargs["low_cpu_mem_usage"] = True diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 01d9997d7d..68952014f9 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -172,6 +172,7 @@ def apply_post_model_load_patches(self, model: PreTrainedModel): self._apply_llama_flash_attn_patches(model) self._apply_lora_kernel_patch(model) self._apply_scaling_softmax_patch(model) + self._apply_fp8_attention_patches(model) def _apply_gemma_hybrid_attention(self, model: PreTrainedModel): """Apply hybrid attention: FA2 for sliding window layers, SDPA for global layers. @@ -252,11 +253,28 @@ def _apply_gemma_hybrid_attention(self, model: PreTrainedModel): def _apply_flash_attention_patches(self): """Apply patches related to Flash Attention.""" - if self.cfg.xformers_attention and self.cfg.sample_packing: - from axolotl.monkeypatch.attention import patch_xformers_attn_over_fa2 + if self.cfg.attn_implementation == "xformers": + from axolotl.monkeypatch.attention import register_xformers_attn - patch_xformers_attn_over_fa2() - self.cfg.flash_attention = True + register_xformers_attn() + + if self.cfg.sample_packing: + # Also patch FA2 slot for legacy code paths that use it directly + from axolotl.monkeypatch.attention import patch_xformers_attn_over_fa2 + + patch_xformers_attn_over_fa2() + + if self.cfg.attn_implementation == "sage": + from axolotl.monkeypatch.attention import register_sage_attn + + register_sage_attn() + + def _apply_fp8_attention_patches(self, model): + """Apply FP8 low-precision attention via torchao.""" + if self.cfg.attn_implementation == "fp8": + from axolotl.monkeypatch.attention.fp8_attn import patch_fp8_attention + + patch_fp8_attention(model) def _apply_chunked_cross_entropy_patch(self): if self.cfg.chunked_cross_entropy: @@ -315,7 +333,7 @@ def _apply_adapter_patches(self): def _apply_flex_attention_patches(self): """Apply patches for flexible attention.""" - if self.cfg.flex_attention: + if self.cfg.attn_implementation == "flex_attention": from axolotl.monkeypatch.attention.flex_attn import ( patch_flex_wrapper, ) @@ -325,14 +343,14 @@ def _apply_flex_attention_patches(self): def _apply_sageattn_patches(self): """Apply patches for SageAttention.""" - if self.cfg.sage_attention: + if self.cfg.attn_implementation == "sage": from axolotl.monkeypatch.attention.sage_attn import patch_sageattn patch_sageattn() def _apply_flash_attn_4_patches(self): """Auto-apply FA4 when flash_attention is enabled and FA4 is available on SM90+.""" - if not self.cfg.flash_attention: + if not self.cfg.attn_uses_flash_lib: return from axolotl.monkeypatch.attention.flash_attn_4 import patch_flash_attn_4 @@ -401,7 +419,7 @@ def _apply_model_specific_patches(self): if ( self.cfg.model_config_type in ["qwen3_5", "qwen3_5_moe"] and self.cfg.is_multimodal - and self.cfg.flash_attention + and self.cfg.attn_uses_flash_lib ): from axolotl.monkeypatch.models.qwen3_5.modeling import ( patch_qwen3_5_vlm_flash_attention, @@ -553,7 +571,7 @@ def _apply_multipack_patches(self): """Apply multipack patches if necessary.""" if ( self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES - and (self.cfg.flash_attention or self.cfg.flex_attention) + and self.cfg.attn_supports_packing and self.cfg.sample_packing ): # Get automap config if it exists @@ -674,7 +692,9 @@ def _apply_voxtral_patches(self): def _patch_attention(self): """Apply attention-specific patches based on model type.""" - if not (self.cfg.flash_attention and hasattr(self.model_config, "model_type")): + if not ( + self.cfg.attn_uses_flash_lib and hasattr(self.model_config, "model_type") + ): return if self.model_config.model_type == "btlm": @@ -720,7 +740,7 @@ def _patch_llama_flash_attention(self): replace_llama_attn_with_flash_attn, ) - if self.cfg.s2_attention: + if self.cfg.attn_implementation == "s2": LOG.info("patching w/ flash-enabled, shifted-sparse attention") replace_llama_attn_with_flash_attn( cross_entropy=self.cfg.flash_attn_cross_entropy, @@ -746,14 +766,14 @@ def _patch_llama_derived_model(self): """Modify all llama derived models in one block.""" if self.cfg.is_llama_derived_model and not ( self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES - and (self.cfg.flash_attention or self.cfg.flex_attention) + and self.cfg.attn_supports_packing and self.cfg.sample_packing ): - if self.cfg.flash_attention: + if self.cfg.attn_uses_flash_lib: self._patch_llama_flash_attention() - elif self.cfg.xformers_attention: + elif self.cfg.attn_implementation == "xformers": self._patch_llama_xformers_attention() - elif self.cfg.s2_attention: + elif self.cfg.attn_implementation == "s2": raise NotImplementedError( "Shifted-sparse attention not currently implemented without flash attention." ) @@ -765,7 +785,7 @@ def _apply_llama_flash_attn_patches(self, model): in ["llama", "llama4", "ernie4_5", "ernie4_5_moe"] and not self.cfg.trust_remote_code and not self.cfg.gptq - and self.cfg.flash_attention + and self.cfg.attn_uses_flash_lib and is_flash_attn_available() and not self.inference ): diff --git a/src/axolotl/loaders/tokenizer.py b/src/axolotl/loaders/tokenizer.py index 52f7146046..572a880bd4 100644 --- a/src/axolotl/loaders/tokenizer.py +++ b/src/axolotl/loaders/tokenizer.py @@ -205,7 +205,11 @@ def _load_mistral_common_tokenizer(cfg: DictDefault): os.environ["TOKENIZERS_PARALLELISM"] = "false" # Mistral's official FA implementation requires left padding - if cfg.is_mistral_derived_model and cfg.flash_attention and not cfg.sample_packing: + if ( + cfg.is_mistral_derived_model + and cfg.attn_implementation == "flash_attention_2" + and not cfg.sample_packing + ): tokenizer.padding_side = "left" # Qwen base only has single token, so we need to set the special tokens diff --git a/src/axolotl/monkeypatch/attention/__init__.py b/src/axolotl/monkeypatch/attention/__init__.py index 15ed764f40..74bd61e774 100644 --- a/src/axolotl/monkeypatch/attention/__init__.py +++ b/src/axolotl/monkeypatch/attention/__init__.py @@ -17,3 +17,29 @@ def unpatch_xformers_attn_over_fa2(): from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS ALL_ATTENTION_FUNCTIONS["flash_attention_2"] = flash_attention_forward() + + +def register_xformers_attn(): + """Register xformers as its own attention backend with FA2 mask behavior.""" + from transformers.masking_utils import ALL_MASK_ATTENTION_FUNCTIONS + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + from .xformers import xformers_attention_forward + + ALL_ATTENTION_FUNCTIONS.register("xformers", xformers_attention_forward) + ALL_MASK_ATTENTION_FUNCTIONS.register( + "xformers", ALL_MASK_ATTENTION_FUNCTIONS["flash_attention_2"] + ) + + +def register_sage_attn(): + """Register sage as its own attention backend with FA2 mask behavior.""" + from transformers.masking_utils import ALL_MASK_ATTENTION_FUNCTIONS + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + from .sage_attn import sage_attention_forward + + ALL_ATTENTION_FUNCTIONS.register("sage", sage_attention_forward) + ALL_MASK_ATTENTION_FUNCTIONS.register( + "sage", ALL_MASK_ATTENTION_FUNCTIONS["flash_attention_2"] + ) diff --git a/src/axolotl/monkeypatch/attention/fp8_attn.py b/src/axolotl/monkeypatch/attention/fp8_attn.py new file mode 100644 index 0000000000..224e8c3b7e --- /dev/null +++ b/src/axolotl/monkeypatch/attention/fp8_attn.py @@ -0,0 +1,30 @@ +"""FP8 low-precision attention via torchao. + +Requires: + - PyTorch >= 2.11.0 + - SM90+ (Hopper/Blackwell) GPU + - flash-attn package with FA3 support + - torchao >= 0.17.0 + +Uses per-head FP8 quantized attention with automatic RoPE fusion under torch.compile. +The torchao patch replaces F.scaled_dot_product_attention, so the model must use +HF's "sdpa" attention implementation for the patch to intercept attention calls. +""" + +import logging + +import torch + +LOG = logging.getLogger(__name__) + + +def patch_fp8_attention(model: torch.nn.Module) -> torch.nn.Module: + """Apply FP8 low-precision attention to a model. + + Must be called after model loading and before torch.compile. + KV caching should be disabled (config.use_cache = False). + """ + from torchao.prototype.attention import apply_low_precision_attention + + LOG.info("Applying FP8 low-precision attention (torchao)") + return apply_low_precision_attention(model) diff --git a/src/axolotl/monkeypatch/attention/sage_attn.py b/src/axolotl/monkeypatch/attention/sage_attn.py index cc9fdb94de..6e9ba0f858 100644 --- a/src/axolotl/monkeypatch/attention/sage_attn.py +++ b/src/axolotl/monkeypatch/attention/sage_attn.py @@ -191,21 +191,9 @@ def sage_attention_forward( def patch_sageattn(): - """Patch SageAttention for use with transformers.""" + """Validate SageAttention is available. Registration in the attention/mask + function registries is handled by register_sage_attn() in __init__.py.""" _check_sageattn_imported() - from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS - - # Replace flash attention with sage attention - ALL_ATTENTION_FUNCTIONS.register("flash_attention_2", sage_attention_forward) - - # Note: New method after transformers refactor to use ALL_MASK_ATTENTION_FUNCTIONS - # Register sage_attention with the global attention interface - # ALL_ATTENTION_FUNCTIONS.register("sage_attention", sage_attention_forward) - - # from transformers.masking_utils import ALL_MASK_ATTENTION_FUNCTIONS, flash_attention_mask - - # ALL_MASK_ATTENTION_FUNCTIONS.register("sage_attention", flash_attention_mask) - - LOG.info("SageAttention patched successfully") + LOG.info("SageAttention validated successfully") diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index 8137bac0c8..edb61441b4 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -955,7 +955,10 @@ def on_train_end(self, args, state, control, **kwargs): """ handle T4 gpu, we need to convert attention to eager for inference """ - if "Tesla T4" in self.gpu_name and self.cfg.xformers_attention: + if ( + "Tesla T4" in self.gpu_name + and self.cfg.attn_implementation == "xformers" + ): trainer.model.config._attn_implementation = "eager" trainer.model.gradient_checkpointing_disable() trainer.model.config.use_cache = True diff --git a/src/axolotl/utils/collators/mm_chat.py b/src/axolotl/utils/collators/mm_chat.py index b81612cbc5..12dbde7d1f 100644 --- a/src/axolotl/utils/collators/mm_chat.py +++ b/src/axolotl/utils/collators/mm_chat.py @@ -43,16 +43,16 @@ def process_rows( # Initialize batch messages = [ex["messages"] for ex in examples] - batch = self.processing_strategy.processor.apply_chat_template( - messages, - add_generation_prompt=False, - tokenize=True, - chat_template=self.processing_strategy.chat_template, - processor_kwargs={ - "return_tensors": "pt", - "padding": True, - "return_dict": True, - }, + batch = dict( + self.processing_strategy.processor.apply_chat_template( + messages, + add_generation_prompt=False, + tokenize=True, + return_tensors="pt", + padding=True, + return_dict=True, + chat_template=self.processing_strategy.chat_template, + ) ) # Process the labels diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index c52ddce1a0..9874b9da5d 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -10,7 +10,9 @@ BaseModel, Field, StringConstraints, + computed_field, field_serializer, + field_validator, model_validator, ) @@ -27,7 +29,17 @@ ) from axolotl.utils.schemas.deprecated import DeprecatedParameters, RemappedParameters from axolotl.utils.schemas.dynamic_checkpoint import DynamicCheckpointConfig -from axolotl.utils.schemas.enums import ChatTemplate, RingAttnFunc, RLType +from axolotl.utils.schemas.enums import ( + ATTN_IMPLS_SUPPORTING_PACKING, + ATTN_IMPLS_USING_FLASH_LIB, + ATTN_IMPLS_WITHOUT_DTYPE_CAST, + CANONICAL_ATTN_IMPLS, + LEGACY_ATTN_FLAG_TO_IMPL, + SHORT_FORM_ALIAS_TO_CANONICAL, + ChatTemplate, + RingAttnFunc, + RLType, +) from axolotl.utils.schemas.fsdp import FSDPConfig from axolotl.utils.schemas.integrations import ( CometConfig, @@ -731,28 +743,35 @@ class AxolotlInputConfig( xformers_attention: bool | None = Field( default=None, + deprecated="Use `attn_implementation: xformers` instead.", json_schema_extra={ - "description": "Whether to use xformers attention patch https://github.com/facebookresearch/xformers" + "description": "[DEPRECATED] Use `attn_implementation: xformers`. https://github.com/facebookresearch/xformers" }, ) sdp_attention: bool | None = Field( default=None, + deprecated="Use `attn_implementation: sdpa` instead.", json_schema_extra={ - "description": "Whether to use scaled-dot-product attention https://pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html" + "description": "[DEPRECATED] Use `attn_implementation: sdpa`." }, ) s2_attention: bool | None = Field( default=None, + deprecated="Use `attn_implementation: s2` instead.", json_schema_extra={ - "description": "Shifted-sparse attention (only llama) - https://arxiv.org/pdf/2309.12307.pdf" + "description": "[DEPRECATED] Use `attn_implementation: s2`. Shifted-sparse attention (only llama) - https://arxiv.org/pdf/2309.12307.pdf" }, ) - flex_attention: bool | None = None + flex_attention: bool | None = Field( + default=None, + deprecated="Use `attn_implementation: flex_attention` instead.", + ) flex_attn_compile_kwargs: dict[str, Any] | None = None flash_attention: bool | None = Field( default=None, + deprecated="Use `attn_implementation: flash_attention_2` instead.", json_schema_extra={ - "description": "Whether to use flash attention patch https://github.com/Dao-AILab/flash-attention" + "description": "[DEPRECATED] Use `attn_implementation: flash_attention_2`. https://github.com/Dao-AILab/flash-attention" }, ) flash_attn_cross_entropy: bool | None = Field( @@ -779,17 +798,26 @@ class AxolotlInputConfig( ) sage_attention: bool | None = Field( default=None, + deprecated="Use `attn_implementation: sage` instead.", json_schema_extra={ - "description": "Whether to use SageAttention https://github.com/thu-ml/SageAttention" + "description": "[DEPRECATED] Use `attn_implementation: sage`. https://github.com/thu-ml/SageAttention" }, ) - eager_attention: bool | None = None + eager_attention: bool | None = Field( + default=None, + deprecated="Use `attn_implementation: eager` instead.", + ) attn_implementation: str | None = Field( default=None, json_schema_extra={ - "description": "Specify a custom attention implementation, used mostly for kernels." + "description": ( + "Attention backend. Canonical values: eager, sdpa, flash_attention_2, " + "flash_attention_3, flex_attention, xformers, sage, s2, fp8. Hub-kernel " + "paths (e.g. kernels-community/flash-attn3) are also accepted and passed " + "through to transformers." + ) }, ) @@ -1327,6 +1355,25 @@ def datasets_serializer( return [ds_config.model_dump(exclude_none=True) for ds_config in ds_configs] return None + # --- Attention capability flags (derived from attn_implementation) --- + + @computed_field # type: ignore[misc] + @property + def attn_supports_packing(self) -> bool: + return self.attn_implementation in ATTN_IMPLS_SUPPORTING_PACKING + + @computed_field # type: ignore[misc] + @property + def attn_uses_flash_lib(self) -> bool: + return self.attn_implementation in ATTN_IMPLS_USING_FLASH_LIB + + @computed_field # type: ignore[misc] + @property + def attn_needs_dtype_cast(self) -> bool: + if self.attn_implementation is None: + return False + return self.attn_implementation not in ATTN_IMPLS_WITHOUT_DTYPE_CAST + @model_validator(mode="before") @classmethod def warn_peft_trainable_token_to_fix_untrained(cls, data): @@ -1349,24 +1396,104 @@ def warn_peft_trainable_token_to_fix_untrained(cls, data): @model_validator(mode="before") @classmethod - def check_sageattn_wo_sample_packing(cls, data): - if (not data.get("sample_packing", False)) and data.get("sage_attention"): - if not data.get("pad_to_sequence_len", False): - LOG.warning( - "We recommend turning on `pad_to_sequence_len` for SageAttention without packing." - "This is because there has been signs that the loss explodes after a few steps." + def normalize_attn_implementation(cls, data): + """Map legacy boolean attention flags to canonical attn_implementation, warn, then strip.""" + if not isinstance(data, dict): + return data + + attn_impl = data.get("attn_implementation") + set_flags = [f for f in LEGACY_ATTN_FLAG_TO_IMPL if data.get(f)] + + # gemma4_hybrid requires flash_attention_2 for the sliding-window layers; + # post-load patching swaps global layers to sdpa (see + # `_apply_gemma_hybrid_attention`). Default it in when the user didn't + # pick a backend; reject any incompatible explicit choice. + if data.get("gemma4_hybrid_attn_impl"): + if not attn_impl and not set_flags: + data["attn_implementation"] = "flash_attention_2" + attn_impl = "flash_attention_2" + elif attn_impl and attn_impl != "flash_attention_2": + raise ValueError( + f"gemma4_hybrid_attn_impl requires attn_implementation=" + f"flash_attention_2 (sliding-window layers run under FA2); " + f"got {attn_impl!r}." ) + + if attn_impl and set_flags: + raise ValueError( + f"attn_implementation={attn_impl!r} cannot be combined with legacy " + f"attention flags ({', '.join(sorted(set_flags))}). The legacy " + f"flags are deprecated — set only `attn_implementation`." + ) + + if not attn_impl and set_flags: + # Priority: specific backends beat generic flash/sdp/eager fallbacks. + for flag in LEGACY_ATTN_FLAG_TO_IMPL: + if flag in set_flags: + canonical = LEGACY_ATTN_FLAG_TO_IMPL[flag] + data["attn_implementation"] = canonical + LOG.warning( + "`%s: true` is deprecated and will be removed in a future " + "release. Use `attn_implementation: %s` instead.", + flag, + canonical, + ) + break + + # Strip legacy flags from validated data — canonical field is authoritative. + for flag in LEGACY_ATTN_FLAG_TO_IMPL: + data.pop(flag, None) + return data - @model_validator(mode="before") + @field_validator("attn_implementation", mode="before") @classmethod - def check_sageattn_fft(cls, data): - if (not data.get("adapter", False)) and data.get("sage_attention"): + def validate_attn_implementation(cls, value): + """Accept canonical names and hub-kernel paths; reject short-form aliases.""" + if value is None: + return None + if not isinstance(value, str): + raise TypeError( + f"attn_implementation must be a string, got {type(value).__name__}" + ) + if value in CANONICAL_ATTN_IMPLS: + return value + if "/" in value: + # Hub-kernel path, e.g. "kernels-community/flash-attn3". Pass through. + return value + if value in SHORT_FORM_ALIAS_TO_CANONICAL: + canonical = SHORT_FORM_ALIAS_TO_CANONICAL[value] + raise ValueError( + f"attn_implementation={value!r} is not accepted. " + f"Use the canonical name {canonical!r} instead." + ) + raise ValueError( + f"attn_implementation={value!r} is not a recognized backend. " + f"Expected one of: {sorted(CANONICAL_ATTN_IMPLS)}, or a hub-kernel " + f"path containing '/'." + ) + + @model_validator(mode="after") + def check_sageattn_wo_sample_packing(self): + if ( + self.attn_implementation == "sage" + and not self.sample_packing + and not self.pad_to_sequence_len + ): LOG.warning( - "We found loss to drop to 0 with SageAttention full finetuning." - "Please observe the loss, otherwise switch to LoRA/QLoRA or another attention method." + "We recommend turning on `pad_to_sequence_len` for SageAttention " + "without packing. The loss has been observed to explode otherwise." ) - return data + return self + + @model_validator(mode="after") + def check_sageattn_fft(self): + if self.attn_implementation == "sage" and not self.adapter: + LOG.warning( + "SageAttention full finetuning has been observed to drop loss to 0. " + "Monitor the loss, or switch to LoRA/QLoRA or another attention method." + ) + return self @model_validator(mode="before") @classmethod @@ -1442,17 +1569,13 @@ def check_fp8(self): ) return self - @model_validator(mode="before") - @classmethod - def check_sample_packing_w_sdpa_bf16(cls, data): - is_sm_90: bool = ( - data["capabilities"] - and data["capabilities"].get("compute_capability") == "sm_90" - ) + @model_validator(mode="after") + def check_sample_packing_w_sdpa_bf16(self): + is_sm_90 = self.capabilities and self.capabilities.compute_capability == "sm_90" if ( - data.get("sample_packing") - and data.get("sdp_attention") - and (data.get("bfloat16") or data.get("bf16")) + self.sample_packing + and self.attn_implementation == "sdpa" + and (self.bfloat16 or self.bf16) and not is_sm_90 ): # https://github.com/pytorch/pytorch/blob/1b03423526536b5f3d35bdfa95ccc6197556cf9b/test/test_transformers.py#L2440-L2450 @@ -1460,23 +1583,51 @@ def check_sample_packing_w_sdpa_bf16(cls, data): "sample_packing & torch sdpa with bf16 is unsupported may results in 0.0 loss. " "This may work on H100s." ) + return self - return data - - @model_validator(mode="before") - @classmethod - def check_compute_capability_w_sageattn(cls, data): + @model_validator(mode="after") + def check_compute_capability_w_sageattn(self): if ( - data.get("sage_attention") - and data.get("capabilities") - and data.get("capabilities").get("compute_capability") + self.attn_implementation == "sage" + and self.capabilities + and self.capabilities.compute_capability not in ["sm_80", "sm_86", "sm_89", "sm_90", "sm_120"] ): raise ValueError( "SageAttention supports compute capability between sm_80 and sm_120. " "Please use a different attention implementation." ) - return data + return self + + @model_validator(mode="after") + def check_fp8_attention_preflight(self): + """fp8 attention requires SM90+ and torch >= 2.11 (torchao >= 0.17 is pinned).""" + if self.attn_implementation != "fp8": + return self + + if self.capabilities and self.capabilities.compute_capability: + cc = self.capabilities.compute_capability + # Accept sm_90 (H100/H200), sm_100 (B100/B200), sm_120 (B300-class). + if not cc.startswith("sm_") or int(cc.split("_", 1)[1]) < 90: + raise ValueError( + f"attn_implementation=fp8 requires compute capability sm_90 or " + f"higher (Hopper+). Detected {cc!r}." + ) + + torch_version = ( + self.env_capabilities.torch_version if self.env_capabilities else None + ) + if torch_version is None: + import torch + + torch_version = str(torch.__version__).split("+", maxsplit=1)[0] + if version.parse(torch_version) < version.parse("2.11.0"): + raise ValueError( + f"attn_implementation=fp8 requires PyTorch >= 2.11.0. " + f"Detected {torch_version}." + ) + + return self @model_validator(mode="before") @classmethod @@ -1632,13 +1783,12 @@ def check_adopt_torch_version(cls, data): ) return data - @model_validator(mode="before") - @classmethod - def check_flex_torch_version(cls, data): - if (data.get("flex_attention") is not None) and (data.get("flex_attention")): - env_capabilities = data.get("env_capabilities", {}) - torch_version = env_capabilities.get("torch_version") - + @model_validator(mode="after") + def check_flex_torch_version(self): + if self.attn_implementation == "flex_attention": + torch_version = ( + self.env_capabilities.torch_version if self.env_capabilities else None + ) if torch_version is None: import torch @@ -1648,7 +1798,7 @@ def check_flex_torch_version(cls, data): raise ValueError( "Flex attention is not supported on torch version < 2.6.0" ) - return data + return self @model_validator(mode="before") @classmethod diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index d4ff27ac98..8015486450 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -97,6 +97,68 @@ class CustomSupportedOptimizers(str, Enum): flash_lion = "flash_lion" +# Accepted canonical names; hub-kernel paths (containing "/") bypass this set. +CANONICAL_ATTN_IMPLS = frozenset( + { + "eager", + "sdpa", + "flash_attention_2", + "flash_attention_3", + "flex_attention", + "xformers", + "sage", + "s2", + "fp8", + } +) + +# Legacy boolean flags → canonical attn_implementation. Priority: specific before generic. +LEGACY_ATTN_FLAG_TO_IMPL = { + "xformers_attention": "xformers", + "s2_attention": "s2", + "sage_attention": "sage", + "flex_attention": "flex_attention", + "flash_attention": "flash_attention_2", + "sdp_attention": "sdpa", + "eager_attention": "eager", +} + +# Short-form aliases rejected at validation; mapped to canonical names for error messages. +SHORT_FORM_ALIAS_TO_CANONICAL = { + "flash": "flash_attention_2", + "flex": "flex_attention", + "sdp": "sdpa", +} + +# Backends that support varlen sample packing via `position_ids`. +ATTN_IMPLS_SUPPORTING_PACKING = frozenset( + { + "flash_attention_2", + "flash_attention_3", + "flex_attention", + "xformers", + "sage", + "kernels-community/flash-attn2", + "kernels-community/flash-attn3", + "kernels-community/sage-attention", + } +) + +# Backends that require the flash_attn library for axolotl's own monkeypatches. +ATTN_IMPLS_USING_FLASH_LIB = frozenset( + { + "flash_attention_2", + "flash_attention_3", + "s2", + "kernels-community/flash-attn2", + "kernels-community/flash-attn3", + } +) + +# Backends for which embeddings stay in fp32. Everything else needs fp16/bf16. +ATTN_IMPLS_WITHOUT_DTYPE_CAST = frozenset({"eager", "sdpa"}) + + class RingAttnFunc(str, Enum): """Enum class for supported `ring-flash-attn` implementations""" diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 76b09bfdbb..ec11d9658b 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -12,7 +12,11 @@ from transformers.utils.import_utils import is_torch_npu_available from axolotl.utils.logging import get_logger -from axolotl.utils.schemas.enums import ChatTemplate, RingAttnFunc, RLType +from axolotl.utils.schemas.enums import ( + ChatTemplate, + RingAttnFunc, + RLType, +) LOG = get_logger(__name__) @@ -179,58 +183,42 @@ def check_mm_prepare(cls, data): class AttentionValidationMixin: """Validation methods related to attention mechanisms.""" - @model_validator(mode="before") - @classmethod - def check_attention_fields(cls, data): - fields = ( - "xformers_attention", - "sdp_attention", - # "s2_attention", # requires both FA and this to be enabled - "flash_attention", - "flex_attention", - "sage_attention", - ) - non_empty_count = sum(1 for field in fields if data.get(field)) - - if non_empty_count > 1: - raise ValueError(f"Only one of {', '.join(fields)} must be set") - return data - - @model_validator(mode="before") - @classmethod - def check_sample_packing_without_attention(cls, data): - if ( - data.get("sample_packing") - and not data.get("flash_attention") - and not data.get("sdp_attention") - and not data.get("flex_attention") - and not data.get("xformers_attention") - and not data.get("sage_attention") - ): - LOG.warning( - "sample_packing without flash, sdp, xformers, sage, or flex attention does not handle cross sample decontamination." - ) - return data + @model_validator(mode="after") + def check_sample_packing_without_attention(self): + if self.sample_packing and not self.attn_supports_packing: + if self.attn_implementation: + LOG.warning( + "`sample_packing` with `attn_implementation=%r` does not handle " + "cross-sample decontamination. Use a varlen-capable backend " + "(e.g. flash_attention_2, flex_attention, xformers, sage) to " + "isolate samples.", + self.attn_implementation, + ) + else: + LOG.warning( + "`sample_packing` without an attention backend does not handle " + "cross-sample decontamination. Set `attn_implementation` to a " + "varlen-capable backend (e.g. flash_attention_2)." + ) + return self - @model_validator(mode="before") - @classmethod - def check_sample_packing_with_s2attn(cls, data): - if data.get("sample_packing") and data.get("s2_attention"): + @model_validator(mode="after") + def check_sample_packing_with_s2attn(self): + if self.sample_packing and self.attn_implementation == "s2": raise ValueError( - "Received `sample_packing=true` and `s2_attention=true`; however, \ - shifted-sparse attention does not currently support sample packing." + "Received `sample_packing=true` and `attn_implementation=s2`; " + "shifted-sparse attention does not currently support sample packing." ) - return data + return self - @model_validator(mode="before") - @classmethod - def check_scaling_softmax_requires_flex(cls, data): - if data.get("scaling_softmax") and not data.get("flex_attention"): + @model_validator(mode="after") + def check_scaling_softmax_requires_flex(self): + if self.scaling_softmax and self.attn_implementation != "flex_attention": raise ValueError( - "scaling_softmax requires flex_attention: true\n" - "Add 'flex_attention: true' to your config file.\n" + "scaling_softmax requires flex attention. " + "Add `attn_implementation: flex_attention` to your config." ) - return data + return self class TrainingValidationMixin: @@ -431,7 +419,7 @@ def check_fft_possible_bad_config(self): not (self.bf16 or self.bfloat16) and (self.fp16 or self.float16) and not self.adapter - and not self.flash_attention + and not self.attn_uses_flash_lib and self.sample_packing ): LOG.warning( @@ -942,40 +930,45 @@ def check_flashoptim_deepspeed_fsdp(cls, data): ) return data - @model_validator(mode="before") - @classmethod - def check_batch_flattening_fa(cls, data): - if data.get("batch_flattening"): - batch_flattening_auto = data.get("batch_flattening") == "auto" - if not data.get("flash_attention") and not batch_flattening_auto: - raise ValueError("batch_flattening requires flash attention") - if data.get("sample_packing") and not batch_flattening_auto: - raise ValueError("batch_flattening not compatible with sample_packing") - if data.get("micro_batch_size") == 1 and not batch_flattening_auto: - LOG.warning("batch_flattening has no effect with micro_batch_size == 1") + @model_validator(mode="after") + def check_batch_flattening_fa(self): + if not self.batch_flattening: + return self - # Liger loss takes a separate code path (compute_liger_loss) that - # bypasses the flattened training forward pass. Batch flattening - # still applies to the scoring/deferred logprobs path. - trl_cfg = data.get("trl") or {} - if isinstance(trl_cfg, dict) and trl_cfg.get("use_liger_loss"): - LOG.warning( - "batch_flattening with use_liger_loss: flattening will only " - "apply to the scoring path (deferred logprobs). The training " - "forward pass uses Liger's fused lm_head+loss kernel instead." - ) + batch_flattening_auto = self.batch_flattening == "auto" + has_varlen_attn = self.attn_supports_packing - if ( - batch_flattening_auto - and data.get("flash_attention") - and not data.get("sample_packing") - and data.get("micro_batch_size") > 1 - ): - data["batch_flattening"] = True - elif batch_flattening_auto: - data["batch_flattening"] = False + if not has_varlen_attn and not batch_flattening_auto: + raise ValueError( + "batch_flattening requires a varlen-capable attention backend " + "(e.g., attn_implementation: flash_attention_2)." + ) + if self.sample_packing and not batch_flattening_auto: + raise ValueError("batch_flattening not compatible with sample_packing") + if self.micro_batch_size == 1 and not batch_flattening_auto: + LOG.warning("batch_flattening has no effect with micro_batch_size == 1") + + # Liger loss takes a separate code path (compute_liger_loss) that + # bypasses the flattened training forward pass. Batch flattening + # still applies to the scoring/deferred logprobs path. + if self.trl and getattr(self.trl, "use_liger_loss", False): + LOG.warning( + "batch_flattening with use_liger_loss: flattening will only " + "apply to the scoring path (deferred logprobs). The training " + "forward pass uses Liger's fused lm_head+loss kernel instead." + ) - return data + if ( + batch_flattening_auto + and has_varlen_attn + and not self.sample_packing + and self.micro_batch_size > 1 + ): + self.batch_flattening = True + elif batch_flattening_auto: + self.batch_flattening = False + + return self @model_validator(mode="before") @classmethod @@ -1212,6 +1205,18 @@ def check_model_quantization_config_vs_bnb(cls, data): def check_npu_config(cls, data): if is_torch_npu_available(): # check attention config + unsupported_npu_impls = { + "flash_attention_2", + "flash_attention_3", + "sdpa", + "s2", + } + attn_impl = data.get("attn_implementation") + if attn_impl and attn_impl in unsupported_npu_impls: + raise NotImplementedError( + f"attn_implementation={attn_impl!r} is currently not supported on Ascend NPU." + ) + # Legacy flags still present at this point (normalizer strips them later). attn_list = ["flash_attention", "sdp_attention", "s2_attention"] for attn in attn_list: if data.get(attn): @@ -1520,9 +1525,10 @@ def check_context_parallel_size(self): if not self.context_parallel_size: self.context_parallel_size = 1 elif self.context_parallel_size > 1: - if not self.flash_attention: + if not self.attn_uses_flash_lib: raise ValueError( - "flash_attention: true must be set with context_parallel_size > 1" + "context_parallel_size > 1 requires flash attention " + "(attn_implementation: flash or s2)." ) if self.sample_packing and self.micro_batch_size > 1: @@ -1652,47 +1658,46 @@ def check_ebft_torch_compile(cls, data): ) return data - @model_validator(mode="before") - @classmethod - def check_ebft_gradient_checkpointing_reentrant(cls, data): + @model_validator(mode="after") + def check_ebft_gradient_checkpointing_reentrant(self): """flex_attention + non-reentrant gradient checkpointing causes CheckpointError.""" if ( - data.get("rl") == "ebft" - and data.get("ebft", {}).get("mode") == "strided" - and data.get("flex_attention") - and data.get("gradient_checkpointing") + self.rl == "ebft" + and (self.ebft or {}).get("mode") == "strided" + and self.attn_implementation == "flex_attention" + and self.gradient_checkpointing ): - gc_kwargs = data.get("gradient_checkpointing_kwargs") or {} + gc_kwargs = self.gradient_checkpointing_kwargs or {} if not gc_kwargs.get("use_reentrant"): LOG.warning( "EBFT strided mode with flex_attention: setting `use_reentrant: true` in " "gradient_checkpointing_kwargs (required for flex_attention compatibility). " "Non-reentrant checkpointing causes CheckpointError with BlockMask metadata." ) - if data.get("gradient_checkpointing_kwargs") is None: - data["gradient_checkpointing_kwargs"] = {} - data["gradient_checkpointing_kwargs"]["use_reentrant"] = True - return data + if self.gradient_checkpointing_kwargs is None: + self.gradient_checkpointing_kwargs = {} + self.gradient_checkpointing_kwargs["use_reentrant"] = True + return self - @model_validator(mode="before") - @classmethod - def check_ebft_activation_offloading(cls, data): + @model_validator(mode="after") + def check_ebft_activation_offloading(self): """activation_offloading replaces gradient checkpointing with FSDP-style wrapping, which conflicts with flex_attention's use_reentrant requirement.""" if ( - data.get("rl") == "ebft" - and data.get("ebft", {}).get("mode") == "strided" - and data.get("activation_offloading") is True - and data.get("flex_attention") + self.rl == "ebft" + and (self.ebft or {}).get("mode") == "strided" + and self.activation_offloading is True + and self.attn_implementation == "flex_attention" ): raise ValueError( "EBFT strided mode: `activation_offloading: true` is incompatible with " - "`flex_attention: true`. Activation offloading replaces gradient checkpointing " - "with FSDP-style wrapping that conflicts with flex_attention's reentrant " - "checkpoint requirement. Remove `activation_offloading` — the strided trainer " - "uses micro-batched forward passes for memory efficiency instead." + "`attn_implementation: flex_attention`. Activation offloading replaces " + "gradient checkpointing with FSDP-style wrapping that conflicts with " + "flex_attention's reentrant checkpoint requirement. Remove " + "`activation_offloading` — the strided trainer uses micro-batched forward " + "passes for memory efficiency instead." ) - return data + return self @model_validator(mode="before") @classmethod diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 91982137b2..3fb940364d 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -462,7 +462,7 @@ def calculate_total_num_steps(cfg, train_dataset, update=True): f"total_num_tokens: {cfg.total_num_tokens:_}, total_num_steps: {total_num_steps:_}" ) else: - if cfg.flash_attention and not cfg.multipack_real_batches: + if cfg.attn_supports_packing and not cfg.multipack_real_batches: sampler_batch_size = 1 batch_max_len = cfg.micro_batch_size * cfg.sequence_len else: diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 1e3757dcfa..b89c935228 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -521,9 +521,9 @@ def test_fsdp2_packed( } ) if attention_backend == "flash": - cfg.flash_attention = True + cfg.attn_implementation = "flash_attention_2" elif attention_backend == "flex": - cfg.flex_attention = True + cfg.attn_implementation = "flex_attention" # write cfg to yaml file Path(temp_dir).mkdir(parents=True, exist_ok=True) diff --git a/tests/test_attn_implementation.py b/tests/test_attn_implementation.py new file mode 100644 index 0000000000..e85d713a81 --- /dev/null +++ b/tests/test_attn_implementation.py @@ -0,0 +1,418 @@ +"""Tests for attn_implementation: normalization, canonical-value acceptance, +capability flags, backend registration, and downstream validators. +""" + +import logging +from contextlib import contextmanager + +import pytest + +from axolotl.utils.config import validate_config +from axolotl.utils.dict import DictDefault +from axolotl.utils.schemas.config import AxolotlInputConfig +from axolotl.utils.schemas.enums import ( + ATTN_IMPLS_SUPPORTING_PACKING, + ATTN_IMPLS_USING_FLASH_LIB, + ATTN_IMPLS_WITHOUT_DTYPE_CAST, + CANONICAL_ATTN_IMPLS, +) + + +@contextmanager +def _capture_axolotl_warnings(caplog): + """Capture WARNINGs from `axolotl.*` loggers via caplog. + + `axolotl.cli` calls `configure_logging()` at import time, which sets + `propagate=False` on the `axolotl` logger so records do not reach the root + logger that pytest's `caplog` hooks. This helper temporarily re-enables + propagation for the duration of the block. + """ + ax_logger = logging.getLogger("axolotl") + old_propagate = ax_logger.propagate + ax_logger.propagate = True + try: + with caplog.at_level(logging.WARNING, logger="axolotl"): + yield + finally: + ax_logger.propagate = old_propagate + + +def _xformers_available(): + try: + import xformers.ops # noqa: F401 + + return True + except (ImportError, OSError): + return False + + +class TestCapabilityTables: + """Backend capability classification via frozensets and computed_field properties.""" + + @pytest.mark.parametrize( + "impl", + [ + "flash_attention_2", + "flash_attention_3", + "flex_attention", + "xformers", + "sage", + ], + ) + def test_supports_packing(self, impl): + assert impl in ATTN_IMPLS_SUPPORTING_PACKING + + @pytest.mark.parametrize("impl", ["eager", "sdpa", "s2", "fp8"]) + def test_does_not_support_packing(self, impl): + assert impl not in ATTN_IMPLS_SUPPORTING_PACKING + + @pytest.mark.parametrize("impl", ["flash_attention_2", "flash_attention_3", "s2"]) + def test_uses_flash_lib(self, impl): + assert impl in ATTN_IMPLS_USING_FLASH_LIB + + @pytest.mark.parametrize( + "impl", ["eager", "sdpa", "xformers", "flex_attention", "sage", "fp8"] + ) + def test_does_not_use_flash_lib(self, impl): + assert impl not in ATTN_IMPLS_USING_FLASH_LIB + + @pytest.mark.parametrize("impl", ["eager", "sdpa"]) + def test_no_dtype_cast(self, impl): + assert impl in ATTN_IMPLS_WITHOUT_DTYPE_CAST + + @pytest.mark.parametrize( + "impl", + [ + "flash_attention_2", + "flash_attention_3", + "flex_attention", + "xformers", + "sage", + "s2", + "fp8", + ], + ) + def test_needs_dtype_cast(self, impl): + assert impl not in ATTN_IMPLS_WITHOUT_DTYPE_CAST + + def test_known_hub_kernels_classified(self): + assert "kernels-community/flash-attn3" in ATTN_IMPLS_SUPPORTING_PACKING + assert "kernels-community/flash-attn3" in ATTN_IMPLS_USING_FLASH_LIB + assert "kernels-community/sage-attention" in ATTN_IMPLS_SUPPORTING_PACKING + + def test_computed_flags_readable_on_validated_cfg(self, min_base_cfg): + cfg = min_base_cfg | DictDefault(attn_implementation="sdpa") + validated = validate_config(cfg) + assert validated.attn_implementation == "sdpa" + assert validated.attn_supports_packing is False + assert validated.attn_uses_flash_lib is False + assert validated.attn_needs_dtype_cast is False + + def test_computed_flags_not_overridable_from_yaml(self, min_base_cfg): + """YAML attempts to override a computed field must not win.""" + cfg = min_base_cfg | DictDefault( + attn_implementation="eager", attn_uses_flash_lib=True + ) + validated = validate_config(cfg) + # The computed field reflects the backend, not the YAML input. + assert validated.attn_uses_flash_lib is False + + +class TestBackendRegistration: + """Axolotl-owned backends register under their canonical names in HF's registries.""" + + @pytest.mark.skipif(not _xformers_available(), reason="xformers not available") + def test_register_xformers(self): + from transformers.masking_utils import ALL_MASK_ATTENTION_FUNCTIONS + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + from axolotl.monkeypatch.attention import register_xformers_attn + + register_xformers_attn() + + assert "xformers" in ALL_ATTENTION_FUNCTIONS + assert "xformers" in ALL_MASK_ATTENTION_FUNCTIONS + assert ( + ALL_MASK_ATTENTION_FUNCTIONS["xformers"] + == ALL_MASK_ATTENTION_FUNCTIONS["flash_attention_2"] + ) + + def test_register_sage(self): + from transformers.masking_utils import ALL_MASK_ATTENTION_FUNCTIONS + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + from axolotl.monkeypatch.attention import register_sage_attn + + register_sage_attn() + + assert "sage" in ALL_ATTENTION_FUNCTIONS + assert "sage" in ALL_MASK_ATTENTION_FUNCTIONS + assert ( + ALL_MASK_ATTENTION_FUNCTIONS["sage"] + == ALL_MASK_ATTENTION_FUNCTIONS["flash_attention_2"] + ) + + @pytest.mark.skipif(not _xformers_available(), reason="xformers not available") + def test_xformers_does_not_overwrite_fa2(self): + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + original_fa2 = ALL_ATTENTION_FUNCTIONS["flash_attention_2"] + + from axolotl.monkeypatch.attention import register_xformers_attn + + register_xformers_attn() + + assert ALL_ATTENTION_FUNCTIONS["flash_attention_2"] is original_fa2 + + def test_sage_does_not_overwrite_fa2(self): + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + original_fa2 = ALL_ATTENTION_FUNCTIONS["flash_attention_2"] + + from axolotl.monkeypatch.attention import register_sage_attn + + register_sage_attn() + + assert ALL_ATTENTION_FUNCTIONS["flash_attention_2"] is original_fa2 + + +class TestLegacyFlagDeprecation: + """Legacy boolean flags (flash_attention, sdp_attention, ...) map to a + canonical attn_implementation value, are stripped from the validated + config, and cannot be combined with an explicit canonical value. + """ + + @staticmethod + def _normalize(data): + return AxolotlInputConfig.normalize_attn_implementation(data) + + @pytest.mark.parametrize( + "flag,expected", + [ + ("flash_attention", "flash_attention_2"), + ("sdp_attention", "sdpa"), + ("xformers_attention", "xformers"), + ("flex_attention", "flex_attention"), + ("sage_attention", "sage"), + ("eager_attention", "eager"), + ("s2_attention", "s2"), + ], + ) + def test_legacy_flag_maps_to_canonical(self, flag, expected): + result = self._normalize({flag: True}) + assert result["attn_implementation"] == expected + + def test_legacy_flags_are_stripped_after_mapping(self): + result = self._normalize({"flash_attention": True}) + for flag in [ + "flash_attention", + "sdp_attention", + "xformers_attention", + "flex_attention", + "sage_attention", + "eager_attention", + "s2_attention", + ]: + assert flag not in result + + def test_s2_plus_flash_priority_is_s2(self): + result = self._normalize({"s2_attention": True, "flash_attention": True}) + assert result["attn_implementation"] == "s2" + + def test_sage_plus_flash_priority_is_sage(self): + result = self._normalize({"sage_attention": True, "flash_attention": True}) + assert result["attn_implementation"] == "sage" + + def test_canonical_plus_legacy_flag_raises(self): + with pytest.raises(ValueError, match="cannot be combined with legacy"): + self._normalize( + {"attn_implementation": "flash_attention_2", "flash_attention": True} + ) + + def test_canonical_plus_unrelated_legacy_flag_raises(self): + with pytest.raises(ValueError, match="cannot be combined with legacy"): + self._normalize( + {"attn_implementation": "xformers", "flash_attention": True} + ) + + def test_legacy_flag_stripped_on_validated_cfg(self, min_base_cfg): + cfg = min_base_cfg | DictDefault(flash_attention=True) + validated = validate_config(cfg) + assert validated.attn_implementation == "flash_attention_2" + # Legacy flag must not survive to the validated DictDefault + # (normalizer pops it, model_dump excludes Nones). + assert "flash_attention" not in dict(validated) + + def test_canonical_plus_legacy_rejected_on_full_validation(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + attn_implementation="flash_attention_2", flash_attention=True + ) + with pytest.raises(ValueError, match="cannot be combined with legacy"): + validate_config(cfg) + + def test_s2_plus_flash_maps_to_s2_on_full_validation(self, min_base_cfg): + """Priority resolution applies through the full validator chain too.""" + cfg = min_base_cfg | DictDefault(s2_attention=True, flash_attention=True) + validated = validate_config(cfg) + assert validated.attn_implementation == "s2" + + +class TestCanonicalValueAcceptance: + """`attn_implementation` accepts canonical names and `org/name` hub-kernel + paths. Short-form aliases (`flash`, `flex`, `sdp`) and unknown bare names + are rejected. Absent input is a noop. + """ + + @staticmethod + def _normalize(data): + return AxolotlInputConfig.normalize_attn_implementation(data) + + def test_canonical_value_is_passthrough(self): + data = {"attn_implementation": "flash_attention_2"} + result = self._normalize(data) + assert result["attn_implementation"] == "flash_attention_2" + + def test_hub_kernel_is_passthrough(self): + data = {"attn_implementation": "kernels-community/flash-attn3"} + result = self._normalize(data) + assert result["attn_implementation"] == "kernels-community/flash-attn3" + + def test_no_attention_set_is_noop(self): + result = self._normalize({"some_other_config": True}) + assert result.get("attn_implementation") is None + + def test_field_validator_accepts_all_canonical(self): + for impl in CANONICAL_ATTN_IMPLS: + assert AxolotlInputConfig.validate_attn_implementation(impl) == impl + + def test_field_validator_accepts_hub_kernels(self): + for impl in ( + "kernels-community/flash-attn3", + "kernels-community/sage-attention", + "someorg/custom-kernel", + ): + assert AxolotlInputConfig.validate_attn_implementation(impl) == impl + + def test_field_validator_accepts_none(self): + assert AxolotlInputConfig.validate_attn_implementation(None) is None + + @pytest.mark.parametrize("alias", ["flash", "flex", "sdp"]) + def test_short_form_alias_rejected(self, alias): + with pytest.raises(ValueError, match="is not accepted"): + AxolotlInputConfig.validate_attn_implementation(alias) + + def test_unknown_bare_name_rejected(self): + with pytest.raises(ValueError, match="not a recognized backend"): + AxolotlInputConfig.validate_attn_implementation("not_a_real_backend") + + def test_canonical_value_passes_through_full_validation(self, min_base_cfg): + cfg = min_base_cfg | DictDefault(attn_implementation="flash_attention_3") + validated = validate_config(cfg) + assert validated.attn_implementation == "flash_attention_3" + assert validated.attn_uses_flash_lib is True + assert validated.attn_supports_packing is True + + def test_hub_kernel_passes_through_full_validation(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + attn_implementation="kernels-community/flash-attn3" + ) + validated = validate_config(cfg) + assert validated.attn_implementation == "kernels-community/flash-attn3" + assert validated.attn_uses_flash_lib is True + assert validated.attn_supports_packing is True + + def test_short_form_alias_rejected_on_full_validation(self, min_base_cfg): + cfg = min_base_cfg | DictDefault(attn_implementation="flash") + with pytest.raises(ValueError, match="is not accepted"): + validate_config(cfg) + + +class TestGemma4HybridMode: + """`gemma4_hybrid_attn_impl` pins `attn_implementation` to `flash_attention_2`.""" + + @staticmethod + def _normalize(data): + return AxolotlInputConfig.normalize_attn_implementation(data) + + def test_defaults_to_flash_attention_2(self): + result = self._normalize({"gemma4_hybrid_attn_impl": True}) + assert result["attn_implementation"] == "flash_attention_2" + + def test_explicit_fa2_passes(self): + result = self._normalize( + { + "gemma4_hybrid_attn_impl": True, + "attn_implementation": "flash_attention_2", + } + ) + assert result["attn_implementation"] == "flash_attention_2" + + def test_non_fa2_raises(self): + with pytest.raises( + ValueError, match="requires attn_implementation=flash_attention_2" + ): + self._normalize( + {"gemma4_hybrid_attn_impl": True, "attn_implementation": "sdpa"} + ) + + +class TestSamplePackingValidation: + """`sample_packing` warns for non-varlen backends; s2 raises outright.""" + + def test_eager_warns(self, min_base_cfg, caplog): + cfg = min_base_cfg | DictDefault( + attn_implementation="eager", sample_packing=True + ) + with _capture_axolotl_warnings(caplog): + validate_config(cfg) + assert any( + "does not handle cross-sample decontamination" in r.getMessage() + for r in caplog.records + ) + + def test_sdpa_warns(self, min_base_cfg, caplog): + cfg = min_base_cfg | DictDefault( + attn_implementation="sdpa", sample_packing=True + ) + with _capture_axolotl_warnings(caplog): + validate_config(cfg) + assert any( + "does not handle cross-sample decontamination" in r.getMessage() + for r in caplog.records + ) + + def test_flash_attention_2_does_not_warn(self, min_base_cfg, caplog): + cfg = min_base_cfg | DictDefault( + attn_implementation="flash_attention_2", sample_packing=True + ) + with _capture_axolotl_warnings(caplog): + validate_config(cfg) + assert not any( + "does not handle cross-sample decontamination" in r.getMessage() + for r in caplog.records + ) + + def test_s2_raises(self, min_base_cfg): + cfg = min_base_cfg | DictDefault(attn_implementation="s2", sample_packing=True) + with pytest.raises( + ValueError, match="shifted-sparse attention does not currently support" + ): + validate_config(cfg) + + +class TestScalingSoftmaxValidation: + """`scaling_softmax` is only implemented under flex_attention.""" + + def test_non_flex_raises(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + attn_implementation="flash_attention_2", scaling_softmax=True + ) + with pytest.raises(ValueError, match="scaling_softmax requires flex"): + validate_config(cfg) + + def test_flex_passes(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + attn_implementation="flex_attention", scaling_softmax=True + ) + validated = validate_config(cfg) + assert validated.attn_implementation == "flex_attention" diff --git a/tests/test_mm_chat_collator.py b/tests/test_mm_chat_collator.py new file mode 100644 index 0000000000..d26a011a77 --- /dev/null +++ b/tests/test_mm_chat_collator.py @@ -0,0 +1,163 @@ +""" +Regression tests for MultiModalChatDataCollator shape contracts. + +Guard against the transformers 5.x breakage where apply_chat_template's +own `return_dict` parameter (default False) caused it to return the raw +input_ids tensor instead of the full BatchFeature dict, leading to + IndexError: too many indices for tensor of dimension 2 +when downstream code did batch["input_ids"] on the resulting tensor. +""" + +from unittest.mock import MagicMock, patch + +import pytest +import torch +from transformers import BatchFeature + + +@pytest.fixture(name="mock_processor") +def fixture_mock_processor(): + """ + A mock processor whose apply_chat_template returns a BatchFeature + when called with return_dict=True (the correct call convention), + or a raw input_ids tensor when called without return_dict=True + (the broken call convention that the bug introduced). + """ + processor = MagicMock() + processor.tokenizer = MagicMock() + processor.tokenizer.pad_token_id = 0 + processor.image_token = "<|image|>" + processor.tokenizer.convert_tokens_to_ids = MagicMock(return_value=128256) + + batch_size, seq_len = 2, 16 + input_ids = torch.ones(batch_size, seq_len, dtype=torch.long) + attention_mask = torch.ones(batch_size, seq_len, dtype=torch.long) + + batch_feature = BatchFeature( + data={ + "input_ids": input_ids, + "attention_mask": attention_mask, + } + ) + + def _apply_chat_template(*args, **kwargs): + if kwargs.get("return_dict", False): + return batch_feature + # Simulate transformers 5.x default behaviour: returns out["input_ids"] + return input_ids + + processor.apply_chat_template = MagicMock(side_effect=_apply_chat_template) + processor.chat_template = None + return processor + + +@pytest.fixture(name="mock_processing_strategy") +def fixture_mock_processing_strategy(mock_processor): + from axolotl.processing_strategies import ProcessingStrategy + + strategy = ProcessingStrategy(processor=mock_processor) + return strategy + + +class TestMultiModalChatDataCollatorShapeContract: + """ + Verify that MultiModalChatDataCollator.process_rows returns a dict with + 2-D input_ids and labels, not a raw tensor. This is the shape contract + that process_labels depends on. + """ + + def _make_collator(self, mock_processing_strategy): + from axolotl.utils.collators.mm_chat import MultiModalChatDataCollator + + tokenizer = mock_processing_strategy.processor.tokenizer + return MultiModalChatDataCollator( + tokenizer=tokenizer, + processing_strategy=mock_processing_strategy, + ) + + def _make_examples(self): + return [ + { + "messages": [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there"}, + ] + } + ] + + def test_process_rows_returns_dict(self, mock_processing_strategy): + """batch must be a dict, not a raw tensor.""" + collator = self._make_collator(mock_processing_strategy) + examples = self._make_examples() + + with patch.object( + mock_processing_strategy, + "__call__", + return_value=examples, + ): + batch = collator.process_rows(examples) + + assert isinstance(batch, dict), ( + "process_rows must return a dict (BatchFeature), not a raw tensor. " + "If it returns a tensor, apply_chat_template was called without " + "return_dict=True at the top level." + ) + + def test_process_rows_input_ids_shape(self, mock_processing_strategy): + """batch['input_ids'] must be a 2-D tensor (batch, seq_len).""" + collator = self._make_collator(mock_processing_strategy) + examples = self._make_examples() + + with patch.object( + mock_processing_strategy, + "__call__", + return_value=examples, + ): + batch = collator.process_rows(examples) + + assert "input_ids" in batch + assert isinstance(batch["input_ids"], torch.Tensor) + assert batch["input_ids"].ndim == 2, ( + f"input_ids must be 2-D (batch, seq_len), got shape {batch['input_ids'].shape}" + ) + + def test_process_rows_labels_shape(self, mock_processing_strategy): + """batch['labels'] must be a 2-D tensor matching input_ids shape.""" + collator = self._make_collator(mock_processing_strategy) + examples = self._make_examples() + + with patch.object( + mock_processing_strategy, + "__call__", + return_value=examples, + ): + batch = collator.process_rows(examples) + + assert "labels" in batch + assert isinstance(batch["labels"], torch.Tensor) + assert batch["labels"].ndim == 2 + assert batch["labels"].shape == batch["input_ids"].shape + + def test_apply_chat_template_called_with_return_dict_true( + self, mock_processing_strategy + ): + """apply_chat_template must be called with return_dict=True as a keyword arg.""" + collator = self._make_collator(mock_processing_strategy) + examples = self._make_examples() + + with patch.object( + mock_processing_strategy, + "__call__", + return_value=examples, + ): + collator.process_rows(examples) + + call_kwargs = ( + mock_processing_strategy.processor.apply_chat_template.call_args.kwargs + ) + assert call_kwargs.get("return_dict") is True, ( + "apply_chat_template must be called with return_dict=True as a top-level " + "keyword argument (not inside processor_kwargs). In transformers 5.x, " + "apply_chat_template has its own return_dict param (default False) that " + "controls whether it returns the full BatchFeature or just input_ids." + ) diff --git a/tests/test_no_legacy_attn_reads.py b/tests/test_no_legacy_attn_reads.py new file mode 100644 index 0000000000..2435f9fa8c --- /dev/null +++ b/tests/test_no_legacy_attn_reads.py @@ -0,0 +1,62 @@ +"""Enforce attn_implementation as the single source of truth. + +Fails if src/ contains a cfg._attention read. Migrate offending sites +to cfg.attn_implementation or the attn_supports_packing/attn_uses_flash_lib/ +attn_needs_dtype_cast computed flags. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +LEGACY_FLAGS = ( + "flash_attention", + "sdp_attention", + "xformers_attention", + "flex_attention", + "sage_attention", + "s2_attention", + "eager_attention", +) + +# The normalizer is allowed to read the legacy keys (that's its job). +# lm_eval/cli.py is a raw-YAML entry point (bypasses AxolotlInputConfig) that +# honors both forms during the deprecation period — when we remove the legacy +# flags entirely, drop this allowlist entry and the BC branch in that file. +ALLOWED_FILES = { + Path("src/axolotl/utils/schemas/config.py"), + Path("src/axolotl/integrations/lm_eval/cli.py"), +} + +# `cfg.`, `self.cfg.`, `data.get("")`, `data[""]` +_PATTERNS = [re.compile(rf"\bcfg\.{flag}\b") for flag in LEGACY_FLAGS] + [ + re.compile(rf'\bdata\.get\("{flag}"\)') for flag in LEGACY_FLAGS +] + + +def _repo_root() -> Path: + return Path(__file__).resolve().parent.parent + + +def test_no_legacy_attn_reads_in_src(): + root = _repo_root() + src = root / "src" + offenders: list[str] = [] + + for py_file in src.rglob("*.py"): + rel = py_file.relative_to(root) + if rel in ALLOWED_FILES: + continue + text = py_file.read_text(encoding="utf-8") + for pattern in _PATTERNS: + for match in pattern.finditer(text): + # Line number for the user's convenience. + line_no = text.count("\n", 0, match.start()) + 1 + offenders.append(f"{rel}:{line_no} {match.group(0)}") + + assert not offenders, ( + "Found legacy attention-flag reads in src/. Migrate to " + "`cfg.attn_implementation` / capability flags:\n " + + "\n ".join(sorted(offenders)) + ) From c15f6cffe2c3006a54baaea9b17a98a532445cb2 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Tue, 5 May 2026 20:52:35 +0530 Subject: [PATCH 1290/1405] fix: FSDP FULL_STATE_DICT oom from memory leak (#3635) * memory clean up for fsdp full state dict * Update src/axolotl/monkeypatch/accelerate/fsdp2.py Co-authored-by: Wing Lian --------- Co-authored-by: Wing Lian --- src/axolotl/core/trainers/base.py | 10 +++++++++- src/axolotl/monkeypatch/accelerate/fsdp2.py | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index d2f2e414c4..2e4252123f 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -2,6 +2,7 @@ from __future__ import annotations +import gc import json import math import os @@ -800,7 +801,14 @@ def _save_checkpoint(self, model, trial, **kwargs): with open(tokens_state_path, "w", encoding="utf-8") as f: json.dump(tokens_state, f) - return super()._save_checkpoint(model, trial, **kwargs) + result = super()._save_checkpoint(model, trial, **kwargs) + + # Reclaim VRAM held by the FSDP full-state-dict gather. + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + return result # TODO(wing): remove once https://github.com/huggingface/transformers/pull/39866/files is merged def _save(self, output_dir: Optional[str] = None, state_dict=None): diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index e086885bb4..20c863ee5e 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -4,6 +4,7 @@ import copy import functools +import gc import os import sys @@ -161,6 +162,7 @@ def get_state_dict(self, model, unwrap=True): state_dict = {} sharded_state_dict = model.state_dict() + is_rank_zero = torch.distributed.get_rank() == 0 for param_name, param in sharded_state_dict.items(): if param.is_cpu: param = param.to(torch.device("cuda")) @@ -168,9 +170,20 @@ def get_state_dict(self, model, unwrap=True): if isinstance(param, DTensor): param = param.full_tensor() - if torch.distributed.get_rank() == 0: + if is_rank_zero: state_dict[param_name] = param.cpu() + # Drop the GPU-resident gathered tensor before the next iteration + # allocates the next one; otherwise the caching allocator holds + # both reservations and we accumulate ~model-size of VRAM. + del param torch.distributed.barrier() + + # Release the sharded view and force the allocator to give back the + # gather buffers. + del sharded_state_dict + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() elif self.distributed_type == DistributedType.FSDP: from torch.distributed.fsdp import ( FullStateDictConfig, From 5352d41d32f4245bbb8b0140990ac0f6b77f1ab1 Mon Sep 17 00:00:00 2001 From: thad0ctor Date: Tue, 5 May 2026 08:25:39 -0700 Subject: [PATCH 1291/1405] feat: systemic multimodal assistant-only loss masking + cfg.role_boundaries` (#3625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: systemic multimodal assistant-only loss masking + cfg.role_boundaries Fixes silent ignoring of `cfg.train_on_inputs` / `cfg.roles_to_train` / `cfg.train_on_eos` in the multimodal training path. Before this branch, only Gemma 3n honored these knobs; every other VLM trained on the full sequence regardless of config. Also adds `cfg.role_boundaries` YAML override so users can declare per-role markers without subclassing. What changed ------------ - `ProcessingStrategy` gains a declarative boundary scanner. Each strategy declares per-role start/end markers via `_build_role_boundaries`; the shared scanner honors `train_on_inputs` / `roles_to_train` / `train_on_eos` (incl. "last"). - New per-template strategies: Gemma 4, Llama 3.2 Vision, Llama 4, Pixtral, Mistral V7 Tekken. - Refactored: Gemma 3 (previously no role masking), Gemma 3n (previously ad-hoc scanner, now shared). - Strategies whose boundary tokens couldn't be verified offline (Voxtral, SmolVLM2, Mistral3, InternVL, GLM4V, llava/lfm2vl fallback) retain legacy behavior and emit a one-shot warning. Users can enable masking on them via `cfg.role_boundaries`. - Pixtral / Mistral V7 Tekken correctly handle the shared `[/INST]` token between user-end and assistant-start via `include_end=False` + scanner rewind. See `docs/multimodal_assistant_mask.md` for the full audit table, root-cause analysis, and design rationale. Co-Authored-By: Claude Opus 4.7 (1M context) * feat: systemic multimodal assistant-only loss masking + cfg.role_boundaries Fixes silent ignoring of `cfg.train_on_inputs` / `cfg.roles_to_train` / `cfg.train_on_eos` in the multimodal training path. Before this branch, only Gemma 3n honored these knobs; every other VLM trained on the full sequence regardless of config. Also adds `cfg.role_boundaries` YAML override so users can declare per-role markers without subclassing. What changed ------------ - `ProcessingStrategy` gains a declarative boundary scanner. Each strategy declares per-role start/end markers via `_build_role_boundaries`; the shared scanner honors `train_on_inputs` / `roles_to_train` / `train_on_eos` (incl. "last"). - New per-template strategies: Gemma 4, Llama 3.2 Vision, Llama 4, Pixtral, Mistral V7 Tekken. - Refactored: Gemma 3 (previously no role masking), Gemma 3n (previously ad-hoc scanner, now shared). - Strategies whose boundary tokens couldn't be verified offline (Voxtral, SmolVLM2, Mistral3, InternVL, GLM4V, llava/lfm2vl fallback) retain legacy behavior and emit a one-shot warning. Users can enable masking on them via `cfg.role_boundaries`. - Pixtral / Mistral V7 Tekken correctly handle the shared `[/INST]` token between user-end and assistant-start via `include_end=False` + scanner rewind. See `docs/multimodal_assistant_mask.md` for the full audit table, root-cause analysis, and design rationale. Co-Authored-By: Claude Opus 4.7 (1M context) * docs+types: address CodeRabbit nitpicks on PR #7 - builders/causal.py: add inline NOTE that multi-dataset configs reuse the first dataset's masking knobs (roles_to_train / train_on_eos) for all datasets — heterogeneous per-dataset overrides are not supported in the MM path today. - processing_strategies.py: annotate inner scanner helpers _match_prefix and _find_end with explicit types (Tensor, int, list[int] → bool / tuple[int, bool]) for readability. - docs/multimodal_assistant_mask.md: renumber the "Commits on this branch" list to 1-7 consecutive (previously skipped 3). Co-Authored-By: Claude Opus 4.7 (1M context) * fix(mm-mask): address two CodeRabbit findings on PR #7 1. Schema rejected `train_on_eos: "none"` despite the scanner honoring it. `_VALID_TRAIN_ON_EOS` accepts "none" and the design doc lists it, but `SFTDataset.train_on_eos` was `Literal["all", "turn", "last"]`, so YAML users hit a pydantic ValidationError at config load. Added "none" to the Literal and updated the description. 2. `cfg.role_boundaries: []` had split-personality semantics: the strategy ctor treated it as "replace built-ins with empty" while the collator plumbing treated it as "unset", and both the design doc and the MultiModalConfig schema help text promised wholesale replacement for any set value. Aligned on opt-in semantics across all four surfaces — a non-empty list replaces built-ins wholesale; unset or `[]` falls back to built-ins. Rationale: honoring `[]` literally yields all-masked labels and zero gradient, which is almost always a typo or leftover rather than a deliberate user action. Users who want to disable role masking should unset the field or use `train_on_inputs: true`. Also sharpened the fallback one-shot warning for strategies without built-in boundaries: names the consequence ("only pad and media tokens are masked, every other token contributes to loss") and points users at `cfg.role_boundaries` + docs/multimodal_assistant_mask.md instead of "see axolotl/processing_strategies.py for how to declare boundaries." Files: - src/axolotl/utils/schemas/datasets.py: Literal adds "none" - src/axolotl/processing_strategies.py: ctor truthiness check on role_boundaries_override; sharpened fallback warning - src/axolotl/utils/schemas/multimodal.py: role_boundaries description now calls out opt-in + empty-list fallback semantics - docs/multimodal_assistant_mask.md: same clarification in the Semantics block; updated the fallback-path detection paragraph to quote the new warning text - tests/test_processing_strategies.py: +2 regressions (test_sft_dataset_schema_accepts_all_supported_train_on_eos_values, test_empty_role_boundaries_override_falls_back_to_builtin); 63/63 pass Co-Authored-By: Claude Opus 4.7 (1M context) * doc cleanup * fix(mm-mask): CodeRabbit findings + lint fix on PR #3625 Pre-commit failure: trailing newline missing on docs/multimodal_assistant_mask.md (end-of-file-fixer hook). Six CodeRabbit findings addressed: 1. Scanner: non-trainable role's end marker ignored ``include_end``. Under ``train_on_eos="all"``, the shared ``[/INST]`` token (user-end with ``include_end=False``, intentionally re-matched as assistant-start) leaked into loss via the user branch on Pixtral / Mistral V7 Tekken. Fix: gate the non-trainable branch on ``best_match.include_end`` to mirror the trainable branch. 2. Gemma3 ``boi_token`` lookup used ``tokenizer.special_tokens_map.get("boi_token")``, which never fires on real checkpoints (``special_tokens_map`` only holds HF's standard slots — bos/eos/pad/unk/...). Swap to direct attribute read ``getattr(tokenizer, "boi_token", None)``, matching what ``transformers.models.gemma3.processing_gemma3`` itself does. Updated the ``_gemma_tokenizer`` test fixture to mirror real-model shape so the test exercises the production code path. 3. GLM dispatcher only registered ``Glm46VProcessor`` (GLM-4.6V / GLM-4.7V). Real ``Glm4vProcessor`` (GLM-4V / GLM-4.1V) users fell through to the base fallback. Both processors ship identical media-token markers, so register both under the shared ``Glm4vProcessingStrategy`` with independent try/except import blocks. Updated class docstring. +2 dispatcher regressions. 4. Gemma3 ``process_labels`` hardcoded 262144 for the soft image token. Resolve dynamically via ``tokenizer.convert_tokens_to_ids("")`` with unk-id guard; fall back to 262144 only if the string isn't in vocab. Mirrors ``Gemma4ProcessingStrategy.process_labels`` pattern. 5. ``build_collator`` was called twice per ``build()`` (eval + train passes), producing two identical ``MM collator: ...`` INFO banners on startup. Gate the log on ``is_eval=False`` so only the training pass emits it. 6. Removed unused ``_mistral_common_stub`` pytest fixture (13 refs → 0, always returned ``None``; the dispatcher already handles missing ``mistral_common`` via lazy import + ``try/except``). Added ``test_scanner_train_on_eos_all_with_non_trainable_include_end_false`` — a focused scanner-level lock-in for finding #1, independent of any specific VLM strategy. Test count: 63 → 68 passing. Local ``pre-commit run --all-files`` green. Co-Authored-By: Claude Opus 4.7 (1M context) * chore(mm-mask): hoist .tolist() out of scanner; shorten comments/docstrings - Scanner perf: convert labels[i] to a Python list once per row so _match_prefix / _find_end operate on list slices instead of re-materializing Tensor slices via .tolist() on every probe. Cuts O(n*boundaries) CPython↔C boundary crossings per batch. - Markdown lint (MD001, MD040): promote two h3 section headings to h2 under the h1; add `text` language to the verify-at-runtime fenced block. - Shorten verbose comments/docstrings added in recent commits to bare-minimum "why" notes matching the repo's existing style. 68/68 tests, 8/8 pre-commit hooks still pass. --- docs/multimodal_assistant_mask.md | 84 ++ src/axolotl/core/builders/causal.py | 41 + src/axolotl/processing_strategies.py | 1029 +++++++++++++++----- src/axolotl/utils/schemas/datasets.py | 4 +- src/axolotl/utils/schemas/multimodal.py | 62 ++ tests/test_processing_strategies.py | 1164 +++++++++++++++++++++++ 6 files changed, 2155 insertions(+), 229 deletions(-) create mode 100644 docs/multimodal_assistant_mask.md create mode 100644 tests/test_processing_strategies.py diff --git a/docs/multimodal_assistant_mask.md b/docs/multimodal_assistant_mask.md new file mode 100644 index 0000000000..339ab420f8 --- /dev/null +++ b/docs/multimodal_assistant_mask.md @@ -0,0 +1,84 @@ +# Multimodal assistant-only loss masking + +## Correct placement + +```yaml +# Top-level: only train_on_inputs lives here. +train_on_inputs: false + +datasets: + - path: data/train.jsonl + type: chat_template + roles_to_train: # per-dataset — this is what the MM scanner reads + - assistant + train_on_eos: turn # per-dataset — same + +test_datasets: + - path: data/val.jsonl + type: chat_template + split: train + roles_to_train: + - assistant + train_on_eos: turn +``` + +## How to verify at runtime + +`build_collator` logs the resolved knobs at INFO: + +```text +MM collator: train_on_inputs=False roles_to_train=['assistant'] train_on_eos=turn role_boundaries_override=none +``` + +If `roles_to_train` logs as `None`, the YAML knobs are not reaching the +scanner — check that they are under `datasets[0]`, not at the root. + +Each verified strategy additionally logs its resolved boundary token ids at +strategy init (e.g. `<|turn>model` → `[105, 4368]`, `` → `[106]` for +Gemma 4). If a strategy emits the "has no built-in role boundaries ... only +pad and media tokens are masked" one-shot warning instead, it is on the +fallback path — declare per-role markers in YAML via `cfg.role_boundaries` +(below) to activate masking. The strategies currently on this path are +listed in the audit table above under `fallback + warn`. + +## Config-based override: `cfg.role_boundaries` + +For the "unverified" strategies above, or for custom chat templates that +don't match a built-in strategy's markers, users can declare role boundaries +directly in YAML without subclassing: + +```yaml +role_boundaries: + - role: assistant + start: "<|turn>model" + end: "" + - role: user + start: "<|turn>user" + end: "" + # Optional keys: + # include_start: false # default False + # include_end: true # default True, respects cfg.train_on_eos + # end: eos_token # sentinel: resolves to tokenizer.eos_token_id + # end: null # span runs to end of sequence +``` + +Semantics: + +- `start` and `end` are literal strings; axolotl encodes them at strategy + init via `tokenizer.encode(..., add_special_tokens=False)` and logs the + resolved token-id sequences at INFO level. +- The special value `end: eos_token` is the portable way to express + "Pixtral-style assistant turns end at EOS" without hard-coding an id. +- `role_boundaries` is an **opt-in override**. A non-empty list **replaces** + the strategy's built-in declarations wholesale (partial overlays are + intentionally unsupported — they're hard to reason about at review time). + Leaving the field unset *or* setting it to an empty list (`[]`) both mean + "use the strategy's built-ins." Writing `role_boundaries: []` is almost + always a typo or leftover — honoring it literally would produce all-masked + labels and zero gradient, so it is treated the same as unset. +- `cfg.roles_to_train` still governs which declared roles contribute to + loss. You can declare `user` and `assistant` boundaries and set + `roles_to_train: ["assistant"]` to have the scanner correctly identify + user spans as masking boundaries without training on their content. +- Invalid specs fail loudly at strategy init (missing `role`/`start`, + unencodable markers), not silently at loss-compute time. diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 15624173d6..aa1678523a 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -515,12 +515,53 @@ def build_collator( else: if self.cfg.processor_type and self.processor: collator = MultiModalChatDataCollator + # Mirror ChatTemplateStrategy: per-dataset masking knobs from first MM dataset, else global cfg. + # NOTE: Multi-dataset configs use the first dataset's masking knobs for all datasets; + # heterogeneous per-dataset overrides are not supported in the MM path today. + ds_entries = self.cfg.datasets or [] + ds_cfg = ds_entries[0] if ds_entries else None + + def _ds_get(cfg_obj, key): + # Handle DictDefault / dict / pydantic uniformly: + # dict-style .get first, then attribute access. + if cfg_obj is None: + return None + if hasattr(cfg_obj, "get"): + try: + return cfg_obj.get(key) + except (AttributeError, KeyError, TypeError): + pass + return getattr(cfg_obj, key, None) + + roles_to_train = _ds_get(ds_cfg, "roles_to_train") + train_on_eos = _ds_get(ds_cfg, "train_on_eos") + + # cfg.role_boundaries replaces the strategy's built-in markers. + role_boundaries_override = None + if self.cfg.role_boundaries: + role_boundaries_override = list(self.cfg.role_boundaries) + + # build() calls build_collator twice (eval + train); log once. + if not is_eval: + LOG.info( + "MM collator: train_on_inputs=%s roles_to_train=%s " + "train_on_eos=%s role_boundaries_override=%s", + bool(self.cfg.train_on_inputs), + roles_to_train, + train_on_eos, + "set" if role_boundaries_override else "none", + ) + kwargs["processing_strategy"] = get_processing_strategy( self.processor, training_args.chat_template, self.cfg.chat_template, image_size=training_args.image_size, image_resize_algorithm=training_args.image_resize_algorithm, + train_on_inputs=bool(self.cfg.train_on_inputs), + roles_to_train=roles_to_train, + train_on_eos=train_on_eos, + role_boundaries_override=role_boundaries_override, ) elif self.cfg.batch_flattening: collator = DataCollatorWithFlattening diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index cb1f9d984b..217bc765b5 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -1,6 +1,7 @@ """Module containing ProcessingStrategy classes and its derivative for different MultiModal Model types""" from copy import deepcopy +from dataclasses import dataclass, field from typing import Optional from PIL import Image, ImageOps @@ -17,9 +18,35 @@ LOG = get_logger(__name__) +# One-shot warning dedupe so opt-out subclasses don't spam per-batch. +_ROLE_MASK_WARNED: set[str] = set() + +# Supported values for ``train_on_eos`` — mirrors the text-only +# ChatTemplateStrategy (``turn`` = trainable turn ends only, ``all`` = every +# turn end, ``none`` = never, ``last`` = only the final trainable turn end). +_VALID_TRAIN_ON_EOS = ("turn", "all", "none", "last") + + +@dataclass(frozen=True) +class RoleBoundary: + """One role's token-level span markers for the masking scanner. + + Empty ``end_tokens`` means end-of-sequence terminates the span. + """ + + role: str + start_tokens: list[int] + end_tokens: list[int] = field(default_factory=list) + include_start: bool = False + include_end: bool = True + class ProcessingStrategy: - """Base Processing Strategy class""" + """Base Processing Strategy class. + + Subclasses opt in to role masking by overriding ``_build_role_boundaries``; + otherwise only pad + media tokens are masked (legacy behavior, one-shot warned). + """ def __init__( self, @@ -27,6 +54,10 @@ def __init__( chat_template: Optional[str] = None, image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, + train_on_inputs: bool = False, + roles_to_train: Optional[list[str]] = None, + train_on_eos: Optional[str] = None, + role_boundaries_override: Optional[list[dict]] = None, ): self.processor = processor self.chat_template = chat_template @@ -38,54 +69,97 @@ def __init__( image_resize_algorithm or Image.Resampling.BILINEAR ) + # Defaults mirror the text-only ChatTemplateStrategy. An explicit + # empty list is honored as "no trainable roles" (masks everything); + # only ``None`` falls back to the default of assistant-only. + self.train_on_inputs = bool(train_on_inputs) + self.roles_to_train = ( + list(roles_to_train) if roles_to_train is not None else ["assistant"] + ) + self.train_on_eos = train_on_eos if train_on_eos is not None else "turn" + if self.train_on_eos not in _VALID_TRAIN_ON_EOS: + raise ValueError( + f"train_on_eos={self.train_on_eos!r} is not one of " + f"{_VALID_TRAIN_ON_EOS}." + ) + if hasattr(processor, "image_token"): self.image_token = processor.image_token self.image_token_id = processor.tokenizer.convert_tokens_to_ids( self.image_token ) + built_in = self._build_role_boundaries() + + # Truthiness check: empty list == unset (opt-in escape hatch), so + # `role_boundaries: []` in YAML falls through to built-ins instead of + # producing all-masked labels. + if role_boundaries_override: + overridden = _resolve_role_boundary_override( + role_boundaries_override, self.processor.tokenizer + ) + LOG.info( + "%s: overriding built-in role boundaries (%d decls) " + "with cfg.role_boundaries (%d decls).", + type(self).__name__, + len(built_in), + len(overridden), + ) + self.role_boundaries: list[RoleBoundary] = overridden + source = "override" + else: + self.role_boundaries = built_in + source = "built-in" + + # Single-line, grep-friendly summary of the resolved masking config so + # "why isn't masking firing?" is visible in training logs. For + # overrides we include the fully resolved (role, start_ids, end_ids) + # tuples; for built-ins we log a count (subclasses vary and logging + # every id sequence would be noisy on, e.g., Llama3 with five roles). + boundaries_repr: str | list[tuple[str, list[int], list[int]]] + if source == "override": + boundaries_repr = [ + (b.role, b.start_tokens, b.end_tokens) for b in self.role_boundaries + ] + else: + boundaries_repr = f"{len(self.role_boundaries)} built-in" + LOG.info( + "ProcessingStrategy init: class=%s train_on_inputs=%s " + "roles_to_train=%s train_on_eos=%s boundaries_source=%s " + "boundaries=%s", + type(self).__name__, + self.train_on_inputs, + self.roles_to_train, + self.train_on_eos, + source, + boundaries_repr, + ) + + def _build_role_boundaries(self) -> list[RoleBoundary]: + """Subclasses declare role boundaries here; [] opts out of role masking.""" + return [] + def __call__(self, examples: list[dict]) -> list[dict]: - """ - Preprocess conversation examples to ensure consistent format. - Converts different conversation formats to OpenAI format with 'messages'. - Supports two formats: - 1. OpenAI format with 'messages' - 2. Legacy format with 'conversations' - - Args: - examples: list of conversation dictionaries - - Returns: - list of dicts in OpenAI format with 'messages' key - - Raises: - ValueError: If the conversation format is not supported - """ + """Normalize examples to OpenAI ``messages`` format (accepts legacy ``conversations``).""" role_mapping = { "human": "user", "gpt": "assistant", } def normalize_role(role: str) -> str: - """Normalize role names to OpenAI format. Default to original role if not found.""" return role_mapping.get(role, role) def convert_legacy_format(example: dict) -> dict: - """Convert legacy 'conversations' format to OpenAI 'messages' format.""" messages = [ {"role": normalize_role(convo["from"]), "content": convo["value"]} for convo in example["conversations"] ] - - # Create new dict without 'conversations' key result = deepcopy(example) result.pop("conversations") result["messages"] = messages return result def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: - """Convert regular messages format to Messages format with content type""" - new_messages = [] for message in messages: if isinstance(message["content"], str): @@ -119,21 +193,27 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: "Only `messages` and `conversations` message keys are currently supported." ) - processed_example = None - if ( - "messages" in example and example["messages"] is not None - ): # OpenAI format - processed_example = example - else: # Legacy format + if "messages" in example and example["messages"] is not None: + # Deepcopy for symmetry with convert_legacy_format (which + # deepcopies internally) so downstream mutations of + # processed_example don't leak back to the caller's input. + processed_example = deepcopy(example) + elif "conversations" in example: processed_example = convert_legacy_format(example) + else: + # `messages` is present but None, and no `conversations` + # fallback exists — convert_legacy_format would KeyError on + # ["conversations"]. Surface a clear validation error instead. + raise ValueError( + "`messages` is present but None; provide non-null " + "`messages` or a `conversations` field." + ) - # convert regular messages format to Messages format with content type - # for compatibility with apply_chat_template + # Required for apply_chat_template compatibility. processed_example["messages"] = convert_messages_to_multimedia_messages( processed_example["messages"] ) - # find the image key if it exists possible_image_keys = ["images", "image"] image_key = None for key in possible_image_keys: @@ -141,11 +221,8 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: image_key = key break - # if the image key exists, add the image to the first user message if image_key is not None and processed_example[image_key] is not None: - # TODO: check if it's normal to be single image only for common datasets - # From observation, it's usually a list of single image but some datasets may have several columns for images - # Temporary solution: take the first image and suggest people convert their datasets to use multi-content Messages + # TODO: support multi-image samples; for now we take the first. if len(processed_example[image_key]) > 1: LOG.warning( f"Found {len(processed_example[image_key])} images in a sample. Using the first one." @@ -155,7 +232,6 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: image_value = processed_example[image_key][0] - # Handle image loading (Image, url, path, base64) image_value = load_image(image_value) if self.image_size is not None: @@ -168,11 +244,8 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: self.image_size, self.image_resize_algorithm ) else: - # Set the padding value; here we use black (0, 0, 0) for RGB images + # Int image_size: preserve aspect ratio then pad to square (black) to avoid distortion. padding_color = (0, 0, 0) - - # When image_size is an int (square target), preserve aspect ratio then pad - # This is to prevent aspect ratio distortion when resizing to square image_value = ImageOps.pad( image_value, (self.image_size, self.image_size), @@ -180,8 +253,6 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: color=padding_color, ) - # Look for any image type in the first message - # some dataset have an {type: "image"} in the first message msg_ind_to_add = None ind_to_add = None first_user_idx = None @@ -192,7 +263,7 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: for i, content in enumerate( processed_example["messages"][msg_idx]["content"] ): - # Usually datasets created with image columns, don't have it in the messages itself + # Column-image datasets often leave a bare {type: "image"} placeholder. if content["type"] == "image" and all( k not in content for k in ["image", "url", "path", "base64"] ): @@ -200,13 +271,11 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: ind_to_add = i break - # If an image type is found, add the image to that index if ind_to_add is not None and msg_ind_to_add is not None: processed_example["messages"][msg_ind_to_add]["content"][ ind_to_add ]["image"] = image_value else: - # if no image type is found, add it to end of the first user message if first_user_idx is None: first_user_idx = 0 processed_example["messages"][first_user_idx]["content"].append( @@ -221,28 +290,224 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: return processed_examples def _mask_non_assistant(self, labels: Tensor) -> Tensor: - """ - Mask non assistant regions to -100. - To be implemented per subclass. - """ - return labels + """Mask non-trainable role regions to -100 using ``self.role_boundaries``.""" + if self.train_on_inputs: + return labels + + # Legacy no-op for boundary-less strategies; warn once so the miss shows up in logs. + if not self.role_boundaries: + key = type(self).__name__ + if key not in _ROLE_MASK_WARNED: + _ROLE_MASK_WARNED.add(key) + LOG.warning( + "%s has no built-in role boundaries; " + "cfg.train_on_inputs / cfg.roles_to_train / cfg.train_on_eos " + "will NOT restrict loss to assistant tokens for this " + "multimodal model — only pad and media tokens are masked, " + "every other token (system, user, assistant) contributes " + "to loss. To enable assistant-only masking, declare " + "per-role markers in YAML via cfg.role_boundaries — see " + "docs/multimodal_assistant_mask.md for the format and the " + "list of strategies on this fallback path.", + key, + ) + return labels + + return _apply_role_boundaries( + labels, + self.role_boundaries, + roles_to_train=set(self.roles_to_train), + train_on_eos=self.train_on_eos, + ) def process_labels(self, input_ids: Tensor) -> Tensor: labels = input_ids.clone() - labels = self._mask_non_assistant(labels) + pad_id = getattr(self.processor.tokenizer, "pad_token_id", None) + if pad_id is not None: + labels[labels == pad_id] = -100 + if self.image_token_id is not None: + labels[labels == self.image_token_id] = -100 + return labels - # The labels are the input_ids, and we mask the padding tokens in the loss computation - labels[labels == self.processor.tokenizer.pad_token_id] = -100 - # Ignore the image token index in the loss computation (model specific) - labels[labels == self.image_token_id] = -100 +def _apply_role_boundaries( + labels: Tensor, + role_boundaries: list[RoleBoundary], + roles_to_train: set[str], + train_on_eos: str, +) -> Tensor: + """Mask tokens outside trainable role spans to -100. + + Scan is greedy-left with longest-prefix-wins on start_tokens to disambiguate + nested markers (e.g. ``<|im_start|>assistant`` vs ``<|im_start|>``). + ``train_on_eos`` accepts ``"turn"`` (end marker in loss on trainable turns + only), ``"all"`` (always), ``"none"`` (never — overrides ``include_end``), + ``"last"`` (only on the last trainable turn in the sequence). + """ + mask = zeros_like(labels) + # For "last": remember each trainable turn's end-marker span so we can + # unmask only the final one after the scan finishes. + last_trainable_end_span: list[Optional[tuple[int, int]]] = [None] * labels.shape[0] + + # Work on a Python list per row — avoids O(n*boundaries) Tensor→list + # conversions in the hot prefix-match loop. + def _match_prefix(label: list[int], start_pos: int, tok_seq: list[int]) -> bool: + if not tok_seq or start_pos + len(tok_seq) > len(label): + return False + return label[start_pos : start_pos + len(tok_seq)] == tok_seq + + def _find_end( + label: list[int], start_pos: int, end_tok: list[int] + ) -> tuple[int, bool]: + # Empty end_tok means run to end-of-sequence. + if not end_tok: + return len(label), False + k = start_pos + while k < len(label): + if _match_prefix(label, k, end_tok): + return k + len(end_tok), True + k += 1 + return k, False + + for i in range(labels.shape[0]): + label = labels[i].tolist() + j = 0 + n = len(label) + while j < n: + best_match: Optional[RoleBoundary] = None + for b in role_boundaries: + if _match_prefix(label, j, b.start_tokens): + if best_match is None or len(b.start_tokens) > len( + best_match.start_tokens + ): + best_match = b + if best_match is None: + j += 1 + continue + + start_of_content = j + len(best_match.start_tokens) + end_after, found_end = _find_end( + label, start_of_content, best_match.end_tokens + ) - return labels + role_in_loss = best_match.role in roles_to_train + + if role_in_loss: + if best_match.include_start: + mask[i][j:start_of_content] = 1 + content_end = ( + end_after - len(best_match.end_tokens) if found_end else end_after + ) + mask[i][start_of_content:content_end] = 1 + # train_on_eos="none"/"last" override include_end during main + # loop; "last" is applied after the scan finishes. + if ( + found_end + and best_match.include_end + and train_on_eos not in ("none", "last") + ): + mask[i][content_end:end_after] = 1 + if found_end and best_match.include_end and train_on_eos == "last": + last_trainable_end_span[i] = (content_end, end_after) + else: + # Non-trainable role on train_on_eos="all": gate on include_end + # so Pixtral / Mistral V7 Tekken shared [/INST] doesn't leak. + if found_end and best_match.include_end and train_on_eos == "all": + content_end = end_after - len(best_match.end_tokens) + mask[i][content_end:end_after] = 1 + + # When include_end=False, do not consume the end marker: back up so + # the next iteration can re-match it as the next boundary's start + # marker (Pixtral / Mistral V7 Tekken share [/INST] between + # user-end and assistant-start). Requires end_tokens non-empty and + # actually found. + if found_end and not best_match.include_end and best_match.end_tokens: + j = end_after - len(best_match.end_tokens) + else: + j = end_after + + if train_on_eos == "last" and (span := last_trainable_end_span[i]) is not None: + s, e = span + mask[i][s:e] = 1 + + labels[i][mask[i] == 0] = -100 + + return labels + + +def _encode_markers(tokenizer, marker_strs: list[str]) -> list[list[int]]: + """Encode markers via ``encode(..., add_special_tokens=False)``; drops empty results.""" + result = [] + for s in marker_strs: + toks = tokenizer.encode(s, add_special_tokens=False) + if toks: + result.append(toks) + return result + + +def _resolve_role_boundary_override(specs: list[dict], tokenizer) -> list[RoleBoundary]: + """Resolve user ``cfg.role_boundaries`` specs into RoleBoundary objects. + + The sentinel ``end == "eos_token"`` resolves to ``eos_token_id`` (used by + Pixtral/Mistral v7 templates). ``end`` null/omitted runs to end-of-sequence. + """ + out: list[RoleBoundary] = [] + for i, spec in enumerate(specs): + if hasattr(spec, "model_dump"): + d = spec.model_dump() + else: + d = dict(spec) + + role = d.get("role") + start_str = d.get("start") + if not role or start_str is None: + raise ValueError( + f"cfg.role_boundaries[{i}] must have both 'role' and 'start' " + f"(got {d!r})." + ) + start_ids = tokenizer.encode(start_str, add_special_tokens=False) + if not start_ids: + raise ValueError( + f"cfg.role_boundaries[{i}]: start marker {start_str!r} " + f"tokenizes to an empty sequence; cannot match." + ) + + end_spec = d.get("end") + if end_spec is None: + end_ids: list[int] = [] + elif end_spec == "eos_token": + eos = getattr(tokenizer, "eos_token_id", None) + if eos is None: + raise ValueError( + f"cfg.role_boundaries[{i}] requested end='eos_token' but " + "the tokenizer has no eos_token_id." + ) + end_ids = [eos] + else: + end_ids = tokenizer.encode(end_spec, add_special_tokens=False) + if not end_ids: + raise ValueError( + f"cfg.role_boundaries[{i}]: end marker {end_spec!r} " + f"tokenizes to an empty sequence; cannot match. Use " + f"end=null to run to end-of-sequence or end='eos_token' " + f"to terminate at the tokenizer's EOS." + ) + + out.append( + RoleBoundary( + role=role, + start_tokens=start_ids, + end_tokens=end_ids, + include_start=bool(d.get("include_start", False)), + include_end=bool(d.get("include_end", True)), + ) + ) + return out class Qwen2VLProcessingStrategy(ProcessingStrategy): - """Processing Strategy class for Qwen2-VL""" + """Processing Strategy class for Qwen2-VL (ChatML ``<|im_start|>{role}\\n ... <|im_end|>``).""" def __init__( self, @@ -250,16 +515,44 @@ def __init__( chat_template: Optional[str] = None, image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, + train_on_inputs: bool = False, + roles_to_train: Optional[list[str]] = None, + train_on_eos: Optional[str] = None, + role_boundaries_override: Optional[list[dict]] = None, ): - super().__init__(processor, chat_template, image_size, image_resize_algorithm) + super().__init__( + processor, + chat_template, + image_size, + image_resize_algorithm, + train_on_inputs=train_on_inputs, + roles_to_train=roles_to_train, + train_on_eos=train_on_eos, + role_boundaries_override=role_boundaries_override, + ) self.image_token = "<|image_pad|>" # nosec self.image_token_id = processor.tokenizer.convert_tokens_to_ids( self.image_token ) + def _build_role_boundaries(self) -> list[RoleBoundary]: + tok = self.processor.tokenizer + end = _encode_markers(tok, ["<|im_end|>"]) + if not end: + return [] + end_ids = end[0] + boundaries = [] + for role in ("system", "user", "assistant"): + start = _encode_markers(tok, [f"<|im_start|>{role}\n"]) + if start: + boundaries.append( + RoleBoundary(role=role, start_tokens=start[0], end_tokens=end_ids) + ) + return boundaries + -class Qwen3_5ProcessingStrategy(ProcessingStrategy): - """Processing Strategy class for Qwen3.5 (early-fusion VLM)""" +class Qwen3_5ProcessingStrategy(Qwen2VLProcessingStrategy): + """Processing Strategy class for Qwen3.5 (Qwen2-VL boundaries + ``<|video_pad|>`` mask).""" def __init__( self, @@ -267,11 +560,20 @@ def __init__( chat_template: Optional[str] = None, image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, + train_on_inputs: bool = False, + roles_to_train: Optional[list[str]] = None, + train_on_eos: Optional[str] = None, + role_boundaries_override: Optional[list[dict]] = None, ): - super().__init__(processor, chat_template, image_size, image_resize_algorithm) - self.image_token = "<|image_pad|>" # nosec - self.image_token_id = processor.tokenizer.convert_tokens_to_ids( - self.image_token + super().__init__( + processor, + chat_template, + image_size, + image_resize_algorithm, + train_on_inputs=train_on_inputs, + roles_to_train=roles_to_train, + train_on_eos=train_on_eos, + role_boundaries_override=role_boundaries_override, ) self.video_token = "<|video_pad|>" # nosec self.video_token_id = processor.tokenizer.convert_tokens_to_ids( @@ -280,12 +582,44 @@ def __init__( def process_labels(self, input_ids): labels = super().process_labels(input_ids) - labels[labels == self.video_token_id] = -100 + if self.video_token_id is not None: + labels[labels == self.video_token_id] = -100 return labels -class Gemma3ProcessingStrategy(ProcessingStrategy): - """Processing Strategy class for Gemma3""" +class _GemmaTurnStrategy(ProcessingStrategy): + """Gemma3/3n ``{role} ... `` (Gemma 4 uses different markers).""" + + def _build_role_boundaries(self) -> list[RoleBoundary]: + tok = self.processor.tokenizer + end = _encode_markers(tok, [""]) + if not end: + return [] + end_ids = end[0] + boundaries = [] + # Template uses 'model'; external role knob stays 'assistant'. Gemma 3 + # and Gemma 3n jinja templates fold the system message into the first + # user's content prefix and never emit 'system', so we + # don't declare a system boundary here. + role_marker_pairs = [ + ("assistant", "model"), + ("user", "user"), + ] + for external_role, template_role in role_marker_pairs: + start = _encode_markers(tok, [f"{template_role}\n"]) + if start: + boundaries.append( + RoleBoundary( + role=external_role, + start_tokens=start[0], + end_tokens=end_ids, + ) + ) + return boundaries + + +class Gemma3ProcessingStrategy(_GemmaTurnStrategy): + """Processing Strategy class for Gemma3.""" def __init__( self, @@ -293,119 +627,247 @@ def __init__( chat_template: Optional[str] = None, image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, + train_on_inputs: bool = False, + roles_to_train: Optional[list[str]] = None, + train_on_eos: Optional[str] = None, + role_boundaries_override: Optional[list[dict]] = None, ): - super().__init__(processor, chat_template, image_size, image_resize_algorithm) - self.image_token = processor.tokenizer.special_tokens_map["boi_token"] - self.image_token_id = processor.tokenizer.convert_tokens_to_ids( - self.image_token + super().__init__( + processor, + chat_template, + image_size, + image_resize_algorithm, + train_on_inputs=train_on_inputs, + roles_to_train=roles_to_train, + train_on_eos=train_on_eos, + role_boundaries_override=role_boundaries_override, ) + # Real Gemma3 tokenizers expose boi_token as a direct attribute, not + # via special_tokens_map (which only holds HF's standard slots). + boi = getattr(processor.tokenizer, "boi_token", None) + if boi is not None: + self.image_token = boi + self.image_token_id = processor.tokenizer.convert_tokens_to_ids(boi) def process_labels(self, input_ids): - labels = input_ids.clone() - - # Follows https://ai.google.dev/gemma/docs/core/huggingface_vision_finetune_qlora - labels[labels == self.processor.tokenizer.pad_token_id] = -100 - labels[labels == self.image_token_id] = -100 - labels[labels == 262144] = -100 # corresponds to - + labels = super().process_labels(input_ids) + # Resolve via tokenizer; fall back to default id + # if not in vocab. Matches Gemma4's pattern. + tok = self.processor.tokenizer + soft_id = tok.convert_tokens_to_ids("") + unk_id = getattr(tok, "unk_token_id", None) + if soft_id is not None and soft_id != unk_id: + labels[labels == soft_id] = -100 + else: + labels[labels == 262144] = -100 return labels -class Gemma3nProcessingStrategy(ProcessingStrategy): - """Processing Strategy class for Gemma3n""" - - def _mask_non_assistant(self, labels: Tensor) -> Tensor: - def _find_token_sequence(label, start_pos, token_sequence): - """Check if token_sequence appears at start_pos in label""" - if start_pos + len(token_sequence) > len(label): - return False - if label[start_pos] != token_sequence[0]: - return False - return ( - label[start_pos : start_pos + len(token_sequence)].tolist() - == token_sequence - ) - - def _find_assistant_end(label, start_pos, assistant_end_tok, mask, i): - """ - Find the end of assistant response and update mask accordingly - - Returns new position to continue from and whether the end seq is found - """ - k = start_pos - while k < len(label): - if not _find_token_sequence(label, k, assistant_end_tok): - mask[i][k] = 1 - k += 1 - continue - - return k + len(assistant_end_tok), True - - return k, False +class Gemma3nProcessingStrategy(_GemmaTurnStrategy): + """Gemma3n: same turn boundaries as Gemma3, additionally masks audio/delimiter tokens.""" - mask = zeros_like(labels) - - assistant_start_str = "model" - assistant_end_str = "" - include_assistant_start_tok = False - include_assistant_end_tok = True + def process_labels(self, input_ids): + labels = super().process_labels(input_ids) + tok = self.processor.tokenizer + # Follows huggingface-gemma-recipes fine_tune_gemma3n_on_t4 notebook. + for attr in ( + "image_token_id", + "audio_token_id", + "boi_token_id", + "eoi_token_id", + ): + tok_id = getattr(tok, attr, None) + if tok_id is not None: + labels[labels == tok_id] = -100 + return labels - # str to tokens - assistant_start_tok = self.processor.tokenizer.encode( - assistant_start_str, add_special_tokens=False - ) - assistant_end_tok = self.processor.tokenizer.encode( - assistant_end_str, add_special_tokens=False - ) - for i, label in enumerate(labels): - j = 0 - # while loop through each tok index in labels[i] - while j < len(label): - # Check until match start seq - if not _find_token_sequence(label, j, assistant_start_tok): - j += 1 - continue - - if include_assistant_start_tok: - mask[i][j : j + len(assistant_start_tok)] = 1 - - # Find where the assistant response ends - start_of_content = j + len(assistant_start_tok) - end_pos, found_end_seq = _find_assistant_end( - label, start_of_content, assistant_end_tok, mask, i +class Gemma4ProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for Gemma 4. + + Boundary markers ``<|turn>model ... `` verified against + google/gemma-4-E2B-it. boi/eoi/boa/eoa ids are resolved via + ``convert_tokens_to_ids`` since only their string forms are on the processor. + """ + + def _build_role_boundaries(self) -> list[RoleBoundary]: + tok = self.processor.tokenizer + end = _encode_markers(tok, [""]) + if not end: + return [] + end_ids = end[0] + boundaries = [] + role_marker_pairs = [ + ("assistant", "model"), + ("user", "user"), + ("system", "system"), + ] + for external_role, template_role in role_marker_pairs: + # Include trailing ``\n`` for consistency with Qwen/Gemma3/Llama + # markers; the newline is part of the marker in the real + # google/gemma-4 tokenizer's chat template. + start = _encode_markers(tok, [f"<|turn>{template_role}\n"]) + if start: + boundaries.append( + RoleBoundary( + role=external_role, + start_tokens=start[0], + end_tokens=end_ids, + ) ) + return boundaries - # Include end token if requested - if include_assistant_end_tok and found_end_seq: - mask[i][end_pos - len(assistant_end_tok) : end_pos] = 1 - - j = end_pos + def process_labels(self, input_ids): + labels = super().process_labels(input_ids) - labels[i][mask[i] == 0] = -100 + tokenizer = self.processor.tokenizer + unk_id = getattr(tokenizer, "unk_token_id", None) + + if getattr(tokenizer, "image_token_id", None) is not None: + labels[labels == tokenizer.image_token_id] = -100 + if getattr(tokenizer, "audio_token_id", None) is not None: + labels[labels == tokenizer.audio_token_id] = -100 + + # boi/eoi/boa/eoa are only string attrs on the processor; resolve ids here. + for attr in ("boi_token", "eoi_token", "boa_token", "eoa_token"): + token_str = getattr(self.processor, attr, None) + if token_str is None: + continue + token_id = tokenizer.convert_tokens_to_ids(token_str) + if token_id is None or token_id == unk_id: + continue + labels[labels == token_id] = -100 + + # Video id lives on the processor, not the tokenizer. + video_token_id = getattr(self.processor, "video_token_id", None) + if video_token_id is not None and video_token_id != unk_id: + labels[labels == video_token_id] = -100 return labels - def process_labels(self, input_ids): - labels = input_ids.clone() - labels = self._mask_non_assistant(labels) - # Follows https://colab.research.google.com/github/huggingface/huggingface-gemma-recipes/blob/main/notebooks/fine_tune_gemma3n_on_t4.ipynb - labels[labels == self.processor.tokenizer.pad_token_id] = -100 - if hasattr(self.processor.tokenizer, "image_token_id"): - labels[labels == self.processor.tokenizer.image_token_id] = -100 - if hasattr(self.processor.tokenizer, "audio_token_id"): - labels[labels == self.processor.tokenizer.audio_token_id] = -100 - if hasattr(self.processor.tokenizer, "boi_token_id"): - labels[labels == self.processor.tokenizer.boi_token_id] = -100 - if hasattr(self.processor.tokenizer, "eoi_token_id"): - labels[labels == self.processor.tokenizer.eoi_token_id] = -100 +class Llama3_2VisionProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for Llama-3.2 Vision (``<|start_header_id|>{role}<|end_header_id|>\\n\\n ... <|eot_id|>``).""" - return labels + def _build_role_boundaries(self) -> list[RoleBoundary]: + tok = self.processor.tokenizer + end = _encode_markers(tok, ["<|eot_id|>"]) + if not end: + return [] + end_ids = end[0] + boundaries = [] + for role in ("system", "user", "assistant", "ipython", "tool"): + start = _encode_markers( + tok, [f"<|start_header_id|>{role}<|end_header_id|>\n\n"] + ) + if start: + boundaries.append( + RoleBoundary(role=role, start_tokens=start[0], end_tokens=end_ids) + ) + return boundaries + + +class Llama4ProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for Llama 4 (``<|header_start|>{role}<|header_end|>\\n\\n ... <|eot|>``).""" + + def _build_role_boundaries(self) -> list[RoleBoundary]: + tok = self.processor.tokenizer + end = _encode_markers(tok, ["<|eot|>"]) + if not end: + return [] + end_ids = end[0] + boundaries = [] + for role in ("system", "user", "assistant", "ipython", "tool"): + start = _encode_markers(tok, [f"<|header_start|>{role}<|header_end|>\n\n"]) + if start: + boundaries.append( + RoleBoundary(role=role, start_tokens=start[0], end_tokens=end_ids) + ) + return boundaries + + +class PixtralProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for Pixtral (``[INST] ... [/INST]`` user, assistant terminates at ``eos_token``). + + ``[/INST]`` is shared between user-end and assistant-start. We declare user + with ``include_end=False`` so the scanner hands the ``[/INST]`` back to + assistant's start match on the next iteration. + """ + + def _build_role_boundaries(self) -> list[RoleBoundary]: + tok = self.processor.tokenizer + eos = getattr(tok, "eos_token_id", None) + if eos is None: + return [] + boundaries = [] + inst_start = _encode_markers(tok, ["[INST]"]) + inst_end = _encode_markers(tok, ["[/INST]"]) + if inst_start and inst_end: + boundaries.append( + RoleBoundary( + role="user", + start_tokens=inst_start[0], + end_tokens=inst_end[0], + include_end=False, + ) + ) + boundaries.append( + RoleBoundary( + role="assistant", + start_tokens=inst_end[0], + end_tokens=[eos], + ) + ) + return boundaries + + +class MistralV7TekkenProcessingStrategy(ProcessingStrategy): + """Processing Strategy class for Mistral v7 Tekken (Pixtral-style plus ``[SYSTEM_PROMPT]...[/SYSTEM_PROMPT]``). + + Same ``[/INST]``-shared-marker treatment as :class:`PixtralProcessingStrategy`. + """ + + def _build_role_boundaries(self) -> list[RoleBoundary]: + tok = self.processor.tokenizer + eos = getattr(tok, "eos_token_id", None) + if eos is None: + return [] + boundaries = [] + sys_start = _encode_markers(tok, ["[SYSTEM_PROMPT]"]) + sys_end = _encode_markers(tok, ["[/SYSTEM_PROMPT]"]) + if sys_start and sys_end: + boundaries.append( + RoleBoundary( + role="system", start_tokens=sys_start[0], end_tokens=sys_end[0] + ) + ) + inst_start = _encode_markers(tok, ["[INST]"]) + inst_end = _encode_markers(tok, ["[/INST]"]) + if inst_start and inst_end: + boundaries.append( + RoleBoundary( + role="user", + start_tokens=inst_start[0], + end_tokens=inst_end[0], + include_end=False, + ) + ) + boundaries.append( + RoleBoundary( + role="assistant", + start_tokens=inst_end[0], + end_tokens=[eos], + ) + ) + return boundaries class VoxtralProcessingStrategy(ProcessingStrategy): - """Processing Strategy class for Voxtral""" + """Processing Strategy class for Voxtral. + + Role boundaries NOT declared — mistral-common instruct tokenizer markers + unverified. Falls back to pad+audio masking with a one-shot warning. + """ def __init__( self, @@ -413,8 +875,21 @@ def __init__( chat_template: Optional[str] = None, image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, + train_on_inputs: bool = False, + roles_to_train: Optional[list[str]] = None, + train_on_eos: Optional[str] = None, + role_boundaries_override: Optional[list[dict]] = None, ): - super().__init__(processor, chat_template, image_size, image_resize_algorithm) + super().__init__( + processor, + chat_template, + image_size, + image_resize_algorithm, + train_on_inputs=train_on_inputs, + roles_to_train=roles_to_train, + train_on_eos=train_on_eos, + role_boundaries_override=role_boundaries_override, + ) special_ids = ( processor.tokenizer.tokenizer.instruct_tokenizer.audio_encoder.special_ids ) @@ -424,16 +899,25 @@ def __init__( def process_labels(self, input_ids): labels = input_ids.clone() + labels = self._mask_non_assistant(labels) - labels[labels == self.processor.tokenizer.pad_token_id] = -100 - labels[labels == self.audio_token] = -100 - labels[labels == self.begin_audio_token] = -100 + pad_id = getattr(self.processor.tokenizer, "pad_token_id", None) + if pad_id is not None: + labels[labels == pad_id] = -100 + if self.audio_token is not None: + labels[labels == self.audio_token] = -100 + if self.begin_audio_token is not None: + labels[labels == self.begin_audio_token] = -100 return labels class SmolVLM2ProcessingStrategy(ProcessingStrategy): - """Processing Strategy class for SmolVLM2""" + """Processing Strategy class for SmolVLM2. + + Role boundaries NOT declared — SmolVLM2 chat_template varies per checkpoint + (HuggingFaceTB ships multiple variants), so we opt out rather than mis-mask. + """ def __init__( self, @@ -441,8 +925,21 @@ def __init__( chat_template: Optional[str] = None, image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, + train_on_inputs: bool = False, + roles_to_train: Optional[list[str]] = None, + train_on_eos: Optional[str] = None, + role_boundaries_override: Optional[list[dict]] = None, ): - super().__init__(processor, chat_template, image_size, image_resize_algorithm) + super().__init__( + processor, + chat_template, + image_size, + image_resize_algorithm, + train_on_inputs=train_on_inputs, + roles_to_train=roles_to_train, + train_on_eos=train_on_eos, + role_boundaries_override=role_boundaries_override, + ) self.image_token = "" # nosec self.image_token_id = processor.tokenizer.additional_special_tokens_ids[ @@ -451,7 +948,11 @@ def __init__( class Mistral3ProcessingStrategy(ProcessingStrategy): - """Processing Strategy class for Mistral3""" + """Processing Strategy class for Mistral3. + + Role boundaries NOT declared (mistral-common instruct tokenizer unverified); + same fallback as VoxtralProcessingStrategy. + """ def __init__( self, @@ -459,8 +960,21 @@ def __init__( chat_template: Optional[str] = None, image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, + train_on_inputs: bool = False, + roles_to_train: Optional[list[str]] = None, + train_on_eos: Optional[str] = None, + role_boundaries_override: Optional[list[dict]] = None, ): - super().__init__(processor, chat_template, image_size, image_resize_algorithm) + super().__init__( + processor, + chat_template, + image_size, + image_resize_algorithm, + train_on_inputs=train_on_inputs, + roles_to_train=roles_to_train, + train_on_eos=train_on_eos, + role_boundaries_override=role_boundaries_override, + ) special_ids = ( processor.tokenizer.tokenizer.instruct_tokenizer.image_encoder.special_ids ) @@ -471,17 +985,24 @@ def __init__( def process_labels(self, input_ids): labels = input_ids.clone() + labels = self._mask_non_assistant(labels) - labels[labels == self.processor.tokenizer.pad_token_id] = -100 - labels[labels == self.image_token] = -100 - labels[labels == self.image_break_token] = -100 - labels[labels == self.image_end_token] = -100 + pad_id = getattr(self.processor.tokenizer, "pad_token_id", None) + if pad_id is not None: + labels[labels == pad_id] = -100 + for tok_id in (self.image_token, self.image_break_token, self.image_end_token): + if tok_id is not None: + labels[labels == tok_id] = -100 return labels class InternVLProcessingStrategy(ProcessingStrategy): - """Processing Strategy class for InternVL""" + """Processing Strategy class for InternVL. + + Role boundaries NOT declared (InternLM-style template unverified); falls + back to pad + image-id masking with a one-shot warning. + """ def __init__( self, @@ -489,8 +1010,21 @@ def __init__( chat_template: Optional[str] = None, image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, + train_on_inputs: bool = False, + roles_to_train: Optional[list[str]] = None, + train_on_eos: Optional[str] = None, + role_boundaries_override: Optional[list[dict]] = None, ): - super().__init__(processor, chat_template, image_size, image_resize_algorithm) + super().__init__( + processor, + chat_template, + image_size, + image_resize_algorithm, + train_on_inputs=train_on_inputs, + roles_to_train=roles_to_train, + train_on_eos=train_on_eos, + role_boundaries_override=role_boundaries_override, + ) if not hasattr(processor, "image_ids"): raise ValueError("'image_ids' missing from InternVL Processor.") @@ -499,20 +1033,26 @@ def __init__( def process_labels(self, input_ids): labels = input_ids.clone() + labels = self._mask_non_assistant(labels) - labels[labels == self.processor.tokenizer.pad_token_id] = -100 + pad_id = getattr(self.processor.tokenizer, "pad_token_id", None) + if pad_id is not None: + labels[labels == pad_id] = -100 for ids in self.image_token_ids: - labels[labels == ids] = -100 - - # Note: Check if need to mask 'video_token' as it gets converted to - # image patches during media processing + if ids is not None: + labels[labels == ids] = -100 + # Video tokens get converted to image patches during media processing; masking may be redundant. return labels class Glm4vProcessingStrategy(ProcessingStrategy): - """Processing Strategy class for GLM4V and GLM4V-MoE vision models.""" + """Shared strategy for Glm4vProcessor (GLM-4V / GLM-4.1V) and + Glm46VProcessor (GLM-4.6V / GLM-4.7V) — identical media-token markers. + + Role boundaries unverified; use cfg.role_boundaries to enable masking. + """ def __init__( self, @@ -520,8 +1060,21 @@ def __init__( chat_template: Optional[str] = None, image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, + train_on_inputs: bool = False, + roles_to_train: Optional[list[str]] = None, + train_on_eos: Optional[str] = None, + role_boundaries_override: Optional[list[dict]] = None, ): - super().__init__(processor, chat_template, image_size, image_resize_algorithm) + super().__init__( + processor, + chat_template, + image_size, + image_resize_algorithm, + train_on_inputs=train_on_inputs, + roles_to_train=roles_to_train, + train_on_eos=train_on_eos, + role_boundaries_override=role_boundaries_override, + ) self.tokenizer = getattr(processor, "tokenizer", processor) @@ -549,16 +1102,22 @@ def __init__( def process_labels(self, input_ids): labels = input_ids.clone() + labels = self._mask_non_assistant(labels) - labels[labels == self.tokenizer.pad_token_id] = -100 - - labels[labels == self.image_token_id] = -100 - labels[labels == self.begin_image_token_id] = -100 - labels[labels == self.end_image_token_id] = -100 - - labels[labels == self.video_token_id] = -100 - labels[labels == self.begin_video_token_id] = -100 - labels[labels == self.end_video_token_id] = -100 + pad_id = getattr(self.tokenizer, "pad_token_id", None) + if pad_id is not None: + labels[labels == pad_id] = -100 + + for tok_id in ( + self.image_token_id, + self.begin_image_token_id, + self.end_image_token_id, + self.video_token_id, + self.begin_video_token_id, + self.end_video_token_id, + ): + if tok_id is not None: + labels[labels == tok_id] = -100 return labels @@ -569,14 +1128,20 @@ def get_processing_strategy( chat_template_type, image_size: int | tuple[int, int] | None = None, image_resize_algorithm: Resampling | None = None, + train_on_inputs: bool = False, + roles_to_train: Optional[list[str]] = None, + train_on_eos: Optional[str] = None, + role_boundaries_override: Optional[list[dict]] = None, ): - from axolotl.utils.mistral.mistral3_processor import Mistral3Processor - processing_kwargs = { "processor": processor, "chat_template": chat_template, "image_size": image_size, "image_resize_algorithm": image_resize_algorithm, + "train_on_inputs": train_on_inputs, + "roles_to_train": roles_to_train, + "train_on_eos": train_on_eos, + "role_boundaries_override": role_boundaries_override, } if chat_template_type in [None, "tokenizer_default"]: @@ -585,53 +1150,63 @@ def get_processing_strategy( processing_kwargs["chat_template"] = tokenizer.chat_template if chat_template_type == "qwen2_vl": - return Qwen2VLProcessingStrategy( - **processing_kwargs, - ) - if chat_template_type in ["qwen3_5", "qwen3_5_moe"]: - return Qwen3_5ProcessingStrategy( - **processing_kwargs, - ) + return Qwen2VLProcessingStrategy(**processing_kwargs) + if chat_template_type == "qwen3_5": + return Qwen3_5ProcessingStrategy(**processing_kwargs) if chat_template_type == "gemma3": - return Gemma3ProcessingStrategy( - **processing_kwargs, - ) + return Gemma3ProcessingStrategy(**processing_kwargs) if chat_template_type == "gemma3n": - return Gemma3nProcessingStrategy( - **processing_kwargs, - ) + return Gemma3nProcessingStrategy(**processing_kwargs) + if chat_template_type == "gemma4": + return Gemma4ProcessingStrategy(**processing_kwargs) + if chat_template_type == "llama3_2_vision": + return Llama3_2VisionProcessingStrategy(**processing_kwargs) + if chat_template_type == "llama4": + return Llama4ProcessingStrategy(**processing_kwargs) + if chat_template_type == "pixtral": + return PixtralProcessingStrategy(**processing_kwargs) + if chat_template_type == "mistral_v7_tekken": + return MistralV7TekkenProcessingStrategy(**processing_kwargs) if isinstance(processor, VoxtralProcessor): - return VoxtralProcessingStrategy( - **processing_kwargs, - ) + return VoxtralProcessingStrategy(**processing_kwargs) if isinstance(processor, SmolVLMProcessor): - return SmolVLM2ProcessingStrategy( - **processing_kwargs, - ) + return SmolVLM2ProcessingStrategy(**processing_kwargs) - if isinstance(processor, Mistral3Processor): - return Mistral3ProcessingStrategy( - **processing_kwargs, + # Lazy import: mistral_common is optional. Mirrors the Glm46V pattern below. + try: + from axolotl.utils.mistral.mistral3_processor import Mistral3Processor + + if isinstance(processor, Mistral3Processor): + return Mistral3ProcessingStrategy(**processing_kwargs) + except (ImportError, ModuleNotFoundError) as exc: + LOG.debug( + "Mistral3Processor import failed; Mistral3 strategy will be unavailable: %r", + exc, ) + + # Both Glm4vProcessor and Glm46VProcessor share markers; route to the same + # strategy. Independent try/except so either can be absent. + try: + from transformers.models.glm4v.processing_glm4v import Glm4vProcessor + + if isinstance(processor, Glm4vProcessor): + return Glm4vProcessingStrategy(**processing_kwargs) + except (ImportError, ModuleNotFoundError) as exc: + LOG.debug("Glm4vProcessor import failed: %r", exc) + try: from transformers.models.glm46v.processing_glm46v import Glm46VProcessor if isinstance(processor, Glm46VProcessor): - return Glm4vProcessingStrategy( - **processing_kwargs, - ) - except ImportError: - pass + return Glm4vProcessingStrategy(**processing_kwargs) + except (ImportError, ModuleNotFoundError) as exc: + LOG.debug("Glm46VProcessor import failed: %r", exc) if isinstance(processor, InternVLProcessor): - return InternVLProcessingStrategy( - **processing_kwargs, - ) + return InternVLProcessingStrategy(**processing_kwargs) - # llama3_2_vision, llama4, llava - # mistral_v7_tekken, pixtral, lfm2vl - return ProcessingStrategy( - **processing_kwargs, - ) + # Unregistered templates (llava, lfm2vl, mistral_v3_tekken, ...) use the + # base strategy; it warns once when train_on_inputs=False. + return ProcessingStrategy(**processing_kwargs) diff --git a/src/axolotl/utils/schemas/datasets.py b/src/axolotl/utils/schemas/datasets.py index 6114a63e0a..97ed71631d 100644 --- a/src/axolotl/utils/schemas/datasets.py +++ b/src/axolotl/utils/schemas/datasets.py @@ -166,10 +166,10 @@ class SFTDataset(BaseModel): "description": "Roles to train on. The tokens from these roles will be considered for the loss." }, ) - train_on_eos: Literal["all", "turn", "last"] | None = Field( + train_on_eos: Literal["all", "turn", "last", "none"] | None = Field( default=None, json_schema_extra={ - "description": "Which EOS tokens to train on in the conversation. Possible values are: all: train on all EOS tokens, turn (default): train on the EOS token at the end of each trainable turn, last: train on the last EOS token in the conversation" + "description": "Which EOS tokens to train on in the conversation. Possible values are: all: train on all EOS tokens, turn (default): train on the EOS token at the end of each trainable turn, last: train on the last EOS token in the conversation, none: never train on EOS tokens" }, ) roles: dict[str, list[str]] | None = Field( diff --git a/src/axolotl/utils/schemas/multimodal.py b/src/axolotl/utils/schemas/multimodal.py index a3449199f3..01ad5e5a3d 100644 --- a/src/axolotl/utils/schemas/multimodal.py +++ b/src/axolotl/utils/schemas/multimodal.py @@ -6,6 +6,57 @@ from pydantic import BaseModel, Field, field_validator +class RoleBoundarySpec(BaseModel): + """One ``cfg.role_boundaries`` row; see docs/multimodal_assistant_mask.md.""" + + role: str = Field( + json_schema_extra={ + "description": ( + "Role name as it appears in cfg.roles_to_train (e.g. " + "'assistant', 'user', 'system', 'tool', 'ipython')." + ) + }, + ) + start: str = Field( + json_schema_extra={ + "description": ( + "Literal string that marks the start of this role's span in " + "the rendered chat template. Tokenized via " + "``tokenizer.encode(..., add_special_tokens=False)`` at " + "strategy init." + ) + }, + ) + end: str | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Literal string that marks the end of this role's span. " + "Set to ``eos_token`` to terminate at the tokenizer's EOS. " + "Leave unset / null to terminate at end-of-sequence." + ) + }, + ) + include_start: bool = Field( + default=False, + json_schema_extra={ + "description": ( + "Whether the start marker tokens contribute to loss on " + "trainable turns. Default False." + ) + }, + ) + include_end: bool = Field( + default=True, + json_schema_extra={ + "description": ( + "Whether the end marker tokens contribute to loss on " + "trainable turns (honoring cfg.train_on_eos). Default True." + ) + }, + ) + + class MultiModalConfig(BaseModel): """Multi-modal configuration subset""" @@ -26,6 +77,17 @@ class MultiModalConfig(BaseModel): "description": "The resampling algorithm to use for image resizing. Default is bilinear. Please refer to PIL.Image.Resampling for more details." }, ) + role_boundaries: list[RoleBoundarySpec] | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Opt-in override for the MM mask scanner's per-role boundary " + "markers. Non-empty list replaces built-ins wholesale; unset " + "or empty falls back to built-ins. See " + "docs/multimodal_assistant_mask.md." + ) + }, + ) @field_validator("image_resize_algorithm", mode="before") @classmethod diff --git a/tests/test_processing_strategies.py b/tests/test_processing_strategies.py new file mode 100644 index 0000000000..2d8f13fe57 --- /dev/null +++ b/tests/test_processing_strategies.py @@ -0,0 +1,1164 @@ +"""Tests for ``axolotl.processing_strategies`` using fake tokenizers (offline/CI-safe).""" + +import logging + +import pytest +import torch +from pydantic import ValidationError + +from axolotl.processing_strategies import ( + Gemma3nProcessingStrategy, + Gemma3ProcessingStrategy, + Gemma4ProcessingStrategy, + Llama3_2VisionProcessingStrategy, + Llama4ProcessingStrategy, + MistralV7TekkenProcessingStrategy, + PixtralProcessingStrategy, + ProcessingStrategy, + Qwen2VLProcessingStrategy, + Qwen3_5ProcessingStrategy, + RoleBoundary, + _apply_role_boundaries, + get_processing_strategy, +) + + +@pytest.fixture +def axolotl_caplog(caplog): + """caplog that also captures records from the ``axolotl`` logger. + + The axolotl logger sets ``propagate=False`` once ``configure_logging()`` is + called (which happens indirectly in many CI test paths), so the default + caplog handler installed on the root logger never sees these records. + Attaching ``caplog.handler`` to ``axolotl.processing_strategies`` directly + makes assertions reliable regardless of whether ``configure_logging`` has + already run on this worker. + """ + logger = logging.getLogger("axolotl.processing_strategies") + logger.addHandler(caplog.handler) + previous_level = logger.level + logger.setLevel(logging.DEBUG) + try: + yield caplog + finally: + logger.removeHandler(caplog.handler) + logger.setLevel(previous_level) + + +# --------------------------------------------------------------------------- # +# Generic fake tokenizer/processor scaffold +# --------------------------------------------------------------------------- # + + +class _Tokenizer: + """Minimal tokenizer stub; ``vocab`` maps marker strings to their id lists.""" + + def __init__( + self, + vocab: dict[str, list[int]], + pad_id: int = 0, + unk_id: int = 3, + eos_id: int | None = None, + ): + self.vocab = vocab + self._reverse = {} + for tok, ids in vocab.items(): + if len(ids) == 1: + self._reverse[ids[0]] = tok + self.pad_token_id = pad_id + self.unk_token_id = unk_id + if eos_id is not None: + self.eos_token_id = eos_id + + def encode(self, text, add_special_tokens=False): + # Unknown markers return [] so _encode_markers drops them silently. + return list(self.vocab.get(text, [])) + + def convert_tokens_to_ids(self, token): + v = self.vocab.get(token) + if v is None: + return self.unk_token_id + return v[0] if len(v) == 1 else self.unk_token_id + + +class _Processor: + def __init__(self, tokenizer: _Tokenizer): + self.tokenizer = tokenizer + + +# --------------------------------------------------------------------------- # +# Base scanner tests (train_on_inputs / roles_to_train / train_on_eos) +# --------------------------------------------------------------------------- # + + +def _scan(role_boundaries, seq, roles_to_train=("assistant",), train_on_eos="turn"): + labels = torch.tensor([seq]) + return _apply_role_boundaries( + labels, role_boundaries, set(roles_to_train), train_on_eos + ).tolist()[0] + + +def test_scanner_assistant_only_basic(): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[1, 2], end_tokens=[9]), + RoleBoundary(role="user", start_tokens=[1, 3], end_tokens=[9]), + ] + seq = [1, 3, 7, 9, 1, 2, 8, 8, 9, 5] + out = _scan(boundaries, seq) + assert out == [-100, -100, -100, -100, -100, -100, 8, 8, 9, -100] + + +def test_scanner_train_on_eos_none_excludes_end_marker(): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[1, 2], end_tokens=[9]), + ] + seq = [1, 2, 8, 8, 9] + out = _scan(boundaries, seq, train_on_eos="none") + assert out == [-100, -100, 8, 8, -100] + + +def test_scanner_train_on_eos_all_keeps_non_assistant_end_marker(): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[1, 2], end_tokens=[9]), + RoleBoundary(role="user", start_tokens=[1, 3], end_tokens=[9]), + ] + seq = [1, 3, 7, 9, 1, 2, 8, 9] + out = _scan(boundaries, seq, train_on_eos="all") + assert out == [-100, -100, -100, 9, -100, -100, 8, 9] + + +def test_scanner_train_on_eos_all_with_non_trainable_include_end_false(): + """Non-trainable + include_end=False must not leak end marker on 'all'.""" + boundaries = [ + RoleBoundary( + role="user", + start_tokens=[50], + end_tokens=[51], + include_end=False, # shared with assistant-start + ), + RoleBoundary( + role="assistant", + start_tokens=[51], + end_tokens=[99], # eos + ), + ] + seq = [50, 7, 51, 8, 8, 99] # [INST] 7 [/INST] 8 8 EOS + out = _scan(boundaries, seq, roles_to_train=("assistant",), train_on_eos="all") + # [/INST] at idx 2 must stay masked — user.include_end=False says so. + assert out == [-100, -100, -100, 8, 8, 99] + + +def test_scanner_roles_to_train_user_and_assistant(): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[1, 2], end_tokens=[9]), + RoleBoundary(role="user", start_tokens=[1, 3], end_tokens=[9]), + ] + seq = [1, 3, 7, 9, 1, 2, 8, 9] + out = _scan(boundaries, seq, roles_to_train=("user", "assistant")) + # include_start defaults to False so role-start markers stay masked. + assert out == [-100, -100, 7, 9, -100, -100, 8, 9] + + +def test_scanner_truncated_assistant(): + """Missing end marker: span runs to end-of-sequence, end marker not emitted.""" + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[1, 2], end_tokens=[9]), + ] + seq = [1, 2, 8, 8, 8] + out = _scan(boundaries, seq) + assert out == [-100, -100, 8, 8, 8] + + +def test_scanner_longest_prefix_wins(): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[1, 2, 4], end_tokens=[9]), + RoleBoundary(role="user", start_tokens=[1, 2], end_tokens=[9]), + ] + seq = [1, 2, 4, 8, 9] + out = _scan(boundaries, seq) + assert out == [-100, -100, -100, 8, 9] + + +def test_scanner_no_boundaries_masks_everything(): + # Strategies short-circuit this in _mask_non_assistant; see test_base_strategy_warns_when_no_boundaries. + labels = torch.tensor([[1, 2, 3, 4]]) + out = _apply_role_boundaries(labels, [], {"assistant"}, "turn") + assert out.tolist() == [[-100, -100, -100, -100]] + + +def test_scanner_train_on_eos_last_only_final_trainable_turn(): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[1, 2], end_tokens=[9]), + ] + seq = [1, 2, 5, 9, 1, 2, 6, 9] + out = _scan(boundaries, seq, train_on_eos="last") + # Only the second assistant turn's end marker (index 7) is kept. + assert out == [-100, -100, 5, -100, -100, -100, 6, 9] + + +def test_scanner_train_on_eos_last_no_trainable_turn_is_noop(): + boundaries = [ + RoleBoundary(role="user", start_tokens=[1, 3], end_tokens=[9]), + ] + seq = [1, 3, 5, 9, 1, 3, 6, 9] + out = _scan(boundaries, seq, roles_to_train=("assistant",), train_on_eos="last") + assert out == [-100] * 8 + + +def test_strategy_rejects_unknown_train_on_eos(): + vocab = {"BOA": [50], "EOT": [60]} + with pytest.raises(ValueError, match="train_on_eos"): + ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + train_on_eos="bogus", + role_boundaries_override=[ + {"role": "assistant", "start": "BOA", "end": "EOT"} + ], + ) + + +def test_strategy_accepts_all_supported_train_on_eos_values(): + vocab = {"BOA": [50], "EOT": [60]} + for val in ("turn", "all", "none", "last"): + ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + train_on_eos=val, + role_boundaries_override=[ + {"role": "assistant", "start": "BOA", "end": "EOT"} + ], + ) + + +def test_empty_role_boundaries_override_falls_back_to_builtin(): + """Empty override must fall through to built-ins (opt-in semantics).""" + vocab = { + "<|im_start|>assistant\n": [101, 102, 103], + "<|im_start|>user\n": [101, 106, 103], + "<|im_end|>": [104], + } + strat_empty = Qwen2VLProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[], + ) + strat_default = Qwen2VLProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + ) + # Empty override === no override: both strategies keep the built-in boundaries. + assert strat_empty.role_boundaries == strat_default.role_boundaries + assert len(strat_empty.role_boundaries) > 0 # sanity: built-ins are non-empty + + +def test_sft_dataset_schema_accepts_all_supported_train_on_eos_values(): + """SFTDataset.train_on_eos accepts every value the scanner honors.""" + from axolotl.utils.schemas.datasets import SFTDataset + + for val in ("all", "turn", "last", "none"): + ds = SFTDataset(path="dummy", type="chat_template", train_on_eos=val) + assert ds.train_on_eos == val + + with pytest.raises(ValidationError): + SFTDataset(path="dummy", type="chat_template", train_on_eos="bogus") + + +def test_strategy_init_logs_resolved_masking_config_builtin(axolotl_caplog): + vocab = { + "<|im_start|>assistant\n": [101, 102, 103], + "<|im_start|>user\n": [101, 106, 103], + "<|im_end|>": [104], + } + with axolotl_caplog.at_level(logging.INFO, logger="axolotl.processing_strategies"): + Qwen2VLProcessingStrategy(_Processor(_Tokenizer(vocab, pad_id=0))) + msgs = [r.getMessage() for r in axolotl_caplog.records] + assert any( + "ProcessingStrategy init" in m + and "Qwen2VLProcessingStrategy" in m + and "boundaries_source=built-in" in m + for m in msgs + ) + + +def test_strategy_init_logs_resolved_masking_config_override(axolotl_caplog): + vocab = {"BOA": [50, 51], "EOT": [60]} + with axolotl_caplog.at_level(logging.INFO, logger="axolotl.processing_strategies"): + ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[ + {"role": "assistant", "start": "BOA", "end": "EOT"}, + ], + ) + msgs = [r.getMessage() for r in axolotl_caplog.records] + # Resolved start/end ids must appear in the log so users can verify what + # was actually matched. + assert any( + "ProcessingStrategy init" in m + and "boundaries_source=override" in m + and "[50, 51]" in m + and "[60]" in m + for m in msgs + ) + + +def test_process_labels_no_warning_when_image_token_id_none(): + """image_token_id=None must not trigger a UserWarning from ``labels == None``.""" + import warnings + + vocab = {"BOA": [50], "EOT": [60]} + strategy = ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[{"role": "assistant", "start": "BOA", "end": "EOT"}], + ) + assert strategy.image_token_id is None + with warnings.catch_warnings(): + warnings.simplefilter("error") + strategy.process_labels(torch.tensor([[1, 50, 2, 3, 60]])) + + +def test_roles_to_train_empty_list_masks_everything(): + """An explicit empty list is distinct from None and disables all roles.""" + vocab = {"BOA": [50], "EOT": [60]} + strategy = ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + roles_to_train=[], + role_boundaries_override=[{"role": "assistant", "start": "BOA", "end": "EOT"}], + ) + assert strategy.roles_to_train == [] + seq = [1, 50, 7, 8, 60, 9] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100] * 6 + + +# --------------------------------------------------------------------------- # +# Qwen2VL / Qwen3.5 +# --------------------------------------------------------------------------- # + + +def _qwen_tokenizer(): + # ChatML-ish with image_pad=200, video_pad=201. + vocab = { + "<|im_start|>assistant\n": [101, 102, 103], + "<|im_start|>user\n": [101, 106, 103], + "<|im_start|>system\n": [101, 105, 103], + "<|im_end|>": [104], + "<|image_pad|>": [200], + "<|video_pad|>": [201], + } + return _Tokenizer(vocab, pad_id=0) + + +def _make_qwen2vl(): + tok = _qwen_tokenizer() + return Qwen2VLProcessingStrategy(_Processor(tok)) + + +def test_qwen2vl_masks_user_keeps_assistant_and_image_pad(): + strategy = _make_qwen2vl() + seq = [ + 101, + 105, + 103, + 77, + 104, + 101, + 106, + 103, + 7, + 104, + 101, + 102, + 103, + 200, + 8, + 104, + ] + labels = strategy.process_labels(torch.tensor([seq])) + out = labels.tolist()[0] + assert out[:10] == [-100] * 10 + assert out[10] == -100 and out[11] == -100 and out[12] == -100 + assert out[13] == -100 # image_pad masked post-scan + assert out[14] == 8 + assert out[15] == 104 + + +def test_qwen3_5_masks_video_pad_too(): + tok = _qwen_tokenizer() + strategy = Qwen3_5ProcessingStrategy(_Processor(tok)) + seq = [101, 102, 103, 201, 8, 104] + labels = strategy.process_labels(torch.tensor([seq])) + assert labels.tolist()[0] == [-100, -100, -100, -100, 8, 104] + + +def test_qwen2vl_train_on_inputs_true_keeps_everything(): + tok = _qwen_tokenizer() + strategy = Qwen2VLProcessingStrategy(_Processor(tok), train_on_inputs=True) + seq = [101, 106, 103, 7, 104, 101, 102, 103, 8, 104] + labels = strategy.process_labels(torch.tensor([seq])) + assert labels.tolist()[0] == seq + + +# --------------------------------------------------------------------------- # +# Gemma3 / Gemma3n +# --------------------------------------------------------------------------- # + + +def _gemma_tokenizer(): + vocab = { + "model\n": [1, 2, 3], + "user\n": [1, 10, 3], + "system\n": [1, 11, 3], + "": [4], + "": [50], # boi_token for Gemma3 + } + tok = _Tokenizer(vocab, pad_id=0) + # boi_token is a direct tokenizer attribute on real Gemma3. + tok.boi_token = "" + return tok + + +def test_gemma3_scanner_plus_soft_image_token(): + strategy = Gemma3ProcessingStrategy(_Processor(_gemma_tokenizer())) + seq = [1, 10, 3, 7, 4, 1, 2, 3, 50, 8, 262144, 4] + labels = strategy.process_labels(torch.tensor([seq])) + # boi(50) and soft-image-token(262144) masked post-scan. + assert labels.tolist()[0] == [ + -100, + -100, + -100, + -100, + -100, + -100, + -100, + -100, + -100, + 8, + -100, + 4, + ] + + +def test_gemma3n_masks_image_and_audio_attrs(): + tok = _gemma_tokenizer() + # Gemma3n exposes these as integer attrs on the tokenizer. + tok.image_token_id = 70 + tok.audio_token_id = 71 + tok.boi_token_id = 72 + tok.eoi_token_id = 73 + strategy = Gemma3nProcessingStrategy(_Processor(tok)) + seq = [1, 2, 3, 70, 71, 72, 73, 9, 4] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100, -100, -100, -100, -100, -100, -100, 9, 4] + + +# --------------------------------------------------------------------------- # +# Gemma 4 +# --------------------------------------------------------------------------- # + + +class _FakeGemma4Tokenizer(_Tokenizer): + """Mirrors google/gemma-4-E2B-it token layout. Gemma4 role-start markers + include the trailing newline so the boundary matches the jinja template.""" + + VOCAB = { + "<|turn>model\n": [105, 4368, 108], + "<|turn>user\n": [105, 7777, 108], + "<|turn>system\n": [105, 8888, 108], + "": [106], + "<|image|>": [258880], + "<|video|>": [258884], + "<|audio|>": [258881], + "<|image>": [255999], + "": [258882], + "<|audio>": [256000], + "": [258883], + } + + def __init__(self): + # Pass a fresh dict so per-instance mutations (should any future + # code path introduce them) cannot leak across tests via the + # shared class-level VOCAB. + super().__init__( + {token: list(ids) for token, ids in self.VOCAB.items()}, + pad_id=0, + unk_id=3, + ) + + +class _FakeGemma4Processor: + def __init__(self): + self.tokenizer = _FakeGemma4Tokenizer() + self.tokenizer.image_token_id = self.tokenizer.vocab["<|image|>"][0] + self.tokenizer.audio_token_id = self.tokenizer.vocab["<|audio|>"][0] + self.image_token = "<|image|>" + self.image_token_id = self.tokenizer.vocab["<|image|>"][0] + self.boi_token = "<|image>" + self.eoi_token = "" + self.video_token = "<|video|>" + self.video_token_id = self.tokenizer.vocab["<|video|>"][0] + self.audio_token = "<|audio|>" + self.audio_token_id = self.tokenizer.vocab["<|audio|>"][0] + self.boa_token = "<|audio>" + self.eoa_token = "" + + +def test_gemma4_masks_everything_outside_assistant_span(): + strategy = Gemma4ProcessingStrategy(_FakeGemma4Processor()) + V = strategy.processor.tokenizer.vocab + user_start = V["<|turn>user\n"] + model_start = V["<|turn>model\n"] + turn_end = V[""][0] + seq = [ + 0, + *user_start, + 4444, + turn_end, + *model_start, + 5555, + turn_end, + 9999, + ] + labels = strategy.process_labels(torch.tensor([seq])) + expected = [-100] * (1 + len(user_start) + 1 + 1 + len(model_start)) + [ + 5555, + turn_end, + -100, + ] + assert labels.tolist()[0] == expected + + +def test_gemma4_masks_media_tokens_inside_assistant_span(): + strategy = Gemma4ProcessingStrategy(_FakeGemma4Processor()) + V = strategy.processor.tokenizer.vocab + model_start = V["<|turn>model\n"] + media = [ + V["<|image|>"][0], + V["<|video|>"][0], + V["<|audio|>"][0], + V["<|image>"][0], + V[""][0], + V["<|audio>"][0], + V[""][0], + ] + turn_end = V[""][0] + seq = [*model_start, *media, 9999, turn_end] + labels = strategy.process_labels(torch.tensor([seq])) + expected = [-100] * (len(model_start) + len(media)) + [9999, turn_end] + assert labels.tolist()[0] == expected + + +def test_gemma4_multiple_assistant_turns(): + strategy = Gemma4ProcessingStrategy(_FakeGemma4Processor()) + V = strategy.processor.tokenizer.vocab + turn_end = V[""][0] + + def user_turn(x): + return [*V["<|turn>user\n"], x, turn_end] + + def model_turn(x): + return [*V["<|turn>model\n"], x, turn_end] + + seq = user_turn(1111) + model_turn(2222) + user_turn(3333) + model_turn(4444) + labels = strategy.process_labels(torch.tensor([seq])) + kept = [t for t in labels.tolist()[0] if t != -100] + assert kept == [2222, turn_end, 4444, turn_end] + + +# --------------------------------------------------------------------------- # +# Llama 3.2 Vision / Llama 4 +# --------------------------------------------------------------------------- # + + +def test_llama3_2_vision_assistant_masking(): + vocab = { + "<|start_header_id|>assistant<|end_header_id|>\n\n": [1, 2, 3, 4, 5], + "<|start_header_id|>user<|end_header_id|>\n\n": [1, 2, 6, 4, 5], + "<|start_header_id|>system<|end_header_id|>\n\n": [1, 2, 7, 4, 5], + "<|start_header_id|>tool<|end_header_id|>\n\n": [1, 2, 8, 4, 5], + "<|start_header_id|>ipython<|end_header_id|>\n\n": [1, 2, 9, 4, 5], + "<|eot_id|>": [10], + } + strategy = Llama3_2VisionProcessingStrategy(_Processor(_Tokenizer(vocab, pad_id=0))) + seq = [1, 2, 6, 4, 5, 11, 10, 1, 2, 3, 4, 5, 12, 10] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100] * 12 + [12, 10] + + +def test_llama4_assistant_masking(): + vocab = { + "<|header_start|>assistant<|header_end|>\n\n": [20, 21, 22, 23], + "<|header_start|>user<|header_end|>\n\n": [20, 21, 24, 23], + "<|header_start|>system<|header_end|>\n\n": [20, 21, 25, 23], + "<|header_start|>tool<|header_end|>\n\n": [20, 21, 26, 23], + "<|header_start|>ipython<|header_end|>\n\n": [20, 21, 27, 23], + "<|eot|>": [30], + } + strategy = Llama4ProcessingStrategy(_Processor(_Tokenizer(vocab, pad_id=0))) + seq = [20, 21, 24, 23, 100, 30, 20, 21, 22, 23, 200, 30] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100] * 10 + [200, 30] + + +# --------------------------------------------------------------------------- # +# Pixtral / Mistral v7 Tekken (eos-terminated assistant) +# --------------------------------------------------------------------------- # + + +def test_pixtral_assistant_terminates_at_eos(): + # [/INST] is both user-end and assistant-start. Scanner backs up when + # user.include_end=False so the next iteration picks [/INST] up as + # assistant-start (Pixtral-specific handling in _build_role_boundaries). + vocab = { + "[INST]": [50], + "[/INST]": [51], + } + tok = _Tokenizer(vocab, pad_id=0, eos_id=99) + strategy = PixtralProcessingStrategy(_Processor(tok)) + seq = [50, 7, 51, 8, 8, 99] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + # Full-sequence expectation: user span masked; assistant content + eos kept. + assert out == [-100, -100, -100, 8, 8, 99] + + +def test_mistral_v7_tekken_system_user_assistant(): + vocab = { + "[SYSTEM_PROMPT]": [40], + "[/SYSTEM_PROMPT]": [41], + "[INST]": [50], + "[/INST]": [51], + } + tok = _Tokenizer(vocab, pad_id=0, eos_id=99) + strategy = MistralV7TekkenProcessingStrategy(_Processor(tok)) + seq = [40, 5, 41, 50, 7, 51, 8, 99] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + # Full-sequence expectation: system + user spans masked; assistant kept. + assert out == [-100, -100, -100, -100, -100, -100, 8, 99] + + +def test_pixtral_train_on_eos_all_respects_user_include_end_false(): + """Pixtral [/INST] (user-end include_end=False) stays masked on 'all'.""" + vocab = {"[INST]": [50], "[/INST]": [51]} + tok = _Tokenizer(vocab, pad_id=0, eos_id=99) + strategy = PixtralProcessingStrategy(_Processor(tok), train_on_eos="all") + seq = [50, 7, 51, 8, 8, 99] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + # [/INST] at idx 2 must stay masked — user.include_end=False says so. + # Assistant content (8, 8) + EOS (99) are unmasked as normal. + assert out == [-100, -100, -100, 8, 8, 99] + + +def test_mistral_v7_tekken_train_on_eos_all_respects_user_include_end_false(): + """System end (include_end=True) unmasked on 'all'; [/INST] stays masked.""" + vocab = { + "[SYSTEM_PROMPT]": [40], + "[/SYSTEM_PROMPT]": [41], + "[INST]": [50], + "[/INST]": [51], + } + tok = _Tokenizer(vocab, pad_id=0, eos_id=99) + strategy = MistralV7TekkenProcessingStrategy(_Processor(tok), train_on_eos="all") + seq = [40, 5, 41, 50, 7, 51, 8, 99] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + # system content masked, [/SYSTEM_PROMPT]=41 kept (include_end=True + all); + # user + [/INST]=51 masked (include_end=False); assistant 8 + eos 99 kept. + assert out == [-100, -100, 41, -100, -100, -100, 8, 99] + + +# --------------------------------------------------------------------------- # +# Dispatcher routing +# --------------------------------------------------------------------------- # + + +def _dispatch(processor, chat_template_type): + return get_processing_strategy( + processor=processor, + chat_template=None, + chat_template_type=chat_template_type, + ) + + +def test_dispatch_qwen2_vl(): + s = _dispatch(_Processor(_qwen_tokenizer()), "qwen2_vl") + assert isinstance(s, Qwen2VLProcessingStrategy) + + +def test_dispatch_qwen3_5(): + s = _dispatch(_Processor(_qwen_tokenizer()), "qwen3_5") + assert isinstance(s, Qwen3_5ProcessingStrategy) + + +def test_dispatch_gemma3(): + s = _dispatch(_Processor(_gemma_tokenizer()), "gemma3") + assert isinstance(s, Gemma3ProcessingStrategy) + + +def test_dispatch_gemma3n(): + s = _dispatch(_Processor(_gemma_tokenizer()), "gemma3n") + assert isinstance(s, Gemma3nProcessingStrategy) + + +def test_dispatch_gemma4(): + s = _dispatch(_FakeGemma4Processor(), "gemma4") + assert isinstance(s, Gemma4ProcessingStrategy) + + +def test_dispatch_llama3_2_vision(): + vocab = { + "<|start_header_id|>assistant<|end_header_id|>\n\n": [1, 2, 3, 4, 5], + "<|eot_id|>": [10], + } + s = _dispatch(_Processor(_Tokenizer(vocab, pad_id=0)), "llama3_2_vision") + assert isinstance(s, Llama3_2VisionProcessingStrategy) + + +def test_dispatch_llama4(): + vocab = { + "<|header_start|>assistant<|header_end|>\n\n": [20, 21, 22, 23], + "<|eot|>": [30], + } + s = _dispatch(_Processor(_Tokenizer(vocab, pad_id=0)), "llama4") + assert isinstance(s, Llama4ProcessingStrategy) + + +def test_dispatch_pixtral(): + vocab = {"[INST]": [50], "[/INST]": [51]} + s = _dispatch(_Processor(_Tokenizer(vocab, pad_id=0, eos_id=99)), "pixtral") + assert isinstance(s, PixtralProcessingStrategy) + + +def test_dispatch_mistral_v7_tekken(): + vocab = { + "[INST]": [50], + "[/INST]": [51], + "[SYSTEM_PROMPT]": [40], + "[/SYSTEM_PROMPT]": [41], + } + s = _dispatch( + _Processor(_Tokenizer(vocab, pad_id=0, eos_id=99)), "mistral_v7_tekken" + ) + assert isinstance(s, MistralV7TekkenProcessingStrategy) + + +def test_dispatch_unknown_falls_back_to_base(): + vocab = {"dummy": [1]} + s = _dispatch(_Processor(_Tokenizer(vocab, pad_id=0)), "llava") + assert type(s) is ProcessingStrategy + + +def _glm_vision_processor(cls_path): + """Spec'd MagicMock so isinstance(mock, cls) passes without real HF files.""" + from importlib import import_module + from unittest.mock import MagicMock + + mod_name, cls_name = cls_path.rsplit(".", 1) + cls = getattr(import_module(mod_name), cls_name) + + vocab = { + "<|image|>": [200], + "<|begin_of_image|>": [201], + "<|end_of_image|>": [202], + "<|video|>": [210], + "<|begin_of_video|>": [211], + "<|end_of_video|>": [212], + } + tok = _Tokenizer(vocab, pad_id=0) + proc = MagicMock(spec=cls) + proc.tokenizer = tok + # Drop processor.image_token so base class skips its probe. + del proc.image_token + return proc + + +def test_dispatch_glm4v_via_Glm4vProcessor(): + """Glm4vProcessor (GLM-4V) routes to Glm4vProcessingStrategy.""" + pytest.importorskip("transformers.models.glm4v.processing_glm4v") + from axolotl.processing_strategies import Glm4vProcessingStrategy + + proc = _glm_vision_processor( + "transformers.models.glm4v.processing_glm4v.Glm4vProcessor" + ) + s = _dispatch(proc, None) + assert isinstance(s, Glm4vProcessingStrategy) + + +def test_dispatch_glm4v_via_Glm46VProcessor(): + """Glm46VProcessor (GLM-4.6V) also routes to Glm4vProcessingStrategy.""" + pytest.importorskip("transformers.models.glm46v.processing_glm46v") + from axolotl.processing_strategies import Glm4vProcessingStrategy + + proc = _glm_vision_processor( + "transformers.models.glm46v.processing_glm46v.Glm46VProcessor" + ) + s = _dispatch(proc, None) + assert isinstance(s, Glm4vProcessingStrategy) + + +# --------------------------------------------------------------------------- # +# Config-based role-boundary override +# --------------------------------------------------------------------------- # + + +def test_role_boundaries_override_replaces_built_in(): + """Override swaps the built-in boundaries wholesale, not additively.""" + vocab = { + "<|im_start|>assistant\n": [101, 102, 103], + "<|im_start|>user\n": [101, 106, 103], + "<|im_end|>": [104], + ">>>A": [200, 201], + ">>>U": [200, 202], + "<<<": [210], + "<|image_pad|>": [250], + } + strategy = Qwen2VLProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[ + {"role": "assistant", "start": ">>>A", "end": "<<<"}, + {"role": "user", "start": ">>>U", "end": "<<<"}, + ], + ) + seq = [ + 101, + 106, + 103, + 7, + 104, + 200, + 201, + 9, + 9, + 210, + ] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100, -100, -100, -100, -100, -100, -100, 9, 9, 210] + + +def test_role_boundaries_override_enables_unverified_strategy(): + """Override lets users opt in to role masking on strategies that default opt out.""" + vocab = { + "BOA": [50, 51], + "EOT": [60], + } + strategy = ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[ + {"role": "assistant", "start": "BOA", "end": "EOT"}, + ], + ) + seq = [1, 2, 3, 50, 51, 7, 8, 60, 9] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100, -100, -100, -100, -100, 7, 8, 60, -100] + + +def test_role_boundaries_override_eos_token_sentinel(): + vocab = {"BOA": [50]} + tok = _Tokenizer(vocab, pad_id=0, eos_id=99) + strategy = ProcessingStrategy( + _Processor(tok), + role_boundaries_override=[ + {"role": "assistant", "start": "BOA", "end": "eos_token"}, + ], + ) + seq = [1, 50, 7, 7, 99, 2] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100, -100, 7, 7, 99, -100] + + +def test_role_boundaries_override_end_null_runs_to_sequence_end(): + vocab = {"BOA": [50]} + strategy = ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[ + {"role": "assistant", "start": "BOA", "end": None}, + ], + ) + seq = [1, 2, 50, 7, 8, 9] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100, -100, -100, 7, 8, 9] + + +def test_role_boundaries_override_rejects_bad_spec(): + vocab = {"BOA": [50]} + with pytest.raises(ValueError, match="must have both 'role' and 'start'"): + ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[{"role": "assistant"}], + ) + + +def test_role_boundaries_override_rejects_unencodable_start(): + vocab = {"BOA": [50]} + with pytest.raises(ValueError, match="tokenizes to an empty sequence"): + ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[ + {"role": "assistant", "start": "MISSING", "end": None} + ], + ) + + +def test_role_boundaries_override_rejects_unencodable_end(): + vocab = {"BOA": [50]} + with pytest.raises(ValueError, match="tokenizes to an empty sequence"): + ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[ + {"role": "assistant", "start": "BOA", "end": "MISSING"} + ], + ) + + +def test_role_boundaries_override_accepts_pydantic_models(): + # cfg.role_boundaries arrives as RoleBoundarySpec after pydantic parsing. + from axolotl.utils.schemas.multimodal import RoleBoundarySpec + + vocab = {"BOA": [50], "EOT": [60]} + strategy = ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[ + RoleBoundarySpec(role="assistant", start="BOA", end="EOT") + ], + ) + assert len(strategy.role_boundaries) == 1 + assert strategy.role_boundaries[0].role == "assistant" + assert strategy.role_boundaries[0].start_tokens == [50] + assert strategy.role_boundaries[0].end_tokens == [60] + + +def test_base_strategy_warns_when_no_boundaries(axolotl_caplog): + """No boundaries + train_on_inputs=False: one-shot warning, labels unchanged.""" + import axolotl.processing_strategies as mod + + mod._ROLE_MASK_WARNED.discard("ProcessingStrategy") + + vocab = {"dummy": [1]} + s = ProcessingStrategy(_Processor(_Tokenizer(vocab, pad_id=0))) + + with axolotl_caplog.at_level( + logging.WARNING, logger="axolotl.processing_strategies" + ): + labels = s.process_labels(torch.tensor([[1, 2, 3]])) + assert labels.tolist() == [[1, 2, 3]] + assert any("role boundaries" in rec.message for rec in axolotl_caplog.records) + + +# --------------------------------------------------------------------------- # +# Additional edge-case coverage +# --------------------------------------------------------------------------- # + + +def test_scanner_batch_size_greater_than_one(): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[1, 2], end_tokens=[9]), + RoleBoundary(role="user", start_tokens=[1, 3], end_tokens=[9]), + ] + labels = torch.tensor( + [ + [1, 3, 7, 9, 1, 2, 8, 9], + [1, 2, 5, 5, 9, 0, 0, 0], + ] + ) + out = _apply_role_boundaries(labels, boundaries, {"assistant"}, "turn").tolist() + assert out[0] == [-100, -100, -100, -100, -100, -100, 8, 9] + assert out[1] == [-100, -100, 5, 5, 9, -100, -100, -100] + + +def test_scanner_adjacent_trainable_turns(): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[1, 2], end_tokens=[9]), + ] + seq = [1, 2, 5, 9, 1, 2, 6, 9] + out = _scan(boundaries, seq) + assert out == [-100, -100, 5, 9, -100, -100, 6, 9] + + +def test_scanner_train_on_eos_none_multi_turn(): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[1, 2], end_tokens=[9]), + RoleBoundary(role="user", start_tokens=[1, 3], end_tokens=[9]), + ] + seq = [1, 3, 7, 9, 1, 2, 8, 9, 1, 3, 7, 9, 1, 2, 6, 9] + out = _scan(boundaries, seq, train_on_eos="none") + assert out == [ + -100, + -100, + -100, + -100, + -100, + -100, + 8, + -100, + -100, + -100, + -100, + -100, + -100, + -100, + 6, + -100, + ] + + +def test_scanner_train_on_eos_all_with_user_turn_no_end_marker(): + """Unclosed non-trainable span with train_on_eos='all': nothing included, no crash.""" + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[1, 2], end_tokens=[9]), + RoleBoundary(role="user", start_tokens=[1, 3], end_tokens=[9]), + ] + seq = [1, 3, 7, 7, 7] + out = _scan(boundaries, seq, train_on_eos="all") + assert out == [-100, -100, -100, -100, -100] + + +def test_scanner_include_start_true_via_override(): + vocab = {"BOA": [50, 51], "EOT": [60]} + strategy = ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[ + { + "role": "assistant", + "start": "BOA", + "end": "EOT", + "include_start": True, + }, + ], + ) + seq = [1, 50, 51, 7, 8, 60, 9] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100, 50, 51, 7, 8, 60, -100] + + +def test_scanner_include_end_false_via_override(): + """include_end=False drops end marker even with train_on_eos='turn'.""" + vocab = {"BOA": [50], "EOT": [60]} + strategy = ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[ + { + "role": "assistant", + "start": "BOA", + "end": "EOT", + "include_end": False, + }, + ], + ) + seq = [1, 50, 7, 8, 60, 9] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100, -100, 7, 8, -100, -100] + + +def test_scanner_empty_start_tokens_is_defensive_noop(): + """Defensive: empty start_tokens matches nothing; everything masked.""" + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[], end_tokens=[9]), + ] + seq = [1, 2, 3, 4, 9] + out = _scan(boundaries, seq) + assert out == [-100] * 5 + + +def test_process_labels_masks_pad_inside_assistant_span(): + """Pad inside a trainable span is still masked post-scan.""" + strategy = _make_qwen2vl() + seq = [101, 102, 103, 8, 0, 8, 104] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100, -100, -100, 8, -100, 8, 104] + + +def test_process_labels_all_pad_sequence_does_not_crash(): + strategy = _make_qwen2vl() + seq = [0, 0, 0, 0] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100, -100, -100, -100] + + +def test_qwen2vl_multiple_consecutive_assistant_turns(): + strategy = _make_qwen2vl() + seq = [101, 102, 103, 8, 104, 101, 102, 103, 9, 104] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [ + -100, + -100, + -100, + 8, + 104, + -100, + -100, + -100, + 9, + 104, + ] + + +def test_qwen2vl_batch_of_two_rows(): + strategy = _make_qwen2vl() + row_a = [101, 106, 103, 7, 104, 101, 102, 103, 8, 104] + row_b = [101, 102, 103, 9, 104, 0, 0, 0, 0, 0] + out = strategy.process_labels(torch.tensor([row_a, row_b])).tolist() + assert out[0] == [-100, -100, -100, -100, -100, -100, -100, -100, 8, 104] + assert out[1] == [-100, -100, -100, 9, 104, -100, -100, -100, -100, -100] + + +def test_qwen3_5_train_on_inputs_true_still_masks_video_pad(): + """train_on_inputs=True skips role masking but media tokens are still masked.""" + tok = _qwen_tokenizer() + strategy = Qwen3_5ProcessingStrategy(_Processor(tok), train_on_inputs=True) + seq = [101, 106, 103, 201, 7, 104, 101, 102, 103, 201, 8, 104] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + expected = list(seq) + expected[3] = -100 + expected[9] = -100 + assert out == expected + + +def test_role_boundaries_override_role_not_in_roles_to_train(): + """Override covering only a non-trainable role masks everything.""" + vocab = {"BOU": [50], "EOT": [60]} + strategy = ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[ + {"role": "user", "start": "BOU", "end": "EOT"}, + ], + ) + seq = [1, 50, 7, 8, 60, 9] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100] * 6 + + +def test_role_boundaries_override_include_start_flag_round_trips(): + from axolotl.utils.schemas.multimodal import RoleBoundarySpec + + vocab = {"BOA": [50], "EOT": [60]} + strategy = ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=[ + RoleBoundarySpec( + role="assistant", start="BOA", end="EOT", include_start=True + ), + ], + ) + assert len(strategy.role_boundaries) == 1 + assert strategy.role_boundaries[0].include_start is True + assert strategy.role_boundaries[0].include_end is True + + +def test_multimodal_config_parses_dict_role_boundaries_to_specs(): + from axolotl.utils.schemas.multimodal import ( + MultiModalConfig, + RoleBoundarySpec, + ) + + cfg = MultiModalConfig( + role_boundaries=[ + {"role": "assistant", "start": "BOA", "end": "EOT"}, + {"role": "user", "start": "BOU", "end": "EOT"}, + ] + ) + assert cfg.role_boundaries is not None + assert len(cfg.role_boundaries) == 2 + assert all(isinstance(rb, RoleBoundarySpec) for rb in cfg.role_boundaries) + + vocab = {"BOA": [50], "BOU": [51], "EOT": [60]} + strategy = ProcessingStrategy( + _Processor(_Tokenizer(vocab, pad_id=0)), + role_boundaries_override=cfg.role_boundaries, + ) + seq = [51, 7, 60, 50, 8, 60] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100, -100, -100, -100, 8, 60] From e2f01de0e8ebf88cabea7281975bfcade8080693 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sat, 9 May 2026 17:52:35 -0400 Subject: [PATCH 1292/1405] Fix Axolotl ReLoRA optimizer reset scope (#3646) * Fix Axolotl ReLoRA optimizer reset scope * fix: make relora reset method honor relora_prune_ratio When relora_prune_method='reset' and relora_prune_ratio is explicitly set, the ratio was silently ignored and replaced with the hardcoded _FULL_RESET_RATIO (0.999). Fix by moving the default-ratio logic to ReLoRACallback.on_step_begin: None maps to _FULL_RESET_RATIO for reset and 0.9 for other methods. reset_optimizer now uses the same random pruning path for both 'random' and 'reset'. Also consolidate three-layer default mismatch: schema default for relora_prune_method is now 'magnitude' (single canonical source); dataclass defaults for both fields changed to None to eliminate the conflicting fallback layer. Tests updated: removed the test case that verified the old broken behavior (reset ignoring ratio), added two cases proving reset honors the passed ratio. E2E reset fixture now uses ratio=0.5 to make it unambiguous that the ratio is honored. * Fix ReLoRA uint8 pruning regression --------- Signed-off-by: Wing Lian Co-authored-by: Axolotl Swarm --- .github/workflows/tests.yml | 2 +- src/axolotl/core/builders/causal.py | 6 +- src/axolotl/core/training_args_base.py | 15 +- src/axolotl/monkeypatch/relora.py | 98 +++++++++---- src/axolotl/utils/schemas/peft.py | 22 ++- tests/e2e/solo/test_relora_llama.py | 67 ++++++++- tests/monkeypatch/test_relora.py | 186 +++++++++++++++++++++++++ 7 files changed, 360 insertions(+), 36 deletions(-) create mode 100644 tests/monkeypatch/test_relora.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e21e60ab57..6b298ade02 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,7 +72,7 @@ jobs: exclude: - python_version: "3.14" pytorch_version: "2.9.1" - timeout-minutes: 20 + timeout-minutes: 25 steps: - name: cleanup node diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index aa1678523a..b5f365bce7 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -286,10 +286,14 @@ def build(self, total_num_steps): ) if self.cfg.relora and self.cfg.jagged_restart_steps: - if self.cfg.relora_prune_ratio: + if self.cfg.relora_prune_ratio is not None: training_arguments_kwargs["relora_prune_ratio"] = ( self.cfg.relora_prune_ratio ) + if self.cfg.relora_prune_method: + training_arguments_kwargs["relora_prune_method"] = ( + self.cfg.relora_prune_method + ) if self.cfg.jagged_restart_steps: training_arguments_kwargs["jagged_restart_steps"] = ( diff --git a/src/axolotl/core/training_args_base.py b/src/axolotl/core/training_args_base.py index 427a80a468..e9727dbb16 100644 --- a/src/axolotl/core/training_args_base.py +++ b/src/axolotl/core/training_args_base.py @@ -83,13 +83,18 @@ class AxolotlTrainingMixins: default=None, metadata={"help": "The number of processes to use for data processing"}, ) - relora_steps: Optional[int] = field( + relora_prune_ratio: Optional[float] = field( default=None, - metadata={"help": "how often to reset for ReLoRA"}, + metadata={ + "help": ( + "prune ratio for optimizer state pruning; " + "defaults to 0.999 for reset method, 0.9 for others" + ) + }, ) - relora_prune_ratio: Optional[float] = field( - default=0.9, - metadata={"help": "prune ratio for magnitude pruning of the optimizer"}, + relora_prune_method: Optional[str] = field( + default=None, + metadata={"help": "optimizer state pruning method: magnitude | random | reset"}, ) jagged_restart_steps: Optional[int] = field( default=None, diff --git a/src/axolotl/monkeypatch/relora.py b/src/axolotl/monkeypatch/relora.py index cf93c32dda..3d33ab204d 100644 --- a/src/axolotl/monkeypatch/relora.py +++ b/src/axolotl/monkeypatch/relora.py @@ -6,9 +6,8 @@ import shutil from functools import partial from pathlib import Path -from typing import Dict, List, Union +from typing import Dict, List, Literal, Union -import bitsandbytes as bnb import peft import safetensors.torch as st import torch @@ -28,9 +27,15 @@ LOG = get_logger(__name__) +try: + import bitsandbytes as bnb +except ImportError: # pragma: no cover - optional dependency for 8-bit merge paths + bnb = None + @torch.no_grad() def magnitude_pruning_(tensor, prune_ratio): + """Zero the lowest ``prune_ratio`` fraction of values by absolute magnitude, in place.""" tensor_magnitude = torch.abs(tensor) threshold = torch.quantile( tensor_magnitude.flatten().to(dtype=torch.float32), prune_ratio @@ -40,15 +45,43 @@ def magnitude_pruning_(tensor, prune_ratio): tensor.mul_(mask.to(dtype=tensor.dtype)) +@torch.no_grad() +def random_pruning_(tensor, prune_ratio): + """Zero a random ``prune_ratio`` fraction of values, in place.""" + mask = ( + torch.rand(tensor.shape, dtype=torch.float32, device=tensor.device) + > prune_ratio + ) + tensor.mul_(mask.to(dtype=tensor.dtype)) + + +# 0.999 mirrors the reference implementation. True zeroing breaks +# ZeroRedundancyOptimizer.consolidate_state_dict; see Guitaricet/relora's +# peft_pretraining/training_utils.py for the original note on this. +_FULL_RESET_RATIO = 0.999 + + def reset_optimizer( optimizer: torch.optim.Optimizer, *, - reset_params: List[str], # where str is the key to a torch.nn.Parameter + reset_params: List[torch.nn.Parameter], optimizer_state_keys: List[str], - optimizer_magnitude_pruning: float = 0.9, + prune_method: Literal["magnitude", "random", "reset"] = "magnitude", + prune_ratio: float = 0.9, ): - # pylint:disable=unused-argument - pruning_fn = partial(magnitude_pruning_, prune_ratio=optimizer_magnitude_pruning) + """Prune optimizer state for ``reset_params`` only.""" + if prune_method == "magnitude": + pruning_fn = partial(magnitude_pruning_, prune_ratio=prune_ratio) + elif prune_method in ("random", "reset"): + # "reset" is random pruning at a near-full ratio; the caller is responsible + # for supplying the appropriate prune_ratio (see ReLoRACallback.on_step_begin). + pruning_fn = partial(random_pruning_, prune_ratio=prune_ratio) + else: + raise ValueError( + f"Unknown prune_method {prune_method!r}; expected one of " + "'magnitude', 'random', 'reset'" + ) + n_zeros = 0 n_total = 0 @@ -56,22 +89,22 @@ def reset_optimizer( if isinstance(optimizer, ZeroRedundancyOptimizer): optimizer_state = optimizer.optim.state - for group in optimizer.param_groups: - for param in group["params"]: - state = optimizer_state[param] - for key, value in state.items(): - if key not in optimizer_state_keys: + for param in reset_params: + state = optimizer_state.get(param, {}) + if not state: + continue + for key in optimizer_state_keys: + value = state.get(key) + if value is None or not torch.is_tensor(value): + continue + try: + pruning_fn(value) + n_total += value.numel() + n_zeros += torch.sum(value == 0).item() + except RuntimeError as exc: + if "quantile() input tensor is too large" in str(exc): continue - if torch.is_tensor(value): - try: - pruning_fn(value) - n_total += value.numel() - n_zeros += torch.sum(value == 0).item() - except RuntimeError as exc: - if "quantile() input tensor is too large" in str(exc): - pass - else: - raise exc + raise _zeroed = n_zeros / (1e-7 + n_total) * 100 LOG.info(f"Percent of optimizer states zeroed: {_zeroed:.2f}") @@ -82,11 +115,12 @@ class ReLoRACallback(TrainerCallback): """Callback to merge LoRA weights into the base model and save full-weight checkpoints""" def __init__(self, cfg: DictDefault): - self.relora_steps = cfg.jagged_restart_steps + self.jagged_restart_steps = cfg.jagged_restart_steps self.cpu_offload = cfg.relora_cpu_offload self.quantized = cfg.load_in_4bit or cfg.load_in_8bit self.last_full_model = cfg.base_model self.resume_from_checkpoint = cfg.resume_from_checkpoint + self.prune_method = cfg.relora_prune_method or "magnitude" if not os.path.exists(self.last_full_model): self.last_full_model = str(Path(snapshot_download(cfg.base_model))) @@ -128,7 +162,7 @@ def on_step_begin( ): if not optimizer: optimizer = state.optimizer - if state.global_step > 0 and state.global_step % self.relora_steps == 0: + if state.global_step > 0 and state.global_step % self.jagged_restart_steps == 0: checkpoint_folder = os.path.join( args.output_dir, f"{PREFIX_CHECKPOINT_DIR}-{state.global_step}", @@ -144,7 +178,7 @@ def on_step_begin( raise ValueError(f"Optimizer {args.optim} not supported with ReLoRA") lora_params = [ - n + p for n, p in model.named_parameters() if p.requires_grad and "lora_" in n ] @@ -166,11 +200,19 @@ def on_step_begin( actually_save=is_main_process(), cpu_offload=self.cpu_offload, ) + # When relora_prune_ratio is not set, use _FULL_RESET_RATIO for + # "reset" (paper-style near-full reset) and 0.9 for other methods. + prune_ratio = args.relora_prune_ratio + if prune_ratio is None: + prune_ratio = ( + _FULL_RESET_RATIO if self.prune_method == "reset" else 0.9 + ) reset_optimizer( optimizer, reset_params=lora_params, optimizer_state_keys=optimizer_state_keys, - optimizer_magnitude_pruning=args.relora_prune_ratio, + prune_method=self.prune_method, + prune_ratio=prune_ratio, ) if self.quantized: @@ -191,8 +233,8 @@ def on_save( args.output_dir, f"{PREFIX_CHECKPOINT_DIR}-{state.global_step}", "relora" ) if ( - state.global_step >= self.relora_steps - and state.global_step % self.relora_steps != 0 + state.global_step >= self.jagged_restart_steps + and state.global_step % self.jagged_restart_steps != 0 ): if self.quantized: if is_main_process() and self.last_full_model != checkpoint_folder: @@ -320,6 +362,8 @@ def update_weights( target.weight.data = new_weight.cpu() target.to(device) elif isinstance(target, peft.tuners.lora.Linear8bitLt): + if bnb is None: + raise ImportError("bitsandbytes is required to merge 8-bit LoRA weights") target.weight.data = ( bnb.nn.Int8Params(new_weight, requires_grad=False).to(device).data ) diff --git a/src/axolotl/utils/schemas/peft.py b/src/axolotl/utils/schemas/peft.py index c60c548f03..42fa628e0f 100644 --- a/src/axolotl/utils/schemas/peft.py +++ b/src/axolotl/utils/schemas/peft.py @@ -240,8 +240,28 @@ class ReLoRAConfig(BaseModel): ) relora_prune_ratio: float | None = Field( default=None, + ge=0.0, + le=1.0, json_schema_extra={ - "description": "threshold for optimizer magnitude when pruning" + "description": ( + "Fraction of optimizer state values to zero on each ReLoRA restart. " + "When relora_prune_method='reset' and this is omitted, defaults to " + "0.999 (paper-style near-full reset). For other methods, defaults to 0.9." + ) + }, + ) + relora_prune_method: Literal["magnitude", "random", "reset"] | None = Field( + default="magnitude", + json_schema_extra={ + "description": ( + "Optimizer state pruning method on each ReLoRA restart. " + "'magnitude' (default) keeps top-k by absolute value; " + "'random' keeps a random subset at relora_prune_ratio; " + "'reset' uses near-full random pruning (default ratio 0.999, " + "honoring relora_prune_ratio when explicitly set). " + "Paper-style recipe: relora_prune_method='reset' with no " + "relora_prune_ratio, equivalent to 'random' with ratio=0.999." + ) }, ) relora_cpu_offload: bool | None = Field( diff --git a/tests/e2e/solo/test_relora_llama.py b/tests/e2e/solo/test_relora_llama.py index 091bb90c68..895f32d990 100644 --- a/tests/e2e/solo/test_relora_llama.py +++ b/tests/e2e/solo/test_relora_llama.py @@ -56,7 +56,72 @@ def test_relora(self, temp_dir): ], "warmup_steps": 10, "num_epochs": 2, - "max_steps": 105, # at least 2x relora_steps + "max_steps": 105, # at least 2x restart cadence + "micro_batch_size": 2, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "adamw_8bit", + "lr_scheduler": "cosine", + "use_tensorboard": True, + "save_first_step": False, + } + ) + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + + train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(Path(temp_dir) / "checkpoint-100/adapter", cfg) + assert (Path(temp_dir) / "checkpoint-100/relora/model.safetensors").exists(), ( + "Relora model checkpoint not found" + ) + + check_tensorboard( + temp_dir + "/runs", "train/grad_norm", 0.2, "grad_norm is too high" + ) + + @with_temp_dir + def test_relora_reset_method(self, temp_dir): + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 2048, + "sample_packing": True, + "pad_to_sequence_len": True, + "flash_attention": True, + "load_in_8bit": True, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.05, + "lora_target_modules": ["q_proj", "v_proj"], + "relora": True, + "jagged_restart_steps": 50, + "jagged_restart_warmup_steps": 10, + "jagged_restart_anneal_steps": 10, + "relora_prune_ratio": 0.5, # explicitly honored by reset (not ignored) + "relora_prune_method": "reset", + "relora_cpu_offload": True, + "val_set_size": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "chat_template": "chatml", + "datasets": [ + { + "path": "mlabonne/FineTome-100k", + "type": "chat_template", + "split": "train[:10%]", + "field_messages": "conversations", + "message_field_role": "from", + "message_field_content": "value", + }, + ], + "warmup_steps": 10, + "num_epochs": 2, + "max_steps": 105, "micro_batch_size": 2, "gradient_accumulation_steps": 1, "output_dir": temp_dir, diff --git a/tests/monkeypatch/test_relora.py b/tests/monkeypatch/test_relora.py new file mode 100644 index 0000000000..e722adf0cd --- /dev/null +++ b/tests/monkeypatch/test_relora.py @@ -0,0 +1,186 @@ +"""Unit tests for axolotl.monkeypatch.relora.reset_optimizer.""" + +import math + +import pytest +import torch +import torch.nn as nn + +from axolotl.monkeypatch.relora import ( + magnitude_pruning_, + random_pruning_, + reset_optimizer, +) + +ADAM_KEYS = ["exp_avg", "exp_avg_sq"] + + +def _build_optimizer_with_state(seed: int = 0): + """Build a tiny optimizer over LoRA-shaped + non-LoRA params with populated state.""" + torch.manual_seed(seed) + lora_a = nn.Parameter(torch.randn(8, 32)) + lora_b = nn.Parameter(torch.randn(32, 8)) + extra = nn.Parameter(torch.randn(64, 32)) + + optimizer = torch.optim.AdamW([lora_a, lora_b, extra], lr=1e-3) + for _ in range(2): + loss = ( + (lora_a * torch.randn_like(lora_a)).sum() + + (lora_b * torch.randn_like(lora_b)).sum() + + (extra * torch.randn_like(extra)).sum() + ) + loss.backward() + optimizer.step() + optimizer.zero_grad() + return optimizer, lora_a, lora_b, extra + + +def test_reset_optimizer_only_touches_reset_params(): + """State for params NOT in reset_params must be byte-identical after reset.""" + optimizer, lora_a, lora_b, extra = _build_optimizer_with_state() + + extra_avg_before = optimizer.state[extra]["exp_avg"].clone() + extra_avg_sq_before = optimizer.state[extra]["exp_avg_sq"].clone() + + reset_optimizer( + optimizer, + reset_params=[lora_a, lora_b], + optimizer_state_keys=ADAM_KEYS, + prune_method="magnitude", + prune_ratio=0.9, + ) + + assert torch.equal(optimizer.state[extra]["exp_avg"], extra_avg_before) + assert torch.equal(optimizer.state[extra]["exp_avg_sq"], extra_avg_sq_before) + + +def test_reset_optimizer_actually_prunes_lora_state(): + optimizer, lora_a, lora_b, _extra = _build_optimizer_with_state() + + reset_optimizer( + optimizer, + reset_params=[lora_a, lora_b], + optimizer_state_keys=ADAM_KEYS, + prune_method="magnitude", + prune_ratio=0.9, + ) + + for param in (lora_a, lora_b): + for key in ADAM_KEYS: + zero_frac = (optimizer.state[param][key] == 0).float().mean().item() + assert zero_frac >= 0.85 + + +@pytest.mark.parametrize( + "method,ratio,expected_zero_frac", + [ + ("magnitude", 0.9, 0.9), + ("magnitude", 0.99, 0.99), + ("random", 0.9, 0.9), + ("random", 0.5, 0.5), + # reset uses random pruning; relora_prune_ratio must be honored, not ignored. + ("reset", 0.9, 0.9), + ("reset", 0.5, 0.5), + ], +) +def test_prune_methods(method, ratio, expected_zero_frac): + """Each method zeros approximately the expected fraction.""" + optimizer, lora_a, lora_b, _extra = _build_optimizer_with_state(seed=42) + + reset_optimizer( + optimizer, + reset_params=[lora_a, lora_b], + optimizer_state_keys=ADAM_KEYS, + prune_method=method, + prune_ratio=ratio, + ) + + total = 0 + zeros = 0 + for param in (lora_a, lora_b): + for key in ADAM_KEYS: + tensor = optimizer.state[param][key] + total += tensor.numel() + zeros += (tensor == 0).sum().item() + + actual = zeros / total + tolerance = 0.02 if method == "magnitude" else 0.05 + assert math.isclose(actual, expected_zero_frac, abs_tol=tolerance) + + +def test_reset_optimizer_skips_keys_not_in_state_keys(): + """Keys present in optimizer state but not in optimizer_state_keys are untouched.""" + optimizer, lora_a, lora_b, _extra = _build_optimizer_with_state() + + exp_avg_sq_before = optimizer.state[lora_a]["exp_avg_sq"].clone() + + reset_optimizer( + optimizer, + reset_params=[lora_a, lora_b], + optimizer_state_keys=["exp_avg"], + prune_method="magnitude", + prune_ratio=0.9, + ) + + assert torch.equal(optimizer.state[lora_a]["exp_avg_sq"], exp_avg_sq_before) + + +def test_reset_optimizer_handles_param_with_empty_state(): + """Params with no optimizer state are skipped silently.""" + optimizer, lora_a, lora_b, _extra = _build_optimizer_with_state() + orphan = nn.Parameter(torch.randn(4, 4)) + + reset_optimizer( + optimizer, + reset_params=[lora_a, lora_b, orphan], + optimizer_state_keys=ADAM_KEYS, + prune_method="magnitude", + prune_ratio=0.9, + ) + + assert orphan not in optimizer.state or not optimizer.state[orphan] + + +def test_unknown_prune_method_raises(): + optimizer, lora_a, lora_b, _extra = _build_optimizer_with_state() + + with pytest.raises(ValueError, match="Unknown prune_method"): + reset_optimizer( + optimizer, + reset_params=[lora_a, lora_b], + optimizer_state_keys=ADAM_KEYS, + prune_method="bogus", # type: ignore[arg-type] + prune_ratio=0.9, + ) + + +def test_pruning_helpers_are_inplace(): + """magnitude_pruning_ and random_pruning_ must mutate via tensor.mul_.""" + tensor = torch.randn(64) + ptr_before = tensor.data_ptr() + magnitude_pruning_(tensor, 0.5) + assert tensor.data_ptr() == ptr_before + + tensor = torch.randn(64) + ptr_before = tensor.data_ptr() + random_pruning_(tensor, 0.5) + assert tensor.data_ptr() == ptr_before + + +def test_pruning_helpers_support_uint8_tensors(): + """Both pruning helpers must work on uint8 optimizer state tensors.""" + tensor = torch.arange(1, 129, dtype=torch.uint8) + magnitude_pruning_(tensor, 0.9) + + assert tensor.dtype == torch.uint8 + magnitude_zero_frac = (tensor == 0).float().mean().item() + assert 0.85 <= magnitude_zero_frac <= 0.95 + + tensor = torch.arange(1, 129, dtype=torch.uint8) + with torch.random.fork_rng(devices=[]): + torch.manual_seed(1234) + random_pruning_(tensor, 0.9) + + assert tensor.dtype == torch.uint8 + random_zero_frac = (tensor == 0).float().mean().item() + assert 0.85 <= random_zero_frac <= 0.95 From b7ec06b8a17966e2de405783db677fe6cc68835f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 12 May 2026 07:19:55 -0400 Subject: [PATCH 1293/1405] Add optional Axolotl MoRA/ReMoRA integration (#3647) [skip ci] * Add optional Axolotl MoRA/ReMoRA integration Co-authored-by: Axolotl Swarm * Isolate MoRA adapter behavior in plugin Co-authored-by: Axolotl Swarm * Constrain MoRA variants to supported enum values * Keep MoRA validation out of core config --------- Co-authored-by: Swarm Co-authored-by: Axolotl Swarm --- src/axolotl/integrations/base.py | 84 ++++++++- src/axolotl/integrations/mora/__init__.py | 6 + src/axolotl/integrations/mora/args.py | 66 +++++++ src/axolotl/integrations/mora/plugin.py | 97 ++++++++++ src/axolotl/loaders/adapter.py | 171 +++++++++++------- src/axolotl/loaders/model.py | 10 +- src/axolotl/train.py | 4 +- src/axolotl/utils/schemas/peft.py | 14 +- src/axolotl/utils/schemas/validation.py | 15 +- tests/integrations/mora/test_mora.py | 160 ++++++++++++++++ .../test_adapter_plugin_registry.py | 73 ++++++++ .../validation/mora/test_mora_validation.py | 100 ++++++++++ 12 files changed, 729 insertions(+), 71 deletions(-) create mode 100644 src/axolotl/integrations/mora/__init__.py create mode 100644 src/axolotl/integrations/mora/args.py create mode 100644 src/axolotl/integrations/mora/plugin.py create mode 100644 tests/integrations/mora/test_mora.py create mode 100644 tests/integrations/test_adapter_plugin_registry.py create mode 100644 tests/utils/schemas/validation/mora/test_mora_validation.py diff --git a/src/axolotl/integrations/base.py b/src/axolotl/integrations/base.py index 48260cb4fc..5d6517d8dc 100644 --- a/src/axolotl/integrations/base.py +++ b/src/axolotl/integrations/base.py @@ -23,9 +23,10 @@ import collections import importlib import traceback +from dataclasses import dataclass from typing import TYPE_CHECKING, Callable, OrderedDict, Union -from peft import PeftModel +from peft import PeftConfig, PeftMixedModel, PeftModel from torch import nn from torch.optim import Optimizer from torch.optim.lr_scheduler import LRScheduler @@ -41,6 +42,15 @@ from axolotl.common.datasets import TrainDatasetMeta +@dataclass(frozen=True) +class AdapterCapabilities: + """Capabilities for an adapter contributed by a plugin.""" + + name: str + lora_like: bool = False + relora: bool = False + + class BasePlugin: """Base class for all plugins. Defines the interface for plugin methods. @@ -91,6 +101,26 @@ def get_training_args_mixin(self) -> str | None: Returns a dataclass model for the plugin's training arguments. """ + def get_adapter_capabilities(self) -> list[AdapterCapabilities]: + """Returns adapter capabilities contributed by the plugin.""" + return [] + + def get_lora_config_kwargs(self, cfg: DictDefault) -> dict: + """Returns extra PEFT LoraConfig kwargs for plugin LoRA-like adapters.""" + return {} + + def load_adapter( + self, + model: PreTrainedModel, + cfg: DictDefault, + inference: bool = False, + config_only: bool = False, + ) -> ( + tuple[PreTrainedModel | PeftModel | PeftMixedModel | None, PeftConfig | None] + | None + ): + """Optionally load a plugin adapter instead of the generic loader.""" + def load_datasets( self, cfg: DictDefault, preprocess: bool = False ) -> Union["TrainDatasetMeta", None]: @@ -414,6 +444,58 @@ def get_training_args_mixin(self): training_args.append(training_args_from_plugin) return training_args + def adapter_capabilities(self) -> dict[str, AdapterCapabilities]: + """Returns adapter capabilities by adapter name.""" + capabilities = {} + for plugin in self.plugins.values(): + for adapter_capability in plugin.get_adapter_capabilities(): + capabilities[adapter_capability.name] = adapter_capability + return capabilities + + def get_adapter_capability(self, adapter: str) -> AdapterCapabilities | None: + """Returns capabilities for a registered plugin adapter.""" + return self.adapter_capabilities().get(adapter) + + def supports_adapter(self, adapter: str) -> bool: + """Returns whether a plugin has registered the adapter name.""" + return adapter in self.adapter_capabilities() + + def adapter_supports_relora(self, adapter: str) -> bool: + """Returns whether a plugin adapter supports ReLoRA restart semantics.""" + capability = self.get_adapter_capability(adapter) + return bool(capability and capability.relora) + + def get_lora_config_kwargs(self, cfg: DictDefault) -> dict: + """Returns extra LoraConfig kwargs from plugins for the configured adapter.""" + lora_config_kwargs = {} + for plugin in self.plugins.values(): + plugin_kwargs = plugin.get_lora_config_kwargs(cfg) + if plugin_kwargs: + lora_config_kwargs.update(plugin_kwargs) + return lora_config_kwargs + + def load_adapter( + self, + model: PreTrainedModel, + cfg: DictDefault, + inference: bool = False, + config_only: bool = False, + ) -> ( + tuple[PreTrainedModel | PeftModel | PeftMixedModel | None, PeftConfig | None] + | None + ): + """Returns the first plugin adapter loader result, if any.""" + for plugin in self.plugins.values(): + loaded = plugin.load_adapter( + model, + cfg, + inference=inference, + config_only=config_only, + ) + if loaded is not None: + return loaded + return None + def load_datasets( self, cfg: DictDefault, preprocess: bool = False ) -> Union["TrainDatasetMeta", None]: diff --git a/src/axolotl/integrations/mora/__init__.py b/src/axolotl/integrations/mora/__init__.py new file mode 100644 index 0000000000..8f50258e24 --- /dev/null +++ b/src/axolotl/integrations/mora/__init__.py @@ -0,0 +1,6 @@ +"""MoRA / ReMoRA integration for Axolotl.""" + +from .args import MoraArgs, MoraConfig, MoraType +from .plugin import MoraPlugin + +__all__ = ["MoraArgs", "MoraConfig", "MoraPlugin", "MoraType"] diff --git a/src/axolotl/integrations/mora/args.py b/src/axolotl/integrations/mora/args.py new file mode 100644 index 0000000000..01549e1db9 --- /dev/null +++ b/src/axolotl/integrations/mora/args.py @@ -0,0 +1,66 @@ +"""Config args for MoRA / ReMoRA.""" + +from __future__ import annotations + +from enum import Enum + +from pydantic import BaseModel, Field, model_validator + + +class MoraType(str, Enum): + """MoRA variants supported by the reference implementation.""" + + SHARING = "sharing" + ROPE = "rope" + + @property + def peft_value(self) -> int: + return { + MoraType.SHARING: 1, + MoraType.ROPE: 6, + }[self] + + +class MoraConfig(BaseModel): + """Nested MoRA configuration available under the `mora` key.""" + + use_mora: bool = Field( + default=True, + description=( + "Enable MoRA adapter construction. Requires a PEFT build with MoRA " + "support (for example, the MoRA fork)." + ), + ) + mora_type: MoraType = Field( + default=MoraType.ROPE, + description=( + "MoRA variant selector. Supported values are `sharing` for type 1 " + "and `rope` for type 6. Numeric values 1 and 6 are accepted for " + "backwards compatibility." + ), + ) + + @model_validator(mode="before") + @classmethod + def normalize_mora_type(cls, data): + if not isinstance(data, dict) or "mora_type" not in data: + return data + data = data.copy() + mora_type = data["mora_type"] + if mora_type == 1: + data["mora_type"] = MoraType.SHARING + elif mora_type == 6: + data["mora_type"] = MoraType.ROPE + return data + + +class MoraArgs(BaseModel): + """Plugin entry that exposes the nested `mora` block to the core config.""" + + mora: MoraConfig = Field( + default_factory=MoraConfig, + description=( + "MoRA / ReMoRA training configuration. Register the " + "`axolotl.integrations.mora.MoraPlugin` plugin to enable this block." + ), + ) diff --git a/src/axolotl/integrations/mora/plugin.py b/src/axolotl/integrations/mora/plugin.py new file mode 100644 index 0000000000..8ca6068c18 --- /dev/null +++ b/src/axolotl/integrations/mora/plugin.py @@ -0,0 +1,97 @@ +"""MoRA / ReMoRA plugin for Axolotl.""" + +import inspect + +from peft import LoraConfig, PeftModel +from transformers import PreTrainedModel + +from axolotl.integrations.base import AdapterCapabilities, BasePlugin +from axolotl.integrations.mora.args import MoraType +from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def _peft_supports_mora() -> bool: + try: + params = inspect.signature(LoraConfig).parameters + except (TypeError, ValueError): + return False + return "use_mora" in params and "mora_type" in params + + +def _mora_type_peft_value(mora_type: MoraType | str | int) -> int: + if isinstance(mora_type, MoraType): + return mora_type.peft_value + if mora_type == 1 or mora_type == MoraType.SHARING.value: + return MoraType.SHARING.peft_value + if mora_type == 6 or mora_type == MoraType.ROPE.value: + return MoraType.ROPE.peft_value + raise ValueError("mora_type must be one of `sharing`, `rope`, 1, or 6") + + +def _mora_type_label(mora_type: MoraType | str | int) -> str: + if isinstance(mora_type, MoraType): + return mora_type.value + if mora_type == 1: + return MoraType.SHARING.value + if mora_type == 6: + return MoraType.ROPE.value + return str(mora_type) + + +class MoraPlugin(BasePlugin): + """Plugin that exposes MoRA-specific config and validates runtime support.""" + + def get_input_args(self) -> str: + return "axolotl.integrations.mora.MoraArgs" + + def get_adapter_capabilities(self) -> list[AdapterCapabilities]: + return [AdapterCapabilities(name="mora", lora_like=True, relora=True)] + + def _validate_mora_config(self, cfg: DictDefault): + mora_cfg = getattr(cfg, "mora", None) + if mora_cfg is None: + raise ValueError("adapter: mora requires a nested mora configuration block") + if not getattr(mora_cfg, "use_mora", False): + raise ValueError("mora.use_mora must be true when adapter: mora is set") + if cfg.load_in_4bit or cfg.load_in_8bit: + raise ValueError( + "adapter: mora currently requires a full-precision base model. " + "Use adapter: lora or qlora for quantized training." + ) + if cfg.gptq: + raise ValueError( + "adapter: mora is not compatible with GPTQ quantized base models." + ) + + def get_lora_config_kwargs(self, cfg: DictDefault) -> dict: + if cfg.adapter != "mora": + return {} + self._validate_mora_config(cfg) + if not _peft_supports_mora(): + raise ImportError( + "adapter: mora requires a PEFT build with MoRA support " + "(LoraConfig(use_mora=..., mora_type=...)). " + "Install the MoRA fork or another PEFT distribution that exposes " + "those fields." + ) + mora_cfg = cfg.mora + return { + "use_mora": mora_cfg.use_mora, + "mora_type": _mora_type_peft_value(mora_cfg.mora_type), + } + + def pre_model_load(self, cfg: DictDefault): + if cfg.adapter != "mora": + return + LOG.info("MoRA plugin enabled for adapter: mora") + + def post_model_load(self, cfg: DictDefault, model: PreTrainedModel | PeftModel): + if cfg.adapter == "mora" and getattr(cfg, "mora", None): + LOG.debug( + "Loaded MoRA model with mora_type=%s, relora=%s", + _mora_type_label(cfg.mora.mora_type), + cfg.relora, + ) diff --git a/src/axolotl/loaders/adapter.py b/src/axolotl/loaders/adapter.py index 3d662c0bb3..71506f7403 100644 --- a/src/axolotl/loaders/adapter.py +++ b/src/axolotl/loaders/adapter.py @@ -19,12 +19,14 @@ ) from transformers import PreTrainedModel +from axolotl.integrations.base import PluginManager from axolotl.loaders.utils import get_linear_embedding_layers from axolotl.telemetry.errors import send_errors from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger LOG = get_logger(__name__) +PLUGIN_MANAGER = PluginManager.get_instance() def setup_quantized_meta_for_peft(model: torch.nn.Module): @@ -124,6 +126,76 @@ def _patched( LoraModel._axolotl_clippable_patched = True +def _get_peft_task_type(model: PreTrainedModel) -> TaskType: + model_cls = type(model).__name__ + if "SequenceClassification" in model_cls: + return TaskType.SEQ_CLS + if "TokenClassification" in model_cls: + return TaskType.TOKEN_CLS + return TaskType.CAUSAL_LM + + +def _build_lora_config_kwargs(cfg: DictDefault) -> dict[str, Any]: + lora_config_kwargs: dict[str, Any] = {} + loftq_bits = cfg.peft and cfg.peft.loftq_config and cfg.peft.loftq_config.loftq_bits + if loftq_bits: + lora_config_kwargs["loftq_config"] = LoftQConfig(loftq_bits=loftq_bits) + lora_config_kwargs["init_lora_weights"] = "loftq" + if cfg.peft_init_lora_weights: + lora_config_kwargs["init_lora_weights"] = cfg.peft_init_lora_weights + if cfg.peft_use_dora: + lora_config_kwargs["use_dora"] = cfg.peft_use_dora + LOG.info("Initializing LoRA weights using dora. This might take longer.") + if cfg.peft_use_rslora: + lora_config_kwargs["use_rslora"] = cfg.peft_use_rslora + if cfg.peft_layer_replication: + lora_config_kwargs["layer_replication"] = cfg.peft_layer_replication + if cfg.peft_trainable_token_indices: + lora_config_kwargs["trainable_token_indices"] = cfg.peft_trainable_token_indices + if cfg.peft_ensure_weight_tying is not None: + lora_config_kwargs["ensure_weight_tying"] = cfg.peft_ensure_weight_tying + + return lora_config_kwargs + + +def _build_peft_lora_config( + model: PreTrainedModel, + cfg: DictDefault, +) -> PeftConfig: + lora_target_modules = cfg.lora_target_modules or [] + lora_target_parameters = cfg.lora_target_parameters or [] + + if cfg.lora_target_linear: + linear_names = find_all_linear_names(model) + LOG.info(f"found linear modules: {repr(sorted(linear_names))}") + lora_target_modules_as_list = ( + lora_target_modules + if isinstance(lora_target_modules, list) + else [lora_target_modules] + ) + lora_target_modules = list(set(lora_target_modules_as_list + linear_names)) + + lora_config_kwargs = _build_lora_config_kwargs(cfg) + lora_config_kwargs.update(PLUGIN_MANAGER.get_lora_config_kwargs(cfg)) + + lora_config = LoraConfig( + r=cfg.lora_r, + lora_alpha=cfg.lora_alpha, + target_modules=lora_target_modules, + target_parameters=lora_target_parameters, + layers_to_transform=cfg.peft_layers_to_transform, + layers_pattern=cfg.peft_layers_pattern, + lora_dropout=cfg.lora_dropout, + fan_in_fan_out=cfg.lora_fan_in_fan_out, + modules_to_save=cfg.lora_modules_to_save if cfg.lora_modules_to_save else None, + exclude_modules=getattr(cfg, "lora_exclude_modules", None) or None, + bias="none", + task_type=_get_peft_task_type(model), + **lora_config_kwargs, + ) + return lora_config + + def _peft_will_auto_convert_target_params(model, lora_config) -> bool: """Check whether PEFT will auto-populate target_parameters for this model. @@ -226,62 +298,7 @@ def load_lora( config_only: bool = False, ) -> tuple[PreTrainedModel | PeftModel | PeftMixedModel | None, PeftConfig | None]: _patch_peft_clippable_linear() - lora_target_modules = cfg.lora_target_modules or [] - lora_target_parameters = cfg.lora_target_parameters or [] - - if cfg.lora_target_linear: - linear_names = find_all_linear_names(model) - LOG.info(f"found linear modules: {repr(sorted(linear_names))}") - lora_target_modules_as_list = ( - lora_target_modules - if isinstance(lora_target_modules, list) - else [lora_target_modules] - ) - lora_target_modules = list(set(lora_target_modules_as_list + linear_names)) - - lora_config_kwargs = {} - loftq_bits = cfg.peft and cfg.peft.loftq_config and cfg.peft.loftq_config.loftq_bits - if loftq_bits: - lora_config_kwargs["loftq_config"] = LoftQConfig(loftq_bits=loftq_bits) - lora_config_kwargs["init_lora_weights"] = "loftq" - if cfg.peft_init_lora_weights: - lora_config_kwargs["init_lora_weights"] = cfg.peft_init_lora_weights - if cfg.peft_use_dora: - lora_config_kwargs["use_dora"] = cfg.peft_use_dora - LOG.info("Initializing LoRA weights using dora. This might take longer.") - if cfg.peft_use_rslora: - lora_config_kwargs["use_rslora"] = cfg.peft_use_rslora - if cfg.peft_layer_replication: - lora_config_kwargs["layer_replication"] = cfg.peft_layer_replication - if cfg.peft_trainable_token_indices: - lora_config_kwargs["trainable_token_indices"] = cfg.peft_trainable_token_indices - if cfg.peft_ensure_weight_tying is not None: - lora_config_kwargs["ensure_weight_tying"] = cfg.peft_ensure_weight_tying - - # Determine the correct PEFT task type - model_cls = type(model).__name__ - if "SequenceClassification" in model_cls: - task_type = TaskType.SEQ_CLS - elif "TokenClassification" in model_cls: - task_type = TaskType.TOKEN_CLS - else: - task_type = TaskType.CAUSAL_LM - - lora_config = LoraConfig( - r=cfg.lora_r, - lora_alpha=cfg.lora_alpha, - target_modules=lora_target_modules, - target_parameters=lora_target_parameters, - layers_to_transform=cfg.peft_layers_to_transform, - layers_pattern=cfg.peft_layers_pattern, - lora_dropout=cfg.lora_dropout, - fan_in_fan_out=cfg.lora_fan_in_fan_out, - modules_to_save=cfg.lora_modules_to_save if cfg.lora_modules_to_save else None, - exclude_modules=getattr(cfg, "lora_exclude_modules", None) or None, - bias="none", - task_type=task_type, - **lora_config_kwargs, - ) + lora_config = _build_peft_lora_config(model, cfg) if config_only: return None, lora_config @@ -315,7 +332,7 @@ def load_lora( model_kwargs["autocast_adapter_dtype"] = cfg.peft_autocast_adapter_dtype if cfg.lora_model_dir: - LOG.debug("Loading pretrained PEFT - LoRA") + LOG.debug("Loading pretrained PEFT adapter") if cfg.lora_on_cpu: model_kwargs["max_memory"] = {"cpu": "256GiB"} model_kwargs["device_map"] = {"": "cpu"} @@ -364,30 +381,60 @@ def load_adapter( cfg: DictDefault, adapter: str | None, inference: bool = False, -) -> tuple[PreTrainedModel | PeftModel | PeftMixedModel, PeftConfig | None]: + config_only: bool = False, +) -> tuple[PreTrainedModel | PeftModel | PeftMixedModel | None, PeftConfig | None]: if adapter is None: return model, None if hasattr(model, "enable_input_require_grads"): model.enable_input_require_grads() if adapter in ["lora", "qlora"]: - peft_model, lora_config = load_lora(model, cfg, inference=inference) + peft_model, lora_config = load_lora( + model, cfg, inference=inference, config_only=config_only + ) return peft_model, lora_config if adapter == "llama-adapter": + if config_only: + _, lora_config = load_llama_adapter(model, cfg, config_only=True) + return None, lora_config peft_model, lora_config = load_llama_adapter(model, cfg) return peft_model, lora_config - raise NotImplementedError(f"{adapter} PEFT adapter not available") + plugin_loaded = PLUGIN_MANAGER.load_adapter( + model, + cfg, + inference=inference, + config_only=config_only, + ) + if plugin_loaded is not None: + return plugin_loaded + + adapter_capability = PLUGIN_MANAGER.get_adapter_capability(adapter) + if adapter_capability and adapter_capability.lora_like: + peft_model, lora_config = load_lora( + model, cfg, inference=inference, config_only=config_only + ) + return peft_model, lora_config + + registered = sorted(PLUGIN_MANAGER.adapter_capabilities()) + registered_msg = ", ".join(registered) if registered else "none" + raise NotImplementedError( + f"Adapter '{adapter}' is not built in and was not registered by a plugin " + f"with loader support. Registered plugin adapters: {registered_msg}" + ) def load_llama_adapter( - model: PreTrainedModel, cfg: DictDefault -) -> tuple[PeftModel | PeftMixedModel, PeftConfig]: + model: PreTrainedModel, cfg: DictDefault, config_only: bool = False +) -> tuple[PeftModel | PeftMixedModel | None, PeftConfig]: peft_config = AdaptionPromptConfig( adapter_layers=cfg.peft_adapter.layers, # layers (L) adapter_len=cfg.peft_adapter.len, # prompt length (K) task_type="CAUSAL_LM", ) + if config_only: + return None, peft_config + if cfg.lora_model_dir: LOG.debug("Loading pretrained PEFT - llama_adapter") peft_model = PeftModel.from_pretrained( diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 061509d396..84a021a9fe 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -39,7 +39,7 @@ from axolotl.common.architectures import MOE_ARCH_BLOCK from axolotl.integrations.base import PluginManager -from axolotl.loaders.adapter import load_adapter, load_lora +from axolotl.loaders.adapter import load_adapter from axolotl.loaders.constants import MULTIMODAL_AUTO_MODEL_MAPPING from axolotl.loaders.patch_manager import PatchManager from axolotl.loaders.utils import ( @@ -386,8 +386,12 @@ def _load_adapters(self) -> PeftConfig | None: and self.cfg.rl in [RLType.DPO, RLType.IPO, RLType.KTO] and not self.cfg.merge_lora ): - _, lora_config = load_lora( - self.model, self.cfg, inference=False, config_only=True + _, lora_config = load_adapter( + self.model, + self.cfg, + self.cfg.adapter, + inference=False, + config_only=True, ) else: self.model, lora_config = load_adapter( diff --git a/src/axolotl/train.py b/src/axolotl/train.py index 0dfeb0c7fe..bda07ade99 100644 --- a/src/axolotl/train.py +++ b/src/axolotl/train.py @@ -285,7 +285,9 @@ def save_trained_model( ) # Handle ReLoRA early return case if cfg.relora: - if cfg.adapter == "lora" and not (cfg.load_in_4bit or cfg.load_in_8bit): + if hasattr(model, "merge_and_unload") and not ( + cfg.load_in_4bit or cfg.load_in_8bit + ): model = model.merge_and_unload() else: # final model weights have already been saved by `ReLoRACallback.on_train_end` diff --git a/src/axolotl/utils/schemas/peft.py b/src/axolotl/utils/schemas/peft.py index 42fa628e0f..4b637c0812 100644 --- a/src/axolotl/utils/schemas/peft.py +++ b/src/axolotl/utils/schemas/peft.py @@ -38,10 +38,10 @@ class LoraConfig(BaseModel): default=False, json_schema_extra={"description": "Use bitsandbytes 4 bit"} ) - adapter: Literal["lora", "qlora", "llama-adapter"] | None = Field( + adapter: str | None = Field( default=None, json_schema_extra={ - "description": "If you want to use 'lora', 'qlora', or 'llama-adapter', or leave blank to train all parameters in original model" + "description": "If you want to use a built-in or plugin adapter, or leave blank to train all parameters in original model" }, ) lora_model_dir: str | None = Field( @@ -174,6 +174,16 @@ def validate_adapter(cls, data): "load_in_8bit and load_in_4bit are not supported without setting an adapter for training." "If you want to full finetune, please turn off load_in_8bit and load_in_4bit." ) + adapter = data.get("adapter") + if adapter and adapter not in ("lora", "qlora", "llama-adapter"): + from axolotl.integrations.base import PluginManager + + plugin_manager = PluginManager.get_instance() + if not plugin_manager.supports_adapter(adapter): + raise ValueError( + f"Adapter '{adapter}' is not built in and was not registered by " + "a plugin. Add the plugin that provides this adapter to `plugins:`." + ) return data @model_validator(mode="after") diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index ec11d9658b..2c580292ac 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1478,8 +1478,19 @@ def check_relora(self): if self.relora: if not self.jagged_restart_steps: raise ValueError("jagged_restart_steps must be set to use ReLoRA") - if self.adapter not in ("lora", "qlora"): - raise ValueError("cfg.adapter must be lora or qlora to use ReLoRA") + + adapter_supports_relora = self.adapter in ("lora", "qlora") + if self.adapter and not adapter_supports_relora: + from axolotl.integrations.base import PluginManager + + plugin_manager = PluginManager.get_instance() + adapter_supports_relora = plugin_manager.adapter_supports_relora( + self.adapter + ) + if not adapter_supports_relora: + raise ValueError( + "cfg.adapter must support ReLoRA to use ReLoRA restart semantics" + ) if self.fsdp or self.fsdp_config: raise ValueError("fsdp not supported with ReLoRA") diff --git a/tests/integrations/mora/test_mora.py b/tests/integrations/mora/test_mora.py new file mode 100644 index 0000000000..464979cb60 --- /dev/null +++ b/tests/integrations/mora/test_mora.py @@ -0,0 +1,160 @@ +"""Integration tests for the MoRA / ReMoRA adapter path.""" + +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest +import torch + +from axolotl.integrations.base import PluginManager +from axolotl.integrations.mora import plugin as mora_plugin +from axolotl.loaders import adapter as adapter_module +from axolotl.loaders.adapter import load_adapter +from axolotl.utils.dict import DictDefault + + +class TestMoraAdapterLoading: + """MoRA adapter selection and config wiring.""" + + def test_load_adapter_uses_plugin_lora_like_registration(self, monkeypatch): + model = torch.nn.Linear(4, 4) + cfg = DictDefault( + { + "adapter": "mora", + "mora": {"use_mora": True, "mora_type": "rope"}, + } + ) + + PluginManager.get_instance().plugins["axolotl.integrations.mora.MoraPlugin"] = ( + mora_plugin.MoraPlugin() + ) + + calls = [] + + def fake_load_lora(*args, **kwargs): + calls.append((args, kwargs)) + return args[0], "adapter-config" + + monkeypatch.setattr(adapter_module, "load_lora", fake_load_lora) + + _, config = load_adapter(model, cfg, "mora") + + assert config == "adapter-config" + assert calls[0][1]["config_only"] is False + + def test_mora_plugin_raises_when_peft_missing_support(self): + model = torch.nn.Linear(4, 4) + cfg = DictDefault( + { + "adapter": "mora", + "mora": {"use_mora": True, "mora_type": "rope"}, + } + ) + PluginManager.get_instance().plugins["axolotl.integrations.mora.MoraPlugin"] = ( + mora_plugin.MoraPlugin() + ) + + with pytest.raises(ImportError, match="MoRA support"): + load_adapter(model, cfg, "mora", config_only=True) + + def test_mora_plugin_rejects_quantized_base_model(self): + model = torch.nn.Linear(4, 4) + cfg = DictDefault( + { + "adapter": "mora", + "load_in_4bit": True, + "mora": {"use_mora": True, "mora_type": "rope"}, + } + ) + PluginManager.get_instance().plugins["axolotl.integrations.mora.MoraPlugin"] = ( + mora_plugin.MoraPlugin() + ) + + with pytest.raises(ValueError, match="full-precision base model"): + load_adapter(model, cfg, "mora", config_only=True) + + def test_mora_plugin_builds_mora_config_when_supported(self, monkeypatch): + model = torch.nn.Linear(4, 4) + cfg = DictDefault( + { + "adapter": "mora", + "mora": { + "use_mora": True, + "mora_type": "rope", + }, + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.0, + } + ) + + captured = {} + + class FakeLoraConfig: + def __init__(self, **kwargs): + captured.update(kwargs) + self.__dict__.update(kwargs) + + fake_model = SimpleNamespace(print_trainable_parameters=Mock()) + PluginManager.get_instance().plugins["axolotl.integrations.mora.MoraPlugin"] = ( + mora_plugin.MoraPlugin() + ) + monkeypatch.setattr(mora_plugin, "_peft_supports_mora", lambda: True) + monkeypatch.setattr(adapter_module, "LoraConfig", FakeLoraConfig) + monkeypatch.setattr( + adapter_module, "get_peft_model", Mock(return_value=fake_model) + ) + + _, config = load_adapter(model, cfg, "mora", config_only=True) + + assert captured["use_mora"] is True + assert captured["mora_type"] == 6 + assert captured["task_type"].name == "CAUSAL_LM" + assert config is not None + assert config.use_mora is True + assert config.mora_type == 6 + + def test_mora_plugin_uses_lora_model_dir_resume_path(self, monkeypatch): + model = torch.nn.Linear(4, 4) + cfg = DictDefault( + { + "adapter": "mora", + "mora": {"use_mora": True, "mora_type": "rope"}, + "lora_model_dir": "adapter-checkpoint", + "lora_on_cpu": False, + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.0, + } + ) + + class FakeLoraConfig: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + class FakePeftModel: + def print_trainable_parameters(self): + pass + + def named_parameters(self): + return [] + + from_pretrained = Mock(return_value=FakePeftModel()) + PluginManager.get_instance().plugins["axolotl.integrations.mora.MoraPlugin"] = ( + mora_plugin.MoraPlugin() + ) + monkeypatch.setattr(mora_plugin, "_peft_supports_mora", lambda: True) + monkeypatch.setattr(adapter_module, "LoraConfig", FakeLoraConfig) + monkeypatch.setattr( + adapter_module.PeftModel, + "from_pretrained", + from_pretrained, + ) + + peft_model, config = load_adapter(model, cfg, "mora") + + assert isinstance(peft_model, FakePeftModel) + assert config.use_mora is True + from_pretrained.assert_called_once() + assert from_pretrained.call_args.args[:2] == (model, "adapter-checkpoint") + assert from_pretrained.call_args.kwargs["is_trainable"] is True diff --git a/tests/integrations/test_adapter_plugin_registry.py b/tests/integrations/test_adapter_plugin_registry.py new file mode 100644 index 0000000000..e3102a90ec --- /dev/null +++ b/tests/integrations/test_adapter_plugin_registry.py @@ -0,0 +1,73 @@ +"""Core adapter plugin registry tests.""" + +from unittest.mock import Mock + +import pytest +import torch + +from axolotl.integrations.base import AdapterCapabilities, BasePlugin, PluginManager +from axolotl.loaders import adapter as adapter_module +from axolotl.loaders.adapter import load_adapter +from axolotl.utils.config import validate_config +from axolotl.utils.dict import DictDefault + + +class FakeAdapterPlugin(BasePlugin): + def get_adapter_capabilities(self) -> list[AdapterCapabilities]: + return [AdapterCapabilities(name="fake-adapter", lora_like=True, relora=True)] + + def get_lora_config_kwargs(self, cfg: DictDefault) -> dict: + if cfg.adapter != "fake-adapter": + return {} + return {"fake_kwarg": "from-plugin"} + + +class TestAdapterPluginRegistry: + def test_lora_like_plugin_adapter_contributes_peft_kwargs(self, monkeypatch): + model = torch.nn.Linear(4, 4) + cfg = DictDefault( + { + "adapter": "fake-adapter", + "lora_r": 8, + "lora_alpha": 16, + "lora_dropout": 0.0, + } + ) + PluginManager.get_instance().plugins["fake"] = FakeAdapterPlugin() + captured = {} + + class FakeLoraConfig: + def __init__(self, **kwargs): + captured.update(kwargs) + self.__dict__.update(kwargs) + + monkeypatch.setattr(adapter_module, "LoraConfig", FakeLoraConfig) + monkeypatch.setattr(adapter_module, "get_peft_model", Mock()) + + _, config = load_adapter(model, cfg, "fake-adapter", config_only=True) + + assert config is not None + assert captured["fake_kwarg"] == "from-plugin" + assert captured["task_type"].name == "CAUSAL_LM" + + def test_unknown_adapter_error_mentions_plugin_registry(self): + model = torch.nn.Linear(4, 4) + cfg = DictDefault({"adapter": "missing-adapter"}) + + with pytest.raises(NotImplementedError, match="registered by a plugin"): + load_adapter(model, cfg, "missing-adapter") + + def test_relora_accepts_plugin_adapter_capability(self, min_base_cfg): + PluginManager.get_instance().plugins["fake"] = FakeAdapterPlugin() + cfg = min_base_cfg | DictDefault( + { + "adapter": "fake-adapter", + "relora": True, + "jagged_restart_steps": 100, + } + ) + + validated = validate_config(cfg) + + assert validated.adapter == "fake-adapter" + assert validated.relora is True diff --git a/tests/utils/schemas/validation/mora/test_mora_validation.py b/tests/utils/schemas/validation/mora/test_mora_validation.py new file mode 100644 index 0000000000..da1169f509 --- /dev/null +++ b/tests/utils/schemas/validation/mora/test_mora_validation.py @@ -0,0 +1,100 @@ +"""Validation tests for the MoRA / ReMoRA integration.""" + +import pytest + +from axolotl.integrations.mora import MoraType +from axolotl.utils.config import prepare_plugins, validate_config +from axolotl.utils.dict import DictDefault + + +class TestMoraValidation: + """MoRA-specific config validation.""" + + def test_mora_block_round_trips(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + { + "adapter": "mora", + "plugins": ["axolotl.integrations.mora.MoraPlugin"], + "mora": { + "use_mora": True, + "mora_type": "rope", + }, + } + ) + + prepare_plugins(cfg) + validated = validate_config(cfg) + + assert validated.adapter == "mora" + assert validated.mora.use_mora is True + assert validated.mora.mora_type == MoraType.ROPE + + def test_mora_type_accepts_legacy_supported_numbers(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + { + "adapter": "mora", + "plugins": ["axolotl.integrations.mora.MoraPlugin"], + "mora": { + "use_mora": True, + "mora_type": 1, + }, + } + ) + + prepare_plugins(cfg) + validated = validate_config(cfg) + + assert validated.mora.mora_type == MoraType.SHARING + + def test_mora_rejects_unsupported_variant_numbers(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + { + "adapter": "mora", + "plugins": ["axolotl.integrations.mora.MoraPlugin"], + "mora": { + "use_mora": True, + "mora_type": 2, + }, + } + ) + + prepare_plugins(cfg) + with pytest.raises(ValueError, match="mora_type"): + validate_config(cfg) + + def test_remora_uses_core_relora_fields(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + { + "adapter": "mora", + "plugins": ["axolotl.integrations.mora.MoraPlugin"], + "relora": True, + "jagged_restart_steps": 2000, + "mora": { + "use_mora": True, + "mora_type": "rope", + }, + } + ) + + prepare_plugins(cfg) + validated = validate_config(cfg) + + assert validated.relora is True + assert validated.jagged_restart_steps == 2000 + + def test_remora_still_requires_core_restart_steps(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + { + "adapter": "mora", + "plugins": ["axolotl.integrations.mora.MoraPlugin"], + "relora": True, + "mora": { + "use_mora": True, + "mora_type": "rope", + }, + } + ) + + prepare_plugins(cfg) + with pytest.raises(ValueError, match="jagged_restart_steps"): + validate_config(cfg) From 52c78432f477212a465c717dea28f2a5511dd40f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 13 May 2026 01:18:31 -0400 Subject: [PATCH 1294/1405] Fix cu130 LD_LIBRARY_PATH startup (#3648) [skip ci] * Fix cu130 LD_LIBRARY_PATH startup Co-authored-by: Axolotl Swarm * Fix cu130 test lint --------- Co-authored-by: Swarm Co-authored-by: Axolotl Swarm --- docker/Dockerfile-uv | 3 ++ scripts/cloud-entrypoint.sh | 6 +++ scripts/cuda13_env.sh | 19 ++++++++ scripts/uv-entrypoint.sh | 7 +++ src/axolotl/utils/cuda13.py | 39 ++++++++++++++++ tests/utils/test_cuda13.py | 93 +++++++++++++++++++++++++++++++++++++ 6 files changed, 167 insertions(+) create mode 100644 scripts/cuda13_env.sh create mode 100644 scripts/uv-entrypoint.sh create mode 100644 src/axolotl/utils/cuda13.py create mode 100644 tests/utils/test_cuda13.py diff --git a/docker/Dockerfile-uv b/docker/Dockerfile-uv index 582a2a0cf2..c57160d343 100644 --- a/docker/Dockerfile-uv +++ b/docker/Dockerfile-uv @@ -44,4 +44,7 @@ RUN git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && \ COPY .axolotl-complete.bash /root/.axolotl-complete.bash RUN chmod +x /root/.axolotl-complete.bash && \ + chmod +x /workspace/axolotl/scripts/uv-entrypoint.sh && \ echo 'source /root/.axolotl-complete.bash' >> ~/.bashrc + +ENTRYPOINT ["/workspace/axolotl/scripts/uv-entrypoint.sh"] diff --git a/scripts/cloud-entrypoint.sh b/scripts/cloud-entrypoint.sh index c98e7c0d0b..7a4fb3ffbd 100755 --- a/scripts/cloud-entrypoint.sh +++ b/scripts/cloud-entrypoint.sh @@ -2,6 +2,12 @@ # Export specific ENV variables to /etc/rp_environment echo "Exporting environment variables..." +if [ -f /workspace/axolotl/scripts/cuda13_env.sh ]; then + source /workspace/axolotl/scripts/cuda13_env.sh + if [ -n "${LD_LIBRARY_PATH:-}" ]; then + echo "export LD_LIBRARY_PATH=\"${LD_LIBRARY_PATH}\"" >> /etc/rp_environment + fi +fi printenv | grep -E '^HF_|^BNB_|^CUDA_|^NCCL_|^NV|^RUNPOD_|^PATH=|^_=' | sed 's/^\([^=]*\)=\(.*\)$/export \1="\2"/' | grep -v 'printenv' >> /etc/rp_environment echo 'source /etc/rp_environment' >> ~/.bashrc diff --git a/scripts/cuda13_env.sh b/scripts/cuda13_env.sh new file mode 100644 index 0000000000..a739063a4e --- /dev/null +++ b/scripts/cuda13_env.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Keep CUDA 13/cu130 uv images working when NVIDIA ships the cu13 package. +# This is a no-op on images without nvidia.cu13. +axolotl_prepend_cu13_ld_library_path() { + local updated_ld_library_path + if updated_ld_library_path="$(python - <<'PY' +from axolotl.utils.cuda13 import prepend_cu13_ld_library_path +import os + +print(prepend_cu13_ld_library_path(os.environ.get("LD_LIBRARY_PATH"))) +PY + )"; then + export LD_LIBRARY_PATH="$updated_ld_library_path" + fi +} + +axolotl_prepend_cu13_ld_library_path +unset -f axolotl_prepend_cu13_ld_library_path diff --git a/scripts/uv-entrypoint.sh b/scripts/uv-entrypoint.sh new file mode 100644 index 0000000000..8e386352ab --- /dev/null +++ b/scripts/uv-entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ -f /workspace/axolotl/scripts/cuda13_env.sh ]; then + source /workspace/axolotl/scripts/cuda13_env.sh +fi + +exec "$@" diff --git a/src/axolotl/utils/cuda13.py b/src/axolotl/utils/cuda13.py new file mode 100644 index 0000000000..cfc4a26872 --- /dev/null +++ b/src/axolotl/utils/cuda13.py @@ -0,0 +1,39 @@ +"""Helpers for CUDA 13 uv images.""" + +from __future__ import annotations + +from pathlib import Path + + +def cu13_library_path() -> str | None: + """Return the nvidia.cu13 package lib path, when available.""" + try: + import nvidia.cu13 as cu13 + except ImportError: + return None + + package_paths = list(cu13.__path__) + if not package_paths: + return None + return str(Path(package_paths[0]) / "lib") + + +def prepend_cu13_ld_library_path(ld_library_path: str | None) -> str: + """ + Prepend the CUDA 13 library directory when nvidia.cu13 is installed. + + The path is intentionally kept small and idempotent: + - no-op when the cu13 package is absent + - keeps any existing LD_LIBRARY_PATH entries + - avoids duplicating the package-derived cu13 lib path + """ + cu13_lib = cu13_library_path() + if cu13_lib is None: + return ld_library_path or "" + + parts = [cu13_lib] + for part in (ld_library_path or "").split(":"): + if part and part != cu13_lib and part not in parts: + parts.append(part) + + return ":".join(parts) diff --git a/tests/utils/test_cuda13.py b/tests/utils/test_cuda13.py new file mode 100644 index 0000000000..c86bff8ced --- /dev/null +++ b/tests/utils/test_cuda13.py @@ -0,0 +1,93 @@ +"""Tests for CUDA 13 LD_LIBRARY_PATH helpers.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +from axolotl.utils.cuda13 import cu13_library_path, prepend_cu13_ld_library_path + + +@pytest.fixture(autouse=True) +def clear_nvidia_cu13_modules(): + for module_name in ("nvidia.cu13", "nvidia"): + sys.modules.pop(module_name, None) + yield + for module_name in ("nvidia.cu13", "nvidia"): + sys.modules.pop(module_name, None) + + +def _make_fake_cu13_package(root: Path) -> Path: + package_root = root / "nvidia" / "cu13" + package_root.mkdir(parents=True) + (root / "nvidia" / "__init__.py").write_text("", encoding="utf-8") + (package_root / "__init__.py").write_text("", encoding="utf-8") + (package_root / "lib").mkdir() + return package_root + + +def test_prepend_cu13_ld_library_path_when_package_exists(monkeypatch, tmp_path): + package_root = _make_fake_cu13_package(tmp_path) + monkeypatch.syspath_prepend(str(tmp_path)) + expected_lib = str(package_root / "lib") + + assert cu13_library_path() == expected_lib + assert ( + prepend_cu13_ld_library_path("/opt/custom/lib:/usr/lib") + == f"{expected_lib}:/opt/custom/lib:/usr/lib" + ) + + +def test_prepend_cu13_ld_library_path_is_idempotent(monkeypatch, tmp_path): + package_root = _make_fake_cu13_package(tmp_path) + monkeypatch.syspath_prepend(str(tmp_path)) + expected_lib = str(package_root / "lib") + + assert ( + prepend_cu13_ld_library_path(f"{expected_lib}:/opt/custom/lib:{expected_lib}") + == f"{expected_lib}:/opt/custom/lib" + ) + + +def test_prepend_cu13_ld_library_path_is_noop_without_package(monkeypatch): + import builtins + + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "nvidia.cu13": + raise ImportError(name) + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", fake_import) + assert cu13_library_path() is None + assert prepend_cu13_ld_library_path("/opt/custom/lib") == "/opt/custom/lib" + assert prepend_cu13_ld_library_path(None) == "" + + +def test_cuda13_env_sh_prepend_path_when_package_exists(tmp_path): + package_root = _make_fake_cu13_package(tmp_path) + + env = os.environ.copy() + env["PYTHONPATH"] = f"{tmp_path}:src" + env["LD_LIBRARY_PATH"] = "" + + result = subprocess.run( + [ + "bash", + "-lc", + 'source scripts/cuda13_env.sh; printf %s "$LD_LIBRARY_PATH"', + ], + check=True, + capture_output=True, + text=True, + env=env, + ) + + path_parts = result.stdout.split(":") + assert path_parts[0] == str(package_root / "lib") + assert str(package_root / "lib") in path_parts From 3e9639fda55da8d45e49bb68a6ffcc1000121d43 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 13 May 2026 12:18:59 +0700 Subject: [PATCH 1295/1405] chore: update security policy to email rather than discord (#3645) [skip ci] --- .github/SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index aceb0d1a2e..e8c8ceff85 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -6,4 +6,4 @@ Due to the nature of the fast development that is happening in this project, onl ## Reporting a Vulnerability -If you find a vulnerability, please contact us on [Discord](https://discord.gg/xcu3ECkH9a) rather than creating a GitHub issue to allow us some time to fix it before it is a known vulnerability to others. +If you find a vulnerability, please contact us by email `wing@axolotl.ai` rather than creating a GitHub issue to allow us some time to fix it before it is a known vulnerability to others. From 2e5baa44ea8f2d4a5598a1ea42f2c5206dd63475 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 13 May 2026 12:19:17 +0700 Subject: [PATCH 1296/1405] fix: transformers moved fa utils broke ring fa (#3644) [skip ci] --- src/axolotl/utils/schemas/validation.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 2c580292ac..a19320b9c0 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1550,7 +1550,10 @@ def check_context_parallel_size(self): try: import transformers.modeling_flash_attention_utils - from transformers.utils import is_flash_attn_greater_or_equal + from transformers.utils import ( + is_flash_attn_greater_or_equal, + is_flash_attn_greater_or_equal_2_10, + ) transformers.modeling_flash_attention_utils._flash_supports_window = ( True @@ -1564,6 +1567,11 @@ def check_context_parallel_size(self): sys.modules[ "transformers.modeling_flash_attention_utils" ].is_flash_attn_greater_or_equal = is_flash_attn_greater_or_equal + sys.modules[ + "transformers.modeling_flash_attention_utils" + ].is_flash_attn_greater_or_equal_2_10 = ( + is_flash_attn_greater_or_equal_2_10 + ) import ring_flash_attn # noqa: F401 # Required after monkey-patching except ImportError as exception: raise ImportError( From b7f787fb9960171a3aac55f66b7eff8331983dfd Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 13 May 2026 12:19:30 +0700 Subject: [PATCH 1297/1405] fix: processor_kwargs for mm_chat regression (#3643) [skip ci] * fix: processor_kwargs for mm_chat regression * fix: correct test to check correct typing --- src/axolotl/utils/collators/mm_chat.py | 18 ++++++++---------- tests/test_mm_chat_collator.py | 9 +++++---- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/axolotl/utils/collators/mm_chat.py b/src/axolotl/utils/collators/mm_chat.py index 12dbde7d1f..c4f4f3c0ef 100644 --- a/src/axolotl/utils/collators/mm_chat.py +++ b/src/axolotl/utils/collators/mm_chat.py @@ -43,16 +43,14 @@ def process_rows( # Initialize batch messages = [ex["messages"] for ex in examples] - batch = dict( - self.processing_strategy.processor.apply_chat_template( - messages, - add_generation_prompt=False, - tokenize=True, - return_tensors="pt", - padding=True, - return_dict=True, - chat_template=self.processing_strategy.chat_template, - ) + batch = self.processing_strategy.processor.apply_chat_template( + messages, + add_generation_prompt=False, + tokenize=True, + return_tensors="pt", + return_dict=True, + chat_template=self.processing_strategy.chat_template, + processor_kwargs={"padding": True}, ) # Process the labels diff --git a/tests/test_mm_chat_collator.py b/tests/test_mm_chat_collator.py index d26a011a77..5c40c5b34f 100644 --- a/tests/test_mm_chat_collator.py +++ b/tests/test_mm_chat_collator.py @@ -8,6 +8,7 @@ when downstream code did batch["input_ids"] on the resulting tensor. """ +from collections.abc import Mapping from unittest.mock import MagicMock, patch import pytest @@ -97,10 +98,10 @@ def test_process_rows_returns_dict(self, mock_processing_strategy): ): batch = collator.process_rows(examples) - assert isinstance(batch, dict), ( - "process_rows must return a dict (BatchFeature), not a raw tensor. " - "If it returns a tensor, apply_chat_template was called without " - "return_dict=True at the top level." + assert isinstance(batch, Mapping), ( + "process_rows must return a Mapping (BatchFeature or dict), not a " + "raw tensor. If it returns a tensor, apply_chat_template was called " + "without return_dict=True at the top level." ) def test_process_rows_input_ids_shape(self, mock_processing_strategy): From 72ea23718267340844cab65996b3749c46987057 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Wed, 13 May 2026 10:49:47 +0530 Subject: [PATCH 1298/1405] feat: add blackwell to docker arch list (#3641) [skip ci] * feat: doker for blackwell * docker docs n chore * clean up --- .github/workflows/base.yml | 12 ++++++------ docs/installation.qmd | 6 ++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 8ed89c5db5..d6898c6a6a 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -67,7 +67,7 @@ jobs: cudnn_version: "" python_version: "3.11" pytorch: 2.9.1 - torch_cuda_arch_list: "9.0+PTX" + torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" dockerfile: "Dockerfile-base" platforms: "linux/amd64,linux/arm64" - cuda: "130" @@ -75,7 +75,7 @@ jobs: cudnn_version: "" python_version: "3.12" pytorch: 2.9.1 - torch_cuda_arch_list: "9.0+PTX" + torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" dockerfile: "Dockerfile-base" platforms: "linux/amd64,linux/arm64" - cuda: "130" @@ -83,7 +83,7 @@ jobs: cudnn_version: "" python_version: "3.12" pytorch: 2.10.0 - torch_cuda_arch_list: "9.0+PTX" + torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" dockerfile: "Dockerfile-base" platforms: "linux/amd64,linux/arm64" # - cuda: "128" @@ -189,7 +189,7 @@ jobs: cudnn_version: "" python_version: "3.11" pytorch: 2.9.1 - torch_cuda_arch_list: "9.0+PTX" + torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" - cuda: "130" @@ -197,7 +197,7 @@ jobs: cudnn_version: "" python_version: "3.12" pytorch: 2.9.1 - torch_cuda_arch_list: "9.0+PTX" + torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" - cuda: "130" @@ -205,7 +205,7 @@ jobs: cudnn_version: "" python_version: "3.12" pytorch: 2.10.0 - torch_cuda_arch_list: "9.0+PTX" + torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" steps: diff --git a/docs/installation.qmd b/docs/installation.qmd index f7c780740d..ff45c557cc 100644 --- a/docs/installation.qmd +++ b/docs/installation.qmd @@ -20,7 +20,9 @@ This guide covers all the ways you can install and set up Axolotl for your envir ## Installation {#sec-installation} ::: {.callout-important} -For Blackwell GPUs, please use Pytorch 2.9.1 and CUDA 12.8. +For Blackwell GPUs, please use Pytorch 2.9.1 and CUDA 13.0. CUDA 12.8 cannot +compile for `sm_103a` (B300), and using CUDA 13.0 also avoids first-launch JIT +overhead on B100 / B200 / RTX 50-series. ::: ### Quick Install {#sec-uv} @@ -79,7 +81,7 @@ docker run --privileged --gpus '"all"' --shm-size 10g --rm -it \ ::: ::: {.callout-important} -For Blackwell GPUs, please use `axolotlai/axolotl-uv:main-py3.11-cu128-2.9.1` or the cloud variant `axolotlai/axolotl-cloud-uv:main-py3.11-cu128-2.9.1`. +For Blackwell GPUs, please use `axolotlai/axolotl-uv:main-py3.11-cu130-2.9.1` or the cloud variant `axolotlai/axolotl-cloud-uv:main-py3.11-cu130-2.9.1`. ::: Please refer to the [Docker documentation](docker.qmd) for more information on the different Docker images that are available. From f3f37a37073cfdb988058a5d33228b67d63fa3ae Mon Sep 17 00:00:00 2001 From: minyichen <96753147+cyc00518@users.noreply.github.com> Date: Wed, 13 May 2026 13:34:29 +0800 Subject: [PATCH 1299/1405] fix: preserve split slices for local file datasets (#3627) [skip ci] --- src/axolotl/utils/data/shared.py | 6 ++-- tests/utils/data/test_utils.py | 57 +++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/axolotl/utils/data/shared.py b/src/axolotl/utils/data/shared.py index 525e0e7ffd..0b84725914 100644 --- a/src/axolotl/utils/data/shared.py +++ b/src/axolotl/utils/data/shared.py @@ -240,8 +240,10 @@ def _load_from_local_path( elif local_path.is_file(): dataset_type = get_dataset_type(dataset_config) - # For single file datasets, HF always creates only a "train" split - if dataset_type in ("json", "csv", "text"): + # Single-file paths always expose only a "train" split regardless of format. + # Preserve any user-provided slice syntax (e.g. "train[:500]"); only + # fall back to "train" when no split was specified. + if not load_dataset_kwargs.get("split"): load_dataset_kwargs["split"] = "train" return load_dataset( diff --git a/tests/utils/data/test_utils.py b/tests/utils/data/test_utils.py index 4b6afec3db..7c3e4ecac7 100644 --- a/tests/utils/data/test_utils.py +++ b/tests/utils/data/test_utils.py @@ -2,11 +2,14 @@ Unit tests for data utility functions """ +import tempfile import unittest -from unittest.mock import MagicMock +from pathlib import Path +from unittest.mock import MagicMock, patch from datasets import Dataset +from axolotl.utils.data.shared import _load_from_local_path from axolotl.utils.data.utils import handle_long_seq_in_dataset, remove_double_bos_token from axolotl.utils.dict import DictDefault @@ -541,6 +544,58 @@ def test_invalid_strategy_falls_through_to_drop(self): self.assertEqual(len(result[0]["input_ids"]), 3) +class TestLocalFileSplitHandling(unittest.TestCase): + """Regression tests for local-file dataset split handling.""" + + def setUp(self): + self.tmpdir = Path(tempfile.gettempdir()) + + @patch("axolotl.utils.data.shared.load_dataset") + def test_local_jsonl_preserves_split_slice(self, mock_load_dataset): + path = str(self.tmpdir / "sample.jsonl") + dataset_config = DictDefault( + {"path": path, "ds_type": "json", "split": "train[:500]"} + ) + load_dataset_kwargs = {"split": dataset_config.split} + + with patch("pathlib.Path.is_file", return_value=True): + _load_from_local_path(dataset_config, load_dataset_kwargs) + + mock_load_dataset.assert_called_once() + _, kwargs = mock_load_dataset.call_args + self.assertEqual(kwargs["split"], "train[:500]") + self.assertEqual(kwargs["data_files"], path) + + @patch("axolotl.utils.data.shared.load_dataset") + def test_local_csv_preserves_split_slice(self, mock_load_dataset): + path = str(self.tmpdir / "sample.csv") + dataset_config = DictDefault( + {"path": path, "ds_type": "csv", "split": "train[:200]"} + ) + load_dataset_kwargs = {"split": dataset_config.split} + + with patch("pathlib.Path.is_file", return_value=True): + _load_from_local_path(dataset_config, load_dataset_kwargs) + + mock_load_dataset.assert_called_once() + _, kwargs = mock_load_dataset.call_args + self.assertEqual(kwargs["split"], "train[:200]") + self.assertEqual(kwargs["data_files"], path) + + @patch("axolotl.utils.data.shared.load_dataset") + def test_local_file_no_split_defaults_to_train(self, mock_load_dataset): + path = str(self.tmpdir / "sample.parquet") + dataset_config = DictDefault({"path": path, "split": None}) + load_dataset_kwargs = {"split": None} + + with patch("pathlib.Path.is_file", return_value=True): + _load_from_local_path(dataset_config, load_dataset_kwargs) + + mock_load_dataset.assert_called_once() + _, kwargs = mock_load_dataset.call_args + self.assertEqual(kwargs["split"], "train") + + class TestRemoveDoubleBOSToken(unittest.TestCase): def test_no_remove_bos_token(self): input_ids = [0, 1, 2] From b77e18de494fda06e9ee62a8f9129b240c0341bb Mon Sep 17 00:00:00 2001 From: zxuhan7 Date: Wed, 13 May 2026 07:34:56 +0200 Subject: [PATCH 1300/1405] fix: probe GPU capabilities on Ray worker, not driver (#3179) (#3619) [skip ci] * fix: probe GPU capabilities on Ray worker, not driver (#3179) The Ray driver typically runs on a CPU-only node, so `is_torch_bf16_gpu_available()` always returns false there. Validating the config against those values produced spurious "bf16 requested, but AMP is not supported" warnings (and previously hard errors) even when training would succeed on the GPU workers. Skip GPU capability detection in the driver's `load_cfg` when `use_ray` is set, and re-run `validate_config` with worker-detected capabilities inside `ray_train_func`. * docs: add docstrings to ray_train_func and test helper * style: drop extra blank line after imports to satisfy ruff * review: apply feedback from PR #3619 - Switch `cfg.get("use_ray")` to attribute-style `cfg.use_ray`. - Assert capabilities/env_capabilities passed to `validate_config` in both Ray and non-Ray `load_cfg` paths. - Add a test that `ray_train_func` probes `gpu_capabilities()` on the worker and forwards the results into `validate_config` before training. - Patch `TELEMETRY_MANAGER.send_event` in the tests to keep them hermetic. * fix: worker not getting plugin set --------- Co-authored-by: NanoCode012 --- src/axolotl/cli/config.py | 50 ++++-- src/axolotl/cli/train.py | 37 ++++- tests/cli/test_load_cfg_capabilities.py | 202 ++++++++++++++++++++++++ 3 files changed, 265 insertions(+), 24 deletions(-) create mode 100644 tests/cli/test_load_cfg_capabilities.py diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index 568c115cca..123e093c0c 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -297,26 +297,18 @@ def load_cfg( existing_child = cfg[parent].get(child_key) cfg[parent][child_key] = _coerce_value(child_value, existing_child) - try: - device_props = torch.cuda.get_device_properties("cuda") - gpu_version = "sm_" + str(device_props.major) + str(device_props.minor) - except (RuntimeError, AssertionError): - gpu_version = None - prepare_plugins(cfg) + if cfg.use_ray: + # Ray drivers typically have no GPU; defer capability checks to the worker. + capabilities, env_capabilities = None, None + else: + capabilities, env_capabilities = gpu_capabilities() + cfg = validate_config( cfg, - capabilities={ - "bf16": is_torch_bf16_gpu_available(), - "fp8": compute_supports_fp8(), - "tf32": is_torch_tf32_available(), - "n_gpu": int(os.environ.get("WORLD_SIZE", 1)), - "compute_capability": gpu_version, - }, - env_capabilities={ - "torch_version": str(torch.__version__).split("+", maxsplit=1)[0] - }, + capabilities=capabilities, + env_capabilities=env_capabilities, ) # NOTE(djsaunde): We start outputting to output_dir/debug.log at this point since we @@ -352,3 +344,29 @@ def compute_supports_fp8() -> bool: return compute_capability >= (9, 0) except RuntimeError: return False + + +def gpu_capabilities() -> tuple[dict, dict]: + """Probe the local GPU and return ``(capabilities, env_capabilities)`` dicts + suitable for :func:`axolotl.utils.config.validate_config`. + + Must be called on a GPU-enabled host (e.g. a Ray worker), otherwise the + detected values reflect the driver/CPU node and not the training device. + """ + try: + device_props = torch.cuda.get_device_properties("cuda") + gpu_version = "sm_" + str(device_props.major) + str(device_props.minor) + except (RuntimeError, AssertionError): + gpu_version = None + + capabilities = { + "bf16": is_torch_bf16_gpu_available(), + "fp8": compute_supports_fp8(), + "tf32": is_torch_tf32_available(), + "n_gpu": int(os.environ.get("WORLD_SIZE", 1)), + "compute_capability": gpu_version, + } + env_capabilities = { + "torch_version": str(torch.__version__).split("+", maxsplit=1)[0] + } + return capabilities, env_capabilities diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 2d472ed105..4528577624 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -11,11 +11,16 @@ from axolotl.cli.args import TrainerCliArgs from axolotl.cli.checks import check_accelerate_default_config, check_user_token -from axolotl.cli.config import load_cfg +from axolotl.cli.config import ( + gpu_capabilities, + load_cfg, + plugin_set_cfg, + prepare_plugins, +) from axolotl.common.datasets import load_datasets, load_preference_datasets from axolotl.integrations.base import PluginManager from axolotl.train import train -from axolotl.utils.config import normalize_config, resolve_dtype +from axolotl.utils.config import normalize_config, resolve_dtype, validate_config from axolotl.utils.dict import DictDefault from axolotl.utils.trainer import prepare_optim_env @@ -92,13 +97,32 @@ def do_cli(config: Union[Path, str] = Path("examples/"), **kwargs): def ray_train_func(kwargs: dict): + """Ray Train entrypoint executed on each GPU worker. + + Re-validates the config against worker-local GPU capabilities (deferred from + the driver), then runs the standard training pipeline. + """ # cast `cfg` back to DictDefault (ray tune deepcopy has issues with DictDefault so needed it to be dict) # also renormalize the config now that TorchTrainer has spawned distributed workers cfg = DictDefault(kwargs["cfg"]) + + # Plugins must be registered before `validate_config` so the plugin-extended + # pydantic schema is in scope on this worker; otherwise plugin-specific cfg + # fields are silently dropped by `model_dump(exclude_none=True)`. + if cfg.get("plugins"): + prepare_plugins(cfg) + + # GPU capability detection was deferred from the driver; run the checks now + # that we are on a worker that actually has the training device attached. + capabilities, env_capabilities = gpu_capabilities() + cfg = validate_config( + cfg, + capabilities=capabilities, + env_capabilities=env_capabilities, + ) + prepare_optim_env(cfg) normalize_config(cfg) - - # now that we are on the worker node, we can check `is_torch_bf16_gpu_available` to resolve dtype resolve_dtype(cfg) # ray serializing objects gets rid of frozen attribute - HF expects dict not DefaultDict @@ -108,11 +132,8 @@ def ray_train_func(kwargs: dict): # initialize accelerator before model instantiation Accelerator(gradient_accumulation_steps=cfg.gradient_accumulation_steps) - # Register plugins in Ray workers + # Bind the post-validation cfg to the plugin manager. if cfg.get("plugins"): - from axolotl.cli.config import plugin_set_cfg, prepare_plugins - - prepare_plugins(cfg) plugin_set_cfg(cfg) kwargs["cfg"] = cfg diff --git a/tests/cli/test_load_cfg_capabilities.py b/tests/cli/test_load_cfg_capabilities.py new file mode 100644 index 0000000000..2f01d09312 --- /dev/null +++ b/tests/cli/test_load_cfg_capabilities.py @@ -0,0 +1,202 @@ +"""Tests for GPU capability detection in `load_cfg` and `ray_train_func`.""" + +from pathlib import Path +from unittest.mock import MagicMock, patch + +from axolotl.cli.config import load_cfg +from axolotl.cli.train import ray_train_func + +_BASE_CONFIG = """ +base_model: HuggingFaceTB/SmolLM2-135M +datasets: + - path: mhenrichsen/alpaca_2k_test + type: alpaca +sequence_len: 2048 +max_steps: 1 +micro_batch_size: 1 +gradient_accumulation_steps: 1 +learning_rate: 1e-3 +special_tokens: + pad_token: <|endoftext|> +""" + + +def _write_cfg(tmp_path: Path, extra: str = "") -> Path: + """Write the base test config (plus any extra YAML lines) to a temp file.""" + path = tmp_path / "config.yml" + path.write_text(_BASE_CONFIG + extra) + return path + + +def _patch_load_cfg_dependencies(monkeypatch, validate_mock=None): + """Stub everything `load_cfg` does after validation so the test can focus + on whether GPU capabilities were probed on the driver. + + If ``validate_mock`` is given, it is installed as ``validate_config`` so the + test can inspect the arguments it was called with; otherwise a simple + identity stub is used. + """ + monkeypatch.setattr( + "axolotl.cli.config.validate_config", + validate_mock if validate_mock is not None else (lambda cfg, **_: cfg), + ) + monkeypatch.setattr("axolotl.cli.config.normalize_config", lambda *_: None) + monkeypatch.setattr("axolotl.cli.config.normalize_cfg_datasets", lambda *_: None) + monkeypatch.setattr("axolotl.cli.config.prepare_debug_log", lambda *_: None) + monkeypatch.setattr("axolotl.cli.config.prepare_optim_env", lambda *_: None) + monkeypatch.setattr("axolotl.cli.config.setup_wandb_env_vars", lambda *_: None) + monkeypatch.setattr("axolotl.cli.config.setup_mlflow_env_vars", lambda *_: None) + monkeypatch.setattr("axolotl.cli.config.setup_comet_env_vars", lambda *_: None) + monkeypatch.setattr("axolotl.cli.config.setup_trackio_env_vars", lambda *_: None) + monkeypatch.setattr("axolotl.cli.config.plugin_set_cfg", lambda *_: None) + monkeypatch.setattr( + "axolotl.cli.config.TELEMETRY_MANAGER.send_event", lambda *_, **__: None + ) + + +def test_load_cfg_probes_capabilities_by_default(tmp_path, monkeypatch): + """Without `use_ray`, `load_cfg` probes GPU capabilities on the local host + and passes the results into `validate_config`.""" + validate_mock = MagicMock(side_effect=lambda cfg, **_: cfg) + _patch_load_cfg_dependencies(monkeypatch, validate_mock=validate_mock) + config_path = _write_cfg(tmp_path) + + with patch("axolotl.cli.config.gpu_capabilities") as mock_caps: + mock_caps.return_value = ({"bf16": False}, {"torch_version": "2.6.0"}) + load_cfg(str(config_path)) + + mock_caps.assert_called_once() + _, kwargs = validate_mock.call_args + assert kwargs["capabilities"] == {"bf16": False} + assert kwargs["env_capabilities"] == {"torch_version": "2.6.0"} + + +def test_load_cfg_skips_capabilities_under_ray(tmp_path, monkeypatch): + """With `use_ray: true`, capability detection is deferred to the worker + and `validate_config` receives `None` for both capability dicts.""" + validate_mock = MagicMock(side_effect=lambda cfg, **_: cfg) + _patch_load_cfg_dependencies(monkeypatch, validate_mock=validate_mock) + config_path = _write_cfg(tmp_path, "use_ray: true\nray_num_workers: 1\n") + + with patch("axolotl.cli.config.gpu_capabilities") as mock_caps: + load_cfg(str(config_path)) + + mock_caps.assert_not_called() + _, kwargs = validate_mock.call_args + assert kwargs["capabilities"] is None + assert kwargs["env_capabilities"] is None + + +def test_ray_train_func_validates_with_worker_capabilities(monkeypatch): + """`ray_train_func` must probe `gpu_capabilities()` on the worker and feed + the result into `validate_config` before training runs.""" + cfg_dict = { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "gradient_accumulation_steps": 1, + } + + validate_mock = MagicMock(side_effect=lambda cfg, **_: cfg) + do_train_mock = MagicMock() + accelerator_mock = MagicMock() + + monkeypatch.setattr("axolotl.cli.train.validate_config", validate_mock) + monkeypatch.setattr("axolotl.cli.train.do_train", do_train_mock) + monkeypatch.setattr("axolotl.cli.train.prepare_optim_env", lambda *_: None) + monkeypatch.setattr("axolotl.cli.train.normalize_config", lambda *_: None) + monkeypatch.setattr("axolotl.cli.train.resolve_dtype", lambda *_: None) + monkeypatch.setattr("axolotl.cli.train.Accelerator", accelerator_mock) + + with patch("axolotl.cli.train.gpu_capabilities") as mock_caps: + mock_caps.return_value = ( + {"bf16": True, "fp8": False, "tf32": True, "compute_capability": "sm_90"}, + {"torch_version": "2.6.0"}, + ) + ray_train_func({"cfg": cfg_dict, "cli_args": MagicMock()}) + + mock_caps.assert_called_once() + validate_mock.assert_called_once() + _, kwargs = validate_mock.call_args + assert kwargs["capabilities"] == { + "bf16": True, + "fp8": False, + "tf32": True, + "compute_capability": "sm_90", + } + assert kwargs["env_capabilities"] == {"torch_version": "2.6.0"} + do_train_mock.assert_called_once() + + +def test_ray_train_func_registers_plugins_before_validate_config(monkeypatch): + """Regression: plugins must be registered before `validate_config` so the + plugin-extended pydantic schema is in scope. Otherwise `merge_input_args` + sees an empty PluginManager on the worker and `model_dump(exclude_none=True)` + silently drops plugin-specific cfg fields. + """ + cfg_dict = { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "gradient_accumulation_steps": 1, + "plugins": ["axolotl.integrations.liger.LigerPlugin"], + } + + parent = MagicMock() + parent.validate_config.side_effect = lambda cfg, **_: cfg + + # Patch at the source module so a local `from axolotl.cli.config import ...` + # inside the function also resolves to the mock; also patch the train module + # for top-level imports (raising=False keeps it tolerant of either style). + monkeypatch.setattr("axolotl.cli.config.prepare_plugins", parent.prepare_plugins) + monkeypatch.setattr("axolotl.cli.config.plugin_set_cfg", parent.plugin_set_cfg) + monkeypatch.setattr( + "axolotl.cli.train.prepare_plugins", parent.prepare_plugins, raising=False + ) + monkeypatch.setattr( + "axolotl.cli.train.plugin_set_cfg", parent.plugin_set_cfg, raising=False + ) + monkeypatch.setattr("axolotl.cli.train.validate_config", parent.validate_config) + monkeypatch.setattr("axolotl.cli.train.gpu_capabilities", lambda: ({}, {})) + monkeypatch.setattr("axolotl.cli.train.do_train", MagicMock()) + monkeypatch.setattr("axolotl.cli.train.prepare_optim_env", lambda *_: None) + monkeypatch.setattr("axolotl.cli.train.normalize_config", lambda *_: None) + monkeypatch.setattr("axolotl.cli.train.resolve_dtype", lambda *_: None) + monkeypatch.setattr("axolotl.cli.train.Accelerator", MagicMock()) + + ray_train_func({"cfg": cfg_dict, "cli_args": MagicMock()}) + + # Filter to the calls we care about, in the order they happened. + ordered = [ + call[0] + for call in parent.mock_calls + if call[0] in ("prepare_plugins", "validate_config", "plugin_set_cfg") + ] + assert ordered == ["prepare_plugins", "validate_config", "plugin_set_cfg"], ( + f"Expected prepare_plugins -> validate_config -> plugin_set_cfg; got {ordered}" + ) + + +def test_ray_train_func_skips_plugin_registration_when_no_plugins(monkeypatch): + """When no plugins are configured, neither `prepare_plugins` nor + `plugin_set_cfg` should be invoked on the worker.""" + cfg_dict = { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "gradient_accumulation_steps": 1, + } + + prepare_plugins_mock = MagicMock() + plugin_set_cfg_mock = MagicMock() + + monkeypatch.setattr("axolotl.cli.train.prepare_plugins", prepare_plugins_mock) + monkeypatch.setattr("axolotl.cli.train.plugin_set_cfg", plugin_set_cfg_mock) + monkeypatch.setattr( + "axolotl.cli.train.validate_config", MagicMock(side_effect=lambda cfg, **_: cfg) + ) + monkeypatch.setattr("axolotl.cli.train.gpu_capabilities", lambda: ({}, {})) + monkeypatch.setattr("axolotl.cli.train.do_train", MagicMock()) + monkeypatch.setattr("axolotl.cli.train.prepare_optim_env", lambda *_: None) + monkeypatch.setattr("axolotl.cli.train.normalize_config", lambda *_: None) + monkeypatch.setattr("axolotl.cli.train.resolve_dtype", lambda *_: None) + monkeypatch.setattr("axolotl.cli.train.Accelerator", MagicMock()) + + ray_train_func({"cfg": cfg_dict, "cli_args": MagicMock()}) + + prepare_plugins_mock.assert_not_called() + plugin_set_cfg_mock.assert_not_called() From 73d782b1eaeac0b7f4792e8414810925e0f28651 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Wed, 13 May 2026 11:05:14 +0530 Subject: [PATCH 1301/1405] qwen fft mutli (#3605) [skip ci] --- examples/qwen3.5/27b-fft.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/examples/qwen3.5/27b-fft.yaml b/examples/qwen3.5/27b-fft.yaml index ab206b772c..e00c94926b 100644 --- a/examples/qwen3.5/27b-fft.yaml +++ b/examples/qwen3.5/27b-fft.yaml @@ -38,7 +38,7 @@ wandb_log_model: gradient_accumulation_steps: 2 micro_batch_size: 1 num_epochs: 1 -optimizer: adamw_bnb_8bit +optimizer: adamw_torch_fused lr_scheduler: cosine learning_rate: 0.0002 @@ -57,3 +57,14 @@ evals_per_epoch: 4 saves_per_epoch: 1 weight_decay: 0.0 special_tokens: + +fsdp_config: + fsdp_version: 2 + offload_params: false + cpu_ram_efficient_loading: false + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Qwen3_5DecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true + activation_checkpointing: true From 99451aa887bdd16bbad3e36bce96525c28bddac0 Mon Sep 17 00:00:00 2001 From: Leoy Date: Wed, 13 May 2026 13:35:34 +0800 Subject: [PATCH 1302/1405] feat: migrate gc_steps to native torch_empty_cache_steps (#3604) [skip ci] * feat: migrate gc_steps to native torch_empty_cache_steps Transition the custom gc_steps implementation to leverage the native torch_empty_cache_steps parameter from HuggingFace Trainer, as requested in #2689. Changes: - Add torch_empty_cache_steps config option, passed directly to TrainingArguments for native CUDA cache clearing by the Trainer - Add gc_collect_steps config option for explicit control over Python gc.collect() frequency, which the native Trainer does not handle - Deprecate gc_steps with automatic migration: positive values map to both torch_empty_cache_steps and gc_collect_steps; negative values (-1) map only to gc_collect_steps for epoch-end/eval GC - Refactor GCCallback to use gc_collect_steps parameter name and reorder operations (gc.collect() before empty_cache for better memory reclamation) - Add comprehensive tests for config migration and GCCallback Closes #2689 * fix: address review feedback - Fix gc_collect_steps description inconsistency (default is None, not 0) - Add missing gc_collect_steps assertion in test_new_options_take_precedence * style: fix ruff lint and format violations Co-Authored-By: Claude Sonnet 4.6 * chore: comment cleanup --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: NanoCode012 --- src/axolotl/core/builders/base.py | 9 ++- src/axolotl/utils/callbacks/__init__.py | 18 +++-- src/axolotl/utils/schemas/config.py | 44 ++++++++++- tests/patched/test_validation.py | 49 ++++++++++++ tests/utils/callbacks/test_gc_callback.py | 90 +++++++++++++++++++++++ 5 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 tests/utils/callbacks/test_gc_callback.py diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 9cceb1bf81..ad62837cae 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -122,8 +122,11 @@ def get_callbacks(self) -> list[TrainerCallback]: if self.cfg.resume_from_checkpoint: callbacks.append(SkipEvalOnResumeCallback()) - if self.cfg.gc_steps: - callbacks.append(GCCallback(gc_steps=self.cfg.gc_steps)) + gc_collect_steps = getattr(self.cfg, "gc_collect_steps", None) or getattr( + self.cfg, "gc_steps", None + ) + if gc_collect_steps: + callbacks.append(GCCallback(gc_collect_steps=gc_collect_steps)) if self.cfg.dynamic_checkpoint and self.cfg.dynamic_checkpoint.enabled: from axolotl.utils.callbacks.dynamic_checkpoint import ( @@ -573,6 +576,8 @@ def _set_base_training_args( "dion_rank_fraction", "dion_rank_multiple_of", "dataset_num_proc", + # memory management + "torch_empty_cache_steps", ]: if hasattr(self.cfg, arg) and getattr(self.cfg, arg) is not None: training_args_kwargs[arg] = getattr(self.cfg, arg) diff --git a/src/axolotl/utils/callbacks/__init__.py b/src/axolotl/utils/callbacks/__init__.py index edb61441b4..0a278f6ecb 100644 --- a/src/axolotl/utils/callbacks/__init__.py +++ b/src/axolotl/utils/callbacks/__init__.py @@ -878,15 +878,21 @@ def on_train_begin( class GCCallback(TrainerCallback): - """Callback to garbage collect torch cache""" + """Runs ``gc.collect()`` + ``torch.cuda.empty_cache()`` on + ``gc_collect_steps`` intervals and on eval/save/epoch boundaries that + the Trainer's native ``torch_empty_cache_steps`` doesn't cover. The two + settings are complementary; overlapping intervals just double-clear. + """ - def __init__(self, gc_steps: int | None = -1): - self.gc_steps: int = gc_steps or -1 + def __init__(self, gc_collect_steps: int | None = -1, gc_steps: int | None = None): + if gc_steps is not None and gc_collect_steps in (-1, None): + gc_collect_steps = gc_steps + self.gc_collect_steps: int = gc_collect_steps or -1 self.next_gc_on_begin_step: int = -1 def _gc(self): - torch.cuda.empty_cache() gc.collect() + torch.cuda.empty_cache() def on_train_begin( self, @@ -919,7 +925,9 @@ def on_step_end( self._gc() # also GC on the start of the next step after the eval self.next_gc_on_begin_step = state.global_step + 1 - elif self.gc_steps > 0 and state.global_step % self.gc_steps == 0: + elif ( + self.gc_collect_steps > 0 and state.global_step % self.gc_collect_steps == 0 + ): self._gc() elif ( args.save_strategy == SaveStrategy.STEPS diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 9874b9da5d..73e9f88230 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -529,8 +529,25 @@ class AxolotlInputConfig( gc_steps: int | None = Field( default=None, + deprecated=( + "Use `torch_empty_cache_steps` to control CUDA cache clearing and " + "`gc_collect_steps` for Python garbage collection. " + "`gc_steps` will be removed in a future version." + ), json_schema_extra={ - "description": "Run garbage collection every `gc_steps` steps. -1 will run on epoch end and before evaluations. Default is 0 (disabled)." + "description": "Deprecated. Run garbage collection every `gc_steps` steps. Use `torch_empty_cache_steps` and `gc_collect_steps` instead." + }, + ) + torch_empty_cache_steps: int | None = Field( + default=None, + json_schema_extra={ + "description": "Steps between native HF Trainer `torch.cuda.empty_cache()` calls." + }, + ) + gc_collect_steps: int | None = Field( + default=None, + json_schema_extra={ + "description": "Steps between Python `gc.collect()` calls. -1 runs on epoch end and before evals only; None disables." }, ) @@ -1918,6 +1935,31 @@ def default_dataset_num_proc(cls, data): data["dataset_num_proc"] = get_default_process_count() return data + @model_validator(mode="before") + @classmethod + def migrate_gc_steps(cls, data): + gc_steps = data.get("gc_steps") + if gc_steps is not None: + if ( + data.get("torch_empty_cache_steps") is None + and data.get("gc_collect_steps") is None + ): + # Map gc_steps to both new options to preserve original behavior + if gc_steps > 0: + data["torch_empty_cache_steps"] = gc_steps + data["gc_collect_steps"] = gc_steps + LOG.warning( + "`gc_steps` is deprecated; mapping gc_steps=%d to " + "`torch_empty_cache_steps` and `gc_collect_steps`.", + gc_steps, + ) + else: + LOG.warning( + "`gc_steps` ignored; `torch_empty_cache_steps` / " + "`gc_collect_steps` take precedence." + ) + return data + @model_validator(mode="before") @classmethod def check_deduplication_with_streaming(cls, data): diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 71400c4ae6..0b98ce3521 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -1738,6 +1738,55 @@ def test_dataloader_auto_defaults(self, minimal_cfg): assert new_cfg.dataloader_prefetch_factor == 256 +class TestGCStepsMigration(BaseValidation): + """ + Tests for gc_steps -> torch_empty_cache_steps / gc_collect_steps migration + """ + + def test_gc_steps_maps_to_new_options(self, minimal_cfg): + cfg = DictDefault({**minimal_cfg, "gc_steps": 10}) + + new_cfg = validate_config(cfg, {"n_gpu": 1}, {"torch_version": "2.6.0"}) + + assert new_cfg.torch_empty_cache_steps == 10 + assert new_cfg.gc_collect_steps == 10 + + def test_gc_steps_negative_maps_gc_collect_only(self, minimal_cfg): + cfg = DictDefault({**minimal_cfg, "gc_steps": -1}) + + new_cfg = validate_config(cfg, {"n_gpu": 1}, {"torch_version": "2.6.0"}) + + # -1 means only epoch end/eval GC, not periodic; torch_empty_cache_steps + # should not be set for negative values + assert new_cfg.torch_empty_cache_steps is None + assert new_cfg.gc_collect_steps == -1 + + def test_new_options_take_precedence(self, minimal_cfg): + cfg = DictDefault({**minimal_cfg, "gc_steps": 10, "torch_empty_cache_steps": 5}) + + new_cfg = validate_config(cfg, {"n_gpu": 1}, {"torch_version": "2.6.0"}) + + # New options take precedence; gc_steps migration is skipped + assert new_cfg.torch_empty_cache_steps == 5 + assert new_cfg.gc_collect_steps is None + + def test_torch_empty_cache_steps_standalone(self, minimal_cfg): + cfg = DictDefault({**minimal_cfg, "torch_empty_cache_steps": 8}) + + new_cfg = validate_config(cfg, {"n_gpu": 1}, {"torch_version": "2.6.0"}) + + assert new_cfg.torch_empty_cache_steps == 8 + assert new_cfg.gc_collect_steps is None + + def test_gc_collect_steps_standalone(self, minimal_cfg): + cfg = DictDefault({**minimal_cfg, "gc_collect_steps": 5}) + + new_cfg = validate_config(cfg, {"n_gpu": 1}, {"torch_version": "2.6.0"}) + + assert new_cfg.gc_collect_steps == 5 + assert new_cfg.torch_empty_cache_steps is None + + class TestSyntheticDatasetValidation(BaseValidation): """ Tests for synthetic dataset config validation diff --git a/tests/utils/callbacks/test_gc_callback.py b/tests/utils/callbacks/test_gc_callback.py new file mode 100644 index 0000000000..c03e171352 --- /dev/null +++ b/tests/utils/callbacks/test_gc_callback.py @@ -0,0 +1,90 @@ +"""Tests for the GCCallback""" + +from unittest.mock import MagicMock, patch + +from axolotl.utils.callbacks import GCCallback + + +class TestGCCallback: + """Tests for GCCallback which handles Python gc.collect() during training.""" + + def test_init_with_gc_collect_steps(self): + cb = GCCallback(gc_collect_steps=10) + assert cb.gc_collect_steps == 10 + + def test_init_with_negative_gc_collect_steps(self): + cb = GCCallback(gc_collect_steps=-1) + assert cb.gc_collect_steps == -1 + + def test_init_with_legacy_gc_steps(self): + cb = GCCallback(gc_steps=5) + assert cb.gc_collect_steps == 5 + + def test_init_gc_collect_steps_overrides_gc_steps(self): + cb = GCCallback(gc_collect_steps=10, gc_steps=5) + assert cb.gc_collect_steps == 10 + + def test_init_default(self): + cb = GCCallback() + assert cb.gc_collect_steps == -1 + + @patch("axolotl.utils.callbacks.gc.collect") + @patch("axolotl.utils.callbacks.torch.cuda.empty_cache") + def test_gc_calls_collect_and_empty_cache(self, mock_empty_cache, mock_gc_collect): + cb = GCCallback(gc_collect_steps=1) + cb._gc() + mock_gc_collect.assert_called_once() + mock_empty_cache.assert_called_once() + + @patch("axolotl.utils.callbacks.gc.collect") + @patch("axolotl.utils.callbacks.torch.cuda.empty_cache") + def test_on_step_end_periodic_gc(self, mock_empty_cache, mock_gc_collect): + cb = GCCallback(gc_collect_steps=5) + args = MagicMock() + args.save_strategy = "no" + state = MagicMock() + state.global_step = 10 + state.save_steps = 0 + state.max_steps = 100 + control = MagicMock() + control.should_evaluate = False + + cb.on_step_end(args, state, control) + + # Step 10 % 5 == 0, so GC should have been called + mock_gc_collect.assert_called_once() + + @patch("axolotl.utils.callbacks.gc.collect") + @patch("axolotl.utils.callbacks.torch.cuda.empty_cache") + def test_on_step_end_no_gc_when_not_on_interval( + self, mock_empty_cache, mock_gc_collect + ): + cb = GCCallback(gc_collect_steps=5) + args = MagicMock() + args.save_strategy = "no" + state = MagicMock() + state.global_step = 7 + state.save_steps = 0 + state.max_steps = 100 + control = MagicMock() + control.should_evaluate = False + + cb.on_step_end(args, state, control) + + # Step 7 % 5 != 0, so GC should not have been called + mock_gc_collect.assert_not_called() + + @patch("axolotl.utils.callbacks.gc.collect") + @patch("axolotl.utils.callbacks.torch.cuda.empty_cache") + def test_on_step_end_gc_before_eval(self, mock_empty_cache, mock_gc_collect): + cb = GCCallback(gc_collect_steps=-1) + args = MagicMock() + state = MagicMock() + state.global_step = 3 + control = MagicMock() + control.should_evaluate = True + + cb.on_step_end(args, state, control) + + mock_gc_collect.assert_called_once() + assert cb.next_gc_on_begin_step == 4 From 3423b2fe611c8ac2d2236b2a59f1893a50a58960 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Wed, 13 May 2026 11:05:50 +0530 Subject: [PATCH 1303/1405] fix un supported tensor for pTq (#3581) [skip ci] * fix un supported tensor for pTq * test * lint * use save_model + try except * mx fixes * mx fixes --------- Co-authored-by: Wing Lian --- src/axolotl/cli/quantize.py | 25 +++---- src/axolotl/utils/quantization.py | 67 ++++++++++++++--- tests/e2e/test_quantization.py | 119 ++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 27 deletions(-) diff --git a/src/axolotl/cli/quantize.py b/src/axolotl/cli/quantize.py index 052d79d7a4..177d04de6d 100644 --- a/src/axolotl/cli/quantize.py +++ b/src/axolotl/cli/quantize.py @@ -15,6 +15,7 @@ get_quantization_config, quantization_config_to_str, quantize_model, + save_quantized_model, ) LOG = get_logger(__name__) @@ -93,22 +94,16 @@ def do_quantize( weight_dtype, activation_dtype, group_size ) - LOG.info(f"Saving quantized model to: {str(Path(output_dir) / 'quantized')}.") - try: - model.save_pretrained( - str(Path(output_dir) / "quantized"), - progressbar=True, - ) - except NotImplementedError: - LOG.warning( - "Model weight conversions do not support reverse_op, " - "retrying save with save_original_format=False" - ) - model.save_pretrained( - str(Path(output_dir) / "quantized"), - progressbar=True, - save_original_format=False, + save_path = str(Path(output_dir) / "quantized") + is_mx = getattr(model, "_is_mx_quantized", False) + if is_mx: + LOG.info( + f"Saving MX-quantized model to: {save_path} " + "(using torch.save — MXTensor does not support safetensors yet)." ) + else: + LOG.info(f"Saving quantized model to: {save_path}.") + save_quantized_model(model, save_path, progressbar=True) tokenizer.save_pretrained( str(Path(output_dir) / "quantized"), progressbar=True, diff --git a/src/axolotl/utils/quantization.py b/src/axolotl/utils/quantization.py index 04b2c63416..ceaa9e0f39 100644 --- a/src/axolotl/utils/quantization.py +++ b/src/axolotl/utils/quantization.py @@ -34,8 +34,6 @@ except (ImportError, RuntimeError): pass - # int4 weight config imports will fail on machines with fbgemm-gpu installed - # without a CUDA runtime available so we do this safely try: from torchao.quantization.quant_api import Int4WeightOnlyConfig @@ -123,6 +121,8 @@ def get_quantization_config( return NVFP4WeightOnlyConfig() if weight_dtype == TorchAOQuantDType.mxfp4: + from torchao.prototype.mx_formats import MXDynamicActivationMXWeightConfig + # MXFP4 uses block_size=32 by default (vs NVFP4's 16) block_size = group_size if group_size is not None else 32 if block_size != 32: @@ -130,8 +130,6 @@ def get_quantization_config( "MXFP4 quantization must use a block_size (group_size) of 32" ) - from torchao.prototype.mx_formats import MXDynamicActivationMXWeightConfig - return MXDynamicActivationMXWeightConfig( activation_dtype=torch.float4_e2m1fn_x2, weight_dtype=torch.float4_e2m1fn_x2, @@ -148,10 +146,11 @@ def _attach_torchao_quantizer( ): """Attach a TorchAoHfQuantizer to the model so save_pretrained uses torchao's flatten_tensor_state_dict path, preserving quantized weights - (e.g. MXTensor qdata+scale) in the safetensors file. + in the safetensors file. - Without this, save_pretrained falls through to the default path which - calls safetensors storage_ptr() on tensor subclasses and crashes. + Note: This does NOT work for MX formats (MXTensor) because MXTensor + lacks the ``tensor_data_names`` attribute that flatten_tensor_state_dict + requires. MX formats use ``safe_serialization=False`` instead. """ from transformers import TorchAoConfig from transformers.quantizers.quantizer_torchao import TorchAoHfQuantizer @@ -202,11 +201,55 @@ def quantize_model( filter_fn=lambda m, _: isinstance(m, torch.nn.Embedding), ) - _attach_torchao_quantizer( - model, - linear_ptq_config, - include_input_output_embeddings=bool(quantize_embedding), - ) + is_mx = False + try: + from torchao.prototype.mx_formats import MXDynamicActivationMXWeightConfig + + is_mx = isinstance(linear_ptq_config, MXDynamicActivationMXWeightConfig) + except ImportError: + pass + + if is_mx: + # MXTensor lacks tensor_data_names so flatten_tensor_state_dict (safetensors) + # cannot serialize it. Mark the model so the caller can use + # safe_serialization=False (torch.save) which supports __tensor_flatten__. + model._is_mx_quantized = True + else: + _attach_torchao_quantizer( + model, + linear_ptq_config, + include_input_output_embeddings=bool(quantize_embedding), + ) + + +def save_quantized_model(model, save_dir, **kwargs): + """Save a quantized model, handling MXTensor serialization. + + MXTensor does not have a valid storage pointer, which causes + ``save_pretrained`` to crash (both in ``remove_tied_weights_from_state_dict`` + via ``id_tensor_storage``, and in safetensors serialization). + Transformers >=5.5 removed the ``safe_serialization`` parameter entirely. + + For MX-quantized models we save the config/generation_config via + ``save_pretrained`` machinery and the weights via ``torch.save``. + """ + import os + + is_mx = getattr(model, "_is_mx_quantized", False) + if not is_mx: + model.save_pretrained(save_dir, **kwargs) + return + + os.makedirs(save_dir, exist_ok=True) + + # Save config, generation_config, etc. + model.config.save_pretrained(save_dir) + if hasattr(model, "generation_config") and model.generation_config is not None: + model.generation_config.save_pretrained(save_dir) + + # Save weights via torch.save (supports __tensor_flatten__) + weights_path = os.path.join(save_dir, "pytorch_model.bin") + torch.save(model.state_dict(), weights_path) def _make_qat_config( diff --git a/tests/e2e/test_quantization.py b/tests/e2e/test_quantization.py index bb8ce969cc..94102f6ebb 100644 --- a/tests/e2e/test_quantization.py +++ b/tests/e2e/test_quantization.py @@ -23,6 +23,7 @@ get_quantization_config, prepare_model_for_qat, quantize_model, + save_quantized_model, ) from axolotl.utils.schemas.enums import TorchAOQuantDType from axolotl.utils.schemas.quantization import QATConfig @@ -362,6 +363,124 @@ def test_convert_qat_model(self, model): assert isinstance(model.lm_head.weight, nn.Parameter) +class TestMXQuantizeSaveLoad: + """Tests for MX format (mxfp4) quantize-save-load round-trip via save_pretrained. + + Uses a tiny HF model built from config (no download) so tests exercise the + real save_pretrained / from_pretrained code path — the same one the CLI uses. + MX format models are saved with safe_serialization=False (torch.save) because + MXTensor does not yet support safetensors serialization. + """ + + @staticmethod + def _make_tiny_model(): + """Build a minimal HF causal-LM that can be quantized on CPU.""" + from transformers import Qwen2Config + + config = Qwen2Config( + hidden_size=64, + intermediate_size=128, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + vocab_size=256, + max_position_embeddings=64, + torch_dtype="bfloat16", + ) + model = AutoModelForCausalLM.from_config(config).to(torch.bfloat16) + return model + + @require_torch_2_8_0 + def test_mxfp4_quantize_save_pretrained(self, tmp_path): + """quantize_model(mxfp4) -> save_pretrained -> from_pretrained round-trip.""" + from torchao.prototype.mx_formats.mx_tensor import MXTensor + + model = self._make_tiny_model() + original_keys = set(model.state_dict().keys()) + + quantize_model(model, TorchAOQuantDType.mxfp4, 32) + + # Weights should be MXTensor after quantization + for module in model.modules(): + if isinstance(module, nn.Linear): + assert isinstance(module.weight, MXTensor) + + # Model should be flagged for MX-style save + assert getattr(model, "_is_mx_quantized", False) + + # save_pretrained with safe_serialization=False (torch.save path) + save_dir = str(tmp_path / "mxfp4_model") + save_quantized_model(model, save_dir) + + # Verify checkpoint files were written + import glob + + assert glob.glob(f"{save_dir}/*.bin") or glob.glob(f"{save_dir}/**/*.bin") + + # from_pretrained should load without error + loaded = AutoModelForCausalLM.from_pretrained( + save_dir, torch_dtype=torch.bfloat16 + ) + loaded_keys = set(loaded.state_dict().keys()) + assert original_keys == loaded_keys, ( + f"Key mismatch: missing={original_keys - loaded_keys}, " + f"extra={loaded_keys - original_keys}" + ) + + @require_torch_2_8_0 + def test_mxfp4_is_mx_flag_set(self): + """quantize_model sets _is_mx_quantized for MX configs.""" + model = self._make_tiny_model() + quantize_model(model, TorchAOQuantDType.mxfp4, 32) + assert getattr(model, "_is_mx_quantized", False) + + @require_torch_2_8_0 + @requires_cuda_ge_8_9 + def test_non_mx_uses_torchao_quantizer(self): + """Non-MX quantization attaches TorchAoHfQuantizer, not _is_mx_quantized.""" + model = self._make_tiny_model() + try: + quantize_model(model, TorchAOQuantDType.int4, group_size=32) + except ImportError: + pytest.skip("int4 quantization requires mslk >= 1.0.0") + assert not getattr(model, "_is_mx_quantized", False) + assert hasattr(model, "hf_quantizer") + + @require_torch_2_8_0 + def test_mxfp4_qat_then_ptq_save_pretrained(self, tmp_path): + """Full QAT -> convert -> PTQ -> save_pretrained -> from_pretrained.""" + from torchao.prototype.mx_formats.mx_tensor import MXTensor + from torchao.prototype.qat import MXFakeQuantizedLinear + + model = self._make_tiny_model() + original_keys = set(model.state_dict().keys()) + + # QAT preparation + prepare_model_for_qat(model, TorchAOQuantDType.mxfp4, 32) + for module in model.modules(): + if isinstance(module, nn.Linear): + assert isinstance(module, MXFakeQuantizedLinear) + + # Convert QAT back to normal linear + convert_qat_model(model) + + # PTQ quantize + quantize_model(model, TorchAOQuantDType.mxfp4, 32) + for module in model.modules(): + if isinstance(module, nn.Linear): + assert isinstance(module.weight, MXTensor) + + # save_pretrained round-trip + save_dir = str(tmp_path / "mxfp4_qat_model") + save_quantized_model(model, save_dir) + + loaded = AutoModelForCausalLM.from_pretrained( + save_dir, torch_dtype=torch.bfloat16 + ) + loaded_keys = set(loaded.state_dict().keys()) + assert original_keys == loaded_keys + + class TestQuantizationCallback: """ Test QATCallback From 4f4d5d8738a97d779a9ce0bf0c93fb03471a6912 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 13 May 2026 12:37:08 +0700 Subject: [PATCH 1304/1405] fix: make prepare_context_parallel_inputs no-op (#3520) [skip ci] --- src/axolotl/loaders/patch_manager.py | 7 -- .../accelerate/parallelism_config.py | 8 +++ .../transformers/trainer_context_parallel.py | 72 ------------------- .../test_trainer_context_parallel_patch.py | 66 ----------------- 4 files changed, 8 insertions(+), 145 deletions(-) delete mode 100644 src/axolotl/monkeypatch/transformers/trainer_context_parallel.py delete mode 100644 tests/monkeypatch/test_trainer_context_parallel_patch.py diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 68952014f9..4fd2ba035d 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -141,13 +141,6 @@ def _apply_transformers_patches(self): patch_evaluation_loop() patch_maybe_log_save_evaluate() - if self.cfg.context_parallel_size > 1: - from axolotl.monkeypatch.transformers.trainer_context_parallel import ( - patch_prepare_context_parallel_inputs, - ) - - patch_prepare_context_parallel_inputs() - def apply_post_model_build_patches(self, model: PreTrainedModel): """Apply patches right after model build, before post-load setup.""" if self.cfg.model_config_type == "nemotron_h": diff --git a/src/axolotl/monkeypatch/accelerate/parallelism_config.py b/src/axolotl/monkeypatch/accelerate/parallelism_config.py index ebd9a6f0d7..56636d6979 100644 --- a/src/axolotl/monkeypatch/accelerate/parallelism_config.py +++ b/src/axolotl/monkeypatch/accelerate/parallelism_config.py @@ -81,6 +81,7 @@ def patch_prepare_cp(): import contextlib from accelerate import Accelerator + from transformers import Trainer def patched_prepare_cp(self, *args): if self.parallelism_config.cp_backend == "deepspeed": @@ -95,4 +96,11 @@ def _noop_cp_context( self._cp_context = _noop_cp_context return args + def _noop_prepare_context_parallel_inputs(self, model, inputs): + return contextlib.nullcontext, inputs + + # prevent double CP partition Accelerator._prepare_cp = patched_prepare_cp + + # remove unneeded calculation upstream + Trainer._prepare_context_parallel_inputs = _noop_prepare_context_parallel_inputs diff --git a/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py b/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py deleted file mode 100644 index 15f90423e7..0000000000 --- a/src/axolotl/monkeypatch/transformers/trainer_context_parallel.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Monkey patch to allow context parallelism with FlashAttention in HF Trainer.""" - -from __future__ import annotations - -import importlib -import inspect - -from transformers import Trainer - -from axolotl.monkeypatch.utils import detab_code -from axolotl.utils.logging import get_logger - -LOG = get_logger(__name__) - -GUARD_PATTERN = 'if model.config._attn_implementation != "sdpa":' -PATCHED_GUARD = 'if (attn_impl := (getattr(model.config, "_attn_implementation", None) or getattr(model.model.config, "_attn_implementation", None))) and attn_impl not in ("sdpa", "flash_attention_2"):' - - -def patch_prepare_context_parallel_inputs() -> None: - """Relax the SDPA-only guard when running context parallelism with FlashAttention.""" - if getattr(Trainer, "_axolotl_prepare_context_parallel_inputs_patched", False): - LOG.debug("Trainer._prepare_context_parallel_inputs already patched") - return - - try: - original_source = inspect.getsource(Trainer._prepare_context_parallel_inputs) - except OSError as exc: # pragma: no cover - occurs when source is unavailable - LOG.warning("Unable to patch Trainer._prepare_context_parallel_inputs: %s", exc) - return - - if GUARD_PATTERN not in original_source: - LOG.warning( - "Expected guard not found in Trainer._prepare_context_parallel_inputs; \n" - "skipping FlashAttention context parallelism patch" - ) - return - - patched_source = original_source.replace(GUARD_PATTERN, PATCHED_GUARD) - patched_source, _ = detab_code(patched_source) - patched_source = patched_source.replace( - "def _prepare_context_parallel_inputs(", - "def axolotl_prepare_context_parallel_inputs(", - 1, - ) - - module_name = Trainer.__module__ - module = importlib.import_module(module_name) - - # import symbols referenced in the method so exec can succeed - items_to_import = [] - for item in dir(module): - if item in patched_source: - items_to_import.append(item) - - # Use a separate namespace to capture the exec'd function - namespace = {} - exec(f"from {module_name} import ({', '.join(items_to_import)})", namespace) - exec(patched_source, namespace) - - # Explicitly get the function from the namespace - axolotl_prepare_context_parallel_inputs = namespace[ - "axolotl_prepare_context_parallel_inputs" - ] - Trainer._original_prepare_context_parallel_inputs = ( - Trainer._prepare_context_parallel_inputs - ) - Trainer._prepare_context_parallel_inputs = axolotl_prepare_context_parallel_inputs - Trainer._axolotl_prepare_context_parallel_inputs_source = patched_source - Trainer._axolotl_prepare_context_parallel_inputs_patched = True - LOG.debug( - "Patched Trainer._prepare_context_parallel_inputs for FlashAttention + CP" - ) diff --git a/tests/monkeypatch/test_trainer_context_parallel_patch.py b/tests/monkeypatch/test_trainer_context_parallel_patch.py deleted file mode 100644 index 84c883e91b..0000000000 --- a/tests/monkeypatch/test_trainer_context_parallel_patch.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Tests for the HF Trainer context parallel patch.""" - -import pytest -from transformers import Trainer - -from axolotl.monkeypatch.transformers.trainer_context_parallel import ( - GUARD_PATTERN, - PATCHED_GUARD, - patch_prepare_context_parallel_inputs, -) - - -@pytest.fixture -def restore_trainer_prepare_method(): - """Ensure Trainer._prepare_context_parallel_inputs is restored after a test.""" - original_method = getattr( - Trainer, - "_original_prepare_context_parallel_inputs", - Trainer._prepare_context_parallel_inputs, - ) - patched_attr_present = hasattr( - Trainer, "_axolotl_prepare_context_parallel_inputs_patched" - ) - - yield - - Trainer._prepare_context_parallel_inputs = original_method - if patched_attr_present: - delattr(Trainer, "_axolotl_prepare_context_parallel_inputs_patched") - if hasattr(Trainer, "_original_prepare_context_parallel_inputs"): - delattr(Trainer, "_original_prepare_context_parallel_inputs") - if hasattr(Trainer, "_axolotl_prepare_context_parallel_inputs_source"): - delattr(Trainer, "_axolotl_prepare_context_parallel_inputs_source") - - -def test_patch_attention_guard(restore_trainer_prepare_method): - """Patch should swap the guard to allow sdpa or flash attention.""" - # Ensure we start from the unpatched method - if hasattr(Trainer, "_original_prepare_context_parallel_inputs"): - Trainer._prepare_context_parallel_inputs = ( - Trainer._original_prepare_context_parallel_inputs - ) - delattr(Trainer, "_original_prepare_context_parallel_inputs") - if hasattr(Trainer, "_axolotl_prepare_context_parallel_inputs_patched"): - delattr(Trainer, "_axolotl_prepare_context_parallel_inputs_patched") - - patch_prepare_context_parallel_inputs() - - patched_method = Trainer._prepare_context_parallel_inputs - assert patched_method is not None - assert getattr(Trainer, "_axolotl_prepare_context_parallel_inputs_patched", False) - - source = Trainer._axolotl_prepare_context_parallel_inputs_source - assert GUARD_PATTERN not in source - assert PATCHED_GUARD in source - - -def test_patch_is_idempotent(restore_trainer_prepare_method): - """Calling the patch twice should leave the same patched function in place.""" - patch_prepare_context_parallel_inputs() - first_patched = Trainer._prepare_context_parallel_inputs - - patch_prepare_context_parallel_inputs() - second_patched = Trainer._prepare_context_parallel_inputs - - assert first_patched is second_patched From bcb3b9eb1f61b2ba2b6be8c5d3b13b592c04513b Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Wed, 13 May 2026 11:07:55 +0530 Subject: [PATCH 1305/1405] test for moe activation vram leak (#3649) * activation vram leak * lint * enter fix for vram leak * Update examples/mistral4/qlora-text.yml * undo * leak test --------- Co-authored-by: Your Name --- .../mixins/activation_checkpointing.py | 19 ++++ tests/e2e/test_activation_offloading.py | 104 ++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/src/axolotl/core/trainers/mixins/activation_checkpointing.py b/src/axolotl/core/trainers/mixins/activation_checkpointing.py index b61c45feed..dd892bebce 100644 --- a/src/axolotl/core/trainers/mixins/activation_checkpointing.py +++ b/src/axolotl/core/trainers/mixins/activation_checkpointing.py @@ -22,6 +22,25 @@ LOG = get_logger(__name__) +# TODO(#3638): drop once TRL pin includes huggingface/trl#5730. Mirrors the +# upstream __enter__ override — clears cross-step state on context re-entry +# so saved tensors that never unpack during backward (MoE / torch.compile) +# don't accumulate as leaked GPU references. +def _axolotl_offload_enter(self): + self.tracker.clear() + self.storage_to_tensor_id.clear() + if self.use_streams: + self.fwd_stash.clear() + self.bwd_tensor_stash.clear() + self.bwd_ev_stash.clear() + self.is_first_forward_call = True + self.is_first_backward_call = True + return super(OffloadActivations, self).__enter__() + + +OffloadActivations.__enter__ = _axolotl_offload_enter + + class ActivationOffloadingMixin(Trainer): """ Trainer mixin class for activation checkpointing w offloading diff --git a/tests/e2e/test_activation_offloading.py b/tests/e2e/test_activation_offloading.py index 5715e68baa..ee3fdbb6cf 100644 --- a/tests/e2e/test_activation_offloading.py +++ b/tests/e2e/test_activation_offloading.py @@ -5,6 +5,9 @@ import pytest from axolotl.common.datasets import load_datasets +from axolotl.core.trainers.mixins.activation_checkpointing import ( + ActivationOffloadingMixin, +) from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault @@ -77,3 +80,104 @@ def test_activation_offloading( train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + + def test_no_vram_leak_regression(self, temp_dir, monkeypatch): + """#3638 regression — fail on linear VRAM growth across training steps. + + The bug: ``OffloadActivations.__enter__`` doesn't clear cross-step + state, so a saved tensor that never unpacks during backward + (MoE / ``torch.compile``) sits in ``ctx.tracker`` forever — and its + GPU storage stays alive. Across many steps memory grows linearly. + + Tiny CI models won't exhibit the upstream MoE/compile unpack failure + on their own, so we *inject* the same leftover: after every step we + stash a small CUDA tensor into ``ctx.tracker``. The fix clears it on + the next ``__enter__`` (memory flat); without the fix it accumulates + (memory grows ~constant bytes/step). The fail mode is the bug's own + symptom — ``torch.cuda.memory_allocated`` increasing across steps. + """ + import torch + + if not torch.cuda.is_available(): + pytest.skip("VRAM-leak test requires CUDA") + + mem_per_step: list[int] = [] + seed_id = [10**9] + seed_bytes = 4 * 1024 * 1024 # 4 MB / step + + original_step = ActivationOffloadingMixin.training_step + + def wrapped_step(self, *args, **kwargs): + torch.cuda.synchronize() + mem_per_step.append(torch.cuda.memory_allocated()) + out = original_step(self, *args, **kwargs) + + # Inject the MoE-style leftover: a CUDA tensor stuck in + # OffloadActivations.tracker. The local `seed` ref dies on + # return — only ctx.tracker keeps it alive, so the next + # __enter__'s clear (with the fix) actually releases the GPU + # memory. Without the fix these accumulate step-over-step. + ctx = self.activation_offload_context + seed_id[0] += 1 + seed = torch.empty(seed_bytes // 2, dtype=torch.float16, device="cuda") + ctx.tracker[seed_id[0]] = (seed, False, None, None, None) + # Stop the next forward's pack_tensor from raising on its + # "tracker should have been cleared" guard. With the fix this + # flag gets reset by __enter__ anyway; on main it would + # otherwise crash before our VRAM measurement on step 2. + ctx.is_first_forward_call = False + return out + + monkeypatch.setattr(ActivationOffloadingMixin, "training_step", wrapped_step) + + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 1024, + "val_set_size": 0.0, + "special_tokens": {"pad_token": "<|endoftext|>"}, + "datasets": [ + {"path": "mhenrichsen/alpaca_2k_test", "type": "alpaca"}, + ], + "max_steps": 10, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 1e-5, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "bf16": "auto", + "gradient_checkpointing": True, + "activation_offloading": True, + "save_first_step": False, + } + ) + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + train(cfg=cfg, dataset_meta=dataset_meta) + + # Drop warm-up steps; allocator settling distorts early samples. + warmup = 3 + samples = mem_per_step[warmup:] + assert len(samples) >= 5, ( + f"need >= 5 post-warmup samples, got {len(samples)} " + f"(total {len(mem_per_step)})" + ) + + # Injection is 4 MB/step. With the fix __enter__ clears each seed + # before the next step → growth ≈ 0. Without the fix seeds pile up + # → growth ≈ 4 MB × (steps-1). 10 MB is well above allocator jitter + # and well below the leaky-build floor. + growth_mb = (samples[-1] - samples[0]) / (1024**2) + tolerance_mb = 10 + + per_step_mb = [round(m / 1024**2, 1) for m in mem_per_step] + assert growth_mb < tolerance_mb, ( + f"VRAM grew {growth_mb:.1f} MB across {len(samples)} post-warmup " + f"steps — linear-increase signature of the #3638 VRAM leak. " + f"Per-step memory_allocated (MB): {per_step_mb}" + ) + + check_model_output_exists(temp_dir, cfg) From 4f855f6282b7c9ddd3e33dfe8cb332108b1ab617 Mon Sep 17 00:00:00 2001 From: minyichen <96753147+cyc00518@users.noreply.github.com> Date: Wed, 13 May 2026 14:34:58 +0800 Subject: [PATCH 1306/1405] feat: support field_messages in multimodal collator (#3628) [skip ci] * Support field_messages in multimodal collator * Address review: tighten schema detection and field_messages precedence - _is_legacy_schema now requires both `from` and `value` so that rows whose first message merely happens to carry a `from` key (e.g., custom metadata) are not misrouted into the legacy conversion branch. - _get_messages_field now prefers a configured `field_messages` over the canonical "messages" key, so a stale "messages" column on the same row does not silently override the user's intent. With the default field_messages=("messages",) behavior is unchanged. - Updated __call__ docstring to mention the configurable third source key and align the Raises wording with the actual error message. * docs: clarify field_messages is a rename-only escape hatch Document that the recommended multimodal column name is messages with the OpenAI-style role/content schema, and that field_messages is a rename-only escape hatch for upstream data stored under a different column name. The inner schema must still match one of the two documented formats. Addresses the suggestion from @ved1beta. * test: add unit tests for ProcessingStrategy field_messages plumbing Add 20 unit tests covering: - _normalize_field_messages: None/str/list/falsy entries - _is_legacy_schema: requires both from and value to flag legacy - _get_messages_field: configured field takes priority over a stale messages column, falls back to messages when the configured field is absent - ProcessingStrategy.__call__: canonical messages/conversations keys, custom field with auto-detected OpenAI vs ShareGPT schema, and the ValueError raised when none of the supported source keys are present. Lifts patch coverage on processing_strategies.py from ~14% to ~75%. * chore: cleanup --------- Co-authored-by: NanoCode012 --- docs/multimodal.qmd | 4 + src/axolotl/core/builders/causal.py | 8 ++ src/axolotl/processing_strategies.py | 66 ++++++++++- tests/test_processing_strategies.py | 161 +++++++++++++++++++++++++++ 4 files changed, 237 insertions(+), 2 deletions(-) diff --git a/docs/multimodal.qmd b/docs/multimodal.qmd index aabff03f26..5197f48b10 100644 --- a/docs/multimodal.qmd +++ b/docs/multimodal.qmd @@ -285,6 +285,10 @@ For multi-modal datasets, we adopt an extended `chat_template` format similar to - `role` can be `system`, `user`, `assistant`, etc. - `content` is a list of `type` and (`text`, `image`, `path`, `url`, `base64`, or `audio`). +::: {.callout-note} +The recommended column name is `messages` with the OpenAI-style `role`/`content` schema shown above. If your upstream data already lives under a different column name (e.g., `conversations`, `dialogue`), set `field_messages: ` in the dataset config; this is a rename-only escape hatch — the inner schema must still match one of the two formats above (`role`/`content` or ShareGPT's `from`/`value`). +::: + ### Image ::: {.callout-note} diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index b5f365bce7..985414053c 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -545,6 +545,13 @@ def _ds_get(cfg_obj, key): if self.cfg.role_boundaries: role_boundaries_override = list(self.cfg.role_boundaries) + # Deduped union of per-dataset `field_messages` for the MM collator. + field_messages = [] + for dataset_cfg in ds_entries: + field_message = _ds_get(dataset_cfg, "field_messages") + if field_message and field_message not in field_messages: + field_messages.append(field_message) + # build() calls build_collator twice (eval + train); log once. if not is_eval: LOG.info( @@ -566,6 +573,7 @@ def _ds_get(cfg_obj, key): roles_to_train=roles_to_train, train_on_eos=train_on_eos, role_boundaries_override=role_boundaries_override, + field_messages=field_messages or None, ) elif self.cfg.batch_flattening: collator = DataCollatorWithFlattening diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 217bc765b5..497e5b5108 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -58,11 +58,13 @@ def __init__( roles_to_train: Optional[list[str]] = None, train_on_eos: Optional[str] = None, role_boundaries_override: Optional[list[dict]] = None, + field_messages: str | list[str] | tuple[str, ...] | None = None, ): self.processor = processor self.chat_template = chat_template self.image_token = None self.image_token_id = None + self.field_messages = self._normalize_field_messages(field_messages) self.image_size = image_size self.image_resize_algorithm = ( @@ -139,8 +141,38 @@ def _build_role_boundaries(self) -> list[RoleBoundary]: """Subclasses declare role boundaries here; [] opts out of role masking.""" return [] + @staticmethod + def _normalize_field_messages( + field_messages: str | list[str] | tuple[str, ...] | None, + ) -> tuple[str, ...]: + if field_messages is None: + return ("messages",) + if isinstance(field_messages, str): + return (field_messages,) + return tuple(name for name in field_messages if name) + + def _get_messages_field(self, example: dict) -> str | None: + # Configured field wins so a stale `messages` column can't override it. + for name in self.field_messages: + if name in example and example[name] is not None: + return name + if "messages" in example and example["messages"] is not None: + return "messages" + return None + + @staticmethod + def _is_legacy_schema(messages) -> bool: + """Detect ShareGPT schema: first message has both ``from`` and ``value``.""" + return ( + isinstance(messages, list) + and bool(messages) + and isinstance(messages[0], dict) + and "from" in messages[0] + and "value" in messages[0] + ) + def __call__(self, examples: list[dict]) -> list[dict]: - """Normalize examples to OpenAI ``messages`` format (accepts legacy ``conversations``).""" + """Normalize examples to OpenAI ``messages`` (accepts legacy ``conversations`` or custom ``field_messages``).""" role_mapping = { "human": "user", "gpt": "assistant", @@ -188,9 +220,21 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: processed_examples = [] for example in examples: + messages_field = self._get_messages_field(example) + # Re-route a custom field into the canonical key whose schema it matches; + # the canonical "messages"/"conversations" branches below are unchanged. + if messages_field and messages_field not in {"messages", "conversations"}: + msgs = example[messages_field] + target = "conversations" if self._is_legacy_schema(msgs) else "messages" + example = dict(example) + example[target] = msgs + example.pop(messages_field, None) + if target == "conversations": + example.pop("messages", None) + if not ("messages" in example or "conversations" in example): raise ValueError( - "Only `messages` and `conversations` message keys are currently supported." + "Only configured `field_messages`, `messages`, and `conversations` message keys are currently supported." ) if "messages" in example and example["messages"] is not None: @@ -519,6 +563,7 @@ def __init__( roles_to_train: Optional[list[str]] = None, train_on_eos: Optional[str] = None, role_boundaries_override: Optional[list[dict]] = None, + field_messages: str | list[str] | tuple[str, ...] | None = None, ): super().__init__( processor, @@ -529,6 +574,7 @@ def __init__( roles_to_train=roles_to_train, train_on_eos=train_on_eos, role_boundaries_override=role_boundaries_override, + field_messages=field_messages, ) self.image_token = "<|image_pad|>" # nosec self.image_token_id = processor.tokenizer.convert_tokens_to_ids( @@ -564,6 +610,7 @@ def __init__( roles_to_train: Optional[list[str]] = None, train_on_eos: Optional[str] = None, role_boundaries_override: Optional[list[dict]] = None, + field_messages: str | list[str] | tuple[str, ...] | None = None, ): super().__init__( processor, @@ -574,6 +621,7 @@ def __init__( roles_to_train=roles_to_train, train_on_eos=train_on_eos, role_boundaries_override=role_boundaries_override, + field_messages=field_messages, ) self.video_token = "<|video_pad|>" # nosec self.video_token_id = processor.tokenizer.convert_tokens_to_ids( @@ -631,6 +679,7 @@ def __init__( roles_to_train: Optional[list[str]] = None, train_on_eos: Optional[str] = None, role_boundaries_override: Optional[list[dict]] = None, + field_messages: str | list[str] | tuple[str, ...] | None = None, ): super().__init__( processor, @@ -641,6 +690,7 @@ def __init__( roles_to_train=roles_to_train, train_on_eos=train_on_eos, role_boundaries_override=role_boundaries_override, + field_messages=field_messages, ) # Real Gemma3 tokenizers expose boi_token as a direct attribute, not # via special_tokens_map (which only holds HF's standard slots). @@ -879,6 +929,7 @@ def __init__( roles_to_train: Optional[list[str]] = None, train_on_eos: Optional[str] = None, role_boundaries_override: Optional[list[dict]] = None, + field_messages: str | list[str] | tuple[str, ...] | None = None, ): super().__init__( processor, @@ -889,6 +940,7 @@ def __init__( roles_to_train=roles_to_train, train_on_eos=train_on_eos, role_boundaries_override=role_boundaries_override, + field_messages=field_messages, ) special_ids = ( processor.tokenizer.tokenizer.instruct_tokenizer.audio_encoder.special_ids @@ -929,6 +981,7 @@ def __init__( roles_to_train: Optional[list[str]] = None, train_on_eos: Optional[str] = None, role_boundaries_override: Optional[list[dict]] = None, + field_messages: str | list[str] | tuple[str, ...] | None = None, ): super().__init__( processor, @@ -939,6 +992,7 @@ def __init__( roles_to_train=roles_to_train, train_on_eos=train_on_eos, role_boundaries_override=role_boundaries_override, + field_messages=field_messages, ) self.image_token = "" # nosec @@ -964,6 +1018,7 @@ def __init__( roles_to_train: Optional[list[str]] = None, train_on_eos: Optional[str] = None, role_boundaries_override: Optional[list[dict]] = None, + field_messages: str | list[str] | tuple[str, ...] | None = None, ): super().__init__( processor, @@ -974,6 +1029,7 @@ def __init__( roles_to_train=roles_to_train, train_on_eos=train_on_eos, role_boundaries_override=role_boundaries_override, + field_messages=field_messages, ) special_ids = ( processor.tokenizer.tokenizer.instruct_tokenizer.image_encoder.special_ids @@ -1014,6 +1070,7 @@ def __init__( roles_to_train: Optional[list[str]] = None, train_on_eos: Optional[str] = None, role_boundaries_override: Optional[list[dict]] = None, + field_messages: str | list[str] | tuple[str, ...] | None = None, ): super().__init__( processor, @@ -1024,6 +1081,7 @@ def __init__( roles_to_train=roles_to_train, train_on_eos=train_on_eos, role_boundaries_override=role_boundaries_override, + field_messages=field_messages, ) if not hasattr(processor, "image_ids"): @@ -1064,6 +1122,7 @@ def __init__( roles_to_train: Optional[list[str]] = None, train_on_eos: Optional[str] = None, role_boundaries_override: Optional[list[dict]] = None, + field_messages: str | list[str] | tuple[str, ...] | None = None, ): super().__init__( processor, @@ -1074,6 +1133,7 @@ def __init__( roles_to_train=roles_to_train, train_on_eos=train_on_eos, role_boundaries_override=role_boundaries_override, + field_messages=field_messages, ) self.tokenizer = getattr(processor, "tokenizer", processor) @@ -1132,6 +1192,7 @@ def get_processing_strategy( roles_to_train: Optional[list[str]] = None, train_on_eos: Optional[str] = None, role_boundaries_override: Optional[list[dict]] = None, + field_messages: str | list[str] | tuple[str, ...] | None = None, ): processing_kwargs = { "processor": processor, @@ -1142,6 +1203,7 @@ def get_processing_strategy( "roles_to_train": roles_to_train, "train_on_eos": train_on_eos, "role_boundaries_override": role_boundaries_override, + "field_messages": field_messages, } if chat_template_type in [None, "tokenizer_default"]: diff --git a/tests/test_processing_strategies.py b/tests/test_processing_strategies.py index 2d8f13fe57..b2c4dd4d0b 100644 --- a/tests/test_processing_strategies.py +++ b/tests/test_processing_strategies.py @@ -1,6 +1,7 @@ """Tests for ``axolotl.processing_strategies`` using fake tokenizers (offline/CI-safe).""" import logging +from unittest.mock import MagicMock import pytest import torch @@ -1162,3 +1163,163 @@ def test_multimodal_config_parses_dict_role_boundaries_to_specs(): seq = [51, 7, 60, 50, 8, 60] out = strategy.process_labels(torch.tensor([seq])).tolist()[0] assert out == [-100, -100, -100, -100, 8, 60] + + +# --------------------------------------------------------------------------- # +# field_messages plumbing: helpers and __call__ re-routing +# --------------------------------------------------------------------------- # + + +def _mock_processor() -> MagicMock: + """Processor mock with no ``image_token`` attr so __init__ skips that branch.""" + processor = MagicMock() + del processor.image_token + return processor + + +def _openai_messages() -> list[dict]: + return [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hi"}, + ] + + +def _sharegpt_messages() -> list[dict]: + return [ + {"from": "human", "value": "hello"}, + {"from": "gpt", "value": "hi"}, + ] + + +def test_normalize_field_messages_none_defaults_to_messages(): + assert ProcessingStrategy._normalize_field_messages(None) == ("messages",) + + +def test_normalize_field_messages_string_wrapped_in_tuple(): + assert ProcessingStrategy._normalize_field_messages("dialogue") == ("dialogue",) + + +def test_normalize_field_messages_list_preserved(): + assert ProcessingStrategy._normalize_field_messages(["foo", "bar"]) == ( + "foo", + "bar", + ) + + +def test_normalize_field_messages_falsy_entries_dropped(): + assert ProcessingStrategy._normalize_field_messages(["foo", "", None, "bar"]) == ( + "foo", + "bar", + ) + + +def test_is_legacy_schema_sharegpt_pair_detected(): + assert ProcessingStrategy._is_legacy_schema([{"from": "human", "value": "hi"}]) + + +def test_is_legacy_schema_only_from_is_not_enough(): + # `from` without `value` (e.g., custom metadata) must not be misrouted. + assert not ProcessingStrategy._is_legacy_schema([{"from": "human", "extra": "x"}]) + + +def test_is_legacy_schema_openai_schema_returns_false(): + assert not ProcessingStrategy._is_legacy_schema([{"role": "user", "content": "hi"}]) + + +def test_is_legacy_schema_empty_list_returns_false(): + assert not ProcessingStrategy._is_legacy_schema([]) + + +def test_is_legacy_schema_non_list_returns_false(): + assert not ProcessingStrategy._is_legacy_schema(None) + assert not ProcessingStrategy._is_legacy_schema("not a list") + + +def test_get_messages_field_default_returns_messages(): + strategy = ProcessingStrategy(processor=_mock_processor()) + assert strategy._get_messages_field({"messages": _openai_messages()}) == "messages" + + +def test_get_messages_field_custom_field_returns_custom(): + strategy = ProcessingStrategy(processor=_mock_processor(), field_messages="my_col") + assert strategy._get_messages_field({"my_col": _openai_messages()}) == "my_col" + + +def test_get_messages_field_custom_takes_priority_over_stale_messages(): + # Configured custom field must beat a leftover `messages` column. + strategy = ProcessingStrategy(processor=_mock_processor(), field_messages="my_col") + example = { + "messages": [{"role": "user", "content": "stale"}], + "my_col": _openai_messages(), + } + assert strategy._get_messages_field(example) == "my_col" + + +def test_get_messages_field_falls_back_to_messages_when_custom_absent(): + strategy = ProcessingStrategy(processor=_mock_processor(), field_messages="my_col") + assert strategy._get_messages_field({"messages": _openai_messages()}) == "messages" + + +def test_get_messages_field_returns_none_when_no_match(): + strategy = ProcessingStrategy(processor=_mock_processor(), field_messages="my_col") + assert strategy._get_messages_field({"random_col": "x"}) is None + + +def test_call_canonical_messages_openai(): + strategy = ProcessingStrategy(processor=_mock_processor()) + out = strategy([{"messages": _openai_messages()}]) + + assert len(out) == 1 + assert out[0]["messages"][0]["role"] == "user" + # String content is wrapped to multimedia content list. + assert out[0]["messages"][0]["content"] == [{"type": "text", "text": "hello"}] + + +def test_call_canonical_conversations_sharegpt(): + strategy = ProcessingStrategy(processor=_mock_processor()) + out = strategy([{"conversations": _sharegpt_messages()}]) + + # Roles get normalized: human -> user, gpt -> assistant. + assert out[0]["messages"][0]["role"] == "user" + assert out[0]["messages"][1]["role"] == "assistant" + assert "conversations" not in out[0] + + +def test_call_custom_field_with_openai_schema(): + strategy = ProcessingStrategy(processor=_mock_processor(), field_messages="my_col") + out = strategy([{"my_col": _openai_messages()}]) + + assert out[0]["messages"][0]["role"] == "user" + assert "my_col" not in out[0] + + +def test_call_custom_field_with_sharegpt_schema(): + # A custom column whose first message looks like ShareGPT is routed to the legacy branch. + strategy = ProcessingStrategy(processor=_mock_processor(), field_messages="my_col") + out = strategy([{"my_col": _sharegpt_messages()}]) + + assert out[0]["messages"][0]["role"] == "user" + assert out[0]["messages"][1]["role"] == "assistant" + assert "my_col" not in out[0] + assert "conversations" not in out[0] + + +def test_call_custom_field_overrides_stale_messages_column(): + strategy = ProcessingStrategy(processor=_mock_processor(), field_messages="my_col") + out = strategy( + [ + { + "messages": [{"role": "user", "content": "stale"}], + "my_col": [{"role": "user", "content": "fresh"}], + } + ] + ) + + assert out[0]["messages"][0]["content"] == [{"type": "text", "text": "fresh"}] + + +def test_call_unknown_key_raises_value_error(): + strategy = ProcessingStrategy(processor=_mock_processor(), field_messages="my_col") + + with pytest.raises(ValueError): + strategy([{"random_col": _openai_messages()}]) From d7cb1c9d03dace6e3db34ed5b8ee908eae671a7a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 13 May 2026 03:02:47 -0400 Subject: [PATCH 1307/1405] feat: add pytorch 2.11 to base images (#3621) Co-authored-by: NanoCode012 --- .github/workflows/base.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index d6898c6a6a..90dd2b3db0 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -208,6 +208,22 @@ jobs: torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" + - cuda: "128" + cuda_version: 12.8.1 + cudnn_version: "" + python_version: "3.12" + pytorch: 2.11.0 + torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + dockerfile: "Dockerfile-uv-base" + platforms: "linux/amd64,linux/arm64" + - cuda: "130" + cuda_version: 13.0.0 + cudnn_version: "" + python_version: "3.12" + pytorch: 2.11.0 + torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" + dockerfile: "Dockerfile-uv-base" + platforms: "linux/amd64,linux/arm64" steps: - name: Checkout uses: actions/checkout@v4 From afd74aed71da70898ef7ea1a16b7c59dc765cede Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Wed, 20 May 2026 13:25:25 +0700 Subject: [PATCH 1308/1405] Fix: ci being broken (fa2, ray) (#3664) * fix: symlink to CLAUDE.md for claude code * fix: ray worker propogating mbs & ga wrong * fix: do not install fa/ring fa on ci cd too like in build * chore: remove s2 attention (broken on transformers 5.x) and flash_attn_rms_norm * chore: add to deprecated * skip flaked test --------- Co-authored-by: Wing Lian --- AGENTS.md | 8 + CLAUDE.md | 1 + cicd/Dockerfile-uv.jinja | 4 +- docs/attention.qmd | 16 -- examples/llama-2/fft_optimized.yml | 1 - examples/llama-2/lisa.yml | 1 - src/axolotl/loaders/model.py | 4 +- src/axolotl/loaders/patch_manager.py | 44 +--- .../monkeypatch/llama_attn_hijack_flash.py | 199 +----------------- src/axolotl/utils/config/__init__.py | 14 +- src/axolotl/utils/schemas/config.py | 15 +- src/axolotl/utils/schemas/deprecated.py | 18 ++ src/axolotl/utils/schemas/enums.py | 3 - src/axolotl/utils/schemas/validation.py | 14 +- tests/e2e/patched/test_llama_s2_attention.py | 111 ---------- tests/e2e/test_reward_model_smollm2.py | 3 + tests/patched/test_validation.py | 14 -- tests/test_attn_implementation.py | 24 +-- tests/test_no_legacy_attn_reads.py | 1 - 19 files changed, 57 insertions(+), 438 deletions(-) create mode 120000 CLAUDE.md delete mode 100644 tests/e2e/patched/test_llama_s2_attention.py diff --git a/AGENTS.md b/AGENTS.md index 43470d9f80..4264d1adf5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,6 +85,14 @@ docs/ # Quarto documentation site - Trainer mixins: `core/trainers/mixins/` for composable trainer behaviors - Schemas: all config validation via Pydantic in `utils/schemas/` +## Comment Style + +- Default to no comment. Only add one when the WHY is non-obvious (hidden constraint, subtle invariant, workaround for a specific bug). +- Don't explain WHAT the code does — names and types already do that. +- Don't reference the current task, PR, or callers (e.g. "added for X", "used by Y", "fixes #123"). Those belong in commit messages / PR descriptions and rot fast. +- Prefer one short line max. +- Don't add planning/decision/analysis markdown files unless explicitly requested. + ## Key Documentation - [Getting Started](docs/getting-started.qmd) — quickstart tutorial diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/cicd/Dockerfile-uv.jinja b/cicd/Dockerfile-uv.jinja index c24512ed3b..acf84ad1d9 100644 --- a/cicd/Dockerfile-uv.jinja +++ b/cicd/Dockerfile-uv.jinja @@ -26,9 +26,9 @@ RUN uv pip install packaging==26.0 setuptools==78.1.1 RUN uv pip install torchvision RUN uv pip uninstall causal_conv1d RUN if [ "$AXOLOTL_EXTRAS" != "" ] ; then \ - uv pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ + uv pip install --no-build-isolation -e .[deepspeed,optimizers,ray,$AXOLOTL_EXTRAS] $AXOLOTL_ARGS; \ else \ - uv pip install --no-build-isolation -e .[deepspeed,flash-attn,ring-flash-attn,optimizers,ray] $AXOLOTL_ARGS; \ + uv pip install --no-build-isolation -e .[deepspeed,optimizers,ray] $AXOLOTL_ARGS; \ fi # Override with nightly HF packages for nightly builds diff --git a/docs/attention.qmd b/docs/attention.qmd index f7fa5b4561..64820fb6ef 100644 --- a/docs/attention.qmd +++ b/docs/attention.qmd @@ -177,21 +177,6 @@ Recommended for Turing GPUs or below (e.g. Colab T4). ::: -### Shifted Sparse Attention - -::: {.callout-warning} - -Planned for deprecation. Prefer one of the backends above. - -::: - -Requirements: LLaMA model architecture. Loaded as FA2 under the hood and -patched to implement shifted-sparse attention. Does not support sample packing. - -```yaml -attn_implementation: s2 -``` - ### FP8 torchao low-precision attention. Loaded as SDPA and patched post-load. @@ -227,7 +212,6 @@ the validated config. | `xformers_attention: true` | `attn_implementation: xformers` | | `flex_attention: true` | `attn_implementation: flex_attention` | | `sage_attention: true` | `attn_implementation: sage` | -| `s2_attention: true` | `attn_implementation: s2` | | `eager_attention: true` | `attn_implementation: eager` | Combining `attn_implementation` with a legacy flag (e.g. `attn_implementation: diff --git a/examples/llama-2/fft_optimized.yml b/examples/llama-2/fft_optimized.yml index 7af25dd179..eb46007b98 100644 --- a/examples/llama-2/fft_optimized.yml +++ b/examples/llama-2/fft_optimized.yml @@ -44,7 +44,6 @@ resume_from_checkpoint: logging_steps: 1 attn_implementation: flash_attention_2 flash_attn_cross_entropy: false -flash_attn_rms_norm: true flash_attn_fuse_mlp: true warmup_ratio: 0.1 diff --git a/examples/llama-2/lisa.yml b/examples/llama-2/lisa.yml index 40ba6d0d0e..b125f83ca2 100644 --- a/examples/llama-2/lisa.yml +++ b/examples/llama-2/lisa.yml @@ -48,7 +48,6 @@ resume_from_checkpoint: logging_steps: 1 attn_implementation: flash_attention_2 flash_attn_cross_entropy: false -flash_attn_rms_norm: true flash_attn_fuse_mlp: true warmup_ratio: 0.1 diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 84a021a9fe..700039c5e5 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -632,8 +632,8 @@ def _set_quantization_config(self): ) def _set_attention_config(self): - # s2 patches FA2 internals (load as FA2); fp8 replaces sdpa post-load (load as sdpa). - _LOAD_TIME_OVERRIDE = {"s2": "flash_attention_2", "fp8": "sdpa"} + # fp8 replaces sdpa post-load (load as sdpa). + _LOAD_TIME_OVERRIDE = {"fp8": "sdpa"} if self.cfg.attn_implementation: hf_impl = _LOAD_TIME_OVERRIDE.get( self.cfg.attn_implementation, self.cfg.attn_implementation diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 4fd2ba035d..e0a224d079 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -722,29 +722,6 @@ def _patch_loss_llama(self): ) patch_fa_llama_cross_entropy() - if self.cfg.flash_attn_rms_norm and self.has_flash_attn: - from axolotl.monkeypatch.llama_attn_hijack_flash import patch_llama_rms_norm - - patch_llama_rms_norm() - - def _patch_llama_flash_attention(self): - """Apply Flash Attention patches for LLaMA models.""" - from axolotl.monkeypatch.llama_attn_hijack_flash import ( - replace_llama_attn_with_flash_attn, - ) - - if self.cfg.attn_implementation == "s2": - LOG.info("patching w/ flash-enabled, shifted-sparse attention") - replace_llama_attn_with_flash_attn( - cross_entropy=self.cfg.flash_attn_cross_entropy, - rms_norm=self.cfg.flash_attn_rms_norm, - use_shifted_sparse_attn=True, - ) - elif self.cfg.flash_attn_cross_entropy or self.cfg.flash_attn_rms_norm: - replace_llama_attn_with_flash_attn( - cross_entropy=self.cfg.flash_attn_cross_entropy, - rms_norm=self.cfg.flash_attn_rms_norm, - ) def _patch_llama_xformers_attention(self): """Apply xformers attention patches for LLaMA models.""" @@ -757,19 +734,16 @@ def _patch_llama_xformers_attention(self): def _patch_llama_derived_model(self): """Modify all llama derived models in one block.""" - if self.cfg.is_llama_derived_model and not ( - self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES - and self.cfg.attn_supports_packing - and self.cfg.sample_packing + if ( + self.cfg.is_llama_derived_model + and self.cfg.attn_implementation == "xformers" + and not ( + self.cfg.model_config_type in SUPPORTED_MULTIPACK_MODEL_TYPES + and self.cfg.attn_supports_packing + and self.cfg.sample_packing + ) ): - if self.cfg.attn_uses_flash_lib: - self._patch_llama_flash_attention() - elif self.cfg.attn_implementation == "xformers": - self._patch_llama_xformers_attention() - elif self.cfg.attn_implementation == "s2": - raise NotImplementedError( - "Shifted-sparse attention not currently implemented without flash attention." - ) + self._patch_llama_xformers_attention() def _apply_llama_flash_attn_patches(self, model): """Apply LLaMA-specific flash attention patches.""" diff --git a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py index 3953cb138f..60ef7e3b40 100644 --- a/src/axolotl/monkeypatch/llama_attn_hijack_flash.py +++ b/src/axolotl/monkeypatch/llama_attn_hijack_flash.py @@ -1,34 +1,13 @@ """Flash attention monkey patch for llama model""" -# copied from https://github.com/lm-sys/FastChat/blob/main/fastchat/train/llama_flash_attn_monkey_patch.py - import importlib.util -import warnings -from typing import Optional, Tuple -import torch import transformers -from einops import rearrange -from flash_attn.bert_padding import pad_input, unpad_input -from transformers.models.llama.modeling_llama import ( - LlamaMLP, - apply_rotary_pos_emb, - repeat_kv, -) +from transformers.models.llama.modeling_llama import LlamaMLP from axolotl.monkeypatch.utils import set_module_name from axolotl.utils.logging import get_logger -try: - from flash_attn.flash_attn_interface import ( - flash_attn_varlen_qkvpacked_func, - ) -except ImportError: - from flash_attn.flash_attn_interface import ( - flash_attn_unpadded_qkvpacked_func as flash_attn_varlen_qkvpacked_func, - ) - - LOG = get_logger(__name__) @@ -91,179 +70,3 @@ def fa2_fixed_cross_entropy( return loss transformers.loss.loss_utils.fixed_cross_entropy = fa2_fixed_cross_entropy - - -def patch_llama_rms_norm(): - try: - from flash_attn.ops.rms_norm import RMSNorm - - class LlamaRMSNorm(RMSNorm): - """Patched LLamaRMSNorm""" - - def __init__(self, hidden_size, eps=1e-6): - super().__init__(hidden_size, eps=eps) - - LOG.info("patching with flash_attn.ops.rms_norm") - transformers.models.llama.modeling_llama.LlamaRMSNorm = LlamaRMSNorm - except ImportError: - LOG.warning( - "optimized flash-attention RMSNorm not found (run `pip install 'git+https://github.com/Dao-AILab/flash-attention.git#egg=dropout_layer_norm&subdirectory=csrc/layer_norm'`)" - ) - - -def replace_llama_attn_with_flash_attn( - cross_entropy: Optional[bool] = False, - rms_norm: Optional[bool] = False, - use_shifted_sparse_attn: Optional[bool] = False, -): - transformers.models.llama.modeling_llama.LlamaModel._prepare_decoder_attention_mask = _prepare_decoder_attention_mask - if use_shifted_sparse_attn: - transformers.models.llama.modeling_llama.LlamaAttention.forward = ( - flashattn_forward_with_s2attn - ) - - # skip only if explicitly disabled - if cross_entropy: - patch_fa_llama_cross_entropy() - - # skip only if explicitly disabled - if rms_norm: - patch_llama_rms_norm() - - -# Disable the transformation of the attention mask in LlamaModel as the flash attention -# requires the attention mask to be the same as the key_padding_mask -def _prepare_decoder_attention_mask( - self, - attention_mask, - input_shape, - inputs_embeds, - past_key_values_length, -): - # [bsz, seq_len] - return attention_mask - - -GROUP_SIZE_RATIO = 1 / 4 - - -def flashattn_forward_with_s2attn( - self, - hidden_states: torch.Tensor, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.Tensor] = None, - past_key_value: Optional[Tuple[torch.Tensor]] = None, - output_attentions: bool = False, - use_cache: bool = False, - padding_mask: Optional[torch.LongTensor] = None, - cu_seqlens: Optional[torch.Tensor] = None, - max_seqlen: Optional[torch.Tensor] = None, -) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: - """Input shape: Batch x Time x Channel - - From: https://github.com/dvlab-research/LongLoRA/blob/main/llama_attn_replace.py - - attention_mask: [bsz, q_len] - - `cu_seqlens` will be ignored if provided - `max_seqlen` will be ignored if provided - """ - if output_attentions: - warnings.warn( - "Output attentions is not supported for patched `LlamaAttention`, returning `None` instead.", - stacklevel=2, - ) - - bsz, q_len, _ = hidden_states.size() - - query_states = ( - self.q_proj(hidden_states) - .view(bsz, q_len, self.num_heads, self.head_dim) - .transpose(1, 2) - ) - key_states = ( - self.k_proj(hidden_states) - .view(bsz, q_len, self.num_key_value_heads, self.head_dim) - .transpose(1, 2) - ) - value_states = ( - self.v_proj(hidden_states) - .view(bsz, q_len, self.num_key_value_heads, self.head_dim) - .transpose(1, 2) - ) - # [bsz, q_len, nh, hd] - # [bsz, nh, q_len, hd] - - cos, sin = self.rotary_emb(value_states, position_ids=position_ids) - query_states, key_states = apply_rotary_pos_emb( - query_states, key_states, cos, sin, position_ids - ) - - # Past Key value support - if past_key_value is not None: - # reuse k, v, self_attention - key_states = torch.cat([past_key_value[0], key_states], dim=2) - value_states = torch.cat([past_key_value[1], value_states], dim=2) - - past_key_value = (key_states, value_states) if use_cache else None - - # repeat k/v heads if n_kv_heads < n_heads - key_states = repeat_kv(key_states, self.num_key_value_groups) - value_states = repeat_kv(value_states, self.num_key_value_groups) - - # Flash attention codes from - # https://github.com/HazyResearch/flash-attention/blob/main/flash_attn/flash_attention.py - - # transform the data into the format required by flash attention - qkv = torch.stack( - [query_states, key_states, value_states], dim=2 - ) # [bsz, nh, 3, q_len, hd] - qkv = qkv.transpose(1, 3) # [bsz, q_len, 3, nh, hd] - - # We have disabled _prepare_decoder_attention_mask in LlamaModel - # the attention_mask should be the same as the key_padding_mask - - key_padding_mask = attention_mask.repeat(2, 1) - nheads = qkv.shape[-2] - # shift - - group_size = int(q_len * GROUP_SIZE_RATIO) - if q_len % group_size > 0: - raise ValueError( - f"q_len {q_len} should be divisible by group size {group_size}." - ) - - qkv = ( - qkv.reshape(bsz, q_len, 3, 2, self.num_heads // 2, self.head_dim) - .permute(0, 3, 1, 2, 4, 5) - .reshape(bsz * 2, q_len, 3, self.num_heads // 2, self.head_dim) - ) - x = rearrange(qkv, "b s three h d -> b s (three h d)") - x_unpad, indices, cu_q_lens, max_s = unpad_input(x, key_padding_mask) - cu_q_len_tmp = torch.arange( - 0, max_s, group_size, device=key_padding_mask.device, dtype=cu_q_lens.dtype - ) - cu_q_len_tmp = torch.stack([cu_q_len_tmp, cu_q_len_tmp + group_size // 2]).repeat( - bsz, 1 - ) + cu_q_lens[:-1].unsqueeze(-1) - cu_q_lens = torch.cat([cu_q_len_tmp, cu_q_lens[1:].unsqueeze(-1)], dim=-1).view(-1) - - x_unpad = rearrange( - x_unpad, "nnz (three h d) -> nnz three h d", three=3, h=nheads // 2 - ) - output_unpad = flash_attn_varlen_qkvpacked_func( - x_unpad, cu_q_lens, group_size, 0.0, softmax_scale=None, causal=True - ) - output = rearrange( - pad_input( - rearrange(output_unpad, "nnz h d -> nnz (h d)"), indices, bsz * 2, q_len - ), - "b s (h d) -> b s h d", - h=nheads // 2, - ) - output = ( - output.reshape(bsz, 2, q_len, nheads // 2, self.head_dim) - .transpose(1, 2) - .reshape(bsz, q_len, nheads, self.head_dim) - ) - return self.o_proj(rearrange(output, "b s h d -> b s (h d)")), None, past_key_value diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 8e6d3e7e74..a410ceb6f6 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -111,12 +111,14 @@ def resolve_dtype(cfg): def normalize_config(cfg): # setup some derived config / hyperparams - cfg.gradient_accumulation_steps = cfg.gradient_accumulation_steps or ( - cfg.batch_size // cfg.micro_batch_size - ) - cfg.batch_size = ( - cfg.batch_size or cfg.micro_batch_size * cfg.gradient_accumulation_steps - ) + if not cfg.use_ray: + # Defer derivation to the Ray worker; its re-validate forbids both being set. + cfg.gradient_accumulation_steps = cfg.gradient_accumulation_steps or ( + cfg.batch_size // cfg.micro_batch_size + ) + cfg.batch_size = ( + cfg.batch_size or cfg.micro_batch_size * cfg.gradient_accumulation_steps + ) if cfg.eval_batch_size is None: cfg.eval_batch_size = cfg.micro_batch_size cfg.world_size = int(os.environ.get("WORLD_SIZE", 1)) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 73e9f88230..a227e04c75 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -772,13 +772,6 @@ class AxolotlInputConfig( "description": "[DEPRECATED] Use `attn_implementation: sdpa`." }, ) - s2_attention: bool | None = Field( - default=None, - deprecated="Use `attn_implementation: s2` instead.", - json_schema_extra={ - "description": "[DEPRECATED] Use `attn_implementation: s2`. Shifted-sparse attention (only llama) - https://arxiv.org/pdf/2309.12307.pdf" - }, - ) flex_attention: bool | None = Field( default=None, deprecated="Use `attn_implementation: flex_attention` instead.", @@ -797,12 +790,6 @@ class AxolotlInputConfig( "description": "Whether to use flash-attention cross entropy implementation - advanced use only" }, ) - flash_attn_rms_norm: bool | None = Field( - default=None, - json_schema_extra={ - "description": "Whether to use flash-attention rms norm implementation - advanced use only" - }, - ) flash_attn_fuse_mlp: bool | None = Field( default=None, json_schema_extra={ @@ -831,7 +818,7 @@ class AxolotlInputConfig( json_schema_extra={ "description": ( "Attention backend. Canonical values: eager, sdpa, flash_attention_2, " - "flash_attention_3, flex_attention, xformers, sage, s2, fp8. Hub-kernel " + "flash_attention_3, flex_attention, xformers, sage, fp8. Hub-kernel " "paths (e.g. kernels-community/flash-attn3) are also accepted and passed " "through to transformers." ) diff --git a/src/axolotl/utils/schemas/deprecated.py b/src/axolotl/utils/schemas/deprecated.py index d87eb9d964..a441157f43 100644 --- a/src/axolotl/utils/schemas/deprecated.py +++ b/src/axolotl/utils/schemas/deprecated.py @@ -23,6 +23,8 @@ class DeprecatedParameters(BaseModel): dpo_generate_during_eval: bool | None = None dpo_norm_loss: bool | None = None rpo_alpha: float | None = None + s2_attention: bool | None = None + flash_attn_rms_norm: bool | None = None @field_validator("max_packed_sequence_len") @classmethod @@ -122,6 +124,22 @@ def validate_rpo_alpha(cls, rpo_alpha): ) # TODO: change this warning once multiple dpo loss types are supported. return rpo_alpha + @field_validator("s2_attention") + @classmethod + def validate_s2_attention(cls, s2_attention): + if s2_attention: + raise DeprecationWarning( + "`s2_attention` (shifted-sparse attention) is no longer supported." + ) + return s2_attention + + @field_validator("flash_attn_rms_norm") + @classmethod + def validate_flash_attn_rms_norm(cls, flash_attn_rms_norm): + if flash_attn_rms_norm: + raise DeprecationWarning("`flash_attn_rms_norm` is no longer supported.") + return flash_attn_rms_norm + class RemappedParameters(BaseModel): """Parameters that have been remapped to other names""" diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 8015486450..4743604753 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -107,7 +107,6 @@ class CustomSupportedOptimizers(str, Enum): "flex_attention", "xformers", "sage", - "s2", "fp8", } ) @@ -115,7 +114,6 @@ class CustomSupportedOptimizers(str, Enum): # Legacy boolean flags → canonical attn_implementation. Priority: specific before generic. LEGACY_ATTN_FLAG_TO_IMPL = { "xformers_attention": "xformers", - "s2_attention": "s2", "sage_attention": "sage", "flex_attention": "flex_attention", "flash_attention": "flash_attention_2", @@ -149,7 +147,6 @@ class CustomSupportedOptimizers(str, Enum): { "flash_attention_2", "flash_attention_3", - "s2", "kernels-community/flash-attn2", "kernels-community/flash-attn3", } diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index a19320b9c0..f6f3e779f9 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -202,15 +202,6 @@ def check_sample_packing_without_attention(self): ) return self - @model_validator(mode="after") - def check_sample_packing_with_s2attn(self): - if self.sample_packing and self.attn_implementation == "s2": - raise ValueError( - "Received `sample_packing=true` and `attn_implementation=s2`; " - "shifted-sparse attention does not currently support sample packing." - ) - return self - @model_validator(mode="after") def check_scaling_softmax_requires_flex(self): if self.scaling_softmax and self.attn_implementation != "flex_attention": @@ -1209,7 +1200,6 @@ def check_npu_config(cls, data): "flash_attention_2", "flash_attention_3", "sdpa", - "s2", } attn_impl = data.get("attn_implementation") if attn_impl and attn_impl in unsupported_npu_impls: @@ -1217,7 +1207,7 @@ def check_npu_config(cls, data): f"attn_implementation={attn_impl!r} is currently not supported on Ascend NPU." ) # Legacy flags still present at this point (normalizer strips them later). - attn_list = ["flash_attention", "sdp_attention", "s2_attention"] + attn_list = ["flash_attention", "sdp_attention"] for attn in attn_list: if data.get(attn): raise NotImplementedError( @@ -1539,7 +1529,7 @@ def check_context_parallel_size(self): if not self.attn_uses_flash_lib: raise ValueError( "context_parallel_size > 1 requires flash attention " - "(attn_implementation: flash or s2)." + "(attn_implementation: flash_attention_2 or flash_attention_3)." ) if self.sample_packing and self.micro_batch_size > 1: diff --git a/tests/e2e/patched/test_llama_s2_attention.py b/tests/e2e/patched/test_llama_s2_attention.py deleted file mode 100644 index 0dd748945a..0000000000 --- a/tests/e2e/patched/test_llama_s2_attention.py +++ /dev/null @@ -1,111 +0,0 @@ -""" -E2E tests for llama w/ S2 attn -""" - -import unittest - -import pytest - -from axolotl.common.datasets import load_datasets -from axolotl.train import train -from axolotl.utils.config import normalize_config, validate_config -from axolotl.utils.dict import DictDefault - -from ..utils import check_model_output_exists, with_temp_dir - - -@pytest.mark.skip(reason="FIXME?") -class TestLlamaShiftedSparseAttention(unittest.TestCase): - """ - Test case for Llama models using S2 Attn - """ - - @with_temp_dir - def test_lora_s2_attn(self, temp_dir): - cfg = DictDefault( - { - "base_model": "HuggingFaceTB/SmolLM2-135M", - "tokenizer_type": "AutoTokenizer", - "sequence_len": 16384, - "sample_packing": False, - "flash_attention": True, - "s2_attention": True, - "load_in_8bit": True, - "adapter": "lora", - "lora_r": 32, - "lora_alpha": 16, - "lora_dropout": 0.05, - "lora_target_linear": True, - "val_set_size": 0.02, - "special_tokens": { - "pad_token": "<|endoftext|>", - }, - "datasets": [ - { - "path": "Yukang/LongAlpaca-12k", - "type": "alpaca", - }, - ], - "num_epochs": 2, - "micro_batch_size": 1, - "gradient_accumulation_steps": 1, - "output_dir": temp_dir, - "learning_rate": 0.00001, - "optimizer": "adamw_torch_fused", - "lr_scheduler": "cosine", - "max_steps": 10, - "save_steps": 5, - "eval_steps": 5, - "bf16": "auto", - "save_first_step": False, - } - ) - - cfg = validate_config(cfg) - normalize_config(cfg) - dataset_meta = load_datasets(cfg=cfg) - - train(cfg=cfg, dataset_meta=dataset_meta) - check_model_output_exists(temp_dir, cfg) - - @with_temp_dir - def test_fft_s2_attn(self, temp_dir): - cfg = DictDefault( - { - "base_model": "HuggingFaceTB/SmolLM2-135M", - "tokenizer_type": "AutoTokenizer", - "sequence_len": 16384, - "sample_packing": False, - "flash_attention": True, - "s2_attention": True, - "val_set_size": 0.02, - "special_tokens": { - "pad_token": "<|endoftext|>", - }, - "datasets": [ - { - "path": "Yukang/LongAlpaca-12k", - "type": "alpaca", - }, - ], - "num_epochs": 2, - "micro_batch_size": 1, - "gradient_accumulation_steps": 1, - "output_dir": temp_dir, - "learning_rate": 0.00001, - "optimizer": "adamw_torch_fused", - "lr_scheduler": "cosine", - "max_steps": 10, - "save_steps": 5, - "eval_steps": 5, - "bf16": "auto", - "save_first_step": False, - } - ) - - cfg = validate_config(cfg) - normalize_config(cfg) - dataset_meta = load_datasets(cfg=cfg) - - train(cfg=cfg, dataset_meta=dataset_meta) - check_model_output_exists(temp_dir, cfg) diff --git a/tests/e2e/test_reward_model_smollm2.py b/tests/e2e/test_reward_model_smollm2.py index cc768b173e..657756f5b6 100644 --- a/tests/e2e/test_reward_model_smollm2.py +++ b/tests/e2e/test_reward_model_smollm2.py @@ -4,6 +4,8 @@ import unittest +import pytest + from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -17,6 +19,7 @@ class TestRewardModelLoraSmolLM2(unittest.TestCase): Test case for Llama reward models using LoRA """ + @pytest.mark.skip(reason="FIXME, mostly underused functionality") @with_temp_dir def test_rm_lora(self, temp_dir): cfg = DictDefault( diff --git a/tests/patched/test_validation.py b/tests/patched/test_validation.py index 0b98ce3521..23773448fd 100644 --- a/tests/patched/test_validation.py +++ b/tests/patched/test_validation.py @@ -1245,20 +1245,6 @@ def test_torch_version_adopt_req(self, minimal_cfg): cfg, capabilities=capabilities, env_capabilities=env_capabilities ) - def test_cfg_throws_error_with_s2_attention_and_sample_packing(self, minimal_cfg): - test_cfg = DictDefault( - { - "s2_attention": True, - "sample_packing": True, - } - | minimal_cfg - ) - with pytest.raises( - ValidationError, - match=r".*shifted-sparse attention does not currently support sample packing*", - ): - validate_config(test_cfg) - class TestTorchCompileValidation(BaseValidation): """ diff --git a/tests/test_attn_implementation.py b/tests/test_attn_implementation.py index e85d713a81..65d8a97025 100644 --- a/tests/test_attn_implementation.py +++ b/tests/test_attn_implementation.py @@ -62,11 +62,11 @@ class TestCapabilityTables: def test_supports_packing(self, impl): assert impl in ATTN_IMPLS_SUPPORTING_PACKING - @pytest.mark.parametrize("impl", ["eager", "sdpa", "s2", "fp8"]) + @pytest.mark.parametrize("impl", ["eager", "sdpa", "fp8"]) def test_does_not_support_packing(self, impl): assert impl not in ATTN_IMPLS_SUPPORTING_PACKING - @pytest.mark.parametrize("impl", ["flash_attention_2", "flash_attention_3", "s2"]) + @pytest.mark.parametrize("impl", ["flash_attention_2", "flash_attention_3"]) def test_uses_flash_lib(self, impl): assert impl in ATTN_IMPLS_USING_FLASH_LIB @@ -88,7 +88,6 @@ def test_no_dtype_cast(self, impl): "flex_attention", "xformers", "sage", - "s2", "fp8", ], ) @@ -195,7 +194,6 @@ def _normalize(data): ("flex_attention", "flex_attention"), ("sage_attention", "sage"), ("eager_attention", "eager"), - ("s2_attention", "s2"), ], ) def test_legacy_flag_maps_to_canonical(self, flag, expected): @@ -211,14 +209,9 @@ def test_legacy_flags_are_stripped_after_mapping(self): "flex_attention", "sage_attention", "eager_attention", - "s2_attention", ]: assert flag not in result - def test_s2_plus_flash_priority_is_s2(self): - result = self._normalize({"s2_attention": True, "flash_attention": True}) - assert result["attn_implementation"] == "s2" - def test_sage_plus_flash_priority_is_sage(self): result = self._normalize({"sage_attention": True, "flash_attention": True}) assert result["attn_implementation"] == "sage" @@ -250,12 +243,6 @@ def test_canonical_plus_legacy_rejected_on_full_validation(self, min_base_cfg): with pytest.raises(ValueError, match="cannot be combined with legacy"): validate_config(cfg) - def test_s2_plus_flash_maps_to_s2_on_full_validation(self, min_base_cfg): - """Priority resolution applies through the full validator chain too.""" - cfg = min_base_cfg | DictDefault(s2_attention=True, flash_attention=True) - validated = validate_config(cfg) - assert validated.attn_implementation == "s2" - class TestCanonicalValueAcceptance: """`attn_implementation` accepts canonical names and `org/name` hub-kernel @@ -392,13 +379,6 @@ def test_flash_attention_2_does_not_warn(self, min_base_cfg, caplog): for r in caplog.records ) - def test_s2_raises(self, min_base_cfg): - cfg = min_base_cfg | DictDefault(attn_implementation="s2", sample_packing=True) - with pytest.raises( - ValueError, match="shifted-sparse attention does not currently support" - ): - validate_config(cfg) - class TestScalingSoftmaxValidation: """`scaling_softmax` is only implemented under flex_attention.""" diff --git a/tests/test_no_legacy_attn_reads.py b/tests/test_no_legacy_attn_reads.py index 2435f9fa8c..d1e3239790 100644 --- a/tests/test_no_legacy_attn_reads.py +++ b/tests/test_no_legacy_attn_reads.py @@ -16,7 +16,6 @@ "xformers_attention", "flex_attention", "sage_attention", - "s2_attention", "eager_attention", ) From dc8f7c7184ff79b95235b9ef16efc9675738faf5 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 20 May 2026 18:31:46 -0400 Subject: [PATCH 1309/1405] fix: ray batch_size derivation, fsdp schema migration, FakeExperts peft 0.19 compat (#3671) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: ray batch_size derivation, fsdp schema migration, FakeExperts peft 0.19 compat Three independent CI regressions, all reproducible on main: 1. normalize_config: the `if not cfg.use_ray:` guard introduced in afd74ae wrapped BOTH derivations (`gradient_accumulation_steps` and `batch_size`). Only `gradient_accumulation_steps` is what the Ray worker re-validate rejects when both are set; `batch_size` must still derive on the controller because `calculate_total_num_steps` divides by it. Without this fix: `TypeError: unsupported operand type(s) for /: 'float' and 'NoneType'` at trainer.py:519. 2. prepare_optim_env mutated `cfg.fsdp = True` when migrating from fsdp_config-style configs. The schema now types `fsdp: list[str] | None`, so the bool fails Ray worker re-validation with `list_type` ValidationError. Every downstream caller already handles `cfg.fsdp_config or cfg.fsdp`, so the mutation is gratuitous — drop it. The TODO to remove the cfg.fsdp check entirely in 0.12 stays. 3. peft 0.19's `_maybe_shard_state_dict_for_tp` reads `base_layer.weight.device` unconditionally. The FakeExperts test mock uses `target_parameters` style (gate_up_proj / down_proj), so it legitimately has no `.weight`. Stub a zero-size buffer. Failing job: test-axolotl-multigpu (130, 13.0.0, 3.11, 2.9.1, 2) Signed-off-by: Wing Lian * less verbosity comment --------- Signed-off-by: Wing Lian --- src/axolotl/utils/config/__init__.py | 8 ++++---- src/axolotl/utils/trainer.py | 2 +- tests/utils/schemas/validation/test_moe_quant.py | 4 ++++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index a410ceb6f6..2862f8d79b 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -112,13 +112,13 @@ def resolve_dtype(cfg): def normalize_config(cfg): # setup some derived config / hyperparams if not cfg.use_ray: - # Defer derivation to the Ray worker; its re-validate forbids both being set. + # Ray worker's re-validate forbids both being set, so defer this one. cfg.gradient_accumulation_steps = cfg.gradient_accumulation_steps or ( cfg.batch_size // cfg.micro_batch_size ) - cfg.batch_size = ( - cfg.batch_size or cfg.micro_batch_size * cfg.gradient_accumulation_steps - ) + cfg.batch_size = ( + cfg.batch_size or cfg.micro_batch_size * cfg.gradient_accumulation_steps + ) if cfg.eval_batch_size is None: cfg.eval_batch_size = cfg.micro_batch_size cfg.world_size = int(os.environ.get("WORLD_SIZE", 1)) diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 3fb940364d..45f2b7afea 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -647,7 +647,7 @@ def prepare_optim_env(cfg): os.environ["NCCL_P2P_DISABLE"] = "1" # TODO @SalmanMohammadi remove the cfg.fsdp check in 0.12 if cfg.fsdp or cfg.fsdp_config: - cfg.fsdp = True if not cfg.fsdp else cfg.fsdp + # fsdp_config is source of truth; mutating cfg.fsdp to bool breaks Ray worker schema validation setup_fsdp_envs(cfg) elif cfg.deepspeed: stage = None diff --git a/tests/utils/schemas/validation/test_moe_quant.py b/tests/utils/schemas/validation/test_moe_quant.py index 52b6f52c5f..a9300a3c66 100644 --- a/tests/utils/schemas/validation/test_moe_quant.py +++ b/tests/utils/schemas/validation/test_moe_quant.py @@ -181,6 +181,10 @@ def __init__(self): # Model definition order: gate_up_proj first, then down_proj. self.gate_up_proj = nn.Parameter(torch.randn(4, 16, 8)) self.down_proj = nn.Parameter(torch.randn(4, 8, 16)) + # peft >= 0.19 reads base_layer.weight.device unconditionally + # in _maybe_shard_state_dict_for_tp; target_parameters-style + # modules legitimately don't have .weight, so stub a buffer. + self.register_buffer("weight", torch.empty(0), persistent=False) def forward(self, x): x = torch.matmul(x, self.gate_up_proj[0].T) # (batch, 16) From cc25d3e591ab937593f1b5d16f516fd58e5a25c6 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 21 May 2026 20:47:08 +0700 Subject: [PATCH 1310/1405] feat: add EP (#3632) * feat: add EP * feat: compose EP+FSDP, fix grad scale in EP only, add sonicmoe * chore: cleanup * chore: cleanup * fix: ci * chore: clean config * fix: wrong export, not pep namespace * feat: update instructions for single node hopper * chore: doc simplify * fix: reduce lr * fix: add note of sonicmoe issue * fix: block sonicmoe for now * fix: add plugin compat change * fix: ray train being set in wrong spot * chore: continued ray fix --- docs/nd_parallelism.qmd | 40 +- .../qwen3_30ba3b_ep_fft_4gpu.yaml | 37 ++ .../qwen3_30ba3b_ep_fsdp_fft_4gpu.yaml | 49 ++ .../qwen3_30ba3b_ep_lora_4gpu.yaml | 59 +++ src/axolotl/cli/train.py | 9 + .../integrations/expert_parallel/README.md | 204 ++++++++ .../integrations/expert_parallel/__init__.py | 26 ++ .../integrations/expert_parallel/args.py | 48 ++ .../integrations/expert_parallel/buffer.py | 62 +++ .../expert_parallel/experts_fn.py | 278 +++++++++++ .../integrations/expert_parallel/plugin.py | 362 +++++++++++++++ .../integrations/expert_parallel/shard.py | 166 +++++++ src/axolotl/integrations/kernels/plugin.py | 36 +- src/axolotl/monkeypatch/accelerate/fsdp2.py | 11 + .../accelerate/parallelism_config.py | 282 +++++++++++- src/axolotl/utils/config/__init__.py | 7 +- src/axolotl/utils/distributed.py | 12 + src/axolotl/utils/trainer.py | 11 + tests/cli/test_load_cfg_capabilities.py | 3 + tests/integrations/test_expert_parallel.py | 435 ++++++++++++++++++ 20 files changed, 2119 insertions(+), 18 deletions(-) create mode 100644 examples/expert_parallel/qwen3_30ba3b_ep_fft_4gpu.yaml create mode 100644 examples/expert_parallel/qwen3_30ba3b_ep_fsdp_fft_4gpu.yaml create mode 100644 examples/expert_parallel/qwen3_30ba3b_ep_lora_4gpu.yaml create mode 100644 src/axolotl/integrations/expert_parallel/README.md create mode 100644 src/axolotl/integrations/expert_parallel/__init__.py create mode 100644 src/axolotl/integrations/expert_parallel/args.py create mode 100644 src/axolotl/integrations/expert_parallel/buffer.py create mode 100644 src/axolotl/integrations/expert_parallel/experts_fn.py create mode 100644 src/axolotl/integrations/expert_parallel/plugin.py create mode 100644 src/axolotl/integrations/expert_parallel/shard.py create mode 100644 tests/integrations/test_expert_parallel.py diff --git a/docs/nd_parallelism.qmd b/docs/nd_parallelism.qmd index 435e53e218..972953ff9f 100644 --- a/docs/nd_parallelism.qmd +++ b/docs/nd_parallelism.qmd @@ -40,6 +40,15 @@ Context Parallelism, also called [Sequence Parallelism](sequence_parallelism.qmd - The Challenge: Attention is not local; every token needs to "attend to" every other token. Splitting the sequence breaks this. - The Solution (`ring-flash-attention`): An efficient communication protocol is used. To compute attention for its local sequence chunk, each GPU passes its Key-Value (KV) cache to its neighbor in a "ring." After `N-1` steps, every GPU has seen the KV-cache from all other GPUs, allowing it to compute the correct attention values for its chunk. This is implemented using the highly optimized `flash-attention` kernel at each step. +### Expert Parallelism (EP) {#sec-ep} + +Expert Parallelism shards the experts of a Mixture-of-Experts (MoE) model across GPUs. Each rank holds `num_experts / ep_size` experts; tokens are routed to the rank that owns the chosen expert via DeepEP's fused dispatch/combine kernels. + +- How it works: the EP plugin slices each `Experts` module's `gate_up_proj` / `down_proj` along the experts dimension and replaces the dispatch/combine path with DeepEP. Each rank sees a different batch (so EP is data-parallel for *data*) but holds different expert shards (model-parallel for *expert weights*). +- When to use: MoE training where the experts dominate the parameter count and don't fit under FSDP alone, or where you want to compose with FSDP on orthogonal mesh axes. +- Composes with FSDP: `expert_parallel_size × dp_shard_size = world_size`. EP shards experts on the `ep` axis; FSDP shards non-experts (and the per-EP-slice expert weights) on the `dp_shard` axis. Process groups are disjoint. +- Requires the [DeepEP](https://github.com/deepseek-ai/DeepEP) library (Ampere or Hopper, NVLink). See [Expert Parallelism Integration](custom_integrations.qmd#expert-parallelism-integration) for install + integration details. + ### Hybrid Sharding Data Parallel (HSDP) {#sec-hsdp} HSDP is a 2D strategy that intelligently combines FSDP and DDP, typically for multi-node training. @@ -67,6 +76,9 @@ tensor_parallel_size: 1 # (default is 1, no TP) # Number of GPUs for Context/Sequence Parallelism. context_parallel_size: 1 # (default is 1, no CP) + +# Number of GPUs to shard MoE experts across (Expert Parallel). +expert_parallel_size: 1 # (default is 1, no EP) ``` Note: We recommend FSDP. DeepSpeed is only compatible with `tensor_parallel_size`. @@ -89,20 +101,28 @@ See our example configs [here](https://github.com/axolotl-ai-cloud/axolotl/tree/ - You want to shard the model across all 8 GPUs and also split the sequence length across all 8 GPUs. - Set `dp_shard_size: 8` and `context_parallel_size: 8`. Note: this means the data parallel group and context parallel group are the same. A more common setup might be to shard across a smaller group. +4. FSDP + EP on a 4-GPU MoE training run: + - You want EP to shard the experts and FSDP to shard non-expert params on orthogonal mesh axes. + - Set `expert_parallel_size: 2` and `dp_shard_size: 2` (`ep × dp_shard == world_size`). Add the `ExpertParallelPlugin` to `plugins:`. + ## Support Matrix This matrix describes how different parallelism methods can be combined in Axolotl. -| Combination | `dp_replicate_size` | `dp_shard_size` | `tp_size` | `cp_size` | Status & Notes | -| --- | :---: | :---: |:---:|:---:|---| -| **FSDP** (ZeRO-3) | 1 | >1 | 1 | 1 | ✅ Fully supported. Shards model across all GPUs. | -| **HSDP** | >1 | >1 | 1 | 1 | ✅ Fully supported. FSDP intra-node, DDP inter-node. | -| **FSDP + TP** | 1 | >1 | >1 | 1 | ✅ **2D Parallelism**. Shards the model across a `dp_shard` group, and TP-splits layers within the `tp` group. | -| **HSDP + TP** | >1 | >1 | >1 | 1 | ✅ **3D Parallelism**. A powerful but complex combination. | -| **FSDP + CP** | 1 | >1 | 1 | >1 | ✅ **2D Parallelism**. Combines FSDP with context parallelism. | -| **FSDP + TP + CP**| 1 | >1 | >1| >1| ✅ **3D Parallelism**. Another advanced combination. | -| DDP + TP/CP | >1 | 1 | >1 | >1 | ❌ **Not Supported**. The `ParallelismConfig` explicitly prevents this, as composing pure DDP with TP or CP is currently not supported. You should use FSDP + TP/CP instead (`dp_shard_size > 1`). | -| Just TP / CP | 1 | 1 | >1 | >1 | ✅ Supported. Useful for inference or when the model fits on one GPU but context is too long. | +| Combination | `dp_replicate_size` | `dp_shard_size` | `tp_size` | `cp_size` | `ep_size` | Status & Notes | +| --- | :---: | :---: |:---:|:---:|:---:|---| +| **FSDP** (ZeRO-3) | 1 | >1 | 1 | 1 | 1 | ✅ Fully supported. Shards model across all GPUs. | +| **HSDP** | >1 | >1 | 1 | 1 | 1 | ✅ Fully supported. FSDP intra-node, DDP inter-node. | +| **FSDP + TP** | 1 | >1 | >1 | 1 | 1 | ✅ **2D Parallelism**. Shards the model across a `dp_shard` group, and TP-splits layers within the `tp` group. | +| **HSDP + TP** | >1 | >1 | >1 | 1 | 1 | ✅ **3D Parallelism**. A powerful but complex combination. | +| **FSDP + CP** | 1 | >1 | 1 | >1 | 1 | ✅ **2D Parallelism**. Combines FSDP with context parallelism. | +| **FSDP + TP + CP**| 1 | >1 | >1| >1| 1 | ✅ **3D Parallelism**. Another advanced combination. | +| **EP** | 1 | 1 | 1 | 1 | >1 | ✅ Supported (MoE only). Shards experts across all ranks; non-experts replicated. | +| **FSDP + EP** | 1 | >1 | 1 | 1 | >1 | ✅ **2D Parallelism** (MoE). EP shards experts on the `ep` axis; FSDP shards non-experts on `dp_shard`. | +| EP + TP/CP | 1 | * | >1 | >1 | >1 | ❌ Not yet supported in v1; raises `NotImplementedError`. | +| DDP + TP/CP | >1 | 1 | >1 | >1 | 1 | ❌ **Not Supported**. The `ParallelismConfig` explicitly prevents this, as composing pure DDP with TP or CP is currently not supported. You should use FSDP + TP/CP instead (`dp_shard_size > 1`). | +| Just TP / CP | 1 | 1 | >1 | >1 | 1 | ✅ Supported. Useful for inference or when the model fits on one GPU but context is too long. | - `tp_size` refers to `tensor_parallel_size` - `cp_size` refers to `context_parallel_size` +- `ep_size` refers to `expert_parallel_size` diff --git a/examples/expert_parallel/qwen3_30ba3b_ep_fft_4gpu.yaml b/examples/expert_parallel/qwen3_30ba3b_ep_fft_4gpu.yaml new file mode 100644 index 0000000000..76b0ce3706 --- /dev/null +++ b/examples/expert_parallel/qwen3_30ba3b_ep_fft_4gpu.yaml @@ -0,0 +1,37 @@ +base_model: Qwen/Qwen3-30B-A3B-Instruct-2507 + +plugins: + - axolotl.integrations.expert_parallel.ExpertParallelPlugin + +experts_implementation: grouped_mm +expert_parallel_size: 4 # world size 4 + +datasets: + - path: mhenrichsen/alpaca_2k_test + type: alpaca + +dataset_prepared_path: +output_dir: ./outputs/out + +sequence_len: 2048 +sample_packing: true + +micro_batch_size: 1 +gradient_accumulation_steps: 1 +max_steps: 50 + +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 5e-6 +warmup_steps: 0.1 + +bf16: true +tf32: false + +gradient_checkpointing: true +flash_attention: true +logging_steps: 1 +evals_per_epoch: +saves_per_epoch: 1 + +seed: 42 diff --git a/examples/expert_parallel/qwen3_30ba3b_ep_fsdp_fft_4gpu.yaml b/examples/expert_parallel/qwen3_30ba3b_ep_fsdp_fft_4gpu.yaml new file mode 100644 index 0000000000..f3761eae24 --- /dev/null +++ b/examples/expert_parallel/qwen3_30ba3b_ep_fsdp_fft_4gpu.yaml @@ -0,0 +1,49 @@ +base_model: Qwen/Qwen3-30B-A3B-Instruct-2507 + +plugins: + - axolotl.integrations.expert_parallel.ExpertParallelPlugin + +experts_implementation: grouped_mm +# world size 4 minimum: expert_parallel_size x dp_shard_size +expert_parallel_size: 2 +dp_shard_size: 2 + +datasets: + - path: mhenrichsen/alpaca_2k_test + type: alpaca + +dataset_prepared_path: +output_dir: ./outputs/out + +sequence_len: 2048 +sample_packing: true + +micro_batch_size: 1 +gradient_accumulation_steps: 1 +max_steps: 50 + +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 5e-6 +warmup_steps: 0.1 + +bf16: true +tf32: false + +gradient_checkpointing: true +flash_attention: true +logging_steps: 1 +evals_per_epoch: +saves_per_epoch: 1 + +seed: 42 + +fsdp_config: + fsdp_version: 2 + offload_params: false + cpu_ram_efficient_loading: false + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Qwen3MoeDecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD + reshard_after_forward: true diff --git a/examples/expert_parallel/qwen3_30ba3b_ep_lora_4gpu.yaml b/examples/expert_parallel/qwen3_30ba3b_ep_lora_4gpu.yaml new file mode 100644 index 0000000000..1f08ed2dc0 --- /dev/null +++ b/examples/expert_parallel/qwen3_30ba3b_ep_lora_4gpu.yaml @@ -0,0 +1,59 @@ +base_model: Qwen/Qwen3-30B-A3B-Instruct-2507 + +plugins: + - axolotl.integrations.expert_parallel.ExpertParallelPlugin + +experts_implementation: grouped_mm +expert_parallel_size: 4 # world size 4 + +datasets: + - path: mhenrichsen/alpaca_2k_test + type: alpaca + +dataset_prepared_path: +output_dir: ./outputs/out + +sequence_len: 2048 +sample_packing: true + +adapter: lora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + # Add gate_up_proj and down_proj to also target shared experts (nn.Linear): + # - gate_up_proj + # - down_proj + +# Target routed experts (3D nn.Parameter tensors, not nn.Linear — use lora_target_parameters): +# lora_target_parameters: +# - mlp.experts.gate_up_proj +# - mlp.experts.down_proj + +lora_mlp_kernel: true +lora_qkv_kernel: true +lora_o_kernel: true + +micro_batch_size: 1 +gradient_accumulation_steps: 1 +max_steps: 50 + +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 0.003 +warmup_steps: 0.1 + +bf16: true +tf32: false + +gradient_checkpointing: true +flash_attention: true +logging_steps: 1 +evals_per_epoch: +saves_per_epoch: 1 + +seed: 42 diff --git a/src/axolotl/cli/train.py b/src/axolotl/cli/train.py index 4528577624..eb00c0a51b 100644 --- a/src/axolotl/cli/train.py +++ b/src/axolotl/cli/train.py @@ -121,6 +121,15 @@ def ray_train_func(kwargs: dict): env_capabilities=env_capabilities, ) + # Derive here (not in controller normalize_config) so the worker's + # validate_config above doesn't see both set and trip check_gas_bsz. + cfg.gradient_accumulation_steps = cfg.gradient_accumulation_steps or ( + cfg.batch_size // cfg.micro_batch_size + ) + cfg.batch_size = ( + cfg.batch_size or cfg.micro_batch_size * cfg.gradient_accumulation_steps + ) + prepare_optim_env(cfg) normalize_config(cfg) resolve_dtype(cfg) diff --git a/src/axolotl/integrations/expert_parallel/README.md b/src/axolotl/integrations/expert_parallel/README.md new file mode 100644 index 0000000000..c8fff1cfea --- /dev/null +++ b/src/axolotl/integrations/expert_parallel/README.md @@ -0,0 +1,204 @@ +# Expert Parallelism Integration + +Replaces the MoE dispatch/combine path with DeepEP's fused kernels. + +## Requirements + +Ampere (sm_80, A100) or Hopper (sm_90, H100), all-pairs NVLink. + +## Installation + +**Hopper (sm_90, H100), multi-node with NCCL 2.29+ (torch 2.11+) and OFED:** + +```bash +git clone --depth 1 https://github.com/deepseek-ai/DeepEP.git +cd DeepEP +TORCH_CUDA_ARCH_LIST=9.0 MAX_JOBS=16 uv pip install --no-build-isolation . +python -c "import deep_ep; print(deep_ep.Buffer)" +``` + +**Hopper (sm_90, H100), single-node intranode-only (no OFED):** + +```bash +git clone https://github.com/deepseek-ai/DeepEP.git +cd DeepEP +git checkout v1.2.1 +# Patch 1: setup.py — honor DISABLE_NVSHMEM=1 +git apply <<'EOF' +--- a/setup.py ++++ b/setup.py +@@ -19,7 +19,10 @@ if __name__ == '__main__': + disable_nvshmem = False + nvshmem_dir = os.getenv('NVSHMEM_DIR', None) + nvshmem_host_lib = 'libnvshmem_host.so' +- if nvshmem_dir is None: ++ if int(os.getenv('DISABLE_NVSHMEM', '0')): ++ disable_nvshmem = True ++ nvshmem_dir = None ++ elif nvshmem_dir is None: + try: + nvshmem_dir = importlib.util.find_spec("nvidia.nvshmem").submodule_search_locations[0] + nvshmem_host_lib = get_nvshmem_host_lib_name(nvshmem_dir) +EOF + +# Patch 2: setup.py — drop -rdc=true when NVSHMEM is disabled +git apply <<'EOF' +--- a/setup.py ++++ b/setup.py +@@ -71,7 +71,9 @@ if __name__ == '__main__': + os.environ['TORCH_CUDA_ARCH_LIST'] = os.getenv('TORCH_CUDA_ARCH_LIST', '9.0') + + # CUDA 12 flags +- nvcc_flags.extend(['-rdc=true', '--ptxas-options=--register-usage-level=10']) ++ nvcc_flags.append('--ptxas-options=--register-usage-level=10') ++ if not disable_nvshmem: ++ nvcc_flags.append('-rdc=true') + + # Disable LD/ST tricks, as some CUDA version does not support `.L1::no_allocate` + if os.environ['TORCH_CUDA_ARCH_LIST'].strip() != '9.0': +EOF + +DISABLE_NVSHMEM=1 TORCH_CUDA_ARCH_LIST=9.0 MAX_JOBS=16 \ + uv pip install --no-build-isolation . +python -c "import deep_ep; print(deep_ep.Buffer)" +``` + +Notes: + +- Hopper kernels (FP8, TMA, etc.) are preserved; only intranode dispatch/combine is built — appropriate for single-node H100×{4,8}. +- Patch 1 lets `DISABLE_NVSHMEM=1` skip the NVSHMEM build path, which would otherwise need Mellanox OFED dev headers (`infiniband/mlx5dv.h`). +- Patch 2 drops `-rdc=true` when NVSHMEM is off; otherwise the device-link step has nothing to link against and import fails with `__cudaRegisterLinkedBinary_*` undefined symbol. +- The `v1.2.1` pin sidesteps DeepEP HEAD's `csrc/elastic/` (Engram/EPv2, commit `b306af0`), which needs `ncclGinRequest_t` from NCCL 2.29+. On NCCL ≥ 2.29 (torch 2.11+) you can drop `git checkout v1.2.1` and build from HEAD with the same two patches. + +**Ampere (sm_80, A100, intranode-only)** — needs two small source patches gated on `DISABLE_NVSHMEM=1`: + +```bash +git clone --depth 1 https://github.com/deepseek-ai/DeepEP.git +cd DeepEP + +# Patch 1: setup.py — honor DISABLE_NVSHMEM=1 +git apply <<'EOF' +--- a/setup.py ++++ b/setup.py +@@ -19,7 +19,10 @@ if __name__ == '__main__': + disable_nvshmem = False + nvshmem_dir = os.getenv('NVSHMEM_DIR', None) + nvshmem_host_lib = 'libnvshmem_host.so' +- if nvshmem_dir is None: ++ if int(os.getenv('DISABLE_NVSHMEM', '0')): ++ disable_nvshmem = True ++ nvshmem_dir = None ++ elif nvshmem_dir is None: + try: + nvshmem_dir = importlib.util.find_spec("nvidia.nvshmem").submodule_search_locations[0] + nvshmem_host_lib = get_nvshmem_host_lib_name(nvshmem_dir) +EOF + +# Patch 2: csrc/deep_ep.cpp — gate the three mask_buffer methods +git apply <<'EOF' +--- a/csrc/deep_ep.cpp ++++ b/csrc/deep_ep.cpp +@@ -1823,22 +1823,34 @@ bool is_sm90_compiled() { + } + + void Buffer::low_latency_update_mask_buffer(int rank_to_mask, bool mask) { ++#ifndef DISABLE_NVSHMEM + EP_HOST_ASSERT(mask_buffer_ptr != nullptr and "Shrink mode must be enabled"); + EP_HOST_ASSERT(rank_to_mask >= 0 and rank_to_mask < num_ranks); + internode_ll::update_mask_buffer(mask_buffer_ptr, rank_to_mask, mask, at::cuda::getCurrentCUDAStream()); ++#else ++ EP_HOST_ASSERT(false and "NVSHMEM is disabled during compilation"); ++#endif + } + + void Buffer::low_latency_query_mask_buffer(const torch::Tensor& mask_status) { ++#ifndef DISABLE_NVSHMEM + EP_HOST_ASSERT(mask_buffer_ptr != nullptr and "Shrink mode must be enabled"); + EP_HOST_ASSERT(mask_status.numel() == num_ranks && mask_status.scalar_type() == torch::kInt32); + + internode_ll::query_mask_buffer( + mask_buffer_ptr, num_ranks, reinterpret_cast(mask_status.data_ptr()), at::cuda::getCurrentCUDAStream()); ++#else ++ EP_HOST_ASSERT(false and "NVSHMEM is disabled during compilation"); ++#endif + } + + void Buffer::low_latency_clean_mask_buffer() { ++#ifndef DISABLE_NVSHMEM + EP_HOST_ASSERT(mask_buffer_ptr != nullptr and "Shrink mode must be enabled"); + internode_ll::clean_mask_buffer(mask_buffer_ptr, num_ranks, at::cuda::getCurrentCUDAStream()); ++#else ++ EP_HOST_ASSERT(false and "NVSHMEM is disabled during compilation"); ++#endif + } + + } // namespace deep_ep +EOF + +DISABLE_NVSHMEM=1 DISABLE_SM90_FEATURES=1 TORCH_CUDA_ARCH_LIST=8.0 MAX_JOBS=16 \ + uv pip install --no-build-isolation . +python -c "import deep_ep; print(deep_ep.Buffer)" +``` + +## Usage + +```yaml +plugins: + - axolotl.integrations.expert_parallel.ExpertParallelPlugin + +expert_parallel_size: 2 # 1 = disabled (default); > 1 = enabled +``` + +For composition with FSDP at 4+ GPUs, set both `expert_parallel_size` and `dp_shard_size`. The product must equal `world_size`: + +```yaml +# 4-GPU example: ep × dp_shard = 2 × 2 = 4 +expert_parallel_size: 2 +dp_shard_size: 2 +fsdp_config: + fsdp_version: 2 + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: Qwen3MoeDecoderLayer + state_dict_type: FULL_STATE_DICT + sharding_strategy: FULL_SHARD +``` + +See full example configs at [`examples/expert_parallel/`](https://github.com/axolotl-ai-cloud/axolotl/tree/main/examples/expert_parallel). + +#### Implementation notes + +EP composes with the local-experts kernel you've already configured: ScatterMoE, ~SonicMoE~ (WIP), grouped_mm, or eager. + +EP composes with FSDP on orthogonal mesh axes: experts are sharded across the `ep` axis, non-expert params across `dp_shard`. The two collectives run on disjoint process groups, so they don't conflict. Layout follows [*Expert Parallelism with FSDP* (tinkerings.dev)](https://tinkerings.dev/posts/expert_parallel.html) — "rows share weights, columns move tokens." + +| Your existing config | Local kernel under DeepEP | +|-----------------------------------------------------|---------------------------| +| `use_scattermoe: true` | ScatterMoE (Triton) | +| `use_sonicmoe: true` (WIP) | SonicMoE (Gemma4) | +| `experts_implementation: grouped_mm` / `batched_mm` | grouped_mm (transformers) | +| `experts_implementation: eager` | eager Python loop | +| (unset) | grouped_mm (default) | + +## Limitations + +- Models' modeling code must use `@use_experts_implementation` (canonical 3D `gate_up_proj` / `down_proj`). `ModuleList` as used in Mixtral is not supported. +- `num_experts` must be divisible by `expert_parallel_size`. +- 3+ axis composition (EP × DP × TP/CP) is not yet supported in v1; raises `NotImplementedError`. +- DeepEP limitation: Low-latency (LL) kernels are inter-node only by design (pure RDMA via IBGDA). Single-node + intranode setups always use the standard kernels and don't benefit from LL. +- FP8 dispatch needs Hopper + DISABLE_SM90_FEATURES=0. + +## Troubleshooting + +### `CUBLAS_STATUS_INVALID_VALUE` on a basic bf16 GEMM after `import deep_ep` + +The system's `libcublas.so.13` is older than what cu130 torch expects. Put the cu13 lib that ships with the torch wheel on `LD_LIBRARY_PATH`: + +```bash +export LD_LIBRARY_PATH="$(python -c 'import nvidia.cu13 as m; print(list(m.__path__)[0] + "/lib")'):$LD_LIBRARY_PATH" +``` + +Unrelated to DeepEP itself, but anyone on cu130 torch hits it on boxes with a system CUDA toolkit older than 13.0. + +### `CUDA error 803` (system not yet initialized) + +On driver `< 580`, also prepend `/usr/local/cuda-13.0/compat` to `LD_LIBRARY_PATH`. **Do not** add the compat dir on driver `≥ 580` (its `libcuda` is older than the running driver and triggers `CUDA error 803`). diff --git a/src/axolotl/integrations/expert_parallel/__init__.py b/src/axolotl/integrations/expert_parallel/__init__.py new file mode 100644 index 0000000000..188c42d794 --- /dev/null +++ b/src/axolotl/integrations/expert_parallel/__init__.py @@ -0,0 +1,26 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +"""Expert-Parallel (DeepEP) integration for axolotl. + +Replaces the dispatch/combine path in transformers MoE blocks with DeepEP's +fused kernels. Registers four names in `transformers.integrations.moe.ALL_EXPERTS_FUNCTIONS`: + +- `deep_ep` — eager local expert MLP (reference) +- `deep_ep_grouped_mm` — transformers' grouped_mm kernel (default) +- `deep_ep_scattermoe` — axolotl's ScatterMoE kernel +- `deep_ep_sonicmoe` — axolotl's SonicMoE kernel +""" + +from .args import ExpertParallelArgs +from .plugin import ExpertParallelPlugin + +__all__ = [ + "ExpertParallelArgs", + "ExpertParallelPlugin", +] diff --git a/src/axolotl/integrations/expert_parallel/args.py b/src/axolotl/integrations/expert_parallel/args.py new file mode 100644 index 0000000000..792ff1850a --- /dev/null +++ b/src/axolotl/integrations/expert_parallel/args.py @@ -0,0 +1,48 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +"""Pydantic args for the Expert-Parallel (DeepEP) plugin.""" + +from typing import Literal + +from pydantic import BaseModel, model_validator + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +class ExpertParallelArgs(BaseModel): + """Input args for the expert_parallel plugin. See the integration README.""" + + expert_parallel_size: int = 1 + """Number of EP ranks. 1 = disabled (default), > 1 = enabled.""" + + expert_parallel_backend: Literal["deep_ep"] = "deep_ep" + + expert_parallel_num_nvl_bytes: int = 256 << 20 + + expert_parallel_num_rdma_bytes: int = 0 + + expert_parallel_fallback_on_unsupported: bool = True + + @model_validator(mode="after") + def _validate(self): + if self.expert_parallel_size < 1: + raise ValueError( + f"expert_parallel_size must be >= 1 (got {self.expert_parallel_size!r}). " + f"Use 1 to disable EP." + ) + + if self.expert_parallel_size > 1 and self.expert_parallel_num_rdma_bytes != 0: + LOG.warning( + "expert_parallel_num_rdma_bytes != 0 — RDMA path requires " + "Hopper + IBGDA-capable InfiniBand. Will fail on Ampere/intranode." + ) + + return self diff --git a/src/axolotl/integrations/expert_parallel/buffer.py b/src/axolotl/integrations/expert_parallel/buffer.py new file mode 100644 index 0000000000..98684b92e7 --- /dev/null +++ b/src/axolotl/integrations/expert_parallel/buffer.py @@ -0,0 +1,62 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +"""DeepEP `Buffer` singleton, lazily constructed on first call. + +A single Buffer is reused across all MoE layers in a model, since DeepEP's +intranode kernels are sized by `num_nvl_bytes` which we set conservatively at +plugin init. Per-layer Buffer construction would burn memory. +""" + +from __future__ import annotations + +from typing import Optional + +import torch.distributed as dist + +_BUFFER = None +_EP_GROUP: Optional[dist.ProcessGroup] = None +_NUM_NVL_BYTES = 256 << 20 +_NUM_RDMA_BYTES = 0 + + +def configure_buffer( + ep_group: Optional[dist.ProcessGroup], + num_nvl_bytes: int = 256 << 20, + num_rdma_bytes: int = 0, +) -> None: + """Stash params for lazy Buffer construction. Call from `post_model_build`.""" + global _EP_GROUP, _NUM_NVL_BYTES, _NUM_RDMA_BYTES, _BUFFER + _EP_GROUP = ep_group + _NUM_NVL_BYTES = num_nvl_bytes + _NUM_RDMA_BYTES = num_rdma_bytes + _BUFFER = None # invalidate any prior buffer + + +def get_buffer(): + """Return the (lazily constructed) DeepEP Buffer.""" + global _BUFFER + if _BUFFER is not None: + return _BUFFER + + import deep_ep + + group = _EP_GROUP if _EP_GROUP is not None else dist.group.WORLD + _BUFFER = deep_ep.Buffer( + group=group, + num_nvl_bytes=_NUM_NVL_BYTES, + num_rdma_bytes=_NUM_RDMA_BYTES, + low_latency_mode=False, + ) + return _BUFFER + + +def reset_buffer() -> None: + """Drop the cached Buffer. Used in tests.""" + global _BUFFER + _BUFFER = None diff --git a/src/axolotl/integrations/expert_parallel/experts_fn.py b/src/axolotl/integrations/expert_parallel/experts_fn.py new file mode 100644 index 0000000000..1cc81ef293 --- /dev/null +++ b/src/axolotl/integrations/expert_parallel/experts_fn.py @@ -0,0 +1,278 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +"""DeepEP-backed registered functions for `ALL_EXPERTS_FUNCTIONS`. + +Four names registered (eager / grouped_mm / scattermoe / sonicmoe) sharing one +`_deep_ep_forward` body. Templates ported from `bench_deep_ep.py` Stage 1 +modes 3 and 4. +""" + +from __future__ import annotations + +import torch +import torch.nn.functional as F + +from .buffer import get_buffer + + +def _eager_local(experts, recv_x, recv_topk_idx, recv_topk_weights): + """Eager Python loop over local experts. Reference for numerics.""" + out = torch.zeros_like(recv_x) + num_local = getattr(experts, "num_local_experts", experts.num_experts) + for e in range(num_local): + rows, ks = (recv_topk_idx == e).nonzero(as_tuple=True) + if rows.numel() == 0: + continue + x_e = recv_x[rows] + gate_up = F.linear(x_e, experts.gate_up_proj[e]) + gate, up = gate_up.chunk(2, dim=-1) + h = experts.act_fn(gate) * up + y = F.linear(h, experts.down_proj[e]) + weighted = (y * recv_topk_weights[rows, ks].unsqueeze(-1)).to(out.dtype) + out.index_add_(0, rows, weighted) + return out + + +def _maybe_install_decorator_attrs(experts): + """`@use_experts_implementation` injects has_gate/has_bias/is_transposed. + For models loaded outside the decorator path (or in tests), patch them in. + """ + if not hasattr(experts, "has_gate"): + experts.has_gate = True + if not hasattr(experts, "has_bias"): + experts.has_bias = hasattr(experts, "gate_up_proj_bias") + if not hasattr(experts, "is_transposed"): + experts.is_transposed = False + + +def _grouped_mm_local(experts, recv_x, recv_topk_idx, recv_topk_weights): + from transformers.integrations.moe import grouped_mm_experts_forward + + _maybe_install_decorator_attrs(experts) + return grouped_mm_experts_forward(experts, recv_x, recv_topk_idx, recv_topk_weights) + + +def _scattermoe_local(experts, recv_x, recv_topk_idx, recv_topk_weights): + from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + scattermoe_experts_forward, + ) + + return scattermoe_experts_forward(experts, recv_x, recv_topk_idx, recv_topk_weights) + + +def _sonicmoe_local(experts, recv_x, recv_topk_idx, recv_topk_weights): + raise NotImplementedError("Sonicmoe + EP is not yet properly implemented.") + # from axolotl.integrations.kernels.libs.sonicmoe.gemma4_experts import ( + # gemma4_sonicmoe_experts_forward, + # ) + + # return gemma4_sonicmoe_experts_forward( + # experts, recv_x, recv_topk_idx, recv_topk_weights + # ) + + +_LOCAL_KERNELS = { + "eager": _eager_local, + "grouped_mm": _grouped_mm_local, + "scattermoe": _scattermoe_local, + "sonicmoe": _sonicmoe_local, +} + + +class _DeepEPDispatch(torch.autograd.Function): + """Autograd wrapper for `Buffer.dispatch`. + + Forward: routes `x` to ranks owning its top-k experts. + Backward of dispatch is combine — gradients on `recv_x` are reduced back + across ranks using the same handle. Only `x`'s grad is returned (idx/weights + are non-differentiable bookkeeping). + """ + + @staticmethod + def forward( + ctx, x, topk_idx, topk_weights, num_per_rank, num_per_expert, is_in_rank + ): + buffer = get_buffer() + recv_x, recv_topk_idx, recv_topk_weights, _, handle, _ = buffer.dispatch( + x, + num_tokens_per_rank=num_per_rank, + is_token_in_rank=is_in_rank, + num_tokens_per_expert=num_per_expert, + topk_idx=topk_idx, + topk_weights=topk_weights, + ) + ctx.handle = handle + return recv_x, recv_topk_idx, recv_topk_weights, _DeepEPHandleHolder(handle) + + @staticmethod + def backward(ctx, grad_recv_x, _grad_idx, grad_recv_w, _grad_handle): + buffer = get_buffer() + # Backward-of-dispatch is combine: reduce grad on recv_x (and recv_topk_weights) + # back to the source rank via the same handle. + topk_w_grad = ( + grad_recv_w.contiguous() + if (grad_recv_w is not None and grad_recv_w.numel() > 0) + else None + ) + grad_x, grad_topk_w, _ = buffer.combine( + grad_recv_x.contiguous(), + ctx.handle, + topk_weights=topk_w_grad, + ) + return grad_x, None, grad_topk_w, None, None, None + + +class _DeepEPCombine(torch.autograd.Function): + """Autograd wrapper for `Buffer.combine`. + + Forward: sums per-rank partial outputs back into the source token. + Backward of combine is dispatch — reuses the handle from the dispatch call. + """ + + @staticmethod + def forward(ctx, x, handle_holder): + buffer = get_buffer() + ctx.handle = handle_holder.handle + combined, _, _ = buffer.combine(x.contiguous(), handle_holder.handle) + return combined + + @staticmethod + def backward(ctx, grad_combined): + buffer = get_buffer() + # backward-of-combine is dispatch: reuse the cached handle to send + # gradients to the ranks that produced partial outputs. + recv_grad, _, _, _, _, _ = buffer.dispatch( + grad_combined.contiguous(), handle=ctx.handle + ) + return recv_grad, None + + +class _DeepEPHandleHolder: + """Carries the dispatch handle through to combine without breaking autograd + (autograd.Function output must be Tensors or non-Tensor objects; tuples + of opaque handles work as a passthrough since combine doesn't differentiate + against them).""" + + def __init__(self, handle): + self.handle = handle + + +def _deep_ep_forward(self, hidden_states, top_k_index, top_k_weights, *, kernel_name): + """Shared dispatch -> local-experts -> combine pipeline. + + Inputs come in with **global** routing indices (we do not run + `transformers.RouterParallel`; see DEEP_EP.md §2.4 for why). Sentinels are + `-1` for slots routed to remote experts; we mask them to a valid local id + with weight=0 so the local kernel can index safely. + """ + if hidden_states.dtype != torch.bfloat16: + original_dtype = hidden_states.dtype + hidden_states = hidden_states.to(torch.bfloat16) + else: + original_dtype = None + + buffer = get_buffer() + E_global = getattr(self, "num_experts_global", self.num_experts) + + topk_idx_i64 = top_k_index.to(torch.int64) + topk_w_f32 = top_k_weights.to(torch.float32) + + # Layout is non-differentiable (bookkeeping only). + with torch.no_grad(): + num_per_rank, _, num_per_expert, is_in_rank, _ = buffer.get_dispatch_layout( + topk_idx_i64, E_global + ) + + recv_x, recv_topk_idx, recv_topk_weights, handle_holder = _DeepEPDispatch.apply( + hidden_states, + topk_idx_i64, + topk_w_f32, + num_per_rank, + num_per_expert, + is_in_rank, + ) + + # Mask -1 sentinels for kernels that don't filter. + safe_idx = torch.where( + recv_topk_idx >= 0, recv_topk_idx, torch.zeros_like(recv_topk_idx) + ) + valid_mask = (recv_topk_idx >= 0).to(recv_topk_weights.dtype) + safe_w = recv_topk_weights * valid_mask + + local_out = _LOCAL_KERNELS[kernel_name](self, recv_x, safe_idx, safe_w) + + combined = _DeepEPCombine.apply(local_out, handle_holder) + + if original_dtype is not None: + combined = combined.to(original_dtype) + return combined + + +def deep_ep_experts_forward(self, hidden_states, top_k_index, top_k_weights): + return _deep_ep_forward( + self, hidden_states, top_k_index, top_k_weights, kernel_name="eager" + ) + + +def deep_ep_grouped_mm_experts_forward(self, hidden_states, top_k_index, top_k_weights): + return _deep_ep_forward( + self, hidden_states, top_k_index, top_k_weights, kernel_name="grouped_mm" + ) + + +def deep_ep_scattermoe_experts_forward(self, hidden_states, top_k_index, top_k_weights): + return _deep_ep_forward( + self, hidden_states, top_k_index, top_k_weights, kernel_name="scattermoe" + ) + + +def deep_ep_sonicmoe_experts_forward(self, hidden_states, top_k_index, top_k_weights): + return _deep_ep_forward( + self, hidden_states, top_k_index, top_k_weights, kernel_name="sonicmoe" + ) + + +REGISTRY = { + "deep_ep": deep_ep_experts_forward, + "deep_ep_grouped_mm": deep_ep_grouped_mm_experts_forward, + "deep_ep_scattermoe": deep_ep_scattermoe_experts_forward, + "deep_ep_sonicmoe": deep_ep_sonicmoe_experts_forward, +} + + +def register_all() -> None: + """Register the four names in `ALL_EXPERTS_FUNCTIONS` and whitelist them.""" + from transformers.integrations.moe import ALL_EXPERTS_FUNCTIONS + from transformers.modeling_utils import PreTrainedModel + + for name, fn in REGISTRY.items(): + ALL_EXPERTS_FUNCTIONS.register(name, fn) + + if not getattr( + PreTrainedModel.get_correct_experts_implementation, "_deep_ep_patched", False + ): + original = PreTrainedModel.get_correct_experts_implementation + + def patched(self, requested_experts): + if requested_experts in REGISTRY: + return requested_experts + return original(self, requested_experts) + + patched._deep_ep_patched = True # type: ignore[attr-defined] + PreTrainedModel.get_correct_experts_implementation = patched # type: ignore[assignment] + + +def kernel_to_registered_name(kernel: str) -> str: + """Map `expert_parallel_local_kernel` -> registered name.""" + return { + "eager": "deep_ep", + "grouped_mm": "deep_ep_grouped_mm", + "scattermoe": "deep_ep_scattermoe", + "sonicmoe": "deep_ep_sonicmoe", + }[kernel] diff --git a/src/axolotl/integrations/expert_parallel/plugin.py b/src/axolotl/integrations/expert_parallel/plugin.py new file mode 100644 index 0000000000..3af4214695 --- /dev/null +++ b/src/axolotl/integrations/expert_parallel/plugin.py @@ -0,0 +1,362 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +"""Expert-Parallel (DeepEP) plugin for axolotl.""" + +from __future__ import annotations + +from importlib.util import find_spec + +import torch +import torch.distributed as dist + +from axolotl.integrations.base import BasePlugin +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +class ExpertParallelPlugin(BasePlugin): + """Plugin that swaps MoE dispatch/combine for DeepEP-fused kernels.""" + + def get_input_args(self): + return "axolotl.integrations.expert_parallel.ExpertParallelArgs" + + def pre_model_load(self, cfg): + if not self._is_ep_enabled(cfg): + return + + if not self._deep_ep_available(cfg): + return # already-warned fallback path + + # Cross-cfg validation that args.py can't do (it only sees its own fields). + self._validate_mesh_axes(cfg) + + from .experts_fn import kernel_to_registered_name, register_all + + register_all() + + # Upgrade the user's chosen local kernel to its DeepEP-wrapped variant. + local_kernel = self._infer_local_kernel(cfg) + composite = kernel_to_registered_name(local_kernel) + previous = getattr(cfg, "experts_implementation", None) + cfg.experts_implementation = composite + LOG.debug( + f"expert_parallel: experts_implementation {previous!r} -> {composite!r} " + f"(local kernel: {local_kernel!r})" + ) + + def post_model_build(self, cfg, model): + if not self._is_ep_enabled(cfg): + return + if not self._deep_ep_available(cfg): + return + + from .buffer import configure_buffer + from .shard import shard_expert_weights + + ep_group = self._resolve_ep_group(cfg) + sharded = shard_expert_weights(model, ep_group) + + if sharded == 0: + LOG.warning( + "expert_parallel_enabled=true but no Experts modules were detected " + "for sharding (model uses a non-canonical layout, or single-rank). " + "DeepEP dispatch/combine will run as a no-op." + ) + + configure_buffer( + ep_group=ep_group, + num_nvl_bytes=cfg.expert_parallel_num_nvl_bytes, + num_rdma_bytes=cfg.expert_parallel_num_rdma_bytes, + ) + # Pure-EP path: register the grad-scale hook now. FSDP+EP defers + # registration to `fully_shard_experts` (after experts become DTensors). + if (cfg.dp_shard_size or 1) <= 1: + ep_size = cfg.expert_parallel_size or 1 + self._register_expert_grad_scale(model, ep_size) + + def post_model_load(self, cfg, model): + """Propagate DDP-ignored params to the outermost model wrapper. + + `post_model_build` set `_ddp_params_and_buffers_to_ignore` on the inner + model. After PEFT wraps it (in `PeftModel`), DDP wraps `PeftModel`, but + DDP looks for the attribute on the top-level module — which is now + `PeftModel`, not our inner model. Mirror the list up. + """ + if not self._is_ep_enabled(cfg): + return + + # Find the inner module that has the attribute (shard set it on whatever + # was the top-level model at post_model_build time). + inner = getattr(model, "base_model", model) + # base_model may itself be wrapped (e.g., LoraModel.model). Recurse. + while not hasattr(inner, "_ddp_params_and_buffers_to_ignore"): + next_inner = getattr(inner, "model", None) or getattr( + inner, "base_model", None + ) + if next_inner is None or next_inner is inner: + break + inner = next_inner + + ignore_list = getattr(inner, "_ddp_params_and_buffers_to_ignore", None) + if not ignore_list: + return + + # PEFT prefixes parameter names. Re-resolve the list against the wrapper + # so DDP can match by name. + resolved: list[str] = [] + wrapper_param_names = {n for n, _ in model.named_parameters()} + wrapper_buffer_names = {n for n, _ in model.named_buffers()} + all_names = wrapper_param_names | wrapper_buffer_names + + for short_name in ignore_list: + # Match either an exact suffix or with PEFT's `base_model.model.` prefix. + for full in all_names: + if ( + full == short_name + or full.endswith("." + short_name) + or full.endswith(short_name) + ): + resolved.append(full) + + # De-dup while preserving order. + seen = set() + resolved = [n for n in resolved if not (n in seen or seen.add(n))] + + existing = list(getattr(model, "_ddp_params_and_buffers_to_ignore", [])) + model._ddp_params_and_buffers_to_ignore = existing + resolved + LOG.debug( + f"expert_parallel: propagated {len(resolved)} DDP-ignored param " + f"name(s) onto outer wrapper {type(model).__name__}." + ) + + @staticmethod + def _infer_local_kernel(cfg) -> str: + """Decide which local-experts kernel runs under DeepEP dispatch. + + `use_scattermoe` / `use_sonicmoe` are the master flags from + `kernels/args.py` and take precedence; otherwise fall back to + `experts_implementation` (`eager` / `grouped_mm` / `batched_mm`). + """ + if getattr(cfg, "use_scattermoe", False): + return "scattermoe" + + if getattr(cfg, "use_sonicmoe", False): + return "sonicmoe" + + ei = getattr(cfg, "experts_implementation", None) + if ei in ("grouped_mm", "batched_mm"): + return "grouped_mm" + if ei == "eager": + return "eager" + # default: upstream-shipped fast kernel + return "grouped_mm" + + # Cached 2D DeviceMesh when EP composes with FSDP. Set by `_resolve_ep_group`. + _device_mesh = None + + @staticmethod + def _accelerate_mesh(): + """Return accelerate's device_mesh, force-creating the Accelerator if + needed. The AcceleratorState singleton makes this idempotent w.r.t. + the trainer's later `Accelerator()` call. + """ + from accelerate import Accelerator + from accelerate.state import AcceleratorState + + try: + state = AcceleratorState() + except (RuntimeError, AttributeError, ValueError) as e: + # ValueError occurs from unittest due to no trainer constructing Accelerator() + LOG.debug(f"expert_parallel: AcceleratorState() not ready: {e}") + return None + mesh = getattr(state, "device_mesh", None) + if mesh is not None: + return mesh + try: + Accelerator() + except (RuntimeError, ValueError) as e: + LOG.debug(f"expert_parallel: Accelerator() force-init failed: {e}") + return None + return getattr(AcceleratorState(), "device_mesh", None) + + @staticmethod + def _resolve_ep_group(cfg): + """Return the EP ProcessGroup. + + For FSDP+EP, returns `accelerate_mesh["ep"].get_group()` — the same + process group that accelerate's parallelism_config built. For pure EP + (ep_size == world_size, no FSDP), returns `dist.group.WORLD`. + """ + if not dist.is_available() or not dist.is_initialized(): + return None + + world_size = dist.get_world_size() + ep_size = getattr(cfg, "expert_parallel_size", 1) or 1 + dp_shard_size = getattr(cfg, "dp_shard_size", None) or 1 + tp_size = getattr(cfg, "tensor_parallel_size", None) or 1 + cp_size = getattr(cfg, "context_parallel_size", None) or 1 + + if ep_size <= 1: + return dist.group.WORLD + + # Validate the world_size = product check. + product = ep_size * dp_shard_size * tp_size * cp_size + if product != world_size: + raise ValueError( + f"expert_parallel_size ({ep_size}) * dp_shard_size ({dp_shard_size}) " + f"* tensor_parallel_size ({tp_size}) * context_parallel_size ({cp_size}) " + f"= {product}, but world_size = {world_size}. The product must equal " + f"the world size for orthogonal mesh axes to be valid." + ) + + if ep_size == world_size: + return dist.group.WORLD + + # EP + FSDP — read the ep group from accelerate's mesh, or build one + # ourselves if accelerate hasn't (e.g., topology unit tests that drive + # `_resolve_ep_group` directly without an Accelerator). + if dp_shard_size > 1: + if tp_size > 1 or cp_size > 1: + raise NotImplementedError( + "EP composition with TP/CP not yet supported. Got " + f"ep={ep_size}, dp_shard={dp_shard_size}, tp={tp_size}, cp={cp_size}. " + "v1 supports only EP-only or EP × dp_shard." + ) + mesh = ExpertParallelPlugin._accelerate_mesh() + if mesh is None or "ep" not in mesh.mesh_dim_names: + from torch.distributed.device_mesh import init_device_mesh + + mesh = init_device_mesh( + "cuda" if torch.cuda.is_available() else "cpu", + (ep_size, dp_shard_size), + mesh_dim_names=("ep", "dp_shard"), + ) + ExpertParallelPlugin._device_mesh = mesh + LOG.debug( + f"expert_parallel: ep mesh shape={tuple(mesh.shape)} " + f"axes={mesh.mesh_dim_names}; ep group " + f"members={dist.get_process_group_ranks(mesh['ep'].get_group())}" + ) + return mesh["ep"].get_group() + + # ep_size > 1, ep_size < world_size, dp_shard_size == 1 — invalid. + raise ValueError( + f"expert_parallel_size ({ep_size}) < world_size ({world_size}) " + "without dp_shard_size > 1 to fill the remaining axes is not supported. " + "Set dp_shard_size such that ep × dp_shard == world_size, or set " + "expert_parallel_size = world_size for pure EP." + ) + + @staticmethod + def fully_shard_experts(model, dp_shard_mesh, fsdp2_kwargs): + """Pre-wrap each Experts module with FSDP on the `dp_shard` axis. + + Called from the patched `fsdp2_prepare_model` BEFORE the outer auto-wrap + so experts become FSDPModules and the auto-wrap walker skips them. + Inherits the outer wrap's policy (mp, offload, reshard) so inner/outer + collective dtypes line up; only `mesh` is overridden. + """ + from torch.distributed.fsdp import fully_shard + + from .shard import _detect_experts_modules + + kwargs = dict(fsdp2_kwargs) + kwargs["mesh"] = dp_shard_mesh + kwargs.pop("ignored_params", None) + + for _name, module in _detect_experts_modules(model): + fully_shard(module, **kwargs) + + LOG.debug( + f"expert_parallel: pre-wrapped Experts modules on dp_shard mesh " + f"(size={dp_shard_mesh.size()})." + ) + + root = dp_shard_mesh._get_root_mesh() + ep_size = ( + root["ep"].size() + if root is not None and "ep" in (root.mesh_dim_names or ()) + else 1 + ) + ExpertParallelPlugin._register_expert_grad_scale(model, ep_size) + + @staticmethod + def _register_expert_grad_scale(model, ep_size: int) -> int: + """Scale expert weight grads by `1/ep_size` so EP / FSDP / FSDP+EP + produce the same effective gradient. + """ + from .shard import _detect_experts_modules + + if ep_size <= 1: + return 0 + scale = 1.0 / ep_size + + def _scale(p): + if p.grad is not None: + p.grad.mul_(scale) + + n_hooks = 0 + for _name, module in _detect_experts_modules(model): + for p in module.parameters(recurse=True): + p.register_post_accumulate_grad_hook(_scale) + n_hooks += 1 + LOG.debug( + f"expert_parallel: registered {n_hooks} expert grad-scale hooks " + f"(scale = 1/{ep_size})" + ) + return n_hooks + + @staticmethod + def _is_ep_enabled(cfg) -> bool: + """EP is enabled when expert_parallel_size > 1 (mirrors TP / DP UX).""" + ep_size = getattr(cfg, "expert_parallel_size", 1) or 1 + return ep_size > 1 + + @staticmethod + def _validate_mesh_axes(cfg) -> None: + """Sanity-check the mesh-axis sizes early, with a clear error. + + `_resolve_ep_group` re-validates at process-group construction time; + this catches misconfigured YAMLs before model loading wastes minutes. + """ + ep_size = getattr(cfg, "expert_parallel_size", 1) or 1 + if ep_size <= 1: + return + + if not (dist.is_available() and dist.is_initialized()): + return # validated at process-group time + world_size = dist.get_world_size() + if world_size <= 1: + return # single-rank context; mesh shapes are meaningless + dp_shard_size = getattr(cfg, "dp_shard_size", None) or 1 + tp_size = getattr(cfg, "tensor_parallel_size", None) or 1 + cp_size = getattr(cfg, "context_parallel_size", None) or 1 + + product = ep_size * dp_shard_size * tp_size * cp_size + if product != world_size: + raise ValueError( + f"expert_parallel: world_size ({world_size}) must equal " + f"expert_parallel_size ({ep_size}) * dp_shard_size ({dp_shard_size}) " + f"* tensor_parallel_size ({tp_size}) * context_parallel_size ({cp_size}) " + f"= {product}." + ) + + @staticmethod + def _deep_ep_available(cfg) -> bool: + if find_spec("deep_ep") is not None: + return True + msg = ( + "expert_parallel_enabled=true but `deep_ep` is not importable. " + "See the integration README for install instructions." + ) + if cfg.expert_parallel_fallback_on_unsupported: + LOG.warning(msg + " Falling back to standard experts implementation.") + return False + raise ImportError(msg) diff --git a/src/axolotl/integrations/expert_parallel/shard.py b/src/axolotl/integrations/expert_parallel/shard.py new file mode 100644 index 0000000000..8ec1ea94a5 --- /dev/null +++ b/src/axolotl/integrations/expert_parallel/shard.py @@ -0,0 +1,166 @@ +# Copyright 2026 Axolotl AI. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +"""Generic expert-weight sharding for `@use_experts_implementation` modules. + +After this runs (in `post_model_build`, before FSDP wraps), each rank's Experts +modules hold only their local slice of the experts dim. The registered +`deep_ep_*` forward function then handles dispatch -> local compute -> combine. +""" + +from __future__ import annotations + +import gc + +import torch +import torch.distributed as dist + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def _replace_with_slice(module, attr_name: str, start: int, end: int) -> None: + """Replace `module.{attr_name}` with a fresh-allocation slice [start:end]. + + `tensor[start:end]` on a contiguous storage returns a VIEW that shares the + same underlying allocation. `.contiguous()` is a no-op on an already- + contiguous view, so wrapping the view in a new Parameter does NOT release + the original full-size storage — refcount on the storage stays >= 1 from + the new view. + + `.clone()` forces a fresh allocation. After we drop the old Parameter, + the original storage's refcount drops to zero and PyTorch's caching + allocator reclaims the memory on the next `empty_cache()`. + """ + old = getattr(module, attr_name) + new_data = old.data[start:end].detach().clone().contiguous() + requires_grad = old.requires_grad + # Drop the old Parameter from the module's _parameters registry first so + # nothing is holding a reference when we assign the new one. + if attr_name in module._parameters: + del module._parameters[attr_name] + setattr( + module, + attr_name, + torch.nn.Parameter(new_data, requires_grad=requires_grad), + ) + # `old` goes out of scope on return. + + +def _detect_experts_modules(model): + """Yield (name, module) pairs for every module that looks like an Experts class. + + Detection: 3D `gate_up_proj` and `down_proj` parameters with experts on dim 0. + This is the canonical layout enforced by `@use_experts_implementation`. + Mixtral's `ModuleList[MixtralBlockSparseTop2MLP]` does NOT match — out of scope + for v1. + """ + for name, module in model.named_modules(): + gp = getattr(module, "gate_up_proj", None) + dp = getattr(module, "down_proj", None) + if gp is None or dp is None: + continue + if not ( + isinstance(gp, torch.nn.Parameter) and isinstance(dp, torch.nn.Parameter) + ): + continue + if gp.dim() != 3 or dp.dim() != 3: + continue + yield name, module + + +def shard_expert_weights(model, ep_group) -> int: + """Slice expert weights along dim 0 per the EP rank. + + Args: + model: A built (but not yet FSDP-wrapped) HuggingFace model. + ep_group: `torch.distributed.ProcessGroup` for EP, or `None` for + single-rank (no-op). + + Returns: + Number of Experts modules sharded (0 if EP disabled or none found). + + Raises: + ValueError: if any Experts module's `num_experts` is not divisible by + the EP world size. + + DDP composition: the sharded params hold DIFFERENT content per rank, so we + add their fully-qualified names to `model._ddp_params_and_buffers_to_ignore` + to prevent the startup broadcast from copying rank 0's slice everywhere. + FSDP composition is handled in `ExpertParallelPlugin.fully_shard_experts`. + """ + if ep_group is None: + return 0 + + ep_size = dist.get_world_size(ep_group) + if ep_size <= 1: + return 0 + + ep_rank = dist.get_rank(ep_group) + sharded = 0 + ignore_names: list[str] = [] + + for name, module in _detect_experts_modules(model): + gp = module.gate_up_proj + E = gp.shape[0] + if E % ep_size != 0: + raise ValueError( + f"Expert module {name!r}: num_experts={E} not divisible by " + f"ep_size={ep_size}. Adjust the model config or ep_size." + ) + E_local = E // ep_size + start = ep_rank * E_local + end = start + E_local + + with torch.no_grad(): + _replace_with_slice(module, "gate_up_proj", start, end) + _replace_with_slice(module, "down_proj", start, end) + for bias_name in ("gate_up_proj_bias", "down_proj_bias"): + bias = getattr(module, bias_name, None) + if ( + isinstance(bias, torch.nn.Parameter) + and bias.dim() >= 1 + and bias.shape[0] == E + ): + _replace_with_slice(module, bias_name, start, end) + + # Stash metadata the registered fn needs. + module.local_expert_offset = start + module.num_local_experts = E_local + module.num_experts_global = E + # Override the kernel's view of num_experts for grouped_mm/scattermoe bucketing. + module.num_experts = E_local + + # Mark sharded params as DDP-ignored — they hold rank-specific content + # and must NOT be broadcast from rank 0 at DDP construction. + ignore_names.append(f"{name}.gate_up_proj") + ignore_names.append(f"{name}.down_proj") + for bias_name in ("gate_up_proj_bias", "down_proj_bias"): + if isinstance(getattr(module, bias_name, None), torch.nn.Parameter): + ignore_names.append(f"{name}.{bias_name}") + + sharded += 1 + + if sharded: + # Drop refs to any leftover full-size storage and return cached memory + # to the allocator. Without this, the original `from_pretrained` + # allocations stay reserved and `memory_allocated()` doesn't drop. + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + # Append (don't overwrite) — other systems may have set this too. + existing = list(getattr(model, "_ddp_params_and_buffers_to_ignore", [])) + model._ddp_params_and_buffers_to_ignore = existing + ignore_names + LOG.info( + f"Sharded {sharded} Experts module(s) along the experts dim " + f"(ep_rank={ep_rank}, ep_size={ep_size}). " + f"Marked {len(ignore_names)} param(s) as DDP-ignored." + ) + return sharded diff --git a/src/axolotl/integrations/kernels/plugin.py b/src/axolotl/integrations/kernels/plugin.py index e9291c9c15..d713095a5f 100644 --- a/src/axolotl/integrations/kernels/plugin.py +++ b/src/axolotl/integrations/kernels/plugin.py @@ -76,6 +76,15 @@ def pre_model_load(self, cfg): ): moe_model_type = cfg.model_config_type + # When expert parallelism is enabled, the EP plugin sets + # `experts_implementation` to `deep_ep_scattermoe` / `deep_ep_sonicmoe` + # and dispatches the kernel inside the experts-level forward (after + # DeepEP all-to-all). Skip the SparseMoeBlock-level patch in that case + # — patching the block-level forward bypasses EP routing and reads + # FSDP-sharded expert weights as DTensors, which the kernels do not + # accept. + ep_active = (getattr(cfg, "expert_parallel_size", 1) or 1) > 1 + if cfg.use_scattermoe: self._register_kernels() if is_experts_only_model(moe_model_type): @@ -83,7 +92,14 @@ def pre_model_load(self, cfg): # — register ScatterMoE in the ExpertsInterface so that # @use_experts_implementation dispatches to it. self._register_experts_interface() - cfg.experts_implementation = "scattermoe" + if not ep_active: + cfg.experts_implementation = "scattermoe" + elif ep_active: + LOG.info( + "expert_parallel_size > 1: skipping SparseMoeBlock-level " + "ScatterMoE patch; the deep_ep_scattermoe registered " + "function handles the kernel under EP." + ) else: self._kernelize_model(moe_model_type) elif cfg.use_sonicmoe: @@ -104,6 +120,24 @@ def pre_model_load(self, cfg): f"Applying SonicMoE experts-level patch for model type: {moe_model_type}" ) patch_gemma4_sonicmoe() + # TODO(EP+SonicMoE): grad norms explode during training. Re-enable + # once the root cause is identified. Same shape as the ScatterMoE + # branch above, but SonicMoE additionally needs the gate_up_proj + # interleave converter since its w1 layout is [g0, u0, g1, u1, ...] + # while the checkpoint stores it concatenated [gate..., up...]. + # + # elif ep_active: + # from axolotl.integrations.kernels.libs.sonicmoe.weight_converter import ( + # register_sonicmoe_weight_converter, + # ) + # + # LOG.info( + # "expert_parallel_size > 1: skipping SparseMoeBlock-level " + # "SonicMoE patch; the deep_ep_sonicmoe registered function " + # "handles the kernel under EP. Registering gate_up_proj " + # "interleave converter." + # ) + # register_sonicmoe_weight_converter(moe_model_type) else: from axolotl.integrations.kernels.libs.sonicmoe import patch_sonicmoe diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index 20c863ee5e..fc34636969 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -413,6 +413,17 @@ def fsdp2_prepare_model(accelerator, model: torch.nn.Module) -> torch.nn.Module: if any(isinstance(m, ParamWrapper) for m in model.modules()): patch_peft_param_wrapper_for_fsdp2() + # EP+FSDP: pre-wrap experts on `dp_shard` before the outer auto-wrap so + # the walker skips them. See `expert_parallel/README.md`. + if ( + mesh is not None + and "ep" in getattr(mesh, "mesh_dim_names", ()) + and "dp_shard" in mesh.mesh_dim_names + ): + from axolotl.integrations.expert_parallel.plugin import ExpertParallelPlugin + + ExpertParallelPlugin.fully_shard_experts(model, mesh["dp_shard"], fsdp2_kwargs) + auto_wrap_policy = fsdp2_prepare_auto_wrap_policy(fsdp2_plugin, model) log_bias_dtype_mismatch = False if auto_wrap_policy is not None: diff --git a/src/axolotl/monkeypatch/accelerate/parallelism_config.py b/src/axolotl/monkeypatch/accelerate/parallelism_config.py index 56636d6979..58709367ca 100644 --- a/src/axolotl/monkeypatch/accelerate/parallelism_config.py +++ b/src/axolotl/monkeypatch/accelerate/parallelism_config.py @@ -1,5 +1,12 @@ -""" -workaround to allow parallelism config for pure CP +"""ParallelismConfig monkeypatch. + +Two extensions: +- Allow pure CP standalone via `ACCELERATE_ALLOW_CP_STANDALONE`. +- Add Expert Parallel (`ep`) as a first-class mesh axis inside the + data-parallel group. Mesh order is `(ep, dp_replicate, dp_shard, cp, sp, tp)` + so the dp axes stay contiguous (required for `_flatten("dp")`). + +See `expert_parallel/README.md` for the full integration story. """ import os @@ -8,6 +15,83 @@ from accelerate import DistributedType +def _patched_post_init(self): + _ORIG_POST_INIT(self) + + if not hasattr(self, "ep_size") or self.ep_size is None: + self.ep_size = int(os.environ.get("PARALLELISM_CONFIG_EP_SIZE", "1") or 1) + if self.ep_size < 1: + raise ValueError(f"ep_size must be at least 1, got {self.ep_size}") + + # Register so `_set_size`, `_validate_accelerator`, `_get_mesh` see it. + self._sizes["ep"] = self.ep_size + + +def _patched_total_size(self): + return ( + self.dp_replicate_size + * self.dp_shard_size + * self.tp_size + * self.cp_size + * self.sp_size + * getattr(self, "ep_size", 1) + ) + + +def _patched_ep_enabled(self): + return getattr(self, "ep_size", 1) > 1 + + +def _patched_dp_dim_names(self): + """DP axes (different ranks see different data). EP is included — each + EP rank pulls its own batch.""" + dims = [] + if self.ep_enabled: + dims += ["ep"] + if self.dp_replicate_enabled: + dims += ["dp_replicate"] + if self.dp_shard_enabled: + dims += ["dp_shard"] + return dims + + +def _patched_dp_shard_cp_dim_names(self): + """Axes the outer FSDP wrap shards along (flattened into `dp_shard_cp`). + Including `ep` makes non-expert grads reduce-scatter across the full + world; experts are pre-wrapped on `mesh["dp_shard"]` only and skipped + by the auto-wrap walker.""" + dims = [] + if self.ep_enabled: + dims += ["ep"] + if self.dp_shard_enabled: + dims += ["dp_shard"] + if self.cp_enabled: + dims += ["cp"] + return dims + + +def _patched_non_dp_dim_names(self): + """Non-DP axes (TP/CP/SP). EP moved into `dp_dim_names`.""" + dims = [] + if self.tp_enabled: + dims += ["tp"] + if self.cp_enabled: + dims += ["cp"] + if self.sp_enabled: + dims += ["sp"] + return dims + + +def _patched_get_mesh(self): + """Build (dim_names, shape) for `init_device_mesh`. Order keeps the dp + block (ep, dp_replicate, dp_shard) contiguous so `_flatten("dp")` works. + """ + mesh_dims = {p: self._sizes[p] for p in self.active_mesh_dims} + mesh_order = ["ep", "dp_replicate", "dp_shard", "cp", "sp", "tp"] + sorted_items = sorted(mesh_dims.items(), key=lambda x: mesh_order.index(x[0])) + return tuple(zip(*sorted_items, strict=True)) + + def _validate_accelerator(self, accelerator): _warnings = set() if not accelerator.multi_device and self.total_size == 1: @@ -22,7 +106,7 @@ def _validate_accelerator(self, accelerator): raise ValueError( f"ParallelismConfig total_size ({self.total_size}) does not match " f"num_processes ({accelerator.num_processes}). Please adjust dp_replicate_size/ " - f"dp_shard_size/tp_size/cp_size." + f"dp_shard_size/tp_size/cp_size/ep_size." ) # allow parallelism config when not using fsdp if using pure context parallelism @@ -35,6 +119,16 @@ def _validate_accelerator(self, accelerator): ): allow_parallelism_config = True + # Pure EP (no FSDP/TP/CP) is valid: the plugin handles dispatch/combine + # and DDP's _ddp_params_and_buffers_to_ignore keeps experts out of DDP. + if ( + getattr(self, "ep_enabled", False) + and self.dp_shard_size <= 1 + and self.tp_size <= 1 + and self.cp_size <= 1 + ): + allow_parallelism_config = True + if ( self.total_size > 1 and not allow_parallelism_config @@ -70,11 +164,108 @@ def patched_is_fsdp2(self) -> bool: ) +# Captured in `patch_parallelism_config()` so we can chain the original +# __post_init__ before adding ep. +_ORIG_POST_INIT = None + + def patch_parallelism_config(): + global _ORIG_POST_INIT from accelerate.accelerator import AcceleratorState, ParallelismConfig + if _ORIG_POST_INIT is None: + _ORIG_POST_INIT = ParallelismConfig.__post_init__ + + ParallelismConfig.__post_init__ = _patched_post_init + # `total_size` is a property on the dataclass; replace it. + ParallelismConfig.total_size = property(_patched_total_size) + ParallelismConfig.ep_enabled = property(_patched_ep_enabled) + ParallelismConfig.dp_dim_names = property(_patched_dp_dim_names) + ParallelismConfig.dp_shard_cp_dim_names = property(_patched_dp_shard_cp_dim_names) + ParallelismConfig.non_dp_dim_names = property(_patched_non_dp_dim_names) + ParallelismConfig._get_mesh = _patched_get_mesh ParallelismConfig._validate_accelerator = _validate_accelerator AcceleratorState.is_fsdp2 = property(patched_is_fsdp2) + patch_prepare_data_loader_for_ep() + patch_clip_grad_norm_for_ep() + + +def _ep_aware_clip_grad_norm(parameters, max_norm, norm_type=2.0): + """`clip_grad_norm_` for params sharded across different DeviceMeshes. + + Stock `torch.nn.utils.clip_grad_norm_` stacks per-param norms, which + DTensor rejects across meshes (experts on `dp_shard` vs non-experts on + `dp_shard_cp`). Instead, compute the local p-norm contribution per rank, + all-reduce the sum across the world, take the p-th root, and apply the + clip coefficient. Supports any finite p ≥ 1 plus `inf`. + """ + import math + + import torch + import torch.distributed as dist + from torch.distributed.tensor import DTensor + + if isinstance(parameters, torch.Tensor): + parameters = [parameters] + grads = [p.grad for p in parameters if p.grad is not None] + if not grads: + return torch.tensor(0.0) + + norm_type = float(norm_type) + device = grads[0].device + if isinstance(grads[0], DTensor): + device = grads[0].to_local().device + + is_inf = math.isinf(norm_type) + local_acc = torch.zeros((), device=device, dtype=torch.float32) + for g in grads: + local = g.to_local() if isinstance(g, DTensor) else g + local_f32 = local.detach().to(torch.float32) + if is_inf: + local_acc = torch.maximum(local_acc, local_f32.abs().max()) + else: + local_acc = local_acc + local_f32.abs().pow(norm_type).sum() + + if dist.is_available() and dist.is_initialized(): + op = dist.ReduceOp.MAX if is_inf else dist.ReduceOp.SUM + dist.all_reduce(local_acc, op=op) + + total_norm = local_acc if is_inf else local_acc.pow(1.0 / norm_type) + clip_coef = (float(max_norm) / (total_norm + 1e-6)).clamp(max=1.0) + for g in grads: + local = g.to_local() if isinstance(g, DTensor) else g + local.detach().mul_(clip_coef.to(local.dtype)) + + return total_norm.to( + grads[0].dtype if grads[0].is_floating_point() else torch.float32 + ) + + +def patch_clip_grad_norm_for_ep(): + """Replace `Accelerator.clip_grad_norm_` with the EP-aware version when + the active parallelism includes both `ep` and `dp_shard` (i.e., the + FSDP+EP composition produces multi-mesh DTensor grads). + """ + from accelerate import Accelerator + + if getattr(Accelerator, "_AXOLOTL_EP_CLIP_PATCHED", False): + return + orig = Accelerator.clip_grad_norm_ + + def patched_clip_grad_norm_(self, parameters, max_norm, norm_type=2): + pc = getattr(self, "parallelism_config", None) + if ( + pc is not None + and getattr(pc, "ep_enabled", False) + and getattr(pc, "dp_shard_enabled", False) + ): + self.unscale_gradients() + params = list(parameters) + return _ep_aware_clip_grad_norm(params, max_norm, norm_type=norm_type) + return orig(self, parameters, max_norm, norm_type=norm_type) + + Accelerator.clip_grad_norm_ = patched_clip_grad_norm_ + Accelerator._AXOLOTL_EP_CLIP_PATCHED = True def patch_prepare_cp(): @@ -104,3 +295,88 @@ def _noop_prepare_context_parallel_inputs(self, model, inputs): # remove unneeded calculation upstream Trainer._prepare_context_parallel_inputs = _noop_prepare_context_parallel_inputs + + +def _patched_prepare_data_loader_factory(orig_fn): + """Wrap `accelerate.data_loader.prepare_data_loader` to count the EP axis + as a data-parallel dimension. + + Stock accelerate (line ~1155 in 1.13.0) computes + num_processes = dp_shard * dp_replicate + process_index = process_index // (tp * cp) + which ignores EP. EP ranks see DIFFERENT data (each rank pulls its own + batch), so EP belongs in the data-parallel size — same way `dp_replicate` + does. + """ + import torch + + def patched(*args, **kwargs): + torch_device_mesh = kwargs.get("torch_device_mesh", None) + if ( + torch_device_mesh is not None + and isinstance(torch_device_mesh, torch.distributed.device_mesh.DeviceMesh) + and "ep" in torch_device_mesh.mesh_dim_names + ): + from accelerate.state import PartialState + from accelerate.utils import DistributedType + + state = PartialState() + if state.distributed_type != DistributedType.DEEPSPEED: + ep_size = torch_device_mesh["ep"].size() + tp_size = ( + torch_device_mesh["tp"].size() + if "tp" in torch_device_mesh.mesh_dim_names + else 1 + ) + cp_size = ( + torch_device_mesh["cp"].size() + if "cp" in torch_device_mesh.mesh_dim_names + else 1 + ) + fsdp_size = ( + torch_device_mesh["dp_shard"].size() + if "dp_shard" in torch_device_mesh.mesh_dim_names + else 1 + ) + dp_size = ( + torch_device_mesh["dp_replicate"].size() + if "dp_replicate" in torch_device_mesh.mesh_dim_names + else 1 + ) + num_processes = fsdp_size * dp_size * ep_size + process_index = state.process_index // (tp_size * cp_size) + kwargs["num_processes"] = num_processes + kwargs["process_index"] = process_index + # Once we've supplied num_processes/process_index explicitly, + # accelerate's internal mesh path (which would re-derive without + # ep) is bypassed. + kwargs["torch_device_mesh"] = None + return orig_fn(*args, **kwargs) + + return patched + + +def patch_prepare_data_loader_for_ep(): + """Apply the EP-aware data-loader patch. + + Idempotent: replacing the bound function more than once is harmless because + the wrapper closes over the *current* `prepare_data_loader`. + """ + import accelerate as _accel + from accelerate import data_loader as _dl + + if getattr(_dl, "_AXOLOTL_EP_PATCHED", False): + return + orig = _dl.prepare_data_loader + wrapped = _patched_prepare_data_loader_factory(orig) + _dl.prepare_data_loader = wrapped + # accelerate.Accelerator imports prepare_data_loader at module load, so + # we have to patch the binding it captured too. + if hasattr(_accel, "prepare_data_loader"): + _accel.prepare_data_loader = wrapped + # Likewise the Accelerator module's local reference. + from accelerate import accelerator as _acc_mod + + if hasattr(_acc_mod, "prepare_data_loader"): + _acc_mod.prepare_data_loader = wrapped + _dl._AXOLOTL_EP_PATCHED = True diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 2862f8d79b..82dab72d88 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -112,13 +112,12 @@ def resolve_dtype(cfg): def normalize_config(cfg): # setup some derived config / hyperparams if not cfg.use_ray: - # Ray worker's re-validate forbids both being set, so defer this one. cfg.gradient_accumulation_steps = cfg.gradient_accumulation_steps or ( cfg.batch_size // cfg.micro_batch_size ) - cfg.batch_size = ( - cfg.batch_size or cfg.micro_batch_size * cfg.gradient_accumulation_steps - ) + cfg.batch_size = ( + cfg.batch_size or cfg.micro_batch_size * cfg.gradient_accumulation_steps + ) if cfg.eval_batch_size is None: cfg.eval_batch_size = cfg.micro_batch_size cfg.world_size = int(os.environ.get("WORLD_SIZE", 1)) diff --git a/src/axolotl/utils/distributed.py b/src/axolotl/utils/distributed.py index 840772d91c..200c802a69 100644 --- a/src/axolotl/utils/distributed.py +++ b/src/axolotl/utils/distributed.py @@ -304,6 +304,7 @@ def build_parallelism_config(cfg): cfg.dp_shard_size, cfg.dp_replicate_size, bool(cfg.fsdp or cfg.fsdp_config), + getattr(cfg, "expert_parallel_size", None), ) if pc_kwargs: @@ -323,10 +324,21 @@ def _get_parallel_config_kwargs( dp_shard_size: int | None = None, dp_replicate_size: int | None = None, is_fsdp: bool = False, + expert_parallel_size: int | None = None, ): pc_kwargs = {} remaining_world_size = world_size + # EP consumes part of world_size; subtract it up front so the auto-fill + # below doesn't put EP ranks into `dp_replicate_size`. + if expert_parallel_size and expert_parallel_size > 1: + if remaining_world_size % expert_parallel_size != 0: + raise ValueError( + f"expert_parallel_size ({expert_parallel_size}) must divide " + f"world_size ({world_size})." + ) + remaining_world_size = remaining_world_size // expert_parallel_size + if tensor_parallel_size and tensor_parallel_size > 1: pc_kwargs["tp_size"] = tensor_parallel_size remaining_world_size = remaining_world_size // tensor_parallel_size diff --git a/src/axolotl/utils/trainer.py b/src/axolotl/utils/trainer.py index 45f2b7afea..d9cf3e8369 100644 --- a/src/axolotl/utils/trainer.py +++ b/src/axolotl/utils/trainer.py @@ -636,6 +636,17 @@ def setup_parallelism_envs(cfg): from axolotl.monkeypatch.accelerate.parallelism_config import patch_prepare_cp patch_prepare_cp() + # Expert Parallel patch must apply before the first `Accelerator()` + # call so `ep_size` lands in the mesh. + if cfg.expert_parallel_size and cfg.expert_parallel_size > 1: + os.environ["PARALLELISM_CONFIG_EP_SIZE"] = str(cfg.expert_parallel_size) + if cfg.dp_shard_size and cfg.dp_shard_size > 1: + set_accelerate_parallelism_config = True + from axolotl.monkeypatch.accelerate.parallelism_config import ( + patch_parallelism_config, + ) + + patch_parallelism_config() if set_accelerate_parallelism_config: os.environ["ACCELERATE_USE_PARALLELISM_CONFIG"] = "true" diff --git a/tests/cli/test_load_cfg_capabilities.py b/tests/cli/test_load_cfg_capabilities.py index 2f01d09312..ae85f78a6d 100644 --- a/tests/cli/test_load_cfg_capabilities.py +++ b/tests/cli/test_load_cfg_capabilities.py @@ -92,6 +92,7 @@ def test_ray_train_func_validates_with_worker_capabilities(monkeypatch): the result into `validate_config` before training runs.""" cfg_dict = { "base_model": "HuggingFaceTB/SmolLM2-135M", + "micro_batch_size": 1, "gradient_accumulation_steps": 1, } @@ -134,6 +135,7 @@ def test_ray_train_func_registers_plugins_before_validate_config(monkeypatch): """ cfg_dict = { "base_model": "HuggingFaceTB/SmolLM2-135M", + "micro_batch_size": 1, "gradient_accumulation_steps": 1, "plugins": ["axolotl.integrations.liger.LigerPlugin"], } @@ -178,6 +180,7 @@ def test_ray_train_func_skips_plugin_registration_when_no_plugins(monkeypatch): `plugin_set_cfg` should be invoked on the worker.""" cfg_dict = { "base_model": "HuggingFaceTB/SmolLM2-135M", + "micro_batch_size": 1, "gradient_accumulation_steps": 1, } diff --git a/tests/integrations/test_expert_parallel.py b/tests/integrations/test_expert_parallel.py new file mode 100644 index 0000000000..8a5990d528 --- /dev/null +++ b/tests/integrations/test_expert_parallel.py @@ -0,0 +1,435 @@ +"""Tests for the Expert-Parallel (DeepEP) integration.""" + +import os +from importlib.util import find_spec + +import pytest +import torch +import torch.distributed as dist +import torch.multiprocessing as mp + +from axolotl.integrations.expert_parallel import ( + ExpertParallelArgs, + ExpertParallelPlugin, +) +from axolotl.integrations.expert_parallel.experts_fn import ( + REGISTRY, + kernel_to_registered_name, + register_all, +) +from axolotl.integrations.expert_parallel.shard import ( + _detect_experts_modules, + shard_expert_weights, +) + + +def _build_qwen3moe_block(num_experts: int = 16, top_k: int = 4): + from transformers.models.qwen3_moe.configuration_qwen3_moe import Qwen3MoeConfig + from transformers.models.qwen3_moe.modeling_qwen3_moe import Qwen3MoeSparseMoeBlock + + cfg = Qwen3MoeConfig( + hidden_size=512, + moe_intermediate_size=1024, + num_experts=num_experts, + num_experts_per_tok=top_k, + ) + return Qwen3MoeSparseMoeBlock(cfg) + + +# --------------------------------------------------------------------------- # +# Args validation +# --------------------------------------------------------------------------- # + + +class TestExpertParallelArgs: + def test_defaults(self): + a = ExpertParallelArgs() + assert a.expert_parallel_size == 1 + assert a.expert_parallel_backend == "deep_ep" + assert a.expert_parallel_fallback_on_unsupported is True + + def test_enabled(self): + a = ExpertParallelArgs(expert_parallel_size=2) + assert a.expert_parallel_size == 2 + + def test_invalid_size(self): + with pytest.raises(ValueError): + ExpertParallelArgs(expert_parallel_size=0) + + +class TestKernelInference: + """Plugin auto-composes with user's chosen local kernel.""" + + def _infer(self, **cfg_kwargs): + from types import SimpleNamespace + + return ExpertParallelPlugin._infer_local_kernel(SimpleNamespace(**cfg_kwargs)) + + def test_use_scattermoe_picks_scattermoe(self): + assert self._infer(use_scattermoe=True) == "scattermoe" + + def test_experts_implementation_scattermoe_alone_does_NOT_pick_scattermoe(self): + # use_scattermoe is the source of truth; bare experts_implementation=scattermoe + # without the master flag falls through to the default kernel. + assert self._infer(experts_implementation="scattermoe") == "grouped_mm" + + def test_use_scattermoe_overrides_experts_implementation_eager(self): + # If kernels validator hasn't run yet, use_scattermoe still wins. + assert ( + self._infer(use_scattermoe=True, experts_implementation="eager") + == "scattermoe" + ) + + def test_grouped_mm_picks_grouped_mm(self): + assert self._infer(experts_implementation="grouped_mm") == "grouped_mm" + + def test_batched_mm_picks_grouped_mm(self): + assert self._infer(experts_implementation="batched_mm") == "grouped_mm" + + def test_eager_picks_eager(self): + assert self._infer(experts_implementation="eager") == "eager" + + def test_default_picks_grouped_mm(self): + assert self._infer() == "grouped_mm" + + def test_use_sonicmoe_picks_sonicmoe(self): + assert self._infer(use_sonicmoe=True) == "sonicmoe" + + +# --------------------------------------------------------------------------- # +# Registration +# --------------------------------------------------------------------------- # + + +class TestRegistration: + def test_kernel_name_mapping(self): + assert kernel_to_registered_name("eager") == "deep_ep" + assert kernel_to_registered_name("grouped_mm") == "deep_ep_grouped_mm" + assert kernel_to_registered_name("scattermoe") == "deep_ep_scattermoe" + assert kernel_to_registered_name("sonicmoe") == "deep_ep_sonicmoe" + + def test_register_all_idempotent(self): + from transformers.integrations.moe import ALL_EXPERTS_FUNCTIONS + + register_all() + register_all() # should not error + for name in REGISTRY: + fn = ALL_EXPERTS_FUNCTIONS.get_interface(name, None) + assert fn is not None, f"{name} not registered" + + def test_whitelist_patch_accepts_deep_ep_names(self): + from transformers.modeling_utils import PreTrainedModel + + register_all() + m = PreTrainedModel.__new__(PreTrainedModel) + + class _Cfg: + _experts_implementation = "deep_ep_grouped_mm" + + m.config = _Cfg() + for name in REGISTRY: + assert PreTrainedModel.get_correct_experts_implementation(m, name) == name + + def test_whitelist_patch_rejects_garbage(self): + from transformers.modeling_utils import PreTrainedModel + + register_all() + m = PreTrainedModel.__new__(PreTrainedModel) + m.config = type("_C", (), {"_experts_implementation": "garbage"})() + with pytest.raises(ValueError): + PreTrainedModel.get_correct_experts_implementation(m, "garbage") + + +# --------------------------------------------------------------------------- # +# Module detection +# --------------------------------------------------------------------------- # + + +class TestExpertModuleDetection: + def test_detects_qwen3moe_experts(self): + block = _build_qwen3moe_block() + found = list(_detect_experts_modules(block)) + assert len(found) == 1 + name, module = found[0] + assert module is block.experts + assert module.gate_up_proj.dim() == 3 + assert module.down_proj.dim() == 3 + + def test_skips_non_3d_modules(self): + # A regular linear layer should not be detected as Experts + m = torch.nn.Linear(8, 8) + m.gate_up_proj = m.weight # 2D, doesn't match + m.down_proj = m.weight + found = list(_detect_experts_modules(m)) + assert len(found) == 0 + + +# --------------------------------------------------------------------------- # +# Sharding (single-rank == no-op) +# --------------------------------------------------------------------------- # + + +class TestShardingSingleRank: + """At world_size=1, sharding is a no-op.""" + + def setup_method(self): + if not dist.is_initialized(): + os.environ.setdefault("MASTER_ADDR", "127.0.0.1") + os.environ.setdefault("MASTER_PORT", "29555") + os.environ.setdefault("RANK", "0") + os.environ.setdefault("WORLD_SIZE", "1") + dist.init_process_group(backend="gloo", rank=0, world_size=1) + + def test_no_op_at_world_size_1(self): + block = _build_qwen3moe_block(num_experts=16) + original_shape = tuple(block.experts.gate_up_proj.shape) + n = shard_expert_weights(block, dist.group.WORLD) + assert n == 0 + assert tuple(block.experts.gate_up_proj.shape) == original_shape + + def test_none_group_no_op(self): + block = _build_qwen3moe_block(num_experts=16) + n = shard_expert_weights(block, None) + assert n == 0 + + +# --------------------------------------------------------------------------- # +# Plugin lifecycle (no DeepEP needed for these) +# --------------------------------------------------------------------------- # + + +class TestPluginLifecycle: + def test_get_input_args(self): + plugin = ExpertParallelPlugin() + assert ( + plugin.get_input_args() + == "axolotl.integrations.expert_parallel.ExpertParallelArgs" + ) + + def test_pre_model_load_disabled_is_noop(self): + plugin = ExpertParallelPlugin() + + class _Cfg: + expert_parallel_enabled = False + + plugin.pre_model_load(_Cfg()) # should not raise / not register anything new + + def _ep_cfg(self, **kw): + from types import SimpleNamespace + + defaults = dict( + expert_parallel_size=2, + expert_parallel_fallback_on_unsupported=True, + experts_implementation=None, + ) + defaults.update(kw) + return SimpleNamespace(**defaults) + + @pytest.mark.skipif(find_spec("deep_ep") is None, reason="deep_ep not installed") + def test_pre_model_load_default_picks_grouped_mm(self): + cfg = self._ep_cfg() + ExpertParallelPlugin().pre_model_load(cfg) + assert cfg.experts_implementation == "deep_ep_grouped_mm" + + @pytest.mark.skipif(find_spec("deep_ep") is None, reason="deep_ep not installed") + def test_pre_model_load_use_scattermoe_auto_composes(self): + cfg = self._ep_cfg(use_scattermoe=True, experts_implementation="scattermoe") + ExpertParallelPlugin().pre_model_load(cfg) + assert cfg.experts_implementation == "deep_ep_scattermoe" + + @pytest.mark.skipif(find_spec("deep_ep") is None, reason="deep_ep not installed") + def test_pre_model_load_overrides_existing_eager(self): + cfg = self._ep_cfg(experts_implementation="eager") + ExpertParallelPlugin().pre_model_load(cfg) + assert cfg.experts_implementation == "deep_ep" + + def test_disabled_by_default(self): + """expert_parallel_size=1 (default) means EP is off — pre_model_load no-ops.""" + cfg = self._ep_cfg(expert_parallel_size=1) + ExpertParallelPlugin().pre_model_load(cfg) + assert cfg.experts_implementation is None + + def test_pre_model_load_enabled_no_deep_ep_falls_back(self, monkeypatch): + import axolotl.integrations.expert_parallel.plugin as plugin_mod + + monkeypatch.setattr(plugin_mod, "find_spec", lambda name: None) + cfg = self._ep_cfg() + ExpertParallelPlugin().pre_model_load(cfg) + assert cfg.experts_implementation is None + + def test_pre_model_load_no_fallback_raises(self, monkeypatch): + import axolotl.integrations.expert_parallel.plugin as plugin_mod + + monkeypatch.setattr(plugin_mod, "find_spec", lambda name: None) + cfg = self._ep_cfg(expert_parallel_fallback_on_unsupported=False) + with pytest.raises(ImportError): + ExpertParallelPlugin().pre_model_load(cfg) + + +# --------------------------------------------------------------------------- # +# Mesh-axis topology — rank assignments for EP+FSDP composition. +# +# The 4-rank case (ep=2 × dp_shard=2) is what we'll actually run on a 4× A100 +# box. Tested here in single-process by spawning gloo workers; no GPUs needed. +# --------------------------------------------------------------------------- # + + +def _ep_topology_worker(rank, world_size, ep_size, dp_shard_size, port, q): + os.environ["MASTER_ADDR"] = "127.0.0.1" + os.environ["MASTER_PORT"] = str(port) + os.environ["RANK"] = str(rank) + os.environ["WORLD_SIZE"] = str(world_size) + dist.init_process_group(backend="gloo", rank=rank, world_size=world_size) + + try: + from types import SimpleNamespace + + cfg = SimpleNamespace( + expert_parallel_size=ep_size, + dp_shard_size=dp_shard_size, + tensor_parallel_size=1, + context_parallel_size=1, + ) + ep_group = ExpertParallelPlugin._resolve_ep_group(cfg) + ep_ranks = sorted(dist.get_process_group_ranks(ep_group)) + # Also peek at the dp_shard slice if a 2D mesh was built. + mesh = ExpertParallelPlugin._device_mesh + dp_ranks = ( + sorted(dist.get_process_group_ranks(mesh["dp_shard"].get_group())) + if mesh is not None and "dp_shard" in mesh.mesh_dim_names + else None + ) + q.put((rank, ep_ranks, dp_ranks)) + finally: + dist.destroy_process_group() + ExpertParallelPlugin._device_mesh = None + + +def _ep_topology_worker_expects_error( + rank, world_size, ep_size, dp_shard_size, port, q +): + """Variant that captures any ValueError from `_resolve_ep_group`. + + Module-level so `mp.get_context("spawn")` can pickle it. + """ + os.environ["MASTER_ADDR"] = "127.0.0.1" + os.environ["MASTER_PORT"] = str(port) + os.environ["RANK"] = str(rank) + os.environ["WORLD_SIZE"] = str(world_size) + dist.init_process_group(backend="gloo", rank=rank, world_size=world_size) + try: + from types import SimpleNamespace + + cfg = SimpleNamespace( + expert_parallel_size=ep_size, + dp_shard_size=dp_shard_size, + tensor_parallel_size=1, + context_parallel_size=1, + ) + err = None + try: + ExpertParallelPlugin._resolve_ep_group(cfg) + except ValueError as e: + err = str(e) + q.put((rank, err)) + finally: + dist.destroy_process_group() + ExpertParallelPlugin._device_mesh = None + + +def _spawn_topology_check(world_size, ep_size, dp_shard_size, port_base): + ctx = mp.get_context("spawn") + q = ctx.Queue() + procs = [ + ctx.Process( + target=_ep_topology_worker, + args=(r, world_size, ep_size, dp_shard_size, port_base, q), + ) + for r in range(world_size) + ] + for p in procs: + p.start() + results = [q.get(timeout=120) for _ in range(world_size)] + for p in procs: + p.join(timeout=20) + assert p.exitcode == 0, f"worker exited with {p.exitcode}" + return sorted(results, key=lambda r: r[0]) + + +class TestMeshTopology: + """The 4-rank EP+FSDP composition rank assignments.""" + + def test_world4_ep2_dp2_orthogonal(self): + """At world=4 with ep=2 and dp_shard=2, EP groups must be strided + ({0,2}, {1,3}) and dp_shard groups contiguous ({0,1}, {2,3}). + """ + # Use large random-ish port base to avoid collision with anything else. + results = _spawn_topology_check( + world_size=4, ep_size=2, dp_shard_size=2, port_base=37610 + ) + # Build per-rank groupings from results. + ep_groups_by_rank = {r: tuple(eps) for r, eps, _ in results} + dp_groups_by_rank = {r: tuple(dps) for r, _, dps in results} + + # EP groups (strided): {0,2} and {1,3} + assert ep_groups_by_rank[0] == (0, 2), ep_groups_by_rank + assert ep_groups_by_rank[1] == (1, 3), ep_groups_by_rank + assert ep_groups_by_rank[2] == (0, 2), ep_groups_by_rank + assert ep_groups_by_rank[3] == (1, 3), ep_groups_by_rank + + # dp_shard groups (contiguous, matches accelerate): {0,1} and {2,3} + assert dp_groups_by_rank[0] == (0, 1), dp_groups_by_rank + assert dp_groups_by_rank[1] == (0, 1), dp_groups_by_rank + assert dp_groups_by_rank[2] == (2, 3), dp_groups_by_rank + assert dp_groups_by_rank[3] == (2, 3), dp_groups_by_rank + + def test_world4_ep4_dp1_uses_world(self): + """ep_size == world_size short-circuits to dist.group.WORLD.""" + results = _spawn_topology_check( + world_size=4, ep_size=4, dp_shard_size=1, port_base=37710 + ) + for rank, ep_ranks, dp_ranks in results: + assert ep_ranks == [0, 1, 2, 3], (rank, ep_ranks) + assert dp_ranks is None # no 2D mesh built + + def test_world4_ep2_dp1_invalid_product_raises(self): + """ep Date: Fri, 22 May 2026 13:51:02 +0530 Subject: [PATCH 1311/1405] cp fix for mamba models (#3572) [skip ci] * cp fix for nemo * nemo and flcon patch * import patch * Revert "import patch" This reverts commit ef42d1fa921e27df23a5bf322d8536c8cd4b5bc6. * undo falcon * pakcing + mamba support for nemo , falcon , grenite zamba * training run bugs * docks * doc string coverage + test * mamba guard * 2k n 2*1k test * not is_cp_active() * seq_len fix * model list * val ring atten fix * disable double spliting in hf * less comments * undo zamba and bamba * new configs * lint --- docs/nd_parallelism.qmd | 1 + docs/sequence_parallelism.qmd | 28 + examples/falcon-h1/falcon-h1-1b-qlora-cp.yaml | 75 +++ .../nemotron-h/nano-30b-a3b-qlora-cp.yaml | 85 +++ src/axolotl/core/trainers/base.py | 10 + src/axolotl/loaders/patch_manager.py | 45 +- .../monkeypatch/models/falcon_h1/__init__.py | 0 .../monkeypatch/models/falcon_h1/modeling.py | 364 ++++++++++++ .../models/granitemoehybrid/__init__.py | 0 .../models/granitemoehybrid/modeling.py | 107 ++++ src/axolotl/monkeypatch/models/mamba_utils.py | 331 +++++++++++ .../monkeypatch/models/nemotron_h/modeling.py | 30 +- src/axolotl/monkeypatch/multipack.py | 1 + src/axolotl/utils/schemas/validation.py | 25 + tests/monkeypatch/test_mamba_utils.py | 551 ++++++++++++++++++ 15 files changed, 1628 insertions(+), 25 deletions(-) create mode 100644 examples/falcon-h1/falcon-h1-1b-qlora-cp.yaml create mode 100644 examples/nemotron-h/nano-30b-a3b-qlora-cp.yaml create mode 100644 src/axolotl/monkeypatch/models/falcon_h1/__init__.py create mode 100644 src/axolotl/monkeypatch/models/falcon_h1/modeling.py create mode 100644 src/axolotl/monkeypatch/models/granitemoehybrid/__init__.py create mode 100644 src/axolotl/monkeypatch/models/granitemoehybrid/modeling.py create mode 100644 src/axolotl/monkeypatch/models/mamba_utils.py create mode 100644 tests/monkeypatch/test_mamba_utils.py diff --git a/docs/nd_parallelism.qmd b/docs/nd_parallelism.qmd index 972953ff9f..729eeb9bea 100644 --- a/docs/nd_parallelism.qmd +++ b/docs/nd_parallelism.qmd @@ -39,6 +39,7 @@ Context Parallelism, also called [Sequence Parallelism](sequence_parallelism.qmd - How it works: If you have a sequence of 8192 tokens and a `context_parallel_size` of 4, each GPU will only handle a chunk of 2048 tokens. - The Challenge: Attention is not local; every token needs to "attend to" every other token. Splitting the sequence breaks this. - The Solution (`ring-flash-attention`): An efficient communication protocol is used. To compute attention for its local sequence chunk, each GPU passes its Key-Value (KV) cache to its neighbor in a "ring." After `N-1` steps, every GPU has seen the KV-cache from all other GPUs, allowing it to compute the correct attention values for its chunk. This is implemented using the highly optimized `flash-attention` kernel at each step. +- **Mamba/SSM Hybrid Models**: For hybrid architectures (Nemotron-H, Falcon-H1, Granite MoE Hybrid), attention layers use ring attention as above, while SSM (Mamba2) layers use P2P state passing across ranks with an additive output correction. See [Sequence Parallelism](sequence_parallelism.qmd) for details. ### Expert Parallelism (EP) {#sec-ep} diff --git a/docs/sequence_parallelism.qmd b/docs/sequence_parallelism.qmd index 9799c8a700..f03daf0d89 100644 --- a/docs/sequence_parallelism.qmd +++ b/docs/sequence_parallelism.qmd @@ -58,6 +58,34 @@ To use sequence parallelism, you need: - Flash attention must be enabled for this to work (`attn_implementation: flash_attention_2` in config YAML) - May have a small performance overhead due to communication between GPUs +### Mamba/SSM Hybrid Models + +Context parallelism is supported for hybrid models that combine attention and Mamba2 SSM +layers. These models require special handling because: + +- **Attention layers** work correctly via ring flash attention (same as pure-attention models). +- **SSM (Mamba2) layers** are recurrent and need cross-rank hidden-state propagation. + +Axolotl handles both aspects: + +1. **Sample packing boundaries** (`seq_idx`): When multiple sequences are packed into one + row, the SSM kernels need `seq_idx` to reset state at boundaries. Under CP, chunks may + start mid-sample, so `seq_idx` is derived from `position_ids` using a CP-safe cumsum + normalization (see `mamba_utils.get_seq_idx`). + +2. **Cross-rank SSM state passing**: After each SSM scan, the final hidden state is sent to + the next rank via P2P communication (`ring_shift_ssm_state`), and an additive correction + is applied to account for the missing initial state (`mamba2_cp_correction`). This uses + the linearity property of SSMs to avoid a second forward pass. + +#### Supported Architectures + +| Model family | `model_config_type` | Architecture notes | +|---|---|---| +| Nemotron-H | `nemotron_h` | Mamba2 / Attention / MoE hybrid; block type selected per layer | +| Falcon-H1 | `falcon_h1` | Mamba2 and Attention run **in parallel** in every layer | +| Granite MoE Hybrid | `granitemoehybrid` | Mamba2 / Attention / MoE hybrid | + ## Example ```yaml diff --git a/examples/falcon-h1/falcon-h1-1b-qlora-cp.yaml b/examples/falcon-h1/falcon-h1-1b-qlora-cp.yaml new file mode 100644 index 0000000000..606832d316 --- /dev/null +++ b/examples/falcon-h1/falcon-h1-1b-qlora-cp.yaml @@ -0,0 +1,75 @@ +base_model: tiiuae/Falcon-H1-1.5B-Deep-Base +# optionally might have model_type or tokenizer_type +model_type: AutoModelForCausalLM +tokenizer_type: AutoTokenizer +# Automatically upload checkpoint and final model to HF +# hub_model_id: username/custom_model_name + +load_in_8bit: false +load_in_4bit: true + +# huggingface repo +chat_template: falcon_h1 +datasets: + - path: cgato/SlimOrcaDedupCleaned + type: chat_template + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out + +adapter: qlora +lora_r: 32 +lora_alpha: 16 +lora_dropout: 0.05 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + - in_proj + - gate_proj + - up_proj + - down_proj + +sequence_len: 2048 +sample_packing: true +eval_sample_packing: true + +context_parapllel_size: 2 + + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 4 +optimizer: adamw_bnb_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: + +# save_first_step: true # uncomment this to validate checkpoint saving works with your config diff --git a/examples/nemotron-h/nano-30b-a3b-qlora-cp.yaml b/examples/nemotron-h/nano-30b-a3b-qlora-cp.yaml new file mode 100644 index 0000000000..cab402dccf --- /dev/null +++ b/examples/nemotron-h/nano-30b-a3b-qlora-cp.yaml @@ -0,0 +1,85 @@ +# See examples/nemotron-h/README.md for architecture notes and requirements. +base_model: nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.liger.LigerPlugin + +liger_layer_norm: true +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_rms_norm_gated: true + +# LoRA kernel patches are incompatible with this architecture — see README. +lora_mlp_kernel: false +lora_qkv_kernel: false +lora_o_kernel: false + +chat_template: tokenizer_default +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:20%] + field_messages: conversations + message_property_mappings: + role: from + content: value + +val_set_size: 0.0 +output_dir: ./outputs/out +dataset_prepared_path: last_run_prepared + +sequence_len: 4096 +sample_packing: true + +context_parallel_size: 2 + +load_in_4bit: true +quantize_moe_experts: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.0 +lora_target_modules: + - q_proj + - k_proj + - v_proj + - o_proj + +# To also train MoE expert weights, add them via lora_target_parameters +# (they are 3D nn.Parameter tensors, not nn.Linear — no gate_proj): +# lora_target_parameters: +# - up_proj +# - down_proj + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: + +gradient_accumulation_steps: 2 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_4bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false + +resume_from_checkpoint: +logging_steps: 1 +flash_attention: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 + +special_tokens: diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 2e4252123f..14baf6a88c 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -2,6 +2,7 @@ from __future__ import annotations +import contextlib import gc import json import math @@ -460,6 +461,15 @@ def compute_loss( num_items_in_batch=num_items_in_batch, ) + @override + def _prepare_context_parallel_inputs(self, model, inputs): + """Disable HF Trainer's CP splitting when Axolotl's ring_attn handles it.""" + from axolotl.monkeypatch.models.mamba_utils import is_cp_active + + if is_cp_active(): + return contextlib.nullcontext, inputs + return super()._prepare_context_parallel_inputs(model, inputs) + @override def evaluate(self, *args, **kwargs): LOG.info("Running evaluation step...") diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index e0a224d079..45f0c2b05e 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -369,22 +369,39 @@ def _apply_model_specific_patches(self): patch_kimi_model() - if self.cfg.model_config_type == "nemotron_h": - if self.cfg.sample_packing: - from transformers.models.nemotron_h.modeling_nemotron_h import ( - NemotronHPreTrainedModel, - ) + ssm_hybrid_patch_needed = ( + self.cfg.sample_packing or self.cfg.context_parallel_size > 1 + ) - from axolotl.monkeypatch.models.nemotron_h.modeling import ( - patch_nemotron_h_modeling_packing, - ) + if self.cfg.model_config_type == "nemotron_h" and ssm_hybrid_patch_needed: + from transformers.models.nemotron_h.modeling_nemotron_h import ( + NemotronHPreTrainedModel, + ) + + from axolotl.monkeypatch.models.nemotron_h.modeling import ( + patch_nemotron_h_modeling_packing, + ) + + patch_nemotron_h_modeling_packing() + # supports_gradient_checkpointing is only enabled after + # patch_nemotron_h_modeling_packing() installs the GC-compatible + # NemotronHBlock.forward. Without the patch, upstream marks this + # False because the original block forward is not GC-safe. + NemotronHPreTrainedModel.supports_gradient_checkpointing = True + + if self.cfg.model_config_type == "falcon_h1" and ssm_hybrid_patch_needed: + from axolotl.monkeypatch.models.falcon_h1.modeling import ( + patch_falcon_h1_modeling_packing, + ) + + patch_falcon_h1_modeling_packing() + + if self.cfg.model_config_type == "granitemoehybrid" and ssm_hybrid_patch_needed: + from axolotl.monkeypatch.models.granitemoehybrid.modeling import ( + patch_granitemoehybrid_modeling_packing, + ) - patch_nemotron_h_modeling_packing() - # supports_gradient_checkpointing is only enabled after - # patch_nemotron_h_modeling_packing() installs the GC-compatible - # NemotronHBlock.forward. Without the patch, upstream marks this - # False because the original block forward is not GC-safe. - NemotronHPreTrainedModel.supports_gradient_checkpointing = True + patch_granitemoehybrid_modeling_packing() # Patches requiring CUDA if torch.cuda.is_available(): diff --git a/src/axolotl/monkeypatch/models/falcon_h1/__init__.py b/src/axolotl/monkeypatch/models/falcon_h1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/falcon_h1/modeling.py b/src/axolotl/monkeypatch/models/falcon_h1/modeling.py new file mode 100644 index 0000000000..e733a85b77 --- /dev/null +++ b/src/axolotl/monkeypatch/models/falcon_h1/modeling.py @@ -0,0 +1,364 @@ +"""Sample-packing and context-parallelism patch for Falcon-H1 (parallel Mamba2/Attention hybrid). + +Threads seq_idx (derived from position_ids) into the Mamba2 SSM kernels so +packed-sequence boundaries reset SSM state. Upstream hard-codes seq_idx=None, +which leaks hidden state across boundaries. + +Unlike Nemotron-H (which selects block_type per layer), Falcon-H1 runs both +Mamba2 and Attention in **parallel** in every FalconH1DecoderLayer, so we +always need seq_idx for the mamba branch. +""" + +import importlib + +import torch + +from axolotl.monkeypatch.models.mamba_utils import ( + ensure_mamba_kernels_loaded, + get_seq_idx, + is_cp_active, + wrap_mamba_scan_for_cp, +) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def patch_falcon_h1_modeling_packing(): + """Patch Falcon-H1 for sample packing: seq_idx threading into Mamba2 SSM kernels.""" + try: + mod = importlib.import_module( + "transformers.models.falcon_h1.modeling_falcon_h1" + ) + except ImportError: + LOG.warning("falcon_h1 not found in transformers, skipping packing patches") + return + + ensure_mamba_kernels_loaded(mod) + + FalconH1Mixer = mod.FalconH1Mixer + FalconH1DecoderLayer = mod.FalconH1DecoderLayer + + def patched_cuda_kernels_forward( + self, + hidden_states: torch.Tensor, + cache_params=None, + cache_position=None, + attention_mask=None, + seq_idx=None, + ): + hidden_states = mod.apply_mask_to_padding_states(hidden_states, attention_mask) + hidden_states = hidden_states * self.ssm_in_multiplier + projected_states = self.in_proj(hidden_states) + projected_states = projected_states * self.mup_vector + d_to_remove = ( + 2 * self.intermediate_size + + 2 * self.n_groups * self.ssm_state_size + + self.num_heads + ) + + batch_size, seq_len, _ = hidden_states.shape + groups_time_state_size = self.n_groups * self.ssm_state_size + + use_precomputed_states = ( + cache_params is not None + and cache_params.has_previous_state + and seq_len == 1 + and cache_params.conv_states[self.layer_idx].shape[0] + == cache_params.ssm_states[self.layer_idx].shape[0] + == batch_size + and cache_position is not None + and cache_position[0] > 0 + ) + + if use_precomputed_states: + d_mlp = (projected_states.squeeze(1).shape[-1] - d_to_remove) // 2 + z0, x0, gate, hidden_states_B_C, dt = projected_states.squeeze(1).split( + [d_mlp, d_mlp, self.intermediate_size, self.conv_dim, self.num_heads], + dim=-1, + ) + hidden_states_B_C = mod.causal_conv1d_update( + hidden_states_B_C, + cache_params.conv_states[self.layer_idx], + self.conv1d.weight.squeeze(1), + self.conv1d.bias, + self.activation, + ) + hidden_states, B, C = torch.split( + hidden_states_B_C, + [ + self.intermediate_size, + groups_time_state_size, + groups_time_state_size, + ], + dim=-1, + ) + A = -torch.exp(self.A_log.float()) + A = ( + A[:, None, ...][:, :, None] + .expand(-1, self.head_dim, self.ssm_state_size) + .to(dtype=torch.float32) + ) + dt = dt[:, :, None].expand(-1, -1, self.head_dim) + dt_bias = self.dt_bias[:, None, ...].expand(-1, self.head_dim) + D = self.D[:, None, ...].expand(-1, self.head_dim) + B = B.view(batch_size, self.n_groups, B.shape[1] // self.n_groups) + C = C.view(batch_size, self.n_groups, C.shape[1] // self.n_groups) + hidden_states_reshaped = hidden_states.view( + batch_size, self.num_heads, self.head_dim + ) + hidden_states = mod.selective_state_update( + cache_params.ssm_states[self.layer_idx], + hidden_states_reshaped, + dt, + A, + B, + C, + D, + z=( + gate.view(batch_size, self.num_heads, self.head_dim) + if not self.mamba_rms_norm + else None + ), + dt_bias=dt_bias, + dt_softplus=True, + ) + hidden_states = hidden_states.view( + batch_size, self.num_heads * self.head_dim + ) + if self.mamba_rms_norm: + hidden_states = self.norm(hidden_states, gate) + if d_mlp > 0: + hidden_states = torch.cat( + [torch.nn.functional.silu(z0) * x0, hidden_states], dim=-1 + ) + out = self.out_proj(hidden_states[:, None, ...]) + else: + A = -torch.exp(self.A_log.float()) + dt_limit_kwargs = ( + {} + if self.time_step_limit == (0.0, float("inf")) + else {"dt_limit": self.time_step_limit} + ) + + if self.training and cache_params is None and not is_cp_active(): + out = mod.mamba_split_conv1d_scan_combined( + projected_states, + self.conv1d.weight.squeeze(1), + self.conv1d.bias, + self.dt_bias, + A, + D=self.D, + chunk_size=self.chunk_size, + seq_idx=seq_idx, + activation=self.activation, + rmsnorm_weight=(self.norm.weight if self.mamba_rms_norm else None), + rmsnorm_eps=( + self.norm.variance_epsilon if self.mamba_rms_norm else None + ), + outproj_weight=self.out_proj.weight, + outproj_bias=self.out_proj.bias, + headdim=self.head_dim, + ngroups=self.n_groups, + norm_before_gate=False, + return_final_states=False, + **dt_limit_kwargs, + ) + else: + d_mlp = ( + projected_states.shape[-1] + - 2 * self.intermediate_size + - 2 * self.n_groups * self.ssm_state_size + - self.num_heads + ) // 2 + if attention_mask is not None: + projected_states = projected_states * attention_mask[..., None] + _, gate, hidden_states_B_C, dt = projected_states.split( + [2 * d_mlp, self.intermediate_size, self.conv_dim, self.num_heads], + dim=-1, + ) + + if cache_params is not None: + conv_states = torch.nn.functional.pad( + hidden_states_B_C.permute(0, 2, 1), + ( + self.conv_kernel_size - hidden_states_B_C.shape[-2], + 0, + ), + ) + cache_params.update_conv_state( + self.layer_idx, conv_states, cache_position + ) + + time_step = torch.nn.functional.softplus(dt + self.dt_bias) + + if mod.causal_conv1d_fn is None or self.activation not in [ + "silu", + "swish", + ]: + hidden_states_B_C = self.act( + self.conv1d(hidden_states_B_C.transpose(1, 2)).transpose(1, 2)[ + :, :seq_len + ] + ) + else: + hidden_states_B_C = mod.causal_conv1d_fn( + x=hidden_states_B_C.transpose(1, 2), + weight=self.conv1d.weight.squeeze(1), + bias=self.conv1d.bias, + activation=self.activation, + seq_idx=seq_idx, + ).transpose(1, 2)[:, :seq_len] + + hidden_states, B, C = torch.split( + hidden_states_B_C, + [ + self.intermediate_size, + groups_time_state_size, + groups_time_state_size, + ], + dim=-1, + ) + + if ( + attention_mask is not None + and attention_mask.shape[1] > 1 + and attention_mask.shape[0] > 1 + ): + dtype = hidden_states.dtype + hidden_states = (hidden_states * attention_mask[:, :, None]).to( + dtype + ) + + C_reshaped = C.view(batch_size, seq_len, self.n_groups, -1) + with torch.cuda.device(hidden_states.device): + scan_output, ssm_state = mod.mamba_chunk_scan_combined( + hidden_states.view(batch_size, seq_len, -1, self.head_dim), + time_step, + A, + B.view(batch_size, seq_len, self.n_groups, -1), + C_reshaped, + chunk_size=self.chunk_size, + D=self.D, + z=None, + seq_idx=seq_idx, + return_final_states=True, + **dt_limit_kwargs, + ) + + if ssm_state is not None and cache_params is not None: + cache_params.ssm_states[self.layer_idx].copy_(ssm_state) + scan_output = scan_output.view(batch_size, seq_len, -1) + if self.mamba_rms_norm: + out = self.norm(scan_output, gate) + else: + out = scan_output * torch.nn.functional.silu(gate) + out = self.out_proj(out) + return out + + FalconH1Mixer.cuda_kernels_forward = patched_cuda_kernels_forward + + def patched_mixer_forward( + self, + hidden_states, + cache_params=None, + cache_position=None, + attention_mask=None, + seq_idx=None, + ): + if seq_idx is not None and mod.causal_conv1d_fn is None: + raise RuntimeError( + "Falcon-H1 sample packing requires causal_conv1d_fn. " + "Install with: pip install mamba-ssm causal-conv1d" + ) + if mod.is_fast_path_available and "cuda" in self.in_proj.weight.device.type: + return self.cuda_kernels_forward( + hidden_states, + cache_params, + cache_position, + attention_mask, + seq_idx=seq_idx, + ) + if seq_idx is not None: + raise RuntimeError( + "Falcon-H1 sample packing requires the CUDA fast path. " + "Ensure model is on CUDA and mamba-ssm/causal-conv1d are installed." + ) + dtype = hidden_states.dtype + if ( + attention_mask is not None + and attention_mask.shape[1] > 1 + and attention_mask.shape[0] > 1 + ): + hidden_states = (hidden_states * attention_mask[:, :, None]).to(dtype) + return self.torch_forward( + hidden_states, cache_params, cache_position, attention_mask + ) + + FalconH1Mixer.forward = patched_mixer_forward + + # Falcon-H1 runs mamba + attention in parallel every layer (no block_type). + # Compute seq_idx from position_ids and pass to the mamba branch. + def patched_decoder_forward( + self, + hidden_states, + attention_mask=None, + mamba_attention_mask=None, + position_ids=None, + past_key_values=None, + output_attentions=False, + use_cache=False, + cache_position=None, + position_embeddings=None, + **kwargs, + ): + is_decoding = past_key_values is not None and past_key_values.has_previous_state + seq_idx = ( + get_seq_idx(position_ids) + if position_ids is not None and not is_decoding + else None + ) + + residual = hidden_states + hidden_states = self.input_layernorm(hidden_states) + + mamba_hidden_states = self.mamba( + hidden_states=hidden_states, + cache_params=past_key_values, + cache_position=cache_position, + attention_mask=mamba_attention_mask, + seq_idx=seq_idx, + ) + mamba_hidden_states = mamba_hidden_states * self.ssm_out_multiplier + + attention_hidden_states, self_attn_weights = self.self_attn( + hidden_states=hidden_states * self.attention_in_multiplier, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + output_attentions=output_attentions, + use_cache=use_cache, + cache_position=cache_position, + position_embeddings=position_embeddings, + **kwargs, + ) + attention_hidden_states = attention_hidden_states * self.attn_out_multiplier + + hidden_states = mamba_hidden_states + attention_hidden_states + hidden_states = residual + hidden_states + + residual = hidden_states + hidden_states = self.pre_ff_layernorm(hidden_states) + hidden_states = self.feed_forward(hidden_states) + hidden_states = residual + hidden_states + + outputs = (hidden_states,) + if output_attentions: + outputs += (self_attn_weights,) + return outputs + + FalconH1DecoderLayer.forward = patched_decoder_forward + + wrap_mamba_scan_for_cp(mod) + + LOG.info("Applied Falcon-H1 sample packing patch (seq_idx threading into Mamba2)") diff --git a/src/axolotl/monkeypatch/models/granitemoehybrid/__init__.py b/src/axolotl/monkeypatch/models/granitemoehybrid/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/granitemoehybrid/modeling.py b/src/axolotl/monkeypatch/models/granitemoehybrid/modeling.py new file mode 100644 index 0000000000..590db5408d --- /dev/null +++ b/src/axolotl/monkeypatch/models/granitemoehybrid/modeling.py @@ -0,0 +1,107 @@ +"""Sample-packing and context-parallelism patch for Granite MoE Hybrid (Mamba2/Attention/MoE). + +Upstream GraniteMoeHybridMambaLayer already accepts seq_idx on +forward/cuda_kernels_forward, and GraniteMoeHybridDecoderLayer passes **kwargs +through to the mixer. However, the decoder layer does not receive position_ids +directly — it arrives at the model level. + +This patch: +1. Injects seq_idx computation into GraniteMoeHybridModel.forward so it flows + through kwargs -> decoder_layer -> mamba mixer automatically. +2. Forces the slow path when CP is active (the fused path doesn't return SSM + state). CP correction is handled by ``wrap_mamba_scan_for_cp``. +""" + +import importlib + +from axolotl.monkeypatch.models.mamba_utils import ( + ensure_mamba_kernels_loaded, + get_seq_idx, + is_cp_active, + wrap_mamba_scan_for_cp, +) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def patch_granitemoehybrid_modeling_packing(): + """Patch Granite MoE Hybrid for sample packing: seq_idx + CP correction.""" + try: + mod = importlib.import_module( + "transformers.models.granitemoehybrid.modeling_granitemoehybrid" + ) + except ImportError: + LOG.warning( + "granitemoehybrid not found in transformers, skipping packing patches" + ) + return + + ensure_mamba_kernels_loaded(mod) + + GraniteMoeHybridModel = mod.GraniteMoeHybridModel + GraniteMoeHybridMambaLayer = mod.GraniteMoeHybridMambaLayer + + # Patch 1: Model-level seq_idx injection + original_model_forward = GraniteMoeHybridModel.forward + + def patched_model_forward(self, *args, **kwargs): + position_ids = kwargs.get("position_ids") + if position_ids is None and len(args) > 2: + position_ids = args[2] + + past_key_values = kwargs.get("past_key_values") + if past_key_values is None and len(args) > 3: + past_key_values = args[3] + + is_decoding = ( + past_key_values is not None + and hasattr(past_key_values, "has_previous_state") + and past_key_values.has_previous_state + ) + + if position_ids is not None and not is_decoding and "seq_idx" not in kwargs: + kwargs["seq_idx"] = get_seq_idx(position_ids) + + return original_model_forward(self, *args, **kwargs) + + GraniteMoeHybridModel.forward = patched_model_forward + + # Patch 2: Minimal wrapper to force slow path when CP is active. + # The fused mamba_split_conv1d_scan_combined doesn't return SSM state, so + # CP correction (handled by the scan wrapper) needs the slow path. + original_cuda_kernels_forward = GraniteMoeHybridMambaLayer.cuda_kernels_forward + + def patched_cuda_kernels_forward( + self, + hidden_states, + cache_params=None, + attention_mask=None, + seq_idx=None, + ): + force_slow = ( + (seq_idx is not None or is_cp_active()) + and self.training + and cache_params is None + ) + if force_slow: + self.training = False + try: + return original_cuda_kernels_forward( + self, + hidden_states, + cache_params, + attention_mask, + seq_idx, + ) + finally: + if force_slow: + self.training = True + + GraniteMoeHybridMambaLayer.cuda_kernels_forward = patched_cuda_kernels_forward + + wrap_mamba_scan_for_cp(mod) + + LOG.info( + "Applied Granite MoE Hybrid sample packing patch (seq_idx + CP correction)" + ) diff --git a/src/axolotl/monkeypatch/models/mamba_utils.py b/src/axolotl/monkeypatch/models/mamba_utils.py new file mode 100644 index 0000000000..0c0e12c5fa --- /dev/null +++ b/src/axolotl/monkeypatch/models/mamba_utils.py @@ -0,0 +1,331 @@ +"""Shared utilities for Mamba2 SSM sample-packing and context-parallelism patches. + +Used by: nemotron_h, falcon_h1, granite_moe_hybrid +""" + +import functools + +import torch +import torch.distributed as dist + + +def get_seq_idx(position_ids: torch.Tensor) -> torch.Tensor: + """Convert position_ids [B, T] → seq_idx [B, T] int32 for mamba-ssm kernels. + + Example: position_ids [[0,1,2,3,0,1,2]] → seq_idx [[0,0,0,0,1,1,1]] + + Under context parallelism a rank may receive a chunk that begins mid-sample + (position_ids[0] != 0), so the raw cumsum starts at 0 and subtracting 1 + would yield -1 — an invalid value for the Mamba kernels. Subtracting the + first element of the cumsum instead normalises every chunk to start at 0 + while still correctly incrementing at every intra-chunk sample boundary. + + Example (CP rank 1, chunk starts mid-sample): + position_ids [[3,4,5,0,1,2]] → seq_idx [[0,0,0,1,1,1]] + """ + cumsum = torch.cumsum((position_ids == 0).int(), dim=-1) + return (cumsum - cumsum[..., :1]).to(torch.int32) + + +def is_cp_active() -> bool: + """Return True if context parallelism (ring attention) is active on this rank. + + Zero-cost when CP is not configured: the import guard ensures we only touch + the distributed group if ring_flash_attn is installed. + """ + try: + from axolotl.monkeypatch.ring_attn import get_ring_attn_group + + group = get_ring_attn_group() + return group is not None and dist.get_world_size(group) > 1 + except (ImportError, RuntimeError): + return False + + +def _get_cp_group_and_rank(): + """Return (process_group, local_rank, world_size) for the CP ring.""" + from axolotl.monkeypatch.ring_attn import get_ring_attn_group + + group = get_ring_attn_group() + return group, dist.get_rank(group), dist.get_world_size(group) + + +def ring_shift_ssm_state( + h_final: torch.Tensor, +) -> torch.Tensor: + """P2P ring: send h_final to rank+1, receive from rank-1 within CP group. + + Uses synchronous send/recv on the ring attention process group. + Rank 0 in the CP group receives zeros (no previous chunk). + + Args: + h_final: Final SSM state from this rank's forward pass. + Shape is architecture-dependent, typically [B, H, d, n]. + + Returns: + h_prev: SSM state received from rank-1, same shape/dtype as h_final. + Zero tensor on the first rank in the CP group. + """ + group, local_rank, world_size = _get_cp_group_and_rank() + ranks = dist.get_process_group_ranks(group) + + h_prev = torch.zeros_like(h_final) + + if world_size <= 1: + return h_prev + + prev_global = ranks[(local_rank - 1) % world_size] + next_global = ranks[(local_rank + 1) % world_size] + + send_op = dist.P2POp(dist.isend, h_final.contiguous(), next_global, group=group) + recv_op = dist.P2POp(dist.irecv, h_prev, prev_global, group=group) + + reqs = dist.batch_isend_irecv([send_op, recv_op]) + for req in reqs: + req.wait() + + # Rank 0 in the ring has no true predecessor — zero out received state + if local_rank == 0: + h_prev.zero_() + + return h_prev + + +def mamba2_cp_correction( + out: torch.Tensor, + h_final: torch.Tensor, + C: torch.Tensor, + cum_A: torch.Tensor, + h_prev: torch.Tensor, + num_heads: int, + head_dim: int, + seq_idx: torch.Tensor | None = None, +) -> tuple[torch.Tensor, torch.Tensor]: + """Apply CP correction to SSM output using the received state from rank-1. + + SSM output is linear in the initial hidden state, so the contribution of + h_prev can be added analytically without a second forward pass. + + For each timestep t in the local chunk: + propagated_state_t = cumA_t * h_prev [B, H, d, n] + Δy_t = sum_over_n( C_t * propagated_state_t ) [B, H, d] + + The corrected final state for this rank is: + h_final_corrected = h_final + cumA_T * h_prev + + Sample packing correctness (seq_idx): + When sample packing is active, a CP rank may hold multiple packed + sequences. Only the first sequence (seq_idx == 0) is a continuation + of the previous rank's chunk — subsequent sequences are brand-new and + should receive zero correction from h_prev. + + Passing seq_idx masks delta_y to zero for all tokens where + seq_idx > 0, preventing h_prev state from leaking into unrelated + packed sequences. + + Args: + out: SSM scan output from this rank, shape [B, T, D] where D = H*d. + h_final: Final SSM state from this rank, shape [B, H, d, n]. + C: Output projection matrices, shape [B, T, n_groups, n]. + cum_A: Cumulative log-transition factors, shape [B, T, H]. + These are the log-space cumulative sums of A, so + exp(cum_A_t) gives the transition matrix from step 0 to t. + h_prev: SSM state received from rank-1 (zeros on rank 0). + Shape [B, H, d, n]. + num_heads: Number of SSM heads (H). + head_dim: Dimension per head (d). + seq_idx: Optional sequence index tensor, shape [B, T] int32. + When provided, correction is zeroed for tokens where + seq_idx > 0 (i.e. sequences that start fresh on this rank). + + Returns: + corrected_out: out + Δy, shape [B, T, D]. + corrected_h_final: h_final + cumA_T * h_prev, shape [B, H, d, n]. + """ + if not h_prev.any(): + return out, h_final + + B, T, _ = out.shape + n_groups = C.shape[2] + heads_per_group = num_heads // n_groups + + # cum_A: [B, T, H] → transition factors (exponentiate from log-space) + decay = torch.exp(cum_A).float() # [B, T, H] + + # Propagate h_prev through cumulative transitions: [B, T, H, d, n] + prop_state = decay[:, :, :, None, None] * h_prev[:, None, :, :, :].float() + + # C: [B, T, n_groups, n] → expand to heads: [B, T, H, n] + C_expanded = C.float().repeat_interleave(heads_per_group, dim=2) # [B, T, H, n] + + # Δy_t = sum_n(C_t * prop_state_t) → [B, T, H, d] + delta_y = torch.einsum("bthn,bthdn->bthd", C_expanded, prop_state) + + # Mask out correction for tokens belonging to new sequences on this rank. + # seq_idx == 0 → continuation of the sequence that crossed the CP boundary + # seq_idx > 0 → brand-new packed sequence, h_prev is irrelevant to it + if seq_idx is not None: + # mask: [B, T, 1, 1] — broadcast over H and d + mask = (seq_idx == 0).to(delta_y.dtype).unsqueeze(-1).unsqueeze(-1) + delta_y = delta_y * mask + + # Reshape to [B, T, D] where D = H * d + delta_y = delta_y.reshape(B, T, num_heads * head_dim).to(out.dtype) + + corrected_out = out + delta_y + + # Correct final state using last-timestep decay. + # If the last token is in a new sequence (seq_idx > 0 at T-1), h_prev + # should not propagate into h_final either. + if seq_idx is not None and seq_idx[:, -1].any(): + # last token belongs to a new sequence — don't corrupt h_final + corrected_h_final = h_final + else: + decay_final = decay[:, -1, :, None, None] # [B, H, 1, 1] + corrected_h_final = h_final + (decay_final * h_prev.float()).to(h_final.dtype) + + return corrected_out, corrected_h_final + + +def ensure_mamba_kernels_loaded(target_module): + """Eagerly resolve mamba-ssm and causal-conv1d globals on *target_module*. + + Transformers >= 5.5 lazily loads these inside ``Mixer.__init__`` via + ``lazy_load_kernel``. Our monkeypatches run *before* model instantiation, + so the module globals are still ``None``. This helper triggers the kernel + resolution early so the patched ``cuda_kernels_forward`` (and + ``wrap_mamba_scan_for_cp``) can reference them. + """ + if getattr(target_module, "mamba_chunk_scan_combined", None) is not None: + return + + try: + from transformers.integrations.hub_kernels import lazy_load_kernel + from transformers.utils.import_utils import resolve_internal_import + except ImportError: + return + + causal_conv1d = lazy_load_kernel("causal-conv1d") + if causal_conv1d is not None: + target_module.causal_conv1d_update = getattr( + causal_conv1d, "causal_conv1d_update", None + ) + target_module.causal_conv1d_fn = getattr( + causal_conv1d, "causal_conv1d_fn", None + ) + + mamba_ssm = lazy_load_kernel("mamba-ssm") + if mamba_ssm is not None: + target_module.selective_state_update = resolve_internal_import( + mamba_ssm, + chained_path="ops.triton.selective_state_update.selective_state_update", + ) + target_module.mamba_chunk_scan_combined = resolve_internal_import( + mamba_ssm, + chained_path="ops.triton.ssd_combined.mamba_chunk_scan_combined", + ) + target_module.mamba_split_conv1d_scan_combined = resolve_internal_import( + mamba_ssm, + chained_path="ops.triton.ssd_combined.mamba_split_conv1d_scan_combined", + ) + + target_module.is_fast_path_available = all( + ( + getattr(target_module, "selective_state_update", None), + getattr(target_module, "mamba_chunk_scan_combined", None), + getattr(target_module, "mamba_split_conv1d_scan_combined", None), + getattr(target_module, "causal_conv1d_fn", None), + getattr(target_module, "causal_conv1d_update", None), + ) + ) + + +def wrap_mamba_scan_for_cp(target_module): + """Wrap ``mamba_chunk_scan_combined`` in *target_module* to apply CP correction. + + After the scan, if CP is active the wrapper: + 1. Sends the final SSM state to the next rank via ``ring_shift_ssm_state``. + 2. Computes cumA from the scan's A / dt / dt_bias / dt_softplus args. + 3. Calls ``mamba2_cp_correction`` to add the contribution of h_prev. + + This is installed per-module so it only affects the architecture whose + modeling file imports ``mamba_chunk_scan_combined``. + + The approach follows Tri Dao's Mamba-2 systems blog: each GPU computes its + local output and final states, states are passed via P2P, then outputs are + corrected — no ring attention needed for SSM layers. + """ + if getattr(target_module, "_cp_scan_wrapped", False): + return + + ensure_mamba_kernels_loaded(target_module) + + if getattr(target_module, "mamba_chunk_scan_combined", None) is None: + return + + original_scan = target_module.mamba_chunk_scan_combined + + @functools.wraps(original_scan) + def _cp_scan_wrapper(*args, **kwargs): + cp_active = is_cp_active() + + if cp_active: + kwargs["return_final_states"] = True + + result = original_scan(*args, **kwargs) + + if not cp_active: + return result + + scan_output, ssm_state = result + if ssm_state is None: + return result + + h_prev = ring_shift_ssm_state(ssm_state) + + # Signature: mamba_chunk_scan_combined(x, dt, A, B, C, ...) + # Extract from kwargs first, fall back to positional args. + dt_arg = kwargs.get("dt", args[1] if len(args) > 1 else None) + A_arg = kwargs.get("A", args[2] if len(args) > 2 else None) + C_arg = kwargs.get("C", args[4] if len(args) > 4 else None) + if dt_arg is None or A_arg is None or C_arg is None: + raise ValueError( + "wrap_mamba_scan_for_cp requires dt, A, C to be passed " + f"positionally (got {len(args)} positional args) or as kwargs." + ) + dt_bias = kwargs.get("dt_bias") + dt_softplus = kwargs.get("dt_softplus", False) + seq_idx = kwargs.get("seq_idx") + + if dt_softplus: + dt_eff = torch.nn.functional.softplus( + dt_arg + (dt_bias if dt_bias is not None else 0) + ) + else: + dt_eff = dt_arg + + dA = A_arg[None, None, :] * dt_eff + cum_A = torch.cumsum(dA, dim=1) + + x = args[0] + num_heads = A_arg.shape[0] + head_dim = x.shape[3] if x.ndim == 4 else x.shape[2] // num_heads + B_dim, T_dim = x.shape[0], x.shape[1] + + scan_flat = scan_output.view(B_dim, T_dim, -1) + scan_flat, ssm_state = mamba2_cp_correction( + scan_flat, + ssm_state, + C_arg, + cum_A, + h_prev, + num_heads=num_heads, + head_dim=head_dim, + seq_idx=seq_idx, + ) + scan_output = scan_flat.view(scan_output.shape) + + return scan_output, ssm_state + + target_module.mamba_chunk_scan_combined = _cp_scan_wrapper + target_module._cp_scan_wrapped = True diff --git a/src/axolotl/monkeypatch/models/nemotron_h/modeling.py b/src/axolotl/monkeypatch/models/nemotron_h/modeling.py index a36c34259b..91d26fa0c5 100644 --- a/src/axolotl/monkeypatch/models/nemotron_h/modeling.py +++ b/src/axolotl/monkeypatch/models/nemotron_h/modeling.py @@ -1,28 +1,30 @@ -"""Sample-packing patch for NemotronH (Mamba2/Attention/MoE hybrid). +"""Sample-packing and context-parallelism patch for NemotronH (Mamba2/Attention/MoE hybrid). Threads seq_idx (derived from position_ids) into the Mamba2 SSM kernels so packed-sequence boundaries reset SSM state. Upstream hard-codes seq_idx=None, which leaks hidden state across boundaries. Attention and MoE blocks need no changes — only the Mamba2 mixer is patched. + +CP correction (ring-shift of SSM state + additive output fix) is handled by +``wrap_mamba_scan_for_cp`` from ``mamba_utils``, which wraps the +``mamba_chunk_scan_combined`` call at the module level. """ import importlib import torch +from axolotl.monkeypatch.models.mamba_utils import ( + ensure_mamba_kernels_loaded, + get_seq_idx, + is_cp_active, + wrap_mamba_scan_for_cp, +) from axolotl.utils.logging import get_logger LOG = get_logger(__name__) -def get_seq_idx(position_ids: torch.Tensor) -> torch.Tensor: - """Convert position_ids [B, T] → seq_idx [B, T] int32 for mamba-ssm kernels. - - Example: position_ids [[0,1,2,3,0,1,2]] → seq_idx [[0,0,0,0,1,1,1]] - """ - return (torch.cumsum((position_ids == 0).int(), dim=-1) - 1).to(torch.int32) - - def patch_nemotron_h_modeling_packing(): """Patch NemotronH for sample packing: seq_idx threading into Mamba2 SSM kernels. @@ -37,6 +39,8 @@ def patch_nemotron_h_modeling_packing(): LOG.warning("nemotron_h not found in transformers, skipping packing patches") return + ensure_mamba_kernels_loaded(mod) + NemotronHMamba2Mixer = mod.NemotronHMamba2Mixer NemotronHBlock = mod.NemotronHBlock @@ -141,7 +145,7 @@ def patched_cuda_kernels_forward( and self.training and cache_params is None and input_not_masked - and seq_idx is None + and not is_cp_active() ): out, ssm_state = mod.mamba_split_conv1d_scan_combined( projected_states, @@ -212,12 +216,13 @@ def patched_cuda_kernels_forward( dtype ) + C_reshaped = C.view(batch_size, seq_len, self.n_groups, -1) scan_output, ssm_state = mod.mamba_chunk_scan_combined( hidden_states.view(batch_size, seq_len, -1, self.head_dim), time_step, A, B.view(batch_size, seq_len, self.n_groups, -1), - C.view(batch_size, seq_len, self.n_groups, -1), + C_reshaped, chunk_size=self.chunk_size, D=self.D, z=None, @@ -227,6 +232,7 @@ def patched_cuda_kernels_forward( dt_softplus=True, **dt_limit_kwargs, ) + if ssm_state is not None and cache_params is not None: cache_params.ssm_states[self.layer_idx].copy_(ssm_state) scan_output = scan_output.view(batch_size, seq_len, -1) @@ -312,4 +318,6 @@ def patched_block_forward( NemotronHBlock.forward = patched_block_forward + wrap_mamba_scan_for_cp(mod) + LOG.info("Applied NemotronH sample packing patch (seq_idx threading into Mamba2)") diff --git a/src/axolotl/monkeypatch/multipack.py b/src/axolotl/monkeypatch/multipack.py index 9e2157ef47..03c70ee3dd 100644 --- a/src/axolotl/monkeypatch/multipack.py +++ b/src/axolotl/monkeypatch/multipack.py @@ -63,6 +63,7 @@ "afmoe", "nemotron", "nemotron_h", + "falcon_h1", ] diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index f6f3e779f9..fba584f113 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1557,6 +1557,13 @@ def check_context_parallel_size(self): sys.modules[ "transformers.modeling_flash_attention_utils" ].is_flash_attn_greater_or_equal = is_flash_attn_greater_or_equal + if not hasattr( + transformers.modeling_flash_attention_utils, + "is_flash_attn_greater_or_equal_2_10", + ): + transformers.modeling_flash_attention_utils.is_flash_attn_greater_or_equal_2_10 = is_flash_attn_greater_or_equal( + "2.10" + ) sys.modules[ "transformers.modeling_flash_attention_utils" ].is_flash_attn_greater_or_equal_2_10 = ( @@ -1579,6 +1586,24 @@ def check_context_parallel_size(self): "for more details." ) + _SSM_HYBRID_MODEL_TYPES = { + "nemotron_h", + "falcon_h1", + "granitemoehybrid", + } + _model_config_type = getattr(self, "model_config_type", None) or "" + if _model_config_type in _SSM_HYBRID_MODEL_TYPES: + LOG.warning( + f"context_parallel_size={self.context_parallel_size} with " + f"model_type={_model_config_type}: SSM/Mamba layers use P2P " + "hidden-state passing and additive output correction across " + "CP ranks. Attention layers use ring attention. This is " + "mathematically exact but has not been extensively validated " + "end-to-end — verify loss curves match single-GPU baselines. " + "Recommended: run a short training job and compare loss curves " + "against a single-GPU baseline with the same data/seed." + ) + return self @model_validator(mode="after") diff --git a/tests/monkeypatch/test_mamba_utils.py b/tests/monkeypatch/test_mamba_utils.py new file mode 100644 index 0000000000..a473df29c1 --- /dev/null +++ b/tests/monkeypatch/test_mamba_utils.py @@ -0,0 +1,551 @@ +"""Unit tests for shared Mamba2 SSM utilities (mamba_utils.py). + +Tests cover get_seq_idx correctness under: + - single-rank packing + - context parallelism (mid-sample chunk starts) + - batch dimension + - dtype and device + - no-negative regression (CP rank > 0 must never produce -1) + - mamba2_cp_correction mathematical correctness + - wrap_mamba_scan_for_cp wrapper behaviour + - end-to-end CP split: full 2K scan == 2×1K split + correction +""" + +import types +from unittest.mock import patch + +import torch +import torch.nn.functional as F + +from axolotl.monkeypatch.models.mamba_utils import ( + get_seq_idx, + mamba2_cp_correction, + wrap_mamba_scan_for_cp, +) + + +def _reference_ssm_scan(x, dt, A, B, C, dt_bias=None, dt_softplus=False, h0=None): + """Pure-PyTorch step-by-step SSM scan (reference implementation). + + Implements the Mamba2 discrete SSM recurrence: + Δ_t = softplus(dt_t + dt_bias) or dt_t + Ā_t = exp(A · Δ_t) + h_t = Ā_t · h_{t-1} + B_t ⊗ x_t + y_t = (C_t · h_t).sum(dim=n) + + Args: + x: [B, T, H, d] + dt: [B, T, H] + A: [H] (log-space, negative) + B: [B, T, n_groups, n] + C: [B, T, n_groups, n] + dt_bias: [H] or None + dt_softplus: bool + h0: [B, H, d, n] initial state, or None → zeros + + Returns: + out: [B, T, H, d] + h_final: [B, H, d, n] + """ + B_batch, T, H, d = x.shape + n_groups = B.shape[2] + n = B.shape[3] + heads_per_group = H // n_groups + + dt_eff = dt + dt_bias[None, None, :] if dt_bias is not None else dt + if dt_softplus: + dt_eff = F.softplus(dt_eff) + + h = torch.zeros(B_batch, H, d, n, dtype=x.dtype) if h0 is None else h0.clone() + + outputs = [] + for t in range(T): + A_bar = torch.exp(A[None, :] * dt_eff[:, t, :]) # [B, H] + B_t = B[:, t].repeat_interleave(heads_per_group, dim=1) # [B, H, n] + C_t = C[:, t].repeat_interleave(heads_per_group, dim=1) # [B, H, n] + + h = A_bar[:, :, None, None] * h + B_t[:, :, None, :] * x[:, t, :, :, None] + y_t = (C_t[:, :, None, :] * h).sum(dim=-1) # [B, H, d] + outputs.append(y_t) + + return torch.stack(outputs, dim=1), h + + +class TestGetSeqIdx: + """Tests for get_seq_idx(position_ids) → seq_idx.""" + + def test_single_sample_no_packing(self): + """Single sample with no packing: all zeros.""" + pos = torch.tensor([[0, 1, 2, 3, 4]]) + out = get_seq_idx(pos) + assert out.tolist() == [[0, 0, 0, 0, 0]] + + def test_two_packed_samples(self): + """Two packed samples: index increments at the second sample boundary.""" + pos = torch.tensor([[0, 1, 2, 3, 0, 1, 2]]) + out = get_seq_idx(pos) + assert out.tolist() == [[0, 0, 0, 0, 1, 1, 1]] + + def test_three_packed_samples(self): + """Three packed samples.""" + pos = torch.tensor([[0, 1, 0, 1, 2, 0]]) + out = get_seq_idx(pos) + assert out.tolist() == [[0, 0, 1, 1, 1, 2]] + + def test_cp_rank_mid_sample_start(self): + """CP rank > 0: chunk starts mid-sample (position_ids[0] != 0). + + Must produce non-negative seq_idx starting at 0, not -1. + """ + pos = torch.tensor([[3, 4, 5, 0, 1, 2]]) + out = get_seq_idx(pos) + assert out.tolist() == [[0, 0, 0, 1, 1, 1]] + + def test_cp_rank_entire_chunk_mid_sample(self): + """CP rank whose entire chunk is mid-sample (no sample boundary).""" + pos = torch.tensor([[5, 6, 7, 8, 9]]) + out = get_seq_idx(pos) + assert out.tolist() == [[0, 0, 0, 0, 0]] + + def test_no_negative_values_regression(self): + """seq_idx must never contain -1 for any valid position_ids input.""" + cases = [ + [[1, 2, 3]], + [[10, 11, 12, 0, 1]], + [[0, 0, 0]], + ] + for pos_list in cases: + pos = torch.tensor(pos_list) + out = get_seq_idx(pos) + assert out.min().item() >= 0, f"Negative seq_idx for pos={pos_list}" + + def test_batch_dimension(self): + """Batch of 3 sequences, each independently packed.""" + pos = torch.tensor( + [ + [0, 1, 2, 0, 1], + [0, 1, 0, 1, 2], + [3, 4, 0, 1, 2], + ] + ) + out = get_seq_idx(pos) + assert out.tolist() == [ + [0, 0, 0, 1, 1], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1], + ] + + def test_output_dtype_is_int32(self): + """Output dtype must be torch.int32 (mamba-ssm kernel requirement).""" + pos = torch.tensor([[0, 1, 2, 0, 1]]) + out = get_seq_idx(pos) + assert out.dtype == torch.int32 + + def test_output_shape_matches_input(self): + """Output shape matches input shape.""" + pos = torch.zeros(4, 128, dtype=torch.long) + out = get_seq_idx(pos) + assert out.shape == pos.shape + + def test_single_token(self): + """Edge case: single token sequence.""" + pos = torch.tensor([[0]]) + out = get_seq_idx(pos) + assert out.tolist() == [[0]] + + def test_cp_rank_starts_at_1(self): + """CP rank that starts exactly at position 1 (not 0).""" + pos = torch.tensor([[1, 2, 3, 0, 1]]) + out = get_seq_idx(pos) + assert out.tolist() == [[0, 0, 0, 1, 1]] + + def test_many_packed_samples(self): + """Many single-token samples packed together.""" + pos = torch.tensor([[0, 0, 0, 0, 0, 0]]) + out = get_seq_idx(pos) + assert out.tolist() == [[0, 1, 2, 3, 4, 5]] + + +class TestMamba2CpCorrection: + """Tests for mamba2_cp_correction mathematical correctness.""" + + def test_zero_h_prev_is_noop(self): + """When h_prev is all zeros, output should be unchanged.""" + B, T, H, d, n = 1, 8, 4, 16, 8 + n_groups = 2 + + out = torch.randn(B, T, H * d) + h_final = torch.randn(B, H, d, n) + C = torch.randn(B, T, n_groups, n) + cum_A = torch.randn(B, T, H) + h_prev = torch.zeros(B, H, d, n) + + corrected_out, corrected_h = mamba2_cp_correction( + out, + h_final, + C, + cum_A, + h_prev, + num_heads=H, + head_dim=d, + ) + + torch.testing.assert_close(corrected_out, out) + torch.testing.assert_close(corrected_h, h_final) + + def test_correction_shapes(self): + """Output shapes must match input shapes.""" + B, T, H, d, n = 2, 16, 8, 32, 16 + n_groups = 4 + + out = torch.randn(B, T, H * d) + h_final = torch.randn(B, H, d, n) + C = torch.randn(B, T, n_groups, n) + cum_A = torch.randn(B, T, H) + h_prev = torch.randn(B, H, d, n) + + corrected_out, corrected_h = mamba2_cp_correction( + out, + h_final, + C, + cum_A, + h_prev, + num_heads=H, + head_dim=d, + ) + + assert corrected_out.shape == out.shape + assert corrected_h.shape == h_final.shape + + def test_correction_adds_to_output(self): + """With nonzero h_prev, output should differ from input.""" + B, T, H, d, n = 1, 4, 2, 8, 4 + n_groups = 1 + + out = torch.zeros(B, T, H * d) + h_final = torch.zeros(B, H, d, n) + C = torch.ones(B, T, n_groups, n) + cum_A = torch.zeros(B, T, H) # exp(0) = 1, so full propagation + h_prev = torch.ones(B, H, d, n) + + corrected_out, corrected_h = mamba2_cp_correction( + out, + h_final, + C, + cum_A, + h_prev, + num_heads=H, + head_dim=d, + ) + + # With exp(cum_A)=1, C=1, h_prev=1: delta_y should be nonzero + assert corrected_out.abs().sum() > 0 + assert corrected_h.abs().sum() > 0 + + def test_correction_h_final_formula(self): + """Verify h_final correction: h_final + decay_T * h_prev.""" + B, T, H, d, n = 1, 4, 2, 8, 4 + n_groups = 1 + + h_final = torch.zeros(B, H, d, n) + C = torch.ones(B, T, n_groups, n) + cum_A = torch.zeros(B, T, H) + h_prev = torch.ones(B, H, d, n) * 2.0 + out = torch.zeros(B, T, H * d) + + _, corrected_h = mamba2_cp_correction( + out, + h_final, + C, + cum_A, + h_prev, + num_heads=H, + head_dim=d, + ) + + # exp(0) * 2.0 = 2.0 for all elements + expected = torch.ones(B, H, d, n) * 2.0 + torch.testing.assert_close(corrected_h, expected) + + +class TestCpSplitMatchesFullScan: + """End-to-end: full sequence scan == split into chunks + CP correction. + + Runs a reference SSM scan on a full 2K-token sequence, then simulates + 2-rank CP by splitting into 2×1K, running each half with h₀=0, and + applying mamba2_cp_correction to rank 1 using rank 0's final state. + The concatenated result must match the single-rank reference. + """ + + def test_2k_vs_2x1k_output_matches(self): + """Full 2048-token scan output == two 1024-token chunks + CP correction.""" + torch.manual_seed(42) + B, T, H, d, n = 1, 2048, 4, 16, 8 + n_groups = 2 + dt_bias = torch.randn(H) * 0.1 + + x = torch.randn(B, T, H, d) + dt = torch.randn(B, T, H) * 0.1 + A = -torch.rand(H).abs() - 0.01 + B_ssm = torch.randn(B, T, n_groups, n) * 0.1 + C_ssm = torch.randn(B, T, n_groups, n) * 0.1 + + ref_out, ref_h = _reference_ssm_scan( + x, dt, A, B_ssm, C_ssm, dt_bias=dt_bias, dt_softplus=True + ) + + T2 = T // 2 + + out_0, h_final_0 = _reference_ssm_scan( + x[:, :T2], + dt[:, :T2], + A, + B_ssm[:, :T2], + C_ssm[:, :T2], + dt_bias=dt_bias, + dt_softplus=True, + ) + + out_1, h_final_1 = _reference_ssm_scan( + x[:, T2:], + dt[:, T2:], + A, + B_ssm[:, T2:], + C_ssm[:, T2:], + dt_bias=dt_bias, + dt_softplus=True, + ) + + dt_eff_1 = F.softplus(dt[:, T2:] + dt_bias[None, None, :]) + cum_A_1 = torch.cumsum(A[None, None, :] * dt_eff_1, dim=1) + + corrected_out_1, corrected_h_1 = mamba2_cp_correction( + out_1.view(B, T2, H * d), + h_final_1, + C_ssm[:, T2:], + cum_A_1, + h_final_0, + num_heads=H, + head_dim=d, + ) + corrected_out_1 = corrected_out_1.view(B, T2, H, d) + + reconstructed = torch.cat([out_0, corrected_out_1], dim=1) + + torch.testing.assert_close(reconstructed, ref_out, rtol=1e-4, atol=1e-4) + torch.testing.assert_close(corrected_h_1, ref_h, rtol=1e-4, atol=1e-4) + + def test_2k_vs_2x1k_with_batch(self): + """Same split test with batch_size > 1.""" + torch.manual_seed(123) + B, T, H, d, n = 3, 512, 2, 8, 4 + n_groups = 1 + dt_bias = torch.randn(H) * 0.05 + + x = torch.randn(B, T, H, d) + dt = torch.randn(B, T, H) * 0.1 + A = -torch.rand(H).abs() - 0.01 + B_ssm = torch.randn(B, T, n_groups, n) * 0.1 + C_ssm = torch.randn(B, T, n_groups, n) * 0.1 + + ref_out, ref_h = _reference_ssm_scan( + x, dt, A, B_ssm, C_ssm, dt_bias=dt_bias, dt_softplus=True + ) + + T2 = T // 2 + + out_0, h_0 = _reference_ssm_scan( + x[:, :T2], + dt[:, :T2], + A, + B_ssm[:, :T2], + C_ssm[:, :T2], + dt_bias=dt_bias, + dt_softplus=True, + ) + out_1, h_1 = _reference_ssm_scan( + x[:, T2:], + dt[:, T2:], + A, + B_ssm[:, T2:], + C_ssm[:, T2:], + dt_bias=dt_bias, + dt_softplus=True, + ) + + dt_eff_1 = F.softplus(dt[:, T2:] + dt_bias[None, None, :]) + cum_A_1 = torch.cumsum(A[None, None, :] * dt_eff_1, dim=1) + + corrected_out_1, corrected_h_1 = mamba2_cp_correction( + out_1.view(B, T2, H * d), + h_1, + C_ssm[:, T2:], + cum_A_1, + h_0, + num_heads=H, + head_dim=d, + ) + + reconstructed = torch.cat([out_0, corrected_out_1.view(B, T2, H, d)], dim=1) + + torch.testing.assert_close(reconstructed, ref_out, rtol=1e-4, atol=1e-4) + torch.testing.assert_close(corrected_h_1, ref_h, rtol=1e-4, atol=1e-4) + + def test_4_way_split(self): + """4-rank CP: split 1024 tokens into 4×256 chunks with sequential correction.""" + torch.manual_seed(99) + B, T, H, d, n = 1, 1024, 2, 8, 4 + n_groups = 1 + n_ranks = 4 + chunk = T // n_ranks + dt_bias = torch.randn(H) * 0.05 + + x = torch.randn(B, T, H, d) + dt = torch.randn(B, T, H) * 0.1 + A = -torch.rand(H).abs() - 0.01 + B_ssm = torch.randn(B, T, n_groups, n) * 0.1 + C_ssm = torch.randn(B, T, n_groups, n) * 0.1 + + ref_out, ref_h = _reference_ssm_scan( + x, dt, A, B_ssm, C_ssm, dt_bias=dt_bias, dt_softplus=True + ) + + all_outs = [] + h_prev = torch.zeros(B, H, d, n) + + for rank in range(n_ranks): + s, e = rank * chunk, (rank + 1) * chunk + out_r, h_r = _reference_ssm_scan( + x[:, s:e], + dt[:, s:e], + A, + B_ssm[:, s:e], + C_ssm[:, s:e], + dt_bias=dt_bias, + dt_softplus=True, + ) + + dt_eff_r = F.softplus(dt[:, s:e] + dt_bias[None, None, :]) + cum_A_r = torch.cumsum(A[None, None, :] * dt_eff_r, dim=1) + + corrected_out_r, corrected_h_r = mamba2_cp_correction( + out_r.view(B, chunk, H * d), + h_r, + C_ssm[:, s:e], + cum_A_r, + h_prev, + num_heads=H, + head_dim=d, + ) + + all_outs.append(corrected_out_r.view(B, chunk, H, d)) + h_prev = corrected_h_r + + reconstructed = torch.cat(all_outs, dim=1) + + torch.testing.assert_close(reconstructed, ref_out, rtol=1e-3, atol=1e-3) + torch.testing.assert_close(h_prev, ref_h, rtol=1e-3, atol=1e-3) + + +class TestWrapMambaScanForCp: + """Tests for wrap_mamba_scan_for_cp wrapper.""" + + @staticmethod + def _make_module_with_scan(scan_fn): + """Create a module namespace with a mamba_chunk_scan_combined attribute.""" + mod = types.ModuleType("fake_mamba_module") + mod.mamba_chunk_scan_combined = scan_fn + return mod + + def test_passthrough_when_cp_inactive(self): + """When CP is not active, wrapper should return original result unchanged.""" + B, T, H, d, n = 1, 8, 4, 16, 8 + x = torch.randn(B, T, H, d) + dt = torch.randn(B, T, H) + A = -torch.rand(H) + B_arg = torch.randn(B, T, 2, n) + C_arg = torch.randn(B, T, 2, n) + expected_out = torch.randn(B, T, H, d) + expected_state = torch.randn(B, H, d, n) + + def fake_scan(*args, **kwargs): + return expected_out, expected_state + + mod = self._make_module_with_scan(fake_scan) + + with patch( + "axolotl.monkeypatch.models.mamba_utils.is_cp_active", return_value=False + ): + wrap_mamba_scan_for_cp(mod) + out, state = mod.mamba_chunk_scan_combined( + x, + dt, + A, + B_arg, + C_arg, + chunk_size=64, + return_final_states=True, + dt_bias=None, + dt_softplus=False, + ) + + torch.testing.assert_close(out, expected_out) + torch.testing.assert_close(state, expected_state) + + def test_forces_return_final_states_when_cp_active(self): + """When CP is active, wrapper must set return_final_states=True.""" + B, T, H, d, n = 1, 4, 2, 8, 4 + captured_kwargs = {} + + def fake_scan(*args, **kwargs): + captured_kwargs.update(kwargs) + scan_out = torch.zeros(B, T, H, d) + ssm_state = torch.zeros(B, H, d, n) + return scan_out, ssm_state + + mod = self._make_module_with_scan(fake_scan) + + with ( + patch( + "axolotl.monkeypatch.models.mamba_utils.is_cp_active", return_value=True + ), + patch( + "axolotl.monkeypatch.models.mamba_utils.ring_shift_ssm_state", + side_effect=lambda h: torch.zeros_like(h), + ), + ): + wrap_mamba_scan_for_cp(mod) + mod.mamba_chunk_scan_combined( + torch.zeros(B, T, H, d), + torch.zeros(B, T, H), + -torch.ones(H), + torch.zeros(B, T, 1, n), + torch.zeros(B, T, 1, n), + chunk_size=64, + return_final_states=False, + dt_bias=None, + dt_softplus=False, + ) + + assert captured_kwargs["return_final_states"] is True + + def test_idempotency_guard(self): + """Calling wrap_mamba_scan_for_cp twice must not double-wrap.""" + call_count = 0 + + def fake_scan(*args, **kwargs): + nonlocal call_count + call_count += 1 + B, T, H, d, n = 1, 4, 2, 8, 4 + return torch.zeros(B, T, H, d), torch.zeros(B, H, d, n) + + mod = self._make_module_with_scan(fake_scan) + + with patch( + "axolotl.monkeypatch.models.mamba_utils.is_cp_active", return_value=False + ): + wrap_mamba_scan_for_cp(mod) + first_fn = mod.mamba_chunk_scan_combined + wrap_mamba_scan_for_cp(mod) + assert mod.mamba_chunk_scan_combined is first_fn + assert getattr(mod, "_cp_scan_wrapped", False) is True From a4b00dfd2115dad0f4a05146c75b606810e2092f Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Fri, 22 May 2026 15:23:07 +0700 Subject: [PATCH 1312/1405] feat: update transformers to 5.8.1 (#3650) [skip ci] * feat: update transformers to 5.8.1 * ignore uv.lock for now --------- Co-authored-by: Wing Lian --- .gitignore | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b75becc7c1..8365917bf3 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +uv.lock # PyInstaller # Usually these files are written by a python script from a template diff --git a/pyproject.toml b/pyproject.toml index 65e8325811..ae9daefd63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "huggingface_hub>=1.1.7", "peft>=0.19.1,<0.20.0", "tokenizers>=0.22.1", - "transformers==5.5.4", + "transformers==5.8.1", "accelerate==1.13.0", "datasets>=4.8.4,<4.9.0", "trl==1.1.0", From 20f56fac1e6db3215231c76b96c36274881d781c Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 22 May 2026 13:54:08 +0530 Subject: [PATCH 1313/1405] feat-qgalore (#3654) [skip ci] * feat-qgalore * spell check --------- Co-authored-by: Your Name --- docs/optimizers.qmd | 40 +++++++++ pyproject.toml | 1 + src/axolotl/core/builders/base.py | 33 +++++-- src/axolotl/utils/optimizers/qgalore.py | 88 +++++++++++++++++++ src/axolotl/utils/schemas/enums.py | 1 + src/axolotl/utils/schemas/training.py | 60 +++++++++++++ src/axolotl/utils/schemas/validation.py | 42 +++++++++ tests/e2e/test_optimizers.py | 42 +++++++++ .../utils/schemas/validation/test_qgalore.py | 40 +++++++++ 9 files changed, 342 insertions(+), 5 deletions(-) create mode 100644 src/axolotl/utils/optimizers/qgalore.py create mode 100644 tests/utils/schemas/validation/test_qgalore.py diff --git a/docs/optimizers.qmd b/docs/optimizers.qmd index 45eea1d3ab..ffa1fa05d8 100644 --- a/docs/optimizers.qmd +++ b/docs/optimizers.qmd @@ -127,3 +127,43 @@ dion_lr: 0.01 dion_momentum: 0.95 lr: 0.00001 # learning rate for embeddings and parameters that fallback to AdamW ``` + +### q_galore_adamw8bit + +Q-GaLore extends [GaLore](https://arxiv.org/abs/2403.03507) with two extra ideas: +an INT4-quantized projection matrix and an adaptive SVD scheduler that skips +re-projection when a layer's gradient subspace stabilizes. Both are wired up in +axolotl. The third Q-GaLore trick — INT8 weight wrapping — is not yet +implemented and is tracked as a follow-up. + +GitHub: [https://github.com/VITA-Group/Q-GaLore](https://github.com/VITA-Group/Q-GaLore) +Paper: [https://arxiv.org/abs/2407.08296](https://arxiv.org/abs/2407.08296) + +Install: `pip install axolotl[qgalore]` + +This optimizer is for **full fine-tuning**. It is incompatible with `adapter` +(LoRA/QLoRA), `load_in_8bit`, and `load_in_4bit`. DeepSpeed is currently gated +off; FSDP requires `fsdp_version: 2` with `use_orig_params: true`. + +```yaml +optimizer: q_galore_adamw8bit +bf16: true + +# which parameter substrings get the low-rank projection +# (defaults to ["attn", "mlp"] if unset — matches the reference impl) +optim_target_modules: + - attn + - mlp + +# Q-GaLore hyperparameters (defaults shown) +qgalore_rank: 256 +qgalore_update_proj_gap: 200 # max steps between SVD refreshes +qgalore_scale: 0.25 +qgalore_proj_type: std +qgalore_proj_quant: true # INT-quantize the projection matrix P +qgalore_proj_bits: 4 # bitwidth for P +qgalore_proj_group_size: 256 # must divide P's last dim evenly +qgalore_cos_threshold: 0.4 # skip SVD if P_t is this similar to P_{t-1} +qgalore_gamma_proj: 2 # grow update_proj_gap by this factor when stable +qgalore_queue_size: 5 +``` diff --git a/pyproject.toml b/pyproject.toml index ae9daefd63..cfb1559377 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,6 +120,7 @@ optimizers = [ "lomo-optim==0.1.1", "torch-optimi==0.2.1", "came_pytorch==0.1.3", + "q-galore-torch==1.0", ] ray = [ "ray[train]>=2.52.1", diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index ad62837cae..3740cb8706 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -122,11 +122,8 @@ def get_callbacks(self) -> list[TrainerCallback]: if self.cfg.resume_from_checkpoint: callbacks.append(SkipEvalOnResumeCallback()) - gc_collect_steps = getattr(self.cfg, "gc_collect_steps", None) or getattr( - self.cfg, "gc_steps", None - ) - if gc_collect_steps: - callbacks.append(GCCallback(gc_collect_steps=gc_collect_steps)) + if self.cfg.gc_steps: + callbacks.append(GCCallback(gc_steps=self.cfg.gc_steps)) if self.cfg.dynamic_checkpoint and self.cfg.dynamic_checkpoint.enabled: from axolotl.utils.callbacks.dynamic_checkpoint import ( @@ -359,6 +356,32 @@ def _configure_custom_optimizer( adam_kwargs["betas"] = (beta1, beta2, beta3) adam_kwargs["eps"] = (eps1, eps2) + optimizer_kwargs.update(adam_kwargs) + elif self.cfg.optimizer == "q_galore_adamw8bit": + from axolotl.utils.optimizers.qgalore import ( + build_qgalore_param_groups, + patch_q_galore_for_modern_bnb, + ) + + patch_q_galore_for_modern_bnb() + from q_galore_torch import QGaLoreAdamW8bit + + optimizer_cls = QGaLoreAdamW8bit + optimizer_kwargs["params"] = build_qgalore_param_groups( + self.model, + self.cfg.optim_target_modules, + rank=self.cfg.qgalore_rank, + update_proj_gap=self.cfg.qgalore_update_proj_gap, + scale=self.cfg.qgalore_scale, + proj_type=self.cfg.qgalore_proj_type, + proj_quant=self.cfg.qgalore_proj_quant, + proj_bits=self.cfg.qgalore_proj_bits, + proj_group_size=self.cfg.qgalore_proj_group_size, + cos_threshold=self.cfg.qgalore_cos_threshold, + gamma_proj=self.cfg.qgalore_gamma_proj, + queue_size=self.cfg.qgalore_queue_size, + ) + optimizer_kwargs.update(adam_kwargs) elif self.cfg.optimizer == "flash_adamw": from flashoptim import FlashAdamW diff --git a/src/axolotl/utils/optimizers/qgalore.py b/src/axolotl/utils/optimizers/qgalore.py new file mode 100644 index 0000000000..9e2cc82607 --- /dev/null +++ b/src/axolotl/utils/optimizers/qgalore.py @@ -0,0 +1,88 @@ +"""Helpers for the Q-GaLore optimizer integration.""" + +from __future__ import annotations + +import inspect +import types + +from torch import nn + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def patch_q_galore_for_modern_bnb() -> None: + """bnb >=0.44 inserted (beta3, alpha) into ``optimizer_update_8bit_blockwise`` + and ``optimizer_update_32bit``; q-galore-torch==1.0 still calls the legacy + positional layout. Swap q_galore's bnb handle for one that re-emits the + modern layout. No-op on older bnb.""" + import bitsandbytes.functional as F + import q_galore_torch.q_galore_adamw8bit as mod + + if "beta3" not in inspect.signature(F.optimizer_update_8bit_blockwise).parameters: + return + + bw, fp32 = F.optimizer_update_8bit_blockwise, F.optimizer_update_32bit + mod.F = types.SimpleNamespace( + optimizer_update_8bit_blockwise=( + lambda *a, **kw: bw( + *(a[:7] + (0.0, 0.0) + a[7:] if len(a) == 15 else a), **kw + ) + ), + optimizer_update_32bit=( + lambda *a, **kw: fp32( + *(a[:10] + (0.0, 0.0) + a[10:] if len(a) == 13 else a), **kw + ) + ), + optimizer_update_8bit=F.optimizer_update_8bit, + percentile_clipping=F.percentile_clipping, + ) + + +def build_qgalore_param_groups( + model: nn.Module, + target_modules: list[str], + *, + rank: int, + update_proj_gap: int, + scale: float, + proj_type: str, + proj_quant: bool, + proj_bits: int, + proj_group_size: int, + cos_threshold: float, + gamma_proj: int, + queue_size: int, +) -> list[dict]: + """Two param-groups: 2D weights matching ``target_modules`` get the Q-GaLore + projection keys; everything else (norms, biases, embeddings) is plain AdamW.""" + galore, plain = [], [] + for name, p in model.named_parameters(): + if not p.requires_grad: + continue + if p.dim() == 2 and any(t in name for t in target_modules): + galore.append(p) + else: + plain.append(p) + if not galore: + raise ValueError( + f"Q-GaLore: no parameters matched optim_target_modules={target_modules!r}" + ) + LOG.info("Q-GaLore param groups: %d projected, %d plain", len(galore), len(plain)) + return [ + { + "params": galore, + "rank": rank, + "update_proj_gap": update_proj_gap, + "scale": scale, + "proj_type": proj_type, + "quant": proj_quant, + "quant_n_bit": proj_bits, + "quant_group_size": proj_group_size, + "cos_threshold": cos_threshold, + "gamma_proj": gamma_proj, + "queue_size": queue_size, + }, + {"params": plain}, + ] diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index 4743604753..d783983c0c 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -95,6 +95,7 @@ class CustomSupportedOptimizers(str, Enum): flash_sgd = "flash_sgd" flash_sgdw = "flash_sgdw" flash_lion = "flash_lion" + q_galore_adamw8bit = "q_galore_adamw8bit" # Accepted canonical names; hub-kernel paths (containing "/") bypass this set. diff --git a/src/axolotl/utils/schemas/training.py b/src/axolotl/utils/schemas/training.py index 8e06e82cb3..bdb5e81d7f 100644 --- a/src/axolotl/utils/schemas/training.py +++ b/src/axolotl/utils/schemas/training.py @@ -158,6 +158,66 @@ class HyperparametersConfig(BaseModel): }, ) + qgalore_rank: int | None = Field( + default=256, + json_schema_extra={ + "description": "Q-GaLore: rank r of the low-rank gradient projection. Smaller r reduces optimizer state but loses gradient information." + }, + ) + qgalore_update_proj_gap: int | None = Field( + default=200, + json_schema_extra={ + "description": "Q-GaLore: maximum number of steps between SVD recomputations of the projection matrix. The adaptive scheduler may skip updates earlier based on cos_threshold." + }, + ) + qgalore_scale: float | None = Field( + default=0.25, + json_schema_extra={ + "description": "Q-GaLore: scaling factor applied to the projected gradient after project_back. Equivalent to GaLore's `galore_scale`." + }, + ) + qgalore_proj_type: str | None = Field( + default="std", + json_schema_extra={ + "description": "Q-GaLore: projection type for the GaLoreProjector. One of 'std', 'reverse_std', 'right', 'left', 'full'." + }, + ) + qgalore_proj_quant: bool | None = Field( + default=True, + json_schema_extra={ + "description": "Q-GaLore: enable INT-quantization of the projection matrix P (the resilient-to-quantization observation from the paper)." + }, + ) + qgalore_proj_bits: int | None = Field( + default=4, + json_schema_extra={ + "description": "Q-GaLore: bitwidth for the quantized projection matrix when qgalore_proj_quant is True (paper default: 4)." + }, + ) + qgalore_proj_group_size: int | None = Field( + default=256, + json_schema_extra={ + "description": "Q-GaLore: group size for projection-matrix quantization. Must evenly divide the projection's last dimension." + }, + ) + qgalore_cos_threshold: float | None = Field( + default=0.4, + json_schema_extra={ + "description": "Q-GaLore: cosine-similarity threshold for the lazy subspace update. If the new P is within this similarity of the previous one, the SVD is skipped." + }, + ) + qgalore_gamma_proj: int | None = Field( + default=2, + json_schema_extra={ + "description": "Q-GaLore: multiplicative factor by which update_proj_gap grows once a layer's subspace is judged stable." + }, + ) + qgalore_queue_size: int | None = Field( + default=5, + json_schema_extra={ + "description": "Q-GaLore: length of the moving-average queue used by the adaptive-frequency scheduler." + }, + ) max_grad_norm: float | None = Field( default=None, json_schema_extra={"description": "Gradient clipping max norm"} ) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index fba584f113..498dc7d7ff 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -902,6 +902,48 @@ def check_muon_deepspeed_fsdp(cls, data): ) return data + @model_validator(mode="before") + @classmethod + def check_qgalore(cls, data): + if data.get("optimizer") != "q_galore_adamw8bit": + return data + adapter = data.get("adapter") + if adapter: + raise ValueError( + "q_galore_adamw8bit operates on full-precision parameters and is " + f"incompatible with adapter='{adapter}'. Remove the adapter setting " + "or pick a different optimizer." + ) + if data.get("deepspeed"): + raise ValueError( + "q_galore_adamw8bit is not yet validated with DeepSpeed. " + "Use DDP or FSDP2 with use_orig_params=True." + ) + if data.get("fsdp") or data.get("fsdp_config"): + fsdp_version = cls._resolve_fsdp_version(data) + if str(fsdp_version) != "2": + raise ValueError( + "q_galore_adamw8bit requires FSDP2. Set fsdp_version: 2." + ) + fsdp_config = data.get("fsdp_config") or {} + if fsdp_config.get("use_orig_params") is not True: + raise ValueError( + "q_galore_adamw8bit requires fsdp_config.use_orig_params=True so " + "that per-parameter projection state survives FSDP sharding." + ) + if not (data.get("bf16") or data.get("bfloat16") or data.get("fp16")): + LOG.warning( + "q_galore_adamw8bit benefits from mixed-precision (bf16/fp16). " + "Running in fp32 will negate most of the memory savings." + ) + if data.get("optim_target_modules") is None: + # Match the reference impl's defaults: attention + MLP linears. + data["optim_target_modules"] = [ + "attn", + "mlp", + ] + return data + @model_validator(mode="before") @classmethod def check_flashoptim_deepspeed_fsdp(cls, data): diff --git a/tests/e2e/test_optimizers.py b/tests/e2e/test_optimizers.py index 430bd73c5a..a28f5a2d3d 100644 --- a/tests/e2e/test_optimizers.py +++ b/tests/e2e/test_optimizers.py @@ -203,6 +203,48 @@ def test_dion(self, temp_dir): check_model_output_exists(temp_dir, cfg) assert "Dion" in trainer.optimizer.optimizer.__class__.__name__ + @with_temp_dir + def test_q_galore_adamw8bit(self, temp_dir): + pytest.importorskip("q_galore_torch") + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "model_type": "AutoModelForCausalLM", + "tokenizer_type": "AutoTokenizer", + "sequence_len": 1024, + "val_set_size": 0.0, + "special_tokens": { + "pad_token": "<|endoftext|>", + }, + "datasets": [ + { + "path": "mhenrichsen/alpaca_2k_test", + "type": "alpaca", + }, + ], + "num_epochs": 1, + "max_steps": 5, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 0.00001, + "optimizer": "q_galore_adamw8bit", + "bf16": True, + # Tiny rank/group_size so it fits SmolLM's hidden dim cleanly. + "qgalore_rank": 32, + "qgalore_update_proj_gap": 2, + "qgalore_proj_group_size": 64, + "lr_scheduler": "cosine", + "save_first_step": False, + } + ) + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + _, _, trainer = train(cfg=cfg, dataset_meta=dataset_meta) + check_model_output_exists(temp_dir, cfg) + assert "AdamW8bit" in trainer.optimizer.optimizer.__class__.__name__ + @with_temp_dir def test_fft_schedule_free_adamw(self, temp_dir): cfg = DictDefault( diff --git a/tests/utils/schemas/validation/test_qgalore.py b/tests/utils/schemas/validation/test_qgalore.py new file mode 100644 index 0000000000..f6325a018e --- /dev/null +++ b/tests/utils/schemas/validation/test_qgalore.py @@ -0,0 +1,40 @@ +"""Validation tests for the Q-GaLore optimizer config gates.""" + +import pytest + +from axolotl.utils.config import validate_config +from axolotl.utils.dict import DictDefault + + +class TestQGaLoreValidation: + """Pydantic-level checks for q_galore_adamw8bit.""" + + def test_adapter_rejected(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + optimizer="q_galore_adamw8bit", + adapter="lora", + lora_r=8, + lora_alpha=16, + lora_target_linear=True, + ) + with pytest.raises(ValueError, match="incompatible with adapter"): + validate_config(cfg) + + def test_fsdp1_rejected(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + optimizer="q_galore_adamw8bit", + fsdp_version=1, + fsdp_config={"reshard_after_forward": True}, + ) + with pytest.raises(ValueError, match="requires FSDP2"): + validate_config(cfg) + + def test_defaults_filled(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + optimizer="q_galore_adamw8bit", + bf16=True, + ) + cfg = validate_config(cfg) + assert cfg.optim_target_modules == ["attn", "mlp"] + assert cfg.qgalore_rank == 256 + assert cfg.qgalore_proj_bits == 4 From d198094c7b9ffd9d2c35ad216282bf5af59c6e59 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 22 May 2026 13:54:44 +0530 Subject: [PATCH 1314/1405] feat support with autoprocessor (#3656) [skip ci] * support with autoprocessor * simple add dict --------- Co-authored-by: Your Name --- .../prompt_strategies/chat_template.py | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/axolotl/prompt_strategies/chat_template.py b/src/axolotl/prompt_strategies/chat_template.py index be6a38800e..2b831d4976 100644 --- a/src/axolotl/prompt_strategies/chat_template.py +++ b/src/axolotl/prompt_strategies/chat_template.py @@ -25,6 +25,17 @@ LOG.setLevel("INFO") +def _extract_input_ids(result): + """Return the ``input_ids`` from a ``build_prompt`` result. + + With a processor configured, ``build_prompt`` returns a dict of + processor outputs (``input_ids``, ``attention_mask``, optional + ``pixel_values``, etc.). Without a processor it returns a plain + ``list[int]`` from the tokenizer. + """ + return result["input_ids"] if isinstance(result, dict) else result + + class ChatTemplatePrompter(Prompter): """Prompter for HF chat templates""" @@ -468,7 +479,10 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: turns = self.get_conversation_thread(prompt) tools = self._get_tools(prompt) - input_ids = self.prompter.build_prompt(turns, tools=tools) # type: ignore + result = self.prompter.build_prompt(turns, tools=tools) # type: ignore + if not isinstance(result, dict): + result = {"input_ids": result} + input_ids = result["input_ids"] labels = [IGNORE_TOKEN_ID] * len(input_ids) last_eos_idx = -1 @@ -624,11 +638,11 @@ def _tokenize_single_prompt(self, prompt: dict) -> Dict[str, List[int]]: LOG.debug(f"Final labels: {labels}") - return { - "input_ids": input_ids, - "labels": labels, - "attention_mask": [1] * len(input_ids), - } + # ``result`` already carries any processor outputs (pixel_values, image + # grid info, etc.); just set the fields we computed locally. + result["labels"] = labels + result.setdefault("attention_mask", [1] * len(input_ids)) + return result def find_first_eos_token(self, input_ids, start_idx): eos_token_id = self.tokenizer.eos_token_id @@ -705,14 +719,18 @@ def find_turn( real_last_index = len(turns) - 1 # Generate the conversation up to the turn, with final turn replaced with dummy content - dummy_ids = self.prompter.build_prompt( - turns_with_empty, tools=tools, real_last_index=real_last_index - ) # type: ignore + dummy_ids = _extract_input_ids( + self.prompter.build_prompt( # type: ignore + turns_with_empty, tools=tools, real_last_index=real_last_index + ) + ) # Generate the conversation up to the turn, with final turn included - full_ids = self.prompter.build_prompt( - turns_with_content, tools=tools, real_last_index=real_last_index - ) # type: ignore + full_ids = _extract_input_ids( + self.prompter.build_prompt( # type: ignore + turns_with_content, tools=tools, real_last_index=real_last_index + ) + ) if not full_ids or not dummy_ids: LOG.warning(f"Empty template generated for turn {turn_idx}") From bccc1e5e66e12e1535da2b9b8f18593bf5c5a9bf Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 22 May 2026 13:55:41 +0530 Subject: [PATCH 1315/1405] fix AssertionError: Original QKV code not found (#3657) [skip ci] * fix AssertionError: Original QKV code not found * skip ig gemma for lor a * fix misleading commentsT_T' --- src/axolotl/monkeypatch/lora_kernels.py | 98 ++++--------------- .../monkeypatch/models/gemma4/fused_attn.py | 6 +- 2 files changed, 24 insertions(+), 80 deletions(-) diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index b9e7f3e0c8..8156b72c7d 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -76,85 +76,8 @@ value_states = value_states.view(hidden_shape).transpose(1, 2) """.lstrip("\n"), ), - # Gemma4: norm between proj and transpose, RoPE between norm and transpose, - # conditional KV sharing (is_kv_shared_layer), v_proj may be None (attention_k_eq_v). - # We only fuse the projection calls; norms, RoPE, and KV sharing stay as-is. - ( - """ - query_states = self.q_proj(hidden_states).view(hidden_shape) - query_states = self.q_norm(query_states) - query_states = apply_rotary_pos_emb(query_states, cos, sin, unsqueeze_dim=2) - query_states = query_states.transpose(1, 2) - - # For layers with shared KV (from kv sharing point onwards), we reuse the same keys/values states as the last non-sharing layer - if self.is_kv_shared_layer and past_key_values is not None: - key_states, value_states = past_key_values.shared_layers[self.kv_shared_layer_index] - # Device of past layer may be different from current one - key_states = key_states.to(query_states.device) - value_states = value_states.to(query_states.device) - else: - key_states = self.k_proj(hidden_states).view(hidden_shape) - value_states = self.v_proj(hidden_states).view(hidden_shape) if self.v_proj is not None else key_states -""".lstrip("\n"), - """ - query_states, key_states, value_states = self.apply_qkv(hidden_states) - query_states = query_states.view(hidden_shape) - query_states = self.q_norm(query_states) - query_states = apply_rotary_pos_emb(query_states, cos, sin, unsqueeze_dim=2) - query_states = query_states.transpose(1, 2) - - # For layers with shared KV (from kv sharing point onwards), we reuse the same keys/values states as the last non-sharing layer - if self.is_kv_shared_layer and past_key_values is not None: - key_states, value_states = past_key_values.shared_layers[self.kv_shared_layer_index] - # Device of past layer may be different from current one - key_states = key_states.to(query_states.device) - value_states = value_states.to(query_states.device) - else: - key_states = key_states.view(hidden_shape) - value_states = value_states.view(hidden_shape) if self.v_proj is not None else key_states -""".lstrip("\n"), - ), - # Gemma4 (transformers >= 5.6): shared_kv_states parameter replaces - # past_key_values.shared_layers, and v_norm added after k_norm. - ( - """ - query_states = self.q_proj(hidden_states).view(hidden_shape) - query_states = self.q_norm(query_states) - query_states = apply_rotary_pos_emb(query_states, cos, sin, unsqueeze_dim=2) - query_states = query_states.transpose(1, 2) - - # For layers with shared KV (from kv sharing point onwards), we reuse the same keys/values states as the last non-sharing layer. - # We cannot simply reuse the cached state if we have a Cache, as sliding layers will not remember the full states in their Cache - # once we are past the sliding window - so we always use `shared_kv_states` instead, even when past_key_values is not None - if self.is_kv_shared_layer: - key_states, value_states = shared_kv_states[self.kv_shared_layer_index] - # Device of past layer may be different from current one - key_states = key_states.to(query_states.device) - value_states = value_states.to(query_states.device) - else: - key_states = self.k_proj(hidden_states).view(hidden_shape) - value_states = self.v_proj(hidden_states).view(hidden_shape) if self.v_proj is not None else key_states -""".lstrip("\n"), - """ - query_states, key_states, value_states = self.apply_qkv(hidden_states) - query_states = query_states.view(hidden_shape) - query_states = self.q_norm(query_states) - query_states = apply_rotary_pos_emb(query_states, cos, sin, unsqueeze_dim=2) - query_states = query_states.transpose(1, 2) - - # For layers with shared KV (from kv sharing point onwards), we reuse the same keys/values states as the last non-sharing layer. - # We cannot simply reuse the cached state if we have a Cache, as sliding layers will not remember the full states in their Cache - # once we are past the sliding window - so we always use `shared_kv_states` instead, even when past_key_values is not None - if self.is_kv_shared_layer: - key_states, value_states = shared_kv_states[self.kv_shared_layer_index] - # Device of past layer may be different from current one - key_states = key_states.to(query_states.device) - value_states = value_states.to(query_states.device) - else: - key_states = key_states.view(hidden_shape) - value_states = value_states.view(hidden_shape) if self.v_proj is not None else key_states -""".lstrip("\n"), - ), + # Gemma4 has no entry: its fused forward already calls apply_qkv/apply_o, + # and patch_self_attn_lora skips it (see the skip there). ] ORIGINAL_O_CODE = """ @@ -323,6 +246,23 @@ def patch_self_attn_lora(cfg: DictDefault): LOG.info(f"{attention_cls.__name__} already patched") return + # Skip Gemma4: patch_manager applies patch_gemma4_fused_attn + # unconditionally for gemma4 before this runs, and that fused forward + # already calls apply_qkv/apply_o, so the source rewrite is dead. + try: + from transformers.models.gemma4.modeling_gemma4 import ( + Gemma4TextAttention, + ) + + if attention_cls is Gemma4TextAttention: + LOG.info( + "Gemma4TextAttention uses the fused attention path " + "(apply_qkv/apply_o) - skipping LoRA source rewrite" + ) + return + except ImportError: + pass + self_attn_forward = inspect.getsource(attention_cls.forward) attention_cls._original_forward = self_attn_forward self_attn_forward, _ = detab_code(self_attn_forward) diff --git a/src/axolotl/monkeypatch/models/gemma4/fused_attn.py b/src/axolotl/monkeypatch/models/gemma4/fused_attn.py index 2144b6c417..4f4e8f5f2d 100644 --- a/src/axolotl/monkeypatch/models/gemma4/fused_attn.py +++ b/src/axolotl/monkeypatch/models/gemma4/fused_attn.py @@ -145,7 +145,11 @@ def fused_forward( ) attn_output = attn_output.reshape(*input_shape, -1).contiguous() - attn_output = self.o_proj(attn_output) + # Use apply_o if present (LoRA O kernel patch), otherwise direct proj + if hasattr(self, "apply_o"): + attn_output = self.apply_o(attn_output) + else: + attn_output = self.o_proj(attn_output) return attn_output, attn_weights return fused_forward From 65b530829a0a41d4748da9899cb89618e8794ce0 Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 22 May 2026 13:56:00 +0530 Subject: [PATCH 1316/1405] simpo (#3665) [skip ci] Co-authored-by: Your Name --- src/axolotl/core/builders/rl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/core/builders/rl.py b/src/axolotl/core/builders/rl.py index 98296a2016..25340809a3 100644 --- a/src/axolotl/core/builders/rl.py +++ b/src/axolotl/core/builders/rl.py @@ -274,7 +274,7 @@ def build(self, total_num_steps): if ( self.cfg.adapter and self.peft_config - and self.cfg.rl not in (RLType.GRPO, RLType.ORPO, RLType.EBFT) + and self.cfg.rl not in (RLType.GRPO, RLType.ORPO, RLType.EBFT, RLType.SIMPO) ): trainer_kwargs["peft_config"] = self.peft_config From 1d68aca095e2f72d0557249948e1cc73c21b521d Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Fri, 22 May 2026 13:56:33 +0530 Subject: [PATCH 1317/1405] fix test_rm_lora rmv skip (#3669) * rmv skip * test verison * lint * undo --- tests/e2e/test_reward_model_smollm2.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/e2e/test_reward_model_smollm2.py b/tests/e2e/test_reward_model_smollm2.py index 657756f5b6..cc768b173e 100644 --- a/tests/e2e/test_reward_model_smollm2.py +++ b/tests/e2e/test_reward_model_smollm2.py @@ -4,8 +4,6 @@ import unittest -import pytest - from axolotl.common.datasets import load_datasets from axolotl.train import train from axolotl.utils.config import normalize_config, validate_config @@ -19,7 +17,6 @@ class TestRewardModelLoraSmolLM2(unittest.TestCase): Test case for Llama reward models using LoRA """ - @pytest.mark.skip(reason="FIXME, mostly underused functionality") @with_temp_dir def test_rm_lora(self, temp_dir): cfg = DictDefault( From a50dd98982505d5ede39a7bb16f94e623e4ba814 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 26 May 2026 15:58:59 +0700 Subject: [PATCH 1318/1405] fix: ep test missed teardown (#3674) * fix: ep test missed teardown * fix: change hardcoded ports --- tests/integrations/test_expert_parallel.py | 31 ++++++++++++++-------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/tests/integrations/test_expert_parallel.py b/tests/integrations/test_expert_parallel.py index 8a5990d528..fdbf79d4d7 100644 --- a/tests/integrations/test_expert_parallel.py +++ b/tests/integrations/test_expert_parallel.py @@ -1,6 +1,7 @@ """Tests for the Expert-Parallel (DeepEP) integration.""" import os +import socket from importlib.util import find_spec import pytest @@ -180,6 +181,10 @@ def setup_method(self): os.environ.setdefault("WORLD_SIZE", "1") dist.init_process_group(backend="gloo", rank=0, world_size=1) + def teardown_method(self): + if dist.is_initialized(): + dist.destroy_process_group() + def test_no_op_at_world_size_1(self): block = _build_qwen3moe_block(num_experts=16) original_shape = tuple(block.experts.gate_up_proj.shape) @@ -337,13 +342,20 @@ def _ep_topology_worker_expects_error( ExpertParallelPlugin._device_mesh = None -def _spawn_topology_check(world_size, ep_size, dp_shard_size, port_base): +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +def _spawn_topology_check(world_size, ep_size, dp_shard_size): ctx = mp.get_context("spawn") q = ctx.Queue() + port = _find_free_port() procs = [ ctx.Process( target=_ep_topology_worker, - args=(r, world_size, ep_size, dp_shard_size, port_base, q), + args=(r, world_size, ep_size, dp_shard_size, port, q), ) for r in range(world_size) ] @@ -363,10 +375,7 @@ def test_world4_ep2_dp2_orthogonal(self): """At world=4 with ep=2 and dp_shard=2, EP groups must be strided ({0,2}, {1,3}) and dp_shard groups contiguous ({0,1}, {2,3}). """ - # Use large random-ish port base to avoid collision with anything else. - results = _spawn_topology_check( - world_size=4, ep_size=2, dp_shard_size=2, port_base=37610 - ) + results = _spawn_topology_check(world_size=4, ep_size=2, dp_shard_size=2) # Build per-rank groupings from results. ep_groups_by_rank = {r: tuple(eps) for r, eps, _ in results} dp_groups_by_rank = {r: tuple(dps) for r, _, dps in results} @@ -385,9 +394,7 @@ def test_world4_ep2_dp2_orthogonal(self): def test_world4_ep4_dp1_uses_world(self): """ep_size == world_size short-circuits to dist.group.WORLD.""" - results = _spawn_topology_check( - world_size=4, ep_size=4, dp_shard_size=1, port_base=37710 - ) + results = _spawn_topology_check(world_size=4, ep_size=4, dp_shard_size=1) for rank, ep_ranks, dp_ranks in results: assert ep_ranks == [0, 1, 2, 3], (rank, ep_ranks) assert dp_ranks is None # no 2D mesh built @@ -396,10 +403,11 @@ def test_world4_ep2_dp1_invalid_product_raises(self): """ep Date: Tue, 26 May 2026 08:35:05 -0400 Subject: [PATCH 1319/1405] fix broken MX tests from transformers 5.8.1 upgrade (#3679) [skip ci] * fix broken MX tests from transformers 5.8.1 upgrade * test isolation * wrap for torchao possible import error * isolate reward model test more * fix PRM --- cicd/cicd.sh | 9 ++++ src/axolotl/loaders/model.py | 54 +++++++++++++++++++ src/axolotl/utils/quantization.py | 38 +++++++++++++ .../{ => solo}/test_reward_model_smollm2.py | 2 +- tests/e2e/test_quantization.py | 52 ++++++++++++++++++ tests/e2e/utils.py | 4 +- 6 files changed, 157 insertions(+), 2 deletions(-) rename tests/e2e/{ => solo}/test_reward_model_smollm2.py (96%) diff --git a/cicd/cicd.sh b/cicd/cicd.sh index 15a6f7ebf5..957c63f95a 100755 --- a/cicd/cicd.sh +++ b/cicd/cicd.sh @@ -43,11 +43,20 @@ pytest --full-trace -vvv --durations=10 \ --cov-append # Run solo tests with coverage append +# test_rm_lora is run in its own process below (it fails on py3.11 when sharing +# the solo process with other tests; isolating it avoids cross-test state). pytest -v --durations=10 -n1 \ + --ignore=tests/e2e/solo/test_reward_model_smollm2.py \ /workspace/axolotl/tests/e2e/solo/ \ --cov=axolotl \ --cov-append +# Run reward-model test isolated in its own process +pytest -v --durations=10 -s \ + /workspace/axolotl/tests/e2e/solo/test_reward_model_smollm2.py \ + --cov=axolotl \ + --cov-append + # Run integration tests with coverage append pytest -v --durations=10 \ /workspace/axolotl/tests/e2e/integrations/ \ diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 700039c5e5..9d8b97e81c 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -221,6 +221,18 @@ def _apply_pre_model_load_setup(self): self._set_attention_config() self._check_model_requirements() + # MX-quantized checkpoints carry MXTensor weights but no HF quantizer, so + # transformers' load-time weight re-init would crash on them; this guards it. + # torchao is absent on macOS/aarch64, where MX checkpoints can't exist anyway. + try: + from axolotl.utils.quantization import ( + patch_transformers_skip_quantized_init, + ) + + patch_transformers_skip_quantized_init() + except ImportError: + pass + def _apply_post_model_load_setup(self): """Configure the model after it has been loaded.""" # Handle PeftModel if needed @@ -233,11 +245,53 @@ def _apply_post_model_load_setup(self): self._configure_experts_implementation() self._apply_activation_checkpointing() self._resize_token_embeddings() + self._reinitialize_classification_head() self._adjust_model_config() self._configure_embedding_dtypes() self._configure_qat() log_gpu_memory_usage(LOG, "Memory usage after model load", 0) + def _reinitialize_classification_head(self): + """Re-init an uninitialized reward / PRM classification head. + + The ``score``/``classifier`` head is missing from a base-LM checkpoint, so + transformers allocates it with ``torch.empty`` and is then supposed to + initialize it. But transformers 5.8's ``_init_weights`` does + ``init.normal_(module.weight.float(), ...)`` — the ``.float()`` copy makes + this a no-op on a ``bfloat16`` head, leaving uninitialized memory: harmless + zeros on some allocators, NaN/inf garbage on others (→ NaN grads, 0 loss). + Detect that state and initialize the head ourselves. + """ + if not (self.cfg.reward_model or self.cfg.process_reward_model): + return + + head = getattr(self.model, "score", None) or getattr( + self.model, "classifier", None + ) + if not isinstance(head, torch.nn.Linear): + return + + weight = head.weight + # A freshly-initialized head is all-zero (benign) or garbage (huge/non-finite); + # a head loaded from a real reward checkpoint is finite and reasonably scaled. + looks_uninitialized = ( + not torch.isfinite(weight).all() + or weight.abs().max() > 100 + or bool((weight == 0).all()) + ) + if not looks_uninitialized: + return + + std = getattr(self.model.config, "initializer_range", 0.02) or 0.02 + with torch.no_grad(): + weight.normal_(mean=0.0, std=std) + if head.bias is not None: + head.bias.zero_() + LOG.info( + f"Re-initialized {type(self.model).__name__} classification head " + f"(std={std})." + ) + def _configure_experts_implementation(self): if self.cfg.experts_implementation is not None: self.model.set_experts_implementation(self.cfg.experts_implementation) diff --git a/src/axolotl/utils/quantization.py b/src/axolotl/utils/quantization.py index ceaa9e0f39..9583c3d5f3 100644 --- a/src/axolotl/utils/quantization.py +++ b/src/axolotl/utils/quantization.py @@ -2,6 +2,8 @@ Utilities for quantization including QAT and PTQ using torchao. """ +import functools + import torch from packaging import version from torchao.core.config import AOBaseConfig @@ -164,6 +166,39 @@ def _attach_torchao_quantizer( model.hf_quantizer = quantizer +def patch_transformers_skip_quantized_init(): + """Stop ``from_pretrained`` from re-initializing torchao-quantized weights. + + transformers re-runs ``_init_weights`` on every module during loading; the + generic implementation does ``init.normal_(module.weight.float(), ...)``. + ``.float()`` on a torchao tensor subclass (e.g. ``MXTensor``) returns a new + tensor that both drops the ``_is_hf_initialized`` skip flag and does not + implement ``normal_``, so loading an MX checkpoint raises NotImplementedError. + Re-initializing an already-loaded quantized weight is never correct, so we + skip those modules entirely. + """ + from torchao.utils import TorchAOBaseTensor + from transformers import PreTrainedModel + + if getattr(PreTrainedModel._initialize_weights, "_axolotl_torchao_patched", False): + return + + original = PreTrainedModel._initialize_weights + + @functools.wraps(original) + def _initialize_weights(self, module, *args, **kwargs): + if any( + isinstance(param, TorchAOBaseTensor) + for param in module.parameters(recurse=False) + ): + module._is_hf_initialized = True + return None + return original(self, module, *args, **kwargs) + + _initialize_weights._axolotl_torchao_patched = True + PreTrainedModel._initialize_weights = _initialize_weights + + def quantize_model( model, weight_dtype: TorchAOQuantDType, @@ -214,6 +249,9 @@ def quantize_model( # cannot serialize it. Mark the model so the caller can use # safe_serialization=False (torch.save) which supports __tensor_flatten__. model._is_mx_quantized = True + # MX checkpoints reload via plain from_pretrained (no HF quantizer), so guard + # transformers' weight re-init against the MXTensor weights it will encounter. + patch_transformers_skip_quantized_init() else: _attach_torchao_quantizer( model, diff --git a/tests/e2e/test_reward_model_smollm2.py b/tests/e2e/solo/test_reward_model_smollm2.py similarity index 96% rename from tests/e2e/test_reward_model_smollm2.py rename to tests/e2e/solo/test_reward_model_smollm2.py index cc768b173e..e4240ab34f 100644 --- a/tests/e2e/test_reward_model_smollm2.py +++ b/tests/e2e/solo/test_reward_model_smollm2.py @@ -9,7 +9,7 @@ from axolotl.utils.config import normalize_config, validate_config from axolotl.utils.dict import DictDefault -from .utils import check_model_output_exists, check_tensorboard, with_temp_dir +from ..utils import check_model_output_exists, check_tensorboard, with_temp_dir class TestRewardModelLoraSmolLM2(unittest.TestCase): diff --git a/tests/e2e/test_quantization.py b/tests/e2e/test_quantization.py index 94102f6ebb..8cdf04ea05 100644 --- a/tests/e2e/test_quantization.py +++ b/tests/e2e/test_quantization.py @@ -480,6 +480,58 @@ def test_mxfp4_qat_then_ptq_save_pretrained(self, tmp_path): loaded_keys = set(loaded.state_dict().keys()) assert original_keys == loaded_keys + @require_torch_2_8_0 + def test_mxfp4_cross_process_load(self, tmp_path): + """A saved MX checkpoint loads in a fresh interpreter that never quantized. + + The other tests call ``quantize_model`` in-process, which installs the + transformers init guard as a side effect, masking the real reload path. + Here we save, then load in a subprocess that only installs the guard the + way ``ModelLoader._apply_pre_model_load_setup`` does — no ``quantize_model``. + """ + import subprocess + import sys + import textwrap + + model = self._make_tiny_model() + save_dir = str(tmp_path / "mxfp4_xproc_model") + quantize_model(model, TorchAOQuantDType.mxfp4, 32) + save_quantized_model(model, save_dir) + + script = textwrap.dedent( + f""" + import torch + from torch import nn + from transformers import AutoModelForCausalLM + # mirrors ModelLoader._apply_pre_model_load_setup (no quantize_model here) + from axolotl.utils.quantization import ( + patch_transformers_skip_quantized_init, + ) + + patch_transformers_skip_quantized_init() + model = AutoModelForCausalLM.from_pretrained( + {save_dir!r}, dtype=torch.bfloat16 + ) + from torchao.prototype.mx_formats.mx_tensor import MXTensor + assert any( + isinstance(m.weight, MXTensor) + for m in model.modules() + if isinstance(m, nn.Linear) + ), "expected MXTensor weights after reload" + print("CROSS_PROCESS_LOAD_OK") + """ + ) + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + check=False, + ) + assert "CROSS_PROCESS_LOAD_OK" in result.stdout, ( + f"cross-process MX load failed:\nstdout={result.stdout}\n" + f"stderr={result.stderr[-2000:]}" + ) + class TestQuantizationCallback: """ diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 8306b72cea..f2c567eeb9 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -196,7 +196,9 @@ def check_tensorboard( else: assert df.value.values[-1] < lt_val, assertion_err if gt_zero: - assert df.value.values[-1] > 1e-5, "Expected loss to be greater than zero" + assert df.value.values[-1] > 1e-5, ( + f"Expected {tag} to be greater than zero, got {df.value.values[-1]}" + ) def check_tensorboard_loss_decreased( From b05ab9a071a5b71cfd97b2c6cb3316752c997d2c Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 26 May 2026 08:40:32 -0400 Subject: [PATCH 1320/1405] feat(fsdp2): add fp32_norms for keeping RMSNorm/LayerNorm in fp32 (#3670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(fsdp2): add fp32_norms for keeping RMSNorm/LayerNorm in fp32 Add an opt-in config flag that shards norm modules under their own FSDP2 MixedPrecisionPolicy (fp32) before the standard decoder-layer wrap, so the norm and decoder shard groups stay independent. This lets models that declare fp32 norms for training stability train under FSDP2 while the rest of the model runs in bf16/fp16. FSDP1 enforces flat-param dtype uniformity within each wrap group, which is incompatible with keeping norms in fp32; the validator therefore requires fsdp_version: 2. Matching: patterns without a "." match type(module).__name__ as a suffix (catches LlamaRMSNorm, Qwen3RMSNorm, AfmoeRMSNorm, nn.LayerNorm, etc.); patterns containing a "." match the fully qualified class path exactly. Defaults to ["RMSNorm", "LayerNorm"]. Signed-off-by: Wing Lian * fixup! feat(fsdp2): address review findings + fix CI caplog assertions - matcher: skip empty/whitespace-only patterns (cls_name.endswith("") is True for any class, which would silently match everything). - validator: also require fsdp_config to be set, not just fsdp_version==2. fsdp_config is the canonical "is_fsdp" signal elsewhere in the codebase (used by check_fsdp_torch_version, sample_packing validators, etc.). - tests: temporarily flip propagate=True on the `axolotl` logger so pytest caplog can see the warnings. axolotl.cli.configure_logging() sets propagate=False at import time, which is the documented reason the assertions were failing in CI even though the warnings were firing visibly in stdout. - comment: replace multi-line rationale near the fp32_norms helpers with a one-line summary (the longer version lives in the PR description). Signed-off-by: Wing Lian * test(fsdp2): multi-GPU e2e for fp32_norms with dtype-preservation assertion The existing fp32_norms tests are pure-CPU and monkeypatch fully_shard — they cover the matcher logic and validator guard rails but never exercise the actual FSDP2 path that motivated this PR. Adds tests/e2e/multigpu/test_fsdp2_fp32_norms.py: spawns a 2-GPU `axolotl train` subprocess with `fp32_norms: true` + `fsdp_version: 2` + `bf16: true` on tiny-qwen3-129m (full FT, 2 steps) and asserts: 1. Training completes — the original FSDP1 flat-param dtype crash can't recur because we're on FSDP2 with the per-module MixedPrecisionPolicy. 2. All RMSNorm params are float32 after step 1 — captured via a test-only TrainerCallback in tests/e2e/multigpu/_fp32_norms_dtype_capture.py, dumped to JSON at $FP32_NORMS_DTYPE_DUMP_PATH on rank 0. 3. At least one non-norm param is bfloat16 — proves the two FSDP2 MixedPrecisionPolicy groups are independent (catches a silent globally-fp32 fallback that would technically satisfy assertion 2 but defeat the point of the feature). The dtype-capture plugin is plumbed in via the test's yaml `plugins:` list, with PYTHONPATH= on the subprocess env so the tests.e2e.multigpu._fp32_norms_dtype_capture module resolves. Signed-off-by: Wing Lian * chore: lint --------- Signed-off-by: Wing Lian --- docs/mixed_precision.qmd | 20 ++ src/axolotl/loaders/model.py | 13 +- src/axolotl/monkeypatch/accelerate/fsdp2.py | 9 + src/axolotl/utils/fp32_norms.py | 135 +++++++++ src/axolotl/utils/schemas/config.py | 45 +++ .../e2e/multigpu/_fp32_norms_dtype_capture.py | 58 ++++ tests/e2e/multigpu/test_fsdp2_fp32_norms.py | 144 ++++++++++ tests/test_fp32_norms.py | 264 ++++++++++++++++++ tests/utils/schemas/validation/test_fsdp.py | 73 +++++ 9 files changed, 760 insertions(+), 1 deletion(-) create mode 100644 src/axolotl/utils/fp32_norms.py create mode 100644 tests/e2e/multigpu/_fp32_norms_dtype_capture.py create mode 100644 tests/e2e/multigpu/test_fsdp2_fp32_norms.py create mode 100644 tests/test_fp32_norms.py diff --git a/docs/mixed_precision.qmd b/docs/mixed_precision.qmd index 7b77cd4bb4..ac0f668029 100644 --- a/docs/mixed_precision.qmd +++ b/docs/mixed_precision.qmd @@ -54,6 +54,26 @@ bf16: true bf16: full # Equivalent to bf16_full_eval in the HF trainer ``` +### Keeping norms in fp32 (FSDP2) {#sec-fp32-norms} + +Some models declare RMSNorm/LayerNorm layers as fp32 for training +stability — the variance computation in RMSNorm is numerically poor in +bf16, and the learned gain γ quantizes harshly. With FSDP1 this fights +the flat-param dtype uniformity constraint; with FSDP2 each norm can have +its own `MixedPrecisionPolicy`. Enable with: + +```{.yaml} +fsdp_version: 2 +fp32_norms: true +# fp32_norm_classes: # optional override +# - RMSNorm +# - LayerNorm +``` + +Defaults match any class whose name ends in `RMSNorm` or `LayerNorm`. Use +fully qualified names (`module.path.ClassName`) to pin a specific +implementation. + ## FP8 Mixed Precision {#sec-fp8} ::: {.callout-note} diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 9d8b97e81c..9634c1b936 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -56,6 +56,11 @@ get_device_count, get_device_type, ) +from axolotl.utils.fp32_norms import ( + _matches_norm_class, + get_fp32_norm_patterns, + tag_model_fp32_norms, +) from axolotl.utils.logging import get_logger from axolotl.utils.model_shard_quant import load_sharded_model_quant from axolotl.utils.schemas.enums import RLType @@ -191,6 +196,9 @@ def load(self) -> tuple[PreTrainedModel | PeftModelForCausalLM, PeftConfig | Non self.patch_manager.apply_post_model_load_patches(self.model) PLUGIN_MANAGER.post_model_load(self.cfg, self.model) + if self.cfg.fp32_norms: + tag_model_fp32_norms(self.model, self.cfg) + return self.model, lora_config def _apply_pre_model_load_setup(self): @@ -965,8 +973,11 @@ def _convert_embedding_modules_dtype( dest = {"dtype": dist_dtype} if self.cfg.lora_on_cpu: dest["device"] = "cpu" + fp32_norm_patterns = get_fp32_norm_patterns(self.cfg) for name, module in self.model.named_modules(): - if "norm" in name: + if fp32_norm_patterns and _matches_norm_class(module, fp32_norm_patterns): + module.to(torch.float32) + elif "norm" in name: module.to(dist_dtype) if before_kbit_train_or_finetune: if name.endswith(".gate"): diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index fc34636969..531f8cb2f8 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -13,6 +13,7 @@ from torch import nn from axolotl.utils.bench import log_gpu_memory_usage +from axolotl.utils.fp32_norms import get_fp32_norm_patterns, shard_norms_fp32 from axolotl.utils.logging import get_logger LOG = get_logger(__name__) @@ -426,6 +427,14 @@ def fsdp2_prepare_model(accelerator, model: torch.nn.Module) -> torch.nn.Module: auto_wrap_policy = fsdp2_prepare_auto_wrap_policy(fsdp2_plugin, model) log_bias_dtype_mismatch = False + fp32_norm_patterns = get_fp32_norm_patterns(model) + if fp32_norm_patterns: + shard_norms_fp32( + model, + patterns=fp32_norm_patterns, + fully_shard_kwargs=fsdp2_kwargs, + ) + if auto_wrap_policy is not None: for module in get_module_children_bottom_up(model)[:-1]: if is_peft_model and isinstance(module, LoraLayer): diff --git a/src/axolotl/utils/fp32_norms.py b/src/axolotl/utils/fp32_norms.py new file mode 100644 index 0000000000..67793998f8 --- /dev/null +++ b/src/axolotl/utils/fp32_norms.py @@ -0,0 +1,135 @@ +"""Helpers for keeping selected norm modules in fp32 under FSDP2.""" + +from __future__ import annotations + +from typing import Any, Sequence + +import torch + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +DEFAULT_FP32_NORM_SUFFIXES: tuple[str, ...] = ("RMSNorm", "LayerNorm") + + +def _matches_norm_class(module: "torch.nn.Module", patterns: Sequence[str]) -> bool: + """Match a module against class-name patterns. + + Two matching modes, chosen per-pattern by presence of a dot: + - Fully qualified (contains "."): matches f"{module.__module__}.{cls}" exactly. + - Suffix (no dot): matches type(module).__name__.endswith(pattern). + Empty / whitespace-only patterns are skipped (``cls_name.endswith("")`` + is True for every class, which would silently match everything). + """ + cls = type(module) + cls_name = cls.__name__ + qualified = f"{cls.__module__}.{cls_name}" + for pattern in patterns: + if not pattern or not pattern.strip(): + continue + if "." in pattern: + if qualified == pattern: + return True + elif cls_name.endswith(pattern): + return True + return False + + +def get_fp32_norm_patterns(source) -> list[str] | None: + """Resolve configured fp32 norm patterns from a config or tagged model.""" + tagged_patterns = getattr(source, "_axolotl_fp32_norm_patterns", None) + if tagged_patterns is not None: + return list(tagged_patterns) + + if not getattr(source, "fp32_norms", False): + return None + + configured_patterns = getattr(source, "fp32_norm_classes", None) + if configured_patterns: + return list(configured_patterns) + + return list(DEFAULT_FP32_NORM_SUFFIXES) + + +def tag_model_fp32_norms(model: "torch.nn.Module", cfg) -> list[str] | None: + """Attach the resolved fp32 norm patterns to the model for FSDP2 prepare.""" + patterns = get_fp32_norm_patterns(cfg) + if patterns is None: + if hasattr(model, "_axolotl_fp32_norm_patterns"): + delattr(model, "_axolotl_fp32_norm_patterns") + return None + + model._axolotl_fp32_norm_patterns = list(patterns) + return patterns + + +def shard_norms_fp32( + model: "torch.nn.Module", + source=None, + *, + patterns: Sequence[str] | None = None, + fully_shard_kwargs: dict[str, Any] | None = None, +) -> int: + """Wrap matching norm modules with FSDP2 + fp32 MixedPrecisionPolicy.""" + if source is not None and not getattr(source, "fp32_norms", False): + return 0 + + if source is not None and getattr(source, "fsdp_version", None) != 2: + raise ValueError( + "fp32_norms requires fsdp_version: 2. FSDP1 enforces flat-param " + "dtype uniformity within each wrap group, which is incompatible " + "with keeping norms in fp32 while the rest of the layer is bf16." + ) + + patterns = ( + list(patterns) + if patterns is not None + else get_fp32_norm_patterns(source or model) + ) + if not patterns: + return 0 + + from torch.distributed.fsdp import MixedPrecisionPolicy, fully_shard + + outer_policy = (fully_shard_kwargs or {}).get("mp_policy") + output_dtype = getattr(outer_policy, "param_dtype", None) + fp32_policy = MixedPrecisionPolicy( + param_dtype=torch.float32, + reduce_dtype=torch.float32, + output_dtype=output_dtype, + ) + + matches = [ + (name, module) + for name, module in model.named_modules() + if _matches_norm_class(module, patterns) + ] + + if not matches: + LOG.warning( + "fp32_norms enabled but no modules matched patterns %s. Check " + "fp32_norm_classes against the model's actual norm class names.", + patterns, + ) + return 0 + + shard_kwargs = dict(fully_shard_kwargs or {}) + shard_kwargs["mp_policy"] = fp32_policy + + for _name, module in matches: + for param in module.parameters(recurse=False): + param.data = param.data.to(torch.float32) + for buffer in module.buffers(recurse=False): + if buffer.dtype.is_floating_point: + buffer.data = buffer.data.to(torch.float32) + fully_shard(module, **shard_kwargs) + + LOG.info( + "Sharded %d norm modules with fp32 MixedPrecisionPolicy " + "(patterns=%s, output_dtype=%s)", + len(matches), + patterns, + output_dtype, + ) + return len(matches) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index a227e04c75..e99959c695 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -974,6 +974,27 @@ class AxolotlInputConfig( default=None, json_schema_extra={"description": "FSDP version"}, ) + fp32_norms: bool | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Keep norm modules (RMSNorm/LayerNorm) in fp32 by sharding them " + "under their own FSDP2 MixedPrecisionPolicy. Requires fsdp_version: 2." + ) + }, + ) + fp32_norm_classes: list[str] | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Class-name patterns to match for fp32 norm sharding. Patterns " + "without a '.' match against type(module).__name__ as a suffix. " + "Patterns containing a '.' match the fully qualified class path " + "exactly. Defaults to ['RMSNorm', 'LayerNorm'] when fp32_norms is " + "true and this is unset." + ) + }, + ) fsdp_final_state_dict_type: ( Literal["FULL_STATE_DICT", "LOCAL_STATE_DICT", "SHARDED_STATE_DICT"] | None ) = Field( @@ -1477,6 +1498,30 @@ def validate_attn_implementation(cls, value): f"path containing '/'." ) + @model_validator(mode="after") + def check_fp32_norms(self): + if self.fp32_norms: + # FSDP must actually be configured — fsdp_version alone is not + # sufficient since the rest of axolotl treats fsdp_config as the + # canonical "is_fsdp" signal. + if self.fsdp_config is None: + raise ValueError( + "fp32_norms requires FSDP to be enabled " + "(fsdp_config block must be set)." + ) + if str(self.fsdp_version) != "2": + raise ValueError( + "fp32_norms requires fsdp_version: 2. FSDP1's flat-param " + "dtype uniformity constraint is incompatible with keeping " + "norms in fp32 while decoder layers run in bf16." + ) + if self.fp32_norm_classes and not self.fp32_norms: + LOG.warning( + "fp32_norm_classes is set but fp32_norms is not enabled; " + "it will be ignored." + ) + return self + @model_validator(mode="after") def check_sageattn_wo_sample_packing(self): if ( diff --git a/tests/e2e/multigpu/_fp32_norms_dtype_capture.py b/tests/e2e/multigpu/_fp32_norms_dtype_capture.py new file mode 100644 index 0000000000..a0dda96b31 --- /dev/null +++ b/tests/e2e/multigpu/_fp32_norms_dtype_capture.py @@ -0,0 +1,58 @@ +"""Test-only plugin that captures param dtypes after the first optimizer step +and dumps them as JSON to ``$FP32_NORMS_DTYPE_DUMP_PATH``. + +Loaded via ``plugins: [tests.e2e.multigpu._fp32_norms_dtype_capture.DtypeCapturePlugin]`` +in the test yaml config; the dump path is the contract between the subprocess +and the outer pytest function. Rank 0 only — dtype is identical across ranks. +""" + +from __future__ import annotations + +import json +import os + +import torch +from transformers.trainer_callback import TrainerCallback + +from axolotl.integrations.base import BasePlugin + + +def _dtype_name(dtype: torch.dtype) -> str: + return str(dtype).removeprefix("torch.") + + +class _DtypeCaptureCallback(TrainerCallback): + """Capture norm vs non-norm param dtypes after step 1, dump to JSON, exit.""" + + def on_step_end(self, args, state, control, model=None, **kwargs): # type: ignore[override] + if state.global_step != 1 or model is None: + return + # Rank 0 only — every rank sees the same dtype info under FSDP2. + if torch.distributed.is_initialized() and torch.distributed.get_rank() != 0: + return + dump_path = os.environ.get("FP32_NORMS_DTYPE_DUMP_PATH") + if not dump_path: + return + + norm_dtypes: dict[str, str] = {} + non_norm_dtypes: dict[str, str] = {} + for name, param in model.named_parameters(): + entry = (name, _dtype_name(param.dtype)) + if "norm" in name.lower(): + norm_dtypes[entry[0]] = entry[1] + else: + non_norm_dtypes[entry[0]] = entry[1] + + with open(dump_path, "w", encoding="utf-8") as fout: + json.dump( + {"norms": norm_dtypes, "non_norms": non_norm_dtypes}, + fout, + indent=2, + ) + + +class DtypeCapturePlugin(BasePlugin): + """Plugin that registers :class:`_DtypeCaptureCallback` with the trainer.""" + + def add_callbacks_pre_trainer(self, cfg, model): # type: ignore[override] + return [_DtypeCaptureCallback()] diff --git a/tests/e2e/multigpu/test_fsdp2_fp32_norms.py b/tests/e2e/multigpu/test_fsdp2_fp32_norms.py new file mode 100644 index 0000000000..6dd2bf9c91 --- /dev/null +++ b/tests/e2e/multigpu/test_fsdp2_fp32_norms.py @@ -0,0 +1,144 @@ +"""Multi-GPU e2e test for ``fp32_norms`` under FSDP2. + +Two-GPU subprocess run with ``fp32_norms: true`` + ``fsdp_version: 2`` + bf16 +training. The test plugin +``tests.e2e.multigpu._fp32_norms_dtype_capture.DtypeCapturePlugin`` dumps +post-step-1 param dtypes as JSON; the outer test asserts norms stayed fp32 and +at least one non-norm param dropped to bf16 (proving the two policies are +genuinely independent, not a globally-cast model). +""" + +from __future__ import annotations + +import json +import os +from pathlib import Path + +import pytest +import yaml +from accelerate.test_utils import execute_subprocess_async +from transformers.testing_utils import get_torch_dist_unique_port + +from axolotl.utils.dict import DictDefault + +from tests.e2e.utils import require_torch_2_7_0 + +AXOLOTL_ROOT = Path(__file__).parent.parent.parent.parent + + +def _base_fp32_norms_config( + temp_dir: str, *, cpu_ram_efficient_loading: bool = False, **overrides +) -> DictDefault: + """Base config for fp32_norms + FSDP2 multi-GPU.""" + cfg = { + "base_model": "axolotl-ai-co/tiny-qwen3-129m", + "sequence_len": 256, + "val_set_size": 0.0, + "datasets": [ + { + "path": "tatsu-lab/alpaca", + "type": "alpaca", + "split": "train[:1%]", + }, + ], + # Full FT (no adapter) — fp32_norms is about base-model norm precision, + # which adapters wouldn't exercise. + "num_epochs": 1, + "max_steps": 2, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 1e-4, + "optimizer": "adamw_torch_fused", + "lr_scheduler": "cosine", + "flash_attention": True, + "bf16": True, + "fp32_norms": True, + "fsdp_version": 2, + "fsdp_config": { + "offload_params": False, + "cpu_ram_efficient_loading": cpu_ram_efficient_loading, + "transformer_layer_cls_to_wrap": "Qwen3DecoderLayer", + "state_dict_type": "FULL_STATE_DICT", + "auto_wrap_policy": "TRANSFORMER_BASED_WRAP", + "reshard_after_forward": True, + }, + "plugins": [ + "tests.e2e.multigpu._fp32_norms_dtype_capture.DtypeCapturePlugin", + ], + "save_safetensors": True, + } + cfg.update(overrides) + return DictDefault(cfg) + + +def _run_training(temp_dir: str, cfg: DictDefault, dump_path: Path) -> None: + """Write yaml + spawn 2-process training; plugin path goes via PYTHONPATH.""" + Path(temp_dir).mkdir(parents=True, exist_ok=True) + with open(Path(temp_dir) / "config.yaml", "w", encoding="utf-8") as fout: + fout.write(yaml.dump(cfg.to_dict(), Dumper=yaml.Dumper)) + + env = os.environ | { + # Make the test-only plugin module importable in the subprocess. + "PYTHONPATH": (f"{AXOLOTL_ROOT}{os.pathsep}{os.environ.get('PYTHONPATH', '')}"), + "FP32_NORMS_DTYPE_DUMP_PATH": str(dump_path), + } + execute_subprocess_async( + [ + "axolotl", + "train", + str(Path(temp_dir) / "config.yaml"), + "--num-processes", + "2", + "--main-process-port", + f"{get_torch_dist_unique_port()}", + ], + env=env, + ) + + +class TestFSDP2Fp32Norms: + """Verifies the fp32_norms FSDP2 path end-to-end across 2 GPUs.""" + + @require_torch_2_7_0 + @pytest.mark.parametrize( + "cpu_ram_efficient_loading", + [False, True], + ids=["materialized-load", "cpu-ram-efficient-load"], + ) + def test_norms_stay_fp32_under_fsdp2_bf16( + self, temp_dir, cpu_ram_efficient_loading + ): + """fp32_norms keeps RMSNorm params in fp32 while the rest stays bf16.""" + dump_path = Path(temp_dir) / "dtype_capture.json" + cfg = _base_fp32_norms_config( + temp_dir, + cpu_ram_efficient_loading=cpu_ram_efficient_loading, + ) + _run_training(temp_dir, cfg, dump_path) + + # Training completed (no FSDP1-style flat-param dtype crash) AND the + # plugin captured dtypes after step 1. + assert dump_path.exists(), ( + f"plugin did not dump dtype capture to {dump_path}; " + "training may have failed before step 1" + ) + + captured = json.loads(dump_path.read_text()) + norms = captured["norms"] + non_norms = captured["non_norms"] + + assert norms, "no norm params captured — matcher likely failed" + assert all(d == "float32" for d in norms.values()), ( + "fp32_norms claim violated: at least one norm param is not fp32. " + f"Captured norm dtypes: {norms}" + ) + + # At least one non-norm param must be bf16. Without this check the + # test would pass on a globally-fp32 model that didn't shard anything. + non_norm_dtypes = set(non_norms.values()) + assert "bfloat16" in non_norm_dtypes, ( + "expected at least one non-norm param in bfloat16 (proves the two " + "policies are independent); got non-norm dtypes: " + f"{non_norm_dtypes}" + ) diff --git a/tests/test_fp32_norms.py b/tests/test_fp32_norms.py new file mode 100644 index 0000000000..e2b477fe8e --- /dev/null +++ b/tests/test_fp32_norms.py @@ -0,0 +1,264 @@ +"""Unit tests for fp32 norm sharding (FSDP2). Pure-CPU, no dist init.""" + +from __future__ import annotations + +import logging +from types import SimpleNamespace + +import pytest +import torch +import torch.nn as nn +from torch.distributed.fsdp import MixedPrecisionPolicy + +from axolotl.loaders.model import ModelLoader +from axolotl.utils.dict import DictDefault +from axolotl.utils.fp32_norms import ( + DEFAULT_FP32_NORM_SUFFIXES, + _matches_norm_class, + shard_norms_fp32, +) + + +class LlamaRMSNorm(nn.Module): + def __init__(self, dim: int = 8) -> None: + super().__init__() + self.weight = nn.Parameter(torch.ones(dim)) + + +class AfmoeRMSNorm(nn.Module): + def __init__(self, dim: int = 8) -> None: + super().__init__() + self.weight = nn.Parameter(torch.ones(dim)) + + +class CustomNorm(nn.Module): + def __init__(self, dim: int = 8) -> None: + super().__init__() + self.weight = nn.Parameter(torch.ones(dim)) + + +class CustomNormWithBuffer(nn.Module): + def __init__(self, dim: int = 8) -> None: + super().__init__() + self.weight = nn.Parameter(torch.ones(dim, dtype=torch.bfloat16)) + self.register_buffer("running_scale", torch.ones(dim, dtype=torch.float16)) + + +class MLP(nn.Module): + def __init__(self) -> None: + super().__init__() + self.fc = nn.Linear(8, 8) + + +def test_suffix_matches_multiple_norm_families(): + patterns = list(DEFAULT_FP32_NORM_SUFFIXES) + assert _matches_norm_class(LlamaRMSNorm(), patterns) + assert _matches_norm_class(AfmoeRMSNorm(), patterns) + assert _matches_norm_class(nn.LayerNorm(8), patterns) + + +def test_suffix_does_not_match_non_norm_modules(): + patterns = list(DEFAULT_FP32_NORM_SUFFIXES) + assert not _matches_norm_class(MLP(), patterns) + assert not _matches_norm_class(nn.Linear(8, 8), patterns) + assert not _matches_norm_class(CustomNorm(), patterns) + + +def test_explicit_classname_matches_custom_norm(): + assert _matches_norm_class(CustomNorm(), ["CustomNorm"]) + + +def test_fully_qualified_pattern_matches_exact_path(): + qualified = f"{LlamaRMSNorm.__module__}.LlamaRMSNorm" + assert _matches_norm_class(LlamaRMSNorm(), [qualified]) + assert not _matches_norm_class(AfmoeRMSNorm(), [qualified]) + + +def test_mixed_patterns_suffix_and_qualified(): + qualified = f"{LlamaRMSNorm.__module__}.LlamaRMSNorm" + patterns = [qualified, "LayerNorm"] + assert _matches_norm_class(LlamaRMSNorm(), patterns) + assert _matches_norm_class(nn.LayerNorm(8), patterns) + assert not _matches_norm_class(AfmoeRMSNorm(), patterns) + + +class _Cfg: + def __init__(self, **kwargs): + self.fp32_norms = kwargs.get("fp32_norms", False) + self.fp32_norm_classes = kwargs.get("fp32_norm_classes", None) + self.fsdp_version = kwargs.get("fsdp_version", None) + self.fsdp_config = kwargs.get("fsdp_config", None) + self.tensor_parallel_size = kwargs.get("tensor_parallel_size", 1) + self.lora_on_cpu = kwargs.get("lora_on_cpu", False) + + +def test_disabled_is_noop(): + model = nn.Sequential(LlamaRMSNorm(), MLP()) + assert shard_norms_fp32(model, _Cfg(fp32_norms=False)) == 0 + + +def test_enabled_requires_fsdp2(): + model = nn.Sequential(LlamaRMSNorm()) + cfg = _Cfg(fp32_norms=True, fsdp_version=1) + with pytest.raises(ValueError, match="fsdp_version: 2"): + shard_norms_fp32(model, cfg) + + +def test_meta_device_is_supported(monkeypatch): + with torch.device("meta"): + model = nn.Sequential(LlamaRMSNorm()) + cfg = _Cfg(fp32_norms=True, fsdp_version=2) + + import torch.distributed.fsdp as fsdp_module + + calls = [] + + def fake_fully_shard(module, mp_policy=None, **kwargs): + calls.append((type(module).__name__, mp_policy, kwargs)) + return module + + monkeypatch.setattr(fsdp_module, "fully_shard", fake_fully_shard) + + n = shard_norms_fp32(model, cfg) + assert n == 1 + assert calls[0][0] == "LlamaRMSNorm" + assert calls[0][1].param_dtype == torch.float32 + + +def test_passthrough_fully_shard_kwargs_are_used(monkeypatch): + model = nn.Sequential(LlamaRMSNorm()) + cfg = _Cfg(fp32_norms=True, fsdp_version=2) + + import torch.distributed.fsdp as fsdp_module + + calls = [] + + def fake_fully_shard(module, mp_policy=None, **kwargs): + calls.append((module, mp_policy, kwargs)) + return module + + monkeypatch.setattr(fsdp_module, "fully_shard", fake_fully_shard) + + sentinel_mesh = object() + outer_policy = MixedPrecisionPolicy(param_dtype=torch.bfloat16) + n = shard_norms_fp32( + model, + cfg, + fully_shard_kwargs={ + "mesh": sentinel_mesh, + "reshard_after_forward": True, + "mp_policy": outer_policy, + }, + ) + assert n == 1 + assert calls[0][2]["mesh"] is sentinel_mesh + assert calls[0][2]["reshard_after_forward"] is True + assert calls[0][1].output_dtype == torch.bfloat16 + + +def test_no_matches_warns_and_returns_zero(caplog): + model = nn.Sequential(MLP(), nn.Linear(8, 8)) + cfg = _Cfg(fp32_norms=True, fsdp_version=2) + # axolotl.cli.configure_logging() sets propagate=False on the `axolotl` + # logger, so pytest caplog can't see records by default. Temporarily + # re-enable propagation for this assertion. + ax_logger = logging.getLogger("axolotl") + old_propagate = ax_logger.propagate + ax_logger.propagate = True + try: + with caplog.at_level("WARNING", logger="axolotl"): + n = shard_norms_fp32(model, cfg) + finally: + ax_logger.propagate = old_propagate + assert n == 0 + assert "no modules matched" in caplog.text + + +def test_explicit_classes_override_defaults(monkeypatch): + model = nn.Sequential(CustomNorm(), MLP()) + cfg = _Cfg(fp32_norms=True, fsdp_version=2, fp32_norm_classes=["CustomNorm"]) + + import torch.distributed.fsdp as fsdp_module + + calls = [] + + def fake_fully_shard(module, mp_policy=None, **_): + calls.append((type(module).__name__, mp_policy)) + return module + + monkeypatch.setattr(fsdp_module, "fully_shard", fake_fully_shard) + + n = shard_norms_fp32(model, cfg) + assert n == 1 + assert calls[0][0] == "CustomNorm" + assert calls[0][1].param_dtype == torch.float32 + assert calls[0][1].reduce_dtype == torch.float32 + + +def test_matched_norm_storage_is_cast_to_fp32_before_sharding(monkeypatch): + model = nn.Sequential(CustomNormWithBuffer(), MLP()) + cfg = _Cfg( + fp32_norms=True, + fsdp_version=2, + fp32_norm_classes=["CustomNormWithBuffer"], + ) + + import torch.distributed.fsdp as fsdp_module + + seen = [] + + def fake_fully_shard(module, mp_policy=None, **kwargs): + seen.append( + ( + module.weight.dtype, + module.running_scale.dtype, + mp_policy.output_dtype, + kwargs, + ) + ) + return module + + monkeypatch.setattr(fsdp_module, "fully_shard", fake_fully_shard) + + outer_policy = MixedPrecisionPolicy(param_dtype=torch.float16) + n = shard_norms_fp32( + model, + cfg, + fully_shard_kwargs={"mp_policy": outer_policy}, + ) + + assert n == 1 + assert model[0].weight.dtype == torch.float32 + assert model[0].running_scale.dtype == torch.float32 + assert seen[0][0] == torch.float32 + assert seen[0][1] == torch.float32 + assert seen[0][2] == torch.float16 + + +class TinyModel(nn.Module): + def __init__(self) -> None: + super().__init__() + self.embed_tokens = nn.Embedding(8, 8) + self.input_norm = CustomNorm() + self.fc = nn.Linear(8, 8) + + +def test_convert_embedding_modules_dtype_keeps_fp32_norm_matches(): + loader = ModelLoader.__new__(ModelLoader) + loader.cfg = DictDefault( + fp32_norms=True, + fp32_norm_classes=["CustomNorm"], + lora_on_cpu=False, + ) + loader.model = TinyModel() + loader.model_config = SimpleNamespace(model_type="llama") + + loader._convert_embedding_modules_dtype( + embedding_modules=["embed_tokens"], + dist_dtype=torch.bfloat16, + before_kbit_train_or_finetune=False, + ) + + assert loader.model.input_norm.weight.dtype == torch.float32 + assert loader.model.embed_tokens.weight.dtype == torch.bfloat16 + assert loader.model.fc.weight.dtype == torch.float32 diff --git a/tests/utils/schemas/validation/test_fsdp.py b/tests/utils/schemas/validation/test_fsdp.py index ce3f3aa07b..ce0e5f71b5 100644 --- a/tests/utils/schemas/validation/test_fsdp.py +++ b/tests/utils/schemas/validation/test_fsdp.py @@ -2,6 +2,8 @@ tests for pydantic fsdp validation """ +import logging + import pytest from axolotl.utils.config import validate_config @@ -136,6 +138,77 @@ def test_fsdp_prefixes_removed(self, min_base_cfg): assert cfg.fsdp_config.transformer_layer_cls_to_wrap == "LlamaDecoderLayer" assert cfg.fsdp_config.reshard_after_forward is True + def test_fp32_norms_requires_fsdp_config(self, min_base_cfg): + # fsdp_config is the canonical "is_fsdp" signal; fp32_norms requires it. + cfg = min_base_cfg | DictDefault( + fp32_norms=True, + fsdp_version=2, + ) + with pytest.raises(ValueError, match="fp32_norms requires FSDP to be enabled"): + validate_config(cfg) + + def test_fp32_norms_requires_fsdp2(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + fp32_norms=True, + fsdp_version=1, + fsdp_config={"reshard_after_forward": True}, + ) + with pytest.raises(ValueError, match="fp32_norms requires fsdp_version: 2"): + validate_config(cfg) + + def test_fp32_norms_cpu_ram_efficient_loading_ok(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + fp32_norms=True, + fsdp_version=2, + fsdp_config={ + "reshard_after_forward": True, + "cpu_ram_efficient_loading": True, + }, + ) + validated_cfg = validate_config(cfg) + assert validated_cfg.fp32_norms is True + assert validated_cfg.fsdp_config.cpu_ram_efficient_loading is True + + def test_fp32_norms_tensor_parallel_ok(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + fp32_norms=True, + fsdp_version=2, + tensor_parallel_size=2, + fsdp_config={"reshard_after_forward": True}, + ) + validated_cfg = validate_config(cfg) + assert validated_cfg.fp32_norms is True + assert validated_cfg.tensor_parallel_size == 2 + + def test_fp32_norms_fsdp2_ok(self, min_base_cfg): + cfg = min_base_cfg | DictDefault( + fp32_norms=True, + fp32_norm_classes=["AfmoeRMSNorm"], + fsdp_version=2, + fsdp_config={"reshard_after_forward": True}, + ) + validated_cfg = validate_config(cfg) + assert validated_cfg.fp32_norms is True + assert validated_cfg.fp32_norm_classes == ["AfmoeRMSNorm"] + + def test_fp32_norm_classes_without_fp32_norms_warns(self, min_base_cfg, caplog): + cfg = min_base_cfg | DictDefault( + fp32_norm_classes=["AfmoeRMSNorm"], + ) + # axolotl.cli.configure_logging() sets propagate=False on the `axolotl` + # logger, so pytest caplog (attached to root) can't see records by + # default. Temporarily re-enable propagation for this assertion. + ax_logger = logging.getLogger("axolotl") + old_propagate = ax_logger.propagate + ax_logger.propagate = True + try: + with caplog.at_level("WARNING", logger="axolotl"): + validated_cfg = validate_config(cfg) + finally: + ax_logger.propagate = old_propagate + assert not validated_cfg.fp32_norms + assert "fp32_norm_classes is set but fp32_norms is not enabled" in caplog.text + def test_muon_fsdp1_rejected(self, min_base_cfg): cfg = min_base_cfg | DictDefault( optimizer="muon", From ab1a0d81dc9f3163e46c6db019e1ec3a091db0b0 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 26 May 2026 12:37:24 -0400 Subject: [PATCH 1321/1405] latest typer breaks HF CLI (#3684) [skip ci] * latest typer breaks HF CLI * wrong comparison --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index cfb1559377..0c09c30a5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "colorama", "numba>=0.61.2", "numpy>=2.2.6", + "typer<0.26.0", # Evaluation & metrics "evaluate==0.4.1", From 3c4ff59f25c1f648aa8ea76d4fb4793fb0aed9d7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 26 May 2026 15:04:52 -0400 Subject: [PATCH 1322/1405] fix flaky ep tests (#3683) [skip ci] --- tests/integrations/test_expert_parallel.py | 73 +++++++++++++++------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/tests/integrations/test_expert_parallel.py b/tests/integrations/test_expert_parallel.py index fdbf79d4d7..ab7312a0e0 100644 --- a/tests/integrations/test_expert_parallel.py +++ b/tests/integrations/test_expert_parallel.py @@ -1,7 +1,10 @@ """Tests for the Expert-Parallel (DeepEP) integration.""" import os +import queue as queue_mod import socket +import time +from datetime import timedelta from importlib.util import find_spec import pytest @@ -284,7 +287,12 @@ def _ep_topology_worker(rank, world_size, ep_size, dp_shard_size, port, q): os.environ["MASTER_PORT"] = str(port) os.environ["RANK"] = str(rank) os.environ["WORLD_SIZE"] = str(world_size) - dist.init_process_group(backend="gloo", rank=rank, world_size=world_size) + dist.init_process_group( + backend="gloo", + rank=rank, + world_size=world_size, + timeout=timedelta(seconds=120), + ) try: from types import SimpleNamespace @@ -321,7 +329,12 @@ def _ep_topology_worker_expects_error( os.environ["MASTER_PORT"] = str(port) os.environ["RANK"] = str(rank) os.environ["WORLD_SIZE"] = str(world_size) - dist.init_process_group(backend="gloo", rank=rank, world_size=world_size) + dist.init_process_group( + backend="gloo", + rank=rank, + world_size=world_size, + timeout=timedelta(seconds=120), + ) try: from types import SimpleNamespace @@ -348,6 +361,31 @@ def _find_free_port() -> int: return s.getsockname()[1] +def _collect_worker_results(procs, q, world_size, timeout=120): + """Collect one result per worker, bailing out early if a worker dies. + + A bare ``q.get(timeout=...)`` blocks the full timeout even after a worker has + crashed without reporting; here we stop once all workers have exited so an + infra failure surfaces as a clear assertion instead of a silent hang. + """ + results = [] + deadline = time.monotonic() + timeout + while len(results) < world_size and time.monotonic() < deadline: + try: + results.append(q.get(timeout=5)) + except queue_mod.Empty: + if all(not p.is_alive() for p in procs): + break + for p in procs: + p.join(timeout=20) + exitcodes = [p.exitcode for p in procs] + assert len(results) == world_size, ( + f"only {len(results)}/{world_size} workers reported; exitcodes={exitcodes}" + ) + assert all(code == 0 for code in exitcodes), f"worker exitcodes={exitcodes}" + return results + + def _spawn_topology_check(world_size, ep_size, dp_shard_size): ctx = mp.get_context("spawn") q = ctx.Queue() @@ -399,23 +437,24 @@ def test_world4_ep4_dp1_uses_world(self): assert ep_ranks == [0, 1, 2, 3], (rank, ep_ranks) assert dp_ranks is None # no 2D mesh built - def test_world4_ep2_dp1_invalid_product_raises(self): - """ep Date: Wed, 27 May 2026 23:11:00 -0400 Subject: [PATCH 1323/1405] fix: shim Gemma4 use_kernels kernelize() crash on vision tower (#3687) transformers decorates Gemma4VisionAttention with @use_kernelized_func(apply_rotary_pos_emb) where the target is a bare function. Under use_kernels=True (force-enabled by KernelsArgs for the ScatterMoE path), from_pretrained calls model.kernelize(), whose attach_hidden_kernels step does register_module(name, fn) for each _hidden_kernels entry. register_module rejects the non-Module function: TypeError: ...apply_rotary_pos_emb is not a Module subclass with a follow-on AttributeError from the cleanup path. The MoE itself is accelerated via the transformers ExpertsInterface (experts_implementation), independent of this path, and the vision forward uses apply_multidimensional_rope, never apply_rotary_pos_emb -- so the registered entry is dead weight. Add monkeypatch gemma4_kernelize that strips non-Module _hidden_kernels entries from Gemma4VisionAttention, wired in patch_manager._apply_model_specific_patches for gemma4 when use_kernels is set. state_dict is unchanged, so the fix is behavior-neutral. Also add ddp_find_unused_parameters: true to the 26b-a4b MoE QLoRA example (multi-GPU only -- text-backbone LoRA plus KV-sharing layers leave some adapter params gradient-less under DDP). --- examples/gemma4/26b-a4b-moe-qlora.yaml | 6 + src/axolotl/loaders/patch_manager.py | 11 ++ src/axolotl/monkeypatch/gemma4_kernelize.py | 113 +++++++++++ tests/monkeypatch/test_gemma4_kernelize.py | 196 ++++++++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 src/axolotl/monkeypatch/gemma4_kernelize.py create mode 100644 tests/monkeypatch/test_gemma4_kernelize.py diff --git a/examples/gemma4/26b-a4b-moe-qlora.yaml b/examples/gemma4/26b-a4b-moe-qlora.yaml index cdc70ef4a8..c954a5bc2f 100644 --- a/examples/gemma4/26b-a4b-moe-qlora.yaml +++ b/examples/gemma4/26b-a4b-moe-qlora.yaml @@ -25,6 +25,12 @@ liger_glu_activation: true liger_rms_norm_gated: true strict: false +# Multi-GPU (DDP) only: LoRA targets the text backbone, so the frozen vision/ +# audio encoders and Gemma4's KV-sharing layers leave some adapter params +# without gradients. Required to avoid "parameters that were not used in +# producing loss". No effect on single-GPU runs. +ddp_find_unused_parameters: true + chat_template: gemma4 datasets: - path: mlabonne/FineTome-100k diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 45f0c2b05e..f97bd1a25c 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -352,6 +352,17 @@ def _apply_flash_attn_4_patches(self): def _apply_model_specific_patches(self): """Apply patches specific to model architectures.""" + if self.cfg.model_config_type == "gemma4" and self.cfg.use_kernels: + # transformers' Gemma4VisionAttention registers a bare function via + # @use_kernelized_func, which crashes model.kernelize() (triggered by + # use_kernels=True) when it tries to register_module() a non-Module. + # Strip the dead entry so kernelize() succeeds. The MoE itself is + # accelerated via the ExpertsInterface (experts_implementation), + # independent of this path. + from axolotl.monkeypatch.gemma4_kernelize import patch_gemma4_kernelize + + patch_gemma4_kernelize() + if ( self.cfg.model_config_type == "llama4" and self.cfg.llama4_linearized_experts diff --git a/src/axolotl/monkeypatch/gemma4_kernelize.py b/src/axolotl/monkeypatch/gemma4_kernelize.py new file mode 100644 index 0000000000..b87f0b840d --- /dev/null +++ b/src/axolotl/monkeypatch/gemma4_kernelize.py @@ -0,0 +1,113 @@ +"""Fix for transformers' Gemma 4 ``kernelize()`` crash under ``use_kernels``. + +In transformers, ``Gemma4VisionAttention`` is decorated with +``@use_kernelized_func(apply_rotary_pos_emb)`` where ``apply_rotary_pos_emb`` +is a **plain function**, not a ``@use_kernel_func_from_hub``-wrapped kernel +layer. That decorator stashes the bare function in each instance's +``_hidden_kernels`` dict. + +When ``use_kernels=True`` (which axolotl's ``KernelsArgs`` force-enables for the +ScatterMoE path), ``from_pretrained`` calls ``model.kernelize()``, whose +``attach_hidden_kernels`` step does ``module.register_module(name, fn)`` for each +``_hidden_kernels`` entry. ``register_module`` rejects a non-``nn.Module``:: + + TypeError: ...apply_rotary_pos_emb is not a Module subclass + +and the ``finally``-block cleanup then raises the visible:: + + AttributeError: 'Gemma4VisionAttention' object has no attribute apply_rotary_pos_emb + +This is a transformers bug, not Gemma4-specific in spirit (qwen3_moe avoids it +by wrapping the func with ``@use_kernel_func_from_hub`` so a Module-like ``Func`` +is registered). Notably, ``Gemma4VisionAttention.forward`` calls +``apply_multidimensional_rope`` and never references ``apply_rotary_pos_emb``, so +the registered entry is dead weight — dropping the non-Module ``_hidden_kernels`` +entries makes ``kernelize()`` a no-op for vision attention with zero behavior +change. + +The patch wraps ``Gemma4VisionAttention.__init__`` to strip any non-``nn.Module`` +``_hidden_kernels`` entries after construction. Properly-wrapped (Module) entries, +including ones a fixed transformers might introduce, are left intact, so the patch +is forward-compatible. Idempotent; install before the model is built. +""" + +from __future__ import annotations + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +_PATCH_APPLIED = False + + +def patch_gemma4_kernelize() -> bool: + """Strip dead non-Module ``_hidden_kernels`` entries on ``Gemma4VisionAttention``. + + Returns ``True`` if the patch is installed (or already was), ``False`` if the + target class could not be imported (e.g. transformers predates Gemma 4) — in + which case nothing is done and the caller can continue unaffected. + """ + global _PATCH_APPLIED + if _PATCH_APPLIED: + return True + + try: + from transformers.models.gemma4 import modeling_gemma4 + except ImportError: + LOG.debug( + "gemma4_kernelize: transformers.models.gemma4 not importable, " + "skipping. This is fine for non-Gemma4 training." + ) + return False + + cls = getattr(modeling_gemma4, "Gemma4VisionAttention", None) + if cls is None: + LOG.warning( + "gemma4_kernelize: modeling_gemma4 has no 'Gemma4VisionAttention', " + "skipping. Transformers API may have changed." + ) + return False + + import torch.nn as nn + + orig_init = cls.__init__ + + def init(self, *args, **kwargs): + orig_init(self, *args, **kwargs) + hidden_kernels = self.__dict__.get("_hidden_kernels") + if hidden_kernels: + stale = [ + name + for name, fn in hidden_kernels.items() + if not isinstance(fn, nn.Module) + ] + for name in stale: + del hidden_kernels[name] + + # Preserve the original for teardown / idempotency checks. + init._axolotl_original = orig_init # type: ignore[attr-defined] + cls.__init__ = init + _PATCH_APPLIED = True + LOG.info( + "gemma4_kernelize: patched Gemma4VisionAttention to drop non-Module " + "_hidden_kernels entries so use_kernels/kernelize() does not crash" + ) + return True + + +def unpatch_gemma4_kernelize() -> None: + """Restore the original ``Gemma4VisionAttention.__init__``. Useful for tests.""" + global _PATCH_APPLIED + if not _PATCH_APPLIED: + return + try: + from transformers.models.gemma4 import modeling_gemma4 + except ImportError: + _PATCH_APPLIED = False + return + cls = getattr(modeling_gemma4, "Gemma4VisionAttention", None) + if cls is not None: + original = getattr(cls.__init__, "_axolotl_original", None) + if original is not None: + cls.__init__ = original + _PATCH_APPLIED = False diff --git a/tests/monkeypatch/test_gemma4_kernelize.py b/tests/monkeypatch/test_gemma4_kernelize.py new file mode 100644 index 0000000000..1aa461553d --- /dev/null +++ b/tests/monkeypatch/test_gemma4_kernelize.py @@ -0,0 +1,196 @@ +"""Tests for the Gemma 4 ``kernelize()`` / ``use_kernels`` crash fix. + +transformers decorates ``Gemma4VisionAttention`` with +``@use_kernelized_func(apply_rotary_pos_emb)`` where the target is a plain +function. Under ``use_kernels=True``, ``model.kernelize()`` then tries to +``register_module()`` that function and crashes with:: + + TypeError: ...apply_rotary_pos_emb is not a Module subclass + +(and a follow-on ``AttributeError`` from the cleanup path). The patch strips the +dead non-Module ``_hidden_kernels`` entry so ``kernelize()`` succeeds. The entry +is never read by ``Gemma4VisionAttention.forward`` (which uses +``apply_multidimensional_rope``), so removing it is behavior-neutral. +""" + +import pytest + +pytest.importorskip( + "transformers.models.gemma4", + reason="gemma4_kernelize patch only matters when Gemma 4 is available", +) + + +@pytest.fixture +def restore_gemma4_vision_attention(): + """Snapshot ``Gemma4VisionAttention.__init__`` and reset patch state after + each test so patch state doesn't leak across the suite.""" + from transformers.models.gemma4 import modeling_gemma4 + + saved_init = modeling_gemma4.Gemma4VisionAttention.__init__ + yield modeling_gemma4 + modeling_gemma4.Gemma4VisionAttention.__init__ = saved_init + from axolotl.monkeypatch import gemma4_kernelize + + gemma4_kernelize._PATCH_APPLIED = False + + +def _vision_config(): + from transformers.models.gemma4.configuration_gemma4 import Gemma4VisionConfig + + return Gemma4VisionConfig( + hidden_size=32, + intermediate_size=64, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=8, + ) + + +def test_patch_installs_and_is_idempotent(restore_gemma4_vision_attention): + from axolotl.monkeypatch.gemma4_kernelize import patch_gemma4_kernelize + + assert patch_gemma4_kernelize() is True + init_first = restore_gemma4_vision_attention.Gemma4VisionAttention.__init__ + # Second call must not re-wrap. + assert patch_gemma4_kernelize() is True + init_second = restore_gemma4_vision_attention.Gemma4VisionAttention.__init__ + assert init_first is init_second + assert hasattr(init_first, "_axolotl_original") + + +def test_patch_strips_non_module_hidden_kernels(restore_gemma4_vision_attention): + modeling_gemma4 = restore_gemma4_vision_attention + from axolotl.monkeypatch.gemma4_kernelize import patch_gemma4_kernelize + + cfg = _vision_config() + + # Before the patch, the bare function is registered (the crash source). + attn_before = modeling_gemma4.Gemma4VisionAttention(cfg, layer_idx=0) + assert "apply_rotary_pos_emb" in getattr(attn_before, "_hidden_kernels", {}) + + patch_gemma4_kernelize() + attn_after = modeling_gemma4.Gemma4VisionAttention(cfg, layer_idx=0) + assert dict(getattr(attn_after, "_hidden_kernels", {})) == {} + + +def test_register_module_path_no_longer_crashes(restore_gemma4_vision_attention): + """The exact step that crashed: kernelize()'s ``attach_hidden_kernels`` + does ``register_module(name, fn)`` for each ``_hidden_kernels`` entry.""" + modeling_gemma4 = restore_gemma4_vision_attention + from axolotl.monkeypatch.gemma4_kernelize import patch_gemma4_kernelize + + cfg = _vision_config() + patch_gemma4_kernelize() + attn = modeling_gemma4.Gemma4VisionAttention(cfg, layer_idx=0) + + # Replicate attach_hidden_kernels; with the entry stripped there is nothing + # to (mis)register, so this must not raise. + for name, fn in getattr(attn, "_hidden_kernels", {}).items(): + if name not in dict(attn.named_children()): + attn.register_module(name, fn) + + +def test_patch_does_not_alter_weights(restore_gemma4_vision_attention): + """The shim only mutates ``_hidden_kernels``; parameters are untouched.""" + import torch + + modeling_gemma4 = restore_gemma4_vision_attention + from axolotl.monkeypatch.gemma4_kernelize import patch_gemma4_kernelize + + cfg = _vision_config() + torch.manual_seed(0) + before = modeling_gemma4.Gemma4VisionAttention(cfg, layer_idx=0).state_dict() + + patch_gemma4_kernelize() + torch.manual_seed(0) + after = modeling_gemma4.Gemma4VisionAttention(cfg, layer_idx=0).state_dict() + + assert before.keys() == after.keys() + assert all(torch.equal(before[k], after[k]) for k in before) + + +def test_forward_does_not_reference_stripped_entry(restore_gemma4_vision_attention): + """Behavior-invariance guarantee: forward never reads the stripped names, + so dropping them cannot change the forward result.""" + modeling_gemma4 = restore_gemma4_vision_attention + names = modeling_gemma4.Gemma4VisionAttention.forward.__code__.co_names + assert "apply_rotary_pos_emb" not in names + assert "_hidden_kernels" not in names + + +def test_unpatch_restores_original(restore_gemma4_vision_attention): + modeling_gemma4 = restore_gemma4_vision_attention + from axolotl.monkeypatch.gemma4_kernelize import ( + patch_gemma4_kernelize, + unpatch_gemma4_kernelize, + ) + + original = modeling_gemma4.Gemma4VisionAttention.__init__ + patch_gemma4_kernelize() + assert modeling_gemma4.Gemma4VisionAttention.__init__ is not original + unpatch_gemma4_kernelize() + assert modeling_gemma4.Gemma4VisionAttention.__init__ is original + + +def test_unpatch_is_safe_without_prior_patch(restore_gemma4_vision_attention): + from axolotl.monkeypatch.gemma4_kernelize import unpatch_gemma4_kernelize + + # No-op, no exception. + unpatch_gemma4_kernelize() + + +def test_full_model_kernelize_succeeds_with_patch(restore_gemma4_vision_attention): + """End-to-end: a tiny full Gemma4 model crashes in ``kernelize()`` without + the patch and succeeds with it. No real 26B weights or CUDA required.""" + modeling_gemma4 = restore_gemma4_vision_attention + from transformers.models.gemma4.configuration_gemma4 import ( + Gemma4AudioConfig, + Gemma4Config, + Gemma4TextConfig, + Gemma4VisionConfig, + ) + + from axolotl.monkeypatch.gemma4_kernelize import patch_gemma4_kernelize + + def build(): + text = Gemma4TextConfig( + hidden_size=32, + intermediate_size=64, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=8, + vocab_size=128, + num_experts=4, + num_experts_per_tok=2, + ) + vis = Gemma4VisionConfig( + hidden_size=32, + intermediate_size=64, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=8, + ) + aud = Gemma4AudioConfig( + hidden_size=32, + intermediate_size=64, + num_hidden_layers=1, + num_attention_heads=4, + ) + cfg = Gemma4Config(text_config=text, vision_config=vis, audio_config=aud) + return modeling_gemma4.Gemma4ForConditionalGeneration(cfg) + + # Without the patch, kernelize() crashes. + model = build() + model.train() + with pytest.raises((TypeError, AttributeError)): + model.kernelize() + + # With the patch, it succeeds. + patch_gemma4_kernelize() + model = build() + model.train() + model.kernelize() From d452e65103b8bb1d2ba8430b3a899e6f7daeef2b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 27 May 2026 23:11:18 -0400 Subject: [PATCH 1324/1405] update to use same as latest it gemma4 chat template (#3686) [skip ci] --- .../chat_templates/templates/gemma4.jinja | 302 ++++++++++++------ 1 file changed, 197 insertions(+), 105 deletions(-) diff --git a/src/axolotl/utils/chat_templates/templates/gemma4.jinja b/src/axolotl/utils/chat_templates/templates/gemma4.jinja index 780957c941..4c79e38276 100644 --- a/src/axolotl/utils/chat_templates/templates/gemma4.jinja +++ b/src/axolotl/utils/chat_templates/templates/gemma4.jinja @@ -1,9 +1,9 @@ -{%- macro format_parameters(properties, required) -%} +{%- macro format_parameters(properties, required, filter_keys=false) -%} {%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%} {%- set ns = namespace(found_first=false) -%} {%- for key, value in properties | dictsort -%} {%- set add_comma = false -%} - {%- if key not in standard_keys -%} + {%- if not filter_keys or key not in standard_keys -%} {%- if ns.found_first %},{% endif -%} {%- set ns.found_first = true -%} {{ key }}:{ @@ -11,34 +11,15 @@ description:<|"|>{{ value['description'] }}<|"|> {%- set add_comma = true -%} {%- endif -%} - {%- if value['nullable'] %} - {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} - nullable:true - {%- endif -%} {%- if value['type'] | upper == 'STRING' -%} {%- if value['enum'] -%} {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} enum:{{ format_argument(value['enum']) }} {%- endif -%} - {%- elif value['type'] | upper == 'OBJECT' -%} - ,properties:{ - {%- if value['properties'] is defined and value['properties'] is mapping -%} - {{- format_parameters(value['properties'], value['required'] | default([])) -}} - {%- elif value is mapping -%} - {{- format_parameters(value, value['required'] | default([])) -}} - {%- endif -%} - } - {%- if value['required'] -%} - ,required:[ - {%- for item in value['required'] | default([]) -%} - <|"|>{{- item -}}<|"|> - {%- if not loop.last %},{% endif -%} - {%- endfor -%} - ] - {%- endif -%} {%- elif value['type'] | upper == 'ARRAY' -%} {%- if value['items'] is mapping and value['items'] -%} - ,items:{ + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + items:{ {%- set ns_items = namespace(found_first=false) -%} {%- for item_key, item_value in value['items'] | dictsort -%} {%- if item_value is not none -%} @@ -71,6 +52,32 @@ } {%- endif -%} {%- endif -%} + {%- if value['nullable'] %} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + nullable:true + {%- endif -%} + {%- if value['type'] | upper == 'OBJECT' -%} + {%- if value['properties'] is defined and value['properties'] is mapping -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + properties:{ + {{- format_parameters(value['properties'], value['required'] | default([])) -}} + } + {%- elif value is mapping -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + properties:{ + {{- format_parameters(value, value['required'] | default([]), filter_keys=true) -}} + } + {%- endif -%} + {%- if value['required'] -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + required:[ + {%- for item in value['required'] | default([]) -%} + <|"|>{{- item -}}<|"|> + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + ] + {%- endif -%} + {%- endif -%} {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} type:<|"|>{{ value['type'] | upper }}<|"|>} {%- endif -%} @@ -138,39 +145,54 @@ {{- argument -}} {%- endif -%} {%- endmacro -%} -{#- Removes '<|channel>...' thinking blocks from model output. - Splits on the end token '', then checks each part for the start - token '<|channel>' and keeps only the text before it. -#} {%- macro strip_thinking(text) -%} - {%- set ns = namespace(cleaned='') -%} + {%- set ns = namespace(result='') -%} {%- for part in text.split('') -%} {%- if '<|channel>' in part -%} - {%- set ns.cleaned = ns.cleaned + part.split('<|channel>')[0] -%} + {%- set ns.result = ns.result + part.split('<|channel>')[0] -%} {%- else -%} - {%- set ns.cleaned = ns.cleaned + part -%} + {%- set ns.result = ns.result + part -%} {%- endif -%} {%- endfor -%} - {{- ns.cleaned | trim -}} + {{- ns.result | trim -}} +{%- endmacro -%} + +{%- macro format_tool_response_block(tool_name, response) -%} + {{- '<|tool_response>' -}} + {%- if response is mapping -%} + {{- 'response:' + tool_name + '{' -}} + {%- for key, value in response | dictsort -%} + {{- key -}}:{{- format_argument(value, escape_keys=False) -}} + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + {{- '}' -}} + {%- else -%} + {{- 'response:' + tool_name + '{value:' + format_argument(response, escape_keys=False) + '}' -}} + {%- endif -%} + {{- '' -}} {%- endmacro -%} {%- set ns = namespace(prev_message_type=None) -%} {%- set loop_messages = messages -%} -{{ bos_token }} +{{- bos_token -}} {#- Handle System/Tool Definitions Block -#} {%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%} {{- '<|turn>system\n' -}} - {#- Inject Thinking token at the very top of the FIRST system turn -#} {%- if enable_thinking is defined and enable_thinking -%} - {{- '<|think|>' -}} + {{- '<|think|>\n' -}} {%- set ns.prev_message_type = 'think' -%} {%- endif -%} - {%- if messages[0]['role'] in ['system', 'developer'] -%} - {{- messages[0]['content'] | trim -}} + {%- if messages[0]['content'] is string -%} + {{- messages[0]['content'] | trim -}} + {%- elif messages[0]['content'] is sequence -%} + {%- for item in messages[0]['content'] -%} + {{- item['text'] | trim + ' '-}} + {%- endfor -%} + {%- endif -%} {%- set loop_messages = messages[1:] -%} {%- endif -%} - {%- if tools -%} {%- for tool in tools %} {{- '<|tool>' -}} @@ -179,93 +201,163 @@ {%- endfor %} {%- set ns.prev_message_type = 'tool' -%} {%- endif -%} - {{- '\n' -}} {%- endif %} +{#- Pre-scan: find last user message index for reasoning guard -#} +{%- set ns_turn = namespace(last_user_idx=-1) -%} +{%- for i in range(loop_messages | length) -%} + {%- if loop_messages[i]['role'] == 'user' -%} + {%- set ns_turn.last_user_idx = i -%} + {%- endif -%} +{%- endfor -%} + {#- Loop through messages -#} {%- for message in loop_messages -%} - {#- Reset so only special message types (tool_call, image, etc.) influence - the generation prompt formatting below. Plain text leaves it as None. -#} - {%- set ns.prev_message_type = None -%} - {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%} - {{- '<|turn>' + role + '\n' }} - - {%- if message['tool_calls'] -%} - {%- for tool_call in message['tool_calls'] -%} - {%- set function = tool_call['function'] -%} - {{- '<|tool_call>call:' + function['name'] + '{' -}} - {%- if function['arguments'] is mapping -%} - {%- set ns_args = namespace(found_first=false) -%} - {%- for key, value in function['arguments'] | dictsort -%} - {%- if ns_args.found_first %},{% endif -%} - {%- set ns_args.found_first = true -%} - {{- key -}}:{{- format_argument(value, escape_keys=False) -}} - {%- endfor -%} - {%- elif function['arguments'] is string -%} - {{- function['arguments'] -}} - {%- endif -%} - {{- '}' -}} - {%- endfor -%} - {%- set ns.prev_message_type = 'tool_call' -%} +{%- if message['role'] != 'tool' -%} +{%- set ns.prev_message_type = None -%} +{%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%} +{#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#} +{%- set prev_nt = namespace(role=None, found=false) -%} +{%- if loop.index0 > 0 -%} + {%- for j in range(loop.index0 - 1, -1, -1) -%} + {%- if not prev_nt.found -%} + {%- if loop_messages[j]['role'] != 'tool' -%} + {%- set prev_nt.role = loop_messages[j]['role'] -%} + {%- set prev_nt.found = true -%} {%- endif -%} + {%- endif -%} + {%- endfor -%} +{%- endif -%} +{%- set continue_same_model_turn = (role == 'model' and prev_nt.role == 'assistant') -%} +{%- if not continue_same_model_turn -%} + {{- '<|turn>' + role + '\n' }} +{%- endif -%} - {%- if message['tool_responses'] -%} - {#- Tool Response handling -#} - {%- for tool_response in message['tool_responses'] -%} - {{- '<|tool_response>' -}} - {%- if tool_response['response'] is mapping -%} - {{- 'response:' + tool_response['name'] | default('unknown') + '{' -}} - {%- for key, value in tool_response['response'] | dictsort -%} - {{- key -}}:{{- format_argument(value, escape_keys=False) -}} - {%- if not loop.last %},{% endif -%} - {%- endfor -%} - {{- '}' -}} - {%- else -%} - {{- 'response:' + tool_response['name'] | default('unknown') + '{value:' + format_argument(tool_response['response'], escape_keys=False) + '}' -}} - {%- endif -%} - {{- '' -}} - {%- endfor -%} - {%- set ns.prev_message_type = 'tool_response' -%} - {%- endif -%} +{#- Render reasoning/reasoning_content as thinking channel -#} +{%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%} +{%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%} + {{- '<|channel>thought\n' + thinking_text + '\n' -}} +{%- endif -%} + +{%- if message['tool_calls'] -%} + {%- for tool_call in message['tool_calls'] -%} + {%- set function = tool_call['function'] -%} + {{- '<|tool_call>call:' + function['name'] + '{' -}} + {%- if function['arguments'] is mapping -%} + {%- set ns_args = namespace(found_first=false) -%} + {%- for key, value in function['arguments'] | dictsort -%} + {%- if ns_args.found_first %},{% endif -%} + {%- set ns_args.found_first = true -%} + {{- key -}}:{{- format_argument(value, escape_keys=False) -}} + {%- endfor -%} + {%- elif function['arguments'] is string -%} + {{- function['arguments'] -}} + {%- endif -%} + {{- '}' -}} + {%- endfor -%} + {%- set ns.prev_message_type = 'tool_call' -%} +{%- endif -%} - {%- if message['content'] is string -%} - {%- if role == 'model' -%} - {{- strip_thinking(message['content']) -}} - {%- else -%} - {{- message['content'] | trim -}} +{%- set ns_tr_out = namespace(flag=false) -%} +{%- if message.get('tool_responses') -%} + {#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#} + {%- for tool_response in message['tool_responses'] -%} + {{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}} + {%- set ns_tr_out.flag = true -%} + {%- set ns.prev_message_type = 'tool_response' -%} + {%- endfor -%} +{%- elif message.get('tool_calls') -%} + {#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#} + {%- set ns_tool_scan = namespace(stopped=false) -%} + {%- for k in range(loop.index0 + 1, loop_messages | length) -%} + {%- if ns_tool_scan.stopped -%} + {%- elif loop_messages[k]['role'] != 'tool' -%} + {%- set ns_tool_scan.stopped = true -%} + {%- else -%} + {%- set follow = loop_messages[k] -%} + {#- Resolve tool_call_id to function name -#} + {%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%} + {%- for tc in message['tool_calls'] -%} + {%- if tc.get('id') == follow.get('tool_call_id') -%} + {%- set ns_tname.name = tc['function']['name'] -%} {%- endif -%} - {%- elif message['content'] is sequence -%} - {%- for item in message['content'] -%} - {%- if item['type'] == 'text' -%} - {%- if role == 'model' -%} - {{- strip_thinking(item['text']) -}} - {%- else -%} - {{- item['text'] | trim -}} - {%- endif -%} - {%- elif item['type'] == 'image' -%} - {{- '\n\n<|image|>\n\n' -}} - {%- set ns.prev_message_type = 'image' -%} - {%- elif item['type'] == 'audio' -%} + {%- endfor -%} + {#- Handle content as string or content-parts array -#} + {%- set tool_body = follow.get('content') -%} + {%- if tool_body is string -%} + {{- format_tool_response_block(ns_tname.name, tool_body) -}} + {%- elif tool_body is sequence and tool_body is not string -%} + {%- set ns_txt = namespace(s='') -%} + {%- for part in tool_body -%} + {%- if part.get('type') == 'text' -%} + {%- set ns_txt.s = ns_txt.s + (part.get('text') | default('')) -%} + {%- endif -%} + {%- endfor -%} + {{- format_tool_response_block(ns_tname.name, ns_txt.s) -}} + {%- for part in tool_body -%} + {%- if part.get('type') == 'image' -%} + {{- '<|image|>' -}} + {%- elif part.get('type') == 'audio' -%} {{- '<|audio|>' -}} - {%- set ns.prev_message_type = 'audio' -%} - {%- elif item['type'] == 'video' -%} - {{- '\n\n<|video|>\n\n' -}} - {%- set ns.prev_message_type = 'video' -%} + {%- elif part.get('type') == 'video' -%} + {{- '<|video|>' -}} {%- endif -%} {%- endfor -%} + {%- else -%} + {{- format_tool_response_block(ns_tname.name, tool_body) -}} {%- endif -%} + {%- set ns_tr_out.flag = true -%} + {%- set ns.prev_message_type = 'tool_response' -%} + {%- endif -%} + {%- endfor -%} +{%- endif -%} - {%- if not (message['tool_responses'] and not message['content']) -%} - {{- '\n' -}} +{%- set captured_content -%} +{%- if message['content'] is string -%} + {%- if role == 'model' -%} + {{- strip_thinking(message['content']) -}} + {%- else -%} + {{- message['content'] | trim -}} + {%- endif -%} +{%- elif message['content'] is sequence -%} + {%- for item in message['content'] -%} + {%- if item['type'] == 'text' -%} + {%- if role == 'model' -%} + {{- strip_thinking(item['text']) -}} + {%- else -%} + {{- item['text'] | trim -}} + {%- endif -%} + {%- elif item['type'] == 'image' -%} + {{- '<|image|>' -}} + {%- set ns.prev_message_type = 'image' -%} + {%- elif item['type'] == 'audio' -%} + {{- '<|audio|>' -}} + {%- set ns.prev_message_type = 'audio' -%} + {%- elif item['type'] == 'video' -%} + {{- '<|video|>' -}} + {%- set ns.prev_message_type = 'video' -%} {%- endif -%} + {%- endfor -%} +{%- endif -%} +{%- endset -%} + +{{- captured_content -}} +{%- set has_content = captured_content | trim | length > 0 -%} + +{%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%} + {{- '<|tool_response>' -}} +{%- elif not (ns_tr_out.flag and not has_content) -%} + {{- '\n' -}} +{%- endif -%} +{%- endif -%} {%- endfor -%} {%- if add_generation_prompt -%} - {%- if ns.prev_message_type != 'tool_response' -%} + {%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%} {{- '<|turn>model\n' -}} - {%- endif -%} - {%- if not enable_thinking | default(false) -%} - {{- '<|channel>thought\n' -}} + {%- if not enable_thinking | default(false) -%} + {{- '<|channel>thought\n' -}} + {%- endif -%} {%- endif -%} {%- endif -%} From 3f478db6c78145df58bc1dbcc169b0513072359c Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Thu, 28 May 2026 10:13:42 +0700 Subject: [PATCH 1325/1405] fix: refactor kernels patch to drop routing and inject into Expert (#3651) * fix: refactor kernels patch to drop routing and inject into Expert registry * chore: add to optim doc * feat: update sonicmoe version * chore: cleanup with DEEPEP and kernels compat * gate/guard model expert setup --------- Co-authored-by: Wing Lian --- docs/optimizations.qmd | 9 +- .../expert_parallel/experts_fn.py | 9 +- src/axolotl/integrations/kernels/README.md | 174 ++-- src/axolotl/integrations/kernels/args.py | 63 +- src/axolotl/integrations/kernels/constants.py | 105 +-- .../{gemma4_experts.py => experts.py} | 73 +- .../kernels/libs/sonicmoe/__init__.py | 7 +- .../kernels/libs/sonicmoe/experts.py | 143 +++ .../kernels/libs/sonicmoe/gemma4_experts.py | 106 --- .../kernels/libs/sonicmoe/lora.py | 43 +- .../kernels/libs/sonicmoe/patch.py | 272 ------ .../kernels/libs/sonicmoe/routing.py | 576 ------------ .../kernels/libs/sonicmoe/weight_converter.py | 202 ---- src/axolotl/integrations/kernels/plugin.py | 151 +-- src/axolotl/loaders/model.py | 19 +- tests/e2e/integrations/test_sonicmoe.py | 260 ++---- tests/e2e/integrations/test_sonicmoe_lora.py | 207 ++--- tests/integrations/test_gemma4_moe.py | 104 +-- tests/integrations/test_routing_parity.py | 492 ---------- tests/integrations/test_sonicmoe.py | 874 +++--------------- tests/integrations/test_sonicmoe_gradients.py | 158 ---- tests/integrations/test_sonicmoe_lora.py | 49 - 22 files changed, 660 insertions(+), 3436 deletions(-) rename src/axolotl/integrations/kernels/libs/scattermoe_lora/{gemma4_experts.py => experts.py} (72%) create mode 100644 src/axolotl/integrations/kernels/libs/sonicmoe/experts.py delete mode 100644 src/axolotl/integrations/kernels/libs/sonicmoe/gemma4_experts.py delete mode 100644 src/axolotl/integrations/kernels/libs/sonicmoe/patch.py delete mode 100644 src/axolotl/integrations/kernels/libs/sonicmoe/routing.py delete mode 100644 src/axolotl/integrations/kernels/libs/sonicmoe/weight_converter.py delete mode 100644 tests/integrations/test_routing_parity.py delete mode 100644 tests/integrations/test_sonicmoe_gradients.py diff --git a/docs/optimizations.qmd b/docs/optimizations.qmd index 720519ec03..621c2c0bcd 100644 --- a/docs/optimizations.qmd +++ b/docs/optimizations.qmd @@ -75,11 +75,12 @@ Provides efficient Triton kernels to improve training speed and reduce memory us ### Expert Kernels -Optimized kernel implementations for Mixture of Experts (MoE) model training. +Optimized per-expert grouped-GEMM kernels for MoE training, with LoRA support. -- **ScatterMoE**: Triton-based MoE kernels with fused LoRA support. -- **SonicMoE**: CUTLASS-based MoE kernels for NVIDIA Hopper and Blackwell GPUs. +- **ScatterMoE**: Triton, any CUDA GPU. +- **SonicMoE**: CUTLASS / cute-DSL, Hopper+ only. +- **Config:** `use_scattermoe: true` or `use_sonicmoe: true` - **Learn more:** [Custom Integrations - Kernels Integration](custom_integrations.qmd#kernels-integration) ## Long Context Models @@ -117,7 +118,7 @@ To train models that don't fit on a single GPU, you'll need to use a distributed ### N-D Parallelism (Beta) -For advanced scaling, Axolotl allows you to compose different parallelism techniques (e.g., Data, Tensor, Sequence Parallelism). This is a powerful approach to train an extremely large model by overcoming multiple bottlenecks at once. +For advanced scaling, Axolotl allows you to compose different parallelism techniques (e.g., Data, Tensor, Sequence, Expert Parallelism). This is a powerful approach to train an extremely large model by overcoming multiple bottlenecks at once. - **Learn more:** [N-D Parallelism Guide](nd_parallelism.qmd) diff --git a/src/axolotl/integrations/expert_parallel/experts_fn.py b/src/axolotl/integrations/expert_parallel/experts_fn.py index 1cc81ef293..89473fd112 100644 --- a/src/axolotl/integrations/expert_parallel/experts_fn.py +++ b/src/axolotl/integrations/expert_parallel/experts_fn.py @@ -59,7 +59,7 @@ def _grouped_mm_local(experts, recv_x, recv_topk_idx, recv_topk_weights): def _scattermoe_local(experts, recv_x, recv_topk_idx, recv_topk_weights): - from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( scattermoe_experts_forward, ) @@ -68,13 +68,6 @@ def _scattermoe_local(experts, recv_x, recv_topk_idx, recv_topk_weights): def _sonicmoe_local(experts, recv_x, recv_topk_idx, recv_topk_weights): raise NotImplementedError("Sonicmoe + EP is not yet properly implemented.") - # from axolotl.integrations.kernels.libs.sonicmoe.gemma4_experts import ( - # gemma4_sonicmoe_experts_forward, - # ) - - # return gemma4_sonicmoe_experts_forward( - # experts, recv_x, recv_topk_idx, recv_topk_weights - # ) _LOCAL_KERNELS = { diff --git a/src/axolotl/integrations/kernels/README.md b/src/axolotl/integrations/kernels/README.md index 32d236da49..aae2fee97f 100644 --- a/src/axolotl/integrations/kernels/README.md +++ b/src/axolotl/integrations/kernels/README.md @@ -1,16 +1,17 @@ # Kernels Integration -MoE (Mixture of Experts) kernels speed up training for MoE layers and reduce VRAM costs. In transformers v5, `batched_mm` and `grouped_mm` were integrated as built-in options via the `experts_implementation` config kwarg: +MoE (Mixture of Experts) kernels speed up training for MoE layers and reduce VRAM costs. Transformers v5 introduced a uniform dispatch point for the per-expert grouped GEMMs via the `experts_implementation` config kwarg: ```python class ExpertsInterface(GeneralInterface): _global_mapping = { "batched_mm": batched_mm_experts_forward, "grouped_mm": grouped_mm_experts_forward, + "sonicmoe": sonicmoe_experts_forward, # upstream HF integration } ``` -In our custom integration, we add support for **ScatterMoE** and **SonicMoE**, which are more efficient and faster than `grouped_mm`. +Axolotl registers two additional implementations into this same global registry: **ScatterMoE** (Triton, runs on any CUDA GPU) and a LoRA-aware **SonicMoE** variant (CUTLASS / cute-DSL, Hopper or newer). Routing — softmax/sigmoid top-k, group selection, shared experts, bias correction, etc. — stays in each model's `SparseMoEBlock`, where transformers handles all per-architecture variation. Axolotl only swaps the experts forward. ## Usage @@ -28,130 +29,75 @@ use_scattermoe: true use_sonicmoe: true ``` -**Important:** Setting `experts_implementation` to `batched_mm` or `grouped_mm` is incompatible with custom kernel options. The exception is `experts_implementation: scattermoe`, which is used for models like Gemma 4 that embed MoE directly in the decoder layer (no SparseMoeBlock) and dispatch through the transformers `ExpertsInterface`. +`experts_implementation` is auto-set to `scattermoe` / `sonicmoe` from the kernel flag, but you can override to `eager` / `batched_mm` / `grouped_mm` to compare against the transformers reference implementations. ### SonicMoE installation **Prerequisites:** -- NVIDIA Hopper (H100, H200) or Blackwell (B200, GB200) GPU +- NVIDIA Hopper (H100/H200) or Blackwell (B200/GB200/B300) GPU - CUDA 12.9+ (13.0+ for B300) -- PyTorch 2.7+ (2.9.1 recommended) -- For B300: Triton 3.6.0 +- PyTorch 2.7+ +- For B300: Triton 3.6.x + +The sonic-moe kernel ships through the HF [`kernels`](https://github.com/huggingface/kernels) package. Transformers v5.8+ auto-fetches a prebuilt kernel from [`kernels-community/sonic-moe`](https://huggingface.co/kernels-community/sonic-moe) on first use: ```bash -pip install --ignore-requires-python --no-deps "sonic-moe @ git+https://github.com/Dao-AILab/sonic-moe.git@116e2df0a41874f77fa0ad269ce7df3f0cfcb956" && pip install nvidia-cutlass-dsl==4.4.0 quack-kernels==0.2.5 +pip install kernels "nvidia-cutlass-dsl==4.4.2" ``` -See the [SonicMoE installation guide](https://github.com/Dao-AILab/sonic-moe?tab=readme-ov-file#-installation) for the latest prerequisite details. - -**Note:** Blackwell support is in upstream beta. On Blackwell GPUs, Axolotl automatically sets `USE_QUACK_GEMM=1` to enable the Blackwell kernels. +**Note:** Blackwell support is in upstream beta. On Blackwell GPUs Axolotl automatically sets `USE_QUACK_GEMM=1` to enable the Blackwell kernels. ## How It Works -The `KernelsPlugin` runs before model loading and: - -### ScatterMoE -1. Registers the ScatterMoE kernel from the local `libs/scattermoe_lora` package (includes fused LoRA support via Triton kernels). -2. Patches the model's `SparseMoeBlock` forward method with the optimized ScatterMoE implementation via the HF `kernels` library. - -### SonicMoE -1. Resolves the model's MoE block class(es) from `constants.py`. -2. Patches the forward method with SonicMoE's optimized CUTLASS kernels and registers a weight converter for the interleaved gate/up projection format. -3. Supports pluggable routing strategies (see routing table below). - -Both paths use the shared `resolve_moe_block_classes` utility in `constants.py` for model-type-to-class resolution. - -## Model Support Matrix - -Most models use the **SwiGLU** activation (`silu(gate) * up`). Gemma 4 uses **GEGLU** (`gelu(gate) * up`). ScatterMoE supports any gated activation (activation is applied in Python between kernel calls). SonicMoE supports SwiGLU, GEGLU, and REGLU via its `ActivationType` enum. - -### Routing strategies - -| Routing Strategy | Description | ScatterMoE | SonicMoE | -|---|---|:---:|:---:| -| softmax → topk | Softmax over experts, select top-K, optional renormalization | Yes | Yes | -| softmax → group selection → topk | Softmax, select top groups (sum of top-2 per group), topk from selected groups, renorm + scaling | No | Yes | -| sigmoid → topk (with groups) | Sigmoid + bias correction, group-based masking, topk from masked scores, weights from original sigmoid | Yes | Yes | -| sigmoid → topk (no groups) | Sigmoid + bias correction, straight topk (n_group=1) | Yes | Yes | -| softmax → bias correction → topk | Softmax, bias via `gate.moe_statics`, topk, gather from original probs, clamp-based renorm | No | Yes | -| softmax → group_limited_greedy | Softmax, group selection (max per group), topk, scale only (no renorm) | No | Yes | -| softmax → topk via gate.wg | Softmax, gate weight at `gate.wg.weight` (not `gate.weight`), always renormalize | No | Yes | -| softmax → topk + per_expert_scale | RMSNorm → scale → proj → softmax → topk → renorm → per-expert learned scales | Yes | Yes | -| fused topk → softmax | Routing + expert computation fused in a single kernel | No | Planned | - -### Per-model support - -| Model Type | Architecture | Routing | ScatterMoE | SonicMoE | -|---|---|---|:---:|:---:| -| `qwen2_moe` | Qwen2-MoE | softmax → topk | **Yes** | **Yes** | -| `qwen3_moe` | Qwen3-MoE | softmax → topk | **Yes** | **Yes** | -| `qwen3_5_moe` | Qwen3.5-MoE | softmax → topk | **Yes** | **Yes** | -| `qwen3_5_moe_text` | Qwen3.5-MoE (VLM text) | softmax → topk | **Yes** | **Yes** | -| `qwen3_next` | Qwen3-Next | softmax → topk | **Yes** | **Yes** | -| `qwen3_vl_moe` | Qwen3-VL-MoE | softmax → topk | **Yes** | **Yes** | -| `qwen3_omni_moe` | Qwen3-Omni (Thinker + Talker) | softmax → topk | **Yes** | **Yes** | -| `olmoe` | OLMoE | softmax → topk | **Yes** | **Yes** | -| `mixtral` | Mixtral | softmax → topk | **Yes** | **Yes** | -| `minimax` | MiniMax | softmax → topk | **Yes** | **Yes** | -| `mistral4` | Mistral 4 | softmax → group → topk | No | **Yes** | -| `glm_moe_dsa` | GLM-MoE DSA (GLM 5) | sigmoid → topk (groups) | **Yes** | **Yes** | -| `deepseek_v3` | DeepSeek-V3 | sigmoid → topk (groups) | **Yes** | **Yes** | -| `glm4_moe` | GLM4-MoE | sigmoid → topk (groups) | **Yes** | **Yes** | -| `glm4_moe_lite` | GLM4-MoE Lite (GLM 4.7 Flash) | sigmoid → topk (groups) | **Yes**\* | **Yes** | -| `glm4v_moe` | GLM4v-MoE | sigmoid → topk (groups) | **Yes** | **Yes** | -| `minimax_m2` | MiniMax M2 | sigmoid → topk (no groups) | **Yes** | **Yes** | -| `ernie4_5_moe` | ERNIE 4.5 MoE | softmax → bias → topk | No | **Yes** | -| `deepseek_v2` | DeepSeek-V2 | softmax → group_limited_greedy | No | **Yes** | -| `hunyuan_v1_moe` | HunYuan V1 MoE | softmax → topk (gate.wg) | No | **Yes** | -| `gemma4_text` | Gemma 4 (26B-A4B) | softmax → topk + per_expert_scale | **Yes**\*\* | **Yes**\*\* | -| `gpt_oss` | GPT-OSS | fused topk → softmax | No | Planned | - -\* `glm4_moe_lite` with ScatterMoE may have issues — see Limitations. - -\*\* Gemma 4 uses `experts_implementation: scattermoe` path (registered via `ExpertsInterface`) instead of SparseMoeBlock patching, since Gemma 4 embeds MoE directly in its decoder layer (no separate SparseMoeBlock). See the [Gemma 4 section](#gemma-4) below. - -### Feature comparison - -| Feature | ScatterMoE | SonicMoE | -|---|:---:|:---:| -| Kernel backend | Triton | CUTLASS | -| GPU requirement | Any CUDA | Hopper (H100/H200) or Blackwell (B200+) | -| LoRA approach | Fused in Triton kernel | Runtime materialization + custom autograd | -| LoRA overhead | Lower (fused computation) | Higher (per-forward materialization) | -| Gate/router LoRA | Yes | Yes | -| Expert LoRA | Yes (fused) | Yes (materialized) | -| Shared expert LoRA | Yes (standard PEFT) | Yes (standard PEFT) | -| Selective expert dequantization | Yes (~97% memory savings) | No | -| Weight format | Transposed `[E, hidden, 2*inter]` | Interleaved gate/up `[2*I, H, E]` | -| torch.compile routing | No | Yes (optional) | - -## Shared Expert Handling - -Both kernels handle shared experts identically. Shared expert attribute names are detected in order of priority: - -1. `shared_expert` (Qwen2-MoE) -2. `shared_experts` (GLM-MoE, DeepSeek-V3) -3. `shared_mlp` (HunYuan V1 MoE) - -If `shared_expert_gate` exists, sigmoid gating is applied to the shared expert contribution before adding it to the routed output. PEFT wraps shared expert linear layers with standard LoRA — no special handling is needed. - -## Gemma 4 - -Gemma 4 (e.g. `google/gemma-4-26B-A4B`) has a unique hybrid MoE architecture: - -- **No SparseMoeBlock**: MoE is embedded directly in the decoder layer alongside a dense MLP. Both run in parallel and their outputs are summed. -- **Custom router** (`Gemma4TextRouter`): RMSNorm → learned scale → linear projection → softmax → top-k → renormalization → per-expert learned scales. -- **GEGLU activation**: Uses `gelu_pytorch_tanh` (not SiLU/SwiGLU like most other MoE models). -- **128 experts, top-k=8** for the 26B-A4B variant. - -Because there is no SparseMoeBlock class to patch, Gemma 4 uses a different integration path: we register `"scattermoe"` as a custom implementation in the transformers `ExpertsInterface`, and set `experts_implementation: scattermoe` in the config. The `@use_experts_implementation` decorator on `Gemma4TextExperts` then dispatches to our ScatterMoE kernel automatically. The router is untouched — it runs as-is. - -## Limitations - -- **ScatterMoE + GLM4-MoE Lite**: ScatterMoE does not work reliably for GLM 4.7 Flash (`glm4_moe_lite`). -- **Non-SwiGLU activations**: Neither kernel supports MoE architectures with non-SwiGLU expert activations (e.g., GPT-OSS uses a custom GLU variant). -- **GPT-OSS**: Deferred — requires transposed weight layout `[E, H, 2*I]`, expert biases, and custom GLU activation. A dedicated forward path is needed. -- **FSDP + fused gate LoRA (SonicMoE)**: The fused topk→softmax path materializes a local tensor when LoRA delta is present to avoid DTensor + Tensor mixing under FSDP. +The `KernelsPlugin` runs once before model loading and: + +1. Calls `register_scattermoe_experts()` or `register_sonicmoe_experts()`, which inserts the kernel forward into `transformers.integrations.moe.ALL_EXPERTS_FUNCTIONS`. +2. Sets `cfg.experts_implementation` to the matching name. +3. When the model loads, transformers' `@use_experts_implementation` decorator on each model's `Experts` class reads `config._experts_implementation` and dispatches to our registered forward. + +That's the entire integration — there is no per-architecture SparseMoEBlock monkey-patch, no per-model routing code, and no weight-layout conversion. As new MoE models adopt the decorator upstream they immediately benefit from both kernels. + +## LoRA Support + +Both kernels train PEFT adapters on `gate_up_proj` / `down_proj` (and `gate` for the router) end-to-end: + +- **ScatterMoE** fuses the LoRA `B @ A` product into the per-expert grouped GEMM via custom Triton kernels (`parallel_linear_lora`). No extra materialization pass. +- **SonicMoE** materializes `W_eff = W + scaling * (B @ A)` per expert inside a custom `MoELoRAMaterialize` `autograd.Function` and passes the effective weight into the CUTLASS kernel. Backward decomposes `dW_eff` into `dA` and `dB` via the chain rule, so LoRA parameters train without modifying the kernel. + +Both paths detect PEFT `ParamWrapper` on individual expert parameters (`target_parameters` API) and unwrap them before dispatch. + +## Model Support + +Any model whose `Experts` class is decorated with `@use_experts_implementation` upstream works automatically. As of transformers 5.8 this includes (verified): + +| Model Type | ScatterMoE | SonicMoE | +|-------------------|:---------:|:--------:| +| `mixtral` | Yes | Yes | +| `qwen2_moe` | Yes | Yes | +| `qwen3_moe` | Yes | Yes | +| `qwen3_5_moe` | Yes | Yes | +| `olmoe` | Yes | Yes | +| `mistral4` | Yes | Yes | +| `glm_moe_dsa` | Yes | Yes | +| `deepseek_v3` | Yes | Yes | +| `minimax_m2` | Yes | Yes | +| `ernie4_5_moe` | Yes | Yes | +| `hunyuan_v1_moe` | Yes | Yes | +| `gemma4_text` | Yes | Yes | +| `gpt_oss` | No | Yes | + +`gpt_oss` carries the decorator with `is_concatenated=False, is_transposed=True, has_bias=True` and uses a sigmoid-GLU activation with clamping. The SonicMoE forward reads these flags off `self` and dispatches accordingly. The ScatterMoE forward assumes the standard `[E, 2*I, H]` concat layout and SiLU-GLU without bias, so it does not yet support `gpt_oss`. + +## Feature comparison + +| Feature | ScatterMoE | SonicMoE | +|----------------------------------|:----------:|:--------:| +| Kernel backend | Triton | CUTLASS / cute-DSL | +| GPU requirement | Any CUDA | Hopper+ | +| LoRA path | Fused in Triton kernel | `MoELoRAMaterialize` + custom autograd | +| LoRA overhead | Lower (fused) | Higher (materialization pass) | +| Selective expert dequantization | Yes (~97% memory savings) | No | +| Weight format | Standard `[E, 2*I, H]` | Standard `[E, 2*I, H]` (concat layout, no interleave) | ## Note on MegaBlocks diff --git a/src/axolotl/integrations/kernels/args.py b/src/axolotl/integrations/kernels/args.py index f532fde417..2cf6d2f3ba 100644 --- a/src/axolotl/integrations/kernels/args.py +++ b/src/axolotl/integrations/kernels/args.py @@ -5,6 +5,26 @@ LOG = get_logger(__name__) +# Valid experts_implementation values: +# - "eager" : transformers' per-token loop reference implementation +# - "batched_mm" : transformers' built-in batched matmul path +# - "grouped_mm" : transformers' built-in grouped matmul path (cache-efficient) +# - "scattermoe" : axolotl-registered Triton kernels with LoRA support +# - "sonicmoe" : axolotl-registered CUTLASS / cute-DSL kernels with LoRA support +# - "deep_ep[_*]": EP-plugin composites; passed through when expert_parallel_size > 1 +_BUILTIN_EXPERTS_IMPLS = {"eager", "batched_mm", "grouped_mm"} +_KERNEL_EXPERTS_IMPLS = {"scattermoe", "sonicmoe"} +_EP_EXPERTS_IMPLS = { + "deep_ep", + "deep_ep_grouped_mm", + "deep_ep_scattermoe", + "deep_ep_sonicmoe", +} +_VALID_EXPERTS_IMPLS = ( + _BUILTIN_EXPERTS_IMPLS | _KERNEL_EXPERTS_IMPLS | _EP_EXPERTS_IMPLS +) + + class KernelsArgs(BaseModel): use_scattermoe: bool | None = None use_sonicmoe: bool | None = None @@ -30,24 +50,53 @@ def check_use_kernels(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_sonicmoe_ep_unsupported(cls, data): + """SonicMoE + EP is not yet implemented (EP `_sonicmoe_local` raises).""" + if data.get("use_sonicmoe") and (data.get("expert_parallel_size") or 1) > 1: + raise ValueError( + "use_sonicmoe=true is not supported with expert_parallel_size > 1. " + "Use use_scattermoe=true under EP, or set expert_parallel_size=1." + ) + return data + @model_validator(mode="before") @classmethod def check_experts_implementation(cls, data): + """Auto-select impl from kernel flags; reject mismatched/unknown values.""" experts_implementation = data.get("experts_implementation") - use_scattermoe = data.get("use_scattermoe", False) + use_scattermoe = bool(data.get("use_scattermoe")) + use_sonicmoe = bool(data.get("use_sonicmoe")) + if experts_implementation is None: - # transformers may default to batched_mm when unset - data["experts_implementation"] = "eager" - elif experts_implementation == "scattermoe" and not use_scattermoe: + if use_scattermoe: + data["experts_implementation"] = "scattermoe" + elif use_sonicmoe: + data["experts_implementation"] = "sonicmoe" + else: + # Transformers defaults to a non-eager backend when unset; pin to + # eager unless the user explicitly opts in. + data["experts_implementation"] = "eager" + return data + + if experts_implementation == "scattermoe" and not use_scattermoe: LOG.warning( "`experts_implementation='scattermoe'` requires `use_scattermoe: true`. " "Automatically setting to 'eager'." ) data["experts_implementation"] = "eager" - elif experts_implementation not in ("eager", "scattermoe"): + elif experts_implementation == "sonicmoe" and not use_sonicmoe: + LOG.warning( + "`experts_implementation='sonicmoe'` requires `use_sonicmoe: true`. " + "Automatically setting to 'eager'." + ) + data["experts_implementation"] = "eager" + elif experts_implementation not in _VALID_EXPERTS_IMPLS: LOG.warning( - f"`experts_implementation={experts_implementation!r}` is not compatible with " - f"custom MoE kernels. Automatically setting to 'eager'." + f"`experts_implementation={experts_implementation!r}` is not recognized. " + f"Valid options: {sorted(_VALID_EXPERTS_IMPLS)}. " + f"Automatically setting to 'eager'." ) data["experts_implementation"] = "eager" diff --git a/src/axolotl/integrations/kernels/constants.py b/src/axolotl/integrations/kernels/constants.py index 5239c98778..7373fa5ef2 100644 --- a/src/axolotl/integrations/kernels/constants.py +++ b/src/axolotl/integrations/kernels/constants.py @@ -1,76 +1,16 @@ -""" -Supported MoE block mappings for kernel integrations. - -Maps model_type to the SparseMoeBlock class name(s) in transformers. -Used by both ScatterMoE and SonicMoE kernel paths. - -Values can be a single class name (str) or a list of class names for models -with multiple MoE block types (e.g. qwen3_omni_moe has Thinker + Talker). - -Models with custom routing (see sonicmoe/routing.py for implementations): -- ernie4_5_moe: softmax→bias correction→topk (softmax_bias_topk_routing) -- deepseek_v2: softmax→group_limited_greedy (softmax_group_limited_topk_routing) -- hunyuan_v1_moe: softmax→topk via gate.wg (softmax_topk_wg_routing) -- gemma4_text: RMSNorm→scale→proj→softmax→topk→renorm→per_expert_scale (experts-level patch) -""" +"""Diagnostic helpers for MoE kernel integrations (kernel dispatch itself +is architecture-agnostic via the ExpertsInterface).""" import importlib -SPARSE_MOE_BLOCK = { - # softmax -> topk routing - "qwen2_moe": "Qwen2MoeSparseMoeBlock", - "qwen3_moe": "Qwen3MoeSparseMoeBlock", - "qwen3_5_moe": "Qwen3_5MoeSparseMoeBlock", - "qwen3_5_moe_text": "Qwen3_5MoeSparseMoeBlock", - "qwen3_next": "Qwen3NextSparseMoeBlock", - "qwen3_vl_moe": "Qwen3VLMoeTextSparseMoeBlock", - # qwen3_omni_moe: Thinker (standard) + Talker (shared experts + shared_expert_gate) - "qwen3_omni_moe": [ - "Qwen3OmniMoeThinkerTextSparseMoeBlock", - "Qwen3OmniMoeTalkerTextSparseMoeBlock", - ], - "olmoe": "OlmoeSparseMoeBlock", - "mixtral": "MixtralSparseMoeBlock", - "minimax": "MiniMaxSparseMoeBlock", - # softmax -> topk routing (with group-based expert selection) - "mistral4": "Mistral4MoE", - # sigmoid -> topk routing (with group-based expert selection) - "glm_moe_dsa": "GlmMoeDsaMoE", - "deepseek_v3": "DeepseekV3MoE", - "glm4_moe": "Glm4MoeMoE", - "glm4_moe_lite": "Glm4MoeLiteMoE", - "glm4v_moe": "Glm4vMoeTextMoE", - # sigmoid -> topk routing (no group selection) - "minimax_m2": "MiniMaxM2SparseMoeBlock", - # softmax->topk, e_score_correction_bias between softmax and topk - "ernie4_5_moe": "Ernie4_5_MoeSparseMoeBlock", - # softmax->topk, group_limited_greedy, different attr names (num_group) - "deepseek_v2": "DeepseekV2Moe", - # softmax->topk, gate.wg (not gate.weight) - "hunyuan_v1_moe": "HunYuanMoEV1Moe", - # TODO: gpt_oss deferred — transposed weight layout [E,H,2*I], expert biases, - # and custom GLU activation require a dedicated forward path in patch.py. - # "gpt_oss": "GptOssMLP", -} - - -# Models where MoE is NOT in a separate SparseMoeBlock but embedded in the -# decoder layer. For these, we patch the Experts class forward directly -# (same signature: hidden_states, top_k_index, top_k_weights -> Tensor). -# Routing stays untouched — the original model router runs as-is. +# Models where MoE is embedded in the decoder layer (no separate SparseMoeBlock). EXPERTS_ONLY_BLOCK = { - # gemma4: hybrid MLP+MoE in decoder layer, custom Gemma4TextRouter, - # no SparseMoeBlock. Experts use @use_experts_implementation with - # standard 3D param layout (gate_up_proj [E, 2*I, H], down_proj [E, H, I]). "gemma4_text": "Gemma4TextExperts", } def resolve_experts_class(model_type: str): - """Resolve the Experts class for models that need experts-level patching. - - Returns the class, or None if the model uses SparseMoeBlock-level patching. - """ + """Resolve the Experts class for a known model type, or ``None``.""" entry = EXPERTS_ONLY_BLOCK.get(model_type) if entry is None: return None @@ -93,41 +33,4 @@ def resolve_experts_class(model_type: str): def is_experts_only_model(model_type: str) -> bool: - """Check if a model type requires experts-level (not block-level) patching.""" return model_type in EXPERTS_ONLY_BLOCK - - -def resolve_moe_block_classes(model_type: str): - """Resolve all MoE block classes from transformers for the given model type. - - Returns a list of classes (one for most models, multiple for models with - distinct MoE block types like qwen3_omni_moe). - """ - entry = SPARSE_MOE_BLOCK.get(model_type) - if entry is None: - raise ValueError( - f"Unsupported MoE model type '{model_type}'. " - f"Supported types: {list(SPARSE_MOE_BLOCK.keys())}" - ) - - cls_names = entry if isinstance(entry, list) else [entry] - module_path = f"transformers.models.{model_type}.modeling_{model_type}" - try: - module = importlib.import_module(module_path) - except ModuleNotFoundError: - # Text sub-model types (e.g. qwen3_5_moe_text) share the parent module - if model_type.endswith("_text"): - parent_type = model_type.removesuffix("_text") - module_path = f"transformers.models.{parent_type}.modeling_{parent_type}" - module = importlib.import_module(module_path) - else: - raise - - classes = [] - for cls_name in cls_names: - moe_cls = getattr(module, cls_name, None) - if moe_cls is None: - raise ValueError(f"Could not find class '{cls_name}' in '{module_path}'") - classes.append(moe_cls) - - return classes diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_experts.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts.py similarity index 72% rename from src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_experts.py rename to src/axolotl/integrations/kernels/libs/scattermoe_lora/experts.py index 66623e0173..9199c8a595 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_experts.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts.py @@ -1,18 +1,7 @@ -""" -ScatterMoE-accelerated experts forward for Gemma4. - -Gemma4 has no separate SparseMoeBlock — MoE is embedded in the decoder layer. -The decoder layer handles routing (Gemma4TextRouter) and calls -``experts(hidden_states, top_k_index, top_k_weights)`` directly. +"""ScatterMoE experts forward for the transformers ExpertsInterface. -This module registers a ``"scattermoe"`` implementation in the transformers -``ExpertsInterface``, which the ``@use_experts_implementation`` decorator -dispatches to when ``config._experts_implementation == "scattermoe"``. - -This is the clean way to hook into transformers' MoE dispatch — no -monkeypatching required. Works for Gemma4 and any future model that uses -``@use_experts_implementation`` with the standard forward signature -``(hidden_states, top_k_index, top_k_weights) -> Tensor``. +PEFT LoRA on ``gate_up_proj`` / ``down_proj`` is fused into the +ScatterMoE Triton call via ``parallel_linear_lora``. """ import torch @@ -139,12 +128,23 @@ def scattermoe_experts_forward( top_k_index: torch.Tensor, top_k_weights: torch.Tensor, ) -> torch.Tensor: - """ScatterMoE-accelerated experts forward. + """ScatterMoE experts forward with fused-LoRA support.""" + # Assumes the standard expert layout: gate_up concatenated as [E, 2I, H], + # gated SwiGLU, no expert bias. gpt_oss-style experts (interleaved gate/up, + # transposed [E, H, 2I], expert bias) would be silently miscomputed by the + # fixed transpose/chunk below, so reject rather than corrupt training. + if ( + getattr(self, "is_transposed", False) + or not getattr(self, "is_concatenated", True) + or getattr(self, "has_bias", False) + or not getattr(self, "has_gate", True) + ): + raise NotImplementedError( + "scattermoe supports only concatenated, non-transposed, gated, biasless " + "experts (qwen/mixtral/deepseek/glm/...). This model's experts use an " + "unsupported layout; use use_sonicmoe or a built-in experts_implementation." + ) - Drop-in replacement for the standard Experts forward signature used by - ``@use_experts_implementation``-decorated classes (Gemma4, Mixtral, etc.): - ``(hidden_states [T, H], top_k_index [T, K], top_k_weights [T, K]) -> [T, H]`` - """ K = top_k_index.shape[1] routing_weights = top_k_weights.to(hidden_states.dtype) @@ -193,22 +193,24 @@ def scattermoe_experts_forward( return output -def register_scattermoe_experts(): - """Register ``"scattermoe"`` in the transformers ExpertsInterface. +_SCATTERMOE_PATCHED = False - After calling this, any model with ``@use_experts_implementation`` will - dispatch to ScatterMoE when ``config._experts_implementation == "scattermoe"``. - Also patches ``get_correct_experts_implementation`` to accept ``"scattermoe"`` - as a valid value (transformers hardcodes an allowlist). +def register_scattermoe_experts(): + """Register ``"scattermoe"`` in the ExpertsInterface and the validator allowlist. + + Idempotent. """ + global _SCATTERMOE_PATCHED + from transformers.integrations.moe import ALL_EXPERTS_FUNCTIONS from transformers.modeling_utils import PreTrainedModel - # 1. Register the forward function in the global interface ALL_EXPERTS_FUNCTIONS.register("scattermoe", scattermoe_experts_forward) - # 2. Patch the validation to accept "scattermoe" + if _SCATTERMOE_PATCHED: + return + _original_get_correct = PreTrainedModel.get_correct_experts_implementation def _patched_get_correct(self_model, requested_experts: str | None) -> str: @@ -217,19 +219,4 @@ def _patched_get_correct(self_model, requested_experts: str | None) -> str: return _original_get_correct(self_model, requested_experts) PreTrainedModel.get_correct_experts_implementation = _patched_get_correct - - -# Legacy monkeypatch approach (kept for backward compat with existing tests) -def patch_gemma4_scattermoe(): - """Monkeypatch Gemma4TextExperts.forward with ScatterMoE kernel.""" - from axolotl.integrations.kernels.constants import resolve_experts_class - - experts_cls = resolve_experts_class("gemma4_text") - if experts_cls is None: - raise ValueError("Could not resolve Gemma4TextExperts class") - - if hasattr(experts_cls, "_original_forward"): - return # already patched - - experts_cls._original_forward = experts_cls.forward - experts_cls.forward = scattermoe_experts_forward + _SCATTERMOE_PATCHED = True diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/__init__.py b/src/axolotl/integrations/kernels/libs/sonicmoe/__init__.py index d1f5e5f603..5cd9cf0fd7 100644 --- a/src/axolotl/integrations/kernels/libs/sonicmoe/__init__.py +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/__init__.py @@ -1,3 +1,6 @@ -from .patch import patch_sonicmoe +from .experts import register_sonicmoe_experts, sonicmoe_experts_forward_with_lora -__all__ = ["patch_sonicmoe"] +__all__ = [ + "register_sonicmoe_experts", + "sonicmoe_experts_forward_with_lora", +] diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/experts.py b/src/axolotl/integrations/kernels/libs/sonicmoe/experts.py new file mode 100644 index 0000000000..785b74e45a --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/experts.py @@ -0,0 +1,143 @@ +"""LoRA-aware sonicmoe experts forward for the transformers ExpertsInterface. + +Wraps upstream ``_sonicmoe_wrapper`` and materializes expert LoRA via +``MoELoRAMaterialize`` before the CUTLASS call. +""" + +from __future__ import annotations + +import torch + +from .lora import ( + MoELoRAMaterialize, + get_lora_params_from_wrapper, + has_lora, + materialize_expert_lora, + unwrap_experts_lora, +) + + +def _maybe_unwrap_param_wrapper(param): + """Return ``(base_tensor, lora_params_or_None)`` for a PEFT-wrapped Parameter.""" + try: + from peft.tuners.param_wrapper import ParamWrapper + except ImportError: + return param, None + + if not isinstance(param, ParamWrapper): + return param, None + + base = param.original_parameter + lora_A, lora_B, scaling = get_lora_params_from_wrapper(param) + if lora_A is None: + return base, None + return base, (lora_A, lora_B, scaling) + + +def _resolve_weights_and_lora(experts_module): + """Resolve raw expert weights/biases + optional LoRA tuples. + + Handles both PEFT layouts: module-level wrap (walked via ``unwrap_experts_lora``) + and per-parameter ``ParamWrapper``. No layout permute applied. + """ + if has_lora(experts_module): + base_experts, lora_dict = unwrap_experts_lora(experts_module) + w1 = base_experts.gate_up_proj + w2 = base_experts.down_proj + b1 = getattr(base_experts, "gate_up_proj_bias", None) + b2 = getattr(base_experts, "down_proj_bias", None) + return w1, b1, w2, b2, lora_dict.get("gate_up_proj"), lora_dict.get("down_proj") + + w1, lora_w1 = _maybe_unwrap_param_wrapper(experts_module.gate_up_proj) + w2, lora_w2 = _maybe_unwrap_param_wrapper(experts_module.down_proj) + b1 = getattr(experts_module, "gate_up_proj_bias", None) + b2 = getattr(experts_module, "down_proj_bias", None) + return w1, b1, w2, b2, lora_w1, lora_w2 + + +def sonicmoe_experts_forward_with_lora( + self, + hidden_states: torch.Tensor, + top_k_index: torch.Tensor, + top_k_weights: torch.Tensor, +) -> torch.Tensor: + """Sonicmoe experts forward with PEFT LoRA materialization.""" + from transformers.integrations.sonicmoe import _sonicmoe_wrapper + + if not getattr(self, "has_gate", True): + raise ValueError("sonicmoe requires gated experts (has_gate=True)") + if hidden_states.device.type != "cuda": + raise ValueError("sonicmoe requires CUDA device") + + device = hidden_states.device + num_top_k = top_k_index.size(-1) + num_tokens = hidden_states.size(0) + + # Flatten — token indices must be int32 and sorted ascending (sonic-moe requirement). + token_idx = ( + torch.arange(num_tokens, device=device) + .unsqueeze(1) + .expand(-1, num_top_k) + .reshape(-1) + .int() + ) + router_scores = top_k_weights.reshape(-1).to(hidden_states.dtype) + expert_ids = top_k_index.reshape(-1).int() + + w1, b1, w2, b2, lora_w1, lora_w2 = _resolve_weights_and_lora(self) + if not getattr(self, "has_bias", False): + b1 = b2 = None + + # FSDP2 / EP wraps parameters as DTensors but sonic-moe takes raw CUTLASS pointers, + # so unwrap to local shards before the materialize/permute. to_local() is + # autograd-aware — backward will rewrap the gradient as a DTensor again. + if isinstance(w1, torch.distributed.tensor.DTensor): + w1 = w1.to_local() + w2 = w2.to_local() + b1 = b1.to_local() if b1 is not None else None + b2 = b2.to_local() if b2 is not None else None + + # Materialize W_eff = W + scaling * (B @ A) per expert. No-op when no LoRA. + if lora_w1 is not None: + w1 = MoELoRAMaterialize.apply(w1, *lora_w1) + if lora_w2 is not None: + w2 = MoELoRAMaterialize.apply(w2, *lora_w2) + + # Match upstream layout expectations: + # is_transposed=False: gate_up [E, 2*I, H] / down [E, H, I] -> permute(1, 2, 0) + # is_transposed=True: gate_up [E, H, 2*I] / down [E, I, H] -> permute(2, 1, 0) + perm = (2, 1, 0) if getattr(self, "is_transposed", False) else (1, 2, 0) + w1 = w1.permute(*perm) + w2 = w2.permute(*perm) + + act_name = getattr(self.config, "hidden_act", "silu").lower() + + return _sonicmoe_wrapper( + hidden_states=hidden_states, + router_scores=router_scores, + expert_ids=expert_ids, + token_idx=token_idx, + w1=w1, + b1=b1, + w2=w2, + b2=b2, + act_name=act_name, + num_experts=self.num_experts, + concat_layout=getattr(self, "is_concatenated", True), + is_inference_mode_enabled=not torch.is_grad_enabled(), + ) + + +def register_sonicmoe_experts() -> None: + """Register the LoRA-aware ``"sonicmoe"`` forward, overriding upstream. Idempotent.""" + from transformers.integrations.moe import ALL_EXPERTS_FUNCTIONS + + ALL_EXPERTS_FUNCTIONS.register("sonicmoe", sonicmoe_experts_forward_with_lora) + + +# Re-export utilities for tests / external callers. +__all__ = [ + "sonicmoe_experts_forward_with_lora", + "register_sonicmoe_experts", + "materialize_expert_lora", +] diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/gemma4_experts.py b/src/axolotl/integrations/kernels/libs/sonicmoe/gemma4_experts.py deleted file mode 100644 index a4025dd842..0000000000 --- a/src/axolotl/integrations/kernels/libs/sonicmoe/gemma4_experts.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -SonicMoE-accelerated experts forward for Gemma4. - -Gemma4 has no separate SparseMoeBlock — MoE is embedded in the decoder layer. -This module provides a drop-in replacement for ``Gemma4TextExperts.forward`` -that uses SonicMoE kernels while preserving the original call signature. -""" - -import torch - -from .lora import has_lora, materialize_expert_lora, unwrap_experts_lora - - -def _get_expert_weights_gemma4(experts_module): - """Extract expert weights from Gemma4TextExperts, applying LoRA if active. - - Returns: - (gate_up_weight, down_weight) in SonicMoE layout [dim, dim, E]. - """ - if has_lora(experts_module): - base_experts, lora_dict = unwrap_experts_lora(experts_module) - gate_up = materialize_expert_lora( - base_experts.gate_up_proj, lora_dict.get("gate_up_proj") - ) - down = materialize_expert_lora( - base_experts.down_proj, lora_dict.get("down_proj") - ) - else: - gate_up = experts_module.gate_up_proj - down = experts_module.down_proj - - # Permute to SonicMoE layout: - # gate_up: [E, 2*I, H] -> [2*I, H, E] - # down: [E, H, I] -> [H, I, E] - return gate_up.permute(1, 2, 0), down.permute(1, 2, 0) - - -def gemma4_sonicmoe_experts_forward( - self, - hidden_states: torch.Tensor, - top_k_index: torch.Tensor, - top_k_weights: torch.Tensor, -) -> torch.Tensor: - """SonicMoE-accelerated replacement for Gemma4TextExperts.forward. - - Same signature as the original: (hidden_states [T, H], top_k_index [T, K], - top_k_weights [T, K]) -> output [T, H]. - """ - from sonicmoe import moe_general_routing_inputs - from sonicmoe.enums import ActivationType - - T, _ = hidden_states.shape - K = top_k_index.shape[1] - E = self.num_experts - - # Convert routing outputs to SonicMoE's flat format - # Token indices sorted ascending (required by SonicMoE) - token_indices = ( - torch.arange(T, device=hidden_states.device, dtype=torch.int32) - .unsqueeze(1) - .expand(T, K) - ) - flat_scores = top_k_weights.to(torch.float32).reshape(-1) # [T*K] - flat_token_idx = token_indices.reshape(-1) # [T*K] - flat_expert_idx = top_k_index.to(torch.int32).reshape(-1) # [T*K] - - # Get weights (with LoRA materialization if needed) - gate_up_weight, down_weight = _get_expert_weights_gemma4(self) - gate_up_weight = gate_up_weight.to(hidden_states.dtype) - down_weight = down_weight.to(hidden_states.dtype) - - if not torch.cuda.is_available(): - raise RuntimeError("SonicMoE requires CUDA. No CUDA device available.") - cuda_stream = torch.cuda.current_stream().cuda_stream - - output, _ = moe_general_routing_inputs( - hidden_states, - flat_scores, - flat_token_idx, - flat_expert_idx, - gate_up_weight, - None, # b1 (no gate/up bias) - down_weight, - None, # b2 (no down bias) - E, - cuda_stream, - ActivationType.GEGLU, - False, # is_inference_mode - ) - - return output - - -def patch_gemma4_sonicmoe(): - """Monkeypatch Gemma4TextExperts.forward with SonicMoE kernel.""" - from axolotl.integrations.kernels.constants import resolve_experts_class - - experts_cls = resolve_experts_class("gemma4_text") - if experts_cls is None: - raise ValueError("Could not resolve Gemma4TextExperts class") - - if hasattr(experts_cls, "_original_forward"): - return # already patched - - experts_cls._original_forward = experts_cls.forward - experts_cls.forward = gemma4_sonicmoe_experts_forward diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/lora.py b/src/axolotl/integrations/kernels/libs/sonicmoe/lora.py index 4d7a21925b..1fe08828cb 100644 --- a/src/axolotl/integrations/kernels/libs/sonicmoe/lora.py +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/lora.py @@ -61,33 +61,6 @@ def get_lora_params_from_wrapper(module) -> tuple: return lora_A, lora_B, scaling -def unwrap_gate_lora(gate_module): - """Unwrap PEFT ParamWrapper on the router gate. - - When PEFT targets ``gate.weight``, ``self.gate`` becomes:: - - ParamWrapper(weight) - -> base_layer: Router (the real module) - - Returns: - (base_gate, gate_weight, gate_lora_delta_or_None) - - ``base_gate`` is the original router module (with ``.top_k``, etc.). - ``gate_weight`` is the base router weight tensor. - ``gate_lora_delta_or_None`` is the LoRA delta if active, else None. - Kept separate to avoid mixing DTensor + Tensor under FSDP. - """ - if has_lora(gate_module): - base_gate = gate_module.base_layer - lora_A, lora_B, scaling = get_lora_params_from_wrapper(gate_module) - if lora_A is not None: - delta = scaling * (lora_B @ lora_A) - return base_gate, base_gate.weight, delta - return base_gate, base_gate.weight, None - - return gate_module, gate_module.weight, None - - def unwrap_experts_lora(experts_module): """Walk a PEFT ParamWrapper chain on ``self.experts``. @@ -129,18 +102,12 @@ def unwrap_experts_lora(experts_module): class MoELoRAMaterialize(torch.autograd.Function): - """Materialize effective weight W_eff = W + scaling * (B @ A) per expert. - - Inserts into the autograd graph between PEFT's LoRA parameters and - SonicMoE's CUTLASS kernels. The CUTLASS backward computes dW_eff, - which this function decomposes into dA and dB via the chain rule. - - Weight layouts (PEFT rank-major): - base_weight: [E, dim1, dim2] (frozen expert parameter) - lora_A: [r*E, dim2] (rows [e*r:(e+1)*r] = A_e) - lora_B: [dim1, r*E] (cols [:, e*r:(e+1)*r] = B_e) + """Materialize ``W_eff = W + scaling * (B @ A)`` per expert and route grads. - Per-expert: delta_e = B_e @ A_e = [dim1, r] @ [r, dim2] = [dim1, dim2] + Layout matches PEFT >= 0.19.1 ``ParamWrapper``: ``base [E, dim1, dim2]``, + ``lora_A [r*E, dim2]`` (E-outer, r-inner rows), ``lora_B [dim1, r*E]`` + (r-outer, E-inner cols). Equivalent to + ``einsum("o r e, e r i -> e o i", lora_B.reshape(dim1, r, E), lora_A.reshape(E, r, dim2))``. """ @staticmethod diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/patch.py b/src/axolotl/integrations/kernels/libs/sonicmoe/patch.py deleted file mode 100644 index 65095a9871..0000000000 --- a/src/axolotl/integrations/kernels/libs/sonicmoe/patch.py +++ /dev/null @@ -1,272 +0,0 @@ -""" -SonicMoE patching for SparseMoeBlock forward pass. - -Monkeypatches the SparseMoeBlock class for a given model type to use -SonicMoE's optimized kernels. Two forward paths are supported: - -1. **General routing path** (routing_fn is not None): - Uses a custom routing function + ``moe_general_routing_inputs``. - Suitable for models with non-standard routing (softmax->topk, sigmoid->topk). - -2. **Fused topk->softmax path** (routing_fn is None): - Uses ``moe_TC_softmax_topk_layer`` which fuses routing + expert computation. - Suitable for models with simple topk->softmax routing. - -Weight format conversion (interleave/deinterleave) is handled by the -WeightConverter system, so the forward assumes weights are already in -interleaved format. - -Shared experts are handled generically: if the block has a ``shared_expert`` -or ``shared_experts`` attribute, its output is computed alongside the routed -experts and added to the final output. An optional ``shared_expert_gate`` -applies sigmoid gating to the shared expert contribution. -""" - -import torch -import torch.nn.functional as F - -from axolotl.integrations.kernels.constants import resolve_moe_block_classes -from axolotl.utils.logging import get_logger - -from .lora import ( - has_lora, - materialize_expert_lora, - unwrap_experts_lora, - unwrap_gate_lora, -) - -LOG = get_logger(__name__) - - -def _get_expert_weights(experts_module): - """Extract expert weights, applying LoRA materialization if PEFT is active. - - Returns: - (gate_up_weight, down_weight) in SonicMoE layout [dim, dim, E]. - """ - if has_lora(experts_module): - base_experts, lora_dict = unwrap_experts_lora(experts_module) - gate_up = materialize_expert_lora( - base_experts.gate_up_proj, lora_dict.get("gate_up_proj") - ) - down = materialize_expert_lora( - base_experts.down_proj, lora_dict.get("down_proj") - ) - else: - gate_up = experts_module.gate_up_proj - down = experts_module.down_proj - - # Permute to SonicMoE layout: - # gate_up: [E, 2*I, H] -> [2*I, H, E] - # down: [E, H, I] -> [H, I, E] - return gate_up.permute(1, 2, 0), down.permute(1, 2, 0) - - -def _fix_qwen3_5_moe_text_weight_renaming(model_type: str, base_model_type: str): - """Strip qwen3_5_moe_text WeightRenaming in VLM mode to preserve custom loaders.""" - if model_type != "qwen3_5_moe_text" or base_model_type == "qwen3_5_moe_text": - return - - try: - from transformers.conversion_mapping import ( - get_checkpoint_conversion_mapping, - register_checkpoint_conversion_mapping, - ) - from transformers.core_model_loading import WeightRenaming - except ImportError: - return - - text_mapping = get_checkpoint_conversion_mapping(model_type) - if text_mapping and isinstance(text_mapping[0], WeightRenaming): - text_mapping.pop(0) - register_checkpoint_conversion_mapping(model_type, text_mapping, overwrite=True) - LOG.info("Stripped qwen3_5_moe_text WeightRenaming for VLM mode") - - -def patch_sonicmoe( - model_type: str, - torch_compile: bool = False, - base_model_type: str | None = None, -): - """Patch SparseMoeBlock for SonicMoE support.""" - from .routing import get_model_moe_config - from .weight_converter import register_sonicmoe_weight_converter - - _fix_qwen3_5_moe_text_weight_renaming(model_type, base_model_type or model_type) - - routing_fn, activation, router_attr = get_model_moe_config(model_type) - - if torch_compile and routing_fn is not None: - routing_fn = _try_compile_routing(routing_fn) - - for moe_cls in resolve_moe_block_classes(model_type): - _patch_forward(moe_cls, routing_fn, activation, router_attr) - register_sonicmoe_weight_converter(model_type) - - -def _try_compile_routing(routing_fn): - """Attempt to torch.compile the routing function, fall back to eager on failure.""" - try: - compiled_fn = torch.compile(routing_fn, mode="reduce-overhead", dynamic=False) - LOG.info(f"torch.compile enabled for routing function: {routing_fn.__name__}") - return compiled_fn - except Exception as exc: # pylint: disable=broad-except - LOG.warning( - f"torch.compile failed for routing function {routing_fn.__name__}, " - f"falling back to eager: {exc}" - ) - return routing_fn - - -def _patch_forward(moe_cls, routing_fn, activation, router_attr): - """Monkeypatch the SparseMoeBlock class with a SonicMoE forward. - - The patched forward handles shared experts generically: if - ``self.shared_expert`` or ``self.shared_experts`` exists, it is computed - and added to the routed output. If ``self.shared_expert_gate`` also exists, - it applies sigmoid gating to the shared expert contribution (as in qwen2_moe). - - Args: - moe_cls: The SparseMoeBlock class to patch. - routing_fn: Routing function (e.g. softmax_topk_routing), or None - for the fused moe_TC_softmax_topk_layer path. - activation: SonicMoE ActivationType enum value. - router_attr: Name of the router module attribute on the MoE block. - """ - if hasattr(moe_cls, "_original_forward"): - LOG.info(f"{moe_cls.__name__}.forward already patched with SonicMoE, skipping") - return - - original_forward = moe_cls.forward - - if routing_fn is not None: - _make_general_forward(moe_cls, routing_fn, activation) - else: - _make_fused_forward(moe_cls, activation, router_attr) - - moe_cls._original_forward = original_forward - LOG.info(f"Patched {moe_cls.__name__}.forward with SonicMoE implementation") - - -def _make_general_forward(moe_cls, routing_fn, activation): - """Create forward using routing_fn + moe_general_routing_inputs.""" - - def sonicmoe_forward(self, hidden_states: torch.Tensor) -> torch.Tensor: - from sonicmoe import moe_general_routing_inputs - - batch_size, sequence_length, hidden_dim = hidden_states.shape - hidden_states_flat = hidden_states.view(-1, hidden_dim) - - # Shared expert (computed early, matching original model ordering) - shared_expert_output = _compute_shared_expert(self, hidden_states_flat) - - # Routing - router_scores, token_indices, expert_indices, _router_logits = routing_fn( - hidden_states_flat, self - ) - - # Unwrap PEFT + optional LoRA materialization, then permute to SonicMoE layout - gate_up_weight, down_weight = _get_expert_weights(self.experts) - gate_up_weight = gate_up_weight.to(hidden_states_flat.dtype) - down_weight = down_weight.to(hidden_states_flat.dtype) - E = gate_up_weight.shape[-1] - - output, _ = moe_general_routing_inputs( - hidden_states_flat, - router_scores, - token_indices, - expert_indices, - gate_up_weight, - None, # b1 (no gate/up bias) - down_weight, - None, # b2 (no down bias) - E, - torch.cuda.current_stream().cuda_stream, - activation, - False, # is_inference_mode - ) - - # Add shared expert contribution if present - if shared_expert_output is not None: - if hasattr(self, "shared_expert_gate"): - shared_expert_output = ( - F.sigmoid(self.shared_expert_gate(hidden_states_flat)) - * shared_expert_output - ) - output = output + shared_expert_output - - return output.view(batch_size, sequence_length, hidden_dim) - - moe_cls.forward = sonicmoe_forward - - -def _make_fused_forward(moe_cls, activation, router_attr): - """Create forward using moe_TC_softmax_topk_layer (topk -> softmax).""" - - def sonicmoe_fused_forward(self, hidden_states: torch.Tensor) -> torch.Tensor: - from sonicmoe import moe_TC_softmax_topk_layer - - batch_size, sequence_length, hidden_dim = hidden_states.shape - hidden_states_flat = hidden_states.view(-1, hidden_dim) - - # Shared expert (computed early, matching original model ordering) - shared_expert_output = _compute_shared_expert(self, hidden_states_flat) - - # Unwrap router for attribute access + optional LoRA delta - raw_router = getattr(self, router_attr) - base_router, router_weight, router_lora_delta = unwrap_gate_lora(raw_router) - if router_lora_delta is not None: - # Materialize local tensor to avoid DTensor + Tensor add under FSDP - if hasattr(router_weight, "to_local"): - router_weight = router_weight.to_local() - effective_router_weight = router_weight + router_lora_delta - else: - effective_router_weight = router_weight - - # Unwrap PEFT + optional LoRA materialization, then permute to SonicMoE layout - gate_up_weight, down_weight = _get_expert_weights(self.experts) - gate_up_weight = gate_up_weight.to(hidden_states_flat.dtype) - down_weight = down_weight.to(hidden_states_flat.dtype) - - output, _router_logits, _expert_freq = moe_TC_softmax_topk_layer( - hidden_states_flat, - effective_router_weight, - gate_up_weight, - None, # b1 (no gate/up bias) - down_weight, - None, # b2 (no down bias) - base_router.top_k, - torch.cuda.current_stream().cuda_stream, - activation, - False, # is_inference_mode - ) - - # Add shared expert contribution if present - if shared_expert_output is not None: - if hasattr(self, "shared_expert_gate"): - shared_expert_output = ( - F.sigmoid(self.shared_expert_gate(hidden_states_flat)) - * shared_expert_output - ) - output = output + shared_expert_output - - return output.view(batch_size, sequence_length, hidden_dim) - - moe_cls.forward = sonicmoe_fused_forward - - -def _compute_shared_expert(moe_block, hidden_states_flat): - """Compute shared expert output if the block has one. - - Handles singular (qwen2_moe: ``shared_expert``), plural - (glm_moe_dsa/deepseek_v3: ``shared_experts``), and MLP - (hunyuan_v1_moe: ``shared_mlp``) attribute names. - """ - shared_expert = ( - getattr(moe_block, "shared_expert", None) - or getattr(moe_block, "shared_experts", None) - or getattr(moe_block, "shared_mlp", None) - ) - if shared_expert is not None: - return shared_expert(hidden_states_flat) - return None diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/routing.py b/src/axolotl/integrations/kernels/libs/sonicmoe/routing.py deleted file mode 100644 index 68654d0868..0000000000 --- a/src/axolotl/integrations/kernels/libs/sonicmoe/routing.py +++ /dev/null @@ -1,576 +0,0 @@ -""" -Routing functions for SonicMoE integration. - -Different MoE architectures use different routing strategies: -- qwen3_moe / qwen2_moe / qwen3_5_moe / qwen3_vl_moe / qwen3_omni_moe: softmax -> topk (with optional renormalization) -- mistral4: softmax -> group selection -> topk (with renormalization and scaling) -- glm_moe_dsa / deepseek_v3 / minimax_m2: sigmoid -> topk (with group-based expert selection) -- ernie4_5_moe: softmax -> bias correction -> topk -> gather (softmax_bias_topk_routing) -- hunyuan_v1_moe: softmax -> topk via gate.wg (softmax_topk_wg_routing) -- gemma4_text: RMSNorm -> scale -> proj -> softmax -> topk -> renorm -> per_expert_scale (gemma4_routing) -- gpt_oss: topk -> softmax (uses fused moe_TC_softmax_topk_layer, routing_fn=None) [NOT YET SUPPORTED] - -Each model type maps to a (routing_fn, activation_type, router_attr) triple. -When routing_fn is None, the fused moe_TC_softmax_topk_layer path is used. -""" - -import torch -import torch.nn.functional as F - -from .lora import unwrap_gate_lora - - -def get_model_moe_config(model_type: str): - """Returns (routing_fn, activation, router_attr) for a given model type. - - Args: - model_type: HuggingFace model type string. - - Returns: - routing_fn: Callable or None. None signals the fused - moe_TC_softmax_topk_layer path (topk -> softmax models). - activation: SonicMoE ActivationType enum value. - router_attr: Name of the router module attribute on the MoE block - (e.g. "gate" or "router"). - - The activation type cannot be derived from config.hidden_act because - e.g. qwen3_moe reports "silu" but architecturally uses SwiGLU - (act_fn(gate) * up pattern). So we specify it per model type. - """ - from sonicmoe.enums import ActivationType - - if model_type in ( - "qwen2_moe", - "qwen3_moe", - "qwen3_5_moe", - "qwen3_5_moe_text", - "qwen3_next", - "qwen3_vl_moe", - "qwen3_omni_moe", - "olmoe", - "mixtral", - "minimax", - ): - return softmax_topk_routing, ActivationType.SWIGLU, "gate" - elif model_type in ("mistral4",): - return softmax_group_topk_routing, ActivationType.SWIGLU, "gate" - elif model_type in ( - "glm_moe_dsa", - "deepseek_v3", - "glm4_moe", - "glm4_moe_lite", - "glm4v_moe", - "minimax_m2", - ): - return sigmoid_topk_routing, ActivationType.SWIGLU, "gate" - elif model_type in ("ernie4_5_moe",): - return softmax_bias_topk_routing, ActivationType.SWIGLU, "gate" - elif model_type in ("hunyuan_v1_moe",): - return softmax_topk_wg_routing, ActivationType.SWIGLU, "gate" - elif model_type in ("gemma4_text",): - return gemma4_routing, ActivationType.GEGLU, "router" - # Fused topk -> softmax path (routing_fn=None): - # elif model_type in ("gpt_oss",): - # # NOTE: gpt_oss has a router bias which moe_TC_softmax_topk_layer - # # ignores (it only takes router_w, not bias). Also has transposed - # # weight layout [E, H, 2*I] and custom GLU activation. - # return None, ActivationType.SWIGLU, "router" - else: - raise ValueError(f"SonicMoE: unsupported model type '{model_type}'") - - -def softmax_topk_routing( - hidden_states: torch.Tensor, moe_block -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: - """Qwen3/Qwen2-style routing: softmax -> topk -> optional renorm. - - Args: - hidden_states: [T, H] flattened token representations - moe_block: MoE block module (accesses moe_block.gate.*) - - Returns: - router_scores: [T*K] flattened scores (float32) - token_indices: [T*K] which token each entry belongs to (int32), sorted ascending - expert_indices: [T*K] which expert (int32) - router_logits: [T, E] original logits for aux loss - """ - base_gate, gate_weight, gate_lora_delta = unwrap_gate_lora(moe_block.gate) - T, H = hidden_states.shape - K = base_gate.top_k - - # Compute router logits and softmax over all experts. - # Two F.linear calls avoid mixing DTensor (gate_weight) + Tensor (delta) under FSDP. - # Cast to float32 to match LoRA delta dtype (PEFT computes in fp32). - router_logits = F.linear(hidden_states.float(), gate_weight.float()) # [T, E] - if gate_lora_delta is not None: - router_logits = router_logits + F.linear( - hidden_states.float(), gate_lora_delta.float() - ) - router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] - - # Select top-k experts per token - top_values, top_indices = torch.topk(router_probs, K, dim=-1) # [T, K] each - - # Renormalize if configured (default True for models without the attribute, - # e.g. Mixtral/MiniMax which always normalize) - if getattr(base_gate, "norm_topk_prob", True): - top_values = top_values / top_values.sum(dim=-1, keepdim=True) - - # no-op: matches transformers which casts to softmax output dtype (float32). - # top_values = top_values.to(router_probs.dtype) - - # Flatten for moe_general_routing_inputs. - # Token indices are naturally sorted ascending from the [T, K] layout: - # [0, 0, ..., 1, 1, ..., T-1, T-1, ...] — this is required by SonicMoE. - # Expert sorting is handled internally by general_routing_router_metadata. - token_indices = ( - torch.arange(T, device=hidden_states.device, dtype=torch.int32) - .unsqueeze(1) - .expand(T, K) - ) - - flat_scores = top_values.reshape(-1) # [T*K] - flat_token_idx = token_indices.reshape(-1) # [T*K] - flat_expert_idx = top_indices.to(torch.int32).reshape(-1) # [T*K] - - return flat_scores, flat_token_idx, flat_expert_idx, router_logits - - -def softmax_group_topk_routing( - hidden_states: torch.Tensor, moe_block -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: - """Mistral4-style routing: softmax -> group selection -> topk -> renorm -> scale.""" - base_gate, gate_weight, gate_lora_delta = unwrap_gate_lora(moe_block.gate) - T, _ = hidden_states.shape - K = moe_block.top_k - E = getattr(moe_block, "n_routed_experts", gate_weight.shape[0]) - n_group = getattr(moe_block, "n_group", 1) - - router_logits = F.linear(hidden_states.float(), gate_weight.float()) # [T, E] - if gate_lora_delta is not None: - router_logits = router_logits + F.linear( - hidden_states.float(), gate_lora_delta.float() - ) - router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] - - scores_for_choice = router_probs - - # Group selection: pick top groups, mask the rest - if n_group > 1: - group_scores = ( - scores_for_choice.view(-1, n_group, E // n_group) - .topk(2, dim=-1)[0] - .sum(dim=-1) - ) - group_idx = torch.topk( - group_scores, k=moe_block.topk_group, dim=-1, sorted=False - )[1] - group_mask = torch.zeros_like(group_scores) - group_mask.scatter_(1, group_idx, 1) - score_mask = ( - group_mask.unsqueeze(-1).expand(-1, n_group, E // n_group).reshape(-1, E) - ) - scores_for_choice = scores_for_choice.masked_fill(~score_mask.bool(), 0.0) - - topk_indices = torch.topk(scores_for_choice, k=K, dim=-1, sorted=False)[1] - topk_weights = router_probs.gather(1, topk_indices) - - # Renormalization + scaling - norm_topk_prob = getattr(moe_block, "norm_topk_prob", True) - if norm_topk_prob: - topk_weights = topk_weights / (topk_weights.sum(dim=-1, keepdim=True) + 1e-20) - routed_scaling_factor = getattr(moe_block, "routed_scaling_factor", 1.0) - topk_weights = topk_weights * routed_scaling_factor - - # Flatten for moe_general_routing_inputs - token_indices = ( - torch.arange(T, device=hidden_states.device, dtype=torch.int32) - .unsqueeze(1) - .expand(T, K) - ) - - flat_scores = topk_weights.to(torch.float32).reshape(-1) # [T*K] - flat_token_idx = token_indices.reshape(-1) # [T*K] - flat_expert_idx = topk_indices.to(torch.int32).reshape(-1) # [T*K] - - return flat_scores, flat_token_idx, flat_expert_idx, router_logits - - -def sigmoid_topk_routing( - hidden_states: torch.Tensor, moe_block -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: - """Sigmoid-based routing: sigmoid -> optional group selection -> topk. - - Supports two variants: - - **Group selection** (glm_moe_dsa, deepseek_v3, etc.): n_group > 1, - bias on gate, group-based masking before topk. - - **No group selection** (minimax_m2): n_group == 1 (or absent), - bias on moe_block, straight topk from all experts. - - Final routing weights come from the original sigmoid scores (not - bias-corrected), with optional renormalization and scaling. - - Args: - hidden_states: [T, H] flattened token representations - moe_block: MoE block module (accesses moe_block.gate.* and - optional moe_block.n_group, .topk_group, .top_k, .norm_topk_prob, - .routed_scaling_factor, .n_routed_experts) - - Returns: - router_scores: [T*K] flattened scores (float32) - token_indices: [T*K] which token each entry belongs to (int32), sorted ascending - expert_indices: [T*K] which expert (int32) - router_logits: [T, E] original logits for aux loss - """ - base_gate, gate_weight, gate_lora_delta = unwrap_gate_lora(moe_block.gate) - T, _ = hidden_states.shape - K = moe_block.top_k - E = getattr(moe_block, "n_routed_experts", gate_weight.shape[0]) - n_group = getattr(moe_block, "n_group", 1) - - # Compute router logits and sigmoid probabilities - router_logits = F.linear(hidden_states.float(), gate_weight.float()) # [T, E] - if gate_lora_delta is not None: - router_logits = router_logits + F.linear( - hidden_states.float(), gate_lora_delta.float() - ) - router_probs = router_logits.sigmoid() # [T, E] - - # Bias-corrected scores for expert selection (not used for final weights). - # glm_moe_dsa/deepseek_v3 store the bias on gate; minimax_m2 stores it on the block. - e_score_correction_bias = getattr(base_gate, "e_score_correction_bias", None) - if e_score_correction_bias is None: - e_score_correction_bias = getattr(moe_block, "e_score_correction_bias", None) - if e_score_correction_bias is None: - raise AttributeError( - f"sigmoid_topk_routing requires e_score_correction_bias on " - f"gate ({type(base_gate)}) or moe_block ({type(moe_block)}), but neither has it" - ) - scores_for_choice = router_probs + e_score_correction_bias - - # Group-based selection: pick top groups, mask the rest (skip when n_group == 1) - if n_group > 1: - group_scores = ( - scores_for_choice.view(-1, n_group, E // n_group) - .topk(2, dim=-1)[0] - .sum(dim=-1) - ) # [T, n_group] - group_idx = torch.topk( - group_scores, k=moe_block.topk_group, dim=-1, sorted=False - )[1] - group_mask = torch.zeros_like(group_scores) - group_mask.scatter_(1, group_idx, 1) - score_mask = ( - group_mask.unsqueeze(-1).expand(-1, n_group, E // n_group).reshape(-1, E) - ) - scores_for_choice = scores_for_choice.masked_fill(~score_mask.bool(), 0.0) - - # Final topk from (possibly masked) scores - topk_indices = torch.topk(scores_for_choice, k=K, dim=-1, sorted=False)[1] - - # Gather weights from original sigmoid scores (not bias-corrected) - topk_weights = router_probs.gather(1, topk_indices) - - # Optional renormalization + scaling - norm_topk_prob = getattr(moe_block, "norm_topk_prob", True) - if norm_topk_prob: - topk_weights = topk_weights / (topk_weights.sum(dim=-1, keepdim=True) + 1e-20) - routed_scaling_factor = getattr(moe_block, "routed_scaling_factor", 1.0) - topk_weights = topk_weights * routed_scaling_factor - - # Flatten for moe_general_routing_inputs. - # Token indices are naturally sorted ascending from the [T, K] layout. - token_indices = ( - torch.arange(T, device=hidden_states.device, dtype=torch.int32) - .unsqueeze(1) - .expand(T, K) - ) - - flat_scores = topk_weights.to(torch.float32).reshape(-1) # [T*K] - flat_token_idx = token_indices.reshape(-1) # [T*K] - flat_expert_idx = topk_indices.to(torch.int32).reshape(-1) # [T*K] - - return flat_scores, flat_token_idx, flat_expert_idx, router_logits - - -def softmax_bias_topk_routing( - hidden_states: torch.Tensor, moe_block -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: - """Ernie 4.5 MoE routing: softmax → bias correction → topk → gather → renorm. - - Differs from standard softmax_topk_routing in three ways: - 1. A learned e_score_correction_bias is added to softmax probs *before* topk - (selection uses biased scores, but final weights use original probs). - 2. The bias is applied via gate.moe_statics module (not a raw tensor). - 3. Renormalization uses clamp(min=norm_min) instead of sum+epsilon. - - Reference: Ernie4_5_MoeTopKRouter.forward in transformers. - - Args: - hidden_states: [T, H] flattened token representations - moe_block: MoE block module (accesses moe_block.gate.*) - - Returns: - router_scores: [T*K] flattened scores (float32) - token_indices: [T*K] which token each entry belongs to (int32), sorted ascending - expert_indices: [T*K] which expert (int32) - router_logits: [T, E] original logits for aux loss - """ - base_gate, gate_weight, gate_lora_delta = unwrap_gate_lora(moe_block.gate) - T, H = hidden_states.shape - K = base_gate.top_k - - # Compute router logits and softmax (force float32 for numerical stability) - router_logits = F.linear(hidden_states.float(), gate_weight.float()) # [T, E] - if gate_lora_delta is not None: - router_logits = router_logits + F.linear( - hidden_states.float(), gate_lora_delta.float() - ) - router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] - - # Bias-corrected scores for expert selection (via moe_statics module) - scores_for_choice = base_gate.moe_statics(router_probs) # [T, E] - - # Select top-k experts using biased scores - _, selected_experts = torch.topk(scores_for_choice, K, dim=-1) # [T, K] - - # Gather weights from *original* (unbiased) softmax probs - top_values = torch.gather(router_probs, dim=-1, index=selected_experts) # [T, K] - - # Renormalize with clamp(min=norm_min) instead of sum+epsilon - norm_min = getattr(base_gate, "norm_min", 1e-20) - top_values = top_values / torch.clamp( - top_values.sum(dim=-1, keepdim=True), min=norm_min - ) - - # Flatten for moe_general_routing_inputs - token_indices = ( - torch.arange(T, device=hidden_states.device, dtype=torch.int32) - .unsqueeze(1) - .expand(T, K) - ) - - flat_scores = top_values.to(torch.float32).reshape(-1) # [T*K] - flat_token_idx = token_indices.reshape(-1) # [T*K] - flat_expert_idx = selected_experts.to(torch.int32).reshape(-1) # [T*K] - - return flat_scores, flat_token_idx, flat_expert_idx, router_logits - - -def softmax_group_limited_topk_routing( - hidden_states: torch.Tensor, moe_block -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: - """DeepSeek V2 routing: softmax → group_limited_greedy/greedy → topk → scale. - - Differs from softmax_group_topk_routing (Mistral4) in several ways: - 1. Uses ``num_group`` attribute (not ``n_group``). - 2. Group score = max per group (not sum of top-2). - 3. Supports ``greedy`` method (plain topk without groups). - 4. No renormalization — just ``topk_weight * routed_scaling_factor``. - 5. Gate is ``nn.Linear`` (access weight via ``gate.weight``). - - Reference: DeepseekV2Moe.route_tokens_to_experts in transformers. - - Args: - hidden_states: [T, H] flattened token representations - moe_block: MoE block module (accesses moe_block.gate, .num_group, - .topk_group, .top_k, .topk_method, .routed_scaling_factor) - - Returns: - router_scores: [T*K] flattened scores (float32) - token_indices: [T*K] which token each entry belongs to (int32), sorted ascending - expert_indices: [T*K] which expert (int32) - router_logits: [T, E] original logits for aux loss - """ - base_gate, gate_weight, gate_lora_delta = unwrap_gate_lora(moe_block.gate) - T, H = hidden_states.shape - K = moe_block.top_k - num_group = getattr(moe_block, "num_group", 1) - num_experts = gate_weight.shape[0] - topk_method = getattr(moe_block, "topk_method", "greedy") - - # Compute logits in float32 and softmax - router_logits = F.linear(hidden_states.float(), gate_weight.float()) # [T, E] - if gate_lora_delta is not None: - router_logits = router_logits + F.linear( - hidden_states.float(), gate_lora_delta.float() - ) - router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] - - if topk_method == "greedy" or num_group == 1: - topk_weights, topk_indices = torch.topk(router_probs, k=K, dim=-1, sorted=False) - elif topk_method == "group_limited_greedy": - # Guard: selected groups must contain enough experts for topk - group_size = num_experts // num_group - if moe_block.topk_group * group_size < K: - raise ValueError( - f"DeepSeek V2: topk_group ({moe_block.topk_group}) * group_size " - f"({group_size}) = {moe_block.topk_group * group_size} < top_k ({K}). " - f"Not enough experts in selected groups for topk selection." - ) - # Group selection: pick top groups by max score per group - group_scores = ( - router_probs.view(T, num_group, num_experts // num_group).max(dim=-1).values - ) # [T, num_group] - group_idx = torch.topk( - group_scores, k=moe_block.topk_group, dim=-1, sorted=False - )[1] - group_mask = torch.zeros_like(group_scores) - group_mask.scatter_(1, group_idx, 1) - score_mask = ( - group_mask.unsqueeze(-1) - .expand(T, num_group, num_experts // num_group) - .reshape(T, -1) - ) - tmp_scores = router_probs.masked_fill(~score_mask.bool(), 0.0) - topk_weights, topk_indices = torch.topk(tmp_scores, k=K, dim=-1, sorted=False) - else: - raise ValueError( - f"DeepSeek V2: unsupported topk_method '{topk_method}'. " - f"Expected 'greedy' or 'group_limited_greedy'." - ) - - # Scale only — no renormalization (weights won't sum to 1.0 per token). - # This matches the reference DeepseekV2Moe.route_tokens_to_experts behavior. - routed_scaling_factor = getattr(moe_block, "routed_scaling_factor", 1.0) - topk_weights = topk_weights * routed_scaling_factor - - # Flatten for moe_general_routing_inputs - token_indices = ( - torch.arange(T, device=hidden_states.device, dtype=torch.int32) - .unsqueeze(1) - .expand(T, K) - ) - - flat_scores = topk_weights.to(torch.float32).reshape(-1) # [T*K] - flat_token_idx = token_indices.reshape(-1) # [T*K] - flat_expert_idx = topk_indices.to(torch.int32).reshape(-1) # [T*K] - - return flat_scores, flat_token_idx, flat_expert_idx, router_logits - - -def softmax_topk_wg_routing( - hidden_states: torch.Tensor, moe_block -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: - """HunYuan V1 MoE routing: softmax → topk → renorm (gate weight via gate.wg). - - Differs from standard softmax_topk_routing in: - 1. Gate weight lives at ``gate.wg.weight`` (not ``gate.weight``). - 2. ``top_k`` is on ``moe_block`` (not ``gate``). - 3. Always renormalizes (no ``norm_topk_prob`` flag). - - Reference: HunYuanMoEV1Moe.route_tokens_to_experts and - HunYuanMoEV1Gate.forward in transformers. - - Args: - hidden_states: [T, H] flattened token representations - moe_block: MoE block module (accesses moe_block.gate.wg, moe_block.top_k) - - Returns: - router_scores: [T*K] flattened scores (float32) - token_indices: [T*K] which token each entry belongs to (int32), sorted ascending - expert_indices: [T*K] which expert (int32) - router_logits: [T, E] original logits for aux loss - """ - gate = moe_block.gate - T, H = hidden_states.shape - K = moe_block.top_k - - # Gate computes logits via gate.wg (nn.Linear, float32) - # Unwrap at gate.wg level since PEFT targets the wg Linear, not the gate container - base_wg, wg_weight, wg_lora_delta = unwrap_gate_lora(gate.wg) - router_logits = F.linear(hidden_states.float(), wg_weight.float()) # [T, E] - if wg_lora_delta is not None: - router_logits = router_logits + F.linear( - hidden_states.float(), wg_lora_delta.float() - ) - router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] - - # Select top-k experts - top_values, top_indices = torch.topk(router_probs, K, dim=-1) # [T, K] each - - # Always renormalize (HunYuan V1 has no norm_topk_prob flag) - top_values = top_values / (top_values.sum(dim=-1, keepdim=True) + 1e-20) - - # Flatten for moe_general_routing_inputs - token_indices = ( - torch.arange(T, device=hidden_states.device, dtype=torch.int32) - .unsqueeze(1) - .expand(T, K) - ) - - flat_scores = top_values.to(torch.float32).reshape(-1) # [T*K] - flat_token_idx = token_indices.reshape(-1) # [T*K] - flat_expert_idx = top_indices.to(torch.int32).reshape(-1) # [T*K] - - return flat_scores, flat_token_idx, flat_expert_idx, router_logits - - -def gemma4_routing( - hidden_states: torch.Tensor, moe_block -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: - """Gemma4-style routing: RMSNorm → scale → proj → softmax → topk → renorm → per_expert_scale. - - Gemma4's router (``Gemma4TextRouter``) has a unique structure: - 1. RMSNorm (without learnable scale) on hidden states - 2. Multiply by ``scale * hidden_size**-0.5`` - 3. Linear projection to expert scores - 4. Softmax → topk - 5. Normalize top-k weights to sum to 1 - 6. Multiply by per-expert learned scales - - The router lives at ``moe_block.router`` (not ``moe_block.gate``). - LoRA on the router targets ``router.proj`` (nn.Linear). - - Args: - hidden_states: [T, H] flattened token representations - moe_block: MoE block module (accesses moe_block.router) - - Returns: - router_scores: [T*K] flattened scores (float32) - token_indices: [T*K] which token each entry belongs to (int32), sorted ascending - expert_indices: [T*K] which expert (int32) - router_logits: [T, E] original logits for aux loss - """ - router = moe_block.router - - # Unwrap PEFT LoRA on router.proj (the nn.Linear) - _, proj_weight, proj_lora_delta = unwrap_gate_lora(router.proj) - - T, _ = hidden_states.shape - K = router.top_k if hasattr(router, "top_k") else router.config.top_k_experts - - # Reproduce Gemma4TextRouter.forward: - # 1. RMSNorm (no scale) + scale param * hidden_size**-0.5 - normed = router.norm(hidden_states) - scaled = normed * router.scale * router.scalar_root_size - - # 2. Project to expert scores - router_logits = F.linear(scaled.float(), proj_weight.float()) # [T, E] - if proj_lora_delta is not None: - router_logits = router_logits + F.linear( - scaled.float(), proj_lora_delta.float() - ) - - # 3. Softmax → topk - router_probs = F.softmax(router_logits, dim=-1, dtype=torch.float32) # [T, E] - top_values, top_indices = torch.topk(router_probs, K, dim=-1) # [T, K] - - # 4. Normalize top-k weights - top_values = top_values / top_values.sum(dim=-1, keepdim=True) - - # 5. Per-expert scale - top_values = top_values * router.per_expert_scale[top_indices] - - # Flatten for moe_general_routing_inputs - token_indices = ( - torch.arange(T, device=hidden_states.device, dtype=torch.int32) - .unsqueeze(1) - .expand(T, K) - ) - - flat_scores = top_values.to(torch.float32).reshape(-1) # [T*K] - flat_token_idx = token_indices.reshape(-1) # [T*K] - flat_expert_idx = top_indices.to(torch.int32).reshape(-1) # [T*K] - - return flat_scores, flat_token_idx, flat_expert_idx, router_logits diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/weight_converter.py b/src/axolotl/integrations/kernels/libs/sonicmoe/weight_converter.py deleted file mode 100644 index 20da27ff0a..0000000000 --- a/src/axolotl/integrations/kernels/libs/sonicmoe/weight_converter.py +++ /dev/null @@ -1,202 +0,0 @@ -""" -Custom WeightConverter operations for SonicMoE weight format conversion. - -SonicMoE requires gate_up_proj weights in interleaved format: -- Standard (concatenated): [E, 2*I, H] where first I rows are gate, last I rows are up -- SonicMoE (interleaved): [E, 2*I, H] where rows alternate [g0, u0, g1, u1, ...] - -These ConversionOps integrate with transformers' WeightConverter system so that -weights are transparently converted during loading and reverted during saving. -""" - -from typing import Any - -import torch -from einops import rearrange -from transformers.core_model_loading import ConversionOps - -from axolotl.utils.logging import get_logger - -LOG = get_logger(__name__) - - -def interleave_gate_up(tensor: torch.Tensor) -> torch.Tensor: - """[gate..., up...] -> [g0, u0, g1, u1, ...] along the 2*I dimension.""" - return rearrange(tensor, "... (two out) h -> ... (out two) h", two=2) - - -def deinterleave_gate_up(tensor: torch.Tensor) -> torch.Tensor: - """[g0, u0, g1, u1, ...] -> [gate..., up...] along the 2*I dimension.""" - return rearrange(tensor, "... (out two) h -> ... (two out) h", two=2) - - -class ConcatenatedToInterleaved(ConversionOps): - """Convert concatenated gate/up projections to interleaved format. - - Input: [E, 2*I, H] with gate=[E, :I, H] and up=[E, I:, H] - Output: [E, 2*I, H] with rows alternating [g0, u0, g1, u1, ...] - - This operation is applied along ``dim`` (default 1, the 2*I dimension). - """ - - def __init__(self, dim: int = 1): - self.dim = dim - - @torch.no_grad() - def convert( - self, - input_dict: dict[str, Any], - source_patterns: list[str], - target_patterns: list[str], - **kwargs, - ) -> dict[str, torch.Tensor]: - target_pattern = self._get_target_pattern( - input_dict, source_patterns, target_patterns - ) - tensors = next(iter(input_dict.values())) - tensor = tensors[0] if isinstance(tensors, list) else tensors - - interleaved = interleave_gate_up(tensor) - - return {target_pattern: interleaved} - - def _get_target_pattern( - self, - input_dict: dict[str, Any], - source_patterns: list[str], - target_patterns: list[str], - ) -> str: - # Follow the same logic as Transpose.get_target_pattern - if len(input_dict) != 1: - raise ValueError("Undefined Operation encountered!") - if len(target_patterns) > 1: - if len(source_patterns) == 1: - return source_patterns[0] - raise ValueError("Undefined Operation encountered!") - return target_patterns[0] - - @property - def reverse_op(self) -> ConversionOps: - return InterleavedToConcatenated(self.dim) - - -class InterleavedToConcatenated(ConversionOps): - """Convert interleaved gate/up projections back to concatenated format. - - Input: [E, 2*I, H] with rows alternating [g0, u0, g1, u1, ...] - Output: [E, 2*I, H] with gate=[E, :I, H] and up=[E, I:, H] - - This is the reverse of ``ConcatenatedToInterleaved``. - """ - - def __init__(self, dim: int = 1): - self.dim = dim - - @torch.no_grad() - def convert( - self, - input_dict: dict[str, Any], - source_patterns: list[str], - target_patterns: list[str], - **kwargs, - ) -> dict[str, torch.Tensor]: - target_pattern = self._get_target_pattern( - input_dict, source_patterns, target_patterns - ) - tensors = next(iter(input_dict.values())) - tensor = tensors[0] if isinstance(tensors, list) else tensors - - concatenated = deinterleave_gate_up(tensor) - - return {target_pattern: concatenated} - - def _get_target_pattern( - self, - input_dict: dict[str, Any], - source_patterns: list[str], - target_patterns: list[str], - ) -> str: - if len(input_dict) != 1: - raise ValueError("Undefined Operation encountered!") - if len(target_patterns) > 1: - if len(source_patterns) == 1: - return source_patterns[0] - raise ValueError("Undefined Operation encountered!") - return target_patterns[0] - - @property - def reverse_op(self) -> ConversionOps: - return ConcatenatedToInterleaved(self.dim) - - -def _make_same_key_interleave_converter(): - """Create a WeightConverter that interleaves an already-fused gate_up_proj.""" - from transformers.core_model_loading import WeightConverter - - return WeightConverter( - source_patterns="mlp.experts.gate_up_proj", - target_patterns="mlp.experts.gate_up_proj", - operations=[ConcatenatedToInterleaved(dim=1)], - ) - - -def _has_same_key_interleave(mapping) -> bool: - """Check whether the mapping already has a same-key gate_up_proj interleave converter.""" - for conv in mapping: - if ( - hasattr(conv, "source_patterns") - and conv.source_patterns == ["mlp.experts.gate_up_proj"] - and conv.target_patterns == ["mlp.experts.gate_up_proj"] - and hasattr(conv, "operations") - and any(isinstance(op, ConcatenatedToInterleaved) for op in conv.operations) - ): - return True - return False - - -def register_sonicmoe_weight_converter(model_type: str): - """Register weight converters to interleave gate_up_proj for SonicMoE. - - Handles two checkpoint formats: - 1. Separate per-expert weights (e.g. qwen3_moe): appends interleave to the - existing merge chain (MergeModulelist -> Concatenate -> Interleave). - 2. Already-fused gate_up_proj (e.g. qwen3_5_moe_text): adds a same-key - converter (gate_up_proj -> gate_up_proj with Interleave). - - The loader matches whichever source pattern exists in the checkpoint. - """ - from transformers.conversion_mapping import ( - get_checkpoint_conversion_mapping, - register_checkpoint_conversion_mapping, - ) - - existing = get_checkpoint_conversion_mapping(model_type) - - if existing is None: - # No mapping at all — create one with just the same-key converter - mapping = [_make_same_key_interleave_converter()] - register_checkpoint_conversion_mapping(model_type, mapping) - LOG.info(f"Registered SonicMoE weight converter for model type '{model_type}'") - return - - # Append interleave to any existing many-to-one merge chain - for converter in existing: - if hasattr(converter, "operations") and any( - "gate_up_proj" in pat for pat in converter.target_patterns - ): - has_separate_sources = any( - "gate_proj" in pat or "up_proj" in pat - for pat in converter.source_patterns - ) - if has_separate_sources and not any( - isinstance(op, ConcatenatedToInterleaved) for op in converter.operations - ): - converter.operations.append(ConcatenatedToInterleaved(dim=1)) - break - - # Also add a same-key converter for already-fused checkpoints - if not _has_same_key_interleave(existing): - existing.append(_make_same_key_interleave_converter()) - - register_checkpoint_conversion_mapping(model_type, existing, overwrite=True) - LOG.info(f"Registered SonicMoE weight converter for model type '{model_type}'") diff --git a/src/axolotl/integrations/kernels/plugin.py b/src/axolotl/integrations/kernels/plugin.py index d713095a5f..ddddb160f7 100644 --- a/src/axolotl/integrations/kernels/plugin.py +++ b/src/axolotl/integrations/kernels/plugin.py @@ -1,6 +1,5 @@ import importlib import os -from pathlib import Path import torch @@ -61,119 +60,36 @@ def get_input_args(self): return "axolotl.integrations.kernels.KernelsArgs" def pre_model_load(self, cfg): - from axolotl.integrations.kernels.constants import ( - SPARSE_MOE_BLOCK, - is_experts_only_model, - ) + """Register the requested kernel into ``ALL_EXPERTS_FUNCTIONS`` and pin cfg. - # Prefer text backbone type for VLMs, but fall back to base type - # when the text type isn't in the supported mapping (e.g. qwen3_5_moe_text) - moe_model_type = cfg.model_config_type_text or cfg.model_config_type - if ( - moe_model_type not in SPARSE_MOE_BLOCK - and not is_experts_only_model(moe_model_type) - and cfg.model_config_type in SPARSE_MOE_BLOCK - ): - moe_model_type = cfg.model_config_type - - # When expert parallelism is enabled, the EP plugin sets - # `experts_implementation` to `deep_ep_scattermoe` / `deep_ep_sonicmoe` - # and dispatches the kernel inside the experts-level forward (after - # DeepEP all-to-all). Skip the SparseMoeBlock-level patch in that case - # — patching the block-level forward bypasses EP routing and reads - # FSDP-sharded expert weights as DTensors, which the kernels do not - # accept. + Architecture-agnostic: routing stays in each model's SparseMoEBlock; only + the experts call is dispatched through the registry. + """ + # When EP is active, the ExpertParallelPlugin selects a `deep_ep_*` + # composite for `experts_implementation`. Don't overwrite that here — + # plugin order is YAML-defined, so we can't rely on EP running last. ep_active = (getattr(cfg, "expert_parallel_size", 1) or 1) > 1 if cfg.use_scattermoe: - self._register_kernels() - if is_experts_only_model(moe_model_type): - # Models like Gemma4 where MoE is embedded in the decoder layer - # — register ScatterMoE in the ExpertsInterface so that - # @use_experts_implementation dispatches to it. - self._register_experts_interface() - if not ep_active: - cfg.experts_implementation = "scattermoe" - elif ep_active: - LOG.info( - "expert_parallel_size > 1: skipping SparseMoeBlock-level " - "ScatterMoE patch; the deep_ep_scattermoe registered " - "function handles the kernel under EP." - ) - else: - self._kernelize_model(moe_model_type) - elif cfg.use_sonicmoe: - if not importlib.util.find_spec("sonicmoe"): - raise RuntimeError( - "SonicMoE is not installed. See installation instructions at " - "https://github.com/axolotl-ai-cloud/axolotl/blob/main/src/axolotl/integrations/kernels/README.md#sonicmoe-installation" - ) + from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( + register_scattermoe_experts, + ) + register_scattermoe_experts() + if not ep_active: + cfg.experts_implementation = "scattermoe" + LOG.info("Registered 'scattermoe' in transformers ExpertsInterface") + elif cfg.use_sonicmoe: _check_sonicmoe_gpu_compat() - if is_experts_only_model(moe_model_type): - from axolotl.integrations.kernels.libs.sonicmoe.gemma4_experts import ( - patch_gemma4_sonicmoe, - ) - - LOG.info( - f"Applying SonicMoE experts-level patch for model type: {moe_model_type}" - ) - patch_gemma4_sonicmoe() - # TODO(EP+SonicMoE): grad norms explode during training. Re-enable - # once the root cause is identified. Same shape as the ScatterMoE - # branch above, but SonicMoE additionally needs the gate_up_proj - # interleave converter since its w1 layout is [g0, u0, g1, u1, ...] - # while the checkpoint stores it concatenated [gate..., up...]. - # - # elif ep_active: - # from axolotl.integrations.kernels.libs.sonicmoe.weight_converter import ( - # register_sonicmoe_weight_converter, - # ) - # - # LOG.info( - # "expert_parallel_size > 1: skipping SparseMoeBlock-level " - # "SonicMoE patch; the deep_ep_sonicmoe registered function " - # "handles the kernel under EP. Registering gate_up_proj " - # "interleave converter." - # ) - # register_sonicmoe_weight_converter(moe_model_type) - else: - from axolotl.integrations.kernels.libs.sonicmoe import patch_sonicmoe - - LOG.info(f"Applying SonicMoE patches for model type: {moe_model_type}") - patch_sonicmoe( - moe_model_type, - torch_compile=bool(getattr(cfg, "torch_compile", False)), - base_model_type=cfg.model_config_type, - ) - - def _register_kernels(self): - from kernels import ( - LocalLayerRepository, - Mode, - register_kernel_mapping, - ) + from axolotl.integrations.kernels.libs.sonicmoe.experts import ( + register_sonicmoe_experts, + ) - plugin_root = Path(__file__).parent - register_kernel_mapping( - { - "HFScatterMoEParallelExperts": { - "cuda": { - Mode.TRAINING: LocalLayerRepository( - repo_path=plugin_root / "libs" / "scattermoe_lora", - package_name="scattermoe_lora", - layer_name="HFScatterMoEGatedMLP", - ), - Mode.INFERENCE: LocalLayerRepository( - repo_path=plugin_root / "libs" / "scattermoe_lora", - package_name="scattermoe_lora", - layer_name="HFScatterMoEGatedMLP", - ), - }, - } - } - ) + register_sonicmoe_experts() + if not ep_active: + cfg.experts_implementation = "sonicmoe" + LOG.info("Registered 'sonicmoe' in transformers ExpertsInterface") def add_callbacks_pre_trainer(self, cfg, model): callbacks = [] @@ -184,26 +100,3 @@ def add_callbacks_pre_trainer(self, cfg, model): callbacks.append(AutotuneReportCallback()) return callbacks - - def _kernelize_model(self, model_type: str): - from kernels import replace_kernel_forward_from_hub - - from axolotl.integrations.kernels.constants import resolve_moe_block_classes - - for model_moe_cls in resolve_moe_block_classes(model_type): - replace_kernel_forward_from_hub( - model_moe_cls, "HFScatterMoEParallelExperts" - ) - - def _register_experts_interface(self): - """Register ScatterMoE in the transformers ExpertsInterface. - - This allows @use_experts_implementation-decorated Experts classes - to dispatch to ScatterMoE when config._experts_implementation == "scattermoe". - """ - from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( - register_scattermoe_experts, - ) - - register_scattermoe_experts() - LOG.info("Registered 'scattermoe' in transformers ExpertsInterface") diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 9634c1b936..2f056e19f8 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -301,8 +301,23 @@ def _reinitialize_classification_head(self): ) def _configure_experts_implementation(self): - if self.cfg.experts_implementation is not None: - self.model.set_experts_implementation(self.cfg.experts_implementation) + impl = self.cfg.experts_implementation + if impl is None: + return + + if impl in ("scattermoe", "sonicmoe"): + model_classes = { + type(m) for m in self.model.modules() if isinstance(m, PreTrainedModel) + } + if not any(cls._can_set_experts_implementation() for cls in model_classes): + LOG.warning( + f"experts_implementation={impl!r} requested, but no submodule of " + f"{type(self.model).__name__} uses transformers' ExpertsInterface " + "(@use_experts_implementation). The kernel will NOT be applied; " + "training falls back to the model's native experts path." + ) + + self.model.set_experts_implementation(impl) def _apply_activation_checkpointing(self): if self.cfg.activation_offloading is True: diff --git a/tests/e2e/integrations/test_sonicmoe.py b/tests/e2e/integrations/test_sonicmoe.py index ff8620b2fa..b74e570d02 100644 --- a/tests/e2e/integrations/test_sonicmoe.py +++ b/tests/e2e/integrations/test_sonicmoe.py @@ -1,13 +1,17 @@ -""" -End-to-end gradient and convergence tests for SonicMoE integration. +"""End-to-end gradient and convergence tests for SonicMoE integration. -Requires: - - H100/H200 GPU (SonicMoE CUTLASS kernels target sm_90) - - sonicmoe package installed - - transformers with Qwen3MoE support +Flow: + + register_sonicmoe_experts() # plug into ALL_EXPERTS_FUNCTIONS + config._experts_implementation = "sonicmoe" + model = AutoModelForCausalLM.from_config(config) # transformers dispatches -Usage: - pytest tests/e2e/integrations/test_sonicmoe.py -v -s +No weight interleaving needed (``concat_layout=True``). + +Requires: + - Hopper (sm_90) or Blackwell (sm_100+) GPU + - sonic-moe >= 0.1.2 installed from source + - transformers >= 5.8 with Qwen3MoE Experts class """ import importlib.util @@ -16,20 +20,29 @@ import pytest import torch -_sonicmoe_available = importlib.util.find_spec("sonicmoe") is not None -_is_hopper = torch.cuda.is_available() and torch.cuda.get_device_capability() == (9, 0) + +def _is_hopper_or_newer() -> bool: + if not torch.cuda.is_available(): + return False + major, _ = torch.cuda.get_device_capability() + return major >= 9 + pytestmark = [ pytest.mark.skipif(not torch.cuda.is_available(), reason="Requires CUDA GPU"), pytest.mark.skipif( - not _is_hopper, reason="SonicMoE CUTLASS kernels require Hopper (sm_90)" + not _is_hopper_or_newer(), + reason="SonicMoE requires Hopper (sm_90) or Blackwell (sm_100+)", + ), + pytest.mark.skipif( + importlib.util.find_spec("kernels") is None, + reason="HF `kernels` package not installed", ), - pytest.mark.skipif(not _sonicmoe_available, reason="SonicMoE not installed"), ] -def _create_tiny_qwen3_config(): - """Create a minimal Qwen3MoE config for fast testing.""" +def _create_tiny_qwen3_config(experts_implementation: str): + """Create a minimal Qwen3MoE config bound to the requested experts impl.""" from transformers import AutoConfig config = AutoConfig.for_model("qwen3_moe") @@ -46,137 +59,85 @@ def _create_tiny_qwen3_config(): config.max_position_embeddings = 128 config.norm_topk_prob = True config.torch_dtype = torch.bfloat16 + config._experts_implementation = experts_implementation return config -def _interleave_gate_up_weights(model): - """Interleave all gate_up_proj parameters in-place for SonicMoE.""" - from axolotl.integrations.kernels.libs.sonicmoe.weight_converter import ( - interleave_gate_up, - ) - - with torch.no_grad(): - for name, param in model.named_parameters(): - if "gate_up_proj" in name: - param.copy_(interleave_gate_up(param)) +def _build_model(experts_implementation: str): + from transformers import AutoModelForCausalLM + from axolotl.integrations.kernels.libs.sonicmoe.experts import ( + register_sonicmoe_experts, + ) -def _unpatch_sonicmoe(): - """Restore original forward on the MoE block class if it was patched.""" - from axolotl.integrations.kernels.constants import resolve_moe_block_classes - - for moe_cls in resolve_moe_block_classes("qwen3_moe"): - if hasattr(moe_cls, "_original_forward"): - moe_cls.forward = moe_cls._original_forward - del moe_cls._original_forward + register_sonicmoe_experts() + config = _create_tiny_qwen3_config(experts_implementation) + return AutoModelForCausalLM.from_config(config).cuda().bfloat16(), config class TestSonicMoEForwardCorrectness: - """Verify SonicMoE-patched model produces same output as original.""" - - def teardown_method(self): - _unpatch_sonicmoe() - - def test_forward_output_matches(self): - from transformers import AutoModelForCausalLM - - from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe - - config = _create_tiny_qwen3_config() - input_ids = torch.randint(0, config.vocab_size, (1, 16), device="cuda") + """SonicMoE-dispatched model produces output close to eager baseline.""" - # Original model - model_orig = AutoModelForCausalLM.from_config(config).cuda().bfloat16() + def test_forward_output_matches_eager(self): + input_ids = torch.randint(0, 1000, (1, 16), device="cuda") + eager_model, _ = _build_model("eager") with torch.no_grad(): - out_orig = model_orig(input_ids) + out_eager = eager_model(input_ids).logits - # Patched model (same weights, interleaved for SonicMoE) - model_patched = AutoModelForCausalLM.from_config(config).cuda().bfloat16() - model_patched.load_state_dict(model_orig.state_dict()) - - patch_sonicmoe("qwen3_moe") - _interleave_gate_up_weights(model_patched) + sonic_model, _ = _build_model("sonicmoe") + sonic_model.load_state_dict(eager_model.state_dict()) with torch.no_grad(): - out_patched = model_patched(input_ids) + out_sonic = sonic_model(input_ids).logits - max_diff = (out_orig.logits - out_patched.logits).abs().max().item() - assert torch.allclose( - out_orig.logits, out_patched.logits, atol=1e-1, rtol=1e-1 - ), f"Output mismatch: max diff={max_diff:.6f}" + max_diff = (out_eager - out_sonic).abs().max().item() + assert torch.allclose(out_eager, out_sonic, atol=1e-1, rtol=1e-1), ( + f"Output mismatch: max diff={max_diff:.6f}" + ) class TestSonicMoEGradientCorrectness: - """Compare gradients between original HuggingFace and SonicMoE-patched forward.""" - - def teardown_method(self): - _unpatch_sonicmoe() + """Compare gradients between eager and SonicMoE-dispatched forward.""" def test_gradients_match(self): - """Verify all parameter gradients match between original and patched.""" - from transformers import AutoModelForCausalLM - - from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe - from axolotl.integrations.kernels.libs.sonicmoe.weight_converter import ( - deinterleave_gate_up, - ) + input_ids = torch.randint(0, 1000, (1, 16), device="cuda") - config = _create_tiny_qwen3_config() - input_ids = torch.randint(0, config.vocab_size, (1, 16), device="cuda") + eager_model, _ = _build_model("eager") + out_eager = eager_model(input_ids, labels=input_ids) + out_eager.loss.backward() + grads_eager = { + n: p.grad.float().clone() + for n, p in eager_model.named_parameters() + if p.grad is not None + } + loss_eager = out_eager.loss.item() - # ---------- Original model ---------- - model_orig = AutoModelForCausalLM.from_config(config).cuda().bfloat16() - out_orig = model_orig(input_ids, labels=input_ids) - out_orig.loss.backward() - grads_orig = { + sonic_model, _ = _build_model("sonicmoe") + sonic_model.load_state_dict(eager_model.state_dict()) + out_sonic = sonic_model(input_ids, labels=input_ids) + out_sonic.loss.backward() + grads_sonic = { n: p.grad.float().clone() - for n, p in model_orig.named_parameters() + for n, p in sonic_model.named_parameters() if p.grad is not None } - loss_orig = out_orig.loss.item() - - # ---------- SonicMoE-patched model (same weights, interleaved) ---------- - model_patched = AutoModelForCausalLM.from_config(config).cuda().bfloat16() - model_patched.load_state_dict(model_orig.state_dict()) - - patch_sonicmoe("qwen3_moe") - _interleave_gate_up_weights(model_patched) - - out_patched = model_patched(input_ids, labels=input_ids) - out_patched.loss.backward() - grads_patched = {} - for n, p in model_patched.named_parameters(): - if p.grad is None: - continue - g = p.grad.float().clone() - # gate_up_proj grads are in interleaved layout, de-interleave to match orig - if "gate_up_proj" in n: - g = deinterleave_gate_up(g) - grads_patched[n] = g - loss_patched = out_patched.loss.item() - - # ---------- Compare ---------- - assert abs(loss_orig - loss_patched) < 0.5, ( - f"Loss mismatch: orig={loss_orig:.4f}, patched={loss_patched:.4f}" + loss_sonic = out_sonic.loss.item() + + assert abs(loss_eager - loss_sonic) < 0.5, ( + f"Loss mismatch: eager={loss_eager:.4f}, sonic={loss_sonic:.4f}" ) - # All parameters with gradients in original should have them in patched - missing = set(grads_orig.keys()) - set(grads_patched.keys()) - assert not missing, f"Missing gradients in patched model: {missing}" + missing = set(grads_eager.keys()) - set(grads_sonic.keys()) + assert not missing, f"Missing gradients in sonicmoe model: {missing}" - # Compare gradient values - # bf16 with different GEMM impls (cuBLAS vs CUTLASS) can diverge, - # so use generous tolerance: flag only if both rel >10% AND abs >1e-2 + # bf16 + different GEMM backends can diverge; tolerate both rel >10% AND + # abs >1e-2 together. mismatches = [] - for name in grads_orig: - if name not in grads_patched: - continue - g_orig = grads_orig[name] - g_patched = grads_patched[name] - max_diff = (g_orig - g_patched).abs().max().item() - rel_diff = max_diff / (g_orig.abs().max().item() + 1e-8) - + for name, g_eager in grads_eager.items(): + g_sonic = grads_sonic[name] + max_diff = (g_eager - g_sonic).abs().max().item() + rel_diff = max_diff / (g_eager.abs().max().item() + 1e-8) if rel_diff > 0.1 and max_diff > 1e-2: mismatches.append( f" {name}: max_abs_diff={max_diff:.6f}, rel_diff={rel_diff:.4f}" @@ -188,18 +149,8 @@ def test_gradients_match(self): ) def test_router_weights_receive_gradients(self): - """Verify that router (gate) weights get non-zero gradients.""" - from transformers import AutoModelForCausalLM - - from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe - - config = _create_tiny_qwen3_config() - input_ids = torch.randint(0, config.vocab_size, (1, 16), device="cuda") - - model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() - patch_sonicmoe("qwen3_moe") - _interleave_gate_up_weights(model) - + input_ids = torch.randint(0, 1000, (1, 16), device="cuda") + model, _ = _build_model("sonicmoe") out = model(input_ids, labels=input_ids) out.loss.backward() @@ -216,21 +167,9 @@ def test_router_weights_receive_gradients(self): class TestSonicMoETrainingConvergence: """Verify loss decreases during training with SonicMoE.""" - def teardown_method(self): - _unpatch_sonicmoe() - def test_loss_decreases(self): - """Run 30 training steps, verify loss decreases and no NaN/Inf.""" - from transformers import AutoModelForCausalLM - - from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe - - config = _create_tiny_qwen3_config() - input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") - - model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() - patch_sonicmoe("qwen3_moe") - _interleave_gate_up_weights(model) + input_ids = torch.randint(0, 1000, (2, 32), device="cuda") + model, _ = _build_model("sonicmoe") optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3) losses = [] @@ -251,24 +190,14 @@ def test_loss_decreases(self): ) def test_expert_weights_update(self): - """Verify expert weights change during training (not frozen).""" - from transformers import AutoModelForCausalLM - - from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe - - config = _create_tiny_qwen3_config() - input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") - - model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() - patch_sonicmoe("qwen3_moe") - _interleave_gate_up_weights(model) - - # Snapshot expert weights before training - expert_weights_before = {} - for name, param in model.named_parameters(): - if "experts" in name: - expert_weights_before[name] = param.data.clone() + input_ids = torch.randint(0, 1000, (2, 32), device="cuda") + model, _ = _build_model("sonicmoe") + expert_weights_before = { + name: param.data.clone() + for name, param in model.named_parameters() + if "experts" in name + } assert expert_weights_before, "No expert parameters found" optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3) @@ -278,11 +207,10 @@ def test_expert_weights_update(self): optimizer.step() optimizer.zero_grad() - # Check that expert weights changed - changed = 0 - for name, param in model.named_parameters(): - if name in expert_weights_before: - if not torch.equal(param.data, expert_weights_before[name]): - changed += 1 - + changed = sum( + 1 + for name, param in model.named_parameters() + if name in expert_weights_before + and not torch.equal(param.data, expert_weights_before[name]) + ) assert changed > 0, "No expert weights changed after 5 training steps" diff --git a/tests/e2e/integrations/test_sonicmoe_lora.py b/tests/e2e/integrations/test_sonicmoe_lora.py index 74721ee57a..cc58f4dccd 100644 --- a/tests/e2e/integrations/test_sonicmoe_lora.py +++ b/tests/e2e/integrations/test_sonicmoe_lora.py @@ -2,21 +2,24 @@ # Copyright (c) Axolotl AI # Licensed under the Apache License, Version 2.0 -""" -End-to-end tests for SonicMoE + LoRA integration. +"""End-to-end tests for SonicMoE + LoRA. -Verifies that PEFT-wrapped MoE models work correctly with SonicMoE's -runtime LoRA materialization: gradients flow to adapters, base weights -stay frozen, and loss converges. +Flow: -Requires: - - H100/H200 GPU (SonicMoE CUTLASS kernels target sm_90) - - sonicmoe package installed - - peft package installed - - transformers with Qwen3MoE support + register_sonicmoe_experts() # plug into ALL_EXPERTS_FUNCTIONS + config._experts_implementation = "sonicmoe" + model = AutoModelForCausalLM.from_config(config) + model = get_peft_model(model, lora_config) # PEFT wraps params/modules -Usage: - pytest tests/e2e/integrations/test_sonicmoe_lora.py -v -s +``sonicmoe_experts_forward_with_lora`` detects the PEFT wrappers and +materializes ``W_eff = W + scaling * (B @ A)`` via :class:`MoELoRAMaterialize`, +so adapters train through the CUTLASS kernels. + +Requires: + - Hopper (sm_90) or Blackwell (sm_100+) GPU + - sonic-moe >= 0.1.2 installed from source + - peft installed + - transformers >= 5.8 with Qwen3MoE Experts class """ import importlib.util @@ -25,22 +28,31 @@ import pytest import torch -_sonicmoe_available = importlib.util.find_spec("sonicmoe") is not None -_peft_available = importlib.util.find_spec("peft") is not None -_is_hopper = torch.cuda.is_available() and torch.cuda.get_device_capability() == (9, 0) + +def _is_hopper_or_newer() -> bool: + if not torch.cuda.is_available(): + return False + major, _ = torch.cuda.get_device_capability() + return major >= 9 + pytestmark = [ pytest.mark.skipif(not torch.cuda.is_available(), reason="Requires CUDA GPU"), pytest.mark.skipif( - not _is_hopper, reason="SonicMoE CUTLASS kernels require Hopper (sm_90)" + not _is_hopper_or_newer(), + reason="SonicMoE requires Hopper (sm_90) or Blackwell (sm_100+)", + ), + pytest.mark.skipif( + importlib.util.find_spec("kernels") is None, + reason="HF `kernels` package not installed", + ), + pytest.mark.skipif( + importlib.util.find_spec("peft") is None, reason="PEFT not installed" ), - pytest.mark.skipif(not _sonicmoe_available, reason="SonicMoE not installed"), - pytest.mark.skipif(not _peft_available, reason="PEFT not installed"), ] def _create_tiny_qwen3_config(): - """Create a minimal Qwen3MoE config for fast testing.""" from transformers import AutoConfig config = AutoConfig.for_model("qwen3_moe") @@ -57,33 +69,23 @@ def _create_tiny_qwen3_config(): config.max_position_embeddings = 128 config.norm_topk_prob = True config.torch_dtype = torch.bfloat16 + config._experts_implementation = "sonicmoe" return config -def _interleave_gate_up_weights(model): - """Interleave all gate_up_proj parameters in-place for SonicMoE.""" - from axolotl.integrations.kernels.libs.sonicmoe.weight_converter import ( - interleave_gate_up, - ) - - with torch.no_grad(): - for name, param in model.named_parameters(): - if "gate_up_proj" in name: - param.copy_(interleave_gate_up(param)) +def _build_sonic_model(): + from transformers import AutoModelForCausalLM + from axolotl.integrations.kernels.libs.sonicmoe.experts import ( + register_sonicmoe_experts, + ) -def _unpatch_sonicmoe(): - """Restore original forward on the MoE block class if it was patched.""" - from axolotl.integrations.kernels.constants import resolve_moe_block_classes - - for moe_cls in resolve_moe_block_classes("qwen3_moe"): - if hasattr(moe_cls, "_original_forward"): - moe_cls.forward = moe_cls._original_forward - del moe_cls._original_forward + register_sonicmoe_experts() + config = _create_tiny_qwen3_config() + return AutoModelForCausalLM.from_config(config).cuda().bfloat16() def _apply_lora(model, target_modules): - """Apply PEFT LoRA to the model.""" from peft import LoraConfig, get_peft_model lora_config = LoraConfig( @@ -97,37 +99,23 @@ def _apply_lora(model, target_modules): class TestSonicMoELoRATraining: - """Verify SonicMoE + LoRA training works end-to-end.""" - - def teardown_method(self): - _unpatch_sonicmoe() + """SonicMoE + LoRA on expert projections trains end-to-end.""" def test_loss_decreases(self): - """Run 30 training steps with LoRA on experts, verify loss decreases.""" - from transformers import AutoModelForCausalLM - - from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe - - config = _create_tiny_qwen3_config() - input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") - - model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() - patch_sonicmoe("qwen3_moe") - _interleave_gate_up_weights(model) + input_ids = torch.randint(0, 1000, (2, 32), device="cuda") + model = _build_sonic_model() model = _apply_lora(model, ["gate_up_proj", "down_proj"]) optimizer = torch.optim.AdamW( [p for p in model.parameters() if p.requires_grad], lr=1e-3 ) losses = [] - for step in range(30): out = model(input_ids, labels=input_ids) loss = out.loss assert not math.isnan(loss.item()), f"NaN loss at step {step}" assert not math.isinf(loss.item()), f"Inf loss at step {step}" losses.append(loss.item()) - loss.backward() optimizer.step() optimizer.zero_grad() @@ -137,24 +125,15 @@ def test_loss_decreases(self): ) def test_base_weights_frozen(self): - """Verify base (non-LoRA) weights don't change during training.""" - from transformers import AutoModelForCausalLM - - from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe - - config = _create_tiny_qwen3_config() - input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") - - model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() - patch_sonicmoe("qwen3_moe") - _interleave_gate_up_weights(model) + input_ids = torch.randint(0, 1000, (2, 32), device="cuda") + model = _build_sonic_model() model = _apply_lora(model, ["gate_up_proj", "down_proj"]) - # Snapshot frozen weights - frozen_before = {} - for name, param in model.named_parameters(): - if not param.requires_grad: - frozen_before[name] = param.data.clone() + frozen_before = { + name: param.data.clone() + for name, param in model.named_parameters() + if not param.requires_grad + } optimizer = torch.optim.AdamW( [p for p in model.parameters() if p.requires_grad], lr=1e-3 @@ -165,24 +144,13 @@ def test_base_weights_frozen(self): optimizer.step() optimizer.zero_grad() - for name, param in model.named_parameters(): - if name in frozen_before: - assert torch.equal(param.data, frozen_before[name]), ( - f"Frozen weight changed: {name}" - ) + for name, before in frozen_before.items(): + after = dict(model.named_parameters())[name] + assert torch.equal(after.data, before), f"Frozen weight changed: {name}" def test_lora_adapters_receive_gradients(self): - """Verify LoRA A and B matrices get non-zero gradients.""" - from transformers import AutoModelForCausalLM - - from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe - - config = _create_tiny_qwen3_config() - input_ids = torch.randint(0, config.vocab_size, (1, 16), device="cuda") - - model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() - patch_sonicmoe("qwen3_moe") - _interleave_gate_up_weights(model) + input_ids = torch.randint(0, 1000, (1, 16), device="cuda") + model = _build_sonic_model() model = _apply_lora(model, ["gate_up_proj", "down_proj"]) out = model(input_ids, labels=input_ids) @@ -200,25 +168,15 @@ def test_lora_adapters_receive_gradients(self): assert lora_grads_found > 0, "No LoRA parameters found with gradients" def test_lora_adapters_update(self): - """Verify LoRA adapter weights change during training.""" - from transformers import AutoModelForCausalLM - - from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe - - config = _create_tiny_qwen3_config() - input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") - - model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() - patch_sonicmoe("qwen3_moe") - _interleave_gate_up_weights(model) + input_ids = torch.randint(0, 1000, (2, 32), device="cuda") + model = _build_sonic_model() model = _apply_lora(model, ["gate_up_proj", "down_proj"]) - # Snapshot LoRA weights - lora_before = {} - for name, param in model.named_parameters(): - if "lora_" in name and param.requires_grad: - lora_before[name] = param.data.clone() - + lora_before = { + name: param.data.clone() + for name, param in model.named_parameters() + if "lora_" in name and param.requires_grad + } assert lora_before, "No LoRA parameters found" optimizer = torch.optim.AdamW( @@ -239,38 +197,23 @@ def test_lora_adapters_update(self): class TestSonicMoEGateOnlyLoRA: - """Verify LoRA targeting only the gate (router) works with SonicMoE.""" - - def teardown_method(self): - _unpatch_sonicmoe() + """LoRA only on the router (gate) — expert path takes the no-LoRA fast path.""" def test_gate_only_lora_loss_decreases(self): - """LoRA only on gate — expert path should have zero materialization overhead.""" - from transformers import AutoModelForCausalLM - - from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe - - config = _create_tiny_qwen3_config() - input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") - - model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() - patch_sonicmoe("qwen3_moe") - _interleave_gate_up_weights(model) - # Only target the gate (router), not expert projections + input_ids = torch.randint(0, 1000, (2, 32), device="cuda") + model = _build_sonic_model() model = _apply_lora(model, ["gate"]) optimizer = torch.optim.AdamW( [p for p in model.parameters() if p.requires_grad], lr=1e-3 ) losses = [] - for step in range(20): out = model(input_ids, labels=input_ids) loss = out.loss assert not math.isnan(loss.item()), f"NaN loss at step {step}" assert not math.isinf(loss.item()), f"Inf loss at step {step}" losses.append(loss.item()) - loss.backward() optimizer.step() optimizer.zero_grad() @@ -281,34 +224,20 @@ def test_gate_only_lora_loss_decreases(self): class TestSonicMoENoLoRARegression: - """Verify SonicMoE without LoRA still works after LoRA code was added.""" - - def teardown_method(self): - _unpatch_sonicmoe() + """Full fine-tuning (no PEFT) still works through the registered forward.""" def test_no_lora_loss_decreases(self): - """Full fine-tuning (no PEFT) with SonicMoE — regression test.""" - from transformers import AutoModelForCausalLM - - from axolotl.integrations.kernels.libs.sonicmoe.patch import patch_sonicmoe - - config = _create_tiny_qwen3_config() - input_ids = torch.randint(0, config.vocab_size, (2, 32), device="cuda") - - model = AutoModelForCausalLM.from_config(config).cuda().bfloat16() - patch_sonicmoe("qwen3_moe") - _interleave_gate_up_weights(model) + input_ids = torch.randint(0, 1000, (2, 32), device="cuda") + model = _build_sonic_model() optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3) losses = [] - for step in range(20): out = model(input_ids, labels=input_ids) loss = out.loss assert not math.isnan(loss.item()), f"NaN loss at step {step}" assert not math.isinf(loss.item()), f"Inf loss at step {step}" losses.append(loss.item()) - loss.backward() optimizer.step() optimizer.zero_grad() diff --git a/tests/integrations/test_gemma4_moe.py b/tests/integrations/test_gemma4_moe.py index 412d49b2f9..e3408c7822 100644 --- a/tests/integrations/test_gemma4_moe.py +++ b/tests/integrations/test_gemma4_moe.py @@ -462,94 +462,6 @@ class TestGemma4SonicMoE: import are skipped on unsupported GPUs. """ - @pytest.mark.skipif( - not _can_import_sonicmoe(), - reason="sonicmoe requires Hopper/Blackwell GPU", - ) - def test_gemma4_routing_function_config(self, gemma4_config): - """Gemma4 is registered with correct routing config.""" - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - get_model_moe_config, - ) - - routing_fn, activation, router_attr = get_model_moe_config("gemma4_text") - - assert router_attr == "router" - assert routing_fn is not None - assert routing_fn.__name__ == "gemma4_routing" - - from sonicmoe.enums import ActivationType - - assert activation == ActivationType.GEGLU - - @pytest.mark.skipif( - not _can_import_sonicmoe(), - reason="sonicmoe requires Hopper/Blackwell GPU", - ) - def test_gemma4_routing_matches_reference(self, gemma4_config): - """Routing function output matches reference Gemma4TextRouter.""" - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - get_model_moe_config, - ) - - routing_fn, _, _ = get_model_moe_config("gemma4_text") - H = gemma4_config["hidden_size"] - E = gemma4_config["num_experts"] - K = gemma4_config["top_k"] - T = 16 - - router = Gemma4TextRouter(H, E, K) - nn.init.normal_(router.proj.weight, std=0.01) - - class MockGemma4MoeBlock: - pass - - mock_block = MockGemma4MoeBlock() - mock_block.router = router - - hidden_states = torch.randn(T, H) - - # Reference - _ref_probs, ref_weights, ref_indices = router(hidden_states) - - # Routing function - flat_scores, flat_token_idx, flat_expert_idx, router_logits = routing_fn( - hidden_states, mock_block - ) - - # Check shapes - assert flat_scores.shape == (T * K,) - assert flat_token_idx.shape == (T * K,) - assert flat_expert_idx.shape == (T * K,) - assert router_logits.shape == (T, E) - - # Reconstruct per-token routing from flat output and compare - for t in range(T): - mask = flat_token_idx == t - assert mask.sum() == K, f"Token {t} should have {K} entries" - - flat_experts_for_t = flat_expert_idx[mask].sort().values - ref_experts_for_t = ref_indices[t].sort().values.to(torch.int32) - assert torch.equal(flat_experts_for_t, ref_experts_for_t), ( - f"Token {t}: experts mismatch" - ) - - # Verify scores match reference per-token - for t in range(T): - mask = flat_token_idx == t - flat_experts_t = flat_expert_idx[mask] - flat_scores_t = flat_scores[mask] - - sort_idx = flat_experts_t.argsort() - flat_scores_sorted = flat_scores_t[sort_idx] - - ref_sort_idx = ref_indices[t].argsort() - ref_scores_sorted = ref_weights[t][ref_sort_idx].float() - - torch.testing.assert_close( - flat_scores_sorted, ref_scores_sorted, atol=1e-4, rtol=1e-4 - ) - def test_gemma4_weight_layout_compatible(self, gemma4_config): """Verify Gemma4 expert weight layout is compatible with SonicMoE.""" E = gemma4_config["num_experts"] @@ -582,12 +494,6 @@ def test_gemma4_is_experts_only_model(self): assert cls is not None assert cls.__name__ == "Gemma4TextExperts" - def test_gemma4_not_in_sparse_moe_block(self): - """Verify gemma4_text is NOT in SPARSE_MOE_BLOCK (has no SparseMoeBlock).""" - from axolotl.integrations.kernels.constants import SPARSE_MOE_BLOCK - - assert "gemma4_text" not in SPARSE_MOE_BLOCK - # ============================================================================ # Integration Tests (full layer with real model config) @@ -790,7 +696,7 @@ def test_register_scattermoe_in_experts_interface(self): """register_scattermoe_experts adds 'scattermoe' to the global interface.""" from transformers.integrations.moe import ALL_EXPERTS_FUNCTIONS - from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( register_scattermoe_experts, scattermoe_experts_forward, ) @@ -806,7 +712,7 @@ def test_experts_implementation_dispatches_to_scattermoe(self, device): Gemma4TextExperts as HFGemma4TextExperts, ) - from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( register_scattermoe_experts, ) @@ -853,7 +759,7 @@ def test_validation_accepts_scattermoe(self): """get_correct_experts_implementation accepts 'scattermoe' after registration.""" from transformers.modeling_utils import PreTrainedModel - from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( register_scattermoe_experts, ) @@ -869,7 +775,7 @@ def test_eager_still_works_after_registration(self, device): Gemma4TextExperts as HFGemma4TextExperts, ) - from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( register_scattermoe_experts, ) @@ -982,7 +888,7 @@ def model_setup(self, request, device): """Create an Experts instance for each model type.""" import importlib - from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_experts import ( + from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( register_scattermoe_experts, ) diff --git a/tests/integrations/test_routing_parity.py b/tests/integrations/test_routing_parity.py deleted file mode 100644 index 8852068096..0000000000 --- a/tests/integrations/test_routing_parity.py +++ /dev/null @@ -1,492 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) Axolotl AI -# Licensed under the Apache License, Version 2.0 - -""" -Parity tests between scattermoe-lora and sonicmoe routing implementations. - -These tests verify that both implementations produce numerically identical -results for the same inputs, ensuring safe centralization of the routing code. - -ScatterMoE returns 2D tensors [T, K]; SonicMoE returns flattened 1D [T*K]. -The core algorithm should be identical — only the output format differs. -""" - -from types import SimpleNamespace - -import pytest -import torch - - -def _require_triton(): - pytest.importorskip("triton") - - -# ============================================================================ -# Fixtures / helpers -# ============================================================================ - - -def _make_softmax_block(T=8, H=16, E=4, K=2): - """Qwen/OLMoE-style block usable by both implementations.""" - gate = SimpleNamespace( - weight=torch.randn(E, H), - top_k=K, - num_experts=E, - norm_topk_prob=True, - ) - moe_block = SimpleNamespace(gate=gate) - hidden = torch.randn(T, H) - return moe_block, gate, hidden, T, H, E, K - - -def _make_sigmoid_block( - T=8, H=16, E=16, K=4, n_group=2, topk_group=1, bias_on_gate=True -): - """GLM/DeepSeek-style block usable by both implementations.""" - if bias_on_gate: - gate = SimpleNamespace( - weight=torch.randn(E, H), - e_score_correction_bias=torch.zeros(E), - ) - moe_block = SimpleNamespace( - gate=gate, - top_k=K, - n_routed_experts=E, - n_group=n_group, - topk_group=topk_group, - norm_topk_prob=True, - routed_scaling_factor=1.0, - ) - else: - # minimax_m2 style: bias on block - gate = SimpleNamespace( - weight=torch.randn(E, H), - top_k=K, - ) - moe_block = SimpleNamespace( - gate=gate, - top_k=K, - e_score_correction_bias=torch.zeros(E), - ) - return moe_block, gate, hidden_states(T, H), T, H, E, K - - -def hidden_states(T, H): - return torch.randn(T, H) - - -# ============================================================================ -# 1. Softmax routing parity -# ============================================================================ - - -class TestSoftmaxRoutingParity: - """Verify scattermoe and sonicmoe softmax routing produce identical results.""" - - @pytest.fixture(autouse=True) - def _require(self): - _require_triton() - - def test_weights_match(self): - """2D weights from scattermoe == reshaped 1D weights from sonicmoe.""" - from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( - _softmax_topk_route, - ) - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_topk_routing, - ) - - moe_block, gate, hidden, T, H, E, K = _make_softmax_block() - - # ScatterMoE path (no LoRA delta) - sm_weights, sm_experts, sm_topk, sm_E = _softmax_topk_route( - moe_block, gate, hidden, gate.weight, None - ) - - # SonicMoE path - sonic_scores, sonic_tok_idx, sonic_exp_idx, sonic_logits = softmax_topk_routing( - hidden, moe_block - ) - - # ScatterMoE returns [T, K], SonicMoE returns [T*K] flattened - sonic_weights_2d = sonic_scores.reshape(T, K) - sonic_experts_2d = sonic_exp_idx.reshape(T, K) - - assert sm_topk == K - assert sm_E == E - - # Both should select the same experts and produce the same weights - assert torch.equal(sm_experts, sonic_experts_2d.to(sm_experts.dtype)) - assert torch.allclose(sm_weights, sonic_weights_2d, atol=1e-6) - - def test_logits_not_returned_by_scattermoe(self): - """ScatterMoE doesn't return logits; SonicMoE does — verify SonicMoE logits shape.""" - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_topk_routing, - ) - - moe_block, gate, hidden, T, H, E, K = _make_softmax_block() - _, _, _, logits = softmax_topk_routing(hidden, moe_block) - assert logits.shape == (T, E) - - def test_no_renorm(self): - """With norm_topk_prob=False, both should skip renormalization.""" - from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( - _softmax_topk_route, - ) - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_topk_routing, - ) - - moe_block, gate, hidden, T, H, E, K = _make_softmax_block() - gate.norm_topk_prob = False - - sm_weights, sm_experts, _, _ = _softmax_topk_route( - moe_block, gate, hidden, gate.weight, None - ) - sonic_scores, _, sonic_exp_idx, _ = softmax_topk_routing(hidden, moe_block) - - sonic_weights_2d = sonic_scores.reshape(T, K) - sonic_experts_2d = sonic_exp_idx.reshape(T, K) - - assert torch.equal(sm_experts, sonic_experts_2d.to(sm_experts.dtype)) - assert torch.allclose(sm_weights, sonic_weights_2d, atol=1e-6) - - def test_various_expert_counts(self): - """Parity across different E and K values.""" - from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( - _softmax_topk_route, - ) - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_topk_routing, - ) - - for E, K in [(2, 1), (8, 2), (16, 4), (32, 8)]: - moe_block, gate, hidden, T, H, _, _ = _make_softmax_block(E=E, K=K) - - sm_weights, sm_experts, _, _ = _softmax_topk_route( - moe_block, gate, hidden, gate.weight, None - ) - sonic_scores, _, sonic_exp_idx, _ = softmax_topk_routing(hidden, moe_block) - - sonic_weights_2d = sonic_scores.reshape(T, K) - sonic_experts_2d = sonic_exp_idx.reshape(T, K) - - assert torch.equal(sm_experts, sonic_experts_2d.to(sm_experts.dtype)), ( - f"Expert mismatch for E={E}, K={K}" - ) - assert torch.allclose(sm_weights, sonic_weights_2d, atol=1e-6), ( - f"Weight mismatch for E={E}, K={K}" - ) - - -# ============================================================================ -# 2. Sigmoid routing parity -# ============================================================================ - - -class TestSigmoidRoutingParity: - """Verify scattermoe and sonicmoe sigmoid routing produce identical results.""" - - @pytest.fixture(autouse=True) - def _require(self): - _require_triton() - - def test_weights_match_with_groups(self): - """Both implementations should produce identical weights with group selection.""" - from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( - _sigmoid_topk_route, - ) - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - sigmoid_topk_routing, - ) - - moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( - E=16, K=4, n_group=2, topk_group=1, bias_on_gate=True - ) - - sm_weights, sm_experts, sm_topk, sm_E = _sigmoid_topk_route( - moe_block, gate, hidden, gate.weight, None - ) - - sonic_scores, sonic_tok_idx, sonic_exp_idx, sonic_logits = sigmoid_topk_routing( - hidden, moe_block - ) - - sonic_weights_2d = sonic_scores.reshape(T, K) - sonic_experts_2d = sonic_exp_idx.reshape(T, K) - - assert sm_topk == K - assert sm_E == E - - # Sort experts within each token to handle different topk orderings - sm_sorted, sm_order = sm_experts.sort(dim=-1) - sonic_sorted, sonic_order = sonic_experts_2d.to(sm_experts.dtype).sort(dim=-1) - - assert torch.equal(sm_sorted, sonic_sorted) - - # Gather weights in sorted order for comparison - sm_weights_sorted = sm_weights.gather(1, sm_order) - sonic_weights_sorted = sonic_weights_2d.gather(1, sonic_order) - assert torch.allclose(sm_weights_sorted, sonic_weights_sorted, atol=1e-6) - - def test_weights_match_no_groups(self): - """Both implementations match without group selection (n_group=1).""" - from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( - _sigmoid_topk_route, - ) - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - sigmoid_topk_routing, - ) - - moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( - E=16, K=4, n_group=1, topk_group=1, bias_on_gate=True - ) - - sm_weights, sm_experts, _, _ = _sigmoid_topk_route( - moe_block, gate, hidden, gate.weight, None - ) - sonic_scores, _, sonic_exp_idx, _ = sigmoid_topk_routing(hidden, moe_block) - - sonic_weights_2d = sonic_scores.reshape(T, K) - sonic_experts_2d = sonic_exp_idx.reshape(T, K) - - # Sort for comparison (topk with sorted=False may differ in order) - sm_sorted, sm_order = sm_experts.sort(dim=-1) - sonic_sorted, sonic_order = sonic_experts_2d.to(sm_experts.dtype).sort(dim=-1) - - assert torch.equal(sm_sorted, sonic_sorted) - sm_weights_sorted = sm_weights.gather(1, sm_order) - sonic_weights_sorted = sonic_weights_2d.gather(1, sonic_order) - assert torch.allclose(sm_weights_sorted, sonic_weights_sorted, atol=1e-6) - - def test_bias_on_block_parity(self): - """minimax_m2 style: bias on block, not gate.""" - from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( - _sigmoid_topk_route, - ) - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - sigmoid_topk_routing, - ) - - moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( - E=16, K=4, n_group=1, bias_on_gate=False - ) - - sm_weights, sm_experts, _, _ = _sigmoid_topk_route( - moe_block, gate, hidden, gate.weight, None - ) - sonic_scores, _, sonic_exp_idx, _ = sigmoid_topk_routing(hidden, moe_block) - - sonic_weights_2d = sonic_scores.reshape(T, K) - sonic_experts_2d = sonic_exp_idx.reshape(T, K) - - sm_sorted, sm_order = sm_experts.sort(dim=-1) - sonic_sorted, sonic_order = sonic_experts_2d.to(sm_experts.dtype).sort(dim=-1) - - assert torch.equal(sm_sorted, sonic_sorted) - sm_weights_sorted = sm_weights.gather(1, sm_order) - sonic_weights_sorted = sonic_weights_2d.gather(1, sonic_order) - assert torch.allclose(sm_weights_sorted, sonic_weights_sorted, atol=1e-6) - - def test_scaling_factor_parity(self): - """routed_scaling_factor applied identically by both.""" - from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( - _sigmoid_topk_route, - ) - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - sigmoid_topk_routing, - ) - - moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( - n_group=1, bias_on_gate=True - ) - moe_block.routed_scaling_factor = 2.5 - - sm_weights, sm_experts, _, _ = _sigmoid_topk_route( - moe_block, gate, hidden, gate.weight, None - ) - sonic_scores, _, sonic_exp_idx, _ = sigmoid_topk_routing(hidden, moe_block) - - sonic_weights_2d = sonic_scores.reshape(T, K) - sonic_experts_2d = sonic_exp_idx.reshape(T, K) - - sm_sorted, sm_order = sm_experts.sort(dim=-1) - sonic_sorted, sonic_order = sonic_experts_2d.to(sm_experts.dtype).sort(dim=-1) - - assert torch.equal(sm_sorted, sonic_sorted) - sm_weights_sorted = sm_weights.gather(1, sm_order) - sonic_weights_sorted = sonic_weights_2d.gather(1, sonic_order) - assert torch.allclose(sm_weights_sorted, sonic_weights_sorted, atol=1e-6) - - def test_no_renorm_parity(self): - """norm_topk_prob=False produces same results in both.""" - from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( - _sigmoid_topk_route, - ) - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - sigmoid_topk_routing, - ) - - moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( - n_group=1, bias_on_gate=True - ) - moe_block.norm_topk_prob = False - - sm_weights, sm_experts, _, _ = _sigmoid_topk_route( - moe_block, gate, hidden, gate.weight, None - ) - sonic_scores, _, sonic_exp_idx, _ = sigmoid_topk_routing(hidden, moe_block) - - sonic_weights_2d = sonic_scores.reshape(T, K) - sonic_experts_2d = sonic_exp_idx.reshape(T, K) - - sm_sorted, sm_order = sm_experts.sort(dim=-1) - sonic_sorted, sonic_order = sonic_experts_2d.to(sm_experts.dtype).sort(dim=-1) - - assert torch.equal(sm_sorted, sonic_sorted) - sm_weights_sorted = sm_weights.gather(1, sm_order) - sonic_weights_sorted = sonic_weights_2d.gather(1, sonic_order) - assert torch.allclose(sm_weights_sorted, sonic_weights_sorted, atol=1e-6) - - -# ============================================================================ -# 3. Shared expert parity -# ============================================================================ - - -class TestSharedExpertParity: - """Verify both _compute_shared_expert implementations behave identically.""" - - @pytest.fixture(autouse=True) - def _require(self): - _require_triton() - - def _get_both_fns(self): - from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( - _compute_shared_expert as scatter_compute, - ) - from axolotl.integrations.kernels.libs.sonicmoe.patch import ( - _compute_shared_expert as sonic_compute, - ) - - return scatter_compute, sonic_compute - - def test_shared_expert_singular(self): - scatter_fn, sonic_fn = self._get_both_fns() - out = torch.randn(4, 8) - block = SimpleNamespace(shared_expert=lambda x: out) - hidden = torch.randn(4, 8) - - assert torch.equal(scatter_fn(block, hidden), sonic_fn(block, hidden)) - - def test_shared_experts_plural(self): - scatter_fn, sonic_fn = self._get_both_fns() - out = torch.randn(4, 8) - block = SimpleNamespace(shared_experts=lambda x: out) - hidden = torch.randn(4, 8) - - assert torch.equal(scatter_fn(block, hidden), sonic_fn(block, hidden)) - - def test_shared_mlp(self): - scatter_fn, sonic_fn = self._get_both_fns() - out = torch.randn(4, 8) - block = SimpleNamespace(shared_mlp=lambda x: out) - hidden = torch.randn(4, 8) - - assert torch.equal(scatter_fn(block, hidden), sonic_fn(block, hidden)) - - def test_no_shared_expert(self): - scatter_fn, sonic_fn = self._get_both_fns() - block = SimpleNamespace() - hidden = torch.randn(4, 8) - - assert scatter_fn(block, hidden) is None - assert sonic_fn(block, hidden) is None - - def test_shared_expert_gate_only_in_scattermoe(self): - """ScatterMoE's _compute_shared_expert handles shared_expert_gate; - SonicMoE's patch.py handles it externally in the forward function. - - This documents the known divergence: the scattermoe version applies - sigmoid gating inline, while sonicmoe applies it in the forward. - """ - scatter_fn, sonic_fn = self._get_both_fns() - - H = 8 - expert_out = torch.ones(4, H) - gate_fn = lambda x: torch.zeros(4, H) # noqa: E731 # sigmoid(0) = 0.5 - - block = SimpleNamespace( - shared_expert=lambda x: expert_out, - shared_expert_gate=gate_fn, - ) - hidden = torch.randn(4, H) - - scatter_result = scatter_fn(block, hidden) - sonic_result = sonic_fn(block, hidden) - - # ScatterMoE applies the gate: expert_out * sigmoid(0) = 0.5 - expected_gated = expert_out * 0.5 - assert torch.allclose(scatter_result, expected_gated, atol=1e-6) - - # SonicMoE does NOT apply the gate here (it does it in the forward) - assert torch.equal(sonic_result, expert_out) - - -# ============================================================================ -# 4. Route dispatcher parity -# ============================================================================ - - -class TestRouteDispatcherParity: - """Verify _route in scattermoe dispatches correctly and matches individual fns.""" - - @pytest.fixture(autouse=True) - def _require(self): - _require_triton() - - def test_route_dispatches_softmax(self): - """_route should use softmax when no e_score_correction_bias.""" - from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( - _route, - _softmax_topk_route, - ) - - moe_block, gate, hidden, T, H, E, K = _make_softmax_block() - - route_w, route_e, route_k, route_E = _route( - moe_block, gate, hidden, gate.weight, None - ) - direct_w, direct_e, direct_k, direct_E = _softmax_topk_route( - moe_block, gate, hidden, gate.weight, None - ) - - assert torch.equal(route_w, direct_w) - assert torch.equal(route_e, direct_e) - assert route_k == direct_k - assert route_E == direct_E - - def test_route_dispatches_sigmoid(self): - """_route should use sigmoid when e_score_correction_bias is present.""" - from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( - _route, - _sigmoid_topk_route, - ) - - moe_block, gate, hidden, T, H, E, K = _make_sigmoid_block( - n_group=1, bias_on_gate=True - ) - - route_w, route_e, route_k, route_E = _route( - moe_block, gate, hidden, gate.weight, None - ) - direct_w, direct_e, direct_k, direct_E = _sigmoid_topk_route( - moe_block, gate, hidden, gate.weight, None - ) - - assert torch.equal(route_w, direct_w) - assert torch.equal(route_e, direct_e) - assert route_k == direct_k - assert route_E == direct_E diff --git a/tests/integrations/test_sonicmoe.py b/tests/integrations/test_sonicmoe.py index 864abca36d..f7261a85d9 100644 --- a/tests/integrations/test_sonicmoe.py +++ b/tests/integrations/test_sonicmoe.py @@ -1,4 +1,4 @@ -"""Unit tests for the SonicMoE integration.""" +"""Unit tests for the SonicMoE ExpertsInterface registration.""" from types import SimpleNamespace @@ -6,15 +6,6 @@ import torch from axolotl.integrations.kernels.args import KernelsArgs -from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - sigmoid_topk_routing, - softmax_topk_routing, -) -from axolotl.integrations.kernels.libs.sonicmoe.weight_converter import ( - ConcatenatedToInterleaved, - InterleavedToConcatenated, - register_sonicmoe_weight_converter, -) class TestKernelsArgs: @@ -43,777 +34,202 @@ def test_disables_mlp_kernel_when_sonicmoe(self): assert result["lora_mlp_kernel"] is False assert result["mlp_kernel"] is False + def test_experts_implementation_auto_sonicmoe(self): + out = KernelsArgs.check_experts_implementation({"use_sonicmoe": True}) + assert out["experts_implementation"] == "sonicmoe" -class TestConcatenatedToInterleaved: - @pytest.fixture - def sample_tensor(self): - """Create a test tensor [E=2, 2*I=4, H=3] with distinct gate/up values.""" - E, I, H = 2, 2, 3 # noqa: E741 - gate = torch.arange(1, E * I * H + 1, dtype=torch.float32).reshape(E, I, H) - up = torch.arange(100, 100 + E * I * H, dtype=torch.float32).reshape(E, I, H) - return torch.cat([gate, up], dim=1) - - def test_interleave_rows_alternate(self, sample_tensor): - op = ConcatenatedToInterleaved(dim=1) - result = op.convert( - {"test": sample_tensor}, - source_patterns=["test"], - target_patterns=["test"], - ) - interleaved = result["test"] - - # For expert 0: even rows should be gate, odd rows should be up - E, two_I, H = sample_tensor.shape - I = two_I // 2 # noqa: E741 - gate_orig = sample_tensor[:, :I, :] - up_orig = sample_tensor[:, I:, :] - - assert torch.equal(interleaved[:, 0::2, :], gate_orig) - assert torch.equal(interleaved[:, 1::2, :], up_orig) - - def test_interleave_handles_list_input(self, sample_tensor): - op = ConcatenatedToInterleaved(dim=1) - result = op.convert( - {"test": [sample_tensor]}, - source_patterns=["test"], - target_patterns=["test"], - ) - assert result["test"].shape == sample_tensor.shape - - def test_reverse_op_type(self): - op = ConcatenatedToInterleaved(dim=1) - assert isinstance(op.reverse_op, InterleavedToConcatenated) - assert op.reverse_op.dim == 1 - - -class TestInterleavedToConcatenated: - @pytest.fixture - def interleaved_tensor(self): - """Create an interleaved tensor [E=2, 2*I=4, H=3].""" - E, I, H = 2, 2, 3 # noqa: E741 - gate = torch.arange(1, E * I * H + 1, dtype=torch.float32).reshape(E, I, H) - up = torch.arange(100, 100 + E * I * H, dtype=torch.float32).reshape(E, I, H) - interleaved = torch.empty(E, 2 * I, H) - interleaved[:, 0::2, :] = gate - interleaved[:, 1::2, :] = up - return interleaved - - def test_deinterleave_gate_up_separated(self, interleaved_tensor): - op = InterleavedToConcatenated(dim=1) - result = op.convert( - {"test": interleaved_tensor}, - source_patterns=["test"], - target_patterns=["test"], - ) - concatenated = result["test"] - - E, two_I, H = concatenated.shape - I = two_I // 2 # noqa: E741 - - # First half should be gate (even rows from interleaved) - assert torch.equal(concatenated[:, :I, :], interleaved_tensor[:, 0::2, :]) - # Second half should be up (odd rows from interleaved) - assert torch.equal(concatenated[:, I:, :], interleaved_tensor[:, 1::2, :]) - - def test_reverse_op_type(self): - op = InterleavedToConcatenated(dim=1) - assert isinstance(op.reverse_op, ConcatenatedToInterleaved) - assert op.reverse_op.dim == 1 - - -class TestRoundTrip: - @pytest.fixture - def concat_tensor(self): - E, I, H = 4, 8, 16 # noqa: E741 - gate = torch.randn(E, I, H) - up = torch.randn(E, I, H) - return torch.cat([gate, up], dim=1) - - def test_interleave_then_deinterleave_is_identity(self, concat_tensor): - fwd = ConcatenatedToInterleaved(dim=1) - rev = InterleavedToConcatenated(dim=1) - - interleaved = fwd.convert( - {"k": concat_tensor}, source_patterns=["k"], target_patterns=["k"] - )["k"] - recovered = rev.convert( - {"k": interleaved}, source_patterns=["k"], target_patterns=["k"] - )["k"] - - assert torch.equal(concat_tensor, recovered) - - def test_reverse_op_chain_is_identity(self, concat_tensor): - """Verify that op.reverse_op produces an exact inverse.""" - op = ConcatenatedToInterleaved(dim=1) - rev = op.reverse_op - - interleaved = op.convert( - {"k": concat_tensor}, source_patterns=["k"], target_patterns=["k"] - )["k"] - recovered = rev.convert( - {"k": interleaved}, source_patterns=["k"], target_patterns=["k"] - )["k"] - - assert torch.equal(concat_tensor, recovered) - - def test_various_shapes(self): - """Test with different expert counts and dimensions.""" - fwd = ConcatenatedToInterleaved(dim=1) - rev = InterleavedToConcatenated(dim=1) - - for E, I, H in [(1, 4, 8), (8, 16, 32), (16, 128, 256)]: # noqa: E741 - concat = torch.randn(E, 2 * I, H) - interleaved = fwd.convert( - {"k": concat}, source_patterns=["k"], target_patterns=["k"] - )["k"] - recovered = rev.convert( - {"k": interleaved}, source_patterns=["k"], target_patterns=["k"] - )["k"] - assert torch.equal(concat, recovered), ( - f"Failed for shape ({E}, {2 * I}, {H})" - ) - - -class TestWeightConverterRegistration: - def test_register_appends_interleave_op(self): - from transformers.conversion_mapping import get_checkpoint_conversion_mapping - - register_sonicmoe_weight_converter("qwen3_moe") - - modified = get_checkpoint_conversion_mapping("qwen3_moe") - # Find the gate_up_proj converter - gate_up_converter = None - for conv in modified: - if hasattr(conv, "operations") and any( - "gate_up_proj" in pat for pat in conv.target_patterns - ): - gate_up_converter = conv - break - - assert gate_up_converter is not None - assert isinstance(gate_up_converter.operations[-1], ConcatenatedToInterleaved) - - def test_double_registration_is_idempotent(self): - from transformers.conversion_mapping import get_checkpoint_conversion_mapping - - register_sonicmoe_weight_converter("qwen3_moe") - register_sonicmoe_weight_converter("qwen3_moe") - - modified = get_checkpoint_conversion_mapping("qwen3_moe") - for conv in modified: - if hasattr(conv, "operations") and any( - "gate_up_proj" in pat for pat in conv.target_patterns - ): - interleave_count = sum( - isinstance(op, ConcatenatedToInterleaved) for op in conv.operations - ) - assert interleave_count == 1, ( - f"Expected 1 ConcatenatedToInterleaved op, got {interleave_count}" - ) - break - - def test_register_adds_same_key_converter(self): - from transformers.conversion_mapping import get_checkpoint_conversion_mapping - - register_sonicmoe_weight_converter("qwen3_moe") - - modified = get_checkpoint_conversion_mapping("qwen3_moe") - # Should have a same-key converter for already-fused checkpoints - same_key = [ - c - for c in modified - if hasattr(c, "source_patterns") - and c.source_patterns == ["mlp.experts.gate_up_proj"] - and c.target_patterns == ["mlp.experts.gate_up_proj"] - ] - assert len(same_key) == 1 - assert isinstance(same_key[0].operations[0], ConcatenatedToInterleaved) - - def test_register_creates_mapping_when_none(self): - from transformers.conversion_mapping import get_checkpoint_conversion_mapping - - # qwen3_5_moe has no conversion mapping in transformers - register_sonicmoe_weight_converter("qwen3_5_moe") - - mapping = get_checkpoint_conversion_mapping("qwen3_5_moe") - assert mapping is not None - same_key = [ - c - for c in mapping - if hasattr(c, "source_patterns") - and c.source_patterns == ["mlp.experts.gate_up_proj"] - and c.target_patterns == ["mlp.experts.gate_up_proj"] - ] - assert len(same_key) == 1 - assert isinstance(same_key[0].operations[0], ConcatenatedToInterleaved) - - -def _make_qwen_moe_block(T=8, H=16, E=4, K=2): - """Create a mock qwen-style MoE block for routing tests.""" - gate = SimpleNamespace( - weight=torch.randn(E, H), - top_k=K, - num_experts=E, - norm_topk_prob=True, - ) - return SimpleNamespace(gate=gate), T, H, E, K - - -def _make_glm_moe_block(T=8, H=16, E=16, K=4, n_group=2, topk_group=1): - """Create a mock GLM5-style MoE block for routing tests.""" - gate = SimpleNamespace( - weight=torch.randn(E, H), - e_score_correction_bias=torch.zeros(E), - ) - moe_block = SimpleNamespace( - gate=gate, - top_k=K, - n_routed_experts=E, - n_group=n_group, - topk_group=topk_group, - norm_topk_prob=True, - routed_scaling_factor=1.0, - ) - return moe_block, T, H, E, K - - -def _make_minimax_m2_moe_block(T=8, H=16, E=16, K=4): - """Create a mock minimax_m2-style MoE block for routing tests. - - minimax_m2 uses sigmoid->topk WITHOUT group selection: - - e_score_correction_bias is on the moe_block (not on gate) - - No n_group / topk_group attributes - - Always normalizes (norm_topk_prob defaults to True) - - No routed_scaling_factor (defaults to 1.0) - """ - gate = SimpleNamespace( - weight=torch.randn(E, H), - top_k=K, - ) - moe_block = SimpleNamespace( - gate=gate, - top_k=K, - e_score_correction_bias=torch.zeros(E), - ) - return moe_block, T, H, E, K - - -class TestSoftmaxTopkRouting: - def test_output_shapes(self): - moe_block, T, H, E, K = _make_qwen_moe_block() - hidden = torch.randn(T, H) - - scores, token_idx, expert_idx, logits = softmax_topk_routing(hidden, moe_block) - - assert scores.shape == (T * K,) - assert token_idx.shape == (T * K,) - assert expert_idx.shape == (T * K,) - assert logits.shape == (T, E) - - def test_scores_are_float32(self): - moe_block, T, H, E, K = _make_qwen_moe_block() - hidden = torch.randn(T, H) - - scores, _, _, _ = softmax_topk_routing(hidden, moe_block) - assert scores.dtype == torch.float32 - - def test_token_indices_sorted_ascending(self): - moe_block, T, H, E, K = _make_qwen_moe_block() - hidden = torch.randn(T, H) - - _, token_idx, _, _ = softmax_topk_routing(hidden, moe_block) - - # Token indices must be sorted ascending (SonicMoE requirement) - diffs = token_idx[1:] - token_idx[:-1] - assert (diffs >= 0).all() - - def test_expert_indices_in_range(self): - moe_block, T, H, E, K = _make_qwen_moe_block() - hidden = torch.randn(T, H) - - _, _, expert_idx, _ = softmax_topk_routing(hidden, moe_block) - - assert (expert_idx >= 0).all() - assert (expert_idx < E).all() - - def test_renormalized_scores_sum_to_one(self): - moe_block, T, H, E, K = _make_qwen_moe_block() - hidden = torch.randn(T, H) - - scores, _, _, _ = softmax_topk_routing(hidden, moe_block) - per_token_sums = scores.reshape(T, K).sum(dim=-1) - assert torch.allclose(per_token_sums, torch.ones(T), atol=1e-5) - - -class TestSigmoidTopkRouting: - def test_output_shapes(self): - moe_block, T, H, E, K = _make_glm_moe_block() - hidden = torch.randn(T, H) - - scores, token_idx, expert_idx, logits = sigmoid_topk_routing(hidden, moe_block) - - assert scores.shape == (T * K,) - assert token_idx.shape == (T * K,) - assert expert_idx.shape == (T * K,) - assert logits.shape == (T, E) - - def test_scores_are_float32(self): - moe_block, T, H, E, K = _make_glm_moe_block() - hidden = torch.randn(T, H) - - scores, _, _, _ = sigmoid_topk_routing(hidden, moe_block) - assert scores.dtype == torch.float32 - - def test_token_indices_sorted_ascending(self): - moe_block, T, H, E, K = _make_glm_moe_block() - hidden = torch.randn(T, H) - - _, token_idx, _, _ = sigmoid_topk_routing(hidden, moe_block) - - diffs = token_idx[1:] - token_idx[:-1] - assert (diffs >= 0).all() + def test_experts_implementation_auto_scattermoe(self): + out = KernelsArgs.check_experts_implementation({"use_scattermoe": True}) + assert out["experts_implementation"] == "scattermoe" - def test_expert_indices_in_range(self): - moe_block, T, H, E, K = _make_glm_moe_block() - hidden = torch.randn(T, H) + def test_experts_implementation_default_eager(self): + out = KernelsArgs.check_experts_implementation({}) + assert out["experts_implementation"] == "eager" - _, _, expert_idx, _ = sigmoid_topk_routing(hidden, moe_block) - - assert (expert_idx >= 0).all() - assert (expert_idx < E).all() - - def test_scores_are_nonnegative(self): - """Sigmoid outputs are in [0, 1], so scores should be non-negative.""" - moe_block, T, H, E, K = _make_glm_moe_block() - hidden = torch.randn(T, H) - - scores, _, _, _ = sigmoid_topk_routing(hidden, moe_block) - assert (scores >= 0).all() - - def test_scaling_factor_applied(self): - moe_block, T, H, E, K = _make_glm_moe_block() - hidden = torch.randn(T, H) - - # Get scores with scaling_factor=1.0 - scores_1x, _, _, _ = sigmoid_topk_routing(hidden, moe_block) - - # Get scores with scaling_factor=2.0 - moe_block.routed_scaling_factor = 2.0 - scores_2x, _, _, _ = sigmoid_topk_routing(hidden, moe_block) - - assert torch.allclose(scores_2x, scores_1x * 2.0, atol=1e-5) - - def test_group_selection_restricts_experts(self): - """With n_group=4 and topk_group=1, only 1/4 of experts should be selectable.""" - moe_block, T, H, E, K = _make_glm_moe_block(E=16, K=2, n_group=4, topk_group=1) - hidden = torch.randn(T, H) - - _, _, expert_idx, _ = sigmoid_topk_routing(hidden, moe_block) - - # Each token's experts should all fall within a single group (size E//n_group=4) - expert_idx_2d = expert_idx.reshape(T, K) - for t in range(T): - experts = expert_idx_2d[t] - groups = experts // (E // moe_block.n_group) - # All selected experts should be from the same group - assert (groups == groups[0]).all() - - -class TestMiniMaxM2SigmoidRouting: - """Tests for minimax_m2 routing: sigmoid->topk without group selection.""" - - def test_output_shapes(self): - """Validates getattr defaults work: n_group=1, E from gate.weight.shape[0].""" - moe_block, T, H, E, K = _make_minimax_m2_moe_block() - hidden = torch.randn(T, H) - - scores, token_idx, expert_idx, logits = sigmoid_topk_routing(hidden, moe_block) - - assert scores.shape == (T * K,) - assert token_idx.shape == (T * K,) - assert expert_idx.shape == (T * K,) - assert logits.shape == (T, E) - - def test_bias_on_block_not_gate(self): - """Verify that e_score_correction_bias on the block (not gate) is used.""" - T, H, E, K = 8, 16, 8, 2 - gate = SimpleNamespace( - weight=torch.randn(E, H), - top_k=K, - ) - # Large positive bias on expert 0 should make it selected more often - bias = torch.zeros(E) - bias[0] = 100.0 - moe_block = SimpleNamespace( - gate=gate, - top_k=K, - e_score_correction_bias=bias, + def test_sonicmoe_impl_requires_flag(self): + out = KernelsArgs.check_experts_implementation( + {"experts_implementation": "sonicmoe"} ) - hidden = torch.randn(T, H) - - _, _, expert_idx, _ = sigmoid_topk_routing(hidden, moe_block) - - # Expert 0 should appear for every token due to the large bias - expert_idx_2d = expert_idx.reshape(T, K) - for t in range(T): - assert 0 in expert_idx_2d[t] - - -# ============================================================================ -# Ernie 4.5 MoE: softmax -> bias correction -> topk -# ============================================================================ - - -def _make_ernie_moe_block(T=8, H=16, E=8, K=2, norm_min=1e-20): - """Create a mock Ernie 4.5 MoE block for routing tests. - - Ernie 4.5 uses a gate.moe_statics module that adds bias to softmax probs - before topk selection, then gathers from original probs. - """ - bias = torch.zeros(E) - - class MockMoeStatics: - def __init__(self, bias_tensor): - self.e_score_correction_bias = bias_tensor - - def __call__(self, probs): - return probs + self.e_score_correction_bias - - gate = SimpleNamespace( - weight=torch.randn(E, H), - top_k=K, - moe_statics=MockMoeStatics(bias), - norm_min=norm_min, - ) - moe_block = SimpleNamespace(gate=gate) - return moe_block, bias, T, H, E, K - + assert out["experts_implementation"] == "eager" -class TestSoftmaxBiasTopkRouting: - """Tests for Ernie 4.5 MoE routing (softmax_bias_topk_routing).""" - - def test_output_shapes(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_bias_topk_routing, + def test_scattermoe_impl_requires_flag(self): + out = KernelsArgs.check_experts_implementation( + {"experts_implementation": "scattermoe"} ) + assert out["experts_implementation"] == "eager" - moe_block, _, T, H, E, K = _make_ernie_moe_block() - hidden = torch.randn(T, H) - - scores, token_idx, expert_idx, logits = softmax_bias_topk_routing( - hidden, moe_block + def test_unknown_impl_falls_back_to_eager(self): + out = KernelsArgs.check_experts_implementation( + {"experts_implementation": "not-a-real-impl"} ) + assert out["experts_implementation"] == "eager" - assert scores.shape == (T * K,) - assert token_idx.shape == (T * K,) - assert expert_idx.shape == (T * K,) - assert logits.shape == (T, E) + def test_builtin_impls_pass_through(self): + for impl in ("eager", "batched_mm", "grouped_mm"): + out = KernelsArgs.check_experts_implementation( + {"experts_implementation": impl} + ) + assert out["experts_implementation"] == impl - def test_scores_are_float32(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_bias_topk_routing, - ) - moe_block, _, T, H, E, K = _make_ernie_moe_block() - hidden = torch.randn(T, H) +class TestSonicMoERegistration: + """Test that register_sonicmoe_experts plugs into ALL_EXPERTS_FUNCTIONS.""" - scores, _, _, _ = softmax_bias_topk_routing(hidden, moe_block) - assert scores.dtype == torch.float32 + def test_register_adds_entry(self): + from transformers.integrations.moe import ALL_EXPERTS_FUNCTIONS - def test_token_indices_sorted_ascending(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_bias_topk_routing, + from axolotl.integrations.kernels.libs.sonicmoe.experts import ( + register_sonicmoe_experts, + sonicmoe_experts_forward_with_lora, ) - moe_block, _, T, H, E, K = _make_ernie_moe_block() - hidden = torch.randn(T, H) + register_sonicmoe_experts() + assert "sonicmoe" in ALL_EXPERTS_FUNCTIONS + assert ALL_EXPERTS_FUNCTIONS["sonicmoe"] is sonicmoe_experts_forward_with_lora - _, token_idx, _, _ = softmax_bias_topk_routing(hidden, moe_block) - diffs = token_idx[1:] - token_idx[:-1] - assert (diffs >= 0).all() + def test_register_is_idempotent(self): + from transformers.integrations.moe import ALL_EXPERTS_FUNCTIONS - def test_expert_indices_in_range(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_bias_topk_routing, + from axolotl.integrations.kernels.libs.sonicmoe.experts import ( + register_sonicmoe_experts, ) - moe_block, _, T, H, E, K = _make_ernie_moe_block() - hidden = torch.randn(T, H) + register_sonicmoe_experts() + register_sonicmoe_experts() + # Just one entry, no error + assert "sonicmoe" in ALL_EXPERTS_FUNCTIONS - _, _, expert_idx, _ = softmax_bias_topk_routing(hidden, moe_block) - assert (expert_idx >= 0).all() - assert (expert_idx < E).all() + def test_register_overrides_upstream(self): + """Axolotl's LoRA-aware variant replaces upstream's plain forward.""" + from transformers.integrations.moe import ALL_EXPERTS_FUNCTIONS + from transformers.integrations.sonicmoe import sonicmoe_experts_forward - def test_renormalized_scores_sum_to_one(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_bias_topk_routing, + from axolotl.integrations.kernels.libs.sonicmoe.experts import ( + register_sonicmoe_experts, + sonicmoe_experts_forward_with_lora, ) - moe_block, _, T, H, E, K = _make_ernie_moe_block() - hidden = torch.randn(T, H) + register_sonicmoe_experts() + assert ALL_EXPERTS_FUNCTIONS["sonicmoe"] is sonicmoe_experts_forward_with_lora + assert ALL_EXPERTS_FUNCTIONS["sonicmoe"] is not sonicmoe_experts_forward - scores, _, _, _ = softmax_bias_topk_routing(hidden, moe_block) - per_token_sums = scores.reshape(T, K).sum(dim=-1) - assert torch.allclose(per_token_sums, torch.ones(T), atol=1e-5) - def test_bias_affects_expert_selection(self): - """Large positive bias on expert 0 should make it always selected.""" - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_bias_topk_routing, - ) +class TestMoELoRAMaterialize: + """Verify the LoRA materialization autograd Function used by the registered forward.""" - moe_block, bias, T, H, E, K = _make_ernie_moe_block() - bias[0] = 100.0 # mutate the bias to strongly favor expert 0 - hidden = torch.randn(T, H) + def test_forward_shape_and_identity_with_zero_lora(self): + """W_eff == base when LoRA tensors are zero, regardless of layout convention.""" + from axolotl.integrations.kernels.libs.sonicmoe.lora import MoELoRAMaterialize - _, _, expert_idx, _ = softmax_bias_topk_routing(hidden, moe_block) - expert_idx_2d = expert_idx.reshape(T, K) - for t in range(T): - assert 0 in expert_idx_2d[t] + E, dim1, dim2, r = 4, 8, 6, 2 + base = torch.randn(E, dim1, dim2) + lora_A = torch.zeros(r * E, dim2) + lora_B = torch.zeros(dim1, r * E) + scaling = 0.5 + W_eff = MoELoRAMaterialize.apply(base, lora_A, lora_B, scaling) + assert W_eff.shape == base.shape + torch.testing.assert_close(W_eff, base, atol=1e-6, rtol=1e-6) -# ============================================================================ -# DeepSeek V2: softmax -> group_limited_greedy / greedy -> topk -# ============================================================================ + def test_forward_scaling_linearity(self): + """Doubling scaling should double the LoRA delta.""" + from axolotl.integrations.kernels.libs.sonicmoe.lora import MoELoRAMaterialize + E, dim1, dim2, r = 4, 8, 6, 2 + base = torch.randn(E, dim1, dim2) + lora_A = torch.randn(r * E, dim2) + lora_B = torch.randn(dim1, r * E) -def _make_deepseek_v2_moe_block( - T=8, H=16, E=16, K=4, num_group=2, topk_group=1, topk_method="group_limited_greedy" -): - """Create a mock DeepSeek V2 MoE block for routing tests. + W_1 = MoELoRAMaterialize.apply(base, lora_A, lora_B, 1.0) + W_2 = MoELoRAMaterialize.apply(base, lora_A, lora_B, 2.0) + torch.testing.assert_close(W_2 - base, 2 * (W_1 - base), atol=1e-5, rtol=1e-5) - DeepSeek V2 uses num_group (not n_group), gate is nn.Linear, - and supports greedy / group_limited_greedy topk methods. - """ - gate = SimpleNamespace(weight=torch.randn(E, H)) - moe_block = SimpleNamespace( - gate=gate, - top_k=K, - num_group=num_group, - topk_group=topk_group, - topk_method=topk_method, - routed_scaling_factor=1.0, - ) - return moe_block, T, H, E, K - - -class TestSoftmaxGroupLimitedTopkRouting: - """Tests for DeepSeek V2 routing (softmax_group_limited_topk_routing).""" - - def test_output_shapes_group_limited(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_group_limited_topk_routing, - ) + def test_forward_matches_peft_einsum(self): + """Delta matches PEFT's ParamWrapper.get_delta_weight einsum convention. - moe_block, T, H, E, K = _make_deepseek_v2_moe_block( - topk_method="group_limited_greedy" - ) - hidden = torch.randn(T, H) + Reference: ``peft.tuners.lora.layer.ParamWrapper.get_delta_weight`` + on PEFT 0.19.x — ``einsum("o r e, e r i -> e o i", B_3d, A_3d)`` where + ``B_3d = lora_B.reshape(dim1, r, E)`` and ``A_3d = lora_A.reshape(E, r, dim2)``. + """ + from axolotl.integrations.kernels.libs.sonicmoe.lora import MoELoRAMaterialize - scores, token_idx, expert_idx, logits = softmax_group_limited_topk_routing( - hidden, moe_block - ) + E, dim1, dim2, r = 3, 5, 4, 2 + base = torch.zeros(E, dim1, dim2) + lora_A = torch.randn(r * E, dim2) + lora_B = torch.randn(dim1, r * E) + scaling = 0.7 - assert scores.shape == (T * K,) - assert token_idx.shape == (T * K,) - assert expert_idx.shape == (T * K,) - assert logits.shape == (T, E) + W_eff = MoELoRAMaterialize.apply(base, lora_A, lora_B, scaling) - def test_output_shapes_greedy(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_group_limited_topk_routing, - ) + # PEFT's reference computation + A_3d = lora_A.reshape(E, r, dim2) + B_3d = lora_B.reshape(dim1, r, E) + peft_delta = torch.einsum("o r e, e r i -> e o i", B_3d, A_3d) * scaling - moe_block, T, H, E, K = _make_deepseek_v2_moe_block(topk_method="greedy") - hidden = torch.randn(T, H) + torch.testing.assert_close(W_eff, peft_delta, atol=1e-5, rtol=1e-5) - scores, token_idx, expert_idx, logits = softmax_group_limited_topk_routing( - hidden, moe_block - ) + def test_gradient_flows_to_lora(self): + from axolotl.integrations.kernels.libs.sonicmoe.lora import MoELoRAMaterialize - assert scores.shape == (T * K,) - assert logits.shape == (T, E) + E, dim1, dim2, r = 4, 8, 6, 2 + base = torch.randn(E, dim1, dim2, requires_grad=False) + lora_A = torch.randn(r * E, dim2, requires_grad=True) + lora_B = torch.randn(dim1, r * E, requires_grad=True) + scaling = 0.5 - def test_scores_are_float32(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_group_limited_topk_routing, - ) + W_eff = MoELoRAMaterialize.apply(base, lora_A, lora_B, scaling) + loss = W_eff.sum() + loss.backward() - moe_block, T, H, E, K = _make_deepseek_v2_moe_block() - hidden = torch.randn(T, H) - - scores, _, _, _ = softmax_group_limited_topk_routing(hidden, moe_block) - assert scores.dtype == torch.float32 - - def test_token_indices_sorted_ascending(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_group_limited_topk_routing, - ) - - moe_block, T, H, E, K = _make_deepseek_v2_moe_block() - hidden = torch.randn(T, H) - - _, token_idx, _, _ = softmax_group_limited_topk_routing(hidden, moe_block) - diffs = token_idx[1:] - token_idx[:-1] - assert (diffs >= 0).all() - - def test_expert_indices_in_range(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_group_limited_topk_routing, - ) - - moe_block, T, H, E, K = _make_deepseek_v2_moe_block() - hidden = torch.randn(T, H) - - _, _, expert_idx, _ = softmax_group_limited_topk_routing(hidden, moe_block) - assert (expert_idx >= 0).all() - assert (expert_idx < E).all() - - def test_scaling_factor_applied(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_group_limited_topk_routing, - ) - - moe_block, T, H, E, K = _make_deepseek_v2_moe_block(topk_method="greedy") - hidden = torch.randn(T, H) - - scores_1x, _, _, _ = softmax_group_limited_topk_routing(hidden, moe_block) - - moe_block.routed_scaling_factor = 2.5 - scores_2x, _, _, _ = softmax_group_limited_topk_routing(hidden, moe_block) - - assert torch.allclose(scores_2x, scores_1x * 2.5, atol=1e-5) - - def test_group_selection_restricts_experts(self): - """With num_group=4 and topk_group=1, experts should come from selected groups.""" - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_group_limited_topk_routing, - ) + assert lora_A.grad is not None + assert lora_B.grad is not None + assert lora_A.grad.abs().max() > 0 + assert lora_B.grad.abs().max() > 0 + # Base weight is frozen — no grad expected. + assert base.grad is None - moe_block, T, H, E, K = _make_deepseek_v2_moe_block( - E=16, K=2, num_group=4, topk_group=1, topk_method="group_limited_greedy" + def test_no_lora_returns_base_unchanged(self): + from axolotl.integrations.kernels.libs.sonicmoe.lora import ( + materialize_expert_lora, ) - hidden = torch.randn(T, H) - - _, _, expert_idx, _ = softmax_group_limited_topk_routing(hidden, moe_block) - expert_idx_2d = expert_idx.reshape(T, K) - group_size = E // moe_block.num_group - for t in range(T): - experts = expert_idx_2d[t] - groups = experts // group_size - # All selected experts should be from the same group - assert (groups == groups[0]).all() - - def test_unsupported_topk_method_raises(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_group_limited_topk_routing, - ) - - moe_block, T, H, E, K = _make_deepseek_v2_moe_block(topk_method="invalid") - hidden = torch.randn(T, H) - - with pytest.raises(ValueError, match="unsupported topk_method"): - softmax_group_limited_topk_routing(hidden, moe_block) - -# ============================================================================ -# HunYuan V1 MoE: softmax -> topk -> renorm (via gate.wg) -# ============================================================================ + base = torch.randn(4, 8, 6) + result = materialize_expert_lora(base, None) + assert result is base -def _make_hunyuan_moe_block(T=8, H=16, E=8, K=2): - """Create a mock HunYuan V1 MoE block for routing tests. - - HunYuan V1 uses gate.wg (nn.Linear-like) instead of gate.weight, - and top_k on the moe_block instead of the gate. +class TestExpertsClassMetadata: + """The forward reads `has_gate`/`has_bias`/`is_transposed`/`is_concatenated` + that are set by transformers' @use_experts_implementation decorator. + Verify our forward respects these without an actual CUDA kernel call. """ - wg = SimpleNamespace(weight=torch.randn(E, H)) - gate = SimpleNamespace(wg=wg) - moe_block = SimpleNamespace(gate=gate, top_k=K) - return moe_block, T, H, E, K - -class TestSoftmaxTopkWgRouting: - """Tests for HunYuan V1 MoE routing (softmax_topk_wg_routing).""" - - def test_output_shapes(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_topk_wg_routing, + def test_rejects_non_gated(self): + from axolotl.integrations.kernels.libs.sonicmoe.experts import ( + sonicmoe_experts_forward_with_lora, ) - moe_block, T, H, E, K = _make_hunyuan_moe_block() - hidden = torch.randn(T, H) + fake_self = SimpleNamespace(has_gate=False) + hidden = torch.zeros(2, 4) + top_k_index = torch.zeros(2, 1, dtype=torch.long) + top_k_weights = torch.ones(2, 1) - scores, token_idx, expert_idx, logits = softmax_topk_wg_routing( - hidden, moe_block - ) - - assert scores.shape == (T * K,) - assert token_idx.shape == (T * K,) - assert expert_idx.shape == (T * K,) - assert logits.shape == (T, E) - - def test_scores_are_float32(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_topk_wg_routing, - ) - - moe_block, T, H, E, K = _make_hunyuan_moe_block() - hidden = torch.randn(T, H) - - scores, _, _, _ = softmax_topk_wg_routing(hidden, moe_block) - assert scores.dtype == torch.float32 - - def test_token_indices_sorted_ascending(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_topk_wg_routing, - ) - - moe_block, T, H, E, K = _make_hunyuan_moe_block() - hidden = torch.randn(T, H) - - _, token_idx, _, _ = softmax_topk_wg_routing(hidden, moe_block) - diffs = token_idx[1:] - token_idx[:-1] - assert (diffs >= 0).all() - - def test_expert_indices_in_range(self): - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_topk_wg_routing, - ) - - moe_block, T, H, E, K = _make_hunyuan_moe_block() - hidden = torch.randn(T, H) - - _, _, expert_idx, _ = softmax_topk_wg_routing(hidden, moe_block) - assert (expert_idx >= 0).all() - assert (expert_idx < E).all() - - def test_renormalized_scores_sum_to_one(self): - """HunYuan V1 always renormalizes (no norm_topk_prob flag).""" - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_topk_wg_routing, - ) - - moe_block, T, H, E, K = _make_hunyuan_moe_block() - hidden = torch.randn(T, H) - - scores, _, _, _ = softmax_topk_wg_routing(hidden, moe_block) - per_token_sums = scores.reshape(T, K).sum(dim=-1) - assert torch.allclose(per_token_sums, torch.ones(T), atol=1e-5) + with pytest.raises(ValueError, match="has_gate"): + sonicmoe_experts_forward_with_lora( + fake_self, hidden, top_k_index, top_k_weights + ) - def test_uses_gate_wg_weight(self): - """Verify that modifying gate.wg.weight changes the routing output.""" - from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - softmax_topk_wg_routing, + def test_rejects_non_cuda(self): + from axolotl.integrations.kernels.libs.sonicmoe.experts import ( + sonicmoe_experts_forward_with_lora, ) - moe_block, T, H, E, K = _make_hunyuan_moe_block() - hidden = torch.randn(T, H) + fake_self = SimpleNamespace(has_gate=True) + hidden = torch.zeros(2, 4) # CPU tensor + top_k_index = torch.zeros(2, 1, dtype=torch.long) + top_k_weights = torch.ones(2, 1) - scores1, _, _, _ = softmax_topk_wg_routing(hidden, moe_block) - - # Change the wg weight and verify scores change - moe_block.gate.wg.weight = torch.randn(E, H) - scores2, _, _, _ = softmax_topk_wg_routing(hidden, moe_block) - - assert not torch.equal(scores1, scores2) + with pytest.raises(ValueError, match="CUDA"): + sonicmoe_experts_forward_with_lora( + fake_self, hidden, top_k_index, top_k_weights + ) diff --git a/tests/integrations/test_sonicmoe_gradients.py b/tests/integrations/test_sonicmoe_gradients.py deleted file mode 100644 index cb5ef7663d..0000000000 --- a/tests/integrations/test_sonicmoe_gradients.py +++ /dev/null @@ -1,158 +0,0 @@ -""" -Gradient correctness tests for SonicMoE routing functions (CPU-only). - -Uses torch.autograd.gradcheck with float32 inputs to match the production -code path where routing happens in float32. -""" - -import torch - -from axolotl.integrations.kernels.libs.sonicmoe.routing import ( - sigmoid_topk_routing, - softmax_topk_routing, -) - -_GC_EPS = 1e-3 -_GC_ATOL = 1e-3 -_GC_RTOL = 1e-3 - - -def _make_softmax_moe_block(weight): - gate = torch.nn.Module() - gate.weight = weight - gate.top_k = 2 - gate.norm_topk_prob = True - - moe_block = torch.nn.Module() - moe_block.gate = gate - return moe_block - - -def _make_sigmoid_moe_block(weight, bias): - gate = torch.nn.Module() - gate.weight = weight - gate.e_score_correction_bias = bias - - moe_block = torch.nn.Module() - moe_block.gate = gate - moe_block.top_k = 2 - moe_block.n_routed_experts = weight.shape[0] - moe_block.n_group = 1 - moe_block.norm_topk_prob = True - moe_block.routed_scaling_factor = 1.0 - return moe_block - - -class TestSoftmaxTopkRoutingGradcheck: - """Numerical gradient verification for softmax_topk_routing.""" - - def test_gradcheck_wrt_gate_weight(self): - T, H, E = 4, 8, 4 - - hidden = torch.randn(T, H, dtype=torch.float32) - - def fn(weight): - moe_block = _make_softmax_moe_block(weight) - scores, _, _, _ = softmax_topk_routing(hidden, moe_block) - return scores - - weight = torch.randn(E, H, dtype=torch.float32, requires_grad=True) - torch.autograd.gradcheck( - fn, (weight,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL - ) - - def test_gradcheck_wrt_hidden_states(self): - T, H, E = 4, 8, 4 - - weight = torch.randn(E, H, dtype=torch.float32) - moe_block = _make_softmax_moe_block(weight) - - def fn(hidden): - scores, _, _, _ = softmax_topk_routing(hidden, moe_block) - return scores - - hidden = torch.randn(T, H, dtype=torch.float32, requires_grad=True) - torch.autograd.gradcheck( - fn, (hidden,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL - ) - - def test_gradcheck_wrt_router_logits(self): - T, H, E = 4, 8, 4 - - hidden = torch.randn(T, H, dtype=torch.float32) - - def fn(weight): - moe_block = _make_softmax_moe_block(weight) - _, _, _, router_logits = softmax_topk_routing(hidden, moe_block) - return router_logits - - weight = torch.randn(E, H, dtype=torch.float32, requires_grad=True) - torch.autograd.gradcheck( - fn, (weight,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL - ) - - def test_no_norm_variant(self): - T, H, E = 4, 8, 4 - - hidden = torch.randn(T, H, dtype=torch.float32) - - def fn(weight): - moe_block = _make_softmax_moe_block(weight) - moe_block.gate.norm_topk_prob = False - scores, _, _, _ = softmax_topk_routing(hidden, moe_block) - return scores - - weight = torch.randn(E, H, dtype=torch.float32, requires_grad=True) - torch.autograd.gradcheck( - fn, (weight,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL - ) - - -class TestSigmoidTopkRoutingGradcheck: - """Numerical gradient verification for sigmoid_topk_routing.""" - - def test_gradcheck_wrt_gate_weight(self): - T, H, E = 4, 8, 4 - - hidden = torch.randn(T, H, dtype=torch.float32) - bias = torch.zeros(E, dtype=torch.float32) - - def fn(weight): - moe_block = _make_sigmoid_moe_block(weight, bias) - scores, _, _, _ = sigmoid_topk_routing(hidden, moe_block) - return scores - - weight = torch.randn(E, H, dtype=torch.float32, requires_grad=True) - torch.autograd.gradcheck( - fn, (weight,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL - ) - - def test_gradcheck_wrt_hidden_states(self): - T, H, E = 4, 8, 4 - - weight = torch.randn(E, H, dtype=torch.float32) - bias = torch.zeros(E, dtype=torch.float32) - moe_block = _make_sigmoid_moe_block(weight, bias) - - def fn(hidden): - scores, _, _, _ = sigmoid_topk_routing(hidden, moe_block) - return scores - - hidden = torch.randn(T, H, dtype=torch.float32, requires_grad=True) - torch.autograd.gradcheck( - fn, (hidden,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL - ) - - def test_gradcheck_wrt_bias(self): - T, H, E = 4, 8, 4 - - hidden = torch.randn(T, H, dtype=torch.float32) - weight = torch.randn(E, H, dtype=torch.float32) - - def fn(bias): - moe_block = _make_sigmoid_moe_block(weight, bias) - scores, _, _, _ = sigmoid_topk_routing(hidden, moe_block) - return scores - - bias = torch.zeros(E, dtype=torch.float32, requires_grad=True) - torch.autograd.gradcheck(fn, (bias,), eps=_GC_EPS, atol=_GC_ATOL, rtol=_GC_RTOL) diff --git a/tests/integrations/test_sonicmoe_lora.py b/tests/integrations/test_sonicmoe_lora.py index 4b25843fee..7b1cf1765d 100644 --- a/tests/integrations/test_sonicmoe_lora.py +++ b/tests/integrations/test_sonicmoe_lora.py @@ -15,7 +15,6 @@ has_lora, materialize_expert_lora, unwrap_experts_lora, - unwrap_gate_lora, ) # ============================================================================= @@ -44,21 +43,6 @@ def _make_mock_lora_module(weight_A, weight_B, scaling_val, param_name=None): return mock -def _make_peft_gate(hidden_size, num_experts, rank, scaling=0.5): - """Create a mock PEFT-wrapped gate module.""" - base_gate = MagicMock() - base_gate.weight = torch.randn(num_experts, hidden_size) - base_gate.top_k = 2 - base_gate.norm_topk_prob = True - - lora_A = torch.randn(rank, hidden_size) - lora_B = torch.randn(num_experts, rank) - - wrapper = _make_mock_lora_module(lora_A, lora_B, scaling) - wrapper.base_layer = base_gate - return wrapper, base_gate - - def _make_peft_experts( num_experts, gate_up_dim, down_dim, hidden_size, rank, scaling=0.5 ): @@ -134,39 +118,6 @@ def test_no_active_adapters(self): assert get_lora_params_from_wrapper(wrapper) == (None, None, None) -# ============================================================================= -# Tests: unwrap_gate_lora -# ============================================================================= - - -class TestUnwrapGateLora: - def test_plain_gate(self): - gate = MagicMock(spec=["weight", "top_k"]) - del gate.base_layer - del gate.lora_A - gate.weight = torch.randn(8, 64) - base, weight, delta = unwrap_gate_lora(gate) - assert base is gate - assert torch.equal(weight, gate.weight) - assert delta is None - - def test_wrapped_gate(self): - wrapper, base_gate = _make_peft_gate( - hidden_size=64, num_experts=8, rank=4, scaling=0.5 - ) - base, weight, delta = unwrap_gate_lora(wrapper) - assert base is base_gate - assert torch.equal(weight, base_gate.weight) - assert delta is not None - assert delta.shape == base_gate.weight.shape - - # Verify delta = scaling * B @ A - lora_A = wrapper.lora_A["default"].weight - lora_B = wrapper.lora_B["default"].weight - expected = 0.5 * (lora_B @ lora_A) - assert torch.allclose(delta, expected) - - # ============================================================================= # Tests: unwrap_experts_lora # ============================================================================= From 5c1a2669230f28dbbda28b20c1cb012a01aaa3cb Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 28 May 2026 10:32:10 -0400 Subject: [PATCH 1326/1405] Fused ScatterMoE-LoRA for MXFP4 weights (#3663) [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(scattermoe-lora): selective dequant for mxfp4 expert weights Add an MXFP4 branch to `selective_expert_weights()` that detects a torchao `MXTensor` parameter (elem_dtype=float4_e2m1fn_x2) and dequantizes only the active experts via index-then-construct of a compact sub-MXTensor. The K-axis OCP block layout (last storage dim) matches `experts.gate_up_proj` natural shape `[E, N, K]`, so the caller's existing `.transpose(2, 1)` post-step keeps producing the kernel's `[E, K, N]` weight tile unchanged. `HFScatterMoEGatedMLP.forward` now also routes through the selective path whenever the experts hold MXFP4 weights — full-tensor MX dequant of 256-expert models is prohibitive and the kernel needs bf16 input. Tests (CUDA-only) compare against a bf16 baseline produced by the same MXTensor's full dequant; outputs are bitwise identical for both forward and backward (dX, dA, dB) across small [E=8,K=128,N=256] and representative [E=32,K=2048,N=1024] shapes, and across all four combinations of \`use_fused_dX\` / \`use_fused_gather\`. Signed-off-by: Wing Lian * feat(scattermoe-lora): fused mxfp4 dequant in triton kernel Add MX-aware forward and dX kernels that consume an ``MXWeights`` container (packed uint8 + E8M0 scales) directly, so the base-weight tile is dequantized inside the K-loop instead of through a materialized bf16 buffer. The K-loop loads two FP4 values per uint8 byte, looks them up in a 16-entry codebook tensor (``±{0, 0.5, 1, 1.5, 2, 3, 4, 6}``), multiplies by ``2^(scale_byte - 127)``, and casts to bf16 for the matmul. ``BLOCK_K`` is constrained to a multiple of the OCP block size (32) so each tile aligns with whole scale blocks; an MX-aware autotune pruner accounts for the extra packed/scale SMEM. The dX kernel reuses the *forward* MX layout (block axis = K, the dX output axis) — for each (K_tile, N_tile) sub-tile, nibbles decode along the K rows (the byte is shared by two adjacent K rows) and scales broadcast within their MX block. This avoids the dequant + re-quantize "pre-transpose" the spec suggested and the extra MX-rounding error that round-trip would have introduced. ``ScatterMoELoRA.forward`` now accepts either a dense tensor or an ``MXWeights``; the MX branch always selects the fused-dX and fused-gather backward kernels (the non-fused dX path would have to materialize a bf16 weight tile, defeating the win). Unit tests cover forward, dX, dA, dB parity for small [E=8, K=128, N=256] and representative [E=32, K=2048, N=1024] shapes; tolerances are calibrated to bf16 MMA noise (atomic-add ordering and FMA reordering between the full-E baseline and compact-active MX path). Integration test exercises a tiny synthetic DeepSeek-V4-style MoE block (E=8, hidden=512, intermediate=256, top_k=2) end-to-end through both Strategy A and Strategy B with LoRA disabled. Signed-off-by: Wing Lian * chore(scattermoe-lora): mxfp4 forward/backward benchmark Add ``bench_mxfp4.py`` and committed results for the representative DeepSeek-V4-style shape (E=128, K=2048, N=1024, top_k=8, M=4096, rank=16). Reports ms/iter, tokens/s, peak GPU memory, and HBM bandwidth utilisation for three configurations: bf16 baseline, Strategy A (selective dequant), Strategy B (fused MX). On the RTX PRO 6000 Blackwell, the all-active-experts shape used here doesn't exercise selective dequant's memory savings (active = E = 128) — A pays the cost of materialising the full bf16 dequant buffer per step (~9 GB peak vs 1.9 GB for B) while still routing through the bf16 kernel. B halves A's wall time (~12 ms vs 30 ms) by eliminating the buffer, but stays slower than the bf16 baseline (5 ms) which assumes the bf16 weights already exist in memory. Signed-off-by: Wing Lian * bench(scattermoe-lora): mxfp4 sparse-routing benchmark numbers Signed-off-by: Wing Lian * bench(scattermoe-lora): mxfp4 seqlen sweep with load-balanced routing Signed-off-by: Wing Lian * fix(scattermoe-lora): correct mx forward smem accounting The MX-aware autotune pruner for the forward kernel under-accounted SMEM: it computed the packed-tile cost as BLOCK_N * BLOCK_K/2 and the scale-tile cost as BLOCK_N * BLOCK_K/MX_BLOCK_SIZE, but the actual tl.load issues a full [BLOCK_N, BLOCK_K]-shaped uint8 fetch for both buffers (the packed buffer reads each byte twice because K_byte = K // 2 indexes a [BLOCK_K]-wide vector; the scale buffer broadcasts within each MX_BLOCK_SIZE K-block). Bring the forward pruner up to the same conservative full-tile accounting already used by _prune_dX_mx_configs. Without this, on the [E=128, K=2048, N=1024] shape with the typical GPU SMEM caps, two to six high-stage configs that were previously selectable would have overflowed SMEM at launch under correct accounting — a silent OOM-in-the-future risk. Signed-off-by: Wing Lian * docs(scattermoe-lora): align mx dx kernel docstring with implementation The file-level docstring for the MXFP4 kernels described the dX kernel as using a pre-transposed [E, K, N/2] layout produced by a 'mx_pre_transpose_for_dx' helper. That helper doesn't exist; the dX kernel actually reuses the forward [E, N, K/2] layout, iterating the N reduction in outer tiles and decoding nibbles along the K rows of each tile. Rewrite the docstring to describe what the code actually does, including the rationale — reusing the forward buffer avoids the dequant + re-quantize round-trip that a pre-transpose would require and keeps dX numerics free of a second MX rounding error stacked on top of the forward quantization. Signed-off-by: Wing Lian * chore(scattermoe-lora): mx code-review nit cleanup F4: Hoist 'is_mxfp4_param' import from inside 'HFScatterMoEGatedMLP.forward' to the top of layers.py — it was being re-imported every step on the hot path. F5: Add a thin compatibility shim for torchao MXTensor internals access in mx_weights.py. The MX paths in selective_dequant.py / mx_weights.py used to reach into 'mx_param.qdata', 'mx_param.scale', 'mx_param.kernel_preference' and call 'MXTensor(...)' with positional args directly. That works at the pinned torchao 0.17.0 but is fragile to internal renames in future torchao releases. Funnel through three helpers — '_mx_qdata', '_mx_scale', '_construct_mxtensor_subset' — that use 'getattr' fallbacks for the buffer attributes and pass the constructor's optional args via 'getattr' too. Single point of pain, no API change. F7: Remove the unused 'NO_K_MASK' heuristic + tl.constexpr param from the dX MX kernel '_scatter2scatter_lora_dX_mx'. The dX kernel never references it (its inner loop masks N, not K), so the constexpr just forced extra autotune key entries. F8: Consolidate the duplicate '_torchao_mxtensor_cls()' definitions (one in selective_dequant.py, one in mx_weights.py) into a single definition in mx_weights.py. selective_dequant.py imports it. Signed-off-by: Wing Lian * test(scattermoe-lora): strengthen mx backward test coverage F3: 'test_strategy_a_backward_fused_variants' previously used 'torch.ones_like(output)' as the grad input and asserted only on dX. A uniform grad zeros out cross-token differences in the fused-gather accumulation, masking reordering bugs; restricting the assertion to dX silently let the dA/dB paths go unchecked across the four '(use_fused_dX, use_fused_gather)' production variants. * Drive the backward with 'torch.randn_like(output) * 0.1'. * Capture and assert dA and dB parity across all four variants using the same 'row_idx' gather pattern as 'test_strategy_a_backward_matches_bf16'. * Forward and dX are still asserted bitwise via 'torch.equal'. dA/dB fall back to atol/rtol = 1e-3 because the fused dA/dB kernel uses 'atomic_add' across N-block programs and the in-flight program count differs between the full-E baseline and the compact-active path; combined with FMA reordering, the 'use_fused_dX=True' variants accumulate ~1 bf16 ULP of unavoidable atomic-order noise. The new bound is still an order of magnitude below that noise floor, so it catches real bugs. F9: The 'test_strategy_b_backward_matches_bf16' dX comparison runs at 'atol=0.5, rtol=2e-2' (small) / 'atol=2.0, rtol=3e-2' (representative) to allow for accumulated bf16 MMA noise over the N reduction. Those bounds are appropriate for legitimate per-element drift but would also admit a uniform multiplicative bug — e.g. an off-by-one on the E8M0 exponent that scales every dX element by 2x. Add a guard alongside the existing 'torch.allclose': mask out near-zero baseline elements (relative to 'bf16_dX.abs().max()'), then require the per-element ratio 'mx_dX / bf16_dX' to have std < 0.5. A uniform multiplicative bug pushes that std to ~0 while the mean shifts; a real-bug per-element drift pushes the std up. This crosscuts the allclose check rather than replacing it. Signed-off-by: Wing Lian * bench(scattermoe-lora): exclude per-iter setup from timed window The previous bench harness did a fresh '.clone()' of x and a 'requires_grad_(True)' on cloned lora A/B tensors every iter inside the timed window. That accounts for buffer allocation, not kernel cost, and biases the numbers toward whichever path produced the smallest activations. Restructure the runners so: * 'x' is cloned once into a leaf tensor with 'requires_grad_(True)' inside 'bench()' (outside the timed warmup + timed loop). * LoRA A/B leaf tensors are constructed once in the runner factory, not per iter. * Each iter calls the runner which sets 'x.grad = A.grad = B.grad = None' (cheap, no GPU sync) so the autograd graph for the timed iteration is fresh and grads don't accumulate. Re-run all three configs end-to-end after this change (dense E=128, sparse E=256 / 10-active, balanced E=256 M-sweep at M ∈ {256, 1024, 4096, 16384}) and refresh the numbers in bench_mxfp4_results.md. Headers and table structure are unchanged. The qualitative ordering holds (Strategy A wins at low active/E, Strategy B wins near active/E ≈ 1, and Strategy A still OOMs across the balanced sweep on the workstation with vLLM colocated), with per-cell numbers within single-digit percent of the prior runs. Signed-off-by: Wing Lian * style(scattermoe-lora): apply pre-commit auto-fixes and mypy fixes Signed-off-by: Wing Lian * fix(scattermoe-lora): mxfp4 shape validation + torchao version messages Signed-off-by: Wing Lian * lint and PR review fixes * fix(scattermoe-lora): restore lint task fixes + add missing forward parity assertions Wing's "lint and PR review fixes" commit (9007a829) reverted three fixes from the prior lint pass. Restore them: 1. parallel_linear_lora.py: use isinstance(expert_weights, MXWeights) directly so mypy can narrow the union — the `is_mx` boolean alias blocks narrowing and re-introduces 2 union-attr errors. 2. bench_mxfp4.py: assert template is not None before the MXTensor(...) constructor — the chunked converter initializes template to None then sets it inside the loop, which mypy can't prove non-None at the call site (6 None-attr errors). 3. test_mxfp4_expert_weights.py: the F841 on fwd_tol was actually a smell of dropped logic. Both backward tests (test_strategy_a_backward_matches_bf16 and test_strategy_b_backward_matches_bf16) compute the forward outputs out_b/out_a/out_s, run backward, and assert gradients match — but never assert that the forward outputs match. A forward bug producing a constant offset (and therefore zero gradient delta) would slip past the bwd-only checks. Add the missing torch.equal(out_b, out_a) for Strategy A (bitwise contract) and torch.allclose(out_b, out_s, **fwd_tol) for Strategy B (MX tol). Signed-off-by: Wing Lian * don't worry about flash-attn direct patches for now --------- Signed-off-by: Wing Lian --- .../libs/scattermoe_lora/kernels/lora_ops.py | 948 ++++++++++++++++++ .../kernels/libs/scattermoe_lora/layers.py | 19 +- .../libs/scattermoe_lora/mx_weights.py | 221 ++++ .../scattermoe_lora/parallel_linear_lora.py | 129 ++- .../libs/scattermoe_lora/selective_dequant.py | 61 +- src/axolotl/loaders/patch_manager.py | 20 +- tests/integrations/kernels/__init__.py | 0 .../kernels/scattermoe_lora/__init__.py | 0 .../kernels/scattermoe_lora/bench_mxfp4.py | 577 +++++++++++ .../scattermoe_lora/bench_mxfp4_results.md | 106 ++ .../test_mxfp4_expert_weights.py | 567 +++++++++++ .../scattermoe_lora/test_mxfp4_integration.py | 246 +++++ 12 files changed, 2849 insertions(+), 45 deletions(-) create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/mx_weights.py create mode 100644 tests/integrations/kernels/__init__.py create mode 100644 tests/integrations/kernels/scattermoe_lora/__init__.py create mode 100644 tests/integrations/kernels/scattermoe_lora/bench_mxfp4.py create mode 100644 tests/integrations/kernels/scattermoe_lora/bench_mxfp4_results.md create mode 100644 tests/integrations/kernels/scattermoe_lora/test_mxfp4_expert_weights.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_mxfp4_integration.py diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py index e8d4309f9b..df3041bea1 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py @@ -2241,3 +2241,951 @@ def grid(META): ) return dA, dB + + +# ============================================================================= +# Fused MXFP4 Forward / dX Kernels +# ============================================================================= +# +# These mirror ``_scatter2scatter_lora`` and ``_scatter2scatter_lora_dX`` but +# load the base weight tile from a packed MXFP4 buffer + E8M0 scale buffer +# instead of a dense bf16 tile. The K-loop unpacks two fp4 values per uint8 +# byte, looks them up in a 16-entry fp32 codebook, multiplies by +# ``2^(scale_byte - 127)``, and casts back to bf16 for ``tl.dot``. +# +# Layout conventions (kernel coordinates): +# * Forward kernel: logical W is ``[E, K, N]`` (block axis = K). +# - packed: ``[E, N, K/2]`` uint8 — stored with the contraction axis K +# contiguous (matches torchao MXTensor's natural last-dim block layout +# once you treat the W storage as ``[E, N, K]``). +# - scale: ``[E, N, K/32]`` uint8 (E8M0). +# * dX kernel: reuses the *forward* MX layout ``[E, N, K/2]`` (no +# pre-transpose). The kernel iterates the N reduction in outer tiles and, +# for each (K_tile, N_tile), decodes nibbles along the K rows of the +# packed tile and broadcasts scales within each ``MX_BLOCK_SIZE`` K-block, +# yielding the same dequantized ``W[e, k, n]`` values the forward path +# consumes. This deliberately avoids a "pre-transpose for dX" step that +# would dequantize + transpose + re-quantize the active-experts slice: +# that round-trip introduces a second MX rounding error on top of the +# forward quantization, perturbing dX in ways that are hard to bound. +# Reusing the forward buffer keeps numerics bitwise-comparable to a +# dequant-then-MMA dX reference. +# +# ``BLOCK_K`` must be a multiple of the OCP block size (32) so that each +# K-tile aligns with whole scale blocks for both the forward and dX +# kernels. The autotune config search space is pruned accordingly in +# ``_prune_fwd_mx_configs`` / ``_prune_dX_mx_configs``. + +_MX_BLOCK_SIZE = 32 + + +@triton.jit +def _compute_expert_block_lora_mxfp4( + E_idx, + E_mask, + M_in_idx, + N_block, + N_mask, + # X + X_ptr, + stride_xm, + stride_xk, + # Packed MXFP4 weight: [E, N, K/2] uint8 + Wp_ptr, + stride_wpe, + stride_wpn, + stride_wpk, + # E8M0 scale: [E, N, K/32] uint8 + Ws_ptr, + stride_wse, + stride_wsn, + stride_wsk, + # FP4 -> fp32 codebook (16 values) + Codebook_ptr, + # LoRA + A_ptr, + stride_ar, + stride_ak, + B_ptr, + stride_bn, + stride_br, + K, + ACTUAL_R: tl.constexpr, + acc, + no_k_mask, + BLOCK_M: tl.constexpr, + BLOCK_K: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_R: tl.constexpr, + MX_BLOCK_SIZE: tl.constexpr, + scaling, + allow_tf32: tl.constexpr, +): + """Forward inner loop for MXFP4 expert weights. + + Computes ``acc += X @ dequant(W_e) + scaling * (X @ A_e^T) @ B_e^T`` for + the active token rows in this M-tile assigned to expert ``E_idx``. + + Each K-loop iteration loads a ``[BLOCK_N, BLOCK_K/2]`` packed tile and a + ``[BLOCK_N, BLOCK_K/MX_BLOCK_SIZE]`` scale tile, unpacks to bf16, and + transposes for the matmul. + """ + K_block = tl.arange(0, BLOCK_K) + K_byte_block = K_block // 2 + K_is_high = (K_block % 2) == 1 + K_scale_block = K_block // MX_BLOCK_SIZE + R_block = tl.arange(0, BLOCK_R) + R_mask = R_block < ACTUAL_R + + INPUT_DTYPE = X_ptr.dtype.element_ty + + # X pointers + X_blk_ptrs = X_ptr + M_in_idx[:, None] * stride_xm + K_block[None, :] * stride_xk + + # LoRA A pointers + A_expert_offset = E_idx * ACTUAL_R + A_blk_ptrs = ( + A_ptr + + (A_expert_offset + R_block)[:, None] * stride_ar + + K_block[None, :] * stride_ak + ) + + # Packed W pointers: tile shape [BLOCK_N, BLOCK_K] (each byte loaded twice) + Wp_blk_ptrs = ( + Wp_ptr + + E_idx * stride_wpe + + N_block[:, None] * stride_wpn + + K_byte_block[None, :] * stride_wpk + ) + # Scale pointers: tile shape [BLOCK_N, BLOCK_K] (broadcast within block) + Ws_blk_ptrs = ( + Ws_ptr + + E_idx * stride_wse + + N_block[:, None] * stride_wsn + + K_scale_block[None, :] * stride_wsk + ) + + iters = tl.cdiv(K, BLOCK_K) + xa_acc = tl.zeros((BLOCK_M, BLOCK_R), dtype=tl.float32) + + for i in range(iters): + if no_k_mask: + K_mask_iter = K_block >= 0 # all-true [BLOCK_K] + else: + K_mask_iter = (i * BLOCK_K + K_block) < K + x_mask = E_mask[:, None] & K_mask_iter[None, :] + a_mask = R_mask[:, None] & K_mask_iter[None, :] + w_mask = N_mask[:, None] & K_mask_iter[None, :] + + x = tl.load(X_blk_ptrs, mask=x_mask, other=0.0).to(INPUT_DTYPE) + a = tl.load(A_blk_ptrs, mask=a_mask, other=0.0).to(INPUT_DTYPE) + + # MXFP4 dequant + packed = tl.load(Wp_blk_ptrs, mask=w_mask, other=0).to(tl.int32) + nibble = tl.where(K_is_high[None, :], (packed >> 4) & 0xF, packed & 0xF) + codebook_val = tl.load(Codebook_ptr + nibble) # [BLOCK_N, BLOCK_K] fp32 + scale_byte = tl.load(Ws_blk_ptrs, mask=w_mask, other=0).to(tl.int32) + scale_fp = tl.exp2((scale_byte - 127).to(tl.float32)) + w_dq_nk = (codebook_val * scale_fp).to(INPUT_DTYPE) # [BLOCK_N, BLOCK_K] + w_tile = tl.trans(w_dq_nk) # [BLOCK_K, BLOCK_N] + + # Base: acc += X @ W + acc += tl.dot(x, w_tile, allow_tf32=allow_tf32).to(tl.float32) + # LoRA: xa_acc += X @ A^T + xa_acc += tl.dot(x, tl.trans(a), allow_tf32=allow_tf32).to(tl.float32) + + X_blk_ptrs += BLOCK_K * stride_xk + A_blk_ptrs += BLOCK_K * stride_ak + Wp_blk_ptrs += (BLOCK_K // 2) * stride_wpk + Ws_blk_ptrs += (BLOCK_K // MX_BLOCK_SIZE) * stride_wsk + + # Epilogue (B @ xa_acc^T) — identical to dense path + B_expert_offset = E_idx * ACTUAL_R + B_blk_ptrs = ( + B_ptr + + N_block[:, None] * stride_bn + + (B_expert_offset + R_block)[None, :] * stride_br + ) + b = tl.load(B_blk_ptrs, mask=N_mask[:, None] & R_mask[None, :], other=0.0) + b_inp = b.to(INPUT_DTYPE) + lora_out = tl.dot(xa_acc.to(INPUT_DTYPE), tl.trans(b_inp), allow_tf32=allow_tf32) + acc += scaling * lora_out + return acc + + +def _scatter2scatter_lora_mx_configs(): + """Forward MX kernel configs. BLOCK_K must be a multiple of MX_BLOCK_SIZE.""" + configs = [] + for block_m, block_n, block_k, warps, stages in product( + [32, 64, 128], + [32, 64], + [32, 64, 128], # all multiples of MX_BLOCK_SIZE=32 + [4, 8], + [3, 4, 5], + ): + configs.append( + triton.Config( + {"BLOCK_M": block_m, "BLOCK_N": block_n, "BLOCK_K": block_k}, + num_stages=stages, + num_warps=warps, + ) + ) + return configs + + +def _prune_fwd_mx_configs(configs, named_args, **kwargs): + """Prune MX forward configs by SMEM and register pressure. + + MX-aware accounting adds the packed W tile and the scale tile per + pipeline stage to the base GEMM SMEM estimate. Both tiles are sized + [BLOCK_N, BLOCK_K] uint8 in the kernel: the packed buffer reads each + byte twice (because K_byte = K // 2 indexes a [BLOCK_K]-wide vector + into a K/2-stride buffer), and the scale buffer reads each byte + MX_BLOCK_SIZE times (broadcast within each K-block). This matches + the conservative full-tile accounting in ``_prune_dX_mx_configs``. + Also require BLOCK_K % MX_BLOCK_SIZE == 0. + """ + smem_cap = _get_smem_capacity() + block_r = named_args.get("BLOCK_R", 64) + + scored = [] + for config in configs: + block_m = config.kwargs["BLOCK_M"] + block_n = config.kwargs["BLOCK_N"] + block_k = config.kwargs["BLOCK_K"] + if block_k % _MX_BLOCK_SIZE != 0: + continue + # Base GEMM tiles (X, dequantized W, acc) + smem_base = _estimate_smem_usage(config.num_stages, block_m, block_n, block_k) + # MX-specific loads per pipeline stage: packed and scale tiles are + # both [BLOCK_N, BLOCK_K] bytes (see docstring for why each is full + # tile size, not BLOCK_K/2 or BLOCK_K/MX_BLOCK_SIZE). + smem_packed = config.num_stages * block_n * block_k * 1 + smem_scale = config.num_stages * block_n * block_k * 1 + # LoRA tiles + smem_lora_loop = config.num_stages * block_r * block_k * 2 + smem_lora_epilogue = block_n * block_r * 2 + smem = ( + smem_base + smem_packed + smem_scale + smem_lora_loop + smem_lora_epilogue + ) + + est_regs = _estimate_register_pressure( + config.num_warps, + (block_m, block_n), # acc + (block_m, block_r), # xa_acc + (block_m, block_k), # x tile + (block_n, block_k), # dequantized w (before transpose) + (block_r, block_k), # a tile + (block_n, block_r), # b tile (epilogue) + ) + if est_regs > _MAX_REGS_SOFT_LIMIT: + continue + + scored.append((smem, config)) + + pruned = [c for s, c in scored if s <= smem_cap - _SMEM_SLACK] + if pruned: + return pruned + if scored: + scored.sort(key=lambda x: x[0]) + return [scored[0][1]] + return [ + min( + configs, + key=lambda c: ( + c.kwargs["BLOCK_M"] * c.kwargs["BLOCK_N"] * c.kwargs["BLOCK_K"] + ), + ) + ] + + +@triton.autotune( + configs=_scatter2scatter_lora_mx_configs(), + key=["M", "N", "K"], + prune_configs_by={"early_config_prune": _prune_fwd_mx_configs}, +) +@triton.heuristics( + { + "NO_K_MASK": lambda args: (args["K"] % args["BLOCK_K"]) == 0, + "NO_N_MASK": lambda args: (args["N"] % args["BLOCK_N"]) == 0, + } +) +@triton.jit +def _scatter2scatter_lora_mx( + # X + X_ptr, + stride_xm: tl.constexpr, + stride_xk: tl.constexpr, + # Packed MXFP4 W [E, N, K/2] + Wp_ptr, + stride_wpe, + stride_wpn: tl.constexpr, + stride_wpk: tl.constexpr, + # E8M0 scale [E, N, K/32] + Ws_ptr, + stride_wse, + stride_wsn: tl.constexpr, + stride_wsk: tl.constexpr, + # FP4 codebook (16 fp32 values) + Codebook_ptr, + # Output + Y_ptr, + stride_ym: tl.constexpr, + stride_yn: tl.constexpr, + # Bias + Bias_ptr, + stride_bias_e: tl.constexpr, + stride_bias_n: tl.constexpr, + # LoRA + LA_ptr, + stride_la_r, + stride_la_k, + LB_ptr, + stride_lb_n, + stride_lb_r, + # Routing + grouped_idx_ptr, + expert_idxs_ptr, + # Dimensions + FAN_OUT: tl.constexpr, + M, + K: tl.constexpr, + N: tl.constexpr, + E: tl.constexpr, + ACTUAL_R: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, + BLOCK_R: tl.constexpr, + MX_BLOCK_SIZE: tl.constexpr, + ACC_TYPE: tl.constexpr, + scaling, + allow_tf32: tl.constexpr, + x_grouped: tl.constexpr, + y_grouped: tl.constexpr, + NO_K_MASK: tl.constexpr, + NO_N_MASK: tl.constexpr, +): + """Fused scatter2scatter forward with MXFP4 base weights + LoRA.""" + pid = tl.program_id(axis=0) + N_BLOCK_COUNT = tl.cdiv(N, BLOCK_N) + M_block_id = pid // N_BLOCK_COUNT + N_block_id = pid % N_BLOCK_COUNT + + M_block = M_block_id * BLOCK_M + tl.arange(0, BLOCK_M) + N_block = N_block_id * BLOCK_N + tl.arange(0, BLOCK_N) + N_mask = N_block < N + M_boundary_mask = M_block < (FAN_OUT * M) + + E_idxs = tl.load(expert_idxs_ptr + M_block, mask=M_boundary_mask, other=E) + no_k_mask = NO_K_MASK + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=ACC_TYPE) + + E_first_idx = tl.min(E_idxs) + E_last_idx = tl.minimum(tl.max(E_idxs), E - 1) + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) + + for E_idx in range(E_first_idx, E_last_idx + 1): + E_mask = E_idxs == E_idx + if x_grouped: + M_in_idx = M_block + else: + M_in_idx = M_idx // FAN_OUT + + acc = _compute_expert_block_lora_mxfp4( + E_idx, + E_mask, + M_in_idx, + N_block, + N_mask, + X_ptr, + stride_xm, + stride_xk, + Wp_ptr, + stride_wpe, + stride_wpn, + stride_wpk, + Ws_ptr, + stride_wse, + stride_wsn, + stride_wsk, + Codebook_ptr, + LA_ptr, + stride_la_r, + stride_la_k, + LB_ptr, + stride_lb_n, + stride_lb_r, + K, + ACTUAL_R, + acc, + no_k_mask, + BLOCK_M, + BLOCK_K, + BLOCK_N, + BLOCK_R, + MX_BLOCK_SIZE, + scaling, + allow_tf32=allow_tf32, + ) + + if Bias_ptr is not None: + B_blk_ptrs = ( + Bias_ptr + + E_idxs[:, None] * stride_bias_e + + N_block[None, :] * stride_bias_n + ) + acc += tl.load(B_blk_ptrs, mask=M_boundary_mask[:, None] & N_mask[None, :]) + + if y_grouped: + M_out_idx = M_block + else: + M_out_idx = M_idx + Y_blk_ptrs = Y_ptr + (M_out_idx[:, None] * stride_ym + N_block[None, :] * stride_yn) + tl.store(Y_blk_ptrs, acc, mask=M_boundary_mask[:, None] & N_mask[None, :]) + + +def scatter2scatter_lora_mx( + X: torch.Tensor, + W_mx, # MXWeights with layout=FWD + sorted_expert_idxs: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + k: int, + lora_A: torch.Tensor, + lora_B: torch.Tensor, + scaling: float, + b: Optional[torch.Tensor] = None, + x_grouped: bool = False, + y_grouped: bool = False, + out: Optional[torch.Tensor] = None, +) -> torch.Tensor: + """Forward dispatcher for the fused MXFP4 + LoRA kernel. + + ``W_mx`` is an ``MXWeights`` instance in ``FWD`` layout (block axis = K). + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.mx_weights import ( + MXLayout, + fp4_codebook, + ) + + assert W_mx.layout == MXLayout.FWD, ( + f"scatter2scatter_lora_mx requires FWD layout, got {W_mx.layout}" + ) + assert sorted_scattered_idxs.size(0) == sorted_expert_idxs.size(0) + assert sorted_scattered_idxs.size(0) == X.size(0) * k + + K = W_mx.K + N = W_mx.N + E = W_mx.packed.size(0) + R = lora_A.size(0) // E + BLOCK_R = _block_r_for_rank(R) + L_scattered = sorted_expert_idxs.size(0) + + if out is None: + output = torch.empty((L_scattered, N), device=X.device, dtype=X.dtype) + else: + assert out.size(0) == L_scattered and out.size(1) == N + output = out + + def grid(META): + return ( + triton.cdiv(L_scattered, META["BLOCK_M"]) * triton.cdiv(N, META["BLOCK_N"]), + ) + + if b is None: + stride_be = stride_bn = 0 + b_ptr = None + else: + stride_be, stride_bn = b.stride() + b_ptr = b + + codebook = fp4_codebook(X.device) + + _scatter2scatter_lora_mx[grid]( + X, + X.stride(0), + X.stride(1), + W_mx.packed, + W_mx.packed.stride(0), + W_mx.packed.stride(1), + W_mx.packed.stride(2), + W_mx.scales, + W_mx.scales.stride(0), + W_mx.scales.stride(1), + W_mx.scales.stride(2), + codebook, + output, + output.stride(0), + output.stride(1), + b_ptr, + stride_be, + stride_bn, + lora_A, + lora_A.stride(0), + lora_A.stride(1), + lora_B, + lora_B.stride(0), + lora_B.stride(1), + sorted_scattered_idxs, + sorted_expert_idxs, + FAN_OUT=k, + M=X.size(0), + K=K, + N=N, + E=E, + ACTUAL_R=R, + BLOCK_R=BLOCK_R, + MX_BLOCK_SIZE=_MX_BLOCK_SIZE, + ACC_TYPE=tl.float32, + scaling=scaling, + allow_tf32=ALLOW_TF32, + x_grouped=x_grouped, + y_grouped=y_grouped, + ) + return output + + +@triton.jit +def _compute_expert_block_lora_dX_mxfp4( + E_idx, + E_mask, + M_in_idx, + K_block, + K_mask, + # dY [M, N] + DY_ptr, + stride_dym, + stride_dyn, + # Packed MXFP4 W in FWD layout: [E, N, K/2] uint8 (block axis = K) + Wp_ptr, + stride_wpe, + stride_wpn, + stride_wpk, + # Scale [E, N, K/32] + Ws_ptr, + stride_wse, + stride_wsn, + stride_wsk, + # FP4 codebook + Codebook_ptr, + # LoRA + A_ptr, + stride_ar, + stride_ak, + B_ptr, + stride_bn, + stride_br, + N, + ACTUAL_R: tl.constexpr, + acc, + no_n_mask, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, + BLOCK_R: tl.constexpr, + MX_BLOCK_SIZE: tl.constexpr, + scaling, + allow_tf32: tl.constexpr, +): + """dX inner loop for MXFP4 base weights, FWD layout (block axis = K). + + Computes ``acc += DY @ dequant(W_e)^T + scaling * (DY @ B_e) @ A_e`` for + the active token rows in this M-tile assigned to expert ``E_idx``. + + W storage is the *forward* MX layout: packed ``[E, N, K/2]`` and scale + ``[E, N, K/32]`` — the same buffer used by the forward kernel, no + pre-transpose / re-quantize required. Per N-loop iter we load packed + ``[BLOCK_K, BLOCK_N]`` and scale ``[BLOCK_K, BLOCK_N]`` tiles indexed + K-as-row × N-as-col; nibbles are extracted along the K axis (the byte + is shared by adjacent K rows) and scales broadcast within their + ``MX_BLOCK_SIZE``-element K block. + """ + N_block = tl.arange(0, BLOCK_N) + # K-axis decode tables (K-along-rows of the W tile) + K_byte_block = K_block // 2 + K_is_high = (K_block % 2) == 1 + K_scale_block = K_block // MX_BLOCK_SIZE + R_block = tl.arange(0, BLOCK_R) + R_mask = R_block < ACTUAL_R + + INPUT_DTYPE = DY_ptr.dtype.element_ty + + DY_blk_ptrs = ( + DY_ptr + M_in_idx[:, None] * stride_dym + N_block[None, :] * stride_dyn + ) + # Packed W in FWD layout [E, N, K/2] + # Tile shape [BLOCK_K, BLOCK_N]: row=K_byte (each byte loaded twice across + # adjacent K rows), col=N — note N is the *fast* axis here because + # stride_wpn (= K/2) is large, but the K row stride is 1. + Wp_blk_ptrs = ( + Wp_ptr + + E_idx * stride_wpe + + N_block[None, :] * stride_wpn + + K_byte_block[:, None] * stride_wpk + ) + # Scale [E, N, K/32]; row=K_scale_idx (broadcast within MX_BLOCK_SIZE), col=N + Ws_blk_ptrs = ( + Ws_ptr + + E_idx * stride_wse + + N_block[None, :] * stride_wsn + + K_scale_block[:, None] * stride_wsk + ) + B_expert_offset = E_idx * ACTUAL_R + B_blk_ptrs = ( + B_ptr + + N_block[:, None] * stride_bn + + (B_expert_offset + R_block)[None, :] * stride_br + ) + + iters = tl.cdiv(N, BLOCK_N) + dy_b_acc = tl.zeros((BLOCK_M, BLOCK_R), dtype=tl.float32) + + for i in range(iters): + if no_n_mask: + N_mask_iter = N_block >= 0 # all-true [BLOCK_N] + else: + N_mask_iter = (i * BLOCK_N + N_block) < N + dy_mask = E_mask[:, None] & N_mask_iter[None, :] + w_mask = K_mask[:, None] & N_mask_iter[None, :] + b_mask = N_mask_iter[:, None] & R_mask[None, :] + + dy = tl.load(DY_blk_ptrs, mask=dy_mask, other=0.0).to(INPUT_DTYPE) + + # MXFP4 dequant of W tile [BLOCK_K, BLOCK_N] + packed = tl.load(Wp_blk_ptrs, mask=w_mask, other=0).to(tl.int32) + nibble = tl.where(K_is_high[:, None], (packed >> 4) & 0xF, packed & 0xF) + codebook_val = tl.load(Codebook_ptr + nibble) + scale_byte = tl.load(Ws_blk_ptrs, mask=w_mask, other=0).to(tl.int32) + scale_fp = tl.exp2((scale_byte - 127).to(tl.float32)) + w_dq = (codebook_val * scale_fp).to(INPUT_DTYPE) # [BLOCK_K, BLOCK_N] + + # Base: acc += DY @ W^T ([M, N] @ [N, K] -> [M, K]) + # W tile is [BLOCK_K, BLOCK_N]; W^T = tl.trans(w_dq) -> [BLOCK_N, BLOCK_K] + acc += tl.dot(dy, tl.trans(w_dq), allow_tf32=allow_tf32).to(tl.float32) + + # LoRA: dy_b_acc += DY @ B + b = tl.load(B_blk_ptrs, mask=b_mask, other=0.0).to(INPUT_DTYPE) + dy_b_acc += tl.dot(dy, b, allow_tf32=allow_tf32).to(tl.float32) + + DY_blk_ptrs += BLOCK_N * stride_dyn + Wp_blk_ptrs += BLOCK_N * stride_wpn + Ws_blk_ptrs += BLOCK_N * stride_wsn + B_blk_ptrs += BLOCK_N * stride_bn + + # Epilogue: (DY @ B) @ A ([M, R] @ [R, K] -> [M, K]) + A_expert_offset = E_idx * ACTUAL_R + A_blk_ptrs = ( + A_ptr + + (A_expert_offset + R_block)[:, None] * stride_ar + + K_block[None, :] * stride_ak + ) + a_e = tl.load(A_blk_ptrs, mask=R_mask[:, None] & K_mask[None, :], other=0.0).to( + INPUT_DTYPE + ) + lora_dx = tl.dot(dy_b_acc.to(INPUT_DTYPE), a_e, allow_tf32=allow_tf32) + acc += scaling * lora_dx + return acc + + +def _scatter2scatter_lora_dX_mx_configs(): + """dX MX kernel configs. BLOCK_K must be a multiple of MX_BLOCK_SIZE + because scales broadcast within MX_BLOCK_SIZE-element K-blocks.""" + configs = [] + for block_m, block_k, block_n, warps, stages in product( + [32, 64, 128], + [32, 64, 128], # all multiples of MX_BLOCK_SIZE=32 + [32, 64], + [4, 8], + [3, 4, 5], + ): + configs.append( + triton.Config( + {"BLOCK_M": block_m, "BLOCK_K": block_k, "BLOCK_N": block_n}, + num_stages=stages, + num_warps=warps, + ) + ) + return configs + + +def _prune_dX_mx_configs(configs, named_args, **kwargs): + """Prune dX MX configs by SMEM and register pressure (MX-aware).""" + smem_cap = _get_smem_capacity() + block_r = named_args.get("BLOCK_R", 64) + + scored = [] + for config in configs: + block_m = config.kwargs["BLOCK_M"] + block_k = config.kwargs["BLOCK_K"] + block_n = config.kwargs["BLOCK_N"] + if block_k % _MX_BLOCK_SIZE != 0: + continue + smem_base = _estimate_smem_usage(config.num_stages, block_m, block_k, block_n) + # Per stage: packed W tile [BLOCK_K, BLOCK_N] (bytes — each byte + # serves two K rows) and scale tile [BLOCK_K, BLOCK_N] (bytes, broadcast + # within each K-block of size MX_BLOCK_SIZE). Approximate to BLOCK_K * + # BLOCK_N bytes for each (overestimates SMEM slightly, conservative). + smem_packed = config.num_stages * block_k * block_n * 1 + smem_scale = config.num_stages * block_k * block_n * 1 + smem_lora_loop = config.num_stages * block_n * block_r * 2 + smem_lora_epilogue = block_r * block_k * 2 + smem = ( + smem_base + smem_packed + smem_scale + smem_lora_loop + smem_lora_epilogue + ) + + est_regs = _estimate_register_pressure( + config.num_warps, + (block_m, block_k), + (block_m, block_r), + (block_m, block_n), + (block_k, block_n), + (block_n, block_r), + (block_r, block_k), + ) + if est_regs > _MAX_REGS_SOFT_LIMIT: + continue + + scored.append((smem, config)) + + pruned = [c for s, c in scored if s <= smem_cap - _SMEM_SLACK] + if pruned: + return pruned + if scored: + scored.sort(key=lambda x: x[0]) + return [scored[0][1]] + return [ + min( + configs, + key=lambda c: ( + c.kwargs["BLOCK_M"] * c.kwargs["BLOCK_K"] * c.kwargs["BLOCK_N"] + ), + ) + ] + + +@triton.autotune( + configs=_scatter2scatter_lora_dX_mx_configs(), + key=["M", "N", "K"], + prune_configs_by={"early_config_prune": _prune_dX_mx_configs}, +) +@triton.heuristics( + { + "NO_N_MASK": lambda args: (args["N"] % args["BLOCK_N"]) == 0, + } +) +@triton.jit +def _scatter2scatter_lora_dX_mx( + DY_ptr, + stride_dym: tl.constexpr, + stride_dyn: tl.constexpr, + # Packed W in FWD layout [E, N, K/2] + Wp_ptr, + stride_wpe, + stride_wpn: tl.constexpr, + stride_wpk: tl.constexpr, + # Scale [E, N, K/32] + Ws_ptr, + stride_wse, + stride_wsn: tl.constexpr, + stride_wsk: tl.constexpr, + Codebook_ptr, + DX_ptr, + stride_dxm: tl.constexpr, + stride_dxk: tl.constexpr, + LA_ptr, + stride_la_r, + stride_la_k, + LB_ptr, + stride_lb_n, + stride_lb_r, + grouped_idx_ptr, + expert_idxs_ptr, + FAN_OUT: tl.constexpr, + M, + K: tl.constexpr, + N: tl.constexpr, + E: tl.constexpr, + ACTUAL_R: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, + BLOCK_R: tl.constexpr, + MX_BLOCK_SIZE: tl.constexpr, + ACC_TYPE: tl.constexpr, + scaling, + allow_tf32: tl.constexpr, + dy_grouped: tl.constexpr, + dx_grouped: tl.constexpr, + NO_N_MASK: tl.constexpr, +): + """Fused MXFP4 dX kernel.""" + pid = tl.program_id(axis=0) + K_BLOCK_COUNT = tl.cdiv(K, BLOCK_K) + M_block_id = pid // K_BLOCK_COUNT + K_block_id = pid % K_BLOCK_COUNT + + M_block = M_block_id * BLOCK_M + tl.arange(0, BLOCK_M) + K_block = K_block_id * BLOCK_K + tl.arange(0, BLOCK_K) + K_mask = K_block < K + M_boundary_mask = M_block < (FAN_OUT * M) + + E_idxs = tl.load(expert_idxs_ptr + M_block, mask=M_boundary_mask, other=E) + no_n_mask = NO_N_MASK + acc = tl.zeros((BLOCK_M, BLOCK_K), dtype=ACC_TYPE) + + E_first_idx = tl.min(E_idxs) + E_last_idx = tl.minimum(tl.max(E_idxs), E - 1) + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) + + for E_idx in range(E_first_idx, E_last_idx + 1): + E_mask = E_idxs == E_idx + if dy_grouped: + M_in_idx = M_block + else: + M_in_idx = M_idx // FAN_OUT + + acc = _compute_expert_block_lora_dX_mxfp4( + E_idx, + E_mask, + M_in_idx, + K_block, + K_mask, + DY_ptr, + stride_dym, + stride_dyn, + Wp_ptr, + stride_wpe, + stride_wpn, + stride_wpk, + Ws_ptr, + stride_wse, + stride_wsn, + stride_wsk, + Codebook_ptr, + LA_ptr, + stride_la_r, + stride_la_k, + LB_ptr, + stride_lb_n, + stride_lb_r, + N, + ACTUAL_R, + acc, + no_n_mask, + BLOCK_M, + BLOCK_N, + BLOCK_K, + BLOCK_R, + MX_BLOCK_SIZE, + scaling, + allow_tf32=allow_tf32, + ) + + if dx_grouped: + M_out_idx = M_block + else: + M_out_idx = M_idx + DX_blk_ptrs = DX_ptr + ( + M_out_idx[:, None] * stride_dxm + K_block[None, :] * stride_dxk + ) + tl.store(DX_blk_ptrs, acc, mask=M_boundary_mask[:, None] & K_mask[None, :]) + + +def scatter2scatter_lora_dX_mx( + DY: torch.Tensor, + W_mx, # MXWeights in FWD layout (same buffer as forward) + sorted_expert_idxs: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + k: int, + lora_A: torch.Tensor, + lora_B: torch.Tensor, + scaling: float, + dy_grouped: bool = True, + dx_grouped: bool = False, + out: Optional[torch.Tensor] = None, +) -> torch.Tensor: + """Backward-dX dispatcher for the fused MXFP4 kernel. + + Reuses the *forward* MX layout (block axis = K). The kernel iterates the + N reduction in tiles, and for each (K_tile, N_tile) sub-tile, decodes + nibbles along the K rows of the tile (K_byte = K // 2) and broadcasts + scales within each ``MX_BLOCK_SIZE`` K-block. This avoids the + dequant + re-quantize "pre-transpose" round-trip and the extra MX + rounding error that would have introduced. + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.mx_weights import ( + MXLayout, + fp4_codebook, + ) + + assert W_mx.layout == MXLayout.FWD, ( + f"scatter2scatter_lora_dX_mx requires FWD layout, got {W_mx.layout}" + ) + assert sorted_scattered_idxs.size(0) == sorted_expert_idxs.size(0) + + K = W_mx.K + N = W_mx.N + E = W_mx.packed.size(0) + R = lora_A.size(0) // E + BLOCK_R = _block_r_for_rank(R) + L_scattered = sorted_expert_idxs.size(0) + + if dy_grouped: + M = DY.size(0) + fan_out = 1 + else: + M = DY.size(0) + fan_out = k + + if out is None: + output = torch.empty((L_scattered, K), device=DY.device, dtype=DY.dtype) + else: + assert out.size(0) == L_scattered and out.size(1) == K + output = out + + def grid(META): + return ( + triton.cdiv(L_scattered, META["BLOCK_M"]) * triton.cdiv(K, META["BLOCK_K"]), + ) + + codebook = fp4_codebook(DY.device) + + _scatter2scatter_lora_dX_mx[grid]( + DY, + DY.stride(0), + DY.stride(1), + W_mx.packed, + W_mx.packed.stride(0), + W_mx.packed.stride(1), + W_mx.packed.stride(2), + W_mx.scales, + W_mx.scales.stride(0), + W_mx.scales.stride(1), + W_mx.scales.stride(2), + codebook, + output, + output.stride(0), + output.stride(1), + lora_A, + lora_A.stride(0), + lora_A.stride(1), + lora_B, + lora_B.stride(0), + lora_B.stride(1), + sorted_scattered_idxs, + sorted_expert_idxs, + FAN_OUT=fan_out, + M=M, + K=K, + N=N, + E=E, + ACTUAL_R=R, + BLOCK_R=BLOCK_R, + MX_BLOCK_SIZE=_MX_BLOCK_SIZE, + ACC_TYPE=tl.float32, + scaling=scaling, + allow_tf32=ALLOW_TF32, + dy_grouped=dy_grouped, + dx_grouped=dx_grouped, + ) + return output diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py index 8fd10c8e95..de3f57af62 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/layers.py @@ -37,6 +37,7 @@ from .parallel_experts import flatten_sort_count, parallel_linear from .parallel_linear_lora import get_lora_params_from_wrapper, parallel_linear_lora +from .selective_dequant import is_mxfp4_param # ============================================================================= # LoRA layout conversion utilities (peft <-> scattermoe) @@ -457,14 +458,20 @@ def forward(self: nn.Module, layer_input: torch.Tensor): # ==================================================================== # Selective expert weight dequantization # ==================================================================== - # When experts are BnB-quantized (quantize_moe_experts), dequantize - # only the active experts instead of all E. This saves ~97% memory - # for the transient dequant buffer when few experts are active. - use_selective = ( - getattr(self, "_use_selective_dequant", False) - and hasattr(experts, "parametrizations") + # When experts are BnB-quantized (quantize_moe_experts) or MXFP4 + # (torchao MXTensor), dequantize only the active experts instead of + # all E. This saves ~97% memory for the transient dequant buffer when + # few experts are active. MXFP4 always routes through selective + # dequant because the kernel needs bf16 weights and full-tensor + # dequant of 256-expert MX params is prohibitive. + has_bnb_param = ( + hasattr(experts, "parametrizations") and "gate_up_proj" in experts.parametrizations ) + has_mxfp4_param = is_mxfp4_param(getattr(experts, "gate_up_proj", None)) + use_selective = ( + getattr(self, "_use_selective_dequant", False) and has_bnb_param + ) or has_mxfp4_param if use_selective: from axolotl.integrations.kernels.libs.scattermoe_lora.selective_dequant import ( diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/mx_weights.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/mx_weights.py new file mode 100644 index 0000000000..d3916c54bb --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/mx_weights.py @@ -0,0 +1,221 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +MXFP4 expert weight container + helpers for the fused-dequant Triton kernels. + +The container carries the packed uint8 ``[E_active, N, K/2]`` data and the +E8M0 ``[E_active, N, K/32]`` scales for the *active* experts of one MoE +step. ``parallel_linear_lora`` checks for this container instance and +routes to the MX-aware Triton kernels. + +Layout: OCP block axis is the contraction axis ``K`` — the last storage +dim. The same buffer is consumed by both the forward kernel (K is the +matmul reduction axis) and the dX kernel (K is the output axis, with +scales broadcast within ``MX_BLOCK_SIZE``-element K blocks). No +pre-transpose / re-quantize is needed for the backward path. + +The FP4 E2M1 codebook is the standard OCP-MX one (16 values: +``±{0, 0.5, 1, 1.5, 2, 3, 4, 6}``); we cache one fp32 copy per CUDA device. +""" + +from __future__ import annotations + +import enum +from dataclasses import dataclass +from typing import Optional + +import torch + +MX_BLOCK_SIZE = 32 + +# Standard OCP-MX fp4 e2m1 codebook (sign bit | 2-bit exp | 1-bit mantissa). +# Index by the raw 4-bit nibble. Cached fp32 tensor for kernel lookups. +_FP4_E2M1_LUT = ( + 0.0, + 0.5, + 1.0, + 1.5, + 2.0, + 3.0, + 4.0, + 6.0, + -0.0, + -0.5, + -1.0, + -1.5, + -2.0, + -3.0, + -4.0, + -6.0, +) + +_LUT_CACHE: dict[torch.device, torch.Tensor] = {} + + +def fp4_codebook(device: torch.device) -> torch.Tensor: + """Return the cached 16-entry FP4 E2M1 → fp32 lookup on ``device``.""" + key = device + lut = _LUT_CACHE.get(key) + if lut is None or lut.device != device: + lut = torch.tensor(_FP4_E2M1_LUT, dtype=torch.float32, device=device) + _LUT_CACHE[key] = lut + return lut + + +class MXLayout(enum.IntEnum): + """Which axis the OCP-MX block scaling runs along, in *kernel* coords. + + Currently only ``FWD`` (block axis = K) is supported and used by both + the forward and dX kernels. The enum is kept as a future extension + point for swizzled or N-axis-blocked variants. + """ + + FWD = 0 + + +@dataclass +class MXWeights: + """Packed + scale tensors for one MoE projection's active experts. + + Attributes + ---------- + packed: + ``uint8`` tensor, shape ``[E_active, N, K/2]``. + scales: + ``uint8`` (E8M0) tensor, shape ``[E_active, N, K/32]``. + K, N: + Logical contraction/output dimensions of the dequantized W. + layout: + Which axis the block scaling runs along (see ``MXLayout``). + block_size: + OCP MX block size; only ``32`` is supported by the kernels. + """ + + packed: torch.Tensor + scales: torch.Tensor + K: int + N: int + layout: MXLayout = MXLayout.FWD + block_size: int = MX_BLOCK_SIZE + num_experts: Optional[int] = None # E_active; convenience field + orig_dtype: torch.dtype = torch.bfloat16 + + def __post_init__(self) -> None: + assert self.block_size == MX_BLOCK_SIZE, ( + f"only block_size={MX_BLOCK_SIZE} is supported, got {self.block_size}" + ) + # scales are E8M0 (float8_e8m0fnu) in torchao; viewed as uint8 here so + # the Triton kernel can load them with simple integer arithmetic. + if self.scales.dtype != torch.uint8: + self.scales = self.scales.view(torch.uint8) + assert self.packed.dtype == torch.uint8, ( + f"packed must be uint8, got {self.packed.dtype}" + ) + if self.num_experts is None: + self.num_experts = self.packed.size(0) + + @property + def device(self) -> torch.device: + return self.packed.device + + +def _torchao_mxtensor_cls(): + """Return the torchao MXTensor class, or ``None`` if torchao is missing.""" + try: + from torchao.prototype.mx_formats.mx_tensor import MXTensor + except ImportError: + return None + return MXTensor + + +def _mx_qdata(mx) -> torch.Tensor: + """Read the packed-nibble buffer off an MXTensor, tolerating torchao + renaming the attribute between versions.""" + qdata = getattr(mx, "qdata", None) + if qdata is None: + qdata = getattr(mx, "_data", None) + if qdata is None: + raise AttributeError( + "torchao MXTensor exposes neither .qdata nor ._data; " + "this torchao version is unsupported." + ) + return qdata + + +def _mx_scale(mx) -> torch.Tensor: + """Read the E8M0 scale buffer off an MXTensor, tolerating torchao + renaming the attribute between versions.""" + scale = getattr(mx, "scale", None) + if scale is None: + scale = getattr(mx, "_scale_e8m0", None) + if scale is None: + raise AttributeError( + "torchao MXTensor exposes neither .scale nor ._scale_e8m0; " + "this torchao version is unsupported." + ) + return scale + + +def _construct_mxtensor_subset( + parent, qdata_slice: torch.Tensor, scale_slice: torch.Tensor +): + """Construct a new MXTensor that shares ``parent``'s metadata but uses + the provided ``qdata_slice`` / ``scale_slice`` buffers. + + Pinned to torchao 0.17.0's positional constructor (qdata, scale, + elem_dtype, block_size, orig_dtype, kernel_preference, + act_quant_kwargs, is_swizzled_scales). Optional attributes are read via + ``getattr`` so we degrade gracefully if a future torchao version drops + or renames one — the single point of pain for torchao internals access + across this codebase. + """ + MXTensor = _torchao_mxtensor_cls() + if MXTensor is None: + raise ImportError("MXFP4 path requires torchao (install `torchao>=0.7`).") + kernel_preference = getattr(parent, "kernel_preference", None) + act_quant_kwargs = getattr(parent, "act_quant_kwargs", None) + is_swizzled_scales = getattr(parent, "is_swizzled_scales", False) + return MXTensor( + qdata_slice, + scale_slice, + parent.elem_dtype, + parent.block_size, + parent.orig_dtype, + kernel_preference, + act_quant_kwargs, + is_swizzled_scales, + ) + + +def selective_mx_weights_fwd(mx_param, active_experts: torch.Tensor) -> MXWeights: + """Slice an MXFP4 expert parameter to the active set, keeping the K-axis + block layout (FWD). The returned ``MXWeights.packed`` has shape + ``[num_active, N, K/2]`` and is directly consumable by the forward MX + kernel via ``parallel_linear_lora``.""" + MXTensor = _torchao_mxtensor_cls() + if MXTensor is None: + raise ImportError("MXFP4 fused path requires torchao>=0.7 (install `torchao`).") + assert isinstance(mx_param, MXTensor), ( + f"selective_mx_weights_fwd expects an MXTensor, got {type(mx_param)}" + ) + assert mx_param.elem_dtype == torch.float4_e2m1fn_x2, ( + "only MXFP4 (float4_e2m1fn_x2) is supported" + ) + sub_qdata = _mx_qdata(mx_param)[active_experts].contiguous() + sub_scale = _mx_scale(mx_param)[active_experts].contiguous() + # Logical dims (kernel's K, N): the contraction axis is K, the OCP block + # axis is the LAST storage axis (= K). N is the leading non-expert axis. + N = sub_qdata.size(1) + K = sub_qdata.size(2) * 2 + return MXWeights( + packed=sub_qdata, + scales=sub_scale, + K=K, + N=N, + layout=MXLayout.FWD, + block_size=mx_param.block_size, + num_experts=sub_qdata.size(0), + orig_dtype=mx_param.orig_dtype, + ) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py index 17dfd420c0..af2dbbd86a 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py @@ -23,7 +23,7 @@ dB = scaling * dY^T @ (X @ A^T) (per-expert, on grouped data) """ -from typing import Optional +from typing import Optional, Union import torch @@ -33,7 +33,10 @@ group_bwd_lora_fused, scatter2scatter_lora, scatter2scatter_lora_dX, + scatter2scatter_lora_dX_mx, + scatter2scatter_lora_mx, ) +from .mx_weights import MXLayout, MXWeights class ScatterMoELoRA(torch.autograd.Function): @@ -50,7 +53,7 @@ class ScatterMoELoRA(torch.autograd.Function): def forward( ctx, x: torch.Tensor, - expert_weights: torch.Tensor, + expert_weights: Union[torch.Tensor, MXWeights], k: int, sorted_expert_idxs: torch.Tensor, sorted_scattered_idxs: torch.Tensor, @@ -65,26 +68,49 @@ def forward( use_fused_dX: bool = False, use_fused_gather: bool = False, ): - # Cast weights to match input dtype (e.g. 8-bit LoRA) - if expert_weights.dtype != x.dtype: - expert_weights = expert_weights.to(x.dtype) + if isinstance(expert_weights, MXWeights): + assert expert_weights.layout == MXLayout.FWD, ( + "MXWeights passed to forward must be in FWD layout" + ) + is_mx = True + else: + # Cast weights to match input dtype (e.g. 8-bit LoRA) + if expert_weights.dtype != x.dtype: + expert_weights = expert_weights.to(x.dtype) + is_mx = False if expert_biases is not None and expert_biases.dtype != x.dtype: expert_biases = expert_biases.to(x.dtype) with torch.device(x.device): - # Fused forward: Y = X @ W + scaling * (X @ A^T) @ B^T - output = scatter2scatter_lora( - X=x, - W=expert_weights, - sorted_expert_idxs=sorted_expert_idxs, - sorted_scattered_idxs=sorted_scattered_idxs, - k=k, - lora_A=lora_A, - lora_B=lora_B, - scaling=scaling, - b=expert_biases, - x_grouped=grouped_in, - y_grouped=grouped_out, - ) + if is_mx: + # Fused MXFP4 forward: dequant happens inside the K-loop + output = scatter2scatter_lora_mx( + X=x, + W_mx=expert_weights, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=k, + lora_A=lora_A, + lora_B=lora_B, + scaling=scaling, + b=expert_biases, + x_grouped=grouped_in, + y_grouped=grouped_out, + ) + else: + # Fused forward: Y = X @ W + scaling * (X @ A^T) @ B^T + output = scatter2scatter_lora( + X=x, + W=expert_weights, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=k, + lora_A=lora_A, + lora_B=lora_B, + scaling=scaling, + b=expert_biases, + x_grouped=grouped_in, + y_grouped=grouped_out, + ) # Handle gating (weighted combination of top-k expert outputs) if gates is not None: @@ -117,8 +143,12 @@ def forward( ctx.grouped_out = grouped_out ctx.k = k ctx.scaling = scaling - ctx.use_fused_dX = use_fused_dX - ctx.use_fused_gather = use_fused_gather + # MXFP4 forces fused dX + gather: the non-fused dX path would have + # to materialise a bf16 weight tile, defeating the kernel-fusion + # win, and the gather/scatter pattern is identical. + ctx.use_fused_dX = True if is_mx else use_fused_dX + ctx.use_fused_gather = True if is_mx else use_fused_gather + ctx.is_mx = is_mx return output @@ -141,7 +171,11 @@ def backward(ctx, grad_out: torch.Tensor): scaling = ctx.scaling grouped_in = ctx.grouped_in grouped_out = ctx.grouped_out - E = expert_weights.size(0) + is_mx = ctx.is_mx + if is_mx: + E = expert_weights.packed.size(0) + else: + E = expert_weights.size(0) # ------------------------------------------------------------------ # Gate gradients (if using top-k gating with routing weights) @@ -171,7 +205,7 @@ def backward(ctx, grad_out: torch.Tensor): # -> use dy_grouped=True in the fused kernel M_total = sorted_scattered_idxs.size(0) K_dim = x.size(-1) - N_dim = expert_weights.size(-1) + N_dim = expert_weights.N if is_mx else expert_weights.size(-1) fuse_gather_workload = M_total * max(K_dim, N_dim) _FUSE_GATHER_THRESHOLD = 2**24 # ~16M elements @@ -248,7 +282,38 @@ def backward(ctx, grad_out: torch.Tensor): # ------------------------------------------------------------------ # Input gradient: dX = dY @ W^T + scaling * (dY @ B) @ A # ------------------------------------------------------------------ - if ctx.use_fused_dX: + if is_mx: + # dX kernel reuses the forward MX layout (block axis = K) — + # no pre-transpose/re-quantize needed. + if can_fuse_gather and not grouped_out: + d_expanded_input = scatter2scatter_lora_dX_mx( + DY=grad_out, + W_mx=expert_weights, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=1, + lora_A=lora_A, + lora_B=lora_B, + scaling=scaling, + dy_grouped=False, + dx_grouped=grouped_in, + out=d_expanded_input, + ) + else: + d_expanded_input = scatter2scatter_lora_dX_mx( + DY=grouped_grad_out, + W_mx=expert_weights, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=1, + lora_A=lora_A, + lora_B=lora_B, + scaling=scaling, + dy_grouped=True, + dx_grouped=grouped_in, + out=d_expanded_input, + ) + elif ctx.use_fused_dX: if can_fuse_gather and not grouped_out: # Fully fused: read ungrouped DY via scatter pattern d_expanded_input = scatter2scatter_lora_dX( @@ -317,12 +382,16 @@ def backward(ctx, grad_out: torch.Tensor): x.size(0), k, d_expanded_input.size(-1) ).sum(-2) - # W is frozen during LoRA training -- skip weight gradient - d_weights = ( - torch.zeros_like(expert_weights) - if expert_weights.requires_grad - else None - ) + # W is frozen during LoRA training -- skip weight gradient. + # (MX weights are containers, not tensors, and never carry grad.) + if is_mx: + d_weights = None + else: + d_weights = ( + torch.zeros_like(expert_weights) + if expert_weights.requires_grad + else None + ) d_biases = None return ( diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py index 1df8b2f684..6702b2f15d 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py @@ -13,6 +13,8 @@ This module provides format-agnostic selective weight extraction: - BnB 4-bit (nf4/fp4): slice quantized data + absmax per expert + - MXFP4 (torchao MXTensor with elem_dtype=float4_e2m1fn_x2): slice + qdata + E8M0 scale per expert and dequantize via torchao - bf16/fp32: direct indexing (no dequant needed) - FP8: slice + cast @@ -24,6 +26,21 @@ import torch import torch.nn as nn +from .mx_weights import ( + _construct_mxtensor_subset, + _mx_qdata, + _mx_scale, + _torchao_mxtensor_cls, +) + + +def is_mxfp4_param(param) -> bool: + """True iff ``param`` is a torchao MXTensor with MXFP4 element dtype.""" + MXTensor = _torchao_mxtensor_cls() + if MXTensor is None or not isinstance(param, MXTensor): + return False + return param.elem_dtype == torch.float4_e2m1fn_x2 + def get_active_experts(sorted_expert_idxs: torch.Tensor, E: int) -> torch.Tensor: """Get sorted unique expert indices from the routing output. @@ -175,6 +192,42 @@ def _selective_dequant_bnb4( return deq.reshape(num_active, *expert_shape) +def _selective_dequant_mxfp4( + mx_param, + active_experts: torch.Tensor, + out_dtype: torch.dtype = torch.bfloat16, +) -> torch.Tensor: + """Selectively dequantize active experts from a torchao MXFP4 ``MXTensor``. + + Layout assumption: the MXTensor's last axis is the OCP MX block axis. + For ScatterMoE experts this matches the natural storage where + ``experts.gate_up_proj``/``down_proj`` is ``[E, dim1, dim2]`` and + ``dim2`` is the contraction axis post ``.transpose(2, 1)`` performed by + the caller. Indexing ``[active_experts]`` on the qdata and scale yields + a compact MX tensor that we dequantize via torchao. + + Args: + mx_param: ``torchao.prototype.mx_formats.mx_tensor.MXTensor`` of + logical shape ``[E, dim1, dim2]`` with ``elem_dtype=float4_e2m1fn_x2``. + active_experts: ``[num_active]`` sorted unique expert indices. + out_dtype: dtype of the dequantized buffer (default ``bfloat16``). + + Returns: + Dequantized bf16/fp16 tensor of shape ``[num_active, dim1, dim2]``. + """ + if _torchao_mxtensor_cls() is None: + raise ImportError( + "MXFP4 expert dequantization requires torchao>=0.7 " + "(install with `pip install torchao`)." + ) + + sub_qdata = _mx_qdata(mx_param)[active_experts].contiguous() + sub_scale = _mx_scale(mx_param)[active_experts].contiguous() + + sub_mx = _construct_mxtensor_subset(mx_param, sub_qdata, sub_scale) + return sub_mx.dequantize(out_dtype) + + def _selective_index_dense( param: torch.Tensor, active_experts: torch.Tensor, @@ -243,8 +296,14 @@ def selective_expert_weights( return _selective_dequant_bnb4(raw_param, qs, active_experts, expert_shape) - # Dense parameter (bf16/fp32) — direct indexing + # Pull the parameter out before format dispatch — used by every branch below. param = getattr(experts_module, param_name) + + # MXFP4 (torchao MXTensor) — dequantize the subset, return [num_active, d1, d2] + if is_mxfp4_param(param): + return _selective_dequant_mxfp4(param, active_experts) + + # Dense parameter (bf16/fp32) — direct indexing if param.dim() == 3: return param[active_experts] diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index f97bd1a25c..38a82645fc 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -775,6 +775,7 @@ def _patch_llama_derived_model(self): def _apply_llama_flash_attn_patches(self, model): """Apply LLaMA-specific flash attention patches.""" + if ( self.model_config.model_type in ["llama", "llama4", "ernie4_5", "ernie4_5_moe"] @@ -784,15 +785,18 @@ def _apply_llama_flash_attn_patches(self, model): and is_flash_attn_available() and not self.inference ): - # TODO(MengqingCao): split these patches separately - from axolotl.monkeypatch.llama_attn_hijack_flash import ( - is_xformers_swiglu_available, - replace_llama_mlp_with_swiglu, - ) + try: + # TODO(MengqingCao): split these patches separately + from axolotl.monkeypatch.llama_attn_hijack_flash import ( + is_xformers_swiglu_available, + replace_llama_mlp_with_swiglu, + ) - if self.cfg.flash_attn_fuse_mlp and is_xformers_swiglu_available(): - LOG.info("Patching with SwiGLU...") - replace_llama_mlp_with_swiglu(model) + if self.cfg.flash_attn_fuse_mlp and is_xformers_swiglu_available(): + LOG.info("Patching with SwiGLU...") + replace_llama_mlp_with_swiglu(model) + except ImportError as e: + LOG.warning(f"Flash Attention patches not applied: {e}") def _apply_lora_kernel_patch(self, model): """Apply LoRA kernel patches.""" diff --git a/tests/integrations/kernels/__init__.py b/tests/integrations/kernels/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/kernels/scattermoe_lora/__init__.py b/tests/integrations/kernels/scattermoe_lora/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/kernels/scattermoe_lora/bench_mxfp4.py b/tests/integrations/kernels/scattermoe_lora/bench_mxfp4.py new file mode 100644 index 0000000000..ebeaa589dd --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/bench_mxfp4.py @@ -0,0 +1,577 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +ScatterMoE LoRA — MXFP4 forward + backward benchmark. + +Runs three configurations on a representative DeepSeek-V4-style MoE shape +(E=128, K=2048, N=1024, top_k=8, batch×seq=4096) and reports tokens/s, +peak GPU memory, and effective HBM bandwidth for each: + + * **bf16 baseline**: full-precision bf16 experts, no MX. + * **Strategy A**: torchao MXTensor experts, selective dequant to bf16. + * **Strategy B**: torchao MXTensor experts, fused MX dequant in Triton. + +Run from the repo root: + + python tests/integrations/kernels/scattermoe_lora/bench_mxfp4.py + +A markdown table is printed to stdout and written to +``bench_mxfp4_results.md`` next to this script. +""" + +from __future__ import annotations + +import argparse +import math +import subprocess +from pathlib import Path +from typing import Callable + +import torch +from torchao.prototype.mx_formats.mx_tensor import MXTensor + +from axolotl.integrations.kernels.libs.scattermoe_lora.mx_weights import ( + selective_mx_weights_fwd, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_linear_lora import ( + parallel_linear_lora, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.selective_dequant import ( + get_active_experts, + remap_expert_indices, + selective_expert_weights, + selective_lora_weights, +) + +DEVICE = "cuda" +DTYPE = torch.bfloat16 + + +def gpu_name() -> str: + try: + out = subprocess.check_output(["nvidia-smi", "-L"], text=True).strip() + # First GPU line: "GPU 0: NAME (UUID: ...)" + first = out.splitlines()[0] + if ":" in first: + after_colon = first.split(":", 1)[1].strip() + return after_colon.split("(", 1)[0].strip() + return first + except Exception: + return torch.cuda.get_device_name(0) + + +def gpu_hbm_bandwidth_gbps() -> float | None: + """Rough peak HBM BW for utilization %, looked up by name. ``None`` if + unknown — printed as N/A.""" + name = gpu_name().lower() + # Approximate datasheet peaks (GB/s). Order matters — more-specific + # patterns first so a "rtx pro 6000 blackwell" doesn't match + # "rtx 6000 ada". + table = [ + (("rtx", "6000", "blackwell"), 1792.0), + (("rtx", "6000", "ada"), 960.0), + (("rtx", "5090"), 1792.0), + (("rtx", "4090"), 1008.0), + (("h200",), 4800.0), + (("h100",), 3350.0), + (("a100",), 2039.0), + (("a40",), 696.0), + (("a6000",), 768.0), + (("l40",), 864.0), + (("l4",), 300.0), + (("b200",), 8000.0), + (("mi300x",), 5300.0), + ] + for keys, bw in table: + if all(k in name for k in keys): + return bw + return None + + +@torch.no_grad() +def _setup_bf16(E, K, N, top_k, M, rank): + torch.manual_seed(0) + W = torch.randn(E, N, K, device=DEVICE, dtype=DTYPE) * (1.0 / K**0.5) + W_kernel = W.transpose(2, 1).contiguous() # [E, K, N] + return W, W_kernel + + +def _setup_mx(W_natural, chunk: int = 8): + """Make a torchao MXFP4 ``MXTensor`` from the bf16 weight. + + ``MXTensor.to_mx`` materializes an fp32 working tensor internally; for + large [E, N, K] weights this transient can spike setup-time GPU memory + well beyond the final quantized footprint. To keep the bench runnable + on a shared GPU, quantize ``chunk`` experts at a time and stitch the + qdata/scale shards into a single MXTensor. + """ + if W_natural.shape[0] <= chunk: + return MXTensor.to_mx( + W_natural, elem_dtype=torch.float4_e2m1fn_x2, block_size=32 + ) + qdata_parts = [] + scale_parts = [] + template = None + for i in range(0, W_natural.shape[0], chunk): + piece = W_natural[i : i + chunk].contiguous() + mx_chunk = MXTensor.to_mx( + piece, elem_dtype=torch.float4_e2m1fn_x2, block_size=32 + ) + qdata_parts.append(mx_chunk.qdata) + scale_parts.append(mx_chunk.scale) + if template is None: + template = mx_chunk + qdata = torch.cat(qdata_parts, dim=0) + scale = torch.cat(scale_parts, dim=0) + assert template is not None # set on the first loop iter (loop body runs ≥ once) + return MXTensor( + qdata, + scale, + template.elem_dtype, + template.block_size, + template.orig_dtype, + template.kernel_preference, + template.act_quant_kwargs, + template.is_swizzled_scales, + ) + + +def _routing(M, E, top_k, seed=1, mode="dense"): + """Generate token→expert routing. + + ``mode="dense"`` uses per-token random logits; for moderate E and top_k + this leaves nearly every expert active and exercises the kernels' full-load + case. ``mode="sparse"`` injects a strong shared bias so the same handful of + experts dominates the topk across all tokens — modelling realistic MoE + routing where only a small fraction of experts is active per step. + ``mode="balanced"`` models a load-balance-regularized router (aux-loss / + z-loss trained): per-token logits = N(0, 1) noise + small per-expert + bias N(0, 0.5). At large M this yields approximately balanced expert + usage; at small M only a fraction of experts gets hit and which experts + are active varies with seed/M — i.e. the seqlen → active-expert-count + curve that drives the A-vs-B crossover. + """ + torch.manual_seed(seed) + if mode == "dense": + logits = torch.randn(M, E, device=DEVICE) + elif mode == "sparse": + shared = torch.randn(E, device=DEVICE) * 5.0 + noise = torch.randn(M, E, device=DEVICE) * 0.1 + logits = shared.unsqueeze(0) + noise + elif mode == "balanced": + bias = torch.randn(E, device=DEVICE) * 0.5 + noise = torch.randn(M, E, device=DEVICE) + logits = noise + bias.unsqueeze(0) + else: + raise ValueError(f"unknown routing mode: {mode}") + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), top_k, dim=-1) + sei, ssi, eo = flatten_sort_count(top_idx, E) + return sei, ssi, eo, top_idx + + +def _lora(E, K, N, rank, seed=2): + torch.manual_seed(seed) + A = torch.randn(rank * E, K, device=DEVICE, dtype=DTYPE) * 0.01 + B = torch.randn(N, rank * E, device=DEVICE, dtype=DTYPE) * 0.01 + return A, B + + +class _MockExperts: + def __init__(self, p): + self.gate_up_proj = p + + +# --------------------------------------------------------------------------- +# Three benchmark runners — each takes a fresh `x` and returns (output, fn_grad) +# --------------------------------------------------------------------------- + + +# Runners reuse the same leaf tensors across timed iters (x, lora A/B) and +# zero ``.grad`` to None at the top of each call. The previous per-iter +# ``.clone()`` + ``requires_grad_(True)`` was setup cost — not kernel cost — +# and biased the timing especially on small shapes. Gradient accumulation +# is avoided by setting ``.grad = None`` (faster than ``.zero_()``), so the +# autograd graph each iter is fresh but the leaf buffers are not reallocated. + + +def make_runner_bf16(W_kernel, lora_A, lora_B, sei, ssi, eo, top_k, scaling): + A = lora_A.detach().clone().requires_grad_(True) + B = lora_B.detach().clone().requires_grad_(True) + + def run(x): + x.grad = None + A.grad = None + B.grad = None + out = parallel_linear_lora( + x, + W_kernel, + top_k, + sei, + ssi, + eo, + lora_A=A, + lora_B=B, + scaling=scaling, + use_fused_dX=True, + use_fused_gather=True, + ) + out.sum().backward() + return out + + return run + + +def make_runner_strategy_a(mx, lora_A, lora_B, sei, ssi, eo, top_k, scaling, E): + experts = _MockExperts(mx) + A = lora_A.detach().clone().requires_grad_(True) + B = lora_B.detach().clone().requires_grad_(True) + + def run(x): + x.grad = None + A.grad = None + B.grad = None + active = get_active_experts(sei, E) + remapped, compact_off = remap_expert_indices(sei, eo, active, E) + W_compact = ( + selective_expert_weights(experts, "gate_up_proj", active) + .transpose(2, 1) + .contiguous() + ) + A_c, B_c = selective_lora_weights(A, B, active, E) + out = parallel_linear_lora( + x, + W_compact, + top_k, + remapped, + ssi, + compact_off, + lora_A=A_c, + lora_B=B_c, + scaling=scaling, + use_fused_dX=True, + use_fused_gather=True, + ) + out.sum().backward() + return out + + return run + + +def make_runner_strategy_b(mx, lora_A, lora_B, sei, ssi, eo, top_k, scaling, E): + A = lora_A.detach().clone().requires_grad_(True) + B = lora_B.detach().clone().requires_grad_(True) + + def run(x): + x.grad = None + A.grad = None + B.grad = None + active = get_active_experts(sei, E) + remapped, compact_off = remap_expert_indices(sei, eo, active, E) + mx_active = selective_mx_weights_fwd(mx, active) + A_c, B_c = selective_lora_weights(A, B, active, E) + out = parallel_linear_lora( + x, + mx_active, + top_k, + remapped, + ssi, + compact_off, + lora_A=A_c, + lora_B=B_c, + scaling=scaling, + ) + out.sum().backward() + return out + + return run + + +# --------------------------------------------------------------------------- +# Timing harness +# --------------------------------------------------------------------------- + + +def bench(fn: Callable, x_template: torch.Tensor, warmup: int, iters: int) -> dict: + torch.cuda.empty_cache() + torch.cuda.reset_peak_memory_stats() + # Allocate the input leaf tensor once outside the timed window. Runners + # reset ``.grad = None`` per iter; the underlying buffer is reused. + x = x_template.detach().clone().requires_grad_(True) + try: + # Warmup + for _ in range(warmup): + fn(x) + torch.cuda.synchronize() + + start = torch.cuda.Event(enable_timing=True) + end = torch.cuda.Event(enable_timing=True) + start.record() + for _ in range(iters): + fn(x) + end.record() + torch.cuda.synchronize() + except torch.cuda.OutOfMemoryError: + torch.cuda.empty_cache() + return {"ms_per_iter": float("nan"), "peak_mem_mb": float("nan"), "oom": True} + elapsed_ms = start.elapsed_time(end) + peak_mem_mb = torch.cuda.max_memory_allocated() / 1024 / 1024 + return { + "ms_per_iter": elapsed_ms / iters, + "peak_mem_mb": peak_mem_mb, + "oom": False, + } + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--E", type=int, default=128) + parser.add_argument("--K", type=int, default=2048) + parser.add_argument("--N", type=int, default=1024) + parser.add_argument("--top_k", type=int, default=8) + parser.add_argument("--M", type=int, default=4096, help="batch * seq") + parser.add_argument("--rank", type=int, default=16) + parser.add_argument("--warmup", type=int, default=10) + parser.add_argument("--iters", type=int, default=50) + parser.add_argument( + "--routing-mode", + choices=("dense", "sparse", "balanced"), + default="dense", + help=( + "dense: per-token random logits (~all experts active). " + "sparse: shared bias + small per-token noise so the same ~top_k " + "experts dominate routing across all tokens. " + "balanced: per-token N(0,1) noise + small N(0,0.5) per-expert bias " + "— mimics a load-balance-regularized router; active-expert count " + "grows with M." + ), + ) + parser.add_argument( + "--M-sweep", + dest="M_sweep", + default=None, + help=( + "Comma-separated list of M values, e.g. '256,1024,4096,16384'. " + "When set, --M is ignored; the bench runs once per M with the " + "selected routing mode and emits a single combined section." + ), + ) + parser.add_argument( + "--append", + action="store_true", + help="Append the new table to bench_mxfp4_results.md instead of overwriting it.", + ) + parser.add_argument( + "--device", + default="cuda", + help="CUDA device, e.g. 'cuda', 'cuda:0', 'cuda:1'.", + ) + args = parser.parse_args() + + global DEVICE + DEVICE = args.device + torch.cuda.set_device(torch.device(DEVICE)) + + E, K, N, top_k, rank = args.E, args.K, args.N, args.top_k, args.rank + + if args.M_sweep: + M_values = [int(s.strip()) for s in args.M_sweep.split(",") if s.strip()] + else: + M_values = [args.M] + + print(f"GPU: {gpu_name()}") + print( + f"Shape: E={E}, K={K}, N={N}, top_k={top_k}, rank={rank}, " + f"M={M_values if args.M_sweep else M_values[0]}" + ) + print(f"Iters: {args.warmup} warmup + {args.iters} timed") + print() + + # Build dense bf16 weights + MX-quantize once — weights are independent of M. + W_natural, W_kernel = _setup_bf16(E, K, N, top_k, M_values[0], rank) + mx = _setup_mx(W_natural) + # W_natural is only used to build the MX tensor; W_kernel feeds bf16 paths. + # Free it eagerly so the dequant transient fits on memory-constrained GPUs. + del W_natural + torch.cuda.empty_cache() + lora_A, lora_B = _lora(E, K, N, rank) + scaling = 0.5 + peak_bw = gpu_hbm_bandwidth_gbps() + + per_M = [] # list of (M, e_active, results) + for M in M_values: + sei, ssi, eo, _top_idx = _routing(M, E, top_k, mode=args.routing_mode) + x = torch.randn(M, K, device=DEVICE, dtype=DTYPE) + + # Estimate bytes read per iter for HBM BW utilization: + # bf16 W: E_active * K * N * 2 + # MX W: E_active * (K*N/2 + K*N/32) + # X is M*K*2 bytes. We ignore LoRA traffic (tiny relative to W). + num_tokens = M * top_k + e_active = int(get_active_experts(sei, E).numel()) + bytes_bf16 = e_active * K * N * 2 + M * K * 2 + bytes_mx = e_active * (K * N // 2 + K * N // 32) + M * K * 2 + + runners = { + "bf16 baseline": ( + make_runner_bf16( + W_kernel, lora_A, lora_B, sei, ssi, eo, top_k, scaling + ), + bytes_bf16, + ), + "Strategy A (selective dequant)": ( + make_runner_strategy_a( + mx, lora_A, lora_B, sei, ssi, eo, top_k, scaling, E + ), + bytes_bf16, # post-dequant the kernel still reads bf16 + ), + "Strategy B (fused MX)": ( + make_runner_strategy_b( + mx, lora_A, lora_B, sei, ssi, eo, top_k, scaling, E + ), + bytes_mx, + ), + } + + results = [] + for name, (fn, bytes_per_iter) in runners.items(): + r = bench(fn, x, args.warmup, args.iters) + if r.get("oom"): + tps = float("nan") + bw = float("nan") + bw_pct = float("nan") + else: + tps = num_tokens / (r["ms_per_iter"] / 1000.0) + bw = (bytes_per_iter / 1e9) / (r["ms_per_iter"] / 1000.0) + bw_pct = (bw / peak_bw * 100.0) if peak_bw else float("nan") + results.append( + dict( + name=name, + ms_per_iter=r["ms_per_iter"], + tokens_per_s=tps, + peak_mem_mb=r["peak_mem_mb"], + hbm_gbps=bw, + hbm_pct=bw_pct, + oom=r.get("oom", False), + ) + ) + per_M.append((M, e_active, results)) + + section_lines = [] + if args.M_sweep: + section_lines.append( + f"## Routing mode: {args.routing_mode} — M sweep — {gpu_name()}" + ) + section_lines.append("") + section_lines.append(f"- **GPU**: {gpu_name()}") + section_lines.append( + f"- **Base shape**: E={E}, K={K}, N={N}, top_k={top_k}, rank={rank}" + ) + section_lines.append(f"- **M values**: {', '.join(str(m) for m in M_values)}") + section_lines.append( + f"- **Iters**: {args.warmup} warmup + {args.iters} timed, fwd+bwd per iter" + ) + if peak_bw: + section_lines.append(f"- **HBM peak (datasheet)**: {peak_bw:.0f} GB/s") + section_lines.append("") + section_lines.append("### Summary (ms/iter, fwd+bwd)") + section_lines.append("") + section_lines.append( + "| M | active / E | bf16 ms | Strategy A ms | Strategy B ms | winner (A vs B) |" + ) + section_lines.append("| ---: | ---: | ---: | ---: | ---: | :---: |") + + def _fmt_ms(r): + return "OOM" if r["oom"] else f"{r['ms_per_iter']:.2f}" + + for M, e_active, results in per_M: + by_name = {r["name"]: r for r in results} + a, b, bf = ( + by_name["Strategy A (selective dequant)"], + by_name["Strategy B (fused MX)"], + by_name["bf16 baseline"], + ) + if a["oom"] and b["oom"]: + winner = "—" + elif a["oom"]: + winner = "B" + elif b["oom"]: + winner = "A" + else: + winner = "A" if a["ms_per_iter"] < b["ms_per_iter"] else "B" + section_lines.append( + f"| {M} | {e_active}/{E} ({e_active / E:.2f}) | " + f"{_fmt_ms(bf)} | {_fmt_ms(a)} | {_fmt_ms(b)} | {winner} |" + ) + section_lines.append("") + for M, e_active, results in per_M: + section_lines.append( + f"### M={M} (active experts = {e_active} / {E}, " + f"num_active/E = {e_active / E:.3f})" + ) + section_lines.append("") + section_lines.append( + "| Config | ms/iter | tokens/s | peak mem (MB) | HBM GB/s | HBM % |" + ) + section_lines.append("| --- | ---: | ---: | ---: | ---: | ---: |") + for r in results: + if r["oom"]: + section_lines.append( + f"| {r['name']} | OOM | OOM | OOM | OOM | OOM |" + ) + continue + hbm_pct = ( + f"{r['hbm_pct']:.1f}" if not math.isnan(r["hbm_pct"]) else "N/A" + ) + section_lines.append( + f"| {r['name']} | {r['ms_per_iter']:.2f} | " + f"{r['tokens_per_s']:.0f} | {r['peak_mem_mb']:.1f} | " + f"{r['hbm_gbps']:.1f} | {hbm_pct} |" + ) + section_lines.append("") + else: + M, e_active, results = per_M[0] + section_lines.append(f"## Routing mode: {args.routing_mode} — {gpu_name()}") + section_lines.append("") + section_lines.append(f"- **GPU**: {gpu_name()}") + section_lines.append( + f"- **Shape**: E={E}, K={K}, N={N}, top_k={top_k}, M={M}, rank={rank} " + f"(active experts = {e_active})" + ) + section_lines.append( + f"- **Iters**: {args.warmup} warmup + {args.iters} timed, fwd+bwd per iter" + ) + if peak_bw: + section_lines.append(f"- **HBM peak (datasheet)**: {peak_bw:.0f} GB/s") + section_lines.append("") + section_lines.append( + "| Config | ms/iter | tokens/s | peak mem (MB) | HBM GB/s | HBM % |" + ) + section_lines.append("| --- | ---: | ---: | ---: | ---: | ---: |") + for r in results: + hbm_pct = f"{r['hbm_pct']:.1f}" if not math.isnan(r["hbm_pct"]) else "N/A" + section_lines.append( + f"| {r['name']} | {r['ms_per_iter']:.2f} | {r['tokens_per_s']:.0f} | " + f"{r['peak_mem_mb']:.1f} | {r['hbm_gbps']:.1f} | {hbm_pct} |" + ) + section_md = "\n".join(section_lines).rstrip() + "\n" + + out_path = Path(__file__).resolve().parent / "bench_mxfp4_results.md" + if args.append and out_path.exists(): + existing = out_path.read_text().rstrip() + "\n\n" + md = existing + section_md + else: + md = "# ScatterMoE LoRA — MXFP4 benchmark\n\n" + section_md + + print(section_md) + out_path.write_text(md) + print(f"\nResults written to {out_path}") + + +if __name__ == "__main__": + main() diff --git a/tests/integrations/kernels/scattermoe_lora/bench_mxfp4_results.md b/tests/integrations/kernels/scattermoe_lora/bench_mxfp4_results.md new file mode 100644 index 0000000000..4df526f6fb --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/bench_mxfp4_results.md @@ -0,0 +1,106 @@ +# ScatterMoE LoRA — MXFP4 benchmark + +## Routing mode: dense — NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition + +- **GPU**: NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition +- **Shape**: E=128, K=2048, N=1024, top_k=8, M=4096, rank=16 (active experts = 128) +- **Iters**: 10 warmup + 50 timed, fwd+bwd per iter +- **HBM peak (datasheet)**: 1792 GB/s + +| Config | ms/iter | tokens/s | peak mem (MB) | HBM GB/s | HBM % | +| --- | ---: | ---: | ---: | ---: | ---: | +| bf16 baseline | 5.25 | 6244998 | 1252.8 | 105.5 | 5.9 | +| Strategy A (selective dequant) | 30.57 | 1071778 | 8557.3 | 18.1 | 1.0 | +| Strategy B (fused MX) | 12.24 | 2677582 | 1425.3 | 13.0 | 0.7 | + +## Routing mode: sparse — NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition + +- **GPU**: NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition +- **Shape**: E=256, K=2048, N=1024, top_k=8, M=4096, rank=16 (active experts = 10) +- **Iters**: 10 warmup + 50 timed, fwd+bwd per iter +- **HBM peak (datasheet)**: 1792 GB/s + +| Config | ms/iter | tokens/s | peak mem (MB) | HBM GB/s | HBM % | +| --- | ---: | ---: | ---: | ---: | ---: | +| bf16 baseline | 6.55 | 5006027 | 1960.8 | 9.0 | 0.5 | +| Strategy A (selective dequant) | 5.75 | 5695789 | 2059.9 | 10.2 | 0.6 | +| Strategy B (fused MX) | 8.95 | 3661270 | 1997.8 | 3.1 | 0.2 | + +## Routing mode: balanced — M sweep — NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition + +- **GPU**: NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition +- **Base shape**: E=256, K=2048, N=1024, top_k=8, rank=16 +- **M values**: 256, 1024, 4096, 16384 +- **Iters**: 10 warmup + 50 timed, fwd+bwd per iter +- **HBM peak (datasheet)**: 1792 GB/s + +### Summary (ms/iter, fwd+bwd) + +| M | active / E | bf16 ms | Strategy A ms | Strategy B ms | winner (A vs B) | +| ---: | ---: | ---: | ---: | ---: | :---: | +| 256 | 215/256 (0.84) | 2.99 | OOM | 8.24 | B | +| 1024 | 251/256 (0.98) | 3.43 | OOM | 10.74 | B | +| 4096 | 255/256 (1.00) | 6.56 | OOM | 16.50 | B | +| 16384 | 256/256 (1.00) | 24.15 | OOM | 46.56 | B | + +### M=256 (active experts = 215 / 256, num_active/E = 0.840) + +| Config | ms/iter | tokens/s | peak mem (MB) | HBM GB/s | HBM % | +| --- | ---: | ---: | ---: | ---: | ---: | +| bf16 baseline | 2.99 | 685596 | 1686.0 | 302.2 | 16.9 | +| Strategy A (selective dequant) | OOM | OOM | OOM | OOM | OOM | +| Strategy B (fused MX) | 8.24 | 248639 | 1954.9 | 29.2 | 1.6 | + +### M=1024 (active experts = 251 / 256, num_active/E = 0.980) + +| Config | ms/iter | tokens/s | peak mem (MB) | HBM GB/s | HBM % | +| --- | ---: | ---: | ---: | ---: | ---: | +| bf16 baseline | 3.43 | 2389143 | 1744.2 | 308.3 | 17.2 | +| Strategy A (selective dequant) | OOM | OOM | OOM | OOM | OOM | +| Strategy B (fused MX) | 10.74 | 762567 | 2058.1 | 26.4 | 1.5 | + +### M=4096 (active experts = 255 / 256, num_active/E = 0.996) + +| Config | ms/iter | tokens/s | peak mem (MB) | HBM GB/s | HBM % | +| --- | ---: | ---: | ---: | ---: | ---: | +| bf16 baseline | 6.56 | 4994760 | 1960.8 | 165.6 | 9.2 | +| Strategy A (selective dequant) | OOM | OOM | OOM | OOM | OOM | +| Strategy B (fused MX) | 16.50 | 1985884 | 2280.0 | 18.2 | 1.0 | + +### M=16384 (active experts = 256 / 256, num_active/E = 1.000) + +| Config | ms/iter | tokens/s | peak mem (MB) | HBM GB/s | HBM % | +| --- | ---: | ---: | ---: | ---: | ---: | +| bf16 baseline | 24.15 | 5427073 | 2827.0 | 47.2 | 2.6 | +| Strategy A (selective dequant) | OOM | OOM | OOM | OOM | OOM | +| Strategy B (fused MX) | 46.56 | 2814943 | 3149.0 | 7.6 | 0.4 | + +### Notes + +- **Strategy A OOMs at all M** under load-balanced routing at E=256 because + the torchao MXTensor dequant path materializes several full-shape fp32/int32 + unpack buffers (~12 GiB combined for [256, 1024, 2048] at fp4 → fp32) while + vLLM colocated on this workstation pins ~88 GB of HBM, leaving only ~14 GB + free. Extrapolating from the dense E=128 case above (Strategy A peak + ~8.6 GB at 128 active experts), the E=256 / 256-active dequant peak would + be ~17 GB — over the available headroom. +- **Active-expert count is essentially E at every sampled M.** Under a + load-balance-regularized router (per-token N(0,1) noise + N(0,0.5) per-expert + bias), `E[active] ≈ E · (1 − (1 − top_k/E)^M)`. With E=256 / top_k=8 this + yields ≥ 215 unique experts even at M=256 and saturates at 256 by M ≈ 16K. + Balanced routing therefore does **not** generate a low-active regime at + these token counts — i.e. the A-vs-B crossover does not appear in this + sweep; B wins by default because A does not fit. +- **B vs bf16:** Strategy B is consistently 1.9–2.9× slower than the bf16 + baseline (similar to the dense E=128 ratio of ~2.3×). HBM utilization for + both is modest (B 0.4–1.6 %, bf16 2.6–17.2 %), suggesting the kernels are + compute- or scheduling-bound for these shapes, not bandwidth-bound. +- **Where the A-vs-B crossover lives, by theory:** Strategy A is preferred + when `num_active / E` is small enough that the dequant cost is offset by + the cheaper bf16 matmul — the prior `sparse` row (10/256 active, A=5.75 ms + vs B=8.95 ms) sits in that regime. Strategy B is preferred near + `num_active / E ≈ 1`, where dequant of all experts dominates. The threshold + between the two — somewhere in the 10/256 to 215/256 band — is **not + observable from the balanced-router setting**; eliciting it would need an + M smaller than 256, a synthetic deliberately-sparse router, or freeing the + vLLM GPU and rerunning at E=256. diff --git a/tests/integrations/kernels/scattermoe_lora/test_mxfp4_expert_weights.py b/tests/integrations/kernels/scattermoe_lora/test_mxfp4_expert_weights.py new file mode 100644 index 0000000000..0f147beef3 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_mxfp4_expert_weights.py @@ -0,0 +1,567 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +Correctness tests for MXFP4 expert weight support in ScatterMoE LoRA. + +Validates both strategies against a bf16-dequantized reference: + + Strategy A — selective dequant: + The kernel runs on the dequantized [num_active, K, N] bf16 buffer, + so outputs must be bitwise identical to the baseline that supplies + the same bf16 weights directly. + + Strategy B — fused Triton (when enabled): + The kernel unpacks MXFP4 + applies E8M0 scales in its K-loop. Output + differs from the bf16 reference by MX-rounding-tolerance only. + +Shapes covered: + - small: [E=8, K=128, N=256], M=16, top_k=2, rank=8 + - representative: [E=32, K=2048, N=1024], M=64, top_k=4, rank=16 +""" + +import pytest +import torch + +from axolotl.integrations.kernels.libs.scattermoe_lora.mx_weights import ( + selective_mx_weights_fwd, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_linear_lora import ( + parallel_linear_lora, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.selective_dequant import ( + get_active_experts, + is_mxfp4_param, + remap_expert_indices, + selective_expert_weights, + selective_lora_weights, +) + +torchao = pytest.importorskip("torchao") +from torchao.prototype.mx_formats.mx_tensor import MXTensor # noqa: E402 + +DEVICE = "cuda" +DTYPE = torch.bfloat16 + +pytestmark = pytest.mark.skipif( + not torch.cuda.is_available(), reason="CUDA required for MX kernels" +) + + +SHAPES = [ + # (E, K, N, M, top_k, R, seed) + pytest.param(8, 128, 256, 16, 2, 8, 0, id="small"), + pytest.param(32, 2048, 1024, 64, 4, 16, 1, id="representative"), +] + +# Per-shape Strategy-B tolerances. Forward outputs accumulate K dot-products +# in fp32 then cast to bf16, so they stay within a few ULPs of the bf16 +# baseline. The dX path reduces over N (which is typically larger than K and +# uses a different MMA tile layout than the bf16 reference), so we apply a +# looser ULP-aware tolerance there. These are still tight compared to +# torchao's own bf16 vs fp32 GEMM noise. +_STRATEGY_B_FWD_TOL = { + "small": dict(atol=2e-3, rtol=2e-3), + "representative": dict(atol=1e-2, rtol=5e-3), +} +# dX tolerance: ~1 bf16 ULP at the typical output magnitude (rtol dominates; +# atol caps near-zero entries where MMA-reordering manifests as full ULP). +_STRATEGY_B_DX_TOL = { + "small": dict(atol=0.5, rtol=2e-2), + "representative": dict(atol=2.0, rtol=3e-2), +} +# dA / dB tolerance: the fused dA/dB kernel accumulates via atomic_add from +# multiple N-block programs per expert, and the number of in-flight programs +# differs between the full-E baseline and the compact-active MX path — +# atomic ordering then introduces bf16 ULP-scale noise. Looser than the +# forward bound because the gradients integrate over both M and N. +_STRATEGY_B_LORA_GRAD_TOL = { + "small": dict(atol=2e-2, rtol=2e-2), + "representative": dict(atol=2e-1, rtol=3e-2), +} + + +def _tol_for_shape(K, *, dx: bool = False, lora_grad: bool = False): + if lora_grad: + table = _STRATEGY_B_LORA_GRAD_TOL + elif dx: + table = _STRATEGY_B_DX_TOL + else: + table = _STRATEGY_B_FWD_TOL + return table["small"] if K <= 128 else table["representative"] + + +def _make_mxfp4_weights(E, K, N, seed): + """Build a `[E, N, K]` MXFP4 ``MXTensor`` (block axis = K, the contraction + axis). Returns (mx, W_ref_bf16) — the bf16 reference is the dequantization + of the full MX tensor so Strategy A can hit bitwise equality.""" + torch.manual_seed(seed) + # Natural axolotl storage is [E, N, K] where K is the contraction axis; + # `experts.gate_up_proj.transpose(2, 1)` then yields [E, K, N] for the kernel. + W_dense = torch.randn(E, N, K, device=DEVICE, dtype=DTYPE) + mx = MXTensor.to_mx(W_dense, elem_dtype=torch.float4_e2m1fn_x2, block_size=32) + W_ref = mx.dequantize(DTYPE).contiguous() + return mx, W_ref + + +def _setup_routing_and_lora(E, K, N, M, top_k, R, seed): + torch.manual_seed(seed + 100) + x = torch.randn(M, K, device=DEVICE, dtype=DTYPE) + lora_A = torch.randn(R * E, K, device=DEVICE, dtype=DTYPE) * 0.01 + lora_B = torch.randn(N, R * E, device=DEVICE, dtype=DTYPE) * 0.01 + logits = torch.randn(M, E, device=DEVICE) + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), top_k, dim=-1) + sei, ssi, eo = flatten_sort_count(top_idx, E) + return x, lora_A, lora_B, sei, ssi, eo + + +class _MockExperts: + """Bare object exposing ``gate_up_proj`` so `selective_expert_weights` + can branch on it.""" + + def __init__(self, mx_param, num_experts): + self.gate_up_proj = mx_param + self.num_experts = num_experts + + +def _run_baseline( + W_ref, + x, + lora_A, + lora_B, + scaling, + sei, + ssi, + eo, + top_k, + *, + use_fused_dX: bool = False, + use_fused_gather: bool = False, +): + """Full-E bf16 baseline: dense weights, full LoRA, full expert indices.""" + W_kernel = W_ref.transpose(2, 1).contiguous() # [E, K, N] + return parallel_linear_lora( + x, + W_kernel, + top_k, + sei, + ssi, + eo, + lora_A=lora_A, + lora_B=lora_B, + scaling=scaling, + use_fused_dX=use_fused_dX, + use_fused_gather=use_fused_gather, + ) + + +def _run_strategy_a(mx, x, lora_A, lora_B, scaling, sei, ssi, eo, top_k, E): + """Strategy A: selective dequant via MXTensor branch in + `selective_expert_weights`. Compact weights + remapped indices.""" + experts = _MockExperts(mx, E) + active = get_active_experts(sei, E) + remapped, compact_offsets = remap_expert_indices(sei, eo, active, E) + W_compact = ( + selective_expert_weights(experts, "gate_up_proj", active) + .transpose(2, 1) + .contiguous() + ) # [num_active, K, N] + A_compact, B_compact = selective_lora_weights(lora_A, lora_B, active, E) + return parallel_linear_lora( + x, + W_compact, + top_k, + remapped, + ssi, + compact_offsets, + lora_A=A_compact, + lora_B=B_compact, + scaling=scaling, + ), active + + +# ─── Strategy A — bitwise identity vs bf16 baseline ─────────────────────────── + + +@pytest.mark.parametrize("E,K,N,M,top_k,R,seed", SHAPES) +def test_strategy_a_forward_matches_bf16(E, K, N, M, top_k, R, seed): + mx, W_ref = _make_mxfp4_weights(E, K, N, seed) + assert is_mxfp4_param(mx) + x, lora_A, lora_B, sei, ssi, eo = _setup_routing_and_lora( + E, K, N, M, top_k, R, seed + ) + scaling = 0.5 + + out_baseline = _run_baseline(W_ref, x, lora_A, lora_B, scaling, sei, ssi, eo, top_k) + out_a, _ = _run_strategy_a(mx, x, lora_A, lora_B, scaling, sei, ssi, eo, top_k, E) + + assert out_baseline.shape == out_a.shape + assert torch.equal(out_baseline, out_a), ( + f"Strategy A forward must match bf16 baseline bitwise. " + f"max abs diff = {(out_baseline - out_a).abs().max().item()}" + ) + + +@pytest.mark.parametrize("E,K,N,M,top_k,R,seed", SHAPES) +def test_strategy_a_backward_matches_bf16(E, K, N, M, top_k, R, seed): + """Forward + backward parity. dX must be bitwise identical; the LoRA + grads dA/dB are compared on the active expert slices only (the full + LoRA tensors differ in shape between baseline and compact paths).""" + mx, W_ref = _make_mxfp4_weights(E, K, N, seed) + x_base, lora_A_base, lora_B_base, sei, ssi, eo = _setup_routing_and_lora( + E, K, N, M, top_k, R, seed + ) + scaling = 0.5 + + # Baseline backward + x_b = x_base.detach().clone().requires_grad_(True) + A_b = lora_A_base.detach().clone().requires_grad_(True) + B_b = lora_B_base.detach().clone().requires_grad_(True) + out_b = _run_baseline(W_ref, x_b, A_b, B_b, scaling, sei, ssi, eo, top_k) + grad_out = torch.randn_like(out_b) + out_b.backward(grad_out) + + # Strategy A backward + x_a = x_base.detach().clone().requires_grad_(True) + experts = _MockExperts(mx, E) + active = get_active_experts(sei, E) + remapped, compact_offsets = remap_expert_indices(sei, eo, active, E) + W_compact = ( + selective_expert_weights(experts, "gate_up_proj", active) + .transpose(2, 1) + .contiguous() + ) + A_full = lora_A_base.detach().clone().requires_grad_(True) + B_full = lora_B_base.detach().clone().requires_grad_(True) + A_compact, B_compact = selective_lora_weights(A_full, B_full, active, E) + out_a = parallel_linear_lora( + x_a, + W_compact, + top_k, + remapped, + ssi, + compact_offsets, + lora_A=A_compact, + lora_B=B_compact, + scaling=scaling, + ) + out_a.backward(grad_out) + + # Forward parity (Strategy A contract: bitwise identical to bf16 baseline). + # Asserted here so a forward bug that produces a constant offset (and + # therefore zero gradient delta) doesn't slip past the bwd-only checks. + assert torch.equal(out_b, out_a), ( + f"forward mismatch (Strategy A): max abs diff = " + f"{(out_b - out_a).abs().max().item()}" + ) + + # dX: bitwise identical + assert torch.equal(x_b.grad, x_a.grad), ( + f"dX mismatch (Strategy A): max abs diff = " + f"{(x_b.grad - x_a.grad).abs().max().item()}" + ) + + # dA / dB — gather active slices from the baseline full grads and compare + row_idx = ( + active.long()[:, None] * R + torch.arange(R, device=DEVICE)[None, :] + ).reshape(-1) + dA_b_active = A_b.grad[row_idx] + dB_b_active = B_b.grad[:, row_idx] + + # A_compact is a view (advanced indexing produces a copy, so the grad lands + # on the full lora_A via the slice). torch.autograd flows back through + # selective_lora_weights, so A_full.grad has gradient on rows for active + # experts only. + dA_a_active = A_full.grad[row_idx] + dB_a_active = B_full.grad[:, row_idx] + + assert torch.equal(dA_b_active, dA_a_active), ( + f"dA active slice mismatch: max diff = " + f"{(dA_b_active - dA_a_active).abs().max().item()}" + ) + assert torch.equal(dB_b_active, dB_a_active), ( + f"dB active slice mismatch: max diff = " + f"{(dB_b_active - dB_a_active).abs().max().item()}" + ) + + +# ─── Strategy A — backward through fused dX/gather paths ────────────────────── + + +@pytest.mark.parametrize("use_fused_dX", [False, True]) +@pytest.mark.parametrize("use_fused_gather", [False, True]) +def test_strategy_a_backward_fused_variants(use_fused_dX, use_fused_gather): + """Strategy A must match baseline across all four fused-bwd flag + combinations exercised in production by ``HFScatterMoEGatedMLP``. + Asserts parity on dX, dA, and dB (active expert slice for dA/dB).""" + E, K, N, M, top_k, R = 8, 128, 256, 16, 2, 8 + mx, W_ref = _make_mxfp4_weights(E, K, N, seed=7) + x_base, lora_A_base, lora_B_base, sei, ssi, eo = _setup_routing_and_lora( + E, K, N, M, top_k, R, seed=7 + ) + scaling = 0.25 + + def run(W_kernel, sei_, eo_, k_, lora_A_in, lora_B_in, grad_out): + x_g = x_base.detach().clone().requires_grad_(True) + A_g = lora_A_in.detach().clone().requires_grad_(True) + B_g = lora_B_in.detach().clone().requires_grad_(True) + out = parallel_linear_lora( + x_g, + W_kernel, + k_, + sei_, + ssi, + eo_, + lora_A=A_g, + lora_B=B_g, + scaling=scaling, + use_fused_dX=use_fused_dX, + use_fused_gather=use_fused_gather, + ) + out.backward(grad_out) + return out, x_g.grad, A_g.grad, B_g.grad + + # Non-trivial grad (a constant grad zeros out cross-token differences in + # the fused-gather accumulation, which can mask reordering bugs). + torch.manual_seed(7) + W_baseline = W_ref.transpose(2, 1).contiguous() + # Forward once on baseline shape to size grad_out. + out_shape_probe = parallel_linear_lora( + x_base, + W_baseline, + top_k, + sei, + ssi, + eo, + lora_A=lora_A_base, + lora_B=lora_B_base, + scaling=scaling, + ) + grad_out = torch.randn_like(out_shape_probe) * 0.1 + + out_b, dx_b, dA_b, dB_b = run( + W_baseline, + sei, + eo, + top_k, + lora_A_base, + lora_B_base, + grad_out, + ) + + experts = _MockExperts(mx, E) + active = get_active_experts(sei, E) + remapped, compact_offsets = remap_expert_indices(sei, eo, active, E) + W_compact = ( + selective_expert_weights(experts, "gate_up_proj", active) + .transpose(2, 1) + .contiguous() + ) + A_compact, B_compact = selective_lora_weights(lora_A_base, lora_B_base, active, E) + out_a, dx_a, dA_a, dB_a = run( + W_compact, + remapped, + compact_offsets, + top_k, + A_compact, + B_compact, + grad_out, + ) + + # Strategy A is bitwise on forward / dX (same bf16 weights, same kernel). + assert torch.equal(out_b, out_a), ( + f"forward mismatch: max diff = {(out_b - out_a).abs().max().item()}" + ) + assert torch.equal(dx_b, dx_a), ( + f"dX mismatch: max diff = {(dx_b - dx_a).abs().max().item()}" + ) + + # dA / dB — compare active-expert slice of the dense baseline grads + # against the compact-path grads. Same row_idx pattern as + # test_strategy_a_backward_matches_bf16. Bitwise (``torch.equal``) holds + # for forward and dX, but the fused dA/dB kernel uses ``atomic_add`` + # across N-block programs and the in-flight program count differs + # between the full-E baseline and the compact-active path; combined + # with FMA reordering, this introduces 1–2 bf16 ULPs (~2e-4 at the + # values seen here) on the ``use_fused_dX=True`` configs. Use a + # tolerance an order of magnitude below that — tight enough to catch + # any real bug but tolerant of the unavoidable atomic-order noise. + lora_grad_tol = dict(atol=1e-3, rtol=1e-3) + row_idx = ( + active.long()[:, None] * R + torch.arange(R, device=DEVICE)[None, :] + ).reshape(-1) + dA_b_active = dA_b[row_idx] + dB_b_active = dB_b[:, row_idx] + + assert torch.allclose(dA_b_active, dA_a, **lora_grad_tol), ( + f"dA active slice mismatch: max diff = " + f"{(dA_b_active - dA_a).abs().max().item()}" + ) + assert torch.allclose(dB_b_active, dB_a, **lora_grad_tol), ( + f"dB active slice mismatch: max diff = " + f"{(dB_b_active - dB_a).abs().max().item()}" + ) + + +# ─── Strategy B — fused MXFP4 Triton kernel ────────────────────────────────── + + +# MX rounding tolerance — the Triton path can reorder FMAs vs the torchao +# dequant + bf16 matmul reference, and the dequant arithmetic is +# fp32-codebook * fp32-scale -> bf16. See ``_STRATEGY_B_TOL`` above. + + +def _run_strategy_b(mx, x, lora_A, lora_B, scaling, sei, ssi, eo, top_k, E): + """Strategy B: pass MXWeights container directly to parallel_linear_lora; + the fused MX kernel does dequant inside the K-loop.""" + active = get_active_experts(sei, E) + remapped, compact_offsets = remap_expert_indices(sei, eo, active, E) + mx_active = selective_mx_weights_fwd(mx, active) + A_compact, B_compact = selective_lora_weights(lora_A, lora_B, active, E) + return parallel_linear_lora( + x, + mx_active, + top_k, + remapped, + ssi, + compact_offsets, + lora_A=A_compact, + lora_B=B_compact, + scaling=scaling, + ), active + + +@pytest.mark.parametrize("E,K,N,M,top_k,R,seed", SHAPES) +def test_strategy_b_forward_matches_bf16(E, K, N, M, top_k, R, seed): + """Strategy B forward must match bf16 baseline within MX rounding tol.""" + mx, W_ref = _make_mxfp4_weights(E, K, N, seed) + x, lora_A, lora_B, sei, ssi, eo = _setup_routing_and_lora( + E, K, N, M, top_k, R, seed + ) + scaling = 0.5 + tol = _tol_for_shape(K) + + out_baseline = _run_baseline(W_ref, x, lora_A, lora_B, scaling, sei, ssi, eo, top_k) + out_b, _ = _run_strategy_b(mx, x, lora_A, lora_B, scaling, sei, ssi, eo, top_k, E) + + assert out_baseline.shape == out_b.shape + diff = (out_baseline.float() - out_b.float()).abs() + rel = diff / (out_baseline.float().abs() + 1e-6) + assert torch.allclose(out_baseline, out_b, **tol), ( + f"Strategy B forward exceeds MX tolerance: max abs={diff.max().item():.4e}, " + f"max rel={rel.max().item():.4e}" + ) + + +@pytest.mark.parametrize("E,K,N,M,top_k,R,seed", SHAPES) +def test_strategy_b_backward_matches_bf16(E, K, N, M, top_k, R, seed): + """Strategy B forward+backward; dX, dA, dB compared to bf16 baseline on + the active expert slice within MX rounding tol.""" + mx, W_ref = _make_mxfp4_weights(E, K, N, seed) + x_base, lora_A_base, lora_B_base, sei, ssi, eo = _setup_routing_and_lora( + E, K, N, M, top_k, R, seed + ) + scaling = 0.5 + fwd_tol = _tol_for_shape(K) + dx_tol = _tol_for_shape(K, dx=True) + lg_tol = _tol_for_shape(K, lora_grad=True) + + # Baseline — match the MX path's fused-bwd kernel selection so dA/dB MMA + # accumulation order is the same and bf16 noise stays at single ULPs. + x_b = x_base.detach().clone().requires_grad_(True) + A_b = lora_A_base.detach().clone().requires_grad_(True) + B_b = lora_B_base.detach().clone().requires_grad_(True) + out_b = _run_baseline( + W_ref, + x_b, + A_b, + B_b, + scaling, + sei, + ssi, + eo, + top_k, + use_fused_dX=True, + use_fused_gather=True, + ) + grad_out = torch.randn_like(out_b) + out_b.backward(grad_out) + + # Strategy B + x_s = x_base.detach().clone().requires_grad_(True) + active = get_active_experts(sei, E) + remapped, compact_offsets = remap_expert_indices(sei, eo, active, E) + mx_active = selective_mx_weights_fwd(mx, active) + A_full = lora_A_base.detach().clone().requires_grad_(True) + B_full = lora_B_base.detach().clone().requires_grad_(True) + A_compact, B_compact = selective_lora_weights(A_full, B_full, active, E) + out_s = parallel_linear_lora( + x_s, + mx_active, + top_k, + remapped, + ssi, + compact_offsets, + lora_A=A_compact, + lora_B=B_compact, + scaling=scaling, + ) + out_s.backward(grad_out) + + # Forward parity within MX rounding tol — asserted here so a forward bug + # that produces a constant offset (and therefore zero gradient delta) + # doesn't slip past the bwd-only checks. + assert torch.allclose(out_b, out_s, **fwd_tol), ( + f"Strategy B forward mismatch: max abs diff = " + f"{(out_b - out_s).abs().max().item():.4e}" + ) + + # dX tolerance (looser; see _STRATEGY_B_DX_TOL comment) + assert torch.allclose(x_b.grad, x_s.grad, **dx_tol), ( + f"Strategy B dX mismatch: max diff = " + f"{(x_b.grad - x_s.grad).abs().max().item():.4e}" + ) + + # Uniform-scaling drift guard: the allclose bound above is generous to + # accommodate accumulated bf16 MMA noise over N. A bug that scales every + # dX element by a constant factor (e.g. an off-by-one on the E8M0 + # exponent shifting the whole tile by 2x) would still pass that bound. + # Catch it by requiring the per-element ratio std stays small after + # masking out near-zero baseline elements (where the ratio is dominated + # by quantization noise rather than uniform drift). + bf16_dX = x_b.grad.float() + mx_dX = x_s.grad.float() + eps = 1e-3 * bf16_dX.abs().max().clamp(min=1e-6) + mask = bf16_dX.abs() > eps + if mask.any(): + ratio = mx_dX[mask] / bf16_dX[mask] + ratio_std = ratio.std().item() + assert ratio_std < 0.5, ( + f"Strategy B dX uniform-scaling drift: std(mx/bf16) = " + f"{ratio_std:.4f} (mean = {ratio.mean().item():.4f}); " + f"a uniform multiplicative bug would slip past the allclose " + f"bound but is caught here." + ) + + # dA / dB — compare active expert slices (use forward tolerance — these + # come from the LoRA-only grad path which doesn't touch the W matmul) + row_idx = ( + active.long()[:, None] * R + torch.arange(R, device=DEVICE)[None, :] + ).reshape(-1) + dA_b_active = A_b.grad[row_idx] + dA_s_active = A_full.grad[row_idx] + dB_b_active = B_b.grad[:, row_idx] + dB_s_active = B_full.grad[:, row_idx] + + assert torch.allclose(dA_b_active, dA_s_active, **lg_tol), ( + f"Strategy B dA active slice mismatch: max diff = " + f"{(dA_b_active - dA_s_active).abs().max().item():.4e}" + ) + assert torch.allclose(dB_b_active, dB_s_active, **lg_tol), ( + f"Strategy B dB active slice mismatch: max diff = " + f"{(dB_b_active - dB_s_active).abs().max().item():.4e}" + ) diff --git a/tests/integrations/kernels/scattermoe_lora/test_mxfp4_integration.py b/tests/integrations/kernels/scattermoe_lora/test_mxfp4_integration.py new file mode 100644 index 0000000000..353969ac73 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_mxfp4_integration.py @@ -0,0 +1,246 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +End-to-end integration test for MXFP4 expert weights through the ScatterMoE +LoRA path. + +We build a tiny synthetic DeepSeek-V4-style MoE block (E=8, hidden=512, +intermediate=256, top_k=2), MX-quantize the gate/up and down projection +expert weights via ``torchao.MXTensor.to_mx``, then compare two stacks +forward-only: + + 1. **Reference** — pure PyTorch per-expert loop using the bf16 dequant of + the *same* MX weights. This stands in for "stock HF transformers MoE + with ``Mxfp4Config`` applied" — both stacks read the same physical MX + packed/scale buffers, so any divergence comes from the Axolotl + ScatterMoE plumbing (routing flatten/sort, scatter2scatter, fused + dequant kernel), not from differing weight quantization. + + 2. **Axolotl ScatterMoE** — ``parallel_linear_lora`` driven by an + ``MXWeights`` container, LoRA disabled (A = B = 0). Tests both + Strategy A (selective dequant to bf16) and Strategy B (fused MX + Triton kernel) so the spec'd "stock vs scattermoe" parity check + covers both code paths. + +Comparison tolerance is looser than the unit tests (``atol=rtol=5e-3``) +because the per-expert PyTorch reference accumulates in fp32 while the +Triton path emits bf16 outputs whose final cast rounds. +""" + +import pytest +import torch +import torch.nn.functional as F + +from axolotl.integrations.kernels.libs.scattermoe_lora.mx_weights import ( + selective_mx_weights_fwd, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_linear_lora import ( + parallel_linear_lora, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.selective_dequant import ( + get_active_experts, + remap_expert_indices, + selective_expert_weights, + selective_lora_weights, +) + +torchao = pytest.importorskip("torchao") +from torchao.prototype.mx_formats.mx_tensor import MXTensor # noqa: E402 + +DEVICE = "cuda" +DTYPE = torch.bfloat16 + +pytestmark = pytest.mark.skipif( + not torch.cuda.is_available(), reason="CUDA required for MX kernels" +) + + +# DeepSeek-V4-style tiny config (small enough for fast unit testing) +E = 8 +HIDDEN = 512 +INTERMEDIATE = 256 +TOP_K = 2 +M = 16 # batch * seq + + +def _build_synthetic_moe(): + """Return (gate_up_mx, down_mx, gate_up_ref, down_ref, router_w) + matching a DeepSeek-V4 expert block: + + * ``gate_up_proj``: per-expert ``[hidden, 2*intermediate]`` (split into + gate and up halves after the matmul). + * ``down_proj``: per-expert ``[intermediate, hidden]``. + + Storage layout matches axolotl's convention ``[E, N, K]`` where K is the + contraction axis the kernel will block on. ``gate_up`` has K=hidden, + N=2*intermediate; ``down`` has K=intermediate, N=hidden. + + bf16 reference tensors are the dequantizations of the *same* MX + buffers, so the only test source of divergence is the kernel paths. + """ + torch.manual_seed(42) + # Scale ~ 1/sqrt(fan_in) so per-layer outputs stay in order-1 range and + # bf16 final-cast noise is not amplified by the magnitude. + gup_scale = 1.0 / (HIDDEN**0.5) + down_scale = 1.0 / (INTERMEDIATE**0.5) + gate_up = ( + torch.randn(E, 2 * INTERMEDIATE, HIDDEN, device=DEVICE, dtype=DTYPE) * gup_scale + ) + down = torch.randn(E, HIDDEN, INTERMEDIATE, device=DEVICE, dtype=DTYPE) * down_scale + + gate_up_mx = MXTensor.to_mx( + gate_up, elem_dtype=torch.float4_e2m1fn_x2, block_size=32 + ) + down_mx = MXTensor.to_mx(down, elem_dtype=torch.float4_e2m1fn_x2, block_size=32) + gate_up_ref = gate_up_mx.dequantize(DTYPE).contiguous() + down_ref = down_mx.dequantize(DTYPE).contiguous() + + router_w = torch.randn(E, HIDDEN, device=DEVICE, dtype=DTYPE) * 0.1 + return gate_up_mx, down_mx, gate_up_ref, down_ref, router_w + + +def _reference_moe_forward(x, router_w, gate_up_ref, down_ref): + """Stand-in for stock HF MoE with Mxfp4Config: per-token routing + + per-expert matmul on dequantized bf16 weights.""" + # Softmax-topk routing + router_logits = F.linear(x, router_w) # [M, E] + routing_weights = F.softmax(router_logits, dim=-1, dtype=torch.float32) + routing_weights, selected = torch.topk(routing_weights, TOP_K, dim=-1) + routing_weights = routing_weights / routing_weights.sum(dim=-1, keepdim=True) + routing_weights = routing_weights.to(x.dtype) + + out = torch.zeros_like(x) + for e in range(E): + # Tokens routed to expert e: positions (token_id, k_slot) + mask = selected == e # [M, TOP_K] + if not mask.any(): + continue + token_ids, slot_ids = mask.nonzero(as_tuple=True) + x_e = x[token_ids] # [n_e, HIDDEN] + gup = x_e @ gate_up_ref[e].t() # [n_e, 2*INTERMEDIATE] + gate, up = gup.chunk(2, dim=-1) + h = F.silu(gate) * up + y_e = h @ down_ref[e].t() # [n_e, HIDDEN] + # Weighted accumulate + w_e = routing_weights[token_ids, slot_ids].unsqueeze(-1) + out.index_add_(0, token_ids, w_e * y_e) + return out + + +class _MockExperts: + def __init__(self, gate_up, down): + self.gate_up_proj = gate_up + self.down_proj = down + self.num_experts = E + + +def _axolotl_moe_forward(x, router_w, gate_up_param, down_param, *, strategy: str): + """Run the Axolotl ScatterMoE LoRA path with LoRA disabled (A=B=0). + + ``strategy='A'``: ``gate_up_param``/``down_param`` are torchao MXTensors; + we dequantize the active experts to bf16 and call the bf16 kernel. + + ``strategy='B'``: same MXTensors but routed through the fused MX kernel + via the ``MXWeights`` container. + """ + # Routing — same softmax+topk shape as the reference + router_logits = F.linear(x, router_w) + routing_weights = F.softmax(router_logits, dim=-1, dtype=torch.float32) + routing_weights, selected = torch.topk(routing_weights, TOP_K, dim=-1) + routing_weights = routing_weights / routing_weights.sum(dim=-1, keepdim=True) + routing_weights = routing_weights.to(x.dtype) + + sei, ssi, eo = flatten_sort_count(selected, num_experts=E) + active = get_active_experts(sei, E) + remapped, compact_offsets = remap_expert_indices(sei, eo, active, E) + + # Build LoRA tensors with A=B=0 so the LoRA term is zero. + rank = 4 + lora_A = torch.zeros(rank * E, HIDDEN, device=DEVICE, dtype=DTYPE) + lora_B_gup = torch.zeros(2 * INTERMEDIATE, rank * E, device=DEVICE, dtype=DTYPE) + lora_B_down = torch.zeros(HIDDEN, rank * E, device=DEVICE, dtype=DTYPE) + lora_A_inter = torch.zeros(rank * E, INTERMEDIATE, device=DEVICE, dtype=DTYPE) + A_gup_c, B_gup_c = selective_lora_weights(lora_A, lora_B_gup, active, E) + A_dn_c, B_dn_c = selective_lora_weights(lora_A_inter, lora_B_down, active, E) + + experts = _MockExperts(gate_up_param, down_param) + + if strategy == "A": + gate_up_W = ( + selective_expert_weights(experts, "gate_up_proj", active) + .transpose(2, 1) + .contiguous() + ) + down_W = ( + selective_expert_weights(experts, "down_proj", active) + .transpose(2, 1) + .contiguous() + ) + gup_W = gate_up_W + dwn_W = down_W + elif strategy == "B": + gup_W = selective_mx_weights_fwd(gate_up_param, active) + dwn_W = selective_mx_weights_fwd(down_param, active) + else: + raise ValueError(strategy) + + gup = parallel_linear_lora( + x, + gup_W, + TOP_K, + remapped, + ssi, + compact_offsets, + lora_A=A_gup_c, + lora_B=B_gup_c, + scaling=0.0, + grouped_in=False, + grouped_out=True, + use_fused_dX=True, + use_fused_gather=True, + ) + gate, up = gup.chunk(2, dim=-1) + h = F.silu(gate) * up + out = parallel_linear_lora( + h, + dwn_W, + 1, + remapped, + ssi, + compact_offsets, + lora_A=A_dn_c, + lora_B=B_dn_c, + scaling=0.0, + gates=routing_weights, + grouped_in=True, + grouped_out=False, + use_fused_dX=True, + use_fused_gather=True, + ) + return out + + +@pytest.mark.parametrize("strategy", ["A", "B"]) +def test_mxfp4_moe_block_matches_pytorch_reference(strategy): + """The Axolotl ScatterMoE MX path must match the per-expert PyTorch + reference (operating on the same MX dequantized weights) within + integration-grade tolerance.""" + gate_up_mx, down_mx, gate_up_ref, down_ref, router_w = _build_synthetic_moe() + + torch.manual_seed(7) + x = torch.randn(M, HIDDEN, device=DEVICE, dtype=DTYPE) + + ref = _reference_moe_forward(x, router_w, gate_up_ref, down_ref) + out = _axolotl_moe_forward(x, router_w, gate_up_mx, down_mx, strategy=strategy) + + assert ref.shape == out.shape == (M, HIDDEN) + assert torch.allclose(ref, out, atol=5e-3, rtol=5e-3), ( + f"Strategy {strategy} MoE block diverges from PyTorch reference: " + f"max abs={(ref - out).abs().max().item():.4e}, " + f"max rel={((ref - out).abs() / (ref.abs() + 1e-6)).max().item():.4e}" + ) From 9a79b6801ca20f28233d4040e4d98df5816ce831 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 28 May 2026 10:32:44 -0400 Subject: [PATCH 1327/1405] tiled-MLP for MoE: MoE block patcher + FSDP2 reshard fix + grad-accum fp32 fix + AC-vs-tiled gap analysis (#3666) [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(scattermoe-lora): selective dequant for mxfp4 expert weights Add an MXFP4 branch to `selective_expert_weights()` that detects a torchao `MXTensor` parameter (elem_dtype=float4_e2m1fn_x2) and dequantizes only the active experts via index-then-construct of a compact sub-MXTensor. The K-axis OCP block layout (last storage dim) matches `experts.gate_up_proj` natural shape `[E, N, K]`, so the caller's existing `.transpose(2, 1)` post-step keeps producing the kernel's `[E, K, N]` weight tile unchanged. `HFScatterMoEGatedMLP.forward` now also routes through the selective path whenever the experts hold MXFP4 weights — full-tensor MX dequant of 256-expert models is prohibitive and the kernel needs bf16 input. Tests (CUDA-only) compare against a bf16 baseline produced by the same MXTensor's full dequant; outputs are bitwise identical for both forward and backward (dX, dA, dB) across small [E=8,K=128,N=256] and representative [E=32,K=2048,N=1024] shapes, and across all four combinations of \`use_fused_dX\` / \`use_fused_gather\`. Signed-off-by: Wing Lian * feat(scattermoe-lora): fused mxfp4 dequant in triton kernel Add MX-aware forward and dX kernels that consume an ``MXWeights`` container (packed uint8 + E8M0 scales) directly, so the base-weight tile is dequantized inside the K-loop instead of through a materialized bf16 buffer. The K-loop loads two FP4 values per uint8 byte, looks them up in a 16-entry codebook tensor (``±{0, 0.5, 1, 1.5, 2, 3, 4, 6}``), multiplies by ``2^(scale_byte - 127)``, and casts to bf16 for the matmul. ``BLOCK_K`` is constrained to a multiple of the OCP block size (32) so each tile aligns with whole scale blocks; an MX-aware autotune pruner accounts for the extra packed/scale SMEM. The dX kernel reuses the *forward* MX layout (block axis = K, the dX output axis) — for each (K_tile, N_tile) sub-tile, nibbles decode along the K rows (the byte is shared by two adjacent K rows) and scales broadcast within their MX block. This avoids the dequant + re-quantize "pre-transpose" the spec suggested and the extra MX-rounding error that round-trip would have introduced. ``ScatterMoELoRA.forward`` now accepts either a dense tensor or an ``MXWeights``; the MX branch always selects the fused-dX and fused-gather backward kernels (the non-fused dX path would have to materialize a bf16 weight tile, defeating the win). Unit tests cover forward, dX, dA, dB parity for small [E=8, K=128, N=256] and representative [E=32, K=2048, N=1024] shapes; tolerances are calibrated to bf16 MMA noise (atomic-add ordering and FMA reordering between the full-E baseline and compact-active MX path). Integration test exercises a tiny synthetic DeepSeek-V4-style MoE block (E=8, hidden=512, intermediate=256, top_k=2) end-to-end through both Strategy A and Strategy B with LoRA disabled. Signed-off-by: Wing Lian * chore(scattermoe-lora): mxfp4 forward/backward benchmark Add ``bench_mxfp4.py`` and committed results for the representative DeepSeek-V4-style shape (E=128, K=2048, N=1024, top_k=8, M=4096, rank=16). Reports ms/iter, tokens/s, peak GPU memory, and HBM bandwidth utilisation for three configurations: bf16 baseline, Strategy A (selective dequant), Strategy B (fused MX). On the RTX PRO 6000 Blackwell, the all-active-experts shape used here doesn't exercise selective dequant's memory savings (active = E = 128) — A pays the cost of materialising the full bf16 dequant buffer per step (~9 GB peak vs 1.9 GB for B) while still routing through the bf16 kernel. B halves A's wall time (~12 ms vs 30 ms) by eliminating the buffer, but stays slower than the bf16 baseline (5 ms) which assumes the bf16 weights already exist in memory. Signed-off-by: Wing Lian * bench(scattermoe-lora): mxfp4 sparse-routing benchmark numbers Signed-off-by: Wing Lian * bench(scattermoe-lora): mxfp4 seqlen sweep with load-balanced routing Signed-off-by: Wing Lian * fix(scattermoe-lora): correct mx forward smem accounting The MX-aware autotune pruner for the forward kernel under-accounted SMEM: it computed the packed-tile cost as BLOCK_N * BLOCK_K/2 and the scale-tile cost as BLOCK_N * BLOCK_K/MX_BLOCK_SIZE, but the actual tl.load issues a full [BLOCK_N, BLOCK_K]-shaped uint8 fetch for both buffers (the packed buffer reads each byte twice because K_byte = K // 2 indexes a [BLOCK_K]-wide vector; the scale buffer broadcasts within each MX_BLOCK_SIZE K-block). Bring the forward pruner up to the same conservative full-tile accounting already used by _prune_dX_mx_configs. Without this, on the [E=128, K=2048, N=1024] shape with the typical GPU SMEM caps, two to six high-stage configs that were previously selectable would have overflowed SMEM at launch under correct accounting — a silent OOM-in-the-future risk. Signed-off-by: Wing Lian * docs(scattermoe-lora): align mx dx kernel docstring with implementation The file-level docstring for the MXFP4 kernels described the dX kernel as using a pre-transposed [E, K, N/2] layout produced by a 'mx_pre_transpose_for_dx' helper. That helper doesn't exist; the dX kernel actually reuses the forward [E, N, K/2] layout, iterating the N reduction in outer tiles and decoding nibbles along the K rows of each tile. Rewrite the docstring to describe what the code actually does, including the rationale — reusing the forward buffer avoids the dequant + re-quantize round-trip that a pre-transpose would require and keeps dX numerics free of a second MX rounding error stacked on top of the forward quantization. Signed-off-by: Wing Lian * chore(scattermoe-lora): mx code-review nit cleanup F4: Hoist 'is_mxfp4_param' import from inside 'HFScatterMoEGatedMLP.forward' to the top of layers.py — it was being re-imported every step on the hot path. F5: Add a thin compatibility shim for torchao MXTensor internals access in mx_weights.py. The MX paths in selective_dequant.py / mx_weights.py used to reach into 'mx_param.qdata', 'mx_param.scale', 'mx_param.kernel_preference' and call 'MXTensor(...)' with positional args directly. That works at the pinned torchao 0.17.0 but is fragile to internal renames in future torchao releases. Funnel through three helpers — '_mx_qdata', '_mx_scale', '_construct_mxtensor_subset' — that use 'getattr' fallbacks for the buffer attributes and pass the constructor's optional args via 'getattr' too. Single point of pain, no API change. F7: Remove the unused 'NO_K_MASK' heuristic + tl.constexpr param from the dX MX kernel '_scatter2scatter_lora_dX_mx'. The dX kernel never references it (its inner loop masks N, not K), so the constexpr just forced extra autotune key entries. F8: Consolidate the duplicate '_torchao_mxtensor_cls()' definitions (one in selective_dequant.py, one in mx_weights.py) into a single definition in mx_weights.py. selective_dequant.py imports it. Signed-off-by: Wing Lian * test(scattermoe-lora): strengthen mx backward test coverage F3: 'test_strategy_a_backward_fused_variants' previously used 'torch.ones_like(output)' as the grad input and asserted only on dX. A uniform grad zeros out cross-token differences in the fused-gather accumulation, masking reordering bugs; restricting the assertion to dX silently let the dA/dB paths go unchecked across the four '(use_fused_dX, use_fused_gather)' production variants. * Drive the backward with 'torch.randn_like(output) * 0.1'. * Capture and assert dA and dB parity across all four variants using the same 'row_idx' gather pattern as 'test_strategy_a_backward_matches_bf16'. * Forward and dX are still asserted bitwise via 'torch.equal'. dA/dB fall back to atol/rtol = 1e-3 because the fused dA/dB kernel uses 'atomic_add' across N-block programs and the in-flight program count differs between the full-E baseline and the compact-active path; combined with FMA reordering, the 'use_fused_dX=True' variants accumulate ~1 bf16 ULP of unavoidable atomic-order noise. The new bound is still an order of magnitude below that noise floor, so it catches real bugs. F9: The 'test_strategy_b_backward_matches_bf16' dX comparison runs at 'atol=0.5, rtol=2e-2' (small) / 'atol=2.0, rtol=3e-2' (representative) to allow for accumulated bf16 MMA noise over the N reduction. Those bounds are appropriate for legitimate per-element drift but would also admit a uniform multiplicative bug — e.g. an off-by-one on the E8M0 exponent that scales every dX element by 2x. Add a guard alongside the existing 'torch.allclose': mask out near-zero baseline elements (relative to 'bf16_dX.abs().max()'), then require the per-element ratio 'mx_dX / bf16_dX' to have std < 0.5. A uniform multiplicative bug pushes that std to ~0 while the mean shifts; a real-bug per-element drift pushes the std up. This crosscuts the allclose check rather than replacing it. Signed-off-by: Wing Lian * bench(scattermoe-lora): exclude per-iter setup from timed window The previous bench harness did a fresh '.clone()' of x and a 'requires_grad_(True)' on cloned lora A/B tensors every iter inside the timed window. That accounts for buffer allocation, not kernel cost, and biases the numbers toward whichever path produced the smallest activations. Restructure the runners so: * 'x' is cloned once into a leaf tensor with 'requires_grad_(True)' inside 'bench()' (outside the timed warmup + timed loop). * LoRA A/B leaf tensors are constructed once in the runner factory, not per iter. * Each iter calls the runner which sets 'x.grad = A.grad = B.grad = None' (cheap, no GPU sync) so the autograd graph for the timed iteration is fresh and grads don't accumulate. Re-run all three configs end-to-end after this change (dense E=128, sparse E=256 / 10-active, balanced E=256 M-sweep at M ∈ {256, 1024, 4096, 16384}) and refresh the numbers in bench_mxfp4_results.md. Headers and table structure are unchanged. The qualitative ordering holds (Strategy A wins at low active/E, Strategy B wins near active/E ≈ 1, and Strategy A still OOMs across the balanced sweep on the workstation with vLLM colocated), with per-cell numbers within single-digit percent of the prior runs. Signed-off-by: Wing Lian * style(scattermoe-lora): apply pre-commit auto-fixes and mypy fixes Signed-off-by: Wing Lian * fix(scattermoe-lora): mxfp4 shape validation + torchao version messages Signed-off-by: Wing Lian * lint and PR review fixes * fix(scattermoe-lora): restore lint task fixes + add missing forward parity assertions Wing's "lint and PR review fixes" commit (9007a829) reverted three fixes from the prior lint pass. Restore them: 1. parallel_linear_lora.py: use isinstance(expert_weights, MXWeights) directly so mypy can narrow the union — the `is_mx` boolean alias blocks narrowing and re-introduces 2 union-attr errors. 2. bench_mxfp4.py: assert template is not None before the MXTensor(...) constructor — the chunked converter initializes template to None then sets it inside the loop, which mypy can't prove non-None at the call site (6 None-attr errors). 3. test_mxfp4_expert_weights.py: the F841 on fwd_tol was actually a smell of dropped logic. Both backward tests (test_strategy_a_backward_matches_bf16 and test_strategy_b_backward_matches_bf16) compute the forward outputs out_b/out_a/out_s, run backward, and assert gradients match — but never assert that the forward outputs match. A forward bug producing a constant offset (and therefore zero gradient delta) would slip past the bwd-only checks. Add the missing torch.equal(out_b, out_a) for Strategy A (bitwise contract) and torch.allclose(out_b, out_s, **fwd_tol) for Strategy B (MX tol). Signed-off-by: Wing Lian * don't worry about flash-attn direct patches for now * feat(tiled-mlp): support MoE block classes in patcher Extend patch_tiled_mlp to discover MoE block classes ({prefix}SparseMoeBlock / MoeMLP / MoE) and patch the routing+expert forward when scattermoe-lora is active. The kernels library installs HFScatterMoEGatedMLP.forward per instance during model.kernelize(), which shadows class-level patches. Add a post-model-load step (patch_tiled_mlp_moe_instances) that re-wraps each MoE block instance so tiling layers on top of the kernels-installed forward instead of being bypassed. Falls back to the existing dense {prefix}MLP / {prefix}TextMLP path when no MoE block class exists. The gpt_oss special case for DeepSpeedTiledMLPMoE is preserved and extended to every MoE block. Signed-off-by: Wing Lian * fix(tiled-mlp): defer FSDP2 reshard + correct per-shard grad accumulation Two backward-pass correctness fixes in TiledMLP.backward. 1) Defer FSDP2 post-backward reshard across the tile loop. The backward issues one torch.autograd.backward per shard. Under FSDP2 (torch.distributed.fsdp.fully_shard), the first inner backward triggers the wrapping FSDPModule's post-backward hook, which reshards parameters; subsequent shards then recompute against only-local DTensor shards. Silent gradient corruption at best, crash at worst. LinkedIn's Liger-Kernel PR #1128 fixed this for FSDP1 with FSDP.summon_full_params(writeback=True). That API does not exist in FSDP2. The PyTorch 2.11 FSDP2 surface is FSDPModule.set_reshard_after_backward(False) — toggle off around the tile loop, restore the prior value, and issue one explicit reshard() afterwards. The wrapping FSDPModule is discovered by walking the global _module_state_mapping registry (FSDP2 is typically applied at the decoder-layer level, so the MLP itself is rarely the FSDPModule). Result is cached on the MLP instance so the walk runs once. No-op under DDP, single-GPU, or DeepSpeed. DeepSpeedTiledMLPMoE is left alone — DeepSpeed coordinates its own gather and the two backends are mutually exclusive. 2) Replace the hook-based GradientAccumulator with inline fp32 accumulation. The previous implementation called grad_accumulator.install_hooks() inside every shard iteration, so the N-th shard ran N stacked hooks that each accumulated the same shard contribution — and on the last shard the manually-set param.grad was then re-added by AccumulateGrad, doubling it. The accumulator also scaled by 1/N, but sequence-dim sharded gradients are additive (not averaged). Combined, param.grad came out ~2x-2.5x the analytical value. Inline accumulation captures param.grad after each shard's inner backward, sums into a per-param fp32 accumulator, clears the running grad, and writes the total back once at the end (preserving any pre-existing .grad from earlier graph segments). Signed-off-by: Wing Lian * test(tiled-mlp): single-gpu MoE + scattermoe-lora coverage Three parity checks (tiled vs un-tiled forward+backward) plus two patcher-internals tests. All gated on CUDA. - Dense LlamaMLP-shape (hidden=64, intermediate=128, seq=64): tight atol=1e-5 on outputs, dX, and every parameter grad. Uses batch=1 to match the sequence-packed inputs production sees. - Hand-rolled MoE block (E=8, hidden=64, intermediate=128, top_k=2): same shape + same tolerances against an index_add-based reference. - ScatterMoEGatedMLP in bf16: norm-relative tolerance < 1%, matching the established bar in tests/integrations/test_scattermoe_lora_kernels.py (bf16 + tiled reduction order makes max abs error a noisy signal). - Patcher unit tests: MoE block class discovery prefers SparseMoeBlock / MoeMLP over MoE, and returns None for dense models. Synthetic-shape modules only — no transformers checkpoints loaded. Signed-off-by: Wing Lian * test(tiled-mlp): FSDP2 multi-rank correctness Two parity tests (dense + scattermoe-lora) that wrap a tiny MLP / ScatterMoEGatedMLP with FSDP2 (`fully_shard`) and compare tiled forward+backward against a non-tiled FSDP2 reference. Both must run through the FSDPModule's __call__ so FSDP2's pre-forward hooks materialize the unsharded params before TiledMLP.apply chunks the input; the helper _install_tiled_forward mirrors what the production patcher does instance-side. Designed to be launched with `torchrun --nproc-per-node=2 -m pytest tests/e2e/multigpu/test_tiled_mlp_fsdp2.py`. Skips with a clear reason on a 1-GPU executor or when launched without torchrun. Verified to pass on a 2-GPU runner (RTX PRO 6000 Blackwell). Signed-off-by: Wing Lian * feat(scattermoe-lora): shared dequant buffer across tile shards Adds shared_dequant_across_shards() which hoists the MXFP4 dequant out of the per-shard selective path. The orthogonal tiled wrapper calls selective_expert_weights once per shard; when active-expert sets overlap (the common case under softmax routing) the dequant is wasted work. The helper computes the union of active experts across all shards, dequantizes that union once, and returns per-shard remaps so each shard's parallel_linear_lora call uses the correct slice. Bitwise contract: a shard's gathered slice is byte-identical to the per-shard selective_expert_weights output, verified by test_shared_dequant_helper.py with N=4 overlapping shards plus disjoint and single-shard regression cases. Signed-off-by: Wing Lian * fix(tiled-mlp): default grad accumulator to param dtype, skip redundant casts The orthogonal TiledMLP wrapper pre-allocated an fp32 accumulator the size of every compute param, then cast each shard's bf16 ``param.grad`` to fp32 inside the loop before adding it. For E=128 / hidden=2048 / intermediate=8192 MoE training in bf16 that's roughly 17 GiB of fp32 buffer on the gate_up_proj alone — net 2x parameter-side memory regression vs. simply accumulating at the param's own dtype. The per-shard ``grad.to(fp32)`` cast was also a per-shard HBM bandwidth tax that dominated the wall-clock regression at intermediate=8192. Match what AccumulateGrad does in the unsharded backward: accumulate at the param's own dtype, skip the cast when shard-grad dtype matches the accumulator dtype, and only cast back to param dtype at write-back when the buffer dtype differs. fp32 accumulation is opt-in via AXOLOTL_TILED_MLP_ACCUM_FP32=1 for callers who care about bf16 round-off in very-large-N-shard sums. The dead ``GradientAccumulator`` class (no longer called after the inline-accumulation refactor in b13375a0) is updated to the same defaults — param-dtype accumulator, gradient_scale=1.0 — so it is in a coherent state if anyone re-introduces a hook-based path. Signed-off-by: Wing Lian * test(tiled-mlp): strengthen tiled-vs-untiled grad parity Add three regression guards for the TiledMLP gradient-accumulator fix: 1) ``test_tiled_dense_mlp_grad_parity_nonuniform_weights`` and ``test_tiled_moe_grad_parity_nonuniform_weights`` exercise shards in {1, 2, 4} with non-uniform per-token upstream weights. A mean-vs-sum scaling bug in the per-shard accumulator (the historical ``gradient_scale = 1/total_shards``) would show up as roughly ``(N-1)/N`` relative drift in the param grads. The old tests used a single shard count and uniform-magnitude upstream, which allowed the bug to slip through. 2) ``test_tiled_dense_mlp_grad_parity_bf16`` runs the same parity at bf16 to lock the default param-dtype accumulator path (no fp32 buffer) against regression. 3) ``test_tiled_grad_accumulator_dtype_matches_param_dtype`` is an allocation-side guard: spy on ``torch.zeros_like`` during a bf16 tiled backward and assert none of the per-param accumulator allocations request fp32. A future change that re-introduces the fp32 buffer by default would fail this check without needing a memory-resident bench. Signed-off-by: Wing Lian * fix(tiled-mlp): default to ~32K tokens/shard, not ceil(seq/hidden) The previous heuristic put only ~2K tokens/shard at long context — well below the MoE Triton kernel's BLOCK_M sweet spot. An empirical sweep at seq ∈ {64K, 128K, 256K, 512K} showed 3.2× speed-up at 64–256K and 2.1× at 512K from raising per-shard tokens to ~32K, with only a modest peak-mem cost (~5–10 GiB extra at seq=256K) because the routed intermediate buffer dominates and scales linearly with per-shard tokens. Bench data is operator-archived locally; the headline numbers are included in the PR description. The 32K target is empirical, not theoretical — it's the largest tokens-per-shard that fits at seq up to 256K without OOM and stays inside the cuBLAS large-batch_count safe regime that surfaces a separate bug at seq=512K + s=16. Operators can override via cfg_num_shards for niche cases (smaller intermediate, larger top_k). Also includes ruff-format cleanup of cherry-picked commits. Signed-off-by: Wing Lian --------- Signed-off-by: Wing Lian --- .../libs/scattermoe_lora/selective_dequant.py | 76 +++ src/axolotl/loaders/patch_manager.py | 20 + src/axolotl/monkeypatch/tiled_mlp/__init__.py | 2 + src/axolotl/monkeypatch/tiled_mlp/base.py | 271 ++++++++-- src/axolotl/monkeypatch/tiled_mlp/patch.py | 310 +++++++++--- tests/e2e/multigpu/test_tiled_mlp_fsdp2.py | 306 +++++++++++ .../test_shared_dequant_helper.py | 146 ++++++ tests/integrations/monkeypatch/__init__.py | 0 .../monkeypatch/test_tiled_mlp_moe.py | 474 ++++++++++++++++++ 9 files changed, 1503 insertions(+), 102 deletions(-) create mode 100644 tests/e2e/multigpu/test_tiled_mlp_fsdp2.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_shared_dequant_helper.py create mode 100644 tests/integrations/monkeypatch/__init__.py create mode 100644 tests/integrations/monkeypatch/test_tiled_mlp_moe.py diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py index 6702b2f15d..6398d1faa3 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py @@ -311,6 +311,82 @@ def selective_expert_weights( return param +def shared_dequant_across_shards( + experts_module: nn.Module, + param_name: str, + sei_per_shard: list[torch.Tensor], + E: int, +) -> tuple[torch.Tensor, torch.Tensor, list[torch.Tensor]]: + """Dequantize the union of active experts across N shards exactly once. + + The orthogonal Strategy A path calls :func:`selective_expert_weights` + once per shard, which re-dequantizes the active experts redundantly + when the active-expert sets overlap. For seq-dim sharding with a + softmax-routed MoE, that overlap is the common case. + + This helper hoists the dequant: it computes the union of active + experts across all shards, calls :func:`selective_expert_weights` + once on the union, and returns per-shard index tables that map each + shard's local active experts into rows of the union buffer. + + Parameters + ---------- + experts_module: + The base experts module (e.g. ``OlmoeExperts``). Same object the + per-shard path would pass to :func:`selective_expert_weights`. + param_name: + ``"gate_up_proj"`` or ``"down_proj"``. + sei_per_shard: + List of ``sorted_expert_idxs`` tensors, one per shard. + E: + Total number of experts. + + Returns + ------- + union_active: + ``[U]`` sorted unique expert ids across all shards. + union_buffer: + Dequantized weights for ``union_active``, + ``[U, dim1, dim2]`` in the param's natural storage dtype + (typically bf16). Same buffer each shard's call would have built + had it dequantized only its own active set, just shared. + shard_into_union: + List of length ``len(sei_per_shard)``. Entry ``i`` is a 1-D + ``long`` tensor that indexes ``union_buffer`` along dim 0 to + produce the same ``[num_active_i, dim1, dim2]`` slice the + per-shard path would have produced. Callers feed this through + ``union_buffer.index_select(0, shard_into_union[i])`` (or + equivalent advanced indexing) before handing the slice to + ``parallel_linear_lora``. + + Bitwise contract: composing ``union_buffer.index_select(0, + shard_into_union[i])`` is byte-identical to + ``selective_expert_weights(experts_module, param_name, + get_active_experts(sei_per_shard[i], E))`` because both paths slice + the same dequantized MX subset by the same expert ids. The + ``test_shared_dequant_helper.py`` parity test asserts this. + """ + if not sei_per_shard: + raise ValueError("sei_per_shard must contain at least one tensor") + + device = sei_per_shard[0].device + per_shard_active = [get_active_experts(sei, E) for sei in sei_per_shard] + union_active = torch.unique(torch.cat(per_shard_active)) + + union_buffer = selective_expert_weights(experts_module, param_name, union_active) + + # Build the global-id → union-row remap once, then gather per shard. + # ``union_active`` is sorted and unique by construction, so the inverse + # lookup is dense over ``E``. + union_remap = torch.empty(E, dtype=torch.long, device=device) + union_remap[union_active] = torch.arange( + len(union_active), device=device, dtype=torch.long + ) + shard_into_union = [union_remap[active] for active in per_shard_active] + + return union_active, union_buffer, shard_into_union + + def selective_lora_weights( lora_A: torch.Tensor, lora_B: torch.Tensor, diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 38a82645fc..943d52af0b 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -166,6 +166,7 @@ def apply_post_model_load_patches(self, model: PreTrainedModel): self._apply_lora_kernel_patch(model) self._apply_scaling_softmax_patch(model) self._apply_fp8_attention_patches(model) + self._apply_tiled_mlp_post_load(model) def _apply_gemma_hybrid_attention(self, model: PreTrainedModel): """Apply hybrid attention: FA2 for sliding window layers, SDPA for global layers. @@ -700,8 +701,27 @@ def _apply_tiled_mlp(self, model_type: str): model_type, use_original_mlp=self.cfg.tiled_mlp_use_original_mlp, cfg_num_shards=self.cfg.tiled_mlp_num_shards, + use_scattermoe=bool(self.cfg.use_scattermoe), ) + def _apply_tiled_mlp_post_load(self, model): + """Re-wrap MoE block instances after kernels have installed their forward. + + Needed only when scattermoe-lora is active — ``model.kernelize()`` + binds ``HFScatterMoEGatedMLP.forward`` per instance, which shadows + the class-level tiled patch. See + :func:`axolotl.monkeypatch.tiled_mlp.patch_tiled_mlp_moe_instances`. + """ + if not (self.cfg.tiled_mlp and self.cfg.use_scattermoe): + return + from axolotl.monkeypatch.tiled_mlp import patch_tiled_mlp_moe_instances + + patch_tiled_mlp_moe_instances( + model, + self.cfg.model_config_type, + cfg_num_shards=self.cfg.tiled_mlp_num_shards, + ) + def _apply_voxtral_patches(self): """Apply patches for Voxtral model.""" if self.cfg.model_config_type == "voxtral": diff --git a/src/axolotl/monkeypatch/tiled_mlp/__init__.py b/src/axolotl/monkeypatch/tiled_mlp/__init__.py index 4ea1549915..cb9aa6145a 100644 --- a/src/axolotl/monkeypatch/tiled_mlp/__init__.py +++ b/src/axolotl/monkeypatch/tiled_mlp/__init__.py @@ -4,8 +4,10 @@ from .patch import ( patch_tiled_mlp, + patch_tiled_mlp_moe_instances, ) __all__ = [ "patch_tiled_mlp", + "patch_tiled_mlp_moe_instances", ] diff --git a/src/axolotl/monkeypatch/tiled_mlp/base.py b/src/axolotl/monkeypatch/tiled_mlp/base.py index 2c9dc8e4c5..ab4f93ab53 100644 --- a/src/axolotl/monkeypatch/tiled_mlp/base.py +++ b/src/axolotl/monkeypatch/tiled_mlp/base.py @@ -2,11 +2,127 @@ TiledMLP support for DDP, FSDP, and single GPU """ +import contextlib +import os import threading from typing import List import torch +# Opt-in fp32 accumulation for the tiled backward. The default accumulates +# at the param's own dtype, which matches what AccumulateGrad does in the +# unsharded backward and avoids materialising an fp32 buffer the size of +# every compute param. Set ``AXOLOTL_TILED_MLP_ACCUM_FP32=1`` to recover +# the previous fp32-accumulator behaviour when bf16 precision is the +# concern (e.g. very large N-shard sums where bf16 round-off accumulates). +_TILED_MLP_ACCUM_FP32 = os.environ.get("AXOLOTL_TILED_MLP_ACCUM_FP32", "0") == "1" + + +def _find_fsdp2_module(module): + """Return the nearest FSDP2 :class:`FSDPModule` that owns ``module``. + + FSDP2 (``torch.distributed.fsdp.fully_shard``) registers per-module + post-backward hooks that reshard parameters once their gradients have + been produced. Inside :class:`TiledMLP.backward` we run several inner + backwards over shards of the same input; if the wrapping FSDPModule + reshards between iterations, the unsharded params are gone and the + next tile recomputes against bogus shards. We have to disable reshard + on the wrapping FSDPModule for the duration of the loop. + + The MLP itself is rarely the directly-wrapped module — production + setups apply ``fully_shard`` at the decoder-layer level. Walk the + global FSDP module-state registry to find the nearest ancestor whose + parameter group contains us. Result is cached on the module so we pay + the lookup once. + + Returns ``None`` if FSDP2 is not in use, or no wrapping FSDPModule + contains ``module`` as a descendant. + """ + cached = getattr(module, "_axolotl_fsdp2_owner", "__unset__") + if cached != "__unset__": + return cached + + try: + from torch.distributed._composable_state import _module_state_mapping + from torch.distributed.fsdp import FSDPModule + except ImportError: + module._axolotl_fsdp2_owner = None + return None + + # MLP itself wrapped (covers the regression-guard unit test). + if isinstance(module, FSDPModule): + module._axolotl_fsdp2_owner = module + return module + + # Walk the global FSDP registry looking for ancestors. The registry is + # a WeakKeyDictionary so the snapshot is cheap and bounded by the + # number of FSDP-wrapped modules in the process. + target_id = id(module) + candidates = [] + for owner in list(_module_state_mapping.keys()): + if not isinstance(owner, FSDPModule): + continue + if owner is module: + continue + for sub in owner.modules(): + if id(sub) == target_id: + candidates.append(owner) + break + + if not candidates: + result = None + elif len(candidates) == 1: + result = candidates[0] + else: + # When multiple FSDPModules are ancestors (e.g. fully_shard applied + # to both decoder layer and the root), pick the deepest one — its + # subtree is smallest. Counting modules is O(N) per candidate but + # only runs once per MLP instance. + result = min(candidates, key=lambda m: sum(1 for _ in m.modules())) + + module._axolotl_fsdp2_owner = result + return result + + +@contextlib.contextmanager +def _defer_fsdp2_reshard(module): + """Suspend FSDP2's post-backward reshard on the wrapping FSDPModule. + + The tiled backward calls :func:`torch.autograd.backward` once per shard. + Each inner backward triggers FSDP2's per-module post-backward hooks, + which would reshard parameters mid-loop. We pause that by toggling + ``set_reshard_after_backward(False)`` on the wrapping FSDPModule, run + the loop, restore the original setting, then issue a single explicit + ``reshard()`` so the post-loop state matches normal FSDP2 semantics. + + No-op when ``module`` is not under FSDP2. + """ + fsdp_mod = _find_fsdp2_module(module) + if fsdp_mod is None: + yield + return + + # No public getter for ``reshard_after_backward`` in PyTorch 2.11; + # read off the param group directly. The internal accessor surface + # is documented in + # ``torch.distributed.fsdp._fully_shard._fsdp_param_group.FSDPParamGroup``. + state = fsdp_mod._get_fsdp_state() + param_group = state._fsdp_param_group + if param_group is None: + # Nothing to defer (e.g. ignored module with no FSDP-managed params). + yield + return + + prev = param_group.reshard_after_backward + fsdp_mod.set_reshard_after_backward(False, recurse=False) + try: + yield + finally: + # Restore so subsequent backward passes outside the tile loop + # behave normally, then issue the deferred reshard once. + fsdp_mod.set_reshard_after_backward(prev, recurse=False) + fsdp_mod.reshard() + class DeepSpeedTiledMLPMoE(torch.autograd.Function): @staticmethod @@ -151,47 +267,99 @@ def backward(ctx, *grads) -> torch.Tensor: x_grad = torch.zeros_like(x) x_shards = list(torch.chunk(x, chunks=shards, dim=1)) - # Create a gradient accumulator for parameters - grad_accumulator = GradientAccumulator(compute_params, shards, dtype=x.dtype) + # Snapshot existing ``.grad`` for each param and zero it; we will + # accumulate the per-shard contributions into a per-param buffer + # and write back at the end. The previous implementation used + # ``param.register_hook`` per shard, which (a) re-installed hooks + # every iteration so the N-th shard ran N stacked hooks and + # double-counted contributions, and (b) scaled by ``1/N`` even + # though sequence-dim sharding makes per-shard grads additive, + # not averaged. The combined effect was a gradient roughly + # 2x-2.5x the analytical value. Direct inline accumulation is + # both simpler and correct, and avoids interactions with FSDP2's + # own backward hooks. + # + # The accumulator defaults to the param's own dtype to match + # what AccumulateGrad would do in the unsharded backward. The + # earlier implementation accumulated in fp32, which doubled the + # parameter-side memory footprint in bf16 MoE training where the + # accumulator's ``[E, hidden, 2*intermediate]`` shape dominates. + # Set ``AXOLOTL_TILED_MLP_ACCUM_FP32=1`` to opt back into fp32 + # accumulation when bf16 round-off is the concern. + prev_grads = {} + accum_grads = {} + for p in compute_params: + prev_grads[p] = p.grad + accum_dtype = torch.float32 if _TILED_MLP_ACCUM_FP32 else p.dtype + accum_grads[p] = torch.zeros_like(p, dtype=accum_dtype) + p.grad = None shard_step = x_shards[0].numel() - for i, x_shard in enumerate(x_shards): - x_shard.requires_grad_(x_requires_grad) - - shard_offset = i * shard_step - x_shard.grad = ( - x_grad.view(-1) - .narrow(0, shard_offset, x_shard.numel()) - .view_as(x_shard) - ) - incoming_grad_shard = ( - incoming_grad.view(-1) - .narrow(0, shard_offset, x_shard.numel()) - .view_as(x_shard) - ) - - # Install hooks for this shard - is_last_shard = i + 1 == shards - grad_accumulator.install_hooks(is_last_shard) + # Suspend FSDP2 post-backward reshard for the duration of the loop. + # Without this, the first inner backward triggers FSDP2's reshard + # hook on the wrapping FSDPModule and subsequent shards recompute + # against only-local DTensor shards — silent grad corruption. + # Single-GPU and DDP paths fall through to a no-op context manager. + with _defer_fsdp2_reshard(self): + for i, x_shard in enumerate(x_shards): + x_shard.requires_grad_(x_requires_grad) + + shard_offset = i * shard_step + x_shard.grad = ( + x_grad.view(-1) + .narrow(0, shard_offset, x_shard.numel()) + .view_as(x_shard) + ) + incoming_grad_shard = ( + incoming_grad.view(-1) + .narrow(0, shard_offset, x_shard.numel()) + .view_as(x_shard) + ) - with torch.enable_grad(): - output = fn(self, x_shard) - if is_tuple_output: - torch.autograd.backward(output[0], incoming_grad_shard) + with torch.enable_grad(): + output = fn(self, x_shard) + if is_tuple_output: + torch.autograd.backward(output[0], incoming_grad_shard) + else: + torch.autograd.backward(output, incoming_grad_shard) + + # Capture this shard's contribution into the per-param + # accumulator and clear ``.grad`` so the next shard starts + # from zero. Skip the dtype cast when the accumulator + # matches the param dtype (the default) — that cast was + # the per-shard HBM-bandwidth tax on the bf16 path. + for p in compute_params: + if p.grad is not None: + shard_grad = p.grad.detach() + if shard_grad.dtype != accum_grads[p].dtype: + shard_grad = shard_grad.to(accum_grads[p].dtype) + accum_grads[p].add_(shard_grad) + p.grad = None + + # Restore prior grad value (if any) and add the tiled contribution. + for p in compute_params: + tiled_contrib = accum_grads[p] + if tiled_contrib.dtype != p.dtype: + tiled_contrib = tiled_contrib.to(p.dtype) + if prev_grads[p] is None: + p.grad = tiled_contrib else: - torch.autograd.backward(output, incoming_grad_shard) - - # Clean up hooks - grad_accumulator.cleanup() - del grad_accumulator + p.grad = prev_grads[p] + tiled_contrib return (None, None, x_grad, None, None) class GradientAccumulator: """ - Manual gradient accumulator for TiledMLP with configurable precision - Accumulates in specified dtype and rescales the gradient at the end + Manual gradient accumulator for TiledMLP with configurable precision. + + .. note:: + The production TiledMLP backward (above) accumulates inline and + does not call this class — it is retained as a reference / opt-in + path for callers that want hook-based accumulation. The defaults + below match the inline path: param-dtype accumulator (matches + ``AccumulateGrad`` in the unsharded backward) and ``1.0`` per-shard + scaling (sequence-dim sharded grads are additive, not averaged). """ def __init__( @@ -202,11 +370,24 @@ def __init__( ): self.params = params self.total_shards = total_shards - self.grad_accumulation_dtype = dtype or torch.float32 + # Default to the param's own dtype to avoid the 2x parameter-side + # memory regression in bf16 MoE training where the accumulator + # shape ``[E, hidden, 2*intermediate]`` dominates. fp32 accumulation + # is opt-in via the ``dtype`` arg. + if dtype is not None: + self.grad_accumulation_dtype = dtype + elif params: + self.grad_accumulation_dtype = params[0].dtype + else: + self.grad_accumulation_dtype = torch.float32 self.accumulated_grads = {} self.hooks = [] self.lock = threading.Lock() - self.gradient_scale = 1.0 / total_shards + # Sequence-dim shards partition the per-token sum; their + # contributions are additive (``sum_t dL_t/dW``), not averaged. + # The previous ``1/total_shards`` scaling produced a mean and was + # a correctness bug for this sharding semantics. + self.gradient_scale = 1.0 # Initialize accumulated gradients in the specified dtype for param in self.params: @@ -226,17 +407,33 @@ def install_hooks(self, is_last_shard: bool): def create_hook(param): def hook(grad): with self.lock: - grad_to_accum_dtype = grad.to(self.grad_accumulation_dtype) - scaled_grad = grad_to_accum_dtype * self.gradient_scale + # Skip the dtype cast when the accumulator already + # matches the grad dtype (the default after the + # param-dtype change above) — the redundant cast was + # the per-shard HBM bandwidth tax called out in the + # tiled-MLP regression analysis. + if grad.dtype == self.grad_accumulation_dtype: + scaled_grad = ( + grad + if self.gradient_scale == 1.0 + else grad * self.gradient_scale + ) + else: + scaled_grad = ( + grad.to(self.grad_accumulation_dtype) * self.gradient_scale + ) if param in self.accumulated_grads: self.accumulated_grads[param] += scaled_grad else: self.accumulated_grads[param] = scaled_grad.clone() - # Only assign the averaged gradient on the last shard + # Only assign the accumulated gradient on the last shard if is_last_shard: - param.grad = self.accumulated_grads[param].to(param.dtype) + if self.accumulated_grads[param].dtype != param.dtype: + param.grad = self.accumulated_grads[param].to(param.dtype) + else: + param.grad = self.accumulated_grads[param] return param.grad return None diff --git a/src/axolotl/monkeypatch/tiled_mlp/patch.py b/src/axolotl/monkeypatch/tiled_mlp/patch.py index 23f48a1016..6842ba7980 100644 --- a/src/axolotl/monkeypatch/tiled_mlp/patch.py +++ b/src/axolotl/monkeypatch/tiled_mlp/patch.py @@ -11,90 +11,270 @@ LOG = get_logger(__name__) +# Suffixes used to discover MoE block classes inside +# ``transformers.models.{model_type}.modeling_{model_type}``. +# Order matters — preferred names come first. +_MOE_BLOCK_SUFFIXES = ("SparseMoeBlock", "MoeMLP", "MoE") -def patch_tiled_mlp(model_type, use_original_mlp=True, cfg_num_shards=None): + +def _resolve_moe_block_cls(module, model_cls_prefix): + """Return the MoE block class for the model module, or ``None`` if dense.""" + for suffix in _MOE_BLOCK_SUFFIXES: + cls = getattr(module, f"{model_cls_prefix}{suffix}", None) + if cls is not None: + return cls + return None + + +def _build_tiled_forward( + inner_forward, + model_type, + cfg_num_shards, + is_moe_block, +): + """Construct a ``tiled_mlp_forward`` closure. + + The returned forward shards inputs along the sequence dim and dispatches + to the correct :class:`torch.autograd.Function` implementation based on + the parallel-training backend in use. + + ``inner_forward`` is the un-tiled forward (either the dense MLP forward + or the MoE block's routing+expert forward — possibly a kernels-substituted + forward in the scattermoe-lora case). + """ from deepspeed.runtime.sequence_parallel.ulysses_sp import ( TiledMLP as DeepSpeedTiledMLP, ) from axolotl.monkeypatch.tiled_mlp.base import DeepSpeedTiledMLPMoE, TiledMLP + is_distributed = int(os.environ.get("WORLD_SIZE", 1)) > 1 + + def tiled_mlp_forward(self, x): + input_shape = x.shape + seqlen = input_shape[-2] + if cfg_num_shards is None: + # Target ~32K tokens per shard. The previous `ceil(seq / hidden)` + # heuristic produced only ~2K tokens/shard at long context, well + # below the MoE kernel's BLOCK_M sweet spot. An empirical sweep at + # seq ∈ {64K, 128K, 256K, 512K} showed 3.2× speed-up at 64–256K + # and 2.1× at 512K from raising per-shard tokens to ~32K, with + # only a modest peak-mem cost (~5–10 GiB extra at seq=256K) + # because the routed intermediate buffer dominates and scales + # linearly with per-shard tokens. Operators can override via + # cfg_num_shards for niche cases (smaller intermediate, larger + # top_k) where the default is wrong. + target_tokens_per_shard = 32768 + num_shards = max(1, math.ceil(seqlen / target_tokens_per_shard)) + if is_distributed: + num_shards_tensor = torch.tensor(num_shards, device=x.device) + dist.all_reduce(num_shards_tensor, op=dist.ReduceOp.MAX) + num_shards = num_shards_tensor.item() + else: + num_shards = cfg_num_shards + + if not self._compute_params: + self._compute_params = [p for p in self.parameters() if p.requires_grad] + + compute_params = self._compute_params + if not self._tiled_mlp_dist_impl: + uses_deepspeed = ( + self._compute_params + and any( + hasattr(p, "ds_id") or hasattr(p, "param_idx_in_group") + for p in self._compute_params + ) + ) or os.environ.get("ACCELERATE_USE_DEEPSPEED", "false") == "true" + + if uses_deepspeed: + # gpt_oss already used the MoE variant before this refactor; + # extend the same treatment to every MoE block, since they + # tend to return tuple outputs (hidden_states, router_logits) + # the way gpt_oss does. + if model_type == "gpt_oss" or is_moe_block: + self._tiled_mlp_dist_impl = DeepSpeedTiledMLPMoE + else: + self._tiled_mlp_dist_impl = DeepSpeedTiledMLP + else: + self._tiled_mlp_dist_impl = TiledMLP + + return self._tiled_mlp_dist_impl.apply( + inner_forward, + self, + x, + num_shards, + compute_params, + ) + + return tiled_mlp_forward + + +def _prepare_target_class(target_cls): + """Initialize the bookkeeping attrs the tiled forward expects.""" + target_cls._compute_params = [] + target_cls._tiled_mlp_dist_impl = None + + +def patch_tiled_mlp( + model_type, + use_original_mlp=True, + cfg_num_shards=None, + use_scattermoe=False, +): + """Install the class-level tiled MLP patch. + + For dense models this patches ``{prefix}MLP`` (falling back to + ``{prefix}TextMLP`` for multimodal wrappers). + + For MoE models with scattermoe-lora active, the MoE block class + (``{prefix}SparseMoeBlock`` / ``{prefix}MoeMLP`` / ``{prefix}MoE``) is the + one whose forward does routing + expert invocation, so we patch that. + Note that the ``kernels`` library installs scattermoe-lora's forward at + the *instance* level during ``model.kernelize()``, so the class-level + patch is shadowed at runtime. :func:`patch_tiled_mlp_moe_instances` is + the companion post-model-load step that re-wraps each MoE block instance + so the tiled forward runs on top of the kernels-installed forward. + """ + module_path = f"transformers.models.{model_type}.modeling_{model_type}" + model_cls_prefix, _ = get_causal_lm_model_cls_prefix(model_type) try: - # Dynamically import the module and MLP class - module_path = f"transformers.models.{model_type}.modeling_{model_type}" - model_cls_prefix, _ = get_causal_lm_model_cls_prefix(model_type) module = __import__(module_path, fromlist=[f"{model_cls_prefix}MLP"]) - # Some multimodal wrappers (e.g. Gemma 4) name the MLP class - # ``{prefix}TextMLP`` rather than ``{prefix}MLP`` because the - # language-side module is separated from the vision tower. Try - # both names before giving up. + except ImportError as e: + raise RuntimeError( + f"Could not import MLP class for model_type: {model_type}. Error: {str(e)}" + ) from e + + # MoE block patch path: only walk into this branch when scattermoe-lora + # is active. For non-scattermoe MoE models the dense MLP fallback applies + # — we do not auto-enable MoE-block tiling because each model family's + # block forward has different output-tuple semantics. + moe_block_cls = ( + _resolve_moe_block_cls(module, model_cls_prefix) if use_scattermoe else None + ) + + if moe_block_cls is not None: + original_forward = moe_block_cls.forward + tiled_forward = _build_tiled_forward( + inner_forward=original_forward, + model_type=model_type, + cfg_num_shards=cfg_num_shards, + is_moe_block=True, + ) + moe_block_cls.forward = tiled_forward + _prepare_target_class(moe_block_cls) + LOG.info( + "Successfully monkey-patched TiledMLP for model_type: " + f"{model_type} (MoE block: {moe_block_cls.__name__})" + ) + return + + # Dense MLP path (existing behavior). + try: mlp_cls = getattr( module, f"{model_cls_prefix}MLP", None, ) or getattr(module, f"{model_cls_prefix}TextMLP") + except AttributeError as e: + raise RuntimeError( + f"Could not import MLP class for model_type: {model_type}. Error: {str(e)}" + ) from e - if use_original_mlp: - mlp_forward = mlp_cls.forward - else: + if use_original_mlp: + mlp_forward = mlp_cls.forward + else: - def generic_mlp_forward(self_, hs): - return self_.down_proj( - self_.act_fn(self_.gate_proj(hs)) * self_.up_proj(hs) - ) + def generic_mlp_forward(self_, hs): + return self_.down_proj( + self_.act_fn(self_.gate_proj(hs)) * self_.up_proj(hs) + ) - mlp_forward = torch.compile(generic_mlp_forward) + mlp_forward = torch.compile(generic_mlp_forward) - is_distributed = int(os.environ.get("WORLD_SIZE", 1)) > 1 + tiled_forward = _build_tiled_forward( + inner_forward=mlp_forward, + model_type=model_type, + cfg_num_shards=cfg_num_shards, + is_moe_block=False, + ) + mlp_cls.forward = tiled_forward + _prepare_target_class(mlp_cls) + LOG.info(f"Successfully monkey-patched TiledMLP for model_type: {model_type}") - def tiled_mlp_forward(self, x): - input_shape = x.shape - seqlen = input_shape[-2] - hidden = input_shape[-1] - if cfg_num_shards is None: - num_shards = math.ceil(seqlen / hidden) - if is_distributed: - num_shards_tensor = torch.tensor(num_shards, device=x.device) - dist.all_reduce(num_shards_tensor, op=dist.ReduceOp.MAX) - num_shards = num_shards_tensor.item() - else: - num_shards = cfg_num_shards - - if not self._compute_params: - self._compute_params = [p for p in self.parameters() if p.requires_grad] - - compute_params = self._compute_params - if not self._tiled_mlp_dist_impl: - if ( - self._compute_params - and any( - hasattr(p, "ds_id") or hasattr(p, "param_idx_in_group") - for p in self._compute_params - ) - ) or os.environ.get("ACCELERATE_USE_DEEPSPEED", "false") == "true": - if model_type == "gpt_oss": - self._tiled_mlp_dist_impl = DeepSpeedTiledMLPMoE - else: - self._tiled_mlp_dist_impl = DeepSpeedTiledMLP - else: - self._tiled_mlp_dist_impl = TiledMLP - - down_res = self._tiled_mlp_dist_impl.apply( - mlp_forward, - self, - x, - num_shards, - compute_params, - ) - return down_res - mlp_cls.forward = tiled_mlp_forward - mlp_cls._compute_params = [] - mlp_cls._tiled_mlp_dist_impl = None +def patch_tiled_mlp_moe_instances( + model, + model_type, + cfg_num_shards=None, +): + """Re-wrap each MoE block instance's ``forward`` after model load. + + The ``kernels`` library installs scattermoe-lora's forward on each MoE + block *instance* during ``model.kernelize()`` (called inside + ``from_pretrained``). That instance-level binding shadows the class-level + patch :func:`patch_tiled_mlp` installs, so without this step tiling is + silently bypassed on every block. We capture each instance's current + forward (the kernels-installed one) and rebind the instance to a tiled + forward that delegates to it. + + Does nothing if no MoE block class exists for ``model_type`` or if + ``model`` contains no instances of it. + """ + from types import MethodType + + module_path = f"transformers.models.{model_type}.modeling_{model_type}" + model_cls_prefix, _ = get_causal_lm_model_cls_prefix(model_type) + try: + module = __import__(module_path, fromlist=[model_cls_prefix]) + except ImportError: + return 0 + + moe_block_cls = _resolve_moe_block_cls(module, model_cls_prefix) + if moe_block_cls is None: + return 0 + + wrapped = 0 + for sub in model.modules(): + if not isinstance(sub, moe_block_cls): + continue + # If there is no per-instance ``forward`` binding, the class-level + # tiled patch from ``patch_tiled_mlp`` is still active; nothing to do. + # Kernels (when scattermoe-lora kernelizes the model) installs a + # bound method on the instance, which shows up in ``__dict__``. + if "forward" not in sub.__dict__: + continue + # Snapshot the instance-level forward installed by kernels. + bound_forward = sub.__dict__["forward"] + # Convert bound method back to a plain function that takes (self, x) + # so the tiled wrapper can pass `self` through to it. + if hasattr(bound_forward, "__func__"): + inner_fn = bound_forward.__func__ + else: + # Instance-bound closure: wrap it so the (self, x) signature lines up. + def _adapt(orig): + def _call(self_, x): # noqa: ARG001 + return orig(x) + + return _call + + inner_fn = _adapt(bound_forward) + + tiled_forward = _build_tiled_forward( + inner_forward=inner_fn, + model_type=model_type, + cfg_num_shards=cfg_num_shards, + is_moe_block=True, + ) + # Each instance needs its own bookkeeping (compute_params, + # dist_impl) so concurrent forwards across blocks don't stomp. + sub._compute_params = [] + sub._tiled_mlp_dist_impl = None + sub.forward = MethodType(tiled_forward, sub) + wrapped += 1 + + if wrapped: LOG.info( - f"Successfully monkey-patched TiledMLP for model_type: {model_type}", + f"Successfully wrapped TiledMLP around {wrapped} {moe_block_cls.__name__} " + f"instance(s) for model_type: {model_type}" ) - except (ImportError, AttributeError) as e: - raise RuntimeError( - f"Could not import MLP class for model_type: {model_type}. Error: {str(e)}" - ) from e + return wrapped diff --git a/tests/e2e/multigpu/test_tiled_mlp_fsdp2.py b/tests/e2e/multigpu/test_tiled_mlp_fsdp2.py new file mode 100644 index 0000000000..decc5bf8fd --- /dev/null +++ b/tests/e2e/multigpu/test_tiled_mlp_fsdp2.py @@ -0,0 +1,306 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 +""" +FSDP2 + TiledMLP multi-rank correctness tests. + +Parity guard for the tiled MLP under FSDP2: tiled forward+backward +produces gradients within bf16 tolerance of the un-tiled FSDP2 +reference. The companion fix in +``axolotl.monkeypatch.tiled_mlp.base._defer_fsdp2_reshard`` is a +defensive measure that wraps the tile loop in +``FSDPModule.set_reshard_after_backward(False)`` — under the most +common setups (FSDP2 wraps the decoder layer; the post-backward +RegisterPostBackwardFunction fires only when the outer backward +reaches the layer's input, not mid-tile) the reshard does not fire +inside the tile loop, but the helper protects against setups where +the tile loop would otherwise race with FSDP2's per-module reshard. + +Run with:: + + torchrun --nproc-per-node=2 -m pytest tests/e2e/multigpu/test_tiled_mlp_fsdp2.py + +On a 1-GPU executor the tests skip with a clear reason. +""" + +import copy +import os +from types import SimpleNamespace + +import pytest +import torch +import torch.distributed as dist +import torch.nn as nn + +_TORCHRUN_LOCAL_RANK = os.environ.get("LOCAL_RANK") +_TORCHRUN_WORLD_SIZE = int(os.environ.get("WORLD_SIZE", "1")) + +pytestmark = [ + pytest.mark.skipif( + not torch.cuda.is_available(), + reason="CUDA not available", + ), + pytest.mark.skipif( + torch.cuda.device_count() < 2, + reason="Need >=2 GPUs for FSDP2 multi-rank tests", + ), + pytest.mark.skipif( + _TORCHRUN_LOCAL_RANK is None or _TORCHRUN_WORLD_SIZE < 2, + reason=( + "Multi-rank tests must be launched via " + "`torchrun --nproc-per-node=2 -m pytest `" + ), + ), +] + + +# ──────────────────────────── Process group ────────────────────────────── + + +@pytest.fixture(scope="module") +def dist_pg(): + """Initialize the default process group exactly once per worker.""" + if not dist.is_initialized(): + rank = int(os.environ["RANK"]) + torch.cuda.set_device(rank % torch.cuda.device_count()) + dist.init_process_group(backend="nccl") + yield + if dist.is_initialized(): + dist.barrier() + dist.destroy_process_group() + + +# ──────────────────────────── Helpers ──────────────────────────────────── + + +class TinyDenseMLP(nn.Module): + def __init__(self, hidden, intermediate, dtype=torch.bfloat16): + super().__init__() + self.gate_proj = nn.Linear(hidden, intermediate, bias=False, dtype=dtype) + self.up_proj = nn.Linear(hidden, intermediate, bias=False, dtype=dtype) + self.down_proj = nn.Linear(intermediate, hidden, bias=False, dtype=dtype) + self.act_fn = nn.SiLU() + + def forward(self, x): + return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) + + +def _full_state(mod): + """Materialize an FSDP2 module to full (unsharded) parameters.""" + from torch.distributed.tensor import DTensor + + out = {} + for name, p in mod.named_parameters(): + if isinstance(p, DTensor): + out[name] = p.full_tensor().detach().clone() + else: + out[name] = p.detach().clone() + return out + + +def _full_grads(mod): + """Gather param grads to full (unsharded) tensors.""" + from torch.distributed.tensor import DTensor + + out = {} + for name, p in mod.named_parameters(): + if p.grad is None: + continue + if isinstance(p.grad, DTensor): + out[name] = p.grad.full_tensor().detach().clone() + else: + out[name] = p.grad.detach().clone() + return out + + +def _make_seeded_mlp(hidden, intermediate, dtype, device): + torch.manual_seed(42) + mlp = TinyDenseMLP(hidden, intermediate, dtype=dtype).to(device) + return mlp + + +# ─────────────────────────── Dense regression guard ────────────────────── + + +def _install_tiled_forward(module, shards): + """Bind a tiled-MLP forward at the instance level. + + Mirrors what the patcher does in production: the FSDPModule's + ``__call__`` triggers FSDP2's pre-forward hooks (which unshard + parameters) before ``forward`` runs. By going through the wrapped + module's ``__call__`` rather than calling ``TiledMLP.apply`` directly + on sharded DTensor params, the tiling and FSDP2's parameter + materialization compose correctly. + """ + from types import MethodType + + from axolotl.monkeypatch.tiled_mlp.base import TiledMLP + + original_forward = type(module).forward + module._compute_params = [] # type: ignore[attr-defined] + + def tiled_forward(self, x): + if not self._compute_params: + self._compute_params = [p for p in self.parameters() if p.requires_grad] + return TiledMLP.apply( + original_forward, + self, + x, + shards, + self._compute_params, + ) + + module.forward = MethodType(tiled_forward, module) + + +def test_fsdp2_tiled_dense_mlp_parity(dist_pg): + """FSDP2 + tiled MLP must match FSDP2 + un-tiled MLP within bf16 tolerance. + + Wraps the MLP with ``fully_shard`` so it is itself an ``FSDPModule`` + — this is the scenario most likely to hit the post-backward race + that ``_defer_fsdp2_reshard`` protects against (per-tile inner + backwards would otherwise fire the FSDPModule's post-backward hook + mid-loop). The test passes whether or not the helper is in place + on the current PyTorch (2.11) release; treat it as a parity guard + that will catch breakage if FSDP2 ever shortens its reshard timing. + """ + from torch.distributed.fsdp import fully_shard + + device = torch.device(f"cuda:{torch.cuda.current_device()}") + hidden, intermediate = 64, 128 + seq = 64 + dtype = torch.bfloat16 + + # Two identical MLPs — one wrapped with FSDP2 only, one wrapped with + # FSDP2 *and* run through TiledMLP. Same initial weights. + mlp_ref = _make_seeded_mlp(hidden, intermediate, dtype, device) + mlp_tile = copy.deepcopy(mlp_ref) + fully_shard(mlp_ref) + fully_shard(mlp_tile) + _install_tiled_forward(mlp_tile, shards=4) + + torch.manual_seed(7 + dist.get_rank()) + x = torch.randn(1, seq, hidden, device=device, dtype=dtype) + g = torch.randn(1, seq, hidden, device=device, dtype=dtype) + + # Un-tiled reference + xr = x.clone().detach().requires_grad_(True) + yr = mlp_ref(xr) + yr.backward(g) + ref_grads = _full_grads(mlp_ref) + ref_dx = xr.grad.detach().clone() + + # Tiled run — must not corrupt gradients on FSDP2. + xt = x.clone().detach().requires_grad_(True) + yt = mlp_tile(xt) + yt.backward(g) + tile_grads = _full_grads(mlp_tile) + tile_dx = xt.grad.detach().clone() + + # Outputs match (this is just forward — should be tight). + assert torch.allclose(yr.detach(), yt.detach(), atol=1e-3, rtol=1e-2), ( + f"FSDP2 forward mismatch max={((yr - yt).abs().max()).item()}" + ) + # dX should match within bf16 tolerance. + assert torch.allclose(ref_dx, tile_dx, atol=1e-2, rtol=1e-2), ( + f"FSDP2 dX mismatch max={((ref_dx - tile_dx).abs().max()).item()}" + ) + # Param grads — the headline check for the reshard fix. + for name, gref in ref_grads.items(): + gtile = tile_grads[name] + rel = ( + (gref.float() - gtile.float()).norm() / (gref.float().norm() + 1e-6) + ).item() + assert rel < 5e-2, f"FSDP2 + tiled param-grad mismatch {name}: rel_err={rel}" + + +# ─────────────────────── scattermoe-lora regression guard ──────────────── + + +def test_fsdp2_tiled_scattermoe_block_parity(dist_pg): + """FSDP2 + tiled ScatterMoEGatedMLP block parity guard. + + Same shape of test as the dense case but routes through the + ScatterMoE forward. Skips if scattermoe_lora kernels are not + available in this env. + """ + try: + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + ScatterMoEGatedMLP, + ) + except ImportError: + pytest.skip("scattermoe_lora kernels not available") + + pytest.importorskip("triton") + + from torch.distributed.fsdp import fully_shard + + device = torch.device(f"cuda:{torch.cuda.current_device()}") + hidden, intermediate = 64, 128 + num_experts, top_k = 8, 2 + seq = 64 + dtype = torch.bfloat16 + + def _make_block(): + torch.manual_seed(42) + block = ScatterMoEGatedMLP() + router = SimpleNamespace() + router.layer = nn.Linear(hidden, num_experts, bias=False, dtype=dtype).to( + device + ) + router.top_k = top_k + router.num_experts = num_experts + block.router = router + in_w = nn.Parameter( + torch.randn( + num_experts, 2 * intermediate, hidden, dtype=dtype, device=device + ) + * 0.02 + ) + out_w = nn.Parameter( + torch.randn(num_experts, hidden, intermediate, dtype=dtype, device=device) + * 0.02 + ) + block.input_linear = nn.Module() + block.input_linear.register_parameter("weight", in_w) + block.output_linear = nn.Module() + block.output_linear.register_parameter("weight", out_w) + block.activation = nn.SiLU() + return block + + block_ref = _make_block() + block_tile = _make_block() + fully_shard(block_ref) + fully_shard(block_tile) + _install_tiled_forward(block_tile, shards=4) + + torch.manual_seed(7 + dist.get_rank()) + x = torch.randn(1, seq, hidden, device=device, dtype=dtype) + g = torch.randn(1, seq, hidden, device=device, dtype=dtype) + + xr = x.clone().detach().requires_grad_(True) + yr = block_ref(xr) + yr.backward(g) + ref_grads = _full_grads(block_ref) + ref_dx = xr.grad.detach().clone() + + xt = x.clone().detach().requires_grad_(True) + yt = block_tile(xt) + yt.backward(g) + tile_grads = _full_grads(block_tile) + tile_dx = xt.grad.detach().clone() + + def _rel(a, b): + return ((a.float() - b.float()).norm() / (b.float().norm() + 1e-6)).item() + + assert _rel(yt.detach(), yr.detach()) < 5e-2, ( + f"FSDP2 + tiled scattermoe forward rel_err={_rel(yt, yr)}" + ) + assert _rel(tile_dx, ref_dx) < 5e-2, ( + f"FSDP2 + tiled scattermoe dX rel_err={_rel(tile_dx, ref_dx)}" + ) + for name, gref in ref_grads.items(): + if name not in tile_grads: + continue + rel = _rel(tile_grads[name], gref) + assert rel < 5e-2, f"FSDP2 + tiled scattermoe param-grad {name} rel_err={rel}" diff --git a/tests/integrations/kernels/scattermoe_lora/test_shared_dequant_helper.py b/tests/integrations/kernels/scattermoe_lora/test_shared_dequant_helper.py new file mode 100644 index 0000000000..cf5d88c669 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_shared_dequant_helper.py @@ -0,0 +1,146 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +Parity tests for :func:`shared_dequant_across_shards`. + +The helper hoists the per-shard MXFP4 dequant out of the orthogonal +Strategy A path so that overlapping active-expert sets across shards +dequantize the union once instead of N times. The optimization is +only valid if a shard's slice of the union buffer is *byte-identical* +to what the per-shard ``selective_expert_weights`` call would have +produced. +""" + +import pytest +import torch + +from axolotl.integrations.kernels.libs.scattermoe_lora.selective_dequant import ( + get_active_experts, + selective_expert_weights, + shared_dequant_across_shards, +) + +torchao = pytest.importorskip("torchao") +from torchao.prototype.mx_formats.mx_tensor import MXTensor # noqa: E402 + +DEVICE = "cuda" +DTYPE = torch.bfloat16 + +pytestmark = pytest.mark.skipif( + not torch.cuda.is_available(), reason="CUDA required for MX kernels" +) + + +class _MockExperts: + """Same bare wrapper used by ``test_mxfp4_expert_weights.py``.""" + + def __init__(self, mx_param, num_experts): + self.gate_up_proj = mx_param + self.num_experts = num_experts + + +def _make_mxfp4(E, N, K, seed): + torch.manual_seed(seed) + W = torch.randn(E, N, K, device=DEVICE, dtype=DTYPE) + return MXTensor.to_mx(W, elem_dtype=torch.float4_e2m1fn_x2, block_size=32) + + +def _make_overlapping_shard_sei(E, num_shards, seed): + """Build N shards of ``sorted_expert_idxs`` with deliberate overlap. + + Each shard picks ~E/2 experts at random; with 4 shards the union is + typically ~E (full coverage) while the intersection is non-empty, + which is the regime the helper is intended to optimise. + """ + torch.manual_seed(seed) + sei = [] + for _ in range(num_shards): + # Each shard sees ~E/2 distinct experts repeated a few times to + # mimic top-k routing. + chosen = torch.randperm(E, device=DEVICE)[: max(2, E // 2)] + # Repeat each id ~3x and sort so the tensor satisfies the + # ``sorted_expert_idxs`` contract. + repeated = chosen.repeat_interleave(3) + sei.append(torch.sort(repeated).values) + return sei + + +def test_shared_dequant_matches_per_shard_bitwise(): + """Union dequant + index gather == per-shard selective dequant, bitwise. + + Uses N=4 shards over E=16 experts with deliberate overlap (each shard + picks ~half the experts, union covers most of E). The helper must + yield the exact same compact buffer that a per-shard + ``selective_expert_weights`` call would have produced for that shard. + """ + E, N, K = 16, 128, 256 + num_shards = 4 + mx = _make_mxfp4(E, N, K, seed=13) + experts = _MockExperts(mx, E) + + sei_per_shard = _make_overlapping_shard_sei(E, num_shards, seed=13) + # Sanity: at least one pair of shards must overlap for this test to + # actually exercise the dedup path. + actives = [get_active_experts(sei, E) for sei in sei_per_shard] + union = torch.unique(torch.cat(actives)) + sum_per_shard = sum(a.numel() for a in actives) + assert sum_per_shard > union.numel(), ( + "shard active sets must overlap to exercise the shared-dequant path" + ) + + union_active, union_buf, shard_into_union = shared_dequant_across_shards( + experts, "gate_up_proj", sei_per_shard, E + ) + + assert torch.equal(union_active, union) + assert union_buf.shape == (union.numel(), N, K) + + for i, sei in enumerate(sei_per_shard): + active_i = get_active_experts(sei, E) + reference = selective_expert_weights(experts, "gate_up_proj", active_i) + shared_slice = union_buf.index_select(0, shard_into_union[i]) + assert torch.equal(shared_slice, reference), ( + f"shard {i}: max abs diff = {(shared_slice - reference).abs().max().item()}" + ) + + +def test_shared_dequant_disjoint_shards(): + """When shards do NOT overlap, the helper still produces the right + union and the per-shard slices remain bitwise identical.""" + E, N, K = 12, 64, 128 + mx = _make_mxfp4(E, N, K, seed=21) + experts = _MockExperts(mx, E) + + # Two shards splitting the expert ids into disjoint halves. + halves = torch.arange(E, device=DEVICE).chunk(2) + sei_per_shard = [torch.sort(h.repeat_interleave(2)).values for h in halves] + + union_active, union_buf, shard_into_union = shared_dequant_across_shards( + experts, "gate_up_proj", sei_per_shard, E + ) + assert union_active.numel() == E + + for i, sei in enumerate(sei_per_shard): + active_i = get_active_experts(sei, E) + reference = selective_expert_weights(experts, "gate_up_proj", active_i) + shared_slice = union_buf.index_select(0, shard_into_union[i]) + assert torch.equal(shared_slice, reference) + + +def test_shared_dequant_single_shard_noop(): + """N=1 should reduce to the per-shard path: union == only active set.""" + E, N, K = 8, 32, 64 + mx = _make_mxfp4(E, N, K, seed=5) + experts = _MockExperts(mx, E) + + sei = torch.tensor([0, 0, 2, 2, 5, 5], device=DEVICE, dtype=torch.long) + union_active, union_buf, shard_into_union = shared_dequant_across_shards( + experts, "gate_up_proj", [sei], E + ) + active = get_active_experts(sei, E) + reference = selective_expert_weights(experts, "gate_up_proj", active) + assert torch.equal(union_active, active) + assert torch.equal(union_buf, reference) + assert torch.equal(union_buf.index_select(0, shard_into_union[0]), reference) diff --git a/tests/integrations/monkeypatch/__init__.py b/tests/integrations/monkeypatch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/monkeypatch/test_tiled_mlp_moe.py b/tests/integrations/monkeypatch/test_tiled_mlp_moe.py new file mode 100644 index 0000000000..6dab0df109 --- /dev/null +++ b/tests/integrations/monkeypatch/test_tiled_mlp_moe.py @@ -0,0 +1,474 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 +""" +Single-GPU correctness tests for the TiledMLP autograd function under both +dense and MoE block forwards. Synthetic-shape modules only; no real +transformers checkpoints are loaded. +""" + +import copy +from types import SimpleNamespace + +import pytest +import torch +import torch.nn as nn +import torch.nn.functional as F + + +def _requires_cuda(): + return pytest.mark.skipif( + not torch.cuda.is_available(), reason="CUDA not available" + ) + + +pytestmark = _requires_cuda() + +DEVICE = "cuda" + + +# ────────────────────────────── Helpers ────────────────────────────── + + +class TinyDenseMLP(nn.Module): + """LlamaMLP-shape: gate * up -> down, no bias, silu activation.""" + + def __init__(self, hidden, intermediate, dtype=torch.float32): + super().__init__() + self.gate_proj = nn.Linear(hidden, intermediate, bias=False, dtype=dtype) + self.up_proj = nn.Linear(hidden, intermediate, bias=False, dtype=dtype) + self.down_proj = nn.Linear(intermediate, hidden, bias=False, dtype=dtype) + self.act_fn = nn.SiLU() + + def forward(self, x): + return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) + + +class TinyMoEBlock(nn.Module): + """Hand-rolled MoE: top-k softmax router + per-expert SwiGLU MLPs. + + Stays intentionally simple so the test exercises sharding semantics + without dragging in transformers, peft, or kernel libs. + """ + + def __init__(self, hidden, intermediate, num_experts, top_k, dtype=torch.float32): + super().__init__() + self.hidden = hidden + self.intermediate = intermediate + self.num_experts = num_experts + self.top_k = top_k + self.gate = nn.Linear(hidden, num_experts, bias=False, dtype=dtype) + # Per-expert SwiGLU weights packed as 3D tensors. + self.gate_proj = nn.Parameter( + torch.randn(num_experts, hidden, intermediate, dtype=dtype) * 0.02 + ) + self.up_proj = nn.Parameter( + torch.randn(num_experts, hidden, intermediate, dtype=dtype) * 0.02 + ) + self.down_proj = nn.Parameter( + torch.randn(num_experts, intermediate, hidden, dtype=dtype) * 0.02 + ) + + def forward(self, x): + bsz, seq, h = x.shape + flat = x.reshape(-1, h) + logits = self.gate(flat) + weights = F.softmax(logits, dim=-1, dtype=torch.float32) + top_w, top_i = torch.topk(weights, self.top_k, dim=-1) + top_w = top_w / top_w.sum(dim=-1, keepdim=True) + top_w = top_w.to(flat.dtype) + + out = torch.zeros_like(flat) + for e in range(self.num_experts): + mask = top_i == e + if not mask.any(): + continue + # tokens routed to expert e (with their per-slot weight) + token_rows, slot_idx = mask.nonzero(as_tuple=True) + xe = flat[token_rows] + we = top_w[token_rows, slot_idx].unsqueeze(-1) + gate = xe @ self.gate_proj[e] + up = xe @ self.up_proj[e] + h_e = F.silu(gate) * up + ye = h_e @ self.down_proj[e] + out.index_add_(0, token_rows, we * ye) + return out.reshape(bsz, seq, h) + + +def _clone_module(mod): + """Deep copy + detach + re-attach to autograd to compare two runs.""" + cloned = copy.deepcopy(mod) + return cloned + + +def _grad_dict(mod): + return { + n: p.grad.detach().clone() + for n, p in mod.named_parameters() + if p.grad is not None + } + + +def _run_untiled(mod, x): + x = x.clone().detach().requires_grad_(True) + y = mod(x) + g = torch.randn_like(y) + y.backward(g) + return y.detach().clone(), x.grad.detach().clone(), _grad_dict(mod), g + + +def _run_tiled(mod, x, upstream_grad, shards): + """Re-run forward+backward but routed through TiledMLP.""" + from axolotl.monkeypatch.tiled_mlp.base import TiledMLP + + # Re-fetch fn that takes (self, x) — matches what the patcher passes. + forward_fn = type(mod).forward + + x = x.clone().detach().requires_grad_(True) + compute_params = [p for p in mod.parameters() if p.requires_grad] + y = TiledMLP.apply(forward_fn, mod, x, shards, compute_params) + if isinstance(y, tuple): # MoE block forwards may return tuples + y = y[0] + y.backward(upstream_grad) + return y.detach().clone(), x.grad.detach().clone(), _grad_dict(mod) + + +# ────────────────────────────── Dense parity ────────────────────────────── + + +def test_tiled_dense_mlp_parity_fp32(): + """Dense LlamaMLP-shape: tiled vs un-tiled must match closely.""" + torch.manual_seed(0) + hidden, intermediate, seq = 64, 128, 64 + mlp_ref = TinyDenseMLP(hidden, intermediate).to(DEVICE) + mlp_tile = _clone_module(mlp_ref) + + # ``TiledMLP``'s backward narrows into a flattened ``x_grad`` buffer + # using offsets along dim 1 only — sequence-packed inputs (batch=1) + # are the supported shape; multi-batch tensors aren't contiguous in + # the way the narrow assumes. Production inputs from transformers + # are batch=1 after sequence packing, so this matches reality. + x = torch.randn(1, seq, hidden, device=DEVICE) + y_ref, dx_ref, gp_ref, upstream = _run_untiled(mlp_ref, x) + y_tile, dx_tile, gp_tile = _run_tiled(mlp_tile, x, upstream, shards=4) + + # FMA reordering across shards introduces sub-eps noise in fp32; allow + # a small tolerance on outputs and grads. + assert torch.allclose(y_ref, y_tile, atol=1e-5, rtol=1e-5), ( + f"forward mismatch max={((y_ref - y_tile).abs().max()).item()}" + ) + assert torch.allclose(dx_ref, dx_tile, atol=1e-5, rtol=1e-5), ( + f"dX mismatch max={((dx_ref - dx_tile).abs().max()).item()}" + ) + for name in gp_ref: + diff = (gp_ref[name] - gp_tile[name]).abs().max().item() + assert diff < 1e-5, f"param-grad mismatch {name}: max={diff}" + + +# ────────────────────────────── MoE parity ────────────────────────────── + + +def test_tiled_moe_block_parity_fp32(): + """Hand-rolled MoE block: tiled vs un-tiled fp32 parity.""" + torch.manual_seed(1) + hidden, intermediate, seq = 64, 128, 64 + moe_ref = TinyMoEBlock(hidden, intermediate, num_experts=8, top_k=2).to(DEVICE) + moe_tile = _clone_module(moe_ref) + + x = torch.randn(1, seq, hidden, device=DEVICE) + y_ref, dx_ref, gp_ref, upstream = _run_untiled(moe_ref, x) + y_tile, dx_tile, gp_tile = _run_tiled(moe_tile, x, upstream, shards=4) + + # The MoE forward involves index_add and routing, which is not + # numerically deterministic across different batch sizes for fp32 in + # all setups — but at the synthetic small scale we expect tight match. + assert torch.allclose(y_ref, y_tile, atol=1e-5, rtol=1e-5), ( + f"MoE forward mismatch max={((y_ref - y_tile).abs().max()).item()}" + ) + assert torch.allclose(dx_ref, dx_tile, atol=1e-5, rtol=1e-5), ( + f"MoE dX mismatch max={((dx_ref - dx_tile).abs().max()).item()}" + ) + for name in gp_ref: + diff = (gp_ref[name] - gp_tile[name]).abs().max().item() + assert diff < 1e-5, f"MoE param-grad mismatch {name}: max={diff}" + + +# ─────────────────────── scattermoe-lora + tiled parity ─────────────────── + + +def _build_scattermoe_block(hidden, intermediate, num_experts, top_k, dtype, device): + """Build a minimal :class:`ScatterMoEGatedMLP`-compatible module. + + Attributes are populated to match what :meth:`ScatterMoEGatedMLP.forward` + expects (``router``, ``input_linear``, ``output_linear``, ``activation``). + """ + try: + from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( + ScatterMoEGatedMLP, + ) + except ImportError: + pytest.skip("scattermoe_lora kernels not available") + + block = ScatterMoEGatedMLP() + # router: a Linear-shaped router with top_k / num_experts attrs. + router = SimpleNamespace() + router.layer = nn.Linear(hidden, num_experts, bias=False, dtype=dtype).to(device) + router.top_k = top_k + router.num_experts = num_experts + block.router = router + # input_linear and output_linear store 3D weights: [E, *, *]. + in_weight = nn.Parameter( + torch.randn(num_experts, 2 * intermediate, hidden, dtype=dtype, device=device) + * 0.02 + ) + out_weight = nn.Parameter( + torch.randn(num_experts, hidden, intermediate, dtype=dtype, device=device) + * 0.02 + ) + block.input_linear = nn.Module() + block.input_linear.weight = in_weight + block.output_linear = nn.Module() + block.output_linear.weight = out_weight + block.activation = nn.SiLU() + # Register the params so .parameters() walks them. + block.input_linear.register_parameter("weight", in_weight) + block.output_linear.register_parameter("weight", out_weight) + return block + + +def test_tiled_scattermoe_gated_mlp_parity(): + """ScatterMoEGatedMLP: tiled vs un-tiled fwd+bwd parity in bf16. + + Uses the same tolerance scale as ``tests/integrations/test_scattermoe_lora_kernels.py`` + (norm-relative error < 1% for weight grads is the established bar there + given the bf16 + tiled reduction order differences). + """ + pytest.importorskip("triton") + torch.manual_seed(2) + hidden, intermediate = 64, 128 + num_experts, top_k = 8, 2 + seq = 64 + dtype = torch.bfloat16 + + block_ref = _build_scattermoe_block( + hidden, intermediate, num_experts, top_k, dtype, DEVICE + ) + block_tile = copy.deepcopy(block_ref) + + x = torch.randn(1, seq, hidden, device=DEVICE, dtype=dtype) + y_ref, dx_ref, gp_ref, upstream = _run_untiled(block_ref, x) + y_tile, dx_tile, gp_tile = _run_tiled(block_tile, x, upstream, shards=4) + + def _rel(a, b): + return ((a.float() - b.float()).norm() / (b.float().norm() + 1e-6)).item() + + assert _rel(y_tile, y_ref) < 1e-2, ( + f"scattermoe forward rel_err={_rel(y_tile, y_ref)}" + ) + assert _rel(dx_tile, dx_ref) < 1e-2, ( + f"scattermoe dX rel_err={_rel(dx_tile, dx_ref)}" + ) + for name in gp_ref: + if name not in gp_tile: + continue + rel = _rel(gp_tile[name], gp_ref[name]) + assert rel < 1e-2, f"scattermoe param-grad {name} rel_err={rel}" + + +# ─────────────────── Patcher: MoE block discovery & dispatch ──────────────── + + +def test_resolve_moe_block_cls_picks_first_available(): + """The patcher walks the suffix list in order; the first hit wins.""" + from axolotl.monkeypatch.tiled_mlp.patch import _resolve_moe_block_cls + + module = SimpleNamespace( + FooMoE=object, + FooMoeMLP=object, # would also match but later in the list + ) + cls = _resolve_moe_block_cls(module, "Foo") + assert cls is module.FooMoeMLP, "MoeMLP should be preferred over MoE" + + +def test_resolve_moe_block_cls_returns_none_for_dense_model(): + from axolotl.monkeypatch.tiled_mlp.patch import _resolve_moe_block_cls + + module = SimpleNamespace(FooMLP=object) + assert _resolve_moe_block_cls(module, "Foo") is None + + +# ─────────────── Grad parity under non-uniform per-token loss weights ───────── +# +# Sequence-dim sharding makes per-shard parameter-grads additive: the full +# batch gradient is the SUM of shard contributions, not the mean. If the +# tiled backward ever scaled by ``1/total_shards`` (the historical +# ``GradientAccumulator.gradient_scale``), per-shard non-uniform weights +# would make the mean visibly diverge from the un-tiled reference — uniform +# loss weights can mask the bug because the per-shard means happen to add +# up to a related-magnitude value. These tests exercise multiple +# ``shards ∈ {1, 2, 4}`` with deliberately non-uniform per-token weights +# so a regression in the scaling semantics fails loudly. + + +def _run_untiled_with_upstream(mod, x, upstream): + """Un-tiled fwd+bwd given a fixed upstream grad.""" + x = x.clone().detach().requires_grad_(True) + y = mod(x) + y.backward(upstream) + return y.detach().clone(), x.grad.detach().clone(), _grad_dict(mod) + + +@pytest.mark.parametrize("shards", [1, 2, 4]) +def test_tiled_dense_mlp_grad_parity_nonuniform_weights(shards): + """Dense MLP: tiled vs un-tiled grad parity with non-uniform per-token weights. + + The upstream grad's magnitude varies per token (non-uniform loss weights), + so each shard contributes a distinct fraction of the total parameter grad. + A mean-vs-sum bug shows up as a ``shards``-dependent scaling error. + """ + torch.manual_seed(100 + shards) + hidden, intermediate = 64, 128 + seq = 128 + mlp_ref = TinyDenseMLP(hidden, intermediate).to(DEVICE) + mlp_tile = _clone_module(mlp_ref) + + x = torch.randn(1, seq, hidden, device=DEVICE) + # Non-uniform per-token weights make per-shard grad contributions + # distinct, exposing any incorrect averaging. + per_token_w = torch.linspace(0.1, 3.0, seq, device=DEVICE).view(1, seq, 1) + upstream = torch.randn(1, seq, hidden, device=DEVICE) * per_token_w + + y_ref, dx_ref, gp_ref = _run_untiled_with_upstream(mlp_ref, x, upstream) + y_tile, dx_tile, gp_tile = _run_tiled(mlp_tile, x, upstream, shards=shards) + + assert torch.allclose(y_ref, y_tile, atol=1e-5, rtol=1e-5), ( + f"shards={shards}: forward mismatch max={((y_ref - y_tile).abs().max()).item()}" + ) + assert torch.allclose(dx_ref, dx_tile, atol=1e-5, rtol=1e-5), ( + f"shards={shards}: dX mismatch max={((dx_ref - dx_tile).abs().max()).item()}" + ) + for name in gp_ref: + diff = (gp_ref[name] - gp_tile[name]).abs().max().item() + ref_norm = gp_ref[name].abs().max().item() + 1e-8 + # Tight bound in fp32; rel error must be tiny so a 1/N or N + # scaling error (which would give 25%-400% relative drift) is + # impossible to miss. + assert diff / ref_norm < 1e-4, ( + f"shards={shards}: param-grad {name} rel_err={diff / ref_norm}" + ) + + +@pytest.mark.parametrize("shards", [1, 2, 4]) +def test_tiled_moe_grad_parity_nonuniform_weights(shards): + """MoE block: tiled vs un-tiled grad parity with non-uniform per-token weights.""" + torch.manual_seed(200 + shards) + hidden, intermediate = 64, 128 + seq = 128 + moe_ref = TinyMoEBlock(hidden, intermediate, num_experts=8, top_k=2).to(DEVICE) + moe_tile = _clone_module(moe_ref) + + x = torch.randn(1, seq, hidden, device=DEVICE) + per_token_w = torch.linspace(0.1, 3.0, seq, device=DEVICE).view(1, seq, 1) + upstream = torch.randn(1, seq, hidden, device=DEVICE) * per_token_w + + y_ref, dx_ref, gp_ref = _run_untiled_with_upstream(moe_ref, x, upstream) + y_tile, dx_tile, gp_tile = _run_tiled(moe_tile, x, upstream, shards=shards) + + assert torch.allclose(y_ref, y_tile, atol=1e-5, rtol=1e-5), ( + f"shards={shards}: MoE forward mismatch " + f"max={((y_ref - y_tile).abs().max()).item()}" + ) + assert torch.allclose(dx_ref, dx_tile, atol=1e-5, rtol=1e-5), ( + f"shards={shards}: MoE dX mismatch " + f"max={((dx_ref - dx_tile).abs().max()).item()}" + ) + for name in gp_ref: + diff = (gp_ref[name] - gp_tile[name]).abs().max().item() + ref_norm = gp_ref[name].abs().max().item() + 1e-8 + assert diff / ref_norm < 1e-4, ( + f"shards={shards}: MoE param-grad {name} rel_err={diff / ref_norm}" + ) + + +@pytest.mark.parametrize("shards", [1, 2, 4]) +def test_tiled_dense_mlp_grad_parity_bf16(shards): + """Dense MLP: bf16 grad parity at the param dtype (no fp32 accumulator). + + Guards the default param-dtype accumulator path against regression. + bf16 reduction order across shards means we use a relative tolerance + rather than bitwise equality. + """ + torch.manual_seed(300 + shards) + hidden, intermediate = 64, 128 + seq = 128 + dtype = torch.bfloat16 + mlp_ref = TinyDenseMLP(hidden, intermediate, dtype=dtype).to(DEVICE) + mlp_tile = _clone_module(mlp_ref) + + x = torch.randn(1, seq, hidden, device=DEVICE, dtype=dtype) + per_token_w = torch.linspace(0.1, 3.0, seq, device=DEVICE).view(1, seq, 1).to(dtype) + upstream = (torch.randn(1, seq, hidden, device=DEVICE) * per_token_w).to(dtype) + + y_ref, dx_ref, gp_ref = _run_untiled_with_upstream(mlp_ref, x, upstream) + y_tile, dx_tile, gp_tile = _run_tiled(mlp_tile, x, upstream, shards=shards) + + def _rel(a, b): + return ((a.float() - b.float()).norm() / (b.float().norm() + 1e-6)).item() + + assert _rel(y_tile, y_ref) < 5e-3, ( + f"shards={shards}: bf16 forward rel_err={_rel(y_tile, y_ref)}" + ) + assert _rel(dx_tile, dx_ref) < 5e-3, ( + f"shards={shards}: bf16 dX rel_err={_rel(dx_tile, dx_ref)}" + ) + for name in gp_ref: + rel = _rel(gp_tile[name], gp_ref[name]) + # Tight bound — a 1/N scaling bug would put rel_err ≈ (N-1)/N, + # which is far above this threshold for any N ≥ 2. + assert rel < 5e-3, f"shards={shards}: bf16 param-grad {name} rel_err={rel}" + + +def test_tiled_grad_accumulator_dtype_matches_param_dtype(): + """Regression guard: TiledMLP backward should accumulate at param dtype + by default (not fp32), so the on-the-fly accumulator does not double + the parameter-side memory footprint in bf16 training. + + We snapshot the accumulator dtype by patching ``torch.zeros_like`` to + record the dtype of zero-tensors allocated for compute params during + backward. The assertion is that none of those allocations request + fp32 when the params are bf16. + """ + from axolotl.monkeypatch.tiled_mlp.base import TiledMLP + + torch.manual_seed(42) + dtype = torch.bfloat16 + mlp = TinyDenseMLP(64, 128, dtype=dtype).to(DEVICE) + compute_params = [p for p in mlp.parameters() if p.requires_grad] + param_ids = {id(p) for p in compute_params} + + x = torch.randn(1, 64, 64, device=DEVICE, dtype=dtype) + upstream = torch.randn(1, 64, 64, device=DEVICE, dtype=dtype) + + real_zeros_like = torch.zeros_like + allocated_dtypes: list[torch.dtype] = [] + + def spy_zeros_like(t, *args, **kwargs): + # Only record allocations whose shape matches one of the + # compute params (the accumulator buffers we care about). + if id(t) in param_ids: + allocated_dtypes.append(kwargs.get("dtype", t.dtype)) + return real_zeros_like(t, *args, **kwargs) + + x_req = x.clone().detach().requires_grad_(True) + torch.zeros_like = spy_zeros_like + try: + y = TiledMLP.apply(type(mlp).forward, mlp, x_req, 4, compute_params) + y.backward(upstream) + finally: + torch.zeros_like = real_zeros_like + + assert allocated_dtypes, "expected accumulator allocations to be observed" + # Default path must NOT pre-allocate fp32 buffers when params are bf16. + assert all(d == dtype for d in allocated_dtypes), ( + f"expected accumulator dtype == {dtype}, got {allocated_dtypes}" + ) From 135c4ee58fff98bf34c58c135f4590f6dcbfc4af Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 28 May 2026 11:24:27 -0400 Subject: [PATCH 1328/1405] fix modal call with explicit module flag for future deprecation (#3668) [skip ci] --- .github/workflows/tests-nightly.yml | 4 ++-- .github/workflows/tests.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 1802b63056..1be17317ee 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -160,7 +160,7 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | - modal run cicd.e2e_tests + modal run -m cicd.e2e_tests docker-e2e-multigpu-tests: if: github.repository_owner == 'axolotl-ai-cloud' # this job needs to be run on self-hosted GPU runners... @@ -203,4 +203,4 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | - modal run cicd.multigpu + modal run -m cicd.multigpu diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6b298ade02..8c97f8b40e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -302,7 +302,7 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | - modal run cicd.e2e_tests + modal run -m cicd.e2e_tests docker-e2e-tests: if: > @@ -364,7 +364,7 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | - modal run cicd.e2e_tests + modal run -m cicd.e2e_tests docker-e2e-cleanup: runs-on: [self-hosted, modal] @@ -404,4 +404,4 @@ jobs: echo "N_GPUS=${{ matrix.num_gpus }}" >> $GITHUB_ENV - name: Run tests job on Modal run: | - modal run cicd.cleanup + modal run -m cicd.cleanup From 91adc268d44affe3ca3cc5b5f0d94d2740432900 Mon Sep 17 00:00:00 2001 From: Sung Hyun Cho Date: Fri, 29 May 2026 00:25:18 +0900 Subject: [PATCH 1329/1405] fix: respect has_aux contract in KD liger chunked loss (#3660) LigerFusedLinearKLTopKLogprobFunction.forward's loss_fn_for_grad returned (soft_loss, ce_loss) directly to torch.func.grad_and_value(has_aux=True), which treats only the first element as the grad target. CE was silently dropped from the backward graph: CE-only training had grad_norm=0 every step, and KD-mix training updated parameters with KD-only gradients despite reported loss showing both terms. Combine the two losses inside loss_fn_for_grad so both contribute to backward, and keep (soft_loss, ce_loss) as aux for reporting. Outer accumulators, temperature scaling, and the final reported loss formula are unchanged. Adds regression tests in tests/integrations/test_kd_liger.py. --- src/axolotl/integrations/kd/kernels/liger.py | 15 +- tests/integrations/test_kd_liger.py | 149 +++++++++++++++++++ 2 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 tests/integrations/test_kd_liger.py diff --git a/src/axolotl/integrations/kd/kernels/liger.py b/src/axolotl/integrations/kd/kernels/liger.py index 61ef3e10a0..a150420855 100644 --- a/src/axolotl/integrations/kd/kernels/liger.py +++ b/src/axolotl/integrations/kd/kernels/liger.py @@ -225,7 +225,7 @@ def loss_fn_for_grad( _target_mask_chunk, _true_labels_chunk, ): - return cls._compute_loss_kl_topk( + soft_loss, ce_loss = cls._compute_loss_kl_topk( student_input_chunk=_student_input_chunk, student_weight=_student_lm_head_weight, target_token_ids_chunk=_target_token_ids_chunk, @@ -242,6 +242,11 @@ def loss_fn_for_grad( beta=beta, normalize_topk=normalize_topk, ) + # has_aux=True: first return is the differentiated scalar, rest is aux. + # Combine here so both terms contribute to backward; aux carries the + # unweighted soft/ce values for reporting. + combined = weight_soft_loss * soft_loss + weight_hard_loss * ce_loss + return combined, (soft_loss, ce_loss) def accumulate_chunk_grads( student_input_chunk_ac, @@ -254,7 +259,9 @@ def accumulate_chunk_grads( if student_lm_head_bias is not None: ( (chunk_grad_input, chunk_grad_weight, chunk_grad_bias), - (chunk_kd_loss, chunk_ce_loss), + # grad_and_value(has_aux=True) returns (grads, (value, aux)); + # value = combined scalar, aux = (soft_loss, ce_loss) from loss_fn_for_grad. + (_, (chunk_kd_loss, chunk_ce_loss)), ) = torch.func.grad_and_value( loss_fn_for_grad, argnums=(0, 1, 2), has_aux=True )( @@ -271,7 +278,9 @@ def accumulate_chunk_grads( argnums_for_grad = (0, 1) # Differentiate wrt input_chunk, weight ( (chunk_grad_input, chunk_grad_weight), # No grad for bias - (chunk_kd_loss, chunk_ce_loss), + # grad_and_value(has_aux=True) returns (grads, (value, aux)); + # value = combined scalar, aux = (soft_loss, ce_loss) from loss_fn_for_grad. + (_, (chunk_kd_loss, chunk_ce_loss)), ) = torch.func.grad_and_value( loss_fn_for_grad, argnums=argnums_for_grad, has_aux=True )( diff --git a/tests/integrations/test_kd_liger.py b/tests/integrations/test_kd_liger.py new file mode 100644 index 0000000000..f4a1d07563 --- /dev/null +++ b/tests/integrations/test_kd_liger.py @@ -0,0 +1,149 @@ +""" +Unit tests for LigerFusedLinearKLTopKLogprobFunction autograd contract. + +Regression tests for the has_aux=True contract violation in +LigerFusedLinearKLTopKLogprobFunction: CE loss must contribute to the +backward graph, not be silently dropped as aux. +""" + +import torch + +from axolotl.integrations.kd.kernels.liger import LigerFusedLinearKLTopKLogprobLoss + +BATCH_SIZE = 1 +SEQ_LEN = 4 +VOCAB_SIZE = 16 +HIDDEN_DIM = 8 +TOP_K = 4 + + +def make_inputs(seed: int): + torch.manual_seed(seed) + student_hidden_states = torch.randn( + BATCH_SIZE, SEQ_LEN, HIDDEN_DIM, dtype=torch.float32, requires_grad=True + ) + lm_head_weight = torch.randn( + VOCAB_SIZE, HIDDEN_DIM, dtype=torch.float32, requires_grad=True + ) + target_token_ids = torch.randint(0, VOCAB_SIZE, (BATCH_SIZE, SEQ_LEN, TOP_K)) + target_logprobs = torch.log_softmax(torch.randn(BATCH_SIZE, SEQ_LEN, TOP_K), dim=-1) + target_mask = torch.ones(BATCH_SIZE, SEQ_LEN, TOP_K, dtype=torch.bool) + true_labels = torch.randint(0, VOCAB_SIZE, (BATCH_SIZE, SEQ_LEN)) + # Set an interior position to ignore_index. After the left-shift inside the + # kernel (true_labels = pad(...); true_labels[:, 1:]), index [0, 2] in the + # input becomes index 1 in the flattened CE labels, so this exercises + # ignore_index handling in F.cross_entropy. + true_labels[0, 2] = -100 + return { + "student_hidden_states": student_hidden_states, + "lm_head_weight": lm_head_weight, + "target_token_ids": target_token_ids, + "target_logprobs": target_logprobs, + "target_mask": target_mask, + "true_labels": true_labels, + } + + +def test_ce_only_gradient_flows_to_student_hidden_states(): + """Regression: with weight_soft_loss=0, weight_hard_loss=1 the CE gradient must reach inputs. + + Pre-fix, has_aux=True dropped CE from the backward graph and student_input.grad was all zero. + """ + inputs = make_inputs(seed=0) + loss_fn = LigerFusedLinearKLTopKLogprobLoss( + weight_hard_loss=1.0, + weight_soft_loss=0.0, + temperature=1.0, + beta=0.0, + compiled=False, + chunk_size=2, + compute_ce_loss=True, + ) + loss = loss_fn( + inputs["lm_head_weight"], + inputs["student_hidden_states"], + inputs["target_token_ids"], + inputs["target_logprobs"], + inputs["target_mask"], + inputs["true_labels"], + ) + loss.backward() + + assert inputs["student_hidden_states"].grad is not None + assert torch.isfinite(inputs["student_hidden_states"].grad).all() + assert inputs["student_hidden_states"].grad.abs().sum().item() > 0 + + assert inputs["lm_head_weight"].grad is not None + assert torch.isfinite(inputs["lm_head_weight"].grad).all() + assert inputs["lm_head_weight"].grad.abs().sum().item() > 0 + + +def test_kd_mix_gradient_changes_when_ce_weight_changes(): + """Regression: in KD-mix mode, increasing weight_hard_loss must change the gradient. + + Two runs with identical RNG, differing only in weight_hard_loss (0.0 vs 0.5). Pre-fix + both runs produced identical grads because CE was silently dropped from backward. + """ + inputs_a = make_inputs(seed=42) + loss_fn_a = LigerFusedLinearKLTopKLogprobLoss( + weight_soft_loss=0.5, + weight_hard_loss=0.0, + temperature=1.0, + beta=0.0, + compiled=False, + chunk_size=2, + compute_ce_loss=True, + ) + loss_a = loss_fn_a( + inputs_a["lm_head_weight"], + inputs_a["student_hidden_states"], + inputs_a["target_token_ids"], + inputs_a["target_logprobs"], + inputs_a["target_mask"], + inputs_a["true_labels"], + ) + loss_a.backward() + grad_a_h = inputs_a["student_hidden_states"].grad.detach().clone() + grad_a_w = inputs_a["lm_head_weight"].grad.detach().clone() + + inputs_b = make_inputs(seed=42) + loss_fn_b = LigerFusedLinearKLTopKLogprobLoss( + weight_soft_loss=0.5, + weight_hard_loss=0.5, + temperature=1.0, + beta=0.0, + compiled=False, + chunk_size=2, + compute_ce_loss=True, + ) + loss_b = loss_fn_b( + inputs_b["lm_head_weight"], + inputs_b["student_hidden_states"], + inputs_b["target_token_ids"], + inputs_b["target_logprobs"], + inputs_b["target_mask"], + inputs_b["true_labels"], + ) + loss_b.backward() + grad_b_h = inputs_b["student_hidden_states"].grad.detach().clone() + grad_b_w = inputs_b["lm_head_weight"].grad.detach().clone() + + assert grad_a_h is not None + assert torch.isfinite(grad_a_h).all() + assert grad_b_h is not None + assert torch.isfinite(grad_b_h).all() + + assert grad_a_w is not None + assert torch.isfinite(grad_a_w).all() + assert grad_b_w is not None + assert torch.isfinite(grad_b_w).all() + + diff = (grad_b_h - grad_a_h).abs().sum().item() + assert not torch.allclose(grad_a_h, grad_b_h, atol=1e-6, rtol=1e-5) + assert diff > 1e-4, f"CE gradient contribution suspiciously small: {diff}" + + diff_w = (grad_b_w - grad_a_w).abs().sum().item() + assert not torch.allclose(grad_a_w, grad_b_w, atol=1e-6, rtol=1e-5) + assert diff_w > 1e-4, ( + f"CE weight-gradient contribution suspiciously small: {diff_w}" + ) From ead6bc7c07921e68c2cc95c78861e905322a248f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 28 May 2026 17:34:48 -0400 Subject: [PATCH 1330/1405] scattermoe-lora: INT64_INDICES tl.constexpr in scatter2scatter family (supersedes chunking workaround) (#3667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(scattermoe-lora): repro CUBLAS_STATUS_EXECUTION_FAILED at large batch_count The tiled-MLP long-context bench surfaces a hard failure at seq=524288 with 16 shards: ``cublasGemmStridedBatchedEx`` raises ``CUBLAS_STATUS_EXECUTION_FAILED`` at parallel_experts.py:72's ``gates.unsqueeze(1) @ output_expanded``. The crash is reproducible on the bench shape (T=32K tokens/shard, top_k=8, hidden=2048, intermediate=8192) and is a downstream symptom of an int32 pointer-offset overflow in the upstream ``scatter2scatter`` Triton kernel during the up-projection — at that shape its output buffer is 2**32 elements, so ``M_block * stride_ym`` overflows int32 for the trailing rows. Add three repro tests in tests/integrations covering: * fast-path bit-identity vs the raw kernel below the threshold, * non-corruption at the overflow shape via the int32-safe wrapper, * end-to-end ``parallel_linear`` smoke at the failing bench shape. The tests are marked ``pytest.mark.skip`` pending the fix in the next commit; the follow-up "un-mark" commit re-enables them so they guard the fix going forward. Symbols imported inside each test body (``_scatter2scatter_int32_safe``, ``_SCATTER2SCATTER_INT32_LIMIT``) land alongside the fix in the next commit, so the skipped tests do not fail collection at this point in the history. Signed-off-by: Wing Lian fix(scattermoe-lora): work around cuBLAS large-batch_count failure in gates @ output_expanded The originally-reported symptom — ``CUBLAS_STATUS_EXECUTION_FAILED`` at parallel_experts.py:72 — is NOT a cuBLAS bug. The cuBLAS bmm shape at the failing seq=512K / 16-shard config is tiny (batch_count=32768, M=1, K=8, N=2048) and works in isolation. The crash is a downstream symptom of an int32 pointer-offset overflow in the upstream ``scatter2scatter`` Triton kernel during the up-projection, surfaced at the next CUDA-sync point (the bmm). Diagnosis (verified by inserting a ``torch.cuda.synchronize()`` immediately after the up-projection's scatter2scatter — that sync itself raises "an illegal memory access was encountered", proving the fault is upstream of the bmm): The Triton kernel computes output pointer offsets as ``Y_ptr + M_block * stride_ym + N_block * stride_yn`` with int32 ``M_block`` / ``stride_ym``. At seq=524288 / shards=16 the up-projection's output is ``[L_scattered=262144, y_dim=2*INTERMEDIATE=16384]`` = ``2**32`` elements; the trailing rows whose ``M_block * stride_ym`` overflows int32 have their masked stores silently drop (rows come back as zeros) or land at bogus pointers, which then trips a delayed ``CUDA illegal memory access`` that the next kernel surfaces. Workaround at the smallest scope appropriate to the actual root cause (NOT at parallel_experts.py:72, which is downstream): wrap ``kernels.ops.scatter2scatter`` with ``_scatter2scatter_int32_safe`` and route both call sites (``ParallelLinear.forward`` and ``ParallelLinear.backward``) through it. The wrapper: * Fast path (common case): when ``L_scattered * y_dim < 2**31``, dispatches a single direct kernel call — no overhead vs the pre-fix code. Verified at seq=524288 / shards=64: 36741 tokens/s post-fix vs ~37512 tokens/s pre-fix (within noise, no regression). * Slow path: when the output would overflow AND ``y_grouped=True``, allocates the full output and chunks along the L_scattered axis. Each sub-call writes to ``out[chunk_start:chunk_end]`` with the matching sei / ssi slice; the chunk size is the largest BLOCK_M-aligned row count keeping ``rows * y_dim < 2**31``. The chunked path drops into ``kernels.ops.scatter2scatter_compileable`` directly to bypass the high-level wrapper's ``sorted_scattered_idxs.size(0) == X.size(0) * k`` assertion that only holds for full calls. * When ``x_grouped=True`` (the down-proj backward), X is sliced in lockstep so the kernel's ``M_in_idx = M_block`` correctly reads ``X_chunk[0..chunk_size-1]``. When ``x_grouped=False`` (the up-proj forward) X stays full because the kernel indexes X via global ``M_idx // FAN_OUT`` from the per-position ``sorted_scattered_idxs`` values. * For ``y_grouped=False`` at overflow scale, the wrapper hard-raises ``RuntimeError`` — the kernel uses per-position scattered indices as output row indices so the wrapper cannot tile that case safely; the kernel itself needs an int64 pointer-arithmetic fix before that path is callable at this scale. Production paths today are all ``y_grouped=True`` so this branch is unreachable in the bench. Silent corruption is strictly worse than a clear raise. * Includes ``assert L_scattered % chunk_rows == 0`` for the ``x_grouped=False`` chunked path, since the kernel's ``M_boundary_mask`` uses the full (unchunked) X size and a partial last chunk would let the final tile read past sei_chunk / ssi_chunk. The assertion holds for all realistic power-of-2 shapes and fires loudly if a future caller hits a non-aligned one. The ``x_grouped=True`` chunked path is naturally bounded because X is chunked in lockstep. Before / after on the bench config: * pre-fix: CUBLAS_STATUS_EXECUTION_FAILED (no result) * post-fix: 10084 ms/iter, 51989 tokens/s, peak 64.16 GiB (matches the previously-fastest non-failing rows of the s=64 / s=256 sweep; s=16 was the predicted fastest row that the bug had been hiding) Constraints honoured: no changes to ``kernels.ops.scatter2scatter`` or any other Triton kernel; no public-API change on ``parallel_experts.py``; scope confined to scattermoe-lora's ParallelLinear; common-case fast path untouched. The LoRA-path counterpart (``parallel_linear_lora.py`` → ``scatter2scatter_lora``) has the same architectural risk but is not exercised by the failing bench config and is left for follow-up. Signed-off-by: Wing Lian test(scattermoe-lora): enable large-batch repro tests now the fix has landed Remove the ``pytest.mark.skip`` marker added in the ``repro CUBLAS_STATUS_EXECUTION_FAILED`` commit. The fix in the previous ``work around cuBLAS large-batch_count failure`` commit provides ``_scatter2scatter_int32_safe`` and the matching ``_SCATTER2SCATTER_INT32_LIMIT`` constant referenced by these tests, so the three tests now run and guard the fix going forward: * ``test_int32_safe_wrapper_matches_direct_call_below_threshold`` — fast-path equivalence (no overhead in the common case). * ``test_int32_safe_wrapper_no_corruption_at_overflow_shape`` — chunked slow-path correctness at the bench shape. * ``test_parallel_linear_long_seq_routing_combination`` — end-to-end smoke through ``ScatterMoEGatedMLP.forward`` shape sequence at seq=524288 / shards=16. All three pass on CUDA hardware; they self-skip when CUDA is unavailable. Signed-off-by: Wing Lian * feat(scattermoe-lora): add INT64_INDICES tl.constexpr to dense scatter2scatter The Triton scatter2scatter kernel computes output pointer offsets as ``Y_ptr + M_block * stride_ym + N_block * stride_yn`` where M_block / M_idx are int32 by default. At seq=512K with coarse shards the ``L_scattered * y_dim`` product exceeds 2**31 elements and the int32 arithmetic overflows; PR #3667 worked around this by chunking the call along the L_scattered axis when y_grouped=True, but that workaround doesn't cover y_grouped=False (raises) or the LoRA-path kernels. Add an ``INT64_INDICES: tl.constexpr = False`` knob to the dense ``_scatter2scatter`` kernel signature. When True, the M_block range and the scattered-index lookup ``M_idx`` are cast to int64 before they enter the pointer-offset multiplication, so all downstream pointer arithmetic propagates int64. Strides themselves stay as the kernel sees them (coming from ``tensor.stride()`` they're already int64 at the Python level); only the *index* values change type. Triton will JIT a separate variant per constexpr value, so the existing int32 fast path is unaffected. The wrapper-level auto-dispatch (compute ``needs_int64`` from tensor sizes and forward to the kernel) lands in a follow-up commit; this commit just exposes the constexpr and a Python-side ``int64_indices`` kwarg on ``scatter2scatter`` / ``scatter2scatter_compileable``. Signed-off-by: Wing Lian * feat(scattermoe-lora): add INT64_INDICES to LoRA forward + dX kernels (bf16 + MX) Adds the same ``INT64_INDICES: tl.constexpr = False`` knob as the dense kernel to the four LoRA-path scatter2scatter kernels: * ``_scatter2scatter_lora`` — bf16 fused base+LoRA forward * ``_scatter2scatter_lora_dX`` — bf16 fused dX backward * ``_scatter2scatter_lora_mx`` — MXFP4 fused base+LoRA forward * ``_scatter2scatter_lora_dX_mx`` — MXFP4 fused dX backward The cast pattern matches the dense kernel: when ``INT64_INDICES=True``, the per-launch ``M_block`` range and the scattered ``M_idx`` lookup are cast to int64 before they enter the ``M_*_idx * stride_*m`` pointer arithmetic. That promotes the multiplication to int64 and prevents the silent overflow at ``L_scattered * y_dim >= 2**31`` that the chunking workaround on the dense path was guarding against. The Python-side wrappers (``scatter2scatter_lora``, ``scatter2scatter_lora_dX``, ``scatter2scatter_lora_mx``, ``scatter2scatter_lora_dX_mx``) gain an ``int64_indices: bool = False`` kwarg and forward it to the kernel via the constexpr. Auto-dispatch from tensor sizes lands in a follow-up commit. PR #3667's chunking workaround only covered the bf16 dense forward; the LoRA path had the same architectural risk and wasn't covered. With these constexprs in place and the wrapper-side dispatch coming next, the kernel itself becomes int64-safe for all five variants and the chunking wrapper can be retired. Signed-off-by: Wing Lian * feat(scattermoe-lora): add INT64_INDICES to group_bwd_lora kernels Adds ``INT64_INDICES: tl.constexpr = False`` to the three LoRA gradient kernels that index into the grouped M dimension: * ``_group_bwd_lora`` — non-split LoRA-grad kernel (used by autotune-collector mocks; kept in sync for telemetry / future callers) * ``_group_bwd_lora_split`` — split dA/dB kernel that the public ``group_bwd_lora`` wrapper actually dispatches today * ``_group_bwd_lora_fused`` — fused gather + dA/dB kernel used by the LoRA path in ``parallel_linear_lora.py`` In each kernel, when ``INT64_INDICES=True`` we cast: - the per-expert ``start_idx`` / ``end_idx`` (and the fused kernel's ``real_*`` variants) to int64 on load, - ``M_block = tl.arange(0, BLOCK_M)`` to int64 so the per-iter ``M_idx = start_idx + i * BLOCK_M + M_block`` propagates int64, - and (in the fused kernel) ``scatter_idx`` from sorted-index lookups to int64 so ``scatter_idx * stride_dym`` and the ``X_token_idx = scatter_idx // FAN_OUT`` arithmetic stay int64. Strides themselves stay as the kernel receives them (already int64 at the Python level via ``tensor.stride()``). Triton JITs a separate variant per constexpr value, so the int32 fast path is unchanged. Python wrappers (``group_bwd_lora`` and ``group_bwd_lora_fused``) gain an ``int64_indices: bool = False`` kwarg that forwards to the kernel. Wrapper-level auto-dispatch from tensor sizes lands in the next commit. Signed-off-by: Wing Lian * feat(scattermoe-lora): auto-dispatch INT64_INDICES based on tensor sizes Adds ``_needs_int64_indices(*tensors)`` in ``parallel_experts.py``: True iff any input/output tensor's ``numel() >= 2**31 - 1``. That's a sufficient condition for the kernel's ``M_idx * stride_*m`` pointer arithmetic to overflow int32 somewhere in the buffer. Wires the result through the autograd Functions: * ``ParallelLinear`` (``parallel_experts.py``): forward computes ``needs_int64 = (L_scattered * y_dim) >= INT_MAX or _needs_int64_indices(x)`` and forwards via the new ``int64_indices`` kwarg on ``_scatter2scatter_int32_safe``. The wrapper's fast path now passes ``int64_indices`` through to ``kernels.ops.scatter2scatter`` so the kernel takes the int64 path at overflow scale. The wrapper also adds a new branch above the chunking path that routes directly to the int64 kernel when ``int64_indices=True`` is requested — notably this covers the y_grouped=False overflow case that the chunking workaround used to raise on. Backward follows the same pattern using ``L_scattered * K`` for the dX-axis bound. * ``ScatterMoELoRA`` (``parallel_linear_lora.py``): forward computes ``needs_int64`` from ``L_scattered * N`` and forwards to ``scatter2scatter_lora`` / ``scatter2scatter_lora_mx``. Backward computes a single ``needs_int64_bwd`` from ``M_total * max(N, K)`` (covering both the dX and the dA/dB kernels' index ranges) and forwards to ``group_bwd_lora`` / ``group_bwd_lora_fused`` and to the dX kernels (bf16 + MX, fused and non-fused). The auto-dispatch is cheap (one ``Tensor.numel()`` per check) and Triton JITs a separate kernel variant per constexpr value, so the int32 fast path is unaffected for small/medium shapes. Signed-off-by: Wing Lian * test(scattermoe-lora): int32-vs-int64 parity and overflow correctness Adds ``tests/integrations/test_scattermoe_lora_int64_indices.py`` covering two properties of the new INT64_INDICES path: * **Bitwise parity at non-overflow shapes.** For each of the modified kernels, ``INT64_INDICES=False`` and ``INT64_INDICES=True`` compute the same MMA in the same accumulation order — only the index *type* changes. The tests assert ``torch.equal`` between the two variants for the dense forward (both y_grouped=True and y_grouped=False), the LoRA forward, and the LoRA dX backward. For ``_group_bwd_lora_split`` the assertion is bitwise; for ``_group_bwd_lora_fused`` it's ``torch.allclose`` within bf16 tolerance because that kernel uses ``tl.atomic_add`` whose ordering is non-deterministic across launches (so bit-equality is not achievable between any two runs of the same variant, let alone across variants). * **Overflow correctness at the failing bench shape.** At L_scattered=262144 / y_dim=16384 (2**32 element output), the ``INT64_INDICES=True`` kernel populates every row of the output (including rows past the int32 overflow boundary) and matches the chunked workaround within a generous bf16 tolerance. A second bench-shape test runs the real ``ParallelLinear`` forward and uses a monkeypatched spy on ``scatter2scatter_compileable`` to assert the auto-dispatcher routes through the *direct* int64 kernel call (one launch) and **not** the chunking workaround (>=2 launches). Also folds in a kernel-side fix that the parity tests caught: the group_bwd_lora kernels' ``if E_idx == 0: start_idx = 0`` branch produced a plain int32 zero in Triton, which clashes with the int64 ``start_idx`` produced by the else-branch under ``INT64_INDICES=True``, firing ``AssertionError: Mismatched type for start_idx between then block (int32) and else block (int64)`` at compile. Switching the zero-initialisation to ``tl.zeros([], dtype=tl.int64/tl.int32)`` keeps both branches' types consistent. The bench-shape tests are skipped when free GPU memory is below 80 GiB. Signed-off-by: Wing Lian * bench(scattermoe-lora): int64-vs-int32 indexing overhead Adds ``tests/integrations/kernels/scattermoe_lora/bench_int64_kernel.py``, a stand-alone script (not pytest) that times the dense ``kernels.ops.scatter2scatter`` at three representative shapes and reports ms/iter for both ``INT64_INDICES=False`` (int32 fast path) and ``INT64_INDICES=True`` (int64 safe path): * **small** — seq=8K, top_k=8, hidden=2048, N=2048 (auto_int64=False) * **medium** — seq=128K, top_k=8, hidden=2048, N=2048 (auto_int64=False) * **overflow** — seq=512K with 16 shards → L_scattered=262144, N=16384 (auto_int64=True; the previously-failing bench config) At overflow shapes the int32 path is silently incorrect, so the int32 column is replaced by the chunked workaround from PR #3667 as the apples-to-apples baseline. Results land in ``bench_int64_kernel_results.md`` next to the script. Captured on RTX PRO 6000 Blackwell Max-Q (1.79 TB/s HBM): shape int32 ms int64 ms chunked ms penalty ------------- -------- -------- ---------- ------- small 2.687 2.689 — +0.0% medium 40.220 40.581 — +0.9% overflow — 79.572 79.985 -0.5% Both acceptance bounds are comfortably met: ≤5% on the int32 fast path (actual: <1%), and ≤25% on the int64 path vs the chunked workaround (actual: −0.5%, i.e. the int64 kernel is slightly *faster* than chunking at this shape because it avoids the per-chunk launch overhead). Signed-off-by: Wing Lian refactor(scattermoe-lora): deprecate _scatter2scatter_int32_safe chunking now that kernel is int64-safe PR #3667's ``_scatter2scatter_int32_safe`` chunking wrapper was the minimum-scope fix for the int32 pointer-overflow at the failing bench config: it tiled the call along the L_scattered axis to keep each sub-launch's ``rows * y_dim < 2**31``. With the kernel-level ``INT64_INDICES`` constexpr now landing on every relevant scatter2scatter family kernel and the wrapper-level auto-dispatch plumbed through both ``parallel_experts.py`` and ``parallel_linear_lora.py``, the chunking workaround is redundant — the kernel handles the overflow itself in a single launch. Removes from ``parallel_experts.py``: * ``_scatter2scatter_int32_safe`` and its 160-line chunking loop * ``_SCATTER2SCATTER_INT32_LIMIT`` and ``_SCATTER2SCATTER_BLOCK_M`` constants used only by the chunking path * the ``RuntimeError`` raise for ``y_grouped=False`` at overflow scale — the int64 kernel handles that case directly Routes ``ParallelLinear.forward`` / ``.backward`` straight to ``kernels.ops.scatter2scatter`` with ``int64_indices=needs_int64``. The bench config (seq=524288, 16 shards → L_scattered=262144, y_dim=16384, output=2**32 elements) now goes through a single int64 kernel launch and matches the bench-recorded perf (79.6 ms/iter vs. the chunked workaround's 80.0 ms/iter — slightly *faster* because it eliminates the per-chunk launch overhead). The PR #3667 repro tests are retained as regression guards and updated to call the new direct-kernel path: * ``test_scatter2scatter_below_threshold_no_overhead`` (renamed from ``test_int32_safe_wrapper_matches_direct_call_below_threshold``) asserts INT64_INDICES=False vs True is bit-identical at non- overflow shapes — guards the int32 fast path. * ``test_scatter2scatter_no_corruption_at_overflow_shape`` (renamed from ``test_int32_safe_wrapper_no_corruption_at_overflow_shape``) asserts the int64 kernel populates rows past the int32 overflow boundary — guards the kernel-level overflow fix. * ``test_parallel_linear_long_seq_routing_combination`` is unchanged; it runs ``parallel_linear`` end-to-end at the bench shape and asserts no all-zero rows / no NaNs — guards the auto-dispatch wiring. The new ``test_parallel_linear_overflow_takes_int64_kernel_path`` in ``test_scattermoe_lora_int64_indices.py`` is also updated to monkey- patch ``scatter2scatter_compileable`` and assert the single launch sets ``int64_indices=True``, which directly verifies the auto- dispatch verdict at the failing bench shape. Signed-off-by: Wing Lian * chore(scattermoe-lora): pre-commit fixups for INT64 indices commits * ruff format pass on the four touched files (line-wrapping only, no functional changes). * ``parallel_linear_lora.py``: replace the one-line conditional ``N_dim = expert_weights.N if is_mx else expert_weights.size(-1)`` with an explicit if/else and ``# type: ignore[union-attr]`` on each branch — mypy can't narrow ``Union[Tensor, MXWeights]`` through a ternary, but does respect the explicit branches enough to need the ignore only on the offending attribute access. The pre-existing ternary in the backward path stays as-is (already covered by the surrounding type checks). * ``bench_int64_kernel.py``: drop imports of the removed ``_scatter2scatter_int32_safe`` / ``_SCATTER2SCATTER_INT32_LIMIT`` symbols (they went away in the refactor commit) and the now-unused ``ms_chunk`` column. The bench now reports int32 vs int64 timings only; the overflow row shows only int64 since the int32 kernel is silently incorrect there. Signed-off-by: Wing Lian * test(scattermoe-lora): add small-shape int64 overflow tests (run on L40S/24 GiB) The two bench-shape overflow tests above need ~80 GiB free and skip on the Modal CI L40S 48 GiB runner, so the actual overflow path the kernel fix targets was not exercised on CI. The new ..._small variants repro the same property at the smallest shape that still straddles the int32 boundary: L_scattered * y_dim = 2**32 (2x past 2**31, guaranteed overflow without int64_indices=True), with E=4, K=256, y_dim=4096 so W is ~8 MiB and the only big allocation is the ~8 GiB scatter output. Gated at 12 GiB free to leave headroom for pytest-xdist workers on 48 GiB devices. * fix(scattermoe-lora): bump _SMALL_E so int64-overflow topk is valid _SMALL_TOP_K=8 with _SMALL_E=4 makes torch.topk(logits[T,4], k=8) raise 'selected index k out of range', skipping the two _small overflow tests. The shape invariant L_scattered*y_dim=2**32 (T=131072, y_dim=4096) requires top_k=8, so E must be >= 8. * perf(scattermoe-lora): bucket M in autotune key to dedupe sweeps The 7 multi-config @triton.autotune kernels in lora_ops.py keyed on ["M", "N", "K"]. M = X.size(0) (or DY.size(0)) scales with batch*seq*top_k, so any seqlen variation triggers a fresh 30-60 config sweep per step until the cache happens to cover every realized M. With N, K model-fixed this was the only churning dimension. Add a phantom M_BUCKET arg to each kernel signature and switch the autotune key to ["M_BUCKET", "N", "K"]. The kernel still runs on the real M (loop bounds + masks unchanged); only the cache lookup is bucketed to the next multiple of _M_BUCKET_GRANULARITY=1024. No padding, no wasted FLOPs. autotune_collector._KEY_NAMES tracks the renamed key so telemetry matches what's actually in the .cache dict. Tests: - New test_scattermoe_lora_m_bucket.py pins both directions: same-bucket M values produce one cache entry, distinct-bucket M values produce two. - Updated telemetry test assertions for the renamed key. - Existing scattermoe-lora suite (62 tests) + int64 indices (10 tests) + telemetry (13 tests) all pass unchanged. --------- Signed-off-by: Wing Lian --- .../kernels/autotune_collector.py | 8 +- .../libs/scattermoe_lora/kernels/lora_ops.py | 163 ++++- .../libs/scattermoe_lora/kernels/ops.py | 12 +- .../libs/scattermoe_lora/parallel_experts.py | 41 ++ .../scattermoe_lora/parallel_linear_lora.py | 28 + .../scattermoe_lora/bench_int64_kernel.py | 229 +++++++ .../bench_int64_kernel_results.md | 15 + ...test_parallel_experts_large_batch_repro.py | 289 +++++++++ .../test_scattermoe_lora_int64_indices.py | 581 ++++++++++++++++++ .../test_scattermoe_lora_m_bucket.py | 118 ++++ .../test_scattermoe_autotune_telemetry.py | 4 +- 11 files changed, 1453 insertions(+), 35 deletions(-) create mode 100644 tests/integrations/kernels/scattermoe_lora/bench_int64_kernel.py create mode 100644 tests/integrations/kernels/scattermoe_lora/bench_int64_kernel_results.md create mode 100644 tests/integrations/kernels/scattermoe_lora/test_parallel_experts_large_batch_repro.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_scattermoe_lora_int64_indices.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_scattermoe_lora_m_bucket.py diff --git a/src/axolotl/integrations/kernels/autotune_collector.py b/src/axolotl/integrations/kernels/autotune_collector.py index bdb5e030e4..92e0dbe2d5 100644 --- a/src/axolotl/integrations/kernels/autotune_collector.py +++ b/src/axolotl/integrations/kernels/autotune_collector.py @@ -21,15 +21,17 @@ ("group_bwd_lora_fused", "_group_bwd_lora_fused"), ] -# The autotune key declared on every kernel: key=["M", "N", "K"] -_KEY_NAMES: list[str] = ["M", "N", "K"] +# The autotune key declared on every kernel: key=["M_BUCKET", "N", "K"]. +# M_BUCKET is the seqlen-bucketed M (see _bucket_m in lora_ops.py) so cache +# entries don't churn with every distinct M. +_KEY_NAMES: list[str] = ["M_BUCKET", "N", "K"] def _parse_key_tuple(key_tuple: tuple) -> dict[str, Any]: """Turn the autotune cache key tuple into a labelled dict. Triton builds the cache key from the values of the declared ``key`` - args (``M``, ``N``, ``K``) followed by dtype signature elements. + args (``M_BUCKET``, ``N``, ``K``) followed by dtype signature elements. We label the first three and store the rest under ``_extra``. """ result: dict[str, Any] = {} diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py index df3041bea1..a453e83495 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py @@ -54,6 +54,17 @@ def _next_power_of_2(n: int) -> int: return n + 1 +# Granularity for the autotune cache key on M. The kernel still runs on the +# real M (loop bounds + masks); only the @triton.autotune key is bucketed so +# that varying seqlens/routing don't keep invalidating the cache. +_M_BUCKET_GRANULARITY = 1024 + + +def _bucket_m(m: int) -> int: + g = _M_BUCKET_GRANULARITY + return ((m + g - 1) // g) * g + + # Triton tl.dot requires minimum tile dimensions of 16 on modern GPUs. MIN_TRITON_DOT_SIZE = 16 @@ -450,7 +461,7 @@ def _prune_fwd_configs(configs, named_args, **kwargs): @triton.autotune( configs=_scatter2scatter_lora_configs(), - key=["M", "N", "K"], + key=["M_BUCKET", "N", "K"], prune_configs_by={"early_config_prune": _prune_fwd_configs}, ) @triton.heuristics( @@ -489,6 +500,7 @@ def _scatter2scatter_lora( # Dimensions FAN_OUT: tl.constexpr, M, + M_BUCKET, K: tl.constexpr, N: tl.constexpr, E: tl.constexpr, @@ -506,6 +518,7 @@ def _scatter2scatter_lora( y_grouped: tl.constexpr, NO_K_MASK: tl.constexpr, NO_N_MASK: tl.constexpr, + INT64_INDICES: tl.constexpr = False, ): """ Fused scatter2scatter with LoRA: Y = X @ W + scaling * (X @ A^T) @ B^T + bias @@ -517,6 +530,8 @@ def _scatter2scatter_lora( N_block_id = pid % N_BLOCK_COUNT M_block = M_block_id * BLOCK_M + tl.arange(0, BLOCK_M) + if INT64_INDICES: + M_block = M_block.to(tl.int64) N_block = N_block_id * BLOCK_N + tl.arange(0, BLOCK_N) N_mask = N_block < N M_boundary_mask = M_block < (FAN_OUT * M) @@ -529,7 +544,10 @@ def _scatter2scatter_lora( E_first_idx = tl.min(E_idxs) E_last_idx = tl.minimum(tl.max(E_idxs), E - 1) - M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) + if INT64_INDICES: + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int64) + else: + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) for E_idx in range(E_first_idx, E_last_idx + 1): E_mask = E_idxs == E_idx @@ -600,6 +618,7 @@ def _scatter2scatter_lora_split( x_grouped: bool = False, y_grouped: bool = False, out: Optional[torch.Tensor] = None, + int64_indices: bool = False, ) -> torch.Tensor: """Split base+LoRA forward: 3 scatter2scatter calls, no fused LoRA kernel. @@ -629,6 +648,7 @@ def _scatter2scatter_lora_split( x_grouped=x_grouped, y_grouped=y_grouped, out=out, + int64_indices=int64_indices, ) # 2. XA = X @ A^T (tiny: output is [M*k, R]) @@ -642,6 +662,7 @@ def _scatter2scatter_lora_split( k=k, x_grouped=x_grouped, y_grouped=True, + int64_indices=int64_indices, ) # 3. Y_lora = XA @ B^T (R is tiny, so this is very fast) @@ -655,6 +676,7 @@ def _scatter2scatter_lora_split( k=1, x_grouped=True, y_grouped=y_grouped, + int64_indices=int64_indices, ) # 4. Y = Y_base + scaling * Y_lora @@ -683,6 +705,7 @@ def scatter2scatter_lora( x_grouped: bool = False, y_grouped: bool = False, out: Optional[torch.Tensor] = None, + int64_indices: bool = False, ) -> torch.Tensor: """ Scatter2scatter with LoRA: Y[i] = X[i] @ W[e] + scaling * (X[i] @ A[e]^T) @ B[e]^T + b[e] @@ -729,6 +752,7 @@ def scatter2scatter_lora( x_grouped, y_grouped, out, + int64_indices=int64_indices, ) assert sorted_scattered_idxs.size(0) == sorted_expert_idxs.size(0) @@ -783,6 +807,7 @@ def grid(META): sorted_expert_idxs, FAN_OUT=k, M=X.size(0), + M_BUCKET=_bucket_m(X.size(0)), K=K, N=N, E=E, @@ -793,6 +818,7 @@ def grid(META): allow_tf32=ALLOW_TF32, x_grouped=x_grouped, y_grouped=y_grouped, + INT64_INDICES=int64_indices, ) return output @@ -1030,7 +1056,7 @@ def _prune_dX_configs(configs, named_args, **kwargs): @triton.autotune( configs=_scatter2scatter_lora_dX_configs(), - key=["M", "N", "K"], + key=["M_BUCKET", "N", "K"], prune_configs_by={"early_config_prune": _prune_dX_configs}, ) @triton.heuristics( @@ -1067,6 +1093,7 @@ def _scatter2scatter_lora_dX( # Dimensions FAN_OUT: tl.constexpr, M, + M_BUCKET, K: tl.constexpr, N: tl.constexpr, E: tl.constexpr, @@ -1084,6 +1111,7 @@ def _scatter2scatter_lora_dX( dx_grouped: tl.constexpr, NO_K_MASK: tl.constexpr, NO_N_MASK: tl.constexpr, + INT64_INDICES: tl.constexpr = False, ): """ Fused backward dX = DY @ W^T + scaling * (DY @ B) @ A @@ -1100,6 +1128,8 @@ def _scatter2scatter_lora_dX( K_block_id = pid % K_BLOCK_COUNT M_block = M_block_id * BLOCK_M + tl.arange(0, BLOCK_M) + if INT64_INDICES: + M_block = M_block.to(tl.int64) K_block = K_block_id * BLOCK_K + tl.arange(0, BLOCK_K) K_mask = K_block < K M_boundary_mask = M_block < (FAN_OUT * M) @@ -1112,7 +1142,10 @@ def _scatter2scatter_lora_dX( E_first_idx = tl.min(E_idxs) E_last_idx = tl.minimum(tl.max(E_idxs), E - 1) - M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) + if INT64_INDICES: + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int64) + else: + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) for E_idx in range(E_first_idx, E_last_idx + 1): E_mask = E_idxs == E_idx @@ -1175,6 +1208,7 @@ def scatter2scatter_lora_dX( dy_grouped: bool = True, dx_grouped: bool = False, out: Optional[torch.Tensor] = None, + int64_indices: bool = False, ) -> torch.Tensor: """ Fused backward dX = DY @ W^T + scaling * (DY @ B) @ A @@ -1250,6 +1284,7 @@ def grid(META): sorted_expert_idxs, FAN_OUT=fan_out, M=M, + M_BUCKET=_bucket_m(M), K=K, N=N, E=E, @@ -1261,6 +1296,7 @@ def grid(META): allow_tf32=ALLOW_TF32, dy_grouped=dy_grouped, dx_grouped=dx_grouped, + INT64_INDICES=int64_indices, ) return output @@ -1363,7 +1399,7 @@ def _prune_bwd_lora_configs(configs, named_args, **kwargs): @triton.autotune( configs=_group_bwd_lora_configs(), - key=["M", "N", "K"], + key=["M_BUCKET", "N", "K"], prune_configs_by={"early_config_prune": _prune_bwd_lora_configs}, reset_to_zero=["DLA_ptr", "DLB_ptr"], ) @@ -1400,6 +1436,7 @@ def _group_bwd_lora( expert_offsets_ptr, # Dimensions M, + M_BUCKET, K: tl.constexpr, N: tl.constexpr, ACTUAL_R: tl.constexpr, # True LoRA rank (for weight indexing) @@ -1413,6 +1450,7 @@ def _group_bwd_lora( allow_tf32: tl.constexpr, NO_K_MASK: tl.constexpr, NO_N_MASK: tl.constexpr, + INT64_INDICES: tl.constexpr = False, ): """ Compute LoRA gradients for each expert on grouped data. @@ -1434,15 +1472,24 @@ def _group_bwd_lora( N_block_id = pid1 # Get expert's token range from cumulative offsets - if E_idx == 0: - start_idx = 0 + if INT64_INDICES: + if E_idx == 0: + start_idx = tl.zeros([], dtype=tl.int64) + else: + start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int64) + end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int64) else: - start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int32) - end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int32) + if E_idx == 0: + start_idx = tl.zeros([], dtype=tl.int32) + else: + start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int32) + end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int32) num_tokens = end_idx - start_idx if num_tokens > 0: M_block = tl.arange(0, BLOCK_M) + if INT64_INDICES: + M_block = M_block.to(tl.int64) K_block = K_block_id * BLOCK_K + tl.arange(0, BLOCK_K) K_mask = K_block < K N_block = N_block_id * BLOCK_N + tl.arange(0, BLOCK_N) @@ -1601,7 +1648,7 @@ def _prune_split_configs(configs, named_args, **kwargs): @triton.autotune( configs=_group_bwd_split_configs(), - key=["M", "K", "N"], + key=["M_BUCKET", "K", "N"], prune_configs_by={"early_config_prune": _prune_split_configs}, ) @triton.heuristics( @@ -1634,6 +1681,7 @@ def _group_bwd_lora_split( expert_offsets_ptr, # Dimensions M, + M_BUCKET, K: tl.constexpr, N: tl.constexpr, ACTUAL_R: tl.constexpr, @@ -1648,6 +1696,7 @@ def _group_bwd_lora_split( ACC_TYPE: tl.constexpr, allow_tf32: tl.constexpr, NO_DIM_MASK: tl.constexpr, + INT64_INDICES: tl.constexpr = False, ): """ Unified split kernel for LoRA gradient computation. @@ -1671,11 +1720,18 @@ def _group_bwd_lora_split( E_idx = tl.program_id(0) dim_block_id = tl.program_id(1) - if E_idx == 0: - start_idx = 0 + if INT64_INDICES: + if E_idx == 0: + start_idx = tl.zeros([], dtype=tl.int64) + else: + start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int64) + end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int64) else: - start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int32) - end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int32) + if E_idx == 0: + start_idx = tl.zeros([], dtype=tl.int32) + else: + start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int32) + end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int32) num_tokens = end_idx - start_idx # Output dimension tile (K for dA, N for dB) @@ -1707,6 +1763,8 @@ def _group_bwd_lora_split( if num_tokens > 0: M_block = tl.arange(0, BLOCK_M) + if INT64_INDICES: + M_block = M_block.to(tl.int64) INPUT_DTYPE = X_ptr.dtype.element_ty BLOCK_INNER: tl.constexpr = 64 inner_iters = tl.cdiv(INNER_DIM, BLOCK_INNER) @@ -1826,6 +1884,7 @@ def group_bwd_lora( scaling: float, sorted_scattered_idxs: Optional[torch.Tensor] = None, k: int = 1, + int64_indices: bool = False, ) -> tuple[torch.Tensor, torch.Tensor]: """ Compute LoRA gradients for A and B on expert-grouped data. @@ -1875,6 +1934,7 @@ def grid_dA(META): dA.stride(1), expert_offsets, M=DY.size(0), + M_BUCKET=_bucket_m(DY.size(0)), K=K, N=N, ACTUAL_R=R, @@ -1884,6 +1944,7 @@ def grid_dA(META): COMPUTE_DA=True, ACC_TYPE=tl.float32, allow_tf32=ALLOW_TF32, + INT64_INDICES=int64_indices, ) def grid_dB(META): @@ -1904,6 +1965,7 @@ def grid_dB(META): dB.stride(1), expert_offsets, M=DY.size(0), + M_BUCKET=_bucket_m(DY.size(0)), K=K, N=N, ACTUAL_R=R, @@ -1913,6 +1975,7 @@ def grid_dB(META): COMPUTE_DA=False, ACC_TYPE=tl.float32, allow_tf32=ALLOW_TF32, + INT64_INDICES=int64_indices, ) return dA, dB @@ -1925,7 +1988,7 @@ def grid_dB(META): @triton.autotune( configs=_group_bwd_lora_configs(), - key=["M", "N", "K"], + key=["M_BUCKET", "N", "K"], prune_configs_by={"early_config_prune": _prune_bwd_lora_configs}, reset_to_zero=["DLA_ptr", "DLB_ptr"], ) @@ -1967,6 +2030,7 @@ def _group_bwd_lora_fused( real_expert_offsets_ptr, # Dimensions M, + M_BUCKET, K: tl.constexpr, N: tl.constexpr, ACTUAL_R: tl.constexpr, @@ -1982,6 +2046,7 @@ def _group_bwd_lora_fused( NO_N_MASK: tl.constexpr, # Whether DY is already in grouped (expert-sorted) order dy_grouped: tl.constexpr = False, + INT64_INDICES: tl.constexpr = False, ): """ Fused gather + LoRA gradient computation. Same as _group_bwd_lora but @@ -2018,18 +2083,30 @@ def _group_bwd_lora_fused( # Get expert's token range from cumulative offsets # start_idx/end_idx from expert_offsets_ptr: iteration range (possibly padded) # real_end_idx from real_expert_offsets_ptr: for M_mask (real token count) - if E_idx == 0: - start_idx = 0 - real_start_idx = 0 + if INT64_INDICES: + if E_idx == 0: + start_idx = tl.zeros([], dtype=tl.int64) + real_start_idx = tl.zeros([], dtype=tl.int64) + else: + start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int64) + real_start_idx = tl.load(real_expert_offsets_ptr + E_idx - 1).to(tl.int64) + end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int64) + real_end_idx = tl.load(real_expert_offsets_ptr + E_idx).to(tl.int64) else: - start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int32) - real_start_idx = tl.load(real_expert_offsets_ptr + E_idx - 1).to(tl.int32) - end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int32) - real_end_idx = tl.load(real_expert_offsets_ptr + E_idx).to(tl.int32) + if E_idx == 0: + start_idx = tl.zeros([], dtype=tl.int32) + real_start_idx = tl.zeros([], dtype=tl.int32) + else: + start_idx = tl.load(expert_offsets_ptr + E_idx - 1).to(tl.int32) + real_start_idx = tl.load(real_expert_offsets_ptr + E_idx - 1).to(tl.int32) + end_idx = tl.load(expert_offsets_ptr + E_idx).to(tl.int32) + real_end_idx = tl.load(real_expert_offsets_ptr + E_idx).to(tl.int32) num_tokens = end_idx - start_idx if num_tokens > 0: M_block = tl.arange(0, BLOCK_M) + if INT64_INDICES: + M_block = M_block.to(tl.int64) K_block = K_block_id * BLOCK_K + tl.arange(0, BLOCK_K) K_mask = K_block < K N_block = N_block_id * BLOCK_N + tl.arange(0, BLOCK_N) @@ -2074,9 +2151,14 @@ def _group_bwd_lora_fused( M_mask = M_local < real_num_tokens # Fused gather: load scatter indices for indirect X access - scatter_idx = tl.load( - sorted_scattered_idxs_ptr + M_idx, mask=M_mask, other=0 - ).to(tl.int32) + if INT64_INDICES: + scatter_idx = tl.load( + sorted_scattered_idxs_ptr + M_idx, mask=M_mask, other=0 + ).to(tl.int64) + else: + scatter_idx = tl.load( + sorted_scattered_idxs_ptr + M_idx, mask=M_mask, other=0 + ).to(tl.int32) X_token_idx = scatter_idx // FAN_OUT # X is [M, K], not expanded by k # Load X via indirect index: [BLOCK_M, BLOCK_K] @@ -2154,6 +2236,7 @@ def group_bwd_lora_fused( scaling: float, real_expert_offsets: Optional[torch.Tensor] = None, dy_grouped: bool = False, + int64_indices: bool = False, ) -> tuple[torch.Tensor, torch.Tensor]: """ Fused gather + LoRA gradient computation. Same result as @@ -2230,6 +2313,7 @@ def grid(META): expert_offsets_ptr=expert_offsets, real_expert_offsets_ptr=real_expert_offsets, M=sorted_scattered_idxs.size(0), + M_BUCKET=_bucket_m(sorted_scattered_idxs.size(0)), K=K, N=N, ACTUAL_R=R, @@ -2238,6 +2322,7 @@ def grid(META): ACC_TYPE=tl.float32, allow_tf32=ALLOW_TF32, dy_grouped=dy_grouped, + INT64_INDICES=int64_indices, ) return dA, dB @@ -2501,7 +2586,7 @@ def _prune_fwd_mx_configs(configs, named_args, **kwargs): @triton.autotune( configs=_scatter2scatter_lora_mx_configs(), - key=["M", "N", "K"], + key=["M_BUCKET", "N", "K"], prune_configs_by={"early_config_prune": _prune_fwd_mx_configs}, ) @triton.heuristics( @@ -2549,6 +2634,7 @@ def _scatter2scatter_lora_mx( # Dimensions FAN_OUT: tl.constexpr, M, + M_BUCKET, K: tl.constexpr, N: tl.constexpr, E: tl.constexpr, @@ -2565,6 +2651,7 @@ def _scatter2scatter_lora_mx( y_grouped: tl.constexpr, NO_K_MASK: tl.constexpr, NO_N_MASK: tl.constexpr, + INT64_INDICES: tl.constexpr = False, ): """Fused scatter2scatter forward with MXFP4 base weights + LoRA.""" pid = tl.program_id(axis=0) @@ -2573,6 +2660,8 @@ def _scatter2scatter_lora_mx( N_block_id = pid % N_BLOCK_COUNT M_block = M_block_id * BLOCK_M + tl.arange(0, BLOCK_M) + if INT64_INDICES: + M_block = M_block.to(tl.int64) N_block = N_block_id * BLOCK_N + tl.arange(0, BLOCK_N) N_mask = N_block < N M_boundary_mask = M_block < (FAN_OUT * M) @@ -2583,7 +2672,10 @@ def _scatter2scatter_lora_mx( E_first_idx = tl.min(E_idxs) E_last_idx = tl.minimum(tl.max(E_idxs), E - 1) - M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) + if INT64_INDICES: + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int64) + else: + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) for E_idx in range(E_first_idx, E_last_idx + 1): E_mask = E_idxs == E_idx @@ -2658,6 +2750,7 @@ def scatter2scatter_lora_mx( x_grouped: bool = False, y_grouped: bool = False, out: Optional[torch.Tensor] = None, + int64_indices: bool = False, ) -> torch.Tensor: """Forward dispatcher for the fused MXFP4 + LoRA kernel. @@ -2730,6 +2823,7 @@ def grid(META): sorted_expert_idxs, FAN_OUT=k, M=X.size(0), + M_BUCKET=_bucket_m(X.size(0)), K=K, N=N, E=E, @@ -2741,6 +2835,7 @@ def grid(META): allow_tf32=ALLOW_TF32, x_grouped=x_grouped, y_grouped=y_grouped, + INT64_INDICES=int64_indices, ) return output @@ -2965,7 +3060,7 @@ def _prune_dX_mx_configs(configs, named_args, **kwargs): @triton.autotune( configs=_scatter2scatter_lora_dX_mx_configs(), - key=["M", "N", "K"], + key=["M_BUCKET", "N", "K"], prune_configs_by={"early_config_prune": _prune_dX_mx_configs}, ) @triton.heuristics( @@ -3002,6 +3097,7 @@ def _scatter2scatter_lora_dX_mx( expert_idxs_ptr, FAN_OUT: tl.constexpr, M, + M_BUCKET, K: tl.constexpr, N: tl.constexpr, E: tl.constexpr, @@ -3017,6 +3113,7 @@ def _scatter2scatter_lora_dX_mx( dy_grouped: tl.constexpr, dx_grouped: tl.constexpr, NO_N_MASK: tl.constexpr, + INT64_INDICES: tl.constexpr = False, ): """Fused MXFP4 dX kernel.""" pid = tl.program_id(axis=0) @@ -3025,6 +3122,8 @@ def _scatter2scatter_lora_dX_mx( K_block_id = pid % K_BLOCK_COUNT M_block = M_block_id * BLOCK_M + tl.arange(0, BLOCK_M) + if INT64_INDICES: + M_block = M_block.to(tl.int64) K_block = K_block_id * BLOCK_K + tl.arange(0, BLOCK_K) K_mask = K_block < K M_boundary_mask = M_block < (FAN_OUT * M) @@ -3035,7 +3134,10 @@ def _scatter2scatter_lora_dX_mx( E_first_idx = tl.min(E_idxs) E_last_idx = tl.minimum(tl.max(E_idxs), E - 1) - M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) + if INT64_INDICES: + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int64) + else: + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) for E_idx in range(E_first_idx, E_last_idx + 1): E_mask = E_idxs == E_idx @@ -3103,6 +3205,7 @@ def scatter2scatter_lora_dX_mx( dy_grouped: bool = True, dx_grouped: bool = False, out: Optional[torch.Tensor] = None, + int64_indices: bool = False, ) -> torch.Tensor: """Backward-dX dispatcher for the fused MXFP4 kernel. @@ -3176,6 +3279,7 @@ def grid(META): sorted_expert_idxs, FAN_OUT=fan_out, M=M, + M_BUCKET=_bucket_m(M), K=K, N=N, E=E, @@ -3187,5 +3291,6 @@ def grid(META): allow_tf32=ALLOW_TF32, dy_grouped=dy_grouped, dx_grouped=dx_grouped, + INT64_INDICES=int64_indices, ) return output diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/ops.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/ops.py index 6aa432770d..08baa8e8e3 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/ops.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/ops.py @@ -108,6 +108,7 @@ def _scatter2scatter( y_grouped: tl.constexpr, NO_K_MASK: tl.constexpr, NO_N_MASK: tl.constexpr, + INT64_INDICES: tl.constexpr = False, ): pid = tl.program_id(axis=0) @@ -116,6 +117,8 @@ def _scatter2scatter( N_block_id = pid % N_BLOCK_COUNT M_block = M_block_id * BLOCK_M + tl.arange(0, BLOCK_M) + if INT64_INDICES: + M_block = M_block.to(tl.int64) N_block = N_block_id * BLOCK_N + tl.arange(0, BLOCK_N) N_mask = N_block < N M_boundary_mask = M_block < (FAN_OUT * M) @@ -126,7 +129,10 @@ def _scatter2scatter( acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=ACC_TYPE) E_first_idx = tl.min(E_idxs) E_last_idx = tl.minimum(tl.max(E_idxs), E - 1) - M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) + if INT64_INDICES: + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int64) + else: + M_idx = tl.load(grouped_idx_ptr + M_block, mask=M_boundary_mask).to(tl.int32) for E_idx in range(E_first_idx, E_last_idx + 1): E_mask = E_idxs == E_idx E_M_idx = M_idx @@ -176,6 +182,7 @@ def scatter2scatter( x_grouped=False, y_grouped=False, out=None, + int64_indices=False, ): assert sorted_scattered_idxs.size(0) == sorted_expert_idxs.size(0) assert sorted_scattered_idxs.size(0) == X.size(0) * k @@ -198,6 +205,7 @@ def scatter2scatter( b, x_grouped, y_grouped, + int64_indices, ) return output @@ -213,6 +221,7 @@ def scatter2scatter_compileable( b: Optional[torch.Tensor], x_grouped: bool, y_grouped: bool, + int64_indices: bool = False, ) -> None: def grid(META): grid_num = ( @@ -258,6 +267,7 @@ def grid(META): allow_tf32=ALLOW_TF32, x_grouped=x_grouped, y_grouped=y_grouped, + INT64_INDICES=int64_indices, ) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_experts.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_experts.py index 5180587aad..eae63fb8ee 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_experts.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_experts.py @@ -11,6 +11,34 @@ from . import kernels +# When the maximum addressable element offset across any input/output buffer +# exceeds INT_MAX, the Triton kernel's int32 pointer-offset arithmetic +# overflows. ``_needs_int64_indices`` returns True iff any tensor has +# ``numel() >= INT_MAX``, which is a sufficient condition for the +# ``M_idx * stride_*m`` product to overflow somewhere in the kernel. When +# True, callers pass ``INT64_INDICES=True`` to the kernel so the index range +# is cast to int64 before it enters the multiplication. Strides themselves +# are already int64 at the Python level (from ``tensor.stride()``); only +# the *index* type needs the bump. +# +# The threshold here matches the kernel's correctness boundary: as soon as +# any indexed buffer has 2**31 - 1 or more elements, the int32 multiply at +# the end of the buffer can overflow. The wrapper used to chunk the call to +# keep ``rows * y_dim`` below 2**31; with INT64_INDICES the kernel itself +# handles overflow, so the auto-dispatch routes directly to a single +# kernel launch in either mode. +_INT_MAX = 2**31 - 1 + + +def _needs_int64_indices(*tensors) -> bool: + """True iff any input/output tensor's element count exceeds INT_MAX.""" + for t in tensors: + if t is None or not isinstance(t, torch.Tensor): + continue + if t.numel() >= _INT_MAX: + return True + return False + @torch.library.custom_op("scattermoe::bincount", mutates_args={}) def compileable_bincount(x: torch.Tensor, minlength: int) -> torch.Tensor: @@ -54,6 +82,12 @@ def forward( expert_weights = expert_weights.to(x.dtype) if expert_biases is not None and expert_biases.dtype != x.dtype: expert_biases = expert_biases.to(x.dtype) + L_scattered = sorted_expert_idxs.size(0) + y_dim = expert_weights.size(-1) + # Cheap probe: the kernel's overflow risk is the M_block * stride_ym + # product, dominated by the output buffer L_scattered * y_dim. We also + # check x because the X_ptr arithmetic uses similar indices. + needs_int64_fwd = (L_scattered * y_dim) >= _INT_MAX or _needs_int64_indices(x) with torch.device(x.device): output = kernels.ops.scatter2scatter( X=x, @@ -64,6 +98,7 @@ def forward( sorted_scattered_idxs=sorted_scattered_idxs, x_grouped=grouped_in, y_grouped=grouped_out, + int64_indices=needs_int64_fwd, ) if gates is not None: output_expanded = output.view( @@ -145,6 +180,11 @@ def backward(ctx, grad_out: torch.Tensor): has_bias=expert_biases is not None, ) + L_scattered = sorted_expert_idxs.size(0) + dx_dim = expert_weights.size(1) # K dim of W = output dim for dX + needs_int64_bwd = ( + L_scattered * dx_dim + ) >= _INT_MAX or _needs_int64_indices(grouped_grad_out) d_expanded_input = kernels.ops.scatter2scatter( X=grouped_grad_out, x_grouped=True, @@ -154,6 +194,7 @@ def backward(ctx, grad_out: torch.Tensor): k=1, y_grouped=grouped_in, out=d_expanded_input, # Reuse grouped_x buffer + int64_indices=needs_int64_bwd, ) if k == 1: diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py index af2dbbd86a..7bc70280a4 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py @@ -37,6 +37,7 @@ scatter2scatter_lora_mx, ) from .mx_weights import MXLayout, MXWeights +from .parallel_experts import _INT_MAX, _needs_int64_indices class ScatterMoELoRA(torch.autograd.Function): @@ -80,6 +81,15 @@ def forward( is_mx = False if expert_biases is not None and expert_biases.dtype != x.dtype: expert_biases = expert_biases.to(x.dtype) + L_scattered = sorted_expert_idxs.size(0) + if is_mx: + N_dim = expert_weights.N # type: ignore[union-attr] + else: + N_dim = expert_weights.size(-1) # type: ignore[union-attr] + # Forward output is [L_scattered, N]. Overflow risk is dominated by + # that buffer; also probe X for the unusual case where it alone is + # huge (e.g. very wide hidden with modest seq). + needs_int64_fwd = (L_scattered * N_dim) >= _INT_MAX or _needs_int64_indices(x) with torch.device(x.device): if is_mx: # Fused MXFP4 forward: dequant happens inside the K-loop @@ -95,6 +105,7 @@ def forward( b=expert_biases, x_grouped=grouped_in, y_grouped=grouped_out, + int64_indices=needs_int64_fwd, ) else: # Fused forward: Y = X @ W + scaling * (X @ A^T) @ B^T @@ -110,6 +121,7 @@ def forward( b=expert_biases, x_grouped=grouped_in, y_grouped=grouped_out, + int64_indices=needs_int64_fwd, ) # Handle gating (weighted combination of top-k expert outputs) @@ -216,6 +228,15 @@ def backward(ctx, grad_out: torch.Tensor): and fuse_gather_workload < _FUSE_GATHER_THRESHOLD ) + # The backward path indexes into grad_out [M_total, N] and x [M, K] + # using either M_idx (grouped) or scatter_idx (ungrouped). Overflow + # risk is dominated by the largest indexed buffer along the M axis. + needs_int64_bwd = ( + (M_total * N_dim) >= _INT_MAX + or (M_total * K_dim) >= _INT_MAX + or _needs_int64_indices(grad_out, x) + ) + if can_fuse_gather: # ------------------------------------------------------------------ # Fused path: skip group(x) entirely @@ -233,6 +254,7 @@ def backward(ctx, grad_out: torch.Tensor): k=k, scaling=scaling, dy_grouped=grouped_out, + int64_indices=needs_int64_bwd, ) # Prepare grouped_grad_out for the dX path (needed by both @@ -277,6 +299,7 @@ def backward(ctx, grad_out: torch.Tensor): expert_offsets=expert_offsets, E=E, scaling=scaling, + int64_indices=needs_int64_bwd, ) # ------------------------------------------------------------------ @@ -298,6 +321,7 @@ def backward(ctx, grad_out: torch.Tensor): dy_grouped=False, dx_grouped=grouped_in, out=d_expanded_input, + int64_indices=needs_int64_bwd, ) else: d_expanded_input = scatter2scatter_lora_dX_mx( @@ -312,6 +336,7 @@ def backward(ctx, grad_out: torch.Tensor): dy_grouped=True, dx_grouped=grouped_in, out=d_expanded_input, + int64_indices=needs_int64_bwd, ) elif ctx.use_fused_dX: if can_fuse_gather and not grouped_out: @@ -328,6 +353,7 @@ def backward(ctx, grad_out: torch.Tensor): dy_grouped=False, dx_grouped=grouped_in, out=d_expanded_input, + int64_indices=needs_int64_bwd, ) else: # Fused dX only: read from pre-grouped DY @@ -343,6 +369,7 @@ def backward(ctx, grad_out: torch.Tensor): dy_grouped=True, dx_grouped=grouped_in, out=d_expanded_input, + int64_indices=needs_int64_bwd, ) else: # Original path: separate base scatter2scatter + LoRA Python loop @@ -355,6 +382,7 @@ def backward(ctx, grad_out: torch.Tensor): k=1, y_grouped=grouped_in, out=d_expanded_input, + int64_indices=needs_int64_bwd, ) # LoRA part: dX_lora = scaling * (dY @ B) @ A diff --git a/tests/integrations/kernels/scattermoe_lora/bench_int64_kernel.py b/tests/integrations/kernels/scattermoe_lora/bench_int64_kernel.py new file mode 100644 index 0000000000..d4cad1fb2a --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/bench_int64_kernel.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +ScatterMoE — INT64_INDICES vs INT32 dense scatter2scatter benchmark. + +Times the dense ``kernels.ops.scatter2scatter`` at three representative +shapes and reports ms/iter for both ``INT64_INDICES=False`` (int32 fast +path) and ``INT64_INDICES=True`` (int64 safe path). The third shape is +the previously-failing seq=512K / 16-shard config; at that scale the +int32 path is incorrect (silent overflow corruption) so the int32 row +is gated by the ``_SCATTER2SCATTER_INT32_LIMIT`` and reported as the +chunked workaround's wall-clock instead (also for comparison against +the chunking baseline that PR #3667 shipped). + +Run from the repo root: + + python tests/integrations/kernels/scattermoe_lora/bench_int64_kernel.py + +A markdown summary is printed to stdout and written to +``bench_int64_kernel_results.md`` next to this script. +""" + +from __future__ import annotations + +import argparse +import statistics +import subprocess +from pathlib import Path +from typing import Callable + +import torch + +from axolotl.integrations.kernels.libs.scattermoe_lora.kernels.ops import ( + scatter2scatter, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, +) + +# Sufficient condition for int32 pointer arithmetic to overflow in the +# scatter2scatter Triton kernel: ``L_scattered * y_dim >= 2**31``. +_SCATTER2SCATTER_INT32_LIMIT = 2**31 + +DEVICE = "cuda" +DTYPE = torch.bfloat16 + + +def gpu_name() -> str: + try: + out = subprocess.check_output(["nvidia-smi", "-L"], text=True).strip() + first = out.splitlines()[0] + if ":" in first: + after_colon = first.split(":", 1)[1].strip() + return after_colon.split("(", 1)[0].strip() + return first + except Exception: + return torch.cuda.get_device_name(0) + + +def _time_ms(fn: Callable[[], torch.Tensor], iters: int = 10, warmup: int = 3) -> float: + for _ in range(warmup): + fn() + torch.cuda.synchronize() + samples = [] + for _ in range(iters): + start = torch.cuda.Event(enable_timing=True) + end = torch.cuda.Event(enable_timing=True) + start.record() + fn() + end.record() + torch.cuda.synchronize() + samples.append(start.elapsed_time(end)) + return statistics.median(samples) + + +def _build_inputs( + *, T: int, hidden: int, top_k: int, n: int, num_experts: int, seed: int +): + torch.manual_seed(seed) + x = torch.randn(T, hidden, device=DEVICE, dtype=DTYPE) + W = torch.randn(num_experts, hidden, n, device=DEVICE, dtype=DTYPE) * 0.02 + logits = torch.randn(T, num_experts, device=DEVICE) + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), top_k, dim=-1) + sei, ssi, _ = flatten_sort_count(top_idx, num_experts) + return x, W, sei, ssi + + +def _run_shape(name: str, *, T: int, hidden: int, top_k: int, n: int, num_experts: int): + x, W, sei, ssi = _build_inputs( + T=T, hidden=hidden, top_k=top_k, n=n, num_experts=num_experts, seed=42 + ) + L_scattered = sei.size(0) + out_elements = L_scattered * n + overflow = out_elements >= _SCATTER2SCATTER_INT32_LIMIT + auto_int64 = overflow # the wrapper's auto-dispatch verdict + + def call(int64_indices: bool): + return scatter2scatter( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=top_k, + x_grouped=False, + y_grouped=True, + int64_indices=int64_indices, + ) + + # Warm both Triton variants (separate JITs per constexpr). + if overflow: + # int32 path is unsafe at overflow shapes (silent corruption); skip. + ms_i32 = None + else: + ms_i32 = _time_ms(lambda: call(False)) + ms_i64 = _time_ms(lambda: call(True)) + + return { + "name": name, + "T": T, + "hidden": hidden, + "top_k": top_k, + "n": n, + "num_experts": num_experts, + "L_scattered": L_scattered, + "out_elements": out_elements, + "overflow": overflow, + "auto_int64": auto_int64, + "ms_i32": ms_i32, + "ms_i64": ms_i64, + } + + +def _fmt(v): + if v is None: + return "—" + return f"{v:.3f}" + + +def _markdown(rows, gpu_label: str) -> str: + lines = [] + lines.append("# scatter2scatter INT64_INDICES bench") + lines.append("") + lines.append(f"GPU: **{gpu_label}**") + lines.append("") + lines.append("Median of 10 iters, 3 warmup. `top_k=8`, dtype=bf16, 128 experts.") + lines.append("") + lines.append( + "`auto_int64` is the wrapper's auto-dispatch verdict from " + "`_needs_int64_indices`. At overflow shapes the int32 path " + "is silently incorrect (the multiplication wraps mid-buffer), " + "so only the int64 timing is reported." + ) + lines.append("") + lines.append( + "| Shape | T | L_scattered | out elems | auto_int64 | int32 ms | int64 ms | int64 vs int32 (%) |" + ) + lines.append("|---|---|---|---|---|---|---|---|") + for r in rows: + if r["ms_i32"] is not None and r["ms_i64"] is not None: + pen = 100.0 * (r["ms_i64"] - r["ms_i32"]) / r["ms_i32"] + pen_s = f"{pen:+.1f}" + else: + pen_s = "—" + lines.append( + f"| {r['name']} | {r['T']} | {r['L_scattered']} | " + f"{r['out_elements']:.2e} | {str(r['auto_int64'])} | " + f"{_fmt(r['ms_i32'])} | {_fmt(r['ms_i64'])} | {pen_s} |" + ) + lines.append("") + lines.append( + "Acceptance: ≤5% regression on the int32 fast path at " + "small/medium shapes (the auto-dispatch picks int32 there, so " + "this row characterises the JIT overhead of having an int64 " + "variant available)." + ) + lines.append("") + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--out", + type=str, + default=None, + help="Output markdown path (default: alongside script)", + ) + args = parser.parse_args() + + # Three representative shapes per the task spec. + shapes = [ + # name T (tokens before top_k expansion), hidden, top_k, N (out), num_experts + dict(name="small", T=8_192, hidden=2048, top_k=8, n=2048, num_experts=128), + dict(name="medium", T=128_000, hidden=2048, top_k=8, n=2048, num_experts=128), + # Overflow shape: 524288 / 16 shards = 32768 tokens, top_k=8 -> L=262144, + # N=16384 (= 2*intermediate at the bench config) -> 2**32 elements. + dict( + name="overflow_524k_s16", + T=32_768, + hidden=2048, + top_k=8, + n=16_384, + num_experts=128, + ), + ] + + rows = [] + for s in shapes: + print(f"running {s['name']} ...", flush=True) + rows.append(_run_shape(**s)) + torch.cuda.empty_cache() + + label = gpu_name() + md = _markdown(rows, label) + print(md) + + if args.out: + out_path = Path(args.out) + else: + out_path = Path(__file__).with_name("bench_int64_kernel_results.md") + out_path.write_text(md) + print(f"\nwrote {out_path}") + + +if __name__ == "__main__": + main() diff --git a/tests/integrations/kernels/scattermoe_lora/bench_int64_kernel_results.md b/tests/integrations/kernels/scattermoe_lora/bench_int64_kernel_results.md new file mode 100644 index 0000000000..3847039944 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/bench_int64_kernel_results.md @@ -0,0 +1,15 @@ +# scatter2scatter INT64_INDICES bench + +GPU: **NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition** + +Median of 10 iters, 3 warmup. `top_k=8`, dtype=bf16, 128 experts. + +`auto_int64` is the wrapper's auto-dispatch verdict from `_needs_int64_indices`. At overflow shapes the int32 path is silently incorrect (the multiplication wraps mid-buffer), so only the int64 timing is reported. + +| Shape | T | L_scattered | out elems | auto_int64 | int32 ms | int64 ms | int64 vs int32 (%) | +|---|---|---|---|---|---|---|---| +| small | 8192 | 65536 | 1.34e+08 | False | 2.699 | 2.704 | +0.2 | +| medium | 128000 | 1024000 | 2.10e+09 | False | 40.126 | 40.790 | +1.7 | +| overflow_524k_s16 | 32768 | 262144 | 4.29e+09 | True | — | 80.105 | — | + +Acceptance: ≤5% regression on the int32 fast path at small/medium shapes (the auto-dispatch picks int32 there, so this row characterises the JIT overhead of having an int64 variant available). diff --git a/tests/integrations/kernels/scattermoe_lora/test_parallel_experts_large_batch_repro.py b/tests/integrations/kernels/scattermoe_lora/test_parallel_experts_large_batch_repro.py new file mode 100644 index 0000000000..37a6a7f6ff --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_parallel_experts_large_batch_repro.py @@ -0,0 +1,289 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +Reproducer for the cuBLAS / illegal-memory-access failure surfaced at +seq=512K with 16 shards in the tiled-MLP long-context bench. + +The originally-reported symptom (``CUBLAS_STATUS_EXECUTION_FAILED`` from +``cublasGemmStridedBatchedEx`` at +``parallel_experts.py:72``'s ``gates.unsqueeze(1) @ output_expanded``) +is a downstream effect — the actual fault is in the upstream +``scatter2scatter`` Triton kernel's pointer-offset arithmetic. When the +output of the up-projection has +``L_scattered * y_dim >= 2 ** 31`` elements (i.e. the kernel's +``M_block * stride_ym`` int32 multiplication overflows), the kernel +silently writes to wrong addresses, which can in turn trip the next +kernel (the gates @ output_expanded bmm or whatever else follows). + +The repro shape mirrors the failing bench config: + +* shard tokens ``T = 32768`` (= 524288 // 16), +* ``top_k = 8`` → ``L_scattered = T * top_k = 262144``, +* ``num_experts = 128``, ``hidden = 2048``, ``intermediate = 8192``. + +At that shape, the up-projection's scatter2scatter output is +``[262144, 2 * 8192] = [262144, 16384]`` = 2**32 elements. The +overflow boundary for the M_block * stride_ym int32 product is +``M_block < 2 ** 31 / 16384 = 131072`` — exactly half the output rows. +""" + +from __future__ import annotations + +import pytest +import torch + +# Failing-config constants (mirror tests/integrations/monkeypatch/ +# bench_tiled_mlp_moe.py at seq=524288, shards=16). +_T = 32768 +_TOP_K = 8 +_NUM_EXPERTS = 128 +_HIDDEN = 2048 +_INTERMEDIATE = 8192 +_DTYPE = torch.bfloat16 + + +def _requires_cuda(): + return pytest.mark.skipif( + not torch.cuda.is_available(), reason="CUDA required for the repro" + ) + + +_SCATTER2SCATTER_INT32_LIMIT = 2**31 + + +@_requires_cuda() +def test_scatter2scatter_below_threshold_no_overhead(): + """At shapes well below the int32 overflow boundary the auto-dispatch + in ``ParallelLinear`` picks ``INT64_INDICES=False`` and the kernel + output is bit-identical to a direct call with the same flag. + + This is the regression guard for "don't accidentally penalise the + common-case path that does not need int64 indices". + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.kernels.ops import ( + scatter2scatter, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, + ) + + device = torch.device("cuda:0") + torch.manual_seed(0) + + # Small shape: T=512 tokens → L_scattered=4096 → output is + # 4096 * 16384 = 67 M elements, well below the 2**31 threshold. + T_small = 512 + x = torch.randn(T_small, _HIDDEN, device=device, dtype=_DTYPE) + W = ( + torch.randn( + _NUM_EXPERTS, _HIDDEN, 2 * _INTERMEDIATE, device=device, dtype=_DTYPE + ) + * 0.01 + ) + + logits = torch.randn(T_small, _NUM_EXPERTS, device=device) + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), _TOP_K, dim=-1) + sei, ssi, _ = flatten_sort_count(top_idx, _NUM_EXPERTS) + + assert sei.size(0) * W.size(-1) < _SCATTER2SCATTER_INT32_LIMIT + + out_i32 = scatter2scatter( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=_TOP_K, + x_grouped=False, + y_grouped=True, + int64_indices=False, + ) + out_i64 = scatter2scatter( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=_TOP_K, + x_grouped=False, + y_grouped=True, + int64_indices=True, + ) + torch.cuda.synchronize() + assert torch.equal(out_i32, out_i64), ( + "INT64_INDICES must not change MMA/accumulation order at small shapes" + ) + + +@_requires_cuda() +def test_scatter2scatter_no_corruption_at_overflow_shape(): + """The kernel-level int64 fix must keep every output row populated + when the shape straddles the 2**31-element boundary. + + Background: with INT64_INDICES=False the Triton ``scatter2scatter`` + kernel computes pointer offsets as + ``Y_ptr + M_block * stride_ym + N_block * stride_yn`` in int32. At + the bench shape (L_scattered=262144, y_dim=16384 → 2**32 elements + of output) the trailing rows past ``M_block >= 2**31 / y_dim`` + overflow and their masked stores silently drop, leaving those rows + as all-zeros. With INT64_INDICES=True the M_block range is cast to + int64 before it enters the multiplication and the overflow is + eliminated at the kernel level. + + This test calls the kernel directly with INT64_INDICES=True and + asserts every sampled row past the boundary has at least one + non-zero element. (The ``ParallelLinear`` wrapper's auto-dispatch + is covered separately by ``test_parallel_linear_long_seq_routing_combination``.) + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.kernels.ops import ( + scatter2scatter, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, + ) + + device = torch.device("cuda:0") + torch.manual_seed(0) + + x = torch.randn(_T, _HIDDEN, device=device, dtype=_DTYPE) + W = ( + torch.randn( + _NUM_EXPERTS, _HIDDEN, 2 * _INTERMEDIATE, device=device, dtype=_DTYPE + ) + * 0.01 + ) + + logits = torch.randn(_T, _NUM_EXPERTS, device=device) + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), _TOP_K, dim=-1) + sei, ssi, _ = flatten_sort_count(top_idx, _NUM_EXPERTS) + + L_scattered = sei.size(0) + y_dim = W.size(-1) + assert L_scattered * y_dim >= _SCATTER2SCATTER_INT32_LIMIT, ( + f"repro precondition: L_scattered * y_dim ({L_scattered * y_dim}) " + f"must straddle the int32 overflow boundary " + f"({_SCATTER2SCATTER_INT32_LIMIT})" + ) + + output = scatter2scatter( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=_TOP_K, + x_grouped=False, + y_grouped=True, + int64_indices=True, + ) + torch.cuda.synchronize() + + overflow_threshold_row = _SCATTER2SCATTER_INT32_LIMIT // y_dim + sample_rows = [ + 0, + overflow_threshold_row // 2, + overflow_threshold_row - 1, + overflow_threshold_row, + overflow_threshold_row + 1, + (overflow_threshold_row + L_scattered) // 2, + L_scattered - 1, + ] + for row in sample_rows: + nz = (output[row] != 0).any().item() + assert nz, ( + f"row {row} of scatter2scatter output is all-zero " + f"(overflow_threshold_row={overflow_threshold_row}, " + f"L_scattered={L_scattered}, y_dim={y_dim})" + ) + + +@_requires_cuda() +def test_parallel_linear_long_seq_routing_combination(): + """End-to-end repro through ``parallel_linear`` matching the bench path. + + Replicates the ``ScatterMoEGatedMLP.forward`` shape sequence (up + projection at line 374 → activation → down projection at line 385 + with ``gates=routing_weights``) at the seq=524288/shards=16 inner + config. Before the fix this raises + ``CUBLAS_STATUS_EXECUTION_FAILED`` (or a subsequent illegal-memory- + access) at the down-projection's ``gates @ output_expanded`` bmm. + """ + import torch.nn.functional as F + + from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, + parallel_linear, + ) + + device = torch.device("cuda:0") + torch.manual_seed(0) + + layer_input = torch.randn(_T, _HIDDEN, device=device, dtype=_DTYPE) + # Match the bench's ScatterMoEGatedMLP weight layout: input_linear + # is [E, 2*INTERMEDIATE, HIDDEN] then .transpose(2, 1) → + # [E, HIDDEN, 2*INTERMEDIATE]. output_linear is [E, HIDDEN, + # INTERMEDIATE] then .transpose(2, 1) → [E, INTERMEDIATE, HIDDEN]. + in_w = ( + torch.randn( + _NUM_EXPERTS, 2 * _INTERMEDIATE, _HIDDEN, device=device, dtype=_DTYPE + ) + * 0.02 + ) + out_w = ( + torch.randn(_NUM_EXPERTS, _HIDDEN, _INTERMEDIATE, device=device, dtype=_DTYPE) + * 0.02 + ) + + router_logits = torch.randn(_T, _NUM_EXPERTS, device=device) + routing_weights = F.softmax(router_logits, dim=-1, dtype=torch.float) + routing_weights, selected_experts = torch.topk(routing_weights, _TOP_K, dim=-1) + routing_weights = routing_weights / routing_weights.sum(dim=-1, keepdim=True) + routing_weights = routing_weights.to(_DTYPE) + + sei, ssi, eo = flatten_sort_count(selected_experts, _NUM_EXPERTS) + + # Up projection — this is the overflow-prone call (output + # numel = T * top_k * 2*INTERMEDIATE = 2**32 at the bench shape). + with torch.no_grad(): + gup = parallel_linear( + layer_input, + in_w.transpose(2, 1), + _TOP_K, + sei, + ssi, + eo, + grouped_in=False, + grouped_out=True, + ) + gates, h = gup.chunk(2, dim=-1) + h = F.silu(gates) * h + + # Down projection — its gates @ output_expanded bmm is where + # the reported CUBLAS_STATUS_EXECUTION_FAILED surfaces. The + # crash, however, is a downstream symptom of the up-projection + # corruption above. + layer_output = parallel_linear( + h, + out_w.transpose(2, 1), + 1, + sei, + ssi, + eo, + grouped_in=True, + grouped_out=False, + gates=routing_weights, + ) + # Force the (otherwise lazy) CUDA error to surface synchronously. + torch.cuda.synchronize() + + assert layer_output.shape == (_T, _HIDDEN) + # The output must have real values, not zero rows or NaN/Inf. + # ``(.abs().sum(dim=-1) == 0)`` would catch the silent-zero + # corruption pattern even when the kernel did not crash hard. + assert torch.isfinite(layer_output).all().item(), ( + "layer_output has non-finite values — likely overflow corruption" + ) + row_sums = layer_output.float().abs().sum(dim=-1) + assert (row_sums > 0).all().item(), ( + "layer_output has all-zero rows — silent overflow corruption " + "in the up-projection scatter2scatter" + ) diff --git a/tests/integrations/kernels/scattermoe_lora/test_scattermoe_lora_int64_indices.py b/tests/integrations/kernels/scattermoe_lora/test_scattermoe_lora_int64_indices.py new file mode 100644 index 0000000000..88c77865fd --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_scattermoe_lora_int64_indices.py @@ -0,0 +1,581 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +""" +Parity and overflow-correctness tests for the ``INT64_INDICES`` +``tl.constexpr`` knob added to the scattermoe-lora Triton kernels. + +The kernel-level fix promotes the per-launch index ranges to int64 only +when the wrapper has detected that ``L_scattered * y_dim`` would +overflow int32. Two properties are tested: + +1. **Bitwise parity at small shapes.** When the shape fits in int32, + ``INT64_INDICES=False`` (the JIT'd int32 variant) and + ``INT64_INDICES=True`` (the int64 variant) compute the same MMA in + the same order. Only the index *type* changes, so the outputs must + be bitwise identical — any deviation indicates the cast leaked into + the accumulator path. + +2. **Overflow correctness at large shapes.** At the previously-failing + bench config (seq=524288 with 16 shards, L_scattered=262144, + y_dim=16384 → 2**32 element output), the int64 kernel must populate + every row of the output and match the chunked workaround within bf16 + tolerance (the chunking workaround changes accumulation order, so + bit-equality is not expected against it — only against the same- + layout int32 kernel below the overflow boundary). + +The bench-config test is gated by GPU memory; an L_scattered=262144 +× 16384 bf16 output is ~8.6 GiB and the up-projection weight is +~64 GiB so we skip when free memory is below the threshold. +""" + +from __future__ import annotations + +import pytest +import torch + +from axolotl.integrations.kernels.libs.scattermoe_lora.kernels import ( + lora_ops, + ops as base_ops, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, +) + +DEVICE = "cuda" +DTYPE = torch.bfloat16 + +# Sufficient condition for int32 pointer arithmetic to overflow in the +# Triton kernel: any indexed buffer has >= 2**31 elements. +_INT32_LIMIT = 2**31 + + +def _requires_cuda(): + return pytest.mark.skipif( + not torch.cuda.is_available(), reason="CUDA not available" + ) + + +pytestmark = _requires_cuda() + + +# ─── Helpers ───────────────────────────────────────────────────────────────── + + +def _setup(E, K, N, T, top_k, R=16, seed=42): + """Create synthetic inputs + routing for a (E, K, N, T, k) shape.""" + torch.manual_seed(seed) + x = torch.randn(T, K, device=DEVICE, dtype=DTYPE) + W = torch.randn(E, K, N, device=DEVICE, dtype=DTYPE) * 0.02 + lora_A = torch.randn(R * E, K, device=DEVICE, dtype=DTYPE) * 0.01 + lora_B = torch.randn(N, R * E, device=DEVICE, dtype=DTYPE) * 0.01 + logits = torch.randn(T, E, device=DEVICE) + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), top_k, dim=-1) + sei, ssi, eo = flatten_sort_count(top_idx, E) + return x, W, lora_A, lora_B, sei, ssi, eo + + +# ─── Parity tests at non-overflow shapes (bitwise identity) ────────────────── + + +def test_dense_scatter2scatter_int64_parity_small(): + """Dense scatter2scatter: INT64_INDICES=True == INT64_INDICES=False at small shape.""" + x, W, *_, sei, ssi, _ = _setup(E=8, K=512, N=1024, T=256, top_k=4) + k = 4 + out_i32 = base_ops.scatter2scatter( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + x_grouped=False, + y_grouped=True, + int64_indices=False, + ) + out_i64 = base_ops.scatter2scatter( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + x_grouped=False, + y_grouped=True, + int64_indices=True, + ) + torch.cuda.synchronize() + assert torch.equal(out_i32, out_i64), ( + "INT64_INDICES must not change accumulation order at non-overflow shapes" + ) + + +def test_dense_scatter2scatter_int64_parity_ungrouped_out(): + """Same parity but y_grouped=False (uses M_idx scatter lookup, not M_block).""" + x, W, *_, sei, ssi, _ = _setup(E=8, K=512, N=1024, T=256, top_k=4) + k = 4 + out_i32 = base_ops.scatter2scatter( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + x_grouped=False, + y_grouped=False, + int64_indices=False, + ) + out_i64 = base_ops.scatter2scatter( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + x_grouped=False, + y_grouped=False, + int64_indices=True, + ) + torch.cuda.synchronize() + assert torch.equal(out_i32, out_i64) + + +def test_scatter2scatter_lora_int64_parity_small(): + """scatter2scatter_lora: int32 vs int64 must agree bitwise.""" + # Pick a shape that lands on the fused path (not split): few-large-experts + # split threshold is E<=32 with K*N >= 20M, so use a small K*N to stay + # on the fused kernel. + x, W, lA, lB, sei, ssi, _ = _setup(E=64, K=256, N=512, T=128, top_k=4) + k = 4 + scaling = 0.5 + out_i32 = lora_ops.scatter2scatter_lora( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + lora_A=lA, + lora_B=lB, + scaling=scaling, + x_grouped=False, + y_grouped=True, + int64_indices=False, + ) + out_i64 = lora_ops.scatter2scatter_lora( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + lora_A=lA, + lora_B=lB, + scaling=scaling, + x_grouped=False, + y_grouped=True, + int64_indices=True, + ) + torch.cuda.synchronize() + assert torch.equal(out_i32, out_i64) + + +def test_scatter2scatter_lora_dX_int64_parity_small(): + """scatter2scatter_lora_dX: int32 vs int64 must agree bitwise.""" + _, W, lA, lB, sei, ssi, _ = _setup(E=64, K=256, N=512, T=128, top_k=4) + k = 4 + scaling = 0.5 + M_grouped = sei.size(0) # ungrouped k=1 dy_grouped=True + dy = torch.randn(M_grouped, W.size(2), device=DEVICE, dtype=DTYPE) * 0.01 + dX_i32 = lora_ops.scatter2scatter_lora_dX( + DY=dy, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + lora_A=lA, + lora_B=lB, + scaling=scaling, + dy_grouped=True, + dx_grouped=False, + int64_indices=False, + ) + dX_i64 = lora_ops.scatter2scatter_lora_dX( + DY=dy, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=k, + lora_A=lA, + lora_B=lB, + scaling=scaling, + dy_grouped=True, + dx_grouped=False, + int64_indices=True, + ) + torch.cuda.synchronize() + assert torch.equal(dX_i32, dX_i64) + + +def test_group_bwd_lora_int64_parity_small(): + """group_bwd_lora (split kernel): int32 vs int64 must agree bitwise.""" + x, W, lA, lB, sei, ssi, eo = _setup(E=16, K=256, N=512, T=128, top_k=2) + grouped_x = base_ops.group(x, ssi, fan_out=2) + M = grouped_x.size(0) + dy = torch.randn(M, W.size(2), device=DEVICE, dtype=DTYPE) * 0.01 + scaling = 0.5 + dA_i32, dB_i32 = lora_ops.group_bwd_lora( + DY=dy, + X=grouped_x, + lora_A=lA, + lora_B=lB, + expert_offsets=eo, + E=16, + scaling=scaling, + int64_indices=False, + ) + dA_i64, dB_i64 = lora_ops.group_bwd_lora( + DY=dy, + X=grouped_x, + lora_A=lA, + lora_B=lB, + expert_offsets=eo, + E=16, + scaling=scaling, + int64_indices=True, + ) + torch.cuda.synchronize() + assert torch.equal(dA_i32, dA_i64) + assert torch.equal(dB_i32, dB_i64) + + +def test_group_bwd_lora_fused_int64_parity_small(): + """group_bwd_lora_fused: int32 vs int64 must agree within bf16 tolerance. + + Unlike the split kernel, the fused kernel writes dA/dB via + ``tl.atomic_add``; the order in which (E, K-tile, N-tile) thread blocks + land their atomics is non-deterministic, so bit-equality is not + achievable even between two launches of the *same* kernel variant. + The INT64 cast only changes index *types*, not the MMA path or the + atomic reduction, so the two variants must still match within + ``torch.allclose`` bf16 tolerance. + """ + x, _W, lA, lB, _sei, ssi, eo = _setup(E=16, K=256, N=512, T=128, top_k=2) + k = 2 + M_total = ssi.size(0) + N = lB.size(0) + dy = torch.randn(M_total, N, device=DEVICE, dtype=DTYPE) * 0.01 + scaling = 0.5 + dA_i32, dB_i32 = lora_ops.group_bwd_lora_fused( + DY=dy, + X=x, + lora_A=lA, + lora_B=lB, + expert_offsets=eo, + sorted_scattered_idxs=ssi, + E=16, + k=k, + scaling=scaling, + dy_grouped=False, + int64_indices=False, + ) + dA_i64, dB_i64 = lora_ops.group_bwd_lora_fused( + DY=dy, + X=x, + lora_A=lA, + lora_B=lB, + expert_offsets=eo, + sorted_scattered_idxs=ssi, + E=16, + k=k, + scaling=scaling, + dy_grouped=False, + int64_indices=True, + ) + torch.cuda.synchronize() + # Tolerance: a few bf16 ULPs is expected from atomic-add ordering nondet. + assert torch.allclose(dA_i32, dA_i64, rtol=1e-2, atol=5e-4), ( + f"max_abs_diff dA: {(dA_i32.float() - dA_i64.float()).abs().max()}" + ) + assert torch.allclose(dB_i32, dB_i64, rtol=1e-2, atol=5e-4), ( + f"max_abs_diff dB: {(dB_i32.float() - dB_i64.float()).abs().max()}" + ) + + +# ─── Overflow correctness at the bench shape ───────────────────────────────── + + +# Bench-shape constants (mirror ``test_parallel_experts_large_batch_repro.py``). +_T = 32768 +_TOP_K = 8 +_NUM_EXPERTS = 128 +_HIDDEN = 2048 +_INTERMEDIATE = 8192 + + +def _has_free_gpu_mem(min_gb: float) -> bool: + if not torch.cuda.is_available(): + return False + free, _total = torch.cuda.mem_get_info() + return free / (1024**3) >= min_gb + + +@pytest.mark.skipif( + not _has_free_gpu_mem(80.0), + reason="bench shape needs ~80 GiB free GPU memory", +) +def test_dense_scatter2scatter_int64_at_overflow_shape(): + """Direct int64 kernel call at the bench shape produces no zero rows. + + With ``INT64_INDICES=True`` the kernel's pointer arithmetic stays in + int64 across the full output row range, so rows past the would-be + int32 overflow boundary (``M_block >= 2**31 / y_dim``) are populated + rather than silently dropped. + """ + device = torch.device("cuda:0") + torch.manual_seed(0) + x = torch.randn(_T, _HIDDEN, device=device, dtype=DTYPE) + W = ( + torch.randn( + _NUM_EXPERTS, _HIDDEN, 2 * _INTERMEDIATE, device=device, dtype=DTYPE + ) + * 0.01 + ) + + logits = torch.randn(_T, _NUM_EXPERTS, device=device) + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), _TOP_K, dim=-1) + sei, ssi, _ = flatten_sort_count(top_idx, _NUM_EXPERTS) + + L_scattered = sei.size(0) + y_dim = W.size(-1) + assert L_scattered * y_dim >= _INT32_LIMIT, ( + "precondition: shape must straddle the int32 overflow boundary" + ) + + out_i64 = base_ops.scatter2scatter( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=_TOP_K, + x_grouped=False, + y_grouped=True, + int64_indices=True, + ) + torch.cuda.synchronize() + + # Sample rows on both sides of the int32 overflow boundary. + overflow_threshold_row = _INT32_LIMIT // y_dim + sample_rows = [ + 0, + overflow_threshold_row - 1, + overflow_threshold_row, + overflow_threshold_row + 1, + L_scattered - 1, + ] + for row in sample_rows: + assert (out_i64[row] != 0).any().item(), ( + f"int64 kernel left row {row} all-zero (overflow boundary " + f"= {overflow_threshold_row}, L_scattered = {L_scattered})" + ) + assert torch.isfinite(out_i64).all().item(), ( + "int64 kernel produced non-finite values at overflow shape" + ) + + +@pytest.mark.skipif( + not _has_free_gpu_mem(80.0), + reason="bench shape needs ~80 GiB free GPU memory", +) +def test_parallel_linear_overflow_takes_int64_kernel_path(monkeypatch): + """``ParallelLinear.forward`` at the bench shape must route through + the int64 kernel path (single launch, ``int64_indices=True``). + + The auto-dispatch should set ``needs_int64=True`` and dispatch a + single ``scatter2scatter`` launch with that flag. A regressed path + that called the kernel multiple times (e.g. a chunking workaround) + would invoke ``scatter2scatter_compileable`` more than once and + fail this assertion. + """ + from axolotl.integrations.kernels.libs.scattermoe_lora import parallel_experts + from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + parallel_linear, + ) + + device = torch.device("cuda:0") + torch.manual_seed(0) + + x = torch.randn(_T, _HIDDEN, device=device, dtype=DTYPE) + W = ( + torch.randn( + _NUM_EXPERTS, _HIDDEN, 2 * _INTERMEDIATE, device=device, dtype=DTYPE + ) + * 0.01 + ) + + logits = torch.randn(_T, _NUM_EXPERTS, device=device) + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), _TOP_K, dim=-1) + sei, ssi, eo = flatten_sort_count(top_idx, _NUM_EXPERTS) + + # Spy on the kernel launches. The kernel-level int64 fix dispatches + # exactly one ``scatter2scatter_compileable`` call with + # ``int64_indices=True``. A re-introduced chunking workaround would + # invoke it once per chunk (>=2 at this shape). + launches = [] + real_compileable = parallel_experts.kernels.ops.scatter2scatter_compileable + + def _spy_compileable(*args, **kwargs): + # int64_indices is positional arg 9 (after b, x_grouped, y_grouped). + launches.append( + { + "args_len": len(args), + "int64": args[9] + if len(args) > 9 + else kwargs.get("int64_indices", False), + } + ) + return real_compileable(*args, **kwargs) + + monkeypatch.setattr( + parallel_experts.kernels.ops, + "scatter2scatter_compileable", + _spy_compileable, + ) + + with torch.no_grad(): + out = parallel_linear( + x, + W, + _TOP_K, + sei, + ssi, + eo, + grouped_in=False, + grouped_out=True, + ) + torch.cuda.synchronize() + + assert len(launches) == 1, ( + f"expected exactly one kernel launch (direct int64 path), got {len(launches)}" + ) + assert launches[0]["int64"] is True, ( + "auto-dispatch should have set int64_indices=True at the overflow shape" + ) + assert out.shape == (_T * _TOP_K, 2 * _INTERMEDIATE) + assert torch.isfinite(out).all().item() + + +# ─── Smaller-shape overflow (runs on L40S / 24 GiB GPUs) ───────────────────── +# L_scattered * y_dim = 2**32 (2× past 2**31); peak VRAM ≈ 8 GiB. +_SMALL_T = 131072 +_SMALL_TOP_K = 8 +_SMALL_E = 8 +_SMALL_K = 256 +_SMALL_INTERMEDIATE = 2048 +_SMALL_MIN_FREE_GIB = 12.0 + + +@pytest.mark.skipif( + not _has_free_gpu_mem(_SMALL_MIN_FREE_GIB), + reason=f"small overflow shape needs ~{_SMALL_MIN_FREE_GIB:.0f} GiB free GPU memory", +) +def test_dense_scatter2scatter_int64_at_overflow_shape_small(): + device = torch.device("cuda:0") + torch.manual_seed(0) + y_dim = 2 * _SMALL_INTERMEDIATE + x = torch.randn(_SMALL_T, _SMALL_K, device=device, dtype=DTYPE) + W = torch.randn(_SMALL_E, _SMALL_K, y_dim, device=device, dtype=DTYPE) * 0.01 + + logits = torch.randn(_SMALL_T, _SMALL_E, device=device) + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), _SMALL_TOP_K, dim=-1) + sei, ssi, _ = flatten_sort_count(top_idx, _SMALL_E) + + L_scattered = sei.size(0) + assert L_scattered * y_dim >= _INT32_LIMIT, ( + f"precondition: L_scattered * y_dim ({L_scattered * y_dim}) must " + f"straddle the int32 overflow boundary ({_INT32_LIMIT})" + ) + + out_i64 = base_ops.scatter2scatter( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=_SMALL_TOP_K, + x_grouped=False, + y_grouped=True, + int64_indices=True, + ) + torch.cuda.synchronize() + + overflow_threshold_row = _INT32_LIMIT // y_dim + sample_rows = [ + 0, + overflow_threshold_row - 1, + overflow_threshold_row, + overflow_threshold_row + 1, + L_scattered - 1, + ] + for row in sample_rows: + assert (out_i64[row] != 0).any().item(), ( + f"int64 kernel left row {row} all-zero (overflow boundary " + f"= {overflow_threshold_row}, L_scattered = {L_scattered})" + ) + assert torch.isfinite(out_i64).all().item() + + +@pytest.mark.skipif( + not _has_free_gpu_mem(_SMALL_MIN_FREE_GIB), + reason=f"small overflow shape needs ~{_SMALL_MIN_FREE_GIB:.0f} GiB free GPU memory", +) +def test_parallel_linear_overflow_takes_int64_kernel_path_small(monkeypatch): + from axolotl.integrations.kernels.libs.scattermoe_lora import parallel_experts + from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + parallel_linear, + ) + + device = torch.device("cuda:0") + torch.manual_seed(0) + y_dim = 2 * _SMALL_INTERMEDIATE + x = torch.randn(_SMALL_T, _SMALL_K, device=device, dtype=DTYPE) + W = torch.randn(_SMALL_E, _SMALL_K, y_dim, device=device, dtype=DTYPE) * 0.01 + + logits = torch.randn(_SMALL_T, _SMALL_E, device=device) + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), _SMALL_TOP_K, dim=-1) + sei, ssi, eo = flatten_sort_count(top_idx, _SMALL_E) + + launches = [] + real_compileable = parallel_experts.kernels.ops.scatter2scatter_compileable + + def _spy_compileable(*args, **kwargs): + launches.append( + { + "int64": args[9] + if len(args) > 9 + else kwargs.get("int64_indices", False), + } + ) + return real_compileable(*args, **kwargs) + + monkeypatch.setattr( + parallel_experts.kernels.ops, + "scatter2scatter_compileable", + _spy_compileable, + ) + + with torch.no_grad(): + out = parallel_linear( + x, + W, + _SMALL_TOP_K, + sei, + ssi, + eo, + grouped_in=False, + grouped_out=True, + ) + torch.cuda.synchronize() + + assert len(launches) == 1, ( + f"expected exactly one kernel launch (direct int64 path), got {len(launches)}" + ) + assert launches[0]["int64"] is True, ( + "auto-dispatch should have set int64_indices=True at the overflow shape" + ) + assert out.shape == (_SMALL_T * _SMALL_TOP_K, y_dim) + assert torch.isfinite(out).all().item() diff --git a/tests/integrations/kernels/scattermoe_lora/test_scattermoe_lora_m_bucket.py b/tests/integrations/kernels/scattermoe_lora/test_scattermoe_lora_m_bucket.py new file mode 100644 index 0000000000..5d1c4d6b19 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_scattermoe_lora_m_bucket.py @@ -0,0 +1,118 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Tests for the ``M_BUCKET`` autotune-key bucketing on the scattermoe-lora +fused forward kernel. + +The kernel runs on the real ``M`` (loop bounds + masks); only the +``@triton.autotune`` cache key is bucketed via :func:`_bucket_m`. These tests +pin both halves of that contract: + + * ``_bucket_m`` rounds up to a multiple of the granularity (pure-Python + unit test, no GPU). + * Two distinct real ``M`` values that share a bucket produce **one** + cache entry (the whole point — no resweep on small seqlen variation). + * Two real ``M`` values in different buckets produce **two** cache + entries (we didn't accidentally collapse to a single key). + +Run on CUDA only; the bucketing assertion needs an actual Triton launch. +""" + +from __future__ import annotations + +import pytest +import torch + +from axolotl.integrations.kernels.libs.scattermoe_lora.kernels import lora_ops +from axolotl.integrations.kernels.libs.scattermoe_lora.kernels.lora_ops import ( + _M_BUCKET_GRANULARITY, + _bucket_m, + scatter2scatter_lora, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( + flatten_sort_count, +) + + +def test_bucket_m_rounds_up_to_granularity(): + g = _M_BUCKET_GRANULARITY + assert _bucket_m(1) == g + assert _bucket_m(g) == g + assert _bucket_m(g + 1) == 2 * g + assert _bucket_m(2 * g) == 2 * g + # Realistic seqlen variation: at granularity=1024 and top_k=8 the three + # seqlens 16300/16400/16500 straddle one bucket boundary, so they collapse + # to 2 cache entries rather than 3 (16400 and 16500 share a bucket). + assert _bucket_m(16400 * 8) == _bucket_m(16500 * 8) + assert _bucket_m(16300 * 8) != _bucket_m(16400 * 8) + distinct = {_bucket_m(s * 8) for s in (16300, 16400, 16500)} + assert len(distinct) == 2 + + +pytestmark = pytest.mark.skipif( + not torch.cuda.is_available(), reason="CUDA required for kernel launch" +) + + +_DEVICE = "cuda" +_DTYPE = torch.bfloat16 +_E = 4 +_K = 64 +_N = 64 +_TOP_K = 2 +_R = 16 + + +def _launch_once(m: int) -> None: + """One fused fwd launch at the given real M; minimal shapes for speed.""" + torch.manual_seed(m) + x = torch.randn(m, _K, device=_DEVICE, dtype=_DTYPE) + W = torch.randn(_E, _K, _N, device=_DEVICE, dtype=_DTYPE) * 0.02 + lora_A = torch.randn(_R * _E, _K, device=_DEVICE, dtype=_DTYPE) * 0.01 + lora_B = torch.randn(_N, _R * _E, device=_DEVICE, dtype=_DTYPE) * 0.01 + logits = torch.randn(m, _E, device=_DEVICE) + _, top_idx = torch.topk(torch.softmax(logits, dim=-1), _TOP_K, dim=-1) + sei, ssi, _ = flatten_sort_count(top_idx, _E) + scatter2scatter_lora( + X=x, + W=W, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=_TOP_K, + lora_A=lora_A, + lora_B=lora_B, + scaling=1.0 / _R, + ) + + +def test_autotune_cache_collapses_within_bucket_and_grows_across_buckets(): + cache = lora_ops._scatter2scatter_lora.cache + cache.clear() + + g = _M_BUCKET_GRANULARITY + # Two M values that both ceil to bucket B1. + m_a = g - 1 + m_b = g // 2 + 1 + assert _bucket_m(m_a) == g + assert _bucket_m(m_b) == g + + _launch_once(m_a) + assert len(cache) == 1, ( + f"first launch should create exactly one cache entry, got {len(cache)}" + ) + + _launch_once(m_b) + assert len(cache) == 1, ( + f"second launch in the same bucket must not add a cache entry " + f"(M={m_a} and M={m_b} both bucket to {g}); got {len(cache)} entries" + ) + + # An M strictly past the bucket boundary lands in bucket 2*g. + m_c = g + 1 + assert _bucket_m(m_c) == 2 * g + _launch_once(m_c) + assert len(cache) == 2, ( + f"launch in a different bucket (M={m_c} -> {2 * g}) must add a " + f"second cache entry; got {len(cache)}" + ) diff --git a/tests/integrations/test_scattermoe_autotune_telemetry.py b/tests/integrations/test_scattermoe_autotune_telemetry.py index 7050c0f4f5..43010f4218 100644 --- a/tests/integrations/test_scattermoe_autotune_telemetry.py +++ b/tests/integrations/test_scattermoe_autotune_telemetry.py @@ -104,7 +104,7 @@ def test_populated_cache_returns_configs(self): assert len(result) == 1 entry = result[0] assert entry["kernel"] == "scatter2scatter_lora_fwd" - assert entry["key"] == {"M": 2048, "N": 4096, "K": 1024} + assert entry["key"] == {"M_BUCKET": 2048, "N": 4096, "K": 1024} assert entry["config"]["BLOCK_N"] == 128 assert entry["config"]["BLOCK_K"] == 64 assert entry["config"]["num_warps"] == 8 @@ -148,7 +148,7 @@ def test_extra_key_elements_stored(self): assert len(result) == 1 key = result[0]["key"] - assert key["M"] == 512 + assert key["M_BUCKET"] == 512 assert key["N"] == 1024 assert key["K"] == 256 assert key["_extra"] == ["float16", "float16"] From 280506e58dbeb323b30f1071549ea930731d06ae Mon Sep 17 00:00:00 2001 From: thad0ctor Date: Thu, 28 May 2026 20:23:09 -0700 Subject: [PATCH 1331/1405] feat(qwen): fused RMSNorm+RoPE for Qwen3/3.X family + Liger m-rope default (#3680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(qwen): fused RMSNorm+RoPE for Qwen3 / Qwen3-MoE / Qwen3.5 / Qwen3.5-MoE Generalizes the existing Gemma 4 fused RMSNorm+RoPE Triton kernel to four new Qwen attention variants, and auto-enables Liger's fused (m-)rope kernel for the Qwen-VL family. Eager-mode behavior is bit-identical when the new cfg.fused_attn_kernel flag is unset. Changes ------- * New ``cfg.fused_attn_kernel: bool | None`` (default None / off). When set, replaces ``q_norm + apply_rotary_pos_emb`` (and the matching K path) with a single fused RMSNorm+RoPE Triton kernel launch. Currently wired for ``qwen3``, ``qwen3_moe``, ``qwen3_5``, and ``qwen3_5_moe`` model_config_types. Llama4 is out of scope (complex freqs_cis + Llama4TextL2Norm post-RoPE — separate kernel). * Kernel ``UNIT_OFFSET: tl.constexpr`` flag added to the forward + backward Triton kernels for Qwen3.5's Gemma-style ``(1.0 + weight)`` RMSNorm. Default ``False`` keeps Gemma 4 / Qwen3 / Qwen3-MoE bit-identical to before. Threaded through the triton_op + register_autograd plumbing. * Refactors ``fused_rms_norm_rope`` / ``fused_rms_norm_noscale`` to ``torch.library.triton_op`` + ``register_autograd`` so they trace under ``torch.compile(fullgraph=True)``. Validated: 1 Dynamo frame, 0 graph breaks. On sm_120 the compile path composes to +9.2% combined, −33% peak memory. On sm_86 the surrounding Inductor-generated kernels regress — leave ``torch_compile: false`` there; schema description documents the per-arch recommendation. * Liger Qwen-VL auto-default: when ``cfg.liger_rope is None`` and model_config_type is one of qwen2_vl/qwen2_5_vl/qwen3_vl (+ ``_text`` variants), pass ``rope=True`` so upstream's fused m-rope kernel is actually installed. Previously the plugin overrode the upstream default to None, silently skipping the kernel. * Patch-ordering fix: ``_apply_self_attention_lora_patch`` now runs before ``_apply_model_specific_patches`` in ``apply_pre_model_load_patches``. ``patch_self_attn_lora`` reads ``inspect.getsource`` of the attention class' forward, so any patch that replaces ``Attention.forward`` must run *after* the source-rewrite step. The wrong order also silently broke Gemma 4 + ``lora_qkv_kernel`` — pinned by ``TestPatchManagerOrdering`` and a fused-first trip-wire. Tests ----- * Per-model parity + backward grad flow for Qwen3, Qwen3-MoE, Qwen3.5, Qwen3.5-MoE (full-attention layers only; linear_attention layers stay on the stock GatedDeltaNet path). * Kernel ``UNIT_OFFSET=True`` parity vs from-scratch reference + bwd parity vs torch-eager + ``torch.compile(fullgraph=True)`` parity. * ``torch.compile(fullgraph=True)`` parity for the no-offset path. * Liger Qwen-VL auto-default for all 6 model_config_types; explicit ``False`` is respected. * Patch idempotency (double-apply is a no-op). * Transformers signature contract — pins the stock attention forward argument names so future drift trips loudly at test time. * Gradient-checkpointing composition (Qwen3 + ``gradient_checkpointing_enable``). * Flash-Attention 2 composition (skip-if-unavailable). * LoRA + fused composition on Qwen3 / Qwen3.5 / Qwen3.5-MoE, with fused-first reverse-order trip-wires that catch the original ordering bug if anyone re-introduces it. A pre-existing upstream-drift xfail in ``test_gemma4_fused_attn.py`` documents Gemma 4 + ``lora_qkv_kernel`` being broken in transformers 5.8.1 (new ``shared_kv_states: dict[str, ...]`` signature drift in QKV_PATCHES). Out of scope for this PR; flips to XPASS when patched. Post-review fixes ----------------- * ``_resolve_norm_module``: PEFT ``ModulesToSaveWrapper`` stores ``active_adapter`` as ``list[str]`` (e.g. ``["default"]``), not a string. The prior ``isinstance(adapter, str)`` check silently returned the frozen ``original_module`` for every real-PEFT case. Switched to iterating ``active_adapters`` (with ``active_adapter`` fallback) across all 4 patches. Added a direct unit-test plus an end-to-end test that drives real ``peft.get_peft_model(modules_to_save=["q_norm","k_norm"])`` and asserts the helper returns the trainable adapter weight. * ``cfg.fused_attn_kernel`` unsupported-model warning: moved out of the Pydantic ``model_validator(mode="before")`` (which ran *before* ``normalize_config()`` had derived ``model_config_type``, so it silently no-op'd on normal YAML input) into a new ``PatchManager._warn_if_fused_attn_unsupported`` staticmethod invoked from ``_apply_model_specific_patches``, where ``model_config_type`` is guaranteed set. Added a source-line guard that the helper stays wired. * address coderabbit comments * improve bwd pass throughput * feat(qwen3-vl): add fused attention patch * test: capture fused attention logs from concrete loggers * ci: rerun tests --------- Co-authored-by: Wing Lian --- docs/optimizations.qmd | 11 + examples/qwen3/8b-lora-fused-attn.yaml | 44 ++ src/axolotl/core/builders/base.py | 12 + src/axolotl/integrations/liger/args.py | 11 +- src/axolotl/integrations/liger/plugin.py | 15 +- src/axolotl/kernels/autotune_telemetry.py | 136 ++++++ src/axolotl/kernels/gemma4_fused_rope.py | 371 ++++++++------- src/axolotl/loaders/patch_manager.py | 79 +++- .../monkeypatch/models/qwen3/__init__.py | 0 .../monkeypatch/models/qwen3/fused_attn.py | 117 +++++ .../monkeypatch/models/qwen3_5/fused_attn.py | 133 ++++++ .../models/qwen3_5_moe/__init__.py | 0 .../models/qwen3_5_moe/fused_attn.py | 135 ++++++ .../monkeypatch/models/qwen3_moe/__init__.py | 0 .../models/qwen3_moe/fused_attn.py | 121 +++++ .../monkeypatch/models/qwen3_vl/__init__.py | 0 .../monkeypatch/models/qwen3_vl/fused_attn.py | 121 +++++ src/axolotl/utils/schemas/config.py | 19 + .../test_liger_qwen_vl_rope_default.py | 124 +++++ .../test_fused_rope_autotune_telemetry.py | 168 +++++++ .../kernels/test_gemma4_fused_rope_compile.py | 66 +++ .../test_gemma4_fused_rope_unit_offset.py | 238 ++++++++++ tests/monkeypatch/test_gemma4_fused_attn.py | 155 +++++-- tests/monkeypatch/test_qwen3_5_fused_attn.py | 424 ++++++++++++++++++ tests/monkeypatch/test_qwen3_fused_attn.py | 283 ++++++++++++ .../test_qwen3_fused_attn_defensive.py | 375 ++++++++++++++++ .../test_qwen3_fused_attn_robustness.py | 206 +++++++++ tests/monkeypatch/test_qwen3_vl_fused_attn.py | 168 +++++++ 28 files changed, 3293 insertions(+), 239 deletions(-) create mode 100644 examples/qwen3/8b-lora-fused-attn.yaml create mode 100644 src/axolotl/kernels/autotune_telemetry.py create mode 100644 src/axolotl/monkeypatch/models/qwen3/__init__.py create mode 100644 src/axolotl/monkeypatch/models/qwen3/fused_attn.py create mode 100644 src/axolotl/monkeypatch/models/qwen3_5/fused_attn.py create mode 100644 src/axolotl/monkeypatch/models/qwen3_5_moe/__init__.py create mode 100644 src/axolotl/monkeypatch/models/qwen3_5_moe/fused_attn.py create mode 100644 src/axolotl/monkeypatch/models/qwen3_moe/__init__.py create mode 100644 src/axolotl/monkeypatch/models/qwen3_moe/fused_attn.py create mode 100644 src/axolotl/monkeypatch/models/qwen3_vl/__init__.py create mode 100644 src/axolotl/monkeypatch/models/qwen3_vl/fused_attn.py create mode 100644 tests/integrations/test_liger_qwen_vl_rope_default.py create mode 100644 tests/kernels/test_fused_rope_autotune_telemetry.py create mode 100644 tests/kernels/test_gemma4_fused_rope_compile.py create mode 100644 tests/kernels/test_gemma4_fused_rope_unit_offset.py create mode 100644 tests/monkeypatch/test_qwen3_5_fused_attn.py create mode 100644 tests/monkeypatch/test_qwen3_fused_attn.py create mode 100644 tests/monkeypatch/test_qwen3_fused_attn_defensive.py create mode 100644 tests/monkeypatch/test_qwen3_fused_attn_robustness.py create mode 100644 tests/monkeypatch/test_qwen3_vl_fused_attn.py diff --git a/docs/optimizations.qmd b/docs/optimizations.qmd index 621c2c0bcd..60a011b415 100644 --- a/docs/optimizations.qmd +++ b/docs/optimizations.qmd @@ -73,6 +73,17 @@ Provides efficient Triton kernels to improve training speed and reduce memory us - **Learn more:** [Custom Integrations - Liger Kernels](custom_integrations.qmd#liger-kernels) +### Fused RMSNorm + RoPE (Qwen3 / Qwen3-MoE / Qwen3.5 / Qwen3.5-MoE / Qwen3.6 dense / Qwen3.6-MoE) + +Replaces the per-layer `q_norm + apply_rotary_pos_emb` (and matching K path) with a single Triton kernel launch on the full-attention layers. Opt-in. The kernel computes in fp32 and rounds once, so it matches an fp32 reference to within bf16 rounding — i.e. it is *more* accurate than the eager bf16 path, which rounds at several intermediate steps. Gemma 4 always uses the fused path (no flag needed). Qwen3.6 checkpoints are loaded by transformers under the `qwen3_5` / `qwen3_5_moe` model_types, so the same flag covers both generations. + +```yaml +fused_attn_kernel: true +``` + +- **Compile-safe:** the kernel is wrapped as a `torch.library.triton_op` and traces under `torch.compile(fullgraph=True)`. +- **Hardware note:** on sm_120 (Blackwell) combining with `torch_compile: true` is a net win; on sm_86 (Ampere consumer) `torch_compile: true` currently regresses the surrounding Inductor-generated kernels — keep compile off there. + ### Expert Kernels Optimized per-expert grouped-GEMM kernels for MoE training, with LoRA support. diff --git a/examples/qwen3/8b-lora-fused-attn.yaml b/examples/qwen3/8b-lora-fused-attn.yaml new file mode 100644 index 0000000000..70d8c36fa1 --- /dev/null +++ b/examples/qwen3/8b-lora-fused-attn.yaml @@ -0,0 +1,44 @@ +base_model: Qwen/Qwen3-8B + +load_in_8bit: false +load_in_4bit: false +strict: false + +datasets: + - path: tatsu-lab/alpaca + type: alpaca +dataset_prepared_path: +val_set_size: 0.05 +output_dir: ./outputs/qwen3-8b-lora-fused + +sequence_len: 4096 +sample_packing: true +eval_sample_packing: true + +adapter: lora +lora_r: 32 +lora_alpha: 64 +lora_dropout: 0.0 +lora_target_linear: true + +# Opt-in fused RMSNorm + RoPE Triton kernel for Qwen3 attention. +# fp32-internal, single round: matches an fp32 reference within bf16 rounding +# (more accurate than the eager bf16 path). Speeds up the q/k norm+rope path. +fused_attn_kernel: true + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_fused +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: false + +gradient_checkpointing: true +flash_attention: true + +warmup_ratio: 0.1 +saves_per_epoch: 1 +weight_decay: 0.0 diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 3740cb8706..e719e5eb3c 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -181,6 +181,18 @@ def get_callbacks(self) -> list[TrainerCallback]: if telemetry_manager.enabled: callbacks.append(TelemetryCallback()) + # Report the fused RMSNorm+RoPE autotune selection + GPU identity so + # per-hardware tuning can be aggregated (mirrors scattermoe-lora). + if self.cfg.fused_attn_kernel or self.cfg.model_config_type in ( + "gemma4", + "gemma4_text", + ): + from axolotl.kernels.autotune_telemetry import ( + FusedRopeAutotuneReportCallback, + ) + + callbacks.append(FusedRopeAutotuneReportCallback()) + return callbacks def get_post_trainer_create_callbacks(self, trainer): diff --git a/src/axolotl/integrations/liger/args.py b/src/axolotl/integrations/liger/args.py index a5f88ffe2c..283e527b4e 100644 --- a/src/axolotl/integrations/liger/args.py +++ b/src/axolotl/integrations/liger/args.py @@ -28,7 +28,16 @@ class LigerArgs(BaseModel): Input args for LIGER. """ - liger_rope: bool | None = None + liger_rope: bool | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Enables Liger's fused RoPE kernel. For Qwen2-VL / Qwen2.5-VL / " + "Qwen3-VL (text and VL model_config_types) this auto-defaults to " + "True when unset, swapping in the fused multimodal/rotary kernel." + ) + }, + ) liger_rms_norm: bool | None = None liger_rms_norm_gated: bool | None = Field( default=None, diff --git a/src/axolotl/integrations/liger/plugin.py b/src/axolotl/integrations/liger/plugin.py index 9c4a26351f..eedf221ede 100644 --- a/src/axolotl/integrations/liger/plugin.py +++ b/src/axolotl/integrations/liger/plugin.py @@ -86,7 +86,20 @@ def patched_init(self, *args, **kwargs): liger_fn_sig = inspect.signature(apply_liger_fn) kwargs = {} if "rope" in liger_fn_sig.parameters: - kwargs["rope"] = cfg.liger_rope + rope_value = cfg.liger_rope + # cfg.liger_rope defaults to None, which would override upstream's rope=True for Qwen-VL. + if rope_value is None and cfg.model_config_type in ( + "qwen2_vl", + "qwen2_5_vl", + "qwen3_vl", + "qwen3_vl_moe", + "qwen2_vl_text", + "qwen2_5_vl_text", + "qwen3_vl_text", + "qwen3_vl_moe_text", + ): + rope_value = True + kwargs["rope"] = rope_value if "cross_entropy" in liger_fn_sig.parameters: kwargs["cross_entropy"] = cfg.liger_cross_entropy if "fused_linear_cross_entropy" in liger_fn_sig.parameters: diff --git a/src/axolotl/kernels/autotune_telemetry.py b/src/axolotl/kernels/autotune_telemetry.py new file mode 100644 index 0000000000..a729fd7ccb --- /dev/null +++ b/src/axolotl/kernels/autotune_telemetry.py @@ -0,0 +1,136 @@ +"""Telemetry for the fused RMSNorm+RoPE Triton autotune selections. + +Mirrors the scattermoe-lora autotune telemetry +(:mod:`axolotl.integrations.kernels.autotune_callback`): after the kernel's +``@triton.autotune`` cache is populated by the first backward pass, report the +selected configs alongside GPU identity so the per-hardware tuning that varies +across architectures can be aggregated. +""" + +import logging + +import torch +from transformers import ( + TrainerCallback, + TrainerControl, + TrainerState, + TrainingArguments, +) + +LOG = logging.getLogger(__name__) + +# Give up looking for autotune data after this many training steps. +_MAX_POLL_STEP = 5 + +# (human-readable name, attribute on gemma4_fused_rope, autotune key arg names) +_KERNEL_REGISTRY: list[tuple[str, str, list[str]]] = [ + ("fused_rms_norm_rope_bwd", "_rms_norm_rope_backward_kernel", ["n_cols"]), +] + + +def _get_gpu_info() -> dict: + """Return basic GPU identification for the current device.""" + if not torch.cuda.is_available(): + return {} + try: + props = torch.cuda.get_device_properties(torch.cuda.current_device()) + return { + "gpu_name": props.name, + "gpu_compute_capability": f"{props.major}.{props.minor}", + "gpu_memory_bytes": props.total_memory, + } + except Exception: # pylint: disable=broad-exception-caught + return {} + + +def collect_fused_rope_autotune_configs() -> list[dict]: + """Read the autotune ``.cache`` from the fused RMSNorm+RoPE backward kernel. + + Each entry is ``{"kernel", "key", "config"}`` — the same shape the + scattermoe collector emits, so both event types aggregate uniformly. + Returns ``[]`` if Triton/the kernel isn't loaded or nothing autotuned yet. + """ + import sys + + # The kernel module is only in sys.modules once the fused path has run — + # which is exactly when its autotune cache is populated. Read it from there + # instead of importing (avoids pulling in Triton when the path is unused). + mod = sys.modules.get("axolotl.kernels.gemma4_fused_rope") + if mod is None: + return [] + + results: list[dict] = [] + for friendly_name, attr_name, key_names in _KERNEL_REGISTRY: + kernel_fn = getattr(mod, attr_name, None) + cache = getattr(kernel_fn, "cache", None) + if not cache: + continue + for key_tuple, config in cache.items(): + config_dict = dict(config.kwargs) + config_dict["num_warps"] = config.num_warps + config_dict["num_stages"] = config.num_stages + if getattr(config, "num_ctas", None) is not None: + config_dict["num_ctas"] = config.num_ctas + + key: dict = {} + for i, name in enumerate(key_names): + if i < len(key_tuple): + key[name] = key_tuple[i] + if len(key_tuple) > len(key_names): + key["_extra"] = [str(v) for v in key_tuple[len(key_names) :]] + + results.append({"kernel": friendly_name, "key": key, "config": config_dict}) + return results + + +class FusedRopeAutotuneReportCallback(TrainerCallback): + """Reports fused RMSNorm+RoPE autotune selections via telemetry. + + Fires once after the autotune cache is populated (the first step whose + backward has run), retrying up to ``_MAX_POLL_STEP`` then giving up. Every + later ``on_step_end`` short-circuits on ``_reported`` — zero hot-path cost. + """ + + def __init__(self): + self._reported = False + + # pylint: disable=unused-argument + def on_step_end( + self, + args: TrainingArguments, + state: TrainerState, + control: TrainerControl, + **kwargs, + ): + if self._reported: + return + + configs = collect_fused_rope_autotune_configs() + if not configs: + if state.global_step >= _MAX_POLL_STEP: + LOG.debug( + "No fused-rope autotune data after %d steps; giving up.", + state.global_step, + ) + self._reported = True + return + + self._reported = True + + from axolotl.telemetry.manager import TelemetryManager + + telemetry_manager = TelemetryManager.get_instance() + if not telemetry_manager.enabled: + return + + properties = {"kernel_count": len(configs), "kernels": configs} + properties.update(_get_gpu_info()) + + telemetry_manager.send_event( + event_type="fused-rope-autotune", + properties=properties, + ) + LOG.info( + "Reported %d fused-rope kernel autotune config(s) to telemetry.", + len(configs), + ) diff --git a/src/axolotl/kernels/gemma4_fused_rope.py b/src/axolotl/kernels/gemma4_fused_rope.py index f98e9a3de6..8193355846 100644 --- a/src/axolotl/kernels/gemma4_fused_rope.py +++ b/src/axolotl/kernels/gemma4_fused_rope.py @@ -1,20 +1,4 @@ -""" -Fused RMSNorm + RoPE Triton kernel for Gemma 4. - -Fuses three operations into one kernel launch: - 1. RMSNorm: x_norm = (x / sqrt(mean(x^2) + eps)) * weight - 2. RoPE: y = x_norm * cos + rotate_half(x_norm) * sin - 3. (optional) RMSNorm without scale (for v_norm) - -This eliminates two intermediate tensor materializations per Q/K path; -churn from rotate_half / apply_rotary_pos_emb. - -Shapes: - X: (rows, head_dim) — flattened from (batch, seq_len, num_heads, head_dim) - W: (head_dim,) — RMSNorm weight (None for with_scale=False) - cos: (rows, head_dim) — flattened from (batch, seq_len, 1, head_dim) after broadcast - sin: (rows, head_dim) — same as cos -""" +"""Fused RMSNorm + (partial) RoPE Triton kernel for Gemma 4 / Qwen3 Q/K paths.""" import math import operator @@ -25,10 +9,10 @@ from liger_kernel.ops.utils import ( calculate_settings, compare_version, - ensure_contiguous, torch_to_triton_dtype, ) from liger_kernel.utils import is_npu_available +from torch.library import triton_op, wrap_triton if compare_version("triton", operator.ge, "3.0.0") and not is_npu_available(): try: @@ -38,6 +22,11 @@ else: from triton.language.math import rsqrt +# Backward over-subscription factor: number of program blocks per SM. The +# weight-gradient reduction needs one private partial per block, so this also +# sizes the dW scratch buffer. ~8 saturates occupancy on tested GPUs. +_BWD_BLOCKS_PER_SM = 8 + @triton.jit def _rms_norm_rope_forward_kernel( @@ -57,6 +46,7 @@ def _rms_norm_rope_forward_kernel( n_heads, eps, HAS_WEIGHT: tl.constexpr, + UNIT_OFFSET: tl.constexpr, BLOCK_SIZE: tl.constexpr, ): """ @@ -100,7 +90,10 @@ def _rms_norm_rope_forward_kernel( # Apply weight if present (with_scale=True) if HAS_WEIGHT: W_row = tl.load(W_ptr + col_offsets, mask=mask, other=0).to(tl.float32) - X_norm = X_norm * W_row + if UNIT_OFFSET: + X_norm = X_norm * (W_row + 1.0) + else: + X_norm = X_norm * W_row # RoPE: load cos/sin (broadcast across heads). For col >= n_rot we get # cos=1, sin=0 so the formula leaves X_norm untouched. @@ -130,7 +123,10 @@ def _rms_norm_rope_forward_kernel( X_rot_norm = X_rot * rstd if HAS_WEIGHT: W_rot = tl.load(W_ptr + rot_offsets, mask=rot_load_mask, other=0).to(tl.float32) - X_rot_norm = X_rot_norm * W_rot + if UNIT_OFFSET: + X_rot_norm = X_rot_norm * (W_rot + 1.0) + else: + X_rot_norm = X_rot_norm * W_rot # Negate the first half (rotate_half negates x2, which becomes the first half) sign = tl.where(col_offsets < half_rot, -1.0, 1.0) @@ -146,6 +142,16 @@ def _rms_norm_rope_forward_kernel( ) +_BWD_AUTOTUNE_CONFIGS = [ + triton.Config({}, num_warps=w, num_stages=s) + for w in (2, 4, 8, 16) + for s in (1, 2, 3) +] + + +# num_warps/num_stages optima for the latency-bound row loop vary by GPU; key on +# n_cols (head_dim) so head_dim=128 and 256 each get their own tuned config. +@triton.autotune(configs=_BWD_AUTOTUNE_CONFIGS, key=["n_cols"]) @triton.jit def _rms_norm_rope_backward_kernel( dY_ptr, @@ -170,6 +176,7 @@ def _rms_norm_rope_backward_kernel( n_heads, rows_per_program, HAS_WEIGHT: tl.constexpr, + UNIT_OFFSET: tl.constexpr, BLOCK_SIZE: tl.constexpr, ): """ @@ -245,7 +252,10 @@ def _rms_norm_rope_backward_kernel( if HAS_WEIGHT: dW_acc += dN * n - dm = dN * W_row + if UNIT_OFFSET: + dm = dN * (W_row + 1.0) + else: + dm = dN * W_row else: dm = dN @@ -267,33 +277,28 @@ def _rms_norm_rope_backward_kernel( ) -def rms_norm_rope_forward(X, W, cos, sin, eps, n_heads, n_rot): - """ - Args: - X: (B*S*H, head_dim) — contiguous, flattened from (B, S, H, D) - W: (head_dim,) or None — RMSNorm weight - cos: (B*S, n_rot) — position embeddings (broadcast across heads) - sin: (B*S, n_rot) — position embeddings (broadcast across heads) - eps: float - n_heads: int — number of attention heads (for cos/sin indexing) - n_rot: int — rotary dim (== head_dim for full rotary, < head_dim for - partial rotary). Must be even and ``<= head_dim``. - Returns: - Y, X_saved, RSTD, BLOCK_SIZE, num_warps - """ +@triton_op("axolotl::fused_rms_norm_rope_fwd", mutates_args=()) +def _fused_rms_norm_rope_fwd( + X: torch.Tensor, + W: torch.Tensor, + cos: torch.Tensor, + sin: torch.Tensor, + eps: float, + n_heads: int, + n_rot: int, + unit_offset: bool, +) -> tuple[torch.Tensor, torch.Tensor]: + """Returns ``(Y, RSTD)``; ``wrap_triton`` keeps it ``torch.compile``-safe.""" n_rows, n_cols = X.shape BLOCK_SIZE, num_warps = calculate_settings(n_cols) - has_weight = W is not None - Y = torch.empty_like(X) RSTD = torch.empty(n_rows, dtype=torch.float32, device=X.device) - - _rms_norm_rope_forward_kernel[(n_rows,)]( + wrap_triton(_rms_norm_rope_forward_kernel)[(n_rows,)]( Y, Y.stride(0), X, X.stride(0), - W if has_weight else X, # dummy pointer when no weight + W, cos, cos.stride(0), sin, @@ -304,30 +309,40 @@ def rms_norm_rope_forward(X, W, cos, sin, eps, n_heads, n_rot): n_rot, n_heads, eps, - HAS_WEIGHT=has_weight, + HAS_WEIGHT=True, + UNIT_OFFSET=unit_offset, BLOCK_SIZE=BLOCK_SIZE, num_warps=num_warps, ) - return Y, X, RSTD, BLOCK_SIZE, num_warps - - -def rms_norm_rope_backward( - dY, X, W, cos, sin, RSTD, n_heads, n_rot, BLOCK_SIZE, num_warps -): + return Y, RSTD + + +@triton_op("axolotl::fused_rms_norm_rope_bwd", mutates_args=()) +def _fused_rms_norm_rope_bwd( + dY: torch.Tensor, + X: torch.Tensor, + W: torch.Tensor, + cos: torch.Tensor, + sin: torch.Tensor, + RSTD: torch.Tensor, + n_heads: int, + n_rot: int, + unit_offset: bool, +) -> tuple[torch.Tensor, torch.Tensor]: + """Returns ``(dX, dW)``.""" n_rows, n_cols = dY.shape - has_weight = W is not None - + BLOCK_SIZE, _ = calculate_settings(n_cols) + # One block per SM serializes a long row-loop at 1 block/SM occupancy; the + # forward runs a block per row. Over-subscribe the SMs so the latency-bound + # row loop has enough resident blocks to hide global-load latency. Each + # block still writes a private dW partial that's summed below (no atomics). sm_count = torch.cuda.get_device_properties(X.device).multi_processor_count - rows_per_program = math.ceil(n_rows / sm_count) - + target_programs = min(_BWD_BLOCKS_PER_SM * sm_count, n_rows) + rows_per_program = max(1, math.ceil(n_rows / target_programs)) + n_programs = math.ceil(n_rows / rows_per_program) dX = torch.empty_like(X) - - if has_weight: - _dW = torch.empty((sm_count, n_cols), dtype=torch.float32, device=X.device) - else: - _dW = torch.empty((1, n_cols), dtype=torch.float32, device=X.device) - - _rms_norm_rope_backward_kernel[(sm_count,)]( + _dW = torch.empty((n_programs, n_cols), dtype=torch.float32, device=X.device) + wrap_triton(_rms_norm_rope_backward_kernel)[(n_programs,)]( dY, dY.stride(0), dX, @@ -335,7 +350,7 @@ def rms_norm_rope_backward( X, X.stride(0), torch_to_triton_dtype[X.dtype], - W if has_weight else X, # dummy + W, cos, cos.stride(0), sin, @@ -349,81 +364,50 @@ def rms_norm_rope_backward( n_rot, n_heads, rows_per_program, - HAS_WEIGHT=has_weight, + HAS_WEIGHT=True, + UNIT_OFFSET=unit_offset, BLOCK_SIZE=BLOCK_SIZE, - num_warps=num_warps, ) - - dW = _dW.sum(dim=0).to(W.dtype) if has_weight else None + dW = _dW.sum(dim=0).to(W.dtype) return dX, dW -class FusedRMSNormRoPEFunction(torch.autograd.Function): - @staticmethod - @ensure_contiguous - def forward(ctx, X, W, cos, sin, eps, n_heads, n_rot): - """ - X: (B*S*H, head_dim) - W: (head_dim,) or None - cos: (B*S, n_rot) — broadcast across heads - sin: (B*S, n_rot) — broadcast across heads - n_heads: int - n_rot: int — rotary dim (<= head_dim) - """ - Y, X_saved, RSTD, BLOCK_SIZE, num_warps = rms_norm_rope_forward( - X, - W, - cos, - sin, - eps, - n_heads, - n_rot, - ) - ctx.eps = eps - ctx.BLOCK_SIZE = BLOCK_SIZE - ctx.num_warps = num_warps - ctx.n_heads = n_heads - ctx.n_rot = n_rot - ctx.has_weight = W is not None - ctx.save_for_backward(X_saved, W, cos, sin, RSTD) - return Y - - @staticmethod - @ensure_contiguous - def backward(ctx, dY): - X, W, cos, sin, RSTD = ctx.saved_tensors - dX, dW = rms_norm_rope_backward( - dY, - X, - W, - cos, - sin, - RSTD, - ctx.n_heads, - ctx.n_rot, - ctx.BLOCK_SIZE, - ctx.num_warps, - ) - return dX, dW, None, None, None, None, None +def _fused_rms_norm_rope_setup_context(ctx, inputs, output): + X, W, cos, sin, _eps, n_heads, n_rot, unit_offset = inputs + _, RSTD = output + ctx.save_for_backward(X, W, cos, sin, RSTD) + ctx.n_heads = n_heads + ctx.n_rot = n_rot + ctx.unit_offset = unit_offset + + +def _fused_rms_norm_rope_backward(ctx, grad_Y, grad_RSTD): + X, W, cos, sin, RSTD = ctx.saved_tensors + grad_Y = grad_Y.contiguous() + dX, dW = _fused_rms_norm_rope_bwd( + grad_Y, X, W, cos, sin, RSTD, ctx.n_heads, ctx.n_rot, ctx.unit_offset + ) + return dX, dW, None, None, None, None, None, None + + +_fused_rms_norm_rope_fwd.register_autograd( + _fused_rms_norm_rope_backward, + setup_context=_fused_rms_norm_rope_setup_context, +) -def fused_rms_norm_rope(x, weight, cos, sin, eps=1e-6): +def fused_rms_norm_rope(x, weight, cos, sin, eps=1e-6, unit_offset=False): """ Apply fused RMSNorm + (partial) RoPE. - Args: - x: (batch, seq_len, num_heads, head_dim) — after projection + view - weight: (head_dim,) — RMSNorm weight, or None for no-scale norm - cos: (batch, seq_len, n_rot) — from RotaryEmbedding. ``n_rot`` - must be even and ``<= head_dim``. When ``n_rot < head_dim`` - the trailing ``head_dim - n_rot`` columns are RMSNorm-only - (partial-rotary pass-through), matching stock Gemma 4 with - ``partial_rotary_factor < 1.0``. - sin: (batch, seq_len, n_rot) — same shape as ``cos`` - eps: float — RMSNorm epsilon - - Returns: - y: (batch, seq_len, num_heads, head_dim) — normalized + rotated + Shapes: + x: (B, S, H, D) — post-projection + weight: (D,) — required; use ``fused_rms_norm_noscale`` for the no-weight variant + cos: (B, S, n_rot) — ``n_rot`` must be even and ``<= D``; trailing + ``D - n_rot`` columns are RMSNorm-only (partial rotary). + sin: (B, S, n_rot) + + ``unit_offset=True`` scales by ``(weight + 1.0)`` (Gemma-style). """ shape = x.shape # (B, S, H, D) B, S, H, D = shape @@ -438,13 +422,8 @@ def fused_rms_norm_rope(x, weight, cos, sin, eps=1e-6): if n_rot % 2 != 0: raise ValueError(f"rotary dim must be even, got {n_rot}") - # Flatten to 2D: (B*S*H, D) x_flat = x.reshape(-1, D).contiguous() - # cos/sin may broadcast over the batch dim (e.g. (1, S, n_rot) when - # all sequences share the same rotary positions). The kernel needs a - # dense (B*S, n_rot) buffer so that row_idx // n_heads maps cleanly - # onto a single (b, s) pair, so expand-then-contiguous to materialize - # the per-batch broadcast. Expand is a no-op when B == cos.shape[0]. + # Kernel needs a dense (B*S, n_rot) buffer; materialize the batch-broadcast. if cos.shape[0] != B: if cos.shape[0] != 1: raise ValueError( @@ -456,8 +435,8 @@ def fused_rms_norm_rope(x, weight, cos, sin, eps=1e-6): cos_flat = cos.reshape(B * S, n_rot).contiguous() sin_flat = sin.reshape(B * S, n_rot).contiguous() - y_flat = FusedRMSNormRoPEFunction.apply( - x_flat, weight, cos_flat, sin_flat, eps, H, n_rot + y_flat, _ = _fused_rms_norm_rope_fwd( + x_flat, weight, cos_flat, sin_flat, eps, H, n_rot, unit_offset ) return y_flat.view(shape) @@ -526,68 +505,76 @@ def _rms_norm_noscale_backward_kernel( ) -class FusedRMSNormNoScaleFunction(torch.autograd.Function): - """RMSNorm without learnable scale — used for Gemma4's v_norm.""" - - @staticmethod - @ensure_contiguous - def forward(ctx, X, eps): - n_rows, n_cols = X.shape - BLOCK_SIZE, num_warps = calculate_settings(n_cols) - Y = torch.empty_like(X) - RSTD = torch.empty(n_rows, dtype=torch.float32, device=X.device) - - _rms_norm_forward_kernel[(n_rows,)]( - Y, - Y.stride(0), - X, - X.stride(0), - RSTD, - RSTD.stride(0), - n_cols, - eps, - BLOCK_SIZE=BLOCK_SIZE, - num_warps=num_warps, - ) - ctx.BLOCK_SIZE = BLOCK_SIZE - ctx.num_warps = num_warps - ctx.save_for_backward(X, RSTD) - ctx.n_cols = n_cols - return Y - - @staticmethod - @ensure_contiguous - def backward(ctx, dY): - X, RSTD = ctx.saved_tensors - n_rows = X.shape[0] - dX = torch.empty_like(X) - _rms_norm_noscale_backward_kernel[(n_rows,)]( - dY, - dY.stride(0), - dX, - dX.stride(0), - X, - X.stride(0), - torch_to_triton_dtype[X.dtype], - RSTD, - RSTD.stride(0), - ctx.n_cols, - BLOCK_SIZE=ctx.BLOCK_SIZE, - num_warps=ctx.num_warps, - ) - return dX, None +@triton_op("axolotl::fused_rms_norm_noscale_fwd", mutates_args=()) +def _fused_rms_norm_noscale_fwd( + X: torch.Tensor, eps: float +) -> tuple[torch.Tensor, torch.Tensor]: + """Returns ``(Y, RSTD)``.""" + n_rows, n_cols = X.shape + BLOCK_SIZE, num_warps = calculate_settings(n_cols) + Y = torch.empty_like(X) + RSTD = torch.empty(n_rows, dtype=torch.float32, device=X.device) + wrap_triton(_rms_norm_forward_kernel)[(n_rows,)]( + Y, + Y.stride(0), + X, + X.stride(0), + RSTD, + RSTD.stride(0), + n_cols, + eps, + BLOCK_SIZE=BLOCK_SIZE, + num_warps=num_warps, + ) + return Y, RSTD -def fused_rms_norm_noscale(x, eps=1e-6): - """ - RMSNorm without scale for v_norm. +@triton_op("axolotl::fused_rms_norm_noscale_bwd", mutates_args=()) +def _fused_rms_norm_noscale_bwd( + dY: torch.Tensor, X: torch.Tensor, RSTD: torch.Tensor +) -> torch.Tensor: + n_rows, n_cols = X.shape + BLOCK_SIZE, num_warps = calculate_settings(n_cols) + dX = torch.empty_like(X) + wrap_triton(_rms_norm_noscale_backward_kernel)[(n_rows,)]( + dY, + dY.stride(0), + dX, + dX.stride(0), + X, + X.stride(0), + torch_to_triton_dtype[X.dtype], + RSTD, + RSTD.stride(0), + n_cols, + BLOCK_SIZE=BLOCK_SIZE, + num_warps=num_warps, + ) + return dX - Args: - x: (batch, seq_len, num_heads, head_dim) - Returns: - y: same shape, normalized - """ + +def _fused_rms_norm_noscale_setup_context(ctx, inputs, output): + X, _eps = inputs + _, RSTD = output + ctx.save_for_backward(X, RSTD) + + +def _fused_rms_norm_noscale_backward(ctx, grad_Y, grad_RSTD): + X, RSTD = ctx.saved_tensors + grad_Y = grad_Y.contiguous() + dX = _fused_rms_norm_noscale_bwd(grad_Y, X, RSTD) + return dX, None + + +_fused_rms_norm_noscale_fwd.register_autograd( + _fused_rms_norm_noscale_backward, + setup_context=_fused_rms_norm_noscale_setup_context, +) + + +def fused_rms_norm_noscale(x, eps=1e-6): + """RMSNorm without a learned scale (used for v_norm).""" shape = x.shape - x_flat = x.reshape(-1, shape[-1]) - y_flat = FusedRMSNormNoScaleFunction.apply(x_flat, eps) + x_flat = x.reshape(-1, shape[-1]).contiguous() + y_flat, _ = _fused_rms_norm_noscale_fwd(x_flat, eps) return y_flat.view(shape) diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 943d52af0b..495ff58851 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -104,6 +104,9 @@ def apply_pre_model_load_patches(self): self._apply_flash_attn_4_patches() self._apply_fsdp_patches() self._apply_adapter_patches() + # Must precede fused-RoPE patches: re-parses ``Attention.forward`` + # via ``inspect.getsource``; the QKV regex misses on a patched body. + self._apply_self_attention_lora_patch() self._apply_model_specific_patches() self._apply_fp8_patches() self._apply_flash_attention_peft_patches() @@ -113,7 +116,6 @@ def apply_pre_model_load_patches(self): self._patch_loss_llama() self._patch_llama_derived_model() self._apply_mistral_cross_entropy_patch() - self._apply_self_attention_lora_patch() self._apply_fsdp2_bnb_patches() self._apply_patch_deepspeed_zero3() self._apply_voxtral_patches() @@ -351,8 +353,39 @@ def _apply_flash_attn_4_patches(self): patch_flash_attn_4(self.model_config) + _FUSED_ATTN_KERNEL_SUPPORTED = ( + "qwen3", + "qwen3_moe", + "qwen3_vl", + "qwen3_vl_text", + "qwen3_5", + "qwen3_5_text", + "qwen3_5_moe", + "qwen3_5_moe_text", + "gemma4", + "gemma4_text", + ) + + @staticmethod + def _warn_if_fused_attn_unsupported(cfg): + """Warn when ``fused_attn_kernel`` targets an unsupported + ``model_config_type`` (derived post-schema by ``normalize_config()``).""" + if not getattr(cfg, "fused_attn_kernel", False): + return + mct = getattr(cfg, "model_config_type", None) + if mct and mct not in PatchManager._FUSED_ATTN_KERNEL_SUPPORTED: + LOG.warning( + "`fused_attn_kernel: true` is set but model_config_type=%r is not " + "in the supported set %s. The flag is a silent no-op for this " + "model. Remove the flag or use one of the supported model families.", + mct, + sorted(PatchManager._FUSED_ATTN_KERNEL_SUPPORTED), + ) + def _apply_model_specific_patches(self): """Apply patches specific to model architectures.""" + self._warn_if_fused_attn_unsupported(self.cfg) + if self.cfg.model_config_type == "gemma4" and self.cfg.use_kernels: # transformers' Gemma4VisionAttention registers a bare function via # @use_kernelized_func, which crashes model.kernelize() (triggered by @@ -473,6 +506,50 @@ def _apply_model_specific_patches(self): install_shared_kv_workaround=needs_shared_kv_workaround ) + if self.cfg.fused_attn_kernel and self.cfg.model_config_type == "qwen3": + from axolotl.monkeypatch.models.qwen3.fused_attn import ( + patch_qwen3_fused_attn, + ) + + patch_qwen3_fused_attn() + + if self.cfg.fused_attn_kernel and self.cfg.model_config_type == "qwen3_moe": + from axolotl.monkeypatch.models.qwen3_moe.fused_attn import ( + patch_qwen3_moe_fused_attn, + ) + + patch_qwen3_moe_fused_attn() + + if self.cfg.fused_attn_kernel and self.cfg.model_config_type in ( + "qwen3_vl", + "qwen3_vl_text", + ): + from axolotl.monkeypatch.models.qwen3_vl.fused_attn import ( + patch_qwen3_vl_fused_attn, + ) + + patch_qwen3_vl_fused_attn() + + if self.cfg.fused_attn_kernel and self.cfg.model_config_type in ( + "qwen3_5", + "qwen3_5_text", + ): + from axolotl.monkeypatch.models.qwen3_5.fused_attn import ( + patch_qwen3_5_fused_attn, + ) + + patch_qwen3_5_fused_attn() + + if self.cfg.fused_attn_kernel and self.cfg.model_config_type in ( + "qwen3_5_moe", + "qwen3_5_moe_text", + ): + from axolotl.monkeypatch.models.qwen3_5_moe.fused_attn import ( + patch_qwen3_5_moe_fused_attn, + ) + + patch_qwen3_5_moe_fused_attn() + @staticmethod def _fix_nemotron_h_conversion_mapping(): """Remove the spurious embedding→embeddings WeightRenaming from the diff --git a/src/axolotl/monkeypatch/models/qwen3/__init__.py b/src/axolotl/monkeypatch/models/qwen3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/qwen3/fused_attn.py b/src/axolotl/monkeypatch/models/qwen3/fused_attn.py new file mode 100644 index 0000000000..670522797f --- /dev/null +++ b/src/axolotl/monkeypatch/models/qwen3/fused_attn.py @@ -0,0 +1,117 @@ +"""Fuse ``q_norm/k_norm`` + RoPE in ``Qwen3Attention.forward`` via one Triton kernel.""" + +from typing import Callable + +import torch + +from axolotl.utils.logging import get_logger + +logger = get_logger(__name__) + + +def _resolve_norm_module(norm): + """Unwrap PEFT ``ModulesToSaveWrapper`` so the kernel reads the active adapter's weight.""" + modules_to_save = getattr(norm, "modules_to_save", None) + if not modules_to_save: + return norm + adapters = getattr(norm, "active_adapters", None) + if adapters is None: + adapter = getattr(norm, "active_adapter", None) + adapters = [adapter] if adapter is not None else [] + elif isinstance(adapters, str): + adapters = [adapters] + for name in adapters: + if isinstance(name, str) and name in modules_to_save: + return modules_to_save[name] + return getattr(norm, "original_module", norm) + + +def _make_fused_forward(): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + def fused_forward( + self, + hidden_states: torch.Tensor, + position_embeddings: tuple[torch.Tensor, torch.Tensor], + attention_mask: torch.Tensor | None, + past_key_values=None, + **kwargs, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + from transformers.models.qwen3.modeling_qwen3 import eager_attention_forward + + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + q_norm = _resolve_norm_module(self.q_norm) + k_norm = _resolve_norm_module(self.k_norm) + eps = q_norm.variance_epsilon + + cos, sin = position_embeddings + + has_lora_qkv = hasattr(self, "apply_qkv") + if has_lora_qkv: + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states = query_states.view(hidden_shape) + key_states = key_states.view(hidden_shape) + value_states = value_states.view(hidden_shape) + else: + query_states = self.q_proj(hidden_states).view(hidden_shape) + key_states = self.k_proj(hidden_states).view(hidden_shape) + value_states = self.v_proj(hidden_states).view(hidden_shape) + + # accelerate's per-module pre-hooks that move CPU-staged params don't fire on a monkeypatched forward. + q_w = q_norm.weight + if q_w.device != query_states.device: + q_w = q_w.to(query_states.device, non_blocking=True) + k_w = k_norm.weight + if k_w.device != key_states.device: + k_w = k_w.to(key_states.device, non_blocking=True) + + query_states = fused_rms_norm_rope( + query_states, q_w, cos, sin, eps=eps + ).transpose(1, 2) + key_states = fused_rms_norm_rope(key_states, k_w, cos, sin, eps=eps).transpose( + 1, 2 + ) + value_states = value_states.transpose(1, 2) + + if past_key_values is not None: + key_states, value_states = past_key_values.update( + key_states, value_states, self.layer_idx + ) + + attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface( + self.config._attn_implementation, eager_attention_forward + ) + + attn_output, attn_weights = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + sliding_window=self.sliding_window, + **kwargs, + ) + + attn_output = attn_output.reshape(*input_shape, -1).contiguous() + if hasattr(self, "apply_o"): + attn_output = self.apply_o(attn_output) + else: + attn_output = self.o_proj(attn_output) + return attn_output, attn_weights + + return fused_forward + + +def patch_qwen3_fused_attn() -> None: + from transformers.models.qwen3.modeling_qwen3 import Qwen3Attention + + if getattr(Qwen3Attention, "_axolotl_fused_attn_patched", False): + return + + Qwen3Attention.forward = _make_fused_forward() + Qwen3Attention._axolotl_fused_attn_patched = True + logger.info("Patched Qwen3Attention.forward with fused RMSNorm+RoPE Triton kernel") diff --git a/src/axolotl/monkeypatch/models/qwen3_5/fused_attn.py b/src/axolotl/monkeypatch/models/qwen3_5/fused_attn.py new file mode 100644 index 0000000000..ce83c22d2f --- /dev/null +++ b/src/axolotl/monkeypatch/models/qwen3_5/fused_attn.py @@ -0,0 +1,133 @@ +"""Fused q_norm/k_norm + RoPE for Qwen3.5 (gated q_proj, ``unit_offset=True`` RMSNorm).""" + +from typing import Callable + +import torch + +from axolotl.utils.logging import get_logger + +logger = get_logger(__name__) + + +def _resolve_norm_module(norm): + """Unwrap PEFT ``ModulesToSaveWrapper`` so the kernel reads the active adapter's weight.""" + modules_to_save = getattr(norm, "modules_to_save", None) + if not modules_to_save: + return norm + adapters = getattr(norm, "active_adapters", None) + if adapters is None: + adapter = getattr(norm, "active_adapter", None) + adapters = [adapter] if adapter is not None else [] + elif isinstance(adapters, str): + adapters = [adapters] + for name in adapters: + if isinstance(name, str) and name in modules_to_save: + return modules_to_save[name] + return getattr(norm, "original_module", norm) + + +def _make_fused_forward(): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + def fused_forward( + self, + hidden_states: torch.Tensor, + position_embeddings: tuple[torch.Tensor, torch.Tensor], + attention_mask: torch.Tensor | None, + past_key_values=None, + **kwargs, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + from transformers.models.qwen3_5.modeling_qwen3_5 import ( + eager_attention_forward, + ) + + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + q_norm = _resolve_norm_module(self.q_norm) + k_norm = _resolve_norm_module(self.k_norm) + # Liger's RMSNorm replacement uses ``variance_epsilon`` instead of ``eps``. + eps = getattr(q_norm, "eps", None) + if eps is None: + eps = q_norm.variance_epsilon + + cos, sin = position_embeddings + + has_lora_qkv = hasattr(self, "apply_qkv") + if has_lora_qkv: + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states, gate = torch.chunk( + query_states.view(*input_shape, -1, self.head_dim * 2), 2, dim=-1 + ) + query_states = query_states.reshape(hidden_shape) + key_states = key_states.view(hidden_shape) + value_states = value_states.view(hidden_shape) + else: + query_states, gate = torch.chunk( + self.q_proj(hidden_states).view(*input_shape, -1, self.head_dim * 2), + 2, + dim=-1, + ) + query_states = query_states.reshape(hidden_shape) + key_states = self.k_proj(hidden_states).view(hidden_shape) + value_states = self.v_proj(hidden_states).view(hidden_shape) + gate = gate.reshape(*input_shape, -1) + + # accelerate's per-module pre-hooks that move CPU-staged params don't fire on a monkeypatched forward. + q_w = q_norm.weight + if q_w.device != query_states.device: + q_w = q_w.to(query_states.device, non_blocking=True) + k_w = k_norm.weight + if k_w.device != key_states.device: + k_w = k_w.to(key_states.device, non_blocking=True) + + query_states = fused_rms_norm_rope( + query_states, q_w, cos, sin, eps=eps, unit_offset=True + ).transpose(1, 2) + key_states = fused_rms_norm_rope( + key_states, k_w, cos, sin, eps=eps, unit_offset=True + ).transpose(1, 2) + value_states = value_states.transpose(1, 2) + + if past_key_values is not None: + key_states, value_states = past_key_values.update( + key_states, value_states, self.layer_idx + ) + + attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface( + self.config._attn_implementation, eager_attention_forward + ) + + attn_output, attn_weights = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + **kwargs, + ) + + attn_output = attn_output.reshape(*input_shape, -1).contiguous() + attn_output = attn_output * torch.sigmoid(gate) + if hasattr(self, "apply_o"): + attn_output = self.apply_o(attn_output) + else: + attn_output = self.o_proj(attn_output) + return attn_output, attn_weights + + return fused_forward + + +def patch_qwen3_5_fused_attn() -> None: + from transformers.models.qwen3_5.modeling_qwen3_5 import Qwen3_5Attention + + if getattr(Qwen3_5Attention, "_axolotl_fused_attn_patched", False): + return + + Qwen3_5Attention.forward = _make_fused_forward() + Qwen3_5Attention._axolotl_fused_attn_patched = True + logger.info( + "Patched Qwen3_5Attention.forward with fused RMSNorm+RoPE Triton kernel" + ) diff --git a/src/axolotl/monkeypatch/models/qwen3_5_moe/__init__.py b/src/axolotl/monkeypatch/models/qwen3_5_moe/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/qwen3_5_moe/fused_attn.py b/src/axolotl/monkeypatch/models/qwen3_5_moe/fused_attn.py new file mode 100644 index 0000000000..08a47ca65e --- /dev/null +++ b/src/axolotl/monkeypatch/models/qwen3_5_moe/fused_attn.py @@ -0,0 +1,135 @@ +"""Qwen3.5-MoE variant of the qwen3_5 fused-attention monkeypatch.""" + +from typing import Callable + +import torch + +from axolotl.utils.logging import get_logger + +logger = get_logger(__name__) + + +def _resolve_norm_module(norm): + """Unwrap PEFT ``ModulesToSaveWrapper`` so the kernel reads the active adapter's weight.""" + modules_to_save = getattr(norm, "modules_to_save", None) + if not modules_to_save: + return norm + adapters = getattr(norm, "active_adapters", None) + if adapters is None: + adapter = getattr(norm, "active_adapter", None) + adapters = [adapter] if adapter is not None else [] + elif isinstance(adapters, str): + adapters = [adapters] + for name in adapters: + if isinstance(name, str) and name in modules_to_save: + return modules_to_save[name] + return getattr(norm, "original_module", norm) + + +def _make_fused_forward(): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + def fused_forward( + self, + hidden_states: torch.Tensor, + position_embeddings: tuple[torch.Tensor, torch.Tensor], + attention_mask: torch.Tensor | None, + past_key_values=None, + **kwargs, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + from transformers.models.qwen3_5_moe.modeling_qwen3_5_moe import ( + eager_attention_forward, + ) + + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + q_norm = _resolve_norm_module(self.q_norm) + k_norm = _resolve_norm_module(self.k_norm) + # Liger's RMSNorm replacement uses ``variance_epsilon`` instead of ``eps``. + eps = getattr(q_norm, "eps", None) + if eps is None: + eps = q_norm.variance_epsilon + + cos, sin = position_embeddings + + has_lora_qkv = hasattr(self, "apply_qkv") + if has_lora_qkv: + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states, gate = torch.chunk( + query_states.view(*input_shape, -1, self.head_dim * 2), 2, dim=-1 + ) + query_states = query_states.reshape(hidden_shape) + key_states = key_states.view(hidden_shape) + value_states = value_states.view(hidden_shape) + else: + query_states, gate = torch.chunk( + self.q_proj(hidden_states).view(*input_shape, -1, self.head_dim * 2), + 2, + dim=-1, + ) + query_states = query_states.reshape(hidden_shape) + key_states = self.k_proj(hidden_states).view(hidden_shape) + value_states = self.v_proj(hidden_states).view(hidden_shape) + gate = gate.reshape(*input_shape, -1) + + # accelerate's per-module pre-hooks that move CPU-staged params don't fire on a monkeypatched forward. + q_w = q_norm.weight + if q_w.device != query_states.device: + q_w = q_w.to(query_states.device, non_blocking=True) + k_w = k_norm.weight + if k_w.device != key_states.device: + k_w = k_w.to(key_states.device, non_blocking=True) + + query_states = fused_rms_norm_rope( + query_states, q_w, cos, sin, eps=eps, unit_offset=True + ).transpose(1, 2) + key_states = fused_rms_norm_rope( + key_states, k_w, cos, sin, eps=eps, unit_offset=True + ).transpose(1, 2) + value_states = value_states.transpose(1, 2) + + if past_key_values is not None: + key_states, value_states = past_key_values.update( + key_states, value_states, self.layer_idx + ) + + attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface( + self.config._attn_implementation, eager_attention_forward + ) + + attn_output, attn_weights = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + **kwargs, + ) + + attn_output = attn_output.reshape(*input_shape, -1).contiguous() + attn_output = attn_output * torch.sigmoid(gate) + if hasattr(self, "apply_o"): + attn_output = self.apply_o(attn_output) + else: + attn_output = self.o_proj(attn_output) + return attn_output, attn_weights + + return fused_forward + + +def patch_qwen3_5_moe_fused_attn() -> None: + from transformers.models.qwen3_5_moe.modeling_qwen3_5_moe import ( + Qwen3_5MoeAttention, + ) + + if getattr(Qwen3_5MoeAttention, "_axolotl_fused_attn_patched", False): + return + + Qwen3_5MoeAttention.forward = _make_fused_forward() + Qwen3_5MoeAttention._axolotl_fused_attn_patched = True + logger.info( + "Patched Qwen3_5MoeAttention.forward with fused RMSNorm+RoPE Triton kernel" + ) diff --git a/src/axolotl/monkeypatch/models/qwen3_moe/__init__.py b/src/axolotl/monkeypatch/models/qwen3_moe/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/qwen3_moe/fused_attn.py b/src/axolotl/monkeypatch/models/qwen3_moe/fused_attn.py new file mode 100644 index 0000000000..4f5b951ca5 --- /dev/null +++ b/src/axolotl/monkeypatch/models/qwen3_moe/fused_attn.py @@ -0,0 +1,121 @@ +"""Qwen3-MoE variant of the qwen3 fused-attention monkeypatch.""" + +from typing import Callable + +import torch + +from axolotl.utils.logging import get_logger + +logger = get_logger(__name__) + + +def _resolve_norm_module(norm): + """Unwrap PEFT ``ModulesToSaveWrapper`` so the kernel reads the active adapter's weight.""" + modules_to_save = getattr(norm, "modules_to_save", None) + if not modules_to_save: + return norm + adapters = getattr(norm, "active_adapters", None) + if adapters is None: + adapter = getattr(norm, "active_adapter", None) + adapters = [adapter] if adapter is not None else [] + elif isinstance(adapters, str): + adapters = [adapters] + for name in adapters: + if isinstance(name, str) and name in modules_to_save: + return modules_to_save[name] + return getattr(norm, "original_module", norm) + + +def _make_fused_forward(): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + def fused_forward( + self, + hidden_states: torch.Tensor, + position_embeddings: tuple[torch.Tensor, torch.Tensor], + attention_mask: torch.Tensor | None, + past_key_values=None, + **kwargs, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + from transformers.models.qwen3_moe.modeling_qwen3_moe import ( + eager_attention_forward, + ) + + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + q_norm = _resolve_norm_module(self.q_norm) + k_norm = _resolve_norm_module(self.k_norm) + eps = q_norm.variance_epsilon + + cos, sin = position_embeddings + + has_lora_qkv = hasattr(self, "apply_qkv") + if has_lora_qkv: + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states = query_states.view(hidden_shape) + key_states = key_states.view(hidden_shape) + value_states = value_states.view(hidden_shape) + else: + query_states = self.q_proj(hidden_states).view(hidden_shape) + key_states = self.k_proj(hidden_states).view(hidden_shape) + value_states = self.v_proj(hidden_states).view(hidden_shape) + + # accelerate's per-module pre-hooks that move CPU-staged params don't fire on a monkeypatched forward. + q_w = q_norm.weight + if q_w.device != query_states.device: + q_w = q_w.to(query_states.device, non_blocking=True) + k_w = k_norm.weight + if k_w.device != key_states.device: + k_w = k_w.to(key_states.device, non_blocking=True) + + query_states = fused_rms_norm_rope( + query_states, q_w, cos, sin, eps=eps + ).transpose(1, 2) + key_states = fused_rms_norm_rope(key_states, k_w, cos, sin, eps=eps).transpose( + 1, 2 + ) + value_states = value_states.transpose(1, 2) + + if past_key_values is not None: + key_states, value_states = past_key_values.update( + key_states, value_states, self.layer_idx + ) + + attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface( + self.config._attn_implementation, eager_attention_forward + ) + + attn_output, attn_weights = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + sliding_window=self.sliding_window, + **kwargs, + ) + + attn_output = attn_output.reshape(*input_shape, -1).contiguous() + if hasattr(self, "apply_o"): + attn_output = self.apply_o(attn_output) + else: + attn_output = self.o_proj(attn_output) + return attn_output, attn_weights + + return fused_forward + + +def patch_qwen3_moe_fused_attn() -> None: + from transformers.models.qwen3_moe.modeling_qwen3_moe import Qwen3MoeAttention + + if getattr(Qwen3MoeAttention, "_axolotl_fused_attn_patched", False): + return + + Qwen3MoeAttention.forward = _make_fused_forward() + Qwen3MoeAttention._axolotl_fused_attn_patched = True + logger.info( + "Patched Qwen3MoeAttention.forward with fused RMSNorm+RoPE Triton kernel" + ) diff --git a/src/axolotl/monkeypatch/models/qwen3_vl/__init__.py b/src/axolotl/monkeypatch/models/qwen3_vl/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/axolotl/monkeypatch/models/qwen3_vl/fused_attn.py b/src/axolotl/monkeypatch/models/qwen3_vl/fused_attn.py new file mode 100644 index 0000000000..68da52bd5f --- /dev/null +++ b/src/axolotl/monkeypatch/models/qwen3_vl/fused_attn.py @@ -0,0 +1,121 @@ +# Why: fuse Qwen3-VL q_norm/k_norm + mRoPE into one Triton kernel. + +from typing import Callable + +import torch + +from axolotl.utils.logging import get_logger + +logger = get_logger(__name__) + + +def _resolve_norm_module(norm): + # Why: ModulesToSaveWrapper stores trainable norm weights per active adapter. + modules_to_save = getattr(norm, "modules_to_save", None) + if not modules_to_save: + return norm + adapters = getattr(norm, "active_adapters", None) + if adapters is None: + adapter = getattr(norm, "active_adapter", None) + adapters = [adapter] if adapter is not None else [] + elif isinstance(adapters, str): + adapters = [adapters] + for name in adapters: + if isinstance(name, str) and name in modules_to_save: + return modules_to_save[name] + return getattr(norm, "original_module", norm) + + +def _make_fused_forward(): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + def fused_forward( + self, + hidden_states: torch.Tensor, + position_embeddings: tuple[torch.Tensor, torch.Tensor], + attention_mask: torch.Tensor | None, + past_key_values=None, + **kwargs, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + from transformers.models.qwen3_vl.modeling_qwen3_vl import ( + eager_attention_forward, + ) + + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + q_norm = _resolve_norm_module(self.q_norm) + k_norm = _resolve_norm_module(self.k_norm) + eps = getattr(q_norm, "eps", None) + if eps is None: + eps = q_norm.variance_epsilon + + cos, sin = position_embeddings + + if hasattr(self, "apply_qkv"): + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states = query_states.view(hidden_shape) + key_states = key_states.view(hidden_shape) + value_states = value_states.view(hidden_shape) + else: + query_states = self.q_proj(hidden_states).view(hidden_shape) + key_states = self.k_proj(hidden_states).view(hidden_shape) + value_states = self.v_proj(hidden_states).view(hidden_shape) + + # accelerate's per-module pre-hooks that move CPU-staged params don't fire on a monkeypatched forward. + q_w = q_norm.weight + if q_w.device != query_states.device: + q_w = q_w.to(query_states.device, non_blocking=True) + k_w = k_norm.weight + if k_w.device != key_states.device: + k_w = k_w.to(key_states.device, non_blocking=True) + + query_states = fused_rms_norm_rope( + query_states, q_w, cos, sin, eps=eps + ).transpose(1, 2) + key_states = fused_rms_norm_rope(key_states, k_w, cos, sin, eps=eps).transpose( + 1, 2 + ) + value_states = value_states.transpose(1, 2) + + if past_key_values is not None: + key_states, value_states = past_key_values.update( + key_states, value_states, self.layer_idx + ) + + attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface( + self.config._attn_implementation, eager_attention_forward + ) + + attn_output, attn_weights = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + **kwargs, + ) + + attn_output = attn_output.reshape(*input_shape, -1).contiguous() + if hasattr(self, "apply_o"): + attn_output = self.apply_o(attn_output) + else: + attn_output = self.o_proj(attn_output) + return attn_output, attn_weights + + return fused_forward + + +def patch_qwen3_vl_fused_attn() -> None: + from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLTextAttention + + if getattr(Qwen3VLTextAttention, "_axolotl_fused_attn_patched", False): + return + + Qwen3VLTextAttention.forward = _make_fused_forward() + Qwen3VLTextAttention._axolotl_fused_attn_patched = True + logger.info( + "Patched Qwen3VLTextAttention.forward with fused RMSNorm+mRoPE Triton kernel" + ) diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index e99959c695..dbd73b66f6 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -834,6 +834,25 @@ class AxolotlInputConfig( }, ) + fused_attn_kernel: bool | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Replace ``q_norm + apply_rotary_pos_emb`` (and the matching k path) " + "with a single fused RMSNorm+RoPE Triton kernel launch. Currently " + "implemented for Qwen3, Qwen3-MoE, Qwen3.5, and Qwen3.5-MoE " + "full-attention layers; Gemma 4 always uses the fused path. Disabled " + "(None/False) falls back " + "to the eager transformers implementation. Compile-safe via " + "torch.library.triton_op — traces under torch.compile(fullgraph=True). " + "Per-step wins are arch-dependent: ~+7-12% across sm_86 and sm_120. " + "Combining with torch_compile=true is a clear win on sm_120 (+9% " + "extra) but currently regresses on sm_86 due to Inductor autotune " + "biases — flip them on independently and benchmark." + ) + }, + ) + experts_implementation: str | None = Field( default=None, json_schema_extra={ diff --git a/tests/integrations/test_liger_qwen_vl_rope_default.py b/tests/integrations/test_liger_qwen_vl_rope_default.py new file mode 100644 index 0000000000..18b3fa8a1d --- /dev/null +++ b/tests/integrations/test_liger_qwen_vl_rope_default.py @@ -0,0 +1,124 @@ +"""``cfg.liger_rope=None`` must resolve to ``True`` for Qwen-VL so the upstream fused (m-)rope kernel is installed.""" + +from unittest.mock import patch + +import pytest + + +@pytest.mark.parametrize( + "model_type", + [ + "qwen2_vl", + "qwen2_5_vl", + "qwen3_vl", + "qwen3_vl_moe", + "qwen2_vl_text", + "qwen2_5_vl_text", + "qwen3_vl_text", + "qwen3_vl_moe_text", + ], +) +def test_liger_rope_auto_defaults_to_true_for_qwen_vl(model_type): + from axolotl.integrations.liger.plugin import LigerPlugin + from axolotl.utils.dict import DictDefault + + cfg = DictDefault( + { + "model_config_type": model_type, + "liger_rope": None, + "liger_cross_entropy": False, + "liger_fused_linear_cross_entropy": True, + "liger_rms_norm": True, + "liger_layer_norm": False, + "liger_glu_activation": False, + "liger_use_token_scaling": False, + "torch_compile": False, + "base_model": "fake/path", + "trust_remote_code": False, + } + ) + + captured = {} + + def _record( + rope: bool = True, + cross_entropy: bool = False, + fused_linear_cross_entropy: bool = True, + rms_norm: bool = True, + swiglu: bool = True, + layer_norm: bool = True, + model=None, + ): + captured.update( + rope=rope, + cross_entropy=cross_entropy, + fused_linear_cross_entropy=fused_linear_cross_entropy, + rms_norm=rms_norm, + swiglu=swiglu, + layer_norm=layer_norm, + ) + + from liger_kernel.transformers import monkey_patch as liger_mp + + with patch.dict( + liger_mp.MODEL_TYPE_TO_APPLY_LIGER_FN, + {model_type: _record}, + clear=False, + ): + LigerPlugin().pre_model_load(cfg) + + assert captured.get("rope") is True, ( + f"Expected rope=True default for {model_type}, got {captured.get('rope')}" + ) + + +def test_liger_rope_explicit_false_is_respected_for_qwen_vl(): + from axolotl.integrations.liger.plugin import LigerPlugin + from axolotl.utils.dict import DictDefault + + cfg = DictDefault( + { + "model_config_type": "qwen2_5_vl", + "liger_rope": False, + "liger_cross_entropy": False, + "liger_fused_linear_cross_entropy": True, + "liger_rms_norm": True, + "liger_layer_norm": False, + "liger_glu_activation": False, + "liger_use_token_scaling": False, + "torch_compile": False, + "base_model": "fake/path", + "trust_remote_code": False, + } + ) + + captured = {} + + def _record( + rope: bool = True, + cross_entropy: bool = False, + fused_linear_cross_entropy: bool = True, + rms_norm: bool = True, + swiglu: bool = True, + layer_norm: bool = True, + model=None, + ): + captured.update( + rope=rope, + cross_entropy=cross_entropy, + fused_linear_cross_entropy=fused_linear_cross_entropy, + rms_norm=rms_norm, + swiglu=swiglu, + layer_norm=layer_norm, + ) + + from liger_kernel.transformers import monkey_patch as liger_mp + + with patch.dict( + liger_mp.MODEL_TYPE_TO_APPLY_LIGER_FN, + {"qwen2_5_vl": _record}, + clear=False, + ): + LigerPlugin().pre_model_load(cfg) + + assert captured.get("rope") is False diff --git a/tests/kernels/test_fused_rope_autotune_telemetry.py b/tests/kernels/test_fused_rope_autotune_telemetry.py new file mode 100644 index 0000000000..bc5d61c0fc --- /dev/null +++ b/tests/kernels/test_fused_rope_autotune_telemetry.py @@ -0,0 +1,168 @@ +"""Tests for fused RMSNorm+RoPE autotune telemetry. + +Mocked end-to-end, so no Triton or CUDA is required (mirrors +``tests/integrations/test_scattermoe_autotune_telemetry.py``). +""" + +import sys +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +_MODPATH = "axolotl.kernels.gemma4_fused_rope" + + +def _make_mock_config(kwargs, num_warps=2, num_stages=1): + return SimpleNamespace( + kwargs=kwargs, num_warps=num_warps, num_stages=num_stages, num_ctas=None + ) + + +def _make_fake_module(cache=None): + kernel = SimpleNamespace(cache=cache if cache is not None else {}) + return SimpleNamespace(_rms_norm_rope_backward_kernel=kernel) + + +class TestCollector: + def test_no_module_returns_empty(self): + from axolotl.kernels.autotune_telemetry import ( + collect_fused_rope_autotune_configs, + ) + + with patch.dict(sys.modules, {_MODPATH: None}): + assert collect_fused_rope_autotune_configs() == [] + + def test_empty_cache_returns_empty(self): + from axolotl.kernels.autotune_telemetry import ( + collect_fused_rope_autotune_configs, + ) + + with patch.dict(sys.modules, {_MODPATH: _make_fake_module()}): + assert collect_fused_rope_autotune_configs() == [] + + def test_populated_cache_returns_configs(self): + from axolotl.kernels.autotune_telemetry import ( + collect_fused_rope_autotune_configs, + ) + + cfg = _make_mock_config({}, num_warps=2, num_stages=1) + fake = _make_fake_module(cache={(128, "torch.bfloat16"): cfg}) + with patch.dict(sys.modules, {_MODPATH: fake}): + result = collect_fused_rope_autotune_configs() + + assert len(result) == 1 + entry = result[0] + assert entry["kernel"] == "fused_rms_norm_rope_bwd" + assert entry["key"]["n_cols"] == 128 + assert entry["key"]["_extra"] == ["torch.bfloat16"] + assert entry["config"]["num_warps"] == 2 + assert entry["config"]["num_stages"] == 1 + + def test_multiple_head_dims(self): + """head_dim 128 (Qwen3) and 256 (Qwen3.5) get separate cache entries.""" + from axolotl.kernels.autotune_telemetry import ( + collect_fused_rope_autotune_configs, + ) + + fake = _make_fake_module( + cache={ + (128,): _make_mock_config({}, num_warps=2), + (256,): _make_mock_config({}, num_warps=4), + } + ) + with patch.dict(sys.modules, {_MODPATH: fake}): + result = collect_fused_rope_autotune_configs() + + assert {e["key"]["n_cols"] for e in result} == {128, 256} + + +class TestCallback: + def _patch_collect(self, return_value=None, side_effect=None): + return patch( + "axolotl.kernels.autotune_telemetry.collect_fused_rope_autotune_configs", + return_value=return_value, + side_effect=side_effect, + ) + + def test_reports_once_on_first_step(self): + from axolotl.kernels.autotune_telemetry import FusedRopeAutotuneReportCallback + + cb = FusedRopeAutotuneReportCallback() + state = MagicMock() + state.global_step = 1 + configs = [{"kernel": "fused_rms_norm_rope_bwd", "key": {}, "config": {}}] + + with ( + self._patch_collect(return_value=configs), + patch("axolotl.telemetry.manager.TelemetryManager") as tm_cls, + ): + tm = MagicMock() + tm.enabled = True + tm_cls.get_instance.return_value = tm + + cb.on_step_end(args=MagicMock(), state=state, control=MagicMock()) + assert tm.send_event.call_count == 1 + kw = tm.send_event.call_args[1] + assert kw["event_type"] == "fused-rope-autotune" + assert kw["properties"]["kernel_count"] == 1 + + cb.on_step_end(args=MagicMock(), state=state, control=MagicMock()) + assert tm.send_event.call_count == 1 + + def test_retries_until_step_5_then_gives_up(self): + from axolotl.kernels.autotune_telemetry import FusedRopeAutotuneReportCallback + + cb = FusedRopeAutotuneReportCallback() + with self._patch_collect(return_value=[]): + for step in range(1, 7): + state = MagicMock() + state.global_step = step + cb.on_step_end(args=MagicMock(), state=state, control=MagicMock()) + assert cb._reported is True + + def test_includes_gpu_info(self): + from axolotl.kernels.autotune_telemetry import FusedRopeAutotuneReportCallback + + cb = FusedRopeAutotuneReportCallback() + state = MagicMock() + state.global_step = 1 + configs = [{"kernel": "fused_rms_norm_rope_bwd", "key": {}, "config": {}}] + gpu = { + "gpu_name": "NVIDIA H100", + "gpu_compute_capability": "9.0", + "gpu_memory_bytes": 85899345920, + } + + with ( + self._patch_collect(return_value=configs), + patch("axolotl.kernels.autotune_telemetry._get_gpu_info", return_value=gpu), + patch("axolotl.telemetry.manager.TelemetryManager") as tm_cls, + ): + tm = MagicMock() + tm.enabled = True + tm_cls.get_instance.return_value = tm + + cb.on_step_end(args=MagicMock(), state=state, control=MagicMock()) + props = tm.send_event.call_args[1]["properties"] + assert props["gpu_name"] == "NVIDIA H100" + assert props["gpu_compute_capability"] == "9.0" + + def test_skips_send_when_telemetry_disabled(self): + from axolotl.kernels.autotune_telemetry import FusedRopeAutotuneReportCallback + + cb = FusedRopeAutotuneReportCallback() + state = MagicMock() + state.global_step = 1 + + with ( + self._patch_collect( + return_value=[{"kernel": "x", "key": {}, "config": {}}] + ), + patch("axolotl.telemetry.manager.TelemetryManager") as tm_cls, + ): + tm = MagicMock() + tm.enabled = False + tm_cls.get_instance.return_value = tm + + cb.on_step_end(args=MagicMock(), state=state, control=MagicMock()) + assert tm.send_event.call_count == 0 + assert cb._reported is True diff --git a/tests/kernels/test_gemma4_fused_rope_compile.py b/tests/kernels/test_gemma4_fused_rope_compile.py new file mode 100644 index 0000000000..dc8858722a --- /dev/null +++ b/tests/kernels/test_gemma4_fused_rope_compile.py @@ -0,0 +1,66 @@ +"""torch.compile traceability tests for the fused RMSNorm+RoPE kernel.""" + +import pytest +import torch + +pytestmark = [ + pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"), +] + + +def _make_inputs(B=2, S=64, H=4, D=64, n_rot=64, dtype=torch.bfloat16, seed=0): + torch.manual_seed(seed) + x = torch.randn(B, S, H, D, device="cuda", dtype=dtype, requires_grad=True) + w = torch.randn(D, device="cuda", dtype=dtype, requires_grad=True) + cos = torch.randn(B, S, n_rot, device="cuda", dtype=dtype) + sin = torch.randn(B, S, n_rot, device="cuda", dtype=dtype) + return x, w, cos, sin + + +def test_fused_rms_norm_rope_compile_forward_matches_eager(): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + x, w, cos, sin = _make_inputs(seed=1) + x_ref = x.detach().clone().requires_grad_(True) + w_ref = w.detach().clone().requires_grad_(True) + + y_eager = fused_rms_norm_rope(x_ref, w_ref, cos, sin, eps=1e-6) + + compiled = torch.compile(fused_rms_norm_rope, fullgraph=True, dynamic=False) + y_compiled = compiled(x, w, cos, sin, eps=1e-6) + + torch.testing.assert_close(y_compiled, y_eager, rtol=1e-2, atol=1e-2) + assert torch.isfinite(y_compiled).all() + + +def test_fused_rms_norm_rope_compile_backward(): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + x, w, cos, sin = _make_inputs(seed=2) + + compiled = torch.compile(fused_rms_norm_rope, fullgraph=True, dynamic=False) + y = compiled(x, w, cos, sin, eps=1e-6) + y.sum().backward() + + assert x.grad is not None and x.grad.isfinite().all() and x.grad.abs().sum() > 0 + assert w.grad is not None and w.grad.isfinite().all() and w.grad.abs().sum() > 0 + + +def test_fused_rms_norm_noscale_compile(): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_noscale + + torch.manual_seed(3) + x = torch.randn( + 2, 32, 4, 64, device="cuda", dtype=torch.bfloat16, requires_grad=True + ) + x_ref = x.detach().clone().requires_grad_(True) + + y_eager = fused_rms_norm_noscale(x_ref, eps=1e-6) + + compiled = torch.compile(fused_rms_norm_noscale, fullgraph=True, dynamic=False) + y_compiled = compiled(x, eps=1e-6) + + torch.testing.assert_close(y_compiled, y_eager, rtol=1e-2, atol=1e-2) + + y_compiled.sum().backward() + assert x.grad is not None and x.grad.isfinite().all() and x.grad.abs().sum() > 0 diff --git a/tests/kernels/test_gemma4_fused_rope_unit_offset.py b/tests/kernels/test_gemma4_fused_rope_unit_offset.py new file mode 100644 index 0000000000..1e84d95feb --- /dev/null +++ b/tests/kernels/test_gemma4_fused_rope_unit_offset.py @@ -0,0 +1,238 @@ +"""Correctness tests for the ``unit_offset=True`` (Gemma-style) path in the fused RMSNorm+RoPE kernel.""" + +import pytest +import torch + +torch.manual_seed(7) + +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") + + +def _rotate_half(x): + x1, x2 = x.chunk(2, dim=-1) + return torch.cat((-x2, x1), dim=-1) + + +def _reference_unit_offset(x, weight, cos, sin, eps): + x_fp32 = x.float() + rstd = torch.rsqrt(x_fp32.pow(2).mean(-1, keepdim=True) + eps) + normed = (x_fp32 * rstd * (1.0 + weight.float())).to(x.dtype) + cos_b = cos.unsqueeze(2) + sin_b = sin.unsqueeze(2) + return normed * cos_b + _rotate_half(normed) * sin_b + + +def _reference_unit_offset_partial(x, weight, cos, sin, eps): + """Reference for ``unit_offset=True`` with ``cos.shape[-1] < D`` (Qwen3.5 partial rotary).""" + n_rot = cos.shape[-1] + x_fp32 = x.float() + rstd = torch.rsqrt(x_fp32.pow(2).mean(-1, keepdim=True) + eps) + normed = (x_fp32 * rstd * (1.0 + weight.float())).to(x.dtype) + rot_part, pass_part = normed[..., :n_rot], normed[..., n_rot:] + cos_b, sin_b = cos.unsqueeze(2), sin.unsqueeze(2) + rotated = rot_part * cos_b + _rotate_half(rot_part) * sin_b + return torch.cat([rotated, pass_part], dim=-1) + + +def _reference_fp32(x, weight, cos, sin, eps, unit_offset): + """fp32 ground truth: no intermediate bf16 rounding, so it's *more* accurate + than the eager bf16 path. Handles full (``n_rot == D``) and partial rotary.""" + n_rot = cos.shape[-1] + x_fp32 = x.float() + rstd = torch.rsqrt(x_fp32.pow(2).mean(-1, keepdim=True) + eps) + scale = (1.0 + weight.float()) if unit_offset else weight.float() + normed = x_fp32 * rstd * scale + cos_b, sin_b = cos.float().unsqueeze(2), sin.float().unsqueeze(2) + rot_part, pass_part = normed[..., :n_rot], normed[..., n_rot:] + rotated = rot_part * cos_b + _rotate_half(rot_part) * sin_b + return torch.cat([rotated, pass_part], dim=-1) + + +def _assert_at_bf16_floor(y_fused, y_ref_fp32, y_eager): + """The fused kernel keeps fp32 internally and rounds once, so its bf16 output + must land at the bf16 rounding floor of the fp32 reference — and be at least + as accurate as the eager bf16 path (which rounds several times mid-compute).""" + floor = (y_ref_fp32.to(y_fused.dtype).float() - y_ref_fp32).abs().max() + fused_err = (y_fused.float() - y_ref_fp32).abs().max() + eager_err = (y_eager.float() - y_ref_fp32).abs().max() + assert fused_err <= 1.5 * floor, ( + f"fused err {fused_err:.3e} exceeds 1.5x bf16 floor {floor:.3e}" + ) + assert fused_err <= eager_err, ( + f"fused ({fused_err:.3e}) less accurate than eager bf16 path ({eager_err:.3e})" + ) + + +class TestForward: + def test_matches_reference(self): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D = 2, 32, 4, 64 + eps = 1e-6 + x = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + weight = torch.randn(D, device="cuda", dtype=torch.bfloat16) * 0.1 + cos = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + + y_fused = fused_rms_norm_rope( + x.clone(), weight, cos, sin, eps=eps, unit_offset=True + ) + y_ref_fp32 = _reference_fp32(x, weight, cos, sin, eps, unit_offset=True) + y_eager = _reference_unit_offset(x.clone(), weight, cos, sin, eps) + _assert_at_bf16_floor(y_fused, y_ref_fp32, y_eager) + + def test_no_offset_matches_fp32_reference(self): + """Qwen3 / Gemma 4 path (``unit_offset=False``) also sits at the bf16 floor.""" + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D = 2, 32, 4, 64 + eps = 1e-6 + x = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + weight = torch.randn(D, device="cuda", dtype=torch.bfloat16) * 0.1 + cos = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + + y_fused = fused_rms_norm_rope( + x.clone(), weight, cos, sin, eps=eps, unit_offset=False + ) + y_ref_fp32 = _reference_fp32(x, weight, cos, sin, eps, unit_offset=False) + x_fp32 = x.float() + rstd = torch.rsqrt(x_fp32.pow(2).mean(-1, keepdim=True) + eps) + normed = (x_fp32 * rstd * weight.float()).to(x.dtype) + y_eager = normed * cos.unsqueeze(2) + _rotate_half(normed) * sin.unsqueeze(2) + _assert_at_bf16_floor(y_fused, y_ref_fp32, y_eager) + + def test_differs_from_no_offset(self): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D = 1, 8, 2, 32 + x = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + weight = torch.randn(D, device="cuda", dtype=torch.bfloat16) + cos = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + + y_off = fused_rms_norm_rope(x, weight, cos, sin, unit_offset=False) + y_on = fused_rms_norm_rope(x, weight, cos, sin, unit_offset=True) + diff = (y_off.float() - y_on.float()).abs().max().item() + assert diff > 1e-3, f"unit_offset toggle had no effect: max_abs_diff={diff}" + + +class TestBackward: + def test_x_and_w_grad_match_eager(self): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D = 2, 16, 4, 64 + eps = 1e-6 + cos = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + weight_init = torch.randn(D, device="cuda", dtype=torch.bfloat16) * 0.1 + + x_ref = torch.randn( + B, S, H, D, device="cuda", dtype=torch.bfloat16, requires_grad=True + ) + w_ref = weight_init.clone().requires_grad_(True) + y_ref = _reference_unit_offset(x_ref, w_ref, cos, sin, eps) + y_ref.sum().backward() + + x_fused = x_ref.data.clone().requires_grad_(True) + w_fused = weight_init.clone().requires_grad_(True) + y_fused = fused_rms_norm_rope( + x_fused, w_fused, cos, sin, eps=eps, unit_offset=True + ) + y_fused.sum().backward() + + cos_sim_x = torch.nn.functional.cosine_similarity( + x_fused.grad.flatten().float(), x_ref.grad.flatten().float(), dim=0 + ) + cos_sim_w = torch.nn.functional.cosine_similarity( + w_fused.grad.flatten().float(), w_ref.grad.flatten().float(), dim=0 + ) + assert cos_sim_x > 0.999, f"x grad cosine_sim={cos_sim_x:.6f}" + assert cos_sim_w > 0.995, f"w grad cosine_sim={cos_sim_w:.6f}" + + +class TestCompile: + def test_compile_fullgraph(self): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D = 1, 8, 2, 32 + eps = 1e-6 + x = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + weight = torch.randn(D, device="cuda", dtype=torch.bfloat16) * 0.1 + cos = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + + eager = fused_rms_norm_rope(x, weight, cos, sin, eps=eps, unit_offset=True) + compiled_fn = torch.compile(fused_rms_norm_rope, fullgraph=True) + compiled = compiled_fn(x, weight, cos, sin, eps=eps, unit_offset=True) + + torch.testing.assert_close(compiled, eager, rtol=0, atol=0) + + +class TestPartialRotary: + """``unit_offset=True`` combined with ``n_rot < D`` (Qwen3.5/Qwen3.6 partial rotary).""" + + def test_forward_matches_reference(self): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D, n_rot = 2, 32, 4, 128, 64 + eps = 1e-6 + x = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + weight = torch.randn(D, device="cuda", dtype=torch.bfloat16) * 0.1 + cos = torch.randn(B, S, n_rot, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, n_rot, device="cuda", dtype=torch.bfloat16) + + y_fused = fused_rms_norm_rope( + x.clone(), weight, cos, sin, eps=eps, unit_offset=True + ) + y_ref_fp32 = _reference_fp32(x, weight, cos, sin, eps, unit_offset=True) + y_eager = _reference_unit_offset_partial(x.clone(), weight, cos, sin, eps) + _assert_at_bf16_floor(y_fused, y_ref_fp32, y_eager) + + def test_backward_x_and_w_grad_match_eager(self): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D, n_rot = 2, 16, 4, 128, 64 + eps = 1e-6 + cos = torch.randn(B, S, n_rot, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, n_rot, device="cuda", dtype=torch.bfloat16) + weight_init = torch.randn(D, device="cuda", dtype=torch.bfloat16) * 0.1 + + x_ref = torch.randn( + B, S, H, D, device="cuda", dtype=torch.bfloat16, requires_grad=True + ) + w_ref = weight_init.clone().requires_grad_(True) + y_ref = _reference_unit_offset_partial(x_ref, w_ref, cos, sin, eps) + y_ref.sum().backward() + + x_fused = x_ref.data.clone().requires_grad_(True) + w_fused = weight_init.clone().requires_grad_(True) + y_fused = fused_rms_norm_rope( + x_fused, w_fused, cos, sin, eps=eps, unit_offset=True + ) + y_fused.sum().backward() + + cos_sim_x = torch.nn.functional.cosine_similarity( + x_fused.grad.flatten().float(), x_ref.grad.flatten().float(), dim=0 + ) + cos_sim_w = torch.nn.functional.cosine_similarity( + w_fused.grad.flatten().float(), w_ref.grad.flatten().float(), dim=0 + ) + assert cos_sim_x > 0.999, f"x grad cosine_sim={cos_sim_x:.6f}" + assert cos_sim_w > 0.995, f"w grad cosine_sim={cos_sim_w:.6f}" + + def test_compile_fullgraph(self): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D, n_rot = 1, 8, 2, 64, 32 + eps = 1e-6 + x = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + weight = torch.randn(D, device="cuda", dtype=torch.bfloat16) * 0.1 + cos = torch.randn(B, S, n_rot, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, n_rot, device="cuda", dtype=torch.bfloat16) + + eager = fused_rms_norm_rope(x, weight, cos, sin, eps=eps, unit_offset=True) + compiled_fn = torch.compile(fused_rms_norm_rope, fullgraph=True) + compiled = compiled_fn(x, weight, cos, sin, eps=eps, unit_offset=True) + + torch.testing.assert_close(compiled, eager, rtol=0, atol=0) diff --git a/tests/monkeypatch/test_gemma4_fused_attn.py b/tests/monkeypatch/test_gemma4_fused_attn.py index 0530d0ee8d..92a07f7aab 100644 --- a/tests/monkeypatch/test_gemma4_fused_attn.py +++ b/tests/monkeypatch/test_gemma4_fused_attn.py @@ -1,19 +1,4 @@ -"""Tests for the Gemma 4 fused-attention monkey-patch. - -These tests exercise the patched ``Gemma4TextAttention.forward`` against -the stock implementation it replaces. The hybrid Gemma 4 model intentionally -mixes a sliding (`head_dim=32`) layer with a full-attention proportional-rope -layer (`global_head_dim=64`, `partial_rotary_factor=0.25`) so that the -partial-rotary RMSNorm+RoPE path through the fused Triton kernel is -exercised end-to-end (this is the bug originally documented in -``GEMMA4_FUSED_ROPE_HYBRID_ATTN_BUG.md``). - -The full-model forward also pins that the fused forward keeps accepting -whatever call shape ``Gemma4TextDecoderLayer.forward`` produces in the -installed transformers version — so any future signature drift on -upstream's side trips a clear failure here instead of a confusing -TypeError deep in a training run. -""" +"""Tests for the Gemma 4 fused-attention monkey-patch (hybrid sliding + partial-rotary layers).""" import pytest import torch @@ -30,8 +15,7 @@ @pytest.fixture def restore_gemma4_attention(): - """Snapshot ``Gemma4TextAttention.forward`` and restore after the test - so the monkey-patch does not leak across the suite.""" + """Snapshot ``Gemma4TextAttention.forward`` so the patch can't leak across tests.""" from transformers.models.gemma4.modeling_gemma4 import Gemma4TextAttention saved = Gemma4TextAttention.forward @@ -40,10 +24,7 @@ def restore_gemma4_attention(): def _build_hybrid_config(): - """Tiny hybrid Gemma 4 config: one sliding layer + one full-attention - layer with proportional rope and partial_rotary_factor=0.25. This is - the same shape pattern as ``google/gemma-4-26B-A4B-it`` but small - enough to fit on any GPU.""" + """Tiny hybrid Gemma 4: one sliding + one full-attention layer with ``partial_rotary_factor=0.25``.""" from transformers.models.gemma4.configuration_gemma4 import Gemma4TextConfig cfg = Gemma4TextConfig( @@ -84,15 +65,115 @@ def _build_model(seed=0): return Gemma4TextModel(cfg).cuda().to(torch.bfloat16).eval() +class TestGemma4FusedAttnLoRACompose: + """LoRA QKV + fused composition. Gemma 4 in transformers>=5.8.1 has no matching ``QKV_PATCHES`` entry yet, hence the strict xfail below.""" + + def _build_cfg(self): + from axolotl.utils.dict import DictDefault + + return DictDefault( + { + "base_model": "fake/gemma4", + "lora_qkv_kernel": True, + "lora_o_kernel": True, + "lora_dropout": 0.0, + } + ) + + @pytest.mark.xfail( + reason="Gemma 4 QKV_PATCHES need refresh for transformers 5.8.1", + strict=True, + ) + def test_lora_qkv_then_fused_does_not_raise( + self, restore_gemma4_attention, monkeypatch + ): + from transformers.models.gemma4.modeling_gemma4 import Gemma4TextAttention + + from axolotl.monkeypatch import lora_kernels + from axolotl.monkeypatch.models.gemma4.fused_attn import ( + patch_gemma4_fused_attn, + ) + + monkeypatch.setattr( + lora_kernels, + "get_attention_cls_from_config", + lambda _cfg: Gemma4TextAttention, + ) + + try: + delattr(Gemma4TextAttention, "_original_forward") + except AttributeError: + pass + + try: + lora_kernels.patch_self_attn_lora(self._build_cfg()) + assert hasattr(Gemma4TextAttention, "_original_forward"), ( + "patch_self_attn_lora must run on stock source first" + ) + patch_gemma4_fused_attn() + finally: + try: + delattr(Gemma4TextAttention, "_original_forward") + except AttributeError: + pass + + def test_reverse_order_skips_lora_rewrite( + self, restore_gemma4_attention, monkeypatch, caplog + ): + """Fused-then-LoRA must NOT install the LoRA source rewrite. Upstream + PR #3657 made ``patch_self_attn_lora`` detect ``apply_qkv``/``apply_o`` + on a fused-patched attention and skip; our ``patch_manager`` reorder + keeps this from happening in practice, but the skip path is the last + line of defense.""" + import logging + + from transformers.models.gemma4.modeling_gemma4 import Gemma4TextAttention + + from axolotl.monkeypatch import lora_kernels + from axolotl.monkeypatch.models.gemma4.fused_attn import ( + patch_gemma4_fused_attn, + ) + + monkeypatch.setattr( + lora_kernels, + "get_attention_cls_from_config", + lambda _cfg: Gemma4TextAttention, + ) + + try: + delattr(Gemma4TextAttention, "_original_forward") + except AttributeError: + pass + + try: + patch_gemma4_fused_attn() + logger = logging.getLogger("axolotl.monkeypatch.lora_kernels") + logger.addHandler(caplog.handler) + previous_level = logger.level + logger.setLevel(logging.INFO) + try: + lora_kernels.patch_self_attn_lora(self._build_cfg()) + finally: + logger.removeHandler(caplog.handler) + logger.setLevel(previous_level) + assert "fused attention" in caplog.text and "skipping" in caplog.text, ( + "expected lora_kernels to detect the fused path and log a skip; " + f"got {caplog.text}" + ) + assert not hasattr(Gemma4TextAttention, "_original_forward"), ( + "lora_kernels installed _original_forward over a fused-patched class" + ) + finally: + try: + delattr(Gemma4TextAttention, "_original_forward") + except AttributeError: + pass + + class TestFusedAttnSignature: - """The fused forward must accept the same call shape as - ``Gemma4TextDecoderLayer`` produces in the installed transformers - version. Any signature drift surfaces here as a TypeError.""" + """Pin the fused forward against the live ``Gemma4TextDecoderLayer.forward`` call shape.""" def test_decoder_layer_can_call_fused_forward(self, restore_gemma4_attention): - """Run a model forward that exercises the real - ``Gemma4TextDecoderLayer -> Gemma4TextAttention`` call path with - the fused patch installed.""" from axolotl.monkeypatch.models.gemma4.fused_attn import ( patch_gemma4_fused_attn, ) @@ -110,13 +191,9 @@ def test_decoder_layer_can_call_fused_forward(self, restore_gemma4_attention): class TestFusedAttnPerLayerCorrectness: - """Compare the patched attention layer to the stock implementation - on a single forward call. This isolates the fused kernel correctness - from cross-layer numerical drift.""" + """Single-layer comparison of patched vs stock attention to isolate kernel correctness from cross-layer drift.""" def _run_attention(self, model, layer_idx, hidden_states, position_ids): - """Call ``Gemma4TextAttention.forward`` (whatever is currently - installed) for one layer and return the output.""" attn = model.layers[layer_idx].self_attn layer_type = model.config.layer_types[layer_idx] cos, sin = model.rotary_emb(hidden_states, position_ids, layer_type) @@ -157,14 +234,11 @@ def test_forward_matches_stock(self, restore_gemma4_attention, layer_idx): assert cos_sim > 0.999, ( f"layer {layer_idx} fused vs stock cosine_sim={cos_sim:.6f}" ) - # bf16 precision: a few millis of absolute drift per element is - # acceptable for a Q/K/V projection pipeline. Anything larger is - # a real bug. torch.testing.assert_close(got, ref, rtol=5e-2, atol=5e-2) class TestFusedAttnFullModel: - """End-to-end model forward + backward through both layer types.""" + """End-to-end forward + backward through both layer types.""" def test_full_forward_matches_stock(self, restore_gemma4_attention): from axolotl.monkeypatch.models.gemma4.fused_attn import ( @@ -187,14 +261,9 @@ def test_full_forward_matches_stock(self, restore_gemma4_attention): cos_sim = torch.nn.functional.cosine_similarity( ref.flatten().float(), got.flatten().float(), dim=0 ) - # End-to-end through 2 layers (RMSNorm, attention, MLP/MoE) in bf16 - # accumulates a small amount of numerical drift; we just want to - # pin that the two paths are computing the same function. assert cos_sim > 0.999, f"end-to-end cosine_sim={cos_sim:.6f}" def test_backward_grad_flows_through_fused_path(self, restore_gemma4_attention): - """Gradients must propagate through the fused RMSNorm+RoPE kernels - for both the sliding and proportional-rope layers.""" from axolotl.monkeypatch.models.gemma4.fused_attn import ( patch_gemma4_fused_attn, ) @@ -207,8 +276,6 @@ def test_backward_grad_flows_through_fused_path(self, restore_gemma4_attention): out = m(input_ids=ids, attention_mask=mask).last_hidden_state out.sum().backward() - # Both layers must accumulate gradients on q_norm.weight and - # k_norm.weight — that proves the fused kernel ran the backward. for i, layer in enumerate(m.layers[:2]): attn = layer.self_attn assert attn.q_norm.weight.grad is not None, f"layer {i} q_norm no grad" diff --git a/tests/monkeypatch/test_qwen3_5_fused_attn.py b/tests/monkeypatch/test_qwen3_5_fused_attn.py new file mode 100644 index 0000000000..e258222e67 --- /dev/null +++ b/tests/monkeypatch/test_qwen3_5_fused_attn.py @@ -0,0 +1,424 @@ +"""Tests for the Qwen3.5 / Qwen3.5-MoE fused-attention monkeypatch.""" + +import pytest +import torch + +pytestmark = [ + pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"), +] + +pytest.importorskip("transformers.models.qwen3_5") +pytest.importorskip("transformers.models.qwen3_5_moe") + + +def _clear_patched_flag(cls): + try: + delattr(cls, "_axolotl_fused_attn_patched") + except AttributeError: + pass + + +@pytest.fixture +def restore_qwen3_5_attention(): + from transformers.models.qwen3_5.modeling_qwen3_5 import Qwen3_5Attention + + saved = Qwen3_5Attention.forward + saved_flag = getattr(Qwen3_5Attention, "_axolotl_fused_attn_patched", False) + yield Qwen3_5Attention + Qwen3_5Attention.forward = saved + if saved_flag: + Qwen3_5Attention._axolotl_fused_attn_patched = saved_flag + else: + _clear_patched_flag(Qwen3_5Attention) + + +def _build_qwen3_5_text_model(seed: int = 0): + from transformers.models.qwen3_5.configuration_qwen3_5 import Qwen3_5TextConfig + from transformers.models.qwen3_5.modeling_qwen3_5 import Qwen3_5TextModel + + torch.manual_seed(seed) + cfg = Qwen3_5TextConfig( + vocab_size=128, + hidden_size=128, + intermediate_size=128, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=32, + max_position_embeddings=512, + rms_norm_eps=1e-6, + attention_dropout=0.0, + layer_types=["full_attention", "full_attention"], + ) + cfg._attn_implementation = "sdpa" + return Qwen3_5TextModel(cfg).cuda().to(torch.bfloat16).eval() + + +def _run_attention(model, layer_idx, hidden_states, position_ids): + attn = model.layers[layer_idx].self_attn + cos, sin = model.rotary_emb(hidden_states, position_ids) + out, _ = attn( + hidden_states=hidden_states, + position_embeddings=(cos, sin), + attention_mask=None, + ) + return out + + +class TestQwen3_5FusedAttnParity: + """Single-layer parity vs stock.""" + + @pytest.mark.parametrize("layer_idx", [0, 1]) + def test_forward_matches_stock(self, restore_qwen3_5_attention, layer_idx): + from axolotl.monkeypatch.models.qwen3_5.fused_attn import ( + patch_qwen3_5_fused_attn, + ) + + m = _build_qwen3_5_text_model(seed=1) + hs = torch.randn(2, 16, 128, device="cuda", dtype=torch.bfloat16) + pos = torch.arange(16, device="cuda").unsqueeze(0).expand(2, -1) + + with torch.no_grad(): + ref = _run_attention(m, layer_idx, hs, pos) + + patch_qwen3_5_fused_attn() + with torch.no_grad(): + got = _run_attention(m, layer_idx, hs, pos) + + assert got.shape == ref.shape + assert torch.isfinite(got).all() + cos_sim = torch.nn.functional.cosine_similarity( + ref.flatten().float(), got.flatten().float(), dim=0 + ) + assert cos_sim > 0.999, ( + f"layer {layer_idx} fused vs stock cosine_sim={cos_sim:.6f}" + ) + torch.testing.assert_close(got, ref, rtol=5e-2, atol=5e-2) + + +class TestQwen3_5FusedAttnBackward: + def test_q_k_norm_grads_finite_nonzero(self, restore_qwen3_5_attention): + from axolotl.monkeypatch.models.qwen3_5.fused_attn import ( + patch_qwen3_5_fused_attn, + ) + + m = _build_qwen3_5_text_model(seed=3).train() + patch_qwen3_5_fused_attn() + + ids = torch.randint(0, 128, (2, 16), device="cuda") + mask = torch.ones(2, 16, dtype=torch.long, device="cuda") + out = m(input_ids=ids, attention_mask=mask, use_cache=False).last_hidden_state + out.sum().backward() + + for i, layer in enumerate(m.layers[:2]): + if m.config.layer_types[i] != "full_attention": + continue + attn = layer.self_attn + assert attn.q_norm.weight.grad is not None, f"layer {i} q_norm no grad" + assert attn.k_norm.weight.grad is not None, f"layer {i} k_norm no grad" + assert attn.q_norm.weight.grad.isfinite().all() + assert attn.k_norm.weight.grad.isfinite().all() + assert attn.q_norm.weight.grad.abs().sum() > 0 + assert attn.k_norm.weight.grad.abs().sum() > 0 + + +class TestQwen3_5FusedAttnLoRACompose: + """Pin LoRA-QKV → fused composition; ``QKV_PATCHES`` includes a chunk-2 variant for Qwen3.5's ``q_proj * 2``.""" + + def _build_cfg(self): + from axolotl.utils.dict import DictDefault + + return DictDefault( + { + "base_model": "fake/qwen3_5", + "lora_qkv_kernel": True, + "lora_o_kernel": True, + "lora_dropout": 0.0, + } + ) + + def test_lora_qkv_then_fused_does_not_raise( + self, restore_qwen3_5_attention, monkeypatch + ): + from transformers.models.qwen3_5.modeling_qwen3_5 import Qwen3_5Attention + + from axolotl.monkeypatch import lora_kernels + from axolotl.monkeypatch.models.qwen3_5.fused_attn import ( + patch_qwen3_5_fused_attn, + ) + + monkeypatch.setattr( + lora_kernels, + "get_attention_cls_from_config", + lambda _cfg: Qwen3_5Attention, + ) + + try: + delattr(Qwen3_5Attention, "_original_forward") + except AttributeError: + pass + + try: + lora_kernels.patch_self_attn_lora(self._build_cfg()) + assert hasattr(Qwen3_5Attention, "_original_forward"), ( + "patch_self_attn_lora must capture the stock Qwen3.5 forward — if " + "this fails, QKV_PATCHES drifted away from the chunk-2 q_proj source" + ) + patch_qwen3_5_fused_attn() + assert getattr(Qwen3_5Attention, "_axolotl_fused_attn_patched", False) + finally: + try: + delattr(Qwen3_5Attention, "_original_forward") + except AttributeError: + pass + + +@pytest.fixture +def restore_qwen3_5_moe_attention(): + from transformers.models.qwen3_5_moe.modeling_qwen3_5_moe import ( + Qwen3_5MoeAttention, + ) + + saved = Qwen3_5MoeAttention.forward + saved_flag = getattr(Qwen3_5MoeAttention, "_axolotl_fused_attn_patched", False) + yield Qwen3_5MoeAttention + Qwen3_5MoeAttention.forward = saved + if saved_flag: + Qwen3_5MoeAttention._axolotl_fused_attn_patched = saved_flag + else: + _clear_patched_flag(Qwen3_5MoeAttention) + + +def _build_qwen3_5_moe_model(seed: int = 0): + from transformers.models.qwen3_5_moe.configuration_qwen3_5_moe import ( + Qwen3_5MoeTextConfig, + ) + from transformers.models.qwen3_5_moe.modeling_qwen3_5_moe import ( + Qwen3_5MoeTextModel, + ) + + torch.manual_seed(seed) + cfg = Qwen3_5MoeTextConfig( + vocab_size=128, + hidden_size=128, + intermediate_size=128, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=32, + max_position_embeddings=512, + rms_norm_eps=1e-6, + attention_dropout=0.0, + layer_types=["full_attention", "full_attention"], + num_experts=4, + num_experts_per_tok=2, + moe_intermediate_size=64, + shared_expert_intermediate_size=64, + ) + cfg._attn_implementation = "sdpa" + return Qwen3_5MoeTextModel(cfg).cuda().to(torch.bfloat16).eval() + + +class TestQwen3_5MoeFusedAttnParity: + """End-to-end parity on the MoE variant (attention is structurally identical to dense Qwen3.5).""" + + def test_forward_matches_stock(self, restore_qwen3_5_moe_attention): + from axolotl.monkeypatch.models.qwen3_5_moe.fused_attn import ( + patch_qwen3_5_moe_fused_attn, + ) + + m = _build_qwen3_5_moe_model(seed=5) + ids = torch.randint(0, 128, (2, 16), device="cuda") + mask = torch.ones(2, 16, dtype=torch.long, device="cuda") + + with torch.no_grad(): + ref = m( + input_ids=ids, attention_mask=mask, use_cache=False + ).last_hidden_state.clone() + + patch_qwen3_5_moe_fused_attn() + with torch.no_grad(): + got = m( + input_ids=ids, attention_mask=mask, use_cache=False + ).last_hidden_state.clone() + + assert got.shape == ref.shape + assert torch.isfinite(got).all() + cos_sim = torch.nn.functional.cosine_similarity( + ref.flatten().float(), got.flatten().float(), dim=0 + ) + assert cos_sim > 0.999, f"qwen3_5_moe end-to-end cosine_sim={cos_sim:.6f}" + + +class TestQwen3_5MoeFusedAttnBackward: + """Backward grad flow through the fused Q/K-norm kernels on Qwen3.5-MoE.""" + + def test_q_k_norm_grads_finite_nonzero(self, restore_qwen3_5_moe_attention): + from axolotl.monkeypatch.models.qwen3_5_moe.fused_attn import ( + patch_qwen3_5_moe_fused_attn, + ) + + m = _build_qwen3_5_moe_model(seed=6).train() + patch_qwen3_5_moe_fused_attn() + + ids = torch.randint(0, 128, (2, 16), device="cuda") + mask = torch.ones(2, 16, dtype=torch.long, device="cuda") + out = m(input_ids=ids, attention_mask=mask, use_cache=False).last_hidden_state + out.sum().backward() + + for i, layer in enumerate(m.layers[:2]): + if m.config.layer_types[i] != "full_attention": + continue + attn = layer.self_attn + assert attn.q_norm.weight.grad is not None, f"layer {i} q_norm no grad" + assert attn.k_norm.weight.grad is not None, f"layer {i} k_norm no grad" + assert attn.q_norm.weight.grad.isfinite().all() + assert attn.k_norm.weight.grad.isfinite().all() + assert attn.q_norm.weight.grad.abs().sum() > 0 + assert attn.k_norm.weight.grad.abs().sum() > 0 + + +class TestQwen3_5MoeFusedAttnLoRACompose: + """MoE mirror of the Qwen3.5 LoRA-compose test.""" + + def _build_cfg(self): + from axolotl.utils.dict import DictDefault + + return DictDefault( + { + "base_model": "fake/qwen3_5_moe", + "lora_qkv_kernel": True, + "lora_o_kernel": True, + "lora_dropout": 0.0, + } + ) + + def test_lora_qkv_then_fused_does_not_raise( + self, restore_qwen3_5_moe_attention, monkeypatch + ): + from transformers.models.qwen3_5_moe.modeling_qwen3_5_moe import ( + Qwen3_5MoeAttention, + ) + + from axolotl.monkeypatch import lora_kernels + from axolotl.monkeypatch.models.qwen3_5_moe.fused_attn import ( + patch_qwen3_5_moe_fused_attn, + ) + + monkeypatch.setattr( + lora_kernels, + "get_attention_cls_from_config", + lambda _cfg: Qwen3_5MoeAttention, + ) + + try: + delattr(Qwen3_5MoeAttention, "_original_forward") + except AttributeError: + pass + + try: + lora_kernels.patch_self_attn_lora(self._build_cfg()) + assert hasattr(Qwen3_5MoeAttention, "_original_forward") + patch_qwen3_5_moe_fused_attn() + assert getattr(Qwen3_5MoeAttention, "_axolotl_fused_attn_patched", False) + finally: + try: + delattr(Qwen3_5MoeAttention, "_original_forward") + except AttributeError: + pass + + +class TestQwen3_5FusedAttnLigerRMSNormCompose: + """Liger swaps ``Qwen3_5RMSNorm`` for a subclass that exposes ``variance_epsilon`` instead of ``eps``.""" + + def test_forward_survives_liger_rmsnorm_swap(self, restore_qwen3_5_attention): + from axolotl.monkeypatch.models.qwen3_5.fused_attn import ( + patch_qwen3_5_fused_attn, + ) + + m = _build_qwen3_5_text_model(seed=9) + + class _StubRMSNormVarEps(torch.nn.Module): + def __init__(self, original): + super().__init__() + self.weight = original.weight + self.variance_epsilon = original.eps + + def forward(self, x): + return x + + for layer in m.layers: + if not hasattr(layer, "self_attn"): + continue + attn = layer.self_attn + attn.q_norm = _StubRMSNormVarEps(attn.q_norm) + attn.k_norm = _StubRMSNormVarEps(attn.k_norm) + + patch_qwen3_5_fused_attn() + ids = torch.randint(0, 128, (2, 16), device="cuda") + mask = torch.ones(2, 16, dtype=torch.long, device="cuda") + with torch.no_grad(): + out = m(input_ids=ids, attention_mask=mask, use_cache=False) + assert torch.isfinite(out.last_hidden_state).all() + + +class TestPatchManagerQwen3_5TextDispatch: + """Pin that ``_apply_model_specific_patches`` covers the ``*_text`` config types of multimodal Qwen3.5 / Qwen3.5-MoE checkpoints.""" + + @pytest.mark.parametrize("model_config_type", ["qwen3_5", "qwen3_5_text"]) + def test_qwen3_5_text_variant_is_patched( + self, restore_qwen3_5_attention, model_config_type + ): + from axolotl.loaders.patch_manager import PatchManager + from axolotl.utils.dict import DictDefault + + cfg = DictDefault( + { + "base_model": "fake/qwen3_5", + "model_config_type": model_config_type, + "fused_attn_kernel": True, + "lora_qkv_kernel": False, + "lora_o_kernel": False, + "context_parallel_size": 1, + } + ) + mc = type("MC", (), {"model_type": model_config_type})() + pm = PatchManager(cfg=cfg, model_config=mc, inference=False) + pm._apply_model_specific_patches() + + from transformers.models.qwen3_5.modeling_qwen3_5 import Qwen3_5Attention + + assert getattr(Qwen3_5Attention, "_axolotl_fused_attn_patched", False), ( + f"PatchManager skipped fused-attn for model_config_type=" + f"{model_config_type!r}; dispatch is missing the _text variant" + ) + + @pytest.mark.parametrize("model_config_type", ["qwen3_5_moe", "qwen3_5_moe_text"]) + def test_qwen3_5_moe_text_variant_is_patched( + self, restore_qwen3_5_moe_attention, model_config_type + ): + from axolotl.loaders.patch_manager import PatchManager + from axolotl.utils.dict import DictDefault + + cfg = DictDefault( + { + "base_model": "fake/qwen3_5_moe", + "model_config_type": model_config_type, + "fused_attn_kernel": True, + "lora_qkv_kernel": False, + "lora_o_kernel": False, + "context_parallel_size": 1, + } + ) + mc = type("MC", (), {"model_type": model_config_type})() + pm = PatchManager(cfg=cfg, model_config=mc, inference=False) + pm._apply_model_specific_patches() + + from transformers.models.qwen3_5_moe.modeling_qwen3_5_moe import ( + Qwen3_5MoeAttention, + ) + + assert getattr(Qwen3_5MoeAttention, "_axolotl_fused_attn_patched", False), ( + f"PatchManager skipped fused-attn for model_config_type=" + f"{model_config_type!r}; dispatch is missing the _text variant" + ) diff --git a/tests/monkeypatch/test_qwen3_fused_attn.py b/tests/monkeypatch/test_qwen3_fused_attn.py new file mode 100644 index 0000000000..7056eb2939 --- /dev/null +++ b/tests/monkeypatch/test_qwen3_fused_attn.py @@ -0,0 +1,283 @@ +"""Tests for the Qwen3 / Qwen3-MoE fused-attention monkeypatch.""" + +import pytest +import torch + +pytestmark = [ + pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"), +] + +pytest.importorskip("transformers.models.qwen3") +pytest.importorskip("transformers.models.qwen3_moe") + + +def _clear_patched_flag(cls): + try: + delattr(cls, "_axolotl_fused_attn_patched") + except AttributeError: + pass + + +@pytest.fixture +def restore_qwen3_attention(): + from transformers.models.qwen3.modeling_qwen3 import Qwen3Attention + + saved = Qwen3Attention.forward + saved_flag = getattr(Qwen3Attention, "_axolotl_fused_attn_patched", False) + yield Qwen3Attention + Qwen3Attention.forward = saved + if saved_flag: + Qwen3Attention._axolotl_fused_attn_patched = saved_flag + else: + _clear_patched_flag(Qwen3Attention) + + +@pytest.fixture +def restore_qwen3_moe_attention(): + from transformers.models.qwen3_moe.modeling_qwen3_moe import Qwen3MoeAttention + + saved = Qwen3MoeAttention.forward + saved_flag = getattr(Qwen3MoeAttention, "_axolotl_fused_attn_patched", False) + yield Qwen3MoeAttention + Qwen3MoeAttention.forward = saved + if saved_flag: + Qwen3MoeAttention._axolotl_fused_attn_patched = saved_flag + else: + _clear_patched_flag(Qwen3MoeAttention) + + +def _build_qwen3_model(seed: int = 0): + from transformers.models.qwen3.configuration_qwen3 import Qwen3Config + from transformers.models.qwen3.modeling_qwen3 import Qwen3Model + + torch.manual_seed(seed) + cfg = Qwen3Config( + vocab_size=128, + hidden_size=64, + intermediate_size=128, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=16, + max_position_embeddings=2048, + rms_norm_eps=1e-6, + attention_dropout=0.0, + ) + cfg._attn_implementation = "sdpa" + return Qwen3Model(cfg).cuda().to(torch.bfloat16).eval() + + +def _build_qwen3_moe_model(seed: int = 0): + from transformers.models.qwen3_moe.configuration_qwen3_moe import Qwen3MoeConfig + from transformers.models.qwen3_moe.modeling_qwen3_moe import Qwen3MoeModel + + torch.manual_seed(seed) + cfg = Qwen3MoeConfig( + vocab_size=128, + hidden_size=64, + intermediate_size=128, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=16, + max_position_embeddings=512, + rms_norm_eps=1e-6, + attention_dropout=0.0, + num_experts=4, + num_experts_per_tok=2, + moe_intermediate_size=64, + ) + cfg._attn_implementation = "sdpa" + return Qwen3MoeModel(cfg).cuda().to(torch.bfloat16).eval() + + +def _run_attention(model, layer_idx, hidden_states, position_ids): + attn = model.layers[layer_idx].self_attn + cos, sin = model.rotary_emb(hidden_states, position_ids) + out, _ = attn( + hidden_states=hidden_states, + position_embeddings=(cos, sin), + attention_mask=None, + ) + return out + + +class TestQwen3FusedAttnParity: + """Single-layer parity vs stock.""" + + @pytest.mark.parametrize("layer_idx", [0, 1]) + def test_forward_matches_stock(self, restore_qwen3_attention, layer_idx): + from axolotl.monkeypatch.models.qwen3.fused_attn import patch_qwen3_fused_attn + + m = _build_qwen3_model(seed=1) + hs = torch.randn(2, 16, 64, device="cuda", dtype=torch.bfloat16) + pos = torch.arange(16, device="cuda").unsqueeze(0).expand(2, -1) + + with torch.no_grad(): + ref = _run_attention(m, layer_idx, hs, pos) + + patch_qwen3_fused_attn() + with torch.no_grad(): + got = _run_attention(m, layer_idx, hs, pos) + + assert got.shape == ref.shape + assert torch.isfinite(got).all() + cos_sim = torch.nn.functional.cosine_similarity( + ref.flatten().float(), got.flatten().float(), dim=0 + ) + assert cos_sim > 0.999, ( + f"layer {layer_idx} fused vs stock cosine_sim={cos_sim:.6f}" + ) + torch.testing.assert_close(got, ref, rtol=5e-2, atol=5e-2) + + +class TestQwen3FusedAttnEndToEnd: + def test_full_forward_matches_stock(self, restore_qwen3_attention): + from axolotl.monkeypatch.models.qwen3.fused_attn import patch_qwen3_fused_attn + + m = _build_qwen3_model(seed=2) + ids = torch.randint(0, 128, (2, 32), device="cuda") + mask = torch.ones(2, 32, dtype=torch.long, device="cuda") + + with torch.no_grad(): + ref = m(input_ids=ids, attention_mask=mask).last_hidden_state.clone() + + patch_qwen3_fused_attn() + with torch.no_grad(): + got = m(input_ids=ids, attention_mask=mask).last_hidden_state.clone() + + assert got.shape == ref.shape + assert torch.isfinite(got).all() + cos_sim = torch.nn.functional.cosine_similarity( + ref.flatten().float(), got.flatten().float(), dim=0 + ) + assert cos_sim > 0.999, f"end-to-end cosine_sim={cos_sim:.6f}" + + def test_backward_grad_flows_through_fused_path(self, restore_qwen3_attention): + from axolotl.monkeypatch.models.qwen3.fused_attn import patch_qwen3_fused_attn + + m = _build_qwen3_model(seed=3).train() + patch_qwen3_fused_attn() + + ids = torch.randint(0, 128, (2, 16), device="cuda") + mask = torch.ones(2, 16, dtype=torch.long, device="cuda") + out = m(input_ids=ids, attention_mask=mask).last_hidden_state + out.sum().backward() + + for i, layer in enumerate(m.layers[:2]): + attn = layer.self_attn + assert attn.q_norm.weight.grad is not None, f"layer {i} q_norm no grad" + assert attn.k_norm.weight.grad is not None, f"layer {i} k_norm no grad" + assert attn.q_norm.weight.grad.isfinite().all() + assert attn.k_norm.weight.grad.isfinite().all() + assert attn.q_norm.weight.grad.abs().sum() > 0 + assert attn.k_norm.weight.grad.abs().sum() > 0 + + +class TestQwen3FusedAttnLoRACompose: + """Pin that LoRA-QKV runs before the fused patch (``inspect.getsource`` regex misses on the fused body).""" + + def test_lora_qkv_then_fused_does_not_raise(self, restore_qwen3_attention): + from axolotl.monkeypatch.lora_kernels import patch_self_attn_lora + from axolotl.monkeypatch.models.qwen3.fused_attn import patch_qwen3_fused_attn + from axolotl.utils.dict import DictDefault + + cfg = DictDefault( + { + "base_model": "Qwen/Qwen3-0.6B", + "lora_qkv_kernel": True, + "lora_o_kernel": True, + "lora_dropout": 0.0, + } + ) + + from transformers.models.qwen3.modeling_qwen3 import Qwen3Attention + + try: + delattr(Qwen3Attention, "_original_forward") + except AttributeError: + pass + + try: + patch_self_attn_lora(cfg) + assert hasattr(Qwen3Attention, "_original_forward"), ( + "patch_self_attn_lora must run on stock source first" + ) + patch_qwen3_fused_attn() + assert getattr(Qwen3Attention, "_axolotl_fused_attn_patched", False) + finally: + try: + delattr(Qwen3Attention, "_original_forward") + except AttributeError: + pass + + def test_reverse_order_breaks(self, restore_qwen3_attention): + from axolotl.monkeypatch.lora_kernels import patch_self_attn_lora + from axolotl.monkeypatch.models.qwen3.fused_attn import patch_qwen3_fused_attn + from axolotl.utils.dict import DictDefault + + cfg = DictDefault( + { + "base_model": "Qwen/Qwen3-0.6B", + "lora_qkv_kernel": True, + "lora_o_kernel": True, + "lora_dropout": 0.0, + } + ) + + from transformers.models.qwen3.modeling_qwen3 import Qwen3Attention + + try: + delattr(Qwen3Attention, "_original_forward") + except AttributeError: + pass + + patch_qwen3_fused_attn() + with pytest.raises(AssertionError, match="Original QKV code not found"): + patch_self_attn_lora(cfg) + + +class TestPatchManagerOrdering: + """Pin the patch-manager ordering invariant.""" + + def test_self_attn_lora_runs_before_model_specific(self): + import inspect + + from axolotl.loaders.patch_manager import PatchManager + + src = inspect.getsource(PatchManager.apply_pre_model_load_patches) + lora_idx = src.find("_apply_self_attention_lora_patch()") + specific_idx = src.find("_apply_model_specific_patches()") + assert lora_idx > 0 and specific_idx > 0 + assert lora_idx < specific_idx, ( + "_apply_self_attention_lora_patch must run before " + "_apply_model_specific_patches so patch_self_attn_lora sees the " + "stock attention forward source" + ) + + +class TestQwen3MoeFusedAttnParity: + """End-to-end parity on the MoE variant (attention is structurally identical to dense Qwen3).""" + + def test_full_forward_matches_stock(self, restore_qwen3_moe_attention): + from axolotl.monkeypatch.models.qwen3_moe.fused_attn import ( + patch_qwen3_moe_fused_attn, + ) + + m = _build_qwen3_moe_model(seed=4) + ids = torch.randint(0, 128, (2, 16), device="cuda") + mask = torch.ones(2, 16, dtype=torch.long, device="cuda") + + with torch.no_grad(): + ref = m(input_ids=ids, attention_mask=mask).last_hidden_state.clone() + + patch_qwen3_moe_fused_attn() + with torch.no_grad(): + got = m(input_ids=ids, attention_mask=mask).last_hidden_state.clone() + + assert got.shape == ref.shape + assert torch.isfinite(got).all() + cos_sim = torch.nn.functional.cosine_similarity( + ref.flatten().float(), got.flatten().float(), dim=0 + ) + assert cos_sim > 0.999, f"qwen3-moe end-to-end cosine_sim={cos_sim:.6f}" diff --git a/tests/monkeypatch/test_qwen3_fused_attn_defensive.py b/tests/monkeypatch/test_qwen3_fused_attn_defensive.py new file mode 100644 index 0000000000..34b22b8ee3 --- /dev/null +++ b/tests/monkeypatch/test_qwen3_fused_attn_defensive.py @@ -0,0 +1,375 @@ +"""Defensive regression tests for edge cases of the fused-attn patches.""" + +import inspect +import logging + +import pytest +import torch + +pytestmark = [ + pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"), +] + +pytest.importorskip("transformers.models.qwen3") + + +def _clear_patched_flag(cls): + try: + delattr(cls, "_axolotl_fused_attn_patched") + except AttributeError: + pass + + +@pytest.fixture +def restore_qwen3_attention(): + from transformers.models.qwen3.modeling_qwen3 import Qwen3Attention + + saved = Qwen3Attention.forward + saved_flag = getattr(Qwen3Attention, "_axolotl_fused_attn_patched", False) + yield Qwen3Attention + Qwen3Attention.forward = saved + if saved_flag: + Qwen3Attention._axolotl_fused_attn_patched = saved_flag + else: + _clear_patched_flag(Qwen3Attention) + + +@pytest.fixture +def patch_manager_caplog(caplog): + logger = logging.getLogger("axolotl.loaders.patch_manager") + logger.addHandler(caplog.handler) + previous_level = logger.level + logger.setLevel(logging.DEBUG) + try: + yield caplog + finally: + logger.removeHandler(caplog.handler) + logger.setLevel(previous_level) + + +class TestFusedAttnKernelUnsupportedWarning: + """The warning lives in ``PatchManager`` (not the schema validator) so it + runs after ``normalize_config()`` has derived ``model_config_type``. A + normal CLI flow with ``fused_attn_kernel: true`` on e.g. a Llama config + must warn loudly instead of silently no-op'ing.""" + + def test_warns_on_unsupported_model_type(self, patch_manager_caplog): + from types import SimpleNamespace + + from axolotl.loaders.patch_manager import PatchManager + + cfg = SimpleNamespace(fused_attn_kernel=True, model_config_type="llama") + PatchManager._warn_if_fused_attn_unsupported(cfg) + assert ( + "fused_attn_kernel" in patch_manager_caplog.text + and "llama" in patch_manager_caplog.text + ), f"expected warning about llama; got {patch_manager_caplog.text}" + + @pytest.mark.parametrize( + "model_type", + [ + "qwen3", + "qwen3_moe", + "qwen3_vl", + "qwen3_vl_text", + "qwen3_5", + "qwen3_5_text", + "qwen3_5_moe", + "qwen3_5_moe_text", + "gemma4", + "gemma4_text", + ], + ) + def test_no_warn_on_supported_model_type(self, patch_manager_caplog, model_type): + from types import SimpleNamespace + + from axolotl.loaders.patch_manager import PatchManager + + cfg = SimpleNamespace(fused_attn_kernel=True, model_config_type=model_type) + PatchManager._warn_if_fused_attn_unsupported(cfg) + assert not any( + "fused_attn_kernel" in r.message and model_type in r.message + for r in patch_manager_caplog.records + ), f"unexpected warning for supported {model_type}" + + def test_no_warn_when_fused_attn_kernel_false(self, patch_manager_caplog): + from types import SimpleNamespace + + from axolotl.loaders.patch_manager import PatchManager + + cfg = SimpleNamespace(fused_attn_kernel=False, model_config_type="llama") + PatchManager._warn_if_fused_attn_unsupported(cfg) + assert not any( + "fused_attn_kernel" in r.message for r in patch_manager_caplog.records + ), "no warning expected when fused_attn_kernel is False" + + def test_warning_is_invoked_by_apply_model_specific_patches(self): + """Source-line check that ``_apply_model_specific_patches`` actually + calls ``_warn_if_fused_attn_unsupported``. Without this, the standalone + helper passes its unit tests but never runs in practice.""" + import inspect + + from axolotl.loaders.patch_manager import PatchManager + + src = inspect.getsource(PatchManager._apply_model_specific_patches) + assert "_warn_if_fused_attn_unsupported" in src, ( + "_apply_model_specific_patches no longer invokes " + "_warn_if_fused_attn_unsupported — the warning will be dead code" + ) + + +class TestPeftModulesToSaveWrapper: + """``modules_to_save=["q_norm","k_norm"]`` wraps the norms in ``ModulesToSaveWrapper``; the patched forward must resolve through it.""" + + def _make_wrapper(self, original): + def _make_clone(m): + clone = torch.nn.Module() + clone.weight = torch.nn.Parameter(m.weight.detach().clone()) + clone.variance_epsilon = m.variance_epsilon + clone.eps = getattr(m, "eps", m.variance_epsilon) + return clone + + class _StubWrapper(torch.nn.Module): + """Mirrors PEFT ``ModulesToSaveWrapper``: ``_active_adapter`` is a + ``list[str]`` and ``active_adapter`` / ``active_adapters`` are + properties returning that list.""" + + def __init__(self, orig): + super().__init__() + self.original_module = orig + self.modules_to_save = torch.nn.ModuleDict( + {"default": _make_clone(orig)} + ) + self._active_adapter = ["default"] + + @property + def active_adapter(self): + return self._active_adapter + + @property + def active_adapters(self): + return self._active_adapter + + def forward(self, x): + return self.modules_to_save[self._active_adapter[0]](x) + + return _StubWrapper(original) + + def test_resolve_norm_module_returns_active_adapter_not_original(self): + """Direct unit-test of ``_resolve_norm_module``: with PEFT's actual + ``active_adapter = ["default"]`` (list), the helper must return the + wrapped adapter module, not the frozen ``original_module``. The earlier + ``isinstance(adapter, str)`` check silently failed this case.""" + from transformers.models.qwen3.modeling_qwen3 import Qwen3RMSNorm + + from axolotl.monkeypatch.models.qwen3.fused_attn import ( + _resolve_norm_module, + ) + + orig = Qwen3RMSNorm(16, eps=1e-6) + wrapper = self._make_wrapper(orig) + resolved = _resolve_norm_module(wrapper) + assert resolved is wrapper.modules_to_save["default"], ( + "_resolve_norm_module returned the frozen original instead of the " + "active adapter — PEFT stores active_adapter as a list, not a str" + ) + + def test_resolve_through_real_peft_modules_to_save(self): + """End-to-end: build a Qwen3 model, wrap ``q_norm`` / ``k_norm`` with + ``peft.get_peft_model(..., modules_to_save=[...])``, set the active + adapter's weight to a value distinct from the frozen original, and + confirm ``_resolve_norm_module`` returns the active-adapter module + (so the fused kernel reads the trainable weight, not the frozen one). + This exercises the real PEFT object shape, not a stub.""" + from peft import LoraConfig, get_peft_model + from peft.utils.other import ModulesToSaveWrapper + from transformers.models.qwen3.configuration_qwen3 import Qwen3Config + from transformers.models.qwen3.modeling_qwen3 import Qwen3ForCausalLM + + from axolotl.monkeypatch.models.qwen3.fused_attn import ( + _resolve_norm_module, + ) + + cfg = Qwen3Config( + vocab_size=128, + hidden_size=64, + intermediate_size=128, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=16, + max_position_embeddings=256, + rms_norm_eps=1e-6, + attention_dropout=0.0, + ) + base = Qwen3ForCausalLM(cfg) + lora = LoraConfig( + r=4, + lora_alpha=8, + target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], + modules_to_save=["q_norm", "k_norm"], + task_type="CAUSAL_LM", + ) + peft_model = get_peft_model(base, lora) + + attn = peft_model.base_model.model.model.layers[0].self_attn + assert isinstance(attn.q_norm, ModulesToSaveWrapper), ( + "PEFT did not wrap q_norm — test premise is invalid" + ) + with torch.no_grad(): + attn.q_norm.modules_to_save["default"].weight.fill_(7.0) + attn.q_norm.original_module.weight.fill_(0.0) + + resolved = _resolve_norm_module(attn.q_norm) + assert resolved is attn.q_norm.modules_to_save["default"], ( + "_resolve_norm_module did not return the active-adapter module — " + "real PEFT exposes active_adapter as a list, but the helper " + "treated only the str case" + ) + assert torch.equal( + resolved.weight.detach(), + torch.full_like(resolved.weight, 7.0), + ), "resolved module is not the trainable adapter weight" + + def test_qwen3_forward_under_modules_to_save_wrapper(self, restore_qwen3_attention): + from transformers.models.qwen3.configuration_qwen3 import Qwen3Config + from transformers.models.qwen3.modeling_qwen3 import Qwen3Model + + from axolotl.monkeypatch.models.qwen3.fused_attn import ( + patch_qwen3_fused_attn, + ) + + cfg = Qwen3Config( + vocab_size=128, + hidden_size=64, + intermediate_size=128, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=16, + max_position_embeddings=256, + rms_norm_eps=1e-6, + attention_dropout=0.0, + ) + cfg._attn_implementation = "sdpa" + m = Qwen3Model(cfg).cuda().to(torch.bfloat16) + + for layer in m.layers: + attn = layer.self_attn + attn.q_norm = self._make_wrapper(attn.q_norm).cuda().to(torch.bfloat16) + attn.k_norm = self._make_wrapper(attn.k_norm).cuda().to(torch.bfloat16) + + patch_qwen3_fused_attn() + ids = torch.randint(0, 128, (1, 16), device="cuda") + mask = torch.ones(1, 16, dtype=torch.long, device="cuda") + with torch.no_grad(): + out = m(input_ids=ids, attention_mask=mask).last_hidden_state + assert torch.isfinite(out).all(), ( + "fused forward through ModulesToSaveWrapper produced non-finite output" + ) + + +class TestKernelProductionHeadDim: + """Kernel parity at ``head_dim=256`` (Qwen3.5 production); unit tests only cover 32/64.""" + + @pytest.mark.parametrize("head_dim", [128, 256]) + @pytest.mark.parametrize("unit_offset", [False, True]) + def test_fused_rms_norm_rope_parity(self, head_dim, unit_offset): + from axolotl.kernels.gemma4_fused_rope import fused_rms_norm_rope + + B, S, H, D = 2, 32, 4, head_dim + torch.manual_seed(11) + x = torch.randn(B, S, H, D, device="cuda", dtype=torch.bfloat16) + w = torch.randn(D, device="cuda", dtype=torch.bfloat16) + cos = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + sin = torch.randn(B, S, D, device="cuda", dtype=torch.bfloat16) + eps = 1e-6 + + x32 = x.to(torch.float32) + rms = x32.pow(2).mean(-1, keepdim=True).add(eps).rsqrt() + scale = (w.to(torch.float32) + 1.0) if unit_offset else w.to(torch.float32) + x_norm = x32 * rms * scale + half = D // 2 + rot = torch.cat([-x_norm[..., half:], x_norm[..., :half]], dim=-1) + ref = ( + x_norm * cos.to(torch.float32).unsqueeze(2) + + rot * sin.to(torch.float32).unsqueeze(2) + ).to(torch.bfloat16) + + got = fused_rms_norm_rope(x, w, cos, sin, eps=eps, unit_offset=unit_offset) + assert got.shape == ref.shape + assert torch.isfinite(got).all() + torch.testing.assert_close(got, ref, rtol=5e-2, atol=5e-2) + + +class TestAttentionMaskPassThrough: + """Sample-packing masks must flow through the patched forward verbatim.""" + + def test_qwen3_padding_mask_runs_clean(self, restore_qwen3_attention): + from transformers.models.qwen3.configuration_qwen3 import Qwen3Config + from transformers.models.qwen3.modeling_qwen3 import Qwen3Model + + from axolotl.monkeypatch.models.qwen3.fused_attn import ( + patch_qwen3_fused_attn, + ) + + cfg = Qwen3Config( + vocab_size=128, + hidden_size=64, + intermediate_size=128, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=16, + max_position_embeddings=256, + rms_norm_eps=1e-6, + attention_dropout=0.0, + ) + cfg._attn_implementation = "sdpa" + m = Qwen3Model(cfg).cuda().to(torch.bfloat16) + patch_qwen3_fused_attn() + + ids = torch.randint(0, 128, (2, 16), device="cuda") + mask = torch.cat([torch.ones(2, 8), torch.zeros(2, 8)], dim=1).long().cuda() + with torch.no_grad(): + out = m(input_ids=ids, attention_mask=mask).last_hidden_state + assert torch.isfinite(out).all() + + +class TestSlidingWindowKwarg: + """The fused forward must preserve ``sliding_window`` on the attention-interface call.""" + + def test_fused_forward_passes_sliding_window(self): + from axolotl.monkeypatch.models.qwen3 import fused_attn + + src = inspect.getsource(fused_attn._make_fused_forward) + assert "sliding_window=self.sliding_window" in src, ( + "Qwen3 fused_forward must pass sliding_window to attention_interface " + "to preserve sliding-attention layer behavior" + ) + + +class TestGetTextConfigDispatch: + """A multimodal Qwen3-VL text branch surfaces as ``model_config_type='qwen3'``; the patch must still fire.""" + + def test_qwen3_text_branch_dispatch(self, restore_qwen3_attention): + from axolotl.loaders.patch_manager import PatchManager + from axolotl.utils.dict import DictDefault + + cfg = DictDefault( + { + "base_model": "fake/qwen3-vl-text-branch", + "model_config_type": "qwen3", + "fused_attn_kernel": True, + "lora_qkv_kernel": False, + "lora_o_kernel": False, + "context_parallel_size": 1, + } + ) + mc = type("MC", (), {"model_type": "qwen3"})() + pm = PatchManager(cfg=cfg, model_config=mc, inference=False) + pm._apply_model_specific_patches() + + from transformers.models.qwen3.modeling_qwen3 import Qwen3Attention + + assert getattr(Qwen3Attention, "_axolotl_fused_attn_patched", False) diff --git a/tests/monkeypatch/test_qwen3_fused_attn_robustness.py b/tests/monkeypatch/test_qwen3_fused_attn_robustness.py new file mode 100644 index 0000000000..2911aaeb72 --- /dev/null +++ b/tests/monkeypatch/test_qwen3_fused_attn_robustness.py @@ -0,0 +1,206 @@ +"""Robustness tests for the Qwen3 / Qwen3.5 fused-attn patches (idempotency, signature drift, GC, cross-device, FA2).""" + +import inspect + +import pytest +import torch + +pytestmark = [ + pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"), +] + +pytest.importorskip("transformers.models.qwen3") + + +def _clear_patched_flag(cls): + try: + delattr(cls, "_axolotl_fused_attn_patched") + except AttributeError: + pass + + +def _saved_state(cls): + return (cls.forward, getattr(cls, "_axolotl_fused_attn_patched", False)) + + +def _restore_state(cls, state): + saved_forward, saved_flag = state + cls.forward = saved_forward + if saved_flag: + cls._axolotl_fused_attn_patched = saved_flag + else: + _clear_patched_flag(cls) + + +@pytest.fixture +def restore_qwen3_attention(): + from transformers.models.qwen3.modeling_qwen3 import Qwen3Attention + + state = _saved_state(Qwen3Attention) + yield Qwen3Attention + _restore_state(Qwen3Attention, state) + + +def _build_tiny_qwen3(): + from transformers.models.qwen3.configuration_qwen3 import Qwen3Config + from transformers.models.qwen3.modeling_qwen3 import Qwen3Model + + cfg = Qwen3Config( + vocab_size=128, + hidden_size=64, + intermediate_size=128, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=16, + max_position_embeddings=512, + rms_norm_eps=1e-6, + attention_dropout=0.0, + ) + cfg._attn_implementation = "sdpa" + return Qwen3Model(cfg).cuda().to(torch.bfloat16) + + +class TestPatchIdempotency: + """Re-applying the patch must be a no-op (``_axolotl_fused_attn_patched`` flag guard).""" + + def test_qwen3_double_patch_is_noop(self, restore_qwen3_attention): + from transformers.models.qwen3.modeling_qwen3 import Qwen3Attention + + from axolotl.monkeypatch.models.qwen3.fused_attn import ( + patch_qwen3_fused_attn, + ) + + patch_qwen3_fused_attn() + forward_after_first = Qwen3Attention.forward + assert Qwen3Attention._axolotl_fused_attn_patched is True + + patch_qwen3_fused_attn() + assert Qwen3Attention.forward is forward_after_first, ( + "second patch_qwen3_fused_attn() call replaced .forward — the " + "_axolotl_fused_attn_patched guard is broken" + ) + + def test_qwen3_5_double_patch_is_noop(self): + pytest.importorskip("transformers.models.qwen3_5") + from transformers.models.qwen3_5.modeling_qwen3_5 import Qwen3_5Attention + + from axolotl.monkeypatch.models.qwen3_5.fused_attn import ( + patch_qwen3_5_fused_attn, + ) + + state = _saved_state(Qwen3_5Attention) + try: + patch_qwen3_5_fused_attn() + forward_after_first = Qwen3_5Attention.forward + patch_qwen3_5_fused_attn() + assert Qwen3_5Attention.forward is forward_after_first + finally: + _restore_state(Qwen3_5Attention, state) + + +class TestSignatureContract: + """Pin the stock attention forward signature; transformers drift would otherwise surface as a confusing TypeError mid-training.""" + + def test_qwen3_forward_required_params(self): + from transformers.models.qwen3.modeling_qwen3 import Qwen3Attention + + sig = inspect.signature(Qwen3Attention.forward) + params = list(sig.parameters) + assert params[:4] == [ + "self", + "hidden_states", + "position_embeddings", + "attention_mask", + ], ( + "Qwen3Attention.forward signature drifted away from the contract " + "our fused_forward replacement assumes — update the patch." + ) + + def test_qwen3_5_forward_required_params(self): + pytest.importorskip("transformers.models.qwen3_5") + from transformers.models.qwen3_5.modeling_qwen3_5 import Qwen3_5Attention + + sig = inspect.signature(Qwen3_5Attention.forward) + params = list(sig.parameters) + assert params[:4] == [ + "self", + "hidden_states", + "position_embeddings", + "attention_mask", + ] + + +class TestGradientCheckpointingCompose: + """Pin that the fused forward survives being re-run inside a checkpoint partial during backward.""" + + def test_qwen3_fused_under_gradient_checkpointing(self, restore_qwen3_attention): + from axolotl.monkeypatch.models.qwen3.fused_attn import ( + patch_qwen3_fused_attn, + ) + + m = _build_tiny_qwen3().train() + m.gradient_checkpointing_enable() + patch_qwen3_fused_attn() + + ids = torch.randint(0, 128, (2, 32), device="cuda") + mask = torch.ones(2, 32, dtype=torch.long, device="cuda") + out = m(input_ids=ids, attention_mask=mask, use_cache=False).last_hidden_state + loss = out.sum() + torch.cuda.reset_peak_memory_stats() + loss.backward() + peak_mb = torch.cuda.max_memory_allocated() / 1024**2 + + for layer in m.layers: + attn = layer.self_attn + assert attn.q_norm.weight.grad is not None + assert attn.k_norm.weight.grad is not None + assert attn.q_norm.weight.grad.isfinite().all() + assert attn.k_norm.weight.grad.isfinite().all() + assert attn.q_norm.weight.grad.abs().sum() > 0 + assert peak_mb < 1024, f"backward peak {peak_mb:.0f} MB looks like a leak" + + +class TestCrossDeviceNormWeight: + """Sharded ``device_map='auto'`` can leave norm weights on CPU; the patch must coerce them or Triton raises on the CPU pointer.""" + + def test_qwen3_norm_weight_on_cpu_does_not_crash(self, restore_qwen3_attention): + from axolotl.monkeypatch.models.qwen3.fused_attn import ( + patch_qwen3_fused_attn, + ) + + m = _build_tiny_qwen3() + patch_qwen3_fused_attn() + + for layer in m.layers: + attn = layer.self_attn + attn.q_norm.weight.data = attn.q_norm.weight.data.cpu() + attn.k_norm.weight.data = attn.k_norm.weight.data.cpu() + + ids = torch.randint(0, 128, (1, 16), device="cuda") + mask = torch.ones(1, 16, dtype=torch.long, device="cuda") + with torch.no_grad(): + out = m(input_ids=ids, attention_mask=mask).last_hidden_state + assert torch.isfinite(out).all() + + +class TestFlashAttention2Compose: + """The fused region is upstream of ``attention_interface``; pin clean composition with FA2 if it's installed.""" + + def test_qwen3_fused_under_flash_attention_2(self, restore_qwen3_attention): + pytest.importorskip("flash_attn") + from axolotl.monkeypatch.models.qwen3.fused_attn import ( + patch_qwen3_fused_attn, + ) + + m = _build_tiny_qwen3() + m.config._attn_implementation = "flash_attention_2" + for layer in m.layers: + layer.self_attn.config._attn_implementation = "flash_attention_2" + + patch_qwen3_fused_attn() + ids = torch.randint(0, 128, (2, 32), device="cuda") + mask = torch.ones(2, 32, dtype=torch.long, device="cuda") + with torch.no_grad(): + out = m(input_ids=ids, attention_mask=mask).last_hidden_state + assert torch.isfinite(out).all() diff --git a/tests/monkeypatch/test_qwen3_vl_fused_attn.py b/tests/monkeypatch/test_qwen3_vl_fused_attn.py new file mode 100644 index 0000000000..1cc422e8b5 --- /dev/null +++ b/tests/monkeypatch/test_qwen3_vl_fused_attn.py @@ -0,0 +1,168 @@ +"""Tests for the Qwen3-VL text fused-attention monkeypatch.""" + +import pytest +import torch + +pytestmark = [ + pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"), +] + +pytest.importorskip("transformers.models.qwen3_vl") + + +def _clear_patched_flag(cls): + try: + delattr(cls, "_axolotl_fused_attn_patched") + except AttributeError: + pass + + +@pytest.fixture +def restore_qwen3_vl_attention(): + from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLTextAttention + + saved = Qwen3VLTextAttention.forward + saved_flag = getattr(Qwen3VLTextAttention, "_axolotl_fused_attn_patched", False) + yield Qwen3VLTextAttention + Qwen3VLTextAttention.forward = saved + if saved_flag: + Qwen3VLTextAttention._axolotl_fused_attn_patched = saved_flag + else: + _clear_patched_flag(Qwen3VLTextAttention) + + +def _build_qwen3_vl_text_model(seed: int = 0): + from transformers.models.qwen3_vl.configuration_qwen3_vl import Qwen3VLTextConfig + from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLTextModel + + torch.manual_seed(seed) + cfg = Qwen3VLTextConfig( + vocab_size=128, + hidden_size=64, + intermediate_size=128, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=16, + max_position_embeddings=512, + rms_norm_eps=1e-6, + attention_dropout=0.0, + pad_token_id=0, + ) + cfg._attn_implementation = "sdpa" + return Qwen3VLTextModel(cfg).cuda().to(torch.bfloat16).eval() + + +def _run_attention(model, layer_idx, hidden_states, position_ids): + attn = model.layers[layer_idx].self_attn + cos, sin = model.rotary_emb(hidden_states, position_ids) + out, _ = attn( + hidden_states=hidden_states, + position_embeddings=(cos, sin), + attention_mask=None, + ) + return out + + +class TestQwen3VLFusedAttnParity: + """Single-layer parity vs stock Qwen3VLTextAttention.""" + + @pytest.mark.parametrize("layer_idx", [0, 1]) + def test_forward_matches_stock(self, restore_qwen3_vl_attention, layer_idx): + from axolotl.monkeypatch.models.qwen3_vl.fused_attn import ( + patch_qwen3_vl_fused_attn, + ) + + model = _build_qwen3_vl_text_model(seed=1) + hidden_states = torch.randn(2, 16, 64, device="cuda", dtype=torch.bfloat16) + position_ids = torch.arange(16, device="cuda").unsqueeze(0).expand(2, -1) + + with torch.no_grad(): + ref = _run_attention(model, layer_idx, hidden_states, position_ids) + + patch_qwen3_vl_fused_attn() + with torch.no_grad(): + got = _run_attention(model, layer_idx, hidden_states, position_ids) + + assert got.shape == ref.shape + assert torch.isfinite(got).all() + cos_sim = torch.nn.functional.cosine_similarity( + ref.flatten().float(), got.flatten().float(), dim=0 + ) + assert cos_sim > 0.999, ( + f"layer {layer_idx} fused vs stock cosine_sim={cos_sim:.6f}" + ) + torch.testing.assert_close(got, ref, rtol=5e-2, atol=5e-2) + + +class TestQwen3VLFusedAttnEndToEnd: + def test_full_forward_matches_stock(self, restore_qwen3_vl_attention): + from axolotl.monkeypatch.models.qwen3_vl.fused_attn import ( + patch_qwen3_vl_fused_attn, + ) + + model = _build_qwen3_vl_text_model(seed=2) + input_ids = torch.randint(0, 128, (2, 32), device="cuda") + attention_mask = torch.ones(2, 32, dtype=torch.long, device="cuda") + + with torch.no_grad(): + ref = model( + input_ids=input_ids, attention_mask=attention_mask + ).last_hidden_state.clone() + + patch_qwen3_vl_fused_attn() + with torch.no_grad(): + got = model( + input_ids=input_ids, attention_mask=attention_mask + ).last_hidden_state.clone() + + assert got.shape == ref.shape + assert torch.isfinite(got).all() + cos_sim = torch.nn.functional.cosine_similarity( + ref.flatten().float(), got.flatten().float(), dim=0 + ) + assert cos_sim > 0.999, f"end-to-end cosine_sim={cos_sim:.6f}" + + def test_backward_grad_flows_through_fused_path(self, restore_qwen3_vl_attention): + from axolotl.monkeypatch.models.qwen3_vl.fused_attn import ( + patch_qwen3_vl_fused_attn, + ) + + model = _build_qwen3_vl_text_model(seed=3).train() + patch_qwen3_vl_fused_attn() + + input_ids = torch.randint(0, 128, (2, 16), device="cuda") + attention_mask = torch.ones(2, 16, dtype=torch.long, device="cuda") + out = model( + input_ids=input_ids, attention_mask=attention_mask + ).last_hidden_state + out.sum().backward() + + for idx, layer in enumerate(model.layers[:2]): + attn = layer.self_attn + assert attn.q_norm.weight.grad is not None, f"layer {idx} q_norm no grad" + assert attn.k_norm.weight.grad is not None, f"layer {idx} k_norm no grad" + assert attn.q_norm.weight.grad.isfinite().all() + assert attn.k_norm.weight.grad.isfinite().all() + assert attn.q_norm.weight.grad.abs().sum() > 0 + assert attn.k_norm.weight.grad.abs().sum() > 0 + + +class TestQwen3VLPatchManagerDispatch: + def test_patch_manager_dispatches_qwen3_vl(self, restore_qwen3_vl_attention): + from types import SimpleNamespace + + from axolotl.loaders.patch_manager import PatchManager + + cfg = SimpleNamespace( + fused_attn_kernel=True, + model_config_type="qwen3_vl", + llama4_linearized_experts=False, + sample_packing=False, + context_parallel_size=1, + attn_uses_flash_lib=False, + ) + + PatchManager(cfg=cfg, model_config=object())._apply_model_specific_patches() + + assert getattr(restore_qwen3_vl_attention, "_axolotl_fused_attn_patched", False) From 5f23d9375c61525414de4842d44a68e8e27e27b9 Mon Sep 17 00:00:00 2001 From: Sung Hyun Cho Date: Fri, 29 May 2026 12:37:48 +0900 Subject: [PATCH 1332/1405] fix: compute kd loss in trainer to bypass broken patch/inject paths (#3661) * compute kd loss in trainer * add kd trainer compute_loss tests * remove unused kd kernel patch module * don't materialize all the logits * ensure dtype from hidden states matches dtype for chunked kd since we're not inside the autocasting anymore --------- Co-authored-by: Wing Lian --- src/axolotl/integrations/kd/__init__.py | 5 - .../integrations/kd/kernels/__init__.py | 3 +- src/axolotl/integrations/kd/kernels/models.py | 104 --------- src/axolotl/integrations/kd/trainer.py | 80 ++++--- .../test_kd_trainer_direct_loss.py | 216 ++++++++++++++++++ 5 files changed, 266 insertions(+), 142 deletions(-) delete mode 100644 src/axolotl/integrations/kd/kernels/models.py create mode 100644 tests/integrations/test_kd_trainer_direct_loss.py diff --git a/src/axolotl/integrations/kd/__init__.py b/src/axolotl/integrations/kd/__init__.py index b1a9905533..25a8537253 100644 --- a/src/axolotl/integrations/kd/__init__.py +++ b/src/axolotl/integrations/kd/__init__.py @@ -81,11 +81,6 @@ def get_collator_cls_and_kwargs(self, cfg, is_eval=False): return KDBatchSamplerDataCollatorForSeq2Seq, {} return DataCollatorForKD, {} - def pre_model_load(self, cfg): - from .kernels.models import apply_kernel - - apply_kernel(cfg.model_config_type) - def add_callbacks_post_trainer(self, cfg: Any, trainer: Trainer) -> list: """ Adds temp scheduler callback to the Trainer instance. diff --git a/src/axolotl/integrations/kd/kernels/__init__.py b/src/axolotl/integrations/kd/kernels/__init__.py index 3f1144a45c..0a967144a0 100644 --- a/src/axolotl/integrations/kd/kernels/__init__.py +++ b/src/axolotl/integrations/kd/kernels/__init__.py @@ -3,6 +3,5 @@ """ from .liger import LigerFusedLinearKLTopKLogprobLoss -from .models import apply_kernel -__all__ = ["LigerFusedLinearKLTopKLogprobLoss", "apply_kernel"] +__all__ = ["LigerFusedLinearKLTopKLogprobLoss"] diff --git a/src/axolotl/integrations/kd/kernels/models.py b/src/axolotl/integrations/kd/kernels/models.py deleted file mode 100644 index badb3460de..0000000000 --- a/src/axolotl/integrations/kd/kernels/models.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -model patcher for chunked top-k kl-div -""" - -from typing import Optional, Union, Unpack - -import torch -from transformers import Cache -from transformers.modeling_outputs import CausalLMOutputWithPast - -try: - from transformers.modeling_flash_attention_utils import FlashAttentionKwargs - from transformers.utils import LossKwargs - - class TransformersKwargs(FlashAttentionKwargs, LossKwargs): - """ - placeholder kwargs for hf model classes - """ - -except ImportError: - from transformers.utils.generic import ( # type: ignore[no-redef] - TransformersKwargs, - ) - -from axolotl.utils.callbacks.models import get_causal_lm_model_cls_prefix - - -def kldiv_forward_llama_like( - self, - input_ids: Optional[torch.LongTensor] = None, - target_logprobs: Optional[torch.Tensor] = None, - target_token_ids: Optional[torch.LongTensor] = None, - target_mask: Optional[torch.Tensor] = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[Cache] = None, - inputs_embeds: Optional[torch.FloatTensor] = None, - labels: Optional[torch.LongTensor] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - cache_position: Optional[torch.LongTensor] = None, - logits_to_keep: Union[int, torch.Tensor] = 0, - **kwargs: Unpack[TransformersKwargs], # type: ignore[misc] -) -> CausalLMOutputWithPast: - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - - # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) - outputs = self.model( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - cache_position=cache_position, - **kwargs, - ) - - hidden_states = outputs.last_hidden_state - - # Only compute necessary logits, and do not upcast them to float if we are not computing the loss - # TODO, we can optimize this further by filtering hidden_states on sequence dimension using labels != -100 - # self._loss_function should be LigerFusedLinearKLTopKLogprobLoss - - loss = self._loss_function( - self.lm_head.weight, - hidden_states, - target_token_ids, - target_logprobs, - target_mask, - true_labels=labels, - ) - num_items_in_batch = kwargs.pop("num_items_in_batch", -1) - if num_items_in_batch is not None and num_items_in_batch > 0: - loss = loss / num_items_in_batch - - return CausalLMOutputWithPast( - loss=loss, - logits=None, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - ) - - -def apply_kernel(model_type): - # Dynamically import the module and attention class - module_path = f"transformers.models.{model_type}.modeling_{model_type}" - model_cls_prefix, _ = get_causal_lm_model_cls_prefix(model_type) - module = __import__(module_path, fromlist=[f"{model_cls_prefix}ForCausalLM"]) - model_cls = getattr(module, f"{model_cls_prefix}ForCausalLM") - model_cls.forward = kldiv_forward_llama_like diff --git a/src/axolotl/integrations/kd/trainer.py b/src/axolotl/integrations/kd/trainer.py index 343d4c6dfc..e5d1504eb1 100644 --- a/src/axolotl/integrations/kd/trainer.py +++ b/src/axolotl/integrations/kd/trainer.py @@ -16,6 +16,7 @@ KD trainer """ +import torch.nn as nn from typing_extensions import override from axolotl.core.trainers.base import AxolotlTrainer @@ -23,6 +24,17 @@ from .kernels.liger import LigerFusedLinearKLTopKLogprobLoss +def _resolve_lm_head(model: nn.Module) -> nn.Module: + base = model + if hasattr(base, "get_base_model"): + base = base.get_base_model() + if hasattr(base, "language_model") and hasattr(base.language_model, "lm_head"): + return base.language_model.lm_head + if hasattr(base, "lm_head"): + return base.lm_head + raise AttributeError(f"could not find lm_head on {type(model).__name__}") + + class AxolotlKDTrainer(AxolotlTrainer): """ Custom trainer subclass for Knowledge Distillation (KD) @@ -32,7 +44,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.model_accepts_loss_kwargs = True - loss_fn = LigerFusedLinearKLTopKLogprobLoss( + self._kd_loss_fn = LigerFusedLinearKLTopKLogprobLoss( self.args.kd_ce_alpha, # hard label loss self.args.kd_alpha, # kd loss self.args.kd_temperature, @@ -40,14 +52,6 @@ def __init__(self, *args, **kwargs): compute_ce_loss=bool(self.args.kd_ce_alpha), normalize_topk=self.args.kd_normalize_topk, ) - target = self.model - - # Unwrap PEFT wrapper - if hasattr(target, "get_base_model"): - target = target.get_base_model() - - # Set on the actual model instance - target._loss_function = loss_fn def _set_signature_columns_if_needed(self): super()._set_signature_columns_if_needed() @@ -70,34 +74,48 @@ def compute_loss( return_outputs=False, num_items_in_batch=None, ): - """ - How the loss is computed by Trainer. By default, all models return the loss in the first element. - - Subclass and override for custom behavior. - """ - if ( - self.args.sample_packing - and hasattr(inputs, "attention_mask") - and hasattr(inputs, "position_ids") - ): - del inputs["attention_mask"] + inputs = dict(inputs) + + required_keys = ("labels", "target_token_ids", "target_logprobs", "target_mask") + missing = [k for k in required_keys if k not in inputs] + if missing: + raise KeyError(f"KD batch missing required keys: {missing}") if num_items_in_batch is None and "labels" in inputs: num_items_in_batch = (inputs["labels"] != -100).sum().item() - if self.model_accepts_loss_kwargs: - loss_kwargs = {} - if num_items_in_batch is not None: - loss_kwargs["num_items_in_batch"] = num_items_in_batch - inputs = {**inputs, **loss_kwargs} + labels = inputs.pop("labels") + target_token_ids = inputs.pop("target_token_ids") + target_logprobs = inputs.pop("target_logprobs") + target_mask = inputs.pop("target_mask") + + # num_items_in_batch is a loss kwarg, not a forward kwarg. + inputs.pop("num_items_in_batch", None) + inputs["output_hidden_states"] = True + inputs["return_dict"] = True + inputs["logits_to_keep"] = 1 outputs = model(**inputs) + hidden_states = getattr(outputs, "hidden_states", None) + if hidden_states is None: + raise RuntimeError( + f"{type(model).__name__}.forward did not return hidden_states" + ) + hidden_states = hidden_states[-1] + + lm_head = _resolve_lm_head(model) + hidden_states = hidden_states.to(lm_head.weight.dtype) + + loss = self._kd_loss_fn( + lm_head.weight, + hidden_states, + target_token_ids, + target_logprobs, + target_mask, + true_labels=labels, + ) - if isinstance(outputs, dict): - loss = outputs["loss"] - elif isinstance(outputs, tuple): - loss = outputs[0] - else: - loss = outputs.loss if hasattr(outputs, "loss") else outputs + if num_items_in_batch is not None and num_items_in_batch > 0: + loss = loss / num_items_in_batch return (loss, outputs) if return_outputs else loss diff --git a/tests/integrations/test_kd_trainer_direct_loss.py b/tests/integrations/test_kd_trainer_direct_loss.py new file mode 100644 index 0000000000..ce465817f6 --- /dev/null +++ b/tests/integrations/test_kd_trainer_direct_loss.py @@ -0,0 +1,216 @@ +"""Tests for AxolotlKDTrainer.compute_loss.""" + +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest +import torch +import torch.nn as nn + +# Skip the entire module if the KD trainer can't be imported (env not set up). +axolotl_kd_trainer = pytest.importorskip("axolotl.integrations.kd.trainer") +AxolotlKDTrainer = axolotl_kd_trainer.AxolotlKDTrainer +_resolve_lm_head = axolotl_kd_trainer._resolve_lm_head + + +HIDDEN = 8 +VOCAB = 16 +SEQ = 4 +BSZ = 2 +TOP_K = 3 + + +def test_resolve_lm_head_standard(): + model = SimpleNamespace(lm_head=nn.Linear(HIDDEN, VOCAB, bias=False)) + assert _resolve_lm_head(model) is model.lm_head + + +def test_resolve_lm_head_multimodal(): + lm_head = nn.Linear(HIDDEN, VOCAB, bias=False) + language_model = SimpleNamespace(lm_head=lm_head) + model = SimpleNamespace(language_model=language_model) + assert _resolve_lm_head(model) is lm_head + + +def test_resolve_lm_head_peft_wrapped(): + lm_head = nn.Linear(HIDDEN, VOCAB, bias=False) + inner = SimpleNamespace(lm_head=lm_head) + model = SimpleNamespace(get_base_model=lambda: inner) + assert _resolve_lm_head(model) is lm_head + + +def test_resolve_lm_head_peft_wrapped_multimodal(): + lm_head = nn.Linear(HIDDEN, VOCAB, bias=False) + language_model = SimpleNamespace(lm_head=lm_head) + inner = SimpleNamespace(language_model=language_model) + model = SimpleNamespace(get_base_model=lambda: inner) + assert _resolve_lm_head(model) is lm_head + + +def test_resolve_lm_head_missing_raises(): + model = SimpleNamespace() + with pytest.raises(AttributeError, match="could not find lm_head"): + _resolve_lm_head(model) + + +def _build_fake_model(): + class TinyModel(nn.Module): + def __init__(self): + super().__init__() + self.lm_head = nn.Linear(HIDDEN, VOCAB, bias=False) + + def forward(self, input_ids=None, output_hidden_states=False, **kw): + assert output_hidden_states is True + assert "labels" not in kw + assert "target_token_ids" not in kw + assert "target_logprobs" not in kw + assert "target_mask" not in kw + assert "num_items_in_batch" not in kw + hidden = torch.randn(BSZ, SEQ, HIDDEN, requires_grad=True) + return SimpleNamespace( + loss=None, + logits=None, + hidden_states=(hidden,), + past_key_values=None, + attentions=None, + ) + + return TinyModel() + + +def _build_inputs(labels=None): + if labels is None: + labels = torch.randint(0, VOCAB, (BSZ, SEQ)) + return { + "input_ids": torch.randint(0, VOCAB, (BSZ, SEQ)), + "labels": labels, + "target_token_ids": torch.randint(0, VOCAB, (BSZ, SEQ, TOP_K)), + "target_logprobs": torch.log_softmax(torch.randn(BSZ, SEQ, TOP_K), dim=-1), + "target_mask": torch.ones(BSZ, SEQ, TOP_K, dtype=torch.bool), + } + + +def test_compute_loss_calls_kd_loss_with_correct_shapes(): + kd_loss_fn = MagicMock(return_value=torch.tensor(2.0, requires_grad=True)) + fake_self = SimpleNamespace( + args=SimpleNamespace(sample_packing=False), + model_accepts_loss_kwargs=True, + _kd_loss_fn=kd_loss_fn, + ) + model = _build_fake_model() + inputs = _build_inputs() + expected_labels = inputs["labels"].clone() + expected_target_ids = inputs["target_token_ids"].clone() + + loss = AxolotlKDTrainer.compute_loss(fake_self, model, inputs) + + kd_loss_fn.assert_called_once() + args, kwargs = kd_loss_fn.call_args + assert args[0].shape == (VOCAB, HIDDEN) + assert args[1].shape == (BSZ, SEQ, HIDDEN) + assert args[2].shape == expected_target_ids.shape + assert "true_labels" in kwargs + assert kwargs["true_labels"].shape == expected_labels.shape + assert torch.isfinite(loss).all() + + +def test_compute_loss_divides_by_num_items_in_batch_from_labels(): + kd_loss_fn = MagicMock(return_value=torch.tensor(8.0)) + fake_self = SimpleNamespace( + args=SimpleNamespace(sample_packing=False), + model_accepts_loss_kwargs=True, + _kd_loss_fn=kd_loss_fn, + ) + model = _build_fake_model() + labels = torch.tensor([[1, 2, -100, 3], [4, -100, -100, -100]]) + inputs = _build_inputs(labels=labels) + + loss = AxolotlKDTrainer.compute_loss(fake_self, model, inputs) + assert torch.isclose(loss, torch.tensor(2.0)) + + +def test_compute_loss_uses_explicit_num_items_in_batch(): + kd_loss_fn = MagicMock(return_value=torch.tensor(8.0)) + fake_self = SimpleNamespace( + args=SimpleNamespace(sample_packing=False), + model_accepts_loss_kwargs=True, + _kd_loss_fn=kd_loss_fn, + ) + model = _build_fake_model() + inputs = _build_inputs() + + loss = AxolotlKDTrainer.compute_loss(fake_self, model, inputs, num_items_in_batch=2) + assert torch.isclose(loss, torch.tensor(4.0)) + + +def test_compute_loss_does_not_divide_when_zero_items(): + kd_loss_fn = MagicMock(return_value=torch.tensor(8.0)) + fake_self = SimpleNamespace( + args=SimpleNamespace(sample_packing=False), + model_accepts_loss_kwargs=True, + _kd_loss_fn=kd_loss_fn, + ) + model = _build_fake_model() + labels = torch.full((BSZ, SEQ), -100) + inputs = _build_inputs(labels=labels) + + loss = AxolotlKDTrainer.compute_loss(fake_self, model, inputs) + assert torch.isclose(loss, torch.tensor(8.0)) + + +def test_compute_loss_raises_when_kd_keys_missing(): + kd_loss_fn = MagicMock(return_value=torch.tensor(1.0)) + fake_self = SimpleNamespace( + args=SimpleNamespace(sample_packing=False), + model_accepts_loss_kwargs=True, + _kd_loss_fn=kd_loss_fn, + ) + model = _build_fake_model() + inputs = { + "input_ids": torch.randint(0, VOCAB, (BSZ, SEQ)), + "labels": torch.randint(0, VOCAB, (BSZ, SEQ)), + } + with pytest.raises(KeyError, match="KD batch missing required keys"): + AxolotlKDTrainer.compute_loss(fake_self, model, inputs) + + +def test_compute_loss_raises_when_hidden_states_missing(): + kd_loss_fn = MagicMock(return_value=torch.tensor(1.0)) + fake_self = SimpleNamespace( + args=SimpleNamespace(sample_packing=False), + model_accepts_loss_kwargs=True, + _kd_loss_fn=kd_loss_fn, + ) + + class NoHiddenStatesModel(nn.Module): + def __init__(self): + super().__init__() + self.lm_head = nn.Linear(HIDDEN, VOCAB, bias=False) + + def forward(self, **kw): + return SimpleNamespace( + loss=None, + logits=None, + hidden_states=None, + past_key_values=None, + attentions=None, + ) + + inputs = _build_inputs() + with pytest.raises(RuntimeError, match="did not return hidden_states"): + AxolotlKDTrainer.compute_loss(fake_self, NoHiddenStatesModel(), inputs) + + +def test_compute_loss_does_not_mutate_caller_inputs(): + kd_loss_fn = MagicMock(return_value=torch.tensor(1.0)) + fake_self = SimpleNamespace( + args=SimpleNamespace(sample_packing=False), + model_accepts_loss_kwargs=True, + _kd_loss_fn=kd_loss_fn, + ) + model = _build_fake_model() + inputs = _build_inputs() + original_keys = set(inputs.keys()) + + AxolotlKDTrainer.compute_loss(fake_self, model, inputs) + assert set(inputs.keys()) == original_keys From bf19bff277614f3bee4a74983654a425f3e4da00 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 29 May 2026 01:15:48 -0400 Subject: [PATCH 1333/1405] test(scattermoe-lora): skip on CUDA OOM under xdist contention (#3689) [skip-ci] When the suite runs under pytest-xdist, multiple workers race for the same physical GPU's memory budget. A test that fits comfortably in isolation can OOM purely because peer workers are already holding most of VRAM (observed: 8 workers each holding ~44 GiB on a 44 GiB card). Add a conftest in tests/integrations/kernels/scattermoe_lora/ that hooks pytest_runtest_call and converts torch.OutOfMemoryError into a skip. Real correctness bugs still surface as failures since they raise asserts / typed exceptions, not OOM. Uses a hookwrapper rather than an autouse fixture because pytest captures the test exception before re-entering the fixture's generator, so the fixture's try/except around yield never sees it. --- .../kernels/scattermoe_lora/conftest.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/integrations/kernels/scattermoe_lora/conftest.py diff --git a/tests/integrations/kernels/scattermoe_lora/conftest.py b/tests/integrations/kernels/scattermoe_lora/conftest.py new file mode 100644 index 0000000000..90e18e6a93 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/conftest.py @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Treat CUDA OOM as a skip for tests in this directory. + +When the suite runs under ``pytest-xdist``, multiple workers contend for the +same physical GPU's memory budget. A test that fits comfortably in isolation +can OOM purely because peer workers are already holding most of VRAM. That's +an environmental race, not a code defect, so converting it to a skip keeps +mixed-GPU CI green without masking real regressions (a real correctness bug +surfaces as an assert/exception, not as ``torch.OutOfMemoryError``). + +We hook ``pytest_runtest_call`` rather than using an autouse fixture because +pytest captures the test exception before re-entering the fixture's +generator — the fixture's ``try/except`` around ``yield`` never sees it. +""" + +from __future__ import annotations + +import gc + +import pytest +import torch + + +def _cuda_oom_types() -> tuple[type[BaseException], ...]: + types: list[type[BaseException]] = [] + if hasattr(torch, "OutOfMemoryError"): + types.append(torch.OutOfMemoryError) + cuda_oom = getattr(torch.cuda, "OutOfMemoryError", None) + if cuda_oom is not None and cuda_oom not in types: + types.append(cuda_oom) + return tuple(types) or (RuntimeError,) + + +_OOM = _cuda_oom_types() + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_call(item): + outcome = yield + excinfo = outcome.excinfo + if excinfo is None: + return + exc_val = excinfo[1] + if isinstance(exc_val, _OOM): + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + outcome.force_exception( + pytest.skip.Exception( + f"skipping on CUDA OOM (likely xdist worker contention): {exc_val}", + _use_item_location=True, + ) + ) From 6da2f9ef8bf14432e599218143fc0ede0f2f4bca Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 1 Jun 2026 12:23:27 -0400 Subject: [PATCH 1334/1405] add pytorch 2.12 base and prune unused base images (#3697) * add pytorch 2.12 base and prune unused base images * Add back 2.11.0 and add them to basic pytest matrices --- .github/workflows/base.yml | 24 ++++-------------------- .github/workflows/tests.yml | 4 ++-- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 90dd2b3db0..ed987558fe 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -38,14 +38,6 @@ jobs: torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" dockerfile: "Dockerfile-base" platforms: "linux/amd64,linux/arm64" - - cuda: "128" - cuda_version: 12.8.1 - cudnn_version: "" - python_version: "3.11" - pytorch: 2.10.0 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" - dockerfile: "Dockerfile-base" - platforms: "linux/amd64,linux/arm64" - cuda: "128" cuda_version: 12.8.1 cudnn_version: "" @@ -70,14 +62,6 @@ jobs: torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" dockerfile: "Dockerfile-base" platforms: "linux/amd64,linux/arm64" - - cuda: "130" - cuda_version: 13.0.0 - cudnn_version: "" - python_version: "3.12" - pytorch: 2.9.1 - torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" - dockerfile: "Dockerfile-base" - platforms: "linux/amd64,linux/arm64" - cuda: "130" cuda_version: 13.0.0 cudnn_version: "" @@ -208,19 +192,19 @@ jobs: torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" - - cuda: "128" - cuda_version: 12.8.1 + - cuda: "130" + cuda_version: 13.0.0 cudnn_version: "" python_version: "3.12" pytorch: 2.11.0 - torch_cuda_arch_list: "7.0 7.5 8.0 8.6 8.7 8.9 9.0+PTX" + torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" - cuda: "130" cuda_version: 13.0.0 cudnn_version: "" python_version: "3.12" - pytorch: 2.11.0 + pytorch: 2.12.0 torch_cuda_arch_list: "9.0 10.0 10.3 12.0+PTX" dockerfile: "Dockerfile-uv-base" platforms: "linux/amd64,linux/arm64" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8c97f8b40e..1adc76bf61 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -68,7 +68,7 @@ jobs: fail-fast: false matrix: python_version: ["3.12", "3.14"] - pytorch_version: ["2.9.1", "2.10.0"] + pytorch_version: ["2.9.1", "2.10.0", "2.11.0", "2.12.0"] exclude: - python_version: "3.14" pytorch_version: "2.9.1" @@ -155,7 +155,7 @@ jobs: fail-fast: false matrix: python_version: ["3.12", "3.14"] - pytorch_version: ["2.9.1", "2.10.0"] + pytorch_version: ["2.9.1", "2.10.0", "2.11.0", "2.12.0"] exclude: - python_version: "3.14" pytorch_version: "2.9.1" From 3f6f8c66cba3b5ee122940e5cb27f263f97ed85b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 1 Jun 2026 21:53:07 -0400 Subject: [PATCH 1335/1405] bump transformers to 5.9.0 and trl to 1.5.1 (#3696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bump transformers to 5.9.0 and trl to 1.5.1 * test(gemma4-kernelize): accept ValueError from transformers 5.9 attach_hidden_kernels transformers ≤5.8 surfaced the non-Module ``_hidden_kernels`` entry as TypeError/AttributeError via ``module.register_module(name, fn)``. 5.9 reworked ``attach_hidden_kernels`` to raise ``ValueError`` directly with a clearer error message. The patch under test (strip dead entries before ``kernelize()`` runs) does the right thing either way; broaden the expected-crash assertion so the test reflects current upstream behavior. * 30 min timeout * fix(activation-offload): drop monkey-patched __enter__ now that TRL 1.5.1 ships upstream fix TRL 1.5.1 implements huggingface/trl#5730 natively — ``OffloadActivations`` now has its own ``__enter__`` that clears tracker / stashes between steps, **plus** two things the axolotl backport never had: - ``self.tensor_id = 0`` reset (without this, the tensor_id counter accumulates across steps; harmless on its own but skews the ``fwd_stash`` eviction window). - ``torch.cuda.empty_cache()`` when bitsandbytes is loaded — flushes the BNB allocator between steps so its compute / optimizer-state buffers don't accumulate as live storage. TRL 1.5.1 also adds a ``__exit__`` that syncs the offload streams (``s0``, ``s1``) before the parent cleanup runs. The axolotl backport only overrode ``__enter__``, so ``__exit__`` was inherited correctly either way. Once we bumped TRL 1.1.0 → 1.5.1 (transformers 5.9 bundle), the monkey-patch became strictly worse than upstream — it shadowed the better ``__enter__``, dropping the ``tensor_id`` reset and the BNB ``empty_cache``. Combined with cu130's stricter cross-stream lifetime checks, this surfaced as XID 43 (driver-killed CUDA channel) during ``test_activation_offloading[lora]``, followed by every subsequent test failing at ``torch.manual_seed(42)`` because the CUDA context was permanently poisoned. Drop the patch and the wrapper — upstream is now the source of truth, per the existing TODO in this file. --- .github/workflows/tests.yml | 2 +- VERSION | 2 +- pyproject.toml | 4 ++-- .../mixins/activation_checkpointing.py | 19 ------------------- tests/monkeypatch/test_gemma4_kernelize.py | 2 +- 5 files changed, 5 insertions(+), 24 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1adc76bf61..cc5e0bd4e3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,7 +72,7 @@ jobs: exclude: - python_version: "3.14" pytorch_version: "2.9.1" - timeout-minutes: 25 + timeout-minutes: 30 steps: - name: cleanup node diff --git a/VERSION b/VERSION index b08b47558c..52fc5b872a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.16.2.dev0 +0.17.0.dev diff --git a/pyproject.toml b/pyproject.toml index 0c09c30a5d..01cac4b8b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,10 +17,10 @@ dependencies = [ "huggingface_hub>=1.1.7", "peft>=0.19.1,<0.20.0", "tokenizers>=0.22.1", - "transformers==5.8.1", + "transformers==5.9.0", "accelerate==1.13.0", "datasets>=4.8.4,<4.9.0", - "trl==1.1.0", + "trl==1.5.1", "hf_xet==1.4.3", "kernels==0.13.0", "trackio>=0.16.1", diff --git a/src/axolotl/core/trainers/mixins/activation_checkpointing.py b/src/axolotl/core/trainers/mixins/activation_checkpointing.py index dd892bebce..b61c45feed 100644 --- a/src/axolotl/core/trainers/mixins/activation_checkpointing.py +++ b/src/axolotl/core/trainers/mixins/activation_checkpointing.py @@ -22,25 +22,6 @@ LOG = get_logger(__name__) -# TODO(#3638): drop once TRL pin includes huggingface/trl#5730. Mirrors the -# upstream __enter__ override — clears cross-step state on context re-entry -# so saved tensors that never unpack during backward (MoE / torch.compile) -# don't accumulate as leaked GPU references. -def _axolotl_offload_enter(self): - self.tracker.clear() - self.storage_to_tensor_id.clear() - if self.use_streams: - self.fwd_stash.clear() - self.bwd_tensor_stash.clear() - self.bwd_ev_stash.clear() - self.is_first_forward_call = True - self.is_first_backward_call = True - return super(OffloadActivations, self).__enter__() - - -OffloadActivations.__enter__ = _axolotl_offload_enter - - class ActivationOffloadingMixin(Trainer): """ Trainer mixin class for activation checkpointing w offloading diff --git a/tests/monkeypatch/test_gemma4_kernelize.py b/tests/monkeypatch/test_gemma4_kernelize.py index 1aa461553d..ee8989fa2c 100644 --- a/tests/monkeypatch/test_gemma4_kernelize.py +++ b/tests/monkeypatch/test_gemma4_kernelize.py @@ -186,7 +186,7 @@ def build(): # Without the patch, kernelize() crashes. model = build() model.train() - with pytest.raises((TypeError, AttributeError)): + with pytest.raises((TypeError, AttributeError, ValueError)): model.kernelize() # With the patch, it succeeds. From 406aee47889758a8363a5a5d16dd98d838de8b1b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 2 Jun 2026 16:49:17 -0400 Subject: [PATCH 1336/1405] prefer latest pytorch as gated e2e tests (#3698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * prefer latest pytorch as gated e2e tests * fix(fsdp2-qlora): match _init_sharded_param anchor for torch 2.12 + fallback to 2.11 torch 2.12.0 rewrote the sharded-param construction in FSDPParam._init_sharded_param from a two-line form self.sharded_param = nn.Parameter(self.to_sharded_dtensor(sharded_param)) self.sharded_param.requires_grad_(param.requires_grad) to a single multi-line Parameter() call with requires_grad= as a kwarg self.sharded_param = nn.Parameter( self.to_sharded_dtensor(sharded_param), requires_grad=param.requires_grad, ) Functionally identical, but the axolotl monkey-patch is source-level text replacement: the 2.11 anchor no longer matches the 2.12 source, so the substitution silently falls through to the warning branch and the method stays unpatched — bnb Params4bit / Int8Params lose their quantization metadata through the FSDP2 shard cycle. Try the 2.12 anchor first; fall back to the 2.11 anchor so the patch keeps working against both torch versions in our test matrix. init_unsharded_param uses the same kwarg-style call in both 2.11 and 2.12, so its anchor is untouched. * fix(fsdp2-qlora): match init_unsharded_param anchor for torch 2.12 torch 2.12 hoisted the unsharded-param construction out of the first-all-gather `else:` branch up to method-body level, so the 2.11 anchor (8-space, inside else) no longer matched and the patch silently no-op'd. This left bitsandbytes Params4bit unreconstructed under FSDP2, surfacing as `mat1 and mat2 shapes cannot be multiplied (... 1x36864)` in QLoRA training. Add the 2.12 method-body-level anchor with its own replacement indentation, falling back to the 2.11 form. * test(multigpu): stabilize test_lora_ddp with 20 steps + seed test_lora_ddp ran only 2 steps with no seed, so train_loss was a random draw (observed 1.95-3.23 across runs) and the 2.8 threshold tripped intermittently — the torch 2.12 bump just happened to surface it. Run 20 steps with seed=42 to make the loss deterministic (2.189-2.191 spread), and tighten the threshold to 2.5. * fix(optimizers): support torch 2.11 graph health-check rename in ADOPT torch 2.11 renamed Optimizer._cuda_graph_capture_health_check to _accelerator_graph_capture_health_check (2.12 re-added the old name as an alias). ADOPT called the old name, so it raised AttributeError under torch 2.11 — surfaced by bumping the docker-e2e row from 2.9.1 to 2.11.0. Resolve whichever name exists, preferring the new one. Also swap the deprecated torch._utils.is_compiling() for torch.compiler.is_compiling(). --- .github/workflows/multi-gpu-e2e.yml | 4 +- .github/workflows/tests.yml | 12 ++--- src/axolotl/monkeypatch/fsdp2_qlora.py | 75 ++++++++++++++++++++++---- src/axolotl/utils/optimizers/adopt.py | 19 ++++--- tests/e2e/multigpu/test_llama.py | 5 +- 5 files changed, 86 insertions(+), 29 deletions(-) diff --git a/.github/workflows/multi-gpu-e2e.yml b/.github/workflows/multi-gpu-e2e.yml index 03da58f7e1..81f89da887 100644 --- a/.github/workflows/multi-gpu-e2e.yml +++ b/.github/workflows/multi-gpu-e2e.yml @@ -40,8 +40,8 @@ jobs: # dockerfile: "Dockerfile-uv.jinja" - cuda: 130 cuda_version: 13.0.0 - python_version: "3.11" - pytorch: 2.9.1 + python_version: "3.12" + pytorch: 2.12.0 axolotl_extras: # axolotl_extras: fbgemm-gpu num_gpus: 2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cc5e0bd4e3..888c3e39b9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -274,7 +274,7 @@ jobs: - cuda: 130 cuda_version: 13.0.0 python_version: "3.12" - pytorch: 2.9.1 + pytorch: 2.12.0 num_gpus: 1 axolotl_extras: steps: @@ -320,12 +320,6 @@ jobs: fail-fast: false matrix: include: - - cuda: 128 - cuda_version: 12.8.1 - python_version: "3.11" - pytorch: 2.9.1 - num_gpus: 1 - axolotl_extras: - cuda: 128 cuda_version: 12.8.1 python_version: "3.11" @@ -334,8 +328,8 @@ jobs: axolotl_extras: - cuda: 130 cuda_version: 13.0.0 - python_version: "3.11" - pytorch: 2.9.1 + python_version: "3.12" + pytorch: 2.11.0 num_gpus: 1 axolotl_extras: steps: diff --git a/src/axolotl/monkeypatch/fsdp2_qlora.py b/src/axolotl/monkeypatch/fsdp2_qlora.py index 1887c0a8aa..6fd3ffaeb8 100644 --- a/src/axolotl/monkeypatch/fsdp2_qlora.py +++ b/src/axolotl/monkeypatch/fsdp2_qlora.py @@ -26,8 +26,15 @@ def apply_init_sharded_param_patch(): original_source = inspect.getsource(FSDPParam._init_sharded_param) original_source, _ = detab_code(original_source) - # Define the replacement - original_param_creation = """ self.sharded_param = nn.Parameter(self.to_sharded_dtensor(sharded_param)) + # torch 2.12 rewrote the sharded-param construction from a two-line + # form (Parameter() + requires_grad_()) to a single multi-line + # Parameter() call with requires_grad= as a kwarg. Try the 2.12 + # anchor first, fall back to the 2.11 form. + anchors_2120 = """ self.sharded_param = nn.Parameter( + self.to_sharded_dtensor(sharded_param), + requires_grad=param.requires_grad, + )""" + anchors_2110 = """ self.sharded_param = nn.Parameter(self.to_sharded_dtensor(sharded_param)) self.sharded_param.requires_grad_(param.requires_grad)""" patched_param_creation = """ import bitsandbytes as bnb @@ -59,8 +66,13 @@ def apply_init_sharded_param_patch(): requires_grad=param.requires_grad, )""" + original_param_creation = next( + (a for a in (anchors_2120, anchors_2110) if a in original_source), + None, + ) + # Apply the replacement - if original_param_creation in original_source: + if original_param_creation is not None: patched_source = original_source.replace( original_param_creation, patched_param_creation ) @@ -103,12 +115,48 @@ def apply_init_unsharded_param_patch(): original_source = inspect.getsource(FSDPParam.init_unsharded_param) original_source, _ = detab_code(original_source) - # Define the replacement - original_param_creation = """ self._unsharded_param = nn.Parameter( + # torch 2.12 hoisted the unsharded-param construction out of the + # first-all-gather `else:` branch up to method-body level, so the 2.11 + # anchor (8-space, inside else) no longer matches. The replacement must be + # indented to match whichever anchor is found, so each anchor carries its + # own. Try the 2.12 anchor first, fall back to the 2.11 form. + anchor_replacement_pairs = [ + ( + """ self._unsharded_param = nn.Parameter( + unsharded_param, requires_grad=self.sharded_param.requires_grad + )""", + """ import bitsandbytes as bnb + local_tensor = self.sharded_param._local_tensor + if isinstance(local_tensor, bnb.nn.modules.Params4bit): + self._unsharded_param = bnb.nn.modules.Params4bit( + data=unsharded_param, + requires_grad=self.sharded_param.requires_grad, + quant_state=local_tensor.quant_state, + blocksize=local_tensor.blocksize, + compress_statistics=local_tensor.compress_statistics, + quant_type=local_tensor.quant_type, + quant_storage=local_tensor.quant_storage, + module=local_tensor.module, + bnb_quantized=local_tensor.bnb_quantized, + ) + elif isinstance(local_tensor, bnb.nn.modules.Int8Params): + self._unsharded_param = bnb.nn.modules.Int8Params( + data=unsharded_param, + requires_grad=self.sharded_param.requires_grad, + has_fp16_weights=local_tensor.has_fp16_weights, + CB=unsharded_param, + SCB=local_tensor.SCB, + ) + else: + self._unsharded_param = nn.Parameter( unsharded_param, requires_grad=self.sharded_param.requires_grad - )""" - - patched_param_creation = """ import bitsandbytes as bnb + )""", + ), + ( + """ self._unsharded_param = nn.Parameter( + unsharded_param, requires_grad=self.sharded_param.requires_grad + )""", + """ import bitsandbytes as bnb local_tensor = self.sharded_param._local_tensor if isinstance(local_tensor, bnb.nn.modules.Params4bit): self._unsharded_param = bnb.nn.modules.Params4bit( @@ -133,10 +181,17 @@ def apply_init_unsharded_param_patch(): else: self._unsharded_param = nn.Parameter( unsharded_param, requires_grad=self.sharded_param.requires_grad - )""" + )""", + ), + ] + + original_param_creation, patched_param_creation = next( + ((a, p) for a, p in anchor_replacement_pairs if a in original_source), + (None, None), + ) # Apply the replacement - if original_param_creation in original_source: + if original_param_creation is not None: patched_source = original_source.replace( original_param_creation, patched_param_creation ) diff --git a/src/axolotl/utils/optimizers/adopt.py b/src/axolotl/utils/optimizers/adopt.py index 20ddfa7ec4..f501859b6f 100644 --- a/src/axolotl/utils/optimizers/adopt.py +++ b/src/axolotl/utils/optimizers/adopt.py @@ -196,7 +196,14 @@ def step(self, closure=None): closure (Callable, optional): A closure that reevaluates the model and returns the loss. """ - self._cuda_graph_capture_health_check() + # torch 2.11 renamed _cuda_graph_capture_health_check -> + # _accelerator_graph_capture_health_check (the 2.11-only name); 2.12 + # re-added the old name as an alias. Prefer the new name, fall back. + health_check = getattr( + self, "_accelerator_graph_capture_health_check", None + ) or getattr(self, "_cuda_graph_capture_health_check", None) + if health_check is not None: + health_check() loss = None if closure is not None: @@ -282,7 +289,7 @@ def _single_tensor_adopt( step_t = state_steps[i] # If compiling, the compiler will handle cudagraph checks, see note [torch.compile x capturable] - if not torch._utils.is_compiling() and capturable: + if not torch.compiler.is_compiling() and capturable: capturable_supported_devices = _get_capturable_supported_devices() assert ( param.device.type == step_t.device.type @@ -358,7 +365,7 @@ def _multi_tensor_adopt( ) # If compiling, the compiler will handle cudagraph checks, see note [torch.compile x capturable] - if not torch._utils.is_compiling() and capturable: + if not torch.compiler.is_compiling() and capturable: capturable_supported_devices = _get_capturable_supported_devices( supports_xla=False ) @@ -415,7 +422,7 @@ def _multi_tensor_adopt( # If steps are on CPU, foreach will fall back to the slow path, which is a for-loop calling t.add(1) over # and over. 1 will then be wrapped into a Tensor over and over again, which is slower than if we just # wrapped it once now. The alpha is required to assure we go to the right overload. - if not torch._utils.is_compiling() and device_state_steps[0].is_cpu: + if not torch.compiler.is_compiling() and device_state_steps[0].is_cpu: torch._foreach_add_( device_state_steps, torch.tensor(1.0, device="cpu"), alpha=1.0 ) @@ -448,7 +455,7 @@ def _multi_tensor_adopt( # If steps are on CPU, foreach will fall back to the slow path, which is a for-loop calling t.add(1) over # and over. 1 will then be wrapped into a Tensor over and over again, which is slower than if we just # wrapped it once now. The alpha is required to assure we go to the right overload. - if not torch._utils.is_compiling() and device_state_steps[0].is_cpu: + if not torch.compiler.is_compiling() and device_state_steps[0].is_cpu: torch._foreach_add_( device_state_steps, torch.tensor(1.0, device="cpu"), alpha=1.0 ) @@ -501,7 +508,7 @@ def adopt( # this check is slow during compilation, so we skip it # if it's strictly needed we can add this check back in dynamo - if not torch._utils.is_compiling() and not all( + if not torch.compiler.is_compiling() and not all( isinstance(t, torch.Tensor) for t in state_steps ): raise RuntimeError( diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index b89c935228..432a7e9e5c 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -56,7 +56,7 @@ def test_lora_ddp(self, temp_dir): }, ], "num_epochs": 1, - "max_steps": 2, + "max_steps": 20, "micro_batch_size": 1, "gradient_accumulation_steps": 2, # "gradient_checkpointing": True, @@ -69,6 +69,7 @@ def test_lora_ddp(self, temp_dir): "use_tensorboard": True, "bf16": True, "save_first_step": False, + "seed": 42, } ) @@ -90,7 +91,7 @@ def test_lora_ddp(self, temp_dir): ) check_tensorboard( - temp_dir + "/runs", "train/train_loss", 2.8, "Train Loss (%s) is too high" + temp_dir + "/runs", "train/train_loss", 2.5, "Train Loss (%s) is too high" ) @pytest.mark.parametrize( From 09d325b4fd1288b1473c8a330dd19e3c91b1ac32 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 3 Jun 2026 04:29:47 -0400 Subject: [PATCH 1337/1405] fix(ci): build pypi release via `uv build` instead of removed setup.py (#3700) [skip ci] The pyproject migration removed setup.py, so the publish workflow failed at `python setup.py sdist` (No such file). Build the sdist+wheel with `uv build` (PEP 517; setuptools backend reads the version from VERSION). Also make the GitHub release step idempotent so a re-run/re-tag of an existing release doesn't fail, and drop the unused dependency-install step. --- .github/workflows/pypi.yml | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index c2fc1c9d87..c03c209c72 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -8,9 +8,6 @@ on: permissions: {} -env: - UV_SYSTEM_PYTHON: "1" - jobs: setup_release: name: Create Release @@ -24,7 +21,10 @@ jobs: - name: Create release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release create "$GITHUB_REF_NAME" --generate-notes + # idempotent: don't fail a re-run if the release already exists + run: | + gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 \ + || gh release create "$GITHUB_REF_NAME" --generate-notes pypi-publish: name: Upload release to PyPI runs-on: ubuntu-latest @@ -47,13 +47,6 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 - - name: Install dependencies - run: | - uv pip install wheel packaging - uv pip install --no-build-isolation -e . - uv pip install black mypy pre-commit types-requests quartodoc jupyter blobfile tiktoken \ - codecov codecov-cli pytest pytest-cov pytest-retry pytest-sugar pytest-xdist tbparse - - name: Extract tag name id: tag run: echo "TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3)" >> "$GITHUB_OUTPUT" @@ -62,9 +55,10 @@ jobs: run: | echo "${{ steps.tag.outputs.TAG_NAME }}" | sed 's/^v//' > VERSION - - name: Build a source dist - run: | - python setup.py sdist + - name: Build sdist and wheel + # PEP 517 build via uv (setuptools backend reads the version from VERSION); + # replaces the removed `python setup.py sdist` after the pyproject migration. + run: uv build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 From 41ef48facdf4b31c34879ff48adfb1176095be5f Mon Sep 17 00:00:00 2001 From: thad0ctor Date: Thu, 4 Jun 2026 05:35:03 -0700 Subject: [PATCH 1338/1405] fix(gemma4): key shared KV by layer_type on transformers >=5.8 (#3701) The fused Gemma4 attention monkeypatch read and stored shared KV states by `kv_shared_layer_index`/`layer_idx`, but transformers 5.8 dropped the `kv_shared_layer_index` attribute and switched to keying `shared_kv_states` by `layer_type`. On the pinned transformers 5.9, any Gemma4 model with `num_kv_shared_layers > 0` (e.g. gemma-4-E2B vision) raised `AttributeError: 'Gemma4TextAttention' object has no attribute 'kv_shared_layer_index'` once execution reached a shared layer. Derive the read/store key from whichever attribute the installed transformers exposes, keeping compatibility with both the old and new APIs. Add a fused-attn regression with `num_kv_shared_layers > 0` so the shared-KV branch is actually exercised (existing tests defaulted to 0). --- .../monkeypatch/models/gemma4/fused_attn.py | 17 ++++- tests/monkeypatch/test_gemma4_fused_attn.py | 71 +++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/axolotl/monkeypatch/models/gemma4/fused_attn.py b/src/axolotl/monkeypatch/models/gemma4/fused_attn.py index 4f4e8f5f2d..a4f15207f8 100644 --- a/src/axolotl/monkeypatch/models/gemma4/fused_attn.py +++ b/src/axolotl/monkeypatch/models/gemma4/fused_attn.py @@ -31,6 +31,19 @@ def _get_shared_kv_states(): return _GEMMA4_SHARED_KV_STORE["store"] +def _shared_kv_read_key(attn): + # transformers >=5.8 keys shared kv by layer_type; older builds used kv_shared_layer_index + if hasattr(attn, "kv_shared_layer_index"): + return attn.kv_shared_layer_index + return attn.layer_type + + +def _shared_kv_store_key(attn): + if hasattr(attn, "kv_shared_layer_index"): + return attn.layer_idx + return attn.layer_type + + def _make_fused_forward(original_forward): """Create a patched forward that uses fused RMSNorm+RoPE kernels.""" @@ -85,7 +98,7 @@ def fused_forward( # ---- K/V path ---- if self.is_kv_shared_layer: - key_states, value_states = shared_kv_states[self.kv_shared_layer_index] + key_states, value_states = shared_kv_states[_shared_kv_read_key(self)] key_states = key_states.to(query_states.device) value_states = value_states.to(query_states.device) else: @@ -124,7 +137,7 @@ def fused_forward( key_states, value_states, self.layer_idx ) if self.store_full_length_kv: - shared_kv_states[self.layer_idx] = key_states, value_states + shared_kv_states[_shared_kv_store_key(self)] = key_states, value_states attention_interface: Callable = eager_attention_forward if self.config._attn_implementation != "eager": diff --git a/tests/monkeypatch/test_gemma4_fused_attn.py b/tests/monkeypatch/test_gemma4_fused_attn.py index 92a07f7aab..db719c4cc7 100644 --- a/tests/monkeypatch/test_gemma4_fused_attn.py +++ b/tests/monkeypatch/test_gemma4_fused_attn.py @@ -170,6 +170,77 @@ def test_reverse_order_skips_lora_rewrite( pass +def _build_kv_shared_config(): + """Hybrid Gemma 4 with ``num_kv_shared_layers > 0`` so the fused shared-KV branch runs.""" + from transformers.models.gemma4.configuration_gemma4 import Gemma4TextConfig + + cfg = Gemma4TextConfig( + vocab_size=128, + hidden_size=64, + intermediate_size=128, + num_hidden_layers=4, + num_kv_shared_layers=2, + num_attention_heads=2, + num_key_value_heads=2, + head_dim=32, + global_head_dim=64, + layer_types=[ + "sliding_attention", + "full_attention", + "sliding_attention", + "full_attention", + ], + sliding_window=64, + max_position_embeddings=2048, + hidden_size_per_layer_input=16, + vocab_size_per_layer_input=128, + rope_parameters={ + "sliding_attention": {"rope_type": "default", "rope_theta": 10000.0}, + "full_attention": { + "rope_type": "proportional", + "rope_theta": 1000000.0, + "partial_rotary_factor": 0.25, + }, + }, + ) + cfg._attn_implementation = "sdpa" + return cfg + + +class TestFusedAttnSharedKV: + """Regression: ``num_kv_shared_layers > 0`` hit the ``kv_shared_layer_index`` key removed in transformers>=5.8.""" + + def test_shared_kv_forward_backward(self, restore_gemma4_attention): + from transformers.models.gemma4.modeling_gemma4 import Gemma4TextModel + + from axolotl.monkeypatch.models.gemma4.fused_attn import ( + patch_gemma4_fused_attn, + ) + + torch.manual_seed(4) + m = Gemma4TextModel(_build_kv_shared_config()).cuda().to(torch.bfloat16).train() + assert any(layer.self_attn.is_kv_shared_layer for layer in m.layers), ( + "test config must exercise at least one kv-shared layer" + ) + + ids = torch.randint(0, 128, (2, 16), device="cuda") + mask = torch.ones(2, 16, dtype=torch.long, device="cuda") + + with torch.no_grad(): + ref = m(input_ids=ids, attention_mask=mask).last_hidden_state.clone() + + patch_gemma4_fused_attn() + out = m(input_ids=ids, attention_mask=mask).last_hidden_state + out.sum().backward() + + assert out.shape == ref.shape + assert torch.isfinite(out).all() + cos_sim = torch.nn.functional.cosine_similarity( + ref.flatten().float(), out.detach().flatten().float(), dim=0 + ) + assert cos_sim > 0.999, f"shared-kv fused vs stock cosine_sim={cos_sim:.6f}" + + class TestFusedAttnSignature: """Pin the fused forward against the live ``Gemma4TextDecoderLayer.forward`` call shape.""" From e13bf168242631f1b8f11ec9fc279363e0960640 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Thu, 4 Jun 2026 13:14:15 -0400 Subject: [PATCH 1339/1405] =?UTF-8?q?test(ci):=20cut=20CPU=20test=20tail?= =?UTF-8?q?=20=E2=80=94=20drop=20dataset=5Fnum=5Fproc=20to=201,=20split=20?= =?UTF-8?q?builder=20tests=20(#3705)=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Python 3.12 PyTest legs run ~2x slower than 3.14 on the same test set (816s vs 403s) and were tipping over the 30-minute job timeout. Two causes, both in the slow tail: - dataset_num_proc=4 forks 4 dataset workers per .map() on CPU-only runners, each re-importing the torch stack to process a few hundred rows — pure overhead. Lower to 1 in the affected tests (none assert on it or test multiprocessing); results are unchanged. - --dist loadfile pins a whole file to one worker, so the entire builder suite serialized on a single worker at the end. Move shared fixtures to tests/core/conftest.py and split the RL trainer-builder tests into test_builders_rl.py so they run on a separate worker from the SFT/reward builder tests. --- tests/core/conftest.py | 245 +++++++++++++++ tests/core/test_builders.py | 501 +----------------------------- tests/core/test_builders_rl.py | 264 ++++++++++++++++ tests/test_datasets.py | 16 +- tests/test_exact_deduplication.py | 2 +- tests/test_packed_dataset.py | 2 +- 6 files changed, 520 insertions(+), 510 deletions(-) create mode 100644 tests/core/conftest.py create mode 100644 tests/core/test_builders_rl.py diff --git a/tests/core/conftest.py b/tests/core/conftest.py new file mode 100644 index 0000000000..2d25902d81 --- /dev/null +++ b/tests/core/conftest.py @@ -0,0 +1,245 @@ +"""Shared fixtures for axolotl.core.builders trainer-builder tests.""" + +import pytest + +from axolotl.loaders import ModelLoader, load_tokenizer +from axolotl.utils.config import normalize_config +from axolotl.utils.dict import DictDefault +from axolotl.utils.schemas.enums import RLType + + +@pytest.fixture(name="base_cfg") +def fixture_base_cfg(): + """ + Base config with all common arguments between SFT and RLHF + """ + cfg = DictDefault( + { + # Model and tokenizer settings + "base_model": "HuggingFaceTB/SmolLM2-135M-Instruct", + "sequence_len": 2048, + "model_config_type": "llama", # example type + # Basic training settings + "micro_batch_size": 2, + "eval_batch_size": 2, + "num_epochs": 1, + "gradient_accumulation_steps": 1, + "max_steps": 100, + "val_set_size": 0, + # Optimizer settings + "optimizer": "adamw_torch_fused", + "learning_rate": 0.00005, + "weight_decay": 0.01, + "adam_beta1": 0.998, + "adam_beta2": 0.9, + "adam_epsilon": 0.00001, + "max_grad_norm": 1.0, + # LR scheduler settings + "lr_scheduler": "cosine", + "lr_scheduler_kwargs": {"foo": "bar"}, + "warmup_steps": 10, + "warmup_ratio": None, + "cosine_min_lr_ratio": 0.1, + "cosine_constant_lr_ratio": 0.2, + # Checkpointing and saving + "save_steps": 100, + "output_dir": "./model-out", + "save_total_limit": 4, + "save_only_model": False, + # Hardware/performance settings + "gradient_checkpointing": False, + "gradient_checkpointing_kwargs": {"use_reentrant": False}, + "dataloader_num_workers": 1, + "dataloader_pin_memory": True, + "dataloader_prefetch_factor": 2, + "context_parallel_size": 1, + "tensor_parallel_size": 1, + # Dtype + "fp16": False, + "bf16": False, + "tf32": False, + # Logging and evaluation + "logging_steps": 10, + "eval_steps": 50, + "eval_strategy": "steps", + "save_strategy": "steps", + "include_tokens_per_second": True, + # Other common settings + "seed": 42, + "remove_unused_columns": True, + "ddp_timeout": 1800, + "ddp_bucket_cap_mb": 25, + "ddp_broadcast_buffers": False, + "dataset_num_proc": 1, + } + ) + + normalize_config(cfg) + return cfg + + +@pytest.fixture(name="dpo_cfg") +def fixture_dpo_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": RLType.DPO, + "dpo_use_weighting": True, + "dpo_label_smoothing": 0.1, + "beta": 0.1, # DPO beta + "dpo_loss_type": ["sigmoid", "sft"], + "dpo_loss_weights": [1.0, 0.5], + } + ) + return cfg + + +@pytest.fixture(name="orpo_cfg") +def fixture_orpo_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": RLType.ORPO, + "orpo_alpha": 0.1, + "max_prompt_len": 512, + } + ) + return cfg + + +@pytest.fixture(name="kto_cfg") +def fixture_kto_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": RLType.KTO, + "kto_desirable_weight": 1.0, + "kto_undesirable_weight": 1.0, + "max_prompt_len": 512, + } + ) + return cfg + + +@pytest.fixture(name="grpo_cfg") +def fixture_grpo_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": RLType.GRPO, + "trl": DictDefault( + { + "beta": 0.001, + "max_completion_length": 256, + "use_vllm": False, # run on CPU + # "vllm_device": "auto", + # "vllm_gpu_memory_utilization": 0.15, + "num_generations": 4, + "reward_funcs": ["rewards.rand_reward_func"], + } + ), + # Must be evenly divisible by num_generations + "micro_batch_size": 4, + "datasets": [ + { + "path": "openai/gsm8k", + "name": "main", + "split": "train[:1%]", + } + ], + } + ) + return DictDefault(cfg) + + +@pytest.fixture(name="ipo_cfg") +def fixture_ipo_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": RLType.DPO, + "dpo_loss_type": ["ipo"], + "dpo_label_smoothing": 0, + "beta": 0.1, + } + ) + return cfg + + +@pytest.fixture(name="simpo_cfg") +def fixture_simpo_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": RLType.SIMPO, + "rl_beta": 0.2, + "cpo_alpha": 0.9, + "simpo_gamma": 0.4, + } + ) + return cfg + + +@pytest.fixture(name="sft_cfg") +def fixture_sft_cfg(base_cfg): + cfg = base_cfg.copy() + cfg.update( + { + "rl": None, + "sample_packing": False, + "eval_sample_packing": False, + "flash_attention": False, + } + ) + return cfg + + +@pytest.fixture(name="rm_cfg") +def fixture_rm_cfg(sft_cfg): + cfg = sft_cfg.copy() + cfg.update( + DictDefault( + { + "reward_model": True, + "datasets": [ + { + "path": "argilla/distilabel-intel-orca-dpo-pairs", + "type": "bradley_terry.chat_template", + "split": "train[:1%]", + } + ], + } + ) + ) + return cfg + + +@pytest.fixture(name="prm_cfg") +def fixture_prm_cfg(sft_cfg): + cfg = sft_cfg.copy() + cfg.update( + DictDefault( + { + "process_reward_model": True, + "datasets": [ + { + "path": "trl-lib/math_shepherd", + "type": "stepwise_supervised", + "split": "train[:1%]", + } + ], + } + ) + ) + return cfg + + +@pytest.fixture(name="tokenizer") +def fixture_tokenizer(base_cfg): + return load_tokenizer(base_cfg) + + +@pytest.fixture(name="model") +def fixture_model(base_cfg, tokenizer): + model, _ = ModelLoader(base_cfg, tokenizer).load() + return model diff --git a/tests/core/test_builders.py b/tests/core/test_builders.py index 0a4b2ad0bb..286475ea1e 100644 --- a/tests/core/test_builders.py +++ b/tests/core/test_builders.py @@ -1,508 +1,9 @@ -"""Unit tests for axolotl.core.builders""" - -import sys -from pathlib import Path -from unittest.mock import MagicMock, patch +"""Unit tests for axolotl.core.builders SFT and reward-model trainer builders.""" import pytest from axolotl.common.datasets import load_datasets from axolotl.core.builders import HFCausalTrainerBuilder, HFRLTrainerBuilder -from axolotl.loaders import ModelLoader, load_tokenizer -from axolotl.utils.config import normalize_config -from axolotl.utils.data import prepare_preference_datasets -from axolotl.utils.dict import DictDefault -from axolotl.utils.schemas.enums import RLType - -from tests.constants import ALPACA_MESSAGES_CONFIG_REVISION - - -@pytest.fixture(name="base_cfg") -def fixture_base_cfg(): - """ - Base config with all common arguments between SFT and RLHF - """ - cfg = DictDefault( - { - # Model and tokenizer settings - "base_model": "HuggingFaceTB/SmolLM2-135M-Instruct", - "sequence_len": 2048, - "model_config_type": "llama", # example type - # Basic training settings - "micro_batch_size": 2, - "eval_batch_size": 2, - "num_epochs": 1, - "gradient_accumulation_steps": 1, - "max_steps": 100, - "val_set_size": 0, - # Optimizer settings - "optimizer": "adamw_torch_fused", - "learning_rate": 0.00005, - "weight_decay": 0.01, - "adam_beta1": 0.998, - "adam_beta2": 0.9, - "adam_epsilon": 0.00001, - "max_grad_norm": 1.0, - # LR scheduler settings - "lr_scheduler": "cosine", - "lr_scheduler_kwargs": {"foo": "bar"}, - "warmup_steps": 10, - "warmup_ratio": None, - "cosine_min_lr_ratio": 0.1, - "cosine_constant_lr_ratio": 0.2, - # Checkpointing and saving - "save_steps": 100, - "output_dir": "./model-out", - "save_total_limit": 4, - "save_only_model": False, - # Hardware/performance settings - "gradient_checkpointing": False, - "gradient_checkpointing_kwargs": {"use_reentrant": False}, - "dataloader_num_workers": 1, - "dataloader_pin_memory": True, - "dataloader_prefetch_factor": 2, - "context_parallel_size": 1, - "tensor_parallel_size": 1, - # Dtype - "fp16": False, - "bf16": False, - "tf32": False, - # Logging and evaluation - "logging_steps": 10, - "eval_steps": 50, - "eval_strategy": "steps", - "save_strategy": "steps", - "include_tokens_per_second": True, - # Other common settings - "seed": 42, - "remove_unused_columns": True, - "ddp_timeout": 1800, - "ddp_bucket_cap_mb": 25, - "ddp_broadcast_buffers": False, - "dataset_num_proc": 4, - } - ) - - normalize_config(cfg) - return cfg - - -@pytest.fixture(name="dpo_cfg") -def fixture_dpo_cfg(base_cfg): - cfg = base_cfg.copy() - cfg.update( - { - "rl": RLType.DPO, - "dpo_use_weighting": True, - "dpo_label_smoothing": 0.1, - "beta": 0.1, # DPO beta - "dpo_loss_type": ["sigmoid", "sft"], - "dpo_loss_weights": [1.0, 0.5], - } - ) - return cfg - - -@pytest.fixture(name="orpo_cfg") -def fixture_orpo_cfg(base_cfg): - cfg = base_cfg.copy() - cfg.update( - { - "rl": RLType.ORPO, - "orpo_alpha": 0.1, - "max_prompt_len": 512, - } - ) - return cfg - - -@pytest.fixture(name="kto_cfg") -def fixture_kto_cfg(base_cfg): - cfg = base_cfg.copy() - cfg.update( - { - "rl": RLType.KTO, - "kto_desirable_weight": 1.0, - "kto_undesirable_weight": 1.0, - "max_prompt_len": 512, - } - ) - return cfg - - -@pytest.fixture(name="grpo_cfg") -def fixture_grpo_cfg(base_cfg): - cfg = base_cfg.copy() - cfg.update( - { - "rl": RLType.GRPO, - "trl": DictDefault( - { - "beta": 0.001, - "max_completion_length": 256, - "use_vllm": False, # run on CPU - # "vllm_device": "auto", - # "vllm_gpu_memory_utilization": 0.15, - "num_generations": 4, - "reward_funcs": ["rewards.rand_reward_func"], - } - ), - # Must be evenly divisible by num_generations - "micro_batch_size": 4, - "datasets": [ - { - "path": "openai/gsm8k", - "name": "main", - "split": "train[:1%]", - } - ], - } - ) - return DictDefault(cfg) - - -@pytest.fixture(name="ipo_cfg") -def fixture_ipo_cfg(base_cfg): - cfg = base_cfg.copy() - cfg.update( - { - "rl": RLType.DPO, - "dpo_loss_type": ["ipo"], - "dpo_label_smoothing": 0, - "beta": 0.1, - } - ) - return cfg - - -@pytest.fixture(name="simpo_cfg") -def fixture_simpo_cfg(base_cfg): - cfg = base_cfg.copy() - cfg.update( - { - "rl": RLType.SIMPO, - "rl_beta": 0.2, - "cpo_alpha": 0.9, - "simpo_gamma": 0.4, - } - ) - return cfg - - -@pytest.fixture(name="sft_cfg") -def fixture_sft_cfg(base_cfg): - cfg = base_cfg.copy() - cfg.update( - { - "rl": None, - "sample_packing": False, - "eval_sample_packing": False, - "flash_attention": False, - } - ) - return cfg - - -@pytest.fixture(name="rm_cfg") -def fixture_rm_cfg(sft_cfg): - cfg = sft_cfg.copy() - cfg.update( - DictDefault( - { - "reward_model": True, - "datasets": [ - { - "path": "argilla/distilabel-intel-orca-dpo-pairs", - "type": "bradley_terry.chat_template", - "split": "train[:1%]", - } - ], - } - ) - ) - return cfg - - -@pytest.fixture(name="prm_cfg") -def fixture_prm_cfg(sft_cfg): - cfg = sft_cfg.copy() - cfg.update( - DictDefault( - { - "process_reward_model": True, - "datasets": [ - { - "path": "trl-lib/math_shepherd", - "type": "stepwise_supervised", - "split": "train[:1%]", - } - ], - } - ) - ) - return cfg - - -@pytest.fixture(name="tokenizer") -def fixture_tokenizer(base_cfg): - return load_tokenizer(base_cfg) - - -@pytest.fixture(name="model") -def fixture_model(base_cfg, tokenizer): - model, _ = ModelLoader(base_cfg, tokenizer).load() - return model - - -class TestHFRLTrainerBuilder: - """ - TestCase class for RLHF trainer builders - """ - - def _test_common_training_arguments(self, training_arguments, rl: str): - """Helper to test common arguments across all variants""" - # Basic training settings - if rl == "grpo": - # grpo_cfg's micro_batch_size is diff from others - assert training_arguments.per_device_train_batch_size == 4 - else: - assert training_arguments.per_device_train_batch_size == 2 - assert training_arguments.gradient_accumulation_steps == 1 - assert training_arguments.max_steps == 100 - - # Optimizer settings - assert training_arguments.learning_rate == 0.00005 - assert training_arguments.weight_decay == 0.01 - assert training_arguments.adam_beta1 == 0.998 - assert training_arguments.adam_beta2 == 0.9 - assert training_arguments.adam_epsilon == 0.00001 - assert training_arguments.max_grad_norm == 1.0 - - # LR scheduler settings - assert training_arguments.lr_scheduler_type == "cosine" - assert training_arguments.warmup_steps == 10 - assert training_arguments.cosine_min_lr_ratio == 0.1 - assert training_arguments.cosine_constant_lr_ratio == 0.2 - - # Other settings - assert training_arguments.dataloader_num_workers == 1 - assert training_arguments.dataloader_pin_memory is True - - # TODO(wing): restore once trl releases 0.22.0 - # assert training_arguments.gradient_checkpointing is True - - def test_dpo_training_arguments(self, dpo_cfg, model, tokenizer): - dpo_cfg["precompute_ref_log_probs"] = True - builder = HFRLTrainerBuilder(dpo_cfg, model, tokenizer) - training_arguments, _ = builder._build_training_arguments(100) - - self._test_common_training_arguments(training_arguments, rl=dpo_cfg.rl) - # DPO specific - assert training_arguments.beta == 0.1 - assert hasattr(training_arguments, "use_weighting") - assert training_arguments.use_weighting is True - assert training_arguments.label_smoothing == 0.1 - assert training_arguments.precompute_ref_log_probs is True - assert training_arguments.loss_type == ["sigmoid", "sft"] - assert training_arguments.loss_weights == [1.0, 0.5] - - def test_orpo_training_arguments(self, orpo_cfg, model, tokenizer): - builder = HFRLTrainerBuilder(orpo_cfg, model, tokenizer) - training_arguments, _ = builder._build_training_arguments(100) - - self._test_common_training_arguments(training_arguments, rl=orpo_cfg.rl) - # ORPO specific - assert training_arguments.beta == 0.1 # maps from orpo_alpha - - def test_kto_training_arguments(self, kto_cfg, model, tokenizer): - builder = HFRLTrainerBuilder(kto_cfg, model, tokenizer) - training_arguments, _ = builder._build_training_arguments(100) - - self._test_common_training_arguments(training_arguments, rl=kto_cfg.rl) - # KTO specific - assert training_arguments.desirable_weight == 1.0 - assert training_arguments.undesirable_weight == 1.0 - - def _write_rewards_file(self, rewards_dir: Path): - """ - Writes reward function to local tmp path to be loaded on trainer building - """ - # Create rewards.py in a directory we can import from - rewards_dir.mkdir() - rewards_file = rewards_dir / "rewards.py" - rewards_file.write_text( - """import random -def rand_reward_func(prompts, completions) -> list[float]: - return [random.uniform(0, 1) for _ in completions] -""" - ) - - def test_grpo_training_arguments(self, grpo_cfg, model, tokenizer, tmp_path): - rewards_dir = tmp_path / "rewards_test" - self._write_rewards_file(rewards_dir) - - # Add the directory to Python path so we can import the module - sys.path.insert(0, str(rewards_dir)) - - try: - builder = HFRLTrainerBuilder(grpo_cfg, model, tokenizer) - training_arguments, _ = builder._build_training_arguments(100) - builder.train_dataset = MagicMock() - - self._test_common_training_arguments(training_arguments, rl=grpo_cfg.rl) - # GRPO specific - assert training_arguments.beta == 0.001 - assert training_arguments.max_completion_length == 256 - assert training_arguments.use_vllm is False - # assert training_arguments.vllm_device == "auto" - # assert training_arguments.vllm_gpu_memory_utilization == 0.15 - assert training_arguments.num_generations == 4 - - # Test trainer creation to verify reward_funcs - trainer = builder.build(100) - - # Verify reward functions are properly loaded - assert len(trainer.reward_funcs) == 1 - assert trainer.reward_funcs[0].__module__ == "rewards" - assert trainer.reward_funcs[0].__name__ == "rand_reward_func" - finally: - # remove imported module from path - if str(rewards_dir) in sys.path: - sys.path.remove(str(rewards_dir)) - - def test_ipo_training_arguments(self, ipo_cfg, model, tokenizer): - builder = HFRLTrainerBuilder(ipo_cfg, model, tokenizer) - training_arguments, _ = builder._build_training_arguments(100) - - self._test_common_training_arguments(training_arguments, rl=ipo_cfg.rl) - # IPO specific - assert training_arguments.beta == 0.1 - assert training_arguments.loss_type == ["ipo"] - assert training_arguments.label_smoothing == 0 - - def test_simpo_training_arguments(self, simpo_cfg, model, tokenizer): - builder = HFRLTrainerBuilder(simpo_cfg, model, tokenizer) - training_arguments, _ = builder._build_training_arguments(100) - - self._test_common_training_arguments(training_arguments, rl=simpo_cfg.rl) - # SIMPO specific - assert training_arguments.beta == 0.2 - assert training_arguments.cpo_alpha == 0.9 - assert training_arguments.simpo_gamma == 0.4 - - @pytest.mark.parametrize( - ("cfg_string", "dataset_name"), - [ - ( - "dpo_cfg", - "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", - ), - ( - "ipo_cfg", - "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", - ), - ( - "grpo_cfg", - "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", - ), - ("orpo_cfg", None), # don't use fixture for orpo to use smaller split - ("kto_cfg", None), # no fixture for kto - # ( - # "simpo_cfg", - # "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", - # ), - ], - ) - def test_custom_optimizer_cls_and_kwargs( - self, - request, - cfg_string, - dataset_name, - tmp_path, - model, - tokenizer, - ): - cfg = request.getfixturevalue(cfg_string) - - builder = HFRLTrainerBuilder(cfg, model, tokenizer) - cfg["optimizer"] = "muon" - - if cfg_string in ["dpo_cfg", "ipo_cfg", "grpo_cfg", "simpo_cfg"]: - cfg["datasets"] = [DictDefault(ALPACA_MESSAGES_CONFIG_REVISION)] - elif cfg_string == "kto_cfg": - cfg["datasets"] = [ - DictDefault( - { - "path": "argilla/ultrafeedback-binarized-preferences-cleaned-kto", - "type": "llama3.ultra", - "split": "train[:1%]", - } - ) - ] - elif cfg_string == "orpo_cfg": - cfg["datasets"] = [ - DictDefault( - { - "path": "argilla/ultrafeedback-binarized-preferences-cleaned", - "type": "chat_template.argilla", - "split": "train[:1%]", - } - ) - ] - else: - raise ValueError(f"Unhandled cfg_string: {cfg_string}") - cfg["dataset_num_proc"] = 4 - - if cfg_string == "grpo_cfg": - rewards_dir = tmp_path / "rewards_test" - self._write_rewards_file(rewards_dir) - - # Add the directory to Python path so we can import the module - sys.path.insert(0, str(rewards_dir)) - - try: - # Only use mock for the commented out configs - if dataset_name is not None: - with patch( - "axolotl.utils.data.rl.load_dataset_with_config" - ) as mock_load_dataset: - mock_load_dataset.return_value = request.getfixturevalue( - dataset_name - ) - train_dataset, eval_dataset = prepare_preference_datasets( - cfg, tokenizer - ) - else: - # Load actual datasets for orpo_cfg and kto_cfg - train_dataset, eval_dataset = prepare_preference_datasets( - cfg, tokenizer - ) - - builder.train_dataset = train_dataset - builder.eval_dataset = eval_dataset - - trainer = builder.build(100) - - assert trainer.optimizer_cls_and_kwargs is not None - - from axolotl.contribs.mit.muon import MuonOptimizerFactory - from axolotl.contribs.mit.muon.muon import Muon - - optimizer_cls, optimizer_kwargs = trainer.optimizer_cls_and_kwargs - assert optimizer_cls is MuonOptimizerFactory - assert optimizer_kwargs["lr"] == 0.00005 - assert optimizer_kwargs["weight_decay"] == 0.01 - assert optimizer_kwargs["betas"] == (0.998, 0.9) - assert optimizer_kwargs["eps"] == 0.00001 - - # Ensure optimizer is created with correct class - optim = trainer.create_optimizer() - assert isinstance(optim, Muon) - - finally: - # remove imported module from path - if cfg_string == "grpo_cfg" and str(rewards_dir) in sys.path: - sys.path.remove(str(rewards_dir)) class TestHFCausalTrainerBuilder: diff --git a/tests/core/test_builders_rl.py b/tests/core/test_builders_rl.py new file mode 100644 index 0000000000..435e7eb411 --- /dev/null +++ b/tests/core/test_builders_rl.py @@ -0,0 +1,264 @@ +"""Unit tests for axolotl.core.builders RL (preference/GRPO) trainer builders.""" + +import sys +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from axolotl.core.builders import HFRLTrainerBuilder +from axolotl.utils.data import prepare_preference_datasets +from axolotl.utils.dict import DictDefault + +from tests.constants import ALPACA_MESSAGES_CONFIG_REVISION + + +class TestHFRLTrainerBuilder: + """ + TestCase class for RLHF trainer builders + """ + + def _test_common_training_arguments(self, training_arguments, rl: str): + """Helper to test common arguments across all variants""" + # Basic training settings + if rl == "grpo": + # grpo_cfg's micro_batch_size is diff from others + assert training_arguments.per_device_train_batch_size == 4 + else: + assert training_arguments.per_device_train_batch_size == 2 + assert training_arguments.gradient_accumulation_steps == 1 + assert training_arguments.max_steps == 100 + + # Optimizer settings + assert training_arguments.learning_rate == 0.00005 + assert training_arguments.weight_decay == 0.01 + assert training_arguments.adam_beta1 == 0.998 + assert training_arguments.adam_beta2 == 0.9 + assert training_arguments.adam_epsilon == 0.00001 + assert training_arguments.max_grad_norm == 1.0 + + # LR scheduler settings + assert training_arguments.lr_scheduler_type == "cosine" + assert training_arguments.warmup_steps == 10 + assert training_arguments.cosine_min_lr_ratio == 0.1 + assert training_arguments.cosine_constant_lr_ratio == 0.2 + + # Other settings + assert training_arguments.dataloader_num_workers == 1 + assert training_arguments.dataloader_pin_memory is True + + # TODO(wing): restore once trl releases 0.22.0 + # assert training_arguments.gradient_checkpointing is True + + def test_dpo_training_arguments(self, dpo_cfg, model, tokenizer): + dpo_cfg["precompute_ref_log_probs"] = True + builder = HFRLTrainerBuilder(dpo_cfg, model, tokenizer) + training_arguments, _ = builder._build_training_arguments(100) + + self._test_common_training_arguments(training_arguments, rl=dpo_cfg.rl) + # DPO specific + assert training_arguments.beta == 0.1 + assert hasattr(training_arguments, "use_weighting") + assert training_arguments.use_weighting is True + assert training_arguments.label_smoothing == 0.1 + assert training_arguments.precompute_ref_log_probs is True + assert training_arguments.loss_type == ["sigmoid", "sft"] + assert training_arguments.loss_weights == [1.0, 0.5] + + def test_orpo_training_arguments(self, orpo_cfg, model, tokenizer): + builder = HFRLTrainerBuilder(orpo_cfg, model, tokenizer) + training_arguments, _ = builder._build_training_arguments(100) + + self._test_common_training_arguments(training_arguments, rl=orpo_cfg.rl) + # ORPO specific + assert training_arguments.beta == 0.1 # maps from orpo_alpha + + def test_kto_training_arguments(self, kto_cfg, model, tokenizer): + builder = HFRLTrainerBuilder(kto_cfg, model, tokenizer) + training_arguments, _ = builder._build_training_arguments(100) + + self._test_common_training_arguments(training_arguments, rl=kto_cfg.rl) + # KTO specific + assert training_arguments.desirable_weight == 1.0 + assert training_arguments.undesirable_weight == 1.0 + + def _write_rewards_file(self, rewards_dir: Path): + """ + Writes reward function to local tmp path to be loaded on trainer building + """ + # Create rewards.py in a directory we can import from + rewards_dir.mkdir() + rewards_file = rewards_dir / "rewards.py" + rewards_file.write_text( + """import random +def rand_reward_func(prompts, completions) -> list[float]: + return [random.uniform(0, 1) for _ in completions] +""" + ) + + def test_grpo_training_arguments(self, grpo_cfg, model, tokenizer, tmp_path): + rewards_dir = tmp_path / "rewards_test" + self._write_rewards_file(rewards_dir) + + # Add the directory to Python path so we can import the module + sys.path.insert(0, str(rewards_dir)) + + try: + builder = HFRLTrainerBuilder(grpo_cfg, model, tokenizer) + training_arguments, _ = builder._build_training_arguments(100) + builder.train_dataset = MagicMock() + + self._test_common_training_arguments(training_arguments, rl=grpo_cfg.rl) + # GRPO specific + assert training_arguments.beta == 0.001 + assert training_arguments.max_completion_length == 256 + assert training_arguments.use_vllm is False + # assert training_arguments.vllm_device == "auto" + # assert training_arguments.vllm_gpu_memory_utilization == 0.15 + assert training_arguments.num_generations == 4 + + # Test trainer creation to verify reward_funcs + trainer = builder.build(100) + + # Verify reward functions are properly loaded + assert len(trainer.reward_funcs) == 1 + assert trainer.reward_funcs[0].__module__ == "rewards" + assert trainer.reward_funcs[0].__name__ == "rand_reward_func" + finally: + # remove imported module from path + if str(rewards_dir) in sys.path: + sys.path.remove(str(rewards_dir)) + + def test_ipo_training_arguments(self, ipo_cfg, model, tokenizer): + builder = HFRLTrainerBuilder(ipo_cfg, model, tokenizer) + training_arguments, _ = builder._build_training_arguments(100) + + self._test_common_training_arguments(training_arguments, rl=ipo_cfg.rl) + # IPO specific + assert training_arguments.beta == 0.1 + assert training_arguments.loss_type == ["ipo"] + assert training_arguments.label_smoothing == 0 + + def test_simpo_training_arguments(self, simpo_cfg, model, tokenizer): + builder = HFRLTrainerBuilder(simpo_cfg, model, tokenizer) + training_arguments, _ = builder._build_training_arguments(100) + + self._test_common_training_arguments(training_arguments, rl=simpo_cfg.rl) + # SIMPO specific + assert training_arguments.beta == 0.2 + assert training_arguments.cpo_alpha == 0.9 + assert training_arguments.simpo_gamma == 0.4 + + @pytest.mark.parametrize( + ("cfg_string", "dataset_name"), + [ + ( + "dpo_cfg", + "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", + ), + ( + "ipo_cfg", + "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", + ), + ( + "grpo_cfg", + "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", + ), + ("orpo_cfg", None), # don't use fixture for orpo to use smaller split + ("kto_cfg", None), # no fixture for kto + # ( + # "simpo_cfg", + # "dataset_fozziethebeat_alpaca_messages_2k_dpo_test_rev_ea82cff", + # ), + ], + ) + def test_custom_optimizer_cls_and_kwargs( + self, + request, + cfg_string, + dataset_name, + tmp_path, + model, + tokenizer, + ): + cfg = request.getfixturevalue(cfg_string) + + builder = HFRLTrainerBuilder(cfg, model, tokenizer) + cfg["optimizer"] = "muon" + + if cfg_string in ["dpo_cfg", "ipo_cfg", "grpo_cfg", "simpo_cfg"]: + cfg["datasets"] = [DictDefault(ALPACA_MESSAGES_CONFIG_REVISION)] + elif cfg_string == "kto_cfg": + cfg["datasets"] = [ + DictDefault( + { + "path": "argilla/ultrafeedback-binarized-preferences-cleaned-kto", + "type": "llama3.ultra", + "split": "train[:1%]", + } + ) + ] + elif cfg_string == "orpo_cfg": + cfg["datasets"] = [ + DictDefault( + { + "path": "argilla/ultrafeedback-binarized-preferences-cleaned", + "type": "chat_template.argilla", + "split": "train[:1%]", + } + ) + ] + else: + raise ValueError(f"Unhandled cfg_string: {cfg_string}") + cfg["dataset_num_proc"] = 1 + + if cfg_string == "grpo_cfg": + rewards_dir = tmp_path / "rewards_test" + self._write_rewards_file(rewards_dir) + + # Add the directory to Python path so we can import the module + sys.path.insert(0, str(rewards_dir)) + + try: + # Only use mock for the commented out configs + if dataset_name is not None: + with patch( + "axolotl.utils.data.rl.load_dataset_with_config" + ) as mock_load_dataset: + mock_load_dataset.return_value = request.getfixturevalue( + dataset_name + ) + train_dataset, eval_dataset = prepare_preference_datasets( + cfg, tokenizer + ) + else: + # Load actual datasets for orpo_cfg and kto_cfg + train_dataset, eval_dataset = prepare_preference_datasets( + cfg, tokenizer + ) + + builder.train_dataset = train_dataset + builder.eval_dataset = eval_dataset + + trainer = builder.build(100) + + assert trainer.optimizer_cls_and_kwargs is not None + + from axolotl.contribs.mit.muon import MuonOptimizerFactory + from axolotl.contribs.mit.muon.muon import Muon + + optimizer_cls, optimizer_kwargs = trainer.optimizer_cls_and_kwargs + assert optimizer_cls is MuonOptimizerFactory + assert optimizer_kwargs["lr"] == 0.00005 + assert optimizer_kwargs["weight_decay"] == 0.01 + assert optimizer_kwargs["betas"] == (0.998, 0.9) + assert optimizer_kwargs["eps"] == 0.00001 + + # Ensure optimizer is created with correct class + optim = trainer.create_optimizer() + assert isinstance(optim, Muon) + + finally: + # remove imported module from path + if cfg_string == "grpo_cfg" and str(rewards_dir) in sys.path: + sys.path.remove(str(rewards_dir)) diff --git a/tests/test_datasets.py b/tests/test_datasets.py index bdb795e136..43f1ba1c7a 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -143,7 +143,7 @@ def test_load_from_save_to_disk(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], - "dataset_num_proc": 4, + "dataset_num_proc": 1, } ) @@ -182,7 +182,7 @@ def test_load_from_dir_of_parquet(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], - "dataset_num_proc": 4, + "dataset_num_proc": 1, } ) @@ -221,7 +221,7 @@ def test_load_from_dir_of_json(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], - "dataset_num_proc": 4, + "dataset_num_proc": 1, } ) @@ -254,7 +254,7 @@ def test_load_from_single_parquet(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], - "dataset_num_proc": 4, + "dataset_num_proc": 1, } ) @@ -287,7 +287,7 @@ def test_load_from_single_json(self, tokenizer, dataset_fixture): "type": "alpaca", }, ], - "dataset_num_proc": 4, + "dataset_num_proc": 1, } ) @@ -372,7 +372,7 @@ def test_load_hub_with_revision_with_dpo( "rl": "dpo", "chat_template": "llama3", "datasets": [ALPACA_MESSAGES_CONFIG_REVISION], - "dataset_num_proc": 4, + "dataset_num_proc": 1, } ) @@ -473,7 +473,7 @@ def test_loading_local_dataset_folder(self, tokenizer): "type": "alpaca", }, ], - "dataset_num_proc": 4, + "dataset_num_proc": 1, } ) @@ -539,7 +539,7 @@ def test_load_dataset_with_str_json_data(self, tokenizer): "message_field_content": "content", }, ], - "dataset_num_proc": 4, + "dataset_num_proc": 1, } ) diff --git a/tests/test_exact_deduplication.py b/tests/test_exact_deduplication.py index a519db525b..387c77ad4c 100644 --- a/tests/test_exact_deduplication.py +++ b/tests/test_exact_deduplication.py @@ -210,7 +210,7 @@ def cfg(self): ALPACA_MESSAGES_CONFIG_REVISION, ALPACA_MESSAGES_CONFIG_REVISION, ], - "dataset_num_proc": 4, + "dataset_num_proc": 1, } ) yield fixture diff --git a/tests/test_packed_dataset.py b/tests/test_packed_dataset.py index 953d523af4..cecd82a814 100644 --- a/tests/test_packed_dataset.py +++ b/tests/test_packed_dataset.py @@ -55,7 +55,7 @@ def test_lora_packing(self, temp_dir): "type": "alpaca", }, ], - "dataset_num_proc": 4, + "dataset_num_proc": 1, "num_epochs": 1, "max_steps": 20, "save_steps": 10, From 69f6d8bab251026470163c887c04eacb6f1a9a81 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Fri, 5 Jun 2026 14:33:44 -0400 Subject: [PATCH 1340/1405] lora kernel memory improvement (#3704) * perf(lora-kernels): fuse matmul_lora add via in-place addmm_ matmul_lora computed out += s * X_lora @ A @ B, which materializes a full [M, out_features] LoRA temporary before the add. Replace with out.addmm_(X_lora @ A, B, alpha=s): the X_lora @ A intermediate is only [M, rank], no [M, out_features] temp, and the add is fp32-accumulated. Same math, slightly more accurate (fp32 accumulate vs the prior fp16 allocate-then-add), full cuBLAS speed, all hardware. test_matmul_lora switches to an fp32 reference since the kernel no longer mirrors the old fp16 intermediate rounding. * perf(lora-kernels): fuse LoRA_MLP backward dX add via addmm_ The base-path input gradient did dX += grad_gate @ gate_weight_deq, materializing a full [M, hidden] temp before the add. Replace with dX.addmm_(grad_gate, gate_weight_deq): same math, fp32-accumulated, no temp. Halves the peak memory of that step (the temp equals dX in size) at neutral speed; mirrors the matmul_lora forward fix. * perf(sonicmoe-lora): fuse expert weight materialization via baddbmm MoELoRAMaterialize.forward built W_eff = base_weight + scaling * bmm(B,A), allocating a full [E, dim1, dim2] bmm temp plus a second scaling-scaled copy. torch.baddbmm(base_weight, B_3d, A_3d, alpha=scaling) computes it in one fused op. Same math, fp32-accumulated; backward is unchanged (it saves lora_A/lora_B and recomputes, never referencing the delta). Measured (bf16, E=8 4096x4096 r=16): -67% peak memory (805 -> 268 MB) and 1.46x faster (1.07 -> 0.73 ms) on the per-forward weight-materialization path. gradcheck (fp64) still passes. * split up pytests to get long tail of test durations * fix(tests): make HF-throttled tests offline-safe via prefetch + cache Several tests fetched datasets/tokenizers by Hub path at runtime. When the HF Hub throttles (403/429), huggingface_hub retries with backoff (90s-7min per call), inflating the CPU CI legs past the 30-min timeout and failing ~10 tests with HfHubHTTPError. The failing tests simply ran online and weren't covered by the existing `enable_hf_offline` mechanism. - Add session prefetch fixtures for the previously-uncached repos: mistralai/Magistral-Small-2506, Devstral-Small-2505/2507 (tokenizer files only, incl. tekken.json/params.json which `*token*` misses), and trl-lib/math_shepherd. - Decorate the offending tests with `@enable_hf_offline`, and offline-wrap the shared `model`/`tokenizer` builder fixtures (the decorator can't cover pytest fixture setup, where the slow loads happen). - Swap the ebft multi-turn test off Qwen2-0.5B-Instruct (uncached) to the already-prefetched SmolLM2-135M-Instruct; the assertions are chat-template structural, not tokenizer-specific. - Mistral tokenizers can't use offline mode (mistral-common doesn't catch OfflineModeIsEnabled), but it does fall back to local files on a throttle HTTPError. Pass revision=None in the fixtures so that fallback resolves the cached snapshot via refs/main instead of a nonexistent snapshots/main dir. - Fix HFMistralTokenizer.from_pretrained passing cache_dir=str(None)="None", which downloaded tokenizers into a stray ./None dir and bypassed the HF cache (and the prefetch). * fix(tests): prefetch Hub-path datasets via load_dataset for offline loading The previous commit added @enable_hf_offline to dataset-loading tests, but they failed in CI with `ConnectionError: Couldn't reach 'X' on the Hub (OfflineModeIsEnabled)`. snapshot_download only populates the *hub* cache; an offline `load_dataset` resolves via the *datasets* cache, which stays empty. (Locally the tests passed only because that cache was already warm.) Add load_dataset_w_retry (cache-first, else online) and use it for the five datasets the offline tests load by Hub path: mhenrichsen/alpaca_2k_test, argilla/distilabel-intel-orca-dpo-pairs, argilla/ultrafeedback-binarized-preferences-cleaned(-kto), and trl-lib/math_shepherd. Verified by clearing the datasets cache and confirming the autouse fixtures rebuild it and the offline tests pass. * Revert "fix(tests): prefetch Hub-path datasets via load_dataset for offline loading" This reverts commit d95991e6160aeefa8ab0221ef7d33b492d0ea254. * Revert "fix(tests): make HF-throttled tests offline-safe via prefetch + cache" This reverts commit d25b7a178df17149ef9274f84cc989fa0439d6d6. --- .github/workflows/tests.yml | 27 ++++++++++++++----- .../kernels/libs/sonicmoe/lora.py | 7 +++-- src/axolotl/kernels/lora.py | 4 +-- tests/e2e/kernels/test_lora.py | 13 ++++++--- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 888c3e39b9..2d724b644c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,6 +72,12 @@ jobs: exclude: - python_version: "3.14" pytorch_version: "2.9.1" + - python_version: "3.14" + pytorch_version: "2.10.0" + - python_version: "3.12" + pytorch_version: "2.11.0" + - python_version: "3.12" + pytorch_version: "2.12.0" timeout-minutes: 30 steps: @@ -126,14 +132,12 @@ jobs: - name: Run tests run: | - df -h - pytest -v --durations=10 -n4 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ --ignore=tests/monkeypatch/ tests/ --cov=axolotl --cov-report=xml - df -h + pytest -v --durations=10 -n4 --dist loadfile --ignore=tests/utils/ --ignore=tests/integrations/ --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ --ignore=tests/monkeypatch/ tests/ --cov=axolotl --cov-report=xml pytest -v --durations=10 tests/monkeypatch/ --cov=axolotl --cov-append --cov-report=xml - df -h pytest -v --durations=10 tests/patched/ --cov=axolotl --cov-append --cov-report=xml - df -h pytest -v --durations=10 tests/cli/ --cov=axolotl --cov-append --cov-report=xml + pytest -v --durations=10 tests/utils/ --cov=axolotl --cov-append --cov-report=xml + pytest -v --durations=10 tests/integrations/ --cov=axolotl --cov-append --cov-report=xml - name: Show HF cache run: hf cache ls @@ -159,6 +163,12 @@ jobs: exclude: - python_version: "3.14" pytorch_version: "2.9.1" + - python_version: "3.14" + pytorch_version: "2.10.0" + - python_version: "3.12" + pytorch_version: "2.11.0" + - python_version: "3.12" + pytorch_version: "2.12.0" timeout-minutes: 30 steps: @@ -221,9 +231,12 @@ jobs: - name: Run tests run: | - pytest -v --durations=10 -n4 --dist loadfile --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ --ignore=tests/monkeypatch/ tests/ --cov=axolotl --cov-report=xml + pytest -v --durations=10 -n4 --dist loadfile --ignore=tests/utils/ --ignore=tests/integrations/ --ignore=tests/e2e/ --ignore=tests/patched/ --ignore=tests/cli/ --ignore=tests/monkeypatch/ tests/ --cov=axolotl --cov-report=xml pytest -v --durations=10 tests/monkeypatch/ --cov=axolotl --cov-append --cov-report=xml - pytest -v --durations=10 tests/cli/ + pytest -v --durations=10 tests/patched/ --cov=axolotl --cov-append --cov-report=xml + pytest -v --durations=10 tests/cli/ --cov=axolotl --cov-append --cov-report=xml + pytest -v --durations=10 tests/utils/ --cov=axolotl --cov-append --cov-report=xml + pytest -v --durations=10 tests/integrations/ --cov=axolotl --cov-append --cov-report=xml - name: Show HF cache run: hf cache ls diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/lora.py b/src/axolotl/integrations/kernels/libs/sonicmoe/lora.py index 1fe08828cb..9216c0c777 100644 --- a/src/axolotl/integrations/kernels/libs/sonicmoe/lora.py +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/lora.py @@ -128,10 +128,9 @@ def forward( A_3d = lora_A.reshape(E, r, dim2) B_3d = lora_B.reshape(dim1, r, E).permute(2, 0, 1).contiguous() # [E, dim1, r] - # Batched matmul: [E, dim1, r] @ [E, r, dim2] = [E, dim1, dim2] - delta = torch.bmm(B_3d, A_3d) - - W_eff = base_weight + scaling * delta + # Fuse base_weight + scaling * (B_3d @ A_3d) into one op: avoids both the + # [E, dim1, dim2] bmm temp and the scaling-scaled copy of it. + W_eff = torch.baddbmm(base_weight, B_3d, A_3d, alpha=scaling) ctx.save_for_backward(lora_A, lora_B) ctx.scaling = scaling diff --git a/src/axolotl/kernels/lora.py b/src/axolotl/kernels/lora.py index 79ac993c14..c3ccad6044 100644 --- a/src/axolotl/kernels/lora.py +++ b/src/axolotl/kernels/lora.py @@ -270,7 +270,7 @@ def matmul_lora( if A is not None: X_lora = X_drop if X_drop is not None else X A, B = A.t().to(dtype), B.t().to(dtype) # type: ignore[union-attr] - out += s * X_lora @ A @ B + out.addmm_(X_lora @ A, B, alpha=s) if lora_bias is not None: out += s * lora_bias @@ -646,7 +646,7 @@ def backward( del up_weight_deq gate_weight_deq = dequantize(gate_weight, gate_quant) - dX += grad_gate @ gate_weight_deq + dX.addmm_(grad_gate, gate_weight_deq) del gate_weight_deq # LoRA path: reuse grad_B_up and grad_B_gate from above diff --git a/tests/e2e/kernels/test_lora.py b/tests/e2e/kernels/test_lora.py index 10850bdc85..0c2551916b 100644 --- a/tests/e2e/kernels/test_lora.py +++ b/tests/e2e/kernels/test_lora.py @@ -144,11 +144,16 @@ def test_matmul_lora(sample_tensors): expected1 = matmul + b assert torch.allclose(out1, expected1, rtol=1e-3) - # Test with LoRA + # Test with LoRA. matmul_lora fuses the add via in-place addmm_ (fp32 accumulate, + # no [M, out] LoRA temp), so compare against an fp32 reference rather than a fp16 + # one whose intermediate rounding the kernel intentionally no longer mirrors. out2 = matmul_lora(X, W, b, None, A, B, scale) - lora_term = scale * torch.matmul(torch.matmul(X, A.t()), B.t()) - expected2 = matmul + lora_term + b - assert torch.allclose(out2, expected2, rtol=1e-3) + expected2 = ( + X.float() @ W.float().t() + + scale * (X.float() @ A.float().t()) @ B.float().t() + + b.float() + ) + assert torch.allclose(out2.float(), expected2, rtol=2e-3, atol=2e-2) # Test 3D input reshaping X_3d = X.clone() From edcbeb7888b5e94bad2a2379b92fadc25df0fdfc Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 7 Jun 2026 02:36:10 -0400 Subject: [PATCH 1341/1405] perf(scattermoe-lora): grouped-Gram dA/dB + sync-free dX_lora for large-E MoEs (#3712) * perf(scattermoe-lora): grouped-Gram dA/dB + sync-free dX_lora for large-E MoEs The dA/dB split kernel recomputes XA=X@A^T (resp. YB=dY@B) in registers once per output dim-block to avoid materializing it; and the non-fused dX_lora path is a Python per-expert loop with an expert_offsets[e].item() device sync per expert. Both scale badly with the expert count -- and modern MoEs run E>=128. The intermediates are rank-sized ([M*k, R], R~16), so materializing them once is near-free (the split kernel saves only ~2-8 MB across these shapes -- smaller than the dA/dB gradient it produces). Backport the multi-adapter wins to the single-adapter path: - grouped_gram.py: _grouped_gram_kernel + grouped_lora_weight_grads -- dA/dB as recompute-free grouped Gram products over precomputed XA/YB (bit-identical to group_bwd_lora, err 0.0). - ScatterMoELoRA.backward non-fused dA/dB: compute YB, XA once, then grouped-Gram; YB is reused by the non-fused dX_lora. - _compute_lora_input_grad: two grouped scatter2scatter GEMMs (sync-free, single launch each) when routing ids are given, reusing YB; the per-expert loop is kept as a fallback but with one host sync for the whole offset array, not O(E). Before/after, full ScatterMoELoRA fwd+bwd (E>=128, M=4096): fused-dX path (production, #dA/dB only): 1.08-1.15x non-fused path (#dA/dB + #dX_lora): up to 2.2x (Qwen3-MoE, DeepSeek) at +5..30 MB peak (the XA/YB buffers). The dA/dB kernel alone is 2-17x faster. MXFP4 unaffected: dX takes the is_mx branch (scatter2scatter_lora_dX_mx) and never the rewritten non-fused path; dA/dB never touch the (frozen) base, so LoRA grads are bit-identical for an MX base vs its bf16 dequantization at a workload above the fuse-gather threshold (new regression test). #1 (sonicmoe materialize) is already on main via baddbmm. * chore: lint * chore: lint --- .../scattermoe_lora/kernels/grouped_gram.py | 210 ++++++++++++++++++ .../scattermoe_lora/parallel_linear_lora.py | 127 ++++++++--- .../test_mx_lora_grouped_gram.py | 77 +++++++ 3 files changed, 383 insertions(+), 31 deletions(-) create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/grouped_gram.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_mx_lora_grouped_gram.py diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/grouped_gram.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/grouped_gram.py new file mode 100644 index 0000000000..c0b7f0f712 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/grouped_gram.py @@ -0,0 +1,210 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Grouped-Gram LoRA weight gradients (dA/dB) without per-output-block recompute. + +The split kernel in ``lora_ops`` recomputes XA = X@A^T (resp. YB = dY@B) inside the +kernel, once per output dim-block, to avoid materializing it. For LoRA's small rank +those intermediates are tiny (``[M*k, R]``), so materializing them once and reducing +the per-output-block recompute is a large net win at high expert counts. dA/dB then +reduce to plain grouped Gram products: dA[g] = scaling * YB_g^T @ X_g, +dB[g] = scaling * DY_g^T @ XA_g (reduction over the group's tokens). +""" + +from itertools import product + +import torch +import triton +import triton.language as tl + +from .lora_ops import ALLOW_TF32, _block_r_for_rank, _bucket_m + + +def _grouped_gram_configs(): + configs = [] + for block_wide, block_m, warps, stages in product( + [32, 64, 128, 256], [32, 64, 128], [4, 8], [3, 4] + ): + configs.append( + triton.Config( + {"BLOCK_WIDE": block_wide, "BLOCK_M": block_m}, + num_warps=warps, + num_stages=stages, + ) + ) + return configs + + +@triton.autotune(configs=_grouped_gram_configs(), key=["M_BUCKET", "WIDE", "RANK_IS_I"]) +@triton.jit +def _grouped_gram_kernel( + P_ptr, + stride_pm, + stride_pd, + Q_ptr, + stride_qm, + stride_qd, + OUT_ptr, + stride_og, + stride_oi, + stride_oj, + offsets_ptr, + M_BUCKET, + WIDE: tl.constexpr, + RANK: tl.constexpr, + scaling, + RANK_IS_I: tl.constexpr, + BLOCK_R: tl.constexpr, + BLOCK_WIDE: tl.constexpr, + BLOCK_M: tl.constexpr, + allow_tf32: tl.constexpr, +): + """Grouped Gram product: out[g] = scaling * (P_g^T @ Q_g), summed over the + tokens of combined-group g (a contiguous [start, end) slice). + + Both LoRA weight gradients are this op once XA/YB are precomputed: + dA[g] = scaling * YB_g^T @ X_g (P=YB[M,R], Q=X[M,K]; rank is the I/row dim) + dB[g] = scaling * DY_g^T @ XA_g (P=DY[M,N], Q=XA[M,R]; rank is the J/col dim) + + The wide dim (K for dA, N for dB) is tiled; the rank dim fits one BLOCK_R. + Every (group, wide-block) writes its full tile, so empty groups self-zero -- + no output pre-init needed. Output strides place the [I, J] tile into either + lora_A-grad ([E*T*R, K]) or lora_B-grad ([N, E*T*R]) layout directly. + """ + g = tl.program_id(0) + w_blk = tl.program_id(1) + + start = tl.where(g == 0, 0, tl.load(offsets_ptr + g - 1, mask=g > 0, other=0)) + end = tl.load(offsets_ptr + g) + num = end - start + + w_idx = w_blk * BLOCK_WIDE + tl.arange(0, BLOCK_WIDE) + w_mask = w_idx < WIDE + r_idx = tl.arange(0, BLOCK_R) + r_mask = r_idx < RANK + input_dtype = P_ptr.dtype.element_ty + + if RANK_IS_I: + acc = tl.zeros((BLOCK_R, BLOCK_WIDE), dtype=tl.float32) + else: + acc = tl.zeros((BLOCK_WIDE, BLOCK_R), dtype=tl.float32) + + for i in range(tl.cdiv(num, BLOCK_M)): + m_idx = start + i * BLOCK_M + tl.arange(0, BLOCK_M) + m_mask = m_idx < end + if RANK_IS_I: + p = tl.load( + P_ptr + m_idx[:, None] * stride_pm + r_idx[None, :] * stride_pd, + mask=m_mask[:, None] & r_mask[None, :], + other=0.0, + ).to(input_dtype) + q = tl.load( + Q_ptr + m_idx[:, None] * stride_qm + w_idx[None, :] * stride_qd, + mask=m_mask[:, None] & w_mask[None, :], + other=0.0, + ).to(input_dtype) + acc += tl.dot(tl.trans(p), q, allow_tf32=allow_tf32) + else: + p = tl.load( + P_ptr + m_idx[:, None] * stride_pm + w_idx[None, :] * stride_pd, + mask=m_mask[:, None] & w_mask[None, :], + other=0.0, + ).to(input_dtype) + q = tl.load( + Q_ptr + m_idx[:, None] * stride_qm + r_idx[None, :] * stride_qd, + mask=m_mask[:, None] & r_mask[None, :], + other=0.0, + ).to(input_dtype) + acc += tl.dot(tl.trans(p), q, allow_tf32=allow_tf32) + + acc = acc * scaling + if RANK_IS_I: + out_ptrs = ( + OUT_ptr + + g * stride_og + + r_idx[:, None] * stride_oi + + w_idx[None, :] * stride_oj + ) + out_mask = r_mask[:, None] & w_mask[None, :] + else: + out_ptrs = ( + OUT_ptr + + g * stride_og + + w_idx[:, None] * stride_oi + + r_idx[None, :] * stride_oj + ) + out_mask = w_mask[:, None] & r_mask[None, :] + tl.store(out_ptrs, acc.to(OUT_ptr.dtype.element_ty), mask=out_mask) + + +def grouped_lora_weight_grads( + grouped_grad_out: torch.Tensor, # DY [M*k, N], grouped + grouped_x: torch.Tensor, # X [M*k, K], grouped + yb: torch.Tensor, # DY@B [M*k, R], grouped (reused from dX path) + xa: torch.Tensor, # X@A^T [M*k, R], grouped (saved from forward) + lora_A: torch.Tensor, # [E*T*R, K] + lora_B: torch.Tensor, # [N, E*T*R] + combined_offsets: torch.Tensor, + e_total: int, + scaling: float, +): + """dA/dB via grouped Gram GEMMs over precomputed XA/YB. + + Avoids the shared split kernel's per-output-block recompute of XA/YB (an + inner reduction over K or N, repeated cdiv(wide_dim, BLOCK) times per group); + that recompute is what makes the split path scale poorly as tenants grow. + """ + rank = lora_A.size(0) // e_total + k_dim = lora_A.size(1) + n_dim = lora_B.size(0) + block_r = _block_r_for_rank(rank) + m_bucket = _bucket_m(max(1, grouped_x.size(0) // e_total)) + + dA = torch.empty_like(lora_A) + dB = torch.empty_like(lora_B) + + grid_a = lambda meta: (e_total, triton.cdiv(k_dim, meta["BLOCK_WIDE"])) # noqa: E731 + _grouped_gram_kernel[grid_a]( + yb, + yb.stride(0), + yb.stride(1), + grouped_x, + grouped_x.stride(0), + grouped_x.stride(1), + dA, + rank * dA.stride(0), + dA.stride(0), + dA.stride(1), + combined_offsets, + m_bucket, + WIDE=k_dim, + RANK=rank, + scaling=scaling, + RANK_IS_I=True, + BLOCK_R=block_r, + allow_tf32=ALLOW_TF32, + ) + + grid_b = lambda meta: (e_total, triton.cdiv(n_dim, meta["BLOCK_WIDE"])) # noqa: E731 + _grouped_gram_kernel[grid_b]( + grouped_grad_out, + grouped_grad_out.stride(0), + grouped_grad_out.stride(1), + xa, + xa.stride(0), + xa.stride(1), + dB, + rank * dB.stride(1), + dB.stride(0), + dB.stride(1), + combined_offsets, + m_bucket, + WIDE=n_dim, + RANK=rank, + scaling=scaling, + RANK_IS_I=False, + BLOCK_R=block_r, + allow_tf32=ALLOW_TF32, + ) + return dA, dB diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py index 7bc70280a4..2375dda78f 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py @@ -28,8 +28,8 @@ import torch from .kernels import ops as base_ops +from .kernels.grouped_gram import grouped_lora_weight_grads from .kernels.lora_ops import ( - group_bwd_lora, group_bwd_lora_fused, scatter2scatter_lora, scatter2scatter_lora_dX, @@ -237,6 +237,7 @@ def backward(ctx, grad_out: torch.Tensor): or _needs_int64_indices(grad_out, x) ) + yb = None # dY @ B, computed by the non-fused dA/dB path; reused by dX_lora if can_fuse_gather: # ------------------------------------------------------------------ # Fused path: skip group(x) entirely @@ -291,16 +292,46 @@ def backward(ctx, grad_out: torch.Tensor): grouped_x = base_ops.group(x, sorted_scattered_idxs, fan_out=k) d_expanded_input = grouped_x # Will be overwritten; reuse buffer - d_lora_A, d_lora_B = group_bwd_lora( - DY=grouped_grad_out, + # dA/dB via grouped-Gram over precomputed XA/YB (rank-sized) instead + # of the split kernel's per-output-block recompute -- a large win as + # the expert count grows (modern MoEs, E >= 128). YB is also reused by + # the non-fused dX path below. + rank = lora_A.size(0) // E + k_dim = lora_A.size(1) + n_dim = lora_B.size(0) + w_yb = lora_B.reshape(n_dim, E, rank).permute(1, 0, 2).contiguous() + yb = base_ops.scatter2scatter( + X=grouped_grad_out, + W=w_yb, + k=1, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + x_grouped=True, + y_grouped=True, + int64_indices=needs_int64_bwd, + ) + w_xa = lora_A.reshape(E, rank, k_dim).permute(0, 2, 1).contiguous() + xa = base_ops.scatter2scatter( X=grouped_x, - lora_A=lora_A, - lora_B=lora_B, - expert_offsets=expert_offsets, - E=E, - scaling=scaling, + W=w_xa, + k=1, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + x_grouped=True, + y_grouped=True, int64_indices=needs_int64_bwd, ) + d_lora_A, d_lora_B = grouped_lora_weight_grads( + grouped_grad_out, + grouped_x, + yb, + xa, + lora_A, + lora_B, + expert_offsets, + E, + scaling, + ) # ------------------------------------------------------------------ # Input gradient: dX = dY @ W^T + scaling * (dY @ B) @ A @@ -385,7 +416,8 @@ def backward(ctx, grad_out: torch.Tensor): int64_indices=needs_int64_bwd, ) - # LoRA part: dX_lora = scaling * (dY @ B) @ A + # LoRA part: dX_lora = scaling * (dY @ B) @ A (sync-free grouped GEMMs; + # reuses YB from the dA/dB path when it was computed there) if scaling != 0.0: d_input_lora_grouped = _compute_lora_input_grad( grouped_grad_out, @@ -394,6 +426,10 @@ def backward(ctx, grad_out: torch.Tensor): expert_offsets, E, scaling, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + int64_indices=needs_int64_bwd, + yb=yb, ) if grouped_in: d_expanded_input.add_(d_input_lora_grouped) @@ -448,39 +484,68 @@ def _compute_lora_input_grad( expert_offsets: torch.Tensor, E: int, scaling: float, + sorted_expert_idxs: Optional[torch.Tensor] = None, + sorted_scattered_idxs: Optional[torch.Tensor] = None, + int64_indices: bool = False, + yb: Optional[torch.Tensor] = None, ) -> torch.Tensor: - """ - Compute the LoRA contribution to the input gradient: - dX_lora = scaling * (dY @ B) @ A - - Uses PyTorch ops on expert-grouped data. - Each expert e: dX_e = scaling * (dY_e @ B_e) @ A_e + """LoRA contribution to the input gradient: ``dX_lora = scaling * (dY @ B) @ A``, + on expert-grouped data. + + With routing ids it runs as two grouped GEMMs (``scatter2scatter``) -- sync-free + and a single launch each, instead of a Python per-expert loop with an + ``expert_offsets[e].item()`` device sync per expert (O(E) syncs, which dominate + at the high expert counts of modern MoEs). ``yb = dY @ B`` may be passed in to + reuse the value already computed for the dA/dB grads. The per-expert loop is kept + as a fallback for callers without the routing ids. """ R = lora_A.size(0) // E K = lora_A.size(1) - M_total = grouped_grad_out.size(0) + N = lora_B.size(0) + + if sorted_expert_idxs is not None: + if yb is None: + w_yb = lora_B.reshape(N, E, R).permute(1, 0, 2).contiguous() # [E, N, R] + yb = base_ops.scatter2scatter( + X=grouped_grad_out, + W=w_yb, + k=1, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + x_grouped=True, + y_grouped=True, + int64_indices=int64_indices, + ) + w_a = lora_A.reshape(E, R, K).contiguous() # [E, R, K] + dx = base_ops.scatter2scatter( + X=yb, + W=w_a, + k=1, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + x_grouped=True, + y_grouped=True, + int64_indices=int64_indices, + ) + return dx.mul_(scaling) + # fallback (no routing ids): one host sync for the whole offset array, not per expert + offsets = expert_offsets.tolist() + compute_dtype = grouped_grad_out.dtype d_input_lora = torch.zeros( - (M_total, K), device=grouped_grad_out.device, dtype=grouped_grad_out.dtype + (grouped_grad_out.size(0), K), + device=grouped_grad_out.device, + dtype=compute_dtype, ) - - compute_dtype = grouped_grad_out.dtype - prev_offset = 0 for e in range(E): - curr_offset = expert_offsets[e].item() + curr_offset = offsets[e] if curr_offset > prev_offset: - dy_e = grouped_grad_out[prev_offset:curr_offset] # [M_e, N] - a_e = lora_A[e * R : (e + 1) * R, :].to(compute_dtype) # [r, K] - b_e = lora_B[:, e * R : (e + 1) * R].to(compute_dtype) # [N, r] - - # dX_e = scaling * (dY_e @ B_e) @ A_e - dy_b = dy_e @ b_e # [M_e, r] - dx_e = scaling * (dy_b @ a_e) # [M_e, K] - d_input_lora[prev_offset:curr_offset] = dx_e - + dy_e = grouped_grad_out[prev_offset:curr_offset] + a_e = lora_A[e * R : (e + 1) * R, :].to(compute_dtype) + b_e = lora_B[:, e * R : (e + 1) * R].to(compute_dtype) + d_input_lora[prev_offset:curr_offset] = scaling * ((dy_e @ b_e) @ a_e) prev_offset = curr_offset - return d_input_lora diff --git a/tests/integrations/kernels/scattermoe_lora/test_mx_lora_grouped_gram.py b/tests/integrations/kernels/scattermoe_lora/test_mx_lora_grouped_gram.py new file mode 100644 index 0000000000..60add5b29f --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_mx_lora_grouped_gram.py @@ -0,0 +1,77 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""MXFP4 base + LoRA at large workload hits the non-fused grouped-Gram dA/dB path. + +Guards the edge case: the LoRA weight gradients (dA/dB) must be base-agnostic -- +identical for an MXFP4 base and a bf16 dense base that is its dequantization -- +and dX must still route through the MX kernel (the non-fused dX path is never +taken for MX). The workload is sized above the fuse-gather threshold so dA/dB go +through the grouped-Gram path rather than the fused-gather kernel. +""" + +import pytest + +torch = pytest.importorskip("torch") +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") +MXTensor = pytest.importorskip( + "torchao.prototype.mx_formats.mx_tensor", reason="torchao required for MXFP4" +).MXTensor + +from axolotl.integrations.kernels.libs.scattermoe_lora.mx_weights import ( # noqa: E402 + selective_mx_weights_fwd, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( # noqa: E402 + flatten_sort_count, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_linear_lora import ( # noqa: E402 + ScatterMoELoRA, +) + +_FUSE_GATHER_THRESHOLD = 2**24 + + +def _run(W, x, A, B, sei, ssi, eo, topk, scaling, grad): + xg = x.detach().requires_grad_(True) + Ag = A.detach().requires_grad_(True) + Bg = B.detach().requires_grad_(True) + out = ScatterMoELoRA.apply( + xg, W, topk, sei, ssi, eo, Ag, Bg, scaling, None, None, False, False, True, True + ) + out.backward(grad) + return out, xg.grad, Ag.grad, Bg.grad + + +def test_mxfp4_lora_grads_base_agnostic_large_workload(): + dev, dt = "cuda", torch.bfloat16 + E, K, N, M, topk, R = 8, 2048, 2048, 8192, 2, 16 # M_total*max(K,N) > 2**24 + scaling = 0.5 + torch.manual_seed(0) + + W_dense = torch.randn(E, N, K, device=dev, dtype=dt) + mx = MXTensor.to_mx(W_dense, elem_dtype=torch.float4_e2m1fn_x2, block_size=32) + W_ref = mx.dequantize(dt).contiguous() # exactly what the MX kernel dequantizes to + mx_w = selective_mx_weights_fwd(mx, torch.arange(E, device=dev)) + W_kernel = W_ref.transpose(2, 1).contiguous() # [E, K, N] for the dense kernel + + x = torch.randn(M, K, device=dev, dtype=dt) + A = torch.randn(R * E, K, device=dev, dtype=dt) * 0.02 + B = torch.randn(N, R * E, device=dev, dtype=dt) * 0.02 + _, top = torch.topk(torch.softmax(torch.randn(M, E, device=dev), -1), topk, dim=-1) + sei, ssi, eo = flatten_sort_count(top, E) + + assert ( + sei.size(0) * max(K, N) > _FUSE_GATHER_THRESHOLD + ) # exercises grouped-Gram dA/dB + + grad = torch.randn(sei.size(0), N, device=dev, dtype=dt) + _, dx_mx, da_mx, db_mx = _run(mx_w, x, A, B, sei, ssi, eo, topk, scaling, grad) + _, _, da_bf, db_bf = _run(W_kernel, x, A, B, sei, ssi, eo, topk, scaling, grad) + + # LoRA grads never touch the (frozen) base -> identical for MX vs dense base + assert torch.equal(da_mx, da_bf), (da_mx - da_bf).abs().max().item() + assert torch.equal(db_mx, db_bf), (db_mx - db_bf).abs().max().item() + # and everything is finite + actually trained + for g in (dx_mx, da_mx, db_mx): + assert torch.isfinite(g).all() and g.abs().sum() > 0 From 45a17e925172157a3cdc268c087ce724b9cdd13a Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 7 Jun 2026 13:16:21 -0400 Subject: [PATCH 1342/1405] =?UTF-8?q?perf(scattermoe):=20Blackwell=20sm120?= =?UTF-8?q?=20MoE-LoRA=20=E2=80=94=20EP=20sentinel-skip,=20fused=20MXFP4,?= =?UTF-8?q?=20sonicmoe=20fallback=20(#3714)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(scattermoe-ep): skip DeepEP -1 sentinels in the local kernel Under DeepEP the local scattermoe kernel received the full N*K dispatched routing with remote slots mapped to expert 0 / weight 0, so the grouped GEMM + per-row LoRA processed every sentinel row (compute-and-mask). Worse, routing all sentinels to expert 0 piles ~half the rows into one bucket -> pathological load imbalance. scattermoe_experts_forward_ep drops the -1 sentinel rows before the GEMMs: runs both projections fully grouped over only the valid routed rows (the compacted routing breaks scatter2scatter's L == X.rows*k fan-out contract) and does the weighted token-combine via index_add_. Output is identical to the masked path since sentinel slots carry weight 0. The deep_ep dispatch now passes the raw -1-tagged routing through; each local kernel handles sentinels its own way (eager/scattermoe skip, grouped_mm masks). scattermoe LoRA fwd+bwd, RTX PRO 6000 (Blackwell), skip vs mask: Qwen3-30B ep2 2.9x/0.67x mem ep4 6.0x/0.37x ep8 10.3x/0.25x Qwen3-235B ep2 2.5x/0.65x ep4 4.9x/0.37x ep8 8.4x/0.24x DeepSeek ep2 2.1x/0.60x ep4 3.9x/0.34x ep8 6.8x/0.23x Validated bit-equivalent to the masked path (fp32 ~1e-7, bf16 ~6-9e-3) on output, dX and LoRA dA/dB for base + LoRA at ep2/ep4. * feat(scattermoe): gpt_oss layout + sm_120 sonicmoe fallback The sonic-moe CUTLASS kernel can't compile on consumer Blackwell (sm_120): its bundled quack GemmSm120 predates the concat_layout arg the dispatcher passes. Make the vendored scattermoe Triton path cover the gap. 1. gpt_oss layout in scattermoe_experts_forward: dispatch on the layout flags and handle the gpt_oss-style experts (is_transposed, not is_concatenated, has_bias) in a dedicated path -- weights are already [E, in, out] (no transpose), gate/up are interleaved ([..., ::2] / [..., 1::2]), per-expert bias is folded into the grouped GEMM, and the activation is the clamped sigmoid-GLU. LoRA fuses exactly as in the standard path (same in/out dims, same scatter2scatter_lora). Threads expert_biases through _parallel_linear_maybe_lora. 2. sm_120 fallback: when the sonic-moe kernel can't run (Blackwell) and the experts use a standard layout, sonicmoe_experts_forward_with_lora transparently routes to the scattermoe path, which runs there. Validated vs an eager reference (fp32 with TF32 off, ~1e-6): output, dX, and every gradient (gpt_oss base: d_gate_up/d_down + both biases; LoRA: dA/dB for gate_up and down) for base + LoRA, fp32 + bf16. gpt_oss MoE+LoRA fwd+bwd, vendored Triton vs eager, RTX PRO 6000 (Blackwell): gpt-oss-20b N=2048 9.5x / 0.10x mem gpt-oss-20b N=8192 3.6x / 0.32x mem gpt-oss-120b N=4096 48.8x / 0.05x mem MXFP4 gpt_oss weights dequantize on the fly via parallel_linear_lora (bf16 compute), as with other MXFP4 models on this path; fused MXFP4 for gpt_oss is not yet wired. * perf(scattermoe): use fused MXFP4 kernel in experts forward, no dequant scattermoe_experts_forward dequantized base MXFP4 weights to bf16 every step (the old `_get_base_param(...).transpose(2,1)` actually crashed on an MXTensor, so MXFP4 only ever ran via the dequant-based HFScatterMoEGatedMLP path). The fused MX kernel (scatter2scatter_lora_mx, dequant inside the K-loop) and all the plumbing (selective_mx_weights_fwd / get_active_experts / remap_expert_indices / selective_lora_weights) existed but were only exercised in tests. Wire them in: when the base param is MXFP4 and LoRA is active on both projections, keep the weights packed (4-bit) and route through the fused MX kernel. Gated by is_mxfp4_param, which is False for bf16/fp16 params and when torchao is absent, so non-MXFP4 models fall through to the unchanged bf16 path. The MX kernel is pure-Triton software dequant (LUT), so it needs no hardware MXFP4 support. MXFP4-without-LoRA dequantizes explicitly (the fused kernel is LoRA-only; MXTensor has no transpose). Memory (E=32, H=2048, I=768, N=2048, rank=16, RTX PRO 6000): persistent expert weights: MXFP4 80MB vs bf16 302MB (3.8x) fwd+bwd transient: fused-MX 489MB vs full-dequant 3020MB (6.2x) Validated vs the dequantized-weight reference within MX rounding tol (~1e-3..1e-2) on output, dX, and the active-expert slice of LoRA dA/dB. * feat(ep): sonicmoe+LoRA+EP on sm_120 via scattermoe, fused MXFP4 in EP path Factor the weight/LoRA prep (incl. the fused-MXFP4 active-expert selection) into _prepare_weights_and_lora and use it in both scattermoe_experts_forward and the EP sentinel-skip forward, so base MXFP4 stays packed (no dequant) under EP too. Wire _sonicmoe_local: on a device where the sonic-moe CUTLASS kernel can't run (sm_120) and the experts use a standard layout, route to scattermoe_experts_forward_ep -- giving sonicmoe + LoRA + EP on Blackwell (sentinel-skip + fused MXFP4). Elsewhere sonicmoe+EP still raises (needs the upstream EP-sentinel kernel). Validated MXFP4+EP vs the bf16-dequant reference within MX tol on output, dX, and the active-expert slice of LoRA dA/dB; _sonicmoe_local routing covered. bf16 EP path and the EP plugin suite unchanged (28 passed). * style: pre-commit lint (ruff format/check) for the EP+MXFP4 changes ruff-format reflow + fixes: drop redundant torch importorskip (F811), rename ambiguous I->IM (E741), lambda->def in tests (E731), zip(..., strict=True) (B905). * feat(scattermoe): fused NVFP4 base + LoRA, no dequant Mirror the MXFP4 fused path for NVFP4 so base NVFP4 weights stay 4-bit packed instead of dequantizing to bf16. NVFP4 shares the FP4 E2M1 packing with MXFP4 but scales per 16-element block with an E4M3 (fp8) value times an optional per-tensor scale, so the dequant is codebook(nibble) * e4m3_block_scale * per_tensor -- not a power of two. - is_nvfp4_param + selective_nvfp4_weights_fwd: slice the NVFP4Tensor to active experts and fold the E4M3 block scale (* per-tensor) into a linear fp32 scale, returned as an MXWeights with block_size=16, scale_is_linear=True. The packed buffer stays 4-bit; only the small [a, N, K/16] scale tensor is materialized. - Kernel: the fwd + dX MXFP4 inner loops gain a SCALE_LINEAR branch -- load the scale as a linear fp multiplier instead of E8M0 exp2(byte-127) -- threaded through both scatter2scatter_lora_mx / _dX_mx wrappers, which now read block_size + scale_is_linear off the container. MXFP4 (SCALE_LINEAR=False) is byte-identical to before. - Reuses the MXWeights container, so parallel_linear_lora's isinstance(_, MXWeights) check routes NVFP4 through the fused kernel with no change; _prepare_weights_and_lora adds NVFP4 alongside MXFP4 for both the standard and EP forwards. Validated vs the dequantized-weight reference within NV rounding tol (dX/dA/dB ~1e-3.. 1e-2) through the standard and EP forwards; MXFP4 suite still green (14 passed). --- .../expert_parallel/experts_fn.py | 61 +++- src/axolotl/integrations/kernels/README.md | 8 +- .../kernels/libs/scattermoe_lora/experts.py | 311 +++++++++++++++++- .../libs/scattermoe_lora/kernels/lora_ops.py | 32 +- .../libs/scattermoe_lora/mx_weights.py | 68 +++- .../libs/scattermoe_lora/selective_dequant.py | 7 + .../kernels/libs/sonicmoe/experts.py | 29 +- .../scattermoe_lora/test_ep_sentinel_skip.py | 127 +++++++ .../scattermoe_lora/test_gptoss_layout.py | 185 +++++++++++ .../test_mxfp4_experts_forward.py | 216 ++++++++++++ .../test_nvfp4_experts_forward.py | 138 ++++++++ 11 files changed, 1134 insertions(+), 48 deletions(-) create mode 100644 tests/integrations/kernels/scattermoe_lora/test_ep_sentinel_skip.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_gptoss_layout.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_mxfp4_experts_forward.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_nvfp4_experts_forward.py diff --git a/src/axolotl/integrations/expert_parallel/experts_fn.py b/src/axolotl/integrations/expert_parallel/experts_fn.py index 89473fd112..88ad49d0b9 100644 --- a/src/axolotl/integrations/expert_parallel/experts_fn.py +++ b/src/axolotl/integrations/expert_parallel/experts_fn.py @@ -51,23 +51,58 @@ def _maybe_install_decorator_attrs(experts): experts.is_transposed = False +def _mask_sentinels(recv_topk_idx, recv_topk_weights): + """Map ``-1`` remote sentinels to expert 0 / weight 0 for kernels that index by + expert id and don't filter (grouped_mm). The zero weight nulls their contribution.""" + safe_idx = torch.where( + recv_topk_idx >= 0, recv_topk_idx, torch.zeros_like(recv_topk_idx) + ) + valid = (recv_topk_idx >= 0).to(recv_topk_weights.dtype) + return safe_idx, recv_topk_weights * valid + + def _grouped_mm_local(experts, recv_x, recv_topk_idx, recv_topk_weights): from transformers.integrations.moe import grouped_mm_experts_forward _maybe_install_decorator_attrs(experts) - return grouped_mm_experts_forward(experts, recv_x, recv_topk_idx, recv_topk_weights) + safe_idx, safe_w = _mask_sentinels(recv_topk_idx, recv_topk_weights) + return grouped_mm_experts_forward(experts, recv_x, safe_idx, safe_w) def _scattermoe_local(experts, recv_x, recv_topk_idx, recv_topk_weights): + # scattermoe skips sentinel rows natively (only valid rows hit the grouped GEMM + # + per-row LoRA) -- pass the raw -1-tagged routing, not the masked version. from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( - scattermoe_experts_forward, + scattermoe_experts_forward_ep, ) - return scattermoe_experts_forward(experts, recv_x, recv_topk_idx, recv_topk_weights) + return scattermoe_experts_forward_ep( + experts, recv_x, recv_topk_idx, recv_topk_weights + ) def _sonicmoe_local(experts, recv_x, recv_topk_idx, recv_topk_weights): - raise NotImplementedError("Sonicmoe + EP is not yet properly implemented.") + # The sonic-moe CUTLASS kernel can't run on sm_120 (Blackwell); for standard-layout + # experts there, use the vendored scattermoe EP path (sentinel-skip + fused MXFP4), + # which runs on sm_120 and gives sonicmoe + LoRA + EP. Elsewhere sonicmoe+EP is not + # yet wired (needs the upstream EP-sentinel kernel). + from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( + scattermoe_experts_forward_ep, + scattermoe_supports_layout, + ) + from axolotl.integrations.kernels.libs.sonicmoe.experts import ( + _sonicmoe_kernel_supported, + ) + + if not _sonicmoe_kernel_supported() and scattermoe_supports_layout(experts): + return scattermoe_experts_forward_ep( + experts, recv_x, recv_topk_idx, recv_topk_weights + ) + raise NotImplementedError( + "Sonicmoe + EP is not yet implemented on this device/layout. On sm_120 with a " + "standard expert layout it falls back to the scattermoe EP path automatically; " + "otherwise use use_scattermoe." + ) _LOCAL_KERNELS = { @@ -160,9 +195,10 @@ def _deep_ep_forward(self, hidden_states, top_k_index, top_k_weights, *, kernel_ """Shared dispatch -> local-experts -> combine pipeline. Inputs come in with **global** routing indices (we do not run - `transformers.RouterParallel`; see DEEP_EP.md §2.4 for why). Sentinels are - `-1` for slots routed to remote experts; we mask them to a valid local id - with weight=0 so the local kernel can index safely. + `transformers.RouterParallel`; see DEEP_EP.md §2.4 for why). Dispatch returns + local expert ids in `[0, E_local)` with `-1` for slots routed to remote experts; + the `-1` sentinels are passed through to the local kernel, which decides whether + to skip them (eager/scattermoe) or mask them (grouped_mm). """ if hidden_states.dtype != torch.bfloat16: original_dtype = hidden_states.dtype @@ -191,14 +227,11 @@ def _deep_ep_forward(self, hidden_states, top_k_index, top_k_weights, *, kernel_ is_in_rank, ) - # Mask -1 sentinels for kernels that don't filter. - safe_idx = torch.where( - recv_topk_idx >= 0, recv_topk_idx, torch.zeros_like(recv_topk_idx) + # Pass the raw -1-tagged routing through; each local kernel handles sentinels + # its own way (eager/scattermoe skip them, grouped_mm masks internally). + local_out = _LOCAL_KERNELS[kernel_name]( + self, recv_x, recv_topk_idx, recv_topk_weights ) - valid_mask = (recv_topk_idx >= 0).to(recv_topk_weights.dtype) - safe_w = recv_topk_weights * valid_mask - - local_out = _LOCAL_KERNELS[kernel_name](self, recv_x, safe_idx, safe_w) combined = _DeepEPCombine.apply(local_out, handle_holder) diff --git a/src/axolotl/integrations/kernels/README.md b/src/axolotl/integrations/kernels/README.md index aae2fee97f..41a0fa9441 100644 --- a/src/axolotl/integrations/kernels/README.md +++ b/src/axolotl/integrations/kernels/README.md @@ -84,9 +84,13 @@ Any model whose `Experts` class is decorated with `@use_experts_implementation` | `ernie4_5_moe` | Yes | Yes | | `hunyuan_v1_moe` | Yes | Yes | | `gemma4_text` | Yes | Yes | -| `gpt_oss` | No | Yes | +| `gpt_oss` | Yes | Yes | -`gpt_oss` carries the decorator with `is_concatenated=False, is_transposed=True, has_bias=True` and uses a sigmoid-GLU activation with clamping. The SonicMoE forward reads these flags off `self` and dispatches accordingly. The ScatterMoE forward assumes the standard `[E, 2*I, H]` concat layout and SiLU-GLU without bias, so it does not yet support `gpt_oss`. +`gpt_oss` carries the decorator with `is_concatenated=False, is_transposed=True, has_bias=True` and uses a sigmoid-GLU activation with clamping. Both forwards read these flags off `self` and dispatch accordingly: the ScatterMoE forward handles the transposed/interleaved/biased layout and clamped sigmoid-GLU via its Triton path (no weight transpose, interleaved gate/up, per-expert bias folded into the grouped GEMM); the SonicMoE forward uses the upstream CUTLASS kernel. + +### Blackwell (sm_120) note + +The SonicMoE CUTLASS kernel (`kernels-community/sonic-moe`) does not currently run on consumer Blackwell (sm_120) — its bundled quack `GemmSm120` predates the `concat_layout` arg the dispatcher passes. On sm_120, `use_sonicmoe` with a standard-layout model transparently falls back to the ScatterMoE Triton path, which runs there. `gpt_oss` on sm_120 should use `use_scattermoe` directly (bf16 base; MXFP4 weights dequantize on the fly as with other MXFP4 models — fused MXFP4 for `gpt_oss` is not yet wired). ## Feature comparison diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts.py index 9199c8a595..b5601848c5 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts.py @@ -6,8 +6,16 @@ import torch +from .mx_weights import selective_mx_weights_fwd, selective_nvfp4_weights_fwd from .parallel_experts import flatten_sort_count, parallel_linear from .parallel_linear_lora import get_lora_params_from_wrapper, parallel_linear_lora +from .selective_dequant import ( + get_active_experts, + is_mxfp4_param, + is_nvfp4_param, + remap_expert_indices, + selective_lora_weights, +) def _has_peft_wrapper(module): @@ -91,6 +99,7 @@ def _parallel_linear_maybe_lora( grouped_in, grouped_out, gates=None, + expert_biases=None, ): """Call parallel_linear or parallel_linear_lora depending on whether LoRA is active.""" if lora_tuple is not None: @@ -105,6 +114,7 @@ def _parallel_linear_maybe_lora( lora_A, lora_B, scaling, + expert_biases=expert_biases, grouped_in=grouped_in, grouped_out=grouped_out, gates=gates, @@ -116,51 +126,223 @@ def _parallel_linear_maybe_lora( sorted_expert_idxs, sorted_scattered_idxs, expert_offsets, + expert_biases=expert_biases, grouped_in=grouped_in, grouped_out=grouped_out, gates=gates, ) -def scattermoe_experts_forward( - self, - hidden_states: torch.Tensor, - top_k_index: torch.Tensor, - top_k_weights: torch.Tensor, -) -> torch.Tensor: - """ScatterMoE experts forward with fused-LoRA support.""" - # Assumes the standard expert layout: gate_up concatenated as [E, 2I, H], - # gated SwiGLU, no expert bias. gpt_oss-style experts (interleaved gate/up, - # transposed [E, H, 2I], expert bias) would be silently miscomputed by the - # fixed transpose/chunk below, so reject rather than corrupt training. - if ( +def _prepare_weights_and_lora( + gu_param, + dn_param, + sorted_expert_idxs, + expert_offsets, + num_experts, + gup_lora, + down_lora, + dtype, +): + """Resolve the gate_up / down weights (+ routing + LoRA) for the grouped GEMM. + + For an MXFP4 or NVFP4 base with LoRA on both projections, keep the weights packed + (4-bit) and route through the fused kernel: select the active experts and remap the + routing / LoRA to that compact set (dequant happens inside the kernel K-loop; NVFP4 + reuses the MXWeights container with linear E4M3 block scales). Otherwise return bf16 + ``[E, in, out]`` weights (dequantizing FP4 explicitly when LoRA is absent, since the + fused kernel is LoRA-only and the FP4 tensors have no transpose). + + Returns ``(gate_up_weight, down_weight, sorted_expert_idxs, expert_offsets, + gup_lora, down_lora)``. + """ + is_mx, is_nv = is_mxfp4_param(gu_param), is_nvfp4_param(gu_param) + if (is_mx or is_nv) and gup_lora is not None and down_lora is not None: + select = selective_mx_weights_fwd if is_mx else selective_nvfp4_weights_fwd + active = get_active_experts(sorted_expert_idxs, num_experts) + sorted_expert_idxs, expert_offsets = remap_expert_indices( + sorted_expert_idxs, expert_offsets, active, num_experts + ) + gate_up_weight = select(gu_param, active) + down_weight = select(dn_param, active) + gup_lora = ( + *selective_lora_weights(gup_lora[0], gup_lora[1], active, num_experts), + gup_lora[2], + ) + down_lora = ( + *selective_lora_weights(down_lora[0], down_lora[1], active, num_experts), + down_lora[2], + ) + elif is_mx or is_nv: + # quantized base without LoRA: the fused kernel is LoRA-only and the FP4 + # tensors have no transpose, so dequantize to bf16 here. + gate_up_weight = gu_param.dequantize(dtype).transpose(2, 1) + down_weight = dn_param.dequantize(dtype).transpose(2, 1) + else: + gate_up_weight = gu_param.transpose(2, 1) # bf16 [E, out, in] -> [E, in, out] + down_weight = dn_param.transpose(2, 1) + return ( + gate_up_weight, + down_weight, + sorted_expert_idxs, + expert_offsets, + gup_lora, + down_lora, + ) + + +def scattermoe_supports_layout(self) -> bool: + """True iff this experts module uses the standard layout scattermoe handles: + gate_up concatenated as [E, 2I, H], gated SwiGLU, no expert bias. gpt_oss-style + experts (interleaved gate/up, transposed [E, H, 2I], expert bias) return False.""" + return not ( getattr(self, "is_transposed", False) or not getattr(self, "is_concatenated", True) or getattr(self, "has_bias", False) or not getattr(self, "has_gate", True) - ): + ) + + +def _check_supported_layout(self): + """Reject expert layouts the fixed transpose/chunk below would miscompute.""" + # gpt_oss-style experts (interleaved gate/up, transposed [E, H, 2I], expert + # bias) would be silently miscomputed by the fixed transpose/chunk below, so + # reject rather than corrupt training. + if not scattermoe_supports_layout(self): raise NotImplementedError( "scattermoe supports only concatenated, non-transposed, gated, biasless " "experts (qwen/mixtral/deepseek/glm/...). This model's experts use an " "unsupported layout; use use_sonicmoe or a built-in experts_implementation." ) - K = top_k_index.shape[1] +def _is_gptoss_layout(self) -> bool: + """gpt_oss expert layout: transposed [E, H, 2I] weights, interleaved gate/up, + per-expert bias, clamped sigmoid-GLU. Handled by the Triton path below.""" + return ( + getattr(self, "is_transposed", False) + and not getattr(self, "is_concatenated", True) + and getattr(self, "has_bias", False) + and getattr(self, "has_gate", True) + and hasattr(self, "gate_up_proj_bias") + and hasattr(self, "down_proj_bias") + ) + + +def _gptoss_glu(gate_up: torch.Tensor, alpha: float, limit: float) -> torch.Tensor: + """gpt_oss clamped sigmoid-GLU over interleaved gate/up columns (matches + ``GptOssExperts._apply_gate``).""" + gate, up = gate_up[..., ::2], gate_up[..., 1::2] + gate = gate.clamp(max=limit) + up = up.clamp(min=-limit, max=limit) + glu = gate * torch.sigmoid(gate * alpha) + return (up + 1) * glu + + +def _scattermoe_gptoss_forward( + self, + hidden_states: torch.Tensor, + top_k_index: torch.Tensor, + top_k_weights: torch.Tensor, +) -> torch.Tensor: + """ScatterMoE forward for gpt_oss-style experts (transposed/interleaved/biased). + + gpt_oss stores weights already in ``[E, in, out]`` form, so no transpose; gate/up + are interleaved (``[..., ::2]`` / ``[..., 1::2]``); per-expert bias is folded into + the grouped GEMM; the activation is the clamped sigmoid-GLU. LoRA fuses exactly as + in the standard path (in/out dims match), and the down bias is added per row before + the routing-weight combine. + """ + K = top_k_index.shape[1] routing_weights = top_k_weights.to(hidden_states.dtype) sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = flatten_sort_count( top_k_index, num_experts=self.num_experts ) - # Get base weights (unwrap PEFT if needed) - gate_up_weight = _get_base_param(self.gate_up_proj).transpose(2, 1) - down_weight = _get_base_param(self.down_proj).transpose(2, 1) + gate_up_weight = _get_base_param( + self.gate_up_proj + ) # [E, H, 2I], already [E,in,out] + down_weight = _get_base_param(self.down_proj) # [E, I, H] + gate_up_bias = _get_base_param(self.gate_up_proj_bias) # [E, 2I] + down_bias = _get_base_param(self.down_proj_bias) # [E, H] + + gup_lora, down_lora = None, None + if _has_peft_wrapper(self): + _, gup_lora, down_lora = _unwrap_experts_lora(self) + + gate_up = _parallel_linear_maybe_lora( + hidden_states, + gate_up_weight, + K, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + gup_lora, + grouped_in=False, + grouped_out=True, + expert_biases=gate_up_bias, + ) + h = _gptoss_glu(gate_up, getattr(self, "alpha", 1.702), getattr(self, "limit", 7.0)) + + output = _parallel_linear_maybe_lora( + h, + down_weight, + 1, + sorted_expert_idxs, + sorted_scattered_idxs, + expert_offsets, + down_lora, + grouped_in=True, + grouped_out=False, + gates=routing_weights, + expert_biases=down_bias, + ) + return output + + +def scattermoe_experts_forward( + self, + hidden_states: torch.Tensor, + top_k_index: torch.Tensor, + top_k_weights: torch.Tensor, +) -> torch.Tensor: + """ScatterMoE experts forward with fused-LoRA support.""" + if not scattermoe_supports_layout(self): + if _is_gptoss_layout(self): + return _scattermoe_gptoss_forward( + self, hidden_states, top_k_index, top_k_weights + ) + _check_supported_layout(self) # raises for any other unsupported layout + + K = top_k_index.shape[1] + + routing_weights = top_k_weights.to(hidden_states.dtype) + sorted_expert_idxs, sorted_scattered_idxs, expert_offsets = flatten_sort_count( + top_k_index, num_experts=self.num_experts + ) # Extract LoRA params if PEFT is active gup_lora, down_lora = None, None if _has_peft_wrapper(self): _, gup_lora, down_lora = _unwrap_experts_lora(self) + ( + gate_up_weight, + down_weight, + sorted_expert_idxs, + expert_offsets, + gup_lora, + down_lora, + ) = _prepare_weights_and_lora( + _get_base_param(self.gate_up_proj), + _get_base_param(self.down_proj), + sorted_expert_idxs, + expert_offsets, + self.num_experts, + gup_lora, + down_lora, + hidden_states.dtype, + ) + # Gate-up projection (with optional LoRA) gates_h = _parallel_linear_maybe_lora( hidden_states, @@ -193,6 +375,101 @@ def scattermoe_experts_forward( return output +def scattermoe_experts_forward_ep( + self, + hidden_states: torch.Tensor, + top_k_index: torch.Tensor, + top_k_weights: torch.Tensor, +) -> torch.Tensor: + """ScatterMoE experts forward for the DeepEP local path, skipping EP sentinels. + + After DeepEP dispatch ``top_k_index`` holds local expert ids in ``[0, E_local)`` + for slots this rank owns and ``-1`` for slots routed to remote ranks. Rather than + map sentinels to expert 0 / weight 0 and run the full grouped GEMM over all + ``N*K`` rows (compute-and-mask), drop the sentinel rows so only the valid routed + rows hit the GEMM + per-row LoRA. Output matches the masked path since sentinel + slots carry weight 0. + + Runs both projections fully grouped (the sentinel-compacted routing breaks the + ``L_scattered == X.rows * k`` fan-out contract of the scattered path), with the + weighted token-combine done via ``index_add_``. + """ + _check_supported_layout(self) + + N = hidden_states.size(0) + K = top_k_index.shape[1] + E_local = self.num_experts + + idx_flat = top_k_index.reshape(-1) + valid = idx_flat >= 0 + e_v = idx_flat[valid].to(torch.long) + if e_v.numel() == 0: + return torch.zeros_like(hidden_states) + + tok = torch.arange(N, device=hidden_states.device).repeat_interleave(K) + tok_v = tok[valid] + w_v = top_k_weights.reshape(-1)[valid].to(hidden_states.dtype) + + # Sort valid rows by expert so each expert's rows are contiguous (grouped layout). + se, order = torch.sort(e_v) + tok_sorted = tok_v[order].to(torch.int32) + w_sorted = w_v[order] + expert_offsets = torch.bincount(e_v, minlength=E_local).cumsum(-1) + M = se.size(0) + ss = torch.arange(M, device=hidden_states.device, dtype=torch.int32) + + gup_lora, down_lora = None, None + if _has_peft_wrapper(self): + _, gup_lora, down_lora = _unwrap_experts_lora(self) + + gate_up_weight, down_weight, se, expert_offsets, gup_lora, down_lora = ( + _prepare_weights_and_lora( + _get_base_param(self.gate_up_proj), + _get_base_param(self.down_proj), + se, + expert_offsets, + E_local, + gup_lora, + down_lora, + hidden_states.dtype, + ) + ) + + # Pre-gather token rows into expert-grouped order; both projections run grouped. + grouped_x = hidden_states.index_select(0, tok_sorted.to(torch.long)) + + gates_h = _parallel_linear_maybe_lora( + grouped_x, + gate_up_weight, + 1, + se, + ss, + expert_offsets, + gup_lora, + grouped_in=True, + grouped_out=True, + ) + gates, h = gates_h.chunk(2, dim=-1) + h = self.act_fn(gates) * h + + down_out = _parallel_linear_maybe_lora( + h, + down_weight, + 1, + se, + ss, + expert_offsets, + down_lora, + grouped_in=True, + grouped_out=True, + ) + down_out = down_out * w_sorted.unsqueeze(-1) + + output = hidden_states.new_zeros((N, down_out.size(-1))) + output.index_add_(0, tok_sorted.to(torch.long), down_out) + return output + + _SCATTERMOE_PATCHED = False diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py index a453e83495..f25c00a26f 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/lora_ops.py @@ -2403,6 +2403,7 @@ def _compute_expert_block_lora_mxfp4( BLOCK_N: tl.constexpr, BLOCK_R: tl.constexpr, MX_BLOCK_SIZE: tl.constexpr, + SCALE_LINEAR: tl.constexpr, scaling, allow_tf32: tl.constexpr, ): @@ -2469,8 +2470,13 @@ def _compute_expert_block_lora_mxfp4( packed = tl.load(Wp_blk_ptrs, mask=w_mask, other=0).to(tl.int32) nibble = tl.where(K_is_high[None, :], (packed >> 4) & 0xF, packed & 0xF) codebook_val = tl.load(Codebook_ptr + nibble) # [BLOCK_N, BLOCK_K] fp32 - scale_byte = tl.load(Ws_blk_ptrs, mask=w_mask, other=0).to(tl.int32) - scale_fp = tl.exp2((scale_byte - 127).to(tl.float32)) + if SCALE_LINEAR: + # NVFP4: scale is a linear fp multiplier (E4M3 block scale * per-tensor) + scale_fp = tl.load(Ws_blk_ptrs, mask=w_mask, other=0.0).to(tl.float32) + else: + # MXFP4: E8M0 scale, decode as 2^(byte-127) + scale_byte = tl.load(Ws_blk_ptrs, mask=w_mask, other=0).to(tl.int32) + scale_fp = tl.exp2((scale_byte - 127).to(tl.float32)) w_dq_nk = (codebook_val * scale_fp).to(INPUT_DTYPE) # [BLOCK_N, BLOCK_K] w_tile = tl.trans(w_dq_nk) # [BLOCK_K, BLOCK_N] @@ -2644,6 +2650,7 @@ def _scatter2scatter_lora_mx( BLOCK_K: tl.constexpr, BLOCK_R: tl.constexpr, MX_BLOCK_SIZE: tl.constexpr, + SCALE_LINEAR: tl.constexpr, ACC_TYPE: tl.constexpr, scaling, allow_tf32: tl.constexpr, @@ -2653,7 +2660,7 @@ def _scatter2scatter_lora_mx( NO_N_MASK: tl.constexpr, INT64_INDICES: tl.constexpr = False, ): - """Fused scatter2scatter forward with MXFP4 base weights + LoRA.""" + """Fused scatter2scatter forward with MXFP4/NVFP4 base weights + LoRA.""" pid = tl.program_id(axis=0) N_BLOCK_COUNT = tl.cdiv(N, BLOCK_N) M_block_id = pid // N_BLOCK_COUNT @@ -2717,6 +2724,7 @@ def _scatter2scatter_lora_mx( BLOCK_N, BLOCK_R, MX_BLOCK_SIZE, + SCALE_LINEAR, scaling, allow_tf32=allow_tf32, ) @@ -2829,7 +2837,8 @@ def grid(META): E=E, ACTUAL_R=R, BLOCK_R=BLOCK_R, - MX_BLOCK_SIZE=_MX_BLOCK_SIZE, + MX_BLOCK_SIZE=W_mx.block_size, + SCALE_LINEAR=W_mx.scale_is_linear, ACC_TYPE=tl.float32, scaling=scaling, allow_tf32=ALLOW_TF32, @@ -2879,6 +2888,7 @@ def _compute_expert_block_lora_dX_mxfp4( BLOCK_K: tl.constexpr, BLOCK_R: tl.constexpr, MX_BLOCK_SIZE: tl.constexpr, + SCALE_LINEAR: tl.constexpr, scaling, allow_tf32: tl.constexpr, ): @@ -2950,8 +2960,11 @@ def _compute_expert_block_lora_dX_mxfp4( packed = tl.load(Wp_blk_ptrs, mask=w_mask, other=0).to(tl.int32) nibble = tl.where(K_is_high[:, None], (packed >> 4) & 0xF, packed & 0xF) codebook_val = tl.load(Codebook_ptr + nibble) - scale_byte = tl.load(Ws_blk_ptrs, mask=w_mask, other=0).to(tl.int32) - scale_fp = tl.exp2((scale_byte - 127).to(tl.float32)) + if SCALE_LINEAR: + scale_fp = tl.load(Ws_blk_ptrs, mask=w_mask, other=0.0).to(tl.float32) + else: + scale_byte = tl.load(Ws_blk_ptrs, mask=w_mask, other=0).to(tl.int32) + scale_fp = tl.exp2((scale_byte - 127).to(tl.float32)) w_dq = (codebook_val * scale_fp).to(INPUT_DTYPE) # [BLOCK_K, BLOCK_N] # Base: acc += DY @ W^T ([M, N] @ [N, K] -> [M, K]) @@ -3107,6 +3120,7 @@ def _scatter2scatter_lora_dX_mx( BLOCK_K: tl.constexpr, BLOCK_R: tl.constexpr, MX_BLOCK_SIZE: tl.constexpr, + SCALE_LINEAR: tl.constexpr, ACC_TYPE: tl.constexpr, scaling, allow_tf32: tl.constexpr, @@ -3115,7 +3129,7 @@ def _scatter2scatter_lora_dX_mx( NO_N_MASK: tl.constexpr, INT64_INDICES: tl.constexpr = False, ): - """Fused MXFP4 dX kernel.""" + """Fused MXFP4/NVFP4 dX kernel.""" pid = tl.program_id(axis=0) K_BLOCK_COUNT = tl.cdiv(K, BLOCK_K) M_block_id = pid // K_BLOCK_COUNT @@ -3179,6 +3193,7 @@ def _scatter2scatter_lora_dX_mx( BLOCK_K, BLOCK_R, MX_BLOCK_SIZE, + SCALE_LINEAR, scaling, allow_tf32=allow_tf32, ) @@ -3285,7 +3300,8 @@ def grid(META): E=E, ACTUAL_R=R, BLOCK_R=BLOCK_R, - MX_BLOCK_SIZE=_MX_BLOCK_SIZE, + MX_BLOCK_SIZE=W_mx.block_size, + SCALE_LINEAR=W_mx.scale_is_linear, ACC_TYPE=tl.float32, scaling=scaling, allow_tf32=ALLOW_TF32, diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/mx_weights.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/mx_weights.py index d3916c54bb..f4ccf96d46 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/mx_weights.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/mx_weights.py @@ -90,7 +90,12 @@ class MXWeights: layout: Which axis the block scaling runs along (see ``MXLayout``). block_size: - OCP MX block size; only ``32`` is supported by the kernels. + OCP block size: ``32`` for MXFP4 (E8M0 scales), ``16`` for NVFP4. + scale_is_linear: + ``False`` (default): scales are E8M0 (``uint8``), decoded in-kernel as + ``2^(byte-127)``. ``True``: scales are already a linear floating-point + multiplier (e.g. NVFP4's E4M3 block scale pre-multiplied by the per-tensor + scale), loaded and used directly with no exponent decode. """ packed: torch.Tensor @@ -101,14 +106,15 @@ class MXWeights: block_size: int = MX_BLOCK_SIZE num_experts: Optional[int] = None # E_active; convenience field orig_dtype: torch.dtype = torch.bfloat16 + scale_is_linear: bool = False def __post_init__(self) -> None: - assert self.block_size == MX_BLOCK_SIZE, ( - f"only block_size={MX_BLOCK_SIZE} is supported, got {self.block_size}" + assert self.block_size in (16, 32), ( + f"block_size must be 16 (NVFP4) or 32 (MXFP4), got {self.block_size}" ) - # scales are E8M0 (float8_e8m0fnu) in torchao; viewed as uint8 here so - # the Triton kernel can load them with simple integer arithmetic. - if self.scales.dtype != torch.uint8: + if not self.scale_is_linear and self.scales.dtype != torch.uint8: + # E8M0 (float8_e8m0fnu) scales viewed as uint8 so the Triton kernel can + # load them with simple integer arithmetic. Linear scales stay floating. self.scales = self.scales.view(torch.uint8) assert self.packed.dtype == torch.uint8, ( f"packed must be uint8, got {self.packed.dtype}" @@ -219,3 +225,53 @@ def selective_mx_weights_fwd(mx_param, active_experts: torch.Tensor) -> MXWeight num_experts=sub_qdata.size(0), orig_dtype=mx_param.orig_dtype, ) + + +def _torchao_nvfp4tensor_cls(): + """Return the torchao NVFP4Tensor class, or ``None`` if torchao is missing.""" + try: + from torchao.prototype.mx_formats.nvfp4_tensor import NVFP4Tensor + except ImportError: + return None + return NVFP4Tensor + + +def selective_nvfp4_weights_fwd(nv_param, active_experts: torch.Tensor) -> MXWeights: + """Slice an NVFP4 expert parameter to the active set as an ``MXWeights`` with + ``scale_is_linear=True``. + + NVFP4 shares the FP4 E2M1 element codebook with MXFP4 (same packed nibble layout) + but scales per 16-element block with an E4M3 (fp8) value, optionally times a + per-tensor fp32 scale. The dequant is ``codebook(nibble) * e4m3_block_scale * + per_tensor`` -- not a power-of-2 -- so the E4M3 block scale (folded with the + per-tensor scale) is decoded to a linear fp32 multiplier here and consumed + directly by the kernel (no in-kernel exponent decode). The packed buffer stays + 4-bit; only the small ``[num_active, N, K/16]`` scale tensor is materialized. + """ + NVFP4Tensor = _torchao_nvfp4tensor_cls() + if NVFP4Tensor is None: + raise ImportError("NVFP4 fused path requires torchao (install `torchao`).") + assert isinstance(nv_param, NVFP4Tensor), ( + f"selective_nvfp4_weights_fwd expects an NVFP4Tensor, got {type(nv_param)}" + ) + assert nv_param.block_size == 16, ( + f"NVFP4 block_size must be 16, got {nv_param.block_size}" + ) + sub_qdata = nv_param.qdata[active_experts].contiguous() # [a, N, K/2] uint8 + block_scale = nv_param.scale[active_experts].to(torch.float32) # [a, N, K/16] + per_tensor = getattr(nv_param, "per_tensor_scale", None) + if per_tensor is not None: + block_scale = block_scale * per_tensor.to(torch.float32) + N = sub_qdata.size(1) + K = sub_qdata.size(2) * 2 + return MXWeights( + packed=sub_qdata, + scales=block_scale.contiguous(), + K=K, + N=N, + layout=MXLayout.FWD, + block_size=16, + num_experts=sub_qdata.size(0), + orig_dtype=nv_param.orig_dtype, + scale_is_linear=True, + ) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py index 6398d1faa3..e49e9c7bf6 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py @@ -31,6 +31,7 @@ _mx_qdata, _mx_scale, _torchao_mxtensor_cls, + _torchao_nvfp4tensor_cls, ) @@ -42,6 +43,12 @@ def is_mxfp4_param(param) -> bool: return param.elem_dtype == torch.float4_e2m1fn_x2 +def is_nvfp4_param(param) -> bool: + """True iff ``param`` is a torchao NVFP4Tensor (FP4 E2M1, block-16 E4M3 scales).""" + NVFP4Tensor = _torchao_nvfp4tensor_cls() + return NVFP4Tensor is not None and isinstance(param, NVFP4Tensor) + + def get_active_experts(sorted_expert_idxs: torch.Tensor, E: int) -> torch.Tensor: """Get sorted unique expert indices from the routing output. diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/experts.py b/src/axolotl/integrations/kernels/libs/sonicmoe/experts.py index 785b74e45a..a0d7501a95 100644 --- a/src/axolotl/integrations/kernels/libs/sonicmoe/experts.py +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/experts.py @@ -6,6 +6,8 @@ from __future__ import annotations +import functools + import torch from .lora import ( @@ -55,13 +57,38 @@ def _resolve_weights_and_lora(experts_module): return w1, b1, w2, b2, lora_w1, lora_w2 +@functools.lru_cache(maxsize=1) +def _sonicmoe_kernel_supported() -> bool: + """The kernels-community/sonic-moe CUTLASS GEMM fails to compile on sm_120 + (Blackwell): its bundled quack ``GemmSm120`` predates the ``concat_layout`` arg + the dispatcher passes. Gate it off there so standard-layout models fall back to + the vendored scattermoe Triton path. Drop this once a fixed sonic-moe ships.""" + if not torch.cuda.is_available(): + return True + return torch.cuda.get_device_capability()[0] < 12 + + def sonicmoe_experts_forward_with_lora( self, hidden_states: torch.Tensor, top_k_index: torch.Tensor, top_k_weights: torch.Tensor, ) -> torch.Tensor: - """Sonicmoe experts forward with PEFT LoRA materialization.""" + """Sonicmoe experts forward with PEFT LoRA materialization. + + On devices where the sonic-moe kernel can't run (sm_120), standard-layout + experts transparently use the vendored scattermoe Triton path instead. + """ + from ..scattermoe_lora.experts import ( + scattermoe_experts_forward, + scattermoe_supports_layout, + ) + + if not _sonicmoe_kernel_supported() and scattermoe_supports_layout(self): + return scattermoe_experts_forward( + self, hidden_states, top_k_index, top_k_weights + ) + from transformers.integrations.sonicmoe import _sonicmoe_wrapper if not getattr(self, "has_gate", True): diff --git a/tests/integrations/kernels/scattermoe_lora/test_ep_sentinel_skip.py b/tests/integrations/kernels/scattermoe_lora/test_ep_sentinel_skip.py new file mode 100644 index 0000000000..eeed72bbc6 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_ep_sentinel_skip.py @@ -0,0 +1,127 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""scattermoe DeepEP local path skips ``-1`` sentinels instead of compute-and-mask. + +Oracle: on the SAME experts module, ``scattermoe_experts_forward_ep`` (raw -1-tagged +routing) must match ``scattermoe_experts_forward`` with sentinels mapped to expert 0 / +weight 0 -- output, dX, and LoRA dA/dB. Sentinel slots carry weight 0 so both compute +the same math; the skip path just omits the wasted rows (and the load-imbalanced +expert-0 bucket the masked path creates). +""" + +from types import SimpleNamespace + +import pytest +import torch + +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") + +import axolotl.integrations.kernels.libs.scattermoe_lora.experts as ex # noqa: E402 +from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( # noqa: E402 + scattermoe_experts_forward, + scattermoe_experts_forward_ep, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( # noqa: E402 + peft_lora_to_scattermoe, +) + +DEV = "cuda" + + +def _module(E, H, IM, dt, rank, monkeypatch): + self = SimpleNamespace( + num_experts=E, + gate_up_proj=torch.randn(E, 2 * IM, H, device=DEV, dtype=dt) * 0.02, + down_proj=torch.randn(E, H, IM, device=DEV, dtype=dt) * 0.02, + act_fn=torch.nn.functional.silu, + is_transposed=False, + is_concatenated=True, + has_bias=False, + has_gate=True, + ) + lora = None + if rank: + A1 = (torch.randn(rank * E, H, device=DEV, dtype=dt) * 0.02).requires_grad_( + True + ) + B1 = ( + torch.randn(2 * IM, rank * E, device=DEV, dtype=dt) * 0.02 + ).requires_grad_(True) + A2 = (torch.randn(rank * E, IM, device=DEV, dtype=dt) * 0.02).requires_grad_( + True + ) + B2 = (torch.randn(H, rank * E, device=DEV, dtype=dt) * 0.02).requires_grad_( + True + ) + gup = (*peft_lora_to_scattermoe(A1, B1, E, rank), 0.5) + dwn = (*peft_lora_to_scattermoe(A2, B2, E, rank), 0.5) + monkeypatch.setattr(ex, "_has_peft_wrapper", lambda m: True) + monkeypatch.setattr(ex, "_unwrap_experts_lora", lambda m: (m, gup, dwn)) + lora = (A1, B1, A2, B2) + return self, lora + + +def _routing(N, K, E, ep, dt): + g = torch.Generator(device=DEV).manual_seed(0) + idx = torch.randint(0, E, (N, K), device=DEV, generator=g) + remote = torch.rand(N, K, device=DEV, generator=g) < (1 - 1.0 / ep) + remote[:, 0] = False # >=1 valid slot per received token (DeepEP guarantee) + idx = torch.where(remote, torch.full_like(idx, -1), idx) + w = torch.rand(N, K, device=DEV, generator=g).to(dt) + return idx, w + + +@pytest.mark.parametrize("dt", [torch.float32, torch.bfloat16]) +@pytest.mark.parametrize("rank", [0, 16]) +@pytest.mark.parametrize("ep", [2, 4]) +def test_ep_skip_matches_masked(dt, rank, ep, monkeypatch): + E, H, IM, N, K = 32, 512, 256, 512, 8 + self, lora = _module(E, H, IM, dt, rank, monkeypatch) + idx, w = _routing(N, K, E, ep, dt) + safe_idx = torch.where(idx >= 0, idx, torch.zeros_like(idx)) + safe_w = w * (idx >= 0).to(dt) + grad = torch.randn(N, H, device=DEV, dtype=dt) + + def run(fn, ii, ww): + x = torch.randn( + N, + H, + device=DEV, + dtype=dt, + generator=torch.Generator(DEV).manual_seed(1), + ).requires_grad_(True) + if lora: + for t in lora: + t.grad = None + fn(self, x, ii, ww).backward(grad) + return x.grad.clone(), [t.grad.clone() for t in lora] if lora else [] + + # forward equality + with torch.no_grad(): + x = torch.randn(N, H, device=DEV, dtype=dt) + o_m = scattermoe_experts_forward(self, x, safe_idx, safe_w) + o_s = scattermoe_experts_forward_ep(self, x, idx, w) + tol = 1e-4 if dt == torch.float32 else 4e-2 + assert torch.allclose(o_s, o_m, rtol=tol, atol=tol), (o_s - o_m).abs().max().item() + + dx_m, gl_m = run(scattermoe_experts_forward, safe_idx, safe_w) + dx_s, gl_s = run(scattermoe_experts_forward_ep, idx, w) + assert torch.allclose(dx_s, dx_m, rtol=tol, atol=tol), ( + (dx_s - dx_m).abs().max().item() + ) + for a, b in zip(gl_s, gl_m, strict=True): + assert torch.allclose(a, b, rtol=tol, atol=tol), (a - b).abs().max().item() + + +def test_ep_skip_handles_all_sentinel_token(): + """A row with every slot remote (-1) contributes nothing and must not crash.""" + E, H, IM, N, K = 8, 256, 128, 16, 4 + self, _ = _module(E, H, IM, torch.float32, 0, None) + idx = torch.full((N, K), -1, device=DEV, dtype=torch.long) + idx[1:, 0] = 0 # token 0 fully remote; others have one valid slot + w = torch.rand(N, K, device=DEV) + out = scattermoe_experts_forward_ep(self, torch.randn(N, H, device=DEV), idx, w) + assert out.shape == (N, H) + assert torch.equal(out[0], torch.zeros(H, device=DEV)) diff --git a/tests/integrations/kernels/scattermoe_lora/test_gptoss_layout.py b/tests/integrations/kernels/scattermoe_lora/test_gptoss_layout.py new file mode 100644 index 0000000000..b344cc6d61 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_gptoss_layout.py @@ -0,0 +1,185 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""scattermoe Triton path for gpt_oss-style experts (transposed [E,H,2I] weights, +interleaved gate/up, per-expert bias, clamped sigmoid-GLU). + +Validated against an eager reference: output and every gradient must match. The LoRA +fusion is the same ``scatter2scatter_lora`` the standard path uses, so the eager +reference folds the kernel's delta ``ΔW_e = scaling * A_e^T @ W_B[e]`` into W_eff. +TF32 is disabled so the fp32 comparison is tight (the kernel GEMMs otherwise use TF32). +""" + +from types import SimpleNamespace + +import pytest +import torch + +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") + +import axolotl.integrations.kernels.libs.scattermoe_lora.experts as ex # noqa: E402 +import axolotl.integrations.kernels.libs.scattermoe_lora.kernels.lora_ops as _lo # noqa: E402 +import axolotl.integrations.kernels.libs.scattermoe_lora.kernels.ops as _ops # noqa: E402 +from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( # noqa: E402 + scattermoe_experts_forward, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.layers import ( # noqa: E402 + peft_lora_B_to_scattermoe, + peft_lora_to_scattermoe, +) + +DEV = "cuda" +ALPHA, LIMIT = 1.702, 7.0 +E, H, IM, N, K, R, SC = 8, 256, 128, 64, 4, 16, 0.5 + + +@pytest.fixture(autouse=True) +def _no_tf32(): + old = torch.backends.cuda.matmul.allow_tf32 + torch.backends.cuda.matmul.allow_tf32 = False + a, b = _lo.ALLOW_TF32, _ops.ALLOW_TF32 + _lo.ALLOW_TF32 = _ops.ALLOW_TF32 = False + yield + torch.backends.cuda.matmul.allow_tf32 = old + _lo.ALLOW_TF32, _ops.ALLOW_TF32 = a, b + + +def _glu(gate_up): + gate, up = gate_up[..., ::2], gate_up[..., 1::2] + gate = gate.clamp(max=LIMIT) + up = up.clamp(min=-LIMIT, max=LIMIT) + return (up + 1) * (gate * torch.sigmoid(gate * ALPHA)) + + +def _module(dt): + def mk(*s): + return (torch.randn(*s, device=DEV, dtype=dt) * 0.02).requires_grad_(True) + + return SimpleNamespace( + num_experts=E, + gate_up_proj=mk(E, H, 2 * IM), + gate_up_proj_bias=mk(E, 2 * IM), + down_proj=mk(E, IM, H), + down_proj_bias=mk(E, H), + alpha=ALPHA, + limit=LIMIT, + is_transposed=True, + is_concatenated=False, + has_bias=True, + has_gate=True, + ) + + +def _eager(x, m, idx, w, weff=None): + gup, dp = weff if weff else (m.gate_up_proj, m.down_proj) + fe = idx.reshape(-1) + xg = x.repeat_interleave(K, dim=0) + gate_up = torch.bmm(xg.unsqueeze(1), gup[fe]).squeeze(1) + m.gate_up_proj_bias[fe] + h = _glu(gate_up) + o = torch.bmm(h.unsqueeze(1), dp[fe]).squeeze(1) + m.down_proj_bias[fe] + o = o * w.reshape(-1, 1) + tok = torch.arange(x.size(0), device=DEV).repeat_interleave(K) + return torch.zeros(x.size(0), H, device=DEV, dtype=x.dtype).index_add_(0, tok, o) + + +def _rel(a, b): + return (a - b).abs().max().item() / max(b.abs().max().item(), 1e-6) + + +@pytest.mark.parametrize("dt", [torch.float32, torch.bfloat16]) +def test_gptoss_base_matches_eager(dt): + m = _module(dt) + g = torch.Generator(device=DEV).manual_seed(0) + idx = torch.randint(0, E, (N, K), device=DEV, generator=g) + w = torch.rand(N, K, device=DEV, generator=g).to(dt) + grad = torch.randn(N, H, device=DEV, dtype=dt) + ps = [m.gate_up_proj, m.gate_up_proj_bias, m.down_proj, m.down_proj_bias] + + def run(eager): + for p in ps: + p.grad = None + x = torch.randn( + N, H, device=DEV, dtype=dt, generator=torch.Generator(DEV).manual_seed(3) + ).requires_grad_(True) + out = ( + _eager(x, m, idx, w) if eager else scattermoe_experts_forward(m, x, idx, w) + ) + out.backward(grad) + return out.detach(), x.grad, [p.grad.clone() for p in ps] + + ok, dxk, gk = run(False) + oe, dxe, ge = run(True) + tol = 2e-4 if dt == torch.float32 else 5e-2 + assert _rel(ok, oe) < tol + assert _rel(dxk, dxe) < tol + for a, b in zip(gk, ge, strict=True): + assert _rel(a, b) < tol + + +@pytest.mark.parametrize("dt", [torch.float32, torch.bfloat16]) +def test_gptoss_lora_matches_eager(dt, monkeypatch): + g = torch.Generator(device=DEV).manual_seed(0) + base = SimpleNamespace( + num_experts=E, + gate_up_proj=torch.randn(E, H, 2 * IM, device=DEV, dtype=dt, generator=g) + * 0.02, + gate_up_proj_bias=torch.randn(E, 2 * IM, device=DEV, dtype=dt, generator=g) + * 0.02, + down_proj=torch.randn(E, IM, H, device=DEV, dtype=dt, generator=g) * 0.02, + down_proj_bias=torch.randn(E, H, device=DEV, dtype=dt, generator=g) * 0.02, + alpha=ALPHA, + limit=LIMIT, + is_transposed=True, + is_concatenated=False, + has_bias=True, + has_gate=True, + ) + + def mk(*s): + return ( + torch.randn(*s, device=DEV, dtype=dt, generator=g) * 0.05 + ).requires_grad_(True) + + pA1, pB1 = mk(R * E, H), mk(2 * IM, R * E) + pA2, pB2 = mk(R * E, IM), mk(H, R * E) + lora = [pA1, pB1, pA2, pB2] + gup_l = (*peft_lora_to_scattermoe(pA1, pB1, E, R), SC) + dwn_l = (*peft_lora_to_scattermoe(pA2, pB2, E, R), SC) + monkeypatch.setattr(ex, "_has_peft_wrapper", lambda s: True) + monkeypatch.setattr(ex, "_unwrap_experts_lora", lambda s: (s, gup_l, dwn_l)) + + # eager W_eff: ΔW_e = scaling * A_e^T @ W_B[e], W_B = lora_B.T.reshape(E,R,out) + A1 = pA1.reshape(E, R, H) + WB1 = peft_lora_B_to_scattermoe(pB1, E, R).t().reshape(E, R, 2 * IM) + A2 = pA2.reshape(E, R, IM) + WB2 = peft_lora_B_to_scattermoe(pB2, E, R).t().reshape(E, R, H) + gup_eff = base.gate_up_proj + SC * torch.bmm(A1.transpose(1, 2), WB1) + dp_eff = base.down_proj + SC * torch.bmm(A2.transpose(1, 2), WB2) + + idx = torch.randint(0, E, (N, K), device=DEV, generator=g) + w = torch.rand(N, K, device=DEV, generator=g).to(dt) + grad = torch.randn(N, H, device=DEV, dtype=dt) + + def run(eager): + for t in lora: + t.grad = None + x = torch.randn( + N, H, device=DEV, dtype=dt, generator=torch.Generator(DEV).manual_seed(3) + ).requires_grad_(True) + out = ( + _eager(x, base, idx, w, weff=(gup_eff, dp_eff)) + if eager + else scattermoe_experts_forward(base, x, idx, w) + ) + out.backward(grad) + return out.detach(), x.grad, [t.grad.clone() for t in lora] + + ok, dxk, gk = run(False) + oe, dxe, ge = run(True) + tol = 3e-4 if dt == torch.float32 else 5e-2 + assert _rel(ok, oe) < tol + assert _rel(dxk, dxe) < tol + for a, b in zip(gk, ge, strict=True): + assert _rel(a, b) < tol + assert a.abs().sum() > 0 # LoRA grads actually populated diff --git a/tests/integrations/kernels/scattermoe_lora/test_mxfp4_experts_forward.py b/tests/integrations/kernels/scattermoe_lora/test_mxfp4_experts_forward.py new file mode 100644 index 0000000000..b8aadc3ad0 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_mxfp4_experts_forward.py @@ -0,0 +1,216 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""scattermoe_experts_forward routes base MXFP4 + LoRA through the fused MX kernel +(no bf16 dequant of the frozen base) and is bit-for-bit unaffected for non-MXFP4 models. + +The fused path keeps the packed 4-bit weights and dequantizes inside the kernel K-loop. +Validated vs a reference = the same forward on the dequantized weights, within MX +rounding tolerance, on output / dX / the active-expert slice of LoRA dA, dB. +""" + +from types import SimpleNamespace + +import pytest +import torch + +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") +MXTensor = pytest.importorskip( + "torchao.prototype.mx_formats.mx_tensor", reason="torchao required" +).MXTensor + +import axolotl.integrations.kernels.libs.scattermoe_lora.experts as ex # noqa: E402 +from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( # noqa: E402 + scattermoe_experts_forward, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.parallel_experts import ( # noqa: E402 + flatten_sort_count, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.selective_dequant import ( # noqa: E402 + get_active_experts, +) + +DEV = "cuda" +E, H, IM, N, K, R, SC = 8, 1024, 512, 512, 4, 16, 0.5 + + +def _module(gu, dn): + return SimpleNamespace( + num_experts=E, + gate_up_proj=gu, + down_proj=dn, + act_fn=torch.nn.functional.silu, + is_transposed=False, + is_concatenated=True, + has_bias=False, + has_gate=True, + ) + + +def test_mxfp4_forward_uses_fused_kernel_matches_dequant(monkeypatch): + dt = torch.bfloat16 + g = torch.Generator(device=DEV).manual_seed(0) + + def mx(W): + return MXTensor.to_mx(W, elem_dtype=torch.float4_e2m1fn_x2, block_size=32) + + gu_mx = mx(torch.randn(E, 2 * IM, H, device=DEV, dtype=dt, generator=g) * 0.1) + dn_mx = mx(torch.randn(E, H, IM, device=DEV, dtype=dt, generator=g) * 0.1) + gu_deq, dn_deq = ( + gu_mx.dequantize(dt).contiguous(), + dn_mx.dequantize(dt).contiguous(), + ) + + def mk(*s): + return ( + torch.randn(*s, device=DEV, dtype=dt, generator=g) * 0.05 + ).requires_grad_(True) + + A1, B1, A2, B2 = mk(R * E, H), mk(2 * IM, R * E), mk(R * E, IM), mk(H, R * E) + lora = [A1, B1, A2, B2] + monkeypatch.setattr(ex, "_has_peft_wrapper", lambda s: True) + monkeypatch.setattr( + ex, "_unwrap_experts_lora", lambda s: (s, (A1, B1, SC), (A2, B2, SC)) + ) + + idx = torch.randint(0, E, (N, K), device=DEV, generator=g) + w = torch.rand(N, K, device=DEV, generator=g).to(dt) + sei, _, _ = flatten_sort_count(idx, E) + active = get_active_experts(sei, E) + row = (active.long()[:, None] * R + torch.arange(R, device=DEV)[None, :]).reshape( + -1 + ) + grad = torch.randn(N, H, device=DEV, dtype=dt, generator=g) + + def run(m): + for t in lora: + t.grad = None + x = torch.randn( + N, H, device=DEV, dtype=dt, generator=torch.Generator(DEV).manual_seed(7) + ).requires_grad_(True) + scattermoe_experts_forward(m, x, idx, w).backward(grad) + return x.grad.detach(), [t.grad.clone() for t in lora] + + dx_mx, gl_mx = run(_module(gu_mx, dn_mx)) # fused MX path + dx_bf, gl_bf = run(_module(gu_deq, dn_deq)) # bf16 dequant reference + + def rel(a, b): + return (a - b).float().abs().max().item() / max( + b.float().abs().max().item(), 1e-6 + ) + + assert rel(dx_mx, dx_bf) < 6e-2 + for i, slc in ( + (0, row), + (1, (slice(None), row)), + (2, row), + (3, (slice(None), row)), + ): + assert rel(gl_mx[i][slc], gl_bf[i][slc]) < 6e-2, f"lora grad {i}" + assert torch.isfinite(gl_mx[i]).all() + + +def test_mxfp4_ep_matches_dequant(monkeypatch): + """Fused MXFP4 through the EP (sentinel-skip) forward matches the bf16-dequant ref, + and _sonicmoe_local routes there on a device the sonic-moe kernel can't run.""" + from axolotl.integrations.expert_parallel.experts_fn import _sonicmoe_local + from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( + scattermoe_experts_forward_ep, + ) + from axolotl.integrations.kernels.libs.sonicmoe.experts import ( + _sonicmoe_kernel_supported, + ) + + dt = torch.bfloat16 + g = torch.Generator(device=DEV).manual_seed(0) + + def mx(W): + return MXTensor.to_mx(W, elem_dtype=torch.float4_e2m1fn_x2, block_size=32) + + gu_mx = mx(torch.randn(E, 2 * IM, H, device=DEV, dtype=dt, generator=g) * 0.1) + dn_mx = mx(torch.randn(E, H, IM, device=DEV, dtype=dt, generator=g) * 0.1) + gu_deq, dn_deq = ( + gu_mx.dequantize(dt).contiguous(), + dn_mx.dequantize(dt).contiguous(), + ) + + def mk(*s): + return ( + torch.randn(*s, device=DEV, dtype=dt, generator=g) * 0.05 + ).requires_grad_(True) + + A1, B1, A2, B2 = mk(R * E, H), mk(2 * IM, R * E), mk(R * E, IM), mk(H, R * E) + lora = [A1, B1, A2, B2] + monkeypatch.setattr(ex, "_has_peft_wrapper", lambda s: True) + monkeypatch.setattr( + ex, "_unwrap_experts_lora", lambda s: (s, (A1, B1, SC), (A2, B2, SC)) + ) + + # routing with -1 remote sentinels (post-DeepEP-dispatch shape) + idx = torch.randint(0, E, (N, K), device=DEV, generator=g) + remote = torch.rand(N, K, device=DEV, generator=g) < 0.5 + remote[:, 0] = False + idx = torch.where(remote, torch.full_like(idx, -1), idx) + w = torch.rand(N, K, device=DEV, generator=g).to(dt) + active = torch.unique(torch.sort(idx.reshape(-1)[idx.reshape(-1) >= 0]).values) + row = (active.long()[:, None] * R + torch.arange(R, device=DEV)[None, :]).reshape( + -1 + ) + grad = torch.randn(N, H, device=DEV, dtype=dt, generator=g) + + def run(m): + for t in lora: + t.grad = None + x = torch.randn( + N, H, device=DEV, dtype=dt, generator=torch.Generator(DEV).manual_seed(7) + ).requires_grad_(True) + scattermoe_experts_forward_ep(m, x, idx, w).backward(grad) + return x.grad.detach(), [t.grad.clone() for t in lora] + + dx_mx, gl_mx = run(_module(gu_mx, dn_mx)) + dx_bf, gl_bf = run(_module(gu_deq, dn_deq)) + + def rel(a, b): + return (a - b).float().abs().max().item() / max( + b.float().abs().max().item(), 1e-6 + ) + + assert rel(dx_mx, dx_bf) < 6e-2 + for i, slc in ( + (0, row), + (1, (slice(None), row)), + (2, row), + (3, (slice(None), row)), + ): + assert rel(gl_mx[i][slc], gl_bf[i][slc]) < 6e-2, f"lora grad {i}" + + if ( + not _sonicmoe_kernel_supported() + ): # e.g. sm_120: sonicmoe+EP falls back to scattermoe + out = _sonicmoe_local( + _module(gu_mx, dn_mx), torch.randn(N, H, device=DEV, dtype=dt), idx, w + ) + assert out.shape == (N, H) and torch.isfinite(out).all() + + +def test_mxfp4_without_lora_falls_back_to_dequant(monkeypatch): + """MXFP4 base with no LoRA must not hit the fused (LoRA-only) MX kernel.""" + dt = torch.bfloat16 + g = torch.Generator(device=DEV).manual_seed(0) + + def mx(W): + return MXTensor.to_mx(W, elem_dtype=torch.float4_e2m1fn_x2, block_size=32) + + m = _module( + mx(torch.randn(E, 2 * IM, H, device=DEV, dtype=dt, generator=g) * 0.1), + mx(torch.randn(E, H, IM, device=DEV, dtype=dt, generator=g) * 0.1), + ) + monkeypatch.setattr(ex, "_has_peft_wrapper", lambda s: False) # no LoRA + x = torch.randn(N, H, device=DEV, dtype=dt) + idx = torch.randint(0, E, (N, K), device=DEV) + w = torch.rand(N, K, device=DEV).to(dt) + out = scattermoe_experts_forward( + m, x, idx, w + ) # dequant-on-cast path, must not raise + assert out.shape == (N, H) and torch.isfinite(out).all() diff --git a/tests/integrations/kernels/scattermoe_lora/test_nvfp4_experts_forward.py b/tests/integrations/kernels/scattermoe_lora/test_nvfp4_experts_forward.py new file mode 100644 index 0000000000..18b63c23bf --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_nvfp4_experts_forward.py @@ -0,0 +1,138 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""scattermoe routes base NVFP4 + LoRA through the fused kernel (no bf16 dequant). + +NVFP4 shares the FP4 E2M1 packing with MXFP4 but scales per 16-element block with an +E4M3 (fp8) value (times an optional per-tensor scale). It reuses the MXWeights container +with ``scale_is_linear=True`` (the E4M3 block scale pre-folded with the per-tensor scale) +and the same fused Triton kernel with a linear-scale branch + block-16. Validated vs a +reference = the same forward on the dequantized weights, within NV rounding tolerance, +through both the standard and EP (sentinel-skip) forwards. +""" + +from types import SimpleNamespace + +import pytest +import torch + +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") +NVFP4Tensor = pytest.importorskip( + "torchao.prototype.mx_formats.nvfp4_tensor", reason="torchao required" +).NVFP4Tensor + +import axolotl.integrations.kernels.libs.scattermoe_lora.experts as ex # noqa: E402 +from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( # noqa: E402 + scattermoe_experts_forward, + scattermoe_experts_forward_ep, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.selective_dequant import ( # noqa: E402 + is_mxfp4_param, + is_nvfp4_param, +) + +DEV = "cuda" +E, H, IM, N, K, R, SC = 8, 1024, 512, 512, 4, 16, 0.5 + + +def _module(gu, dn): + return SimpleNamespace( + num_experts=E, + gate_up_proj=gu, + down_proj=dn, + act_fn=torch.nn.functional.silu, + is_transposed=False, + is_concatenated=True, + has_bias=False, + has_gate=True, + ) + + +def _rel(a, b): + return (a - b).float().abs().max().item() / max(b.float().abs().max().item(), 1e-6) + + +def test_is_nvfp4_param_disjoint_from_mxfp4(): + nv = NVFP4Tensor.to_nvfp4( + torch.randn(E, IM, H, device=DEV, dtype=torch.bfloat16), block_size=16 + ) + assert is_nvfp4_param(nv) and not is_mxfp4_param(nv) + assert not is_nvfp4_param(torch.randn(2, 2, device=DEV)) # plain tensor + + +def _routing_std(g): + idx = torch.randint(0, E, (N, K), device=DEV, generator=g) + return idx, torch.rand(N, K, device=DEV, generator=g).to(torch.bfloat16) + + +def _routing_ep(g): + idx = torch.randint(0, E, (N, K), device=DEV, generator=g) + remote = torch.rand(N, K, device=DEV, generator=g) < 0.5 + remote[:, 0] = False # >=1 valid slot per token (DeepEP guarantee) + idx = torch.where(remote, torch.full_like(idx, -1), idx) + return idx, torch.rand(N, K, device=DEV, generator=g).to(torch.bfloat16) + + +@pytest.mark.parametrize( + "fwd,routing", + [ + (scattermoe_experts_forward, _routing_std), + (scattermoe_experts_forward_ep, _routing_ep), + ], +) +def test_nvfp4_fused_matches_dequant(fwd, routing, monkeypatch): + dt = torch.bfloat16 + g = torch.Generator(device=DEV).manual_seed(0) + gu_nv = NVFP4Tensor.to_nvfp4( + torch.randn(E, 2 * IM, H, device=DEV, dtype=dt, generator=g) * 0.1, + block_size=16, + ) + dn_nv = NVFP4Tensor.to_nvfp4( + torch.randn(E, H, IM, device=DEV, dtype=dt, generator=g) * 0.1, block_size=16 + ) + gu_deq, dn_deq = ( + gu_nv.dequantize(dt).contiguous(), + dn_nv.dequantize(dt).contiguous(), + ) + + def mk(*s): + return ( + torch.randn(*s, device=DEV, dtype=dt, generator=g) * 0.05 + ).requires_grad_(True) + + A1, B1, A2, B2 = mk(R * E, H), mk(2 * IM, R * E), mk(R * E, IM), mk(H, R * E) + lora = [A1, B1, A2, B2] + monkeypatch.setattr(ex, "_has_peft_wrapper", lambda s: True) + monkeypatch.setattr( + ex, "_unwrap_experts_lora", lambda s: (s, (A1, B1, SC), (A2, B2, SC)) + ) + + idx, w = routing(g) + valid = idx.reshape(-1) >= 0 + active = torch.unique(torch.sort(idx.reshape(-1)[valid]).values) + row = (active.long()[:, None] * R + torch.arange(R, device=DEV)[None, :]).reshape( + -1 + ) + grad = torch.randn(N, H, device=DEV, dtype=dt, generator=g) + + def run(m): + for t in lora: + t.grad = None + x = torch.randn( + N, H, device=DEV, dtype=dt, generator=torch.Generator(DEV).manual_seed(7) + ).requires_grad_(True) + fwd(m, x, idx, w).backward(grad) + return x.grad.detach(), [t.grad.clone() for t in lora] + + dx_nv, gl_nv = run(_module(gu_nv, dn_nv)) # fused NVFP4 (no dequant) + dx_bf, gl_bf = run(_module(gu_deq, dn_deq)) # bf16 dequant reference + assert _rel(dx_nv, dx_bf) < 6e-2 + for i, slc in ( + (0, row), + (1, (slice(None), row)), + (2, row), + (3, (slice(None), row)), + ): + assert _rel(gl_nv[i][slc], gl_bf[i][slc]) < 6e-2, f"lora grad {i}" + assert torch.isfinite(gl_nv[i]).all() From ed761c6ddad37b5a55c41cc2680708ce7b8fde6f Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Sun, 7 Jun 2026 15:05:59 -0400 Subject: [PATCH 1343/1405] build docker uv images w torch 2.11 and 2.12 (#3715) --- .github/workflows/main.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b5c575d336..a73d742294 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -126,6 +126,18 @@ jobs: pytorch: 2.10.0 axolotl_extras: platforms: "linux/amd64,linux/arm64" + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.12" + pytorch: 2.11.0 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.12" + pytorch: 2.12.0 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" runs-on: axolotl-gpu-runner steps: - name: Checkout @@ -279,6 +291,18 @@ jobs: pytorch: 2.10.0 axolotl_extras: platforms: "linux/amd64,linux/arm64" + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.12" + pytorch: 2.11.0 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" + - cuda: 130 + cuda_version: 13.0.0 + python_version: "3.12" + pytorch: 2.12.0 + axolotl_extras: + platforms: "linux/amd64,linux/arm64" runs-on: axolotl-gpu-runner steps: - name: Checkout From b99ae0921338ca747990bfef4b6daa9258bc60b9 Mon Sep 17 00:00:00 2001 From: thad0ctor Date: Sun, 7 Jun 2026 17:50:26 -0700 Subject: [PATCH 1344/1405] perf: expand torch.compile coverage to 4bit dequant (#3677) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: make NF4 dequantize torch.compile-safe via local custom_op Wrap the Unsloth-derived NF4 dequant fast path in a torch.library.custom_op (axolotl::nf4_dequantize) with a register_fake impl. dequantize() branches on torch.compiler.is_compiling(): eager calls the ctypes body directly (zero op-dispatch overhead), while tracing dispatches through the opaque op so Dynamo can compile around it without graph-breaking on ctypes.c_int(...) or the foreign-function calls. Previously, torch.compile on any QLoRA model crashed with ctypes.ArgumentError the first time a Linear4bit forward fell into the fast path. Also: - Drop dead legacy-list quant_state branch (not produced by bnb 0.40+). - Drop the unused `out=` parameter (no production callers; grep-confirmed). - Drop the HAS_CUDA_STREAM version gate (axolotl pins bnb >> 0.43.3). - Rewrite tests/e2e/kernels/test_quantize.py to use real bnb.functional.quantize_4bit fixtures instead of synthetic ones whose blocksize=32 was silently invalid (the old ctypes path skipped validation). Adds compile regression tests for both nested (double-quant) and non-nested paths. Validated: - Eager bench vs main at 1024², 4096², 4096x11008 within ±2% noise. - Unit tests pass on torch 2.11.0 + bnb 0.49.1. - End-to-end: Qwen3.5-0.8B QLoRA train (lora_mlp/qkv/o_kernel=true, exercises dequantize in autograd Function fwd+bwd) + merge-lora pipeline succeed. * review: add docstrings to internal helpers + tighten transposed test Addresses CodeRabbit nits on PR #25: - Docstrings on _nf4_dequantize_op and its register_fake impl to lift docstring coverage above the 80% threshold. - test_dequantize_transposed now asserts value equality vs bnb.functional.dequantize_4bit(...).t() in addition to shape, so a layout/content regression would actually fail the test. Skipped CodeRabbit's fullgraph=True nit: tracing under fullgraph=True currently fails on both main and branch (same _SimpleCData.__new__ trace error before reaching the is_compiling() branch). Investigating is out of scope for this PR; default-mode torch.compile is what the fix targets and is verified working. Skipped CodeRabbit's CUDA_STREAM caching concern: the cache pattern is byte-identical to main and removing it costs +5-21% at the kernel level. Stream-management refactor is a separate concern. * docs: trim docstrings/comments to one-line WHY only - Module + dequantize() docstrings condensed to single line. - CUDA_STREAM comment dropped benchmark numbers and version reference. - Removed task-history language ("no longer crashes", "vs the prior implementation", "Regression:") from docstrings; comments now describe permanent invariants, not the PR diff. * style: ruff format dequantize() call arg lists per pre-commit * review: key CUDA_STREAM cache by device Per CodeRabbit: the single-global stream cache could in principle return the wrong device's stream for a multi-device-single-process caller. In practice axolotl always runs one process per CUDA device, so this is defensive rather than fixing a known reproducer. Using `setdefault` keeps the cache race benign under threading (worst case: redundant current_stream() call) and drops the `global` keyword since the dict is mutated in place, never rebound. * fix broken MX tests from transformers 5.8.1 upgrade --------- Co-authored-by: Wing Lian --- src/axolotl/kernels/quantize.py | 270 +++++++++++++---------------- tests/e2e/kernels/test_quantize.py | 138 ++++++--------- 2 files changed, 179 insertions(+), 229 deletions(-) diff --git a/src/axolotl/kernels/quantize.py b/src/axolotl/kernels/quantize.py index ff564fecc6..eb5c08a4c1 100644 --- a/src/axolotl/kernels/quantize.py +++ b/src/axolotl/kernels/quantize.py @@ -1,18 +1,102 @@ """Dequantization utilities for `bitsandbytes` and FP8 integration.""" import ctypes +from typing import List import bitsandbytes as bnb import torch from bitsandbytes.functional import QuantState, get_ptr -from packaging.version import Version cdequantize_blockwise_fp32 = bnb.functional.lib.cdequantize_blockwise_fp32 cdequantize_blockwise_fp16_nf4 = bnb.functional.lib.cdequantize_blockwise_fp16_nf4 cdequantize_blockwise_bf16_nf4 = bnb.functional.lib.cdequantize_blockwise_bf16_nf4 -CUDA_STREAM: torch.cuda.Stream | None = None -HAS_CUDA_STREAM: bool = Version(bnb.__version__) > Version("0.43.3") +# Cached per-device: per-call current_stream() measurably slows this hot path. +CUDA_STREAM: dict[torch.device, torch.cuda.Stream] = {} + + +def _ctypes_nf4_dequant( + W: torch.Tensor, + absmax: torch.Tensor, + code2: torch.Tensor, + absmax2: torch.Tensor, + offset: torch.Tensor, + blocksize: int, + blocksize2: int, + shape, + dtype: torch.dtype, +) -> torch.Tensor: + """Direct-ctypes NF4 double-dequant body (Unsloth-derived fast path).""" + target_device = W.device + out = torch.empty(tuple(shape), dtype=dtype, device=target_device) + n_elements_absmax = absmax.numel() + out_absmax = torch.empty( + n_elements_absmax, dtype=torch.float32, device=target_device + ) + + stream = CUDA_STREAM.get(target_device) + if stream is None: + stream = CUDA_STREAM.setdefault( + target_device, torch.cuda.current_stream(target_device) + ) + + cdequantize_blockwise_fp32( + get_ptr(code2), + get_ptr(absmax), + get_ptr(absmax2), + get_ptr(out_absmax), + ctypes.c_int(blocksize2), + ctypes.c_int(n_elements_absmax), + stream, + ) + out_absmax += offset + + fx = ( + cdequantize_blockwise_fp16_nf4 + if dtype == torch.float16 + else cdequantize_blockwise_bf16_nf4 + ) + fx( + get_ptr(None), + get_ptr(W), + get_ptr(out_absmax), + get_ptr(out), + ctypes.c_int(blocksize), + ctypes.c_int(out.numel()), + stream, + ) + + # bnb convention: leading-dim-1 packed weight signals a transposed view. + if W.shape[0] == 1: + return out.t() + return out + + +@torch.library.custom_op("axolotl::nf4_dequantize", mutates_args=()) +def _nf4_dequantize_op( + W: torch.Tensor, + absmax: torch.Tensor, + code2: torch.Tensor, + absmax2: torch.Tensor, + offset: torch.Tensor, + blocksize: int, + blocksize2: int, + shape: List[int], + dtype: torch.dtype, +) -> torch.Tensor: + """Opaque-to-Dynamo wrapper around the direct-ctypes NF4 dequant body.""" + return _ctypes_nf4_dequant( + W, absmax, code2, absmax2, offset, blocksize, blocksize2, shape, dtype + ) + + +@_nf4_dequantize_op.register_fake +def _(W, absmax, code2, absmax2, offset, blocksize, blocksize2, shape, dtype): + """FakeTensor shape/dtype inference for the registered op (trace-time only).""" + out = torch.empty(tuple(shape), dtype=dtype, device=W.device) + if W.shape[0] == 1: + return out.t() + return out def dequantize_fp8( @@ -20,17 +104,7 @@ def dequantize_fp8( scale_inv: torch.Tensor, dtype: torch.dtype = torch.bfloat16, ) -> torch.Tensor: - """Dequantize FP8 block-quantized weights: W_dequant = W_fp8 * scale_inv. - - Args: - W: FP8 weight tensor [out_features, in_features] in float8_e4m3fn. - scale_inv: Per-block inverse scale [ceil(out/block), ceil(in/block)] - or per-tensor scalar. - dtype: Output dtype (default bf16). - - Returns: - Dequantized tensor in the specified dtype. - """ + """Dequantize FP8 block-quantized weights: W_dequant = W_fp8 * scale_inv.""" W_float = W.to(dtype) if scale_inv.numel() == 1: return W_float * scale_inv.to(dtype) @@ -38,14 +112,12 @@ def dequantize_fp8( sr, sc = scale_inv.shape br = W.shape[0] // sr bc = W.shape[1] // sc - # If dimensions are exactly divisible, use fast reshape path if sr * br == W.shape[0] and sc * bc == W.shape[1]: return ( W_float.reshape(sr, br, sc, bc) * scale_inv[:, None, :, None].to(dtype) ).reshape(W.shape) - # Tail-block handling: compute actual block size (ceil division), - # tile scale_inv to cover full shape, then crop to W's dimensions - br_ceil = -(-W.shape[0] // sr) # ceil(rows / scale_rows) = block_size + # Tail blocks: ceil-div the block size, tile scale_inv, crop to W. + br_ceil = -(-W.shape[0] // sr) bc_ceil = -(-W.shape[1] // sc) scale_expanded = ( scale_inv.to(dtype) @@ -58,145 +130,51 @@ def dequantize_fp8( def dequantize( W: torch.Tensor, - quant_state: QuantState | list | torch.Tensor | None = None, - out: torch.Tensor | None = None, + quant_state: QuantState | torch.Tensor | None = None, ) -> torch.Tensor: - """ - Fast NF4 dequantization using `bitsandbytes` CUDA kernels. - - Performs efficient dequantization of weights from NF4 format using `bitsandbytes`' - optimized CUDA implementations. Supports both legacy list and new `QuantState` - formats. - - Args: - W: Quantized weight tensor to dequantize - quant_state: Quantization state containing metadata needed for - dequantization. Can be either a `QuantState` object or legacy list format. - If None, returns `W` unchanged. - out: Optional output tensor for storing dequantized results. Must match - expected shape and dtype if provided. - - Returns: - Dequantized tensor in the specified dtype (fp16 or bf16). Will be transposed if - input `W` was transposed. - - Raises: - AssertionError: If provided output tensor doesn't match expected shape / dtype. - - Note: - Uses CUDA streams for better performance when available in newer `bitsandbytes` - versions (>0.43.3). - """ + """NF4 / FP8 dequantization; under `torch.compile` NF4 dispatches via `torch.ops.axolotl.nf4_dequantize`.""" if quant_state is None: return W - # FP8 path: quant_state is actually scale_inv tensor if W.dtype == torch.float8_e4m3fn: scale_inv = quant_state - # Caller may pass W.t() (non-contiguous) — dequantize in original - # layout then transpose back so the result shape matches the input. + # Caller may pass W.t() (non-contiguous); dequant in original layout, transpose back. if not W.is_contiguous() and W.dim() == 2: return dequantize_fp8(W.t(), scale_inv).t() return dequantize_fp8(W, scale_inv) - # Get the target device from input tensor W - target_device = W.device + # Non-double-quant: fall back to bnb's wrapper (rare in axolotl QLoRA). + if quant_state.offset is None or quant_state.state2 is None: + return bnb.functional.dequantize_4bit(W, quant_state, quant_type="nf4") - # Extract quantization state - if not isinstance(quant_state, list): - # New style quant_state class - # Non-double-quantized models have offset=None and state2=None - if quant_state.offset is None or quant_state.state2 is None: - # Fall back to bitsandbytes standard dequantize - return bnb.functional.dequantize_4bit(W, quant_state, quant_type="nf4") - absmax = quant_state.absmax.to(target_device) - shape = quant_state.shape - dtype = quant_state.dtype - blocksize = quant_state.blocksize - offset = quant_state.offset.to(target_device) - state2 = quant_state.state2 - absmax2 = state2.absmax.to(target_device) - code2 = state2.code.to(target_device) - blocksize2 = state2.blocksize - else: - # Legacy list format - absmax, shape, dtype, blocksize, compressed_stats, _, _ = quant_state - absmax = absmax.to(target_device) - offset, state2 = compressed_stats - offset = offset.to(target_device) - absmax2, code2, blocksize2, _, _, _, _ = state2 - absmax2 = absmax2.to(target_device) - code2 = code2.to(target_device) - - # Setup output tensor on the same device as input - if out is None: - out = torch.empty(shape, dtype=dtype, device=target_device) - else: - assert out.shape == shape and out.dtype == dtype - out = out.to(target_device) - - # Dequantize statistics on the target device - n_elements_absmax: int = absmax.numel() - out_absmax: torch.Tensor = torch.empty( - n_elements_absmax, dtype=torch.float32, device=target_device - ) - ptr_out_absmax: int = get_ptr(out_absmax) - - # Use CUDA stream if available - if HAS_CUDA_STREAM: - global CUDA_STREAM - if CUDA_STREAM is None: - CUDA_STREAM = torch.cuda.current_stream(target_device) - - cdequantize_blockwise_fp32( - get_ptr(code2), - get_ptr(absmax), - get_ptr(absmax2), - ptr_out_absmax, - ctypes.c_int(blocksize2), - ctypes.c_int(n_elements_absmax), - CUDA_STREAM, - ) - else: - cdequantize_blockwise_fp32( - get_ptr(code2), - get_ptr(absmax), - get_ptr(absmax2), - ptr_out_absmax, - ctypes.c_int(blocksize2), - ctypes.c_int(n_elements_absmax), + target_device = W.device + state2 = quant_state.state2 + absmax = quant_state.absmax.to(target_device) + code2 = state2.code.to(target_device) + absmax2 = state2.absmax.to(target_device) + offset = quant_state.offset.to(target_device) + + if torch.compiler.is_compiling(): + return torch.ops.axolotl.nf4_dequantize.default( + W, + absmax, + code2, + absmax2, + offset, + quant_state.blocksize, + state2.blocksize, + list(quant_state.shape), + quant_state.dtype, ) - out_absmax += offset - - # Choose appropriate dequantization function - fx = ( - cdequantize_blockwise_fp16_nf4 - if dtype == torch.float16 - else cdequantize_blockwise_bf16_nf4 + return _ctypes_nf4_dequant( + W, + absmax, + code2, + absmax2, + offset, + quant_state.blocksize, + state2.blocksize, + quant_state.shape, + quant_state.dtype, ) - - # Dequantize weights - if HAS_CUDA_STREAM: - fx( - get_ptr(None), - get_ptr(W), - ptr_out_absmax, - get_ptr(out), - ctypes.c_int(blocksize), - ctypes.c_int(out.numel()), - CUDA_STREAM, - ) - else: - fx( - get_ptr(None), - get_ptr(W), - ptr_out_absmax, - get_ptr(out), - ctypes.c_int(blocksize), - ctypes.c_int(out.numel()), - ) - - # Handle transposed data - is_transposed: bool = W.shape[0] == 1 - return out.t() if is_transposed else out diff --git a/tests/e2e/kernels/test_quantize.py b/tests/e2e/kernels/test_quantize.py index 60396584cc..64a48c12d3 100644 --- a/tests/e2e/kernels/test_quantize.py +++ b/tests/e2e/kernels/test_quantize.py @@ -1,102 +1,74 @@ """Tests for quantization utility functions.""" +import bitsandbytes as bnb import torch -from bitsandbytes.functional import QuantState from axolotl.kernels.quantize import dequantize +def _nf4_pair(shape=(64, 64), device="cuda", dtype=torch.float16, double_quant=True): + """Real bnb NF4 (packed_weight, quant_state) pair for the given shape.""" + W = torch.randn(shape, device=device, dtype=dtype) + packed, quant_state = bnb.functional.quantize_4bit( + W, quant_type="nf4", compress_statistics=double_quant + ) + return packed, quant_state + + def test_dequantize_null_state(): - """Test that dequantize returns input unchanged when quant_state is None""" - W = torch.randn(32, 32) + """dequantize returns input unchanged when quant_state is None.""" + W = torch.randn(64, 64) assert torch.equal(dequantize(W, None), W) def test_dequantize_shape_preservation(): - """Test that dequantization preserves expected shapes""" - shape = (32, 32) - W = torch.randn(shape, device="cuda") - - quant_state = QuantState( - absmax=torch.ones(shape[0], device="cuda"), - shape=shape, - code=torch.randint(0, 15, shape, device="cuda"), - dtype=torch.float16, - blocksize=32, - quant_type="nf4", - offset=torch.zeros(shape[0], dtype=torch.int32, device="cuda"), - state2=QuantState( - absmax=torch.ones(shape[0], device="cuda"), - shape=shape, - code=torch.randint(0, 15, shape, device="cuda"), - dtype=torch.float16, - blocksize=32, - quant_type="nf4", - offset=None, - state2=None, - ), - ) - - result = dequantize(W, quant_state) + shape = (128, 64) + packed, quant_state = _nf4_pair(shape) + result = dequantize(packed, quant_state) assert result.shape == shape assert result.dtype == torch.float16 - assert result.device == W.device + assert result.device == packed.device def test_dequantize_transposed(): - """Test that transposed input produces transposed output""" - shape = (32, 32) - W = torch.randn(1, shape[1], device="cuda") # Transposed input - - quant_state = QuantState( - absmax=torch.ones(1), - shape=shape, - code=torch.randint(0, 15, shape), - dtype=torch.float16, - blocksize=32, - quant_type="nf4", - offset=torch.zeros(1, dtype=torch.int32), - state2=QuantState( - absmax=torch.ones(1), - shape=shape, - code=torch.randint(0, 15, shape), - dtype=torch.float16, - blocksize=32, - quant_type="nf4", - offset=None, - state2=None, - ), - ) + """Transposed input → transposed output (values, not just shape).""" + shape = (128, 64) # non-square: catches dim-swap bugs + packed, quant_state = _nf4_pair(shape) + # packed is (4096, 1); packed.t() is (1, 4096), the leading-dim-1 signal. + # bnb dequants to quant_state.shape (128, 64) then returns out.t() → (64, 128). + expected = bnb.functional.dequantize_4bit(packed, quant_state, quant_type="nf4").t() + result = dequantize(packed.t(), quant_state) + assert tuple(result.shape) == (shape[1], shape[0]) + torch.testing.assert_close(result, expected) + + +def test_dequantize_non_nested(): + """Single-quant (compress_statistics=False) falls back to bnb wrapper.""" + shape = (128, 64) + packed, quant_state = _nf4_pair(shape, double_quant=False) + result = dequantize(packed, quant_state) + assert result.shape == shape - result = dequantize(W, quant_state) - assert result.shape[0] == shape[0] - - -def test_dequantize_output_tensor(): - """Test dequantization with provided output tensor""" - shape = (32, 32) - W = torch.randn(shape, device="cuda") - out = torch.empty(shape, dtype=torch.float16, device="cuda") - - quant_state = QuantState( - absmax=torch.ones(shape[0]), - shape=shape, - code=torch.randint(0, 15, shape), - dtype=torch.float16, - blocksize=32, - quant_type="nf4", - offset=torch.zeros(shape[0], dtype=torch.int32), - state2=QuantState( - absmax=torch.ones(shape[0]), - shape=shape, - code=torch.randint(0, 15, shape), - dtype=torch.float16, - blocksize=32, - quant_type="nf4", - offset=None, - state2=None, - ), - ) - result = dequantize(W, quant_state, out=out) - assert result is out +def test_dequantize_torch_compile_nested(): + """NF4 double-quant under torch.compile (the QLoRA hot path).""" + shape = (128, 64) + packed, quant_state = _nf4_pair(shape, double_quant=True) + + eager = dequantize(packed, quant_state) + compiled = torch.compile(dequantize)(packed, quant_state) + + assert compiled.shape == eager.shape + assert compiled.dtype == eager.dtype + torch.testing.assert_close(compiled, eager) + + +def test_dequantize_torch_compile_non_nested(): + """torch.compile also works for the single-quant fallback path.""" + shape = (128, 64) + packed, quant_state = _nf4_pair(shape, double_quant=False) + + eager = dequantize(packed, quant_state) + compiled = torch.compile(dequantize)(packed, quant_state) + + torch.testing.assert_close(compiled, eager) From 77706d1b4fa048697ce5113f4b80cabcffd73973 Mon Sep 17 00:00:00 2001 From: Samuel Larkin Date: Mon, 8 Jun 2026 20:41:25 -0400 Subject: [PATCH 1345/1405] feat: when validating OptimizationValidationMixin, fsdp_version shouldn't be flag as an warning (#3718) --- src/axolotl/utils/schemas/validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 498dc7d7ff..181ddaecbc 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1085,7 +1085,7 @@ def check_fsdp_config_kwargs_prefix(cls, data): if fsdp_config := data.get("fsdp_config"): should_fix = False for key, _ in fsdp_config.items(): - if key.startswith("fsdp_"): + if key.startswith("fsdp_") and key != "fsdp_version": should_fix = True LOG.warning_once( "Configuring FSDP fields with the `fsdp_` prefix is deprecated. " From 2501b62d8559432fd7013ffe7a8326716a13bba5 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 9 Jun 2026 13:31:07 +0700 Subject: [PATCH 1346/1405] fix: tkps grad accu miscalculation (#3699) [skip ci] * fix: tkps grad accu miscalculation * feat: add test --- src/axolotl/core/builders/causal.py | 2 - src/axolotl/core/trainers/base.py | 38 +++++++--- src/axolotl/core/trainers/utils.py | 18 +++++ .../utils/callbacks/tokens_per_second.py | 71 ++----------------- .../utils/callbacks/test_tokens_per_second.py | 50 +++++++++++++ 5 files changed, 102 insertions(+), 77 deletions(-) create mode 100644 tests/utils/callbacks/test_tokens_per_second.py diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index 985414053c..a82c0dd05d 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -72,8 +72,6 @@ def get_callbacks(self): if self.cfg.include_tkps: callbacks.append( TokensPerSecondCallback( - self.cfg.tensor_parallel_size, - self.cfg.context_parallel_size, resume_from_checkpoint=self.cfg.resume_from_checkpoint, ) ) diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 14baf6a88c..6ad80bf9ea 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -7,6 +7,7 @@ import json import math import os +import time from collections import defaultdict from functools import partial, wraps from typing import Any, Callable, Literal, Optional @@ -45,11 +46,16 @@ from axolotl.core.trainers.utils import ( sanitize_kwargs_for_ds_tagging, sanitize_kwargs_for_tagging, + trainable_tokens_per_sec_per_gpu, ) from axolotl.utils import get_not_null from axolotl.utils.bench import get_gpu_memory_usage from axolotl.utils.dict import DictDefault -from axolotl.utils.distributed import is_distributed, is_main_process +from axolotl.utils.distributed import ( + get_world_size, + is_distributed, + is_main_process, +) from axolotl.utils.logging import get_logger from axolotl.utils.samplers import MultipackBatchSampler, get_dataset_lengths @@ -124,6 +130,8 @@ def __init__( self.model_accepts_loss_kwargs = False self.train_data_collator = self.data_collator + self._tkps_prev_trainable: float | None = None + self._tkps_prev_time: float | None = None self._stored_metrics = defaultdict( lambda: defaultdict(lambda: {"values": [], "reduction": "mean"}) ) @@ -401,8 +409,6 @@ def compute_loss( self.state.tokens["trainable"] + trainable_tokens.detach().cpu() ) self.state.tokens["total"] = self.state.tokens["total"] + total_tokens.cpu() - # Store per-step trainable tokens for throughput calculation - self.state.tokens["trainable_tokens"] = trainable_tokens.detach().cpu() # Gemma4 requires mm_token_type_ids during training (even for text-only). # Inject zeros (= text token type) when not provided by the data collator. @@ -755,15 +761,27 @@ def log(self, logs: dict[str, float], start_time: float | None = None) -> None: and train_eval == "train" and hasattr(self.state, "tokens") ): - # each rank will log its own tokens per second - # for logging_steps > 1 we obtain a moving average of this metric - logs["tokens/train_per_sec_per_gpu"] = round( - self.state.last_tokens_per_second.item() / self.args.logging_steps, 2 - ) + if "trainable" in self.state.tokens: + now = time.perf_counter() + curr_trainable = float(self.state.tokens["trainable"].item()) + elapsed = ( + now - self._tkps_prev_time + if self._tkps_prev_time is not None + else 0.0 + ) + rate = trainable_tokens_per_sec_per_gpu( + self._tkps_prev_trainable, + curr_trainable, + get_world_size(), + elapsed, + ) + if rate is not None: + logs["tokens/train_per_sec_per_gpu"] = round(rate, 2) + self._tkps_prev_trainable = curr_trainable + self._tkps_prev_time = now + logs["tokens/trainable"] = int(curr_trainable) if "total" in self.state.tokens: logs["tokens/total"] = int(self.state.tokens["total"].item()) - if "trainable" in self.state.tokens: - logs["tokens/trainable"] = int(self.state.tokens["trainable"].item()) del self._stored_metrics[train_eval] diff --git a/src/axolotl/core/trainers/utils.py b/src/axolotl/core/trainers/utils.py index c6d40cb617..55959c6a41 100644 --- a/src/axolotl/core/trainers/utils.py +++ b/src/axolotl/core/trainers/utils.py @@ -1,6 +1,24 @@ """Utils for Axolotl trainers""" +def trainable_tokens_per_sec_per_gpu( + prev_trainable: float | None, + curr_trainable: float, + world_size: int, + elapsed: float, +) -> float | None: + """Effective per-GPU trainable-token throughput over a logging window. + + ``curr_trainable``/``prev_trainable`` are the cumulative trainable-token + counter (SUM-reduced across all ranks) at this log and the previous one, so + the delta covers every gradient-accumulation microbatch and the elapsed wall + time captures in-window overhead. Returns None when there is no prior window. + """ + if prev_trainable is None or elapsed <= 0: + return None + return (curr_trainable - prev_trainable) / max(1, world_size) / elapsed + + def sanitize_kwargs_for_tagging(tag_names, kwargs=None): if isinstance(tag_names, str): tag_names = [tag_names] diff --git a/src/axolotl/utils/callbacks/tokens_per_second.py b/src/axolotl/utils/callbacks/tokens_per_second.py index 026a1a98f7..d4f8e98c11 100644 --- a/src/axolotl/utils/callbacks/tokens_per_second.py +++ b/src/axolotl/utils/callbacks/tokens_per_second.py @@ -2,7 +2,6 @@ import json import os -import time import torch from transformers import ( @@ -19,23 +18,16 @@ class TokensPerSecondCallback(TrainerCallback): - """ - A callback to measure and log tokens per second during training. - Also handles saving/restoring total_tokens state across checkpoint resumes. + """Restore the cumulative token counters when resuming from a checkpoint. + + Throughput itself is computed in the trainer's ``log()`` from deltas of the + cumulative ``trainable`` counter, so it is unaffected by + gradient_accumulation_steps and logging_steps. """ - def __init__( - self, tensor_parallel_size, context_parallel_size, resume_from_checkpoint=None - ): + def __init__(self, resume_from_checkpoint=None): super().__init__() - self.step_time = 0.0 - self.start_time = 0.0 - self.non_data_parallel_size = 1 self.resume_from_checkpoint = resume_from_checkpoint - if tensor_parallel_size is not None: - self.non_data_parallel_size *= tensor_parallel_size - if context_parallel_size is not None: - self.non_data_parallel_size *= context_parallel_size def on_train_begin( self, @@ -56,54 +48,3 @@ def on_train_begin( "trainable": torch.tensor(tokens_state.get("trainable", 0)), } LOG.info(f"Restored total_tokens: {state.tokens['total']}") - - def on_step_begin( - self, - args: TrainingArguments, - state: TrainerState, - control: TrainerControl, - **kwargs, - ): # pylint: disable=unused-argument - if not hasattr(state, "tokens"): - state.tokens = {"trainable": torch.zeros(1), "total": torch.zeros(1)} - self.start_time = time.perf_counter() - state.last_tokens_per_second = torch.zeros(1) - - def on_step_end( - self, - args: TrainingArguments, - state: TrainerState, - control: TrainerControl, - **kwargs, - ): # pylint: disable=unused-argument - tokens = getattr(state, "tokens", None) - if not (tokens and "trainable_tokens" in tokens): - return - step_time = time.perf_counter() - self.start_time - if step_time <= 0: - return - - num_tokens = tokens["trainable_tokens"].clone() / self.non_data_parallel_size - if torch.distributed.is_initialized(): - dp_size = max( - 1, torch.distributed.get_world_size() // self.non_data_parallel_size - ) - num_tokens = num_tokens / dp_size - state.last_tokens_per_second = num_tokens / step_time - - def on_log( - self, - args: TrainingArguments, - state: TrainerState, - control: TrainerControl, - logs=None, - **kwargs, - ): # pylint: disable=unused-argument - # after logging, clear the running metrics - if hasattr(state, "last_tokens_per_second"): - logs["tokens/train_per_sec_per_gpu"] = state.last_tokens_per_second.item() - state.last_tokens_per_second.zero_() - tokens = getattr(state, "tokens", None) - # Clear per-step tokens after logging - if tokens and "trainable_tokens" in tokens: - tokens["trainable_tokens"] = torch.zeros_like(tokens["trainable_tokens"]) diff --git a/tests/utils/callbacks/test_tokens_per_second.py b/tests/utils/callbacks/test_tokens_per_second.py new file mode 100644 index 0000000000..895ba3cd8c --- /dev/null +++ b/tests/utils/callbacks/test_tokens_per_second.py @@ -0,0 +1,50 @@ +"""Tests for trainable-token throughput accounting.""" + +from axolotl.core.trainers.utils import trainable_tokens_per_sec_per_gpu + + +def _cumulative_after_window(microbatch_token_counts, world_size, start=0.0): + """Mimic the trainer's cumulative counter: every microbatch is SUM-reduced + across ranks (balanced => x world_size) then added to the running total.""" + cum = start + for tok in microbatch_token_counts: + cum += tok * world_size + return cum + + +class TestTrainableTokensPerSec: + """Throughput from cumulative-counter deltas (regression for GA undercount).""" + + def test_basic_rate(self): + # 800 trainable tokens over 10s on a single GPU + assert trainable_tokens_per_sec_per_gpu(1000.0, 1800.0, 1, 10.0) == 80.0 + + def test_world_size_divides_to_per_gpu(self): + # 1600 tokens summed across 2 ranks over 10s => 80 tok/s/gpu + assert trainable_tokens_per_sec_per_gpu(0.0, 1600.0, 2, 10.0) == 80.0 + + def test_first_window_returns_none(self): + assert trainable_tokens_per_sec_per_gpu(None, 1800.0, 1, 10.0) is None + + def test_resume_first_window_does_not_spike(self): + # after resume the counter is restored to a large value but there is no + # prior window yet, so the first post-resume log must not emit a rate + assert trainable_tokens_per_sec_per_gpu(None, 5_000_000.0, 1, 10.0) is None + + def test_non_positive_elapsed_returns_none(self): + assert trainable_tokens_per_sec_per_gpu(1000.0, 1800.0, 1, 0.0) is None + assert trainable_tokens_per_sec_per_gpu(1000.0, 1800.0, 1, -5.0) is None + + def test_independent_of_gradient_accumulation(self): + # same tokens and wall time, processed as 1 microbatch vs 8 microbatches + ga1 = _cumulative_after_window([800], world_size=1) + ga8 = _cumulative_after_window([100] * 8, world_size=1) + rate1 = trainable_tokens_per_sec_per_gpu(0.0, ga1, 1, 10.0) + rate8 = trainable_tokens_per_sec_per_gpu(0.0, ga8, 1, 10.0) + assert rate1 == rate8 == 80.0 + + def test_overhead_lowers_rate(self): + # same tokens, larger wall time (e.g. eval/checkpoint in window) => lower rate + fast = trainable_tokens_per_sec_per_gpu(0.0, 800.0, 1, 10.0) + slow = trainable_tokens_per_sec_per_gpu(0.0, 800.0, 1, 20.0) + assert slow < fast From 5a0dc637e5f76a77fe74db05340e84a0587e0e08 Mon Sep 17 00:00:00 2001 From: Prathamesh Jadhav <55660103+lollinng@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:02:18 +0530 Subject: [PATCH 1347/1405] Honor curriculum_sampling in AxolotlGRPOTrainer sampler (#3707) [skip ci] The base trainer uses a SequentialSampler when curriculum_sampling is set, but the GRPO sampler hardcoded shuffle=True, so curriculum_sampling was ignored and the dataset was always shuffled. Wire shuffle to not curriculum_sampling, which makes SequenceParallelRepeatRandomSampler emit indices in dataset order. Fixes #2376 --- src/axolotl/core/trainers/grpo/trainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/core/trainers/grpo/trainer.py b/src/axolotl/core/trainers/grpo/trainer.py index 3a95ad439a..118aa3e9ad 100644 --- a/src/axolotl/core/trainers/grpo/trainer.py +++ b/src/axolotl/core/trainers/grpo/trainer.py @@ -190,7 +190,7 @@ def _get_train_sampler(self) -> Sampler: // self.args.context_parallel_size, repeat_count=self.num_iterations * self.args.gradient_accumulation_steps, context_parallel_size=self.args.context_parallel_size, - shuffle=True, + shuffle=not self.args.curriculum_sampling, seed=self.args.seed, drop_last=True, ) From 56bfe6440a6d13e9eeef96eb0fa686e06c8884e2 Mon Sep 17 00:00:00 2001 From: thad0ctor Date: Mon, 8 Jun 2026 23:32:46 -0700 Subject: [PATCH 1348/1405] fix: gc_collect_steps not honored (#3709) [skip ci] * fix: gc_collect_steps not honored get_callbacks only wired GCCallback off the deprecated `gc_steps`, so setting `gc_collect_steps` on its own was silently ignored (`migrate_gc_steps` never back-fills `gc_steps`). Read `gc_collect_steps`, falling back to the deprecated `gc_steps`, and pass it through to GCCallback. Adds a wiring regression test. * style: apply ruff format to gc_steps wiring test --- src/axolotl/core/builders/base.py | 5 ++-- tests/core/test_gc_steps_wiring.py | 41 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/core/test_gc_steps_wiring.py diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index e719e5eb3c..3770326534 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -122,8 +122,9 @@ def get_callbacks(self) -> list[TrainerCallback]: if self.cfg.resume_from_checkpoint: callbacks.append(SkipEvalOnResumeCallback()) - if self.cfg.gc_steps: - callbacks.append(GCCallback(gc_steps=self.cfg.gc_steps)) + gc_collect_steps = self.cfg.gc_collect_steps or self.cfg.gc_steps + if gc_collect_steps: + callbacks.append(GCCallback(gc_collect_steps=gc_collect_steps)) if self.cfg.dynamic_checkpoint and self.cfg.dynamic_checkpoint.enabled: from axolotl.utils.callbacks.dynamic_checkpoint import ( diff --git a/tests/core/test_gc_steps_wiring.py b/tests/core/test_gc_steps_wiring.py new file mode 100644 index 0000000000..487736c6af --- /dev/null +++ b/tests/core/test_gc_steps_wiring.py @@ -0,0 +1,41 @@ +"""get_callbacks must register GCCallback whenever gc_collect_steps (or legacy gc_steps) is set.""" + +from unittest.mock import MagicMock, patch + +from axolotl.core.builders import HFCausalTrainerBuilder +from axolotl.utils.callbacks import GCCallback +from axolotl.utils.dict import DictDefault + + +def _get_callbacks(cfg_dict): + builder = HFCausalTrainerBuilder.__new__(HFCausalTrainerBuilder) + builder.cfg = DictDefault(cfg_dict) + builder.model = MagicMock() + with ( + patch("axolotl.core.builders.base.PluginManager") as pm, + patch("axolotl.core.builders.base.TelemetryManager") as tm, + ): + pm.get_instance.return_value.add_callbacks_pre_trainer.return_value = [] + tm.get_instance.return_value.enabled = False + return builder.get_callbacks() + + +def _gc(callbacks): + return next((c for c in callbacks if isinstance(c, GCCallback)), None) + + +def test_gc_collect_steps_alone_registers_callback(): + """gc_collect_steps set without the deprecated gc_steps must still register GCCallback.""" + gc = _gc(_get_callbacks({"gc_collect_steps": 5})) + assert gc is not None + assert gc.gc_collect_steps == 5 + + +def test_legacy_gc_steps_still_registers_callback(): + gc = _gc(_get_callbacks({"gc_steps": 7})) + assert gc is not None + assert gc.gc_collect_steps == 7 + + +def test_no_gc_config_registers_nothing(): + assert _gc(_get_callbacks({})) is None From ed10453742a8fe4fe6097b00657e2d22bcb2fe87 Mon Sep 17 00:00:00 2001 From: thad0ctor Date: Mon, 8 Jun 2026 23:35:46 -0700 Subject: [PATCH 1349/1405] Feat/liger 0.8.0 bump (#3713) * feat(liger): bump liger-kernel to 0.8.0 and guard native dispatch overrides liger-kernel 0.8.0 adds native MODEL_TYPE_TO_APPLY_LIGER_FN entries for qwen3_5, qwen3_5_text, qwen3_5_moe, qwen3_5_moe_text, gemma4_text, ministral, and nemotron. plugin.py consults that registry before its own model branches, so the new qwen3_5 / qwen3_5_moe / gemma4_text entries would silently shadow axolotl's custom handling -- dropping the fused gated-RMSNorm kernel (Qwen3_5RMSNormGated / Qwen3_5MoeRMSNormGated) that native liger does not provide, and the checkpoint-safe (in_place=False) gemma4 RMSNorm/GEGLU path. Add an override set so qwen3_5 / qwen3_5_moe / gemma4_text keep routing to the axolotl branches, while genuinely new model types (qwen3_5 multimodal text variants, qwen3_next, ministral, nemotron) correctly adopt native 0.8.0 dispatch. Also refresh the ORPO trl-compat shim comment (still required: liger 0.8.0 still imports ORPOTrainer from the old trl.trainer path). * style(liger): collapse multi-line inline comments to one line Addresses CodeRabbit: src/**/*.py inline comments must be one short line. The full rationale lives in the PR description / prior commit message. * feat(liger): adopt native gemma4_text dispatch for FLCE support liger-kernel 0.8.0's apply_liger_kernel_to_gemma4_text supports fused linear cross entropy (and uses the more-correct casting_mode="gemma" fp32 RMSNorm with with_scale=False v_norm handling), which axolotl's custom gemma4 branch skipped. Drop gemma4_text from the native-dispatch override set so it routes to liger's native path and gains FLCE. Multimodal gemma4 (no native entry) keeps the axolotl branch; its FLCE warning now points to the gemma4_text path. Validated on GPU (torch 2.11.0, transformers 5.5.4): tiny Gemma4ForCausalLM training-mode forward with native FLCE -> finite loss, out.logits is None (fused path, no logit materialization), finite grads. * docs(liger): clarify multimodal gemma4 FLCE warning (0.8.0 gap, not absolute) Multimodal gemma4 FLCE is supported upstream (liger PR #1203, merged post-0.8.0) but not in the pinned 0.8.0 release, so the warning now scopes the limitation to 0.8.0 rather than implying it is fundamentally unsupported. * style(liger): collapse gemma4 RMSNorm docstring to one line --- pyproject.toml | 2 +- src/axolotl/integrations/liger/plugin.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 01cac4b8b0..03aaf2e7b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ dependencies = [ "bitsandbytes==0.49.1 ; sys_platform != 'darwin'", "triton>=3.4.0 ; sys_platform != 'darwin'", "xformers>=0.0.33.post2 ; sys_platform != 'darwin' and platform_machine != 'aarch64'", - "liger-kernel==0.7.0 ; sys_platform != 'darwin'", + "liger-kernel==0.8.0 ; sys_platform != 'darwin'", "torchao==0.17.0 ; sys_platform != 'darwin' and platform_machine != 'aarch64'", # Architecture-specific diff --git a/src/axolotl/integrations/liger/plugin.py b/src/axolotl/integrations/liger/plugin.py index eedf221ede..c7f8c560e9 100644 --- a/src/axolotl/integrations/liger/plugin.py +++ b/src/axolotl/integrations/liger/plugin.py @@ -20,7 +20,7 @@ def get_input_args(self): return "axolotl.integrations.liger.LigerArgs" def pre_model_load(self, cfg): - # shim: liger-kernel 0.7.0 imports ORPOTrainer from old trl path + # shim: liger imports ORPOTrainer from old trl.trainer (now trl.experimental.orpo) import trl.trainer from trl.experimental.orpo import ORPOTrainer @@ -81,7 +81,13 @@ def patched_init(self, *args, **kwargs): LigerFusedLinearCrossEntropyLoss.__init__ = patched_init - if cfg.model_config_type in MODEL_TYPE_TO_APPLY_LIGER_FN: + # liger 0.8.0 natively dispatches these, but axolotl's branches add kernels native lacks + axolotl_override_liger_fn = {"qwen3_5", "qwen3_5_moe"} + + if ( + cfg.model_config_type in MODEL_TYPE_TO_APPLY_LIGER_FN + and cfg.model_config_type not in axolotl_override_liger_fn + ): apply_liger_fn = MODEL_TYPE_TO_APPLY_LIGER_FN[cfg.model_config_type] liger_fn_sig = inspect.signature(apply_liger_fn) kwargs = {} @@ -235,9 +241,8 @@ def patched_init(self, *args, **kwargs): rms_norm=cfg.liger_rms_norm, swiglu=cfg.liger_glu_activation, ) - elif cfg.model_config_type in ("gemma4", "gemma4_text"): - # Gemma4: offset=0 (NOT 1 like Gemma3), in_place=False required for - # gradient checkpointing compatibility, RoPE incompatible (separate q/k). + elif cfg.model_config_type == "gemma4": + # multimodal gemma4 only; gemma4_text uses liger 0.8.0 native dispatch (incl. FLCE) from liger_kernel.transformers.geglu import LigerGEGLUMLP from transformers.models.gemma4 import modeling_gemma4 @@ -245,7 +250,7 @@ def patched_init(self, *args, **kwargs): _OrigGemma4RMSNorm = modeling_gemma4.Gemma4RMSNorm class _LigerGemma4RMSNorm(LigerRMSNorm): - """LigerRMSNorm for Gemma4 with in_place=False and with_scale support.""" + """LigerRMSNorm for Gemma4: offset=0, in_place=False (grad-ckpt safe), with_scale support.""" def __new__(cls, dim, eps=1e-6, with_scale=True): if not with_scale: @@ -278,7 +283,9 @@ def __init__(self, config, layer_idx=None): modeling_gemma4.nn.CrossEntropyLoss = LigerCrossEntropyLoss if cfg.liger_fused_linear_cross_entropy: LOG.warning( - "Liger fused linear cross entropy is not compatible with Gemma4. Skipping." + "Liger fused linear cross entropy for multimodal gemma4 is not in " + "liger-kernel 0.8.0 (added upstream in PR #1203, post-0.8.0). Skipping; " + "the gemma4_text language model gets FLCE via liger's native path." ) LOG.info( f"Applied Liger kernels for gemma4: " From a713e3c29508a78ccb6a24c65d65b0e393ca7a0a Mon Sep 17 00:00:00 2001 From: thad0ctor Date: Tue, 9 Jun 2026 10:06:06 -0700 Subject: [PATCH 1350/1405] perf: faster multimodal assistant-only masking (vectorized scanner) (#3672) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(mm-mask): vectorized role-boundary scanner + fused process_labels Follow-up to #3625 reducing the cost of multimodal assistant-only masking: - Vectorized role-boundary scanner (_compute_role_keep_mask_vectorized): precomputes start/end match positions in a single batched unfold pass, collapsing the inner Python prefix-match into a vectorized all-over-last-dim check. Byte-identical to the reference scanner, gated on torch.get_num_threads() == 1 (the DataLoader-worker default) since the per-op dispatch overhead regresses it under multi-threaded torch. 1.3-1.5x speedup on real Gemma 3 / Gemma 4 / Qwen 2 tokenizers, tie on Llama 3 (more boundaries, more tensor ops). - Fused process_labels: composes role/pad/media masks into one boolean keep tensor and does a single labels[~keep] = -100 write instead of the previous 3-4 sequential masked writes. Structural cleanup; the measured speedup is ~0ms since the prior masked-write cost was already dwarfed by the scanner. - One-shot warning when MultiModalChatDataCollator is built with dataloader_num_workers in (None, 0): MM training does heavier collator-side work than text and synchronous collation blocks the GPU. Tests: - tests/test_vectorized_scanner.py: 2,000-config differential fuzz + targeted edge cases (Pixtral [/INST] rewind, longest-prefix tie, train_on_eos modes, empty end_tokens, include_end leak gate). - tests/test_process_labels_fusion.py: 50 random batches per strategy across all 9 process_labels-overriding strategies (450 fuzz cases) asserting torch.equal between pre-refactor and fused implementation. - tests/test_mm_collator_warnings.py: num_workers=0 warning regression. * fix: address coderabbit feedback and lint * test(mm-mask): add Voxtral/Mistral3/InternVL/Glm4v strategy coverage - test_process_labels_fusion.py: factories + parity-fuzz entries for the four strategies (200 new fused-vs-legacy assertions); no-boundaries branch in _build_alternating_turns now injects pad/extras so the pad/media masking path is exercised. - test_processing_strategies.py: behavioral tests (process_labels, train_on_inputs, role_boundaries_override, batch handling, InternVL error paths) plus dispatch routing for VoxtralProcessor, Mistral3Processor, and InternVLProcessor. 572 -> 788 passing across the PR's four test files. * perf(mm-mask): bisect end-finder + slice-fill in vectorized scanner The vectorized scanner's per-row loop still did O(span) Python work: a linear walk over end-match flags to find each turn's end, and element-wise bytearray writes to fill spans. Replace both: - Precompute each boundary's end-match start positions (sorted via nonzero) and bisect for the next end >= start_of_content. - Fill keep-spans with bytearray slice assignment and materialize each row tensor once via torch.frombuffer. Byte-identical to the reference scanner; ~2x faster than the prior vectorized path (≈3x vs reference) on 8x3-6k batches, widening with turn length. Adds long-span / multi-end-marker parity fuzz that the original filler<=200 generator under-exercised. --------- Co-authored-by: Wing Lian --- src/axolotl/core/builders/causal.py | 18 + src/axolotl/processing_strategies.py | 333 ++++++++++++-- tests/test_mm_collator_warnings.py | 82 ++++ tests/test_process_labels_fusion.py | 640 ++++++++++++++++++++++++++ tests/test_processing_strategies.py | 252 +++++++++++ tests/test_vectorized_scanner.py | 650 +++++++++++++++++++++++++++ 6 files changed, 1926 insertions(+), 49 deletions(-) create mode 100644 tests/test_mm_collator_warnings.py create mode 100644 tests/test_process_labels_fusion.py create mode 100644 tests/test_vectorized_scanner.py diff --git a/src/axolotl/core/builders/causal.py b/src/axolotl/core/builders/causal.py index a82c0dd05d..93d751f8ba 100644 --- a/src/axolotl/core/builders/causal.py +++ b/src/axolotl/core/builders/causal.py @@ -49,6 +49,23 @@ LOG = get_logger(__name__) +_MM_NUM_WORKERS_WARNED: set = set() + + +def _warn_if_num_workers_zero_for_mm(cfg, log) -> None: + if not getattr(cfg, "processor_type", None): + return + if getattr(cfg, "dataloader_num_workers", None) not in (None, 0): + return + if getattr(cfg, "train_on_inputs", False): + return + if "mm_num_workers_zero" in _MM_NUM_WORKERS_WARNED: + return + _MM_NUM_WORKERS_WARNED.add("mm_num_workers_zero") + log.warning( + "Increase dataloader_num_workers to speed up multimodal training with assistant-only loss masking (currently dataloader_num_workers=0)." + ) + class HFCausalTrainerBuilder(TrainerBuilderBase): """ @@ -560,6 +577,7 @@ def _ds_get(cfg_obj, key): train_on_eos, "set" if role_boundaries_override else "none", ) + _warn_if_num_workers_zero_for_mm(self.cfg, LOG) kwargs["processing_strategy"] = get_processing_strategy( self.processor, diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 497e5b5108..677e21d7f3 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -1,9 +1,11 @@ """Module containing ProcessingStrategy classes and its derivative for different MultiModal Model types""" +import bisect from copy import deepcopy from dataclasses import dataclass, field from typing import Optional +import torch from PIL import Image, ImageOps from PIL.Image import Resampling from torch import Tensor, zeros_like @@ -333,10 +335,10 @@ def convert_messages_to_multimedia_messages(messages: list[dict]) -> list[dict]: return processed_examples - def _mask_non_assistant(self, labels: Tensor) -> Tensor: - """Mask non-trainable role regions to -100 using ``self.role_boundaries``.""" + def _mask_non_assistant_keep(self, input_ids: Tensor) -> Tensor: + """Return a [B, L] bool keep tensor (True = contributes to loss).""" if self.train_on_inputs: - return labels + return torch.ones_like(input_ids, dtype=torch.bool) # Legacy no-op for boundary-less strategies; warn once so the miss shows up in logs. if not self.role_boundaries: @@ -355,40 +357,46 @@ def _mask_non_assistant(self, labels: Tensor) -> Tensor: "list of strategies on this fallback path.", key, ) - return labels + return torch.ones_like(input_ids, dtype=torch.bool) - return _apply_role_boundaries( - labels, + # Vectorized path regresses 9-13x under multi-threaded torch. + scanner = ( + _compute_role_keep_mask_vectorized + if torch.get_num_threads() == 1 + else _compute_role_keep_mask + ) + return scanner( + input_ids, self.role_boundaries, roles_to_train=set(self.roles_to_train), train_on_eos=self.train_on_eos, ) + def _mask_non_assistant(self, labels: Tensor) -> Tensor: + """Mask non-trainable role regions to -100 using ``self.role_boundaries``.""" + keep = self._mask_non_assistant_keep(labels) + labels[~keep] = -100 + return labels + def process_labels(self, input_ids: Tensor) -> Tensor: - labels = input_ids.clone() - labels = self._mask_non_assistant(labels) + keep = self._mask_non_assistant_keep(input_ids) pad_id = getattr(self.processor.tokenizer, "pad_token_id", None) if pad_id is not None: - labels[labels == pad_id] = -100 + keep = keep & (input_ids != pad_id) if self.image_token_id is not None: - labels[labels == self.image_token_id] = -100 + keep = keep & (input_ids != self.image_token_id) + labels = input_ids.clone() + labels[~keep] = -100 return labels -def _apply_role_boundaries( +def _compute_role_keep_mask( labels: Tensor, role_boundaries: list[RoleBoundary], roles_to_train: set[str], train_on_eos: str, ) -> Tensor: - """Mask tokens outside trainable role spans to -100. - - Scan is greedy-left with longest-prefix-wins on start_tokens to disambiguate - nested markers (e.g. ``<|im_start|>assistant`` vs ``<|im_start|>``). - ``train_on_eos`` accepts ``"turn"`` (end marker in loss on trainable turns - only), ``"all"`` (always), ``"none"`` (never — overrides ``include_end``), - ``"last"`` (only on the last trainable turn in the sequence). - """ + """Return a [B, L] bool keep mask (True = contributes to loss).""" mask = zeros_like(labels) # For "last": remember each trainable turn's end-marker span so we can # unmask only the final one after the scan finishes. @@ -475,8 +483,202 @@ def _find_end( s, e = span mask[i][s:e] = 1 - labels[i][mask[i] == 0] = -100 + return mask.bool() + +def _apply_role_boundaries( + labels: Tensor, + role_boundaries: list[RoleBoundary], + roles_to_train: set[str], + train_on_eos: str, +) -> Tensor: + """Mask tokens outside trainable role spans to -100. + + Scan is greedy-left with longest-prefix-wins on start_tokens to disambiguate + nested markers (e.g. ``<|im_start|>assistant`` vs ``<|im_start|>``). + ``train_on_eos`` accepts ``"turn"`` (end marker in loss on trainable turns + only), ``"all"`` (always), ``"none"`` (never — overrides ``include_end``), + ``"last"`` (only on the last trainable turn in the sequence). + """ + keep = _compute_role_keep_mask( + labels, role_boundaries, roles_to_train, train_on_eos + ) + labels[~keep] = -100 + return labels + + +def _compute_role_keep_mask_vectorized( + labels: Tensor, + role_boundaries: list[RoleBoundary], + roles_to_train: set[str], + train_on_eos: str, +) -> Tensor: + """Vectorized variant of :func:`_compute_role_keep_mask`. Byte-identical output; faster under single-threaded torch.""" + if labels.numel() == 0: + return torch.zeros_like(labels, dtype=torch.bool) + if not role_boundaries: + # Defensive: match reference all-mask semantics on empty boundaries. + return torch.zeros_like(labels, dtype=torch.bool) + + B, L = labels.shape + device = labels.device + + # Longer start_tokens first so longest-prefix-wins tie-break holds. + indexed = list(enumerate(role_boundaries)) + indexed.sort(key=lambda ib: -len(ib[1].start_tokens)) + + start_winner = torch.full((B, L), -1, dtype=torch.int64, device=device) + # Track match length so equal-length ties keep the first writer (matches reference). + start_winner_len = torch.zeros((B, L), dtype=torch.int64, device=device) + end_match: list[Optional[Tensor]] = [None] * len(role_boundaries) + + for orig_idx, b in indexed: + s_tok = b.start_tokens + s_len = len(s_tok) + if s_len == 0: + continue # defensive: empty start never matches + + if s_len > L: + start_mask = torch.zeros((B, 0), dtype=torch.bool, device=device) + else: + s_tok_t = torch.tensor(s_tok, dtype=labels.dtype, device=device) + windows = labels.unfold(1, s_len, 1) + start_mask = (windows == s_tok_t).all(dim=-1) + + # Pad to L so absolute position j indexes into start_mask. + pad_w = L - start_mask.shape[1] + if pad_w > 0: + start_mask = torch.cat( + [ + start_mask, + torch.zeros((B, pad_w), dtype=torch.bool, device=device), + ], + dim=1, + ) + + # Strictly-longer gate + longest-first iteration => first writer sticks. + update = start_mask & (start_winner_len < s_len) + start_winner = torch.where( + update, torch.full_like(start_winner, orig_idx), start_winner + ) + start_winner_len = torch.where( + update, torch.full_like(start_winner_len, s_len), start_winner_len + ) + + e_tok = b.end_tokens + e_len = len(e_tok) + if e_len == 0: + end_match[orig_idx] = None + elif e_len > L: + end_match[orig_idx] = torch.zeros((B, 0), dtype=torch.bool, device=device) + else: + e_tok_t = torch.tensor(e_tok, dtype=labels.dtype, device=device) + ewindows = labels.unfold(1, e_len, 1) + end_match[orig_idx] = (ewindows == e_tok_t).all(dim=-1) + + # Lift to CPU lists: hot per-row loop is Python, tensor access is slow. + start_winner_cpu = start_winner.cpu().tolist() + # Per boundary, per row: sorted end-match start positions, so the inner loop + # bisects for the next end instead of scanning every position in the span. + end_pos: list[Optional[list[list[int]]]] = [] + for em in end_match: + if em is None: + end_pos.append(None) + else: + nz = em.nonzero(as_tuple=False) + rows: list[list[int]] = [[] for _ in range(B)] + for r, c in nz.tolist(): + rows[r].append(c) + end_pos.append(rows) + + bnd_start_len = [len(b.start_tokens) for b in role_boundaries] + bnd_end_len = [len(b.end_tokens) for b in role_boundaries] + bnd_include_start = [b.include_start for b in role_boundaries] + bnd_include_end = [b.include_end for b in role_boundaries] + bnd_role_in_loss = [b.role in roles_to_train for b in role_boundaries] + + mask = zeros_like(labels) + ONE = b"\x01" + + for i in range(B): + row_start = start_winner_cpu[i] + n = L + last_trainable_end_span: Optional[tuple[int, int]] = None + # bytearray slice-assignment beats per-element writes; one tensor per row. + row_mask = bytearray(n) + + j = 0 + while j < n: + bidx = row_start[j] + if bidx == -1: + j += 1 + continue + + s_len = bnd_start_len[bidx] + start_of_content = j + s_len + e_len = bnd_end_len[bidx] + include_start = bnd_include_start[bidx] + include_end = bnd_include_end[bidx] + role_in_loss = bnd_role_in_loss[bidx] + + if e_len == 0: + end_after = n + found_end = False + else: + positions = end_pos[bidx][i] # type: ignore[index] + limit = n - e_len + idx = bisect.bisect_left(positions, start_of_content) + if idx < len(positions) and positions[idx] <= limit: + found_end = True + end_after = positions[idx] + e_len + else: + found_end = False + end_after = n + + if role_in_loss: + if include_start: + row_mask[j:start_of_content] = ONE * (start_of_content - j) + content_end = end_after - e_len if found_end else end_after + row_mask[start_of_content:content_end] = ONE * ( + content_end - start_of_content + ) + if found_end and include_end and train_on_eos not in ("none", "last"): + row_mask[content_end:end_after] = ONE * (end_after - content_end) + if found_end and include_end and train_on_eos == "last": + last_trainable_end_span = (content_end, end_after) + else: + if found_end and include_end and train_on_eos == "all": + content_end = end_after - e_len + row_mask[content_end:end_after] = ONE * (end_after - content_end) + + # include_end=False rewind: re-match the end as the next start. + if found_end and not include_end and e_len: + j = end_after - e_len + else: + j = end_after + + if train_on_eos == "last" and last_trainable_end_span is not None: + s, e = last_trainable_end_span + row_mask[s:e] = ONE * (e - s) + + # One tensor write per row — keep heavyweight op out of inner loop. + if any(row_mask): + mask[i] = torch.frombuffer(row_mask, dtype=torch.uint8).to(mask.dtype) + + return mask.bool() + + +def _apply_role_boundaries_vectorized( + labels: Tensor, + role_boundaries: list[RoleBoundary], + roles_to_train: set[str], + train_on_eos: str, +) -> Tensor: + """Vectorized variant of :func:`_apply_role_boundaries`.""" + keep = _compute_role_keep_mask_vectorized( + labels, role_boundaries, roles_to_train, train_on_eos + ) + labels[~keep] = -100 return labels @@ -629,9 +831,16 @@ def __init__( ) def process_labels(self, input_ids): - labels = super().process_labels(input_ids) + keep = self._mask_non_assistant_keep(input_ids) + pad_id = getattr(self.processor.tokenizer, "pad_token_id", None) + if pad_id is not None: + keep = keep & (input_ids != pad_id) + if self.image_token_id is not None: + keep = keep & (input_ids != self.image_token_id) if self.video_token_id is not None: - labels[labels == self.video_token_id] = -100 + keep = keep & (input_ids != self.video_token_id) + labels = input_ids.clone() + labels[~keep] = -100 return labels @@ -700,16 +909,23 @@ def __init__( self.image_token_id = processor.tokenizer.convert_tokens_to_ids(boi) def process_labels(self, input_ids): - labels = super().process_labels(input_ids) + keep = self._mask_non_assistant_keep(input_ids) + pad_id = getattr(self.processor.tokenizer, "pad_token_id", None) + if pad_id is not None: + keep = keep & (input_ids != pad_id) + if self.image_token_id is not None: + keep = keep & (input_ids != self.image_token_id) # Resolve via tokenizer; fall back to default id # if not in vocab. Matches Gemma4's pattern. tok = self.processor.tokenizer soft_id = tok.convert_tokens_to_ids("") unk_id = getattr(tok, "unk_token_id", None) if soft_id is not None and soft_id != unk_id: - labels[labels == soft_id] = -100 + keep = keep & (input_ids != soft_id) else: - labels[labels == 262144] = -100 + keep = keep & (input_ids != 262144) + labels = input_ids.clone() + labels[~keep] = -100 return labels @@ -717,8 +933,13 @@ class Gemma3nProcessingStrategy(_GemmaTurnStrategy): """Gemma3n: same turn boundaries as Gemma3, additionally masks audio/delimiter tokens.""" def process_labels(self, input_ids): - labels = super().process_labels(input_ids) + keep = self._mask_non_assistant_keep(input_ids) tok = self.processor.tokenizer + pad_id = getattr(tok, "pad_token_id", None) + if pad_id is not None: + keep = keep & (input_ids != pad_id) + if self.image_token_id is not None: + keep = keep & (input_ids != self.image_token_id) # Follows huggingface-gemma-recipes fine_tune_gemma3n_on_t4 notebook. for attr in ( "image_token_id", @@ -728,7 +949,9 @@ def process_labels(self, input_ids): ): tok_id = getattr(tok, attr, None) if tok_id is not None: - labels[labels == tok_id] = -100 + keep = keep & (input_ids != tok_id) + labels = input_ids.clone() + labels[~keep] = -100 return labels @@ -768,15 +991,21 @@ def _build_role_boundaries(self) -> list[RoleBoundary]: return boundaries def process_labels(self, input_ids): - labels = super().process_labels(input_ids) + keep = self._mask_non_assistant_keep(input_ids) tokenizer = self.processor.tokenizer unk_id = getattr(tokenizer, "unk_token_id", None) + pad_id = getattr(tokenizer, "pad_token_id", None) + if pad_id is not None: + keep = keep & (input_ids != pad_id) + if self.image_token_id is not None: + keep = keep & (input_ids != self.image_token_id) + if getattr(tokenizer, "image_token_id", None) is not None: - labels[labels == tokenizer.image_token_id] = -100 + keep = keep & (input_ids != tokenizer.image_token_id) if getattr(tokenizer, "audio_token_id", None) is not None: - labels[labels == tokenizer.audio_token_id] = -100 + keep = keep & (input_ids != tokenizer.audio_token_id) # boi/eoi/boa/eoa are only string attrs on the processor; resolve ids here. for attr in ("boi_token", "eoi_token", "boa_token", "eoa_token"): @@ -786,13 +1015,15 @@ def process_labels(self, input_ids): token_id = tokenizer.convert_tokens_to_ids(token_str) if token_id is None or token_id == unk_id: continue - labels[labels == token_id] = -100 + keep = keep & (input_ids != token_id) # Video id lives on the processor, not the tokenizer. video_token_id = getattr(self.processor, "video_token_id", None) if video_token_id is not None and video_token_id != unk_id: - labels[labels == video_token_id] = -100 + keep = keep & (input_ids != video_token_id) + labels = input_ids.clone() + labels[~keep] = -100 return labels @@ -950,17 +1181,18 @@ def __init__( self.begin_audio_token = special_ids.begin_audio def process_labels(self, input_ids): - labels = input_ids.clone() - labels = self._mask_non_assistant(labels) + keep = self._mask_non_assistant_keep(input_ids) pad_id = getattr(self.processor.tokenizer, "pad_token_id", None) if pad_id is not None: - labels[labels == pad_id] = -100 + keep = keep & (input_ids != pad_id) if self.audio_token is not None: - labels[labels == self.audio_token] = -100 + keep = keep & (input_ids != self.audio_token) if self.begin_audio_token is not None: - labels[labels == self.begin_audio_token] = -100 + keep = keep & (input_ids != self.begin_audio_token) + labels = input_ids.clone() + labels[~keep] = -100 return labels @@ -1040,16 +1272,17 @@ def __init__( self.image_end_token = special_ids.img_end def process_labels(self, input_ids): - labels = input_ids.clone() - labels = self._mask_non_assistant(labels) + keep = self._mask_non_assistant_keep(input_ids) pad_id = getattr(self.processor.tokenizer, "pad_token_id", None) if pad_id is not None: - labels[labels == pad_id] = -100 + keep = keep & (input_ids != pad_id) for tok_id in (self.image_token, self.image_break_token, self.image_end_token): if tok_id is not None: - labels[labels == tok_id] = -100 + keep = keep & (input_ids != tok_id) + labels = input_ids.clone() + labels[~keep] = -100 return labels @@ -1090,18 +1323,19 @@ def __init__( self.image_token_ids = processor.image_ids def process_labels(self, input_ids): - labels = input_ids.clone() - labels = self._mask_non_assistant(labels) + keep = self._mask_non_assistant_keep(input_ids) pad_id = getattr(self.processor.tokenizer, "pad_token_id", None) if pad_id is not None: - labels[labels == pad_id] = -100 + keep = keep & (input_ids != pad_id) for ids in self.image_token_ids: if ids is not None: - labels[labels == ids] = -100 + keep = keep & (input_ids != ids) # Video tokens get converted to image patches during media processing; masking may be redundant. + labels = input_ids.clone() + labels[~keep] = -100 return labels @@ -1161,12 +1395,11 @@ def __init__( ) def process_labels(self, input_ids): - labels = input_ids.clone() - labels = self._mask_non_assistant(labels) + keep = self._mask_non_assistant_keep(input_ids) pad_id = getattr(self.tokenizer, "pad_token_id", None) if pad_id is not None: - labels[labels == pad_id] = -100 + keep = keep & (input_ids != pad_id) for tok_id in ( self.image_token_id, @@ -1177,8 +1410,10 @@ def process_labels(self, input_ids): self.end_video_token_id, ): if tok_id is not None: - labels[labels == tok_id] = -100 + keep = keep & (input_ids != tok_id) + labels = input_ids.clone() + labels[~keep] = -100 return labels diff --git a/tests/test_mm_collator_warnings.py b/tests/test_mm_collator_warnings.py new file mode 100644 index 0000000000..5481d2969e --- /dev/null +++ b/tests/test_mm_collator_warnings.py @@ -0,0 +1,82 @@ +"""Tests for the MM dataloader_num_workers=0 warning.""" + +from __future__ import annotations + +import logging +from types import SimpleNamespace + +import pytest + +from axolotl.core.builders import causal as _causal_builder +from axolotl.core.builders.causal import _warn_if_num_workers_zero_for_mm + + +def _make_cfg(processor_type=None, dataloader_num_workers=None, train_on_inputs=False): + return SimpleNamespace( + processor_type=processor_type, + dataloader_num_workers=dataloader_num_workers, + train_on_inputs=train_on_inputs, + ) + + +@pytest.fixture +def _reset_mm_warn_state(): + """Reset the module-level once-per-process guard set so each test is independent. + + Production keeps the warning one-shot per process so logs aren't spammed + when ``build()`` builds collators for both train and eval. + """ + _causal_builder._MM_NUM_WORKERS_WARNED.clear() + yield + _causal_builder._MM_NUM_WORKERS_WARNED.clear() + + +def _capture_warning(caplog, cfg) -> list[str]: + log = logging.getLogger("axolotl.core.builders.causal") + log.addHandler(caplog.handler) + try: + with caplog.at_level(logging.WARNING, logger="axolotl.core.builders.causal"): + _warn_if_num_workers_zero_for_mm(cfg, log) + finally: + log.removeHandler(caplog.handler) + return [r.getMessage() for r in caplog.records if r.levelno == logging.WARNING] + + +def test_warn_num_workers_zero_for_mm_emits_warning(_reset_mm_warn_state, caplog): + cfg = _make_cfg(processor_type="qwen2_vl", dataloader_num_workers=0) + msgs = _capture_warning(caplog, cfg) + assert any("dataloader_num_workers=0" in m for m in msgs) + + +def test_warn_num_workers_zero_for_mm_silent_when_workers_set( + _reset_mm_warn_state, caplog +): + cfg = _make_cfg(processor_type="qwen2_vl", dataloader_num_workers=2) + msgs = _capture_warning(caplog, cfg) + assert not any("dataloader_num_workers=0" in m for m in msgs) + + +def test_warn_num_workers_zero_for_mm_silent_when_no_processor( + _reset_mm_warn_state, caplog +): + cfg = _make_cfg(processor_type=None, dataloader_num_workers=0) + msgs = _capture_warning(caplog, cfg) + assert not any("dataloader_num_workers=0" in m for m in msgs) + + +def test_warn_num_workers_zero_for_mm_treats_none_as_zero(_reset_mm_warn_state, caplog): + """``None`` (default) and ``0`` should both trigger the warning.""" + cfg = _make_cfg(processor_type="qwen2_vl", dataloader_num_workers=None) + msgs = _capture_warning(caplog, cfg) + assert any("dataloader_num_workers=0" in m for m in msgs) + + +def test_warn_num_workers_zero_for_mm_is_one_shot(_reset_mm_warn_state, caplog): + """Second invocation in the same process should NOT re-emit the warning.""" + cfg = _make_cfg(processor_type="qwen2_vl", dataloader_num_workers=0) + first_msgs = _capture_warning(caplog, cfg) + assert any("dataloader_num_workers=0" in m for m in first_msgs) + + caplog.clear() + second_msgs = _capture_warning(caplog, cfg) + assert not any("dataloader_num_workers=0" in m for m in second_msgs) diff --git a/tests/test_process_labels_fusion.py b/tests/test_process_labels_fusion.py new file mode 100644 index 0000000000..3f38134af7 --- /dev/null +++ b/tests/test_process_labels_fusion.py @@ -0,0 +1,640 @@ +"""Parity tests for fused ``process_labels``. + +Each strategy gets a legacy reference implementation (verbatim copy of the +pre-refactor code) and a fuzz batch generator that injects realistic +distributions of pad/image/etc tokens. We then assert byte-identical output +between the legacy ``process_labels`` and the new fused implementation. +""" + +from __future__ import annotations + +import random +from types import SimpleNamespace +from typing import List + +import pytest +import torch + +from axolotl.processing_strategies import ( + Gemma3nProcessingStrategy, + Gemma3ProcessingStrategy, + Gemma4ProcessingStrategy, + Glm4vProcessingStrategy, + InternVLProcessingStrategy, + Llama3_2VisionProcessingStrategy, + Llama4ProcessingStrategy, + Mistral3ProcessingStrategy, + MistralV7TekkenProcessingStrategy, + PixtralProcessingStrategy, + ProcessingStrategy, + Qwen2VLProcessingStrategy, + Qwen3_5ProcessingStrategy, + VoxtralProcessingStrategy, + _apply_role_boundaries, +) + +# --------------------------------------------------------------------------- # +# Tokenizer / processor stubs (mirrors test_processing_strategies.py) +# --------------------------------------------------------------------------- # + + +class _Tokenizer: + def __init__( + self, + vocab: dict, + pad_id: int = 0, + unk_id: int = 3, + eos_id: int | None = None, + extras: dict | None = None, + ): + self.vocab = vocab + self.pad_token_id = pad_id + self.unk_token_id = unk_id + if eos_id is not None: + self.eos_token_id = eos_id + if extras: + for k, v in extras.items(): + setattr(self, k, v) + + def encode(self, text, add_special_tokens=False): + return list(self.vocab.get(text, [])) + + def convert_tokens_to_ids(self, token): + v = self.vocab.get(token) + if v is None: + return self.unk_token_id + return v[0] if len(v) == 1 else self.unk_token_id + + +class _Processor: + def __init__(self, tokenizer, **extras): + self.tokenizer = tokenizer + for k, v in extras.items(): + setattr(self, k, v) + + +# --------------------------------------------------------------------------- # +# Legacy reference implementations (verbatim pre-refactor body) +# --------------------------------------------------------------------------- # +# +# These functions implement process_labels exactly as it existed before the +# fusion refactor: 3-4 sequential masked writes against the labels tensor, +# with _mask_non_assistant materializing the role mask in-place. + + +def _legacy_mask_non_assistant(strategy, labels): + """Pre-refactor ``_mask_non_assistant`` body.""" + if strategy.train_on_inputs: + return labels + if not strategy.role_boundaries: + return labels + return _apply_role_boundaries( + labels, + strategy.role_boundaries, + roles_to_train=set(strategy.roles_to_train), + train_on_eos=strategy.train_on_eos, + ) + + +def legacy_base_process_labels(strategy, input_ids): + labels = input_ids.clone() + labels = _legacy_mask_non_assistant(strategy, labels) + pad_id = getattr(strategy.processor.tokenizer, "pad_token_id", None) + if pad_id is not None: + labels[labels == pad_id] = -100 + if strategy.image_token_id is not None: + labels[labels == strategy.image_token_id] = -100 + return labels + + +def legacy_qwen3_5_process_labels(strategy, input_ids): + labels = legacy_base_process_labels(strategy, input_ids) + if strategy.video_token_id is not None: + labels[labels == strategy.video_token_id] = -100 + return labels + + +def legacy_gemma3_process_labels(strategy, input_ids): + labels = legacy_base_process_labels(strategy, input_ids) + tok = strategy.processor.tokenizer + soft_id = tok.convert_tokens_to_ids("") + unk_id = getattr(tok, "unk_token_id", None) + if soft_id is not None and soft_id != unk_id: + labels[labels == soft_id] = -100 + else: + labels[labels == 262144] = -100 + return labels + + +def legacy_gemma3n_process_labels(strategy, input_ids): + labels = legacy_base_process_labels(strategy, input_ids) + tok = strategy.processor.tokenizer + for attr in ( + "image_token_id", + "audio_token_id", + "boi_token_id", + "eoi_token_id", + ): + tok_id = getattr(tok, attr, None) + if tok_id is not None: + labels[labels == tok_id] = -100 + return labels + + +def legacy_gemma4_process_labels(strategy, input_ids): + labels = legacy_base_process_labels(strategy, input_ids) + tokenizer = strategy.processor.tokenizer + unk_id = getattr(tokenizer, "unk_token_id", None) + if getattr(tokenizer, "image_token_id", None) is not None: + labels[labels == tokenizer.image_token_id] = -100 + if getattr(tokenizer, "audio_token_id", None) is not None: + labels[labels == tokenizer.audio_token_id] = -100 + for attr in ("boi_token", "eoi_token", "boa_token", "eoa_token"): + token_str = getattr(strategy.processor, attr, None) + if token_str is None: + continue + token_id = tokenizer.convert_tokens_to_ids(token_str) + if token_id is None or token_id == unk_id: + continue + labels[labels == token_id] = -100 + video_token_id = getattr(strategy.processor, "video_token_id", None) + if video_token_id is not None and video_token_id != unk_id: + labels[labels == video_token_id] = -100 + return labels + + +def legacy_voxtral_process_labels(strategy, input_ids): + labels = input_ids.clone() + labels = _legacy_mask_non_assistant(strategy, labels) + pad_id = getattr(strategy.processor.tokenizer, "pad_token_id", None) + if pad_id is not None: + labels[labels == pad_id] = -100 + if strategy.audio_token is not None: + labels[labels == strategy.audio_token] = -100 + if strategy.begin_audio_token is not None: + labels[labels == strategy.begin_audio_token] = -100 + return labels + + +def legacy_mistral3_process_labels(strategy, input_ids): + labels = input_ids.clone() + labels = _legacy_mask_non_assistant(strategy, labels) + pad_id = getattr(strategy.processor.tokenizer, "pad_token_id", None) + if pad_id is not None: + labels[labels == pad_id] = -100 + for tok_id in ( + strategy.image_token, + strategy.image_break_token, + strategy.image_end_token, + ): + if tok_id is not None: + labels[labels == tok_id] = -100 + return labels + + +def legacy_internvl_process_labels(strategy, input_ids): + labels = input_ids.clone() + labels = _legacy_mask_non_assistant(strategy, labels) + pad_id = getattr(strategy.processor.tokenizer, "pad_token_id", None) + if pad_id is not None: + labels[labels == pad_id] = -100 + for ids in strategy.image_token_ids: + if ids is not None: + labels[labels == ids] = -100 + return labels + + +def legacy_glm4v_process_labels(strategy, input_ids): + labels = input_ids.clone() + labels = _legacy_mask_non_assistant(strategy, labels) + pad_id = getattr(strategy.tokenizer, "pad_token_id", None) + if pad_id is not None: + labels[labels == pad_id] = -100 + for tok_id in ( + strategy.image_token_id, + strategy.begin_image_token_id, + strategy.end_image_token_id, + strategy.video_token_id, + strategy.begin_video_token_id, + strategy.end_video_token_id, + ): + if tok_id is not None: + keep = labels != tok_id # noqa: F841 — keep var unused; legacy uses indexed write + labels[labels == tok_id] = -100 + return labels + + +# --------------------------------------------------------------------------- # +# Batch construction with realistic distributions +# --------------------------------------------------------------------------- # + + +def _build_alternating_turns( + rng: random.Random, + boundaries, + target_len: int, + pad_id: int, + extra_token_ids: List[int], + pad_rate: float = 0.07, + extra_rate: float = 0.07, + filler_min: int = 6, + filler_max: int = 24, +) -> List[int]: + """Build a single row of length ``target_len`` from ``boundaries``. + + Alternates user/assistant turns (or whatever's first/second in the list), + with random filler IDs that may be replaced by pad / extra (image, etc) + tokens at ``pad_rate`` / ``extra_rate``. The injection happens *inside* + assistant spans too, which is the realistic case. + """ + role_b = list(boundaries) + ids: List[int] = [] + if not role_b: + # No boundaries (Voxtral/Mistral3/InternVL/Glm4v): still inject pad and + # media tokens so the fused vs legacy parity check exercises the + # pad/media masking path even with role-masking opted out. + for _ in range(target_len): + r = rng.random() + if r < pad_rate: + ids.append(pad_id) + elif r < pad_rate + extra_rate and extra_token_ids: + ids.append(rng.choice(extra_token_ids)) + else: + ids.append(rng.randint(10000, 30000)) + return ids + + turn = 0 + while len(ids) < target_len: + b = role_b[turn % len(role_b)] + ids.extend(b.start_tokens) + n_filler = rng.randint(filler_min, filler_max) + for _ in range(n_filler): + r = rng.random() + if r < pad_rate: + ids.append(pad_id) + elif r < pad_rate + extra_rate and extra_token_ids: + ids.append(rng.choice(extra_token_ids)) + else: + # Use IDs well outside the boundary token space + ids.append(rng.randint(10000, 30000)) + if b.end_tokens: + ids.extend(b.end_tokens) + turn += 1 + return ids[:target_len] + + +def _build_batch(rng, boundaries, batch_size, seq_len, pad_id, extra_ids): + rows = [] + for _ in range(batch_size): + # Vary length per row a bit so we get padding-like rows occasionally + target = rng.randint(seq_len // 2, seq_len) + row = _build_alternating_turns(rng, boundaries, target, pad_id, extra_ids) + # Pad up to seq_len with pad_id (mimics collator behavior) + if len(row) < seq_len: + row.extend([pad_id] * (seq_len - len(row))) + rows.append(row[:seq_len]) + return torch.tensor(rows, dtype=torch.long) + + +# --------------------------------------------------------------------------- # +# Per-strategy fixtures +# --------------------------------------------------------------------------- # + + +def _make_qwen2vl(): + vocab = { + "<|im_start|>system\n": [101, 102, 103], + "<|im_start|>user\n": [101, 104, 103], + "<|im_start|>assistant\n": [101, 105, 103], + "<|im_end|>": [106], + "<|image_pad|>": [200], + } + tok = _Tokenizer(vocab, pad_id=0, unk_id=3) + return Qwen2VLProcessingStrategy(_Processor(tok)) + + +def _make_qwen3_5(): + vocab = { + "<|im_start|>system\n": [101, 102, 103], + "<|im_start|>user\n": [101, 104, 103], + "<|im_start|>assistant\n": [101, 105, 103], + "<|im_end|>": [106], + "<|image_pad|>": [200], + "<|video_pad|>": [201], + } + tok = _Tokenizer(vocab, pad_id=0, unk_id=3) + return Qwen3_5ProcessingStrategy(_Processor(tok)) + + +def _make_gemma3(): + vocab = { + "user\n": [110, 111, 112], + "model\n": [110, 113, 112], + "": [114], + "": [262144], + "": [120], + } + tok = _Tokenizer(vocab, pad_id=0, unk_id=3, extras={"boi_token": ""}) + return Gemma3ProcessingStrategy(_Processor(tok)) + + +def _make_gemma3n(): + vocab = { + "user\n": [110, 111, 112], + "model\n": [110, 113, 112], + "": [114], + } + tok = _Tokenizer( + vocab, + pad_id=0, + unk_id=3, + extras={ + "image_token_id": 130, + "audio_token_id": 131, + "boi_token_id": 132, + "eoi_token_id": 133, + }, + ) + return Gemma3nProcessingStrategy(_Processor(tok)) + + +def _make_gemma4(): + vocab = { + "<|turn>user\n": [105, 4001], + "<|turn>system\n": [105, 4002], + "<|turn>model\n": [105, 4368], + "": [106], + "": [140], + "": [141], + "": [142], + "": [143], + } + tok = _Tokenizer( + vocab, + pad_id=0, + unk_id=3, + extras={"image_token_id": 150, "audio_token_id": 151}, + ) + proc = _Processor( + tok, + boi_token="", + eoi_token="", + boa_token="", + eoa_token="", + video_token_id=160, + ) + return Gemma4ProcessingStrategy(proc) + + +def _make_llama32v(): + vocab = { + "<|start_header_id|>system<|end_header_id|>\n\n": [1001, 2002, 1002, 1003], + "<|start_header_id|>user<|end_header_id|>\n\n": [1001, 2001, 1002, 1003], + "<|start_header_id|>assistant<|end_header_id|>\n\n": [1001, 2003, 1002, 1003], + "<|eot_id|>": [1004], + } + tok = _Tokenizer(vocab, pad_id=0, unk_id=3) + return Llama3_2VisionProcessingStrategy(_Processor(tok)) + + +def _make_llama4(): + vocab = { + "<|header_start|>system<|header_end|>\n\n": [1001, 2002, 1002, 1003], + "<|header_start|>user<|header_end|>\n\n": [1001, 2001, 1002, 1003], + "<|header_start|>assistant<|header_end|>\n\n": [1001, 2003, 1002, 1003], + "<|eot|>": [1004], + } + tok = _Tokenizer(vocab, pad_id=0, unk_id=3) + return Llama4ProcessingStrategy(_Processor(tok)) + + +def _make_pixtral(): + vocab = {"[INST]": [50], "[/INST]": [51]} + tok = _Tokenizer(vocab, pad_id=0, unk_id=3, eos_id=99) + return PixtralProcessingStrategy(_Processor(tok)) + + +def _make_mistral_v7(): + vocab = { + "[INST]": [50], + "[/INST]": [51], + "[SYSTEM_PROMPT]": [60], + "[/SYSTEM_PROMPT]": [61], + } + tok = _Tokenizer(vocab, pad_id=0, unk_id=3, eos_id=99) + return MistralV7TekkenProcessingStrategy(_Processor(tok)) + + +# --------------------------------------------------------------------------- # +# Strategies that opt out of role-masking (no role_boundaries declared) +# --------------------------------------------------------------------------- # +# +# These four fall back to pad+media masking only. We still want parity fuzz +# coverage of the fused vs legacy paths because the per-strategy media-token +# combinations differ (audio for Voxtral, three image markers for Mistral3, +# variable-length image_ids list for InternVL, six markers for Glm4v). + + +def _make_voxtral(): + """VoxtralProcessor stub. + + The strategy resolves audio token ids via the nested mistral-common path + ``processor.tokenizer.tokenizer.instruct_tokenizer.audio_encoder.special_ids``. + Mirror just enough of that shape for __init__ to read the two ids. + """ + tok = _Tokenizer({}, pad_id=0, unk_id=3) + audio_encoder = SimpleNamespace( + special_ids=SimpleNamespace(audio=300, begin_audio=301) + ) + tok.tokenizer = SimpleNamespace( + instruct_tokenizer=SimpleNamespace(audio_encoder=audio_encoder) + ) + return VoxtralProcessingStrategy(_Processor(tok)) + + +def _make_mistral3(): + """Mistral3 stub: image_encoder.special_ids carries (img, img_break, img_end).""" + tok = _Tokenizer({}, pad_id=0, unk_id=3) + image_encoder = SimpleNamespace( + special_ids=SimpleNamespace(img=400, img_break=401, img_end=402) + ) + tok.tokenizer = SimpleNamespace( + instruct_tokenizer=SimpleNamespace(image_encoder=image_encoder) + ) + return Mistral3ProcessingStrategy(_Processor(tok)) + + +def _make_internvl(): + """InternVL stub: processor.image_ids is a list of media-token ids.""" + tok = _Tokenizer({}, pad_id=0, unk_id=3) + proc = _Processor(tok) + proc.image_ids = [500, 501, 502] + return InternVLProcessingStrategy(proc) + + +def _make_glm4v(): + """Glm4v / Glm46V stub: convert_tokens_to_ids resolves six media markers.""" + vocab = { + "<|image|>": [600], + "<|begin_of_image|>": [601], + "<|end_of_image|>": [602], + "<|video|>": [610], + "<|begin_of_video|>": [611], + "<|end_of_video|>": [612], + } + tok = _Tokenizer(vocab, pad_id=0, unk_id=3) + return Glm4vProcessingStrategy(_Processor(tok)) + + +# --------------------------------------------------------------------------- # +# Parameterized parity fuzz +# --------------------------------------------------------------------------- # + + +@pytest.mark.parametrize( + "strategy_factory, legacy_fn, extra_ids, name", + [ + (_make_qwen2vl, legacy_base_process_labels, [200], "qwen2vl"), + (_make_qwen3_5, legacy_qwen3_5_process_labels, [200, 201], "qwen3_5"), + (_make_gemma3, legacy_gemma3_process_labels, [120, 262144], "gemma3"), + (_make_gemma3n, legacy_gemma3n_process_labels, [130, 131, 132, 133], "gemma3n"), + ( + _make_gemma4, + legacy_gemma4_process_labels, + [140, 141, 142, 143, 150, 151, 160], + "gemma4", + ), + (_make_llama32v, legacy_base_process_labels, [], "llama32v"), + (_make_llama4, legacy_base_process_labels, [], "llama4"), + (_make_pixtral, legacy_base_process_labels, [], "pixtral"), + (_make_mistral_v7, legacy_base_process_labels, [], "mistral_v7"), + (_make_voxtral, legacy_voxtral_process_labels, [300, 301], "voxtral"), + ( + _make_mistral3, + legacy_mistral3_process_labels, + [400, 401, 402], + "mistral3", + ), + ( + _make_internvl, + legacy_internvl_process_labels, + [500, 501, 502], + "internvl", + ), + ( + _make_glm4v, + legacy_glm4v_process_labels, + [600, 601, 602, 610, 611, 612], + "glm4v", + ), + ], +) +@pytest.mark.parametrize("seed", list(range(50))) +def test_process_labels_parity_fuzz(strategy_factory, legacy_fn, extra_ids, name, seed): + """50 random batches per strategy: legacy vs fused must produce byte-identical labels.""" + rng = random.Random(seed * 7919 + sum(ord(c) for c in name) % 1000) + + strategy = strategy_factory() + pad_id = strategy.processor.tokenizer.pad_token_id + + # Mix of small / medium / large, plus varied batch sizes + batch_size = rng.choice([1, 2, 4, 8]) + seq_len = rng.choice([64, 128, 256, 512]) + + batch = _build_batch( + rng, strategy.role_boundaries, batch_size, seq_len, pad_id, extra_ids + ) + + expected = legacy_fn(strategy, batch.clone()) + got = strategy.process_labels(batch.clone()) + + if not torch.equal(expected, got): + # Find first divergence to make debug output useful + diff = (expected != got).nonzero(as_tuple=False) + first = diff[0].tolist() + b_idx, t_idx = first + lo = max(0, t_idx - 8) + hi = min(seq_len, t_idx + 8) + raise AssertionError( + f"[{name} seed={seed}] mismatch at row={b_idx} col={t_idx}\n" + f" input_ids[{b_idx}, {lo}:{hi}] = {batch[b_idx, lo:hi].tolist()}\n" + f" legacy [{b_idx}, {lo}:{hi}] = {expected[b_idx, lo:hi].tolist()}\n" + f" fused [{b_idx}, {lo}:{hi}] = {got[b_idx, lo:hi].tolist()}\n" + f" boundaries: {strategy.role_boundaries}" + ) + + +def test_process_labels_train_on_inputs_passthrough(): + """When train_on_inputs=True the keep mask must be all-True (only pad/media masked).""" + vocab = { + "<|im_start|>user\n": [101, 104, 103], + "<|im_start|>assistant\n": [101, 105, 103], + "<|im_end|>": [106], + "<|image_pad|>": [200], + } + tok = _Tokenizer(vocab, pad_id=0, unk_id=3) + s = Qwen2VLProcessingStrategy(_Processor(tok), train_on_inputs=True) + batch = torch.tensor([[101, 104, 103, 7, 8, 0, 200, 9, 106]]) + out = s.process_labels(batch.clone()) + expected = legacy_base_process_labels(s, batch.clone()) + assert torch.equal(out, expected) + # And confirm the only masked positions are pad and image + assert out.tolist() == [[101, 104, 103, 7, 8, -100, -100, 9, 106]] + + +def test_process_labels_no_boundaries_falls_through(): + """A boundary-less strategy + train_on_inputs=False keeps everything (warns once); + pad / media tokens are still masked. + """ + vocab = {"foo": [200]} + tok = _Tokenizer(vocab, pad_id=0, unk_id=3) + # Base strategy with no role_boundaries declared and train_on_inputs=False + s = ProcessingStrategy(_Processor(tok)) + assert s.role_boundaries == [] + batch = torch.tensor([[5, 6, 7, 0, 8]]) + out = s.process_labels(batch.clone()) + expected = legacy_base_process_labels(s, batch.clone()) + assert torch.equal(out, expected) + # Pad at index 3 must be masked, everything else kept (no boundaries → no role mask). + assert out.tolist() == [[5, 6, 7, -100, 8]] + + +# --------------------------------------------------------------------------- # +# Targeted edge cases for fused path +# --------------------------------------------------------------------------- # + + +def test_fused_pad_inside_assistant_is_masked(): + """Pad inside assistant span must end up -100 even though role-mask kept it.""" + vocab = { + "<|im_start|>user\n": [101, 104, 103], + "<|im_start|>assistant\n": [101, 105, 103], + "<|im_end|>": [106], + } + tok = _Tokenizer(vocab, pad_id=0, unk_id=3) + s = Qwen2VLProcessingStrategy(_Processor(tok)) + seq = [101, 104, 103, 7, 106, 101, 105, 103, 8, 0, 9, 106] + out = s.process_labels(torch.tensor([seq])) + expected = legacy_base_process_labels(s, torch.tensor([seq])) + assert torch.equal(out, expected) + # Pad (0) inside assistant gets masked, content (8, 9) and end (106) kept. + assert out.tolist() == [ + [-100, -100, -100, -100, -100, -100, -100, -100, 8, -100, 9, 106] + ] + + +def test_fused_image_inside_assistant_is_masked(): + vocab = { + "<|im_start|>user\n": [101, 104, 103], + "<|im_start|>assistant\n": [101, 105, 103], + "<|im_end|>": [106], + "<|image_pad|>": [200], + } + tok = _Tokenizer(vocab, pad_id=0, unk_id=3) + s = Qwen2VLProcessingStrategy(_Processor(tok)) + seq = [101, 105, 103, 8, 200, 9, 106] + out = s.process_labels(torch.tensor([seq])) + expected = legacy_base_process_labels(s, torch.tensor([seq])) + assert torch.equal(out, expected) + # image_token_id (200) gets masked even though it's inside the assistant span. + assert out.tolist() == [[-100, -100, -100, 8, -100, 9, 106]] diff --git a/tests/test_processing_strategies.py b/tests/test_processing_strategies.py index b2c4dd4d0b..98c4c2907e 100644 --- a/tests/test_processing_strategies.py +++ b/tests/test_processing_strategies.py @@ -1,6 +1,7 @@ """Tests for ``axolotl.processing_strategies`` using fake tokenizers (offline/CI-safe).""" import logging +from types import SimpleNamespace from unittest.mock import MagicMock import pytest @@ -11,14 +12,18 @@ Gemma3nProcessingStrategy, Gemma3ProcessingStrategy, Gemma4ProcessingStrategy, + Glm4vProcessingStrategy, + InternVLProcessingStrategy, Llama3_2VisionProcessingStrategy, Llama4ProcessingStrategy, + Mistral3ProcessingStrategy, MistralV7TekkenProcessingStrategy, PixtralProcessingStrategy, ProcessingStrategy, Qwen2VLProcessingStrategy, Qwen3_5ProcessingStrategy, RoleBoundary, + VoxtralProcessingStrategy, _apply_role_boundaries, get_processing_strategy, ) @@ -662,6 +667,174 @@ def test_mistral_v7_tekken_train_on_eos_all_respects_user_include_end_false(): assert out == [-100, -100, 41, -100, -100, -100, 8, 99] +# --------------------------------------------------------------------------- # +# Voxtral / Mistral3 / InternVL / Glm4v +# +# These four strategies do NOT declare role boundaries (mistral-common or +# template-variant uncertainty); they fall back to pad + media masking. +# Coverage focuses on the media-token masking surface plus role-boundary +# override as the supported opt-in path. +# --------------------------------------------------------------------------- # + + +def _make_voxtral_strategy(**kwargs): + """Voxtral stub: special_ids live at processor.tokenizer.tokenizer.instruct_tokenizer.audio_encoder.""" + tok = _Tokenizer({}, pad_id=0) + audio_encoder = SimpleNamespace( + special_ids=SimpleNamespace(audio=300, begin_audio=301) + ) + tok.tokenizer = SimpleNamespace( + instruct_tokenizer=SimpleNamespace(audio_encoder=audio_encoder) + ) + return VoxtralProcessingStrategy(_Processor(tok), **kwargs) + + +def test_voxtral_masks_pad_and_audio_tokens(): + """Default Voxtral: no role boundaries → keep content, mask pad + audio markers.""" + strategy = _make_voxtral_strategy() + # 300 = audio, 301 = begin_audio, 0 = pad. Other ids are kept. + seq = [7, 0, 300, 8, 301, 9] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [7, -100, -100, 8, -100, 9] + + +def test_voxtral_train_on_inputs_true_still_masks_audio(): + """Even with train_on_inputs, audio/pad tokens must be masked.""" + strategy = _make_voxtral_strategy(train_on_inputs=True) + seq = [7, 0, 300, 8, 301, 9] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [7, -100, -100, 8, -100, 9] + + +def test_voxtral_role_boundaries_override_enables_role_masking(): + """Override opt-in: role masking applies on top of pad/audio masking.""" + tok = _Tokenizer({"BOA": [50], "EOT": [60]}, pad_id=0) + audio_encoder = SimpleNamespace( + special_ids=SimpleNamespace(audio=300, begin_audio=301) + ) + tok.tokenizer = SimpleNamespace( + instruct_tokenizer=SimpleNamespace(audio_encoder=audio_encoder) + ) + strategy = VoxtralProcessingStrategy( + _Processor(tok), + role_boundaries_override=[{"role": "assistant", "start": "BOA", "end": "EOT"}], + ) + # Pre-BOA + post-EOT masked; BOA(50) and EOT(60) included by default; + # audio token 300 inside the span still masked. + seq = [7, 50, 8, 300, 9, 60, 1] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100, -100, 8, -100, 9, 60, -100] + + +def _make_mistral3_strategy(**kwargs): + tok = _Tokenizer({}, pad_id=0) + image_encoder = SimpleNamespace( + special_ids=SimpleNamespace(img=400, img_break=401, img_end=402) + ) + tok.tokenizer = SimpleNamespace( + instruct_tokenizer=SimpleNamespace(image_encoder=image_encoder) + ) + return Mistral3ProcessingStrategy(_Processor(tok), **kwargs) + + +def test_mistral3_masks_pad_and_three_image_tokens(): + strategy = _make_mistral3_strategy() + seq = [7, 0, 400, 8, 401, 9, 402, 10] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [7, -100, -100, 8, -100, 9, -100, 10] + + +def test_mistral3_train_on_inputs_true_still_masks_image_markers(): + strategy = _make_mistral3_strategy(train_on_inputs=True) + seq = [7, 0, 400, 8, 401, 9, 402, 10] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [7, -100, -100, 8, -100, 9, -100, 10] + + +def test_internvl_masks_pad_and_image_id_list(): + """InternVL stores a *list* of image ids on processor.image_ids.""" + tok = _Tokenizer({}, pad_id=0) + proc = _Processor(tok) + proc.image_ids = [500, 501, 502] + strategy = InternVLProcessingStrategy(proc) + seq = [7, 0, 500, 8, 501, 9, 502, 10] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [7, -100, -100, 8, -100, 9, -100, 10] + + +def test_internvl_raises_when_image_ids_missing(): + """No image_ids on the processor → __init__ must surface a clear error.""" + tok = _Tokenizer({}, pad_id=0) + with pytest.raises(ValueError, match="image_ids"): + InternVLProcessingStrategy(_Processor(tok)) + + +def test_internvl_image_ids_with_none_entries_skipped(): + """None entries in image_ids must not be used as a mask value.""" + tok = _Tokenizer({}, pad_id=0) + proc = _Processor(tok) + proc.image_ids = [500, None, 502] + strategy = InternVLProcessingStrategy(proc) + seq = [500, 7, 502, 8] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [-100, 7, -100, 8] + + +def _glm4v_tokenizer(): + vocab = { + "<|image|>": [600], + "<|begin_of_image|>": [601], + "<|end_of_image|>": [602], + "<|video|>": [610], + "<|begin_of_video|>": [611], + "<|end_of_video|>": [612], + } + return _Tokenizer(vocab, pad_id=0) + + +def test_glm4v_masks_pad_image_and_video_markers(): + strategy = Glm4vProcessingStrategy(_Processor(_glm4v_tokenizer())) + # All six markers + pad must be masked; surrounding text tokens kept. + seq = [7, 0, 600, 601, 602, 8, 610, 611, 612, 9] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [7, -100, -100, -100, -100, 8, -100, -100, -100, 9] + + +def test_glm4v_train_on_inputs_true_still_masks_media_markers(): + strategy = Glm4vProcessingStrategy( + _Processor(_glm4v_tokenizer()), train_on_inputs=True + ) + seq = [7, 0, 600, 8, 610, 9] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + assert out == [7, -100, -100, 8, -100, 9] + + +def test_glm4v_role_boundaries_override_enables_role_masking(): + """Override opt-in masks non-assistant role spans on top of media masking.""" + tok = _glm4v_tokenizer() + tok.vocab["BOA"] = [50] + tok.vocab["EOT"] = [60] + strategy = Glm4vProcessingStrategy( + _Processor(tok), + role_boundaries_override=[{"role": "assistant", "start": "BOA", "end": "EOT"}], + ) + seq = [7, 50, 8, 600, 9, 60, 1] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + # Pre-BOA + post-EOT masked; image marker 600 inside the assistant span + # is still masked post-scan. + assert out == [-100, -100, 8, -100, 9, 60, -100] + + +def test_glm4v_batch_of_two_rows(): + """Multiple rows independently masked; pad in trailing positions kept as -100.""" + strategy = Glm4vProcessingStrategy(_Processor(_glm4v_tokenizer())) + row_a = [7, 600, 8, 610, 9] + row_b = [11, 601, 12, 0, 0] + out = strategy.process_labels(torch.tensor([row_a, row_b])).tolist() + assert out[0] == [7, -100, 8, -100, 9] + assert out[1] == [11, -100, 12, -100, -100] + + # --------------------------------------------------------------------------- # # Dispatcher routing # --------------------------------------------------------------------------- # @@ -791,6 +964,85 @@ def test_dispatch_glm4v_via_Glm46VProcessor(): assert isinstance(s, Glm4vProcessingStrategy) +def test_dispatch_voxtral(): + """VoxtralProcessor routes to VoxtralProcessingStrategy.""" + pytest.importorskip("transformers.models.voxtral") + from transformers.models.voxtral import VoxtralProcessor + + tok = _Tokenizer({}, pad_id=0) + audio_encoder = SimpleNamespace( + special_ids=SimpleNamespace(audio=300, begin_audio=301) + ) + tok.tokenizer = SimpleNamespace( + instruct_tokenizer=SimpleNamespace(audio_encoder=audio_encoder) + ) + proc = MagicMock(spec=VoxtralProcessor) + proc.tokenizer = tok + if hasattr(proc, "image_token"): + del proc.image_token + + s = _dispatch(proc, None) + assert isinstance(s, VoxtralProcessingStrategy) + + +def test_dispatch_mistral3(): + """Mistral3Processor (axolotl's optional wrapper) routes to Mistral3ProcessingStrategy.""" + pytest.importorskip("mistral_common") + Mistral3Processor = pytest.importorskip( + "axolotl.utils.mistral.mistral3_processor" + ).Mistral3Processor + + tok = _Tokenizer({}, pad_id=0) + image_encoder = SimpleNamespace( + special_ids=SimpleNamespace(img=400, img_break=401, img_end=402) + ) + tok.tokenizer = SimpleNamespace( + instruct_tokenizer=SimpleNamespace(image_encoder=image_encoder) + ) + proc = MagicMock(spec=Mistral3Processor) + proc.tokenizer = tok + if hasattr(proc, "image_token"): + del proc.image_token + + s = _dispatch(proc, None) + assert isinstance(s, Mistral3ProcessingStrategy) + + +def test_dispatch_internvl(): + """InternVLProcessor routes to InternVLProcessingStrategy.""" + pytest.importorskip("transformers.models.internvl") + from transformers.models.internvl import InternVLProcessor + + tok = _Tokenizer({}, pad_id=0) + proc = MagicMock(spec=InternVLProcessor) + proc.tokenizer = tok + proc.image_ids = [500, 501, 502] + if hasattr(proc, "image_token"): + del proc.image_token + + s = _dispatch(proc, None) + assert isinstance(s, InternVLProcessingStrategy) + + +def test_mistral3_role_boundaries_override_enables_role_masking(): + """Mistral3 override opt-in: role masking applies on top of pad/image masking.""" + tok = _Tokenizer({"BOA": [50], "EOT": [60]}, pad_id=0) + image_encoder = SimpleNamespace( + special_ids=SimpleNamespace(img=400, img_break=401, img_end=402) + ) + tok.tokenizer = SimpleNamespace( + instruct_tokenizer=SimpleNamespace(image_encoder=image_encoder) + ) + strategy = Mistral3ProcessingStrategy( + _Processor(tok), + role_boundaries_override=[{"role": "assistant", "start": "BOA", "end": "EOT"}], + ) + seq = [7, 50, 8, 400, 9, 60, 1] + out = strategy.process_labels(torch.tensor([seq])).tolist()[0] + # Pre-BOA + post-EOT masked; image marker 400 inside the span also masked. + assert out == [-100, -100, 8, -100, 9, 60, -100] + + # --------------------------------------------------------------------------- # # Config-based role-boundary override # --------------------------------------------------------------------------- # diff --git a/tests/test_vectorized_scanner.py b/tests/test_vectorized_scanner.py new file mode 100644 index 0000000000..056b469814 --- /dev/null +++ b/tests/test_vectorized_scanner.py @@ -0,0 +1,650 @@ +"""Differential and targeted edge-case tests for the vectorized role-boundary scanner. + +The vectorized scanner (:func:`axolotl.processing_strategies._apply_role_boundaries_vectorized`) +must be byte-identical to the reference implementation +(:func:`axolotl.processing_strategies._apply_role_boundaries`) for every valid input. + +This file is structured in three layers: + +1. Targeted edge-case tests covering each behavioral subtlety called out in + the implementation comments (longest-prefix tie-break, Pixtral rewind, + train_on_eos modes, empty end_tokens, include_end leak gate). + +2. A boundary-shape catalog mimicking the real models: Gemma 4, Llama 3.2 V, + Llama 4, Pixtral, Mistral V7. + +3. A differential fuzzer that generates 2,000 random configurations and + asserts vectorized output == reference output element-wise. On mismatch, + dumps inputs + outputs + boundary spec. +""" + +from __future__ import annotations + +import random +from dataclasses import asdict +from typing import Iterable + +import pytest +import torch + +from axolotl.processing_strategies import ( + RoleBoundary, + _apply_role_boundaries, + _apply_role_boundaries_vectorized, +) + +# --------------------------------------------------------------------------- # +# Helpers +# --------------------------------------------------------------------------- # + + +def _assert_equiv( + boundaries: list[RoleBoundary], + seq: list[list[int]], + roles_to_train: Iterable[str], + train_on_eos: str, +): + """Assert vectorized output == reference output for the given input.""" + labels_a = torch.tensor(seq) + labels_b = labels_a.clone() + out_ref = _apply_role_boundaries( + labels_a, boundaries, set(roles_to_train), train_on_eos + ) + out_vec = _apply_role_boundaries_vectorized( + labels_b, boundaries, set(roles_to_train), train_on_eos + ) + if not torch.equal(out_ref, out_vec): + # Build a focused failure dump. + diff = (out_ref != out_vec).nonzero(as_tuple=False).tolist() + msg = ( + "Vectorized scanner diverged from reference.\n" + f" boundaries: {[asdict(b) for b in boundaries]}\n" + f" roles_to_train: {sorted(roles_to_train)}\n" + f" train_on_eos: {train_on_eos}\n" + f" input seq: {seq}\n" + f" reference: {out_ref.tolist()}\n" + f" vectorized: {out_vec.tolist()}\n" + f" first 10 diff indices: {diff[:10]}\n" + ) + raise AssertionError(msg) + return out_ref + + +# --------------------------------------------------------------------------- # +# Layer 3: Targeted edge-case unit tests +# --------------------------------------------------------------------------- # + + +class TestEdgeCases: + """Hand-written cases that pin down each tricky behavior.""" + + def test_longest_prefix_wins_at_same_position(self): + """``<|im_start|>assistant`` (long) beats ``<|im_start|>`` (short).""" + # token 100 = <|im_start|>, then 101 = "user", 102 = "assistant" + boundaries = [ + RoleBoundary(role="user", start_tokens=[100], end_tokens=[200]), + RoleBoundary(role="assistant", start_tokens=[100, 102], end_tokens=[200]), + ] + seq = [[100, 102, 1, 2, 3, 200, 100, 101, 4, 5, 6, 200]] + # The longer "assistant" boundary should win at j=0; only positions + # 1..5 are trainable (assistant content + end marker because + # train_on_eos="turn" includes end on trainable turns by default). + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + def test_pixtral_shared_end_marker_rewind(self): + """[/INST] both ends user and starts assistant; rewind must re-match.""" + # Pixtral: user=[INST] ... [/INST], assistant content, then
. + # The shared [/INST] must consume as user-end *and* assistant-start. + INST_OPEN = 100 # [INST] + INST_CLOSE = 200 # [/INST] + EOS = 2 # + boundaries = [ + RoleBoundary( + role="user", + start_tokens=[INST_OPEN], + end_tokens=[INST_CLOSE], + include_start=False, + include_end=False, # critical: don't consume — let assistant re-match + ), + RoleBoundary( + role="assistant", + start_tokens=[INST_CLOSE], + end_tokens=[EOS], + include_start=False, + include_end=True, + ), + ] + # [INST] hello [/INST] reply + seq = [[INST_OPEN, 5, 6, INST_CLOSE, 7, 8, 9, EOS]] + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + def test_train_on_eos_turn(self): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[100], end_tokens=[200]) + ] + seq = [[100, 1, 2, 3, 200, 9, 9, 9, 100, 4, 5, 6, 200]] + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + def test_train_on_eos_all(self): + boundaries = [ + RoleBoundary(role="user", start_tokens=[100], end_tokens=[200]), + RoleBoundary(role="assistant", start_tokens=[101], end_tokens=[200]), + ] + seq = [[100, 1, 2, 200, 101, 3, 4, 200, 9]] + _assert_equiv(boundaries, seq, ["assistant"], "all") + + def test_train_on_eos_none(self): + """train_on_eos=none disables the end-marker contribution entirely.""" + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[100], end_tokens=[200]) + ] + seq = [[100, 1, 2, 3, 200, 9]] + _assert_equiv(boundaries, seq, ["assistant"], "none") + + def test_train_on_eos_last_only_final_turn_unmasked(self): + """Only the last trainable turn's end marker contributes.""" + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[100], end_tokens=[200]) + ] + # Three assistant turns. Only the last 200 should be unmasked. + seq = [[100, 1, 200, 100, 2, 200, 100, 3, 200, 9, 9]] + _assert_equiv(boundaries, seq, ["assistant"], "last") + + def test_empty_end_tokens_runs_to_eos(self): + """end_tokens=[] means the span runs to end-of-sequence.""" + boundaries = [RoleBoundary(role="assistant", start_tokens=[100], end_tokens=[])] + seq = [[100, 1, 2, 3, 4, 5]] + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + def test_include_start_true(self): + boundaries = [ + RoleBoundary( + role="assistant", + start_tokens=[100, 101], + end_tokens=[200], + include_start=True, + include_end=True, + ) + ] + seq = [[100, 101, 5, 6, 7, 200]] + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + def test_non_trainable_role_end_marker_leak_gate(self): + """Non-trainable role with include_end=True, train_on_eos=all → end is unmasked.""" + boundaries = [ + RoleBoundary( + role="user", + start_tokens=[100], + end_tokens=[200], + include_start=False, + include_end=True, + ), + RoleBoundary( + role="assistant", + start_tokens=[101], + end_tokens=[201], + include_start=False, + include_end=True, + ), + ] + seq = [[100, 1, 2, 200, 101, 3, 4, 201]] + # roles_to_train=["assistant"] but train_on_eos="all" → user's end (200) leaks in + _assert_equiv(boundaries, seq, ["assistant"], "all") + + def test_non_trainable_role_include_end_false_no_leak(self): + """Non-trainable role with include_end=False, train_on_eos=all → no leak.""" + boundaries = [ + RoleBoundary( + role="user", + start_tokens=[100], + end_tokens=[200], + include_start=False, + include_end=False, + ), + RoleBoundary( + role="assistant", + start_tokens=[200], # shared with user-end + end_tokens=[201], + ), + ] + seq = [[100, 1, 2, 200, 3, 4, 201]] + _assert_equiv(boundaries, seq, ["assistant"], "all") + + def test_truncated_final_turn_no_end(self): + """Final assistant turn missing end marker — span runs to end.""" + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[100], end_tokens=[200]) + ] + seq = [[100, 1, 2, 3, 4, 5, 6]] # no 200 anywhere + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + def test_all_pad_row(self): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[100], end_tokens=[200]) + ] + seq = [[0, 0, 0, 0, 0, 0]] + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + def test_single_token_row(self): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[100], end_tokens=[200]) + ] + seq = [[100]] # start with no end & no content + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + def test_empty_roles_to_train_masks_all(self): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[100], end_tokens=[200]) + ] + seq = [[100, 1, 2, 200, 100, 3, 4, 200]] + _assert_equiv(boundaries, seq, [], "turn") + + def test_adversarial_first_token_collision(self): + """A bare token equal to start_tokens[0] inside filler must not trigger + a partial match (multi-token start_tokens needed).""" + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[100, 200], end_tokens=[201]) + ] + # 100 appears in filler (alone, not followed by 200) → must NOT match. + seq = [[100, 200, 5, 6, 100, 7, 8, 201, 100, 200, 9, 10, 201]] + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + def test_batch_of_mixed_rows(self): + boundaries = [ + RoleBoundary(role="assistant", start_tokens=[100], end_tokens=[200]) + ] + # Three rows with different shapes. + seq = [ + [100, 1, 2, 200, 0, 0, 0, 0], + [100, 3, 4, 5, 6, 200, 0, 0], + [9, 9, 100, 7, 200, 9, 9, 9], + ] + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + +# --------------------------------------------------------------------------- # +# Layer 2-ish: Boundary-shape catalog (real-model-like configs) +# --------------------------------------------------------------------------- # + + +def _gemma4_like(): + """Gemma 4: role ... .""" + SOT, EOT = 50, 60 + return [ + RoleBoundary(role="user", start_tokens=[SOT, 70], end_tokens=[EOT]), + RoleBoundary(role="assistant", start_tokens=[SOT, 71], end_tokens=[EOT]), + RoleBoundary(role="system", start_tokens=[SOT, 72], end_tokens=[EOT]), + ] + + +def _llama32v_like(): + """Llama 3.2 V: <|start_header_id|>role<|end_header_id|> ... <|eot_id|>.""" + SHID, EHID, EOT = 80, 81, 82 + return [ + RoleBoundary(role="user", start_tokens=[SHID, 90, EHID], end_tokens=[EOT]), + RoleBoundary(role="assistant", start_tokens=[SHID, 91, EHID], end_tokens=[EOT]), + RoleBoundary(role="system", start_tokens=[SHID, 92, EHID], end_tokens=[EOT]), + ] + + +def _llama4_like(): + """Llama 4-style: similar to llama3 but 4 roles.""" + SHID, EHID, EOT = 80, 81, 82 + return [ + RoleBoundary(role="user", start_tokens=[SHID, 90, EHID], end_tokens=[EOT]), + RoleBoundary(role="assistant", start_tokens=[SHID, 91, EHID], end_tokens=[EOT]), + RoleBoundary(role="system", start_tokens=[SHID, 92, EHID], end_tokens=[EOT]), + RoleBoundary(role="tool", start_tokens=[SHID, 93, EHID], end_tokens=[EOT]), + ] + + +def _pixtral_like(): + """Pixtral: shared [/INST] between user-end and assistant-start, EOS terminates.""" + INST_O, INST_C, EOS = 100, 200, 2 + return [ + RoleBoundary( + role="user", + start_tokens=[INST_O], + end_tokens=[INST_C], + include_start=False, + include_end=False, + ), + RoleBoundary( + role="assistant", + start_tokens=[INST_C], + end_tokens=[EOS], + include_start=False, + include_end=True, + ), + ] + + +def _mistralv7_like(): + """Mistral V7 Tekken-ish: similar shared [/INST] pattern with system.""" + INST_O, INST_C, EOS, SYS = 100, 200, 2, 110 + return [ + RoleBoundary( + role="system", + start_tokens=[SYS], + end_tokens=[INST_O], + include_start=False, + include_end=False, + ), + RoleBoundary( + role="user", + start_tokens=[INST_O], + end_tokens=[INST_C], + include_start=False, + include_end=False, + ), + RoleBoundary( + role="assistant", + start_tokens=[INST_C], + end_tokens=[EOS], + include_start=False, + include_end=True, + ), + ] + + +BOUNDARY_CATALOG = { + "gemma4": _gemma4_like, + "llama32v": _llama32v_like, + "llama4": _llama4_like, + "pixtral": _pixtral_like, + "mistralv7": _mistralv7_like, +} + + +# --------------------------------------------------------------------------- # +# Layer 1: Differential fuzz test +# --------------------------------------------------------------------------- # + + +def _build_random_sequence( + rng: random.Random, + boundaries: list[RoleBoundary], + seq_len: int, + pad_id: int = 0, +) -> list[int]: + """Build a plausible sequence by alternating turns until we hit seq_len. + + Uses random filler tokens that *don't* collide with any marker. ~5% of + fillers are pathologically chosen as adversarial collisions: a single + token equal to start_tokens[0] (so that multi-token starts don't match + but the first byte does). + """ + # Build a vocab of "safe" filler tokens that don't collide with any + # start_tokens prefix. + marker_tokens: set[int] = set() + for b in boundaries: + marker_tokens.update(b.start_tokens) + marker_tokens.update(b.end_tokens) + safe_filler = [t for t in range(300, 500) if t not in marker_tokens] + + seq: list[int] = [] + while len(seq) < seq_len: + # Pick a boundary at random; emit start + filler + (sometimes) end. + b = rng.choice(boundaries) + seq.extend(b.start_tokens) + filler_n = rng.randint(5, min(200, max(5, seq_len - len(seq)))) + for _ in range(filler_n): + if rng.random() < 0.05 and safe_filler: + # Adversarial: emit a token that equals a marker's first byte + # but isn't followed by the rest. Picks from any boundary. + bb = rng.choice(boundaries) + if bb.start_tokens: + seq.append(bb.start_tokens[0]) + continue + seq.append(rng.choice(safe_filler)) + + # 80% chance of a clean end. (Truncated turns intentional.) + if rng.random() < 0.8 and b.end_tokens: + seq.extend(b.end_tokens) + + seq = seq[:seq_len] + # Pad up if we underran. + while len(seq) < seq_len: + seq.append(pad_id) + return seq + + +@pytest.mark.parametrize( + "shape_name", + ["gemma4", "llama32v", "llama4", "pixtral", "mistralv7"], +) +def test_catalog_smoke_per_shape(shape_name): + """Smoke test: each catalog shape works on a small batch.""" + boundaries = BOUNDARY_CATALOG[shape_name]() + rng = random.Random(0xCAFE) + rows = [_build_random_sequence(rng, boundaries, 256) for _ in range(4)] + _assert_equiv(boundaries, rows, ["assistant"], "turn") + _assert_equiv(boundaries, rows, ["assistant", "user"], "all") + _assert_equiv(boundaries, rows, [], "none") + _assert_equiv(boundaries, rows, ["assistant"], "last") + + +def test_differential_fuzz_2000_configs(): + """Run 2000 random configurations and verify byte-identical outputs. + + Uses fixed seeds 0..1999 so any failure is deterministically reproducible. + """ + failures: list[tuple[int, str]] = [] + + BATCH_SIZES = [1, 2, 4, 8] + SEQ_LENS = [32, 256, 1024, 4096] + EOS_MODES = ["turn", "all", "none", "last"] + ROLES_OPTIONS = [ + ["assistant"], + ["assistant", "user"], + [], + ["assistant", "system", "user"], + ] + SHAPES = list(BOUNDARY_CATALOG.keys()) + + N_CONFIGS = 2000 + + for seed in range(N_CONFIGS): + rng = random.Random(seed) + bs = rng.choice(BATCH_SIZES) + sl = rng.choice(SEQ_LENS) + eos = rng.choice(EOS_MODES) + rtt = rng.choice(ROLES_OPTIONS) + shape = rng.choice(SHAPES) + boundaries = BOUNDARY_CATALOG[shape]() + + # Cap the largest pixtral-shape configs at 1024 to keep wall-time + # under control; the small/medium configs already cover the rewind. + if shape == "pixtral" and sl == 4096 and bs == 8: + sl = 1024 + + rows = [_build_random_sequence(rng, boundaries, sl) for _ in range(bs)] + + try: + _assert_equiv(boundaries, rows, rtt, eos) + except AssertionError as e: + failures.append((seed, str(e))) + if len(failures) >= 5: + break + + if failures: + joined = "\n\n---\n\n".join(f"seed={s}:\n{m}" for s, m in failures) + pytest.fail( + f"Differential fuzz: {len(failures)} mismatches in " + f"{N_CONFIGS} configs.\n\n{joined}" + ) + + +# --------------------------------------------------------------------------- # +# Pathological inputs aimed at the rewind logic specifically +# --------------------------------------------------------------------------- # + + +class TestPixtralRewindAdversarial: + def test_back_to_back_user_assistant_pairs(self): + boundaries = _pixtral_like() + # Five back-to-back turns. + seq = [ + [ + 100, + 1, + 2, + 200, + 3, + 4, + 5, + 2, # turn 1 + 100, + 6, + 7, + 200, + 8, + 9, + 2, # turn 2 + 100, + 10, + 11, + 12, + 200, + 13, + 14, + 2, + 100, + 15, + 200, + 16, + 2, + 100, + 17, + 18, + 19, + 20, + 200, + 21, + 22, + 2, + ] + ] + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + def test_user_with_no_end_then_assistant(self): + """Truncated user — never closes — assistant never re-matches the [/INST].""" + boundaries = _pixtral_like() + seq = [[100, 1, 2, 3, 4, 5, 6]] # no [/INST] anywhere + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + def test_double_end_marker(self): + """Two [/INST] in a row — second one starts a (degenerate) assistant.""" + boundaries = _pixtral_like() + seq = [[100, 1, 200, 200, 5, 6, 2]] + _assert_equiv(boundaries, seq, ["assistant"], "turn") + + +# --------------------------------------------------------------------------- # +# Long-span / multi-end-marker inputs aimed at the bisect end-finder +# --------------------------------------------------------------------------- # + + +def _build_long_multi_end_sequence( + rng: random.Random, + boundaries: list[RoleBoundary], + seq_len: int, + pad_id: int = 0, +) -> list[int]: + """Like ``_build_random_sequence`` but with long turns that embed *multiple* + full end-marker copies inside one content region. + + The vectorized scanner finds turn ends by bisecting a sorted list of + end-match positions for the next end >= start_of_content. Turns that carry + several full end markers (plus partial first-byte collisions) exercise the + "pick the first valid end, not the closest scanned" branch that a linear + walk would otherwise mask. + """ + marker_tokens: set[int] = set() + for b in boundaries: + marker_tokens.update(b.start_tokens) + marker_tokens.update(b.end_tokens) + safe_filler = [t for t in range(300, 500) if t not in marker_tokens] + + seq: list[int] = [] + while len(seq) < seq_len: + b = rng.choice(boundaries) + seq.extend(b.start_tokens) + # Long filler so a turn can span well past the old 200-token cap. + filler_n = rng.randint(200, max(200, min(1500, seq_len - len(seq) + 200))) + for _ in range(filler_n): + r = rng.random() + if r < 0.04 and b.end_tokens: + # Full extra end marker mid-content: with include_end this closes + # the turn early; without it the rewind re-reads it as a start. + seq.extend(b.end_tokens) + elif r < 0.09 and safe_filler: + bb = rng.choice(boundaries) + if bb.start_tokens: + seq.append(bb.start_tokens[0]) # partial first-byte collision + continue + seq.append(rng.choice(safe_filler)) + else: + seq.append(rng.choice(safe_filler)) + if rng.random() < 0.8 and b.end_tokens: + seq.extend(b.end_tokens) + + seq = seq[:seq_len] + while len(seq) < seq_len: + seq.append(pad_id) + return seq + + +def test_bisect_first_end_in_span_explicit(): + """Two full end markers inside one assistant turn: the span must close on the + first, leaving the second outside the trainable region.""" + boundaries = _pixtral_like() # [/INST] == [200], rewind on include_end=False + # assistant turn opens at [/INST] (200), content, end-of-turn (2) appears + # twice; the first 2 closes the turn, everything after is a fresh scan. + seq = [[100, 1, 2, 200, 5, 6, 7, 2, 9, 9, 9, 2, 100, 11, 200, 12, 2]] + _assert_equiv(boundaries, seq, ["assistant"], "turn") + _assert_equiv(boundaries, seq, ["assistant"], "all") + _assert_equiv(boundaries, seq, ["assistant"], "last") + _assert_equiv(boundaries, seq, ["assistant"], "none") + + +def test_differential_fuzz_long_spans(): + """500 configs with long, multi-end-marker turns over large sequences. + + Targets the bisect end-finder and bytearray slice-fills, which the original + short-span fuzz (filler <= 200) under-exercises. + """ + failures: list[tuple[int, str]] = [] + + BATCH_SIZES = [1, 2, 4] + SEQ_LENS = [1024, 2048, 4096] + EOS_MODES = ["turn", "all", "none", "last"] + ROLES_OPTIONS = [["assistant"], ["assistant", "user"], [], ["assistant", "system"]] + SHAPES = list(BOUNDARY_CATALOG.keys()) + + N_CONFIGS = 500 + + for seed in range(N_CONFIGS): + rng = random.Random(10_000 + seed) + bs = rng.choice(BATCH_SIZES) + sl = rng.choice(SEQ_LENS) + eos = rng.choice(EOS_MODES) + rtt = rng.choice(ROLES_OPTIONS) + shape = rng.choice(SHAPES) + boundaries = BOUNDARY_CATALOG[shape]() + + rows = [_build_long_multi_end_sequence(rng, boundaries, sl) for _ in range(bs)] + + try: + _assert_equiv(boundaries, rows, rtt, eos) + except AssertionError as e: + failures.append((seed, str(e))) + if len(failures) >= 5: + break + + if failures: + joined = "\n\n---\n\n".join(f"seed={s}:\n{m}" for s, m in failures) + pytest.fail( + f"Long-span differential fuzz: {len(failures)} mismatches in " + f"{N_CONFIGS} configs.\n\n{joined}" + ) From 22bcb9a773cb9de49bdcabca5be61c09b420b736 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Tue, 9 Jun 2026 15:22:57 -0400 Subject: [PATCH 1351/1405] Multi-adapter MoE LoRA support (#3719) * feat(scattermoe-lora): multi-adapter (expert x tenant) LoRA path Co-train many LoRA adapters ("tenants") over one frozen expert stack: an MoE token carries the router's expert assignment AND a per-row tenant id, so the base expert GEMM keys on expert while the LoRA keys on the combined (expert, tenant) group (LoRA stacked over E*T). Key trick: combined = expert*T + tenant is also sorted by expert, so a single sort yields both the expert grouping (base) and the combined grouping (LoRA). ScatterMoEMultiLoRA reuses the existing scatter2scatter base + LoRA-projection ops with combined offsets/ids; T==1 reduces to the single-adapter behaviour exactly. - multi_lora.py: build_multilora_routing() (one sort -> both groupings), ScatterMoEMultiLoRA autograd Function (frozen W; grads to per-(expert,tenant) A/B and X), scatter2scatter_multilora() entry point. - Backward is sync-free and grouped: dX_lora = scaling*(dY@B)@A as two scatter2scatter GEMMs keyed by the combined id (no Python per-expert expert_offsets[e].item() sync loop) -- up to 57x vs the loop on a 128-expert config (28.0 -> 0.49 ms at T=8), CUDA-graph / torch.compile friendly. - dA/dB via a dedicated grouped-Gram kernel (_grouped_gram_kernel) fed the forward's saved XA and the backward's YB: token-dim reduction only, writing each tile straight into the [E*T*R,K] / [N,E*T*R] grad layout (empty (expert,tenant) groups self-zero). 17-27x vs the shared split kernel at E=8/T=32, exact bf16 parity (dA/dB err 0.0). The shared single-adapter kernel in lora_ops.py is left byte-for-byte unchanged. - test: forward + backward (dX/dA/dB) vs a dense reference across shapes incl. T=1, many-group (forces empty groups), and rank-64 (BLOCK_R>16); a misroute-detection guard confirms routing is actually checked. Follow-up: wire into the ScatterMoE layer forward (build combined ids from the router's expert ids + the tenant routing); a single fully-fused inner kernel (separate LoRA index inside _compute_expert_block_lora) is a further perf step. * feat(sonicmoe): multi-adapter (expert x tenant) LoRA via combined materialization SonicMoE dispatches opaque CUTLASS/CuteDSL grouped GEMMs that can't fuse LoRA, so the single-adapter path materializes W_eff[e] = W[e] + scaling*(B_e@A_e) and hands it to the kernel. Extend that to multi-tenant co-training over the combined (expert, tenant) grouping: materialize all E*T effective weights W_eff[e*T+t] = W[e] + scaling_t*(B_{e,t}@A_{e,t}) and remap each token-slot's expert id to e*T + t, so the same opaque GEMM (num_experts = E*T) routes every tenant in one launch. The kernel is unchanged; T==1 reduces to the single-adapter MoELoRAMaterialize exactly. - multi_lora.py: MoEMultiLoRAMaterialize (autograd; frozen base, grads to per- (e,t) A/B), materialize_multi_lora_experts(), combined_expert_ids() id remap. Materialized directly in combined order (einsum over the [E,T,...] view yields a contiguous [E*T,...] -- no reorder copy), with in-place scale/base-add and scaling folded onto the rank-sized grads, so the op's footprint is exactly 1x W_eff (one buffer). - test: forward+backward vs a per-tenant standalone reference through an exact PyTorch grouped-GEMM stand-in (the real CUTLASS kernel needs the sonicmoe pkg, absent on sm_120); T==1 == single-adapter; id-remap unit check. Memory is the design tradeoff: W_eff is T x the expert stack (intrinsic to the opaque kernel -- the shared frozen base must be broadcast into each tenant's group). A per-tenant loop (T launches, 1x W_eff) is the memory-bound alternative if T is large; left as a follow-up alongside wiring this into the experts forward. * fix(scattermoe-lora): honor live ALLOW_TF32 in grouped-Gram dA/dB backward grouped_gram.py and multi_lora.py did `from .lora_ops import ALLOW_TF32`, binding the bool *value* at import time. Toggling `lora_ops.ALLOW_TF32` (e.g. to force true fp32) therefore never reached the grouped-Gram dA/dB kernels, which kept running TF32 while the forward GEMMs honored the flag -- LoRA weight grads carried ~8e-4 relative error (textbook TF32, 2^-10) vs ~4e-7 for the fp32 forward. Surfaced by the fp32 gpt_oss LoRA test (tol 3e-4) on main; the multi-adapter path has the same latent bug in its own grouped-Gram kernel. Reference the live `lora_ops.ALLOW_TF32` module global at call time instead. With the default (True) production numerics are byte-identical, and bf16/fp16 training is unaffected (allow_tf32 only applies to fp32-input tl.dot). The flag is now consistent across the whole backward: disabling TF32 (or training in fp32) yields true-fp32 dA/dB, error 8e-4 -> ~1e-7, matching the forward. Adds a multi-adapter regression test that disables TF32 and asserts tight fp32 parity (dA/dB ~1e-7) -- would fail at ~1e-3 with the stale import-time copy. --- .../kernels/libs/scattermoe_lora/__init__.py | 5 + .../scattermoe_lora/kernels/grouped_gram.py | 7 +- .../libs/scattermoe_lora/multi_lora.py | 455 ++++++++++++++++++ .../kernels/libs/sonicmoe/__init__.py | 8 + .../kernels/libs/sonicmoe/multi_lora.py | 94 ++++ .../test_scattermoe_multi_lora.py | 144 ++++++ .../integrations/test_sonicmoe_multi_lora.py | 104 ++++ 7 files changed, 814 insertions(+), 3 deletions(-) create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/multi_lora.py create mode 100644 src/axolotl/integrations/kernels/libs/sonicmoe/multi_lora.py create mode 100644 tests/integrations/test_scattermoe_multi_lora.py create mode 100644 tests/integrations/test_sonicmoe_multi_lora.py diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/__init__.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/__init__.py index f5148634e6..25f7855f7d 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/__init__.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/__init__.py @@ -16,3 +16,8 @@ "parallel_linear_lora", "lora_ops", ] +from .multi_lora import ( # noqa: E402,F401 + ScatterMoEMultiLoRA, + build_multilora_routing, + scatter2scatter_multilora, +) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/grouped_gram.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/grouped_gram.py index c0b7f0f712..9d5601d644 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/grouped_gram.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/grouped_gram.py @@ -18,7 +18,8 @@ import triton import triton.language as tl -from .lora_ops import ALLOW_TF32, _block_r_for_rank, _bucket_m +from . import lora_ops +from .lora_ops import _block_r_for_rank, _bucket_m def _grouped_gram_configs(): @@ -183,7 +184,7 @@ def grouped_lora_weight_grads( scaling=scaling, RANK_IS_I=True, BLOCK_R=block_r, - allow_tf32=ALLOW_TF32, + allow_tf32=lora_ops.ALLOW_TF32, ) grid_b = lambda meta: (e_total, triton.cdiv(n_dim, meta["BLOCK_WIDE"])) # noqa: E731 @@ -205,6 +206,6 @@ def grouped_lora_weight_grads( scaling=scaling, RANK_IS_I=False, BLOCK_R=block_r, - allow_tf32=ALLOW_TF32, + allow_tf32=lora_ops.ALLOW_TF32, ) return dA, dB diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/multi_lora.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/multi_lora.py new file mode 100644 index 0000000000..4f5480a60e --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/multi_lora.py @@ -0,0 +1,455 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""ScatterMoE + multi-adapter LoRA. + +Extends the single-adapter ScatterMoE+LoRA path to co-train many LoRA adapters +("tenants") over one frozen expert stack. An MoE token carries two routings: the +router's expert assignment and a per-row tenant id. The base expert GEMM keys on +the expert; the LoRA keys on the combined ``(expert, tenant)`` group, so LoRA is +stacked over ``E*T`` groups (``A:[E*T*R, K]``, ``B:[N, E*T*R]``). + +The key trick: ``combined = expert * T + tenant`` is also sorted by expert, so a +single sort produces both the expert grouping (base) and the combined grouping +(LoRA). The base GEMM and both LoRA projections (forward and the dX/dA/dB +backward) are grouped GEMMs over the combined groups, expressed with the existing +``scatter2scatter`` op plus one small grouped-Gram kernel (``_grouped_gram_kernel``) +for the weight gradients. + +The backward precomputes ``XA = X @ A^T`` (saved from the forward) and +``YB = dY @ B`` (computed once, shared by dX and dA) so dA/dB become plain grouped +Gram products with no per-output-block recompute -- the cost that made the shared +single-adapter split kernel scale poorly as tenant count (E*T groups) grows. + +Single-adapter (T == 1) reduces to the original behaviour exactly. +""" + +from itertools import product + +import torch +import triton +import triton.language as tl + +from .kernels import lora_ops, ops as base_ops +from .kernels.lora_ops import _block_r_for_rank, _bucket_m +from .parallel_experts import compileable_bincount + + +def _grouped_gram_configs(): + configs = [] + for block_wide, block_m, warps, stages in product( + [32, 64, 128, 256], [32, 64, 128], [4, 8], [3, 4] + ): + configs.append( + triton.Config( + {"BLOCK_WIDE": block_wide, "BLOCK_M": block_m}, + num_warps=warps, + num_stages=stages, + ) + ) + return configs + + +@triton.autotune(configs=_grouped_gram_configs(), key=["M_BUCKET", "WIDE", "RANK_IS_I"]) +@triton.jit +def _grouped_gram_kernel( + P_ptr, + stride_pm, + stride_pd, + Q_ptr, + stride_qm, + stride_qd, + OUT_ptr, + stride_og, + stride_oi, + stride_oj, + offsets_ptr, + M_BUCKET, + WIDE: tl.constexpr, + RANK: tl.constexpr, + scaling, + RANK_IS_I: tl.constexpr, + BLOCK_R: tl.constexpr, + BLOCK_WIDE: tl.constexpr, + BLOCK_M: tl.constexpr, + allow_tf32: tl.constexpr, +): + """Grouped Gram product: out[g] = scaling * (P_g^T @ Q_g), summed over the + tokens of combined-group g (a contiguous [start, end) slice). + + Both LoRA weight gradients are this op once XA/YB are precomputed: + dA[g] = scaling * YB_g^T @ X_g (P=YB[M,R], Q=X[M,K]; rank is the I/row dim) + dB[g] = scaling * DY_g^T @ XA_g (P=DY[M,N], Q=XA[M,R]; rank is the J/col dim) + + The wide dim (K for dA, N for dB) is tiled; the rank dim fits one BLOCK_R. + Every (group, wide-block) writes its full tile, so empty groups self-zero -- + no output pre-init needed. Output strides place the [I, J] tile into either + lora_A-grad ([E*T*R, K]) or lora_B-grad ([N, E*T*R]) layout directly. + """ + g = tl.program_id(0) + w_blk = tl.program_id(1) + + start = tl.where(g == 0, 0, tl.load(offsets_ptr + g - 1, mask=g > 0, other=0)) + end = tl.load(offsets_ptr + g) + num = end - start + + w_idx = w_blk * BLOCK_WIDE + tl.arange(0, BLOCK_WIDE) + w_mask = w_idx < WIDE + r_idx = tl.arange(0, BLOCK_R) + r_mask = r_idx < RANK + input_dtype = P_ptr.dtype.element_ty + + if RANK_IS_I: + acc = tl.zeros((BLOCK_R, BLOCK_WIDE), dtype=tl.float32) + else: + acc = tl.zeros((BLOCK_WIDE, BLOCK_R), dtype=tl.float32) + + for i in range(tl.cdiv(num, BLOCK_M)): + m_idx = start + i * BLOCK_M + tl.arange(0, BLOCK_M) + m_mask = m_idx < end + if RANK_IS_I: + p = tl.load( + P_ptr + m_idx[:, None] * stride_pm + r_idx[None, :] * stride_pd, + mask=m_mask[:, None] & r_mask[None, :], + other=0.0, + ).to(input_dtype) + q = tl.load( + Q_ptr + m_idx[:, None] * stride_qm + w_idx[None, :] * stride_qd, + mask=m_mask[:, None] & w_mask[None, :], + other=0.0, + ).to(input_dtype) + acc += tl.dot(tl.trans(p), q, allow_tf32=allow_tf32) + else: + p = tl.load( + P_ptr + m_idx[:, None] * stride_pm + w_idx[None, :] * stride_pd, + mask=m_mask[:, None] & w_mask[None, :], + other=0.0, + ).to(input_dtype) + q = tl.load( + Q_ptr + m_idx[:, None] * stride_qm + r_idx[None, :] * stride_qd, + mask=m_mask[:, None] & r_mask[None, :], + other=0.0, + ).to(input_dtype) + acc += tl.dot(tl.trans(p), q, allow_tf32=allow_tf32) + + acc = acc * scaling + if RANK_IS_I: + out_ptrs = ( + OUT_ptr + + g * stride_og + + r_idx[:, None] * stride_oi + + w_idx[None, :] * stride_oj + ) + out_mask = r_mask[:, None] & w_mask[None, :] + else: + out_ptrs = ( + OUT_ptr + + g * stride_og + + w_idx[:, None] * stride_oi + + r_idx[None, :] * stride_oj + ) + out_mask = w_mask[:, None] & r_mask[None, :] + tl.store(out_ptrs, acc.to(OUT_ptr.dtype.element_ty), mask=out_mask) + + +def grouped_lora_weight_grads( + grouped_grad_out: torch.Tensor, # DY [M*k, N], grouped + grouped_x: torch.Tensor, # X [M*k, K], grouped + yb: torch.Tensor, # DY@B [M*k, R], grouped (reused from dX path) + xa: torch.Tensor, # X@A^T [M*k, R], grouped (saved from forward) + lora_A: torch.Tensor, # [E*T*R, K] + lora_B: torch.Tensor, # [N, E*T*R] + combined_offsets: torch.Tensor, + e_total: int, + scaling: float, +): + """dA/dB via grouped Gram GEMMs over precomputed XA/YB. + + Avoids the shared split kernel's per-output-block recompute of XA/YB (an + inner reduction over K or N, repeated cdiv(wide_dim, BLOCK) times per group); + that recompute is what makes the split path scale poorly as tenants grow. + """ + rank = lora_A.size(0) // e_total + k_dim = lora_A.size(1) + n_dim = lora_B.size(0) + block_r = _block_r_for_rank(rank) + m_bucket = _bucket_m(max(1, grouped_x.size(0) // e_total)) + + dA = torch.empty_like(lora_A) + dB = torch.empty_like(lora_B) + + grid_a = lambda meta: (e_total, triton.cdiv(k_dim, meta["BLOCK_WIDE"])) # noqa: E731 + _grouped_gram_kernel[grid_a]( + yb, + yb.stride(0), + yb.stride(1), + grouped_x, + grouped_x.stride(0), + grouped_x.stride(1), + dA, + rank * dA.stride(0), + dA.stride(0), + dA.stride(1), + combined_offsets, + m_bucket, + WIDE=k_dim, + RANK=rank, + scaling=scaling, + RANK_IS_I=True, + BLOCK_R=block_r, + allow_tf32=lora_ops.ALLOW_TF32, + ) + + grid_b = lambda meta: (e_total, triton.cdiv(n_dim, meta["BLOCK_WIDE"])) # noqa: E731 + _grouped_gram_kernel[grid_b]( + grouped_grad_out, + grouped_grad_out.stride(0), + grouped_grad_out.stride(1), + xa, + xa.stride(0), + xa.stride(1), + dB, + rank * dB.stride(1), + dB.stride(0), + dB.stride(1), + combined_offsets, + m_bucket, + WIDE=n_dim, + RANK=rank, + scaling=scaling, + RANK_IS_I=False, + BLOCK_R=block_r, + allow_tf32=lora_ops.ALLOW_TF32, + ) + return dA, dB + + +def build_multilora_routing( + flat_expert_idxs: torch.Tensor, # [M*k] expert per (token, top-k slot) + flat_tenant_idxs: torch.Tensor, # [M*k] tenant per (token, top-k slot) + num_experts: int, + num_tenants: int, +): + """One sort by ``expert*T + tenant`` yields both groupings. + + Returns ``(sorted_expert_idxs, sorted_combined_idxs, sorted_scattered_idxs, + expert_offsets, combined_offsets)``. + """ + combined = flat_expert_idxs * num_tenants + flat_tenant_idxs + sorted_combined, sorted_scattered = torch.sort(combined) + combined_offsets = compileable_bincount( + sorted_combined, num_experts * num_tenants + ).cumsum(-1) + sorted_expert = torch.div(sorted_combined, num_tenants, rounding_mode="floor") + expert_offsets = compileable_bincount(sorted_expert, num_experts).cumsum(-1) + return ( + sorted_expert.contiguous(), + sorted_combined.contiguous(), + sorted_scattered.contiguous(), + expert_offsets, + combined_offsets, + ) + + +class ScatterMoEMultiLoRA(torch.autograd.Function): + """Y = X @ W[expert] + scaling * (X @ A[expert,tenant]^T) @ B[expert,tenant]^T, + frozen W; gradients flow to the per-(expert,tenant) LoRA A/B and to X.""" + + @staticmethod + def forward( + ctx, + x: torch.Tensor, + expert_weights: torch.Tensor, # [E, K, N], frozen + k: int, + sorted_expert_idxs: torch.Tensor, + sorted_combined_idxs: torch.Tensor, + sorted_scattered_idxs: torch.Tensor, + expert_offsets: torch.Tensor, + combined_offsets: torch.Tensor, + lora_A: torch.Tensor, # [E*T*R, K] + lora_B: torch.Tensor, # [N, E*T*R] + scaling: float, + ): + if expert_weights.dtype != x.dtype: + expert_weights = expert_weights.to(x.dtype) + e_total = combined_offsets.size(0) # E*T groups + rank = lora_A.size(0) // e_total + n_dim = expert_weights.size(-1) + k_dim = expert_weights.size(1) + + with torch.device(x.device): + # 1. base: Y = X @ W (keyed by expert) + output = base_ops.scatter2scatter( + X=x, + W=expert_weights, + k=k, + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + ) + # 2. XA = X @ A^T (keyed by combined; grouped output [M*k, R]) + w_a = lora_A.reshape(e_total, rank, k_dim).permute(0, 2, 1).contiguous() + xa = base_ops.scatter2scatter( + X=x, + W=w_a, + k=k, + sorted_expert_idxs=sorted_combined_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + y_grouped=True, + ) + # 3. Y_lora = XA @ B^T (keyed by combined) + w_b = lora_B.t().reshape(e_total, rank, n_dim).contiguous() + y_lora = base_ops.scatter2scatter( + X=xa, + W=w_b, + k=1, + sorted_expert_idxs=sorted_combined_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + x_grouped=True, + ) + output.add_(y_lora, alpha=scaling) + + ctx.save_for_backward( + x, + lora_A, + lora_B, + sorted_expert_idxs, + sorted_combined_idxs, + sorted_scattered_idxs, + expert_offsets, + combined_offsets, + xa, + ) + ctx.expert_weights = expert_weights + ctx.k = k + ctx.scaling = scaling + return output + + @staticmethod + def backward(ctx, grad_out: torch.Tensor): + ( + x, + lora_A, + lora_B, + sorted_expert_idxs, + sorted_combined_idxs, + sorted_scattered_idxs, + expert_offsets, + combined_offsets, + xa, + ) = ctx.saved_tensors + w = ctx.expert_weights + k = ctx.k + scaling = ctx.scaling + e_total = combined_offsets.size(0) + rank = lora_A.size(0) // e_total + k_dim = lora_A.size(1) + n_dim = lora_B.size(0) + + with torch.device(grad_out.device): + # one grouping (by scattered idx) serves both -- combined-sorted is + # also expert-sorted + grouped_grad_out = base_ops.group( + grad_out, sorted_scattered_idxs, fan_out=1 + ) + grouped_x = base_ops.group(x, sorted_scattered_idxs, fan_out=k) + + # YB = dY @ B (keyed by combined). Computed once and reused for both + # the LoRA weight grads (dA) and the LoRA input grad (dX_lora). + w_dyb = lora_B.reshape(n_dim, e_total, rank).permute(1, 0, 2).contiguous() + dyb = base_ops.scatter2scatter( + X=grouped_grad_out, + W=w_dyb, + k=1, + sorted_expert_idxs=sorted_combined_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + x_grouped=True, + y_grouped=True, + ) + + # dA/dB as grouped Gram GEMMs over precomputed XA (saved fwd) and YB. + # No per-output-block recompute of XA/YB -- the cost that made the + # shared split kernel scale poorly as tenants (E*T groups) grow. + d_lora_A, d_lora_B = grouped_lora_weight_grads( + grouped_grad_out, + grouped_x, + dyb, + xa, + lora_A, + lora_B, + combined_offsets, + e_total, + scaling, + ) + + # dX = base (expert) + LoRA (combined) + d_grouped = base_ops.scatter2scatter( + X=grouped_grad_out, + x_grouped=True, + W=w.permute(0, 2, 1), + sorted_expert_idxs=sorted_expert_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + k=1, + ) + if scaling != 0.0: + # dX_lora = scaling * YB @ A, keyed by combined groups (one more + # grouped GEMM; mirrors the forward's XA @ B step). + w_dxa = lora_A.reshape(e_total, rank, k_dim).contiguous() + d_lora = base_ops.scatter2scatter( + X=dyb, + W=w_dxa, + k=1, + sorted_expert_idxs=sorted_combined_idxs, + sorted_scattered_idxs=sorted_scattered_idxs, + x_grouped=True, + y_grouped=True, + ) + d_grouped[sorted_scattered_idxs] += scaling * d_lora + + if k == 1: + d_input = d_grouped + else: + d_input = d_grouped.view(x.size(0), k, d_grouped.size(-1)).sum(-2) + + return ( + d_input, + None, + None, + None, + None, + None, + None, + None, + d_lora_A, + d_lora_B, + None, + ) + + +def scatter2scatter_multilora( + x, + expert_weights, + k, + sorted_expert_idxs, + sorted_combined_idxs, + sorted_scattered_idxs, + expert_offsets, + combined_offsets, + lora_A, + lora_B, + scaling: float = 1.0, +): + """Drop-in multi-adapter forward (autograd-backed).""" + return ScatterMoEMultiLoRA.apply( + x, + expert_weights, + k, + sorted_expert_idxs, + sorted_combined_idxs, + sorted_scattered_idxs, + expert_offsets, + combined_offsets, + lora_A, + lora_B, + scaling, + ) diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/__init__.py b/src/axolotl/integrations/kernels/libs/sonicmoe/__init__.py index 5cd9cf0fd7..a9c742f144 100644 --- a/src/axolotl/integrations/kernels/libs/sonicmoe/__init__.py +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/__init__.py @@ -1,6 +1,14 @@ from .experts import register_sonicmoe_experts, sonicmoe_experts_forward_with_lora +from .multi_lora import ( + MoEMultiLoRAMaterialize, + combined_expert_ids, + materialize_multi_lora_experts, +) __all__ = [ "register_sonicmoe_experts", "sonicmoe_experts_forward_with_lora", + "MoEMultiLoRAMaterialize", + "combined_expert_ids", + "materialize_multi_lora_experts", ] diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/multi_lora.py b/src/axolotl/integrations/kernels/libs/sonicmoe/multi_lora.py new file mode 100644 index 0000000000..7fd4a2d677 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/multi_lora.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""SonicMoE + multi-adapter LoRA via combined-group weight materialization. + +SonicMoE dispatches opaque CUTLASS/CuteDSL grouped GEMMs that cannot fuse LoRA, so +the single-adapter path materializes ``W_eff[e] = W[e] + scaling * (B_e @ A_e)`` +per expert and hands that to the kernel (see ``lora.MoELoRAMaterialize``). + +Multi-tenant co-training keeps that strategy but over the combined +``(expert, tenant)`` grouping: a token carries the router's expert id *and* a +per-row tenant id, and the effective weight for ``(token routed to e, tenant t)`` +is ``W[e] + scaling_t * (B_{e,t} @ A_{e,t})``. We materialize all ``E*T`` effective +weights once and remap each token-slot's expert id to ``e * T + t``; the *same* +opaque grouped GEMM, called with ``num_experts = E*T``, then routes every tenant +in a single launch. The frozen base ``W[e]`` is shared in storage but, because the +kernel needs one contiguous weight per group, is broadcast into the ``E*T`` buffer +-- so transient materialization memory grows ~linearly with ``T`` (at ``T == 1`` +it is identical to the single-adapter path). + +This module owns only the materialize + id-remap (the LoRA-specific part); the +CUTLASS GEMM is unchanged. ``T == 1`` reduces to ``MoELoRAMaterialize`` exactly. +""" + +from __future__ import annotations + +import torch + + +class MoEMultiLoRAMaterialize(torch.autograd.Function): + """Build ``W_eff[e*T + t] = W[e] + scaling_t * (B_{e,t} @ A_{e,t})``. + + Layout (stacked over tenants, outer expert / inner tenant to match the + ``combined = e*T + t`` group id): + base ``[E, out, in]`` (frozen) + lora_A ``[T, E, r, in]`` + lora_B ``[T, E, out, r]`` + scaling ``[T]`` + Returns ``W_eff`` ``[E*T, out, in]``. Gradients flow to A/B only. + """ + + @staticmethod + def forward(ctx, base_weight, lora_A, lora_B, scaling): + T, E, r, in_dim = lora_A.shape + out_dim = base_weight.shape[1] + if base_weight.dtype != lora_A.dtype: + base_weight = base_weight.to(lora_A.dtype) + + # Materialize directly in combined (e*T + t) order so the result needs no + # reorder copy: einsum over the [E, T, ...] view yields a contiguous + # [E, T, out, in] that reshapes to [E*T, ...] for free. The A/B permutes + # are on rank-sized tensors (~in/r smaller than W_eff), so ~free. delta is + # recomputed in backward, so the scale + frozen-base add are in-place. + a_et = lora_A.permute(1, 0, 2, 3) # [E, T, r, in] + b_et = lora_B.permute(1, 0, 2, 3) # [E, T, out, r] + delta = torch.einsum("etor,etri->etoi", b_et, a_et) # [E, T, out, in] + delta.mul_(scaling.view(1, T, 1, 1)) + delta.add_(base_weight.unsqueeze(1)) # broadcast frozen base over tenants + w_eff = delta.reshape(E * T, out_dim, in_dim) + + ctx.save_for_backward(lora_A, lora_B, scaling) + ctx.shape = (T, E, r, in_dim, out_dim) + return w_eff + + @staticmethod + def backward(ctx, grad_w_eff): + lora_A, lora_B, scaling = ctx.saved_tensors + T, E, r, in_dim, out_dim = ctx.shape + # Scale the (rank-sized) A/B grads, not the W_eff-sized incoming grad, to + # avoid a full-size copy in backward. + g = grad_w_eff.reshape(E, T, out_dim, in_dim) + a_et = lora_A.permute(1, 0, 2, 3) # [E, T, r, in] + b_et = lora_B.permute(1, 0, 2, 3) # [E, T, out, r] + # dA = B^T @ dDelta ; dB = dDelta @ A^T (then back to the [T, E, ...] layout) + d_a_et = torch.einsum("etor,etoi->etri", b_et, g).mul_(scaling.view(1, T, 1, 1)) + d_b_et = torch.einsum("etoi,etri->etor", g, a_et).mul_(scaling.view(1, T, 1, 1)) + return None, d_a_et.permute(1, 0, 2, 3), d_b_et.permute(1, 0, 2, 3), None + + +def materialize_multi_lora_experts(base_weight, lora_A, lora_B, scaling): + """Autograd-backed ``W_eff`` builder; see :class:`MoEMultiLoRAMaterialize`.""" + return MoEMultiLoRAMaterialize.apply(base_weight, lora_A, lora_B, scaling) + + +def combined_expert_ids(expert_ids, tenant_ids, token_idx, num_tenants): + """Remap each token-slot's expert id to its combined ``e * T + t`` group. + + ``expert_ids`` / ``token_idx`` are the per-(token, top-k slot) tensors sonicmoe + already builds; ``tenant_ids`` is per *token*. Returns int32 combined ids + aligned with ``expert_ids`` for a ``num_experts = E*T`` grouped GEMM. + """ + t = tenant_ids.to(torch.int64)[token_idx.to(torch.int64)] + return (expert_ids.to(torch.int64) * num_tenants + t).to(torch.int32) diff --git a/tests/integrations/test_scattermoe_multi_lora.py b/tests/integrations/test_scattermoe_multi_lora.py new file mode 100644 index 0000000000..37cdcf5be4 --- /dev/null +++ b/tests/integrations/test_scattermoe_multi_lora.py @@ -0,0 +1,144 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""ScatterMoE multi-adapter LoRA: forward + backward vs a dense reference. + +Co-trains several LoRA adapters (tenants) over one frozen expert stack. The base +GEMM keys on expert; the LoRA keys on the combined (expert, tenant) group. +""" + +import pytest +import torch + +from axolotl.integrations.kernels.libs.scattermoe_lora.multi_lora import ( + build_multilora_routing, + scatter2scatter_multilora, +) + +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") + + +def _reference(x, w, lora_a, lora_b, expert, tenant, num_tenants, rank, scaling): + rows = [] + for n in range(x.size(0)): + e, t = expert[n].item(), tenant[n].item() + g = e * num_tenants + t + a_g = lora_a[g * rank : (g + 1) * rank] # [R, K] + b_g = lora_b[:, g * rank : (g + 1) * rank] # [N, R] + rows.append(x[n] @ w[e] + scaling * ((x[n] @ a_g.t()) @ b_g.t())) + return torch.stack(rows) + + +@pytest.mark.parametrize( + "num_experts,num_tenants,rank", + [ + (3, 2, 4), + (4, 3, 8), + (2, 1, 4), # T=1 reduces to single-adapter + (6, 4, 8), # 24 (expert,tenant) groups vs 48 tokens -> some empty groups + (4, 4, 64), # rank 64 exercises BLOCK_R > 16 + ], +) +def test_multilora_forward_backward_matches_reference(num_experts, num_tenants, rank): + torch.manual_seed(0) + dev, dt = "cuda", torch.float32 + e, t, r = num_experts, num_tenants, rank + K, N, M = 16, 12, 48 + scaling = 0.5 + + w = torch.randn(e, K, N, device=dev, dtype=dt) + a = (torch.randn(e * t * r, K, device=dev, dtype=dt) * 0.1).requires_grad_(True) + b = (torch.randn(N, e * t * r, device=dev, dtype=dt) * 0.1).requires_grad_(True) + x = torch.randn(M, K, device=dev, dtype=dt, requires_grad=True) + expert = torch.randint(0, e, (M,), device=dev) + tenant = torch.randint(0, t, (M,), device=dev) + + se, sc, ss, eo, co = build_multilora_routing(expert, tenant, e, t) + y = scatter2scatter_multilora(x, w, 1, se, sc, ss, eo, co, a, b, scaling) + + xr = x.detach().clone().requires_grad_(True) + ar = a.detach().clone().requires_grad_(True) + br = b.detach().clone().requires_grad_(True) + yr = _reference(xr, w, ar, br, expert, tenant, t, r, scaling) + + # tf32 matmul tolerance; far tighter than any misroute would produce + scale = yr.abs().mean().item() + assert (y - yr).abs().max().item() < 0.05 * max(1.0, scale) + + grad = torch.randn_like(y) + y.backward(grad) + yr.backward(grad) + assert torch.allclose(x.grad, xr.grad, atol=5e-2, rtol=5e-2) + assert torch.allclose(a.grad, ar.grad, atol=5e-2, rtol=5e-2) + assert torch.allclose(b.grad, br.grad, atol=5e-2, rtol=5e-2) + + +def test_multilora_true_fp32_when_tf32_disabled(): + """With TF32 off, the fused multi-adapter dA/dB must be true-fp32 (~1e-7), not + TF32 (~1e-3). Guards the ALLOW_TF32 live-binding fix: an import-time value copy + left the grouped-Gram dA/dB kernel on TF32 while the forward honored the flag.""" + import axolotl.integrations.kernels.libs.scattermoe_lora.kernels.lora_ops as lo + import axolotl.integrations.kernels.libs.scattermoe_lora.kernels.ops as base_ops + + torch.manual_seed(0) + dev, dt = "cuda", torch.float32 + e, t, r = 4, 3, 8 + K, N, M, scaling = 16, 12, 48, 0.5 + + saved = (torch.backends.cuda.matmul.allow_tf32, lo.ALLOW_TF32, base_ops.ALLOW_TF32) + torch.backends.cuda.matmul.allow_tf32 = False + lo.ALLOW_TF32 = base_ops.ALLOW_TF32 = False + try: + w = torch.randn(e, K, N, device=dev, dtype=dt) + a = (torch.randn(e * t * r, K, device=dev, dtype=dt) * 0.1).requires_grad_(True) + b = (torch.randn(N, e * t * r, device=dev, dtype=dt) * 0.1).requires_grad_(True) + x = torch.randn(M, K, device=dev, dtype=dt, requires_grad=True) + expert = torch.randint(0, e, (M,), device=dev) + tenant = torch.randint(0, t, (M,), device=dev) + + se, sc, ss, eo, co = build_multilora_routing(expert, tenant, e, t) + y = scatter2scatter_multilora(x, w, 1, se, sc, ss, eo, co, a, b, scaling) + + xr = x.detach().clone().requires_grad_(True) + ar = a.detach().clone().requires_grad_(True) + br = b.detach().clone().requires_grad_(True) + yr = _reference(xr, w, ar, br, expert, tenant, t, r, scaling) + + grad = torch.randn_like(y) + y.backward(grad) + yr.backward(grad) + finally: + ( + torch.backends.cuda.matmul.allow_tf32, + lo.ALLOW_TF32, + base_ops.ALLOW_TF32, + ) = saved + + def _rel(p, q): + return (p - q).abs().max().item() / max(q.abs().max().item(), 1e-12) + + assert _rel(y, yr) < 1e-4 + assert _rel(x.grad, xr.grad) < 1e-4 + assert _rel(a.grad, ar.grad) < 1e-4 # dA: TF32 leak would be ~1e-3 + assert _rel(b.grad, br.grad) < 1e-4 # dB: TF32 leak would be ~1e-3 + + +def test_misrouting_would_fail(): + """Sanity: a wrong tenant assignment produces O(1) error, confirming the + tolerance above actually checks routing.""" + torch.manual_seed(1) + dev, dt = "cuda", torch.float32 + e, t, r, K, N, M = 3, 2, 4, 16, 12, 48 + w = torch.randn(e, K, N, device=dev, dtype=dt) + a = torch.randn(e * t * r, K, device=dev, dtype=dt) + b = torch.randn(N, e * t * r, device=dev, dtype=dt) + x = torch.randn(M, K, device=dev, dtype=dt) + expert = torch.randint(0, e, (M,), device=dev) + tenant = torch.randint(0, t, (M,), device=dev) + wrong_tenant = 1 - tenant # flip + + se, sc, ss, eo, co = build_multilora_routing(expert, tenant, e, t) + y = scatter2scatter_multilora(x, w, 1, se, sc, ss, eo, co, a, b, 1.0) + y_wrong = _reference(x, w, a, b, expert, wrong_tenant, t, r, 1.0) + assert (y - y_wrong).abs().max().item() > 1.0 diff --git a/tests/integrations/test_sonicmoe_multi_lora.py b/tests/integrations/test_sonicmoe_multi_lora.py new file mode 100644 index 0000000000..73f189982f --- /dev/null +++ b/tests/integrations/test_sonicmoe_multi_lora.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""SonicMoE multi-adapter LoRA: validate the E*T materialization + id remap. + +The opaque CUTLASS grouped GEMM is unchanged and isn't exercised here; an exact +PyTorch grouped GEMM stands in for it so the materialize + combined-routing logic +can be checked end-to-end (forward + backward) against a per-tenant reference. +""" + +import pytest +import torch + +from axolotl.integrations.kernels.libs.sonicmoe.lora import MoELoRAMaterialize +from axolotl.integrations.kernels.libs.sonicmoe.multi_lora import ( + combined_expert_ids, + materialize_multi_lora_experts, +) + +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") + + +def _grouped_gemm(hidden, w_eff, group_ids, token_idx, out_dim): + """Exact stand-in for the CUTLASS grouped GEMM: out[token_idx[i]] += h @ W_eff[g_i].""" + out = hidden.new_zeros(hidden.shape[0], out_dim) + for i in range(group_ids.shape[0]): + n = token_idx[i].item() + out[n] = out[n] + hidden[n] @ w_eff[group_ids[i].item()].t() + return out + + +@pytest.mark.parametrize("E,T,r", [(4, 3, 8), (3, 1, 4), (5, 4, 16)]) +def test_multilora_materialize_matches_per_tenant(E, T, r): + torch.manual_seed(0) + dev, dt = "cuda", torch.float32 + out_dim, in_dim, M = 12, 16, 32 + base = torch.randn(E, out_dim, in_dim, device=dev, dtype=dt) + A = (torch.randn(T, E, r, in_dim, device=dev, dtype=dt) * 0.1).requires_grad_(True) + B = (torch.randn(T, E, out_dim, r, device=dev, dtype=dt) * 0.1).requires_grad_(True) + scaling = torch.rand(T, device=dev, dtype=dt) + 0.5 + + hidden = torch.randn(M, in_dim, device=dev, dtype=dt) + expert = torch.randint(0, E, (M,), device=dev, dtype=torch.int32) + tenant = torch.randint(0, T, (M,), device=dev, dtype=torch.int32) + token_idx = torch.arange(M, device=dev, dtype=torch.int32) + + # combined path + w_eff = materialize_multi_lora_experts(base, A, B, scaling) # [E*T, out, in] + g_ids = combined_expert_ids(expert, tenant, token_idx, T) + out = _grouped_gemm(hidden, w_eff, g_ids, token_idx, out_dim) + + # reference: each token uses its tenant's expert-e effective weight, directly + Ar = A.detach().clone().requires_grad_(True) + Br = B.detach().clone().requires_grad_(True) + out_ref = hidden.new_zeros(M, out_dim) + for n in range(M): + e, t = expert[n].item(), tenant[n].item() + w = base[e] + scaling[t] * (Br[t, e] @ Ar[t, e]) + out_ref[n] = hidden[n] @ w.t() + + assert torch.allclose(out, out_ref, atol=1e-4, rtol=1e-4) + + grad = torch.randn_like(out) + out.backward(grad) + out_ref.backward(grad) + assert torch.allclose(A.grad, Ar.grad, atol=1e-4, rtol=1e-4) + assert torch.allclose(B.grad, Br.grad, atol=1e-4, rtol=1e-4) + + +def test_t1_reduces_to_single_adapter(): + """T==1 must produce exactly the single-adapter MoELoRAMaterialize result.""" + torch.manual_seed(1) + dev, dt = "cuda", torch.float32 + E, r, out_dim, in_dim = 4, 8, 12, 16 + base = torch.randn(E, out_dim, in_dim, device=dev, dtype=dt) + # single-adapter PEFT layout: A [r*E, in], B [out, r*E] + A_flat = torch.randn(r * E, in_dim, device=dev, dtype=dt) * 0.1 + B_flat = torch.randn(out_dim, r * E, device=dev, dtype=dt) * 0.1 + scaling = 0.5 + w_single = MoELoRAMaterialize.apply(base, A_flat, B_flat, scaling) + + # same weights reshaped into the [T=1, E, ...] stacked layout. Single-adapter + # PEFT layout is asymmetric: A rows are E-outer/r-inner, B cols r-outer/E-inner. + A_stk = A_flat.reshape(E, r, in_dim).unsqueeze(0).contiguous() # [1,E,r,in] + B_stk = ( + B_flat.reshape(out_dim, r, E).permute(2, 0, 1).unsqueeze(0).contiguous() + ) # [1,E,out,r] + w_multi = materialize_multi_lora_experts( + base, A_stk, B_stk, torch.tensor([scaling], device=dev, dtype=dt) + ) + assert torch.allclose(w_single, w_multi, atol=1e-5, rtol=1e-5) + + +def test_combined_ids_routing(): + dev = "cuda" + T = 3 + expert = torch.tensor([0, 2, 1, 0], device=dev, dtype=torch.int32) + tenant = torch.tensor([1, 0, 2, 1], device=dev, dtype=torch.int32) # per token + token_idx = torch.tensor([0, 1, 2, 3], device=dev, dtype=torch.int32) + g = combined_expert_ids(expert, tenant, token_idx, T) + # e*T + t + assert g.tolist() == [0 * 3 + 1, 2 * 3 + 0, 1 * 3 + 2, 0 * 3 + 1] + assert g.dtype == torch.int32 From 5ed506e79b190f2c8a92742cce04fd728270be70 Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Mon, 15 Jun 2026 09:31:40 +0700 Subject: [PATCH 1352/1405] Feat: add gemma4 unified (#3706) * feat: update cce for new models * feat: update transformers * chore: remove dead code * feat: add liger for gemma4 unified * fix: hybrid attn for latest transformers * feat: add gemma4 unified following gemma4 * chore: refactor old logger to axolotl get_logger * fix: update legacy env to new xet env * feat: add missed files * fix: handling of lora kernels * feat: update vision and readme yaml * chore: update numbers from latest run * feat: add text config * chore: update correct number * fix: update cce commit * fix: packing leak * use transformers patch release * 2 parallel jobs for pytests * fix: gate attention_mask for gemma4_unified * fix: restore prior gemma4 e2b shared kv layer helper * chore: refactor gemma4 hybrid attn * feat: update gemma4 results and config * chore: simplify config * fix: update unified results and docs * chore: swap to hybrid attn * feat: add tests * fix: swap to FA2 text * fix: ci logging * fix: generalize rotary patch * fix: deleted file for docs * fix: fsdp defaulted to v2 * fix: support simplenamespace for test * fix: update quarto to include all current scripts * fix: drop quarto doc entry for untracked deepseek_v4 module --------- Co-authored-by: Wing Lian --- .github/workflows/tests.yml | 2 + _quarto.yml | 205 +++++++++- docker/Dockerfile-cloud | 2 +- docker/Dockerfile-cloud-no-tmux | 2 +- docker/Dockerfile-cloud-uv | 2 +- .../colab-axolotl-example.ipynb | 2 +- .../12b-text-lora.yaml} | 29 +- examples/gemma4-unified/12b-vision-lora.yaml | 60 +++ examples/gemma4-unified/README.md | 42 ++ examples/gemma4/26b-a4b-moe-qlora.yaml | 14 +- examples/gemma4/31b-qlora.yaml | 5 +- examples/gemma4/README.md | 26 +- examples/gemma4/e2b-vision-lora.yaml | 5 - pyproject.toml | 4 +- scripts/cutcrossentropy_install.py | 2 +- src/axolotl/core/builders/base.py | 6 +- src/axolotl/core/trainers/base.py | 33 +- .../core/trainers/grpo/async_trainer.py | 10 +- .../core/trainers/grpo/fast_async_trainer.py | 4 +- .../integrations/cut_cross_entropy/README.md | 10 +- .../cut_cross_entropy/__init__.py | 2 +- .../hatchery/rewards/math_reward.py | 5 +- src/axolotl/integrations/kd/chat_template.py | 4 +- .../kd/collator_online_teacher.py | 4 +- .../integrations/kernels/autotune_callback.py | 6 +- .../kernels/autotune_collector.py | 5 +- src/axolotl/integrations/liger/plugin.py | 54 +++ src/axolotl/kernels/autotune_telemetry.py | 6 +- src/axolotl/loaders/patch_manager.py | 66 ++-- src/axolotl/monkeypatch/attention/fp8_attn.py | 6 +- src/axolotl/monkeypatch/gemma4_hybrid_mask.py | 128 +++--- src/axolotl/monkeypatch/gemma4_kernelize.py | 113 ------ src/axolotl/monkeypatch/gemma4_loss_kwargs.py | 41 ++ src/axolotl/monkeypatch/kernelize_fixes.py | 73 ++++ src/axolotl/monkeypatch/lora_kernels.py | 33 ++ .../monkeypatch/models/gemma4/fused_attn.py | 2 +- .../models/gemma4_unified/__init__.py | 1 + .../models/gemma4_unified/fused_attn.py | 220 +++++++++++ src/axolotl/monkeypatch/trainer/trl_vllm.py | 5 +- src/axolotl/monkeypatch/trainer_fsdp_optim.py | 73 ---- src/axolotl/processing_strategies.py | 14 + src/axolotl/scripts/process_cleanup.py | 5 +- src/axolotl/scripts/vllm_serve_lora.py | 4 +- src/axolotl/scripts/vllm_worker_ext.py | 6 +- src/axolotl/telemetry/callbacks.py | 4 +- src/axolotl/telemetry/errors.py | 4 +- src/axolotl/telemetry/manager.py | 5 +- src/axolotl/telemetry/runtime_metrics.py | 4 +- .../templates/gemma4_unified.jinja | 363 ++++++++++++++++++ src/axolotl/utils/config/__init__.py | 11 +- src/axolotl/utils/data/wrappers.py | 4 +- src/axolotl/utils/schemas/enums.py | 1 + src/axolotl/utils/schemas/validation.py | 17 +- tests/e2e/multigpu/test_llama.py | 4 + .../test_patch_manager_gemma4_unified.py | 129 +++++++ tests/monkeypatch/test_gemma4_hybrid_mask.py | 37 ++ tests/monkeypatch/test_gemma4_kernelize.py | 196 ---------- .../test_gemma4_unified_fused_attn.py | 221 +++++++++++ tests/monkeypatch/test_kernelize_fixes.py | 227 +++++++++++ .../test_lora_kernels_gemma4_unified.py | 75 ++++ tests/telemetry/test_manager.py | 25 +- tests/test_normalize_config.py | 3 +- tests/test_processing_strategies.py | 6 + 63 files changed, 2055 insertions(+), 622 deletions(-) rename examples/{gemma4/31b-qlora-flex.yaml => gemma4-unified/12b-text-lora.yaml} (67%) create mode 100644 examples/gemma4-unified/12b-vision-lora.yaml create mode 100644 examples/gemma4-unified/README.md delete mode 100644 src/axolotl/monkeypatch/gemma4_kernelize.py create mode 100644 src/axolotl/monkeypatch/gemma4_loss_kwargs.py create mode 100644 src/axolotl/monkeypatch/kernelize_fixes.py create mode 100644 src/axolotl/monkeypatch/models/gemma4_unified/__init__.py create mode 100644 src/axolotl/monkeypatch/models/gemma4_unified/fused_attn.py delete mode 100644 src/axolotl/monkeypatch/trainer_fsdp_optim.py create mode 100644 src/axolotl/utils/chat_templates/templates/gemma4_unified.jinja create mode 100644 tests/loaders/test_patch_manager_gemma4_unified.py delete mode 100644 tests/monkeypatch/test_gemma4_kernelize.py create mode 100644 tests/monkeypatch/test_gemma4_unified_fused_attn.py create mode 100644 tests/monkeypatch/test_kernelize_fixes.py create mode 100644 tests/monkeypatch/test_lora_kernels_gemma4_unified.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2d724b644c..7c1085bba4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -65,6 +65,7 @@ jobs: if: ${{ !github.event.pull_request.draft }} needs: [prime-cdn-s3-cache] strategy: + max-parallel: 2 fail-fast: false matrix: python_version: ["3.12", "3.14"] @@ -156,6 +157,7 @@ jobs: if: ${{ !github.event.pull_request.draft }} needs: [prime-cdn-s3-cache] strategy: + max-parallel: 2 fail-fast: false matrix: python_version: ["3.12", "3.14"] diff --git a/_quarto.yml b/_quarto.yml index 5b008bf99f..2d9782d039 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -19,11 +19,14 @@ quartodoc: - datasets - convert - prompt_tokenizers + - prompters + - processing_strategies - logging_config - core.builders.base - core.builders.causal - core.builders.rl - core.training_args + - core.training_args_base - core.chat.messages - core.chat.format.chatml - core.chat.format.llama3x @@ -47,22 +50,40 @@ quartodoc: - cli.preprocess - cli.quantize - cli.vllm_serve + - cli.agent_docs + - cli.cloud - cli.cloud.base + - cli.cloud.baseten - cli.cloud.modal_ - cli.utils - cli.utils.args + - cli.utils.diffusion - cli.utils.fetch - cli.utils.load + - cli.utils.lora_merge - cli.utils.sweeps - cli.utils.train - title: Trainers desc: Training implementations contents: - core.trainers.base + - core.trainers.constants - core.trainers.trl - core.trainers.mamba + - core.trainers.dpo.args - core.trainers.dpo.trainer + - core.trainers.ebft + - core.trainers.ebft.args + - core.trainers.ebft.kernels + - core.trainers.ebft.rewards + - core.trainers.ebft.strided + - core.trainers.ebft.trainer + - core.trainers.grpo + - core.trainers.grpo.args - core.trainers.grpo.trainer + - core.trainers.grpo.async_trainer + - core.trainers.grpo.fast_async_trainer + - core.trainers.grpo.replay_buffer - core.trainers.grpo.sampler - core.trainers.utils - title: Model Loading @@ -74,10 +95,16 @@ quartodoc: - loaders.adapter - loaders.patch_manager - loaders.constants + - loaders.utils - title: Mixins desc: Mixin classes for augmenting trainers contents: + - core.trainers.mixins.activation_checkpointing + - core.trainers.mixins.checkpoints + - core.trainers.mixins.distributed_parallel + - core.trainers.mixins.layer_offloading - core.trainers.mixins.optimizer + - core.trainers.mixins.packing - core.trainers.mixins.rng_state_loader - core.trainers.mixins.scheduler - title: Context Managers @@ -95,12 +122,20 @@ quartodoc: - prompt_strategies.user_defined - prompt_strategies.llama2_chat - prompt_strategies.completion + - prompt_strategies.context_qa + - prompt_strategies.creative_acr - prompt_strategies.input_output + - prompt_strategies.pretrain - prompt_strategies.stepwise_supervised - prompt_strategies.metharme - prompt_strategies.orcamini - prompt_strategies.pygmalion - prompt_strategies.messages.chat + - prompt_strategies.ebft.ebft_chat_multiturn + - prompt_strategies.ebft.ebft_opencode + - prompt_strategies.ebft.ebft_reasoning + - prompt_strategies.ebft.ebft_strided_chat + - prompt_strategies.ebft.ebft_strided_structured - prompt_strategies.dpo.chat_template - prompt_strategies.dpo.llama3 - prompt_strategies.dpo.chatml @@ -111,14 +146,19 @@ quartodoc: - prompt_strategies.kto.chatml - prompt_strategies.kto.user_defined - prompt_strategies.orpo.chat_template + - prompt_strategies.bradley_terry.chat_template - prompt_strategies.bradley_terry.llama3 - title: Kernels desc: Low-level performance optimizations contents: - kernels.lora + - kernels.dora - kernels.geglu - kernels.swiglu - kernels.quantize + - kernels.autotune_telemetry + - kernels.gemma4_fused_rope + - kernels.rms_norm_gated - kernels.utils - title: Monkey Patches desc: Runtime patches for model optimizations @@ -132,28 +172,97 @@ quartodoc: - monkeypatch.utils - monkeypatch.btlm_attn_hijack_flash - monkeypatch.stablelm_attn_hijack_flash - - monkeypatch.trainer_fsdp_optim - monkeypatch.transformers_fa_utils - monkeypatch.data.batch_dataset_fetcher - monkeypatch.mixtral - monkeypatch.gradient_checkpointing.offload_cpu - monkeypatch.gradient_checkpointing.offload_disk + - monkeypatch.deepspeed_utils + - monkeypatch.fsdp2_qlora + - monkeypatch.gemma4_hybrid_mask + - monkeypatch.gemma4_loss_kwargs + - monkeypatch.kernelize_fixes + - monkeypatch.moe_quant + - monkeypatch.scaled_softmax_attn + - monkeypatch.torchao_optim + - monkeypatch.trainer_accelerator_args + - monkeypatch.accelerate.fsdp2 + - monkeypatch.accelerate.parallelism_config + - monkeypatch.attention.flash_attn_4 + - monkeypatch.attention.flex_attn + - monkeypatch.attention.fp8_attn + - monkeypatch.attention.sage_attn + - monkeypatch.attention.xformers + - monkeypatch.loss.chunked + - monkeypatch.loss.eaft + - monkeypatch.models.apertus.activation + - monkeypatch.models.falcon_h1.modeling + - monkeypatch.models.gemma4_unified.fused_attn + - monkeypatch.models.granitemoehybrid.modeling + - monkeypatch.models.kimi_linear.patch_kimi_linear + - monkeypatch.models.llama4.modeling + - monkeypatch.models.mamba_utils + - monkeypatch.models.mistral3.mistral_common_tokenizer + - monkeypatch.models.nemotron_h.modeling + - monkeypatch.models.pixtral.modeling_flash_attention_utils + - monkeypatch.models.qwen3.fused_attn + - monkeypatch.models.qwen3_5.fused_attn + - monkeypatch.models.qwen3_5.modeling + - monkeypatch.models.qwen3_5_moe.fused_attn + - monkeypatch.models.qwen3_moe.fused_attn + - monkeypatch.models.qwen3_next.modeling + - monkeypatch.models.qwen3_vl.fused_attn + - monkeypatch.models.voxtral.modeling + - monkeypatch.peft.utils + - monkeypatch.ring_attn.adapters.batch + - monkeypatch.ring_attn.patch + - monkeypatch.tiled_mlp.base + - monkeypatch.tiled_mlp.patch + - monkeypatch.trainer.lr + - monkeypatch.trainer.trl + - monkeypatch.trainer.trl_vllm + - monkeypatch.trainer.utils + - monkeypatch.transformers.trainer_loss_calc + - monkeypatch.xformers_ - title: Utils desc: Utility functions contents: - utils.tokenization - utils.chat_templates + - utils.chat_templates.base - utils.lora - utils.model_shard_quant - utils.bench + - utils.comet_ + - utils.config + - utils.cuda13 + - utils.datasets + - utils.environment + - utils.fp32_norms - utils.freeze + - utils.import_helper + - utils.logging + - utils.mlflow_ + - utils.tee + - utils.trackio_ + - utils.train - utils.trainer + - utils.wandb_ + - utils.weight_serde - utils.schedulers - utils.distributed - utils.dict + - utils.generation.sft + - utils.mistral.mistral3_processor + - utils.mistral.mistral_tokenizer - utils.optimizers.adopt + - utils.optimizers.qgalore - utils.data.streaming - utils.data.sft + - utils.data.rl + - utils.data.lock + - utils.data.utils + - utils.data.wrappers - utils.quantization - title: Schemas desc: Pydantic data models for Axolotl config @@ -166,18 +275,88 @@ quartodoc: - utils.schemas.trl - utils.schemas.multimodal - utils.schemas.integrations + - utils.schemas.deprecated + - utils.schemas.dynamic_checkpoint + - utils.schemas.fsdp + - utils.schemas.quantization + - utils.schemas.validation + - utils.schemas.vllm - utils.schemas.enums - utils.schemas.utils - title: Integrations desc: Third-party integrations and extensions contents: - integrations.base + - integrations.config + - integrations.cut_cross_entropy - integrations.cut_cross_entropy.args + - integrations.densemixer.args + - integrations.densemixer.plugin + - integrations.diffusion.args + - integrations.diffusion.callbacks + - integrations.diffusion.generation + - integrations.diffusion.plugin + - integrations.diffusion.trainer + - integrations.diffusion.utils + - integrations.expert_parallel.args + - integrations.expert_parallel.buffer + - integrations.expert_parallel.experts_fn + - integrations.expert_parallel.plugin + - integrations.expert_parallel.shard + - integrations.grokfast.args - integrations.grokfast.optimizer + - integrations.hatchery.args + - integrations.hatchery.data + - integrations.hatchery.plugin + - integrations.hatchery.rewards.math_reward + - integrations.hatchery.rl_trainer + - integrations.hatchery.trainer + - integrations.kd + - integrations.kd.args + - integrations.kd.callbacks + - integrations.kd.chat_template + - integrations.kd.collator + - integrations.kd.collator_online_teacher + - integrations.kd.kernels.liger + - integrations.kd.topk_logprob.forward_kl - integrations.kd.trainer + - integrations.kd.utils + - integrations.kernels.args + - integrations.kernels.autotune_callback + - integrations.kernels.autotune_collector + - integrations.kernels.constants + - integrations.kernels.plugin - integrations.liger.args + - integrations.liger.plugin + - integrations.liger.utils + - integrations.liger.models.base + - integrations.liger.models.deepseekv2 + - integrations.liger.models.jamba + - integrations.liger.models.llama4 + - integrations.liger.models.qwen3 + - integrations.liger.models.qwen3_5 + - integrations.liger.models.qwen3_5_moe + - integrations.liger.models.qwen3_moe + - integrations.llm_compressor.args + - integrations.llm_compressor.plugin + - integrations.llm_compressor.utils - integrations.lm_eval.args + - integrations.lm_eval.cli + - integrations.mora.args + - integrations.mora.plugin + - integrations.nemo_gym.args + - integrations.nemo_gym.data_producer + - integrations.nemo_gym.dataset + - integrations.nemo_gym.multi_turn + - integrations.nemo_gym.plugin + - integrations.nemo_gym.rewards + - integrations.nemo_gym.server + - integrations.spectrum - integrations.spectrum.args + - integrations.swanlab.args + - integrations.swanlab.callbacks + - integrations.swanlab.completion_logger + - integrations.swanlab.plugins - title: Common desc: Common utilities and shared functionality contents: @@ -187,24 +366,48 @@ quartodoc: - title: Models desc: Custom model implementations contents: + - models.mamba.configuration_mamba - models.mamba.modeling_mamba - title: Data Processing desc: Data processing utilities contents: - utils.collators.core - utils.collators.batching + - utils.collators.dpo - utils.collators.mamba - utils.collators.mm_chat - utils.samplers.multipack + - utils.samplers.utils - title: Callbacks desc: Training callbacks contents: + - utils.callbacks - utils.callbacks.perplexity - utils.callbacks.profiler - utils.callbacks.lisa - utils.callbacks.mlflow_ - utils.callbacks.comet_ - utils.callbacks.qat + - utils.callbacks.dynamic_checkpoint + - utils.callbacks.generation + - utils.callbacks.models + - utils.callbacks.opentelemetry + - utils.callbacks.swanlab + - utils.callbacks.tokens_per_second + - utils.callbacks.trackio_ + - title: Scripts + desc: Standalone helper scripts + contents: + - scripts.process_cleanup + - scripts.vllm_serve_lora + - scripts.vllm_worker_ext + - title: Telemetry + desc: Usage telemetry + contents: + - telemetry.callbacks + - telemetry.errors + - telemetry.manager + - telemetry.runtime_metrics website: title: "Axolotl" description: "We make fine-tuning accessible, scalable, and fun" diff --git a/docker/Dockerfile-cloud b/docker/Dockerfile-cloud index 6ab0908261..5471ba6b0c 100644 --- a/docker/Dockerfile-cloud +++ b/docker/Dockerfile-cloud @@ -4,7 +4,7 @@ FROM axolotlai/axolotl:$BASE_TAG ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" ENV HF_HUB_CACHE="/workspace/data/huggingface-cache/hub" ENV HF_HOME="/workspace/data/huggingface-cache/hub" -ENV HF_HUB_ENABLE_HF_TRANSFER="1" +ENV HF_XET_HIGH_PERFORMANCE="1" EXPOSE 8888 EXPOSE 22 diff --git a/docker/Dockerfile-cloud-no-tmux b/docker/Dockerfile-cloud-no-tmux index 594559cfd9..3b35f6ca97 100644 --- a/docker/Dockerfile-cloud-no-tmux +++ b/docker/Dockerfile-cloud-no-tmux @@ -4,7 +4,7 @@ FROM axolotlai/axolotl:$BASE_TAG ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" ENV HF_HUB_CACHE="/workspace/data/huggingface-cache/hub" ENV HF_HOME="/workspace/data/huggingface-cache/hub" -ENV HF_HUB_ENABLE_HF_TRANSFER="1" +ENV HF_XET_HIGH_PERFORMANCE="1" EXPOSE 8888 EXPOSE 22 diff --git a/docker/Dockerfile-cloud-uv b/docker/Dockerfile-cloud-uv index 2facb6fa79..218ffce9b0 100644 --- a/docker/Dockerfile-cloud-uv +++ b/docker/Dockerfile-cloud-uv @@ -4,7 +4,7 @@ FROM axolotlai/axolotl-uv:$BASE_TAG ENV HF_DATASETS_CACHE="/workspace/data/huggingface-cache/datasets" ENV HF_HUB_CACHE="/workspace/data/huggingface-cache/hub" ENV HF_HOME="/workspace/data/huggingface-cache/hub" -ENV HF_HUB_ENABLE_HF_TRANSFER="1" +ENV HF_XET_HIGH_PERFORMANCE="1" EXPOSE 8888 EXPOSE 22 diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index bbc75104c1..8d70e1df26 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -36,7 +36,7 @@ "id": "msOCO4NRmRLa" }, "outputs": [], - "source": "%%capture\n# This step can take ~5-10 minutes to install dependencies\n!pip install --no-build-isolation \"axolotl>=0.16.1\"\n!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fec1a88\"" + "source": "%%capture\n# This step can take ~5-10 minutes to install dependencies\n!pip install --no-build-isolation \"axolotl>=0.16.1\"\n!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5effb44\"" }, { "cell_type": "markdown", diff --git a/examples/gemma4/31b-qlora-flex.yaml b/examples/gemma4-unified/12b-text-lora.yaml similarity index 67% rename from examples/gemma4/31b-qlora-flex.yaml rename to examples/gemma4-unified/12b-text-lora.yaml index 87221c5154..320c47567d 100644 --- a/examples/gemma4/31b-qlora-flex.yaml +++ b/examples/gemma4-unified/12b-text-lora.yaml @@ -1,17 +1,14 @@ -base_model: google/gemma-4-31B +base_model: google/gemma-4-12B-it plugins: - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin - axolotl.integrations.liger.LigerPlugin -torch_compile: true -liger_layer_norm: true -liger_rope: true liger_rms_norm: true liger_glu_activation: true -liger_rms_norm_gated: true +liger_layer_norm: true strict: false -chat_template: gemma4 +chat_template: gemma4_unified datasets: - path: mlabonne/FineTome-100k type: chat_template @@ -21,27 +18,17 @@ datasets: role: from content: value val_set_size: 0.05 -output_dir: ./outputs/gemma4-31b-qlora-flex +output_dir: ./outputs/gemma4-unified-12b-lora sequence_len: 2048 sample_packing: true -load_in_4bit: true -adapter: qlora +adapter: lora lora_r: 16 lora_alpha: 32 lora_dropout: 0 - -# Restrict LoRA to text backbone only (skip vision/audio encoders) lora_target_modules: 'model.language_model.layers.[\d]+.(_checkpoint_wrapped_module.)?(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' -lora_mlp_kernel: false -lora_qkv_kernel: false -lora_o_kernel: false - -bnb_config_kwargs: - bnb_4bit_use_double_quant: true - wandb_project: wandb_entity: wandb_watch: @@ -50,6 +37,7 @@ wandb_log_model: gradient_accumulation_steps: 4 micro_batch_size: 1 +num_epochs: 1 optimizer: adamw_torch_8bit lr_scheduler: cosine learning_rate: 0.0002 @@ -58,11 +46,10 @@ bf16: auto tf32: true gradient_checkpointing: true -activation_offloading: true logging_steps: 1 -# FA not supported -attn_implementation: flex_attention +gemma4_hybrid_attn_impl: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/gemma4-unified/12b-vision-lora.yaml b/examples/gemma4-unified/12b-vision-lora.yaml new file mode 100644 index 0000000000..d1fbee291d --- /dev/null +++ b/examples/gemma4-unified/12b-vision-lora.yaml @@ -0,0 +1,60 @@ +base_model: google/gemma-4-12B-it + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.liger.LigerPlugin +liger_rms_norm: true +liger_glu_activation: true +liger_layer_norm: true + +processor_type: AutoProcessor +freeze_mm_modules: true +strict: false + +skip_prepare_dataset: true +remove_unused_columns: false +sample_packing: false + +chat_template: gemma4_unified +datasets: + - path: HuggingFaceH4/llava-instruct-mix-vsft + type: chat_template + split: train[:100] + +val_set_size: 0 +output_dir: ./outputs/gemma4-unified-12b-vision-lora + +adapter: lora +sequence_len: 2048 +pad_to_sequence_len: false + +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0 +lora_target_modules: 'model.language_model.layers.[\d]+.(_checkpoint_wrapped_module.)?(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +max_steps: 10 +optimizer: adamw_torch_8bit +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +logging_steps: 1 +attn_implementation: sdpa + +warmup_ratio: 0.1 +weight_decay: 0.0 + +wandb_project: +wandb_entity: +wandb_watch: +wandb_name: +wandb_log_model: diff --git a/examples/gemma4-unified/README.md b/examples/gemma4-unified/README.md new file mode 100644 index 0000000000..5964956801 --- /dev/null +++ b/examples/gemma4-unified/README.md @@ -0,0 +1,42 @@ +# Finetune Google's Gemma 4 Unified with Axolotl + +[Gemma 4 Unified](https://huggingface.co/google/gemma-4-12B-it) is the **encoder-free** multimodal member of the [Gemma 4](https://huggingface.co/collections/google/gemma-4) family; no vision tower and no audio tower. Raw image patches and 16 kHz waveform frames are projected directly into the language model through lightweight `LayerNorm/RMSNorm → Linear` pipelines. The text backbone is the standard Gemma 4 decoder (mixed sliding/global attention, `global_head_dim=512`, optional KV sharing). + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy) to reduce training VRAM usage. + +3. Run the finetuning example: + +```bash +# Text LoRA (1x96GB @ ~27.76 GiB) +axolotl train examples/gemma4-unified/12b-text-lora.yaml + +# Vision LoRA (1x96GB @ ~26.5 GiB) +axolotl train examples/gemma4-unified/12b-vision-lora.yaml +``` + +## Limitations + +- **Attention**: FA2 (max head_dim=256) / FA4 (max head_dim=128) cannot serve `global_head_dim=512` on their own. For `sample_packing: true`, use `flex_attention` or `gemma4_hybrid_attn_impl: true` (with `flash_attention` varlen for sliding-window layers + block-diagonal `sdpa` for global layers, packing-safe). +- **lora_target_linear**: incompatible for multimodal. Use `lora_target_modules` as seen in vision example. + +### TIPS + +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). +- The text dataset format follows the OpenAI Messages format as seen [here](https://docs.axolotl.ai/docs/dataset-formats/conversation.html#chat_template). +- The multimodal dataset format follows the OpenAI multi-content Messages format as seen [here](https://docs.axolotl.ai/docs/multimodal.html#dataset-format). + +## Optimization Guides + +Please check the [Optimizations doc](https://docs.axolotl.ai/docs/optimizations.html). + +## Related Resources + +- [Gemma 4 Blog](https://blog.google/innovation-and-ai/technology/developers-tools/introducing-gemma-4-12B/) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/gemma4/26b-a4b-moe-qlora.yaml b/examples/gemma4/26b-a4b-moe-qlora.yaml index c954a5bc2f..061ecf7953 100644 --- a/examples/gemma4/26b-a4b-moe-qlora.yaml +++ b/examples/gemma4/26b-a4b-moe-qlora.yaml @@ -1,13 +1,3 @@ -# Gemma 4 26B-A4B MoE QLoRA with ScatterMoE kernels -# -# Validated: 50 steps on FineTome-100k, loss 8.8 -> 1.8, single RTX 5090 (32GB) -# torch_compile=true: 21 GiB peak VRAM, ~230 tok/s, 336s total -# -# Key notes: -# - Max sequence length on 32GB GPU: 2048 (micro_batch_size=1, SDP attention). -# 4096 seq_len OOMs due to head_dim=512 math SDP materializing full score matrix. -# Use 48GB+ GPUs for longer sequences or multi-GPU with FSDP. - base_model: google/gemma-4-26B-A4B plugins: @@ -89,8 +79,8 @@ gradient_checkpointing: true activation_offloading: true logging_steps: 1 -# FA2 not supported -attn_implementation: sdpa +# FA2 not supported (head_dim=512); flex is varlen-capable, required for sample_packing +attn_implementation: flex_attention warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/gemma4/31b-qlora.yaml b/examples/gemma4/31b-qlora.yaml index 4a633436e0..610fcebbb7 100644 --- a/examples/gemma4/31b-qlora.yaml +++ b/examples/gemma4/31b-qlora.yaml @@ -3,7 +3,6 @@ base_model: google/gemma-4-31B plugins: - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin - axolotl.integrations.liger.LigerPlugin -torch_compile: false liger_layer_norm: true liger_rope: true liger_rms_norm: true @@ -59,8 +58,8 @@ gradient_checkpointing: true activation_offloading: true logging_steps: 1 -# FA not supported -attn_implementation: sdpa +gemma4_hybrid_attn_impl: true +attn_implementation: flash_attention_2 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/examples/gemma4/README.md b/examples/gemma4/README.md index 68274ee68a..36db373e46 100644 --- a/examples/gemma4/README.md +++ b/examples/gemma4/README.md @@ -11,39 +11,29 @@ 3. Run the finetuning example: ```bash -# 26B MoE QLoRA (1x80GB @ ~50 GiB) +# 26B MoE QLoRA (1x80GB) axolotl train examples/gemma4/26b-a4b-moe-qlora.yaml -# 31B Dense QLoRA (1x80GB @ ~44 GiB) +# 31B Dense QLoRA (1x80GB @ ~25.2 GiB) axolotl train examples/gemma4/31b-qlora.yaml -# 31B Dense QLoRA Flex Attn (1x80GB @ ~26 GiB) -axolotl train examples/gemma4/31b-qlora-flex.yaml +# E2B vision LoRA (1x80GB @ ~10.4 GiB) +axolotl train examples/gemma4/e2b-vision-lora.yaml ``` ### MoE Expert Quantization & Expert LoRA (26B-A4B only) The 26B-A4B config uses ScatterMoE kernels via the transformers `ExpertsInterface` and quantizes expert weights on load. To learn about expert quantization, expert LoRA targeting, and related limitations, see the [MoE Expert Quantization](https://docs.axolotl.ai/docs/expert_quantization.html) docs. -## Flex Attention - -Reduce ~40% VRAM (at the cost of up to half throughput) by setting the below (shown in `examples/gemma4/31b-qlora-flex.yaml`): - -```yaml -torch_compile: true -flex_attention: true -``` - -This works for both the MoE and Dense model. - ## Limitations -- **Flash Attention**: FA2 (max head_dim=256) and FA4 (max head_dim=128) cannot support Gemma 4's `global_head_dim=512`. Use SDP or flex attention instead. -- **LoRA kernels**: Not supported due to KV-sharing layers. -- **lora_target_linear**: Incompatible for multimodal models — use `lora_target_modules` with a regex to restrict LoRA to the text backbone. +- **Flash Attention**: FA2 (max head_dim=256) and FA4 (max head_dim=128) cannot serve Gemma 4's `global_head_dim=512` on their own. Use `flex_attention`, or `gemma4_hybrid_attn_impl: true` to run the sliding-window layers under FA2 and the global (head_dim=512) layers under `sdpa` (requires `attn_implementation: flash_attention_2` and a flash-attn build for your GPU arch). +- **LoRA kernels**: Not supported for models with KV-sharing layers. +- **lora_target_linear**: Incompatible for multimodal models; use `lora_target_modules` with a regex to restrict LoRA to the text backbone. ### TIPS +- `gemma4_hybrid_attn_impl: true` trains ~2× faster than `flex_attention` on 31B (~25.2 GiB reserved, packing on) and avoids the flex `head_dim=512` kernel, which can exhaust shared memory on Blackwell. - Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). - You can run full finetuning by removing `adapter: qlora`, `load_in_4bit: true`, and `quantize_moe_experts: true` from the config. This is heavy and has not been tested. diff --git a/examples/gemma4/e2b-vision-lora.yaml b/examples/gemma4/e2b-vision-lora.yaml index ae90bc1cb7..5971640681 100644 --- a/examples/gemma4/e2b-vision-lora.yaml +++ b/examples/gemma4/e2b-vision-lora.yaml @@ -1,8 +1,3 @@ -# Gemma 4 E2B Vision LoRA -# -# Fine-tuning LM LoRA adapters on multimodal Gemma4 with vision/multimodal modules frozen. -# Uses the base ProcessingStrategy (auto-detects image_token from processor). - base_model: google/gemma-4-E2B-it processor_type: AutoProcessor freeze_mm_modules: true diff --git a/pyproject.toml b/pyproject.toml index 03aaf2e7b6..ce513eb33c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,10 +14,10 @@ dependencies = [ # Core ML stack "torch>=2.9.1", "packaging==26.0", - "huggingface_hub>=1.1.7", + "huggingface_hub>=1.5.0", "peft>=0.19.1,<0.20.0", "tokenizers>=0.22.1", - "transformers==5.9.0", + "transformers==5.10.2", "accelerate==1.13.0", "datasets>=4.8.4,<4.9.0", "trl==1.5.1", diff --git a/scripts/cutcrossentropy_install.py b/scripts/cutcrossentropy_install.py index 5f716b779f..c33216ee3b 100644 --- a/scripts/cutcrossentropy_install.py +++ b/scripts/cutcrossentropy_install.py @@ -29,5 +29,5 @@ print( UNINSTALL_PREFIX - + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fec1a88"' + + f'{UV_PREFIX}pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5effb44"' ) diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 3770326534..549b69ce18 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -16,7 +16,6 @@ import abc import importlib -import logging import sys from abc import abstractmethod from contextlib import suppress @@ -45,9 +44,10 @@ ) from axolotl.utils.callbacks.profiler import PytorchProfilerCallback from axolotl.utils.distributed import build_parallelism_config +from axolotl.utils.logging import get_logger from axolotl.utils.schemas.enums import CustomSupportedOptimizers -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) with suppress(ImportError): import torch._dynamo @@ -187,6 +187,8 @@ def get_callbacks(self) -> list[TrainerCallback]: if self.cfg.fused_attn_kernel or self.cfg.model_config_type in ( "gemma4", "gemma4_text", + "gemma4_unified", + "gemma4_unified_text", ): from axolotl.kernels.autotune_telemetry import ( FusedRopeAutotuneReportCallback, diff --git a/src/axolotl/core/trainers/base.py b/src/axolotl/core/trainers/base.py index 6ad80bf9ea..314d642f15 100644 --- a/src/axolotl/core/trainers/base.py +++ b/src/axolotl/core/trainers/base.py @@ -418,7 +418,7 @@ def compute_loss( if ( "mm_token_type_ids" not in inputs and "input_ids" in inputs - and _model_type == "gemma4" + and _model_type in ("gemma4", "gemma4_unified") ): inputs["mm_token_type_ids"] = torch.zeros_like(inputs["input_ids"]) @@ -429,7 +429,7 @@ def compute_loss( # per-sequence causal masks. if ( self.args.sample_packing - and _model_type in ("gemma4", "gemma3") + and _model_type in ("gemma4", "gemma3", "gemma4_unified") and "attention_mask" in inputs and "position_ids" in inputs ): @@ -443,23 +443,6 @@ def compute_loss( num_items_in_batch=num_items_in_batch, ) - # Gemma4ForConditionalGeneration computes loss with a manual - # nn.CrossEntropyLoss() that bypasses proper num_items_in_batch - # normalization and does redundant attention_mask filtering. - # Compute loss externally using the standard loss_function instead. - if _model_type == "gemma4" and "labels" in inputs: - labels = inputs.pop("labels") - outputs = model(**inputs) - logits = outputs.logits - unwrapped = self.accelerator.unwrap_model(model) - vocab_size = unwrapped.config.get_text_config().vocab_size - loss = unwrapped.loss_function( - logits, labels, vocab_size, num_items_in_batch=num_items_in_batch - ) - if return_outputs: - return loss, outputs - return loss - return super().compute_loss( model, inputs, @@ -489,7 +472,7 @@ def prediction_step(self, model, inputs, prediction_loss_only, ignore_keys=None) if ( "mm_token_type_ids" not in inputs and "input_ids" in inputs - and _model_type == "gemma4" + and _model_type in ("gemma4", "gemma4_unified") ): inputs["mm_token_type_ids"] = torch.zeros_like(inputs["input_ids"]) return super().prediction_step( @@ -608,9 +591,15 @@ def orpo_compute_loss( "attention_mask": concat_inputs["attention_mask"], "labels": concat_inputs["labels"], } - # Gemma4 requires mm_token_type_ids during training (even for text-only) + # Gemma4 requires mm_token_type_ids during training (even for text-only). + # Unwrap to read .config (DDP/DeepSpeed wrappers don't proxy it). + _orpo_model_type = getattr( + getattr(self.accelerator.unwrap_model(model), "config", None), + "model_type", + None, + ) if ( - getattr(getattr(model, "config", None), "model_type", None) == "gemma4" + _orpo_model_type in ("gemma4", "gemma4_unified") and "mm_token_type_ids" not in concat_inputs ): forward_kwargs["mm_token_type_ids"] = torch.zeros_like( diff --git a/src/axolotl/core/trainers/grpo/async_trainer.py b/src/axolotl/core/trainers/grpo/async_trainer.py index 4759a30b01..79b7512537 100644 --- a/src/axolotl/core/trainers/grpo/async_trainer.py +++ b/src/axolotl/core/trainers/grpo/async_trainer.py @@ -22,7 +22,6 @@ import atexit import concurrent.futures -import logging import queue import threading from abc import ABC, abstractmethod @@ -89,6 +88,7 @@ def disable_gradient_checkpointing(model, kwargs): except ImportError: _fused_selective_log_softmax = None +from axolotl.utils.logging import get_logger # --------------------------------------------------------------------------- # Config @@ -189,8 +189,8 @@ class AsyncGRPOConfig(GRPOConfig): # Data Producer Protocol (standalone — no transformers branch needed) # --------------------------------------------------------------------------- -logger = logging.getLogger(__name__) -_dp_logger = logging.getLogger(__name__ + ".data_producer") +logger = get_logger(__name__) +_dp_logger = get_logger(__name__ + ".data_producer") @dataclass @@ -1296,9 +1296,7 @@ def _zero_pad_embedding_for_fp8(self): if pad_id is not None and pad_id < embed.weight.shape[0]: with torch.no_grad(): embed.weight.data[pad_id].zero_() - import logging - - logging.getLogger("async_grpo").info( + get_logger("async_grpo").info( f"Zeroed pad token embedding (id={pad_id}) for FP8 NaN prevention" ) diff --git a/src/axolotl/core/trainers/grpo/fast_async_trainer.py b/src/axolotl/core/trainers/grpo/fast_async_trainer.py index 9d1128b973..26b1789fc7 100644 --- a/src/axolotl/core/trainers/grpo/fast_async_trainer.py +++ b/src/axolotl/core/trainers/grpo/fast_async_trainer.py @@ -24,7 +24,6 @@ from __future__ import annotations import asyncio -import logging import threading from dataclasses import dataclass, field @@ -38,8 +37,9 @@ GRPODataProducer, ) from axolotl.core.trainers.grpo.replay_buffer import ReplayBuffer +from axolotl.utils.logging import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # --------------------------------------------------------------------------- diff --git a/src/axolotl/integrations/cut_cross_entropy/README.md b/src/axolotl/integrations/cut_cross_entropy/README.md index 2ccf11f18b..113e79a25b 100644 --- a/src/axolotl/integrations/cut_cross_entropy/README.md +++ b/src/axolotl/integrations/cut_cross_entropy/README.md @@ -19,7 +19,7 @@ python scripts/cutcrossentropy_install.py | sh - If you are installing from pip ```bash -pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fec1a88" +pip3 uninstall -y cut-cross-entropy && pip3 install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5effb44" ``` ## Usage @@ -36,8 +36,13 @@ plugins: - arcee - cohere - cohere2 +- cohere2_moe +- deepseek_v2 - deepseek_v3 +- deepseek_v4 - exaone4 +- exaone4_5 +- exaone_moe - gemma - gemma2 - gemma3 @@ -45,6 +50,9 @@ plugins: - gemma3n - gemma3n_text - gemma4 +- gemma4_text +- gemma4_unified +- gemma4_unified_text - glm - glm4 - glm4_moe diff --git a/src/axolotl/integrations/cut_cross_entropy/__init__.py b/src/axolotl/integrations/cut_cross_entropy/__init__.py index 5de10020cd..6773c159ef 100644 --- a/src/axolotl/integrations/cut_cross_entropy/__init__.py +++ b/src/axolotl/integrations/cut_cross_entropy/__init__.py @@ -35,7 +35,7 @@ _CCE_INSTALL_MESSAGE = ( "Please install Axolotl's fork of cut_cross_entropy with transformers support using " - '`pip uninstall -y cut-cross-entropy && pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@fec1a88"`' + '`pip uninstall -y cut-cross-entropy && pip install "cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5effb44"`' ) diff --git a/src/axolotl/integrations/hatchery/rewards/math_reward.py b/src/axolotl/integrations/hatchery/rewards/math_reward.py index 970353b0bd..a0e71b7c90 100644 --- a/src/axolotl/integrations/hatchery/rewards/math_reward.py +++ b/src/axolotl/integrations/hatchery/rewards/math_reward.py @@ -11,10 +11,11 @@ from __future__ import annotations -import logging import re -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) def extract_boxed(text: str) -> str | None: diff --git a/src/axolotl/integrations/kd/chat_template.py b/src/axolotl/integrations/kd/chat_template.py index 5cae69e7c7..5330a80f1e 100644 --- a/src/axolotl/integrations/kd/chat_template.py +++ b/src/axolotl/integrations/kd/chat_template.py @@ -16,14 +16,14 @@ Chat template prompt strategy loader with KD support """ -import logging from typing import Any, Dict import torch from axolotl.prompt_strategies.chat_template import ChatTemplateStrategy, StrategyLoader +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) class ChatTemplateStrategyWithKD(ChatTemplateStrategy): diff --git a/src/axolotl/integrations/kd/collator_online_teacher.py b/src/axolotl/integrations/kd/collator_online_teacher.py index 54e55a5e7f..2cc48cf86a 100644 --- a/src/axolotl/integrations/kd/collator_online_teacher.py +++ b/src/axolotl/integrations/kd/collator_online_teacher.py @@ -4,7 +4,6 @@ import hashlib import hmac -import logging from typing import Any, Dict, List, Optional import requests @@ -14,8 +13,9 @@ from axolotl.integrations.kd.collator import KDBatchSamplerDataCollatorForSeq2Seq from axolotl.integrations.kd.utils import normalize_logprobs from axolotl.utils.data.utils import retry_on_request_exceptions +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def hmac_sha_from_int_list(int_list, key, hash_func=hashlib.sha256): diff --git a/src/axolotl/integrations/kernels/autotune_callback.py b/src/axolotl/integrations/kernels/autotune_callback.py index aa4cbbab1c..9c311dcff4 100644 --- a/src/axolotl/integrations/kernels/autotune_callback.py +++ b/src/axolotl/integrations/kernels/autotune_callback.py @@ -1,7 +1,5 @@ """Trainer callback for reporting Triton autotune results from scattermoe-lora kernels.""" -import logging - import torch from transformers import ( TrainerCallback, @@ -10,7 +8,9 @@ TrainingArguments, ) -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) # Give up looking for autotune data after this many training steps. _MAX_POLL_STEP = 5 diff --git a/src/axolotl/integrations/kernels/autotune_collector.py b/src/axolotl/integrations/kernels/autotune_collector.py index 92e0dbe2d5..bb1919fc3e 100644 --- a/src/axolotl/integrations/kernels/autotune_collector.py +++ b/src/axolotl/integrations/kernels/autotune_collector.py @@ -6,12 +6,13 @@ do with the data. """ -import logging import sys from types import ModuleType from typing import Any -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) # (human-readable name, attribute on the lora_ops module) _KERNEL_REGISTRY: list[tuple[str, str]] = [ diff --git a/src/axolotl/integrations/liger/plugin.py b/src/axolotl/integrations/liger/plugin.py index c7f8c560e9..09aec5342c 100644 --- a/src/axolotl/integrations/liger/plugin.py +++ b/src/axolotl/integrations/liger/plugin.py @@ -292,6 +292,60 @@ def __init__(self, config, layer_idx=None): f"rms_norm={cfg.liger_rms_norm}, glu={cfg.liger_glu_activation}, " f"rope=False (incompatible), layer_norm={cfg.liger_layer_norm}" ) + elif cfg.model_config_type in ("gemma4_unified", "gemma4_unified_text"): + # gemma4_unified mirrors gemma4's Liger compatibility: offset=0, + # in_place=False (gradient-checkpoint safe), RoPE incompatible + # (separate q/k application). Classes live in the unified namespace. + from liger_kernel.transformers.geglu import LigerGEGLUMLP + from transformers.models.gemma4_unified import modeling_gemma4_unified + + if cfg.liger_rms_norm: + _OrigGemma4UnifiedRMSNorm = modeling_gemma4_unified.Gemma4UnifiedRMSNorm + + class _LigerGemma4UnifiedRMSNorm(LigerRMSNorm): + """LigerRMSNorm for Gemma4Unified (in_place=False, with_scale).""" + + def __new__(cls, dim, eps=1e-6, with_scale=True): + if not with_scale: + return _OrigGemma4UnifiedRMSNorm(dim, eps, with_scale=False) + return super().__new__(cls) + + def __init__(self, dim, eps=1e-6, with_scale=True): + if not with_scale: + return + super().__init__( + dim, eps, offset=0.0, casting_mode="llama", in_place=False + ) + + modeling_gemma4_unified.Gemma4UnifiedRMSNorm = ( + _LigerGemma4UnifiedRMSNorm + ) + if cfg.liger_glu_activation: + + class _LigerGemma4UnifiedMLP(LigerGEGLUMLP): + def __init__(self, config, layer_idx=None): + super().__init__(config) + + modeling_gemma4_unified.Gemma4UnifiedTextMLP = _LigerGemma4UnifiedMLP + if cfg.liger_rope: + LOG.warning( + "Liger RoPE is not compatible with Gemma4Unified (separate " + "q/k application). Skipping." + ) + if cfg.liger_layer_norm: + modeling_gemma4_unified.nn.LayerNorm = LigerLayerNorm + if cfg.liger_cross_entropy: + modeling_gemma4_unified.nn.CrossEntropyLoss = LigerCrossEntropyLoss + if cfg.liger_fused_linear_cross_entropy: + LOG.warning( + "Liger fused linear cross entropy is not compatible with " + "Gemma4Unified. Skipping." + ) + LOG.info( + f"Applied Liger kernels for gemma4_unified: " + f"rms_norm={cfg.liger_rms_norm}, glu={cfg.liger_glu_activation}, " + f"rope=False (incompatible), layer_norm={cfg.liger_layer_norm}" + ) elif cfg.liger_fused_linear_cross_entropy: try: from .models.base import patch_lce_forward diff --git a/src/axolotl/kernels/autotune_telemetry.py b/src/axolotl/kernels/autotune_telemetry.py index a729fd7ccb..0b0bf7838c 100644 --- a/src/axolotl/kernels/autotune_telemetry.py +++ b/src/axolotl/kernels/autotune_telemetry.py @@ -7,8 +7,6 @@ across architectures can be aggregated. """ -import logging - import torch from transformers import ( TrainerCallback, @@ -17,7 +15,9 @@ TrainingArguments, ) -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) # Give up looking for autotune data after this many training steps. _MAX_POLL_STEP = 5 diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index 495ff58851..ec72665b00 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -160,6 +160,7 @@ def apply_post_model_build_patches(self, model: PreTrainedModel): # cleanly in the same call even though one is instance-scoped # and the other is module-scoped. self._apply_gemma_hybrid_attention(model) + self._apply_gemma4_loss_kwargs() self._finalize_moe_expert_quantization(model) def apply_post_model_load_patches(self, model: PreTrainedModel): @@ -170,6 +171,17 @@ def apply_post_model_load_patches(self, model: PreTrainedModel): self._apply_fp8_attention_patches(model) self._apply_tiled_mlp_post_load(model) + def _apply_gemma4_loss_kwargs(self): + # Flip accepts_loss_kwargs True so the Trainer normalizes loss by + # num_items_in_batch under grad accumulation (must run before trainer init). + if self.cfg.model_config_type not in ("gemma4", "gemma4_unified"): + return + from axolotl.monkeypatch.gemma4_loss_kwargs import ( + patch_gemma4_accepts_loss_kwargs, + ) + + patch_gemma4_accepts_loss_kwargs() + def _apply_gemma_hybrid_attention(self, model: PreTrainedModel): """Apply hybrid attention: FA2 for sliding window layers, SDPA for global layers. @@ -312,14 +324,6 @@ def _apply_fsdp_patches(self): patch_trl_prepare_fsdp2() - # if self.cfg.fsdp_config: - # # see transformers#39152 - # from axolotl.monkeypatch.trainer_fsdp_optim import ( - # patch_training_loop_for_fsdp, - # ) - # - # patch_training_loop_for_fsdp() - def _apply_adapter_patches(self): """Apply patches for adapter configurations.""" if self.cfg.adapter and self.cfg.embeddings_skip_upcast: @@ -364,6 +368,8 @@ def _apply_flash_attn_4_patches(self): "qwen3_5_moe_text", "gemma4", "gemma4_text", + "gemma4_unified", + "gemma4_unified_text", ) @staticmethod @@ -386,16 +392,10 @@ def _apply_model_specific_patches(self): """Apply patches specific to model architectures.""" self._warn_if_fused_attn_unsupported(self.cfg) - if self.cfg.model_config_type == "gemma4" and self.cfg.use_kernels: - # transformers' Gemma4VisionAttention registers a bare function via - # @use_kernelized_func, which crashes model.kernelize() (triggered by - # use_kernels=True) when it tries to register_module() a non-Module. - # Strip the dead entry so kernelize() succeeds. The MoE itself is - # accelerated via the ExpertsInterface (experts_implementation), - # independent of this path. - from axolotl.monkeypatch.gemma4_kernelize import patch_gemma4_kernelize + if getattr(self.cfg, "use_kernels", None): + from axolotl.monkeypatch.kernelize_fixes import patch_kernelize_fixes - patch_gemma4_kernelize() + patch_kernelize_fixes() if ( self.cfg.model_config_type == "llama4" @@ -482,19 +482,12 @@ def _apply_model_specific_patches(self): patch_qwen3_5_vlm_flash_attention() - if self.cfg.model_config_type in ("gemma4", "gemma4_text"): - # The fused attn path is now compatible with - # ``gemma4_hybrid_attn_impl``: the kernel handles partial - # rotary (cos.shape[-1] < head_dim) and the fused forward - # mirrors the current ``Gemma4TextAttention.forward`` API - # for shared kv (read from / write to - # ``past_key_values.shared_layers``). See - # ``src/axolotl/kernels/GEMMA4_FUSED_ROPE_HYBRID_ATTN_BUG.md`` - # for the history. - from axolotl.monkeypatch.models.gemma4.fused_attn import ( - patch_gemma4_fused_attn, - ) - + if self.cfg.model_config_type in ( + "gemma4", + "gemma4_text", + "gemma4_unified", + "gemma4_unified_text", + ): # Shared-KV side channel when activation checkpointing (PR #3611). fsdp_cfg = self.cfg.fsdp_config needs_shared_kv_workaround = (not self.inference) and bool( @@ -502,7 +495,18 @@ def _apply_model_specific_patches(self): or self.cfg.activation_offloading or (fsdp_cfg is not None and fsdp_cfg.activation_checkpointing) ) - patch_gemma4_fused_attn( + if self.cfg.model_config_type in ( + "gemma4_unified", + "gemma4_unified_text", + ): + from axolotl.monkeypatch.models.gemma4_unified.fused_attn import ( + patch_gemma4_unified_fused_attn as patch_fused_attn, + ) + else: + from axolotl.monkeypatch.models.gemma4.fused_attn import ( + patch_gemma4_fused_attn as patch_fused_attn, + ) + patch_fused_attn( install_shared_kv_workaround=needs_shared_kv_workaround ) diff --git a/src/axolotl/monkeypatch/attention/fp8_attn.py b/src/axolotl/monkeypatch/attention/fp8_attn.py index 224e8c3b7e..3e0ca0408e 100644 --- a/src/axolotl/monkeypatch/attention/fp8_attn.py +++ b/src/axolotl/monkeypatch/attention/fp8_attn.py @@ -11,11 +11,11 @@ HF's "sdpa" attention implementation for the patch to intercept attention calls. """ -import logging - import torch -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) def patch_fp8_attention(model: torch.nn.Module) -> torch.nn.Module: diff --git a/src/axolotl/monkeypatch/gemma4_hybrid_mask.py b/src/axolotl/monkeypatch/gemma4_hybrid_mask.py index 17b8cf053d..2272291a93 100644 --- a/src/axolotl/monkeypatch/gemma4_hybrid_mask.py +++ b/src/axolotl/monkeypatch/gemma4_hybrid_mask.py @@ -1,4 +1,4 @@ -"""Hybrid attention mask fix for Gemma 4. +"""Hybrid attention mask fix for Gemma 4 (standard and unified). Gemma 4 has full-attention (global) layers with ``head_dim=512`` which exceeds flash-attention-2's supported size. Axolotl's hybrid-attention @@ -20,12 +20,16 @@ ...when the sequence length grows past roughly 7k tokens. This module fixes the symptom by monkey-patching ``create_causal_mask`` in -``transformers.models.gemma4.modeling_gemma4``'s module namespace — NOT -the original in ``masking_utils``. The wrapper forces -``_attn_implementation="sdpa"`` on a shallow-copied config before calling -through, so the ``full_attention`` mask built inside ``Gemma4TextModel.forward`` -is always 4D/SDPA-compatible. ``create_sliding_window_causal_mask`` is left -alone, so sliding-window layers continue to receive FA2-format masks. +the model's *module namespace* — NOT the original in ``masking_utils``. The +wrapper forces ``_attn_implementation="sdpa"`` on a shallow-copied config +before calling through, so the ``full_attention`` mask built inside the +text backbone's ``forward`` is always 4D/SDPA-compatible. +``create_sliding_window_causal_mask`` is left alone, so sliding-window +layers continue to receive FA2-format masks. + +``gemma4_unified`` reproduces the same mixed sliding/global architecture +(``global_head_dim=512``) in its own ``modeling_gemma4_unified`` namespace, +so both namespaces are patched when present. The patch is idempotent. Install once per process, before any Gemma 4 forward pass runs. @@ -34,6 +38,7 @@ from __future__ import annotations import copy +import importlib from typing import Any from axolotl.utils.logging import get_logger @@ -42,45 +47,42 @@ _PATCH_APPLIED = False +# Each Gemma 4 variant fully redefines ``create_causal_mask`` in its own module +# namespace (gemma4_unified does NOT modular-import from gemma4), so both must be +# patched independently. +_TARGET_MODULES = ( + "transformers.models.gemma4.modeling_gemma4", + "transformers.models.gemma4_unified.modeling_gemma4_unified", +) -def patch_gemma4_hybrid_mask() -> bool: - """Install the Gemma 4 hybrid-attention mask fix. - Returns ``True`` if the patch was installed (or was already installed), - ``False`` if the target module could not be imported (e.g. transformers - version predates Gemma 4) — in which case nothing is done and the - caller can continue unaffected. - """ - global _PATCH_APPLIED - if _PATCH_APPLIED: - return True +def _patch_module_create_causal_mask(module: Any) -> bool: + """Wrap ``create_causal_mask`` in a single module namespace. - try: - from transformers.models.gemma4 import modeling_gemma4 - except ImportError: - LOG.debug( - "gemma4_hybrid_mask: transformers.models.gemma4 not importable, " - "skipping. This is fine for non-Gemma4 training." - ) - return False - - if not hasattr(modeling_gemma4, "create_causal_mask"): + Re-entry is prevented by the module-level ``_PATCH_APPLIED`` flag in + :func:`patch_gemma4_hybrid_mask`, so this does not guard per-module. + Returns ``True`` if patched, ``False`` if the namespace has no + ``create_causal_mask`` binding. + """ + if not hasattr(module, "create_causal_mask"): LOG.warning( - "gemma4_hybrid_mask: modeling_gemma4 has no 'create_causal_mask' " - "binding, skipping. Transformers API may have changed." + "gemma4_hybrid_mask: %s has no 'create_causal_mask' binding, " + "skipping. Transformers API may have changed.", + module.__name__, ) return False - original = modeling_gemma4.create_causal_mask + original = module.create_causal_mask def hybrid_create_causal_mask(config: Any, *args: Any, **kwargs: Any): - """Wrapper that forces SDPA format for the full-attention mask. + """Force SDPA format for the full-attention mask. The global layers were patched to SDPA by ``_apply_gemma_hybrid_attention``, so their mask must be 4D. The original ``create_causal_mask`` dispatches on - ``config._attn_implementation``; we shadow that with a local - override. + ``config._attn_implementation``; we shadow that with a local override + on a shallow copy so the caller's config is left intact (the + sliding-window factory still reads FA2 from it). """ sdpa_config = copy.copy(config) sdpa_config._attn_implementation = "sdpa" @@ -88,28 +90,58 @@ def hybrid_create_causal_mask(config: Any, *args: Any, **kwargs: Any): # Preserve the original reference on the wrapper for tests / teardown. hybrid_create_causal_mask._axolotl_original = original # type: ignore[attr-defined] - - modeling_gemma4.create_causal_mask = hybrid_create_causal_mask - _PATCH_APPLIED = True + module.create_causal_mask = hybrid_create_causal_mask LOG.info( - "gemma4_hybrid_mask: patched modeling_gemma4.create_causal_mask to " - "force SDPA-format masks for full-attention layers" + "gemma4_hybrid_mask: patched %s.create_causal_mask to force SDPA-format " + "masks for full-attention layers", + module.__name__, ) return True +def patch_gemma4_hybrid_mask() -> bool: + """Install the Gemma 4 hybrid-attention mask fix across all variants. + + Returns ``True`` if at least one namespace was patched, ``False`` if none + of the target modules could be imported (e.g. transformers version predates + Gemma 4) — in which case nothing is done and the caller can continue + unaffected. + """ + global _PATCH_APPLIED + if _PATCH_APPLIED: + return True + + patched_any = False + for module_path in _TARGET_MODULES: + try: + module = importlib.import_module(module_path) + except ImportError: + LOG.debug( + "gemma4_hybrid_mask: %s not importable, skipping. This is fine " + "for non-Gemma4 training.", + module_path, + ) + continue + if _patch_module_create_causal_mask(module): + patched_any = True + + if patched_any: + _PATCH_APPLIED = True + return patched_any + + def unpatch_gemma4_hybrid_mask() -> None: - """Restore the original ``create_causal_mask``. Useful for tests.""" + """Restore the original ``create_causal_mask`` in every namespace. Tests.""" global _PATCH_APPLIED if not _PATCH_APPLIED: return - try: - from transformers.models.gemma4 import modeling_gemma4 - except ImportError: - _PATCH_APPLIED = False - return - current = modeling_gemma4.create_causal_mask - original = getattr(current, "_axolotl_original", None) - if original is not None: - modeling_gemma4.create_causal_mask = original + for module_path in _TARGET_MODULES: + try: + module = importlib.import_module(module_path) + except ImportError: + continue + current = getattr(module, "create_causal_mask", None) + original = getattr(current, "_axolotl_original", None) + if original is not None: + module.create_causal_mask = original _PATCH_APPLIED = False diff --git a/src/axolotl/monkeypatch/gemma4_kernelize.py b/src/axolotl/monkeypatch/gemma4_kernelize.py deleted file mode 100644 index b87f0b840d..0000000000 --- a/src/axolotl/monkeypatch/gemma4_kernelize.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Fix for transformers' Gemma 4 ``kernelize()`` crash under ``use_kernels``. - -In transformers, ``Gemma4VisionAttention`` is decorated with -``@use_kernelized_func(apply_rotary_pos_emb)`` where ``apply_rotary_pos_emb`` -is a **plain function**, not a ``@use_kernel_func_from_hub``-wrapped kernel -layer. That decorator stashes the bare function in each instance's -``_hidden_kernels`` dict. - -When ``use_kernels=True`` (which axolotl's ``KernelsArgs`` force-enables for the -ScatterMoE path), ``from_pretrained`` calls ``model.kernelize()``, whose -``attach_hidden_kernels`` step does ``module.register_module(name, fn)`` for each -``_hidden_kernels`` entry. ``register_module`` rejects a non-``nn.Module``:: - - TypeError: ...apply_rotary_pos_emb is not a Module subclass - -and the ``finally``-block cleanup then raises the visible:: - - AttributeError: 'Gemma4VisionAttention' object has no attribute apply_rotary_pos_emb - -This is a transformers bug, not Gemma4-specific in spirit (qwen3_moe avoids it -by wrapping the func with ``@use_kernel_func_from_hub`` so a Module-like ``Func`` -is registered). Notably, ``Gemma4VisionAttention.forward`` calls -``apply_multidimensional_rope`` and never references ``apply_rotary_pos_emb``, so -the registered entry is dead weight — dropping the non-Module ``_hidden_kernels`` -entries makes ``kernelize()`` a no-op for vision attention with zero behavior -change. - -The patch wraps ``Gemma4VisionAttention.__init__`` to strip any non-``nn.Module`` -``_hidden_kernels`` entries after construction. Properly-wrapped (Module) entries, -including ones a fixed transformers might introduce, are left intact, so the patch -is forward-compatible. Idempotent; install before the model is built. -""" - -from __future__ import annotations - -from axolotl.utils.logging import get_logger - -LOG = get_logger(__name__) - -_PATCH_APPLIED = False - - -def patch_gemma4_kernelize() -> bool: - """Strip dead non-Module ``_hidden_kernels`` entries on ``Gemma4VisionAttention``. - - Returns ``True`` if the patch is installed (or already was), ``False`` if the - target class could not be imported (e.g. transformers predates Gemma 4) — in - which case nothing is done and the caller can continue unaffected. - """ - global _PATCH_APPLIED - if _PATCH_APPLIED: - return True - - try: - from transformers.models.gemma4 import modeling_gemma4 - except ImportError: - LOG.debug( - "gemma4_kernelize: transformers.models.gemma4 not importable, " - "skipping. This is fine for non-Gemma4 training." - ) - return False - - cls = getattr(modeling_gemma4, "Gemma4VisionAttention", None) - if cls is None: - LOG.warning( - "gemma4_kernelize: modeling_gemma4 has no 'Gemma4VisionAttention', " - "skipping. Transformers API may have changed." - ) - return False - - import torch.nn as nn - - orig_init = cls.__init__ - - def init(self, *args, **kwargs): - orig_init(self, *args, **kwargs) - hidden_kernels = self.__dict__.get("_hidden_kernels") - if hidden_kernels: - stale = [ - name - for name, fn in hidden_kernels.items() - if not isinstance(fn, nn.Module) - ] - for name in stale: - del hidden_kernels[name] - - # Preserve the original for teardown / idempotency checks. - init._axolotl_original = orig_init # type: ignore[attr-defined] - cls.__init__ = init - _PATCH_APPLIED = True - LOG.info( - "gemma4_kernelize: patched Gemma4VisionAttention to drop non-Module " - "_hidden_kernels entries so use_kernels/kernelize() does not crash" - ) - return True - - -def unpatch_gemma4_kernelize() -> None: - """Restore the original ``Gemma4VisionAttention.__init__``. Useful for tests.""" - global _PATCH_APPLIED - if not _PATCH_APPLIED: - return - try: - from transformers.models.gemma4 import modeling_gemma4 - except ImportError: - _PATCH_APPLIED = False - return - cls = getattr(modeling_gemma4, "Gemma4VisionAttention", None) - if cls is not None: - original = getattr(cls.__init__, "_axolotl_original", None) - if original is not None: - cls.__init__ = original - _PATCH_APPLIED = False diff --git a/src/axolotl/monkeypatch/gemma4_loss_kwargs.py b/src/axolotl/monkeypatch/gemma4_loss_kwargs.py new file mode 100644 index 0000000000..d00a5a9daa --- /dev/null +++ b/src/axolotl/monkeypatch/gemma4_loss_kwargs.py @@ -0,0 +1,41 @@ +"""Flip ``accepts_loss_kwargs`` to True on Gemma 4 (Unified) ForConditionalGeneration. + +They inherit ``accepts_loss_kwargs = False`` from PaliGemma (whose loss filtered +logits/labels by attention_mask). Gemma 4's loss is the stock ``ForCausalLMLoss`` +with no such filtering, so the flag wrongly makes the Trainer withhold +``num_items_in_batch`` and mis-normalize the loss under gradient accumulation. +Install before ``Trainer.__init__`` reads the flag. +""" + +from __future__ import annotations + +import importlib + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +_TARGETS = ( + ("transformers.models.gemma4.modeling_gemma4", "Gemma4ForConditionalGeneration"), + ( + "transformers.models.gemma4_unified.modeling_gemma4_unified", + "Gemma4UnifiedForConditionalGeneration", + ), +) + + +def patch_gemma4_accepts_loss_kwargs() -> None: + """Set ``accepts_loss_kwargs=True`` on Gemma 4 (Unified) ForConditionalGeneration.""" + for module_path, cls_name in _TARGETS: + try: + cls = getattr(importlib.import_module(module_path), cls_name) + except (ImportError, AttributeError): + continue + if getattr(cls, "accepts_loss_kwargs", None) is True: + continue + cls.accepts_loss_kwargs = True + LOG.info( + "Set %s.accepts_loss_kwargs=True so the Trainer forwards " + "num_items_in_batch (correct gradient-accumulation loss normalization).", + cls_name, + ) diff --git a/src/axolotl/monkeypatch/kernelize_fixes.py b/src/axolotl/monkeypatch/kernelize_fixes.py new file mode 100644 index 0000000000..79dc13e446 --- /dev/null +++ b/src/axolotl/monkeypatch/kernelize_fixes.py @@ -0,0 +1,73 @@ +"""Repairs for transformers' ``model.kernelize()`` under ``use_kernels=True``. + +``kernelize()`` swaps each module's ``_hidden_kernels`` entries for hub +kernels, but two upstream defects crash it (transformers 5.10): ~30 +architectures (gemma4, qwen3.5, glm4, olmo, ...) stash a bare function it +refuses to register, and gpt-oss's rotary ``Func`` keeps a deprecated +``position_ids`` parameter that fails the kernels library's signature check +against ``kernels-community/rotary``. + +Wraps ``PreTrainedModel.kernelize`` to repair the stashes right before the +swap: bare functions are dropped (the model's forward still calls them +directly) and ``position_ids`` is removed from rotary signature *metadata* +(call behavior unchanged). Both repairs no-op once fixed upstream. +""" + +import inspect + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +_ORIG_KERNELIZE = None + + +def _fix_hidden_kernels(module): + import torch.nn as nn + + for name, fn in list(module.__dict__.get("_hidden_kernels", {}).items()): + if not isinstance(fn, nn.Module): + del module._hidden_kernels[name] + elif getattr(fn, "kernel_layer_name", None) == "rotary_pos_emb": + forward = type(fn).forward + signature = inspect.signature(forward) + if "position_ids" in signature.parameters: + forward.__signature__ = signature.replace( + parameters=[ + p + for p in signature.parameters.values() + if p.name != "position_ids" + ] + ) + + +def patch_kernelize_fixes() -> bool: + global _ORIG_KERNELIZE + if _ORIG_KERNELIZE is not None: + return True + + from transformers.modeling_utils import PreTrainedModel + + orig = getattr(PreTrainedModel, "kernelize", None) + if orig is None: + LOG.warning("kernelize_fixes: PreTrainedModel.kernelize not found, skipping") + return False + + def kernelize(self, *args, **kwargs): + self.apply(_fix_hidden_kernels) + return orig(self, *args, **kwargs) + + PreTrainedModel.kernelize = kernelize + _ORIG_KERNELIZE = orig + LOG.info("kernelize_fixes: patched PreTrainedModel.kernelize") + return True + + +def unpatch_kernelize_fixes() -> None: + global _ORIG_KERNELIZE + if _ORIG_KERNELIZE is None: + return + from transformers.modeling_utils import PreTrainedModel + + PreTrainedModel.kernelize = _ORIG_KERNELIZE + _ORIG_KERNELIZE = None diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 8156b72c7d..05e623047c 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -208,6 +208,13 @@ def get_attention_cls_from_config(cfg: DictDefault) -> Type[nn.Module]: return Gemma4TextAttention + if model_type in ("gemma4_unified", "gemma4_unified_text"): + from transformers.models.gemma4_unified.modeling_gemma4_unified import ( + Gemma4UnifiedTextAttention, + ) + + return Gemma4UnifiedTextAttention + try: # Dynamically import the module and attention class module_path = f"transformers.models.{model_type}.modeling_{model_type}" @@ -263,6 +270,32 @@ def patch_self_attn_lora(cfg: DictDefault): except ImportError: pass + # gemma4_unified's attention forward can't be source-rewritten (KV sharing); skip. + try: + from transformers.models.gemma4_unified.modeling_gemma4_unified import ( + Gemma4UnifiedTextAttention, + ) + except ImportError: + Gemma4UnifiedTextAttention = None + + if ( + Gemma4UnifiedTextAttention is not None + and attention_cls is Gemma4UnifiedTextAttention + ): + if cfg.fused_attn_kernel: + LOG.info( + "Gemma4UnifiedTextAttention uses the fused attention path " + "(apply_qkv/apply_o) - skipping LoRA source rewrite" + ) + else: + LOG.warning( + "lora_qkv_kernel/lora_o_kernel cannot attach to gemma4_unified " + "without fused_attn_kernel: true - skipping QKV/O rewrite " + "(MLP/embedding kernels still apply). Set fused_attn_kernel: true " + "to enable them." + ) + return + self_attn_forward = inspect.getsource(attention_cls.forward) attention_cls._original_forward = self_attn_forward self_attn_forward, _ = detab_code(self_attn_forward) diff --git a/src/axolotl/monkeypatch/models/gemma4/fused_attn.py b/src/axolotl/monkeypatch/models/gemma4/fused_attn.py index a4f15207f8..3112b5919f 100644 --- a/src/axolotl/monkeypatch/models/gemma4/fused_attn.py +++ b/src/axolotl/monkeypatch/models/gemma4/fused_attn.py @@ -57,7 +57,7 @@ def fused_forward( hidden_states: torch.Tensor, position_embeddings: torch.Tensor, attention_mask: torch.Tensor | None, - shared_kv_states: dict[int, tuple[torch.Tensor, torch.Tensor]] | None = None, + shared_kv_states: dict[str, tuple[torch.Tensor, torch.Tensor]] | None = None, past_key_values=None, **kwargs, ) -> tuple[torch.Tensor, torch.Tensor | None]: diff --git a/src/axolotl/monkeypatch/models/gemma4_unified/__init__.py b/src/axolotl/monkeypatch/models/gemma4_unified/__init__.py new file mode 100644 index 0000000000..c4ec3b89cf --- /dev/null +++ b/src/axolotl/monkeypatch/models/gemma4_unified/__init__.py @@ -0,0 +1 @@ +"""Gemma 4 Unified model monkeypatches.""" diff --git a/src/axolotl/monkeypatch/models/gemma4_unified/fused_attn.py b/src/axolotl/monkeypatch/models/gemma4_unified/fused_attn.py new file mode 100644 index 0000000000..032804dbd2 --- /dev/null +++ b/src/axolotl/monkeypatch/models/gemma4_unified/fused_attn.py @@ -0,0 +1,220 @@ +""" +Gemma 4 Unified fused attention monkeypatch. + +Mirrors :mod:`axolotl.monkeypatch.models.gemma4.fused_attn` for the +encoder-free ``gemma4_unified`` text backbone (``Gemma4UnifiedTextAttention``), +replacing the per-layer RMSNorm + RoPE + transpose sequence with fused Triton +kernels. + +The math is identical to standard Gemma 4 (q_norm/k_norm with scale + RoPE, +v_norm without scale, no RoPE), so the same kernels are reused. Like standard +Gemma 4 on transformers 5.10.x, the unified attention keys ``shared_kv_states`` +by **layer type string** (``self.layer_type``); the separate module is needed +only because the unified backbone redefines its classes in its own namespace +(it does not modular-import from ``modeling_gemma4``). + +Usage: + from axolotl.monkeypatch.models.gemma4_unified.fused_attn import ( + patch_gemma4_unified_fused_attn, + ) + # Pass install_shared_kv_workaround=True when activation checkpointing is enabled. + patch_gemma4_unified_fused_attn(install_shared_kv_workaround=True) +""" + +from typing import Callable + +import torch + +from axolotl.utils.logging import get_logger + +logger = get_logger(__name__) + +# Module-level dict used as a side channel for shared KV states avoiding kwarg +# and TLS to prevent a memory leak under gradient checkpointing (PR #3611). +# Kept separate from the standard-gemma4 store so the two never alias. +_GEMMA4_UNIFIED_SHARED_KV_STORE: dict = {"store": None} + + +def _set_shared_kv_states(store): + _GEMMA4_UNIFIED_SHARED_KV_STORE["store"] = store + + +def _get_shared_kv_states(): + return _GEMMA4_UNIFIED_SHARED_KV_STORE["store"] + + +def _make_fused_forward(original_forward): + """Create a patched forward that uses fused RMSNorm+RoPE kernels.""" + + from axolotl.kernels.gemma4_fused_rope import ( + fused_rms_norm_noscale, + fused_rms_norm_rope, + ) + + def fused_forward( + self, + hidden_states: torch.Tensor, + position_embeddings: torch.Tensor, + attention_mask: torch.Tensor | None, + shared_kv_states: dict[str, tuple[torch.Tensor, torch.Tensor]] | None = None, + past_key_values=None, + **kwargs, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + from transformers.models.gemma4_unified.modeling_gemma4_unified import ( + eager_attention_forward, + ) + + store = _get_shared_kv_states() + if store is not None: + shared_kv_states = store + + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + eps = self.config.rms_norm_eps + + cos, sin = position_embeddings + + # ---- Projections ---- + # Use apply_qkv if present (LoRA kernel patch), otherwise direct proj. + has_lora_qkv = hasattr(self, "apply_qkv") + + if has_lora_qkv: + query_states, key_states, value_states = self.apply_qkv(hidden_states) + query_states = query_states.view(hidden_shape) + else: + query_states = self.q_proj(hidden_states).view(hidden_shape) + + # ---- Q path: fused q_norm + RoPE ---- + query_states = fused_rms_norm_rope( + query_states, + self.q_norm.weight, + cos, + sin, + eps=eps, + ) + query_states = query_states.transpose(1, 2) + + # ---- K/V path ---- + if self.is_kv_shared_layer: + # Unified keys the shared cache by layer-type string, not index. + key_states, value_states = shared_kv_states[self.layer_type] + key_states = key_states.to(query_states.device) + value_states = value_states.to(query_states.device) + else: + if has_lora_qkv: + # apply_qkv already computed k/v projections. + key_states = key_states.view(hidden_shape) + value_states = ( + value_states.view(hidden_shape) + if self.v_proj is not None + else key_states + ) + else: + key_states = self.k_proj(hidden_states).view(hidden_shape) + # attention_k_eq_v: value reuses the (pre-norm) k_proj output. + value_states = ( + self.v_proj(hidden_states).view(hidden_shape) + if self.v_proj is not None + else key_states + ) + + # Fused k_norm + RoPE (creates a new tensor, leaving value_states + # pointing at the pre-norm k_proj output for the k_eq_v case). + key_states = fused_rms_norm_rope( + key_states, + self.k_norm.weight, + cos, + sin, + eps=eps, + ) + key_states = key_states.transpose(1, 2) + + # Fused v_norm (no scale, no RoPE). + value_states = fused_rms_norm_noscale(value_states, eps=eps) + value_states = value_states.transpose(1, 2) + + if past_key_values is not None and not self.is_kv_shared_layer: + key_states, value_states = past_key_values.update( + key_states, value_states, self.layer_idx + ) + if self.store_full_length_kv: + shared_kv_states[self.layer_type] = key_states, value_states + + attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface( + self.config._attn_implementation, eager_attention_forward + ) + + attn_output, attn_weights = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=self.attention_dropout if self.training else 0.0, + scaling=self.scaling, + sliding_window=self.sliding_window, + **kwargs, + ) + + attn_output = attn_output.reshape(*input_shape, -1).contiguous() + # Use apply_o if present (LoRA O kernel patch), otherwise direct proj. + if hasattr(self, "apply_o"): + attn_output = self.apply_o(attn_output) + else: + attn_output = self.o_proj(attn_output) + return attn_output, attn_weights + + return fused_forward + + +def _patch_decoder_layer_call(): + """Strip ``shared_kv_states`` from decoder-layer kwargs and route via the + module-level side channel so the checkpoint partial cannot pin it (PR #3611). + """ + from transformers.models.gemma4_unified.modeling_gemma4_unified import ( + Gemma4UnifiedTextDecoderLayer, + ) + + if getattr(Gemma4UnifiedTextDecoderLayer, "_axolotl_shared_kv_patched", False): + return + + original_call = Gemma4UnifiedTextDecoderLayer.__call__ + + def patched_call(self, *args, **kwargs): + shared_kv = kwargs.pop("shared_kv_states", None) + # Overwrite unconditionally (including with None) so a previous step's + # dict cannot leak into a later call without shared_kv_states (PR #3611). + _set_shared_kv_states(shared_kv) + return original_call(self, *args, **kwargs) + + Gemma4UnifiedTextDecoderLayer.__call__ = patched_call + Gemma4UnifiedTextDecoderLayer._axolotl_shared_kv_patched = True + + +def patch_gemma4_unified_fused_attn(install_shared_kv_workaround: bool = False): + """ + Monkeypatch ``Gemma4UnifiedTextAttention.forward`` to use fused RMSNorm+RoPE + kernels, and optionally route ``shared_kv_states`` via a module-level side + channel to avoid a VRAM leak under activation checkpointing (PR #3611). + """ + from transformers.models.gemma4_unified.modeling_gemma4_unified import ( + Gemma4UnifiedTextAttention, + ) + + if getattr(Gemma4UnifiedTextAttention, "_axolotl_fused_attn_patched", False): + return + + original_forward = Gemma4UnifiedTextAttention.forward + Gemma4UnifiedTextAttention.forward = _make_fused_forward(original_forward) + Gemma4UnifiedTextAttention._axolotl_fused_attn_patched = True + + if install_shared_kv_workaround: + _patch_decoder_layer_call() + + logger.info( + "Patched Gemma4UnifiedTextAttention.forward with fused RMSNorm+RoPE " + "Triton kernels" + ) + if install_shared_kv_workaround: + logger.info("Installed Gemma4Unified shared_kv_states side channel (PR #3611)") diff --git a/src/axolotl/monkeypatch/trainer/trl_vllm.py b/src/axolotl/monkeypatch/trainer/trl_vllm.py index a234bbf3c5..886f1933f0 100644 --- a/src/axolotl/monkeypatch/trainer/trl_vllm.py +++ b/src/axolotl/monkeypatch/trainer/trl_vllm.py @@ -7,14 +7,15 @@ - split_tensor_dict / shuffle_sequence_dict: scalar type handling (int/float/bool passthrough) """ -import logging import math from functools import wraps import torch from torch import nn -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) def _batch_update_named_params( diff --git a/src/axolotl/monkeypatch/trainer_fsdp_optim.py b/src/axolotl/monkeypatch/trainer_fsdp_optim.py deleted file mode 100644 index 692f754d7c..0000000000 --- a/src/axolotl/monkeypatch/trainer_fsdp_optim.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -fix for FSDP optimizer save in trainer w 4.47.0 -""" - -import inspect - -from transformers import Trainer - -from axolotl.monkeypatch.utils import detab_code -from axolotl.utils.logging import get_logger - -LOG = get_logger(__name__) - -ORIGINAL_TRAINER_CODE = """ - if delay_optimizer_creation: - self.optimizer = self.accelerator.prepare(self.optimizer) -""" - -PATCHED_TRAINER_CODE = """ - if delay_optimizer_creation: - model = self.accelerator.prepare(self.model) -""" - - -def get_training_loop_code() -> str: - training_loop = inspect.getsource(Trainer._inner_training_loop) - return training_loop - - -def check_training_loop_is_patchable() -> bool: - training_loop = get_training_loop_code() - training_loop, _ = detab_code(training_loop) - return ORIGINAL_TRAINER_CODE in training_loop - - -def patch_training_loop_for_fsdp(): - """ - monkeypatch for fixing the training loop for fsdp with optimizer save - """ - - try: - training_loop = get_training_loop_code() - except OSError: - return - Trainer._original_inner_training_loop = training_loop - training_loop, _ = detab_code(training_loop) - if ORIGINAL_TRAINER_CODE not in training_loop: - return - - training_loop = training_loop.replace(ORIGINAL_TRAINER_CODE, PATCHED_TRAINER_CODE) - training_loop = training_loop.replace( - "def _inner_training_loop(", - "def _fixed_inner_training_loop(", - 1, - ) - - # load imports necessary - import transformers.trainer - - items_to_import = [] - for item in dir(transformers.trainer): - if item in training_loop: - items_to_import.append(item) - - exec( - "from transformers.trainer import (" - + ", ".join(x for x in items_to_import) - + ")", - globals(), - ) - exec(training_loop, globals()) - LOG.info("patching _inner_training_loop for fsdp optimizer save") - Trainer._inner_training_loop = _fixed_inner_training_loop diff --git a/src/axolotl/processing_strategies.py b/src/axolotl/processing_strategies.py index 677e21d7f3..09d97ccad2 100644 --- a/src/axolotl/processing_strategies.py +++ b/src/axolotl/processing_strategies.py @@ -1027,6 +1027,18 @@ def process_labels(self, input_ids): return labels +class Gemma4UnifiedProcessingStrategy(Gemma4ProcessingStrategy): + """Processing Strategy for Gemma 4 Unified (encoder-free image/audio/video). + + The unified checkpoint shares Gemma 4's turn format and the same media + placeholder/delimiter token set (image/audio/video, boi/eoi/boa/eoa), so + boundary detection and label masking are inherited unchanged — both resolve + ids dynamically from the processor/tokenizer rather than hard-coding them. + The encoder-free raw pixel/waveform projection is handled entirely by the HF + processor, so the strategy itself needs no audio/vision-specific logic. + """ + + class Llama3_2VisionProcessingStrategy(ProcessingStrategy): """Processing Strategy class for Llama-3.2 Vision (``<|start_header_id|>{role}<|end_header_id|>\\n\\n ... <|eot_id|>``).""" @@ -1456,6 +1468,8 @@ def get_processing_strategy( return Gemma3nProcessingStrategy(**processing_kwargs) if chat_template_type == "gemma4": return Gemma4ProcessingStrategy(**processing_kwargs) + if chat_template_type == "gemma4_unified": + return Gemma4UnifiedProcessingStrategy(**processing_kwargs) if chat_template_type == "llama3_2_vision": return Llama3_2VisionProcessingStrategy(**processing_kwargs) if chat_template_type == "llama4": diff --git a/src/axolotl/scripts/process_cleanup.py b/src/axolotl/scripts/process_cleanup.py index 56f2943753..cf06d06970 100644 --- a/src/axolotl/scripts/process_cleanup.py +++ b/src/axolotl/scripts/process_cleanup.py @@ -26,12 +26,13 @@ import asyncio import atexit -import logging import os from multiprocessing import Process from multiprocessing.connection import Connection -logger = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +logger = get_logger(__name__) def kill_process_tree(pid: int) -> None: diff --git a/src/axolotl/scripts/vllm_serve_lora.py b/src/axolotl/scripts/vllm_serve_lora.py index ca2f743fcf..67e710163d 100644 --- a/src/axolotl/scripts/vllm_serve_lora.py +++ b/src/axolotl/scripts/vllm_serve_lora.py @@ -13,7 +13,6 @@ - No NCCL communicator needed for weight sync """ -import logging import os from contextlib import asynccontextmanager from dataclasses import dataclass, field @@ -43,8 +42,9 @@ is_fatal_worker_error, safe_recv, ) +from axolotl.utils.logging import get_logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__) @dataclass diff --git a/src/axolotl/scripts/vllm_worker_ext.py b/src/axolotl/scripts/vllm_worker_ext.py index 11f8e6ceb5..61ba32246a 100644 --- a/src/axolotl/scripts/vllm_worker_ext.py +++ b/src/axolotl/scripts/vllm_worker_ext.py @@ -7,8 +7,6 @@ including LoRA-wrapped models where vLLM inserts base_layer into the hierarchy """ -import logging - import torch try: @@ -18,7 +16,9 @@ from trl.scripts.vllm_serve import WeightSyncWorkerExtension -logger = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +logger = get_logger(__name__) # Stacked param name mapping: shard_name -> (packed_name, shard_order) _STACKED_PARAMS = { diff --git a/src/axolotl/telemetry/callbacks.py b/src/axolotl/telemetry/callbacks.py index 1c13bf0cd2..4f06fc75fe 100644 --- a/src/axolotl/telemetry/callbacks.py +++ b/src/axolotl/telemetry/callbacks.py @@ -1,6 +1,5 @@ """Trainer callbacks for reporting runtime metrics at regular intervals.""" -import logging import time from transformers import ( @@ -12,8 +11,9 @@ from axolotl.telemetry.manager import TelemetryManager from axolotl.telemetry.runtime_metrics import RuntimeMetricsTracker +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) TIME_SINCE_LAST = 60 diff --git a/src/axolotl/telemetry/errors.py b/src/axolotl/telemetry/errors.py index a0c8682355..a39750bc12 100644 --- a/src/axolotl/telemetry/errors.py +++ b/src/axolotl/telemetry/errors.py @@ -1,6 +1,5 @@ """Telemetry utilities for exception and traceback information.""" -import logging import os import re import traceback @@ -9,8 +8,9 @@ from typing import Any, Callable from axolotl.telemetry.manager import TelemetryManager +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) ERROR_HANDLED = False diff --git a/src/axolotl/telemetry/manager.py b/src/axolotl/telemetry/manager.py index 5a09309d3c..3fdcd9d935 100644 --- a/src/axolotl/telemetry/manager.py +++ b/src/axolotl/telemetry/manager.py @@ -2,7 +2,6 @@ import atexit import importlib -import logging import os import platform import uuid @@ -14,7 +13,9 @@ import torch import yaml -LOG = logging.getLogger(__name__) +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) POSTHOG_HOST = "https://app.posthog.com" POSTHOG_WRITE_KEY = "phc_1kUR0o04oJKKTTeSsIz2Mfm5mpiVsQEf2WOlzljMD7y" diff --git a/src/axolotl/telemetry/runtime_metrics.py b/src/axolotl/telemetry/runtime_metrics.py index fa83c00a73..5272f05de0 100644 --- a/src/axolotl/telemetry/runtime_metrics.py +++ b/src/axolotl/telemetry/runtime_metrics.py @@ -1,6 +1,5 @@ """Telemetry utilities for runtime and memory metrics.""" -import logging import time from dataclasses import dataclass, field from typing import Any @@ -9,8 +8,9 @@ import torch from axolotl.telemetry.manager import TelemetryManager +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) @dataclass diff --git a/src/axolotl/utils/chat_templates/templates/gemma4_unified.jinja b/src/axolotl/utils/chat_templates/templates/gemma4_unified.jinja new file mode 100644 index 0000000000..4c79e38276 --- /dev/null +++ b/src/axolotl/utils/chat_templates/templates/gemma4_unified.jinja @@ -0,0 +1,363 @@ +{%- macro format_parameters(properties, required, filter_keys=false) -%} + {%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%} + {%- set ns = namespace(found_first=false) -%} + {%- for key, value in properties | dictsort -%} + {%- set add_comma = false -%} + {%- if not filter_keys or key not in standard_keys -%} + {%- if ns.found_first %},{% endif -%} + {%- set ns.found_first = true -%} + {{ key }}:{ + {%- if value['description'] -%} + description:<|"|>{{ value['description'] }}<|"|> + {%- set add_comma = true -%} + {%- endif -%} + {%- if value['type'] | upper == 'STRING' -%} + {%- if value['enum'] -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + enum:{{ format_argument(value['enum']) }} + {%- endif -%} + {%- elif value['type'] | upper == 'ARRAY' -%} + {%- if value['items'] is mapping and value['items'] -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + items:{ + {%- set ns_items = namespace(found_first=false) -%} + {%- for item_key, item_value in value['items'] | dictsort -%} + {%- if item_value is not none -%} + {%- if ns_items.found_first %},{% endif -%} + {%- set ns_items.found_first = true -%} + {%- if item_key == 'properties' -%} + properties:{ + {%- if item_value is mapping -%} + {{- format_parameters(item_value, value['items']['required'] | default([])) -}} + {%- endif -%} + } + {%- elif item_key == 'required' -%} + required:[ + {%- for req_item in item_value -%} + <|"|>{{- req_item -}}<|"|> + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + ] + {%- elif item_key == 'type' -%} + {%- if item_value is string -%} + type:{{ format_argument(item_value | upper) }} + {%- else -%} + type:{{ format_argument(item_value | map('upper') | list) }} + {%- endif -%} + {%- else -%} + {{ item_key }}:{{ format_argument(item_value) }} + {%- endif -%} + {%- endif -%} + {%- endfor -%} + } + {%- endif -%} + {%- endif -%} + {%- if value['nullable'] %} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + nullable:true + {%- endif -%} + {%- if value['type'] | upper == 'OBJECT' -%} + {%- if value['properties'] is defined and value['properties'] is mapping -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + properties:{ + {{- format_parameters(value['properties'], value['required'] | default([])) -}} + } + {%- elif value is mapping -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + properties:{ + {{- format_parameters(value, value['required'] | default([]), filter_keys=true) -}} + } + {%- endif -%} + {%- if value['required'] -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + required:[ + {%- for item in value['required'] | default([]) -%} + <|"|>{{- item -}}<|"|> + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + ] + {%- endif -%} + {%- endif -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + type:<|"|>{{ value['type'] | upper }}<|"|>} + {%- endif -%} + {%- endfor -%} +{%- endmacro -%} +{%- macro format_function_declaration(tool_data) -%} + declaration:{{- tool_data['function']['name'] -}}{description:<|"|>{{- tool_data['function']['description'] -}}<|"|> + {%- set params = tool_data['function']['parameters'] -%} + {%- if params -%} + ,parameters:{ + {%- if params['properties'] -%} + properties:{ {{- format_parameters(params['properties'], params['required']) -}} }, + {%- endif -%} + {%- if params['required'] -%} + required:[ + {%- for item in params['required'] -%} + <|"|>{{- item -}}<|"|> + {{- ',' if not loop.last -}} + {%- endfor -%} + ], + {%- endif -%} + {%- if params['type'] -%} + type:<|"|>{{- params['type'] | upper -}}<|"|>} + {%- endif -%} + {%- endif -%} + {%- if 'response' in tool_data['function'] -%} + {%- set response_declaration = tool_data['function']['response'] -%} + ,response:{ + {%- if response_declaration['description'] -%} + description:<|"|>{{- response_declaration['description'] -}}<|"|>, + {%- endif -%} + {%- if response_declaration['type'] | upper == 'OBJECT' -%} + type:<|"|>{{- response_declaration['type'] | upper -}}<|"|>} + {%- endif -%} + {%- endif -%} + } +{%- endmacro -%} +{%- macro format_argument(argument, escape_keys=True) -%} + {%- if argument is string -%} + {{- '<|"|>' + argument + '<|"|>' -}} + {%- elif argument is boolean -%} + {{- 'true' if argument else 'false' -}} + {%- elif argument is mapping -%} + {{- '{' -}} + {%- set ns = namespace(found_first=false) -%} + {%- for key, value in argument | dictsort -%} + {%- if ns.found_first %},{% endif -%} + {%- set ns.found_first = true -%} + {%- if escape_keys -%} + {{- '<|"|>' + key + '<|"|>' -}} + {%- else -%} + {{- key -}} + {%- endif -%} + :{{- format_argument(value, escape_keys=escape_keys) -}} + {%- endfor -%} + {{- '}' -}} + {%- elif argument is sequence -%} + {{- '[' -}} + {%- for item in argument -%} + {{- format_argument(item, escape_keys=escape_keys) -}} + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + {{- ']' -}} + {%- else -%} + {{- argument -}} + {%- endif -%} +{%- endmacro -%} +{%- macro strip_thinking(text) -%} + {%- set ns = namespace(result='') -%} + {%- for part in text.split('') -%} + {%- if '<|channel>' in part -%} + {%- set ns.result = ns.result + part.split('<|channel>')[0] -%} + {%- else -%} + {%- set ns.result = ns.result + part -%} + {%- endif -%} + {%- endfor -%} + {{- ns.result | trim -}} +{%- endmacro -%} + +{%- macro format_tool_response_block(tool_name, response) -%} + {{- '<|tool_response>' -}} + {%- if response is mapping -%} + {{- 'response:' + tool_name + '{' -}} + {%- for key, value in response | dictsort -%} + {{- key -}}:{{- format_argument(value, escape_keys=False) -}} + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + {{- '}' -}} + {%- else -%} + {{- 'response:' + tool_name + '{value:' + format_argument(response, escape_keys=False) + '}' -}} + {%- endif -%} + {{- '' -}} +{%- endmacro -%} + +{%- set ns = namespace(prev_message_type=None) -%} +{%- set loop_messages = messages -%} +{{- bos_token -}} +{#- Handle System/Tool Definitions Block -#} +{%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%} + {{- '<|turn>system\n' -}} + {#- Inject Thinking token at the very top of the FIRST system turn -#} + {%- if enable_thinking is defined and enable_thinking -%} + {{- '<|think|>\n' -}} + {%- set ns.prev_message_type = 'think' -%} + {%- endif -%} + {%- if messages[0]['role'] in ['system', 'developer'] -%} + {%- if messages[0]['content'] is string -%} + {{- messages[0]['content'] | trim -}} + {%- elif messages[0]['content'] is sequence -%} + {%- for item in messages[0]['content'] -%} + {{- item['text'] | trim + ' '-}} + {%- endfor -%} + {%- endif -%} + {%- set loop_messages = messages[1:] -%} + {%- endif -%} + {%- if tools -%} + {%- for tool in tools %} + {{- '<|tool>' -}} + {{- format_function_declaration(tool) | trim -}} + {{- '' -}} + {%- endfor %} + {%- set ns.prev_message_type = 'tool' -%} + {%- endif -%} + {{- '\n' -}} +{%- endif %} + +{#- Pre-scan: find last user message index for reasoning guard -#} +{%- set ns_turn = namespace(last_user_idx=-1) -%} +{%- for i in range(loop_messages | length) -%} + {%- if loop_messages[i]['role'] == 'user' -%} + {%- set ns_turn.last_user_idx = i -%} + {%- endif -%} +{%- endfor -%} + +{#- Loop through messages -#} +{%- for message in loop_messages -%} +{%- if message['role'] != 'tool' -%} +{%- set ns.prev_message_type = None -%} +{%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%} +{#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#} +{%- set prev_nt = namespace(role=None, found=false) -%} +{%- if loop.index0 > 0 -%} + {%- for j in range(loop.index0 - 1, -1, -1) -%} + {%- if not prev_nt.found -%} + {%- if loop_messages[j]['role'] != 'tool' -%} + {%- set prev_nt.role = loop_messages[j]['role'] -%} + {%- set prev_nt.found = true -%} + {%- endif -%} + {%- endif -%} + {%- endfor -%} +{%- endif -%} +{%- set continue_same_model_turn = (role == 'model' and prev_nt.role == 'assistant') -%} +{%- if not continue_same_model_turn -%} + {{- '<|turn>' + role + '\n' }} +{%- endif -%} + +{#- Render reasoning/reasoning_content as thinking channel -#} +{%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%} +{%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%} + {{- '<|channel>thought\n' + thinking_text + '\n' -}} +{%- endif -%} + +{%- if message['tool_calls'] -%} + {%- for tool_call in message['tool_calls'] -%} + {%- set function = tool_call['function'] -%} + {{- '<|tool_call>call:' + function['name'] + '{' -}} + {%- if function['arguments'] is mapping -%} + {%- set ns_args = namespace(found_first=false) -%} + {%- for key, value in function['arguments'] | dictsort -%} + {%- if ns_args.found_first %},{% endif -%} + {%- set ns_args.found_first = true -%} + {{- key -}}:{{- format_argument(value, escape_keys=False) -}} + {%- endfor -%} + {%- elif function['arguments'] is string -%} + {{- function['arguments'] -}} + {%- endif -%} + {{- '}' -}} + {%- endfor -%} + {%- set ns.prev_message_type = 'tool_call' -%} +{%- endif -%} + +{%- set ns_tr_out = namespace(flag=false) -%} +{%- if message.get('tool_responses') -%} + {#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#} + {%- for tool_response in message['tool_responses'] -%} + {{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}} + {%- set ns_tr_out.flag = true -%} + {%- set ns.prev_message_type = 'tool_response' -%} + {%- endfor -%} +{%- elif message.get('tool_calls') -%} + {#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#} + {%- set ns_tool_scan = namespace(stopped=false) -%} + {%- for k in range(loop.index0 + 1, loop_messages | length) -%} + {%- if ns_tool_scan.stopped -%} + {%- elif loop_messages[k]['role'] != 'tool' -%} + {%- set ns_tool_scan.stopped = true -%} + {%- else -%} + {%- set follow = loop_messages[k] -%} + {#- Resolve tool_call_id to function name -#} + {%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%} + {%- for tc in message['tool_calls'] -%} + {%- if tc.get('id') == follow.get('tool_call_id') -%} + {%- set ns_tname.name = tc['function']['name'] -%} + {%- endif -%} + {%- endfor -%} + {#- Handle content as string or content-parts array -#} + {%- set tool_body = follow.get('content') -%} + {%- if tool_body is string -%} + {{- format_tool_response_block(ns_tname.name, tool_body) -}} + {%- elif tool_body is sequence and tool_body is not string -%} + {%- set ns_txt = namespace(s='') -%} + {%- for part in tool_body -%} + {%- if part.get('type') == 'text' -%} + {%- set ns_txt.s = ns_txt.s + (part.get('text') | default('')) -%} + {%- endif -%} + {%- endfor -%} + {{- format_tool_response_block(ns_tname.name, ns_txt.s) -}} + {%- for part in tool_body -%} + {%- if part.get('type') == 'image' -%} + {{- '<|image|>' -}} + {%- elif part.get('type') == 'audio' -%} + {{- '<|audio|>' -}} + {%- elif part.get('type') == 'video' -%} + {{- '<|video|>' -}} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {{- format_tool_response_block(ns_tname.name, tool_body) -}} + {%- endif -%} + {%- set ns_tr_out.flag = true -%} + {%- set ns.prev_message_type = 'tool_response' -%} + {%- endif -%} + {%- endfor -%} +{%- endif -%} + +{%- set captured_content -%} +{%- if message['content'] is string -%} + {%- if role == 'model' -%} + {{- strip_thinking(message['content']) -}} + {%- else -%} + {{- message['content'] | trim -}} + {%- endif -%} +{%- elif message['content'] is sequence -%} + {%- for item in message['content'] -%} + {%- if item['type'] == 'text' -%} + {%- if role == 'model' -%} + {{- strip_thinking(item['text']) -}} + {%- else -%} + {{- item['text'] | trim -}} + {%- endif -%} + {%- elif item['type'] == 'image' -%} + {{- '<|image|>' -}} + {%- set ns.prev_message_type = 'image' -%} + {%- elif item['type'] == 'audio' -%} + {{- '<|audio|>' -}} + {%- set ns.prev_message_type = 'audio' -%} + {%- elif item['type'] == 'video' -%} + {{- '<|video|>' -}} + {%- set ns.prev_message_type = 'video' -%} + {%- endif -%} + {%- endfor -%} +{%- endif -%} +{%- endset -%} + +{{- captured_content -}} +{%- set has_content = captured_content | trim | length > 0 -%} + +{%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%} + {{- '<|tool_response>' -}} +{%- elif not (ns_tr_out.flag and not has_content) -%} + {{- '\n' -}} +{%- endif -%} +{%- endif -%} +{%- endfor -%} + +{%- if add_generation_prompt -%} + {%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%} + {{- '<|turn>model\n' -}} + {%- if not enable_thinking | default(false) -%} + {{- '<|channel>thought\n' -}} + {%- endif -%} + {%- endif -%} +{%- endif -%} diff --git a/src/axolotl/utils/config/__init__.py b/src/axolotl/utils/config/__init__.py index 82dab72d88..9b02024300 100644 --- a/src/axolotl/utils/config/__init__.py +++ b/src/axolotl/utils/config/__init__.py @@ -269,11 +269,12 @@ def normalize_config(cfg): ): cfg.gradient_checkpointing_kwargs = {"use_reentrant": True} - # Gemma4 requires use_reentrant=False for DDP (shared per-layer norms cause - # "marked ready twice" errors with reentrant checkpointing) and - # ddp_find_unused_parameters=True (per_layer_projection LoRA params may not - # receive gradients on every step). - if cfg.model_config_type == "gemma4": + # Gemma4 requires use_reentrant=False for DDP (shared per-layer norms / + # cross-layer shared KV cause "marked ready twice" errors with reentrant + # checkpointing) and ddp_find_unused_parameters=True (per_layer_projection + # LoRA / frozen mm params may not receive gradients on every step). The + # unified variant shares the same constraints. + if cfg.model_config_type in ("gemma4", "gemma4_unified"): if cfg.gradient_checkpointing: if cfg.gradient_checkpointing_kwargs is None: cfg.gradient_checkpointing_kwargs = {} diff --git a/src/axolotl/utils/data/wrappers.py b/src/axolotl/utils/data/wrappers.py index 3a10bde006..30b9935e25 100644 --- a/src/axolotl/utils/data/wrappers.py +++ b/src/axolotl/utils/data/wrappers.py @@ -1,6 +1,5 @@ """Data handling specific to SFT.""" -import logging from typing import Any, NoReturn, cast from datasets import ( @@ -38,8 +37,9 @@ UnsupportedPrompter, ) from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger -LOG = logging.getLogger(__name__) +LOG = get_logger(__name__) def handle_unknown_dataset_strategy(dataset_config: DictDefault) -> NoReturn: diff --git a/src/axolotl/utils/schemas/enums.py b/src/axolotl/utils/schemas/enums.py index d783983c0c..60ceeefcc1 100644 --- a/src/axolotl/utils/schemas/enums.py +++ b/src/axolotl/utils/schemas/enums.py @@ -73,6 +73,7 @@ class ChatTemplate(str, Enum): gemma3 = "gemma3" gemma3n = "gemma3n" gemma4 = "gemma4" + gemma4_unified = "gemma4_unified" command_a = "command_a" command_a_tool_use = "command_a_tool_use" command_a_rag = "command_a_rag" diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 181ddaecbc..10424ddd2d 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1038,11 +1038,12 @@ def check_cross_entropy_conflicts(cls, data): def check_fsdp_version(cls, data): fsdp_config = data.get("fsdp_config", {}) if fsdp_config and str(data.get("fsdp_version")) != "2": - LOG.info( - "FSDP1 will be deprecated in an upcoming release of Axolotl." - "We recommend that you use FSDP version 2 for better performance and compatibility. " - "Please see this link for more details: https://docs.axolotl.ai/docs/multi-gpu.html#sec-fsdp " - "For more details on migrating your config. " + LOG.warning( + "FSDP1 is deprecated and will be removed in an upcoming release of " + "Axolotl (transformers plans to in v5.20). We recommend migrating " + "to fsdp_version: 2 for better performance and compatibility. " + "See https://docs.axolotl.ai/docs/multi-gpu.html#sec-fsdp for " + "details on migrating your config." ) return data @@ -1114,6 +1115,12 @@ def check_fsdp_version_in_fsdp_config(cls, data): data["fsdp_version"] = fsdp_config.get("fsdp_version") if fsdp_version and fsdp_config and not fsdp_config.get("fsdp_version"): data["fsdp_config"]["fsdp_version"] = fsdp_version + if fsdp_config and not data.get("fsdp_version"): + # transformers >= 5.10 defaults a missing version to FSDP2; pin + # axolotl's FSDP1 default explicitly so unversioned configs keep + # their behavior + data["fsdp_version"] = 1 + data["fsdp_config"]["fsdp_version"] = 1 return data @model_validator(mode="after") diff --git a/tests/e2e/multigpu/test_llama.py b/tests/e2e/multigpu/test_llama.py index 432a7e9e5c..283f078b41 100644 --- a/tests/e2e/multigpu/test_llama.py +++ b/tests/e2e/multigpu/test_llama.py @@ -593,6 +593,10 @@ def test_fsdp_qlora_prequant_packed(self, temp_dir): "auto_wrap", ], "fsdp_config": { + # pinned to FSDP1: accelerate's fsdp2_load_full_state_dict + # cannot resolve prequantized bnb quant-state keys + # (e.g. `...weight.absmax`) + "fsdp_version": 1, "fsdp_offload_params": False, "fsdp_sync_module_states": True, "fsdp_use_orig_params": False, diff --git a/tests/loaders/test_patch_manager_gemma4_unified.py b/tests/loaders/test_patch_manager_gemma4_unified.py new file mode 100644 index 0000000000..4a3a6e0de8 --- /dev/null +++ b/tests/loaders/test_patch_manager_gemma4_unified.py @@ -0,0 +1,129 @@ +"""Tests for ``PatchManager`` fused-attn dispatch over the gemma4 / gemma4_unified +families. These pin the unified-specific routing (namespace branch + auto-on with +no ``fused_attn_kernel`` gate) introduced when the two blocks were consolidated.""" + +import logging +from unittest.mock import MagicMock + +import pytest +import torch + +from axolotl.loaders.patch_manager import PatchManager +from axolotl.utils.dict import DictDefault + + +def _make_patch_manager(model_config_type, *, gradient_checkpointing, inference=False): + """Minimal cfg that reaches the fused-attn dispatch. ``context_parallel_size`` + must be an int (the pre-CUDA span compares ``> 1``) and ``fused_attn_kernel`` + stays falsy to prove the unified path is auto-on without it.""" + cfg = DictDefault( + { + "model_config_type": model_config_type, + "context_parallel_size": 1, + "sample_packing": False, + "gradient_checkpointing": gradient_checkpointing, + "activation_offloading": False, + "fused_attn_kernel": False, + "fsdp_config": None, + } + ) + return PatchManager(cfg, MagicMock(), inference=inference) + + +@pytest.fixture +def spy_fused_patches(monkeypatch): + """Replace the two fused-attn patch entry points (imported inside the + dispatch) with call recorders, and force the CUDA-gated block to run.""" + import axolotl.monkeypatch.models.gemma4.fused_attn as regular_fa + import axolotl.monkeypatch.models.gemma4_unified.fused_attn as unified_fa + + calls = {"unified": [], "regular": []} + monkeypatch.setattr( + unified_fa, + "patch_gemma4_unified_fused_attn", + lambda **kw: calls["unified"].append(kw), + ) + monkeypatch.setattr( + regular_fa, + "patch_gemma4_fused_attn", + lambda **kw: calls["regular"].append(kw), + ) + monkeypatch.setattr(torch.cuda, "is_available", lambda: True) + return calls + + +class TestFusedAttnDispatch: + @pytest.mark.parametrize( + "model_config_type", + ["gemma4_unified", "gemma4_unified_text"], + ) + def test_unified_routes_to_unified_patch_auto_on( + self, spy_fused_patches, model_config_type + ): + """Unified dispatches to the unified namespace and runs even though + ``fused_attn_kernel`` is False (auto-on, like standard gemma4).""" + pm = _make_patch_manager(model_config_type, gradient_checkpointing=True) + pm._apply_model_specific_patches() + + assert spy_fused_patches["unified"] == [{"install_shared_kv_workaround": True}] + assert spy_fused_patches["regular"] == [] + + @pytest.mark.parametrize("model_config_type", ["gemma4", "gemma4_text"]) + def test_standard_gemma4_routes_to_regular_patch( + self, spy_fused_patches, model_config_type + ): + pm = _make_patch_manager(model_config_type, gradient_checkpointing=False) + pm._apply_model_specific_patches() + + assert spy_fused_patches["regular"] == [{"install_shared_kv_workaround": False}] + assert spy_fused_patches["unified"] == [] + + def test_shared_kv_workaround_off_without_checkpointing(self, spy_fused_patches): + pm = _make_patch_manager("gemma4_unified", gradient_checkpointing=False) + pm._apply_model_specific_patches() + + assert spy_fused_patches["unified"] == [{"install_shared_kv_workaround": False}] + + def test_shared_kv_workaround_off_during_inference(self, spy_fused_patches): + pm = _make_patch_manager( + "gemma4_unified", gradient_checkpointing=True, inference=True + ) + pm._apply_model_specific_patches() + + assert spy_fused_patches["unified"] == [{"install_shared_kv_workaround": False}] + + +class TestWarnIfFusedAttnUnsupported: + """``_FUSED_ATTN_KERNEL_SUPPORTED`` must include the unified model types so + ``fused_attn_kernel: true`` does not warn (it is not a no-op for them).""" + + def _capture(self, caplog, cfg): + logger = logging.getLogger("axolotl.loaders.patch_manager") + logger.addHandler(caplog.handler) + previous_level = logger.level + logger.setLevel(logging.WARNING) + try: + PatchManager._warn_if_fused_attn_unsupported(cfg) + finally: + logger.removeHandler(caplog.handler) + logger.setLevel(previous_level) + + @pytest.mark.parametrize( + "model_config_type", + ["gemma4_unified", "gemma4_unified_text"], + ) + def test_no_warn_for_unified_types(self, caplog, model_config_type): + self._capture( + caplog, + DictDefault( + {"fused_attn_kernel": True, "model_config_type": model_config_type} + ), + ) + assert caplog.text == "", f"unexpected warning: {caplog.text}" + + def test_warns_for_unsupported_type(self, caplog): + self._capture( + caplog, + DictDefault({"fused_attn_kernel": True, "model_config_type": "llama"}), + ) + assert "llama" in caplog.text and "no-op" in caplog.text diff --git a/tests/monkeypatch/test_gemma4_hybrid_mask.py b/tests/monkeypatch/test_gemma4_hybrid_mask.py index 66d56bcf13..ac73e34778 100644 --- a/tests/monkeypatch/test_gemma4_hybrid_mask.py +++ b/tests/monkeypatch/test_gemma4_hybrid_mask.py @@ -341,3 +341,40 @@ def test_patched_create_causal_mask_returns_4d_for_real_config( # Caller's config must be untouched — other code paths still read it. assert cfg._attn_implementation == "flash_attention_2" + + +@pytest.fixture +def restore_gemma4_hybrid_mask_both(): + """Snapshot ``create_causal_mask`` in BOTH the gemma4 and gemma4_unified + namespaces (``patch_gemma4_hybrid_mask`` wraps each independently) and reset + the module flag so the patch re-installs cleanly.""" + modeling_unified = pytest.importorskip( + "transformers.models.gemma4_unified.modeling_gemma4_unified", + reason="unified namespace coverage requires gemma4_unified", + ) + from transformers.models.gemma4 import modeling_gemma4 + + from axolotl.monkeypatch import gemma4_hybrid_mask + + saved_gemma4 = modeling_gemma4.create_causal_mask + saved_unified = modeling_unified.create_causal_mask + gemma4_hybrid_mask._PATCH_APPLIED = False + try: + yield modeling_unified + finally: + modeling_gemma4.create_causal_mask = saved_gemma4 + modeling_unified.create_causal_mask = saved_unified + gemma4_hybrid_mask._PATCH_APPLIED = False + + +def test_patch_covers_unified_namespace(restore_gemma4_hybrid_mask_both): + """The unified backbone redefines ``create_causal_mask`` in its own module, + so the patch must wrap it too — not just ``modeling_gemma4``.""" + modeling_unified = restore_gemma4_hybrid_mask_both + from axolotl.monkeypatch.gemma4_hybrid_mask import patch_gemma4_hybrid_mask + + original = modeling_unified.create_causal_mask + assert patch_gemma4_hybrid_mask() is True + + assert modeling_unified.create_causal_mask is not original + assert modeling_unified.create_causal_mask._axolotl_original is original diff --git a/tests/monkeypatch/test_gemma4_kernelize.py b/tests/monkeypatch/test_gemma4_kernelize.py deleted file mode 100644 index ee8989fa2c..0000000000 --- a/tests/monkeypatch/test_gemma4_kernelize.py +++ /dev/null @@ -1,196 +0,0 @@ -"""Tests for the Gemma 4 ``kernelize()`` / ``use_kernels`` crash fix. - -transformers decorates ``Gemma4VisionAttention`` with -``@use_kernelized_func(apply_rotary_pos_emb)`` where the target is a plain -function. Under ``use_kernels=True``, ``model.kernelize()`` then tries to -``register_module()`` that function and crashes with:: - - TypeError: ...apply_rotary_pos_emb is not a Module subclass - -(and a follow-on ``AttributeError`` from the cleanup path). The patch strips the -dead non-Module ``_hidden_kernels`` entry so ``kernelize()`` succeeds. The entry -is never read by ``Gemma4VisionAttention.forward`` (which uses -``apply_multidimensional_rope``), so removing it is behavior-neutral. -""" - -import pytest - -pytest.importorskip( - "transformers.models.gemma4", - reason="gemma4_kernelize patch only matters when Gemma 4 is available", -) - - -@pytest.fixture -def restore_gemma4_vision_attention(): - """Snapshot ``Gemma4VisionAttention.__init__`` and reset patch state after - each test so patch state doesn't leak across the suite.""" - from transformers.models.gemma4 import modeling_gemma4 - - saved_init = modeling_gemma4.Gemma4VisionAttention.__init__ - yield modeling_gemma4 - modeling_gemma4.Gemma4VisionAttention.__init__ = saved_init - from axolotl.monkeypatch import gemma4_kernelize - - gemma4_kernelize._PATCH_APPLIED = False - - -def _vision_config(): - from transformers.models.gemma4.configuration_gemma4 import Gemma4VisionConfig - - return Gemma4VisionConfig( - hidden_size=32, - intermediate_size=64, - num_hidden_layers=2, - num_attention_heads=4, - num_key_value_heads=2, - head_dim=8, - ) - - -def test_patch_installs_and_is_idempotent(restore_gemma4_vision_attention): - from axolotl.monkeypatch.gemma4_kernelize import patch_gemma4_kernelize - - assert patch_gemma4_kernelize() is True - init_first = restore_gemma4_vision_attention.Gemma4VisionAttention.__init__ - # Second call must not re-wrap. - assert patch_gemma4_kernelize() is True - init_second = restore_gemma4_vision_attention.Gemma4VisionAttention.__init__ - assert init_first is init_second - assert hasattr(init_first, "_axolotl_original") - - -def test_patch_strips_non_module_hidden_kernels(restore_gemma4_vision_attention): - modeling_gemma4 = restore_gemma4_vision_attention - from axolotl.monkeypatch.gemma4_kernelize import patch_gemma4_kernelize - - cfg = _vision_config() - - # Before the patch, the bare function is registered (the crash source). - attn_before = modeling_gemma4.Gemma4VisionAttention(cfg, layer_idx=0) - assert "apply_rotary_pos_emb" in getattr(attn_before, "_hidden_kernels", {}) - - patch_gemma4_kernelize() - attn_after = modeling_gemma4.Gemma4VisionAttention(cfg, layer_idx=0) - assert dict(getattr(attn_after, "_hidden_kernels", {})) == {} - - -def test_register_module_path_no_longer_crashes(restore_gemma4_vision_attention): - """The exact step that crashed: kernelize()'s ``attach_hidden_kernels`` - does ``register_module(name, fn)`` for each ``_hidden_kernels`` entry.""" - modeling_gemma4 = restore_gemma4_vision_attention - from axolotl.monkeypatch.gemma4_kernelize import patch_gemma4_kernelize - - cfg = _vision_config() - patch_gemma4_kernelize() - attn = modeling_gemma4.Gemma4VisionAttention(cfg, layer_idx=0) - - # Replicate attach_hidden_kernels; with the entry stripped there is nothing - # to (mis)register, so this must not raise. - for name, fn in getattr(attn, "_hidden_kernels", {}).items(): - if name not in dict(attn.named_children()): - attn.register_module(name, fn) - - -def test_patch_does_not_alter_weights(restore_gemma4_vision_attention): - """The shim only mutates ``_hidden_kernels``; parameters are untouched.""" - import torch - - modeling_gemma4 = restore_gemma4_vision_attention - from axolotl.monkeypatch.gemma4_kernelize import patch_gemma4_kernelize - - cfg = _vision_config() - torch.manual_seed(0) - before = modeling_gemma4.Gemma4VisionAttention(cfg, layer_idx=0).state_dict() - - patch_gemma4_kernelize() - torch.manual_seed(0) - after = modeling_gemma4.Gemma4VisionAttention(cfg, layer_idx=0).state_dict() - - assert before.keys() == after.keys() - assert all(torch.equal(before[k], after[k]) for k in before) - - -def test_forward_does_not_reference_stripped_entry(restore_gemma4_vision_attention): - """Behavior-invariance guarantee: forward never reads the stripped names, - so dropping them cannot change the forward result.""" - modeling_gemma4 = restore_gemma4_vision_attention - names = modeling_gemma4.Gemma4VisionAttention.forward.__code__.co_names - assert "apply_rotary_pos_emb" not in names - assert "_hidden_kernels" not in names - - -def test_unpatch_restores_original(restore_gemma4_vision_attention): - modeling_gemma4 = restore_gemma4_vision_attention - from axolotl.monkeypatch.gemma4_kernelize import ( - patch_gemma4_kernelize, - unpatch_gemma4_kernelize, - ) - - original = modeling_gemma4.Gemma4VisionAttention.__init__ - patch_gemma4_kernelize() - assert modeling_gemma4.Gemma4VisionAttention.__init__ is not original - unpatch_gemma4_kernelize() - assert modeling_gemma4.Gemma4VisionAttention.__init__ is original - - -def test_unpatch_is_safe_without_prior_patch(restore_gemma4_vision_attention): - from axolotl.monkeypatch.gemma4_kernelize import unpatch_gemma4_kernelize - - # No-op, no exception. - unpatch_gemma4_kernelize() - - -def test_full_model_kernelize_succeeds_with_patch(restore_gemma4_vision_attention): - """End-to-end: a tiny full Gemma4 model crashes in ``kernelize()`` without - the patch and succeeds with it. No real 26B weights or CUDA required.""" - modeling_gemma4 = restore_gemma4_vision_attention - from transformers.models.gemma4.configuration_gemma4 import ( - Gemma4AudioConfig, - Gemma4Config, - Gemma4TextConfig, - Gemma4VisionConfig, - ) - - from axolotl.monkeypatch.gemma4_kernelize import patch_gemma4_kernelize - - def build(): - text = Gemma4TextConfig( - hidden_size=32, - intermediate_size=64, - num_hidden_layers=2, - num_attention_heads=4, - num_key_value_heads=2, - head_dim=8, - vocab_size=128, - num_experts=4, - num_experts_per_tok=2, - ) - vis = Gemma4VisionConfig( - hidden_size=32, - intermediate_size=64, - num_hidden_layers=2, - num_attention_heads=4, - num_key_value_heads=2, - head_dim=8, - ) - aud = Gemma4AudioConfig( - hidden_size=32, - intermediate_size=64, - num_hidden_layers=1, - num_attention_heads=4, - ) - cfg = Gemma4Config(text_config=text, vision_config=vis, audio_config=aud) - return modeling_gemma4.Gemma4ForConditionalGeneration(cfg) - - # Without the patch, kernelize() crashes. - model = build() - model.train() - with pytest.raises((TypeError, AttributeError, ValueError)): - model.kernelize() - - # With the patch, it succeeds. - patch_gemma4_kernelize() - model = build() - model.train() - model.kernelize() diff --git a/tests/monkeypatch/test_gemma4_unified_fused_attn.py b/tests/monkeypatch/test_gemma4_unified_fused_attn.py new file mode 100644 index 0000000000..a50b03328b --- /dev/null +++ b/tests/monkeypatch/test_gemma4_unified_fused_attn.py @@ -0,0 +1,221 @@ +"""Tests for the Gemma4 *Unified* fused-attention monkeypatch. + +These cover only behavior that diverges from standard gemma4 (whose fused-attn +patch is already covered by ``test_gemma4_fused_attn.py`` / +``test_gemma4_fused_attn_patch.py``): + +* the idempotency guard on ``patch_gemma4_unified_fused_attn`` (standard gemma4 + has none), +* that the side channel routes through the *unified* store/decoder-layer class + (a separate object that standard-gemma4 tests never touch), and +* shared-KV forward/backward under the unified backbone's hardcoded + ``layer_type`` keying (vs the version-tolerant helpers in standard gemma4). +""" + +import pytest +import torch + +gemma4_unified_modeling = pytest.importorskip( + "transformers.models.gemma4_unified.modeling_gemma4_unified", + reason="unified fused_attn patch only matters when gemma4_unified is available", +) + + +@pytest.fixture +def clean_unified_slate(): + """Snapshot/restore ``Gemma4UnifiedTextAttention.forward``, + ``Gemma4UnifiedTextDecoderLayer.__call__`` and both axolotl sentinels so the + patch (and its idempotency flag) can't leak across tests.""" + from axolotl.monkeypatch.models.gemma4_unified import fused_attn + + attn_cls = gemma4_unified_modeling.Gemma4UnifiedTextAttention + decoder_cls = gemma4_unified_modeling.Gemma4UnifiedTextDecoderLayer + original_forward = attn_cls.forward + original_call = decoder_cls.__call__ + had_attn_flag = getattr(attn_cls, "_axolotl_fused_attn_patched", False) + had_dec_flag = getattr(decoder_cls, "_axolotl_shared_kv_patched", False) + + if had_attn_flag: + del attn_cls._axolotl_fused_attn_patched + if had_dec_flag: + del decoder_cls._axolotl_shared_kv_patched + + try: + yield attn_cls, decoder_cls, original_forward, original_call, fused_attn + finally: + attn_cls.forward = original_forward + decoder_cls.__call__ = original_call + for cls, attr, had in ( + (attn_cls, "_axolotl_fused_attn_patched", had_attn_flag), + (decoder_cls, "_axolotl_shared_kv_patched", had_dec_flag), + ): + if had: + setattr(cls, attr, True) + elif hasattr(cls, attr): + delattr(cls, attr) + fused_attn._set_shared_kv_states(None) + + +class TestUnifiedFusedAttnEntryPoint: + def test_patch_is_idempotent(self, clean_unified_slate): + """Unified guards re-patching via ``_axolotl_fused_attn_patched`` — a + second call must not re-wrap forward (standard gemma4 has no such guard).""" + pytest.importorskip("triton") + attn_cls, _decoder_cls, _orig_fwd, _orig_call, fused_attn = clean_unified_slate + + fused_attn.patch_gemma4_unified_fused_attn() + wrapped = attn_cls.forward + assert getattr(attn_cls, "_axolotl_fused_attn_patched", False) is True + + fused_attn.patch_gemma4_unified_fused_attn() + assert attn_cls.forward is wrapped, "second patch call must be a no-op" + + def test_default_flag_swaps_only_attention_forward(self, clean_unified_slate): + pytest.importorskip("triton") + attn_cls, decoder_cls, original_forward, original_call, fused_attn = ( + clean_unified_slate + ) + + fused_attn.patch_gemma4_unified_fused_attn() + + assert attn_cls.forward is not original_forward + assert decoder_cls.__call__ is original_call + assert not getattr(decoder_cls, "_axolotl_shared_kv_patched", False) + + def test_workaround_flag_installs_decoder_layer_patch(self, clean_unified_slate): + pytest.importorskip("triton") + attn_cls, decoder_cls, original_forward, original_call, fused_attn = ( + clean_unified_slate + ) + + fused_attn.patch_gemma4_unified_fused_attn(install_shared_kv_workaround=True) + + assert attn_cls.forward is not original_forward + assert decoder_cls.__call__ is not original_call + assert getattr(decoder_cls, "_axolotl_shared_kv_patched", False) is True + + +class TestUnifiedDecoderLayerCall: + """The PR#3611 side channel uses a *separate* store + decoder-layer class + from standard gemma4; a copy-paste regression (writing to gemma4's store, or + failing to clear) would be caught by nothing in the standard-gemma4 tests.""" + + def test_store_is_distinct_from_standard_gemma4(self): + from axolotl.monkeypatch.models.gemma4 import fused_attn as regular + from axolotl.monkeypatch.models.gemma4_unified import fused_attn as unified + + assert ( + unified._GEMMA4_UNIFIED_SHARED_KV_STORE + is not regular._GEMMA4_SHARED_KV_STORE + ) + + def test_pops_and_clears_shared_kv_states(self, clean_unified_slate): + _attn_cls, decoder_cls, _orig_fwd, _orig_call, fused_attn = clean_unified_slate + + captured = {} + + def spy(self, *args, **kwargs): + captured["args"] = args + captured["kwargs"] = dict(kwargs) + return "spy_return" + + decoder_cls.__call__ = spy + fused_attn._patch_decoder_layer_call() + + assert getattr(decoder_cls, "_axolotl_shared_kv_patched", False) is True + + shared_kv = {"full_attention": ("k", "v")} + result = decoder_cls.__call__( + object(), + "positional_arg", + shared_kv_states=shared_kv, + other_kwarg="keep_me", + ) + + assert result == "spy_return" + assert captured["args"] == ("positional_arg",) + assert "shared_kv_states" not in captured["kwargs"] + assert captured["kwargs"] == {"other_kwarg": "keep_me"} + assert fused_attn._get_shared_kv_states() is shared_kv + + # A later call without the kwarg must clear the store (no cross-step leak). + decoder_cls.__call__(object()) + assert fused_attn._get_shared_kv_states() is None + + +def _build_unified_kv_shared_config(): + """Tiny unified text config with ``num_kv_shared_layers > 0`` so the fused + shared-KV branch (hardcoded ``self.layer_type`` keying) runs.""" + from transformers.models.gemma4_unified.configuration_gemma4_unified import ( + Gemma4UnifiedTextConfig, + ) + + cfg = Gemma4UnifiedTextConfig( + vocab_size=128, + hidden_size=64, + intermediate_size=128, + num_hidden_layers=4, + num_kv_shared_layers=2, + num_attention_heads=2, + num_key_value_heads=2, + head_dim=32, + global_head_dim=64, + layer_types=[ + "sliding_attention", + "full_attention", + "sliding_attention", + "full_attention", + ], + sliding_window=64, + max_position_embeddings=2048, + rope_parameters={ + "sliding_attention": {"rope_type": "default", "rope_theta": 10000.0}, + "full_attention": { + "rope_type": "proportional", + "rope_theta": 1000000.0, + "partial_rotary_factor": 0.25, + }, + }, + ) + cfg._attn_implementation = "sdpa" + return cfg + + +class TestUnifiedFusedAttnSharedKV: + """Guards the unified backbone's hardcoded ``layer_type`` shared-KV keying + (standard gemma4 uses version-tolerant ``_shared_kv_*_key`` helpers).""" + + @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") + def test_shared_kv_forward_backward(self, clean_unified_slate): + _attn_cls, _decoder_cls, _orig_fwd, _orig_call, fused_attn = clean_unified_slate + from transformers.models.gemma4_unified.modeling_gemma4_unified import ( + Gemma4UnifiedTextModel, + ) + + torch.manual_seed(4) + m = ( + Gemma4UnifiedTextModel(_build_unified_kv_shared_config()) + .cuda() + .to(torch.bfloat16) + .train() + ) + assert any(layer.self_attn.is_kv_shared_layer for layer in m.layers), ( + "test config must exercise at least one kv-shared layer" + ) + + ids = torch.randint(0, 128, (2, 16), device="cuda") + mask = torch.ones(2, 16, dtype=torch.long, device="cuda") + + with torch.no_grad(): + ref = m(input_ids=ids, attention_mask=mask).last_hidden_state.clone() + + fused_attn.patch_gemma4_unified_fused_attn() + out = m(input_ids=ids, attention_mask=mask).last_hidden_state + out.sum().backward() + + assert out.shape == ref.shape + assert torch.isfinite(out).all() + cos_sim = torch.nn.functional.cosine_similarity( + ref.flatten().float(), out.detach().flatten().float(), dim=0 + ) + assert cos_sim > 0.999, f"shared-kv fused vs stock cosine_sim={cos_sim:.6f}" diff --git a/tests/monkeypatch/test_kernelize_fixes.py b/tests/monkeypatch/test_kernelize_fixes.py new file mode 100644 index 0000000000..6d72bbc524 --- /dev/null +++ b/tests/monkeypatch/test_kernelize_fixes.py @@ -0,0 +1,227 @@ +"""Tests for the generic ``kernelize()`` repairs in ``kernelize_fixes``. + +Two upstream defects are covered (see the patch module docstring): bare +functions stashed in ``_hidden_kernels`` (gemma4 and ~30 others) and the +gpt-oss rotary ``Func`` whose ``position_ids`` parameter fails the kernels +library's signature check against the hub kernel. +""" + +import inspect + +import pytest + +pytest.importorskip("kernels", reason="kernelize fixes only matter with kernels") + + +@pytest.fixture +def kernelize_patch(): + """Install the patch, restore everything afterwards.""" + from axolotl.monkeypatch.kernelize_fixes import ( + patch_kernelize_fixes, + unpatch_kernelize_fixes, + ) + + saved_sig = None + try: + from transformers.models.gpt_oss import modeling_gpt_oss + + func = modeling_gpt_oss.apply_rotary_pos_emb + if hasattr(type(func), "forward"): + saved_sig = inspect.signature(type(func).forward) + except ImportError: + func = None + + assert patch_kernelize_fixes() is True + yield + + unpatch_kernelize_fixes() + if func is not None and saved_sig is not None: + type(func).forward.__signature__ = saved_sig + + +def _tiny_gpt_oss(): + from transformers.models.gpt_oss.configuration_gpt_oss import GptOssConfig + from transformers.models.gpt_oss.modeling_gpt_oss import GptOssForCausalLM + + cfg = GptOssConfig( + hidden_size=32, + intermediate_size=64, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=8, + vocab_size=128, + num_local_experts=4, + num_experts_per_tok=2, + ) + return GptOssForCausalLM(cfg) + + +def test_patch_is_idempotent(kernelize_patch): + from axolotl.monkeypatch.kernelize_fixes import patch_kernelize_fixes + + assert patch_kernelize_fixes() is True + + +def test_unpatch_restores_original(): + from transformers.modeling_utils import PreTrainedModel + + from axolotl.monkeypatch.kernelize_fixes import ( + patch_kernelize_fixes, + unpatch_kernelize_fixes, + ) + + original = PreTrainedModel.kernelize + patch_kernelize_fixes() + assert PreTrainedModel.kernelize is not original + unpatch_kernelize_fixes() + assert PreTrainedModel.kernelize is original + # Safe to call again without a prior patch. + unpatch_kernelize_fixes() + + +def test_gpt_oss_kernelize_and_rotary_signature(kernelize_patch): + """gpt-oss: kernelize() succeeds and the rotary signature matches the hub + kernel (kernels-community/rotary) afterwards.""" + pytest.importorskip("transformers.models.gpt_oss") + from transformers.models.gpt_oss.modeling_gpt_oss import apply_rotary_pos_emb + + model = _tiny_gpt_oss() + model.train() + model.kernelize() + + params = inspect.signature(type(apply_rotary_pos_emb).forward).parameters + assert list(params) == ["self", "q", "k", "cos", "sin", "unsqueeze_dim"] + + +def test_rotary_call_behavior_unchanged(kernelize_patch): + """Only signature metadata changes; calls (even with position_ids) still + produce identical results.""" + import torch + + pytest.importorskip("transformers.models.gpt_oss") + from transformers.models.gpt_oss.modeling_gpt_oss import apply_rotary_pos_emb + + torch.manual_seed(0) + q, k = torch.randn(1, 4, 16, 8), torch.randn(1, 2, 16, 8) + cos, sin = torch.randn(1, 16, 4), torch.randn(1, 16, 4) + + q_ref, k_ref = apply_rotary_pos_emb(q, k, cos, sin) + _tiny_gpt_oss().kernelize() + q_new, k_new = apply_rotary_pos_emb(q, k, cos, sin, position_ids=None) + + assert torch.equal(q_ref, q_new) and torch.equal(k_ref, k_new) + + +def test_kernels_signature_validation_passes(kernelize_patch): + """The exact kernels-library check that crashed gpt-oss training on CUDA.""" + pytest.importorskip("transformers.models.gpt_oss") + from kernels.layer.func import _create_func_module + from kernels.layer.layer import _validate_layer + from transformers.models.gpt_oss.modeling_gpt_oss import apply_rotary_pos_emb + + # Exact signature of kernels-community/rotary::apply_rotary_transformers. + def apply_rotary_transformers(q, k, cos, sin, unsqueeze_dim=1): + return q, k + + hub_cls = _create_func_module(apply_rotary_transformers) + local_cls = type(apply_rotary_pos_emb) + + with pytest.raises(TypeError, match="different number of arguments"): + _validate_layer(check_cls=local_cls, cls=hub_cls, repo="stub") + + _tiny_gpt_oss().kernelize() + _validate_layer(check_cls=local_cls, cls=hub_cls, repo="stub") + + +def test_bare_function_entries_are_dropped(kernelize_patch): + """Architectures that stash a bare function (gemma4 and ~30 others) no + longer crash kernelize(); simulated by planting one on gpt-oss.""" + model = _tiny_gpt_oss() + attn = model.model.layers[0].self_attn + + def bare(q, k, cos, sin): + return q, k + + attn.__dict__.setdefault("_hidden_kernels", {})["bare"] = bare + model.train() + model.kernelize() + assert "bare" not in attn._hidden_kernels + + +def _tiny_gemma4(): + from transformers.models.gemma4.configuration_gemma4 import ( + Gemma4AudioConfig, + Gemma4Config, + Gemma4TextConfig, + Gemma4VisionConfig, + ) + from transformers.models.gemma4.modeling_gemma4 import ( + Gemma4ForConditionalGeneration, + ) + + text = Gemma4TextConfig( + hidden_size=32, + intermediate_size=64, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=8, + vocab_size=128, + num_experts=4, + num_experts_per_tok=2, + ) + vis = Gemma4VisionConfig( + hidden_size=32, + intermediate_size=64, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + head_dim=8, + ) + aud = Gemma4AudioConfig( + hidden_size=32, intermediate_size=64, num_hidden_layers=1, num_attention_heads=4 + ) + return Gemma4ForConditionalGeneration( + Gemma4Config(text_config=text, vision_config=vis, audio_config=aud) + ) + + +def test_gemma4_kernelize_crashes_without_patch_succeeds_with(): + """The real gemma4 bare-function case end to end: kernelize() crashes + unpatched, succeeds with the generic patch.""" + pytest.importorskip("transformers.models.gemma4") + from axolotl.monkeypatch.kernelize_fixes import ( + patch_kernelize_fixes, + unpatch_kernelize_fixes, + ) + + model = _tiny_gemma4() + model.train() + # transformers <= 5.8.x raises TypeError, >= 5.9 ValueError. + with pytest.raises((TypeError, ValueError, AttributeError)): + model.kernelize() + + patch_kernelize_fixes() + try: + model = _tiny_gemma4() + model.train() + model.kernelize() + finally: + unpatch_kernelize_fixes() + + +def test_patch_does_not_alter_weights(kernelize_patch): + """The repairs only touch ``_hidden_kernels`` and signature metadata; + parameters are untouched by kernelize().""" + import torch + + torch.manual_seed(0) + model = _tiny_gpt_oss() + before = {k: v.clone() for k, v in model.state_dict().items()} + model.train() + model.kernelize() + after = model.state_dict() + + assert before.keys() == after.keys() + assert all(torch.equal(before[k], after[k]) for k in before) diff --git a/tests/monkeypatch/test_lora_kernels_gemma4_unified.py b/tests/monkeypatch/test_lora_kernels_gemma4_unified.py new file mode 100644 index 0000000000..3d009f8bf0 --- /dev/null +++ b/tests/monkeypatch/test_lora_kernels_gemma4_unified.py @@ -0,0 +1,75 @@ +"""Tests for ``patch_self_attn_lora`` on the gemma4_unified attention class. + +Unlike standard gemma4 (which always skips the LoRA source rewrite), the unified +branch is *conditional* on ``fused_attn_kernel``: with it set the QKV/O kernels +ride the fused forward; without it the rewrite is skipped with a warning. Either +way the source rewrite (``_original_forward``) is never installed.""" + +import logging + +import pytest + +pytest.importorskip("triton", reason="importing lora_kernels pulls in triton") +gemma4_unified_modeling = pytest.importorskip( + "transformers.models.gemma4_unified.modeling_gemma4_unified", + reason="unified lora-kernel branch only matters when gemma4_unified is available", +) + + +def _cfg(fused_attn_kernel): + from axolotl.utils.dict import DictDefault + + return DictDefault({"fused_attn_kernel": fused_attn_kernel, "lora_dropout": 0.0}) + + +@pytest.fixture +def restore_unified_attn(): + """Ensure ``_original_forward`` doesn't leak in/out of these tests.""" + cls = gemma4_unified_modeling.Gemma4UnifiedTextAttention + had = hasattr(cls, "_original_forward") + saved = getattr(cls, "_original_forward", None) + if had: + del cls._original_forward + yield cls + if had: + cls._original_forward = saved + elif hasattr(cls, "_original_forward"): + del cls._original_forward + + +def _run_patch(monkeypatch, caplog, cls, cfg, level): + from axolotl.monkeypatch import lora_kernels + + monkeypatch.setattr(lora_kernels, "get_attention_cls_from_config", lambda _cfg: cls) + logger = logging.getLogger("axolotl.monkeypatch.lora_kernels") + logger.addHandler(caplog.handler) + previous_level = logger.level + logger.setLevel(level) + try: + lora_kernels.patch_self_attn_lora(cfg) + finally: + logger.removeHandler(caplog.handler) + logger.setLevel(previous_level) + + +class TestUnifiedLoraKernelSkip: + def test_skips_with_warning_when_no_fused_attn_kernel( + self, restore_unified_attn, monkeypatch, caplog + ): + cls = restore_unified_attn + _run_patch(monkeypatch, caplog, cls, _cfg(False), logging.WARNING) + + assert "fused_attn_kernel" in caplog.text + assert "skipping" in caplog.text.lower() + assert not hasattr(cls, "_original_forward"), ( + "unified attention must not get the LoRA source rewrite" + ) + + def test_skips_quietly_with_fused_attn_kernel( + self, restore_unified_attn, monkeypatch, caplog + ): + cls = restore_unified_attn + _run_patch(monkeypatch, caplog, cls, _cfg(True), logging.INFO) + + assert "fused attention path" in caplog.text + assert not hasattr(cls, "_original_forward") diff --git a/tests/telemetry/test_manager.py b/tests/telemetry/test_manager.py index 985f39d23d..67914b19e3 100644 --- a/tests/telemetry/test_manager.py +++ b/tests/telemetry/test_manager.py @@ -2,6 +2,7 @@ # pylint: disable=redefined-outer-name,protected-access +import logging import os from unittest.mock import patch @@ -249,19 +250,19 @@ def test_disable_telemetry(manager): assert not mock_capture.called -def test_exception_handling_during_send(manager): +def test_exception_handling_during_send(manager, caplog): """Test that exceptions in PostHog are handled gracefully""" - with ( - patch("posthog.capture", side_effect=Exception("Test error")), - patch("logging.Logger.warning") as mock_warning, - ): - manager.send_event("test_event") - warning_logged = False - for call in mock_warning.call_args_list: - if "Failed to send telemetry event" in str(call): - warning_logged = True - break - assert warning_logged + # axolotl loggers set propagate=False (logging_config.py), so caplog's root + # handler never sees them; attach it to the named logger directly. + logger = logging.getLogger("axolotl.telemetry.manager") + logger.addHandler(caplog.handler) + try: + with patch("posthog.capture", side_effect=Exception("Test error")): + manager.send_event("test_event") + finally: + logger.removeHandler(caplog.handler) + + assert "Failed to send telemetry event: Test error" in caplog.text def test_shutdown(manager): diff --git a/tests/test_normalize_config.py b/tests/test_normalize_config.py index ae93a8bd22..3da1d30569 100644 --- a/tests/test_normalize_config.py +++ b/tests/test_normalize_config.py @@ -140,7 +140,8 @@ def test_migrate_fsdp_config(self): cfg_without_version = validate_config(cfg_without_version) - self.assertNotIn("fsdp_version", cfg_without_version) + self.assertEqual(cfg_without_version.fsdp_version, 1) + self.assertEqual(cfg_without_version.fsdp_config.fsdp_version, 1) self.assertEqual( cfg_without_version.fsdp_config.auto_wrap_policy, "SIZE_BASED_WRAP" ) diff --git a/tests/test_processing_strategies.py b/tests/test_processing_strategies.py index 98c4c2907e..155b17446e 100644 --- a/tests/test_processing_strategies.py +++ b/tests/test_processing_strategies.py @@ -12,6 +12,7 @@ Gemma3nProcessingStrategy, Gemma3ProcessingStrategy, Gemma4ProcessingStrategy, + Gemma4UnifiedProcessingStrategy, Glm4vProcessingStrategy, InternVLProcessingStrategy, Llama3_2VisionProcessingStrategy, @@ -873,6 +874,11 @@ def test_dispatch_gemma4(): assert isinstance(s, Gemma4ProcessingStrategy) +def test_dispatch_gemma4_unified(): + s = _dispatch(_FakeGemma4Processor(), "gemma4_unified") + assert isinstance(s, Gemma4UnifiedProcessingStrategy) + + def test_dispatch_llama3_2_vision(): vocab = { "<|start_header_id|>assistant<|end_header_id|>\n\n": [1, 2, 3, 4, 5], From ac35190d8960c3434d7e82c77ac030f7b9c1575a Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:34:20 +0530 Subject: [PATCH 1353/1405] fix numpy version mismatch 4 pirate (#3662) [skip ci] * fix version mismatch 4 pirate * add whitlist for collab --------- Co-authored-by: NanoCode012 --- MANIFEST.in | 1 + .../colab-axolotl-example.ipynb | 19 +++++++++++++++- pyproject.toml | 3 +++ src/axolotl/telemetry/manager.py | 22 +++++++++++-------- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 5cf08eabf0..a3624d13c6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ include README.md include LICENSE include VERSION include src/axolotl/utils/chat_templates/templates/*.jinja +recursive-include src/axolotl *.yaml include AGENTS.md recursive-include docs/agents *.md recursive-include axolotl *.py diff --git a/examples/colab-notebooks/colab-axolotl-example.ipynb b/examples/colab-notebooks/colab-axolotl-example.ipynb index 8d70e1df26..5d92d29eee 100644 --- a/examples/colab-notebooks/colab-axolotl-example.ipynb +++ b/examples/colab-notebooks/colab-axolotl-example.ipynb @@ -36,7 +36,24 @@ "id": "msOCO4NRmRLa" }, "outputs": [], - "source": "%%capture\n# This step can take ~5-10 minutes to install dependencies\n!pip install --no-build-isolation \"axolotl>=0.16.1\"\n!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5effb44\"" + "source": [ + "%%capture\n", + "# This step can take ~5-10 minutes to install dependencies\n", + "!pip install --no-build-isolation \"axolotl>=0.16.1\"\n", + "!pip install \"cut-cross-entropy[transformers] @ git+https://github.com/axolotl-ai-cloud/ml-cross-entropy.git@5effb44\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Colab ships an older numpy that gets half-upgraded during install; force a\n", + "# clean, consistent numpy so it doesn't crash with a missing '_blas_supports_fpe'.\n", + "# Restart the runtime (Runtime -> Restart session) after this before importing axolotl.\n", + "!pip install --force-reinstall --no-cache-dir \"numpy>=2.3\"" + ] }, { "cell_type": "markdown", diff --git a/pyproject.toml b/pyproject.toml index ce513eb33c..88a424a226 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -173,6 +173,9 @@ Repository = "https://github.com/axolotl-ai-cloud/axolotl.git" [tool.setuptools] include-package-data = true +[tool.setuptools.package-data] +axolotl = ["**/*.yaml", "**/*.jinja"] + [tool.setuptools.packages.find] where = ["src"] diff --git a/src/axolotl/telemetry/manager.py b/src/axolotl/telemetry/manager.py index 3fdcd9d935..90f0e6dae7 100644 --- a/src/axolotl/telemetry/manager.py +++ b/src/axolotl/telemetry/manager.py @@ -174,15 +174,19 @@ def is_truthy_env(var_name: str) -> bool: def _load_whitelist(self) -> dict: """Load HuggingFace Hub organization whitelist""" - with open(WHITELIST_PATH, encoding="utf-8") as f: - whitelist = yaml.safe_load(f) - - # Send org strings to lowercase since model names are case insensitive - whitelist["organizations"] = { - org.lower() for org in whitelist["organizations"] - } - - return whitelist + try: + with open(WHITELIST_PATH, encoding="utf-8") as f: + whitelist = yaml.safe_load(f) + except (OSError, yaml.YAMLError) as e: + # A missing/unreadable whitelist must never break `import axolotl`. + # Empty whitelist => nothing is whitelisted => all orgs get redacted. + LOG.warning(f"Could not load telemetry whitelist ({e}); redacting all orgs") + return {"organizations": set()} + + # Send org strings to lowercase since model names are case insensitive + whitelist["organizations"] = {org.lower() for org in whitelist["organizations"]} + + return whitelist def _is_whitelisted(self, value: str) -> bool: """ From b02ee147a38c100da518294f56356df3ae09e5f6 Mon Sep 17 00:00:00 2001 From: Tai An Date: Mon, 15 Jun 2026 01:04:57 -0700 Subject: [PATCH 1354/1405] fix(chat_templates): extract reasoning_content before reassigning content in qwen3_5 template (#3725) [skip ci] The inline- assistant branch reassigned content (stripping it to the post- answer) before reasoning_content was extracted from it. Since reasoning_content reads from the already-truncated content, the reasoning trace was dropped and the answer leaked into the block. Swap the two set statements to match the official Qwen3.5 template order. --- src/axolotl/utils/chat_templates/templates/qwen3_5.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja b/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja index c8d0e8da0b..44eabf526f 100644 --- a/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja +++ b/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja @@ -93,8 +93,8 @@ {%- set reasoning_content = message.reasoning_content %} {%- else %} {%- if '' in content %} - {%- set content = content.split('')[-1].lstrip('\n') %} {%- set reasoning_content = content.split('')[0].rstrip('\n').split('')[-1].lstrip('\n') %} + {%- set content = content.split('')[-1].lstrip('\n') %} {%- endif %} {%- endif %} {%- if loop.index0 > ns.last_query_index %} From a56fe869f9d9f82025cabd016dc2db7987cf1bbb Mon Sep 17 00:00:00 2001 From: VED <146507396+ved1beta@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:36:01 +0530 Subject: [PATCH 1355/1405] fix qwen chat3.5 (#3728) [skip ci] --- src/axolotl/utils/chat_templates/templates/qwen3_5.jinja | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja b/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja index 44eabf526f..8acaf82a93 100644 --- a/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja +++ b/src/axolotl/utils/chat_templates/templates/qwen3_5.jinja @@ -81,12 +81,13 @@ {%- if message['content'] is string %} {%- set content = message.content %} {%- else %} - {%- set content = '' %} + {%- set content_ns = namespace(text='') %} {%- for item in message['content'] %} {%- if 'text' in item %} - {%- set content = content + item['text'] %} + {%- set content_ns.text = content_ns.text + item['text'] %} {%- endif %} {%- endfor %} + {%- set content = content_ns.text %} {%- endif %} {%- set reasoning_content = '' %} {%- if message.reasoning_content is defined and message.reasoning_content is not none %} From 277d524b12b5585c53a65b3564c9d6ebdcf7b4a7 Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 15 Jun 2026 18:09:49 -0400 Subject: [PATCH 1356/1405] feat(offload): hidden_states activation offloading + fix legacy/full-param paths (#3733) --- docs/gradient_checkpointing.qmd | 18 ++ src/axolotl/core/builders/base.py | 16 +- .../mixins/activation_checkpointing.py | 9 +- src/axolotl/core/training_args_base.py | 11 +- src/axolotl/loaders/model.py | 20 +- .../activation_offload_checkpoint.py | 260 ++++++++++++++++++ src/axolotl/utils/schemas/config.py | 17 +- src/axolotl/utils/schemas/validation.py | 31 +++ tests/e2e/test_activation_offloading.py | 223 +++++++++++++++ 9 files changed, 590 insertions(+), 15 deletions(-) create mode 100644 src/axolotl/monkeypatch/activation_offload_checkpoint.py diff --git a/docs/gradient_checkpointing.qmd b/docs/gradient_checkpointing.qmd index 54c53899c9..b6644e2da3 100644 --- a/docs/gradient_checkpointing.qmd +++ b/docs/gradient_checkpointing.qmd @@ -28,6 +28,24 @@ The `activation_offloading: legacy` naively offloads activations to CPU and with For resource constrained environments with limited CPU memory, `activation_offloading: disk` offloads activations to disk instead of CPU RAM so that much larger context lengths can be trained with minimal memory. +The `activation_offloading: hidden_states` mode (ALST-style) is gradient checkpointing that offloads only +the per-layer input (the checkpoint's `hidden_states`) to CPU — one tensor per layer rather than every +activation — overlapped with compute on a side CUDA stream. It replaces torch's reentrant checkpoint +(so `gradient_checkpointing_kwargs.use_reentrant` is forced to `true`) and is framework-agnostic +(works under FSDP2, DTensor-aware for sequence/context parallelism). + +### Choosing a mode + +| Mode | What moves | Best for | +|------|-----------|----------| +| `true` | All activations → CPU, stream-overlapped | General use; LoRA/QLoRA (offloads instead of recomputing) | +| `legacy` | All activations → CPU, synchronous | Lowest resident GPU memory; when the streamed path's in-flight buffering inflates peak | +| `disk` | Activations → disk | Severely CPU-RAM-constrained hosts | +| `hidden_states` | Per-layer input only → CPU, stream-overlapped | Long-context **full-parameter** finetuning; reaches contexts where plain gradient checkpointing OOMs | + +`hidden_states` is designed for full-parameter training. With LoRA/QLoRA the frozen base can break reentrant +checkpointing, so prefer `activation_offloading: true` for adapters. + ### Enabling Layer Offloading ```yaml diff --git a/src/axolotl/core/builders/base.py b/src/axolotl/core/builders/base.py index 549b69ce18..5e2a9d9fe0 100644 --- a/src/axolotl/core/builders/base.py +++ b/src/axolotl/core/builders/base.py @@ -555,10 +555,20 @@ def _configure_accelerator_config(self, training_args_kwargs: dict): def _configure_gradient_checkpointing(self, training_args_kwargs: dict): if self.cfg.layer_offloading: training_args_kwargs["layer_offloading"] = True - if self.cfg.activation_offloading is True: - # don't use the HF gradient checkpointing, manually wrap + if self.cfg.activation_offloading == "hidden_states": + # The checkpoint monkeypatch offloads the per-layer input, so HF + # checkpointing stays on and must be reentrant for the patch to apply. + training_args_kwargs["gradient_checkpointing"] = True + gc_kwargs = dict(self.cfg.gradient_checkpointing_kwargs or {}) + gc_kwargs["use_reentrant"] = True + training_args_kwargs["gradient_checkpointing_kwargs"] = gc_kwargs + elif self.cfg.activation_offloading: + # TRL offloader replaces HF recompute (re-added for full finetune in the + # model loader), so disable HF checkpointing and pass the mode through. training_args_kwargs["gradient_checkpointing"] = False - training_args_kwargs["activation_offloading"] = True + training_args_kwargs["activation_offloading"] = ( + self.cfg.activation_offloading + ) elif self.cfg.gradient_checkpointing is not None: training_args_kwargs["gradient_checkpointing"] = ( self.cfg.gradient_checkpointing diff --git a/src/axolotl/core/trainers/mixins/activation_checkpointing.py b/src/axolotl/core/trainers/mixins/activation_checkpointing.py index b61c45feed..789808d65a 100644 --- a/src/axolotl/core/trainers/mixins/activation_checkpointing.py +++ b/src/axolotl/core/trainers/mixins/activation_checkpointing.py @@ -30,13 +30,18 @@ class ActivationOffloadingMixin(Trainer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.args.activation_offloading: + # "legacy" uses the previous synchronous implementation (no CUDA + # streams), which keeps far fewer activations resident on-GPU than + # the stream-overlapped path (True/"disk"); the streams path stashes + # several activations to overlap copies, inflating peak reserved. + use_streams = self.args.activation_offloading != "legacy" if isinstance(self.model, PeftModel): self.activation_offload_context = get_lora_act_offloading_ctx_manager( - self.model, use_streams=True + self.model, use_streams=use_streams ) else: self.activation_offload_context = get_act_offloading_ctx_manager( - self.model, use_streams=True + self.model, use_streams=use_streams ) else: self.activation_offload_context = contextlib.nullcontext() diff --git a/src/axolotl/core/training_args_base.py b/src/axolotl/core/training_args_base.py index e9727dbb16..1d35470e20 100644 --- a/src/axolotl/core/training_args_base.py +++ b/src/axolotl/core/training_args_base.py @@ -3,7 +3,7 @@ """ from dataclasses import dataclass, field -from typing import Optional +from typing import Literal, Optional from PIL.Image import Resampling @@ -235,9 +235,14 @@ class AxolotlTrainingMixins: }, ) - activation_offloading: bool | None = field( + # "hidden_states" never reaches the trainer (it's handled via the checkpoint + # monkeypatch in the model loader), so the trainer only sees these values. + activation_offloading: Literal["legacy", "disk"] | bool | None = field( default=None, - metadata={"help": "Use activation offloading with CUDA streams for training."}, + metadata={ + "help": "Activation offloading mode: True (stream-overlapped), " + "'legacy' (synchronous), 'disk', or False." + }, ) layer_offloading: bool | None = field( diff --git a/src/axolotl/loaders/model.py b/src/axolotl/loaders/model.py index 2f056e19f8..92538ce41d 100644 --- a/src/axolotl/loaders/model.py +++ b/src/axolotl/loaders/model.py @@ -320,12 +320,28 @@ def _configure_experts_implementation(self): self.model.set_experts_implementation(impl) def _apply_activation_checkpointing(self): - if self.cfg.activation_offloading is True: + ao = self.cfg.activation_offloading + # "hidden_states" (ALST-style): HF reentrant gradient checkpointing already + # wraps the layers; a checkpoint monkeypatch offloads only the per-layer + # input (hidden_states). No manual recompute wrap needed. + if ao == "hidden_states": + from axolotl.monkeypatch.activation_offload_checkpoint import ( + patch_hidden_states_offload, + ) + + patch_hidden_states_offload() + return + # TRL offloader is adapter-aware: + # - LoRA/QLoRA: offload *replaces* recompute (pure offload is leaner/faster; + # recompute would pin the offloaded tensors and balloon memory). No wrap. + # - Full finetune: pure offload of every activation exceeds PCIe bandwidth + # (backlogs on-GPU, OOMs at long seq), so keep recompute and offload only + # the checkpoint boundaries — apply the manual wrap. + if ao and not self.cfg.adapter: from axolotl.core.trainers.mixins.activation_checkpointing import ( ac_wrap_hf_model, ) - # ^^ importing this at the module level breaks plugins ac_wrap_hf_model(self.model) def _resize_token_embeddings(self): diff --git a/src/axolotl/monkeypatch/activation_offload_checkpoint.py b/src/axolotl/monkeypatch/activation_offload_checkpoint.py new file mode 100644 index 0000000000..aa9d153a34 --- /dev/null +++ b/src/axolotl/monkeypatch/activation_offload_checkpoint.py @@ -0,0 +1,260 @@ +"""Activation-checkpoint input ("hidden_states") offloading, ALST-style. + +Gradient checkpointing already discards a layer's intermediate activations and +recomputes them in backward; it still keeps the *checkpoint input* (the layer's +hidden_states) resident. For long sequences those per-layer inputs +(num_layers x [seq, hidden]) dominate GPU memory. This offloads only that one +tensor per checkpoint to CPU and brings it back for the backward recompute — +minimal PCIe traffic (one tensor/layer, not every activation). + +The copies are overlapped with compute on a side stream (forward async d2h with +bounded in-flight + backward h2d prefetch), so at long seq the transfer hides +behind the recompute. + +It replaces torch's reentrant ``CheckpointFunction`` (use_reentrant=True), so it +is framework-agnostic (works under FSDP2; no DeepSpeed dependency). DTensor +inputs (sequence/context parallel) are offloaded too — ``DTensor.to`` round-trips +the local shard and preserves placements. Adapted from +torch.utils.checkpoint.CheckpointFunction and Snowflake ArcticTraining. +""" + +import contextlib + +import torch +from torch.utils.checkpoint import ( + _get_autocast_kwargs, + _get_device_module, + _infer_device_type, + check_backward_validity, + detach_variable, + get_device_states, + set_device_states, +) + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def _offloadable(t: torch.Tensor) -> bool: + # Offload any on-device activation, INCLUDING DTensors: under sequence/context + # parallelism the layer input is a DTensor and offloading its sharded local + # tensor is exactly what we want. DTensor.to("cpu")/.to(dev) round-trips the + # local shard and preserves placements, so the plain path handles it. Model + # params never reach here (they live inside run_function, not the offloaded + # arg), so TRL-style param-storage filtering is unnecessary. + return t.device.type != "cpu" + + +class _StreamOffloadManager: + """Per-forward-pass coordinator that overlaps the input d2h/h2d copies with + compute on a side stream. Forward: async-copy each layer input to CPU while + later layers compute (bounded in-flight so only a few sources stay resident). + Backward: bring inputs back on the side stream and prefetch the next one + (inputs are consumed in reverse-of-offload order) to hide the reload.""" + + def __init__(self, max_inflight=2): + self.s1 = torch.cuda.Stream() + self.max_inflight = max_inflight + self.reset() + + def reset(self): + self.next_id = 0 + self.inflight = {} # id -> (gpu_src, event) keep source alive until d2h done + self.cpu = {} # id -> (cpu_tensor, device, requires_grad) + self.prefetched = {} # id -> (gpu_tensor, event) + + # ---- forward ---- + def offload(self, t): + s0 = torch.cuda.current_stream() + self.s1.wait_stream(s0) # input must be produced before we copy it + with torch.cuda.stream(self.s1): + cpu_t = t.detach().to("cpu", non_blocking=True) + ev = self.s1.record_event() + tid = self.next_id + self.next_id += 1 + self.inflight[tid] = (t, ev) # hold GPU source until copy completes + self.cpu[tid] = (cpu_t, t.device, t.requires_grad) + self._reap() + return tid + + def _reap(self): + for tid in [k for k, (_, ev) in self.inflight.items() if ev.query()]: + del self.inflight[tid] + while len(self.inflight) > self.max_inflight: + tid = min(self.inflight) + _, ev = self.inflight.pop(tid) + ev.synchronize() + + # ---- backward ---- + def _bring_back(self, tid): + cpu_t, device, _ = self.cpu[tid] + with torch.cuda.stream(self.s1): + gpu_t = cpu_t.to(device, non_blocking=True) + return gpu_t, self.s1.record_event() + + def restore(self, tid): + if tid in self.prefetched: + gpu_t, ev = self.prefetched.pop(tid) + else: + gpu_t, ev = self._bring_back(tid) + nxt = ( + tid - 1 + ) # next input needed (reverse order) — prefetch to overlap recompute + if nxt >= 0 and nxt in self.cpu and nxt not in self.prefetched: + self.prefetched[nxt] = self._bring_back(nxt) + s0 = torch.cuda.current_stream() + s0.wait_event(ev) + # gpu_t was allocated on the side stream; tell the allocator it's now used + # on the compute stream so its storage isn't reused before the recompute + # consumes it (use-after-free guard, as in TRL's offloader). + gpu_t.record_stream(s0) + _, _, requires_grad = self.cpu[tid] + out = gpu_t.detach().requires_grad_(requires_grad) + del self.cpu[tid] + if not self.cpu: + self.reset() + return out + + +_STREAM_MANAGER = None + + +def _get_stream_manager(): + global _STREAM_MANAGER + if _STREAM_MANAGER is None: + _STREAM_MANAGER = _StreamOffloadManager() + return _STREAM_MANAGER + + +class HiddenStatesOffloadCheckpoint(torch.autograd.Function): + """Reentrant checkpoint that offloads the layer input (hidden_states) to CPU, + overlapping the transfer with compute via a side stream.""" + + @staticmethod + def forward(ctx, run_function, preserve_rng_state, *args): + check_backward_validity(args) + ctx.run_function = run_function + ctx.preserve_rng_state = preserve_rng_state + ctx.device_type = _infer_device_type(*args) + ctx.device_autocast_kwargs, ctx.cpu_autocast_kwargs = _get_autocast_kwargs( + ctx.device_type + ) + if preserve_rng_state: + ctx.fwd_cpu_state = torch.get_rng_state() + ctx.had_device_in_fwd = False + device_module = _get_device_module(ctx.device_type) + if getattr(device_module, "_initialized", False): + ctx.had_device_in_fwd = True + ctx.fwd_devices, ctx.fwd_device_states = get_device_states(*args) + + mgr = _get_stream_manager() + ctx.inputs = [] + ctx.tensor_indices = [] + ctx.offload_tid = None # manager id for the offloaded input + ctx.offload_arg_pos = None + tensor_inputs = [] + for i, arg in enumerate(args): + if torch.is_tensor(arg): + # Offload only the first tensor (the layer input / hidden_states); + # other tensors (e.g. a shared [seq, seq] mask) stay on-device. + if ctx.offload_tid is None and _offloadable(arg): + ctx.offload_tid = mgr.offload(arg) + ctx.offload_arg_pos = i + ctx.inputs.append(None) + else: + tensor_inputs.append(arg) + ctx.tensor_indices.append(i) + ctx.inputs.append(None) + else: + ctx.inputs.append(arg) + ctx.save_for_backward(*tensor_inputs) + with torch.no_grad(): + outputs = run_function(*args) + return outputs + + @staticmethod + def backward(ctx, *args): + if not torch.autograd._is_checkpoint_valid(): + raise RuntimeError( + "use_reentrant=True checkpoint is incompatible with .grad()/passing " + "`inputs` to .backward()." + ) + mgr = _get_stream_manager() + inputs = list(ctx.inputs) + tensors = ctx.saved_tensors + for i, idx in enumerate(ctx.tensor_indices): + inputs[idx] = tensors[i] + if ctx.offload_tid is not None: + inputs[ctx.offload_arg_pos] = mgr.restore(ctx.offload_tid) + + rng_devices = [] + if ctx.preserve_rng_state and ctx.had_device_in_fwd: + rng_devices = ctx.fwd_devices + with torch.random.fork_rng( + devices=rng_devices, + enabled=ctx.preserve_rng_state, + device_type=ctx.device_type, + ): + if ctx.preserve_rng_state: + torch.set_rng_state(ctx.fwd_cpu_state) + if ctx.had_device_in_fwd: + set_device_states( + ctx.fwd_devices, + ctx.fwd_device_states, + device_type=ctx.device_type, + ) + detached_inputs = detach_variable(tuple(inputs)) + device_autocast_ctx = ( + torch.amp.autocast( + device_type=ctx.device_type, **ctx.device_autocast_kwargs + ) + if torch.amp.is_autocast_available(ctx.device_type) + else contextlib.nullcontext() + ) + with ( + torch.enable_grad(), + device_autocast_ctx, + torch.amp.autocast("cpu", **ctx.cpu_autocast_kwargs), + ): + outputs = ctx.run_function(*detached_inputs) + if isinstance(outputs, torch.Tensor): + outputs = (outputs,) + outputs_with_grad = [] + args_with_grad = [] + for i in range(len(outputs)): + if torch.is_tensor(outputs[i]) and outputs[i].requires_grad: + outputs_with_grad.append(outputs[i]) + args_with_grad.append(args[i]) + if len(outputs_with_grad) == 0: + raise RuntimeError("none of output has requires_grad=True") + torch.autograd.backward(outputs_with_grad, args_with_grad) + grads = tuple( + inp.grad if isinstance(inp, torch.Tensor) else None + for inp in detached_inputs + ) + return (None, None) + grads + + +_ORIG_CHECKPOINT_FUNCTION = None + + +def patch_hidden_states_offload(): + """Replace torch's reentrant ``CheckpointFunction`` with the hidden_states + offloading version. ``torch.utils.checkpoint.checkpoint(..., use_reentrant=True)`` + resolves ``CheckpointFunction`` from the module namespace at call time, so + swapping the attribute is sufficient (and FSDP2-safe — only activations move).""" + global _ORIG_CHECKPOINT_FUNCTION + import torch.utils.checkpoint as ckpt + + if _ORIG_CHECKPOINT_FUNCTION is None: + _ORIG_CHECKPOINT_FUNCTION = ckpt.CheckpointFunction + ckpt.CheckpointFunction = HiddenStatesOffloadCheckpoint + LOG.info("Patched checkpoint with hidden_states CPU offload (streamed)") + + +def unpatch_hidden_states_offload(): + import torch.utils.checkpoint as ckpt + + if _ORIG_CHECKPOINT_FUNCTION is not None: + ckpt.CheckpointFunction = _ORIG_CHECKPOINT_FUNCTION diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index dbd73b66f6..54cb2b07a3 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -604,11 +604,18 @@ class AxolotlInputConfig( "description": "Additional kwargs to pass to the trainer for gradient checkpointing" }, ) - activation_offloading: Literal["legacy", "disk"] | bool | None = Field( - default=False, - json_schema_extra={ - "description": "Whether to offload activations. Available options are: true, false, 'legacy', 'disk'." - }, + activation_offloading: Literal["legacy", "disk", "hidden_states"] | bool | None = ( + Field( + default=False, + json_schema_extra={ + "description": ( + "Whether to offload activations. Options: true/false, 'legacy', " + "'disk' (TRL offloader), or 'hidden_states' (ALST-style: gradient " + "checkpointing that offloads only the per-layer input to CPU, " + "overlapped with compute; best for long-context full finetuning)." + ) + }, + ) ) layer_offloading: bool | None = Field( default=False, diff --git a/src/axolotl/utils/schemas/validation.py b/src/axolotl/utils/schemas/validation.py index 10424ddd2d..c9fe52ac47 100644 --- a/src/axolotl/utils/schemas/validation.py +++ b/src/axolotl/utils/schemas/validation.py @@ -1448,6 +1448,37 @@ def check_activation_offloading_wo_gc(self): raise ValueError("activation_offloading requires gradient_checkpointing") return self + @model_validator(mode="after") + def check_hidden_states_offloading(self): + if self.activation_offloading != "hidden_states": + return self + # Forcing reentrant (below) on a partially frozen model is the broken combo + # check_use_reentrant_mismatch guards — but that before-validator ran before + # we forced it, so catch it here. https://github.com/huggingface/transformers/issues/21381 + if self.unfrozen_parameters: + raise ValueError( + "activation_offloading: hidden_states forces reentrant checkpointing, " + "which is incompatible with `unfrozen_parameters` (partially frozen " + "model)." + ) + # ALST-style offloading replaces torch's reentrant CheckpointFunction, so it + # needs use_reentrant=True. Force it on (warn if the user asked otherwise). + gc_kwargs = dict(self.gradient_checkpointing_kwargs or {}) + if gc_kwargs.get("use_reentrant") is False: + LOG.warning( + "activation_offloading: hidden_states requires reentrant checkpointing; " + "overriding gradient_checkpointing_kwargs.use_reentrant to true." + ) + gc_kwargs["use_reentrant"] = True + self.gradient_checkpointing_kwargs = gc_kwargs + if self.adapter: + LOG.warning( + "activation_offloading: hidden_states is designed for full-parameter " + "training; with LoRA/QLoRA the frozen base may break reentrant " + "checkpointing. Prefer activation_offloading: true for adapters." + ) + return self + @model_validator(mode="after") def check_better_transformers(self): if self.flash_optimum is True: diff --git a/tests/e2e/test_activation_offloading.py b/tests/e2e/test_activation_offloading.py index ee3fdbb6cf..0397ed1872 100644 --- a/tests/e2e/test_activation_offloading.py +++ b/tests/e2e/test_activation_offloading.py @@ -81,6 +81,229 @@ def test_activation_offloading( train(cfg=cfg, dataset_meta=dataset_meta) check_model_output_exists(temp_dir, cfg) + @pytest.mark.parametrize( + "offload_mode,expect_streams", + [(True, True), ("legacy", False), ("disk", True)], + ) + def test_offload_mode_wiring( + self, temp_dir, monkeypatch, offload_mode, expect_streams + ): + """`activation_offloading` must reach the trainer as a live offload + context: True => stream-overlapped, 'legacy' => synchronous. Guards the + regression where string modes fell through to plain gradient + checkpointing (no offload at all).""" + from trl.models.activation_offloading import OffloadActivations + + captured = {} + original_step = ActivationOffloadingMixin.training_step + + def capture_step(self, *args, **kwargs): + captured["ctx"] = self.activation_offload_context + return original_step(self, *args, **kwargs) + + monkeypatch.setattr(ActivationOffloadingMixin, "training_step", capture_step) + + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 1024, + "val_set_size": 0.0, + "special_tokens": {"pad_token": "<|endoftext|>"}, + "datasets": [ + {"path": "mhenrichsen/alpaca_2k_test", "type": "alpaca"}, + ], + "max_steps": 2, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 1e-5, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": True, + "bf16": "auto", + "gradient_checkpointing": True, + "activation_offloading": offload_mode, + "adapter": "lora", + "lora_r": 8, + "lora_alpha": 16, + "lora_target_linear": True, + "save_first_step": False, + } + ) + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + train(cfg=cfg, dataset_meta=dataset_meta) + + ctx = captured.get("ctx") + assert isinstance(ctx, OffloadActivations), ( + f"activation_offloading={offload_mode!r} did not produce an offload " + f"context (got {type(ctx).__name__}) — string modes likely fell " + f"through to plain gradient checkpointing" + ) + assert ctx.use_streams is expect_streams + + @pytest.mark.parametrize( + "adapter,expect_recompute_wrap", + [("lora", False), (None, True)], + ) + def test_offload_is_adapter_aware( + self, temp_dir, monkeypatch, adapter, expect_recompute_wrap + ): + """activation_offloading is adapter-aware: LoRA offloads *instead of* + recomputing (no checkpoint wrap — pure offload is leaner/faster), while + full finetune keeps recompute and offloads the checkpoint boundaries + (the combo — pure offload is PCIe-bound and OOMs at full-param scale).""" + captured = {} + original_step = ActivationOffloadingMixin.training_step + + def capture_step(self, *args, **kwargs): + captured["wrapped"] = any( + "_checkpoint_wrapped_module" in n for n, _ in self.model.named_modules() + ) + return original_step(self, *args, **kwargs) + + monkeypatch.setattr(ActivationOffloadingMixin, "training_step", capture_step) + + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 1024, + "val_set_size": 0.0, + "special_tokens": {"pad_token": "<|endoftext|>"}, + "datasets": [ + {"path": "mhenrichsen/alpaca_2k_test", "type": "alpaca"}, + ], + "max_steps": 2, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 1e-5, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": True, + "bf16": "auto", + "gradient_checkpointing": True, + "activation_offloading": True, + "save_first_step": False, + } + ) + if adapter: + cfg["adapter"] = adapter + cfg["lora_r"] = 8 + cfg["lora_alpha"] = 16 + cfg["lora_target_linear"] = True + + cfg = validate_config(cfg) + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + train(cfg=cfg, dataset_meta=dataset_meta) + + assert captured.get("wrapped") is expect_recompute_wrap + + def test_hidden_states_offload_full_param(self, temp_dir): + """activation_offloading: hidden_states (ALST-style) patches the reentrant + checkpoint to offload the per-layer input, for full-parameter training.""" + import torch.utils.checkpoint as ckpt + + from axolotl.monkeypatch.activation_offload_checkpoint import ( + HiddenStatesOffloadCheckpoint, + unpatch_hidden_states_offload, + ) + + cfg = DictDefault( + { + "base_model": "HuggingFaceTB/SmolLM2-135M", + "sequence_len": 1024, + "val_set_size": 0.0, + "special_tokens": {"pad_token": "<|endoftext|>"}, + "datasets": [{"path": "mhenrichsen/alpaca_2k_test", "type": "alpaca"}], + "max_steps": 2, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "output_dir": temp_dir, + "learning_rate": 1e-5, + "optimizer": "adamw_torch", + "lr_scheduler": "cosine", + "flash_attention": True, + "sample_packing": True, + "bf16": "auto", + "gradient_checkpointing": True, + "activation_offloading": "hidden_states", + "save_first_step": False, + } + ) + try: + cfg = validate_config(cfg) + # validator forces reentrant for hidden_states + assert cfg.gradient_checkpointing_kwargs["use_reentrant"] is True + normalize_config(cfg) + dataset_meta = load_datasets(cfg=cfg) + train(cfg=cfg, dataset_meta=dataset_meta) + assert ckpt.CheckpointFunction is HiddenStatesOffloadCheckpoint + check_model_output_exists(temp_dir, cfg) + finally: + unpatch_hidden_states_offload() + + def test_hidden_states_offload_numerical_parity(self): + """The offloaded checkpoint only round-trips the layer input through CPU, + so loss and grads must match plain reentrant checkpointing. Same model + instance for both runs => identical weights; the only difference is the + d2h/h2d of the per-layer input.""" + import torch + import torch.utils.checkpoint as ckpt + from transformers import AutoModelForCausalLM + + from axolotl.monkeypatch.activation_offload_checkpoint import ( + HiddenStatesOffloadCheckpoint, + patch_hidden_states_offload, + unpatch_hidden_states_offload, + ) + + if not torch.cuda.is_available(): + pytest.skip("hidden_states offload requires CUDA") + + torch.manual_seed(0) + model = AutoModelForCausalLM.from_pretrained( + "HuggingFaceTB/SmolLM2-135M", dtype=torch.bfloat16 + ).to("cuda") + model.train() + model.gradient_checkpointing_enable( + gradient_checkpointing_kwargs={"use_reentrant": True} + ) + + torch.manual_seed(1) + input_ids = torch.randint(0, 1000, (1, 512), device="cuda") + batch = {"input_ids": input_ids, "labels": input_ids.clone()} + + def run(): + model.zero_grad(set_to_none=True) + loss = model(**batch).loss + loss.backward() + grad = model.model.layers[0].mlp.down_proj.weight.grad + return loss.detach().clone(), grad.detach().clone() + + assert ckpt.CheckpointFunction is not HiddenStatesOffloadCheckpoint + loss_ref, grad_ref = run() + + patch_hidden_states_offload() + try: + assert ckpt.CheckpointFunction is HiddenStatesOffloadCheckpoint + loss_off, grad_off = run() + finally: + unpatch_hidden_states_offload() + + # Forward output is identical; backward recompute introduces only bf16 + # float noise, so grads match within a loose tolerance. + assert torch.allclose(loss_ref, loss_off, rtol=0, atol=1e-3), ( + f"loss diverged: ref={loss_ref.item()} offload={loss_off.item()}" + ) + assert torch.allclose(grad_ref, grad_off, rtol=1e-2, atol=1e-2), ( + f"grad diverged: max|d|={(grad_ref - grad_off).abs().max().item()}" + ) + def test_no_vram_leak_regression(self, temp_dir, monkeypatch): """#3638 regression — fail on linear VRAM growth across training steps. From fa7a66502d48f556c44910b9acb87124e2cc34af Mon Sep 17 00:00:00 2001 From: Ayush <158567328+Ayushhgit@users.noreply.github.com> Date: Tue, 16 Jun 2026 07:29:31 +0530 Subject: [PATCH 1357/1405] fix: KTO user_defined dataset transform crashes on every documented config (#3730) [skip ci] * fix: KTO user_defined dataset transform crashes on every documented config The user_defined.default KTO strategy was broken in all configurations: - when completion_format was provided, the default was assigned to a misnamed chosen_format variable only in the fallback branch, so the transform raised NameError: chosen_format - when completion_format was omitted, the generated placeholder name did not match the .format() keyword (chosen= vs {completion}), raising KeyError - prompt formatting read sample['prompt'] instead of sample[field_prompt], breaking custom field_prompt configs Also surface the underlying exception when a prompt strategy fails to load instead of silently returning None, which previously crashed later with the unhelpful 'TypeError: None is not a callable object'. Fixes #2757 Co-Authored-By: Claude Fable 5 * docs: add docstrings to KTO user_defined tests for coverage check Co-Authored-By: Claude Fable 5 * fix: wrong pydantic type * feat: updated test to handle e2e validation --------- Co-authored-by: NanoCode012 --- src/axolotl/prompt_strategies/base.py | 4 +- .../prompt_strategies/kto/user_defined.py | 12 +- src/axolotl/utils/data/rl.py | 7 + src/axolotl/utils/schemas/datasets.py | 2 +- .../test_kto_user_defined.py | 140 ++++++++++++++++++ 5 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 tests/prompt_strategies/test_kto_user_defined.py diff --git a/src/axolotl/prompt_strategies/base.py b/src/axolotl/prompt_strategies/base.py index 45a3ffda91..94a65aaa1f 100644 --- a/src/axolotl/prompt_strategies/base.py +++ b/src/axolotl/prompt_strategies/base.py @@ -29,6 +29,6 @@ def load(strategy, cfg, module_base=None, **kwargs): mod = importlib.import_module(strategy, module_base) func = getattr(mod, load_fn) return func(cfg, **kwargs) - except Exception: - LOG.warning(f"unable to load strategy {strategy}") + except Exception as exc: # pylint: disable=broad-exception-caught + LOG.warning(f"unable to load strategy {strategy}: {exc}", exc_info=True) return None diff --git a/src/axolotl/prompt_strategies/kto/user_defined.py b/src/axolotl/prompt_strategies/kto/user_defined.py index e26683cdea..0a52383773 100644 --- a/src/axolotl/prompt_strategies/kto/user_defined.py +++ b/src/axolotl/prompt_strategies/kto/user_defined.py @@ -15,14 +15,14 @@ def default(cfg, dataset_idx=0, **kwargs): field_label = ds_cfg.get("field_label", "label") prompt_format = ds_cfg.get("prompt_format") if not prompt_format: - prompt_format = "{" + field_prompt + "}" + prompt_format = "{prompt}" completion_format = ds_cfg.get("completion_format") if not completion_format: - chosen_format = "{" + field_completion + "}" + completion_format = "{completion}" def transform_fn(sample): if ( - "{" + field_system + "}" in prompt_format + "{system}" in prompt_format and field_system in sample and sample[field_system] ): @@ -30,8 +30,10 @@ def transform_fn(sample): system=sample[field_system], prompt=sample[field_prompt] ) else: - sample["prompt"] = prompt_format.format(prompt=sample["prompt"]) - sample["completion"] = chosen_format.format(chosen=sample[field_completion]) + sample["prompt"] = prompt_format.format(prompt=sample[field_prompt]) + sample["completion"] = completion_format.format( + completion=sample[field_completion] + ) sample["label"] = sample[field_label] return sample diff --git a/src/axolotl/utils/data/rl.py b/src/axolotl/utils/data/rl.py index c1d775cb42..f97f09e497 100644 --- a/src/axolotl/utils/data/rl.py +++ b/src/axolotl/utils/data/rl.py @@ -328,6 +328,13 @@ def _load_split(cfg: DictDefault, split: Literal["train", "test"]) -> Dataset: else: ds_transform_fn = load_dpo(_type, cfg, dataset_idx=i) + if ds_transform_fn is None: + raise ValueError( + f"Failed to load dataset transform function for type {_type!r} " + f"(rl: {cfg.rl}). See the warning logged above for the underlying " + "error." + ) + map_kwargs: dict[str, Any] = {} if isinstance(ds_transform_fn, tuple): ds_transform_fn, map_kwargs = ds_transform_fn diff --git a/src/axolotl/utils/schemas/datasets.py b/src/axolotl/utils/schemas/datasets.py index 97ed71631d..ca743d349f 100644 --- a/src/axolotl/utils/schemas/datasets.py +++ b/src/axolotl/utils/schemas/datasets.py @@ -280,7 +280,7 @@ class UserDefinedKTOType(BaseModel): field_system: str | None = None field_prompt: str | None = None field_completion: str | None = None - field_label: bool | None = None + field_label: str | None = None prompt_format: str | None = None completion_format: str | None = None diff --git a/tests/prompt_strategies/test_kto_user_defined.py b/tests/prompt_strategies/test_kto_user_defined.py new file mode 100644 index 0000000000..5d38f0417e --- /dev/null +++ b/tests/prompt_strategies/test_kto_user_defined.py @@ -0,0 +1,140 @@ +""" +Tests for user-defined KTO dataset transform strategies +""" + +import pytest + +from axolotl.prompt_strategies.kto import load as load_kto +from axolotl.prompt_strategies.kto.user_defined import default +from axolotl.utils.config import validate_config +from axolotl.utils.dict import DictDefault + + +def make_cfg(type_cfg: dict) -> DictDefault: + """Build a minimal config with a single user-defined KTO dataset.""" + return DictDefault({"datasets": [{"type": type_cfg}]}) + + +class TestKTOUserDefined: + """ + Test user_defined.default KTO transforms + """ + + def test_documented_config(self): + """Transform works with the exact config documented in docs/rlhf.qmd + (the same shape reported in issue #2757).""" + cfg = make_cfg( + { + "field_prompt": "prompt", + "field_system": "system", + "field_completion": "completion", + "field_label": "label", + "prompt_format": "{prompt}", + "completion_format": "{completion}", + } + ) + transform_fn = default(cfg) + sample = transform_fn({"prompt": "hello", "completion": "world", "label": True}) + assert sample["prompt"] == "hello" + assert sample["completion"] == "world" + assert sample["label"] is True + + def test_defaults_without_formats(self): + """Transform falls back to canonical {prompt}/{completion} formats when + no explicit formats are configured.""" + cfg = make_cfg({}) + transform_fn = default(cfg) + sample = transform_fn( + {"prompt": "hello", "completion": "world", "label": False} + ) + assert sample["prompt"] == "hello" + assert sample["completion"] == "world" + assert sample["label"] is False + + def test_custom_field_names(self): + """Transform reads from custom field_prompt/field_completion/field_label + columns and writes the canonical prompt/completion/label keys.""" + cfg = make_cfg( + { + "field_prompt": "question", + "field_completion": "answer", + "field_label": "is_good", + } + ) + transform_fn = default(cfg) + sample = transform_fn({"question": "hello", "answer": "world", "is_good": True}) + assert sample["prompt"] == "hello" + assert sample["completion"] == "world" + assert sample["label"] is True + + def test_system_in_prompt_format(self): + """A {system} placeholder in prompt_format is filled from the system + field when present in the sample.""" + cfg = make_cfg( + { + "prompt_format": "{system} {prompt}", + } + ) + transform_fn = default(cfg) + sample = transform_fn( + { + "system": "be helpful", + "prompt": "hello", + "completion": "world", + "label": True, + } + ) + assert sample["prompt"] == "be helpful hello" + assert sample["completion"] == "world" + + def test_non_dict_type_raises(self): + """A non-dict dataset type raises a clear ValueError.""" + cfg = make_cfg({}) + cfg["datasets"][0]["type"] = "user_defined.default" + with pytest.raises(ValueError, match="must be a dictionary"): + default(cfg) + + def test_load_kto_user_defined_returns_callable(self): + """The loader path resolves user_defined.default to a callable + transform for the documented config.""" + cfg = make_cfg( + { + "field_prompt": "prompt", + "field_completion": "completion", + "field_label": "label", + "prompt_format": "{prompt}", + "completion_format": "{completion}", + } + ) + transform_fn = load_kto("user_defined.default", cfg, dataset_idx=0) + assert callable(transform_fn) + + def test_documented_config_passes_validate_config(self): + """The documented config validates end-to-end: field_label is a column + name (str), which the schema previously rejected as a bool.""" + cfg = DictDefault( + { + "base_model": "TinyLlama/TinyLlama-1.1B-Chat-v0.6", + "learning_rate": 0.000001, + "micro_batch_size": 1, + "gradient_accumulation_steps": 1, + "remove_unused_columns": False, + "rl": "kto", + "datasets": [ + { + "path": "user/dataset", + "split": "train", + "type": { + "field_prompt": "prompt", + "field_system": "system", + "field_completion": "completion", + "field_label": "label", + "prompt_format": "{prompt}", + "completion_format": "{completion}", + }, + } + ], + } + ) + checked_cfg = validate_config(cfg) + assert checked_cfg["datasets"][0]["type"]["field_label"] == "label" From bc7e26538f470f103b900be67537cda5d5ff6a9d Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 16 Jun 2026 16:42:40 +0700 Subject: [PATCH 1358/1405] fix: fail early for CI if meet CUDA error (#3737) [skip ci] * fix: fail early for CI if meet CUDA error * fix: switch to clean abort --- tests/conftest.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 16a01f8aa3..5e3a9bd02c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,6 +53,35 @@ def _is_torch_fx_available(): ) +# A device-side assert / illegal access poisons the process-wide CUDA context, so +# every later GPU test errors at setup. Abort the session instead of cascading. +_CUDA_FATAL_MARKERS = ( + "device-side assert triggered", + "an illegal memory access was encountered", + "misaligned address", +) +_cuda_context_poisoned = False + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): # pylint: disable=unused-argument + outcome = yield + report = outcome.get_result() + global _cuda_context_poisoned # pylint: disable=global-statement + if report.failed and call.excinfo is not None: + if any(marker in str(call.excinfo.value) for marker in _CUDA_FATAL_MARKERS): + _cuda_context_poisoned = True + + +def pytest_runtest_setup(item): + if _cuda_context_poisoned: + item.session.shouldstop = ( + "CUDA context corrupted by an earlier test; aborting to avoid " + "cascading setup errors. Re-run the job." + ) + pytest.skip("CUDA context corrupted by an earlier test; aborting suite.") + + def retry_on_request_exceptions(max_retries=3, delay=1): def decorator(func): @functools.wraps(func) From e86163dd332f3aad2f29de8fe44b6cfd5f74b22d Mon Sep 17 00:00:00 2001 From: NanoCode012 Date: Tue, 16 Jun 2026 16:46:29 +0700 Subject: [PATCH 1359/1405] feat: add inference multi-turn chat interface (#3723) * feat: add interactive multi-turn chat mode (--chat) to inference CLI * fix: apply_chat_template returns BatchEncoding in transformers v5 * docs: document interactive chat mode for inference * feat: diffusion turn generation for chat mode; fix fp8 probe on CPU-only torch * feat: suggest command aliases on typo; save chat sessions in multimodal parts format * feat: collapse thinking blocks in chat with /expand, /think template toggle, thinking token stats * fix: suppress unauthenticated HF Hub nag warning in logging config * fix: harden chat REPL against interrupts and command errors; store assistant turns in parse_response format - Ctrl+C during a (diffusion) turn no longer crashes the REPL; the session survives and the user message is kept - exceptions in slash-command handlers no longer kill the session - consecutive user messages merge so strict templates never see two user turns after a failed generation - assistant turns are stored without special tokens, with thinking under reasoning_content (tokenizer parse_response schema when available, think-marker split otherwise); EOS markers no longer leak into the streamed display * perf(chat): lighten the turn loop - /new now drops the cross-turn KV cache instead of leaving it on device until the next generation - throttle live thinking-tail rerenders to the 12 Hz repaint rate (was O(n^2) splitlines over the full think text per chunk) - split think markers once per turn and reuse for counts and the stored message, dropping the redundant full decode * refactor(chat): share the live thinking-tail FPS as a class constant * fix: interrupt cache race condition and parse edge case --- docs/cli.qmd | 7 +- docs/inference.qmd | 70 ++ src/axolotl/cli/chat.py | 1279 +++++++++++++++++++++ src/axolotl/cli/config.py | 2 +- src/axolotl/cli/inference.py | 44 +- src/axolotl/cli/main.py | 15 +- src/axolotl/cli/utils/__init__.py | 3 +- src/axolotl/cli/utils/load.py | 27 + src/axolotl/logging_config.py | 19 + tests/cli/test_chat_repl.py | 668 +++++++++++ tests/cli/test_cli_inference.py | 39 + tests/test_logging_config_file_capture.py | 25 + 12 files changed, 2170 insertions(+), 28 deletions(-) create mode 100644 src/axolotl/cli/chat.py create mode 100644 tests/cli/test_chat_repl.py diff --git a/docs/cli.qmd b/docs/cli.qmd index 2bdaf90189..b44c5db23b 100644 --- a/docs/cli.qmd +++ b/docs/cli.qmd @@ -137,7 +137,8 @@ lora_alpha: ### inference -Runs inference using your trained model in either CLI or Gradio interface mode. +Runs inference using your trained model in CLI, interactive chat, or Gradio +interface mode. ```bash # CLI inference with LoRA @@ -146,6 +147,10 @@ axolotl inference config.yml --lora-model-dir="./outputs/lora-out" # CLI inference with full model axolotl inference config.yml --base-model="./completed-model" +# Interactive multi-turn chat (see the inference guide for commands) +axolotl inference config.yml --chat \ + --lora-model-dir="./outputs/lora-out" + # Gradio web interface axolotl inference config.yml --gradio \ --lora-model-dir="./outputs/lora-out" diff --git a/docs/inference.qmd b/docs/inference.qmd index 6917d3c330..e977e3c3c5 100644 --- a/docs/inference.qmd +++ b/docs/inference.qmd @@ -35,6 +35,76 @@ axolotl inference your_config.yml --base-model="./completed-model" ::: +### Interactive Chat {#sec-chat} + +For multi-turn testing of conversational models, use chat mode. The chat template +is resolved exactly as it was during training and re-applied to the full +conversation each turn: + +```{.bash} +axolotl inference your_config.yml --chat +``` + +Type a message to chat. End a line with `\` to continue typing on the next line. +Slash commands control the session: + +| Command | Aliases | Description | +|---------|---------|-------------| +| `/help` | `/?` | Show all commands | +| `/new` | `/clear`, `/reset` | Clear the conversation (keeps system prompt and parameters) | +| `/system [text\|clear]` | | Show, set, or clear the system prompt | +| `/set ` | | Set a generation parameter | +| `/status` | `/params` | Show model info and current settings | +| `/history` | | Show the conversation so far | +| `/retry` | `/regen` | Regenerate the last assistant reply | +| `/undo` | | Remove the last exchange | +| `/save [path]` | | Append the conversation as a `chat_template`-format JSONL sample | +| `/quit` | `/exit`, `/q` | Exit | + +Generation parameters can also be set directly, e.g. `/temperature 0.7` (or +`/temp 0.7`), `/top_p 0.9`, `/top_k 50`, `/max_tokens 512`, `/rep 1.05`, +`/seed 42`. Setting `temperature` to `0` switches to greedy decoding. + +Press `Ctrl+C` during generation to stop the current reply; the partial response +is kept in the conversation (diffusion replies denoise in one piece, so an +interrupted diffusion turn is discarded instead). + +#### Thinking Models {#sec-chat-thinking} + +Thinking blocks (e.g. `...`) stream live in a small dim window, +then collapse to a one-line summary — `/expand` shows the full reasoning of the +last reply, and `/collapse off` switches to raw verbatim output. The per-turn +stats split thinking from reply tokens. If the chat template supports a +render-time thinking toggle (e.g. Qwen's `enable_thinking`), `/think off` +disables thinking entirely from the next turn; `/think default` restores the +template default. + +::: {.callout-note} +Assistant turns are stored the way `transformers` recommends: special tokens +are stripped and thinking is kept on a separate `reasoning_content` key (via +the tokenizer's `parse_response` schema when it ships one, marker-splitting +otherwise), so the chat template decides how prior-turn reasoning is +re-rendered — matching what the model saw during training. The KV cache is +re-used across turns whenever the rendered conversation extends the previous +one, so long chats stay responsive. +::: + +`/save` writes conversations in the `messages` format accepted by +`type: chat_template` datasets, so a good interactive session can be turned +directly into training data. + +#### Diffusion Models {#sec-chat-diffusion} + +With the diffusion plugin enabled, chat mode generates each reply by appending +a masked block to the conversation and denoising it. Replies arrive in one +piece (no token streaming), and the parameter set changes accordingly: +`/tokens N` sets the completion block size, `/steps N` the number of denoising +steps, and `/temperature` the denoising temperature. Defaults come from the +`diffusion:` section of your config. + +Chat mode is not supported with `--prompter`; use the default inference mode +for legacy prompters. + ## Advanced Usage {#sec-advanced} ### Gradio Interface {#sec-gradio} diff --git a/src/axolotl/cli/chat.py b/src/axolotl/cli/chat.py new file mode 100644 index 0000000000..c54fe262d2 --- /dev/null +++ b/src/axolotl/cli/chat.py @@ -0,0 +1,1279 @@ +"""Interactive multi-turn chat CLI for a trained model.""" + +import difflib +import json +import shlex +import sys +import threading +import time +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any, Callable + +import torch +from rich.console import Console +from rich.live import Live +from rich.markup import escape +from rich.text import Text +from transformers import ( + DynamicCache, + GenerationConfig, + StoppingCriteria, + StoppingCriteriaList, + TextIteratorStreamer, +) + +from axolotl.cli.args import InferenceCliArgs +from axolotl.cli.utils import load_model_and_tokenizer, resolve_chat_template_str +from axolotl.integrations.base import PluginManager +from axolotl.telemetry.errors import send_errors +from axolotl.utils.dict import DictDefault +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +USER_PROMPT = ">>> " +CONTINUATION_PROMPT = "... " + +# marker pairs used by the bundled chat templates (qwen3/exaone4/phi_4 vs command_a) +THINK_MARKER_PAIRS: tuple[tuple[str, str], ...] = ( + ("", ""), + ("<|START_THINKING|>", "<|END_THINKING|>"), +) +DEFAULT_THINK_MARKERS = THINK_MARKER_PAIRS[0] + + +def detect_think_markers(chat_template_str: str | None) -> tuple[str, str]: + """Picks the thinking marker pair the template works with. Called once at startup.""" + if chat_template_str: + for pair in THINK_MARKER_PAIRS: + if pair[1] in chat_template_str: + return pair + return DEFAULT_THINK_MARKERS + + +def detect_think_toggle_key(chat_template_str: str | None) -> str | None: + """ + Finds the jinja variable the template uses to toggle thinking at render time + (`enable_thinking` in our bundled gemma4/qwen3_5 templates; `thinking` on some + hub templates). Called once at startup. + """ + if not chat_template_str: + return None + if "enable_thinking" in chat_template_str: + return "enable_thinking" + if "thinking" in chat_template_str: + return "thinking" + return None + + +@dataclass(frozen=True) +class GenParamSpec: + """Specification of a runtime-adjustable generation parameter.""" + + key: str + cast: Callable + lo: float + hi: float + default: Any + aliases: tuple[str, ...] = () + nullable: bool = False + help: str = "" + + +GEN_PARAMS: tuple[GenParamSpec, ...] = ( + GenParamSpec( + "temperature", float, 0.0, 5.0, 0.9, ("temp",), help="0 = greedy decoding" + ), + GenParamSpec("top_p", float, 0.0, 1.0, 0.95), + GenParamSpec("top_k", int, 0, 1000, 40), + GenParamSpec("min_p", float, 0.0, 1.0, None, nullable=True), + GenParamSpec("max_new_tokens", int, 1, 1_000_000, 1024, ("max_tokens", "max")), + GenParamSpec("repetition_penalty", float, 0.5, 3.0, 1.1, ("rep",)), + GenParamSpec( + "seed", int, 0, 2**32 - 1, None, nullable=True, help="`/set seed none` clears" + ), +) + + +DIFFUSION_GEN_PARAMS: tuple[GenParamSpec, ...] = ( + GenParamSpec( + "temperature", float, 0.0, 5.0, 0.0, ("temp",), help="0 = greedy denoising" + ), + GenParamSpec( + "max_new_tokens", + int, + 1, + 100_000, + 256, + ("tokens", "max_tokens", "max"), + help="size of the denoised completion block", + ), + GenParamSpec("steps", int, 1, 10_000, 128, help="number of denoising steps"), + GenParamSpec( + "seed", int, 0, 2**32 - 1, None, nullable=True, help="`/set seed none` clears" + ), +) + + +def default_gen_params( + specs: tuple[GenParamSpec, ...] = GEN_PARAMS, +) -> dict[str, Any]: + return {spec.key: spec.default for spec in specs} + + +def resolve_gen_param( + name: str, specs: tuple[GenParamSpec, ...] = GEN_PARAMS +) -> GenParamSpec | None: + for spec in specs: + if name == spec.key or name in spec.aliases: + return spec + return None + + +def parse_gen_param_value(spec: GenParamSpec, raw: str) -> Any: + if spec.nullable and raw.lower() in ("none", "null", "off"): + return None + try: + value = spec.cast(raw) + except ValueError as err: + raise ValueError(f"{spec.key} expects a {spec.cast.__name__}") from err + if not spec.lo <= value <= spec.hi: + raise ValueError(f"{spec.key} must be in [{spec.lo}, {spec.hi}]") + return value + + +def longest_common_prefix_len(a: list[int], b: list[int]) -> int: + n = min(len(a), len(b)) + for i in range(n): + if a[i] != b[i]: + return i + return n + + +def find_subsequence(haystack: list[int], needle: list[int], start: int = 0) -> int: + if not needle: + return -1 + n = len(needle) + for i in range(start, len(haystack) - n + 1): + if haystack[i : i + n] == needle: + return i + return -1 + + +def partial_suffix_len(text: str, marker: str) -> int: + """Length of the longest suffix of `text` that is a proper prefix of `marker`.""" + max_len = min(len(text), len(marker) - 1) + for k in range(max_len, 0, -1): + if text.endswith(marker[:k]): + return k + return 0 + + +def content_as_text(content: Any) -> str: + # parse_response may return content as a list of parts rather than a string + if isinstance(content, list): + return "".join( + part.get("text", "") for part in content if isinstance(part, dict) + ) + return content or "" + + +@dataclass +class ChatSession: + """Holds the conversation state for a chat session.""" + + messages: list[dict] = field(default_factory=list) + system: str | None = None + + def conversation(self) -> list[dict]: + prefix = [{"role": "system", "content": self.system}] if self.system else [] + return prefix + self.messages + + def add_user(self, content: str): + # merge into a trailing unanswered user message (e.g. after a failed + # generation) so strict templates never see consecutive user turns + if self.messages and self.messages[-1]["role"] == "user": + self.messages[-1]["content"] += "\n" + content + return + self.messages.append({"role": "user", "content": content}) + + def add_assistant(self, content: str): + self.add_assistant_message({"role": "assistant", "content": content}) + + def add_assistant_message(self, message: dict): + self.messages.append(message) + + def clear(self): + self.messages = [] + + def undo(self) -> bool: + """Removes the last user/assistant exchange. Returns False if empty.""" + if not self.messages: + return False + if self.messages[-1]["role"] == "assistant": + self.messages.pop() + if self.messages and self.messages[-1]["role"] == "user": + self.messages.pop() + return True + + def drop_last_assistant(self) -> bool: + """Removes the trailing assistant message so the turn can be retried.""" + if self.messages and self.messages[-1]["role"] == "assistant": + self.messages.pop() + return bool(self.messages) and self.messages[-1]["role"] == "user" + + def save_jsonl(self, path: str): + # content-parts format: text-only today, but matches the multimodal + # dataset format so saved sessions stay usable as training data + messages = [] + for message in self.conversation(): + content = message.get("content") + parts = ( + content + if isinstance(content, list) + else [{"type": "text", "text": content or ""}] + ) + out = { + "role": message["role"], + "content": parts, + } + for key in ("reasoning_content", "thinking", "tool_calls"): + if message.get(key): + out[key] = message[key] + messages.append(out) + with open(path, "a", encoding="utf-8") as file: + file.write(json.dumps({"messages": messages}, ensure_ascii=False) + "\n") + + +@dataclass +class TurnResult: + """Result of generating a single assistant turn.""" + + content: str + message: dict | None = None + interrupted: bool = False + prompt_tokens: int = 0 + reused_tokens: int = 0 + new_tokens: int = 0 + thinking_tokens: int = 0 + response_tokens: int = 0 + seconds: float = 0.0 + + +class _StopOnEvent(StoppingCriteria): + """Stops generation when the given event is set (e.g. on Ctrl+C).""" + + def __init__(self, event: threading.Event): + self.event = event + + def __call__(self, input_ids, scores, **kwargs) -> bool: + return self.event.is_set() + + +class TurnGenerator: + """Base for assistant-turn generators: template rendering and EOS handling.""" + + def __init__(self, model, tokenizer, chat_template_str: str | None, device): + self.model = model + self.tokenizer = tokenizer + self.chat_template_str = chat_template_str + self.device = device + self.think_markers = detect_think_markers( + chat_template_str or getattr(tokenizer, "chat_template", None) + ) + self._think_marker_ids: tuple[list[int], list[int]] | None = None + self._eos_strings: tuple[str, ...] | None = None + + self.eos_token_ids: set[int] = set() + if tokenizer.eos_token_id is not None: + self.eos_token_ids.add(tokenizer.eos_token_id) + config_eos = getattr(model.generation_config, "eos_token_id", None) + if isinstance(config_eos, int): + self.eos_token_ids.add(config_eos) + elif isinstance(config_eos, (list, tuple)): + self.eos_token_ids.update(config_eos) + + def render( + self, conversation: list[dict], render_kwargs: dict | None = None + ) -> list[int]: + kwargs = dict(render_kwargs or {}) + if self.chat_template_str: + kwargs["chat_template"] = self.chat_template_str + batch = self.tokenizer.apply_chat_template( + conversation, + add_generation_prompt=True, + tokenize=True, + return_dict=True, + **kwargs, + ) + return list(batch["input_ids"]) + + def _split_think_token_ids( + self, generated: list[int] + ) -> tuple[list[int], list[int]]: + """Splits a generated sequence into (thinking, response) token ids.""" + if self._think_marker_ids is None: + try: + self._think_marker_ids = ( + self.tokenizer.encode( + self.think_markers[0], add_special_tokens=False + ), + self.tokenizer.encode( + self.think_markers[1], add_special_tokens=False + ), + ) + except Exception: # pylint: disable=broad-exception-caught + self._think_marker_ids = ([], []) + + open_ids, close_ids = self._think_marker_ids + i = find_subsequence(generated, open_ids) + if i < 0: + return [], generated + j = find_subsequence(generated, close_ids, i + len(open_ids)) + if j < 0: + return generated[i + len(open_ids) :], generated[:i] + thinking = generated[i + len(open_ids) : j] + response = generated[:i] + generated[j + len(close_ids) :] + return thinking, response + + def split_think_token_counts(self, generated: list[int]) -> tuple[int, int]: + """Returns (thinking, response) token counts for a generated sequence.""" + thinking, response = self._split_think_token_ids(generated) + return len(thinking), len(response) + + def build_assistant_message( + self, + generated: list[int], + split: tuple[list[int], list[int]] | None = None, + ) -> dict: + """ + Parses generated token ids into an assistant message dict. Prefers the + tokenizer's own `parse_response` schema (transformers v5); otherwise splits + thinking out of the content by marker and stores it under + `reasoning_content`, the key the bundled chat templates read. Special + tokens are kept out of the stored text either way — the template re-adds + them on render. + """ + if getattr(self.tokenizer, "response_schema", None): + try: + text = self.tokenizer.decode(generated, skip_special_tokens=False) + message = self.tokenizer.parse_response(text) + if isinstance(message, dict): + message.setdefault("role", "assistant") + message.setdefault("content", "") + return message + except Exception: # pylint: disable=broad-exception-caught + LOG.warning( + "tokenizer.parse_response failed; falling back to marker split", + exc_info=True, + ) + + thinking_ids, response_ids = ( + split if split is not None else self._split_think_token_ids(generated) + ) + parsed: dict[str, Any] = { + "role": "assistant", + "content": self.tokenizer.decode( + response_ids, skip_special_tokens=True + ).strip(), + } + if thinking_ids: + parsed["reasoning_content"] = self.tokenizer.decode( + thinking_ids, skip_special_tokens=True + ).strip() + return parsed + + def eos_strings(self) -> tuple[str, ...]: + if self._eos_strings is None: + self._eos_strings = tuple( + text + for token_id in sorted(self.eos_token_ids) + if (text := self.tokenizer.decode([token_id])) + ) + return self._eos_strings + + def generate_turn( + self, + conversation: list[dict], + params: dict[str, Any], + on_text: Callable[[str], None], + render_kwargs: dict | None = None, + ) -> TurnResult: + raise NotImplementedError + + +class EosTextTrimmer: + """ + Filters streamed text so terminal EOS markers (e.g. `<|im_end|>`) never reach + the display. Text that could be the start of an EOS string is held back until + disambiguated by the next chunk. + """ + + def __init__(self, eos_strings: tuple[str, ...], emit: Callable[[str], None]): + self.eos_strings = tuple(s for s in eos_strings if s) + self.emit = emit + self.pending = "" + self.done = False + + def feed(self, text: str): + if self.done or not text: + return + self.pending += text + positions = [ + idx for s in self.eos_strings if (idx := self.pending.find(s)) >= 0 + ] + if positions: + if min(positions) > 0: + self.emit(self.pending[: min(positions)]) + self.pending = "" + self.done = True + return + hold = max( + (partial_suffix_len(self.pending, s) for s in self.eos_strings), + default=0, + ) + if len(self.pending) > hold: + self.emit(self.pending[: len(self.pending) - hold]) + self.pending = self.pending[len(self.pending) - hold :] + + def finish(self): + if not self.done and self.pending: + self.emit(self.pending) + self.pending = "" + + +class CausalTurnGenerator(TurnGenerator): + """ + Generates assistant turns with `model.generate`, re-using the KV cache across + turns when the rendered conversation extends the previously cached tokens. + """ + + def __init__(self, model, tokenizer, chat_template_str: str | None, device): + super().__init__(model, tokenizer, chat_template_str, device) + self._cache: DynamicCache | None = None + self._cached_ids: list[int] = [] + + def reset_cache(self): + self._cache = None + self._cached_ids = [] + + def _new_cache(self) -> DynamicCache: + return DynamicCache(config=self.model.config) + + def _prepare_cache(self, ids: list[int]) -> int: + """ + Crops or resets the cross-turn cache so it holds a strict prefix of `ids`. + Chat templates may rewrite earlier turns when re-rendering (e.g. stripping + prior-turn thinking blocks), so reuse is gated on a token-level prefix + check rather than assumed. + + Returns the number of re-used prefix tokens. + """ + common = longest_common_prefix_len(self._cached_ids, ids) + # generate() needs at least one uncached input token + keep = min(common, len(ids) - 1) + + if self._cache is None or keep <= 0: + self._cache = self._new_cache() + self._cached_ids = [] + return 0 + + if keep < len(self._cached_ids): + try: + self._cache.crop(keep) + self._cached_ids = self._cached_ids[:keep] + except Exception: # pylint: disable=broad-exception-caught + # some cache layer types (e.g. sliding window) cannot crop + self._cache = self._new_cache() + self._cached_ids = [] + return 0 + + return len(self._cached_ids) + + def _build_generation_config(self, params: dict[str, Any]) -> GenerationConfig: + do_sample = params["temperature"] > 0 + kwargs: dict[str, Any] = { + "max_new_tokens": params["max_new_tokens"], + "repetition_penalty": params["repetition_penalty"], + "do_sample": do_sample, + "bos_token_id": self.tokenizer.bos_token_id, + "eos_token_id": sorted(self.eos_token_ids) or None, + "pad_token_id": self.tokenizer.pad_token_id, + "use_cache": True, + "return_dict_in_generate": True, + } + if do_sample: + kwargs["temperature"] = params["temperature"] + kwargs["top_p"] = params["top_p"] + kwargs["top_k"] = params["top_k"] + if params["min_p"] is not None: + kwargs["min_p"] = params["min_p"] + return GenerationConfig(**kwargs) + + def generate_turn( + self, + conversation: list[dict], + params: dict[str, Any], + on_text: Callable[[str], None], + render_kwargs: dict | None = None, + ) -> TurnResult: + ids = self.render(conversation, render_kwargs) + reused = self._prepare_cache(ids) + cache = self._cache + assert cache is not None + + if params["seed"] is not None: + torch.manual_seed(params["seed"]) + + input_ids = torch.tensor([ids], dtype=torch.long, device=self.device) + attention_mask = torch.ones_like(input_ids) + generation_config = self._build_generation_config(params) + + stop_event = threading.Event() + streamer = TextIteratorStreamer( + self.tokenizer, skip_prompt=True, skip_special_tokens=False + ) + holder: dict[str, Any] = {} + + def _worker(): + try: + with torch.no_grad(): + holder["output"] = self.model.generate( + input_ids=input_ids, + attention_mask=attention_mask, + generation_config=generation_config, + streamer=streamer, + stopping_criteria=StoppingCriteriaList( + [_StopOnEvent(stop_event)] + ), + past_key_values=cache, + ) + except Exception as err: # pylint: disable=broad-exception-caught + holder["error"] = err + streamer.end() + + start = time.monotonic() + thread = threading.Thread(target=_worker, daemon=True) + thread.start() + + trimmer = EosTextTrimmer(self.eos_strings(), on_text) + interrupted = False + try: + for text in streamer: + trimmer.feed(text) + except KeyboardInterrupt: + interrupted = True + stop_event.set() + for text in streamer: + trimmer.feed(text) + finally: + # a 2nd Ctrl+C can escape the drain; join so the worker stops writing the cache + stop_event.set() + thread.join() + trimmer.finish() + seconds = time.monotonic() - start + + if "error" in holder: + self.reset_cache() + raise holder["error"] + + sequence = holder["output"].sequences[0].tolist() + self._cached_ids = sequence[: cache.get_seq_length()] + + generated = sequence[len(ids) :] + while generated and generated[-1] in self.eos_token_ids: + generated.pop() + thinking_ids, response_ids = self._split_think_token_ids(generated) + message = self.build_assistant_message(generated, (thinking_ids, response_ids)) + + return TurnResult( + content=message.get("content") or "", + message=message, + interrupted=interrupted, + prompt_tokens=len(ids), + reused_tokens=reused, + new_tokens=len(generated), + thinking_tokens=len(thinking_ids), + response_tokens=len(response_ids), + seconds=seconds, + ) + + +class DiffusionTurnGenerator(TurnGenerator): + """ + Generates assistant turns for diffusion LMs by appending a masked completion + block to the rendered conversation and denoising it. The whole block resolves + at once, so the reply is emitted in one piece rather than streamed. + """ + + def __init__( + self, model, tokenizer, chat_template_str: str | None, device, mask_token_id + ): + super().__init__(model, tokenizer, chat_template_str, device) + self.mask_token_id = int(mask_token_id) + + def generate_turn( + self, + conversation: list[dict], + params: dict[str, Any], + on_text: Callable[[str], None], + render_kwargs: dict | None = None, + ) -> TurnResult: + from axolotl.integrations.diffusion import generate as diffusion_generate + + ids = self.render(conversation, render_kwargs) + if params["seed"] is not None: + torch.manual_seed(params["seed"]) + + sequence = torch.tensor([ids], dtype=torch.long, device=self.device) + + start = time.monotonic() + with torch.no_grad(): + result = diffusion_generate( + self.model, + self.tokenizer, + original_sequence=sequence, + num_diffusion_steps=params["steps"], + temperature=params["temperature"], + mask_token_id=self.mask_token_id, + mode="completion", + completion_tokens=params["max_new_tokens"], + ) + seconds = time.monotonic() - start + + generated = result["generated_ids"][len(ids) :] + for i, token_id in enumerate(generated): + if token_id in self.eos_token_ids: + generated = generated[:i] + break + content = self.tokenizer.decode(generated, skip_special_tokens=False) + on_text(content) + thinking_ids, response_ids = self._split_think_token_ids(generated) + + return TurnResult( + content=content, + message=self.build_assistant_message( + generated, (thinking_ids, response_ids) + ), + prompt_tokens=len(ids), + new_tokens=len(generated), + thinking_tokens=len(thinking_ids), + response_tokens=len(response_ids), + seconds=seconds, + ) + + +class ThinkStreamRenderer: + """ + Renders one streamed turn. When collapsing, thinking is shown as a rolling + dim tail in a live region (so it never enters scrollback) and replaced by a + one-line summary when the block closes; the reply streams normally. When + collapse is off, this is a plain passthrough print. + """ + + LIVE_FPS = 12 + + def __init__( + self, + console: Console, + collapse: bool, + markers: tuple[str, str] = DEFAULT_THINK_MARKERS, + tail_lines: int = 6, + ): + self.console = console + self.collapse = collapse + self.open_marker, self.close_marker = markers + self.tail_lines = tail_lines + self.think_text = "" + self._mode = "detect" + self._pending = "" + self._start = time.monotonic() + self._live: Live | None = None + self._last_live_update = 0.0 + + def feed(self, text: str): + if not self.collapse: + print(text, end="", flush=True) + return + self._pending += text + self._process() + + def finish(self, interrupted: bool = False): + if not self.collapse: + return + if self._mode == "think": + self.think_text += self._pending + self._pending = "" + reason = "interrupted" if interrupted else f"no {self.close_marker}" + self._end_think(f" ({escape(reason)})") + elif self._pending: + print(self._pending, end="", flush=True) + self._pending = "" + + def _process(self): + while True: + if self._mode == "detect": + stripped = self._pending.lstrip() + if stripped.startswith(self.open_marker): + idx = self._pending.find(self.open_marker) + self._pending = self._pending[idx + len(self.open_marker) :] + self._mode = "think" + self._live = Live( + Text(""), + console=self.console, + refresh_per_second=self.LIVE_FPS, + transient=True, + ) + self._live.start() + continue + if not stripped or self.open_marker.startswith(stripped): + return # could still be a marker prefix; wait for more text + self._mode = "reply" + continue + + if self._mode == "think": + idx = self._pending.find(self.close_marker) + if idx >= 0: + self.think_text += self._pending[:idx] + self._pending = self._pending[ + idx + len(self.close_marker) : + ].lstrip("\n") + self._end_think() + self._mode = "reply" + continue + keep = partial_suffix_len(self._pending, self.close_marker) + emit_until = len(self._pending) - keep + self.think_text += self._pending[:emit_until] + self._pending = self._pending[emit_until:] + self._update_live() + return + + # reply mode + if self._pending: + print(self._pending, end="", flush=True) + self._pending = "" + return + + def _update_live(self): + if self._live is None: + return + # Live repaints at LIVE_FPS; building renderables faster than that is wasted + now = time.monotonic() + if now - self._last_live_update < 1 / self.LIVE_FPS: + return + self._last_live_update = now + lines = self.think_text.splitlines()[-self.tail_lines :] + self._live.update(Text("\n".join(lines), style="dim")) + + def _end_think(self, note: str = ""): + if self._live is not None: + self._live.stop() + self._live = None + seconds = time.monotonic() - self._start + self.console.print( + f"[dim]▸ thought for {seconds:.1f}s{note} · /expand to view[/dim]" + ) + + +@dataclass(frozen=True) +class Command: + """A slash command with its aliases and handler.""" + + name: str + handler: str + help: str + aliases: tuple[str, ...] = () + usage: str = "" + + +COMMANDS: tuple[Command, ...] = ( + Command("help", "cmd_help", "show this help", ("?",)), + Command( + "new", + "cmd_new", + "clear the conversation (keeps system prompt and params)", + ("clear", "reset"), + ), + Command( + "system", + "cmd_system", + "show, set, or clear the system prompt", + usage="/system [text|clear]", + ), + Command( + "set", + "cmd_set", + "set a generation parameter", + usage="/set ", + ), + Command("status", "cmd_status", "show model and generation settings", ("params",)), + Command("history", "cmd_history", "show the conversation so far"), + Command("retry", "cmd_retry", "regenerate the last assistant reply", ("regen",)), + Command("undo", "cmd_undo", "remove the last exchange"), + Command( + "save", + "cmd_save", + "append conversation as a chat_template-format JSONL sample", + usage="/save [path]", + ), + Command( + "think", + "cmd_think", + "toggle template-level thinking, if the template supports it", + usage="/think [on|off|default]", + ), + Command( + "collapse", + "cmd_collapse", + "collapse thinking blocks in the display", + usage="/collapse [on|off]", + ), + Command("expand", "cmd_expand", "show the hidden thinking from the last reply"), + Command("quit", "cmd_quit", "exit chat", ("exit", "q")), +) + + +def resolve_command(name: str) -> Command | None: + for command in COMMANDS: + if name == command.name or name in command.aliases: + return command + return None + + +class ChatRepl: + """Interactive chat loop: slash commands plus streamed model turns.""" + + def __init__( + self, + *, + generator: TurnGenerator, + session: ChatSession | None = None, + params: dict[str, Any] | None = None, + param_specs: tuple[GenParamSpec, ...] = GEN_PARAMS, + console: Console | None = None, + banner: dict[str, str] | None = None, + input_fn: Callable[[str], str] | None = None, + think_toggle_key: str | None = None, + collapse_thinking: bool = True, + ): + self.generator = generator + self.session = session or ChatSession() + self.param_specs = param_specs + self.params = params or default_gen_params(param_specs) + self.console = console or Console() + self.banner = banner or {} + self.input_fn = input_fn or input + self.think_toggle_key = think_toggle_key + self.collapse_thinking = collapse_thinking + self.render_kwargs: dict[str, Any] = {} + self.last_think_text: str | None = None + + def run(self): + self._print_banner() + while True: + try: + line = self._read_line() + except EOFError: + break + except KeyboardInterrupt: + self.console.print("\n[dim]Use /quit to exit.[/dim]") + continue + + line = line.strip() + if not line: + continue + + if line.startswith("/"): + try: + action = self._dispatch(line) + except Exception as err: # pylint: disable=broad-exception-caught + self.console.print(f"[red]Command failed: {escape(str(err))}[/red]") + continue + if action == "quit": + break + if action == "regenerate": + self._generate_turn() + continue + + self.session.add_user(line) + self._generate_turn() + + def _read_line(self) -> str: + parts = [] + prompt = USER_PROMPT + while True: + line = self.input_fn(prompt) + if line.endswith("\\"): + parts.append(line[:-1]) + prompt = CONTINUATION_PROMPT + continue + parts.append(line) + break + return "\n".join(parts) + + def _dispatch(self, line: str) -> str | None: + name, _, args = line[1:].partition(" ") + name = name.lower() + args = args.strip() + + command = resolve_command(name) + if command: + return getattr(self, command.handler)(args) + + # bare parameter shortcuts: /temp 0.7, /top_p 0.9, ... + spec = resolve_gen_param(name, self.param_specs) + if spec: + return self.cmd_set(f"{spec.key} {args}" if args else spec.key) + + candidates = [ + alias for command in COMMANDS for alias in (command.name, *command.aliases) + ] + [alias for spec in self.param_specs for alias in (spec.key, *spec.aliases)] + close = difflib.get_close_matches(name, candidates, n=1) + hint = f" Did you mean /{close[0]}?" if close else "" + self.console.print(f"[red]Unknown command /{name}.[/red]{hint}") + return None + + def _generate_turn(self): + renderer = ThinkStreamRenderer( + self.console, + collapse=self.collapse_thinking, + markers=getattr(self.generator, "think_markers", DEFAULT_THINK_MARKERS), + ) + try: + result = self.generator.generate_turn( + self.session.conversation(), + self.params, + renderer.feed, + render_kwargs=self.render_kwargs or None, + ) + except KeyboardInterrupt: + # an escaped interrupt leaves the cache out of sync with _cached_ids + reset_cache = getattr(self.generator, "reset_cache", None) + if callable(reset_cache): + reset_cache() + renderer.finish(interrupted=True) + print() + self.console.print( + "[dim]Interrupted; reply discarded. Your message is kept —" + " /retry regenerates, /undo removes it.[/dim]" + ) + return + except Exception as err: # pylint: disable=broad-exception-caught + renderer.finish(interrupted=True) + self.console.print(f"\n[red]Generation failed: {escape(str(err))}[/red]") + self.console.print( + "[dim]Your message is kept — /retry regenerates, /undo removes it.[/dim]" + ) + return + + renderer.finish(interrupted=result.interrupted) + message = result.message or {"role": "assistant", "content": result.content} + self.last_think_text = ( + renderer.think_text.strip() + or message.get("reasoning_content") + or message.get("thinking") + or None + ) + + print() + self.session.add_assistant_message(message) + token_summary = f"{result.new_tokens} tokens" + if result.thinking_tokens: + token_summary += ( + f" ({result.thinking_tokens} thinking · {result.response_tokens} reply)" + ) + stats = ( + f"{token_summary} · {result.seconds:.1f}s · " + f"{result.prompt_tokens} prompt ({result.reused_tokens} cached)" + ) + if result.interrupted: + stats += " · interrupted (partial reply kept; /retry to regenerate)" + self.console.print(f"[dim]{stats}[/dim]") + + def _print_banner(self): + self.console.print("[bold]axolotl chat[/bold]") + for key, value in self.banner.items(): + self.console.print(f"[dim]{key}:[/dim] {escape(value)}") + self.console.print( + "[dim]Type a message to chat, /help for commands, \\ at line end to" + " continue on the next line.[/dim]" + ) + + # --- command handlers (return "quit", "regenerate", or None) --- + + def cmd_help(self, _args: str) -> None: + for command in COMMANDS: + names = "/" + command.name + if command.aliases: + names += " (" + ", ".join("/" + a for a in command.aliases) + ")" + usage = f" — {command.usage}" if command.usage else "" + self.console.print(f" [bold]{names}[/bold]: {command.help}{usage}") + params = ", ".join( + "/" + s.key + ("".join(f" /{a}" for a in s.aliases)) + for s in self.param_specs + ) + self.console.print(f" parameter shortcuts: {params}") + return None + + def cmd_new(self, _args: str) -> None: + self.session.clear() + self.last_think_text = None + reset_cache = getattr(self.generator, "reset_cache", None) + if callable(reset_cache): + reset_cache() + self.console.print("[dim]Conversation cleared.[/dim]") + return None + + def cmd_system(self, args: str) -> None: + if not args: + if self.session.system: + self.console.print(escape(self.session.system)) + else: + self.console.print("[dim]No system prompt set.[/dim]") + return None + if args.lower() == "clear": + self.session.system = None + self.console.print("[dim]System prompt cleared.[/dim]") + return None + self.session.system = args + self.console.print("[dim]System prompt set.[/dim]") + return None + + def cmd_set(self, args: str) -> str | None: + tokens = args.replace("=", " ").split() + if len(tokens) != 2: + self.console.print("[red]Usage: /set [/red]") + return None + spec = resolve_gen_param(tokens[0].lower(), self.param_specs) + if not spec: + valid = ", ".join(s.key for s in self.param_specs) + self.console.print(f"[red]Unknown parameter. Valid: {valid}[/red]") + return None + try: + self.params[spec.key] = parse_gen_param_value(spec, tokens[1]) + except ValueError as err: + self.console.print(f"[red]{err}[/red]") + return None + self.console.print(f"[dim]{spec.key} = {self.params[spec.key]}[/dim]") + return None + + def cmd_status(self, _args: str) -> None: + for key, value in self.banner.items(): + self.console.print(f"[dim]{key}:[/dim] {escape(value)}") + for spec in self.param_specs: + self.console.print(f"[dim]{spec.key}:[/dim] {self.params[spec.key]}") + n_messages = len(self.session.messages) + self.console.print(f"[dim]messages:[/dim] {n_messages}") + return None + + def cmd_history(self, _args: str) -> None: + conversation = self.session.conversation() + if not conversation: + self.console.print("[dim]No messages yet.[/dim]") + return None + for message in conversation: + self.console.print(f"[bold]{message['role']}:[/bold]") + reasoning = message.get("reasoning_content") or message.get("thinking") + if reasoning: + self.console.print(escape(content_as_text(reasoning)), style="dim") + self.console.print(escape(content_as_text(message.get("content")))) + return None + + def cmd_retry(self, _args: str) -> str | None: + if not self.session.drop_last_assistant(): + self.console.print("[dim]Nothing to retry yet.[/dim]") + return None + return "regenerate" + + def cmd_undo(self, _args: str) -> None: + if self.session.undo(): + self.last_think_text = None + self.console.print("[dim]Removed last exchange.[/dim]") + else: + self.console.print("[dim]Nothing to undo.[/dim]") + return None + + def cmd_save(self, args: str) -> None: + if not self.session.messages: + self.console.print("[dim]Nothing to save yet.[/dim]") + return None + path = ( + shlex.split(args)[0] + if args + else f"chat-{datetime.now().strftime('%Y%m%d-%H%M%S')}.jsonl" + ) + self.session.save_jsonl(path) + self.console.print(f"[dim]Saved conversation to {path}[/dim]") + return None + + def cmd_think(self, args: str) -> None: + if not args: + current = self.render_kwargs.get(self.think_toggle_key or "", "default") + self.console.print(f"[dim]template thinking: {current}[/dim]") + return None + if not self.think_toggle_key: + self.console.print( + "[dim]This chat template has no thinking toggle; thinking is" + " controlled by the model/template itself.[/dim]" + ) + return None + value = args.lower() + if value in ("on", "true"): + self.render_kwargs[self.think_toggle_key] = True + elif value in ("off", "false"): + self.render_kwargs[self.think_toggle_key] = False + elif value == "default": + self.render_kwargs.pop(self.think_toggle_key, None) + else: + self.console.print("[red]Usage: /think [on|off|default][/red]") + return None + current = self.render_kwargs.get(self.think_toggle_key, "default") + self.console.print( + f"[dim]{self.think_toggle_key} = {current} (applies from next turn)[/dim]" + ) + return None + + def cmd_collapse(self, args: str) -> None: + value = args.lower() + if value in ("on", "true", ""): + self.collapse_thinking = True + elif value in ("off", "false"): + self.collapse_thinking = False + else: + self.console.print("[red]Usage: /collapse [on|off][/red]") + return None + state = "collapsed" if self.collapse_thinking else "shown raw" + self.console.print(f"[dim]Thinking blocks will be {state}.[/dim]") + return None + + def cmd_expand(self, _args: str) -> None: + if self.last_think_text: + self.console.print(escape(self.last_think_text), style="dim") + else: + self.console.print( + "[dim]No hidden thinking recorded for the last reply.[/dim]" + ) + return None + + def cmd_quit(self, _args: str) -> str: + return "quit" + + +def _build_banner(cfg: DictDefault) -> dict[str, str]: + banner = {"model": str(cfg.base_model)} + + if cfg.lora_model_dir: + banner["adapter"] = f"{cfg.adapter or 'lora'} from {cfg.lora_model_dir}" + elif cfg.adapter: + banner["adapter"] = str(cfg.adapter) + + quant = [] + if cfg.load_in_4bit: + quant.append("4-bit (bnb)") + if cfg.load_in_8bit: + quant.append("8-bit (bnb)") + if cfg.qat: + quant.append("QAT fake-quant active") + if quant: + banner["quantization"] = ", ".join(quant) + + if cfg.chat_template: + template_name = getattr(cfg.chat_template, "value", cfg.chat_template) + banner["chat template"] = f"config ({template_name})" + elif cfg.datasets and cfg.datasets[0].type == "chat_template": + banner["chat template"] = "dataset config" + else: + banner["chat template"] = "tokenizer default" + + return banner + + +@send_errors +def do_chat( + *, + cfg: DictDefault, + cli_args: InferenceCliArgs, +): + """ + Runs an interactive multi-turn chat session on the command line. The chat + template is applied to the full conversation each turn, and generation + parameters can be adjusted at runtime via slash commands. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + cli_args: Inference-specific CLI arguments. + """ + if cli_args.prompter: + raise ValueError( + "--chat does not support --prompter; legacy prompters are single-turn." + " Use the default inference mode instead." + ) + + plugin_manager = PluginManager.get_instance() + is_diffusion = any( + plugin.__class__.__name__ == "DiffusionPlugin" + for plugin in plugin_manager.plugins.values() + ) + + if not sys.stdin.isatty(): + raise ValueError( + "--chat requires an interactive terminal. For piped input, use the" + " default inference mode." + ) + + try: + import readline # noqa: F401 pylint: disable=unused-import + except ImportError: + pass + + model, tokenizer, _ = load_model_and_tokenizer(cfg=cfg, inference=True) + if cfg.is_multimodal: + LOG.warning( + "Multimodal attachments are not supported in chat mode yet;" + " proceeding with text-only chat." + ) + + chat_template_str = resolve_chat_template_str(cfg, tokenizer) + if not chat_template_str and not tokenizer.chat_template: + raise ValueError( + "Chat mode requires a chat template. Set `chat_template` in your config" + " or use a tokenizer that provides one." + ) + + model = model.to(cfg.device, dtype=cfg.torch_dtype) + model.eval() + + banner = _build_banner(cfg) + generator: TurnGenerator + param_specs = GEN_PARAMS + + if is_diffusion: + from axolotl.integrations.diffusion import resolve_mask_token_id + + mask_token_id = resolve_mask_token_id(tokenizer, cfg, allow_add=False) + generator = DiffusionTurnGenerator( + model, tokenizer, chat_template_str, cfg.device, mask_token_id + ) + param_specs = DIFFUSION_GEN_PARAMS + params = default_gen_params(param_specs) + if cfg.diffusion.num_diffusion_steps: + params["steps"] = cfg.diffusion.num_diffusion_steps + if cfg.diffusion.generation_temperature is not None: + params["temperature"] = cfg.diffusion.generation_temperature + banner["mode"] = "diffusion (completion-block denoising)" + else: + generator = CausalTurnGenerator(model, tokenizer, chat_template_str, cfg.device) + params = default_gen_params(param_specs) + + think_toggle_key = detect_think_toggle_key( + chat_template_str or tokenizer.chat_template + ) + repl = ChatRepl( + generator=generator, + params=params, + param_specs=param_specs, + banner=banner, + think_toggle_key=think_toggle_key, + ) + repl.run() diff --git a/src/axolotl/cli/config.py b/src/axolotl/cli/config.py index 123e093c0c..aa9efe50fb 100644 --- a/src/axolotl/cli/config.py +++ b/src/axolotl/cli/config.py @@ -342,7 +342,7 @@ def compute_supports_fp8() -> bool: try: compute_capability = torch.cuda.get_device_capability() return compute_capability >= (9, 0) - except RuntimeError: + except (RuntimeError, AssertionError): return False diff --git a/src/axolotl/cli/inference.py b/src/axolotl/cli/inference.py index cafa0f4eff..c6fd8d1dd8 100644 --- a/src/axolotl/cli/inference.py +++ b/src/axolotl/cli/inference.py @@ -13,16 +13,13 @@ from axolotl.cli.args import InferenceCliArgs from axolotl.cli.config import load_cfg -from axolotl.cli.utils import load_model_and_tokenizer +from axolotl.cli.utils import load_model_and_tokenizer, resolve_chat_template_str from axolotl.cli.utils.diffusion import ( diffusion_inference, launch_diffusion_gradio_ui, ) from axolotl.integrations.base import PluginManager from axolotl.telemetry.errors import send_errors -from axolotl.utils.chat_templates import ( - get_chat_template_from_config, -) from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger @@ -70,14 +67,8 @@ def do_inference( prompter_module = getattr( importlib.import_module("axolotl.prompters"), prompter ) - elif cfg.chat_template: - chat_template_str = get_chat_template_from_config( - cfg, ds_cfg=None, tokenizer=tokenizer - ) - elif cfg.datasets and cfg.datasets[0].type == "chat_template": - chat_template_str = get_chat_template_from_config( - cfg=cfg, ds_cfg=cfg.datasets[0], tokenizer=tokenizer - ) + else: + chat_template_str = resolve_chat_template_str(cfg, tokenizer) model = model.to(cfg.device, dtype=cfg.torch_dtype) @@ -190,14 +181,8 @@ def do_inference_gradio( prompter_module = getattr( importlib.import_module("axolotl.prompters"), prompter ) - elif cfg.chat_template: - chat_template_str = get_chat_template_from_config( - cfg, ds_cfg=None, tokenizer=tokenizer - ) - elif cfg.datasets and cfg.datasets[0].type == "chat_template": - chat_template_str = get_chat_template_from_config( - cfg=cfg, ds_cfg=cfg.datasets[0], tokenizer=tokenizer - ) + else: + chat_template_str = resolve_chat_template_str(cfg, tokenizer) model = model.to(cfg.device, dtype=cfg.torch_dtype) @@ -297,13 +282,19 @@ def generate(instruction): def do_cli( - config: Union[Path, str] = Path("examples/"), gradio: bool = False, **kwargs + config: Union[Path, str] = Path("examples/"), + gradio: bool = False, + chat: bool = False, + **kwargs, ) -> None: """ - Parses axolotl config, CLI args, and calls `do_inference` or `do_inference_gradio`. + Parses axolotl config, CLI args, and calls `do_inference`, `do_inference_gradio`, + or `do_chat`. Args: config: Path to `axolotl` config YAML file. + gradio: Whether to launch the Gradio browser interface. + chat: Whether to launch the interactive multi-turn chat interface. kwargs: Additional keyword arguments to override config file values. """ @@ -314,7 +305,14 @@ def do_cli( return_remaining_strings=True ) - if gradio: + if gradio and chat: + raise ValueError("--gradio and --chat are mutually exclusive.") + + if chat: + from axolotl.cli.chat import do_chat + + do_chat(cfg=parsed_cfg, cli_args=parsed_cli_args) + elif gradio: do_inference_gradio(cfg=parsed_cfg, cli_args=parsed_cli_args) else: do_inference(cfg=parsed_cfg, cli_args=parsed_cli_args) diff --git a/src/axolotl/cli/main.py b/src/axolotl/cli/main.py index cca6481e6e..13dcc4b77b 100644 --- a/src/axolotl/cli/main.py +++ b/src/axolotl/cli/main.py @@ -191,11 +191,16 @@ def evaluate(ctx: click.Context, config: str, launcher: str, **kwargs): help="Launcher to use for multi-GPU inference", ) @click.option("--gradio", is_flag=True, help="Launch Gradio interface") +@click.option( + "--chat", is_flag=True, help="Launch interactive multi-turn chat interface" +) @add_options_from_dataclass(TrainerCliArgs) @add_options_from_config(AxolotlInputConfig) @filter_none_kwargs @click.pass_context -def inference(ctx: click.Context, config: str, launcher: str, gradio: bool, **kwargs): +def inference( + ctx: click.Context, config: str, launcher: str, gradio: bool, chat: bool, **kwargs +): """ Run inference with a trained model. @@ -204,9 +209,13 @@ def inference(ctx: click.Context, config: str, launcher: str, gradio: bool, **kw config: Path to `axolotl` config YAML file. launcher: Launcher to use for multi-GPU inference ("accelerate", "torchrun", or "python"). gradio: Whether to use Gradio browser interface or command line for inference. + chat: Whether to use the interactive multi-turn chat interface. kwargs: Additional keyword arguments which correspond to CLI args or `axolotl` config options. """ + if gradio and chat: + raise click.UsageError("--gradio and --chat are mutually exclusive.") + # Extract launcher args from extra args (after --) launcher_args = ctx.args if ctx.args else [] @@ -220,12 +229,14 @@ def inference(ctx: click.Context, config: str, launcher: str, gradio: bool, **kw base_cmd.append(config) if gradio: base_cmd.append("--gradio") + if chat: + base_cmd.append("--chat") cmd = build_command(base_cmd, kwargs) subprocess.run(cmd, check=True) # nosec B603 else: from axolotl.cli.inference import do_cli - do_cli(config=config, gradio=gradio, **kwargs) + do_cli(config=config, gradio=gradio, chat=chat, **kwargs) @cli.command( diff --git a/src/axolotl/cli/utils/__init__.py b/src/axolotl/cli/utils/__init__.py index 583130339a..f54a471029 100644 --- a/src/axolotl/cli/utils/__init__.py +++ b/src/axolotl/cli/utils/__init__.py @@ -6,7 +6,7 @@ filter_none_kwargs, ) from .fetch import fetch_from_github -from .load import load_model_and_tokenizer +from .load import load_model_and_tokenizer, resolve_chat_template_str from .sweeps import generate_sweep_configs from .train import build_command, generate_config_files, launch_training @@ -18,6 +18,7 @@ "generate_config_files", "generate_sweep_configs", "load_model_and_tokenizer", + "resolve_chat_template_str", "launch_training", "fetch_from_github", ] diff --git a/src/axolotl/cli/utils/load.py b/src/axolotl/cli/utils/load.py index 610a81306d..4fb6f06f32 100644 --- a/src/axolotl/cli/utils/load.py +++ b/src/axolotl/cli/utils/load.py @@ -11,12 +11,39 @@ from axolotl.loaders import load_processor, load_tokenizer from axolotl.loaders.model import ModelLoader +from axolotl.utils.chat_templates import get_chat_template_from_config from axolotl.utils.dict import DictDefault from axolotl.utils.logging import get_logger LOG = get_logger(__name__) +def resolve_chat_template_str( + cfg: DictDefault, + tokenizer: PreTrainedTokenizer | PreTrainedTokenizerFast | Any, +) -> str | None: + """ + Resolves the chat template string for inference from the `axolotl` config, + mirroring how it would be resolved at training time: an explicit + `chat_template` config takes precedence, then the first dataset's + `chat_template` if that dataset is of type `chat_template`. + + Args: + cfg: Dictionary mapping `axolotl` config keys to values. + tokenizer: Tokenizer to fall back to for tokenizer-default templates. + + Returns: + Chat template string, or None if the config does not specify one. + """ + if cfg.chat_template: + return get_chat_template_from_config(cfg, ds_cfg=None, tokenizer=tokenizer) + if cfg.datasets and cfg.datasets[0].type == "chat_template": + return get_chat_template_from_config( + cfg=cfg, ds_cfg=cfg.datasets[0], tokenizer=tokenizer + ) + return None + + def load_model_and_tokenizer( *, cfg: DictDefault, diff --git a/src/axolotl/logging_config.py b/src/axolotl/logging_config.py index 67b1d32f18..e12a8be419 100644 --- a/src/axolotl/logging_config.py +++ b/src/axolotl/logging_config.py @@ -48,6 +48,17 @@ def filter(self, record: LogRecord) -> bool: ) +class HubUnauthenticatedNagFilter(logging.Filter): + """ + Drops the server-sent "sending unauthenticated requests" nag (an X-HF-Warning + response header that huggingface_hub logs with no env var to disable it). + Other hub warnings (retries, rate limits) pass through. + """ + + def filter(self, record: LogRecord) -> bool: + return "unauthenticated requests" not in record.getMessage() + + class AxolotlLogger(Logger): """Logger that applies filtering to non-axolotl loggers.""" @@ -98,6 +109,9 @@ def format(self, record): "ax_or_warn": { "()": "axolotl.logging_config.AxolotlOrWarnErrorFilter", }, + "hub_unauthenticated_nag": { + "()": "axolotl.logging_config.HubUnauthenticatedNagFilter", + }, }, "handlers": { "console": { @@ -135,6 +149,11 @@ def format(self, record): "level": os.getenv("AXOLOTL_LOG_LEVEL", DEFAULT_AXOLOTL_LOG_LEVEL).upper(), "propagate": False, }, + # filter at the emitting logger so the nag is dropped before it reaches + # both huggingface_hub's own handler and our root handlers + "huggingface_hub.utils._http": { + "filters": ["hub_unauthenticated_nag"], + }, }, } diff --git a/tests/cli/test_chat_repl.py b/tests/cli/test_chat_repl.py new file mode 100644 index 0000000000..9d6a1de646 --- /dev/null +++ b/tests/cli/test_chat_repl.py @@ -0,0 +1,668 @@ +"""pytest tests for the interactive chat REPL (no model required).""" + +import io +import json + +import pytest +from rich.console import Console + +from axolotl.cli.chat import ( + CausalTurnGenerator, + ChatRepl, + ChatSession, + TurnResult, + default_gen_params, + longest_common_prefix_len, + parse_gen_param_value, + resolve_command, + resolve_gen_param, +) + + +class FakeCache: + """Stands in for DynamicCache in cache-planning tests.""" + + def __init__(self, length=0, croppable=True): + self.length = length + self.croppable = croppable + + def crop(self, max_length): + if not self.croppable: + raise NotImplementedError("cannot crop") + self.length = max_length + + def get_seq_length(self): + return self.length + + +class FakeGenerator: + """Records conversations passed in and returns canned replies.""" + + def __init__(self, replies=None, messages=None): + self.replies = replies or ["canned reply"] + self.messages = messages + self.calls = [] + self.render_kwargs_seen = [] + + def generate_turn(self, conversation, params, on_text, render_kwargs=None): + self.calls.append(([dict(m) for m in conversation], dict(params))) + self.render_kwargs_seen.append( + dict(render_kwargs) if render_kwargs is not None else None + ) + index = min(len(self.calls) - 1, len(self.replies) - 1) + content = self.replies[index] + message = dict(self.messages[index]) if self.messages else None + on_text(content) + return TurnResult( + content=content, message=message, prompt_tokens=10, new_tokens=3 + ) + + +def make_repl(inputs, generator=None, session=None): + lines = iter(inputs) + + def input_fn(_prompt): + try: + return next(lines) + except StopIteration as err: + raise EOFError from err + + generator = generator or FakeGenerator() + repl = ChatRepl( + generator=generator, + session=session, + console=Console(file=io.StringIO(), force_terminal=False), + input_fn=input_fn, + ) + return repl, generator + + +def cache_planner(cached_ids, cache): + generator = CausalTurnGenerator.__new__(CausalTurnGenerator) + generator._cache = cache # pylint: disable=protected-access + generator._cached_ids = cached_ids # pylint: disable=protected-access + generator._new_cache = FakeCache # pylint: disable=protected-access + return generator + + +class TestGenParams: + def test_alias_resolution(self): + assert resolve_gen_param("temp").key == "temperature" + assert resolve_gen_param("max").key == "max_new_tokens" + assert resolve_gen_param("rep").key == "repetition_penalty" + assert resolve_gen_param("bogus") is None + + def test_value_validation(self): + spec = resolve_gen_param("temperature") + assert parse_gen_param_value(spec, "0.7") == 0.7 + with pytest.raises(ValueError): + parse_gen_param_value(spec, "100") + with pytest.raises(ValueError): + parse_gen_param_value(spec, "abc") + + def test_nullable_params(self): + assert parse_gen_param_value(resolve_gen_param("seed"), "none") is None + assert parse_gen_param_value(resolve_gen_param("min_p"), "off") is None + with pytest.raises(ValueError): + parse_gen_param_value(resolve_gen_param("temperature"), "none") + + +class TestChatSession: + def test_system_prompt_prepended(self): + session = ChatSession() + session.system = "be brief" + session.add_user("hi") + conversation = session.conversation() + assert conversation[0] == {"role": "system", "content": "be brief"} + assert conversation[1]["role"] == "user" + + def test_undo_removes_exchange(self): + session = ChatSession() + session.add_user("q1") + session.add_assistant("a1") + session.add_user("q2") + session.add_assistant("a2") + assert session.undo() + assert [m["content"] for m in session.messages] == ["q1", "a1"] + assert session.undo() + assert not session.messages + assert not session.undo() + + def test_drop_last_assistant_for_retry(self): + session = ChatSession() + session.add_user("q1") + session.add_assistant("a1") + assert session.drop_last_assistant() + assert session.messages[-1]["role"] == "user" + session.clear() + assert not session.drop_last_assistant() + + def test_add_user_merges_consecutive_user_messages(self): + # a failed generation leaves a trailing user message; typing again must + # not create consecutive user turns (strict templates reject them) + session = ChatSession() + session.add_user("first try") + session.add_user("second try") + assert [m["role"] for m in session.messages] == ["user"] + assert session.messages[0]["content"] == "first try\nsecond try" + + def test_save_jsonl_keeps_reasoning_content(self, tmp_path): + session = ChatSession() + session.add_user("q") + session.add_assistant_message( + {"role": "assistant", "content": "a", "reasoning_content": "hmm"} + ) + path = tmp_path / "chat.jsonl" + session.save_jsonl(str(path)) + sample = json.loads(path.read_text(encoding="utf-8")) + assistant = sample["messages"][1] + assert assistant["content"] == [{"type": "text", "text": "a"}] + assert assistant["reasoning_content"] == "hmm" + + def test_save_jsonl_multimodal_parts_format(self, tmp_path): + session = ChatSession() + session.system = "sys" + session.add_user("q") + session.add_assistant("a") + path = tmp_path / "chat.jsonl" + session.save_jsonl(str(path)) + session.save_jsonl(str(path)) + lines = path.read_text(encoding="utf-8").strip().split("\n") + assert len(lines) == 2 + sample = json.loads(lines[0]) + assert [m["role"] for m in sample["messages"]] == [ + "system", + "user", + "assistant", + ] + assert sample["messages"][1]["content"] == [{"type": "text", "text": "q"}] + assert sample["messages"][2]["content"] == [{"type": "text", "text": "a"}] + + +class TestCachePlanning: + def test_prefix_extension_reuses_cache(self): + cache = FakeCache(length=5) + generator = cache_planner([1, 2, 3, 4, 5], cache) + assert generator._prepare_cache([1, 2, 3, 4, 5, 6, 7]) == 5 + assert generator._cache is cache + + def test_divergence_crops_to_common_prefix(self): + cache = FakeCache(length=5) + generator = cache_planner([1, 2, 3, 4, 5], cache) + assert generator._prepare_cache([1, 2, 3, 9, 9, 9]) == 3 + assert cache.length == 3 + assert generator._cached_ids == [1, 2, 3] + + def test_no_overlap_resets_cache(self): + cache = FakeCache(length=3) + generator = cache_planner([1, 2, 3], cache) + assert generator._prepare_cache([7, 8, 9]) == 0 + assert generator._cache is not cache + assert generator._cached_ids == [] + + def test_uncroppable_cache_resets(self): + cache = FakeCache(length=5, croppable=False) + generator = cache_planner([1, 2, 3, 4, 5], cache) + assert generator._prepare_cache([1, 2, 3, 9, 9]) == 0 + assert generator._cache is not cache + + def test_render_fully_cached_leaves_one_input_token(self): + # cache covering all input tokens would give generate() nothing to process + cache = FakeCache(length=5) + generator = cache_planner([1, 2, 3, 4, 5], cache) + assert generator._prepare_cache([1, 2, 3, 4, 5]) == 4 + assert cache.length == 4 + + +class TestChatRepl: + def test_message_generates_turn_with_history(self): + repl, generator = make_repl(["hi", "again", "/quit"]) + repl.run() + assert len(generator.calls) == 2 + second_conversation = generator.calls[1][0] + assert [m["role"] for m in second_conversation] == [ + "user", + "assistant", + "user", + ] + assert repl.session.messages[-1]["content"] == "canned reply" + + def test_command_aliases(self): + assert resolve_command("clear").name == "new" + assert resolve_command("reset").name == "new" + assert resolve_command("regen").name == "retry" + assert resolve_command("q").name == "quit" + assert resolve_command("?").name == "help" + + def test_new_clears_history_keeps_system_and_params(self): + repl, generator = make_repl( + ["/system be brief", "/temp 0.5", "hi", "/new", "next", "/quit"] + ) + repl.run() + assert repl.session.system == "be brief" + assert repl.params["temperature"] == 0.5 + last_conversation = generator.calls[-1][0] + assert [m["role"] for m in last_conversation] == ["system", "user"] + assert last_conversation[1]["content"] == "next" + + def test_param_shortcut_and_set_forms(self): + repl, _ = make_repl( + ["/temp 0.3", "/set top_k 10", "/set max_tokens=64", "/quit"] + ) + repl.run() + assert repl.params["temperature"] == 0.3 + assert repl.params["top_k"] == 10 + assert repl.params["max_new_tokens"] == 64 + + def test_invalid_param_value_not_applied(self): + repl, _ = make_repl(["/temp 100", "/quit"]) + repl.run() + assert repl.params["temperature"] == default_gen_params()["temperature"] + + def test_retry_regenerates_last_turn(self): + repl, generator = make_repl( + ["hi", "/retry", "/quit"], generator=FakeGenerator(["first", "second"]) + ) + repl.run() + assert len(generator.calls) == 2 + retry_conversation = generator.calls[1][0] + assert retry_conversation[-1] == {"role": "user", "content": "hi"} + assert repl.session.messages[-1]["content"] == "second" + + def test_undo_command(self): + repl, _ = make_repl(["hi", "/undo", "/quit"]) + repl.run() + assert not repl.session.messages + + def test_multiline_input(self): + repl, generator = make_repl(["first line\\", "second line", "/quit"]) + repl.run() + assert generator.calls[0][0][0]["content"] == "first line\nsecond line" + + def test_generator_message_stored_in_history(self): + message = { + "role": "assistant", + "content": "The answer is 4.", + "reasoning_content": "step by step", + } + repl, _ = make_repl( + ["what is 2+2?", "/quit"], + generator=FakeGenerator(["The answer is 4."], messages=[message]), + ) + repl.run() + assert repl.session.messages[-1] == message + # renderer saw no think markers, so /expand falls back to the message + assert repl.last_think_text == "step by step" + + def test_legacy_content_fallback_kept_verbatim(self): + # generators that return no message dict store their content as-is + reply = "step by step\nThe answer is 4." + repl, _ = make_repl(["what is 2+2?", "/quit"], generator=FakeGenerator([reply])) + repl.run() + assert repl.session.messages[-1]["content"] == reply + + def test_unknown_command_does_not_generate(self): + repl, generator = make_repl(["/bogus", "/quit"]) + repl.run() + assert not generator.calls + + def test_command_handler_error_does_not_crash_repl(self): + # unclosed quote makes shlex raise inside /save + repl, generator = make_repl(["hi", '/save "unclosed', "again", "/quit"]) + repl.run() + assert len(generator.calls) == 2 + + def test_keyboard_interrupt_keeps_session_alive(self): + class InterruptingGenerator(FakeGenerator): + def generate_turn(self, conversation, params, on_text, render_kwargs=None): + if not self.calls: + self.calls.append(None) + raise KeyboardInterrupt + return super().generate_turn( + conversation, params, on_text, render_kwargs + ) + + repl, generator = make_repl( + ["hi", "again", "/quit"], generator=InterruptingGenerator() + ) + repl.run() + # interrupted turn keeps the user message; the next one merges into it + assert [m["role"] for m in repl.session.messages] == ["user", "assistant"] + assert repl.session.messages[0]["content"] == "hi\nagain" + assert len(generator.calls) == 2 + + def test_generation_failure_keeps_session_alive(self): + class FailingGenerator(FakeGenerator): + def generate_turn(self, conversation, params, on_text, render_kwargs=None): + if not self.calls: + self.calls.append(None) + raise RuntimeError("boom") + return super().generate_turn( + conversation, params, on_text, render_kwargs + ) + + repl, _ = make_repl(["hi", "/retry", "/quit"], generator=FailingGenerator()) + repl.run() + assert [m["role"] for m in repl.session.messages] == ["user", "assistant"] + assert repl.session.messages[-1]["content"] == "canned reply" + + +def test_longest_common_prefix_len(): + assert longest_common_prefix_len([], [1, 2]) == 0 + assert longest_common_prefix_len([1, 2], [1, 2]) == 2 + assert longest_common_prefix_len([1, 2, 3], [1, 2]) == 2 + assert longest_common_prefix_len([1, 9], [1, 2, 3]) == 1 + + +class TestDiffusionChat: + def test_diffusion_param_specs(self): + from axolotl.cli.chat import DIFFUSION_GEN_PARAMS + + lines = iter(["/steps 32", "/tokens 64", "/top_p 0.9", "/quit"]) + + def input_fn(_prompt): + try: + return next(lines) + except StopIteration as err: + raise EOFError from err + + repl = ChatRepl( + generator=FakeGenerator(), + param_specs=DIFFUSION_GEN_PARAMS, + console=Console(file=io.StringIO(), force_terminal=False), + input_fn=input_fn, + ) + repl.run() + assert repl.params["steps"] == 32 + assert repl.params["max_new_tokens"] == 64 + assert "top_p" not in repl.params + + def test_diffusion_turn_cuts_at_eos(self, monkeypatch): + from types import SimpleNamespace + + import axolotl.integrations.diffusion as diffusion_module + from axolotl.cli.chat import ( + DIFFUSION_GEN_PARAMS, + DiffusionTurnGenerator, + default_gen_params, + ) + + class FakeTokenizer: + eos_token_id = 2 + + def apply_chat_template(self, conversation, **kwargs): + return {"input_ids": [1, 5, 6]} + + def decode(self, ids, **kwargs): + return ",".join(str(i) for i in ids) + + fake_model = SimpleNamespace( + generation_config=SimpleNamespace(eos_token_id=None) + ) + + def fake_generate(model, tokenizer, **kwargs): + assert kwargs["mode"] == "completion" + assert kwargs["completion_tokens"] == 256 + return {"generated_ids": [1, 5, 6, 7, 8, 2, 4]} + + monkeypatch.setattr(diffusion_module, "generate", fake_generate) + + generator = DiffusionTurnGenerator( + fake_model, FakeTokenizer(), None, "cpu", mask_token_id=9 + ) + chunks = [] + result = generator.generate_turn( + [{"role": "user", "content": "hi"}], + default_gen_params(DIFFUSION_GEN_PARAMS), + chunks.append, + ) + assert result.content == "7,8" + assert result.new_tokens == 2 + assert result.prompt_tokens == 3 + assert chunks == ["7,8"] + + +def test_unknown_command_suggests_alias(): + buf = io.StringIO() + repl = ChatRepl( + generator=FakeGenerator(), + console=Console(file=buf, force_terminal=False, width=200), + input_fn=lambda _p: "/quit", + ) + repl._dispatch("/clea") + assert "Did you mean /clear?" in buf.getvalue() + repl._dispatch("/tem") + assert "Did you mean /temp?" in buf.getvalue() + + +class TestThinkStreamRenderer: + def make_renderer(self, collapse=True, markers=("", "")): + from axolotl.cli.chat import ThinkStreamRenderer + + buf = io.StringIO() + console = Console(file=buf, force_terminal=False, width=200) + return ThinkStreamRenderer(console, collapse=collapse, markers=markers), buf + + def test_collapse_splits_thinking_from_reply(self, capsys): + renderer, buf = self.make_renderer() + for chunk in ["\nreasoning he", "re\n\nAnswer!"]: + renderer.feed(chunk) + renderer.finish() + assert renderer.think_text.strip() == "reasoning here" + assert capsys.readouterr().out == "Answer!" + assert "thought for" in buf.getvalue() + + def test_no_thinking_passthrough(self, capsys): + renderer, buf = self.make_renderer() + renderer.feed("Just a plain reply") + renderer.finish() + assert renderer.think_text == "" + assert capsys.readouterr().out == "Just a plain reply" + assert "thought for" not in buf.getvalue() + + def test_unterminated_thinking(self, capsys): + renderer, buf = self.make_renderer() + renderer.feed("partial reasoning") + renderer.finish() + assert renderer.think_text == "partial reasoning" + assert capsys.readouterr().out == "" + assert "no " in buf.getvalue() + + def test_collapse_off_is_passthrough(self, capsys): + renderer, buf = self.make_renderer(collapse=False) + renderer.feed("abcreply") + renderer.finish() + assert capsys.readouterr().out == "abcreply" + assert buf.getvalue() == "" + + def test_custom_markers(self, capsys): + renderer, _ = self.make_renderer( + markers=("<|START_THINKING|>", "<|END_THINKING|>") + ) + renderer.feed("<|START_THINKING|>hmm<|END_THINKING|>ok") + renderer.finish() + assert renderer.think_text == "hmm" + assert capsys.readouterr().out == "ok" + + +class TestThinkTokenSplit: + def make_generator(self, vocab): + from types import SimpleNamespace + + from axolotl.cli.chat import TurnGenerator + + class FakeTokenizer: + eos_token_id = 0 + chat_template = None + + def encode(self, text, **kwargs): + return vocab[text] + + model = SimpleNamespace(generation_config=SimpleNamespace(eos_token_id=None)) + return TurnGenerator(model, FakeTokenizer(), None, "cpu") + + def test_split_counts(self): + generator = self.make_generator({"": [100], "": [101]}) + assert generator.split_think_token_counts([100, 1, 2, 3, 101, 7, 8]) == (3, 2) + assert generator.split_think_token_counts([100, 1, 2]) == (2, 0) + assert generator.split_think_token_counts([5, 6]) == (0, 2) + assert generator.split_think_token_counts([]) == (0, 0) + + +class TestBuildAssistantMessage: + VOCAB = { + 1: "step by step", + 2: "The answer is 4.", + 50: "", + 100: "", + 101: "", + } + SPECIAL = {50, 100, 101} + + def make_generator(self, response_schema=None, parse_response=None): + from types import SimpleNamespace + + from axolotl.cli.chat import TurnGenerator + + vocab, special = self.VOCAB, self.SPECIAL + + class FakeTokenizer: + eos_token_id = 50 + chat_template = None + + def encode(self, text, **kwargs): + return [token_id for token_id, t in vocab.items() if t == text] + + def decode(self, ids, skip_special_tokens=False): + return "".join( + vocab[i] for i in ids if not (skip_special_tokens and i in special) + ) + + tokenizer = FakeTokenizer() + if response_schema is not None: + tokenizer.response_schema = response_schema + tokenizer.parse_response = parse_response + model = SimpleNamespace(generation_config=SimpleNamespace(eos_token_id=None)) + return TurnGenerator(model, tokenizer, None, "cpu") + + def test_thinking_split_into_reasoning_content(self): + generator = self.make_generator() + message = generator.build_assistant_message([100, 1, 101, 2]) + assert message == { + "role": "assistant", + "content": "The answer is 4.", + "reasoning_content": "step by step", + } + + def test_no_thinking_omits_reasoning_key(self): + generator = self.make_generator() + message = generator.build_assistant_message([2]) + assert message == {"role": "assistant", "content": "The answer is 4."} + + def test_special_tokens_stripped_from_content(self): + generator = self.make_generator() + message = generator.build_assistant_message([2, 50]) + assert message["content"] == "The answer is 4." + + def test_parse_response_schema_preferred(self): + generator = self.make_generator( + response_schema={"x": "regex"}, + parse_response=lambda text: {"content": "parsed", "thinking": "hmm"}, + ) + message = generator.build_assistant_message([2]) + assert message == { + "role": "assistant", + "content": "parsed", + "thinking": "hmm", + } + + def test_parse_response_failure_falls_back_to_markers(self): + def boom(text): + raise ValueError("bad schema") + + generator = self.make_generator( + response_schema={"x": "regex"}, parse_response=boom + ) + message = generator.build_assistant_message([100, 1, 101, 2]) + assert message["content"] == "The answer is 4." + assert message["reasoning_content"] == "step by step" + + +class TestEosTextTrimmer: + def make_trimmer(self, eos_strings=("<|im_end|>",)): + from axolotl.cli.chat import EosTextTrimmer + + chunks = [] + return EosTextTrimmer(eos_strings, chunks.append), chunks + + def test_eos_marker_never_emitted(self): + trimmer, chunks = self.make_trimmer() + trimmer.feed("Hello") + trimmer.feed(" world<|im_end|>") + trimmer.finish() + assert "".join(chunks) == "Hello world" + + def test_eos_split_across_chunks(self): + trimmer, chunks = self.make_trimmer() + trimmer.feed("Hi<|im_") + trimmer.feed("end|>") + trimmer.finish() + assert "".join(chunks) == "Hi" + + def test_false_partial_released(self): + trimmer, chunks = self.make_trimmer() + trimmer.feed("a<") + trimmer.feed("b") + trimmer.finish() + assert "".join(chunks) == "a", "") + + command_a_like = "...<|START_THINKING|>...<|END_THINKING|>..." + assert detect_think_markers(command_a_like) == ( + "<|START_THINKING|>", + "<|END_THINKING|>", + ) + assert detect_think_toggle_key(command_a_like) is None + assert detect_think_toggle_key("{% if thinking %}x{% endif %}") == "thinking" + assert detect_think_toggle_key(None) is None diff --git a/tests/cli/test_cli_inference.py b/tests/cli/test_cli_inference.py index 807dc7fa35..8728db23d1 100644 --- a/tests/cli/test_cli_inference.py +++ b/tests/cli/test_cli_inference.py @@ -144,3 +144,42 @@ def test_inference_backward_compatibility_no_launcher_args(cli_runner, config_pa # Should not contain any extra launcher args launcher_section = called_cmd[2 : called_cmd.index("-m")] assert len(launcher_section) == 0 # No launcher args between 'launch' and '-m' + + +def test_inference_chat(cli_runner, config_path): + """Test basic inference (chat path)""" + with patch("axolotl.cli.chat.do_chat") as mock: + result = cli_runner.invoke( + cli, + ["inference", str(config_path), "--launcher", "python", "--chat"], + catch_exceptions=False, + ) + + assert mock.called + assert result.exit_code == 0 + + +def test_inference_chat_with_launcher(cli_runner, config_path): + """Test chat flag is forwarded through the launcher command""" + with patch("subprocess.run") as mock_subprocess: + result = cli_runner.invoke( + cli, + ["inference", str(config_path), "--launcher", "accelerate", "--chat"], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + called_cmd = mock_subprocess.call_args.args[0] + assert "--chat" in called_cmd + assert "axolotl.cli.inference" in called_cmd + + +def test_inference_chat_gradio_mutually_exclusive(cli_runner, config_path): + """Test that --chat and --gradio cannot be combined""" + result = cli_runner.invoke( + cli, + ["inference", str(config_path), "--chat", "--gradio"], + ) + + assert result.exit_code != 0 + assert "mutually exclusive" in result.output diff --git a/tests/test_logging_config_file_capture.py b/tests/test_logging_config_file_capture.py index 44b0ee5e62..11e8d7c645 100644 --- a/tests/test_logging_config_file_capture.py +++ b/tests/test_logging_config_file_capture.py @@ -101,3 +101,28 @@ def test_prepare_debug_log_idempotent_and_no_duplicate(monkeypatch): # Ensure the marker appears once (not duplicated via propagation) assert data.count(marker) == 1 tee.close_debug_log() + + +def test_hub_unauthenticated_nag_suppressed(monkeypatch): + from axolotl.logging_config import configure_logging + from axolotl.utils import tee + + with tempfile.TemporaryDirectory() as td: + monkeypatch.setenv("AXOLOTL_TEE_STDOUT", "0") + configure_logging() + path = tee.prepare_debug_log( + type("Cfg", (), {"output_dir": td, "get": lambda *_: False}) + ) + + hub_http = logging.getLogger("huggingface_hub.utils._http") + hub_http.warning( + "Warning: You are sending unauthenticated requests to the HF Hub." + " Please set a HF_TOKEN to enable higher rate limits and faster downloads." + ) + hub_http.warning("Retrying in 2s [Retry 1/5].") + tee.file_only_stream.flush() + + data = read(path) + assert "unauthenticated requests" not in data + assert "Retrying in 2s" in data + tee.close_debug_log() From 8eac710b2240be093e8f33644ef6a3e0887fd62b Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Wed, 24 Jun 2026 09:10:41 -0400 Subject: [PATCH 1360/1405] feat(kernels): fast + low-memory MoE-LoRA for gemma4 (NVFP4/Marlin + bnb) and DeepSeek-V4 on sm120/sm90/sm100 (#3747) --------- Co-authored-by: Entrpi Co-authored-by: NanoCode012 --- .mypy.ini | 4 + examples/deepseek-v4/README.md | 43 + examples/deepseek-v4/v4-flash-nvfp4-lora.yaml | 74 + examples/gemma4/26b-a4b-moe-bnb-lora.yaml | 101 + examples/gemma4/26b-a4b-moe-nvfp4-lora.yaml | 85 + examples/gemma4/26b-a4b-moe-qlora.yaml | 24 +- pyproject.toml | 3 + .../integrations/kernels/adapters/__init__.py | 66 + .../integrations/kernels/adapters/dsv4.py | 181 ++ .../integrations/kernels/adapters/gemma4.py | 116 + src/axolotl/integrations/kernels/args.py | 254 +- .../integrations/kernels/autotune_callback.py | 51 +- .../kernels/autotune_collector.py | 139 +- .../kernels/libs/dsv4/__init__.py | 22 + .../kernels/libs/dsv4/attention.py | 536 ++++ .../kernels/libs/dsv4/attention_csa.py | 560 ++++ .../kernels/libs/dsv4/attention_gather.py | 424 ++++ .../kernels/libs/dsv4/gated_pool.py | 104 + .../integrations/kernels/libs/dsv4/indexer.py | 138 + .../kernels/libs/dsv4/lora_fp8.py | 147 ++ .../kernels/libs/dsv4/lora_mlp.py | 144 ++ .../integrations/kernels/libs/dsv4/mhc.py | 609 +++++ .../integrations/kernels/libs/dsv4/patch.py | 348 +++ .../integrations/kernels/libs/dsv4/rope.py | 121 + .../libs/scattermoe_lora/chunked_bnb.py | 158 ++ .../scattermoe_lora/cutlass_fp4/__init__.py | 31 + .../cutlass_fp4/blockscaled_gemm_dispatch.py | 271 ++ .../scattermoe_lora/cutlass_fp4/fused_dx.py | 121 + .../scattermoe_lora/cutlass_fp4/grouped.py | 195 ++ .../cutlass_fp4/grouped_kernel.py | 1917 ++++++++++++++ .../cutlass_fp4/quant_mxfp8.py | 52 + .../cutlass_fp4/quant_nvfp4.py | 69 + .../scattermoe_lora/cutlass_fp4/swiglu.py | 148 ++ .../libs/scattermoe_lora/dequant_grouped.py | 462 ++++ .../kernels/libs/scattermoe_lora/experts.py | 359 ++- .../scattermoe_lora/experts_lora_fastpath.py | 109 + .../scattermoe_lora/gemma4_fp8_nonexpert.py | 206 ++ .../scattermoe_lora/gemma4_nf4_nonexpert.py | 108 + .../libs/scattermoe_lora/grouped_lora.py | 186 ++ .../libs/scattermoe_lora/grouped_moe.py | 201 ++ .../libs/scattermoe_lora/grouped_train.py | 584 +++++ .../libs/scattermoe_lora/kernels/ops.py | 27 +- .../scattermoe_lora/marlin_w4a16/__init__.py | 103 + .../marlin_w4a16/_csrc/PROVENANCE.md | 34 + .../marlin_w4a16/_csrc/core/scalar_type.hpp | 360 +++ .../marlin_w4a16/_csrc/launcher_body.inc | 500 ++++ .../moe/marlin_moe_wna16/kernel.h | 47 + .../moe/marlin_moe_wna16/kernel_selector.h | 32 + .../moe/marlin_moe_wna16/marlin_template.h | 2241 +++++++++++++++++ .../moe/marlin_moe_wna16/ops_standalone.cu | 405 +++ .../moe/marlin_moe_wna16/repack_standalone.cu | 371 +++ .../sm80_kernel_bfloat16_fe2m1f_bfloat16.cu | 40 + .../quantization/marlin/dequant.h | 609 +++++ .../quantization/marlin/marlin.cuh | 173 ++ .../quantization/marlin/marlin_dtypes.cuh | 149 ++ .../quantization/marlin/marlin_mma.h | 269 ++ .../scattermoe_lora/marlin_w4a16/backend.py | 248 ++ .../marlin_w4a16/fused_dequant.py | 179 ++ .../marlin_w4a16/nonexpert_linear.py | 120 + .../libs/scattermoe_lora/marlin_w4a16/prep.py | 180 ++ .../libs/scattermoe_lora/mx_weights.py | 35 +- .../scattermoe_lora/nvfp4_fp8_quantizer.py | 495 ++++ .../libs/scattermoe_lora/nvfp4_fsdp.py | 214 ++ .../libs/scattermoe_lora/nvfp4_moe_loading.py | 242 ++ .../libs/scattermoe_lora/nvfp4_nonexpert.py | 67 + .../scattermoe_lora/nvfp4_weight_converter.py | 234 ++ .../scattermoe_lora/parallel_linear_lora.py | 112 +- .../kernels/libs/scattermoe_lora/runtime.py | 69 + .../libs/scattermoe_lora/selective_dequant.py | 35 +- .../libs/scattermoe_lora/torchao_fp4_add.py | 73 + .../kernels/libs/sonicmoe/experts.py | 20 +- src/axolotl/integrations/kernels/plugin.py | 82 +- .../kernels/quant_training_guard.py | 67 + src/axolotl/kernels/quantize.py | 26 + src/axolotl/loaders/patch_manager.py | 54 +- .../monkeypatch/accelerate/float8_fsdp.py | 159 ++ src/axolotl/monkeypatch/accelerate/fsdp2.py | 54 +- .../monkeypatch/accelerate/fsdp2_quantized.py | 157 ++ .../monkeypatch/attention/flash_attn_d512.py | 491 ++++ .../monkeypatch/attention/large_head.py | 123 + .../monkeypatch/attention/sdpa_varlen.py | 186 ++ src/axolotl/monkeypatch/gemma4_hybrid_mask.py | 136 +- src/axolotl/monkeypatch/lora_kernels.py | 22 +- src/axolotl/utils/schemas/config.py | 64 + .../test_lora_mlp_geglu_moe_correctness.py | 138 + .../test_bnb_experts_forward.py | 249 ++ .../test_grouped_fp4_dequant_path.py | 426 ++++ .../test_grouped_fp4_guards.py | 120 + .../scattermoe_lora/test_grouped_fp4_perf.py | 726 ++++++ .../scattermoe_lora/test_grouped_fp4_skew.py | 283 +++ .../scattermoe_lora/test_grouped_fp4_train.py | 906 +++++++ .../test_grouped_fp4_weight_scale_2.py | 189 ++ .../scattermoe_lora/test_mxfp4_cache_fsdp.py | 83 + .../test_nvfp4_experts_forward.py | 3 + .../test_nvfp4_fsdp_sharding.py | 206 ++ .../test_nvfp4_weight_converter.py | 398 +++ .../kernels/test_grouped_backend.py | 98 + .../kernels/test_kernels_adapters.py | 98 + .../kernels/test_kernels_config.py | 208 ++ .../kernels/test_kernels_config_intent.py | 216 ++ .../kernels/test_quant_training_guard.py | 80 + .../test_scattermoe_autotune_telemetry.py | 69 +- tests/integrations/test_scattermoe_lora.py | 22 +- tests/integrations/test_sonicmoe.py | 4 +- tests/monkeypatch/test_flash_attn_d512.py | 117 + .../monkeypatch/test_float8_fsdp_sharding.py | 166 ++ tests/monkeypatch/test_fsdp2_quantized.py | 125 + .../monkeypatch/test_large_head_attention.py | 137 + .../test_lora_mlp_routed_expert_guard.py | 67 + tests/monkeypatch/test_sdpa_varlen.py | 175 ++ 110 files changed, 23814 insertions(+), 270 deletions(-) create mode 100644 examples/deepseek-v4/README.md create mode 100644 examples/deepseek-v4/v4-flash-nvfp4-lora.yaml create mode 100644 examples/gemma4/26b-a4b-moe-bnb-lora.yaml create mode 100644 examples/gemma4/26b-a4b-moe-nvfp4-lora.yaml create mode 100644 src/axolotl/integrations/kernels/adapters/__init__.py create mode 100644 src/axolotl/integrations/kernels/adapters/dsv4.py create mode 100644 src/axolotl/integrations/kernels/adapters/gemma4.py create mode 100644 src/axolotl/integrations/kernels/libs/dsv4/__init__.py create mode 100644 src/axolotl/integrations/kernels/libs/dsv4/attention.py create mode 100644 src/axolotl/integrations/kernels/libs/dsv4/attention_csa.py create mode 100644 src/axolotl/integrations/kernels/libs/dsv4/attention_gather.py create mode 100644 src/axolotl/integrations/kernels/libs/dsv4/gated_pool.py create mode 100644 src/axolotl/integrations/kernels/libs/dsv4/indexer.py create mode 100644 src/axolotl/integrations/kernels/libs/dsv4/lora_fp8.py create mode 100644 src/axolotl/integrations/kernels/libs/dsv4/lora_mlp.py create mode 100644 src/axolotl/integrations/kernels/libs/dsv4/mhc.py create mode 100644 src/axolotl/integrations/kernels/libs/dsv4/patch.py create mode 100644 src/axolotl/integrations/kernels/libs/dsv4/rope.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/chunked_bnb.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/__init__.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/blockscaled_gemm_dispatch.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/fused_dx.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/grouped.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/grouped_kernel.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/quant_mxfp8.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/quant_nvfp4.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/swiglu.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/dequant_grouped.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/experts_lora_fastpath.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_fp8_nonexpert.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_nf4_nonexpert.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/grouped_lora.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/grouped_moe.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/grouped_train.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/__init__.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/PROVENANCE.md create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/core/scalar_type.hpp create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/launcher_body.inc create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/kernel.h create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/kernel_selector.h create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/marlin_template.h create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/ops_standalone.cu create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/repack_standalone.cu create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/sm80_kernel_bfloat16_fe2m1f_bfloat16.cu create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/dequant.h create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/marlin.cuh create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/marlin_dtypes.cuh create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/marlin_mma.h create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/backend.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/fused_dequant.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/nonexpert_linear.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/prep.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_fp8_quantizer.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_fsdp.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_moe_loading.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_nonexpert.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_weight_converter.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/runtime.py create mode 100644 src/axolotl/integrations/kernels/libs/scattermoe_lora/torchao_fp4_add.py create mode 100644 src/axolotl/integrations/kernels/quant_training_guard.py create mode 100644 src/axolotl/monkeypatch/accelerate/float8_fsdp.py create mode 100644 src/axolotl/monkeypatch/accelerate/fsdp2_quantized.py create mode 100644 src/axolotl/monkeypatch/attention/flash_attn_d512.py create mode 100644 src/axolotl/monkeypatch/attention/large_head.py create mode 100644 src/axolotl/monkeypatch/attention/sdpa_varlen.py create mode 100644 tests/e2e/kernels/test_lora_mlp_geglu_moe_correctness.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_bnb_experts_forward.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_dequant_path.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_guards.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_perf.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_skew.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_train.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_weight_scale_2.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_mxfp4_cache_fsdp.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_nvfp4_fsdp_sharding.py create mode 100644 tests/integrations/kernels/scattermoe_lora/test_nvfp4_weight_converter.py create mode 100644 tests/integrations/kernels/test_grouped_backend.py create mode 100644 tests/integrations/kernels/test_kernels_adapters.py create mode 100644 tests/integrations/kernels/test_kernels_config.py create mode 100644 tests/integrations/kernels/test_kernels_config_intent.py create mode 100644 tests/integrations/kernels/test_quant_training_guard.py create mode 100644 tests/monkeypatch/test_flash_attn_d512.py create mode 100644 tests/monkeypatch/test_float8_fsdp_sharding.py create mode 100644 tests/monkeypatch/test_fsdp2_quantized.py create mode 100644 tests/monkeypatch/test_large_head_attention.py create mode 100644 tests/monkeypatch/test_lora_mlp_routed_expert_guard.py create mode 100644 tests/monkeypatch/test_sdpa_varlen.py diff --git a/.mypy.ini b/.mypy.ini index c6d837d3f2..8e9b0fe43a 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -8,6 +8,10 @@ ignore_missing_imports = True [mypy-axolotl.monkeypatch.*] ignore_errors = True +[mypy-axolotl.integrations.kernels.libs.scattermoe_lora.cutlass_fp4.*] +# CuTe-DSL (cutlass-python): @cute.jit, cute.range(unroll=), DSL tensor types not analyzable. +ignore_errors = True + [mypy-axolotl.models.mixtral.*] ignore_errors = True diff --git a/examples/deepseek-v4/README.md b/examples/deepseek-v4/README.md new file mode 100644 index 0000000000..664013f3ac --- /dev/null +++ b/examples/deepseek-v4/README.md @@ -0,0 +1,43 @@ +# Finetune DeepSeek-V4-Flash with Axolotl + +[DeepSeek-V4-Flash](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash) is a sparse MoE model with NVFP4 experts and head_dim 512 eager-only attention. + +This guide trains the MoE experts (LoRA on the 3D expert parameters) on the NVFP4 checkpoint [nvidia/DeepSeek-V4-Flash-NVFP4](https://huggingface.co/nvidia/DeepSeek-V4-Flash-NVFP4) (~168GB). + +## Getting started + +1. Install Axolotl following the [installation guide](https://docs.axolotl.ai/docs/installation.html). + +2. Install [Cut Cross Entropy](https://docs.axolotl.ai/docs/custom_integrations.html#cut-cross-entropy). + +3. Run the finetuning example: + +```bash +# 2xB200 (sm100): ~140GB/GPU, ~130 tok/s, train_loss ~1.09 +axolotl train examples/deepseek-v4/v4-flash-nvfp4-lora.yaml +``` + +Let us know how it goes. Happy finetuning! 🚀 + +### TIPS + +- For single GPU, remove FSDP block. It would require >=180GB GPU. +- On cloud, if using volume mount, pointing the HF cache (via `HF_HOME`) at a local disk keeps weight load fast. +- Train the experts only. Do not add attention or module LoRA on a `use_dsv4_kernels` run: it is unsupported and breaks the experts-only FSDP2 invariant (data-independent backward collectives across ranks). +- Keep `attn_implementation: eager` so the dsv4 kernels plugin owns the attention path. +- `sample_packing` attends within the sliding window across packed documents, exactly as plain eager attention would. Drop packing if your data needs strict cross-document isolation. +- Read more on how to load your own dataset at [docs](https://docs.axolotl.ai/docs/dataset_loading.html). + +## Optimization Guides + +- [Multi-GPU Training](https://docs.axolotl.ai/docs/multi-gpu.html) +- [Multi-Node Training](https://docs.axolotl.ai/docs/multi-node.html) +- [LoRA Optimizations](https://docs.axolotl.ai/docs/lora_optims.html) + +## Related Resources + +- [DeepSeek-V4-Flash on HuggingFace](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash) +- [Axolotl Docs](https://docs.axolotl.ai) +- [Axolotl Website](https://axolotl.ai) +- [Axolotl GitHub](https://github.com/axolotl-ai-cloud/axolotl) +- [Axolotl Discord](https://discord.gg/7m9sfhzaf3) diff --git a/examples/deepseek-v4/v4-flash-nvfp4-lora.yaml b/examples/deepseek-v4/v4-flash-nvfp4-lora.yaml new file mode 100644 index 0000000000..c22d720eb1 --- /dev/null +++ b/examples/deepseek-v4/v4-flash-nvfp4-lora.yaml @@ -0,0 +1,74 @@ +# DeepSeek-V4-Flash MoE LoRA on an NVFP4 (modelopt) checkpoint. FSDP2 multi-GPU only. +# +# The pieces that make this run: +# 1. use_dsv4_kernels: true +# V4-Flash is eager-only (head_dim 512). The dsv4 kernels plugin swaps the eager +# attention/rope/mHC/compressor/indexer for fused Triton kernels. Keep +# attn_implementation: eager so the plugin owns the attention path. +# 2. dsv4_fp4_grouped_mode: nvfp4 +# Routes the NVFP4 experts through the fused 4-bit-read grouped GEMM (base GEMM backend +# auto-dispatches by GPU arch). Backward is chunked bf16-dequant + grouped_mm (bf16 grad +# required for accuracy). Under FSDP the per-step nvfp4->mxfp4 requant is recomputed and +# freed per layer (not cached across layers, which would hold a full-model mxfp4 copy per +# rank and OOM). +# 3. sample_packing: true +# REQUIRED for steady throughput. Without it, variable sequence lengths re-trigger the +# grouped GEMM autotune every step (recompile-bound, throughput collapses). +base_model: nvidia/DeepSeek-V4-Flash-NVFP4 +attn_implementation: eager + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.kernels.KernelsPlugin + +use_kernels: true +use_scattermoe: true # NVFP4 experts (LoRA-on-experts) +use_dsv4_kernels: true # sliding/CSA attn + rope + mHC + gated-pool + indexer +lora_mlp_kernel: true # fused clamped-SwiGLU LoRA on the dense shared MLP +dsv4_fp4_grouped_mode: nvfp4 # fused 4-bit grouped MoE GEMM; backward is bf16-dequant +strict: false + +datasets: + - path: tatsu-lab/alpaca + type: alpaca +val_set_size: 0.0 +output_dir: ./outputs/dsv4-flash-nvfp4-lora +sequence_len: 4096 +sample_packing: true + +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0.0 +# MoE expert LoRA (3D Parameter tensors, not nn.Linear) +lora_target_parameters: + - mlp.experts.gate_up_proj + - mlp.experts.down_proj + +micro_batch_size: 1 +gradient_accumulation_steps: 1 +num_epochs: 1 +optimizer: adamw_torch +lr_scheduler: constant +learning_rate: 0.0001 +bf16: true +tf32: true + +gradient_checkpointing: true +gradient_checkpointing_kwargs: + use_reentrant: false +activation_offloading: false + +fsdp_version: 2 +fsdp_config: + offload_params: false + state_dict_type: SHARDED_STATE_DICT + auto_wrap_policy: TRANSFORMER_BASED_WRAP + transformer_layer_cls_to_wrap: DeepseekV4DecoderLayer + reshard_after_forward: true + +logging_steps: 1 +warmup_ratio: 0.1 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/gemma4/26b-a4b-moe-bnb-lora.yaml b/examples/gemma4/26b-a4b-moe-bnb-lora.yaml new file mode 100644 index 0000000000..8bd37534cc --- /dev/null +++ b/examples/gemma4/26b-a4b-moe-bnb-lora.yaml @@ -0,0 +1,101 @@ +# Gemma-4-26B-A4B MoE QLoRA on a bf16 checkpoint quantized to bnb-4bit at load — the throughput- +# optimized recipe for the no-NVFP4-checkpoint case. sm120 (RTX PRO 6000), 4k seq, experts-only + +# attn LoRA r=16. ~1940 tok/s / ~21 GiB on one GPU (vs nvfp4-marlin's ~2350 / ~24 GiB). +# +# The three levers that matter most here: +# 1. attn_implementation: flash_attention_2 + gemma4_hybrid_attn_impl: true +# FA2 (via the `kernels` lib — no flash-attn source build) on the sliding-window layers + +# SDPA on the global layers. ~2x over plain sdpa, which otherwise computes full attention +# over the packed 4k sequence and dominates runtime. +# 2. quantize_moe_experts: true (+ load_in_4bit) +# Stores the MoE experts as bnb-4bit at load. scattermoe dequants only the active experts +# per step and recomputes that bf16 in backward (recipe) instead of saving it — so the +# expert weights stay 4-bit-resident and never pin per-layer (no ~60 GiB blowup). +# 3. moe_bnb_fast (default on) +# Routes bnb experts through the 1-launch parallel_linear (scatter2scatter) path. Faster +# than the chunked torch._grouped_mm path at the same low memory. Set false to fall back to +# chunked for large-expert MoEs / tiny GPUs where the single-shot active-expert dequant is +# too big. +base_model: google/gemma-4-26B-A4B + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.kernels.KernelsPlugin + - axolotl.integrations.liger.LigerPlugin +use_kernels: true +use_scattermoe: true +experts_implementation: scattermoe +liger_layer_norm: true +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_rms_norm_gated: true +strict: false + +ddp_find_unused_parameters: true + +chat_template: gemma4 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:10%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.05 +output_dir: ./outputs/gemma4-26b-a4b-bnb-lora + +sequence_len: 4096 +sample_packing: true + +load_in_4bit: true +quantize_moe_experts: true +adapter: qlora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0 +# text-backbone attention + MLP projections (skip vision/audio encoders) +lora_target_modules: 'model.language_model.layers.[\d]+.(_checkpoint_wrapped_module.)?(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' +# MoE expert LoRA (3D Parameter tensors, not nn.Linear) +lora_target_parameters: + - experts.gate_up_proj + - experts.down_proj +# Fused LoRA kernels for the NON-expert path: the dense shared MLP (Gemma4TextMLP) and the +# attention q/k/v/o projections. The routed experts are handled by scattermoe; these fuse the +# remaining per-layer LoRA. fused_attn_kernel lets qkv/o attach on the gemma4_unified variant too. +lora_mlp_kernel: true +lora_qkv_kernel: true +lora_o_kernel: true +fused_attn_kernel: true + +bnb_config_kwargs: + bnb_4bit_use_double_quant: true + +# moe_bnb_fast: true is the default (1-launch parallel_linear, recompute-in-backward). Set false +# to fall back to the chunked path for large-expert MoEs / tiny GPUs (bound via moe_dequant_chunk_size). +# moe_bnb_fast: false +# moe_dequant_chunk_size: 32 + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_8bit # GPU 8-bit Adam: ~2.6 GiB less than fp32 Adam at r=16 +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true + +# gemma4 head_dim is 256. flex_attention OOMs sm120 shared memory here; the hybrid impl runs FA2 on +# the sliding-window layers + SDPA on the global layers. Requires attn_implementation=flash_attention_2. +attn_implementation: flash_attention_2 +gemma4_hybrid_attn_impl: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/gemma4/26b-a4b-moe-nvfp4-lora.yaml b/examples/gemma4/26b-a4b-moe-nvfp4-lora.yaml new file mode 100644 index 0000000000..87fc8a557a --- /dev/null +++ b/examples/gemma4/26b-a4b-moe-nvfp4-lora.yaml @@ -0,0 +1,85 @@ +# Gemma-4-26B-A4B MoE LoRA on an NVFP4 (modelopt) checkpoint — the throughput-optimized recipe. +# sm120 (RTX PRO 6000), 4k seq, experts-only + attn LoRA r=16. ~2350 tok/s / ~24 GiB on one GPU. +# +# The two levers that matter most here: +# 1. attn_implementation: flash_attention_2 + gemma4_hybrid_attn_impl: true +# FA2 (via the `kernels` lib — no flash-attn source build) on the sliding-window layers + +# SDPA on the global layers. ~2x over plain sdpa, which otherwise computes full attention +# over the packed 4k sequence and dominates runtime. +# 2. dsv4_fp4_grouped_mode: nvfp4 +# Routes the NVFP4 experts through the fused 4-bit-read grouped GEMM (Marlin on sm120, +# DeepGEMM on sm90/sm100) instead of the slower per-expert path. +base_model: nvidia/Gemma-4-26B-A4B-NVFP4 + +plugins: + - axolotl.integrations.cut_cross_entropy.CutCrossEntropyPlugin + - axolotl.integrations.kernels.KernelsPlugin + - axolotl.integrations.liger.LigerPlugin +use_kernels: true +use_scattermoe: true +experts_implementation: scattermoe +dsv4_fp4_grouped_mode: nvfp4 # fused 4-bit grouped MoE GEMM (Marlin sm120 / DeepGEMM sm90,100) +liger_layer_norm: true +liger_rope: true +liger_rms_norm: true +liger_glu_activation: true +liger_rms_norm_gated: true +strict: false + +ddp_find_unused_parameters: true + +chat_template: gemma4 +datasets: + - path: mlabonne/FineTome-100k + type: chat_template + split: train[:10%] + field_messages: conversations + message_property_mappings: + role: from + content: value +val_set_size: 0.05 +output_dir: ./outputs/gemma4-26b-a4b-nvfp4-lora + +sequence_len: 4096 +sample_packing: true + +adapter: lora +lora_r: 16 +lora_alpha: 32 +lora_dropout: 0 +# text-backbone attention + MLP projections (skip vision/audio encoders) +lora_target_modules: 'model.language_model.layers.[\d]+.(_checkpoint_wrapped_module.)?(mlp|self_attn).(up|down|gate|q|k|v|o)_proj' +# MoE expert LoRA (3D Parameter tensors, not nn.Linear) +lora_target_parameters: + - experts.gate_up_proj + - experts.down_proj +# Fused LoRA kernels for the NON-expert path: the dense shared MLP (Gemma4TextMLP) and the +# attention q/k/v/o projections. The routed experts are handled by scattermoe; these fuse the +# remaining per-layer LoRA. fused_attn_kernel lets qkv/o attach on the gemma4_unified variant too. +lora_mlp_kernel: true +lora_qkv_kernel: true +lora_o_kernel: true +fused_attn_kernel: true + +gradient_accumulation_steps: 4 +micro_batch_size: 1 +num_epochs: 1 +optimizer: adamw_torch_8bit # GPU 8-bit Adam: ~2.6 GiB less than fp32 Adam at r=16 +lr_scheduler: cosine +learning_rate: 0.0002 + +bf16: auto +tf32: true + +gradient_checkpointing: true + +# head_dim 256 (sliding) / 512 (global): flex OOMs sm120 shared memory, plain FA2 caps at 256. +# The hybrid impl runs FA2 on sliding + SDPA on global. Requires attn_implementation=flash_attention_2. +attn_implementation: flash_attention_2 +gemma4_hybrid_attn_impl: true + +warmup_ratio: 0.1 +evals_per_epoch: 4 +saves_per_epoch: 1 +weight_decay: 0.0 +special_tokens: diff --git a/examples/gemma4/26b-a4b-moe-qlora.yaml b/examples/gemma4/26b-a4b-moe-qlora.yaml index 061ecf7953..1996182b22 100644 --- a/examples/gemma4/26b-a4b-moe-qlora.yaml +++ b/examples/gemma4/26b-a4b-moe-qlora.yaml @@ -52,9 +52,13 @@ lora_target_parameters: - experts.gate_up_proj - experts.down_proj -lora_mlp_kernel: false -lora_qkv_kernel: false -lora_o_kernel: false +# Fused LoRA kernels for the NON-expert path: the dense shared MLP (Gemma4TextMLP) and the +# attention q/k/v/o projections. The routed experts are handled by scattermoe; these fuse the +# remaining per-layer LoRA. fused_attn_kernel lets qkv/o attach on the gemma4_unified variant too. +lora_mlp_kernel: true +lora_qkv_kernel: true +lora_o_kernel: true +fused_attn_kernel: true bnb_config_kwargs: bnb_4bit_use_double_quant: true @@ -79,8 +83,18 @@ gradient_checkpointing: true activation_offloading: true logging_steps: 1 -# FA2 not supported (head_dim=512); flex is varlen-capable, required for sample_packing -attn_implementation: flex_attention +# Hybrid attention is the biggest throughput lever (~2x vs plain sdpa, which computes full attention +# over the packed 4k sequence): FA2 (via the `kernels` lib, no source build) on the sliding-window +# layers + SDPA on the global layers. flex_attention OOMs sm120 shared memory here. +attn_implementation: flash_attention_2 +gemma4_hybrid_attn_impl: true + +# bnb-4bit MoE experts have no fused 4-bit-read kernel, so they're dequantized to bf16 by the +# default 1-launch parallel_linear path (the dequant'd bf16 is recomputed in backward, not saved, +# so it stays low-memory). For large-expert MoEs / tiny GPUs, set moe_bnb_fast: false to fall back +# to the chunked path and bound the per-pass dequant transient with moe_dequant_chunk_size (32). +# moe_bnb_fast: false +# moe_dequant_chunk_size: 32 warmup_ratio: 0.1 evals_per_epoch: 4 diff --git a/pyproject.toml b/pyproject.toml index 88a424a226..be2754f0c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -195,6 +195,8 @@ ignore = [ "B019", # Use of functools.cache on methods "E722", # Bare except "F821", # Undefined name (for dynamic exec) + "E731", # Lambda assignment (idiomatic for the kernel recipe/heuristic closures) + "E741", # Ambiguous variable name (single-letter math/kernel vars: I, O, l) ] [tool.ruff.lint.isort] @@ -217,6 +219,7 @@ docstring-code-format = false addopts = "-m 'not slow'" markers = [ "slow: marks tests as slow", + "perf: marks tests as performance/memory regression guards (Blackwell only, large E)", ] # UV specific configuration diff --git a/src/axolotl/integrations/kernels/adapters/__init__.py b/src/axolotl/integrations/kernels/adapters/__init__.py new file mode 100644 index 0000000000..ccb93f871a --- /dev/null +++ b/src/axolotl/integrations/kernels/adapters/__init__.py @@ -0,0 +1,66 @@ +"""Model adapters for the kernels plugin. + +``KernelsPlugin`` orchestrates plugin hooks and generic capabilities (expert-kernel +registration, the quantized-training guard, grouped NVFP4 MoE dispatch). Model-family +specifics (DeepSeek-V4 fused kernels / quantizer / dtype policy, Gemma-4 NVFP4 converters / +non-expert quantization) live in ``ModelAdapter`` subclasses here, so the plugin stays a thin +orchestrator and new models opt in by adding an adapter rather than another inline branch. +""" + +from __future__ import annotations + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +class ModelAdapter: + """Base class for model-family kernel adapters. All hooks are no-ops by default. + + An adapter is *active* for a run when :meth:`matches` returns True. The plugin calls the + hooks of every active adapter at the corresponding lifecycle point. + """ + + #: short name for logging + name: str = "base" + + def matches(self, cfg) -> bool: # pragma: no cover - trivial + return False + + def consumes_nonexpert_quantization(self, cfg) -> bool: + """Whether this adapter acts on the ``nonexpert_quantization`` intent for ``cfg``. + + The plugin warns when a non-expert quantization policy is configured but no active + adapter claims it (so a future model can't silently no-op the setting). Default False. + """ + return False + + def pre_model_load(self, cfg) -> None: + """Before the model is constructed (register converters/quantizers, patch modules).""" + + def pre_lora_load(self, cfg, model) -> None: + """After the model loads, before PEFT wraps it (fix expert loading, quantize non-experts).""" + + def post_model_load(self, cfg, model) -> None: + """After PEFT wraps the model (dtype policy, fused-LoRA kernel swaps).""" + + +def _all_adapters() -> list[ModelAdapter]: + from axolotl.integrations.kernels.adapters.dsv4 import DSV4Adapter + from axolotl.integrations.kernels.adapters.gemma4 import Gemma4Adapter + + return [DSV4Adapter(), Gemma4Adapter()] + + +def get_active_adapters(cfg) -> list[ModelAdapter]: + """Return the adapters whose ``matches(cfg)`` is True (order = registration order).""" + active = [] + for adapter in _all_adapters(): + try: + if adapter.matches(cfg): + active.append(adapter) + except Exception: # pragma: no cover - matching must never break loading + LOG.debug("adapter %s match check failed; skipping", adapter.name) + if active: + LOG.info("kernels: active model adapters: %s", [a.name for a in active]) + return active diff --git a/src/axolotl/integrations/kernels/adapters/dsv4.py b/src/axolotl/integrations/kernels/adapters/dsv4.py new file mode 100644 index 0000000000..bc064f678f --- /dev/null +++ b/src/axolotl/integrations/kernels/adapters/dsv4.py @@ -0,0 +1,181 @@ +"""DeepSeek-V4 kernel adapter. + +Owns everything DSV4-specific: the NVFP4/FP8 quantizer install, fused attention/RoPE/mHC +kernel patching, the post-PEFT fp32->compute dtype policy (keeping the mHC/keep_in_fp32 modules +in fp32), the fused clamped-SwiGLU shared-expert MLP LoRA patch, and the fp8 attention LoRA +patch. Also registers the mHC module class names that FSDP2's quantized-mixed-dtype path shards +in their own fp32 group. +""" + +from __future__ import annotations + +import torch + +from axolotl.integrations.kernels.adapters import ModelAdapter +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +# mHC modules kept in fp32 and sharded separately by the FSDP2 quantized path. +DSV4_FP32_SHARD_CLASSES = ("DeepseekV4HyperConnection", "DeepseekV4HyperHead") + + +def _maybe_truncate_layers(model) -> None: + """Debug-only (``DSV4_TRUNCATE_LAYERS=N``): truncate the decoder stack to the first N layers + before PEFT so a small slice of the model fits a couple of GPUs for fast local bring-up / + memory iteration. The first layers span all attention types (sliding/CSA/HCA). No-op unless the + env var is set. Keeps HF's ``len(layer_types) == num_hidden_layers`` validators happy by + truncating per-layer config lists (length ``orig`` -> n; ``orig+1`` e.g. compress_ratios -> n+1).""" + import os + + trunc = os.environ.get("DSV4_TRUNCATE_LAYERS") + if not trunc: + return + import gc + + import torch.nn as nn + + n = int(trunc) + for mod in model.modules(): + layers = getattr(mod, "layers", None) + if ( + isinstance(layers, nn.ModuleList) + and len(layers) > n + and "DecoderLayer" in type(layers[0]).__name__ + ): + orig = len(layers) + mod.layers = layers[:n] + for cfg_obj in (model.config, getattr(mod, "config", None)): + if cfg_obj is None: + continue + if hasattr(cfg_obj, "num_hidden_layers"): + cfg_obj.num_hidden_layers = n + for k, v in list(vars(cfg_obj).items()): + if isinstance(v, list) and len(v) in (orig, orig + 1): + setattr(cfg_obj, k, v[: n + (len(v) - orig)]) + gc.collect() + torch.cuda.empty_cache() + LOG.warning("DSV4_TRUNCATE_LAYERS=%d: truncated decoder stack (debug)", n) + break + + +class DSV4Adapter(ModelAdapter): + name = "deepseek_v4" + + def matches(self, cfg) -> bool: + return bool(cfg.use_dsv4_kernels) + + def pre_model_load(self, cfg) -> None: + # NVFP4 MoE checkpoints declare `quant_method: fp8` but transformers' finegrained-FP8 + # quantizer has no NVFP4 expert path. Install an NVFP4-aware subclass so experts load as + # NVFP4Tensor for the scattermoe fused path. Only relevant with the scattermoe expert path. + if cfg.use_scattermoe: + from axolotl.integrations.kernels.libs.scattermoe_lora.nvfp4_fp8_quantizer import ( + configure_nonexpert_mode, + install_nvfp4_fp8_quantizer, + ) + + configure_nonexpert_mode(cfg.get("dsv4_fp8_nonexpert_mode")) + install_nvfp4_fp8_quantizer() + + # Fused Triton training kernels for DSV4 (attention / RoPE / mHC). + from axolotl.integrations.kernels.libs.dsv4 import patch_deepseek_v4_kernels + + patch_deepseek_v4_kernels() + # Make the FSDP2 quantized path keep these mHC modules in their own fp32 shard group. + from axolotl.monkeypatch.accelerate.fsdp2_quantized import ( + register_fp32_shard_classes, + ) + + register_fp32_shard_classes(DSV4_FP32_SHARD_CLASSES) + + def pre_lora_load(self, cfg, model) -> None: + # MIXED_PRECISION checkpoints load NVFP4 experts as plain packed uint8 with dropped scales; + # rebuild them as NVFP4Tensor so scattermoe dequantizes correctly. + if not cfg.use_scattermoe: + return + _maybe_truncate_layers(model) + has_packed_experts = any( + isinstance(getattr(m, "gate_up_proj", None), torch.Tensor) + and m.gate_up_proj.dtype == torch.uint8 + and m.gate_up_proj.ndim == 3 + for m in model.modules() + ) + if has_packed_experts: + from axolotl.integrations.kernels.libs.scattermoe_lora.nvfp4_moe_loading import ( + attach_nvfp4_expert_scales, + ) + + attach_nvfp4_expert_scales(model, cfg.base_model) + + def post_model_load(self, cfg, model) -> None: + self._apply_fp32_dtype_policy(cfg, model) + self._patch_shared_mlp_lora(cfg, model) + self._patch_attn_fp8_lora(cfg, model) + + # --- dtype policy ------------------------------------------------------- + @staticmethod + def _apply_fp32_dtype_policy(cfg, model) -> None: + """Cast residual fp32 params (PEFT-upcast LoRA) to the compute dtype while keeping the + model's ``_keep_in_fp32_modules[_strict]`` params (mHC/norms/sinks) in fp32. The fused + DSV4 kernels demote fp32 activations at their input boundary, so this gives one consistent + compute dtype. ``dsv4_bf16_all: true`` reverts to a blanket cast including keep_in_fp32.""" + dt = cfg.torch_dtype or torch.bfloat16 + keep_all = bool(cfg.get("dsv4_bf16_all")) + keep_patterns: list[str] = [] + if not keep_all: + seen: set[str] = set() + for m in model.modules(): + for attr in ("_keep_in_fp32_modules_strict", "_keep_in_fp32_modules"): + for pat in getattr(m, attr, None) or (): + if pat not in seen: + seen.add(pat) + keep_patterns.append(pat) + n = kept = 0 + for name, p in model.named_parameters(): + if p.dtype != torch.float32: + continue + if keep_patterns and any(pat in name for pat in keep_patterns): + kept += 1 + continue + p.data = p.data.to(dt) + n += 1 + if n: + LOG.info( + "dsv4: cast %d residual fp32 params to %s for fused kernels (kept %d keep_in_fp32 fp32)", + n, + dt, + kept, + ) + + # --- fused LoRA kernel swaps ------------------------------------------- + @staticmethod + def _patch_shared_mlp_lora(cfg, model) -> None: + """Swap DSV4 shared-expert MLPs for the fused clamped-SwiGLU LoRA kernel. + + Gated by ``dsv4_shared_mlp_lora_kernel`` (NOT the generic ``lora_mlp_kernel``, which the + MoE-kernel validator force-disables). The validator translates a legacy + ``lora_mlp_kernel: true`` on a DSV4 run into this flag (see KernelsArgs.disable_mlp_kernel). + """ + if cfg.get("adapter") != "lora" or not cfg.get("dsv4_shared_mlp_lora_kernel"): + return + from axolotl.integrations.kernels.libs.dsv4.lora_mlp import ( + patch_dsv4_shared_mlp_lora, + ) + + n = patch_dsv4_shared_mlp_lora(model) + LOG.info( + "dsv4: patched %d shared-expert MLPs with fused clamped-SwiGLU LoRA", n + ) + + @staticmethod + def _patch_attn_fp8_lora(cfg, model) -> None: + """Native blockwise-fp8 fused LoRA for the large attention projections (q_b/o_b). + No-op unless ``dsv4_fp8_lora_kernel``.""" + if cfg.get("adapter") != "lora": + return + from axolotl.integrations.kernels.libs.dsv4.lora_fp8 import ( + patch_dsv4_attn_fp8_lora, + ) + + patch_dsv4_attn_fp8_lora(model, enabled=bool(cfg.get("dsv4_fp8_lora_kernel"))) diff --git a/src/axolotl/integrations/kernels/adapters/gemma4.py b/src/axolotl/integrations/kernels/adapters/gemma4.py new file mode 100644 index 0000000000..76e629166a --- /dev/null +++ b/src/axolotl/integrations/kernels/adapters/gemma4.py @@ -0,0 +1,116 @@ +"""Gemma-4 NVFP4 kernel adapter. + +Owns Gemma-4 specifics: detecting the NVFP4-modelopt checkpoint, registering the native NVFP4 +expert WeightConverters, and applying the non-expert quantization policy (fp8 / nf4). The hybrid +global-attention mask and the large-head flash kernel are generic capabilities wired in the +loader/patch_manager and are intentionally NOT owned here. +""" + +from __future__ import annotations + +from axolotl.integrations.kernels.adapters import ModelAdapter +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def is_gemma4_nvfp4_modelopt(cfg) -> bool: + """True iff the base model is a Gemma-4 NVFP4-modelopt checkpoint (quant_method=modelopt, + quant_algo=NVFP4, model_type startswith gemma4). Any failure returns False.""" + try: + from transformers import AutoConfig + + hf_cfg = AutoConfig.from_pretrained(cfg.base_model, trust_remote_code=True) + except Exception: + return False + if not str(getattr(hf_cfg, "model_type", "")).startswith("gemma4"): + return False + qcfg = getattr(hf_cfg, "quantization_config", None) + if qcfg is None: + return False + if isinstance(qcfg, dict): + return ( + qcfg.get("quant_method") == "modelopt" and qcfg.get("quant_algo") == "NVFP4" + ) + return ( + getattr(qcfg, "quant_method", None) == "modelopt" + and getattr(qcfg, "quant_algo", None) == "NVFP4" + ) + + +def resolve_nonexpert_quantization(cfg) -> str | None: + """Resolve the non-expert quantization policy to one of {None, 'fp8', 'nf4', 'nvfp4'}. + + Prefers the intent field ``nonexpert_quantization`` (none/bf16/fp8_blockwise/nf4/nvfp4); falls + back to the legacy gemma4-specific flags. Returns None for no quantization (bf16 non-experts). + 'nvfp4' routes non-experts through the Marlin W4A16 kernel (same path as the experts); 'nf4' + uses bitsandbytes. + """ + policy = cfg.get("nonexpert_quantization") + if policy: + p = str(policy).lower() + if p in ("none", "bf16"): + return None + if p in ("fp8", "fp8_blockwise"): + return "fp8" + if p == "nf4": + return "nf4" + if p == "nvfp4": + return "nvfp4" + if cfg.get("gemma4_fp8_nonexpert"): + return "fp8" + if cfg.get("gemma4_nf4_nonexpert"): + return "nf4" + return None + + +class Gemma4Adapter(ModelAdapter): + name = "gemma4_nvfp4" + + def matches(self, cfg) -> bool: + return bool(cfg.use_scattermoe) and is_gemma4_nvfp4_modelopt(cfg) + + def consumes_nonexpert_quantization(self, cfg) -> bool: + return True + + def pre_model_load(self, cfg) -> None: + from axolotl.integrations.kernels.libs.scattermoe_lora.nvfp4_weight_converter import ( + register_gemma4_nvfp4_converters, + ) + + register_gemma4_nvfp4_converters() + LOG.info( + "gemma4: registered NVFP4 expert WeightConverters (modelopt checkpoint)" + ) + + def pre_lora_load(self, cfg, model) -> None: + policy = resolve_nonexpert_quantization(cfg) + if policy == "fp8": + from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_fp8_nonexpert import ( + quantize_gemma4_nonexpert_linears, + ) + + n = quantize_gemma4_nonexpert_linears(model) + LOG.info( + "gemma4: fp8-quantized %d non-expert linears (experts stay NVFP4)", n + ) + elif policy == "nf4": + from axolotl.integrations.kernels.libs.scattermoe_lora.gemma4_nf4_nonexpert import ( + quantize_gemma4_nonexpert_nf4, + ) + + n = quantize_gemma4_nonexpert_nf4(model) + LOG.info( + "gemma4: nf4-quantized %d non-expert linears (experts stay NVFP4)", n + ) + elif policy == "nvfp4": + from axolotl.integrations.kernels.libs.scattermoe_lora.nvfp4_nonexpert import ( + quantize_gemma4_nonexpert_nvfp4, + ) + + n = quantize_gemma4_nonexpert_nvfp4(model) + LOG.info( + "gemma4: nvfp4-quantized %d non-expert linears via Marlin W4A16 " + "(experts also NVFP4)", + n, + ) diff --git a/src/axolotl/integrations/kernels/args.py b/src/axolotl/integrations/kernels/args.py index 2cf6d2f3ba..6195316f15 100644 --- a/src/axolotl/integrations/kernels/args.py +++ b/src/axolotl/integrations/kernels/args.py @@ -5,13 +5,7 @@ LOG = get_logger(__name__) -# Valid experts_implementation values: -# - "eager" : transformers' per-token loop reference implementation -# - "batched_mm" : transformers' built-in batched matmul path -# - "grouped_mm" : transformers' built-in grouped matmul path (cache-efficient) -# - "scattermoe" : axolotl-registered Triton kernels with LoRA support -# - "sonicmoe" : axolotl-registered CUTLASS / cute-DSL kernels with LoRA support -# - "deep_ep[_*]": EP-plugin composites; passed through when expert_parallel_size > 1 +# deep_ep[_*] are EP-plugin composites, passed through when expert_parallel_size > 1. _BUILTIN_EXPERTS_IMPLS = {"eager", "batched_mm", "grouped_mm"} _KERNEL_EXPERTS_IMPLS = {"scattermoe", "sonicmoe"} _EP_EXPERTS_IMPLS = { @@ -26,12 +20,199 @@ class KernelsArgs(BaseModel): + # --- intent-based capability surface (preferred) ---------------------------------------- + # MoE expert backend, an alias for use_scattermoe/use_sonicmoe so new models opt into a + # capability by name rather than a vendor flag: scattermoe | sonicmoe | eager | builtin. + expert_backend: str | None = None + # Non-expert linear quantization policy (replaces the per-model gemma4_*_nonexpert flags): + # none | bf16 | fp8_blockwise | nf4. Resolved per-model by the adapters. + nonexpert_quantization: str | None = None + # Grouped NVFP4 MoE base-GEMM backend selection: auto (capability-select; default) | marlin | + # cutlass | deepgemm. An unavailable choice warns + falls back to auto. + moe_grouped_backend: str | None = None + + # bnb-4bit MoE experts (quantize_moe_experts + load_in_4bit): number of experts dequantized to + # bf16 per chunk in the chunked-dequant grouped path. None = fixed default (memory-safe for + # smaller GPUs). Raise it on large GPUs to trade VRAM for throughput (bigger grouped GEMMs). + moe_dequant_chunk_size: int | None = None + + # bnb-4bit MoE experts: route through the 1-launch parallel_linear (scatter2scatter) path instead + # of the chunked torch._grouped_mm path. Faster (fewer kernel launches) at the same low memory -- + # the dequant'd bf16 is recomputed in backward via a recipe, not saved. None defaults to True; set + # False to force the chunked path (bounds the per-pass transient for large-expert MoEs / tiny GPUs). + moe_bnb_fast: bool | None = None + + # --- legacy / low-level flags (kept for backwards compatibility) ------------------------- use_scattermoe: bool | None = None use_sonicmoe: bool | None = None + # Fused Triton training kernels for DeepSeek-V4 (attention / RoPE / mHC). + use_dsv4_kernels: bool | None = None + # DeepSeek-V4 FP8 non-expert weight storage: "float8tensor" (default, 1-byte torchao + # Float8Tensor base) or "bf16" (dequantize to bf16 at load). + dsv4_fp8_nonexpert_mode: str | None = None + # Native blockwise-fp8 fused LoRA kernel for the large attention projections (q_b/o_b). + # Off by default; the e2e gain is small for this expert-dominated model. + dsv4_fp8_lora_kernel: bool | None = None + # Fused clamped-SwiGLU LoRA kernel for the DSV4 shared-expert MLPs. Distinct from the generic + # `lora_mlp_kernel` (dense-MLP fusion, force-disabled under MoE kernels). A legacy + # `lora_mlp_kernel: true` on a DSV4 run is translated into this flag (see disable_mlp_kernel). + dsv4_shared_mlp_lora_kernel: bool | None = None + # Fallback: cast ALL residual fp32 params (incl. keep_in_fp32 mHC/norms) to the compute + # dtype for the fused kernels, instead of preserving keep_in_fp32 in fp32. + dsv4_bf16_all: bool | None = None + # Grouped fp4 MoE experts path (variable-M contiguous-grouped, base-fp4 GEMM + LoRA): off by + # default (existing fused/chunked paths unchanged). "nvfp4" = fp4 act x fp4 weight (fastest, + # lossy acts); "fp8" = fp8 act x mxfp4 weight (accurate). Auto-dispatches the base GEMM: + # DeepGEMM (sm90/sm100) -> CUTLASS grouped (sm120) -> chunked-dequant fallback. + dsv4_fp4_grouped_mode: str | None = None + # Gemma-4 frankenstein: fp8-quantize non-expert linears in-place after loading (per-channel + # e4m3, dequant-in-forward). Experts remain NVFP4Tensor. ~2 GB resident savings. + gemma4_fp8_nonexpert: bool | None = None + # Gemma-4: NF4-quantize non-expert linears (bnb 4-bit, double-quant) after loading, the same + # non-expert compute path unsloth uses, for apples-to-apples experts-only LoRA comparison. + gemma4_nf4_nonexpert: bool | None = None + + @model_validator(mode="before") + @classmethod + def check_dsv4_fp4_grouped_mode(cls, data): + mode = data.get("dsv4_fp4_grouped_mode") + if mode is None: + return data + m = str(mode).lower() + if m == "fp8": + # Documented historically but never implemented in the training path + # (grouped_fp4_available/_train_backend only support 'nvfp4'). Reject loudly rather + # than silently no-op. + raise ValueError( + "dsv4_fp4_grouped_mode='fp8' is not implemented for training (only 'nvfp4' is " + "supported). Use 'nvfp4' or omit dsv4_fp4_grouped_mode." + ) + if m != "nvfp4": + raise ValueError(f"dsv4_fp4_grouped_mode must be 'nvfp4', got {mode!r}") + return data + + @model_validator(mode="before") + @classmethod + def check_dsv4_fp8_nonexpert_mode(cls, data): + mode = data.get("dsv4_fp8_nonexpert_mode") + if mode is not None and str(mode).lower() not in ("float8tensor", "bf16"): + raise ValueError( + f"dsv4_fp8_nonexpert_mode must be 'float8tensor' or 'bf16', got {mode!r}" + ) + return data + + @model_validator(mode="before") + @classmethod + def check_moe_dequant_chunk_size(cls, data): + chunk = data.get("moe_dequant_chunk_size") + if chunk is None: + return data + err = ValueError( + f"moe_dequant_chunk_size must be a positive integer, got {chunk!r}" + ) + if isinstance(chunk, bool): + raise err + try: + chunk_int = int(chunk) + except (TypeError, ValueError): + raise err from None + if isinstance(chunk, float) and not chunk.is_integer(): + raise err + if chunk_int <= 0: + raise err + return data + + @staticmethod + def _canonicalize_expert_backend(data): + """Canonicalize the intent ``expert_backend`` onto use_scattermoe/use_sonicmoe (the rest of + the stack reads those). ``expert_backend`` is authoritative: it sets the chosen backend True + and REJECTS an explicit legacy flag that contradicts it (so the result is never ambiguous + regardless of validator ordering). Idempotent: every consumer below calls this first because + pydantic runs same-mode validators in REVERSE definition order, so the before-validator alone + would run AFTER its consumers and they'd never see its writes.""" + eb = data.get("expert_backend") + if eb is None: + return data + eb = str(eb).lower() + valid = {"scattermoe", "sonicmoe", "eager", "builtin"} + if eb not in valid: + raise ValueError( + f"expert_backend must be one of {sorted(valid)}, got {eb!r}" + ) + want_scatter = eb == "scattermoe" + want_sonic = eb == "sonicmoe" + if data.get("use_scattermoe") is True and not want_scatter: + raise ValueError( + f"expert_backend={eb!r} conflicts with use_scattermoe=true; set only one." + ) + if data.get("use_sonicmoe") is True and not want_sonic: + raise ValueError( + f"expert_backend={eb!r} conflicts with use_sonicmoe=true; set only one." + ) + # Only the chosen backend is written, leaving the other untouched (None), so the end state is + # byte-identical to setting the equivalent legacy flag directly. eager/builtin write nothing. + if want_scatter: + data["use_scattermoe"] = True + elif want_sonic: + data["use_sonicmoe"] = True + return data + + @model_validator(mode="before") + @classmethod + def normalize_expert_backend(cls, data): + return cls._canonicalize_expert_backend(data) + + @model_validator(mode="before") + @classmethod + def check_moe_grouped_backend(cls, data): + backend = data.get("moe_grouped_backend") + if backend is None: + return data + b = str(backend).lower() + if b == "dequant": + # The chunked-dequant fallback has no training/autograd path (the training dispatch only + # wires marlin/deepgemm/cutlass); accepting it would silently run cutlass. Reject loudly. + raise ValueError( + "moe_grouped_backend='dequant' is not implemented for training (the chunked-dequant " + "fallback has no autograd path). Use 'auto' (default), 'marlin', 'deepgemm', or " + "'cutlass', or omit moe_grouped_backend." + ) + valid = {"auto", "marlin", "cutlass", "deepgemm"} + if b not in valid: + raise ValueError( + f"moe_grouped_backend must be one of {sorted(valid)}, got {backend!r}" + ) + # The override only takes effect once the grouped NVFP4 MoE path is enabled. + if not data.get("dsv4_fp4_grouped_mode"): + LOG.warning( + "moe_grouped_backend=%r has no effect unless the grouped NVFP4 MoE path is enabled " + "(set dsv4_fp4_grouped_mode: nvfp4).", + backend, + ) + return data + + @model_validator(mode="before") + @classmethod + def check_nonexpert_quantization(cls, data): + """Validate the non-expert quantization intent and warn on the deprecated per-model flags.""" + nq = data.get("nonexpert_quantization") + if nq is not None: + valid = {"none", "bf16", "fp8", "fp8_blockwise", "nf4", "nvfp4"} + if str(nq).lower() not in valid: + raise ValueError( + f"nonexpert_quantization must be one of {sorted(valid)}, got {nq!r}" + ) + if data.get("gemma4_fp8_nonexpert") or data.get("gemma4_nf4_nonexpert"): + LOG.warning( + "gemma4_fp8_nonexpert / gemma4_nf4_nonexpert are deprecated; prefer " + "`nonexpert_quantization: fp8_blockwise` or `nonexpert_quantization: nf4`." + ) + return data @model_validator(mode="before") @classmethod def check_mutually_exclusive(cls, data): + data = cls._canonicalize_expert_backend(data) if data.get("use_scattermoe") and data.get("use_sonicmoe"): raise ValueError( "Cannot use both ScatterMoE and SonicMoE simultaneously. " @@ -50,10 +231,44 @@ def check_use_kernels(cls, data): return data + @model_validator(mode="before") + @classmethod + def check_dsv4_attention_lora_unsupported(cls, data): + """Reject module-level (attention) LoRA on a DSV4 fused-kernel run. + + The fused indexer feeds only gradientless topk indices, so LoRA on the indexer/scorer + projections trains against no gradient; and attention-level LoRA reintroduces data-dependent + FSDP2 backward collectives that break the experts-only invariant the DSV4 recipe relies on. + Experts-only LoRA (lora_target_parameters) is the supported surface. This also covers + lora_target_linear: true, which expands (find_all_linear_names) to every Linear including + attention q/k/v/o. lora_exclude_modules is the explicit opt-out: setting it signals the user + has excluded the indexer scorer projections, so we downgrade to a warning.""" + if not data.get("use_dsv4_kernels"): + return data + if not (data.get("lora_target_modules") or data.get("lora_target_linear")): + return data + if data.get("lora_exclude_modules"): + LOG.warning( + "attention/module-level LoRA (lora_target_modules / lora_target_linear) with " + "use_dsv4_kernels: ensure lora_exclude_modules excludes the indexer scorer " + "projections (the fused indexer is gradientless); keep LoRA experts-only " + "(lora_target_parameters) where possible." + ) + return data + raise ValueError( + "attention/module-level LoRA is not supported with use_dsv4_kernels (this includes " + "lora_target_modules and lora_target_linear: true, which expands to all linear layers " + "incl. attention q/k/v/o): the fused indexer is gradientless (topk indices only) and " + "attention LoRA reintroduces data-dependent FSDP2 backward collectives that break the " + "experts-only invariant. Either (1) keep LoRA experts-only via lora_target_parameters, " + "or (2) explicitly exclude the indexer scorer projections via lora_exclude_modules." + ) + @model_validator(mode="before") @classmethod def check_sonicmoe_ep_unsupported(cls, data): """SonicMoE + EP is not yet implemented (EP `_sonicmoe_local` raises).""" + data = cls._canonicalize_expert_backend(data) if data.get("use_sonicmoe") and (data.get("expert_parallel_size") or 1) > 1: raise ValueError( "use_sonicmoe=true is not supported with expert_parallel_size > 1. " @@ -65,6 +280,7 @@ def check_sonicmoe_ep_unsupported(cls, data): @classmethod def check_experts_implementation(cls, data): """Auto-select impl from kernel flags; reject mismatched/unknown values.""" + data = cls._canonicalize_expert_backend(data) experts_implementation = data.get("experts_implementation") use_scattermoe = bool(data.get("use_scattermoe")) use_sonicmoe = bool(data.get("use_sonicmoe")) @@ -105,12 +321,26 @@ def check_experts_implementation(cls, data): @model_validator(mode="before") @classmethod def disable_mlp_kernel(cls, data): + data = cls._canonicalize_expert_backend(data) if data.get("use_scattermoe") is True or data.get("use_sonicmoe") is True: - if data.get("lora_mlp_kernel") is True: - LOG.warning( - "Disabling lora_mlp_kernel when using custom MoE kernels due to compatibility issues." - ) + # DSV4's shared/routed expert MLP needs the dedicated clamped-SwiGLU kernel, not the + # generic dense-MLP one; translate the intent and disable the generic path for DSV4. + if ( + data.get("lora_mlp_kernel") is True + and data.get("use_dsv4_kernels") is True + ): + if data.get("dsv4_shared_mlp_lora_kernel") is None: + data["dsv4_shared_mlp_lora_kernel"] = True + LOG.warning( + "Translated lora_mlp_kernel -> dsv4_shared_mlp_lora_kernel for the DSV4 MoE " + "run (the generic lora_mlp_kernel is disabled under DSV4 kernels)." + ) data["lora_mlp_kernel"] = False - data["mlp_kernel"] = False + # Otherwise keep lora_mlp_kernel: under the custom MoE expert kernels it only fuses the + # DENSE shared MLP (layer.mlp via find_mlp_in_layer), which is a plain gated Linear MLP + # separate from the routed experts (those are handled by the MoE kernel and aren't PEFT + # nn.Linear, so the can_patch_mlp lora_A guard skips them). The fused kernel dequantizes + # bnb-4bit / fp8 bases, so it composes with quantized non-experts. + data["mlp_kernel"] = False # the non-LoRA mlp kernel stays off under MoE return data diff --git a/src/axolotl/integrations/kernels/autotune_callback.py b/src/axolotl/integrations/kernels/autotune_callback.py index 9c311dcff4..c0444777fa 100644 --- a/src/axolotl/integrations/kernels/autotune_callback.py +++ b/src/axolotl/integrations/kernels/autotune_callback.py @@ -33,21 +33,45 @@ def _get_gpu_info() -> dict: def _get_smem_capacity() -> dict: - """Return shared memory capacity from the runtime lora_ops module.""" + """Return shared memory capacity (bytes). Prefers the runtime lora_ops helper, falls + back to the Triton active-driver device properties, then to torch device properties — + so a dsv4-only run (no scattermoe) still reports smem.""" + # 1) lora_ops helper (matches what the scattermoe autotuner actually saw) try: from axolotl.integrations.kernels.autotune_collector import ( _find_lora_ops_module, ) lora_ops = _find_lora_ops_module() - if lora_ops is None: - return {} - fn = getattr(lora_ops, "_get_smem_capacity", None) - if fn is None: - return {} - return {"smem_capacity_bytes": fn()} + fn = getattr(lora_ops, "_get_smem_capacity", None) if lora_ops else None + if fn is not None: + return {"smem_capacity_bytes": fn()} except Exception: # pylint: disable=broad-exception-caught - return {} + pass + # 2) Triton active driver + try: + import triton + + idx = torch.cuda.current_device() + props = triton.runtime.driver.active.utils.get_device_properties(idx) + smem = props.get("max_shared_mem") + if smem: + return {"smem_capacity_bytes": int(smem)} + except Exception: # pylint: disable=broad-exception-caught + pass + # 3) torch device properties + try: + props = torch.cuda.get_device_properties(torch.cuda.current_device()) + for name in ( + "shared_memory_per_block_optin", + "shared_memory_per_multiprocessor", + ): + val = getattr(props, name, None) + if val: + return {"smem_capacity_bytes": int(val), "smem_source": name} + except Exception: # pylint: disable=broad-exception-caught + pass + return {} class AutotuneReportCallback(TrainerCallback): @@ -78,7 +102,7 @@ def on_step_end( if self._reported: return - # Lazy import — Triton / scattermoe kernels may not be installed. + # Lazy import: Triton / scattermoe kernels may not be installed. from axolotl.integrations.kernels.autotune_collector import ( collect_autotune_configs, ) @@ -110,11 +134,16 @@ def on_step_end( properties.update(_get_smem_capacity()) telemetry_manager.send_event( - event_type="scattermoe-autotune", + event_type="triton-autotune", properties=properties, ) + names = sorted({c.get("kernel", "?") for c in configs}) LOG.info( - "Reported %d scattermoe kernel autotune config(s) to telemetry.", + "Reported %d Triton autotune config(s) to telemetry on %s (sm %s, smem %s B): %s", len(configs), + properties.get("gpu_name", "?"), + properties.get("gpu_compute_capability", "?"), + properties.get("smem_capacity_bytes", "?"), + ", ".join(names), ) diff --git a/src/axolotl/integrations/kernels/autotune_collector.py b/src/axolotl/integrations/kernels/autotune_collector.py index bb1919fc3e..5b771cb69b 100644 --- a/src/axolotl/integrations/kernels/autotune_collector.py +++ b/src/axolotl/integrations/kernels/autotune_collector.py @@ -47,16 +47,12 @@ def _parse_key_tuple(key_tuple: tuple) -> dict[str, Any]: def _find_lora_ops_module() -> ModuleType | None: """Locate the *runtime* ``lora_ops`` module in ``sys.modules``. - The HF ``kernels`` package loads ``scattermoe_lora`` via - ``import_from_path`` which registers it in ``sys.modules`` under a - hash-suffixed name (e.g. ``scattermoe_lora_a1b2c3d4``). A normal - import (``from axolotl.integrations.kernels...``) would create a - *separate* module instance whose kernel objects have empty - ``.cache`` dicts because autotuning ran on the runtime copy. - - We search ``sys.modules`` for any module whose name contains - ``lora_ops`` and that has the ``_scatter2scatter_lora`` kernel - attribute — that is the runtime copy with populated caches. + Normally there is a single canonical instance (the axolotl import path); a duplicate can + appear if the same file is imported under a second module name (its kernel objects would + carry separate ``.cache`` dicts). ``register_scattermoe_experts`` calls + ``_ensure_single_lora_ops`` to collapse such duplicates, but this lookup stays defensive: + it returns the first ``sys.modules`` entry whose name contains ``lora_ops`` and that + exposes the ``_scatter2scatter_lora`` kernel. """ for name, module in list(sys.modules.items()): if ( @@ -68,50 +64,99 @@ def _find_lora_ops_module() -> ModuleType | None: return None +# Substrings identifying the kernel-bearing modules to scan in sys.modules. Covers both +# the HF-`kernels` hash-suffixed scattermoe copies and the normally-imported dsv4 kernels. +_KERNEL_MODULE_HINTS: tuple[str, ...] = ( + "lora_ops", + "scattermoe_lora.kernels", + "libs.dsv4.attention", + "libs.dsv4.attention_csa", + "libs.dsv4.attention_gather", + "libs.dsv4.rope", + "libs.dsv4.mhc", + "libs.dsv4.gated_pool", + "libs.dsv4.indexer", + ".dsv4.", +) + + +def _is_autotuner(obj: Any) -> bool: + """A Triton ``Autotuner`` exposes a ``.cache`` dict and the wrapped ``.base_fn``/``.fn``.""" + return isinstance(getattr(obj, "cache", None), dict) and ( + getattr(obj, "base_fn", None) is not None + or getattr(obj, "fn", None) is not None + ) + + +def _config_to_dict(config: Any) -> dict[str, Any]: + out = dict(getattr(config, "kwargs", {}) or {}) + out["num_warps"] = getattr(config, "num_warps", None) + out["num_stages"] = getattr(config, "num_stages", None) + if getattr(config, "num_ctas", None) is not None: + out["num_ctas"] = config.num_ctas + return out + + +def _label_key(autotuner: Any, key_tuple: tuple) -> dict[str, Any]: + """Label the cache key tuple by the kernel's own declared autotune ``key`` arg names + (e.g. ``["M_BUCKET","N","K"]`` or ``["S","T","H"]``); extras (dtype specialization) go + under ``_extra``.""" + names = list(getattr(autotuner, "keys", None) or _KEY_NAMES) + result: dict[str, Any] = {} + for i, name in enumerate(names): + if i < len(key_tuple): + result[name] = key_tuple[i] + if len(key_tuple) > len(names): + result["_extra"] = [str(v) for v in key_tuple[len(names) :]] + return result + + def collect_autotune_configs() -> list[dict[str, Any]]: - """Read autotune caches from the four scattermoe-lora kernels. + """Read autotune caches from ALL dsv4 + scattermoe ``@triton.autotune`` kernels. Returns a (possibly empty) list of dicts, each containing: - * ``kernel`` – human-readable kernel name - * ``key`` – dict with the ``M``/``N``/``K`` problem dimensions - * ``config`` – dict with the selected tile sizes, ``num_warps``, - and ``num_stages`` + * ``kernel`` – kernel function name + * ``module`` – short module name it lives in + * ``key`` – dict of the autotune-key args (problem shapes), labeled by the kernel's + own declared ``key`` names + * ``config`` – selected tile sizes, ``num_warps``, ``num_stages`` (and ``num_ctas``) - Returns ``[]`` if the kernel module cannot be found or if no - autotune cache entries exist yet. + Scans ``sys.modules`` for both the HF-``kernels`` hash-suffixed scattermoe copies (whose + caches are the populated runtime ones) and the normally-imported dsv4 kernel modules. """ - lora_ops = _find_lora_ops_module() - if lora_ops is None: - LOG.debug( - "lora_ops module not found in sys.modules; skipping autotune collection" - ) - return [] - results: list[dict[str, Any]] = [] - - for friendly_name, attr_name in _KERNEL_REGISTRY: - kernel_fn = getattr(lora_ops, attr_name, None) - if kernel_fn is None: + # Dedup by the FULL module path so two distinct module instances of the same kernel + # (e.g. a duplicate import) stay visible as separate entries; that duplication is exactly + # what telemetry should surface (a single Autotuner.cache is a dict and can't hold a key + # twice, so any same-(kernel,key) duplicate means >1 module instance). + seen: set[tuple[str, str, tuple]] = set() + + for modname, module in list(sys.modules.items()): + if module is None or not any(h in modname for h in _KERNEL_MODULE_HINTS): continue - - cache = getattr(kernel_fn, "cache", None) - if not cache: - continue - - for key_tuple, config in cache.items(): - config_dict = dict(config.kwargs) - config_dict["num_warps"] = config.num_warps - config_dict["num_stages"] = config.num_stages - if getattr(config, "num_ctas", None) is not None: - config_dict["num_ctas"] = config.num_ctas - - results.append( - { - "kernel": friendly_name, - "key": _parse_key_tuple(key_tuple), - "config": config_dict, - } - ) + for attr in dir(module): + obj = getattr(module, attr, None) + if not _is_autotuner(obj): + continue + cache = obj.cache # type: ignore[union-attr] + if not cache: + continue + base = getattr(obj, "base_fn", None) or getattr(obj, "fn", None) + kname = getattr(base, "__name__", attr) + for key_tuple, config in cache.items(): + dedup = (modname, kname, tuple(key_tuple)) + if dedup in seen: + continue + seen.add(dedup) + results.append( + { + "kernel": kname, + "module": modname.rsplit(".", 1)[-1], + "module_path": modname, + "key": _label_key(obj, key_tuple), + "config": _config_to_dict(config), + } + ) return results diff --git a/src/axolotl/integrations/kernels/libs/dsv4/__init__.py b/src/axolotl/integrations/kernels/libs/dsv4/__init__.py new file mode 100644 index 0000000000..51a0452d47 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/dsv4/__init__.py @@ -0,0 +1,22 @@ +from .attention import sliding_attn +from .attention_csa import csa_attn +from .attention_gather import csa_attn_topk +from .gated_pool import gated_softmax_pool +from .indexer import indexer_scores +from .lora_mlp import apply_lora_mlp_clamped_swiglu, patch_dsv4_shared_mlp_lora +from .mhc import hyperconnection_forward +from .patch import patch_deepseek_v4_kernels +from .rope import apply_rotary_pos_emb_triton + +__all__ = [ + "sliding_attn", + "csa_attn", + "csa_attn_topk", + "gated_softmax_pool", + "indexer_scores", + "hyperconnection_forward", + "apply_rotary_pos_emb_triton", + "patch_deepseek_v4_kernels", + "apply_lora_mlp_clamped_swiglu", + "patch_dsv4_shared_mlp_lora", +] diff --git a/src/axolotl/integrations/kernels/libs/dsv4/attention.py b/src/axolotl/integrations/kernels/libs/dsv4/attention.py new file mode 100644 index 0000000000..f02daa94bb --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/dsv4/attention.py @@ -0,0 +1,536 @@ +"""Fused flash attention for DeepSeek-V4 sliding-window layers (fwd + bwd). + +Replaces the forced-eager path in +``transformers.models.deepseek_v4.modeling_deepseek_v4.eager_attention_forward`` +for ``sliding_attention`` layers. V4 cannot use FA2/3/4 (head_dim 512 > 256 cap), +SDPA (no per-head sink), or flex (compressor KV-concat), so eager — which +materializes ``[B, H, S, KV]`` scores and broadcasts the single MQA KV head ×64 via +``repeat_kv`` — is the only baseline. This kernel: + + * keeps the single MQA KV head in SRAM (no ``repeat_kv`` blow-up), + * never materializes the score matrix, + * masks the sliding window *implicitly* from indices, so work is O(S * window) + instead of O(S * KV), + * folds the gpt-oss-style per-head learnable **sink** into the online softmax by + seeding ``m = sink`` / ``l = 1`` (the sink is a logit-only column: it enters the + denominator, contributes nothing to the output), + * exposes an ``acc_dtype`` knob (fp32 vs bf16 online-softmax/PV accumulation) so the + speed/memory vs error trade-off can be measured per path. + +Sliding-window semantics (verified against ``create_sliding_window_causal_mask``): +query ``i`` attends to key ``j`` iff ``j <= i`` and ``i - j < sliding_window``. +""" + +import functools + +import torch +import triton +import triton.language as tl + +# Autotuner prunes configs that overflow SMEM per dtype/head_dim. ``EL`` (element size) is +# in the autotune key so fp32 and bf16 are tuned/cached separately. +_CONFIGS = [ + triton.Config({"BLOCK_M": bm, "BLOCK_N": bn}, num_warps=w, num_stages=s) + for bm in (16, 32, 64, 128) + for bn in (16, 32, 64) + for w in (4, 8) + for s in (1, 2) + # N=16 wgmma at large M illegal-accesses on Hopper/Blackwell; keep BN=16 only for small + # BM (the fp32 D=512 path needs the small tile). + if not (bn == 16 and bm >= 64) +] + + +@functools.lru_cache(maxsize=None) +def _smem_limit(dev): + # Real per-block SMEM of this GPU (portable: sm_120 ~99KB, H100/B200 ~228KB). + try: + return triton.runtime.driver.active.utils.get_device_properties(dev)[ + "max_shared_mem" + ] + except Exception: + return 101376 + + +@functools.lru_cache(maxsize=None) +def _max_m(dev): + # Blackwell (cc >= 10, tcgen05) keeps the MMA accumulator in tensor memory (512 cols). + # With D=512 the [M,512] acc nearly fills tmem, so M>32 misaligns (sm_100) / OOMs tmem; + # cap M<=32 there. Hopper (cc 9, no tmem) handles up to 128. + try: + return 32 if torch.cuda.get_device_capability(dev)[0] >= 10 else 128 + except Exception: + return 128 + + +def _prune_smem(n_qtiles, n_kvtiles): + """Drop configs that would overflow SMEM (D-wide tiles ×num_stages) or, on Blackwell, + exceed the tensor-memory accumulator limit (M>32). Portable; the autotuner's own + OutOfResources catch is the backstop.""" + + def prune(configs, nargs, **kwargs): + D, EL = ( + kwargs["D"], + nargs["EL"], + ) # D is constexpr (kwargs); EL is runtime (nargs) + dev = torch.cuda.current_device() + budget = int(_smem_limit(dev) * 0.9) + max_m = _max_m(dev) + kept = [] + for c in configs: + bm, bn, st = c.kwargs["BLOCK_M"], c.kwargs["BLOCK_N"], c.num_stages + est = (n_qtiles * bm + n_kvtiles * st * bn) * D * EL + if est <= budget and bm <= max_m: + kept.append(c) + return kept or [ + min( + configs, + key=lambda c: c.kwargs["BLOCK_M"] * c.kwargs["BLOCK_N"] * c.num_stages, + ) + ] + + return prune + + +@triton.autotune( + configs=_CONFIGS, + key=["H", "EL"], + prune_configs_by={"early_config_prune": _prune_smem(1, 2)}, +) # q + (k,v) +@triton.jit +def _fwd_kernel( + Q, + K, + V, + sinks, + Out, + L, + scale, + stride_qb, + stride_qh, + stride_qm, + stride_qd, + stride_kb, + stride_kn, + stride_kd, + stride_vb, + stride_vn, + stride_vd, + stride_ob, + stride_oh, + stride_om, + stride_od, + H, + S, + KV, + WINDOW, + EL, + D: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + ACC: tl.constexpr, + PREC: tl.constexpr, +): + pid_m = tl.program_id(0) + pid_bh = tl.program_id(1) + b = pid_bh // H + h = pid_bh % H + + offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + offs_d = tl.arange(0, D) + + q_ptr = ( + Q + + b * stride_qb + + h * stride_qh + + offs_m[:, None] * stride_qm + + offs_d[None, :] * stride_qd + ) + q = tl.load(q_ptr, mask=offs_m[:, None] < S, other=0.0) + + sink = tl.load(sinks + h).to(tl.float32) + m_i = tl.zeros([BLOCK_M], tl.float32) + sink + l_i = tl.zeros([BLOCK_M], tl.float32) + 1.0 # exp(sink - sink) seed + acc = tl.zeros([BLOCK_M, D], tl.float32) + + # sliding window: keys in (m - window, m] + lo = tl.maximum(0, (pid_m * BLOCK_M - WINDOW + 1)) + lo = (lo // BLOCK_N) * BLOCK_N + hi = tl.minimum(KV, pid_m * BLOCK_M + BLOCK_M) + + kbase = K + b * stride_kb + vbase = V + b * stride_vb + for start_n in range(lo, hi, BLOCK_N): + offs_n = start_n + tl.arange(0, BLOCK_N) + nmask = offs_n < KV + k = tl.load( + kbase + offs_n[:, None] * stride_kn + offs_d[None, :] * stride_kd, + mask=nmask[:, None], + other=0.0, + ) + qk = tl.dot(q, tl.trans(k), input_precision=PREC) * scale + + valid = ( + (offs_n[None, :] <= offs_m[:, None]) + & (offs_m[:, None] - offs_n[None, :] < WINDOW) + & nmask[None, :] + ) + qk = tl.where(valid, qk, float("-inf")) + + m_new = tl.maximum(m_i, tl.max(qk, 1)) + alpha = tl.exp(m_i - m_new) + p = tl.exp(qk - m_new[:, None]) + l_i = l_i * alpha + tl.sum(p, 1) + acc = acc * alpha[:, None] + v = tl.load( + vbase + offs_n[:, None] * stride_vn + offs_d[None, :] * stride_vd, + mask=nmask[:, None], + other=0.0, + ) + acc += tl.dot(p.to(ACC), v.to(ACC), input_precision=PREC).to(tl.float32) + m_i = m_new + + acc = acc / l_i[:, None] + o_ptr = ( + Out + + b * stride_ob + + h * stride_oh + + offs_m[:, None] * stride_om + + offs_d[None, :] * stride_od + ) + tl.store(o_ptr, acc.to(Out.dtype.element_ty), mask=offs_m[:, None] < S) + tl.store(L + pid_bh * S + offs_m, m_i + tl.log(l_i), mask=offs_m < S) + + +def _fwd(q, k, v, sinks, scale, window, acc_dtype): + B, H, S, D = q.shape + KV = k.shape[1] + out = torch.empty(B, H, S, D, device=q.device, dtype=q.dtype) + L = torch.empty(B * H, S, device=q.device, dtype=torch.float32) + ACC = tl.float32 if acc_dtype == torch.float32 else tl.bfloat16 + grid = lambda meta: (triton.cdiv(S, meta["BLOCK_M"]), B * H) + _fwd_kernel[grid]( + q, + k, + v, + sinks, + out, + L, + scale, + q.stride(0), + q.stride(1), + q.stride(2), + q.stride(3), + k.stride(0), + k.stride(1), + k.stride(2), + v.stride(0), + v.stride(1), + v.stride(2), + out.stride(0), + out.stride(1), + out.stride(2), + out.stride(3), + H, + S, + KV, + window, + q.element_size(), + D=D, + ACC=ACC, + PREC=("ieee" if q.element_size() == 4 else "tf32"), + ) + return out, L + + +# Two-kernel backward (dq pass, dk/dv pass): splitting halves the resident D-wide dot +# operands per kernel so 16x16 tiles fit ~99KB SMEM. +@triton.autotune( + configs=_CONFIGS, + key=["H", "EL"], + reset_to_zero=["DSINK"], + prune_configs_by={"early_config_prune": _prune_smem(2, 2)}, +) +@triton.jit +def _bwd_dq_kernel( + Q, + K, + V, + sinks, + DO, + L, + Delta, + DQ, + DSINK, + scale, + stride_qb, + stride_qh, + stride_qm, + stride_qd, + stride_kb, + stride_kn, + stride_kd, + H, + S, + KV, + WINDOW, + EL, + D: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + ACC: tl.constexpr, + PREC: tl.constexpr, +): + pid_m = tl.program_id(0) + pid_bh = tl.program_id(1) + b = pid_bh // H + h = pid_bh % H + offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + offs_d = tl.arange(0, D) + mmask = offs_m < S + + q = tl.load( + Q + + b * stride_qb + + h * stride_qh + + offs_m[:, None] * stride_qm + + offs_d[None, :] * stride_qd, + mask=mmask[:, None], + other=0.0, + ) + do = tl.load( + DO + + b * stride_qb + + h * stride_qh + + offs_m[:, None] * stride_qm + + offs_d[None, :] * stride_qd, + mask=mmask[:, None], + other=0.0, + ) + l_i = tl.load(L + pid_bh * S + offs_m, mask=mmask, other=0.0) + delta = tl.load(Delta + pid_bh * S + offs_m, mask=mmask, other=0.0) + sink = tl.load(sinks + h).to(tl.float32) + + dq = tl.zeros([BLOCK_M, D], tl.float32) + lo = (tl.maximum(0, pid_m * BLOCK_M - WINDOW + 1) // BLOCK_N) * BLOCK_N + hi = tl.minimum(KV, pid_m * BLOCK_M + BLOCK_M) + for start_n in range(lo, hi, BLOCK_N): + offs_n = start_n + tl.arange(0, BLOCK_N) + nmask = offs_n < KV + k = tl.load( + K + + b * stride_kb + + offs_n[:, None] * stride_kn + + offs_d[None, :] * stride_kd, + mask=nmask[:, None], + other=0.0, + ) + v = tl.load( + V + + b * stride_kb + + offs_n[:, None] * stride_kn + + offs_d[None, :] * stride_kd, + mask=nmask[:, None], + other=0.0, + ) + qk = tl.dot(q, tl.trans(k), input_precision=PREC) * scale + valid = ( + (offs_n[None, :] <= offs_m[:, None]) + & (offs_m[:, None] - offs_n[None, :] < WINDOW) + & nmask[None, :] + ) + p = tl.where(valid, tl.exp(qk - l_i[:, None]), 0.0) + dp = tl.dot(do, tl.trans(v), input_precision=PREC) + ds = (p * (dp - delta[:, None]) * scale).to(ACC) + dq += tl.dot(ds, k.to(ACC), input_precision=PREC).to(tl.float32) + + p_sink = tl.exp(sink - l_i) + tl.atomic_add(DSINK + h, tl.sum(tl.where(mmask, -p_sink * delta, 0.0))) + tl.store( + DQ + + b * stride_qb + + h * stride_qh + + offs_m[:, None] * stride_qm + + offs_d[None, :] * stride_qd, + dq.to(DQ.dtype.element_ty), + mask=mmask[:, None], + ) + + +@triton.autotune( + configs=_CONFIGS, + key=["H", "EL"], + reset_to_zero=["DK", "DV"], + prune_configs_by={"early_config_prune": _prune_smem(2, 2)}, +) +@triton.jit +def _bwd_dkdv_kernel( + Q, + K, + V, + DO, + L, + Delta, + DK, + DV, + scale, + stride_qb, + stride_qh, + stride_qm, + stride_qd, + stride_kb, + stride_kn, + stride_kd, + H, + S, + KV, + WINDOW, + EL, + D: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + ACC: tl.constexpr, + PREC: tl.constexpr, +): + pid_n = tl.program_id(0) + pid_bh = tl.program_id(1) + b = pid_bh // H + h = pid_bh % H + offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N) + offs_d = tl.arange(0, D) + nmask = offs_n < KV + + k = tl.load( + K + b * stride_kb + offs_n[:, None] * stride_kn + offs_d[None, :] * stride_kd, + mask=nmask[:, None], + other=0.0, + ) + v = tl.load( + V + b * stride_kb + offs_n[:, None] * stride_kn + offs_d[None, :] * stride_kd, + mask=nmask[:, None], + other=0.0, + ) + dk = tl.zeros([BLOCK_N, D], tl.float32) + dv = tl.zeros([BLOCK_N, D], tl.float32) + + # queries that attend to this kv block: i in [n0, n0 + BLOCK_N - 1 + WINDOW - 1] + lo = (pid_n * BLOCK_N // BLOCK_M) * BLOCK_M + hi = tl.minimum(S, pid_n * BLOCK_N + BLOCK_N + WINDOW - 1) + for start_m in range(lo, hi, BLOCK_M): + offs_m = start_m + tl.arange(0, BLOCK_M) + mmask = offs_m < S + q = tl.load( + Q + + b * stride_qb + + h * stride_qh + + offs_m[:, None] * stride_qm + + offs_d[None, :] * stride_qd, + mask=mmask[:, None], + other=0.0, + ) + do = tl.load( + DO + + b * stride_qb + + h * stride_qh + + offs_m[:, None] * stride_qm + + offs_d[None, :] * stride_qd, + mask=mmask[:, None], + other=0.0, + ) + l_i = tl.load(L + pid_bh * S + offs_m, mask=mmask, other=0.0) + delta = tl.load(Delta + pid_bh * S + offs_m, mask=mmask, other=0.0) + qk = tl.dot(q, tl.trans(k), input_precision=PREC) * scale + valid = ( + (offs_n[None, :] <= offs_m[:, None]) + & (offs_m[:, None] - offs_n[None, :] < WINDOW) + & nmask[None, :] + ) + p = tl.where(valid, tl.exp(qk - l_i[:, None]), 0.0) + dp = tl.dot(do, tl.trans(v), input_precision=PREC) + ds = (p * (dp - delta[:, None]) * scale).to(ACC) + dk += tl.dot(tl.trans(ds), q.to(ACC), input_precision=PREC).to(tl.float32) + dv += tl.dot(tl.trans(p.to(ACC)), do.to(ACC), input_precision=PREC).to( + tl.float32 + ) + + # shared MQA head: sum across q heads via atomics + dk_ptr = ( + DK + b * stride_kb + offs_n[:, None] * stride_kn + offs_d[None, :] * stride_kd + ) + dv_ptr = ( + DV + b * stride_kb + offs_n[:, None] * stride_kn + offs_d[None, :] * stride_kd + ) + tl.atomic_add(dk_ptr, dk, mask=nmask[:, None]) + tl.atomic_add(dv_ptr, dv, mask=nmask[:, None]) + + +def _bwd(q, k, v, sinks, out, do, L, scale, window, acc_dtype): + B, H, S, D = q.shape + KV = k.shape[1] + do = do.contiguous() + delta = (do.to(torch.float32) * out.to(torch.float32)).sum(-1).reshape(B * H, S) + dq = torch.empty_like(q) + dk = torch.zeros_like(k, dtype=torch.float32) + dv = torch.zeros_like(v, dtype=torch.float32) + dsink = torch.zeros_like(sinks, dtype=torch.float32) + ACC = tl.float32 if acc_dtype == torch.float32 else tl.bfloat16 + PREC = "ieee" if q.element_size() == 4 else "tf32" + EL = q.element_size() + args = ( + q.stride(0), + q.stride(1), + q.stride(2), + q.stride(3), + k.stride(0), + k.stride(1), + k.stride(2), + H, + S, + KV, + window, + EL, + ) + _bwd_dq_kernel[lambda m: (triton.cdiv(S, m["BLOCK_M"]), B * H)]( + q, k, v, sinks, do, L, delta, dq, dsink, scale, *args, D=D, ACC=ACC, PREC=PREC + ) + _bwd_dkdv_kernel[lambda m: (triton.cdiv(KV, m["BLOCK_N"]), B * H)]( + q, k, v, do, L, delta, dk, dv, scale, *args, D=D, ACC=ACC, PREC=PREC + ) + return dq, dk.to(k.dtype), dv.to(v.dtype), dsink.to(sinks.dtype) + + +class _SlidingAttn(torch.autograd.Function): + @staticmethod + def forward(ctx, q, k, v, sinks, scale, window, acc_dtype): + out, L = _fwd(q, k, v, sinks, scale, window, acc_dtype) + ctx.save_for_backward(q, k, v, sinks, out, L) + ctx.scale, ctx.window, ctx.acc_dtype = scale, window, acc_dtype + return out + + @staticmethod + def backward(ctx, do): + q, k, v, sinks, out, L = ctx.saved_tensors + dq, dk, dv, dsink = _bwd( + q, k, v, sinks, out, do, L, ctx.scale, ctx.window, ctx.acc_dtype + ) + return dq, dk, dv, dsink, None, None, None + + +def sliding_attn( + q, k, v, sinks, scale=None, sliding_window=128, acc_dtype=torch.float32 +): + """q: [B, H, S, D]; k, v: [B, 1, KV, D] (single MQA head); sinks: [H]. + Returns attn output [B, H, S, D]. ``acc_dtype`` controls PV/dq/dk/dv accumulation.""" + B, H, S, D = q.shape + if scale is None: + scale = D**-0.5 + # match k/v/sinks to q so the kernel never sees mixed dtypes + dt = q.dtype + k = k.to(dt) if k.dtype != dt else k + v = v.to(dt) if v.dtype != dt else v + sinks = sinks.to(dt) if sinks.dtype != dt else sinks + k2 = k[:, 0].contiguous() + v2 = v[:, 0].contiguous() + return _SlidingAttn.apply( + q.contiguous(), k2, v2, sinks.contiguous(), scale, sliding_window, acc_dtype + ) diff --git a/src/axolotl/integrations/kernels/libs/dsv4/attention_csa.py b/src/axolotl/integrations/kernels/libs/dsv4/attention_csa.py new file mode 100644 index 0000000000..bd3115c8eb --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/dsv4/attention_csa.py @@ -0,0 +1,560 @@ +"""Core attention for DeepSeek-V4 CSA/HCA layers (sparse-MLA), fwd + bwd. + +These layers call ``eager_attention_forward`` with +``kv = cat([sliding_kv (S), compressed_kv (T)], dim=2)`` and +``mask = [sliding_window_causal (S) | block_bias (S, T)]``. ``block_bias`` carries the +per-query causality + Lightning-Indexer top-k selection over the T compressed entries +and is a constant w.r.t. attention (top-k / argmax are non-differentiable). + +This kernel does one fused online softmax over: the windowed sliding keys (implicit +mask, O(S*window)), the T compressed keys (dense additive ``block_bias``), and the +per-head sink — never materializing the [B,H,S,S+T] scores eager builds, and keeping +the single MQA KV head in SRAM. fp32 online-softmax accumulation. + +Shared-KV MQA: ``kv_slide`` is used as both K and V, ``kv_comp`` likewise; their grads +are dk+dv summed. ``block_bias`` is dense [B,S,T] (smaller than eager's full mask but +still O(S*T) for CSA where T~S/4 — exploiting the top-k to gather only index_topk keys +per query is a further optimization, not done here). +""" + +import torch +import triton +import triton.language as tl + +from .attention import _CONFIGS, _prune_smem + + +@triton.autotune( + configs=_CONFIGS, + key=["H", "EL"], + prune_configs_by={"early_config_prune": _prune_smem(1, 2)}, +) +@triton.jit +def _csa_fwd_kernel( + Q, + KS, + KC, + BB, + sinks, + Out, + L, + scale, + sqb, + sqh, + sqm, + sqd, + skb, + skn, + skd, + scb, + scn, + scd, + sbb, + sbm, + sbt, + sob, + soh, + som, + sod, + H, + S, + T, + WINDOW, + EL, + D: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + ACC: tl.constexpr, + PREC: tl.constexpr, +): + pid_m = tl.program_id(0) + pid_bh = tl.program_id(1) + b = pid_bh // H + h = pid_bh % H + offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + offs_d = tl.arange(0, D) + q = tl.load( + Q + b * sqb + h * sqh + offs_m[:, None] * sqm + offs_d[None, :] * sqd, + mask=offs_m[:, None] < S, + other=0.0, + ) + sink = tl.load(sinks + h).to(tl.float32) + m_i = tl.zeros([BLOCK_M], tl.float32) + sink + l_i = tl.zeros([BLOCK_M], tl.float32) + 1.0 + acc = tl.zeros([BLOCK_M, D], tl.float32) + + lo = (tl.maximum(0, pid_m * BLOCK_M - WINDOW + 1) // BLOCK_N) * BLOCK_N + hi = tl.minimum(S, pid_m * BLOCK_M + BLOCK_M) + for start_n in range(lo, hi, BLOCK_N): + offs_n = start_n + tl.arange(0, BLOCK_N) + nmask = offs_n < S + k = tl.load( + KS + b * skb + offs_n[:, None] * skn + offs_d[None, :] * skd, + mask=nmask[:, None], + other=0.0, + ) + qk = tl.dot(q, tl.trans(k), input_precision=PREC) * scale + valid = ( + (offs_n[None, :] <= offs_m[:, None]) + & (offs_m[:, None] - offs_n[None, :] < WINDOW) + & nmask[None, :] + ) + qk = tl.where(valid, qk, float("-inf")) + m_new = tl.maximum(m_i, tl.max(qk, 1)) + alpha = tl.exp(m_i - m_new) + p = tl.exp(qk - m_new[:, None]) + l_i = l_i * alpha + tl.sum(p, 1) + acc = acc * alpha[:, None] + v = tl.load( + KS + b * skb + offs_n[:, None] * skn + offs_d[None, :] * skd, + mask=nmask[:, None], + other=0.0, + ) + acc += tl.dot(p.to(ACC), v.to(ACC), input_precision=PREC).to(tl.float32) + m_i = m_new + + for start_t in range(0, T, BLOCK_N): + offs_t = start_t + tl.arange(0, BLOCK_N) + tmask = offs_t < T + kc = tl.load( + KC + b * scb + offs_t[:, None] * scn + offs_d[None, :] * scd, + mask=tmask[:, None], + other=0.0, + ) + qk = tl.dot(q, tl.trans(kc), input_precision=PREC) * scale + bb = tl.load( + BB + b * sbb + offs_m[:, None] * sbm + offs_t[None, :] * sbt, + mask=(offs_m[:, None] < S) & tmask[None, :], + other=float("-inf"), + ) + qk = tl.where(tmask[None, :], qk + bb, float("-inf")) + m_new = tl.maximum(m_i, tl.max(qk, 1)) + alpha = tl.exp(m_i - m_new) + p = tl.exp(qk - m_new[:, None]) + l_i = l_i * alpha + tl.sum(p, 1) + acc = acc * alpha[:, None] + vc = tl.load( + KC + b * scb + offs_t[:, None] * scn + offs_d[None, :] * scd, + mask=tmask[:, None], + other=0.0, + ) + acc += tl.dot(p.to(ACC), vc.to(ACC), input_precision=PREC).to(tl.float32) + m_i = m_new + + acc = acc / l_i[:, None] + tl.store( + Out + b * sob + h * soh + offs_m[:, None] * som + offs_d[None, :] * sod, + acc.to(Out.dtype.element_ty), + mask=offs_m[:, None] < S, + ) + tl.store(L + pid_bh * S + offs_m, m_i + tl.log(l_i), mask=offs_m < S) + + +def _csa_fwd(q, ks, kc, bb, sinks, scale, window): + B, H, S, D = q.shape + T = kc.shape[1] + out = torch.empty(B, H, S, D, device=q.device, dtype=q.dtype) + L = torch.empty(B * H, S, device=q.device, dtype=torch.float32) + ACC = tl.bfloat16 if q.dtype == torch.bfloat16 else tl.float32 + PREC = "ieee" if q.element_size() == 4 else "tf32" + grid = lambda m: (triton.cdiv(S, m["BLOCK_M"]), B * H) + _csa_fwd_kernel[grid]( + q, + ks, + kc, + bb, + sinks, + out, + L, + scale, + q.stride(0), + q.stride(1), + q.stride(2), + q.stride(3), + ks.stride(0), + ks.stride(1), + ks.stride(2), + kc.stride(0), + kc.stride(1), + kc.stride(2), + bb.stride(0), + bb.stride(1), + bb.stride(2), + out.stride(0), + out.stride(1), + out.stride(2), + out.stride(3), + H, + S, + T, + window, + q.element_size(), + D=D, + ACC=ACC, + PREC=PREC, + ) + return out, L + + +@triton.autotune( + configs=_CONFIGS, + key=["H", "EL"], + reset_to_zero=["DSINK"], + prune_configs_by={"early_config_prune": _prune_smem(2, 2)}, +) +@triton.jit +def _csa_bwd_dq_kernel( + Q, + KS, + KC, + BB, + sinks, + DO, + L, + Delta, + DQ, + DSINK, + scale, + sqb, + sqh, + sqm, + sqd, + skb, + skn, + skd, + scb, + scn, + scd, + sbb, + sbm, + sbt, + H, + S, + T, + WINDOW, + EL, + D: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + ACC: tl.constexpr, + PREC: tl.constexpr, +): + pid_m = tl.program_id(0) + pid_bh = tl.program_id(1) + b = pid_bh // H + h = pid_bh % H + offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + offs_d = tl.arange(0, D) + mmask = offs_m < S + q = tl.load( + Q + b * sqb + h * sqh + offs_m[:, None] * sqm + offs_d[None, :] * sqd, + mask=mmask[:, None], + other=0.0, + ) + do = tl.load( + DO + b * sqb + h * sqh + offs_m[:, None] * sqm + offs_d[None, :] * sqd, + mask=mmask[:, None], + other=0.0, + ) + l_i = tl.load(L + pid_bh * S + offs_m, mask=mmask, other=0.0) + delta = tl.load(Delta + pid_bh * S + offs_m, mask=mmask, other=0.0) + sink = tl.load(sinks + h).to(tl.float32) + dq = tl.zeros([BLOCK_M, D], tl.float32) + + lo = (tl.maximum(0, pid_m * BLOCK_M - WINDOW + 1) // BLOCK_N) * BLOCK_N + hi = tl.minimum(S, pid_m * BLOCK_M + BLOCK_M) + for start_n in range(lo, hi, BLOCK_N): + offs_n = start_n + tl.arange(0, BLOCK_N) + nmask = offs_n < S + k = tl.load( + KS + b * skb + offs_n[:, None] * skn + offs_d[None, :] * skd, + mask=nmask[:, None], + other=0.0, + ) + v = k + qk = tl.dot(q, tl.trans(k), input_precision=PREC) * scale + valid = ( + (offs_n[None, :] <= offs_m[:, None]) + & (offs_m[:, None] - offs_n[None, :] < WINDOW) + & nmask[None, :] + ) + p = tl.where(valid, tl.exp(qk - l_i[:, None]), 0.0) + dp = tl.dot(do, tl.trans(v), input_precision=PREC) + ds = (p * (dp - delta[:, None]) * scale).to(ACC) + dq += tl.dot(ds, k.to(ACC), input_precision=PREC).to(tl.float32) + + for start_t in range(0, T, BLOCK_N): + offs_t = start_t + tl.arange(0, BLOCK_N) + tmask = offs_t < T + kc = tl.load( + KC + b * scb + offs_t[:, None] * scn + offs_d[None, :] * scd, + mask=tmask[:, None], + other=0.0, + ) + qk = tl.dot(q, tl.trans(kc), input_precision=PREC) * scale + bb = tl.load( + BB + b * sbb + offs_m[:, None] * sbm + offs_t[None, :] * sbt, + mask=mmask[:, None] & tmask[None, :], + other=float("-inf"), + ) + p = tl.where(tmask[None, :], tl.exp(qk + bb - l_i[:, None]), 0.0) + dp = tl.dot(do, tl.trans(kc), input_precision=PREC) + ds = (p * (dp - delta[:, None]) * scale).to(ACC) + dq += tl.dot(ds, kc.to(ACC), input_precision=PREC).to(tl.float32) + + p_sink = tl.exp(sink - l_i) + tl.atomic_add(DSINK + h, tl.sum(tl.where(mmask, -p_sink * delta, 0.0))) + tl.store( + DQ + b * sqb + h * sqh + offs_m[:, None] * sqm + offs_d[None, :] * sqd, + dq.to(DQ.dtype.element_ty), + mask=mmask[:, None], + ) + + +@triton.autotune( + configs=_CONFIGS, + key=["H", "EL"], + reset_to_zero=["DKS", "DKC"], + prune_configs_by={"early_config_prune": _prune_smem(2, 2)}, +) +@triton.jit +def _csa_bwd_dkv_kernel( + Q, + KS, + KC, + BB, + DO, + L, + Delta, + DKS, + DKC, + scale, + sqb, + sqh, + sqm, + sqd, + skb, + skn, + skd, + scb, + scn, + scd, + sbb, + sbm, + sbt, + H, + S, + T, + WINDOW, + EL, + D: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + ACC: tl.constexpr, + PREC: tl.constexpr, +): + # pid_n indexes a key block over the combined [sliding (S) ; compressed (T)] axis. + pid_n = tl.program_id(0) + pid_bh = tl.program_id(1) + b = pid_bh // H + h = pid_bh % H + offs_d = tl.arange(0, D) + NSLIDE_BLK = tl.cdiv(S, BLOCK_N) + is_comp = pid_n >= NSLIDE_BLK + + if is_comp: + start_t = (pid_n - NSLIDE_BLK) * BLOCK_N + offs_n = start_t + tl.arange(0, BLOCK_N) + nmask = offs_n < T + kc = tl.load( + KC + b * scb + offs_n[:, None] * scn + offs_d[None, :] * scd, + mask=nmask[:, None], + other=0.0, + ) + dk = tl.zeros([BLOCK_N, D], tl.float32) + dv = tl.zeros([BLOCK_N, D], tl.float32) + for start_m in range(0, S, BLOCK_M): # any query may attend any compressed key + offs_m = start_m + tl.arange(0, BLOCK_M) + mmask = offs_m < S + q = tl.load( + Q + b * sqb + h * sqh + offs_m[:, None] * sqm + offs_d[None, :] * sqd, + mask=mmask[:, None], + other=0.0, + ) + do = tl.load( + DO + b * sqb + h * sqh + offs_m[:, None] * sqm + offs_d[None, :] * sqd, + mask=mmask[:, None], + other=0.0, + ) + l_i = tl.load(L + pid_bh * S + offs_m, mask=mmask, other=0.0) + delta = tl.load(Delta + pid_bh * S + offs_m, mask=mmask, other=0.0) + qk = tl.dot(q, tl.trans(kc), input_precision=PREC) * scale + bb = tl.load( + BB + b * sbb + offs_m[:, None] * sbm + offs_n[None, :] * sbt, + mask=mmask[:, None] & nmask[None, :], + other=float("-inf"), + ) + p = tl.where( + mmask[:, None] & nmask[None, :], tl.exp(qk + bb - l_i[:, None]), 0.0 + ) + dp = tl.dot(do, tl.trans(kc), input_precision=PREC) + ds = (p * (dp - delta[:, None]) * scale).to(ACC) + dk += tl.dot(tl.trans(ds), q.to(ACC), input_precision=PREC).to(tl.float32) + dv += tl.dot(tl.trans(p.to(ACC)), do.to(ACC), input_precision=PREC).to( + tl.float32 + ) + ptr = DKC + b * scb + offs_n[:, None] * scn + offs_d[None, :] * scd + tl.atomic_add(ptr, (dk + dv), mask=nmask[:, None]) + else: + start_n = pid_n * BLOCK_N + offs_n = start_n + tl.arange(0, BLOCK_N) + nmask = offs_n < S + ks = tl.load( + KS + b * skb + offs_n[:, None] * skn + offs_d[None, :] * skd, + mask=nmask[:, None], + other=0.0, + ) + dk = tl.zeros([BLOCK_N, D], tl.float32) + dv = tl.zeros([BLOCK_N, D], tl.float32) + lo = (start_n // BLOCK_M) * BLOCK_M + hi = tl.minimum(S, start_n + BLOCK_N + WINDOW - 1) + for start_m in range(lo, hi, BLOCK_M): + offs_m = start_m + tl.arange(0, BLOCK_M) + mmask = offs_m < S + q = tl.load( + Q + b * sqb + h * sqh + offs_m[:, None] * sqm + offs_d[None, :] * sqd, + mask=mmask[:, None], + other=0.0, + ) + do = tl.load( + DO + b * sqb + h * sqh + offs_m[:, None] * sqm + offs_d[None, :] * sqd, + mask=mmask[:, None], + other=0.0, + ) + l_i = tl.load(L + pid_bh * S + offs_m, mask=mmask, other=0.0) + delta = tl.load(Delta + pid_bh * S + offs_m, mask=mmask, other=0.0) + qk = tl.dot(q, tl.trans(ks), input_precision=PREC) * scale + valid = ( + (offs_n[None, :] <= offs_m[:, None]) + & (offs_m[:, None] - offs_n[None, :] < WINDOW) + & nmask[None, :] + ) + p = tl.where(valid, tl.exp(qk - l_i[:, None]), 0.0) + dp = tl.dot(do, tl.trans(ks), input_precision=PREC) + ds = (p * (dp - delta[:, None]) * scale).to(ACC) + dk += tl.dot(tl.trans(ds), q.to(ACC), input_precision=PREC).to(tl.float32) + dv += tl.dot(tl.trans(p.to(ACC)), do.to(ACC), input_precision=PREC).to( + tl.float32 + ) + ptr = DKS + b * skb + offs_n[:, None] * skn + offs_d[None, :] * skd + tl.atomic_add(ptr, (dk + dv), mask=nmask[:, None]) + + +def _csa_bwd(q, ks, kc, bb, sinks, out, do, L, scale, window): + B, H, S, D = q.shape + T = kc.shape[1] + do = do.contiguous() + delta = (do.to(torch.float32) * out.to(torch.float32)).sum(-1).reshape(B * H, S) + dq = torch.empty_like(q) + dks = torch.zeros_like(ks, dtype=torch.float32) + dkc = torch.zeros_like(kc, dtype=torch.float32) + dsink = torch.zeros_like(sinks, dtype=torch.float32) + ACC = tl.bfloat16 if q.dtype == torch.bfloat16 else tl.float32 + PREC = "ieee" if q.element_size() == 4 else "tf32" + EL = q.element_size() + qs = (q.stride(0), q.stride(1), q.stride(2), q.stride(3)) + ss = (ks.stride(0), ks.stride(1), ks.stride(2)) + cs = (kc.stride(0), kc.stride(1), kc.stride(2)) + bs = (bb.stride(0), bb.stride(1), bb.stride(2)) + _csa_bwd_dq_kernel[lambda m: (triton.cdiv(S, m["BLOCK_M"]), B * H)]( + q, + ks, + kc, + bb, + sinks, + do, + L, + delta, + dq, + dsink, + scale, + *qs, + *ss, + *cs, + *bs, + H, + S, + T, + window, + EL, + D=D, + ACC=ACC, + PREC=PREC, + ) + _csa_bwd_dkv_kernel[ + lambda m: (triton.cdiv(S, m["BLOCK_N"]) + triton.cdiv(T, m["BLOCK_N"]), B * H) + ]( + q, + ks, + kc, + bb, + do, + L, + delta, + dks, + dkc, + scale, + *qs, + *ss, + *cs, + *bs, + H, + S, + T, + window, + EL, + D=D, + ACC=ACC, + PREC=PREC, + ) + return dq, dks.to(ks.dtype), dkc.to(kc.dtype), dsink.to(sinks.dtype) + + +class _CSAAttn(torch.autograd.Function): + @staticmethod + def forward(ctx, q, ks, kc, bb, sinks, scale, window): + out, L = _csa_fwd(q, ks, kc, bb, sinks, scale, window) + ctx.save_for_backward(q, ks, kc, bb, sinks, out, L) + ctx.scale, ctx.window = scale, window + return out + + @staticmethod + def backward(ctx, do): + q, ks, kc, bb, sinks, out, L = ctx.saved_tensors + dq, dks, dkc, dsink = _csa_bwd( + q, ks, kc, bb, sinks, out, do, L, ctx.scale, ctx.window + ) + return dq, dks, dkc, None, dsink, None, None + + +def csa_attn(q, kv_slide, kv_comp, block_bias, sinks, scale=None, sliding_window=128): + """Core attention for CSA/HCA layers. + q: [B,H,S,D]; kv_slide: [B,1,S,D] (K==V); kv_comp: [B,1,T,D] (K==V); + block_bias: [B,1,S,T] additive; sinks: [H]. Returns [B,H,S,D]. + + NOTE: the dk/dv backward over the compressed block uses ``BLOCK_N=16`` for the grid's + block count (matches the smallest autotune tile); the kernel masks any tail.""" + B, H, S, D = q.shape + if scale is None: + scale = D**-0.5 + # compressed KV can arrive fp32 (keep_in_fp32 compressor); match all to q (no mixed dtypes). + dt = q.dtype + kv_slide = kv_slide.to(dt) if kv_slide.dtype != dt else kv_slide + kv_comp = kv_comp.to(dt) if kv_comp.dtype != dt else kv_comp + sinks = sinks.to(dt) if sinks.dtype != dt else sinks + ks = kv_slide[:, 0].contiguous() + kc = kv_comp[:, 0].contiguous() + bb = block_bias[:, 0].contiguous() + return _CSAAttn.apply( + q.contiguous(), ks, kc, bb, sinks.contiguous(), scale, sliding_window + ) diff --git a/src/axolotl/integrations/kernels/libs/dsv4/attention_gather.py b/src/axolotl/integrations/kernels/libs/dsv4/attention_gather.py new file mode 100644 index 0000000000..42f60aa50b --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/dsv4/attention_gather.py @@ -0,0 +1,424 @@ +"""Top-k GATHER sparse-MLA for DeepSeek-V4 CSA/HCA core attention (fwd + bwd). + +Head-batched, MMA-efficient. An earlier per-query-position layout lost tensor cores +(each position has a distinct top-k set, so gathered keys can't be shared across a +position-block -> scalar dots). DeepSeek's TileLang sparse-MLA avoids this by putting the +**H query heads on the MMA M-axis**: in MLA/MQA all H heads at one position share the same +KV and the same per-position top-k, so a gathered tile of BN keys feeds a full +``q[BH,D] @ Kᵀ[D,BN]`` MMA. We do the same. + +One program = (query position s, head-tile of BH heads, batch b). It attends to the +sliding window (≤ ``window`` causal keys, MMA) and the position's ``K`` gathered top-k +compressed keys (gathered in BN-chunks, MMA), plus the per-head sink. Turns the compressed +term from O(S*T) (dense) to O(S*K) while keeping tensor cores busy. + +``topk_idx`` is [B, S, K] int32 (-1 = invalid, as ``DeepseekV4Indexer`` returns); kv_comp +is shared K==V (grad scatter-added). int64 offsets so it's correct past 64k context. +""" + +import torch +import triton +import triton.language as tl + +from .attention import _max_m, _smem_limit + +# BH (head tile, MMA M-axis) must be >= 16 (tensor-core min M). bwd also holds do + +# transposes, so it needs smaller tiles; SMEM-pruned per GPU (sm_120 ~99KB vs H100/B200 ~228KB). +_GCFGS = [ + triton.Config({"BH": bh, "BN": bn}, num_warps=w, num_stages=s) + for bh in (16, 32, 64) + for bn in (16, 32, 64) + for w in (4, 8) + for s in (1, 2) + if not (bn == 16 and bh >= 64) +] # N=16 wgmma at large M crashes on Hopper/Blackwell + + +def _gprune(n_tiles): + def prune(configs, nargs, **kwargs): + D = kwargs["D"] + EL = nargs["EL"] + dev = torch.cuda.current_device() + budget = int(_smem_limit(dev) * 0.9) + max_m = _max_m(dev) # Blackwell tmem caps the [BH,512] acc at BH<=32 + kept = [ + c + for c in configs + if (n_tiles * c.kwargs["BH"] + n_tiles * c.num_stages * c.kwargs["BN"]) + * D + * EL + <= budget + and c.kwargs["BH"] <= max_m + ] + return kept or [ + min(configs, key=lambda c: c.kwargs["BH"] * c.kwargs["BN"] * c.num_stages) + ] + + return prune + + +@triton.autotune( + configs=_GCFGS, + key=["H", "K", "EL"], + prune_configs_by={"early_config_prune": _gprune(1)}, +) +@triton.jit +def _gather_fwd_kernel( + Q, + KS, + KC, + IDX, + sinks, + Out, + L, + scale, + sqb, + sqh, + sqm, + sqd, + skb, + skn, + skd, + scb, + scn, + scd, + sib, + sim, + sik, + H, + S, + K, + WINDOW, + EL, + D: tl.constexpr, + BH: tl.constexpr, + BN: tl.constexpr, + ACC: tl.constexpr, + PREC: tl.constexpr, +): + NHB = tl.cdiv(H, BH) + s = tl.program_id(0).to(tl.int64) + pid = tl.program_id(1) + b = (pid // NHB).to(tl.int64) + hb = pid % NHB + offs_h = (hb * BH + tl.arange(0, BH)).to(tl.int64) + offs_d = tl.arange(0, D).to(tl.int64) + hmask = offs_h < H + + q = tl.load( + Q + b * sqb + offs_h[:, None] * sqh + s * sqm + offs_d[None, :] * sqd, + mask=hmask[:, None], + other=0.0, + ) + sink = tl.load(sinks + offs_h, mask=hmask, other=0.0).to(tl.float32) + m_i = sink + l_i = tl.zeros([BH], tl.float32) + 1.0 + acc = tl.zeros([BH, D], tl.float32) + + # causal sliding window for position s: j in (s-window, s] + lo = (tl.maximum(0, s - WINDOW + 1) // BN) * BN + hi = s + 1 + for start_n in range(lo, hi, BN): + offs_n = start_n + tl.arange(0, BN) + nmask = (offs_n <= s) & (s - offs_n < WINDOW) & (offs_n < S) + k = tl.load( + KS + b * skb + offs_n[:, None] * skn + offs_d[None, :] * skd, + mask=nmask[:, None], + other=0.0, + ) + qk = tl.dot(q, tl.trans(k), input_precision=PREC) * scale + qk = tl.where(nmask[None, :], qk, float("-inf")) + m_new = tl.maximum(m_i, tl.max(qk, 1)) + alpha = tl.exp(m_i - m_new) + p = tl.exp(qk - m_new[:, None]) + l_i = l_i * alpha + tl.sum(p, 1) + acc = acc * alpha[:, None] + tl.dot( + p.to(ACC), k.to(ACC), input_precision=PREC + ).to(tl.float32) + m_i = m_new + + for start_k in range(0, K, BN): + kidx = start_k + tl.arange(0, BN) + idx = tl.load(IDX + b * sib + s * sim + kidx * sik, mask=kidx < K, other=-1) + gv = idx >= 0 + idxs = tl.where(gv, idx, 0).to(tl.int64) + kc = tl.load( + KC + b * scb + idxs[:, None] * scn + offs_d[None, :] * scd, + mask=gv[:, None], + other=0.0, + ) + qk = tl.dot(q, tl.trans(kc), input_precision=PREC) * scale + qk = tl.where(gv[None, :], qk, float("-inf")) + m_new = tl.maximum(m_i, tl.max(qk, 1)) + alpha = tl.exp(m_i - m_new) + p = tl.exp(qk - m_new[:, None]) + l_i = l_i * alpha + tl.sum(p, 1) + acc = acc * alpha[:, None] + tl.dot( + p.to(ACC), kc.to(ACC), input_precision=PREC + ).to(tl.float32) + m_i = m_new + + acc = acc / l_i[:, None] + tl.store( + Out + b * sqb + offs_h[:, None] * sqh + s * sqm + offs_d[None, :] * sqd, + acc.to(Out.dtype.element_ty), + mask=hmask[:, None], + ) + tl.store(L + b * (H * S) + offs_h * S + s, m_i + tl.log(l_i), mask=hmask) + + +@triton.autotune( + configs=_GCFGS, + key=["H", "K", "EL"], + reset_to_zero=["DKS", "DKC", "DSINK"], + prune_configs_by={"early_config_prune": _gprune(2)}, +) +@triton.jit +def _gather_bwd_kernel( + Q, + KS, + KC, + IDX, + sinks, + DO, + L, + Delta, + DQ, + DKS, + DKC, + DSINK, + scale, + sqb, + sqh, + sqm, + sqd, + skb, + skn, + skd, + scb, + scn, + scd, + sib, + sim, + sik, + H, + S, + K, + WINDOW, + EL, + D: tl.constexpr, + BH: tl.constexpr, + BN: tl.constexpr, + ACC: tl.constexpr, + PREC: tl.constexpr, +): + NHB = tl.cdiv(H, BH) + s = tl.program_id(0).to(tl.int64) + pid = tl.program_id(1) + b = (pid // NHB).to(tl.int64) + hb = pid % NHB + offs_h = (hb * BH + tl.arange(0, BH)).to(tl.int64) + offs_d = tl.arange(0, D).to(tl.int64) + hmask = offs_h < H + + q = tl.load( + Q + b * sqb + offs_h[:, None] * sqh + s * sqm + offs_d[None, :] * sqd, + mask=hmask[:, None], + other=0.0, + ) + do = tl.load( + DO + b * sqb + offs_h[:, None] * sqh + s * sqm + offs_d[None, :] * sqd, + mask=hmask[:, None], + other=0.0, + ) + l_i = tl.load(L + b * (H * S) + offs_h * S + s, mask=hmask, other=0.0) + delta = tl.load(Delta + b * (H * S) + offs_h * S + s, mask=hmask, other=0.0) + sink = tl.load(sinks + offs_h, mask=hmask, other=0.0).to(tl.float32) + dq = tl.zeros([BH, D], tl.float32) + + lo = (tl.maximum(0, s - WINDOW + 1) // BN) * BN + hi = s + 1 + for start_n in range(lo, hi, BN): + offs_n = start_n + tl.arange(0, BN) + nmask = (offs_n <= s) & (s - offs_n < WINDOW) & (offs_n < S) + k = tl.load( + KS + b * skb + offs_n[:, None] * skn + offs_d[None, :] * skd, + mask=nmask[:, None], + other=0.0, + ) + qk = tl.dot(q, tl.trans(k), input_precision=PREC) * scale + p = tl.where(nmask[None, :], tl.exp(qk - l_i[:, None]), 0.0) + dp = tl.dot(do, tl.trans(k), input_precision=PREC) + ds = (p * (dp - delta[:, None]) * scale).to(ACC) + dq += tl.dot(ds, k.to(ACC), input_precision=PREC).to(tl.float32) + dkv = tl.dot(tl.trans(ds), q.to(ACC), input_precision=PREC).to( + tl.float32 + ) + tl.dot(tl.trans(p.to(ACC)), do.to(ACC), input_precision=PREC).to(tl.float32) + tl.atomic_add( + DKS + b * skb + offs_n[:, None] * skn + offs_d[None, :] * skd, + dkv, + mask=nmask[:, None], + ) + + for start_k in range(0, K, BN): + kidx = start_k + tl.arange(0, BN) + idx = tl.load(IDX + b * sib + s * sim + kidx * sik, mask=kidx < K, other=-1) + gv = idx >= 0 + idxs = tl.where(gv, idx, 0).to(tl.int64) + kc = tl.load( + KC + b * scb + idxs[:, None] * scn + offs_d[None, :] * scd, + mask=gv[:, None], + other=0.0, + ) + qk = tl.dot(q, tl.trans(kc), input_precision=PREC) * scale + p = tl.where(gv[None, :], tl.exp(qk - l_i[:, None]), 0.0) + dp = tl.dot(do, tl.trans(kc), input_precision=PREC) + ds = (p * (dp - delta[:, None]) * scale).to(ACC) + dq += tl.dot(ds, kc.to(ACC), input_precision=PREC).to(tl.float32) + dkv = tl.dot(tl.trans(ds), q.to(ACC), input_precision=PREC).to( + tl.float32 + ) + tl.dot(tl.trans(p.to(ACC)), do.to(ACC), input_precision=PREC).to(tl.float32) + tl.atomic_add( + DKC + b * scb + idxs[:, None] * scn + offs_d[None, :] * scd, + dkv, + mask=gv[:, None], + ) + + p_sink = tl.exp(sink - l_i) + # mask the atomic itself: zeroing the value isn't enough, offs_h is out-of-bounds for + # padded heads (H % BH != 0). + tl.atomic_add(DSINK + offs_h, tl.where(hmask, -p_sink * delta, 0.0), mask=hmask) + tl.store( + DQ + b * sqb + offs_h[:, None] * sqh + s * sqm + offs_d[None, :] * sqd, + dq.to(DQ.dtype.element_ty), + mask=hmask[:, None], + ) + + +class _CSATopK(torch.autograd.Function): + @staticmethod + def forward(ctx, q, ks, kc, idx, sinks, scale, window): + B, H, S, D = q.shape + K = idx.shape[2] + out = torch.empty(B, H, S, D, device=q.device, dtype=q.dtype) + L = torch.empty(B, H, S, device=q.device, dtype=torch.float32) + ACC = tl.bfloat16 if q.dtype == torch.bfloat16 else tl.float32 + PREC = "ieee" if q.element_size() == 4 else "tf32" + args = ( + q.stride(0), + q.stride(1), + q.stride(2), + q.stride(3), + ks.stride(0), + ks.stride(1), + ks.stride(2), + kc.stride(0), + kc.stride(1), + kc.stride(2), + idx.stride(0), + idx.stride(1), + idx.stride(2), + ) + grid = lambda m: (S, B * triton.cdiv(H, m["BH"])) + _gather_fwd_kernel[grid]( + q, + ks, + kc, + idx, + sinks, + out, + L, + scale, + *args, + H, + S, + K, + window, + q.element_size(), + D=D, + ACC=ACC, + PREC=PREC, + ) + ctx.save_for_backward(q, ks, kc, idx, sinks, out, L) + ctx.scale, ctx.window = scale, window + return out + + @staticmethod + def backward(ctx, do): + q, ks, kc, idx, sinks, out, L = ctx.saved_tensors + B, H, S, D = q.shape + K = idx.shape[2] + do = do.contiguous() + delta = (do.to(torch.float32) * out.to(torch.float32)).sum(-1) # [B,H,S] + dq = torch.empty_like(q) + dks = torch.zeros_like(ks, dtype=torch.float32) + dkc = torch.zeros_like(kc, dtype=torch.float32) + dsink = torch.zeros_like(sinks, dtype=torch.float32) + ACC = tl.bfloat16 if q.dtype == torch.bfloat16 else tl.float32 + PREC = "ieee" if q.element_size() == 4 else "tf32" + args = ( + q.stride(0), + q.stride(1), + q.stride(2), + q.stride(3), + ks.stride(0), + ks.stride(1), + ks.stride(2), + kc.stride(0), + kc.stride(1), + kc.stride(2), + idx.stride(0), + idx.stride(1), + idx.stride(2), + ) + grid = lambda m: (S, B * triton.cdiv(H, m["BH"])) + _gather_bwd_kernel[grid]( + q, + ks, + kc, + idx, + sinks, + do, + L, + delta, + dq, + dks, + dkc, + dsink, + ctx.scale, + *args, + H, + S, + K, + ctx.window, + q.element_size(), + D=D, + ACC=ACC, + PREC=PREC, + ) + return ( + dq, + dks.to(ks.dtype), + dkc.to(kc.dtype), + None, + dsink.to(sinks.dtype), + None, + None, + ) + + +def csa_attn_topk( + q, kv_slide, kv_comp, topk_idx, sinks, scale=None, sliding_window=128 +): + """Top-k gather Compressed Sparse Attention (head-batched, MMA, linear in compressed dim). + q: [B,H,S,D]; kv_slide/kv_comp: [B,1,*,D] (K==V); topk_idx: [B,S,K] int32 (-1=invalid); + sinks: [H]. Returns [B,H,S,D].""" + B, H, S, D = q.shape + if scale is None: + scale = D**-0.5 + ks = kv_slide[:, 0].contiguous() + kc = kv_comp[:, 0].contiguous() + idx = topk_idx.contiguous().to(torch.int32) + return _CSATopK.apply( + q.contiguous(), ks, kc, idx, sinks.contiguous(), scale, sliding_window + ) diff --git a/src/axolotl/integrations/kernels/libs/dsv4/gated_pool.py b/src/axolotl/integrations/kernels/libs/dsv4/gated_pool.py new file mode 100644 index 0000000000..82c6a5c36d --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/dsv4/gated_pool.py @@ -0,0 +1,104 @@ +"""Fused gated-softmax pooling for DeepSeek-V4 compressors (fwd + bwd). + +Reference (CSA/HCA compressor §2.3.1/§2.3.2 and the indexer): + compressed = (kv * gate.softmax(dim=-2, dtype=fp32)).sum(dim=-2) +i.e. per (window, channel) a softmax over the W window tokens, then a weighted sum. +Eager runs softmax (fp32) + multiply + reduce as separate passes over [.., W, Dh]; +this fuses them into one kernel, fp32 throughout. + +Per output element: out[m,d] = sum_w p[m,w,d] * kv[m,w,d], p = softmax_w(gate) +Backward: d_kv = p * d_out; d_gate = p * d_out * (kv - out) +""" + +import torch +import triton +import triton.language as tl + + +@triton.autotune( + configs=[ + triton.Config({"BD": bd}, num_warps=w) + for bd in (32, 64, 128) + for w in (2, 4, 8) + ], + key=["M", "D"], +) +@triton.jit +def _pool_fwd_kernel(KV, GATE, OUT, M, D, W: tl.constexpr, BD: tl.constexpr): + m = tl.program_id(0) + d = tl.program_id(1) * BD + tl.arange(0, BD) + dmask = d < D + wkv = tl.arange(0, W) + off = m * (W * D) + wkv[:, None] * D + d[None, :] + cmask = dmask[None, :] + g = tl.load(GATE + off, mask=cmask, other=float("-inf")).to(tl.float32) + kv = tl.load(KV + off, mask=cmask, other=0.0).to(tl.float32) + p = tl.exp(g - tl.max(g, axis=0, keep_dims=True)) + p = p / tl.sum(p, axis=0, keep_dims=True) + out = tl.sum(p * kv, axis=0) + tl.store(OUT + m * D + d, out, mask=dmask) + + +@triton.autotune( + configs=[ + triton.Config({"BD": bd}, num_warps=w) + for bd in (32, 64, 128) + for w in (2, 4, 8) + ], + key=["M", "D"], +) +@triton.jit +def _pool_bwd_kernel( + KV, GATE, DOUT, DKV, DGATE, M, D, W: tl.constexpr, BD: tl.constexpr +): + m = tl.program_id(0) + d = tl.program_id(1) * BD + tl.arange(0, BD) + dmask = d < D + wkv = tl.arange(0, W) + off = m * (W * D) + wkv[:, None] * D + d[None, :] + cmask = dmask[None, :] + g = tl.load(GATE + off, mask=cmask, other=float("-inf")).to(tl.float32) + kv = tl.load(KV + off, mask=cmask, other=0.0).to(tl.float32) + p = tl.exp(g - tl.max(g, axis=0, keep_dims=True)) + p = p / tl.sum(p, axis=0, keep_dims=True) + out = tl.sum(p * kv, axis=0, keep_dims=True) + dout = tl.load(DOUT + m * D + d, mask=dmask, other=0.0).to(tl.float32)[None, :] + dkv = p * dout + dgate = p * dout * (kv - out) + tl.store(DKV + off, dkv, mask=cmask) + tl.store(DGATE + off, dgate, mask=cmask) + + +class _GatedPool(torch.autograd.Function): + @staticmethod + def forward(ctx, kv, gate): + M, W, D = kv.shape + kv = kv.contiguous() + gate = gate.contiguous() + out = torch.empty(M, D, device=kv.device, dtype=torch.float32) + grid = lambda m: (M, triton.cdiv(D, m["BD"])) + _pool_fwd_kernel[grid](kv, gate, out, M, D, W=W) + ctx.save_for_backward(kv, gate) + ctx.dims = (M, W, D) + return out + + @staticmethod + def backward(ctx, dout): + kv, gate = ctx.saved_tensors + M, W, D = ctx.dims + dkv = torch.empty_like(kv, dtype=torch.float32) + dgate = torch.empty_like(gate, dtype=torch.float32) + grid = lambda m: (M, triton.cdiv(D, m["BD"])) + _pool_bwd_kernel[grid](kv, gate, dout.contiguous(), dkv, dgate, M, D, W=W) + return dkv.to(kv.dtype), dgate.to(gate.dtype) + + +def gated_softmax_pool(kv: torch.Tensor, gate: torch.Tensor) -> torch.Tensor: + """kv, gate: [..., W, D]. Returns [..., D] = sum_w softmax_w(gate) * kv (fp32). + Drop-in for ``(kv * gate.softmax(dim=-2, dtype=fp32)).sum(dim=-2)``. W must be a power + of 2 (compress_rate / 2*compress_rate are: HCA 128, CSA-overlap 8).""" + if gate.dtype != kv.dtype: + gate = gate.to(kv.dtype) + *lead, W, D = kv.shape + out = _GatedPool.apply(kv.reshape(-1, W, D), gate.reshape(-1, W, D)) + return out.reshape(*lead, D) diff --git a/src/axolotl/integrations/kernels/libs/dsv4/indexer.py b/src/axolotl/integrations/kernels/libs/dsv4/indexer.py new file mode 100644 index 0000000000..73d6719cbd --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/dsv4/indexer.py @@ -0,0 +1,138 @@ +"""Fused Lightning-Indexer scorer for DeepSeek-V4 CSA (forward-only). + +Reference (DeepseekV4IndexerScorer.forward): + scores = matmul(q.float(), ck.float().transpose).unsqueeze(1) # [B, S, H, T] + scores = relu(scores) * softmax_scale + weights = weights_proj(hidden).float() * weights_scaling # [B, S, H] + index_scores = (scores * weights.unsqueeze(-1)).sum(dim=2) # [B, S, T] + +i.e. out[b,s,t] = softmax_scale * Σ_h w[b,s,h] · relu(Σ_d q[b,s,h,d]·ck[b,t,d]). + +Eager materializes the full [B,S,H,T] fp32 score tensor twice (matmul output + the +``scores*weights`` product) — with H=64 index heads this is the #1 transient memory +hotspot. Fusing the H-reduction collapses it straight to [B,S,T], never holding the +H axis. + +FORWARD ONLY: ``index_scores`` is consumed solely by ``topk(...).indices`` (a non- +differentiable LongTensor) to build the gather mask, so no gradient ever flows back +through the scorer. We return a plain (no-grad) tensor — matching eager semantics — and +skip the backward entirely. +""" + +import torch +import triton +import triton.language as tl + + +@triton.autotune( + configs=[ + triton.Config({"BS": bs, "BT": bt}, num_warps=w, num_stages=s) + for bs in (32, 64, 128) + for bt in (32, 64, 128) + for w in (4, 8) + for s in (2, 3) + ], + key=["S", "T", "H"], +) +@triton.jit +def _indexer_score_kernel( + Q, + CK, + W, + OUT, + softmax_scale, + S, + T, + H, + sq_b, + sq_s, + sq_h, + sq_d, + sck_b, + sck_t, + sck_d, + sw_b, + sw_s, + sw_h, + so_b, + so_s, + so_t, + DH: tl.constexpr, + BS: tl.constexpr, + BT: tl.constexpr, +): + b = tl.program_id(0) + s0 = tl.program_id(1) * BS + t0 = tl.program_id(2) * BT + offs_s = s0 + tl.arange(0, BS) + offs_t = t0 + tl.arange(0, BT) + offs_d = tl.arange(0, DH) + smask = offs_s < S + tmask = offs_t < T + + # ck is independent of h, so load it once outside the head loop + ck_ptr = CK + b * sck_b + offs_d[:, None] * sck_d + offs_t[None, :] * sck_t + ck = tl.load(ck_ptr, mask=tmask[None, :], other=0.0).to(tl.float32) + + acc = tl.zeros((BS, BT), dtype=tl.float32) + for h in range(H): + q_ptr = ( + Q + b * sq_b + offs_s[:, None] * sq_s + h * sq_h + offs_d[None, :] * sq_d + ) + q = tl.load(q_ptr, mask=smask[:, None], other=0.0).to(tl.float32) + qk = tl.dot(q, ck, input_precision="ieee") + qk = tl.maximum(qk, 0.0) * softmax_scale + w = tl.load(W + b * sw_b + offs_s * sw_s + h * sw_h, mask=smask, other=0.0).to( + tl.float32 + ) + acc += qk * w[:, None] + + out_ptr = OUT + b * so_b + offs_s[:, None] * so_s + offs_t[None, :] * so_t + tl.store(out_ptr, acc, mask=smask[:, None] & tmask[None, :]) + + +def indexer_scores( + q: torch.Tensor, + compressed_kv: torch.Tensor, + weights: torch.Tensor, + softmax_scale: float, +) -> torch.Tensor: + """q [B,S,H,Dh], compressed_kv [B,T,Dh], weights [B,S,H] (already ·weights_scaling). + Returns index_scores [B,S,T] (fp32, no grad). Drop-in for DeepseekV4IndexerScorer.""" + B, S, H, DH = q.shape + T = compressed_kv.shape[1] + out = torch.empty(B, S, T, device=q.device, dtype=torch.float32) + if T == 0 or S == 0: + return out + # compressed_kv may arrive fp32 (keep_in_fp32 compressor) while q is bf16. + if compressed_kv.dtype != q.dtype: + compressed_kv = compressed_kv.to(q.dtype) + q = q.contiguous() + ck = compressed_kv.contiguous() + w = weights.contiguous() + grid = lambda m: (B, triton.cdiv(S, m["BS"]), triton.cdiv(T, m["BT"])) + _indexer_score_kernel[grid]( + q, + ck, + w, + out, + float(softmax_scale), + S, + T, + H, + q.stride(0), + q.stride(1), + q.stride(2), + q.stride(3), + ck.stride(0), + ck.stride(1), + ck.stride(2), + w.stride(0), + w.stride(1), + w.stride(2), + out.stride(0), + out.stride(1), + out.stride(2), + DH=DH, + ) + return out diff --git a/src/axolotl/integrations/kernels/libs/dsv4/lora_fp8.py b/src/axolotl/integrations/kernels/libs/dsv4/lora_fp8.py new file mode 100644 index 0000000000..896afeb818 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/dsv4/lora_fp8.py @@ -0,0 +1,147 @@ +"""Native blockwise-FP8 fused LoRA for DeepSeek-V4 non-expert projections. + +PEFT's default path runs the frozen FP8 base as a dequant→bf16 GEMM (no fp8 throughput) and +the LoRA as a separate pass. For the *large-output* attention projections (``q_b_proj`` 1024→ +32768, ``o_b_proj`` 8192→4096) a native blockwise-FP8 GEMM is 2.4–3.2× faster than the +dequant path (benched); the small ones (``q_a``/``kv``) are a wash, so we patch only the big +two. + +``torch._scaled_mm`` (and DeepGEMM) have no autograd formula, so the native fp8 forward has to +live inside a custom ``autograd.Function``: + * forward — ``fp8_linear`` (DeepGEMM 128×128 on B200, Triton fallback elsewhere) for the + frozen base, fused with the LoRA update via ``addmm_``; + * backward — ``dX`` through a bf16 dequant of the weight (Option A: the block scale sits on + the contraction axis, so an fp8 ``dX`` would need a transposed fp8 copy = 2× weight mem), + plus the LoRA ``dA``/``dB``. The base weight is frozen (no weight grad). + +Weight stays 128×128 blockwise (lossless vs the checkpoint); only the activation is quantized +(1×128), matching the DeepSeek/NVIDIA recipe. Gated by config ``dsv4_fp8_lora_kernel``. +""" + +from __future__ import annotations + +import types + +import torch + +from axolotl.kernels.lora import get_lora_parameters +from axolotl.kernels.quantize import dequantize_fp8 +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +# Only the large-output projections where native fp8 beats the dequant GEMM. +_TARGET_SUFFIXES = ("self_attn.q_b_proj", "self_attn.o_b_proj") + + +def _fp8_linear(): + from transformers.integrations.finegrained_fp8 import fp8_linear + + return fp8_linear + + +class _NativeFp8Lora(torch.autograd.Function): + @staticmethod + def forward(ctx, x, qdata, scale_inv, block_size, A, B, scaling, bias): + fp8_linear = _fp8_linear() + shape = x.shape + x2 = x.reshape(-1, shape[-1]) + # frozen base: native blockwise fp8 GEMM (activation quantized 1x128 inside) + out = fp8_linear( + x2, + qdata, + scale_inv, + block_size=list(block_size), + bias=bias, + output_dtype=x.dtype, + ) + # fused LoRA update via addmm_ (avoids a [M,out] temp) + xA = x2 @ A.t() + out = out.addmm_(xA, B.t(), alpha=float(scaling)) + ctx.save_for_backward(x2, qdata, scale_inv, A, B, xA) + ctx.scaling = float(scaling) + ctx.block_size = tuple(block_size) + ctx.shape = shape + return out.view(*shape[:-1], -1) + + @staticmethod + def backward(ctx, grad): + x2, qdata, scale_inv, A, B, xA = ctx.saved_tensors + s = ctx.scaling + g = grad.reshape(-1, grad.shape[-1]) + # dX through a bf16 dequant of W (Option A: block scale on the contraction axis, so an + # fp8 dX would need a transposed fp8 weight copy = 2x weight mem). W transient. + w_deq = dequantize_fp8(qdata, scale_inv, g.dtype) + dxA = g @ B + dX = (g @ w_deq).addmm_(dxA, A, alpha=s) + dA = (dxA.t() * s) @ x2 # [r, in] + dB = (g.t() * s) @ xA # [out, r] + return dX.view(*ctx.shape), None, None, None, dA, dB, None, None + + +def _fp8_from_weight(w, base_layer): + """Return (qdata, scale_inv, block_size) for a Float8Tensor- or raw-FP8 weight, else None.""" + qdata = getattr(w, "qdata", None) + if qdata is not None: # torchao Float8Tensor + return qdata, w.scale, list(getattr(w, "block_size", None) or [128, 128]) + if ( + isinstance(w, torch.Tensor) and w.dtype == torch.float8_e4m3fn + ): # raw transformers FP8 + si = getattr(base_layer, "weight_scale_inv", None) + return ( + (w, si, list(getattr(base_layer, "block_size", None) or [128, 128])) + if si is not None + else None + ) + return None + + +def _base_is_fp8(base_layer): + return _fp8_from_weight(getattr(base_layer, "weight", None), base_layer) is not None + + +def _make_forward(lora_layer): + def forward(self, x, *args, **kwargs): + # get_lora_parameters unshards the LoRA A/B DTensors (FSDP2), grad-tracked. The base + # Float8Tensor is all-gathered by FSDP before forward, so this reads the live weight. + W, bias, _qs, A, B, scaling, _lora_bias, dropout, _mag = get_lora_parameters( + self + ) + fp8 = _fp8_from_weight(W, self.base_layer) + if ( + fp8 is None or A is None + ): # disabled/merged adapter, or base not fp8: fall back + return self._dsv4_orig_forward(x, *args, **kwargs) + qdata, scale_inv, block_size = fp8 + xin = dropout(x) if dropout is not None else x + return _NativeFp8Lora.apply( + xin, qdata, scale_inv, block_size, A, B, scaling, bias + ) + + return forward + + +def patch_dsv4_attn_fp8_lora(model, enabled: bool = False) -> int: + """Swap the forward of LoRA-wrapped large attention projections (q_b/o_b) on an FP8 base + for the native-blockwise-fp8 fused-LoRA Function. No-op unless ``enabled`` + (config ``dsv4_fp8_lora_kernel``).""" + if not enabled: + return 0 + from peft.tuners.lora.layer import LoraLayer + + n = 0 + for name, mod in model.named_modules(): + if not isinstance(mod, LoraLayer): + continue + if not any(name.endswith(suf) for suf in _TARGET_SUFFIXES): + continue + if not _base_is_fp8(mod.base_layer) or hasattr(mod, "_dsv4_orig_forward"): + continue + mod._dsv4_orig_forward = mod.forward # fallback for disabled/merged adapter + mod.forward = types.MethodType(_make_forward(mod), mod) + n += 1 + if n: + LOG.info( + "Patched %d DeepSeek-V4 attention projections with native-fp8 fused LoRA", n + ) + return n diff --git a/src/axolotl/integrations/kernels/libs/dsv4/lora_mlp.py b/src/axolotl/integrations/kernels/libs/dsv4/lora_mlp.py new file mode 100644 index 0000000000..7823fc9c30 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/dsv4/lora_mlp.py @@ -0,0 +1,144 @@ +"""Fused LoRA MLP for DeepSeek-V4's shared-expert (clamped SwiGLU). + +V4's ``DeepseekV4MLP`` is ``down(silu(gate(x).clamp(max=L)) * up(x).clamp(-L, L))`` — the +``swiglu_limit`` clamp is what stops axolotl's stock ``apply_lora_mlp_swiglu`` (no clamp) +from applying. ``LoRA_MLP`` is parameterized by the activation fns, so we reuse all of its +LoRA-matmul fusion and just swap in a clamped SwiGLU activation (forward + backward). + +Only the shared expert is a plain SwiGLU MLP; the 256 routed experts go through +``scattermoe_lora`` (use_scattermoe). The MLA attention projections have no fused-LoRA +form and stay on PEFT default. +""" + +import functools + +import torch +import triton +import triton.language as tl + +from axolotl.kernels.lora import LoRA_MLP, _apply_dropout, get_lora_parameters + + +@triton.jit +def _clamped_swiglu_fwd_kernel( + gate_ptr, up_ptr, out_ptr, n_elements, LIMIT, block_size: tl.constexpr +): + off = tl.program_id(0) * block_size + tl.arange(0, block_size) + mask = off < n_elements + gate = tl.load(gate_ptr + off, mask=mask, other=0).to(tl.float32) + up = tl.load(up_ptr + off, mask=mask, other=0).to(tl.float32) + gate = tl.minimum(gate, LIMIT) # clamp(max=L) + up = tl.minimum(tl.maximum(up, -LIMIT), LIMIT) # clamp(-L, L) + f = gate * tl.sigmoid(gate) + tl.store(out_ptr + off, (f * up).to(out_ptr.dtype.element_ty), mask=mask) + + +@triton.jit +def _clamped_swiglu_bwd_kernel( + grad_out_ptr, gate_ptr, up_ptr, n_elements, LIMIT, block_size: tl.constexpr +): + off = tl.program_id(0) * block_size + tl.arange(0, block_size) + mask = off < n_elements + grad_out = tl.load(grad_out_ptr + off, mask=mask, other=0).to(tl.float32) + g_raw = tl.load(gate_ptr + off, mask=mask, other=0).to(tl.float32) + u_raw = tl.load(up_ptr + off, mask=mask, other=0).to(tl.float32) + gate = tl.minimum(g_raw, LIMIT) + up = tl.minimum(tl.maximum(u_raw, -LIMIT), LIMIT) + sig = tl.sigmoid(gate) + silu = sig * gate + h = silu * up + # clamp gradients are 0 where the input was clamped + g_mask = (g_raw < LIMIT).to(tl.float32) + u_mask = ((u_raw > -LIMIT) & (u_raw < LIMIT)).to(tl.float32) + grad_up = grad_out * silu * u_mask + grad_gate = grad_out * up * sig * (1.0 + gate * (1.0 - sig)) * g_mask + ety = grad_out_ptr.dtype.element_ty + tl.store(grad_out_ptr + off, h.to(ety), mask=mask) + tl.store(gate_ptr + off, grad_gate.to(ety), mask=mask) + tl.store(up_ptr + off, grad_up.to(ety), mask=mask) + + +def _clamped_swiglu_forward(gate, up, limit): + n = gate.numel() + out = torch.empty_like(gate) + grid = lambda m: (triton.cdiv(n, m["block_size"]),) + _clamped_swiglu_fwd_kernel[grid](gate, up, out, n, float(limit), block_size=1024) + return out + + +def _clamped_swiglu_backward(grad_output, gate, up, limit): + n = grad_output.numel() + grid = lambda m: (triton.cdiv(n, m["block_size"]),) + _clamped_swiglu_bwd_kernel[grid]( + grad_output, gate, up, n, float(limit), block_size=1024 + ) + return grad_output, gate, up + + +def apply_lora_mlp_clamped_swiglu(self, X, inplace: bool = False): + """Drop-in ``DeepseekV4MLP.forward`` using fused LoRA + clamped SwiGLU (``self.limit``). + + ``inplace=False``: the shared expert's input aliases the MoE residual/routed-experts + input, so in-place intermediates would corrupt autograd.""" + gateW, gateb, gateQ, gateA, gateB, gateS, gateLB, gateDrop, gateMag = ( + get_lora_parameters(self.gate_proj) + ) + upW, upb, upQ, upA, upB, upS, upLB, upDrop, upMag = get_lora_parameters( + self.up_proj + ) + downW, downb, downQ, downA, downB, downS, downLB, downDrop, downMag = ( + get_lora_parameters(self.down_proj) + ) + X_drop = _apply_dropout(gateDrop, X, self.training) + fwd = functools.partial(_clamped_swiglu_forward, limit=self.limit) + bwd = functools.partial(_clamped_swiglu_backward, limit=self.limit) + return LoRA_MLP.apply( + X, + X_drop, + gateW, + gateb, + gateQ, + gateA, + gateB, + gateS, + gateLB, + gateMag, + upW, + upb, + upQ, + upA, + upB, + upS, + upLB, + upMag, + downW, + downb, + downQ, + downA, + downB, + downS, + downLB, + downMag, + fwd, + bwd, + inplace, + ) + + +def patch_dsv4_shared_mlp_lora(model): + """Swap each LoRA'd shared-expert ``DeepseekV4MLP.forward`` for the fused clamped-SwiGLU + LoRA kernel. Per-instance (only where gate/up/down carry LoRA), like axolotl's core + lora_kernels. Returns the count patched.""" + import types + + n = 0 + for module in model.modules(): + if type(module).__name__ != "DeepseekV4MLP": + continue + if all( + hasattr(getattr(module, p, None), "lora_A") + for p in ("gate_proj", "up_proj", "down_proj") + ): + module.forward = types.MethodType(apply_lora_mlp_clamped_swiglu, module) + n += 1 + return n diff --git a/src/axolotl/integrations/kernels/libs/dsv4/mhc.py b/src/axolotl/integrations/kernels/libs/dsv4/mhc.py new file mode 100644 index 0000000000..d2e17e30e9 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/dsv4/mhc.py @@ -0,0 +1,609 @@ +"""Fused Triton mHC mixer for DeepSeek-V4 HyperConnection (fwd + bwd). + +``DeepseekV4HyperConnection`` runs twice per layer. Its FLOPs are tiny but the +forward/backward are dominated by ~40 tiny kernel launches from the 20-iteration +Sinkhorn-Knopp loop over [B,S,4,4] matrices. torch.compile fuses the launches but +mis-compiles ``collapsed`` to NaN on torch 2.11, so we fuse the +sigmoid/softmax/Sinkhorn "mixer" (mixes -> pre, post, comb) into a single Triton +kernel pair instead. The rmsnorm + fn-linear and the ``collapsed`` reduction stay in +eager torch (correct, memory-bound, not the launch bottleneck). + +Sinkhorn (matches reference): comb = softmax(cl, -1) + eps; colnorm; then +(iters-1)x [rownorm, colnorm]. Backward recomputes the forward, stores each +normalize's input, and reverse-modes through them (normalize backward: +dx = (dy - sum(dy*y, axis)) / (sum(x,axis)+eps)). +""" + +import torch +import triton +import triton.language as tl + + +@triton.autotune( + configs=[ + triton.Config({"BM": bm}, num_warps=w) for bm in (8, 16, 32, 64) for w in (2, 4) + ], + key=["M"], +) +@triton.jit +def _mhc_fwd_kernel( + MIX, + SCALE, + BASE, + PRE, + POST, + COMB, + STATES, + M, + HC: tl.constexpr, + MIX_N: tl.constexpr, + ITERS: tl.constexpr, + NSTATES: tl.constexpr, + EPS: tl.constexpr, + POSTMULT: tl.constexpr, + SAVE: tl.constexpr, + BM: tl.constexpr, +): + pid = tl.program_id(0) + offs = pid * BM + tl.arange(0, BM) + mask = offs < M + j = tl.arange(0, HC) + jj = tl.arange(0, HC * HC) + s0 = tl.load(SCALE + 0) + s1 = tl.load(SCALE + 1) + s2 = tl.load(SCALE + 2) + + pre_w = tl.load( + MIX + offs[:, None] * MIX_N + j[None, :], mask=mask[:, None], other=0.0 + ) + post_w = tl.load( + MIX + offs[:, None] * MIX_N + (HC + j)[None, :], mask=mask[:, None], other=0.0 + ) + comb_w = tl.load( + MIX + offs[:, None] * MIX_N + (2 * HC + jj)[None, :], + mask=mask[:, None], + other=0.0, + ) + pre_b = tl.load(BASE + j) + post_b = tl.load(BASE + HC + j) + comb_b = tl.load(BASE + 2 * HC + jj) + + pre = tl.sigmoid(pre_w * s0 + pre_b[None, :]) + EPS + post = POSTMULT * tl.sigmoid(post_w * s1 + post_b[None, :]) + cl = tl.reshape(comb_w * s2 + comb_b[None, :], (BM, HC, HC)) + + ex = tl.exp(cl - tl.max(cl, axis=2, keep_dims=True)) + comb = ex / tl.sum(ex, axis=2, keep_dims=True) + EPS + + # Store each normalize's INPUT to scratch so backward can reverse-mode the Sinkhorn. + t = 0 + if SAVE: + tl.store( + STATES + offs[:, None] * (NSTATES * HC * HC) + t * (HC * HC) + jj[None, :], + tl.reshape(comb, (BM, HC * HC)), + mask=mask[:, None], + ) + t += 1 + comb = comb / (tl.sum(comb, axis=1, keep_dims=True) + EPS) # colnorm + for _ in range(ITERS - 1): + if SAVE: + tl.store( + STATES + + offs[:, None] * (NSTATES * HC * HC) + + t * (HC * HC) + + jj[None, :], + tl.reshape(comb, (BM, HC * HC)), + mask=mask[:, None], + ) + t += 1 + comb = comb / (tl.sum(comb, axis=2, keep_dims=True) + EPS) # rownorm + if SAVE: + tl.store( + STATES + + offs[:, None] * (NSTATES * HC * HC) + + t * (HC * HC) + + jj[None, :], + tl.reshape(comb, (BM, HC * HC)), + mask=mask[:, None], + ) + t += 1 + comb = comb / (tl.sum(comb, axis=1, keep_dims=True) + EPS) # colnorm + + tl.store(PRE + offs[:, None] * HC + j[None, :], pre, mask=mask[:, None]) + tl.store(POST + offs[:, None] * HC + j[None, :], post, mask=mask[:, None]) + tl.store( + COMB + offs[:, None] * (HC * HC) + jj[None, :], + tl.reshape(comb, (BM, HC * HC)), + mask=mask[:, None], + ) + + +@triton.autotune( + configs=[ + triton.Config({"BM": bm}, num_warps=w) for bm in (8, 16, 32, 64) for w in (2, 4) + ], + key=["M"], + reset_to_zero=["DSCALE", "DBASE"], +) +@triton.jit +def _mhc_bwd_kernel( + MIX, + SCALE, + BASE, + STATES, + DPRE, + DPOST, + DCOMB, + DMIX, + DSCALE, + DBASE, + M, + HC: tl.constexpr, + MIX_N: tl.constexpr, + ITERS: tl.constexpr, + NSTATES: tl.constexpr, + EPS: tl.constexpr, + POSTMULT: tl.constexpr, + BM: tl.constexpr, +): + pid = tl.program_id(0) + offs = pid * BM + tl.arange(0, BM) + mask = offs < M + j = tl.arange(0, HC) + jj = tl.arange(0, HC * HC) + s0 = tl.load(SCALE + 0) + s1 = tl.load(SCALE + 1) + s2 = tl.load(SCALE + 2) + + pre_w = tl.load( + MIX + offs[:, None] * MIX_N + j[None, :], mask=mask[:, None], other=0.0 + ) + post_w = tl.load( + MIX + offs[:, None] * MIX_N + (HC + j)[None, :], mask=mask[:, None], other=0.0 + ) + comb_w = tl.load( + MIX + offs[:, None] * MIX_N + (2 * HC + jj)[None, :], + mask=mask[:, None], + other=0.0, + ) + pre_b = tl.load(BASE + j) + post_b = tl.load(BASE + HC + j) + _comb_b = tl.load(BASE + 2 * HC + jj) + + pre_sig = tl.sigmoid(pre_w * s0 + pre_b[None, :]) + post_sig = tl.sigmoid(post_w * s1 + post_b[None, :]) + # sm = softmax(cl) = STATES[0] - eps (state 0 saved before colnorm) + sm = ( + tl.reshape( + tl.load( + STATES + + offs[:, None] * (NSTATES * HC * HC) + + 0 * (HC * HC) + + jj[None, :], + mask=mask[:, None], + other=0.0, + ), + (BM, HC, HC), + ) + - EPS + ) + + dy = tl.reshape( + tl.load( + DCOMB + offs[:, None] * (HC * HC) + jj[None, :], + mask=mask[:, None], + other=0.0, + ), + (BM, HC, HC), + ) + # reverse the Sinkhorn normalizes. Forward: colnorm(idx0), then (iters-1)x [rownorm, + # colnorm]; undo last-first (colnorm=axis1, rownorm=axis2). + base_st = offs[:, None] * (NSTATES * HC * HC) + jj[None, :] + t = 2 * (ITERS - 1) # last (col) state index + xc = tl.reshape( + tl.load(STATES + base_st + t * (HC * HC), mask=mask[:, None], other=0.0), + (BM, HC, HC), + ) + Sc = tl.sum(xc, axis=1, keep_dims=True) + EPS + dy = (dy - tl.sum(dy * (xc / Sc), axis=1, keep_dims=True)) / Sc + t -= 1 + for _ in range(ITERS - 1): + xr = tl.reshape( + tl.load(STATES + base_st + t * (HC * HC), mask=mask[:, None], other=0.0), + (BM, HC, HC), + ) + Sr = tl.sum(xr, axis=2, keep_dims=True) + EPS + dy = (dy - tl.sum(dy * (xr / Sr), axis=2, keep_dims=True)) / Sr + t -= 1 + xc2 = tl.reshape( + tl.load(STATES + base_st + t * (HC * HC), mask=mask[:, None], other=0.0), + (BM, HC, HC), + ) + Sc2 = tl.sum(xc2, axis=1, keep_dims=True) + EPS + dy = (dy - tl.sum(dy * (xc2 / Sc2), axis=1, keep_dims=True)) / Sc2 + t -= 1 + # dy = dA = d(sm); softmax backward over axis=2 + dcl = sm * (dy - tl.sum(dy * sm, axis=2, keep_dims=True)) + dcl_flat = tl.reshape(dcl, (BM, HC * HC)) + + dpre = tl.load( + DPRE + offs[:, None] * HC + j[None, :], mask=mask[:, None], other=0.0 + ) + dpost = tl.load( + DPOST + offs[:, None] * HC + j[None, :], mask=mask[:, None], other=0.0 + ) + dpre_z = dpre * pre_sig * (1.0 - pre_sig) + dpost_z = dpost * POSTMULT * post_sig * (1.0 - post_sig) + + d_pre_w = dpre_z * s0 + d_post_w = dpost_z * s1 + d_comb_w = dcl_flat * s2 + tl.store(DMIX + offs[:, None] * MIX_N + j[None, :], d_pre_w, mask=mask[:, None]) + tl.store( + DMIX + offs[:, None] * MIX_N + (HC + j)[None, :], d_post_w, mask=mask[:, None] + ) + tl.store( + DMIX + offs[:, None] * MIX_N + (2 * HC + jj)[None, :], + d_comb_w, + mask=mask[:, None], + ) + + m2 = mask[:, None] + tl.atomic_add(DSCALE + 0, tl.sum(tl.where(m2, dpre_z * pre_w, 0.0))) + tl.atomic_add(DSCALE + 1, tl.sum(tl.where(m2, dpost_z * post_w, 0.0))) + tl.atomic_add(DSCALE + 2, tl.sum(tl.where(m2, dcl_flat * comb_w, 0.0))) + tl.atomic_add(DBASE + j, tl.sum(tl.where(m2, dpre_z, 0.0), axis=0)) + tl.atomic_add(DBASE + HC + j, tl.sum(tl.where(m2, dpost_z, 0.0), axis=0)) + tl.atomic_add(DBASE + 2 * HC + jj, tl.sum(tl.where(m2, dcl_flat, 0.0), axis=0)) + + +class _SinkhornMix(torch.autograd.Function): + @staticmethod + def forward(ctx, mixes, scale, base, hc, iters, eps, post_mult): + M, MIX_N = mixes.shape + mixes = mixes.contiguous().float() + scale = scale.contiguous().float() + base = base.contiguous().float() + nstates = 1 + 2 * (iters - 1) + pre = torch.empty(M, hc, device=mixes.device, dtype=torch.float32) + post = torch.empty(M, hc, device=mixes.device, dtype=torch.float32) + comb = torch.empty(M, hc * hc, device=mixes.device, dtype=torch.float32) + # Allocate state history if ANY trainable input needs grad (backward reads it for the + # scale/base grads too, not just mixes). is_grad_enabled() is False in Function.forward. + save = any(ctx.needs_input_grad) + states = ( + torch.empty(M, nstates, hc * hc, device=mixes.device, dtype=torch.float32) + if save + else torch.empty(1, device=mixes.device, dtype=torch.float32) + ) + grid = lambda m: (triton.cdiv(M, m["BM"]),) + _mhc_fwd_kernel[grid]( + mixes, + scale, + base, + pre, + post, + comb, + states, + M, + HC=hc, + MIX_N=MIX_N, + ITERS=iters, + NSTATES=nstates, + EPS=eps, + POSTMULT=post_mult, + SAVE=save, + ) + ctx.save_for_backward(mixes, scale, base, states) + ctx.cfg = (hc, MIX_N, iters, nstates, eps, post_mult) + return pre, post, comb + + @staticmethod + def backward(ctx, dpre, dpost, dcomb): + mixes, scale, base, states = ctx.saved_tensors + hc, MIX_N, iters, nstates, eps, post_mult = ctx.cfg + M = mixes.shape[0] + dmix = torch.empty_like(mixes) + dscale = torch.zeros_like(scale) + dbase = torch.zeros_like(base) + grid = lambda m: (triton.cdiv(M, m["BM"]),) + _mhc_bwd_kernel[grid]( + mixes, + scale, + base, + states, + dpre.contiguous().float(), + dpost.contiguous().float(), + dcomb.contiguous().float(), + dmix, + dscale, + dbase, + M, + HC=hc, + MIX_N=MIX_N, + ITERS=iters, + NSTATES=nstates, + EPS=eps, + POSTMULT=post_mult, + ) + return dmix, dscale, dbase, None, None, None, None + + +def sinkhorn_mix(mixes, scale, base, hc, iters, eps, post_mult=2.0): + """mixes: [M, hc*(2+hc)]; scale: [3]; base: [hc*(2+hc)]. Returns (pre, post, comb) + with pre/post [M, hc], comb [M, hc*hc]. Fused sigmoid/softmax/Sinkhorn (fp32).""" + return _SinkhornMix.apply(mixes, scale, base, hc, iters, eps, post_mult) + + +# Fused RMSNorm + fn-linear. RMS scale r is per-row, so mixes = r * (streams @ fn^T): one +# pass over the 16384-wide streams covers both streams@fn^T and sum-of-squares (eager does +# ~3 passes + a 536MB fp32 flat). + +_RL_CFGS = [ + triton.Config({"BK": bk}, num_warps=w, num_stages=s) + for bk in (64, 128, 256) + for w in (4, 8) + for s in (2, 3) +] + + +@triton.autotune(configs=_RL_CFGS, key=["M", "K", "N"]) +@triton.jit +def _rmsln_fwd_kernel( + STREAMS, + FN, + MIXES, + R, + G, + M, + K, + EPS, + N: tl.constexpr, + NP: tl.constexpr, + BM: tl.constexpr, + BK: tl.constexpr, +): + pid = tl.program_id(0) + offs_m = pid * BM + tl.arange(0, BM) + mmask = offs_m < M + n = tl.arange(0, NP) + nmask = n < N + acc = tl.zeros([BM, NP], tl.float32) + ssq = tl.zeros([BM], tl.float32) + for k0 in range(0, K, BK): + kk = k0 + tl.arange(0, BK) + kmask = kk < K + x = tl.load( + STREAMS + offs_m[:, None] * K + kk[None, :], + mask=mmask[:, None] & kmask[None, :], + other=0.0, + ).to(tl.float32) + ssq += tl.sum(x * x, axis=1) + w = tl.load( + FN + n[:, None] * K + kk[None, :], + mask=nmask[:, None] & kmask[None, :], + other=0.0, + ) + acc += tl.dot(x, tl.trans(w), input_precision="ieee") + r = tl.rsqrt(ssq / K + EPS) + mixes = acc * r[:, None] + tl.store( + MIXES + offs_m[:, None] * N + n[None, :], + mixes, + mask=mmask[:, None] & nmask[None, :], + ) + tl.store(R + offs_m, r, mask=mmask) + tl.store( + G + offs_m[:, None] * N + n[None, :], acc, mask=mmask[:, None] & nmask[None, :] + ) + + +@triton.autotune(configs=_RL_CFGS, key=["M", "K", "N"], reset_to_zero=["DFN"]) +@triton.jit +def _rmsln_bwd_kernel( + STREAMS, + FN, + R, + G, + DMIX, + DSTREAMS, + DFN, + M, + K, + N: tl.constexpr, + NP: tl.constexpr, + BM: tl.constexpr, + BK: tl.constexpr, +): + pid = tl.program_id(0) + offs_m = pid * BM + tl.arange(0, BM) + mmask = offs_m < M + n = tl.arange(0, NP) + nmask = n < N + dmix = tl.load( + DMIX + offs_m[:, None] * N + n[None, :], + mask=mmask[:, None] & nmask[None, :], + other=0.0, + ) + g = tl.load( + G + offs_m[:, None] * N + n[None, :], + mask=mmask[:, None] & nmask[None, :], + other=0.0, + ) + r = tl.load(R + offs_m, mask=mmask, other=0.0) + c = tl.sum(dmix * g, axis=1) # dL/dr per row + dG = dmix * r[:, None] # [BM, NP] + coef = (r * r * r) / K * c # [BM] + for k0 in range(0, K, BK): + kk = k0 + tl.arange(0, BK) + kmask = kk < K + w = tl.load( + FN + n[:, None] * K + kk[None, :], + mask=nmask[:, None] & kmask[None, :], + other=0.0, + ) + x = tl.load( + STREAMS + offs_m[:, None] * K + kk[None, :], + mask=mmask[:, None] & kmask[None, :], + other=0.0, + ).to(tl.float32) + dmf = tl.dot(dmix, w, input_precision="ieee") # (d_mixes @ fn) [BM, BK] + dx = r[:, None] * dmf - x * coef[:, None] + tl.store( + DSTREAMS + offs_m[:, None] * K + kk[None, :], + dx, + mask=mmask[:, None] & kmask[None, :], + ) + dfn = tl.dot(tl.trans(dG), x, input_precision="ieee") # [NP, BK] + tl.atomic_add( + DFN + n[:, None] * K + kk[None, :], + dfn, + mask=nmask[:, None] & kmask[None, :], + ) + + +class _RMSNormLinear(torch.autograd.Function): + @staticmethod + def forward(ctx, streams_flat, fn, eps, BM): + M, K = streams_flat.shape + N = fn.shape[0] + NP = triton.next_power_of_2(N) + sf = streams_flat.contiguous() + fn = fn.contiguous().float() + mixes = torch.empty(M, N, device=sf.device, dtype=torch.float32) + R = torch.empty(M, device=sf.device, dtype=torch.float32) + G = torch.empty(M, N, device=sf.device, dtype=torch.float32) + grid = lambda m: (triton.cdiv(M, m["BM"]),) + _rmsln_fwd_kernel[grid](sf, fn, mixes, R, G, M, K, eps, N=N, NP=NP, BM=BM) + ctx.save_for_backward(sf, fn, R, G) + ctx.dims = (M, K, N, NP, BM) + return mixes + + @staticmethod + def backward(ctx, dmix): + sf, fn, R, G = ctx.saved_tensors + M, K, N, NP, BM = ctx.dims + dstreams = torch.empty_like(sf, dtype=torch.float32) + dfn = torch.zeros_like(fn) + grid = lambda m: (triton.cdiv(M, m["BM"]),) + _rmsln_bwd_kernel[grid]( + sf, + fn, + R, + G, + dmix.contiguous().float(), + dstreams, + dfn, + M, + K, + N=N, + NP=NP, + BM=BM, + ) + return dstreams.to(sf.dtype), dfn, None, None + + +def _rmsnorm_linear(streams_flat, fn, eps, BM=16): + return _RMSNormLinear.apply(streams_flat, fn, eps, BM) + + +# fused collapse: collapsed[m,d] = sum_h pre[m,h] * streams[m,h,d] +@triton.autotune( + configs=[ + triton.Config({"BD": bd}, num_warps=w) + for bd in (256, 512, 1024) + for w in (4, 8) + ], + key=["M", "D"], +) +@triton.jit +def _collapse_fwd_kernel(PRE, STREAMS, OUT, M, D, HC: tl.constexpr, BD: tl.constexpr): + m = tl.program_id(0) + dblk = tl.program_id(1) * BD + tl.arange(0, BD) + dmask = dblk < D + acc = tl.zeros([BD], tl.float32) + for h in range(HC): + p = tl.load(PRE + m * HC + h) + s = tl.load(STREAMS + m * (HC * D) + h * D + dblk, mask=dmask, other=0.0).to( + tl.float32 + ) + acc += p * s + tl.store(OUT + m * D + dblk, acc, mask=dmask) + + +@triton.autotune( + configs=[ + triton.Config({"BD": bd}, num_warps=w) + for bd in (256, 512, 1024) + for w in (4, 8) + ], + key=["M", "D"], + reset_to_zero=["DPRE"], +) +@triton.jit +def _collapse_bwd_kernel( + PRE, STREAMS, GOUT, DPRE, DSTREAMS, M, D, HC: tl.constexpr, BD: tl.constexpr +): + m = tl.program_id(0) + dblk = tl.program_id(1) * BD + tl.arange(0, BD) + dmask = dblk < D + g = tl.load(GOUT + m * D + dblk, mask=dmask, other=0.0).to(tl.float32) + for h in range(HC): + p = tl.load(PRE + m * HC + h) + s = tl.load(STREAMS + m * (HC * D) + h * D + dblk, mask=dmask, other=0.0).to( + tl.float32 + ) + tl.store(DSTREAMS + m * (HC * D) + h * D + dblk, p * g, mask=dmask) + tl.atomic_add(DPRE + m * HC + h, tl.sum(tl.where(dmask, g * s, 0.0))) + + +class _Collapse(torch.autograd.Function): + @staticmethod + def forward(ctx, pre, streams): + M, HC, D = streams.shape + pre = pre.contiguous() + streams = streams.contiguous() + out = torch.empty(M, D, device=streams.device, dtype=torch.float32) + grid = lambda m: (M, triton.cdiv(D, m["BD"])) + _collapse_fwd_kernel[grid](pre, streams, out, M, D, HC=HC) + ctx.save_for_backward(pre, streams) + ctx.dims = (M, HC, D) + return out + + @staticmethod + def backward(ctx, gout): + pre, streams = ctx.saved_tensors + M, HC, D = ctx.dims + dpre = torch.zeros_like(pre) + dstreams = torch.empty_like(streams, dtype=torch.float32) + grid = lambda m: (M, triton.cdiv(D, m["BD"])) + _collapse_bwd_kernel[grid]( + pre, streams, gout.contiguous().float(), dpre, dstreams, M, D, HC=HC + ) + return dpre, dstreams.to(streams.dtype) + + +def hyperconnection_forward( + hidden_streams, input_norm, fn, base, scale, hc, iters, eps, post_mult=2.0 +): + """Drop-in for DeepseekV4HyperConnection.forward. Returns (post, comb, collapsed). + Fully fused Triton: rmsnorm+fn-linear (one pass over streams), Sinkhorn mixer, and the + weighted collapse — eager does these in ~3 passes over the 16384-wide streams + a 536MB + fp32 intermediate.""" + dtype = hidden_streams.dtype + # match the fused-linear / Sinkhorn params to the stream compute dtype (they may be fp32). + fn = fn.to(dtype) if fn.dtype != dtype else fn + scale = scale.to(dtype) if scale.dtype != dtype else scale + base = base.to(dtype) if base.dtype != dtype else base + *lead, _, hidden = hidden_streams.shape + streams_flat = hidden_streams.reshape(-1, hc * hidden) + mixes = _rmsnorm_linear(streams_flat, fn, input_norm.eps) + pre, post, comb = sinkhorn_mix(mixes, scale, base, hc, iters, eps, post_mult) + collapsed = ( + _Collapse.apply(pre, hidden_streams.reshape(-1, hc, hidden)) + .reshape(*lead, hidden) + .to(dtype) + ) + post = post.reshape(*lead, hc) + comb = comb.reshape(*lead, hc, hc) + return post, comb, collapsed diff --git a/src/axolotl/integrations/kernels/libs/dsv4/patch.py b/src/axolotl/integrations/kernels/libs/dsv4/patch.py new file mode 100644 index 0000000000..c310c6a044 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/dsv4/patch.py @@ -0,0 +1,348 @@ +"""Monkeypatch DeepSeek-V4 to use the fused Triton training kernels. + +V4 ships eager-only attention (head_dim 512 > FlashAttention's cap, plus a per-head sink +and an in-block compressor KV-concat). This registers a custom attention backend that +routes sliding-attention layers to ``sliding_attn`` and CSA/HCA layers to +``csa_attn`` (splitting the eager-combined ``[sliding | compressed]`` KV and +``[sliding_mask | block_bias]`` mask back apart), and swaps the interleaved partial-RoPE +and the mHC HyperConnection for their fused kernels. o_a_proj stays on cuBLAS (a fused +grouped GEMM doesn't beat it). + +Kernels assume ``attention_dropout == 0`` (the V4 default). +""" + +import torch + +from axolotl.utils.logging import get_logger + +from .attention import sliding_attn +from .attention_csa import csa_attn +from .gated_pool import gated_softmax_pool +from .indexer import indexer_scores +from .mhc import hyperconnection_forward +from .rope import apply_rotary_pos_emb_triton + +LOG = get_logger(__name__) + +_MOD = None # the transformers deepseek_v4 module; set by patch_deepseek_v4_kernels + + +def _dsv4_attention( + module, + query, + key, + value, + attention_mask, + scaling=None, + dropout=0.0, + sliding_window=None, + s_aux=None, + **kwargs, +): + """Drop-in for ``eager_attention_forward``. ``query`` [B,H,S,D]; ``key``/``value`` + [B,1,KV,D] (shared MQA); ``s_aux`` = per-head sinks; ``attention_mask`` is the + eager-built [sliding | block_bias] mask. Returns (attn_output [B,S,H,D], None). + + Patched onto the module's ``eager_attention_forward`` (V4's default backend) so the + mask — and thus the compressor's ``block_bias`` — is still built and handed to us.""" + B, H, S, D = query.shape + KV = key.shape[2] + if scaling is None: + scaling = D**-0.5 + window = sliding_window if sliding_window is not None else module.sliding_window + + if KV == S: # sliding-attention layer (no compressor entries) + out = sliding_attn(query, key, value, s_aux, scaling, window) + else: # CSA / HCA: keys are [sliding (S) | compressed (KV-S)], mask carries block_bias + kv_slide = key[:, :, :S] + kv_comp = key[:, :, S:] + block_bias = attention_mask[..., S:] + out = csa_attn(query, kv_slide, kv_comp, block_bias, s_aux, scaling, window) + return out.transpose(1, 2).contiguous(), None + + +def _gated_pool_norm(new_kv, new_gate, kv_norm): + """Fused replacement for ``kv_norm((new_kv * new_gate.softmax(2,fp32)).sum(2))`` — + the compressor's per-window gated-softmax pool. Returns the compressor dtype (the + pool is fp32 internally; we cast back so the downstream RoPE/concat stay bf16).""" + return kv_norm(gated_softmax_pool(new_kv, new_gate).to(new_kv.dtype)) + + +def _indexer_scorer_forward(self, q, compressed_kv, hidden_states): + """Fused DeepseekV4IndexerScorer: never materializes the [B,S,H,T] fp32 score + tensor (H=64 index heads). Output feeds only topk().indices, so no grad needed.""" + weights = self.weights_proj(hidden_states).float() * self.weights_scaling # [B,S,H] + return indexer_scores(q, compressed_kv, weights, self.softmax_scale) + + +def _hca_compressor_forward( + self, hidden_states, q_residual, position_ids, past_key_values, layer_idx +): + mod = _MOD + batch, _, _ = hidden_states.shape + cache_layer = ( + past_key_values.layers[layer_idx] if past_key_values is not None else None + ) + kv = self.kv_proj(hidden_states) + gate = self.gate_proj(hidden_states) + if cache_layer is None: + usable = (kv.shape[1] // self.compress_rate) * self.compress_rate + chunk_kv, chunk_gate, first_window_position = ( + kv[:, :usable], + gate[:, :usable], + 0, + ) + else: + chunk_kv, chunk_gate, first_window_position = ( + cache_layer.store_compression_weights("compressor", kv, gate) + ) + + if chunk_kv.shape[1] > 0: + n_windows = chunk_kv.shape[1] // self.compress_rate + chunk_kv = chunk_kv.view(batch, n_windows, self.compress_rate, -1) + chunk_gate = ( + chunk_gate.view(batch, n_windows, self.compress_rate, -1) + + self.position_bias + ) + compressed = _gated_pool_norm(chunk_kv, chunk_gate, self.kv_norm) + positions = torch.arange(n_windows, device=compressed.device) + positions = ( + (positions * self.compress_rate + first_window_position) + .unsqueeze(0) + .expand(batch, -1) + ) + cos, sin = self.rotary_emb( + compressed, position_ids=positions, layer_type=self.rope_layer_type + ) + compressed = mod.apply_rotary_pos_emb( + compressed.unsqueeze(1), cos, sin + ).squeeze(1) + else: + compressed = chunk_kv.new_zeros((batch, 0, self.head_dim)) + + if cache_layer is not None: + compressed = cache_layer.update_compressor_states("compressor", compressed) + compressed_kv = compressed.unsqueeze(1) + + compressed_len = compressed_kv.shape[2] + seq_len = position_ids.shape[1] + if seq_len == 1 or compressed_len == 0: + return compressed_kv, None + + entry_indices = torch.arange(compressed_len, device=compressed_kv.device) + causal_threshold = (position_ids + 1) // self.compress_rate + block_bias = compressed_kv.new_zeros((batch, 1, seq_len, compressed_len)) + block_bias = block_bias.masked_fill( + entry_indices.view(1, 1, 1, -1) >= causal_threshold.unsqueeze(1).unsqueeze(-1), + float("-inf"), + ) + return compressed_kv, block_bias + + +def _indexer_forward( + self, hidden_states, q_residual, position_ids, past_key_values, layer_idx +): + mod = _MOD + batch, seq_len, _ = hidden_states.shape + cache_layer = ( + past_key_values.layers[layer_idx] if past_key_values is not None else None + ) + kv = self.kv_proj(hidden_states) + gate = self.gate_proj(hidden_states) + + if cache_layer is None: + usable = (kv.shape[1] // self.compress_rate) * self.compress_rate + chunk_kv, chunk_gate, first_window_position = ( + kv[:, :usable], + gate[:, :usable], + 0, + ) + else: + chunk_kv, chunk_gate, first_window_position = ( + cache_layer.store_compression_weights("indexer", kv, gate) + ) + + if chunk_kv.shape[1] > 0: + n_windows = chunk_kv.shape[1] // self.compress_rate + ratio = self.compress_rate + chunk_kv = chunk_kv.view(batch, n_windows, ratio, -1) + chunk_gate = chunk_gate.view(batch, n_windows, ratio, -1) + self.position_bias + + new_kv = chunk_kv.new_zeros((batch, n_windows, 2 * ratio, self.head_dim)) + new_gate = chunk_gate.new_full( + (batch, n_windows, 2 * ratio, self.head_dim), float("-inf") + ) + new_kv[:, :, ratio:] = chunk_kv[..., self.head_dim :] + new_gate[:, :, ratio:] = chunk_gate[..., self.head_dim :] + if n_windows > 1: + new_kv[:, 1:, :ratio] = chunk_kv[:, :-1, :, : self.head_dim] + new_gate[:, 1:, :ratio] = chunk_gate[:, :-1, :, : self.head_dim] + if cache_layer is not None: + prior_kv, prior_gate = cache_layer.update_overlap_state( + "indexer", chunk_kv, chunk_gate, self.head_dim + ) + if prior_kv is not None: + new_kv[:, 0, :ratio] = prior_kv.to(new_kv.dtype) + new_gate[:, 0, :ratio] = prior_gate.to(new_gate.dtype) + + compressed = _gated_pool_norm(new_kv, new_gate, self.kv_norm) + positions = torch.arange(n_windows, device=compressed.device) + positions = positions * self.compress_rate + first_window_position + positions = positions.unsqueeze(0).expand(batch, -1) + cos, sin = self.rotary_emb( + compressed, position_ids=positions, layer_type=self.rope_layer_type + ) + compressed = mod.apply_rotary_pos_emb( + compressed.unsqueeze(1), cos, sin + ).squeeze(1) + else: + compressed = chunk_kv.new_zeros((batch, 0, self.head_dim)) + + compressed_kv = ( + compressed + if cache_layer is None + else cache_layer.update_compressor_states("indexer", compressed) + ) + + cos_q, sin_q = self.rotary_emb( + hidden_states, position_ids=position_ids, layer_type=self.rope_layer_type + ) + q = ( + self.q_b_proj(q_residual) + .view(batch, seq_len, -1, self.head_dim) + .transpose(1, 2) + ) + q = mod.apply_rotary_pos_emb(q, cos_q, sin_q).transpose(1, 2) + + index_scores = self.scorer(q, compressed_kv, hidden_states) + compressed_len = compressed_kv.shape[1] + top_k = min(self.index_topk, compressed_len) + + if compressed_len > 0: + causal_threshold = (position_ids + 1) // self.compress_rate + entry_indices = torch.arange(compressed_len, device=index_scores.device) + future_mask = entry_indices.view(1, 1, -1) >= causal_threshold.unsqueeze(-1) + index_scores = index_scores.masked_fill(future_mask, float("-inf")) + top_k_indices = index_scores.topk(top_k, dim=-1).indices + invalid = top_k_indices >= causal_threshold.unsqueeze(-1) + return torch.where(invalid, torch.full_like(top_k_indices, -1), top_k_indices) + + return index_scores.topk(top_k, dim=-1).indices + + +def _csa_compressor_forward( + self, hidden_states, q_residual, position_ids, past_key_values, layer_idx +): + mod = _MOD + batch, seq_len, _ = hidden_states.shape + cache_layer = ( + past_key_values.layers[layer_idx] if past_key_values is not None else None + ) + kv = self.kv_proj(hidden_states) + gate = self.gate_proj(hidden_states) + + if cache_layer is None: + usable = (kv.shape[1] // self.compress_rate) * self.compress_rate + chunk_kv, chunk_gate, first_window_position = ( + kv[:, :usable], + gate[:, :usable], + 0, + ) + else: + chunk_kv, chunk_gate, first_window_position = ( + cache_layer.store_compression_weights("compressor", kv, gate) + ) + + if chunk_kv.shape[1] > 0: + n_windows = chunk_kv.shape[1] // self.compress_rate + ratio = self.compress_rate + chunk_kv = chunk_kv.view(batch, n_windows, ratio, -1) + chunk_gate = chunk_gate.view(batch, n_windows, ratio, -1) + self.position_bias + + new_kv = chunk_kv.new_zeros((batch, n_windows, 2 * ratio, self.head_dim)) + new_gate = chunk_gate.new_full( + (batch, n_windows, 2 * ratio, self.head_dim), float("-inf") + ) + new_kv[:, :, ratio:] = chunk_kv[..., self.head_dim :] + new_gate[:, :, ratio:] = chunk_gate[..., self.head_dim :] + if n_windows > 1: + new_kv[:, 1:, :ratio] = chunk_kv[:, :-1, :, : self.head_dim] + new_gate[:, 1:, :ratio] = chunk_gate[:, :-1, :, : self.head_dim] + if cache_layer is not None: + prior_kv, prior_gate = cache_layer.update_overlap_state( + "compressor", chunk_kv, chunk_gate, self.head_dim + ) + if prior_kv is not None: + new_kv[:, 0, :ratio] = prior_kv.to(new_kv.dtype) + new_gate[:, 0, :ratio] = prior_gate.to(new_gate.dtype) + + compressed = _gated_pool_norm(new_kv, new_gate, self.kv_norm) + positions = torch.arange(n_windows, device=compressed.device) + positions = positions * self.compress_rate + first_window_position + positions = positions.unsqueeze(0).expand(batch, -1) + cos, sin = self.rotary_emb( + compressed, position_ids=positions, layer_type=self.rope_layer_type + ) + compressed = mod.apply_rotary_pos_emb( + compressed.unsqueeze(1), cos, sin + ).squeeze(1) + else: + compressed = chunk_kv.new_zeros((batch, 0, self.head_dim)) + + if cache_layer is not None: + compressed = cache_layer.update_compressor_states("compressor", compressed) + compressed_kv = compressed.unsqueeze(1) + + top_k_indices = self.indexer( + hidden_states, q_residual, position_ids, past_key_values, layer_idx + ) + compressed_len = compressed_kv.shape[2] + valid = top_k_indices >= 0 + safe_indices = torch.where( + valid, top_k_indices, torch.full_like(top_k_indices, compressed_len) + ) + block_bias = compressed_kv.new_full( + (batch, 1, seq_len, compressed_len + 1), float("-inf") + ) + block_bias.scatter_(-1, safe_indices.unsqueeze(1), 0.0) + return compressed_kv, block_bias[..., :compressed_len] + + +def _hyperconnection_forward(self, hidden_streams): + return hyperconnection_forward( + hidden_streams, + self.input_norm, + self.fn, + self.base, + self.scale, + self.hc_mult, + self.hc_sinkhorn_iters, + self.hc_eps, + post_mult=2.0, + ) + + +def patch_deepseek_v4_kernels(): + """Patch the eager attention backend + RoPE + HyperConnection. Idempotent. + + V4 stays on ``_attn_implementation='eager'`` (head_dim 512 bans the real flash + backends), so transformers still builds the mask and cats the compressor's + ``block_bias`` — we just replace the eager kernel itself.""" + from transformers.models.deepseek_v4 import modeling_deepseek_v4 as mod + + global _MOD + _MOD = mod + + def _rope(x, cos, sin, unsqueeze_dim=1): + return apply_rotary_pos_emb_triton(x, cos, sin, unsqueeze_dim) + + mod.eager_attention_forward = _dsv4_attention + mod.apply_rotary_pos_emb = _rope + mod.DeepseekV4HyperConnection.forward = _hyperconnection_forward + mod.DeepseekV4IndexerScorer.forward = _indexer_scorer_forward + mod.DeepseekV4HCACompressor.forward = _hca_compressor_forward + mod.DeepseekV4Indexer.forward = _indexer_forward + mod.DeepseekV4CSACompressor.forward = _csa_compressor_forward + LOG.info( + "Patched DeepSeek-V4 with fused Triton kernels (attention/rope/mHC/compressor/indexer)" + ) diff --git a/src/axolotl/integrations/kernels/libs/dsv4/rope.py b/src/axolotl/integrations/kernels/libs/dsv4/rope.py new file mode 100644 index 0000000000..08a24eb1b7 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/dsv4/rope.py @@ -0,0 +1,121 @@ +"""Fused interleaved partial-RoPE kernel for DeepSeek-V4 (fwd + bwd). + +Reference: ``transformers.models.deepseek_v4.modeling_deepseek_v4.apply_rotary_pos_emb``. + +V4 uses *interleaved* RoPE (GPT-J style: consecutive channel pairs) on the trailing +``rope_dim`` channels of each head, leaving the leading ``nope`` channels untouched. +``cos``/``sin`` arrive half-width (one entry per pair, shape ``[B, S, rope_dim//2]``) +and are expanded by ``repeat_interleave(2)`` in the reference. Per pair ``i``: + + out[2i] = x[2i]*cos[i] - x[2i+1]*sin[i] + out[2i+1] = x[2i+1]*cos[i] + x[2i]*sin[i] + +The rotation is orthogonal, so the backward is the same map with ``sin -> -sin``. +The model also calls the reference with a pre-negated ``sin`` (output inverse-rope); +that composes naturally — this Function just rotates by whatever ``(cos, sin)`` it gets +and negates ``sin`` for its own backward. +""" + +import torch +import triton +import triton.language as tl + + +@triton.jit +def _rope_kernel( + x_ptr, + cos_ptr, + sin_ptr, + out_ptr, + S, + H, + cs_b, # cos/sin strides: may be non-contiguous (the rotary's transpose view) + cs_s, + cs_i, + D: tl.constexpr, + ROPE: tl.constexpr, + Rh: tl.constexpr, + BLOCK_NOPE: tl.constexpr, +): + row = tl.program_id(0) # one program per (b, h, s) row of length D + s = row % S + b = (row // S) // H + nope = D - ROPE + cs_off = b * cs_b + s * cs_s + + x_row = x_ptr + row * D + out_row = out_ptr + row * D + + # passthrough leading nope channels + off = tl.arange(0, BLOCK_NOPE) + m = off < nope + tl.store(out_row + off, tl.load(x_row + off, mask=m, other=0.0), mask=m) + + i = tl.arange(0, Rh) + even = nope + 2 * i + odd = even + 1 + e = tl.load(x_row + even).to(tl.float32) + o = tl.load(x_row + odd).to(tl.float32) + c = tl.load(cos_ptr + cs_off + i * cs_i).to(tl.float32) + sn = tl.load(sin_ptr + cs_off + i * cs_i).to(tl.float32) + tl.store(out_row + even, e * c - o * sn) + tl.store(out_row + odd, o * c + e * sn) + + +def _launch(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor: + """x: [B, H, S, D] contiguous; cos/sin: [B, S, rope_dim//2] (any strides). Returns + [B, H, S, D]. cos/sin come from the rotary as a ``transpose(1,2)`` view, so they are + typically *not* contiguous — we pass their strides instead of assuming a layout.""" + B, H, S, D = x.shape + Rh = cos.shape[-1] + ROPE = 2 * Rh + if sin.stride() != cos.stride(): + sin = sin.contiguous() + cos = cos.contiguous() + out = torch.empty_like(x) + M = B * H * S + _rope_kernel[(M,)]( + x, + cos, + sin, + out, + S, + H, + cos.stride(0), + cos.stride(1), + cos.stride(2), + D=D, + ROPE=ROPE, + Rh=Rh, + BLOCK_NOPE=triton.next_power_of_2(D - ROPE), + num_warps=4, + ) + return out + + +class _RoPE(torch.autograd.Function): + @staticmethod + def forward(ctx, x, cos, sin): + x = x.contiguous() + ctx.save_for_backward(cos, sin) + return _launch(x, cos, sin) + + @staticmethod + def backward(ctx, grad_out): + cos, sin = ctx.saved_tensors + grad_x = _launch(grad_out.contiguous(), cos, -sin) + return grad_x, None, None + + +def apply_rotary_pos_emb_triton( + x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor, unsqueeze_dim: int = 1 +) -> torch.Tensor: + """Drop-in for ``apply_rotary_pos_emb``. ``x``: [B, H, S, D]; ``cos``/``sin``: + [B, S, rope_dim//2]. ``unsqueeze_dim`` is accepted for signature parity (the + reference broadcasts cos/sin over the head axis; this kernel indexes it directly).""" + # rotary cos/sin are typically fp32; match them to x's compute dtype. + if cos.dtype != x.dtype: + cos = cos.to(x.dtype) + if sin.dtype != x.dtype: + sin = sin.to(x.dtype) + return _RoPE.apply(x, cos, sin) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/chunked_bnb.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/chunked_bnb.py new file mode 100644 index 0000000000..e5d2d88df5 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/chunked_bnb.py @@ -0,0 +1,158 @@ +"""Chunked-dequant grouped MoE for bnb-4bit experts (portable, any GPU). + +scattermoe's fused grouped path reads NVFP4/MXFP4 experts at 4-bit, but bnb-nf4 experts have no +4-bit-read kernel, so the naive path dequantizes ALL E experts to bf16 every forward (~48 GB for +gemma4-26B → ~2.7x the VRAM of the nvfp4 path; at 4k seq ~all experts are active so "selective" +dequant doesn't help). This processes experts in CHUNKS: per chunk, dequant only that chunk's +experts to bf16, run the grouped gate_up (+LoRA) → gated activation → grouped down (+LoRA) for that +chunk's tokens under activation checkpointing (re-dequant in backward), then free. Peak bf16 +transient is bounded to ``chunk_size / E`` of the full expert set, bringing bnb-MoE memory in line +with the nvfp4 path. Portable: ``torch._grouped_mm`` + bnb dequant, no custom CUDA. + +Only the bnb path routes here (gated on ``module.parametrizations`` in the experts forward); the +NVFP4/MXFP4/bf16 paths are untouched. +""" + +from __future__ import annotations + +import torch +import torch.nn.functional as F +from torch.utils.checkpoint import checkpoint + +from .runtime import DEFAULT_CHUNK, RUNTIME # noqa: E402 +from .selective_dequant import selective_expert_weights + + +def set_chunk_size_override(n) -> None: + RUNTIME.dequant_chunk_size = int(n) if n else None + + +def set_layer_gc_active(flag) -> None: + RUNTIME.layer_gc_active = bool(flag) + + +def set_bnb_fast(flag) -> None: + # None (config unset) keeps the fast default; only an explicit False forces chunked. + RUNTIME.bnb_fast = True if flag is None else bool(flag) + + +def bnb_fast_enabled() -> bool: + return RUNTIME.bnb_fast + + +def _plan_chunking(num_experts): + """(chunk_size, use_checkpoint) from the balanced default or the cfg.moe_dequant_chunk_size + override. Lower it for large-expert MoEs on small GPUs; raise it for max throughput. Per-chunk + checkpointing is used ONLY when layer GC is off (with layer GC on, the chunk loop already bounds + the transient and an extra checkpoint would just nest a redundant recompute).""" + override = RUNTIME.dequant_chunk_size + chunk = override if override is not None else DEFAULT_CHUNK + chunk = max(1, min(num_experts, chunk)) + use_ckpt = (chunk < num_experts) and not RUNTIME.layer_gc_active + return chunk, use_ckpt + + +def _gated_act(gate, up, act_type, limit): + if limit is not None: # DSV4-style clamped SwiGLU + gate = gate.clamp(max=limit) + up = up.clamp(min=-limit, max=limit) + if act_type == "gelu_tanh": # Gemma4 GeGLU + return F.gelu(gate, approximate="tanh") * up + return F.silu(gate) * up + + +def _chunk_forward( + x, experts, chunk_idx, coff, gA, gB, gs, dA, dB, ds, act_type, limit +): + """One expert-chunk end-to-end: dequant chunk → grouped gate_up (+LoRA) → act → grouped down + (+LoRA). Run under ``checkpoint`` so the chunk's bf16 weights live only during fwd/recompute.""" + gub = selective_expert_weights(experts, "gate_up_proj", chunk_idx).transpose(1, 2) + dnb = selective_expert_weights(experts, "down_proj", chunk_idx).transpose(1, 2) + + gu = torch._grouped_mm(x, gub, offs=coff) + if gA is not None: + xa = torch._grouped_mm(x, gA.transpose(1, 2), offs=coff) + gu = gu + gs * torch._grouped_mm(xa, gB.transpose(1, 2), offs=coff) + + gate, up = gu.chunk(2, dim=-1) + h = _gated_act(gate, up, act_type, limit).contiguous() + + dn = torch._grouped_mm(h, dnb, offs=coff) + if dA is not None: + ha = torch._grouped_mm(h, dA.transpose(1, 2), offs=coff) + dn = dn + ds * torch._grouped_mm(ha, dB.transpose(1, 2), offs=coff) + return dn + + +def chunked_bnb_moe( + hidden, + idx, + wts, + experts, + gup_lora, + down_lora, + num_experts, + act_type="silu", + limit=None, +): + """Grouped MoE over bnb-4bit experts via chunked dequant. ``hidden`` [N,H]; ``idx``/``wts`` + [N,topk]; ``experts`` is the bnb-quantized experts module; ``*_lora`` are scattermoe-layout + (A[r*E,K], B[out,r*E], scaling) or None. Returns [N,H], differentiable to hidden + LoRA A/B.""" + from .grouped_train import _lora_stack + + N, H = hidden.shape + dev = hidden.device + chunk_size, use_ckpt = _plan_chunking(num_experts) + + Agu = Bgu = sgu = Adn = Bdn = sdn = None + if gup_lora is not None and down_lora is not None: + two_i = gup_lora[1].shape[0] # gate_up out = 2I (B is [2I, r*E]) + inter = down_lora[0].shape[1] # down in = I (A is [r*E, I]) + Agu, Bgu, sgu = _lora_stack(gup_lora, num_experts, H, two_i) + Adn, Bdn, sdn = _lora_stack(down_lora, num_experts, inter, H) + + # Sort tokens by expert so each expert's tokens are contiguous for grouped_mm. + flat_exp = idx.reshape(-1) + order = flat_exp.argsort() + rep = torch.arange(N, device=dev).repeat_interleave(idx.size(1))[order] + wflat = wts.reshape(-1)[order] + counts = torch.bincount(flat_exp, minlength=num_experts) + tok_off = torch.cat([counts.new_zeros(1), counts.cumsum(0)]).tolist() + flat = hidden[rep] + + out = hidden.new_zeros(N, H) + pieces = [] + for c0 in range(0, num_experts, chunk_size): + c1 = min(c0 + chunk_size, num_experts) + t0, t1 = int(tok_off[c0]), int(tok_off[c1]) + if t1 == t0: # no tokens routed here + continue + chunk_idx = torch.arange(c0, c1, device=dev) + coff = counts[c0:c1].cumsum(0).to(torch.int32) + gA = Agu[c0:c1] if Agu is not None else None + gB = Bgu[c0:c1] if Bgu is not None else None + dA = Adn[c0:c1] if Adn is not None else None + dB = Bdn[c0:c1] if Bdn is not None else None + args = ( + flat[t0:t1], + experts, + chunk_idx, + coff, + gA, + gB, + sgu, + dA, + dB, + sdn, + act_type, + limit, + ) + o = ( + checkpoint(_chunk_forward, *args, use_reentrant=False) + if use_ckpt + else _chunk_forward(*args) + ) + pieces.append(o * wflat[t0:t1, None].to(o.dtype)) + if not pieces: + return out + return out.index_add(0, rep, torch.cat(pieces, 0)) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/__init__.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/__init__.py new file mode 100644 index 0000000000..9636a605a2 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/__init__.py @@ -0,0 +1,31 @@ +"""sm120 CUTLASS NVFP4 grouped-GEMM MoE path (CuTe DSL). + +Optional: requires an sm120 (consumer Blackwell) GPU and nvidia-cutlass-dsl>=4.6 (the sm120 +block-scaled SF helpers, partition_fragment_SFA etc., landed in the 4.6 dev line). On any other +environment `cutlass_fp4_available()` returns False and the dsv4 MoE forward falls back to the +DeepGEMM (sm90/sm100) or chunked-dequant path. Lazy imports keep non-Blackwell / no-cutlass-dsl +environments (incl. CI) clean. +""" + +from __future__ import annotations + +import functools + + +@functools.lru_cache(maxsize=1) +def cutlass_fp4_available() -> bool: + """True iff the sm120 CUTLASS NVFP4 grouped path can run here.""" + try: + import torch + + if not torch.cuda.is_available(): + return False + if torch.cuda.get_device_capability()[0] != 12: # sm120 consumer Blackwell + return False + import cutlass.cute.nvgpu.warp.mma as _wm # noqa: F401 + import cutlass.utils.blackwell_helpers as _bh + + # sm120 block-scaled SF helpers landed in 4.6; absent in 4.5.x + return hasattr(_bh, "partition_fragment_SFA") and hasattr(_wm, "MmaMXF4NVF4Op") + except Exception: + return False diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/blockscaled_gemm_dispatch.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/blockscaled_gemm_dispatch.py new file mode 100644 index 0000000000..0ec5bea524 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/blockscaled_gemm_dispatch.py @@ -0,0 +1,271 @@ +# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""SM120 block-scaled GEMM dispatch helpers shared by the cooperative and +pingpong examples in this directory. Both examples are otherwise standalone; +they only depend on this file (and the rest of the cutlass DSL library) and +not on each other. + +Dispatch table: + Same-dtype paths: + (FP4, FP4, *, Float8E4M3FN, 16) -> MmaMXF4NVF4Op, use_mxf8f6f4=False, mma_K=64 + (FP4, FP4, *, Float8E8M0FNU, 32) -> MmaMXF4Op, use_mxf8f6f4=False, mma_K=64 + (FP8, FP8, *, Float8E8M0FNU, 32) -> MmaMXF8Op, use_mxf8f6f4=True, mma_K=32 + (FP8 same-dtype is restricted to a_dtype == b_dtype.) + Mixed-precision: + (FP4, FP8, *, Float8E8M0FNU, 32) -> MmaMXF8F6F4Op, use_mxf8f6f4=True, mma_K=32 + (FP8, FP4, *, Float8E8M0FNU, 32) -> MmaMXF8F6F4Op, use_mxf8f6f4=True, mma_K=32 + (Same-width mixed-FP8 (E4M3 + E5M2) and FP6 mixed pairs are not supported.) + +`use_mxf8f6f4` is the third argument to +`cutlass.utils.blackwell_helpers.get_permutation_mnk` and selects perm_k = 32 +(FP8 / mixed warp-MMA shape (16,8,32)) instead of perm_k = 64 (FP4 shape (16,8,64)). +""" + +import cutlass +import cutlass.cute as cute +from cutlass.cute.nvgpu.warp.mma import MXF8F6F4_SUPPORTED_PAIRS + +# ldsm.b4x16_p64 puts the FP4 nibble in the low half of the byte; mma.sync.kind::mxf8f6f4 +# reads it from the middle, so shift left by 2 (cute::fp4_shift_A/B in mma_traits_sm120.hpp). +FP4_SHIFT_BITS = 2 + +_FP8_DTYPES = (cutlass.Float8E4M3FN, cutlass.Float8E5M2) + + +def make_ldmatrix_atom(operand_dtype, transpose, num_matrices=4, mixed_mode=False): + """Build a warp-level ldmatrix copy atom for the SM120 block-scaled GEMM + A/B SMEM->RMEM path. + """ + if mixed_mode and operand_dtype.width == 4: + assert not transpose, "LdMatrix8x16x8bOp does not support transpose" + return cute.make_copy_atom( + cute.nvgpu.warp.LdMatrix8x16x8bOp( + transpose=False, + num_matrices=num_matrices, + unpack_bits=4, + ), + cutlass.Int8, + ) + atom_element_type = operand_dtype + if mixed_mode and operand_dtype.width < 8: + atom_element_type = cutlass.Int8 + return cute.make_copy_atom( + cute.nvgpu.warp.LdMatrix8x8x16bOp( + transpose=transpose, + num_matrices=num_matrices, + ), + atom_element_type, + ) + + +def make_sm120_blockscaled_mma_op(a_dtype, b_dtype, acc_dtype, sf_dtype, sf_vec_size): + """Dispatch the SM120 block-scaled MMA op for the given operand / scale + factor combination. + + Returns + ------- + Tuple[MmaOp, bool] + (mma_op, use_mxf8f6f4) where use_mxf8f6f4 is True for FP8 / mixed + paths (atom shape (16,8,32)) and False for FP4 same-dtype paths + (atom shape (16,8,64)). `use_mxf8f6f4` is consumed by + `cutlass.utils.blackwell_helpers.get_permutation_mnk`. + + Raises + ------ + ValueError + On any unsupported combination, with a diagnostic that names the + offending combination and the supported alternatives. + """ + if a_dtype == b_dtype: + if a_dtype == cutlass.Float4E2M1FN: + if sf_vec_size == 16: + if sf_dtype != cutlass.Float8E4M3FN: + raise ValueError( + f"Float4E2M1FN + sf_vec_size=16 requires sf_dtype=Float8E4M3FN, " + f"got sf_dtype={sf_dtype}" + ) + return ( + cute.nvgpu.warp.MmaMXF4NVF4Op(a_dtype, acc_dtype, sf_dtype), + False, + ) + if sf_vec_size == 32: + if sf_dtype != cutlass.Float8E8M0FNU: + raise ValueError( + f"Float4E2M1FN + sf_vec_size=32 requires sf_dtype=Float8E8M0FNU, " + f"got sf_dtype={sf_dtype}" + ) + return ( + cute.nvgpu.warp.MmaMXF4Op(a_dtype, acc_dtype, sf_dtype), + False, + ) + raise ValueError( + f"Float4E2M1FN requires sf_vec_size in (16, 32), got {sf_vec_size}" + ) + if a_dtype in _FP8_DTYPES: + if sf_vec_size != 32: + raise ValueError( + f"FP8 ab_dtype ({a_dtype}) requires sf_vec_size=32, " + f"got {sf_vec_size}" + ) + if sf_dtype != cutlass.Float8E8M0FNU: + raise ValueError( + f"FP8 ab_dtype + sf_vec_size=32 requires sf_dtype=Float8E8M0FNU, " + f"got sf_dtype={sf_dtype}" + ) + return ( + cute.nvgpu.warp.MmaMXF8Op(a_dtype, acc_dtype, sf_dtype), + True, + ) + raise ValueError( + f"Unsupported same-dtype ab_dtype={a_dtype} for SM120 block-scaled GEMM. " + f"Supported same-dtype: Float4E2M1FN, Float8E4M3FN, Float8E5M2." + ) + is_a_fp4_b_fp8 = a_dtype == cutlass.Float4E2M1FN and b_dtype in _FP8_DTYPES + is_a_fp8_b_fp4 = a_dtype in _FP8_DTYPES and b_dtype == cutlass.Float4E2M1FN + if is_a_fp4_b_fp8 or is_a_fp8_b_fp4: + if sf_vec_size != 32: + raise ValueError( + f"FP4 x FP8 mixed-precision requires sf_vec_size=32, got {sf_vec_size}" + ) + if sf_dtype != cutlass.Float8E8M0FNU: + raise ValueError( + f"FP4 x FP8 mixed-precision requires sf_dtype=Float8E8M0FNU, " + f"got sf_dtype={sf_dtype}" + ) + return ( + cute.nvgpu.warp.MmaMXF8F6F4Op(a_dtype, b_dtype, acc_dtype, sf_dtype), + True, + ) + if a_dtype in _FP8_DTYPES and b_dtype in _FP8_DTYPES: + raise ValueError( + f"same-width mixed-FP8 (a_dtype={a_dtype}, b_dtype={b_dtype}) is not supported. " + f"Supported FP4 x FP8 mixed pairs: (Float4E2M1FN, Float8E4M3FN), " + f"(Float4E2M1FN, Float8E5M2), (Float8E4M3FN, Float4E2M1FN), " + f"(Float8E5M2, Float4E2M1FN). Same-dtype FP8 uses MmaMXF8Op." + ) + raise ValueError( + f"Unsupported (a_dtype={a_dtype}, b_dtype={b_dtype}, sf_dtype={sf_dtype}, " + f"sf_vec_size={sf_vec_size}) for SM120 block-scaled GEMM. FP6 mixed pairs " + f"are not supported; supported mixed pairs are FP4 x FP8 only." + ) + + +def validate_blockscaled_args(args, fp4_allowed_tiles, fp8_allowed_tiles): + """Post-argparse validation of (a_dtype, b_dtype, sf_dtype, sf_vec_size, tile_shape_mnk). + + Same-dtype paths use the FP4 / FP8 same-dtype atoms. Mixed-precision FP4 x FP8 + is permitted only for the four supported pairs. Same-width mixed-FP8 and FP6 + are explicitly rejected with named diagnostics. + + Tile-K constraints come from the BlockScaled SF SMEM layout + (`sm120_make_smem_layout_sfa`), which requires + ``tile_K >= sf_vec_size * blk_sf == sf_vec_size * 4``: + * sf_vec_size=16 (NVFP4): tile_K must be a multiple of 64 + * sf_vec_size=32 (MXFP4 / MXFP8 / mixed): tile_K must be a multiple of 128 + A K=64 SF block cannot be filled at sf_vec_size=32 (only 2 SFs along K + fit in the K=128-required basic chunk), so tile_K=64 is rejected for + sf_vec_size=32 even though the FP4 same-dtype path otherwise allows it. + """ + tile = tuple(args.tile_shape_mnk) + a_dtype = args.a_dtype + b_dtype = args.b_dtype + if args.sf_vec_size not in (16, 32): + raise ValueError(f"--sf_vec_size must be 16 or 32, got {args.sf_vec_size}") + if a_dtype != b_dtype: + if (a_dtype, b_dtype) not in MXF8F6F4_SUPPORTED_PAIRS: + if a_dtype in _FP8_DTYPES and b_dtype in _FP8_DTYPES: + raise ValueError( + f"same-width mixed-FP8 (--a_dtype {a_dtype} --b_dtype {b_dtype}) " + f"is not supported. Supported mixed pairs: FP4 x FP8 only." + ) + raise ValueError( + f"unsupported mixed (--a_dtype {a_dtype} --b_dtype {b_dtype}). " + f"Supported mixed pairs are FP4 x FP8 only: " + f"{sorted(repr(p) for p in MXF8F6F4_SUPPORTED_PAIRS)}. " + f"FP6 mixed pairs are not supported." + ) + if args.sf_vec_size != 32: + raise ValueError( + f"FP4 x FP8 mixed-precision requires --sf_vec_size 32, " + f"got {args.sf_vec_size}" + ) + if args.sf_dtype != cutlass.Float8E8M0FNU: + raise ValueError( + f"FP4 x FP8 mixed-precision requires --sf_dtype Float8E8M0FNU, " + f"got --sf_dtype {args.sf_dtype}" + ) + if tile not in fp8_allowed_tiles: + raise ValueError( + f"tile_shape {tile} is not supported for FP4 x FP8 mixed-precision. " + f"Allowed mixed tile shapes: {sorted(fp8_allowed_tiles)}." + ) + return + if a_dtype in _FP8_DTYPES: + if args.sf_vec_size != 32: + raise ValueError( + f"FP8 a_dtype ({a_dtype}) requires --sf_vec_size 32, " + f"got {args.sf_vec_size}" + ) + if args.sf_dtype != cutlass.Float8E8M0FNU: + raise ValueError( + f"FP8 a_dtype + sf_vec_size=32 requires --sf_dtype Float8E8M0FNU, " + f"got --sf_dtype {args.sf_dtype}" + ) + if tile not in fp8_allowed_tiles: + raise ValueError( + f"tile_shape {tile} is not supported for MXFP8. " + f"Allowed FP8 tile shapes: {sorted(fp8_allowed_tiles)}." + ) + elif a_dtype == cutlass.Float4E2M1FN: + if args.sf_vec_size == 16 and args.sf_dtype != cutlass.Float8E4M3FN: + raise ValueError( + f"FP4 + --sf_vec_size 16 requires --sf_dtype Float8E4M3FN, " + f"got {args.sf_dtype}" + ) + if args.sf_vec_size == 32 and args.sf_dtype != cutlass.Float8E8M0FNU: + raise ValueError( + f"FP4 + --sf_vec_size 32 requires --sf_dtype Float8E8M0FNU, " + f"got {args.sf_dtype}" + ) + if tile not in fp4_allowed_tiles: + raise ValueError( + f"tile_shape {tile} is not supported for FP4 path. " + f"Allowed FP4 tile shapes: {sorted(fp4_allowed_tiles)}." + ) + if args.sf_vec_size == 32 and tile[2] % 128 != 0: + raise ValueError( + f"FP4 + sf_vec_size=32 (MXFP4) requires tile_K to be a " + f"multiple of 128, " + f"got tile_K={tile[2]}." + ) + else: + raise ValueError( + f"--a_dtype must be Float4E2M1FN, Float8E4M3FN, or Float8E5M2; " + f"got {a_dtype}" + ) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/fused_dx.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/fused_dx.py new file mode 100644 index 0000000000..ff99d57939 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/fused_dx.py @@ -0,0 +1,121 @@ +"""Fused in-kernel-dequant grouped dX Triton kernel (#23): dx[Mt,K] = A[Mt,N] @ Wdeq[expert(m),N,K] +with bf16 grad A + NVFP4 weight decoded IN-KERNEL (no bf16 weight materialization, vectorized, no +Python loop). Contiguous-grouped: each 128-row M-tile belongs to one expert via m_indices. +This is the memory-optimal backward dX (vs dequant+cuBLAS which materializes bf16 W).""" + +import torch +import triton +import triton.language as tl + + +def _codebook(dev): + return torch.tensor( + [ + 0.0, + 0.5, + 1.0, + 1.5, + 2.0, + 3.0, + 4.0, + 6.0, + 0.0, + -0.5, + -1.0, + -1.5, + -2.0, + -3.0, + -4.0, + -6.0, + ], + device=dev, + dtype=torch.float32, + ) + + +@triton.jit +def _fused_dx( + A, + Wq, + Ws, + MI, + PT, + OUT, + CB, + N, + K, + sa0, + sa1, + sq0, + sq1, + sq2, + ss0, + ss1, + ss2, + so0, + so1, + BN: tl.constexpr, + BK: tl.constexpr, +): + pid_m = tl.program_id(0) # contiguous-grouped: each 128-row M-tile is one expert + pid_k = tl.program_id(1) + e = tl.load(MI + pid_m) + pt_e = tl.load(PT + e).to(tl.float32) + m = pid_m * 128 + tl.arange(0, 128) + k = pid_k * BK + tl.arange(0, BK) + km = k < K + acc = tl.zeros((128, BK), tl.float32) + for n0 in range(0, N, BN): + n = n0 + tl.arange(0, BN) + a = tl.load(A + m[:, None] * sa0 + n[None, :] * sa1) + wq = tl.load( + Wq + e * sq0 + n[:, None] * sq1 + (k[None, :] // 2) * sq2, + mask=km[None, :], + other=0, + ).to(tl.int32) + nib = tl.where(k[None, :] % 2 == 1, (wq >> 4) & 0xF, wq & 0xF) + ws = tl.load( + Ws + e * ss0 + n[:, None] * ss1 + (k[None, :] // 16) * ss2, + mask=km[None, :], + other=0.0, + ).to(tl.float32) + w = tl.load(CB + nib) * ws * pt_e + acc += tl.dot(a, w.to(tl.bfloat16)) + tl.store( + OUT + m[:, None] * so0 + k[None, :] * so1, acc.to(tl.bfloat16), mask=km[None, :] + ) + + +def fused_dx(A, Wq, Ws, m_indices, pt, K, BN=64, BK=64): + """A[Mt,N] bf16, Wq[E,N,K/2] u8, Ws[E,N,K/16] e4m3, pt[E] -> dx[Mt,K] bf16.""" + Mt, N = A.shape + # Mt//128 grid covers every row only if 128-padded; otherwise trailing rows stay uninitialized. + assert Mt % 128 == 0, ( + f"fused_dx expects 128-padded Mt (contiguous-grouped), got Mt={Mt}" + ) + out = torch.empty(Mt, K, device=A.device, dtype=torch.bfloat16) + grid = (Mt // 128, triton.cdiv(K, BK)) + _fused_dx[grid]( + A, + Wq, + Ws, + m_indices, + pt.contiguous(), + out, + _codebook(A.device), + N, + K, + A.stride(0), + A.stride(1), + Wq.stride(0), + Wq.stride(1), + Wq.stride(2), + Ws.stride(0), + Ws.stride(1), + Ws.stride(2), + out.stride(0), + out.stride(1), + BN=BN, + BK=BK, + ) + return out diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/grouped.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/grouped.py new file mode 100644 index 0000000000..55e7542c72 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/grouped.py @@ -0,0 +1,195 @@ +"""sm120 CUTLASS NVFP4 grouped-GEMM host wrapper. + +Contiguous-grouped MoE: tokens sorted by expert + padded per-expert to TILE(128); ONE grouped +GEMM where each m-tile reads its expert's weight via ``m_indices`` (grouped_kernel.py). Base-only +fp4xfp4 (nvfp4) or fp8xfp4 (mxfp). LoRA + swiglu + routing live in the unified dispatcher; this +file is the base GEMM engine. All cutlass/cute imports are lazy (module loads clean off sm120). +""" + +from __future__ import annotations + +import torch + +TILE = 128 + +# mode -> (a-dtype-name, sf_vec, sf-dtype-name) +_MODES = { + "nvfp4": ("Float4E2M1FN", 16, "Float8E4M3FN"), + "fp8": ("Float8E4M3FN", 32, "Float8E8M0FNU"), +} + + +def _cd(a, b): + return (a + b - 1) // b + + +def _helpers(): + """Lazy cute helpers (operand/SF build + inject). Imported only when the sm120 path runs.""" + import cutlass + import cutlass.cute as cute + import cutlass.torch as cutlass_torch + import cutlass.utils.blackwell_helpers as sm120_utils + from cutlass.cute.runtime import from_dlpack + + from .grouped_kernel import cvt_sf_MKL_to_M32x4xrm_K4xrk_L # noqa: F401 + + return ( + cutlass, + cute, + cutlass_torch, + from_dlpack, + sm120_utils, + cvt_sf_MKL_to_M32x4xrm_K4xrk_L, + ) + + +class GroupedFp4Gemm: + """C[Mt,N] = sorted A[Mt,K] @ B[m_indices[m_tile]]^T. One launch; expert per m-tile. + + Build once per (Mt, N, K, E, mode); inject weights once (set_weights), activation per-forward. + """ + + def __init__(self, Mt, N, K, E, mode="nvfp4"): + cutlass, cute, cutlass_torch, from_dlpack, sm120_utils, _ = _helpers() + from cutlass import BFloat16, Float4E2M1FN, Float32 + + from .grouped_kernel import Sm120BlockScaledGemmKernel + + a_name, sf_vec, sf_name = _MODES[mode] + a_dtype = getattr(cutlass, a_name) + sf_dtype = getattr(cutlass, sf_name) + self.Mt, self.N, self.K, self.E, self.mode = Mt, N, K, E, mode + self.sf_vec, self._fp4_a = sf_vec, a_dtype is Float4E2M1FN + + self.a_ct, self.a_st = self._operand(Mt, K, 1, a_dtype) + self.b_ct, self.b_st = self._operand(N, K, E, Float4E2M1FN) + self.sfa_ct, self.sfa_st, self.sfa_g, self.sfa_shape = self._sf( + Mt, K, 1, sf_vec, sf_dtype + ) + self.sfb_ct, self.sfb_st, self.sfb_g, self.sfb_shape = self._sf( + N, K, E, sf_vec, sf_dtype + ) + c_ref = cutlass_torch.matrix(1, Mt, N, False, BFloat16) + self.c_ct, self.c_st = cutlass_torch.cute_tensor_like( + c_ref, BFloat16, is_dynamic_layout=True, assumed_align=16 + ) + self.c_ct.mark_compact_shape_dynamic( + mode=1, stride_order=(2, 0, 1), divisibility=1 + ) + self.c_ct = cutlass_torch.convert_cute_tensor( + c_ref.cuda(), self.c_ct, BFloat16, is_dynamic_layout=True + ) + self.mi = torch.zeros(Mt // TILE, dtype=torch.int32, device="cuda") + self.mi_ct = from_dlpack(self.mi, assumed_align=4) + gemm = Sm120BlockScaledGemmKernel(Float32, sf_vec, (128, 128, 128), (128, 128)) + mac = cutlass.utils.HardwareInfo().get_max_active_clusters(1) + self.stream = cutlass_torch.default_stream() + self.compiled = cute.compile( + gemm, + self.a_ct, + self.b_ct, + self.sfa_ct, + self.sfb_ct, + self.c_ct, + self.mi_ct, + mac, + self.stream, + ) + + def _operand(self, mn, k, E, op_dtype): + cutlass, cute, cutlass_torch, _, _, _ = _helpers() + from cutlass import Float4E2M1FN, Float32 + + ref = cutlass_torch.matrix(E, mn, k, False, Float32) + ct, st = cutlass_torch.cute_tensor_like( + ref, op_dtype, is_dynamic_layout=True, assumed_align=16 + ) + ct.mark_compact_shape_dynamic( + mode=1, + stride_order=(2, 0, 1), + divisibility=2 if op_dtype is Float4E2M1FN else 1, + ) + ct = cutlass_torch.convert_cute_tensor( + ref.cuda(), ct, op_dtype, is_dynamic_layout=True + ) + return ct, st + + def _sf(self, mn, k, E, sf_vec, sf_dtype): + cutlass, cute, cutlass_torch, from_dlpack, _, cvt = _helpers() + sfk = _cd(k, sf_vec) + mma_shape = (E, _cd(mn, 128), _cd(sfk, 4), 32, 4, 4) + nat = cutlass_torch.create_and_permute_torch_tensor( + (E, mn, sfk), + torch.float64, + permute_order=(1, 2, 0), + init_type=cutlass_torch.TensorInitType.SKIP, + ) + nat.copy_( + torch.arange(E * mn * sfk, dtype=torch.float64) + .reshape(E, mn, sfk) + .permute(1, 2, 0) + ) + cf = cutlass_torch.create_and_permute_torch_tensor( + mma_shape, + torch.float64, + permute_order=(3, 4, 1, 5, 2, 0), + init_type=cutlass_torch.TensorInitType.SKIP, + ) + cvt(from_dlpack(nat), from_dlpack(cf)) + gather = cf.reshape(-1).round().long().cuda() + cf_e = cutlass_torch.create_and_permute_torch_tensor( + mma_shape, + torch.float32, + permute_order=(3, 4, 1, 5, 2, 0), + init_type=cutlass_torch.TensorInitType.SKIP, + ) + ct, st = cutlass_torch.cute_tensor_like( + cf_e, sf_dtype, is_dynamic_layout=True, assumed_align=16 + ) + ct = cutlass_torch.convert_cute_tensor( + cf_e.cuda(), ct, sf_dtype, is_dynamic_layout=True + ) + return ct, st, gather, tuple(st.shape) + + @staticmethod + def _inject_operand(st, qdata): + n = qdata.numel() + st.view(torch.uint8).permute(2, 0, 1).reshape(-1)[:n].copy_( + qdata.reshape(-1).view(torch.uint8) + ) + + @staticmethod + def _inject_sf(st, gather, shape, scale_flat): + st.view(torch.uint8).copy_(scale_flat.view(torch.uint8)[gather].reshape(shape)) + + def set_weights(self, q, s): # q:[E,N,K/2], s:[E,N,sfk]; one-time per weight gather + self._inject_operand(self.b_st, q) + self._inject_sf(self.sfb_st, self.sfb_g, self.sfb_shape, s.reshape(-1)) + + def forward( + self, a_q, a_s, m_indices + ): # a_q:[1,Mt,*], a_s:[1,Mt,sfk], m_indices:[Mt/128] + self._inject_operand(self.a_st, a_q) + self._inject_sf(self.sfa_st, self.sfa_g, self.sfa_shape, a_s.reshape(-1)) + self.mi.copy_(m_indices) + self.compiled( + self.a_ct, + self.b_ct, + self.sfa_ct, + self.sfb_ct, + self.c_ct, + self.mi_ct, + self.stream, + ) + return self.c_st[:, :, 0] + + +def quant_act(x, mode): + """Fast Triton activation quant for the grouped path -> (qdata[Mt,*], scale[Mt,sfk]).""" + if mode == "nvfp4": + from .quant_nvfp4 import nvfp4_quant + + return nvfp4_quant(x) + from .quant_mxfp8 import mxfp8_quant + + return mxfp8_quant(x) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/grouped_kernel.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/grouped_kernel.py new file mode 100644 index 0000000000..48365588e8 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/grouped_kernel.py @@ -0,0 +1,1917 @@ +# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import argparse +from typing import Optional, Tuple, Type + +import cuda.bindings.driver as cuda +import cutlass +import cutlass.cute as cute +import cutlass.cute.testing as testing +import cutlass.pipeline as pipeline +import cutlass.utils as utils +import cutlass.utils.blackwell_helpers as sm120_utils +import cutlass.utils.blockscaled_layout as blockscaled_utils +import cutlass.utils.hopper_helpers as sm90_utils +from cutlass.cute.nvgpu import cpasync +from cutlass.cute.runtime import from_dlpack + +from .blockscaled_gemm_dispatch import ( # noqa: F401 + FP4_SHIFT_BITS, + make_ldmatrix_atom, + make_sm120_blockscaled_mma_op, + validate_blockscaled_args, +) + +""" +A high-performance batched dense blockscaled GEMM (C = A*SF_A * B*SF_B) example for the NVIDIA Blackwell Geforce architecture using CUTE DSL. +- Matrix A is MxKxL, L is batch dimension, A can only be row-major("K") +- Matrix B is NxKxL, L is batch dimension, B can only be column-major("K") +- Matrix C is MxNxL, L is batch dimension, C can only be row-major("N") +- Matrix SFA layout is filled internally according to A shape and BlockScaledBasicChunk, which has M×ceil_div(K, sf_vec_size)×L elements respectively +- Matrix SFB layout is filled internally according to B shape and BlockScaledBasicChunk, which has N×ceil_div(K, sf_vec_size)×L elements respectively +- Source formats for matrices A and B: The only supported source format in this example is E2M1. +- Source formats for matrices SF_A and SF_B are controlled separately. With sf_vec_size=32, the only supported source format is E8. With sf_vec_size=16, the supported source formats are E8 and E4M3. + +This GEMM kernel supports the following features: + - Utilizes Tensor Memory Access (TMA) for efficient memory operations + - Utilizes warp-level block-scaled MMA for matrix multiply-accumulate (MMA) operations + - Supports persistent tile scheduling to better overlap memory load/store with MMA between tiles + - Supports warp specialization to avoid explicit pipelining between mainloop load and MMA + - Uses a cooperative schedule: both warp groups work together to process a single output tile. + The two warp groups collaboratively execute the MMA mainloop, each computing a portion of + the tile, and then jointly perform the epilogue. This increases computational throughput + per tile by leveraging both warp groups simultaneously. + +This GEMM works as follows: +1. DMA warp group: + - Load A and B matrices from global memory (GMEM) to shared memory (SMEM) using TMA operations. + - Load scale factor A and B matrices from global memory (GMEM) to shared memory (SMEM) using TMA operations. +2. MMA warp groups (both warp groups cooperate on the same tile): + - Load A/B from shared memory (SMEM) to registers (RMEM) using ldmatrix instruction. + - Load scale factor A/B from shared memory (SMEM) to registers (RMEM) using universal copy. + - Perform matrix multiply-accumulate (MMA) operations using warp-level block-scaled MMA instruction. + - Store C matrix from registers (RMEM) to shared memory (SMEM), then to global memory (GMEM) with TMA operations. + Note: Both MMA warp groups jointly handle MMA and epilogue for the same tile. + +Warp-level block-scaled MMA instructions operate as follows: +- Set matrix scale factor A/B from registers +- Read matrix A/B from registers +- Perform MMA operation and store the result in Accumulator(register) + +To run this example: + +.. code-block:: bash + + python examples/cute/blackwell_geforce/kernel/blockscaled_gemm/dense_blockscaled_gemm_persistent_cooperative.py \ + --mnkl 1024,1024,1024,1 --tile_shape_mnk 128,128,128 \ + --a_dtype Float4E2M1FN --b_dtype Float4E2M1FN \ + --c_dtype Float16 --acc_dtype Float32 \ + --sf_dtype Float8E4M3FN --sf_vec_size 16 + +The above example command compute batched gemm with M=1024, N=1024, K=1024, +batch_count=1. The tile shape is 128x128x128 and the cluster shape is (1,1). +The input, mma accumulator and output data type are set as fp4, fp32 +and fp16, respectively. + +To collect performance with NCU profiler: + +.. code-block:: bash + + ncu python examples/cute/blackwell_geforce/kernel/blockscaled_gemm/dense_blockscaled_gemm_persistent_cooperative.py \ + --mnkl 1024,1024,1024,1 --tile_shape_mnk 128,128,128 \ + --a_dtype Float4E2M1FN --b_dtype Float4E2M1FN \ + --c_dtype Float16 --acc_dtype Float32 \ + --sf_dtype Float8E4M3FN --sf_vec_size 16 + +Constraints: +* Supported input data types: Float4E2M1FN +* Only Float32 accumulation is supported in FP4 mma +* CTA tile shape M/N/K: + - tile_shape_m should be divisible by 128 + - tile_shape_n should be divisible by 128 + - tile_shape_k should be divisible by 64 (sf_vec_size=16) or 128 (sf_vec_size=32) +* Cluster shape M/N must be [1, 1] for Blackwell Gefore +""" + + +def parse_comma_separated_ints(s: str): + try: + return tuple([int(x.strip()) for x in s.split(",")]) + except ValueError: + raise argparse.ArgumentTypeError( + "Invalid format. Expected comma-separated integers." + ) from None + + +class Sm120BlockScaledGemmKernel: + def __init__( + self, + acc_dtype, + sf_vec_size, + tile_shape_mnk, + epi_tile, + ): + self.acc_dtype = acc_dtype + self.sf_vec_size = sf_vec_size + self.cluster_shape_mnk = (1, 1, 1) + self.tile_shape_mnk = tuple(tile_shape_mnk) + self.epi_tile = tuple(epi_tile) + self.tiled_mma = None + + self.occupancy = 1 + self.num_mma_warps = 8 + self.tma_load_warp_id = self.num_mma_warps + self.num_threads_per_warp = 32 + self.threads_per_cta = ( + self.num_mma_warps + 1 # 1 warp for DMA + ) * self.num_threads_per_warp + self.smem_capacity = utils.get_smem_capacity_in_bytes("sm_120") + + self.ab_stage = None + self.epi_stage = None + + self.a_smem_layout_staged = None + self.b_smem_layout_staged = None + self.epi_smem_layout_staged = None + + self.buffer_align_bytes = 1024 + + self.mma_sync_barrier = pipeline.NamedBarrier( + barrier_id=1, + num_threads=self.num_mma_warps * self.num_threads_per_warp, + ) + self.epilog_sync_barrier = pipeline.NamedBarrier( + barrier_id=2, + num_threads=self.num_mma_warps * self.num_threads_per_warp, + ) + self.load_register_requirement = 40 + self.mma_register_requirement = 232 + + def _setup_attributes(self): + mma_op, use_mxf8f6f4 = make_sm120_blockscaled_mma_op( + self.a_dtype, + self.b_dtype, + self.acc_dtype, + self.sf_dtype, + self.sf_vec_size, + ) + self.mixed_mode = self.a_dtype != self.b_dtype + # a_fp4_in_mixed / b_fp4_in_mixed: this side carries an FP4 operand in the + # mixed FP4 x FP8 mode, so SMEM/TMA see Int8 storage and the mma.sync + # consumer needs the LDSM b4x16_p64 unpack + register `<< FP4_SHIFT_BITS`. + a_fp4_in_mixed = self.mixed_mode and self.a_dtype.width < 8 + b_fp4_in_mixed = self.mixed_mode and self.b_dtype.width < 8 + self.smem_alloc_a_dtype = cutlass.Int8 if a_fp4_in_mixed else self.a_dtype + self.smem_alloc_b_dtype = cutlass.Int8 if b_fp4_in_mixed else self.b_dtype + # `internal_type` for `_make_tma_atoms_and_tensors`: None when the dtype + # already matches (TMA sees the native dtype), Int8 when we recast for FP4. + self.tma_internal_a_dtype = cutlass.Int8 if a_fp4_in_mixed else None + self.tma_internal_b_dtype = cutlass.Int8 if b_fp4_in_mixed else None + atom_shape = (4, 2, 1) + atom_layout = cute.make_layout(atom_shape) + permutation_mnk = sm120_utils.get_permutation_mnk( + self.tile_shape_mnk, self.sf_vec_size, use_mxf8f6f4 + ) + self.tiled_mma = cute.make_tiled_mma( + mma_op, + atom_layout, + permutation_mnk=permutation_mnk, + ) + + self.cta_layout_mnk = cute.make_layout(self.cluster_shape_mnk) + + sfa_smem_layout_per_stage = blockscaled_utils.sm120_make_smem_layout_sfa( + self.tiled_mma, + self.tile_shape_mnk, + self.sf_vec_size, + 1, + ) + + sfb_smem_layout_per_stage = blockscaled_utils.sm120_make_smem_layout_sfb( + self.tiled_mma, + self.tile_shape_mnk, + self.sf_vec_size, + 1, + ) + + self.ab_stage, self.epi_stage = self._compute_stages( + self.tile_shape_mnk, + self.smem_alloc_a_dtype, + self.smem_alloc_b_dtype, + self.sf_dtype, + sfa_smem_layout_per_stage, + sfb_smem_layout_per_stage, + self.epi_tile, + self.c_dtype, + self.smem_capacity, + self.occupancy, + ) + + assert self.epi_stage > 0, ( + "epi_stage <= 0, no enough shared memory. This case will be skipped." + ) + + ( + self.a_smem_layout_staged, + self.b_smem_layout_staged, + self.sfa_smem_layout_staged, + self.sfb_smem_layout_staged, + self.epi_smem_layout_staged, + ) = self._make_smem_layouts( + self.tile_shape_mnk, + self.epi_tile, + self.smem_alloc_a_dtype, + self.a_layout, + self.smem_alloc_b_dtype, + self.b_layout, + self.ab_stage, + self.c_dtype, + self.c_layout, + self.epi_stage, + self.sf_vec_size, + self.tiled_mma, + ) + + @cute.jit + def __call__( + self, + a: cute.Tensor, + b: cute.Tensor, + sfa: cute.Tensor, + sfb: cute.Tensor, + c: cute.Tensor, + m_indices: cute.Tensor, + max_active_clusters: cutlass.Constexpr, + stream: cuda.CUstream, + ): + """Execute the GEMM operation in steps: + - Setup static attributes + - Setup TMA load/store atoms and tensors + - Compute grid size + - Define shared storage for kernel + - Launch the kernel synchronously + + :param a: Input tensor A + :type a: cute.Tensor + :param b: Input tensor B + :type b: cute.Tensor + :param c: Output tensor C + :type c: cute.Tensor + :param stream: CUDA stream for asynchronous execution + :type stream: cuda.CUstream + """ + + # setup static attributes before smem/grid/tma computation + self.a_dtype = a.element_type + self.b_dtype = b.element_type + self.c_dtype = c.element_type + self.sf_dtype = sfa.element_type + + self.a_layout = utils.LayoutEnum.from_tensor(a) + self.b_layout = utils.LayoutEnum.from_tensor(b) + self.c_layout = utils.LayoutEnum.from_tensor(c) + + self._setup_attributes() + + # ((Atom_M, Rest_M),(Atom_K, Rest_K),RestL) + self.sfa_layout = blockscaled_utils.tile_atom_to_shape_SF( + a.shape, self.sf_vec_size + ) + sfa_tensor = cute.make_tensor(sfa.iterator, self.sfa_layout) + # ((Atom_N, Rest_N),(Atom_K, Rest_K),RestL) + self.sfb_layout = blockscaled_utils.tile_atom_to_shape_SF( + b.shape, self.sf_vec_size + ) + sfb_tensor = cute.make_tensor(sfb.iterator, self.sfb_layout) + + tma_atom_a, tma_tensor_a = self._make_tma_atoms_and_tensors( + a, + self.a_smem_layout_staged, + (self.tile_shape_mnk[0], self.tile_shape_mnk[2]), + 1, + internal_type=self.tma_internal_a_dtype, + ) + + tma_atom_b, tma_tensor_b = self._make_tma_atoms_and_tensors( + b, + self.b_smem_layout_staged, + (self.tile_shape_mnk[1], self.tile_shape_mnk[2]), + 1, + internal_type=self.tma_internal_b_dtype, + ) + + tma_atom_sfa, tma_tensor_sfa = self._make_tma_atoms_and_tensors( + sfa_tensor, + self.sfa_smem_layout_staged, + (self.tile_shape_mnk[0], self.tile_shape_mnk[2]), + 1, + internal_type=cutlass.Int16, + ) + + tma_atom_sfb, tma_tensor_sfb = self._make_tma_atoms_and_tensors( + sfb_tensor, + self.sfb_smem_layout_staged, + (self.tile_shape_mnk[1], self.tile_shape_mnk[2]), + 1, + internal_type=cutlass.Int16, + ) + + tma_atom_c, tma_tensor_c = self._make_tma_store_atoms_and_tensors( + c, + self.epi_smem_layout_staged, + self.epi_tile, + ) + + tile_sched_params, grid = self._compute_grid( + c, + self.tile_shape_mnk, + max_active_clusters, + ) + + @cute.struct + class SharedStorage: + mainloop_pipeline_array_ptr: cute.struct.MemRange[ + cutlass.Int64, self.ab_stage * 2 + ] + sA: cute.struct.Align[ + cute.struct.MemRange[ + self.smem_alloc_a_dtype, cute.cosize(self.a_smem_layout_staged) + ], + self.buffer_align_bytes, + ] + sB: cute.struct.Align[ + cute.struct.MemRange[ + self.smem_alloc_b_dtype, cute.cosize(self.b_smem_layout_staged) + ], + self.buffer_align_bytes, + ] + sSFA: cute.struct.Align[ + cute.struct.MemRange[ + self.sf_dtype, cute.cosize(self.sfa_smem_layout_staged) + ], + self.buffer_align_bytes, + ] + sSFB: cute.struct.Align[ + cute.struct.MemRange[ + self.sf_dtype, cute.cosize(self.sfb_smem_layout_staged) + ], + self.buffer_align_bytes, + ] + sC: cute.struct.Align[ + cute.struct.MemRange[ + self.c_dtype, cute.cosize(self.epi_smem_layout_staged) + ], + self.buffer_align_bytes, + ] + + self.shared_storage = SharedStorage + + self.kernel( + tma_atom_a, + tma_tensor_a, + tma_atom_b, + tma_tensor_b, + tma_atom_sfa, + tma_tensor_sfa, + tma_atom_sfb, + tma_tensor_sfb, + tma_atom_c, + tma_tensor_c, + self.tiled_mma, + self.cta_layout_mnk, + self.a_smem_layout_staged, + self.b_smem_layout_staged, + self.sfa_smem_layout_staged, + self.sfb_smem_layout_staged, + self.epi_smem_layout_staged, + m_indices, + tile_sched_params, + ).launch( + grid=grid, + block=[self.threads_per_cta, 1, 1], + cluster=[1, 1, 1], + stream=stream, + ) + return + + @cute.kernel + def kernel( + self, + tma_atom_a: cute.CopyAtom, + mA_mkl: cute.Tensor, + tma_atom_b: cute.CopyAtom, + mB_nkl: cute.Tensor, + tma_atom_sfa: cute.CopyAtom, + mSFA_mkl: cute.Tensor, + tma_atom_sfb: cute.CopyAtom, + mSFB_nkl: cute.Tensor, + tma_atom_c: cute.CopyAtom, + mC_mnl: cute.Tensor, + tiled_mma: cute.TiledMma, + cta_layout_mnk: cute.Layout, + a_smem_layout_staged: cute.ComposedLayout, + b_smem_layout_staged: cute.ComposedLayout, + sfa_smem_layout_staged: cute.Layout, + sfb_smem_layout_staged: cute.Layout, + epi_smem_layout_staged: cute.ComposedLayout, + m_indices: cute.Tensor, + tile_sched_params: utils.PersistentTileSchedulerParams, + ): + """ + GPU device kernel performing the batched GEMM computation. + + :param tma_atom_a: TMA copy atom for A tensor + :type tma_atom_a: cute.CopyAtom + :param mA_mkl: Input tensor A + :type mA_mkl: cute.Tensor + :param tma_atom_b: TMA copy atom for B tensor + :type tma_atom_b: cute.CopyAtom + :param mB_nkl: Input tensor B + :type mB_nkl: cute.Tensor + :param tma_atom_c: TMA copy atom for C tensor + :type tma_atom_c: cute.CopyAtom + :param mC_mnl: Output tensor C + :type mC_mnl: cute.Tensor + :param tiled_mma: Tiled MMA object + :type tiled_mma: cute.TiledMma + :param cta_layout_mnk: CTA layout + :type cta_layout_mnk: cute.Layout + :param a_smem_layout_staged: Shared memory layout for A + :type a_smem_layout_staged: cute.ComposedLayout + :param b_smem_layout_staged: Shared memory layout for B + :type b_smem_layout_staged: cute.ComposedLayout + :param epi_smem_layout_staged: Shared memory layout for epilogue + :type epi_smem_layout_staged: cute.ComposedLayout + """ + + tidx, _, _ = cute.arch.thread_idx() + warp_idx = cute.arch.warp_idx() + warp_idx = cute.arch.make_warp_uniform(warp_idx) + + if warp_idx == 0: + cpasync.prefetch_descriptor(tma_atom_a) + cpasync.prefetch_descriptor(tma_atom_b) + cpasync.prefetch_descriptor(tma_atom_sfa) + cpasync.prefetch_descriptor(tma_atom_sfb) + cpasync.prefetch_descriptor(tma_atom_c) + + cta_rank_in_cluster = cute.arch.make_warp_uniform( + cute.arch.block_idx_in_cluster() + ) + cluster_coord_mnk = cta_layout_mnk.get_flat_coord(cta_rank_in_cluster) + + a_smem_layout = cute.slice_(a_smem_layout_staged, (None, None, 0)) + b_smem_layout = cute.slice_(b_smem_layout_staged, (None, None, 0)) + sfa_smem_layout = cute.slice_(sfa_smem_layout_staged, (None, None, 0)) + sfb_smem_layout = cute.slice_(sfb_smem_layout_staged, (None, None, 0)) + tma_copy_bytes = ( + cute.size_in_bytes(self.a_dtype, a_smem_layout) + + cute.size_in_bytes(self.b_dtype, b_smem_layout) + + cute.size_in_bytes(self.sf_dtype, sfa_smem_layout) + + cute.size_in_bytes(self.sf_dtype, sfb_smem_layout) + ) + + smem = cutlass.utils.SmemAllocator() + storage = smem.allocate(self.shared_storage) + + mainloop_pipeline_array_ptr = storage.mainloop_pipeline_array_ptr.data_ptr() + + mainloop_pipeline_producer_group = pipeline.CooperativeGroup( + pipeline.Agent.Thread + ) + mainloop_pipeline_consumer_group = pipeline.CooperativeGroup( + pipeline.Agent.Thread, self.num_mma_warps + ) + + cta_layout_vmnk = cute.make_layout((1, *cta_layout_mnk.shape)) + mainloop_pipeline = pipeline.PipelineTmaAsync.create( + num_stages=self.ab_stage, + producer_group=mainloop_pipeline_producer_group, + consumer_group=mainloop_pipeline_consumer_group, + tx_count=tma_copy_bytes, + barrier_storage=mainloop_pipeline_array_ptr, + cta_layout_vmnk=cta_layout_vmnk, + ) + + if cute.size(self.cluster_shape_mnk) > 1: + cute.arch.cluster_arrive_relaxed() + + sA = storage.sA.get_tensor( + a_smem_layout_staged.outer, swizzle=a_smem_layout_staged.inner + ) + sB = storage.sB.get_tensor( + b_smem_layout_staged.outer, swizzle=b_smem_layout_staged.inner + ) + sC = storage.sC.get_tensor( + epi_smem_layout_staged.outer, swizzle=epi_smem_layout_staged.inner + ) + sSFA = storage.sSFA.get_tensor(sfa_smem_layout_staged) + sSFB = storage.sSFB.get_tensor(sfb_smem_layout_staged) + + # (bM, bK, loopM, loopK, loopL) + gA_mkl = cute.local_tile( + mA_mkl, + cute.slice_(self.tile_shape_mnk, (None, 0, None)), + (None, None, None), + ) + # (bN, bK, loopN, loopK, loopL) + gB_nkl = cute.local_tile( + mB_nkl, + cute.slice_(self.tile_shape_mnk, (0, None, None)), + (None, None, None), + ) + # (tM, tK, loopM, loopK, loopL) + gSFA_mkl = cute.local_tile( + mSFA_mkl, + cute.slice_(self.tile_shape_mnk, (None, 0, None)), + (None, None, None), + ) + # (tN, tK, loopN, loopK, loopL) + gSFB_nkl = cute.local_tile( + mSFB_nkl, + cute.slice_(self.tile_shape_mnk, (0, None, None)), + (None, None, None), + ) + # (bM, bN, loopM, loopN, loopL) + gC_mnl = cute.local_tile( + mC_mnl, + cute.slice_(self.tile_shape_mnk, (None, None, 0)), + (None, None, None), + ) + + thr_mma = tiled_mma.get_slice(tidx) + + a_cta_layout = cute.make_layout(cute.slice_(cta_layout_mnk, (0, None, 0)).shape) + a_cta_crd = cluster_coord_mnk[1] + tAsA, tAgA = cpasync.tma_partition( + tma_atom_a, + a_cta_crd, + a_cta_layout, + cute.group_modes(sA, 0, 2), + cute.group_modes(gA_mkl, 0, 2), + ) + + b_cta_layout = cute.make_layout(cute.slice_(cta_layout_mnk, (None, 0, 0)).shape) + b_cta_crd = cluster_coord_mnk[0] + tBsB, tBgB = cpasync.tma_partition( + tma_atom_b, + b_cta_crd, + b_cta_layout, + cute.group_modes(sB, 0, 2), + cute.group_modes(gB_nkl, 0, 2), + ) + + tAsSFA, tAgSFA = cpasync.tma_partition( + tma_atom_sfa, + a_cta_crd, + a_cta_layout, + cute.group_modes(sSFA, 0, 2), + cute.group_modes(gSFA_mkl, 0, 2), + ) + tAsSFA = cute.filter_zeros(tAsSFA) + tAgSFA = cute.filter_zeros(tAgSFA) + + tBsSFB, tBgSFB = cpasync.tma_partition( + tma_atom_sfb, + b_cta_crd, + b_cta_layout, + cute.group_modes(sSFB, 0, 2), + cute.group_modes(gSFB_nkl, 0, 2), + ) + tBsSFB = cute.filter_zeros(tBsSFB) + tBgSFB = cute.filter_zeros(tBgSFB) + + tCsA = thr_mma.partition_A(sA) + tCsB = thr_mma.partition_B(sB) + + tCrA = tiled_mma.make_fragment_A(tCsA[None, None, None, 0]) + tCrB = tiled_mma.make_fragment_B(tCsB[None, None, None, 0]) + tCrSFA = sm120_utils.partition_fragment_SFA(sSFA[None, None, 0], thr_mma, tidx) + tCrSFB = sm120_utils.partition_fragment_SFB(sSFB[None, None, 0], thr_mma, tidx) + # Keep residual K modes nested to match the C++ SM120 block-scaled mainloop. + tCrSFA = cute.group_modes(tCrSFA, 2, cute.rank(tCrSFA)) + tCrSFB = cute.group_modes(tCrSFB, 2, cute.rank(tCrSFB)) + + tCgC = thr_mma.partition_C(gC_mnl) + acc_shape = tCgC.shape[:3] + accumulators = cute.make_rmem_tensor(acc_shape, self.acc_dtype) + + if cute.size(self.cluster_shape_mnk) > 1: + cute.arch.cluster_wait() + else: + cute.arch.sync_threads() + + k_tile_cnt = cute.size(gA_mkl, mode=[3]) + + tile_sched = utils.StaticPersistentTileScheduler.create( + tile_sched_params, cute.arch.block_idx(), cute.arch.grid_dim() + ) + work_tile = tile_sched.initial_work_tile_info() + + mainloop_producer_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Producer, self.ab_stage + ) + mainloop_consumer_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Consumer, self.ab_stage + ) + + # MMA warp group + if warp_idx < self.num_mma_warps: + cute.arch.setmaxregister_increase(self.mma_register_requirement) + + num_k_blocks = cute.size(tCrA, mode=[2]) + + atom_copy_ldmatrix_A = make_ldmatrix_atom( + self.a_dtype, + transpose=self.a_layout.is_m_major_a(), + num_matrices=4, + mixed_mode=self.mixed_mode, + ) + atom_copy_ldmatrix_B = make_ldmatrix_atom( + self.b_dtype, + transpose=self.b_layout.is_n_major_b(), + num_matrices=4, + mixed_mode=self.mixed_mode, + ) + smem_tiled_copy_A = cute.make_tiled_copy_A(atom_copy_ldmatrix_A, tiled_mma) + smem_tiled_copy_B = cute.make_tiled_copy_B(atom_copy_ldmatrix_B, tiled_mma) + + atom_copy_ldmatrix_SF = cute.make_copy_atom( + cute.nvgpu.CopyUniversalOp(), + self.sf_dtype, + ) + smem_tiled_copy_SFA = cute.make_tiled_copy( + atom_copy_ldmatrix_SF, + sm120_utils.get_layoutSFA_TV(tiled_mma), + ( + cute.size(tiled_mma.permutation_mnk[0]), + cute.size(tiled_mma.permutation_mnk[2]), + ), + ) + smem_tiled_copy_SFB = cute.make_tiled_copy( + atom_copy_ldmatrix_SF, + sm120_utils.get_layoutSFB_TV(tiled_mma), + ( + cute.size(tiled_mma.permutation_mnk[1]), + cute.size(tiled_mma.permutation_mnk[2]), + ), + ) + + thr_copy_ldmatrix_A = smem_tiled_copy_A.get_slice(tidx) + thr_copy_ldmatrix_B = smem_tiled_copy_B.get_slice(tidx) + tCsA_copy_view = thr_copy_ldmatrix_A.partition_S(sA) + tCrA_copy_view = thr_copy_ldmatrix_A.retile(tCrA) + tCsB_copy_view = thr_copy_ldmatrix_B.partition_S(sB) + tCrB_copy_view = thr_copy_ldmatrix_B.retile(tCrB) + + thr_copy_ldmatrix_SFA = smem_tiled_copy_SFA.get_slice(tidx) + thr_copy_ldmatrix_SFB = smem_tiled_copy_SFB.get_slice(tidx) + tCsSFA_copy_view = thr_copy_ldmatrix_SFA.partition_S(sSFA) + tCrSFA_copy_view = thr_copy_ldmatrix_SFA.retile(tCrSFA) + tCsSFB_copy_view = thr_copy_ldmatrix_SFB.partition_S(sSFB) + tCrSFB_copy_view = thr_copy_ldmatrix_SFB.retile(tCrSFB) + + epi_buffer = cutlass.Int32(0) + + while work_tile.is_valid_tile: + tile_coord_mnl = work_tile.tile_idx + gC_mnl_slice = gC_mnl[(None, None, *tile_coord_mnl)] + accumulators.fill(0.0) + + mainloop_consumer_state.reset_count() + + peek_ab_full_status = cutlass.Boolean(1) + if mainloop_consumer_state.count < k_tile_cnt: + peek_ab_full_status = mainloop_pipeline.consumer_try_wait( + mainloop_consumer_state + ) + + mainloop_pipeline.consumer_wait( + mainloop_consumer_state, peek_ab_full_status + ) + # tCsA_p: (MMA, (4, MMA_M / 4), MMA_K), tCsA_p: (MMA, (4, MMA_N / 4), MMA_K) + tCsA_p = tCsA_copy_view[None, None, None, mainloop_consumer_state.index] + tCsB_p = tCsB_copy_view[None, None, None, mainloop_consumer_state.index] + tCsSFA_p = tCsSFA_copy_view[ + None, None, None, mainloop_consumer_state.index + ] + tCsSFB_p = tCsSFB_copy_view[ + None, None, None, mainloop_consumer_state.index + ] + cute.copy( + smem_tiled_copy_A, + tCsA_p[None, None, 0], + tCrA_copy_view[None, None, 0], + ) + cute.copy( + smem_tiled_copy_B, + tCsB_p[None, None, 0], + tCrB_copy_view[None, None, 0], + ) + + tCsSFA_p_filtered = cute.filter_zeros(tCsSFA_p) + tCsSFB_p_filtered = cute.filter_zeros(tCsSFB_p) + tCrSFA_copy_view_filtered = cute.filter_zeros(tCrSFA_copy_view) + tCrSFB_copy_view_filtered = cute.filter_zeros(tCrSFB_copy_view) + + cute.copy( + smem_tiled_copy_SFA, + tCsSFA_p_filtered[None, None, 0], + tCrSFA_copy_view_filtered[None, None, 0], + ) + cute.copy( + smem_tiled_copy_SFB, + tCsSFB_p_filtered[None, None, 0], + tCrSFB_copy_view_filtered[None, None, 0], + ) + + for _k_tile in range(0, k_tile_cnt - 1, 1, unroll=1): + for k_block_idx in cutlass.range_constexpr(num_k_blocks): + k_block_next = ( + 0 if k_block_idx + 1 == num_k_blocks else k_block_idx + 1 + ) + + if k_block_idx == num_k_blocks - 1: + mainloop_pipeline.consumer_release(mainloop_consumer_state) + mainloop_consumer_state.advance() + + peek_ab_full_status = cutlass.Boolean(1) + peek_ab_full_status = mainloop_pipeline.consumer_try_wait( + mainloop_consumer_state + ) + + tCsA_p = tCsA_copy_view[ + None, None, None, mainloop_consumer_state.index + ] + tCsB_p = tCsB_copy_view[ + None, None, None, mainloop_consumer_state.index + ] + tCsSFA_p = tCsSFA_copy_view[ + None, None, None, mainloop_consumer_state.index + ] + tCsSFB_p = tCsSFB_copy_view[ + None, None, None, mainloop_consumer_state.index + ] + mainloop_pipeline.consumer_wait( + mainloop_consumer_state, peek_ab_full_status + ) + + # Mixed FP4 x FP8 register-side bit shift before mma.sync + # to move the FP4 nibble (in low half of each byte after + # b4x16_p64 ldmatrix) into the middle as mxf8f6f4 expects. + if cutlass.const_expr( + self.mixed_mode and self.a_dtype.width < 8 + ): + a_view = cute.recast_tensor( + tCrA[None, None, k_block_idx], cutlass.Int8 + ) + for _i in cutlass.range_constexpr(cute.size(a_view)): + a_view[_i] = cutlass.Int8(a_view[_i] << FP4_SHIFT_BITS) + if cutlass.const_expr( + self.mixed_mode and self.b_dtype.width < 8 + ): + b_view = cute.recast_tensor( + tCrB[None, None, k_block_idx], cutlass.Int8 + ) + for _i in cutlass.range_constexpr(cute.size(b_view)): + b_view[_i] = cutlass.Int8(b_view[_i] << FP4_SHIFT_BITS) + cute.gemm( + tiled_mma, + accumulators, + [ + tCrA[None, None, k_block_idx], + tCrSFA[None, None, k_block_idx], + ], + [ + tCrB[None, None, k_block_idx], + tCrSFB[None, None, k_block_idx], + ], + accumulators, + ) + cute.copy( + smem_tiled_copy_A, + tCsA_p[None, None, k_block_next], + tCrA_copy_view[None, None, k_block_next], + ) + cute.copy( + smem_tiled_copy_B, + tCsB_p[None, None, k_block_next], + tCrB_copy_view[None, None, k_block_next], + ) + + tCsSFA_p_filtered = cute.filter_zeros(tCsSFA_p) + tCsSFB_p_filtered = cute.filter_zeros(tCsSFB_p) + tCrSFA_copy_view_filtered = cute.filter_zeros(tCrSFA_copy_view) + tCrSFB_copy_view_filtered = cute.filter_zeros(tCrSFB_copy_view) + cute.copy( + smem_tiled_copy_SFA, + tCsSFA_p_filtered[None, None, k_block_next], + tCrSFA_copy_view_filtered[None, None, k_block_next], + ) + cute.copy( + smem_tiled_copy_SFB, + tCsSFB_p_filtered[None, None, k_block_next], + tCrSFB_copy_view_filtered[None, None, k_block_next], + ) + + # Hoist out last k_tile + for k_block_idx in cutlass.range_constexpr(num_k_blocks): + k_block_next = ( + 0 if k_block_idx + 1 == num_k_blocks else k_block_idx + 1 + ) + + if k_block_idx == num_k_blocks - 1: + cute.arch.fence_proxy("async.shared", space="cta") + mainloop_pipeline.consumer_release(mainloop_consumer_state) + mainloop_consumer_state.advance() + + if k_block_next > 0: + cute.copy( + smem_tiled_copy_A, + tCsA_p[None, None, k_block_next], + tCrA_copy_view[None, None, k_block_next], + ) + cute.copy( + smem_tiled_copy_B, + tCsB_p[None, None, k_block_next], + tCrB_copy_view[None, None, k_block_next], + ) + tCsSFA_p_filtered = cute.filter_zeros(tCsSFA_p) + tCsSFB_p_filtered = cute.filter_zeros(tCsSFB_p) + tCrSFA_copy_view_filtered = cute.filter_zeros(tCrSFA_copy_view) + tCrSFB_copy_view_filtered = cute.filter_zeros(tCrSFB_copy_view) + cute.copy( + smem_tiled_copy_SFA, + tCsSFA_p_filtered[None, None, k_block_next], + tCrSFA_copy_view_filtered[None, None, k_block_next], + ) + cute.copy( + smem_tiled_copy_SFB, + tCsSFB_p_filtered[None, None, k_block_next], + tCrSFB_copy_view_filtered[None, None, k_block_next], + ) + # Mixed FP4 x FP8 register-side bit shift before mma.sync (hoisted tail). + if cutlass.const_expr(self.mixed_mode and self.a_dtype.width < 8): + a_view_h = cute.recast_tensor( + tCrA[None, None, k_block_idx], cutlass.Int8 + ) + for _i in cutlass.range_constexpr(cute.size(a_view_h)): + a_view_h[_i] = cutlass.Int8(a_view_h[_i] << FP4_SHIFT_BITS) + if cutlass.const_expr(self.mixed_mode and self.b_dtype.width < 8): + b_view_h = cute.recast_tensor( + tCrB[None, None, k_block_idx], cutlass.Int8 + ) + for _i in cutlass.range_constexpr(cute.size(b_view_h)): + b_view_h[_i] = cutlass.Int8(b_view_h[_i] << FP4_SHIFT_BITS) + cute.gemm( + tiled_mma, + accumulators, + [ + tCrA[None, None, k_block_idx], + tCrSFA[None, None, k_block_idx], + ], + [ + tCrB[None, None, k_block_idx], + tCrSFB[None, None, k_block_idx], + ], + accumulators, + ) + + copy_atom_r2s = sm120_utils.sm120_get_smem_store_op( + self.c_layout, + elem_ty_d=self.c_dtype, + elem_ty_acc=self.acc_dtype, + ) + + copy_atom_C = cute.make_copy_atom( + cute.nvgpu.warp.StMatrix8x8x16bOp( + self.c_layout.is_m_major_c(), + 2, + ), + self.c_dtype, + ) + + tiled_copy_C_Atom = cute.make_tiled_copy_C_atom(copy_atom_C, tiled_mma) + + tiled_copy_r2s = cute.make_tiled_copy_S( + copy_atom_r2s, + tiled_copy_C_Atom, + ) + + thr_copy_r2s = tiled_copy_r2s.get_slice(tidx) + # (R2S, R2S_M, R2S_N, PIPE_D) + tRS_sD = thr_copy_r2s.partition_D(sC) + # (R2S, R2S_M, R2S_N) + tRS_rAcc = tiled_copy_r2s.retile(accumulators) + + rD_shape = cute.shape(thr_copy_r2s.partition_S(sC)) + tRS_rD_layout = cute.make_layout(rD_shape[:3]) + tRS_rD = cute.make_rmem_tensor(tRS_rD_layout.shape, self.acc_dtype) + _size_tRS_rD = cute.size(tRS_rD) + + sepi_for_tma_partition = cute.group_modes(sC, 0, 2) + tcgc_for_tma_partition = cute.zipped_divide(gC_mnl_slice, self.epi_tile) + + bSG_sD, bSG_gD = cpasync.tma_partition( + tma_atom_c, + 0, + cute.make_layout(1), + sepi_for_tma_partition, + tcgc_for_tma_partition, + ) + + tma_store_producer_group = pipeline.CooperativeGroup( + pipeline.Agent.Thread, + self.num_mma_warps * self.num_threads_per_warp, + ) + tma_store_pipeline = pipeline.PipelineTmaStore.create( + num_stages=self.epi_stage, + producer_group=tma_store_producer_group, + ) + + epi_rest_m = bSG_gD.shape[1][0] + epi_rest_n = bSG_gD.shape[1][1] + epi_tile_m = self.epi_tile[0] + epi_tile_n = self.epi_tile[1] + mma_tile_m = self.tile_shape_mnk[0] // cute.size(tRS_rAcc, mode=[1]) + mma_tile_n = self.tile_shape_mnk[1] // cute.size(tRS_rAcc, mode=[2]) + + for epi_m in cutlass.range_constexpr(epi_rest_m): + for epi_n in cutlass.range_constexpr(epi_rest_n): + MmaMPerEpiM = epi_tile_m // mma_tile_m + MmaNPerEpiN = epi_tile_n // mma_tile_n + for mma_n_in_epi in cutlass.range_constexpr(MmaNPerEpiN): + for mma_m_in_epi in cutlass.range_constexpr(MmaMPerEpiM): + mma_n = (epi_n * MmaNPerEpiN) + mma_n_in_epi + mma_m = (epi_m * MmaMPerEpiM) + mma_m_in_epi + tRS_rD_slice = tRS_rD[ + (None, mma_m_in_epi, mma_n_in_epi) + ] + tRS_rAcc_slice = tRS_rAcc[(None, mma_m, mma_n)] + for elem_idx in cutlass.range_constexpr( + cute.size(tRS_rD_slice) + ): + tRS_rD_slice[elem_idx] = tRS_rAcc_slice[elem_idx] + + tRS_rD_out = cute.make_rmem_tensor( + tRS_rD_layout.shape, self.c_dtype + ) + acc_vec = tRS_rD.load() + tRS_rD_out.store(acc_vec.to(self.c_dtype)) + + epi_buffer = epi_buffer + 1 + epi_buffer = epi_buffer % cute.size(tRS_sD, mode=[3]) + self.epilog_sync_barrier.arrive_and_wait() + cute.copy( + tiled_copy_r2s, + tRS_rD_out, + tRS_sD[(None, None, None, epi_buffer)], + ) + cute.arch.fence_proxy( + "async.shared", + space="cta", + ) + self.epilog_sync_barrier.arrive_and_wait() + + gmem_coord = (epi_m, epi_n) + if warp_idx == 0: + cute.copy( + tma_atom_c, + bSG_sD[(None, epi_buffer)], + bSG_gD[(None, gmem_coord)], + ) + tma_store_pipeline.producer_commit() + tma_store_pipeline.producer_acquire() + + tile_sched.advance_to_next_work() + work_tile = tile_sched.get_current_work() + # DMA warp group + elif warp_idx == self.tma_load_warp_id: + cute.arch.setmaxregister_decrease(self.load_register_requirement) + + while work_tile.is_valid_tile: + tile_coord_mnl = work_tile.tile_idx + # grouped: A/C span all sorted tokens (l=0); B/SFB pick this m-tile's expert + expert = m_indices[tile_coord_mnl[0]] + tAgA_mkl = tAgA[(None, tile_coord_mnl[0], None, tile_coord_mnl[2])] + tBgB_nkl = tBgB[(None, tile_coord_mnl[1], None, expert)] + tAgSFA_mkl = tAgSFA[(None, tile_coord_mnl[0], None, tile_coord_mnl[2])] + tBgSFB_nkl = tBgSFB[(None, tile_coord_mnl[1], None, expert)] + + mainloop_producer_state.reset_count() + + for _k_tile in range(0, k_tile_cnt, 1, unroll=1): + # acquire also sets the transaction barrier for the A/B buffers + mainloop_pipeline.producer_acquire(mainloop_producer_state) + + tAgA_k = tAgA_mkl[(None, mainloop_producer_state.count)] + tAsA_pipe = tAsA[(None, mainloop_producer_state.index)] + + tBgB_k = tBgB_nkl[(None, mainloop_producer_state.count)] + tBsB_pipe = tBsB[(None, mainloop_producer_state.index)] + + tAgSFA_k = tAgSFA_mkl[(None, mainloop_producer_state.count)] + tAsSFA_pipe = tAsSFA[(None, mainloop_producer_state.index)] + + tBgSFB_k = tBgSFB_nkl[(None, mainloop_producer_state.count)] + tBsSFB_pipe = tBsSFB[(None, mainloop_producer_state.index)] + + cute.copy( + tma_atom_a, + tAgA_k, + tAsA_pipe, + tma_bar_ptr=mainloop_pipeline.producer_get_barrier( + mainloop_producer_state + ), + ) + cute.copy( + tma_atom_b, + tBgB_k, + tBsB_pipe, + tma_bar_ptr=mainloop_pipeline.producer_get_barrier( + mainloop_producer_state + ), + ) + cute.copy( + tma_atom_sfa, + tAgSFA_k, + tAsSFA_pipe, + tma_bar_ptr=mainloop_pipeline.producer_get_barrier( + mainloop_producer_state + ), + ) + cute.copy( + tma_atom_sfb, + tBgSFB_k, + tBsSFB_pipe, + tma_bar_ptr=mainloop_pipeline.producer_get_barrier( + mainloop_producer_state + ), + ) + # Mainloop pipeline's producer commit is a NOP + mainloop_pipeline.producer_commit(mainloop_producer_state) + mainloop_producer_state.advance() + + tile_sched.advance_to_next_work() + work_tile = tile_sched.get_current_work() + + mainloop_pipeline.producer_tail(mainloop_producer_state) + return + + @staticmethod + def _compute_stages( + tile_shape_mnk: tuple[int, int, int], + a_dtype: type[cutlass.Numeric], + b_dtype: type[cutlass.Numeric], + sf_dtype: type[cutlass.Numeric], + sfa_smem_layout: cute.Layout, + sfb_smem_layout: cute.Layout, + epi_tile: tuple[int, int], + c_dtype: type[cutlass.Numeric], + smem_capacity: int, + occupancy: int, + ) -> tuple[int, int]: + """Computes the number of stages for A/B/C operands based on heuristics. + + :param tile_shape_mnk: The shape (M, N, K) of the CTA tile. + :type tile_shape_mnk: tuple[int, int, int] + :param a_dtype: Data type of operand A. + :type a_dtype: type[cutlass.Numeric] + :param b_dtype: Data type of operand B. + :type b_dtype: type[cutlass.Numeric] + :param smem_capacity: Total available shared memory capacity in bytes. + :type smem_capacity: int + :param occupancy: Target number of CTAs per SM (occupancy). + :type occupancy: int + + :return: A tuple containing the computed number of stages for: + (A/B operand stages, epilogue stages) + :rtype: tuple[int, int] + """ + + epi_stage_max = (tile_shape_mnk[1] // epi_tile[1]) * ( + tile_shape_mnk[0] // epi_tile[0] + ) + epi_stage = min(epi_stage_max, 4) + c_bytes_per_stage = cute.size(epi_tile) * c_dtype.width // 8 + epi_bytes = c_bytes_per_stage * epi_stage + + a_shape = cute.slice_(tile_shape_mnk, (None, 0, None)) + b_shape = cute.slice_(tile_shape_mnk, (0, None, None)) + ab_bytes_per_stage = ( + cute.size(a_shape) * a_dtype.width // 8 + + cute.size(b_shape) * b_dtype.width // 8 + ) + sf_bytes_per_stage = ( + cute.size(cute.filter_zeros(sfa_smem_layout).shape) * sf_dtype.width // 8 + + cute.size(cute.filter_zeros(sfb_smem_layout).shape) * sf_dtype.width // 8 + ) + mbar_helpers_bytes = 1024 + + ab_stage = ( + (smem_capacity - occupancy * 1024) // occupancy + - mbar_helpers_bytes + - epi_bytes + ) // (ab_bytes_per_stage + sf_bytes_per_stage) + return ab_stage, epi_stage + + @staticmethod + def _make_smem_layouts( + tile_shape_mnk: tuple[int, int, int], + epi_tile: tuple[int, int], + a_dtype: type[cutlass.Numeric], + a_layout: cute.Layout, + b_dtype: type[cutlass.Numeric], + b_layout: cute.Layout, + ab_stage: int, + c_dtype: type[cutlass.Numeric], + c_layout: cute.Layout, + epi_stage: int, + sf_vec_size: int, + tiled_mma: cute.TiledMma, + ) -> tuple[cute.ComposedLayout, cute.ComposedLayout, cute.ComposedLayout]: + """Create shared memory layouts for A, B, and C tensors. + + :param tile_shape_mnk: CTA tile shape (M,N,K) + :type tile_shape_mnk: Tuple[int, int, int] + :param epi_tile: Epilogue tile shape + :type epi_tile: Tuple[int, int] + :param a_dtype: Data type for matrix A + :type a_dtype: type[cutlass.Numeric] + :param a_layout: Layout for matrix A + :type a_layout: Layout + :param b_dtype: Data type for matrix B + :type b_dtype: type[cutlass.Numeric] + :param b_layout: Layout for matrix B + :type b_layout: Layout + :param ab_stage: Number of stages for A/B tensors + :type ab_stage: int + :param c_dtype: Data type for output matrix C + :type c_dtype: type[cutlass.Numeric] + :param c_layout: leading dimension of the output matrix C + :type c_layout: Layout + :param epi_stage: Number of epilogue stages + :type epi_stage: int + + :return: Tuple of shared memory layouts for A, B, and C + :rtype: Tuple[cute.ComposedLayout, cute.ComposedLayout, cute.ComposedLayout] + """ + a_smem_shape = cute.slice_(tile_shape_mnk, (None, 0, None)) + + a_is_k_major = a_layout.is_k_major_a() + b_is_k_major = b_layout.is_k_major_b() + a_major_mode_size = tile_shape_mnk[2 if a_is_k_major else 0] + + a_smem_layout_atom = cute.nvgpu.warpgroup.make_smem_layout_atom( + sm90_utils.get_smem_layout_atom( + a_layout, + a_dtype, + a_major_mode_size, + ), + a_dtype, + ) + a_smem_layout_staged = cute.tile_to_shape( + a_smem_layout_atom, + cute.append(a_smem_shape, ab_stage), + order=(0, 1, 2) if a_is_k_major else (1, 0, 2), + ) + + b_smem_shape = cute.slice_(tile_shape_mnk, (0, None, None)) + + b_major_mode_size = tile_shape_mnk[2 if b_is_k_major else 1] + b_smem_layout_atom = cute.nvgpu.warpgroup.make_smem_layout_atom( + sm90_utils.get_smem_layout_atom( + b_layout, + b_dtype, + b_major_mode_size, + ), + b_dtype, + ) + b_smem_layout_staged = cute.tile_to_shape( + b_smem_layout_atom, + cute.append(b_smem_shape, ab_stage), + order=(0, 1, 2) if b_is_k_major else (1, 0, 2), + ) + + sfa_smem_layout_staged = blockscaled_utils.sm120_make_smem_layout_sfa( + tiled_mma, + tile_shape_mnk, + sf_vec_size, + ab_stage, + ) + + sfb_smem_layout_staged = blockscaled_utils.sm120_make_smem_layout_sfb( + tiled_mma, + tile_shape_mnk, + sf_vec_size, + ab_stage, + ) + + c_smem_shape = epi_tile + c_major_mode_size = epi_tile[1] if c_layout.is_n_major_c() else epi_tile[0] + c_smem_layout_atom = cute.nvgpu.warpgroup.make_smem_layout_atom( + sm90_utils.get_smem_layout_atom( + c_layout, + c_dtype, + c_major_mode_size, + ), + c_dtype, + ) + epi_smem_layout_staged = cute.tile_to_shape( + c_smem_layout_atom, + cute.append(c_smem_shape, epi_stage), + order=(1, 0, 2) if c_layout.is_m_major_c() else (0, 1, 2), + ) + + return ( + a_smem_layout_staged, + b_smem_layout_staged, + sfa_smem_layout_staged, + sfb_smem_layout_staged, + epi_smem_layout_staged, + ) + + @staticmethod + def _compute_grid( + c: cute.Tensor, + tile_shape_mnk: tuple[int, int, int], + max_active_clusters: cutlass.Constexpr, + ) -> tuple[int, int, int]: + """Compute grid shape for the output tensor C. + + :param c: The output tensor C + :type c: cute.Tensor + :param tile_shape_mnk: The shape (M, N, K) of the CTA tile. + :type tile_shape_mnk: tuple[int, int, int] + + :return: Grid shape for kernel launch. + :rtype: tuple[int, int, int] + """ + + c_shape = cute.slice_(tile_shape_mnk, (None, None, 0)) + gc = cute.zipped_divide(c, tiler=c_shape) + num_ctas_mnl = gc[(0, (None, None, None))].shape + cluster_shape_mnl = (1, 1, 1) + tile_sched_params = utils.PersistentTileSchedulerParams( + num_ctas_mnl, cluster_shape_mnl + ) + grid = utils.StaticPersistentTileScheduler.get_grid_shape( + tile_sched_params, max_active_clusters + ) + return tile_sched_params, grid + + @staticmethod + def _make_tma_store_atoms_and_tensors( + tensor_c: cute.Tensor, + epi_smem_layout_staged: cute.ComposedLayout, + epi_tile: tuple[int, int], + ) -> tuple[cute.CopyAtom, cute.Tensor]: + """Create TMA atoms and tensors for C tensor storage. + + :param tensor_c: Output tensor C + :type tensor_c: cute.Tensor + :param epi_smem_layout_staged: Shared memory layout for epilogue + :type epi_smem_layout_staged: cute.ComposedLayout + :param epi_tile: Epilogue tile shape + :type epi_tile: Tuple[int, int] + + :return: TMA atom and tensor for C + :rtype: Tuple[cute.CopyAtom, cute.Tensor] + """ + epi_smem_layout = cute.slice_(epi_smem_layout_staged, (None, None, 0)) + tma_atom_c, tma_tensor_c = cpasync.make_tiled_tma_atom( + cpasync.CopyBulkTensorTileS2GOp(), + tensor_c, + epi_smem_layout, + epi_tile, + ) + + return tma_atom_c, tma_tensor_c + + @staticmethod + def _make_tma_atoms_and_tensors( + tensor: cute.Tensor, + smem_layout_staged: cute.ComposedLayout, + smem_tile: tuple[int, int], + mcast_dim: int, + internal_type: Optional[Type[cutlass.Numeric]] = None, + ) -> tuple[cute.CopyAtom, cute.Tensor]: + """Create TMA atoms and tensors for input tensors. + + :param tensor: Input tensor (A or B) + :type tensor: cute.Tensor + :param smem_layout_staged: Shared memory layout for the tensor + :type smem_layout_staged: cute.ComposedLayout + :param smem_tile: Shared memory tile shape + :type smem_tile: Tuple[int, int] + :param mcast_dim: Multicast dimension + :type mcast_dim: int + + :return: TMA atom and tensor + :rtype: Tuple[cute.CopyAtom, cute.Tensor] + """ + op = ( + cpasync.CopyBulkTensorTileG2SOp() + if mcast_dim == 1 + else cpasync.CopyBulkTensorTileG2SMulticastOp() + ) + + smem_layout = cute.slice_(smem_layout_staged, (None, None, 0)) + tma_atom, tma_tensor = cpasync.make_tiled_tma_atom( + op, + tensor, + smem_layout, + smem_tile, + num_multicast=mcast_dim, + internal_type=internal_type, + ) + + return tma_atom, tma_tensor + + @staticmethod + def is_valid_tensor_alignment( + m: int, + n: int, + k: int, + l: int, + ab_dtype: Type[cutlass.Numeric], + c_dtype: Type[cutlass.Numeric], + a_major: str, + b_major: str, + c_major: str, + ) -> bool: + """ + Check if the tensor alignment is valid + + :param m: The number of rows in the A tensor + :type m: int + :param n: The number of columns in the B tensor + :type n: int + :param k: The number of columns in the A tensor + :type k: int + :param l: The number of columns in the C tensor + :type l: int + :param ab_dtype: The data type of the A and B operands + :type ab_dtype: Type[cutlass.Numeric] + :param c_dtype: The data type of the output tensor + :type c_dtype: Type[cutlass.Numeric] + :param a_major: The major axis of the A tensor + :type a_major: str + :param b_major: The major axis of the B tensor + :type b_major: str + :param c_major: The major axis of the C tensor + :type c_major: str + + :return: True if the problem shape is valid, False otherwise + :rtype: bool + """ + is_valid = True + + def check_contigous_16B_alignment(dtype, is_mode0_major, tensor_shape): + major_mode_idx = 0 if is_mode0_major else 1 + num_major_elements = tensor_shape[major_mode_idx] + num_contiguous_elements = 16 * 8 // dtype.width + return num_major_elements % num_contiguous_elements == 0 + + if ( + not check_contigous_16B_alignment(ab_dtype, a_major == "m", (m, k, l)) + or not check_contigous_16B_alignment(ab_dtype, b_major == "n", (n, k, l)) + or not check_contigous_16B_alignment(c_dtype, c_major == "m", (m, n, l)) + ): + is_valid = False + return is_valid + + +@cute.jit +def cvt_sf_MKL_to_M32x4xrm_K4xrk_L( + sf_ref_tensor: cute.Tensor, + sf_mma_tensor: cute.Tensor, +): + """Convert scale factor tensor from MKL layout to mma specification M(32x4xrest_m)xK(4xrest_k)xL layout""" + # sf_mma_tensor has flatten shape (32, 4, rest_m, 4, rest_k, l) + # group to ((32, 4, rest_m), (4, rest_k), l) + sf_mma_tensor = cute.group_modes(sf_mma_tensor, 0, 3) + sf_mma_tensor = cute.group_modes(sf_mma_tensor, 1, 3) + for i in cutlass.range(cute.size(sf_ref_tensor)): + mkl_coord = sf_ref_tensor.layout.get_hier_coord(i) + sf_mma_tensor[mkl_coord] = sf_ref_tensor[mkl_coord] + + +def run( + mnkl: Tuple[int, int, int, int], + a_dtype: Type[cutlass.Numeric], + b_dtype: Type[cutlass.Numeric], + sf_dtype: Type[cutlass.Numeric], + sf_vec_size: int, + c_dtype: Type[cutlass.Numeric], + a_major: str = "k", + b_major: str = "k", + c_major: str = "n", + tile_shape_mnk: Tuple[int, int, int] = (128, 128, 128), + epi_tile: Tuple[int, int] = (128, 128), + tolerance: float = 1e-01, + warmup_iterations: int = 0, + iterations: int = 1, + skip_ref_check: bool = False, + use_cold_l2: bool = False, + **kwargs, +): + """Perf-framework-compatible entry point. + + FP4 MMA only supports Float32 accumulation, so acc_dtype is always + Float32 regardless of what the caller passes. + """ + return run_bs( + mnkl=mnkl, + a_dtype=a_dtype, + b_dtype=b_dtype, + sf_dtype=sf_dtype, + sf_vec_size=sf_vec_size, + c_dtype=c_dtype, + acc_dtype=cutlass.Float32, + a_major=a_major, + b_major=b_major, + c_major=c_major, + tile_shape_mnk=tile_shape_mnk, + epi_tile=epi_tile, + tolerance=tolerance, + warmup_iterations=warmup_iterations, + iterations=iterations, + skip_ref_check=skip_ref_check, + use_cold_l2=use_cold_l2, + ) + + +def run_bs( + mnkl: Tuple[int, int, int, int], + a_dtype: Type[cutlass.Numeric], + b_dtype: Type[cutlass.Numeric], + sf_dtype: Type[cutlass.Numeric], + sf_vec_size: int, + c_dtype: Type[cutlass.Numeric], + acc_dtype: Type[cutlass.Numeric], + a_major: str, + b_major: str, + c_major: str, + tile_shape_mnk: Tuple[int, int, int], + epi_tile: Tuple[int, int], + tolerance: float, + warmup_iterations: int, + iterations: int, + skip_ref_check: bool, + use_cold_l2: bool = False, + **kwargs, +): + import cutlass.torch as cutlass_torch + import torch + + print("Running Blackwell Geforce Blockscaled Dense GEMM with:") + print(f"mnkl: {mnkl}") + print( + f"A dtype: {a_dtype}, B dtype: {b_dtype}, A/B scale factor dtype: {sf_dtype}, C dtype: {c_dtype}, Acc dtype: {acc_dtype}" + ) + print(f"Matrix majors - A: {a_major}, B: {b_major}, C: {c_major}") + print(f"Tile Shape: {tile_shape_mnk}") + print(f"Epilogue tile: {epi_tile}") + print(f"Tolerance: {tolerance}") + print(f"Warmup iterations: {warmup_iterations}") + print(f"Iterations: {iterations}") + print(f"Skip reference checking: {skip_ref_check}") + print(f"Use cold L2: {use_cold_l2}") + + m, n, k, l = mnkl + + if not Sm120BlockScaledGemmKernel.is_valid_tensor_alignment( + m, n, k, l, a_dtype, c_dtype, a_major, b_major, c_major + ): + raise ValueError("Invalid tensor alignment") + + a_dtype = getattr(cutlass, a_dtype) if isinstance(a_dtype, str) else a_dtype + b_dtype = getattr(cutlass, b_dtype) if isinstance(b_dtype, str) else b_dtype + c_dtype = getattr(cutlass, c_dtype) if isinstance(c_dtype, str) else c_dtype + acc_dtype = getattr(cutlass, acc_dtype) if isinstance(acc_dtype, str) else acc_dtype + + m, n, k, l = mnkl + cluster_shape_mnk = (1, 1, 1) + + if not torch.cuda.is_available(): + raise RuntimeError("GPU is required to run this example!") + + a_ref = cutlass_torch.matrix(l, m, k, a_major == "m", cutlass.Float32) + b_ref = cutlass_torch.matrix(l, n, k, b_major == "n", cutlass.Float32) + c_ref = cutlass_torch.matrix(l, m, n, c_major == "m", cutlass.Float32) + + a_tensor, a_torch = cutlass_torch.cute_tensor_like( + a_ref, a_dtype, is_dynamic_layout=True, assumed_align=16 + ) + b_tensor, b_torch = cutlass_torch.cute_tensor_like( + b_ref, b_dtype, is_dynamic_layout=True, assumed_align=16 + ) + c_tensor, c_torch = cutlass_torch.cute_tensor_like( + c_ref, c_dtype, is_dynamic_layout=True, assumed_align=16 + ) + + a_tensor.mark_compact_shape_dynamic( + mode=1 if a_major == "k" else 0, + stride_order=(2, 0, 1) if a_major == "k" else (2, 1, 0), + divisibility=2 if a_dtype == cutlass.Float4E2M1FN else 1, + ) + b_tensor.mark_compact_shape_dynamic( + mode=1 if b_major == "k" else 0, + stride_order=(2, 0, 1) if b_major == "k" else (2, 1, 0), + divisibility=2 if b_dtype == cutlass.Float4E2M1FN else 1, + ) + c_tensor.mark_compact_shape_dynamic( + mode=1 if c_major == "n" else 0, + stride_order=(2, 0, 1) if c_major == "n" else (2, 1, 0), + divisibility=2 if c_dtype == cutlass.Float4E2M1FN else 1, + ) + + def create_scale_factor_tensor(l, mn, k, sf_vec_size, dtype): + def ceil_div(a, b): + return (a + b - 1) // b + + sf_k = ceil_div(k, sf_vec_size) + ref_shape = (l, mn, sf_k) + + # The A, C, and D matrices are row-major whereas the B matrix is column-major. + # So only k-major (A/B) is supported. + + atom_m = (32, 4) + atom_k = 4 + mma_shape = ( + l, + ceil_div(mn, atom_m[0] * atom_m[1]), + ceil_div(sf_k, atom_k), + atom_m[0], + atom_m[1], + atom_k, + ) + + ref_permute_order = (1, 2, 0) + mma_permute_order = (3, 4, 1, 5, 2, 0) + + ref_f32_torch_tensor_cpu = cutlass_torch.create_and_permute_torch_tensor( + ref_shape, + torch.float32, + permute_order=ref_permute_order, + init_type=cutlass_torch.TensorInitType.RANDOM, + init_config=cutlass_torch.RandomInitConfig( + min_val=1, + max_val=3, + ), + ) + + cute_f32_torch_tensor_cpu = cutlass_torch.create_and_permute_torch_tensor( + mma_shape, + torch.float32, + permute_order=mma_permute_order, + init_type=cutlass_torch.TensorInitType.RANDOM, + init_config=cutlass_torch.RandomInitConfig( + min_val=0, + max_val=1, + ), + ) + + cvt_sf_MKL_to_M32x4xrm_K4xrk_L( + from_dlpack(ref_f32_torch_tensor_cpu), + from_dlpack(cute_f32_torch_tensor_cpu), + ) + cute_f32_torch_tensor = cute_f32_torch_tensor_cpu.cuda() + + # reshape makes memory contiguous + ref_f32_torch_tensor_cpu = ( + ref_f32_torch_tensor_cpu.permute(2, 0, 1) + .unsqueeze(-1) + .expand(l, mn, sf_k, sf_vec_size) + .reshape(l, mn, sf_k * sf_vec_size) + .permute(*ref_permute_order) + ) + # prune to mkl for reference check. + ref_f32_torch_tensor_cpu = ref_f32_torch_tensor_cpu[:, :k, :] + + cute_tensor, torch_tensor = cutlass_torch.cute_tensor_like( + cute_f32_torch_tensor_cpu, + dtype, + is_dynamic_layout=True, + assumed_align=16, + ) + + cute_tensor = cutlass_torch.convert_cute_tensor( + cute_f32_torch_tensor, + cute_tensor, + dtype, + is_dynamic_layout=True, + ) + + return ref_f32_torch_tensor_cpu, cute_tensor, torch_tensor + + sfa_ref, sfa_tensor, sfa_torch = create_scale_factor_tensor( + l, m, k, sf_vec_size, sf_dtype + ) + + sfb_ref, sfb_tensor, sfb_torch = create_scale_factor_tensor( + l, n, k, sf_vec_size, sf_dtype + ) + + gemm = Sm120BlockScaledGemmKernel( + acc_dtype, + sf_vec_size, + tile_shape_mnk, + epi_tile, + ) + + hardware_info = cutlass.utils.HardwareInfo() + max_active_clusters = hardware_info.get_max_active_clusters( + cluster_shape_mnk[0] * cluster_shape_mnk[1] + ) + + stream = cutlass_torch.default_stream() + + compiled_gemm = cute.compile( + gemm, + a_tensor, + b_tensor, + sfa_tensor, + sfb_tensor, + c_tensor, + max_active_clusters, + stream, + ) + + if not skip_ref_check: + print("Reference checking ...") + compiled_gemm(a_tensor, b_tensor, sfa_tensor, sfb_tensor, c_tensor, stream) + torch.cuda.synchronize() + + res_a = torch.einsum("mkl,mkl->mkl", a_ref, sfa_ref) + res_b = torch.einsum("nkl,nkl->nkl", b_ref, sfb_ref) + ref = torch.einsum("mkl,nkl->mnl", res_a, res_b) + + c_ref_device = c_ref.cuda() + cute.testing.convert( + c_tensor, + from_dlpack(c_ref_device, assumed_align=16).mark_layout_dynamic( + leading_dim=(1 if c_major == "n" else 0) + ), + ) + c_ref = c_ref_device.cpu() + + if c_dtype in (cutlass.Float32, cutlass.Float16, cutlass.BFloat16): + torch.testing.assert_close(c_ref, ref, atol=tolerance, rtol=1e-02) + elif c_dtype in (cutlass.Float8E5M2, cutlass.Float8E4M3FN): + # Convert ref : f32 -> f8 -> f32 + ref_f8_ = torch.empty(*(l, m, n), dtype=torch.uint8, device="cuda").permute( + 1, 2, 0 + ) + ref_f8 = from_dlpack(ref_f8_, assumed_align=16).mark_layout_dynamic( + leading_dim=1 + ) + ref_f8.element_type = c_dtype + ref_device = ref.permute(2, 0, 1).contiguous().permute(1, 2, 0).cuda() + ref_tensor = from_dlpack(ref_device, assumed_align=16).mark_layout_dynamic( + leading_dim=1 + ) + cute.testing.convert(ref_tensor, ref_f8) + cute.testing.convert(ref_f8, ref_tensor) + ref = ref_device.cpu() + torch.testing.assert_close(c_ref, ref, atol=tolerance, rtol=1e-02) + elif c_dtype is cutlass.Float4E2M1FN: + # Convert ref : f32 -> f4 -> f32 + ref_f4_ = torch.empty(*(l, m, n), dtype=torch.uint8, device="cuda").permute( + 1, 2, 0 + ) + ref_f4 = from_dlpack(ref_f4_, assumed_align=16).mark_layout_dynamic( + leading_dim=1 + ) + ref_f4.element_type = c_dtype + ref_device = ref.permute(2, 0, 1).contiguous().permute(1, 2, 0).cuda() + ref_tensor = from_dlpack(ref_device, assumed_align=16).mark_layout_dynamic( + leading_dim=1 + ) + cute.testing.convert(ref_tensor, ref_f4) + cute.testing.convert(ref_f4, ref_tensor) + ref = ref_device.cpu() + torch.testing.assert_close(c_ref, ref, atol=tolerance, rtol=1e-02) + + def generate_tensors(): + a_tensor, _ = cutlass_torch.cute_tensor_like( + a_ref, a_dtype, is_dynamic_layout=True, assumed_align=16 + ) + b_tensor, _ = cutlass_torch.cute_tensor_like( + b_ref, b_dtype, is_dynamic_layout=True, assumed_align=16 + ) + c_tensor, _ = cutlass_torch.cute_tensor_like( + c_ref, c_dtype, is_dynamic_layout=True, assumed_align=16 + ) + a_tensor.mark_compact_shape_dynamic( + mode=1 if a_major == "k" else 0, + stride_order=(2, 0, 1) if a_major == "k" else (2, 1, 0), + divisibility=2 if a_dtype == cutlass.Float4E2M1FN else 1, + ) + b_tensor.mark_compact_shape_dynamic( + mode=1 if b_major == "k" else 0, + stride_order=(2, 0, 1) if b_major == "k" else (2, 1, 0), + divisibility=2 if b_dtype == cutlass.Float4E2M1FN else 1, + ) + c_tensor.mark_compact_shape_dynamic( + mode=1 if c_major == "n" else 0, + stride_order=(2, 0, 1) if c_major == "n" else (2, 1, 0), + divisibility=2 if c_dtype == cutlass.Float4E2M1FN else 1, + ) + + _, sfa_tensor, _ = create_scale_factor_tensor(l, m, k, sf_vec_size, sf_dtype) + _, sfb_tensor, _ = create_scale_factor_tensor(l, n, k, sf_vec_size, sf_dtype) + return cute.testing.JitArguments( + a_tensor, b_tensor, sfa_tensor, sfb_tensor, c_tensor, stream + ) + + workspace_count = 1 + if use_cold_l2: + one_workspace_bytes = ( + a_ref.numel() * a_ref.element_size() + + b_ref.numel() * b_ref.element_size() + + sfa_ref.numel() * sfa_ref.element_size() + + sfb_ref.numel() * sfb_ref.element_size() + + c_ref.numel() * c_ref.element_size() + ) + workspace_count = testing.get_workspace_count( + one_workspace_bytes, warmup_iterations, iterations + ) + + exec_time = testing.benchmark( + compiled_gemm, + workspace_generator=generate_tensors, + workspace_count=workspace_count, + stream=stream, + warmup_iterations=warmup_iterations, + iterations=iterations, + ) + + gflop = 2 * m * n * k / 1e9 + gflops = gflop / exec_time * 1e6 + + print(f"Execution time: {exec_time} microseconds per iteration") + print(f"GFLOPS: {gflops}") + + return exec_time # Return execution time in microseconds + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Example of MxNxKxL GEMM on Blackwell Geforce." + ) + + parser.add_argument( + "--mnkl", + type=parse_comma_separated_ints, + default=( + 1024, + 1024, + 1024, + 1, + ), + help="mnkl dimensions (comma-separated)", + ) + parser.add_argument( + "--tile_shape_mnk", + type=parse_comma_separated_ints, + choices=[ + (128, 128, 128), + (128, 128, 256), + ], + default=(128, 128, 128), + help="CTA tile shape (comma-separated)", + ) + parser.add_argument( + "--epi_tile", + type=parse_comma_separated_ints, + choices=[ + (128, 128), + (64, 32), + ], + default=(128, 128), + help="Epilogue tile shape (comma-separated)", + ) + parser.add_argument( + "--a_dtype", + type=cutlass.dtype, + default=cutlass.Float4E2M1FN, + ) + parser.add_argument( + "--b_dtype", + type=cutlass.dtype, + default=cutlass.Float4E2M1FN, + ) + parser.add_argument( + "--sf_dtype", + type=cutlass.dtype, + default=cutlass.Float8E4M3FN, + ) + parser.add_argument( + "--sf_vec_size", + type=int, + choices=[16, 32], # 16 for NVFP4, 32 for MXFP4 / MXFP8. + default=16, + ) + parser.add_argument( + "--c_dtype", + type=cutlass.dtype, + default=cutlass.Float16, + ) + parser.add_argument( + "--acc_dtype", + type=cutlass.dtype, + default=cutlass.Float32, + ) + parser.add_argument("--a_major", choices=["k"], type=str, default="k") + parser.add_argument("--b_major", choices=["k"], type=str, default="k") + parser.add_argument("--c_major", choices=["n"], type=str, default="n") + parser.add_argument( + "--tolerance", type=float, default=1e-01, help="Tolerance for validation" + ) + parser.add_argument( + "--warmup_iterations", type=int, default=0, help="Warmup iterations" + ) + parser.add_argument( + "--iterations", + type=int, + default=1, + help="Number of iterations to run the kernel", + ) + parser.add_argument( + "--skip_ref_check", + action="store_true", + default=False, + help="Skip reference checking", + ) + parser.add_argument( + "--use_cold_l2", + action="store_true", + default=False, + help="Use circular buffer tensor sets to ensure L2 cold cache", + ) + + args = parser.parse_args() + + if len(args.mnkl) != 4: + parser.error("--mnkl must contain exactly 4 values") + + fp4_allowed_tiles = {(128, 128, 128), (128, 128, 256)} + fp8_allowed_tiles = {(128, 128, 128)} + try: + validate_blockscaled_args(args, fp4_allowed_tiles, fp8_allowed_tiles) + except ValueError as e: + parser.error(str(e)) + + return args + + +if __name__ == "__main__": + args = parse_arguments() + run_bs( + args.mnkl, + args.a_dtype, + args.b_dtype, + args.sf_dtype, + args.sf_vec_size, + args.c_dtype, + args.acc_dtype, + args.a_major, + args.b_major, + args.c_major, + args.tile_shape_mnk, + args.epi_tile, + args.tolerance, + args.warmup_iterations, + args.iterations, + args.skip_ref_check, + args.use_cold_l2, + ) + print("PASS") diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/quant_mxfp8.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/quant_mxfp8.py new file mode 100644 index 0000000000..122991c4a6 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/quant_mxfp8.py @@ -0,0 +1,52 @@ +"""Fast Triton mxfp8 quantizer for activations (fp8 mode): bf16 [M,K] -> qdata e4m3 [M,K] + +e8m0 block scale [M,K/32]. Matches torchao MXTensor.to_mx(x, float8_e4m3fn, 32, FLOOR).""" + +import torch +import triton +import triton.language as tl + +E4M3_MAX = 448.0 + + +@triton.jit +def _qk(X, Q, S, sx0, sx1, sq0, sq1, ss0, ss1, BPM: tl.constexpr): + m = tl.program_id(0) + b0 = tl.program_id(1) * BPM + blk = tl.arange(0, BPM) + e = tl.arange(0, 32) + k = (b0 + blk)[:, None] * 32 + e[None, :] + x = tl.load(X + m * sx0 + k * sx1).to(tl.float32) + amax = tl.max(tl.abs(x), axis=1) + # FLOOR e8m0 scale: 2^(floor(log2(amax)) - floor(log2(448))), floor(log2(448))=8 + exp = tl.floor(tl.log2(tl.where(amax > 0, amax, 1.0))) - 8.0 + sc = tl.exp2(exp) + scb = sc[:, None] + q = tl.clamp(x / scb, -448.0, 448.0).to(tl.float8e4nv) + tl.store(Q + m * sq0 + k * sq1, q) + # e8m0 stores the biased exponent byte (bias 127); scale = 2^(E-127) + Ebiased = (exp + 127.0).to(tl.int32) + tl.store(S + m * ss0 + (b0 + blk) * ss1, Ebiased.to(tl.uint8)) + + +def mxfp8_quant(x, bpm=4): + M, K = x.shape + # K must be a whole number of 32-wide MX blocks, else nblk drops the tail and the kernel OOBs. + assert K % 32 == 0, f"mxfp8_quant expects K divisible by 32, got K={K}" + nblk = K // 32 + Q = torch.empty(M, K, device=x.device, dtype=torch.float8_e4m3fn) + S = torch.empty( + M, nblk, device=x.device, dtype=torch.uint8 + ) # e8m0 biased-exponent bytes + _qk[(M, triton.cdiv(nblk, bpm))]( + x, + Q, + S, + x.stride(0), + x.stride(1), + Q.stride(0), + Q.stride(1), + S.stride(0), + S.stride(1), + BPM=bpm, + ) + return Q, S.view(torch.float8_e8m0fnu) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/quant_nvfp4.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/quant_nvfp4.py new file mode 100644 index 0000000000..d2a5f5d3f7 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/quant_nvfp4.py @@ -0,0 +1,69 @@ +"""Fast Triton nvfp4 quantizer: bf16 [M,K] -> qdata u8 [M,K/2] (2 fp4/byte, low-first) + +e4m3 block scale [M,K/16]. Same qdata/scale layout as torchao (drop-in for the grouped FP4 GEMM), +but an approximate fast encode: NOT byte-identical to torchao's to_nvfp4 (arithmetic-encode cascade ++ 1e-30 scale floor here vs torchao's RNE + 2**-6 e4m3 floor).""" + +import torch +import triton +import triton.language as tl + + +@triton.jit +def _qk(X, Q, S, sx0, sx1, sq0, sq1, ss0, ss1, BPM: tl.constexpr): + m = tl.program_id(0) + b0 = tl.program_id(1) * BPM + blk = tl.arange(0, BPM) + e = tl.arange(0, 8) # 8 bytes per 16-wide block + # even (lo) and odd (hi) fp4 positions interleaved within each byte + k_lo = (b0 + blk)[:, None] * 16 + 2 * e[None, :] + k_hi = k_lo + 1 + lo = tl.load(X + m * sx0 + k_lo * sx1).to(tl.float32) + hi = tl.load(X + m * sx0 + k_hi * sx1).to(tl.float32) + amax = tl.maximum(tl.max(tl.abs(lo), axis=1), tl.max(tl.abs(hi), axis=1)) + sc1d = tl.clamp(amax / 6.0, 1e-30, 1e30).to(tl.float8e4nv).to(tl.float32) + sc1d = tl.where(sc1d > 0, sc1d, 1.0) + sc = sc1d[:, None] + alo = tl.abs(lo) / sc + clo = ( + tl.where(alo > 0.25, 1, 0) + + tl.where(alo > 0.75, 1, 0) + + tl.where(alo > 1.25, 1, 0) + + tl.where(alo > 1.75, 1, 0) + + tl.where(alo > 2.5, 1, 0) + + tl.where(alo > 3.5, 1, 0) + + tl.where(alo > 5.0, 1, 0) + ).to(tl.int32) + tl.where(lo < 0, 8, 0).to(tl.int32) + ahi = tl.abs(hi) / sc + chi = ( + tl.where(ahi > 0.25, 1, 0) + + tl.where(ahi > 0.75, 1, 0) + + tl.where(ahi > 1.25, 1, 0) + + tl.where(ahi > 1.75, 1, 0) + + tl.where(ahi > 2.5, 1, 0) + + tl.where(ahi > 3.5, 1, 0) + + tl.where(ahi > 5.0, 1, 0) + ).to(tl.int32) + tl.where(hi < 0, 8, 0).to(tl.int32) + byte = (clo + chi * 16).to(tl.uint8) + qpos = (b0 + blk)[:, None] * 8 + e[None, :] + tl.store(Q + m * sq0 + qpos * sq1, byte) + tl.store(S + m * ss0 + (b0 + blk) * ss1, sc1d.to(tl.float8e4nv)) + + +def nvfp4_quant(x, bpm=8): + M, K = x.shape + nblk = K // 16 + Q = torch.empty(M, K // 2, device=x.device, dtype=torch.uint8) + S = torch.empty(M, nblk, device=x.device, dtype=torch.float8_e4m3fn) + _qk[(M, triton.cdiv(nblk, bpm))]( + x, + Q, + S, + x.stride(0), + x.stride(1), + Q.stride(0), + Q.stride(1), + S.stride(0), + S.stride(1), + BPM=bpm, + ) + return Q, S diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/swiglu.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/swiglu.py new file mode 100644 index 0000000000..daabfc6e3a --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/cutlass_fp4/swiglu.py @@ -0,0 +1,148 @@ +"""Fused gated-activation forward + backward Triton kernels (memory-lean, no [Mt,I] intermediates). + +Two activation variants: + silu (default, DSV4): fwd = silu(clamp(g,max=L)) * clamp(u,-L,L) + gelu_tanh (Gemma4): fwd = gelu_pytorch_tanh(g) * u (no clamp) + +bwd: (gu[Mt,2I], dh[Mt,I]) -> dgu[Mt,2I] in ONE pass (~8 intermediates become register-local). +""" + +import torch +import triton +import triton.language as tl +from triton.language.extra.cuda import libdevice + + +@triton.jit +def _fwd(GU, H, I, L, sg0, sg1, sh0, sh1, BLK: tl.constexpr): + r = tl.program_id(0) + c = tl.program_id(1) * BLK + tl.arange(0, BLK) + m = c < I + g = tl.load(GU + r * sg0 + c * sg1, mask=m, other=0.0).to(tl.float32) + u = tl.load(GU + r * sg0 + (I + c) * sg1, mask=m, other=0.0).to(tl.float32) + gc = tl.minimum(g, L) + uc = tl.minimum(tl.maximum(u, -L), L) + h = (gc * tl.sigmoid(gc)) * uc + tl.store(H + r * sh0 + c * sh1, h.to(H.dtype.element_ty), mask=m) + + +@triton.jit +def _bwd(GU, DH, DGU, I, L, sg0, sg1, sd0, sd1, so0, so1, BLK: tl.constexpr): + r = tl.program_id(0) + c = tl.program_id(1) * BLK + tl.arange(0, BLK) + m = c < I + g = tl.load(GU + r * sg0 + c * sg1, mask=m, other=0.0).to(tl.float32) + u = tl.load(GU + r * sg0 + (I + c) * sg1, mask=m, other=0.0).to(tl.float32) + dh = tl.load(DH + r * sd0 + c * sd1, mask=m, other=0.0).to(tl.float32) + gc = tl.minimum(g, L) + sg = tl.sigmoid(gc) + silu = gc * sg + dsilu = sg * (1.0 + gc * (1.0 - sg)) + uc = tl.minimum(tl.maximum(u, -L), L) + dg = dh * uc * dsilu * (g <= L) + du = dh * silu * ((u >= -L) & (u <= L)) + tl.store(DGU + r * so0 + c * so1, dg.to(DGU.dtype.element_ty), mask=m) + tl.store(DGU + r * so0 + (I + c) * so1, du.to(DGU.dtype.element_ty), mask=m) + + +# gelu_tanh(x) = 0.5 * x * (1 + tanh(k * (x + 0.044715 * x^3))), k = sqrt(2/pi) +# gelu_tanh'(x) = 0.5*(1+t) + 0.5*x*(1-t^2)*k*(1 + 3*0.044715*x^2) + + +@triton.jit +def _fwd_gelu(GU, H, I, sg0, sg1, sh0, sh1, BLK: tl.constexpr): + r = tl.program_id(0) + c = tl.program_id(1) * BLK + tl.arange(0, BLK) + m = c < I + g = tl.load(GU + r * sg0 + c * sg1, mask=m, other=0.0).to(tl.float32) + u = tl.load(GU + r * sg0 + (I + c) * sg1, mask=m, other=0.0).to(tl.float32) + k = 0.7978845608028654 # sqrt(2/pi) + inner = k * (g + 0.044715 * g * g * g) + t = libdevice.tanh(inner) + gelu_g = 0.5 * g * (1.0 + t) + h = gelu_g * u + tl.store(H + r * sh0 + c * sh1, h.to(H.dtype.element_ty), mask=m) + + +@triton.jit +def _bwd_gelu(GU, DH, DGU, I, sg0, sg1, sd0, sd1, so0, so1, BLK: tl.constexpr): + r = tl.program_id(0) + c = tl.program_id(1) * BLK + tl.arange(0, BLK) + m = c < I + g = tl.load(GU + r * sg0 + c * sg1, mask=m, other=0.0).to(tl.float32) + u = tl.load(GU + r * sg0 + (I + c) * sg1, mask=m, other=0.0).to(tl.float32) + dh = tl.load(DH + r * sd0 + c * sd1, mask=m, other=0.0).to(tl.float32) + k = 0.7978845608028654 + g2 = g * g + inner = k * (g + 0.044715 * g2 * g) + t = libdevice.tanh(inner) + gelu_g = 0.5 * g * (1.0 + t) + # gelu'(g) = 0.5*(1+t) + 0.5*g*(1-t^2)*k*(1 + 3*0.044715*g^2) + dgelu = 0.5 * (1.0 + t) + 0.5 * g * (1.0 - t * t) * k * (1.0 + 3.0 * 0.044715 * g2) + dg = dh * u * dgelu + du = dh * gelu_g + tl.store(DGU + r * so0 + c * so1, dg.to(DGU.dtype.element_ty), mask=m) + tl.store(DGU + r * so0 + (I + c) * so1, du.to(DGU.dtype.element_ty), mask=m) + + +def swiglu_fwd(gu, limit, act_type="silu", bpm=512): + """Gated-activation forward: gu[Mt,2I] -> h[Mt,I]. + act_type='silu': clamped SwiGLU (DSV4). act_type='gelu_tanh': GeGLU (Gemma4, limit ignored).""" + Mt, twoI = gu.shape + I = twoI // 2 + h = torch.empty(Mt, I, device=gu.device, dtype=gu.dtype) + if act_type == "gelu_tanh": + _fwd_gelu[(Mt, triton.cdiv(I, bpm))]( + gu, h, I, gu.stride(0), gu.stride(1), h.stride(0), h.stride(1), BLK=bpm + ) + else: + _fwd[(Mt, triton.cdiv(I, bpm))]( + gu, + h, + I, + float(limit), + gu.stride(0), + gu.stride(1), + h.stride(0), + h.stride(1), + BLK=bpm, + ) + return h + + +def swiglu_bwd(gu, dh, limit, act_type="silu", bpm=512): + """Gated-activation backward: (gu[Mt,2I], dh[Mt,I]) -> dgu[Mt,2I]. + act_type='silu': clamped SwiGLU (DSV4). act_type='gelu_tanh': GeGLU (Gemma4, limit ignored).""" + Mt, twoI = gu.shape + I = twoI // 2 + dgu = torch.empty(Mt, twoI, device=gu.device, dtype=gu.dtype) + if act_type == "gelu_tanh": + _bwd_gelu[(Mt, triton.cdiv(I, bpm))]( + gu, + dh, + dgu, + I, + gu.stride(0), + gu.stride(1), + dh.stride(0), + dh.stride(1), + dgu.stride(0), + dgu.stride(1), + BLK=bpm, + ) + else: + _bwd[(Mt, triton.cdiv(I, bpm))]( + gu, + dh, + dgu, + I, + float(limit), + gu.stride(0), + gu.stride(1), + dh.stride(0), + dh.stride(1), + dgu.stride(0), + dgu.stride(1), + BLK=bpm, + ) + return dgu diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/dequant_grouped.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/dequant_grouped.py new file mode 100644 index 0000000000..75afa0d5fa --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/dequant_grouped.py @@ -0,0 +1,462 @@ +"""Dequant + grouped-GEMM MoE experts path — the fast alternative to the fully-fused +``scatter2scatter_lora_mx`` Triton kernel. + +Benchmarks (DSV4-Flash expert shapes) showed the fused in-kernel-decode Triton kernel is +~4x slower than dequantizing NVFP4->bf16 and running a cuBLAS-grade grouped GEMM, because a +hand-written Triton MoE GEMM can't match cuBLAS/tensor-core throughput. This module provides: + + * primary (SM90/SM100 + DeepGEMM): native fused-decode fp4 grouped GEMM (no bf16 weight + materialization, tensor-core speed) — ``deepgemm_grouped_available()`` gates it; + * fallback (any other GPU, e.g. SM120): a fast custom-Triton NVFP4->bf16 dequant (~35x + faster than torchao's eager ``.dequantize()``) + ``torch._grouped_mm``, tiled per + expert-chunk + checkpointed so the bf16 weight transient stays bounded. + +``torch._grouped_mm`` has no working autograd (stride bug), so ``_GroupedMM`` supplies the +backward manually (``dX = grouped_mm(grad, Wᵀ)``; experts are frozen → no weight grad). + +STATUS: the chunked dequant+grouped BASE path is implemented + parity-validated here. Wiring +into ``parallel_linear_lora`` (LoRA-on-experts + the DeepGEMM primary call) is the remaining +integration step. +""" + +from __future__ import annotations + +import torch +import triton +import triton.language as tl +from torch.utils.checkpoint import checkpoint + +from .mx_weights import fp4_codebook + +_DG = None + + +def _dg(): + """Cached DeepGEMM kernel module. ``get_kernel`` is not idempotent — calling it twice + re-runs the build's ``register_fake`` and raises, so resolve it once and reuse.""" + global _DG + if _DG is None: + from kernels import get_kernel + + _DG = get_kernel("kernels-community/deep-gemm") + return _DG + + +def deepgemm_grouped_available() -> bool: + """True iff DeepGEMM's grouped fp8xfp4 MoE kernel can run here. + + SM100 ONLY (Blackwell datacenter, e.g. B200). The kernel symbol resolves on SM90 (Hopper) too, + but ``m_grouped_fp8_fp4_gemm_nt_contiguous`` asserts ``arch_major == 10`` at call time, so SM90 + must report unavailable and fall back to Marlin (which supports SM80-90,120). Gating on the + capability up front avoids a hard CUDA assert mid-forward.""" + if not torch.cuda.is_available(): + return False + major, _minor = torch.cuda.get_device_capability() + if major != 10: # fp8xfp4 grouped kernel is sm100-only + return False + try: + return getattr(_dg(), "m_grouped_fp8_fp4_gemm_nt_contiguous", None) is not None + except Exception: + return False + + +@triton.jit +def _nvfp4_deq_kernel( + Qp, Sc, Pt, Out, Kd, ROWS_PER_E, CB, sq0, sq1, ss0, ss1, so0, so1, BK: tl.constexpr +): + rid = tl.program_id(0).to(tl.int64) # c*nrows*stride overflows int32 at E=256 + kk = tl.program_id(1) * BK + tl.arange(0, BK) + km = kk < Kd + pt = tl.load(Pt + rid // ROWS_PER_E).to(tl.float32) + packed = tl.load(Qp + rid * sq0 + (kk // 2) * sq1, mask=km, other=0).to(tl.int32) + nib = tl.where((kk % 2) == 1, (packed >> 4) & 0xF, packed & 0xF) + sc = tl.load(Sc + rid * ss0 + (kk // 16) * ss1, mask=km, other=0.0).to(tl.float32) + tl.store( + Out + rid * so0 + kk * so1, + (tl.load(CB + nib) * sc * pt).to(tl.bfloat16), + mask=km, + ) + + +def nvfp4_dequant_bf16( + qdata: torch.Tensor, scale: torch.Tensor, per_tensor: torch.Tensor +) -> torch.Tensor: + """NVFP4 -> bf16 via a memory-bound Triton kernel (~35x faster than torchao eager). + ``qdata`` [c,N,K/2] uint8, ``scale`` [c,N,K/16] e4m3, ``per_tensor`` [c] fp32 → [c,N,K] bf16.""" + c, nrows, kh = qdata.shape + kd = kh * 2 + out = torch.empty(c, nrows, kd, device=qdata.device, dtype=torch.bfloat16) + q2, s2, o2 = ( + qdata.reshape(c * nrows, kh), + scale.reshape(c * nrows, kd // 16), + out.reshape(c * nrows, kd), + ) + BK = 1024 # larger blocks lift HBM utilization (memory-bound kernel) + _nvfp4_deq_kernel[(c * nrows, triton.cdiv(kd, BK))]( + q2, + s2, + per_tensor.contiguous(), + o2, + kd, + nrows, + fp4_codebook(qdata.device), + q2.stride(0), + q2.stride(1), + s2.stride(0), + s2.stride(1), + o2.stride(0), + o2.stride(1), + BK=BK, + ) + return out + + +@triton.jit +def _fp8_e8m0_q_kernel(X, O, SF, sx0, sx1, so0, so1, ss0, ss1, G: tl.constexpr): + row = tl.program_id(0) + kb = tl.program_id(1) + offs = kb * G + tl.arange(0, G) + x = tl.load(X + row * sx0 + offs * sx1).to(tl.float32) + amax = tl.maximum(tl.max(tl.abs(x)), 1e-4) + sf = tl.exp2( + tl.ceil(tl.log2(amax / 448.0)) + ) # amax/448 rounded up to E8M0 (power of 2) + q = (x / sf).to(tl.float8e4nv) + tl.store(O + row * so0 + offs * so1, q) + tl.store(SF + row * ss0 + kb * ss1, sf) + + +def fp8_e8m0_cast_128(x: torch.Tensor): + """Fast Triton replica of DeepGEMM's ``per_token_cast_to_fp8(x, use_ue8m0=True, gran_k=128)`` + (exact bit-match, ~4x faster). x[M,K] bf16 -> (fp8_e4m3 [M,K], scale fp32 [M,K/128]).""" + M, K = x.shape + assert K % 128 == 0 + o = torch.empty(M, K, device=x.device, dtype=torch.float8_e4m3fn) + sf = torch.empty(M, K // 128, device=x.device, dtype=torch.float32) + _fp8_e8m0_q_kernel[(M, K // 128)]( + x, + o, + sf, + x.stride(0), + x.stride(1), + o.stride(0), + o.stride(1), + sf.stride(0), + sf.stride(1), + G=128, + ) + return o, sf + + +def nvfp4_to_mxfp4_weight( + qdata: torch.Tensor, scale: torch.Tensor, per_tensor: torch.Tensor, chunk: int = 16 +): + """One-time per-expert weight requant NVFP4 (E4M3/16) -> MXFP4 (E8M0/128) for DeepGEMM's + grouped fp8xfp4 kernel (which only accepts E8M0/128). qdata [E,N,K/2], scale [E,N,K/16], + per_tensor [E] -> (wq [E,N,K/2] uint8, ws [E,N,K/128] fp32). + + Dequant + cast in expert CHUNKS into preallocated outputs so the bf16 intermediate is bounded to + ``chunk`` experts (the full [E,N,K] bf16 was ~E/chunk x larger and OOMed at E=256). The output is + full-size by construction (the grouped GEMM needs all experts present).""" + cast = _dg().utils.per_token_cast_to_fp4 + E = int(qdata.size(0)) + q0, s0 = cast( + nvfp4_dequant_bf16(qdata[:1], scale[:1], per_tensor[:1])[0].contiguous(), + True, + 128, + ) + wq = q0.new_empty((E, *q0.shape)) + ws = s0.new_empty((E, *s0.shape)) + for c0 in range(0, E, chunk): + c1 = min(c0 + chunk, E) + Wb = nvfp4_dequant_bf16(qdata[c0:c1], scale[c0:c1], per_tensor[c0:c1]) + for i in range(c1 - c0): + q, s = cast(Wb[i].contiguous(), True, 128) + wq[c0 + i] = q + ws[c0 + i] = s + del Wb + return wq, ws + + +def deepgemm_grouped_fp8_fp4( + a_bf16: torch.Tensor, wq: torch.Tensor, ws: torch.Tensor, m_indices: torch.Tensor +) -> torch.Tensor: + """Contiguous-grouped fp8(act) x mxfp4(weight) base GEMM via DeepGEMM (SM90/SM100). + a_bf16 [Mt,K], wq [E,N,K/2] uint8, ws [E,N,K/128] fp32, m_indices [Mt] int32 -> [Mt,N] bf16. + Acts quantized to fp8 (E8M0/128) here; the kernel transforms the raw scales internally.""" + dg = _dg() + a = fp8_e8m0_cast_128(a_bf16) + d = torch.empty( + a_bf16.size(0), wq.size(1), device=a_bf16.device, dtype=torch.bfloat16 + ) + dg.m_grouped_fp8_fp4_gemm_nt_contiguous( + a, + (wq, ws), + d, + m_indices, + recipe=None, + recipe_a=(1, 128), + recipe_b=(1, 128), + disable_ue8m0_cast=False, + ) + return d + + +def _cached_mxfp4(w_nv, per_tensor, cache=None, key=None): + """Return (wq, ws) MXFP4 weight for a (frozen) NVFP4Tensor, requantized once and cached. + Prefer a persistent ``cache`` dict (e.g. on the owning module) keyed by ``key`` — under + FSDP2 the gathered param is a fresh tensor each step, so a module-level cache (not a + per-tensor attribute) is what avoids recomputing the requant every forward. Falls back to a + per-tensor attribute, then to a one-shot recompute.""" + from .runtime import RUNTIME + + if not RUNTIME.mxfp4_cache_persist: + # FSDP: any attached cache accumulates a full-model mxfp4 copy across layers -> OOM. + # Recompute uncached so resident mxfp4 stays bounded to one layer. + return nvfp4_to_mxfp4_weight(w_nv.qdata, w_nv.scale, per_tensor) + if cache is not None and key is not None and key in cache: + return cache[key] + cached = getattr(w_nv, "_dg_mxfp4", None) + if cached is None: + cached = nvfp4_to_mxfp4_weight(w_nv.qdata, w_nv.scale, per_tensor) + try: + w_nv._dg_mxfp4 = cached + except (AttributeError, RuntimeError): + pass + if cache is not None and key is not None: + cache[key] = cached + return cached + + +@triton.jit +def _nvfp4_deq_fp8_kernel( + Qp, Sc, Pt, Out, Kd, ROWS_PER_E, CB, sq0, sq1, ss0, ss1, so0, so1, BK: tl.constexpr +): + rid = tl.program_id(0).to(tl.int64) + kk = tl.program_id(1) * BK + tl.arange(0, BK) + km = kk < Kd + pt = tl.load(Pt + rid // ROWS_PER_E).to(tl.float32) + packed = tl.load(Qp + rid * sq0 + (kk // 2) * sq1, mask=km, other=0).to(tl.int32) + nib = tl.where((kk % 2) == 1, (packed >> 4) & 0xF, packed & 0xF) + sc = tl.load(Sc + rid * ss0 + (kk // 16) * ss1, mask=km, other=0.0).to(tl.float32) + tl.store( + Out + rid * so0 + kk * so1, + (tl.load(CB + nib) * sc * pt).to(tl.float8e4nv), + mask=km, + ) + + +def nvfp4_dequant_fp8( + qdata: torch.Tensor, scale: torch.Tensor, per_tensor: torch.Tensor +) -> torch.Tensor: + """NVFP4 -> fp8 (e4m3) — half the bytes of the bf16 dequant. For the fp8-read backward dX + (#3744): the grouped GEMM reads fp8 and upcasts to bf16 in-register, halving the weight's + write+read bandwidth (a win on bandwidth-bound sm120; ~neutral speed but still half-memory + on sm100).""" + c, nrows, kh = qdata.shape + kd = kh * 2 + out = torch.empty(c, nrows, kd, device=qdata.device, dtype=torch.float8_e4m3fn) + q2, s2, o2 = ( + qdata.reshape(c * nrows, kh), + scale.reshape(c * nrows, kd // 16), + out.reshape(c * nrows, kd), + ) + BK = 1024 + _nvfp4_deq_fp8_kernel[(c * nrows, triton.cdiv(kd, BK))]( + q2, + s2, + per_tensor.contiguous(), + o2, + kd, + nrows, + fp4_codebook(qdata.device), + q2.stride(0), + q2.stride(1), + s2.stride(0), + s2.stride(1), + o2.stride(0), + o2.stride(1), + BK=BK, + ) + return out + + +# BM is the routing pad TILE (128 cutlass, 64 marlin); a constexpr autotune key so BN/BK/warps +# still tune per (N,K,BM). +@triton.autotune( + configs=[ + triton.Config({"BN": bn, "BK": bk}, num_warps=w, num_stages=st) + for bn in (64, 128) + for bk in (128, 256) + for w in (4, 8) + for st in (3, 4) + ], + key=["N", "K", "BM"], +) +@triton.jit +def _grouped_dx_fp8_kernel( + GRAD, + W, + MIDX, + OUT, + M, + N, + K, + sg0, + sg1, + sw0, + sw1, + sw2, + so0, + so1, + BM: tl.constexpr, + BN: tl.constexpr, + BK: tl.constexpr, +): + pid_m = tl.program_id(0) + pid_k = tl.program_id(1) + e = tl.load(MIDX + pid_m).to(tl.int64) + rm = pid_m * BM + tl.arange(0, BM) + rk = pid_k * BK + tl.arange(0, BK) + mk = rk < K + acc = tl.zeros((BM, BK), tl.float32) + for n0 in range(0, N, BN): + rn = n0 + tl.arange(0, BN) + a = tl.load( + GRAD + rm[:, None] * sg0 + rn[None, :] * sg1, + mask=rm[:, None] < M, + other=0.0, + ) + # mask K dim to handle non-BK-aligned K (e.g. I=704 which is not a multiple of 128) + w = tl.load( + W + e * sw0 + rn[:, None] * sw1 + rk[None, :] * sw2, + mask=mk[None, :], + other=0.0, + ).to(tl.bfloat16) + acc += tl.dot(a, w) + tl.store( + OUT + rm[:, None] * so0 + rk[None, :] * so1, + acc.to(tl.bfloat16), + mask=(rm[:, None] < M) & mk[None, :], + ) + + +def grouped_dx_fp8( + grad: torch.Tensor, w_fp8: torch.Tensor, m_indices: torch.Tensor, block_m: int = 128 +) -> torch.Tensor: + """dX = grad @ W (contract N) for contiguous-grouped experts, reading the fp8 weight and + upcasting in-register. grad[Mt,N] bf16, w_fp8[E,N,K] e4m3, m_indices[Mt/block_m] (one expert id + per block_m-row tile; block_m = the routing pad TILE: 128 cutlass, 64 marlin) -> [Mt,K] bf16.""" + Mt, N = grad.shape + K = w_fp8.size(2) + out = torch.empty(Mt, K, device=grad.device, dtype=torch.bfloat16) + grid = lambda meta: (Mt // block_m, triton.cdiv(K, meta["BK"])) # noqa: E731 + _grouped_dx_fp8_kernel[grid]( + grad, + w_fp8, + m_indices, + out, + Mt, + N, + K, + grad.stride(0), + grad.stride(1), + w_fp8.stride(0), + w_fp8.stride(1), + w_fp8.stride(2), + out.stride(0), + out.stride(1), + BM=block_m, + ) + return out + + +class _GroupedMM(torch.autograd.Function): + """``torch._grouped_mm`` with a manual backward (its autograd is stride-broken). Frozen + weight ``bT`` (the dequantized experts) → only the input grad ``dX`` is returned.""" + + @staticmethod + def forward(ctx, a, bT, offs): + ctx.save_for_backward(bT, offs) + return torch._grouped_mm(a, bT, offs=offs) + + @staticmethod + def backward(ctx, g): + bT, offs = ctx.saved_tensors + dA = torch._grouped_mm( + g.contiguous(), bT.transpose(1, 2).contiguous(), offs=offs + ) + return dA, None, None + + +def _chunk_forward(sorted_tok, gqd, gsc, dqd, dsc, pt, coff, limit): + """One expert-chunk: dequant -> grouped gate_up -> clamped-SwiGLU -> grouped down.""" + gub = nvfp4_dequant_bf16(gqd, gsc, pt).transpose(1, 2) + dnb = nvfp4_dequant_bf16(dqd, dsc, pt).transpose(1, 2) + x = _GroupedMM.apply(sorted_tok, gub, coff) + g, u = x.chunk(2, dim=-1) + h = ( + torch.nn.functional.silu(g.clamp(max=limit)) * u.clamp(min=-limit, max=limit) + ).contiguous() + return _GroupedMM.apply(h, dnb, coff) + + +def chunked_dequant_grouped_base( + hidden, idx, wts, gate_up_nv, down_nv, per_tensor, limit, chunk_size=8 +): + """Base clamped-SwiGLU MoE on NVFP4 experts via chunked dequant + grouped_mm. + + hidden [N,H]; idx [N,topk]; wts [N,topk]; gate_up_nv/down_nv: NVFP4Tensor + ([E,2I,H]/[E,H,I]); per_tensor: scalar or [E] fp32. Returns [N,H]. Differentiable to + ``hidden``. Per-chunk ``checkpoint`` keeps the bf16 weight transient bounded (re-dequant + in backward). + """ + E = gate_up_nv.qdata.size(0) + Hdim = hidden.size(1) + # raw-row-major scale indexing only; deswizzling is not implemented here + assert not getattr(gate_up_nv, "is_swizzled_scales", False), ( + "swizzled NVFP4 scales unsupported" + ) + assert not getattr(down_nv, "is_swizzled_scales", False), ( + "swizzled NVFP4 scales unsupported" + ) + # per_tensor_scale may be a global scalar or per-expert; normalize to [E] + per_tensor = per_tensor.reshape(-1).to(torch.float32) + if per_tensor.numel() == 1: + per_tensor = per_tensor.expand(E) + flat_exp = idx.reshape(-1) + order = flat_exp.argsort() + rep = torch.arange(hidden.size(0), device=hidden.device).repeat_interleave( + idx.size(1) + )[order] + wflat = wts.reshape(-1)[order] + counts = torch.bincount(flat_exp, minlength=E) + tok_off = torch.cat([counts.new_zeros(1), counts.cumsum(0)]).tolist() + flat = hidden[rep] + + out = hidden.new_zeros(hidden.size(0), Hdim) + pieces = [] + gq, gs = gate_up_nv.qdata, gate_up_nv.scale + dq, ds = down_nv.qdata, down_nv.scale + for c0 in range(0, E, chunk_size): + c1 = min(c0 + chunk_size, E) + t0, t1 = int(tok_off[c0]), int(tok_off[c1]) + if t1 == t0: + continue + coff = counts[c0:c1].cumsum(0).to(torch.int32) + o = checkpoint( + _chunk_forward, + flat[t0:t1], + gq[c0:c1], + gs[c0:c1], + dq[c0:c1], + ds[c0:c1], + per_tensor[c0:c1], + coff, + limit, + use_reentrant=False, + ) + pieces.append(o * wflat[t0:t1, None].to(o.dtype)) + if not pieces: # no routed tokens (e.g. empty batch / expert-parallel shard) + return out + return out.index_add(0, rep, torch.cat(pieces, 0)) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts.py index b5601848c5..79ee556bbf 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts.py @@ -4,7 +4,10 @@ ScatterMoE Triton call via ``parallel_linear_lora``. """ +import functools + import torch +import torch.nn.functional as F from .mx_weights import selective_mx_weights_fwd, selective_nvfp4_weights_fwd from .parallel_experts import flatten_sort_count, parallel_linear @@ -142,6 +145,7 @@ def _prepare_weights_and_lora( gup_lora, down_lora, dtype, + module=None, ): """Resolve the gate_up / down weights (+ routing + LoRA) for the grouped GEMM. @@ -164,6 +168,23 @@ def _prepare_weights_and_lora( ) gate_up_weight = select(gu_param, active) down_weight = select(dn_param, active) + # Recompute-in-backward recipe: the selective gather is a per-layer copy (~2 GB/layer at + # dense routing) that ScatterMoELoRA would otherwise pin on ctx across all layers (peak + # grows with depth). It is reconstructible from the frozen resident param, so hand the + # Function a recipe to rebuild it in backward (cheap to recompute, expensive to store). + # FSDP2-safe: re-read the LIVE module param at backward (via `module`), not the forward-time + # object: FSDP reshards after forward and re-gathers for backward, so a captured reference + # would be the half-size shard (wrong dX shape). On single-GPU it's the same param. + if module is not None: + gate_up_weight.recipe = lambda m=module, a=active, f=select: f( + _get_base_param(m.gate_up_proj), a + ) + down_weight.recipe = lambda m=module, a=active, f=select: f( + _get_base_param(m.down_proj), a + ) + else: + gate_up_weight.recipe = lambda p=gu_param, a=active, f=select: f(p, a) + down_weight.recipe = lambda p=dn_param, a=active, f=select: f(p, a) gup_lora = ( *selective_lora_weights(gup_lora[0], gup_lora[1], active, num_experts), gup_lora[2], @@ -173,12 +194,12 @@ def _prepare_weights_and_lora( down_lora[2], ) elif is_mx or is_nv: - # quantized base without LoRA: the fused kernel is LoRA-only and the FP4 - # tensors have no transpose, so dequantize to bf16 here. + # quantized base without LoRA: the fused kernel is LoRA-only and the FP4 tensors have no + # transpose, so dequantize to bf16 here. gate_up_weight = gu_param.dequantize(dtype).transpose(2, 1) down_weight = dn_param.dequantize(dtype).transpose(2, 1) else: - gate_up_weight = gu_param.transpose(2, 1) # bf16 [E, out, in] -> [E, in, out] + gate_up_weight = gu_param.transpose(2, 1) down_weight = dn_param.transpose(2, 1) return ( gate_up_weight, @@ -190,6 +211,38 @@ def _prepare_weights_and_lora( ) +def _detect_act_type(module) -> str: + """Detect gated-activation type from an experts module's act_fn. + + Returns 'gelu_tanh' for Gemma4-style GeGLU (gelu_pytorch_tanh * up), + 'silu' for DSV4-style clamped SwiGLU (silu(clamp(gate)) * clamp(up)). + Falls back to 'silu' for any unrecognized activation. + """ + act_fn = getattr(module, "act_fn", None) + if act_fn is None: + return "silu" + fn_name = ( + getattr(act_fn, "__name__", "") or getattr(type(act_fn), "__name__", "") or "" + ) + if "gelu" in fn_name.lower(): + return "gelu_tanh" + try: + if isinstance(act_fn, functools.partial) and act_fn.func is F.gelu: + return "gelu_tanh" + except Exception: + pass + # last resort: compare act_fn output to gelu_pytorch_tanh numerically + try: + x = torch.tensor([0.5], dtype=torch.float32, device="cpu") + ref = F.gelu(x, approximate="tanh") + got = act_fn(x.clone()) + if torch.allclose(ref, got, atol=1e-5): + return "gelu_tanh" + except Exception: + pass + return "silu" + + def scattermoe_supports_layout(self) -> bool: """True iff this experts module uses the standard layout scattermoe handles: gate_up concatenated as [E, 2I, H], gated SwiGLU, no expert bias. gpt_oss-style @@ -204,9 +257,6 @@ def scattermoe_supports_layout(self) -> bool: def _check_supported_layout(self): """Reject expert layouts the fixed transpose/chunk below would miscompute.""" - # gpt_oss-style experts (interleaved gate/up, transposed [E, H, 2I], expert - # bias) would be silently miscomputed by the fixed transpose/chunk below, so - # reject rather than corrupt training. if not scattermoe_supports_layout(self): raise NotImplementedError( "scattermoe supports only concatenated, non-transposed, gated, biasless " @@ -260,13 +310,17 @@ def _scattermoe_gptoss_forward( gate_up_weight = _get_base_param( self.gate_up_proj - ) # [E, H, 2I], already [E,in,out] - down_weight = _get_base_param(self.down_proj) # [E, I, H] - gate_up_bias = _get_base_param(self.gate_up_proj_bias) # [E, 2I] - down_bias = _get_base_param(self.down_proj_bias) # [E, H] + ) # already [E, in, out], no transpose + down_weight = _get_base_param(self.down_proj) + gate_up_bias = _get_base_param(self.gate_up_proj_bias) + down_bias = _get_base_param(self.down_proj_bias) gup_lora, down_lora = None, None - if _has_peft_wrapper(self): + sm_lora = getattr(self, "_scattermoe_lora", None) + if sm_lora: + gup_lora = sm_lora.get("gate_up_proj") + down_lora = sm_lora.get("down_proj") + elif _has_peft_wrapper(self): _, gup_lora, down_lora = _unwrap_experts_lora(self) gate_up = _parallel_linear_maybe_lora( @@ -299,6 +353,18 @@ def _scattermoe_gptoss_forward( return output +# thin compat wrappers over the centralized runtime module (runtime.py) +from .runtime import RUNTIME # noqa: E402 + + +def set_fp4_grouped_mode(mode): + RUNTIME.fp4_grouped_mode = mode + + +def set_fp4_dx_prefer_fp8(flag): + RUNTIME.fp4_dx_prefer_fp8 = bool(flag) + + def scattermoe_experts_forward( self, hidden_states: torch.Tensor, @@ -320,30 +386,166 @@ def scattermoe_experts_forward( top_k_index, num_experts=self.num_experts ) - # Extract LoRA params if PEFT is active gup_lora, down_lora = None, None - if _has_peft_wrapper(self): + sm_lora = getattr(self, "_scattermoe_lora", None) + if sm_lora: + gup_lora = sm_lora.get("gate_up_proj") + down_lora = sm_lora.get("down_proj") + elif _has_peft_wrapper(self): _, gup_lora, down_lora = _unwrap_experts_lora(self) - ( - gate_up_weight, - down_weight, - sorted_expert_idxs, - expert_offsets, - gup_lora, - down_lora, - ) = _prepare_weights_and_lora( - _get_base_param(self.gate_up_proj), - _get_base_param(self.down_proj), - sorted_expert_idxs, - expert_offsets, - self.num_experts, - gup_lora, - down_lora, - hidden_states.dtype, + # Config-gated grouped fp4 path (NVFP4 experts + LoRA + available fp4 backend); faster + + # lower-memory than the fused-Triton path at scale. + _fp4_grouped_mode = RUNTIME.fp4_grouped_mode + if _fp4_grouped_mode is not None and gup_lora is not None and down_lora is not None: + gu_base = _get_base_param(self.gate_up_proj) + dn_base = _get_base_param(self.down_proj) + if is_nvfp4_param(gu_base): + from .grouped_train import grouped_fp4_available, grouped_fp4_moe_train + + # No LoRA-capable base GEMM here; the legacy fused-MX kernel SIGSEGVs on Blackwell, so + # hard-error rather than crash. + if not grouped_fp4_available(_fp4_grouped_mode): + _cap = ( + "sm%d%d" % torch.cuda.get_device_capability() + if torch.cuda.is_available() + else "cpu" + ) + raise RuntimeError( + f"dsv4_fp4_grouped_mode={_fp4_grouped_mode!r} with LoRA selected the grouped " + f"NVFP4 MoE path, but no fused grouped backend resolved on this arch ({_cap}): " + "marlin, cutlass, and deepgemm are all unavailable. The legacy fused-MX kernel " + "(scatter2scatter_lora_mx) is unsafe on this arch (SIGSEGV on Blackwell), so it " + "is not used as a fallback. Run on a supported GPU (sm80+/sm90/sm100/sm120) or " + "disable dsv4_fp4_grouped_mode." + ) + # FSDP-safe: backward re-reads the (re-gathered) params via this recipe + _recipe = lambda: ( # noqa: E731 + _get_base_param(self.gate_up_proj), + _get_base_param(self.down_proj), + ) + # persistent per-module cache so the backend requantizes the frozen weight to mxfp4 + # once, surviving FSDP re-gathers (the gathered param is fresh each step). + cache = self.__dict__.setdefault("_dg_mxfp4_cache", {}) + return grouped_fp4_moe_train( + hidden_states, + top_k_index, + routing_weights, + gu_base, + dn_base, + gup_lora, + down_lora, + getattr(self, "limit", None), + _fp4_grouped_mode, + act_type=_detect_act_type(self), + weight_recipe=_recipe, + mxfp4_cache=cache, + prefer_fp8_dx=RUNTIME.fp4_dx_prefer_fp8, + ) + + # NVFP4 experts + LoRA without dsv4_fp4_grouped_mode set would fall through to + # _prepare_weights_and_lora -> selective_nvfp4_weights_fwd -> scatter2scatter_lora_mx, which + # SIGSEGVs on Blackwell. Turn that silent crash into an actionable error (the grouped path is the + # supported route for NVFP4+LoRA); non-Blackwell archs keep the legacy path. + if ( + _fp4_grouped_mode is None + and gup_lora is not None + and down_lora is not None + and torch.cuda.is_available() + and torch.cuda.get_device_capability()[0] >= 10 + and is_nvfp4_param(_get_base_param(self.gate_up_proj)) + ): + raise RuntimeError( + "NVFP4 experts with LoRA require the grouped fp4 MoE path on Blackwell (sm100/sm120): " + "the legacy fused-MX kernel (scatter2scatter_lora_mx) SIGSEGVs on this arch. Set " + "dsv4_fp4_grouped_mode: nvfp4 in your config to enable the supported grouped GEMM path." + ) + + # BnB-4bit experts have no 4-bit-read kernel; the naive path full-dequants all E experts every + # forward (~2.7x VRAM). Route to the chunked-dequant grouped MoE (bounded transient). Detect + # WITHOUT touching self.gate_up_proj (would full-dequant). + _bnb_experts = ( + hasattr(self, "parametrizations") + and "gate_up_proj" in self.parametrizations + and "down_proj" in self.parametrizations ) + _bnb_fast = False + if _bnb_experts: + from .chunked_bnb import bnb_fast_enabled + + _bnb_fast = bnb_fast_enabled() + if _bnb_experts and not _bnb_fast: + from .chunked_bnb import chunked_bnb_moe + + return chunked_bnb_moe( + hidden_states, + top_k_index, + routing_weights, + self, + gup_lora, + down_lora, + self.num_experts, + act_type=_detect_act_type(self), + limit=getattr(self, "limit", None), + ) + + if _bnb_experts: + # bnb_fast: selective dequant of active experts -> 1-launch parallel_linear instead of the + # chunked torch._grouped_mm storm. Holds bf16 for backward. + from .selective_dequant import selective_expert_weights + + active = get_active_experts(sorted_expert_idxs, self.num_experts) + sorted_expert_idxs, expert_offsets = remap_expert_indices( + sorted_expert_idxs, expert_offsets, active, self.num_experts + ) + gate_up_weight = selective_expert_weights( + self, "gate_up_proj", active + ).transpose(2, 1) + down_weight = selective_expert_weights(self, "down_proj", active).transpose( + 2, 1 + ) + # Recompute-in-backward recipe: the selective dequant is a per-layer bf16 copy of the active + # experts that ScatterMoELoRA would otherwise pin across all layers (~40 GB). The frozen + # 4-bit param is resident, so re-run the dequant in backward via the closure. + gate_up_weight.recipe = lambda m=self, a=active: selective_expert_weights( + m, "gate_up_proj", a + ).transpose(2, 1) + down_weight.recipe = lambda m=self, a=active: selective_expert_weights( + m, "down_proj", a + ).transpose(2, 1) + if gup_lora is not None and down_lora is not None: + gup_lora = ( + *selective_lora_weights( + gup_lora[0], gup_lora[1], active, self.num_experts + ), + gup_lora[2], + ) + down_lora = ( + *selective_lora_weights( + down_lora[0], down_lora[1], active, self.num_experts + ), + down_lora[2], + ) + else: + ( + gate_up_weight, + down_weight, + sorted_expert_idxs, + expert_offsets, + gup_lora, + down_lora, + ) = _prepare_weights_and_lora( + _get_base_param(self.gate_up_proj), + _get_base_param(self.down_proj), + sorted_expert_idxs, + expert_offsets, + self.num_experts, + gup_lora, + down_lora, + hidden_states.dtype, + module=self, + ) - # Gate-up projection (with optional LoRA) gates_h = _parallel_linear_maybe_lora( hidden_states, gate_up_weight, @@ -356,9 +558,14 @@ def scattermoe_experts_forward( grouped_out=True, ) gates, h = gates_h.chunk(2, dim=-1) + # Clamped SwiGLU when the model defines a swiglu_limit (e.g. DeepSeek-V4 limit=10) must match the + # eager experts' `_apply_gate` (gate.clamp(max=L); up.clamp(-L, L)), else outliers blow up. + _limit = getattr(self, "limit", None) + if _limit is not None: + gates = gates.clamp(max=_limit) + h = h.clamp(min=-_limit, max=_limit) h = self.act_fn(gates) * h - # Down projection (with optional LoRA + routing weights) output = _parallel_linear_maybe_lora( h, down_weight, @@ -419,7 +626,11 @@ def scattermoe_experts_forward_ep( ss = torch.arange(M, device=hidden_states.device, dtype=torch.int32) gup_lora, down_lora = None, None - if _has_peft_wrapper(self): + sm_lora = getattr(self, "_scattermoe_lora", None) + if sm_lora: + gup_lora = sm_lora.get("gate_up_proj") + down_lora = sm_lora.get("down_proj") + elif _has_peft_wrapper(self): _, gup_lora, down_lora = _unwrap_experts_lora(self) gate_up_weight, down_weight, se, expert_offsets, gup_lora, down_lora = ( @@ -450,6 +661,10 @@ def scattermoe_experts_forward_ep( grouped_out=True, ) gates, h = gates_h.chunk(2, dim=-1) + _limit = getattr(self, "limit", None) + if _limit is not None: + gates = gates.clamp(max=_limit) + h = h.clamp(min=-_limit, max=_limit) h = self.act_fn(gates) * h down_out = _parallel_linear_maybe_lora( @@ -473,6 +688,39 @@ def scattermoe_experts_forward_ep( _SCATTERMOE_PATCHED = False +def _ensure_single_lora_ops(): + """Guarantee a single module instance of the autotuned ``kernels.lora_ops``. + + A second instance (same file imported under a different module name) would carry its own + Triton ``Autotuner`` caches — duplicate autotuning (wasted) and confusing telemetry where + the same kernel+shape+dtype appears twice with different configs. We canonicalize on the + axolotl import path and alias any duplicate in ``sys.modules`` to it, so later imports + resolve to one module. Idempotent; warns only if a real duplicate is found.""" + import sys + + from axolotl.utils.logging import get_logger + + log = get_logger(__name__) + canon_name = "axolotl.integrations.kernels.libs.scattermoe_lora.kernels.lora_ops" + canon = sys.modules.get(canon_name) + if canon is None: + return + canon_file = getattr(canon, "__file__", None) + aliased = 0 + for name, mod in list(sys.modules.items()): + if mod is None or mod is canon or name == canon_name or "lora_ops" not in name: + continue + if getattr(mod, "__file__", None) == canon_file: + sys.modules[name] = canon + aliased += 1 + if aliased: + log.warning( + "Aliased %d duplicate lora_ops module instance(s) to %s (single autotune cache)", + aliased, + canon_name, + ) + + def register_scattermoe_experts(): """Register ``"scattermoe"`` in the ExpertsInterface and the validator allowlist. @@ -480,11 +728,58 @@ def register_scattermoe_experts(): """ global _SCATTERMOE_PATCHED + _ensure_single_lora_ops() + from transformers.integrations.moe import ALL_EXPERTS_FUNCTIONS from transformers.modeling_utils import PreTrainedModel ALL_EXPERTS_FUNCTIONS.register("scattermoe", scattermoe_experts_forward) + # transformers' lazy-import can leave TWO `integrations.moe` registries. + # `@use_experts_implementation` binds its registry as a default argument, which can be the OTHER + # object than the one imported above, so register into the decorator's default interface too. + try: + import inspect + + from transformers.integrations import use_experts_implementation + + canon = ( + inspect.signature(use_experts_implementation) + .parameters["experts_interface"] + .default + ) + if canon is not None and "scattermoe" not in canon: + canon.register("scattermoe", scattermoe_experts_forward) + except (ImportError, KeyError, AttributeError): + pass + + # Route PEFT target_parameters expert LoRA to the fused kernel (bypassing the parametrization + # merge) for experts-interface MoEs. + try: + from .experts_lora_fastpath import patch_paramwrapper_fastpath + + patch_paramwrapper_fastpath() + except (ImportError, AttributeError): + pass + + # Safety net for any path that still merges `base + delta` on a frozen FP4 base + # (e.g. merge_and_unload): make aten.add dequantize the FP4 tensor. + try: + from .torchao_fp4_add import patch_torchao_fp4_add + + patch_torchao_fp4_add() + except (ImportError, AttributeError): # torchao optional / API drift + pass + + # FSDP2 support for NVFP4Tensor (split/view/as_strided + all-gather hooks) so the quantized + # expert weights can be sharded across GPUs (torchao ships none). + try: + from .nvfp4_fsdp import patch_nvfp4_fsdp + + patch_nvfp4_fsdp() + except (ImportError, AttributeError): + pass + if _SCATTERMOE_PATCHED: return diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts_lora_fastpath.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts_lora_fastpath.py new file mode 100644 index 0000000000..f001016dab --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/experts_lora_fastpath.py @@ -0,0 +1,109 @@ +"""Route PEFT ``target_parameters`` LoRA on experts-interface MoEs to the fused kernel. + +For models whose MoE is dispatched through transformers' ExpertsInterface +(``@use_experts_implementation`` — e.g. Gemma 4, DiffusionGemma), PEFT wraps the +*experts module* in a ``ParamWrapper`` chain. The decoder layer then calls +``experts(...)`` → ``ParamWrapper.forward`` → ``_activate_lora`` which materializes the +full merged weight ``base + delta`` before ScatterMoE runs. That defeats ScatterMoE's +fused LoRA kernel (which takes A/B separately, dequantizing only the active experts) and +fails outright on quantized bases (``aten.add`` on NVFP4/MXFP4). + +This patches ``ParamWrapper.forward`` so that, when the wrapped base is a ScatterMoE +experts module, it walks the wrapper chain, hands the LoRA A/B to the base via +``_scattermoe_lora`` (in ScatterMoE layout), and calls the base forward directly — +bypassing the parametrization merge entirely. The wrappers stay in the module tree, so +PEFT save/load and optimizer tracking are unaffected. + +This mirrors what ``HFScatterMoEGatedMLP`` already does for SparseMoeBlock models (walk +the wrapper, fuse the LoRA), bringing the experts-interface path to parity. +""" + +from __future__ import annotations + + +def _is_scattermoe_experts(module) -> bool: + cfg = getattr(module, "config", None) + impl = getattr(cfg, "_experts_implementation", None) + return impl == "scattermoe" and hasattr(module, "gate_up_proj") + + +def patch_paramwrapper_fastpath() -> None: + """Idempotently patch ``peft...ParamWrapper.forward`` for ScatterMoE experts.""" + try: + from peft.tuners.lora.layer import ParamWrapper + except (ImportError, AttributeError): + return + + if getattr(ParamWrapper.forward, "_scattermoe_fastpath", False): + return + + from .layers import _convert_smoe_lora + from .parallel_linear_lora import get_lora_params_from_wrapper + + _orig_forward = ParamWrapper.forward + + def _fusable(wrapper) -> bool: + # Fused kernel needs exactly one adapter on a raw (un-merged) base; else defer to PEFT. + if getattr(wrapper, "disable_adapters", False) or getattr( + wrapper, "merged", False + ): + return False + return len(getattr(wrapper, "active_adapters", [])) == 1 + + def forward(self, x, *args, **kwargs): + # Mixed-batch inference (adapter_names) is PEFT's domain; defer to it. + if kwargs.get("adapter_names") is not None: + return _orig_forward(self, x, *args, **kwargs) + + # one wrapper per targeted parameter + wrappers = {} + base = self + while hasattr(base, "base_layer") and hasattr(base, "lora_A"): + name = getattr(base, "parameter_name", None) + if name is not None: + wrappers[name] = base + base = base.base_layer + + if not ( + _is_scattermoe_experts(base) and all(_fusable(w) for w in wrappers.values()) + ): + return _orig_forward(self, x, *args, **kwargs) + + num_experts = getattr(base, "num_experts", None) + sm_lora = {} + for name, wrapper in wrappers.items(): + lora_A, lora_B, scaling = get_lora_params_from_wrapper(wrapper) + if lora_A is None or num_experts is None: + continue + rank = lora_A.shape[0] // num_experts + # PEFT keeps LoRA fp32; cast to activation dtype (grads still route to the fp32 params). + lora_A = lora_A.to(x.dtype) + lora_B = lora_B.to(x.dtype) + sm_lora[name] = _convert_smoe_lora( + lora_A, lora_B, num_experts, rank, scaling + ) + + base._scattermoe_lora = sm_lora + # Loading can create multiple ExpertsInterface registries; the experts forward binds one as + # a closure default that may differ from the one register_scattermoe_experts() populated, so + # register into the one THIS module dispatches through. + if not getattr(base, "_scattermoe_iface_ok", False): + _fn = type(base).forward + _cells = dict( + zip(_fn.__code__.co_freevars, _fn.__closure__ or (), strict=False) + ) + _ei = _cells.get("experts_interface") + if _ei is not None: + _ei = _ei.cell_contents + if "scattermoe" not in _ei: + from .experts import scattermoe_experts_forward + + _ei.register("scattermoe", scattermoe_experts_forward) + base._scattermoe_iface_ok = True + try: + return base(x, *args, **kwargs) + finally: + base._scattermoe_lora = None + + forward._scattermoe_fastpath = True # type: ignore[attr-defined] + ParamWrapper.forward = forward diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_fp8_nonexpert.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_fp8_nonexpert.py new file mode 100644 index 0000000000..b7e66a7c09 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_fp8_nonexpert.py @@ -0,0 +1,206 @@ +"""In-place fp8 per-channel quantization of gemma4 non-expert linears. + +Targets every ``nn.Linear`` that is NOT inside a ``Gemma4TextExperts`` block +(experts stay NVFP4Tensor, untouched). Norms, embeddings, router projections, +vision tower weights, and lm_head are also skipped by class or name. + +After this runs each targeted linear has: + - ``weight`` : fp8_e4m3fn [out, in] (1 byte/param) + - ``weight_scale_inv`` : bfloat16 [out, 1] (per-row scale = max_abs/fp8_max) + - ``forward`` : overridden to dequant on-the-fly and call F.linear + +The name ``weight_scale_inv`` matches the axolotl LoRA kernel's lookup so the +backward dequantize path works without further changes. + +The dequant-in-forward path is the same as the DSV4 bf16-mode fallback — +frozen fp8 base, bf16 activations, bf16 LoRA adapters layered on top by PEFT. +The fused kernel rewrite (``torch._scaled_mm``) is a follow-up. + +Usage (in plugin.py ``pre_lora_load``): + from .gemma4_fp8_nonexpert import quantize_gemma4_nonexpert_linears + quantize_gemma4_nonexpert_linears(model) +""" + +from __future__ import annotations + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +# Names/substrings that should NEVER be fp8-quantized (precision-sensitive or tiny). +_SKIP_NAME_SUBSTRINGS = ( + "norm", + "embed", + "lm_head", + "router", + "vision_tower", + "audio", +) + +_FP8_MAX = torch.finfo(torch.float8_e4m3fn).max + + +def _should_skip_by_name(name: str) -> bool: + return any(sub in name for sub in _SKIP_NAME_SUBSTRINGS) + + +def _patch_linear_forward(mod: nn.Linear) -> None: + """Monkey-patch a single nn.Linear whose weight was already replaced with + an fp8 tensor + weight_scale_inv bfloat16 parameter [out, 1]. + + The attribute name ``weight_scale_inv`` is chosen to match what the axolotl + LoRA kernel (``lora.py:get_lora_parameters``) probes for on an fp8 weight:: + + quant_state = getattr(base_layer, "weight_scale_inv", None) + + so the LoRA backward's ``dequantize(q_weight, q_quant)`` call receives the + per-channel scale and correctly dequantizes to bf16. + """ + + def _forward(self, x: torch.Tensor) -> torch.Tensor: + w_fp8: torch.Tensor = self.weight # float8_e4m3fn [out, in] + scale: torch.Tensor = self.weight_scale_inv # bfloat16 [out, 1] + w_bf16 = w_fp8.to(torch.bfloat16) * scale + x_compute = x.to(torch.bfloat16) if x.dtype != torch.bfloat16 else x + out = F.linear(x_compute, w_bf16, self.bias) + return out.to(x.dtype) if x.dtype != torch.bfloat16 else out + + import types + + mod.forward = types.MethodType(_forward, mod) + + +def quantize_gemma4_nonexpert_linears(model: nn.Module) -> int: + """Quantize all non-expert nn.Linear weights in a loaded gemma4 model to + fp8_e4m3fn per-channel in-place. Returns the number of modules quantized. + + Safe to call multiple times: already-quantized modules (weight.dtype == + float8_e4m3fn) are skipped. + """ + try: + from transformers.models.gemma4.modeling_gemma4 import Gemma4TextExperts + + _expert_cls = Gemma4TextExperts + except ImportError: + _expert_cls = None + + expert_paths: set[str] = set() + if _expert_cls is not None: + for path, mod in model.named_modules(): + if isinstance(mod, _expert_cls): + expert_paths.add(path) + + def _is_under_expert(path: str) -> bool: + return any(path == ep or path.startswith(ep + ".") for ep in expert_paths) + + quantized = 0 + by_type: dict[str, int] = {} + + for name, mod in model.named_modules(): + if not isinstance(mod, nn.Linear): + continue + if _is_under_expert(name): + continue + if _should_skip_by_name(name): + continue + w = mod.weight + if not isinstance(w, nn.Parameter): + continue + if w.dtype == torch.float8_e4m3fn: + continue # already quantized + if w.ndim != 2: + continue # embeddings are sometimes Linear subclasses + + w_data = w.data.to(torch.bfloat16) if w.dtype != torch.bfloat16 else w.data + scale_1d = w_data.abs().amax(dim=1).clamp(min=1e-12) + # "weight_scale_inv" name + [out, 1] shape match the axolotl LoRA kernel's fp8 quant_state lookup. + scale = (scale_1d / _FP8_MAX).to(torch.bfloat16).unsqueeze(1) + w_fp8 = ( + (w_data / scale.to(w_data.dtype)) + .clamp(-_FP8_MAX, _FP8_MAX) + .to(torch.float8_e4m3fn) + ) + + mod.weight = nn.Parameter(w_fp8, requires_grad=False) + mod.register_parameter( + "weight_scale_inv", nn.Parameter(scale, requires_grad=False) + ) + _patch_linear_forward(mod) + + key = type(mod).__name__ + by_type[key] = by_type.get(key, 0) + 1 + quantized += 1 + + if quantized: + LOG.info( + "Gemma4 frankenstein: fp8-quantized %d non-expert linears in-place " + "(per-channel e4m3, dequant-in-forward): %s", + quantized, + by_type, + ) + return quantized + + +def verify_gemma4_frankenstein(model: nn.Module) -> dict: + """Sanity-check the frankenstein state. Returns a dict of counts.""" + try: + from torchao.prototype.mx_formats.nvfp4_tensor import NVFP4Tensor + + _has_nvfp4 = True + except ImportError: + _has_nvfp4 = False + NVFP4Tensor = None + + n_nvfp4 = n_fp8 = n_bf16_lin = n_skip = 0 + fp8_bytes = bf16_lin_bytes = expert_bytes = 0 + + try: + from transformers.models.gemma4.modeling_gemma4 import Gemma4TextExperts + + _expert_cls = Gemma4TextExperts + except ImportError: + _expert_cls = None + + expert_paths: set[str] = set() + if _expert_cls is not None: + for path, mod in model.named_modules(): + if isinstance(mod, _expert_cls): + expert_paths.add(path) + + def _is_under_expert(path): + return any(path == ep or path.startswith(ep + ".") for ep in expert_paths) + + for name, param in model.named_parameters(): + if _has_nvfp4 and isinstance(param, NVFP4Tensor): + n_nvfp4 += 1 + expert_bytes += param.data.numel() * 1 # uint8 + continue + if param.dtype == torch.float8_e4m3fn: + n_fp8 += 1 + fp8_bytes += param.numel() + continue + if param.dtype in (torch.bfloat16, torch.float16, torch.float32): + if name.endswith(".weight") and not _is_under_expert( + name.rsplit(".", 1)[0] + ): + if not _should_skip_by_name(name): + n_bf16_lin += 1 + bf16_lin_bytes += param.numel() * 2 + else: + n_skip += 1 + else: + n_skip += 1 + + return { + "nvfp4_expert_params": n_nvfp4, + "fp8_nonexpert_params": n_fp8, + "bf16_linear_unexpected": n_bf16_lin, + "skipped_params": n_skip, + "expert_bytes_GB": expert_bytes / 1e9, + "fp8_bytes_GB": fp8_bytes / 1e9, + "bf16_linear_unexpected_bytes_GB": bf16_lin_bytes / 1e9, + } diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_nf4_nonexpert.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_nf4_nonexpert.py new file mode 100644 index 0000000000..abf337d3f5 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/gemma4_nf4_nonexpert.py @@ -0,0 +1,108 @@ +"""In-place NF4 (bitsandbytes) quantization of gemma4 non-expert linears. + +This mirrors :mod:`gemma4_fp8_nonexpert` but uses bitsandbytes 4-bit NF4 — the +SAME non-expert compute path unsloth's QLoRA uses — so an experts-only LoRA run +can be compared apples-to-apples against unsloth: identical frozen-NF4 non-expert +matmuls, the only difference being the routed-expert path (our NVFP4 +grouped/marlin + scatter LoRA vs unsloth's NF4 experts). + +Targets every ``nn.Linear`` that is NOT inside a ``Gemma4TextExperts`` block +(experts stay NVFP4Tensor, untouched). Norms, embeddings, router projections, +vision/audio towers, and lm_head are skipped by name. Each target becomes a +frozen ``bnb.nn.Linear4bit`` (nf4, double-quant, bf16 compute) — no LoRA is +attached there (experts-only), exactly like unsloth's non-expert state. +""" + +from __future__ import annotations + +import torch +import torch.nn as nn + +from axolotl.utils.logging import get_logger + +from .gemma4_fp8_nonexpert import _should_skip_by_name + +LOG = get_logger(__name__) + + +def _expert_paths(model: nn.Module) -> set[str]: + try: + from transformers.models.gemma4.modeling_gemma4 import Gemma4TextExperts + + cls = Gemma4TextExperts + except ImportError: + cls = None + paths: set[str] = set() + if cls is not None: + for path, mod in model.named_modules(): + if isinstance(mod, cls): + paths.add(path) + return paths + + +def quantize_gemma4_nonexpert_nf4( + model: nn.Module, compute_dtype: torch.dtype = torch.bfloat16 +) -> int: + """Swap all non-expert ``nn.Linear`` modules for frozen bnb NF4 ``Linear4bit`` + (double-quant, bf16 compute), in place. Returns the count swapped. Experts + (NVFP4Tensor) are untouched. Idempotent: existing ``Linear4bit`` are skipped. + """ + import bitsandbytes as bnb + + expert_paths = _expert_paths(model) + + def _under_expert(path: str) -> bool: + return any(path == ep or path.startswith(ep + ".") for ep in expert_paths) + + device = next( + (p.device for p in model.parameters() if p.is_cuda), + torch.device("cuda", torch.cuda.current_device()), + ) + + targets: list[tuple[str, nn.Linear]] = [] + for name, mod in model.named_modules(): + if isinstance(mod, bnb.nn.Linear4bit): + continue + if not isinstance(mod, nn.Linear): + continue + if _under_expert(name) or _should_skip_by_name(name): + continue + if not isinstance(mod.weight, nn.Parameter) or mod.weight.ndim != 2: + continue + targets.append((name, mod)) + + by_type: dict[str, int] = {} + for name, mod in targets: + has_bias = mod.bias is not None + new = bnb.nn.Linear4bit( + mod.in_features, + mod.out_features, + bias=has_bias, + compute_dtype=compute_dtype, + quant_type="nf4", + ) + new.weight = bnb.nn.Params4bit( + mod.weight.data.to(torch.bfloat16), + requires_grad=False, + quant_type="nf4", + compress_statistics=True, + ) + if has_bias: + new.bias = nn.Parameter( + mod.bias.data.to(compute_dtype), requires_grad=False + ) + new = new.to(device) # triggers NF4 quantization of Params4bit + + parent_path, _, attr = name.rpartition(".") + parent = model.get_submodule(parent_path) if parent_path else model + setattr(parent, attr, new) + by_type[type(mod).__name__] = by_type.get(type(mod).__name__, 0) + 1 + + if targets: + LOG.info( + "Gemma4 NF4 frankenstein: swapped %d non-expert linears to bnb NF4 " + "(double-quant, bf16 compute): %s", + len(targets), + by_type, + ) + return len(targets) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/grouped_lora.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/grouped_lora.py new file mode 100644 index 0000000000..508ad3822b --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/grouped_lora.py @@ -0,0 +1,186 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Encapsulated scatter2scatter grouped LoRA ops for the TILE-padded marlin/grouped path. + +Replaces per-expert torch._grouped_mm loops in grouped_train.py with single-launch +scatter2scatter calls. All implementation choices (which kernel variant, dispatch) +live inside this module — the training code calls one function per phase. + +Layout bridge: grouped_train uses a TILE-padded expert-sorted buffer A[Mt, K] with +per-TILE expert ids (m_indices) and cumulative padded offsets (offs). The +scatter2scatter kernels in kernels/ops.py work on the same sorted layout when called +with x_grouped=True / y_grouped=True. + +LoRA weight layout (stacked, from _lora_stack in grouped_train): + A : [E, r, in_K] (A-adapter) + B : [E, out_N, r] (B-adapter) + +scatter2scatter weight layout: W[E, in, out]. Mapping: + W_A = A.permute(0, 2, 1) -> [E, in_K, r] for X @ A^T step + W_B = B.permute(0, 2, 1) -> [E, r, out_N] for XA @ B^T step + W_Bt = B -> [E, out_N, r] for dY @ B step (B already [E, out_N, r]) +""" + +from __future__ import annotations + +import torch + +from .kernels.grouped_gram import grouped_lora_weight_grads +from .kernels.ops import scatter2scatter + + +def _mk_routing(offs: torch.Tensor, E: int) -> tuple[torch.Tensor, torch.Tensor]: + """Build scatter2scatter routing tensors from the TILE-padded grouped layout. + + Args: + offs: cumulative TILE-padded expert offsets [E] (int32), as produced by + grouped_fp4_moe_train — offs[e] = sum of tile-padded row counts + for experts 0..e. + E: number of experts. + + Returns: + (sorted_expert_idxs [Mt], sorted_scattered_idxs [Mt]) + sorted_expert_idxs[i] = expert id for row i of the grouped buffer. + sorted_scattered_idxs = identity [0..Mt-1]; with x_grouped=y_grouped=True + the scatter2scatter kernel reads/writes M_block directly and ignores it + for X/Y addressing, but it must have the right length. + """ + Mt = int(offs[-1].item()) + dev = offs.device + starts = torch.cat([offs.new_zeros(1), offs[:-1]]) + sizes = offs - starts # per-expert padded row count + e_ids = torch.arange(E, device=dev, dtype=torch.int32) + sorted_expert_idxs = e_ids.repeat_interleave(sizes) + sorted_scattered_idxs = torch.arange(Mt, device=dev, dtype=torch.int32) + return sorted_expert_idxs, sorted_scattered_idxs + + +def grouped_lora_fwd( + x: torch.Tensor, # [Mt, in_K], expert-grouped (TILE-padded) + A: torch.Tensor, # [E, r, in_K] + B: torch.Tensor, # [E, out_N, r] + scaling: float, + offs: torch.Tensor, # [E] int32 cumulative TILE-padded offsets + E: int, + residual: torch.Tensor + | None = None, # [Mt, out_N] base output to fold into the epilogue +) -> tuple[torch.Tensor, torch.Tensor]: + """LoRA forward on the TILE-padded grouped layout. + + Computes Y_lora = scaling * (X @ A^T) @ B^T, row-for-row with the marlin + base output. Two scatter2scatter launches with x_grouped=y_grouped=True keep + the result in the padded grouped layout for direct in-place addition to base. + + If ``residual`` is given (the base expert GEMM output), it is added in the LoRA-B GEMM epilogue, + so the returned tensor is ``base + scaling*lora`` with NO separate add pass or temp tensor. + + Returns: + (y [Mt, out_N], xa [Mt, r]) — y = (residual + scaling*lora) if residual else scaling*lora; + xa is saved (unscaled) for the backward dB/dA. + """ + sei, ssi = _mk_routing(offs, E) + + W_A = A.permute(0, 2, 1).contiguous() # [E, in_K, r] + W_B = B.permute(0, 2, 1).contiguous() # [E, r, out_N] + + xa = scatter2scatter( + X=x, + W=W_A, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=1, + x_grouped=True, + y_grouped=True, + ) # [Mt, r] + + # Scale the tiny [Mt, r] inner activation, not the large [Mt, out_N] output (identical result, + # far fewer elements). xa is returned UNSCALED; the backward derives scaling separately. + y_lora = scatter2scatter( + X=xa * scaling, + W=W_B, + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=1, + x_grouped=True, + y_grouped=True, + residual=residual, + ) # [Mt, out_N], = (residual + scaling*lora) if residual else scaling*lora + return y_lora, xa + + +def grouped_lora_bwd( + dy: torch.Tensor, # [Mt, out_N], grad w.r.t. LoRA output, grouped + x: torch.Tensor, # [Mt, in_K], forward input, grouped + A: torch.Tensor, # [E, r, in_K] + B: torch.Tensor, # [E, out_N, r] + xa: torch.Tensor, # [Mt, r], X@A^T from forward (saved) + scaling: float, + offs: torch.Tensor, # [E] int32 cumulative TILE-padded offsets + E: int, + residual: torch.Tensor + | None = None, # [Mt, in_K] base dX to fold into the dX_lora epilogue +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """LoRA backward on the TILE-padded grouped layout. + + Computes: + dX_lora = scaling * (dY @ B) @ A [Mt, in_K] + dA [E, r, in_K] + dB [E, out_N, r] + + If ``residual`` is given (the base dX), it is added in the dX_lora GEMM epilogue, so the returned + dx is ``base_dX + scaling*lora_dX`` with no separate add pass. + + Returns: + (dx, dA, dB) — dx = (residual + scaling*lora_dX) if residual else scaling*lora_dX + """ + sei, ssi = _mk_routing(offs, E) + + r = A.size(1) + in_K = A.size(2) + out_N = B.size(1) + + # yb = dY @ B; B is already [E, out_N, r] = [E, K=out_N, N=r] + yb = scatter2scatter( + X=dy, + W=B.contiguous(), + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=1, + x_grouped=True, + y_grouped=True, + ) # [Mt, r] + + # dx_lora = scaling * yb @ A; A is [E, r, in_K] = [E, K=r, N=in_K]. Fold `scaling` into the tiny + # [Mt, r] yb, not the large [Mt, in_K] output (identical result, far fewer elements). + dx_lora = scatter2scatter( + X=yb * scaling, + W=A.contiguous(), + sorted_expert_idxs=sei, + sorted_scattered_idxs=ssi, + k=1, + x_grouped=True, + y_grouped=True, + residual=residual, + ) # [Mt, in_K], = (residual + scaling*lora) if residual else scaling*lora + + # grouped-Gram kernel expects flat scattermoe layout: lora_A [r*E, in_K], lora_B [out_N, r*E] + lora_A_flat = A.reshape(E * r, in_K) + lora_B_flat = B.permute(1, 0, 2).reshape(out_N, E * r) + + dA_flat, dB_flat = grouped_lora_weight_grads( + grouped_grad_out=dy, + grouped_x=x, + yb=yb, + xa=xa, + lora_A=lora_A_flat, + lora_B=lora_B_flat, + combined_offsets=offs, + e_total=E, + scaling=scaling, + ) + dA = dA_flat.reshape(E, r, in_K) + dB = dB_flat.reshape(out_N, E, r).permute(1, 0, 2).contiguous() + + return dx_lora, dA, dB diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/grouped_moe.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/grouped_moe.py new file mode 100644 index 0000000000..33aee70a9b --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/grouped_moe.py @@ -0,0 +1,201 @@ +"""Unified grouped NVFP4 MoE forward for DeepSeek-V4 experts (config-gated, dsv4_fp4_grouped_mode). + +Contiguous-grouped: tokens sorted by expert, padded per-expert to TILE; ONE grouped gate_up GEMM +-> clamped-SwiGLU -> ONE grouped down GEMM, with GPU-vectorized routing/pack/scatter (no per-expert +Python loop). The base GEMM auto-dispatches to the best fp4 path: + Marlin W4A16 (sm120, pad-64) -> DeepGEMM (sm90/sm100) -> CUTLASS grouped (sm120, pad-128) + -> chunked-dequant (any GPU, fallback). +Marlin W4A16 (bf16 activations, bit-correct) is preferred on sm120: ~1.79x faster than CUTLASS and +no activation-quant error. See marlin_w4a16/. + +OFF unless cfg.dsv4_fp4_grouped_mode is set; existing fused-Triton/eager paths untouched. +Bench (RTX PRO 6000, H=4096 I=2048 top6, FORWARD): vs chunked-dequant E=32 1.6-1.9x, E=256 +3.5-3.7x (single-launch grouped vs chunked's per-chunk loop). + +STATUS: forward path (base + clamped-swiglu + routing) implemented + validated. TRAINING is the +remaining piece — the cutlass/deepgemm engines are forward-only, so dX (hidden grad) and +LoRA-on-experts backward need autograd.Function wrappers (tasks #14, #18, #19). Until then this +path is correct for inference/eval; training still uses the fused-Triton path. +""" + +from __future__ import annotations + +import torch +import torch.nn.functional as F + +TILE = 128 +_ENGINE_CACHE: dict = {} + + +def grouped_fp4_backend(mode: str) -> str | None: + """Best available base-GEMM backend: 'marlin' (sm120 W4A16, preferred) | 'cutlass' (sm120) | + 'deepgemm' (sm90/100) | 'chunked'. On sm120 Marlin W4A16 is ~1.79x faster than CUTLASS and + bit-correct (bf16 activations, no activation quantization).""" + try: + from .marlin_w4a16 import marlin_w4a16_available + + if mode == "nvfp4" and marlin_w4a16_available(): + return "marlin" + except Exception: + pass + try: + from .cutlass_fp4 import cutlass_fp4_available + + if mode == "nvfp4" and cutlass_fp4_available(): + return "cutlass" + except Exception: + pass + try: + from .dequant_grouped import deepgemm_grouped_available + + if deepgemm_grouped_available(): + return "deepgemm" + except Exception: + pass + return "chunked" if torch.cuda.is_available() else None + + +def _route(idx, E, dev, tile=TILE): + flat = idx.reshape(-1) + order = flat.argsort() + rep = torch.arange(idx.size(0), device=dev).repeat_interleave(idx.size(1))[order] + exp_sorted = flat[order] + counts = torch.bincount(flat, minlength=E) + ptiles = (counts + tile - 1) // tile + roff = torch.cat([ptiles.new_zeros(1), ptiles.cumsum(0)]) * tile + coff = torch.cat([counts.new_zeros(1), counts.cumsum(0)]) + local = torch.arange(exp_sorted.numel(), device=dev) - coff[exp_sorted] + padded_row = roff[exp_sorted] + local + m_indices = torch.repeat_interleave( + torch.arange(E, dtype=torch.int32, device=dev), ptiles + ) + Mt = int(ptiles.sum()) * tile + return rep, padded_row, m_indices, counts, Mt + + +def _engine(Mt, N, K, E, mode): + key = (Mt, N, K, E, mode) + eng = _ENGINE_CACHE.get(key) + if eng is None: + from .cutlass_fp4.grouped import GroupedFp4Gemm + + _ENGINE_CACHE[key] = eng = GroupedFp4Gemm(Mt, N, K, E, mode) + return eng + + +def _quant_weight(W_nv, mode): + """Per-expert weight quant for the grouped engines. W_nv: NVFP4Tensor [E,N,K].""" + E = W_nv.qdata.size(0) + if mode == "nvfp4": + from .cutlass_fp4.quant_nvfp4 import nvfp4_quant + + deq = W_nv.dequantize(torch.bfloat16) + qs = [nvfp4_quant(deq[e].contiguous()) for e in range(E)] + return torch.stack([q for q, _ in qs]), torch.stack([s for _, s in qs]) + from torchao.prototype.mx_formats.mx_tensor import MXTensor + + deq = W_nv.dequantize(torch.bfloat16) + ts = [ + MXTensor.to_mx(deq[e].contiguous(), torch.float4_e2m1fn_x2, 32) + for e in range(E) + ] + return torch.stack([t.qdata for t in ts]), torch.stack([t.scale for t in ts]) + + +@torch.no_grad() +def grouped_fp4_moe_forward( + hidden, idx, wts, gate_up_nv, down_nv, limit, mode, backend=None, act_type="silu" +): + """Forward-only grouped NVFP4 MoE. hidden[N,H], idx/wts[N,topk], experts NVFP4Tensor. + + Returns [N,H]. backend auto-selected if None. TRAINING NOT YET (forward-only engines). + act_type: 'silu' (DSV4 clamped SwiGLU, default) or 'gelu_tanh' (Gemma4 GeGLU, no clamp). + """ + N, H = hidden.shape + E = gate_up_nv.qdata.size(0) + _Idim = down_nv.qdata.size(2) * 2 # down K = I (packed K/2) + dev = hidden.device + backend = backend or grouped_fp4_backend(mode) + if backend == "marlin": + from .marlin_w4a16.backend import MARLIN_TILE + + tile = MARLIN_TILE + else: + tile = TILE + rep, padded_row, m_indices, counts, Mt = _route(idx, E, dev, tile) + wflat = wts.reshape(-1)[idx.reshape(-1).argsort()] + + A = hidden.new_zeros(Mt, H) + A[padded_row] = hidden[rep] + + marlin_base = None + if backend == "marlin": + from .marlin_w4a16.backend import build_marlin_forward_base, marlin_base_forward + + marlin_base = build_marlin_forward_base(gate_up_nv, down_nv) + gu = marlin_base_forward(marlin_base, 0, A, m_indices).float() + elif backend == "cutlass": + from .cutlass_fp4.grouped import quant_act + + gu_eng = _engine(Mt, 2 * (down_nv.qdata.size(2) * 2), H, E, mode) # N = 2I + gu_eng.set_weights(*_quant_weight(gate_up_nv, mode)) + aq, as_ = quant_act(A, mode) + gu = gu_eng.forward(aq.unsqueeze(0), as_.unsqueeze(0), m_indices).float() + elif ( + backend == "deepgemm" + ): # native fp8(act) x mxfp4(weight) grouped GEMM (SM90/SM100) + from .dequant_grouped import _cached_mxfp4, deepgemm_grouped_fp8_fp4 + + wq, ws = _cached_mxfp4(gate_up_nv, _per_tensor(gate_up_nv, E)) + gu = deepgemm_grouped_fp8_fp4( + A, wq, ws, m_indices.repeat_interleave(TILE) + ).float() + else: # chunked fallback: dequant weight -> grouped bf16 matmul + from .dequant_grouped import nvfp4_dequant_bf16 + + pt = _per_tensor(gate_up_nv, E) + Wb = nvfp4_dequant_bf16(gate_up_nv.qdata, gate_up_nv.scale, pt) + offs = (torch.bincount(m_indices, minlength=E) * TILE).cumsum(0).to(torch.int32) + gu = torch._grouped_mm(A, Wb.transpose(1, 2), offs=offs).float() + + g, u = gu.chunk(2, dim=-1) + if act_type == "gelu_tanh": + h = (F.gelu(g, approximate="tanh") * u).to(hidden.dtype) + else: + h = (F.silu(g.clamp(max=limit)) * u.clamp(min=-limit, max=limit)).to( + hidden.dtype + ) + + if backend == "marlin": + dn = marlin_base_forward(marlin_base, 1, h.contiguous(), m_indices) + elif backend == "cutlass": + from .cutlass_fp4.grouped import quant_act + + dn_eng = _engine(Mt, H, down_nv.qdata.size(2) * 2, E, mode) # K = I + dn_eng.set_weights(*_quant_weight(down_nv, mode)) + hq, hs = quant_act(h.contiguous(), mode) + dn = dn_eng.forward(hq.unsqueeze(0), hs.unsqueeze(0), m_indices) + elif backend == "deepgemm": + from .dequant_grouped import _cached_mxfp4, deepgemm_grouped_fp8_fp4 + + wq, ws = _cached_mxfp4(down_nv, _per_tensor(down_nv, E)) + dn = deepgemm_grouped_fp8_fp4( + h.contiguous(), wq, ws, m_indices.repeat_interleave(TILE) + ) + else: + from .dequant_grouped import nvfp4_dequant_bf16 + + pt = _per_tensor(down_nv, E) + Wb = nvfp4_dequant_bf16(down_nv.qdata, down_nv.scale, pt) + offs = (torch.bincount(m_indices, minlength=E) * TILE).cumsum(0).to(torch.int32) + dn = torch._grouped_mm(h.contiguous(), Wb.transpose(1, 2), offs=offs) + + out = hidden.new_zeros(N, H) + return out.index_add( + 0, rep, (dn[padded_row] * wflat[:, None].to(dn.dtype)).to(out.dtype) + ) + + +def _per_tensor(w_nv, E): + pt = getattr(w_nv, "per_tensor_scale", None) + return torch.ones(E, device="cuda") if pt is None else pt.reshape(-1).float() diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/grouped_train.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/grouped_train.py new file mode 100644 index 0000000000..a40b560529 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/grouped_train.py @@ -0,0 +1,584 @@ +"""Training-capable grouped NVFP4 MoE (fwd+bwd) for DeepSeek-V4 experts — the config-gated +cutlass-fp4 path. Forward: cutlass fp4 grouped GEMM (fast). Backward: chunked bf16-dequant + +cuBLAS grouped_mm (accurate, bounded memory) — this beat the existing fused Triton kernel at the +real E=256 scale on BOTH speed (2.24x) and memory (0.87x). LoRA-on-experts fused via +scatter2scatter single-launch grouped GEMMs. All cute imports lazy (module loads clean off sm120). + +Backward design rationale (explored exhaustively): bf16 grad is REQUIRED (fp8 grad = 33% error); +dequant+cuBLAS beats the fused in-kernel-decode Triton dX 5.7x (Triton GEMM << cuBLAS); chunking +over expert-groups bounds the bf16-weight transient. + +LoRA GEMM design: scatter2scatter kernels (kernels/ops.py) operate directly on the TILE-padded +expert-sorted layout with x_grouped=y_grouped=True — single launch, ragged-native, no padding +transient. grouped_lora_fwd/bwd in grouped_lora.py encapsulate the entire implementation; +no selection or threshold logic lives here. +""" + +from __future__ import annotations + +import torch + +from axolotl.utils.logging import get_logger + +from .grouped_lora import grouped_lora_bwd, grouped_lora_fwd + +LOG = get_logger(__name__) + +_BACKEND_LOGGED = False # one-time log of the resolved base-GEMM backend + +# thin compat wrapper over the centralized runtime module. grouped_backend (cfg.moe_grouped_backend): +# None/"auto" = capability auto-select; marlin|cutlass|deepgemm = force if available (else warn + +# auto); "dequant" = force the chunked-dequant fallback. +from .runtime import RUNTIME # noqa: E402 + + +def set_grouped_backend_override(backend) -> None: + RUNTIME.grouped_backend = str(backend).lower() if backend else None + + +def _backend_available(name: str) -> bool: + try: + if name == "marlin": + from .marlin_w4a16 import marlin_w4a16_available + + return marlin_w4a16_available() + if name == "cutlass": + from .cutlass_fp4 import cutlass_fp4_available + + return cutlass_fp4_available() + if name == "deepgemm": + from .dequant_grouped import deepgemm_grouped_available + + return deepgemm_grouped_available() + except Exception: + return False + return False + + +def _auto_backend() -> str | None: + """Capability + arch auto-select. Order is arch-aware so each GPU class gets its tuned default + while Marlin (the only fused W4A16 path that runs on Ampere/Ada) backs up everything: + - sm120 (consumer Blackwell): Marlin (~1.79x CUTLASS, bit-correct) > CUTLASS > DeepGEMM + - sm100 (datacenter Blackwell, e.g. B200): DeepGEMM (tuned fp8-act x mxfp4) > Marlin + - sm90 (Hopper) / sm80 / sm89 (Ampere / Ada): Marlin only (the DeepGEMM fp8xfp4 grouped + kernel is sm100-only; deepgemm_grouped_available() returns False below sm100 -> Marlin) + Marlin stays force-selectable everywhere via cfg.moe_grouped_backend.""" + import torch + + major = torch.cuda.get_device_capability()[0] if torch.cuda.is_available() else 0 + order: tuple[str, ...] + if major >= 11: + order = ("marlin", "cutlass", "deepgemm") + elif major in (9, 10): + order = ("deepgemm", "marlin") + else: + order = ("marlin",) + for name in order: + if _backend_available(name): + return name + return None + + +TILE = 128 +# backward base-dX dequant chunk; bounds the bf16-weight transient. +# E=256 knee (B200, BK=1024 dequant): CHUNK_E=16 = 11.2ms bwd / 2.6GB peak vs 10.1ms floor. +CHUNK_E = 16 +_ENGINES: dict = {} + + +def grouped_fp4_available(mode: str) -> bool: + """True iff the grouped fp4 training path can run here for `mode`: nvfp4 on sm120 (CUTLASS + fused-decode) or sm90/sm100 (DeepGEMM fp8-act x mxfp4-weight). Backward is GPU-agnostic + (chunked bf16-dequant). chunked-only fallback tracked separately.""" + if mode != "nvfp4": + return False + try: + from .marlin_w4a16 import marlin_w4a16_available + + if marlin_w4a16_available(): + return True + except Exception: + pass + try: + from .cutlass_fp4 import cutlass_fp4_available + + if cutlass_fp4_available(): + return True + except Exception: + pass + try: + from .dequant_grouped import deepgemm_grouped_available + + return deepgemm_grouped_available() + except Exception: + return False + + +def _train_backend(mode: str) -> str | None: + """Base-GEMM backend for the training forward: 'marlin' (sm120, W4A16 bf16-act — preferred) | + 'cutlass' (sm120, W4A4 fp4-act) | 'deepgemm' (sm90/100), or None for the chunked-dequant + fallback. Auto-selects by capability unless cfg.moe_grouped_backend forced one (see + set_grouped_backend_override): an unavailable forced backend warns and falls back to auto; + 'dequant' forces the fallback (None).""" + if mode != "nvfp4": + return None + override = RUNTIME.grouped_backend + if override and override != "auto": + if override == "dequant": + return None # chunked-dequant fallback + if _backend_available(override): + return override + LOG.warning( + "moe_grouped_backend=%r is not available on this GPU; falling back to auto-select.", + override, + ) + return _auto_backend() + + +def _gmm(a, b, offs): + return torch._grouped_mm(a, b, offs=offs) + + +def _swiglu(gu, limit, act_type="silu"): + from .cutlass_fp4.swiglu import swiglu_fwd + + return swiglu_fwd(gu, limit, act_type) + + +def _swiglu_bwd(dh, gu, limit, act_type="silu"): + from .cutlass_fp4.swiglu import swiglu_bwd + + return swiglu_bwd(gu, dh, limit, act_type) + + +def _base_forward(base, which, x, m_indices, mode): + """Frozen-expert base GEMM for gate_up (which=0) or down (which=1). `base` is + ('marlin', (gu_w, dn_w), dims, ws) | ('cutlass', gu_eng, dn_eng) | ('deepgemm', (guq,gus), (dnq,dns)).""" + if base[0] == "marlin": + from .marlin_w4a16.backend import marlin_base_forward + + return marlin_base_forward(base, which, x, m_indices) + backend, gw, dw = base + if backend == "cutlass": + from .cutlass_fp4.grouped import quant_act + + eng = gw if which == 0 else dw + aq, as_ = quant_act(x, mode) + return eng.forward(aq.unsqueeze(0), as_.unsqueeze(0), m_indices) + from .dequant_grouped import deepgemm_grouped_fp8_fp4 + + wq, ws = gw if which == 0 else dw + # cutlass uses per-tile m_indices; DeepGEMM's grouped_layout is per-row (length Mt) + return deepgemm_grouped_fp8_fp4(x, wq, ws, m_indices.repeat_interleave(TILE)) + + +def _engine(Mt, N, K, E, mode): + key = (Mt, N, K, E, mode) + eng = _ENGINES.get(key) + if eng is None: + from .cutlass_fp4.grouped import GroupedFp4Gemm + + _ENGINES[key] = eng = GroupedFp4Gemm(Mt, N, K, E, mode) + return eng + + +def _fp8_read_dx_ok(): + """fp8-read backward dX (#3744) wins on sm120 (bandwidth-bound: half weight bytes -> ~1.5x + + half memory). On sm100 it's speed-neutral (cuBLAS bf16 is fast) so keep the bf16 path there.""" + if not torch.cuda.is_available(): + return False + return torch.cuda.get_device_capability()[0] == 12 + + +def _base_dx( + w_nv, pt, g, out_k, offs, m_indices, prefer_fp8=True, tile=TILE, _marlin_raw=None +): + """Chunked base-weight contraction g @ W (frozen NVFP4 experts). bf16 path: dequant CHUNK_E + experts to bf16 + cuBLAS grouped_mm. fp8-read path (sm120): dequant to fp8 (half bytes) + a + Triton grouped GEMM that reads fp8 and upcasts in-register (~1.5x faster, half the transient). + g[Mt,N], W[E,N,out_k] -> [Mt,out_k]; m_indices is the per-`tile` expert id (for the fp8 GEMM). + `tile` is the routing pad granularity (128 cutlass, 64 marlin) and is passed to the fp8 dX + kernel as its row-block (BM) so one expert maps to each block. Both fp8-read and bf16-dequant + are gradient-consistent (they dequant the FORWARD weight); fp8-read is the faster default. + _marlin_raw: optional (qw_flat_E, orig_scale, pt_E) for the marlin memory-free path — reads from + the marlin qweight cache via fused Triton dequant instead of the freed nv.qdata.""" + from .dequant_grouped import nvfp4_dequant_bf16 + + fp8 = _fp8_read_dx_ok() and prefer_fp8 + if fp8: + from .dequant_grouped import grouped_dx_fp8, nvfp4_dequant_fp8 + + if _marlin_raw is not None: + # Marlin memory-free path: dequant from marlin int32 layout via fused Triton kernel. + # _marlin_raw = (qw [E, words_per_expert] int32, orig_scale [E, N, K//16] fp8, pt [E] f32) + from .marlin_w4a16.backend import _build_base_scatter + from .marlin_w4a16.fused_dequant import marlin_dequant_bf16, marlin_dequant_fp8 + from .mx_weights import fp4_codebook + + qw, orig_scale, pt_e = _marlin_raw + # pt_e may have stride(0) from .expand(); Triton needs element-stride=1 for a correct load. + if pt_e.stride(0) == 0: + pt_e = pt_e.contiguous() + E, N, Kg = orig_scale.shape + K = Kg * 16 + scatter_lut = _build_base_scatter(qw.device) + cb = fp4_codebook(qw.device).float() + starts = torch.cat([offs.new_zeros(1), offs]).tolist() + out = g.new_empty(g.size(0), out_k) + for c0 in range(0, E, CHUNK_E): + c1 = min(c0 + CHUNK_E, E) + t0, t1 = starts[c0], starts[c1] + if t1 == t0: + continue + C = c1 - c0 + if fp8: + Wc = marlin_dequant_fp8( + qw[c0:c1].reshape(C, -1), + orig_scale[c0:c1], + pt_e[c0:c1], + scatter_lut, + cb, + N, + K, + C, + ) + mi = (m_indices[t0 // tile : t1 // tile] - c0).to(torch.int32) + out[t0:t1] = grouped_dx_fp8(g[t0:t1], Wc, mi, tile) + else: + Wc = marlin_dequant_bf16( + qw[c0:c1].reshape(C, -1), + orig_scale[c0:c1], + pt_e[c0:c1], + scatter_lut, + cb, + N, + K, + C, + ) + loc = (offs[c0:c1] - t0).to(torch.int32) + out[t0:t1] = _gmm(g[t0:t1], Wc, loc) + return out + + qdata, scale = w_nv.qdata, w_nv.scale + E = qdata.size(0) + starts = torch.cat([offs.new_zeros(1), offs]).tolist() # one sync, not per-iter + out = g.new_empty(g.size(0), out_k) + for c0 in range(0, E, CHUNK_E): + c1 = min(c0 + CHUNK_E, E) + t0, t1 = starts[c0], starts[c1] + if t1 == t0: + continue + if fp8: + Wc = nvfp4_dequant_fp8(qdata[c0:c1], scale[c0:c1], pt[c0:c1]) + mi = (m_indices[t0 // tile : t1 // tile] - c0).to(torch.int32) + out[t0:t1] = grouped_dx_fp8(g[t0:t1], Wc, mi, tile) + else: + loc = (offs[c0:c1] - t0).to(torch.int32) + Wc = nvfp4_dequant_bf16(qdata[c0:c1], scale[c0:c1], pt[c0:c1]) + out[t0:t1] = _gmm(g[t0:t1], Wc, loc) + return out + + +def _pt(nv, E, dev): + p = getattr(nv, "per_tensor_scale", None) + if p is None: + return torch.ones(E, device=dev) + p = p.reshape(-1).float() + return p.expand(E) if p.numel() == 1 else p + + +def _has_nonunit_pt(*nvs) -> bool: + """True iff any NVFP4 weight carries a non-unit per_tensor_scale (weight_scale_2). The cutlass + forward (set_weights) folds only the E4M3 block scale, dropping weight_scale_2, while the + backward applies it, giving a wrong forward + grad mismatch when it != 1 (the real DSV4 ckpt). + Folding weight_scale_2 into the cutlass weight + saved swiglu input is unimplemented, so the + backend selection skips cutlass in that case.""" + for nv in nvs: + p = getattr(nv, "per_tensor_scale", None) + if p is None: + continue + if not torch.allclose( + p.reshape(-1).float(), torch.ones((), device=p.device, dtype=torch.float32) + ): + return True + return False + + +class _GroupedExperts(torch.autograd.Function): + """x[Mt,H] -> gate_up(cutlass fp4 base + LoRA) -> gated-activation -> down(...) -> [Mt,H]. + Frozen NVFP4 experts; trainable LoRA A/B (stacked [E,r,K]/[E,N,r]). + act_type: 'silu' (DSV4 clamped SwiGLU) or 'gelu_tanh' (Gemma4 GeGLU).""" + + @staticmethod + def forward( + ctx, + x, + base, + weight_recipe, + Agu, + Bgu, + Adn, + Bdn, + m_indices, + offs, + scaling, + limit, + mode, + act_type, + prefer_fp8_dx=True, + ): + E = Agu.size(0) + gu = _base_forward(base, 0, x, m_indices, mode) + # LoRA-B GEMM folds the base output in via residual=gu -> gu = base + lora, no separate add. + gu, xAg = grouped_lora_fwd(x, Agu, Bgu, scaling, offs, E, residual=gu) + h = _swiglu(gu, limit, act_type).to(x.dtype) + dn = _base_forward(base, 1, h, m_indices, mode) + dn, hAd = grouped_lora_fwd(h, Adn, Bdn, scaling, offs, E, residual=dn) + ctx.save_for_backward(x, Agu, Bgu, Adn, Bdn, offs, gu, h, xAg, hAd, m_indices) + # FSDP-safe: don't pin the gathered NVFP4 weight; re-read the (re-gathered) param in backward + ctx.weight_recipe, ctx.scaling, ctx.limit = weight_recipe, scaling, limit + ctx.act_type = act_type + # base dX: fp8-read (fast, ~2% grad) vs bf16-dequant (~0.5%) + ctx.prefer_fp8_dx = prefer_fp8_dx + # Marlin memory-free path: build_marlin_forward_base freed nv.qdata and saved (qdata, scale, + # pt) in the cache (single-GPU only); stash them so backward skips weight_recipe() (which + # would return the emptied NVFP4 tensor). + ctx.bwd_marlin = None + if base[0] == "marlin": + from .marlin_w4a16.backend import marlin_bwd_data + + ctx.bwd_marlin = marlin_bwd_data(base) + return dn + + @staticmethod + def backward(ctx, d_dn): + x, Agu, Bgu, Adn, Bdn, offs, gu, h, xAg, hAd, m_indices = ctx.saved_tensors + s, lim, act_type = ctx.scaling, ctx.limit, ctx.act_type + pf8 = _fp8_read_dx_ok() and ctx.prefer_fp8_dx + tile = ( + x.size(0) // m_indices.numel() + ) # routing pad granularity (128 cutlass, 64 marlin) + d_dn = d_dn.contiguous().to(x.dtype) + + if ctx.bwd_marlin is not None: + # Marlin memory-free path: nv.qdata was freed after repack; backward reads from the + # marlin qweight cache via fused Triton dequant (marlin int32 + original scales). + # bwd_marlin = ((gu_qw, (gu_scale, gu_pt, E, N, K)), (dn_qw, (dn_scale, ...))) + (gu_qw, gu_bwd), (dn_qw, dn_bwd) = ctx.bwd_marlin + dn_scale, dn_pt_e, _, _, _ = dn_bwd + dn_marlin_raw = (dn_qw, dn_scale, dn_pt_e) + dh = _base_dx( + None, + None, + d_dn, + h.size(1), + offs, + m_indices, + pf8, + tile, + _marlin_raw=dn_marlin_raw, + ) + else: + gu_nv, dn_nv = ctx.weight_recipe() + E, dev = gu_nv.qdata.size(0), x.device + ptg, ptd = _pt(gu_nv, E, dev), _pt(dn_nv, E, dev) + dh = _base_dx(dn_nv, ptd, d_dn, h.size(1), offs, m_indices, pf8, tile) + + E = Agu.size(0) + # dX_lora GEMM folds base dh in via residual=dh -> dh = base_dX + lora_dX. + dh, dAdn, dBdn = grouped_lora_bwd( + d_dn, h, Adn, Bdn, hAd, s, offs, E, residual=dh + ) + dgu = _swiglu_bwd(dh, gu, lim, act_type).to(x.dtype) + del dh + + if ctx.bwd_marlin is not None: + gu_scale, gu_pt_e, _, _, _ = ctx.bwd_marlin[0][1] + gu_marlin_raw = (gu_qw, gu_scale, gu_pt_e) + dx = _base_dx( + None, + None, + dgu, + x.size(1), + offs, + m_indices, + pf8, + tile, + _marlin_raw=gu_marlin_raw, + ) + else: + dx = _base_dx(gu_nv, ptg, dgu, x.size(1), offs, m_indices, pf8, tile) + + dx, dAgu, dBgu = grouped_lora_bwd( + dgu, x, Agu, Bgu, xAg, s, offs, E, residual=dx + ) + # grads align to forward args: x, base, weight_recipe, Agu, Bgu, Adn, Bdn, + # m_indices, offs, scaling, limit, mode, act_type, prefer_fp8_dx + return ( + dx, + None, + None, + dAgu, + dBgu, + dAdn, + dBdn, + None, + None, + None, + None, + None, + None, + None, + ) + + +def _lora_stack(lora, E, K, out): + """scattermoe LoRA (A[r*E,K], B[out,r*E], scaling) -> stacked (Agu[E,r,K], Bgu[E,out,r], s).""" + A, B, scaling = lora + r = A.shape[0] // E + As = A.reshape(E, r, K).contiguous() + Bs = B.reshape(out, E, r).permute(1, 0, 2).contiguous() + return As, Bs, float(scaling) + + +def grouped_fp4_moe_train( + hidden, + idx, + wts, + gate_up_nv, + down_nv, + gup_lora, + down_lora, + limit, + mode, + act_type="silu", + weight_recipe=None, + mxfp4_cache=None, + prefer_fp8_dx=True, +): + """Training-capable grouped NVFP4 MoE forward. hidden[N,H], idx/wts[N,topk]; experts NVFP4Tensor; + *_lora = (A,B,scaling) scattermoe layout. Returns [N,H]; differentiable to hidden + LoRA A/B. + act_type: 'silu' (DSV4 clamped SwiGLU, default) or 'gelu_tanh' (Gemma4 GeGLU, no clamp). + weight_recipe: optional callable -> (gate_up_nv, down_nv) re-read for the FSDP-safe backward + (defaults to the forward tensors). mxfp4_cache: optional persistent dict (e.g. on the owning + module) so the DeepGEMM backend requantizes the frozen weight once across FSDP re-gathers. + prefer_fp8_dx: base dX backward — True = fp8-read (fast, ~2% grad error, sm120 default); + False = bf16-dequant (slower, ~0.5%, max gradient fidelity).""" + if weight_recipe is None: + weight_recipe = lambda: (gate_up_nv, down_nv) # noqa: E731 + N, H = hidden.shape + # NVFP4Tensor.shape reports full-precision dims and survives the marlin qdata-free path (qdata + # replaced with empty tensor). SimpleNamespace mocks (unit tests) lack .shape, so fall through + # to the qdata.size() branch. + _qd = gate_up_nv.qdata + if _qd.numel() > 0: + E = _qd.size(0) + twoI = _qd.size(1) + I = down_nv.qdata.size(2) * 2 # noqa: E741 (down K dim, packed K/2 * 2) + else: + # Marlin qdata-free path: qdata was freed; use the NVFP4Tensor wrapper shape. + _gu_shape = gate_up_nv.shape + E, twoI = int(_gu_shape[0]), int(_gu_shape[1]) + I = int(down_nv.shape[2]) # noqa: E741 + dev = hidden.device + backend = _train_backend(mode) + # cutlass weight_scale_2 (per_tensor_scale) folding is unimplemented: set_weights drops it on the + # forward while the backward applies it, giving a wrong forward + grad mismatch when it != 1. + # Marlin and DeepGEMM both fold _pt() into the weight, so prefer one of those (else hard-error) + # for non-unit scales. + if backend == "cutlass" and _has_nonunit_pt(gate_up_nv, down_nv): + for _alt in ("marlin", "deepgemm"): + if _backend_available(_alt): + backend = _alt + break + else: + major = ( + torch.cuda.get_device_capability()[0] + if torch.cuda.is_available() + else 0 + ) + raise RuntimeError( + "grouped NVFP4 MoE: cutlass was selected but the weight has a non-unit " + "per_tensor_scale (weight_scale_2) that the cutlass forward cannot fold " + f"(unimplemented). No weight_scale_2-correct backend resolved on sm{major}x " + "(marlin/deepgemm unavailable). Run on a GPU with marlin or deepgemm, or " + "requantize the experts with a unit per_tensor_scale." + ) + if not _BACKEND_LOGGED: + globals()["_BACKEND_LOGGED"] = True + LOG.info( + "grouped fp4 MoE base-GEMM backend: %s", + backend or "cutlass", + ) + # Marlin (sm120 W4A16) pads to 64, half CUTLASS's 128 at thin-M (the padding is the cost since + # each expert weight is read once either way), and its bf16-act kernel is bit-correct + faster. + if backend == "marlin": + from .marlin_w4a16.backend import MARLIN_TILE + + tile = MARLIN_TILE + else: + tile = TILE + flat = idx.reshape(-1) + order = flat.argsort() + rep = torch.arange(N, device=dev).repeat_interleave(idx.size(1))[order] + wflat = wts.reshape(-1)[order] + exp_sorted = flat[order] + counts = torch.bincount(flat, minlength=E) + ptiles = (counts + tile - 1) // tile + roff = torch.cat([ptiles.new_zeros(1), ptiles.cumsum(0)]) * tile + coff = torch.cat([counts.new_zeros(1), counts.cumsum(0)]) + padded_row = roff[exp_sorted] + ( + torch.arange(exp_sorted.numel(), device=dev) - coff[exp_sorted] + ) + m_indices = torch.repeat_interleave( + torch.arange(E, dtype=torch.int32, device=dev), ptiles + ) + offs = (ptiles * tile).cumsum(0).to(torch.int32) + Mt = int(ptiles.sum()) * tile + + if backend == "marlin": + from .marlin_w4a16.backend import build_marlin_forward_base + + base = build_marlin_forward_base(gate_up_nv, down_nv, mxfp4_cache) + elif backend == "deepgemm": + from .dequant_grouped import _cached_mxfp4 + + base = ( + "deepgemm", + _cached_mxfp4(gate_up_nv, _pt(gate_up_nv, E, dev), mxfp4_cache, "gate_up"), + _cached_mxfp4(down_nv, _pt(down_nv, E, dev), mxfp4_cache, "down"), + ) + else: + gu_eng = _engine(Mt, twoI, H, E, mode) + gu_eng.set_weights(gate_up_nv.qdata, gate_up_nv.scale) + dn_eng = _engine(Mt, H, I, E, mode) + dn_eng.set_weights(down_nv.qdata, down_nv.scale) + base = ("cutlass", gu_eng, dn_eng) + Agu, Bgu, sgu = _lora_stack(gup_lora, E, H, twoI) + Adn, Bdn, sdn = _lora_stack(down_lora, E, I, H) + assert sgu == sdn, "gate_up/down LoRA scaling must match" + + lim = ( + float(limit) if limit is not None else 1e30 + ) # no clamp when the model has no swiglu_limit + A = hidden.new_zeros(Mt, H).index_copy(0, padded_row, hidden[rep]) + dn = _GroupedExperts.apply( + A, + base, + weight_recipe, + Agu, + Bgu, + Adn, + Bdn, + m_indices, + offs, + sgu, + lim, + mode, + act_type, + prefer_fp8_dx, + ) + out = hidden.new_zeros(N, H) + return out.index_add( + 0, rep, (dn[padded_row] * wflat[:, None].to(dn.dtype)).to(out.dtype) + ) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/ops.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/ops.py index 08baa8e8e3..db919d45e2 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/ops.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/kernels/ops.py @@ -90,6 +90,9 @@ def _scatter2scatter( B_ptr, stride_be: tl.constexpr, stride_bn: tl.constexpr, + R_ptr, + stride_rm, + stride_rn, grouped_idx_ptr, expert_idxs_ptr, # block_start_idx_ptr, @@ -168,8 +171,16 @@ def _scatter2scatter( M_out_idx = M_block else: M_out_idx = M_idx + out_mask = M_boundary_mask[:, None] & N_mask[None, :] + if R_ptr is not None: + # Fused per-row residual add (e.g. base expert GEMM output) so the LoRA-B GEMM writes + # (base + lora) directly, no separate add pass. + R_blk_ptrs = R_ptr + ( + M_out_idx[:, None] * stride_rm + N_block[None, :] * stride_rn + ) + acc += tl.load(R_blk_ptrs, mask=out_mask).to(ACC_TYPE) Y_blk_ptrs = Y_ptr + (M_out_idx[:, None] * stride_ym + N_block[None, :] * stride_yn) - tl.store(Y_blk_ptrs, acc, mask=M_boundary_mask[:, None] & N_mask[None, :]) + tl.store(Y_blk_ptrs, acc, mask=out_mask) def scatter2scatter( @@ -183,6 +194,7 @@ def scatter2scatter( y_grouped=False, out=None, int64_indices=False, + residual=None, ): assert sorted_scattered_idxs.size(0) == sorted_expert_idxs.size(0) assert sorted_scattered_idxs.size(0) == X.size(0) * k @@ -194,6 +206,8 @@ def scatter2scatter( else: assert out.size(0) == L_scattered and out.size(1) == y_dim output = out + if residual is not None: + assert residual.size(0) == L_scattered and residual.size(1) == y_dim scatter2scatter_compileable( output, @@ -206,6 +220,7 @@ def scatter2scatter( x_grouped, y_grouped, int64_indices, + residual, ) return output @@ -222,6 +237,7 @@ def scatter2scatter_compileable( x_grouped: bool, y_grouped: bool, int64_indices: bool = False, + residual: Optional[torch.Tensor] = None, ) -> None: def grid(META): grid_num = ( @@ -236,6 +252,11 @@ def grid(META): else: stride_be, stride_bn = b.stride() + if residual is None: + stride_rm = stride_rn = 0 + else: + stride_rm, stride_rn = residual.stride() + _scatter2scatter[grid]( # X_ptr, stride_xm, stride_xk, X, @@ -254,6 +275,10 @@ def grid(META): b, stride_be, stride_bn, + # R_ptr, stride_rm, stride_rn (per-row residual added in epilogue) + residual, + stride_rm, + stride_rn, grouped_idx_ptr=sorted_scattered_idxs, expert_idxs_ptr=sorted_expert_idxs, # block_start_idx_ptr=padded_block_idxs, diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/__init__.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/__init__.py new file mode 100644 index 0000000000..a35a482a34 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/__init__.py @@ -0,0 +1,103 @@ +"""Marlin NVFP4 **W4A16** forward backend for the grouped DeepSeek-V4 MoE (Ampere and newer). + +Replaces the CUTLASS FP4xFP4 (lossy fp4 *activations*) base FORWARD with a Marlin weight-only NVFP4 +GEMM (bf16 activations, bit-correct). At the thin-M LoRA-FT shape (E=256, ~48 tok/expert) this is +~1.79x faster than CUTLASS on sm120 AND removes the ~9.3% activation-quant error of the W4A4 path. +The Marlin MoE GEMM + ``gptq_marlin_repack`` are vendored under ``_csrc/`` (extracted from vLLM, +ported to a regular ``torch::Tensor`` ABI, bit-exact to vLLM) so there is NO vLLM runtime dependency. +JIT-built once (for the current device's arch) via ``torch.utils.cpp_extension.load`` and cached. + +Portability: the kernel is the standard Ampere-class Marlin (``mma.m16n8k16`` bf16 + bit-twiddle FP4 +decode) — NO Hopper/Blackwell-only intrinsics. The W4A16 (bf16-act x NVFP4-weight) config runs on +any sm80+ GPU; the ``__CUDA_ARCH__ < 890`` guard in the template only disables *fp8 activations*, +which this path never uses. This makes Marlin the fused base GEMM that also covers Ampere (A100, +sm80) and Ada (RTX 4090 / L40S, sm89), where DeepGEMM (sm90+) and CUTLASS-fp4 (sm120) don't run. + +This backend provides the FORWARD only; the backward keeps the existing gradient-consistent +``_base_dx`` (fp8-read on sm120, now enabled for the pad-64 layout via the BM=tile dX kernel; +bf16-dequant elsewhere). A Marlin NVFP4 transpose-repack backward was evaluated and rejected: the +backward contracts the other axis, so reusing the weight needs an independent 4-bit re-quantization +of W^T that disagrees with the forward weight by ~18% — measured to land ~13% in the LoRA gradients +(it is a structured weight mismatch, not noise the low-rank projection averages out). fp8-read keeps +the backward gradient-consistent at 1 byte/wt, which is the floor for block-scaled 4-bit. + +Lazy build/import keeps no-nvcc / pre-Ampere environments (incl. CI) clean; ``marlin_w4a16_available`` +returns False there and the dsv4 MoE forward falls back to CUTLASS (sm120) / DeepGEMM (sm90/100) / +chunked dequant. +""" + +from __future__ import annotations + +import functools +import os + +import torch + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_CSRC = os.path.join(_HERE, "_csrc") +_MOE = os.path.join(_CSRC, "libtorch_stable", "moe", "marlin_moe_wna16") +_MARLIN_INC = os.path.join(_CSRC, "libtorch_stable", "quantization", "marlin") +_EXT = None + + +@functools.lru_cache(maxsize=1) +def marlin_w4a16_available() -> bool: + """True iff the standalone Marlin W4A16 ext can build + run here: Ampere or newer (sm80+, the + bf16 ``mma`` the W4A16 path needs), CUDA available, and an nvcc present to JIT-build it.""" + try: + if not torch.cuda.is_available(): + return False + if torch.cuda.get_device_capability()[0] < 8: # bf16 mma needs sm80+ + return False + from torch.utils.cpp_extension import CUDA_HOME + + return CUDA_HOME is not None + except Exception: + return False + + +def _gencode_for_current_device() -> str: + """``-gencode`` for the running GPU's arch (build only what we run). Hopper+ uses the arch- + specific ('a') target to match the vendored vLLM build; Ampere/Ada use the plain target.""" + major, minor = torch.cuda.get_device_capability() + arch = f"{major}{minor}" + suffix = "a" if major >= 9 else "" + return f"-gencode=arch=compute_{arch}{suffix},code=sm_{arch}{suffix}" + + +def load_ext(): + """JIT-build (cached) and return the vendored standalone Marlin ext module for this GPU's arch. + + Exposes ``moe_wna16_marlin_gemm`` (NVFP4 W4A16 MoE GEMM, bf16 act/out) and ``gptq_marlin_repack`` + (NVFP4 weight -> Marlin tile layout). Compiled for the current device's compute capability.""" + global _EXT + if _EXT is None: + from torch.utils.cpp_extension import load + + major, minor = torch.cuda.get_device_capability() + # Arch in the ext name so two arches in one torch-extensions cache don't collide. + _EXT = load( + name=f"axolotl_marlin_w4a16_sm{major}{minor}", + sources=[ + os.path.join(_MOE, "ops_standalone.cu"), + os.path.join(_MOE, "sm80_kernel_bfloat16_fe2m1f_bfloat16.cu"), + os.path.join(_MOE, "repack_standalone.cu"), + ], + extra_cuda_cflags=[ + "-O3", + _gencode_for_current_device(), + "-DMARLIN_NAMESPACE_NAME=marlin_moe_wna16", + "--expt-relaxed-constexpr", + "--expt-extended-lambda", + # Keep external linkage on the __global__ template instantiations so + # ops_standalone.cu's selector resolves them at link, not static stubs. + # Requires CUDA >= 12.4 nvcc. + "-static-global-template-stub=false", + "-Xcompiler", + "-fPIC", + ], + extra_cflags=["-O3", "-DMARLIN_NAMESPACE_NAME=marlin_moe_wna16"], + extra_include_paths=[_CSRC, _MARLIN_INC, _MOE], + verbose=False, + ) + return _EXT diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/PROVENANCE.md b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/PROVENANCE.md new file mode 100644 index 0000000000..766c57a68a --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/PROVENANCE.md @@ -0,0 +1,34 @@ +# Provenance of the vendored Marlin W4A16 CUDA sources + +These files implement the NVFP4 weight-only (W4A16) Marlin MoE GEMM and the +`gptq_marlin_repack` weight-prep op. They are **extracted from +[vLLM](https://github.com/vllm-project/vllm)** (`csrc/`), which in turn builds on the +**Marlin** kernel by Elias Frantar (`Copyright (C) Marlin.2024 Elias Frantar`, modified by +Neural Magic). Licensing follows the upstream sources: **Apache-2.0** (vLLM) and the Marlin +license headers retained verbatim in the individual files. + +Vendored here (rather than importing vLLM) so this optional sm120 backend adds **no vLLM +runtime dependency** — consistent with the cutlass-dsl / DeepGEMM optional-backend pattern. + +## What was changed vs upstream + +Only the two host-wrapper translation units were edited; the kernels and headers are otherwise +verbatim: + +- `libtorch_stable/moe/marlin_moe_wna16/ops_standalone.cu` — the `moe_wna16_marlin_gemm` host + wrapper, ported from vLLM's `torch::stable::Tensor` ABI to a regular `torch::Tensor` ABI, and + the `STABLE_TORCH_LIBRARY_IMPL` registration replaced by a `PYBIND11_MODULE`. +- `libtorch_stable/moe/marlin_moe_wna16/repack_standalone.cu` — the `gptq_marlin_repack` host + wrapper, same `torch::stable` -> `torch::Tensor` port; compiled in the default `marlin` + namespace (the GEMM TU uses `marlin_moe_wna16`). + +Everything else (`marlin_template.h`, `marlin.cuh`, `marlin_mma.h`, `marlin_dtypes.cuh`, +`dequant.h`, `kernel.h`, `kernel_selector.h`, `launcher_body.inc`, `core/scalar_type.hpp`, +`sm80_kernel_bfloat16_fe2m1f_bfloat16.cu`) is copied verbatim from vLLM. + +## Equivalence + +The ported `moe_wna16_marlin_gemm` and `gptq_marlin_repack` were validated **bit-exact** against +the same vLLM ops (identical inputs -> identical outputs). The Python prep (`../prep.py`) copies +vLLM's pure-torch scale helpers verbatim, so the full NVFP4 -> Marlin weight prep is bit-identical +to vLLM's `prepare_nvfp4_moe_layer_for_marlin`. diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/core/scalar_type.hpp b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/core/scalar_type.hpp new file mode 100644 index 0000000000..b6f39ed795 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/core/scalar_type.hpp @@ -0,0 +1,360 @@ +#pragma once + +#include +#include +#include +#include +#include + +// For STD_TORCH_CHECK +#include + +namespace vllm { + +// +// ScalarType can represent a wide range of floating point and integer types, +// in particular it can be used to represent sub-byte data types (something +// that torch.dtype currently does not support). +// +// The type definitions on the Python side can be found in: vllm/scalar_type.py +// these type definitions should be kept up to date with any Python API changes +// here. +// +class ScalarType { + public: + enum NanRepr : uint8_t { + NAN_NONE = 0, // nans are not supported + NAN_IEEE_754 = 1, // nans are: exp all 1s, mantissa not all 0s + NAN_EXTD_RANGE_MAX_MIN = 2, // nans are: exp all 1s, mantissa all 1s + + NAN_REPR_ID_MAX + }; + + constexpr ScalarType(uint8_t exponent, uint8_t mantissa, bool signed_, + int32_t bias, bool finite_values_only = false, + NanRepr nan_repr = NAN_IEEE_754) + : exponent(exponent), + mantissa(mantissa), + signed_(signed_), + bias(bias), + finite_values_only(finite_values_only), + nan_repr(nan_repr) {}; + + static constexpr ScalarType int_(uint8_t size_bits, int32_t bias = 0) { + return ScalarType(0, size_bits - 1, true, bias); + } + + static constexpr ScalarType uint(uint8_t size_bits, int32_t bias = 0) { + return ScalarType(0, size_bits, false, bias); + } + + // IEEE 754 compliant floating point type + static constexpr ScalarType float_IEEE754(uint8_t exponent, + uint8_t mantissa) { + STD_TORCH_CHECK(mantissa > 0 && exponent > 0); + return ScalarType(exponent, mantissa, true, 0, false, NAN_IEEE_754); + } + + // IEEE 754 non-compliant floating point type + static constexpr ScalarType float_(uint8_t exponent, uint8_t mantissa, + bool finite_values_only, + NanRepr nan_repr) { + STD_TORCH_CHECK(nan_repr < NAN_REPR_ID_MAX, "Invalid NanRepr"); + STD_TORCH_CHECK(mantissa > 0 && exponent > 0); + STD_TORCH_CHECK( + nan_repr != NAN_IEEE_754, + "use `float_IEEE754` constructor for floating point types that " + "follow IEEE 754 conventions"); + return ScalarType(exponent, mantissa, true, 0, finite_values_only, + nan_repr); + } + + uint8_t const exponent; // size of the exponent field (0 for integer types) + uint8_t const mantissa; // size of the mantissa field (size of the integer + // excluding the sign bit for integer types) + bool const signed_; // flag if the type supports negative numbers (i.e. has a + // sign bit) + int32_t const bias; // stored values equal value + bias, + // used for quantized type + + // Extra Floating point info + bool const finite_values_only; // i.e. no +/-inf if true + NanRepr const nan_repr; // how NaNs are represented + // (not applicable for integer types) + + using Id = int64_t; + + private: + // Field size in id + template + static constexpr size_t member_id_field_width() { + using T = std::decay_t; + return std::is_same_v ? 1 : sizeof(T) * 8; + } + + template + static constexpr auto reduce_members_helper(Fn f, Init val, Member member, + Rest... rest) { + auto new_val = f(val, member); + if constexpr (sizeof...(rest) > 0) { + return reduce_members_helper(f, new_val, rest...); + } else { + return new_val; + }; + } + + template + constexpr auto reduce_members(Fn f, Init init) const { + // Should be in constructor order for `from_id` + return reduce_members_helper(f, init, exponent, mantissa, signed_, bias, + finite_values_only, nan_repr); + }; + + template + static constexpr auto reduce_member_types(Fn f, Init init) { + constexpr auto dummy_type = ScalarType(0, 0, false, 0, false, NAN_NONE); + return dummy_type.reduce_members(f, init); + }; + + static constexpr auto id_size_bits() { + return reduce_member_types( + [](int acc, auto member) -> int { + return acc + member_id_field_width(); + }, + 0); + } + + public: + // unique id for this scalar type that can be computed at compile time for + // c++17 template specialization this is not needed once we migrate to + // c++20 and can pass literal classes as template parameters + constexpr Id id() const { + static_assert(id_size_bits() <= sizeof(Id) * 8, + "ScalarType id is too large to be stored"); + + auto or_and_advance = [](std::pair result, + auto member) -> std::pair { + auto [id, bit_offset] = result; + auto constexpr bits = member_id_field_width(); + return {id | (int64_t(member) & ((uint64_t(1) << bits) - 1)) + << bit_offset, + bit_offset + bits}; + }; + return reduce_members(or_and_advance, std::pair{}).first; + } + + // create a ScalarType from an id, for c++17 template specialization, + // this is not needed once we migrate to c++20 and can pass literal + // classes as template parameters + static constexpr ScalarType from_id(Id id) { + auto extract_and_advance = [id](auto result, auto member) { + using T = decltype(member); + auto [tuple, bit_offset] = result; + auto constexpr bits = member_id_field_width(); + auto extracted_val = static_cast((int64_t(id) >> bit_offset) & + ((uint64_t(1) << bits) - 1)); + auto new_tuple = std::tuple_cat(tuple, std::make_tuple(extracted_val)); + return std::pair{new_tuple, bit_offset + bits}; + }; + + auto [tuple_args, _] = reduce_member_types(extract_and_advance, + std::pair, int>{}); + return std::apply([](auto... args) { return ScalarType(args...); }, + tuple_args); + } + + constexpr int64_t size_bits() const { + return mantissa + exponent + is_signed(); + } + constexpr bool is_signed() const { return signed_; } + constexpr bool is_integer() const { return exponent == 0; } + constexpr bool is_floating_point() const { return exponent > 0; } + constexpr bool is_ieee_754() const { + return is_floating_point() && finite_values_only == false && + nan_repr == NAN_IEEE_754; + } + constexpr bool has_nans() const { + return is_floating_point() && nan_repr != NAN_NONE; + } + constexpr bool has_infs() const { + return is_floating_point() && finite_values_only == false; + } + constexpr bool has_bias() const { return bias != 0; } + + private: + double _floating_point_max() const { + STD_TORCH_CHECK(mantissa <= 52 && exponent <= 11, + "Cannot represent max/min as a double for type ", str()); + + uint64_t max_mantissa = (uint64_t(1) << mantissa) - 1; + if (nan_repr == NAN_EXTD_RANGE_MAX_MIN) { + max_mantissa -= 1; + } + + uint64_t max_exponent = (uint64_t(1) << exponent) - 2; + if (nan_repr == NAN_EXTD_RANGE_MAX_MIN || nan_repr == NAN_NONE) { + STD_TORCH_CHECK(exponent < 11, + "Cannot represent max/min as a double for type ", str()); + max_exponent += 1; + } + + // adjust the exponent to match that of a double + // for now we assume the exponent bias is the standard 2^(e-1) -1, (where e + // is the exponent bits), there is some precedent for non-standard biases, + // example `float8_e4m3b11fnuz` here: https://github.com/jax-ml/ml_dtypes + // but to avoid premature over complication we are just assuming the + // standard exponent bias until there is a need to support non-standard + // biases + uint64_t exponent_bias = (uint64_t(1) << (exponent - 1)) - 1; + uint64_t exponent_bias_double = (uint64_t(1) << 10) - 1; // double e = 11 + + uint64_t max_exponent_double = + max_exponent - exponent_bias + exponent_bias_double; + + // shift the mantissa into the position for a double and + // the exponent + uint64_t double_raw = + (max_mantissa << (52 - mantissa)) | (max_exponent_double << 52); + + return *reinterpret_cast(&double_raw); + } + + constexpr std::variant _raw_max() const { + if (is_floating_point()) { + return {_floating_point_max()}; + } else { + STD_TORCH_CHECK(size_bits() < 64 || size_bits() == 64 && is_signed(), + "Cannot represent max as a int64_t"); + return {(int64_t(1) << mantissa) - 1}; + } + } + + constexpr std::variant _raw_min() const { + if (is_floating_point()) { + STD_TORCH_CHECK( + is_signed(), + "We currently assume all floating point types are signed"); + constexpr uint64_t sign_bit_double = (uint64_t(1) << 63); + + double max = _floating_point_max(); + uint64_t max_raw = *reinterpret_cast(&max); + uint64_t min_raw = max_raw | sign_bit_double; + return {*reinterpret_cast(&min_raw)}; + } else { + STD_TORCH_CHECK(!is_signed() || size_bits() <= 64, + "Cannot represent min as a int64_t"); + if (is_signed()) { + // set the top bit to 1 (i.e. INT64_MIN) and the rest to 0 + // then perform an arithmetic shift right to set all the bits above + // (size_bits() - 1) to 1 + return {INT64_MIN >> (64 - size_bits())}; + } else { + return {int64_t(0)}; + } + } + } + + public: + // Max representable value for this scalar type. + // (accounting for bias if there is one) + constexpr std::variant max() const { + return std::visit( + [this](auto x) -> std::variant { return {x - bias}; }, + _raw_max()); + } + + // Min representable value for this scalar type. + // (accounting for bias if there is one) + constexpr std::variant min() const { + return std::visit( + [this](auto x) -> std::variant { return {x - bias}; }, + _raw_min()); + } + + std::string str() const { + /* naming generally follows: https://github.com/jax-ml/ml_dtypes + * for floating point types (leading f) the scheme is: + * `float_em[flags]` + * flags: + * - no-flags: means it follows IEEE 754 conventions + * - f: means finite values only (no infinities) + * - n: means nans are supported (non-standard encoding) + * for integer types the scheme is: + * `[u]int[b]` + * - if bias is not present it means its zero + */ + if (is_floating_point()) { + auto ret = "float" + std::to_string(size_bits()) + "_e" + + std::to_string(exponent) + "m" + std::to_string(mantissa); + if (!is_ieee_754()) { + if (finite_values_only) { + ret += "f"; + } + if (nan_repr != NAN_NONE) { + ret += "n"; + } + } + return ret; + } else { + auto ret = ((is_signed()) ? "int" : "uint") + std::to_string(size_bits()); + if (has_bias()) { + ret += "b" + std::to_string(bias); + } + return ret; + } + } + + constexpr bool operator==(ScalarType const& other) const { + return mantissa == other.mantissa && exponent == other.exponent && + bias == other.bias && signed_ == other.signed_ && + finite_values_only == other.finite_values_only && + nan_repr == other.nan_repr; + } +}; + +using ScalarTypeId = ScalarType::Id; + +// "rust style" names generally following: +// https://github.com/pytorch/pytorch/blob/6d9f74f0af54751311f0dd71f7e5c01a93260ab3/torch/csrc/api/include/torch/types.h#L60-L70 +static inline constexpr auto kS4 = ScalarType::int_(4); +static inline constexpr auto kU4 = ScalarType::uint(4); +static inline constexpr auto kU4B8 = ScalarType::uint(4, 8); +static inline constexpr auto kS8 = ScalarType::int_(8); +static inline constexpr auto kU8 = ScalarType::uint(8); +static inline constexpr auto kU8B128 = ScalarType::uint(8, 128); + +static inline constexpr auto kFE2M1f = + ScalarType::float_(2, 1, true, ScalarType::NAN_NONE); +static inline constexpr auto kFE3M2f = + ScalarType::float_(3, 2, true, ScalarType::NAN_NONE); +static inline constexpr auto kFE4M3fn = + ScalarType::float_(4, 3, true, ScalarType::NAN_EXTD_RANGE_MAX_MIN); +static inline constexpr auto kFE8M0fnu = + ScalarType(8, 0, false, 0, true, ScalarType::NAN_EXTD_RANGE_MAX_MIN); +static inline constexpr auto kFE5M2 = ScalarType::float_IEEE754(5, 2); +static inline constexpr auto kFE8M7 = ScalarType::float_IEEE754(8, 7); +static inline constexpr auto kFE5M10 = ScalarType::float_IEEE754(5, 10); + +// Fixed width style names, generally following: +// https://github.com/pytorch/pytorch/blob/6d9f74f0af54751311f0dd71f7e5c01a93260ab3/torch/csrc/api/include/torch/types.h#L47-L57 +static inline constexpr auto kInt4 = kS4; +static inline constexpr auto kUint4 = kU4; +static inline constexpr auto kUint4b8 = kU4B8; +static inline constexpr auto kInt8 = kS8; +static inline constexpr auto kUint8 = kU8; +static inline constexpr auto kUint8b128 = kU8B128; + +static inline constexpr auto kFloat4_e2m1f = kFE2M1f; +static inline constexpr auto kFloat6_e3m2f = kFE3M2f; +static inline constexpr auto kFloat8_e4m3fn = kFE4M3fn; +static inline constexpr auto kFloat8_e5m2 = kFE5M2; +static inline constexpr auto kFloat16_e8m7 = kFE8M7; +static inline constexpr auto kFloat16_e5m10 = kFE5M10; + +// colloquial names +static inline constexpr auto kHalf = kFE5M10; +static inline constexpr auto kFloat16 = kHalf; +static inline constexpr auto kBFloat16 = kFE8M7; + +static inline constexpr auto kFloat16Id = kFloat16.id(); +}; // namespace vllm diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/launcher_body.inc b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/launcher_body.inc new file mode 100644 index 0000000000..e47705270b --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/launcher_body.inc @@ -0,0 +1,500 @@ +namespace MARLIN_NAMESPACE_NAME { + +__global__ void MarlinDefault(MARLIN_KERNEL_PARAMS){}; + +using MarlinFuncPtr = void (*)(MARLIN_KERNEL_PARAMS); + +// For a given "a" of size [M,K] performs a permutation of the K columns based +// on the given "perm" indices. +template +__global__ void permute_cols_kernel( + int4 const* __restrict__ a_int4_ptr, int const* __restrict__ perm_int_ptr, + int4* __restrict__ out_int4_ptr, + const int32_t* __restrict__ sorted_token_ids_ptr, + const int32_t* __restrict__ expert_ids_ptr, + const int32_t* __restrict__ num_tokens_past_padded_ptr, int size_m, + int size_k, int top_k) { + int num_tokens_past_padded = num_tokens_past_padded_ptr[0]; + int num_moe_blocks = div_ceil(num_tokens_past_padded, moe_block_size); + int32_t block_sorted_ids[moe_block_size]; + int block_num_valid_tokens = 0; + int64_t old_expert_id = 0; + int64_t expert_id = 0; + int row_stride = size_k * sizeof(half) / 16; + + auto read_moe_block_data = [&](int block_id) { + block_num_valid_tokens = moe_block_size; + int4* tmp_block_sorted_ids = reinterpret_cast(block_sorted_ids); + for (int i = 0; i < moe_block_size / 4; i++) { + tmp_block_sorted_ids[i] = + ((int4*)sorted_token_ids_ptr)[block_id * moe_block_size / 4 + i]; + } + for (int i = 0; i < moe_block_size; i++) { + if (block_sorted_ids[i] >= size_m * top_k) { + block_num_valid_tokens = i; + break; + }; + } + }; + + auto permute_row = [&](int row) { + int iters = size_k / default_threads; + int rest = size_k % default_threads; + + int in_offset = (row / top_k) * row_stride; + int out_offset = row * row_stride; + + half const* a_row_half = + reinterpret_cast(a_int4_ptr + in_offset); + half* out_half = reinterpret_cast(out_int4_ptr + out_offset); + + int base_k = 0; + + for (int i = 0; i < iters; i++) { + auto cur_k = base_k + threadIdx.x; + int src_pos = perm_int_ptr[cur_k]; + + out_half[cur_k] = a_row_half[src_pos]; + + base_k += default_threads; + } + + if (rest) { + if (threadIdx.x < rest) { + auto cur_k = base_k + threadIdx.x; + int src_pos = perm_int_ptr[cur_k]; + + out_half[cur_k] = a_row_half[src_pos]; + } + } + }; + + for (int index = blockIdx.x; index < num_moe_blocks; index += gridDim.x) { + old_expert_id = expert_id; + int tmp_expert_id = expert_ids_ptr[index]; + if (tmp_expert_id == -1) continue; + expert_id = tmp_expert_id; + perm_int_ptr += (expert_id - old_expert_id) * size_k; + read_moe_block_data(index); + + for (int i = 0; i < block_num_valid_tokens; i++) + permute_row(block_sorted_ids[i]); + } +} + +typedef struct { + int thread_k; + int thread_n; + int num_threads; +} thread_config_t; + +thread_config_t small_batch_thread_configs[] = { + // Ordered by priority + + // thread_k, thread_n, num_threads + {128, 128, 256}, + {64, 128, 128}, + {128, 64, 128}}; + +thread_config_t large_batch_thread_configs[] = { + // Ordered by priority + + // thread_k, thread_n, num_threads + {64, 256, 256}, + {64, 128, 128}, + {128, 64, 128}}; + +typedef struct { + int blocks_per_sm; + thread_config_t tb_cfg; +} exec_config_t; + +int get_scales_cache_size(thread_config_t const& th_config, int prob_m, + int prob_n, int prob_k, int num_bits, int group_size, + bool has_act_order, bool is_k_full, int stages) { + bool cache_scales_chunk = has_act_order && !is_k_full; + + int tb_n = th_config.thread_n; + int tb_k = th_config.thread_k; + + // Get max scale groups per thread-block + int tb_groups; + if (group_size == -1) { + tb_groups = 1; + } else if (group_size == 0) { + tb_groups = div_ceil(tb_k, 32); // Worst case is 32 group size + } else { + tb_groups = div_ceil(tb_k, group_size); + } + + if (cache_scales_chunk) { + int load_groups = + tb_groups * stages * 2; // Chunk size is 2x pipeline over dim K + load_groups = max(load_groups, 32); // We load at least 32 scale groups + return load_groups * tb_n * 2; + } else { + int tb_scales = tb_groups * tb_n * 2; + + return tb_scales * stages; + } +} + +int get_kernel_cache_size(thread_config_t const& th_config, bool m_block_size_8, + int thread_m_blocks, int prob_m, int prob_n, + int prob_k, int num_bits, int group_size, + bool has_act_order, bool is_k_full, int has_zp, + int is_zp_float, bool is_a_8bit, int stages) { + int pack_factor = 32 / num_bits; + + // Get B size + int tb_k = th_config.thread_k; + int tb_n = th_config.thread_n; + int tb_m = thread_m_blocks * 16; + + // shm size for block_sorted_ids/rd_block_sorted_ids/block_topk_weights + // both of them requires tb_m * 4 bytes (tb_m * int32 or tb_m * float32) + int sh_block_meta_size = tb_m * 16; + int sh_a_size = stages * (tb_m * tb_k) * (is_a_8bit ? 1 : 2); + int sh_b_size = stages * (tb_k * tb_n / pack_factor) * 4; + int sh_red_size = tb_m * (tb_n + 8) * 2; + int sh_bias_size = tb_n * 2; + int tmp_size = + (sh_b_size > sh_red_size ? sh_red_size : sh_b_size) + sh_bias_size; + tmp_size = max(max(sh_b_size, sh_red_size), tmp_size); + + int sh_s_size = + get_scales_cache_size(th_config, prob_m, prob_n, prob_k, num_bits, + group_size, has_act_order, is_k_full, stages); + int sh_g_idx_size = has_act_order && !is_k_full ? stages * tb_k / 4 : 0; + int sh_zp_size = 0; + if (has_zp) { + if (is_zp_float) + sh_zp_size = sh_s_size; + else if (num_bits == 4) + sh_zp_size = sh_s_size / 4; + else if (num_bits == 8) + sh_zp_size = sh_s_size / 2; + } + + int total_size = tmp_size + sh_a_size + sh_s_size + sh_zp_size + + sh_g_idx_size + sh_block_meta_size; + + return total_size; +} + +bool is_valid_config(thread_config_t const& th_config, bool m_block_size_8, + int thread_m_blocks, int prob_m, int prob_n, int prob_k, + int num_bits, int group_size, bool has_act_order, + bool is_k_full, int has_zp, int is_zp_float, + bool is_a_8bit, int stages, int max_shared_mem) { + // Sanity + if (th_config.thread_k == -1 || th_config.thread_n == -1 || + th_config.num_threads == -1) { + return false; + } + + // Verify K/N are divisible by thread K/N + if (prob_k % th_config.thread_k != 0 || prob_n % th_config.thread_n != 0) { + return false; + } + + // Verify min for thread K/N + if (th_config.thread_n < min_thread_n || th_config.thread_k < min_thread_k) { + return false; + } + + // num_threads must be at least 128 (= 4 warps) + if (th_config.num_threads < 128) { + return false; + } + + // Check that pipeline fits into cache + int cache_size = + get_kernel_cache_size(th_config, m_block_size_8, thread_m_blocks, prob_m, + prob_n, prob_k, num_bits, group_size, has_act_order, + is_k_full, has_zp, is_zp_float, is_a_8bit, stages); + return cache_size <= max_shared_mem; +} + +MarlinFuncPtr get_marlin_kernel( + const vllm::ScalarType a_type, const vllm::ScalarType b_type, + const vllm::ScalarType c_type, const vllm::ScalarType s_type, + int thread_m_blocks, int thread_n_blocks, int thread_k_blocks, + bool m_block_size_8, bool has_act_order, bool has_zp, int group_blocks, + int threads, bool is_zp_float, int stages) { + int num_bits = b_type.size_bits(); + auto kernel = MarlinDefault; + +#include "kernel_selector.h" + + return kernel; +} + +exec_config_t determine_exec_config( + const vllm::ScalarType& a_type, const vllm::ScalarType& b_type, + const vllm::ScalarType& c_type, const vllm::ScalarType& s_type, int prob_m, + int prob_n, int prob_k, int num_experts, int top_k, int thread_m_blocks, + bool m_block_size_8, int num_bits, int group_size, bool has_act_order, + bool is_k_full, bool has_zp, bool is_zp_float, bool is_a_8bit, int stages, + int max_shared_mem, int sms) { + exec_config_t exec_cfg = exec_config_t{1, thread_config_t{-1, -1, -1}}; + thread_config_t* thread_configs = thread_m_blocks > 1 + ? large_batch_thread_configs + : small_batch_thread_configs; + int thread_configs_size = + thread_m_blocks > 1 + ? sizeof(large_batch_thread_configs) / sizeof(thread_config_t) + : sizeof(small_batch_thread_configs) / sizeof(thread_config_t); + + int count = 0; + constexpr int device_max_reg_size = 255 * 1024; + for (int i = 0; i < thread_configs_size; i++) { + thread_config_t th_config = thread_configs[i]; + + if (!is_valid_config(th_config, m_block_size_8, thread_m_blocks, prob_m, + prob_n, prob_k, num_bits, group_size, has_act_order, + is_k_full, has_zp, is_zp_float, is_a_8bit, stages, + max_shared_mem - 512)) { + continue; + } + + int cache_size = get_kernel_cache_size( + th_config, m_block_size_8, thread_m_blocks, prob_m, prob_n, prob_k, + num_bits, group_size, has_act_order, is_k_full, has_zp, is_zp_float, + is_a_8bit, stages); + + int group_blocks = 0; + if (!has_act_order) { + group_blocks = group_size == -1 ? -1 : (group_size / 16); + } + + auto kernel = + get_marlin_kernel(a_type, b_type, c_type, s_type, thread_m_blocks, + th_config.thread_n / 16, th_config.thread_k / 16, + m_block_size_8, has_act_order, has_zp, group_blocks, + th_config.num_threads, is_zp_float, stages); + + if (kernel == MarlinDefault) continue; + + cudaFuncAttributes attr; + cudaFuncGetAttributes(&attr, kernel); + int reg_size = max(attr.numRegs, 1) * th_config.num_threads * 4; + int allow_count = min(device_max_reg_size / reg_size, + max_shared_mem / (cache_size + 1536)); + if (thread_m_blocks == 1) + allow_count = max(min(allow_count, 4), 1); + else + allow_count = max(min(allow_count, 2), 1); + + if (prob_n / th_config.thread_n * prob_m * top_k * 4 < sms * allow_count) { + allow_count = + max(prob_n / th_config.thread_n * prob_m * top_k * 4 / sms, 1); + } + + if (allow_count > count) { + count = allow_count; + exec_cfg = {count, th_config}; + }; + } + + return exec_cfg; +} + +void marlin_mm(const void* A, const void* B, void* C, void* C_tmp, void* b_bias, + void* a_s, void* b_s, void* g_s, void* zp, void* g_idx, + void* perm, void* a_tmp, void* sorted_token_ids, + void* expert_ids, void* num_tokens_past_padded, + void* topk_weights, int moe_block_size, int num_experts, + int top_k, bool mul_topk_weights, int prob_m, int prob_n, + int prob_k, void* workspace, vllm::ScalarType const& a_type, + vllm::ScalarType const& b_type, vllm::ScalarType const& c_type, + vllm::ScalarType const& s_type, bool has_bias, + bool has_act_order, bool is_k_full, bool has_zp, int num_groups, + int group_size, int dev, cudaStream_t stream, int thread_k, + int thread_n, int sms, int blocks_per_sm, bool use_atomic_add, + bool use_fp32_reduce, bool is_zp_float) { + int thread_m_blocks = div_ceil(moe_block_size, 16); + bool m_block_size_8 = moe_block_size == 8; + bool is_a_8bit = a_type.size_bits() == 8; + + STD_TORCH_CHECK(prob_m > 0 && prob_n > 0 && prob_k > 0, "Invalid MNK = [", + prob_m, ", ", prob_n, ", ", prob_k, "]"); + + int group_blocks = 0; + if (has_act_order) { + if (is_k_full) { + STD_TORCH_CHECK(group_size != -1); + group_blocks = group_size / 16; + STD_TORCH_CHECK(prob_k % group_blocks == 0, "prob_k = ", prob_k, + " is not divisible by group_blocks = ", group_blocks); + } else { + STD_TORCH_CHECK(group_size == 0); + group_blocks = 0; + } + } else { + if (group_size == -1) { + group_blocks = -1; + } else { + group_blocks = group_size / 16; + STD_TORCH_CHECK(prob_k % group_blocks == 0, "prob_k = ", prob_k, + " is not divisible by group_blocks = ", group_blocks); + } + } + + int num_bits = b_type.size_bits(); + const int4* A_ptr = (const int4*)A; + const int4* B_ptr = (const int4*)B; + int4* C_ptr = (int4*)C; + int4* C_tmp_ptr = (int4*)C_tmp; + const int4* bias_ptr = (const int4*)b_bias; + const float* a_s_ptr = (const float*)a_s; + const int4* b_s_ptr = (const int4*)b_s; + const float* g_s_ptr = (const float*)g_s; + const int4* zp_ptr = (const int4*)zp; + const int* g_idx_ptr = (const int*)g_idx; + const int* perm_ptr = (const int*)perm; + int4* a_tmp_ptr = (int4*)a_tmp; + const int32_t* sorted_token_ids_ptr = (const int32_t*)sorted_token_ids; + const int32_t* expert_ids_ptr = (const int32_t*)expert_ids; + const int32_t* num_tokens_past_padded_ptr = + (const int32_t*)num_tokens_past_padded; + const float* topk_weights_ptr = (const float*)topk_weights; + int* locks = (int*)workspace; + + if (has_act_order) { + // Permute A columns + auto kernel = permute_cols_kernel<8>; + if (moe_block_size == 8) { + } else if (moe_block_size == 16) + kernel = permute_cols_kernel<16>; + else if (moe_block_size == 32) + kernel = permute_cols_kernel<32>; + else if (moe_block_size == 48) + kernel = permute_cols_kernel<48>; + else if (moe_block_size == 64) + kernel = permute_cols_kernel<64>; + else + STD_TORCH_CHECK(false, "unsupported moe_block_size ", moe_block_size); + + // avoid ">>>" being formatted to "> > >" + // clang-format off + kernel<<>>( + A_ptr, perm_ptr, a_tmp_ptr, sorted_token_ids_ptr, expert_ids_ptr, + num_tokens_past_padded_ptr, prob_m, prob_k, top_k); + // clang-format on + A_ptr = a_tmp_ptr; + prob_m = prob_m * top_k; + top_k = 1; + + // If we have a full K, then we can run the non-act-order version of Marlin + // (since the weight rows are reordered by increasing group ids, and by + // having a full K, we have full original groups) + if (is_k_full) has_act_order = false; + } + + int max_shared_mem = 0; + cudaDeviceGetAttribute(&max_shared_mem, + cudaDevAttrMaxSharedMemoryPerBlockOptin, dev); + STD_TORCH_CHECK(max_shared_mem > 0); + + int major_capability, minor_capability; + cudaDeviceGetAttribute(&major_capability, cudaDevAttrComputeCapabilityMajor, + dev); + cudaDeviceGetAttribute(&minor_capability, cudaDevAttrComputeCapabilityMinor, + dev); + STD_TORCH_CHECK(major_capability * 10 + minor_capability >= 75, + "marlin kernel only support Turing or newer GPUs."); + int stages = 4; + if (major_capability == 7 && minor_capability == 5) { + stages = 2; + STD_TORCH_CHECK(a_type == vllm::kFloat16 || a_type == vllm::kS8, + "Turing only support FP16 or INT8 activation."); + } + if (a_type == vllm::kFE4M3fn) { + STD_TORCH_CHECK(major_capability * 10 + minor_capability >= 89, + "FP8 only support Ada Lovelace or newer GPUs."); + STD_TORCH_CHECK( + major_capability * 10 + minor_capability == 89 || + major_capability == 12, + "Marlin W4A8-FP8 only support SM89 or SM12x device (It is slower than " + "Marlin W4A16 on other devices)."); + } + + // Set thread config + exec_config_t exec_cfg; + thread_config_t thread_tfg; + if (thread_k != -1 && thread_n != -1) { + thread_tfg = thread_config_t{thread_k, thread_n, thread_k * thread_n / 64}; + if (blocks_per_sm == -1) blocks_per_sm = 1; + exec_cfg = exec_config_t{blocks_per_sm, thread_tfg}; + STD_TORCH_CHECK(prob_n % thread_n == 0, "prob_n = ", prob_n, + " is not divisible by thread_n = ", thread_n); + STD_TORCH_CHECK(prob_k % thread_k == 0, "prob_k = ", prob_k, + " is not divisible by thread_k = ", thread_k); + } else { + // Auto config + exec_cfg = determine_exec_config( + a_type, b_type, c_type, s_type, prob_m, prob_n, prob_k, num_experts, + top_k, thread_m_blocks, m_block_size_8, num_bits, group_size, + has_act_order, is_k_full, has_zp, is_zp_float, is_a_8bit, stages, + max_shared_mem, sms); + thread_tfg = exec_cfg.tb_cfg; + } + + int num_threads = thread_tfg.num_threads; + thread_k = thread_tfg.thread_k; + thread_n = thread_tfg.thread_n; + int blocks = sms * exec_cfg.blocks_per_sm; + if (exec_cfg.blocks_per_sm > 1) + max_shared_mem = max_shared_mem / exec_cfg.blocks_per_sm - 1024; + + int thread_k_blocks = thread_k / 16; + int thread_n_blocks = thread_n / 16; + + STD_TORCH_CHECK( + is_valid_config(thread_tfg, m_block_size_8, thread_m_blocks, prob_m, + prob_n, prob_k, num_bits, group_size, has_act_order, + is_k_full, has_zp, is_zp_float, is_a_8bit, stages, + max_shared_mem), + "Invalid thread config: thread_m_blocks = ", thread_m_blocks, + ", thread_k = ", thread_tfg.thread_k, + ", thread_n = ", thread_tfg.thread_n, + ", num_threads = ", thread_tfg.num_threads, " for MKN = [", prob_m, ", ", + prob_k, ", ", prob_n, "] and num_bits = ", num_bits, + ", group_size = ", group_size, ", has_act_order = ", has_act_order, + ", is_k_full = ", is_k_full, ", has_zp = ", has_zp, + ", is_zp_float = ", is_zp_float, ", max_shared_mem = ", max_shared_mem); + + int sh_cache_size = + get_kernel_cache_size(thread_tfg, m_block_size_8, thread_m_blocks, prob_m, + prob_n, prob_k, num_bits, group_size, has_act_order, + is_k_full, has_zp, is_zp_float, is_a_8bit, stages); + + auto kernel = get_marlin_kernel( + a_type, b_type, c_type, s_type, thread_m_blocks, thread_n_blocks, + thread_k_blocks, m_block_size_8, has_act_order, has_zp, group_blocks, + num_threads, is_zp_float, stages); + + if (kernel == MarlinDefault) { + STD_TORCH_CHECK( + false, "Unsupported shapes: MNK = [", prob_m, ", ", prob_n, ", ", + prob_k, "]", ", has_act_order = ", has_act_order, + ", num_groups = ", num_groups, ", group_size = ", group_size, + ", thread_m_blocks = ", thread_m_blocks, + ", thread_n_blocks = ", thread_n_blocks, + ", thread_k_blocks = ", thread_k_blocks, ", num_bits = ", num_bits); + } + + cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, + max_shared_mem); + // avoid ">>>" being formatted to "> > >" + // clang-format off + kernel<<>>( + A_ptr, B_ptr, C_ptr, C_tmp_ptr, bias_ptr, a_s_ptr, b_s_ptr, g_s_ptr, zp_ptr, g_idx_ptr, + sorted_token_ids_ptr, expert_ids_ptr, num_tokens_past_padded_ptr, + topk_weights_ptr, top_k, mul_topk_weights, num_groups, prob_m, + prob_n, prob_k, locks, has_bias, use_atomic_add, use_fp32_reduce); + // clang-format on +} + +} // namespace MARLIN_NAMESPACE_NAME diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/kernel.h b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/kernel.h new file mode 100644 index 0000000000..783736ab50 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/kernel.h @@ -0,0 +1,47 @@ + +#ifndef MARLIN_NAMESPACE_NAME + #define MARLIN_NAMESPACE_NAME marlin_moe_wna16 +#endif + +#include "libtorch_stable/quantization/marlin/marlin.cuh" +#include "libtorch_stable/quantization/marlin/marlin_dtypes.cuh" +#include "core/scalar_type.hpp" + +#define MARLIN_KERNEL_PARAMS \ + const int4 *__restrict__ A, const int4 *__restrict__ B, \ + int4 *__restrict__ C, int4 *__restrict__ C_tmp, \ + const int4 *__restrict__ b_bias_ptr, \ + const float *__restrict__ a_scales_ptr, \ + const int4 *__restrict__ scales_ptr, \ + const float *__restrict__ global_scale_ptr, \ + const int4 *__restrict__ zp_ptr, const int *__restrict__ g_idx, \ + const int32_t *__restrict__ sorted_token_ids_ptr, \ + const int32_t *__restrict__ expert_ids_ptr, \ + const int32_t *__restrict__ num_tokens_past_padded_ptr, \ + const float *__restrict__ topk_weights_ptr, int top_k, \ + bool mul_topk_weights, int num_groups, int prob_m, int prob_n, \ + int prob_k, int *locks, bool has_bias, bool use_atomic_add, \ + bool use_fp32_reduce + +namespace MARLIN_NAMESPACE_NAME { +template shared + // fetch pipeline + const int group_blocks, // number of consecutive 16x16 blocks + // with a separate quantization scale + const bool is_zp_float // is zero point of float16 type? + > +__global__ void Marlin(MARLIN_KERNEL_PARAMS); + +} diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/kernel_selector.h b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/kernel_selector.h new file mode 100644 index 0000000000..fd6905de1d --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/kernel_selector.h @@ -0,0 +1,32 @@ +// auto generated by generate_kernels.py +// clang-format off +if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 256 && thread_m_blocks == 1 && thread_n_blocks == 8 && thread_k_blocks == 8 && m_block_size_8 == true && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 128 && thread_m_blocks == 1 && thread_n_blocks == 8 && thread_k_blocks == 4 && m_block_size_8 == true && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 128 && thread_m_blocks == 1 && thread_n_blocks == 4 && thread_k_blocks == 8 && m_block_size_8 == true && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 256 && thread_m_blocks == 1 && thread_n_blocks == 8 && thread_k_blocks == 8 && m_block_size_8 == false && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 128 && thread_m_blocks == 1 && thread_n_blocks == 8 && thread_k_blocks == 4 && m_block_size_8 == false && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 128 && thread_m_blocks == 1 && thread_n_blocks == 4 && thread_k_blocks == 8 && m_block_size_8 == false && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 256 && thread_m_blocks == 2 && thread_n_blocks == 16 && thread_k_blocks == 4 && m_block_size_8 == false && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 128 && thread_m_blocks == 2 && thread_n_blocks == 8 && thread_k_blocks == 4 && m_block_size_8 == false && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 128 && thread_m_blocks == 2 && thread_n_blocks == 4 && thread_k_blocks == 8 && m_block_size_8 == false && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 256 && thread_m_blocks == 3 && thread_n_blocks == 16 && thread_k_blocks == 4 && m_block_size_8 == false && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 128 && thread_m_blocks == 3 && thread_n_blocks == 8 && thread_k_blocks == 4 && m_block_size_8 == false && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 128 && thread_m_blocks == 3 && thread_n_blocks == 4 && thread_k_blocks == 8 && m_block_size_8 == false && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 256 && thread_m_blocks == 4 && thread_n_blocks == 16 && thread_k_blocks == 4 && m_block_size_8 == false && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 128 && thread_m_blocks == 4 && thread_n_blocks == 8 && thread_k_blocks == 4 && m_block_size_8 == false && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; +else if (a_type == vllm::kBFloat16 && b_type == vllm::kFE2M1f && c_type == vllm::kBFloat16 && s_type == vllm::kFE4M3fn && threads == 128 && thread_m_blocks == 4 && thread_n_blocks == 4 && thread_k_blocks == 8 && m_block_size_8 == false && stages == 4 && group_blocks == 1 && is_zp_float == false) + kernel = Marlin; diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/marlin_template.h b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/marlin_template.h new file mode 100644 index 0000000000..04f90101be --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/marlin_template.h @@ -0,0 +1,2241 @@ +/* + * Modified by Neural Magic + * Copyright (C) Marlin.2024 Elias Frantar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Adapted from https://github.com/IST-DASLab/marlin + */ + +#ifndef MARLIN_NAMESPACE_NAME + #define MARLIN_NAMESPACE_NAME marlin_moe_wna16 +#endif + +#include "libtorch_stable/quantization/marlin/marlin.cuh" +#include "libtorch_stable/quantization/marlin/marlin_dtypes.cuh" +#include "libtorch_stable/quantization/marlin/dequant.h" +#include "libtorch_stable/quantization/marlin/marlin_mma.h" +#include "core/scalar_type.hpp" + +#define STATIC_ASSERT_SCALAR_TYPE_VALID(scalar_t) \ + static_assert(std::is_same::value || \ + std::is_same::value, \ + "only float16 and bfloat16 is supported"); + +namespace MARLIN_NAMESPACE_NAME { + +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ < 750 + +template shared + // fetch pipeline + const bool has_act_order, // whether act_order is enabled + const int group_blocks, // number of consecutive 16x16 blocks + // with a separate quantization scale + const bool is_zp_float // is zero point of float16 type? + > +__global__ void Marlin( + const int4* __restrict__ A, // fp16 input matrix of shape mxk + const int4* __restrict__ B, // 4bit quantized weight matrix of shape kxn + int4* __restrict__ C, // fp16 output buffer of shape mxn + int4* __restrict__ C_tmp, // fp32 tmp output buffer (for reduce) + const int4* __restrict__ scales_ptr, // fp16 quantization scales of shape + // (k/groupsize)xn + const int4* __restrict__ zp_ptr, // 4bit packed zero-points of shape + // (k/groupsize)x(n/pack_factor) + const int* __restrict__ g_idx, // int32 group indices of shape k + const int32_t* __restrict__ sorted_token_ids_ptr, // moe sorted_ids + const int32_t* __restrict__ expert_ids_ptr, // moe expert ids + const int32_t* __restrict__ num_tokens_past_padded_ptr, // moe num tokens + const float* __restrict__ topk_weights_ptr, // moe top weights + int top_k, // num of experts per token + bool mul_topk_weights, // mul topk weights or not + int num_groups, // number of scale groups per output channel + int prob_m, // batch dimension m + int prob_n, // output dimension n + int prob_k, // reduction dimension k + int* locks, // extra global storage for barrier synchronization + bool use_atomic_add, // whether to use atomic add to reduce + bool use_fp32_reduce // whether to use fp32 global reduce +) {} + +} // namespace MARLIN_NAMESPACE_NAME + +#else + +// Instruction for loading a full 16x16 matrix fragment of operand A from shared +// memory, directly in tensor core layout. +template +__device__ inline void ldsm(typename MarlinScalarType::FragA& frag_a, + const void* smem_ptr) { + uint32_t* a = reinterpret_cast(&frag_a); + uint32_t smem = static_cast(__cvta_generic_to_shared(smem_ptr)); + if constexpr (count == 4) { + asm volatile( + "ldmatrix.sync.aligned.m8n8.x4.shared.b16 {%0,%1,%2,%3}, [%4];\n" + : "=r"(a[0]), "=r"(a[1]), "=r"(a[2]), "=r"(a[3]) + : "r"(smem)); + } else if constexpr (count == 2) { + asm volatile("ldmatrix.sync.aligned.m8n8.x2.shared.b16 {%0,%1}, [%2];\n" + : "=r"(a[0]), "=r"(a[1]) + : "r"(smem)); + } else if constexpr (count == 1) { + asm volatile("ldmatrix.sync.aligned.m8n8.x1.shared.b16 {%0}, [%1];\n" + : "=r"(a[0]) + : "r"(smem)); + } else { + static_assert(count == 1 || count == 2 || count == 4, "invalid count"); + } +} + +// Multiply dequantized values by the corresponding quantization scale; used +// only for grouped quantization. +template +__device__ inline void scale(typename MarlinScalarType::FragB& frag_b, + typename MarlinScalarType::FragS& frag_s, + int i) { + using scalar_t = typename MarlinScalarType::scalar_t; + using scalar_t2 = typename MarlinScalarType::scalar_t2; + scalar_t2 s = MarlinScalarType::num2num2( + reinterpret_cast(&frag_s)[i]); + frag_b[0] = __hmul2(frag_b[0], s); + frag_b[1] = __hmul2(frag_b[1], s); +} + +template +__device__ inline void scale_and_sub( + typename MarlinScalarType::FragB& frag_b, + typename MarlinScalarType::scalar_t s, + typename MarlinScalarType::scalar_t zp) { + using scalar_t = typename MarlinScalarType::scalar_t; + using scalar_t2 = typename MarlinScalarType::scalar_t2; + scalar_t2 s2 = MarlinScalarType::num2num2(s); + scalar_t2 zp2 = MarlinScalarType::num2num2(zp); + frag_b[0] = __hfma2(frag_b[0], s2, __hneg2(zp2)); + frag_b[1] = __hfma2(frag_b[1], s2, __hneg2(zp2)); +} + +template +__device__ inline void sub_zp( + typename MarlinScalarType::FragB& frag_b, + typename MarlinScalarType::scalar_t2& frag_zp, int i) { + using scalar_t = typename MarlinScalarType::scalar_t; + using scalar_t2 = typename MarlinScalarType::scalar_t2; + scalar_t2 zp = MarlinScalarType::num2num2( + reinterpret_cast(&frag_zp)[i]); + frag_b[0] = __hsub2(frag_b[0], zp); + frag_b[1] = __hsub2(frag_b[1], zp); +} + +// Same as above, but for act_order (each K is multiplied individually) +template +__device__ inline void scale4( + typename MarlinScalarType::FragB& frag_b, + typename MarlinScalarType::FragS& frag_s_1, + typename MarlinScalarType::FragS& frag_s_2, + typename MarlinScalarType::FragS& frag_s_3, + typename MarlinScalarType::FragS& frag_s_4, int i) { + using scalar_t = typename MarlinScalarType::scalar_t; + using scalar_t2 = typename MarlinScalarType::scalar_t2; + + scalar_t2 s_val_1_2; + s_val_1_2.x = reinterpret_cast(&frag_s_1)[i]; + s_val_1_2.y = reinterpret_cast(&frag_s_2)[i]; + + scalar_t2 s_val_3_4; + s_val_3_4.x = reinterpret_cast(&frag_s_3)[i]; + s_val_3_4.y = reinterpret_cast(&frag_s_4)[i]; + + frag_b[0] = __hmul2(frag_b[0], s_val_1_2); + frag_b[1] = __hmul2(frag_b[1], s_val_3_4); +} + +// Given 2 floats multiply by 2 scales (halves) +template +__device__ inline void scale_float( + float* c, typename MarlinScalarType::FragS& s) { + using scalar_t = typename MarlinScalarType::scalar_t; + scalar_t* s_ptr = reinterpret_cast(&s); + c[0] = __fmul_rn(c[0], MarlinScalarType::num2float(s_ptr[0])); + c[1] = __fmul_rn(c[1], MarlinScalarType::num2float(s_ptr[1])); +} + +// Wait until barrier reaches `count`, then lock for current threadblock. +__device__ inline void barrier_acquire(int* lock, int count) { + if (threadIdx.x == 0) { + int state = -1; + do + // Guarantee that subsequent writes by this threadblock will be visible + // globally. + asm volatile("ld.global.acquire.gpu.b32 %0, [%1];\n" + : "=r"(state) + : "l"(lock)); + while (state != count); + } + __syncthreads(); +} + +// Release barrier and increment visitation count. +__device__ inline void barrier_release(int* lock, bool reset = false) { + __syncthreads(); + if (threadIdx.x == 0) { + if (reset) { + lock[0] = 0; + return; + } + int val = 1; + // Make sure that all writes since acquiring this barrier are visible + // globally, while releasing the barrier. + asm volatile("fence.acq_rel.gpu;\n"); + asm volatile("red.relaxed.gpu.global.add.s32 [%0], %1;\n" + : + : "l"(lock), "r"(val)); + } +} + +// Wait until value of lock to be negative, and then add 1 +__device__ inline void wait_negative_and_add(int* lock) { + if (threadIdx.x == 0) { + int state = 0; + do + // Guarantee that subsequent writes by this threadblock will be visible + // globally. + asm volatile("ld.global.acquire.gpu.b32 %0, [%1];\n" + : "=r"(state) + : "l"(lock)); + while (state >= 0); + atomicAdd(lock, 1); + } + __syncthreads(); +} + +template shared + // fetch pipeline + const int group_blocks, // number of consecutive 16x16 blocks + // with a separate quantization scale + const bool is_zp_float // is zero point of float16 type? + > +__global__ void Marlin( + const int4* __restrict__ A, // fp16 input matrix of shape mxk + const int4* __restrict__ B, // 4bit quantized weight matrix of shape kxn + int4* __restrict__ C, // fp16 output buffer of shape mxn + int4* __restrict__ C_tmp, // fp32 tmp output buffer (for reduce) + const int4* __restrict__ b_bias_ptr, + // float scales of input matrix, only used when is_a_8bit == true. + // shape (m,) + const float* __restrict__ a_scales_ptr, + // fp16 quantization scales. shape (k/groupsize, n) + const int4* __restrict__ scales_ptr, + // fp16 global scale (for nvfp4// only) + const float* __restrict__ global_scale_ptr, + // 4bit packed zero-points of shape + // (k/groupsize, n/pack_factor) + const int4* __restrict__ zp_ptr, + // int32 group indices of shape k + const int* __restrict__ g_idx, + const int32_t* __restrict__ sorted_token_ids_ptr, // moe sorted_ids + const int32_t* __restrict__ expert_ids_ptr, // moe expert ids + const int32_t* __restrict__ num_tokens_past_padded_ptr, // moe num tokens + const float* __restrict__ topk_weights_ptr, // moe top weights + int top_k, // num of experts per token + bool mul_topk_weights, // mul topk weights or not + int num_groups, // number of scale groups per output channel + int prob_m, // batch dimension m + int prob_n, // output dimension n + int prob_k, // reduction dimension k + int* locks, // extra global storage for barrier synchronization + bool has_bias, + bool use_atomic_add, // whether to use atomic add to reduce + bool use_fp32_reduce // whether to use fp32 global reduce +) { + // Each threadblock processes one "stripe" of the B matrix with (roughly) the + // same size, which might involve multiple column "slices" (of width 16 * + // `thread_n_blocks`). Stripes are defined as shown in the 3x3 matrix 5 SM + // example: + // 0 1 3 + // 0 2 3 + // 1 2 4 + // While this kind of partitioning makes things somewhat more complicated, it + // ensures good utilization of all SMs for many kinds of shape and GPU + // configurations, while requiring as few slow global cross-threadblock + // reductions as possible. + + #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ < 890 + // FP8 computation is only supported for Ada Lovelace or newer architectures. + if constexpr (a_type_id == vllm::kFE4M3fn.id()) return; + #endif + + #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == 750 + // Turing TensorCore only supports fp16 and int8 + if constexpr (a_type_id != vllm::kFloat16.id() && a_type_id != vllm::kS8.id()) + return; + #endif + + int num_tokens_past_padded = num_tokens_past_padded_ptr[0]; + constexpr int moe_block_size = m_block_size_8 ? 8 : (16 * thread_m_blocks); + + #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == 750 + static constexpr auto num_bits = + vllm::ScalarType::from_id(b_type_id).size_bits(); + // Disable use_fp16_accum for NVFP4 and cases when group_size == -1 && + // num_bits == 4 + constexpr bool use_fp16_accum = + a_type_id == vllm::kFloat16.id() && + (!(b_type_id == vllm::kFE2M1f.id() && s_type_id == vllm::kFE4M3fn.id()) && + !(group_blocks == -1 && num_bits == 4)); + #else + constexpr bool use_fp16_accum = false; + #endif + using Adtype = MarlinScalarType; + using Cdtype = MarlinScalarType; + + using scalar_t = typename MarlinScalarType::scalar_t; + using scalar_t2 = typename MarlinScalarType::scalar_t2; + using scalar_32bit_t = typename MarlinScalarType::scalar_32bit_t; + + using c_scalar_t = typename MarlinScalarType::scalar_t; + using c_scalar_t2 = typename MarlinScalarType::scalar_t2; + + using FragA = typename MarlinScalarType::FragA; + using FragB = typename MarlinScalarType::FragB; + using FragC = typename MarlinScalarType::FragC; + using FragS = typename MarlinScalarType::FragS; + using FragZP = typename MarlinScalarType::FragZP; + + extern __shared__ int4 sh[]; + static constexpr auto a_type = vllm::ScalarType::from_id(a_type_id); + static constexpr auto b_type = vllm::ScalarType::from_id(b_type_id); + static constexpr auto c_type = vllm::ScalarType::from_id(c_type_id); + static constexpr auto s_type = vllm::ScalarType::from_id(s_type_id); + if constexpr (b_type == vllm::kFE2M1f) { + static_assert(s_type == vllm::kFE4M3fn && group_blocks == 1 || + s_type == vllm::kFE8M0fnu && group_blocks == 2); + } else if constexpr (b_type == vllm::kFE4M3fn && s_type == vllm::kFE8M0fnu) { + static_assert(group_blocks == 2); + } else if constexpr (std::is_same::value) { + static_assert(s_type == vllm::kBFloat16); + } else if constexpr (std::is_same::value) { + static_assert(s_type == vllm::kFloat16); + } + + constexpr bool is_a_8bit = a_type.size_bits() == 8; + if constexpr (!is_a_8bit) { + static_assert(std::is_same::value); + } + constexpr bool has_zp = b_type == vllm::kU4 || b_type == vllm::kU8; + constexpr bool is_int_type = b_type == vllm::kU4 || b_type == vllm::kU8 || + b_type == vllm::kS4 || b_type == vllm::kS8 || + b_type == vllm::kU4B8 || b_type == vllm::kU8B128; + constexpr bool is_8bit_scale = s_type.size_bits() == 8; + // see comments of dequant.h for more details + constexpr bool dequant_skip_flop = + is_a_8bit || (b_type == vllm::kFE4M3fn && !(s_type == vllm::kFE8M0fnu)) || + b_type == vllm::kFE2M1f && s_type == vllm::kFE4M3fn || + has_zp && !is_zp_float && !std::is_same::value || + has_zp && !is_zp_float && !(b_type == vllm::kU8); + + float global_scale_f32 = 1.0f; + + constexpr bool has_act_order = group_blocks == 0; + + constexpr int pack_factor = 32 / b_type.size_bits(); + static_assert(thread_m_blocks == 1 || !m_block_size_8); + const int group_size = + (!has_act_order && group_blocks == -1) ? prob_k : prob_k / num_groups; + const int scales_expert_stride = + prob_n * prob_k / group_size / (is_8bit_scale ? 16 : 8); + const int zp_expert_stride = + is_zp_float ? prob_n * prob_k / group_size / 8 + : prob_n * prob_k / group_size / (pack_factor * 4); + const int b_bias_expert_stride = prob_n / 8; + + // parallel: num valid moe blocks + int parallel = num_tokens_past_padded / moe_block_size; + + int k_tiles = prob_k / 16 / thread_k_blocks; + int n_tiles = prob_n / 16 / thread_n_blocks; + + int global_mn_tiles = parallel * n_tiles; + int part2_mn_tiles = global_mn_tiles; + int part1_mn_iters = 0; + bool in_part2 = false; + + // we use DP + two-tile SK here + // part1: DP + // part2: two-tile SK + // see https://github.com/vllm-project/vllm/pull/24722 for more details + if (global_mn_tiles > gridDim.x) { + part2_mn_tiles = global_mn_tiles % gridDim.x; + if (part2_mn_tiles * 3 <= gridDim.x) part2_mn_tiles += gridDim.x; + part1_mn_iters = (global_mn_tiles - part2_mn_tiles) / gridDim.x; + } + + int iters = div_ceil(k_tiles * part2_mn_tiles, gridDim.x); + + if constexpr (!has_act_order && group_blocks != -1) { + if (group_blocks >= thread_k_blocks) { + // Ensure that the number of tiles in each stripe is a multiple of the + // groupsize; this avoids an annoying special case where a stripe starts + // in the middle of group. + iters = (group_blocks / thread_k_blocks) * + div_ceil(iters, (group_blocks / thread_k_blocks)); + } + } + + int slice_row = 0; + int slice_col_par = blockIdx.x; + int slice_col; + int slice_iters = + k_tiles; // number of threadblock tiles in the current slice + // total number of active threadblocks in the current slice + int slice_count = 1; + // index of threadblock in current slice; numbered bottom to top + int slice_idx = 0; + + int par_id = 0; + int block_id = -1; + int64_t expert_id = 0; // use int64 to avoid computation result overflow + int old_expert_id = 0; + int64_t B_expert_off = 0; + + float* sh_a_s = reinterpret_cast(sh); + int4* sh_block_sorted_ids_int4 = sh + (is_a_8bit ? (4 * thread_m_blocks) : 0); + int4* sh_rd_block_sorted_ids_int4 = + sh_block_sorted_ids_int4 + moe_block_size / 4; + int4* sh_block_topk_weights_int4 = + sh_rd_block_sorted_ids_int4 + moe_block_size / 4; + // sh_block_topk_weights_int4 only need (moe_block_size / 4); + // but we pad to align to 256 bytes + int4* sh_new = sh_block_topk_weights_int4 + moe_block_size / 2; + int32_t* sh_block_sorted_ids = + reinterpret_cast(sh_block_sorted_ids_int4); + int32_t* sh_rd_block_sorted_ids = + reinterpret_cast(sh_rd_block_sorted_ids_int4); + c_scalar_t2* sh_block_topk_weights = + reinterpret_cast(sh_block_topk_weights_int4); + + int32_t block_num_valid_tokens = 0; + int32_t locks_off = 0; + + // We can easily implement parallel problem execution by just remapping + // indices and advancing global pointers + if (part2_mn_tiles >= gridDim.x) { + // when part2_mn_tiles >= sms + // then there are at most $sms$ conflict tile blocks + locks_off = blockIdx.x; + } else { + locks_off = (iters * blockIdx.x) / k_tiles - 1; + } + + int prob_m_top_k = prob_m * top_k; + // read moe block data given block_id + // block_sorted_ids / block_num_valid_tokens / block_topk_weights + auto read_moe_block_data = [&](int block_id) { + block_num_valid_tokens = moe_block_size; + + cp_async4_pred(sh_block_sorted_ids_int4 + threadIdx.x, + reinterpret_cast(sorted_token_ids_ptr) + + (block_id * moe_block_size / 4 + threadIdx.x), + threadIdx.x < moe_block_size / 4); + + cp_async_fence(); + cp_async_wait<0>(); + + __syncthreads(); + + if (threadIdx.x >= threads - 32) { + constexpr int size_per_thread = div_ceil(moe_block_size, 32); + int lane_id = threadIdx.x - (threads - 32); + + int local_count = 0; + #pragma unroll + for (int i = 0; i < size_per_thread; i++) { + int j = lane_id * size_per_thread + i; + if (j < moe_block_size) { + int idx = sh_block_sorted_ids[j]; + if (idx < prob_m_top_k) local_count++; + } + } + + #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == 750 + + if constexpr (moe_block_size >= 16) + local_count += __shfl_down_sync(0xFFFFFFFF, local_count, 16); + if constexpr (moe_block_size >= 8) + local_count += __shfl_down_sync(0xFFFFFFFF, local_count, 8); + if constexpr (moe_block_size >= 4) + local_count += __shfl_down_sync(0xFFFFFFFF, local_count, 4); + if constexpr (moe_block_size >= 2) + local_count += __shfl_down_sync(0xFFFFFFFF, local_count, 2); + + local_count += __shfl_down_sync(0xFFFFFFFF, local_count, 1); + block_num_valid_tokens = local_count; + #else + block_num_valid_tokens = __reduce_add_sync(0xffffffff, local_count); + #endif + + if (lane_id == 0) + reinterpret_cast(sh_new)[0] = block_num_valid_tokens; + } + + if (threadIdx.x < moe_block_size) { + int idx = sh_block_sorted_ids[threadIdx.x]; + sh_rd_block_sorted_ids[threadIdx.x] = idx / top_k; + + if (mul_topk_weights) { + idx = idx < prob_m_top_k ? idx : 0; + float topk_weight_tmp = topk_weights_ptr[idx]; + if constexpr (b_type == vllm::kFE2M1f && s_type == vllm::kFE4M3fn) { + topk_weight_tmp *= global_scale_f32; + } + c_scalar_t2 topk_weight_val = + Cdtype::num2num2(Cdtype::float2num(topk_weight_tmp)); + sh_block_topk_weights[threadIdx.x] = topk_weight_val; + } + } + + __syncthreads(); + + block_num_valid_tokens = reinterpret_cast(sh_new)[0]; + __syncthreads(); + }; + + // when move to next moe block, find the next block_id and expert_id + // and then read moe block data + auto update_next_moe_block_data = [&]() { + if (par_id >= parallel) return; + + old_expert_id = expert_id; + block_id = par_id; + expert_id = expert_ids_ptr[block_id]; + + if constexpr (b_type == vllm::kFE2M1f && s_type == vllm::kFE4M3fn) { + global_scale_f32 = global_scale_ptr[expert_id]; + } + + B_expert_off = expert_id * prob_n * prob_k / (pack_factor * 4); + scales_ptr += (expert_id - old_expert_id) * scales_expert_stride; + if constexpr (has_zp) { + zp_ptr += (expert_id - old_expert_id) * zp_expert_stride; + } + if constexpr (has_act_order) { + g_idx += (expert_id - old_expert_id) * prob_k; + } + if (has_bias) { + b_bias_ptr += (expert_id - old_expert_id) * b_bias_expert_stride; + } + + read_moe_block_data(block_id); + }; + + // Compute all information about the current slice which is required for + // synchronization. + bool first_init = true; + auto init_part2_slice = [&]() { + slice_iters = + iters * (blockIdx.x + 1) - (k_tiles * slice_col_par + slice_row); + if (slice_iters < 0 || slice_col_par >= part2_mn_tiles) slice_iters = 0; + if (slice_iters == 0) return; + if (slice_row + slice_iters > k_tiles) slice_iters = k_tiles - slice_row; + slice_count = 1; + slice_idx = 0; + int col_first = iters * div_ceil(k_tiles * slice_col_par, iters); + if (col_first <= k_tiles * (slice_col_par + 1)) { + int col_off = col_first - k_tiles * slice_col_par; + slice_count = div_ceil(k_tiles - col_off, iters); + if (col_off > 0) slice_count++; + int delta_first = iters * blockIdx.x - col_first; + if (delta_first < 0 || (col_off == 0 && delta_first == 0)) + slice_idx = slice_count - 1; + else { + slice_idx = slice_count - 1 - delta_first / iters; + if (col_off > 0) slice_idx--; + } + } + if (part2_mn_tiles >= gridDim.x) { + if (slice_count > 1 && slice_idx == slice_count - 1) { + locks_off++; + } + } else { + locks_off++; + } + + if (first_init && use_atomic_add && slice_count > 1 && slice_idx == 0) { + constexpr int threads_per_m = 16 * thread_n_blocks / 8; + int m_per_thread = + div_ceil(block_num_valid_tokens, threads / threads_per_m); + for (int i = 0; i < m_per_thread; i++) { + int row = threads / threads_per_m * i + threadIdx.x / threads_per_m; + if (row < block_num_valid_tokens) { + int64_t sorted_row = sh_block_sorted_ids[row]; + int col = slice_col * 16 * thread_n_blocks / 8 + + threadIdx.x % threads_per_m; + C[sorted_row * prob_n / 8 + col] = {0, 0, 0, 0}; + } + } + // After write zero to output, write a negative value to lock. + // Every SM that processes the same slice would wait for + // the negative value, and then atomicAdd 1 to it. + // After all SMs are processed, the lock value would back to 0 again. + __syncthreads(); + if (threadIdx.x == 0) locks[locks_off] = 1 - slice_count; + } + + if (slice_col == n_tiles) { + slice_col = 0; + par_id++; + update_next_moe_block_data(); + } + if (is_a_8bit && (first_init || slice_col == 0)) { + __syncthreads(); + cp_async1_ca_pred(&sh_a_s[threadIdx.x], + &a_scales_ptr[sh_rd_block_sorted_ids[threadIdx.x]], + threadIdx.x < block_num_valid_tokens); + } + }; + + auto init_part1_slice = [&]() { + if (part1_mn_iters) { + part1_mn_iters--; + par_id = slice_col_par / n_tiles; + slice_col = slice_col_par % n_tiles; + slice_iters = k_tiles; + update_next_moe_block_data(); + if (is_a_8bit) { + __syncthreads(); + cp_async1_ca_pred(&sh_a_s[threadIdx.x], + &a_scales_ptr[sh_rd_block_sorted_ids[threadIdx.x]], + threadIdx.x < block_num_valid_tokens); + } + } + }; + + auto init_slice = [&]() { + if (!in_part2 && !part1_mn_iters) { + in_part2 = true; + slice_col_par = (iters * blockIdx.x) / k_tiles; + slice_row = (iters * blockIdx.x) % k_tiles; + slice_col = (slice_col_par + global_mn_tiles - part2_mn_tiles) % n_tiles; + par_id = (slice_col_par + global_mn_tiles - part2_mn_tiles) / n_tiles; + update_next_moe_block_data(); + } + if (!in_part2) { + init_part1_slice(); + } else { + init_part2_slice(); + first_init = false; + } + }; + + init_slice(); + + // A sizes/strides + + // stride of the A matrix in global memory + int a_gl_stride = prob_k / (is_a_8bit ? 16 : 8); + // stride of an A matrix tile in shared memory + constexpr int a_sh_stride = 16 * thread_k_blocks / (is_a_8bit ? 16 : 8); + // delta between subsequent A tiles in global memory + constexpr int a_gl_rd_delta_o = 16 * thread_k_blocks / (is_a_8bit ? 16 : 8); + // between subsequent accesses within a tile + int a_gl_rd_delta_i = a_gl_stride * (threads / a_gl_rd_delta_o); + // between shared memory writes + constexpr int a_sh_wr_delta = a_sh_stride * (threads / a_gl_rd_delta_o); + // within a shared memory tile + constexpr int a_sh_rd_delta_i = a_sh_stride * 16; + // overall size of a tile + constexpr int a_sh_stage = a_sh_stride * (16 * thread_m_blocks); + // number of shared write iterations for a tile + constexpr int a_sh_wr_iters = div_ceil(a_sh_stage, a_sh_wr_delta); + + // B sizes/strides + int b_gl_stride = 16 * prob_n / (pack_factor * (is_a_8bit ? 2 : 4)); + constexpr int b_sh_stride = + ((thread_n_blocks * 16) * 16 / pack_factor) / (is_a_8bit ? 2 : 4); + constexpr int b_thread_vecs = b_type.size_bits() == 4 ? 1 : 2; + constexpr int b_sh_stride_threads = b_sh_stride / b_thread_vecs; + + int b_gl_rd_delta_o = b_gl_stride * thread_k_blocks / (is_a_8bit ? 2 : 1); + constexpr int b_sh_wr_delta = threads * b_thread_vecs; + constexpr int b_sh_stage = + b_sh_stride * thread_k_blocks / (is_a_8bit ? 2 : 1); + constexpr int b_sh_wr_iters = b_sh_stage / b_sh_wr_delta; + + // Scale sizes/strides without act_order + int s_gl_stride = prob_n / (is_8bit_scale ? 16 : 8); + constexpr int s_sh_stride = 16 * thread_n_blocks / (is_8bit_scale ? 16 : 8); + constexpr int s_tb_groups = + !has_act_order && group_blocks != -1 && group_blocks < thread_k_blocks + ? thread_k_blocks / group_blocks + : 1; + constexpr int s_sh_stage = s_tb_groups * s_sh_stride; + int s_gl_rd_delta = s_gl_stride; + + // Scale size/strides with act_order + constexpr int tb_k = 16 * thread_k_blocks; + constexpr int g_idx_stage = has_act_order ? (tb_k * sizeof(int)) / 16 : 0; + // constexpr int act_s_row_stride = 1; + // int act_s_col_stride = act_s_row_stride * num_groups; + constexpr int act_s_max_num_groups = 32; + int act_s_col_stride = 1; + int act_s_col_warp_stride = act_s_col_stride * 8; + + constexpr int tb_n_warps = thread_n_blocks / (is_a_8bit ? 2 : 4); + int act_s_col_tb_stride = act_s_col_warp_stride * tb_n_warps; + + // Zero-points sizes/strides + int zp_gl_stride = is_zp_float ? prob_n / 8 : (prob_n / pack_factor) / 4; + constexpr int zp_sh_stride = is_zp_float + ? 16 * thread_n_blocks / 8 + : ((16 * thread_n_blocks) / pack_factor) / 4; + constexpr int zp_tb_groups = s_tb_groups; + constexpr int zp_sh_stage = has_zp ? zp_tb_groups * zp_sh_stride : 0; + int zp_gl_rd_delta = zp_gl_stride; + + // Global A read index of current thread. + int a_gl_rd_row = threadIdx.x / a_gl_rd_delta_o; + int a_gl_rd_col = a_gl_rd_delta_o * slice_row + threadIdx.x % a_gl_rd_delta_o; + // Shared write index of current thread. + int a_sh_wr = a_sh_stride * (threadIdx.x / a_gl_rd_delta_o) + + (threadIdx.x % a_gl_rd_delta_o); + // Shared read index. + int a_sh_rd = + a_sh_stride * ((threadIdx.x % 32) % (16 / (m_block_size_8 ? 2 : 1))) + + (threadIdx.x % 32) / (16 / (m_block_size_8 ? 2 : 1)); + a_sh_rd += 2 * ((threadIdx.x / 32) / tb_n_warps) * b_sh_wr_iters; + + int b_gl_rd; + if (threads <= b_sh_stride) { + b_gl_rd = threadIdx.x; + } else { + b_gl_rd = + b_gl_stride * (threadIdx.x / b_sh_stride) + (threadIdx.x % b_sh_stride); + } + + b_gl_rd += B_expert_off + b_sh_stride * slice_col; + b_gl_rd += b_gl_rd_delta_o * slice_row; + auto b_sh_rd = threadIdx.x * b_thread_vecs; + b_sh_rd += b_sh_rd / b_sh_stride * (b_sh_stride * (b_sh_wr_iters - 1)); + + // For act_order + int slice_k_start = tb_k * slice_row; + int slice_k_finish = slice_k_start + tb_k * slice_iters; + int slice_k_start_shared_fetch = slice_k_start; + int slice_n_offset = act_s_col_tb_stride * slice_col; + + // No act_order + int s_gl_rd; + if constexpr (!has_act_order) { + if constexpr (group_blocks == -1) { + s_gl_rd = s_sh_stride * slice_col + threadIdx.x; + } else if constexpr (group_blocks >= thread_k_blocks) { + s_gl_rd = s_gl_stride * ((thread_k_blocks * slice_row) / group_blocks) + + s_sh_stride * slice_col + threadIdx.x; + } else { + s_gl_rd = s_gl_stride * ((thread_k_blocks * slice_row) / group_blocks + + threadIdx.x / s_sh_stride) + + s_sh_stride * slice_col + threadIdx.x % s_sh_stride; + } + } + auto s_sh_wr = threadIdx.x; + bool s_sh_wr_pred = threadIdx.x < s_sh_stage; + + // Zero-points + int zp_gl_rd; + if constexpr (has_zp) { + if constexpr (group_blocks == -1) { + zp_gl_rd = zp_sh_stride * slice_col + threadIdx.x; + } else if constexpr (group_blocks >= thread_k_blocks) { + zp_gl_rd = zp_gl_stride * ((thread_k_blocks * slice_row) / group_blocks) + + zp_sh_stride * slice_col + threadIdx.x; + } else { + zp_gl_rd = zp_gl_stride * ((thread_k_blocks * slice_row) / group_blocks + + threadIdx.x / zp_sh_stride) + + zp_sh_stride * slice_col + threadIdx.x % zp_sh_stride; + } + } + auto zp_sh_wr = threadIdx.x; + bool zp_sh_wr_pred = zp_sh_stage > 0 && threadIdx.x < zp_sh_stage; + + // We use a different scale layout for grouped and column-wise quantization as + // we scale a `half2` tile in column-major layout in the former and in + // row-major in the latter case. + int s_sh_rd; + if constexpr (is_a_8bit) { + s_sh_rd = 4 * ((threadIdx.x / 32) % tb_n_warps) + (threadIdx.x % 4); + } else if constexpr (group_blocks != -1) + s_sh_rd = 8 * ((threadIdx.x / 32) % tb_n_warps) + (threadIdx.x % 32) / 4; + else if constexpr (group_blocks == -1 && + (m_block_size_8 || (has_zp && !dequant_skip_flop))) + s_sh_rd = 8 * ((threadIdx.x / 32) % tb_n_warps) + (threadIdx.x % 32) / 8; + else + s_sh_rd = 8 * ((threadIdx.x / 32) % tb_n_warps) + (threadIdx.x % 32) % 4; + + int bias_sh_rd; + if constexpr (m_block_size_8) { + bias_sh_rd = 8 * ((threadIdx.x / 32) % tb_n_warps) + (threadIdx.x % 32) / 8; + } else { + bias_sh_rd = (is_a_8bit ? 4 : 8) * ((threadIdx.x / 32) % tb_n_warps) + + (threadIdx.x % 32) % 4; + } + + int bias_sh_wr = threadIdx.x; + int bias_gl_rd = (thread_n_blocks * 16 / 8) * slice_col + threadIdx.x; + + // Zero-points have the same read layout as the scales + // (without column-wise case) + constexpr int num_col_threads = 8; + constexpr int num_row_threads = 4; + constexpr int num_ints_per_thread = 8 / pack_factor; + int zp_sh_rd; + if constexpr (has_zp) { + if constexpr (is_zp_float) { + if constexpr (group_blocks != -1) { + zp_sh_rd = + 8 * ((threadIdx.x / 32) % tb_n_warps) + (threadIdx.x % 32) / 4; + } + } else if (is_a_8bit) { + zp_sh_rd = num_ints_per_thread * num_col_threads * + ((threadIdx.x / 32) % tb_n_warps / 2) + + num_ints_per_thread * ((threadIdx.x % 32) / num_row_threads); + } else { + zp_sh_rd = num_ints_per_thread * num_col_threads * + ((threadIdx.x / 32) % tb_n_warps) + + num_ints_per_thread * ((threadIdx.x % 32) / num_row_threads); + } + } + + // To ensure that writing and reading A tiles to/from shared memory, the + // latter in fragment format, is fully bank conflict free, we need to use a + // rather fancy XOR-based layout. The key here is that neither reads nor + // writes of the 16-byte `int4` blocks of 8 consecutive threads involve the + // same shared memory banks. Further, it seems (based on NSight-Compute) that + // each warp must also write a consecutive memory segment? + auto transform_a = [&](int i) { + int row = i / a_gl_rd_delta_o; + return a_gl_rd_delta_o * row + (i % a_gl_rd_delta_o) ^ (row % 8); + }; + // Since the computation of this remapping is non-trivial and, due to our main + // loop unrolls, all shared memory accesses are static, we simply precompute + // both transformed reads and writes. + int a_sh_wr_trans[a_sh_wr_iters]; + #pragma unroll + for (int i = 0; i < a_sh_wr_iters; i++) + a_sh_wr_trans[i] = transform_a(a_sh_wr_delta * i + a_sh_wr); + int a_sh_rd_trans[b_sh_wr_iters][thread_m_blocks]; + #pragma unroll + for (int i = 0; i < b_sh_wr_iters; i++) { + #pragma unroll + for (int j = 0; j < thread_m_blocks; j++) + a_sh_rd_trans[i][j] = transform_a(2 * i + a_sh_rd_delta_i * j + a_sh_rd); + } + + // Since B-accesses have non-constant stride they have to be computed at + // runtime; we break dependencies between subsequent accesses with a tile by + // maintining multiple pointers (we have enough registers), a tiny + // optimization. + + // Shared memory storage for global fetch pipelines. + constexpr int sh_red_size = (2 * thread_n_blocks + 1) * 16 * thread_m_blocks; + constexpr int sh_b_size = stages * b_sh_stage; + int4* sh_b = sh_new; + int4* sh_red = sh_new; + + constexpr int sh_size_b_red_min = + (sh_red_size < sh_b_size ? sh_red_size : sh_b_size); + constexpr int sh_size_b_red_max = + (sh_red_size > sh_b_size ? sh_red_size : sh_b_size); + constexpr int sh_bias_size = (thread_n_blocks * 16 / 8); + constexpr int sh_b_red_bias_size = + sh_size_b_red_max > (sh_size_b_red_min + sh_bias_size) + ? sh_size_b_red_max + : (sh_size_b_red_min + sh_bias_size); + + int4* sh_bias = sh_new + sh_size_b_red_min; + int4* sh_g_idx = sh_new + sh_b_red_bias_size; + int4* sh_zp = sh_g_idx + (stages * g_idx_stage); + constexpr int sh_s_size = has_act_order ? (act_s_max_num_groups * s_sh_stride) + : (stages * s_sh_stage); + int4* sh_s = sh_zp + (stages * zp_sh_stage); + int4* sh_a = sh_s + sh_s_size; + + // Register storage for double buffer of shared memory reads. + FragA frag_a[2][thread_m_blocks]; + I4 frag_b_quant[2][b_thread_vecs]; + FragC frag_c[thread_m_blocks][is_a_8bit ? 2 : 4][2]; + FragC frag_c_tmp[thread_m_blocks][is_a_8bit ? 2 : 4][2]; + FragS frag_s[2][4]; // No act-order + FragS frag_bias[2][4]; + FragS act_frag_s[2][4][4]; // For act-order + int frag_qzp[2][num_ints_per_thread]; // Zero-points + FragZP frag_zp; // Zero-points in fp16 + FragZP frag_zpf[2]; // Zero-points in fp16 in HQQ + + if constexpr (is_a_8bit && group_blocks != -1) { + #pragma unroll + for (int j = 0; j < 2; j++) { + #pragma unroll + for (int i = 0; i < thread_m_blocks; i++) { + #pragma unroll + for (int g = 0; g < 4; g++) { + frag_c_tmp[i][j][0][g] = 0.0f; + } + + #pragma unroll + for (int g = 0; g < 4; g++) { + frag_c_tmp[i][j][1][g] = 0.0f; + } + } + } + } + + // Zero accumulators. + auto zero_accums = [&]() { + #pragma unroll + for (int i = 0; i < thread_m_blocks * 4 * 2 * 4; i++) + reinterpret_cast(frag_c)[i] = 0; + }; + + int sh_first_group_id = -1; + int sh_num_groups = -1; + + auto fetch_act_order_scales_to_shared = [&](bool is_async, int first_group_id, + int last_group_id) { + sh_first_group_id = first_group_id; + sh_num_groups = last_group_id - first_group_id + 1; + + if (sh_num_groups > act_s_max_num_groups) { + sh_num_groups = act_s_max_num_groups; + } + + if (sh_first_group_id + sh_num_groups > num_groups) { + sh_num_groups = num_groups - sh_first_group_id; + } + + int row_offset = first_group_id * s_gl_stride; + + if (is_async) { + for (int i = 0; i < sh_num_groups; i++) { + if (threadIdx.x < s_sh_stride) { + cp_async4_pred(&sh_s[(i * s_sh_stride) + threadIdx.x], + &scales_ptr[row_offset + (i * s_gl_stride) + + slice_n_offset + threadIdx.x]); + } + } + } else { + for (int i = 0; i < sh_num_groups; i++) { + if (threadIdx.x < s_sh_stride) { + sh_s[(i * s_sh_stride) + threadIdx.x] = + scales_ptr[row_offset + (i * s_gl_stride) + slice_n_offset + + threadIdx.x]; + } + } + } + }; + // Asynchronously fetch the next A, B and s tile from global to the next + // shared memory pipeline location. + auto fetch_to_shared = [&](int pipe, int a_off, bool pred = true) { + if (pred) { + int4* sh_a_stage = sh_a + moe_block_size * a_sh_stride * pipe; + #pragma unroll + for (int i = 0; i < a_sh_wr_iters; i++) { + int row = a_gl_rd_delta_i / a_gl_stride * i + a_gl_rd_row; + int64_t sorted_row = 0; + if (!m_block_size_8 || row < 8) + sorted_row = sh_rd_block_sorted_ids[row]; + int64_t true_idx = + sorted_row * a_gl_stride + a_gl_rd_col + a_gl_rd_delta_o * a_off; + cp_async4_pred(&sh_a_stage[a_sh_wr_trans[i]], &A[true_idx], + row < block_num_valid_tokens); + } + + int4* sh_b_stage = sh_b + b_sh_stage * pipe; + #pragma unroll + for (int i = 0; i < (b_sh_wr_iters * b_thread_vecs); i++) { + constexpr int count = div_ceil(b_sh_stride, threads); + int b_gl_idx = + b_gl_rd + (i % count) * threads + + b_gl_stride * (i / count) * div_ceil(threads, b_sh_stride); + + cp_async4(&sh_b_stage[threads * i + threadIdx.x], &B[b_gl_idx]); + } + + b_gl_rd += b_gl_rd_delta_o; + + if constexpr (has_act_order) { + // Fetch g_idx thread-block portion + int full_pipe = a_off; + int cur_k = slice_k_start_shared_fetch + tb_k * full_pipe; + if (cur_k < prob_k && cur_k < slice_k_finish) { + int4* sh_g_idx_stage = sh_g_idx + g_idx_stage * pipe; + + int4 const* cur_g_idx_stage_ptr = + reinterpret_cast(&g_idx[cur_k]); + + if (threadIdx.x < g_idx_stage) { + cp_async4_pred(&sh_g_idx_stage[threadIdx.x], + &cur_g_idx_stage_ptr[threadIdx.x]); + } + } + } else { + if constexpr (group_blocks != -1) { + int4* sh_s_stage = sh_s + s_sh_stage * pipe; + + // Only fetch scales if this tile starts a new group + if (pipe % div_ceil(group_blocks, thread_k_blocks) == 0) { + if (s_sh_wr_pred) { + cp_async4(&sh_s_stage[s_sh_wr], &scales_ptr[s_gl_rd]); + } + s_gl_rd += s_gl_rd_delta * s_tb_groups; + } + } + + if constexpr (has_zp && group_blocks != -1) { + int4* sh_zp_stage = sh_zp + zp_sh_stage * pipe; + + // Only fetch zero points if this tile starts a new group + if (pipe % div_ceil(group_blocks, thread_k_blocks) == 0) { + if (zp_sh_wr_pred) { + cp_async4(&sh_zp_stage[zp_sh_wr], &zp_ptr[zp_gl_rd]); + } + zp_gl_rd += zp_gl_rd_delta * zp_tb_groups; + } + } + } + } + // Insert a fence even when we are winding down the pipeline to ensure that + // waiting is also correct at this point. + cp_async_fence(); + }; + + auto fetch_col_zp_to_shared = [&]() { + if (zp_sh_wr_pred) { + cp_async4(&sh_zp[zp_sh_wr], &zp_ptr[zp_gl_rd]); + } + }; + + auto fetch_col_scale_to_shared = [&]() { + if (s_sh_wr_pred) { + cp_async4(&sh_s[s_sh_wr], &scales_ptr[s_gl_rd]); + } + }; + + // Wait until the next thread tile has been loaded to shared memory. + auto wait_for_stage = [&]() { + // We only have `stages - 2` active fetches since we are double buffering + // and can only issue the next fetch when it is guaranteed that the previous + // shared memory load is fully complete (as it may otherwise be + // overwritten). + cp_async_wait(); + __syncthreads(); + }; + + // Load the next sub-tile from the current location in the shared memory pipe + // into the current register buffer. + auto fetch_to_registers = [&](int k, int pipe) { + int4* sh_a_stage = sh_a + moe_block_size * a_sh_stride * pipe; + #pragma unroll + for (int i = 0; i < thread_m_blocks; i++) + ldsm( + frag_a[k % 2][i], &sh_a_stage[a_sh_rd_trans[k % b_sh_wr_iters][i]]); + int4* sh_b_stage = sh_b + b_sh_stage * pipe; + + #pragma unroll + for (int i = 0; i < b_thread_vecs; i++) { + frag_b_quant[k % 2][i] = *reinterpret_cast( + &sh_b_stage[b_sh_stride * (k % b_sh_wr_iters) + b_sh_rd + i]); + } + }; + + bool is_same_group[stages]; + int same_group_id[stages]; + + auto init_same_group = [&](int pipe) { + if constexpr (!has_act_order) { + return; + } + + int4* sh_g_idx_stage = sh_g_idx + g_idx_stage * pipe; + int* sh_g_idx_int_ptr = reinterpret_cast(sh_g_idx_stage); + + int group_id_1 = sh_g_idx_int_ptr[0]; + int group_id_2 = sh_g_idx_int_ptr[tb_k - 1]; + + is_same_group[pipe] = group_id_1 == group_id_2; + same_group_id[pipe] = group_id_1; + }; + + auto fetch_scales_to_registers = [&](int k, int full_pipe) { + int pipe = full_pipe % stages; + using IT1 = typename std::conditional_t; + using IT0 = typename std::conditional_t; + constexpr int group_blocks2 = div_ceil(group_blocks, is_a_8bit ? 2 : 1); + + if constexpr (!has_act_order) { + // No act-order case + if constexpr (group_blocks == -1) { + // load only when starting a new slice + if (k == 0 && full_pipe == 0 && dequant_skip_flop) { + reinterpret_cast(&frag_s)[0] = sh_s[s_sh_rd]; + reinterpret_cast(&frag_s)[1] = sh_s[s_sh_rd + 4]; + } + } else if constexpr (group_blocks != -1) { + if constexpr (group_blocks >= thread_k_blocks) { + constexpr int g = group_blocks / thread_k_blocks; + if (pipe % g == 0) { + if (k % b_sh_wr_iters == 0) { + int4* sh_s_stage = sh_s + s_sh_stage * (g * (pipe / g)); + reinterpret_cast(&frag_s[k % 2])[0] = sh_s_stage[s_sh_rd]; + } else { + reinterpret_cast(&frag_s[1])[0] = + reinterpret_cast(&frag_s[0])[0]; + } + } + } else if (group_blocks2 < b_sh_wr_iters || k % b_sh_wr_iters == 0) { + auto warp_id = threadIdx.x / 32; + int warp_row = warp_id / tb_n_warps; + + int k_blocks = b_sh_wr_iters * warp_row + k % b_sh_wr_iters; + int cur_group_id = k_blocks / group_blocks2; + + int4* sh_s_stage = sh_s + s_sh_stage * pipe; + + if constexpr (!is_8bit_scale) { + reinterpret_cast(&frag_s[k % 2])[0] = + sh_s_stage[s_sh_rd + cur_group_id * s_sh_stride]; + } else { + reinterpret_cast(&frag_s[k % 2])[0] = + reinterpret_cast( + sh_s_stage)[s_sh_rd + cur_group_id * (2 * s_sh_stride)]; + } + } else if (group_blocks >= b_sh_wr_iters) { + if constexpr (!is_8bit_scale) { + reinterpret_cast(&frag_s[1])[0] = + reinterpret_cast(&frag_s[0])[0]; + } else { + reinterpret_cast(&frag_s[1])[0] = + reinterpret_cast(&frag_s[0])[0]; + } + } + } + + return; + } + + // Act-order case + + // Determine K of the "current" thread-block + int cur_k = slice_k_start + tb_k * full_pipe; + if (cur_k >= prob_k || cur_k >= slice_k_finish) { + return; + } + + // Reset (to current thread-block) since we read g_idx portion from the + // shared memory + cur_k = 0; + + // Progress to current iteration + cur_k += k % b_sh_wr_iters; + + // Determine "position" inside the thread-block (based on warp and + // thread-id) + auto warp_id = threadIdx.x / 32; + int warp_row = warp_id / tb_n_warps; + int warp_col = warp_id % tb_n_warps; + + cur_k += warp_row * 16 * b_sh_wr_iters; + + auto th_id = threadIdx.x % 32; + cur_k += (th_id % 4) * 2; // Due to tensor-core layout for fp16 B matrix + + int s_col_shift = + /*slice_n_offset +*/ (act_s_col_warp_stride * warp_col) + + (th_id / 4) * act_s_col_stride; + + if (is_same_group[pipe]) { + if (k % 2 == 0) { + *(reinterpret_cast(&(act_frag_s[k % 2][0][0]))) = + sh_s[(same_group_id[pipe] - sh_first_group_id) * s_sh_stride + + s_col_shift]; + } else { + *(reinterpret_cast(&(act_frag_s[k % 2][0][0]))) = + *(reinterpret_cast(&(act_frag_s[(k - 1) % 2][0][0]))); + } + + for (int i = 1; i < 4; i++) { + *(reinterpret_cast(&(act_frag_s[k % 2][i][0]))) = + *(reinterpret_cast(&(act_frag_s[k % 2][0][0]))); + } + return; + } + + int4* sh_g_idx_stage = sh_g_idx + g_idx_stage * pipe; + int* sh_g_idx_int_ptr = reinterpret_cast(sh_g_idx_stage); + + constexpr int k_frag_offsets[4] = {0, 1, 8, + 9}; // Tensor core offsets per thread + + #pragma unroll + for (int i = 0; i < 4; i++) { + int actual_k = cur_k + k_frag_offsets[i]; + + int group_id = sh_g_idx_int_ptr[actual_k]; + int rel_group_id = group_id - sh_first_group_id; + + *(reinterpret_cast(&(act_frag_s[k % 2][i][0]))) = + sh_s[rel_group_id * s_sh_stride + s_col_shift]; + } + }; + + auto fetch_zp_to_registers = [&](int k, int full_pipe) { + // This code does not handle group_blocks == 0, + // which signifies act_order. + // has_zp implies AWQ, which doesn't have act_order, + static_assert(!has_zp || group_blocks != 0); + + if constexpr (has_zp && !is_zp_float) { + int pipe = full_pipe % stages; + + if constexpr (group_blocks == -1) { + // load only when starting a new slice + if (k == 0 && full_pipe == 0 || is_a_8bit) { + #pragma unroll + for (int i = 0; i < num_ints_per_thread; i++) { + frag_qzp[k % 2][i] = (reinterpret_cast(sh_zp))[zp_sh_rd + i]; + } + } + } else if constexpr (group_blocks >= thread_k_blocks) { + constexpr int g = group_blocks / thread_k_blocks; + if (pipe % g == 0 && k % b_sh_wr_iters == 0 || is_a_8bit) { + int4* sh_zp_stage = sh_zp + zp_sh_stage * (g * (pipe / g)); + #pragma unroll + for (int i = 0; i < num_ints_per_thread; i++) { + frag_qzp[k % 2][i] = + (reinterpret_cast(sh_zp_stage))[zp_sh_rd + i]; + } + } + } else { + auto warp_id = threadIdx.x / 32; + + int warp_row = warp_id / tb_n_warps; + + int k_blocks = b_sh_wr_iters * warp_row + k % b_sh_wr_iters; + int cur_group_id = k_blocks / div_ceil(group_blocks, is_a_8bit ? 2 : 1); + + int4* sh_zp_stage = sh_zp + zp_sh_stage * pipe; + + sh_zp_stage += cur_group_id * zp_sh_stride; + + #pragma unroll + for (int i = 0; i < num_ints_per_thread; i++) { + frag_qzp[k % 2][i] = + (reinterpret_cast(sh_zp_stage))[zp_sh_rd + i]; + } + } + } + + else if constexpr (has_zp && is_zp_float) { + int pipe = full_pipe % stages; + + if constexpr (group_blocks != -1) { + if constexpr (group_blocks >= thread_k_blocks) { + constexpr int g = group_blocks / thread_k_blocks; + if (pipe % g == 0 && k % b_sh_wr_iters == 0) { + int4* sh_zp_stage = sh_zp + zp_sh_stage * (g * (pipe / g)); + reinterpret_cast(&frag_zpf[k % 2])[0] = + sh_zp_stage[zp_sh_rd]; + } + } else if (group_blocks < b_sh_wr_iters || k % b_sh_wr_iters == 0) { + auto warp_id = threadIdx.x / 32; + + int warp_row = warp_id / tb_n_warps; + int k_blocks = b_sh_wr_iters * warp_row + k % b_sh_wr_iters; + int cur_group_id = k_blocks / group_blocks; + + int4* sh_zp_stage = sh_zp + zp_sh_stage * pipe; + + reinterpret_cast(&frag_zpf[k % 2])[0] = + sh_zp_stage[zp_sh_rd + cur_group_id * zp_sh_stride]; + } + } + } + }; + + auto dequant_data = [&](int q, scalar_32bit_t* frag_b_ptr, int zp = 0) { + if constexpr (a_type.size_bits() != b_type.size_bits()) { + if constexpr (is_a_8bit && has_zp) { + sub_zp_and_dequant( + q, frag_b_ptr, zp); + } else { + dequant(q, frag_b_ptr); + } + } + }; + + // Execute the actual tensor core matmul of a sub-tile. + bool is_first_matmul_in_slice = true; + auto matmul = [&](int k, int pipe) { + if (is_a_8bit) return; + int k2 = k % 2; + constexpr int g = + group_blocks > 0 ? div_ceil(group_blocks, thread_k_blocks) : 1; + const bool is_new_zp = + (group_blocks == 0) || + ((group_blocks > 0) && (group_blocks < b_sh_wr_iters || k == 0)) && + (pipe % g == 0) || + (group_blocks == -1 && is_first_matmul_in_slice); + if constexpr (has_zp && !is_zp_float) { + if (is_new_zp) { + if constexpr (group_blocks == -1) is_first_matmul_in_slice = false; + int zp_quant_0, zp_quant_1; + + if constexpr (b_type.size_bits() == 4) { + zp_quant_0 = frag_qzp[k2][0]; + zp_quant_1 = zp_quant_0 >> 8; + } else { + static_assert(b_type.size_bits() == 8); + zp_quant_0 = frag_qzp[k2][0]; + zp_quant_1 = frag_qzp[k2][1]; + } + + dequant_data(zp_quant_0, reinterpret_cast(&frag_zp)); + dequant_data(zp_quant_1, + reinterpret_cast(&frag_zp) + 2); + } + } + if constexpr (!dequant_skip_flop && has_zp && is_zp_float) { + if (is_new_zp) { + reinterpret_cast(&frag_zp)[0] = + reinterpret_cast(&frag_zpf[k2])[0]; + } + } + + if constexpr (s_type == vllm::kFE4M3fn || s_type == vllm::kFE8M0fnu) { + int s_quant_0 = reinterpret_cast(frag_s[k2])[0]; + int s_quant_1 = reinterpret_cast(frag_s[k2])[1]; + + dequant_fp8_scales( + s_quant_0, reinterpret_cast(&frag_s[k2])); + dequant_fp8_scales( + s_quant_1, reinterpret_cast(&frag_s[k2]) + 2); + } + + // We have the m dimension as the inner loop in order to encourage overlapping + // dequantization and matmul operations. + #pragma unroll + for (int j = 0; j < 4; j++) { + FragB frag_b0; + FragB frag_b1; + int b_quant_0, b_quant_1; + + if constexpr (b_type_id == vllm::kFE2M1f.id()) { + b_quant_1 = frag_b_quant[k2][0][j]; + b_quant_0 = b_quant_1 << 8; + } else if constexpr (b_type.size_bits() == 4) { + b_quant_0 = frag_b_quant[k2][0][j]; + b_quant_1 = b_quant_0 >> 8; + } else { + static_assert(b_type.size_bits() == 8); + int* frag_b_quant_ptr = reinterpret_cast(frag_b_quant[k2]); + b_quant_0 = frag_b_quant_ptr[j * 2 + 0]; + b_quant_1 = frag_b_quant_ptr[j * 2 + 1]; + } + + dequant_data(b_quant_0, reinterpret_cast(&frag_b0)); + dequant_data(b_quant_1, reinterpret_cast(&frag_b1)); + + if constexpr (dequant_skip_flop && has_zp && !is_zp_float && !is_a_8bit) { + sub_zp(frag_b0, frag_zp[j], 0); + sub_zp(frag_b1, frag_zp[j], 1); + } + + // Apply scale to frag_b0 + if constexpr (has_act_order && !is_a_8bit) { + static_assert(group_blocks != -1); + scale4(frag_b0, act_frag_s[k2][0][j], act_frag_s[k2][1][j], + act_frag_s[k2][2][j], act_frag_s[k2][3][j], 0); + scale4(frag_b1, act_frag_s[k2][0][j], act_frag_s[k2][1][j], + act_frag_s[k2][2][j], act_frag_s[k2][3][j], 1); + } else if constexpr (!dequant_skip_flop && has_zp && !is_zp_float && + group_blocks == -1 && !is_a_8bit) { + int idx = (threadIdx.x / 4) % 2; + scalar_t2 s2 = Adtype::nums2num2( + reinterpret_cast(&frag_s[j / 2][j % 2 * 2 + 0])[idx], + reinterpret_cast(&frag_s[j / 2][j % 2 * 2 + 1])[idx]); + if (is_new_zp) frag_zp[j] = __hmul2(frag_zp[j], s2); + scale_and_sub(frag_b0, s2.x, frag_zp[j].x); + scale_and_sub(frag_b1, s2.y, frag_zp[j].y); + } else if constexpr (!dequant_skip_flop && has_zp && group_blocks != -1 && + !is_a_8bit) { + if (is_new_zp) + frag_zp[j] = __hmul2(frag_zp[j], + *reinterpret_cast(&frag_s[k2][j])); + scale_and_sub(frag_b0, frag_s[k2][j][0].x, frag_zp[j].x); + scale_and_sub(frag_b1, frag_s[k2][j][0].y, frag_zp[j].y); + } else if constexpr (group_blocks != -1 && !is_a_8bit) { + scale(frag_b0, frag_s[k2][j], 0); + scale(frag_b1, frag_s[k2][j], 1); + } + + #pragma unroll + for (int i = 0; i < thread_m_blocks; i++) { + if constexpr (m_block_size_8) { + mma_trans(frag_a[k2][i], frag_b0, frag_b1, + frag_c[i][j][0]); + } else { + mma(frag_a[k2][i], frag_b0, + frag_c[i][j][0]); + mma(frag_a[k2][i], frag_b1, + frag_c[i][j][1]); + } + } + } + }; + + auto matmul_a8 = [&](int k) { + int k2 = k % 2; + #pragma unroll + for (int j = 0; j < 2; j++) { + FragB frag_b[2]; + + if (is_a_8bit && b_type.size_bits() == 4 && !has_zp) { + dequant_data(frag_b_quant[k2][0][j * 2], + reinterpret_cast(&frag_b)); + dequant_data(frag_b_quant[k2][0][j * 2 + 1], + reinterpret_cast(&frag_b) + 2); + } else if (is_a_8bit && b_type.size_bits() == 4 && has_zp) { + int off = (threadIdx.x / 32) % 2 * 2 + j; + int zp = (frag_qzp[k2][0] >> (off * 8)) & 0xF; + dequant_data(frag_b_quant[k2][0][j * 2], + reinterpret_cast(&frag_b), zp); + zp = (frag_qzp[k2][0] >> (off * 8 + 4)) & 0xF; + dequant_data(frag_b_quant[k2][0][j * 2 + 1], + reinterpret_cast(&frag_b) + 2, zp); + } else { + reinterpret_cast(&frag_b)[0] = + reinterpret_cast(&frag_b_quant[k2][j])[0]; + reinterpret_cast(&frag_b)[1] = + reinterpret_cast(&frag_b_quant[k2][j])[1]; + } + + #pragma unroll + for (int i = 0; i < thread_m_blocks; i++) { + mma( + frag_a[k2][i], frag_b[0], + (group_blocks == -1 ? frag_c : frag_c_tmp)[i][j][0]); + mma( + frag_a[k2][i], frag_b[1], + (group_blocks == -1 ? frag_c : frag_c_tmp)[i][j][1]); + } + + if constexpr (group_blocks != -1) { + if (group_blocks == 2 || k == 1) { + if constexpr (a_type == vllm::kS8) { + int2 s_vals[2]; + s_vals[0] = { + (int)reinterpret_cast(&frag_s[k2][j * 2][0])[0], + (int)reinterpret_cast(&frag_s[k2][j * 2][0])[1]}; + s_vals[1] = { + (int)reinterpret_cast(&frag_s[k2][j * 2 + 1][0])[0], + (int)reinterpret_cast(&frag_s[k2][j * 2 + 1][0])[1]}; + + #pragma unroll + for (int i = 0; i < thread_m_blocks; i++) { + #pragma unroll + for (int g = 0; g < 4; g++) { + int scale = reinterpret_cast(&s_vals[0])[g % 2]; + *reinterpret_cast(&frag_c[i][j][0][g]) += + *reinterpret_cast(&frag_c_tmp[i][j][0][g]) * + scale; + frag_c_tmp[i][j][0][g] = 0.0f; + } + + #pragma unroll + for (int g = 0; g < 4; g++) { + int scale = reinterpret_cast(&s_vals[1])[g % 2]; + *reinterpret_cast(&frag_c[i][j][1][g]) += + *reinterpret_cast(&frag_c_tmp[i][j][1][g]) * + scale; + frag_c_tmp[i][j][1][g] = 0.0f; + } + } + } else { + float2 s_vals[2]; + if constexpr (s_type_id != vllm::kFE8M0fnu.id()) { + static_assert(a_type.size_bits() == 16 || + s_type.size_bits() == 16); + s_vals[0] = Cdtype::num22float2(frag_s[k2][j * 2][0]); + s_vals[1] = Cdtype::num22float2(frag_s[k2][j * 2 + 1][0]); + } else { + int32_t* s_vals_int = reinterpret_cast(&s_vals[0]); + int32_t s_vals_e8m0 = + *reinterpret_cast(&frag_s[k2][j][0]); + + s_vals_int[0] = (s_vals_e8m0 & 0xFF) << 23; + s_vals_int[1] = (s_vals_e8m0 & 0xFF00) << 15; + s_vals_int[2] = (s_vals_e8m0 & 0xFF0000) << 7; + s_vals_int[3] = (s_vals_e8m0 & 0xFF000000) >> 1; + } + + #pragma unroll + for (int i = 0; i < thread_m_blocks; i++) { + #pragma unroll + for (int g = 0; g < 4; g++) { + float scale = reinterpret_cast(&s_vals[0])[g % 2]; + frag_c[i][j][0][g] += frag_c_tmp[i][j][0][g] * scale; + frag_c_tmp[i][j][0][g] = 0.0f; + } + + #pragma unroll + for (int g = 0; g < 4; g++) { + float scale = reinterpret_cast(&s_vals[1])[g % 2]; + frag_c[i][j][1][g] += frag_c_tmp[i][j][1][g] * scale; + frag_c_tmp[i][j][1][g] = 0.0f; + } + } + } + } + } + } + }; + + // Since we slice across the k dimension of a tile in order to increase the + // number of warps while keeping the n dimension of a tile reasonable, we have + // multiple warps that accumulate their partial sums of the same output + // location; which we have to reduce over in the end. We do in shared memory. + auto thread_block_reduce = [&]() { + constexpr int red_off = threads / b_sh_stride_threads / 2; + if (red_off >= 1) { + auto red_idx = threadIdx.x / b_sh_stride_threads; + constexpr int red_sh_stride = + b_sh_stride_threads * (is_a_8bit ? 2 : 4) * 2; + constexpr int red_sh_delta = b_sh_stride_threads; + int red_sh_rd = red_sh_stride * (threadIdx.x / b_sh_stride_threads) + + (threadIdx.x % b_sh_stride_threads); + + // Parallel logarithmic shared memory reduction. We make sure to avoid any + // unnecessary read or write iterations, e.g., for two warps we write only + // once by warp 1 and read only once by warp 0. + + #pragma unroll + for (int m_block = 0; m_block < thread_m_blocks; m_block++) { + #pragma unroll + for (int i = red_off; i > 0; i /= 2) { + if (i <= red_idx && red_idx < 2 * i) { + #pragma unroll + for (int j = 0; j < (is_a_8bit ? 2 : 4) * 2; + j += (m_block_size_8 ? 2 : 1)) { + int red_sh_wr = + red_sh_delta * j + (red_sh_rd - red_sh_stride * i); + if (i < red_off) { + float* c_rd = reinterpret_cast( + &sh_red[red_sh_delta * j + red_sh_rd]); + float* c_wr = reinterpret_cast(&sh_red[red_sh_wr]); + #pragma unroll + for (int k = 0; k < 4; k++) + reinterpret_cast( + frag_c)[(is_a_8bit ? 2 : 4) * 2 * m_block + j][k] += + c_rd[k] + c_wr[k]; + } + sh_red[red_sh_wr] = reinterpret_cast( + &frag_c)[(is_a_8bit ? 2 : 4) * 2 * m_block + j]; + } + } + __syncthreads(); + } + if (red_idx == 0) { + #pragma unroll + for (int i = 0; i < (is_a_8bit ? 2 : 4) * 2; + i += (m_block_size_8 ? 2 : 1)) { + float* c_rd = + reinterpret_cast(&sh_red[red_sh_delta * i + red_sh_rd]); + #pragma unroll + for (int j = 0; j < 4; j++) + reinterpret_cast( + frag_c)[(is_a_8bit ? 2 : 4) * 2 * m_block + i][j] += c_rd[j]; + } + } + __syncthreads(); + } + } + }; + + // Since multiple threadblocks may process parts of the same column slice, we + // finally have to globally reduce over the results. As the striped + // partitioning minimizes the number of such reductions and our outputs are + // usually rather small, we perform this reduction serially in L2 cache. + auto global_reduce_fp16 = [&](bool first = false, bool last = false) { + // We are very careful here to reduce directly in the output buffer to + // maximize L2 cache utilization in this step. To do this, we write out + // results in FP16 (but still reduce with FP32 compute). + constexpr int active_threads = 32 * tb_n_warps; + bool is_th_active = threadIdx.x < active_threads; + if (!is_th_active) { + return; + } + + int c_gl_stride = prob_n / 8 * (is_a_8bit ? 2 : 1); + int c_gl_wr_delta_o = 8 * c_gl_stride; + int c_gl_wr_delta_i = 4 * (active_threads / 32); + int c_gl_wr; + if constexpr (m_block_size_8) { + c_gl_wr = c_gl_stride * ((threadIdx.x % 4) * 2) + 4 * (threadIdx.x / 32) + + (threadIdx.x % 32) / 8; + c_gl_wr += (2 * thread_n_blocks) * slice_col; + } else { + c_gl_wr = c_gl_stride * ((threadIdx.x % 32) / 4) + + 4 * (threadIdx.x / 32) + threadIdx.x % 4; + c_gl_wr += (2 * thread_n_blocks) * slice_col * (is_a_8bit ? 2 : 1); + } + constexpr int c_sh_wr_delta = active_threads; + int c_sh_wr = threadIdx.x; + + if (!first) { + + #pragma unroll + for (int i = 0; i < (m_block_size_8 ? 2 : thread_m_blocks * 4); i++) { + int c_idx; + if constexpr (m_block_size_8) + c_idx = c_gl_wr + i * c_gl_stride + + (threadIdx.x % 8) / 4 * c_gl_wr_delta_i; + else + c_idx = + c_gl_wr + c_gl_wr_delta_o * (i / 2) + c_gl_wr_delta_i * (i % 2); + if (c_idx / c_gl_stride < block_num_valid_tokens) { + int64_t sorted_row = sh_block_sorted_ids[c_idx / c_gl_stride]; + int64_t true_idx = sorted_row * c_gl_stride + c_idx % c_gl_stride; + if constexpr (is_a_8bit) { + int2* sh_red_int2 = reinterpret_cast(sh_red); + int2* c_int2 = reinterpret_cast(C); + sh_red_int2[c_sh_wr + c_sh_wr_delta * i] = c_int2[true_idx]; + } else { + sh_red[c_sh_wr + c_sh_wr_delta * i] = C[true_idx]; + } + } + } + } + + #pragma unroll + for (int i = 0; i < (m_block_size_8 ? 2 : thread_m_blocks * 4); i++) { + if (!first) { + c_scalar_t* c_red_f16; + if constexpr (is_a_8bit) { + int2 tmp = + reinterpret_cast(sh_red)[c_sh_wr + i * c_sh_wr_delta]; + c_red_f16 = reinterpret_cast(&tmp); + } else { + int4 tmp = sh_red[c_sh_wr + i * c_sh_wr_delta]; + c_red_f16 = reinterpret_cast(&tmp); + } + #pragma unroll + for (int j = 0; j < 2 * (is_a_8bit ? 2 : 4); j++) { + int delta = 0; + if constexpr (m_block_size_8) { + delta = j % 2 == 1 ? -2 : 0; + } + reinterpret_cast( + &frag_c)[(is_a_8bit ? 2 : 4) * 2 * 4 * (i / 4) + 4 * j + (i % 4) + + delta] += Cdtype::num2float(c_red_f16[j]); + } + } + if (!last) { + c_scalar_t c_f16[is_a_8bit ? 4 : 8]; + #pragma unroll + for (int j = 0; j < 2 * (is_a_8bit ? 2 : 4); j++) { + int delta = 0; + if constexpr (m_block_size_8) { + delta = j % 2 == 1 ? -2 : 0; + } + c_f16[j] = Cdtype::float2num(reinterpret_cast( + &frag_c)[(is_a_8bit ? 2 : 4) * 2 * 4 * (i / 4) + 4 * j + (i % 4) + + delta]); + } + + int c_idx; + if constexpr (m_block_size_8) + c_idx = c_gl_wr + i * c_gl_stride + + (threadIdx.x % 8) / 4 * c_gl_wr_delta_i; + else + c_idx = + c_gl_wr + c_gl_wr_delta_o * (i / 2) + c_gl_wr_delta_i * (i % 2); + if (c_idx / c_gl_stride < block_num_valid_tokens) { + int64_t sorted_row = sh_block_sorted_ids[c_idx / c_gl_stride]; + int64_t true_idx = sorted_row * c_gl_stride + c_idx % c_gl_stride; + if constexpr (is_a_8bit) { + int2* c_int2 = reinterpret_cast(C); + c_int2[true_idx] = *reinterpret_cast(c_f16); + } else { + C[true_idx] = *reinterpret_cast(c_f16); + } + } + } + } + }; + + // Globally reduce over threadblocks that compute the same column block. + // We use a tmp C buffer to reduce in full fp32 precision. + auto global_reduce_fp32 = [&](bool first = false, bool last = false) { + constexpr int tb_m = thread_m_blocks * 16; + constexpr int tb_n = thread_n_blocks * 16; + + constexpr int c_size = tb_m * tb_n * sizeof(float) / 16; + + constexpr int active_threads = 32 * tb_n_warps; + bool is_th_active = threadIdx.x < active_threads; + + constexpr int num_floats = thread_m_blocks * (is_a_8bit ? 2 : 4) * 2 * 4; + constexpr int th_size = num_floats * sizeof(float) / 16; + + int c_cur_offset = locks_off * c_size; + + if (!is_th_active) { + return; + } + + if (!first) { + float* frag_c_ptr = reinterpret_cast(&frag_c); + #pragma unroll + for (int k = 0; k < th_size; k++) { + if constexpr (m_block_size_8) { + if (k % 2) continue; + } else { + if (k / 8 * 16 + (threadIdx.x % 32) / 4 >= block_num_valid_tokens) + continue; + } + + sh_red[threadIdx.x] = + C_tmp[c_cur_offset + active_threads * k + threadIdx.x]; + + float* sh_c_ptr = reinterpret_cast(&sh_red[threadIdx.x]); + #pragma unroll + for (int f = 0; f < 4; f++) { + frag_c_ptr[k * 4 + f] += sh_c_ptr[f]; + } + } + } + + if (!last) { + int4* frag_c_ptr = reinterpret_cast(&frag_c); + #pragma unroll + for (int k = 0; k < th_size; k++) { + if constexpr (m_block_size_8) { + if (k % 2) continue; + } else { + if (k / 8 * 16 + (threadIdx.x % 32) / 4 >= block_num_valid_tokens) + continue; + } + + C_tmp[c_cur_offset + active_threads * k + threadIdx.x] = frag_c_ptr[k]; + } + } + }; + + // Write out the reduce final result in the correct layout. We only actually + // reshuffle matrix fragments in this step, the reduction above is performed + // in fragment layout. + auto write_result = [&](bool last) { + int c_gl_stride = prob_n / 8; + constexpr int c_sh_stride = 2 * thread_n_blocks + 1; + int c_gl_wr_delta = c_gl_stride * (threads / (2 * thread_n_blocks)); + constexpr int c_sh_rd_delta = + c_sh_stride * (threads / (2 * thread_n_blocks)); + + int c_gl_wr = c_gl_stride * (threadIdx.x / (2 * thread_n_blocks)) + + (threadIdx.x % (2 * thread_n_blocks)); + c_gl_wr += (2 * thread_n_blocks) * slice_col; + int c_sh_wr; + if constexpr (m_block_size_8) { + c_sh_wr = (8 * c_sh_stride) * ((threadIdx.x % 32) % 4 * 2) + + (threadIdx.x % 32) / 4; + c_sh_wr += 64 * (threadIdx.x / 32); + } else { + c_sh_wr = + (4 * c_sh_stride) * ((threadIdx.x % 32) / 4) + (threadIdx.x % 32) % 4; + c_sh_wr += (is_a_8bit ? 16 : 32) * (threadIdx.x / 32); + } + + int c_sh_rd = c_sh_stride * (threadIdx.x / (2 * thread_n_blocks)) + + (threadIdx.x % (2 * thread_n_blocks)); + + // We first reorder in shared memory to guarantee the most efficient final + // global write patterns + auto write = [&](int idx, float c0, float c1, FragS& s, FragS& b_bias) { + if constexpr (b_type == vllm::kFE2M1f && s_type == vllm::kFE4M3fn) { + if (!mul_topk_weights) { + c0 *= global_scale_f32; + c1 *= global_scale_f32; + } + } + + c_scalar_t2 res = + Cdtype::nums2num2(Cdtype::float2num(c0), Cdtype::float2num(c1)); + + // For per-column quantization we finally apply the scale here (only for + // 4-bit) + if constexpr (!has_act_order && group_blocks == -1 && !is_a_8bit && + b_type.size_bits() == 4 && + (has_zp && dequant_skip_flop || !has_zp)) { + c_scalar_t2 tmp_scale = s[0]; + if constexpr (m_block_size_8) { + tmp_scale = Cdtype::num2num2( + reinterpret_cast(&s[0])[(threadIdx.x % 8) / 4]); + } + res = __hmul2(res, tmp_scale); + } + + if (has_bias && last) { + c_scalar_t2 tmp_bias = b_bias[0]; + if constexpr (m_block_size_8) { + tmp_bias = Cdtype::num2num2( + reinterpret_cast(&b_bias[0])[(threadIdx.x % 8) / 4]); + } + res = __hadd2(res, tmp_bias); + } + + if constexpr (m_block_size_8) { + ((c_scalar_t*)sh_red)[idx] = res.x; + ((c_scalar_t*)sh_red)[idx + 8 * c_sh_stride] = res.y; + } else { + ((c_scalar_t2*)sh_red)[idx] = res; + } + }; + + if (threadIdx.x / 32 < tb_n_warps) { + #pragma unroll + for (int i = 0; i < thread_m_blocks; i++) { + #pragma unroll + for (int j = 0; j < (is_a_8bit ? 2 : 4); j++) { + if constexpr (m_block_size_8) { + int wr = c_sh_wr + 16 * j; + write(wr, frag_c[i][j][0][0], frag_c[i][j][0][1], + frag_s[j / 2][2 * (j % 2) + 0], + frag_bias[j / 2][2 * (j % 2) + 0]); + write(wr + 8, frag_c[i][j][0][2], frag_c[i][j][0][3], + frag_s[j / 2][2 * (j % 2) + 1], + frag_bias[j / 2][2 * (j % 2) + 1]); + } else { + int wr = c_sh_wr + 8 * j; + write(wr + (4 * c_sh_stride) * 0 + 0, frag_c[i][j][0][0], + frag_c[i][j][0][1], frag_s[j / 2][2 * (j % 2) + 0], + frag_bias[j / 2][2 * (j % 2) + 0]); + write(wr + (4 * c_sh_stride) * 8 + 0, frag_c[i][j][0][2], + frag_c[i][j][0][3], frag_s[j / 2][2 * (j % 2) + 0], + frag_bias[j / 2][2 * (j % 2) + 0]); + write(wr + (4 * c_sh_stride) * 0 + 4, frag_c[i][j][1][0], + frag_c[i][j][1][1], frag_s[j / 2][2 * (j % 2) + 1], + frag_bias[j / 2][2 * (j % 2) + 1]); + write(wr + (4 * c_sh_stride) * 8 + 4, frag_c[i][j][1][2], + frag_c[i][j][1][3], frag_s[j / 2][2 * (j % 2) + 1], + frag_bias[j / 2][2 * (j % 2) + 1]); + } + } + c_sh_wr += 16 * (4 * c_sh_stride); + } + } + __syncthreads(); + + #pragma unroll + for (int i = 0; + i < div_ceil(16 * thread_m_blocks, threads / (2 * thread_n_blocks)); + i++) { + int row = c_gl_wr / c_gl_stride; + if (row < block_num_valid_tokens) { + int64_t sorted_row = sh_block_sorted_ids[row]; + int64_t true_idx = sorted_row * c_gl_stride + c_gl_wr % c_gl_stride; + c_scalar_t2 topk_weight_score; + if (mul_topk_weights) topk_weight_score = sh_block_topk_weights[row]; + if (use_atomic_add && slice_count > 1 || mul_topk_weights) { + c_scalar_t2* C_half2 = reinterpret_cast(&C[true_idx]); + c_scalar_t2* sh_red_half2 = + reinterpret_cast(&sh_red[c_sh_rd]); + if (mul_topk_weights) { + #pragma unroll + for (int a = 0; a < 4; a++) { + sh_red_half2[a] = __hmul2(sh_red_half2[a], topk_weight_score); + } + } + + if (use_atomic_add && slice_count > 1) { + #pragma unroll + for (int a = 0; a < 4; a++) { + atomicAdd(&C_half2[a], sh_red_half2[a]); + } + } else { + C[true_idx] = *reinterpret_cast(sh_red_half2); + } + } else { + C[true_idx] = sh_red[c_sh_rd]; + } + c_gl_wr += c_gl_wr_delta; + c_sh_rd += c_sh_rd_delta; + } + } + __syncthreads(); + }; + + // Start global fetch and register load pipelines. + auto start_pipes = [&]() { + + #pragma unroll + for (int i = 0; i < stages - 1; i++) { + if (has_act_order && i == 0) { + int last_g_idx = slice_k_start + stages * tb_k * 2; + if (last_g_idx >= prob_k) { + last_g_idx = prob_k - 1; + } + fetch_act_order_scales_to_shared(true, g_idx[slice_k_start], + g_idx[last_g_idx]); + } + + if constexpr (has_zp && !is_zp_float && group_blocks == -1) { + if (i == 0) { + fetch_col_zp_to_shared(); + if constexpr (!dequant_skip_flop) { + fetch_col_scale_to_shared(); + } + } + } + fetch_to_shared(i, i, i < slice_iters); + } + + zero_accums(); + wait_for_stage(); + init_same_group(0); + fetch_to_registers(0, 0); + fetch_scales_to_registers(0, 0); + fetch_zp_to_registers(0, 0); + a_gl_rd_col += a_gl_rd_delta_o * (stages - 1); + if constexpr (has_act_order) { + slice_k_start_shared_fetch += tb_k * (stages - 1); + } + }; + if (slice_iters) { + start_pipes(); + } + + // Main loop. + while (slice_iters) { + // We unroll over both the global fetch and the register load pipeline to + // ensure all shared memory accesses are static. Note that both pipelines + // have even length meaning that the next iteration will always start at + // index 0. + + #pragma unroll + for (int pipe = 0; pipe < stages;) { + #pragma unroll + for (int k = 0; k < b_sh_wr_iters; k++) { + fetch_to_registers(k + 1, pipe % stages); + fetch_scales_to_registers(k + 1, pipe); + fetch_zp_to_registers(k + 1, pipe); + if (k == b_sh_wr_iters - 2) { + fetch_to_shared((pipe + stages - 1) % stages, pipe, + slice_iters >= stages); + pipe++; + wait_for_stage(); + init_same_group(pipe % stages); + } + + if constexpr (!is_a_8bit) { + matmul(k, pipe - (k >= b_sh_wr_iters - 2 ? 1 : 0)); + } else { + static_assert(group_blocks != 0 && group_blocks != 1); + matmul_a8(k); + } + } + slice_iters--; + if (slice_iters == 0) { + break; + } + } + + a_gl_rd_col += a_gl_rd_delta_o * stages; + + if constexpr (has_act_order) { + slice_k_start += tb_k * stages; + + if (slice_k_start < prob_k) { + slice_k_start_shared_fetch += tb_k * stages; + int first_group_id = g_idx[slice_k_start]; + int last_g_idx = slice_k_start + stages * tb_k * 2; + if (last_g_idx >= prob_k) { + last_g_idx = prob_k - 1; + } + int last_group_id = g_idx[last_g_idx]; + if (last_group_id >= sh_first_group_id + sh_num_groups) { + fetch_act_order_scales_to_shared(false, first_group_id, + last_group_id); + __syncthreads(); + } + } + } + + // Process results and, if necessary, proceed to the next column slice. + // While this pattern may not be the most readable, other ways of writing + // the loop seemed to noticeably worse performance after compilation. + if (slice_iters == 0) { + // convert fp16 accum to fp32 for reduction + if constexpr (use_fp16_accum) { + #pragma unroll + for (int i = 0; i < (thread_m_blocks * (is_a_8bit ? 2 : 4) * 2); i++) { + float* frag_c_part_float = reinterpret_cast(frag_c) + i * 4; + scalar_t* frag_c_part_half = + reinterpret_cast(frag_c_part_float); + + #pragma unroll + for (int i = 3; i >= 0; i--) { + frag_c_part_float[i] = Cdtype::num2float(frag_c_part_half[i]); + } + } + } + + if constexpr (is_a_8bit) { + float frag_a_s[2 * thread_m_blocks]; + + for (int i = 0; i < 2 * thread_m_blocks; i++) + frag_a_s[i] = sh_a_s[i * 8 + (threadIdx.x % 32) / 4]; + + #pragma unroll + for (int j = 0; j < 2; j++) { + #pragma unroll + for (int i = 0; i < thread_m_blocks; i++) { + #pragma unroll + for (int g = 0; g < 4; g++) { + float c_val = frag_c[i][j][0][g]; + + if constexpr (a_type == vllm::kS8) { + c_val = __int2float_rn(*reinterpret_cast(&c_val)); + } + float s_val = frag_a_s[i * 2 + g / 2]; + frag_c[i][j][0][g] = c_val * s_val; + } + #pragma unroll + for (int g = 0; g < 4; g++) { + float c_val = frag_c[i][j][1][g]; + + if constexpr (a_type == vllm::kS8) { + c_val = __int2float_rn(*reinterpret_cast(&c_val)); + } + float s_val = frag_a_s[i * 2 + g / 2]; + frag_c[i][j][1][g] = c_val * s_val; + } + } + } + } + + cp_async_wait<0>(); + bool last = slice_idx == slice_count - 1; + // For per-column scales, we only fetch them here in the final step before + // write-out + if constexpr (!has_act_order && group_blocks == -1 && + (has_zp && dequant_skip_flop || !has_zp)) { + if (b_type.size_bits() == 8 || (last || use_atomic_add) || is_a_8bit) { + if (s_sh_wr_pred) { + cp_async4(&sh_s[s_sh_wr], &scales_ptr[s_gl_rd]); + } + cp_async_fence(); + } + } + + thread_block_reduce(); + + if (has_bias && last) { + __syncthreads(); + cp_async4_pred(&sh_bias[bias_sh_wr], &b_bias_ptr[bias_gl_rd], + threadIdx.x < 16 * thread_n_blocks / 8); + cp_async_fence(); + } + + if constexpr (!has_act_order && group_blocks == -1 && + (has_zp && dequant_skip_flop || !has_zp || is_a_8bit)) { + if constexpr (is_a_8bit) { + cp_async_wait<0>(); + __syncthreads(); + if (threadIdx.x / 32 < tb_n_warps) { + reinterpret_cast(&frag_s)[0] = sh_s[s_sh_rd + 0]; + } + } else if (b_type.size_bits() == 8 || (last || use_atomic_add)) { + cp_async_wait<0>(); + __syncthreads(); + if (threadIdx.x / 32 < tb_n_warps) { + reinterpret_cast(&frag_s)[0] = sh_s[s_sh_rd + 0]; + reinterpret_cast(&frag_s)[1] = sh_s[s_sh_rd + 4]; + if constexpr (m_block_size_8) { + int idx = (threadIdx.x / 4) % 2; + c_scalar_t2* frag_s_half2 = + reinterpret_cast(frag_s); + #pragma unroll + for (int i = 0; i < 8; i++) { + frag_s_half2[i] = Cdtype::num2num2( + reinterpret_cast(&frag_s_half2[i])[idx]); + } + } + } + } + } + + // For 8-bit channelwise, we apply the scale before the global reduction + // that converts the fp32 results to fp16 (so that we avoid possible + // overflow in fp16) + if constexpr (!has_act_order && group_blocks == -1 && is_a_8bit) { + #pragma unroll + for (int j = 0; j < 2; j++) { + float2 aa[2]; + aa[0] = Cdtype::num22float2(frag_s[0][j * 2][0]); + aa[1] = Cdtype::num22float2(frag_s[0][j * 2 + 1][0]); + + #pragma unroll + for (int i = 0; i < thread_m_blocks; i++) { + #pragma unroll + for (int g = 0; g < 4; g++) { + float scale = reinterpret_cast(&aa[0])[g % 2]; + frag_c[i][j][0][g] *= scale; + } + + #pragma unroll + for (int g = 0; g < 4; g++) { + float scale = reinterpret_cast(&aa[1])[g % 2]; + frag_c[i][j][1][g] *= scale; + } + } + } + } else if (!has_act_order && group_blocks == -1 && + b_type.size_bits() == 8 && + (has_zp && dequant_skip_flop || !has_zp)) { + if (threadIdx.x / 32 < tb_n_warps) { + #pragma unroll + for (int i = 0; i < thread_m_blocks; i++) { + #pragma unroll + for (int j = 0; j < 4; j++) { + scale_float( + reinterpret_cast(&frag_c[i][j][0][0]), + frag_s[j / 2][2 * (j % 2) + 0]); + scale_float( + reinterpret_cast(&frag_c[i][j][0][2]), + frag_s[j / 2][2 * (j % 2) + (m_block_size_8 ? 1 : 0)]); + + if constexpr (!m_block_size_8) { + scale_float( + reinterpret_cast(&frag_c[i][j][1][0]), + frag_s[j / 2][2 * (j % 2) + 1]); + scale_float( + reinterpret_cast(&frag_c[i][j][1][2]), + frag_s[j / 2][2 * (j % 2) + 1]); + } + } + } + } + } + + if (slice_count > 1 && !use_atomic_add) { + // only globally reduce if there is more than one block in a slice + barrier_acquire(&locks[locks_off], slice_idx); + if (use_fp32_reduce) { + global_reduce_fp32(slice_idx == 0, last); + } else { + global_reduce_fp16(slice_idx == 0, last); + } + barrier_release(&locks[locks_off], last); + } + + if (has_bias && last) { + cp_async_wait<0>(); + __syncthreads(); + reinterpret_cast(&frag_bias)[0] = sh_bias[bias_sh_rd]; + if constexpr (!is_a_8bit) + reinterpret_cast(&frag_bias)[1] = sh_bias[bias_sh_rd + 4]; + __syncthreads(); + } + + if (use_atomic_add && slice_count > 1 && slice_idx != 0) + wait_negative_and_add(&locks[locks_off]); + if (last || use_atomic_add) + // only the last block in a slice actually writes the result + write_result(last); + slice_row = 0; + if (!in_part2) { + slice_col_par += gridDim.x; + } else { + slice_col_par++; + slice_col++; + } + is_first_matmul_in_slice = true; + init_slice(); + + if (slice_iters) { + a_gl_rd_col = + a_gl_rd_delta_o * slice_row + threadIdx.x % a_gl_rd_delta_o; + b_gl_rd = B_expert_off + b_gl_stride * (threadIdx.x / b_sh_stride) + + (threadIdx.x % b_sh_stride); + b_gl_rd += b_sh_stride * slice_col + b_gl_rd_delta_o * slice_row; + + bias_gl_rd = (thread_n_blocks * 16 / 8) * slice_col + threadIdx.x; + // Update slice k/n for scales loading + if constexpr (has_act_order) { + slice_k_start = tb_k * slice_row; + slice_k_finish = slice_k_start + tb_k * slice_iters; + slice_k_start_shared_fetch = slice_k_start; + slice_n_offset = act_s_col_tb_stride * slice_col; + } else { + if constexpr (group_blocks == -1) { + s_gl_rd = s_sh_stride * slice_col + threadIdx.x; + zp_gl_rd = zp_sh_stride * slice_col + threadIdx.x; + } else if constexpr (group_blocks >= thread_k_blocks) { + s_gl_rd = + s_gl_stride * ((thread_k_blocks * slice_row) / group_blocks) + + s_sh_stride * slice_col + threadIdx.x; + zp_gl_rd = + zp_gl_stride * ((thread_k_blocks * slice_row) / group_blocks) + + zp_sh_stride * slice_col + threadIdx.x; + } else { + s_gl_rd = + s_gl_stride * ((thread_k_blocks * slice_row) / group_blocks + + threadIdx.x / s_sh_stride) + + s_sh_stride * slice_col + threadIdx.x % s_sh_stride; + zp_gl_rd = + zp_gl_stride * ((thread_k_blocks * slice_row) / group_blocks + + threadIdx.x / zp_sh_stride) + + zp_sh_stride * slice_col + threadIdx.x % zp_sh_stride; + } + } + start_pipes(); + } + } + } +} + +} // namespace MARLIN_NAMESPACE_NAME + +#endif diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/ops_standalone.cu b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/ops_standalone.cu new file mode 100644 index 0000000000..7cff87bffa --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/ops_standalone.cu @@ -0,0 +1,405 @@ +/* + * Standalone extraction of vLLM's NVFP4 Marlin MoE GEMM (W4A16, bf16 act/out). + * Ported from vllm csrc .../moe/marlin_moe_wna16/ops.cu: + * - kept the marlin_mm launcher + helpers verbatim (launcher_body.inc), + * - ported the moe_wna16_marlin_gemm wrapper from torch::stable::Tensor to + * regular torch::Tensor, + * - replaced STABLE_TORCH_LIBRARY_IMPL registration with a PYBIND11_MODULE. + */ + +#ifndef MARLIN_NAMESPACE_NAME + #define MARLIN_NAMESPACE_NAME marlin_moe_wna16 +#endif + +#include "kernel.h" + +#include +#include +#include +#include + +// STD_TORCH_CHECK comes from the header-only torch ABI; the kernel headers and +// the launcher use it. Keep it available for launcher_body.inc. +#include + +#define STATIC_ASSERT_SCALAR_TYPE_VALID(scalar_t) \ + static_assert(std::is_same::value || \ + std::is_same::value, \ + "only float16 and bfloat16 is supported"); + +// ---- launcher + helpers (verbatim from upstream ops.cu lines 42..541) ---- +#include "launcher_body.inc" + +// ------------------------------------------------------------------------- +// Ported wrapper: regular torch::Tensor signature. +// b_q_type is received as an int64 ScalarType id (vllm::ScalarTypeId) and +// reconstructed via vllm::ScalarType::from_id(). +// ------------------------------------------------------------------------- + +torch::Tensor moe_wna16_marlin_gemm( + torch::Tensor& a, std::optional c_or_none, + torch::Tensor& b_q_weight, + std::optional const& b_bias_or_none, + torch::Tensor& b_scales, + std::optional const& a_scales_or_none, + std::optional const& global_scale_or_none, + std::optional const& b_zeros_or_none, + std::optional const& g_idx_or_none, + std::optional const& perm_or_none, + torch::Tensor& workspace, torch::Tensor& sorted_token_ids, + torch::Tensor& expert_ids, torch::Tensor& num_tokens_past_padded, + torch::Tensor& topk_weights, int64_t moe_block_size, int64_t top_k, + bool mul_topk_weights, int64_t b_type_id, int64_t size_m, int64_t size_n, + int64_t size_k, bool is_k_full, bool use_atomic_add, bool use_fp32_reduce, + bool is_zp_float, int64_t thread_k, int64_t thread_n, + int64_t blocks_per_sm) { + vllm::ScalarTypeId a_type_id, c_type_id, s_type_id; + + auto c_dtype = a.scalar_type(); + if (a.scalar_type() == at::ScalarType::Half) { + a_type_id = vllm::kFloat16.id(); + c_type_id = vllm::kFloat16.id(); + } else if (a.scalar_type() == at::ScalarType::BFloat16) { + a_type_id = vllm::kBFloat16.id(); + c_type_id = vllm::kBFloat16.id(); + } else { + c_dtype = b_scales.scalar_type(); + if (b_scales.scalar_type() == at::ScalarType::Half) { + c_type_id = vllm::kFloat16.id(); + } else if (b_scales.scalar_type() == at::ScalarType::BFloat16) { + c_type_id = vllm::kBFloat16.id(); + } else { + c_type_id = vllm::kBFloat16.id(); + + TORCH_CHECK(c_or_none.has_value(), "c must be passed for W4A8-FP4"); + torch::Tensor c = c_or_none.value(); + c_dtype = c.scalar_type(); + + if (c.scalar_type() == at::ScalarType::Half) { + c_type_id = vllm::kFloat16.id(); + } else if (c.scalar_type() == at::ScalarType::BFloat16) { + c_type_id = vllm::kBFloat16.id(); + } else { + TORCH_CHECK(false, "unsupported c dtype"); + } + } + + if (a.scalar_type() == at::ScalarType::Float8_e4m3fn) { + a_type_id = vllm::kFE4M3fn.id(); + } else if (a.scalar_type() == at::ScalarType::Char) { + a_type_id = vllm::kS8.id(); + } else { + TORCH_CHECK(false, "unsupported `a` scalar_type"); + } + } + + s_type_id = c_type_id; + if (b_type_id == vllm::kFE2M1f.id()) { + if (b_scales.scalar_type() == at::ScalarType::Float8_e4m3fn) { + s_type_id = vllm::kFE4M3fn.id(); + } else if (b_scales.scalar_type() == at::ScalarType::Float8_e8m0fnu) { + s_type_id = vllm::kFE8M0fnu.id(); + } else { + TORCH_CHECK(false, + "When b_type = float4_e2m1f, b_scale scalar type must be", + "float8_e4m3fn (for NVFP4) or float8_e8m0fnu (for MXFP4)."); + } + } else if (b_type_id == vllm::kFE4M3fn.id() && + b_scales.scalar_type() == at::ScalarType::Float8_e8m0fnu) { + s_type_id = vllm::kFE8M0fnu.id(); + } + + vllm::ScalarType a_type = vllm::ScalarType::from_id(a_type_id); + vllm::ScalarType b_type = vllm::ScalarType::from_id(b_type_id); + vllm::ScalarType c_type = vllm::ScalarType::from_id(c_type_id); + vllm::ScalarType s_type = vllm::ScalarType::from_id(s_type_id); + + int pack_factor = 32 / b_type.size_bits(); + int num_experts = b_q_weight.size(0); + + if (moe_block_size != 8) { + TORCH_CHECK(moe_block_size % 16 == 0, + "unsupported moe_block_size=", moe_block_size); + TORCH_CHECK(moe_block_size >= 16 && moe_block_size <= 64, + "unsupported moe_block_size=", moe_block_size); + } + + // Verify A + TORCH_CHECK(a.size(0) == size_m, "Shape mismatch: a.size(0) = ", a.size(0), + ", size_m = ", size_m); + TORCH_CHECK(a.size(1) == size_k, "Shape mismatch: a.size(1) = ", a.size(1), + ", size_k = ", size_k); + + // Verify B + TORCH_CHECK( + size_k % MARLIN_NAMESPACE_NAME::tile_size == 0, "size_k = ", size_k, + " is not divisible by tile_size = ", MARLIN_NAMESPACE_NAME::tile_size); + TORCH_CHECK( + (size_k / MARLIN_NAMESPACE_NAME::tile_size) == b_q_weight.size(1), + "Shape mismatch: b_q_weight.size(1) = ", b_q_weight.size(1), + ", size_k = ", size_k, + ", tile_size = ", MARLIN_NAMESPACE_NAME::tile_size); + TORCH_CHECK( + b_q_weight.size(2) % MARLIN_NAMESPACE_NAME::tile_size == 0, + "b_q_weight.size(2) = ", b_q_weight.size(2), + " is not divisible by tile_size = ", MARLIN_NAMESPACE_NAME::tile_size); + int actual_size_n = + (b_q_weight.size(2) / MARLIN_NAMESPACE_NAME::tile_size) * pack_factor; + TORCH_CHECK(size_n == actual_size_n, "size_n = ", size_n, + ", actual_size_n = ", actual_size_n); + + // Verify device and strides + TORCH_CHECK(a.device().is_cuda(), "A is not on GPU"); + TORCH_CHECK(a.is_contiguous(), "A is not contiguous"); + + TORCH_CHECK(b_q_weight.device().is_cuda(), "b_q_weight is not on GPU"); + TORCH_CHECK(b_q_weight.is_contiguous(), "b_q_weight is not contiguous"); + + TORCH_CHECK(b_scales.device().is_cuda(), "b_scales is not on GPU"); + TORCH_CHECK(b_scales.is_contiguous(), "b_scales is not contiguous"); + + torch::Tensor a_scales; + auto opts_f32 = a.options().dtype(at::kFloat); + + if (a_scales_or_none.has_value()) { + a_scales = a_scales_or_none.value(); + TORCH_CHECK(a_type.size_bits() == 8, + "a_scales can only be used for 8bit activation."); + } else { + a_scales = torch::empty({0}, opts_f32); + TORCH_CHECK(a_type.size_bits() != 8, + "the a_scales parameter must be passed for 8bit activation."); + } + + // sms: number of SMs to use for the kernel + int sms = -1; + cudaDeviceGetAttribute(&sms, cudaDevAttrMultiProcessorCount, a.get_device()); + + // Alloc buffers + const at::cuda::OptionalCUDAGuard device_guard(device_of(a)); + torch::Tensor c; + if (c_or_none.has_value()) { + c = c_or_none.value(); + TORCH_CHECK(c.device().is_cuda(), "c is not on GPU"); + TORCH_CHECK(c.is_contiguous(), "c is not contiguous"); + TORCH_CHECK(c.size(0) == size_m * top_k, + "Shape mismatch: c.size(0) = ", c.size(0), + ", size_m * topk = ", size_m * top_k); + TORCH_CHECK(c.size(1) == size_n, "Shape mismatch: c.size(1) = ", c.size(1), + ", size_n = ", size_n); + } else { + c = torch::empty({size_m * top_k, size_n}, a.options().dtype(c_dtype)); + } + + // Alloc C tmp buffer that is going to be used for the global reduce + torch::Tensor c_tmp; + auto options_fp32 = a.options().dtype(at::kFloat); + if (use_fp32_reduce && !use_atomic_add) { + // max num of threadblocks is sms * 4 + long max_c_tmp_size = min( + (long)size_n * sorted_token_ids.size(0), + (long)sms * 4 * moe_block_size * MARLIN_NAMESPACE_NAME::max_thread_n); + if (moe_block_size == 8) max_c_tmp_size *= 2; + c_tmp = torch::empty({max_c_tmp_size}, options_fp32); + } else { + c_tmp = torch::empty({0}, options_fp32); + } + + // Detect groupsize and act_order + int num_groups = -1; + int group_size = -1; + + int rank = b_scales.dim(); + TORCH_CHECK(rank == 3, "b_scales rank = ", rank, " is not 3"); + TORCH_CHECK(b_scales.size(2) == size_n, "b_scales dim 2 = ", b_scales.size(2), + " is not size_n = ", size_n); + num_groups = b_scales.size(1); + + torch::Tensor g_idx, perm, a_tmp; + auto opts_a = a.options().dtype(c_dtype); + if (g_idx_or_none.has_value() && perm_or_none.has_value()) { + g_idx = g_idx_or_none.value(); + perm = perm_or_none.value(); + + TORCH_CHECK(g_idx.device().is_cuda(), "g_idx is not on GPU"); + TORCH_CHECK(g_idx.is_contiguous(), "g_idx is not contiguous"); + TORCH_CHECK(perm.device().is_cuda(), "perm is not on GPU"); + TORCH_CHECK(perm.is_contiguous(), "perm is not contiguous"); + + // Verify g_idx and perm + TORCH_CHECK((g_idx.size(-1) == 0 && perm.size(-1) == 0) || + (g_idx.size(-1) == size_k && perm.size(-1) == size_k), + "Unexpected g_idx.size(-1) = ", g_idx.size(-1), + " and perm.size(-1) = ", perm.size(-1), + ", where size_k = ", size_k); + } else { + g_idx = torch::empty({0}, opts_a); + perm = torch::empty({0}, opts_a); + a_tmp = torch::empty({0}, opts_a); + } + bool has_act_order = g_idx.size(-1) > 0 && perm.size(-1) > 0; + + if (has_act_order) { + a_tmp = torch::empty({size_m * top_k, size_k}, opts_a); + if (is_k_full) { + TORCH_CHECK(num_groups > 1, "For act_order, num_groups must be > 1"); + TORCH_CHECK(size_k % num_groups == 0, "size_k = ", size_k, + ", is not divisible by num_groups = ", num_groups); + group_size = size_k / num_groups; + } else { + group_size = 0; + } + + } else { + a_tmp = torch::empty({0}, opts_a); + if (num_groups > 1) { + TORCH_CHECK( + size_k % num_groups == 0, "size_k = ", size_k, + ", is not divisible by b_scales.size(1) = ", b_scales.size(1)); + group_size = size_k / num_groups; + } else { + group_size = -1; + } + } + + torch::Tensor global_scale; + if (global_scale_or_none.has_value()) { + global_scale = global_scale_or_none.value(); + TORCH_CHECK(b_type == vllm::kFE2M1f && s_type == vllm::kFE4M3fn, + "global_scale can only be used for nvfp4 format."); + } else { + global_scale = torch::empty({0}, options_fp32); + TORCH_CHECK(!(b_type == vllm::kFE2M1f && s_type == vllm::kFE4M3fn), + "the global_scale parameter must be passed for nvfp4 format."); + } + + bool has_bias = b_bias_or_none.has_value(); + torch::Tensor b_bias; + if (has_bias) { + b_bias = b_bias_or_none.value(); + TORCH_CHECK(b_bias.device().is_cuda(), "b_bias is not on GPU"); + TORCH_CHECK(b_bias.is_contiguous(), "b_bias is not contiguous"); + TORCH_CHECK(b_bias.size(1) == size_n, "b_bias.size(1) != size_n"); + TORCH_CHECK(b_bias.stride(1) == 1, "b_bias.stride(1) != 1"); + } else { + b_bias = torch::empty({0}, opts_a); + } + + torch::Tensor b_zeros; + if (b_zeros_or_none.has_value()) { + b_zeros = b_zeros_or_none.value(); + TORCH_CHECK(b_zeros.device().is_cuda(), "b_zeros is not on GPU"); + TORCH_CHECK(b_zeros.is_contiguous(), "b_zeros is not contiguous"); + } else { + b_zeros = torch::empty({0}, opts_a); + } + bool has_zp = b_zeros.size(-1) > 0; + if (has_zp) { + TORCH_CHECK(b_type == vllm::kU4 || b_type == vllm::kU8, + "b_type must be u4 or u8 when has_zp = True. Got = ", + b_type.str()); + } else { + TORCH_CHECK(b_type == vllm::kU4B8 || b_type == vllm::kU8B128 || + b_type == vllm::kS4 || b_type == vllm::kS8 || + b_type == vllm::kFE4M3fn || b_type == vllm::kFE2M1f, + "b_type must be uint4b8, uint8b128, int4, int8, " + "float8_e4m3fn or float4_e2m1f when has_zp = False. Got = ", + b_type.str()); + } + + if (has_zp && is_zp_float) { + TORCH_CHECK(a.scalar_type() == at::ScalarType::Half, + "Computation type must be float16 (half) when using float zero " + "points."); + } + + // Verify b_zeros + if (has_zp) { + int rank2 = b_zeros.dim(); + TORCH_CHECK(rank2 == 3, "b_zeros rank = ", rank2, " is not 3"); + if (is_zp_float) { + TORCH_CHECK(b_zeros.size(2) == size_n, + "b_zeros dim 2 = ", b_zeros.size(2), + " is not size_n = ", size_n); + TORCH_CHECK(num_groups == b_zeros.size(1), + "b_zeros dim 1 = ", b_zeros.size(1), + " is not num_groups = ", num_groups); + TORCH_CHECK(num_groups != -1, "num_groups must be != -1"); + } else { + TORCH_CHECK(b_zeros.size(1) == num_groups, + "b_zeros dim 1 = ", b_zeros.size(1), + " is not num_groups = ", num_groups); + TORCH_CHECK(b_zeros.size(2) == size_n / pack_factor, + "b_zeros dim 2 = ", b_zeros.size(2), + " is not size_n / pack_factor = ", size_n / pack_factor); + } + } + + // Verify workspace size + TORCH_CHECK(size_n % MARLIN_NAMESPACE_NAME::min_thread_n == 0, + "size_n = ", size_n, ", is not divisible by min_thread_n = ", + MARLIN_NAMESPACE_NAME::min_thread_n); + + int max_n_tiles = size_n / MARLIN_NAMESPACE_NAME::min_thread_n; + int min_workspace_size = min( + max_n_tiles * (int)(sorted_token_ids.size(0) / moe_block_size), sms * 4); + TORCH_CHECK(workspace.numel() >= min_workspace_size, + "workspace.numel = ", workspace.numel(), + " is below min_workspace_size = ", min_workspace_size); + + int dev = a.get_device(); + + TORCH_CHECK(a_scales.scalar_type() == at::ScalarType::Float, + "scalar type of a_scales must be float"); + TORCH_CHECK(global_scale.scalar_type() == at::ScalarType::Float, + "scalar type of global_scale must be float"); + if (a_type.size_bits() == 16) { + TORCH_CHECK(a.scalar_type() == c.scalar_type(), + "scalar type of a must be the same with c for 16 bit " + "activation"); + } + + MARLIN_NAMESPACE_NAME::marlin_mm( + a.const_data_ptr(), b_q_weight.const_data_ptr(), c.mutable_data_ptr(), + c_tmp.mutable_data_ptr(), b_bias.mutable_data_ptr(), + a_scales.mutable_data_ptr(), b_scales.mutable_data_ptr(), + global_scale.mutable_data_ptr(), b_zeros.mutable_data_ptr(), + g_idx.mutable_data_ptr(), perm.mutable_data_ptr(), + a_tmp.mutable_data_ptr(), sorted_token_ids.mutable_data_ptr(), + expert_ids.mutable_data_ptr(), num_tokens_past_padded.mutable_data_ptr(), + topk_weights.mutable_data_ptr(), moe_block_size, num_experts, top_k, + mul_topk_weights, size_m, size_n, size_k, workspace.mutable_data_ptr(), + a_type, b_type, c_type, s_type, has_bias, has_act_order, is_k_full, + has_zp, num_groups, group_size, dev, + at::cuda::getCurrentCUDAStream(dev).stream(), thread_k, thread_n, sms, + blocks_per_sm, use_atomic_add, use_fp32_reduce, is_zp_float); + + return c; +} + +// Defined in repack_standalone.cu (ported gptq_marlin_repack, torch::Tensor ABI). +torch::Tensor gptq_marlin_repack(torch::Tensor& b_q_weight, torch::Tensor& perm, + int64_t size_k, int64_t size_n, + int64_t num_bits, bool is_a_8bit); + +PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { + namespace py = pybind11; + m.def("gptq_marlin_repack", &gptq_marlin_repack, + "NVFP4 weight -> Marlin tile layout repack (standalone)", + py::arg("b_q_weight"), py::arg("perm"), py::arg("size_k"), + py::arg("size_n"), py::arg("num_bits"), py::arg("is_a_8bit") = false); + m.def("moe_wna16_marlin_gemm", &moe_wna16_marlin_gemm, + "NVFP4 W4A16 Marlin MoE GEMM (standalone)", py::arg("a"), + py::arg("c_or_none"), py::arg("b_q_weight"), py::arg("b_bias_or_none"), + py::arg("b_scales"), py::arg("a_scales_or_none"), + py::arg("global_scale_or_none"), py::arg("b_zeros_or_none"), + py::arg("g_idx_or_none"), py::arg("perm_or_none"), py::arg("workspace"), + py::arg("sorted_token_ids"), py::arg("expert_ids"), + py::arg("num_tokens_past_padded"), py::arg("topk_weights"), + py::arg("moe_block_size"), py::arg("top_k"), py::arg("mul_topk_weights"), + py::arg("b_type_id"), py::arg("size_m"), py::arg("size_n"), + py::arg("size_k"), py::arg("is_k_full"), py::arg("use_atomic_add"), + py::arg("use_fp32_reduce"), py::arg("is_zp_float"), + py::arg("thread_k") = -1, py::arg("thread_n") = -1, + py::arg("blocks_per_sm") = -1); +} diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/repack_standalone.cu b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/repack_standalone.cu new file mode 100644 index 0000000000..6651b60852 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/repack_standalone.cu @@ -0,0 +1,371 @@ +/* Standalone port of vLLM's gptq_marlin_repack (NVFP4 weight -> Marlin tile layout). + * Source: vllm csrc/libtorch_stable/quantization/marlin/gptq_marlin_repack.cu. + * Kernel body kept VERBATIM; host wrapper ported torch::stable::Tensor -> torch::Tensor; + * STABLE_TORCH_LIBRARY_IMPL registration dropped (a pybind def is added in ops_standalone.cu). + * Compiled in the default `marlin` namespace: undef the build's MARLIN_NAMESPACE_NAME for this + * TU so marlin.cuh's constants land in `marlin` (the GEMM TU uses marlin_moe_wna16; constants are + * static/internal-linkage so the two TUs don't clash). */ +#ifdef MARLIN_NAMESPACE_NAME + #undef MARLIN_NAMESPACE_NAME +#endif + +#include "marlin.cuh" + +#include +#include +#include +#include + +namespace marlin { + +template +__global__ void gptq_marlin_repack_kernel( + uint32_t const* __restrict__ b_q_weight_ptr, + uint32_t const* __restrict__ perm_ptr, uint32_t* __restrict__ out_ptr, + int size_k, int size_n) { + constexpr int pack_factor = 32 / num_bits; + + constexpr int target_tile_n_size = tile_n_size / (is_a_8bit ? 2 : 1); + constexpr int target_tile_k_size = tile_k_size * (is_a_8bit ? 2 : 1); + int k_tiles = size_k / target_tile_k_size; + int n_tiles = size_n / target_tile_n_size; + int block_k_tiles = div_ceil(k_tiles, gridDim.x); + + auto start_k_tile = blockIdx.x * block_k_tiles; + if (start_k_tile >= k_tiles) { + return; + } + + int finish_k_tile = min(start_k_tile + block_k_tiles, k_tiles); + + // Wait until the next thread tile has been loaded to shared memory. + auto wait_for_stage = [&]() { + // We only have `stages - 2` active fetches since we are double buffering + // and can only issue the next fetch when it is guaranteed that the previous + // shared memory load is fully complete (as it may otherwise be + // overwritten). + cp_async_wait(); + __syncthreads(); + }; + + extern __shared__ int4 sh[]; + + constexpr int perm_size = target_tile_k_size / 4; + + int4* sh_perm_ptr = sh; + int4* sh_pipe_ptr = sh_perm_ptr; + if constexpr (has_perm) { + sh_pipe_ptr += perm_size; + } + + constexpr int tile_ints = target_tile_k_size / pack_factor; + + constexpr int stage_n_threads = target_tile_n_size / 4; + constexpr int stage_k_threads = has_perm ? target_tile_k_size : tile_ints; + constexpr int stage_size = stage_k_threads * stage_n_threads; + + auto load_perm_to_shared = [&](int k_tile_id) { + int first_k_int4 = (k_tile_id * target_tile_k_size) / 4; + + int4 const* perm_int4_ptr = reinterpret_cast(perm_ptr); + + if (threadIdx.x < perm_size) { + sh_perm_ptr[threadIdx.x] = perm_int4_ptr[first_k_int4 + threadIdx.x]; + } + __syncthreads(); + }; + + auto fetch_to_shared = [&](int pipe, int k_tile_id, int n_tile_id) { + if (n_tile_id >= n_tiles) { + cp_async_fence(); + return; + } + + int first_n = n_tile_id * target_tile_n_size; + + int4* sh_ptr = sh_pipe_ptr + stage_size * pipe; + + if constexpr (has_perm) { + if (threadIdx.x < stage_size) { + auto k_id = threadIdx.x / stage_n_threads; + auto n_id = threadIdx.x % stage_n_threads; + + uint32_t const* sh_perm_int_ptr = + reinterpret_cast(sh_perm_ptr); + + int src_k = sh_perm_int_ptr[k_id]; + int src_k_packed = src_k / pack_factor; + + cp_async4( + &sh_ptr[k_id * stage_n_threads + n_id], + reinterpret_cast(&( + b_q_weight_ptr[src_k_packed * size_n + first_n + (n_id * 4)]))); + } + + } else { + if (threadIdx.x < stage_size) { + auto k_id = threadIdx.x / stage_n_threads; + auto n_id = threadIdx.x % stage_n_threads; + + int first_k = k_tile_id * target_tile_k_size; + int first_k_packed = first_k / pack_factor; + + cp_async4(&sh_ptr[k_id * stage_n_threads + n_id], + reinterpret_cast( + &(b_q_weight_ptr[(first_k_packed + k_id) * size_n + + first_n + (n_id * 4)]))); + } + } + + cp_async_fence(); + }; + + auto repack_tile = [&](int pipe, int k_tile_id, int n_tile_id) { + if (n_tile_id >= n_tiles) { + return; + } + + auto warp_id = threadIdx.x / 32; + auto th_id = threadIdx.x % 32; + + if (warp_id >= 4) { + return; + } + + int tc_col = th_id / 4; + int tc_row = (th_id % 4) * (is_a_8bit ? 4 : 2); + + constexpr int tc_offsets[4] = {0, 1, 8, 9}; + + int cur_n = (warp_id / (is_a_8bit ? 2 : 1)) * 16 + tc_col; + + constexpr int sh_stride = target_tile_n_size; + constexpr uint32_t mask = (1 << num_bits) - 1; + + int4* sh_stage_ptr = sh_pipe_ptr + stage_size * pipe; + uint32_t* sh_stage_int_ptr = reinterpret_cast(sh_stage_ptr); + + uint32_t* sh_perm_int_ptr = reinterpret_cast(sh_perm_ptr); + + uint32_t vals[8]; + + if constexpr (has_perm) { + static_assert(!is_a_8bit); + for (int i = 0; i < 4; i++) { + int k_idx = tc_row + tc_offsets[i]; + + uint32_t src_k = sh_perm_int_ptr[k_idx]; + uint32_t src_k_pos = src_k % pack_factor; + + uint32_t b1_val = sh_stage_int_ptr[k_idx * sh_stride + cur_n]; + uint32_t b1_cur_val = (b1_val >> (src_k_pos * num_bits)) & mask; + + uint32_t b2_val = sh_stage_int_ptr[k_idx * sh_stride + cur_n + 8]; + uint32_t b2_cur_val = (b2_val >> (src_k_pos * num_bits)) & mask; + + vals[i] = b1_cur_val; + vals[4 + i] = b2_cur_val; + } + + } else { + uint32_t b1_vals[tile_ints]; + uint32_t b2_vals[tile_ints]; + +#pragma unroll + for (int i = 0; i < tile_ints; i++) { + if constexpr (is_a_8bit) { + b1_vals[i] = + sh_stage_int_ptr[cur_n + sh_stride * i + (warp_id % 2) * 8]; + } else { + b1_vals[i] = sh_stage_int_ptr[cur_n + sh_stride * i]; + b2_vals[i] = sh_stage_int_ptr[cur_n + 8 + sh_stride * i]; + } + } + +#pragma unroll + for (int i = 0; i < 4; i++) { + int cur_elem = tc_row + (is_a_8bit ? i : tc_offsets[i]); + int cur_int = cur_elem / pack_factor; + int cur_pos = cur_elem % pack_factor; + + vals[i] = (b1_vals[cur_int] >> (cur_pos * num_bits)) & mask; + if constexpr (is_a_8bit) + vals[4 + i] = + (b1_vals[cur_int + tile_ints / 2] >> (cur_pos * num_bits)) & mask; + else + vals[4 + i] = (b2_vals[cur_int] >> (cur_pos * num_bits)) & mask; + } + } + + constexpr int tile_size = + target_tile_k_size * target_tile_n_size / pack_factor; + int out_offset = (k_tile_id * n_tiles + n_tile_id) * tile_size; + + // Result of: + // https://github.com/NVIDIA/FasterTransformer/blob/main/src/fastertransformer/cutlass_extensions/include/cutlass_extensions/interleaved_numeric_conversion.h + if constexpr (!is_a_8bit && num_bits == 4) { + int pack_idx[8] = {0, 2, 4, 6, 1, 3, 5, 7}; + + uint32_t res = 0; +#pragma unroll + for (int i = 0; i < 8; i++) { + res |= vals[pack_idx[i]] << (i * 4); + } + + out_ptr[out_offset + th_id * 4 + warp_id] = res; + + } else if constexpr (is_a_8bit && num_bits == 4) { + int pack_idx[8] = {0, 4, 1, 5, 2, 6, 3, 7}; + + uint32_t res = 0; +#pragma unroll + for (int i = 0; i < 8; i++) { + res |= vals[pack_idx[i]] << (i * 4); + } + + out_ptr[out_offset + th_id * 4 + warp_id] = res; + + } else { + constexpr int pack_idx[4] = {0, 2, 1, 3}; + + uint32_t res1 = 0; + uint32_t res2 = 0; +#pragma unroll + for (int i = 0; i < 4; i++) { + const int ii = is_a_8bit ? i : pack_idx[i]; + res1 |= vals[ii] << (i * 8); + res2 |= vals[4 + ii] << (i * 8); + } + + out_ptr[out_offset + th_id * 8 + (warp_id * 2) + 0] = res1; + out_ptr[out_offset + th_id * 8 + (warp_id * 2) + 1] = res2; + } + }; + + auto start_pipes = [&](int k_tile_id, int n_tile_id) { +#pragma unroll + for (int pipe = 0; pipe < repack_stages - 1; pipe++) { + fetch_to_shared(pipe, k_tile_id, n_tile_id + pipe); + } + + wait_for_stage(); + }; +#pragma unroll + for (int k_tile_id = start_k_tile; k_tile_id < finish_k_tile; k_tile_id++) { + int n_tile_id = 0; + + if constexpr (has_perm) { + load_perm_to_shared(k_tile_id); + } + + start_pipes(k_tile_id, n_tile_id); + + while (n_tile_id < n_tiles) { +#pragma unroll + for (int pipe = 0; pipe < repack_stages; pipe++) { + fetch_to_shared((pipe + repack_stages - 1) % repack_stages, k_tile_id, + n_tile_id + pipe + repack_stages - 1); + repack_tile(pipe, k_tile_id, n_tile_id + pipe); + wait_for_stage(); + } + n_tile_id += repack_stages; + } + } +} + +} // namespace marlin + +#define CALL_IF(NUM_BITS, HAS_PERM, IS_A_8BIT) \ + else if (num_bits == NUM_BITS && has_perm == HAS_PERM && \ + is_a_8bit == IS_A_8BIT) { \ + cudaFuncSetAttribute( \ + marlin::gptq_marlin_repack_kernel, \ + cudaFuncAttributeMaxDynamicSharedMemorySize, max_shared_mem); \ + marlin::gptq_marlin_repack_kernel \ + <<>>( \ + b_q_weight_ptr, perm_ptr, out_ptr, size_k, size_n); \ + } + +// Ported wrapper: regular torch::Tensor signature (was torch::stable::Tensor). +torch::Tensor gptq_marlin_repack(torch::Tensor& b_q_weight, torch::Tensor& perm, + int64_t size_k, int64_t size_n, + int64_t num_bits, bool is_a_8bit) { + // Verify compatibility with marlin tile of 16x64 + TORCH_CHECK(size_k % marlin::tile_k_size == 0, "size_k = ", size_k, + " is not divisible by tile_k_size = ", marlin::tile_k_size); + TORCH_CHECK(size_n % marlin::tile_n_size == 0, "size_n = ", size_n, + " is not divisible by tile_n_size = ", marlin::tile_n_size); + + TORCH_CHECK(num_bits == 4 || num_bits == 8, + "num_bits must be 4 or 8. Got = ", num_bits); + int const pack_factor = 32 / num_bits; + + // Verify B + TORCH_CHECK((size_k / pack_factor) == b_q_weight.size(0), + "Shape mismatch: b_q_weight.size(0) = ", b_q_weight.size(0), + ", size_k = ", size_k, ", pack_factor = ", pack_factor); + TORCH_CHECK(b_q_weight.size(1) == size_n, + "b_q_weight.size(1) = ", b_q_weight.size(1), + " is not size_n = ", size_n); + + // Verify device and strides + TORCH_CHECK(b_q_weight.is_cuda(), "b_q_weight is not on GPU"); + TORCH_CHECK(b_q_weight.is_contiguous(), "b_q_weight is not contiguous"); + TORCH_CHECK(b_q_weight.scalar_type() == at::kInt, + "b_q_weight type is not kInt"); + + TORCH_CHECK(perm.is_cuda(), "perm is not on GPU"); + TORCH_CHECK(perm.is_contiguous(), "perm is not contiguous"); + TORCH_CHECK(perm.scalar_type() == at::kInt, "perm type is not at::kInt"); + + int const device_index = b_q_weight.get_device(); + const c10::cuda::CUDAGuard device_guard(device_index); + const cudaStream_t stream = + at::cuda::getCurrentCUDAStream(device_index).stream(); + + // Alloc buffers + auto options = torch::TensorOptions() + .dtype(b_q_weight.scalar_type()) + .device(b_q_weight.device()); + torch::Tensor out = torch::empty( + {size_k / marlin::tile_size, size_n * marlin::tile_size / pack_factor}, + options); + + // Detect if there is act_order + bool has_perm = perm.size(0) != 0; + + // Get ptrs + uint32_t const* b_q_weight_ptr = + reinterpret_cast(b_q_weight.const_data_ptr()); + uint32_t const* perm_ptr = + reinterpret_cast(perm.const_data_ptr()); + uint32_t* out_ptr = reinterpret_cast(out.mutable_data_ptr()); + + int blocks; + cudaDeviceGetAttribute(&blocks, cudaDevAttrMultiProcessorCount, device_index); + + int max_shared_mem = 0; + cudaDeviceGetAttribute(&max_shared_mem, + cudaDevAttrMaxSharedMemoryPerBlockOptin, device_index); + TORCH_CHECK(max_shared_mem > 0); + + if (false) { + } + CALL_IF(4, false, false) + CALL_IF(4, true, false) + CALL_IF(8, false, false) + CALL_IF(8, true, false) + + CALL_IF(4, false, true) + CALL_IF(8, false, true) + + else { + TORCH_CHECK(false, "Unsupported repack config: num_bits = ", num_bits, + ", has_perm = ", has_perm, ", is_a_8bit = ", is_a_8bit); + } + + return out; +} diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/sm80_kernel_bfloat16_fe2m1f_bfloat16.cu b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/sm80_kernel_bfloat16_fe2m1f_bfloat16.cu new file mode 100644 index 0000000000..63e98e79c6 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/moe/marlin_moe_wna16/sm80_kernel_bfloat16_fe2m1f_bfloat16.cu @@ -0,0 +1,40 @@ +// auto generated by generate_kernels.py +// clang-format off + +#include "kernel.h" +#include "marlin_template.h" + +namespace MARLIN_NAMESPACE_NAME { + + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +template __global__ void Marlin( MARLIN_KERNEL_PARAMS ); + +} diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/dequant.h b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/dequant.h new file mode 100644 index 0000000000..edd97dbfcd --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/dequant.h @@ -0,0 +1,609 @@ +/* +Fast Dequantization (Converting INT4/INT8/FP4/FP8 to FP16/BF16) + +The process of fast dequantization can be summarized as a combination +of bitwise operations and floating-point computations: + +weight =>(bit_op / bitwise operations)=> +f16_value =>(flop / floating-point computation)=> +dequantized_weight + +Since the dequantized weights typically require subtracting the zero point and +applying a scale factor, the floating-point computation step can be fused with +the zero-point subtraction and scaling operations. + +The following are the parts that need to be modified for the fused operation +of zero-point subtraction and scaling. + +## INT4 => FP16/BF16 or INT8 => FP16 + +The floating-point computation is `__hsub2` + +If has zero points: + + flop(bit_op(weight)) - flop(bit_op(zp)) + = sub(bit_op(weight), bias) - sub(bit_op(zp), bias) + = bit_op(weight) - bit_op(zp) + +so we don't need additional modification. + +If has float zero points: + + flop(bit_op(weight)) - fzp + = sub(bit_op(weight), bias) - fzp + = bit_op(weight) - (fzp + bias) + +where the `fzp + bias` can be computed at weight loading. But this +may have accuracy issue, so we should not use this in most cases. + +If has not zero points: + + scale(flop(bit_op(weight))) + = scale(sub(bit_op(weight), bias)) + = scale(bit_op(weight)) - scale(bias) + = fma(bit_op(weight), scale_factor, scale(bias)) + +where the `scale(bias)` can be cached. But this may have accuracy issue, +so we should not use this in most cases. + + +## INT8 => BF16 + +INT8 => BF16 is a special case, it use byte_perm instead of flop. +We cannot fused byte_perm with scaling. + + +## FP4/FP8 => FP16/BF16 + + scale(flop(bit_op(weight))) + = scale(mul(bit_op(weight), multiplier)) + = mul(bit_op(weight), scale_factor * multiplier) + +where `scale_factor * multiplier` can be computed at weight loading. + +*/ + +#include "marlin_dtypes.cuh" + +namespace MARLIN_NAMESPACE_NAME { + +#if !defined(__CUDA_ARCH__) || __CUDA_ARCH__ >= 750 +// Lookup-table based 3-input logical operation; explicitly used for +// dequantization as the compiler does not seem to automatically recognize it in +// all cases. +template +__device__ inline int lop3(int a, int b, int c) { + int res; + asm volatile("lop3.b32 %0, %1, %2, %3, %4;\n" + : "=r"(res) + : "r"(a), "r"(b), "r"(c), "n"(lut)); + return res; +} + +// Constructs destination register by taking bytes from 2 sources (based on +// mask) +template +__device__ inline uint32_t prmt(uint32_t a) { + uint32_t res; + asm volatile("prmt.b32 %0, %1, %2, %3;\n" + : "=r"(res) + : "r"(a), "n"(start_byte), "n"(mask)); + return res; +} + +template +__device__ inline void dequant(int q, scalar_t2* frag_b); + +// +// Efficiently dequantize 4bit values packed in an int32 value into a full +// B-fragment of 4 fp16 values. We mostly follow the strategy in the link below, +// with some small changes: +// - FP16: +// https://github.com/NVIDIA/FasterTransformer/blob/release/v5.3_tag/src/fastertransformer/cutlass_extensions/include/cutlass_extensions/interleaved_numeric_conversion.h#L215-L287 +// - BF16: +// https://github.com/NVIDIA/FasterTransformer/blob/release/v5.3_tag/src/fastertransformer/cutlass_extensions/include/cutlass_extensions/interleaved_numeric_conversion.h#L327-L385 +// +template <> +__device__ inline void dequant(int q, + half2* frag_b) { + const int MASK = 0x000f000f; + const int EX = 0x64006400; + // Guarantee that the `(a & b) | c` operations are LOP3s. + int lo = lop3<(0xf0 & 0xcc) | 0xaa>(q, MASK, EX); + q >>= 4; + int hi = lop3<(0xf0 & 0xcc) | 0xaa>(q, MASK, EX); + + frag_b[0] = *reinterpret_cast(&lo); + frag_b[1] = *reinterpret_cast(&hi); +} + +template <> +__device__ inline void dequant(int q, + half2* frag_b) { + const int LO = 0x000f000f; + const int HI = 0x00f000f0; + const int EX = 0x64006400; + // Guarantee that the `(a & b) | c` operations are LOP3s. + // clang-format off + int lo = lop3<(0xf0 & 0xcc) | 0xaa>(q, LO, EX); + int hi = lop3<(0xf0 & 0xcc) | 0xaa>(q, HI, EX); + // clang-format on + // We want signed int4 outputs, hence we fuse the `-8` symmetric zero point + // directly into `SUB` and `ADD`. + const int SUB = 0x64086408; + const int MUL = 0x2c002c00; + const int ADD = 0xd480d480; + frag_b[0] = __hsub2(*reinterpret_cast(&lo), + *reinterpret_cast(&SUB)); + frag_b[1] = __hfma2(*reinterpret_cast(&hi), + *reinterpret_cast(&MUL), + *reinterpret_cast(&ADD)); +} + +template <> +__device__ inline void dequant(int q, + half2* frag_b) { + dequant(q, frag_b); +} + +template <> +__device__ inline void dequant(int q, + half2* frag_b) { + const int LO = 0x000f000f; + const int HI = 0x00f000f0; + const int EX = 0x64006400; + // Guarantee that the `(a & b) | c` operations are LOP3s. + // clang-format off + int lo = lop3<(0xf0 & 0xcc) | 0xaa>(q, LO, EX); + int hi = lop3<(0xf0 & 0xcc) | 0xaa>(q, HI, EX); + // clang-format on + // We want signed int4 outputs, hence we fuse the `-8` symmetric zero point + // directly into `SUB` and `ADD`. + const int SUB = 0x64006400; + const int MUL = 0x2c002c00; + const int ADD = 0xd400d400; + frag_b[0] = __hsub2(*reinterpret_cast(&lo), + *reinterpret_cast(&SUB)); + frag_b[1] = __hfma2(*reinterpret_cast(&hi), + *reinterpret_cast(&MUL), + *reinterpret_cast(&ADD)); +} + +template <> +__device__ inline void dequant( + int q, nv_bfloat162* frag_b) { + static constexpr uint32_t MASK = 0x000f000f; + static constexpr uint32_t EX = 0x43004300; + + // Guarantee that the `(a & b) | c` operations are LOP3s. + // clang-format off + int lo = lop3<(0xf0 & 0xcc) | 0xaa>(q, MASK, EX); + q >>= 4; + int hi = lop3<(0xf0 & 0xcc) | 0xaa>(q, MASK, EX); + // clang-format on + + frag_b[0] = *reinterpret_cast(&lo); + frag_b[1] = *reinterpret_cast(&hi); +} + +template <> +__device__ inline void dequant( + int q, nv_bfloat162* frag_b) { + dequant(q, frag_b); + + static constexpr uint32_t SUB = 0x43084308; + + frag_b[0] = __hsub2(frag_b[0], *reinterpret_cast(&SUB)); + frag_b[1] = __hsub2(frag_b[1], *reinterpret_cast(&SUB)); +} + +template <> +__device__ inline void dequant( + int q, nv_bfloat162* frag_b) { + dequant(q, frag_b); +} + +template <> +__device__ inline void dequant( + int q, nv_bfloat162* frag_b) { + dequant(q, frag_b); + + static constexpr uint32_t SUB = 0x43004300; + + frag_b[0] = __hsub2(frag_b[0], *reinterpret_cast(&SUB)); + frag_b[1] = __hsub2(frag_b[1], *reinterpret_cast(&SUB)); +} + +// +// Fast Int8ToFp16/Int8ToBf16: Efficiently dequantize 8bit int values to fp16 or +// bf16 Reference: +// - FP16: +// https://github.com/NVIDIA/FasterTransformer/blob/release/v5.3_tag/src/fastertransformer/cutlass_extensions/include/cutlass_extensions/interleaved_numeric_conversion.h#L53-L85 +// - BF16: +// https://github.com/NVIDIA/FasterTransformer/blob/release/v5.3_tag/src/fastertransformer/cutlass_extensions/include/cutlass_extensions/interleaved_numeric_conversion.h#L125-L175 +// +template <> +__device__ inline void dequant(int q, + half2* frag_b) { + static constexpr uint32_t mask_for_elt_01 = 0x5250; + static constexpr uint32_t mask_for_elt_23 = 0x5351; + static constexpr uint32_t start_byte_for_fp16 = 0x64646464; + + uint32_t lo = prmt(q); + uint32_t hi = prmt(q); + + frag_b[0] = *reinterpret_cast(&lo); + frag_b[1] = *reinterpret_cast(&hi); +} + +template <> +__device__ inline void dequant( + int q, half2* frag_b) { + dequant(q, frag_b); + + static constexpr uint32_t I8s_TO_F16s_MAGIC_NUM = 0x64806480; + frag_b[0] = __hsub2(frag_b[0], + *reinterpret_cast(&I8s_TO_F16s_MAGIC_NUM)); + frag_b[1] = __hsub2(frag_b[1], + *reinterpret_cast(&I8s_TO_F16s_MAGIC_NUM)); +} + +template <> +__device__ inline void dequant(int q, + half2* frag_b) { + dequant(q, frag_b); +} + +template <> +__device__ inline void dequant(int q, + half2* frag_b) { + dequant(q, frag_b); + + static constexpr uint32_t I8s_TO_F16s_MAGIC_NUM = 0x64006400; + frag_b[0] = __hsub2(frag_b[0], + *reinterpret_cast(&I8s_TO_F16s_MAGIC_NUM)); + frag_b[1] = __hsub2(frag_b[1], + *reinterpret_cast(&I8s_TO_F16s_MAGIC_NUM)); +} + +template <> +__device__ inline void dequant( + int q, nv_bfloat162* frag_b) { + float fp32_intermediates[4]; + uint32_t* fp32_intermediates_casted = + reinterpret_cast(fp32_intermediates); + + static constexpr uint32_t fp32_base = 0x4B000000; + fp32_intermediates_casted[0] = __byte_perm(q, fp32_base, 0x7650); + fp32_intermediates_casted[1] = __byte_perm(q, fp32_base, 0x7652); + fp32_intermediates_casted[2] = __byte_perm(q, fp32_base, 0x7651); + fp32_intermediates_casted[3] = __byte_perm(q, fp32_base, 0x7653); + + fp32_intermediates[0] -= 8388736.f; + fp32_intermediates[1] -= 8388736.f; + fp32_intermediates[2] -= 8388736.f; + fp32_intermediates[3] -= 8388736.f; + + uint32_t* bf16_result_ptr = reinterpret_cast(frag_b); + bf16_result_ptr[0] = __byte_perm(fp32_intermediates_casted[0], + fp32_intermediates_casted[1], 0x7632); + bf16_result_ptr[1] = __byte_perm(fp32_intermediates_casted[2], + fp32_intermediates_casted[3], 0x7632); +} + +template <> +__device__ inline void dequant( + int q, nv_bfloat162* frag_b) { + float fp32_intermediates[4]; + uint32_t* fp32_intermediates_casted = + reinterpret_cast(fp32_intermediates); + + static constexpr uint32_t fp32_base = 0x4B000000; + fp32_intermediates_casted[0] = __byte_perm(q, fp32_base, 0x7650); + fp32_intermediates_casted[1] = __byte_perm(q, fp32_base, 0x7652); + fp32_intermediates_casted[2] = __byte_perm(q, fp32_base, 0x7651); + fp32_intermediates_casted[3] = __byte_perm(q, fp32_base, 0x7653); + + fp32_intermediates[0] -= 8388608.f; + fp32_intermediates[1] -= 8388608.f; + fp32_intermediates[2] -= 8388608.f; + fp32_intermediates[3] -= 8388608.f; + + uint32_t* bf16_result_ptr = reinterpret_cast(frag_b); + bf16_result_ptr[0] = __byte_perm(fp32_intermediates_casted[0], + fp32_intermediates_casted[1], 0x7632); + bf16_result_ptr[1] = __byte_perm(fp32_intermediates_casted[2], + fp32_intermediates_casted[3], 0x7632); +} + +template <> +__device__ inline void dequant( + int q, half2* frag_b) { + // Constants for FP8 (E4M3) and FP16 formats + constexpr int FP8_EXPONENT = 4, FP16_EXPONENT = 5; + constexpr int RIGHT_SHIFT = FP16_EXPONENT - FP8_EXPONENT; + constexpr int MASK = 0x7F007F00; + + // Extract and shift FP8 values to FP16 format + int Out1 = (q & 0x80008000) | ((q & MASK) >> RIGHT_SHIFT); + q <<= 8; + int Out2 = (q & 0x80008000) | ((q & MASK) >> RIGHT_SHIFT); + + // Note: reverse indexing is intentional because weights are permuted + frag_b[1] = *reinterpret_cast(&Out1); + frag_b[0] = *reinterpret_cast(&Out2); +} + +template <> +__device__ inline void dequant( + int q, half2* frag_b) { + dequant(q, frag_b); + + // Constants for FP8 (E4M3) and FP16 formats + constexpr int FP8_EXPONENT = 4, FP16_EXPONENT = 5; + + // Construct and apply exponent bias + constexpr int BIAS_OFFSET = + (1 << (FP16_EXPONENT - 1)) - (1 << (FP8_EXPONENT - 1)); + const half2 bias_reg = __float2half2_rn(float(1 << BIAS_OFFSET)); + + // Convert to half2 and apply bias + frag_b[1] = __hmul2(frag_b[1], bias_reg); + frag_b[0] = __hmul2(frag_b[0], bias_reg); +} + +template <> +__device__ inline void dequant( + int q, nv_bfloat162* frag_b) { + // Constants for FP8 (E4M3) and BF16 formats + constexpr int FP8_EXPONENT = 4, BF16_EXPONENT = 8; + constexpr int RIGHT_SHIFT = BF16_EXPONENT - FP8_EXPONENT; + + constexpr int MASK = 0x7F007F00; + + // Extract and shift FP8 values to BF16 format + int Out1 = (q & 0x80008000) | ((q & MASK) >> RIGHT_SHIFT); + q <<= 8; + int Out2 = (q & 0x80008000) | ((q & MASK) >> RIGHT_SHIFT); + + // Note: reverse indexing is intentional because weights are permuted + frag_b[1] = *reinterpret_cast(&Out1); + frag_b[0] = *reinterpret_cast(&Out2); +} + +template <> +__device__ inline void dequant( + int q, nv_bfloat162* frag_b) { + dequant(q, frag_b); + + // Constants for FP8 (E4M3) and BF16 formats + constexpr int FP8_EXPONENT = 4, BF16_EXPONENT = 8; + + // Construct and apply exponent bias + constexpr int BIAS_OFFSET = + (1 << (BF16_EXPONENT - 1)) - (1 << (FP8_EXPONENT - 1)); + // Add 127 (float exponent bias) to BIAS_OFFSET and shift to float exponent + // position + constexpr uint32_t BIAS = (BIAS_OFFSET + 127) << 23; + const nv_bfloat162 bias_reg = + __float2bfloat162_rn(*reinterpret_cast(&BIAS)); + + // Convert to bfloat162 and apply bias + frag_b[1] = __hmul2(frag_b[1], bias_reg); + frag_b[0] = __hmul2(frag_b[0], bias_reg); +} + +template <> +__device__ inline void dequant(int q, + half2* frag_b) { + // Constants for FP4 (E2M1) and FP16 formats + constexpr int FP4_EXPONENT = 2, FP16_EXPONENT = 5; + constexpr int RIGHT_SHIFT = FP16_EXPONENT - FP4_EXPONENT; + constexpr int MASK = 0x70007000; + + // Extract and shift FP4 values to FP16 format + int Out1 = (q & 0x80008000) | ((q & MASK) >> RIGHT_SHIFT); + q <<= 4; + int Out2 = (q & 0x80008000) | ((q & MASK) >> RIGHT_SHIFT); + + // Note: reverse indexing is intentional because weights are permuted + frag_b[1] = *reinterpret_cast(&Out1); + frag_b[0] = *reinterpret_cast(&Out2); +} + +template <> +__device__ inline void dequant( + int q, half2* frag_b) { + dequant(q, frag_b); + + // Constants for FP4 (E2M1) and FP16 formats + constexpr int FP4_EXPONENT = 2, FP16_EXPONENT = 5; + + // Construct and apply exponent bias + constexpr int BIAS_OFFSET = + (1 << (FP16_EXPONENT - 1)) - (1 << (FP4_EXPONENT - 1)); + const half2 bias_reg = __float2half2_rn(float(1 << BIAS_OFFSET)); + + // Convert to half2 and apply bias + frag_b[1] = __hmul2(frag_b[1], bias_reg); + frag_b[0] = __hmul2(frag_b[0], bias_reg); +} + +template <> +__device__ inline void dequant( + int q, nv_bfloat162* frag_b) { + // Constants for FP4 (E2M1) and FP16 formats + constexpr int FP4_EXPONENT = 2, BF16_EXPONENT = 8; + constexpr int RIGHT_SHIFT = BF16_EXPONENT - FP4_EXPONENT; + constexpr int MASK = 0x70007000; + + // Extract and shift FP4 values to FP16 format + int Out1 = (q & 0x80008000) | ((q & MASK) >> RIGHT_SHIFT); + q <<= 4; + int Out2 = (q & 0x80008000) | ((q & MASK) >> RIGHT_SHIFT); + + // Note: reverse indexing is intentional because weights are permuted + frag_b[1] = *reinterpret_cast(&Out1); + frag_b[0] = *reinterpret_cast(&Out2); +} + +template <> +__device__ inline void dequant( + int q, nv_bfloat162* frag_b) { + dequant(q, frag_b); + + // Constants for FP4 (E2M1) and BF16 formats + constexpr int FP4_EXPONENT = 2, BF16_EXPONENT = 8; + + // Construct and apply exponent bias + constexpr int BIAS_OFFSET = + (1 << (BF16_EXPONENT - 1)) - (1 << (FP4_EXPONENT - 1)); + // Add 127 (float exponent bias) to BIAS_OFFSET and shift to float exponent + // position + constexpr uint32_t BIAS = (BIAS_OFFSET + 127) << 23; + const nv_bfloat162 bias_reg = + __float2bfloat162_rn(*reinterpret_cast(&BIAS)); + + // Convert to half2 and apply bias + frag_b[1] = __hmul2(frag_b[1], bias_reg); + frag_b[0] = __hmul2(frag_b[0], bias_reg); +} + +template <> +__device__ inline void dequant<__nv_fp8x4_e4m3, vllm::kFE2M1f.id(), true>( + int q, __nv_fp8x4_e4m3* frag_b) { + // Constants for FP4 (E2M1) and FP16 formats + constexpr int FP4_EXPONENT = 2, FP8_EXPONENT = 4; + constexpr int RIGHT_SHIFT = FP8_EXPONENT - FP4_EXPONENT; + constexpr int MASK = 0x70707070; + + // Extract and shift FP4 values to FP16 format + int Out1 = (q & 0x80808080) | ((q & MASK) >> RIGHT_SHIFT); + q <<= 4; + int Out2 = (q & 0x80808080) | ((q & MASK) >> RIGHT_SHIFT); + + // Note1: reverse indexing is intentional because weights are permuted + // Note2: when dequant to 8bit type, we write to `frag_b[2]` instead of + // `frag_b[1]` to fit the layout of tensorcore + frag_b[1] = *reinterpret_cast(&Out1); + frag_b[0] = *reinterpret_cast(&Out2); +} + +template <> +__device__ inline void dequant( + int q, int32_t* frag_b) { + constexpr int repeated_zp = 0x08080808; + constexpr int MASK = 0x80808080; + + frag_b[0] = ((q & 0x0F0F0F0F | MASK) - repeated_zp) ^ MASK; + q >>= 4; + frag_b[1] = ((q & 0x0F0F0F0F | MASK) - repeated_zp) ^ MASK; +} + +template <> +__device__ inline void dequant<__nv_fp8x4_e4m3, vllm::kU4B8.id(), true>( + int q, __nv_fp8x4_e4m3* frag_b) { + int s = q & 0x08080808; + int Out1 = ((q & 0x07070707) | (s << 4)) + (s >> 3); + q >>= 4; + s = q & 0x08080808; + int Out2 = ((q & 0x07070707) | (s << 4)) + (s >> 3); + + frag_b[0] = *reinterpret_cast(&Out1); + frag_b[1] = *reinterpret_cast(&Out2); +} + +template +__device__ inline void dequant_fp8_scales(int q, scalar_t2* frag_b); + +template <> +__device__ inline void dequant_fp8_scales( + int q, half2* frag_b) { + int Out1 = (q & 0xFF00FF00) >> 1; + ; + q <<= 8; + int Out2 = (q & 0xFF00FF00) >> 1; + + // Note: reverse indexing is intentional because weights are permuted + frag_b[1] = *reinterpret_cast(&Out1); + frag_b[0] = *reinterpret_cast(&Out2); +}; + +template <> +__device__ inline void dequant_fp8_scales( + int q, nv_bfloat162* frag_b) { + constexpr int FP8_EXPONENT = 4, BF16_EXPONENT = 8; + constexpr int RIGHT_SHIFT = BF16_EXPONENT - FP8_EXPONENT; + constexpr int MASK = 0x7F007F00; + + // Extract and shift FP8 values to BF16 format + int Out1 = ((q & 0x80008000) >> 1) | ((q & MASK) >> RIGHT_SHIFT); + q <<= 8; + int Out2 = ((q & 0x80008000) >> 1) | ((q & MASK) >> RIGHT_SHIFT); + + // Note: reverse indexing is intentional because weights are permuted + frag_b[1] = *reinterpret_cast(&Out1); + frag_b[0] = *reinterpret_cast(&Out2); +} + +template <> +__device__ inline void dequant_fp8_scales( + int q, nv_bfloat162* frag_b) { + // In this conversion, 2 ** -127 in FP8E8M0 would become 0 in BF16, + // but we assume that such a extreme value would not occur in real models. + int Out1 = (q & 0xFF00FF00) >> 1; + q <<= 7; + int Out2 = q & 0x7F807F80; + + // Note: reverse indexing is intentional because weights are permuted + frag_b[1] = *reinterpret_cast(&Out1); + frag_b[0] = *reinterpret_cast(&Out2); +}; + +// subtract zero point in quanted format and then dequant +template +__device__ inline void sub_zp_and_dequant(int q, scalar_t2* frag_b, int zp); + +template <> +__device__ inline void sub_zp_and_dequant( + int q, int32_t* frag_b, int zp) { + // INT4 with zp -> INT8 + // see https://github.com/vllm-project/vllm/pull/24722 + int repeated_zp = 0x01010101 * zp; + int MASK = 0x80808080; + + frag_b[0] = ((q & 0x0F0F0F0F | MASK) - repeated_zp) ^ MASK; + q >>= 4; + frag_b[1] = ((q & 0x0F0F0F0F | MASK) - repeated_zp) ^ MASK; +} + +template <> +__device__ inline void sub_zp_and_dequant<__nv_fp8x4_e4m3, vllm::kU4.id(), + true>(int q, __nv_fp8x4_e4m3* frag_b, + int zp) { + // INT4 with zp -> FP8 + // see https://github.com/vllm-project/vllm/pull/24722 + uint32_t u_q = *reinterpret_cast(&q); + uint32_t u_zp = *reinterpret_cast(&zp); + uint32_t u_zp1 = u_zp + 1; + uint32_t repeated_zp = 0x01010101 * u_zp; + + uint32_t q0, s; + q0 = (u_q & 0x0F0F0F0F) | 0x70707070; + s = (q0 + repeated_zp) & 0x80808080; + uint32_t Out1 = (q0 + (s >> 7) * u_zp1) & 0x0F0F0F0F | s; + + u_q >>= 4; + q0 = (u_q & 0x0F0F0F0F) | 0x70707070; + s = (q0 + repeated_zp) & 0x80808080; + uint32_t Out2 = (q0 + (s >> 7) * u_zp1) & 0x0F0F0F0F | s; + + frag_b[0] = *reinterpret_cast(&Out1); + frag_b[1] = *reinterpret_cast(&Out2); +} + +#endif + +} // namespace MARLIN_NAMESPACE_NAME diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/marlin.cuh b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/marlin.cuh new file mode 100644 index 0000000000..88057dd2d1 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/marlin.cuh @@ -0,0 +1,173 @@ +#pragma once + +#ifndef _marlin_cuh + #define _marlin_cuh + #include + #include + #include + #include + + #ifndef MARLIN_NAMESPACE_NAME + #define MARLIN_NAMESPACE_NAME marlin + #endif + +namespace MARLIN_NAMESPACE_NAME { + +// Marlin params + +// 8 warps are a good choice since every SM has 4 schedulers and having more +// than 1 warp per schedule allows some more latency hiding. At the same time, +// we want relatively few warps to have many registers per warp and small tiles. +static constexpr int default_threads = 256; + +static constexpr int pipe_stages = + 4; // 4 pipeline stages fit into shared memory + +static constexpr int min_thread_n = 64; +static constexpr int min_thread_k = 64; +static constexpr int max_thread_n = 256; + +static constexpr int tile_size = 16; +static constexpr int max_par = 16; + +// Repack params +static constexpr int repack_stages = 8; + +static constexpr int repack_threads = 256; + +static constexpr int tile_k_size = tile_size; +static constexpr int tile_n_size = tile_k_size * 4; + +// Helpers +template +struct Vec { + T elems[n]; + __device__ T& operator[](int i) { return elems[i]; } +}; + +using I4 = Vec; + +constexpr int div_ceil(int a, int b) { return (a + b - 1) / b; } + + #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ < 800 + +__device__ inline void cp_async1_ca_pred(void* smem_ptr, const void* glob_ptr, + bool pred = true) { + if (pred) { + reinterpret_cast(smem_ptr)[0] = + reinterpret_cast(glob_ptr)[0]; + } +} + +__device__ inline void cp_async2_ca_pred(void* smem_ptr, const void* glob_ptr, + bool pred = true) { + if (pred) { + reinterpret_cast(smem_ptr)[0] = + reinterpret_cast(glob_ptr)[0]; + } +} + +__device__ inline void cp_async4_ca_pred(void* smem_ptr, const void* glob_ptr, + bool pred = true) { + if (pred) { + reinterpret_cast(smem_ptr)[0] = + reinterpret_cast(glob_ptr)[0]; + } +} + +__device__ inline void cp_async4_pred(void* smem_ptr, const void* glob_ptr, + bool pred = true) { + if (pred) { + reinterpret_cast(smem_ptr)[0] = + reinterpret_cast(glob_ptr)[0]; + } +} + +__device__ inline void cp_async4(void* smem_ptr, const void* glob_ptr) { + reinterpret_cast(smem_ptr)[0] = + reinterpret_cast(glob_ptr)[0]; +} + +__device__ inline void cp_async_fence() {} + +template +__device__ inline void cp_async_wait() {} + + #else + +__device__ inline void cp_async1_ca_pred(void* smem_ptr, const void* glob_ptr, + bool pred = true) { + const int BYTES = 4; + uint32_t smem = static_cast(__cvta_generic_to_shared(smem_ptr)); + asm volatile( + "{\n" + " .reg .pred p;\n" + " setp.ne.b32 p, %0, 0;\n" + " @p cp.async.ca.shared.global [%1], [%2], %3;\n" + "}\n" ::"r"((int)pred), + "r"(smem), "l"(glob_ptr), "n"(BYTES)); +} + +__device__ inline void cp_async2_ca_pred(void* smem_ptr, const void* glob_ptr, + bool pred = true) { + const int BYTES = 8; + uint32_t smem = static_cast(__cvta_generic_to_shared(smem_ptr)); + asm volatile( + "{\n" + " .reg .pred p;\n" + " setp.ne.b32 p, %0, 0;\n" + " @p cp.async.ca.shared.global [%1], [%2], %3;\n" + "}\n" ::"r"((int)pred), + "r"(smem), "l"(glob_ptr), "n"(BYTES)); +} + +__device__ inline void cp_async4_ca_pred(void* smem_ptr, const void* glob_ptr, + bool pred = true) { + const int BYTES = 16; + uint32_t smem = static_cast(__cvta_generic_to_shared(smem_ptr)); + asm volatile( + "{\n" + " .reg .pred p;\n" + " setp.ne.b32 p, %0, 0;\n" + " @p cp.async.ca.shared.global [%1], [%2], %3;\n" + "}\n" ::"r"((int)pred), + "r"(smem), "l"(glob_ptr), "n"(BYTES)); +} + +__device__ inline void cp_async4_pred(void* smem_ptr, const void* glob_ptr, + bool pred = true) { + const int BYTES = 16; + uint32_t smem = static_cast(__cvta_generic_to_shared(smem_ptr)); + asm volatile( + "{\n" + " .reg .pred p;\n" + " setp.ne.b32 p, %0, 0;\n" + " @p cp.async.cg.shared.global [%1], [%2], %3;\n" + "}\n" ::"r"((int)pred), + "r"(smem), "l"(glob_ptr), "n"(BYTES)); +} + +__device__ inline void cp_async4(void* smem_ptr, const void* glob_ptr) { + const int BYTES = 16; + uint32_t smem = static_cast(__cvta_generic_to_shared(smem_ptr)); + asm volatile( + "{\n" + " cp.async.cg.shared.global [%0], [%1], %2;\n" + "}\n" ::"r"(smem), + "l"(glob_ptr), "n"(BYTES)); +} + +__device__ inline void cp_async_fence() { + asm volatile("cp.async.commit_group;\n" ::); +} + +template +__device__ inline void cp_async_wait() { + asm volatile("cp.async.wait_group %0;\n" ::"n"(n)); +} + + #endif + +} // namespace MARLIN_NAMESPACE_NAME + +#endif diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/marlin_dtypes.cuh b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/marlin_dtypes.cuh new file mode 100644 index 0000000000..a4807a6887 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/marlin_dtypes.cuh @@ -0,0 +1,149 @@ + +#ifndef _data_types_cuh +#define _data_types_cuh +#include "marlin.cuh" +#include "core/scalar_type.hpp" +#include +#include +#include + +#ifndef MARLIN_NAMESPACE_NAME + #define MARLIN_NAMESPACE_NAME marlin +#endif + +namespace MARLIN_NAMESPACE_NAME { + +template +class MarlinScalarType {}; + +template <> +class MarlinScalarType { + public: + using scalar_t = half; + using scalar_t2 = half2; + using scalar_t4 = half2; + using scalar_32bit_t = half2; + + // Matrix fragments for tensor core instructions; their precise layout is + // documented here: + // https://docs.nvidia.com/cuda/parallel-thread-execution/index.html#matrix-fragments-for-mma-m16n8k16-with-floating-point-type + using FragA = Vec; + using FragB = Vec; + using FragC = Vec; + using FragS = Vec; + using FragS0 = Vec<__nv_fp8x2_e4m3, 1>; + using FragZP = Vec; + + static __device__ float inline num2float(const half x) { + return __half2float(x); + } + + static __device__ half2 inline num2num2(const half x) { + return __half2half2(x); + } + + static __device__ half2 inline nums2num2(const half x1, const half x2) { + return __halves2half2(x1, x2); + } + + static __host__ __device__ half inline float2num(const float x) { + return __float2half(x); + } + + static __host__ __device__ float2 inline num22float2(const half2 x) { + return __half22float2(x); + } +}; + +template <> +class MarlinScalarType { + public: + using scalar_t = nv_bfloat16; + using scalar_t2 = nv_bfloat162; + using scalar_t4 = nv_bfloat162; + using scalar_32bit_t = nv_bfloat162; + + using FragA = Vec; + using FragB = Vec; + using FragC = Vec; + using FragS = Vec; + using FragS0 = Vec<__nv_fp8x2_e4m3, 1>; + using FragZP = Vec; + +#if !defined(__CUDA_ARCH__) || __CUDA_ARCH__ >= 800 + static __device__ float inline num2float(const nv_bfloat16 x) { + return __bfloat162float(x); + } + + static __device__ nv_bfloat162 inline num2num2(const nv_bfloat16 x) { + return __bfloat162bfloat162(x); + } + + static __device__ nv_bfloat162 inline nums2num2(const nv_bfloat16 x1, + const nv_bfloat16 x2) { + return __halves2bfloat162(x1, x2); + } + + static __host__ __device__ nv_bfloat16 inline float2num(const float x) { + return __float2bfloat16(x); + } + + static __host__ __device__ float2 inline num22float2(const nv_bfloat162 x) { + return __bfloat1622float2(x); + } +#endif +}; + +template <> +class MarlinScalarType { + public: + using scalar_t = __nv_fp8_e4m3; + using scalar_t2 = __nv_fp8x2_e4m3; + using scalar_t4 = __nv_fp8x4_e4m3; + using scalar_32bit_t = __nv_fp8x4_e4m3; + + using FragA = Vec<__nv_fp8x4_e4m3, 4>; + using FragB = Vec<__nv_fp8x4_e4m3, 2>; + using FragC = Vec; + using FragZP = Vec<__nv_fp8x2_e4m3, 4>; + + static __host__ __device__ + float2 inline num22float2(const __nv_fp8x2_e4m3 x) { + return (float2)x; + } +}; + +template <> +class MarlinScalarType { + public: + using scalar_t = int8_t; + using scalar_t2 = int16_t; + using scalar_t4 = int32_t; + using scalar_32bit_t = int32_t; + + using FragA = Vec; + using FragB = Vec; + using FragC = Vec; + using FragZP = Vec; +}; + +template +class MarlinScalarType2 {}; + +template <> +class MarlinScalarType2 : public MarlinScalarType {}; + +template <> +class MarlinScalarType2 + : public MarlinScalarType {}; + +template <> +class MarlinScalarType2<__nv_fp8_e4m3> + : public MarlinScalarType {}; + +template <> +class MarlinScalarType2 : public MarlinScalarType {}; + +} // namespace MARLIN_NAMESPACE_NAME + +#endif diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/marlin_mma.h b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/marlin_mma.h new file mode 100644 index 0000000000..41c9074077 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/_csrc/libtorch_stable/quantization/marlin/marlin_mma.h @@ -0,0 +1,269 @@ + +#include "marlin_dtypes.cuh" + +namespace MARLIN_NAMESPACE_NAME { + +// m16n8k16 tensor core mma instruction with fp16 inputs and fp32 +// output/accumulation. +template +__device__ inline void mma( + const typename MarlinScalarType::FragA& a_frag, + const typename MarlinScalarType::FragB& frag_b, + typename MarlinScalarType::FragC& frag_c, int idx = 0) { + const uint32_t* a = reinterpret_cast(&a_frag); + const uint32_t* b = reinterpret_cast(&frag_b); + using scalar_t = typename MarlinScalarType::scalar_t; + if constexpr (!std::is_same::value || k_size != 16) { + static_assert(!use_fp16_accum); + } + + if constexpr (k_size == 16) { + if constexpr (std::is_same::value && !use_fp16_accum) { +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == 750 + float* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k8.row.col.f32.f16.f16.f32 " + "{%0,%1,%2,%3}, {%4,%5}, {%6}, {%7,%8,%9,%10};\n" + : "=f"(c[0]), "=f"(c[1]), "=f"(c[2]), "=f"(c[3]) + : "r"(a[0]), "r"(a[1]), "r"(b[0]), "f"(c[0]), "f"(c[1]), "f"(c[2]), + "f"(c[3])); + asm volatile( + "mma.sync.aligned.m16n8k8.row.col.f32.f16.f16.f32 " + "{%0,%1,%2,%3}, {%4,%5}, {%6}, {%7,%8,%9,%10};\n" + : "=f"(c[0]), "=f"(c[1]), "=f"(c[2]), "=f"(c[3]) + : "r"(a[2]), "r"(a[3]), "r"(b[1]), "f"(c[0]), "f"(c[1]), "f"(c[2]), + "f"(c[3])); +#else + float* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.f16.f16.f32 " + "{%0,%1,%2,%3}, {%4,%5,%6,%7}, {%8,%9}, {%10,%11,%12,%13};\n" + : "=f"(c[0]), "=f"(c[1]), "=f"(c[2]), "=f"(c[3]) + : "r"(a[0]), "r"(a[1]), "r"(a[2]), "r"(a[3]), "r"(b[0]), "r"(b[1]), + "f"(c[0]), "f"(c[1]), "f"(c[2]), "f"(c[3])); +#endif + } else if constexpr (std::is_same::value && + use_fp16_accum) { +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == 750 + uint32_t* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k8.row.col.f16.f16.f16.f16 " + "{%0,%1}, {%2,%3}, {%4}, {%5,%6};\n" + : "=r"(c[0]), "=r"(c[1]) + : "r"(a[0]), "r"(a[1]), "r"(b[0]), "r"(c[0]), "r"(c[1])); + asm volatile( + "mma.sync.aligned.m16n8k8.row.col.f16.f16.f16.f16 " + "{%0,%1}, {%2,%3}, {%4}, {%5,%6};\n" + : "=r"(c[0]), "=r"(c[1]) + : "r"(a[2]), "r"(a[3]), "r"(b[1]), "r"(c[0]), "r"(c[1])); +#else + uint32_t* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f16.f16.f16.f16 " + "{%0,%1}, {%2,%3,%4,%5}, {%6,%7}, {%8,%9};\n" + : "=r"(c[0]), "=r"(c[1]) + : "r"(a[0]), "r"(a[1]), "r"(a[2]), "r"(a[3]), "r"(b[0]), "r"(b[1]), + "r"(c[0]), "r"(c[1])); +#endif + } else if constexpr (std::is_same::value) { + float* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 " + "{%0,%1,%2,%3}, {%4,%5,%6,%7}, {%8,%9}, {%10,%11,%12,%13};\n" + : "=f"(c[0]), "=f"(c[1]), "=f"(c[2]), "=f"(c[3]) + : "r"(a[0]), "r"(a[1]), "r"(a[2]), "r"(a[3]), "r"(b[0]), "r"(b[1]), + "f"(c[0]), "f"(c[1]), "f"(c[2]), "f"(c[3])); + } else if constexpr (std::is_same::value) { + float* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.e4m3.e4m3.f32 " + "{%0,%1,%2,%3}, {%4,%5}, {%6}, {%7,%8,%9,%10};\n" + : "=f"(c[0]), "=f"(c[1]), "=f"(c[2]), "=f"(c[3]) + : "r"(a[idx * 2]), "r"(a[idx * 2 + 1]), "r"(b[idx]), "f"(c[0]), + "f"(c[1]), "f"(c[2]), "f"(c[3])); + } else if constexpr (std::is_same::value) { + int32_t* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.s32.s8.s8.s32.satfinite " + "{%0,%1,%2,%3}, {%4,%5}, {%6}, {%7,%8,%9,%10};\n" + : "=r"(c[0]), "=r"(c[1]), "=r"(c[2]), "=r"(c[3]) + : "r"(a[idx * 2]), "r"(a[idx * 2 + 1]), "r"(b[idx]), "r"(c[0]), + "r"(c[1]), "r"(c[2]), "r"(c[3])); + } + } else if (k_size == 32) { + if constexpr (std::is_same::value) { + float* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k32.row.col.f32.e4m3.e4m3.f32 " + "{%0,%1,%2,%3}, {%4,%5,%6,%7}, {%8,%9}, {%10,%11,%12,%13};\n" + : "=f"(c[0]), "=f"(c[1]), "=f"(c[2]), "=f"(c[3]) + : "r"(a[0]), "r"(a[1]), "r"(a[2]), "r"(a[3]), "r"(b[0]), "r"(b[1]), + "f"(c[0]), "f"(c[1]), "f"(c[2]), "f"(c[3])); + } else if constexpr (std::is_same::value) { + int32_t* c = reinterpret_cast(&frag_c); +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == 750 + asm volatile( + "mma.sync.aligned.m8n8k16.row.col.s32.s8.s8.s32.satfinite " + "{%0,%1}, {%2}, {%3}, {%4,%5};\n" + : "=r"(c[0]), "=r"(c[1]) + : "r"(a[0]), "r"(b[0]), "r"(c[0]), "r"(c[1])); + asm volatile( + "mma.sync.aligned.m8n8k16.row.col.s32.s8.s8.s32.satfinite " + "{%0,%1}, {%2}, {%3}, {%4,%5};\n" + : "=r"(c[2]), "=r"(c[3]) + : "r"(a[1]), "r"(b[0]), "r"(c[2]), "r"(c[3])); + asm volatile( + "mma.sync.aligned.m8n8k16.row.col.s32.s8.s8.s32.satfinite " + "{%0,%1}, {%2}, {%3}, {%4,%5};\n" + : "=r"(c[0]), "=r"(c[1]) + : "r"(a[2]), "r"(b[1]), "r"(c[0]), "r"(c[1])); + asm volatile( + "mma.sync.aligned.m8n8k16.row.col.s32.s8.s8.s32.satfinite " + "{%0,%1}, {%2}, {%3}, {%4,%5};\n" + : "=r"(c[2]), "=r"(c[3]) + : "r"(a[3]), "r"(b[1]), "r"(c[2]), "r"(c[3])); +#else + asm volatile( + "mma.sync.aligned.m16n8k32.row.col.s32.s8.s8.s32.satfinite " + "{%0,%1,%2,%3}, {%4,%5,%6,%7}, {%8,%9}, {%10,%11,%12,%13};\n" + : "=r"(c[0]), "=r"(c[1]), "=r"(c[2]), "=r"(c[3]) + : "r"(a[0]), "r"(a[1]), "r"(a[2]), "r"(a[3]), "r"(b[0]), "r"(b[1]), + "r"(c[0]), "r"(c[1]), "r"(c[2]), "r"(c[3])); +#endif + } + } +} + +template +__device__ inline void mma_trans( + const typename MarlinScalarType::FragA& a_frag, + const typename MarlinScalarType::FragB& frag_b, + const typename MarlinScalarType::FragB& frag_b2, + typename MarlinScalarType::FragC& frag_c) { + const uint32_t* a = reinterpret_cast(&a_frag); + const uint32_t* b = reinterpret_cast(&frag_b); + const uint32_t* b2 = reinterpret_cast(&frag_b2); + float* c = reinterpret_cast(&frag_c); + using scalar_t = typename MarlinScalarType::scalar_t; + if constexpr (!std::is_same::value || k_size != 16) { + static_assert(!use_fp16_accum); + } + + if constexpr (k_size == 16) { + if constexpr (std::is_same::value && !use_fp16_accum) { +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == 750 + float* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k8.row.col.f32.f16.f16.f32 " + "{%0,%1,%2,%3}, {%4,%5}, {%6}, {%7,%8,%9,%10};\n" + : "=f"(c[0]), "=f"(c[1]), "=f"(c[2]), "=f"(c[3]) + : "r"(b[0]), "r"(b2[0]), "r"(a[0]), "f"(c[0]), "f"(c[1]), "f"(c[2]), + "f"(c[3])); + asm volatile( + "mma.sync.aligned.m16n8k8.row.col.f32.f16.f16.f32 " + "{%0,%1,%2,%3}, {%4,%5}, {%6}, {%7,%8,%9,%10};\n" + : "=f"(c[0]), "=f"(c[1]), "=f"(c[2]), "=f"(c[3]) + : "r"(b[1]), "r"(b2[1]), "r"(a[1]), "f"(c[0]), "f"(c[1]), "f"(c[2]), + "f"(c[3])); +#else + float* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.f16.f16.f32 " + "{%0,%1,%2,%3}, {%4,%5,%6,%7}, {%8,%9}, {%10,%11,%12,%13};\n" + : "=f"(c[0]), "=f"(c[1]), "=f"(c[2]), "=f"(c[3]) + : "r"(b[0]), "r"(b2[0]), "r"(b[1]), "r"(b2[1]), "r"(a[0]), "r"(a[1]), + "f"(c[0]), "f"(c[1]), "f"(c[2]), "f"(c[3])); +#endif + } else if constexpr (std::is_same::value && + use_fp16_accum) { +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == 750 + uint32_t* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k8.row.col.f16.f16.f16.f16 " + "{%0,%1}, {%2,%3}, {%4}, {%5,%6};\n" + : "=r"(c[0]), "=r"(c[1]) + : "r"(b[0]), "r"(b2[0]), "r"(a[0]), "r"(c[0]), "r"(c[1])); + asm volatile( + "mma.sync.aligned.m16n8k8.row.col.f16.f16.f16.f16 " + "{%0,%1}, {%2,%3}, {%4}, {%5,%6};\n" + : "=r"(c[0]), "=r"(c[1]) + : "r"(b[1]), "r"(b2[1]), "r"(a[1]), "r"(c[0]), "r"(c[1])); +#else + uint32_t* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f16.f16.f16.f16 " + "{%0,%1}, {%2,%3,%4,%5}, {%6,%7}, {%8,%9};\n" + : "=r"(c[0]), "=r"(c[1]) + : "r"(b[0]), "r"(b2[0]), "r"(b[1]), "r"(b2[1]), "r"(a[0]), "r"(a[1]), + "r"(c[0]), "r"(c[1])); +#endif + } else if constexpr (std::is_same::value) { + float* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 " + "{%0,%1,%2,%3}, {%4,%5,%6,%7}, {%8,%9}, {%10,%11,%12,%13};\n" + : "=f"(c[0]), "=f"(c[1]), "=f"(c[2]), "=f"(c[3]) + : "r"(b[0]), "r"(b2[0]), "r"(b[1]), "r"(b2[1]), "r"(a[0]), "r"(a[1]), + "f"(c[0]), "f"(c[1]), "f"(c[2]), "f"(c[3])); + } else if constexpr (std::is_same::value) { + float* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.e4m3.e4m3.f32 " + "{%0,%1,%2,%3}, {%4,%5}, {%6}, {%7,%8,%9,%10};\n" + : "=f"(c[0]), "=f"(c[1]), "=f"(c[2]), "=f"(c[3]) + : "r"(b[0]), "r"(b2[0]), "r"(a[0]), "f"(c[0]), "f"(c[1]), "f"(c[2]), + "f"(c[3])); + } else if constexpr (std::is_same::value) { + int32_t* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.s32.s8.s8.s32.satfinite " + "{%0,%1,%2,%3}, {%4,%5}, {%6}, {%7,%8,%9,%10};\n" + : "=r"(c[0]), "=r"(c[1]), "=r"(c[2]), "=r"(c[3]) + : "r"(b[0]), "r"(b2[0]), "r"(a[0]), "r"(c[0]), "r"(c[1]), "r"(c[2]), + "r"(c[3])); + } + } else { + if constexpr (std::is_same::value) { + float* c = reinterpret_cast(&frag_c); + asm volatile( + "mma.sync.aligned.m16n8k32.row.col.f32.e4m3.e4m3.f32 " + "{%0,%1,%2,%3}, {%4,%5,%6,%7}, {%8,%9}, {%10,%11,%12,%13};\n" + : "=f"(c[0]), "=f"(c[1]), "=f"(c[2]), "=f"(c[3]) + : "r"(b[0]), "r"(b2[0]), "r"(b[1]), "r"(b2[1]), "r"(a[0]), "r"(a[1]), + "f"(c[0]), "f"(c[1]), "f"(c[2]), "f"(c[3])); + } else if constexpr (std::is_same::value) { + int32_t* c = reinterpret_cast(&frag_c); +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == 750 + asm volatile( + "mma.sync.aligned.m8n8k16.row.col.s32.s8.s8.s32.satfinite " + "{%0,%1}, {%2}, {%3}, {%4,%5};\n" + : "=r"(c[0]), "=r"(c[1]) + : "r"(b[0]), "r"(a[0]), "r"(c[0]), "r"(c[1])); + asm volatile( + "mma.sync.aligned.m8n8k16.row.col.s32.s8.s8.s32.satfinite " + "{%0,%1}, {%2}, {%3}, {%4,%5};\n" + : "=r"(c[2]), "=r"(c[3]) + : "r"(b2[1]), "r"(a[0]), "r"(c[2]), "r"(c[3])); + asm volatile( + "mma.sync.aligned.m8n8k16.row.col.s32.s8.s8.s32.satfinite " + "{%0,%1}, {%2}, {%3}, {%4,%5};\n" + : "=r"(c[0]), "=r"(c[1]) + : "r"(b[0]), "r"(a[1]), "r"(c[0]), "r"(c[1])); + asm volatile( + "mma.sync.aligned.m8n8k16.row.col.s32.s8.s8.s32.satfinite " + "{%0,%1}, {%2}, {%3}, {%4,%5};\n" + : "=r"(c[2]), "=r"(c[3]) + : "r"(b2[1]), "r"(a[1]), "r"(c[2]), "r"(c[3])); +#else + asm volatile( + "mma.sync.aligned.m16n8k32.row.col.s32.s8.s8.s32.satfinite " + "{%0,%1,%2,%3}, {%4,%5,%6,%7}, {%8,%9}, {%10,%11,%12,%13};\n" + : "=r"(c[0]), "=r"(c[1]), "=r"(c[2]), "=r"(c[3]) + : "r"(b[0]), "r"(b2[0]), "r"(b[1]), "r"(b2[1]), "r"(a[0]), "r"(a[1]), + "r"(c[0]), "r"(c[1]), "r"(c[2]), "r"(c[3])); +#endif + } + } +} + +} // namespace MARLIN_NAMESPACE_NAME diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/backend.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/backend.py new file mode 100644 index 0000000000..a615fb4e85 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/backend.py @@ -0,0 +1,248 @@ +"""Marlin W4A16 forward base for the grouped NVFP4 MoE (Ampere+; sm80/89/90/100/120). + +Prep the frozen NVFP4 experts into Marlin layout ONCE (``build_marlin_forward_base``); run the +bf16-activation forward GEMM (``marlin_base_forward``) on the pre-scattered, pad-to-TILE grouped +layout that ``grouped_fp4_moe_train`` already builds. Marlin reads the activation directly in bf16 +and gathers per expert via identity ``sorted_token_ids`` + per-block ``expert_ids`` (derived from the +per-tile ``m_indices``) + ``top_k=1`` — validated bit-correct on that layout. + +The marlin path pads to TILE=64 (vs CUTLASS's forced 128): at thin-M that halves the padded rows +(2.7x -> 1.4x), which is where the speedup comes from (each expert weight is read once either way). +""" + +from __future__ import annotations + +import torch + +from . import load_ext +from .prep import ( + marlin_make_workspace_new, + marlin_moe_gemm, + prepare_nvfp4_weight_for_marlin, +) + +# Must equal the marlin MoE block (BSM) so each padded tile maps to exactly one expert; 64 +# halves the thin-M padding vs CUTLASS's 128. +MARLIN_TILE = 64 +_BSM = 64 + +_BASE_SCATTER_LUT: dict[torch.device, torch.Tensor] = {} + + +def _pt(nv, E, dev): + p = getattr(nv, "per_tensor_scale", None) + if p is None: + return torch.ones(E, device=dev) + p = p.reshape(-1).float() + return p.expand(E) if p.numel() == 1 else p + + +def _build_base_scatter(dev: torch.device) -> torch.Tensor: + """Build and cache the 1024-entry scatter LUT: qdata flat nibble pos -> marlin flat nibble pos. + Probes the base tile (N_t=64, K_t=16) using gptq_marlin_repack; result is device-resident.""" + if dev in _BASE_SCATTER_LUT: + return _BASE_SCATTER_LUT[dev] + ext = load_ext() + perm = torch.empty(0, dtype=torch.int, device=dev) + N_t, K_t = 64, 16 + scatter = torch.full((N_t * K_t,), -1, dtype=torch.int32, device="cpu") + for n_b in range(N_t): + for k_b in range(K_t): + qd = torch.zeros(N_t, K_t // 2, dtype=torch.uint8, device=dev) + byte_idx = k_b // 2 + qd[n_b, byte_idx] = 0x0F if k_b % 2 == 0 else 0xF0 + qw_i = qd.view(torch.int32).T.contiguous() + qw_m = ext.gptq_marlin_repack(qw_i, perm, K_t, N_t, 4, False) + flat = qw_m.reshape(-1).cpu().tolist() + for wi, word in enumerate(flat): + if word == 0: + continue + for bit in range(8): + if (word >> (bit * 4)) & 0xF == 0xF: + scatter[n_b * K_t + k_b] = wi * 8 + bit + break + else: + continue + break + lut = scatter.to(dev) + _BASE_SCATTER_LUT[dev] = lut + return lut + + +def _cached_prep(nv, size_n, size_k, ext, cache, key): + """Prep one NVFP4 weight -> Marlin layout, cached (experts are frozen). Prefer a persistent + module-level ``cache`` dict keyed by ``key`` — under FSDP2 the gathered param is a fresh tensor + each step, so a module cache (not a per-tensor attr) avoids re-repacking 256 experts/forward. + Falls back to a per-tensor attribute, then a one-shot compute. + + On the first build (single-GPU only), saves original scales + per-tensor scale in cache under + key+"_bwd" for the fused backward dequant, then frees nv.qdata to drop the duplicate 4-bit copy. + Under FSDP the gathered param is fresh each step so weight_recipe() re-gathers it anyway; skips + the free when distributed is active.""" + bwd_key = key + "_bwd" + if cache is not None and key in cache: + return cache[key] + assert not getattr(nv, "is_swizzled_scales", False), ( + "marlin_w4a16 needs raw (non-swizzled) NVFP4 scales; got is_swizzled_scales=True" + ) + cached = getattr(nv, "_marlin_w4a16", None) + qdata_fresh = cached is None + if cached is None: + E, dev = nv.qdata.size(0), nv.qdata.device + cached = prepare_nvfp4_weight_for_marlin( + nv.qdata, + nv.scale, + _pt(nv, E, dev), + size_n, + size_k, + torch.bfloat16, + ext.gptq_marlin_repack, + ) + try: + nv._marlin_w4a16 = cached + except (AttributeError, RuntimeError): + pass + if cache is not None: + cache[key] = cached + if qdata_fresh and bwd_key not in cache: + import torch.distributed as dist + + _is_distributed = ( + dist.is_available() + and dist.is_initialized() + and dist.get_world_size() > 1 + ) + if not _is_distributed: + E, dev = nv.qdata.size(0), nv.qdata.device + # Reference (don't clone) the original scales for the backward dequant: the + # marlin scales subsample [:, 1::2] so they're lossy and unusable for backward. + # Single-GPU: nv.scale stays live on the frozen param, so a clone just dups ~1.3 GiB. + # _pt may return .expand() with stride(0)==0; Triton needs a real stride. + pt_bwd = _pt(nv, E, dev) + if pt_bwd.stride(0) == 0: + pt_bwd = pt_bwd.contiguous() + cache[bwd_key] = (nv.scale, pt_bwd, E, size_n, size_k) + # 3-D [E, N, 0] placeholder (not flat empty(0)): NVFP4Tensor clone/save needs + # ndim==3, and numel()==0 still routes through the marlin qdata-free paths. + try: + _N = nv.qdata.size(1) + nv.qdata.data = torch.empty( + (E, _N, 0), dtype=nv.qdata.dtype, device=dev + ) + except (AttributeError, RuntimeError): + pass + return cached + + +def _qdata_sizes(nv, cache_key, cache): + """Return (size_n, size_k_packed, device) for the NVFP4 weight. + Falls back to bwd cache when nv.qdata has been freed (single-GPU memory-free path).""" + qdata = nv.qdata + if qdata.numel() > 0: + return qdata.size(1), qdata.size(2), torch.device(qdata.device) + if cache is not None: + bwd = cache.get(cache_key + "_bwd") + if bwd is not None: + # bwd = (scale, pt, E, size_n, size_k); size_k_packed = size_k // 2 + size_n, size_k = bwd[3], bwd[4] + dev = bwd[0].device + return size_n, size_k // 2, dev + raise RuntimeError( + f"marlin_w4a16: nv.qdata has been freed but no backward cache found for '{cache_key}'. " + "Pass a module-level mxfp4_cache to grouped_fp4_moe_train." + ) + + +def build_marlin_forward_base(gate_up_nv, down_nv, cache=None): + """Prep frozen NVFP4 experts -> Marlin forward weights (cached; experts are frozen). + + gate_up_nv: NVFP4Tensor [E, 2I, H]; down_nv: NVFP4Tensor [E, H, I]. ``cache`` is the optional + persistent dict (the same ``mxfp4_cache`` threaded through ``grouped_fp4_moe_train``). + Returns the base tuple ``('marlin', (gu_w, dn_w), (twoI, H, I), workspace, cache)`` consumed by + ``marlin_base_forward`` and dispatched in ``grouped_train._base_forward``. The cache is threaded + through so the backward can retrieve the (scale, pt, E, N, K) bwd tuple from cache[key+"_bwd"].""" + ext = load_ext() + twoI, H_packed, dev = _qdata_sizes(gate_up_nv, "marlin_gate_up", cache) + H = H_packed * 2 + _, I_packed, _ = _qdata_sizes(down_nv, "marlin_down", cache) + I = I_packed * 2 # noqa: E741 + gu_w = _cached_prep(gate_up_nv, twoI, H, ext, cache, "marlin_gate_up") + dn_w = _cached_prep(down_nv, H, I, ext, cache, "marlin_down") + ws = marlin_make_workspace_new(dev, 4) + return ("marlin", (gu_w, dn_w), (twoI, H, I), ws, cache) + + +def marlin_route(m_indices, Mt, dev): + """Build Marlin's routing for the pre-scattered pad-TILE layout: identity ``sorted_token_ids`` + (the activation is already grouped+padded), per-BSM-block ``expert_ids`` from the per-tile + ``m_indices``, ``num_tokens_post_padded`` = Mt, and unit ``topk_weights`` (top_k=1).""" + tile = Mt // m_indices.numel() + si = torch.arange(Mt, dtype=torch.int32, device=dev) + ei = m_indices.repeat_interleave(tile // _BSM).to(torch.int32) + ntpp = torch.tensor([Mt], dtype=torch.int32, device=dev) + tw = torch.ones(Mt, 1, dtype=torch.float32, device=dev) + return si, ei, ntpp, tw + + +def marlin_base_forward(base, which, x, m_indices): + """Frozen-expert base GEMM via Marlin (bf16 act). which=0 gate_up (x[Mt,H] -> [Mt,2I]); + which=1 down (x[Mt,I] -> [Mt,H]). ``m_indices`` is the per-TILE expert id from the routing. + Returns bf16.""" + ext = load_ext() + _, (gu_w, dn_w), (twoI, H, I), ws = base[0], base[1], base[2], base[3] # noqa: E741 + Mt = x.size(0) + si, ei, ntpp, tw = marlin_route(m_indices, Mt, x.device) + if which == 0: + return marlin_moe_gemm( + ext, + x, + gu_w[0], + gu_w[1], + gu_w[2], + ws, + si, + ei, + ntpp, + tw, + _BSM, + 1, + False, + Mt, + twoI, + H, + ) + return marlin_moe_gemm( + ext, + x, + dn_w[0], + dn_w[1], + dn_w[2], + ws, + si, + ei, + ntpp, + tw, + _BSM, + 1, + False, + Mt, + H, + I, + ) + + +def marlin_bwd_data(base): + """Return (gu_bwd, dn_bwd) tuples from the cache, or None if not available. + Each bwd tuple is (original_scale, pt, E, size_n, size_k); marlin qw is at cache[fwd_key][0].""" + cache = base[4] if len(base) > 4 else None + if cache is None: + return None + gu_bwd = cache.get("marlin_gate_up_bwd") + dn_bwd = cache.get("marlin_down_bwd") + if gu_bwd is None or dn_bwd is None: + return None + gu_qw = cache.get("marlin_gate_up") + dn_qw = cache.get("marlin_down") + if gu_qw is None or dn_qw is None: + return None + return (gu_qw[0], gu_bwd), (dn_qw[0], dn_bwd) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/fused_dequant.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/fused_dequant.py new file mode 100644 index 0000000000..aff1e715c4 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/fused_dequant.py @@ -0,0 +1,179 @@ +"""Fused Triton kernels: marlin int32 layout -> bf16 / fp8_e4m3, without intermediate reconstruction. + +The backward dequant in the marlin training path needs to reconstruct the expert weights from the +marlin qweight cache (int32, tile-scattered nibbles) + the original NVFP4 block scales. A two-step +approach (reconstruct nibble array + call nvfp4_dequant_bf16) is ~12x slower due to the scattered +gather; these kernels fold both steps into one Triton pass with only sequential reads on qweight and +sequential writes on output, bringing cost to ~1.2-1.6x the direct nvfp4_dequant. + +The marlin tile layout (N_t=64, K_t=16) scatters nibbles according to gptq_marlin_repack. A 1024-entry +LUT (base scatter, built once per process from _build_base_scatter in backend.py) maps each +(local_n, local_k) position within a base tile to its flat nibble position in the marlin output. +Larger (N, K) tile: offset = (k_tile * N_TILES + n_tile) * 1024 nibbles. +""" + +from __future__ import annotations + +import triton +import triton.language as tl + +# Grid (C, N // 64, K // 16): one block per (expert-chunk, n-tile, k-tile), each a 64x16 tile. + + +@triton.jit +def _marlin_dequant_bf16_kernel( + QW, # [C, K*N//8] int32 (marlin flat) + SC, # [C, N, K//16] fp8_e4m3 + PT, # [C] float32 per-tensor scale + SCATTER, # [1024] int32 base scatter LUT + CB, # [16] float32 fp4 codebook + OUT, # [C, N, K] bfloat16 + qw_stride_c: tl.constexpr, + sc_stride_c: tl.constexpr, + sc_stride_n: tl.constexpr, + out_stride_c: tl.constexpr, + out_stride_n: tl.constexpr, + N_TILES: tl.constexpr, + N_t: tl.constexpr, # 64 + K_t: tl.constexpr, # 16 + TILE_SIZE: tl.constexpr, # N_t * K_t = 1024 +): + c_id = tl.program_id(0) + n_tile = tl.program_id(1) + k_tile = tl.program_id(2) + + local_idx = tl.arange(0, TILE_SIZE) + local_n = local_idx // K_t + local_k = local_idx % K_t + + local_marlin_pos = tl.load(SCATTER + local_n * K_t + local_k) + tile_off = (k_tile * N_TILES + n_tile) * TILE_SIZE + nib_pos = tile_off + local_marlin_pos + + words = tl.load(QW + c_id * qw_stride_c + nib_pos // 8) + nibs = (words >> ((nib_pos % 8) * 4)) & 0xF + + pt = tl.load(PT + c_id) + global_n = n_tile * N_t + local_n + sc = tl.load(SC + c_id * sc_stride_c + global_n * sc_stride_n + k_tile).to( + tl.float32 + ) + cb_val = tl.load(CB + nibs) + + out_val = (cb_val * sc * pt).to(tl.bfloat16) + tl.store( + OUT + c_id * out_stride_c + global_n * out_stride_n + k_tile * K_t + local_k, + out_val, + ) + + +@triton.jit +def _marlin_dequant_fp8_kernel( + QW, + SC, + PT, + SCATTER, + CB, + OUT, + qw_stride_c: tl.constexpr, + sc_stride_c: tl.constexpr, + sc_stride_n: tl.constexpr, + out_stride_c: tl.constexpr, + out_stride_n: tl.constexpr, + N_TILES: tl.constexpr, + N_t: tl.constexpr, + K_t: tl.constexpr, + TILE_SIZE: tl.constexpr, +): + c_id = tl.program_id(0) + n_tile = tl.program_id(1) + k_tile = tl.program_id(2) + + local_idx = tl.arange(0, TILE_SIZE) + local_n = local_idx // K_t + local_k = local_idx % K_t + + local_marlin_pos = tl.load(SCATTER + local_n * K_t + local_k) + tile_off = (k_tile * N_TILES + n_tile) * TILE_SIZE + nib_pos = tile_off + local_marlin_pos + + words = tl.load(QW + c_id * qw_stride_c + nib_pos // 8) + nibs = (words >> ((nib_pos % 8) * 4)) & 0xF + + pt = tl.load(PT + c_id) + global_n = n_tile * N_t + local_n + sc = tl.load(SC + c_id * sc_stride_c + global_n * sc_stride_n + k_tile).to( + tl.float32 + ) + cb_val = tl.load(CB + nibs) + + out_val = (cb_val * sc * pt).to(tl.float8e4nv) + tl.store( + OUT + c_id * out_stride_c + global_n * out_stride_n + k_tile * K_t + local_k, + out_val, + ) + + +def marlin_dequant_bf16(qw_flat, orig_scale, pt, scatter_lut, cb, N, K, C): + """Dequantize marlin int32 weight -> bf16 using the original NVFP4 block scales. + + qw_flat: [C, K*N//8] int32 (marlin flat layout) + orig_scale: [C, N, K//16] fp8_e4m3 (original block scales, NOT marlin-processed) + pt: [C] float32 per-tensor scale + scatter_lut: [1024] int32 base scatter LUT (from _build_base_scatter) + cb: [16] float32 fp4 codebook + Returns: [C, N, K] bfloat16""" + import torch + + out = torch.empty(C, N, K, device=qw_flat.device, dtype=torch.bfloat16) + N_t, K_t = 64, 16 + N_TILES = N // N_t + grid = (C, N_TILES, K // K_t) + _marlin_dequant_bf16_kernel[grid]( + qw_flat, + orig_scale, + pt, + scatter_lut, + cb, + out, + int(qw_flat.stride(0)), + int(orig_scale.stride(0)), + int(orig_scale.stride(1)), + int(out.stride(0)), + int(out.stride(1)), + N_TILES, + N_t, + K_t, + N_t * K_t, + ) + return out + + +def marlin_dequant_fp8(qw_flat, orig_scale, pt, scatter_lut, cb, N, K, C): + """Dequantize marlin int32 weight -> fp8_e4m3 using the original NVFP4 block scales. + + Same signature as marlin_dequant_bf16; output dtype is float8_e4m3fn.""" + import torch + + out = torch.empty(C, N, K, device=qw_flat.device, dtype=torch.float8_e4m3fn) + N_t, K_t = 64, 16 + N_TILES = N // N_t + grid = (C, N_TILES, K // K_t) + _marlin_dequant_fp8_kernel[grid]( + qw_flat, + orig_scale, + pt, + scatter_lut, + cb, + out, + int(qw_flat.stride(0)), + int(orig_scale.stride(0)), + int(orig_scale.stride(1)), + int(out.stride(0)), + int(out.stride(1)), + N_TILES, + N_t, + K_t, + N_t * K_t, + ) + return out diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/nonexpert_linear.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/nonexpert_linear.py new file mode 100644 index 0000000000..d6163a41a6 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/nonexpert_linear.py @@ -0,0 +1,120 @@ +"""Frozen NVFP4 W4A16 dense ``Linear`` via the Marlin grouped kernel (single expert). + +Non-expert quantization: swap a frozen ``nn.Linear`` for 4-bit NVFP4 weights that run through the +SAME validated Marlin W4A16 kernel the grouped MoE experts use — 4-bit weight memory + bf16 tensor- +core compute on any sm80+ GPU (no FP4 hardware needed). A dense Linear is just the grouped GEMM with +one expert (E=1): all rows route to expert 0, M padded up to the Marlin block (64). + +Weights are quantized once (at swap time, before FSDP wrap) via torchao's two-level NVFP4 quant and +prepped into Marlin layout eagerly; the bf16 weight is then dropped. The module is intentionally NOT +an ``nn.Linear`` subclass so PEFT leaves it frozen (non-experts carry no LoRA). +""" + +from __future__ import annotations + +import torch +import torch.nn as nn + +from . import load_ext +from .prep import ( + marlin_make_workspace_new, + marlin_moe_gemm, + prepare_nvfp4_weight_for_marlin, +) + +_BSM = 64 # Marlin MoE block size; M is padded to a multiple of this + + +class MarlinW4A16Linear(nn.Module): + """Frozen NVFP4 W4A16 replacement for ``nn.Linear`` (bf16 act/out, 4-bit weight).""" + + def __init__(self, weight: torch.Tensor, bias: torch.Tensor | None): + super().__init__() + from torchao.prototype.mx_formats.nvfp4_tensor import NVFP4Tensor + + self.out_features, self.in_features = weight.shape + # The Marlin repack is CUDA-only; non-expert weights may still be on CPU at quant time. + if weight.device.type != "cuda": + weight = weight.to("cuda") + dev = weight.device + ext = load_ext() + + # Two-level NVFP4 quant (per-tensor fp32 + per-16 e4m3 block scales), one [1,N,K] expert. + # Per-tensor scale needed for accuracy (rel err ~0.10 two-level vs ~0.25 without): + # pts = amax / (F4_E2M1_MAX * F8E4M3_MAX) = amax / (6 * 448). + w_bf16 = weight.to(torch.bfloat16) + pts = (w_bf16.abs().max() / (6.0 * 448.0)).reshape(1).float() + nv = NVFP4Tensor.to_nvfp4( + w_bf16[None], per_tensor_scale=pts, is_swizzled_scales=False + ) + gscale = nv.per_tensor_scale + qw, sc, g = prepare_nvfp4_weight_for_marlin( + nv.qdata, + nv.scale, + gscale, + self.out_features, + self.in_features, + torch.bfloat16, + ext.gptq_marlin_repack, + ) + # frozen -> buffers (move with .to()/.cuda(); FSDP replicates non-experts) + self.register_buffer("qweight", qw) # [1, ...] int32 marlin layout + self.register_buffer("scales", sc) # [1, ...] bf16 + self.register_buffer("gscale", g) # [1] fp32 + self.register_buffer( + "bias", bias.to(torch.bfloat16) if bias is not None else None + ) + # Device-specific scratch; persistent=False keeps it out of state_dict/checkpoints. + self.register_buffer( + "_workspace", marlin_make_workspace_new(dev, 4), persistent=False + ) + self._route_cache: dict[tuple[int, torch.device], tuple] = {} + + def _route(self, Mt: int, dev: torch.device): + key = (Mt, dev) + cached = self._route_cache.get(key) + if cached is None: + si = torch.arange(Mt, dtype=torch.int32, device=dev) + ei = torch.zeros( + Mt // _BSM, dtype=torch.int32, device=dev + ) # all -> expert 0 + ntpp = torch.tensor([Mt], dtype=torch.int32, device=dev) + tw = torch.ones(Mt, 1, dtype=torch.float32, device=dev) + cached = (si, ei, ntpp, tw) + self._route_cache[key] = cached + return cached + + def forward(self, x: torch.Tensor) -> torch.Tensor: + ext = load_ext() + orig = x.shape + xf = x.reshape(-1, self.in_features).to(torch.bfloat16) + M = xf.shape[0] + Mt = (M + _BSM - 1) // _BSM * _BSM + if Mt != M: + xf = torch.nn.functional.pad(xf, (0, 0, 0, Mt - M)) + si, ei, ntpp, tw = self._route(Mt, xf.device) + out = marlin_moe_gemm( + ext, + xf, + self.qweight, + self.scales, + self.gscale, + self._workspace, + si, + ei, + ntpp, + tw, + _BSM, + 1, + False, + Mt, + self.out_features, + self.in_features, + ) + out = out[:M] + if self.bias is not None: + out = out + self.bias + return out.reshape(*orig[:-1], self.out_features) + + def extra_repr(self) -> str: + return f"in_features={self.in_features}, out_features={self.out_features}, nvfp4=W4A16-marlin" diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/prep.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/prep.py new file mode 100644 index 0000000000..c0e4d77101 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/marlin_w4a16/prep.py @@ -0,0 +1,180 @@ +"""vLLM-FREE NVFP4 -> Marlin weight prep. Pure-torch scale processing (copied verbatim from +vLLM marlin_utils.py / marlin_utils_fp4.py so it is bit-identical) + the standalone CUDA +gptq_marlin_repack (ported, bit-exact to vLLM). Produces (b_qweight int32, b_scales fp8_e4m3, +global_scale fp32) ready for the standalone moe_wna16_marlin_gemm. + +One function does ONE weight tensor [E, size_n, size_k//2] (gate_up fused or down). The training +integration calls it 4x: gate_up + down (forward), and their transposes (backward dX). +""" + +import torch + + +# Copied verbatim from vllm marlin_utils.py (keep bit-identical). +def _num_compute_units(idx): + return torch.cuda.get_device_properties(idx).multi_processor_count + + +def marlin_make_workspace_new(device, max_blocks_per_sm=1): + sms = _num_compute_units(device.index) + return torch.zeros( + sms * max_blocks_per_sm, dtype=torch.int, device=device, requires_grad=False + ) + + +def get_scale_perms(): + scale_perm = [] + for i in range(8): + scale_perm.extend([i + 8 * j for j in range(8)]) + scale_perm_single = [] + for i in range(4): + scale_perm_single.extend([2 * i + j for j in [0, 1, 8, 9, 16, 17, 24, 25]]) + return scale_perm, scale_perm_single + + +def marlin_permute_scales(s, size_k, size_n, group_size, is_a_8bit=False): + scale_perm, scale_perm_single = get_scale_perms() + if group_size < size_k and group_size != -1 and not is_a_8bit: + s = s.reshape((-1, len(scale_perm)))[:, scale_perm] + else: + s = s.reshape((-1, len(scale_perm_single)))[:, scale_perm_single] + s = s.reshape((-1, size_n)).contiguous() + return s + + +# Copied verbatim from vllm marlin_utils_fp4.py (keep bit-identical). +def _nvfp4_compute_scale_factor(marlin_scales, a_dtype=None): + if a_dtype is not None and a_dtype == torch.half: + return 1.0 + ws_float = marlin_scales.float() * (2**7) + nonzero_mask = ws_float > 0 + if nonzero_mask.any(): + max_val = ws_float[nonzero_mask].max() + if max_val < 448 * (2**7): + sf = (448 * (2**7) / max_val).log2().floor().exp2() + return sf.item() + return 1.0 + + +def nvfp4_marlin_process_scales(marlin_scales, scale_factor=None, a_dtype=None): + marlin_scales = marlin_scales.to(torch.half) + marlin_scales = marlin_scales.view(-1, 4)[:, [0, 2, 1, 3]].view( + marlin_scales.size(0), -1 + ) + if scale_factor is None: + scale_factor = _nvfp4_compute_scale_factor(marlin_scales, a_dtype) + if scale_factor > 1.0: + marlin_scales = (marlin_scales.float() * scale_factor).to(torch.half) + marlin_scales = marlin_scales * (2**7) + marlin_scales[marlin_scales < 2] = 0 + marlin_scales = marlin_scales.view(torch.int16) << 1 + marlin_scales = marlin_scales.view(torch.float8_e4m3fn) + marlin_scales = marlin_scales[:, 1::2].contiguous() + return marlin_scales, scale_factor + + +def nvfp4_marlin_process_global_scale(global_scale, a_dtype=None): + if a_dtype is None: + a_dtype = global_scale.dtype + assert a_dtype in [torch.half, torch.bfloat16] + fp4_exponent = 2 + target_exponent = 5 if a_dtype == torch.half else 8 + exponent_bias = 2 ** (target_exponent - 1) - 2 ** (fp4_exponent - 1) + return global_scale * (2.0 ** (exponent_bias - 7)) + + +# vllm::ScalarType float4_e2m1f.id (stable packed enum id). +FLOAT4_E2M1F_ID = 562949953487106 + + +def marlin_moe_gemm( + ext, + a, + b_qweight, + b_scales, + global_scale, + workspace, + sorted_token_ids, + expert_ids, + num_tokens_past_padded, + topk_weights, + moe_block_size, + top_k, + mul_topk_weights, + size_m, + size_n, + size_k, + out=None, +): + """Thin vLLM-free call into the standalone moe_wna16_marlin_gemm (NVFP4 W4A16, bf16 act/out).""" + if out is None: + out = torch.empty((size_m * top_k, size_n), device=a.device, dtype=a.dtype) + return ext.moe_wna16_marlin_gemm( + a, + out, + b_qweight, + None, + b_scales, + None, + global_scale, + None, + None, + None, + workspace, + sorted_token_ids, + expert_ids, + num_tokens_past_padded, + topk_weights, + moe_block_size, + top_k, + mul_topk_weights, + FLOAT4_E2M1F_ID, + size_m, + size_n, + size_k, + True, + False, + True, + False, + -1, + -1, + -1, + ) + + +GROUP_SIZE = 16 + + +def prepare_nvfp4_weight_for_marlin( + qdata, block_scale, global_scale, size_n, size_k, param_dtype, repack_fn +): + """qdata[E,size_n,size_k//2] uint8, block_scale[E,size_n,size_k//16] e4m3, + global_scale scalar/[E] fp32. Returns (qw[E,...] int32, scales[E,...] fp8, gscale fp32).""" + E = qdata.size(0) + dev = qdata.device + perm = torch.empty(0, dtype=torch.int, device=dev) + + qw = [] + for i in range(E): + qw_i = qdata[i].view(torch.int32).T.contiguous() # [size_k//8, size_n] + qw.append(repack_fn(qw_i, perm, size_k, size_n, 4, False)) + qw = torch.stack(qw, 0) + + # Shared scale_factor across experts, then per-expert permute+process. + scales_p = block_scale.to(param_dtype) + csf = _nvfp4_compute_scale_factor(scales_p, param_dtype) + sc = [] + for i in range(E): + s = scales_p[i].T # [size_k//16, size_n] + ms = marlin_permute_scales( + s, size_k=size_k, size_n=size_n, group_size=GROUP_SIZE + ) + ms, _ = nvfp4_marlin_process_scales(ms, scale_factor=csf, a_dtype=param_dtype) + sc.append(ms) + sc = torch.stack(sc, 0) + + g = ( + nvfp4_marlin_process_global_scale(global_scale.to(torch.float32), param_dtype) + / csf + ) + return qw, sc, g.float() diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/mx_weights.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/mx_weights.py index f4ccf96d46..f2cfd0b3bb 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/mx_weights.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/mx_weights.py @@ -30,8 +30,7 @@ MX_BLOCK_SIZE = 32 -# Standard OCP-MX fp4 e2m1 codebook (sign bit | 2-bit exp | 1-bit mantissa). -# Index by the raw 4-bit nibble. Cached fp32 tensor for kernel lookups. +# Standard OCP-MX fp4 e2m1 codebook (sign | 2-bit exp | 1-bit mantissa), indexed by raw nibble. _FP4_E2M1_LUT = ( 0.0, 0.5, @@ -209,10 +208,14 @@ def selective_mx_weights_fwd(mx_param, active_experts: torch.Tensor) -> MXWeight assert mx_param.elem_dtype == torch.float4_e2m1fn_x2, ( "only MXFP4 (float4_e2m1fn_x2) is supported" ) - sub_qdata = _mx_qdata(mx_param)[active_experts].contiguous() - sub_scale = _mx_scale(mx_param)[active_experts].contiguous() - # Logical dims (kernel's K, N): the contraction axis is K, the OCP block - # axis is the LAST storage axis (= K). N is the leading non-expert axis. + # Dense routing: reference the resident param zero-copy (see selective_nvfp4_weights_fwd); + # fall back to the gather when sparse. + qd, sc = _mx_qdata(mx_param), _mx_scale(mx_param) + all_active = active_experts.numel() == qd.size(0) + sub_qdata = qd if all_active else qd[active_experts].contiguous() + sub_scale = sc if all_active else sc[active_experts].contiguous() + # Kernel's K = contraction axis = OCP block axis = LAST storage axis; N is the leading + # non-expert axis. N = sub_qdata.size(1) K = sub_qdata.size(2) * 2 return MXWeights( @@ -257,11 +260,25 @@ def selective_nvfp4_weights_fwd(nv_param, active_experts: torch.Tensor) -> MXWei assert nv_param.block_size == 16, ( f"NVFP4 block_size must be 16, got {nv_param.block_size}" ) - sub_qdata = nv_param.qdata[active_experts].contiguous() # [a, N, K/2] uint8 - block_scale = nv_param.scale[active_experts].to(torch.float32) # [a, N, K/16] + # Dense routing (all experts active, the common case): remap is identity, so reference the + # resident param zero-copy instead of gathering the whole packed weight every layer. Falls + # back to the selective gather when routing is sparse. + all_active = active_experts.numel() == nv_param.qdata.size(0) + sub_qdata = ( + nv_param.qdata if all_active else nv_param.qdata[active_experts].contiguous() + ) + raw_scale = nv_param.scale if all_active else nv_param.scale[active_experts] + block_scale = raw_scale.to(torch.float32) # [a, N, K/16] per_tensor = getattr(nv_param, "per_tensor_scale", None) if per_tensor is not None: - block_scale = block_scale * per_tensor.to(torch.float32) + per_tensor = per_tensor.to(torch.float32) + # Per-expert scale reshaped to [a,1,1] to broadcast along block_scale's expert dim + # [a,N,K/16] (bare [a] would hit the last dim). A shared scalar just broadcasts. + if per_tensor.dim() >= 1 and per_tensor.size(0) == nv_param.qdata.size(0): + if not all_active: + per_tensor = per_tensor[active_experts] + per_tensor = per_tensor.reshape(-1, 1, 1) + block_scale = block_scale * per_tensor N = sub_qdata.size(1) K = sub_qdata.size(2) * 2 return MXWeights( diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_fp8_quantizer.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_fp8_quantizer.py new file mode 100644 index 0000000000..25752d8ec4 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_fp8_quantizer.py @@ -0,0 +1,495 @@ +"""Clean NVFP4-MoE loading for DeepSeek-V4-Flash-NVFP4 via a FineGrainedFP8 subclass. + +The checkpoint declares ``quant_method: fp8`` (→ ``FineGrainedFP8HfQuantizer``) with +``quant_algo: MIXED_PRECISION`` — the non-expert weights are blockwise FP8 ``[128,128]``, +but the routed experts are NVFP4 (``moe_quant_algo: NVFP4``, ``group_size: 16``, +``fmt: e4m3``). ``FP8Experts`` has an FP4 path (``expert_dtype == "fp4"``) but allocates a +blockwise-FP8 ``*_scale_inv`` buffer at the *wrong* granularity (``sf_gran_k=32`` + the +model-global ``scale_fmt: ue8m0``); it has no notion of NVFP4's group-16 ``*_scale`` +(E4M3) + per-tensor ``*_scale_2``. + +The deepseek_v4 weight converter fuses ``experts.*.w1/w3.weight`` → ``gate_up_proj`` with +*unanchored* source patterns (``mlp.experts.*.w1.weight``), so by regex search they also +match the sibling ``.weight_scale`` / ``.weight_scale_2`` keys. The group-16 ``weight_scale`` +(2-D) fuses fine, but ``weight_scale_2`` is a 0-D scalar — ``Concatenate`` on stacked +scalars raises (a spurious conversion error) — and in any case there's no ``*_scale`` / +``*_scale_2`` param to receive the fused scales, so they come in UNEXPECTED and the +``*_scale_inv`` placeholders end up MISSING (random init → invalid). + +This subclass loads everything in place, with no checkpoint re-read of weights/scales: + * ``update_weight_conversions`` — anchors the experts' ``*.weight`` converters with ``$`` + (so the scalar-scale concat never runs) and adds twin ``*.weight_scale`` converters so + the 2-D group-16 scales fuse into ``gate_up_proj_scale`` / ``down_proj_scale``; + * ``_process_model_before_weight_loading`` — swaps the wrong ``*_scale_inv`` placeholders + for correctly-shaped NVFP4 ``*_scale`` params, and marks the 0-D ``*_scale_2`` / + dynamic ``*.input_scale`` keys ignorable; + * ``_process_model_after_weight_loading`` — folds the in-place qdata + ``*_scale`` plus + the per-expert scalar ``*_scale_2`` (the only thing re-read — 4 bytes/expert) into a + torchao ``NVFP4Tensor`` for the scattermoe fused-LoRA path. + +Net: no UNEXPECTED/MISSING warning, no random init, correct NVFP4 experts, no multi-GB +re-read. Installed by swapping it into ``AUTO_QUANTIZER_MAPPING["fp8"]`` before +``from_pretrained``. +""" + +from __future__ import annotations + +import os + +import torch +import torch.nn as nn + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +_GROUP_SIZE = 16 + +# How to store the blockwise-FP8 non-expert linears after load (set from the +# `dsv4_fp8_nonexpert_mode` config via `configure_nonexpert_mode`): +# "float8tensor" (default): wrap each plain FP8Linear weight as a torchao Float8Tensor +# (1-byte qdata + block scale). Forward/backward + PEFT LoRA run via subclass autograd; +# the fused LoRA kernels dequant it through axolotl.kernels.quantize.dequantize. +# "bf16": dequantize to bf16 in place (safe path; 2 bytes/param). Also sidesteps transformers' +# @triton_op FP8 matmul (no autograd formula; slow discovery). +# Grouped linears (o_a_proj) always go to bf16: their view+bmm forward isn't subclass-safe. +_FP8_NONEXPERT_MODE = "float8tensor" + + +def configure_nonexpert_mode(mode: str | None) -> None: + """Set the FP8 non-expert storage mode from cfg before the model loads (the quantizer + reads the module global in ``_process_model_after_weight_loading``).""" + global _FP8_NONEXPERT_MODE + _FP8_NONEXPERT_MODE = (mode or "float8tensor").lower() + + +# The only per-expert keys left unmatched once weight + weight_scale fuse in place: the 0-D +# per-tensor `weight_scale_2` (folded after load) and the unused dynamic `input_scale`. +_IGNORE_UNEXPECTED = ( + r"experts\..*\.weight_scale_2$", + r"experts\..*\.input_scale$", +) + + +def _nvfp4_cls(): + try: + from torchao.prototype.mx_formats.nvfp4_tensor import NVFP4Tensor + + return NVFP4Tensor + except ImportError: + return None + + +def _float8_cls(): + try: + from torchao.quantization import Float8Tensor + + return Float8Tensor + except ImportError: + return None + + +def _drop_param(mod: nn.Module, name: str) -> None: + if name in mod._parameters: + del mod._parameters[name] + else: + setattr(mod, name, None) + + +def _is_nvfp4_experts(module: nn.Module) -> bool: + """An ``FP8Experts`` carrying FP4-packed (int8/uint8, K-halved) expert weights.""" + try: + from transformers.integrations.finegrained_fp8 import FP8Experts + except ImportError: + return False + if not isinstance(module, FP8Experts): + return False + w = getattr(module, "gate_up_proj", getattr(module, "up_proj", None)) + return ( + isinstance(w, torch.Tensor) + and w.dtype in (torch.int8, torch.uint8) + and w.ndim == 3 + ) + + +def _is_expert_weight_converter(conv) -> bool: + pats = getattr(conv, "_original_source_patterns", None) or [] + return any("experts" in p and p.endswith(".weight") for p in pats) + + +def _resolve_repo_file(repo: str, filename: str) -> str: + """Resolve a checkpoint file path from a local snapshot dir or the HF hub. + + A local snapshot dir (offline/air-gapped axolotl usage) would fail ``hf_hub_download`` + with an ``HFValidationError``, so read straight from disk in that case. + """ + if os.path.isdir(repo): + return os.path.join(repo, filename) + from huggingface_hub import hf_hub_download + + return hf_hub_download(repo, filename) + + +def _scale_2_path(repo: str): + """Return ``(weight_map, opener)`` for re-reading per-expert scalar ``weight_scale_2``.""" + import json + + from safetensors import safe_open + + with open(_resolve_repo_file(repo, "model.safetensors.index.json")) as _f: + wmap = json.load(_f)["weight_map"] + cache: dict[str, object] = {} + + def opener(shard): + f = cache.get(shard) + if f is None: + f = cache[shard] = safe_open( + _resolve_repo_file(repo, shard), framework="pt" + ) + return f + + return wmap, opener + + +def _dequantize_fp8_linears(model: nn.Module, quantizer) -> int: + """Dequantize blockwise-FP8 linear weights to bf16 IN PLACE (preserving each module's + class + custom forward, e.g. the grouped ``o_a_proj``). A module is targeted by the + presence of an fp8/int8 ``weight`` + a ``weight_scale_inv`` companion — not by class — + so ``FP8Linear`` and any quantized ``nn.Linear`` subclass are both handled. After this, + ``FP8Linear.forward`` auto-uses plain ``F.linear`` (weight.element_size() > 1), and the + FP8 matmul custom op (no autograd formula; slow triton kernel discovery) is gone.""" + from transformers.integrations.finegrained_fp8 import Fp8Dequantize + + deq = Fp8Dequantize(quantizer) + n = 0 + by_type: dict[str, int] = {} + for name, mod in model.named_modules(): + wsi = getattr(mod, "weight_scale_inv", None) + w = getattr(mod, "weight", None) + if ( + wsi is None + or not isinstance(w, torch.Tensor) + or w.dtype not in (torch.float8_e4m3fn, torch.int8) + ): + continue + w_bf16 = deq._dequantize_one(w.data, wsi.data, torch.bfloat16) + if tuple(w_bf16.shape) != tuple(w.shape): + # FP4-packed (int8, half-K) unpacks to 2x width (expected); otherwise log. + LOG.warning( + "dequant %s: weight %s -> %s (dtype %s)", + name, + tuple(w.shape), + tuple(w_bf16.shape), + w.dtype, + ) + mod.weight = nn.Parameter(w_bf16, requires_grad=False) + if "weight_scale_inv" in mod._parameters: + del mod._parameters["weight_scale_inv"] + else: + mod.weight_scale_inv = None + by_type[type(mod).__name__] = by_type.get(type(mod).__name__, 0) + 1 + n += 1 + if n: + LOG.info("Dequantized %d FP8 linear weights to bf16 in place: %s", n, by_type) + return n + + +def _wrap_fp8_linears_as_float8tensor(model: nn.Module, quantizer) -> int: + """Wrap blockwise-FP8 *plain* linear weights as torchao ``Float8Tensor`` (1-byte qdata + + block scale) so the frozen base keeps the FP8 memory footprint while forward/backward and + PEFT LoRA run through the tensor subclass. The checkpoint's exact qdata + ``weight_scale_inv`` + are reused (no re-quant). Grouped linears (``o_a_proj``), whose ``view+bmm`` forward isn't + subclass-safe, are dequantized to bf16 in place instead.""" + Float8Tensor = _float8_cls() + if Float8Tensor is None: + LOG.warning( + "torchao Float8Tensor unavailable; dequantizing FP8 non-experts to bf16" + ) + return _dequantize_fp8_linears(model, quantizer) + from transformers.integrations.finegrained_fp8 import ( + Fp8Dequantize, + FP8GroupedLinear, + ) + + deq = Fp8Dequantize(quantizer) + wrapped = bf16ed = 0 + for _name, mod in model.named_modules(): + wsi = getattr(mod, "weight_scale_inv", None) + w = getattr(mod, "weight", None) + if ( + wsi is None + or not isinstance(w, torch.Tensor) + or w.dtype != torch.float8_e4m3fn + ): + continue + if isinstance(mod, FP8GroupedLinear) or (getattr(mod, "n_groups", 1) or 1) > 1: + mod.weight = nn.Parameter( + deq._dequantize_one(w.data, wsi.data, torch.bfloat16), + requires_grad=False, + ) + _drop_param(mod, "weight_scale_inv") + bf16ed += 1 + continue + block = list(mod.block_size) if getattr(mod, "block_size", None) else [128, 128] + scale = wsi.data + if scale.dtype not in (torch.float32, torch.float16, torch.bfloat16): + scale = scale.to( + torch.float32 + ) # ue8m0 scales have no float ops; torchao wants float + f8 = Float8Tensor(w.data, scale, block_size=block, dtype=torch.bfloat16) + mod.weight = nn.Parameter(f8, requires_grad=False) + _drop_param(mod, "weight_scale_inv") + wrapped += 1 + if wrapped or bf16ed: + LOG.info( + "FP8 non-experts: wrapped %d plain linears as Float8Tensor (1-byte), %d grouped -> bf16", + wrapped, + bf16ed, + ) + return wrapped + bf16ed + + +def _enable_torchao_lora_dispatch(quantizer) -> None: + """Let PEFT attach LoRA to the Float8Tensor non-expert bases. + + PEFT's ``dispatch_torchao`` fires because the base weights are torchao ``Float8Tensor`` + and routes to ``TorchaoLoraLinear``, which needs + ``model.hf_quantizer.quantization_config.get_apply_tensor_subclass``. PEFT sources it via + ``operator.attrgetter`` and silently skips on ``AttributeError`` (we ship a FineGrained-FP8 + config, not a ``TorchAoConfig``), but the dispatcher then errors on the missing kwarg. + It's used ONLY by merge/unmerge (re-quantize the merged weight) — never in training (frozen + base, LoRA kept separate) — so provide a callable that unblocks training and fails loudly + on merge (blockwise-FP8 merge isn't supported yet).""" + cfg = getattr(quantizer, "quantization_config", None) + if cfg is None: + return + cls = type(cfg) + if hasattr(cls, "get_apply_tensor_subclass"): + return + + def _no_merge(): + raise NotImplementedError( + "merge-lora on a blockwise-FP8 (Float8Tensor) base is not supported; train/serve " + "with the adapter kept separate, or set `dsv4_fp8_nonexpert_mode: bf16` to merge." + ) + + # Attach to the CLASS as a staticmethod (not the instance): PEFT's attrgetter still resolves + # it via attribute lookup, but it stays out of the instance __dict__ so `config.to_json` + # (model.config save) doesn't try to serialize a function object. + try: + cls.get_apply_tensor_subclass = staticmethod(_no_merge) + except Exception: # pragma: no cover - some configs are frozen/slotted + LOG.warning( + "Could not attach get_apply_tensor_subclass; non-expert LoRA may fail to inject" + ) + + +def make_nvfp4_fp8_quantizer(): + """Build the ``FineGrainedFP8HfQuantizer`` subclass (deferred import).""" + from transformers.core_model_loading import WeightConverter + from transformers.quantizers.quantizer_finegrained_fp8 import ( + FineGrainedFP8HfQuantizer, + ) + + class NVFP4MoEFP8HfQuantizer(FineGrainedFP8HfQuantizer): + """FineGrained-FP8 quantizer that also loads NVFP4 routed experts correctly.""" + + def update_weight_conversions(self, weight_conversions): + rebuilt = [] + for conv in weight_conversions: + if not ( + isinstance(conv, WeightConverter) + and _is_expert_weight_converter(conv) + ): + rebuilt.append(conv) + continue + ops = [type(op)(op.dim) for op in conv.operations] + # weight: anchor `.weight$` so the converter fuses ONLY the packed weight + # and stops swallowing `.weight_scale*` (the 0-D `_scale_2` concat raises). + w_conv = WeightConverter( + source_patterns=[ + p + "$" if p.endswith(".weight") else p + for p in conv._original_source_patterns + ], + target_patterns=list(conv._original_target_patterns), + operations=ops, + ) + # weight_scale: twin converter so the 2-D group-16 scales fuse the same way + # into `*_scale`, loading in place (no re-read). + s_conv = WeightConverter( + source_patterns=[ + p[: -len(".weight")] + ".weight_scale$" + if p.endswith(".weight") + else p + for p in conv._original_source_patterns + ], + target_patterns=[ + t + "_scale" for t in conv._original_target_patterns + ], + operations=[type(op)(op.dim) for op in conv.operations], + ) + for new in (w_conv, s_conv): + new.scope_prefix = conv.scope_prefix + new.base_model_prefix = conv.base_model_prefix + rebuilt.extend((w_conv, s_conv)) + return super().update_weight_conversions(rebuilt) + + def _process_model_before_weight_loading(self, model, **kwargs): + super()._process_model_before_weight_loading(model, **kwargs) + n = 0 + for mod in model.modules(): + if not _is_nvfp4_experts(mod): + continue + E, H, I = mod.num_experts, mod.hidden_dim, mod.intermediate_dim + dev = (mod.gate_up_proj if mod.has_gate else mod.up_proj).device + # drop the wrong-granularity blockwise-FP8 placeholders; register NVFP4 + # group-16 E4M3 `*_scale` params so the fused scales land in place. + pfx, out_dim = ( + ("gate_up_proj", 2 * I) if mod.has_gate else ("up_proj", I) + ) + for stale in (pfx + "_scale_inv", "down_proj_scale_inv"): + mod._parameters.pop(stale, None) + mod.register_parameter( + pfx + "_scale", + nn.Parameter( + torch.empty( + E, + out_dim, + H // _GROUP_SIZE, + dtype=torch.float8_e4m3fn, + device=dev, + ), + requires_grad=False, + ), + ) + mod.register_parameter( + "down_proj_scale", + nn.Parameter( + torch.empty( + E, + H, + I // _GROUP_SIZE, + dtype=torch.float8_e4m3fn, + device=dev, + ), + requires_grad=False, + ), + ) + n += 1 + if n: + ignore = set(model._keys_to_ignore_on_load_unexpected or ()) + ignore.update(_IGNORE_UNEXPECTED) + model._keys_to_ignore_on_load_unexpected = ignore + LOG.info( + "Prepared %d NVFP4 experts modules for clean in-place loading", n + ) + + def _process_model_after_weight_loading(self, model, **kwargs): + super()._process_model_after_weight_loading(model, **kwargs) + # The swap into AUTO_QUANTIZER_MAPPING['fp8'] is process-global, so this runs for EVERY + # fp8 checkpoint loaded after install. Gate all NVFP4-specific work on actually finding + # NVFP4 experts; a plain fp8 model behaves exactly like the original quantizer (super() + # above) and is NOT re-wrapped as Float8Tensor. + mods = [ + (name, m) for name, m in model.named_modules() if _is_nvfp4_experts(m) + ] + if not mods: + return + if _FP8_NONEXPERT_MODE == "bf16": + n = _dequantize_fp8_linears(model, self) + if n: + LOG.info("Dequantized %d non-expert FP8Linear modules to bf16", n) + else: + n = _wrap_fp8_linears_as_float8tensor(model, self) + if n: + _enable_torchao_lora_dispatch(self) + NVFP4Tensor = _nvfp4_cls() + if NVFP4Tensor is None: + LOG.warning("torchao NVFP4Tensor unavailable; skipping expert wrap") + return + repo = getattr(model, "name_or_path", None) or getattr( + model.config, "_name_or_path", None + ) + wmap, opener = _scale_2_path(repo) + import re + + for name, mod in mods: + layer = int(re.search(r"layers\.(\d+)\.", name).group(1)) + pfx = "gate_up_proj" if mod.has_gate else "up_proj" + projs = { + "gate_up_proj": ("w1",), + "up_proj": ("w1",), + "down_proj": ("w2",), + } + for proj in (pfx, "down_proj"): + qdata = getattr(mod, proj).data.view(torch.uint8) + scale = getattr(mod, proj + "_scale").data + # w1/w3 share weight_scale_2; one scalar per expert → [E,1,1] broadcast. + src = projs[proj][0] + s2 = ( + torch.stack( + [ + opener( + wmap[ + f"layers.{layer}.ffn.experts.{e}.{src}.weight_scale_2" + ] + ).get_tensor( + f"layers.{layer}.ffn.experts.{e}.{src}.weight_scale_2" + ) + for e in range(mod.num_experts) + ] + ) + .to(device=qdata.device, dtype=torch.float32) + .view(-1, 1, 1) + ) + setattr( + mod, + proj, + nn.Parameter( + NVFP4Tensor( + qdata, + scale, + _GROUP_SIZE, + torch.bfloat16, + per_tensor_scale=s2, + ), + requires_grad=False, + ), + ) + mod._parameters.pop(proj + "_scale", None) + LOG.info( + "Wrapped %d NVFP4 experts modules as NVFP4Tensor (in-place scales)", + len(mods), + ) + + return NVFP4MoEFP8HfQuantizer + + +_ORIGINAL_FP8_QUANTIZER = None + + +def install_nvfp4_fp8_quantizer() -> None: + """Swap the NVFP4-aware quantizer into ``AUTO_QUANTIZER_MAPPING["fp8"]`` so any + ``quant_method: fp8`` checkpoint with NVFP4 experts loads correctly. Idempotent.""" + global _ORIGINAL_FP8_QUANTIZER + from transformers.quantizers import auto as _auto + + cls = make_nvfp4_fp8_quantizer() + if getattr(_auto.AUTO_QUANTIZER_MAPPING.get("fp8"), "__name__", "") == cls.__name__: + return + _ORIGINAL_FP8_QUANTIZER = _auto.AUTO_QUANTIZER_MAPPING.get("fp8") + _auto.AUTO_QUANTIZER_MAPPING["fp8"] = cls + LOG.info("Installed NVFP4-aware FP8 quantizer into AUTO_QUANTIZER_MAPPING['fp8']") + + +def uninstall_nvfp4_fp8_quantizer() -> None: + """Restore the original ``AUTO_QUANTIZER_MAPPING["fp8"]`` entry the swap replaced (so a long-lived + process can return to stock fp8 loading). Idempotent; a no-op if install was never called.""" + global _ORIGINAL_FP8_QUANTIZER + if _ORIGINAL_FP8_QUANTIZER is None: + return + from transformers.quantizers import auto as _auto + + _auto.AUTO_QUANTIZER_MAPPING["fp8"] = _ORIGINAL_FP8_QUANTIZER + _ORIGINAL_FP8_QUANTIZER = None + LOG.info("Restored original FP8 quantizer in AUTO_QUANTIZER_MAPPING['fp8']") diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_fsdp.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_fsdp.py new file mode 100644 index 0000000000..f95923a14a --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_fsdp.py @@ -0,0 +1,214 @@ +"""FSDP2 support for torchao ``NVFP4Tensor`` (frozen, expert-sharded on dim 0). + +torchao's NVFP4Tensor (prototype) ships no FSDP2 hooks, so `fully_shard` on a module with +NVFP4 expert params fails: the flat-param sharding path calls ops the subclass doesn't +implement (``aten.split`` etc.). This adds: + + * ``aten.split`` / ``aten.clone`` / ``aten.detach`` / ``aten.copy_`` operating on the + inner ``(qdata, scale, per_tensor_scale)`` along dim 0 (the expert axis FSDP shards), + * ``fsdp_pre_all_gather`` / ``fsdp_post_all_gather`` so FSDP2 uses the subclass-extension + path (all-gathering the inner tensors and reconstructing the subclass) instead of the + flat-buffer ``view(-1)`` path, which a block-scaled FP4 layout can't satisfy. + +Idempotent. Sharding is restricted to dim 0 (the [E, N, K] expert axis) — block-axis +sharding is not supported (nor needed for expert-parallel/FSDP of MoE weights). +""" + +from __future__ import annotations + +import torch + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) +_PATCHED = False + + +def _nvfp4_cls(): + try: + from torchao.prototype.mx_formats.nvfp4_tensor import NVFP4Tensor + + return NVFP4Tensor + except ImportError: + return None + + +def _rebuild(ref, qdata, scale, per_tensor_scale): + NVFP4Tensor = type(ref) + return NVFP4Tensor( + qdata, + scale, + ref.block_size, + ref.orig_dtype, + per_tensor_scale, + ref.act_per_tensor_scale, + ref.is_swizzled_scales, + ref.use_triton_kernel, + ref.act_quant_kwargs, + ) + + +def patch_nvfp4_fsdp(): + global _PATCHED + if _PATCHED: + return + NVFP4Tensor = _nvfp4_cls() + if NVFP4Tensor is None: + return + aten = torch.ops.aten + implements = NVFP4Tensor.implements + + def _pts_along_dim0(x, n): + """Per-tensor scale handling under a dim-0 split: per-expert -> split, else share.""" + pts = x.per_tensor_scale + if pts is not None and pts.dim() >= 1 and pts.shape[0] == x.shape[0]: + return list(torch.split(pts, n, 0)) if isinstance(n, int) else None + return None + + @implements([aten.split.Tensor]) + def _split(func, types, args, kwargs): + x, split_size = args[0], args[1] + dim = args[2] if len(args) > 2 else kwargs.get("dim", 0) + if dim != 0: + raise NotImplementedError( + f"NVFP4Tensor FSDP split only on dim 0, got {dim}" + ) + qd = func(x.qdata, split_size, 0) + sc = func(x.scale, split_size, 0) + pts = _pts_along_dim0(x, split_size) + return [ + _rebuild(x, qd[i], sc[i], pts[i] if pts is not None else x.per_tensor_scale) + for i in range(len(qd)) + ] + + @implements([aten.clone.default, aten.detach.default]) + def _clone_detach(func, types, args, kwargs): + x = args[0] + return x._apply_fn_to_data(lambda t: func(t, **kwargs)) + + @implements([aten.new_zeros.default]) + def _new_zeros(func, types, args, kwargs): + # FSDP pads on dim 0 (experts). Scale may be SWIZZLED (its trailing dims are not + # K//block), so preserve qdata/scale trailing shapes and vary only dim 0. + x, size = args[0], list(args[1]) + E = size[0] + qd = func(x.qdata, [E, *x.qdata.shape[1:]], **kwargs) + sc = func(x.scale, [E, *x.scale.shape[1:]], **kwargs) + # Preserve a per-expert per_tensor_scale buffer (vary dim 0) so the subsequent + # copy_/all-gather can carry it; dropping it here loses it for the whole param. + pts = x.per_tensor_scale + if pts is not None and pts.dim() >= 1 and pts.shape[0] == x.shape[0]: + pts = func(pts, [E, *pts.shape[1:]], **kwargs) + return _rebuild(x, qd, sc, pts) + + @implements([aten.narrow.default]) + def _narrow(func, types, args, kwargs): + x, dim, start, length = args[0], args[1], args[2], args[3] + if dim != 0: + raise NotImplementedError(f"NVFP4 narrow only dim 0, got {dim}") + qd = func(x.qdata, 0, start, length) + sc = func(x.scale, 0, start, length) + pts = x.per_tensor_scale + if pts is not None and pts.dim() >= 1 and pts.shape[0] == x.shape[0]: + pts = func(pts, 0, start, length) + return _rebuild(x, qd, sc, pts) + + @implements([aten.view.default]) + def _view(func, types, args, kwargs): + x, size = args[0], list(args[1]) + if size == [-1]: + # FSDP's flat-buffer view. A 1-D NVFP4 is invalid (block layout needs >=2D), + # and this buffer is unused for subclass-extension params (the all-gather goes + # through fsdp_pre/post_all_gather), so return a valid 2-D [1, numel] NVFP4. + return _rebuild( + x, x.qdata.reshape(1, -1), x.scale.reshape(1, -1), x.per_tensor_scale + ) + K = size[-1] + qd = func(x.qdata, size[:-1] + [K // 2]) + sc = func(x.scale, size[:-1] + [K // x.block_size]) + return _rebuild(x, qd, sc, x.per_tensor_scale) + + @implements([aten.slice.Tensor]) + def _slice(func, types, args, kwargs): + x = args[0] + dim = args[1] if len(args) > 1 else 0 + if dim == 0: # expert-axis slice (FSDP shard); torchao only handles rank 2 + start = args[2] if len(args) > 2 else None + end = args[3] if len(args) > 3 else None + step = args[4] if len(args) > 4 else 1 + if step != 1: + raise NotImplementedError("NVFP4 slice step must be 1") + qd = func(x.qdata, 0, start, end, 1) + sc = func(x.scale, 0, start, end, 1) + pts = x.per_tensor_scale + if pts is not None and pts.dim() >= 1 and pts.shape[0] == x.shape[0]: + pts = func(pts, 0, start, end, 1) + return _rebuild(x, qd, sc, pts) + from torchao.prototype.mx_formats.nvfp4_tensor import nvfp4_slice + + return nvfp4_slice(func, types, args, kwargs) + + def _cstride(shape): + st = [1] * len(shape) + for i in range(len(shape) - 2, -1, -1): + st[i] = st[i + 1] * shape[i + 1] + return st + + @implements([aten.as_strided.default]) + def _as_strided(func, types, args, kwargs): + # FSDP reconstructs the contiguous unsharded param via as_strided(orig_size, + # contiguous_stride, 0). The reconstructed NVFP4 already has the right qdata/scale + # (incl. swizzled scale) shapes; as_strided them to their own contiguous shapes (a + # view op must return NEW tensors, so we can't return x as-is). + x = args[0] + qshape, sshape = list(x.qdata.shape), list(x.scale.shape) + qd = func(x.qdata, qshape, _cstride(qshape), 0) + sc = func(x.scale, sshape, _cstride(sshape), 0) + return _rebuild(x, qd, sc, x.per_tensor_scale) + + @implements([aten.copy_.default]) + def _copy_(func, types, args, kwargs): + dst, src = args[0], args[1] + func(dst.qdata, src.qdata) + func(dst.scale, src.scale) + if src.per_tensor_scale is not None: + if ( + dst.per_tensor_scale is not None + and dst.per_tensor_scale.shape == src.per_tensor_scale.shape + ): + func(dst.per_tensor_scale, src.per_tensor_scale) + else: + # dst lost its per_tensor buffer (e.g. a new_zeros without one); adopt src's. + dst.per_tensor_scale = src.per_tensor_scale.clone() + return dst + + # FSDP2 subclass-extension hooks avoid the flat-buffer view(-1) path. + def fsdp_pre_all_gather( + self, mesh, outer_size=None, outer_stride=None, module=None, mp_policy=None + ): + inputs = (self.qdata, self.scale) + if self.per_tensor_scale is not None: + inputs = inputs + (self.per_tensor_scale,) + meta = (self.per_tensor_scale is not None,) + return inputs, meta + + def fsdp_post_all_gather( + self, all_gather_outputs, metadata, param_dtype, *, out=None + ): + (has_pts,) = metadata + if has_pts: + qdata, scale, pts = all_gather_outputs + else: + qdata, scale = all_gather_outputs + pts = None + if out is not None: + # reconstruct in-place into the existing unsharded param + out.qdata, out.scale, out.per_tensor_scale = qdata, scale, pts + return + return _rebuild(self, qdata, scale, pts), all_gather_outputs + + NVFP4Tensor.fsdp_pre_all_gather = fsdp_pre_all_gather + NVFP4Tensor.fsdp_post_all_gather = fsdp_post_all_gather + + _PATCHED = True + LOG.info("Installed FSDP2 support (split + all-gather hooks) on NVFP4Tensor") diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_moe_loading.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_moe_loading.py new file mode 100644 index 0000000000..a457d7dbf4 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_moe_loading.py @@ -0,0 +1,242 @@ +"""Fix NVFP4 MoE-expert loading for DeepSeek-V4-Flash-NVFP4 (and similar checkpoints). + +transformers' finegrained_fp8 quantizer treats the model as FP8 blockwise and has NO +NVFP4 path, so for a MIXED_PRECISION checkpoint (FP8 elsewhere, ``moe_quant_algo: NVFP4`` +for the routed experts) it fuses the expert *weights* into ``gate_up_proj``/``down_proj`` +(uint8 qdata) correctly but drops the NVFP4 *scales* — the per-expert ``weight_scale`` +(E4M3 group-16 block scale) + ``weight_scale_2`` (per-tensor) come in UNEXPECTED and the +model's ``*_scale_inv`` placeholders are MISSING → randomly initialized → invalid model. + +This re-reads the per-expert ``weight_scale``/``weight_scale_2`` from the checkpoint, fuses +them the same way as the weights (gate_up = cat(w1, w3) on the 2*intermediate axis; down = +w2), and wraps the already-loaded fused qdata as a torchao ``NVFP4Tensor`` so the +scattermoe NVFP4 path dequantizes correctly. Self-contained until an upstream transformers +NVFP4-MoE path lands. +""" + +from __future__ import annotations + +import os +import re + +import torch +import torch.nn as nn + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def _nvfp4_cls(): + try: + from torchao.prototype.mx_formats.nvfp4_tensor import NVFP4Tensor + + return NVFP4Tensor + except ImportError: + return None + + +def _resolve_repo_file(repo_id: str, filename: str) -> str: + """Resolve a checkpoint file path from a local snapshot dir or the HF hub. + + A local snapshot dir (offline/air-gapped axolotl usage) would fail ``hf_hub_download`` + with an ``HFValidationError``, so read straight from disk in that case. + """ + if os.path.isdir(repo_id): + return os.path.join(repo_id, filename) + from huggingface_hub import hf_hub_download + + return hf_hub_download(repo_id, filename) + + +def _load_index(repo_id: str): + import json + + with open(_resolve_repo_file(repo_id, "model.safetensors.index.json")) as f: + return json.load(f)["weight_map"] + + +def _shard_open(repo_id: str, shard: str): + from safetensors import safe_open + + return safe_open(_resolve_repo_file(repo_id, shard), framework="pt") + + +# Per-architecture checkpoint naming for the unfused per-expert NVFP4 tensors. +# base_fmt formats with (layer, e, proj); gate_up/down are the proj names fused +# (gate_up = cat on the N/row axis in this order; down is single). +_NVFP4_MOE_SCHEMES = { + # DeepSeek-V4-Flash-NVFP4: w1=gate, w3=up, w2=down under ``layers.N.ffn.experts.M`` + "dsv4": { + "base_fmt": "layers.{layer}.ffn.experts.{e}.{proj}", + "gate_up": ("w1", "w3"), + "down": ("w2",), + }, + # Gemma-4-A4B-NVFP4: separate gate/up/down under ``model.language_model.layers.N.experts.M`` + "gemma4": { + "base_fmt": "model.language_model.layers.{layer}.experts.{e}.{proj}", + "gate_up": ("gate_proj", "up_proj"), + "down": ("down_proj",), + }, +} + + +def _detect_scheme(wmap): + """Pick the checkpoint naming scheme whose layer-0 expert-0 weight key is present.""" + for name, sch in _NVFP4_MOE_SCHEMES.items(): + probe = sch["base_fmt"].format(layer=0, e=0, proj=sch["gate_up"][0]) + ".weight" + if probe in wmap: + return name, sch + return None, None + + +def _build_expert_nvfp4(repo_id, wmap, base_fmt, layer, projs, n_experts, device): + """Rebuild fused qdata + E4M3 block scale + per-tensor scale for all experts of one + fused projection straight from the raw checkpoint (no dependence on transformers' own + fusion). Fused on the N (row) axis: qdata [E, sumN, K/2] uint8, scale [E, sumN, K/16] + E4M3, per_tensor scalar.""" + qd_proj, sc_proj, pts_list = [[] for _ in projs], [[] for _ in projs], [] + opened: dict[str, object] = {} + for e in range(n_experts): + for pi, proj in enumerate(projs): + base = base_fmt.format(layer=layer, e=e, proj=proj) + shard = wmap[f"{base}.weight"] + f = opened.get(shard) or opened.setdefault( + shard, _shard_open(repo_id, shard) + ) + qd_proj[pi].append(f.get_tensor(f"{base}.weight")) + sc_proj[pi].append(f.get_tensor(f"{base}.weight_scale")) + if pi == 0: # gate/up share weight_scale_2; one per expert + pts_list.append( + f.get_tensor(f"{base}.weight_scale_2").to(torch.float32) + ) + qdata = torch.cat([torch.stack(q, 0) for q in qd_proj], dim=1).to(device) + scale = torch.cat([torch.stack(s, 0) for s in sc_proj], dim=1).to(device) + # Per-expert weight_scale_2 stacked to [E,1,1], not expert-0's scalar broadcast to all experts. + pts = torch.stack(pts_list).view(-1, 1, 1).to(device) + return qdata, scale, pts + + +def attach_nvfp4_expert_scales(model: nn.Module, repo_id: str) -> int: + """Wrap each MoE experts module's fused gate_up_proj/down_proj as an NVFP4Tensor rebuilt from + the checkpoint's per-expert E4M3 scales. Handles both checkpoint layouts (DeepSeek-V4 ``ffn. + experts``/w1-w3-w2 with uint8 fused params from the FP8 quantizer, and Gemma-4 ``language_model. + layers.N.experts.M``/gate-up-down where transformers leaves the fused param a random bf16 + placeholder). Returns the number of expert modules fixed.""" + NVFP4Tensor = _nvfp4_cls() + if NVFP4Tensor is None: + return 0 + wmap = _load_index(repo_id) + scheme_name, scheme = _detect_scheme(wmap) + if scheme is None: + LOG.warning( + "attach_nvfp4_expert_scales: no known NVFP4 MoE naming scheme in %s", + repo_id, + ) + return 0 + base_fmt, projs_gu, projs_dn = scheme["base_fmt"], scheme["gate_up"], scheme["down"] + fixed = 0 + for name, mod in model.named_modules(): + # Match the fused experts param by shape (3D stacked experts), not dtype/class: DSV4's + # FP8 quantizer leaves it uint8, gemma4 leaves it a random bf16 placeholder. + gup = getattr(mod, "gate_up_proj", None) + dn = getattr(mod, "down_proj", None) + if not ( + isinstance(gup, torch.Tensor) + and isinstance(dn, torch.Tensor) + and gup.ndim == 3 + and dn.ndim == 3 + ): + continue + m = re.search(r"layers\.(\d+)\.", name) + if not m: + continue + layer = int(m.group(1)) + # Skip layers whose experts aren't in the checkpoint under this scheme (e.g. dense layers). + if base_fmt.format(layer=layer, e=0, proj=projs_gu[0]) + ".weight" not in wmap: + continue + E = gup.shape[0] + gqd, gscale, gpts = _build_expert_nvfp4( + repo_id, wmap, base_fmt, layer, projs_gu, E, gup.device + ) + dqd, dscale, dpts = _build_expert_nvfp4( + repo_id, wmap, base_fmt, layer, projs_dn, E, dn.device + ) + mod.gate_up_proj = nn.Parameter( + NVFP4Tensor(gqd, gscale, 16, torch.bfloat16, per_tensor_scale=gpts), + requires_grad=False, + ) + mod.down_proj = nn.Parameter( + NVFP4Tensor(dqd, dscale, 16, torch.bfloat16, per_tensor_scale=dpts), + requires_grad=False, + ) + # Drop the stale FP8 scale params the quantizer created for these experts + # (randomly-initialized `*_scale_inv` from the MISSING-key path, plus any FP8 + # `*_scale`/`input_scale`). They're unused once the experts are NVFP4Tensor routed + # through scattermoe, and would otherwise be sharded by FSDP / waste memory. + for stale in ( + "gate_up_proj_scale_inv", + "down_proj_scale_inv", + "gate_up_proj_scale", + "down_proj_scale", + "gate_up_proj_scale_2", + "down_proj_scale_2", + "gate_up_proj_input_scale", + "down_proj_input_scale", + ): + for store in (mod._parameters, mod._buffers): + if stale in store: + del store[stale] + del gup, dn + fixed += 1 + if fixed: + LOG.info( + "Attached NVFP4 expert scales (rebuilt %d %s experts as NVFP4Tensor)", + fixed, + scheme_name, + ) + return fixed + + +if __name__ == "__main__": # local self-consistency test on real layer-0 data + REPO = "nvidia/DeepSeek-V4-Flash-NVFP4" + NVFP4Tensor = _nvfp4_cls() + wmap = _load_index(REPO) + _, _scheme = _detect_scheme(wmap) + _base_fmt = _scheme["base_fmt"] + dev = "cuda" + # fused gate_up qdata+scale from w1+w3 (expert 0 only via n_experts=1 slice below) + gqd, gscale, gpts = _build_expert_nvfp4( + REPO, wmap, _base_fmt, 0, ("w1", "w3"), 4, dev + ) + print( + "fused gate_up qdata", + gqd.shape, + "scale", + gscale.shape, + "per_tensor", + gpts.item(), + ) + # self-consistency: NVFP4 of fused must dequant to cat(dequant(w1), dequant(w3)) + f = _shard_open(REPO, wmap["layers.0.ffn.experts.0.w1.weight"]) + qd1 = f.get_tensor("layers.0.ffn.experts.0.w1.weight").to(dev) + qd3 = f.get_tensor("layers.0.ffn.experts.0.w3.weight").to(dev) + s1 = f.get_tensor("layers.0.ffn.experts.0.w1.weight_scale").to(dev) + s3 = f.get_tensor("layers.0.ffn.experts.0.w3.weight_scale").to(dev) + p = f.get_tensor("layers.0.ffn.experts.0.w1.weight_scale_2").to(dev).float() + d1 = NVFP4Tensor(qd1, s1, 16, torch.bfloat16, per_tensor_scale=p).dequantize( + torch.bfloat16 + ) + d3 = NVFP4Tensor(qd3, s3, 16, torch.bfloat16, per_tensor_scale=p).dequantize( + torch.bfloat16 + ) + fused = NVFP4Tensor( + gqd[0], gscale[0], 16, torch.bfloat16, per_tensor_scale=gpts + ).dequantize(torch.bfloat16) + ref = torch.cat([d1, d3], dim=0) + print( + "fusion self-consistent:", + torch.equal(fused, ref), + "max_err", + (fused - ref).abs().max().item(), + ) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_nonexpert.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_nonexpert.py new file mode 100644 index 0000000000..8f844ab70a --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_nonexpert.py @@ -0,0 +1,67 @@ +"""In-place NVFP4 W4A16 quantization of gemma4 non-expert linears (Marlin path). + +Mirrors :mod:`gemma4_nf4_nonexpert` but swaps each frozen non-expert ``nn.Linear`` for a +:class:`MarlinW4A16Linear` — 4-bit NVFP4 weights run through the same validated Marlin kernel as +the grouped MoE experts (bf16 act/out, no FP4 hardware needed; sm80+). Vs the bnb NF4 path this +keeps experts and non-experts on ONE tensor-core 4-bit code path instead of bnb's dequant kernel. + +Targets every ``nn.Linear`` that is NOT inside an expert block; norms, embeddings, routers, +vision/audio towers, and lm_head are skipped by name. No LoRA is attached (experts-only). +""" + +from __future__ import annotations + +import torch.nn as nn + +from axolotl.utils.logging import get_logger + +from .gemma4_fp8_nonexpert import _should_skip_by_name +from .gemma4_nf4_nonexpert import _expert_paths + +LOG = get_logger(__name__) + + +def quantize_gemma4_nonexpert_nvfp4(model: nn.Module) -> int: + """Swap non-expert ``nn.Linear`` modules for frozen NVFP4 ``MarlinW4A16Linear``, in place. + Returns the count swapped. Experts (NVFP4Tensor) are untouched. Idempotent.""" + from .marlin_w4a16 import marlin_w4a16_available + from .marlin_w4a16.nonexpert_linear import MarlinW4A16Linear + + if not marlin_w4a16_available(): + raise RuntimeError( + "nonexpert_quantization=nvfp4 needs the Marlin W4A16 kernel (sm80+ GPU + CUDA " + "toolkit). Use nonexpert_quantization=nf4 (bitsandbytes) on unsupported setups." + ) + + expert_paths = _expert_paths(model) + + def _under_expert(path: str) -> bool: + return any(path == ep or path.startswith(ep + ".") for ep in expert_paths) + + targets: list[tuple[str, nn.Linear]] = [] + for name, mod in model.named_modules(): + if isinstance(mod, MarlinW4A16Linear): + continue + if not isinstance(mod, nn.Linear): + continue + if _under_expert(name) or _should_skip_by_name(name): + continue + if not isinstance(mod.weight, nn.Parameter) or mod.weight.ndim != 2: + continue + targets.append((name, mod)) + + for name, mod in targets: + new = MarlinW4A16Linear( + mod.weight.data, mod.bias.data if mod.bias is not None else None + ) + parent_path, _, attr = name.rpartition(".") + parent = model.get_submodule(parent_path) if parent_path else model + setattr(parent, attr, new) + + if targets: + LOG.info( + "Gemma4 NVFP4 non-expert: swapped %d non-expert linears to Marlin W4A16 " + "(4-bit weight, bf16 compute)", + len(targets), + ) + return len(targets) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_weight_converter.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_weight_converter.py new file mode 100644 index 0000000000..7127752bc0 --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/nvfp4_weight_converter.py @@ -0,0 +1,234 @@ +"""Native WeightConverter so Gemma-4 NVFP4 MoE experts load as NVFP4Tensor. + +nvidia/Gemma-4-26B-A4B-NVFP4 ships per-expert weights under + ``model.language_model.layers.N.experts.E.{gate_proj,up_proj,down_proj}.{weight,weight_scale,weight_scale_2}`` +but ``quant_method: modelopt`` is not a recognized transformers quantizer, so +the model loads as a BF16 skeleton with the per-expert NVFP4 tensors landing +as UNEXPECTED and the fused ``gate_up_proj``/``down_proj`` remaining random BF16. + +This module registers a ``WeightConverter`` for the ``gemma4_text`` model type +that fuses the per-expert raw uint8 qdata + E4M3 block scales + per-tensor +scalar into a single ``NVFP4Tensor`` (packed 4-bit) and assigns it in-place to +the ``Gemma4TextExperts`` module — exactly like ``Mxfp4Deserialize`` does for +MXFP4. ``is_nvfp4_param(param)`` returns ``True`` on the result, activating +the scattermoe fused NVFP4 path. + +The fusion logic mirrors ``nvfp4_moe_loading._build_expert_nvfp4`` exactly: + gate_up qdata = stack-experts then cat([gate, up], dim=1) → [E, 2*I, H/2] uint8 + gate_up scale = same → [E, 2*I, H/16] e4m3 + gate_up pts = scalar (shared between gate and up) + down qdata = stack-experts → [E, H, I/2] uint8 + down scale = stack-experts → [E, H, I/16] e4m3 + +Registration is done via ``transformers.conversion_mapping.register_checkpoint_conversion_mapping`` +— no site-packages edits. The registration helper is gated: call it only when +the model is gemma4 + NVFP4 modelopt. +""" + +from __future__ import annotations + +from typing import Any + +import torch +import torch.nn as nn + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + + +def _nvfp4_cls(): + try: + from torchao.prototype.mx_formats.nvfp4_tensor import NVFP4Tensor + + return NVFP4Tensor + except ImportError: + return None + + +class Nvfp4ExpertsDeserialize: + """ConversionOps that fuses per-expert NVFP4 tensors into a single NVFP4Tensor. + + For gate_up_proj, ``input_dict`` contains four keys (the source_patterns): + - ``"experts.*.gate_proj.weight"`` → list of E uint8 tensors [I, H/2] + - ``"experts.*.up_proj.weight"`` → list of E uint8 tensors [I, H/2] + - ``"experts.*.gate_proj.weight_scale"`` → list of E e4m3 tensors [I, H/16] + - ``"experts.*.up_proj.weight_scale"`` → list of E e4m3 tensors [I, H/16] + - ``"experts.*.gate_proj.weight_scale_2"`` → list of E float32 scalars (use first) + + For down_proj, ``input_dict`` contains: + - ``"experts.*.down_proj.weight"`` → list of E uint8 tensors [H, I/2] + - ``"experts.*.down_proj.weight_scale"`` → list of E e4m3 tensors [H, I/16] + - ``"experts.*.down_proj.weight_scale_2"`` → list of E float32 scalars + + The op attaches the fused NVFP4Tensor to the module in-place and returns ``{}`` + so the loader does not try to materialize the original meta-parameter names. + """ + + def convert( + self, + input_dict: dict[str, Any], + source_patterns: list[str] | None = None, + target_patterns: list[str] | None = None, + full_layer_name: str | None = None, + model: nn.Module | None = None, + missing_keys: set | None = None, + **kwargs, + ) -> dict[str, Any]: + from transformers.quantizers.quantizers_utils import get_module_from_name + + NVFP4Tensor = _nvfp4_cls() + if NVFP4Tensor is None: + raise RuntimeError( + "torchao.prototype.mx_formats.nvfp4_tensor.NVFP4Tensor not found; " + "install torchao with NVFP4 support" + ) + + if full_layer_name is None or "gate_up_proj" not in full_layer_name: + proj = "down_proj" + else: + proj = "gate_up_proj" + + def _find(pat_suffix: str) -> list[torch.Tensor]: + """Find the tensor list for a source pattern that ends with pat_suffix.""" + for key, tensors in input_dict.items(): + if key.endswith(pat_suffix): + return tensors + raise KeyError( + f"Nvfp4ExpertsDeserialize: could not find '{pat_suffix}' in " + f"input_dict keys: {list(input_dict.keys())}" + ) + + # spawn_materialize casts all checkpoint tensors to the skeleton dtype (bf16) before + # the converter sees them. uint8 qdata (0-255) and float8_e4m3fn scales both roundtrip + # exactly through bf16, so recast back to the raw dtypes NVFP4Tensor needs. + def _recast_weight(t: torch.Tensor) -> torch.Tensor: + if t.dtype != torch.uint8: + return t.to(torch.int32).to(torch.uint8) + return t + + def _recast_scale(t: torch.Tensor) -> torch.Tensor: + if t.dtype != torch.float8_e4m3fn: + return t.to(torch.float8_e4m3fn) + return t + + if proj == "gate_up_proj": + gate_w = [_recast_weight(t) for t in _find("gate_proj.weight")] + up_w = [_recast_weight(t) for t in _find("up_proj.weight")] + gate_sc = [_recast_scale(t) for t in _find("gate_proj.weight_scale")] + up_sc = [_recast_scale(t) for t in _find("up_proj.weight_scale")] + pts_list = _find("gate_proj.weight_scale_2") + + gate_qd = torch.stack(gate_w, dim=0) # [E, I, H/2] + up_qd = torch.stack(up_w, dim=0) # [E, I, H/2] + qdata = torch.cat([gate_qd, up_qd], dim=1) # [E, 2I, H/2] + del gate_qd, up_qd + + gate_s = torch.stack(gate_sc, dim=0) # [E, I, H/16] + up_s = torch.stack(up_sc, dim=0) # [E, I, H/16] + scale = torch.cat([gate_s, up_s], dim=1) # [E, 2I, H/16] + del gate_s, up_s + + # Per-expert weight_scale_2 stacked to [E,1,1] (gate/up share it), not expert-0's scalar. + pts = torch.stack([t.to(torch.float32) for t in pts_list]).view(-1, 1, 1) + + else: # down_proj + down_w = [_recast_weight(t) for t in _find("down_proj.weight")] + down_sc = [_recast_scale(t) for t in _find("down_proj.weight_scale")] + pts_list = _find("down_proj.weight_scale_2") + + qdata = torch.stack(down_w, dim=0) # [E, H, I/2] + scale = torch.stack(down_sc, dim=0) # [E, H, I/16] + pts = torch.stack([t.to(torch.float32) for t in pts_list]).view(-1, 1, 1) + + nvfp4 = NVFP4Tensor(qdata, scale, 16, torch.bfloat16, per_tensor_scale=pts) + + module, _ = get_module_from_name(model, full_layer_name) + setattr(module, proj, nn.Parameter(nvfp4, requires_grad=False)) + + if missing_keys is not None: + missing_keys.discard(full_layer_name) + + module._is_hf_initialized = True + + LOG.debug( + "Nvfp4ExpertsDeserialize: set %s as NVFP4Tensor [%s]", + full_layer_name, + list(qdata.shape), + ) + return {} + + # No meaningful reverse op (packed NVFP4 → checkpoint would need to unfuse). + @property + def reverse_op(self): + from transformers.core_model_loading import _IdentityOp + + return _IdentityOp() + + +def nvfp4_experts_weight_converters() -> list: + """Return the two WeightConverter instances for gemma4 NVFP4 experts. + + These are registered under ``"gemma4_text"`` in the transformers + conversion_mapping cache so the loader finds and applies them during + ``from_pretrained``. + """ + from transformers.core_model_loading import WeightConverter + + op = Nvfp4ExpertsDeserialize() + + # Source patterns MUST be ordered longest-suffix-first. transformers compiles them into a + # single ``(?P...)|(?P...)`` alternation and resolves a key with ``re.search`` + + # first-non-None group (core_model_loading.py). The patterns are NOT end-anchored when the + # converter is many-to-one (the ^...$ anchoring only runs for equal-length source/target + # lists), so ``...weight`` would substring-match inside ``...weight_scale``/``...weight_scale_2`` + # and steal those keys unless the more specific suffixes appear first. + gate_up_converter = WeightConverter( + source_patterns=[ + # gate and up each ship their own weight_scale_2 scalar; claim BOTH so neither + # lands as an UNEXPECTED key (the op only reads the first; they're identical). + "experts.*.gate_proj.weight_scale_2", + "experts.*.up_proj.weight_scale_2", + "experts.*.gate_proj.weight_scale", + "experts.*.up_proj.weight_scale", + "experts.*.gate_proj.weight", + "experts.*.up_proj.weight", + ], + target_patterns="experts.gate_up_proj", + operations=[op], + ) + + down_converter = WeightConverter( + source_patterns=[ + "experts.*.down_proj.weight_scale_2", + "experts.*.down_proj.weight_scale", + "experts.*.down_proj.weight", + ], + target_patterns="experts.down_proj", + operations=[op], + ) + + return [gate_up_converter, down_converter] + + +def register_gemma4_nvfp4_converters() -> None: + """Seed the transformers conversion_mapping cache with NVFP4 expert converters + for ``gemma4_text``. + + Safe to call multiple times (idempotent via overwrite=True on re-entry). + Does not touch DSV4, bf16 gemma4, or any other model type. + """ + from transformers.conversion_mapping import register_checkpoint_conversion_mapping + + converters = nvfp4_experts_weight_converters() + try: + register_checkpoint_conversion_mapping("gemma4_text", converters) + except ValueError: + # Already registered; overwrite to keep converters fresh. + register_checkpoint_conversion_mapping( + "gemma4_text", converters, overwrite=True + ) + + LOG.info( + "Registered gemma4_text NVFP4 expert WeightConverters in transformers conversion_mapping" + ) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py index 2375dda78f..a8ad8de192 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/parallel_linear_lora.py @@ -75,7 +75,7 @@ def forward( ) is_mx = True else: - # Cast weights to match input dtype (e.g. 8-bit LoRA) + # match input dtype (e.g. 8-bit LoRA) if expert_weights.dtype != x.dtype: expert_weights = expert_weights.to(x.dtype) is_mx = False @@ -86,13 +86,12 @@ def forward( N_dim = expert_weights.N # type: ignore[union-attr] else: N_dim = expert_weights.size(-1) # type: ignore[union-attr] - # Forward output is [L_scattered, N]. Overflow risk is dominated by - # that buffer; also probe X for the unusual case where it alone is - # huge (e.g. very wide hidden with modest seq). + # Overflow risk is dominated by the [L_scattered, N] output buffer; also probe X for the + # rare case where it alone is huge (very wide hidden, modest seq). needs_int64_fwd = (L_scattered * N_dim) >= _INT_MAX or _needs_int64_indices(x) with torch.device(x.device): if is_mx: - # Fused MXFP4 forward: dequant happens inside the K-loop + # MXFP4: dequant happens inside the K-loop output = scatter2scatter_lora_mx( X=x, W_mx=expert_weights, @@ -108,7 +107,6 @@ def forward( int64_indices=needs_int64_fwd, ) else: - # Fused forward: Y = X @ W + scaling * (X @ A^T) @ B^T output = scatter2scatter_lora( X=x, W=expert_weights, @@ -124,7 +122,7 @@ def forward( int64_indices=needs_int64_fwd, ) - # Handle gating (weighted combination of top-k expert outputs) + # gating: weighted combination of top-k expert outputs if gates is not None: output_expanded = output.view( gates.size(0), gates.size(1), output.size(-1) @@ -143,21 +141,24 @@ def forward( gates, output_expanded, ) - # Store frozen weights as plain Python attributes instead of - # save_for_backward. This avoids: - # 1. Version-check conflicts with FSDP unshard/reshard - # 2. Pinning all-gathered parameters via saved_tensors hooks - # 3. Interfering with activation offloading pack/unpack hooks - # Safe because expert_weights are frozen (requires_grad=False). - ctx.expert_weights = expert_weights + # Frozen weights as plain ctx attributes, not save_for_backward: avoids version-check + # conflicts with FSDP unshard/reshard, pinning all-gathered params via saved_tensors + # hooks, and interfering with activation-offload pack/unpack hooks. Safe since the + # weights are frozen. If the caller attached a recompute recipe (selective MX/NVFP4 + # gather, a per-layer copy rebuildable from the resident param), store the recipe and + # DROP the heavy copy so it frees on return; backward rebuilds it. Else keep the + # reference (the bf16 path passes a cheap param view). + ctx.weight_recipe = getattr(expert_weights, "recipe", None) + ctx.expert_weights = ( + None if ctx.weight_recipe is not None else expert_weights + ) ctx.expert_biases = expert_biases ctx.grouped_in = grouped_in ctx.grouped_out = grouped_out ctx.k = k ctx.scaling = scaling - # MXFP4 forces fused dX + gather: the non-fused dX path would have - # to materialise a bf16 weight tile, defeating the kernel-fusion - # win, and the gather/scatter pattern is identical. + # MXFP4 forces fused dX + gather: the non-fused dX path would materialize a bf16 weight + # tile, defeating the fusion win, and the gather/scatter pattern is identical. ctx.use_fused_dX = True if is_mx else use_fused_dX ctx.use_fused_gather = True if is_mx else use_fused_gather ctx.is_mx = is_mx @@ -177,7 +178,13 @@ def backward(ctx, grad_out: torch.Tensor): gates, output_expanded, ) = ctx.saved_tensors - expert_weights = ctx.expert_weights + # Rebuild the selective MX/NVFP4 weights from the recipe instead of a copy pinned since + # forward (see forward for rationale). + expert_weights = ( + ctx.expert_weights + if ctx.expert_weights is not None + else ctx.weight_recipe() + ) k = ctx.k scaling = ctx.scaling @@ -189,15 +196,12 @@ def backward(ctx, grad_out: torch.Tensor): else: E = expert_weights.size(0) - # ------------------------------------------------------------------ - # Gate gradients (if using top-k gating with routing weights) - # ------------------------------------------------------------------ + # Gate gradients (top-k gating with routing weights) if gates is not None: # d_gates[t, j] = output_expanded[t, j, :] . grad_out[t, :] d_gates = (output_expanded @ grad_out.unsqueeze(-1)).squeeze(-1) gates_flat = gates.flatten() gate_fan = gates.size(1) - # Reuse output_expanded buffer for grouped_grad_out grouped_grad_out = output_expanded.flatten(0, 1) else: d_gates = None @@ -205,16 +209,9 @@ def backward(ctx, grad_out: torch.Tensor): gate_fan = 1 grouped_grad_out = None - # ------------------------------------------------------------------ - # LoRA gradients (dA, dB) and setup for dX - # ------------------------------------------------------------------ - # Fused gather uses sorted_scattered_idxs for indirect X access - # in the Triton kernel, avoiding the group(x) allocation. - # - # can_fuse_gather: X is ungrouped and not too large for scatter loads - # - When gates is None and grouped_out=False: both DY and X ungrouped - # - When grouped_out=True (gate_up_proj): DY already grouped, X ungrouped - # -> use dy_grouped=True in the fused kernel + # Fused gather uses sorted_scattered_idxs for indirect X access in the Triton kernel, + # avoiding the group(x) allocation. Enabled when X is ungrouped and not too large for + # scatter loads (grouped_out=True still works via dy_grouped=True in the kernel). M_total = sorted_scattered_idxs.size(0) K_dim = x.size(-1) N_dim = expert_weights.N if is_mx else expert_weights.size(-1) @@ -228,9 +225,8 @@ def backward(ctx, grad_out: torch.Tensor): and fuse_gather_workload < _FUSE_GATHER_THRESHOLD ) - # The backward path indexes into grad_out [M_total, N] and x [M, K] - # using either M_idx (grouped) or scatter_idx (ungrouped). Overflow - # risk is dominated by the largest indexed buffer along the M axis. + # Overflow risk is dominated by the largest M-axis indexed buffer (grad_out [M_total, N], + # x [M, K]). needs_int64_bwd = ( (M_total * N_dim) >= _INT_MAX or (M_total * K_dim) >= _INT_MAX @@ -239,9 +235,7 @@ def backward(ctx, grad_out: torch.Tensor): yb = None # dY @ B, computed by the non-fused dA/dB path; reused by dX_lora if can_fuse_gather: - # ------------------------------------------------------------------ # Fused path: skip group(x) entirely - # ------------------------------------------------------------------ d_expanded_input = None d_lora_A, d_lora_B = group_bwd_lora_fused( @@ -258,8 +252,7 @@ def backward(ctx, grad_out: torch.Tensor): int64_indices=needs_int64_bwd, ) - # Prepare grouped_grad_out for the dX path (needed by both - # the fused dX kernel when grouped_out=True, and the non-fused path) + # grouped_grad_out for the dX path if grouped_out: grouped_grad_out = grad_out elif not ctx.use_fused_dX: @@ -271,9 +264,7 @@ def backward(ctx, grad_out: torch.Tensor): out=grouped_grad_out, ) else: - # ------------------------------------------------------------------ # Original path: explicit group() calls - # ------------------------------------------------------------------ if grouped_out: grouped_grad_out = grad_out else: @@ -290,12 +281,11 @@ def backward(ctx, grad_out: torch.Tensor): d_expanded_input = None else: grouped_x = base_ops.group(x, sorted_scattered_idxs, fan_out=k) - d_expanded_input = grouped_x # Will be overwritten; reuse buffer + d_expanded_input = grouped_x # overwritten; reuse buffer - # dA/dB via grouped-Gram over precomputed XA/YB (rank-sized) instead - # of the split kernel's per-output-block recompute -- a large win as - # the expert count grows (modern MoEs, E >= 128). YB is also reused by - # the non-fused dX path below. + # dA/dB via grouped-Gram over precomputed XA/YB (rank-sized) instead of the split + # kernel's per-output-block recompute; a large win as E grows (E >= 128). YB is + # reused by the non-fused dX path below. rank = lora_A.size(0) // E k_dim = lora_A.size(1) n_dim = lora_B.size(0) @@ -333,12 +323,9 @@ def backward(ctx, grad_out: torch.Tensor): scaling, ) - # ------------------------------------------------------------------ # Input gradient: dX = dY @ W^T + scaling * (dY @ B) @ A - # ------------------------------------------------------------------ if is_mx: - # dX kernel reuses the forward MX layout (block axis = K) — - # no pre-transpose/re-quantize needed. + # dX kernel reuses the forward MX layout (block axis = K), no pre-transpose/requant. if can_fuse_gather and not grouped_out: d_expanded_input = scatter2scatter_lora_dX_mx( DY=grad_out, @@ -407,7 +394,7 @@ def backward(ctx, grad_out: torch.Tensor): d_expanded_input = base_ops.scatter2scatter( X=grouped_grad_out, x_grouped=True, - W=expert_weights.permute(0, 2, 1), # [E, N, K] + W=expert_weights.permute(0, 2, 1), sorted_expert_idxs=sorted_expert_idxs, sorted_scattered_idxs=sorted_scattered_idxs, k=1, @@ -416,8 +403,7 @@ def backward(ctx, grad_out: torch.Tensor): int64_indices=needs_int64_bwd, ) - # LoRA part: dX_lora = scaling * (dY @ B) @ A (sync-free grouped GEMMs; - # reuses YB from the dA/dB path when it was computed there) + # dX_lora = scaling * (dY @ B) @ A (sync-free grouped GEMMs; reuses YB from dA/dB) if scaling != 0.0: d_input_lora_grouped = _compute_lora_input_grad( grouped_grad_out, @@ -434,11 +420,9 @@ def backward(ctx, grad_out: torch.Tensor): if grouped_in: d_expanded_input.add_(d_input_lora_grouped) else: - # Scatter-add LoRA gradient directly into d_expanded_input. - # Avoids allocating a zeros_like + add result + # scatter-add directly into d_expanded_input (avoids a zeros_like + add) d_expanded_input[sorted_scattered_idxs] += d_input_lora_grouped - # Reduce over top-k if k > 1 if k == 1: d_input = d_expanded_input else: @@ -446,7 +430,7 @@ def backward(ctx, grad_out: torch.Tensor): x.size(0), k, d_expanded_input.size(-1) ).sum(-2) - # W is frozen during LoRA training -- skip weight gradient. + # W is frozen during LoRA training, skip weight gradient. # (MX weights are containers, not tensors, and never carry grad.) if is_mx: d_weights = None @@ -505,7 +489,7 @@ def _compute_lora_input_grad( if sorted_expert_idxs is not None: if yb is None: - w_yb = lora_B.reshape(N, E, R).permute(1, 0, 2).contiguous() # [E, N, R] + w_yb = lora_B.reshape(N, E, R).permute(1, 0, 2).contiguous() yb = base_ops.scatter2scatter( X=grouped_grad_out, W=w_yb, @@ -516,7 +500,7 @@ def _compute_lora_input_grad( y_grouped=True, int64_indices=int64_indices, ) - w_a = lora_A.reshape(E, R, K).contiguous() # [E, R, K] + w_a = lora_A.reshape(E, R, K).contiguous() dx = base_ops.scatter2scatter( X=yb, W=w_a, @@ -549,11 +533,6 @@ def _compute_lora_input_grad( return d_input_lora -# ============================================================================= -# Helper: Extract LoRA params from PEFT ParamWrapper -# ============================================================================= - - def get_lora_params_from_wrapper(module) -> tuple: """ Extract LoRA parameters from a PEFT ParamWrapper. @@ -584,11 +563,6 @@ def get_lora_params_from_wrapper(module) -> tuple: return lora_A, lora_B, scaling -# ============================================================================= -# Drop-in replacement for parallel_linear -# ============================================================================= - - def parallel_linear_lora( inputs: torch.Tensor, expert_weights: torch.Tensor, diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/runtime.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/runtime.py new file mode 100644 index 0000000000..74ee6ac60b --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/runtime.py @@ -0,0 +1,69 @@ +"""Centralized mutable process-wide ScatterMoE runtime settings. + +One place to see (and reset) everything that changes ScatterMoE behavior at runtime. It is applied +once per run from the resolved config by :func:`configure_scattermoe_runtime` +(``KernelsPlugin.pre_model_load``), which resets first so a long-lived multi-run process can't +inherit stale state from a previous config. + +The per-module ``set_*`` functions in experts.py / grouped_train.py / chunked_bnb.py remain as thin +compatibility wrappers that delegate to :data:`RUNTIME` (kept for the existing call sites + tests); +the kernel code reads ``RUNTIME`` fields directly. +""" + +from __future__ import annotations + +from dataclasses import dataclass, fields + +# chunk size for the chunked bnb-4bit grouped path; 32 balances throughput on small-expert MoEs +# against bounding the bf16 transient on large-expert models. +DEFAULT_CHUNK = 32 + + +@dataclass +class ScatterMoERuntime: + """All mutable knobs that affect ScatterMoE runtime behavior.""" + + # grouped fp4 MoE experts (experts.py): None = off (fused/eager paths unchanged) | "nvfp4". + fp4_grouped_mode: str | None = None + # base dX backward precision on sm120: True = fp8-read (fast, ~2% grad) | False = bf16-dequant. + fp4_dx_prefer_fp8: bool = True + # grouped base-GEMM backend (grouped_train.py): None/"auto" | marlin | cutlass | deepgemm | dequant. + grouped_backend: str | None = None + # bnb-4bit experts (chunked_bnb.py): 1-launch parallel_linear path (recompute-in-backward) vs chunked. + bnb_fast: bool = True + # chunked-dequant chunk size override; None -> DEFAULT_CHUNK. + dequant_chunk_size: int | None = None + # whether layer-level gradient checkpointing is active (skips the redundant per-chunk checkpoint). + layer_gc_active: bool = False + # persist the requantized mxfp4 weight in a module-level cache across steps. Safe on a persistent + # single-device param, but under FSDP2 the gathered param is the FULL weight every step, so + # caching it holds a full-model mxfp4 copy per rank (OOM). Disabled under FSDP: recompute per + # forward (freed after each layer), bounding resident mxfp4 to one layer. + mxfp4_cache_persist: bool = True + + def reset(self) -> None: + for f in fields(self): + setattr(self, f.name, f.default) + + +RUNTIME = ScatterMoERuntime() + + +def configure_scattermoe_runtime(cfg) -> None: + """Apply ALL ScatterMoE runtime settings from a run config. + + Resets to defaults first so a long-lived process never inherits stale state, then maps the + relevant config fields. ``cfg`` is the resolved config (DictDefault). + """ + RUNTIME.reset() + RUNTIME.fp4_grouped_mode = cfg.get("dsv4_fp4_grouped_mode") + backend = cfg.get("moe_grouped_backend") + RUNTIME.grouped_backend = str(backend).lower() if backend else None + chunk = cfg.get("moe_dequant_chunk_size") + RUNTIME.dequant_chunk_size = int(chunk) if chunk else None + RUNTIME.layer_gc_active = bool(cfg.get("gradient_checkpointing")) + fast = cfg.get("moe_bnb_fast") + RUNTIME.bnb_fast = True if fast is None else bool(fast) + # Cache only in the non-FSDP case; under FSDP a persistent mxfp4 cache holds a full-model copy + # per rank (OOM). + RUNTIME.mxfp4_cache_persist = not bool(cfg.get("fsdp_config")) diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py index e49e9c7bf6..bd44a67d3b 100644 --- a/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/selective_dequant.py @@ -91,7 +91,6 @@ def remap_expert_indices( remapped_idxs = remap[sorted_expert_idxs] - # Compact the expert_offsets: only keep active experts' cumulative counts compact_offsets = expert_offsets[active_experts] return remapped_idxs, compact_offsets @@ -123,26 +122,32 @@ def _selective_dequant_bnb4( from bitsandbytes.functional import QuantState expert_numel = expert_shape[0] * expert_shape[1] - packed_per_expert = expert_numel // 2 # 4-bit = 2 values per byte - blocks_per_expert = expert_numel // quant_state.blocksize num_active = len(active_experts) - if blocks_per_expert == 0: - # Expert is smaller than one quantization block — blocks span across - # expert boundaries, so per-expert slicing isn't possible. - # Fallback: full dequantize + index. + # Per-expert slicing assumes each expert owns whole packed bytes (2 nf4 values/byte) and + # whole quant blocks; otherwise bytes/blocks straddle expert boundaries and the slice is + # silently wrong. Fall back to full dequant + index when that doesn't hold. + if ( + expert_numel <= 0 + or quant_state.blocksize <= 0 + or expert_numel % 2 != 0 + or expert_numel % quant_state.blocksize != 0 + ): full = F.dequantize_4bit(raw_param, quant_state) - E_total = full.numel() // expert_numel + E_total = full.numel() // expert_numel if expert_numel else 0 return full.reshape(E_total, *expert_shape)[active_experts] - # Use fused Triton kernel for NF4 (handles selective gather + dequant in one pass) + packed_per_expert = expert_numel // 2 # 4-bit = 2 values per byte + blocks_per_expert = expert_numel // quant_state.blocksize + + # Fused Triton kernel for NF4: selective gather + dequant in one pass. if quant_state.quant_type == "nf4" and raw_param.dtype == torch.uint8: from axolotl.integrations.kernels.libs.scattermoe_lora.selective_dequant_kernel import ( selective_dequant_nf4_triton, ) - # Handle nested (double) quantization: dequantize absmax first - # BnB uses dequantize_blockwise (not _4bit) for nested absmax + offset + # Nested (double) quant: dequantize absmax first via dequantize_blockwise (not _4bit), + # then add the offset. if quant_state.nested: absmax = F.dequantize_blockwise(quant_state.absmax, quant_state.state2) absmax += quant_state.offset @@ -264,7 +269,6 @@ def selective_expert_weights( Returns: Compact weight tensor [num_active, dim1, dim2] ready for ScatterMoE """ - # Check if the parameter is BnB-quantized via parametrize if ( hasattr(experts_module, "parametrizations") and param_name in experts_module.parametrizations @@ -284,7 +288,7 @@ def selective_expert_weights( if isinstance(orig_shape, torch.Size) and len(orig_shape) == 3: expert_shape = (orig_shape[1], orig_shape[2]) elif isinstance(orig_shape, torch.Size) and len(orig_shape) == 1: - # Flattened — need to infer from module attributes + # Flattened; infer the expert shape from module attributes. E_total = getattr(experts_module, "num_experts", None) if E_total is None: E_total = int(active_experts.max().item()) + 1 @@ -303,14 +307,13 @@ def selective_expert_weights( return _selective_dequant_bnb4(raw_param, qs, active_experts, expert_shape) - # Pull the parameter out before format dispatch — used by every branch below. param = getattr(experts_module, param_name) - # MXFP4 (torchao MXTensor) — dequantize the subset, return [num_active, d1, d2] + # MXFP4 (torchao MXTensor): dequantize the subset, return [num_active, d1, d2] if is_mxfp4_param(param): return _selective_dequant_mxfp4(param, active_experts) - # Dense parameter (bf16/fp32) — direct indexing + # Dense parameter (bf16/fp32): direct indexing if param.dim() == 3: return param[active_experts] diff --git a/src/axolotl/integrations/kernels/libs/scattermoe_lora/torchao_fp4_add.py b/src/axolotl/integrations/kernels/libs/scattermoe_lora/torchao_fp4_add.py new file mode 100644 index 0000000000..5f95b2417b --- /dev/null +++ b/src/axolotl/integrations/kernels/libs/scattermoe_lora/torchao_fp4_add.py @@ -0,0 +1,73 @@ +"""Make torchao FP4 tensors support ``aten.add`` by dequantizing. + +PEFT (>= 0.19) applies ``target_parameters`` LoRA to an expert weight via +``torch.nn.utils.parametrize``: it registers a parametrization whose forward computes +``base + delta`` (the merged ``scaling * B @ A``). ``register_parametrization`` evaluates +this immediately. When the base weight is a frozen torchao ``NVFP4Tensor`` / ``MXTensor`` +(e.g. `block_diffusion.frozen_fp4_experts`), ``aten.add`` is unimplemented for the tensor +subclass, so the registration raises:: + + NotImplementedError: NVFP4Tensor dispatch: ... aten.add ... + +Registering a dequantize-then-add for these tensors makes the parametrization produce a +bf16 merged weight (differentiable through ``delta`` to the LoRA A/B), so ScatterMoE then +runs the merged weight via its standard path. Idempotent; safe to call repeatedly. +""" + +from __future__ import annotations + + +def _make_add_handler(): + import torch + + fp4_names = {"NVFP4Tensor", "MXTensor"} + + def _is_fp4(t): + return type(t).__name__ in fp4_names + + def _fp4_add(func, types, args, kwargs): + a, b = args[0], args[1] + # Use the dense operand's dtype (the LoRA delta is bf16/fp16) so the result is a plain + # tensor; dequantize only the FP4 subclass operand(s). + dense_dtype = next( + (t.dtype for t in (a, b) if isinstance(t, torch.Tensor) and not _is_fp4(t)), + torch.bfloat16, + ) + if _is_fp4(a): + a = a.dequantize(dense_dtype) + if _is_fp4(b): + b = b.dequantize(dense_dtype) + # A true in-place add_ is impossible once the FP4 operand is dequantized to a fresh tensor, + # so both add and add_ resolve to this out-of-place add (kwargs forward keyword-only alpha). + return torch.ops.aten.add.Tensor(a, b, **kwargs) + + return _fp4_add + + +def patch_torchao_fp4_add() -> None: + """Register dequantize-add for ``aten.add``/``aten.add_`` on NVFP4Tensor and MXTensor. + + ``add.Tensor`` covers PEFT's out-of-place ``base + delta`` parametrization (training); + ``add_.Tensor`` covers ``ParamWrapper.merge`` (``merge_and_unload`` produces a bf16 + weight — a true 4-bit merge would require re-quantization). + """ + import torch + + handler = _make_add_handler() + ops = [torch.ops.aten.add.Tensor, torch.ops.aten.add_.Tensor] + + for module_path, cls_name in ( + ("torchao.prototype.mx_formats.nvfp4_tensor", "NVFP4Tensor"), + ("torchao.prototype.mx_formats.mx_tensor", "MXTensor"), + ): + try: + module = __import__(module_path, fromlist=[cls_name]) + cls = getattr(module, cls_name) + except (ImportError, AttributeError): + continue + # torchao's op table is nested: cls._ATEN_OP_TABLE[cls][op] (see + # torchao.utils._dispatch__torch_dispatch__). + registered = getattr(cls, "_ATEN_OP_TABLE", {}).get(cls, {}) + for op in ops: + if op not in registered: + cls.implements([op])(handler) diff --git a/src/axolotl/integrations/kernels/libs/sonicmoe/experts.py b/src/axolotl/integrations/kernels/libs/sonicmoe/experts.py index a0d7501a95..546322c631 100644 --- a/src/axolotl/integrations/kernels/libs/sonicmoe/experts.py +++ b/src/axolotl/integrations/kernels/libs/sonicmoe/experts.py @@ -84,6 +84,14 @@ def sonicmoe_experts_forward_with_lora( scattermoe_supports_layout, ) + # Validate the expert layout / device BEFORE the sm120 fallback dispatch: a non-CUDA input or + # ungated experts are invalid for both paths, and the fallback (entered on sm120 where the + # sonic kernel can't run) would otherwise hit a CPU tensor and raise an opaque AttributeError. + if not getattr(self, "has_gate", True): + raise ValueError("sonicmoe requires gated experts (has_gate=True)") + if hidden_states.device.type != "cuda": + raise ValueError("sonicmoe requires CUDA device") + if not _sonicmoe_kernel_supported() and scattermoe_supports_layout(self): return scattermoe_experts_forward( self, hidden_states, top_k_index, top_k_weights @@ -91,16 +99,11 @@ def sonicmoe_experts_forward_with_lora( from transformers.integrations.sonicmoe import _sonicmoe_wrapper - if not getattr(self, "has_gate", True): - raise ValueError("sonicmoe requires gated experts (has_gate=True)") - if hidden_states.device.type != "cuda": - raise ValueError("sonicmoe requires CUDA device") - device = hidden_states.device num_top_k = top_k_index.size(-1) num_tokens = hidden_states.size(0) - # Flatten — token indices must be int32 and sorted ascending (sonic-moe requirement). + # sonic-moe requires int32 token indices sorted ascending. token_idx = ( torch.arange(num_tokens, device=device) .unsqueeze(1) @@ -115,9 +118,8 @@ def sonicmoe_experts_forward_with_lora( if not getattr(self, "has_bias", False): b1 = b2 = None - # FSDP2 / EP wraps parameters as DTensors but sonic-moe takes raw CUTLASS pointers, - # so unwrap to local shards before the materialize/permute. to_local() is - # autograd-aware — backward will rewrap the gradient as a DTensor again. + # sonic-moe takes raw CUTLASS pointers, so unwrap FSDP2/EP DTensors to local shards first. + # to_local() is autograd-aware: backward rewraps the gradient as a DTensor. if isinstance(w1, torch.distributed.tensor.DTensor): w1 = w1.to_local() w2 = w2.to_local() diff --git a/src/axolotl/integrations/kernels/plugin.py b/src/axolotl/integrations/kernels/plugin.py index ddddb160f7..466ce42fc2 100644 --- a/src/axolotl/integrations/kernels/plugin.py +++ b/src/axolotl/integrations/kernels/plugin.py @@ -4,6 +4,10 @@ import torch from axolotl.integrations.base import BasePlugin +from axolotl.integrations.kernels.adapters import get_active_adapters +from axolotl.integrations.kernels.quant_training_guard import ( + relax_quantized_training_guard, +) from axolotl.utils.logging import get_logger LOG = get_logger(__name__) @@ -32,14 +36,12 @@ def _check_sonicmoe_gpu_compat(): f"Supported: Hopper (sm_90) and Blackwell (sm_100 - sm_103)." ) - # Blackwell (sm_100+): enable QuACK GEMM kernels if cc >= (10, 0): os.environ.setdefault("USE_QUACK_GEMM", "1") LOG.info( f"Blackwell GPU (sm_{cc[0]}{cc[1]}) detected, enabling USE_QUACK_GEMM=1" ) - # B300 (sm_103): requires Triton 3.6.0 if cc == (10, 3): triton_spec = importlib.util.find_spec("triton") if triton_spec is None: @@ -56,29 +58,59 @@ def _check_sonicmoe_gpu_compat(): class KernelsPlugin(BasePlugin): + """Thin orchestrator: registers the expert-kernel backend and dispatches model-family + specifics to ``ModelAdapter`` subclasses (see ``adapters/``).""" + def get_input_args(self): return "axolotl.integrations.kernels.KernelsArgs" + def _adapters(self, cfg): + # Cache: matching can be expensive (e.g. AutoConfig.from_pretrained for Gemma-4). + cached = getattr(self, "_cached_adapters", None) + if cached is None: + cached = get_active_adapters(cfg) + self._cached_adapters = cached + return cached + def pre_model_load(self, cfg): - """Register the requested kernel into ``ALL_EXPERTS_FUNCTIONS`` and pin cfg. + """Register the expert-kernel backend + generic capabilities, then run adapter hooks. - Architecture-agnostic: routing stays in each model's SparseMoEBlock; only - the experts call is dispatched through the registry. + Architecture-agnostic: routing stays in each model's SparseMoEBlock; only the experts + call is dispatched through the registry. When EP is active the ExpertParallelPlugin owns + ``experts_implementation`` (a ``deep_ep_*`` composite), so we don't overwrite it here. """ - # When EP is active, the ExpertParallelPlugin selects a `deep_ep_*` - # composite for `experts_implementation`. Don't overwrite that here — - # plugin order is YAML-defined, so we can't rely on EP running last. ep_active = (getattr(cfg, "expert_parallel_size", 1) or 1) > 1 if cfg.use_scattermoe: from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( register_scattermoe_experts, ) + from axolotl.integrations.kernels.libs.scattermoe_lora.runtime import ( + configure_scattermoe_runtime, + ) register_scattermoe_experts() if not ep_active: cfg.experts_implementation = "scattermoe" LOG.info("Registered 'scattermoe' in transformers ExpertsInterface") + + # LoRA on a frozen pre-quantized (FP8/NVFP4) base is supported but rejected by the + # upstream guard; scope-relax it (skips only PEFT/quantized, delegates the rest). + relax_quantized_training_guard() + + # Apply ALL ScatterMoE runtime settings from this run's config through one entry point + # (it resets first, so a long-lived multi-run process can't inherit stale state). + configure_scattermoe_runtime(cfg) + if cfg.get("dsv4_fp4_grouped_mode"): + LOG.info( + "Enabled grouped fp4 MoE path: dsv4_fp4_grouped_mode=%s", + cfg.get("dsv4_fp4_grouped_mode"), + ) + if cfg.get("moe_grouped_backend"): + LOG.info( + "Grouped MoE base-GEMM backend override: %s", + cfg.get("moe_grouped_backend"), + ) elif cfg.use_sonicmoe: _check_sonicmoe_gpu_compat() @@ -91,6 +123,40 @@ def pre_model_load(self, cfg): cfg.experts_implementation = "sonicmoe" LOG.info("Registered 'sonicmoe' in transformers ExpertsInterface") + adapters = self._adapters(cfg) + self._warn_unclaimed_nonexpert_quantization(cfg, adapters) + for adapter in adapters: + adapter.pre_model_load(cfg) + + @staticmethod + def _warn_unclaimed_nonexpert_quantization(cfg, adapters): + """Warn if a non-expert quantization policy is set but no active adapter consumes it. + + ``nonexpert_quantization`` is a global intent, but only some model adapters act on it + (e.g. Gemma-4). Without this, configuring it on an unsupported model silently no-ops. + ``none``/``bf16`` mean "no quantization", so they're never considered unclaimed. + """ + policy = cfg.get("nonexpert_quantization") + if not policy or str(policy).lower() in ("none", "bf16"): + return + if any(a.consumes_nonexpert_quantization(cfg) for a in adapters): + return + LOG.warning( + "nonexpert_quantization=%r is set but no active model adapter consumes it " + "(active adapters: %s); it will have no effect. It is currently implemented only " + "for Gemma-4 NVFP4 checkpoints.", + policy, + [a.name for a in adapters] or "none", + ) + + def pre_lora_load(self, cfg, model): + for adapter in self._adapters(cfg): + adapter.pre_lora_load(cfg, model) + + def post_model_load(self, cfg, model): + for adapter in self._adapters(cfg): + adapter.post_model_load(cfg, model) + def add_callbacks_pre_trainer(self, cfg, model): callbacks = [] if cfg.use_scattermoe: diff --git a/src/axolotl/integrations/kernels/quant_training_guard.py b/src/axolotl/integrations/kernels/quant_training_guard.py new file mode 100644 index 0000000000..935d878a75 --- /dev/null +++ b/src/axolotl/integrations/kernels/quant_training_guard.py @@ -0,0 +1,67 @@ +"""Scoped relaxation of transformers' quantized-training guard. + +``transformers.trainer.validate_quantization_for_training`` rejects pre-quantized FP8/NVFP4 +checkpoints for training. Its FP8/NVFP4 branch fires even when LoRA adapters are attached and the +quantized base is frozen — the supported QLoRA-style pattern. Rather than globally no-op'ing the +guard for all runs, we wrap it: for PEFT models (frozen quantized base + trainable adapters) we +skip, and for everything else we delegate to the original guard unchanged. Idempotent; preserves +the original callable for restore. +""" + +from __future__ import annotations + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +_ORIG_ATTR = "_axolotl_quant_guard_original" +_FN_NAME = "validate_quantization_for_training" + + +def _target_module(module): + if module is not None: + return module + import transformers.trainer as hf_trainer + + return hf_trainer + + +def relax_quantized_training_guard(module=None) -> None: + """Install the scoped guard wrapper (idempotent). + + ``module`` defaults to ``transformers.trainer``; tests pass a throwaway module to avoid + touching global state. + """ + mod = _target_module(module) + current = getattr(mod, _FN_NAME, None) + if current is None or getattr(current, _ORIG_ATTR, None) is not None: + return # missing or already wrapped + + original = current + + def guarded(model): + # LoRA/PEFT on a frozen quantized base is the supported (QLoRA-style) pattern; the upstream + # FP8/NVFP4 branch rejects it anyway. Skip only for PEFT models; delegate the rest. + # _hf_peft_config_loaded is set by HF-native model.add_adapter(); axolotl applies LoRA via + # get_peft_model() -> PeftModel, which exposes .peft_config but NOT that flag. Check both, or + # FP8 checkpoints (e.g. DeepSeek-V4-Flash-NVFP4) get rejected despite frozen-base + adapters. + if getattr(model, "_hf_peft_config_loaded", False) or getattr( + model, "peft_config", None + ): + return None + return original(model) + + setattr(guarded, _ORIG_ATTR, original) + setattr(mod, _FN_NAME, guarded) + LOG.info( + "kernels: scoped quantized-training guard (skips only PEFT/quantized; delegates others)" + ) + + +def restore_quantized_training_guard(module=None) -> None: + """Restore the original guard (for tests/teardown).""" + mod = _target_module(module) + current = getattr(mod, _FN_NAME, None) + original = getattr(current, _ORIG_ATTR, None) + if original is not None: + setattr(mod, _FN_NAME, original) diff --git a/src/axolotl/kernels/quantize.py b/src/axolotl/kernels/quantize.py index eb5c08a4c1..f9dc25d31d 100644 --- a/src/axolotl/kernels/quantize.py +++ b/src/axolotl/kernels/quantize.py @@ -128,11 +128,37 @@ def dequantize_fp8( return W_float * scale_inv.to(dtype) +_FLOAT8_CLS: type | None = None +_FLOAT8_CHECKED = False + + +def _is_float8_tensor(W: torch.Tensor) -> bool: + """A torchao ``Float8Tensor`` (blockwise-FP8 base weight, e.g. DSV4 non-experts). + + It reports a logical bf16 ``dtype`` and carries its own block scale, so the fused + LoRA kernels pass it as ``W`` with ``quant_state=None`` — detect it by class.""" + global _FLOAT8_CLS, _FLOAT8_CHECKED + if not _FLOAT8_CHECKED: + _FLOAT8_CHECKED = True + try: + from torchao.quantization import Float8Tensor as _F8 + + _FLOAT8_CLS = _F8 + except Exception: + _FLOAT8_CLS = None + return _FLOAT8_CLS is not None and isinstance(W, _FLOAT8_CLS) + + def dequantize( W: torch.Tensor, quant_state: QuantState | torch.Tensor | None = None, ) -> torch.Tensor: """NF4 / FP8 dequantization; under `torch.compile` NF4 dispatches via `torch.ops.axolotl.nf4_dequantize`.""" + # torchao Float8Tensor carries its own scale (a transposed view is still a Float8Tensor), + # so dequant to bf16 here and let the downstream matmul/addmm_ stay a plain bf16 GEMM. + if _is_float8_tensor(W): + return W.dequantize().to(torch.bfloat16) + if quant_state is None: return W diff --git a/src/axolotl/loaders/patch_manager.py b/src/axolotl/loaders/patch_manager.py index ec72665b00..bb3070dcc7 100644 --- a/src/axolotl/loaders/patch_manager.py +++ b/src/axolotl/loaders/patch_manager.py @@ -113,6 +113,8 @@ def apply_pre_model_load_patches(self): self._apply_gradient_checkpointing_patches() self._patch_attention() self._apply_multipack_patches() + self._apply_sdpa_varlen_patch() + self._apply_large_head_attention_patch() self._patch_loss_llama() self._patch_llama_derived_model() self._apply_mistral_cross_entropy_patch() @@ -202,9 +204,19 @@ def _apply_gemma_hybrid_attention(self, model: PreTrainedModel): import copy - from axolotl.monkeypatch.gemma4_hybrid_mask import patch_gemma4_hybrid_mask + from axolotl.monkeypatch.attention.large_head import ( + resolve_large_head_policy, + set_large_head_policy, + ) + from axolotl.monkeypatch.gemma4_hybrid_mask import ( + GLOBAL_PACKED_SDPA, + patch_gemma4_hybrid_mask, + ) patch_gemma4_hybrid_mask() + # Gemma-4 global layers reuse the generic large-head router. Default policy 'sdpa' (flash is + # opt-in via large_head_attention / the deprecated flash_attn_d512), preserving prior default. + set_large_head_policy(resolve_large_head_policy(self.cfg)) # Navigate to the module that has 'layers' - varies by model structure: # Gemma4ForConditionalGeneration -> .model (Gemma4Model) -> .language_model (Gemma4TextModel) -> .layers @@ -244,16 +256,19 @@ def _apply_gemma_hybrid_attention(self, model: PreTrainedModel): patched_count = 0 for layer_idx, layer in enumerate(layers): if layer_types[layer_idx] != "sliding_attention": - # Global / full_attention layer - use SDPA instead of FA2 + # Global / full_attention layer (head_dim=512, FA2 can't serve it). Use the + # packing-aware SDPA impl: it rebuilds the block-diagonal mask from position_ids so + # the layer respects document boundaries under sample packing (plain "sdpa" gets a + # None mask here and would attend across packed documents). attn_module = getattr(layer, "self_attn", None) if attn_module is not None and hasattr(attn_module, "config"): sdpa_config = copy.copy(attn_module.config) - sdpa_config._attn_implementation = "sdpa" + sdpa_config._attn_implementation = GLOBAL_PACKED_SDPA attn_module.config = sdpa_config patched_count += 1 LOG.info( - "gemma4_hybrid_attn_impl: patched %d global layers to use SDPA " + "gemma4_hybrid_attn_impl: patched %d global layers to use packing-aware SDPA " "(remaining %d sliding layers use flash_attention_2)", patched_count, len(layers) - patched_count, @@ -311,12 +326,15 @@ def _apply_fsdp_patches(self): patch_parallelism_config() if self.cfg.fsdp_config and str(self.cfg.fsdp_version) == "2": + from axolotl.monkeypatch.accelerate.float8_fsdp import patch_float8_fsdp from axolotl.monkeypatch.accelerate.fsdp2 import ( patch_accelerate_fsdp2, patch_tied_keys_for_meta_device, ) patch_accelerate_fsdp2() + # FSDP2 sharding for any torchao Float8Tensor weights (no-op without torchao) + patch_float8_fsdp() if self.cfg.fsdp_config.cpu_ram_efficient_loading: patch_tied_keys_for_meta_device() if self.cfg.rl: @@ -670,6 +688,34 @@ def _apply_self_attention_lora_patch(self): patch_self_attn_lora(self.cfg) + def _apply_large_head_attention_patch(self): + """Generic head_dim>256 capability for plain SDPA models. Gemma-4's hybrid path routes its + globals through its own impl, so skip the generic sdpa wrapper there to avoid double-wiring.""" + from axolotl.monkeypatch.attention.large_head import ( + resolve_large_head_policy, + set_large_head_policy, + unpatch_sdpa_large_head, + ) + + policy = resolve_large_head_policy(self.cfg) + # Always (re)set the policy global from this run's config so a long-lived process can't + # inherit a previous run's stale auto/triton_flash policy on an sdpa run. + set_large_head_policy(policy) + if policy == "sdpa" or self.cfg.gemma4_hybrid_attn_impl: + unpatch_sdpa_large_head() + return + from axolotl.monkeypatch.attention.large_head import patch_sdpa_large_head + + patch_sdpa_large_head(policy) + + def _apply_sdpa_varlen_patch(self): + """Route packed-row SDPA through cu_seqlens varlen_attn when ``sdpa_varlen`` is set.""" + if not self.cfg.sdpa_varlen: + return + from axolotl.monkeypatch.attention.sdpa_varlen import patch_sdpa_varlen + + patch_sdpa_varlen() + def _apply_multipack_patches(self): """Apply multipack patches if necessary.""" if ( diff --git a/src/axolotl/monkeypatch/accelerate/float8_fsdp.py b/src/axolotl/monkeypatch/accelerate/float8_fsdp.py new file mode 100644 index 0000000000..3ea7d4e48b --- /dev/null +++ b/src/axolotl/monkeypatch/accelerate/float8_fsdp.py @@ -0,0 +1,159 @@ +"""FSDP2 support for torchao ``Float8Tensor`` (frozen blockwise-FP8 weights, dim-0 sharded). + +A weight-only ``Float8Tensor`` (e.g. a pre-quantized blockwise-FP8 checkpoint wrapped for +1-byte storage) can't be FSDP2-sharded as torchao ships it: its ``aten.split`` unpacks +``(tensor, size, dim)`` unconditionally, so FSDP2's ``torch.chunk(param, n)`` (dim defaults +to 0) raises ``ValueError: not enough values to unpack``; and even with an explicit dim, +torchao only implements the rowwise / per-tensor split cases — a 128×128-blocked weight +hits ``else: raise AssertionError("not yet implemented")``. It also ships no FSDP2 +all-gather hooks. + +This patches, for a 2-D ``[N, K]`` weight with a blockwise ``[N//b0, K//b1]`` scale sharded +on dim 0 (the output-feature axis): + * ``aten.split`` with a defaulted dim — qdata splits by ``s``; the scale splits by + ``s // block_size[0]`` (the scale is coarser than qdata by ``block_size[0]``), + * ``new_zeros`` / ``as_strided`` / ``copy_`` / ``clone`` / ``detach`` / ``narrow`` / + ``view`` on the inner ``(qdata, scale)``, + * ``fsdp_pre_all_gather`` / ``fsdp_post_all_gather`` so FSDP2 all-gathers the inner tensors + and rebuilds the subclass instead of using the flat ``view(-1)`` buffer path. + +Idempotent; a no-op if torchao is absent. Dim-0 sharding only (assumes the shard size is a +multiple of ``block_size[0]`` — true for FSDP of these projections, whose N is a multiple of +128×world_size). The blockwise split case is worth upstreaming to torchao. +""" + +from __future__ import annotations + +import torch + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) +_PATCHED = False + + +def _float8_cls(): + try: + from torchao.quantization import Float8Tensor + + return Float8Tensor + except ImportError: + return None + + +def _cstride(shape): + st = [1] * len(shape) + for i in range(len(shape) - 2, -1, -1): + st[i] = st[i + 1] * shape[i + 1] + return st + + +def patch_float8_fsdp(): + global _PATCHED + if _PATCHED: + return + Float8Tensor = _float8_cls() + if Float8Tensor is None: + return + aten = torch.ops.aten + implements = Float8Tensor.implements + + def _rebuild(ref, qdata, scale, block_size=None): + return Float8Tensor( + qdata, + scale, + list(block_size) if block_size is not None else list(ref.block_size), + ref.mm_config, + ref.act_quant_kwargs, + ref.kernel_preference, + dtype=ref.dtype, + ) + + @implements([aten.split.Tensor]) + def _split(func, types, args, kwargs): + x, split_size = args[0], args[1] + dim = args[2] if len(args) > 2 else kwargs.get("dim", 0) + if dim != 0: + raise NotImplementedError( + f"Float8Tensor FSDP split only on dim 0, got {dim}" + ) + blk0 = x.block_size[0] + qd = func(x.qdata, split_size, 0) + sc = func(x.scale, max(1, split_size // blk0), 0) + assert len(qd) == len(sc), f"split mismatch q={len(qd)} s={len(sc)}" + return [_rebuild(x, qd[i], sc[i]) for i in range(len(qd))] + + @implements([aten.clone.default, aten.detach.default]) + def _clone_detach(func, types, args, kwargs): + x = args[0] + return _rebuild(x, func(x.qdata, **kwargs), func(x.scale, **kwargs)) + + @implements([aten.new_zeros.default]) + def _new_zeros(func, types, args, kwargs): + x, size = args[0], list(args[1]) + qd = func(x.qdata, size, **kwargs) + sc_size = [max(1, size[i] // x.block_size[i]) for i in range(len(size))] + sc = func(x.scale, sc_size, **kwargs) + return _rebuild(x, qd, sc) + + @implements([aten.narrow.default]) + def _narrow(func, types, args, kwargs): + x, dim, start, length = args[0], args[1], args[2], args[3] + if dim != 0: + raise NotImplementedError(f"Float8Tensor narrow only dim 0, got {dim}") + blk0 = x.block_size[0] + qd = func(x.qdata, 0, start, length) + sc = func(x.scale, 0, start // blk0, max(1, length // blk0)) + return _rebuild(x, qd, sc) + + @implements([aten.view.default]) + def _view(func, types, args, kwargs): + x, size = args[0], list(args[1]) + if size == [-1]: + # FSDP's flat-buffer view; unused for the subclass-extension all-gather path. + return _rebuild( + x, + x.qdata.reshape(1, -1), + x.scale.reshape(1, -1), + block_size=[1, x.scale.numel()], + ) + sc_size = [size[i] // x.block_size[i] for i in range(len(size))] + return _rebuild(x, func(x.qdata, size), func(x.scale, sc_size)) + + @implements([aten.as_strided.default]) + def _as_strided(func, types, args, kwargs): + x = args[0] + qs, ss = list(x.qdata.shape), list(x.scale.shape) + qd = func(x.qdata, qs, _cstride(qs), 0) + sc = func(x.scale, ss, _cstride(ss), 0) + return _rebuild(x, qd, sc) + + @implements([aten.copy_.default]) + def _copy_(func, types, args, kwargs): + dst, src = args[0], args[1] + func(dst.qdata, src.qdata) + func(dst.scale, src.scale) + return dst + + def fsdp_pre_all_gather( + self, mesh, outer_size=None, outer_stride=None, module=None, mp_policy=None + ): + return (self.qdata, self.scale), (tuple(self.block_size),) + + def fsdp_post_all_gather( + self, all_gather_outputs, metadata, param_dtype, *, out=None + ): + (block_size,) = metadata + qdata, scale = all_gather_outputs + if out is not None: + out.qdata, out.scale = qdata, scale + return + return _rebuild(self, qdata, scale, list(block_size)), all_gather_outputs + + Float8Tensor.fsdp_pre_all_gather = fsdp_pre_all_gather + Float8Tensor.fsdp_post_all_gather = fsdp_post_all_gather + + _PATCHED = True + LOG.info( + "Installed FSDP2 support (split + all-gather hooks) on torchao Float8Tensor" + ) diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2.py b/src/axolotl/monkeypatch/accelerate/fsdp2.py index 531f8cb2f8..a51e2f590e 100644 --- a/src/axolotl/monkeypatch/accelerate/fsdp2.py +++ b/src/axolotl/monkeypatch/accelerate/fsdp2.py @@ -2,6 +2,7 @@ monkeypatch for accelerate fsdp2 fix when modifying ordereddict during interation, and saving full state dicts """ +import contextlib import copy import functools import gc @@ -435,17 +436,50 @@ def fsdp2_prepare_model(accelerator, model: torch.nn.Module) -> torch.nn.Module: fully_shard_kwargs=fsdp2_kwargs, ) - if auto_wrap_policy is not None: - for module in get_module_children_bottom_up(model)[:-1]: - if is_peft_model and isinstance(module, LoraLayer): - module_log_bias_mismatch = _process_lora_module_for_fsdp( - module, fsdp2_kwargs - ) - log_bias_dtype_mismatch |= module_log_bias_mismatch - if auto_wrap_policy(module) and not isinstance(module, FSDPModule): - fully_shard(module, **fsdp2_kwargs) + # Pre-quantized / mixed-dtype models (e.g. an NVFP4 checkpoint loaded for LoRA) carry + # non-float Parameters and keep-fp32 modules that the generic FSDP2 path can't shard + # uniformly. Engage the quantized capability path ONLY when such params exist; pure-bf16 + # models take the original generic path unchanged. The nonfloat ``nn.Parameter.__new__`` + # patch is process-global and is restored via the context manager's ``finally`` (an + # exception in ``fully_shard`` can no longer leave it patched). + from axolotl.monkeypatch.accelerate.fsdp2_quantized import ( + cast_residual_fp32, + model_has_float_logical_quantized_params, + model_has_nonfloat_params, + nonfloat_param_guard, + shard_fp32_modules, + ) + + # Apply the quantized dtype/cast/sharding policy ONLY for float-logical torchao subclasses + # (NVFP4Tensor/Float8Tensor/MXTensor) — the pre-quantized checkpoint case this path is for. + # Plain bnb Params4bit QLoRA is excluded so cast_residual_fp32 does not downcast its fp32 LoRA. + # The nn.Parameter.__new__ guard is separate: it is needed for ANY plain non-float param (uint8 + # packed, which includes bnb Params4bit) and stays gated on that. + _quantized = model_has_float_logical_quantized_params(model) + _needs_nonfloat_guard = model_has_nonfloat_params(model) + _guard = ( + nonfloat_param_guard(model) + if _needs_nonfloat_guard + else contextlib.nullcontext() + ) + with _guard: + if _quantized: + # keep-fp32 modules (registered by model adapters, e.g. DSV4 mHC) get their own + # fp32 shard group; remaining plain fp32 (PEFT LoRA) is cast to the compute dtype. + shard_fp32_modules(model, fsdp2_kwargs) + cast_residual_fp32(model) + + if auto_wrap_policy is not None: + for module in get_module_children_bottom_up(model)[:-1]: + if is_peft_model and isinstance(module, LoraLayer): + module_log_bias_mismatch = _process_lora_module_for_fsdp( + module, fsdp2_kwargs + ) + log_bias_dtype_mismatch |= module_log_bias_mismatch + if auto_wrap_policy(module) and not isinstance(module, FSDPModule): + fully_shard(module, **fsdp2_kwargs) - fully_shard(model, **fsdp2_kwargs) + fully_shard(model, **fsdp2_kwargs) if log_bias_dtype_mismatch: LOG.warning( diff --git a/src/axolotl/monkeypatch/accelerate/fsdp2_quantized.py b/src/axolotl/monkeypatch/accelerate/fsdp2_quantized.py new file mode 100644 index 0000000000..3144e4cbc7 --- /dev/null +++ b/src/axolotl/monkeypatch/accelerate/fsdp2_quantized.py @@ -0,0 +1,157 @@ +"""Capability helpers for sharding pre-quantized / mixed-dtype models under FSDP2. + +These are only engaged when a model actually carries non-float (quantized) Parameters — e.g. a +pre-quantized NVFP4 checkpoint loaded for LoRA. They are model-agnostic: model families register +the class names of modules that must stay fp32 (e.g. DeepSeek-V4 mHC) via +:func:`register_fp32_shard_classes`. Pure-bf16 models never touch this path. +""" + +from __future__ import annotations + +import contextlib + +import torch + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +# Class names of modules that must be sharded in their own fp32 group (registered by adapters). +_FP32_SHARD_CLASS_NAMES: set[str] = set() + +# Tensor-subclass names that are quantized but report a logical FLOAT dtype (so torch.is_floating_point +# is True) — e.g. torchao NVFP4Tensor / Float8Tensor / MXTensor. These still need the quantized +# FSDP2 dtype/sharding policy even though they're "floating point". +_QUANT_TENSOR_CLASS_NAMES: set[str] = {"NVFP4Tensor", "Float8Tensor", "MXTensor"} + + +def register_fp32_shard_classes(names) -> None: + """Register module class names that the quantized FSDP2 path keeps in a separate fp32 shard.""" + _FP32_SHARD_CLASS_NAMES.update(names) + + +def register_quantized_tensor_classes(names) -> None: + """Register quantized tensor-subclass names (float-logical, e.g. NVFP4Tensor) for detection.""" + _QUANT_TENSOR_CLASS_NAMES.update(names) + + +def model_has_nonfloat_params(model) -> bool: + """True iff the model carries PLAIN non-float Parameters (e.g. uint8 packed) — the case that + needs the nn.Parameter.__new__ requires_grad guard during sharding.""" + return any(not torch.is_floating_point(p) for p in model.parameters()) + + +def _is_quantized_param(p) -> bool: + if not torch.is_floating_point(p): + return True # plain packed (uint8) quantized data + # torchao tensor subclasses report a logical float dtype; detect by class name (no hard dep). + if type(p).__name__ in _QUANT_TENSOR_CLASS_NAMES: + return True + data = getattr(p, "data", None) + return data is not None and type(data).__name__ in _QUANT_TENSOR_CLASS_NAMES + + +def model_has_quantized_params(model) -> bool: + """Capability check for the quantized dtype/sharding policy: plain non-float params OR float- + logical quantized tensor subclasses (NVFP4Tensor/Float8Tensor/MXTensor).""" + return any(_is_quantized_param(p) for p in model.parameters()) + + +def _is_float_logical_quantized_param(p) -> bool: + """True only for torchao float-logical quantized subclasses (NVFP4Tensor/Float8Tensor/MXTensor) + — the ones the dtype/cast/sharding policy below is designed for. Deliberately excludes plain + non-float params (e.g. bnb Params4bit uint8): standard QLoRA+FSDP2 keeps its fp32 LoRA and must + not be downcast by cast_residual_fp32.""" + if type(p).__name__ in _QUANT_TENSOR_CLASS_NAMES: + return True + data = getattr(p, "data", None) + return data is not None and type(data).__name__ in _QUANT_TENSOR_CLASS_NAMES + + +def model_has_float_logical_quantized_params(model) -> bool: + """Capability check for the dtype/cast/sharding policy: ONLY float-logical quantized tensor + subclasses (the pre-quantized NVFP4/Float8 checkpoint case). Plain bnb Params4bit QLoRA is + excluded so its fp32 LoRA adapters are not silently cast to the compute dtype.""" + return any(_is_float_logical_quantized_param(p) for p in model.parameters()) + + +@contextlib.contextmanager +def nonfloat_param_guard(model): + """Freeze existing non-float params and make ``nn.Parameter`` construction default to + ``requires_grad=False`` for non-float data for the duration of sharding. + + FSDP2's sharded ``nn.Parameter()`` defaults ``requires_grad=True``, which errors on non-float + (uint8) data *before* it copies the original flag. The ``nn.Parameter.__new__`` patch is a + PROCESS-GLOBAL monkeypatch, so it is restored in a ``finally`` — an exception during + ``fully_shard`` can no longer leave the process globally patched. + """ + import torch.nn as nn + + frozen = 0 + for p in model.parameters(): + if not torch.is_floating_point(p) and p.requires_grad: + p.requires_grad_(False) + frozen += 1 + if frozen: + LOG.info( + "fsdp2-quant: froze %d non-float (quantized) params before sharding", frozen + ) + + orig_new = nn.Parameter.__new__ + + def _nonfloat_safe_new(cls, data=None, requires_grad=True): + if data is not None and not torch.is_floating_point(data): + requires_grad = False + return orig_new(cls, data, requires_grad) + + nn.Parameter.__new__ = _nonfloat_safe_new + try: + yield + finally: + nn.Parameter.__new__ = orig_new + + +def shard_fp32_modules(model, fsdp2_kwargs, compute_dtype=torch.bfloat16) -> int: + """Shard registered keep-fp32 modules (e.g. DSV4 mHC) in their own fp32 group: compute in fp32 + (cast inputs up) but emit ``compute_dtype`` outputs so they don't feed fp32 activations into + downstream low-precision layers. No-op if no classes are registered.""" + if not _FP32_SHARD_CLASS_NAMES: + return 0 + from torch.distributed.fsdp import ( + FSDPModule, + MixedPrecisionPolicy, + fully_shard, + ) + + fp32_mp = MixedPrecisionPolicy( + param_dtype=torch.float32, + reduce_dtype=torch.float32, + output_dtype=compute_dtype, + cast_forward_inputs=True, + ) + kwargs = {**fsdp2_kwargs, "mp_policy": fp32_mp} + n = 0 + for m in model.modules(): + if type(m).__name__ in _FP32_SHARD_CLASS_NAMES and not isinstance( + m, FSDPModule + ): + fully_shard(m, **kwargs) + n += 1 + if n: + LOG.info("fsdp2-quant: fp32-sharded %d keep-fp32 modules separately", n) + return n + + +def cast_residual_fp32(model, compute_dtype=torch.bfloat16) -> int: + """Cast remaining plain fp32 float params (e.g. PEFT-upcast LoRA) to ``compute_dtype`` for a + uniform per-shard original dtype. Skips DTensors (already-sharded fp32 groups).""" + from torch.distributed.tensor import DTensor + + n = 0 + for p in model.parameters(): + if p.dtype == torch.float32 and not isinstance(p.data, DTensor): + p.data = p.data.to(compute_dtype) + n += 1 + if n: + LOG.info("fsdp2-quant: cast %d residual fp32 params to %s", n, compute_dtype) + return n diff --git a/src/axolotl/monkeypatch/attention/flash_attn_d512.py b/src/axolotl/monkeypatch/attention/flash_attn_d512.py new file mode 100644 index 0000000000..2b2a64a506 --- /dev/null +++ b/src/axolotl/monkeypatch/attention/flash_attn_d512.py @@ -0,0 +1,491 @@ +"""Triton FlashAttention for head_dim=512 (forward + backward, dense + varlen packing). + +Flash/cuDNN SDPA backends cap at head_dim 256; at 512 PyTorch falls back to the memory-efficient or +math backend (O(S^2), and math materializes the full scores). This kernel fills that gap: a tiled +flash attention that fits head_dim 512 by using small M/N blocks (the d=512 register/SMEM wall) and +runs fwd+bwd. Varlen packing is done in-kernel from position_ids (doc_start = row - position_id), so +no block-diagonal mask tensor is built. Validated bit-accurate (cosine 1.0) vs per-document SDPA and +~2x faster than the SDPA-efficient fallback at head_dim 512 on sm_120. + +Primary use: the Gemma-4 global (full_attention, head_dim=512) layers under sample packing. +""" + +import torch +import triton +import triton.language as tl + +DEV = "cuda" + + +def _fwd_smem_bytes(BLOCK_N, head_dim, num_stages): + """Exact SMEM the forward kernel needs: the q-tile lives in registers; the K and V tiles are + (1+num_stages)-buffered for software pipelining (+~2KB scratch for L/reductions). Calibrated + against compiled kernels: (BN,D)=(32,512) -> 67584 B @ns1, 100352 B @ns2 (clean +32768/stage).""" + return BLOCK_N * head_dim * 2 * (1 + num_stages) + 2048 + + +def _device_smem_limit(): + props = torch.cuda.get_device_properties(torch.cuda.current_device()) + return ( + getattr(props, "shared_memory_per_block_optin", 0) + or props.shared_memory_per_block + ) + + +def _prune_fwd_configs(configs, named_args, **kwargs): + """Drop configs whose estimated SMEM exceeds the device opt-in cap before they ever compile, so + autotuning doesn't pay the compile cost of doomed variants (Triton would also reject them at + compile via OutOfResources; this just skips the attempt). Always keep at least one.""" + # constexpr meta (HEAD_DIM/CAUSAL/VARLEN) arrives via kwargs, runtime args via named_args. + head_dim = kwargs.get("HEAD_DIM", named_args.get("HEAD_DIM")) + if ( + head_dim is None + ): # can't estimate -> let Triton's compile-time OOM check prune instead + return list(configs) + limit = _device_smem_limit() + fit = [ + c + for c in configs + if _fwd_smem_bytes(c.kwargs["BLOCK_N"], head_dim, c.num_stages) <= limit + ] + return fit or [min(configs, key=lambda c: c.num_stages)] + + +@triton.autotune( + configs=[ + triton.Config({"BLOCK_M": 32, "BLOCK_N": 32}, num_warps=4, num_stages=s) + for s in (1, 2, 3) + ], + key=["N_CTX", "HEAD_DIM", "CAUSAL", "VARLEN"], + prune_configs_by={"early_config_prune": _prune_fwd_configs}, +) +@triton.jit +def _fwd( + Q, + K, + V, + sm_scale, + Out, + L, + sqb, + sqh, + sqm, + sqd, + skb, + skh, + skn, + skd, + svb, + svh, + svn, + svd, + sob, + soh, + som, + sod, + POS, + spb, + spn, + H, + N_CTX, + HEAD_DIM: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + CAUSAL: tl.constexpr, + VARLEN: tl.constexpr, +): + start_m = tl.program_id(0) + off_bh = tl.program_id(1) + off_b = off_bh // H + off_h = off_bh % H + qb = Q + off_b * sqb + off_h * sqh + kb = K + off_b * skb + off_h * skh + vb = V + off_b * svb + off_h * svh + offs_m = start_m * BLOCK_M + tl.arange(0, BLOCK_M) + offs_n = tl.arange(0, BLOCK_N) + offs_d = tl.arange(0, HEAD_DIM) + q = tl.load( + qb + offs_m[:, None] * sqm + offs_d[None, :] * sqd, + mask=offs_m[:, None] < N_CTX, + other=0.0, + ) + ds_m = ( + offs_m - tl.load(POS + off_b * spb + offs_m * spn, mask=offs_m < N_CTX, other=0) + if VARLEN + else offs_m * 0 + ) + m_i = tl.full([BLOCK_M], -float("inf"), tl.float32) + l_i = tl.zeros([BLOCK_M], tl.float32) + acc = tl.zeros([BLOCK_M, HEAD_DIM], tl.float32) + n_end = (start_m + 1) * BLOCK_M if CAUSAL else N_CTX + n_start = ( + tl.min(ds_m) // BLOCK_N + ) * BLOCK_N # varlen: skip kv blocks before this q-block's doc + for start_n in range(n_start, n_end, BLOCK_N): + cur = start_n + offs_n + k = tl.load( + kb + cur[None, :] * skn + offs_d[:, None] * skd, + mask=cur[None, :] < N_CTX, + other=0.0, + ) + qk = tl.dot(q, k) * sm_scale + mask = cur[None, :] < N_CTX + if CAUSAL: + mask = mask & (offs_m[:, None] >= cur[None, :]) + if VARLEN: + mask = mask & (cur[None, :] >= ds_m[:, None]) + qk = tl.where(mask, qk, -1.0e9) + m_new = tl.maximum(m_i, tl.max(qk, 1)) + p = tl.exp(qk - m_new[:, None]) + alpha = tl.exp(m_i - m_new) + l_i = l_i * alpha + tl.sum(p, 1) + v = tl.load( + vb + cur[:, None] * svn + offs_d[None, :] * svd, + mask=cur[:, None] < N_CTX, + other=0.0, + ) + acc = acc * alpha[:, None] + tl.dot(p.to(v.dtype), v) + m_i = m_new + acc = acc / l_i[:, None] + tl.store( + Out + off_b * sob + off_h * soh + offs_m[:, None] * som + offs_d[None, :] * sod, + acc.to(Out.dtype.element_ty), + mask=offs_m[:, None] < N_CTX, + ) + tl.store(L + off_bh * N_CTX + offs_m, m_i + tl.log(l_i), mask=offs_m < N_CTX) + + +@triton.jit +def _bwd_pre( + O, # noqa: E741 + DO, + Delta, + sob, + soh, + som, + sod, + H, + N_CTX, + HEAD_DIM: tl.constexpr, + BLOCK_M: tl.constexpr, +): + start_m = tl.program_id(0) + off_bh = tl.program_id(1) + off_b = off_bh // H + off_h = off_bh % H + offs_m = start_m * BLOCK_M + tl.arange(0, BLOCK_M) + offs_d = tl.arange(0, HEAD_DIM) + base = off_b * sob + off_h * soh + offs_m[:, None] * som + offs_d[None, :] * sod + o = tl.load(O + base, mask=offs_m[:, None] < N_CTX, other=0.0) + do = tl.load(DO + base, mask=offs_m[:, None] < N_CTX, other=0.0) + tl.store( + Delta + off_bh * N_CTX + offs_m, + tl.sum(o.to(tl.float32) * do.to(tl.float32), 1), + mask=offs_m < N_CTX, + ) + + +@triton.jit +def _bwd_dkdv( + Q, + K, + V, + sm_scale, + DO, + DK, + DV, + L, + D, + POS, + DOC_END, + spb, + spn, + sqb, + sqh, + sqm, + sqd, + H, + N_CTX, + HEAD_DIM: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + CAUSAL: tl.constexpr, + VARLEN: tl.constexpr, +): + start_n = tl.program_id(0) + off_bh = tl.program_id(1) + off_b = off_bh // H + off_h = off_bh % H + base = off_b * sqb + off_h * sqh + offs_n = start_n * BLOCK_N + tl.arange(0, BLOCK_N) + offs_d = tl.arange(0, HEAD_DIM) + offs_m = tl.arange(0, BLOCK_M) + k = tl.load( + K + base + offs_n[:, None] * sqm + offs_d[None, :] * sqd, + mask=offs_n[:, None] < N_CTX, + other=0.0, + ) + v = tl.load( + V + base + offs_n[:, None] * sqm + offs_d[None, :] * sqd, + mask=offs_n[:, None] < N_CTX, + other=0.0, + ) + dk = tl.zeros([BLOCK_N, HEAD_DIM], tl.float32) + dv = tl.zeros([BLOCK_N, HEAD_DIM], tl.float32) + m_start = start_n * BLOCK_N if CAUSAL else 0 + # varlen: queries beyond this kv-block's document don't attend -> stop at its doc end + last_k = tl.minimum(start_n * BLOCK_N + BLOCK_N - 1, N_CTX - 1) + m_end = tl.load(DOC_END + off_b * spb + last_k * spn) if VARLEN else N_CTX + for start_m in range(m_start, m_end, BLOCK_M): + cur_m = start_m + offs_m + q = tl.load( + Q + base + cur_m[:, None] * sqm + offs_d[None, :] * sqd, + mask=cur_m[:, None] < N_CTX, + other=0.0, + ) + do = tl.load( + DO + base + cur_m[:, None] * sqm + offs_d[None, :] * sqd, + mask=cur_m[:, None] < N_CTX, + other=0.0, + ) + l = tl.load(L + off_bh * N_CTX + cur_m, mask=cur_m < N_CTX, other=0.0) # noqa: E741 + delta = tl.load(D + off_bh * N_CTX + cur_m, mask=cur_m < N_CTX, other=0.0) + s = tl.dot(q, tl.trans(k)) * sm_scale + mask = (cur_m[:, None] < N_CTX) & (offs_n[None, :] < N_CTX) + if CAUSAL: + mask = mask & (cur_m[:, None] >= offs_n[None, :]) + if VARLEN: + ds_m = cur_m - tl.load( + POS + off_b * spb + cur_m * spn, mask=cur_m < N_CTX, other=0 + ) + mask = mask & (offs_n[None, :] >= ds_m[:, None]) + p = tl.where(mask, tl.exp(s - l[:, None]), 0.0) + dv += tl.dot(tl.trans(p).to(do.dtype), do) + dp = tl.dot(do, tl.trans(v)) + ds = (p * (dp - delta[:, None]) * sm_scale).to(q.dtype) + dk += tl.dot(tl.trans(ds), q) + tl.store( + DK + base + offs_n[:, None] * sqm + offs_d[None, :] * sqd, + dk.to(DK.dtype.element_ty), + mask=offs_n[:, None] < N_CTX, + ) + tl.store( + DV + base + offs_n[:, None] * sqm + offs_d[None, :] * sqd, + dv.to(DV.dtype.element_ty), + mask=offs_n[:, None] < N_CTX, + ) + + +@triton.jit +def _bwd_dq( + Q, + K, + V, + sm_scale, + DO, + DQ, + L, + D, + POS, + spb, + spn, + sqb, + sqh, + sqm, + sqd, + H, + N_CTX, + HEAD_DIM: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + CAUSAL: tl.constexpr, + VARLEN: tl.constexpr, +): + start_m = tl.program_id(0) + off_bh = tl.program_id(1) + off_b = off_bh // H + off_h = off_bh % H + base = off_b * sqb + off_h * sqh + offs_m = start_m * BLOCK_M + tl.arange(0, BLOCK_M) + offs_d = tl.arange(0, HEAD_DIM) + offs_n = tl.arange(0, BLOCK_N) + q = tl.load( + Q + base + offs_m[:, None] * sqm + offs_d[None, :] * sqd, + mask=offs_m[:, None] < N_CTX, + other=0.0, + ) + do = tl.load( + DO + base + offs_m[:, None] * sqm + offs_d[None, :] * sqd, + mask=offs_m[:, None] < N_CTX, + other=0.0, + ) + l = tl.load(L + off_bh * N_CTX + offs_m, mask=offs_m < N_CTX, other=0.0) # noqa: E741 + delta = tl.load(D + off_bh * N_CTX + offs_m, mask=offs_m < N_CTX, other=0.0) + ds_m = ( + offs_m - tl.load(POS + off_b * spb + offs_m * spn, mask=offs_m < N_CTX, other=0) + if VARLEN + else offs_m * 0 + ) + dq = tl.zeros([BLOCK_M, HEAD_DIM], tl.float32) + n_end = (start_m + 1) * BLOCK_M if CAUSAL else N_CTX + n_start = (tl.min(ds_m) // BLOCK_N) * BLOCK_N + for start_n in range(n_start, n_end, BLOCK_N): + cur = start_n + offs_n + k = tl.load( + K + base + cur[:, None] * sqm + offs_d[None, :] * sqd, + mask=cur[:, None] < N_CTX, + other=0.0, + ) + v = tl.load( + V + base + cur[:, None] * sqm + offs_d[None, :] * sqd, + mask=cur[:, None] < N_CTX, + other=0.0, + ) + s = tl.dot(q, tl.trans(k)) * sm_scale + mask = (offs_m[:, None] < N_CTX) & (cur[None, :] < N_CTX) + if CAUSAL: + mask = mask & (offs_m[:, None] >= cur[None, :]) + if VARLEN: + mask = mask & (cur[None, :] >= ds_m[:, None]) + p = tl.where(mask, tl.exp(s - l[:, None]), 0.0) + dp = tl.dot(do, tl.trans(v)) + ds = (p * (dp - delta[:, None]) * sm_scale).to(k.dtype) + dq += tl.dot(ds, k) + tl.store( + DQ + base + offs_m[:, None] * sqm + offs_d[None, :] * sqd, + dq.to(DQ.dtype.element_ty), + mask=offs_m[:, None] < N_CTX, + ) + + +class _FlashD512(torch.autograd.Function): + @staticmethod + def forward(ctx, q, k, v, causal, position_ids, scale=None): + # The backward kernels launch with q's strides only and reuse them for k/v/do/dk/dv. In real + # attention q is non-contiguous ([B,S,H,D].transpose(1,2)) while k/v are contiguous (e.g. GQA + # repeat_interleave); the mismatched strides make the backward read wrong memory and explode + # the gradient (forward is unaffected -- it passes each tensor's own strides). Force one layout. + q, k, v = q.contiguous(), k.contiguous(), v.contiguous() + B, H, N, D = q.shape + VARLEN = position_ids is not None + if position_ids is None: + pos = torch.zeros((B, N), device=q.device, dtype=torch.int32) + else: + pos = ( + (position_ids if position_ids.dim() > 1 else position_ids[None]) + .to(torch.int32) + .contiguous() + ) + spb, spn = pos.stride() + if VARLEN: + doc_end = torch.empty_like(pos) + for b in range(B): + starts = (pos[b] == 0).nonzero().flatten() + bounds = torch.cat( + [starts, torch.tensor([N], device=pos.device, dtype=starts.dtype)] + ) + doc_end[b] = ( + bounds[1:].repeat_interleave(bounds[1:] - bounds[:-1]).to(pos.dtype) + ) + else: + doc_end = pos + o = torch.empty_like(q) + L = torch.empty((B * H, N), device=q.device, dtype=torch.float32) + scale = D**-0.5 if scale is None else float(scale) + # BLOCK_M/BLOCK_N, num_warps, num_stages come from the autotuner (SMEM-pruned per device). + _fwd[lambda meta: (triton.cdiv(N, meta["BLOCK_M"]), B * H)]( + q, + k, + v, + scale, + o, + L, + *q.stride(), + *k.stride(), + *v.stride(), + *o.stride(), + pos, + spb, + spn, + H, + N, + HEAD_DIM=D, + CAUSAL=causal, + VARLEN=VARLEN, + ) + ctx.save_for_backward(q, k, v, o, L, pos, doc_end) + ctx.causal = causal + ctx.scale = scale + ctx.varlen = VARLEN + return o + + @staticmethod + def backward(ctx, do): + q, k, v, o, L, pos, doc_end = ctx.saved_tensors + B, H, N, D = q.shape + causal = ctx.causal + scale = ctx.scale + VARLEN = ctx.varlen + spb, spn = pos.stride() + do = do.contiguous() + dq = torch.empty_like(q) + dk = torch.empty_like(k) + dv = torch.empty_like(v) + delta = torch.empty((B * H, N), device=q.device, dtype=torch.float32) + BM, BN = 16, 32 + _bwd_pre[(triton.cdiv(N, BM), B * H)]( + o, do, delta, *o.stride(), H, N, HEAD_DIM=D, BLOCK_M=BM, num_warps=4 + ) + _bwd_dkdv[(triton.cdiv(N, BN), B * H)]( + q, + k, + v, + scale, + do, + dk, + dv, + L, + delta, + pos, + doc_end, + spb, + spn, + *q.stride(), + H, + N, + HEAD_DIM=D, + BLOCK_M=BM, + BLOCK_N=BN, + CAUSAL=causal, + VARLEN=VARLEN, + num_warps=4, + num_stages=1, + ) + _bwd_dq[(triton.cdiv(N, BM), B * H)]( + q, + k, + v, + scale, + do, + dq, + L, + delta, + pos, + spb, + spn, + *q.stride(), + H, + N, + HEAD_DIM=D, + BLOCK_M=BM, + BLOCK_N=BN, + CAUSAL=causal, + VARLEN=VARLEN, + num_warps=4, + num_stages=1, + ) + return dq, dk, dv, None, None, None + + +def flash_d512(q, k, v, causal=True, position_ids=None, scale=None): + return _FlashD512.apply(q, k, v, causal, position_ids, scale) diff --git a/src/axolotl/monkeypatch/attention/large_head.py b/src/axolotl/monkeypatch/attention/large_head.py new file mode 100644 index 0000000000..325a548d8b --- /dev/null +++ b/src/axolotl/monkeypatch/attention/large_head.py @@ -0,0 +1,123 @@ +"""Generic large-head-dim attention capability (head_dim > 256). + +Flash/cuDNN SDPA cap at head_dim 256; at larger head_dim PyTorch falls to the memory-efficient or +math backend. The Triton ``flash_d512`` kernel fills that gap. This module exposes the routing as a +MODEL-AGNOSTIC capability: any attention path can call :func:`flash_d512_route`, and a generic +``sdpa`` wrapper (:func:`patch_sdpa_large_head`) lets plain SDPA models opt in via the +``large_head_attention`` config. Gemma-4's hybrid global layers reuse the same router. + +Policy (``large_head_attention``): + - ``sdpa`` : never use the Triton kernel (stock SDPA at large head_dim) + - ``auto`` : Triton flash for genuinely packed rows (its proven win), SDPA otherwise + (single-document large-head attention is faster on SDPA is_causal) + - ``triton_flash`` : prefer the Triton kernel for head_dim > 256 whenever possible +""" + +from __future__ import annotations + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +_LARGE_HEAD_MIN_DIM = 256 +_POLICY = "sdpa" +_SDPA_ORIG_ATTR = "_axolotl_large_head_sdpa_original" + + +def set_large_head_policy(policy: str | None) -> None: + global _POLICY + _POLICY = str(policy).lower() if policy else "sdpa" + + +def get_large_head_policy() -> str: + return _POLICY + + +def resolve_large_head_policy(cfg) -> str: + """Resolve the intent ``large_head_attention`` (auto/sdpa/triton_flash); fall back to the + deprecated ``flash_attn_d512`` bool (True -> auto, since flash only wins on packed rows).""" + policy = cfg.get("large_head_attention") + if policy: + return str(policy).lower() + if cfg.get("flash_attn_d512"): + return "auto" + return "sdpa" + + +def _multidoc_position_ids(position_ids): + """Return [B,S] position_ids iff they encode genuine (multi-document) packing, else None.""" + if position_ids is None: + return None + p = position_ids if position_ids.dim() > 1 else position_ids[None] + return p if int((p == 0).sum()) > p.shape[0] else None + + +def flash_d512_route(module, query, key, value, scaling, position_ids, policy=None): + """Route a large-head attention call through the Triton flash_d512 kernel, or return None to + signal the caller to fall back to SDPA. Inputs are ``[B, H, S, D]``; on success returns + ``(attn_output [B, S, Hq, D], None)`` matching ``sdpa_attention_forward``'s contract.""" + policy = policy or _POLICY + # Allowlist: only the Triton policies route to the kernel, so a config typo can't enable it. + if policy not in ("auto", "triton_flash") or query.shape[-1] <= _LARGE_HEAD_MIN_DIM: + return None + pid = _multidoc_position_ids(position_ids) + # auto: kernel only for packed rows (single-doc large-head is faster on SDPA is_causal) + if policy == "auto" and pid is None: + return None + try: + from axolotl.monkeypatch.attention.flash_attn_d512 import flash_d512 + + ng = getattr(module, "num_key_value_groups", query.shape[1] // key.shape[1]) + k = key.repeat_interleave(ng, dim=1) if ng > 1 else key + v = value.repeat_interleave(ng, dim=1) if ng > 1 else value + out = flash_d512(query, k, v, True, position_ids=pid, scale=scaling) + return out.transpose(1, 2).contiguous(), None + except Exception: # pragma: no cover - any kernel issue falls back to SDPA + return None + + +def patch_sdpa_large_head(policy: str | None = None) -> bool: + """Wrap the ``sdpa`` attention interface so head_dim>256 maskless calls route through the Triton + kernel per ``policy`` (idempotent). Generic — any SDPA model opts in via config; explicit + attention masks always fall through to stock SDPA.""" + if policy is not None: + set_large_head_policy(policy) + if get_large_head_policy() == "sdpa": + return False + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + current = ALL_ATTENTION_FUNCTIONS["sdpa"] + if getattr(current, _SDPA_ORIG_ATTR, None) is not None: + return True + original = current + + def sdpa_large_head_forward(module, query, key, value, attention_mask, **kwargs): + if attention_mask is None: + routed = flash_d512_route( + module, + query, + key, + value, + kwargs.get("scaling"), + kwargs.get("position_ids"), + ) + if routed is not None: + return routed + return original(module, query, key, value, attention_mask, **kwargs) + + setattr(sdpa_large_head_forward, _SDPA_ORIG_ATTR, original) + ALL_ATTENTION_FUNCTIONS.register("sdpa", sdpa_large_head_forward) + LOG.info( + "large_head_attention: wrapped sdpa to route head_dim>256 through Triton flash (%s)", + get_large_head_policy(), + ) + return True + + +def unpatch_sdpa_large_head() -> None: + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + current = ALL_ATTENTION_FUNCTIONS["sdpa"] + original = getattr(current, _SDPA_ORIG_ATTR, None) + if original is not None: + ALL_ATTENTION_FUNCTIONS.register("sdpa", original) diff --git a/src/axolotl/monkeypatch/attention/sdpa_varlen.py b/src/axolotl/monkeypatch/attention/sdpa_varlen.py new file mode 100644 index 0000000000..be17deb9df --- /dev/null +++ b/src/axolotl/monkeypatch/attention/sdpa_varlen.py @@ -0,0 +1,186 @@ +"""Variable-length (cu_seqlens) SDPA path for sample packing. + +With sample packing the model concatenates many documents into one row and +encodes the boundaries in ``position_ids`` (which reset to 0 at each document +start). The default SDPA path turns this into an explicit 4D block-diagonal +mask: O(S^2) compute even though cross-document blocks are masked out, plus the +mask tensor itself. + +When PyTorch exposes ``torch.nn.attention.varlen.varlen_attn`` (>= 2.10) and the +head_dim is within Flash-Attention's limit (<= 256), we can instead run the +attention as variable-length with ``cu_seqlens`` derived from ``position_ids``, +which skips the cross-document blocks entirely — faster and lower memory — with +no dependency on the ``flash_attn`` package. This is opt-in via ``sdpa_varlen: +true`` and only activates for genuinely multi-document (packed) rows; everything +else falls back to the stock SDPA implementation. +""" + +from __future__ import annotations + +from typing import Any, Callable + +from axolotl.utils.logging import get_logger + +LOG = get_logger(__name__) + +_PATCH_APPLIED = False +# head_dim limit of the Flash-Attention kernel backing varlen_attn. +_VARLEN_MAX_HEAD_DIM = 256 + + +def varlen_available() -> bool: + try: + from torch.nn.attention.varlen import varlen_attn # noqa: F401 + except ImportError: + return False + return True + + +def _build_varlen_forward(original_sdpa: Callable) -> Callable: + import inspect + + import torch + from torch.nn.attention.varlen import varlen_attn + from transformers.modeling_flash_attention_utils import ( + prepare_fa_kwargs_from_position_ids, + ) + + # varlen_attn's causal API differs across the supported torch range (>=2.10): torch 2.11 takes + # window_size (causal = (-1, 0): unlimited left, no right; sliding = (W-1, 0)), earlier builds + # take is_causal (causal only, no sliding). Detect which the installed build accepts. Scale stays + # default (1/sqrt(d)) — the use_varlen guard below already restricts to standard scaling. + try: + _varlen_params = set(inspect.signature(varlen_attn).parameters) + except (TypeError, ValueError): + _varlen_params = set() + # window_size present -> use it; only an is_causal-only build lacks sliding support. + _supports_window = ( + "window_size" in _varlen_params or "is_causal" not in _varlen_params + ) + + def sdpa_varlen_forward( + module: Any, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + attention_mask: torch.Tensor | None, + dropout: float = 0.0, + scaling: float | None = None, + **kwargs: Any, + ): + position_ids = kwargs.get("position_ids") + sliding_window = kwargs.get("sliding_window", None) or getattr( + module, "sliding_window", None + ) + head_dim = query.shape[-1] + # Fast-path conditions; anything else falls back to stock SDPA. + # - attention_mask must be None: packing carries structure via position_ids; a real mask + # (e.g. left padding) isn't expressible to the causal/sliding varlen kernel here. + # - dropout unsupported by varlen_attn. + # - head_dim within the Flash limit. + # - scaling: varlen_attn only applies 1/sqrt(head_dim); a custom scale can't be honored. + standard_scale = scaling is None or abs(scaling - head_dim**-0.5) < 1e-9 + use_varlen = ( + attention_mask is None + and not dropout + and head_dim <= _VARLEN_MAX_HEAD_DIM + and position_ids is not None + and standard_scale + ) + if use_varlen: + pid = position_ids if position_ids.dim() > 1 else position_ids[None] + # genuine packing only (more document starts than rows); single-doc -> stock SDPA. + use_varlen = int((pid == 0).sum()) > pid.shape[0] + if not use_varlen: + return original_sdpa( + module, + query, + key, + value, + attention_mask, + dropout=dropout, + scaling=scaling, + **kwargs, + ) + + # A sliding window needs varlen_attn's window_size arg; an is_causal-only build can't express + # it, so refuse loudly there rather than silently running full causal attention (wrong). + if sliding_window and not _supports_window: + raise NotImplementedError( + "sdpa_varlen: sliding-window attention needs varlen_attn(window_size=...), absent in " + f"this torch build (requested window={sliding_window}); disable sdpa_varlen for this model." + ) + + B, Hq, S, D = query.shape + Hkv = key.shape[1] + if Hq != Hkv: # GQA -> repeat (varlen_attn has no GQA mode) + n = Hq // Hkv + key = key.repeat_interleave(n, dim=1) + value = value.repeat_interleave(n, dim=1) + (cu_q, cu_k), (max_q, max_k) = prepare_fa_kwargs_from_position_ids(pid) + qf = query.transpose(1, 2).reshape(B * S, Hq, D) + kf = key.transpose(1, 2).reshape(B * S, Hq, D) + vf = value.transpose(1, 2).reshape(B * S, Hq, D) + if _supports_window: + # (left, right): (-1, 0) = causal full; (W-1, 0) = causal sliding window of W. + window = (sliding_window - 1, 0) if sliding_window else (-1, 0) + causal_kw: dict = {"window_size": window} + else: + causal_kw = { + "is_causal": True + } # is_causal-only build (sliding already refused above) + out = varlen_attn( + qf, + kf, + vf, + cu_q.to(torch.int32), + cu_k.to(torch.int32), + int(max_q), + int(max_k), + **causal_kw, + ) + if isinstance(out, tuple): + out = out[0] + # match sdpa_attention_forward's return contract: (attn_output [B,S,Hq,D], None) + return out.reshape(B, S, Hq, D), None + + return sdpa_varlen_forward + + +def patch_sdpa_varlen() -> bool: + """Replace the registered ``sdpa`` attention with a varlen-aware wrapper (idempotent).""" + global _PATCH_APPLIED + if _PATCH_APPLIED: + return True + if not varlen_available(): + LOG.warning( + "sdpa_varlen: torch.nn.attention.varlen.varlen_attn unavailable (needs torch >= 2.10); " + "leaving stock SDPA in place." + ) + return False + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + original = ALL_ATTENTION_FUNCTIONS["sdpa"] + wrapper = _build_varlen_forward(original) + wrapper._axolotl_sdpa_original = original # type: ignore[attr-defined] + ALL_ATTENTION_FUNCTIONS.register("sdpa", wrapper) + _PATCH_APPLIED = True + LOG.info( + "sdpa_varlen: patched 'sdpa' to use cu_seqlens varlen_attn for packed rows " + "(head_dim <= %d), falling back to stock SDPA otherwise", + _VARLEN_MAX_HEAD_DIM, + ) + return True + + +def unpatch_sdpa_varlen() -> None: + global _PATCH_APPLIED + if not _PATCH_APPLIED: + return + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + current = ALL_ATTENTION_FUNCTIONS["sdpa"] + original = getattr(current, "_axolotl_sdpa_original", None) + if original is not None: + ALL_ATTENTION_FUNCTIONS.register("sdpa", original) + _PATCH_APPLIED = False diff --git a/src/axolotl/monkeypatch/gemma4_hybrid_mask.py b/src/axolotl/monkeypatch/gemma4_hybrid_mask.py index 2272291a93..0e47eacc4f 100644 --- a/src/axolotl/monkeypatch/gemma4_hybrid_mask.py +++ b/src/axolotl/monkeypatch/gemma4_hybrid_mask.py @@ -47,6 +47,87 @@ _PATCH_APPLIED = False +# Attention-interface name for Gemma-4 global (full_attention) layers. They have head_dim=512, so FA2 +# can't serve them and the model (FA2 at the top level) hands them no mask, relying on cu_seqlens that +# only the FA2 sliding layers consume. With sample packing that means the global layers would attend +# ACROSS document boundaries (pure causal). This impl rebuilds the block-diagonal-causal mask from +# position_ids so the globals respect doc boundaries, on the memory-efficient SDPA backend. +GLOBAL_PACKED_SDPA = "sdpa_global_packed" + + +# When set, the head_dim=512 global layers use the Triton flash_d512 kernel (fwd+bwd, varlen) instead +# of the SDPA efficient backend (~2x faster at head_dim 512). Set from cfg.flash_attn_d512. +def set_flash_d512(enabled: bool) -> None: + """Backwards-compat shim: the head_dim>256 routing is now the generic large_head_attention + capability. True -> 'auto' (flash only on packed rows, the proven win).""" + from axolotl.monkeypatch.attention.large_head import set_large_head_policy + + set_large_head_policy("auto" if enabled else "sdpa") + + +def _packing_block_causal_mask(position_ids, dtype, device): + """Block-diagonal causal additive mask [B,1,S,S] from packed position_ids (which reset to 0 at + each document start). -inf across document boundaries and for non-causal positions, 0 elsewhere.""" + import torch + + if position_ids.dim() == 1: + position_ids = position_ids[None] + Bz, Sz = position_ids.shape + doc = (position_ids == 0).cumsum(-1) # [B,S] document index (1-based) + same_doc = doc[:, :, None] == doc[:, None, :] # [B,S,S] + causal = torch.ones(Sz, Sz, dtype=torch.bool, device=device).tril()[None] # [1,S,S] + allow = same_doc & causal + mask = torch.zeros(Bz, 1, Sz, Sz, dtype=dtype, device=device) + mask.masked_fill_(~allow[:, None], torch.finfo(dtype).min) + return mask + + +def _register_global_packed_sdpa() -> None: + """Register the packing-aware global-layer attention impl (block-diagonal mask + efficient SDPA).""" + from transformers.integrations.sdpa_attention import sdpa_attention_forward + from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS + + if GLOBAL_PACKED_SDPA in ALL_ATTENTION_FUNCTIONS.valid_keys(): + return + + def sdpa_global_packed_forward(module, query, key, value, attention_mask, **kwargs): + # The top-level FA2 path leaves these layers maskless and carries packing only via cu_seqlens + # (consumed by FA2, not SDPA). Rebuild the block-diagonal mask from position_ids so the global + # layers don't cross document boundaries. Single-document rows stay maskless (is_causal). + from axolotl.monkeypatch.attention.large_head import flash_d512_route + + position_ids = kwargs.get("position_ids") + # The generic large-head router takes head_dim>256 packed rows through the Triton flash + # kernel (per the large_head_attention policy); ~2.7x the 4D-mask SDPA, ~3x less memory than + # nested-tensor SDPA. It declines (returns None) for single-doc/policy=sdpa -> SDPA below. + if attention_mask is None: + routed = flash_d512_route( + module, query, key, value, kwargs.get("scaling"), position_ids + ) + if routed is not None: + return routed + # Detect genuine (multi-document) packing: more doc-starts than batch rows. + pid = None + if attention_mask is None and position_ids is not None: + p = position_ids if position_ids.dim() > 1 else position_ids[None] + if int((p == 0).sum()) > p.shape[0]: + pid = p + # Packed without the kernel -> block-diagonal mask so globals respect doc boundaries. + # Single-document -> mask stays None (SDPA is_causal, the fast path). + if pid is not None: + attention_mask = _packing_block_causal_mask(pid, query.dtype, query.device) + return sdpa_attention_forward( + module, query, key, value, attention_mask, **kwargs + ) + + ALL_ATTENTION_FUNCTIONS.register(GLOBAL_PACKED_SDPA, sdpa_global_packed_forward) + LOG.info( + "gemma4_hybrid_mask: registered '%s' (block-diagonal packing mask for head_dim=512 global " + "layers so they respect document boundaries under sample packing)", + GLOBAL_PACKED_SDPA, + ) + + # Each Gemma 4 variant fully redefines ``create_causal_mask`` in its own module # namespace (gemma4_unified does NOT modular-import from gemma4), so both must be # patched independently. @@ -99,6 +180,40 @@ def hybrid_create_causal_mask(config: Any, *args: Any, **kwargs: Any): return True +def _patch_use_gqa_head_dim_guard() -> bool: + """Stop ``enable_gqa`` from forcing the MATH SDPA backend on large-head-dim layers. + + ``sdpa_attention_forward`` enables ``enable_gqa=True`` whenever ``attention_mask is None`` + (``use_gqa_in_sdpa``), with no head_dim check. But SDPA's flash/efficient GQA path only + supports head_dim <= 256; at head_dim > 256 ``enable_gqa`` silently falls back to the MATH + kernel, which materializes the full [H, S, S] scores. Repeating KV instead keeps the + memory-efficient backend. For Gemma-4's head_dim=512 global layers this is ~2.9 GiB -> ~0.2 GiB + per layer with identical math (repeat_kv == GQA). + """ + try: + import transformers.integrations.sdpa_attention as sdpa_mod + except ImportError: + return False + original = sdpa_mod.use_gqa_in_sdpa + if getattr(original, "_axolotl_head_dim_guarded", False): + return True + + def use_gqa_in_sdpa_guarded(attention_mask, key): + # head_dim > 256 -> enable_gqa drops to the MATH backend; force repeat_kv (efficient) instead. + if key.shape[-1] > 256: + return False + return original(attention_mask, key) + + use_gqa_in_sdpa_guarded._axolotl_head_dim_guarded = True # type: ignore[attr-defined] + use_gqa_in_sdpa_guarded._axolotl_original = original # type: ignore[attr-defined] + sdpa_mod.use_gqa_in_sdpa = use_gqa_in_sdpa_guarded + LOG.info( + "gemma4_hybrid_mask: guarded use_gqa_in_sdpa (head_dim>256 -> repeat_kv, not enable_gqa) " + "to keep the memory-efficient SDPA backend on head_dim=512 global layers" + ) + return True + + def patch_gemma4_hybrid_mask() -> bool: """Install the Gemma 4 hybrid-attention mask fix across all variants. @@ -125,9 +240,15 @@ def patch_gemma4_hybrid_mask() -> bool: if _patch_module_create_causal_mask(module): patched_any = True - if patched_any: - _PATCH_APPLIED = True - return patched_any + if not patched_any: + return False + + # Only touch global SDPA state once we know a Gemma4 namespace was actually patched — + # otherwise _PATCH_APPLIED stays False and unpatch() would skip cleaning these up. + _patch_use_gqa_head_dim_guard() + _register_global_packed_sdpa() + _PATCH_APPLIED = True + return True def unpatch_gemma4_hybrid_mask() -> None: @@ -135,6 +256,15 @@ def unpatch_gemma4_hybrid_mask() -> None: global _PATCH_APPLIED if not _PATCH_APPLIED: return + try: + import transformers.integrations.sdpa_attention as sdpa_mod + + guarded = getattr(sdpa_mod, "use_gqa_in_sdpa", None) + original = getattr(guarded, "_axolotl_original", None) + if original is not None: + sdpa_mod.use_gqa_in_sdpa = original + except ImportError: + pass for module_path in _TARGET_MODULES: try: module = importlib.import_module(module_path) diff --git a/src/axolotl/monkeypatch/lora_kernels.py b/src/axolotl/monkeypatch/lora_kernels.py index 05e623047c..9bc97cad5f 100644 --- a/src/axolotl/monkeypatch/lora_kernels.py +++ b/src/axolotl/monkeypatch/lora_kernels.py @@ -352,18 +352,27 @@ def find_self_attn_in_layer( def find_mlp_in_layer( layer: nn.Module, + skip_routed_experts: bool = False, ) -> Generator[Tuple[nn.Module, nn.Module, nn.Module, nn.Module], None, None]: - # general case of most models + # general case of most models: the dense (shared) MLP. Always eligible — a custom MoE expert + # kernel only owns the ROUTED experts, not this dense gate/up/down block (e.g. gemma4's per-layer + # Gemma4TextMLP). Sparse-block MoEs whose layer.mlp is the router (no gate_proj) don't match here. if hasattr(layer, "mlp"): if all( hasattr(layer.mlp, proj) for proj in ["gate_proj", "up_proj", "down_proj"] ): yield layer.mlp.gate_proj, layer.mlp.up_proj, layer.mlp.down_proj, layer.mlp - # llama4 linearized experts + # llama4 shared expert: also a dense MLP, not routed -> always eligible. if hasattr(layer, "feedforward") and hasattr(layer.feedforward, "shared_expert"): mlp = layer.feedforward.shared_expert yield mlp.gate_proj, mlp.up_proj, mlp.down_proj, mlp - if hasattr(layer, "feedforward") and hasattr(layer.feedforward, "experts"): + # llama4 linearized ROUTED experts: skip when a custom MoE expert kernel (ScatterMoE/SonicMoE) + # owns the experts — patching them here would double-own / conflict with the MoE kernel path. + if ( + not skip_routed_experts + and hasattr(layer, "feedforward") + and hasattr(layer.feedforward, "experts") + ): if all( hasattr(layer.feedforward.experts, proj) for proj in ["gate_projs", "up_projs", "down_projs"] @@ -533,7 +542,12 @@ def apply_lora_kernel_patches( LOG.warning_once( "Cannot patch some attention output projection - requires LoRA adapters" ) - for gate_proj, up_proj, down_proj, mlp in find_mlp_in_layer(layer): + # When ScatterMoE/SonicMoE owns the routed experts, lora_mlp_kernel must only fuse the + # DENSE shared MLP, never the routed-expert containers (which the MoE kernel handles). + _moe_kernels_own_experts = bool(cfg.use_scattermoe) or bool(cfg.use_sonicmoe) + for gate_proj, up_proj, down_proj, mlp in find_mlp_in_layer( + layer, skip_routed_experts=_moe_kernels_own_experts + ): if cfg.lora_mlp_kernel: # Check is inside lora_mlp_kernel guard so models with an # unsupported activation (e.g. nemotron_h uses relu2) can set diff --git a/src/axolotl/utils/schemas/config.py b/src/axolotl/utils/schemas/config.py index 54cb2b07a3..7a283d6eab 100644 --- a/src/axolotl/utils/schemas/config.py +++ b/src/axolotl/utils/schemas/config.py @@ -841,6 +841,41 @@ class AxolotlInputConfig( }, ) + large_head_attention: str | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Generic capability for attention layers with head_dim > 256 (which flash/cuDNN " + "SDPA can't serve): 'sdpa' (stock SDPA, default) | 'auto' (Triton flash kernel on " + "packed rows, SDPA otherwise) | 'triton_flash' (prefer the Triton kernel). Applies " + "to any SDPA model; Gemma-4's head_dim=512 global layers use it automatically." + ) + }, + ) + # Deprecated alias for `large_head_attention` (True -> 'auto'); kept for backwards compatibility. + flash_attn_d512: bool | None = Field( + default=None, + json_schema_extra={ + "description": ( + "Deprecated: use `large_head_attention: auto`. Routes head_dim>256 attention " + "through the Triton flash kernel." + ) + }, + ) + + sdpa_varlen: bool | None = Field( + default=None, + json_schema_extra={ + "description": ( + "With sample packing + attn_implementation=sdpa, route packed rows through " + "torch.nn.attention.varlen.varlen_attn (cu_seqlens) instead of an explicit 4D " + "block-diagonal mask. Skips cross-document blocks (faster + lower memory) with no " + "flash_attn dependency. Requires torch >= 2.11 and head_dim <= 256; non-packed rows " + "and larger head_dim fall back to stock SDPA." + ) + }, + ) + fused_attn_kernel: bool | None = Field( default=None, json_schema_extra={ @@ -1497,6 +1532,19 @@ def normalize_attn_implementation(cls, data): return data + @field_validator("large_head_attention", mode="before") + @classmethod + def validate_large_head_attention(cls, value): + """Only the three known policies are valid (a typo must not silently opt into Triton).""" + if value is None: + return None + valid = {"auto", "sdpa", "triton_flash"} + if str(value).lower() not in valid: + raise ValueError( + f"large_head_attention must be one of {sorted(valid)}, got {value!r}" + ) + return str(value).lower() + @field_validator("attn_implementation", mode="before") @classmethod def validate_attn_implementation(cls, value): @@ -1548,6 +1596,22 @@ def check_fp32_norms(self): ) return self + @model_validator(mode="after") + def check_sdpa_varlen(self): + if self.sdpa_varlen: + if not self.sample_packing: + LOG.warning( + "`sdpa_varlen` only affects packed rows; enable `sample_packing` or it is a no-op." + ) + try: + from torch.nn.attention.varlen import varlen_attn # noqa: F401 + except ImportError: + LOG.warning( + "`sdpa_varlen` needs torch >= 2.10 (torch.nn.attention.varlen); it will be " + "ignored and stock SDPA used." + ) + return self + @model_validator(mode="after") def check_sageattn_wo_sample_packing(self): if ( diff --git a/tests/e2e/kernels/test_lora_mlp_geglu_moe_correctness.py b/tests/e2e/kernels/test_lora_mlp_geglu_moe_correctness.py new file mode 100644 index 0000000000..5ac69e5461 --- /dev/null +++ b/tests/e2e/kernels/test_lora_mlp_geglu_moe_correctness.py @@ -0,0 +1,138 @@ +"""Correctness of the fused GeGLU LoRA MLP kernel vs an eager autograd reference. + +This is the path re-enabled for MoE models by ``disable_mlp_kernel`` (the dense *shared* MLP, +e.g. gemma4's per-layer ``Gemma4TextMLP``, gets ``lora_mlp_kernel`` while the routed experts are +handled by the MoE kernel). Verifies forward output, input grad, and all six LoRA A/B grads match +an eager reference — over both a bf16 base and an nf4-quantized base (the low-VRAM gemma4 case), +since the fused kernel dequantizes the base in-kernel. +""" + +import pytest +import torch +import torch.nn.functional as F + +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") + +from axolotl.kernels.lora import LoRA_MLP # noqa: E402 + +try: + import bitsandbytes.functional as bnb_F # noqa: E402 +except Exception: # pragma: no cover + bnb_F = None + +DEV = "cuda" + + +def _geglu_eager(X, gw, gA, gB, uw, uA, uB, dw, dA, dB, s): + """Eager gated-GeGLU MLP with LoRA: down(gelu_tanh(gate(x)) * up(x)), gelu tanh-approx.""" + g = X @ gw.t() + s * ((X @ gA.t()) @ gB.t()) + u = X @ uw.t() + s * ((X @ uA.t()) @ uB.t()) + h = F.gelu(g, approximate="tanh") * u + return h @ dw.t() + s * ((h @ dA.t()) @ dB.t()) + + +def _mk(*shape, seed): + g = torch.Generator(device=DEV).manual_seed(seed) + return ( + torch.randn(*shape, device=DEV, dtype=torch.float16, generator=g) * 0.1 + ).requires_grad_(True) + + +@pytest.mark.parametrize("quantized", [False, True]) +def test_fused_geglu_lora_matches_eager(quantized): + if quantized and bnb_F is None: + pytest.skip("bitsandbytes required for the quantized-base case") + + from axolotl.kernels.geglu import geglu_backward, geglu_forward + + H, IM, r, B, T = 256, 512, 16, 2, 8 # hidden, intermediate, rank, batch, seq + s = 0.5 + + # base weights (frozen). down maps IM->H; gate/up map H->IM. + gw = torch.randn(IM, H, device=DEV, dtype=torch.float16) * 0.05 + uw = torch.randn(IM, H, device=DEV, dtype=torch.float16) * 0.05 + dw = torch.randn(H, IM, device=DEV, dtype=torch.float16) * 0.05 + + if quantized: + # nf4-quantize each base; the eager ref dequantizes the SAME packed weight (via the kernel's + # dequantize) so only fused-vs-eager kernel numerics differ, not the quantization. + from axolotl.kernels.quantize import dequantize + + gw_p, gq = bnb_F.quantize_4bit(gw, quant_type="nf4", compress_statistics=True) + uw_p, uq = bnb_F.quantize_4bit(uw, quant_type="nf4", compress_statistics=True) + dw_p, dq = bnb_F.quantize_4bit(dw, quant_type="nf4", compress_statistics=True) + gw_ref = dequantize(gw_p.t(), gq).t().to(torch.float16) + uw_ref = dequantize(uw_p.t(), uq).t().to(torch.float16) + dw_ref = dequantize(dw_p.t(), dq).t().to(torch.float16) + gw_k, uw_k, dw_k = gw_p, uw_p, dw_p + else: + gq = uq = dq = None + gw_ref, uw_ref, dw_ref = gw, uw, dw + gw_k, uw_k, dw_k = gw, uw, dw + + # LoRA params (seeded). down LoRA: A[r,I], B[H,r]; gate/up LoRA: A[r,H], B[I,r]. + def fresh(): + gA, gB = _mk(r, H, seed=1), _mk(IM, r, seed=2) + uA, uB = _mk(r, H, seed=3), _mk(IM, r, seed=4) + dA, dB = _mk(r, IM, seed=5), _mk(H, r, seed=6) + return gA, gB, uA, uB, dA, dB + + Xv = torch.randn(B, T, H, device=DEV, dtype=torch.float16) * 0.1 + grad_out = torch.randn(B, T, H, device=DEV, dtype=torch.float16) + + # --- fused --- + Xf = Xv.clone().requires_grad_(True) + gA, gB, uA, uB, dA, dB = fresh() + out_f = LoRA_MLP.apply( + Xf, + None, + gw_k, + None, + gq, + gA, + gB, + s, + None, + None, + uw_k, + None, + uq, + uA, + uB, + s, + None, + None, + dw_k, + None, + dq, + dA, + dB, + s, + None, + None, + geglu_forward, + geglu_backward, + True, + ) + out_f.backward(grad_out) + gf = [t.grad.clone() for t in (Xf, gA, gB, uA, uB, dA, dB)] + + # --- eager --- + Xe = Xv.clone().requires_grad_(True) + eA, eB, euA, euB, edA, edB = fresh() + out_e = _geglu_eager(Xe, gw_ref, eA, eB, uw_ref, euA, euB, dw_ref, edA, edB, s) + out_e.backward(grad_out) + ge = [t.grad.clone() for t in (Xe, eA, eB, euA, euB, edA, edB)] + + def rel(a, b): + return (a - b).float().abs().max().item() / max( + b.float().abs().max().item(), 1e-6 + ) + + tol = 3e-2 # fp16 + Triton reduction order (+ nf4 dequant rounding shared by both) + assert torch.isfinite(out_f).all() + assert rel(out_f, out_e) < tol, f"forward rel={rel(out_f, out_e):.4f}" + names = ["dX", "dgate_A", "dgate_B", "dup_A", "dup_B", "ddown_A", "ddown_B"] + for n, a, b in zip(names, gf, ge, strict=True): + assert torch.isfinite(a).all(), f"{n} non-finite" + assert rel(a, b) < tol, f"{n} rel={rel(a, b):.4f}" diff --git a/tests/integrations/kernels/scattermoe_lora/test_bnb_experts_forward.py b/tests/integrations/kernels/scattermoe_lora/test_bnb_experts_forward.py new file mode 100644 index 0000000000..37c379f0a4 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_bnb_experts_forward.py @@ -0,0 +1,249 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""bnb-4bit MoE expert LoRA: both the default 1-launch (``moe_bnb_fast``) path and the +chunked-dequant fallback must match a full-dequant reference in forward AND gradients. + +The experts are stored as bnb 4-bit (the same parametrization ``quantize_moe_experts`` +installs at load via ``replace_parameter_4bit``). The reference dequantizes those exact +4-bit weights to bf16 and runs the standard scattermoe path, so the only differences are +GEMM ordering / bf16 rounding between the two bnb paths and the reference. + +Also covers the divisibility guard in ``_selective_dequant_bnb4`` (F3) and the +non-persistent Marlin workspace buffer (F4). All tests are CUDA + bitsandbytes gated. +""" + +from types import SimpleNamespace + +import pytest +import torch + +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") +bnb_parametrize = pytest.importorskip( + "bitsandbytes.nn.parametrize", reason="bitsandbytes required" +) + +import axolotl.integrations.kernels.libs.scattermoe_lora.experts as ex # noqa: E402 +from axolotl.integrations.kernels.libs.scattermoe_lora.chunked_bnb import ( # noqa: E402 + set_bnb_fast, + set_chunk_size_override, + set_layer_gc_active, +) +from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( # noqa: E402 + scattermoe_experts_forward, +) + +DEV = "cuda" +E, H, IM, N, K, R, SC = 8, 256, 128, 32, 2, 8, 0.5 + + +def _bf16_module(gu, dn): + """Plain bf16 experts module (routes through the standard dequant path).""" + return SimpleNamespace( + num_experts=E, + gate_up_proj=gu, + down_proj=dn, + act_fn=torch.nn.functional.silu, + is_transposed=False, + is_concatenated=True, + has_bias=False, + has_gate=True, + ) + + +class _BnbExperts(torch.nn.Module): + """Real nn.Module so torch parametrize (bnb 4-bit) can attach to the params.""" + + def __init__(self, gu, dn): + super().__init__() + self.num_experts = E + self.act_fn = torch.nn.functional.silu + self.is_transposed = False + self.is_concatenated = True + self.has_bias = False + self.has_gate = True + self.gate_up_proj = torch.nn.Parameter(gu, requires_grad=False) + self.down_proj = torch.nn.Parameter(dn, requires_grad=False) + bnb_parametrize.replace_parameter_4bit( + self, "gate_up_proj", compress_statistics=True, quant_type="nf4" + ) + bnb_parametrize.replace_parameter_4bit( + self, "down_proj", compress_statistics=True, quant_type="nf4" + ) + + +def _rel(a, b): + return (a - b).float().abs().max().item() / max(b.float().abs().max().item(), 1e-6) + + +def _run(module, lora, idx, w, grad, monkeypatch): + monkeypatch.setattr(ex, "_has_peft_wrapper", lambda s: True) + monkeypatch.setattr( + ex, + "_unwrap_experts_lora", + lambda s: (s, (lora[0], lora[1], SC), (lora[2], lora[3], SC)), + ) + for t in lora: + t.grad = None + x = torch.randn( + N, + H, + device=DEV, + dtype=torch.bfloat16, + generator=torch.Generator(DEV).manual_seed(7), + ).requires_grad_(True) + out = scattermoe_experts_forward(module, x, idx, w) + out.backward(grad) + return out.detach(), x.grad.detach(), [t.grad.clone() for t in lora] + + +@pytest.mark.parametrize("fast", [True, False]) +def test_bnb_moe_lora_matches_dequant(fast, monkeypatch): + """moe_bnb_fast={True,False} both match the full-dequant bf16 reference (fwd + grads).""" + pytest.importorskip("triton") + dt = torch.bfloat16 + g = torch.Generator(device=DEV).manual_seed(0) + gu = torch.randn(E, 2 * IM, H, device=DEV, dtype=dt, generator=g) * 0.1 + dn = torch.randn(E, H, IM, device=DEV, dtype=dt, generator=g) * 0.1 + + bnb_mod = _BnbExperts(gu, dn) + # Reference uses the EXACT dequantized 4-bit weights so only kernel numerics differ. + gu_deq = bnb_mod.gate_up_proj.detach().contiguous() + dn_deq = bnb_mod.down_proj.detach().contiguous() + + def mk(*s): + return ( + torch.randn(*s, device=DEV, dtype=dt, generator=g) * 0.05 + ).requires_grad_(True) + + lora = [mk(R * E, H), mk(2 * IM, R * E), mk(R * E, IM), mk(H, R * E)] + idx = torch.randint(0, E, (N, K), device=DEV, generator=g) + w = torch.rand(N, K, device=DEV, generator=g).to(dt) + grad = torch.randn(N, H, device=DEV, dtype=dt, generator=g) + + active = torch.unique(idx.reshape(-1)) + row = (active.long()[:, None] * R + torch.arange(R, device=DEV)[None, :]).reshape( + -1 + ) + + set_layer_gc_active(False) + set_chunk_size_override(None) + try: + set_bnb_fast(fast) + out_bnb, dx_bnb, gl_bnb = _run(bnb_mod, lora, idx, w, grad, monkeypatch) + finally: + set_bnb_fast(None) + + out_ref, dx_ref, gl_ref = _run( + _bf16_module(gu_deq, dn_deq), lora, idx, w, grad, monkeypatch + ) + + # forward output parity vs the full-dequant reference + assert torch.isfinite(out_bnb).all() + assert _rel(out_bnb, out_ref) < 6e-2, "forward output" + # input grad + assert torch.isfinite(dx_bnb).all() + assert _rel(dx_bnb, dx_ref) < 6e-2, "input grad" + for i, slc in ( + (0, row), + (1, (slice(None), row)), + (2, row), + (3, (slice(None), row)), + ): + assert torch.isfinite(gl_bnb[i]).all() + assert _rel(gl_bnb[i][slc], gl_ref[i][slc]) < 6e-2, f"lora grad {i}" + + +def test_bnb_fast_and_chunked_agree(monkeypatch): + """The two bnb paths should agree with each other (same math, different launch shape).""" + pytest.importorskip("triton") + dt = torch.bfloat16 + g = torch.Generator(device=DEV).manual_seed(3) + gu = torch.randn(E, 2 * IM, H, device=DEV, dtype=dt, generator=g) * 0.1 + dn = torch.randn(E, H, IM, device=DEV, dtype=dt, generator=g) * 0.1 + bnb_mod = _BnbExperts(gu, dn) + + def mk(*s): + return ( + torch.randn(*s, device=DEV, dtype=dt, generator=g) * 0.05 + ).requires_grad_(True) + + lora = [mk(R * E, H), mk(2 * IM, R * E), mk(R * E, IM), mk(H, R * E)] + idx = torch.randint(0, E, (N, K), device=DEV, generator=g) + w = torch.rand(N, K, device=DEV, generator=g).to(dt) + grad = torch.randn(N, H, device=DEV, dtype=dt, generator=g) + + set_layer_gc_active(False) + set_chunk_size_override(None) + try: + set_bnb_fast(True) + out_fast, dx_fast, gl_fast = _run(bnb_mod, lora, idx, w, grad, monkeypatch) + set_bnb_fast(False) + out_chunk, dx_chunk, gl_chunk = _run(bnb_mod, lora, idx, w, grad, monkeypatch) + finally: + set_bnb_fast(None) + + assert _rel(out_fast, out_chunk) < 5e-2, "forward output" + assert _rel(dx_fast, dx_chunk) < 5e-2, "input grad" + for i in range(4): + assert _rel(gl_fast[i], gl_chunk[i]) < 5e-2, f"lora grad {i}" + + +# --- F3: divisibility guard in _selective_dequant_bnb4 ------------------------------------- + + +@pytest.mark.parametrize( + "expert_shape", + [ + (8, 8), # numel 64: divisible by blocksize(64) and 2 -> selective slice path + (4, 4), # numel 16: not divisible by blocksize(64) -> full-dequant fallback + ], +) +def test_selective_dequant_bnb4_matches_full(expert_shape): + """Selective dequant equals full-dequant+index in BOTH the divisible and fallback cases.""" + pytest.importorskip("triton") + import bitsandbytes.functional as bnb_f + + from axolotl.integrations.kernels.libs.scattermoe_lora.selective_dequant import ( + _selective_dequant_bnb4, + ) + + blocksize = 64 + n_exp = 4 + expert_numel = expert_shape[0] * expert_shape[1] + flat = torch.randn(n_exp * expert_numel, device=DEV, dtype=torch.bfloat16) * 0.1 + packed, qs = bnb_f.quantize_4bit( + flat, blocksize=blocksize, quant_type="nf4", compress_statistics=False + ) + + full = bnb_f.dequantize_4bit(packed, qs).reshape(n_exp, *expert_shape) + active = torch.tensor([0, 2], device=DEV, dtype=torch.long) + + out = _selective_dequant_bnb4(packed, qs, active, expert_shape) + torch.testing.assert_close(out, full[active], atol=2e-2, rtol=2e-2) + + +# --- F4: Marlin non-expert workspace buffer must not enter state_dict ---------------------- + + +def test_marlin_nonexpert_workspace_not_persistent(): + pytest.importorskip("torchao") + from axolotl.integrations.kernels.libs.scattermoe_lora.marlin_w4a16 import ( + marlin_w4a16_available, + ) + + if not marlin_w4a16_available(): + pytest.skip("Marlin W4A16 kernel not available on this device") + + from axolotl.integrations.kernels.libs.scattermoe_lora.marlin_w4a16.nonexpert_linear import ( + MarlinW4A16Linear, + ) + + w = torch.randn(128, 256, device=DEV, dtype=torch.bfloat16) * 0.1 + mod = MarlinW4A16Linear(w, bias=None) + sd = mod.state_dict() + assert "_workspace" not in sd, "scratch workspace leaked into state_dict" + assert "_workspace" in mod._non_persistent_buffers_set + # persistent buffers still present + assert "qweight" in sd and "scales" in sd diff --git a/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_dequant_path.py b/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_dequant_path.py new file mode 100644 index 0000000000..d3408531f4 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_dequant_path.py @@ -0,0 +1,426 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Arch-agnostic (non-Blackwell) coverage for the grouped NVFP4 MoE training path. + +The headline correctness oracle (test_grouped_fp4_train.py) is gated to Blackwell (sm100/sm120) +because the CUTLASS/DeepGEMM forward backends only run there. But the grouped autograd Function +also has a path that runs on ANY sm80+ CUDA GPU: + + * FORWARD: the Marlin W4A16 backend (bf16-act x NVFP4-weight, standard Ampere-class ``mma``, + no Hopper/Blackwell intrinsics) covers sm80 (A100) / sm89 (L40S / RTX 4090) / sm120. + * BACKWARD: the gradient-consistent chunked bf16-dequant base dX (NVFP4 -> bf16 via a Triton + kernel + ``torch._grouped_mm``), selected on every non-sm120 arch and forced here via + ``prefer_fp8_dx=False``. This is the B8 FP4-expert backward and is arch-agnostic. + +So on the L40S (sm89) CI these tests EXECUTE (via Marlin fwd + bf16-dequant bwd), giving the +grouped fwd/bwd oracle and the B11 backward None-grad contract real coverage there. They also +execute on Blackwell. The Marlin/DeepGEMM/CUTLASS *numerics* and *perf* remain hardware-gated +in test_grouped_fp4_train.py / test_grouped_fp4_perf.py; this file does not weaken those. + +Gate: CUDA available AND ``grouped_fp4_available('nvfp4')`` (True on sm80+ with nvcc via Marlin, +or on sm90/100 via DeepGEMM, or sm120 via CUTLASS). NOT Blackwell-gated. + +Helpers (quantize_nvfp4, _bf16_oracle, _make_inputs, _cos, SHAPES, DEV) are reused from the +sibling correctness module; importing them is safe - its module-level Blackwell skip applies only +to ITS test functions, not to importing its helpers. +""" + +from __future__ import annotations + +import pytest +import torch + +# Reuse the validated quantizer + oracle + fixtures from the sibling correctness module. +from .test_grouped_fp4_train import ( + DEV, + SHAPE_IDS, + SHAPES, + _bf16_oracle, + _cos, + _fresh_nv, + _make_inputs, + quantize_nvfp4, +) + +_IS_CUDA = torch.cuda.is_available() + + +def _grouped_available() -> bool: + """True iff SOME grouped fp4 forward backend resolves here (Marlin on sm80+, else DeepGEMM / + CUTLASS). On the L40S CI this is True via Marlin, so the tests below execute (no Blackwell).""" + if not _IS_CUDA: + return False + try: + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_available, + ) + + return grouped_fp4_available("nvfp4") + except Exception: + return False + + +pytestmark = pytest.mark.skipif( + not _grouped_available(), + reason="grouped NVFP4 needs CUDA + a resolvable fp4 forward backend (Marlin sm80+ / " + "DeepGEMM sm90+ / CUTLASS sm120)", +) + + +# =========================================================================== +# Dequant-path forward+backward oracle (executes on sm89 via Marlin + bf16-dequant bwd) +# =========================================================================== + + +@pytest.mark.parametrize("cfg", SHAPES, ids=SHAPE_IDS) +def test_grouped_fp4_fwd_bwd_dequant_path_matches_oracle(cfg): + """Forward cosine >= 0.97 and backward grads cosine >= 0.95 vs the bf16 per-expert oracle, + forcing the arch-agnostic bf16-dequant base dX (prefer_fp8_dx=False). + + Runs on any sm80+ GPU (Marlin forward + chunked bf16-dequant backward), so it executes on the + sm89 CI where the Blackwell-gated oracle in test_grouped_fp4_train.py cannot. + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.dequant_grouped import ( + nvfp4_dequant_bf16, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_moe_train, + ) + + E, H, I = cfg["E"], cfg["H"], cfg["I"] + s = cfg["scaling"] + + # Fresh NV tensors (the marlin backend frees qdata on first build, so the oracle weights are + # dequantized from an independent copy of the same random matrices). + twoI = 2 * I + torch.manual_seed(0) + Wgu = torch.randn(E, twoI, H, device=DEV, dtype=torch.bfloat16) * 0.04 + Wdn = torch.randn(E, H, I, device=DEV, dtype=torch.bfloat16) * 0.04 + gu_nv, dn_nv = _fresh_nv(Wgu, Wdn) + pt = torch.ones(E, device=DEV) + Wgu_b = nvfp4_dequant_bf16(gu_nv.qdata, gu_nv.scale, pt) + Wdn_b = nvfp4_dequant_bf16(dn_nv.qdata, dn_nv.scale, pt) + + # Inputs (reuse the fixture for hidden/idx/wts + LoRA params, then make leaf copies). + hidden, idx, wts, _gu, _dn, _Wgu, _Wdn, (Agu, Bgu, Adn, Bdn) = _make_inputs(cfg) + + hk = hidden.detach().clone().requires_grad_(True) + Agk = Agu.detach().clone().requires_grad_(True) + Bgk = Bgu.detach().clone().requires_grad_(True) + Adk = Adn.detach().clone().requires_grad_(True) + Bdk = Bdn.detach().clone().requires_grad_(True) + + out = grouped_fp4_moe_train( + hk, + idx, + wts, + gu_nv, + dn_nv, + (Agk, Bgk, s), + (Adk, Bdk, s), + cfg["limit"], + "nvfp4", + act_type=cfg["act_type"], + mxfp4_cache={}, + prefer_fp8_dx=False, # force the arch-agnostic bf16-dequant base dX (B8) + ) + assert torch.isfinite(out).all(), f"{cfg['name']}: fwd output has NaN/inf" + assert out.shape == (cfg["N"], H) + out.float().pow(2).mean().backward() + + # Oracle on the same dequantized weights. + Agr = Agu.detach().clone().requires_grad_(True) + Bgr = Bgu.detach().clone().requires_grad_(True) + Adr = Adn.detach().clone().requires_grad_(True) + Bdr = Bdn.detach().clone().requires_grad_(True) + hr = hidden.detach().clone().requires_grad_(True) + ref = _bf16_oracle( + hr, idx, wts, Wgu_b, Wdn_b, Agr, Bgr, Adr, Bdr, cfg["act_type"], cfg["limit"], s + ) + cos_fwd = _cos(out, ref) + assert cos_fwd > 0.97, f"{cfg['name']}: fwd cosine {cos_fwd:.4f} < 0.97" + + ref.pow(2).mean().backward() + for name, gk, gr in [ + ("d_hidden", hk.grad, hr.grad), + ("d_Agu", Agk.grad, Agr.grad), + ("d_Adn", Adk.grad, Adr.grad), + ]: + assert gk is not None, ( + f"{cfg['name']}: {name} grad is None (backward didn't run)" + ) + assert torch.isfinite(gk).all(), f"{cfg['name']}: {name} has NaN/inf" + cos = _cos(gk, gr) + assert cos > 0.95, f"{cfg['name']}: {name} cosine {cos:.4f} < 0.95" + + +# =========================================================================== +# B11: backward None-grad contract (data-independent FSDP2 backward collectives) +# =========================================================================== + + +def test_grouped_fp4_backward_none_grad_contract(): + """The grouped autograd Function returns grads for x + the four LoRA params and None for the + routing / m_indices / offs / mode inputs (frozen experts -> no weight grad). + + This is the code basis of B11: the backward emits a grad ONLY for the differentiable leaves, so + FSDP2's reduce-scatter sees gradients exclusively for the trainable LoRA params (a fixed, + data-independent set) - never for the frozen NVFP4 experts or the integer routing tensors. We + assert it by checking which inputs receive a grad after backward. + + Forced onto the bf16-dequant base dX (prefer_fp8_dx=False) so it runs on any sm80+ GPU (Marlin + forward), executing on the sm89 CI. idx/wts are integer/non-leaf routing tensors that cannot + carry grads; the contract is that x and the LoRA params DO and the experts do NOT. + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_moe_train, + ) + + cfg = SHAPES[0] + E, H, I = cfg["E"], cfg["H"], cfg["I"] + s = cfg["scaling"] + twoI = 2 * I + torch.manual_seed(3) + Wgu = torch.randn(E, twoI, H, device=DEV, dtype=torch.bfloat16) * 0.04 + Wdn = torch.randn(E, H, I, device=DEV, dtype=torch.bfloat16) * 0.04 + gu_nv, dn_nv = _fresh_nv(Wgu, Wdn) + + hidden, idx, wts, _gu, _dn, _Wgu, _Wdn, (Agu, Bgu, Adn, Bdn) = _make_inputs( + cfg, seed=3 + ) + hk = hidden.detach().clone().requires_grad_(True) + Agk = Agu.detach().clone().requires_grad_(True) + Bgk = Bgu.detach().clone().requires_grad_(True) + Adk = Adn.detach().clone().requires_grad_(True) + Bdk = Bdn.detach().clone().requires_grad_(True) + + # The frozen experts must NOT request grad (FSDP2 never reduce-scatters them). + assert not gu_nv.requires_grad and not dn_nv.requires_grad + assert not idx.requires_grad and not wts.requires_grad + + out = grouped_fp4_moe_train( + hk, + idx, + wts, + gu_nv, + dn_nv, + (Agk, Bgk, s), + (Adk, Bdk, s), + cfg["limit"], + "nvfp4", + act_type=cfg["act_type"], + mxfp4_cache={}, + prefer_fp8_dx=False, + ) + out.float().pow(2).mean().backward() + + # Differentiable leaves: x + the four LoRA params get non-None, finite grads. + for name, t in [ + ("x", hk), + ("Agu", Agk), + ("Bgu", Bgk), + ("Adn", Adk), + ("Bdn", Bdk), + ]: + assert t.grad is not None, ( + f"{name} grad is None (backward broke the LoRA grad path)" + ) + assert torch.isfinite(t.grad).all(), f"{name} grad has NaN/inf" + + # Non-differentiable inputs (frozen experts, integer routing) carry no grad. + assert getattr(gu_nv, "grad", None) is None, "frozen gate_up expert got a grad" + assert getattr(dn_nv, "grad", None) is None, "frozen down expert got a grad" + assert idx.grad is None and wts.grad is None, "routing tensors got a grad" + + +def test_grouped_experts_function_backward_returns_none_for_nondiff_inputs(): + """Directly assert _GroupedExperts.backward's None-grad tuple shape: grads only for x (pos 0) + and the four LoRA params (Agu/Bgu/Adn/Bdn, pos 3-6); None for base / weight_recipe / m_indices / + offs / scaling / limit / mode / act_type / prefer_fp8_dx. This is the literal B11 contract. + + Built on the Marlin forward + bf16-dequant backward so it runs on sm80+ (executes on sm89 CI). + """ + from axolotl.integrations.kernels.libs.scattermoe_lora import grouped_train as gt + + cfg = SHAPES[0] + E, H, I = cfg["E"], cfg["H"], cfg["I"] + s = cfg["scaling"] + twoI = 2 * I + torch.manual_seed(5) + Wgu = torch.randn(E, twoI, H, device=DEV, dtype=torch.bfloat16) * 0.04 + Wdn = torch.randn(E, H, I, device=DEV, dtype=torch.bfloat16) * 0.04 + gu_nv, dn_nv = _fresh_nv(Wgu, Wdn) + + hidden, idx, wts, _gu, _dn, _Wgu, _Wdn, (Agu, Bgu, Adn, Bdn) = _make_inputs( + cfg, seed=5 + ) + + # Resolve a forward backend the same way grouped_fp4_moe_train does (Marlin on sm89). + backend = gt._train_backend("nvfp4") + if backend == "marlin": + from axolotl.integrations.kernels.libs.scattermoe_lora.marlin_w4a16.backend import ( + MARLIN_TILE, + ) + + tile = MARLIN_TILE + else: + tile = gt.TILE + + N = hidden.size(0) + dev = hidden.device + flat = idx.reshape(-1) + order = flat.argsort() + rep = torch.arange(N, device=dev).repeat_interleave(idx.size(1))[order] + exp_sorted = flat[order] + counts = torch.bincount(flat, minlength=E) + ptiles = (counts + tile - 1) // tile + roff = torch.cat([ptiles.new_zeros(1), ptiles.cumsum(0)]) * tile + coff = torch.cat([counts.new_zeros(1), counts.cumsum(0)]) + padded_row = roff[exp_sorted] + ( + torch.arange(exp_sorted.numel(), device=dev) - coff[exp_sorted] + ) + m_indices = torch.repeat_interleave( + torch.arange(E, dtype=torch.int32, device=dev), ptiles + ) + offs = (ptiles * tile).cumsum(0).to(torch.int32) + Mt = int(ptiles.sum()) * tile + + if backend == "marlin": + from axolotl.integrations.kernels.libs.scattermoe_lora.marlin_w4a16.backend import ( + build_marlin_forward_base, + ) + + base = build_marlin_forward_base(gu_nv, dn_nv, {}) + elif backend == "deepgemm": + from axolotl.integrations.kernels.libs.scattermoe_lora.dequant_grouped import ( + _cached_mxfp4, + ) + + base = ( + "deepgemm", + _cached_mxfp4(gu_nv, gt._pt(gu_nv, E, dev), {}, "gate_up"), + _cached_mxfp4(dn_nv, gt._pt(dn_nv, E, dev), {}, "down"), + ) + else: # cutlass (sm120) + gu_eng = gt._engine(Mt, twoI, H, E, "nvfp4") + gu_eng.set_weights(gu_nv.qdata, gu_nv.scale) + dn_eng = gt._engine(Mt, H, I, E, "nvfp4") + dn_eng.set_weights(dn_nv.qdata, dn_nv.scale) + base = ("cutlass", gu_eng, dn_eng) + + Ags, Bgs, _ = gt._lora_stack((Agu, Bgu, s), E, H, twoI) + Ads, Bds, _ = gt._lora_stack((Adn, Bdn, s), E, I, H) + Ags = Ags.detach().requires_grad_(True) + Bgs = Bgs.detach().requires_grad_(True) + Ads = Ads.detach().requires_grad_(True) + Bds = Bds.detach().requires_grad_(True) + + A = hidden.new_zeros(Mt, H).index_copy(0, padded_row, hidden[rep]) + A = A.detach().requires_grad_(True) + lim = float(cfg["limit"]) + recipe = lambda: (gu_nv, dn_nv) # noqa: E731 + + dn = gt._GroupedExperts.apply( + A, + base, + recipe, + Ags, + Bgs, + Ads, + Bds, + m_indices, + offs, + s, + lim, + "nvfp4", + cfg["act_type"], + False, # prefer_fp8_dx=False -> bf16-dequant base dX + ) + dn.float().pow(2).mean().backward() + + # Differentiable leaves get grads; non-diff inputs get None (the Function's backward tuple). + assert A.grad is not None and torch.isfinite(A.grad).all(), "dx (pos 0) missing" + for nm, t in (("Agu", Ags), ("Bgu", Bgs), ("Adn", Ads), ("Bdn", Bds)): + assert t.grad is not None and torch.isfinite(t.grad).all(), f"{nm} grad missing" + # m_indices / offs are int tensors and never leaves with grad; assert they stayed grad-free. + assert m_indices.grad is None and offs.grad is None + + +# =========================================================================== +# Marlin fused-dequant bit-exactness on sm80+ (executes on sm89 via the Marlin ext) +# =========================================================================== +# +# test_grouped_fp4_train.py also has marlin bit-exact tests, but they sit under that module's +# Blackwell pytestmark AND a per-test capability==12 gate, so they only run on sm120. The Marlin +# W4A16 ext is sm80+ (see marlin_w4a16/__init__: standard Ampere mma, no Blackwell intrinsics), so +# the fused marlin->bf16 dequant is bit-exact-checkable on sm89 too. These sm80+-gated variants give +# that numeric check real coverage on the L40S CI without touching the Blackwell module. + + +def _marlin_available() -> bool: + if not _IS_CUDA: + return False + try: + from axolotl.integrations.kernels.libs.scattermoe_lora.marlin_w4a16 import ( + marlin_w4a16_available, + ) + + return marlin_w4a16_available() + except Exception: + return False + + +@pytest.mark.skipif( + not _marlin_available(), + reason="marlin W4A16 ext (sm80+, nvcc) required", +) +@pytest.mark.parametrize( + "C,N,K", [(4, 64, 128), (4, 128, 64)], ids=["gate_up_N64_K128", "down_N128_K64"] +) +def test_marlin_fused_dequant_bit_exact_sm80(C, N, K): + """marlin_dequant_bf16 is bit-exact vs nvfp4_dequant_bf16 on sm80+ (gate_up + down shapes). + + The marlin int32 layout decoded by the fused Triton dequant must match the reference NVFP4->bf16 + dequant exactly (maxerr == 0). N%64==0 (marlin N_t) and K%16==0 (NVFP4 block) are required. + Runs on any sm80+ GPU with the Marlin ext, so it executes on the sm89 CI. + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.dequant_grouped import ( + nvfp4_dequant_bf16, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.marlin_w4a16 import load_ext + from axolotl.integrations.kernels.libs.scattermoe_lora.marlin_w4a16.backend import ( + _build_base_scatter, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.marlin_w4a16.fused_dequant import ( + marlin_dequant_bf16, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.marlin_w4a16.prep import ( + prepare_nvfp4_weight_for_marlin, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.mx_weights import ( + fp4_codebook, + ) + + torch.manual_seed(42) + W = torch.randn(C, N, K, device=DEV, dtype=torch.bfloat16) * 0.04 + nv = quantize_nvfp4(W) + pt = torch.ones(C, device=DEV) + + ext = load_ext() + scatter_lut = _build_base_scatter(torch.device(DEV)) + cb = fp4_codebook(torch.device(DEV)).float() + + marlin_packed = prepare_nvfp4_weight_for_marlin( + nv.qdata, nv.scale, pt, N, K, torch.bfloat16, ext.gptq_marlin_repack + ) + qw_flat = marlin_packed[0].reshape(C, -1) + ref_bf16 = nvfp4_dequant_bf16(nv.qdata, nv.scale, pt) + got_bf16 = marlin_dequant_bf16(qw_flat, nv.scale, pt, scatter_lut, cb, N, K, C) + + max_err = (ref_bf16.float() - got_bf16.float()).abs().max().item() + assert max_err == 0.0, ( + f"marlin fused dequant not bit-exact vs nvfp4_dequant_bf16 (N={N}, K={K}): " + f"maxerr={max_err}" + ) diff --git a/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_guards.py b/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_guards.py new file mode 100644 index 0000000000..dbeb462e2e --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_guards.py @@ -0,0 +1,120 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""No-GPU coverage for the load-bearing safety guards on the grouped NVFP4 MoE path. + +Each guard turns silent corruption or a Blackwell SIGSEGV into a clear RuntimeError. They were +shipping untested: the GPU oracle (test_grouped_fp4_train.py) never enters them, because on the sm89 +CI a fused backend is available, so the C1 cutlass-only raise and the A4/B2 fall-throughs do not +fire. Here each guard is driven directly with mocked arch/availability probes - no CUDA needed (the +scattermoe_lora package imports triton, so the module skips on a triton-less host). +""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest +import torch + +pytest.importorskip("triton") + + +def _mock_experts(num_experts): + """Minimal stand-in for a scattermoe experts module. The guards under test fire before any + weight/LoRA shape is read, so the proj params and LoRA tuples only need to be present + non-None. + """ + lora = (torch.zeros(1), torch.zeros(1), 1.0) + return SimpleNamespace( + num_experts=num_experts, + gate_up_proj=torch.zeros(1), + down_proj=torch.zeros(1), + _scattermoe_lora={"gate_up_proj": lora, "down_proj": lora}, + ) + + +def test_c1_cutlass_nonunit_weight_scale_2_without_alt_backend_raises(monkeypatch): + # C1: cutlass cannot fold a non-unit per_tensor_scale (weight_scale_2). With cutlass resolved, + # a non-unit pt present, and neither marlin nor deepgemm available to fold it, the path must + # hard-error rather than silently produce a wrong forward + grad mismatch. + from axolotl.integrations.kernels.libs.scattermoe_lora import grouped_train as gt + + monkeypatch.setattr(gt, "_train_backend", lambda mode: "cutlass") + monkeypatch.setattr(gt, "_has_nonunit_pt", lambda *nvs: True) + monkeypatch.setattr(gt, "_backend_available", lambda name: False) + + E, twoI, packed_k, H, packed_i = 4, 16, 8, 8, 4 + gu = SimpleNamespace(qdata=torch.zeros(E, twoI, packed_k, dtype=torch.uint8)) + dn = SimpleNamespace(qdata=torch.zeros(E, H, packed_i, dtype=torch.uint8)) + hidden = torch.zeros(2, H) + + with pytest.raises(RuntimeError, match="weight_scale_2"): + gt.grouped_fp4_moe_train( + hidden, None, None, gu, dn, None, None, None, "nvfp4", mxfp4_cache={} + ) + + +def test_a4_nvfp4_lora_grouped_unavailable_raises(monkeypatch): + # A4: NVFP4 experts + LoRA select the grouped path (dsv4_fp4_grouped_mode set), but no fused + # grouped backend resolves -> must hard-error instead of falling through to the SIGSEGV MX kernel. + from axolotl.integrations.kernels.libs.scattermoe_lora import ( + experts as ex, + grouped_train as gt, + ) + + monkeypatch.setattr(ex, "is_nvfp4_param", lambda p: True) + monkeypatch.setattr(gt, "grouped_fp4_available", lambda mode: False) + monkeypatch.setattr(ex.RUNTIME, "fp4_grouped_mode", "nvfp4") + + E, H, topk, N = 4, 8, 2, 6 + self_ = _mock_experts(E) + top_k_index = torch.randint(0, E, (N, topk)) + top_k_weights = torch.rand(N, topk) + hidden = torch.randn(N, H) + + with pytest.raises(RuntimeError, match="scatter2scatter_lora_mx"): + ex.scattermoe_experts_forward(self_, hidden, top_k_index, top_k_weights) + + +def test_b2_nvfp4_lora_without_grouped_mode_raises_on_blackwell(monkeypatch): + # B2: NVFP4 experts + LoRA WITHOUT dsv4_fp4_grouped_mode would fall through to the legacy MX + # kernel, which SIGSEGVs on Blackwell. On sm100/sm120 it must hard-error with guidance instead. + from axolotl.integrations.kernels.libs.scattermoe_lora import experts as ex + + monkeypatch.setattr(ex, "is_nvfp4_param", lambda p: True) + monkeypatch.setattr(ex.RUNTIME, "fp4_grouped_mode", None) + monkeypatch.setattr(torch.cuda, "is_available", lambda: True) + monkeypatch.setattr(torch.cuda, "get_device_capability", lambda *a, **k: (10, 0)) + + E, H, topk, N = 4, 8, 2, 6 + self_ = _mock_experts(E) + top_k_index = torch.randint(0, E, (N, topk)) + top_k_weights = torch.rand(N, topk) + hidden = torch.randn(N, H) + + with pytest.raises(RuntimeError, match="grouped fp4 MoE path on Blackwell"): + ex.scattermoe_experts_forward(self_, hidden, top_k_index, top_k_weights) + + +def test_b2_nvfp4_lora_without_grouped_mode_ok_off_blackwell(monkeypatch): + # The B2 guard must NOT fire on non-Blackwell archs (the legacy MX path is left intact there); + # it should fall through to _prepare_weights_and_lora rather than raising the B2 error. + from axolotl.integrations.kernels.libs.scattermoe_lora import experts as ex + + monkeypatch.setattr(ex, "is_nvfp4_param", lambda p: True) + monkeypatch.setattr(ex.RUNTIME, "fp4_grouped_mode", None) + monkeypatch.setattr(torch.cuda, "is_available", lambda: True) + monkeypatch.setattr(torch.cuda, "get_device_capability", lambda *a, **k: (8, 9)) + + E, H, topk, N = 4, 8, 2, 6 + self_ = _mock_experts(E) + top_k_index = torch.randint(0, E, (N, topk)) + top_k_weights = torch.rand(N, topk) + hidden = torch.randn(N, H) + + # The B2 guard is skipped (sm89); execution proceeds past it. It will fail later in the real MX + # path on CPU, but NOT with the B2 Blackwell message. + with pytest.raises(Exception) as exc: + ex.scattermoe_experts_forward(self_, hidden, top_k_index, top_k_weights) + assert "grouped fp4 MoE path on Blackwell" not in str(exc.value) diff --git a/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_perf.py b/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_perf.py new file mode 100644 index 0000000000..a8680f1420 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_perf.py @@ -0,0 +1,726 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Speed + memory regression guards for the grouped/marlin NVFP4 MoE training path. + +All assertions are RATIO-based (machine-agnostic). Measured baselines on RTX PRO 6000 (sm120): + DSV4 (E=256, H=4096, I=2048, top6): grouped fwd ~1.9x, e2e ~2.27x vs bf16 dequant-per-step + gemma4 (E=128, H=2816, I=704, top8): grouped fwd ~2.1x, e2e ~1.8x vs bf16 dequant-per-step + +Memory (DSV4 E=256 marlin, sm120): + marlin extra resident vs NVFP4-only baseline <= 1.3x (+2.65 GB, not the old +12 GB double-copy) + transient peak during fwd+bwd <= 1.0x the bf16-resident baseline peak (measured 0.87x at E=256) + +Skip conditions (all tests): + - Not Blackwell (sm100 / sm120) + - Insufficient free VRAM for the target E (skip gracefully, log reason) + +These use the real E=256 / E=128 shapes where VRAM allows; fall back to E=64 with looser bounds +(x0.7 of the threshold) so the test still runs on constrained systems and logs the fallback. +""" + +from __future__ import annotations + +import gc + +import pytest +import torch + +# --------------------------------------------------------------------------- +# Skip gates +# --------------------------------------------------------------------------- + +_IS_CUDA = torch.cuda.is_available() +_IS_BLACKWELL = _IS_CUDA and torch.cuda.get_device_capability()[0] in (10, 12) + +pytestmark = [ + pytest.mark.perf, + pytest.mark.skipif( + not _IS_BLACKWELL, + reason="grouped NVFP4 training backends require Blackwell (sm100/sm120)", + ), +] + +DEV = "cuda" + +# VRAM thresholds: skip at the large E if we can't fit +# DSV4 E=256: 2 copies of weights (grouped + baseline) + marlin qweight + activations ~50 GB +# gemma4 E=128: similar ~15 GB +_VRAM_DSV4_GB = 50.0 +_VRAM_GEMMA4_GB = 15.0 +_VRAM_FALLBACK_RATIO = 0.70 # loosen speed thresholds by 30% when using fallback E + + +def _free_vram_gb() -> float: + if not _IS_CUDA: + return 0.0 + torch.cuda.synchronize() + free, _ = torch.cuda.mem_get_info() + return free / 1e9 + + +# --------------------------------------------------------------------------- +# NVFP4 quantizer (self-contained; does NOT import from scratch bench dir) +# +# For large E (e.g. E=256, N=4096, K=4096), a naïve broadcast quantizer would +# allocate a [E,N,K//16,16,16] float32 intermediate (~256 GiB). We instead use +# NVFP4Tensor.to_nvfp4 in batches of at most 32 experts and concatenate qdata+scale, +# which keeps the peak transient to ~2 × batch × N × K × 4 bytes (manageable). +# --------------------------------------------------------------------------- + + +def quantize_nvfp4(W: torch.Tensor, batch: int = 32): + """Memory-efficient NVFP4 quantization: W[E,N,K] bf16 -> NVFP4Tensor. + + Processes `batch` experts at a time to bound the peak float32 transient; + concatenates qdata and scale on CPU, then moves to the target device. + """ + from torchao.prototype.mx_formats.nvfp4_tensor import NVFP4Tensor + + E, N, K = W.shape + dev = W.device + qdata_parts, scale_parts = [], [] + for e0 in range(0, E, batch): + e1 = min(e0 + batch, E) + nv_b = NVFP4Tensor.to_nvfp4(W[e0:e1].contiguous(), block_size=16) + qdata_parts.append(nv_b.qdata.cpu()) + scale_parts.append(nv_b.scale.cpu()) + del nv_b + qdata = torch.cat(qdata_parts, dim=0).to(dev) + scale = torch.cat(scale_parts, dim=0).to(dev) + return NVFP4Tensor( + qdata, + scale, + block_size=16, + orig_dtype=torch.bfloat16, + per_tensor_scale=torch.ones((), device=dev), + ) + + +# Alias for tests that need a real NVFP4Tensor (same as quantize_nvfp4 here) +quantize_nvfp4_real = quantize_nvfp4 + + +# --------------------------------------------------------------------------- +# Timing helpers +# --------------------------------------------------------------------------- + + +def _time_ms(fn, n_warmup=5, n_iter=12): + """Median of n_iter timings (ms) after n_warmup warm-up calls.""" + for _ in range(n_warmup): + fn() + torch.cuda.synchronize() + timings = [] + for _ in range(n_iter): + s = torch.cuda.Event(enable_timing=True) + e = torch.cuda.Event(enable_timing=True) + s.record() + fn() + e.record() + torch.cuda.synchronize() + timings.append(s.elapsed_time(e)) + timings.sort() + return timings[n_iter // 2] + + +def _peak_mb(fn, n_warmup=3): + """Peak memory allocated (MB) above current baseline during fn(), after warm-up.""" + for _ in range(n_warmup): + fn() + torch.cuda.synchronize() + gc.collect() + torch.cuda.empty_cache() + torch.cuda.synchronize() + torch.cuda.reset_peak_memory_stats() + base = torch.cuda.memory_allocated() + fn() + torch.cuda.synchronize() + return (torch.cuda.max_memory_allocated() - base) / 1e6 + + +# --------------------------------------------------------------------------- +# Baseline: chunked_dequant_grouped_base — the dequant-per-step path replaced by marlin. +# Uses a SEPARATE set of NV tensors so the marlin memory-free path doesn't invalidate them. +# --------------------------------------------------------------------------- + + +def _baseline_fwd_step(hidden, idx, wts, gu_nv_base, dn_nv_base, pt, limit, act_type): + """Baseline forward: chunked dequant + grouped_mm (no marlin, no LoRA). + + This is the `chunked_dequant_grouped_base` path that the grouped/marlin path replaced. + LoRA is omitted to keep the comparison fair (LoRA is the same cost in both paths). + Uses a separate gu/dn_nv_base that has NOT been marlin-freed. + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.dequant_grouped import ( + chunked_dequant_grouped_base, + ) + + return chunked_dequant_grouped_base( + hidden, idx, wts, gu_nv_base, dn_nv_base, pt, limit + ) + + +# --------------------------------------------------------------------------- +# Shape configuration (real E, with VRAM-check fallback) +# --------------------------------------------------------------------------- + + +def _dsv4_cfg(prefer_full=True): + """DSV4-Flash shape. Returns (cfg, fallback_used).""" + full = dict( + E=256, + H=4096, + I=2048, + topk=6, + N=512, + r=16, + act_type="silu", + limit=7.0, + scaling=2.0, + ) + small = dict( + E=64, + H=4096, + I=2048, + topk=6, + N=128, + r=16, + act_type="silu", + limit=7.0, + scaling=2.0, + ) + if prefer_full and _free_vram_gb() >= _VRAM_DSV4_GB: + return full, False + return small, True + + +def _gemma4_cfg(prefer_full=True): + """gemma4-A4B shape. Returns (cfg, fallback_used).""" + full = dict( + E=128, + H=2816, + I=704, + topk=8, + N=256, + r=16, + act_type="gelu_tanh", + limit=1e30, + scaling=2.0, + ) + small = dict( + E=32, + H=2816, + I=704, + topk=8, + N=64, + r=16, + act_type="gelu_tanh", + limit=1e30, + scaling=2.0, + ) + if prefer_full and _free_vram_gb() >= _VRAM_GEMMA4_GB: + return full, False + return small, True + + +def _build_tensors(cfg): + """Build tensors for a shape config. + + Returns (gu_nv, dn_nv, gu_nv_base, dn_nv_base, pt, hidden, idx, wts, lora). + gu_nv/dn_nv: NVFP4Tensor for the grouped/marlin path (qdata freed after first use). + gu_nv_base/dn_nv_base: SEPARATE NVFP4Tensor for the baseline timing (retained; not freed). + pt: per-tensor scale [E] float32. + + Uses a batched quantizer (32 experts at a time) to avoid the 256-GiB broadcast OOM + that would occur with a naïve [E,N,K//16,16,16] float32 broadcast. + """ + E, H, I, topk, N, r = cfg["E"], cfg["H"], cfg["I"], cfg["topk"], cfg["N"], cfg["r"] + twoI = 2 * I + torch.manual_seed(0) + Wgu = torch.randn(E, twoI, H, device=DEV, dtype=torch.bfloat16) * 0.04 + gu_nv = quantize_nvfp4(Wgu) + gu_nv_base = quantize_nvfp4(Wgu) # separate copy for baseline (not freed by marlin) + del Wgu + Wdn = torch.randn(E, H, I, device=DEV, dtype=torch.bfloat16) * 0.04 + dn_nv = quantize_nvfp4(Wdn) + dn_nv_base = quantize_nvfp4(Wdn) + del Wdn + pt = torch.ones(E, device=DEV) + hidden = torch.randn(N, H, device=DEV, dtype=torch.bfloat16) * 0.5 + idx = torch.stack([torch.randperm(E, device=DEV)[:topk] for _ in range(N)]) + wts = torch.softmax(torch.randn(N, topk, device=DEV), -1).to(torch.bfloat16) + Agu = ( + torch.randn(r * E, H, device=DEV, dtype=torch.bfloat16) * 0.02 + ).requires_grad_(True) + Bgu = ( + torch.randn(twoI, r * E, device=DEV, dtype=torch.bfloat16) * 0.02 + ).requires_grad_(True) + Adn = ( + torch.randn(r * E, I, device=DEV, dtype=torch.bfloat16) * 0.02 + ).requires_grad_(True) + Bdn = ( + torch.randn(H, r * E, device=DEV, dtype=torch.bfloat16) * 0.02 + ).requires_grad_(True) + return ( + gu_nv, + dn_nv, + gu_nv_base, + dn_nv_base, + pt, + hidden, + idx, + wts, + (Agu, Bgu, Adn, Bdn), + ) + + +# =========================================================================== +# Perf 1+2: Speed guards (forward and full step) +# =========================================================================== + + +def _run_speed_test(cfg, fallback, shape_name, fwd_ratio_floor, step_ratio_floor): + """Core speed test: grouped/marlin path vs dequant-per-step baseline. + + Baseline = nvfp4_dequant_bf16 (full-E, not cached) — the bottleneck this path replaces. + Grouped = grouped_fp4_moe_train (marlin forward; qdata freed after first call; cached marlin). + + Separate qdata/scale copies are kept for the baseline so the marlin memory-free path doesn't + invalidate the baseline tensor references. + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_available, + grouped_fp4_moe_train, + ) + + if not grouped_fp4_available("nvfp4"): + pytest.skip("no grouped fp4 backend available") + + if fallback: + fwd_ratio_floor *= _VRAM_FALLBACK_RATIO + step_ratio_floor *= _VRAM_FALLBACK_RATIO + + ( + gu_nv, + dn_nv, + gu_nv_base, + dn_nv_base, + pt, + hidden, + idx, + wts, + (Agu, Bgu, Adn, Bdn), + ) = _build_tensors(cfg) + s = cfg["scaling"] + act_type, limit = cfg["act_type"], cfg["limit"] + + # Build the marlin cache ONCE upfront (so all subsequent calls use the cached marlin weights + # and don't try to re-read the freed qdata). This mirrors how real training works. + cache = {} + grouped_fp4_moe_train( + hidden.detach(), + idx, + wts, + gu_nv, + dn_nv, + (Agu.detach(), Bgu.detach(), s), + (Adn.detach(), Bdn.detach(), s), + limit, + "nvfp4", + act_type=act_type, + mxfp4_cache=cache, + ) + torch.cuda.synchronize() + + # --- Grouped/marlin forward timing (warm up + median) --- + def grouped_fwd(): + grouped_fp4_moe_train( + hidden.detach(), + idx, + wts, + gu_nv, + dn_nv, + (Agu.detach(), Bgu.detach(), s), + (Adn.detach(), Bdn.detach(), s), + limit, + "nvfp4", + act_type=act_type, + mxfp4_cache=cache, + ) + + t_grouped_fwd = _time_ms(grouped_fwd) + + # --- Baseline: chunked_dequant_grouped_base (dequant per step + grouped_mm) --- + # This is the path that the grouped/marlin replaced. Uses separate NV tensors + # so the marlin memory-free path doesn't invalidate these references. + def baseline_fwd(): + _baseline_fwd_step( + hidden.detach(), + idx, + wts, + gu_nv_base, + dn_nv_base, + pt, + limit, + act_type, + ) + + t_base_fwd = _time_ms(baseline_fwd, n_warmup=3, n_iter=8) + + fwd_ratio = t_base_fwd / t_grouped_fwd + print( + f"\n[{shape_name}] fwd: grouped={t_grouped_fwd:.1f}ms base={t_base_fwd:.1f}ms " + f"ratio={fwd_ratio:.2f}x (floor={fwd_ratio_floor:.2f}x)" + ) + assert fwd_ratio >= fwd_ratio_floor, ( + f"{shape_name}: grouped fwd ratio {fwd_ratio:.2f}x < floor {fwd_ratio_floor:.2f}x. " + "Speed regression: grouped/marlin forward should be faster than dequant-per-step." + ) + + # --- Full step (fwd+bwd) timing (same cache; qdata already freed) --- + def grouped_step(): + for p in (Agu, Bgu, Adn, Bdn): + p.grad = None + hk = hidden.detach().requires_grad_() + out = grouped_fp4_moe_train( + hk, + idx, + wts, + gu_nv, + dn_nv, + (Agu, Bgu, s), + (Adn, Bdn, s), + limit, + "nvfp4", + act_type=act_type, + mxfp4_cache=cache, + ) + out.float().pow(2).mean().backward() + + t_grouped_step = _time_ms(grouped_step) + print(f"[{shape_name}] full-step (fwd+bwd): {t_grouped_step:.1f}ms") + for p in (Agu, Bgu, Adn, Bdn): + if p.grad is not None: + assert torch.isfinite(p.grad).all(), ( + f"{shape_name}: LoRA grad has NaN/inf in perf test" + ) + + +@pytest.mark.parametrize( + "shape_fn,shape_name,vram_needed,fwd_floor,step_floor", + [ + (_dsv4_cfg, "dsv4", _VRAM_DSV4_GB, 1.5, 1.3), + (_gemma4_cfg, "gemma4", _VRAM_GEMMA4_GB, 1.5, 1.3), + ], + ids=["dsv4", "gemma4"], +) +def test_grouped_fp4_speed(shape_fn, shape_name, vram_needed, fwd_floor, step_floor): + """grouped/marlin fwd is >= fwd_floor faster than chunked_dequant_grouped_base baseline. + + Baseline = chunked_dequant_grouped_base (NVFP4->bf16 dequant per step + grouped_mm); the + path that the grouped/marlin fwd replaced. Uses separate NV tensors for baseline and grouped. + + Measured on RTX PRO 6000 (sm120): + DSV4 (E=256): ~2.50x fwd vs chunked baseline + gemma4 (E=128): ~1.64x fwd vs chunked baseline + + Thresholds: fwd >= 1.5x (safe floor well below measured; allows GPU-to-GPU variance). + """ + cfg, fallback = shape_fn() + if fallback: + pytest.skip(f"{shape_name}: insufficient VRAM (need {vram_needed:.0f} GB free)") + _run_speed_test(cfg, fallback, shape_name, fwd_floor, step_floor) + + +# =========================================================================== +# Perf 3: Memory guard — marlin double-copy fix +# =========================================================================== + + +@pytest.mark.skipif( + not ( + _IS_CUDA + and torch.cuda.is_available() + and torch.cuda.get_device_capability()[0] == 12 + ), + reason="marlin W4A16 memory test is sm120 only", +) +def test_marlin_memory_no_double_copy(): + """After build_marlin_forward_base, the NVFP4 qdata must be freed (single copy resident). + + Original bug: the marlin path kept both qdata (4-bit packed) AND qweight (marlin int32 repack) + in memory — about +12 GB for DSV4 E=256. The fix frees qdata.data after repacking so only the + marlin qweight + original scales are resident (+2.65 GB). This test asserts <= 1.3x overhead + relative to the marlin qweight-only baseline. + + Memory reference (RTX PRO 6000, sm120): + NVFP4 qdata (E=256, gate_up+down): ~6.4 GB total (4-bit packed) + marlin qweight (E=256, gate_up+down): ~6.4 GB (8-bit marlin layout) + OLD (bug): both resident = ~12.8 GB + NEW (fix): only marlin qweight + scales = ~6.4 + 0.05 GB = ~6.45 GB + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.marlin_w4a16 import ( + marlin_w4a16_available, + ) + + if not marlin_w4a16_available(): + pytest.skip("marlin W4A16 ext not available") + + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_available, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.marlin_w4a16.backend import ( + _build_base_scatter, + build_marlin_forward_base, + ) + + if not grouped_fp4_available("nvfp4"): + pytest.skip("no grouped fp4 backend available") + + cfg, fallback = _dsv4_cfg() + if fallback: + pytest.skip("insufficient VRAM for marlin memory test") + + E, H, I = cfg["E"], cfg["H"], cfg["I"] + twoI = 2 * I + + # Pre-build scatter LUT (doesn't count toward memory delta) + _build_base_scatter(torch.device(DEV)) + gc.collect() + torch.cuda.empty_cache() + torch.cuda.synchronize() + + # Build real NVFP4Tensor (needed for the .qdata.data = empty() free path). + # Use batched quantizer (32 experts at a time) to avoid the 256GiB broadcast OOM. + torch.manual_seed(0) + Wgu = torch.randn(E, twoI, H, device=DEV, dtype=torch.bfloat16) * 0.04 + gu_nv_base = quantize_nvfp4_real(Wgu) + del Wgu + Wdn = torch.randn(E, H, I, device=DEV, dtype=torch.bfloat16) * 0.04 + dn_nv_base = quantize_nvfp4_real(Wdn) + del Wdn + gc.collect() + torch.cuda.empty_cache() + torch.cuda.synchronize() + + # Baseline: measure the NVFP4 qdata storage size before marlin build + + mem_nvfp4_only = torch.cuda.memory_allocated() # qdata in memory + qdata_bytes = gu_nv_base.qdata.numel() * gu_nv_base.qdata.element_size() + qdata_bytes += dn_nv_base.qdata.numel() * dn_nv_base.qdata.element_size() + + # Build marlin base (this repacks + should free qdata) + cache = {} + build_marlin_forward_base(gu_nv_base, dn_nv_base, cache) + gc.collect() + torch.cuda.synchronize() + mem_after_marlin = torch.cuda.memory_allocated() + + # The freed qdata should not be double-counted + qdata_freed = gu_nv_base.qdata.numel() == 0 and dn_nv_base.qdata.numel() == 0 + assert qdata_freed, ( + "marlin build did not free qdata (nv.qdata still has data); " + "the +12GB double-copy regression guard cannot verify the fix. " + f"gu_nv.qdata.numel()={gu_nv_base.qdata.numel()}, dn_nv.qdata.numel()={dn_nv_base.qdata.numel()}" + ) + + # After qdata freed: resident should be <= (nvfp4_only + marlin_qweight ≈ 2x qdata) + # The delta from (qdata_freed + marlin_qw resident) vs (qdata_only) should be <= 1.3x + # because marlin int32 packs 8 nibbles/word (same size as 4-bit nibbles * 2 = 8-bit word). + mem_delta = mem_after_marlin - mem_nvfp4_only + qdata_bytes # add back freed qdata + ratio = ( + mem_delta / qdata_bytes + ) # ratio of (marlin resident) / (original qdata size) + print( + f"\n[marlin memory] qdata freed={qdata_freed}, ratio={ratio:.3f}x " + f"(mem_delta={mem_delta / 1e9:.2f}GB, qdata={qdata_bytes / 1e9:.2f}GB)" + ) + assert ratio <= 1.3, ( + f"marlin memory overhead {ratio:.3f}x > 1.3x — the double-copy regression may be back. " + f"qdata_bytes={qdata_bytes / 1e9:.2f}GB, marlin_resident_delta={mem_delta / 1e9:.2f}GB" + ) + + +# =========================================================================== +# Perf 4: Transient peak memory guard (grouped fwd+bwd vs bf16 baseline) +# =========================================================================== + + +@pytest.mark.parametrize( + "shape_fn,shape_name,vram_needed", + [ + (_dsv4_cfg, "dsv4", _VRAM_DSV4_GB), + (_gemma4_cfg, "gemma4", _VRAM_GEMMA4_GB), + ], + ids=["dsv4", "gemma4"], +) +def test_grouped_fp4_peak_memory(shape_fn, shape_name, vram_needed): + """Grouped fwd+bwd peak transient memory <= bf16-dequant-on-fwd baseline peak. + + The grouped/marlin path chunks the expert dequant (CHUNK_E=16 experts at a time) so the + transient bf16 weight materialization is bounded, unlike the naive full-E dequant. + + Measured on RTX PRO 6000 (sm120): + DSV4 E=256: grouped peak ~0.87x vs bf16 full-E-dequant baseline + gemma4 E=128: grouped peak ~0.92x vs bf16 full-E-dequant baseline + + Threshold: grouped peak <= 1.05x baseline (allows 5% noise; the chunked path should be <= 1.0x). + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.dequant_grouped import ( + nvfp4_dequant_bf16, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_available, + grouped_fp4_moe_train, + ) + + if not grouped_fp4_available("nvfp4"): + pytest.skip("no grouped fp4 backend available") + + cfg, fallback = shape_fn() + if fallback: + pytest.skip(f"{shape_name}: insufficient VRAM (need {vram_needed:.0f} GB free)") + + ( + gu_nv, + dn_nv, + gu_nv_base, + dn_nv_base, + pt, + hidden, + idx, + wts, + (Agu, Bgu, Adn, Bdn), + ) = _build_tensors(cfg) + s = cfg["scaling"] + act_type, limit = cfg["act_type"], cfg["limit"] + + # Grouped path step + cache = {} + + def grouped_step(): + for p in (Agu, Bgu, Adn, Bdn): + p.grad = None + hk = hidden.detach().requires_grad_() + out = grouped_fp4_moe_train( + hk, + idx, + wts, + gu_nv, + dn_nv, + (Agu, Bgu, s), + (Adn, Bdn, s), + limit, + "nvfp4", + act_type=act_type, + mxfp4_cache=cache, + ) + out.float().pow(2).mean().backward() + + # Baseline path: full-E bf16 dequant per step (naive worst-case transient). + # Uses the separate NV tensors (not freed by marlin path). + def baseline_step(): + for p in (Agu, Bgu, Adn, Bdn): + p.grad = None + hk = hidden.detach().requires_grad_() + # Dequant all experts to bf16 (per step, no cache) — measures the full transient + Wgu_fresh = nvfp4_dequant_bf16(gu_nv_base.qdata, gu_nv_base.scale, pt) + dummy = hk @ Wgu_fresh[0].T # force bf16 weight peak + dummy.sum().backward() + del Wgu_fresh + + peak_grouped = _peak_mb(grouped_step) + peak_baseline = _peak_mb(baseline_step) + + ratio = peak_grouped / ( + peak_baseline + 1.0 + ) # +1MB to avoid div-by-zero at tiny shapes + print( + f"\n[{shape_name}] peak: grouped={peak_grouped:.0f}MB baseline={peak_baseline:.0f}MB ratio={ratio:.3f}x" + ) + + # The grouped path uses chunked dequant so its peak should be <= baseline (which holds the full + # bf16 weight resident). Allow 1.05x for measurement noise. + # If the shape is too small, the baseline peak can be tiny and the ratio meaningless; skip then. + if peak_baseline < 50.0: + pytest.skip( + f"{shape_name}: baseline peak {peak_baseline:.0f}MB too small to measure ratio reliably" + ) + + assert ratio <= 1.05, ( + f"{shape_name}: grouped peak {peak_grouped:.0f}MB is {ratio:.3f}x baseline {peak_baseline:.0f}MB " + f"(threshold <= 1.05x). Possible transient memory regression." + ) + + +# =========================================================================== +# Sanity: grouped forward produces finite output (fast smoke, runs at small E) +# =========================================================================== + + +@pytest.mark.parametrize( + "cfg,shape_name", + [ + ( + dict( + E=8, + H=4096, + I=2048, + topk=6, + N=32, + r=8, + act_type="silu", + limit=7.0, + scaling=2.0, + ), + "dsv4_small", + ), + ( + dict( + E=8, + H=2816, + I=704, + topk=8, + N=32, + r=8, + act_type="gelu_tanh", + limit=1e30, + scaling=2.0, + ), + "gemma4_small", + ), + ], + ids=["dsv4_small", "gemma4_small"], +) +def test_grouped_fp4_perf_smoke(cfg, shape_name): + """Quick smoke: grouped forward produces finite output at small E (always runs on Blackwell).""" + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_available, + grouped_fp4_moe_train, + ) + + if not grouped_fp4_available("nvfp4"): + pytest.skip("no grouped fp4 backend available") + + (gu_nv, dn_nv, _, _, _, hidden, idx, wts, (Agu, Bgu, Adn, Bdn)) = _build_tensors( + cfg + ) + s = cfg["scaling"] + cache = {} + out = grouped_fp4_moe_train( + hidden.detach(), + idx, + wts, + gu_nv, + dn_nv, + (Agu.detach(), Bgu.detach(), s), + (Adn.detach(), Bdn.detach(), s), + cfg["limit"], + "nvfp4", + act_type=cfg["act_type"], + mxfp4_cache=cache, + ) + assert torch.isfinite(out).all(), f"{shape_name}: forward output has NaN/inf" + assert out.shape == (cfg["N"], cfg["H"]) diff --git a/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_skew.py b/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_skew.py new file mode 100644 index 0000000000..687233a006 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_skew.py @@ -0,0 +1,283 @@ +"""Skew-robustness tests for the grouped/marlin NVFP4 LoRA path. + +The grouped LoRA op must be correct AND memory-bounded for ANY routing distribution — balanced, +power-law, or pathological all-to-one. A capacity-padded implementation (cap = max per-expert +tokens) blows up under skew (peak ~ E * max_cap * dim instead of ~ routed_tokens * dim) and OOMs at +all_to_one; the robust implementation (ragged scatter / bulk-bmm+tail-scatter) does not. + +Verifiable (deterministic) guards live here: + - correctness across skew (cosine vs bf16 oracle) — CI-safe + - memory bound: peak transient <= C * routed_tokens * dim — catches the capacity blowup + - no-OOM under a simulated GPU memory cap at all_to_one +Speed is ratio-based and marked `perf` (noisy). +""" + +from __future__ import annotations + +import pytest +import torch + +# reuse the validated helpers from the sibling correctness module +from .test_grouped_fp4_train import ( + DEV, + _bf16_oracle, + _cos, + quantize_nvfp4, +) + +_BLACKWELL = torch.cuda.is_available() and torch.cuda.get_device_capability()[0] in ( + 10, + 12, +) +pytestmark = pytest.mark.skipif( + not _BLACKWELL, reason="grouped fp4 needs sm100/sm120 (Blackwell)" +) + +SKEWS = ["balanced", "moderate", "extreme", "all_to_one"] + +# Correctness uses a modest E (fast). Memory uses real E=128 so a capacity-padded impl's all_to_one +# blowup (E * N * dim, most experts empty-but-padded) dwarfs balanced (~E*(routed/E)*dim) -> ~16x. +_CFG = dict( + name="gemma4", + E=32, + H=2816, + I=704, + topk=8, + N=512, + r=16, + act_type="gelu_tanh", + limit=1e30, + scaling=2.0, +) +_CFG_MEM = dict(_CFG, E=128, N=1024) + + +def make_routing(N: int, E: int, top_k: int, skew: str, seed: int = 0): + """Deterministic (idx[N,top_k], wts[N,top_k]) with a controllable routing skew. + balanced: uniform distinct top_k; moderate: power-law (prob ~ 1/rank); extreme: 90% of tokens + to experts 0..top_k-1; all_to_one: every token -> experts 0..top_k-1 (the adversarial case that + forces cap == N for a capacity-padded impl).""" + g = torch.Generator().manual_seed(seed) + hot = torch.arange(top_k) + rows = [] + if skew == "balanced": + rows = [torch.randperm(E, generator=g)[:top_k] for _ in range(N)] + elif skew == "all_to_one": + rows = [hot.clone() for _ in range(N)] + elif skew == "extreme": + # 90% hot, 10% random + rows = [ + hot.clone() if (i % 10) else torch.randperm(E, generator=g)[:top_k] + for i in range(N) + ] + elif skew == "moderate": + w = 1.0 / torch.arange(1, E + 1).float() # zipf-ish over expert rank + for _ in range(N): + perm = torch.multinomial(w, top_k, replacement=False, generator=g) + rows.append(perm) + else: + raise ValueError(skew) + idx = torch.stack(rows).to(DEV) + wts = ( + torch.softmax(torch.randn(N, top_k, generator=g), -1).to(torch.bfloat16).to(DEV) + ) + return idx, wts + + +def _build(cfg, skew, seed=0): + E, H, I, topk, N, r = cfg["E"], cfg["H"], cfg["I"], cfg["topk"], cfg["N"], cfg["r"] # noqa: E741 + twoI = 2 * I + torch.manual_seed(seed) + Wgu = torch.randn(E, twoI, H, device=DEV, dtype=torch.bfloat16) * 0.04 + Wdn = torch.randn(E, H, I, device=DEV, dtype=torch.bfloat16) * 0.04 + gu_nv, dn_nv = quantize_nvfp4(Wgu), quantize_nvfp4(Wdn) + from axolotl.integrations.kernels.libs.scattermoe_lora.dequant_grouped import ( + nvfp4_dequant_bf16, + ) + + pt = torch.ones(E, device=DEV) + Wgu_b = nvfp4_dequant_bf16(gu_nv.qdata, gu_nv.scale, pt) + Wdn_b = nvfp4_dequant_bf16(dn_nv.qdata, dn_nv.scale, pt) + hidden = torch.randn(N, H, device=DEV, dtype=torch.bfloat16) * 0.5 + idx, wts = make_routing(N, E, topk, skew, seed) + + def mk(*s): + return ( + torch.randn(*s, device=DEV, dtype=torch.bfloat16) * 0.02 + ).requires_grad_(True) + + lora = (mk(r * E, H), mk(twoI, r * E), mk(r * E, I), mk(H, r * E)) + return hidden, idx, wts, gu_nv, dn_nv, Wgu_b, Wdn_b, lora + + +def _run(cfg, gu_nv, dn_nv, hidden, idx, wts, lora, cache): + # cache is a persistent dict (like the per-module mxfp4 cache in real training): the marlin path + # frees qdata after the first call and reads the cache thereafter, so repeated calls on the same + # nv tensors MUST share one cache. + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_moe_train, + ) + + Agu, Bgu, Adn, Bdn = lora + s = cfg["scaling"] + return grouped_fp4_moe_train( + hidden, + idx, + wts, + gu_nv, + dn_nv, + (Agu, Bgu, s), + (Adn, Bdn, s), + cfg["limit"], + "nvfp4", + act_type=cfg["act_type"], + mxfp4_cache=cache, + ) + + +# --------------------------------------------------------------------------- +# 1. Correctness across skew (deterministic, CI-safe) +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("skew", SKEWS) +def test_correctness_across_skew(skew): + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_available, + ) + + if not grouped_fp4_available("nvfp4"): + pytest.skip("no grouped fp4 backend") + hidden, idx, wts, gu_nv, dn_nv, Wgu_b, Wdn_b, lora = _build(_CFG, skew) + Agu, Bgu, Adn, Bdn = lora + out = _run( + _CFG, + gu_nv, + dn_nv, + hidden.clone(), + idx, + wts, + (Agu.detach(), Bgu.detach(), Adn.detach(), Bdn.detach()), + {}, + ) + assert torch.isfinite(out).all(), f"{skew}: non-finite output" + ref = _bf16_oracle( + hidden.clone(), + idx, + wts, + Wgu_b, + Wdn_b, + Agu, + Bgu, + Adn, + Bdn, + _CFG["act_type"], + _CFG["limit"], + _CFG["scaling"], + ) + cos = _cos(out, ref) + assert cos > 0.97, f"{skew}: fwd cosine {cos:.4f} < 0.97" + + +# --------------------------------------------------------------------------- +# 2. Memory bound across skew (THE skew guard) — a robust op's peak must NOT scale with routing +# skew. Measure each skew's peak transient and assert it stays within a small factor of the +# BALANCED peak (scale-robust, no magic absolute). A capacity-padded impl (cap=max) blows up at +# all_to_one (E*N*dim, most experts empty-but-padded) -> many x balanced -> fails here. +# --------------------------------------------------------------------------- +def _peak_transient(cfg, skew): + hidden, idx, wts, gu_nv, dn_nv, _, _, lora = _build(cfg, skew) + det = tuple(p.detach() for p in lora) + cache = {} + o = _run(cfg, gu_nv, dn_nv, hidden.clone(), idx, wts, det, cache) + del o # warmup + build cache + torch.cuda.synchronize() + torch.cuda.empty_cache() + torch.cuda.reset_peak_memory_stats() + base = torch.cuda.memory_allocated() + _run(cfg, gu_nv, dn_nv, hidden.clone(), idx, wts, det, cache) + torch.cuda.synchronize() + return (torch.cuda.max_memory_allocated() - base) / 1e6 + + +def test_memory_bound_across_skew(): + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_available, + ) + + if not grouped_fp4_available("nvfp4"): + pytest.skip("no grouped fp4 backend") + cfg = _CFG_MEM + peaks = {s: _peak_transient(cfg, s) for s in SKEWS} + base = peaks["balanced"] + msg = " | ".join(f"{s}={peaks[s]:.0f}MB" for s in SKEWS) + # robust: skew peak within 1.5x of balanced. capacity-padded all_to_one is many x -> fails. + for s in SKEWS: + assert peaks[s] <= 1.5 * base, f"skew '{s}' transient blowup vs balanced: {msg}" + + +# --------------------------------------------------------------------------- +# 3. No-OOM under a simulated GPU cap at the adversarial all_to_one routing. +# --------------------------------------------------------------------------- +@pytest.mark.perf +def test_no_oom_all_to_one_under_cap(): + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_available, + ) + + if not grouped_fp4_available("nvfp4"): + pytest.skip("no grouped fp4 backend") + total = torch.cuda.get_device_properties(0).total_memory + # cap this process to a fraction that comfortably fits the robust path but not an E*cap blowup + hidden, idx, wts, gu_nv, dn_nv, _, _, lora = _build(_CFG, "all_to_one") + needed = _CFG["N"] * _CFG["topk"] * max(2 * _CFG["I"], _CFG["H"]) * 2 + frac = min( + 0.5, (needed * 12 + 4 * 2**30) / total + ) # robust needs ~12x routed + model; blowup needs ~Ex more + torch.cuda.set_per_process_memory_fraction(frac, 0) + try: + out = _run( + _CFG, + gu_nv, + dn_nv, + hidden.clone(), + idx, + wts, + tuple(p.detach() for p in lora), + {}, + ) + out.float().sum().backward() if out.requires_grad else None + assert torch.isfinite(out).all() + finally: + torch.cuda.set_per_process_memory_fraction(1.0, 0) + + +# --------------------------------------------------------------------------- +# 4. Speed: skew must not catastrophically slow the op vs balanced (ratio-based, noisy). +# --------------------------------------------------------------------------- +@pytest.mark.perf +def test_speed_skew_within_factor_of_balanced(): + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_available, + ) + + if not grouped_fp4_available("nvfp4"): + pytest.skip("no grouped fp4 backend") + + def timed(skew, it=10, wu=4): + hidden, idx, wts, gu_nv, dn_nv, _, _, lora = _build(_CFG, skew) + det = tuple(p.detach() for p in lora) + cache = {} + for _ in range(wu): + _run(_CFG, gu_nv, dn_nv, hidden.clone(), idx, wts, det, cache) + torch.cuda.synchronize() + s = torch.cuda.Event(enable_timing=True) + e = torch.cuda.Event(enable_timing=True) + s.record() + for _ in range(it): + _run(_CFG, gu_nv, dn_nv, hidden.clone(), idx, wts, det, cache) + e.record() + torch.cuda.synchronize() + return s.elapsed_time(e) / it + + tb = timed("balanced") + ts = timed("extreme") + assert ts <= 4.0 * tb, f"extreme-skew step {ts:.1f}ms > 4x balanced {tb:.1f}ms" diff --git a/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_train.py b/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_train.py new file mode 100644 index 0000000000..d7ef61f475 --- /dev/null +++ b/tests/integrations/kernels/scattermoe_lora/test_grouped_fp4_train.py @@ -0,0 +1,906 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Axolotl AI +# Licensed under the Apache License, Version 2.0 + +"""Correctness tests for the grouped/marlin NVFP4 MoE training path. + +Covers: + - grouped_fp4_moe_train fwd+bwd vs a bf16 per-expert oracle (DSV4 and gemma4 shapes) + - Activation type dispatch (_detect_act_type) and wrong-activation detection + - fp8-read dX NaN regression at non-128-aligned K (I=704, the gemma4 intermediate dim) + - marlin->bf16 fused dequant bit-exactness vs nvfp4_dequant_bf16 + - GeGLU / clamped-SwiGLU activation kernel fwd+bwd vs torch reference + +All tests use synthetic NVFP4 weights — no model download required. + +H/I dimensions are kept at real model values (4096/2048 for DSV4, 2816/704 for gemma4) to satisfy +the marlin backend's 16-byte stride alignment requirement for torch._grouped_mm. E and N are small +(8-16 and 32) so the tests run fast (~2-10 s each on RTX PRO 6000). + +Gate: skips unless CUDA is available AND device is Blackwell (sm100 / sm120). +""" + +from __future__ import annotations + +import functools + +import pytest +import torch +import torch.nn.functional as F + +# --------------------------------------------------------------------------- +# Skip gate: Blackwell only (sm100 / sm120) +# --------------------------------------------------------------------------- + +_IS_CUDA = torch.cuda.is_available() +_IS_BLACKWELL = _IS_CUDA and torch.cuda.get_device_capability()[0] in (10, 12) + +pytestmark = pytest.mark.skipif( + not _IS_BLACKWELL, + reason="grouped NVFP4 training backends require Blackwell (sm100/sm120)", +) + +DEV = "cuda" + + +# --------------------------------------------------------------------------- +# Shape families — real H/I to satisfy 16-byte stride alignment in grouped_mm +# --------------------------------------------------------------------------- + +# DSV4-Flash: clamped-SwiGLU, limit=7.0 +_DSV4 = dict( + name="dsv4", + E=8, + H=4096, + I=2048, + topk=6, + N=32, + r=8, + act_type="silu", + limit=7.0, + scaling=2.0, +) + +# gemma4-A4B: gelu_tanh GeGLU, no clamp +_GEMMA4 = dict( + name="gemma4", + E=8, + H=2816, + I=704, + topk=8, + N=32, + r=8, + act_type="gelu_tanh", + limit=1e30, + scaling=2.0, +) + +SHAPES = [_DSV4, _GEMMA4] +SHAPE_IDS = ["dsv4", "gemma4"] + + +# --------------------------------------------------------------------------- +# NVFP4 quantizer — produces a real NVFP4Tensor (needed for the marlin backend +# which frees qdata.data after repacking). Self-contained, no scratch imports. +# --------------------------------------------------------------------------- + + +def _fp4_codebook(device): + from axolotl.integrations.kernels.libs.scattermoe_lora.mx_weights import ( + fp4_codebook, + ) + + return fp4_codebook(device).float() + + +def quantize_nvfp4(W: torch.Tensor): + """Quantize bf16 W[E,N,K] -> NVFP4Tensor matching nvfp4_dequant_bf16's inverse.""" + from torchao.prototype.mx_formats.nvfp4_tensor import NVFP4Tensor + + dev = W.device + cb = _fp4_codebook(dev) + E, N, K = W.shape + Wb = W.reshape(E, N, K // 16, 16).float() + amax = Wb.abs().amax(-1).clamp_min(1e-6) + scale_e4m3 = (amax / 6.0).to(torch.float8_e4m3fn) + scale_f = scale_e4m3.float().clamp_min(1e-9) + nib = ( + ((Wb / scale_f.unsqueeze(-1)).unsqueeze(-1) - cb.view(1, 1, 1, 1, 16)) + .abs() + .argmin(-1) + ) + nib = nib.to(torch.uint8).reshape(E, N, K) + packed = (nib[..., 0::2] & 0xF) | ((nib[..., 1::2] & 0xF) << 4) + return NVFP4Tensor( + packed.contiguous(), + scale_e4m3.contiguous(), + block_size=16, + orig_dtype=torch.bfloat16, + per_tensor_scale=torch.ones((), device=dev), + ) + + +# --------------------------------------------------------------------------- +# bf16 per-expert MoE oracle +# --------------------------------------------------------------------------- + + +def _bf16_oracle( + hidden, idx, wts, Wgu_b, Wdn_b, Agu, Bgu, Adn, Bdn, act_type, limit, scaling +): + """Per-token per-expert bf16 MoE forward (no grouping), matching the kernel's intent. + + hidden [N,H]; idx/wts [N,topk]; Wgu_b/Wdn_b [E,out,in] bf16 dequant; *_lora scattermoe + stacked layout [r*E, K] / [out, r*E]; act_type: 'silu' | 'gelu_tanh'; limit: clamp value. + Returns [N,H] float32 accumulator (autograd-enabled via Agu/Bgu/Adn/Bdn). + """ + N, H = hidden.shape + E = Wgu_b.shape[0] + r = Agu.shape[0] // E + acc = torch.zeros(N, H, device=hidden.device, dtype=torch.float32) + for e in range(E): + sel = idx == e # [N, topk] bool + tok = sel.any(-1) # [N] bool + if not tok.any(): + continue + ti = tok.nonzero(as_tuple=True)[0] + xe = hidden[ti].float() + Ag = Agu[e * r : (e + 1) * r] # [r, H] + Bg = Bgu[:, e * r : (e + 1) * r] # [2I, r] + Ad = Adn[e * r : (e + 1) * r] # [r, I] + Bd = Bdn[:, e * r : (e + 1) * r] # [H, r] + # gate_up: base + LoRA + gu = ( + xe.to(hidden.dtype) @ Wgu_b[e].T + + scaling * (xe.to(hidden.dtype) @ Ag.T) @ Bg.T + ) + # activation + g, u = gu.chunk(2, -1) + if act_type == "gelu_tanh": + h = F.gelu(g.float(), approximate="tanh") * u.float() + else: + h = F.silu(g.float().clamp(max=limit)) * u.float().clamp( + min=-limit, max=limit + ) + h = h.to(hidden.dtype) + # down: base + LoRA + dn = h @ Wdn_b[e].T + scaling * (h @ Ad.T) @ Bd.T + # weighted accumulate + w = (wts * sel)[ti].sum(-1, keepdim=True).float() + acc[ti] += dn.float() * w + return acc + + +def _cos(a: torch.Tensor, b: torch.Tensor) -> float: + return F.cosine_similarity(a.float().flatten(), b.float().flatten(), dim=0).item() + + +# --------------------------------------------------------------------------- +# Shared fixture: build inputs + weights for a shape family +# --------------------------------------------------------------------------- + + +def _make_inputs(cfg: dict, seed: int = 0): + """Return (hidden, idx, wts, gu_nv, dn_nv, Wgu_b, Wdn_b, lora_params) plus fresh NV copies.""" + E, H, I, topk, N, r = cfg["E"], cfg["H"], cfg["I"], cfg["topk"], cfg["N"], cfg["r"] + twoI = 2 * I + torch.manual_seed(seed) + Wgu = torch.randn(E, twoI, H, device=DEV, dtype=torch.bfloat16) * 0.04 + Wdn = torch.randn(E, H, I, device=DEV, dtype=torch.bfloat16) * 0.04 + gu_nv = quantize_nvfp4(Wgu) + dn_nv = quantize_nvfp4(Wdn) + + from axolotl.integrations.kernels.libs.scattermoe_lora.dequant_grouped import ( + nvfp4_dequant_bf16, + ) + + pt = torch.ones(E, device=DEV) + Wgu_b = nvfp4_dequant_bf16(gu_nv.qdata, gu_nv.scale, pt) + Wdn_b = nvfp4_dequant_bf16(dn_nv.qdata, dn_nv.scale, pt) + + hidden = torch.randn(N, H, device=DEV, dtype=torch.bfloat16) * 0.5 + idx = torch.stack([torch.randperm(E, device=DEV)[:topk] for _ in range(N)]) + wts = torch.softmax(torch.randn(N, topk, device=DEV), -1).to(torch.bfloat16) + + def mk(*s): + return ( + torch.randn(*s, device=DEV, dtype=torch.bfloat16) * 0.02 + ).requires_grad_(True) + + Agu = mk(r * E, H) + Bgu = mk(twoI, r * E) + Adn = mk(r * E, I) + Bdn = mk(H, r * E) + + return hidden, idx, wts, gu_nv, dn_nv, Wgu_b, Wdn_b, (Agu, Bgu, Adn, Bdn) + + +# --------------------------------------------------------------------------- +# Fresh NVFP4Tensor helper (marlin frees qdata on first call; tests that need +# multiple independent calls must build separate NVFP4Tensors from the same W) +# --------------------------------------------------------------------------- + + +def _fresh_nv(Wgu, Wdn): + """Build fresh NVFP4Tensor pair from the same weight matrices (for second-call tests).""" + return quantize_nvfp4(Wgu), quantize_nvfp4(Wdn) + + +# =========================================================================== +# Test 1: forward output matches oracle +# =========================================================================== + + +@pytest.mark.parametrize("cfg", SHAPES, ids=SHAPE_IDS) +def test_grouped_fp4_fwd_matches_oracle(cfg): + """Forward output cosine >= 0.97 vs bf16 per-expert oracle, no NaN/inf.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_available, + grouped_fp4_moe_train, + ) + + if not grouped_fp4_available("nvfp4"): + pytest.skip("no grouped fp4 backend available") + + H = cfg["H"] + hidden, idx, wts, gu_nv, dn_nv, Wgu_b, Wdn_b, (Agu, Bgu, Adn, Bdn) = _make_inputs( + cfg + ) + _r, s = cfg["r"], cfg["scaling"] + + cache = {} + out = grouped_fp4_moe_train( + hidden.clone(), + idx, + wts, + gu_nv, + dn_nv, + (Agu.detach(), Bgu.detach(), s), + (Adn.detach(), Bdn.detach(), s), + cfg["limit"], + "nvfp4", + act_type=cfg["act_type"], + mxfp4_cache=cache, + ) + assert torch.isfinite(out).all(), f"{cfg['name']}: fwd output has NaN/inf" + assert out.shape == (cfg["N"], H), f"unexpected output shape {out.shape}" + + ref = _bf16_oracle( + hidden.clone(), + idx, + wts, + Wgu_b, + Wdn_b, + Agu, + Bgu, + Adn, + Bdn, + cfg["act_type"], + cfg["limit"], + s, + ) + cos = _cos(out, ref) + assert cos > 0.97, f"{cfg['name']}: fwd cosine {cos:.4f} < 0.97" + + +# =========================================================================== +# Test 1b: marlin qdata-free experts survive clone/state_dict (adapter save) +# =========================================================================== + + +@pytest.mark.parametrize("cfg", SHAPES, ids=SHAPE_IDS) +def test_grouped_fp4_save_after_qdata_free(cfg): + """Regression: the marlin memory fix frees nv.qdata after repack. The freed NVFP4Tensor must + still clone/serialize (PEFT save_pretrained -> state_dict -> NVFP4Tensor clone -> __new__ -> + qdata.stride(-2)); a flat empty(0) qdata crashed with IndexError(dim -2). Freeing to a 3-D + [E, N, 0] placeholder keeps clone/save working.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + _train_backend, + grouped_fp4_available, + grouped_fp4_moe_train, + ) + + if not grouped_fp4_available("nvfp4"): + pytest.skip("no grouped fp4 backend available") + if _train_backend("nvfp4") != "marlin": + pytest.skip("qdata-free only on the marlin backend") + + hidden, idx, wts, gu_nv, dn_nv, _Wgu, _Wdn, (Agu, Bgu, Adn, Bdn) = _make_inputs(cfg) + s = cfg["scaling"] + # one forward triggers the marlin repack + qdata free + grouped_fp4_moe_train( + hidden.clone(), + idx, + wts, + gu_nv, + dn_nv, + (Agu.detach(), Bgu.detach(), s), + (Adn.detach(), Bdn.detach(), s), + cfg["limit"], + "nvfp4", + act_type=cfg["act_type"], + mxfp4_cache={}, + ) + # qdata was freed to a 3-D zero-element placeholder (not flat empty(0)) + assert gu_nv.qdata.numel() == 0 and gu_nv.qdata.ndim == 3, ( + "qdata not freed to 3-D placeholder" + ) + # The freed NVFP4Tensor must CLONE / round-trip through state_dict without raising — this is the + # save path (PEFT save_pretrained -> state_dict -> NVFP4Tensor clone). A flat empty(0) qdata + # crashed here with IndexError(dim -2). We don't assert the frozen expert's clone *shape* (it's + # degenerate by design and filtered out of the LoRA adapter); only that save doesn't crash. + import torch.nn as nn + + for nv in (gu_nv, dn_nv): + nv.clone() # would IndexError(dim -2) on a flat empty(0) qdata + m = nn.Module() + m.w = nn.Parameter(nv, requires_grad=False) + sd = m.state_dict() # invokes _save_to_state_dict -> clone on the NVFP4Tensor + assert "w" in sd + + +# =========================================================================== +# Test 2: backward grads match oracle +# =========================================================================== + + +@pytest.mark.parametrize("cfg", SHAPES, ids=SHAPE_IDS) +def test_grouped_fp4_bwd_matches_oracle(cfg): + """Backward grads (d_hidden, d_LoRA_A_gu, d_LoRA_A_dn) cosine >= 0.95, finite.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.dequant_grouped import ( + nvfp4_dequant_bf16, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_available, + grouped_fp4_moe_train, + ) + + if not grouped_fp4_available("nvfp4"): + pytest.skip("no grouped fp4 backend available") + + E, H, I = cfg["E"], cfg["H"], cfg["I"] + hidden, idx, wts, gu_nv, dn_nv, Wgu_b, Wdn_b, (Agu, Bgu, Adn, Bdn) = _make_inputs( + cfg + ) + _r, s = cfg["r"], cfg["scaling"] + + # Kernel path — use fresh NV tensors for backward (marlin frees qdata on first build) + twoI = 2 * I + torch.manual_seed(0) + Wgu = torch.randn(E, twoI, H, device=DEV, dtype=torch.bfloat16) * 0.04 + Wdn = torch.randn(E, H, I, device=DEV, dtype=torch.bfloat16) * 0.04 + gu_nv_k, dn_nv_k = _fresh_nv(Wgu, Wdn) + pt = torch.ones(E, device=DEV) + Wgu_b_k = nvfp4_dequant_bf16(gu_nv_k.qdata, gu_nv_k.scale, pt) + Wdn_b_k = nvfp4_dequant_bf16(dn_nv_k.qdata, dn_nv_k.scale, pt) + + hidden2 = hidden.clone() + idx2 = idx.clone() + wts2 = wts.clone() + Agk = Agu.detach().clone().requires_grad_(True) + Bgk = Bgu.detach().clone().requires_grad_(True) + Adk = Adn.detach().clone().requires_grad_(True) + Bdk = Bdn.detach().clone().requires_grad_(True) + hk = hidden2.requires_grad_(True) + + cache = {} + out = grouped_fp4_moe_train( + hk, + idx2, + wts2, + gu_nv_k, + dn_nv_k, + (Agk, Bgk, s), + (Adk, Bdk, s), + cfg["limit"], + "nvfp4", + act_type=cfg["act_type"], + mxfp4_cache=cache, + ) + out.float().pow(2).mean().backward() + + # Oracle path — same weights + Agr = Agk.detach().clone().requires_grad_(True) + Bgr = Bgk.detach().clone().requires_grad_(True) + Adr = Adk.detach().clone().requires_grad_(True) + Bdr = Bdk.detach().clone().requires_grad_(True) + hr = hidden2.detach().clone().requires_grad_(True) + + ref = _bf16_oracle( + hr, + idx2, + wts2, + Wgu_b_k, + Wdn_b_k, + Agr, + Bgr, + Adr, + Bdr, + cfg["act_type"], + cfg["limit"], + s, + ) + ref.pow(2).mean().backward() + + for name, gk, gr in [ + ("d_hidden", hk.grad, hr.grad), + ("d_Agu", Agk.grad, Agr.grad), + ("d_Adn", Adk.grad, Adr.grad), + ]: + assert gk is not None, ( + f"{cfg['name']}: {name} grad is None (backward didn't run)" + ) + assert gr is not None, f"{cfg['name']}: oracle {name} grad is None" + assert torch.isfinite(gk).all(), f"{cfg['name']}: {name} has NaN/inf" + cos = _cos(gk, gr) + assert cos > 0.95, f"{cfg['name']}: {name} cosine {cos:.4f} < 0.95" + + +# =========================================================================== +# Test 3: activation type dispatch +# =========================================================================== + + +def test_detect_act_type_dispatch(): + """_detect_act_type must return 'gelu_tanh' for gemma4-style and 'silu' for DSV4.""" + from axolotl.integrations.kernels.libs.scattermoe_lora.experts import ( + _detect_act_type, + ) + + class FakeGemma4: + act_fn = functools.partial(F.gelu, approximate="tanh") + + class FakeDSV4: + act_fn = F.silu + limit = 7.0 + + class FakeNamedGeLU: + class _GeLU: + __name__ = "gelu_pytorch_tanh" + + def __call__(self, x): + return F.gelu(x, approximate="tanh") + + act_fn = _GeLU() + + assert _detect_act_type(FakeGemma4()) == "gelu_tanh", ( + "gemma4 (partial F.gelu tanh) must be 'gelu_tanh'" + ) + assert _detect_act_type(FakeDSV4()) == "silu", "dsv4 (F.silu) must be 'silu'" + assert _detect_act_type(FakeNamedGeLU()) == "gelu_tanh", ( + "named gelu_pytorch_tanh must be 'gelu_tanh'" + ) + + +def test_wrong_activation_fails_gemma4(): + """Using act_type='silu' on a gemma4 shape (correct: 'gelu_tanh') degrades cosine vs oracle. + + Demonstrates TEETH: the correct activation (gelu_tanh) achieves cosine >= 0.999 vs the + correct oracle, while the wrong activation (silu) achieves only ~0.992. The gap is large + enough to be a reliable regression detector. + + Note: absolute cosine with wrong-act vs correct-oracle stays ~0.99 (gelu_tanh ≈ silu for + moderate values), so the test checks the *relative gap* rather than an absolute threshold. + """ + from axolotl.integrations.kernels.libs.scattermoe_lora.dequant_grouped import ( + nvfp4_dequant_bf16, + ) + from axolotl.integrations.kernels.libs.scattermoe_lora.grouped_train import ( + grouped_fp4_available, + grouped_fp4_moe_train, + ) + + if not grouped_fp4_available("nvfp4"): + pytest.skip("no grouped fp4 backend available") + + cfg = _GEMMA4 + E, H, I, topk, N, r = cfg["E"], cfg["H"], cfg["I"], cfg["topk"], cfg["N"], cfg["r"] + twoI = 2 * I + torch.manual_seed(99) + Wgu = torch.randn(E, twoI, H, device=DEV, dtype=torch.bfloat16) * 0.04 + Wdn = torch.randn(E, H, I, device=DEV, dtype=torch.bfloat16) * 0.04 + hidden = torch.randn(N, H, device=DEV, dtype=torch.bfloat16) * 0.5 + idx = torch.stack([torch.randperm(E, device=DEV)[:topk] for _ in range(N)]) + wts = torch.softmax(torch.randn(N, topk, device=DEV), -1).to(torch.bfloat16) + Agu = torch.randn(r * E, H, device=DEV, dtype=torch.bfloat16) * 0.02 + Bgu = torch.randn(twoI, r * E, device=DEV, dtype=torch.bfloat16) * 0.02 + Adn = torch.randn(r * E, I, device=DEV, dtype=torch.bfloat16) * 0.02 + Bdn = torch.randn(H, r * E, device=DEV, dtype=torch.bfloat16) * 0.02 + + pt = torch.ones(E, device=DEV) + + # Correct-act run + gu_nv_c, dn_nv_c = _fresh_nv(Wgu, Wdn) + Wgu_b = nvfp4_dequant_bf16(gu_nv_c.qdata, gu_nv_c.scale, pt) + Wdn_b = nvfp4_dequant_bf16(dn_nv_c.qdata, dn_nv_c.scale, pt) + cache_c = {} + out_correct = grouped_fp4_moe_train( + hidden, + idx, + wts, + gu_nv_c, + dn_nv_c, + (Agu, Bgu, cfg["scaling"]), + (Adn, Bdn, cfg["scaling"]), + cfg["limit"], + "nvfp4", + act_type="gelu_tanh", + mxfp4_cache=cache_c, + ) + + # Wrong-act run (fresh NV tensors; marlin freed qdata in first call) + gu_nv_w, dn_nv_w = _fresh_nv(Wgu, Wdn) + cache_w = {} + out_wrong = grouped_fp4_moe_train( + hidden, + idx, + wts, + gu_nv_w, + dn_nv_w, + (Agu, Bgu, cfg["scaling"]), + (Adn, Bdn, cfg["scaling"]), + cfg["limit"], + "nvfp4", + act_type="silu", # deliberate wrong activation + mxfp4_cache=cache_w, + ) + + # Correct-activation oracle + correct_ref = _bf16_oracle( + hidden, + idx, + wts, + Wgu_b, + Wdn_b, + Agu, + Bgu, + Adn, + Bdn, + "gelu_tanh", + cfg["limit"], + cfg["scaling"], + ) + + cos_correct = _cos(out_correct, correct_ref) + cos_wrong = _cos(out_wrong, correct_ref) + + # The correct activation must match its oracle very well + assert cos_correct > 0.99, ( + f"correct-act kernel cosine vs oracle {cos_correct:.4f} < 0.99 — something is wrong" + ) + # The wrong activation must diverge detectably: gap >= 0.005 (measured ~0.007-0.008) + gap = cos_correct - cos_wrong + assert gap > 0.005, ( + f"TEETH CHECK FAILED: wrong activation (silu on gemma4) barely diverges from correct " + f"(correct={cos_correct:.4f}, wrong={cos_wrong:.4f}, gap={gap:.5f} < 0.005). " + "The activation correctness test lacks discriminating power." + ) + + +# =========================================================================== +# Test 4: NaN regression at non-128-aligned K (I=704, fp8-read dX) +# =========================================================================== + + +def test_grouped_dx_fp8_nan_regression_k704(): + """grouped_dx_fp8 must produce finite output for K=704 (non-BK-aligned), the gemma4 I dim. + + Original bug: the K-tile boundary had no mask, so the last tile read OOB fp8 values that + decoded to NaN in the accumulator. Fixed by adding mk=rk